Remove unsupported LZMA terminal-error guarantees

This commit is contained in:
Adam Hathcock
2026-10-16 16:03:00 +01:00
parent d4234a2b35
commit 1fc4a7cdf1
3 changed files with 5 additions and 32 deletions

View File

@@ -874,8 +874,6 @@ using (var reader = ReaderFactory.OpenReader(stream))
`LzmaStream` also rejects reads and writes after disposal. Failed initialization releases internal buffers without closing the supplied stream; successful construction retains the specified `leaveOpen` semantics. Disposal releases internal buffers even if finalization or underlying stream disposal throws.
Once `LzmaStream` reports corrupt or truncated LZMA/LZMA2 data, it is in a terminal error state. Subsequent synchronous or asynchronous reads throw a decoding exception without consuming more input; dispose the stream instead of retrying it.
`SharpCompress.Compressors.LZMA.Utilities.CrcCheckStream` validates its expected CRC on disposal. A mismatch throws `ArchiveOperationException`, but pooled buffers are still released. Repeated disposal is safe, including after failed CRC validation. Once disposed, `CanWrite` is `false`, and `Write` / `WriteAsync` throw `ObjectDisposedException`.
## Related Documentation

View File

@@ -46,10 +46,6 @@ Default behavior in factory APIs:
If using Compression Stream classes directly and you don't want the wrapped stream to be closed. Use the `NonDisposingStream` as a wrapper to prevent the stream being disposed. The change in 0.21 simplified a lot even though the usage is a bit more convoluted.
If an LZMA/LZMA2 decoding stream reports corrupt or truncated data, dispose it rather than
retrying reads. Subsequent reads from that `LzmaStream` throw a decoding exception; the
partially decoded state cannot be resumed safely.
## Samples
Also, look over the tests for more thorough [examples](https://github.com/adamhathcock/sharpcompress/tree/master/tests/SharpCompress.Test)

View File

@@ -34,25 +34,18 @@ public class LzmaDecoderSafetyTests
[InlineData(false, 65536)]
[InlineData(true, 1)]
[InlineData(true, 65536)]
public void CorruptionPreventsFurtherReads(bool raw, int bufferSize)
public void CorruptInputThrowsDataError(bool raw, int bufferSize)
{
using var input = CreateInput(raw);
using var stream = CreateStream(input, raw);
var buffer = new byte[bufferSize];
Assert.Throws<DataErrorException>(() => Drain(stream, buffer));
var position = input.Position;
for (var attempt = 0; attempt < 3; attempt++)
{
Assert.Throws<DataErrorException>(() => stream.Read(buffer, 0, buffer.Length));
Assert.Throws<DataErrorException>(() => stream.ReadByte());
Assert.Equal(position, input.Position);
}
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void ReadByteCorruptionPreventsFurtherReads(bool raw)
public void ReadByteCorruptInputThrowsDataError(bool raw)
{
using var input = CreateInput(raw);
using var stream = CreateStream(input, raw);
@@ -63,15 +56,12 @@ public class LzmaDecoderSafetyTests
Assert.NotEqual(-1, stream.ReadByte());
}
});
var position = input.Position;
Assert.Throws<DataErrorException>(() => stream.ReadByte());
Assert.Equal(position, input.Position);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public async Task AsyncCorruptionPreventsFurtherReads(bool raw)
public async Task AsyncCorruptInputThrowsDataError(bool raw)
{
using var input = CreateInput(raw);
using var stream = await CreateStreamAsync(input, raw);
@@ -86,18 +76,13 @@ public class LzmaDecoderSafetyTests
);
}
});
var position = input.Position;
await Assert.ThrowsAsync<DataErrorException>(() =>
stream.ReadAsync(buffer, 0, buffer.Length, CancellationToken.None)
);
Assert.Equal(position, input.Position);
}
#if !LEGACY_DOTNET
[Theory]
[InlineData(false)]
[InlineData(true)]
public async Task AsyncMemoryCorruptionPreventsFurtherReads(bool raw)
public async Task AsyncMemoryCorruptInputThrowsDataError(bool raw)
{
using var input = CreateInput(raw);
using var stream = await CreateStreamAsync(input, raw);
@@ -112,11 +97,6 @@ public class LzmaDecoderSafetyTests
);
}
});
var position = input.Position;
await Assert.ThrowsAsync<DataErrorException>(async () =>
await stream.ReadAsync(buffer.AsMemory(), CancellationToken.None)
);
Assert.Equal(position, input.Position);
}
[Fact]
@@ -126,7 +106,7 @@ public class LzmaDecoderSafetyTests
using var stream = LzmaStream.Create(new byte[] { 0 }, input, leaveOpen: true);
Assert.Throws<DataErrorException>(() => Drain(stream, new byte[65536]));
// Bypass the stream's fault guard to verify the unsafe decoder is independently safe.
// Exercise the unsafe decoder directly after corruption to verify its bounds checks.
var decoder = GetField<Decoder>(stream, "_decoder");
var window = GetField<OutWindow>(stream, "_outWindow");
var range = GetField<SharpCompress.Compressors.LZMA.RangeCoder.Decoder>(
@@ -307,7 +287,6 @@ public class LzmaDecoderSafetyTests
leaveOpen: true
);
Assert.Throws<DataErrorException>(() => stream.ReadByte());
Assert.Throws<DataErrorException>(() => stream.Read(new byte[1], 0, 1));
Assert.Equal(5, input.Position);
}
#endif