From 0333b565d22db8b0560f65a26d39dd9d93d2a790 Mon Sep 17 00:00:00 2001 From: alinpahontu2912 Date: Wed, 10 Jun 2026 13:27:59 +0300 Subject: [PATCH 01/10] add readwriteradapter for tarhelpers --- .../src/System.Formats.Tar.csproj | 1 + .../src/System/Formats/Tar/TarHeader.Read.cs | 258 +++++------------- .../src/System/Formats/Tar/TarHelpers.cs | 104 +++---- .../System/Formats/Tar/TarReadWriteAdapter.cs | 70 +++++ 4 files changed, 190 insertions(+), 243 deletions(-) create mode 100644 src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarReadWriteAdapter.cs diff --git a/src/libraries/System.Formats.Tar/src/System.Formats.Tar.csproj b/src/libraries/System.Formats.Tar/src/System.Formats.Tar.csproj index 7311a76f27102b..abd591acbc6df5 100644 --- a/src/libraries/System.Formats.Tar/src/System.Formats.Tar.csproj +++ b/src/libraries/System.Formats.Tar/src/System.Formats.Tar.csproj @@ -35,6 +35,7 @@ + diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Read.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Read.cs index 2673bd4e21aa16..2fd0b1a274bfcb 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Read.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Read.cs @@ -19,44 +19,48 @@ internal sealed partial class TarHeader // Attempts to retrieve the next header from the specified tar archive stream. // Throws if end of stream is reached or if any data type conversion fails. // Returns a valid TarHeader object if the attributes were read successfully, null otherwise. - internal static unsafe TarHeader? TryGetNextHeader(Stream archiveStream, bool copyData, TarEntryFormat initialFormat, bool processDataBlock) + internal static TarHeader? TryGetNextHeader(Stream archiveStream, bool copyData, TarEntryFormat initialFormat, bool processDataBlock) { - // The four supported formats have a header that fits in the default record size - Span buffer = stackalloc byte[TarHelpers.RecordSize]; - - archiveStream.ReadExactly(buffer); - - TarHeader? header = TryReadAttributes(initialFormat, buffer, archiveStream); - if (header != null && processDataBlock) - { - header.ProcessDataBlock(archiveStream, copyData); - } - - return header; + ValueTask vt = TryGetNextHeaderCoreAsync(archiveStream, copyData, initialFormat, processDataBlock, CancellationToken.None); + Debug.Assert(vt.IsCompleted, "Synchronous TryGetNextHeader completed asynchronously."); + return vt.GetAwaiter().GetResult(); } // Asynchronously attempts read all the fields of the next header. // Throws if end of stream is reached or if any data type conversion fails. // Returns true if all the attributes were read successfully, false otherwise. - internal static async ValueTask TryGetNextHeaderAsync(Stream archiveStream, bool copyData, TarEntryFormat initialFormat, bool processDataBlock, CancellationToken cancellationToken) + internal static ValueTask TryGetNextHeaderAsync(Stream archiveStream, bool copyData, TarEntryFormat initialFormat, bool processDataBlock, CancellationToken cancellationToken) { - cancellationToken.ThrowIfCancellationRequested(); + if (cancellationToken.IsCancellationRequested) + { + return ValueTask.FromCanceled(cancellationToken); + } + return TryGetNextHeaderCoreAsync(archiveStream, copyData, initialFormat, processDataBlock, cancellationToken); + } + + private static async ValueTask TryGetNextHeaderCoreAsync(Stream archiveStream, bool copyData, TarEntryFormat initialFormat, bool processDataBlock, CancellationToken cancellationToken) + where TAdapter : IReadWriteAdapter + { // The four supported formats have a header that fits in the default record size byte[] rented = ArrayPool.Shared.Rent(minimumLength: TarHelpers.RecordSize); Memory buffer = rented.AsMemory(0, TarHelpers.RecordSize); // minimumLength means the array could've been larger + try + { + await TAdapter.ReadExactlyAsync(archiveStream, buffer, cancellationToken).ConfigureAwait(false); - await archiveStream.ReadExactlyAsync(buffer, cancellationToken).ConfigureAwait(false); + TarHeader? header = TryReadAttributes(initialFormat, buffer.Span, archiveStream); + if (header != null && processDataBlock) + { + await header.ProcessDataBlockCoreAsync(archiveStream, copyData, cancellationToken).ConfigureAwait(false); + } - TarHeader? header = TryReadAttributes(initialFormat, buffer.Span, archiveStream); - if (header != null && processDataBlock) + return header; + } + finally { - await header.ProcessDataBlockAsync(archiveStream, copyData, cancellationToken).ConfigureAwait(false); + ArrayPool.Shared.Return(rented); } - - ArrayPool.Shared.Return(rented); - - return header; } private static TarHeader? TryReadAttributes(TarEntryFormat initialFormat, ReadOnlySpan buffer, Stream archiveStream) @@ -233,16 +237,24 @@ internal void ReplaceNormalAttributesWithExtended(IEnumerable(archiveStream, copyData, CancellationToken.None); + Debug.Assert(vt.IsCompleted, "Synchronous ProcessDataBlock completed asynchronously."); + vt.GetAwaiter().GetResult(); + } + + internal async ValueTask ProcessDataBlockCoreAsync(Stream archiveStream, bool copyData, CancellationToken cancellationToken) + where TAdapter : IReadWriteAdapter { bool skipBlockAlignmentPadding = true; switch (_typeFlag) { case TarEntryType.ExtendedAttributes or TarEntryType.GlobalExtendedAttributes: - ReadExtendedAttributesBlock(archiveStream); + await ReadExtendedAttributesBlockCoreAsync(archiveStream, cancellationToken).ConfigureAwait(false); break; case TarEntryType.LongLink or TarEntryType.LongPath: - ReadGnuLongPathDataBlock(archiveStream); + await ReadGnuLongPathDataBlockCoreAsync(archiveStream, cancellationToken).ConfigureAwait(false); break; case TarEntryType.BlockDevice: case TarEntryType.CharacterDevice: @@ -265,7 +277,7 @@ internal void ProcessDataBlock(Stream archiveStream, bool copyData) case TarEntryType.SparseFile: // Contains portion of a file case TarEntryType.TapeVolume: // Might contain data default: // Unrecognized entry types could potentially have a data section - _dataStream = GetDataStream(archiveStream, copyData); + _dataStream = await GetDataStreamCoreAsync(archiveStream, copyData, cancellationToken).ConfigureAwait(false); // GNU sparse format 1.0 PAX entries embed a sparse map at the start of the // data section. Create a GnuSparseStream wrapper that presents the expanded @@ -281,78 +293,7 @@ internal void ProcessDataBlock(Stream archiveStream, bool copyData) { if (archiveStream.CanSeek) { - TarHelpers.AdvanceStream(archiveStream, _size); - } - else - { - // This stream gives the user the chance to optionally read the data section - // when the underlying archive stream is unseekable - skipBlockAlignmentPadding = false; - } - } - - break; - } - - if (skipBlockAlignmentPadding) - { - if (_size > 0) - { - TarHelpers.SkipBlockAlignmentPadding(archiveStream, _size); - } - - if (archiveStream.CanSeek) - { - _endOfHeaderAndDataAndBlockAlignment = archiveStream.Position; - } - } - } - - private async Task ProcessDataBlockAsync(Stream archiveStream, bool copyData, CancellationToken cancellationToken) - { - bool skipBlockAlignmentPadding = true; - - switch (_typeFlag) - { - case TarEntryType.ExtendedAttributes or TarEntryType.GlobalExtendedAttributes: - await ReadExtendedAttributesBlockAsync(archiveStream, cancellationToken).ConfigureAwait(false); - break; - case TarEntryType.LongLink or TarEntryType.LongPath: - await ReadGnuLongPathDataBlockAsync(archiveStream, cancellationToken).ConfigureAwait(false); - break; - case TarEntryType.BlockDevice: - case TarEntryType.CharacterDevice: - case TarEntryType.Directory: - case TarEntryType.Fifo: - case TarEntryType.HardLink: - case TarEntryType.SymbolicLink: - // No data section - if (_size > 0) - { - throw new InvalidDataException(SR.Format(SR.TarSizeFieldTooLargeForEntryType, _typeFlag)); - } - break; - case TarEntryType.RegularFile: - case TarEntryType.V7RegularFile: // Treated as regular file - case TarEntryType.ContiguousFile: // Treated as regular file - case TarEntryType.DirectoryList: // Contains the list of filesystem entries in the data section - case TarEntryType.MultiVolume: // Contains portion of a file - case TarEntryType.RenamedOrSymlinked: // Might contain data - case TarEntryType.SparseFile: // Contains portion of a file - case TarEntryType.TapeVolume: // Might contain data - default: // Unrecognized entry types could potentially have a data section - _dataStream = await GetDataStreamAsync(archiveStream, copyData, _size, cancellationToken).ConfigureAwait(false); - - if (_isGnuSparse10 && _gnuSparseRealSize > 0 && _dataStream is not null) - { - _gnuSparseDataStream = new GnuSparseStream(_dataStream, _gnuSparseRealSize); - } - - if (_dataStream is SubReadStream) - { - if (archiveStream.CanSeek) - { - await TarHelpers.AdvanceStreamAsync(archiveStream, _size, cancellationToken).ConfigureAwait(false); + await TarHelpers.AdvanceStreamCoreAsync(archiveStream, _size, cancellationToken).ConfigureAwait(false); } else { @@ -369,7 +310,7 @@ private async Task ProcessDataBlockAsync(Stream archiveStream, bool copyData, Ca { if (_size > 0) { - await TarHelpers.SkipBlockAlignmentPaddingAsync(archiveStream, _size, cancellationToken).ConfigureAwait(false); + await TarHelpers.SkipBlockAlignmentPaddingCoreAsync(archiveStream, _size, cancellationToken).ConfigureAwait(false); } if (archiveStream.CanSeek) @@ -383,7 +324,8 @@ private async Task ProcessDataBlockAsync(Stream archiveStream, bool copyData, Ca // If copyData is true, then a total number of _size bytes will be copied to a new MemoryStream, which is then returned. // Otherwise, if the archive stream is seekable, returns a seekable wrapper stream. // Otherwise, it returns an unseekable wrapper stream. - private Stream? GetDataStream(Stream archiveStream, bool copyData) + private async ValueTask GetDataStreamCoreAsync(Stream archiveStream, bool copyData, CancellationToken cancellationToken) + where TAdapter : IReadWriteAdapter { if (_size == 0) { @@ -393,7 +335,7 @@ private async Task ProcessDataBlockAsync(Stream archiveStream, bool copyData, Ca if (copyData) { MemoryStream copiedData = new MemoryStream(); - TarHelpers.CopyBytes(archiveStream, copiedData, _size); + await TarHelpers.CopyBytesCoreAsync(archiveStream, copiedData, _size, cancellationToken).ConfigureAwait(false); // Reset position pointer so the user can do the first DataStream read from the beginning copiedData.Position = 0; return copiedData; @@ -402,31 +344,6 @@ private async Task ProcessDataBlockAsync(Stream archiveStream, bool copyData, Ca return new SubReadStream(archiveStream, archiveStream.CanSeek ? archiveStream.Position : 0, _size); } - // Asynchronously returns a stream that represents the data section of the current header. - // If copyData is true, then a total number of _size bytes will be copied to a new MemoryStream, which is then returned. - // Otherwise, if the archive stream is seekable, returns a seekable wrapper stream. - // Otherwise, it returns an unseekable wrapper stream. - private static async ValueTask GetDataStreamAsync(Stream archiveStream, bool copyData, long size, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (size == 0) - { - return null; - } - - if (copyData) - { - MemoryStream copiedData = new MemoryStream(); - await TarHelpers.CopyBytesAsync(archiveStream, copiedData, size, cancellationToken).ConfigureAwait(false); - // Reset position pointer so the user can do the first DataStream read from the beginning - copiedData.Position = 0; - return copiedData; - } - - return new SubReadStream(archiveStream, archiveStream.CanSeek ? archiveStream.Position : 0, size); - } - // Attempts to read the fields shared by all formats and stores them in their expected data type. // Throws if any data type conversion fails. // Returns true on success, false if checksum is zero. @@ -683,48 +600,26 @@ private void ReadUstarAttributes(ReadOnlySpan buffer) // Collects the extended attributes found in the data section of a PAX entry of type 'x' or 'g'. // Throws if end of stream is reached or if an attribute is malformed. - private void ReadExtendedAttributesBlock(Stream archiveStream) + private async ValueTask ReadExtendedAttributesBlockCoreAsync(Stream archiveStream, CancellationToken cancellationToken) + where TAdapter : IReadWriteAdapter { - long size = _size; - if (size != 0) + if (_size != 0) { ValidateSize(); - - byte[]? buffer = null; - Span span = (ulong)size <= 256 ? - stackalloc byte[256] : - (buffer = ArrayPool.Shared.Rent((int)size)); - span = span.Slice(0, (int)size); - - archiveStream.ReadExactly(span); - ReadExtendedAttributesFromBuffer(span, _name); - - if (buffer is not null) + byte[] buffer = ArrayPool.Shared.Rent((int)_size); + try + { + Memory memory = buffer.AsMemory(0, (int)_size); + await TAdapter.ReadExactlyAsync(archiveStream, memory, cancellationToken).ConfigureAwait(false); + ReadExtendedAttributesFromBuffer(memory.Span, _name); + } + finally { ArrayPool.Shared.Return(buffer); } } } - // Asynchronously collects the extended attributes found in the data section of a PAX entry of type 'x' or 'g'. - // Throws if end of stream is reached or if an attribute is malformed. - private async ValueTask ReadExtendedAttributesBlockAsync(Stream archiveStream, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (_size != 0) - { - ValidateSize(); - byte[] buffer = ArrayPool.Shared.Rent((int)_size); - Memory memory = buffer.AsMemory(0, (int)_size); - - await archiveStream.ReadExactlyAsync(memory, cancellationToken).ConfigureAwait(false); - ReadExtendedAttributesFromBuffer(memory.Span, _name); - - ArrayPool.Shared.Return(buffer); - } - } - private void ValidateSize() { if ((ulong)_size > (ulong)MaxMetadataBlockSize) @@ -757,49 +652,26 @@ private void ReadExtendedAttributesFromBuffer(ReadOnlySpan buffer, string // Reads the long path found in the data section of a GNU entry of type 'K' or 'L' // and replaces Name or LinkName, respectively, with the found string. // Throws if end of stream is reached. - private void ReadGnuLongPathDataBlock(Stream archiveStream) + private async ValueTask ReadGnuLongPathDataBlockCoreAsync(Stream archiveStream, CancellationToken cancellationToken) + where TAdapter : IReadWriteAdapter { - long size = _size; - if (size != 0) + if (_size != 0) { ValidateSize(); - - byte[]? buffer = null; - Span span = (ulong)size <= 256 ? - stackalloc byte[256] : - (buffer = ArrayPool.Shared.Rent((int)size)); - span = span.Slice(0, (int)size); - - archiveStream.ReadExactly(span); - ReadGnuLongPathDataFromBuffer(span); - - if (buffer is not null) + byte[] buffer = ArrayPool.Shared.Rent((int)_size); + try + { + Memory memory = buffer.AsMemory(0, (int)_size); + await TAdapter.ReadExactlyAsync(archiveStream, memory, cancellationToken).ConfigureAwait(false); + ReadGnuLongPathDataFromBuffer(memory.Span); + } + finally { ArrayPool.Shared.Return(buffer); } } } - // Asynchronously reads the long path found in the data section of a GNU entry of type 'K' or 'L' - // and replaces Name or LinkName, respectively, with the found string. - // Throws if end of stream is reached. - private async ValueTask ReadGnuLongPathDataBlockAsync(Stream archiveStream, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (_size != 0) - { - ValidateSize(); - byte[] buffer = ArrayPool.Shared.Rent((int)_size); - Memory memory = buffer.AsMemory(0, (int)_size); - - await archiveStream.ReadExactlyAsync(memory, cancellationToken).ConfigureAwait(false); - ReadGnuLongPathDataFromBuffer(memory.Span); - - ArrayPool.Shared.Return(buffer); - } - } - // Collects the GNU long path info from the buffer and sets it in the right field depending on the type flag. private void ReadGnuLongPathDataFromBuffer(ReadOnlySpan buffer) { diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHelpers.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHelpers.cs index 18f4d56f7ddb03..4503129d2177c3 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHelpers.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHelpers.cs @@ -11,6 +11,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; +using System.Diagnostics; namespace System.Formats.Tar { @@ -51,28 +52,25 @@ internal static int GetDefaultMode(TarEntryType type) // Helps advance the stream a total number of bytes larger than int.MaxValue. internal static void AdvanceStream(Stream archiveStream, long bytesToDiscard) { - if (archiveStream.CanSeek) - { - archiveStream.Position += bytesToDiscard; - } - else if (bytesToDiscard > 0) - { - byte[] buffer = ArrayPool.Shared.Rent(minimumLength: (int)Math.Min(MaxBufferLength, bytesToDiscard)); - while (bytesToDiscard > 0) - { - int currentLengthToRead = (int)Math.Min(MaxBufferLength, bytesToDiscard); - archiveStream.ReadExactly(buffer.AsSpan(0, currentLengthToRead)); - bytesToDiscard -= currentLengthToRead; - } - ArrayPool.Shared.Return(buffer); - } + ValueTask vt = AdvanceStreamCoreAsync(archiveStream, bytesToDiscard, CancellationToken.None); + Debug.Assert(vt.IsCompleted, "Synchronous AdvanceStream completed asynchronously."); + vt.GetAwaiter().GetResult(); } // Asynchronously helps advance the stream a total number of bytes larger than int.MaxValue. - internal static async ValueTask AdvanceStreamAsync(Stream archiveStream, long bytesToDiscard, CancellationToken cancellationToken) + internal static ValueTask AdvanceStreamAsync(Stream archiveStream, long bytesToDiscard, CancellationToken cancellationToken) { - cancellationToken.ThrowIfCancellationRequested(); + if (cancellationToken.IsCancellationRequested) + { + return ValueTask.FromCanceled(cancellationToken); + } + + return AdvanceStreamCoreAsync(archiveStream, bytesToDiscard, cancellationToken); + } + internal static async ValueTask AdvanceStreamCoreAsync(Stream archiveStream, long bytesToDiscard, CancellationToken cancellationToken) + where TAdapter : IReadWriteAdapter + { if (archiveStream.CanSeek) { archiveStream.Position += bytesToDiscard; @@ -80,45 +78,42 @@ internal static async ValueTask AdvanceStreamAsync(Stream archiveStream, long by else if (bytesToDiscard > 0) { byte[] buffer = ArrayPool.Shared.Rent(minimumLength: (int)Math.Min(MaxBufferLength, bytesToDiscard)); - while (bytesToDiscard > 0) + try { - int currentLengthToRead = (int)Math.Min(MaxBufferLength, bytesToDiscard); - await archiveStream.ReadExactlyAsync(buffer, 0, currentLengthToRead, cancellationToken).ConfigureAwait(false); - bytesToDiscard -= currentLengthToRead; + while (bytesToDiscard > 0) + { + int currentLengthToRead = (int)Math.Min(MaxBufferLength, bytesToDiscard); + await TAdapter.ReadExactlyAsync(archiveStream, buffer.AsMemory(0, currentLengthToRead), cancellationToken).ConfigureAwait(false); + bytesToDiscard -= currentLengthToRead; + } + } + finally + { + ArrayPool.Shared.Return(buffer); } - ArrayPool.Shared.Return(buffer); } } // Helps copy a specific number of bytes from one stream into another. - internal static void CopyBytes(Stream origin, Stream destination, long bytesToCopy) + internal static async ValueTask CopyBytesCoreAsync(Stream origin, Stream destination, long bytesToCopy, CancellationToken cancellationToken) + where TAdapter : IReadWriteAdapter { byte[] buffer = ArrayPool.Shared.Rent(minimumLength: (int)Math.Min(MaxBufferLength, bytesToCopy)); - while (bytesToCopy > 0) + try { - int currentLengthToRead = (int)Math.Min(MaxBufferLength, bytesToCopy); - origin.ReadExactly(buffer.AsSpan(0, currentLengthToRead)); - destination.Write(buffer.AsSpan(0, currentLengthToRead)); - bytesToCopy -= currentLengthToRead; + while (bytesToCopy > 0) + { + int currentLengthToRead = (int)Math.Min(MaxBufferLength, bytesToCopy); + Memory memory = buffer.AsMemory(0, currentLengthToRead); + await TAdapter.ReadExactlyAsync(origin, memory, cancellationToken).ConfigureAwait(false); + await TAdapter.WriteAsync(destination, memory, cancellationToken).ConfigureAwait(false); + bytesToCopy -= currentLengthToRead; + } } - ArrayPool.Shared.Return(buffer); - } - - // Asynchronously helps copy a specific number of bytes from one stream into another. - internal static async ValueTask CopyBytesAsync(Stream origin, Stream destination, long bytesToCopy, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - - byte[] buffer = ArrayPool.Shared.Rent(minimumLength: (int)Math.Min(MaxBufferLength, bytesToCopy)); - while (bytesToCopy > 0) + finally { - int currentLengthToRead = (int)Math.Min(MaxBufferLength, bytesToCopy); - Memory memory = buffer.AsMemory(0, currentLengthToRead); - await origin.ReadExactlyAsync(buffer, 0, currentLengthToRead, cancellationToken).ConfigureAwait(false); - await destination.WriteAsync(memory, cancellationToken).ConfigureAwait(false); - bytesToCopy -= currentLengthToRead; + ArrayPool.Shared.Return(buffer); } - ArrayPool.Shared.Return(buffer); } // Returns the number of bytes until the next multiple of the record size. @@ -295,20 +290,29 @@ internal static ReadOnlySpan TrimNullTerminated(ReadOnlySpan buffer) // set the stream position to the first byte of the next entry. internal static int SkipBlockAlignmentPadding(Stream archiveStream, long size) { - int bytesToSkip = CalculatePadding(size); - AdvanceStream(archiveStream, bytesToSkip); - return bytesToSkip; + ValueTask vt = SkipBlockAlignmentPaddingCoreAsync(archiveStream, size, CancellationToken.None); + Debug.Assert(vt.IsCompleted, "Synchronous SkipBlockAlignmentPadding completed asynchronously."); + return vt.GetAwaiter().GetResult(); } // After the file contents, there may be zero or more null characters, // which exist to ensure the data is aligned to the record size. // Asynchronously skip them and set the stream position to the first byte of the next entry. - internal static async ValueTask SkipBlockAlignmentPaddingAsync(Stream archiveStream, long size, CancellationToken cancellationToken) + internal static ValueTask SkipBlockAlignmentPaddingAsync(Stream archiveStream, long size, CancellationToken cancellationToken) { - cancellationToken.ThrowIfCancellationRequested(); + if (cancellationToken.IsCancellationRequested) + { + return ValueTask.FromCanceled(cancellationToken); + } + + return SkipBlockAlignmentPaddingCoreAsync(archiveStream, size, cancellationToken); + } + internal static async ValueTask SkipBlockAlignmentPaddingCoreAsync(Stream archiveStream, long size, CancellationToken cancellationToken) + where TAdapter : IReadWriteAdapter + { int bytesToSkip = CalculatePadding(size); - await AdvanceStreamAsync(archiveStream, bytesToSkip, cancellationToken).ConfigureAwait(false); + await AdvanceStreamCoreAsync(archiveStream, bytesToSkip, cancellationToken).ConfigureAwait(false); return bytesToSkip; } diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarReadWriteAdapter.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarReadWriteAdapter.cs new file mode 100644 index 00000000000000..a3cb3350057fba --- /dev/null +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarReadWriteAdapter.cs @@ -0,0 +1,70 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace System.Formats.Tar +{ + // Static-abstract adapter that lets a single generic implementation serve both + // synchronous and asynchronous Tar code paths. The sync entry point passes + // SyncReadWriteAdapter and asserts the returned ValueTask is already completed; + // the async entry point passes AsyncReadWriteAdapter. Mirrors the IReadWriteAdapter + // pattern used by SslStream / SmtpClient (see src/libraries/Common/src/System/Net/ReadWriteAdapter.cs). + internal interface IReadWriteAdapter + { + static abstract ValueTask ReadAsync(Stream stream, Memory buffer, CancellationToken cancellationToken); + static abstract ValueTask ReadExactlyAsync(Stream stream, Memory buffer, CancellationToken cancellationToken); + static abstract ValueTask WriteAsync(Stream stream, ReadOnlyMemory buffer, CancellationToken cancellationToken); + static abstract ValueTask CopyToAsync(Stream source, Stream destination, CancellationToken cancellationToken); + static abstract ValueTask DisposeAsync(Stream stream); + } + + internal readonly struct AsyncReadWriteAdapter : IReadWriteAdapter + { + public static ValueTask ReadAsync(Stream stream, Memory buffer, CancellationToken cancellationToken) => + stream.ReadAsync(buffer, cancellationToken); + + public static ValueTask ReadExactlyAsync(Stream stream, Memory buffer, CancellationToken cancellationToken) => + stream.ReadExactlyAsync(buffer, cancellationToken); + + public static ValueTask WriteAsync(Stream stream, ReadOnlyMemory buffer, CancellationToken cancellationToken) => + stream.WriteAsync(buffer, cancellationToken); + + public static ValueTask CopyToAsync(Stream source, Stream destination, CancellationToken cancellationToken) => + new ValueTask(source.CopyToAsync(destination, cancellationToken)); + + public static ValueTask DisposeAsync(Stream stream) => stream.DisposeAsync(); + } + + internal readonly struct SyncReadWriteAdapter : IReadWriteAdapter + { + public static ValueTask ReadAsync(Stream stream, Memory buffer, CancellationToken cancellationToken) => + new ValueTask(stream.Read(buffer.Span)); + + public static ValueTask ReadExactlyAsync(Stream stream, Memory buffer, CancellationToken cancellationToken) + { + stream.ReadExactly(buffer.Span); + return default; + } + + public static ValueTask WriteAsync(Stream stream, ReadOnlyMemory buffer, CancellationToken cancellationToken) + { + stream.Write(buffer.Span); + return default; + } + + public static ValueTask CopyToAsync(Stream source, Stream destination, CancellationToken cancellationToken) + { + source.CopyTo(destination); + return default; + } + + public static ValueTask DisposeAsync(Stream stream) + { + stream.Dispose(); + return default; + } + } +} From f6bef1f17789cdd3630a9d64ec8512a684051156 Mon Sep 17 00:00:00 2001 From: alinpahontu2912 Date: Wed, 10 Jun 2026 13:42:48 +0300 Subject: [PATCH 02/10] refactor more reader methods --- .../src/System/Formats/Tar/TarHeader.Read.cs | 29 +- .../src/System/Formats/Tar/TarHelpers.cs | 20 -- .../System/Formats/Tar/TarReadWriteAdapter.cs | 10 + .../src/System/Formats/Tar/TarReader.cs | 292 +++--------------- 4 files changed, 50 insertions(+), 301 deletions(-) diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Read.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Read.cs index 2fd0b1a274bfcb..1a729094a68328 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Read.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Read.cs @@ -19,27 +19,7 @@ internal sealed partial class TarHeader // Attempts to retrieve the next header from the specified tar archive stream. // Throws if end of stream is reached or if any data type conversion fails. // Returns a valid TarHeader object if the attributes were read successfully, null otherwise. - internal static TarHeader? TryGetNextHeader(Stream archiveStream, bool copyData, TarEntryFormat initialFormat, bool processDataBlock) - { - ValueTask vt = TryGetNextHeaderCoreAsync(archiveStream, copyData, initialFormat, processDataBlock, CancellationToken.None); - Debug.Assert(vt.IsCompleted, "Synchronous TryGetNextHeader completed asynchronously."); - return vt.GetAwaiter().GetResult(); - } - - // Asynchronously attempts read all the fields of the next header. - // Throws if end of stream is reached or if any data type conversion fails. - // Returns true if all the attributes were read successfully, false otherwise. - internal static ValueTask TryGetNextHeaderAsync(Stream archiveStream, bool copyData, TarEntryFormat initialFormat, bool processDataBlock, CancellationToken cancellationToken) - { - if (cancellationToken.IsCancellationRequested) - { - return ValueTask.FromCanceled(cancellationToken); - } - - return TryGetNextHeaderCoreAsync(archiveStream, copyData, initialFormat, processDataBlock, cancellationToken); - } - - private static async ValueTask TryGetNextHeaderCoreAsync(Stream archiveStream, bool copyData, TarEntryFormat initialFormat, bool processDataBlock, CancellationToken cancellationToken) + internal static async ValueTask TryGetNextHeaderCoreAsync(Stream archiveStream, bool copyData, TarEntryFormat initialFormat, bool processDataBlock, CancellationToken cancellationToken) where TAdapter : IReadWriteAdapter { // The four supported formats have a header that fits in the default record size @@ -236,13 +216,6 @@ internal void ReplaceNormalAttributesWithExtended(IEnumerable(archiveStream, copyData, CancellationToken.None); - Debug.Assert(vt.IsCompleted, "Synchronous ProcessDataBlock completed asynchronously."); - vt.GetAwaiter().GetResult(); - } - internal async ValueTask ProcessDataBlockCoreAsync(Stream archiveStream, bool copyData, CancellationToken cancellationToken) where TAdapter : IReadWriteAdapter { diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHelpers.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHelpers.cs index 4503129d2177c3..c59a65a0073027 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHelpers.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHelpers.cs @@ -288,26 +288,6 @@ internal static ReadOnlySpan TrimNullTerminated(ReadOnlySpan buffer) // After the file contents, there may be zero or more null characters, // which exist to ensure the data is aligned to the record size. Skip them and // set the stream position to the first byte of the next entry. - internal static int SkipBlockAlignmentPadding(Stream archiveStream, long size) - { - ValueTask vt = SkipBlockAlignmentPaddingCoreAsync(archiveStream, size, CancellationToken.None); - Debug.Assert(vt.IsCompleted, "Synchronous SkipBlockAlignmentPadding completed asynchronously."); - return vt.GetAwaiter().GetResult(); - } - - // After the file contents, there may be zero or more null characters, - // which exist to ensure the data is aligned to the record size. - // Asynchronously skip them and set the stream position to the first byte of the next entry. - internal static ValueTask SkipBlockAlignmentPaddingAsync(Stream archiveStream, long size, CancellationToken cancellationToken) - { - if (cancellationToken.IsCancellationRequested) - { - return ValueTask.FromCanceled(cancellationToken); - } - - return SkipBlockAlignmentPaddingCoreAsync(archiveStream, size, cancellationToken); - } - internal static async ValueTask SkipBlockAlignmentPaddingCoreAsync(Stream archiveStream, long size, CancellationToken cancellationToken) where TAdapter : IReadWriteAdapter { diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarReadWriteAdapter.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarReadWriteAdapter.cs index a3cb3350057fba..f492530a0d6b5f 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarReadWriteAdapter.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarReadWriteAdapter.cs @@ -18,6 +18,7 @@ internal interface IReadWriteAdapter static abstract ValueTask ReadExactlyAsync(Stream stream, Memory buffer, CancellationToken cancellationToken); static abstract ValueTask WriteAsync(Stream stream, ReadOnlyMemory buffer, CancellationToken cancellationToken); static abstract ValueTask CopyToAsync(Stream source, Stream destination, CancellationToken cancellationToken); + static abstract ValueTask AdvanceToEndAsync(SubReadStream stream, CancellationToken cancellationToken); static abstract ValueTask DisposeAsync(Stream stream); } @@ -35,6 +36,9 @@ public static ValueTask WriteAsync(Stream stream, ReadOnlyMemory buffer, C public static ValueTask CopyToAsync(Stream source, Stream destination, CancellationToken cancellationToken) => new ValueTask(source.CopyToAsync(destination, cancellationToken)); + public static ValueTask AdvanceToEndAsync(SubReadStream stream, CancellationToken cancellationToken) => + stream.AdvanceToEndAsync(cancellationToken); + public static ValueTask DisposeAsync(Stream stream) => stream.DisposeAsync(); } @@ -61,6 +65,12 @@ public static ValueTask CopyToAsync(Stream source, Stream destination, Cancellat return default; } + public static ValueTask AdvanceToEndAsync(SubReadStream stream, CancellationToken cancellationToken) + { + stream.AdvanceToEnd(); + return default; + } + public static ValueTask DisposeAsync(Stream stream) { stream.Dispose(); diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarReader.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarReader.cs index 26e623f9eb54d6..1071557a496238 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarReader.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarReader.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.IO; using System.Threading; using System.Threading.Tasks; @@ -53,30 +52,19 @@ public TarReader(Stream archiveStream, bool leaveOpen = false) /// The property of any entry can be replaced with a new stream. If the user decides to replace it on a instance that was obtained using a , the underlying stream gets disposed immediately, freeing the of origin from the responsibility of having to dispose it. public void Dispose() { - if (!_isDisposed) - { - _isDisposed = true; - - if (!_leaveOpen) - { - if (_dataStreamsToDispose?.Count > 0) - { - foreach (Stream s in _dataStreamsToDispose) - { - s.Dispose(); - } - } - - _archiveStream.Dispose(); - } - } + ValueTask vt = DisposeCoreAsync(); + Debug.Assert(vt.IsCompleted, "Synchronous Dispose completed asynchronously."); + vt.GetAwaiter().GetResult(); } /// /// Asynchronously disposes the current instance, and disposes the non-null instances of all the entries that were read from the archive. /// /// The property of any entry can be replaced with a new stream. If the user decides to replace it on a instance that was obtained using a , the underlying stream gets disposed immediately, freeing the of origin from the responsibility of having to dispose it. - public async ValueTask DisposeAsync() + public ValueTask DisposeAsync() => DisposeCoreAsync(); + + private async ValueTask DisposeCoreAsync() + where TAdapter : IReadWriteAdapter { if (!_isDisposed) { @@ -88,11 +76,11 @@ public async ValueTask DisposeAsync() { foreach (Stream s in _dataStreamsToDispose) { - await s.DisposeAsync().ConfigureAwait(false); + await TAdapter.DisposeAsync(s).ConfigureAwait(false); } } - await _archiveStream.DisposeAsync().ConfigureAwait(false); + await TAdapter.DisposeAsync(_archiveStream).ConfigureAwait(false); } } } @@ -128,32 +116,9 @@ public async ValueTask DisposeAsync() return null; } - AdvanceDataStreamIfNeeded(); - - TarHeader? header = TryGetNextEntryHeader(copyData); - if (header != null) - { - TarEntry entry = header._format switch - { - TarEntryFormat.Pax => header._typeFlag is TarEntryType.GlobalExtendedAttributes ? - new PaxGlobalExtendedAttributesTarEntry(header, this) : new PaxTarEntry(header, this), - TarEntryFormat.Gnu => new GnuTarEntry(header, this), - TarEntryFormat.Ustar => new UstarTarEntry(header, this), - TarEntryFormat.V7 or TarEntryFormat.Unknown or _ => new V7TarEntry(header, this), - }; - - if (_archiveStream.CanSeek && _archiveStream.Length == _archiveStream.Position) - { - _reachedEndMarkers = true; - } - - _previouslyReadEntry = entry; - PreserveDataStreamForDisposalIfNeeded(entry); - return entry; - } - - _reachedEndMarkers = true; - return null; + ValueTask vt = GetNextEntryCoreAsync(copyData, CancellationToken.None); + Debug.Assert(vt.IsCompleted, "Synchronous GetNextEntry completed asynchronously."); + return vt.GetAwaiter().GetResult(); } /// @@ -193,45 +158,21 @@ public async ValueTask DisposeAsync() return ValueTask.FromResult(null); } - return GetNextEntryInternalAsync(copyData, cancellationToken); + return GetNextEntryCoreAsync(copyData, cancellationToken); } // Moves the underlying archive stream position pointer to the beginning of the next header. internal void AdvanceDataStreamIfNeeded() { - if (_previouslyReadEntry == null) - { - return; - } - - if (_archiveStream.CanSeek) - { - Debug.Assert(_previouslyReadEntry._header._endOfHeaderAndDataAndBlockAlignment > 0); - _archiveStream.Position = _previouslyReadEntry._header._endOfHeaderAndDataAndBlockAlignment; - } - else if (_previouslyReadEntry._header._size > 0) - { - // When working with unseekable streams, every time we return an entry, we avoid advancing the pointer beyond the data section - // This is so the user can read the data if desired. But if the data was not read by the user, we need to advance the pointer - // here until it's located at the beginning of the next entry header. - // This should only be done if the previous entry came from a TarReader and it still had its original SubReadStream. - - if (_previouslyReadEntry._header._dataStream is not SubReadStream dataStream) - { - return; - } - - dataStream.AdvanceToEnd(); - - TarHelpers.SkipBlockAlignmentPadding(_archiveStream, _previouslyReadEntry._header._size); - } + ValueTask vt = AdvanceDataStreamIfNeededCoreAsync(CancellationToken.None); + Debug.Assert(vt.IsCompleted, "Synchronous AdvanceDataStreamIfNeeded completed asynchronously."); + vt.GetAwaiter().GetResult(); } - // Asynchronously moves the underlying archive stream position pointer to the beginning of the next header. - internal async ValueTask AdvanceDataStreamIfNeededAsync(CancellationToken cancellationToken) + // Moves the underlying archive stream position pointer to the beginning of the next header. + private async ValueTask AdvanceDataStreamIfNeededCoreAsync(CancellationToken cancellationToken) + where TAdapter : IReadWriteAdapter { - cancellationToken.ThrowIfCancellationRequested(); - if (_previouslyReadEntry == null) { return; @@ -254,18 +195,19 @@ internal async ValueTask AdvanceDataStreamIfNeededAsync(CancellationToken cancel return; } - await dataStream.AdvanceToEndAsync(cancellationToken).ConfigureAwait(false); + await TAdapter.AdvanceToEndAsync(dataStream, cancellationToken).ConfigureAwait(false); - await TarHelpers.SkipBlockAlignmentPaddingAsync(_archiveStream, _previouslyReadEntry._header._size, cancellationToken).ConfigureAwait(false); + await TarHelpers.SkipBlockAlignmentPaddingCoreAsync(_archiveStream, _previouslyReadEntry._header._size, cancellationToken).ConfigureAwait(false); } } - // Asynchronously retrieves the next entry if one is found. - private async ValueTask GetNextEntryInternalAsync(bool copyData, CancellationToken cancellationToken) + // Retrieves the next entry if one is found. + private async ValueTask GetNextEntryCoreAsync(bool copyData, CancellationToken cancellationToken) + where TAdapter : IReadWriteAdapter { - await AdvanceDataStreamIfNeededAsync(cancellationToken).ConfigureAwait(false); + await AdvanceDataStreamIfNeededCoreAsync(cancellationToken).ConfigureAwait(false); - TarHeader? header = await TryGetNextEntryHeaderAsync(copyData, cancellationToken).ConfigureAwait(false); + TarHeader? header = await TryGetNextEntryHeaderCoreAsync(copyData, cancellationToken).ConfigureAwait(false); if (header != null) { TarEntry entry = header._format switch @@ -296,53 +238,12 @@ internal async ValueTask AdvanceDataStreamIfNeededAsync(CancellationToken cancel // An entry header represents any typeflag that is contains metadata. // Metadata typeflags: ExtendedAttributes, GlobalExtendedAttributes, LongLink, LongPath. // Metadata typeflag entries get handled internally by this method until a valid header entry can be returned. - private TarHeader? TryGetNextEntryHeader(bool copyData) - { - Debug.Assert(!_reachedEndMarkers); - - TarHeader? header = TarHeader.TryGetNextHeader(_archiveStream, copyData, TarEntryFormat.Unknown, processDataBlock: true); - - if (header == null) - { - return null; - } - - // If a metadata typeflag entry is retrieved, handle it here, then read the next entry - - // PAX metadata - if (header._typeFlag is TarEntryType.ExtendedAttributes) - { - if (!TryProcessExtendedAttributesHeader(header, copyData, out TarHeader? mainHeader)) - { - return null; - } - header = mainHeader; - } - // GNU metadata - else if (header._typeFlag is TarEntryType.LongLink or TarEntryType.LongPath) - { - if (!TryProcessGnuMetadataHeader(header, copyData, out TarHeader mainHeader)) - { - return null; - } - header = mainHeader; - } - - return header; - } - - // Asynchronously attempts to read the next tar archive entry header. - // Returns true if an entry header was collected successfully, false otherwise. - // An entry header represents any typeflag that is contains metadata. - // Metadata typeflags: ExtendedAttributes, GlobalExtendedAttributes, LongLink, LongPath. - // Metadata typeflag entries get handled internally by this method until a valid header entry can be returned. - private async ValueTask TryGetNextEntryHeaderAsync(bool copyData, CancellationToken cancellationToken) + private async ValueTask TryGetNextEntryHeaderCoreAsync(bool copyData, CancellationToken cancellationToken) + where TAdapter : IReadWriteAdapter { - cancellationToken.ThrowIfCancellationRequested(); - Debug.Assert(!_reachedEndMarkers); - TarHeader? header = await TarHeader.TryGetNextHeaderAsync(_archiveStream, copyData, TarEntryFormat.Unknown, processDataBlock: true, cancellationToken).ConfigureAwait(false); + TarHeader? header = await TarHeader.TryGetNextHeaderCoreAsync(_archiveStream, copyData, TarEntryFormat.Unknown, processDataBlock: true, cancellationToken).ConfigureAwait(false); if (header == null) { return null; @@ -353,7 +254,7 @@ internal async ValueTask AdvanceDataStreamIfNeededAsync(CancellationToken cancel // PAX metadata if (header._typeFlag is TarEntryType.ExtendedAttributes) { - TarHeader? mainHeader = await TryProcessExtendedAttributesHeaderAsync(header, copyData, cancellationToken).ConfigureAwait(false); + TarHeader? mainHeader = await TryProcessExtendedAttributesHeaderCoreAsync(header, copyData, cancellationToken).ConfigureAwait(false); if (mainHeader == null) { return null; @@ -363,7 +264,7 @@ internal async ValueTask AdvanceDataStreamIfNeededAsync(CancellationToken cancel // GNU metadata else if (header._typeFlag is TarEntryType.LongLink or TarEntryType.LongPath) { - TarHeader? mainHeader = await TryProcessGnuMetadataHeaderAsync(header, copyData, cancellationToken).ConfigureAwait(false); + TarHeader? mainHeader = await TryProcessGnuMetadataHeaderCoreAsync(header, copyData, cancellationToken).ConfigureAwait(false); if (mainHeader == null) { return null; @@ -376,44 +277,12 @@ internal async ValueTask AdvanceDataStreamIfNeededAsync(CancellationToken cancel // Tries to read the contents of the PAX metadata entry as extended attributes, tries to also read the actual entry that follows, // and returns the actual entry with the processed extended attributes saved in the _extendedAttributes dictionary. - private bool TryProcessExtendedAttributesHeader(TarHeader extendedAttributesHeader, bool copyData, [NotNullWhen(returnValue: true)] out TarHeader? actualHeader) - { - // Don't process the data block of the actual entry just yet, because there's a slim chance - // that the extended attributes contain a size that we need to override in the header - actualHeader = TarHeader.TryGetNextHeader(_archiveStream, copyData, TarEntryFormat.Pax, processDataBlock: false); - - if (actualHeader == null) - { - return false; - } - - // We're currently processing an extended attributes header, so we can never have two extended entries in a row - if (actualHeader._typeFlag is TarEntryType.GlobalExtendedAttributes or - TarEntryType.ExtendedAttributes or - TarEntryType.LongLink or - TarEntryType.LongPath) - { - throw new InvalidDataException(SR.Format(SR.TarUnexpectedMetadataEntry, actualHeader._typeFlag, TarEntryType.ExtendedAttributes)); - } - - // Replace all the attributes representing standard fields with the extended ones, if any - actualHeader.ReplaceNormalAttributesWithExtended(extendedAttributesHeader.ExtendedAttributes); - - // We retrieved the extended attributes, now we can read the data, and always with the right size - actualHeader.ProcessDataBlock(_archiveStream, copyData); - - return true; - } - - // Asynchronously tries to read the contents of the PAX metadata entry as extended attributes, tries to also read the actual entry that follows, - // and returns the actual entry with the processed extended attributes saved in the _extendedAttributes dictionary. - private async ValueTask TryProcessExtendedAttributesHeaderAsync(TarHeader extendedAttributesHeader, bool copyData, CancellationToken cancellationToken) + private async ValueTask TryProcessExtendedAttributesHeaderCoreAsync(TarHeader extendedAttributesHeader, bool copyData, CancellationToken cancellationToken) + where TAdapter : IReadWriteAdapter { - cancellationToken.ThrowIfCancellationRequested(); - // Don't process the data block of the actual entry just yet, because there's a slim chance // that the extended attributes contain a size that we need to override in the header - TarHeader? actualHeader = await TarHeader.TryGetNextHeaderAsync(_archiveStream, copyData, TarEntryFormat.Pax, processDataBlock: false, cancellationToken).ConfigureAwait(false); + TarHeader? actualHeader = await TarHeader.TryGetNextHeaderCoreAsync(_archiveStream, copyData, TarEntryFormat.Pax, processDataBlock: false, cancellationToken).ConfigureAwait(false); if (actualHeader == null) { return null; @@ -428,105 +297,22 @@ TarEntryType.LongLink or throw new InvalidDataException(SR.Format(SR.TarUnexpectedMetadataEntry, actualHeader._typeFlag, TarEntryType.ExtendedAttributes)); } - // Can't have two extended attribute metadata entries in a row - if (actualHeader._typeFlag is TarEntryType.ExtendedAttributes) - { - throw new InvalidDataException(SR.Format(SR.TarUnexpectedMetadataEntry, TarEntryType.ExtendedAttributes, TarEntryType.ExtendedAttributes)); - } - // Replace all the attributes representing standard fields with the extended ones, if any actualHeader.ReplaceNormalAttributesWithExtended(extendedAttributesHeader.ExtendedAttributes); // We retrieved the extended attributes, now we can read the data, and always with the right size - actualHeader.ProcessDataBlock(_archiveStream, copyData); + await actualHeader.ProcessDataBlockCoreAsync(_archiveStream, copyData, cancellationToken).ConfigureAwait(false); return actualHeader; } // Tries to read the contents of the GNU metadata entry, then tries to read the next entry, which could either be another GNU metadata entry // or the actual entry. Processes them all and returns the actual entry updating its path and/or linkpath fields as needed. - private bool TryProcessGnuMetadataHeader(TarHeader header, bool copyData, out TarHeader finalHeader) - { - finalHeader = new(TarEntryFormat.Gnu); - - TarHeader? secondHeader = TarHeader.TryGetNextHeader(_archiveStream, copyData, TarEntryFormat.Gnu, processDataBlock: true); - - // Get the second entry, which is the actual entry - if (secondHeader == null) - { - return false; - } - - // Can't have two identical metadata entries in a row - if (secondHeader._typeFlag == header._typeFlag) - { - throw new InvalidDataException(SR.Format(SR.TarUnexpectedMetadataEntry, secondHeader._typeFlag, header._typeFlag)); - } - - // It's possible to have the two different metadata entries in a row - if ((header._typeFlag is TarEntryType.LongLink && secondHeader._typeFlag is TarEntryType.LongPath) || - (header._typeFlag is TarEntryType.LongPath && secondHeader._typeFlag is TarEntryType.LongLink)) - { - TarHeader? thirdHeader = TarHeader.TryGetNextHeader(_archiveStream, copyData, TarEntryFormat.Gnu, processDataBlock: true); - - // Get the third entry, which is the actual entry - if (thirdHeader == null) - { - return false; - } - - // Can't have three GNU metadata entries in a row - if (thirdHeader._typeFlag is TarEntryType.LongLink or TarEntryType.LongPath) - { - throw new InvalidDataException(SR.Format(SR.TarUnexpectedMetadataEntry, thirdHeader._typeFlag, secondHeader._typeFlag)); - } - - if (header._typeFlag is TarEntryType.LongLink) - { - Debug.Assert(header._linkName != null); - Debug.Assert(secondHeader._name != null); - - thirdHeader._linkName = header._linkName; - thirdHeader._name = secondHeader._name; - } - else if (header._typeFlag is TarEntryType.LongPath) - { - Debug.Assert(header._name != null); - Debug.Assert(secondHeader._linkName != null); - thirdHeader._name = header._name; - thirdHeader._linkName = secondHeader._linkName; - } - - finalHeader = thirdHeader; - } - // Only one metadata entry was found - else - { - if (header._typeFlag is TarEntryType.LongLink) - { - Debug.Assert(header._linkName != null); - secondHeader._linkName = header._linkName; - } - else if (header._typeFlag is TarEntryType.LongPath) - { - Debug.Assert(header._name != null); - secondHeader._name = header._name; - } - - finalHeader = secondHeader; - } - - return true; - } - - // Asynchronously tries to read the contents of the GNU metadata entry, then tries to read the next entry, which could either be another GNU metadata entry - // or the actual entry. Processes them all and returns the actual entry updating its path and/or linkpath fields as needed. - private async ValueTask TryProcessGnuMetadataHeaderAsync(TarHeader header, bool copyData, CancellationToken cancellationToken) + private async ValueTask TryProcessGnuMetadataHeaderCoreAsync(TarHeader header, bool copyData, CancellationToken cancellationToken) + where TAdapter : IReadWriteAdapter { - cancellationToken.ThrowIfCancellationRequested(); - // Get the second entry, which is the actual entry - TarHeader? secondHeader = await TarHeader.TryGetNextHeaderAsync(_archiveStream, copyData, TarEntryFormat.Gnu, processDataBlock: true, cancellationToken).ConfigureAwait(false); + TarHeader? secondHeader = await TarHeader.TryGetNextHeaderCoreAsync(_archiveStream, copyData, TarEntryFormat.Gnu, processDataBlock: true, cancellationToken).ConfigureAwait(false); if (secondHeader == null) { return null; @@ -545,7 +331,7 @@ private bool TryProcessGnuMetadataHeader(TarHeader header, bool copyData, out Ta (header._typeFlag is TarEntryType.LongPath && secondHeader._typeFlag is TarEntryType.LongLink)) { // Get the third entry, which is the actual entry - TarHeader? thirdHeader = await TarHeader.TryGetNextHeaderAsync(_archiveStream, copyData, TarEntryFormat.Gnu, processDataBlock: true, cancellationToken).ConfigureAwait(false); + TarHeader? thirdHeader = await TarHeader.TryGetNextHeaderCoreAsync(_archiveStream, copyData, TarEntryFormat.Gnu, processDataBlock: true, cancellationToken).ConfigureAwait(false); if (thirdHeader == null) { return null; From c0cf61c2858f19d73693e4f07e10d54b79ce6e5f Mon Sep 17 00:00:00 2001 From: alinpahontu2912 Date: Wed, 10 Jun 2026 14:05:21 +0300 Subject: [PATCH 03/10] refactor write helpers --- .../src/System/Formats/Tar/TarHeader.Write.cs | 321 +++--------------- .../src/System/Formats/Tar/TarWriter.cs | 163 ++++----- 2 files changed, 108 insertions(+), 376 deletions(-) diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs index c3d166da29389f..dc22b113fee950 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs @@ -34,39 +34,25 @@ internal sealed partial class TarHeader private const string RootUNameGName = "root"; // Writes the entry in the order required to be able to obtain the seekable data stream size. - private void WriteWithSeekableDataStream(TarEntryFormat format, Stream archiveStream, Span buffer) - { - Debug.Assert(format is > TarEntryFormat.Unknown and <= TarEntryFormat.Gnu); - Debug.Assert(_dataStream == null || _dataStream.CanSeek); - - _size = GetTotalDataBytesToWrite(); - WriteFieldsToBuffer(format, buffer); - archiveStream.Write(buffer); - - if (_dataStream != null) - { - WriteData(archiveStream, _dataStream); - } - } - - // Asynchronously writes the entry in the order required to be able to obtain the seekable data stream size. - private async Task WriteWithSeekableDataStreamAsync(TarEntryFormat format, Stream archiveStream, Memory buffer, CancellationToken cancellationToken) + private async ValueTask WriteWithSeekableDataStreamCoreAsync(TarEntryFormat format, Stream archiveStream, Memory buffer, CancellationToken cancellationToken) + where TAdapter : IReadWriteAdapter { Debug.Assert(format is > TarEntryFormat.Unknown and <= TarEntryFormat.Gnu); Debug.Assert(_dataStream == null || _dataStream.CanSeek); _size = GetTotalDataBytesToWrite(); WriteFieldsToBuffer(format, buffer.Span); - await archiveStream.WriteAsync(buffer, cancellationToken).ConfigureAwait(false); + await TAdapter.WriteAsync(archiveStream, buffer, cancellationToken).ConfigureAwait(false); if (_dataStream != null) { - await WriteDataAsync(archiveStream, _dataStream, cancellationToken).ConfigureAwait(false); + await WriteDataCoreAsync(archiveStream, _dataStream, cancellationToken).ConfigureAwait(false); } } // Writes into the specified destination stream the entry in the order required to be able to obtain the unseekable data stream size. - private void WriteWithUnseekableDataStream(TarEntryFormat format, Stream destinationStream, Span buffer, bool shouldAdvanceToEnd) + private async ValueTask WriteWithUnseekableDataStreamCoreAsync(TarEntryFormat format, Stream destinationStream, Memory buffer, bool shouldAdvanceToEnd, CancellationToken cancellationToken) + where TAdapter : IReadWriteAdapter { // When the data stream is unseekable, the order in which we write the entry data changes Debug.Assert(destinationStream.CanSeek); @@ -92,66 +78,14 @@ private void WriteWithUnseekableDataStream(TarEntryFormat format, Stream destina // Move to the data start location and write the data destinationStream.Seek(dataLocation, SeekOrigin.Current); - _dataStream.CopyTo(destinationStream); // The data gets copied from the current position + await TAdapter.CopyToAsync(_dataStream, destinationStream, cancellationToken).ConfigureAwait(false); // The data gets copied from the current position // Get the new archive stream position, and the difference is the size of the data stream long dataEndPosition = destinationStream.Position; _size = dataEndPosition - dataStartPosition; // Write the padding now so that we can go back to writing the entry's header metadata - WriteEmptyPadding(destinationStream); - - // Store the end of the current header, we will write the next one after this position - long endOfHeaderPosition = destinationStream.Position; - - // Go back to the start of the entry header to write the rest of the fields - destinationStream.Position = headerStartPosition; - - WriteFieldsToBuffer(format, buffer); - destinationStream.Write(buffer); - - if (shouldAdvanceToEnd) - { - // Finally, move to the end of the header to continue with the next entry - destinationStream.Position = endOfHeaderPosition; - } - } - - // Asynchronously writes into the destination stream the entry in the order required to be able to obtain the unseekable data stream size. - private async Task WriteWithUnseekableDataStreamAsync(TarEntryFormat format, Stream destinationStream, Memory buffer, bool shouldAdvanceToEnd, CancellationToken cancellationToken) - { - // When the data stream is unseekable, the order in which we write the entry data changes - Debug.Assert(destinationStream.CanSeek); - Debug.Assert(_dataStream != null); - Debug.Assert(!_dataStream.CanSeek); - - // Store the start of the current entry's header, it'll be used later - long headerStartPosition = destinationStream.Position; - - ushort dataLocation = format switch - { - TarEntryFormat.V7 => FieldLocations.V7Data, - TarEntryFormat.Ustar or TarEntryFormat.Pax => FieldLocations.PosixData, - TarEntryFormat.Gnu => FieldLocations.GnuData, - _ => throw new ArgumentOutOfRangeException(nameof(format)) - }; - - // We know the exact location where the data starts depending on the format - long dataStartPosition = headerStartPosition + dataLocation; - - // Before writing, update the offset field now that the entry belongs to an archive - _dataOffset = dataStartPosition; - - // Move to the data start location and write the data - destinationStream.Seek(dataLocation, SeekOrigin.Current); - await _dataStream.CopyToAsync(destinationStream, cancellationToken).ConfigureAwait(false); // The data gets copied from the current position - - // Get the new archive stream position, and the difference is the size of the data stream - long dataEndPosition = destinationStream.Position; - _size = dataEndPosition - dataStartPosition; - - // Write the padding now so that we can go back to writing the entry's header metadata - await WriteEmptyPaddingAsync(destinationStream, cancellationToken).ConfigureAwait(false); + await WriteEmptyPaddingCoreAsync(destinationStream, cancellationToken).ConfigureAwait(false); // Store the end of the current header, we will write the next one after this position long endOfHeaderPosition = destinationStream.Position; @@ -160,7 +94,7 @@ private async Task WriteWithUnseekableDataStreamAsync(TarEntryFormat format, Str destinationStream.Position = headerStartPosition; WriteFieldsToBuffer(format, buffer.Span); - await destinationStream.WriteAsync(buffer, cancellationToken).ConfigureAwait(false); + await TAdapter.WriteAsync(destinationStream, buffer, cancellationToken).ConfigureAwait(false); if (shouldAdvanceToEnd) { @@ -193,22 +127,11 @@ private void WriteUstarFieldsToBuffer(Span buffer) } // Writes the current header as a PAX Global Extended Attributes entry into the archive stream. - internal void WriteAsPaxGlobalExtendedAttributes(Stream archiveStream, Span buffer, int globalExtendedAttributesEntryNumber) + internal ValueTask WriteAsPaxGlobalExtendedAttributesCoreAsync(Stream archiveStream, Memory buffer, int globalExtendedAttributesEntryNumber, CancellationToken cancellationToken) + where TAdapter : IReadWriteAdapter { VerifyGlobalExtendedAttributesDataIsValid(globalExtendedAttributesEntryNumber); - WriteAsPaxExtendedAttributes(archiveStream, buffer, ExtendedAttributes, isGea: true, globalExtendedAttributesEntryNumber); - } - - // Writes the current header as a PAX Global Extended Attributes entry into the archive stream and returns the value of the final checksum. - internal Task WriteAsPaxGlobalExtendedAttributesAsync(Stream archiveStream, Memory buffer, int globalExtendedAttributesEntryNumber, CancellationToken cancellationToken) - { - if (cancellationToken.IsCancellationRequested) - { - return Task.FromCanceled(cancellationToken); - } - - VerifyGlobalExtendedAttributesDataIsValid(globalExtendedAttributesEntryNumber); - return WriteAsPaxExtendedAttributesAsync(archiveStream, buffer, ExtendedAttributes, isGea: true, globalExtendedAttributesEntryNumber, cancellationToken); + return WriteAsPaxExtendedAttributesCoreAsync(archiveStream, buffer, ExtendedAttributes, isGea: true, globalExtendedAttributesEntryNumber, cancellationToken); } // Verifies the data is valid for writing a Global Extended Attributes entry. @@ -218,63 +141,38 @@ private void VerifyGlobalExtendedAttributesDataIsValid(int globalExtendedAttribu Debug.Assert(globalExtendedAttributesEntryNumber >= 0); } - internal void WriteAsV7(Stream archiveStream, Span buffer) + internal ValueTask WriteAsV7CoreAsync(Stream archiveStream, Memory buffer, CancellationToken cancellationToken) + where TAdapter : IReadWriteAdapter { Debug.Assert(archiveStream.CanSeek || _dataStream == null || _dataStream.CanSeek); if (archiveStream.CanSeek && _dataStream is { CanSeek: false }) { - WriteWithUnseekableDataStream(TarEntryFormat.V7, archiveStream, buffer, shouldAdvanceToEnd: true); - } - else // Seek status of archive does not matter - { - WriteWithSeekableDataStream(TarEntryFormat.V7, archiveStream, buffer); - } - } - - internal Task WriteAsV7Async(Stream archiveStream, Memory buffer, CancellationToken cancellationToken) - { - Debug.Assert(archiveStream.CanSeek || _dataStream == null || _dataStream.CanSeek); - - if (archiveStream.CanSeek && _dataStream is { CanSeek: false }) - { - return WriteWithUnseekableDataStreamAsync(TarEntryFormat.V7, archiveStream, buffer, shouldAdvanceToEnd: true, cancellationToken); + return WriteWithUnseekableDataStreamCoreAsync(TarEntryFormat.V7, archiveStream, buffer, shouldAdvanceToEnd: true, cancellationToken); } // Else: Seek status of archive does not matter - return WriteWithSeekableDataStreamAsync(TarEntryFormat.V7, archiveStream, buffer, cancellationToken); - } - - internal void WriteAsUstar(Stream archiveStream, Span buffer) - { - Debug.Assert(archiveStream.CanSeek || _dataStream == null || _dataStream.CanSeek); - - if (archiveStream.CanSeek && _dataStream is { CanSeek: false }) - { - WriteWithUnseekableDataStream(TarEntryFormat.Ustar, archiveStream, buffer, shouldAdvanceToEnd: true); - } - else // Seek status of archive does not matter - { - WriteWithSeekableDataStream(TarEntryFormat.Ustar, archiveStream, buffer); - } + return WriteWithSeekableDataStreamCoreAsync(TarEntryFormat.V7, archiveStream, buffer, cancellationToken); } - internal Task WriteAsUstarAsync(Stream archiveStream, Memory buffer, CancellationToken cancellationToken) + internal ValueTask WriteAsUstarCoreAsync(Stream archiveStream, Memory buffer, CancellationToken cancellationToken) + where TAdapter : IReadWriteAdapter { Debug.Assert(archiveStream.CanSeek || _dataStream == null || _dataStream.CanSeek); if (archiveStream.CanSeek && _dataStream is { CanSeek: false }) { - return WriteWithUnseekableDataStreamAsync(TarEntryFormat.Ustar, archiveStream, buffer, shouldAdvanceToEnd: true, cancellationToken); + return WriteWithUnseekableDataStreamCoreAsync(TarEntryFormat.Ustar, archiveStream, buffer, shouldAdvanceToEnd: true, cancellationToken); } // Else: Seek status of archive does not matter - return WriteWithSeekableDataStreamAsync(TarEntryFormat.Ustar, archiveStream, buffer, cancellationToken); + return WriteWithSeekableDataStreamCoreAsync(TarEntryFormat.Ustar, archiveStream, buffer, cancellationToken); } // Writes the current header as a PAX entry into the archive stream. // Makes sure to add the preceding extended attributes entry before the actual entry. - internal void WriteAsPax(Stream archiveStream, Span buffer) + internal async ValueTask WriteAsPaxCoreAsync(Stream archiveStream, Memory buffer, CancellationToken cancellationToken) + where TAdapter : IReadWriteAdapter { Debug.Assert(archiveStream.CanSeek || _dataStream == null || _dataStream.CanSeek); Debug.Assert(_typeFlag is not TarEntryType.GlobalExtendedAttributes); @@ -287,51 +185,7 @@ internal void WriteAsPax(Stream archiveStream, Span buffer) // Write the full entry header into a temporary stream, which will also collect the data length in the _size field using MemoryStream tempStream = new(); // Don't advance the tempStream, instead, we will rewind it to the beginning for copying later - WriteWithUnseekableDataStream(TarEntryFormat.Pax, tempStream, buffer, shouldAdvanceToEnd: false); - tempStream.Position = 0; - buffer.Clear(); - - // If the data length is larger than it fits in the standard size field, it will get stored as an extended attribute - CollectExtendedAttributesFromStandardFieldsIfNeeded(); - - // Write the extended attributes entry into the archive first - extendedAttributesHeader.WriteAsPaxExtendedAttributes(archiveStream, buffer, ExtendedAttributes, isGea: false, globalExtendedAttributesEntryNumber: -1); - buffer.Clear(); - - // And then write the stored entry into the archive - tempStream.CopyTo(archiveStream); - } - else // Seek status of archive does not matter - { - _size = GetTotalDataBytesToWrite(); - // Fill the current header's dict - CollectExtendedAttributesFromStandardFieldsIfNeeded(); - // And pass the attributes to the preceding extended attributes header for writing - extendedAttributesHeader.WriteAsPaxExtendedAttributes(archiveStream, buffer, ExtendedAttributes, isGea: false, globalExtendedAttributesEntryNumber: -1); - buffer.Clear(); // Reset it to reuse it - - // Second, we write this header as a normal one - WriteWithSeekableDataStream(TarEntryFormat.Pax, archiveStream, buffer); - } - } - - // Asynchronously writes the current header as a PAX entry into the archive stream. - // Makes sure to add the preceding exteded attributes entry before the actual entry. - internal async Task WriteAsPaxAsync(Stream archiveStream, Memory buffer, CancellationToken cancellationToken) - { - Debug.Assert(archiveStream.CanSeek || _dataStream == null || _dataStream.CanSeek); - Debug.Assert(_typeFlag is not TarEntryType.GlobalExtendedAttributes); - cancellationToken.ThrowIfCancellationRequested(); - - // First, we create the preceding extended attributes header - TarHeader extendedAttributesHeader = new(TarEntryFormat.Pax); - - if (archiveStream.CanSeek && _dataStream is { CanSeek: false }) - { - // Write the full entry header into a temporary stream, which will also collect the data length in the _size field - using MemoryStream tempStream = new(); - // Don't advance the tempStream, instead, we will rewind it to the beginning for copying later - await WriteWithUnseekableDataStreamAsync(TarEntryFormat.Pax, tempStream, buffer, shouldAdvanceToEnd: false, cancellationToken).ConfigureAwait(false); + await WriteWithUnseekableDataStreamCoreAsync(TarEntryFormat.Pax, tempStream, buffer, shouldAdvanceToEnd: false, cancellationToken).ConfigureAwait(false); tempStream.Position = 0; buffer.Span.Clear(); @@ -339,11 +193,11 @@ internal async Task WriteAsPaxAsync(Stream archiveStream, Memory buffer, C CollectExtendedAttributesFromStandardFieldsIfNeeded(); // Write the extended attributes entry into the archive first - await extendedAttributesHeader.WriteAsPaxExtendedAttributesAsync(archiveStream, buffer, ExtendedAttributes, isGea: false, globalExtendedAttributesEntryNumber: -1, cancellationToken).ConfigureAwait(false); + await extendedAttributesHeader.WriteAsPaxExtendedAttributesCoreAsync(archiveStream, buffer, ExtendedAttributes, isGea: false, globalExtendedAttributesEntryNumber: -1, cancellationToken).ConfigureAwait(false); buffer.Span.Clear(); // And then write the stored entry into the archive - await tempStream.CopyToAsync(archiveStream, cancellationToken).ConfigureAwait(false); + await TAdapter.CopyToAsync(tempStream, archiveStream, cancellationToken).ConfigureAwait(false); } else // Seek status of archive does not matter { @@ -351,11 +205,11 @@ internal async Task WriteAsPaxAsync(Stream archiveStream, Memory buffer, C // Fill the current header's dict CollectExtendedAttributesFromStandardFieldsIfNeeded(); // And pass the attributes to the preceding extended attributes header for writing - await extendedAttributesHeader.WriteAsPaxExtendedAttributesAsync(archiveStream, buffer, ExtendedAttributes, isGea: false, globalExtendedAttributesEntryNumber: -1, cancellationToken).ConfigureAwait(false); + await extendedAttributesHeader.WriteAsPaxExtendedAttributesCoreAsync(archiveStream, buffer, ExtendedAttributes, isGea: false, globalExtendedAttributesEntryNumber: -1, cancellationToken).ConfigureAwait(false); buffer.Span.Clear(); // Reset it to reuse it // Second, we write this header as a normal one - await WriteWithSeekableDataStreamAsync(TarEntryFormat.Pax, archiveStream, buffer, cancellationToken).ConfigureAwait(false); + await WriteWithSeekableDataStreamCoreAsync(TarEntryFormat.Pax, archiveStream, buffer, cancellationToken).ConfigureAwait(false); } } // Checks if the linkname string is too long to fit in the regular header field. @@ -368,52 +222,17 @@ internal async Task WriteAsPaxAsync(Stream archiveStream, Memory buffer, C // Writes the current header as a Gnu entry into the archive stream. // Makes sure to add the preceding LongLink and/or LongPath entries if necessary, before the actual entry. - internal void WriteAsGnu(Stream archiveStream, Span buffer) - { - Debug.Assert(archiveStream.CanSeek || _dataStream == null || _dataStream.CanSeek); - - if (IsLinkNameTooLongForRegularField()) - { - // Linkname is too long for the regular header field, create a longlink entry where the linkname will be stored. - TarHeader longLinkHeader = GetGnuLongLinkMetadataHeader(); - Debug.Assert(longLinkHeader._dataStream != null && longLinkHeader._dataStream.CanSeek); // We generate the long metadata data stream, should always be seekable - longLinkHeader.WriteWithSeekableDataStream(TarEntryFormat.Gnu, archiveStream, buffer); - buffer.Clear(); // Reset it to reuse it - } - - if (IsNameTooLongForRegularField()) - { - // Name is too long for the regular header field, create a longpath entry where the name will be stored. - TarHeader longPathHeader = GetGnuLongPathMetadataHeader(); - Debug.Assert(longPathHeader._dataStream != null && longPathHeader._dataStream.CanSeek); // We generate the long metadata data stream, should always be seekable - longPathHeader.WriteWithSeekableDataStream(TarEntryFormat.Gnu, archiveStream, buffer); - buffer.Clear(); // Reset it to reuse it - } - - // Third, we write this header as a normal one - if (archiveStream.CanSeek && _dataStream is { CanSeek: false }) - { - WriteWithUnseekableDataStream(TarEntryFormat.Gnu, archiveStream, buffer, shouldAdvanceToEnd: true); - } - else // Seek status of archive does not matter - { - WriteWithSeekableDataStream(TarEntryFormat.Gnu, archiveStream, buffer); - } - } - - // Writes the current header as a Gnu entry into the archive stream. - // Makes sure to add the preceding LongLink and/or LongPath entries if necessary, before the actual entry. - internal async Task WriteAsGnuAsync(Stream archiveStream, Memory buffer, CancellationToken cancellationToken) + internal async ValueTask WriteAsGnuCoreAsync(Stream archiveStream, Memory buffer, CancellationToken cancellationToken) + where TAdapter : IReadWriteAdapter { Debug.Assert(archiveStream.CanSeek || _dataStream == null || _dataStream.CanSeek); - cancellationToken.ThrowIfCancellationRequested(); if (IsLinkNameTooLongForRegularField()) { // Linkname is too long for the regular header field, create a longlink entry where the linkname will be stored. TarHeader longLinkHeader = GetGnuLongLinkMetadataHeader(); Debug.Assert(longLinkHeader._dataStream != null && longLinkHeader._dataStream.CanSeek); // We generate the long metadata data stream, should always be seekable - await longLinkHeader.WriteWithSeekableDataStreamAsync(TarEntryFormat.Gnu, archiveStream, buffer, cancellationToken).ConfigureAwait(false); + await longLinkHeader.WriteWithSeekableDataStreamCoreAsync(TarEntryFormat.Gnu, archiveStream, buffer, cancellationToken).ConfigureAwait(false); buffer.Span.Clear(); // Reset it to reuse it } @@ -422,18 +241,18 @@ internal async Task WriteAsGnuAsync(Stream archiveStream, Memory buffer, C // Name is too long for the regular header field, create a longpath entry where the name will be stored. TarHeader longPathHeader = GetGnuLongPathMetadataHeader(); Debug.Assert(longPathHeader._dataStream != null && longPathHeader._dataStream.CanSeek); // We generate the long metadata data stream, should always be seekable - await longPathHeader.WriteWithSeekableDataStreamAsync(TarEntryFormat.Gnu, archiveStream, buffer, cancellationToken).ConfigureAwait(false); + await longPathHeader.WriteWithSeekableDataStreamCoreAsync(TarEntryFormat.Gnu, archiveStream, buffer, cancellationToken).ConfigureAwait(false); buffer.Span.Clear(); // Reset it to reuse it } // Third, we write this header as a normal one if (archiveStream.CanSeek && _dataStream is { CanSeek: false }) { - await WriteWithUnseekableDataStreamAsync(TarEntryFormat.Gnu, archiveStream, buffer, shouldAdvanceToEnd: true, cancellationToken).ConfigureAwait(false); + await WriteWithUnseekableDataStreamCoreAsync(TarEntryFormat.Gnu, archiveStream, buffer, shouldAdvanceToEnd: true, cancellationToken).ConfigureAwait(false); } else // Seek status of archive does not matter { - await WriteWithSeekableDataStreamAsync(TarEntryFormat.Gnu, archiveStream, buffer, cancellationToken).ConfigureAwait(false); + await WriteWithSeekableDataStreamCoreAsync(TarEntryFormat.Gnu, archiveStream, buffer, cancellationToken).ConfigureAwait(false); } } @@ -491,20 +310,12 @@ private void WriteGnuFieldsToBuffer(Span buffer) } // Writes the current header as a PAX Extended Attributes entry into the archive stream. - private void WriteAsPaxExtendedAttributes(Stream archiveStream, Span buffer, Dictionary extendedAttributes, bool isGea, int globalExtendedAttributesEntryNumber) + private ValueTask WriteAsPaxExtendedAttributesCoreAsync(Stream archiveStream, Memory buffer, Dictionary extendedAttributes, bool isGea, int globalExtendedAttributesEntryNumber, CancellationToken cancellationToken) + where TAdapter : IReadWriteAdapter { WriteAsPaxExtendedAttributesShared(isGea, globalExtendedAttributesEntryNumber, extendedAttributes); Debug.Assert(_dataStream == null || (extendedAttributes.Count > 0 && _dataStream.CanSeek)); // We generate the extended attributes data stream, should always be seekable - WriteWithSeekableDataStream(TarEntryFormat.Pax, archiveStream, buffer); - } - - // Asynchronously writes the current header as a PAX Extended Attributes entry into the archive stream and returns the value of the final checksum. - private Task WriteAsPaxExtendedAttributesAsync(Stream archiveStream, Memory buffer, Dictionary extendedAttributes, bool isGea, int globalExtendedAttributesEntryNumber, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - WriteAsPaxExtendedAttributesShared(isGea, globalExtendedAttributesEntryNumber, extendedAttributes); - Debug.Assert(_dataStream == null || (extendedAttributes.Count > 0 && _dataStream.CanSeek)); // We generate the extended attributes data stream, should always be seekable - return WriteWithSeekableDataStreamAsync(TarEntryFormat.Pax, archiveStream, buffer, cancellationToken); + return WriteWithSeekableDataStreamCoreAsync(TarEntryFormat.Pax, archiveStream, buffer, cancellationToken); } // Initializes the name, mode and type flag of a PAX extended attributes entry. @@ -797,65 +608,35 @@ private int WriteGnuFields(Span buffer) } // Writes the current header's data stream into the archive stream. - private void WriteData(Stream archiveStream, Stream dataStream) + private async ValueTask WriteDataCoreAsync(Stream archiveStream, Stream dataStream, CancellationToken cancellationToken) + where TAdapter : IReadWriteAdapter { // Before writing, update the offset field now that the entry belongs to an archive SetDataOffset(this, archiveStream); - dataStream.CopyTo(archiveStream); // The data gets copied from the current position - WriteEmptyPadding(archiveStream); + await TAdapter.CopyToAsync(dataStream, archiveStream, cancellationToken).ConfigureAwait(false); // The data gets copied from the current position + await WriteEmptyPaddingCoreAsync(archiveStream, cancellationToken).ConfigureAwait(false); } // Calculates the padding for the current entry and writes it after the data. - private unsafe void WriteEmptyPadding(Stream archiveStream) + private async ValueTask WriteEmptyPaddingCoreAsync(Stream archiveStream, CancellationToken cancellationToken) + where TAdapter : IReadWriteAdapter { int paddingAfterData = TarHelpers.CalculatePadding(_size); if (paddingAfterData != 0) { Debug.Assert(paddingAfterData <= TarHelpers.RecordSize); - Span zeros = stackalloc byte[TarHelpers.RecordSize]; - zeros = zeros.Slice(0, paddingAfterData); - zeros.Clear(); - - archiveStream.Write(zeros); - } - } - - // Calculates the padding for the current entry and asynchronously writes it after the data. - private ValueTask WriteEmptyPaddingAsync(Stream archiveStream, CancellationToken cancellationToken) - { - int paddingAfterData = TarHelpers.CalculatePadding(_size); - if (paddingAfterData != 0) - { - Debug.Assert(paddingAfterData <= TarHelpers.RecordSize); - - byte[] zeros = new byte[paddingAfterData]; - return archiveStream.WriteAsync(zeros, cancellationToken); - } - - return ValueTask.CompletedTask; - } - - // Asynchronously writes the current header's data stream into the archive stream. - private async Task WriteDataAsync(Stream archiveStream, Stream dataStream, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - - // Before writing, update the offset field now that the entry belongs to an archive - SetDataOffset(this, archiveStream); - - await dataStream.CopyToAsync(archiveStream, cancellationToken).ConfigureAwait(false); // The data gets copied from the current position - - int paddingAfterData = TarHelpers.CalculatePadding(_size); - if (paddingAfterData != 0) - { byte[] buffer = ArrayPool.Shared.Rent(paddingAfterData); - Array.Clear(buffer, 0, paddingAfterData); - - await archiveStream.WriteAsync(buffer.AsMemory(0, paddingAfterData), cancellationToken).ConfigureAwait(false); - - ArrayPool.Shared.Return(buffer); + try + { + Array.Clear(buffer, 0, paddingAfterData); + await TAdapter.WriteAsync(archiveStream, buffer.AsMemory(0, paddingAfterData), cancellationToken).ConfigureAwait(false); + } + finally + { + ArrayPool.Shared.Return(buffer); + } } } diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarWriter.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarWriter.cs index 31a860cd993a26..4fd1cec860daa1 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarWriter.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarWriter.cs @@ -114,26 +114,18 @@ public TarWriter(Stream archiveStream, TarWriterOptions options, bool leaveOpen /// public void Dispose() { - if (!_isDisposed) - { - _isDisposed = true; - - if (_wroteEntries) - { - WriteFinalRecords(); - } - - if (!_leaveOpen) - { - _archiveStream.Dispose(); - } - } + ValueTask vt = DisposeCoreAsync(); + Debug.Assert(vt.IsCompleted, "Synchronous Dispose completed asynchronously."); + vt.GetAwaiter().GetResult(); } /// /// Asynchronously disposes the current instance, and closes the archive stream if the leaveOpen argument was set to in the constructor. /// - public async ValueTask DisposeAsync() + public ValueTask DisposeAsync() => DisposeCoreAsync(); + + private async ValueTask DisposeCoreAsync() + where TAdapter : IReadWriteAdapter { if (!_isDisposed) { @@ -141,12 +133,12 @@ public async ValueTask DisposeAsync() if (_wroteEntries) { - await WriteFinalRecordsAsync().ConfigureAwait(false); + await WriteFinalRecordsCoreAsync().ConfigureAwait(false); } if (!_leaveOpen) { - await _archiveStream.DisposeAsync().ConfigureAwait(false); + await TAdapter.DisposeAsync(_archiveStream).ConfigureAwait(false); } } } @@ -191,29 +183,27 @@ public Task WriteEntryAsync(string fileName, string? entryName, CancellationToke } (string fullPath, string actualEntryName) = ValidateWriteEntryArguments(fileName, entryName); - return ReadFileFromDiskAndWriteToArchiveStreamAsEntryAsync(fullPath, actualEntryName, cancellationToken); + return ReadFileFromDiskAndWriteToArchiveStreamAsEntryCoreAsync(fullPath, actualEntryName, FileOptions.Asynchronous, cancellationToken).AsTask(); } // Reads an entry from disk and writes it into the archive stream. private void ReadFileFromDiskAndWriteToArchiveStreamAsEntry(string fullPath, string entryName) { - TarEntry entry = ConstructEntryForWriting(fullPath, entryName, FileOptions.None); - - WriteEntry(entry); - entry._header._dataStream?.Dispose(); + ValueTask vt = ReadFileFromDiskAndWriteToArchiveStreamAsEntryCoreAsync(fullPath, entryName, FileOptions.None, CancellationToken.None); + Debug.Assert(vt.IsCompleted, "Synchronous WriteEntry completed asynchronously."); + vt.GetAwaiter().GetResult(); } - // Asynchronously reads an entry from disk and writes it into the archive stream. - private async Task ReadFileFromDiskAndWriteToArchiveStreamAsEntryAsync(string fullPath, string entryName, CancellationToken cancellationToken) + // Reads an entry from disk and writes it into the archive stream. + private async ValueTask ReadFileFromDiskAndWriteToArchiveStreamAsEntryCoreAsync(string fullPath, string entryName, FileOptions fileOptions, CancellationToken cancellationToken) + where TAdapter : IReadWriteAdapter { - cancellationToken.ThrowIfCancellationRequested(); - - TarEntry entry = ConstructEntryForWriting(fullPath, entryName, FileOptions.Asynchronous); + TarEntry entry = ConstructEntryForWriting(fullPath, entryName, fileOptions); - await WriteEntryAsync(entry, cancellationToken).ConfigureAwait(false); + await WriteEntryCoreAsync(entry, cancellationToken).ConfigureAwait(false); if (entry._header._dataStream != null) { - await entry._header._dataStream.DisposeAsync().ConfigureAwait(false); + await TAdapter.DisposeAsync(entry._header._dataStream).ConfigureAwait(false); } } @@ -261,7 +251,10 @@ public void WriteEntry(TarEntry entry) ArgumentNullException.ThrowIfNull(entry); ValidateEntryLinkName(entry._header._typeFlag, entry._header._linkName); ValidateStreamsSeekability(entry); - WriteEntryInternal(entry); + + ValueTask vt = WriteEntryCoreAsync(entry, CancellationToken.None); + Debug.Assert(vt.IsCompleted, "Synchronous WriteEntry completed asynchronously."); + vt.GetAwaiter().GetResult(); } /// @@ -310,97 +303,55 @@ public Task WriteEntryAsync(TarEntry entry, CancellationToken cancellationToken ArgumentNullException.ThrowIfNull(entry); ValidateEntryLinkName(entry._header._typeFlag, entry._header._linkName); ValidateStreamsSeekability(entry); - return WriteEntryAsyncInternal(entry, cancellationToken); - } - - // Portion of the WriteEntry(entry) method that rents a buffer and writes to the archive. - private unsafe void WriteEntryInternal(TarEntry entry) - { - Span buffer = stackalloc byte[TarHelpers.RecordSize]; - buffer.Clear(); - - switch (entry.Format) - { - case TarEntryFormat.V7: - entry._header.WriteAsV7(_archiveStream, buffer); - break; - - case TarEntryFormat.Ustar: - entry._header.WriteAsUstar(_archiveStream, buffer); - break; - - case TarEntryFormat.Pax: - if (entry._header._typeFlag is TarEntryType.GlobalExtendedAttributes) - { - entry._header.WriteAsPaxGlobalExtendedAttributes(_archiveStream, buffer, _nextGlobalExtendedAttributesEntryNumber++); - } - else - { - entry._header.WriteAsPax(_archiveStream, buffer); - } - break; - - case TarEntryFormat.Gnu: - entry._header.WriteAsGnu(_archiveStream, buffer); - break; - - default: - Debug.Assert(entry.Format == TarEntryFormat.Unknown, "Missing format handler"); - throw new InvalidDataException(SR.Format(SR.TarInvalidFormat, Format)); - } - - _wroteEntries = true; + return WriteEntryCoreAsync(entry, cancellationToken).AsTask(); } - // Portion of the WriteEntryAsync(TarEntry, CancellationToken) method containing awaits. - private async Task WriteEntryAsyncInternal(TarEntry entry, CancellationToken cancellationToken) + // Portion of the WriteEntry methods that rents a buffer and writes to the archive. + private async ValueTask WriteEntryCoreAsync(TarEntry entry, CancellationToken cancellationToken) + where TAdapter : IReadWriteAdapter { - cancellationToken.ThrowIfCancellationRequested(); - byte[] rented = ArrayPool.Shared.Rent(minimumLength: TarHelpers.RecordSize); Memory buffer = rented.AsMemory(0, TarHelpers.RecordSize); // minimumLength means the array could've been larger buffer.Span.Clear(); // Rented arrays aren't clean - - Task task = entry.Format switch + try { - TarEntryFormat.V7 => entry._header.WriteAsV7Async(_archiveStream, buffer, cancellationToken), - TarEntryFormat.Ustar => entry._header.WriteAsUstarAsync(_archiveStream, buffer, cancellationToken), - TarEntryFormat.Pax when entry._header._typeFlag is TarEntryType.GlobalExtendedAttributes => entry._header.WriteAsPaxGlobalExtendedAttributesAsync(_archiveStream, buffer, _nextGlobalExtendedAttributesEntryNumber++, cancellationToken), - TarEntryFormat.Pax => entry._header.WriteAsPaxAsync(_archiveStream, buffer, cancellationToken), - TarEntryFormat.Gnu => entry._header.WriteAsGnuAsync(_archiveStream, buffer, cancellationToken), - _ => throw new InvalidDataException(SR.Format(SR.TarInvalidFormat, Format)), - }; - await task.ConfigureAwait(false); - - _wroteEntries = true; - - ArrayPool.Shared.Return(rented); - } - - // The spec indicates that the end of the archive is indicated - // by two records consisting entirely of zero bytes. - private unsafe void WriteFinalRecords() - { - Span emptyRecord = stackalloc byte[TarHelpers.RecordSize]; - emptyRecord.Clear(); - - _archiveStream.Write(emptyRecord); - _archiveStream.Write(emptyRecord); + ValueTask task = entry.Format switch + { + TarEntryFormat.V7 => entry._header.WriteAsV7CoreAsync(_archiveStream, buffer, cancellationToken), + TarEntryFormat.Ustar => entry._header.WriteAsUstarCoreAsync(_archiveStream, buffer, cancellationToken), + TarEntryFormat.Pax when entry._header._typeFlag is TarEntryType.GlobalExtendedAttributes => entry._header.WriteAsPaxGlobalExtendedAttributesCoreAsync(_archiveStream, buffer, _nextGlobalExtendedAttributesEntryNumber++, cancellationToken), + TarEntryFormat.Pax => entry._header.WriteAsPaxCoreAsync(_archiveStream, buffer, cancellationToken), + TarEntryFormat.Gnu => entry._header.WriteAsGnuCoreAsync(_archiveStream, buffer, cancellationToken), + _ => throw new InvalidDataException(SR.Format(SR.TarInvalidFormat, Format)), + }; + await task.ConfigureAwait(false); + + _wroteEntries = true; + } + finally + { + ArrayPool.Shared.Return(rented); + } } // The spec indicates that the end of the archive is indicated // by two records consisting entirely of zero bytes. - // This method is called from DisposeAsync, so we don't want to propagate a cancelled CancellationToken. - private async ValueTask WriteFinalRecordsAsync() + // This method is called from Dispose/DisposeAsync, so we don't want to propagate a cancelled CancellationToken. + private async ValueTask WriteFinalRecordsCoreAsync() + where TAdapter : IReadWriteAdapter { const int TwoRecordSize = TarHelpers.RecordSize * 2; byte[] twoEmptyRecords = ArrayPool.Shared.Rent(TwoRecordSize); - Array.Clear(twoEmptyRecords, 0, TwoRecordSize); - - await _archiveStream.WriteAsync(twoEmptyRecords.AsMemory(0, TwoRecordSize), cancellationToken: default).ConfigureAwait(false); - - ArrayPool.Shared.Return(twoEmptyRecords); + try + { + Array.Clear(twoEmptyRecords, 0, TwoRecordSize); + await TAdapter.WriteAsync(_archiveStream, twoEmptyRecords.AsMemory(0, TwoRecordSize), CancellationToken.None).ConfigureAwait(false); + } + finally + { + ArrayPool.Shared.Return(twoEmptyRecords); + } } private (string, string) ValidateWriteEntryArguments(string fileName, string? entryName) From 296c608e5a87f9be082beef0d1cfa32db195b786 Mon Sep 17 00:00:00 2001 From: alinpahontu2912 Date: Fri, 12 Jun 2026 11:37:43 +0300 Subject: [PATCH 04/10] inline helpers --- .../src/System/Formats/Tar/TarEntry.cs | 4 +++- .../src/System/Formats/Tar/TarHelpers.cs | 6 +++++- .../src/System/Formats/Tar/TarReader.cs | 10 +--------- .../src/System/Formats/Tar/TarWriter.cs | 12 +++--------- 4 files changed, 12 insertions(+), 20 deletions(-) diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs index 6315930d7a1f4a..f909cb8e029d46 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs @@ -285,7 +285,9 @@ public Stream? DataStream // This entry came from a reader, so if the underlying stream is unseekable, we need to // manually advance the stream pointer to the next header before doing the substitution // The original stream will get disposed when the reader gets disposed. - _readerOfOrigin.AdvanceDataStreamIfNeeded(); + ValueTask vt = _readerOfOrigin.AdvanceDataStreamIfNeededCoreAsync(CancellationToken.None); + Debug.Assert(vt.IsCompleted, "Synchronous AdvanceDataStreamIfNeeded completed asynchronously."); + vt.GetAwaiter().GetResult(); // We only do this once _readerOfOrigin = null; } diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHelpers.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHelpers.cs index c59a65a0073027..150d9a90ca9442 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHelpers.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHelpers.cs @@ -3,6 +3,7 @@ using System.Buffers; using System.Collections.Generic; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; @@ -11,7 +12,6 @@ using System.Text; using System.Threading; using System.Threading.Tasks; -using System.Diagnostics; namespace System.Formats.Tar { @@ -71,6 +71,8 @@ internal static ValueTask AdvanceStreamAsync(Stream archiveStream, long bytesToD internal static async ValueTask AdvanceStreamCoreAsync(Stream archiveStream, long bytesToDiscard, CancellationToken cancellationToken) where TAdapter : IReadWriteAdapter { + cancellationToken.ThrowIfCancellationRequested(); + if (archiveStream.CanSeek) { archiveStream.Position += bytesToDiscard; @@ -98,6 +100,8 @@ internal static async ValueTask AdvanceStreamCoreAsync(Stream archiveS internal static async ValueTask CopyBytesCoreAsync(Stream origin, Stream destination, long bytesToCopy, CancellationToken cancellationToken) where TAdapter : IReadWriteAdapter { + cancellationToken.ThrowIfCancellationRequested(); + byte[] buffer = ArrayPool.Shared.Rent(minimumLength: (int)Math.Min(MaxBufferLength, bytesToCopy)); try { diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarReader.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarReader.cs index 1071557a496238..4ac0c72fa66a33 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarReader.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarReader.cs @@ -162,15 +162,7 @@ private async ValueTask DisposeCoreAsync() } // Moves the underlying archive stream position pointer to the beginning of the next header. - internal void AdvanceDataStreamIfNeeded() - { - ValueTask vt = AdvanceDataStreamIfNeededCoreAsync(CancellationToken.None); - Debug.Assert(vt.IsCompleted, "Synchronous AdvanceDataStreamIfNeeded completed asynchronously."); - vt.GetAwaiter().GetResult(); - } - - // Moves the underlying archive stream position pointer to the beginning of the next header. - private async ValueTask AdvanceDataStreamIfNeededCoreAsync(CancellationToken cancellationToken) + internal async ValueTask AdvanceDataStreamIfNeededCoreAsync(CancellationToken cancellationToken) where TAdapter : IReadWriteAdapter { if (_previouslyReadEntry == null) diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarWriter.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarWriter.cs index 4fd1cec860daa1..4928949ca4d3ec 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarWriter.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarWriter.cs @@ -158,7 +158,9 @@ private async ValueTask DisposeCoreAsync() public void WriteEntry(string fileName, string? entryName) { (string fullPath, string actualEntryName) = ValidateWriteEntryArguments(fileName, entryName); - ReadFileFromDiskAndWriteToArchiveStreamAsEntry(fullPath, actualEntryName); + ValueTask vt = ReadFileFromDiskAndWriteToArchiveStreamAsEntryCoreAsync(fullPath, actualEntryName, FileOptions.None, CancellationToken.None); + Debug.Assert(vt.IsCompleted, "Synchronous WriteEntry completed asynchronously."); + vt.GetAwaiter().GetResult(); } /// @@ -186,14 +188,6 @@ public Task WriteEntryAsync(string fileName, string? entryName, CancellationToke return ReadFileFromDiskAndWriteToArchiveStreamAsEntryCoreAsync(fullPath, actualEntryName, FileOptions.Asynchronous, cancellationToken).AsTask(); } - // Reads an entry from disk and writes it into the archive stream. - private void ReadFileFromDiskAndWriteToArchiveStreamAsEntry(string fullPath, string entryName) - { - ValueTask vt = ReadFileFromDiskAndWriteToArchiveStreamAsEntryCoreAsync(fullPath, entryName, FileOptions.None, CancellationToken.None); - Debug.Assert(vt.IsCompleted, "Synchronous WriteEntry completed asynchronously."); - vt.GetAwaiter().GetResult(); - } - // Reads an entry from disk and writes it into the archive stream. private async ValueTask ReadFileFromDiskAndWriteToArchiveStreamAsEntryCoreAsync(string fullPath, string entryName, FileOptions fileOptions, CancellationToken cancellationToken) where TAdapter : IReadWriteAdapter From 44c034e71f898d0708ac0274b02ce242be2b22cd Mon Sep 17 00:00:00 2001 From: alinpahontu2912 Date: Thu, 18 Jun 2026 11:42:24 +0300 Subject: [PATCH 05/10] refactor gnusparsestream --- .../src/System/Formats/Tar/GnuSparseStream.cs | 164 +++++------------- .../src/System/Formats/Tar/TarHelpers.cs | 18 -- .../System/Formats/Tar/TarReadWriteAdapter.cs | 7 + 3 files changed, 51 insertions(+), 138 deletions(-) diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/GnuSparseStream.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/GnuSparseStream.cs index 73429b525e10db..cbf9cd1b4f50df 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/GnuSparseStream.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/GnuSparseStream.cs @@ -54,25 +54,15 @@ internal GnuSparseStream(Stream rawStream, long realSize) // Parses the sparse map on first read. Populates _segments, _packedStartOffsets, // and _dataStart. Throws InvalidDataException if the sparse map is malformed. - private void EnsureInitialized() + private async ValueTask EnsureInitializedCoreAsync(CancellationToken cancellationToken) + where TAdapter : IReadWriteAdapter { if (_segments is not null) { return; } - var segments = ParseSparseMap(isAsync: false, _rawStream, CancellationToken.None).GetAwaiter().GetResult(); - InitializeFromParsedMap(segments); - } - - private async ValueTask EnsureInitializedAsync(CancellationToken cancellationToken) - { - if (_segments is not null) - { - return; - } - - var segments = await ParseSparseMap(isAsync: true, _rawStream, cancellationToken).ConfigureAwait(false); + var segments = await ParseSparseMapCoreAsync(_rawStream, cancellationToken).ConfigureAwait(false); InitializeFromParsedMap(segments); } @@ -176,48 +166,24 @@ public override int Read(byte[] buffer, int offset, int count) public override int Read(Span destination) { ThrowIfDisposed(); - EnsureInitialized(); - Debug.Assert(_segments is not null && _packedStartOffsets is not null); - if (destination.IsEmpty || _virtualPosition >= _realSize) { return 0; } - int toRead = (int)Math.Min(destination.Length, _realSize - _virtualPosition); - destination = destination.Slice(0, toRead); - - int totalFilled = 0; - while (totalFilled < toRead) + byte[] rented = ArrayPool.Shared.Rent(destination.Length); + try { - long vPos = _virtualPosition + totalFilled; - int segIdx = FindSegmentFromCurrent(vPos); - - if (segIdx < 0) - { - // vPos is in a sparse hole — fill with zeros until the next segment or end of file. - long nextSegStart = ~segIdx < _segments.Length ? _segments[~segIdx].Offset : _realSize; - int zeroCount = (int)Math.Min(toRead - totalFilled, nextSegStart - vPos); - destination.Slice(totalFilled, zeroCount).Clear(); - totalFilled += zeroCount; - } - else - { - // vPos is within segment segIdx — read from packed data. - var (segOffset, segLength) = _segments[segIdx]; - long offsetInSeg = vPos - segOffset; - long remainingInSeg = segLength - offsetInSeg; - int countToRead = (int)Math.Min(toRead - totalFilled, remainingInSeg); - - long packedOffset = _packedStartOffsets[segIdx] + offsetInSeg; - int bytesRead = ReadFromPackedData(destination.Slice(totalFilled, countToRead), packedOffset); - totalFilled += bytesRead; - break; // Return after an underlying read; caller can call Read again for more. - } + ValueTask vt = ReadCoreAsync(rented.AsMemory(0, destination.Length), CancellationToken.None); + Debug.Assert(vt.IsCompleted, "Synchronous Read completed asynchronously."); + int bytesRead = vt.GetAwaiter().GetResult(); + rented.AsSpan(0, bytesRead).CopyTo(destination); + return bytesRead; + } + finally + { + ArrayPool.Shared.Return(rented); } - - _virtualPosition += totalFilled; - return totalFilled; } public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) @@ -241,12 +207,13 @@ public override ValueTask ReadAsync(Memory buffer, CancellationToken { return ValueTask.FromResult(0); } - return ReadAsyncCore(buffer, cancellationToken); + return ReadCoreAsync(buffer, cancellationToken); } - private async ValueTask ReadAsyncCore(Memory buffer, CancellationToken cancellationToken) + private async ValueTask ReadCoreAsync(Memory buffer, CancellationToken cancellationToken) + where TAdapter : IReadWriteAdapter { - await EnsureInitializedAsync(cancellationToken).ConfigureAwait(false); + await EnsureInitializedCoreAsync(cancellationToken).ConfigureAwait(false); Debug.Assert(_segments is not null && _packedStartOffsets is not null); int toRead = (int)Math.Min(buffer.Length, _realSize - _virtualPosition); @@ -273,7 +240,7 @@ private async ValueTask ReadAsyncCore(Memory buffer, CancellationToke int countToRead = (int)Math.Min(toRead - totalFilled, remainingInSeg); long packedOffset = _packedStartOffsets[segIdx] + offsetInSeg; - int bytesRead = await ReadFromPackedDataAsync(buffer.Slice(totalFilled, countToRead), packedOffset, cancellationToken).ConfigureAwait(false); + int bytesRead = await ReadFromPackedDataCoreAsync(buffer.Slice(totalFilled, countToRead), packedOffset, cancellationToken).ConfigureAwait(false); totalFilled += bytesRead; break; } @@ -297,56 +264,27 @@ private async ValueTask ReadAsyncCore(Memory buffer, CancellationToke // the entry's real (expanded) size. internal void CopyPopulatedDataTo(FileStream destination) { - ThrowIfDisposed(); - EnsureInitialized(); - Debug.Assert(_segments is not null && _packedStartOffsets is not null); - - byte[] buffer = ArrayPool.Shared.Rent(81920); - try - { - for (int i = 0; i < _segments.Length; i++) - { - (long virtualOffset, long segmentLength) = _segments[i]; - if (segmentLength == 0) - { - continue; - } - - destination.Position = virtualOffset; - long written = 0; - while (written < segmentLength) - { - int toRead = (int)Math.Min(segmentLength - written, buffer.Length); - int bytesRead = ReadFromPackedData(buffer.AsSpan(0, toRead), _packedStartOffsets[i] + written); - if (bytesRead == 0) - { - throw new EndOfStreamException(); - } - destination.Write(buffer, 0, bytesRead); - written += bytesRead; - } - } - } - finally - { - ArrayPool.Shared.Return(buffer); - } + ValueTask vt = CopyPopulatedDataToCoreAsync(destination, CancellationToken.None); + Debug.Assert(vt.IsCompleted, "Synchronous CopyPopulatedDataTo completed asynchronously."); + vt.GetAwaiter().GetResult(); + } - // Extend the destination to the full real size so any trailing hole is materialized - // (as an unallocated extent on sparse-capable file systems, or as zeros otherwise). - if (destination.Length < _realSize) + // Async counterpart to CopyPopulatedDataTo. + internal ValueTask CopyPopulatedDataToAsync(FileStream destination, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) { - destination.SetLength(_realSize); + return ValueTask.FromCanceled(cancellationToken); } - _virtualPosition = _realSize; + return CopyPopulatedDataToCoreAsync(destination, cancellationToken); } - // Async counterpart to CopyPopulatedDataTo. - internal async ValueTask CopyPopulatedDataToAsync(FileStream destination, CancellationToken cancellationToken) + private async ValueTask CopyPopulatedDataToCoreAsync(FileStream destination, CancellationToken cancellationToken) + where TAdapter : IReadWriteAdapter { ThrowIfDisposed(); - await EnsureInitializedAsync(cancellationToken).ConfigureAwait(false); + await EnsureInitializedCoreAsync(cancellationToken).ConfigureAwait(false); Debug.Assert(_segments is not null && _packedStartOffsets is not null); byte[] buffer = ArrayPool.Shared.Rent(81920); @@ -365,12 +303,12 @@ internal async ValueTask CopyPopulatedDataToAsync(FileStream destination, Cancel while (written < segmentLength) { int toRead = (int)Math.Min(segmentLength - written, buffer.Length); - int bytesRead = await ReadFromPackedDataAsync(buffer.AsMemory(0, toRead), _packedStartOffsets[i] + written, cancellationToken).ConfigureAwait(false); + int bytesRead = await ReadFromPackedDataCoreAsync(buffer.AsMemory(0, toRead), _packedStartOffsets[i] + written, cancellationToken).ConfigureAwait(false); if (bytesRead == 0) { throw new EndOfStreamException(); } - await destination.WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken).ConfigureAwait(false); + await TAdapter.WriteAsync(destination, buffer.AsMemory(0, bytesRead), cancellationToken).ConfigureAwait(false); written += bytesRead; } } @@ -380,6 +318,8 @@ internal async ValueTask CopyPopulatedDataToAsync(FileStream destination, Cancel ArrayPool.Shared.Return(buffer); } + // Extend the destination to the full real size so any trailing hole is materialized + // (as an unallocated extent on sparse-capable file systems, or as zeros otherwise). if (destination.Length < _realSize) { destination.SetLength(_realSize); @@ -392,23 +332,8 @@ internal async ValueTask CopyPopulatedDataToAsync(FileStream destination, Cancel // After EnsureInitialized, the raw stream is positioned at _dataStart and // _nextPackedOffset tracks how far into the packed data we've read. // Returns the number of bytes actually read (may be less than destination.Length). - private int ReadFromPackedData(Span destination, long packedOffset) - { - long skipBytes = packedOffset - _nextPackedOffset; - if (skipBytes < 0 && !_rawStream.CanSeek) - { - throw new InvalidOperationException(SR.IO_NotSupported_UnseekableStream); - } - if (skipBytes != 0) - { - TarHelpers.AdvanceStream(_rawStream, skipBytes); - } - int bytesRead = _rawStream.Read(destination); - _nextPackedOffset = packedOffset + bytesRead; - return bytesRead; - } - - private async ValueTask ReadFromPackedDataAsync(Memory destination, long packedOffset, CancellationToken cancellationToken) + private async ValueTask ReadFromPackedDataCoreAsync(Memory destination, long packedOffset, CancellationToken cancellationToken) + where TAdapter : IReadWriteAdapter { long skipBytes = packedOffset - _nextPackedOffset; if (skipBytes < 0 && !_rawStream.CanSeek) @@ -417,9 +342,9 @@ private async ValueTask ReadFromPackedDataAsync(Memory destination, l } if (skipBytes != 0) { - await TarHelpers.AdvanceStreamAsync(_rawStream, skipBytes, cancellationToken).ConfigureAwait(false); + await TarHelpers.AdvanceStreamCoreAsync(_rawStream, skipBytes, cancellationToken).ConfigureAwait(false); } - int bytesRead = await _rawStream.ReadAsync(destination, cancellationToken).ConfigureAwait(false); + int bytesRead = await TAdapter.ReadAsync(_rawStream, destination, cancellationToken).ConfigureAwait(false); _nextPackedOffset = packedOffset + bytesRead; return bytesRead; } @@ -512,8 +437,9 @@ private int BinarySearchSegment(long virtualPosition, int lo, int hi) // and then the packed data begins. // // Returns the parsed segments. - private static async Task<(long Offset, long Length)[]> ParseSparseMap( - bool isAsync, Stream rawStream, CancellationToken cancellationToken) + private static async ValueTask<(long Offset, long Length)[]> ParseSparseMapCoreAsync( + Stream rawStream, CancellationToken cancellationToken) + where TAdapter : IReadWriteAdapter { // The buffer is 2 * RecordSize (1024 bytes) and each fill reads exactly RecordSize (512) // bytes. This guarantees that the total bytes read is always a multiple of RecordSize, @@ -538,9 +464,7 @@ async ValueTask FillBufferAsync() activeStart = 0; availableStart = active; - int newBytes = isAsync - ? await rawStream.ReadAtLeastAsync(bytes.AsMemory(availableStart, TarHelpers.RecordSize), TarHelpers.RecordSize, throwOnEndOfStream: false, cancellationToken).ConfigureAwait(false) - : rawStream.ReadAtLeast(bytes.AsSpan(availableStart, TarHelpers.RecordSize), TarHelpers.RecordSize, throwOnEndOfStream: false); + int newBytes = await TAdapter.ReadAtLeastAsync(rawStream, bytes.AsMemory(availableStart, TarHelpers.RecordSize), TarHelpers.RecordSize, throwOnEndOfStream: false, cancellationToken).ConfigureAwait(false); availableStart += newBytes; return newBytes > 0; diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHelpers.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHelpers.cs index 150d9a90ca9442..d66d834e9e9f7c 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHelpers.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHelpers.cs @@ -50,24 +50,6 @@ internal static int GetDefaultMode(TarEntryType type) => type is TarEntryType.Directory or TarEntryType.DirectoryList ? (int)DefaultDirectoryMode : (int)DefaultFileMode; // Helps advance the stream a total number of bytes larger than int.MaxValue. - internal static void AdvanceStream(Stream archiveStream, long bytesToDiscard) - { - ValueTask vt = AdvanceStreamCoreAsync(archiveStream, bytesToDiscard, CancellationToken.None); - Debug.Assert(vt.IsCompleted, "Synchronous AdvanceStream completed asynchronously."); - vt.GetAwaiter().GetResult(); - } - - // Asynchronously helps advance the stream a total number of bytes larger than int.MaxValue. - internal static ValueTask AdvanceStreamAsync(Stream archiveStream, long bytesToDiscard, CancellationToken cancellationToken) - { - if (cancellationToken.IsCancellationRequested) - { - return ValueTask.FromCanceled(cancellationToken); - } - - return AdvanceStreamCoreAsync(archiveStream, bytesToDiscard, cancellationToken); - } - internal static async ValueTask AdvanceStreamCoreAsync(Stream archiveStream, long bytesToDiscard, CancellationToken cancellationToken) where TAdapter : IReadWriteAdapter { diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarReadWriteAdapter.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarReadWriteAdapter.cs index f492530a0d6b5f..fe02014ced4260 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarReadWriteAdapter.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarReadWriteAdapter.cs @@ -15,6 +15,7 @@ namespace System.Formats.Tar internal interface IReadWriteAdapter { static abstract ValueTask ReadAsync(Stream stream, Memory buffer, CancellationToken cancellationToken); + static abstract ValueTask ReadAtLeastAsync(Stream stream, Memory buffer, int minimumBytes, bool throwOnEndOfStream, CancellationToken cancellationToken); static abstract ValueTask ReadExactlyAsync(Stream stream, Memory buffer, CancellationToken cancellationToken); static abstract ValueTask WriteAsync(Stream stream, ReadOnlyMemory buffer, CancellationToken cancellationToken); static abstract ValueTask CopyToAsync(Stream source, Stream destination, CancellationToken cancellationToken); @@ -27,6 +28,9 @@ internal interface IReadWriteAdapter public static ValueTask ReadAsync(Stream stream, Memory buffer, CancellationToken cancellationToken) => stream.ReadAsync(buffer, cancellationToken); + public static ValueTask ReadAtLeastAsync(Stream stream, Memory buffer, int minimumBytes, bool throwOnEndOfStream, CancellationToken cancellationToken) => + stream.ReadAtLeastAsync(buffer, minimumBytes, throwOnEndOfStream, cancellationToken); + public static ValueTask ReadExactlyAsync(Stream stream, Memory buffer, CancellationToken cancellationToken) => stream.ReadExactlyAsync(buffer, cancellationToken); @@ -47,6 +51,9 @@ public static ValueTask AdvanceToEndAsync(SubReadStream stream, CancellationToke public static ValueTask ReadAsync(Stream stream, Memory buffer, CancellationToken cancellationToken) => new ValueTask(stream.Read(buffer.Span)); + public static ValueTask ReadAtLeastAsync(Stream stream, Memory buffer, int minimumBytes, bool throwOnEndOfStream, CancellationToken cancellationToken) => + new ValueTask(stream.ReadAtLeast(buffer.Span, minimumBytes, throwOnEndOfStream)); + public static ValueTask ReadExactlyAsync(Stream stream, Memory buffer, CancellationToken cancellationToken) { stream.ReadExactly(buffer.Span); From b935944ea2cb6cb6e0403de4f2f120fc2693e46f Mon Sep 17 00:00:00 2001 From: alinpahontu2912 Date: Thu, 18 Jun 2026 12:30:38 +0300 Subject: [PATCH 06/10] avoid large sized rent arrays --- .../src/System/Formats/Tar/GnuSparseStream.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/GnuSparseStream.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/GnuSparseStream.cs index cbf9cd1b4f50df..81662e1f03e6b3 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/GnuSparseStream.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/GnuSparseStream.cs @@ -171,10 +171,11 @@ public override int Read(Span destination) return 0; } - byte[] rented = ArrayPool.Shared.Rent(destination.Length); + int toRead = (int)Math.Min(destination.Length, _realSize - _virtualPosition); + byte[] rented = ArrayPool.Shared.Rent(toRead); try { - ValueTask vt = ReadCoreAsync(rented.AsMemory(0, destination.Length), CancellationToken.None); + ValueTask vt = ReadCoreAsync(rented.AsMemory(0, toRead), CancellationToken.None); Debug.Assert(vt.IsCompleted, "Synchronous Read completed asynchronously."); int bytesRead = vt.GetAwaiter().GetResult(); rented.AsSpan(0, bytesRead).CopyTo(destination); From 6dc287e6e0e4e9f47477a0d8c6d2913df8da5569 Mon Sep 17 00:00:00 2001 From: Stefan-Alin Pahontu <56953855+alinpahontu2912@users.noreply.github.com> Date: Fri, 19 Jun 2026 10:35:47 +0300 Subject: [PATCH 07/10] remove unnecessary using Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../System.Formats.Tar/src/System/Formats/Tar/TarHelpers.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHelpers.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHelpers.cs index d66d834e9e9f7c..89252d198d31dc 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHelpers.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHelpers.cs @@ -3,7 +3,6 @@ using System.Buffers; using System.Collections.Generic; -using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; From e63a8c1551c5b4028c8664f90fdb01c144fc9e18 Mon Sep 17 00:00:00 2001 From: alinpahontu2912 Date: Mon, 22 Jun 2026 14:11:09 +0300 Subject: [PATCH 08/10] leave just one shared adaptor + refactoring --- .../Common/src/System/Net/ReadWriteAdapter.cs | 76 ------------------- .../src/System/ReadWriteAdapter.cs} | 52 +++++++++---- .../src/System.Formats.Tar.csproj | 2 +- .../src/System/Formats/Tar/TarReader.cs | 13 +++- .../src/System.Net.Mail.csproj | 4 +- .../Unit/System.Net.Mail.Unit.Tests.csproj | 4 +- .../src/System.Net.Security.csproj | 4 +- .../System.Net.Security.Unit.Tests.csproj | 4 +- 8 files changed, 60 insertions(+), 99 deletions(-) delete mode 100644 src/libraries/Common/src/System/Net/ReadWriteAdapter.cs rename src/libraries/{System.Formats.Tar/src/System/Formats/Tar/TarReadWriteAdapter.cs => Common/src/System/ReadWriteAdapter.cs} (68%) diff --git a/src/libraries/Common/src/System/Net/ReadWriteAdapter.cs b/src/libraries/Common/src/System/Net/ReadWriteAdapter.cs deleted file mode 100644 index 7019d6f1da4917..00000000000000 --- a/src/libraries/Common/src/System/Net/ReadWriteAdapter.cs +++ /dev/null @@ -1,76 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace System.Net -{ - internal interface IReadWriteAdapter - { - static abstract ValueTask ReadAsync(Stream stream, Memory buffer, CancellationToken cancellationToken); - static abstract ValueTask ReadAtLeastAsync(Stream stream, Memory buffer, int minimumBytes, bool throwOnEndOfStream, CancellationToken cancellationToken); - static abstract ValueTask WriteAsync(Stream stream, ReadOnlyMemory buffer, CancellationToken cancellationToken); - static abstract Task FlushAsync(Stream stream, CancellationToken cancellationToken); - static abstract Task WaitAsync(TaskCompletionSource waiter); - static abstract Task WaitAsync(Task task); - static abstract ValueTask WaitAsync(ValueTask task); - } - - internal readonly struct AsyncReadWriteAdapter : IReadWriteAdapter - { - public static ValueTask ReadAsync(Stream stream, Memory buffer, CancellationToken cancellationToken) => - stream.ReadAsync(buffer, cancellationToken); - - public static ValueTask ReadAtLeastAsync(Stream stream, Memory buffer, int minimumBytes, bool throwOnEndOfStream, CancellationToken cancellationToken) => - stream.ReadAtLeastAsync(buffer, minimumBytes, throwOnEndOfStream, cancellationToken); - - public static ValueTask WriteAsync(Stream stream, ReadOnlyMemory buffer, CancellationToken cancellationToken) => - stream.WriteAsync(buffer, cancellationToken); - - public static Task FlushAsync(Stream stream, CancellationToken cancellationToken) => stream.FlushAsync(cancellationToken); - - public static Task WaitAsync(TaskCompletionSource waiter) => waiter.Task; - public static Task WaitAsync(Task task) => task; - public static ValueTask WaitAsync(ValueTask task) => task; - } - - internal readonly struct SyncReadWriteAdapter : IReadWriteAdapter - { - public static ValueTask ReadAsync(Stream stream, Memory buffer, CancellationToken cancellationToken) => - new ValueTask(stream.Read(buffer.Span)); - - public static ValueTask ReadAtLeastAsync(Stream stream, Memory buffer, int minimumBytes, bool throwOnEndOfStream, CancellationToken cancellationToken) => - new ValueTask(stream.ReadAtLeast(buffer.Span, minimumBytes, throwOnEndOfStream)); - - public static ValueTask WriteAsync(Stream stream, ReadOnlyMemory buffer, CancellationToken cancellationToken) - { - stream.Write(buffer.Span); - return default; - } - - public static Task FlushAsync(Stream stream, CancellationToken cancellationToken) - { - stream.Flush(); - return Task.CompletedTask; - } - - public static Task WaitAsync(TaskCompletionSource waiter) - { - waiter.Task.GetAwaiter().GetResult(); - return Task.CompletedTask; - } - - public static Task WaitAsync(Task task) - { - task.GetAwaiter().GetResult(); - return Task.CompletedTask; - } - - public static ValueTask WaitAsync(ValueTask task) - { - return ValueTask.FromResult(task.AsTask().GetAwaiter().GetResult()); - } - } -} diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarReadWriteAdapter.cs b/src/libraries/Common/src/System/ReadWriteAdapter.cs similarity index 68% rename from src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarReadWriteAdapter.cs rename to src/libraries/Common/src/System/ReadWriteAdapter.cs index fe02014ced4260..5b8632d1110b35 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarReadWriteAdapter.cs +++ b/src/libraries/Common/src/System/ReadWriteAdapter.cs @@ -5,13 +5,16 @@ using System.Threading; using System.Threading.Tasks; -namespace System.Formats.Tar +namespace System { // Static-abstract adapter that lets a single generic implementation serve both - // synchronous and asynchronous Tar code paths. The sync entry point passes + // synchronous and asynchronous code paths. The sync entry point passes // SyncReadWriteAdapter and asserts the returned ValueTask is already completed; - // the async entry point passes AsyncReadWriteAdapter. Mirrors the IReadWriteAdapter - // pattern used by SslStream / SmtpClient (see src/libraries/Common/src/System/Net/ReadWriteAdapter.cs). + // the async entry point passes AsyncReadWriteAdapter. + // + // Originally used by SslStream and SmtpClient for networking I/O deduplication. + // Extended for System.Formats.Tar (CopyToAsync, ReadExactlyAsync, DisposeAsync) + // to unify TarReader/TarWriter/GnuSparseStream sync/async pairs. internal interface IReadWriteAdapter { static abstract ValueTask ReadAsync(Stream stream, Memory buffer, CancellationToken cancellationToken); @@ -19,8 +22,11 @@ internal interface IReadWriteAdapter static abstract ValueTask ReadExactlyAsync(Stream stream, Memory buffer, CancellationToken cancellationToken); static abstract ValueTask WriteAsync(Stream stream, ReadOnlyMemory buffer, CancellationToken cancellationToken); static abstract ValueTask CopyToAsync(Stream source, Stream destination, CancellationToken cancellationToken); - static abstract ValueTask AdvanceToEndAsync(SubReadStream stream, CancellationToken cancellationToken); static abstract ValueTask DisposeAsync(Stream stream); + static abstract Task FlushAsync(Stream stream, CancellationToken cancellationToken); + static abstract Task WaitAsync(TaskCompletionSource waiter); + static abstract Task WaitAsync(Task task); + static abstract ValueTask WaitAsync(ValueTask task); } internal readonly struct AsyncReadWriteAdapter : IReadWriteAdapter @@ -40,10 +46,13 @@ public static ValueTask WriteAsync(Stream stream, ReadOnlyMemory buffer, C public static ValueTask CopyToAsync(Stream source, Stream destination, CancellationToken cancellationToken) => new ValueTask(source.CopyToAsync(destination, cancellationToken)); - public static ValueTask AdvanceToEndAsync(SubReadStream stream, CancellationToken cancellationToken) => - stream.AdvanceToEndAsync(cancellationToken); - public static ValueTask DisposeAsync(Stream stream) => stream.DisposeAsync(); + + public static Task FlushAsync(Stream stream, CancellationToken cancellationToken) => stream.FlushAsync(cancellationToken); + + public static Task WaitAsync(TaskCompletionSource waiter) => waiter.Task; + public static Task WaitAsync(Task task) => task; + public static ValueTask WaitAsync(ValueTask task) => task; } internal readonly struct SyncReadWriteAdapter : IReadWriteAdapter @@ -72,16 +81,33 @@ public static ValueTask CopyToAsync(Stream source, Stream destination, Cancellat return default; } - public static ValueTask AdvanceToEndAsync(SubReadStream stream, CancellationToken cancellationToken) + public static ValueTask DisposeAsync(Stream stream) { - stream.AdvanceToEnd(); + stream.Dispose(); return default; } - public static ValueTask DisposeAsync(Stream stream) + public static Task FlushAsync(Stream stream, CancellationToken cancellationToken) { - stream.Dispose(); - return default; + stream.Flush(); + return Task.CompletedTask; + } + + public static Task WaitAsync(TaskCompletionSource waiter) + { + waiter.Task.GetAwaiter().GetResult(); + return Task.CompletedTask; + } + + public static Task WaitAsync(Task task) + { + task.GetAwaiter().GetResult(); + return Task.CompletedTask; + } + + public static ValueTask WaitAsync(ValueTask task) + { + return ValueTask.FromResult(task.AsTask().GetAwaiter().GetResult()); } } } diff --git a/src/libraries/System.Formats.Tar/src/System.Formats.Tar.csproj b/src/libraries/System.Formats.Tar/src/System.Formats.Tar.csproj index abd591acbc6df5..d6464be9ee414b 100644 --- a/src/libraries/System.Formats.Tar/src/System.Formats.Tar.csproj +++ b/src/libraries/System.Formats.Tar/src/System.Formats.Tar.csproj @@ -35,7 +35,7 @@ - + diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarReader.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarReader.cs index 4ac0c72fa66a33..948f601b72e33f 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarReader.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarReader.cs @@ -187,7 +187,18 @@ internal async ValueTask AdvanceDataStreamIfNeededCoreAsync(Cancellati return; } - await TAdapter.AdvanceToEndAsync(dataStream, cancellationToken).ConfigureAwait(false); + // SubReadStream is not available in all assemblies that consume the shared + // IReadWriteAdapter (e.g. Net.Security, Net.Mail), so AdvanceToEnd cannot + // live on the adapter interface. Use typeof(TAdapter) to dispatch sync/async; + // the JIT eliminates the dead branch when the generic is specialized. + if (typeof(TAdapter) == typeof(SyncReadWriteAdapter)) + { + dataStream.AdvanceToEnd(); + } + else + { + await dataStream.AdvanceToEndAsync(cancellationToken).ConfigureAwait(false); + } await TarHelpers.SkipBlockAlignmentPaddingCoreAsync(_archiveStream, _previouslyReadEntry._header._size, cancellationToken).ConfigureAwait(false); } diff --git a/src/libraries/System.Net.Mail/src/System.Net.Mail.csproj b/src/libraries/System.Net.Mail/src/System.Net.Mail.csproj index 811532eb7d1ee5..46fc41705f60cb 100644 --- a/src/libraries/System.Net.Mail/src/System.Net.Mail.csproj +++ b/src/libraries/System.Net.Mail/src/System.Net.Mail.csproj @@ -68,8 +68,8 @@ - + - + - + - + Date: Wed, 24 Jun 2026 08:45:34 +0300 Subject: [PATCH 09/10] fix reflection usage --- .../System.Net.Mail/tests/Functional/MailMessageTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/System.Net.Mail/tests/Functional/MailMessageTest.cs b/src/libraries/System.Net.Mail/tests/Functional/MailMessageTest.cs index b3d3d89e77aa26..046cf4f95bab26 100644 --- a/src/libraries/System.Net.Mail/tests/Functional/MailMessageTest.cs +++ b/src/libraries/System.Net.Mail/tests/Functional/MailMessageTest.cs @@ -261,7 +261,7 @@ private static (string Raw, string Attachment) DecodeSentMailMessage(MailMessage culture: null, activationAttributes: null); - var syncSendAdapterType = Type.GetType("System.Net.SyncReadWriteAdapter, System.Net.Mail"); + var syncSendAdapterType = Type.GetType("System.SyncReadWriteAdapter, System.Net.Mail"); // Send the message. #pragma warning disable IL3050 // Roslyn analyzer can't see through the private reflection, but publish process can. This is safe. From 10f292b6f42e894b8c873f4cace6d39eaf5f4980 Mon Sep 17 00:00:00 2001 From: alinpahontu2912 Date: Wed, 24 Jun 2026 10:21:32 +0300 Subject: [PATCH 10/10] add cancellationtoken check --- .../System.Formats.Tar/src/System/Formats/Tar/TarWriter.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarWriter.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarWriter.cs index 4928949ca4d3ec..72b9f3b9d49d73 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarWriter.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarWriter.cs @@ -192,6 +192,8 @@ public Task WriteEntryAsync(string fileName, string? entryName, CancellationToke private async ValueTask ReadFileFromDiskAndWriteToArchiveStreamAsEntryCoreAsync(string fullPath, string entryName, FileOptions fileOptions, CancellationToken cancellationToken) where TAdapter : IReadWriteAdapter { + cancellationToken.ThrowIfCancellationRequested(); + TarEntry entry = ConstructEntryForWriting(fullPath, entryName, fileOptions); await WriteEntryCoreAsync(entry, cancellationToken).ConfigureAwait(false);