Grow Shrink output from decoded bytes and stop at input EOF

This commit is contained in:
Adam Hathcock
2026-10-16 15:35:27 +01:00
parent 0860da8bc5
commit 8a9b76dd44
6 changed files with 134 additions and 27 deletions

View File

@@ -9,6 +9,7 @@ internal class BitStream
private int _bitIdx;
private int _bitsLeft;
private ulong _bitBuffer;
private bool _lastReadValid;
private static uint[] _maskBits = new uint[17]
{
0U,
@@ -62,6 +63,11 @@ internal class BitStream
_bitsLeft += 8;
}
}
_lastReadValid = _bitsLeft >= nbits;
if (!_lastReadValid)
{
return 0;
}
result = (int)(_bitBuffer & _maskBits[nbits]);
_bitBuffer >>= nbits;
_bitsLeft -= nbits;
@@ -70,10 +76,6 @@ internal class BitStream
public bool Advance(int count)
{
if (_byteIdx > _srcLen)
{
return false;
}
return true;
return _lastReadValid;
}
}

View File

@@ -238,6 +238,43 @@ public class HwUnshrink
byte[] dst,
int dstCap,
out int dstUsed
) => UnshrinkCore(src, srcLen, out srcUsed, ref dst, dstCap, out dstUsed, false);
internal static UnshrnkStatus UnshrinkGrowing(
byte[] src,
int dstCap,
out byte[] dst,
out int dstUsed
)
{
dst = Array.Empty<byte>();
return UnshrinkCore(src, src.Length, out _, ref dst, dstCap, out dstUsed, true);
}
private static void ReserveOutput(ref byte[] dst, int dstPos, int dstCap, bool grow)
{
if (!grow)
{
return;
}
// A code string is bounded by the code table's ushort length. Reserve at most
// one string ahead of actual output, not the size advertised by the ZIP header.
var required = (int)Math.Min(dstCap, (long)dstPos + ushort.MaxValue + 1);
if (required > dst.Length)
{
var capacity = (int)Math.Min(dstCap, Math.Max(required, (long)dst.Length * 2));
Array.Resize(ref dst, capacity);
}
}
private static UnshrnkStatus UnshrinkCore(
byte[] src,
int srcLen,
out int srcUsed,
ref byte[] dst,
int dstCap,
out int dstUsed,
bool grow
)
{
var codeTab = new CodeTabEntry[HASHTAB_SIZE];
@@ -279,6 +316,7 @@ public class HwUnshrink
return UnshrnkStatus.Full;
}
ReserveOutput(ref dst, dstPos, dstCap, grow);
firstByte = (byte)currCode;
dst[dstPos] = (byte)currCode;
codeTab[currCode].lastDstPos = dstPos;
@@ -301,6 +339,7 @@ public class HwUnshrink
return UnshrnkStatus.Full;
}
ReserveOutput(ref dst, dstPos, dstCap, grow);
// Handle KwKwK: next code used before being added.
if (currCode == queue.codes[queue.nextIdx])
{

View File

@@ -31,12 +31,8 @@ internal partial class ShrinkStream : Stream
using var srcMs = new MemoryStream();
await _inStream.CopyToAsync(srcMs, 81920, cancellationToken).ConfigureAwait(false);
var src = srcMs.ToArray();
var srcLen = src.Length;
// Decompress synchronously (CPU-bound operation)
HwUnshrink.Unshrink(src, srcLen, out _, _byteOut, (int)_uncompressedSize, out var dstUsed);
_outBytesCount = dstUsed;
_decompressed = true;
// Decompress synchronously (CPU-bound operation).
Decode(src);
}
public override async Task<int> ReadAsync(

View File

@@ -9,14 +9,14 @@ internal partial class ShrinkStream : Stream
private readonly Stream _inStream;
private readonly long _uncompressedSize;
private readonly byte[] _byteOut;
private byte[] _byteOut = Array.Empty<byte>();
private long _outBytesCount;
private bool _decompressed;
private long _position;
public ShrinkStream(Stream stream, long uncompressedSize)
{
if (uncompressedSize > int.MaxValue)
if (uncompressedSize < 0 || uncompressedSize > int.MaxValue)
{
throw new InvalidFormatException(
$"Shrink: declared uncompressed size {uncompressedSize} exceeds maximum supported size."
@@ -26,7 +26,6 @@ internal partial class ShrinkStream : Stream
_inStream = stream;
_uncompressedSize = uncompressedSize;
_byteOut = new byte[(int)_uncompressedSize];
_outBytesCount = 0L;
}
@@ -56,19 +55,7 @@ internal partial class ShrinkStream : Stream
using var srcMs = new MemoryStream();
_inStream.CopyTo(srcMs);
var src = srcMs.ToArray();
var srcLen = src.Length;
HwUnshrink.Unshrink(
src,
srcLen,
out _,
_byteOut,
(int)_uncompressedSize,
out var dstUsed
);
_outBytesCount = dstUsed;
_decompressed = true;
_position = 0;
Decode(src);
}
long remaining = _outBytesCount - _position;
@@ -86,6 +73,24 @@ internal partial class ShrinkStream : Stream
public override long Seek(long offset, SeekOrigin origin) =>
throw new NotImplementedException();
private void Decode(byte[] src)
{
var status = HwUnshrink.UnshrinkGrowing(
src,
(int)_uncompressedSize,
out var output,
out var used
);
if (status != HwUnshrink.UnshrnkStatus.Ok || used != _uncompressedSize)
{
throw new InvalidFormatException("Shrink: invalid or truncated compressed data.");
}
_byteOut = output;
_outBytesCount = used;
_decompressed = true;
_position = 0;
}
public override void SetLength(long value) => throw new NotImplementedException();
public override void Write(byte[] buffer, int offset, int count) =>