diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 463b5d6fb..a77a0e278 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -23,7 +23,7 @@ jobs: env: LIB_PROJ: src/ICSharpCode.SharpZipLib/ICSharpCode.SharpZipLib.csproj steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: ref: ${{ github.events.inputs.tag }} fetch-depth: 0 @@ -79,7 +79,7 @@ jobs: DOTCOVER_PKG: jetbrains.dotcover.commandlinetools COVER_SNAPSHOT: SharpZipLib.dcvr steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: fetch-depth: 0 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 549469d55..54ce5b08b 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -39,11 +39,11 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v1 + uses: github/codeql-action/init@v2 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -54,7 +54,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v1 + uses: github/codeql-action/autobuild@v2 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -68,4 +68,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 388f7f5e9..bb6f67445 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,7 +16,7 @@ jobs: runs-on: windows-latest name: Generate DocFX documentation steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: ref: ${{ github.events.inputs.tag }} diff --git a/benchmark/ICSharpCode.SharpZipLib.Benchmark/ICSharpCode.SharpZipLib.Benchmark.csproj b/benchmark/ICSharpCode.SharpZipLib.Benchmark/ICSharpCode.SharpZipLib.Benchmark.csproj index 7688d0ff2..7fa26f80f 100644 --- a/benchmark/ICSharpCode.SharpZipLib.Benchmark/ICSharpCode.SharpZipLib.Benchmark.csproj +++ b/benchmark/ICSharpCode.SharpZipLib.Benchmark/ICSharpCode.SharpZipLib.Benchmark.csproj @@ -1,18 +1,16 @@  - - Exe - net6.0;netcoreapp3.1;net462 - + + Exe + net462;net6.0 + - - 0.12.1 - + - + diff --git a/benchmark/ICSharpCode.SharpZipLib.Benchmark/Program.cs b/benchmark/ICSharpCode.SharpZipLib.Benchmark/Program.cs index 697e2923a..3a7beebbb 100644 --- a/benchmark/ICSharpCode.SharpZipLib.Benchmark/Program.cs +++ b/benchmark/ICSharpCode.SharpZipLib.Benchmark/Program.cs @@ -9,9 +9,8 @@ public class MultipleRuntimes : ManualConfig { public MultipleRuntimes() { - AddJob(Job.Default.WithToolchain(CsProjClassicNetToolchain.Net461).AsBaseline()); // NET 4.6.1 - AddJob(Job.Default.WithToolchain(CsProjCoreToolchain.NetCoreApp21)); // .NET Core 2.1 - AddJob(Job.Default.WithToolchain(CsProjCoreToolchain.NetCoreApp31)); // .NET Core 3.1 + AddJob(Job.Default.WithToolchain(CsProjClassicNetToolchain.Net462).AsBaseline()); // NET 4.6.2 + AddJob(Job.Default.WithToolchain(CsProjCoreToolchain.NetCoreApp60)); // .NET 6.0 } } diff --git a/benchmark/ICSharpCode.SharpZipLib.Benchmark/Zip/ZipFile.cs b/benchmark/ICSharpCode.SharpZipLib.Benchmark/Zip/ZipFile.cs new file mode 100644 index 000000000..0a84e0b88 --- /dev/null +++ b/benchmark/ICSharpCode.SharpZipLib.Benchmark/Zip/ZipFile.cs @@ -0,0 +1,63 @@ +using System; +using System.IO; +using System.Net.Http; +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; +using ICSharpCode.SharpZipLib.Zip; + +namespace ICSharpCode.SharpZipLib.Benchmark.Zip +{ + [MemoryDiagnoser] + [Config(typeof(MultipleRuntimes))] + public class ZipFile + { + private readonly byte[] readBuffer = new byte[4096]; + private string zipFileWithLargeAmountOfEntriesPath; + + [GlobalSetup] + public async Task GlobalSetup() + { + SharpZipLibOptions.InflaterPoolSize = 4; + + // large real-world test file from test262 repository + string commitSha = "2e4e0e6b8ebe3348a207144204cb6d7a5571c863"; + zipFileWithLargeAmountOfEntriesPath = Path.Combine(Path.GetTempPath(), $"{commitSha}.zip"); + if (!File.Exists(zipFileWithLargeAmountOfEntriesPath)) + { + var uri = $"https://github.com/tc39/test262/archive/{commitSha}.zip"; + + Console.WriteLine("Loading test262 repository archive from {0}", uri); + + using (var client = new HttpClient()) + { + using (var downloadStream = await client.GetStreamAsync(uri)) + { + using (var writeStream = File.OpenWrite(zipFileWithLargeAmountOfEntriesPath)) + { + await downloadStream.CopyToAsync(writeStream); + Console.WriteLine("File downloaded and saved to {0}", zipFileWithLargeAmountOfEntriesPath); + } + } + } + } + + } + + [Benchmark] + public void ReadLargeZipFile() + { + using (var file = new SharpZipLib.Zip.ZipFile(zipFileWithLargeAmountOfEntriesPath)) + { + foreach (ZipEntry entry in file) + { + using (var stream = file.GetInputStream(entry)) + { + while (stream.Read(readBuffer, 0, readBuffer.Length) > 0) + { + } + } + } + } + } + } +} diff --git a/benchmark/ICSharpCode.SharpZipLib.Benchmark/Zip/ZipInputStream.cs b/benchmark/ICSharpCode.SharpZipLib.Benchmark/Zip/ZipInputStream.cs index eb099ebfd..2e0c057d8 100644 --- a/benchmark/ICSharpCode.SharpZipLib.Benchmark/Zip/ZipInputStream.cs +++ b/benchmark/ICSharpCode.SharpZipLib.Benchmark/Zip/ZipInputStream.cs @@ -1,5 +1,4 @@ -using System; -using System.IO; +using System.IO; using BenchmarkDotNet.Attributes; namespace ICSharpCode.SharpZipLib.Benchmark.Zip @@ -15,7 +14,8 @@ public class ZipInputStream byte[] zippedData; byte[] readBuffer = new byte[4096]; - public ZipInputStream() + [GlobalSetup] + public void GlobalSetup() { using (var memoryStream = new MemoryStream()) { diff --git a/benchmark/ICSharpCode.SharpZipLib.Benchmark/Zip/ZipOutputStream.cs b/benchmark/ICSharpCode.SharpZipLib.Benchmark/Zip/ZipOutputStream.cs index ed125c1c7..c4e8620e3 100644 --- a/benchmark/ICSharpCode.SharpZipLib.Benchmark/Zip/ZipOutputStream.cs +++ b/benchmark/ICSharpCode.SharpZipLib.Benchmark/Zip/ZipOutputStream.cs @@ -1,5 +1,4 @@ -using System; -using System.IO; +using System.IO; using System.Threading.Tasks; using BenchmarkDotNet.Attributes; @@ -16,7 +15,8 @@ public class ZipOutputStream byte[] outputBuffer; byte[] inputBuffer; - public ZipOutputStream() + [GlobalSetup] + public void GlobalSetup() { inputBuffer = new byte[ChunkSize]; outputBuffer = new byte[N]; diff --git a/src/ICSharpCode.SharpZipLib/Core/InflaterPool.cs b/src/ICSharpCode.SharpZipLib/Core/InflaterPool.cs new file mode 100644 index 000000000..39db32e8c --- /dev/null +++ b/src/ICSharpCode.SharpZipLib/Core/InflaterPool.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Concurrent; +using ICSharpCode.SharpZipLib.Zip.Compression; + +namespace ICSharpCode.SharpZipLib.Core +{ + /// + /// Pool for instances as they can be costly due to byte array allocations. + /// + internal sealed class InflaterPool + { + private readonly ConcurrentQueue noHeaderPool = new ConcurrentQueue(); + private readonly ConcurrentQueue headerPool = new ConcurrentQueue(); + + internal static InflaterPool Instance { get; } = new InflaterPool(); + + private InflaterPool() + { + } + + internal Inflater Rent(bool noHeader = false) + { + if (SharpZipLibOptions.InflaterPoolSize <= 0) + { + return new Inflater(noHeader); + } + + var pool = GetPool(noHeader); + + PooledInflater inf; + if (pool.TryDequeue(out var inflater)) + { + inf = inflater; + inf.Reset(); + } + else + { + inf = new PooledInflater(noHeader); + } + + return inf; + } + + internal void Return(Inflater inflater) + { + if (SharpZipLibOptions.InflaterPoolSize <= 0) + { + return; + } + + if (!(inflater is PooledInflater pooledInflater)) + { + throw new ArgumentException("Returned inflater was not a pooled one"); + } + + var pool = GetPool(inflater.noHeader); + if (pool.Count < SharpZipLibOptions.InflaterPoolSize) + { + pooledInflater.Reset(); + pool.Enqueue(pooledInflater); + } + } + + private ConcurrentQueue GetPool(bool noHeader) => noHeader ? noHeaderPool : headerPool; + } +} diff --git a/src/ICSharpCode.SharpZipLib/Core/PathUtils.cs b/src/ICSharpCode.SharpZipLib/Core/PathUtils.cs index b8d0dd409..52f01d079 100644 --- a/src/ICSharpCode.SharpZipLib/Core/PathUtils.cs +++ b/src/ICSharpCode.SharpZipLib/Core/PathUtils.cs @@ -16,6 +16,9 @@ public static class PathUtils /// The path with the root removed if it was present; path otherwise. public static string DropPathRoot(string path) { + // No need to drop anything + if (path == string.Empty) return path; + var invalidChars = Path.GetInvalidPathChars(); // If the first character after the root is a ':', .NET < 4.6.2 throws var cleanRootSep = path.Length >= 3 && path[1] == ':' && path[2] == ':'; @@ -26,7 +29,7 @@ public static string DropPathRoot(string path) var cleanPath = new string(path.Take(258) .Select( (c, i) => invalidChars.Contains(c) || (i == 2 && cleanRootSep) ? '_' : c).ToArray()); - var stripLength = Path.GetPathRoot(cleanPath).Length; + var stripLength = Path.GetPathRoot(cleanPath)?.Length ?? 0; while (path.Length > stripLength && (path[stripLength] == '/' || path[stripLength] == '\\')) stripLength++; return path.Substring(stripLength); } diff --git a/src/ICSharpCode.SharpZipLib/GZip/GzipInputStream.cs b/src/ICSharpCode.SharpZipLib/GZip/GzipInputStream.cs index db5aef2da..feca66b3d 100644 --- a/src/ICSharpCode.SharpZipLib/GZip/GzipInputStream.cs +++ b/src/ICSharpCode.SharpZipLib/GZip/GzipInputStream.cs @@ -4,6 +4,7 @@ using System; using System.IO; using System.Text; +using ICSharpCode.SharpZipLib.Core; namespace ICSharpCode.SharpZipLib.GZip { @@ -82,7 +83,7 @@ public GZipInputStream(Stream baseInputStream) /// Size of the buffer to use /// public GZipInputStream(Stream baseInputStream, int size) - : base(baseInputStream, new Inflater(true), size) + : base(baseInputStream, InflaterPool.Instance.Rent(true), size) { } diff --git a/src/ICSharpCode.SharpZipLib/SharpZipLibOptions.cs b/src/ICSharpCode.SharpZipLib/SharpZipLibOptions.cs new file mode 100644 index 000000000..a6694e71e --- /dev/null +++ b/src/ICSharpCode.SharpZipLib/SharpZipLibOptions.cs @@ -0,0 +1,15 @@ +using ICSharpCode.SharpZipLib.Zip.Compression; + +namespace ICSharpCode.SharpZipLib +{ + /// + /// Global options to alter behavior. + /// + public static class SharpZipLibOptions + { + /// + /// The max pool size allowed for reusing instances, defaults to 0 (disabled). + /// + public static int InflaterPoolSize { get; set; } = 0; + } +} diff --git a/src/ICSharpCode.SharpZipLib/Zip/Compression/Inflater.cs b/src/ICSharpCode.SharpZipLib/Zip/Compression/Inflater.cs index 439b4c601..5bf2a985e 100644 --- a/src/ICSharpCode.SharpZipLib/Zip/Compression/Inflater.cs +++ b/src/ICSharpCode.SharpZipLib/Zip/Compression/Inflater.cs @@ -137,7 +137,7 @@ public class Inflater /// True means, that the inflated stream doesn't contain a Zlib header or /// footer. /// - private bool noHeader; + internal bool noHeader; private readonly StreamManipulator input; private OutputWindow outputWindow; diff --git a/src/ICSharpCode.SharpZipLib/Zip/Compression/PooledInflater.cs b/src/ICSharpCode.SharpZipLib/Zip/Compression/PooledInflater.cs new file mode 100644 index 000000000..0828de3ef --- /dev/null +++ b/src/ICSharpCode.SharpZipLib/Zip/Compression/PooledInflater.cs @@ -0,0 +1,14 @@ +using ICSharpCode.SharpZipLib.Core; + +namespace ICSharpCode.SharpZipLib.Zip.Compression +{ + /// + /// A marker type for pooled version of an inflator that we can return back to . + /// + internal sealed class PooledInflater : Inflater + { + public PooledInflater(bool noHeader) : base(noHeader) + { + } + } +} diff --git a/src/ICSharpCode.SharpZipLib/Zip/Compression/Streams/DeflaterOutputStream.cs b/src/ICSharpCode.SharpZipLib/Zip/Compression/Streams/DeflaterOutputStream.cs index be63c7cfb..a7e6807ca 100644 --- a/src/ICSharpCode.SharpZipLib/Zip/Compression/Streams/DeflaterOutputStream.cs +++ b/src/ICSharpCode.SharpZipLib/Zip/Compression/Streams/DeflaterOutputStream.cs @@ -392,7 +392,12 @@ public override void Flush() baseOutputStream_.Flush(); } - /// + /// + /// Asynchronously clears all buffers for this stream, causes any buffered data to be written to the underlying device, and monitors cancellation requests. + /// + /// + /// The token to monitor for cancellation requests. The default value is . + /// public override async Task FlushAsync(CancellationToken cancellationToken) { deflater_.Flush(); @@ -504,7 +509,21 @@ public override void Write(byte[] buffer, int offset, int count) Deflate(); } - /// + /// + /// Asynchronously writes a sequence of bytes to the current stream, advances the current position within this stream by the number of bytes written, and monitors cancellation requests. + /// + /// + /// The byte array + /// + /// + /// The offset into the byte array where to start. + /// + /// + /// The number of bytes to write. + /// + /// + /// The token to monitor for cancellation requests. The default value is . + /// public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken ct) { deflater_.SetInput(buffer, offset, count); diff --git a/src/ICSharpCode.SharpZipLib/Zip/Compression/Streams/InflaterInputStream.cs b/src/ICSharpCode.SharpZipLib/Zip/Compression/Streams/InflaterInputStream.cs index 7790474d2..980ffc701 100644 --- a/src/ICSharpCode.SharpZipLib/Zip/Compression/Streams/InflaterInputStream.cs +++ b/src/ICSharpCode.SharpZipLib/Zip/Compression/Streams/InflaterInputStream.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Security.Cryptography; +using ICSharpCode.SharpZipLib.Core; namespace ICSharpCode.SharpZipLib.Zip.Compression.Streams { @@ -339,7 +340,7 @@ public class InflaterInputStream : Stream /// The InputStream to read bytes from /// public InflaterInputStream(Stream baseInputStream) - : this(baseInputStream, new Inflater(), 4096) + : this(baseInputStream, InflaterPool.Instance.Rent(), 4096) { } @@ -630,6 +631,12 @@ protected override void Dispose(bool disposing) baseInputStream.Dispose(); } } + + if (inf is PooledInflater inflater) + { + InflaterPool.Instance.Return(inflater); + } + inf = null; } /// diff --git a/src/ICSharpCode.SharpZipLib/Zip/ZipFile.cs b/src/ICSharpCode.SharpZipLib/Zip/ZipFile.cs index 69bb9f6a9..7fc1c5592 100644 --- a/src/ICSharpCode.SharpZipLib/Zip/ZipFile.cs +++ b/src/ICSharpCode.SharpZipLib/Zip/ZipFile.cs @@ -314,7 +314,7 @@ public enum FileUpdateMode /// } /// /// - public class ZipFile : IEnumerable, IDisposable + public class ZipFile : IEnumerable, IDisposable { #region KeyHandling @@ -387,6 +387,23 @@ private bool HaveKeys #region Constructors + /// + /// Opens a Zip file with the given name for reading. + /// + /// The name of the file to open. + /// The argument supplied is null. + /// + /// An i/o error occurs + /// + /// + /// The file doesn't contain a valid zip archive. + /// + public ZipFile(string name) : + this(name, null) + { + + } + /// /// Opens a Zip file with the given name for reading. /// @@ -399,7 +416,7 @@ private bool HaveKeys /// /// The file doesn't contain a valid zip archive. /// - public ZipFile(string name, StringCodec stringCodec = null) + public ZipFile(string name, StringCodec stringCodec) { name_ = name ?? throw new ArgumentNullException(nameof(name)); @@ -500,6 +517,29 @@ public ZipFile(Stream stream) : } + /// + /// Opens a Zip file reading the given . + /// + /// The to read archive data from. + /// true to leave the stream open when the ZipFile is disposed, false to dispose of it + /// + /// An i/o error occurs + /// + /// + /// The stream doesn't contain a valid zip archive.
+ ///
+ /// + /// The stream doesnt support seeking. + /// + /// + /// The stream argument is null. + /// + public ZipFile(Stream stream, bool leaveOpen) : + this(stream, leaveOpen, null) + { + + } + /// /// Opens a Zip file reading the given . /// @@ -518,7 +558,7 @@ public ZipFile(Stream stream) : /// /// The stream argument is null. /// - public ZipFile(Stream stream, bool leaveOpen, StringCodec stringCodec = null) + public ZipFile(Stream stream, bool leaveOpen, StringCodec stringCodec) { if (stream == null) { @@ -770,7 +810,31 @@ public StringCodec StringCodec /// /// The Zip file has been closed. /// - public IEnumerator GetEnumerator() + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// + /// Gets an enumerator for the Zip entries in this Zip file. + /// + /// Returns an for this archive. + /// + /// The Zip file has been closed. + /// + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// + /// Gets an enumerator for the Zip entries in this Zip file. + /// + /// Returns an for this archive. + /// + /// The Zip file has been closed. + /// + public ZipEntryEnumerator GetEnumerator() { if (isDisposed_) { @@ -914,7 +978,7 @@ public Stream GetInputStream(long entryIndex) case CompressionMethod.Deflated: // No need to worry about ownership and closing as underlying stream close does nothing. - result = new InflaterInputStream(result, new Inflater(true)); + result = new InflaterInputStream(result, InflaterPool.Instance.Rent(true)); break; case CompressionMethod.BZip2: @@ -3961,20 +4025,26 @@ public static implicit operator string(ZipString zipString) /// /// An enumerator for Zip entries /// - private class ZipEntryEnumerator : IEnumerator + public struct ZipEntryEnumerator : IEnumerator { #region Constructors + /// + /// Constructs a new instance of . + /// + /// Entries to iterate. public ZipEntryEnumerator(ZipEntry[] entries) { array = entries; + index = -1; } #endregion Constructors #region IEnumerator Members - public object Current + /// + public ZipEntry Current { get { @@ -3982,22 +4052,32 @@ public object Current } } + /// + object IEnumerator.Current => Current; + + /// public void Reset() { index = -1; } + /// public bool MoveNext() { return (++index < array.Length); } + /// + public void Dispose() + { + } + #endregion IEnumerator Members #region Instance Fields private ZipEntry[] array; - private int index = -1; + private int index; #endregion Instance Fields } diff --git a/src/ICSharpCode.SharpZipLib/Zip/ZipInputStream.cs b/src/ICSharpCode.SharpZipLib/Zip/ZipInputStream.cs index 4d258afc8..37e9e8ba8 100644 --- a/src/ICSharpCode.SharpZipLib/Zip/ZipInputStream.cs +++ b/src/ICSharpCode.SharpZipLib/Zip/ZipInputStream.cs @@ -1,10 +1,11 @@ using ICSharpCode.SharpZipLib.Checksum; using ICSharpCode.SharpZipLib.Encryption; -using ICSharpCode.SharpZipLib.Zip.Compression; using ICSharpCode.SharpZipLib.Zip.Compression.Streams; using System; using System.Diagnostics; using System.IO; +using ICSharpCode.SharpZipLib.Core; +using ICSharpCode.SharpZipLib.Zip.Compression; namespace ICSharpCode.SharpZipLib.Zip { @@ -88,7 +89,7 @@ public class ZipInputStream : InflaterInputStream ///
/// The underlying providing data. public ZipInputStream(Stream baseInputStream) - : base(baseInputStream, new Inflater(true)) + : base(baseInputStream, InflaterPool.Instance.Rent(true)) { internalReader = new ReadDataHandler(ReadingNotAvailable); } @@ -99,7 +100,7 @@ public ZipInputStream(Stream baseInputStream) /// The underlying providing data. /// Size of the buffer. public ZipInputStream(Stream baseInputStream, int bufferSize) - : base(baseInputStream, new Inflater(true), bufferSize) + : base(baseInputStream, InflaterPool.Instance.Rent(true), bufferSize) { internalReader = new ReadDataHandler(ReadingNotAvailable); } diff --git a/test/ICSharpCode.SharpZipLib.Tests/Zip/ZipFileHandling.cs b/test/ICSharpCode.SharpZipLib.Tests/Zip/ZipFileHandling.cs index e594cd17f..c25059da4 100644 --- a/test/ICSharpCode.SharpZipLib.Tests/Zip/ZipFileHandling.cs +++ b/test/ICSharpCode.SharpZipLib.Tests/Zip/ZipFileHandling.cs @@ -1815,5 +1815,27 @@ public void TestDescriptorUpdateOnAdd(UseZip64 useZip64) } } } + + /// + /// Check that Zip files can be created with an empty file name + /// + [Test] + [Category("Zip")] + public void HandlesEmptyFileName() + { + using var ms = new MemoryStream(); + using (var zos = new ZipOutputStream(ms){IsStreamOwner = false}) + { + zos.PutNextEntry(new ZipEntry(String.Empty)); + Utils.WriteDummyData(zos, 64); + } + ms.Seek(0, SeekOrigin.Begin); + using (var zis = new ZipInputStream(ms){IsStreamOwner = false}) + { + var entry = zis.GetNextEntry(); + Assert.That(entry.Name, Is.Empty); + Assert.That(zis.ReadBytes(64).Length, Is.EqualTo(64)); + } + } } }