From c770bc4788fdb7e2b0a4cb33e5726ea8f2c04dc6 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Wed, 28 Jan 2026 16:33:19 +0000 Subject: [PATCH] reintroduce RewindableStream stream. SharpCompressStream does too much --- .../Compressors/ZStandard/ZStandardStream.cs | 10 +- src/SharpCompress/Factories/ArcFactory.cs | 7 +- src/SharpCompress/Factories/Factory.cs | 9 +- src/SharpCompress/Factories/GZipFactory.cs | 13 +- .../Factories/SevenZipFactory.cs | 2 +- src/SharpCompress/Factories/TarFactory.cs | 34 ++-- src/SharpCompress/IO/RewindableStream.cs | 163 ++++++++++++++++++ src/SharpCompress/Readers/ReaderFactory.cs | 9 +- 8 files changed, 208 insertions(+), 39 deletions(-) create mode 100644 src/SharpCompress/IO/RewindableStream.cs diff --git a/src/SharpCompress/Compressors/ZStandard/ZStandardStream.cs b/src/SharpCompress/Compressors/ZStandard/ZStandardStream.cs index 944cdcc9..e8adc96b 100644 --- a/src/SharpCompress/Compressors/ZStandard/ZStandardStream.cs +++ b/src/SharpCompress/Compressors/ZStandard/ZStandardStream.cs @@ -35,8 +35,14 @@ internal partial class ZStandardStream : DecompressionStream, IStreamStack internal static bool IsZStandard(Stream stream) { - var br = new BinaryReader(stream); - var magic = br.ReadUInt32(); + var buffer = new byte[4]; + var bytesRead = stream.Read(buffer, 0, 4); + if (bytesRead < 4) + { + return false; + } + + var magic = BitConverter.ToUInt32(buffer, 0); if (ZstandardConstants.MAGIC != magic) { return false; diff --git a/src/SharpCompress/Factories/ArcFactory.cs b/src/SharpCompress/Factories/ArcFactory.cs index 593fb03e..effaa02f 100644 --- a/src/SharpCompress/Factories/ArcFactory.cs +++ b/src/SharpCompress/Factories/ArcFactory.cs @@ -36,8 +36,11 @@ namespace SharpCompress.Factories var buffer = ArrayPool.Shared.Rent(2); try { - stream.ReadExact(buffer, 0, 2); - return buffer[0] == 0x1A && buffer[1] < 10; //rather thin, but this is all we have + if (stream.ReadFully(buffer.AsSpan(0, 2))) + { + return buffer[0] == 0x1A && buffer[1] < 10; //rather thin, but this is all we have + } + return false; } finally { diff --git a/src/SharpCompress/Factories/Factory.cs b/src/SharpCompress/Factories/Factory.cs index 075863e3..d838173b 100644 --- a/src/SharpCompress/Factories/Factory.cs +++ b/src/SharpCompress/Factories/Factory.cs @@ -75,7 +75,7 @@ public abstract class Factory : IFactory /// /// internal virtual bool TryOpenReader( - SharpCompressStream stream, + RewindableStream stream, ReaderOptions options, out IReader? reader ) @@ -84,16 +84,15 @@ public abstract class Factory : IFactory if (this is IReaderFactory readerFactory) { - long pos = ((IStreamStack)stream).GetPosition(); - + stream.Rewind(); if (IsArchive(stream, options.Password)) { - ((IStreamStack)stream).StackSeek(pos); + stream.Rewind(); reader = readerFactory.OpenReader(stream, options); return true; } } - + stream.Rewind(); return false; } } diff --git a/src/SharpCompress/Factories/GZipFactory.cs b/src/SharpCompress/Factories/GZipFactory.cs index fe90c391..c69b9a2a 100644 --- a/src/SharpCompress/Factories/GZipFactory.cs +++ b/src/SharpCompress/Factories/GZipFactory.cs @@ -107,31 +107,28 @@ public class GZipFactory /// internal override bool TryOpenReader( - SharpCompressStream rewindableStream, + RewindableStream rewindableStream, ReaderOptions options, out IReader? reader ) { reader = null; - long pos = ((IStreamStack)rewindableStream).GetPosition(); - if (GZipArchive.IsGZipFile(rewindableStream)) { - ((IStreamStack)rewindableStream).StackSeek(pos); + rewindableStream.Rewind(); var testStream = new GZipStream(rewindableStream, CompressionMode.Decompress); if (TarArchive.IsTarFile(testStream)) { - ((IStreamStack)rewindableStream).StackSeek(pos); + rewindableStream.Rewind(); reader = new TarReader(rewindableStream, options, CompressionType.GZip); return true; } - - ((IStreamStack)rewindableStream).StackSeek(pos); + rewindableStream.Rewind(); reader = OpenReader(rewindableStream, options); return true; } - + rewindableStream.Rewind(); return false; } diff --git a/src/SharpCompress/Factories/SevenZipFactory.cs b/src/SharpCompress/Factories/SevenZipFactory.cs index a371cce1..b643d29a 100644 --- a/src/SharpCompress/Factories/SevenZipFactory.cs +++ b/src/SharpCompress/Factories/SevenZipFactory.cs @@ -94,7 +94,7 @@ public class SevenZipFactory : Factory, IArchiveFactory, IMultiArchiveFactory #region reader internal override bool TryOpenReader( - SharpCompressStream rewindableStream, + RewindableStream rewindableStream, ReaderOptions options, out IReader? reader ) diff --git a/src/SharpCompress/Factories/TarFactory.cs b/src/SharpCompress/Factories/TarFactory.cs index 0170a3f7..3653f570 100644 --- a/src/SharpCompress/Factories/TarFactory.cs +++ b/src/SharpCompress/Factories/TarFactory.cs @@ -1,4 +1,6 @@ +using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Threading; using System.Threading.Tasks; @@ -47,18 +49,18 @@ public class TarFactory /// public override bool IsArchive(Stream stream, string? password = null) { - var rewindableStream = new SharpCompressStream(stream); - long pos = rewindableStream.GetPosition(); + var rewindableStream = new RewindableStream(stream); + rewindableStream.StartRecording(); foreach (var wrapper in TarWrapper.Wrappers) { - rewindableStream.StackSeek(pos); + rewindableStream.Rewind(); if (wrapper.IsMatch(rewindableStream)) { - rewindableStream.StackSeek(pos); + rewindableStream.Rewind(); var decompressedStream = wrapper.CreateStream(rewindableStream); if (TarArchive.IsTarFile(decompressedStream)) { - rewindableStream.StackSeek(pos); + rewindableStream.Rewind(); return true; } } @@ -74,18 +76,18 @@ public class TarFactory CancellationToken cancellationToken = default ) { - var rewindableStream = new SharpCompressStream(stream); - long pos = rewindableStream.GetPosition(); + var rewindableStream = new RewindableStream(stream); + rewindableStream.StartRecording(); foreach (var wrapper in TarWrapper.Wrappers) { - rewindableStream.StackSeek(pos); + rewindableStream.Rewind(); if (await wrapper.IsMatchAsync(rewindableStream, cancellationToken)) { - rewindableStream.StackSeek(pos); + rewindableStream.Rewind(); var decompressedStream = wrapper.CreateStream(rewindableStream); if (await TarArchive.IsTarFileAsync(decompressedStream, cancellationToken)) { - rewindableStream.StackSeek(pos); + rewindableStream.Rewind(); return true; } } @@ -155,18 +157,18 @@ public class TarFactory public IReader OpenReader(Stream stream, ReaderOptions? options) { options ??= new ReaderOptions(); - var rewindableStream = new SharpCompressStream(stream); - long pos = rewindableStream.GetPosition(); + var rewindableStream = new RewindableStream(stream); + rewindableStream.StartRecording(); foreach (var wrapper in TarWrapper.Wrappers) { - rewindableStream.StackSeek(pos); + rewindableStream.Rewind(); if (wrapper.IsMatch(rewindableStream)) { - rewindableStream.StackSeek(pos); + rewindableStream.Rewind(); var decompressedStream = wrapper.CreateStream(rewindableStream); if (TarArchive.IsTarFile(decompressedStream)) { - rewindableStream.StackSeek(pos); + rewindableStream.Rewind(); return new TarReader(rewindableStream, options, wrapper.CompressionType); } } @@ -184,7 +186,7 @@ public class TarFactory cancellationToken.ThrowIfCancellationRequested(); options ??= new ReaderOptions(); var rewindableStream = new SharpCompressStream(stream); - long pos = rewindableStream.GetPosition(); + var pos = rewindableStream.GetPosition(); foreach (var wrapper in TarWrapper.Wrappers) { rewindableStream.StackSeek(pos); diff --git a/src/SharpCompress/IO/RewindableStream.cs b/src/SharpCompress/IO/RewindableStream.cs new file mode 100644 index 00000000..d265fe4d --- /dev/null +++ b/src/SharpCompress/IO/RewindableStream.cs @@ -0,0 +1,163 @@ +using System; +using System.IO; + +namespace SharpCompress.IO +{ + internal partial class RewindableStream : Stream + { + private readonly Stream stream; + private MemoryStream bufferStream = new MemoryStream(); + private bool isRewound; + private bool isDisposed; + + public RewindableStream(Stream stream) + { + this.stream = stream; + } + + internal bool IsRecording { get; private set; } + + protected override void Dispose(bool disposing) + { + if (isDisposed) + { + return; + } + isDisposed = true; + base.Dispose(disposing); + if (disposing) + { + stream.Dispose(); + } + } + + public void Rewind(bool stopRecording = false) + { + isRewound = true; + IsRecording = !stopRecording; + bufferStream.Position = 0; + } + + public void Rewind(MemoryStream buffer) + { + if (bufferStream.Position >= buffer.Length) + { + bufferStream.Position -= buffer.Length; + } + else + { + + bufferStream.TransferTo(buffer, buffer.Length - bufferStream.Position); + //create new memorystream to allow proper resizing as memorystream could be a user provided buffer + //https://github.com/adamhathcock/sharpcompress/issues/306 + bufferStream = new MemoryStream(); + buffer.Position = 0; + buffer.TransferTo(bufferStream, buffer.Length); + bufferStream.Position = 0; + } + isRewound = true; + } + + public void StartRecording() + { + //if (isRewound && bufferStream.Position != 0) + // throw new System.NotImplementedException(); + if (bufferStream.Position != 0) + { + byte[] data = bufferStream.ToArray(); + long position = bufferStream.Position; + bufferStream.SetLength(0); + bufferStream.Write(data, (int)position, data.Length - (int)position); + bufferStream.Position = 0; + } + IsRecording = true; + } + + public override bool CanRead => true; + + public override bool CanSeek => stream.CanSeek; + + public override bool CanWrite => false; + + public override void Flush() + { + throw new NotSupportedException(); + } + + public override long Length => throw new NotSupportedException(); + + public override long Position + { + get { return stream.Position + bufferStream.Position - bufferStream.Length; } + set + { + if (!isRewound) + { + stream.Position = value; + } + else if (value < stream.Position - bufferStream.Length || value >= stream.Position) + { + stream.Position = value; + isRewound = false; + bufferStream.SetLength(0); + } + else + { + bufferStream.Position = value - stream.Position + bufferStream.Length; + } + } + } + + public override int Read(byte[] buffer, int offset, int count) + { + //don't actually read if we don't really want to read anything + //currently a network stream bug on Windows for .NET Core + if (count == 0) + { + return 0; + } + int read; + if (isRewound && bufferStream.Position != bufferStream.Length) + { + read = bufferStream.Read(buffer, offset, count); + if (read < count) + { + int tempRead = stream.Read(buffer, offset + read, count - read); + if (IsRecording) + { + bufferStream.Write(buffer, offset + read, tempRead); + } + read += tempRead; + } + if (bufferStream.Position == bufferStream.Length && !IsRecording) + { + isRewound = false; + bufferStream.SetLength(0); + } + return read; + } + + read = stream.Read(buffer, offset, count); + if (IsRecording) + { + bufferStream.Write(buffer, offset, read); + } + return read; + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + } +} diff --git a/src/SharpCompress/Readers/ReaderFactory.cs b/src/SharpCompress/Readers/ReaderFactory.cs index b5c9b294..ba3a01bd 100644 --- a/src/SharpCompress/Readers/ReaderFactory.cs +++ b/src/SharpCompress/Readers/ReaderFactory.cs @@ -34,9 +34,8 @@ public static partial class ReaderFactory stream.NotNull(nameof(stream)); options ??= new ReaderOptions() { LeaveStreamOpen = false }; - var bStream = new SharpCompressStream(stream, bufferSize: options.BufferSize); - - long pos = bStream.GetPosition(); + var bStream = new RewindableStream(stream); + bStream.StartRecording(); var factories = Factories.Factory.Factories.OfType(); @@ -53,9 +52,9 @@ public static partial class ReaderFactory && reader != null ) { + bStream.Rewind(); return reader; } - bStream.StackSeek(pos); } foreach (var factory in factories) @@ -64,7 +63,7 @@ public static partial class ReaderFactory { continue; // Already tested above } - bStream.StackSeek(pos); + bStream.Rewind(); if (factory.TryOpenReader(bStream, options, out var reader) && reader != null) { return reader;