Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion src/SharpCompress/Compressors/Deflate/GZipStream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,25 @@ public GZipStream(
CompressionMode mode,
CompressionLevel level,
Encoding encoding
)
: this(stream, mode, level, leaveOpen: false, encoding) { }

public GZipStream(
Stream stream,
CompressionMode mode,
CompressionLevel level,
bool leaveOpen,
Encoding encoding
)
{
BaseStream = new ZlibBaseStream(stream, mode, level, ZlibStreamFlavor.GZIP, encoding);
BaseStream = new ZlibBaseStream(
stream,
mode,
level,
ZlibStreamFlavor.GZIP,
leaveOpen,
encoding
);
_encoding = encoding;
}

Expand Down
60 changes: 43 additions & 17 deletions src/SharpCompress/Compressors/Deflate/ZlibBaseStream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -630,17 +630,17 @@ public override Int64 Seek(Int64 offset, SeekOrigin origin) =>
//_outStream.Seek(offset, origin);
public override void SetLength(Int64 value) => _stream.SetLength(value);

#if NOT
public int Read()
public override int ReadByte()
{
if (Read(_buf1, 0, 1) == 0)
return 0;
// calculate CRC after reading
if (crc != null)
crc.SlurpBlock(_buf1, 0, 1);
return (_buf1[0] & 0xFF);
var buffer = _buf1;
var n = Read(buffer, 0, 1);
if (n == 0)
{
return -1;
}

return buffer[0];
}
#endif

private bool nomoreinput;
private bool isDisposed;
Expand Down Expand Up @@ -987,6 +987,11 @@ public override Int32 Read(Byte[] buffer, Int32 offset, Int32 count)
}
}

// Save the inflate/deflate result code before overwriting rc with the
// byte count. Z_STREAM_END == 1, so when count == 1 and all bytes were
// produced, (count - AvailableBytesOut) == 1 which would falsely match
// Z_STREAM_END and trigger a spurious rewind.
var inflateResult = rc;
rc = (count - _z.AvailableBytesOut);

// calculate CRC after reading
Expand All @@ -995,11 +1000,19 @@ public override Int32 Read(Byte[] buffer, Int32 offset, Int32 count)
crc.SlurpBlock(buffer, offset, rc);
}

if (rc == ZlibConstants.Z_STREAM_END && z.AvailableBytesIn != 0 && !_wantCompress)
if (
inflateResult == ZlibConstants.Z_STREAM_END
&& z.AvailableBytesIn != 0
&& !_wantCompress
)
{
//rewind the buffer
this.Rewind(z.AvailableBytesIn);
z.AvailableBytesIn = 0;
// Rewind the buffer. Only clear AvailableBytesIn if rewind succeeded.
// If rewind fails (e.g., non-seekable stream), the trailer bytes are
// still in the working buffer and will be needed by finish().
if (this.Rewind(z.AvailableBytesIn))
{
z.AvailableBytesIn = 0;
}
}

return rc;
Expand Down Expand Up @@ -1181,6 +1194,11 @@ CancellationToken cancellationToken
}
}

// Save the inflate/deflate result code before overwriting rc with the
// byte count. Z_STREAM_END == 1, so when count == 1 and all bytes were
// produced, (count - AvailableBytesOut) == 1 which would falsely match
// Z_STREAM_END and trigger a spurious rewind.
var inflateResult = rc;
rc = (count - _z.AvailableBytesOut);

// calculate CRC after reading
Expand All @@ -1189,11 +1207,19 @@ CancellationToken cancellationToken
crc.SlurpBlock(buffer, offset, rc);
}

if (rc == ZlibConstants.Z_STREAM_END && z.AvailableBytesIn != 0 && !_wantCompress)
if (
inflateResult == ZlibConstants.Z_STREAM_END
&& z.AvailableBytesIn != 0
&& !_wantCompress
)
{
//rewind the buffer
this.Rewind(z.AvailableBytesIn);
z.AvailableBytesIn = 0;
// Rewind the buffer. Only clear AvailableBytesIn if rewind succeeded.
// If rewind fails (e.g., non-seekable stream), the trailer bytes are
// still in the working buffer and will be needed by finish().
if (this.Rewind(z.AvailableBytesIn))
{
z.AvailableBytesIn = 0;
}
}

