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 5a6645d551a2fa..16df825fa04515 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