[CHD] Add support for v5 images. Fixes #106

This commit is contained in:
2026-03-02 11:07:24 +00:00
parent 47016dce57
commit c0d23e50fe
5 changed files with 991 additions and 79 deletions

View File

@@ -71,12 +71,14 @@ public sealed partial class Chd : IOpticalMediaImage, IVerifiableImage
int _maxSectorCache;
Dictionary<ulong, uint> _offsetmap;
List<Partition> _partitions;
byte[] _rawMap;
SectorBuilder _sectorBuilder;
Dictionary<ulong, byte[]> _sectorCache;
uint _sectorsPerHunk;
bool _swapAudio;
uint _totalHunks;
Dictionary<uint, Track> _tracks;
uint _unitBytes;
public Chd() => _imageInfo = new ImageInfo
{

View File

@@ -76,6 +76,15 @@ public sealed partial class Chd
const int MAX_CACHE_SIZE = 16777216;
const int CD_MAX_SECTOR_DATA = 2352;
const int CD_MAX_SUBCODE_DATA = 96;
const int CD_FRAME_SIZE = CD_MAX_SECTOR_DATA + CD_MAX_SUBCODE_DATA;
static readonly byte[] _cdSyncHeader =
{
0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00
};
// ReSharper disable InconsistentNaming
const string TRACK_TYPE_MODE1 = "MODE1";
const string TRACK_TYPE_MODE1_2K = "MODE1/2048";

View File

@@ -81,6 +81,46 @@ public sealed partial class Chd
#endregion
#region Nested type: CompressionV5
enum CompressionV5 : uint
{
None = 0,
Zlib = 0x7A6C6962,
Lzma = 0x6C7A6D61,
Huffman = 0x68756666,
Flac = 0x666C6163,
Zstd = 0x7A737464,
CdZlib = 0x63647A6C,
CdLzma = 0x63646C7A,
CdFlac = 0x6364666C,
CdZstd = 0x63647A73
}
#endregion
#region Nested type: EntryFlagsV5
enum EntryFlagsV5 : byte
{
/// <summary>Compressed with compressor 0</summary>
Compressed0 = 0,
/// <summary>Compressed with compressor 1</summary>
Compressed1 = 1,
/// <summary>Compressed with compressor 2</summary>
Compressed2 = 2,
/// <summary>Compressed with compressor 3</summary>
Compressed3 = 3,
/// <summary>Uncompressed</summary>
Uncompressed = 4,
/// <summary>Same as another hunk in file</summary>
SelfHunk = 5,
/// <summary>Same as another hunk in parent</summary>
ParentHunk = 6
}
#endregion
#region Nested type: SubTypeOld
enum SubTypeOld : uint

View File

@@ -229,14 +229,23 @@ public sealed partial class Chd
if(_hdrCompression == 0)
{
buffer = new byte[_bytesPerHunk];
_imageStream.Seek(_hunkTableSmall[hunkNo] * _bytesPerHunk, SeekOrigin.Begin);
_imageStream.EnsureRead(buffer, 0, buffer.Length);
if(_hunkTableSmall[hunkNo] == 0)
{
// Entry value 0 means zeroed hunk
Array.Clear(buffer, 0, buffer.Length);
}
else
{
_imageStream.Seek((long)_hunkTableSmall[hunkNo] * _bytesPerHunk, SeekOrigin.Begin);
_imageStream.EnsureRead(buffer, 0, buffer.Length);
}
}
else
{
AaruLogging.Error(Localization.Compressed_v5_hunks_not_yet_supported);
ErrorNumber err = GetHunkV5Compressed(hunkNo, out buffer);
return ErrorNumber.NotSupported;
if(err != ErrorNumber.NoError) return err;
}
break;
@@ -252,4 +261,884 @@ public sealed partial class Chd
return ErrorNumber.NoError;
}
ErrorNumber GetHunkV5Compressed(ulong hunkNo, out byte[] buffer)
{
buffer = null;
if(hunkNo >= _totalHunks) return ErrorNumber.OutOfRange;
var mapOffset = (int)(hunkNo * 12);
byte compType = _rawMap[mapOffset];
var compLength = (uint)(_rawMap[mapOffset + 1] << 16 | _rawMap[mapOffset + 2] << 8 | _rawMap[mapOffset + 3]);
ulong fileOffset = (ulong)_rawMap[mapOffset + 4] << 40 |
(ulong)_rawMap[mapOffset + 5] << 32 |
(ulong)_rawMap[mapOffset + 6] << 24 |
(ulong)_rawMap[mapOffset + 7] << 16 |
(ulong)_rawMap[mapOffset + 8] << 8 |
_rawMap[mapOffset + 9];
switch((EntryFlagsV5)compType)
{
case EntryFlagsV5.Compressed0:
case EntryFlagsV5.Compressed1:
case EntryFlagsV5.Compressed2:
case EntryFlagsV5.Compressed3:
{
uint[] codecs = [_hdrCompression, _hdrCompression1, _hdrCompression2, _hdrCompression3];
uint codec = codecs[compType];
AaruLogging.Debug(MODULE_NAME,
"GetHunkV5Compressed: hunk={0} compType={1} codec=0x{2:X8} compLength={3} fileOffset={4}",
hunkNo,
compType,
codec,
compLength,
fileOffset);
var compData = new byte[compLength];
_imageStream.Seek((long)fileOffset, SeekOrigin.Begin);
_imageStream.EnsureRead(compData, 0, compData.Length);
buffer = new byte[_bytesPerHunk];
ErrorNumber err = DecompressV5Codec(codec, compData, (int)compLength, buffer, (int)_bytesPerHunk);
if(err != ErrorNumber.NoError) return err;
break;
}
case EntryFlagsV5.Uncompressed:
buffer = new byte[_bytesPerHunk];
_imageStream.Seek((long)fileOffset, SeekOrigin.Begin);
_imageStream.EnsureRead(buffer, 0, buffer.Length);
break;
case EntryFlagsV5.SelfHunk:
return GetHunk(fileOffset, out buffer);
case EntryFlagsV5.ParentHunk:
AaruLogging.Error(Localization.Parent_images_are_not_supported);
return ErrorNumber.NotImplemented;
default:
AaruLogging.Error(string.Format(Localization.Hunk_type_0_is_not_supported, compType));
return ErrorNumber.NotSupported;
}
return ErrorNumber.NoError;
}
ErrorNumber DecompressV5Codec(uint codec, byte[] src, int srcLen, byte[] dest, int destLen)
{
switch((CompressionV5)codec)
{
case CompressionV5.Zlib:
{
var zStream = new DeflateStream(new MemoryStream(src, 0, srcLen), CompressionMode.Decompress);
int read = zStream.EnsureRead(dest, 0, destLen);
zStream.Close();
if(read != destLen)
{
AaruLogging.Error(string.Format(Localization
.Unable_to_decompress_hunk_correctly_got_0_bytes_expected_1,
read,
destLen));
return ErrorNumber.InOutError;
}
break;
}
case CompressionV5.Lzma:
{
byte[] properties = ComputeLzmaProperties(_bytesPerHunk);
int decoded = LZMA.DecodeBuffer(src, dest, properties);
if(decoded != destLen)
{
AaruLogging.Error(string.Format(Localization
.Unable_to_decompress_hunk_correctly_got_0_bytes_expected_1,
decoded,
destLen));
return ErrorNumber.InOutError;
}
break;
}
case CompressionV5.Huffman:
{
ErrorNumber err = DecompressHuffmanHunk(src, srcLen, dest, destLen);
if(err != ErrorNumber.NoError) return err;
break;
}
case CompressionV5.Flac:
{
if(!FLAC.IsSupported)
{
AaruLogging.Error(Localization.FLAC_is_not_supported);
return ErrorNumber.NotImplemented;
}
bool swapEndian = src[0] == 'B';
var flacData = new byte[srcLen - 1];
Array.Copy(src, 1, flacData, 0, srcLen - 1);
int decoded = FLAC.DecodeBuffer(flacData, dest);
if(decoded != destLen)
{
AaruLogging.Error(string.Format(Localization
.Unable_to_decompress_hunk_correctly_got_0_bytes_expected_1,
decoded,
destLen));
return ErrorNumber.InOutError;
}
if(swapEndian)
{
for(var i = 0; i < destLen; i += 2) (dest[i], dest[i + 1]) = (dest[i + 1], dest[i]);
}
break;
}
case CompressionV5.Zstd:
{
if(!ZSTD.IsSupported)
{
AaruLogging.Error(Localization.FLAC_is_not_supported);
return ErrorNumber.NotImplemented;
}
int decoded = ZSTD.DecodeBuffer(src, dest);
if(decoded != destLen)
{
AaruLogging.Error(string.Format(Localization
.Unable_to_decompress_hunk_correctly_got_0_bytes_expected_1,
decoded,
destLen));
return ErrorNumber.InOutError;
}
break;
}
case CompressionV5.CdZlib:
return DecompressCdCodec(CompressionV5.Zlib, CompressionV5.Zlib, src, srcLen, dest, destLen);
case CompressionV5.CdLzma:
return DecompressCdCodec(CompressionV5.Lzma, CompressionV5.Zlib, src, srcLen, dest, destLen);
case CompressionV5.CdZstd:
return DecompressCdCodec(CompressionV5.Zstd, CompressionV5.Zstd, src, srcLen, dest, destLen);
case CompressionV5.CdFlac:
return DecompressCdFlac(src, srcLen, dest, destLen);
default:
AaruLogging.Error(string.Format(Localization.Unsupported_compression_0, (CompressionV5)codec));
return ErrorNumber.NotSupported;
}
return ErrorNumber.NoError;
}
ErrorNumber DecompressCdCodec(CompressionV5 baseCodec, CompressionV5 subCodec, byte[] src, int srcLen, byte[] dest,
int destLen)
{
int frames = destLen / CD_FRAME_SIZE;
int compLenBytes = destLen < 65536 ? 2 : 3;
int eccBytes = (frames + 7) / 8;
int headerBytes = eccBytes + compLenBytes;
int baseCompLen = compLenBytes == 2
? src[eccBytes] << 8 | src[eccBytes + 1]
: src[eccBytes] << 16 | src[eccBytes + 1] << 8 | src[eccBytes + 2];
AaruLogging.Debug(MODULE_NAME,
"DecompressCdCodec: srcLen={0} destLen={1} frames={2} eccBytes={3} compLenBytes={4} headerBytes={5} baseCompLen={6} headerBytes+baseCompLen={7}",
srcLen,
destLen,
frames,
eccBytes,
compLenBytes,
headerBytes,
baseCompLen,
headerBytes + baseCompLen);
// Decompress sector data
var sectorBuf = new byte[frames * CD_MAX_SECTOR_DATA];
ErrorNumber err = DecompressRawCodec(baseCodec, src, headerBytes, baseCompLen, sectorBuf, sectorBuf.Length);
if(err != ErrorNumber.NoError) return err;
// Decompress subcode data
int subOffset = headerBytes + baseCompLen;
int subCompLen = srcLen - subOffset;
var subBuf = new byte[frames * CD_MAX_SUBCODE_DATA];
err = DecompressRawCodec(subCodec, src, subOffset, subCompLen, subBuf, subBuf.Length);
if(err != ErrorNumber.NoError) return err;
// Reassemble interleaved frames
for(var i = 0; i < frames; i++)
{
Array.Copy(sectorBuf, i * CD_MAX_SECTOR_DATA, dest, i * CD_FRAME_SIZE, CD_MAX_SECTOR_DATA);
Array.Copy(subBuf,
i * CD_MAX_SUBCODE_DATA,
dest,
i * CD_FRAME_SIZE + CD_MAX_SECTOR_DATA,
CD_MAX_SUBCODE_DATA);
// Restore ECC/sync if bitmap bit set
int bytePos = i / 8;
int bitPos = i % 8;
if((src[bytePos] & 1 << bitPos) == 0) continue;
// Copy sync header
Array.Copy(_cdSyncHeader, 0, dest, i * CD_FRAME_SIZE, 12);
// Reconstruct ECC for this sector
var sectorData = new byte[CD_MAX_SECTOR_DATA];
Array.Copy(dest, i * CD_FRAME_SIZE, sectorData, 0, CD_MAX_SECTOR_DATA);
_sectorBuilder?.ReconstructEcc(ref sectorData, TrackType.CdMode1);
Array.Copy(sectorData, 0, dest, i * CD_FRAME_SIZE, CD_MAX_SECTOR_DATA);
}
return ErrorNumber.NoError;
}
ErrorNumber DecompressCdFlac(byte[] src, int srcLen, byte[] dest, int destLen)
{
if(!FLAC.IsSupported)
{
AaruLogging.Error(Localization.FLAC_is_not_supported);
return ErrorNumber.NotImplemented;
}
int frames = destLen / CD_FRAME_SIZE;
bool swapEndian = src[0] == 'B';
// Decode FLAC audio data (sector data)
var flacSrc = new byte[srcLen - 1];
Array.Copy(src, 1, flacSrc, 0, srcLen - 1);
var sectorBuf = new byte[frames * CD_MAX_SECTOR_DATA];
int decoded = FLAC.DecodeBuffer(flacSrc, sectorBuf);
if(decoded != frames * CD_MAX_SECTOR_DATA)
{
AaruLogging.Error(string.Format(Localization.Unable_to_decompress_hunk_correctly_got_0_bytes_expected_1,
decoded,
frames * CD_MAX_SECTOR_DATA));
return ErrorNumber.InOutError;
}
if(swapEndian)
{
for(var i = 0; i < sectorBuf.Length; i += 2)
(sectorBuf[i], sectorBuf[i + 1]) = (sectorBuf[i + 1], sectorBuf[i]);
}
// The FLAC stream length is unknown without consumed byte tracking,
// so decompress subcode by trying from the end.
// The subcode compressed data is everything after the FLAC stream.
// We try to find it by scanning backwards for valid zlib data.
var subBuf = new byte[frames * CD_MAX_SUBCODE_DATA];
var subDecoded = false;
// Try binary search approach: FLAC must produce exactly frames*2352 bytes
// The compressed FLAC stream is between src[1] and some offset before srcLen
// Subcode zlib data follows immediately after FLAC data
for(int flacEnd = srcLen - 1; flacEnd >= 2; flacEnd--)
{
try
{
int subLen = srcLen - 1 - flacEnd;
var subSrc = new byte[subLen];
Array.Copy(src, 1 + flacEnd, subSrc, 0, subLen);
var zStream = new DeflateStream(new MemoryStream(subSrc), CompressionMode.Decompress);
int read = zStream.EnsureRead(subBuf, 0, subBuf.Length);
zStream.Close();
if(read == subBuf.Length)
{
subDecoded = true;
break;
}
}
catch(InvalidDataException)
{
// Invalid zlib data at this offset, try next
}
catch(InvalidOperationException)
{
// Invalid zlib data at this offset, try next
}
}
if(!subDecoded) Array.Clear(subBuf, 0, subBuf.Length);
// Reassemble interleaved frames
for(var i = 0; i < frames; i++)
{
Array.Copy(sectorBuf, i * CD_MAX_SECTOR_DATA, dest, i * CD_FRAME_SIZE, CD_MAX_SECTOR_DATA);
Array.Copy(subBuf,
i * CD_MAX_SUBCODE_DATA,
dest,
i * CD_FRAME_SIZE + CD_MAX_SECTOR_DATA,
CD_MAX_SUBCODE_DATA);
}
return ErrorNumber.NoError;
}
ErrorNumber DecompressRawCodec(CompressionV5 codec, byte[] src, int srcOffset, int srcLen, byte[] dest, int destLen)
{
switch(codec)
{
case CompressionV5.Zlib:
{
var zStream = new DeflateStream(new MemoryStream(src, srcOffset, srcLen), CompressionMode.Decompress);
int read = zStream.EnsureRead(dest, 0, destLen);
zStream.Close();
if(read != destLen)
{
AaruLogging.Error(string.Format(Localization
.Unable_to_decompress_hunk_correctly_got_0_bytes_expected_1,
read,
destLen));
return ErrorNumber.InOutError;
}
break;
}
case CompressionV5.Lzma:
{
byte[] properties = ComputeLzmaProperties((uint)destLen);
byte[] lzmaSrc;
if(src[srcOffset] != 0x00)
{
// Old-style LZMA stream (LZMA SDK before 2018):
// Byte 0 is part of the range coder code (bytes 0-3 = initial code).
// Modern LZMA SDK expects byte 0 = 0x00 (check only), bytes 1-4 = code.
// Prepend a 0x00 check byte to make old-style data compatible.
lzmaSrc = new byte[srcLen + 1];
lzmaSrc[0] = 0x00;
Array.Copy(src, srcOffset, lzmaSrc, 1, srcLen);
}
else
{
lzmaSrc = new byte[srcLen];
Array.Copy(src, srcOffset, lzmaSrc, 0, srcLen);
}
int decoded = LZMA.DecodeBuffer(lzmaSrc, dest, properties);
if(decoded != destLen)
{
AaruLogging.Error(string.Format(Localization
.Unable_to_decompress_hunk_correctly_got_0_bytes_expected_1,
decoded,
destLen));
return ErrorNumber.InOutError;
}
break;
}
case CompressionV5.Zstd:
{
if(!ZSTD.IsSupported)
{
AaruLogging.Error(Localization.FLAC_is_not_supported);
return ErrorNumber.NotImplemented;
}
var zstdSrc = new byte[srcLen];
Array.Copy(src, srcOffset, zstdSrc, 0, srcLen);
int decoded = ZSTD.DecodeBuffer(zstdSrc, dest);
if(decoded != destLen)
{
AaruLogging.Error(string.Format(Localization
.Unable_to_decompress_hunk_correctly_got_0_bytes_expected_1,
decoded,
destLen));
return ErrorNumber.InOutError;
}
break;
}
default:
AaruLogging.Error(string.Format(Localization.Unsupported_compression_0, codec));
return ErrorNumber.NotSupported;
}
return ErrorNumber.NoError;
}
ErrorNumber DecompressHuffmanHunk(byte[] src, int srcLen, byte[] dest, int destLen)
{
var bitstream = new BitStreamReader(src, srcLen);
// Import Huffman tree with RLE (256 symbols, maxbits=16)
var numbits = 5; // maxbits >= 16
var codeLengths = new int[256];
var curNode = 0;
while(curNode < 256)
{
int nodeBits = bitstream.Read(numbits);
if(nodeBits != 1)
codeLengths[curNode++] = nodeBits;
else
{
nodeBits = bitstream.Read(numbits);
if(nodeBits == 1)
codeLengths[curNode++] = 1;
else
{
int repCount = bitstream.Read(numbits) + 3;
for(var r = 0; r < repCount && curNode < 256; r++) codeLengths[curNode++] = nodeBits;
}
}
}
// Build canonical Huffman lookup table
var maxBits = 16;
int[] lookup = BuildHuffmanLookup(codeLengths, 256, maxBits);
if(lookup == null)
{
AaruLogging.Error(Localization.Invalid_hunk_found);
return ErrorNumber.InvalidArgument;
}
// Decode each byte
for(var i = 0; i < destLen; i++)
{
int bits = bitstream.Peek(maxBits);
int val = lookup[bits];
int symbol = val >> 5;
int codeLen = val & 0x1F;
bitstream.Remove(codeLen);
dest[i] = (byte)symbol;
}
return ErrorNumber.NoError;
}
static ErrorNumber DecompressV5Map(Stream stream, ulong mapOffset, uint totalHunks, uint hunkBytes, uint unitBytes,
out byte[] rawMap)
{
rawMap = null;
// Read 16-byte map header
var mapHeader = new byte[16];
stream.Seek((long)mapOffset, SeekOrigin.Begin);
stream.EnsureRead(mapHeader, 0, 16);
var mapBytes = BigEndianBitConverter.ToUInt32(mapHeader, 0);
ulong firstOffs = (ulong)mapHeader[4] << 40 |
(ulong)mapHeader[5] << 32 |
(ulong)mapHeader[6] << 24 |
(ulong)mapHeader[7] << 16 |
(ulong)mapHeader[8] << 8 |
mapHeader[9];
var mapCrc = (ushort)(mapHeader[10] << 8 | mapHeader[11]);
int lengthBits = mapHeader[12];
int selfBits = mapHeader[13];
int parentBits = mapHeader[14];
// Read compressed map data
var mapData = new byte[mapBytes];
stream.EnsureRead(mapData, 0, (int)mapBytes);
var bitstream = new BitStreamReader(mapData, (int)mapBytes);
// Import Huffman tree with RLE (16 symbols, maxbits=8)
var numbits = 4; // maxbits >= 8
var codeLengths = new int[16];
var curNode = 0;
while(curNode < 16)
{
int nodeBits = bitstream.Read(numbits);
if(nodeBits != 1)
codeLengths[curNode++] = nodeBits;
else
{
nodeBits = bitstream.Read(numbits);
if(nodeBits == 1)
codeLengths[curNode++] = 1;
else
{
int treeRepCount = bitstream.Read(numbits) + 3;
for(var r = 0; r < treeRepCount && curNode < 16; r++) codeLengths[curNode++] = nodeBits;
}
}
}
// Build canonical Huffman lookup table
var maxBits = 8;
int[] lookup = BuildHuffmanLookup(codeLengths, 16, maxBits);
if(lookup == null)
{
AaruLogging.Error(Localization.Invalid_hunk_found);
return ErrorNumber.InvalidArgument;
}
// Pass 1: Decode compression types (literal port from working Python)
var compTypes = new int[totalHunks];
var repCount = 0;
var lastComp = 0;
for(uint hunk = 0; hunk < totalHunks; hunk++)
{
if(repCount > 0)
{
compTypes[hunk] = lastComp;
repCount -= 1;
}
else
{
int bits = bitstream.Peek(maxBits);
int val = lookup[bits];
int symbol = val >> 5;
int codeLen = val & 0x1F;
bitstream.Remove(codeLen);
if(symbol == 7)
{
// RLE small
compTypes[hunk] = lastComp;
bits = bitstream.Peek(maxBits);
val = lookup[bits];
int count = val >> 5;
codeLen = val & 0x1F;
bitstream.Remove(codeLen);
repCount = 2 + count;
}
else if(symbol == 8)
{
// RLE large
compTypes[hunk] = lastComp;
bits = bitstream.Peek(maxBits);
val = lookup[bits];
int high = val >> 5;
codeLen = val & 0x1F;
bitstream.Remove(codeLen);
repCount = 2 + 16 + (high << 4);
bits = bitstream.Peek(maxBits);
val = lookup[bits];
int low = val >> 5;
codeLen = val & 0x1F;
bitstream.Remove(codeLen);
repCount += low;
}
else
compTypes[hunk] = lastComp = symbol;
}
}
// Pass 2: Decode lengths, offsets, CRCs (literal port from working Python)
rawMap = new byte[totalHunks * 12];
ulong curOffset = firstOffs;
uint lastSelf = 0;
ulong lastParent = 0;
for(uint hunk = 0; hunk < totalHunks; hunk++)
{
var entryOffset = (int)(hunk * 12);
int compType = compTypes[hunk];
ulong offset = curOffset;
uint length = 0;
ushort crc = 0;
if(compType >= 0 && compType <= 3)
{
length = (uint)bitstream.Read(lengthBits);
crc = (ushort)bitstream.Read(16);
curOffset += length;
}
else if(compType == 4)
{
length = hunkBytes;
crc = (ushort)bitstream.Read(16);
curOffset += hunkBytes;
}
else if(compType == 5)
{
lastSelf = (uint)bitstream.Read(selfBits);
offset = lastSelf;
}
else if(compType == 6)
{
offset = (ulong)bitstream.Read(parentBits);
lastParent = offset;
}
else if(compType == 10)
{
lastSelf += 1;
compType = 5;
offset = lastSelf;
}
else if(compType == 9)
{
compType = 5;
offset = lastSelf;
}
else if(compType == 11)
{
compType = 6;
lastParent = (ulong)hunk * hunkBytes / unitBytes;
offset = lastParent;
}
else if(compType == 13)
{
lastParent += hunkBytes / unitBytes;
compType = 6;
offset = lastParent;
}
else if(compType == 12)
{
compType = 6;
offset = lastParent;
}
rawMap[entryOffset] = (byte)(compType & 0xFF);
rawMap[entryOffset + 1] = (byte)(length >> 16 & 0xFF);
rawMap[entryOffset + 2] = (byte)(length >> 8 & 0xFF);
rawMap[entryOffset + 3] = (byte)(length & 0xFF);
rawMap[entryOffset + 4] = (byte)(offset >> 40 & 0xFF);
rawMap[entryOffset + 5] = (byte)(offset >> 32 & 0xFF);
rawMap[entryOffset + 6] = (byte)(offset >> 24 & 0xFF);
rawMap[entryOffset + 7] = (byte)(offset >> 16 & 0xFF);
rawMap[entryOffset + 8] = (byte)(offset >> 8 & 0xFF);
rawMap[entryOffset + 9] = (byte)(offset & 0xFF);
rawMap[entryOffset + 10] = (byte)(crc >> 8 & 0xFF);
rawMap[entryOffset + 11] = (byte)(crc & 0xFF);
}
// Verify map CRC16 (CCITT polynomial 0x1021, init 0xFFFF)
ushort computedCrc = 0xFFFF;
foreach(byte b in rawMap)
{
computedCrc ^= (ushort)(b << 8);
for(var bit = 0; bit < 8; bit++)
{
if((computedCrc & 0x8000) != 0)
computedCrc = (ushort)(computedCrc << 1 ^ 0x1021);
else
computedCrc = (ushort)(computedCrc << 1);
}
}
if(mapCrc != computedCrc)
{
AaruLogging.Error("Map CRC mismatch: expected 0x{0:X4}, computed 0x{1:X4}", mapCrc, computedCrc);
return ErrorNumber.InvalidArgument;
}
return ErrorNumber.NoError;
}
static int[] BuildHuffmanLookup(int[] codeLengths, int numCodes, int maxBits)
{
// Build histogram of bit lengths
var bitHisto = new int[33];
for(var i = 0; i < numCodes; i++)
{
if(codeLengths[i] > maxBits) return null;
if(codeLengths[i] <= 32) bitHisto[codeLengths[i]]++;
}
// Assign starting codes from length 32 down to 1
uint curStart = 0;
for(var codeLen = 32; codeLen > 0; codeLen--)
{
var nextStart = (uint)(curStart + bitHisto[codeLen] >> 1);
bitHisto[codeLen] = (int)curStart;
curStart = nextStart;
}
// Assign canonical codes
var codes = new uint[numCodes];
for(var i = 0; i < numCodes; i++)
{
if(codeLengths[i] > 0) codes[i] = (uint)bitHisto[codeLengths[i]]++;
}
// Build lookup table
var lookup = new int[1 << maxBits];
for(var i = 0; i < numCodes; i++)
{
if(codeLengths[i] <= 0) continue;
int value = i << 5 | codeLengths[i] & 0x1F;
int shift = maxBits - codeLengths[i];
var start = (int)(codes[i] << shift);
var end = (int)((codes[i] + 1 << shift) - 1);
for(int j = start; j <= end; j++) lookup[j] = value;
}
return lookup;
}
static byte[] ComputeLzmaProperties(uint hunkBytes)
{
var properties = new byte[5];
// lc=3, lp=0, pb=2 → 2*45+0+3 = 93 = 0x5D
properties[0] = 0x5D;
uint dictSize = 1;
while(dictSize < hunkBytes && dictSize < 1u << 26) dictSize <<= 1;
properties[1] = (byte)dictSize;
properties[2] = (byte)(dictSize >> 8);
properties[3] = (byte)(dictSize >> 16);
properties[4] = (byte)(dictSize >> 24);
return properties;
}
/// <summary>MSB-first bitstream reader matching MAME's bitstream_in implementation</summary>
sealed class BitStreamReader
{
readonly byte[] _data;
readonly int _dataLength;
internal BitStreamReader(byte[] data, int dataLength)
{
_data = data;
_dataLength = dataLength;
DOffset = 0;
DBitOffs = 0;
Buffer = 0;
Bits = 0;
}
internal int DOffset { get; private set; }
internal int DBitOffs { get; private set; }
internal uint Buffer { get; private set; }
internal int Bits { get; private set; }
internal int Peek(int numBits)
{
if(numBits == 0) return 0;
if(numBits > Bits)
{
while(Bits < 32)
{
uint newBits = 0;
if(DOffset < _dataLength) newBits = (uint)(_data[DOffset] << DBitOffs & 0xFF);
if(Bits + 8 > 32)
{
DBitOffs = 32 - Bits;
newBits >>= 8 - DBitOffs;
Buffer |= newBits;
Bits += DBitOffs;
}
else
{
Buffer |= newBits << 24 - Bits;
Bits += 8 - DBitOffs;
DBitOffs = 0;
DOffset++;
}
}
}
return (int)(Buffer >> 32 - numBits & (uint)((1 << numBits) - 1));
}
internal void Remove(int numBits)
{
Buffer = Buffer << numBits & 0xFFFFFFFF;
Bits -= numBits;
}
internal int Read(int numBits)
{
int result = Peek(numBits);
Remove(numBits);
return result;
}
}
}

