Files
Aerofoil/PortabilityLayer/BinHex4.cpp
2019-11-11 00:11:59 -05:00

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);
}
}