return rc;
Expand Down
13 changes: 11 additions & 2 deletions src/SharpCompress/IO/IStreamStack.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,20 @@ public static Stream GetRootStream(this IStreamStack stack)
return current;
}

internal static void Rewind(this IStreamStack stream, int count)
internal static bool Rewind(this IStreamStack stream, int count)
{
IStreamStack? current = stream;

while (current != null)
{
if (current is SharpCompressStream sharpCompressStream)
{
// Check if stream supports seeking before attempting rewind
if (!sharpCompressStream.CanSeek)
{
return false;
}

// Try to rewind within the buffer. If the position is outside the buffered
// region, silently ignore (matching release behavior where streams without
// buffering simply didn't rewind).
Comment thread
kenkendk marked this conversation as resolved.
Outdated
Expand All @@ -66,15 +72,18 @@ internal static void Rewind(this IStreamStack stream, int count)
try
{
sharpCompressStream.Position = targetPosition;
return true;
}
catch (NotSupportedException)
{
// Cannot seek outside buffered region - silently ignore
return false;
}
}
return;
return false;
}
current = current.BaseStream() as IStreamStack;
}
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,13 @@ public sealed class GZipCompressionProvider : CompressionProviderBase
public override Stream CreateCompressStream(Stream destination, int compressionLevel)
{
var level = (CompressionLevel)compressionLevel;
return new GZipStream(destination, CompressionMode.Compress, level, Encoding.UTF8);
return new GZipStream(
destination,
CompressionMode.Compress,
level,
leaveOpen: true,
Comment thread
kenkendk marked this conversation as resolved.
Outdated
Encoding.UTF8
);
}

public override Stream CreateDecompressStream(Stream source)
Expand Down
192 changes: 192 additions & 0 deletions tests/SharpCompress.Test/Zip/ZipCompressionRoundtripTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
using System;
using System.IO;
using System.Linq;
using SharpCompress.Archives.Zip;
using SharpCompress.Common;
using SharpCompress.Readers;
using SharpCompress.Writers;
using SharpCompress.Writers.Zip;
using Xunit;

namespace SharpCompress.Test.Zip;