View File

@@ -343,11 +343,6 @@ public sealed partial class Chd
case 5:
{
// TODO: Check why reading is misaligned
AaruLogging.Error(Localization.CHD_version_5_is_not_yet_supported);
return ErrorNumber.NotImplemented;
HeaderV5 hdrV5 = Marshal.ByteArrayToStructureBigEndian<HeaderV5>(buffer);
AaruLogging.Debug(MODULE_NAME, "hdrV5.tag = \"{0}\"", Encoding.ASCII.GetString(hdrV5.tag));
@@ -387,69 +382,54 @@ public sealed partial class Chd
AaruLogging.Debug(MODULE_NAME, "hdrV5.rawsha1 = {0}", ArrayHelpers.ByteArrayToHex(hdrV5.rawsha1));
// TODO: Implement compressed CHD v5
if(hdrV5.compressor0 == 0)
{
AaruLogging.Debug(MODULE_NAME, Localization.Reading_Hunk_map);
hunkMapStopwatch.Restart();
_hunkTableSmall = new uint[hdrV5.logicalbytes / hdrV5.hunkbytes];
var hunkSectorCount = (uint)Math.Ceiling((double)_hunkTableSmall.Length * 4 / 512);
var hunkSectorBytes = new byte[512];
stream.Seek((long)hdrV5.mapoffset, SeekOrigin.Begin);
for(var i = 0; i < hunkSectorCount; i++)
{
stream.EnsureRead(hunkSectorBytes, 0, 512);
// This does the big-endian trick but reverses the order of elements also
Array.Reverse(hunkSectorBytes);
HunkSectorSmall hunkSector =
Marshal.ByteArrayToStructureLittleEndian<HunkSectorSmall>(hunkSectorBytes);
// This restores the order of elements
Array.Reverse(hunkSector.hunkEntry);
if(_hunkTableSmall.Length >= i * 512 / 4 + 512 / 4)
Array.Copy(hunkSector.hunkEntry, 0, _hunkTableSmall, i * 512 / 4, 512 / 4);
else
{
Array.Copy(hunkSector.hunkEntry,
0,
_hunkTableSmall,
i * 512 / 4,
_hunkTableSmall.Length - i * 512 / 4);
}
}
hunkMapStopwatch.Stop();
AaruLogging.Debug(MODULE_NAME, Localization.Took_0_seconds, hunkMapStopwatch.Elapsed.TotalSeconds);
}
else
{
AaruLogging.Error(Localization.Cannot_read_compressed_CHD_version_5);
return ErrorNumber.NotSupported;
}
nextMetaOff = hdrV5.metaoffset;
_imageInfo.ImageSize = hdrV5.logicalbytes;
_imageInfo.Version = "5";
_totalHunks = (uint)(hdrV5.logicalbytes / hdrV5.hunkbytes);
_totalHunks = (uint)((hdrV5.logicalbytes + hdrV5.hunkbytes - 1) / hdrV5.hunkbytes);
_bytesPerHunk = hdrV5.hunkbytes;
_unitBytes = hdrV5.unitbytes;
_hdrCompression = hdrV5.compressor0;
_hdrCompression1 = hdrV5.compressor1;
_hdrCompression2 = hdrV5.compressor2;
_hdrCompression3 = hdrV5.compressor3;
_mapVersion = 5;
AaruLogging.Debug(MODULE_NAME, Localization.Reading_Hunk_map);
hunkMapStopwatch.Restart();
if(hdrV5.compressor0 == 0)
{
// Uncompressed map: 4 bytes per hunk (big-endian offset / hunkbytes)
_hunkTableSmall = new uint[_totalHunks];
stream.Seek((long)hdrV5.mapoffset, SeekOrigin.Begin);
for(uint i = 0; i < _totalHunks; i++)
{
var entryBytes = new byte[4];
stream.EnsureRead(entryBytes, 0, 4);
_hunkTableSmall[i] = BigEndianBitConverter.ToUInt32(entryBytes, 0);
}
}
else
{
// Compressed map
ErrorNumber mapErr = DecompressV5Map(stream,
hdrV5.mapoffset,
_totalHunks,
_bytesPerHunk,
_unitBytes,
out _rawMap);
if(mapErr != ErrorNumber.NoError) return mapErr;
}
hunkMapStopwatch.Stop();
AaruLogging.Debug(MODULE_NAME, Localization.Took_0_seconds, hunkMapStopwatch.Elapsed.TotalSeconds);
nextMetaOff = hdrV5.metaoffset;
_imageInfo.ImageSize = hdrV5.logicalbytes;
_imageInfo.Version = "5";
break;
}
@@ -1208,8 +1188,7 @@ public sealed partial class Chd
}
else if(_isCdrom)
{
// Hardcoded on MAME for CD-ROM
_sectorsPerHunk = 4;
_sectorsPerHunk = _bytesPerHunk / CD_FRAME_SIZE;
_imageInfo.MediaType = MediaType.CDROM;
_imageInfo.MetadataMediaType = MetadataMediaType.OpticalDisc;
@@ -1218,8 +1197,7 @@ public sealed partial class Chd
}
else if(_isGdrom)
{
// Hardcoded on MAME for GD-ROM
_sectorsPerHunk = 4;
_sectorsPerHunk = _bytesPerHunk / CD_FRAME_SIZE;
_imageInfo.MediaType = MediaType.GDROM;
_imageInfo.MetadataMediaType = MetadataMediaType.OpticalDisc;
@@ -1358,8 +1336,8 @@ public sealed partial class Chd
if(sectorAddress > _imageInfo.Sectors - 1) return ErrorNumber.OutOfRange;
var track = new Track();
uint sectorSize;
Track track = _isHdd ? new Track() : GetTrack(sectorAddress);
uint sectorSize;
if(!_sectorCache.TryGetValue(sectorAddress, out byte[] sector))
{
@@ -1367,8 +1345,6 @@ public sealed partial class Chd
sectorSize = _imageInfo.SectorSize;
else
{
track = GetTrack(sectorAddress);
sectorSize = (uint)track.RawBytesPerSector +
(track.SubchannelType != TrackSubchannelType.None ? 96u : 0u);
}
@@ -1502,14 +1478,12 @@ public sealed partial class Chd
if(sectorAddress > _imageInfo.Sectors - 1) return ErrorNumber.OutOfRange;
var track = new Track();
Track track = GetTrack(sectorAddress);
uint sectorSize;
if(!_sectorCache.TryGetValue(sectorAddress, out byte[] sector))
{
track = GetTrack(sectorAddress);
sectorSize = (uint)track.RawBytesPerSector + (track.SubchannelType != TrackSubchannelType.None ? 96u : 0u);
ulong hunkNo = sectorAddress / _sectorsPerHunk;
@@ -1842,12 +1816,10 @@ public sealed partial class Chd
if(sectorAddress > _imageInfo.Sectors - 1) return ErrorNumber.OutOfRange;
var track = new Track();
Track track = GetTrack(sectorAddress);
if(!_sectorCache.TryGetValue(sectorAddress, out byte[] sector))
{
track = GetTrack(sectorAddress);
uint sectorSize = (uint)track.RawBytesPerSector +
(track.SubchannelType != TrackSubchannelType.None ? 96u : 0u);