From 1b0ec2410d69cb9068f6eb6eee8273bd9111b5b8 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Tue, 3 Feb 2026 17:07:39 +0000 Subject: [PATCH] fix deflate rewinding? --- .../Common/Zip/StreamingZipHeaderFactory.cs | 48 ++++--------------- .../Compressors/Deflate/DeflateStream.cs | 4 -- .../Compressors/Deflate/ZlibBaseStream.cs | 34 ++++--------- src/SharpCompress/IO/BufferedSubStream.cs | 10 ---- src/SharpCompress/IO/IStreamStack.cs | 17 +++++++ src/SharpCompress/IO/RewindableStream.cs | 4 +- .../IO/SeekableRewindableStream.cs | 1 + 7 files changed, 37 insertions(+), 81 deletions(-) diff --git a/src/SharpCompress/Common/Zip/StreamingZipHeaderFactory.cs b/src/SharpCompress/Common/Zip/StreamingZipHeaderFactory.cs index 24d7c0ff..6310d1f9 100644 --- a/src/SharpCompress/Common/Zip/StreamingZipHeaderFactory.cs +++ b/src/SharpCompress/Common/Zip/StreamingZipHeaderFactory.cs @@ -2,15 +2,12 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using SharpCompress.Common; using SharpCompress.Common.Zip.Headers; using SharpCompress.IO; namespace SharpCompress.Common.Zip; -internal sealed partial class StreamingZipHeaderFactory : ZipHeaderFactory +internal partial class StreamingZipHeaderFactory : ZipHeaderFactory { private IEnumerable? _entries; @@ -134,13 +131,8 @@ internal sealed partial class StreamingZipHeaderFactory : ZipHeaderFactory { _lastEntryHeader.DataStartPosition = pos - _lastEntryHeader.CompressedSize; - // For SeekableRewindableStream, seek back to just after the local header signature. - // Plain RewindableStream cannot seek to arbitrary positions, so we skip this. // 4 = First 4 bytes of the entry header (i.e. 50 4B 03 04) - if (rewindableStream is SeekableRewindableStream) - { - rewindableStream.Position = pos.Value + 4; - } + rewindableStream.Position = pos.Value + 4; } } else @@ -181,38 +173,14 @@ internal sealed partial class StreamingZipHeaderFactory : ZipHeaderFactory } // Check if zip is streaming ( Length is 0 and is declared in PostDataDescriptor ) else if (local_header.Flags.HasFlag(HeaderFlags.UsePostDataDescriptor)) { - // Peek ahead to check if next data is a header or file data. - // For SeekableRewindableStream, use direct position save/restore to avoid - // interfering with any recording state set by the caller (e.g., ReaderFactory). - // Plain RewindableStream can use StartRecording/Rewind safely since it was - // created fresh by EnsureSeekable and isn't shared with the caller. - if (rewindableStream is SeekableRewindableStream) + var nextHeaderBytes = reader.ReadUInt32(); + if (rewindableStream is IStreamStack ss) { - var savedPosition = rewindableStream.Position; - var nextHeaderBytes = reader.ReadUInt32(); - rewindableStream.Position = savedPosition; - header.HasData = !IsHeader(nextHeaderBytes); - } - else - { - // Only start recording if not already recording. - // The stream may already be recording if it was created by ReaderFactory. - if (!rewindableStream.IsRecording) - { - rewindableStream.StartRecording(); - var nextHeaderBytes = reader.ReadUInt32(); - rewindableStream.Rewind(true); - header.HasData = !IsHeader(nextHeaderBytes); - } - else - { - // If already recording, save position and restore after peek - var savedPosition = rewindableStream.Position; - var nextHeaderBytes = reader.ReadUInt32(); - rewindableStream.Position = savedPosition; - header.HasData = !IsHeader(nextHeaderBytes); - } + ss.Rewind(sizeof(uint)); } + + // Check if next data is PostDataDescriptor, streamed file with 0 length + header.HasData = !IsHeader(nextHeaderBytes); } else // We are not streaming and compressed size is 0, we have no data { diff --git a/src/SharpCompress/Compressors/Deflate/DeflateStream.cs b/src/SharpCompress/Compressors/Deflate/DeflateStream.cs index 1d79a3d8..6b8c1c2c 100644 --- a/src/SharpCompress/Compressors/Deflate/DeflateStream.cs +++ b/src/SharpCompress/Compressors/Deflate/DeflateStream.cs @@ -64,10 +64,6 @@ public partial class DeflateStream : Stream, IStreamStack leaveOpen, forceEncoding ); - -#if DEBUG_STREAMS - this.DebugConstruct(typeof(DeflateStream)); -#endif } #region Zlib properties diff --git a/src/SharpCompress/Compressors/Deflate/ZlibBaseStream.cs b/src/SharpCompress/Compressors/Deflate/ZlibBaseStream.cs index b748f741..f4070ea7 100644 --- a/src/SharpCompress/Compressors/Deflate/ZlibBaseStream.cs +++ b/src/SharpCompress/Compressors/Deflate/ZlibBaseStream.cs @@ -593,25 +593,16 @@ internal class ZlibBaseStream : Stream, IStreamStack { _stream.Flush(); } - else if (_streamMode == StreamMode.Reader && z.AvailableBytesIn > 0) + else if (z.AvailableBytesIn > 0) { // Rewind the underlying stream by the number of unconsumed bytes in the buffer // This handles the case where the decompressor over-read past the end of the entry if (_stream is IStreamStack stack) { - var root = stack.GetRootStream(); - if (root.CanSeek) - { - // Try to seek backward on the root stream; ignore if not supported - try - { - root.Seek(-z.AvailableBytesIn, SeekOrigin.Current); - } - catch (NotSupportedException) { } - } + stack.Rewind(z.AvailableBytesIn); } + z.AvailableBytesIn = 0; } - z.AvailableBytesIn = 0; } public override async Task FlushAsync(CancellationToken cancellationToken) @@ -623,25 +614,16 @@ internal class ZlibBaseStream : Stream, IStreamStack { await _stream.FlushAsync(cancellationToken).ConfigureAwait(false); } - else if (_streamMode == StreamMode.Reader && z.AvailableBytesIn > 0) + else if (z.AvailableBytesIn > 0) { // Rewind the underlying stream by the number of unconsumed bytes in the buffer // This handles the case where the decompressor over-read past the end of the entry if (_stream is IStreamStack stack) { - var root = stack.GetRootStream(); - if (root.CanSeek) - { - // Try to seek backward on the root stream; ignore if not supported - try - { - root.Seek(-z.AvailableBytesIn, SeekOrigin.Current); - } - catch (NotSupportedException) { } - } + stack.Rewind(z.AvailableBytesIn); } + z.AvailableBytesIn = 0; } - z.AvailableBytesIn = 0; } public override Int64 Seek(Int64 offset, SeekOrigin origin) => @@ -1016,7 +998,7 @@ internal class ZlibBaseStream : Stream, IStreamStack if (rc == ZlibConstants.Z_STREAM_END && z.AvailableBytesIn != 0 && !_wantCompress) { //rewind the buffer - //unused: ((IStreamStack)this).Rewind(z.AvailableBytesIn); + this.Rewind(z.AvailableBytesIn); z.AvailableBytesIn = 0; } @@ -1208,7 +1190,7 @@ internal class ZlibBaseStream : Stream, IStreamStack if (rc == ZlibConstants.Z_STREAM_END && z.AvailableBytesIn != 0 && !_wantCompress) { //rewind the buffer - //unused: ((IStreamStack)this).Rewind(z.AvailableBytesIn); + this.Rewind(z.AvailableBytesIn); z.AvailableBytesIn = 0; } diff --git a/src/SharpCompress/IO/BufferedSubStream.cs b/src/SharpCompress/IO/BufferedSubStream.cs index 51501949..e177fab5 100755 --- a/src/SharpCompress/IO/BufferedSubStream.cs +++ b/src/SharpCompress/IO/BufferedSubStream.cs @@ -8,10 +8,6 @@ namespace SharpCompress.IO; internal partial class BufferedSubStream : Stream, IStreamStack { -#if DEBUG_STREAMS - long IStreamStack.InstanceId { get; set; } -#endif - Stream IStreamStack.BaseStream() => _stream; private readonly Stream _stream; @@ -19,18 +15,12 @@ internal partial class BufferedSubStream : Stream, IStreamStack public BufferedSubStream(Stream stream, long origin, long bytesToRead) { _stream = stream ?? throw new ArgumentNullException(nameof(stream)); -#if DEBUG_STREAMS - this.DebugConstruct(typeof(BufferedSubStream)); -#endif this.origin = origin; this.BytesLeftToRead = bytesToRead; } protected override void Dispose(bool disposing) { -#if DEBUG_STREAMS - this.DebugDispose(typeof(BufferedSubStream)); -#endif if (_isDisposed) { return; diff --git a/src/SharpCompress/IO/IStreamStack.cs b/src/SharpCompress/IO/IStreamStack.cs index 48742c22..d708402a 100644 --- a/src/SharpCompress/IO/IStreamStack.cs +++ b/src/SharpCompress/IO/IStreamStack.cs @@ -49,5 +49,22 @@ namespace SharpCompress.IO } return current; } + + internal static void Rewind(this IStreamStack stream, int count) + { + IStreamStack? buffStream = null; + IStreamStack? current = stream; + + while (buffStream == null && current != null) + { + if (current is RewindableStream rewindableStream) + { + buffStream = current; + rewindableStream.Position -= Math.Min(rewindableStream.Position, count); + } + current = current.BaseStream() as IStreamStack; + } + } + } } diff --git a/src/SharpCompress/IO/RewindableStream.cs b/src/SharpCompress/IO/RewindableStream.cs index 74b9e85c..307c5a9d 100644 --- a/src/SharpCompress/IO/RewindableStream.cs +++ b/src/SharpCompress/IO/RewindableStream.cs @@ -3,8 +3,10 @@ using System.IO; namespace SharpCompress.IO; -internal partial class RewindableStream : Stream +internal partial class RewindableStream : Stream, IStreamStack { + public virtual Stream BaseStream() => stream; + private readonly Stream stream; private MemoryStream bufferStream = new MemoryStream(); private bool isRewound; diff --git a/src/SharpCompress/IO/SeekableRewindableStream.cs b/src/SharpCompress/IO/SeekableRewindableStream.cs index 6cb1d1ec..d2f7fca3 100644 --- a/src/SharpCompress/IO/SeekableRewindableStream.cs +++ b/src/SharpCompress/IO/SeekableRewindableStream.cs @@ -5,6 +5,7 @@ namespace SharpCompress.IO; internal sealed partial class SeekableRewindableStream : RewindableStream { + public override Stream BaseStream() => _underlyingStream; private readonly Stream _underlyingStream; private long? _recordedPosition;