public class ZipCompressionRoundtripTests : TestBase
{
private const int TestSize = 1024 * 1024;
Comment thread
kenkendk marked this conversation as resolved.
Outdated

private static (byte[] TestSet1, byte[] TestSet2, MemoryStream Stream) CreateTestZip()
{
var testset1 = Enumerable
.Range(0, TestSize)
.Select(i => (byte)((i * 22695477) % 257))
.ToArray();
var testset2 = Enumerable
.Range(0, TestSize)
.Select(i => (byte)((i * 48271) % 257))
.ToArray();

var stream = new MemoryStream();

var writerOptions = new ZipWriterOptions(CompressionType.Deflate, compressionLevel: 9);

using (var writer = WriterFactory.OpenWriter(stream, ArchiveType.Zip, writerOptions))
{
writer.Write("sample1", new MemoryStream(testset1));
writer.Write("sample2", new MemoryStream(testset2));
}

stream.Position = 0;
return (testset1, testset2, stream);
}

[Fact]
public void Zip_Deflate_Roundtrip_ArchiveApi_BufferedRead_Succeeds()
{
var (testset1, testset2, stream) = CreateTestZip();

// Decompress and verify using Archive API with buffered read (CopyTo)
stream.Position = 0;
using (var archive = ZipArchive.OpenArchive(stream))
{
var files = archive.Entries.Where(e => !e.IsDirectory).OrderBy(e => e.Key).ToList();
Assert.Equal(2, files.Count);

// Read using CopyTo pattern (buffered - should succeed)
using (var entryStream = files[1].OpenEntryStream())
{
using var extracted = new MemoryStream();
entryStream.CopyTo(extracted);
Assert.Equal(testset2, extracted.ToArray());
}

using (var entryStream = files[0].OpenEntryStream())
{
using var extracted = new MemoryStream();
entryStream.CopyTo(extracted);
Assert.Equal(testset1, extracted.ToArray());
}
}
}

[Fact]
public void Zip_Deflate_Roundtrip_ArchiveApi_ByteByByteRead_Succeeds()
{
var (testset1, testset2, stream) = CreateTestZip();
using (var archive = ZipArchive.OpenArchive(stream))
{
var files = archive.Entries.Where(e => !e.IsDirectory).OrderBy(e => e.Key).ToList();
Assert.Equal(2, files.Count);

// Read second file byte by byte
using (var entryStream = files[1].OpenEntryStream())
{
var buffer = new byte[testset2.Length];
for (var i = 0; i < buffer.Length; i++)
{
var b = entryStream.ReadByte();
if (b == -1)
{
throw new InvalidOperationException($"Unexpected EOF at offset {i}");
}

buffer[i] = (byte)b;
}
Assert.Equal(testset2, buffer);

// Verify EOF
Assert.Equal(-1, entryStream.ReadByte());
}

// Read first file byte by byte using All pattern
using (var entryStream = files[0].OpenEntryStream())
{
var match =
testset1.All(b => b == entryStream.ReadByte()) && entryStream.ReadByte() == -1;
Assert.True(
match,
"Decompressed file sample1 contents do not match the source file."
);
}
}
}

[Fact]
public void Zip_Deflate_Roundtrip_ReaderApi_BufferedRead_Succeeds()
{
var (testset1, testset2, stream) = CreateTestZip();
using (var reader = ReaderFactory.OpenReader(stream))
{
// Read first entry
Assert.True(reader.MoveToNextEntry());
Assert.Equal("sample1", reader.Entry.Key);
using (var entryStream = reader.OpenEntryStream())
{
using var extracted = new MemoryStream();
entryStream.CopyTo(extracted);
Assert.Equal(testset1, extracted.ToArray());
}

// Read second entry
Assert.True(reader.MoveToNextEntry());
Assert.Equal("sample2", reader.Entry.Key);
using (var entryStream = reader.OpenEntryStream())
{
using var extracted = new MemoryStream();
entryStream.CopyTo(extracted);
Assert.Equal(testset2, extracted.ToArray());
}

// No more entries
Assert.False(reader.MoveToNextEntry());
}
}

[Fact]
public void Zip_Deflate_Roundtrip_ReaderApi_ByteByByteRead_Succeeds()
{
var (testset1, testset2, stream) = CreateTestZip();
using (var reader = ReaderFactory.OpenReader(stream))
{
// Read first entry byte by byte
Assert.True(reader.MoveToNextEntry());
Assert.Equal("sample1", reader.Entry.Key);
using (var entryStream = reader.OpenEntryStream())
{
var buffer = new byte[testset1.Length];
for (var i = 0; i < buffer.Length; i++)
{
var b = entryStream.ReadByte();
if (b == -1)
{
throw new InvalidOperationException($"Unexpected EOF at offset {i}");
}
buffer[i] = (byte)b;
}
Assert.Equal(testset1, buffer);
Assert.Equal(-1, entryStream.ReadByte());
}

// Read second entry byte by byte
Assert.True(reader.MoveToNextEntry());
Assert.Equal("sample2", reader.Entry.Key);
using (var entryStream = reader.OpenEntryStream())
{
var buffer = new byte[testset2.Length];
for (var i = 0; i < buffer.Length; i++)
{
var b = entryStream.ReadByte();
if (b == -1)
{
throw new InvalidOperationException($"Unexpected EOF at offset {i}");
}
buffer[i] = (byte)b;
}
Assert.Equal(testset2, buffer);
Assert.Equal(-1, entryStream.ReadByte());
}

// No more entries
Assert.False(reader.MoveToNextEntry());
}
}
}
Loading