Fixed XZ block CRC64 computation and added notes on XZ format specifics

This commit is contained in:
Adam Hathcock
2026-06-13 14:31:36 +01:00
parent 160ba0938a
commit 2d041e5c76
6 changed files with 35 additions and 10 deletions

View File

@@ -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.

View File

@@ -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<byte> buffer)
{
_xzTable ??= CreateTable(XZ_POLYNOMIAL);
return CalculateHash(seed, _xzTable, buffer);
}
public static ulong CalculateHash(ulong seed, ulong[] table, ReadOnlySpan<byte> buffer)
{
var crc = seed;

View File

@@ -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);

View File

@@ -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);
}
}

View File

@@ -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);

View File

@@ -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);