mirror of
https://github.com/elasota/Aerofoil.git
synced 2025-09-23 14:53:52 +00:00
303 lines
7.6 KiB
C++
303 lines
7.6 KiB
C++
#include "BinHex4.h"
|
|
#include "IOStream.h"
|
|
|
|
#include <string.h>
|
|
#include <vector>
|
|
#include <assert.h>
|
|
|
|
// See: https://files.stairways.com/other/binhex-40-specs-info.txt
|
|
// Unfortunately, while the spec specifies that decoding is to be done high-to-low,
|
|
// it doesn't specify how the encoded 6-bit value is split.
|
|
|
|
#include "MacFileInfo.h"
|
|
#include "ByteUnpack.h"
|
|
#include "XModemCRC.h"
|
|
#include "MacFileMem.h"
|
|
|
|
namespace
|
|
{
|
|
static bool IsEOL(char c)
|
|
{
|
|
return c == '\r' || c == '\n';
|
|
}
|
|
|
|
static bool IsWhitespaceChar(char c)
|
|
{
|
|
return c == '\r' || c == '\n' || c == ' ' || c == '\t';
|
|
}
|
|
|
|
uint16_t BinHexCRCNoPadding(const uint8_t *bytes, size_t size, int initialValue)
|
|
{
|
|
uint16_t crc = initialValue;
|
|
for (size_t b = 0; b < size; b++)
|
|
{
|
|
uint8_t v = bytes[b];
|
|
|
|
for (int i = 0; i < 8; i++)
|
|
{
|
|
int temp = (crc & 0x8000);
|
|
crc = (crc << 1) | (v >> 7);
|
|
|
|
if (temp)
|
|
crc = crc ^ 0x1021;
|
|
|
|
v = (v << 1) & 0xff;
|
|
}
|
|
}
|
|
|
|
return static_cast<uint16_t>(crc);
|
|
}
|
|
|
|
uint16_t BinHexCRC(const uint8_t *bytes, size_t size)
|
|
{
|
|
const uint8_t zeroBytes[] = { 0, 0 };
|
|
|
|
uint16_t crc = BinHexCRCNoPadding(bytes, size, 0);
|
|
return BinHexCRCNoPadding(zeroBytes, 2, crc);
|
|
}
|
|
}
|
|
|
|
namespace PortabilityLayer
|
|
{
|
|
MacFileMem *BinHex4::LoadHQX(IOStream *stream)
|
|
{
|
|
const uint8_t errCodeChar = 64;
|
|
|
|
uint8_t charMap[128];
|
|
for (int i = 0; i < 128; i++)
|
|
charMap[i] = errCodeChar;
|
|
|
|
const char binHexCharacters[] = "!\"#$%&'()*+,-012345689@ABCDEFGHIJKLMNPQRSTUVXYZ[`abcdefhijklmpqr";
|
|
|
|
for (int i = 0; i < 64; i++)
|
|
charMap[binHexCharacters[i]] = static_cast<uint8_t>(i);
|
|
|
|
const char expectedPrefix[] = "(This file must be converted with BinHex";
|
|
const size_t prefixSizeChars = sizeof(expectedPrefix) - 1;
|
|
char prefix[prefixSizeChars];
|
|
|
|
if (stream->Read(prefix, prefixSizeChars) != prefixSizeChars)
|
|
return nullptr;
|
|
|
|
if (memcmp(prefix, expectedPrefix, prefixSizeChars))
|
|
return nullptr;
|
|
|
|
// Find end char
|
|
for (;;)
|
|
{
|
|
char nextChar;
|
|
if (!stream->Read(&nextChar, 1))
|
|
return nullptr;
|
|
|
|
if (IsEOL(nextChar))
|
|
break;
|
|
}
|
|
|
|
// Find start colon
|
|
for (;;)
|
|
{
|
|
char nextChar;
|
|
if (!stream->Read(&nextChar, 1))
|
|
return nullptr;
|
|
|
|
if (IsWhitespaceChar(nextChar))
|
|
continue;
|
|
else if (nextChar == ':')
|
|
break;
|
|
else
|
|
return nullptr;
|
|
}
|
|
|
|
std::vector<uint8_t> bytesAfter6To8;
|
|
|
|
if (stream->IsSeekable())
|
|
{
|
|
UFilePos_t filePos = stream->Tell();
|
|
if (stream->SeekEnd(0))
|
|
{
|
|
UFilePos_t endPos = stream->Tell();
|
|
if (!stream->SeekStart(filePos))
|
|
return nullptr;
|
|
|
|
if (endPos > filePos && (endPos - filePos) < SIZE_MAX / 6)
|
|
bytesAfter6To8.reserve(static_cast<size_t>(endPos - filePos) * 6 / 8);
|
|
}
|
|
}
|
|
|
|
// Undo 8-to-6 coding
|
|
const size_t bufferCapacity = 128;
|
|
size_t bufferReadPos = 0;
|
|
size_t bufferSize = 0;
|
|
char buffer[bufferCapacity];
|
|
|
|
bool isEOF = false;
|
|
|
|
int decodedByte = 0;
|
|
int decodedByteBitPos = 8;
|
|
|
|
for (;;)
|
|
{
|
|
if (bufferReadPos == bufferSize)
|
|
{
|
|
const size_t numRead = stream->Read(buffer, bufferCapacity);
|
|
if (numRead == 0)
|
|
return nullptr; // Missing terminator
|
|
|
|
bufferSize = numRead;
|
|
bufferReadPos = 0;
|
|
}
|
|
|
|
char nextChar = buffer[bufferReadPos++];
|
|
|
|
if (nextChar == ':')
|
|
break;
|
|
|
|
if (IsWhitespaceChar(nextChar))
|
|
continue;
|
|
|
|
if (nextChar < 0 || nextChar > 127)
|
|
return nullptr;
|
|
|
|
uint8_t value6Bit = charMap[nextChar];
|
|
if (value6Bit == errCodeChar)
|
|
return nullptr;
|
|
|
|
switch (decodedByteBitPos)
|
|
{
|
|
case 8:
|
|
decodedByte = value6Bit << 2;
|
|
decodedByteBitPos = 2;
|
|
break;
|
|
case 6:
|
|
decodedByte |= value6Bit;
|
|
bytesAfter6To8.push_back(decodedByte);
|
|
decodedByte = 0;
|
|
decodedByteBitPos = 8;
|
|
break;
|
|
case 4:
|
|
decodedByte |= (value6Bit >> 2);
|
|
bytesAfter6To8.push_back(decodedByte);
|
|
decodedByte = (value6Bit << 6) & 0xff;
|
|
decodedByteBitPos = 6;
|
|
break;
|
|
case 2:
|
|
decodedByte |= (value6Bit >> 4);
|
|
bytesAfter6To8.push_back(decodedByte);
|
|
decodedByte = (value6Bit << 4) & 0xff;
|
|
decodedByteBitPos = 4;
|
|
break;
|
|
default:
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
const size_t bytesBeforeRLEDec = bytesAfter6To8.size();
|
|
size_t decodedDataSize = 0;
|
|
for (size_t i = 0; i < bytesBeforeRLEDec; i++)
|
|
{
|
|
const uint8_t b = bytesAfter6To8[i];
|
|
if (b == 0x90)
|
|
{
|
|
if (i == bytesBeforeRLEDec - 1)
|
|
return nullptr;
|
|
|
|
const uint8_t runLength = bytesAfter6To8[++i];
|
|
|
|
if (runLength == 0)
|
|
decodedDataSize++; // 0x90 literal
|
|
else
|
|
decodedDataSize += runLength - 1; // RLE, runs of length 1 are permitted
|
|
}
|
|
else
|
|
decodedDataSize++;
|
|
}
|
|
|
|
std::vector<uint8_t> decodedBytes;
|
|
decodedBytes.reserve(decodedDataSize);
|
|
|
|
for (size_t i = 0; i < bytesBeforeRLEDec; i++)
|
|
{
|
|
const uint8_t b = bytesAfter6To8[i];
|
|
|
|
if (b == 0x90)
|
|
{
|
|
const uint8_t runLength = bytesAfter6To8[++i];
|
|
|
|
if (runLength == 0)
|
|
decodedBytes.push_back(0x90);
|
|
else
|
|
{
|
|
if (decodedBytes.size() == 0)
|
|
return nullptr;
|
|
|
|
const uint8_t lastByte = *(decodedBytes.end() - 1);
|
|
for (size_t r = 1; r < runLength; r++)
|
|
decodedBytes.push_back(lastByte);
|
|
}
|
|
}
|
|
else
|
|
decodedBytes.push_back(b);
|
|
}
|
|
|
|
assert(decodedBytes.size() == decodedDataSize);
|
|
|
|
if (decodedBytes.size() == 0)
|
|
return nullptr;
|
|
|
|
const uint8_t nameLength = decodedBytes[0];
|
|
if (decodedBytes.size() < 22 + nameLength || nameLength > 63)
|
|
return nullptr;
|
|
|
|
// Header format:
|
|
// uint8_t nameLength
|
|
// char name[nameLength]
|
|
// char <null>
|
|
// char fileType[4]
|
|
// char fileCreator[4]
|
|
// word flags
|
|
// dword dataLength
|
|
// dword resourceLength
|
|
// word headerCRC
|
|
|
|
const size_t headerStartLoc = 2 + nameLength;
|
|
|
|
if (decodedBytes[nameLength + 1] != 0)
|
|
return nullptr;
|
|
|
|
MacFileInfo mfi;
|
|
mfi.m_fileName.Set(nameLength, reinterpret_cast<const char*>(&decodedBytes[1]));
|
|
memcpy(mfi.m_properties.m_fileType, &decodedBytes[headerStartLoc + 0], 4);
|
|
memcpy(mfi.m_properties.m_fileCreator, &decodedBytes[headerStartLoc + 4], 4);
|
|
mfi.m_properties.m_finderFlags = ByteUnpack::BigUInt16(&decodedBytes[headerStartLoc + 8]);
|
|
mfi.m_dataForkSize = ByteUnpack::BigUInt32(&decodedBytes[headerStartLoc + 10]);
|
|
mfi.m_resourceForkSize = ByteUnpack::BigUInt32(&decodedBytes[headerStartLoc + 14]);
|
|
|
|
const size_t availableDataSize = decodedBytes.size() - 26 - nameLength; // +4 bytes for CRCs
|
|
|
|
if (mfi.m_dataForkSize > availableDataSize || availableDataSize - mfi.m_dataForkSize < mfi.m_resourceForkSize)
|
|
return nullptr;
|
|
|
|
const uint16_t expectedHeaderCRC = ByteUnpack::BigUInt16(&decodedBytes[headerStartLoc + 18]);
|
|
const uint16_t actualHeaderCRC = BinHexCRC(&decodedBytes[0], headerStartLoc + 18);
|
|
|
|
if (expectedHeaderCRC != actualHeaderCRC)
|
|
return nullptr;
|
|
|
|
const size_t dataForkStart = headerStartLoc + 20;
|
|
const size_t dataForkCRCLoc = dataForkStart + mfi.m_dataForkSize;
|
|
const size_t resourceForkStart = dataForkCRCLoc + 2;
|
|
const size_t resourceForkCRCLoc = resourceForkStart + mfi.m_resourceForkSize;
|
|
|
|
const uint16_t expectedDataCRC = ByteUnpack::BigUInt16(&decodedBytes[dataForkCRCLoc]);
|
|
const uint16_t expectedResCRC = ByteUnpack::BigUInt16(&decodedBytes[resourceForkCRCLoc]);
|
|
|
|
if (expectedDataCRC != BinHexCRC(&decodedBytes[dataForkStart], mfi.m_dataForkSize))
|
|
return false;
|
|
|
|
if (expectedResCRC != BinHexCRC(&decodedBytes[resourceForkStart], mfi.m_resourceForkSize))
|
|
return false;
|
|
|
|
return new MacFileMem(&decodedBytes[dataForkStart], &decodedBytes[resourceForkStart], nullptr, mfi);
|
|
}
|
|
}
|