diff --git a/docs/FORMATS.md b/docs/FORMATS.md index 5b149b48..3504d659 100644 --- a/docs/FORMATS.md +++ b/docs/FORMATS.md @@ -48,9 +48,15 @@ 2. Skip through the decompressed data to reach each file's starting position **Recommendation**: For best performance with 7Zip archives, use synchronous extraction methods (`MoveToNextEntry()` and `WriteEntryToDirectory()`) when possible. Use async methods only when you need to avoid blocking the thread (e.g., in UI applications or async-only contexts). - + **Technical Details**: 7Zip archives group files into "folders" (compression units), where all files in a folder share one continuous LZMA-compressed stream. The LZMA decoder maintains internal state (dictionary window, decoder positions) that assumes sequential, non-interruptible processing. Async operations can yield control during awaits, which would corrupt this shared state. To avoid this, async extraction creates a fresh decoder stream for each file. +### XZ Format Notes + +- XZ is a container format around LZMA2-compressed blocks, not just raw LZMA/LZMA2 data. +- XZ streams can include per-block integrity checks selected by the stream header: CRC32, CRC64/XZ, SHA-256, or none. SharpCompress validates these checks while reading XZ blocks. +- Raw LZMA/LZMA2 decoding does not provide the same container-level CRC validation; it only validates what the decoder format itself can detect, such as malformed compressed data or invalid end markers. + ## Compression Streams For those who want to directly compress/decompress bits. The single file formats are represented here as well. However, BZip2, LZip and XZ have no metadata (GZip has a little) so using them without something like a Tar file makes little sense. diff --git a/src/SharpCompress/Compressors/Xz/Crc64.cs b/src/SharpCompress/Compressors/Xz/Crc64.cs index e37d04a9..3cb7fbe1 100644 --- a/src/SharpCompress/Compressors/Xz/Crc64.cs +++ b/src/SharpCompress/Compressors/Xz/Crc64.cs @@ -6,10 +6,13 @@ namespace SharpCompress.Compressors.Xz; public static class Crc64 { public const ulong DefaultSeed = 0x0; + internal const ulong XZ_SEED = 0xffffffffffffffff; internal static ulong[]? Table; + private static ulong[]? _xzTable; public const ulong Iso3309Polynomial = 0xD800000000000000; + private const ulong XZ_POLYNOMIAL = 0xC96C5795D7870F42; public static ulong Compute(byte[] buffer) => Compute(DefaultSeed, buffer); @@ -20,6 +23,15 @@ public static class Crc64 return CalculateHash(seed, Table, buffer); } + public static ulong ComputeXz(byte[] buffer) => ~UpdateXz(XZ_SEED, buffer); + + public static ulong UpdateXz(ulong seed, ReadOnlySpan buffer) + { + _xzTable ??= CreateTable(XZ_POLYNOMIAL); + + return CalculateHash(seed, _xzTable, buffer); + } + public static ulong CalculateHash(ulong seed, ulong[] table, ReadOnlySpan buffer) { var crc = seed; diff --git a/src/SharpCompress/Compressors/Xz/XZBlock.cs b/src/SharpCompress/Compressors/Xz/XZBlock.cs index bfb5f3b0..81db843b 100644 --- a/src/SharpCompress/Compressors/Xz/XZBlock.cs +++ b/src/SharpCompress/Compressors/Xz/XZBlock.cs @@ -21,7 +21,7 @@ public sealed partial class XZBlock : XZReadOnlyStream private readonly CheckType _checkType; private readonly int _checkSize; private uint _crc32 = Crc32.DefaultSeed; - private ulong _crc64 = Crc64.DefaultSeed; + private ulong _crc64 = Crc64.XZ_SEED; private readonly SHA256? _sha256; private bool _streamConnected; private int _numFilters; @@ -125,8 +125,7 @@ public sealed partial class XZBlock : XZReadOnlyStream _crc32 = Crc32.Update(_crc32, bytes); break; case CheckType.CRC64: - Crc64.Table ??= Crc64.CreateTable(Crc64.Iso3309Polynomial); - _crc64 = Crc64.CalculateHash(_crc64, Crc64.Table, bytes); + _crc64 = Crc64.UpdateXz(_crc64, bytes); break; case CheckType.SHA256: _sha256.NotNull().TransformBlock(buffer, offset, count, null, 0); diff --git a/tests/SharpCompress.Test/Xz/Crc64Tests.cs b/tests/SharpCompress.Test/Xz/Crc64Tests.cs index 8cf71e1e..e2338c64 100644 --- a/tests/SharpCompress.Test/Xz/Crc64Tests.cs +++ b/tests/SharpCompress.Test/Xz/Crc64Tests.cs @@ -27,4 +27,12 @@ public class Crc64Tests Assert.Equal((ulong)0x416B4150508661EE, actual); } + + [Fact] + public void XzCheckString() + { + var actual = Crc64.ComputeXz(Encoding.ASCII.GetBytes("123456789")); + + Assert.Equal(0x995DC9BBDF1939FAUL, actual); + } } diff --git a/tests/SharpCompress.Test/Xz/XZBlockAsyncTests.cs b/tests/SharpCompress.Test/Xz/XZBlockAsyncTests.cs index 6460ed53..9d59d6d4 100644 --- a/tests/SharpCompress.Test/Xz/XZBlockAsyncTests.cs +++ b/tests/SharpCompress.Test/Xz/XZBlockAsyncTests.cs @@ -85,7 +85,7 @@ public class XzBlockAsyncTests : XzTestsBase { using var xzBlock = new XZBlock(CompressedStream, CheckType.CRC64, 8); using var sr = new StreamReader(xzBlock); - Assert.Equal(await sr.ReadToEndAsync().ConfigureAwait(false), Original); + Assert.Equal(Original, await sr.ReadToEndAsync().ConfigureAwait(false)); } [Fact] @@ -101,8 +101,8 @@ public class XzBlockAsyncTests : XzTestsBase [Fact] public async ValueTask SkipsPaddingWhenPresentAsync() { - // CompressedIndexedStream's first block has 1-byte padding. - using var xzBlock = new XZBlock(CompressedIndexedStream, CheckType.CRC64, 8); + // CompressedIndexedStream uses CRC32 checks. + using var xzBlock = new XZBlock(CompressedIndexedStream, CheckType.CRC32, 4); using var sr = new StreamReader(xzBlock); await sr.ReadToEndAsync().ConfigureAwait(false); Assert.Equal(0L, CompressedIndexedStream.Position % 4L); diff --git a/tests/SharpCompress.Test/Xz/XZBlockTests.cs b/tests/SharpCompress.Test/Xz/XZBlockTests.cs index 3b06008e..9b6efe6e 100644 --- a/tests/SharpCompress.Test/Xz/XZBlockTests.cs +++ b/tests/SharpCompress.Test/Xz/XZBlockTests.cs @@ -72,7 +72,7 @@ public class XzBlockTests : XzTestsBase { var xzBlock = new XZBlock(CompressedStream, CheckType.CRC64, 8); using var sr = new StreamReader(xzBlock); - Assert.Equal(sr.ReadToEnd(), Original); + Assert.Equal(Original, sr.ReadToEnd()); } [Fact] @@ -88,8 +88,8 @@ public class XzBlockTests : XzTestsBase [Fact] public void SkipsPaddingWhenPresent() { - // CompressedIndexedStream's first block has 1-byte padding. - using var xzBlock = new XZBlock(CompressedIndexedStream, CheckType.CRC64, 8); + // CompressedIndexedStream uses CRC32 checks. + using var xzBlock = new XZBlock(CompressedIndexedStream, CheckType.CRC32, 4); using var sr = new StreamReader(xzBlock); sr.ReadToEnd(); Assert.Equal(0L, CompressedIndexedStream.Position % 4L);