Dynamic RingBuffer for BZip2: expand buffer after format detection

Agent-Logs-Url: https://github.com/adamhathcock/sharpcompress/sessions/2d1412f8-34f8-4a32-8802-e52770342940

Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-03-31 09:01:21 +00:00
committed by GitHub
parent ddcbb3e474
commit ca52cec0b3
7 changed files with 200 additions and 8 deletions

View File

@@ -127,4 +127,96 @@ public class SharpCompressStreamSeekTest
Assert.Equal(3, readBuffer[0]);
Assert.Equal(4, readBuffer[1]);
}
[Fact]
public void EnsureMinimumRewindBufferSize_ExpandsSmallBuffer_PreservesExistingData()
{
// Arrange: create a stream with a small initial buffer (size 10)
var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 });
var nonSeekableMs = new NonSeekableStreamWrapper(ms);
var stream = SharpCompressStream.Create(nonSeekableMs, 10);
stream.StartRecording();
// Read 4 bytes — they are now in the ring buffer
var buffer = new byte[8];
stream.Read(buffer, 0, 4);
Assert.Equal(4, stream.Position);
// Rewind to verify 4 bytes are present
stream.Rewind();
// Act: expand the ring buffer to 200 bytes while data is present
stream.EnsureMinimumRewindBufferSize(200);
// Verify the data is still replayable after expansion
var readBuffer = new byte[4];
stream.Read(readBuffer, 0, 4);
Assert.Equal(1, readBuffer[0]);
Assert.Equal(2, readBuffer[1]);
Assert.Equal(3, readBuffer[2]);
Assert.Equal(4, readBuffer[3]);
}
[Fact]
public void EnsureMinimumRewindBufferSize_BufferAlreadyLarger_DoesNotShrink()
{
// Arrange: create a stream with a large initial buffer (size 200)
var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 });
var nonSeekableMs = new NonSeekableStreamWrapper(ms);
var stream = SharpCompressStream.Create(nonSeekableMs, 200);
stream.StartRecording();
stream.Read(new byte[5], 0, 5);
// Act: request a smaller minimum — buffer should stay at 200
stream.EnsureMinimumRewindBufferSize(50);
// Assert: buffer can still hold the 5 bytes written before expansion request
stream.Rewind();
var readBuffer = new byte[5];
stream.Read(readBuffer, 0, 5);
Assert.Equal(1, readBuffer[0]);
Assert.Equal(5, readBuffer[4]);
}
[Fact]
public void EnsureMinimumRewindBufferSize_AllowsRewindAfterLargeRead()
{
// Simulate the BZip2 scenario: small initial buffer, expand after format detection,
// then verify a large read still allows Rewind.
const int initialSize = 10;
const int expandedSize = 100;
const int largeReadSize = 80;
var data = new byte[100];
for (var i = 0; i < data.Length; i++)
{
data[i] = (byte)(i + 1);
}
var ms = new MemoryStream(data);
var nonSeekableMs = new NonSeekableStreamWrapper(ms);
var stream = SharpCompressStream.Create(nonSeekableMs, initialSize);
stream.StartRecording();
// Read 4 bytes (format detection — magic bytes)
var buffer = new byte[4];
stream.Read(buffer, 0, 4);
stream.Rewind();
// Expand the ring buffer to cover the anticipated large probe read
stream.EnsureMinimumRewindBufferSize(expandedSize);
// Read a large amount (simulating BZip2 block decompression)
var largeBuffer = new byte[largeReadSize];
stream.Read(largeBuffer, 0, largeReadSize);
// Rewind must succeed even though largeReadSize > initialSize
stream.Rewind();
// Verify data replays correctly
var verifyBuffer = new byte[largeReadSize];
stream.Read(verifyBuffer, 0, largeReadSize);
Assert.Equal(data[0], verifyBuffer[0]);
Assert.Equal(data[largeReadSize - 1], verifyBuffer[largeReadSize - 1]);
}
}

View File

@@ -2,6 +2,8 @@ using System;
using System.Collections.Generic;
using System.IO;
using SharpCompress.Common;
using SharpCompress.Compressors.BZip2;
using SharpCompress.Factories;
using SharpCompress.Readers;
using SharpCompress.Readers.Tar;
using SharpCompress.Test.Mocks;
@@ -58,6 +60,53 @@ public class TarReaderTests : ReaderTests
[Fact]
public void Tar_GZip_OldGnu_Reader() => Read("Tar.oldgnu.tar.gz", CompressionType.GZip);
[Fact]
public void Tar_BZip2_Reader_NonSeekable()
{
// Regression test for: Dynamic default RingBuffer for BZip2
// Opening a .tar.bz2 from a non-seekable stream should succeed
// because EnsureMinimumRewindBufferSize expands the ring buffer
// to hold the BZip2 block before calling IsTarFile.
using var fs = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.bz2"));
using var nonSeekable = new ForwardOnlyStream(fs);
using var reader = ReaderFactory.OpenReader(nonSeekable);
var entryCount = 0;
while (reader.MoveToNextEntry())
{
if (!reader.Entry.IsDirectory)
{
entryCount++;
}
}
Assert.True(entryCount > 0);
}
[Fact]
public void TarWrapper_BZip2_MinimumRewindBufferSize_IsMaxBZip2BlockSize()
{
// The BZip2 TarWrapper must declare a MinimumRewindBufferSize large enough
// to hold an entire maximum-size compressed BZip2 block (9 × 100 000 bytes).
var bzip2Wrapper = Array.Find(
TarWrapper.Wrappers,
w => w.CompressionType == CompressionType.BZip2
);
Assert.NotNull(bzip2Wrapper);
Assert.Equal(BZip2Constants.baseBlockSize * 9, bzip2Wrapper.MinimumRewindBufferSize);
}
[Fact]
public void TarWrapper_Default_MinimumRewindBufferSize_Is_DefaultRewindableBufferSize()
{
// Non-BZip2 wrappers that don't specify a custom size default to
// Constants.RewindableBufferSize so existing behaviour is unchanged.
var noneWrapper = Array.Find(
TarWrapper.Wrappers,
w => w.CompressionType == CompressionType.None
);
Assert.NotNull(noneWrapper);
Assert.Equal(Common.Constants.RewindableBufferSize, noneWrapper.MinimumRewindBufferSize);
}
[Fact]
public void Tar_BZip2_Entry_Stream()
{