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();
///