diff --git a/src/libraries/System.Formats.Tar/src/Resources/Strings.resx b/src/libraries/System.Formats.Tar/src/Resources/Strings.resx index 001d85b764bcaf..5308e9153c9791 100644 --- a/src/libraries/System.Formats.Tar/src/Resources/Strings.resx +++ b/src/libraries/System.Formats.Tar/src/Resources/Strings.resx @@ -205,6 +205,9 @@ The entry is a symbolic link or a hard link but the LinkName field is null or empty. + Entry type '{0}' not supported. + + Entry type '{0}' not supported in format '{1}'. diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Read.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Read.cs index e43ae6973607dc..5578707d64eb18 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Read.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Read.cs @@ -20,7 +20,7 @@ internal sealed partial class TarHeader // Attempts to retrieve the next header from the specified tar archive stream. // Throws if end of stream is reached or if any data type conversion fails. // Returns a valid TarHeader object if the attributes were read successfully, null otherwise. - internal static TarHeader? TryGetNextHeader(Stream archiveStream, bool copyData, TarEntryFormat initialFormat) + internal static TarHeader? TryGetNextHeader(Stream archiveStream, bool copyData, TarEntryFormat initialFormat, bool processDataBlock) { // The four supported formats have a header that fits in the default record size Span buffer = stackalloc byte[TarHelpers.RecordSize]; @@ -28,7 +28,7 @@ internal sealed partial class TarHeader archiveStream.ReadExactly(buffer); TarHeader? header = TryReadAttributes(initialFormat, buffer); - if (header != null) + if (header != null && processDataBlock) { header.ProcessDataBlock(archiveStream, copyData); } @@ -39,7 +39,7 @@ internal sealed partial class TarHeader // Asynchronously attempts read all the fields of the next header. // Throws if end of stream is reached or if any data type conversion fails. // Returns true if all the attributes were read successfully, false otherwise. - internal static async ValueTask TryGetNextHeaderAsync(Stream archiveStream, bool copyData, TarEntryFormat initialFormat, CancellationToken cancellationToken) + internal static async ValueTask TryGetNextHeaderAsync(Stream archiveStream, bool copyData, TarEntryFormat initialFormat, bool processDataBlock, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); @@ -50,7 +50,7 @@ internal sealed partial class TarHeader await archiveStream.ReadExactlyAsync(buffer, cancellationToken).ConfigureAwait(false); TarHeader? header = TryReadAttributes(initialFormat, buffer.Span); - if (header != null) + if (header != null && processDataBlock) { await header.ProcessDataBlockAsync(archiveStream, copyData, cancellationToken).ConfigureAwait(false); } @@ -180,7 +180,7 @@ internal void ReplaceNormalAttributesWithExtended(Dictionary? di // will get all the data section read and the stream pointer positioned at the beginning of the next header. // - Block, Character, Directory, Fifo, HardLink and SymbolicLink typeflag entries have no data section so the archive stream pointer will be positioned at the beginning of the next header. // - All other typeflag entries with a data section will generate a stream wrapping the data section: SeekableSubReadStream for seekable archive streams, and SubReadStream for unseekable archive streams. - private void ProcessDataBlock(Stream archiveStream, bool copyData) + internal void ProcessDataBlock(Stream archiveStream, bool copyData) { bool skipBlockAlignmentPadding = true; @@ -199,6 +199,10 @@ private void ProcessDataBlock(Stream archiveStream, bool copyData) case TarEntryType.HardLink: case TarEntryType.SymbolicLink: // No data section + if (_size > 0) + { + throw new FormatException(string.Format(SR.TarSizeFieldTooLargeForEntryType, _typeFlag)); + } break; case TarEntryType.RegularFile: case TarEntryType.V7RegularFile: // Treated as regular file @@ -257,6 +261,10 @@ private async Task ProcessDataBlockAsync(Stream archiveStream, bool copyData, Ca case TarEntryType.HardLink: case TarEntryType.SymbolicLink: // No data section + if (_size > 0) + { + throw new FormatException(string.Format(SR.TarSizeFieldTooLargeForEntryType, _typeFlag)); + } break; case TarEntryType.RegularFile: case TarEntryType.V7RegularFile: // Treated as regular file @@ -311,6 +319,8 @@ private async Task ProcessDataBlockAsync(Stream archiveStream, bool copyData, Ca { MemoryStream copiedData = new MemoryStream(); TarHelpers.CopyBytes(archiveStream, copiedData, _size); + // Reset position pointer so the user can do the first DataStream read from the beginning + copiedData.Position = 0; return copiedData; } @@ -336,6 +346,8 @@ private async Task ProcessDataBlockAsync(Stream archiveStream, bool copyData, Ca { MemoryStream copiedData = new MemoryStream(); await TarHelpers.CopyBytesAsync(archiveStream, copiedData, size, cancellationToken).ConfigureAwait(false); + // Reset position pointer so the user can do the first DataStream read from the beginning + copiedData.Position = 0; return copiedData; } @@ -396,12 +408,13 @@ TarEntryType.LongLink or TarEntryType.LongPath or TarEntryType.MultiVolume or TarEntryType.RenamedOrSymlinked or - TarEntryType.SparseFile or TarEntryType.TapeVolume => TarEntryFormat.Gnu, // V7 is the only one that uses 'V7RegularFile'. TarEntryType.V7RegularFile => TarEntryFormat.V7, + TarEntryType.SparseFile => throw new NotSupportedException(string.Format(SR.TarEntryTypeNotSupported, header._typeFlag)), + // We can quickly determine the *minimum* possible format if the entry type // is the POSIX 'RegularFile', although later we could upgrade it to PAX or GNU _ => (header._typeFlag == TarEntryType.RegularFile) ? TarEntryFormat.Ustar : TarEntryFormat.V7 diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHelpers.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHelpers.cs index 898daf1e6c3ed7..c2a1b3b854c14b 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHelpers.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHelpers.cs @@ -203,6 +203,12 @@ internal static TarEntryType GetCorrectTypeFlagForFormat(TarEntryFormat format, internal static T ParseOctal(ReadOnlySpan buffer) where T : struct, INumber { buffer = TrimEndingNullsAndSpaces(buffer); + buffer = TrimLeadingNullsAndSpaces(buffer); + + if (buffer.Length == 0) + { + return T.Zero; + } T octalFactor = T.CreateTruncating(8u); T value = T.Zero; @@ -243,6 +249,17 @@ internal static ReadOnlySpan TrimEndingNullsAndSpaces(ReadOnlySpan b return buffer.Slice(0, trimmedLength); } + private static ReadOnlySpan TrimLeadingNullsAndSpaces(ReadOnlySpan buffer) + { + int newStart = 0; + while (newStart < buffer.Length && buffer[newStart] is 0 or 32) + { + newStart++; + } + + return buffer.Slice(newStart); + } + // Returns the ASCII string contained in the specified buffer of bytes, // removing the trailing null or space chars. internal static string GetTrimmedAsciiString(ReadOnlySpan buffer) => GetTrimmedString(buffer, Encoding.ASCII); @@ -351,7 +368,7 @@ TarEntryType.RegularFile or throw new FormatException(string.Format(SR.TarInvalidFormat, archiveFormat)); } - throw new InvalidOperationException(string.Format(SR.TarEntryTypeNotSupported, entryType, archiveFormat)); + throw new InvalidOperationException(string.Format(SR.TarEntryTypeNotSupportedInFormat, entryType, archiveFormat)); } } } diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarReader.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarReader.cs index 915cb81caab664..5328d61190280b 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarReader.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarReader.cs @@ -19,7 +19,6 @@ public sealed class TarReader : IDisposable, IAsyncDisposable private readonly bool _leaveOpen; private TarEntry? _previouslyReadEntry; private List? _dataStreamsToDispose; - private bool _readFirstEntry; private bool _reachedEndMarkers; internal Stream _archiveStream; @@ -44,7 +43,6 @@ public TarReader(Stream archiveStream, bool leaveOpen = false) _previouslyReadEntry = null; _isDisposed = false; - _readFirstEntry = false; _reachedEndMarkers = false; } @@ -124,11 +122,6 @@ public async ValueTask DisposeAsync() TarHeader? header = TryGetNextEntryHeader(copyData); if (header != null) { - if (!_readFirstEntry) - { - _readFirstEntry = true; - } - TarEntry entry = header._format switch { TarEntryFormat.Pax => header._typeFlag is TarEntryType.GlobalExtendedAttributes ? @@ -282,11 +275,6 @@ internal async ValueTask AdvanceDataStreamIfNeededAsync(CancellationToken cancel TarHeader? header = await TryGetNextEntryHeaderAsync(copyData, cancellationToken).ConfigureAwait(false); if (header != null) { - if (!_readFirstEntry) - { - _readFirstEntry = true; - } - TarEntry entry = header._format switch { TarEntryFormat.Pax => header._typeFlag is TarEntryType.GlobalExtendedAttributes ? @@ -319,7 +307,7 @@ internal async ValueTask AdvanceDataStreamIfNeededAsync(CancellationToken cancel { Debug.Assert(!_reachedEndMarkers); - TarHeader? header = TarHeader.TryGetNextHeader(_archiveStream, copyData, TarEntryFormat.Unknown); + TarHeader? header = TarHeader.TryGetNextHeader(_archiveStream, copyData, TarEntryFormat.Unknown, processDataBlock: true); if (header == null) { @@ -361,7 +349,7 @@ internal async ValueTask AdvanceDataStreamIfNeededAsync(CancellationToken cancel Debug.Assert(!_reachedEndMarkers); - TarHeader? header = await TarHeader.TryGetNextHeaderAsync(_archiveStream, copyData, TarEntryFormat.Unknown, cancellationToken).ConfigureAwait(false); + TarHeader? header = await TarHeader.TryGetNextHeaderAsync(_archiveStream, copyData, TarEntryFormat.Unknown, processDataBlock: true, cancellationToken).ConfigureAwait(false); if (header == null) { return null; @@ -397,9 +385,10 @@ internal async ValueTask AdvanceDataStreamIfNeededAsync(CancellationToken cancel // and returns the actual entry with the processed extended attributes saved in the _extendedAttributes dictionary. private bool TryProcessExtendedAttributesHeader(TarHeader extendedAttributesHeader, bool copyData, [NotNullWhen(returnValue: true)] out TarHeader? actualHeader) { - actualHeader = TarHeader.TryGetNextHeader(_archiveStream, copyData, TarEntryFormat.Pax); + // Don't process the data block of the actual entry just yet, because there's a slim chance + // that the extended attributes contain a size that we need to override in the header + actualHeader = TarHeader.TryGetNextHeader(_archiveStream, copyData, TarEntryFormat.Pax, processDataBlock: false); - // Now get the actual entry if (actualHeader == null) { return false; @@ -417,6 +406,9 @@ TarEntryType.LongLink or // Replace all the attributes representing standard fields with the extended ones, if any actualHeader.ReplaceNormalAttributesWithExtended(extendedAttributesHeader.ExtendedAttributes); + // We retrieved the extended attributes, now we can read the data, and always with the right size + actualHeader.ProcessDataBlock(_archiveStream, copyData); + return true; } @@ -426,8 +418,9 @@ TarEntryType.LongLink or { cancellationToken.ThrowIfCancellationRequested(); - // Now get the actual entry - TarHeader? actualHeader = await TarHeader.TryGetNextHeaderAsync(_archiveStream, copyData, TarEntryFormat.Pax, cancellationToken).ConfigureAwait(false); + // Don't process the data block of the actual entry just yet, because there's a slim chance + // that the extended attributes contain a size that we need to override in the header + TarHeader? actualHeader = await TarHeader.TryGetNextHeaderAsync(_archiveStream, copyData, TarEntryFormat.Pax, processDataBlock: false, cancellationToken).ConfigureAwait(false); if (actualHeader == null) { return null; @@ -451,6 +444,9 @@ TarEntryType.LongLink or // Replace all the attributes representing standard fields with the extended ones, if any actualHeader.ReplaceNormalAttributesWithExtended(extendedAttributesHeader.ExtendedAttributes); + // We retrieved the extended attributes, now we can read the data, and always with the right size + actualHeader.ProcessDataBlock(_archiveStream, copyData); + return actualHeader; } @@ -460,7 +456,7 @@ private bool TryProcessGnuMetadataHeader(TarHeader header, bool copyData, out Ta { finalHeader = new(TarEntryFormat.Gnu); - TarHeader? secondHeader = TarHeader.TryGetNextHeader(_archiveStream, copyData, TarEntryFormat.Gnu); + TarHeader? secondHeader = TarHeader.TryGetNextHeader(_archiveStream, copyData, TarEntryFormat.Gnu, processDataBlock: true); // Get the second entry, which is the actual entry if (secondHeader == null) @@ -478,7 +474,7 @@ private bool TryProcessGnuMetadataHeader(TarHeader header, bool copyData, out Ta if ((header._typeFlag is TarEntryType.LongLink && secondHeader._typeFlag is TarEntryType.LongPath) || (header._typeFlag is TarEntryType.LongPath && secondHeader._typeFlag is TarEntryType.LongLink)) { - TarHeader? thirdHeader = TarHeader.TryGetNextHeader(_archiveStream, copyData, TarEntryFormat.Gnu); + TarHeader? thirdHeader = TarHeader.TryGetNextHeader(_archiveStream, copyData, TarEntryFormat.Gnu, processDataBlock: true); // Get the third entry, which is the actual entry if (thirdHeader == null) @@ -537,7 +533,7 @@ private bool TryProcessGnuMetadataHeader(TarHeader header, bool copyData, out Ta cancellationToken.ThrowIfCancellationRequested(); // Get the second entry, which is the actual entry - TarHeader? secondHeader = await TarHeader.TryGetNextHeaderAsync(_archiveStream, copyData, TarEntryFormat.Gnu, cancellationToken).ConfigureAwait(false); + TarHeader? secondHeader = await TarHeader.TryGetNextHeaderAsync(_archiveStream, copyData, TarEntryFormat.Gnu, processDataBlock: true, cancellationToken).ConfigureAwait(false); if (secondHeader == null) { return null; @@ -556,7 +552,7 @@ private bool TryProcessGnuMetadataHeader(TarHeader header, bool copyData, out Ta (header._typeFlag is TarEntryType.LongPath && secondHeader._typeFlag is TarEntryType.LongLink)) { // Get the third entry, which is the actual entry - TarHeader? thirdHeader = await TarHeader.TryGetNextHeaderAsync(_archiveStream, copyData, TarEntryFormat.Gnu, cancellationToken).ConfigureAwait(false); + TarHeader? thirdHeader = await TarHeader.TryGetNextHeaderAsync(_archiveStream, copyData, TarEntryFormat.Gnu, processDataBlock: true, cancellationToken).ConfigureAwait(false); if (thirdHeader == null) { return null; diff --git a/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.File.Async.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.File.Async.Tests.cs index 14087fb030d020..69a543a206d4b9 100644 --- a/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.File.Async.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.File.Async.Tests.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Globalization; using System.IO; +using System.IO.Compression; using System.Linq; using System.Threading.Tasks; using Xunit; @@ -125,5 +126,245 @@ public Task Read_Archive_LongFileName_Over100_Under255_Async(TarEntryFormat form [InlineData(TarEntryFormat.Gnu, TestTarFormat.oldgnu)] public Task Read_Archive_LongPath_Over255_Async(TarEntryFormat format, TestTarFormat testFormat) => Read_Archive_LongPath_Over255_Async_Internal(format, testFormat); + + [Theory] + [MemberData(nameof(GetV7TestCaseNames))] + public Task ReadDataStreamOfTarGzV7Async(string testCaseName) => + VerifyDataStreamOfTarGzInternalAsync(TestTarFormat.v7, testCaseName, copyData: false); + + [Theory] + [MemberData(nameof(GetUstarTestCaseNames))] + public Task ReadDataStreamOfTarGzUstarAsync(string testCaseName) => + VerifyDataStreamOfTarGzInternalAsync(TestTarFormat.ustar, testCaseName, copyData: false); + + [Theory] + [MemberData(nameof(GetPaxAndGnuTestCaseNames))] + public Task ReadDataStreamOfTarGzPaxAsync(string testCaseName) => + VerifyDataStreamOfTarGzInternalAsync(TestTarFormat.pax, testCaseName, copyData: false); + + [Theory] + [MemberData(nameof(GetPaxAndGnuTestCaseNames))] + public Task ReadDataStreamOfTarGzPaxGeaAsync(string testCaseName) => + VerifyDataStreamOfTarGzInternalAsync(TestTarFormat.pax_gea, testCaseName, copyData: false); + + [Theory] + [MemberData(nameof(GetPaxAndGnuTestCaseNames))] + public Task ReadDataStreamOfTarGzOldGnuAsync(string testCaseName) => + VerifyDataStreamOfTarGzInternalAsync(TestTarFormat.oldgnu, testCaseName, copyData: false); + + [Theory] + [MemberData(nameof(GetPaxAndGnuTestCaseNames))] + public Task ReadDataStreamOfTarGzGnuAsync(string testCaseName) => + VerifyDataStreamOfTarGzInternalAsync(TestTarFormat.gnu, testCaseName, copyData: false); + + [Theory] + [MemberData(nameof(GetV7TestCaseNames))] + public Task ReadCopiedDataStreamOfTarGzV7Async(string testCaseName) => + VerifyDataStreamOfTarGzInternalAsync(TestTarFormat.v7, testCaseName, copyData: true); + + [Theory] + [MemberData(nameof(GetUstarTestCaseNames))] + public Task ReadCopiedDataStreamOfTarGzUstarAsync(string testCaseName) => + VerifyDataStreamOfTarGzInternalAsync(TestTarFormat.ustar, testCaseName, copyData: true); + + [Theory] + [MemberData(nameof(GetPaxAndGnuTestCaseNames))] + public Task ReadCopiedDataStreamOfTarGzPaxAsync(string testCaseName) => + VerifyDataStreamOfTarGzInternalAsync(TestTarFormat.pax, testCaseName, copyData: true); + + [Theory] + [MemberData(nameof(GetPaxAndGnuTestCaseNames))] + public Task ReadCopiedDataStreamOfTarGzPaxGeaAsync(string testCaseName) => + VerifyDataStreamOfTarGzInternalAsync(TestTarFormat.pax_gea, testCaseName, copyData: true); + + [Theory] + [MemberData(nameof(GetPaxAndGnuTestCaseNames))] + public Task ReadCopiedDataStreamOfTarGzOldGnuAsync(string testCaseName) => + VerifyDataStreamOfTarGzInternalAsync(TestTarFormat.oldgnu, testCaseName, copyData: true); + + [Theory] + [MemberData(nameof(GetPaxAndGnuTestCaseNames))] + public Task ReadCopiedDataStreamOfTarGzGnuAsync(string testCaseName) => + VerifyDataStreamOfTarGzInternalAsync(TestTarFormat.gnu, testCaseName, copyData: true); + + [Theory] + [MemberData(nameof(GetGoLangTarTestCaseNames))] + public Task ReadDataStreamOfExternalAssetsGoLangAsync(string testCaseName) => + VerifyDataStreamOfTarUncompressedInternalAsync("golang_tar", testCaseName, copyData: false); + + [Theory] + [MemberData(nameof(GetNodeTarTestCaseNames))] + public Task ReadDataStreamOfExternalAssetsNodeAsync(string testCaseName) => + VerifyDataStreamOfTarUncompressedInternalAsync("node-tar", testCaseName, copyData: false); + + [Theory] + [MemberData(nameof(GetRsTarTestCaseNames))] + public Task ReadDataStreamOfExternalAssetsRsAsync(string testCaseName) => + VerifyDataStreamOfTarUncompressedInternalAsync("tar-rs", testCaseName, copyData: false); + + [Theory] + [MemberData(nameof(GetGoLangTarTestCaseNames))] + public Task ReadCopiedDataStreamOfExternalAssetsGoLangAsync(string testCaseName) => + VerifyDataStreamOfTarUncompressedInternalAsync("golang_tar", testCaseName, copyData: true); + + [Theory] + [MemberData(nameof(GetNodeTarTestCaseNames))] + public Task ReadCopiedDataStreamOfExternalAssetsNodeAsync(string testCaseName) => + VerifyDataStreamOfTarUncompressedInternalAsync("node-tar", testCaseName, copyData: true); + + [Theory] + [MemberData(nameof(GetRsTarTestCaseNames))] + public Task ReadCopiedDataStreamOfExternalAssetsRsAsync(string testCaseName) => + VerifyDataStreamOfTarUncompressedInternalAsync("tar-rs", testCaseName, copyData: true); + + [Fact] + public async Task Throw_FifoContainsNonZeroDataSectionAsync() + { + await using MemoryStream archiveStream = GetTarMemoryStream(CompressionMethod.Uncompressed, "golang_tar", "hdr-only"); + await using TarReader reader = new TarReader(archiveStream); + Assert.NotNull(await reader.GetNextEntryAsync()); + Assert.NotNull(await reader.GetNextEntryAsync()); + Assert.NotNull(await reader.GetNextEntryAsync()); + Assert.NotNull(await reader.GetNextEntryAsync()); + Assert.NotNull(await reader.GetNextEntryAsync()); + Assert.NotNull(await reader.GetNextEntryAsync()); + Assert.NotNull(await reader.GetNextEntryAsync()); + Assert.NotNull(await reader.GetNextEntryAsync()); + await Assert.ThrowsAsync(async () => await reader.GetNextEntryAsync()); + } + + [Fact] + public async Task Throw_SingleExtendedAttributesEntryWithNoActualEntryAsync() + { + await using MemoryStream archiveStream = GetTarMemoryStream(CompressionMethod.Uncompressed, "golang_tar", "pax-path-hdr"); + await using TarReader reader = new TarReader(archiveStream); + await Assert.ThrowsAsync(async () => await reader.GetNextEntryAsync()); + } + + [Theory] + [InlineData("tar-rs", "spaces")] + [InlineData("golang_tar", "v7")] + public async Task AllowSpacesInOctalFieldsAsync(string folderName, string testCaseName) + { + await using MemoryStream archiveStream = GetTarMemoryStream(CompressionMethod.Uncompressed, folderName, testCaseName); + await using TarReader reader = new TarReader(archiveStream); + TarEntry entry; + while ((entry = await reader.GetNextEntryAsync()) != null) + { + AssertExtensions.GreaterThan(entry.Checksum, 0); + AssertExtensions.GreaterThan((int)entry.Mode, 0); + } + } + + [Theory] + [InlineData("pax-multi-hdrs")] // Multiple consecutive PAX metadata entries + [InlineData("gnu-multi-hdrs")] // Multiple consecutive GNU metadata entries + [InlineData("neg-size")] // Garbage chars + [InlineData("invalid-go17")] // Many octal fields are all zero chars + [InlineData("issue11169")] // Checksum with null in the middle + [InlineData("issue10968")] // Garbage chars + [InlineData("writer-big")] // The size field contains an euro char + public async Task Throw_ArchivesWithRandomCharsAsync(string testCaseName) + { + await using MemoryStream archiveStream = GetTarMemoryStream(CompressionMethod.Uncompressed, "golang_tar", testCaseName); + await using TarReader reader = new TarReader(archiveStream); + await Assert.ThrowsAsync(async () => await reader.GetNextEntryAsync()); + } + + [Fact] + public async Task GarbageEntryChecksumZeroReturnNullAsync() + { + await using MemoryStream archiveStream = GetTarMemoryStream(CompressionMethod.Uncompressed, "golang_tar", "issue12435"); + await using TarReader reader = new TarReader(archiveStream); + Assert.Null(await reader.GetNextEntryAsync()); + } + + [Theory] + [InlineData("golang_tar", "gnu-nil-sparse-data")] + [InlineData("golang_tar", "gnu-nil-sparse-hole")] + [InlineData("golang_tar", "gnu-sparse-big")] + [InlineData("golang_tar", "sparse-formats")] + [InlineData("tar-rs", "sparse-1")] + [InlineData("tar-rs", "sparse")] + public async Task SparseEntryNotSupportedAsync(string testFolderName, string testCaseName) + { + // Currently sparse entries are not supported. + + // There are PAX archives archives in the golang folder that have extended attributes for treating a regular file as a sparse file. + // Sparse entries were created for the GNU format, so they are very rare entry types which are excluded from this test method: + // pax-nil-sparse-data, pax-nil-sparse-hole, pax-sparse-big + + await using MemoryStream archiveStream = GetTarMemoryStream(CompressionMethod.Uncompressed, testFolderName, testCaseName); + await using TarReader reader = new TarReader(archiveStream); + await Assert.ThrowsAsync(async () => await reader.GetNextEntryAsync()); + } + + [Fact] + public async Task DirectoryListRegularFileAndSparseAsync() + { + await using MemoryStream archiveStream = GetTarMemoryStream(CompressionMethod.Uncompressed, "golang_tar", "gnu-incremental"); + await using TarReader reader = new TarReader(archiveStream); + TarEntry directoryList = await reader.GetNextEntryAsync(); + + Assert.Equal(TarEntryType.DirectoryList, directoryList.EntryType); + Assert.NotNull(directoryList.DataStream); + Assert.Equal(14, directoryList.Length); + + Assert.NotNull(await reader.GetNextEntryAsync()); // Just a regular file + + await Assert.ThrowsAsync(async () => await reader.GetNextEntryAsync()); // Sparse + } + + [Fact] + public async Task PaxSizeLargerThanMaxAllowedByStreamAsync() + { + await using MemoryStream archiveStream = GetTarMemoryStream(CompressionMethod.Uncompressed, "golang_tar", "writer-big-long"); + await using TarReader reader = new TarReader(archiveStream); + // The extended attribute 'size' has the value 17179869184 + // Exception message: Stream length must be non-negative and less than 2^31 - 1 - origin + await Assert.ThrowsAsync(async () => await reader.GetNextEntryAsync()); + } + + private static async Task VerifyDataStreamOfTarUncompressedInternalAsync(string testFolderName, string testCaseName, bool copyData) + { + await using MemoryStream archiveStream = GetTarMemoryStream(CompressionMethod.Uncompressed, testFolderName, testCaseName); + await VerifyDataStreamOfTarInternalAsync(archiveStream, copyData); + } + + private static async Task VerifyDataStreamOfTarGzInternalAsync(TestTarFormat testTarFormat, string testCaseName, bool copyData) + { + await using MemoryStream archiveStream = GetTarMemoryStream(CompressionMethod.GZip, testTarFormat, testCaseName); + await using GZipStream decompressor = new GZipStream(archiveStream, CompressionMode.Decompress); + await VerifyDataStreamOfTarInternalAsync(decompressor, copyData); + } + + private static async Task VerifyDataStreamOfTarInternalAsync(Stream archiveStream, bool copyData) + { + await using TarReader reader = new TarReader(archiveStream); + + TarEntry entry; + + await using MemoryStream ms = new MemoryStream(); + while ((entry = await reader.GetNextEntryAsync(copyData)) != null) + { + if (entry.EntryType is TarEntryType.V7RegularFile or TarEntryType.RegularFile) + { + if (entry.Length == 0) + { + Assert.Null(entry.DataStream); + } + else + { + Assert.NotNull(entry.DataStream); + Assert.Equal(entry.DataStream.Length, entry.Length); + if (copyData) + { + Assert.True(entry.DataStream.CanSeek); + Assert.Equal(0, entry.DataStream.Position); + } + } + } + } + } } } diff --git a/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.File.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.File.Tests.cs index 5e35c12c3aaac9..298e3e8be47947 100644 --- a/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.File.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.File.Tests.cs @@ -4,8 +4,10 @@ using System.Collections.Generic; using System.Globalization; using System.IO; +using System.IO.Compression; using System.Linq; using Xunit; +using static System.Formats.Tar.Tests.TarTestsBase; namespace System.Formats.Tar.Tests { @@ -125,28 +127,241 @@ public void Read_Archive_LongFileName_Over100_Under255(TarEntryFormat format, Te public void Read_Archive_LongPath_Over255(TarEntryFormat format, TestTarFormat testFormat) => Read_Archive_LongPath_Over255_Internal(format, testFormat); + [Theory] + [MemberData(nameof(GetV7TestCaseNames))] + public void ReadDataStreamOfTarGzV7(string testCaseName) => + VerifyDataStreamOfTarGzInternal(TestTarFormat.v7, testCaseName, copyData: false); + + [Theory] + [MemberData(nameof(GetUstarTestCaseNames))] + public void ReadDataStreamOfTarGzUstar(string testCaseName) => + VerifyDataStreamOfTarGzInternal(TestTarFormat.ustar, testCaseName, copyData: false); + + [Theory] + [MemberData(nameof(GetPaxAndGnuTestCaseNames))] + public void ReadDataStreamOfTarGzPax(string testCaseName) => + VerifyDataStreamOfTarGzInternal(TestTarFormat.pax, testCaseName, copyData: false); + + [Theory] + [MemberData(nameof(GetPaxAndGnuTestCaseNames))] + public void ReadDataStreamOfTarGzPaxGea(string testCaseName) => + VerifyDataStreamOfTarGzInternal(TestTarFormat.pax_gea, testCaseName, copyData: false); + + [Theory] + [MemberData(nameof(GetPaxAndGnuTestCaseNames))] + public void ReadDataStreamOfTarGzOldGnu(string testCaseName) => + VerifyDataStreamOfTarGzInternal(TestTarFormat.oldgnu, testCaseName, copyData: false); + + [Theory] + [MemberData(nameof(GetPaxAndGnuTestCaseNames))] + public void ReadDataStreamOfTarGzGnu(string testCaseName) => + VerifyDataStreamOfTarGzInternal(TestTarFormat.gnu, testCaseName, copyData: false); + + [Theory] + [MemberData(nameof(GetV7TestCaseNames))] + public void ReadCopiedDataStreamOfTarGzV7(string testCaseName) => + VerifyDataStreamOfTarGzInternal(TestTarFormat.v7, testCaseName, copyData: true); + + [Theory] + [MemberData(nameof(GetUstarTestCaseNames))] + public void ReadCopiedDataStreamOfTarGzUstar(string testCaseName) => + VerifyDataStreamOfTarGzInternal(TestTarFormat.ustar, testCaseName, copyData: true); + + [Theory] + [MemberData(nameof(GetPaxAndGnuTestCaseNames))] + public void ReadCopiedDataStreamOfTarGzPax(string testCaseName) => + VerifyDataStreamOfTarGzInternal(TestTarFormat.pax, testCaseName, copyData: true); + + [Theory] + [MemberData(nameof(GetPaxAndGnuTestCaseNames))] + public void ReadCopiedDataStreamOfTarGzPaxGea(string testCaseName) => + VerifyDataStreamOfTarGzInternal(TestTarFormat.pax_gea, testCaseName, copyData: true); + + [Theory] + [MemberData(nameof(GetPaxAndGnuTestCaseNames))] + public void ReadCopiedDataStreamOfTarGzOldGnu(string testCaseName) => + VerifyDataStreamOfTarGzInternal(TestTarFormat.oldgnu, testCaseName, copyData: true); + + [Theory] + [MemberData(nameof(GetPaxAndGnuTestCaseNames))] + public void ReadCopiedDataStreamOfTarGzGnu(string testCaseName) => + VerifyDataStreamOfTarGzInternal(TestTarFormat.gnu, testCaseName, copyData: true); + + [Theory] + [MemberData(nameof(GetGoLangTarTestCaseNames))] + public void ReadDataStreamOfExternalAssetsGoLang(string testCaseName) => + VerifyDataStreamOfTarUncompressedInternal("golang_tar", testCaseName, copyData: false); + + [Theory] + [MemberData(nameof(GetNodeTarTestCaseNames))] + public void ReadDataStreamOfExternalAssetsNode(string testCaseName) => + VerifyDataStreamOfTarUncompressedInternal("node-tar", testCaseName, copyData: false); + + [Theory] + [MemberData(nameof(GetRsTarTestCaseNames))] + public void ReadDataStreamOfExternalAssetsRs(string testCaseName) => + VerifyDataStreamOfTarUncompressedInternal("tar-rs", testCaseName, copyData: false); + + [Theory] + [MemberData(nameof(GetGoLangTarTestCaseNames))] + public void ReadCopiedDataStreamOfExternalAssetsGoLang(string testCaseName) => + VerifyDataStreamOfTarUncompressedInternal("golang_tar", testCaseName, copyData: true); + + [Theory] + [MemberData(nameof(GetNodeTarTestCaseNames))] + public void ReadCopiedDataStreamOfExternalAssetsNode(string testCaseName) => + VerifyDataStreamOfTarUncompressedInternal("node-tar", testCaseName, copyData: true); + + [Theory] + [MemberData(nameof(GetRsTarTestCaseNames))] + public void ReadCopiedDataStreamOfExternalAssetsRs(string testCaseName) => + VerifyDataStreamOfTarUncompressedInternal("tar-rs", testCaseName, copyData: true); + + [Fact] + public void Throw_FifoContainsNonZeroDataSection() + { + using MemoryStream archiveStream = GetTarMemoryStream(CompressionMethod.Uncompressed, "golang_tar", "hdr-only"); + using TarReader reader = new TarReader(archiveStream); + Assert.NotNull(reader.GetNextEntry()); + Assert.NotNull(reader.GetNextEntry()); + Assert.NotNull(reader.GetNextEntry()); + Assert.NotNull(reader.GetNextEntry()); + Assert.NotNull(reader.GetNextEntry()); + Assert.NotNull(reader.GetNextEntry()); + Assert.NotNull(reader.GetNextEntry()); + Assert.NotNull(reader.GetNextEntry()); + Assert.Throws(() => reader.GetNextEntry()); + } + [Fact] - public void Read_NodeTarArchives_Successfully() + public void Throw_SingleExtendedAttributesEntryWithNoActualEntry() { - string nodeTarPath = Path.Join(Directory.GetCurrentDirectory(), "tar", "node-tar"); - foreach (string file in Directory.EnumerateFiles(nodeTarPath, "*.tar", SearchOption.AllDirectories)) + using MemoryStream archiveStream = GetTarMemoryStream(CompressionMethod.Uncompressed, "golang_tar", "pax-path-hdr"); + using TarReader reader = new TarReader(archiveStream); + Assert.Throws(() => reader.GetNextEntry()); + } + + [Theory] + [InlineData("tar-rs", "spaces")] + [InlineData("golang_tar", "v7")] + public void AllowSpacesInOctalFields(string folderName, string testCaseName) + { + using MemoryStream archiveStream = GetTarMemoryStream(CompressionMethod.Uncompressed, folderName, testCaseName); + using TarReader reader = new TarReader(archiveStream); + TarEntry entry; + while ((entry = reader.GetNextEntry()) != null) { - using FileStream sourceStream = File.Open(file, FileMode.Open, FileAccess.Read, FileShare.Read); - using var reader = new TarReader(sourceStream); + AssertExtensions.GreaterThan(entry.Checksum, 0); + AssertExtensions.GreaterThan((int)entry.Mode, 0); + } + } - TarEntry? entry = null; - while (true) - { - Exception ex = Record.Exception(() => entry = reader.GetNextEntry()); - Assert.Null(ex); + [Theory] + [InlineData("pax-multi-hdrs")] // Multiple consecutive PAX metadata entries + [InlineData("gnu-multi-hdrs")] // Multiple consecutive GNU metadata entries + [InlineData("neg-size")] // Garbage chars + [InlineData("invalid-go17")] // Many octal fields are all zero chars + [InlineData("issue11169")] // Checksum with null in the middle + [InlineData("issue10968")] // Garbage chars + [InlineData("writer-big")] // The size field contains an euro char + public void Throw_ArchivesWithRandomChars(string testCaseName) + { + using MemoryStream archiveStream = GetTarMemoryStream(CompressionMethod.Uncompressed, "golang_tar", testCaseName); + using TarReader reader = new TarReader(archiveStream); + Assert.Throws(() => reader.GetNextEntry()); + } + + [Fact] + public void GarbageEntryChecksumZeroReturnNull() + { + using MemoryStream archiveStream = GetTarMemoryStream(CompressionMethod.Uncompressed, "golang_tar", "issue12435"); + using TarReader reader = new TarReader(archiveStream); + Assert.Null(reader.GetNextEntry()); + } + + [Theory] + [InlineData("golang_tar", "gnu-nil-sparse-data")] + [InlineData("golang_tar", "gnu-nil-sparse-hole")] + [InlineData("golang_tar", "gnu-sparse-big")] + [InlineData("golang_tar", "sparse-formats")] + [InlineData("tar-rs", "sparse-1")] + [InlineData("tar-rs", "sparse")] + public void SparseEntryNotSupported(string testFolderName, string testCaseName) + { + // Currently sparse entries are not supported. + + // There are PAX archives archives in the golang folder that have extended attributes for treating a regular file as a sparse file. + // Sparse entries were created for the GNU format, so they are very rare entry types which are excluded from this test method: + // pax-nil-sparse-data, pax-nil-sparse-hole, pax-sparse-big + + using MemoryStream archiveStream = GetTarMemoryStream(CompressionMethod.Uncompressed, testFolderName, testCaseName); + using TarReader reader = new TarReader(archiveStream); + Assert.Throws(() => reader.GetNextEntry()); + } - if (entry is null) break; + [Fact] + public void DirectoryListRegularFileAndSparse() + { + using MemoryStream archiveStream = GetTarMemoryStream(CompressionMethod.Uncompressed, "golang_tar", "gnu-incremental"); + using TarReader reader = new TarReader(archiveStream); + TarEntry directoryList = reader.GetNextEntry(); + + Assert.Equal(TarEntryType.DirectoryList, directoryList.EntryType); + Assert.NotNull(directoryList.DataStream); + Assert.Equal(14, directoryList.Length); + + Assert.NotNull(reader.GetNextEntry()); // Just a regular file + + Assert.Throws(() => reader.GetNextEntry()); // Sparse + } + + [Fact] + public void PaxSizeLargerThanMaxAllowedByStream() + { + using MemoryStream archiveStream = GetTarMemoryStream(CompressionMethod.Uncompressed, "golang_tar", "writer-big-long"); + using TarReader reader = new TarReader(archiveStream); + // The extended attribute 'size' has the value 17179869184 + // Exception message: Stream length must be non-negative and less than 2^31 - 1 - origin + Assert.Throws(() => reader.GetNextEntry()); + } - ex = Record.Exception(() => entry.Name); - Assert.Null(ex); + private static void VerifyDataStreamOfTarUncompressedInternal(string testFolderName, string testCaseName, bool copyData) + { + using MemoryStream archiveStream = GetTarMemoryStream(CompressionMethod.Uncompressed, testFolderName, testCaseName); + VerifyDataStreamOfTarInternal(archiveStream, copyData); + } + + private static void VerifyDataStreamOfTarGzInternal(TestTarFormat testTarFormat, string testCaseName, bool copyData) + { + using MemoryStream archiveStream = GetTarMemoryStream(CompressionMethod.GZip, testTarFormat, testCaseName); + using GZipStream decompressor = new GZipStream(archiveStream, CompressionMode.Decompress); + VerifyDataStreamOfTarInternal(decompressor, copyData); + } - ex = Record.Exception(() => entry.Length); - Assert.Null(ex); + private static void VerifyDataStreamOfTarInternal(Stream archiveStream, bool copyData) + { + using TarReader reader = new TarReader(archiveStream); + + TarEntry entry; + + while ((entry = reader.GetNextEntry(copyData)) != null) + { + if (entry.EntryType is TarEntryType.V7RegularFile or TarEntryType.RegularFile) + { + if (entry.Length == 0) + { + Assert.Null(entry.DataStream); + } + else + { + Assert.NotNull(entry.DataStream); + Assert.Equal(entry.DataStream.Length, entry.Length); + if (copyData) + { + Assert.True(entry.DataStream.CanSeek); + Assert.Equal(0, entry.DataStream.Position); + } + } } } } 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 1c8dfeea19fce5..dda0cae56b69f8 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 @@ -252,10 +252,13 @@ public void GetNextEntry_UnseekableArchive_ReplaceDataStream_ExcludeFromDisposin } [Theory] - [InlineData(512)] - [InlineData(512 + 1)] - [InlineData(512 + 512 - 1)] - public void BlockAlignmentPadding_DoesNotAffectNextEntries(int contentSize) + [InlineData(512, false)] + [InlineData(512, true)] + [InlineData(512 + 1, false)] + [InlineData(512 + 1, true)] + [InlineData(512 + 512 - 1, false)] + [InlineData(512 + 512 - 1, true)] + public void BlockAlignmentPadding_DoesNotAffectNextEntries(int contentSize, bool copyData) { byte[] fileContents = new byte[contentSize]; Array.Fill(fileContents, 0x1); @@ -275,17 +278,17 @@ public void BlockAlignmentPadding_DoesNotAffectNextEntries(int contentSize) using var unseekable = new WrappedStream(archive, archive.CanRead, archive.CanWrite, canSeek: false); using var reader = new TarReader(unseekable); - TarEntry e = reader.GetNextEntry(); + TarEntry e = reader.GetNextEntry(copyData); Assert.Equal(contentSize, e.Length); byte[] buffer = new byte[contentSize]; while (e.DataStream.Read(buffer) > 0) ; AssertExtensions.SequenceEqual(fileContents, buffer); - e = reader.GetNextEntry(); + e = reader.GetNextEntry(copyData); Assert.Equal(0, e.Length); - e = reader.GetNextEntry(); + e = reader.GetNextEntry(copyData); Assert.Null(e); } } 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 a2e14598557c95..1dcd9326ee81ee 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 @@ -290,10 +290,13 @@ public async Task GetNextEntry_UnseekableArchive_ReplaceDataStream_ExcludeFromDi } [Theory] - [InlineData(512)] - [InlineData(512 + 1)] - [InlineData(512 + 512 - 1)] - public async Task BlockAlignmentPadding_DoesNotAffectNextEntries_Async(int contentSize) + [InlineData(512, false)] + [InlineData(512, true)] + [InlineData(512 + 1, false)] + [InlineData(512 + 1, true)] + [InlineData(512 + 512 - 1, false)] + [InlineData(512 + 512 - 1, true)] + public async Task BlockAlignmentPadding_DoesNotAffectNextEntries_Async(int contentSize, bool copyData) { byte[] fileContents = new byte[contentSize]; Array.Fill(fileContents, 0x1); @@ -313,17 +316,17 @@ public async Task BlockAlignmentPadding_DoesNotAffectNextEntries_Async(int conte using var unseekable = new WrappedStream(archive, archive.CanRead, archive.CanWrite, canSeek: false); using var reader = new TarReader(unseekable); - TarEntry e = await reader.GetNextEntryAsync(); + TarEntry e = await reader.GetNextEntryAsync(copyData); Assert.Equal(contentSize, e.Length); byte[] buffer = new byte[contentSize]; while (e.DataStream.Read(buffer) > 0) ; AssertExtensions.SequenceEqual(fileContents, buffer); - e = await reader.GetNextEntryAsync(); + e = await reader.GetNextEntryAsync(copyData); Assert.Equal(0, e.Length); - e = await reader.GetNextEntryAsync(); + e = await reader.GetNextEntryAsync(copyData); Assert.Null(e); } } diff --git a/src/libraries/System.Formats.Tar/tests/TarTestsBase.cs b/src/libraries/System.Formats.Tar/tests/TarTestsBase.cs index 04d034ce8764f8..23dc653c83a165 100644 --- a/src/libraries/System.Formats.Tar/tests/TarTestsBase.cs +++ b/src/libraries/System.Formats.Tar/tests/TarTestsBase.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; using Microsoft.DotNet.RemoteExecutor; using Xunit; @@ -88,6 +90,94 @@ public abstract partial class TarTestsBase : FileCleanupTestBase protected const string PaxEaDevMajor = "devmajor"; protected const string PaxEaDevMinor = "devminor"; + private static readonly string[] V7TestCaseNames = new[] + { + "file", + "file_hardlink", + "file_symlink", + "folder_file", + "folder_file_utf8", + "folder_subfolder_file", + "foldersymlink_folder_subfolder_file", + "many_small_files" + }; + + private static readonly string[] UstarTestCaseNames = new[] + { + "longpath_splitable_under255", + "specialfiles" }; + + private static readonly string[] PaxAndGnuTestCaseNames = new[] + { + "file_longsymlink", + "longfilename_over100_under255", + "longpath_over255" + }; + + private static readonly string[] GoLangTestCaseNames = new[] + { + "empty", + "file-and-dir", + "gnu-long-nul", + "gnu-not-utf8", + "gnu-utf8", + "gnu", + "hardlink", + "nil-uid", + "pax-bad-hdr-file", + "pax-bad-mtime-file", + "pax-global-records", + "pax-nul-path", + "pax-nul-xattrs", + "pax-pos-size-file", + "pax-records", + "pax", + "star", + "trailing-slash", + "ustar-file-devs", + "ustar-file-reg", + "ustar", + "writer", + "xattrs" + }; + + private static readonly string[] NodeTarTestCaseNames = new[] + { + "bad-cksum", + "body-byte-counts", + "dir", + "emptypax", + "file", + "global-header", + "links-invalid", + "links-strip", + "links", + "long-paths", + "long-pax", + "next-file-has-long", + "null-byte", + "path-missing", + "trailing-slash-corner-case", + "utf8" + }; + + private static readonly string[] RsTarTestCaseNames = new[] + { + "7z_long_path", + "directory", + "duplicate_dirs", + "empty_filename", + "file_times", + "link", + "pax_size", + "pax", + "pax2", + "reading_files", + "simple_missing_last_header", + "simple", + "xattrs" + }; + protected enum CompressionMethod { // Archiving only, no compression @@ -121,30 +211,41 @@ public enum TestTarFormat protected TarTestsBase() { CreateDirectoryDefaultMode = Directory.CreateDirectory(GetRandomDirPath()).UnixFileMode; // '0777 & ~umask' - UMask = ~CreateDirectoryDefaultMode & (UnixFileMode)Convert.ToInt32("777", 8); + UMask = ~CreateDirectoryDefaultMode & (UnixFileMode)Convert.ToInt32("777", + 8); } protected static string GetTestCaseUnarchivedFolderPath(string testCaseName) => - Path.Join(Directory.GetCurrentDirectory(), "unarchived", testCaseName); + Path.Join(Directory.GetCurrentDirectory(), "unarchived", + testCaseName); protected static string GetTarFilePath(CompressionMethod compressionMethod, TestTarFormat format, string testCaseName) + => GetTarFilePath(compressionMethod, format.ToString(), testCaseName); + + protected static string GetTarFilePath(CompressionMethod compressionMethod, string testFolderName, string testCaseName) { (string compressionMethodFolder, string fileExtension) = compressionMethod switch { - CompressionMethod.Uncompressed => ("tar", ".tar"), - CompressionMethod.GZip => ("targz", ".tar.gz"), + CompressionMethod.Uncompressed => ("tar", + ".tar"), + CompressionMethod.GZip => ("targz", + ".tar.gz"), _ => throw new InvalidOperationException($"Unexpected compression method: {compressionMethod}"), }; - return Path.Join(Directory.GetCurrentDirectory(), compressionMethodFolder, format.ToString(), testCaseName + fileExtension); + return Path.Join(Directory.GetCurrentDirectory(), compressionMethodFolder, testFolderName, testCaseName + fileExtension); } // MemoryStream containing the copied contents of the specified file. Meant for reading and writing. protected static MemoryStream GetTarMemoryStream(CompressionMethod compressionMethod, TestTarFormat format, string testCaseName) => - GetMemoryStream(GetTarFilePath(compressionMethod, format, testCaseName)); + GetTarMemoryStream(compressionMethod, format.ToString(), testCaseName); + + protected static MemoryStream GetTarMemoryStream(CompressionMethod compressionMethod, string testFolderName, string testCaseName) => + GetMemoryStream(GetTarFilePath(compressionMethod, testFolderName, testCaseName)); protected static string GetStrangeTarFilePath(string testCaseName) => - Path.Join(Directory.GetCurrentDirectory(), "strange", testCaseName + ".tar"); + Path.Join(Directory.GetCurrentDirectory(), "strange", + testCaseName + ".tar"); protected static MemoryStream GetStrangeTarMemoryStream(string testCaseName) => GetMemoryStream(GetStrangeTarFilePath(testCaseName)); @@ -462,5 +563,53 @@ protected void Verify_Extract(string destination, TarEntry entry, TarEntryType e AssertFileModeEquals(destination, TestPermission1); } + + public static IEnumerable GetNodeTarTestCaseNames() + { + foreach (string name in NodeTarTestCaseNames) + { + yield return new object[] { name }; + } + } + + public static IEnumerable GetGoLangTarTestCaseNames() + { + foreach (string name in GoLangTestCaseNames) + { + yield return new object[] { name }; + } + } + + public static IEnumerable GetRsTarTestCaseNames() + { + foreach (string name in RsTarTestCaseNames) + { + yield return new object[] { name }; + } + } + + public static IEnumerable GetV7TestCaseNames() + { + foreach (string name in V7TestCaseNames) + { + yield return new object[] { name }; + } + } + + public static IEnumerable GetUstarTestCaseNames() + { + foreach (string name in UstarTestCaseNames.Concat(V7TestCaseNames)) + { + yield return new object[] { name }; + } + } + + public static IEnumerable GetPaxAndGnuTestCaseNames() + { + foreach (string name in UstarTestCaseNames.Concat(V7TestCaseNames).Concat(PaxAndGnuTestCaseNames)) + { + yield return new object[] { name }; + } + } } }