mirror of
https://github.com/adamhathcock/sharpcompress.git
synced 2026-11-15 17:02:03 +00:00
Buffer ARC LZW input incrementally and reject truncated entries
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
58
tests/SharpCompress.Test/Arc/ArcAllocationTests.cs
Normal file
58
tests/SharpCompress.Test/Arc/ArcAllocationTests.cs
Normal 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));
|
||||
}
|
||||
Reference in New Issue
Block a user