diff --git a/Aaru.Images/CHD/CHD.cs b/Aaru.Images/CHD/CHD.cs index 1b8db700d..7ccde0180 100644 --- a/Aaru.Images/CHD/CHD.cs +++ b/Aaru.Images/CHD/CHD.cs @@ -71,12 +71,14 @@ public sealed partial class Chd : IOpticalMediaImage, IVerifiableImage int _maxSectorCache; Dictionary _offsetmap; List _partitions; + byte[] _rawMap; SectorBuilder _sectorBuilder; Dictionary _sectorCache; uint _sectorsPerHunk; bool _swapAudio; uint _totalHunks; Dictionary _tracks; + uint _unitBytes; public Chd() => _imageInfo = new ImageInfo { diff --git a/Aaru.Images/CHD/Constants.cs b/Aaru.Images/CHD/Constants.cs index 3ce1b2d97..dde45ef5a 100644 --- a/Aaru.Images/CHD/Constants.cs +++ b/Aaru.Images/CHD/Constants.cs @@ -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"; diff --git a/Aaru.Images/CHD/Enums.cs b/Aaru.Images/CHD/Enums.cs index 0cdce5fc9..47ae5399f 100644 --- a/Aaru.Images/CHD/Enums.cs +++ b/Aaru.Images/CHD/Enums.cs @@ -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 + { + /// Compressed with compressor 0 + Compressed0 = 0, + /// Compressed with compressor 1 + Compressed1 = 1, + /// Compressed with compressor 2 + Compressed2 = 2, + /// Compressed with compressor 3 + Compressed3 = 3, + /// Uncompressed + Uncompressed = 4, + /// Same as another hunk in file + SelfHunk = 5, + /// Same as another hunk in parent + ParentHunk = 6 + } + +#endregion + #region Nested type: SubTypeOld enum SubTypeOld : uint diff --git a/Aaru.Images/CHD/Helpers.cs b/Aaru.Images/CHD/Helpers.cs index dea4ddcb4..a267ebb33 100644 --- a/Aaru.Images/CHD/Helpers.cs +++ b/Aaru.Images/CHD/Helpers.cs @@ -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; + } + + /// MSB-first bitstream reader matching MAME's bitstream_in implementation + 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; + } + } } \ No newline at end of file diff --git a/Aaru.Images/CHD/Read.cs b/Aaru.Images/CHD/Read.cs index 5889cbe0f..a87c49bfb 100644 --- a/Aaru.Images/CHD/Read.cs +++ b/Aaru.Images/CHD/Read.cs @@ -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(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(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);