From 0a50386ada4833b37a05e259e628795cc27893aa Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Tue, 27 Jan 2026 10:46:54 +0000 Subject: [PATCH 01/21] Using Constants class differently --- src/SharpCompress/Archives/ArchiveFactory.cs | 14 ++------ .../Archives/AutoArchiveFactory.cs | 7 ++-- .../Archives/IArchiveEntryExtensions.cs | 6 ++-- src/SharpCompress/Archives/Tar/TarArchive.cs | 2 +- src/SharpCompress/Archives/Zip/ZipArchive.cs | 33 +++++-------------- src/SharpCompress/Common/Constants.cs | 10 ++++++ src/SharpCompress/Factories/AceFactory.cs | 6 +--- src/SharpCompress/Factories/ArcFactory.cs | 14 +++----- src/SharpCompress/Factories/ArjFactory.cs | 6 +--- src/SharpCompress/Factories/Factory.cs | 8 ++--- src/SharpCompress/Factories/GZipFactory.cs | 7 ++-- src/SharpCompress/Factories/IFactory.cs | 6 +--- src/SharpCompress/Factories/RarFactory.cs | 7 ++-- .../Factories/SevenZipFactory.cs | 7 ++-- src/SharpCompress/Factories/TarFactory.cs | 7 ++-- .../Factories/ZStandardFactory.cs | 7 ++-- src/SharpCompress/Factories/ZipFactory.cs | 12 +++---- src/SharpCompress/Readers/AbstractReader.cs | 10 ++++-- src/SharpCompress/Readers/ReaderOptions.cs | 4 +-- src/SharpCompress/Utility.cs | 8 ++--- src/SharpCompress/Writers/GZip/GZipWriter.cs | 2 +- src/SharpCompress/Writers/Zip/ZipWriter.cs | 3 +- .../Writers/Zip/ZipWriterEntryOptions.cs | 2 +- .../Mocks/ForwardOnlyStream.cs | 6 ++-- tests/SharpCompress.Test/packages.lock.json | 12 ------- 25 files changed, 69 insertions(+), 137 deletions(-) create mode 100644 src/SharpCompress/Common/Constants.cs diff --git a/src/SharpCompress/Archives/ArchiveFactory.cs b/src/SharpCompress/Archives/ArchiveFactory.cs index 94368ece..1f4c8e6f 100644 --- a/src/SharpCompress/Archives/ArchiveFactory.cs +++ b/src/SharpCompress/Archives/ArchiveFactory.cs @@ -166,22 +166,14 @@ public static class ArchiveFactory ); } - public static bool IsArchive( - string filePath, - out ArchiveType? type, - int bufferSize = ReaderOptions.DefaultBufferSize - ) + public static bool IsArchive(string filePath, out ArchiveType? type) { filePath.NotNullOrEmpty(nameof(filePath)); using Stream s = File.OpenRead(filePath); - return IsArchive(s, out type, bufferSize); + return IsArchive(s, out type); } - public static bool IsArchive( - Stream stream, - out ArchiveType? type, - int bufferSize = ReaderOptions.DefaultBufferSize - ) + public static bool IsArchive(Stream stream, out ArchiveType? type) { type = null; stream.NotNull(nameof(stream)); diff --git a/src/SharpCompress/Archives/AutoArchiveFactory.cs b/src/SharpCompress/Archives/AutoArchiveFactory.cs index 78313df5..12b70266 100644 --- a/src/SharpCompress/Archives/AutoArchiveFactory.cs +++ b/src/SharpCompress/Archives/AutoArchiveFactory.cs @@ -14,11 +14,8 @@ class AutoArchiveFactory : IArchiveFactory public IEnumerable GetSupportedExtensions() => throw new NotSupportedException(); - public bool IsArchive( - Stream stream, - string? password = null, - int bufferSize = ReaderOptions.DefaultBufferSize - ) => throw new NotSupportedException(); + public bool IsArchive(Stream stream, string? password = null) => + throw new NotSupportedException(); public FileInfo? GetFilePart(int index, FileInfo part1) => throw new NotSupportedException(); diff --git a/src/SharpCompress/Archives/IArchiveEntryExtensions.cs b/src/SharpCompress/Archives/IArchiveEntryExtensions.cs index af2c9be4..1460c2ec 100644 --- a/src/SharpCompress/Archives/IArchiveEntryExtensions.cs +++ b/src/SharpCompress/Archives/IArchiveEntryExtensions.cs @@ -9,8 +9,6 @@ namespace SharpCompress.Archives; public static class IArchiveEntryExtensions { - private const int BufferSize = 81920; - /// The archive entry to extract. extension(IArchiveEntry archiveEntry) { @@ -28,7 +26,7 @@ public static class IArchiveEntryExtensions using var entryStream = archiveEntry.OpenEntryStream(); var sourceStream = WrapWithProgress(entryStream, archiveEntry, progress); - sourceStream.CopyTo(streamToWriteTo, BufferSize); + sourceStream.CopyTo(streamToWriteTo, Constants.BufferSize); } /// @@ -51,7 +49,7 @@ public static class IArchiveEntryExtensions using var entryStream = await archiveEntry.OpenEntryStreamAsync(cancellationToken); var sourceStream = WrapWithProgress(entryStream, archiveEntry, progress); await sourceStream - .CopyToAsync(streamToWriteTo, BufferSize, cancellationToken) + .CopyToAsync(streamToWriteTo, Constants.BufferSize, cancellationToken) .ConfigureAwait(false); } } diff --git a/src/SharpCompress/Archives/Tar/TarArchive.cs b/src/SharpCompress/Archives/Tar/TarArchive.cs index 2754fd9b..24bf77b7 100644 --- a/src/SharpCompress/Archives/Tar/TarArchive.cs +++ b/src/SharpCompress/Archives/Tar/TarArchive.cs @@ -180,7 +180,7 @@ public class TarArchive : AbstractWritableArchive using (var entryStream = entry.OpenEntryStream()) { using var memoryStream = new MemoryStream(); - entryStream.CopyTo(memoryStream); + entryStream.CopyTo(memoryStream, Constants.BufferSize); memoryStream.Position = 0; var bytes = memoryStream.ToArray(); diff --git a/src/SharpCompress/Archives/Zip/ZipArchive.cs b/src/SharpCompress/Archives/Zip/ZipArchive.cs index 57db85c2..8a870b99 100644 --- a/src/SharpCompress/Archives/Zip/ZipArchive.cs +++ b/src/SharpCompress/Archives/Zip/ZipArchive.cs @@ -124,38 +124,27 @@ public class ZipArchive : AbstractWritableArchive ); } - public static bool IsZipFile( - string filePath, - string? password = null, - int bufferSize = ReaderOptions.DefaultBufferSize - ) => IsZipFile(new FileInfo(filePath), password, bufferSize); + public static bool IsZipFile(string filePath, string? password = null) => + IsZipFile(new FileInfo(filePath), password); - public static bool IsZipFile( - FileInfo fileInfo, - string? password = null, - int bufferSize = ReaderOptions.DefaultBufferSize - ) + public static bool IsZipFile(FileInfo fileInfo, string? password = null) { if (!fileInfo.Exists) { return false; } using Stream stream = fileInfo.OpenRead(); - return IsZipFile(stream, password, bufferSize); + return IsZipFile(stream, password); } - public static bool IsZipFile( - Stream stream, - string? password = null, - int bufferSize = ReaderOptions.DefaultBufferSize - ) + public static bool IsZipFile(Stream stream, string? password = null) { var headerFactory = new StreamingZipHeaderFactory(password, new ArchiveEncoding(), null); try { if (stream is not SharpCompressStream) { - stream = new SharpCompressStream(stream, bufferSize: bufferSize); + stream = new SharpCompressStream(stream, bufferSize: Constants.BufferSize); } var header = headerFactory @@ -177,18 +166,14 @@ public class ZipArchive : AbstractWritableArchive } } - public static bool IsZipMulti( - Stream stream, - string? password = null, - int bufferSize = ReaderOptions.DefaultBufferSize - ) + public static bool IsZipMulti(Stream stream, string? password = null) { var headerFactory = new StreamingZipHeaderFactory(password, new ArchiveEncoding(), null); try { if (stream is not SharpCompressStream) { - stream = new SharpCompressStream(stream, bufferSize: bufferSize); + stream = new SharpCompressStream(stream, bufferSize: Constants.BufferSize); } var header = headerFactory @@ -229,7 +214,7 @@ public class ZipArchive : AbstractWritableArchive if (streams.Count() > 1) //test part 2 - true = multipart not split { streams[1].Position += 4; //skip the POST_DATA_DESCRIPTOR to prevent an exception - var isZip = IsZipFile(streams[1], ReaderOptions.Password, ReaderOptions.BufferSize); + var isZip = IsZipFile(streams[1], ReaderOptions.Password); streams[1].Position -= 4; if (isZip) { diff --git a/src/SharpCompress/Common/Constants.cs b/src/SharpCompress/Common/Constants.cs new file mode 100644 index 00000000..08653fc6 --- /dev/null +++ b/src/SharpCompress/Common/Constants.cs @@ -0,0 +1,10 @@ +namespace SharpCompress.Common; + +public static class Constants +{ + /// + /// The default buffer size for stream operations, matching .NET's Stream.CopyTo default of 81920 bytes. + /// This can be modified globally at runtime. + /// + public static int BufferSize = 81920; +} diff --git a/src/SharpCompress/Factories/AceFactory.cs b/src/SharpCompress/Factories/AceFactory.cs index 5b80ae24..883c7a92 100644 --- a/src/SharpCompress/Factories/AceFactory.cs +++ b/src/SharpCompress/Factories/AceFactory.cs @@ -22,11 +22,7 @@ namespace SharpCompress.Factories yield return "ace"; } - public override bool IsArchive( - Stream stream, - string? password = null, - int bufferSize = ReaderOptions.DefaultBufferSize - ) + public override bool IsArchive(Stream stream, string? password = null) { return AceHeader.IsArchive(stream); } diff --git a/src/SharpCompress/Factories/ArcFactory.cs b/src/SharpCompress/Factories/ArcFactory.cs index b5180afa..c9611d7c 100644 --- a/src/SharpCompress/Factories/ArcFactory.cs +++ b/src/SharpCompress/Factories/ArcFactory.cs @@ -23,18 +23,14 @@ namespace SharpCompress.Factories yield return "arc"; } - public override bool IsArchive( - Stream stream, - string? password = null, - int bufferSize = ReaderOptions.DefaultBufferSize - ) + public override bool IsArchive(Stream stream, string? password = null) { //You may have to use some(paranoid) checks to ensure that you actually are - //processing an ARC file, since other archivers also adopted the idea of putting - //a 01Ah byte at offset 0, namely the Hyper archiver. To check if you have a + //processing an ARC file, since other archivers also adopted to the idea of putting + //a 01Ah byte at offset 0, namely: Hyper archiver. To check if you have a //Hyper - archive, check the next two bytes for "HP" or "ST"(or look below for - //"HYP").Also the ZOO archiver also does put a 01Ah at the start of the file, - //see the ZOO entry below. + //"HYP").Also, ZOO archiver also does put a 01Ah at the start of the file, + //see: ZOO entry below. var bytes = new byte[2]; stream.Read(bytes, 0, 2); return bytes[0] == 0x1A && bytes[1] < 10; //rather thin, but this is all we have diff --git a/src/SharpCompress/Factories/ArjFactory.cs b/src/SharpCompress/Factories/ArjFactory.cs index f6f7a393..fad03b75 100644 --- a/src/SharpCompress/Factories/ArjFactory.cs +++ b/src/SharpCompress/Factories/ArjFactory.cs @@ -22,11 +22,7 @@ namespace SharpCompress.Factories yield return "arj"; } - public override bool IsArchive( - Stream stream, - string? password = null, - int bufferSize = ReaderOptions.DefaultBufferSize - ) + public override bool IsArchive(Stream stream, string? password = null) { return ArjHeader.IsArchive(stream); } diff --git a/src/SharpCompress/Factories/Factory.cs b/src/SharpCompress/Factories/Factory.cs index 4651ccb2..3f505d53 100644 --- a/src/SharpCompress/Factories/Factory.cs +++ b/src/SharpCompress/Factories/Factory.cs @@ -51,11 +51,7 @@ public abstract class Factory : IFactory public abstract IEnumerable GetSupportedExtensions(); /// - public abstract bool IsArchive( - Stream stream, - string? password = null, - int bufferSize = ReaderOptions.DefaultBufferSize - ); + public abstract bool IsArchive(Stream stream, string? password = null); /// public virtual FileInfo? GetFilePart(int index, FileInfo part1) => null; @@ -82,7 +78,7 @@ public abstract class Factory : IFactory { long pos = ((IStreamStack)stream).GetPosition(); - if (IsArchive(stream, options.Password, options.BufferSize)) + if (IsArchive(stream, options.Password)) { ((IStreamStack)stream).StackSeek(pos); reader = readerFactory.OpenReader(stream, options); diff --git a/src/SharpCompress/Factories/GZipFactory.cs b/src/SharpCompress/Factories/GZipFactory.cs index 17f344cf..89a0cdec 100644 --- a/src/SharpCompress/Factories/GZipFactory.cs +++ b/src/SharpCompress/Factories/GZipFactory.cs @@ -40,11 +40,8 @@ public class GZipFactory } /// - public override bool IsArchive( - Stream stream, - string? password = null, - int bufferSize = ReaderOptions.DefaultBufferSize - ) => GZipArchive.IsGZipFile(stream); + public override bool IsArchive(Stream stream, string? password = null) => + GZipArchive.IsGZipFile(stream); #endregion diff --git a/src/SharpCompress/Factories/IFactory.cs b/src/SharpCompress/Factories/IFactory.cs index 63d5eeec..e95da612 100644 --- a/src/SharpCompress/Factories/IFactory.cs +++ b/src/SharpCompress/Factories/IFactory.cs @@ -36,11 +36,7 @@ public interface IFactory /// /// A stream, pointing to the beginning of the archive. /// optional password - bool IsArchive( - Stream stream, - string? password = null, - int bufferSize = ReaderOptions.DefaultBufferSize - ); + bool IsArchive(Stream stream, string? password = null); /// /// From a passed in archive (zip, rar, 7z, 001), return all parts. diff --git a/src/SharpCompress/Factories/RarFactory.cs b/src/SharpCompress/Factories/RarFactory.cs index 61099905..cfdb7ff2 100644 --- a/src/SharpCompress/Factories/RarFactory.cs +++ b/src/SharpCompress/Factories/RarFactory.cs @@ -29,11 +29,8 @@ public class RarFactory : Factory, IArchiveFactory, IMultiArchiveFactory, IReade } /// - public override bool IsArchive( - Stream stream, - string? password = null, - int bufferSize = ReaderOptions.DefaultBufferSize - ) => RarArchive.IsRarFile(stream); + public override bool IsArchive(Stream stream, string? password = null) => + RarArchive.IsRarFile(stream); /// public override FileInfo? GetFilePart(int index, FileInfo part1) => diff --git a/src/SharpCompress/Factories/SevenZipFactory.cs b/src/SharpCompress/Factories/SevenZipFactory.cs index 18dedbfd..84b1d8e9 100644 --- a/src/SharpCompress/Factories/SevenZipFactory.cs +++ b/src/SharpCompress/Factories/SevenZipFactory.cs @@ -28,11 +28,8 @@ public class SevenZipFactory : Factory, IArchiveFactory, IMultiArchiveFactory } /// - public override bool IsArchive( - Stream stream, - string? password = null, - int bufferSize = ReaderOptions.DefaultBufferSize - ) => SevenZipArchive.IsSevenZipFile(stream); + public override bool IsArchive(Stream stream, string? password = null) => + SevenZipArchive.IsSevenZipFile(stream); #endregion diff --git a/src/SharpCompress/Factories/TarFactory.cs b/src/SharpCompress/Factories/TarFactory.cs index d32020fd..225270d8 100644 --- a/src/SharpCompress/Factories/TarFactory.cs +++ b/src/SharpCompress/Factories/TarFactory.cs @@ -53,11 +53,8 @@ public class TarFactory } /// - public override bool IsArchive( - Stream stream, - string? password = null, - int bufferSize = ReaderOptions.DefaultBufferSize - ) => TarArchive.IsTarFile(stream); + public override bool IsArchive(Stream stream, string? password = null) => + TarArchive.IsTarFile(stream); #endregion diff --git a/src/SharpCompress/Factories/ZStandardFactory.cs b/src/SharpCompress/Factories/ZStandardFactory.cs index a5c6d84f..7a26b27a 100644 --- a/src/SharpCompress/Factories/ZStandardFactory.cs +++ b/src/SharpCompress/Factories/ZStandardFactory.cs @@ -20,9 +20,6 @@ internal class ZStandardFactory : Factory yield return "zstd"; } - public override bool IsArchive( - Stream stream, - string? password = null, - int bufferSize = 65536 - ) => ZStandardStream.IsZStandard(stream); + public override bool IsArchive(Stream stream, string? password = null) => + ZStandardStream.IsZStandard(stream); } diff --git a/src/SharpCompress/Factories/ZipFactory.cs b/src/SharpCompress/Factories/ZipFactory.cs index 5c2fcad8..1e9e06ea 100644 --- a/src/SharpCompress/Factories/ZipFactory.cs +++ b/src/SharpCompress/Factories/ZipFactory.cs @@ -39,11 +39,7 @@ public class ZipFactory } /// - public override bool IsArchive( - Stream stream, - string? password = null, - int bufferSize = ReaderOptions.DefaultBufferSize - ) + public override bool IsArchive(Stream stream, string? password = null) { var startPosition = stream.CanSeek ? stream.Position : -1; @@ -51,10 +47,10 @@ public class ZipFactory if (stream is not SharpCompressStream) // wrap to provide buffer bef { - stream = new SharpCompressStream(stream, bufferSize: bufferSize); + stream = new SharpCompressStream(stream, bufferSize: Constants.BufferSize); } - if (ZipArchive.IsZipFile(stream, password, bufferSize)) + if (ZipArchive.IsZipFile(stream, password)) { return true; } @@ -69,7 +65,7 @@ public class ZipFactory stream.Position = startPosition; //test the zip (last) file of a multipart zip - if (ZipArchive.IsZipMulti(stream, password, bufferSize)) + if (ZipArchive.IsZipMulti(stream, password)) { return true; } diff --git a/src/SharpCompress/Readers/AbstractReader.cs b/src/SharpCompress/Readers/AbstractReader.cs index cd37bb5f..c822393e 100644 --- a/src/SharpCompress/Readers/AbstractReader.cs +++ b/src/SharpCompress/Readers/AbstractReader.cs @@ -262,7 +262,7 @@ public abstract class AbstractReader : IReader { using Stream s = OpenEntryStream(); var sourceStream = WrapWithProgress(s, Entry); - sourceStream.CopyTo(writeStream, 81920); + sourceStream.CopyTo(writeStream, Constants.BufferSize); } internal async Task WriteAsync(Stream writeStream, CancellationToken cancellationToken) @@ -270,11 +270,15 @@ public abstract class AbstractReader : IReader #if NETFRAMEWORK || NETSTANDARD2_0 using Stream s = OpenEntryStream(); var sourceStream = WrapWithProgress(s, Entry); - await sourceStream.CopyToAsync(writeStream, 81920, cancellationToken).ConfigureAwait(false); + await sourceStream + .CopyToAsync(writeStream, Constants.BufferSize, cancellationToken) + .ConfigureAwait(false); #else await using Stream s = OpenEntryStream(); var sourceStream = WrapWithProgress(s, Entry); - await sourceStream.CopyToAsync(writeStream, 81920, cancellationToken).ConfigureAwait(false); + await sourceStream + .CopyToAsync(writeStream, Constants.BufferSize, cancellationToken) + .ConfigureAwait(false); #endif } diff --git a/src/SharpCompress/Readers/ReaderOptions.cs b/src/SharpCompress/Readers/ReaderOptions.cs index cedf8bed..49a91e93 100644 --- a/src/SharpCompress/Readers/ReaderOptions.cs +++ b/src/SharpCompress/Readers/ReaderOptions.cs @@ -5,8 +5,6 @@ namespace SharpCompress.Readers; public class ReaderOptions : OptionsBase { - public const int DefaultBufferSize = 0x10000; - /// /// Look for RarArchive (Check for self-extracting archives or cases where RarArchive isn't at the start of the file) /// @@ -16,7 +14,7 @@ public class ReaderOptions : OptionsBase public bool DisableCheckIncomplete { get; set; } - public int BufferSize { get; set; } = DefaultBufferSize; + public int BufferSize { get; set; } = Constants.BufferSize; /// /// Provide a hint for the extension of the archive being read, can speed up finding the correct decoder. Should be without the leading period in the form like: tar.gz or zip diff --git a/src/SharpCompress/Utility.cs b/src/SharpCompress/Utility.cs index 8f2d6788..bf9d91e1 100644 --- a/src/SharpCompress/Utility.cs +++ b/src/SharpCompress/Utility.cs @@ -11,8 +11,6 @@ namespace SharpCompress; internal static class Utility { - //80kb is a good industry standard temporary buffer size - private const int TEMP_BUFFER_SIZE = 81920; private static readonly HashSet invalidChars = new(Path.GetInvalidFileNameChars()); public static ReadOnlyCollection ToReadOnly(this IList items) => new(items); @@ -151,7 +149,7 @@ internal static class Utility public static long TransferTo(this Stream source, Stream destination, long maxLength) { - var array = ArrayPool.Shared.Rent(TEMP_BUFFER_SIZE); + var array = ArrayPool.Shared.Rent(Common.Constants.BufferSize); try { var maxReadSize = array.Length; @@ -190,7 +188,7 @@ internal static class Utility CancellationToken cancellationToken = default ) { - var array = ArrayPool.Shared.Rent(TEMP_BUFFER_SIZE); + var array = ArrayPool.Shared.Rent(Common.Constants.BufferSize); try { var maxReadSize = array.Length; @@ -268,7 +266,7 @@ internal static class Utility return; } - var array = ArrayPool.Shared.Rent(TEMP_BUFFER_SIZE); + var array = ArrayPool.Shared.Rent(Common.Constants.BufferSize); try { while (advanceAmount > 0) diff --git a/src/SharpCompress/Writers/GZip/GZipWriter.cs b/src/SharpCompress/Writers/GZip/GZipWriter.cs index ad1d59d7..68353284 100644 --- a/src/SharpCompress/Writers/GZip/GZipWriter.cs +++ b/src/SharpCompress/Writers/GZip/GZipWriter.cs @@ -48,7 +48,7 @@ public sealed class GZipWriter : AbstractWriter stream.FileName = filename; stream.LastModified = modificationTime; var progressStream = WrapWithProgress(source, filename); - progressStream.CopyTo(stream); + progressStream.CopyTo(stream, Constants.BufferSize); _wroteToStream = true; } diff --git a/src/SharpCompress/Writers/Zip/ZipWriter.cs b/src/SharpCompress/Writers/Zip/ZipWriter.cs index 8c4b96b6..e11ac01d 100644 --- a/src/SharpCompress/Writers/Zip/ZipWriter.cs +++ b/src/SharpCompress/Writers/Zip/ZipWriter.cs @@ -15,6 +15,7 @@ using SharpCompress.Compressors.LZMA; using SharpCompress.Compressors.PPMd; using SharpCompress.Compressors.ZStandard; using SharpCompress.IO; +using Constants = SharpCompress.Common.Constants; namespace SharpCompress.Writers.Zip; @@ -87,7 +88,7 @@ public class ZipWriter : AbstractWriter { using var output = WriteToStream(entryPath, zipWriterEntryOptions); var progressStream = WrapWithProgress(source, entryPath); - progressStream.CopyTo(output); + progressStream.CopyTo(output, Constants.BufferSize); } public Stream WriteToStream(string entryPath, ZipWriterEntryOptions options) diff --git a/src/SharpCompress/Writers/Zip/ZipWriterEntryOptions.cs b/src/SharpCompress/Writers/Zip/ZipWriterEntryOptions.cs index dcadb21c..f532a473 100644 --- a/src/SharpCompress/Writers/Zip/ZipWriterEntryOptions.cs +++ b/src/SharpCompress/Writers/Zip/ZipWriterEntryOptions.cs @@ -4,7 +4,7 @@ using SharpCompress.Compressors.Deflate; namespace SharpCompress.Writers.Zip; -public class ZipWriterEntryOptions +public class ZipWriterEntryOptions : OptionsBase { public CompressionType? CompressionType { get; set; } diff --git a/tests/SharpCompress.Test/Mocks/ForwardOnlyStream.cs b/tests/SharpCompress.Test/Mocks/ForwardOnlyStream.cs index 064d2066..dd9c76dc 100644 --- a/tests/SharpCompress.Test/Mocks/ForwardOnlyStream.cs +++ b/tests/SharpCompress.Test/Mocks/ForwardOnlyStream.cs @@ -2,8 +2,8 @@ using System; using System.IO; using System.Threading; using System.Threading.Tasks; +using SharpCompress.Common; using SharpCompress.IO; -using SharpCompress.Readers; namespace SharpCompress.Test.Mocks; @@ -31,8 +31,8 @@ public class ForwardOnlyStream : SharpCompressStream, IStreamStack public bool IsDisposed { get; private set; } - public ForwardOnlyStream(Stream stream, int bufferSize = ReaderOptions.DefaultBufferSize) - : base(stream, bufferSize: bufferSize) + public ForwardOnlyStream(Stream stream, int? bufferSize = null) + : base(stream, bufferSize: bufferSize ?? Constants.BufferSize) { this.stream = stream; #if DEBUG_STREAMS diff --git a/tests/SharpCompress.Test/packages.lock.json b/tests/SharpCompress.Test/packages.lock.json index 7f87d400..d36081cd 100644 --- a/tests/SharpCompress.Test/packages.lock.json +++ b/tests/SharpCompress.Test/packages.lock.json @@ -29,12 +29,6 @@ "Microsoft.NETFramework.ReferenceAssemblies.net48": "1.0.3" } }, - "Mono.Posix.NETStandard": { - "type": "Direct", - "requested": "[1.0.0, )", - "resolved": "1.0.0", - "contentHash": "vSN/L1uaVwKsiLa95bYu2SGkF0iY3xMblTfxc8alSziPuVfJpj3geVqHGAA75J7cZkMuKpFVikz82Lo6y6LLdA==" - }, "xunit": { "type": "Direct", "requested": "[2.9.3, )", @@ -222,12 +216,6 @@ "Microsoft.NETFramework.ReferenceAssemblies.net461": "1.0.3" } }, - "Mono.Posix.NETStandard": { - "type": "Direct", - "requested": "[1.0.0, )", - "resolved": "1.0.0", - "contentHash": "vSN/L1uaVwKsiLa95bYu2SGkF0iY3xMblTfxc8alSziPuVfJpj3geVqHGAA75J7cZkMuKpFVikz82Lo6y6LLdA==" - }, "xunit": { "type": "Direct", "requested": "[2.9.3, )", From d52facd4ab139b0b9de8eb95e54b9facf2284ac8 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Tue, 27 Jan 2026 10:48:32 +0000 Subject: [PATCH 02/21] Remove change --- src/SharpCompress/Writers/Zip/ZipWriterEntryOptions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SharpCompress/Writers/Zip/ZipWriterEntryOptions.cs b/src/SharpCompress/Writers/Zip/ZipWriterEntryOptions.cs index f532a473..dcadb21c 100644 --- a/src/SharpCompress/Writers/Zip/ZipWriterEntryOptions.cs +++ b/src/SharpCompress/Writers/Zip/ZipWriterEntryOptions.cs @@ -4,7 +4,7 @@ using SharpCompress.Compressors.Deflate; namespace SharpCompress.Writers.Zip; -public class ZipWriterEntryOptions : OptionsBase +public class ZipWriterEntryOptions { public CompressionType? CompressionType { get; set; } From 014bbc3ea480e7cdcf07b0842c398ca42b49952e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 11:47:52 +0000 Subject: [PATCH 03/21] Initial plan From a11415518911db25c5a7810dbd2e9599a933c4ba Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 11:48:05 +0000 Subject: [PATCH 04/21] Initial plan From 6c866324b2d1d61c2aed78e189adb479e6a8d3a0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 11:49:29 +0000 Subject: [PATCH 05/21] Fix grammatical errors in ArcFactory comments Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com> --- src/SharpCompress/Factories/ArcFactory.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/SharpCompress/Factories/ArcFactory.cs b/src/SharpCompress/Factories/ArcFactory.cs index c9611d7c..469ee136 100644 --- a/src/SharpCompress/Factories/ArcFactory.cs +++ b/src/SharpCompress/Factories/ArcFactory.cs @@ -26,11 +26,11 @@ namespace SharpCompress.Factories public override bool IsArchive(Stream stream, string? password = null) { //You may have to use some(paranoid) checks to ensure that you actually are - //processing an ARC file, since other archivers also adopted to the idea of putting - //a 01Ah byte at offset 0, namely: Hyper archiver. To check if you have a + //processing an ARC file, since other archivers also adopted the idea of putting + //a 01Ah byte at offset 0, namely the Hyper archiver. To check if you have a //Hyper - archive, check the next two bytes for "HP" or "ST"(or look below for - //"HYP").Also, ZOO archiver also does put a 01Ah at the start of the file, - //see: ZOO entry below. + //"HYP").Also the ZOO archiver also does put a 01Ah at the start of the file, + //see the ZOO entry below. var bytes = new byte[2]; stream.Read(bytes, 0, 2); return bytes[0] == 0x1A && bytes[1] < 10; //rather thin, but this is all we have From 53f12d75db91be1d1c38773cea93a516c18e916b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 11:53:37 +0000 Subject: [PATCH 06/21] Add [Obsolete] attribute to ReaderOptions.DefaultBufferSize Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com> --- src/SharpCompress/Readers/ReaderOptions.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/SharpCompress/Readers/ReaderOptions.cs b/src/SharpCompress/Readers/ReaderOptions.cs index 49a91e93..6998ba9c 100644 --- a/src/SharpCompress/Readers/ReaderOptions.cs +++ b/src/SharpCompress/Readers/ReaderOptions.cs @@ -5,6 +5,14 @@ namespace SharpCompress.Readers; public class ReaderOptions : OptionsBase { + /// + /// The default buffer size for stream operations. + /// + [Obsolete( + "Use Constants.BufferSize instead. This constant will be removed in a future version." + )] + public const int DefaultBufferSize = 0x10000; + /// /// Look for RarArchive (Check for self-extracting archives or cases where RarArchive isn't at the start of the file) /// From 010a38bb7369ccb646479dd638262a2082522575 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 11:54:50 +0000 Subject: [PATCH 07/21] Add clarifying comment about buffer size value difference Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com> --- src/SharpCompress/Readers/ReaderOptions.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/SharpCompress/Readers/ReaderOptions.cs b/src/SharpCompress/Readers/ReaderOptions.cs index 6998ba9c..b016a3b6 100644 --- a/src/SharpCompress/Readers/ReaderOptions.cs +++ b/src/SharpCompress/Readers/ReaderOptions.cs @@ -7,6 +7,8 @@ public class ReaderOptions : OptionsBase { /// /// The default buffer size for stream operations. + /// This value (65536 bytes) is preserved for backward compatibility. + /// New code should use Constants.BufferSize instead (81920 bytes), which matches .NET's Stream.CopyTo default. /// [Obsolete( "Use Constants.BufferSize instead. This constant will be removed in a future version." From d6156f0f1e617ef1757629aa3f2e1b0669b87d83 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Tue, 27 Jan 2026 12:14:03 +0000 Subject: [PATCH 08/21] release branch builds increment patch versions and master builds increment minor versions --- build/Program.cs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/build/Program.cs b/build/Program.cs index 47a86745..e14e1304 100644 --- a/build/Program.cs +++ b/build/Program.cs @@ -240,8 +240,22 @@ static async Task<(string version, bool isPrerelease)> GetVersion() var lastTag = allTags.OrderBy(tag => Version.Parse(tag)).LastOrDefault() ?? "0.0.0"; var lastVersion = Version.Parse(lastTag); - // Increment minor version for next release - var nextVersion = new Version(lastVersion.Major, lastVersion.Minor + 1, 0); + // Determine version increment based on branch + var currentBranch = await GetCurrentBranch(); + Version nextVersion; + + if (currentBranch == "release") + { + // Release branch: increment patch version + nextVersion = new Version(lastVersion.Major, lastVersion.Minor, lastVersion.Build + 1); + Console.WriteLine($"Building prerelease for release branch (patch increment)"); + } + else + { + // Master or other branches: increment minor version + nextVersion = new Version(lastVersion.Major, lastVersion.Minor + 1, 0); + Console.WriteLine($"Building prerelease for {currentBranch} branch (minor increment)"); + } // Use commit count since the last version tag if available; otherwise, fall back to total count var revListArgs = allTags.Any() ? $"--count {lastTag}..HEAD" : "--count HEAD"; From c82744c51c9e61d2f93b3a98a6cf3319ab775a01 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Tue, 27 Jan 2026 12:15:31 +0000 Subject: [PATCH 09/21] fmt --- build/Program.cs | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/build/Program.cs b/build/Program.cs index e14e1304..1ad21f50 100644 --- a/build/Program.cs +++ b/build/Program.cs @@ -230,7 +230,7 @@ static async Task<(string version, bool isPrerelease)> GetVersion() } else { - // Not tagged - create prerelease version based on next minor version + // Not tagged - create prerelease version var allTags = (await GetGitOutput("tag", "--list")) .Split('\n', StringSplitOptions.RemoveEmptyEntries) .Where(tag => Regex.IsMatch(tag.Trim(), @"^\d+\.\d+\.\d+$")) @@ -267,6 +267,28 @@ static async Task<(string version, bool isPrerelease)> GetVersion() } } +static async Task GetCurrentBranch() +{ + // In GitHub Actions, GITHUB_REF_NAME contains the branch name + var githubRefName = Environment.GetEnvironmentVariable("GITHUB_REF_NAME"); + if (!string.IsNullOrEmpty(githubRefName)) + { + return githubRefName; + } + + // Fallback to git command for local builds + try + { + var (output, _) = await ReadAsync("git", "branch --show-current"); + return output.Trim(); + } + catch (Exception ex) + { + Console.WriteLine($"Warning: Could not determine current branch: {ex.Message}"); + return "unknown"; + } +} + static async Task GetGitOutput(string command, string args) { try From 4d31436740a1b2a752718db0062569f8053a931b Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Tue, 27 Jan 2026 12:39:01 +0000 Subject: [PATCH 10/21] constant should be a static property --- src/SharpCompress/Common/Constants.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SharpCompress/Common/Constants.cs b/src/SharpCompress/Common/Constants.cs index 08653fc6..1e6d57f5 100644 --- a/src/SharpCompress/Common/Constants.cs +++ b/src/SharpCompress/Common/Constants.cs @@ -6,5 +6,5 @@ public static class Constants /// The default buffer size for stream operations, matching .NET's Stream.CopyTo default of 81920 bytes. /// This can be modified globally at runtime. /// - public static int BufferSize = 81920; + public static int BufferSize { get; set; } = 81920; } From 970934a40b7e6d56184356c8412853966f7f30f3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 15:51:50 +0000 Subject: [PATCH 11/21] Initial plan From a706a9d725e613c5ca79acf0fdaf1b07eec4b7c5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 16:00:44 +0000 Subject: [PATCH 12/21] Fix ZIP parsing regression with short reads on non-seekable streams Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com> --- src/SharpCompress/IO/SharpCompressStream.cs | 113 +++++++++++++---- .../Zip/ZipShortReadTests.cs | 117 ++++++++++++++++++ tests/SharpCompress.Test/packages.lock.json | 12 ++ 3 files changed, 216 insertions(+), 26 deletions(-) create mode 100644 tests/SharpCompress.Test/Zip/ZipShortReadTests.cs diff --git a/src/SharpCompress/IO/SharpCompressStream.cs b/src/SharpCompress/IO/SharpCompressStream.cs index 101f7b16..59a3c52d 100644 --- a/src/SharpCompress/IO/SharpCompressStream.cs +++ b/src/SharpCompress/IO/SharpCompressStream.cs @@ -206,11 +206,11 @@ public class SharpCompressStream : Stream, IStreamStack { ValidateBufferState(); - // Fill buffer if needed + // Fill buffer if needed, handling short reads from underlying stream if (_bufferedLength == 0) { - _bufferedLength = Stream.Read(_buffer!, 0, _bufferSize); _bufferPosition = 0; + _bufferedLength = FillBuffer(_buffer!, 0, _bufferSize); } int available = _bufferedLength - _bufferPosition; int toRead = Math.Min(count, available); @@ -222,11 +222,8 @@ public class SharpCompressStream : Stream, IStreamStack return toRead; } // If buffer exhausted, refill - int r = Stream.Read(_buffer!, 0, _bufferSize); - if (r == 0) - return 0; - _bufferedLength = r; _bufferPosition = 0; + _bufferedLength = FillBuffer(_buffer!, 0, _bufferSize); if (_bufferedLength == 0) { return 0; @@ -250,6 +247,30 @@ public class SharpCompressStream : Stream, IStreamStack } } + /// + /// Fills the buffer by reading from the underlying stream, handling short reads. + /// Continues reading until the buffer is full or EOF is reached. + /// This ensures that buffering properly handles non-seekable streams that return short reads. + /// + /// Buffer to fill + /// Offset in buffer + /// Number of bytes to read + /// Total number of bytes read + private int FillBuffer(byte[] buffer, int offset, int count) + { + int totalRead = 0; + while (totalRead < count) + { + int read = Stream.Read(buffer, offset + totalRead, count - totalRead); + if (read == 0) + { + break; // EOF reached + } + totalRead += read; + } + return totalRead; + } + public override long Seek(long offset, SeekOrigin origin) { if (_bufferingEnabled) @@ -324,13 +345,12 @@ public class SharpCompressStream : Stream, IStreamStack { ValidateBufferState(); - // Fill buffer if needed + // Fill buffer if needed, handling short reads from underlying stream if (_bufferedLength == 0) { - _bufferedLength = await Stream - .ReadAsync(_buffer!, 0, _bufferSize, cancellationToken) - .ConfigureAwait(false); _bufferPosition = 0; + _bufferedLength = await FillBufferAsync(_buffer!, 0, _bufferSize, cancellationToken) + .ConfigureAwait(false); } int available = _bufferedLength - _bufferPosition; int toRead = Math.Min(count, available); @@ -342,13 +362,9 @@ public class SharpCompressStream : Stream, IStreamStack return toRead; } // If buffer exhausted, refill - int r = await Stream - .ReadAsync(_buffer!, 0, _bufferSize, cancellationToken) - .ConfigureAwait(false); - if (r == 0) - return 0; - _bufferedLength = r; _bufferPosition = 0; + _bufferedLength = await FillBufferAsync(_buffer!, 0, _bufferSize, cancellationToken) + .ConfigureAwait(false); if (_bufferedLength == 0) { return 0; @@ -369,6 +385,32 @@ public class SharpCompressStream : Stream, IStreamStack } } + /// + /// Async version of FillBuffer. Fills the buffer by reading from the underlying stream, handling short reads. + /// Continues reading until the buffer is full or EOF is reached. + /// + private async Task FillBufferAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken + ) + { + int totalRead = 0; + while (totalRead < count) + { + int read = await Stream + .ReadAsync(buffer, offset + totalRead, count - totalRead, cancellationToken) + .ConfigureAwait(false); + if (read == 0) + { + break; // EOF reached + } + totalRead += read; + } + return totalRead; + } + public override async Task WriteAsync( byte[] buffer, int offset, @@ -399,13 +441,12 @@ public class SharpCompressStream : Stream, IStreamStack { ValidateBufferState(); - // Fill buffer if needed + // Fill buffer if needed, handling short reads from underlying stream if (_bufferedLength == 0) { - _bufferedLength = await Stream - .ReadAsync(_buffer.AsMemory(0, _bufferSize), cancellationToken) - .ConfigureAwait(false); _bufferPosition = 0; + _bufferedLength = await FillBufferMemoryAsync(_buffer.AsMemory(0, _bufferSize), cancellationToken) + .ConfigureAwait(false); } int available = _bufferedLength - _bufferPosition; int toRead = Math.Min(buffer.Length, available); @@ -417,13 +458,9 @@ public class SharpCompressStream : Stream, IStreamStack return toRead; } // If buffer exhausted, refill - int r = await Stream - .ReadAsync(_buffer.AsMemory(0, _bufferSize), cancellationToken) - .ConfigureAwait(false); - if (r == 0) - return 0; - _bufferedLength = r; _bufferPosition = 0; + _bufferedLength = await FillBufferMemoryAsync(_buffer.AsMemory(0, _bufferSize), cancellationToken) + .ConfigureAwait(false); if (_bufferedLength == 0) { return 0; @@ -442,6 +479,30 @@ public class SharpCompressStream : Stream, IStreamStack } } + /// + /// Async version of FillBuffer for Memory{byte}. Fills the buffer by reading from the underlying stream, handling short reads. + /// Continues reading until the buffer is full or EOF is reached. + /// + private async ValueTask FillBufferMemoryAsync( + Memory buffer, + CancellationToken cancellationToken + ) + { + int totalRead = 0; + while (totalRead < buffer.Length) + { + int read = await Stream + .ReadAsync(buffer.Slice(totalRead), cancellationToken) + .ConfigureAwait(false); + if (read == 0) + { + break; // EOF reached + } + totalRead += read; + } + return totalRead; + } + public override async ValueTask WriteAsync( ReadOnlyMemory buffer, CancellationToken cancellationToken = default diff --git a/tests/SharpCompress.Test/Zip/ZipShortReadTests.cs b/tests/SharpCompress.Test/Zip/ZipShortReadTests.cs new file mode 100644 index 00000000..c3904331 --- /dev/null +++ b/tests/SharpCompress.Test/Zip/ZipShortReadTests.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using SharpCompress.Common; +using SharpCompress.IO; +using SharpCompress.Readers; +using SharpCompress.Readers.Zip; +using SharpCompress.Test.Mocks; +using SharpCompress.Writers; +using Xunit; + +namespace SharpCompress.Test.Zip; + +/// +/// Tests for ZIP reading with streams that return short reads. +/// Reproduces the regression where ZIP parsing fails depending on Stream.Read chunking patterns. +/// +public class ZipShortReadTests : ReaderTests +{ + /// + /// A non-seekable stream that returns controlled short reads. + /// Simulates real-world network/multipart streams that legally return fewer bytes than requested. + /// + private sealed class PatternReadStream : Stream + { + private readonly MemoryStream _inner; + private readonly int _firstReadSize; + private readonly int _chunkSize; + private bool _firstReadDone; + + public PatternReadStream(byte[] bytes, int firstReadSize, int chunkSize) + { + _inner = new MemoryStream(bytes, writable: false); + _firstReadSize = firstReadSize; + _chunkSize = chunkSize; + } + + public override int Read(byte[] buffer, int offset, int count) + { + int limit = !_firstReadDone ? _firstReadSize : _chunkSize; + _firstReadDone = true; + + int toRead = Math.Min(count, limit); + return _inner.Read(buffer, offset, toRead); + } + + public override bool CanRead => true; + public override bool CanSeek => false; + public override bool CanWrite => false; + public override long Length => throw new NotSupportedException(); + public override long Position + { + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); + } + public override void Flush() => throw new NotSupportedException(); + 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(); + } + + /// + /// Test that ZIP reading works correctly with short reads on non-seekable streams. + /// Uses a test archive and different chunking patterns. + /// + [Theory] + [InlineData("Zip.deflate.zip", 1000, 4096)] + [InlineData("Zip.deflate.zip", 999, 4096)] + [InlineData("Zip.deflate.zip", 100, 4096)] + [InlineData("Zip.deflate.zip", 50, 512)] + [InlineData("Zip.deflate.zip", 1, 1)] // Extreme case: 1 byte at a time + [InlineData("Zip.deflate.dd.zip", 1000, 4096)] + [InlineData("Zip.deflate.dd.zip", 999, 4096)] + [InlineData("Zip.zip64.zip", 3816, 4096)] + [InlineData("Zip.zip64.zip", 3815, 4096)] // Similar to the issue pattern + public void Zip_Reader_Handles_Short_Reads(string zipFile, int firstReadSize, int chunkSize) + { + // Use an existing test ZIP file + var zipPath = Path.Combine(TEST_ARCHIVES_PATH, zipFile); + if (!File.Exists(zipPath)) + { + return; // Skip if file doesn't exist + } + + var bytes = File.ReadAllBytes(zipPath); + + // Baseline with MemoryStream (seekable, no short reads) + var baseline = ReadEntriesFromStream(new MemoryStream(bytes, writable: false)); + Assert.NotEmpty(baseline); + + // Non-seekable stream with controlled short read pattern + var chunked = ReadEntriesFromStream(new PatternReadStream(bytes, firstReadSize, chunkSize)); + Assert.Equal(baseline, chunked); + } + + private List ReadEntriesFromStream(Stream stream) + { + var names = new List(); + using var reader = ReaderFactory.Open(stream, new ReaderOptions { LeaveStreamOpen = true }); + + while (reader.MoveToNextEntry()) + { + if (reader.Entry.IsDirectory) + { + continue; + } + + names.Add(reader.Entry.Key!); + + using var entryStream = reader.OpenEntryStream(); + entryStream.CopyTo(Stream.Null); + } + + return names; + } +} diff --git a/tests/SharpCompress.Test/packages.lock.json b/tests/SharpCompress.Test/packages.lock.json index d36081cd..7f87d400 100644 --- a/tests/SharpCompress.Test/packages.lock.json +++ b/tests/SharpCompress.Test/packages.lock.json @@ -29,6 +29,12 @@ "Microsoft.NETFramework.ReferenceAssemblies.net48": "1.0.3" } }, + "Mono.Posix.NETStandard": { + "type": "Direct", + "requested": "[1.0.0, )", + "resolved": "1.0.0", + "contentHash": "vSN/L1uaVwKsiLa95bYu2SGkF0iY3xMblTfxc8alSziPuVfJpj3geVqHGAA75J7cZkMuKpFVikz82Lo6y6LLdA==" + }, "xunit": { "type": "Direct", "requested": "[2.9.3, )", @@ -216,6 +222,12 @@ "Microsoft.NETFramework.ReferenceAssemblies.net461": "1.0.3" } }, + "Mono.Posix.NETStandard": { + "type": "Direct", + "requested": "[1.0.0, )", + "resolved": "1.0.0", + "contentHash": "vSN/L1uaVwKsiLa95bYu2SGkF0iY3xMblTfxc8alSziPuVfJpj3geVqHGAA75J7cZkMuKpFVikz82Lo6y6LLdA==" + }, "xunit": { "type": "Direct", "requested": "[2.9.3, )", From 71655e04c474d3d93678a02401954e5d4e3e5d9e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 16:02:26 +0000 Subject: [PATCH 13/21] Apply code formatting with CSharpier Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com> --- src/SharpCompress/IO/SharpCompressStream.cs | 10 ++++++++-- .../SharpCompress.Test/Zip/ZipShortReadTests.cs | 16 +++++++++++----- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/SharpCompress/IO/SharpCompressStream.cs b/src/SharpCompress/IO/SharpCompressStream.cs index 59a3c52d..e90a1b88 100644 --- a/src/SharpCompress/IO/SharpCompressStream.cs +++ b/src/SharpCompress/IO/SharpCompressStream.cs @@ -445,7 +445,10 @@ public class SharpCompressStream : Stream, IStreamStack if (_bufferedLength == 0) { _bufferPosition = 0; - _bufferedLength = await FillBufferMemoryAsync(_buffer.AsMemory(0, _bufferSize), cancellationToken) + _bufferedLength = await FillBufferMemoryAsync( + _buffer.AsMemory(0, _bufferSize), + cancellationToken + ) .ConfigureAwait(false); } int available = _bufferedLength - _bufferPosition; @@ -459,7 +462,10 @@ public class SharpCompressStream : Stream, IStreamStack } // If buffer exhausted, refill _bufferPosition = 0; - _bufferedLength = await FillBufferMemoryAsync(_buffer.AsMemory(0, _bufferSize), cancellationToken) + _bufferedLength = await FillBufferMemoryAsync( + _buffer.AsMemory(0, _bufferSize), + cancellationToken + ) .ConfigureAwait(false); if (_bufferedLength == 0) { diff --git a/tests/SharpCompress.Test/Zip/ZipShortReadTests.cs b/tests/SharpCompress.Test/Zip/ZipShortReadTests.cs index c3904331..1faa97a2 100644 --- a/tests/SharpCompress.Test/Zip/ZipShortReadTests.cs +++ b/tests/SharpCompress.Test/Zip/ZipShortReadTests.cs @@ -54,10 +54,16 @@ public class ZipShortReadTests : ReaderTests get => throw new NotSupportedException(); set => throw new NotSupportedException(); } + public override void Flush() => throw new NotSupportedException(); - public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + + 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(); + + public override void Write(byte[] buffer, int offset, int count) => + throw new NotSupportedException(); } /// @@ -69,11 +75,11 @@ public class ZipShortReadTests : ReaderTests [InlineData("Zip.deflate.zip", 999, 4096)] [InlineData("Zip.deflate.zip", 100, 4096)] [InlineData("Zip.deflate.zip", 50, 512)] - [InlineData("Zip.deflate.zip", 1, 1)] // Extreme case: 1 byte at a time + [InlineData("Zip.deflate.zip", 1, 1)] // Extreme case: 1 byte at a time [InlineData("Zip.deflate.dd.zip", 1000, 4096)] [InlineData("Zip.deflate.dd.zip", 999, 4096)] [InlineData("Zip.zip64.zip", 3816, 4096)] - [InlineData("Zip.zip64.zip", 3815, 4096)] // Similar to the issue pattern + [InlineData("Zip.zip64.zip", 3815, 4096)] // Similar to the issue pattern public void Zip_Reader_Handles_Short_Reads(string zipFile, int firstReadSize, int chunkSize) { // Use an existing test ZIP file @@ -82,7 +88,7 @@ public class ZipShortReadTests : ReaderTests { return; // Skip if file doesn't exist } - + var bytes = File.ReadAllBytes(zipPath); // Baseline with MemoryStream (seekable, no short reads) From d59e4c2a0dac46892189bee9f19f54e5b6998bea Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 16:25:24 +0000 Subject: [PATCH 14/21] Refactor FillBuffer to use ReadFully pattern Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com> --- src/SharpCompress/IO/SharpCompressStream.cs | 78 ++++++++++++--------- 1 file changed, 45 insertions(+), 33 deletions(-) diff --git a/src/SharpCompress/IO/SharpCompressStream.cs b/src/SharpCompress/IO/SharpCompressStream.cs index e90a1b88..e054be97 100644 --- a/src/SharpCompress/IO/SharpCompressStream.cs +++ b/src/SharpCompress/IO/SharpCompressStream.cs @@ -249,26 +249,27 @@ public class SharpCompressStream : Stream, IStreamStack /// /// Fills the buffer by reading from the underlying stream, handling short reads. - /// Continues reading until the buffer is full or EOF is reached. - /// This ensures that buffering properly handles non-seekable streams that return short reads. + /// Implements the ReadFully pattern: reads in a loop until buffer is full or EOF is reached. /// /// Buffer to fill - /// Offset in buffer + /// Offset in buffer (always 0 in current usage) /// Number of bytes to read - /// Total number of bytes read + /// Total number of bytes read (may be less than count if EOF is reached) private int FillBuffer(byte[] buffer, int offset, int count) { - int totalRead = 0; - while (totalRead < count) + // Implement ReadFully pattern but return the actual count read + // This is the same logic as Utility.ReadFully but returns count instead of bool + var total = 0; + int read; + while ((read = Stream.Read(buffer, offset + total, count - total)) > 0) { - int read = Stream.Read(buffer, offset + totalRead, count - totalRead); - if (read == 0) + total += read; + if (total >= count) { - break; // EOF reached + return total; } - totalRead += read; } - return totalRead; + return total; } public override long Seek(long offset, SeekOrigin origin) @@ -386,8 +387,8 @@ public class SharpCompressStream : Stream, IStreamStack } /// - /// Async version of FillBuffer. Fills the buffer by reading from the underlying stream, handling short reads. - /// Continues reading until the buffer is full or EOF is reached. + /// Async version of FillBuffer. Implements the ReadFullyAsync pattern. + /// Reads in a loop until buffer is full or EOF is reached. /// private async Task FillBufferAsync( byte[] buffer, @@ -396,19 +397,25 @@ public class SharpCompressStream : Stream, IStreamStack CancellationToken cancellationToken ) { - int totalRead = 0; - while (totalRead < count) + // Implement ReadFullyAsync pattern but return the actual count read + // This is the same logic as Utility.ReadFullyAsync but returns count instead of bool + var total = 0; + int read; + while ( + ( + read = await Stream + .ReadAsync(buffer, offset + total, count - total, cancellationToken) + .ConfigureAwait(false) + ) > 0 + ) { - int read = await Stream - .ReadAsync(buffer, offset + totalRead, count - totalRead, cancellationToken) - .ConfigureAwait(false); - if (read == 0) + total += read; + if (total >= count) { - break; // EOF reached + return total; } - totalRead += read; } - return totalRead; + return total; } public override async Task WriteAsync( @@ -486,27 +493,32 @@ public class SharpCompressStream : Stream, IStreamStack } /// - /// Async version of FillBuffer for Memory{byte}. Fills the buffer by reading from the underlying stream, handling short reads. - /// Continues reading until the buffer is full or EOF is reached. + /// Async version of FillBuffer for Memory{byte}. Implements the ReadFullyAsync pattern. + /// Reads in a loop until buffer is full or EOF is reached. /// private async ValueTask FillBufferMemoryAsync( Memory buffer, CancellationToken cancellationToken ) { - int totalRead = 0; - while (totalRead < buffer.Length) + // Implement ReadFullyAsync pattern but return the actual count read + var total = 0; + int read; + while ( + ( + read = await Stream + .ReadAsync(buffer.Slice(total), cancellationToken) + .ConfigureAwait(false) + ) > 0 + ) { - int read = await Stream - .ReadAsync(buffer.Slice(totalRead), cancellationToken) - .ConfigureAwait(false); - if (read == 0) + total += read; + if (total >= buffer.Length) { - break; // EOF reached + return total; } - totalRead += read; } - return totalRead; + return total; } public override async ValueTask WriteAsync( From 8a3be35d676c7b181a3594d6ca17565f5d65e121 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Tue, 27 Jan 2026 16:43:13 +0000 Subject: [PATCH 15/21] Update tests/SharpCompress.Test/Zip/ZipShortReadTests.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tests/SharpCompress.Test/Zip/ZipShortReadTests.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/SharpCompress.Test/Zip/ZipShortReadTests.cs b/tests/SharpCompress.Test/Zip/ZipShortReadTests.cs index 1faa97a2..d61a27c7 100644 --- a/tests/SharpCompress.Test/Zip/ZipShortReadTests.cs +++ b/tests/SharpCompress.Test/Zip/ZipShortReadTests.cs @@ -1,13 +1,7 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; -using SharpCompress.Common; -using SharpCompress.IO; using SharpCompress.Readers; -using SharpCompress.Readers.Zip; -using SharpCompress.Test.Mocks; -using SharpCompress.Writers; using Xunit; namespace SharpCompress.Test.Zip; From 72eaf66f055fc39077751e8cf4cfea5b26629123 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 16:53:53 +0000 Subject: [PATCH 16/21] Initial plan From db2f5c9cb9197e1bc566055e6cf16da08e67b05d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 17:01:18 +0000 Subject: [PATCH 17/21] Fix SevenZipReader to iterate entries as contiguous streams Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com> --- .../Archives/SevenZip/SevenZipArchive.cs | 54 +++++++++++++++---- .../SevenZip/SevenZipArchiveTests.cs | 24 +++++++++ tests/SharpCompress.Test/packages.lock.json | 12 +++++ 3 files changed, 79 insertions(+), 11 deletions(-) diff --git a/src/SharpCompress/Archives/SevenZip/SevenZipArchive.cs b/src/SharpCompress/Archives/SevenZip/SevenZipArchive.cs index d9b5ba1a..1d41224a 100644 --- a/src/SharpCompress/Archives/SevenZip/SevenZipArchive.cs +++ b/src/SharpCompress/Archives/SevenZip/SevenZipArchive.cs @@ -216,6 +216,9 @@ public class SevenZipArchive : AbstractArchive this._archive = archive; @@ -231,9 +234,10 @@ public class SevenZipArchive : AbstractArchive !x.IsDirectory)) { _currentEntry = entry; @@ -243,19 +247,47 @@ public class SevenZipArchive : AbstractArchive Date: Tue, 27 Jan 2026 17:03:20 +0000 Subject: [PATCH 18/21] Remove unused _currentFolderIndex field Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com> --- src/SharpCompress/Archives/SevenZip/SevenZipArchive.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/SharpCompress/Archives/SevenZip/SevenZipArchive.cs b/src/SharpCompress/Archives/SevenZip/SevenZipArchive.cs index 1d41224a..4ca1aa3d 100644 --- a/src/SharpCompress/Archives/SevenZip/SevenZipArchive.cs +++ b/src/SharpCompress/Archives/SevenZip/SevenZipArchive.cs @@ -218,7 +218,6 @@ public class SevenZipArchive : AbstractArchive this._archive = archive; @@ -261,7 +260,6 @@ public class SevenZipArchive : AbstractArchive Date: Tue, 27 Jan 2026 17:29:44 +0000 Subject: [PATCH 19/21] Add test to verify folder stream reuse in solid archives Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com> --- .../SevenZip/SevenZipArchiveTests.cs | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/tests/SharpCompress.Test/SevenZip/SevenZipArchiveTests.cs b/tests/SharpCompress.Test/SevenZip/SevenZipArchiveTests.cs index e0690c15..c0f2d2ba 100644 --- a/tests/SharpCompress.Test/SevenZip/SevenZipArchiveTests.cs +++ b/tests/SharpCompress.Test/SevenZip/SevenZipArchiveTests.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Linq; +using System.Reflection; using SharpCompress.Archives; using SharpCompress.Archives.SevenZip; using SharpCompress.Common; @@ -275,4 +276,89 @@ public class SevenZipArchiveTests : ArchiveTests VerifyFiles(); } + + [Fact] + public void SevenZipArchive_Solid_VerifyStreamReuse() + { + // This test verifies that the folder stream is reused within each folder + // and not recreated for each entry in solid archives + var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "7Zip.solid.7z"); + using var archive = SevenZipArchive.Open(testArchive); + Assert.True(archive.IsSolid); + + using var reader = archive.ExtractAllEntries(); + + // Use reflection to access the private fields + var readerType = reader.GetType(); + var folderStreamField = readerType.GetField( + "_currentFolderStream", + BindingFlags.NonPublic | BindingFlags.Instance + ); + var currentFolderField = readerType.GetField( + "_currentFolder", + BindingFlags.NonPublic | BindingFlags.Instance + ); + + Assert.NotNull(folderStreamField); + Assert.NotNull(currentFolderField); + + Stream? currentFolderStreamInstance = null; + object? currentFolder = null; + var entryCount = 0; + var entriesInCurrentFolder = 0; + var streamRecreationsWithinFolder = 0; + + while (reader.MoveToNextEntry()) + { + if (!reader.Entry.IsDirectory) + { + // Extract the entry to trigger GetEntryStream + using var entryStream = reader.OpenEntryStream(); + var buffer = new byte[4096]; + while (entryStream.Read(buffer, 0, buffer.Length) > 0) + { + // Read the stream to completion + } + + entryCount++; + + // Get the current folder and folder stream via reflection + var folderStream = folderStreamField.GetValue(reader) as Stream; + var folder = currentFolderField.GetValue(reader); + + Assert.NotNull(folderStream); // Folder stream should exist + + // Check if we're in a new folder + if (currentFolder == null || !ReferenceEquals(currentFolder, folder)) + { + // Starting a new folder + currentFolder = folder; + currentFolderStreamInstance = folderStream; + entriesInCurrentFolder = 1; + } + else + { + // Same folder - verify stream wasn't recreated + entriesInCurrentFolder++; + + if (!ReferenceEquals(currentFolderStreamInstance, folderStream)) + { + // Stream was recreated within the same folder - this is the bug we're testing for! + streamRecreationsWithinFolder++; + } + + currentFolderStreamInstance = folderStream; + } + } + } + + // Verify we actually tested multiple entries + Assert.True(entryCount > 1, "Test should have multiple entries to verify stream reuse"); + + // The critical check: within a single folder, the stream should NEVER be recreated + Assert.Equal( + 0, + streamRecreationsWithinFolder + ); // Folder stream should remain the same for all entries in the same folder + } } From 8a67d501a8164f1580d0e556f5594d05f4833cbd Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Wed, 28 Jan 2026 08:10:06 +0000 Subject: [PATCH 20/21] Don't use reflection in tests --- .../Archives/SevenZip/SevenZipArchive.cs | 21 ++++++++++++++- .../SevenZip/SevenZipArchiveTests.cs | 26 ++++--------------- 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/src/SharpCompress/Archives/SevenZip/SevenZipArchive.cs b/src/SharpCompress/Archives/SevenZip/SevenZipArchive.cs index 4ca1aa3d..72787434 100644 --- a/src/SharpCompress/Archives/SevenZip/SevenZipArchive.cs +++ b/src/SharpCompress/Archives/SevenZip/SevenZipArchive.cs @@ -212,13 +212,32 @@ public class SevenZipArchive : AbstractArchive _database?._packSizes.Aggregate(0L, (total, packSize) => total + packSize) ?? 0; - private sealed class SevenZipReader : AbstractReader + internal sealed class SevenZipReader : AbstractReader { private readonly SevenZipArchive _archive; private SevenZipEntry? _currentEntry; private Stream? _currentFolderStream; private CFolder? _currentFolder; + /// + /// Enables internal diagnostics for tests. + /// When disabled (default), diagnostics properties return null to avoid exposing internal state. + /// + internal bool DiagnosticsEnabled { get; set; } + + /// + /// Current folder instance used to decide whether the solid folder stream should be reused. + /// Only available when is true. + /// + internal object? DiagnosticsCurrentFolder => DiagnosticsEnabled ? _currentFolder : null; + + /// + /// Current shared folder stream instance. + /// Only available when is true. + /// + internal Stream? DiagnosticsCurrentFolderStream => + DiagnosticsEnabled ? _currentFolderStream : null; + internal SevenZipReader(ReaderOptions readerOptions, SevenZipArchive archive) : base(readerOptions, ArchiveType.SevenZip) => this._archive = archive; diff --git a/tests/SharpCompress.Test/SevenZip/SevenZipArchiveTests.cs b/tests/SharpCompress.Test/SevenZip/SevenZipArchiveTests.cs index c0f2d2ba..38c33eaa 100644 --- a/tests/SharpCompress.Test/SevenZip/SevenZipArchiveTests.cs +++ b/tests/SharpCompress.Test/SevenZip/SevenZipArchiveTests.cs @@ -1,7 +1,6 @@ using System; using System.IO; using System.Linq; -using System.Reflection; using SharpCompress.Archives; using SharpCompress.Archives.SevenZip; using SharpCompress.Common; @@ -288,19 +287,8 @@ public class SevenZipArchiveTests : ArchiveTests using var reader = archive.ExtractAllEntries(); - // Use reflection to access the private fields - var readerType = reader.GetType(); - var folderStreamField = readerType.GetField( - "_currentFolderStream", - BindingFlags.NonPublic | BindingFlags.Instance - ); - var currentFolderField = readerType.GetField( - "_currentFolder", - BindingFlags.NonPublic | BindingFlags.Instance - ); - - Assert.NotNull(folderStreamField); - Assert.NotNull(currentFolderField); + var sevenZipReader = Assert.IsType(reader); + sevenZipReader.DiagnosticsEnabled = true; Stream? currentFolderStreamInstance = null; object? currentFolder = null; @@ -322,9 +310,8 @@ public class SevenZipArchiveTests : ArchiveTests entryCount++; - // Get the current folder and folder stream via reflection - var folderStream = folderStreamField.GetValue(reader) as Stream; - var folder = currentFolderField.GetValue(reader); + var folderStream = sevenZipReader.DiagnosticsCurrentFolderStream; + var folder = sevenZipReader.DiagnosticsCurrentFolder; Assert.NotNull(folderStream); // Folder stream should exist @@ -356,9 +343,6 @@ public class SevenZipArchiveTests : ArchiveTests Assert.True(entryCount > 1, "Test should have multiple entries to verify stream reuse"); // The critical check: within a single folder, the stream should NEVER be recreated - Assert.Equal( - 0, - streamRecreationsWithinFolder - ); // Folder stream should remain the same for all entries in the same folder + Assert.Equal(0, streamRecreationsWithinFolder); // Folder stream should remain the same for all entries in the same folder } } From 484bc740d7cbfc801e6cc2d92e2114faca0ba3a2 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Wed, 28 Jan 2026 08:26:28 +0000 Subject: [PATCH 21/21] Update src/SharpCompress/Archives/SevenZip/SevenZipArchive.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/SharpCompress/Archives/SevenZip/SevenZipArchive.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/SharpCompress/Archives/SevenZip/SevenZipArchive.cs b/src/SharpCompress/Archives/SevenZip/SevenZipArchive.cs index 72787434..43618dbc 100644 --- a/src/SharpCompress/Archives/SevenZip/SevenZipArchive.cs +++ b/src/SharpCompress/Archives/SevenZip/SevenZipArchive.cs @@ -271,8 +271,15 @@ public class SevenZipArchive : AbstractArchive