From 5f3031db4a2cc91da3cdcac62afc4ddd0f959b53 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Thu, 12 Feb 2026 11:53:19 +0000 Subject: [PATCH 1/7] Open for writing should be async --- .../Archives/ArchiveFactory.Async.cs | 8 +++- .../Archives/IMultiArchiveFactory.cs | 15 ++++++-- src/SharpCompress/Factories/GZipFactory.cs | 31 +++++++++++---- src/SharpCompress/Factories/RarFactory.cs | 23 ++++++++--- .../Factories/SevenZipFactory.cs | 26 ++++++++++--- src/SharpCompress/Factories/TarFactory.cs | 31 +++++++++++---- src/SharpCompress/Factories/ZipFactory.cs | 31 +++++++++++---- src/SharpCompress/Utility.Async.cs | 38 +++++++++++++++++++ src/SharpCompress/Writers/IWriterFactory.cs | 3 +- src/SharpCompress/Writers/WriterFactory.cs | 21 +++++----- tests/SharpCompress.Test/ArchiveTests.cs | 4 +- tests/SharpCompress.Test/GZip/AsyncTests.cs | 2 +- .../GZip/GZipWriterAsyncTests.cs | 2 +- .../Tar/TarArchiveAsyncTests.cs | 4 +- tests/SharpCompress.Test/WriterTests.cs | 2 +- .../Zip/ZipMemoryArchiveWithCrcAsyncTests.cs | 14 +++++-- 16 files changed, 190 insertions(+), 65 deletions(-) diff --git a/src/SharpCompress/Archives/ArchiveFactory.Async.cs b/src/SharpCompress/Archives/ArchiveFactory.Async.cs index 4af94a57..04f009cb 100644 --- a/src/SharpCompress/Archives/ArchiveFactory.Async.cs +++ b/src/SharpCompress/Archives/ArchiveFactory.Async.cs @@ -77,7 +77,9 @@ public static partial class ArchiveFactory var factory = await FindFactoryAsync(fileInfo, cancellationToken) .ConfigureAwait(false); - return factory.OpenAsyncArchive(filesArray, options); + return await factory + .OpenAsyncArchive(filesArray, options, cancellationToken) + .ConfigureAwait(false); } public static async ValueTask OpenAsyncArchive( @@ -106,7 +108,9 @@ public static partial class ArchiveFactory var factory = await FindFactoryAsync(firstStream, cancellationToken) .ConfigureAwait(false); - return factory.OpenAsyncArchive(streamsArray, options); + return await factory + .OpenAsyncArchive(streamsArray, options, cancellationToken) + .ConfigureAwait(false); } public static ValueTask FindFactoryAsync( diff --git a/src/SharpCompress/Archives/IMultiArchiveFactory.cs b/src/SharpCompress/Archives/IMultiArchiveFactory.cs index 936222e6..3f9e0ec1 100644 --- a/src/SharpCompress/Archives/IMultiArchiveFactory.cs +++ b/src/SharpCompress/Archives/IMultiArchiveFactory.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.IO; using System.Threading; +using System.Threading.Tasks; using SharpCompress.Factories; using SharpCompress.Readers; @@ -33,9 +34,12 @@ public interface IMultiArchiveFactory : IFactory /// /// /// reading options. - IAsyncArchive OpenAsyncArchive( + /// Cancellation token. + /// A containing the opened async archive. + ValueTask OpenAsyncArchive( IReadOnlyList streams, - ReaderOptions? readerOptions = null + ReaderOptions? readerOptions = null, + CancellationToken cancellationToken = default ); /// @@ -50,8 +54,11 @@ public interface IMultiArchiveFactory : IFactory /// /// /// reading options. - IAsyncArchive OpenAsyncArchive( + /// Cancellation token. + /// A containing the opened async archive. + ValueTask OpenAsyncArchive( IReadOnlyList fileInfos, - ReaderOptions? readerOptions = null + ReaderOptions? readerOptions = null, + CancellationToken cancellationToken = default ); } diff --git a/src/SharpCompress/Factories/GZipFactory.cs b/src/SharpCompress/Factories/GZipFactory.cs index ab40438c..447e15f0 100644 --- a/src/SharpCompress/Factories/GZipFactory.cs +++ b/src/SharpCompress/Factories/GZipFactory.cs @@ -95,10 +95,17 @@ public class GZipFactory ) => GZipArchive.OpenArchive(streams, readerOptions); /// - public IAsyncArchive OpenAsyncArchive( + public async ValueTask OpenAsyncArchive( IReadOnlyList streams, - ReaderOptions? readerOptions = null - ) => (IAsyncArchive)OpenArchive(streams, readerOptions); + ReaderOptions? readerOptions = null, + CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + return await GZipArchive + .OpenAsyncArchive(streams, readerOptions, cancellationToken) + .ConfigureAwait(false); + } /// public IArchive OpenArchive( @@ -107,10 +114,17 @@ public class GZipFactory ) => GZipArchive.OpenArchive(fileInfos, readerOptions); /// - public IAsyncArchive OpenAsyncArchive( + public async ValueTask OpenAsyncArchive( IReadOnlyList fileInfos, - ReaderOptions? readerOptions = null - ) => (IAsyncArchive)OpenArchive(fileInfos, readerOptions); + ReaderOptions? readerOptions = null, + CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + return await GZipArchive + .OpenAsyncArchive(fileInfos, readerOptions, cancellationToken) + .ConfigureAwait(false); + } #endregion @@ -187,14 +201,15 @@ public class GZipFactory } /// - public IAsyncWriter OpenAsyncWriter( + public ValueTask OpenAsyncWriter( Stream stream, IWriterOptions writerOptions, CancellationToken cancellationToken = default ) { cancellationToken.ThrowIfCancellationRequested(); - return (IAsyncWriter)OpenWriter(stream, writerOptions); + var writer = OpenWriter(stream, writerOptions); + return new((IAsyncWriter)writer); } #endregion diff --git a/src/SharpCompress/Factories/RarFactory.cs b/src/SharpCompress/Factories/RarFactory.cs index c81e54a7..615f7fb4 100644 --- a/src/SharpCompress/Factories/RarFactory.cs +++ b/src/SharpCompress/Factories/RarFactory.cs @@ -90,10 +90,17 @@ public class RarFactory : Factory, IArchiveFactory, IMultiArchiveFactory, IReade ) => RarArchive.OpenArchive(streams, readerOptions); /// - public IAsyncArchive OpenAsyncArchive( + public async ValueTask OpenAsyncArchive( IReadOnlyList streams, - ReaderOptions? readerOptions = null - ) => (IAsyncArchive)OpenArchive(streams, readerOptions); + ReaderOptions? readerOptions = null, + CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + return await RarArchive + .OpenAsyncArchive(streams, readerOptions, cancellationToken) + .ConfigureAwait(false); + } /// public IArchive OpenArchive( @@ -102,12 +109,16 @@ public class RarFactory : Factory, IArchiveFactory, IMultiArchiveFactory, IReade ) => RarArchive.OpenArchive(fileInfos, readerOptions); /// - public IAsyncArchive OpenAsyncArchive( + public async ValueTask OpenAsyncArchive( IReadOnlyList fileInfos, - ReaderOptions? readerOptions = null + ReaderOptions? readerOptions = null, + CancellationToken cancellationToken = default ) { - return (IAsyncArchive)OpenArchive(fileInfos, readerOptions); + cancellationToken.ThrowIfCancellationRequested(); + return await RarArchive + .OpenAsyncArchive(fileInfos, readerOptions, cancellationToken) + .ConfigureAwait(false); } #endregion diff --git a/src/SharpCompress/Factories/SevenZipFactory.cs b/src/SharpCompress/Factories/SevenZipFactory.cs index 93826468..e4942041 100644 --- a/src/SharpCompress/Factories/SevenZipFactory.cs +++ b/src/SharpCompress/Factories/SevenZipFactory.cs @@ -85,10 +85,17 @@ public class SevenZipFactory : Factory, IArchiveFactory, IMultiArchiveFactory ) => SevenZipArchive.OpenArchive(streams, readerOptions); /// - public IAsyncArchive OpenAsyncArchive( + public async ValueTask OpenAsyncArchive( IReadOnlyList streams, - ReaderOptions? readerOptions = null - ) => (IAsyncArchive)OpenArchive(streams, readerOptions); + ReaderOptions? readerOptions = null, + CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + return await SevenZipArchive + .OpenAsyncArchive(streams, readerOptions, cancellationToken) + .ConfigureAwait(false); + } /// public IArchive OpenArchive( @@ -97,10 +104,17 @@ public class SevenZipFactory : Factory, IArchiveFactory, IMultiArchiveFactory ) => SevenZipArchive.OpenArchive(fileInfos, readerOptions); /// - public IAsyncArchive OpenAsyncArchive( + public async ValueTask OpenAsyncArchive( IReadOnlyList fileInfos, - ReaderOptions? readerOptions = null - ) => (IAsyncArchive)OpenArchive(fileInfos, readerOptions); + ReaderOptions? readerOptions = null, + CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + return await SevenZipArchive + .OpenAsyncArchive(fileInfos, readerOptions, cancellationToken) + .ConfigureAwait(false); + } #endregion diff --git a/src/SharpCompress/Factories/TarFactory.cs b/src/SharpCompress/Factories/TarFactory.cs index fd2f528f..00ba6be2 100644 --- a/src/SharpCompress/Factories/TarFactory.cs +++ b/src/SharpCompress/Factories/TarFactory.cs @@ -195,10 +195,17 @@ public class TarFactory ) => TarArchive.OpenArchive(streams, readerOptions); /// - public IAsyncArchive OpenAsyncArchive( + public async ValueTask OpenAsyncArchive( IReadOnlyList streams, - ReaderOptions? readerOptions = null - ) => (IAsyncArchive)OpenArchive(streams, readerOptions); + ReaderOptions? readerOptions = null, + CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + return await TarArchive + .OpenAsyncArchive(streams, readerOptions, cancellationToken) + .ConfigureAwait(false); + } /// public IArchive OpenArchive( @@ -207,10 +214,17 @@ public class TarFactory ) => TarArchive.OpenArchive(fileInfos, readerOptions); /// - public IAsyncArchive OpenAsyncArchive( + public async ValueTask OpenAsyncArchive( IReadOnlyList fileInfos, - ReaderOptions? readerOptions = null - ) => (IAsyncArchive)OpenArchive(fileInfos, readerOptions); + ReaderOptions? readerOptions = null, + CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + return await TarArchive + .OpenAsyncArchive(fileInfos, readerOptions, cancellationToken) + .ConfigureAwait(false); + } #endregion @@ -298,14 +312,15 @@ public class TarFactory } /// - public IAsyncWriter OpenAsyncWriter( + public ValueTask OpenAsyncWriter( Stream stream, IWriterOptions writerOptions, CancellationToken cancellationToken = default ) { cancellationToken.ThrowIfCancellationRequested(); - return (IAsyncWriter)OpenWriter(stream, writerOptions); + var writer = OpenWriter(stream, writerOptions); + return new((IAsyncWriter)writer); } #endregion diff --git a/src/SharpCompress/Factories/ZipFactory.cs b/src/SharpCompress/Factories/ZipFactory.cs index 6c683a38..057bfb65 100644 --- a/src/SharpCompress/Factories/ZipFactory.cs +++ b/src/SharpCompress/Factories/ZipFactory.cs @@ -163,10 +163,17 @@ public class ZipFactory ) => ZipArchive.OpenArchive(streams, readerOptions); /// - public IAsyncArchive OpenAsyncArchive( + public async ValueTask OpenAsyncArchive( IReadOnlyList streams, - ReaderOptions? readerOptions = null - ) => (IAsyncArchive)OpenArchive(streams, readerOptions); + ReaderOptions? readerOptions = null, + CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + return await ZipArchive + .OpenAsyncArchive(streams, readerOptions, cancellationToken) + .ConfigureAwait(false); + } /// public IArchive OpenArchive( @@ -175,10 +182,17 @@ public class ZipFactory ) => ZipArchive.OpenArchive(fileInfos, readerOptions); /// - public IAsyncArchive OpenAsyncArchive( + public async ValueTask OpenAsyncArchive( IReadOnlyList fileInfos, - ReaderOptions? readerOptions = null - ) => (IAsyncArchive)OpenArchive(fileInfos, readerOptions); + ReaderOptions? readerOptions = null, + CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + return await ZipArchive + .OpenAsyncArchive(fileInfos, readerOptions, cancellationToken) + .ConfigureAwait(false); + } #endregion @@ -219,14 +233,15 @@ public class ZipFactory } /// - public IAsyncWriter OpenAsyncWriter( + public ValueTask OpenAsyncWriter( Stream stream, IWriterOptions writerOptions, CancellationToken cancellationToken = default ) { cancellationToken.ThrowIfCancellationRequested(); - return (IAsyncWriter)OpenWriter(stream, writerOptions); + var writer = OpenWriter(stream, writerOptions); + return new((IAsyncWriter)writer); } #endregion diff --git a/src/SharpCompress/Utility.Async.cs b/src/SharpCompress/Utility.Async.cs index e8e6b0c3..bc90ac96 100644 --- a/src/SharpCompress/Utility.Async.cs +++ b/src/SharpCompress/Utility.Async.cs @@ -119,4 +119,42 @@ internal static partial class Utility return (total >= count); } } + + /// + /// Opens a file stream for asynchronous writing. + /// Uses File.OpenHandle with FileOptions.Asynchronous on .NET 8.0+ for optimal performance. + /// Falls back to FileStream constructor with async options on legacy frameworks. + /// + /// The file path to open. + /// Cancellation token. + /// A FileStream configured for asynchronous operations. + public static Stream OpenAsyncWriteStream( + string path, + CancellationToken cancellationToken + ) + { + cancellationToken.ThrowIfCancellationRequested(); + +#if !LEGACY_DOTNET + // Use File.OpenHandle with async options for .NET 8.0+ + var handle = File.OpenHandle( + path, + FileMode.Create, + FileAccess.Write, + FileShare.None, + FileOptions.Asynchronous + ); + return new FileStream(handle, FileAccess.Write); +#else + // For legacy .NET, use FileStream constructor with async options + return new FileStream( + path, + FileMode.Create, + FileAccess.Write, + FileShare.None, + bufferSize: 4096, + FileOptions.Asynchronous + ); +#endif + } } diff --git a/src/SharpCompress/Writers/IWriterFactory.cs b/src/SharpCompress/Writers/IWriterFactory.cs index fc2ef301..c9eef005 100644 --- a/src/SharpCompress/Writers/IWriterFactory.cs +++ b/src/SharpCompress/Writers/IWriterFactory.cs @@ -1,5 +1,6 @@ using System.IO; using System.Threading; +using System.Threading.Tasks; using SharpCompress.Common.Options; using SharpCompress.Factories; @@ -9,7 +10,7 @@ public interface IWriterFactory : IFactory { IWriter OpenWriter(Stream stream, IWriterOptions writerOptions); - IAsyncWriter OpenAsyncWriter( + ValueTask OpenAsyncWriter( Stream stream, IWriterOptions writerOptions, CancellationToken cancellationToken = default diff --git a/src/SharpCompress/Writers/WriterFactory.cs b/src/SharpCompress/Writers/WriterFactory.cs index 1d392797..f923df51 100644 --- a/src/SharpCompress/Writers/WriterFactory.cs +++ b/src/SharpCompress/Writers/WriterFactory.cs @@ -2,6 +2,7 @@ using System; using System.IO; using System.Linq; using System.Threading; +using System.Threading.Tasks; using SharpCompress.Common; using SharpCompress.Common.Options; @@ -29,7 +30,7 @@ public static class WriterFactory return OpenWriter(fileInfo.OpenWrite(), archiveType, writerOptions); } - public static IAsyncWriter OpenAsyncWriter( + public static async ValueTask OpenAsyncWriter( string filePath, ArchiveType archiveType, IWriterOptions writerOptions, @@ -37,7 +38,7 @@ public static class WriterFactory ) { filePath.NotNullOrEmpty(nameof(filePath)); - return OpenAsyncWriter( + return await OpenAsyncWriter( new FileInfo(filePath), archiveType, writerOptions, @@ -45,7 +46,7 @@ public static class WriterFactory ); } - public static IAsyncWriter OpenAsyncWriter( + public static async ValueTask OpenAsyncWriter( FileInfo fileInfo, ArchiveType archiveType, IWriterOptions writerOptions, @@ -53,12 +54,8 @@ public static class WriterFactory ) { fileInfo.NotNull(nameof(fileInfo)); - return OpenAsyncWriter( - fileInfo.Open(FileMode.Create, FileAccess.Write), - archiveType, - writerOptions, - cancellationToken - ); + var stream = Utility.OpenAsyncWriteStream(fileInfo.FullName, cancellationToken); + return await OpenAsyncWriter(stream, archiveType, writerOptions, cancellationToken); } public static IWriter OpenWriter( @@ -86,8 +83,8 @@ public static class WriterFactory /// The archive type. /// Writer options. /// Cancellation token. - /// A task that returns an IWriter. - public static IAsyncWriter OpenAsyncWriter( + /// A containing the async writer. + public static async ValueTask OpenAsyncWriter( Stream stream, ArchiveType archiveType, IWriterOptions writerOptions, @@ -100,7 +97,7 @@ public static class WriterFactory if (factory != null) { - return factory.OpenAsyncWriter(stream, writerOptions, cancellationToken); + return await factory.OpenAsyncWriter(stream, writerOptions, cancellationToken); } throw new NotSupportedException("Archive Type does not have a Writer: " + archiveType); diff --git a/tests/SharpCompress.Test/ArchiveTests.cs b/tests/SharpCompress.Test/ArchiveTests.cs index 0d98e805..270900b9 100644 --- a/tests/SharpCompress.Test/ArchiveTests.cs +++ b/tests/SharpCompress.Test/ArchiveTests.cs @@ -383,7 +383,7 @@ public class ArchiveTests : ReaderTests return WriterFactory.OpenWriter(stream, ArchiveType.Zip, writerOptions); } - protected static IAsyncWriter CreateWriterWithLevelAsync( + protected static async ValueTask CreateWriterWithLevelAsync( Stream stream, CompressionType compressionType, int? compressionLevel = null @@ -392,7 +392,7 @@ public class ArchiveTests : ReaderTests var writerOptions = compressionLevel.HasValue ? new WriterOptions(compressionType, compressionLevel.Value) { LeaveStreamOpen = true } : new WriterOptions(compressionType) { LeaveStreamOpen = true }; - return WriterFactory.OpenAsyncWriter( + return await WriterFactory.OpenAsyncWriter( new AsyncOnlyStream(stream), ArchiveType.Zip, writerOptions diff --git a/tests/SharpCompress.Test/GZip/AsyncTests.cs b/tests/SharpCompress.Test/GZip/AsyncTests.cs index 8c0d19e1..cf334ef9 100644 --- a/tests/SharpCompress.Test/GZip/AsyncTests.cs +++ b/tests/SharpCompress.Test/GZip/AsyncTests.cs @@ -104,7 +104,7 @@ public class AsyncTests : TestBase await using (var stream = File.Create(outputPath)) #endif using ( - var writer = WriterFactory.OpenAsyncWriter( + var writer = await WriterFactory.OpenAsyncWriter( new AsyncOnlyStream(stream), ArchiveType.Zip, new WriterOptions(CompressionType.Deflate) { LeaveStreamOpen = false } diff --git a/tests/SharpCompress.Test/GZip/GZipWriterAsyncTests.cs b/tests/SharpCompress.Test/GZip/GZipWriterAsyncTests.cs index 60e115c0..a5d81899 100644 --- a/tests/SharpCompress.Test/GZip/GZipWriterAsyncTests.cs +++ b/tests/SharpCompress.Test/GZip/GZipWriterAsyncTests.cs @@ -24,7 +24,7 @@ public class GZipWriterAsyncTests : WriterTests ) ) using ( - var writer = WriterFactory.OpenAsyncWriter( + var writer = await WriterFactory.OpenAsyncWriter( new AsyncOnlyStream(stream), ArchiveType.GZip, new WriterOptions(CompressionType.GZip) diff --git a/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs b/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs index 194a07d8..84d8ca40 100644 --- a/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs +++ b/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs @@ -35,7 +35,7 @@ public class TarArchiveAsyncTests : ArchiveTests using (Stream stream = File.OpenWrite(Path.Combine(SCRATCH2_FILES_PATH, archive))) { using ( - var writer = WriterFactory.OpenAsyncWriter( + var writer = await WriterFactory.OpenAsyncWriter( new AsyncOnlyStream(stream), ArchiveType.Tar, new WriterOptions(CompressionType.None) { LeaveStreamOpen = false } @@ -94,7 +94,7 @@ public class TarArchiveAsyncTests : ArchiveTests // Step 1: create a tar file containing a file with a long name using (Stream stream = File.OpenWrite(Path.Combine(SCRATCH2_FILES_PATH, archive))) using ( - var writer = WriterFactory.OpenAsyncWriter( + var writer = await WriterFactory.OpenAsyncWriter( new AsyncOnlyStream(stream), ArchiveType.Tar, new WriterOptions(CompressionType.None) { LeaveStreamOpen = false } diff --git a/tests/SharpCompress.Test/WriterTests.cs b/tests/SharpCompress.Test/WriterTests.cs index 786cc41d..5fc18c6c 100644 --- a/tests/SharpCompress.Test/WriterTests.cs +++ b/tests/SharpCompress.Test/WriterTests.cs @@ -70,7 +70,7 @@ public class WriterTests : TestBase writerOptions.ArchiveEncoding.Default = encoding ?? Encoding.Default; - using var writer = WriterFactory.OpenAsyncWriter(stream, _type, writerOptions); + using var writer = await WriterFactory.OpenAsyncWriter(stream, _type, writerOptions); await writer.WriteAllAsync( ORIGINAL_FILES_PATH, "*", diff --git a/tests/SharpCompress.Test/Zip/ZipMemoryArchiveWithCrcAsyncTests.cs b/tests/SharpCompress.Test/Zip/ZipMemoryArchiveWithCrcAsyncTests.cs index c57d5026..52a0d98d 100644 --- a/tests/SharpCompress.Test/Zip/ZipMemoryArchiveWithCrcAsyncTests.cs +++ b/tests/SharpCompress.Test/Zip/ZipMemoryArchiveWithCrcAsyncTests.cs @@ -62,7 +62,11 @@ public class ZipTypesLevelsWithCrcRatioAsyncTests : ArchiveTests // Create zip archive in memory using var zipStream = new MemoryStream(); using ( - var writer = CreateWriterWithLevelAsync(zipStream, compressionType, compressionLevel) + var writer = await CreateWriterWithLevelAsync( + zipStream, + compressionType, + compressionLevel + ) ) { await writer.WriteAsync($"file1_{sizeMb}MiB.txt", new MemoryStream(file1Data)); @@ -133,7 +137,7 @@ public class ZipTypesLevelsWithCrcRatioAsyncTests : ArchiveTests }; using ( - var writer = WriterFactory.OpenAsyncWriter( + var writer = await WriterFactory.OpenAsyncWriter( new AsyncOnlyStream(zipStream), ArchiveType.Zip, writerOptions @@ -201,7 +205,11 @@ public class ZipTypesLevelsWithCrcRatioAsyncTests : ArchiveTests // Create archive with specified compression and level using var zipStream = new MemoryStream(); using ( - var writer = CreateWriterWithLevelAsync(zipStream, compressionType, compressionLevel) + var writer = await CreateWriterWithLevelAsync( + zipStream, + compressionType, + compressionLevel + ) ) { await writer.WriteAsync( From 92c04a9ba46677e13d97978b7675e1ee8118b4c7 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Thu, 12 Feb 2026 11:59:11 +0000 Subject: [PATCH 2/7] don't leave created streams open --- src/SharpCompress/Writers/WriterFactory.cs | 13 ++++++++-- .../Writers/WriterOptionsExtensions.cs | 26 +++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/src/SharpCompress/Writers/WriterFactory.cs b/src/SharpCompress/Writers/WriterFactory.cs index f923df51..43d46b3b 100644 --- a/src/SharpCompress/Writers/WriterFactory.cs +++ b/src/SharpCompress/Writers/WriterFactory.cs @@ -27,7 +27,11 @@ public static class WriterFactory ) { fileInfo.NotNull(nameof(fileInfo)); - return OpenWriter(fileInfo.OpenWrite(), archiveType, writerOptions); + return OpenWriter( + fileInfo.OpenWrite(), + archiveType, + writerOptions.WithLeaveStreamOpen(false) + ); } public static async ValueTask OpenAsyncWriter( @@ -55,7 +59,12 @@ public static class WriterFactory { fileInfo.NotNull(nameof(fileInfo)); var stream = Utility.OpenAsyncWriteStream(fileInfo.FullName, cancellationToken); - return await OpenAsyncWriter(stream, archiveType, writerOptions, cancellationToken); + return await OpenAsyncWriter( + stream, + archiveType, + writerOptions.WithLeaveStreamOpen(false), + cancellationToken + ); } public static IWriter OpenWriter( diff --git a/src/SharpCompress/Writers/WriterOptionsExtensions.cs b/src/SharpCompress/Writers/WriterOptionsExtensions.cs index d86b5406..0c3fb15e 100644 --- a/src/SharpCompress/Writers/WriterOptionsExtensions.cs +++ b/src/SharpCompress/Writers/WriterOptionsExtensions.cs @@ -1,6 +1,9 @@ using System; using SharpCompress.Common; using SharpCompress.Common.Options; +using SharpCompress.Writers.GZip; +using SharpCompress.Writers.Tar; +using SharpCompress.Writers.Zip; namespace SharpCompress.Writers; @@ -20,6 +23,29 @@ public static class WriterOptionsExtensions bool leaveStreamOpen ) => options with { LeaveStreamOpen = leaveStreamOpen }; + /// + /// Creates a copy with the specified LeaveStreamOpen value. + /// Works with any IWriterOptions implementation. + /// + /// The source options. + /// Whether to leave the stream open. + /// A new options instance with the specified LeaveStreamOpen value. + public static IWriterOptions WithLeaveStreamOpen( + this IWriterOptions options, + bool leaveStreamOpen + ) => + options switch + { + WriterOptions writerOptions => writerOptions with { LeaveStreamOpen = leaveStreamOpen }, + ZipWriterOptions zipOptions => zipOptions with { LeaveStreamOpen = leaveStreamOpen }, + TarWriterOptions tarOptions => tarOptions with { LeaveStreamOpen = leaveStreamOpen }, + GZipWriterOptions gzipOptions => gzipOptions with { LeaveStreamOpen = leaveStreamOpen }, + _ => throw new NotSupportedException( + $"Cannot set LeaveStreamOpen on options of type {options.GetType().Name}. " + + "Options must be a record type implementing IWriterOptions." + ), + }; + /// /// Creates a copy with the specified compression level. /// From 2dfe535e0b550881e0020f1276349ff218b0ac74 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Thu, 12 Feb 2026 12:10:43 +0000 Subject: [PATCH 3/7] add async read and write for files --- .../Readers/ReaderFactory.Async.cs | 5 +- .../Readers/Tar/TarReader.Factory.cs | 5 +- src/SharpCompress/Utility.Async.cs | 69 +++++++++++++++++++ src/SharpCompress/Writers/WriterFactory.cs | 2 +- 4 files changed, 76 insertions(+), 5 deletions(-) diff --git a/src/SharpCompress/Readers/ReaderFactory.Async.cs b/src/SharpCompress/Readers/ReaderFactory.Async.cs index c74a7133..f14a5f11 100644 --- a/src/SharpCompress/Readers/ReaderFactory.Async.cs +++ b/src/SharpCompress/Readers/ReaderFactory.Async.cs @@ -35,14 +35,15 @@ public static partial class ReaderFactory /// /// /// - public static ValueTask OpenAsyncReader( + public static async ValueTask OpenAsyncReader( FileInfo fileInfo, ReaderOptions? options = null, CancellationToken cancellationToken = default ) { options ??= ReaderOptions.ForOwnedFile; - return OpenAsyncReader(fileInfo.OpenRead(), options, cancellationToken); + var stream = fileInfo.OpenAsyncReadStream(cancellationToken); + return await OpenAsyncReader(stream, options, cancellationToken); } public static async ValueTask OpenAsyncReader( diff --git a/src/SharpCompress/Readers/Tar/TarReader.Factory.cs b/src/SharpCompress/Readers/Tar/TarReader.Factory.cs index fc775d71..ba60296b 100644 --- a/src/SharpCompress/Readers/Tar/TarReader.Factory.cs +++ b/src/SharpCompress/Readers/Tar/TarReader.Factory.cs @@ -121,14 +121,15 @@ public partial class TarReader return new TarReader(sharpCompressStream, options, CompressionType.None); } - public static ValueTask OpenAsyncReader( + public static async ValueTask OpenAsyncReader( FileInfo fileInfo, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default ) { readerOptions ??= new ReaderOptions() { LeaveStreamOpen = false }; - return OpenAsyncReader(fileInfo.OpenRead(), readerOptions, cancellationToken); + var stream = fileInfo.OpenAsyncReadStream(cancellationToken); + return await OpenAsyncReader(stream, readerOptions, cancellationToken); } public static IReader OpenReader(string filePath, ReaderOptions? readerOptions = null) diff --git a/src/SharpCompress/Utility.Async.cs b/src/SharpCompress/Utility.Async.cs index bc90ac96..564feb14 100644 --- a/src/SharpCompress/Utility.Async.cs +++ b/src/SharpCompress/Utility.Async.cs @@ -152,9 +152,78 @@ internal static partial class Utility FileMode.Create, FileAccess.Write, FileShare.None, + bufferSize: 4096, //default + FileOptions.Asynchronous + ); +#endif + } + + /// + /// Opens a file stream for asynchronous writing from a FileInfo. + /// Uses File.OpenHandle with FileOptions.Asynchronous on .NET 8.0+ for optimal performance. + /// Falls back to FileStream constructor with async options on legacy frameworks. + /// + /// The FileInfo to open. + /// Cancellation token. + /// A FileStream configured for asynchronous operations. + public static Stream OpenAsyncWriteStream( + this FileInfo fileInfo, + CancellationToken cancellationToken + ) + { + fileInfo.NotNull(nameof(fileInfo)); + return OpenAsyncWriteStream(fileInfo.FullName, cancellationToken); + } + + /// + /// Opens a file stream for asynchronous reading. + /// Uses File.OpenHandle with FileOptions.Asynchronous on .NET 8.0+ for optimal performance. + /// Falls back to FileStream constructor with async options on legacy frameworks. + /// + /// The file path to open. + /// Cancellation token. + /// A FileStream configured for asynchronous operations. + public static Stream OpenAsyncReadStream(string path, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + +#if !LEGACY_DOTNET + // Use File.OpenHandle with async options for .NET 8.0+ + var handle = File.OpenHandle( + path, + FileMode.Open, + FileAccess.Read, + FileShare.Read, + FileOptions.Asynchronous + ); + return new FileStream(handle, FileAccess.Read); +#else + // For legacy .NET, use FileStream constructor with async options + return new FileStream( + path, + FileMode.Open, + FileAccess.Read, + FileShare.Read, bufferSize: 4096, FileOptions.Asynchronous ); #endif } + + /// + /// Opens a file stream for asynchronous reading from a FileInfo. + /// Uses File.OpenHandle with FileOptions.Asynchronous on .NET 8.0+ for optimal performance. + /// Falls back to FileStream constructor with async options on legacy frameworks. + /// + /// The FileInfo to open. + /// Cancellation token. + /// A FileStream configured for asynchronous operations. + public static Stream OpenAsyncReadStream( + this FileInfo fileInfo, + CancellationToken cancellationToken + ) + { + fileInfo.NotNull(nameof(fileInfo)); + return OpenAsyncReadStream(fileInfo.FullName, cancellationToken); + } } diff --git a/src/SharpCompress/Writers/WriterFactory.cs b/src/SharpCompress/Writers/WriterFactory.cs index 43d46b3b..29ff3eed 100644 --- a/src/SharpCompress/Writers/WriterFactory.cs +++ b/src/SharpCompress/Writers/WriterFactory.cs @@ -58,7 +58,7 @@ public static class WriterFactory ) { fileInfo.NotNull(nameof(fileInfo)); - var stream = Utility.OpenAsyncWriteStream(fileInfo.FullName, cancellationToken); + var stream = fileInfo.OpenAsyncWriteStream(cancellationToken); return await OpenAsyncWriter( stream, archiveType, From b5bd4cbf53e88542518fd3f3c531b78d85f10975 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Thu, 12 Feb 2026 12:15:00 +0000 Subject: [PATCH 4/7] fmt --- src/SharpCompress/Utility.Async.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/SharpCompress/Utility.Async.cs b/src/SharpCompress/Utility.Async.cs index 564feb14..26d08ff7 100644 --- a/src/SharpCompress/Utility.Async.cs +++ b/src/SharpCompress/Utility.Async.cs @@ -128,10 +128,7 @@ internal static partial class Utility /// The file path to open. /// Cancellation token. /// A FileStream configured for asynchronous operations. - public static Stream OpenAsyncWriteStream( - string path, - CancellationToken cancellationToken - ) + public static Stream OpenAsyncWriteStream(string path, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); From 158460bc77593ec32423ba7d64aaeaeedea30a1e Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Thu, 12 Feb 2026 13:22:13 +0000 Subject: [PATCH 5/7] fix benchmarks --- tests/SharpCompress.Performance/Benchmarks/TarBenchmarks.cs | 2 +- tests/SharpCompress.Performance/Benchmarks/ZipBenchmarks.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/SharpCompress.Performance/Benchmarks/TarBenchmarks.cs b/tests/SharpCompress.Performance/Benchmarks/TarBenchmarks.cs index 521e7438..921d3282 100644 --- a/tests/SharpCompress.Performance/Benchmarks/TarBenchmarks.cs +++ b/tests/SharpCompress.Performance/Benchmarks/TarBenchmarks.cs @@ -122,7 +122,7 @@ public class TarBenchmarks : ArchiveBenchmarkBase public async Task TarCreateSmallFilesAsync() { using var outputStream = new MemoryStream(); - await using var writer = WriterFactory.OpenAsyncWriter( + await using var writer = await WriterFactory.OpenAsyncWriter( outputStream, ArchiveType.Tar, new WriterOptions(CompressionType.None) { LeaveStreamOpen = true } diff --git a/tests/SharpCompress.Performance/Benchmarks/ZipBenchmarks.cs b/tests/SharpCompress.Performance/Benchmarks/ZipBenchmarks.cs index 375c5690..c848ab45 100644 --- a/tests/SharpCompress.Performance/Benchmarks/ZipBenchmarks.cs +++ b/tests/SharpCompress.Performance/Benchmarks/ZipBenchmarks.cs @@ -98,7 +98,7 @@ public class ZipBenchmarks : ArchiveBenchmarkBase public async Task ZipCreateSmallFilesAsync() { using var outputStream = new MemoryStream(); - await using var writer = WriterFactory.OpenAsyncWriter( + await using var writer = await WriterFactory.OpenAsyncWriter( outputStream, ArchiveType.Zip, new WriterOptions(CompressionType.Deflate) { LeaveStreamOpen = true } From 0d913f6bbc24abc051f9683a241427f87c344f8a Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Sun, 22 Feb 2026 13:19:04 +0000 Subject: [PATCH 6/7] some code fixes --- .../Readers/ReaderFactory.Async.cs | 2 +- .../Readers/Tar/TarReader.Factory.cs | 3 ++- src/SharpCompress/Utility.Async.cs | 8 +++--- src/SharpCompress/Writers/WriterFactory.cs | 26 +++++++++++-------- tests/SharpCompress.Test/WriterTests.cs | 2 +- 5 files changed, 23 insertions(+), 18 deletions(-) diff --git a/src/SharpCompress/Readers/ReaderFactory.Async.cs b/src/SharpCompress/Readers/ReaderFactory.Async.cs index 920b0645..e3f3a4be 100644 --- a/src/SharpCompress/Readers/ReaderFactory.Async.cs +++ b/src/SharpCompress/Readers/ReaderFactory.Async.cs @@ -43,7 +43,7 @@ public static partial class ReaderFactory { options ??= ReaderOptions.ForOwnedFile; var stream = fileInfo.OpenAsyncReadStream(cancellationToken); - return await OpenAsyncReader(stream, options, cancellationToken); + return await OpenAsyncReader(stream, options, cancellationToken).ConfigureAwait(false); } public static async ValueTask OpenAsyncReader( diff --git a/src/SharpCompress/Readers/Tar/TarReader.Factory.cs b/src/SharpCompress/Readers/Tar/TarReader.Factory.cs index ac002eca..13c8453c 100644 --- a/src/SharpCompress/Readers/Tar/TarReader.Factory.cs +++ b/src/SharpCompress/Readers/Tar/TarReader.Factory.cs @@ -141,7 +141,8 @@ public partial class TarReader { readerOptions ??= new ReaderOptions() { LeaveStreamOpen = false }; var stream = fileInfo.OpenAsyncReadStream(cancellationToken); - return await OpenAsyncReader(stream, readerOptions, cancellationToken); + return await OpenAsyncReader(stream, readerOptions, cancellationToken) + .ConfigureAwait(false); } public static IReader OpenReader(string filePath, ReaderOptions? readerOptions = null) diff --git a/src/SharpCompress/Utility.Async.cs b/src/SharpCompress/Utility.Async.cs index dc967568..f717eb31 100644 --- a/src/SharpCompress/Utility.Async.cs +++ b/src/SharpCompress/Utility.Async.cs @@ -133,7 +133,7 @@ internal static partial class Utility { cancellationToken.ThrowIfCancellationRequested(); -#if !LEGACY_DOTNET +#if NET8_0_OR_GREATER // Use File.OpenHandle with async options for .NET 8.0+ var handle = File.OpenHandle( path, @@ -144,7 +144,7 @@ internal static partial class Utility ); return new FileStream(handle, FileAccess.Write); #else - // For legacy .NET, use FileStream constructor with async options + // For older target frameworks, use FileStream constructor with async options return new FileStream( path, FileMode.Create, @@ -185,7 +185,7 @@ internal static partial class Utility { cancellationToken.ThrowIfCancellationRequested(); -#if !LEGACY_DOTNET +#if NET8_0_OR_GREATER // Use File.OpenHandle with async options for .NET 8.0+ var handle = File.OpenHandle( path, @@ -196,7 +196,7 @@ internal static partial class Utility ); return new FileStream(handle, FileAccess.Read); #else - // For legacy .NET, use FileStream constructor with async options + // For older target frameworks, use FileStream constructor with async options return new FileStream( path, FileMode.Open, diff --git a/src/SharpCompress/Writers/WriterFactory.cs b/src/SharpCompress/Writers/WriterFactory.cs index 29ff3eed..48ef1d47 100644 --- a/src/SharpCompress/Writers/WriterFactory.cs +++ b/src/SharpCompress/Writers/WriterFactory.cs @@ -43,11 +43,12 @@ public static class WriterFactory { filePath.NotNullOrEmpty(nameof(filePath)); return await OpenAsyncWriter( - new FileInfo(filePath), - archiveType, - writerOptions, - cancellationToken - ); + new FileInfo(filePath), + archiveType, + writerOptions, + cancellationToken + ) + .ConfigureAwait(false); } public static async ValueTask OpenAsyncWriter( @@ -60,11 +61,12 @@ public static class WriterFactory fileInfo.NotNull(nameof(fileInfo)); var stream = fileInfo.OpenAsyncWriteStream(cancellationToken); return await OpenAsyncWriter( - stream, - archiveType, - writerOptions.WithLeaveStreamOpen(false), - cancellationToken - ); + stream, + archiveType, + writerOptions.WithLeaveStreamOpen(false), + cancellationToken + ) + .ConfigureAwait(false); } public static IWriter OpenWriter( @@ -106,7 +108,9 @@ public static class WriterFactory if (factory != null) { - return await factory.OpenAsyncWriter(stream, writerOptions, cancellationToken); + return await factory + .OpenAsyncWriter(stream, writerOptions, cancellationToken) + .ConfigureAwait(false); } throw new NotSupportedException("Archive Type does not have a Writer: " + archiveType); diff --git a/tests/SharpCompress.Test/WriterTests.cs b/tests/SharpCompress.Test/WriterTests.cs index 2e441735..8ceefa9b 100644 --- a/tests/SharpCompress.Test/WriterTests.cs +++ b/tests/SharpCompress.Test/WriterTests.cs @@ -70,7 +70,7 @@ public class WriterTests : TestBase writerOptions.ArchiveEncoding.Default = encoding ?? Encoding.Default; - using var writer = WriterFactory.OpenAsyncWriter( + await using var writer = await WriterFactory.OpenAsyncWriter( stream, _type, writerOptions, From 8d24dff9676aa05184feaa53e49d2e20a0d2a84c Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Sun, 22 Feb 2026 13:22:37 +0000 Subject: [PATCH 7/7] fix aot --- src/SharpCompress/SharpCompress.csproj | 2 +- src/SharpCompress/packages.lock.json | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/src/SharpCompress/SharpCompress.csproj b/src/SharpCompress/SharpCompress.csproj index bf8579ac..7cf1686a 100644 --- a/src/SharpCompress/SharpCompress.csproj +++ b/src/SharpCompress/SharpCompress.csproj @@ -31,7 +31,7 @@ $(DefineConstants);LEGACY_DOTNET - + true true diff --git a/src/SharpCompress/packages.lock.json b/src/SharpCompress/packages.lock.json index 06de73fa..5059aafe 100644 --- a/src/SharpCompress/packages.lock.json +++ b/src/SharpCompress/packages.lock.json @@ -488,12 +488,6 @@ } }, "net9.0": { - "Microsoft.NET.ILLink.Tasks": { - "type": "Direct", - "requested": "[9.0.11, )", - "resolved": "9.0.11", - "contentHash": "vvB9rtDmWaXgYkViT00KORBVmA3pcYsHlgd9vOPqL9sf5bKy3rvLMF1+sI1uUfVj28S3itirHlHmX5/kcpZKNw==" - }, "Microsoft.NETFramework.ReferenceAssemblies": { "type": "Direct", "requested": "[1.0.3, )",