diff --git a/src/libraries/System.Formats.Tar/src/Resources/Strings.resx b/src/libraries/System.Formats.Tar/src/Resources/Strings.resx
index 0ece0c6c56152..1c5a56c4da149 100644
--- a/src/libraries/System.Formats.Tar/src/Resources/Strings.resx
+++ b/src/libraries/System.Formats.Tar/src/Resources/Strings.resx
@@ -1,64 +1,5 @@
-
@@ -270,4 +211,7 @@
The value of the extended attribute key '{0}' contains a disallowed '{1}' character.
-
\ No newline at end of file
+
+ Cannot write the unseekable data stream of entry '{0}' into an unseekable archive stream.
+
+
diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/FieldLocations.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/FieldLocations.cs
index 90856330aa2ef..adc9819b2cab0 100644
--- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/FieldLocations.cs
+++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/FieldLocations.cs
@@ -49,5 +49,9 @@ internal static class FieldLocations
internal const ushort V7Padding = LinkName + FieldLengths.LinkName;
internal const ushort PosixPadding = Prefix + FieldLengths.Prefix;
internal const ushort GnuPadding = RealSize + FieldLengths.RealSize;
+
+ internal const ushort V7Data = V7Padding + FieldLengths.V7Padding;
+ internal const ushort PosixData = PosixPadding + FieldLengths.PosixPadding;
+ internal const ushort GnuData = GnuPadding + FieldLengths.GnuPadding;
}
}
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 41be887ed3891..5d9d3a5443190 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
@@ -25,83 +25,163 @@ internal sealed partial class TarHeader
private const string GnuLongMetadataName = "././@LongLink";
private const string ArgNameEntry = "entry";
- // Writes the current header as a V7 entry into the archive stream.
- internal void WriteAsV7(Stream archiveStream, Span buffer)
+ internal void WriteAs(TarEntryFormat format, Stream archiveStream, Span buffer)
{
- WriteV7FieldsToBuffer(buffer);
+ Debug.Assert(format > TarEntryFormat.Unknown && format <= TarEntryFormat.Gnu);
+ Debug.Assert(archiveStream.CanSeek || _dataStream == null || _dataStream.CanSeek);
- archiveStream.Write(buffer);
-
- if (_dataStream != null)
+ if (archiveStream.CanSeek && _dataStream is { CanSeek: false })
+ {
+ WriteWithUnseekableDataStreamAs(format, archiveStream, buffer);
+ }
+ else // Seek status of archive does not matter
{
- WriteData(archiveStream, _dataStream, _size);
+ long bytesToWrite = GetTotalDataBytesToWrite();
+ WriteFieldsToBuffer(format, bytesToWrite, buffer);
+ archiveStream.Write(buffer);
+
+ if (_dataStream != null)
+ {
+ WriteData(archiveStream, _dataStream, _size);
+ }
}
}
- // Asynchronously writes the current header as a V7 entry into the archive stream and returns the value of the final checksum.
- internal async Task WriteAsV7Async(Stream archiveStream, Memory buffer, CancellationToken cancellationToken)
+ internal async Task WriteAsAsync(TarEntryFormat format, Stream archiveStream, Memory buffer, CancellationToken cancellationToken)
{
- cancellationToken.ThrowIfCancellationRequested();
+ Debug.Assert(format > TarEntryFormat.Unknown && format <= TarEntryFormat.Gnu);
+ Debug.Assert(archiveStream.CanSeek || _dataStream == null || _dataStream.CanSeek);
- WriteV7FieldsToBuffer(buffer.Span);
-
- await archiveStream.WriteAsync(buffer, cancellationToken).ConfigureAwait(false);
-
- if (_dataStream != null)
+ if (archiveStream.CanSeek && _dataStream is { CanSeek: false })
{
- await WriteDataAsync(archiveStream, _dataStream, _size, cancellationToken).ConfigureAwait(false);
+ await WriteWithUnseekableDataStreamAsAsync(format, archiveStream, buffer, cancellationToken).ConfigureAwait(false);
+ }
+ else // seek status of archive does not matter
+ {
+ long bytesToWrite = GetTotalDataBytesToWrite();
+ WriteFieldsToBuffer(format, bytesToWrite, buffer.Span);
+ await archiveStream.WriteAsync(buffer, cancellationToken).ConfigureAwait(false);
+
+ if (_dataStream != null)
+ {
+ await WriteDataAsync(archiveStream, _dataStream, _size, cancellationToken).ConfigureAwait(false);
+ }
}
}
- // Writes the V7 header fields to the specified buffer, calculates and writes the checksum, then returns the final data length.
- private void WriteV7FieldsToBuffer(Span buffer)
+ private void WriteWithUnseekableDataStreamAs(TarEntryFormat format, Stream archiveStream, Span buffer)
{
- _size = GetTotalDataBytesToWrite();
- TarEntryType actualEntryType = TarHelpers.GetCorrectTypeFlagForFormat(TarEntryFormat.V7, _typeFlag);
+ // When the data stream is unseekable, the order in which we write the entry data changes
+ Debug.Assert(archiveStream.CanSeek);
+ Debug.Assert(_dataStream != null);
+ Debug.Assert(!_dataStream.CanSeek);
- int tmpChecksum = WriteName(buffer);
- tmpChecksum += WriteCommonFields(buffer, actualEntryType);
- _checksum = WriteChecksum(tmpChecksum, buffer);
- }
+ // Store the start of the current entry's header, it'll be used later
+ long headerStartPosition = archiveStream.Position;
- // Writes the current header as a Ustar entry into the archive stream.
- internal void WriteAsUstar(Stream archiveStream, Span buffer)
- {
- WriteUstarFieldsToBuffer(buffer);
+ 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;
+
+ // Move to the data start location and write the data
+ archiveStream.Seek(dataLocation, SeekOrigin.Current);
+ _dataStream.CopyTo(archiveStream); // 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 = archiveStream.Position;
+ long actualLength = dataEndPosition - dataStartPosition;
+
+ // Write the padding now so that we can go back to writing the entry's header metadata
+ WriteEmptyPadding(archiveStream, actualLength);
+
+ // Store the end of the current header, we will write the next one after this position
+ long endOfHeaderPosition = archiveStream.Position;
+ // Go back to the start of the entry header to write the rest of the fields
+ archiveStream.Position = headerStartPosition;
+
+ WriteFieldsToBuffer(format, actualLength, buffer);
archiveStream.Write(buffer);
- if (_dataStream != null)
- {
- WriteData(archiveStream, _dataStream, _size);
- }
+ // Finally, move to the end of the header to continue with the next entry
+ archiveStream.Position = endOfHeaderPosition;
}
- // Asynchronously rites the current header as a Ustar entry into the archive stream and returns the value of the final checksum.
- internal async Task WriteAsUstarAsync(Stream archiveStream, Memory buffer, CancellationToken cancellationToken)
+ // Asynchronously writes the entry in the order required to be able to obtain the unseekable data stream size.
+ private async Task WriteWithUnseekableDataStreamAsAsync(TarEntryFormat format, Stream archiveStream, Memory buffer, CancellationToken cancellationToken)
{
- cancellationToken.ThrowIfCancellationRequested();
+ // When the data stream is unseekable, the order in which we write the entry data changes
+ Debug.Assert(archiveStream.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 = archiveStream.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))
+ };
- WriteUstarFieldsToBuffer(buffer.Span);
+ // We know the exact location where the data starts depending on the format
+ long dataStartPosition = headerStartPosition + dataLocation;
+ // Move to the data start location and write the data
+ archiveStream.Seek(dataLocation, SeekOrigin.Current);
+ await _dataStream.CopyToAsync(archiveStream, 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 = archiveStream.Position;
+ long actualLength = dataEndPosition - dataStartPosition;
+
+ // Write the padding now so that we can go back to writing the entry's header metadata
+ await WriteEmptyPaddingAsync(archiveStream, actualLength, cancellationToken).ConfigureAwait(false);
+
+ // Store the end of the current header, we will write the next one after this position
+ long endOfHeaderPosition = archiveStream.Position;
+
+ // Go back to the start of the entry header to write the rest of the fields
+ archiveStream.Position = headerStartPosition;
+
+ WriteFieldsToBuffer(format, actualLength, buffer.Span);
await archiveStream.WriteAsync(buffer, cancellationToken).ConfigureAwait(false);
- if (_dataStream != null)
- {
- await WriteDataAsync(archiveStream, _dataStream, _size, cancellationToken).ConfigureAwait(false);
- }
+ // Finally, move to the end of the header to continue with the next entry
+ archiveStream.Position = endOfHeaderPosition;
+ }
+
+ // Writes the V7 header fields to the specified buffer, calculates and writes the checksum, then returns the final data length.
+ private void WriteV7FieldsToBuffer(long size, Span buffer)
+ {
+ _size = size;
+ TarEntryType actualEntryType = TarHelpers.GetCorrectTypeFlagForFormat(TarEntryFormat.V7, _typeFlag);
+
+ int tmpChecksum = WriteName(buffer);
+ tmpChecksum += WriteCommonFields(buffer, actualEntryType);
+ _checksum = WriteChecksum(tmpChecksum, buffer);
}
// Writes the Ustar header fields to the specified buffer, calculates and writes the checksum, then returns the final data length.
- private void WriteUstarFieldsToBuffer(Span buffer)
+ private void WriteUstarFieldsToBuffer(long size, Span buffer)
{
- _size = GetTotalDataBytesToWrite();
+ _size = size;
TarEntryType actualEntryType = TarHelpers.GetCorrectTypeFlagForFormat(TarEntryFormat.Ustar, _typeFlag);
int tmpChecksum = WriteUstarName(buffer);
tmpChecksum += WriteCommonFields(buffer, actualEntryType);
tmpChecksum += WritePosixMagicAndVersion(buffer);
tmpChecksum += WritePosixAndGnuSharedFields(buffer);
+
_checksum = WriteChecksum(tmpChecksum, buffer);
}
@@ -140,13 +220,12 @@ internal void WriteAsPax(Stream archiveStream, Span buffer)
// First, we write the preceding extended attributes header
TarHeader extendedAttributesHeader = new(TarEntryFormat.Pax);
// Fill the current header's dict
- _size = GetTotalDataBytesToWrite();
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
- WriteAsPaxInternal(archiveStream, buffer);
+ WriteAs(TarEntryFormat.Pax, archiveStream, buffer);
}
// Asynchronously writes the current header as a PAX entry into the archive stream.
@@ -159,14 +238,13 @@ internal async Task WriteAsPaxAsync(Stream archiveStream, Memory buffer, C
// First, we write the preceding extended attributes header
TarHeader extendedAttributesHeader = new(TarEntryFormat.Pax);
// Fill the current header's dict
- _size = GetTotalDataBytesToWrite();
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);
buffer.Span.Clear(); // Reset it to reuse it
// Second, we write this header as a normal one
- await WriteAsPaxInternalAsync(archiveStream, buffer, cancellationToken).ConfigureAwait(false);
+ await WriteAsAsync(TarEntryFormat.Pax, archiveStream, buffer, cancellationToken).ConfigureAwait(false);
}
// Writes the current header as a Gnu entry into the archive stream.
@@ -177,7 +255,7 @@ internal void WriteAsGnu(Stream archiveStream, Span buffer)
if (_linkName != null && Encoding.UTF8.GetByteCount(_linkName) > FieldLengths.LinkName)
{
TarHeader longLinkHeader = GetGnuLongMetadataHeader(TarEntryType.LongLink, _linkName);
- longLinkHeader.WriteAsGnuInternal(archiveStream, buffer);
+ longLinkHeader.WriteAs(TarEntryFormat.Gnu, archiveStream, buffer);
buffer.Clear(); // Reset it to reuse it
}
@@ -185,12 +263,12 @@ internal void WriteAsGnu(Stream archiveStream, Span buffer)
if (Encoding.UTF8.GetByteCount(_name) > FieldLengths.Name)
{
TarHeader longPathHeader = GetGnuLongMetadataHeader(TarEntryType.LongPath, _name);
- longPathHeader.WriteAsGnuInternal(archiveStream, buffer);
+ longPathHeader.WriteAs(TarEntryFormat.Gnu, archiveStream, buffer);
buffer.Clear(); // Reset it to reuse it
}
// Third, we write this header as a normal one
- WriteAsGnuInternal(archiveStream, buffer);
+ WriteAs(TarEntryFormat.Gnu, archiveStream, buffer);
}
// Writes the current header as a Gnu entry into the archive stream.
@@ -203,7 +281,7 @@ internal async Task WriteAsGnuAsync(Stream archiveStream, Memory buffer, C
if (_linkName != null && Encoding.UTF8.GetByteCount(_linkName) > FieldLengths.LinkName)
{
TarHeader longLinkHeader = GetGnuLongMetadataHeader(TarEntryType.LongLink, _linkName);
- await longLinkHeader.WriteAsGnuInternalAsync(archiveStream, buffer, cancellationToken).ConfigureAwait(false);
+ await longLinkHeader.WriteAsAsync(TarEntryFormat.Gnu, archiveStream, buffer, cancellationToken).ConfigureAwait(false);
buffer.Span.Clear(); // Reset it to reuse it
}
@@ -211,12 +289,12 @@ internal async Task WriteAsGnuAsync(Stream archiveStream, Memory buffer, C
if (Encoding.UTF8.GetByteCount(_name) > FieldLengths.Name)
{
TarHeader longPathHeader = GetGnuLongMetadataHeader(TarEntryType.LongPath, _name);
- await longPathHeader.WriteAsGnuInternalAsync(archiveStream, buffer, cancellationToken).ConfigureAwait(false);
+ await longPathHeader.WriteAsAsync(TarEntryFormat.Gnu, archiveStream, buffer, cancellationToken).ConfigureAwait(false);
buffer.Span.Clear(); // Reset it to reuse it
}
// Third, we write this header as a normal one
- await WriteAsGnuInternalAsync(archiveStream, buffer, cancellationToken).ConfigureAwait(false);
+ await WriteAsAsync(TarEntryFormat.Gnu, archiveStream, buffer, cancellationToken).ConfigureAwait(false);
}
// Creates and returns a GNU long metadata header, with the specified long text written into its data stream.
@@ -237,38 +315,10 @@ private static TarHeader GetGnuLongMetadataHeader(TarEntryType entryType, string
return longMetadataHeader;
}
- // Writes the current header as a GNU entry into the archive stream.
- internal void WriteAsGnuInternal(Stream archiveStream, Span buffer)
- {
- WriteAsGnuSharedInternal(buffer);
-
- archiveStream.Write(buffer);
-
- if (_dataStream != null)
- {
- WriteData(archiveStream, _dataStream, _size);
- }
- }
-
- // Asynchronously writes the current header as a GNU entry into the archive stream.
- internal async Task WriteAsGnuInternalAsync(Stream archiveStream, Memory buffer, CancellationToken cancellationToken)
- {
- cancellationToken.ThrowIfCancellationRequested();
-
- WriteAsGnuSharedInternal(buffer.Span);
-
- await archiveStream.WriteAsync(buffer, cancellationToken).ConfigureAwait(false);
-
- if (_dataStream != null)
- {
- await WriteDataAsync(archiveStream, _dataStream, _size, cancellationToken).ConfigureAwait(false);
- }
- }
-
// Shared checksum and data length calculations for GNU entry writing.
- private void WriteAsGnuSharedInternal(Span buffer)
+ private void WriteGnuFieldsToBuffer(long size, Span buffer)
{
- _size = GetTotalDataBytesToWrite();
+ _size = size;
int tmpChecksum = WriteName(buffer);
tmpChecksum += WriteCommonFields(buffer, TarHelpers.GetCorrectTypeFlagForFormat(TarEntryFormat.Gnu, _typeFlag));
@@ -283,7 +333,7 @@ private void WriteAsGnuSharedInternal(Span buffer)
private void WriteAsPaxExtendedAttributes(Stream archiveStream, Span buffer, Dictionary extendedAttributes, bool isGea, int globalExtendedAttributesEntryNumber)
{
WriteAsPaxExtendedAttributesShared(isGea, globalExtendedAttributesEntryNumber, extendedAttributes);
- WriteAsPaxInternal(archiveStream, buffer);
+ WriteAs(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.
@@ -291,7 +341,7 @@ private Task WriteAsPaxExtendedAttributesAsync(Stream archiveStream, Memory buffer)
- {
- WriteAsPaxSharedInternal(buffer);
-
- archiveStream.Write(buffer);
-
- if (_dataStream != null)
- {
- WriteData(archiveStream, _dataStream, _size);
- }
- }
-
- // Both the Extended Attributes and Global Extended Attributes entry headers are written in a similar way, just the data changes
- // This method asynchronously writes an entry as both entries require, using the data from the current header instance.
- private async Task WriteAsPaxInternalAsync(Stream archiveStream, Memory buffer, CancellationToken cancellationToken)
- {
- cancellationToken.ThrowIfCancellationRequested();
-
- WriteAsPaxSharedInternal(buffer.Span);
-
- await archiveStream.WriteAsync(buffer, cancellationToken).ConfigureAwait(false);
-
- if (_dataStream != null)
- {
- await WriteDataAsync(archiveStream, _dataStream, _size, cancellationToken).ConfigureAwait(false);
- }
- }
-
// Shared checksum and data length calculations for PAX entry writing.
- private void WriteAsPaxSharedInternal(Span buffer)
+ private void WritePaxFieldsToBuffer(long size, Span buffer)
{
+ _size = size;
int tmpChecksum = WriteName(buffer);
tmpChecksum += WriteCommonFields(buffer, TarHelpers.GetCorrectTypeFlagForFormat(TarEntryFormat.Pax, _typeFlag));
tmpChecksum += WritePosixMagicAndVersion(buffer);
@@ -350,6 +370,26 @@ private void WriteAsPaxSharedInternal(Span buffer)
_checksum = WriteChecksum(tmpChecksum, buffer);
}
+ // Writes the format-specific fields of the current entry, as well as the entry data length, into the specified buffer.
+ private void WriteFieldsToBuffer(TarEntryFormat format, long bytesToWrite, Span buffer)
+ {
+ switch (format)
+ {
+ case TarEntryFormat.V7:
+ WriteV7FieldsToBuffer(bytesToWrite, buffer);
+ break;
+ case TarEntryFormat.Ustar:
+ WriteUstarFieldsToBuffer(bytesToWrite, buffer);
+ break;
+ case TarEntryFormat.Pax:
+ WritePaxFieldsToBuffer(bytesToWrite, buffer);
+ break;
+ case TarEntryFormat.Gnu:
+ WriteGnuFieldsToBuffer(bytesToWrite, buffer);
+ break;
+ }
+ }
+
// Gnu and pax save in the name byte array only the UTF8 bytes that fit.
// V7 does not support more than 100 bytes so it throws.
private int WriteName(Span buffer)
@@ -507,18 +547,18 @@ private int WriteCommonFields(Span buffer, TarEntryType actualEntryType)
}
// Calculates how many data bytes should be written, depending on the position pointer of the stream.
+ // Only works if the stream is seekable.
private long GetTotalDataBytesToWrite()
{
- if (_dataStream != null)
+ if (_dataStream == null)
{
- long length = _dataStream.Length;
- long position = _dataStream.Position;
- if (position < length)
- {
- return length - position;
- }
+ return 0;
}
- return 0;
+
+ long length = _dataStream.Length;
+ long position = _dataStream.Position;
+
+ return position < length ? length - position : 0;
}
// Writes the magic and version fields of a ustar or pax entry into the specified spans.
@@ -609,18 +649,38 @@ private int WriteGnuFields(Span buffer)
private static void WriteData(Stream archiveStream, Stream dataStream, long actualLength)
{
dataStream.CopyTo(archiveStream); // The data gets copied from the current position
+ WriteEmptyPadding(archiveStream, actualLength);
+ }
+ // Calculates the padding for the current entry and writes it after the data.
+ private static void WriteEmptyPadding(Stream archiveStream, long actualLength)
+ {
int paddingAfterData = TarHelpers.CalculatePadding(actualLength);
if (paddingAfterData != 0)
{
Debug.Assert(paddingAfterData <= TarHelpers.RecordSize);
- Span padding = stackalloc byte[TarHelpers.RecordSize];
- padding = padding.Slice(0, paddingAfterData);
- padding.Clear();
+ 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 static ValueTask WriteEmptyPaddingAsync(Stream archiveStream, long actualLength, CancellationToken cancellationToken)
+ {
+ int paddingAfterData = TarHelpers.CalculatePadding(actualLength);
+ if (paddingAfterData != 0)
+ {
+ Debug.Assert(paddingAfterData <= TarHelpers.RecordSize);
- archiveStream.Write(padding);
+ 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.
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 c4ec17272a8b8..13654eaa7279e 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
@@ -222,6 +222,7 @@ public void WriteEntry(TarEntry entry)
ObjectDisposedException.ThrowIf(_isDisposed, this);
ArgumentNullException.ThrowIfNull(entry);
ValidateEntryLinkName(entry._header._typeFlag, entry._header._linkName);
+ ValidateStreamsSeekability(entry);
WriteEntryInternal(entry);
}
@@ -270,6 +271,7 @@ public Task WriteEntryAsync(TarEntry entry, CancellationToken cancellationToken
ObjectDisposedException.ThrowIf(_isDisposed, this);
ArgumentNullException.ThrowIfNull(entry);
ValidateEntryLinkName(entry._header._typeFlag, entry._header._linkName);
+ ValidateStreamsSeekability(entry);
return WriteEntryAsyncInternal(entry, cancellationToken);
}
@@ -281,12 +283,8 @@ private void WriteEntryInternal(TarEntry entry)
switch (entry.Format)
{
- case TarEntryFormat.V7:
- entry._header.WriteAsV7(_archiveStream, buffer);
- break;
-
- case TarEntryFormat.Ustar:
- entry._header.WriteAsUstar(_archiveStream, buffer);
+ case TarEntryFormat.V7 or TarEntryFormat.Ustar:
+ entry._header.WriteAs(entry.Format, _archiveStream, buffer);
break;
case TarEntryFormat.Pax:
@@ -323,8 +321,7 @@ private async Task WriteEntryAsyncInternal(TarEntry entry, CancellationToken can
Task task = entry.Format switch
{
- TarEntryFormat.V7 => entry._header.WriteAsV7Async(_archiveStream, buffer, cancellationToken),
- TarEntryFormat.Ustar => entry._header.WriteAsUstarAsync(_archiveStream, buffer, cancellationToken),
+ TarEntryFormat.V7 or TarEntryFormat.Ustar => entry._header.WriteAsAsync(entry.Format, _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),
@@ -374,6 +371,14 @@ private async ValueTask WriteFinalRecordsAsync()
return (fullPath, actualEntryName);
}
+ private void ValidateStreamsSeekability(TarEntry entry)
+ {
+ if (!_archiveStream.CanSeek && entry._header._dataStream != null && !entry._header._dataStream.CanSeek)
+ {
+ throw new IOException(SR.Format(SR.TarStreamSeekabilityUnsupportedCombination, entry.Name));
+ }
+ }
+
private static void ValidateEntryLinkName(TarEntryType entryType, string? linkName)
{
if (entryType is TarEntryType.HardLink or TarEntryType.SymbolicLink)
diff --git a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectory.Stream.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectory.Stream.Tests.cs
index ab90f0043bc7d..bf3cec4fcb083 100644
--- a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectory.Stream.Tests.cs
+++ b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectory.Stream.Tests.cs
@@ -1,9 +1,9 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
+using System.IO.Enumeration;
using System.Linq;
using Xunit;
@@ -204,5 +204,65 @@ public void PaxNameCollision_DedupInExtendedAttributes()
Assert.True(File.Exists(path1));
Assert.True(Path.Exists(path2));
}
+
+ [Theory]
+ [MemberData(nameof(GetTestTarFormats))]
+ public void UnseekableStreams_RoundTrip(TestTarFormat testFormat)
+ {
+ using TempDirectory root = new();
+
+ using MemoryStream sourceStream = GetTarMemoryStream(CompressionMethod.Uncompressed, testFormat, "many_small_files");
+ using WrappedStream sourceUnseekableArchiveStream = new(sourceStream, canRead: true, canWrite: false, canSeek: false);
+
+ TarFile.ExtractToDirectory(sourceUnseekableArchiveStream, root.Path, overwriteFiles: false);
+
+ using MemoryStream destinationStream = new();
+ using WrappedStream destinationUnseekableArchiveStream = new(destinationStream, canRead: true, canWrite: true, canSeek: false);
+ TarFile.CreateFromDirectory(root.Path, destinationUnseekableArchiveStream, includeBaseDirectory: false);
+
+ FileSystemEnumerable fileSystemEntries = new FileSystemEnumerable(
+ directory: root.Path,
+ transform: (ref FileSystemEntry entry) => entry.ToFileSystemInfo(),
+ options: new EnumerationOptions() { RecurseSubdirectories = true });
+
+ destinationStream.Position = 0;
+ using TarReader reader = new TarReader(destinationStream, leaveOpen: false);
+
+ // Size of files in many_small_files.tar are expected to be tiny and all equal
+ int bufferLength = 1024;
+ byte[] fileContent = new byte[bufferLength];
+ byte[] dataStreamContent = new byte[bufferLength];
+ TarEntry entry = reader.GetNextEntry();
+ do
+ {
+ Assert.NotNull(entry);
+ string entryPath = Path.TrimEndingDirectorySeparator(Path.GetFullPath(Path.Join(root.Path, entry.Name)));
+ FileSystemInfo fsi = fileSystemEntries.SingleOrDefault(file =>
+ file.FullName == entryPath);
+ Assert.NotNull(fsi);
+ if (entry.EntryType is TarEntryType.RegularFile or TarEntryType.V7RegularFile)
+ {
+ Assert.NotNull(entry.DataStream);
+
+ using Stream fileData = File.OpenRead(fsi.FullName);
+
+ // If the size of the files in manu_small_files.tar ever gets larger than bufferLength,
+ // these asserts should fail and the test will need to be updated
+ AssertExtensions.LessThanOrEqualTo(entry.Length, bufferLength);
+ AssertExtensions.LessThanOrEqualTo(fileData.Length, bufferLength);
+
+ Assert.Equal(fileData.Length, entry.Length);
+
+ Array.Clear(fileContent);
+ Array.Clear(dataStreamContent);
+
+ fileData.ReadExactly(fileContent, 0, (int)entry.Length);
+ entry.DataStream.ReadExactly(dataStreamContent, 0, (int)entry.Length);
+
+ AssertExtensions.SequenceEqual(fileContent, dataStreamContent);
+ }
+ }
+ while ((entry = reader.GetNextEntry()) != null);
+ }
}
}
diff --git a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectoryAsync.Stream.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectoryAsync.Stream.Tests.cs
index d7502d940e94e..307a165099f4f 100644
--- a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectoryAsync.Stream.Tests.cs
+++ b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectoryAsync.Stream.Tests.cs
@@ -1,9 +1,9 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
+using System.IO.Enumeration;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -269,5 +269,65 @@ public async Task PaxNameCollision_DedupInExtendedAttributesAsync()
Assert.True(File.Exists(path1));
Assert.True(Path.Exists(path2));
}
+
+ [Theory]
+ [MemberData(nameof(GetTestTarFormats))]
+ public async Task UnseekableStreams_RoundTrip_Async(TestTarFormat testFormat)
+ {
+ using TempDirectory root = new();
+
+ await using MemoryStream sourceStream = GetTarMemoryStream(CompressionMethod.Uncompressed, testFormat, "many_small_files");
+ await using WrappedStream sourceUnseekableArchiveStream = new(sourceStream, canRead: true, canWrite: false, canSeek: false);
+
+ await TarFile.ExtractToDirectoryAsync(sourceUnseekableArchiveStream, root.Path, overwriteFiles: false);
+
+ await using MemoryStream destinationStream = new();
+ await using WrappedStream destinationUnseekableArchiveStream = new(destinationStream, canRead: true, canWrite: true, canSeek: false);
+ await TarFile.CreateFromDirectoryAsync(root.Path, destinationUnseekableArchiveStream, includeBaseDirectory: false);
+
+ FileSystemEnumerable fileSystemEntries = new FileSystemEnumerable(
+ directory: root.Path,
+ transform: (ref FileSystemEntry entry) => entry.ToFileSystemInfo(),
+ options: new EnumerationOptions() { RecurseSubdirectories = true });
+
+ destinationStream.Position = 0;
+ await using TarReader reader = new TarReader(destinationStream, leaveOpen: false);
+
+ // Size of files in many_small_files.tar are expected to be tiny and all equal
+ int bufferLength = 1024;
+ byte[] fileContent = new byte[bufferLength];
+ byte[] dataStreamContent = new byte[bufferLength];
+ TarEntry entry = await reader.GetNextEntryAsync();
+ do
+ {
+ Assert.NotNull(entry);
+ string entryPath = Path.TrimEndingDirectorySeparator(Path.GetFullPath(Path.Join(root.Path, entry.Name)));
+ FileSystemInfo fsi = fileSystemEntries.SingleOrDefault(file =>
+ file.FullName == entryPath);
+ Assert.NotNull(fsi);
+ if (entry.EntryType is TarEntryType.RegularFile or TarEntryType.V7RegularFile)
+ {
+ Assert.NotNull(entry.DataStream);
+
+ await using Stream fileData = File.OpenRead(fsi.FullName);
+
+ // If the size of the files in manu_small_files.tar ever gets larger than bufferLength,
+ // these asserts should fail and the test will need to be updated
+ AssertExtensions.LessThanOrEqualTo(entry.Length, bufferLength);
+ AssertExtensions.LessThanOrEqualTo(fileData.Length, bufferLength);
+
+ Assert.Equal(fileData.Length, entry.Length);
+
+ Array.Clear(fileContent);
+ Array.Clear(dataStreamContent);
+
+ await fileData.ReadExactlyAsync(fileContent, 0, (int)entry.Length);
+ await entry.DataStream.ReadExactlyAsync(dataStreamContent, 0, (int)entry.Length);
+
+ AssertExtensions.SequenceEqual(fileContent, dataStreamContent);
+ }
+ }
+ while ((entry = await reader.GetNextEntryAsync()) != null);
+ }
}
}
diff --git a/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.GetNextEntry.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.GetNextEntry.Tests.cs
index 2cede3a350c82..ad87b69e08cef 100644
--- a/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.GetNextEntry.Tests.cs
+++ b/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.GetNextEntry.Tests.cs
@@ -161,13 +161,18 @@ public void GetNextEntry_CopyDataTrue_UnseekableArchive()
Assert.Throws(() => entry.DataStream.Read(new byte[1]));
}
- [Fact]
- public void GetNextEntry_CopyDataFalse_UnseekableArchive_Exceptions()
+ [Theory]
+ [InlineData(TarEntryFormat.V7)]
+ [InlineData(TarEntryFormat.Ustar)]
+ [InlineData(TarEntryFormat.Pax)]
+ [InlineData(TarEntryFormat.Gnu)]
+ public void GetNextEntry_CopyDataFalse_UnseekableArchive_Exceptions(TarEntryFormat format)
{
- MemoryStream archive = new MemoryStream();
- using (TarWriter writer = new TarWriter(archive, TarEntryFormat.Ustar, leaveOpen: true))
+ TarEntryType fileEntryType = GetTarEntryTypeForTarEntryFormat(TarEntryType.RegularFile, format);
+ using MemoryStream archive = new MemoryStream();
+ using (TarWriter writer = new TarWriter(archive, format, leaveOpen: true))
{
- UstarTarEntry entry1 = new UstarTarEntry(TarEntryType.RegularFile, "file.txt");
+ TarEntry entry1 = InvokeTarEntryCreationConstructor(format, fileEntryType, "file.txt");
entry1.DataStream = new MemoryStream();
using (StreamWriter streamWriter = new StreamWriter(entry1.DataStream, leaveOpen: true))
{
@@ -176,30 +181,34 @@ public void GetNextEntry_CopyDataFalse_UnseekableArchive_Exceptions()
entry1.DataStream.Seek(0, SeekOrigin.Begin); // Rewind to ensure it gets written from the beginning
writer.WriteEntry(entry1);
- UstarTarEntry entry2 = new UstarTarEntry(TarEntryType.Directory, "dir");
+ TarEntry entry2 = InvokeTarEntryCreationConstructor(format, TarEntryType.Directory, "dir");
writer.WriteEntry(entry2);
}
archive.Seek(0, SeekOrigin.Begin);
using WrappedStream wrapped = new WrappedStream(archive, canRead: true, canWrite: false, canSeek: false);
- UstarTarEntry entry;
+ TarEntry entry;
+ byte[] b = new byte[1];
using (TarReader reader = new TarReader(wrapped)) // Unseekable
{
- entry = reader.GetNextEntry(copyData: false) as UstarTarEntry;
+ entry = reader.GetNextEntry(copyData: false);
Assert.NotNull(entry);
- Assert.Equal(TarEntryType.RegularFile, entry.EntryType);
+ Assert.Equal(fileEntryType, entry.EntryType);
entry.DataStream.ReadByte(); // Reading is possible as long as we don't move to the next entry
// Attempting to read the next entry should automatically move the position pointer to the beginning of the next header
- Assert.NotNull(reader.GetNextEntry());
+ TarEntry entry2 = reader.GetNextEntry();
+ Assert.NotNull(entry2);
+ Assert.Equal(format, entry2.Format);
+ Assert.Equal(TarEntryType.Directory, entry2.EntryType);
Assert.Null(reader.GetNextEntry());
// This is not possible because the position of the main stream is already past the data
- Assert.Throws(() => entry.DataStream.Read(new byte[1]));
+ Assert.Throws(() => entry.DataStream.Read(b));
}
// The reader must stay alive because it's in charge of disposing all the entries it collected
- Assert.Throws(() => entry.DataStream.Read(new byte[1]));
+ Assert.Throws(() => entry.DataStream.Read(b));
}
[Theory]
diff --git a/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.GetNextEntryAsync.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.GetNextEntryAsync.Tests.cs
index f99e5853ebeaa..1c266ae633434 100644
--- a/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.GetNextEntryAsync.Tests.cs
+++ b/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.GetNextEntryAsync.Tests.cs
@@ -191,49 +191,56 @@ public async Task GetNextEntry_CopyDataTrue_UnseekableArchive_Async()
}
}
- [Fact]
- public async Task GetNextEntry_CopyDataFalse_UnseekableArchive_Exceptions_Async()
+ [Theory]
+ [InlineData(TarEntryFormat.V7)]
+ [InlineData(TarEntryFormat.Ustar)]
+ [InlineData(TarEntryFormat.Pax)]
+ [InlineData(TarEntryFormat.Gnu)]
+ public async Task GetNextEntry_CopyDataFalse_UnseekableArchive_Exceptions_Async(TarEntryFormat format)
{
- await using (MemoryStream archive = new MemoryStream())
+ TarEntryType fileEntryType = GetTarEntryTypeForTarEntryFormat(TarEntryType.RegularFile, format);
+ await using MemoryStream archive = new MemoryStream();
+ await using (TarWriter writer = new TarWriter(archive, format, leaveOpen: true))
{
- await using (TarWriter writer = new TarWriter(archive, TarEntryFormat.Ustar, leaveOpen: true))
+ TarEntry entry1 = InvokeTarEntryCreationConstructor(format, fileEntryType, "file.txt");
+ entry1.DataStream = new MemoryStream();
+ using (StreamWriter streamWriter = new StreamWriter(entry1.DataStream, leaveOpen: true))
{
- UstarTarEntry entry1 = new UstarTarEntry(TarEntryType.RegularFile, "file.txt");
- entry1.DataStream = new MemoryStream();
- using (StreamWriter streamWriter = new StreamWriter(entry1.DataStream, leaveOpen: true))
- {
- streamWriter.WriteLine("Hello world!");
- }
- entry1.DataStream.Seek(0, SeekOrigin.Begin); // Rewind to ensure it gets written from the beginning
- await writer.WriteEntryAsync(entry1);
-
- UstarTarEntry entry2 = new UstarTarEntry(TarEntryType.Directory, "dir");
- await writer.WriteEntryAsync(entry2);
+ streamWriter.WriteLine("Hello world!");
}
+ entry1.DataStream.Seek(0, SeekOrigin.Begin); // Rewind to ensure it gets written from the beginning
+ await writer.WriteEntryAsync(entry1);
- archive.Seek(0, SeekOrigin.Begin);
- await using (WrappedStream wrapped = new WrappedStream(archive, canRead: true, canWrite: false, canSeek: false))
- {
- UstarTarEntry entry;
- await using (TarReader reader = new TarReader(wrapped)) // Unseekable
- {
- entry = await reader.GetNextEntryAsync(copyData: false) as UstarTarEntry;
- Assert.NotNull(entry);
- Assert.Equal(TarEntryType.RegularFile, entry.EntryType);
- entry.DataStream.ReadByte(); // Reading is possible as long as we don't move to the next entry
-
- // Attempting to read the next entry should automatically move the position pointer to the beginning of the next header
- Assert.NotNull(await reader.GetNextEntryAsync());
- Assert.Null(await reader.GetNextEntryAsync());
+ TarEntry entry2 = InvokeTarEntryCreationConstructor(format, TarEntryType.Directory, "dir");
+ await writer.WriteEntryAsync(entry2);
+ }
- // This is not possible because the position of the main stream is already past the data
- Assert.Throws(() => entry.DataStream.Read(new byte[1]));
- }
+ archive.Seek(0, SeekOrigin.Begin);
+ await using WrappedStream wrapped = new WrappedStream(archive, canRead: true, canWrite: false, canSeek: false);
+ TarEntry entry;
+ byte[] b = new byte[1];
+ await using (TarReader reader = new TarReader(wrapped)) // Unseekable
+ {
+ entry = await reader.GetNextEntryAsync(copyData: false);
+ Assert.NotNull(entry);
+ Assert.Equal(format, entry.Format);
+ Assert.Equal(fileEntryType, entry.EntryType);
+ entry.DataStream.ReadByte(); // Reading is possible as long as we don't move to the next entry
+
+ // Attempting to read the next entries should automatically move the position pointer to the beginning of the next header
+ TarEntry entry2 = await reader.GetNextEntryAsync();
+ Assert.NotNull(entry2);
+ Assert.Equal(format, entry2.Format);
+ Assert.Equal(TarEntryType.Directory, entry2.EntryType);
+ Assert.Null(await reader.GetNextEntryAsync());
- // The reader must stay alive because it's in charge of disposing all the entries it collected
- Assert.Throws(() => entry.DataStream.Read(new byte[1]));
- }
+ // This is not possible because the position of the main stream is already past the data
+ await Assert.ThrowsAsync(async () => await entry.DataStream.ReadAsync(b));
}
+
+ // The reader must stay alive because it's in charge of disposing all the entries it collected
+ await Assert.ThrowsAsync(async () => await entry.DataStream.ReadAsync(b));
+
}
[Theory]
diff --git a/src/libraries/System.Formats.Tar/tests/TarTestsBase.cs b/src/libraries/System.Formats.Tar/tests/TarTestsBase.cs
index 3363d27946395..e62b5d045d552 100644
--- a/src/libraries/System.Formats.Tar/tests/TarTestsBase.cs
+++ b/src/libraries/System.Formats.Tar/tests/TarTestsBase.cs
@@ -489,6 +489,14 @@ protected static TarEntry InvokeTarEntryCreationConstructor(TarEntryFormat targe
_ => throw new InvalidDataException($"Unexpected format: {targetFormat}")
};
+ public static IEnumerable