mirror of
https://github.com/aaru-dps/Aaru.git
synced 2026-07-08 17:56:18 +00:00
289 lines
9.7 KiB
C#
289 lines
9.7 KiB
C#
using System;
|
|
using System.Text;
|
|
|
|
namespace Aaru.Archives;
|
|
|
|
public sealed partial class Ace
|
|
{
|
|
// =====================================================================
|
|
// Comment decompression — ACE uses a custom LZP + Huffman scheme.
|
|
// =====================================================================
|
|
|
|
/// <summary>Maximum Huffman code width for main symbol table.</summary>
|
|
const int COMMENT_MAX_CODE_WIDTH = 11;
|
|
/// <summary>Maximum Huffman code width for the precode (width-of-widths) table.</summary>
|
|
const int COMMENT_MAX_PRECODE_WIDTH = 7;
|
|
/// <summary>Maximum number of main symbols: 260 + MAX_DIC_BITS(22) + 2 = 284.</summary>
|
|
const int COMMENT_MAX_MAIN_CODE = 284;
|
|
/// <summary>Maximum precode symbol count.</summary>
|
|
const int COMMENT_MAX_PRECODE = 15;
|
|
/// <summary>Precomputed CRC32 lookup table.</summary>
|
|
static readonly uint[] _crcTable = BuildCrcTable();
|
|
|
|
static uint[] BuildCrcTable()
|
|
{
|
|
var table = new uint[256];
|
|
|
|
for(uint i = 0; i < 256; i++)
|
|
{
|
|
uint r = i;
|
|
|
|
for(var j = 0; j < 8; j++) r = (r & 1) != 0 ? r >> 1 ^ CRC_POLY : r >> 1;
|
|
|
|
table[i] = r;
|
|
}
|
|
|
|
return table;
|
|
}
|
|
|
|
/// <summary>Compute CRC32 over a byte span.</summary>
|
|
static uint ComputeCrc(uint crc, ReadOnlySpan<byte> data)
|
|
{
|
|
foreach(byte b in data) crc = _crcTable[(byte)(crc ^ b)] ^ crc >> 8;
|
|
|
|
return crc;
|
|
}
|
|
|
|
/// <summary>Compute CRC16 (lower 16 bits of CRC32) for header validation.</summary>
|
|
static ushort ComputeHeaderCrc(ReadOnlySpan<byte> headerData) => (ushort)ComputeCrc(CRC_MASK, headerData);
|
|
|
|
/// <summary>Convert MS-DOS date/time to DateTime.</summary>
|
|
static DateTime DosToDateTime(uint dosDateTime)
|
|
{
|
|
int second = (int)(dosDateTime & 0x1F) * 2;
|
|
var minute = (int)(dosDateTime >> 5 & 0x3F);
|
|
var hour = (int)(dosDateTime >> 11 & 0x1F);
|
|
var day = (int)(dosDateTime >> 16 & 0x1F);
|
|
var month = (int)(dosDateTime >> 21 & 0x0F);
|
|
int year = (int)(dosDateTime >> 25 & 0x7F) + 1980;
|
|
|
|
if(day < 1) day = 1;
|
|
if(month < 1) month = 1;
|
|
if(month > 12) month = 12;
|
|
|
|
int maxDay = DateTime.DaysInMonth(year, month);
|
|
|
|
if(day > maxDay) day = maxDay;
|
|
if(hour > 23) hour = 23;
|
|
if(minute > 59) minute = 59;
|
|
if(second > 59) second = 59;
|
|
|
|
return new DateTime(year, month, day, hour, minute, second);
|
|
}
|
|
|
|
/// <summary>Decompress an ACE comment from its compressed form.</summary>
|
|
/// <returns>The decompressed comment string, or null on failure.</returns>
|
|
static string DecompressComment(byte[] compressedData)
|
|
{
|
|
if(compressedData is null || compressedData.Length < 4) return null;
|
|
|
|
// Initialize bitstream reader state
|
|
var bitPos = 0; // bit position within current 32-bit word
|
|
var wordIndex = 0; // index into uint array
|
|
|
|
// Copy compressed data into uint array (LE words)
|
|
int wordCount = (compressedData.Length + 3) / 4;
|
|
var words = new uint[wordCount + 1]; // +1 for safe lookahead
|
|
|
|
for(var i = 0; i < compressedData.Length; i += 4)
|
|
{
|
|
uint w = 0;
|
|
int remaining = compressedData.Length - i;
|
|
|
|
if(remaining >= 4)
|
|
w = BitConverter.ToUInt32(compressedData, i);
|
|
else
|
|
{
|
|
for(var j = 0; j < remaining; j++) w |= (uint)compressedData[i + j] << j * 8;
|
|
}
|
|
|
|
words[i / 4] = w;
|
|
}
|
|
|
|
// Current 32-bit code register (MSB-first extraction)
|
|
uint code = words[0];
|
|
|
|
// ---- Helper: read N bits from the bitstream (MSB-first) ----
|
|
uint ReadBits(int bits)
|
|
{
|
|
uint result = code >> 32 - bits;
|
|
|
|
bitPos += bits;
|
|
|
|
if(bitPos >= 32)
|
|
{
|
|
bitPos -= 32;
|
|
wordIndex++;
|
|
}
|
|
|
|
// Reload code register
|
|
uint b0 = wordIndex < words.Length ? words[wordIndex] : 0;
|
|
uint b1 = wordIndex + 1 < words.Length ? words[wordIndex + 1] : 0;
|
|
|
|
code = (b0 << bitPos) + (b1 >> 32 - bitPos & (bitPos != 0 ? 0xFFFFFFFF : 0));
|
|
|
|
return result;
|
|
}
|
|
|
|
// ---- Build lookup table from code widths ----
|
|
bool BuildTable(ushort[] widths, int tabSize, int maxWidth, ushort[] symbols)
|
|
{
|
|
// Sort symbols by width using counting sort
|
|
int maxCodePos = 1 << maxWidth;
|
|
var sortedSyms = new int[tabSize + 2];
|
|
var sortedFreqs = new int[tabSize + 2];
|
|
var count = 0;
|
|
|
|
// Collect non-zero widths
|
|
for(var i = 0; i <= tabSize; i++)
|
|
{
|
|
if(widths[i] > 0)
|
|
{
|
|
sortedSyms[count] = i;
|
|
sortedFreqs[count] = widths[i];
|
|
count++;
|
|
}
|
|
}
|
|
|
|
// Sort by width (ascending), stable
|
|
for(var i = 0; i < count - 1; i++)
|
|
{
|
|
for(int j = i + 1; j < count; j++)
|
|
{
|
|
if(sortedFreqs[j] < sortedFreqs[i])
|
|
{
|
|
(sortedSyms[i], sortedSyms[j]) = (sortedSyms[j], sortedSyms[i]);
|
|
(sortedFreqs[i], sortedFreqs[j]) = (sortedFreqs[j], sortedFreqs[i]);
|
|
}
|
|
}
|
|
}
|
|
|
|
if(count == 0)
|
|
{
|
|
// No symbols — fill with 0
|
|
Array.Clear(symbols, 0, Math.Min(symbols.Length, maxCodePos));
|
|
|
|
return true;
|
|
}
|
|
|
|
if(count == 1)
|
|
{
|
|
// Single symbol — give it width 1
|
|
widths[sortedSyms[0]] = 1;
|
|
sortedFreqs[0] = 1;
|
|
}
|
|
|
|
var codePos = 0;
|
|
|
|
for(int i = count - 1; i >= 0 && codePos < maxCodePos; i--)
|
|
{
|
|
int numCodes = 1 << maxWidth - sortedFreqs[i];
|
|
|
|
if(codePos + numCodes > maxCodePos) return false;
|
|
|
|
var sym = (ushort)sortedSyms[i];
|
|
|
|
for(var j = 0; j < numCodes && codePos < maxCodePos; j++) symbols[codePos++] = sym;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// ---- Read Huffman widths from bitstream ----
|
|
bool ReadWidths(ushort[] widths, ushort[] symbols, int maxWidth, int maxSize)
|
|
{
|
|
Array.Clear(widths, 0, maxSize + 1);
|
|
Array.Clear(symbols, 0, 1 << maxWidth);
|
|
|
|
uint numWidths = ReadBits(9);
|
|
|
|
if(numWidths > (uint)maxSize) numWidths = (uint)maxSize;
|
|
|
|
uint lowerWidth = ReadBits(4);
|
|
uint upperWidth = ReadBits(4);
|
|
|
|
var precodeWidths = new ushort[COMMENT_MAX_PRECODE + 1];
|
|
var precodeSymbols = new ushort[1 << COMMENT_MAX_PRECODE_WIDTH];
|
|
|
|
for(uint i = 0; i <= upperWidth; i++) precodeWidths[i] = (ushort)ReadBits(3);
|
|
|
|
if(!BuildTable(precodeWidths, (int)upperWidth, COMMENT_MAX_PRECODE_WIDTH, precodeSymbols)) return false;
|
|
|
|
uint widthPos = 0;
|
|
|
|
while(widthPos <= numWidths)
|
|
{
|
|
uint sym = precodeSymbols[code >> 32 - COMMENT_MAX_PRECODE_WIDTH];
|
|
ReadBits(precodeWidths[sym]);
|
|
|
|
if(sym < upperWidth)
|
|
widths[widthPos++] = (ushort)sym;
|
|
else
|
|
{
|
|
// Run of zeros
|
|
uint runLen = ReadBits(4) + 4;
|
|
|
|
while(runLen-- > 0 && widthPos <= numWidths) widths[widthPos++] = 0;
|
|
}
|
|
}
|
|
|
|
// Delta-decode widths
|
|
if(upperWidth > 0)
|
|
{
|
|
for(uint i = 1; i <= numWidths; i++) widths[i] = (ushort)((widths[i] + widths[i - 1]) % upperWidth);
|
|
}
|
|
|
|
// Add lower width offset to non-zero entries
|
|
for(uint i = 0; i <= numWidths; i++)
|
|
{
|
|
if(widths[i] > 0) widths[i] += (ushort)lowerWidth;
|
|
}
|
|
|
|
return BuildTable(widths, (int)numWidths, maxWidth, symbols);
|
|
}
|
|
|
|
// ---- Decompress comment using LZP + Huffman ----
|
|
var mainWidths = new ushort[COMMENT_MAX_MAIN_CODE + 2];
|
|
var mainSymbols = new ushort[(1 << COMMENT_MAX_CODE_WIDTH) + 1];
|
|
|
|
// First 15 bits = decompressed length
|
|
var commentLen = (int)ReadBits(15);
|
|
|
|
if(commentLen <= 0 || commentLen > 32768) return null;
|
|
|
|
if(!ReadWidths(mainWidths, mainSymbols, COMMENT_MAX_CODE_WIDTH, COMMENT_MAX_MAIN_CODE)) return null;
|
|
|
|
var output = new byte[commentLen];
|
|
var outPos = 0;
|
|
var hashTab = new short[256 + 255 + 1]; // hash(a, b) = a + b, max = 255 + 255 = 510
|
|
|
|
while(outPos < commentLen)
|
|
{
|
|
var matchPos = 0;
|
|
|
|
if(outPos > 1)
|
|
{
|
|
int hashValue = output[outPos - 1] + output[outPos - 2];
|
|
matchPos = hashTab[hashValue];
|
|
hashTab[hashValue] = (short)outPos;
|
|
}
|
|
|
|
uint sym = mainSymbols[code >> 32 - COMMENT_MAX_CODE_WIDTH];
|
|
ReadBits(mainWidths[sym]);
|
|
|
|
if(sym <= 255)
|
|
output[outPos++] = (byte)sym;
|
|
else
|
|
{
|
|
var copyLen = (int)(sym - 256 + 2);
|
|
|
|
for(var i = 0; i < copyLen && outPos < commentLen; i++)
|
|
output[outPos++] = matchPos + i < output.Length ? output[matchPos + i] : (byte)0;
|
|
}
|
|
}
|
|
|
|
// Convert to string — ACE comments are typically OEM (CP850) encoded
|
|
// but may contain arbitrary bytes. Use Latin1 as a safe fallback.
|
|
return Encoding.Latin1.GetString(output, 0, commentLen).TrimEnd('\0');
|
|
}
|
|
} |