diff --git a/src/libraries/System.Formats.Tar/src/Resources/Strings.resx b/src/libraries/System.Formats.Tar/src/Resources/Strings.resx index 0ece0c6c5615..1c5a56c4da14 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 90856330aa2e..adc9819b2cab 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 41be887ed389..5d9d3a544319 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 c4ec17272a8b..13654eaa7279 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 ab90f0043bc7..bf3cec4fcb08 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 d7502d940e94..307a165099f4 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 @@ await using (TarWriter writer = new(stream, TarEntryFormat.Pax, leaveOpen: true) 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 2cede3a350c8..ad87b69e08ce 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 f99e5853ebea..1c266ae63343 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 @@ await using (WrappedStream wrapped = new WrappedStream(archive, canRead: true, c } } - [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 3363d2794639..e62b5d045d55 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 GetTestTarFormats() + { + foreach (TestTarFormat testFormat in Enum.GetValues()) + { + yield return new object[] { testFormat }; + } + } + public static IEnumerable GetFormatsAndLinks() { foreach (TarEntryFormat format in new[] { TarEntryFormat.V7, TarEntryFormat.Ustar, TarEntryFormat.Pax, TarEntryFormat.Gnu }) diff --git a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.Base.cs b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.Base.cs index efe7c68ced6b..334a27e51fa9 100644 --- a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.Base.cs +++ b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.Base.cs @@ -1,11 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; using System.Collections.Generic; using System.IO; -using System.Linq; -using System.Text; using System.Threading.Tasks; using Xunit; diff --git a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.Tests.cs index 94522d54a617..487c6ab04dbc 100644 --- a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.Tests.cs @@ -21,14 +21,18 @@ public void WriteEntry_AfterDispose_Throws() Assert.Throws(() => writer.WriteEntry(entry)); } - [Fact] - public void WriteEntry_FromUnseekableStream_AdvanceDataStream_WriteFromThatPosition() + + [Theory] + [InlineData(TarEntryFormat.V7)] + [InlineData(TarEntryFormat.Ustar)] + [InlineData(TarEntryFormat.Pax)] + [InlineData(TarEntryFormat.Gnu)] + public void WriteEntry_FromUnseekableStream_AdvanceDataStream_WriteFromThatPosition(TarEntryFormat format) { using MemoryStream source = GetTarMemoryStream(CompressionMethod.Uncompressed, TestTarFormat.ustar, "file"); using WrappedStream unseekable = new WrappedStream(source, canRead: true, canWrite: true, canSeek: false); using MemoryStream destination = new MemoryStream(); - using (TarReader reader1 = new TarReader(unseekable)) { TarEntry entry = reader1.GetNextEntry(); @@ -39,6 +43,8 @@ public void WriteEntry_FromUnseekableStream_AdvanceDataStream_WriteFromThatPosit using (TarWriter writer = new TarWriter(destination, TarEntryFormat.Ustar, leaveOpen: true)) { writer.WriteEntry(entry); + TarEntry dirEntry = InvokeTarEntryCreationConstructor(format, TarEntryType.Directory, "dir"); + writer.WriteEntry(dirEntry); // To validate that next entry is not affected } } @@ -54,6 +60,14 @@ public void WriteEntry_FromUnseekableStream_AdvanceDataStream_WriteFromThatPosit string contents = streamReader.ReadLine(); Assert.Equal("ello file", contents); } + + TarEntry dirEntry = reader2.GetNextEntry(); + Assert.NotNull(dirEntry); + Assert.Equal(format, dirEntry.Format); + Assert.Equal(TarEntryType.Directory, dirEntry.EntryType); + Assert.Equal("dir", dirEntry.Name); + + Assert.Null(reader2.GetNextEntry()); } } @@ -502,8 +516,8 @@ public void WriteEntry_FileSizeOverLegacyLimit_Throws(TarEntryFormat entryFormat { const long FileSizeOverLimit = LegacyMaxFileSize + 1; - MemoryStream ms = new(); - Stream s = unseekableStream ? new WrappedStream(ms, ms.CanRead, ms.CanWrite, canSeek: false) : ms; + using MemoryStream ms = new(); + using Stream s = unseekableStream ? new WrappedStream(ms, ms.CanRead, ms.CanWrite, canSeek: false) : ms; using TarWriter writer = new(s); TarEntry writeEntry = InvokeTarEntryCreationConstructor(entryFormat, entryFormat is TarEntryFormat.V7 ? TarEntryType.V7RegularFile : TarEntryType.RegularFile, "foo"); @@ -513,5 +527,77 @@ public void WriteEntry_FileSizeOverLegacyLimit_Throws(TarEntryFormat entryFormat Assert.Throws(() => writer.WriteEntry(writeEntry)); } + + [Theory] + [InlineData(TarEntryFormat.V7)] + [InlineData(TarEntryFormat.Ustar)] + [InlineData(TarEntryFormat.Pax)] + [InlineData(TarEntryFormat.Gnu)] + public void WritingUnseekableDataStream_To_UnseekableArchiveStream_Throws(TarEntryFormat entryFormat) + { + using MemoryStream internalDataStream = new(); + using WrappedStream unseekableDataStream = new(internalDataStream, canRead: true, canWrite: false, canSeek: false); + + using MemoryStream internalArchiveStream = new(); + using WrappedStream unseekableArchiveStream = new(internalArchiveStream, canRead: true, canWrite: true, canSeek: false); + + using TarWriter writer = new(unseekableArchiveStream); + TarEntry entry = InvokeTarEntryCreationConstructor(entryFormat, GetTarEntryTypeForTarEntryFormat(TarEntryType.RegularFile, entryFormat), "file.txt"); + entry.DataStream = unseekableDataStream; + Assert.Throws(() => writer.WriteEntry(entry)); + } + + [Theory] + [InlineData(TarEntryFormat.V7)] + [InlineData(TarEntryFormat.Ustar)] + [InlineData(TarEntryFormat.Pax)] + [InlineData(TarEntryFormat.Gnu)] + public void Write_TwoEntries_With_UnseekableDataStreams(TarEntryFormat entryFormat) + { + byte[] expectedBytes = new byte[] { 0x1, 0x2, 0x3, 0x4, 0x5 }; + + using MemoryStream internalDataStream1 = new(); + internalDataStream1.Write(expectedBytes.AsSpan()); + internalDataStream1.Position = 0; + + TarEntryType fileEntryType = GetTarEntryTypeForTarEntryFormat(TarEntryType.RegularFile, entryFormat); + + using WrappedStream unseekableDataStream1 = new(internalDataStream1, canRead: true, canWrite: false, canSeek: false); + TarEntry entry1 = InvokeTarEntryCreationConstructor(entryFormat, fileEntryType, "file1.txt"); + entry1.DataStream = unseekableDataStream1; + + using MemoryStream internalDataStream2 = new(); + internalDataStream2.Write(expectedBytes.AsSpan()); + internalDataStream2.Position = 0; + + using WrappedStream unseekableDataStream2 = new(internalDataStream2, canRead: true, canWrite: false, canSeek: false); + TarEntry entry2 = InvokeTarEntryCreationConstructor(entryFormat, fileEntryType, "file2.txt"); + entry2.DataStream = unseekableDataStream2; + + using MemoryStream archiveStream = new(); + using (TarWriter writer = new(archiveStream, leaveOpen: true)) + { + writer.WriteEntry(entry1); // Should not throw + writer.WriteEntry(entry2); // To verify that second entry is written in correct place + } + + // Verify + archiveStream.Position = 0; + byte[] actualBytes = new byte[] { 0, 0, 0, 0, 0 }; + using (TarReader reader = new(archiveStream)) + { + TarEntry readEntry = reader.GetNextEntry(); + Assert.NotNull(readEntry); + readEntry.DataStream.ReadExactly(actualBytes); + Assert.Equal(expectedBytes, actualBytes); + + readEntry = reader.GetNextEntry(); + Assert.NotNull(readEntry); + readEntry.DataStream.ReadExactly(actualBytes); + Assert.Equal(expectedBytes, actualBytes); + + Assert.Null(reader.GetNextEntry()); + } + } } } diff --git a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.Tests.cs index f0d4d238b2f8..45b26ad66e60 100644 --- a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.Tests.cs @@ -43,8 +43,12 @@ public async Task WriteEntry_AfterDispose_Throws_Async() await Assert.ThrowsAsync(() => writer.WriteEntryAsync(entry)); } - [Fact] - public async Task WriteEntry_FromUnseekableStream_AdvanceDataStream_WriteFromThatPosition_Async() + [Theory] + [InlineData(TarEntryFormat.V7)] + [InlineData(TarEntryFormat.Ustar)] + [InlineData(TarEntryFormat.Pax)] + [InlineData(TarEntryFormat.Gnu)] + public async Task WriteEntry_FromUnseekableStream_AdvanceDataStream_WriteFromThatPosition_Async(TarEntryFormat format) { using MemoryStream source = GetTarMemoryStream(CompressionMethod.Uncompressed, TestTarFormat.ustar, "file"); using WrappedStream unseekable = new WrappedStream(source, canRead: true, canWrite: true, canSeek: false); @@ -57,9 +61,11 @@ await using (TarReader reader1 = new TarReader(unseekable)) Assert.NotNull(entry.DataStream); entry.DataStream.ReadByte(); // Advance one byte, now the expected string would be "ello file" - await using (TarWriter writer = new TarWriter(destination, TarEntryFormat.Ustar, leaveOpen: true)) + await using (TarWriter writer = new TarWriter(destination, format, leaveOpen: true)) { await writer.WriteEntryAsync(entry); + TarEntry dirEntry = InvokeTarEntryCreationConstructor(format, TarEntryType.Directory, "dir"); + await writer.WriteEntryAsync(dirEntry); // To validate that next entry is not affected } } @@ -75,6 +81,14 @@ await using (TarReader reader2 = new TarReader(destination)) string contents = streamReader.ReadLine(); Assert.Equal("ello file", contents); } + + TarEntry dirEntry = await reader2.GetNextEntryAsync(); + Assert.NotNull(dirEntry); + Assert.Equal(format, dirEntry.Format); + Assert.Equal(TarEntryType.Directory, dirEntry.EntryType); + Assert.Equal("dir", dirEntry.Name); + + Assert.Null(await reader2.GetNextEntryAsync()); } } @@ -420,8 +434,8 @@ public async Task WriteEntry_FileSizeOverLegacyLimit_Throws_Async(TarEntryFormat { const long FileSizeOverLimit = LegacyMaxFileSize + 1; - MemoryStream ms = new(); - Stream s = unseekableStream ? new WrappedStream(ms, ms.CanRead, ms.CanWrite, canSeek: false) : ms; + await using MemoryStream ms = new(); + await using Stream s = unseekableStream ? new WrappedStream(ms, ms.CanRead, ms.CanWrite, canSeek: false) : ms; string tarFilePath = GetTestFilePath(); await using TarWriter writer = new(File.Create(tarFilePath)); @@ -432,5 +446,77 @@ public async Task WriteEntry_FileSizeOverLegacyLimit_Throws_Async(TarEntryFormat await Assert.ThrowsAsync(() => writer.WriteEntryAsync(writeEntry)); } + + [Theory] + [InlineData(TarEntryFormat.V7)] + [InlineData(TarEntryFormat.Ustar)] + [InlineData(TarEntryFormat.Pax)] + [InlineData(TarEntryFormat.Gnu)] + public async Task WritingUnseekableDataStream_To_UnseekableArchiveStream_Throws_Async(TarEntryFormat entryFormat) + { + await using MemoryStream internalDataStream = new(); + await using WrappedStream unseekableDataStream = new(internalDataStream, canRead: true, canWrite: false, canSeek: false); + + await using MemoryStream internalArchiveStream = new(); + await using WrappedStream unseekableArchiveStream = new(internalArchiveStream, canRead: true, canWrite: true, canSeek: false); + + await using TarWriter writer = new(unseekableArchiveStream); + TarEntry entry = InvokeTarEntryCreationConstructor(entryFormat, GetTarEntryTypeForTarEntryFormat(TarEntryType.RegularFile, entryFormat), "file.txt"); + entry.DataStream = unseekableDataStream; + await Assert.ThrowsAsync(() => writer.WriteEntryAsync(entry)); + } + + [Theory] + [InlineData(TarEntryFormat.V7)] + [InlineData(TarEntryFormat.Ustar)] + [InlineData(TarEntryFormat.Pax)] + [InlineData(TarEntryFormat.Gnu)] + public async Task Write_TwoEntries_With_UnseekableDataStreams_Async(TarEntryFormat entryFormat) + { + byte[] expectedBytes = new byte[] { 0x1, 0x2, 0x3, 0x4, 0x5 }; + + await using MemoryStream internalDataStream1 = new(); + await internalDataStream1.WriteAsync(expectedBytes.AsMemory()); + internalDataStream1.Position = 0; + + TarEntryType fileEntryType = GetTarEntryTypeForTarEntryFormat(TarEntryType.RegularFile, entryFormat); + + await using WrappedStream unseekableDataStream1 = new(internalDataStream1, canRead: true, canWrite: false, canSeek: false); + TarEntry entry1 = InvokeTarEntryCreationConstructor(entryFormat, fileEntryType, "file1.txt"); + entry1.DataStream = unseekableDataStream1; + + await using MemoryStream internalDataStream2 = new(); + await internalDataStream2.WriteAsync(expectedBytes.AsMemory()); + internalDataStream2.Position = 0; + + await using WrappedStream unseekableDataStream2 = new(internalDataStream2, canRead: true, canWrite: false, canSeek: false); + TarEntry entry2 = InvokeTarEntryCreationConstructor(entryFormat, fileEntryType, "file2.txt"); + entry2.DataStream = unseekableDataStream2; + + await using MemoryStream archiveStream = new(); + await using (TarWriter writer = new(archiveStream, leaveOpen: true)) + { + await writer.WriteEntryAsync(entry1); // Should not throw + await writer.WriteEntryAsync(entry2); // To verify that second entry is written in correct place + } + + // Verify + archiveStream.Position = 0; + byte[] actualBytes = new byte[] { 0, 0, 0, 0, 0 }; + await using (TarReader reader = new(archiveStream)) + { + TarEntry readEntry = await reader.GetNextEntryAsync(); + Assert.NotNull(readEntry); + await readEntry.DataStream.ReadExactlyAsync(actualBytes); + Assert.Equal(expectedBytes, actualBytes); + + readEntry = await reader.GetNextEntryAsync(); + Assert.NotNull(readEntry); + await readEntry.DataStream.ReadExactlyAsync(actualBytes); + Assert.Equal(expectedBytes, actualBytes); + + Assert.Null(await reader.GetNextEntryAsync()); + } + } } }