diff --git a/docs/USAGE.md b/docs/USAGE.md index 18249a87..d78ae51e 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -475,3 +475,5 @@ await archive.WriteToDirectoryAsync( ## Malformed input and memory use ZIP64 end-record extensible data is skipped with bounded buffering. Invalid record sizes and truncated extensible data are rejected rather than driving allocations from the declared size. + +ARC LZW compressed data is buffered incrementally from the bytes actually present. A truncated entry is rejected without preallocating its declared compressed size. diff --git a/src/SharpCompress/Common/Arc/ArcFilePart.Async.cs b/src/SharpCompress/Common/Arc/ArcFilePart.Async.cs index 4af92288..134e3f86 100644 --- a/src/SharpCompress/Common/Arc/ArcFilePart.Async.cs +++ b/src/SharpCompress/Common/Arc/ArcFilePart.Async.cs @@ -37,6 +37,12 @@ public partial class ArcFilePart .ConfigureAwait(false); break; case CompressionType.Crunched: + if (Header.CompressedSize > int.MaxValue) + { + throw new InvalidFormatException( + "ARC: compressed size exceeds the supported range." + ); + } if (Header.OriginalSize > 128 * 1024) { throw new NotSupportedException( diff --git a/src/SharpCompress/Common/Arc/ArcFilePart.cs b/src/SharpCompress/Common/Arc/ArcFilePart.cs index d104396e..b37347fa 100644 --- a/src/SharpCompress/Common/Arc/ArcFilePart.cs +++ b/src/SharpCompress/Common/Arc/ArcFilePart.cs @@ -52,6 +52,12 @@ public partial class ArcFilePart : FilePart compressedStream = SqueezeStream.Create(_stream, (int)Header.CompressedSize); break; case CompressionType.Crunched: + if (Header.CompressedSize > int.MaxValue) + { + throw new InvalidFormatException( + "ARC: compressed size exceeds the supported range." + ); + } if (Header.OriginalSize > 128 * 1024) { throw new NotSupportedException( diff --git a/src/SharpCompress/Compressors/ArcLzw/ArcLzwStream.Async.cs b/src/SharpCompress/Compressors/ArcLzw/ArcLzwStream.Async.cs index 56496c9a..3ab2030b 100644 --- a/src/SharpCompress/Compressors/ArcLzw/ArcLzwStream.Async.cs +++ b/src/SharpCompress/Compressors/ArcLzw/ArcLzwStream.Async.cs @@ -1,8 +1,10 @@ using System; +using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using SharpCompress.Compressors.RLE90; +using SharpCompress.IO; namespace SharpCompress.Compressors.ArcLzw; @@ -15,37 +17,19 @@ public partial class ArcLzwStream CancellationToken cancellationToken ) { - if (_processed) + cancellationToken.ThrowIfCancellationRequested(); + if (count == 0) { return 0; } - _processed = true; - var data = new byte[_compressedSize]; - int totalRead = 0; - while (totalRead < _compressedSize) + if (!_processed) { - int read = await _stream - .ReadAsync(data, totalRead, _compressedSize - totalRead, cancellationToken) - .ConfigureAwait(false); - if (read == 0) - { - break; - } - totalRead += read; + using var limited = new ReadOnlySubStream(_stream, _compressedSize); + using var data = new MemoryStream(); + await limited.CopyToAsync(data, 81920, cancellationToken).ConfigureAwait(false); + FinishDecompression(data); } - var decoded = Decompress(data, _useCrunched); - var result = decoded.Count; - if (_useCrunched) - { - var unpacked = RLE.UnpackRLE(decoded.ToArray()); - unpacked.CopyTo(buffer, 0); - result = unpacked.Count; - } - else - { - decoded.CopyTo(buffer, 0); - } - return result; + return Read(buffer, offset, count); } #if !LEGACY_DOTNET diff --git a/src/SharpCompress/Compressors/ArcLzw/ArcLzwStream.cs b/src/SharpCompress/Compressors/ArcLzw/ArcLzwStream.cs index 542d991a..c0f763dd 100644 --- a/src/SharpCompress/Compressors/ArcLzw/ArcLzwStream.cs +++ b/src/SharpCompress/Compressors/ArcLzw/ArcLzwStream.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using SharpCompress.Common; using SharpCompress.Compressors.RLE90; +using SharpCompress.IO; namespace SharpCompress.Compressors.ArcLzw; @@ -13,6 +14,8 @@ public partial class ArcLzwStream : Stream private bool _processed; private bool _useCrunched; private int _compressedSize; + private byte[]? _decoded; + private int _decodedPosition; private const int BITS = 12; private const int CRUNCH_BITS = 12; @@ -34,6 +37,10 @@ public partial class ArcLzwStream : Stream public ArcLzwStream(Stream stream, int compressedSize, bool useCrunched = true) { + if (compressedSize < 0) + { + throw new InvalidFormatException("ARC: invalid compressed size."); + } _stream = stream; _useCrunched = useCrunched; _compressedSize = compressedSize; @@ -177,28 +184,35 @@ public partial class ArcLzwStream : Stream public override int Read(byte[] buffer, int offset, int count) { - if (_processed) + if (count == 0) { return 0; } - _processed = true; - var data = new byte[_compressedSize]; - _stream.Read(data, 0, _compressedSize); - var decoded = Decompress(data, _useCrunched); - var result = decoded.Count; - if (_useCrunched) + if (!_processed) { - var unpacked = RLE.UnpackRLE(decoded.ToArray()); - unpacked.CopyTo(buffer, 0); - result = unpacked.Count; - } - else - { - decoded.CopyTo(buffer, 0); + using var limited = new ReadOnlySubStream(_stream, _compressedSize); + using var data = new MemoryStream(); + limited.CopyTo(data); + FinishDecompression(data); } + var result = Math.Min(count, _decoded!.Length - _decodedPosition); + Buffer.BlockCopy(_decoded, _decodedPosition, buffer, offset, result); + _decodedPosition += result; return result; } + private void FinishDecompression(MemoryStream data) + { + if (data.Length != _compressedSize) + { + throw new IncompleteArchiveException("Truncated ARC compressed data."); + } + // Buffer only bytes actually read, never the size claimed by the ARC header. + var decoded = Decompress(data.ToArray(), _useCrunched); + _decoded = _useCrunched ? RLE.UnpackRLE(decoded.ToArray()).ToArray() : decoded.ToArray(); + _processed = true; + } + public override long Seek(long offset, SeekOrigin origin) => throw new NotImplementedException(); diff --git a/tests/SharpCompress.Test/Arc/ArcAllocationTests.cs b/tests/SharpCompress.Test/Arc/ArcAllocationTests.cs new file mode 100644 index 00000000..9c16694e --- /dev/null +++ b/tests/SharpCompress.Test/Arc/ArcAllocationTests.cs @@ -0,0 +1,58 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Common; +using SharpCompress.Compressors.ArcLzw; +using Xunit; + +namespace SharpCompress.Test.Arc; + +public class ArcAllocationTests +{ + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Arc_Rejects_Huge_Truncated_Compressed_Data(bool useAsync) + { + using var input = new MemoryStream(new byte[] { 12, 65, 0 }); + using var stream = new ArcLzwStream(input, int.MaxValue); + var buffer = new byte[1]; + if (useAsync) + { + await Assert.ThrowsAsync(() => + stream.ReadAsync(buffer, 0, 1, CancellationToken.None) + ); + } + else + { + Assert.Throws(() => stream.Read(buffer, 0, 1)); + } + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Arc_Preserves_Output_Across_Small_Reads(bool useAsync) + { + // Two nine-bit literals, followed by a byte belonging to the next entry. + using var input = new MemoryStream(new byte[] { 65, 132, 0, 99 }); + using var stream = new ArcLzwStream(input, 3, false); + var buffer = new byte[3]; + foreach (var expected in new byte[] { 65, 66 }) + { + var read = useAsync + ? await stream.ReadAsync(buffer, 1, 1, CancellationToken.None) + : stream.Read(buffer, 1, 1); + Assert.Equal(1, read); + Assert.Equal(expected, buffer[1]); + Assert.Equal(0, buffer[0]); + } + Assert.Equal(0, stream.Read(buffer, 0, 1)); + Assert.Equal(99, input.ReadByte()); + } + + [Fact] + public void Arc_Rejects_Negative_Compressed_Size() => + Assert.Throws(() => new ArcLzwStream(Stream.Null, -1)); +}