diff --git a/Aaru.Images/PartClone/Constants.cs b/Aaru.Images/PartClone/Constants.cs index 521df2e09..ea3f92120 100644 --- a/Aaru.Images/PartClone/Constants.cs +++ b/Aaru.Images/PartClone/Constants.cs @@ -34,7 +34,20 @@ namespace Aaru.Images; public sealed partial class PartClone { - const int CRC_SIZE = 4; + /// Regular size of the per-block CRC32 trailer in partclone v0001. + const int CRC_SIZE_NORMAL = 4; + /// + /// Size of the per-block CRC32 trailer in partclone v0001 images affected by the legacy 64-bit platform bug + /// (the CRC was written using sizeof(unsigned long), producing 8 bytes on x86_64 instead of 4). + /// + const int CRC_SIZE_X64_BUG = 8; + /// Polynomial used by partclone's reflected CRC-32 (Ethernet / ISO). + const uint CRC32_POLYNOMIAL = 0xEDB88320; + /// Initial seed used by partclone's init_crc32(). + const uint CRC32_SEED = 0xFFFFFFFF; + /// Only partclone image format 0001 is supported. + const string SUPPORTED_VERSION = "0001"; + const uint MAX_CACHE_SIZE = 16777216; const uint MAX_CACHED_SECTORS = MAX_CACHE_SIZE / 512; readonly byte[] _biTmAgIc = "BiTmAgIc"u8.ToArray(); diff --git a/Aaru.Images/PartClone/Helpers.cs b/Aaru.Images/PartClone/Helpers.cs index 37efc883b..632e9d4a4 100644 --- a/Aaru.Images/PartClone/Helpers.cs +++ b/Aaru.Images/PartClone/Helpers.cs @@ -34,6 +34,60 @@ namespace Aaru.Images; public sealed partial class PartClone { + /// + /// Reflected CRC-32 lookup table (polynomial ) used by partclone's + /// crc32_0001() implementation. Kept private to the plugin because that routine carries a source-level + /// bug (see ) that would be incorrect for any other CRC-32 consumer. + /// + static readonly uint[] _crc0001Table = BuildCrc0001Table(); + + static uint[] BuildCrc0001Table() + { + var table = new uint[256]; + + for(uint i = 0; i < 256; i++) + { + uint entry = i; + + for(var j = 0; j < 8; j++) + { + if((entry & 1) != 0) + entry = entry >> 1 ^ CRC32_POLYNOMIAL; + else + entry >>= 1; + } + + table[i] = entry; + } + + return table; + } + + /// + /// Advances a running CRC-32 seed the way partclone's crc32_0001() does for image format 0001. The + /// upstream routine has a long-standing bug: the input pointer is never advanced, so every iteration + /// folds the very first byte of into the CRC. This quirk is intentionally + /// reproduced here so that Aaru's verification matches the checksums actually stored on disk. + /// + /// Previous cumulative CRC ( at the start of the stream). + /// Block buffer; only its first byte is used. + /// Number of iterations to run (i.e. the block size in bytes). + /// Updated cumulative CRC. + static uint UpdateCrc32_0001(uint crc, byte[] buffer, int size) + { + if(size <= 0 || buffer is null || buffer.Length == 0) return crc; + + byte b = buffer[0]; + + for(var s = 0; s < size; s++) + { + uint tmp = crc ^ b; + crc = crc >> 8 ^ _crc0001Table[tmp & 0xFF]; + } + + return crc; + } + ulong BlockOffset(ulong sectorAddress) { _extents.GetStart(sectorAddress, out ulong extentStart); diff --git a/Aaru.Images/PartClone/PartClone.cs b/Aaru.Images/PartClone/PartClone.cs index 39699e7eb..f34f7d323 100644 --- a/Aaru.Images/PartClone/PartClone.cs +++ b/Aaru.Images/PartClone/PartClone.cs @@ -46,7 +46,9 @@ public sealed partial class PartClone : IMediaImage, IVerifiableImage // The used block "bitmap" uses one byte per block // TODO: Convert on-image bytemap to on-memory bitmap - byte[] _byteMap; + byte[] _byteMap; + /// Detected stride of the per-block CRC trailer (4 for normal images, 8 for x64-bug ones). + int _crcSize; long _dataOff; ExtentsULong _extents; Dictionary _extentsOff; diff --git a/Aaru.Images/PartClone/Read.cs b/Aaru.Images/PartClone/Read.cs index aad8e65a1..b681dbfbb 100644 --- a/Aaru.Images/PartClone/Read.cs +++ b/Aaru.Images/PartClone/Read.cs @@ -70,6 +70,17 @@ public sealed partial class PartClone AaruLogging.Debug(MODULE_NAME, "pHdr.totalBlocks = {0}", _pHdr.totalBlocks); AaruLogging.Debug(MODULE_NAME, "pHdr.usedBlocks = {0}", _pHdr.usedBlocks); + string version = StringHandlers.CToString(_pHdr.version); + + if(version != SUPPORTED_VERSION) + { + AaruLogging.Error(MODULE_NAME, + "Unsupported partclone image version '{0}', only '0001' is supported", + version); + + return ErrorNumber.NotSupported; + } + _byteMap = new byte[_pHdr.totalBlocks]; AaruLogging.Debug(MODULE_NAME, Localization.Reading_bytemap_0_bytes, _byteMap.Length); stream.EnsureRead(_byteMap, 0, _byteMap.Length); @@ -89,6 +100,35 @@ public sealed partial class PartClone _dataOff = stream.Position; AaruLogging.Debug(MODULE_NAME, "pHdr.dataOff = {0}", _dataOff); + // Autodetect the legacy x64 CRC bug: on 64-bit platforms old partclone versions serialized + // the 4-byte CRC using sizeof(unsigned long), writing 8 bytes per block instead of 4. + // Compare the trailing data region length against both possibilities to pick the right stride. + long trailing = stream.Length - _dataOff; + var expectedNormal = (long)(_pHdr.usedBlocks * (_pHdr.blockSize + CRC_SIZE_NORMAL)); + var expectedX64 = (long)(_pHdr.usedBlocks * (_pHdr.blockSize + CRC_SIZE_X64_BUG)); + + if(trailing == expectedX64 && trailing != expectedNormal) + { + _crcSize = CRC_SIZE_X64_BUG; + + AaruLogging.Debug(MODULE_NAME, + "Detected partclone v0001 x64 CRC bug: using 8-byte CRC stride (trailing={0})", + trailing); + } + else + { + _crcSize = CRC_SIZE_NORMAL; + + if(trailing != expectedNormal && _pHdr.usedBlocks > 0) + { + AaruLogging.Debug(MODULE_NAME, + "Partclone data region size {0} does not match either layout (normal={1}, x64={2}); assuming 4-byte CRC stride", + trailing, + expectedNormal, + expectedX64); + } + } + AaruLogging.Debug(MODULE_NAME, Localization.Filling_extents); var extentFillStopwatch = new Stopwatch(); extentFillStopwatch.Start(); @@ -164,7 +204,7 @@ public sealed partial class PartClone if(_sectorCache.TryGetValue(sectorAddress, out buffer)) return ErrorNumber.NoError; - long imageOff = _dataOff + (long)(BlockOffset(sectorAddress) * (_pHdr.blockSize + CRC_SIZE)); + long imageOff = _dataOff + (long)(BlockOffset(sectorAddress) * (_pHdr.blockSize + (uint)_crcSize)); buffer = new byte[_pHdr.blockSize]; _imageStream.Seek(imageOff, SeekOrigin.Begin); diff --git a/Aaru.Images/PartClone/Verify.cs b/Aaru.Images/PartClone/Verify.cs index e713109ab..9b9def78a 100644 --- a/Aaru.Images/PartClone/Verify.cs +++ b/Aaru.Images/PartClone/Verify.cs @@ -30,15 +30,102 @@ // Copyright © 2011-2026 Natalia Portillo // ****************************************************************************/ +using System; +using System.IO; +using Aaru.Helpers; +using Aaru.Logging; + namespace Aaru.Images; public sealed partial class PartClone { #region IVerifiableImage Members - // TODO: All blocks contain a CRC32 that's incompatible with current implementation. Need to check for compatibility. /// - public bool? VerifyMediaImage() => null; + /// + /// Verifies a partclone v0001 image by replaying the cumulative CRC-32 stored after each used block. The + /// checksum is produced by partclone's buggy crc32_0001() routine (see ) + /// and is never reseeded, so the running seed is propagated from one block to the next. + /// + public bool? VerifyMediaImage() + { + if(_imageStream is null) return null; + + if(_pHdr.usedBlocks == 0) return true; + + var blockSize = (int)_pHdr.blockSize; + + if(blockSize <= 0 || _crcSize != CRC_SIZE_NORMAL && _crcSize != CRC_SIZE_X64_BUG) return null; + + var dataBuffer = new byte[blockSize]; + var crcBuffer = new byte[_crcSize]; + var allOk = true; + + try + { + _imageStream.Seek(_dataOff, SeekOrigin.Begin); + + uint running = CRC32_SEED; + + for(ulong blockIdx = 0; blockIdx < _pHdr.usedBlocks; blockIdx++) + { + int dataRead = _imageStream.EnsureRead(dataBuffer, 0, blockSize); + + if(dataRead != blockSize) + { + AaruLogging.Error(MODULE_NAME, + "Short read while verifying partclone data block {0}: expected {1}, got {2}", + blockIdx, + blockSize, + dataRead); + + return null; + } + + running = UpdateCrc32_0001(running, dataBuffer, blockSize); + + int crcRead = _imageStream.EnsureRead(crcBuffer, 0, _crcSize); + + if(crcRead != _crcSize) + { + AaruLogging.Error(MODULE_NAME, + "Short read while reading CRC trailer at data block {0}: expected {1}, got {2}", + blockIdx, + _crcSize, + crcRead); + + return null; + } + + // The x64 bug stores the CRC as an 8-byte little-endian value whose lower 4 bytes are the actual + // CRC (source cast the seed via `*(uint32_t*)checksum`). Upper 4 bytes are padding and ignored. + var storedCrc = BitConverter.ToUInt32(crcBuffer, 0); + + if(storedCrc != running) + { + AaruLogging.Error(MODULE_NAME, + "CRC mismatch at data block {0}: stored 0x{1:X8}, computed 0x{2:X8}", + blockIdx, + storedCrc, + running); + + allOk = false; + } + else + { + AaruLogging.Debug(MODULE_NAME, "Data block {0} CRC 0x{1:X8} OK", blockIdx, storedCrc); + } + } + } + catch(IOException ex) + { + AaruLogging.Error(MODULE_NAME, "I/O error while verifying partclone data blocks: {0}", ex.Message); + + return null; + } + + return allOk; + } #endregion } \ No newline at end of file