Buffer ARC LZW input incrementally and reject truncated entries

This commit is contained in:
Adam Hathcock
2026-10-16 15:34:07 +01:00
parent 9b0be6f984
commit 0860da8bc5
6 changed files with 110 additions and 40 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<IncompleteArchiveException>(() =>
stream.ReadAsync(buffer, 0, 1, CancellationToken.None)
);
}
else
{
Assert.Throws<IncompleteArchiveException>(() => 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<InvalidFormatException>(() => new ArcLzwStream(Stream.Null, -1));
}