From 877537b914675c5df73959e999946b284717414d Mon Sep 17 00:00:00 2001 From: iremyux Date: Tue, 4 Aug 2026 19:24:09 +0200 Subject: [PATCH 1/4] Restore synchronous TarWriter write path Restore the Span-based synchronous write implementation removed by the sync/async adapter refactor. This keeps header buffers and data padding on the stack and avoids routing synchronous WriteEntry calls through ArrayPool, Memory, generic adapters, and an async ValueTask state machine. The adapter-based asynchronous write path remains unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 00ba5cd3-e0c2-4b68-83b7-4e2d099813bd --- .../src/System/Formats/Tar/TarHeader.Write.cs | 178 ++++++++++++++++++ .../src/System/Formats/Tar/TarWriter.cs | 43 ++++- 2 files changed, 217 insertions(+), 4 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 d6d435b1f491b4..3c065f97020934 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 @@ -33,6 +33,21 @@ internal sealed partial class TarHeader private const int RootUidGid = 0; private const string RootUNameGName = "root"; + private void WriteWithSeekableDataStream(TarEntryFormat format, Stream archiveStream, Span buffer) + { + Debug.Assert(format is > TarEntryFormat.Unknown and <= TarEntryFormat.Gnu); + Debug.Assert(_dataStream is null || _dataStream.CanSeek); + + _size = GetTotalDataBytesToWrite(); + WriteFieldsToBuffer(format, buffer); + archiveStream.Write(buffer); + + if (_dataStream is not null) + { + WriteData(archiveStream, _dataStream); + } + } + // Writes the entry in the order required to be able to obtain the seekable data stream size. private async ValueTask WriteWithSeekableDataStreamCoreAsync(TarEntryFormat format, Stream archiveStream, Memory buffer, CancellationToken cancellationToken) where TAdapter : IReadWriteAdapter @@ -50,6 +65,44 @@ private async ValueTask WriteWithSeekableDataStreamCoreAsync(TarEntryF } } + private void WriteWithUnseekableDataStream(TarEntryFormat format, Stream destinationStream, Span buffer, bool shouldAdvanceToEnd) + { + Debug.Assert(destinationStream.CanSeek); + Debug.Assert(_dataStream is not null); + Debug.Assert(!_dataStream.CanSeek); + + 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)) + }; + + long dataStartPosition = headerStartPosition + dataLocation; + _dataOffset = dataStartPosition; + + destinationStream.Seek(dataLocation, SeekOrigin.Current); + _dataStream.CopyTo(destinationStream); + + long dataEndPosition = destinationStream.Position; + _size = dataEndPosition - dataStartPosition; + WriteEmptyPadding(destinationStream); + + long endOfHeaderPosition = destinationStream.Position; + destinationStream.Position = headerStartPosition; + + WriteFieldsToBuffer(format, buffer); + destinationStream.Write(buffer); + + if (shouldAdvanceToEnd) + { + destinationStream.Position = endOfHeaderPosition; + } + } + // Writes into the specified destination stream the entry in the order required to be able to obtain the unseekable data stream size. private async ValueTask WriteWithUnseekableDataStreamCoreAsync(TarEntryFormat format, Stream destinationStream, Memory buffer, bool shouldAdvanceToEnd, CancellationToken cancellationToken) where TAdapter : IReadWriteAdapter @@ -127,6 +180,12 @@ 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) + { + VerifyGlobalExtendedAttributesDataIsValid(globalExtendedAttributesEntryNumber); + WriteAsPaxExtendedAttributes(archiveStream, buffer, ExtendedAttributes, isGea: true, globalExtendedAttributesEntryNumber); + } + internal ValueTask WriteAsPaxGlobalExtendedAttributesCoreAsync(Stream archiveStream, Memory buffer, int globalExtendedAttributesEntryNumber, CancellationToken cancellationToken) where TAdapter : IReadWriteAdapter { @@ -141,6 +200,20 @@ private void VerifyGlobalExtendedAttributesDataIsValid(int globalExtendedAttribu Debug.Assert(globalExtendedAttributesEntryNumber >= 0); } + internal void WriteAsV7(Stream archiveStream, Span buffer) + { + Debug.Assert(archiveStream.CanSeek || _dataStream is null || _dataStream.CanSeek); + + if (archiveStream.CanSeek && _dataStream is { CanSeek: false }) + { + WriteWithUnseekableDataStream(TarEntryFormat.V7, archiveStream, buffer, shouldAdvanceToEnd: true); + } + else + { + WriteWithSeekableDataStream(TarEntryFormat.V7, archiveStream, buffer); + } + } + internal ValueTask WriteAsV7CoreAsync(Stream archiveStream, Memory buffer, CancellationToken cancellationToken) where TAdapter : IReadWriteAdapter { @@ -155,6 +228,20 @@ internal ValueTask WriteAsV7CoreAsync(Stream archiveStream, Memory(TarEntryFormat.V7, archiveStream, buffer, cancellationToken); } + internal void WriteAsUstar(Stream archiveStream, Span buffer) + { + Debug.Assert(archiveStream.CanSeek || _dataStream is null || _dataStream.CanSeek); + + if (archiveStream.CanSeek && _dataStream is { CanSeek: false }) + { + WriteWithUnseekableDataStream(TarEntryFormat.Ustar, archiveStream, buffer, shouldAdvanceToEnd: true); + } + else + { + WriteWithSeekableDataStream(TarEntryFormat.Ustar, archiveStream, buffer); + } + } + internal ValueTask WriteAsUstarCoreAsync(Stream archiveStream, Memory buffer, CancellationToken cancellationToken) where TAdapter : IReadWriteAdapter { @@ -171,6 +258,37 @@ internal ValueTask WriteAsUstarCoreAsync(Stream archiveStream, Memory< // 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) + { + Debug.Assert(archiveStream.CanSeek || _dataStream is null || _dataStream.CanSeek); + Debug.Assert(_typeFlag is not TarEntryType.GlobalExtendedAttributes); + + TarHeader extendedAttributesHeader = new(TarEntryFormat.Pax); + + if (archiveStream.CanSeek && _dataStream is { CanSeek: false }) + { + using MemoryStream tempStream = new(); + WriteWithUnseekableDataStream(TarEntryFormat.Pax, tempStream, buffer, shouldAdvanceToEnd: false); + tempStream.Position = 0; + buffer.Clear(); + + CollectExtendedAttributesFromStandardFieldsIfNeeded(); + extendedAttributesHeader.WriteAsPaxExtendedAttributes(archiveStream, buffer, ExtendedAttributes, isGea: false, globalExtendedAttributesEntryNumber: -1); + buffer.Clear(); + + tempStream.CopyTo(archiveStream); + } + else + { + _size = GetTotalDataBytesToWrite(); + CollectExtendedAttributesFromStandardFieldsIfNeeded(); + extendedAttributesHeader.WriteAsPaxExtendedAttributes(archiveStream, buffer, ExtendedAttributes, isGea: false, globalExtendedAttributesEntryNumber: -1); + buffer.Clear(); + + WriteWithSeekableDataStream(TarEntryFormat.Pax, archiveStream, buffer); + } + } + internal async ValueTask WriteAsPaxCoreAsync(Stream archiveStream, Memory buffer, CancellationToken cancellationToken) where TAdapter : IReadWriteAdapter { @@ -222,6 +340,36 @@ internal async ValueTask WriteAsPaxCoreAsync(Stream archiveStream, Mem // 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 is null || _dataStream.CanSeek); + + if (IsLinkNameTooLongForRegularField()) + { + TarHeader longLinkHeader = GetGnuLongLinkMetadataHeader(); + Debug.Assert(longLinkHeader._dataStream is not null && longLinkHeader._dataStream.CanSeek); + longLinkHeader.WriteWithSeekableDataStream(TarEntryFormat.Gnu, archiveStream, buffer); + buffer.Clear(); + } + + if (IsNameTooLongForRegularField()) + { + TarHeader longPathHeader = GetGnuLongPathMetadataHeader(); + Debug.Assert(longPathHeader._dataStream is not null && longPathHeader._dataStream.CanSeek); + longPathHeader.WriteWithSeekableDataStream(TarEntryFormat.Gnu, archiveStream, buffer); + buffer.Clear(); + } + + if (archiveStream.CanSeek && _dataStream is { CanSeek: false }) + { + WriteWithUnseekableDataStream(TarEntryFormat.Gnu, archiveStream, buffer, shouldAdvanceToEnd: true); + } + else + { + WriteWithSeekableDataStream(TarEntryFormat.Gnu, archiveStream, buffer); + } + } + internal async ValueTask WriteAsGnuCoreAsync(Stream archiveStream, Memory buffer, CancellationToken cancellationToken) where TAdapter : IReadWriteAdapter { @@ -310,6 +458,13 @@ 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) + { + WriteAsPaxExtendedAttributesShared(isGea, globalExtendedAttributesEntryNumber, extendedAttributes); + Debug.Assert(_dataStream is null || (extendedAttributes.Count > 0 && _dataStream.CanSeek)); + WriteWithSeekableDataStream(TarEntryFormat.Pax, archiveStream, buffer); + } + private ValueTask WriteAsPaxExtendedAttributesCoreAsync(Stream archiveStream, Memory buffer, Dictionary extendedAttributes, bool isGea, int globalExtendedAttributesEntryNumber, CancellationToken cancellationToken) where TAdapter : IReadWriteAdapter { @@ -607,6 +762,14 @@ private int WriteGnuFields(Span buffer) return checksum; } + private void WriteData(Stream archiveStream, Stream dataStream) + { + SetDataOffset(this, archiveStream); + + dataStream.CopyTo(archiveStream); + WriteEmptyPadding(archiveStream); + } + // Writes the current header's data stream into the archive stream. private async ValueTask WriteDataCoreAsync(Stream archiveStream, Stream dataStream, CancellationToken cancellationToken) where TAdapter : IReadWriteAdapter @@ -618,6 +781,21 @@ private async ValueTask WriteDataCoreAsync(Stream archiveStream, Strea await WriteEmptyPaddingCoreAsync(archiveStream, cancellationToken).ConfigureAwait(false); } + private void WriteEmptyPadding(Stream archiveStream) + { + 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 writes it after the data. private async ValueTask WriteEmptyPaddingCoreAsync(Stream archiveStream, CancellationToken cancellationToken) where TAdapter : IReadWriteAdapter 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 72b9f3b9d49d73..d4bd246ee17ed1 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 @@ -247,10 +247,7 @@ public void WriteEntry(TarEntry entry) ArgumentNullException.ThrowIfNull(entry); ValidateEntryLinkName(entry._header._typeFlag, entry._header._linkName); ValidateStreamsSeekability(entry); - - ValueTask vt = WriteEntryCoreAsync(entry, CancellationToken.None); - Debug.Assert(vt.IsCompleted, "Synchronous WriteEntry completed asynchronously."); - vt.GetAwaiter().GetResult(); + WriteEntryInternal(entry); } /// @@ -302,6 +299,44 @@ public Task WriteEntryAsync(TarEntry entry, CancellationToken cancellationToken return WriteEntryCoreAsync(entry, cancellationToken).AsTask(); } + private 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; + } + // 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 From 22612ae8d1841196ca84926a58bcb0cda09579cc Mon Sep 17 00:00:00 2001 From: Irem Yuksel <113098562+iremyux@users.noreply.github.com> Date: Fri, 7 Aug 2026 14:19:22 +0200 Subject: [PATCH 2/4] Use entry.Format Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../System.Formats.Tar/src/System/Formats/Tar/TarWriter.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 d4bd246ee17ed1..d97674cf306ecc 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 @@ -331,8 +331,7 @@ private void WriteEntryInternal(TarEntry entry) default: Debug.Assert(entry.Format == TarEntryFormat.Unknown, "Missing format handler"); - throw new InvalidDataException(SR.Format(SR.TarInvalidFormat, Format)); - } + throw new InvalidDataException(SR.Format(SR.TarInvalidFormat, entry.Format)); _wroteEntries = true; } From fb419f146f72012e31622268bf98ce0242fd06ec Mon Sep 17 00:00:00 2001 From: iremyux Date: Fri, 7 Aug 2026 17:27:19 +0200 Subject: [PATCH 3/4] Missing } --- .../System.Formats.Tar/src/System/Formats/Tar/TarWriter.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 d97674cf306ecc..72bef6f5af46ac 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 @@ -332,7 +332,8 @@ private void WriteEntryInternal(TarEntry entry) default: Debug.Assert(entry.Format == TarEntryFormat.Unknown, "Missing format handler"); throw new InvalidDataException(SR.Format(SR.TarInvalidFormat, entry.Format)); - + } + _wroteEntries = true; } From c448d4616552ea5e7947f0e92a45d0ab8162664c Mon Sep 17 00:00:00 2001 From: iremyux Date: Fri, 7 Aug 2026 17:29:25 +0200 Subject: [PATCH 4/4] Match WriteEntryCoreAsync method --- .../System.Formats.Tar/src/System/Formats/Tar/TarWriter.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 72bef6f5af46ac..d4bd246ee17ed1 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 @@ -331,9 +331,9 @@ private void WriteEntryInternal(TarEntry entry) default: Debug.Assert(entry.Format == TarEntryFormat.Unknown, "Missing format handler"); - throw new InvalidDataException(SR.Format(SR.TarInvalidFormat, entry.Format)); + throw new InvalidDataException(SR.Format(SR.TarInvalidFormat, Format)); } - + _wroteEntries = true; }