From e84c1ca00781bfc779be993835e884d4744e407d Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Fri, 3 Apr 2026 13:23:06 +0100 Subject: [PATCH] move some files around to make more sense --- .../Polyfills/FileInfoExtensions.cs | 108 +++++++++ .../Polyfills/StreamExtensions.Async.cs | 123 ++++++++++ .../Polyfills/StreamExtensions.cs | 144 ++++++++++- src/SharpCompress/Utility.Async.cs | 227 ------------------ src/SharpCompress/Utility.cs | 144 ----------- 5 files changed, 374 insertions(+), 372 deletions(-) create mode 100644 src/SharpCompress/Polyfills/FileInfoExtensions.cs create mode 100644 src/SharpCompress/Polyfills/StreamExtensions.Async.cs delete mode 100644 src/SharpCompress/Utility.Async.cs diff --git a/src/SharpCompress/Polyfills/FileInfoExtensions.cs b/src/SharpCompress/Polyfills/FileInfoExtensions.cs new file mode 100644 index 00000000..eee131bd --- /dev/null +++ b/src/SharpCompress/Polyfills/FileInfoExtensions.cs @@ -0,0 +1,108 @@ +using System.IO; +using System.Threading; + +namespace SharpCompress; + +public static class FileInfoExtensions +{ + /// + /// 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 NET8_0_OR_GREATER + // 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 older target frameworks, use FileStream constructor with async options + return new FileStream( + path, + FileMode.Create, + FileAccess.Write, + FileShare.None, + bufferSize: 4096, //default + FileOptions.Asynchronous + ); +#endif + } + + /// + /// 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 NET8_0_OR_GREATER + // 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 older target frameworks, use FileStream constructor with async options + return new FileStream( + path, + FileMode.Open, + FileAccess.Read, + FileShare.Read, + bufferSize: 4096, + FileOptions.Asynchronous + ); +#endif + } + + /// The FileInfo to open. + extension(FileInfo fileInfo) + { + /// + /// 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. + /// + /// Cancellation token. + /// A FileStream configured for asynchronous operations. + public Stream OpenAsyncReadStream(CancellationToken cancellationToken) + { + fileInfo.NotNull(nameof(fileInfo)); + return OpenAsyncReadStream(fileInfo.FullName, cancellationToken); + } + + /// + /// 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 Stream OpenAsyncWriteStream(CancellationToken cancellationToken) + { + fileInfo.NotNull(nameof(fileInfo)); + return OpenAsyncWriteStream(fileInfo.FullName, cancellationToken); + } + } +} diff --git a/src/SharpCompress/Polyfills/StreamExtensions.Async.cs b/src/SharpCompress/Polyfills/StreamExtensions.Async.cs new file mode 100644 index 00000000..808fb4f3 --- /dev/null +++ b/src/SharpCompress/Polyfills/StreamExtensions.Async.cs @@ -0,0 +1,123 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Common; + +namespace SharpCompress; + +public static partial class StreamExtensions +{ + extension(Stream source) + { + /// + /// Read exactly the requested number of bytes from a stream asynchronously. Throws EndOfStreamException if not enough data is available. + /// + public async ValueTask ReadExactAsync( + byte[] buffer, + int offset, + int length, + CancellationToken cancellationToken = default + ) + { +#if LEGACY_DOTNET + if (source is null) + { + throw new ArgumentNullException(); + } +#else + ThrowHelper.ThrowIfNull(source); +#endif + + ThrowHelper.ThrowIfNull(buffer); + + if (offset < 0 || offset > buffer.Length) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + + if (length < 0 || length > buffer.Length - offset) + { + throw new ArgumentOutOfRangeException(nameof(length)); + } + + while (length > 0) + { + var fetched = await source + .ReadAsync(buffer, offset, length, cancellationToken) + .ConfigureAwait(false); + if (fetched <= 0) + { + throw new IncompleteArchiveException("Unexpected end of stream."); + } + + offset += fetched; + length -= fetched; + } + } + + public async ValueTask TransferToAsync( + Stream destination, + long maxLength, + CancellationToken cancellationToken = default + ) + { + // Use ReadOnlySubStream to limit reading and leverage framework's CopyToAsync + using var limitedStream = new IO.ReadOnlySubStream(source, maxLength); + await limitedStream + .CopyToAsync(destination, Constants.BufferSize, cancellationToken) + .ConfigureAwait(false); + return limitedStream.Position; + } + + public async ValueTask ReadFullyAsync( + byte[] buffer, + CancellationToken cancellationToken = default + ) + { + var total = 0; + int read; + while ( + ( + read = await source + .ReadAsync(buffer, total, buffer.Length - total, cancellationToken) + .ConfigureAwait(false) + ) > 0 + ) + { + total += read; + if (total >= buffer.Length) + { + return true; + } + } + return (total >= buffer.Length); + } + + public async ValueTask ReadFullyAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken = default + ) + { + var total = 0; + int read; + while ( + ( + read = await source + .ReadAsync(buffer, offset + total, count - total, cancellationToken) + .ConfigureAwait(false) + ) > 0 + ) + { + total += read; + if (total >= count) + { + return true; + } + } + return (total >= count); + } + } +} diff --git a/src/SharpCompress/Polyfills/StreamExtensions.cs b/src/SharpCompress/Polyfills/StreamExtensions.cs index 18c5b593..404d7419 100644 --- a/src/SharpCompress/Polyfills/StreamExtensions.cs +++ b/src/SharpCompress/Polyfills/StreamExtensions.cs @@ -3,11 +3,12 @@ using System.Buffers; using System.IO; using System.Threading; using System.Threading.Tasks; +using SharpCompress.Common; using SharpCompress.IO; namespace SharpCompress; -public static class StreamExtensions +public static partial class StreamExtensions { extension(Stream stream) { @@ -68,5 +69,146 @@ public static class StreamExtensions ArrayPool.Shared.Return(temp); } } + + public long TransferTo(Stream destination, long maxLength) + { + // Use ReadOnlySubStream to limit reading and leverage framework's CopyTo + using var limitedStream = new IO.ReadOnlySubStream(stream, maxLength); + limitedStream.CopyTo(destination, Constants.BufferSize); + return limitedStream.Position; + } + + public async ValueTask SkipAsync( + long advanceAmount, + CancellationToken cancellationToken = default + ) + { + if (stream.CanSeek && stream is not SharpCompressStream) + { + stream.Position += advanceAmount; + return; + } + + var array = ArrayPool.Shared.Rent(Constants.BufferSize); + try + { + while (advanceAmount > 0) + { + var toRead = (int)Math.Min(array.Length, advanceAmount); + var read = await stream + .ReadAsync(array, 0, toRead, cancellationToken) + .ConfigureAwait(false); + if (read <= 0) + { + break; + } + + advanceAmount -= read; + } + } + finally + { + ArrayPool.Shared.Return(array); + } + } + +#if NET8_0_OR_GREATER + public bool ReadFully(byte[] buffer) + { + try + { + stream.ReadExactly(buffer); + return true; + } + catch (EndOfStreamException) + { + return false; + } + } + + public bool ReadFully(Span buffer) + { + try + { + stream.ReadExactly(buffer); + return true; + } + catch (EndOfStreamException) + { + return false; + } + } +#else + public bool ReadFully(byte[] buffer) + { + var total = 0; + int read; + while ((read = stream.Read(buffer, total, buffer.Length - total)) > 0) + { + total += read; + if (total >= buffer.Length) + { + return true; + } + } + + return (total >= buffer.Length); + } + + public bool ReadFully(Span buffer) + { + var total = 0; + int read; + while ((read = stream.Read(buffer.Slice(total, buffer.Length - total))) > 0) + { + total += read; + if (total >= buffer.Length) + { + return true; + } + } + + return (total >= buffer.Length); + } +#endif + + /// + /// Read exactly the requested number of bytes from a stream. Throws EndOfStreamException if not enough data is available. + /// + public void ReadExact(byte[] buffer, int offset, int length) + { +#if LEGACY_DOTNET + if (stream is null) + { + throw new ArgumentNullException(); + } +#else + ThrowHelper.ThrowIfNull(stream); +#endif + + ThrowHelper.ThrowIfNull(buffer); + + if (offset < 0 || offset > buffer.Length) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + + if (length < 0 || length > buffer.Length - offset) + { + throw new ArgumentOutOfRangeException(nameof(length)); + } + + while (length > 0) + { + var fetched = stream.Read(buffer, offset, length); + if (fetched <= 0) + { + throw new IncompleteArchiveException("Unexpected end of stream."); + } + + offset += fetched; + length -= fetched; + } + } } } diff --git a/src/SharpCompress/Utility.Async.cs b/src/SharpCompress/Utility.Async.cs deleted file mode 100644 index f717eb31..00000000 --- a/src/SharpCompress/Utility.Async.cs +++ /dev/null @@ -1,227 +0,0 @@ -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using SharpCompress.Common; - -namespace SharpCompress; - -internal static partial class Utility -{ - extension(Stream source) - { - /// - /// Read exactly the requested number of bytes from a stream asynchronously. Throws EndOfStreamException if not enough data is available. - /// - public async ValueTask ReadExactAsync( - byte[] buffer, - int offset, - int length, - CancellationToken cancellationToken = default - ) - { -#if LEGACY_DOTNET - if (source is null) - { - throw new ArgumentNullException(); - } -#else - ThrowHelper.ThrowIfNull(source); -#endif - - ThrowHelper.ThrowIfNull(buffer); - - if (offset < 0 || offset > buffer.Length) - { - throw new ArgumentOutOfRangeException(nameof(offset)); - } - - if (length < 0 || length > buffer.Length - offset) - { - throw new ArgumentOutOfRangeException(nameof(length)); - } - - while (length > 0) - { - var fetched = await source - .ReadAsync(buffer, offset, length, cancellationToken) - .ConfigureAwait(false); - if (fetched <= 0) - { - throw new IncompleteArchiveException("Unexpected end of stream."); - } - - offset += fetched; - length -= fetched; - } - } - - public async ValueTask TransferToAsync( - Stream destination, - long maxLength, - CancellationToken cancellationToken = default - ) - { - // Use ReadOnlySubStream to limit reading and leverage framework's CopyToAsync - using var limitedStream = new IO.ReadOnlySubStream(source, maxLength); - await limitedStream - .CopyToAsync(destination, Constants.BufferSize, cancellationToken) - .ConfigureAwait(false); - return limitedStream.Position; - } - - public async ValueTask ReadFullyAsync( - byte[] buffer, - CancellationToken cancellationToken = default - ) - { - var total = 0; - int read; - while ( - ( - read = await source - .ReadAsync(buffer, total, buffer.Length - total, cancellationToken) - .ConfigureAwait(false) - ) > 0 - ) - { - total += read; - if (total >= buffer.Length) - { - return true; - } - } - return (total >= buffer.Length); - } - - public async ValueTask ReadFullyAsync( - byte[] buffer, - int offset, - int count, - CancellationToken cancellationToken = default - ) - { - var total = 0; - int read; - while ( - ( - read = await source - .ReadAsync(buffer, offset + total, count - total, cancellationToken) - .ConfigureAwait(false) - ) > 0 - ) - { - total += read; - if (total >= count) - { - return true; - } - } - 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 NET8_0_OR_GREATER - // 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 older target frameworks, use FileStream constructor with async options - return new FileStream( - path, - 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 NET8_0_OR_GREATER - // 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 older target frameworks, 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/Utility.cs b/src/SharpCompress/Utility.cs index af8572d7..4e662f36 100644 --- a/src/SharpCompress/Utility.cs +++ b/src/SharpCompress/Utility.cs @@ -138,150 +138,6 @@ internal static partial class Utility return sTime.AddSeconds(unixtime); } - extension(Stream source) - { - public long TransferTo(Stream destination, long maxLength) - { - // Use ReadOnlySubStream to limit reading and leverage framework's CopyTo - using var limitedStream = new IO.ReadOnlySubStream(source, maxLength); - limitedStream.CopyTo(destination, Constants.BufferSize); - return limitedStream.Position; - } - - public async ValueTask SkipAsync( - long advanceAmount, - CancellationToken cancellationToken = default - ) - { - if (source.CanSeek && source is not SharpCompressStream) - { - source.Position += advanceAmount; - return; - } - - var array = ArrayPool.Shared.Rent(Constants.BufferSize); - try - { - while (advanceAmount > 0) - { - var toRead = (int)Math.Min(array.Length, advanceAmount); - var read = await source - .ReadAsync(array, 0, toRead, cancellationToken) - .ConfigureAwait(false); - if (read <= 0) - { - break; - } - - advanceAmount -= read; - } - } - finally - { - ArrayPool.Shared.Return(array); - } - } - -#if NET8_0_OR_GREATER - public bool ReadFully(byte[] buffer) - { - try - { - source.ReadExactly(buffer); - return true; - } - catch (EndOfStreamException) - { - return false; - } - } - - public bool ReadFully(Span buffer) - { - try - { - source.ReadExactly(buffer); - return true; - } - catch (EndOfStreamException) - { - return false; - } - } -#else - public bool ReadFully(byte[] buffer) - { - var total = 0; - int read; - while ((read = source.Read(buffer, total, buffer.Length - total)) > 0) - { - total += read; - if (total >= buffer.Length) - { - return true; - } - } - - return (total >= buffer.Length); - } - - public bool ReadFully(Span buffer) - { - var total = 0; - int read; - while ((read = source.Read(buffer.Slice(total, buffer.Length - total))) > 0) - { - total += read; - if (total >= buffer.Length) - { - return true; - } - } - - return (total >= buffer.Length); - } -#endif - - /// - /// Read exactly the requested number of bytes from a stream. Throws EndOfStreamException if not enough data is available. - /// - public void ReadExact(byte[] buffer, int offset, int length) - { -#if LEGACY_DOTNET - if (source is null) - { - throw new ArgumentNullException(); - } -#else - ThrowHelper.ThrowIfNull(source); -#endif - - ThrowHelper.ThrowIfNull(buffer); - - if (offset < 0 || offset > buffer.Length) - { - throw new ArgumentOutOfRangeException(nameof(offset)); - } - - if (length < 0 || length > buffer.Length - offset) - { - throw new ArgumentOutOfRangeException(nameof(length)); - } - - while (length > 0) - { - var fetched = source.Read(buffer, offset, length); - if (fetched <= 0) - { - throw new IncompleteArchiveException("Unexpected end of stream."); - } - - offset += fetched; - length -= fetched; - } - } - } - public static string TrimNulls(this string source) => source.Replace('\0', ' ').Trim(); ///