From bd7cd72b3c9a96d83fd8f1802e45a1f3e866d4fa Mon Sep 17 00:00:00 2001 From: carlossanlop <1175054+carlossanlop@users.noreply.github.com> Date: Sat, 28 May 2022 12:44:18 -0700 Subject: [PATCH 01/75] ref: TarEntry.ExtractToFileAsync --- src/libraries/System.Formats.Tar/ref/System.Formats.Tar.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libraries/System.Formats.Tar/ref/System.Formats.Tar.cs b/src/libraries/System.Formats.Tar/ref/System.Formats.Tar.cs index 56fb1f8c4cdb30..c090458842ed50 100644 --- a/src/libraries/System.Formats.Tar/ref/System.Formats.Tar.cs +++ b/src/libraries/System.Formats.Tar/ref/System.Formats.Tar.cs @@ -48,6 +48,7 @@ internal TarEntry() { } public string Name { get { throw null; } set { } } public int Uid { get { throw null; } set { } } public void ExtractToFile(string destinationFileName, bool overwrite) { } + public System.Threading.Tasks.Task ExtractToFileAsync(string destinationFileName, bool overwrite, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public override string ToString() { throw null; } } public enum TarEntryFormat From dcc070a08ed4ea7ffb7e49b3d05ec763cd9801ea Mon Sep 17 00:00:00 2001 From: carlossanlop <1175054+carlossanlop@users.noreply.github.com> Date: Sat, 28 May 2022 12:44:37 -0700 Subject: [PATCH 02/75] src: Implement TarEntry.ExtractToFileAsync --- .../src/System/Formats/Tar/TarEntry.cs | 115 +++++++++++++----- 1 file changed, 86 insertions(+), 29 deletions(-) diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs index 3347700db32aad..fa061e23cc1dcb 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs @@ -3,6 +3,8 @@ using System.Diagnostics; using System.IO; +using System.Threading; +using System.Threading.Tasks; using Microsoft.Win32.SafeHandles; namespace System.Formats.Tar @@ -226,29 +228,33 @@ public void ExtractToFile(string destinationFileName, bool overwrite) ExtractToFileInternal(destinationFileName, linkTargetPath: null, overwrite); } - // /// - // /// Asynchronously extracts the current entry to the filesystem. - // /// - // /// The path to the destination file. - // /// if this method should overwrite any existing filesystem object located in the path; to prevent overwriting. - // /// The token to monitor for cancellation requests. The default value is . - // /// A task that represents the asynchronous extraction operation. - // /// Files of type , or can only be extracted in Unix platforms. - // /// Elevation is required to extract a or to disk. - // /// is or empty. - // /// The parent directory of does not exist. - // /// -or- - // /// is and a file already exists in . - // /// -or- - // /// A directory exists with the same name as . - // /// -or- - // /// An I/O problem occurred. - // /// Attempted to extract an unsupported entry type. - // /// Operation not permitted due to insufficient permissions. - // public Task ExtractToFileAsync(string destinationFileName, bool overwrite, CancellationToken cancellationToken = default) - // { - // throw new NotImplementedException(); - // } + /// + /// Asynchronously extracts the current entry to the filesystem. + /// + /// The path to the destination file. + /// if this method should overwrite any existing filesystem object located in the path; to prevent overwriting. + /// The token to monitor for cancellation requests. The default value is . + /// A task that represents the asynchronous extraction operation. + /// Files of type , or can only be extracted in Unix platforms. + /// Elevation is required to extract a or to disk. + /// is or empty. + /// The parent directory of does not exist. + /// -or- + /// is and a file already exists in . + /// -or- + /// A directory exists with the same name as . + /// -or- + /// An I/O problem occurred. + /// Attempted to extract an unsupported entry type. + /// Operation not permitted due to insufficient permissions. + public Task ExtractToFileAsync(string destinationFileName, bool overwrite, CancellationToken cancellationToken = default) + { + if (EntryType is TarEntryType.SymbolicLink or TarEntryType.HardLink) + { + throw new InvalidOperationException(string.Format(SR.TarEntryTypeNotSupportedForExtracting, EntryType)); + } + return ExtractToFileInternalAsync(destinationFileName, linkTargetPath: null, overwrite, cancellationToken); + } /// /// The data section of this entry. If the does not support containing data, then returns . @@ -357,6 +363,35 @@ private void ExtractToFileInternal(string filePath, string? linkTargetPath, bool VerifyPathsForEntryType(filePath, linkTargetPath, overwrite); + if (EntryType is TarEntryType.RegularFile or TarEntryType.V7RegularFile or TarEntryType.ContiguousFile) + { + ExtractAsRegularFile(filePath); + } + else + { + CreateNonRegularFile(filePath, linkTargetPath); + } + } + + // Asynchronously extracts the current entry into the filesystem, regardless of the entry type. + private Task ExtractToFileInternalAsync(string filePath, string? linkTargetPath, bool overwrite, CancellationToken cancellationToken) + { + ArgumentException.ThrowIfNullOrEmpty(filePath); + + VerifyPathsForEntryType(filePath, linkTargetPath, overwrite); + + if (EntryType is TarEntryType.RegularFile or TarEntryType.V7RegularFile or TarEntryType.ContiguousFile) + { + return ExtractAsRegularFileAsync(filePath, cancellationToken); + } + CreateNonRegularFile(filePath, linkTargetPath); + return Task.CompletedTask; + } + + private void CreateNonRegularFile(string filePath, string? linkTargetPath) + { + Debug.Assert(EntryType is not TarEntryType.RegularFile or TarEntryType.V7RegularFile or TarEntryType.ContiguousFile); + switch (EntryType) { case TarEntryType.Directory: @@ -364,12 +399,6 @@ private void ExtractToFileInternal(string filePath, string? linkTargetPath, bool Directory.CreateDirectory(filePath); break; - case TarEntryType.RegularFile: - case TarEntryType.V7RegularFile: - case TarEntryType.ContiguousFile: - ExtractAsRegularFile(filePath); - break; - case TarEntryType.SymbolicLink: Debug.Assert(!string.IsNullOrEmpty(linkTargetPath)); FileInfo link = new(filePath); @@ -494,6 +523,34 @@ private void ExtractAsRegularFile(string destinationFileName) ArchivingUtils.AttemptSetLastWriteTime(destinationFileName, ModificationTime); } + // Asynchronously extracts the current entry as a regular file into the specified destination. + // The assumption is that at this point there is no preexisting file or directory in that destination. + private async Task ExtractAsRegularFileAsync(string destinationFileName, CancellationToken cancellationToken) + { + Debug.Assert(!Path.Exists(destinationFileName)); + + FileStreamOptions fileStreamOptions = new FileStreamOptions() + { + Access = FileAccess.Write, + Mode = FileMode.CreateNew, + Share = FileShare.None, + Options = FileOptions.Asynchronous, + PreallocationSize = Length, + }; + // Rely on FileStream's ctor for further checking destinationFileName parameter + using (FileStream fs = new FileStream(destinationFileName, fileStreamOptions)) + { + if (DataStream != null) + { + // Important: The DataStream will be written from its current position + await DataStream.CopyToAsync(fs, cancellationToken).ConfigureAwait(false); + } + SetModeOnFile(fs.SafeFileHandle, destinationFileName); + } + + ArchivingUtils.AttemptSetLastWriteTime(destinationFileName, ModificationTime); + } + // Abstract method that extracts the current entry when it is a block device. partial void ExtractAsBlockDevice(string destinationFileName); From a6024cf4f73ad17cadd4d91f06642db5ff536e3c Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Thu, 9 Jun 2022 18:11:48 -0700 Subject: [PATCH 03/75] ref: Add TarReader.GetNextEntryAsync --- src/libraries/System.Formats.Tar/ref/System.Formats.Tar.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libraries/System.Formats.Tar/ref/System.Formats.Tar.cs b/src/libraries/System.Formats.Tar/ref/System.Formats.Tar.cs index c090458842ed50..7767bc7488fb1f 100644 --- a/src/libraries/System.Formats.Tar/ref/System.Formats.Tar.cs +++ b/src/libraries/System.Formats.Tar/ref/System.Formats.Tar.cs @@ -92,6 +92,7 @@ public sealed partial class TarReader : System.IDisposable public TarReader(System.IO.Stream archiveStream, bool leaveOpen = false) { } public void Dispose() { } public System.Formats.Tar.TarEntry? GetNextEntry(bool copyData = false) { throw null; } + public System.Threading.Tasks.ValueTask GetNextEntryAsync(bool copyData = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } } public sealed partial class TarWriter : System.IDisposable { From a36e1dc04dca437cbf4a85dbda0dd8bfbf850621 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Thu, 23 Jun 2022 15:24:51 -0700 Subject: [PATCH 04/75] src: Implement TarReader.GetNextEntryAsync --- .../src/System/Formats/Tar/TarHeader.Read.cs | 207 +++++++++++++ .../src/System/Formats/Tar/TarHelpers.cs | 56 ++++ .../src/System/Formats/Tar/TarReader.cs | 292 ++++++++++++++++-- 3 files changed, 537 insertions(+), 18 deletions(-) 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 d834ab2f10462f..f3e5ad6ca62f81 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 @@ -7,6 +7,8 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using System.Text; +using System.Threading; +using System.Threading.Tasks; namespace System.Formats.Tar { @@ -69,6 +71,59 @@ internal bool TryGetNextHeader(Stream archiveStream, bool copyData) } } + // 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 async ValueTask TryGetNextHeaderAsync(Stream archiveStream, bool copyData, CancellationToken cancellationToken) + { + // The four supported formats have a header that fits in the default record size + IMemoryOwner rented = MemoryPool.Shared.Rent(minBufferSize: TarHelpers.RecordSize); + + Memory buffer = rented.Memory.Slice(0, TarHelpers.RecordSize); // minBufferSize means the array could've been larger + + await archiveStream.ReadExactlyAsync(buffer, cancellationToken).ConfigureAwait(false); + + try + { + // Confirms if v7 or pax, or tentatively selects ustar + if (!TryReadCommonAttributes(buffer.Span)) + { + return false; + } + + // Confirms if gnu, or tentatively selects ustar + ReadMagicAttribute(buffer.Span); + + if (_format != TarEntryFormat.V7) + { + // Confirms if gnu + ReadVersionAttribute(buffer.Span); + + // Fields that ustar, pax and gnu share identically + ReadPosixAndGnuSharedAttributes(buffer.Span); + + Debug.Assert(_format is TarEntryFormat.Ustar or TarEntryFormat.Pax or TarEntryFormat.Gnu); + if (_format == TarEntryFormat.Ustar) + { + ReadUstarAttributes(buffer.Span); + } + else if (_format == TarEntryFormat.Gnu) + { + ReadGnuAttributes(buffer.Span); + } + // In PAX, there is nothing to read in this section (empty space) + } + + await ProcessDataBlockAsync(archiveStream, copyData, cancellationToken).ConfigureAwait(false); + + return true; + } + finally + { + rented.Dispose(); + } + } + // Reads the elements from the passed dictionary, which comes from the previous extended attributes entry, // and inserts or replaces those elements into the current header's dictionary. // If any of the dictionary entries use the name of a standard attribute, that attribute's value gets replaced with the one from the dictionary. @@ -218,6 +273,69 @@ private void ProcessDataBlock(Stream archiveStream, bool copyData) } } + // Asynchronously determines what kind of stream needs to be saved for the data section. + // - Metadata typeflag entries (Extended Attributes and Global Extended Attributes in PAX, LongLink and LongPath in GNU) + // 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 async ValueTask ProcessDataBlockAsync(Stream archiveStream, bool copyData, CancellationToken cancellationToken) + { + bool skipBlockAlignmentPadding = true; + + switch (_typeFlag) + { + case TarEntryType.ExtendedAttributes or TarEntryType.GlobalExtendedAttributes: + await ReadExtendedAttributesBlockAsync(archiveStream, cancellationToken).ConfigureAwait(false); + break; + case TarEntryType.LongLink or TarEntryType.LongPath: + await ReadGnuLongPathDataBlockAsync(archiveStream, cancellationToken).ConfigureAwait(false); + break; + case TarEntryType.BlockDevice: + case TarEntryType.CharacterDevice: + case TarEntryType.Directory: + case TarEntryType.Fifo: + case TarEntryType.HardLink: + case TarEntryType.SymbolicLink: + // No data section + break; + case TarEntryType.RegularFile: + case TarEntryType.V7RegularFile: // Treated as regular file + case TarEntryType.ContiguousFile: // Treated as regular file + case TarEntryType.DirectoryList: // Contains the list of filesystem entries in the data section + case TarEntryType.MultiVolume: // Contains portion of a file + case TarEntryType.RenamedOrSymlinked: // Might contain data + case TarEntryType.SparseFile: // Contains portion of a file + case TarEntryType.TapeVolume: // Might contain data + default: // Unrecognized entry types could potentially have a data section + _dataStream = await GetDataStreamAsync(archiveStream, copyData, cancellationToken).ConfigureAwait(false); + if (_dataStream is SeekableSubReadStream) + { + await TarHelpers.AdvanceStreamAsync(archiveStream, _size, cancellationToken).ConfigureAwait(false); + } + else if (_dataStream is SubReadStream) + { + // This stream gives the user the chance to optionally read the data section + // when the underlying archive stream is unseekable + skipBlockAlignmentPadding = false; + } + + break; + } + + if (skipBlockAlignmentPadding) + { + if (_size > 0) + { + await TarHelpers.SkipBlockAlignmentPaddingAsync(archiveStream, _size, cancellationToken).ConfigureAwait(false); + } + + if (archiveStream.CanSeek) + { + _endOfHeaderAndDataAndBlockAlignment = archiveStream.Position; + } + } + } + // Returns a stream that represents the data section of the current header. // If copyData is true, then a total number of _size bytes will be copied to a new MemoryStream, which is then returned. // Otherwise, if the archive stream is seekable, returns a seekable wrapper stream. @@ -241,6 +359,29 @@ private void ProcessDataBlock(Stream archiveStream, bool copyData) : new SubReadStream(archiveStream, 0, _size); } + // Asynchronously returns a stream that represents the data section of the current header. + // If copyData is true, then a total number of _size bytes will be copied to a new MemoryStream, which is then returned. + // Otherwise, if the archive stream is seekable, returns a seekable wrapper stream. + // Otherwise, it returns an unseekable wrapper stream. + private async ValueTask GetDataStreamAsync(Stream archiveStream, bool copyData, CancellationToken cancellationToken) + { + if (_size == 0) + { + return null; + } + + if (copyData) + { + MemoryStream copiedData = new MemoryStream(); + await TarHelpers.CopyBytesAsync(archiveStream, copiedData, _size, cancellationToken).ConfigureAwait(false); + return copiedData; + } + + return archiveStream.CanSeek + ? new SeekableSubReadStream(archiveStream, archiveStream.Position, _size) + : new SubReadStream(archiveStream, 0, _size); + } + // Attempts to read the fields shared by all formats and stores them in their expected data type. // Throws if any data type conversion fails. // Returns true on success, false if checksum is zero. @@ -447,6 +588,45 @@ private void ReadExtendedAttributesBlock(Stream archiveStream) } } + // Asynchronously collects the extended attributes found in the data section of a PAX entry of type 'x' or 'g'. + // Throws if end of stream is reached or if an attribute is malformed. + private async ValueTask ReadExtendedAttributesBlockAsync(Stream archiveStream, CancellationToken cancellationToken) + { + Debug.Assert(_typeFlag is TarEntryType.ExtendedAttributes or TarEntryType.GlobalExtendedAttributes); + + // Regardless of the size, this entry should always have a valid dictionary object + _extendedAttributes ??= new Dictionary(); + + if (_size == 0) + { + return; + } + + // It is not expected that the extended attributes data section will be longer than int.MaxValue, considering + // 4096 is a common max path length, and also the size field is 12 bytes long, which is under int.MaxValue. + if (_size > int.MaxValue) + { + throw new InvalidOperationException(string.Format(SR.TarSizeFieldTooLargeForExtendedAttribute, _typeFlag.ToString())); + } + + byte[] buffer = new byte[(int)_size]; + await archiveStream.ReadExactlyAsync(buffer, cancellationToken).ConfigureAwait(false); + + string dataAsString = TarHelpers.GetTrimmedUtf8String(buffer); + + using StringReader reader = new(dataAsString); + + while (TryGetNextExtendedAttribute(reader, out string? key, out string? value)) + { + _extendedAttributes ??= new Dictionary(); + if (_extendedAttributes.ContainsKey(key)) + { + throw new FormatException(string.Format(SR.TarDuplicateExtendedAttribute, _name)); + } + _extendedAttributes.Add(key, value); + } + } + // Reads the long path found in the data section of a GNU entry of type 'K' or 'L' // and replaces Name or LinkName, respectively, with the found string. // Throws if end of stream is reached. @@ -474,6 +654,33 @@ private void ReadGnuLongPathDataBlock(Stream archiveStream) } } + // Asynchronously reads the long path found in the data section of a GNU entry of type 'K' or 'L' + // and replaces Name or LinkName, respectively, with the found string. + // Throws if end of stream is reached. + private async ValueTask ReadGnuLongPathDataBlockAsync(Stream archiveStream, CancellationToken cancellationToken) + { + Debug.Assert(_typeFlag is TarEntryType.LongLink or TarEntryType.LongPath); + + if (_size == 0) + { + return; + } + + byte[] buffer = new byte[(int)_size]; + await archiveStream.ReadExactlyAsync(buffer, cancellationToken).ConfigureAwait(false); + + string longPath = TarHelpers.GetTrimmedUtf8String(buffer); + + if (_typeFlag == TarEntryType.LongLink) + { + _linkName = longPath; + } + else if (_typeFlag == TarEntryType.LongPath) + { + _name = longPath; + } + } + // Tries to collect the next extended attribute from the string wrapped by the specified reader. // Extended attributes are saved in the ISO/IEC 10646-1:2000 standard UTF-8 encoding format. // https://pubs.opengroup.org/onlinepubs/9699919799/utilities/pax.html 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 0f4640d42143bc..6395234bc5d612 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 @@ -6,6 +6,9 @@ using System.Globalization; using System.IO; using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System; namespace System.Formats.Tar { @@ -46,6 +49,30 @@ internal static void AdvanceStream(Stream archiveStream, long bytesToDiscard) } } + // Asynchronously helps advance the stream a total number of bytes larger than int.MaxValue. + internal static async ValueTask AdvanceStreamAsync(Stream archiveStream, long bytesToDiscard, CancellationToken cancellationToken) + { + if (archiveStream.CanSeek) + { + archiveStream.Position += bytesToDiscard; + } + else if (bytesToDiscard > 0) + { + byte[] buffer = ArrayPool.Shared.Rent(minimumLength: MaxBufferLength); + while (bytesToDiscard > 0) + { + int currentLengthToRead = (int)Math.Min(MaxBufferLength, bytesToDiscard); + int bytesRead = await archiveStream.ReadAsync(buffer.AsMemory(0, currentLengthToRead), cancellationToken).ConfigureAwait(false); + if (bytesRead != currentLengthToRead) + { + throw new EndOfStreamException(); + } + bytesToDiscard -= currentLengthToRead; + } + ArrayPool.Shared.Return(buffer); + } + } + // Helps copy a specific number of bytes from one stream into another. internal static void CopyBytes(Stream origin, Stream destination, long bytesToCopy) { @@ -63,6 +90,25 @@ internal static void CopyBytes(Stream origin, Stream destination, long bytesToCo ArrayPool.Shared.Return(buffer); } + // Asynchronously helps copy a specific number of bytes from one stream into another. + internal static async ValueTask CopyBytesAsync(Stream origin, Stream destination, long bytesToCopy, CancellationToken cancellationToken) + { + byte[] buffer = ArrayPool.Shared.Rent(minimumLength: MaxBufferLength); + while (bytesToCopy > 0) + { + int currentLengthToRead = (int)Math.Min(MaxBufferLength, bytesToCopy); + Memory memory = buffer.AsMemory(0, currentLengthToRead); + int bytesRead = await origin.ReadAsync(memory, cancellationToken).ConfigureAwait(false); + if (bytesRead != currentLengthToRead) + { + throw new EndOfStreamException(); + } + await destination.WriteAsync(memory, cancellationToken).ConfigureAwait(false); + bytesToCopy -= currentLengthToRead; + } + ArrayPool.Shared.Return(buffer); + } + // Returns the number of bytes until the next multiple of the record size. internal static int CalculatePadding(long size) { @@ -222,6 +268,16 @@ internal static int SkipBlockAlignmentPadding(Stream archiveStream, long size) return bytesToSkip; } + // After the file contents, there may be zero or more null characters, + // which exist to ensure the data is aligned to the record size. + // Asynchronously skip them and set the stream position to the first byte of the next entry. + internal static async ValueTask SkipBlockAlignmentPaddingAsync(Stream archiveStream, long size, CancellationToken cancellationToken) + { + int bytesToSkip = CalculatePadding(size); + await AdvanceStreamAsync(archiveStream, bytesToSkip, cancellationToken).ConfigureAwait(false); + return bytesToSkip; + } + // Throws if the specified entry type is not supported for the specified format. internal static void ThrowIfEntryTypeNotSupported(TarEntryType entryType, TarEntryFormat 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 3855bd8d317fea..671772257eb62f 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 @@ -4,6 +4,8 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; +using System.Threading; +using System.Threading.Tasks; namespace System.Formats.Tar { @@ -122,18 +124,64 @@ public void Dispose() return null; } - // /// - // /// Asynchronously retrieves the next entry from the archive stream. - // /// - // /// Set it to to copy the data of the entry into a new . This is helpful when the underlying archive stream is unseekable, and the data needs to be accessed later. - // /// Set it to if the data should not be copied into a new stream. If the underlying stream is unseekable, the user has the responsibility of reading and processing the immediately after calling this method. - // /// The default value is . - // /// The token to monitor for cancellation requests. The default value is . - // /// A value task containing a instance if a valid entry was found, or if the end of the archive has been reached. - // public ValueTask GetNextEntryAsync(bool copyData = false, CancellationToken cancellationToken = default) - // { - // throw new NotImplementedException(); - // } + /// + /// Asynchronously retrieves the next entry from the archive stream. + /// + /// Set it to to copy the data of the entry into a new . This is helpful when the underlying archive stream is unseekable, and the data needs to be accessed later. + /// Set it to if the data should not be copied into a new stream. If the underlying stream is unseekable, the user has the responsibility of reading and processing the immediately after calling this method. + /// The default value is . + /// The token to monitor for cancellation requests. The default value is . + /// A value task containing a instance if a valid entry was found, or if the end of the archive has been reached. + /// The archive is malformed. + /// -or- + /// The archive contains entries in different formats. + /// -or- + /// More than one Global Extended Attributes Entry was found in the current archive. + /// -or- + /// Two or more Extended Attributes entries were found consecutively in the current archive. + /// An I/O problem occurred. + public async ValueTask GetNextEntryAsync(bool copyData = false, CancellationToken cancellationToken = default) + { + if (_reachedEndMarkers) + { + // Avoid advancing the stream if we already found the end of the archive. + return null; + } + + Debug.Assert(_archiveStream.CanRead); + + if (_archiveStream.CanSeek && _archiveStream.Length == 0) + { + // Attempting to get the next entry on an empty tar stream + return null; + } + + await AdvanceDataStreamIfNeededAsync(cancellationToken).ConfigureAwait(false); + + (bool result, TarHeader header) = await TryGetNextEntryHeaderAsync(copyData, cancellationToken).ConfigureAwait(false); + if (result) + { + if (!_readFirstEntry) + { + _readFirstEntry = true; + } + + TarEntry entry = header._format switch + { + TarEntryFormat.Pax => new PaxTarEntry(header, this), + TarEntryFormat.Gnu => new GnuTarEntry(header, this), + TarEntryFormat.Ustar => new UstarTarEntry(header, this), + TarEntryFormat.V7 or TarEntryFormat.Unknown or _ => new V7TarEntry(header, this), + }; + + _previouslyReadEntry = entry; + PreserveDataStreamForDisposalIfNeeded(entry); + return entry; + } + + _reachedEndMarkers = true; + return null; + } // Moves the underlying archive stream position pointer to the beginning of the next header. internal void AdvanceDataStreamIfNeeded() @@ -175,6 +223,46 @@ internal void AdvanceDataStreamIfNeeded() } } + // Asynchronously moves the underlying archive stream position pointer to the beginning of the next header. + internal async ValueTask AdvanceDataStreamIfNeededAsync(CancellationToken cancellationToken) + { + if (_previouslyReadEntry == null) + { + return; + } + + if (_archiveStream.CanSeek) + { + Debug.Assert(_previouslyReadEntry._header._endOfHeaderAndDataAndBlockAlignment > 0); + _archiveStream.Position = _previouslyReadEntry._header._endOfHeaderAndDataAndBlockAlignment; + } + else if (_previouslyReadEntry._header._size > 0) + { + // When working with seekable streams, every time we return an entry, we avoid advancing the pointer beyond the data section + // This is so the user can read the data if desired. But if the data was not read by the user, we need to advance the pointer + // here until it's located at the beginning of the next entry header. + // This should only be done if the previous entry came from a TarReader and it still had its original SubReadStream or SeekableSubReadStream. + + if (_previouslyReadEntry._header._dataStream is not SubReadStream dataStream) + { + return; + } + + if (!dataStream.HasReachedEnd) + { + // If the user did not advance the position, we need to make sure the position + // pointer is located at the beginning of the next header. + if (dataStream.Position < (_previouslyReadEntry._header._size - 1)) + { + long bytesToSkip = _previouslyReadEntry._header._size - dataStream.Position; + await TarHelpers.AdvanceStreamAsync(_archiveStream, bytesToSkip, cancellationToken).ConfigureAwait(false); + await TarHelpers.SkipBlockAlignmentPaddingAsync(_archiveStream, _previouslyReadEntry._header._size, cancellationToken).ConfigureAwait(false); + dataStream.HasReachedEnd = true; // Now the pointer is beyond the limit, so any read attempts should throw + } + } + } + } + // Disposes the current instance. // If 'disposing' is 'false', the method was called from the finalizer. private void Dispose(bool disposing) @@ -249,9 +337,61 @@ private bool TryGetNextEntryHeader(out TarHeader header, bool copyData) return true; } - // When an extended attributes entry is retrieved, we need to collect the key-value pairs from the data section in this first header, - // then retrieve the next header, and save the collected kvps in that second header's extended attributes dictionary. - // Finally, we return the second header, which is what we will give to the user as an entry. + // Asynchronously attempts to read the next tar archive entry header. + // Returns true if an entry header was collected successfully, false otherwise. + // An entry header represents any typeflag that is contains metadata. + // Metadata typeflags: ExtendedAttributes, GlobalExtendedAttributes, LongLink, LongPath. + // Metadata typeflag entries get handled internally by this method until a valid header entry can be returned. + private async ValueTask<(bool, TarHeader)> TryGetNextEntryHeaderAsync(bool copyData, CancellationToken cancellationToken) + { + Debug.Assert(!_reachedEndMarkers); + + TarHeader header = default; + + if (!header.TryGetNextHeader(_archiveStream, copyData)) + { + return (false, default); + } + + // If a metadata typeflag entry is retrieved, handle it here, then read the next entry + + // PAX metadata + if (header._typeFlag is TarEntryType.ExtendedAttributes) + { + (bool paxResult, TarHeader mainHeader) = await TryProcessExtendedAttributesHeaderAsync(header, copyData, cancellationToken).ConfigureAwait(false); + if (!paxResult) + { + return (false, default); + } + header = mainHeader; + } + // GNU metadata + else if (header._typeFlag is TarEntryType.LongLink or TarEntryType.LongPath) + { + (bool gnuResult, TarHeader mainHeader) = await TryProcessGnuMetadataHeaderAsync(header, copyData, cancellationToken).ConfigureAwait(false); + if (!gnuResult) + { + return (false, default); + } + header = mainHeader; + } + + // Common fields should always acquire a value + Debug.Assert(header._name != null); + Debug.Assert(header._linkName != null); + + // Initialize non-common string fields if necessary + header._magic ??= string.Empty; + header._version ??= string.Empty; + header._gName ??= string.Empty; + header._uName ??= string.Empty; + header._prefix ??= string.Empty; + + return (true, header); + } + + // Tries to read the contents of the PAX metadata entry as extended attributes, tries to also read the actual entry that follows, + // and returns the actual entry with the processed extended attributes saved in the _extendedAttributes dictionary. private bool TryProcessExtendedAttributesHeader(TarHeader extendedAttributesHeader, bool copyData, out TarHeader actualHeader) { actualHeader = default; @@ -280,9 +420,45 @@ TarEntryType.LongLink or return true; } - // When a GNU metadata entry is retrieved, we need to read the long link or long path from the data section, - // then collect the next header, replace the long link or long path on that second header. - // Finally, we return the second header, which is what we will give to the user as an entry. + // Asynchronously tries to read the contents of the PAX metadata entry as extended attributes, tries to also read the actual entry that follows, + // and returns the actual entry with the processed extended attributes saved in the _extendedAttributes dictionary. + private async ValueTask<(bool, TarHeader)> TryProcessExtendedAttributesHeaderAsync(TarHeader extendedAttributesHeader, bool copyData, CancellationToken cancellationToken) + { + TarHeader actualHeader = default; + actualHeader._format = TarEntryFormat.Pax; + + // Now get the actual entry + bool result = await actualHeader.TryGetNextHeaderAsync(_archiveStream, copyData, cancellationToken).ConfigureAwait(false); + if (!result) + { + return (false, default); + } + + // We're currently processing an extended attributes header, so we can never have two extended entries in a row + if (actualHeader._typeFlag is TarEntryType.GlobalExtendedAttributes or + TarEntryType.ExtendedAttributes or + TarEntryType.LongLink or + TarEntryType.LongPath) + { + await ValueTask.FromException(new FormatException(string.Format(SR.TarUnexpectedMetadataEntry, actualHeader._typeFlag, TarEntryType.ExtendedAttributes))).ConfigureAwait(false); + } + + // Can't have two extended attribute metadata entries in a row + if (actualHeader._typeFlag is TarEntryType.ExtendedAttributes) + { + throw new FormatException(string.Format(SR.TarUnexpectedMetadataEntry, TarEntryType.ExtendedAttributes, TarEntryType.ExtendedAttributes)); + } + + Debug.Assert(extendedAttributesHeader._extendedAttributes != null); + + // Replace all the attributes representing standard fields with the extended ones, if any + actualHeader.ReplaceNormalAttributesWithExtended(extendedAttributesHeader._extendedAttributes); + + return (true, actualHeader); + } + + // Tries to read the contents of the GNU metadata entry, then tries to read the next entry, which could either be another GNU metadata entry + // or the actual entry. Processes them all and returns the actual entry updating its path and/or linkpath fields as needed. private bool TryProcessGnuMetadataHeader(TarHeader header, bool copyData, out TarHeader finalHeader) { finalHeader = default; @@ -359,6 +535,86 @@ private bool TryProcessGnuMetadataHeader(TarHeader header, bool copyData, out Ta return true; } + // Asynchronously tries to read the contents of the GNU metadata entry, then tries to read the next entry, which could either be another GNU metadata entry + // or the actual entry. Processes them all and returns the actual entry updating its path and/or linkpath fields as needed. + private async ValueTask<(bool, TarHeader)> TryProcessGnuMetadataHeaderAsync(TarHeader header, bool copyData, CancellationToken cancellationToken) + { + TarHeader secondHeader = default; + secondHeader._format = TarEntryFormat.Gnu; + + // Get the second entry, which is the actual entry + bool result1 = await secondHeader.TryGetNextHeaderAsync(_archiveStream, copyData, cancellationToken).ConfigureAwait(false); + if (!result1) + { + return (false, default); + } + + // Can't have two identical metadata entries in a row + if (secondHeader._typeFlag == header._typeFlag) + { + throw new FormatException(string.Format(SR.TarUnexpectedMetadataEntry, secondHeader._typeFlag, header._typeFlag)); + } + + TarHeader finalHeader; + + // It's possible to have the two different metadata entries in a row + if ((header._typeFlag is TarEntryType.LongLink && secondHeader._typeFlag is TarEntryType.LongPath) || + (header._typeFlag is TarEntryType.LongPath && secondHeader._typeFlag is TarEntryType.LongLink)) + { + TarHeader thirdHeader = default; + thirdHeader._format = TarEntryFormat.Gnu; + + // Get the third entry, which is the actual entry + bool result2 = await thirdHeader.TryGetNextHeaderAsync(_archiveStream, copyData, cancellationToken).ConfigureAwait(false); + if (!result2) + { + return (false, default); + } + + // Can't have three GNU metadata entries in a row + if (thirdHeader._typeFlag is TarEntryType.LongLink or TarEntryType.LongPath) + { + throw new FormatException(string.Format(SR.TarUnexpectedMetadataEntry, thirdHeader._typeFlag, secondHeader._typeFlag)); + } + + if (header._typeFlag is TarEntryType.LongLink) + { + Debug.Assert(header._linkName != null); + Debug.Assert(secondHeader._name != null); + + thirdHeader._linkName = header._linkName; + thirdHeader._name = secondHeader._name; + } + else if (header._typeFlag is TarEntryType.LongPath) + { + Debug.Assert(header._name != null); + Debug.Assert(secondHeader._linkName != null); + thirdHeader._name = header._name; + thirdHeader._linkName = secondHeader._linkName; + } + + finalHeader = thirdHeader; + } + // Only one metadata entry was found + else + { + if (header._typeFlag is TarEntryType.LongLink) + { + Debug.Assert(header._linkName != null); + secondHeader._linkName = header._linkName; + } + else if (header._typeFlag is TarEntryType.LongPath) + { + Debug.Assert(header._name != null); + secondHeader._name = header._name; + } + + finalHeader = secondHeader; + } + + return (true, finalHeader); + } + // If the current entry contains a non-null DataStream, that stream gets added to an internal // list of streams that need to be disposed when this TarReader instance gets disposed. private void PreserveDataStreamForDisposalIfNeeded(TarEntry entry) From cb2926e6939b6fa90a26b49f780ccbec95711f58 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Thu, 23 Jun 2022 15:25:11 -0700 Subject: [PATCH 05/75] ref: Add TarWriter.WriteEntryAsync --- src/libraries/System.Formats.Tar/ref/System.Formats.Tar.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/libraries/System.Formats.Tar/ref/System.Formats.Tar.cs b/src/libraries/System.Formats.Tar/ref/System.Formats.Tar.cs index 7767bc7488fb1f..39171da929d816 100644 --- a/src/libraries/System.Formats.Tar/ref/System.Formats.Tar.cs +++ b/src/libraries/System.Formats.Tar/ref/System.Formats.Tar.cs @@ -103,6 +103,8 @@ public TarWriter(System.IO.Stream archiveStream, System.Formats.Tar.TarEntryForm public void Dispose() { } public void WriteEntry(System.Formats.Tar.TarEntry entry) { } public void WriteEntry(string fileName, string? entryName) { } + public System.Threading.Tasks.Task WriteEntryAsync(System.Formats.Tar.TarEntry entry, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public System.Threading.Tasks.Task WriteEntryAsync(string fileName, string? entryName, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } } public sealed partial class UstarTarEntry : System.Formats.Tar.PosixTarEntry { From 5f2249c924dd5d59a99313223b35fb2e24e8b332 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Thu, 23 Jun 2022 15:25:23 -0700 Subject: [PATCH 06/75] src: Implement TarWriter.WriteEntryAsync --- .../src/System/Formats/Tar/TarHeader.Write.cs | 193 ++++++++++++++++++ .../src/System/Formats/Tar/TarWriter.Unix.cs | 89 ++++++++ .../System/Formats/Tar/TarWriter.Windows.cs | 68 ++++++ .../src/System/Formats/Tar/TarWriter.cs | 144 +++++++++---- 4 files changed, 449 insertions(+), 45 deletions(-) diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs index b6ac2470a1e440..6c005db213e03a 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 @@ -5,6 +5,8 @@ using System.Diagnostics; using System.IO; using System.Text; +using System.Threading; +using System.Threading.Tasks; namespace System.Formats.Tar { @@ -42,6 +44,24 @@ internal void WriteAsV7(Stream archiveStream, Span buffer) } } + // Asynchronously writes the current header as a V7 entry into the archive stream. + internal async Task WriteAsV7Async(Stream archiveStream, Memory buffer, CancellationToken cancellationToken) + { + long actualLength = GetTotalDataBytesToWrite(); + TarEntryType actualEntryType = GetCorrectTypeFlagForFormat(TarEntryFormat.V7); + + int checksum = WriteName(buffer.Span, out _); + checksum += WriteCommonFields(buffer.Span, actualLength, actualEntryType); + WriteChecksum(checksum, buffer.Span); + + await archiveStream.WriteAsync(buffer, cancellationToken).ConfigureAwait(false); + + if (_dataStream != null) + { + await WriteDataAsync(archiveStream, _dataStream, actualLength, cancellationToken).ConfigureAwait(false); + } + } + // Writes the current header as a Ustar entry into the archive stream. internal void WriteAsUstar(Stream archiveStream, Span buffer) { @@ -72,6 +92,26 @@ internal void WriteAsPaxGlobalExtendedAttributes(Stream archiveStream, Span buffer, CancellationToken cancellationToken) + { + long actualLength = GetTotalDataBytesToWrite(); + TarEntryType actualEntryType = GetCorrectTypeFlagForFormat(TarEntryFormat.Ustar); + + int checksum = WritePosixName(buffer.Span); + checksum += WriteCommonFields(buffer.Span, actualLength, actualEntryType); + checksum += WritePosixMagicAndVersion(buffer.Span); + checksum += WritePosixAndGnuSharedFields(buffer.Span); + WriteChecksum(checksum, buffer.Span); + + await archiveStream.WriteAsync(buffer, cancellationToken).ConfigureAwait(false); + + if (_dataStream != null) + { + await WriteDataAsync(archiveStream, _dataStream, actualLength, cancellationToken).ConfigureAwait(false); + } + } + // Writes the current header as a PAX entry into the archive stream. // Makes sure to add the preceding extended attributes entry before the actual entry. internal void WriteAsPax(Stream archiveStream, Span buffer) @@ -91,6 +131,23 @@ internal void WriteAsPax(Stream archiveStream, Span buffer) WriteAsPaxInternal(archiveStream, buffer); } + // Asynchronously writes the current header as a PAX entry into the archive stream. + // Makes sure to add the preceding exteded attributes entry before the actual entry. + internal async Task WriteAsPaxAsync(Stream archiveStream, Memory buffer, CancellationToken cancellationToken) + { + // First, we write the preceding extended attributes header + TarHeader extendedAttributesHeader = default; + // Fill the current header's dict + CollectExtendedAttributesFromStandardFieldsIfNeeded(); + // And pass them to the extended attributes header for writing + _extendedAttributes ??= new Dictionary(); + await extendedAttributesHeader.WriteAsPaxExtendedAttributesAsync(archiveStream, buffer, _extendedAttributes, isGea: false, 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); + } + // Writes the current header as a Gnu entry into the archive stream. // Makes sure to add the preceding LongLink and/or LongPath entries if necessary, before the actual entry. internal void WriteAsGnu(Stream archiveStream, Span buffer) @@ -115,6 +172,30 @@ internal void WriteAsGnu(Stream archiveStream, Span buffer) WriteAsGnuInternal(archiveStream, buffer); } + // Writes the current header as a Gnu entry into the archive stream. + // Makes sure to add the preceding LongLink and/or LongPath entries if necessary, before the actual entry. + internal async Task WriteAsGnuAsync(Stream archiveStream, Memory buffer, CancellationToken cancellationToken) + { + // First, we determine if we need a preceding LongLink, and write it if needed + if (_linkName.Length > FieldLengths.LinkName) + { + TarHeader longLinkHeader = GetGnuLongMetadataHeader(TarEntryType.LongLink, _linkName); + await longLinkHeader.WriteAsGnuInternalAsync(archiveStream, buffer, cancellationToken).ConfigureAwait(false); + buffer.Span.Clear(); // Reset it to reuse it + } + + // Second, we determine if we need a preceding LongPath, and write it if needed + if (_name.Length > FieldLengths.Name) + { + TarHeader longPathHeader = await GetGnuLongMetadataHeaderAsync(TarEntryType.LongPath, _name, cancellationToken).ConfigureAwait(false); + await longPathHeader.WriteAsGnuInternalAsync(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); + } + // Creates and returns a GNU long metadata header, with the specified long text written into its data stream. private static TarHeader GetGnuLongMetadataHeader(TarEntryType entryType, string longText) { @@ -137,6 +218,28 @@ private static TarHeader GetGnuLongMetadataHeader(TarEntryType entryType, string return longMetadataHeader; } + // Asynchronously creates and returns a GNU long metadata header, with the specified long text written into its data stream. + private static async Task GetGnuLongMetadataHeaderAsync(TarEntryType entryType, string longText, CancellationToken cancellationToken) + { + Debug.Assert((entryType is TarEntryType.LongPath && longText.Length > FieldLengths.Name) || + (entryType is TarEntryType.LongLink && longText.Length > FieldLengths.LinkName)); + + TarHeader longMetadataHeader = default; + + longMetadataHeader._name = GnuLongMetadataName; // Same name for both longpath or longlink + longMetadataHeader._mode = (int)TarHelpers.DefaultMode; + longMetadataHeader._uid = 0; + longMetadataHeader._gid = 0; + longMetadataHeader._mTime = DateTimeOffset.MinValue; // 0 + longMetadataHeader._typeFlag = entryType; + + longMetadataHeader._dataStream = new MemoryStream(); + await longMetadataHeader._dataStream.WriteAsync(Encoding.UTF8.GetBytes(longText), cancellationToken).ConfigureAwait(false); + longMetadataHeader._dataStream.Seek(0, SeekOrigin.Begin); // Ensure it gets written into the archive from the beginning + + return longMetadataHeader; + } + // Writes the current header as a GNU entry into the archive stream. internal void WriteAsGnuInternal(Stream archiveStream, Span buffer) { @@ -163,6 +266,32 @@ internal void WriteAsGnuInternal(Stream archiveStream, Span buffer) } } + // Asynchronously writes the current header as a GNU entry into the archive stream. + internal async Task WriteAsGnuInternalAsync(Stream archiveStream, Memory buffer, CancellationToken cancellationToken) + { + // Unused GNU fields: offset, longnames, unused, sparse struct, isextended and realsize + // If this header came from another archive, it will have a value + // If it was constructed by the user, it will be an empty array + _gnuUnusedBytes ??= new byte[FieldLengths.AllGnuUnused]; + + long actualLength = GetTotalDataBytesToWrite(); + TarEntryType actualEntryType = GetCorrectTypeFlagForFormat(TarEntryFormat.Gnu); + + int checksum = WriteName(buffer.Span, out _); + checksum += WriteCommonFields(buffer.Span, actualLength, actualEntryType); + checksum += WriteGnuMagicAndVersion(buffer.Span); + checksum += WritePosixAndGnuSharedFields(buffer.Span); + checksum += WriteGnuFields(buffer.Span); + WriteChecksum(checksum, buffer.Span); + + await archiveStream.WriteAsync(buffer, cancellationToken).ConfigureAwait(false); + + if (_dataStream != null) + { + await WriteDataAsync(archiveStream, _dataStream, actualLength, cancellationToken).ConfigureAwait(false); + } + } + // Writes the current header as a PAX Extended Attributes entry into the archive stream. private void WriteAsPaxExtendedAttributes(Stream archiveStream, Span buffer, IEnumerable> extendedAttributes, bool isGea) { @@ -182,6 +311,25 @@ private void WriteAsPaxExtendedAttributes(Stream archiveStream, Span buffe WriteAsPaxInternal(archiveStream, buffer); } + // Asynchronously writes the current header as a PAX Extended Attributes entry into the archive stream. + private async Task WriteAsPaxExtendedAttributesAsync(Stream archiveStream, Memory buffer, IEnumerable> extendedAttributes, bool isGea, CancellationToken cancellationToken) + { + // The ustar fields (uid, gid, linkName, uname, gname, devmajor, devminor) do not get written. + // The mode gets the default value. + _name = GenerateExtendedAttributeName(); + _mode = (int)TarHelpers.DefaultMode; + _typeFlag = isGea ? TarEntryType.GlobalExtendedAttributes : TarEntryType.ExtendedAttributes; + _linkName = string.Empty; + _magic = string.Empty; + _version = string.Empty; + _gName = string.Empty; + _uName = string.Empty; + + _dataStream = await GenerateExtendedAttributesDataStreamAsync(extendedAttributes, cancellationToken).ConfigureAwait(false); + + await WriteAsPaxInternalAsync(archiveStream, buffer, cancellationToken).ConfigureAwait(false); + } + // Both the Extended Attributes and Global Extended Attributes entry headers are written in a similar way, just the data changes // This method writes an entry as both entries require, using the data from the current header instance. private void WriteAsPaxInternal(Stream archiveStream, Span buffer) @@ -203,6 +351,27 @@ private void WriteAsPaxInternal(Stream archiveStream, Span buffer) } } + // 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) + { + long actualLength = GetTotalDataBytesToWrite(); + TarEntryType actualEntryType = GetCorrectTypeFlagForFormat(TarEntryFormat.Pax); + + int checksum = WritePosixName(buffer.Span); + checksum += WriteCommonFields(buffer.Span, actualLength, actualEntryType); + checksum += WritePosixMagicAndVersion(buffer.Span); + checksum += WritePosixAndGnuSharedFields(buffer.Span); + WriteChecksum(checksum, buffer.Span); + + await archiveStream.WriteAsync(buffer, cancellationToken).ConfigureAwait(false); + + if (_dataStream != null) + { + await WriteDataAsync(archiveStream, _dataStream, actualLength, cancellationToken).ConfigureAwait(false); + } + } + // All formats save in the name byte array only the ASCII bytes that fit. The full string is returned in the out byte array. private int WriteName(Span buffer, out byte[] fullNameBytes) { @@ -366,6 +535,14 @@ private static void WriteData(Stream archiveStream, Stream dataStream, long actu archiveStream.Write(new byte[paddingAfterData]); } + // Asynchronously writes the current header's data stream into the archive stream. + private static async Task WriteDataAsync(Stream archiveStream, Stream dataStream, long actualLength, CancellationToken cancellationToken) + { + await dataStream.CopyToAsync(archiveStream, cancellationToken).ConfigureAwait(false); // The data gets copied from the current position + int paddingAfterData = TarHelpers.CalculatePadding(actualLength); + await archiveStream.WriteAsync(new byte[paddingAfterData], cancellationToken).ConfigureAwait(false); + } + // Dumps into the archive stream an extended attribute entry containing metadata of the entry it precedes. private static Stream? GenerateExtendedAttributesDataStream(IEnumerable> extendedAttributes) { @@ -382,6 +559,22 @@ private static void WriteData(Stream archiveStream, Stream dataStream, long actu return dataStream; } + // Asynchronously dumps into the archive stream an extended attribute entry containing metadata of the entry it precedes. + private static async Task GenerateExtendedAttributesDataStreamAsync(IEnumerable> extendedAttributes, CancellationToken cancellationToken) + { + MemoryStream? dataStream = null; + foreach ((string attribute, string value) in extendedAttributes) + { + // Need to do this because IEnumerable has no Count property + dataStream ??= new MemoryStream(); + + byte[] entryBytes = GenerateExtendedAttributeKeyValuePairAsByteArray(Encoding.UTF8.GetBytes(attribute), Encoding.UTF8.GetBytes(value)); + await dataStream.WriteAsync(entryBytes, cancellationToken).ConfigureAwait(false); + } + dataStream?.Seek(0, SeekOrigin.Begin); // Ensure it gets written into the archive from the beginning + return dataStream; + } + // Some fields that have a reserved spot in the header, may not fit in such field anymore, but they can fit in the // extended attributes. They get collected and saved in that dictionary, with no restrictions. private void CollectExtendedAttributesFromStandardFieldsIfNeeded() diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarWriter.Unix.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarWriter.Unix.cs index 11fdf8d3629116..d64b0cb7436d7b 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarWriter.Unix.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarWriter.Unix.cs @@ -4,6 +4,8 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; +using System.Threading; +using System.Threading.Tasks; namespace System.Formats.Tar { @@ -100,5 +102,92 @@ partial void ReadFileFromDiskAndWriteToArchiveStreamAsEntry(string fullPath, str entry._header._dataStream.Dispose(); } } + + // Unix specific implementation of the method that reads an entry from disk and writes it into the archive stream. + private async partial Task ReadFileFromDiskAndWriteToArchiveStreamAsEntryAsync(string fullPath, string entryName, CancellationToken cancellationToken) + { + Interop.Sys.FileStatus status = default; + status.Mode = default; + status.Dev = default; + Interop.CheckIo(Interop.Sys.LStat(fullPath, out status)); + + TarEntryType entryType = (status.Mode & (uint)Interop.Sys.FileTypes.S_IFMT) switch + { + // Hard links are treated as regular files. + // Unix socket files do not get added to tar files. + Interop.Sys.FileTypes.S_IFBLK => TarEntryType.BlockDevice, + Interop.Sys.FileTypes.S_IFCHR => TarEntryType.CharacterDevice, + Interop.Sys.FileTypes.S_IFIFO => TarEntryType.Fifo, + Interop.Sys.FileTypes.S_IFLNK => TarEntryType.SymbolicLink, + Interop.Sys.FileTypes.S_IFREG => Format is TarEntryFormat.V7 ? TarEntryType.V7RegularFile : TarEntryType.RegularFile, + Interop.Sys.FileTypes.S_IFDIR => TarEntryType.Directory, + _ => throw new IOException(string.Format(SR.TarUnsupportedFile, fullPath)), + }; + + FileSystemInfo info = entryType is TarEntryType.Directory ? new DirectoryInfo(fullPath) : new FileInfo(fullPath); + + TarEntry entry = Format switch + { + TarEntryFormat.V7 => new V7TarEntry(entryType, entryName), + TarEntryFormat.Ustar => new UstarTarEntry(entryType, entryName), + TarEntryFormat.Pax => new PaxTarEntry(entryType, entryName), + TarEntryFormat.Gnu => new GnuTarEntry(entryType, entryName), + _ => throw new FormatException(string.Format(SR.TarInvalidFormat, Format)), + }; + + if (entryType is TarEntryType.BlockDevice or TarEntryType.CharacterDevice) + { + uint major; + uint minor; + unsafe + { + Interop.Sys.GetDeviceIdentifiers((ulong)status.RDev, &major, &minor); + } + + entry._header._devMajor = (int)major; + entry._header._devMinor = (int)minor; + } + + entry._header._mTime = TarHelpers.GetDateTimeOffsetFromSecondsSinceEpoch(status.MTime); + entry._header._aTime = TarHelpers.GetDateTimeOffsetFromSecondsSinceEpoch(status.ATime); + entry._header._cTime = TarHelpers.GetDateTimeOffsetFromSecondsSinceEpoch(status.CTime); + + entry._header._mode = status.Mode & 4095; // First 12 bits + + // Uid and UName + entry._header._uid = (int)status.Uid; + if (!_userIdentifiers.TryGetValue(status.Uid, out string? uName)) + { + uName = Interop.Sys.GetUserNameFromPasswd(status.Uid); + _userIdentifiers.Add(status.Uid, uName); + } + entry._header._uName = uName; + + // Gid and GName + entry._header._gid = (int)status.Gid; + if (!_groupIdentifiers.TryGetValue(status.Gid, out string? gName)) + { + gName = Interop.Sys.GetGroupName(status.Gid); + _groupIdentifiers.Add(status.Gid, gName); + } + entry._header._gName = gName; + + if (entry.EntryType == TarEntryType.SymbolicLink) + { + entry.LinkName = info.LinkTarget ?? string.Empty; + } + + if (entry.EntryType is TarEntryType.RegularFile or TarEntryType.V7RegularFile) + { + Debug.Assert(entry._header._dataStream == null); + entry._header._dataStream = File.OpenRead(fullPath); + } + + await WriteEntryAsync(entry, cancellationToken).ConfigureAwait(false); + if (entry._header._dataStream != null) + { + await entry._header._dataStream.DisposeAsync().ConfigureAwait(false); + } + } } } diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarWriter.Windows.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarWriter.Windows.cs index 906a292ac161c0..6a89a74f28983d 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarWriter.Windows.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarWriter.Windows.cs @@ -3,6 +3,8 @@ using System.Diagnostics; using System.IO; +using System.Threading; +using System.Threading.Tasks; namespace System.Formats.Tar { @@ -77,5 +79,71 @@ partial void ReadFileFromDiskAndWriteToArchiveStreamAsEntry(string fullPath, str entry._header._dataStream.Dispose(); } } + + // Windows specific implementation of the method that asynchronously reads an entry from disk and writes it into the archive stream. + private async partial Task ReadFileFromDiskAndWriteToArchiveStreamAsEntryAsync(string fullPath, string entryName, CancellationToken cancellationToken) + { + TarEntryType entryType; + FileAttributes attributes = File.GetAttributes(fullPath); + + if (attributes.HasFlag(FileAttributes.ReparsePoint)) + { + entryType = TarEntryType.SymbolicLink; + } + else if (attributes.HasFlag(FileAttributes.Directory)) + { + entryType = TarEntryType.Directory; + } + else if (attributes.HasFlag(FileAttributes.Normal) || attributes.HasFlag(FileAttributes.Archive)) + { + entryType = Format is TarEntryFormat.V7 ? TarEntryType.V7RegularFile : TarEntryType.RegularFile; + } + else + { + throw new IOException(string.Format(SR.TarUnsupportedFile, fullPath)); + } + + TarEntry entry = Format switch + { + TarEntryFormat.V7 => new V7TarEntry(entryType, entryName), + TarEntryFormat.Ustar => new UstarTarEntry(entryType, entryName), + TarEntryFormat.Pax => new PaxTarEntry(entryType, entryName), + TarEntryFormat.Gnu => new GnuTarEntry(entryType, entryName), + _ => throw new FormatException(string.Format(SR.TarInvalidFormat, Format)), + }; + + FileSystemInfo info = attributes.HasFlag(FileAttributes.Directory) ? new DirectoryInfo(fullPath) : new FileInfo(fullPath); + + entry._header._mTime = new DateTimeOffset(info.LastWriteTimeUtc); + entry._header._aTime = new DateTimeOffset(info.LastAccessTimeUtc); + entry._header._cTime = new DateTimeOffset(info.LastWriteTimeUtc); // There is no "change time" property + + entry.Mode = DefaultWindowsMode; + + if (entry.EntryType == TarEntryType.SymbolicLink) + { + entry.LinkName = info.LinkTarget ?? string.Empty; + } + + if (entry.EntryType is TarEntryType.RegularFile or TarEntryType.V7RegularFile) + { + FileStreamOptions options = new() + { + Mode = FileMode.Open, + Access = FileAccess.Read, + Share = FileShare.Read, + Options = FileOptions.None + }; + + Debug.Assert(entry._header._dataStream == null); + entry._header._dataStream = File.Open(fullPath, options); + } + + await WriteEntryAsync(entry, cancellationToken).ConfigureAwait(false); + if (entry._header._dataStream != null) + { + await entry._header._dataStream.DisposeAsync().ConfigureAwait(false); + } + } } } 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 c3ad156d0fb1a2..8cc41fc29b3f9c 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 @@ -5,6 +5,8 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; +using System.Threading; +using System.Threading.Tasks; namespace System.Formats.Tar { @@ -119,16 +121,31 @@ public void WriteEntry(string fileName, string? entryName) ReadFileFromDiskAndWriteToArchiveStreamAsEntry(fullPath, entryName); } - // /// - // /// Asynchronously writes the specified file into the archive stream as a tar entry. - // /// - // /// The path to the file to write to the archive. - // /// The name of the file as it should be represented in the archive. It should include the optional relative path and the filename. - // /// The token to monitor for cancellation requests. The default value is . - // public Task WriteEntryAsync(string fileName, string? entryName, CancellationToken cancellationToken = default) - // { - // throw new NotImplementedException(); - // } + /// + /// Asynchronously writes the specified file into the archive stream as a tar entry. + /// + /// The path to the file to write to the archive. + /// The name of the file as it should be represented in the archive. It should include the optional relative path and the filename. + /// The token to monitor for cancellation requests. The default value is . + /// A task that represents the asynchronous write operation. + /// The archive stream is disposed. + /// or is or empty. + /// An I/O problem occurred. + public async Task WriteEntryAsync(string fileName, string? entryName, CancellationToken cancellationToken = default) + { + ThrowIfDisposed(); + + ArgumentException.ThrowIfNullOrEmpty(fileName); + + string fullPath = Path.GetFullPath(fileName); + + if (string.IsNullOrEmpty(entryName)) + { + entryName = Path.GetFileName(fileName); + } + + await ReadFileFromDiskAndWriteToArchiveStreamAsEntryAsync(fullPath, entryName, cancellationToken).ConfigureAwait(false); + } /// /// Writes the specified entry into the archive stream. @@ -207,41 +224,75 @@ public void WriteEntry(TarEntry entry) _wroteEntries = true; } - // /// - // /// Asynchronously writes the specified entry into the archive stream. - // /// - // /// The tar entry to write. - // /// The token to monitor for cancellation requests. The default value is . - // /// Before writing an entry to the archive, if you wrote data into the entry's , make sure to rewind it to the desired start position. - // /// These are the entry types supported for writing on each format: - // /// - // /// - // /// - // /// - // /// - // /// - // /// - // /// - // /// - // /// - // /// - // /// , and - // /// - // /// - // /// - // /// - // /// - // /// - // /// - // /// - // /// - // /// - // /// - // /// - // public Task WriteEntryAsync(TarEntry entry, CancellationToken cancellationToken = default) - // { - // throw new NotImplementedException(); - // } + /// + /// Asynchronously writes the specified entry into the archive stream. + /// + /// The tar entry to write. + /// The token to monitor for cancellation requests. The default value is . + /// Before writing an entry to the archive, if you wrote data into the entry's , make sure to rewind it to the desired start position. + /// These are the entry types supported for writing on each format: + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// , and + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// The archive stream is disposed. + /// The entry type of the is not supported for writing. + /// An I/O problem occurred. + public async Task WriteEntryAsync(TarEntry entry, CancellationToken cancellationToken = default) + { + ThrowIfDisposed(); + + IMemoryOwner rented = MemoryPool.Shared.Rent(minBufferSize: TarHelpers.RecordSize); + Memory buffer = rented.Memory.Slice(TarHelpers.RecordSize); // minBufferSize means the array could've been larger + buffer.Span.Clear(); // Rented arrays aren't clean + try + { + switch (entry.Format) + { + case TarEntryFormat.V7: + await entry._header.WriteAsV7Async(_archiveStream, buffer, cancellationToken).ConfigureAwait(false); + break; + case TarEntryFormat.Ustar: + await entry._header.WriteAsUstarAsync(_archiveStream, buffer, cancellationToken).ConfigureAwait(false); + break; + case TarEntryFormat.Pax: + await entry._header.WriteAsPaxAsync(_archiveStream, buffer, cancellationToken).ConfigureAwait(false); + break; + case TarEntryFormat.Gnu: + await entry._header.WriteAsGnuAsync(_archiveStream, buffer, cancellationToken).ConfigureAwait(false); + break; + case TarEntryFormat.Unknown: + default: + throw new FormatException(string.Format(SR.TarInvalidFormat, Format)); + } + } + finally + { + rented.Dispose(); + } + + _wroteEntries = true; + } // Disposes the current instance. // If 'disposing' is 'false', the method was called from the finalizer. @@ -289,5 +340,8 @@ private void WriteFinalRecords() // Partial method for reading an entry from disk and writing it into the archive stream. partial void ReadFileFromDiskAndWriteToArchiveStreamAsEntry(string fullPath, string entryName); + + // Partial method for reading an entry from disk and writing it into the archive stream. + private partial Task ReadFileFromDiskAndWriteToArchiveStreamAsEntryAsync(string fullPath, string entryName, CancellationToken cancellationToken); } } From 24a26dded4a3c901684b08756fb95dd395af2ff7 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Thu, 23 Jun 2022 15:48:54 -0700 Subject: [PATCH 07/75] ref: Add TarFile.CreateFromDirectoryAsync --- src/libraries/System.Formats.Tar/ref/System.Formats.Tar.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/libraries/System.Formats.Tar/ref/System.Formats.Tar.cs b/src/libraries/System.Formats.Tar/ref/System.Formats.Tar.cs index 39171da929d816..28fe80b65cd383 100644 --- a/src/libraries/System.Formats.Tar/ref/System.Formats.Tar.cs +++ b/src/libraries/System.Formats.Tar/ref/System.Formats.Tar.cs @@ -84,6 +84,8 @@ public static partial class TarFile { public static void CreateFromDirectory(string sourceDirectoryName, System.IO.Stream destination, bool includeBaseDirectory) { } public static void CreateFromDirectory(string sourceDirectoryName, string destinationFileName, bool includeBaseDirectory) { } + public static System.Threading.Tasks.Task CreateFromDirectoryAsync(string sourceDirectoryName, System.IO.Stream destination, bool includeBaseDirectory, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public static System.Threading.Tasks.Task CreateFromDirectoryAsync(string sourceDirectoryName, string destinationFileName, bool includeBaseDirectory, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public static void ExtractToDirectory(System.IO.Stream source, string destinationDirectoryName, bool overwriteFiles) { } public static void ExtractToDirectory(string sourceFileName, string destinationDirectoryName, bool overwriteFiles) { } } From 2fdfeebf24173e73122a3f6f0cb132a77201e095 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Thu, 23 Jun 2022 15:49:32 -0700 Subject: [PATCH 08/75] src: Implement TarFile.CreateFromDirectoryAsync --- .../src/System/Formats/Tar/TarFile.cs | 135 ++++++++++++++---- 1 file changed, 111 insertions(+), 24 deletions(-) diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarFile.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarFile.cs index aa30d728ab6a2c..92ee466e17e5c6 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarFile.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarFile.cs @@ -4,6 +4,8 @@ using System.Buffers; using System.Diagnostics; using System.IO; +using System.Threading; +using System.Threading.Tasks; namespace System.Formats.Tar { @@ -39,18 +41,34 @@ public static void CreateFromDirectory(string sourceDirectoryName, Stream destin CreateFromDirectoryInternal(sourceDirectoryName, destination, includeBaseDirectory, leaveOpen: true); } - // /// - // /// Asynchronously creates a tar stream that contains all the filesystem entries from the specified directory. - // /// - // /// The path of the directory to archive. - // /// The destination stream of the archive. - // /// to include the base directory name as the first path segment in all the names of the archive entries. to exclude the base directory name from the entry name paths. - // /// The token to monitor for cancellation requests. The default value is . - // /// A task that represents the asynchronous creation operation. - // public static Task CreateFromDirectoryAsync(string sourceDirectoryName, Stream destination, bool includeBaseDirectory, CancellationToken cancellationToken = default) - // { - // throw new NotImplementedException(); - // } + /// + /// Asynchronously creates a tar stream that contains all the filesystem entries from the specified directory. + /// + /// The path of the directory to archive. + /// The destination stream of the archive. + /// to include the base directory name as the first path segment in all the names of the archive entries. to exclude the base directory name from the entry name paths. + /// The token to monitor for cancellation requests. The default value is . + /// A task that represents the asynchronous creation operation. + public static Task CreateFromDirectoryAsync(string sourceDirectoryName, Stream destination, bool includeBaseDirectory, CancellationToken cancellationToken = default) + { + ArgumentException.ThrowIfNullOrEmpty(sourceDirectoryName); + ArgumentNullException.ThrowIfNull(destination); + + if (!destination.CanWrite) + { + throw new IOException(SR.IO_NotSupported_UnwritableStream); + } + + if (!Directory.Exists(sourceDirectoryName)) + { + throw new DirectoryNotFoundException(string.Format(SR.IO_PathNotFound_Path, sourceDirectoryName)); + } + + // Rely on Path.GetFullPath for validation of paths + sourceDirectoryName = Path.GetFullPath(sourceDirectoryName); + + return CreateFromDirectoryInternalAsync(sourceDirectoryName, destination, includeBaseDirectory, leaveOpen: true, cancellationToken); + } /// /// Creates a tar file that contains all the filesystem entries from the specified directory. @@ -78,18 +96,33 @@ public static void CreateFromDirectory(string sourceDirectoryName, string destin CreateFromDirectoryInternal(sourceDirectoryName, fs, includeBaseDirectory, leaveOpen: false); } - // /// - // /// Asynchronously creates a tar archive from the contents of the specified directory, and outputs them into the specified path. Can optionally include the base directory as the prefix for the entry names. - // /// - // /// The path of the directory to archive. - // /// The path of the destination archive file. - // /// to include the base directory name as the first path segment in all the names of the archive entries. to exclude the base directory name from the entry name paths. - // /// The token to monitor for cancellation requests. The default value is . - // /// A task that represents the asynchronous creation operation. - // public static Task CreateFromDirectoryAsync(string sourceDirectoryName, string destinationFileName, bool includeBaseDirectory, CancellationToken cancellationToken = default) - // { - // throw new NotImplementedException(); - // } + /// + /// Asynchronously creates a tar archive from the contents of the specified directory, and outputs them into the specified path. Can optionally include the base directory as the prefix for the entry names. + /// + /// The path of the directory to archive. + /// The path of the destination archive file. + /// to include the base directory name as the first path segment in all the names of the archive entries. to exclude the base directory name from the entry name paths. + /// The token to monitor for cancellation requests. The default value is . + /// A task that represents the asynchronous creation operation. + public static Task CreateFromDirectoryAsync(string sourceDirectoryName, string destinationFileName, bool includeBaseDirectory, CancellationToken cancellationToken = default) + { + ArgumentException.ThrowIfNullOrEmpty(sourceDirectoryName); + ArgumentException.ThrowIfNullOrEmpty(destinationFileName); + + // Rely on Path.GetFullPath for validation of paths + sourceDirectoryName = Path.GetFullPath(sourceDirectoryName); + destinationFileName = Path.GetFullPath(destinationFileName); + + if (!Directory.Exists(sourceDirectoryName)) + { + throw new DirectoryNotFoundException(string.Format(SR.IO_PathNotFound_Path, sourceDirectoryName)); + } + + // Throws if the destination file exists + using FileStream fs = new(destinationFileName, FileMode.CreateNew, FileAccess.Write); + + return CreateFromDirectoryInternalAsync(sourceDirectoryName, fs, includeBaseDirectory, leaveOpen: false, cancellationToken); + } /// /// Extracts the contents of a stream that represents a tar archive into the specified directory. @@ -241,6 +274,60 @@ private static void CreateFromDirectoryInternal(string sourceDirectoryName, Stre } } + // Asynchronously creates an archive from the contents of a directory. + // It assumes the sourceDirectoryName is a fully qualified path, and allows choosing if the archive stream should be left open or not. + private static async Task CreateFromDirectoryInternalAsync(string sourceDirectoryName, Stream destination, bool includeBaseDirectory, bool leaveOpen, CancellationToken cancellationToken) + { + Debug.Assert(!string.IsNullOrEmpty(sourceDirectoryName)); + Debug.Assert(destination != null); + Debug.Assert(Path.IsPathFullyQualified(sourceDirectoryName)); + Debug.Assert(destination.CanWrite); + + using (TarWriter writer = new TarWriter(destination, TarEntryFormat.Pax, leaveOpen)) + { + bool baseDirectoryIsEmpty = true; + DirectoryInfo di = new(sourceDirectoryName); + string basePath = di.FullName; + + if (includeBaseDirectory && di.Parent != null) + { + basePath = di.Parent.FullName; + } + + // Windows' MaxPath (260) is used as an arbitrary default capacity, as it is likely + // to be greater than the length of typical entry names from the file system, even + // on non-Windows platforms. The capacity will be increased, if needed. + const int DefaultCapacity = 260; + char[] entryNameBuffer = ArrayPool.Shared.Rent(DefaultCapacity); + + try + { + foreach (FileSystemInfo file in di.EnumerateFileSystemInfos("*", SearchOption.AllDirectories)) + { + baseDirectoryIsEmpty = false; + + int entryNameLength = file.FullName.Length - basePath.Length; + Debug.Assert(entryNameLength > 0); + + bool isDirectory = file.Attributes.HasFlag(FileAttributes.Directory); + string entryName = ArchivingUtils.EntryFromPath(file.FullName, basePath.Length, entryNameLength, ref entryNameBuffer, appendPathSeparator: isDirectory); + await writer.WriteEntryAsync(file.FullName, entryName, cancellationToken).ConfigureAwait(false); + } + + if (includeBaseDirectory && baseDirectoryIsEmpty) + { + string entryName = ArchivingUtils.EntryFromPath(di.Name, 0, di.Name.Length, ref entryNameBuffer, appendPathSeparator: true); + PaxTarEntry entry = new PaxTarEntry(TarEntryType.Directory, entryName); + await writer.WriteEntryAsync(entry, cancellationToken).ConfigureAwait(false); + } + } + finally + { + ArrayPool.Shared.Return(entryNameBuffer); + } + } + } + // Extracts an archive into the specified directory. // It assumes the destinationDirectoryName is a fully qualified path, and allows choosing if the archive stream should be left open or not. private static void ExtractToDirectoryInternal(Stream source, string destinationDirectoryPath, bool overwriteFiles, bool leaveOpen) From ea2fc3eca70bef249832ea34045401d9345c8eed Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Thu, 23 Jun 2022 15:49:54 -0700 Subject: [PATCH 09/75] ref: Add TarFile.ExtractToDirectoryAsync --- src/libraries/System.Formats.Tar/ref/System.Formats.Tar.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/libraries/System.Formats.Tar/ref/System.Formats.Tar.cs b/src/libraries/System.Formats.Tar/ref/System.Formats.Tar.cs index 28fe80b65cd383..7d48b4144f6137 100644 --- a/src/libraries/System.Formats.Tar/ref/System.Formats.Tar.cs +++ b/src/libraries/System.Formats.Tar/ref/System.Formats.Tar.cs @@ -88,6 +88,8 @@ public static void CreateFromDirectory(string sourceDirectoryName, string destin public static System.Threading.Tasks.Task CreateFromDirectoryAsync(string sourceDirectoryName, string destinationFileName, bool includeBaseDirectory, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public static void ExtractToDirectory(System.IO.Stream source, string destinationDirectoryName, bool overwriteFiles) { } public static void ExtractToDirectory(string sourceFileName, string destinationDirectoryName, bool overwriteFiles) { } + public static System.Threading.Tasks.Task ExtractToDirectoryAsync(System.IO.Stream source, string destinationDirectoryName, bool overwriteFiles, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public static System.Threading.Tasks.Task ExtractToDirectoryAsync(string sourceFileName, string destinationDirectoryName, bool overwriteFiles, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } } public sealed partial class TarReader : System.IDisposable { From a36a455b4f830b13de0130ee4b7d59befa91a4ab Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Thu, 23 Jun 2022 15:55:14 -0700 Subject: [PATCH 10/75] src: Implement TarFile.ExtractToDirectoryAsync --- .../src/System/Formats/Tar/TarEntry.cs | 56 ++++++++- .../src/System/Formats/Tar/TarFile.cs | 113 +++++++++++++----- 2 files changed, 133 insertions(+), 36 deletions(-) diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs index fa061e23cc1dcb..6615c84abdc2c7 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs @@ -347,6 +347,48 @@ internal void ExtractRelativeToDirectory(string destinationDirectoryPath, bool o } } + // Asynchronously extracts the current entry to a location relative to the specified directory. + internal Task ExtractRelativeToDirectoryAsync(string destinationDirectoryPath, bool overwrite, CancellationToken cancellationToken) + { + Debug.Assert(!string.IsNullOrEmpty(destinationDirectoryPath)); + Debug.Assert(Path.IsPathFullyQualified(destinationDirectoryPath)); + + destinationDirectoryPath = Path.TrimEndingDirectorySeparator(destinationDirectoryPath); + + string? fileDestinationPath = GetSanitizedFullPath(destinationDirectoryPath, Name); + if (fileDestinationPath == null) + { + return Task.FromException(new IOException(string.Format(SR.TarExtractingResultsFileOutside, Name, destinationDirectoryPath))); + } + + string? linkTargetPath = null; + if (EntryType is TarEntryType.SymbolicLink or TarEntryType.HardLink) + { + if (string.IsNullOrEmpty(LinkName)) + { + return Task.FromException(new FormatException(SR.TarEntryHardLinkOrSymlinkLinkNameEmpty)); + } + + linkTargetPath = GetSanitizedFullPath(destinationDirectoryPath, LinkName); + if (linkTargetPath == null) + { + return Task.FromException(new IOException(string.Format(SR.TarExtractingResultsLinkOutside, LinkName, destinationDirectoryPath))); + } + } + + if (EntryType == TarEntryType.Directory) + { + Directory.CreateDirectory(fileDestinationPath); + return Task.CompletedTask; + } + else + { + // If it is a file, create containing directory. + Directory.CreateDirectory(Path.GetDirectoryName(fileDestinationPath)!); + return ExtractToFileInternalAsync(fileDestinationPath, linkTargetPath, overwrite, cancellationToken); + } + } + // If the path can be extracted in the specified destination directory, returns the full path with sanitized file name. Otherwise, returns null. private static string? GetSanitizedFullPath(string destinationDirectoryFullPath, string path) { @@ -374,7 +416,7 @@ private void ExtractToFileInternal(string filePath, string? linkTargetPath, bool } // Asynchronously extracts the current entry into the filesystem, regardless of the entry type. - private Task ExtractToFileInternalAsync(string filePath, string? linkTargetPath, bool overwrite, CancellationToken cancellationToken) + private async Task ExtractToFileInternalAsync(string filePath, string? linkTargetPath, bool overwrite, CancellationToken cancellationToken) { ArgumentException.ThrowIfNullOrEmpty(filePath); @@ -382,10 +424,12 @@ private Task ExtractToFileInternalAsync(string filePath, string? linkTargetPath, if (EntryType is TarEntryType.RegularFile or TarEntryType.V7RegularFile or TarEntryType.ContiguousFile) { - return ExtractAsRegularFileAsync(filePath, cancellationToken); + await ExtractAsRegularFileAsync(filePath, cancellationToken).ConfigureAwait(false); + } + else + { + CreateNonRegularFile(filePath, linkTargetPath); } - CreateNonRegularFile(filePath, linkTargetPath); - return Task.CompletedTask; } private void CreateNonRegularFile(string filePath, string? linkTargetPath) @@ -529,13 +573,13 @@ private async Task ExtractAsRegularFileAsync(string destinationFileName, Cancell { Debug.Assert(!Path.Exists(destinationFileName)); - FileStreamOptions fileStreamOptions = new FileStreamOptions() + FileStreamOptions fileStreamOptions = new() { Access = FileAccess.Write, Mode = FileMode.CreateNew, Share = FileShare.None, - Options = FileOptions.Asynchronous, PreallocationSize = Length, + Options = FileOptions.Asynchronous }; // Rely on FileStream's ctor for further checking destinationFileName parameter using (FileStream fs = new FileStream(destinationFileName, fileStreamOptions)) diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarFile.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarFile.cs index 92ee466e17e5c6..22dc75b07359a7 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarFile.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarFile.cs @@ -155,21 +155,37 @@ public static void ExtractToDirectory(Stream source, string destinationDirectory ExtractToDirectoryInternal(source, destinationDirectoryName, overwriteFiles, leaveOpen: true); } - // /// - // /// Asynchronously extracts the contents of a stream that represents a tar archive into the specified directory. - // /// - // /// The stream containing the tar archive. - // /// The path of the destination directory where the filesystem entries should be extracted. - // /// to overwrite files and directories in ; to avoid overwriting, and throw if any files or directories are found with existing names. - // /// The token to monitor for cancellation requests. The default value is . - // /// A task that represents the asynchronous extraction operation. - // /// Files of type , or can only be extracted in Unix platforms. - // /// Elevation is required to extract a or to disk. - // /// Operation not permitted due to insufficient permissions. - // public static Task ExtractToDirectoryAsync(Stream source, string destinationDirectoryName, bool overwriteFiles, CancellationToken cancellationToken = default) - // { - // throw new NotImplementedException(); - // } + /// + /// Asynchronously extracts the contents of a stream that represents a tar archive into the specified directory. + /// + /// The stream containing the tar archive. + /// The path of the destination directory where the filesystem entries should be extracted. + /// to overwrite files and directories in ; to avoid overwriting, and throw if any files or directories are found with existing names. + /// The token to monitor for cancellation requests. The default value is . + /// A task that represents the asynchronous extraction operation. + /// Files of type , or can only be extracted in Unix platforms. + /// Elevation is required to extract a or to disk. + /// Operation not permitted due to insufficient permissions. + public static Task ExtractToDirectoryAsync(Stream source, string destinationDirectoryName, bool overwriteFiles, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(source); + ArgumentException.ThrowIfNullOrEmpty(destinationDirectoryName); + + if (!source.CanRead) + { + throw new IOException(SR.IO_NotSupported_UnreadableStream); + } + + if (!Directory.Exists(destinationDirectoryName)) + { + throw new DirectoryNotFoundException(string.Format(SR.IO_PathNotFound_Path, destinationDirectoryName)); + } + + // Rely on Path.GetFullPath for validation of paths + destinationDirectoryName = Path.GetFullPath(destinationDirectoryName); + + return ExtractToDirectoryInternalAsync(source, destinationDirectoryName, overwriteFiles, leaveOpen: true, cancellationToken); + } /// /// Extracts the contents of a tar file into the specified directory. @@ -204,21 +220,40 @@ public static void ExtractToDirectory(string sourceFileName, string destinationD ExtractToDirectoryInternal(archive, destinationDirectoryName, overwriteFiles, leaveOpen: false); } - // /// - // /// Asynchronously extracts the contents of a tar file into the specified directory. - // /// - // /// The path of the tar file to extract. - // /// The path of the destination directory where the filesystem entries should be extracted. - // /// to overwrite files and directories in ; to avoid overwriting, and throw if any files or directories are found with existing names. - // /// The token to monitor for cancellation requests. The default value is . - // /// A task that represents the asynchronous extraction operation. - // /// Files of type , or can only be extracted in Unix platforms. - // /// Elevation is required to extract a or to disk. - // /// Operation not permitted due to insufficient permissions. - // public static Task ExtractToDirectoryAsync(string sourceFileName, string destinationDirectoryName, bool overwriteFiles, CancellationToken cancellationToken = default) - // { - // throw new NotImplementedException(); - // } + /// + /// Asynchronously extracts the contents of a tar file into the specified directory. + /// + /// The path of the tar file to extract. + /// The path of the destination directory where the filesystem entries should be extracted. + /// to overwrite files and directories in ; to avoid overwriting, and throw if any files or directories are found with existing names. + /// The token to monitor for cancellation requests. The default value is . + /// A task that represents the asynchronous extraction operation. + /// Files of type , or can only be extracted in Unix platforms. + /// Elevation is required to extract a or to disk. + /// Operation not permitted due to insufficient permissions. + public static Task ExtractToDirectoryAsync(string sourceFileName, string destinationDirectoryName, bool overwriteFiles, CancellationToken cancellationToken = default) + { + ArgumentException.ThrowIfNullOrEmpty(sourceFileName); + ArgumentException.ThrowIfNullOrEmpty(destinationDirectoryName); + + // Rely on Path.GetFullPath for validation of paths + sourceFileName = Path.GetFullPath(sourceFileName); + destinationDirectoryName = Path.GetFullPath(destinationDirectoryName); + + if (!File.Exists(sourceFileName)) + { + throw new FileNotFoundException(string.Format(SR.IO_FileNotFound, sourceFileName)); + } + + if (!Directory.Exists(destinationDirectoryName)) + { + throw new DirectoryNotFoundException(string.Format(SR.IO_PathNotFound_Path, destinationDirectoryName)); + } + + using FileStream archive = File.OpenRead(sourceFileName); + + return ExtractToDirectoryInternalAsync(archive, destinationDirectoryName, overwriteFiles, leaveOpen: false, cancellationToken); + } // Creates an archive from the contents of a directory. // It assumes the sourceDirectoryName is a fully qualified path, and allows choosing if the archive stream should be left open or not. @@ -348,5 +383,23 @@ private static void ExtractToDirectoryInternal(Stream source, string destination } } } + + // Asynchronously extracts an archive into the specified directory. + // It assumes the destinationDirectoryName is a fully qualified path, and allows choosing if the archive stream should be left open or not. + private static async Task ExtractToDirectoryInternalAsync(Stream source, string destinationDirectoryPath, bool overwriteFiles, bool leaveOpen, CancellationToken cancellationToken) + { + Debug.Assert(source != null); + Debug.Assert(!string.IsNullOrEmpty(destinationDirectoryPath)); + Debug.Assert(Path.IsPathFullyQualified(destinationDirectoryPath)); + Debug.Assert(source.CanRead); + + using TarReader reader = new TarReader(source, leaveOpen); + + TarEntry? entry; + while ((entry = await reader.GetNextEntryAsync(cancellationToken: cancellationToken).ConfigureAwait(false)) != null) + { + await entry.ExtractRelativeToDirectoryAsync(destinationDirectoryPath, overwriteFiles, cancellationToken).ConfigureAwait(false); + } + } } } From bfa88315039bddb0896c7fb29768d43b06a4f377 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Thu, 9 Jun 2022 23:32:51 -0700 Subject: [PATCH 11/75] ref: Add TarWriter.DisposeAsync --- src/libraries/System.Formats.Tar/ref/System.Formats.Tar.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libraries/System.Formats.Tar/ref/System.Formats.Tar.cs b/src/libraries/System.Formats.Tar/ref/System.Formats.Tar.cs index 7d48b4144f6137..fe16016e34ec40 100644 --- a/src/libraries/System.Formats.Tar/ref/System.Formats.Tar.cs +++ b/src/libraries/System.Formats.Tar/ref/System.Formats.Tar.cs @@ -98,13 +98,14 @@ public void Dispose() { } public System.Formats.Tar.TarEntry? GetNextEntry(bool copyData = false) { throw null; } public System.Threading.Tasks.ValueTask GetNextEntryAsync(bool copyData = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } } - public sealed partial class TarWriter : System.IDisposable + public sealed partial class TarWriter : System.IAsyncDisposable, System.IDisposable { public TarWriter(System.IO.Stream archiveStream) { } public TarWriter(System.IO.Stream archiveStream, bool leaveOpen = false) { } public TarWriter(System.IO.Stream archiveStream, System.Formats.Tar.TarEntryFormat format = System.Formats.Tar.TarEntryFormat.Pax, bool leaveOpen = false) { } public System.Formats.Tar.TarEntryFormat Format { get { throw null; } } public void Dispose() { } + public System.Threading.Tasks.ValueTask DisposeAsync() { throw null; } public void WriteEntry(System.Formats.Tar.TarEntry entry) { } public void WriteEntry(string fileName, string? entryName) { } public System.Threading.Tasks.Task WriteEntryAsync(System.Formats.Tar.TarEntry entry, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } From 259d5d706f642feab54f55985242df762266a520 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Thu, 9 Jun 2022 23:33:04 -0700 Subject: [PATCH 12/75] src: Implement TarWriter.DisposeAsync --- .../src/System/Formats/Tar/TarFile.cs | 3 +- .../src/System/Formats/Tar/TarWriter.cs | 54 ++++++++++++++++--- 2 files changed, 48 insertions(+), 9 deletions(-) diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarFile.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarFile.cs index 22dc75b07359a7..f8c03a535a83a7 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarFile.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarFile.cs @@ -318,7 +318,8 @@ private static async Task CreateFromDirectoryInternalAsync(string sourceDirector Debug.Assert(Path.IsPathFullyQualified(sourceDirectoryName)); Debug.Assert(destination.CanWrite); - using (TarWriter writer = new TarWriter(destination, TarEntryFormat.Pax, leaveOpen)) + TarWriter writer = new TarWriter(destination, TarEntryFormat.Pax, leaveOpen); + await using (writer.ConfigureAwait(false)) { bool baseDirectoryIsEmpty = true; DirectoryInfo di = new(sourceDirectoryName); 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 8cc41fc29b3f9c..1e31e1394ed648 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 @@ -13,7 +13,7 @@ namespace System.Formats.Tar /// /// Writes a tar archive into a stream. /// - public sealed partial class TarWriter : IDisposable + public sealed partial class TarWriter : IDisposable, IAsyncDisposable { private bool _wroteEntries; private bool _isDisposed; @@ -89,13 +89,14 @@ public void Dispose() GC.SuppressFinalize(this); } - // /// - // /// Asynchronously disposes the current instance, and closes the archive stream if the leaveOpen argument was set to in the constructor. - // /// - // public ValueTask DisposeAsync() - // { - // throw new NotImplementedException(); - // } + /// + /// Asynchronously disposes the current instance, and closes the archive stream if the leaveOpen argument was set to in the constructor. + /// + public async ValueTask DisposeAsync() + { + await DisposeAsync(disposing: true).ConfigureAwait(false); + GC.SuppressFinalize(this); + } /// /// Writes the specified file into the archive stream as a tar entry. @@ -320,6 +321,34 @@ private void Dispose(bool disposing) } } + // Asynchronously disposes the current instance. + // If 'disposing' is 'false', the method was called from the finalizer. + private async ValueTask DisposeAsync(bool disposing) + { + if (disposing && !_isDisposed) + { + try + { + await WriteGlobalExtendedAttributesEntryIfNeededAsync(cancellationToken: default).ConfigureAwait(false); + + if (_wroteEntries) + { + await WriteFinalRecordsAsync().ConfigureAwait(false); + } + + + if (!_leaveOpen) + { + await _archiveStream.DisposeAsync().ConfigureAwait(false); + } + } + finally + { + _isDisposed = true; + } + } + } + // If the underlying archive stream is disposed, throws 'ObjectDisposedException'. private void ThrowIfDisposed() { @@ -338,6 +367,15 @@ private void WriteFinalRecords() _archiveStream.Write(emptyRecord); } + // The spec indicates that the end of the archive is indicated + // by two records consisting entirely of zero bytes. + private async ValueTask WriteFinalRecordsAsync() + { + byte[] emptyRecord = new byte[TarHelpers.RecordSize]; + await _archiveStream.WriteAsync(emptyRecord, cancellationToken: default).ConfigureAwait(false); + await _archiveStream.WriteAsync(emptyRecord, cancellationToken: default).ConfigureAwait(false); + } + // Partial method for reading an entry from disk and writing it into the archive stream. partial void ReadFileFromDiskAndWriteToArchiveStreamAsEntry(string fullPath, string entryName); From 2999ff7ea2102aeda9137137d86237d3ade5f773 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Thu, 9 Jun 2022 23:37:06 -0700 Subject: [PATCH 13/75] ref: Add TarReader.DisposeAsync --- src/libraries/System.Formats.Tar/ref/System.Formats.Tar.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libraries/System.Formats.Tar/ref/System.Formats.Tar.cs b/src/libraries/System.Formats.Tar/ref/System.Formats.Tar.cs index fe16016e34ec40..a4bd74e0ff1022 100644 --- a/src/libraries/System.Formats.Tar/ref/System.Formats.Tar.cs +++ b/src/libraries/System.Formats.Tar/ref/System.Formats.Tar.cs @@ -95,6 +95,7 @@ public sealed partial class TarReader : System.IDisposable { public TarReader(System.IO.Stream archiveStream, bool leaveOpen = false) { } public void Dispose() { } + public System.Threading.Tasks.ValueTask DisposeAsync() { throw null; } public System.Formats.Tar.TarEntry? GetNextEntry(bool copyData = false) { throw null; } public System.Threading.Tasks.ValueTask GetNextEntryAsync(bool copyData = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } } From cb6996a4552e65faa202cc70cf837bebae22725b Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Thu, 9 Jun 2022 23:37:19 -0700 Subject: [PATCH 14/75] src: Implement TarReader.DisposeAsync --- .../src/System/Formats/Tar/TarFile.cs | 12 +++--- .../src/System/Formats/Tar/TarReader.cs | 42 +++++++++++++++---- 2 files changed, 40 insertions(+), 14 deletions(-) diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarFile.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarFile.cs index f8c03a535a83a7..1e9f5cb2bc4097 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarFile.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarFile.cs @@ -394,12 +394,14 @@ private static async Task ExtractToDirectoryInternalAsync(Stream source, string Debug.Assert(Path.IsPathFullyQualified(destinationDirectoryPath)); Debug.Assert(source.CanRead); - using TarReader reader = new TarReader(source, leaveOpen); - - TarEntry? entry; - while ((entry = await reader.GetNextEntryAsync(cancellationToken: cancellationToken).ConfigureAwait(false)) != null) + TarReader reader = new TarReader(source, leaveOpen); + await using (reader.ConfigureAwait(false)) { - await entry.ExtractRelativeToDirectoryAsync(destinationDirectoryPath, overwriteFiles, cancellationToken).ConfigureAwait(false); + TarEntry? entry; + while ((entry = await reader.GetNextEntryAsync(cancellationToken: cancellationToken).ConfigureAwait(false)) != null) + { + await entry.ExtractRelativeToDirectoryAsync(destinationDirectoryPath, overwriteFiles, cancellationToken).ConfigureAwait(false); + } } } } 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 671772257eb62f..29d62b57d2b725 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 @@ -12,7 +12,7 @@ namespace System.Formats.Tar /// /// Reads a tar archive from a stream. /// - public sealed class TarReader : IDisposable + public sealed class TarReader : IDisposable, IAsyncDisposable { private bool _isDisposed; private readonly bool _leaveOpen; @@ -57,14 +57,15 @@ public void Dispose() GC.SuppressFinalize(this); } - // /// - // /// Asynchronously disposes the current instance, and disposes the streams of all the entries that were read from the archive. - // /// - // /// The property of any entry can be replaced with a new stream. If the user decides to replace it on a instance that was obtained using a , the underlying stream gets disposed immediately, freeing the of origin from the responsibility of having to dispose it. - // public ValueTask DisposeAsync() - // { - // throw new NotImplementedException(); - // } + /// + /// Asynchronously disposes the current instance, and disposes the streams of all the entries that were read from the archive. + /// + /// The property of any entry can be replaced with a new stream. If the user decides to replace it on a instance that was obtained using a , the underlying stream gets disposed immediately, freeing the of origin from the responsibility of having to dispose it. + public async ValueTask DisposeAsync() + { + await DisposeAsync(disposing: true).ConfigureAwait(false); + GC.SuppressFinalize(this); + } /// /// Retrieves the next entry from the archive stream. @@ -286,6 +287,29 @@ private void Dispose(bool disposing) } } + // Asynchronously disposes the current instance. + // If 'disposing' is 'false', the method was called from the finalizer. + private async ValueTask DisposeAsync(bool disposing) + { + if (disposing && !_isDisposed) + { + try + { + if (!_leaveOpen && _dataStreamsToDispose?.Count > 0) + { + foreach (Stream s in _dataStreamsToDispose) + { + await s.DisposeAsync().ConfigureAwait(false); + } + } + } + finally + { + _isDisposed = true; + } + } + } + // Attempts to read the next tar archive entry header. // Returns true if an entry header was collected successfully, false otherwise. // An entry header represents any typeflag that is contains metadata. From beb29b6e99381795683bd945315982bcd3927189 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Fri, 17 Jun 2022 12:23:03 -0700 Subject: [PATCH 15/75] Don't use partial methods in TarEntry. --- .../src/System/Formats/Tar/TarEntry.Unix.cs | 10 +++++----- .../src/System/Formats/Tar/TarEntry.Windows.cs | 10 +++++----- .../src/System/Formats/Tar/TarEntry.cs | 16 ---------------- 3 files changed, 10 insertions(+), 26 deletions(-) diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.Unix.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.Unix.cs index 73f15a4996a7c8..58b36f9007e982 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.Unix.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.Unix.cs @@ -11,28 +11,28 @@ namespace System.Formats.Tar public abstract partial class TarEntry { // Unix specific implementation of the method that extracts the current entry as a block device. - partial void ExtractAsBlockDevice(string destinationFileName) + private void ExtractAsBlockDevice(string destinationFileName) { Debug.Assert(EntryType is TarEntryType.BlockDevice); Interop.CheckIo(Interop.Sys.CreateBlockDevice(destinationFileName, (uint)Mode, (uint)_header._devMajor, (uint)_header._devMinor), destinationFileName); } // Unix specific implementation of the method that extracts the current entry as a character device. - partial void ExtractAsCharacterDevice(string destinationFileName) + private void ExtractAsCharacterDevice(string destinationFileName) { Debug.Assert(EntryType is TarEntryType.CharacterDevice); Interop.CheckIo(Interop.Sys.CreateCharacterDevice(destinationFileName, (uint)Mode, (uint)_header._devMajor, (uint)_header._devMinor), destinationFileName); } // Unix specific implementation of the method that extracts the current entry as a fifo file. - partial void ExtractAsFifo(string destinationFileName) + private void ExtractAsFifo(string destinationFileName) { Debug.Assert(EntryType is TarEntryType.Fifo); Interop.CheckIo(Interop.Sys.MkFifo(destinationFileName, (uint)Mode), destinationFileName); } // Unix specific implementation of the method that extracts the current entry as a hard link. - partial void ExtractAsHardLink(string targetFilePath, string hardLinkFilePath) + private void ExtractAsHardLink(string targetFilePath, string hardLinkFilePath) { Debug.Assert(EntryType is TarEntryType.HardLink); Debug.Assert(!string.IsNullOrEmpty(targetFilePath)); @@ -41,7 +41,7 @@ partial void ExtractAsHardLink(string targetFilePath, string hardLinkFilePath) } // Unix specific implementation of the method that specifies the file permissions of the extracted file. - partial void SetModeOnFile(SafeFileHandle handle, string destinationFileName) + private void SetModeOnFile(SafeFileHandle handle, string destinationFileName) { // Only extract USR, GRP, and OTH file permissions, and ignore // S_ISUID, S_ISGID, and S_ISVTX bits. diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.Windows.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.Windows.cs index 45f8a1a0b8eac7..67abb9cb111cbc 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.Windows.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.Windows.cs @@ -10,28 +10,28 @@ namespace System.Formats.Tar public abstract partial class TarEntry { // Throws on Windows. Block devices are not supported on this platform. - partial void ExtractAsBlockDevice(string destinationFileName) + private void ExtractAsBlockDevice(string destinationFileName) { Debug.Assert(EntryType is TarEntryType.BlockDevice or TarEntryType.CharacterDevice); throw new InvalidOperationException(SR.IO_DeviceFiles_NotSupported); } // Throws on Windows. Character devices are not supported on this platform. - partial void ExtractAsCharacterDevice(string destinationFileName) + private void ExtractAsCharacterDevice(string destinationFileName) { Debug.Assert(EntryType is TarEntryType.BlockDevice or TarEntryType.CharacterDevice); throw new InvalidOperationException(SR.IO_DeviceFiles_NotSupported); } // Throws on Windows. Fifo files are not supported on this platform. - partial void ExtractAsFifo(string destinationFileName) + private void ExtractAsFifo(string destinationFileName) { Debug.Assert(EntryType is TarEntryType.Fifo); throw new InvalidOperationException(SR.IO_FifoFiles_NotSupported); } // Windows specific implementation of the method that extracts the current entry as a hard link. - partial void ExtractAsHardLink(string targetFilePath, string hardLinkFilePath) + private void ExtractAsHardLink(string targetFilePath, string hardLinkFilePath) { Debug.Assert(EntryType is TarEntryType.HardLink); Debug.Assert(!string.IsNullOrEmpty(targetFilePath)); @@ -41,7 +41,7 @@ partial void ExtractAsHardLink(string targetFilePath, string hardLinkFilePath) // Mode is not used on Windows. #pragma warning disable CA1822 // Member 'SetModeOnFile' does not access instance data and can be marked as static - partial void SetModeOnFile(SafeFileHandle handle, string destinationFileName) + private void SetModeOnFile(SafeFileHandle handle, string destinationFileName) #pragma warning restore CA1822 { // TODO: Verify that executables get their 'executable' permission applied on Windows when extracted, if applicable. https://github.com/dotnet/runtime/issues/68230 diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs index 6615c84abdc2c7..c7ff540aa49827 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs @@ -5,7 +5,6 @@ using System.IO; using System.Threading; using System.Threading.Tasks; -using Microsoft.Win32.SafeHandles; namespace System.Formats.Tar { @@ -594,20 +593,5 @@ private async Task ExtractAsRegularFileAsync(string destinationFileName, Cancell ArchivingUtils.AttemptSetLastWriteTime(destinationFileName, ModificationTime); } - - // Abstract method that extracts the current entry when it is a block device. - partial void ExtractAsBlockDevice(string destinationFileName); - - // Abstract method that extracts the current entry when it is a character device. - partial void ExtractAsCharacterDevice(string destinationFileName); - - // Abstract method that extracts the current entry when it is a fifo file. - partial void ExtractAsFifo(string destinationFileName); - - // Abstract method that extracts the current entry when it is a hard link. - partial void ExtractAsHardLink(string targetFilePath, string hardLinkFilePath); - - // Abstract method that sets the file permissions of the file. - partial void SetModeOnFile(SafeFileHandle handle, string destinationFileName); } } From e8e40cbaab70e3a18371632603908754a5195e88 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Fri, 17 Jun 2022 12:25:04 -0700 Subject: [PATCH 16/75] Don't use partial methods in TarWriter. --- .../src/System/Formats/Tar/TarWriter.Unix.cs | 4 ++-- .../src/System/Formats/Tar/TarWriter.Windows.cs | 4 ++-- .../System.Formats.Tar/src/System/Formats/Tar/TarWriter.cs | 6 ------ 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarWriter.Unix.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarWriter.Unix.cs index d64b0cb7436d7b..8a0990e0873cdc 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarWriter.Unix.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarWriter.Unix.cs @@ -16,7 +16,7 @@ public sealed partial class TarWriter : IDisposable private readonly Dictionary _groupIdentifiers = new Dictionary(); // Unix specific implementation of the method that reads an entry from disk and writes it into the archive stream. - partial void ReadFileFromDiskAndWriteToArchiveStreamAsEntry(string fullPath, string entryName) + private void ReadFileFromDiskAndWriteToArchiveStreamAsEntry(string fullPath, string entryName) { Interop.Sys.FileStatus status = default; status.Mode = default; @@ -104,7 +104,7 @@ partial void ReadFileFromDiskAndWriteToArchiveStreamAsEntry(string fullPath, str } // Unix specific implementation of the method that reads an entry from disk and writes it into the archive stream. - private async partial Task ReadFileFromDiskAndWriteToArchiveStreamAsEntryAsync(string fullPath, string entryName, CancellationToken cancellationToken) + private async Task ReadFileFromDiskAndWriteToArchiveStreamAsEntryAsync(string fullPath, string entryName, CancellationToken cancellationToken) { Interop.Sys.FileStatus status = default; status.Mode = default; diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarWriter.Windows.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarWriter.Windows.cs index 6a89a74f28983d..8c83666247d287 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarWriter.Windows.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarWriter.Windows.cs @@ -15,7 +15,7 @@ public sealed partial class TarWriter : IDisposable private const UnixFileMode DefaultWindowsMode = UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute | UnixFileMode.GroupRead | UnixFileMode.GroupWrite | UnixFileMode.GroupExecute | UnixFileMode.OtherRead | UnixFileMode.OtherWrite | UnixFileMode.UserExecute; // Windows specific implementation of the method that reads an entry from disk and writes it into the archive stream. - partial void ReadFileFromDiskAndWriteToArchiveStreamAsEntry(string fullPath, string entryName) + private void ReadFileFromDiskAndWriteToArchiveStreamAsEntry(string fullPath, string entryName) { TarEntryType entryType; FileAttributes attributes = File.GetAttributes(fullPath); @@ -81,7 +81,7 @@ partial void ReadFileFromDiskAndWriteToArchiveStreamAsEntry(string fullPath, str } // Windows specific implementation of the method that asynchronously reads an entry from disk and writes it into the archive stream. - private async partial Task ReadFileFromDiskAndWriteToArchiveStreamAsEntryAsync(string fullPath, string entryName, CancellationToken cancellationToken) + private async Task ReadFileFromDiskAndWriteToArchiveStreamAsEntryAsync(string fullPath, string entryName, CancellationToken cancellationToken) { TarEntryType entryType; FileAttributes attributes = File.GetAttributes(fullPath); 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 1e31e1394ed648..8766e94903c316 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 @@ -375,11 +375,5 @@ private async ValueTask WriteFinalRecordsAsync() await _archiveStream.WriteAsync(emptyRecord, cancellationToken: default).ConfigureAwait(false); await _archiveStream.WriteAsync(emptyRecord, cancellationToken: default).ConfigureAwait(false); } - - // Partial method for reading an entry from disk and writing it into the archive stream. - partial void ReadFileFromDiskAndWriteToArchiveStreamAsEntry(string fullPath, string entryName); - - // Partial method for reading an entry from disk and writing it into the archive stream. - private partial Task ReadFileFromDiskAndWriteToArchiveStreamAsEntryAsync(string fullPath, string entryName, CancellationToken cancellationToken); } } From 9aeffa4e8cd5d659f95aa3b09b007ed1ef353c8f Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Fri, 17 Jun 2022 14:26:26 -0700 Subject: [PATCH 17/75] TarFile exceptions async handling. --- .../src/System/Formats/Tar/TarFile.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarFile.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarFile.cs index 1e9f5cb2bc4097..bc98c2e1abd49a 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarFile.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarFile.cs @@ -56,12 +56,12 @@ public static Task CreateFromDirectoryAsync(string sourceDirectoryName, Stream d if (!destination.CanWrite) { - throw new IOException(SR.IO_NotSupported_UnwritableStream); + return Task.FromException(new IOException(SR.IO_NotSupported_UnwritableStream)); } if (!Directory.Exists(sourceDirectoryName)) { - throw new DirectoryNotFoundException(string.Format(SR.IO_PathNotFound_Path, sourceDirectoryName)); + return Task.FromException(new DirectoryNotFoundException(string.Format(SR.IO_PathNotFound_Path, sourceDirectoryName))); } // Rely on Path.GetFullPath for validation of paths @@ -115,7 +115,7 @@ public static Task CreateFromDirectoryAsync(string sourceDirectoryName, string d if (!Directory.Exists(sourceDirectoryName)) { - throw new DirectoryNotFoundException(string.Format(SR.IO_PathNotFound_Path, sourceDirectoryName)); + return Task.FromException(new DirectoryNotFoundException(string.Format(SR.IO_PathNotFound_Path, sourceDirectoryName))); } // Throws if the destination file exists @@ -173,12 +173,12 @@ public static Task ExtractToDirectoryAsync(Stream source, string destinationDire if (!source.CanRead) { - throw new IOException(SR.IO_NotSupported_UnreadableStream); + return Task.FromException(new IOException(SR.IO_NotSupported_UnreadableStream)); } if (!Directory.Exists(destinationDirectoryName)) { - throw new DirectoryNotFoundException(string.Format(SR.IO_PathNotFound_Path, destinationDirectoryName)); + return Task.FromException(new DirectoryNotFoundException(string.Format(SR.IO_PathNotFound_Path, destinationDirectoryName))); } // Rely on Path.GetFullPath for validation of paths @@ -242,12 +242,12 @@ public static Task ExtractToDirectoryAsync(string sourceFileName, string destina if (!File.Exists(sourceFileName)) { - throw new FileNotFoundException(string.Format(SR.IO_FileNotFound, sourceFileName)); + return Task.FromException(new FileNotFoundException(string.Format(SR.IO_FileNotFound, sourceFileName))); } if (!Directory.Exists(destinationDirectoryName)) { - throw new DirectoryNotFoundException(string.Format(SR.IO_PathNotFound_Path, destinationDirectoryName)); + return Task.FromException(new DirectoryNotFoundException(string.Format(SR.IO_PathNotFound_Path, destinationDirectoryName))); } using FileStream archive = File.OpenRead(sourceFileName); From ea3fab0db2cf54ab7215ae6f63d2e1dd8408d798 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Fri, 17 Jun 2022 14:50:54 -0700 Subject: [PATCH 18/75] Move TarWriter awaited code to separate methods. --- .../src/System/Formats/Tar/TarWriter.cs | 84 +++++++++++-------- 1 file changed, 50 insertions(+), 34 deletions(-) diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarWriter.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarWriter.cs index 8766e94903c316..f30d6feeacafed 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 @@ -132,7 +132,7 @@ public void WriteEntry(string fileName, string? entryName) /// The archive stream is disposed. /// or is or empty. /// An I/O problem occurred. - public async Task WriteEntryAsync(string fileName, string? entryName, CancellationToken cancellationToken = default) + public Task WriteEntryAsync(string fileName, string? entryName, CancellationToken cancellationToken = default) { ThrowIfDisposed(); @@ -145,7 +145,7 @@ public async Task WriteEntryAsync(string fileName, string? entryName, Cancellati entryName = Path.GetFileName(fileName); } - await ReadFileFromDiskAndWriteToArchiveStreamAsEntryAsync(fullPath, entryName, cancellationToken).ConfigureAwait(false); + return WriteEntryAsyncInternal(fullPath, entryName, cancellationToken); } /// @@ -259,40 +259,10 @@ public void WriteEntry(TarEntry entry) /// The archive stream is disposed. /// The entry type of the is not supported for writing. /// An I/O problem occurred. - public async Task WriteEntryAsync(TarEntry entry, CancellationToken cancellationToken = default) + public Task WriteEntryAsync(TarEntry entry, CancellationToken cancellationToken = default) { ThrowIfDisposed(); - - IMemoryOwner rented = MemoryPool.Shared.Rent(minBufferSize: TarHelpers.RecordSize); - Memory buffer = rented.Memory.Slice(TarHelpers.RecordSize); // minBufferSize means the array could've been larger - buffer.Span.Clear(); // Rented arrays aren't clean - try - { - switch (entry.Format) - { - case TarEntryFormat.V7: - await entry._header.WriteAsV7Async(_archiveStream, buffer, cancellationToken).ConfigureAwait(false); - break; - case TarEntryFormat.Ustar: - await entry._header.WriteAsUstarAsync(_archiveStream, buffer, cancellationToken).ConfigureAwait(false); - break; - case TarEntryFormat.Pax: - await entry._header.WriteAsPaxAsync(_archiveStream, buffer, cancellationToken).ConfigureAwait(false); - break; - case TarEntryFormat.Gnu: - await entry._header.WriteAsGnuAsync(_archiveStream, buffer, cancellationToken).ConfigureAwait(false); - break; - case TarEntryFormat.Unknown: - default: - throw new FormatException(string.Format(SR.TarInvalidFormat, Format)); - } - } - finally - { - rented.Dispose(); - } - - _wroteEntries = true; + return WriteEntryAsyncInternal(entry, cancellationToken); } // Disposes the current instance. @@ -358,6 +328,52 @@ private void ThrowIfDisposed() } } + private async Task WriteEntryAsyncInternal(string fullPath, string entryName, CancellationToken cancellationToken) + { + if (Format is TarEntryFormat.Pax) + { + await WriteGlobalExtendedAttributesEntryIfNeededAsync(cancellationToken).ConfigureAwait(false); + } + + await ReadFileFromDiskAndWriteToArchiveStreamAsEntryAsync(fullPath, entryName, cancellationToken).ConfigureAwait(false); + } + + private async Task WriteEntryAsyncInternal(TarEntry entry, CancellationToken cancellationToken) + { + await WriteGlobalExtendedAttributesEntryIfNeededAsync(cancellationToken).ConfigureAwait(false); + + IMemoryOwner rented = MemoryPool.Shared.Rent(minBufferSize: TarHelpers.RecordSize); + Memory buffer = rented.Memory.Slice(TarHelpers.RecordSize); // minBufferSize means the array could've been larger + buffer.Span.Clear(); // Rented arrays aren't clean + try + { + switch (entry.Format) + { + case TarEntryFormat.V7: + await entry._header.WriteAsV7Async(_archiveStream, buffer, cancellationToken).ConfigureAwait(false); + break; + case TarEntryFormat.Ustar: + await entry._header.WriteAsUstarAsync(_archiveStream, buffer, cancellationToken).ConfigureAwait(false); + break; + case TarEntryFormat.Pax: + await entry._header.WriteAsPaxAsync(_archiveStream, buffer, cancellationToken).ConfigureAwait(false); + break; + case TarEntryFormat.Gnu: + await entry._header.WriteAsGnuAsync(_archiveStream, buffer, cancellationToken).ConfigureAwait(false); + break; + case TarEntryFormat.Unknown: + default: + throw new FormatException(string.Format(SR.TarInvalidFormat, Format)); + } + } + finally + { + rented.Dispose(); + } + + _wroteEntries = true; + } + // The spec indicates that the end of the archive is indicated // by two records consisting entirely of zero bytes. private void WriteFinalRecords() From 99234b2ab74e7e711810b7a6c555b79d9dba7302 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Fri, 24 Jun 2022 11:29:59 -0700 Subject: [PATCH 19/75] Update Global Extended Attributes async methods to the latest rebase. --- .../src/System/Formats/Tar/TarHeader.Write.cs | 30 ++++++++++++------- .../src/System/Formats/Tar/TarWriter.cs | 28 +++++++---------- 2 files changed, 31 insertions(+), 27 deletions(-) diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs index 6c005db213e03a..3d33d4dcd1b75b 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 @@ -82,16 +82,6 @@ internal void WriteAsUstar(Stream archiveStream, Span buffer) } } - // Writes the current header as a PAX Global Extended Attributes entry into the archive stream. - internal void WriteAsPaxGlobalExtendedAttributes(Stream archiveStream, Span buffer, int globalExtendedAttributesEntryNumber) - { - Debug.Assert(_typeFlag is TarEntryType.GlobalExtendedAttributes); - - _name = GenerateGlobalExtendedAttributeName(globalExtendedAttributesEntryNumber); - _extendedAttributes ??= new Dictionary(); - WriteAsPaxExtendedAttributes(archiveStream, buffer, _extendedAttributes, isGea: true); - } - // Asynchronously rites the current header as a Ustar entry into the archive stream. internal async Task WriteAsUstarAsync(Stream archiveStream, Memory buffer, CancellationToken cancellationToken) { @@ -112,6 +102,26 @@ internal async Task WriteAsUstarAsync(Stream archiveStream, Memory buffer, } } + // Writes the current header as a PAX Global Extended Attributes entry into the archive stream. + internal void WriteAsPaxGlobalExtendedAttributes(Stream archiveStream, Span buffer, int globalExtendedAttributesEntryNumber) + { + Debug.Assert(_typeFlag is TarEntryType.GlobalExtendedAttributes); + + _name = GenerateGlobalExtendedAttributeName(globalExtendedAttributesEntryNumber); + _extendedAttributes ??= new Dictionary(); + WriteAsPaxExtendedAttributes(archiveStream, buffer, _extendedAttributes, isGea: true); + } + + // Writes the current header as a PAX Global Extended Attributes entry into the archive stream. + internal Task WriteAsPaxGlobalExtendedAttributesAsync(Stream archiveStream, Memory buffer, int globalExtendedAttributesEntryNumber, CancellationToken cancellationToken) + { + Debug.Assert(_typeFlag is TarEntryType.GlobalExtendedAttributes); + + _name = GenerateGlobalExtendedAttributeName(globalExtendedAttributesEntryNumber); + _extendedAttributes ??= new Dictionary(); + return WriteAsPaxExtendedAttributesAsync(archiveStream, buffer, _extendedAttributes, isGea: true, cancellationToken); + } + // Writes the current header as a PAX entry into the archive stream. // Makes sure to add the preceding extended attributes entry before the actual entry. internal void WriteAsPax(Stream archiveStream, Span buffer) 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 f30d6feeacafed..c4b88934444ddd 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 @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Buffers; -using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Threading; @@ -145,7 +144,7 @@ public Task WriteEntryAsync(string fileName, string? entryName, CancellationToke entryName = Path.GetFileName(fileName); } - return WriteEntryAsyncInternal(fullPath, entryName, cancellationToken); + return ReadFileFromDiskAndWriteToArchiveStreamAsEntryAsync(fullPath, entryName, cancellationToken); } /// @@ -299,8 +298,6 @@ private async ValueTask DisposeAsync(bool disposing) { try { - await WriteGlobalExtendedAttributesEntryIfNeededAsync(cancellationToken: default).ConfigureAwait(false); - if (_wroteEntries) { await WriteFinalRecordsAsync().ConfigureAwait(false); @@ -328,20 +325,9 @@ private void ThrowIfDisposed() } } - private async Task WriteEntryAsyncInternal(string fullPath, string entryName, CancellationToken cancellationToken) - { - if (Format is TarEntryFormat.Pax) - { - await WriteGlobalExtendedAttributesEntryIfNeededAsync(cancellationToken).ConfigureAwait(false); - } - - await ReadFileFromDiskAndWriteToArchiveStreamAsEntryAsync(fullPath, entryName, cancellationToken).ConfigureAwait(false); - } - + // Portion of the WriteEntryAsync(TarEntry, CancellationToken) method containing awaits. private async Task WriteEntryAsyncInternal(TarEntry entry, CancellationToken cancellationToken) { - await WriteGlobalExtendedAttributesEntryIfNeededAsync(cancellationToken).ConfigureAwait(false); - IMemoryOwner rented = MemoryPool.Shared.Rent(minBufferSize: TarHelpers.RecordSize); Memory buffer = rented.Memory.Slice(TarHelpers.RecordSize); // minBufferSize means the array could've been larger buffer.Span.Clear(); // Rented arrays aren't clean @@ -356,7 +342,15 @@ private async Task WriteEntryAsyncInternal(TarEntry entry, CancellationToken can await entry._header.WriteAsUstarAsync(_archiveStream, buffer, cancellationToken).ConfigureAwait(false); break; case TarEntryFormat.Pax: - await entry._header.WriteAsPaxAsync(_archiveStream, buffer, cancellationToken).ConfigureAwait(false); + if (entry._header._typeFlag is TarEntryType.GlobalExtendedAttributes) + { + await entry._header.WriteAsPaxGlobalExtendedAttributesAsync(_archiveStream, buffer, _nextGlobalExtendedAttributesEntryNumber, cancellationToken).ConfigureAwait(false); + _nextGlobalExtendedAttributesEntryNumber++; + } + else + { + await entry._header.WriteAsPaxAsync(_archiveStream, buffer, cancellationToken).ConfigureAwait(false); + } break; case TarEntryFormat.Gnu: await entry._header.WriteAsGnuAsync(_archiveStream, buffer, cancellationToken).ConfigureAwait(false); From 6cbd587f08e7c235310254c2c462700a0461948b Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Fri, 24 Jun 2022 11:31:07 -0700 Subject: [PATCH 20/75] Remove unused parameter in SetModeOnFile after latest rebase that uses File.SetUnixFileMode. --- .../System.Formats.Tar/src/System/Formats/Tar/TarEntry.Unix.cs | 2 +- .../src/System/Formats/Tar/TarEntry.Windows.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.Unix.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.Unix.cs index 58b36f9007e982..120f8a41da3893 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.Unix.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.Unix.cs @@ -41,7 +41,7 @@ private void ExtractAsHardLink(string targetFilePath, string hardLinkFilePath) } // Unix specific implementation of the method that specifies the file permissions of the extracted file. - private void SetModeOnFile(SafeFileHandle handle, string destinationFileName) + private void SetModeOnFile(SafeFileHandle handle) { // Only extract USR, GRP, and OTH file permissions, and ignore // S_ISUID, S_ISGID, and S_ISVTX bits. diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.Windows.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.Windows.cs index 67abb9cb111cbc..491e51d7cd0228 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.Windows.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.Windows.cs @@ -41,7 +41,7 @@ private void ExtractAsHardLink(string targetFilePath, string hardLinkFilePath) // Mode is not used on Windows. #pragma warning disable CA1822 // Member 'SetModeOnFile' does not access instance data and can be marked as static - private void SetModeOnFile(SafeFileHandle handle, string destinationFileName) + private void SetModeOnFile(SafeFileHandle handle) #pragma warning restore CA1822 { // TODO: Verify that executables get their 'executable' permission applied on Windows when extracted, if applicable. https://github.com/dotnet/runtime/issues/68230 From 1ba88bfe7bee8ac07630dcc9b7a2513ad0bf0b18 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Fri, 24 Jun 2022 11:31:26 -0700 Subject: [PATCH 21/75] Adjust TarEntry async methods to handle exceptions properly. --- .../src/System/Formats/Tar/TarEntry.cs | 51 ++++++++++++++----- 1 file changed, 38 insertions(+), 13 deletions(-) diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs index c7ff540aa49827..d3db1782c25d7b 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs @@ -19,8 +19,8 @@ public abstract partial class TarEntry // Used to access the data section of this entry in an unseekable file private TarReader? _readerOfOrigin; - // Constructor called when reading a TarEntry from a TarReader. - internal TarEntry(TarHeader header, TarReader readerOfOrigin, TarEntryFormat format) + // Constructor called when reading a TarEntry from a TarReader. + internal TarEntry(TarHeader header, TarReader readerOfOrigin, TarEntryFormat format) { // This constructor is called after reading a header from the archive, // and we should've already detected the format of the header. @@ -220,6 +220,7 @@ public int Uid /// Operation not permitted due to insufficient permissions. public void ExtractToFile(string destinationFileName, bool overwrite) { + ArgumentException.ThrowIfNullOrEmpty(destinationFileName); if (EntryType is TarEntryType.SymbolicLink or TarEntryType.HardLink or TarEntryType.GlobalExtendedAttributes) { throw new InvalidOperationException(string.Format(SR.TarEntryTypeNotSupportedForExtracting, EntryType)); @@ -248,9 +249,10 @@ public void ExtractToFile(string destinationFileName, bool overwrite) /// Operation not permitted due to insufficient permissions. public Task ExtractToFileAsync(string destinationFileName, bool overwrite, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrEmpty(destinationFileName); if (EntryType is TarEntryType.SymbolicLink or TarEntryType.HardLink) { - throw new InvalidOperationException(string.Format(SR.TarEntryTypeNotSupportedForExtracting, EntryType)); + return Task.FromException(new InvalidOperationException(string.Format(SR.TarEntryTypeNotSupportedForExtracting, EntryType))); } return ExtractToFileInternalAsync(destinationFileName, linkTargetPath: null, overwrite, cancellationToken); } @@ -400,8 +402,6 @@ internal Task ExtractRelativeToDirectoryAsync(string destinationDirectoryPath, b // Extracts the current entry into the filesystem, regardless of the entry type. private void ExtractToFileInternal(string filePath, string? linkTargetPath, bool overwrite) { - ArgumentException.ThrowIfNullOrEmpty(filePath); - VerifyPathsForEntryType(filePath, linkTargetPath, overwrite); if (EntryType is TarEntryType.RegularFile or TarEntryType.V7RegularFile or TarEntryType.ContiguousFile) @@ -415,19 +415,24 @@ private void ExtractToFileInternal(string filePath, string? linkTargetPath, bool } // Asynchronously extracts the current entry into the filesystem, regardless of the entry type. - private async Task ExtractToFileInternalAsync(string filePath, string? linkTargetPath, bool overwrite, CancellationToken cancellationToken) + private Task ExtractToFileInternalAsync(string filePath, string? linkTargetPath, bool overwrite, CancellationToken cancellationToken) { - ArgumentException.ThrowIfNullOrEmpty(filePath); - - VerifyPathsForEntryType(filePath, linkTargetPath, overwrite); + try + { + VerifyPathsForEntryType(filePath, linkTargetPath, overwrite); + } + catch (Exception e) + { + return Task.FromException(e); + } if (EntryType is TarEntryType.RegularFile or TarEntryType.V7RegularFile or TarEntryType.ContiguousFile) { - await ExtractAsRegularFileAsync(filePath, cancellationToken).ConfigureAwait(false); + return ExtractAsRegularFileAsync(filePath, cancellationToken); } else { - CreateNonRegularFile(filePath, linkTargetPath); + return CreateNonRegularFileAsync(filePath, linkTargetPath); } } @@ -481,6 +486,19 @@ private void CreateNonRegularFile(string filePath, string? linkTargetPath) } } + private Task CreateNonRegularFileAsync(string filePath, string? linkTargetPath) + { + try + { + CreateNonRegularFile(filePath, linkTargetPath); + return Task.CompletedTask; + } + catch (Exception e) + { + return Task.FromException(e); + } + } + // Verifies if the specified paths make sense for the current type of entry. private void VerifyPathsForEntryType(string filePath, string? linkTargetPath, bool overwrite) { @@ -560,7 +578,7 @@ private void ExtractAsRegularFile(string destinationFileName) // Important: The DataStream will be written from its current position DataStream.CopyTo(fs); } - SetModeOnFile(fs.SafeFileHandle, destinationFileName); + SetModeOnFile(fs.SafeFileHandle); } ArchivingUtils.AttemptSetLastWriteTime(destinationFileName, ModificationTime); @@ -588,7 +606,14 @@ private async Task ExtractAsRegularFileAsync(string destinationFileName, Cancell // Important: The DataStream will be written from its current position await DataStream.CopyToAsync(fs, cancellationToken).ConfigureAwait(false); } - SetModeOnFile(fs.SafeFileHandle, destinationFileName); + try + { + SetModeOnFile(fs.SafeFileHandle); + } + catch (Exception e) + { + await Task.FromException(e).ConfigureAwait(false); + } } ArchivingUtils.AttemptSetLastWriteTime(destinationFileName, ModificationTime); From e5a9c9b897e51e67bd807795777167bd67172297 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Fri, 24 Jun 2022 11:33:46 -0700 Subject: [PATCH 22/75] ref: Restore System.IAsyncDisposable inheritance in TarReader, it wasn't properly merged with latest rebase. --- src/libraries/System.Formats.Tar/ref/System.Formats.Tar.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/System.Formats.Tar/ref/System.Formats.Tar.cs b/src/libraries/System.Formats.Tar/ref/System.Formats.Tar.cs index a4bd74e0ff1022..d26411cc965b70 100644 --- a/src/libraries/System.Formats.Tar/ref/System.Formats.Tar.cs +++ b/src/libraries/System.Formats.Tar/ref/System.Formats.Tar.cs @@ -91,7 +91,7 @@ public static void ExtractToDirectory(string sourceFileName, string destinationD public static System.Threading.Tasks.Task ExtractToDirectoryAsync(System.IO.Stream source, string destinationDirectoryName, bool overwriteFiles, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public static System.Threading.Tasks.Task ExtractToDirectoryAsync(string sourceFileName, string destinationDirectoryName, bool overwriteFiles, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } } - public sealed partial class TarReader : System.IDisposable + public sealed partial class TarReader : System.IAsyncDisposable, System.IDisposable { public TarReader(System.IO.Stream archiveStream, bool leaveOpen = false) { } public void Dispose() { } From c9dbfe6c8dad25a046ea9945b9d9704aa253f5b0 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Fri, 24 Jun 2022 12:16:34 -0700 Subject: [PATCH 23/75] Add async tests for TarReader.TarEntry.ExtractToFile. Rename TarReader.ExtractToFile* test files to TarReader.TarEntry.ExtractToFile*, since the actual async method belongs to TarEntry. These test methods extract files coming from assets that are opened as archives by a TarReader. --- .../tests/System.Formats.Tar.Tests.csproj | 9 ++-- .../TarReader.ExtractToFile.Tests.Unix.cs | 43 ----------------- ...eader.TarEntry.ExtractToFile.Tests.Unix.cs | 46 +++++++++++++++++++ .../TarReader.TarEntry.ExtractToFile.Tests.cs | 37 +++++++++++++++ ....TarEntry.ExtractToFileAsync.Tests.Unix.cs | 46 +++++++++++++++++++ ...ader.TarEntry.ExtractToFileAsync.Tests.cs} | 5 +- 6 files changed, 135 insertions(+), 51 deletions(-) delete mode 100644 src/libraries/System.Formats.Tar/tests/TarReader/TarReader.ExtractToFile.Tests.Unix.cs create mode 100644 src/libraries/System.Formats.Tar/tests/TarReader/TarReader.TarEntry.ExtractToFile.Tests.Unix.cs create mode 100644 src/libraries/System.Formats.Tar/tests/TarReader/TarReader.TarEntry.ExtractToFile.Tests.cs create mode 100644 src/libraries/System.Formats.Tar/tests/TarReader/TarReader.TarEntry.ExtractToFileAsync.Tests.Unix.cs rename src/libraries/System.Formats.Tar/tests/TarReader/{TarReader.ExtractToFile.Tests.cs => TarReader.TarEntry.ExtractToFileAsync.Tests.cs} (95%) diff --git a/src/libraries/System.Formats.Tar/tests/System.Formats.Tar.Tests.csproj b/src/libraries/System.Formats.Tar/tests/System.Formats.Tar.Tests.csproj index ffcac5e244b4cd..a9a281a61863c4 100644 --- a/src/libraries/System.Formats.Tar/tests/System.Formats.Tar.Tests.csproj +++ b/src/libraries/System.Formats.Tar/tests/System.Formats.Tar.Tests.csproj @@ -24,7 +24,8 @@ - + + @@ -60,7 +61,8 @@ - + + @@ -73,9 +75,6 @@ - - - diff --git a/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.ExtractToFile.Tests.Unix.cs b/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.ExtractToFile.Tests.Unix.cs deleted file mode 100644 index 471d4430fd97e2..00000000000000 --- a/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.ExtractToFile.Tests.Unix.cs +++ /dev/null @@ -1,43 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.IO; -using Xunit; - -namespace System.Formats.Tar.Tests -{ - public partial class TarReader_ExtractToFile_Tests : TarTestsBase - { - [PlatformSpecific(TestPlatforms.AnyUnix & ~TestPlatforms.tvOS)] // https://github.com/dotnet/runtime/issues/68360 - [Fact] - public void ExtractToFile_SpecialFile_Unelevated_Throws() - { - using TempDirectory root = new TempDirectory(); - using MemoryStream ms = GetTarMemoryStream(CompressionMethod.Uncompressed, TestTarFormat.ustar, "specialfiles"); - - using TarReader reader = new TarReader(ms); - - string path = Path.Join(root.Path, "output"); - - // Block device requires elevation for writing - PosixTarEntry blockDevice = reader.GetNextEntry() as PosixTarEntry; - Assert.NotNull(blockDevice); - Assert.Throws(() => blockDevice.ExtractToFile(path, overwrite: false)); - Assert.False(File.Exists(path)); - - // Character device requires elevation for writing - PosixTarEntry characterDevice = reader.GetNextEntry() as PosixTarEntry; - Assert.NotNull(characterDevice); - Assert.Throws(() => characterDevice.ExtractToFile(path, overwrite: false)); - Assert.False(File.Exists(path)); - - // Fifo does not require elevation, should succeed - PosixTarEntry fifo = reader.GetNextEntry() as PosixTarEntry; - Assert.NotNull(fifo); - fifo.ExtractToFile(path, overwrite: false); - Assert.True(File.Exists(path)); - - Assert.Null(reader.GetNextEntry()); - } - } -} diff --git a/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.TarEntry.ExtractToFile.Tests.Unix.cs b/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.TarEntry.ExtractToFile.Tests.Unix.cs new file mode 100644 index 00000000000000..fb9ee0473166d5 --- /dev/null +++ b/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.TarEntry.ExtractToFile.Tests.Unix.cs @@ -0,0 +1,46 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; +using System.Threading.Tasks; +using Xunit; + +namespace System.Formats.Tar.Tests +{ + public partial class TarReader_TarEntry_ExtractToFile_Tests : TarTestsBase + { + [PlatformSpecific(TestPlatforms.AnyUnix & ~TestPlatforms.tvOS)] // https://github.com/dotnet/runtime/issues/68360 + [Fact] + public async Task SpecialFile_Unelevated_Throws_Async() + { + using TempDirectory root = new TempDirectory(); + using MemoryStream ms = GetTarMemoryStream(CompressionMethod.Uncompressed, TestTarFormat.ustar, "specialfiles"); + + TarReader reader = new TarReader(ms); + await using (reader) + { + string path = Path.Join(root.Path, "output"); + + // Block device requires elevation for writing + PosixTarEntry blockDevice = await reader.GetNextEntryAsync() as PosixTarEntry; + Assert.NotNull(blockDevice); + await Assert.ThrowsAsync(async () => await blockDevice.ExtractToFileAsync(path, overwrite: false)); + Assert.False(File.Exists(path)); + + // Character device requires elevation for writing + PosixTarEntry characterDevice = await reader.GetNextEntryAsync() as PosixTarEntry; + Assert.NotNull(characterDevice); + await Assert.ThrowsAsync(async () => await characterDevice.ExtractToFileAsync(path, overwrite: false)); + Assert.False(File.Exists(path)); + + // Fifo does not require elevation, should succeed + PosixTarEntry fifo = await reader.GetNextEntryAsync() as PosixTarEntry; + Assert.NotNull(fifo); + await fifo.ExtractToFileAsync(path, overwrite: false); + Assert.True(File.Exists(path)); + + Assert.Null(await reader.GetNextEntryAsync()); + } + } + } +} diff --git a/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.TarEntry.ExtractToFile.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.TarEntry.ExtractToFile.Tests.cs new file mode 100644 index 00000000000000..d07eb724099edc --- /dev/null +++ b/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.TarEntry.ExtractToFile.Tests.cs @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; +using Xunit; + +namespace System.Formats.Tar.Tests +{ + public partial class TarReader_TarEntry_ExtractToFile_Tests : TarTestsBase + { + [Fact] + public void EntriesWithSlashDotPrefix() + { + using TempDirectory root = new TempDirectory(); + + using MemoryStream archiveStream = GetStrangeTarMemoryStream("prefixDotSlashAndCurrentFolderEntry"); + using (TarReader reader = new TarReader(archiveStream, leaveOpen: false)) + { + string rootPath = Path.TrimEndingDirectorySeparator(root.Path); + TarEntry entry; + while ((entry = reader.GetNextEntry()) != null) + { + Assert.NotNull(entry); + Assert.StartsWith("./", entry.Name); + // Normalize the path (remove redundant segments), remove trailing separators + // this is so the first entry can be skipped if it's the same as the root directory + string entryPath = Path.TrimEndingDirectorySeparator(Path.GetFullPath(Path.Join(rootPath, entry.Name))); + if (entryPath != rootPath) + { + entry.ExtractToFile(entryPath, overwrite: true); + Assert.True(Path.Exists(entryPath), $"Entry was not extracted: {entryPath}"); + } + } + } + } + } +} diff --git a/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.TarEntry.ExtractToFileAsync.Tests.Unix.cs b/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.TarEntry.ExtractToFileAsync.Tests.Unix.cs new file mode 100644 index 00000000000000..750cd4d181e5b4 --- /dev/null +++ b/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.TarEntry.ExtractToFileAsync.Tests.Unix.cs @@ -0,0 +1,46 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; +using System.Threading.Tasks; +using Xunit; + +namespace System.Formats.Tar.Tests +{ + public partial class TarReader_TarEntry_ExtractToFileAsync_Tests : TarTestsBase + { + [PlatformSpecific(TestPlatforms.AnyUnix & ~TestPlatforms.tvOS)] // https://github.com/dotnet/runtime/issues/68360 + [Fact] + public async Task SpecialFile_Unelevated_Throws_Async() + { + using TempDirectory root = new TempDirectory(); + using MemoryStream ms = GetTarMemoryStream(CompressionMethod.Uncompressed, TestTarFormat.ustar, "specialfiles"); + + TarReader reader = new TarReader(ms); + await using (reader) + { + string path = Path.Join(root.Path, "output"); + + // Block device requires elevation for writing + PosixTarEntry blockDevice = await reader.GetNextEntryAsync() as PosixTarEntry; + Assert.NotNull(blockDevice); + await Assert.ThrowsAsync(async () => await blockDevice.ExtractToFileAsync(path, overwrite: false)); + Assert.False(File.Exists(path)); + + // Character device requires elevation for writing + PosixTarEntry characterDevice = await reader.GetNextEntryAsync() as PosixTarEntry; + Assert.NotNull(characterDevice); + await Assert.ThrowsAsync(async () => await characterDevice.ExtractToFileAsync(path, overwrite: false)); + Assert.False(File.Exists(path)); + + // Fifo does not require elevation, should succeed + PosixTarEntry fifo = await reader.GetNextEntryAsync() as PosixTarEntry; + Assert.NotNull(fifo); + await fifo.ExtractToFileAsync(path, overwrite: false); + Assert.True(File.Exists(path)); + + Assert.Null(await reader.GetNextEntryAsync()); + } + } + } +} diff --git a/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.ExtractToFile.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.TarEntry.ExtractToFileAsync.Tests.cs similarity index 95% rename from src/libraries/System.Formats.Tar/tests/TarReader/TarReader.ExtractToFile.Tests.cs rename to src/libraries/System.Formats.Tar/tests/TarReader/TarReader.TarEntry.ExtractToFileAsync.Tests.cs index c347c5e4fdc3d0..154f10446df24f 100644 --- a/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.ExtractToFile.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.TarEntry.ExtractToFileAsync.Tests.cs @@ -6,7 +6,7 @@ namespace System.Formats.Tar.Tests { - public partial class TarReader_ExtractToFile_Tests : TarTestsBase + public partial class TarReader_ExtractToFileAsync_Tests : TarTestsBase { [Fact] public void ExtractEntriesWithSlashDotPrefix() @@ -33,6 +33,5 @@ public void ExtractEntriesWithSlashDotPrefix() } } } - } -} \ No newline at end of file +} From e0029e9c864effec1f61081dc49b96f812e09f6a Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Fri, 24 Jun 2022 12:41:07 -0700 Subject: [PATCH 24/75] Adjust rented buffer handling based on feedback. --- .../src/System/Formats/Tar/TarHeader.Read.cs | 78 ++++++++++--------- 1 file changed, 42 insertions(+), 36 deletions(-) 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 f3e5ad6ca62f81..13042fc062ca0b 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 @@ -63,65 +63,71 @@ internal bool TryGetNextHeader(Stream archiveStream, bool copyData) ProcessDataBlock(archiveStream, copyData); + ArrayPool.Shared.Return(rented); return true; } finally { - ArrayPool.Shared.Return(rented); } } // 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 async ValueTask TryGetNextHeaderAsync(Stream archiveStream, bool copyData, CancellationToken cancellationToken) + internal ValueTask TryGetNextHeaderAsync(Stream archiveStream, bool copyData, CancellationToken cancellationToken) { - // The four supported formats have a header that fits in the default record size - IMemoryOwner rented = MemoryPool.Shared.Rent(minBufferSize: TarHelpers.RecordSize); + try + { + // The four supported formats have a header that fits in the default record size + byte[] rented = ArrayPool.Shared.Rent(minimumLength: TarHelpers.RecordSize); + Memory buffer = rented.AsMemory(0, TarHelpers.RecordSize); // minimumLength means the array could've been larger + + ValueTask result = TryGetNextHeaderAsyncInternal(archiveStream, buffer, copyData, cancellationToken); - Memory buffer = rented.Memory.Slice(0, TarHelpers.RecordSize); // minBufferSize means the array could've been larger + ArrayPool.Shared.Return(rented); + return result; + } + finally + { + } + } + private async ValueTask TryGetNextHeaderAsyncInternal(Stream archiveStream, Memory buffer, bool copyData, CancellationToken cancellationToken) + { await archiveStream.ReadExactlyAsync(buffer, cancellationToken).ConfigureAwait(false); - try + // Confirms if v7 or pax, or tentatively selects ustar + if (!TryReadCommonAttributes(buffer.Span)) { - // Confirms if v7 or pax, or tentatively selects ustar - if (!TryReadCommonAttributes(buffer.Span)) - { - return false; - } + return false; + } - // Confirms if gnu, or tentatively selects ustar - ReadMagicAttribute(buffer.Span); + // Confirms if gnu, or tentatively selects ustar + ReadMagicAttribute(buffer.Span); - if (_format != TarEntryFormat.V7) - { - // Confirms if gnu - ReadVersionAttribute(buffer.Span); + if (_format != TarEntryFormat.V7) + { + // Confirms if gnu + ReadVersionAttribute(buffer.Span); - // Fields that ustar, pax and gnu share identically - ReadPosixAndGnuSharedAttributes(buffer.Span); + // Fields that ustar, pax and gnu share identically + ReadPosixAndGnuSharedAttributes(buffer.Span); - Debug.Assert(_format is TarEntryFormat.Ustar or TarEntryFormat.Pax or TarEntryFormat.Gnu); - if (_format == TarEntryFormat.Ustar) - { - ReadUstarAttributes(buffer.Span); - } - else if (_format == TarEntryFormat.Gnu) - { - ReadGnuAttributes(buffer.Span); - } - // In PAX, there is nothing to read in this section (empty space) + Debug.Assert(_format is TarEntryFormat.Ustar or TarEntryFormat.Pax or TarEntryFormat.Gnu); + if (_format == TarEntryFormat.Ustar) + { + ReadUstarAttributes(buffer.Span); + } + else if (_format == TarEntryFormat.Gnu) + { + ReadGnuAttributes(buffer.Span); } + // In PAX, there is nothing to read in this section (empty space) + } - await ProcessDataBlockAsync(archiveStream, copyData, cancellationToken).ConfigureAwait(false); + await ProcessDataBlockAsync(archiveStream, copyData, cancellationToken).ConfigureAwait(false); - return true; - } - finally - { - rented.Dispose(); - } + return true; } // Reads the elements from the passed dictionary, which comes from the previous extended attributes entry, From c6576662021c3cf486354b3719ecb2bec568195d Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Fri, 24 Jun 2022 13:15:11 -0700 Subject: [PATCH 25/75] This needs to be awaited. --- .../src/System/Formats/Tar/TarHeader.Read.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) 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 13042fc062ca0b..3000d0569f0486 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 @@ -74,18 +74,19 @@ internal bool TryGetNextHeader(Stream archiveStream, bool copyData) // 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 ValueTask TryGetNextHeaderAsync(Stream archiveStream, bool copyData, CancellationToken cancellationToken) + internal async ValueTask TryGetNextHeaderAsync(Stream archiveStream, bool copyData, CancellationToken cancellationToken) { try { // The four supported formats have a header that fits in the default record size byte[] rented = ArrayPool.Shared.Rent(minimumLength: TarHelpers.RecordSize); Memory buffer = rented.AsMemory(0, TarHelpers.RecordSize); // minimumLength means the array could've been larger + buffer.Span.Clear(); // Rented arrays aren't clean - ValueTask result = TryGetNextHeaderAsyncInternal(archiveStream, buffer, copyData, cancellationToken); + await TryGetNextHeaderAsyncInternal(archiveStream, buffer, copyData, cancellationToken).ConfigureAwait(false); ArrayPool.Shared.Return(rented); - return result; + return true; } finally { From 97f31d992629774cc2fdbf5c58bf7792253b87bd Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Fri, 24 Jun 2022 13:19:03 -0700 Subject: [PATCH 26/75] Reserve ArrayPool in TarWriter WriteEntryAsyncInternal, not MemoryPool. Remove the try finally, there is no return value in this method. --- .../src/System/Formats/Tar/TarWriter.cs | 67 +++++++++---------- 1 file changed, 33 insertions(+), 34 deletions(-) diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarWriter.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarWriter.cs index c4b88934444ddd..adccf84a4568eb 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 @@ -328,44 +328,43 @@ private void ThrowIfDisposed() // Portion of the WriteEntryAsync(TarEntry, CancellationToken) method containing awaits. private async Task WriteEntryAsyncInternal(TarEntry entry, CancellationToken cancellationToken) { - IMemoryOwner rented = MemoryPool.Shared.Rent(minBufferSize: TarHelpers.RecordSize); - Memory buffer = rented.Memory.Slice(TarHelpers.RecordSize); // minBufferSize means the array could've been larger + byte[] rented = ArrayPool.Shared.Rent(minimumLength: TarHelpers.RecordSize); + Memory buffer = rented.AsMemory(0, TarHelpers.RecordSize); // minimumLength means the array could've been larger buffer.Span.Clear(); // Rented arrays aren't clean - try - { - switch (entry.Format) - { - case TarEntryFormat.V7: - await entry._header.WriteAsV7Async(_archiveStream, buffer, cancellationToken).ConfigureAwait(false); - break; - case TarEntryFormat.Ustar: - await entry._header.WriteAsUstarAsync(_archiveStream, buffer, cancellationToken).ConfigureAwait(false); - break; - case TarEntryFormat.Pax: - if (entry._header._typeFlag is TarEntryType.GlobalExtendedAttributes) - { - await entry._header.WriteAsPaxGlobalExtendedAttributesAsync(_archiveStream, buffer, _nextGlobalExtendedAttributesEntryNumber, cancellationToken).ConfigureAwait(false); - _nextGlobalExtendedAttributesEntryNumber++; - } - else - { - await entry._header.WriteAsPaxAsync(_archiveStream, buffer, cancellationToken).ConfigureAwait(false); - } - break; - case TarEntryFormat.Gnu: - await entry._header.WriteAsGnuAsync(_archiveStream, buffer, cancellationToken).ConfigureAwait(false); - break; - case TarEntryFormat.Unknown: - default: - throw new FormatException(string.Format(SR.TarInvalidFormat, Format)); - } - } - finally + + switch (entry.Format) { - rented.Dispose(); - } + case TarEntryFormat.V7: + await entry._header.WriteAsV7Async(_archiveStream, buffer, cancellationToken).ConfigureAwait(false); + break; + case TarEntryFormat.Ustar: + await entry._header.WriteAsUstarAsync(_archiveStream, buffer, cancellationToken).ConfigureAwait(false); + break; + + case TarEntryFormat.Pax: + if (entry._header._typeFlag is TarEntryType.GlobalExtendedAttributes) + { + await entry._header.WriteAsPaxGlobalExtendedAttributesAsync(_archiveStream, buffer, _nextGlobalExtendedAttributesEntryNumber, cancellationToken).ConfigureAwait(false); + _nextGlobalExtendedAttributesEntryNumber++; + } + else + { + await entry._header.WriteAsPaxAsync(_archiveStream, buffer, cancellationToken).ConfigureAwait(false); + } + break; + + case TarEntryFormat.Gnu: + await entry._header.WriteAsGnuAsync(_archiveStream, buffer, cancellationToken).ConfigureAwait(false); + break; + + case TarEntryFormat.Unknown: + default: + throw new FormatException(string.Format(SR.TarInvalidFormat, Format)); + } _wroteEntries = true; + + ArrayPool.Shared.Return(rented); } // The spec indicates that the end of the archive is indicated From 4248de92d342f01ccd77e0d5839c5ade2d1e2ec2 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Fri, 24 Jun 2022 13:19:36 -0700 Subject: [PATCH 27/75] Split writing methods in TarHeader into span based and memory based. --- .../src/System/Formats/Tar/TarHeader.Write.cs | 77 ++++++++++++++----- 1 file changed, 57 insertions(+), 20 deletions(-) diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs index 3d33d4dcd1b75b..bac309a9bc87fa 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 @@ -32,7 +32,7 @@ internal void WriteAsV7(Stream archiveStream, Span buffer) long actualLength = GetTotalDataBytesToWrite(); TarEntryType actualEntryType = GetCorrectTypeFlagForFormat(TarEntryFormat.V7); - int checksum = WriteName(buffer, out _); + int checksum = WriteNameSpan(buffer, out _); checksum += WriteCommonFields(buffer, actualLength, actualEntryType); WriteChecksum(checksum, buffer); @@ -50,7 +50,7 @@ internal async Task WriteAsV7Async(Stream archiveStream, Memory buffer, Ca long actualLength = GetTotalDataBytesToWrite(); TarEntryType actualEntryType = GetCorrectTypeFlagForFormat(TarEntryFormat.V7); - int checksum = WriteName(buffer.Span, out _); + int checksum = WriteNameMemory(buffer, out _); checksum += WriteCommonFields(buffer.Span, actualLength, actualEntryType); WriteChecksum(checksum, buffer.Span); @@ -68,7 +68,7 @@ internal void WriteAsUstar(Stream archiveStream, Span buffer) long actualLength = GetTotalDataBytesToWrite(); TarEntryType actualEntryType = GetCorrectTypeFlagForFormat(TarEntryFormat.Ustar); - int checksum = WritePosixName(buffer); + int checksum = WritePosixNameSpan(buffer); checksum += WriteCommonFields(buffer, actualLength, actualEntryType); checksum += WritePosixMagicAndVersion(buffer); checksum += WritePosixAndGnuSharedFields(buffer); @@ -88,7 +88,7 @@ internal async Task WriteAsUstarAsync(Stream archiveStream, Memory buffer, long actualLength = GetTotalDataBytesToWrite(); TarEntryType actualEntryType = GetCorrectTypeFlagForFormat(TarEntryFormat.Ustar); - int checksum = WritePosixName(buffer.Span); + int checksum = WritePosixNameMemory(buffer); checksum += WriteCommonFields(buffer.Span, actualLength, actualEntryType); checksum += WritePosixMagicAndVersion(buffer.Span); checksum += WritePosixAndGnuSharedFields(buffer.Span); @@ -261,7 +261,7 @@ internal void WriteAsGnuInternal(Stream archiveStream, Span buffer) long actualLength = GetTotalDataBytesToWrite(); TarEntryType actualEntryType = GetCorrectTypeFlagForFormat(TarEntryFormat.Gnu); - int checksum = WriteName(buffer, out _); + int checksum = WriteNameSpan(buffer, out _); checksum += WriteCommonFields(buffer, actualLength, actualEntryType); checksum += WriteGnuMagicAndVersion(buffer); checksum += WritePosixAndGnuSharedFields(buffer); @@ -287,7 +287,7 @@ internal async Task WriteAsGnuInternalAsync(Stream archiveStream, Memory b long actualLength = GetTotalDataBytesToWrite(); TarEntryType actualEntryType = GetCorrectTypeFlagForFormat(TarEntryFormat.Gnu); - int checksum = WriteName(buffer.Span, out _); + int checksum = WriteNameMemory(buffer, out _); checksum += WriteCommonFields(buffer.Span, actualLength, actualEntryType); checksum += WriteGnuMagicAndVersion(buffer.Span); checksum += WritePosixAndGnuSharedFields(buffer.Span); @@ -347,7 +347,7 @@ private void WriteAsPaxInternal(Stream archiveStream, Span buffer) long actualLength = GetTotalDataBytesToWrite(); TarEntryType actualEntryType = GetCorrectTypeFlagForFormat(TarEntryFormat.Pax); - int checksum = WritePosixName(buffer); + int checksum = WritePosixNameSpan(buffer); checksum += WriteCommonFields(buffer, actualLength, actualEntryType); checksum += WritePosixMagicAndVersion(buffer); checksum += WritePosixAndGnuSharedFields(buffer); @@ -368,7 +368,7 @@ private async Task WriteAsPaxInternalAsync(Stream archiveStream, Memory bu long actualLength = GetTotalDataBytesToWrite(); TarEntryType actualEntryType = GetCorrectTypeFlagForFormat(TarEntryFormat.Pax); - int checksum = WritePosixName(buffer.Span); + int checksum = WritePosixNameMemory(buffer); checksum += WriteCommonFields(buffer.Span, actualLength, actualEntryType); checksum += WritePosixMagicAndVersion(buffer.Span); checksum += WritePosixAndGnuSharedFields(buffer.Span); @@ -383,22 +383,43 @@ private async Task WriteAsPaxInternalAsync(Stream archiveStream, Memory bu } // All formats save in the name byte array only the ASCII bytes that fit. The full string is returned in the out byte array. - private int WriteName(Span buffer, out byte[] fullNameBytes) + private int WriteNameSpan(Span buffer, out byte[] fullNameBytes) { fullNameBytes = Encoding.ASCII.GetBytes(_name); int nameBytesLength = Math.Min(fullNameBytes.Length, FieldLengths.Name); - int checksum = WriteLeftAlignedBytesAndGetChecksum(fullNameBytes.AsSpan(0, nameBytesLength), buffer.Slice(FieldLocations.Name, FieldLengths.Name)); + int checksum = WriteLeftAlignedBytesAndGetChecksumSpan(fullNameBytes.AsSpan(0, nameBytesLength), buffer.Slice(FieldLocations.Name, FieldLengths.Name)); + return checksum; + } + + // All formats save in the name byte array only the ASCII bytes that fit. The full string is returned in the out byte array. + private int WriteNameMemory(Memory buffer, out byte[] fullNameBytes) + { + fullNameBytes = Encoding.ASCII.GetBytes(_name); + int nameBytesLength = Math.Min(fullNameBytes.Length, FieldLengths.Name); + int checksum = WriteLeftAlignedBytesAndGetChecksumMemory(fullNameBytes.AsMemory(0, nameBytesLength), buffer.Slice(FieldLocations.Name, FieldLengths.Name)); return checksum; } // Ustar and PAX save in the name byte array only the ASCII bytes that fit, and the rest of that string is saved in the prefix field. - private int WritePosixName(Span buffer) + private int WritePosixNameSpan(Span buffer) { - int checksum = WriteName(buffer, out byte[] fullNameBytes); + int checksum = WriteNameSpan(buffer, out byte[] fullNameBytes); if (fullNameBytes.Length > FieldLengths.Name) { int prefixBytesLength = Math.Min(fullNameBytes.Length - FieldLengths.Name, FieldLengths.Name); - checksum += WriteLeftAlignedBytesAndGetChecksum(fullNameBytes.AsSpan(FieldLengths.Name, prefixBytesLength), buffer.Slice(FieldLocations.Prefix, FieldLengths.Prefix)); + checksum += WriteLeftAlignedBytesAndGetChecksumSpan(fullNameBytes.AsSpan(FieldLengths.Name, prefixBytesLength), buffer.Slice(FieldLocations.Prefix, FieldLengths.Prefix)); + } + return checksum; + } + + // Ustar and PAX save in the name byte array only the ASCII bytes that fit, and the rest of that string is saved in the prefix field. + private int WritePosixNameMemory(Memory buffer) + { + int checksum = WriteNameMemory(buffer, out byte[] fullNameBytes); + if (fullNameBytes.Length > FieldLengths.Name) + { + int prefixBytesLength = Math.Min(fullNameBytes.Length - FieldLengths.Name, FieldLengths.Name); + checksum += WriteLeftAlignedBytesAndGetChecksumMemory(fullNameBytes.AsMemory(FieldLengths.Name, prefixBytesLength), buffer.Slice(FieldLocations.Prefix, FieldLengths.Prefix)); } return checksum; } @@ -482,16 +503,16 @@ private long GetTotalDataBytesToWrite() // Writes the magic and version fields of a ustar or pax entry into the specified spans. private static int WritePosixMagicAndVersion(Span buffer) { - int checksum = WriteLeftAlignedBytesAndGetChecksum(PaxMagicBytes, buffer.Slice(FieldLocations.Magic, FieldLengths.Magic)); - checksum += WriteLeftAlignedBytesAndGetChecksum(PaxVersionBytes, buffer.Slice(FieldLocations.Version, FieldLengths.Version)); + int checksum = WriteLeftAlignedBytesAndGetChecksumSpan(PaxMagicBytes, buffer.Slice(FieldLocations.Magic, FieldLengths.Magic)); + checksum += WriteLeftAlignedBytesAndGetChecksumSpan(PaxVersionBytes, buffer.Slice(FieldLocations.Version, FieldLengths.Version)); return checksum; } // Writes the magic and vresion fields of a gnu entry into the specified spans. private static int WriteGnuMagicAndVersion(Span buffer) { - int checksum = WriteLeftAlignedBytesAndGetChecksum(GnuMagicBytes, buffer.Slice(FieldLocations.Magic, FieldLengths.Magic)); - checksum += WriteLeftAlignedBytesAndGetChecksum(GnuVersionBytes, buffer.Slice(FieldLocations.Version, FieldLengths.Version)); + int checksum = WriteLeftAlignedBytesAndGetChecksumSpan(GnuMagicBytes, buffer.Slice(FieldLocations.Magic, FieldLengths.Magic)); + checksum += WriteLeftAlignedBytesAndGetChecksumSpan(GnuVersionBytes, buffer.Slice(FieldLocations.Version, FieldLengths.Version)); return checksum; } @@ -531,7 +552,7 @@ private int WriteGnuFields(Span buffer) if (_gnuUnusedBytes != null) { - checksum += WriteLeftAlignedBytesAndGetChecksum(_gnuUnusedBytes, buffer.Slice(FieldLocations.GnuUnused, FieldLengths.AllGnuUnused)); + checksum += WriteLeftAlignedBytesAndGetChecksumSpan(_gnuUnusedBytes, buffer.Slice(FieldLocations.GnuUnused, FieldLengths.AllGnuUnused)); } return checksum; @@ -704,7 +725,7 @@ internal void WriteChecksum(int checksum, Span buffer) } // Writes the specified bytes into the specified destination, aligned to the left. Returns the sum of the value of all the bytes that were written. - private static int WriteLeftAlignedBytesAndGetChecksum(ReadOnlySpan bytesToWrite, Span destination) + private static int WriteLeftAlignedBytesAndGetChecksumSpan(ReadOnlySpan bytesToWrite, Span destination) { Debug.Assert(destination.Length > 1); @@ -719,6 +740,22 @@ private static int WriteLeftAlignedBytesAndGetChecksum(ReadOnlySpan bytesT return checksum; } + // Writes the specified bytes into the specified destination, aligned to the left. Returns the sum of the value of all the bytes that were written. + private static int WriteLeftAlignedBytesAndGetChecksumMemory(ReadOnlyMemory bytesToWrite, Memory destination) + { + Debug.Assert(destination.Length > 1); + + int checksum = 0; + + for (int i = 0, j = 0; i < destination.Length && j < bytesToWrite.Length; i++, j++) + { + destination.Span[i] = bytesToWrite.Span[j]; + checksum += destination.Span[i]; + } + + return checksum; + } + // Writes the specified bytes aligned to the right, filling all the leading bytes with the zero char 0x30, // ensuring a null terminator is included at the end of the specified span. private static int WriteRightAlignedBytesAndGetChecksum(ReadOnlySpan bytesToWrite, Span destination) @@ -768,7 +805,7 @@ private static int WriteAsTimestamp(DateTimeOffset timestamp, Span destina private static int WriteAsAsciiString(string str, Span buffer, int location, int length) { byte[] bytes = Encoding.ASCII.GetBytes(str); - return WriteLeftAlignedBytesAndGetChecksum(bytes.AsSpan(), buffer.Slice(location, length)); + return WriteLeftAlignedBytesAndGetChecksumSpan(bytes.AsSpan(), buffer.Slice(location, length)); } // Gets the special name for the 'name' field in an extended attribute entry. From dcd9dfcaebc5626246347685ba8f63354f89b86f Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Fri, 24 Jun 2022 13:26:33 -0700 Subject: [PATCH 28/75] tests: TarReader.GetNextEntryAsync. --- .../tests/System.Formats.Tar.Tests.csproj | 1 + .../TarReader.GetNextEntryAsync.Tests.cs | 276 ++++++++++++++++++ 2 files changed, 277 insertions(+) create mode 100644 src/libraries/System.Formats.Tar/tests/TarReader/TarReader.GetNextEntryAsync.Tests.cs diff --git a/src/libraries/System.Formats.Tar/tests/System.Formats.Tar.Tests.csproj b/src/libraries/System.Formats.Tar/tests/System.Formats.Tar.Tests.csproj index a9a281a61863c4..428b24301edb80 100644 --- a/src/libraries/System.Formats.Tar/tests/System.Formats.Tar.Tests.csproj +++ b/src/libraries/System.Formats.Tar/tests/System.Formats.Tar.Tests.csproj @@ -22,6 +22,7 @@ + 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 new file mode 100644 index 00000000000000..79d4ac789b320b --- /dev/null +++ b/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.GetNextEntryAsync.Tests.cs @@ -0,0 +1,276 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; +using System.Threading.Tasks; +using Xunit; + +namespace System.Formats.Tar.Tests +{ + public class TarReader_GetNextEntryAsync_Tests : TarTestsBase + { + [Fact] + public async Task MalformedArchive_TooSmall_Async() + { + using MemoryStream malformed = new MemoryStream(); + byte[] buffer = new byte[] { 0x1 }; + malformed.Write(buffer); + malformed.Seek(0, SeekOrigin.Begin); + + TarReader reader = new TarReader(malformed); + await using (reader) + { + await Assert.ThrowsAsync(async () => await reader.GetNextEntryAsync()); + } + } + + [Fact] + public async Task MalformedArchive_HeaderSize_Async() + { + using MemoryStream malformed = new MemoryStream(); + byte[] buffer = new byte[512]; // Minimum length of any header + Array.Fill(buffer, 0x1); + malformed.Write(buffer); + malformed.Seek(0, SeekOrigin.Begin); + + TarReader reader = new TarReader(malformed); + await using (reader) + { + await Assert.ThrowsAsync(async () => await reader.GetNextEntryAsync()); + } + } + + [Fact] + public async Task EmptyArchive_Async() + { + using MemoryStream empty = new MemoryStream(); + + TarReader reader = new TarReader(empty); + await using (reader) + { + Assert.Null(await reader.GetNextEntryAsync()); + } + } + + + [Fact] + public async Task LongEndMarkers_DoNotAdvanceStream_Async() + { + using MemoryStream archive = new MemoryStream(); + + TarWriter writer = new TarWriter(archive, TarEntryFormat.Ustar, leaveOpen: true); + await using (writer) + { + UstarTarEntry entry = new UstarTarEntry(TarEntryType.Directory, "dir"); + await writer.WriteEntryAsync(entry); + } + + byte[] buffer = new byte[2048]; // Four additional end markers (512 each) + Array.Fill(buffer, 0x0); + archive.Write(buffer); + archive.Seek(0, SeekOrigin.Begin); + + TarReader reader = new TarReader(archive); + await using (reader) + { + Assert.NotNull(await reader.GetNextEntryAsync()); + Assert.Null(await reader.GetNextEntryAsync()); + long expectedPosition = archive.Position; // After reading the first null entry, should not advance more + Assert.Null(await reader.GetNextEntryAsync()); + Assert.Equal(expectedPosition, archive.Position); + } + } + + [Fact] + public async Task GetNextEntry_CopyDataTrue_SeekableArchive_Async() + { + string expectedText = "Hello world!"; + MemoryStream archive = new MemoryStream(); + TarWriter writer = new TarWriter(archive, TarEntryFormat.Ustar, leaveOpen: true); + await using (writer) + { + UstarTarEntry entry1 = new UstarTarEntry(TarEntryType.RegularFile, "file.txt"); + entry1.DataStream = new MemoryStream(); + using (StreamWriter streamWriter = new StreamWriter(entry1.DataStream, leaveOpen: true)) + { + streamWriter.WriteLine(expectedText); + } + 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); + } + + archive.Seek(0, SeekOrigin.Begin); + + UstarTarEntry entry; + TarReader reader = new TarReader(archive); // Seekable + await using (reader) + { + entry = await reader.GetNextEntryAsync(copyData: true) as UstarTarEntry; + Assert.NotNull(entry); + Assert.Equal(TarEntryType.RegularFile, entry.EntryType); + + // Force reading the next entry to advance the underlying stream position + Assert.NotNull(await reader.GetNextEntryAsync()); + Assert.Null(await reader.GetNextEntryAsync()); + + entry.DataStream.Seek(0, SeekOrigin.Begin); // Should not throw: This is a new stream, not the archive's disposed stream + using (StreamReader streamReader = new StreamReader(entry.DataStream)) + { + string actualText = streamReader.ReadLine(); + Assert.Equal(expectedText, actualText); + } + + } + + // 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])); + } + + [Fact] + public async Task GetNextEntry_CopyDataTrue_UnseekableArchive_Async() + { + string expectedText = "Hello world!"; + MemoryStream archive = new MemoryStream(); + TarWriter writer = new TarWriter(archive, TarEntryFormat.Ustar, leaveOpen: true); + await using (writer) + { + UstarTarEntry entry1 = new UstarTarEntry(TarEntryType.RegularFile, "file.txt"); + entry1.DataStream = new MemoryStream(); + using (StreamWriter streamWriter = new StreamWriter(entry1.DataStream, leaveOpen: true)) + { + streamWriter.WriteLine(expectedText); + } + entry1.DataStream.Seek(0, SeekOrigin.Begin); + await writer.WriteEntryAsync(entry1); + + UstarTarEntry entry2 = new UstarTarEntry(TarEntryType.Directory, "dir"); + await writer.WriteEntryAsync(entry2); + } + + archive.Seek(0, SeekOrigin.Begin); + using WrappedStream wrapped = new WrappedStream(archive, canRead: true, canWrite: false, canSeek: false); + + UstarTarEntry entry; + TarReader reader = new TarReader(wrapped, leaveOpen: true); // Unseekable + await using (reader) + { + entry = await reader.GetNextEntryAsync(copyData: true) as UstarTarEntry; + Assert.NotNull(entry); + Assert.Equal(TarEntryType.RegularFile, entry.EntryType); + + // Force reading the next entry to advance the underlying stream position + Assert.NotNull(await reader.GetNextEntryAsync()); + Assert.Null(await reader.GetNextEntryAsync()); + + Assert.NotNull(entry.DataStream); + entry.DataStream.Seek(0, SeekOrigin.Begin); // Should not throw: This is a new stream, not the archive's disposed stream + using (StreamReader streamReader = new StreamReader(entry.DataStream)) + { + string actualText = streamReader.ReadLine(); + Assert.Equal(expectedText, actualText); + } + + } + + // 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])); + } + + [Fact] + public async Task GetNextEntry_CopyDataFalse_UnseekableArchive_Exceptions_Async() + { + MemoryStream archive = new MemoryStream(); + TarWriter writer = new TarWriter(archive, TarEntryFormat.Ustar, leaveOpen: true); + await using (writer) + { + 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); + } + + archive.Seek(0, SeekOrigin.Begin); + using WrappedStream wrapped = new WrappedStream(archive, canRead: true, canWrite: false, canSeek: false); + UstarTarEntry entry; + TarReader reader = new TarReader(wrapped); // Unseekable + await using (reader) + { + 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()); + + // This is not possible because the position of the main stream is already past the data + Assert.Throws(() => entry.DataStream.Read(new byte[1])); + } + + // 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])); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task GetNextEntry_UnseekableArchive_ReplaceDataStream_ExcludeFromDisposing_Async(bool copyData) + { + MemoryStream archive = new MemoryStream(); + TarWriter writer = new TarWriter(archive, TarEntryFormat.Ustar, leaveOpen: true); + await using (writer) + { + 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); + } + + archive.Seek(0, SeekOrigin.Begin); + using WrappedStream wrapped = new WrappedStream(archive, canRead: true, canWrite: false, canSeek: false); + UstarTarEntry entry; + Stream oldStream; + TarReader reader = new TarReader(wrapped); // Unseekable + await using (reader) + { + entry = await reader.GetNextEntryAsync(copyData) as UstarTarEntry; + Assert.NotNull(entry); + Assert.Equal(TarEntryType.RegularFile, entry.EntryType); + + oldStream = entry.DataStream; + + entry.DataStream = new MemoryStream(); // Substitution, setter should dispose the previous stream + using(StreamWriter streamWriter = new StreamWriter(entry.DataStream, leaveOpen: true)) + { + streamWriter.WriteLine("Substituted"); + } + } // Disposing reader should not dispose the substituted DataStream + + Assert.Throws(() => oldStream.Read(new byte[1])); + + entry.DataStream.Seek(0, SeekOrigin.Begin); + using (StreamReader streamReader = new StreamReader(entry.DataStream)) + { + Assert.Equal("Substituted", streamReader.ReadLine()); + } + } + } +} From 244f84426287641f19eadc89ec42673cf4af4a23 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Fri, 24 Jun 2022 13:30:50 -0700 Subject: [PATCH 29/75] Uncomment InlineData from sync file reading method. --- .../tests/TarReader/TarReader.File.Tests.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 20c4e814decb57..426ea002311d01 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 @@ -13,10 +13,10 @@ public class TarReader_File_Tests : TarReader_File_Tests_Base { [Theory] [InlineData(TarEntryFormat.V7, TestTarFormat.v7)] - //[InlineData(TarEntryFormat.Ustar, TestTarFormat.ustar)] - //[InlineData(TarEntryFormat.Pax, TestTarFormat.pax)] - //[InlineData(TarEntryFormat.Gnu, TestTarFormat.gnu)] - //[InlineData(TarEntryFormat.Gnu, TestTarFormat.oldgnu)] + [InlineData(TarEntryFormat.Ustar, TestTarFormat.ustar)] + [InlineData(TarEntryFormat.Pax, TestTarFormat.pax)] + [InlineData(TarEntryFormat.Gnu, TestTarFormat.gnu)] + [InlineData(TarEntryFormat.Gnu, TestTarFormat.oldgnu)] public void Read_Archive_File(TarEntryFormat format, TestTarFormat testFormat) => Read_Archive_File_Internal(format, testFormat); From b3fa15690f7e130736309861188b7d5adcfbde23 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Fri, 24 Jun 2022 13:59:57 -0700 Subject: [PATCH 30/75] Simplify exception handling. Only return *Task.FromException in public entry-point methods. The rest just let them throw. --- .../src/System/Formats/Tar/TarEntry.cs | 40 ++++--------------- .../src/System/Formats/Tar/TarReader.cs | 2 +- 2 files changed, 8 insertions(+), 34 deletions(-) diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs index d3db1782c25d7b..c7021e32d5377d 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs @@ -359,7 +359,7 @@ internal Task ExtractRelativeToDirectoryAsync(string destinationDirectoryPath, b string? fileDestinationPath = GetSanitizedFullPath(destinationDirectoryPath, Name); if (fileDestinationPath == null) { - return Task.FromException(new IOException(string.Format(SR.TarExtractingResultsFileOutside, Name, destinationDirectoryPath))); + throw new IOException(string.Format(SR.TarExtractingResultsFileOutside, Name, destinationDirectoryPath)); } string? linkTargetPath = null; @@ -367,13 +367,13 @@ internal Task ExtractRelativeToDirectoryAsync(string destinationDirectoryPath, b { if (string.IsNullOrEmpty(LinkName)) { - return Task.FromException(new FormatException(SR.TarEntryHardLinkOrSymlinkLinkNameEmpty)); + throw new FormatException(SR.TarEntryHardLinkOrSymlinkLinkNameEmpty); } linkTargetPath = GetSanitizedFullPath(destinationDirectoryPath, LinkName); if (linkTargetPath == null) { - return Task.FromException(new IOException(string.Format(SR.TarExtractingResultsLinkOutside, LinkName, destinationDirectoryPath))); + throw new IOException(string.Format(SR.TarExtractingResultsLinkOutside, LinkName, destinationDirectoryPath)); } } @@ -417,14 +417,7 @@ private void ExtractToFileInternal(string filePath, string? linkTargetPath, bool // Asynchronously extracts the current entry into the filesystem, regardless of the entry type. private Task ExtractToFileInternalAsync(string filePath, string? linkTargetPath, bool overwrite, CancellationToken cancellationToken) { - try - { - VerifyPathsForEntryType(filePath, linkTargetPath, overwrite); - } - catch (Exception e) - { - return Task.FromException(e); - } + VerifyPathsForEntryType(filePath, linkTargetPath, overwrite); if (EntryType is TarEntryType.RegularFile or TarEntryType.V7RegularFile or TarEntryType.ContiguousFile) { @@ -432,7 +425,8 @@ private Task ExtractToFileInternalAsync(string filePath, string? linkTargetPath, } else { - return CreateNonRegularFileAsync(filePath, linkTargetPath); + CreateNonRegularFile(filePath, linkTargetPath); + return Task.CompletedTask; } } @@ -486,19 +480,6 @@ private void CreateNonRegularFile(string filePath, string? linkTargetPath) } } - private Task CreateNonRegularFileAsync(string filePath, string? linkTargetPath) - { - try - { - CreateNonRegularFile(filePath, linkTargetPath); - return Task.CompletedTask; - } - catch (Exception e) - { - return Task.FromException(e); - } - } - // Verifies if the specified paths make sense for the current type of entry. private void VerifyPathsForEntryType(string filePath, string? linkTargetPath, bool overwrite) { @@ -606,14 +587,7 @@ private async Task ExtractAsRegularFileAsync(string destinationFileName, Cancell // Important: The DataStream will be written from its current position await DataStream.CopyToAsync(fs, cancellationToken).ConfigureAwait(false); } - try - { - SetModeOnFile(fs.SafeFileHandle); - } - catch (Exception e) - { - await Task.FromException(e).ConfigureAwait(false); - } + SetModeOnFile(fs.SafeFileHandle); } ArchivingUtils.AttemptSetLastWriteTime(destinationFileName, ModificationTime); 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 29d62b57d2b725..f7bded7b89615a 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 @@ -464,7 +464,7 @@ TarEntryType.ExtendedAttributes or TarEntryType.LongLink or TarEntryType.LongPath) { - await ValueTask.FromException(new FormatException(string.Format(SR.TarUnexpectedMetadataEntry, actualHeader._typeFlag, TarEntryType.ExtendedAttributes))).ConfigureAwait(false); + throw new FormatException(string.Format(SR.TarUnexpectedMetadataEntry, actualHeader._typeFlag, TarEntryType.ExtendedAttributes)); } // Can't have two extended attribute metadata entries in a row From ffa511593ccaa7b4a2974e8e46b4cc6d3e3dfd2c Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Fri, 24 Jun 2022 18:03:29 -0700 Subject: [PATCH 31/75] Make the TarHeader async methods static if they depend on the struct itself. --- .../src/Resources/Strings.resx | 2 +- .../src/System/Formats/Tar/TarHeader.Read.cs | 288 +++++++++--------- .../src/System/Formats/Tar/TarReader.cs | 20 +- 3 files changed, 150 insertions(+), 160 deletions(-) diff --git a/src/libraries/System.Formats.Tar/src/Resources/Strings.resx b/src/libraries/System.Formats.Tar/src/Resources/Strings.resx index 59404900a65639..5702a915bf11e1 100644 --- a/src/libraries/System.Formats.Tar/src/Resources/Strings.resx +++ b/src/libraries/System.Formats.Tar/src/Resources/Strings.resx @@ -234,7 +234,7 @@ The size field is negative in the tar entry '{0}'. - + The value of the size field for the current entry of type '{0}' is beyond the expected length. 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 3000d0569f0486..7810a34b1440e0 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 @@ -33,30 +33,30 @@ internal bool TryGetNextHeader(Stream archiveStream, bool copyData) try { // Confirms if v7 or pax, or tentatively selects ustar - if (!TryReadCommonAttributes(buffer)) + if (!TryReadCommonAttributesSpan(buffer)) { return false; } // Confirms if gnu, or tentatively selects ustar - ReadMagicAttribute(buffer); + ReadMagicAttributeSpan(buffer); if (_format != TarEntryFormat.V7) { // Confirms if gnu - ReadVersionAttribute(buffer); + ReadVersionAttributeSpan(buffer); // Fields that ustar, pax and gnu share identically - ReadPosixAndGnuSharedAttributes(buffer); + ReadPosixAndGnuSharedAttributesSpan(buffer); Debug.Assert(_format is TarEntryFormat.Ustar or TarEntryFormat.Pax or TarEntryFormat.Gnu); if (_format == TarEntryFormat.Ustar) { - ReadUstarAttributes(buffer); + ReadUstarAttributesSpan(buffer); } else if (_format == TarEntryFormat.Gnu) { - ReadGnuAttributes(buffer); + ReadGnuAttributesSpan(buffer); } // In PAX, there is nothing to read in this section (empty space) } @@ -74,61 +74,122 @@ internal bool TryGetNextHeader(Stream archiveStream, bool copyData) // 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 async ValueTask TryGetNextHeaderAsync(Stream archiveStream, bool copyData, CancellationToken cancellationToken) + internal static async ValueTask<(bool, TarHeader)> TryGetNextHeaderAsync(Stream archiveStream, bool copyData, TarEntryFormat initialFormat, CancellationToken cancellationToken) { - try - { - // The four supported formats have a header that fits in the default record size - byte[] rented = ArrayPool.Shared.Rent(minimumLength: TarHelpers.RecordSize); - Memory buffer = rented.AsMemory(0, TarHelpers.RecordSize); // minimumLength means the array could've been larger - buffer.Span.Clear(); // Rented arrays aren't clean - - await TryGetNextHeaderAsyncInternal(archiveStream, buffer, copyData, cancellationToken).ConfigureAwait(false); + TarHeader header = default; + header._format = initialFormat; - ArrayPool.Shared.Return(rented); - return true; - } - finally - { - } - } + // The four supported formats have a header that fits in the default record size + byte[] rented = ArrayPool.Shared.Rent(minimumLength: TarHelpers.RecordSize); + Memory buffer = rented.AsMemory(0, TarHelpers.RecordSize); // minimumLength means the array could've been larger + buffer.Span.Clear(); // Rented arrays aren't clean - private async ValueTask TryGetNextHeaderAsyncInternal(Stream archiveStream, Memory buffer, bool copyData, CancellationToken cancellationToken) - { await archiveStream.ReadExactlyAsync(buffer, cancellationToken).ConfigureAwait(false); // Confirms if v7 or pax, or tentatively selects ustar - if (!TryReadCommonAttributes(buffer.Span)) + if (!header.TryReadCommonAttributesSpan(buffer.Span)) { - return false; + return (false, default); } // Confirms if gnu, or tentatively selects ustar - ReadMagicAttribute(buffer.Span); + header.ReadMagicAttributeSpan(buffer.Span); - if (_format != TarEntryFormat.V7) + if (header._format != TarEntryFormat.V7) { // Confirms if gnu - ReadVersionAttribute(buffer.Span); + header.ReadVersionAttributeSpan(buffer.Span); // Fields that ustar, pax and gnu share identically - ReadPosixAndGnuSharedAttributes(buffer.Span); + header.ReadPosixAndGnuSharedAttributesSpan(buffer.Span); - Debug.Assert(_format is TarEntryFormat.Ustar or TarEntryFormat.Pax or TarEntryFormat.Gnu); - if (_format == TarEntryFormat.Ustar) + Debug.Assert(header._format is TarEntryFormat.Ustar or TarEntryFormat.Pax or TarEntryFormat.Gnu); + if (header._format == TarEntryFormat.Ustar) { - ReadUstarAttributes(buffer.Span); + header.ReadUstarAttributesSpan(buffer.Span); } - else if (_format == TarEntryFormat.Gnu) + else if (header._format == TarEntryFormat.Gnu) { - ReadGnuAttributes(buffer.Span); + header.ReadGnuAttributesSpan(buffer.Span); } // In PAX, there is nothing to read in this section (empty space) } - await ProcessDataBlockAsync(archiveStream, copyData, cancellationToken).ConfigureAwait(false); + bool skipBlockAlignmentPadding = true; - return true; + string? longPath; + switch (header._typeFlag) + { + case TarEntryType.ExtendedAttributes or TarEntryType.GlobalExtendedAttributes: + Debug.Assert(header._name != null); + header._extendedAttributes = await ReadExtendedAttributesBlockAsync(archiveStream, header._typeFlag, header._size, header._name, cancellationToken).ConfigureAwait(false); + break; + + case TarEntryType.LongLink: + longPath = await ReadGnuLongPathDataBlockAsync(archiveStream, header._typeFlag, header._size, cancellationToken).ConfigureAwait(false); + if (longPath != null) + { + header._linkName = longPath; + } + break; + + case TarEntryType.LongPath: + longPath = await ReadGnuLongPathDataBlockAsync(archiveStream, header._typeFlag, header._size, cancellationToken).ConfigureAwait(false); + if (longPath != null) + { + header._name = longPath; + } + break; + + case TarEntryType.BlockDevice: + case TarEntryType.CharacterDevice: + case TarEntryType.Directory: + case TarEntryType.Fifo: + case TarEntryType.HardLink: + case TarEntryType.SymbolicLink: + // No data section + break; + + case TarEntryType.RegularFile: + case TarEntryType.V7RegularFile: // Treated as regular file + case TarEntryType.ContiguousFile: // Treated as regular file + case TarEntryType.DirectoryList: // Contains the list of filesystem entries in the data section + case TarEntryType.MultiVolume: // Contains portion of a file + case TarEntryType.RenamedOrSymlinked: // Might contain data + case TarEntryType.SparseFile: // Contains portion of a file + case TarEntryType.TapeVolume: // Might contain data + default: // Unrecognized entry types could potentially have a data section + header._dataStream = await GetDataStreamAsync(archiveStream, copyData, header._size, cancellationToken).ConfigureAwait(false); + if (header._dataStream is SeekableSubReadStream) + { + await TarHelpers.AdvanceStreamAsync(archiveStream, header._size, cancellationToken).ConfigureAwait(false); + } + else if (header._dataStream is SubReadStream) + { + // This stream gives the user the chance to optionally read the data section + // when the underlying archive stream is unseekable + skipBlockAlignmentPadding = false; + } + + break; + } + + if (skipBlockAlignmentPadding) + { + if (header._size > 0) + { + await TarHelpers.SkipBlockAlignmentPaddingAsync(archiveStream, header._size, cancellationToken).ConfigureAwait(false); + } + + if (archiveStream.CanSeek) + { + header._endOfHeaderAndDataAndBlockAlignment = archiveStream.Position; + } + } + + ArrayPool.Shared.Return(rented); + + return (true, header); } // Reads the elements from the passed dictionary, which comes from the previous extended attributes entry, @@ -280,69 +341,6 @@ private void ProcessDataBlock(Stream archiveStream, bool copyData) } } - // Asynchronously determines what kind of stream needs to be saved for the data section. - // - Metadata typeflag entries (Extended Attributes and Global Extended Attributes in PAX, LongLink and LongPath in GNU) - // 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 async ValueTask ProcessDataBlockAsync(Stream archiveStream, bool copyData, CancellationToken cancellationToken) - { - bool skipBlockAlignmentPadding = true; - - switch (_typeFlag) - { - case TarEntryType.ExtendedAttributes or TarEntryType.GlobalExtendedAttributes: - await ReadExtendedAttributesBlockAsync(archiveStream, cancellationToken).ConfigureAwait(false); - break; - case TarEntryType.LongLink or TarEntryType.LongPath: - await ReadGnuLongPathDataBlockAsync(archiveStream, cancellationToken).ConfigureAwait(false); - break; - case TarEntryType.BlockDevice: - case TarEntryType.CharacterDevice: - case TarEntryType.Directory: - case TarEntryType.Fifo: - case TarEntryType.HardLink: - case TarEntryType.SymbolicLink: - // No data section - break; - case TarEntryType.RegularFile: - case TarEntryType.V7RegularFile: // Treated as regular file - case TarEntryType.ContiguousFile: // Treated as regular file - case TarEntryType.DirectoryList: // Contains the list of filesystem entries in the data section - case TarEntryType.MultiVolume: // Contains portion of a file - case TarEntryType.RenamedOrSymlinked: // Might contain data - case TarEntryType.SparseFile: // Contains portion of a file - case TarEntryType.TapeVolume: // Might contain data - default: // Unrecognized entry types could potentially have a data section - _dataStream = await GetDataStreamAsync(archiveStream, copyData, cancellationToken).ConfigureAwait(false); - if (_dataStream is SeekableSubReadStream) - { - await TarHelpers.AdvanceStreamAsync(archiveStream, _size, cancellationToken).ConfigureAwait(false); - } - else if (_dataStream is SubReadStream) - { - // This stream gives the user the chance to optionally read the data section - // when the underlying archive stream is unseekable - skipBlockAlignmentPadding = false; - } - - break; - } - - if (skipBlockAlignmentPadding) - { - if (_size > 0) - { - await TarHelpers.SkipBlockAlignmentPaddingAsync(archiveStream, _size, cancellationToken).ConfigureAwait(false); - } - - if (archiveStream.CanSeek) - { - _endOfHeaderAndDataAndBlockAlignment = archiveStream.Position; - } - } - } - // Returns a stream that represents the data section of the current header. // If copyData is true, then a total number of _size bytes will be copied to a new MemoryStream, which is then returned. // Otherwise, if the archive stream is seekable, returns a seekable wrapper stream. @@ -370,29 +368,29 @@ private async ValueTask ProcessDataBlockAsync(Stream archiveStream, bool copyDat // If copyData is true, then a total number of _size bytes will be copied to a new MemoryStream, which is then returned. // Otherwise, if the archive stream is seekable, returns a seekable wrapper stream. // Otherwise, it returns an unseekable wrapper stream. - private async ValueTask GetDataStreamAsync(Stream archiveStream, bool copyData, CancellationToken cancellationToken) + private static async ValueTask GetDataStreamAsync(Stream archiveStream, bool copyData, long size, CancellationToken cancellationToken) { - if (_size == 0) + if (size == 0) { - return null; + return new MemoryStream(); } if (copyData) { MemoryStream copiedData = new MemoryStream(); - await TarHelpers.CopyBytesAsync(archiveStream, copiedData, _size, cancellationToken).ConfigureAwait(false); + await TarHelpers.CopyBytesAsync(archiveStream, copiedData, size, cancellationToken).ConfigureAwait(false); return copiedData; } return archiveStream.CanSeek - ? new SeekableSubReadStream(archiveStream, archiveStream.Position, _size) - : new SubReadStream(archiveStream, 0, _size); + ? new SeekableSubReadStream(archiveStream, archiveStream.Position, size) + : new SubReadStream(archiveStream, 0, size); } // Attempts to read the fields shared by all formats and stores them in their expected data type. // Throws if any data type conversion fails. // Returns true on success, false if checksum is zero. - private bool TryReadCommonAttributes(Span buffer) + private bool TryReadCommonAttributesSpan(Span buffer) { // Start by collecting fields that need special checks that return early when data is wrong @@ -455,7 +453,7 @@ TarEntryType.SparseFile or // Reads fields only found in ustar format or above and converts them to their expected data type. // Throws if any conversion fails. - private void ReadMagicAttribute(Span buffer) + private void ReadMagicAttributeSpan(Span buffer) { Span magic = buffer.Slice(FieldLocations.Magic, FieldLengths.Magic); @@ -482,7 +480,7 @@ private void ReadMagicAttribute(Span buffer) // Reads the version string and determines the format depending on its value. // Throws if converting the bytes to string fails or if an unexpected version string is found. - private void ReadVersionAttribute(Span buffer) + private void ReadVersionAttributeSpan(Span buffer) { if (_format == TarEntryFormat.V7) { @@ -508,7 +506,7 @@ private void ReadVersionAttribute(Span buffer) // Reads the attributes shared by the POSIX and GNU formats. // Throws if converting the bytes to their expected data type fails. - private void ReadPosixAndGnuSharedAttributes(Span buffer) + private void ReadPosixAndGnuSharedAttributesSpan(Span buffer) { // Convert the byte arrays _uName = TarHelpers.GetTrimmedAsciiString(buffer.Slice(FieldLocations.UName, FieldLengths.UName)); @@ -528,7 +526,7 @@ private void ReadPosixAndGnuSharedAttributes(Span buffer) // Reads attributes specific to the GNU format. // Throws if any conversion fails. - private void ReadGnuAttributes(Span buffer) + private void ReadGnuAttributesSpan(Span buffer) { // Convert byte arrays long aTime = TarHelpers.GetTenBaseLongFromOctalAsciiChars(buffer.Slice(FieldLocations.ATime, FieldLengths.ATime)); @@ -542,7 +540,7 @@ private void ReadGnuAttributes(Span buffer) // Reads the ustar prefix attribute. // Throws if a conversion to an expected data type fails. - private void ReadUstarAttributes(Span buffer) + private void ReadUstarAttributesSpan(Span buffer) { _prefix = TarHelpers.GetTrimmedUtf8String(buffer.Slice(FieldLocations.Prefix, FieldLengths.Prefix)); @@ -574,7 +572,7 @@ private void ReadExtendedAttributesBlock(Stream archiveStream) // 4096 is a common max path length, and also the size field is 12 bytes long, which is under int.MaxValue. if (_size > int.MaxValue) { - throw new InvalidOperationException(string.Format(SR.TarSizeFieldTooLargeForExtendedAttribute, _typeFlag.ToString())); + throw new InvalidOperationException(string.Format(SR.TarSizeFieldTooLargeForEntryType, _typeFlag.ToString())); } byte[] buffer = new byte[(int)_size]; @@ -597,26 +595,26 @@ private void ReadExtendedAttributesBlock(Stream archiveStream) // Asynchronously collects the extended attributes found in the data section of a PAX entry of type 'x' or 'g'. // Throws if end of stream is reached or if an attribute is malformed. - private async ValueTask ReadExtendedAttributesBlockAsync(Stream archiveStream, CancellationToken cancellationToken) + private static async ValueTask> ReadExtendedAttributesBlockAsync(Stream archiveStream, TarEntryType entryType, long size, string name, CancellationToken cancellationToken) { - Debug.Assert(_typeFlag is TarEntryType.ExtendedAttributes or TarEntryType.GlobalExtendedAttributes); + Debug.Assert(entryType is TarEntryType.ExtendedAttributes or TarEntryType.GlobalExtendedAttributes); - // Regardless of the size, this entry should always have a valid dictionary object - _extendedAttributes ??= new Dictionary(); - - if (_size == 0) + // It is not expected that the extended attributes data section will be longer than int.MaxValue, considering + // 4096 is a common max path length, and also the size field is 12 bytes long, which is under int.MaxValue. + if (size > int.MaxValue) { - return; + throw new InvalidOperationException(string.Format(SR.TarSizeFieldTooLargeForEntryType, entryType.ToString())); } - // It is not expected that the extended attributes data section will be longer than int.MaxValue, considering - // 4096 is a common max path length, and also the size field is 12 bytes long, which is under int.MaxValue. - if (_size > int.MaxValue) + // Regardless of the size, this entry should always have a valid dictionary object + Dictionary extendedAttributes = new Dictionary(); + + if (size == 0) { - throw new InvalidOperationException(string.Format(SR.TarSizeFieldTooLargeForExtendedAttribute, _typeFlag.ToString())); + return extendedAttributes; } - byte[] buffer = new byte[(int)_size]; + byte[] buffer = new byte[(int)size]; await archiveStream.ReadExactlyAsync(buffer, cancellationToken).ConfigureAwait(false); string dataAsString = TarHelpers.GetTrimmedUtf8String(buffer); @@ -625,13 +623,14 @@ private async ValueTask ReadExtendedAttributesBlockAsync(Stream archiveStream, C while (TryGetNextExtendedAttribute(reader, out string? key, out string? value)) { - _extendedAttributes ??= new Dictionary(); - if (_extendedAttributes.ContainsKey(key)) + if (extendedAttributes.ContainsKey(key)) { - throw new FormatException(string.Format(SR.TarDuplicateExtendedAttribute, _name)); + throw new FormatException(string.Format(SR.TarDuplicateExtendedAttribute, name)); } - _extendedAttributes.Add(key, value); + extendedAttributes.Add(key, value); } + + return extendedAttributes; } // Reads the long path found in the data section of a GNU entry of type 'K' or 'L' @@ -641,6 +640,11 @@ private void ReadGnuLongPathDataBlock(Stream archiveStream) { Debug.Assert(_typeFlag is TarEntryType.LongLink or TarEntryType.LongPath); + if (_size > int.MaxValue) + { + throw new InvalidOperationException(string.Format(SR.TarSizeFieldTooLargeForEntryType, _typeFlag.ToString())); + } + if (_size == 0) { return; @@ -664,28 +668,24 @@ private void ReadGnuLongPathDataBlock(Stream archiveStream) // Asynchronously reads the long path found in the data section of a GNU entry of type 'K' or 'L' // and replaces Name or LinkName, respectively, with the found string. // Throws if end of stream is reached. - private async ValueTask ReadGnuLongPathDataBlockAsync(Stream archiveStream, CancellationToken cancellationToken) + private static async ValueTask ReadGnuLongPathDataBlockAsync(Stream archiveStream, TarEntryType entryType, long size, CancellationToken cancellationToken) { - Debug.Assert(_typeFlag is TarEntryType.LongLink or TarEntryType.LongPath); + Debug.Assert(entryType is TarEntryType.LongLink or TarEntryType.LongPath); - if (_size == 0) + if (size == 0) { - return; + return null; } - byte[] buffer = new byte[(int)_size]; - await archiveStream.ReadExactlyAsync(buffer, cancellationToken).ConfigureAwait(false); - - string longPath = TarHelpers.GetTrimmedUtf8String(buffer); - - if (_typeFlag == TarEntryType.LongLink) - { - _linkName = longPath; - } - else if (_typeFlag == TarEntryType.LongPath) + if (size > int.MaxValue) { - _name = longPath; + throw new InvalidOperationException(string.Format(SR.TarSizeFieldTooLargeForEntryType, entryType.ToString())); } + + byte[] buffer = new byte[(int)size]; + await archiveStream.ReadExactlyAsync(buffer, cancellationToken).ConfigureAwait(false); + + return TarHelpers.GetTrimmedUtf8String(buffer); } // Tries to collect the next extended attribute from the string wrapped by the specified reader. 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 f7bded7b89615a..722ccf2865df2c 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 @@ -370,9 +370,8 @@ private bool TryGetNextEntryHeader(out TarHeader header, bool copyData) { Debug.Assert(!_reachedEndMarkers); - TarHeader header = default; - - if (!header.TryGetNextHeader(_archiveStream, copyData)) + (bool result, TarHeader header) = await TarHeader.TryGetNextHeaderAsync(_archiveStream, copyData, TarEntryFormat.Unknown, cancellationToken).ConfigureAwait(false); + if (!result) { return (false, default); } @@ -448,11 +447,8 @@ TarEntryType.LongLink or // and returns the actual entry with the processed extended attributes saved in the _extendedAttributes dictionary. private async ValueTask<(bool, TarHeader)> TryProcessExtendedAttributesHeaderAsync(TarHeader extendedAttributesHeader, bool copyData, CancellationToken cancellationToken) { - TarHeader actualHeader = default; - actualHeader._format = TarEntryFormat.Pax; - // Now get the actual entry - bool result = await actualHeader.TryGetNextHeaderAsync(_archiveStream, copyData, cancellationToken).ConfigureAwait(false); + (bool result, TarHeader actualHeader) = await TarHeader.TryGetNextHeaderAsync(_archiveStream, copyData, TarEntryFormat.Pax, cancellationToken).ConfigureAwait(false); if (!result) { return (false, default); @@ -563,11 +559,8 @@ private bool TryProcessGnuMetadataHeader(TarHeader header, bool copyData, out Ta // or the actual entry. Processes them all and returns the actual entry updating its path and/or linkpath fields as needed. private async ValueTask<(bool, TarHeader)> TryProcessGnuMetadataHeaderAsync(TarHeader header, bool copyData, CancellationToken cancellationToken) { - TarHeader secondHeader = default; - secondHeader._format = TarEntryFormat.Gnu; - // Get the second entry, which is the actual entry - bool result1 = await secondHeader.TryGetNextHeaderAsync(_archiveStream, copyData, cancellationToken).ConfigureAwait(false); + (bool result1, TarHeader secondHeader) = await TarHeader.TryGetNextHeaderAsync(_archiveStream, copyData, TarEntryFormat.Gnu, cancellationToken).ConfigureAwait(false); if (!result1) { return (false, default); @@ -585,11 +578,8 @@ 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 = default; - thirdHeader._format = TarEntryFormat.Gnu; - // Get the third entry, which is the actual entry - bool result2 = await thirdHeader.TryGetNextHeaderAsync(_archiveStream, copyData, cancellationToken).ConfigureAwait(false); + (bool result2, TarHeader thirdHeader) = await TarHeader.TryGetNextHeaderAsync(_archiveStream, copyData, TarEntryFormat.Gnu, cancellationToken).ConfigureAwait(false); if (!result2) { return (false, default); From d45b9ad1f268d8c99a7dfce6a29b941158a8cb38 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Fri, 24 Jun 2022 18:18:28 -0700 Subject: [PATCH 32/75] Add missing GEA changes from the last PR. --- .../System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs | 2 +- .../System.Formats.Tar/src/System/Formats/Tar/TarFile.cs | 7 +++++-- .../System.Formats.Tar/src/System/Formats/Tar/TarReader.cs | 3 ++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs index c7021e32d5377d..529c3d3f8044c3 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs @@ -250,7 +250,7 @@ public void ExtractToFile(string destinationFileName, bool overwrite) public Task ExtractToFileAsync(string destinationFileName, bool overwrite, CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrEmpty(destinationFileName); - if (EntryType is TarEntryType.SymbolicLink or TarEntryType.HardLink) + if (EntryType is TarEntryType.SymbolicLink or TarEntryType.HardLink or TarEntryType.GlobalExtendedAttributes) { return Task.FromException(new InvalidOperationException(string.Format(SR.TarEntryTypeNotSupportedForExtracting, EntryType))); } diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarFile.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarFile.cs index bc98c2e1abd49a..2f129b1da58eb3 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarFile.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarFile.cs @@ -378,7 +378,7 @@ private static void ExtractToDirectoryInternal(Stream source, string destination TarEntry? entry; while ((entry = reader.GetNextEntry()) != null) { - if (entry is not PaxGlobalExtendedAttributesTarEntry) + if (entry.EntryType is not TarEntryType.GlobalExtendedAttributes) { entry.ExtractRelativeToDirectory(destinationDirectoryPath, overwriteFiles); } @@ -400,7 +400,10 @@ private static async Task ExtractToDirectoryInternalAsync(Stream source, string TarEntry? entry; while ((entry = await reader.GetNextEntryAsync(cancellationToken: cancellationToken).ConfigureAwait(false)) != null) { - await entry.ExtractRelativeToDirectoryAsync(destinationDirectoryPath, overwriteFiles, cancellationToken).ConfigureAwait(false); + if (entry.EntryType is not TarEntryType.GlobalExtendedAttributes) + { + await entry.ExtractRelativeToDirectoryAsync(destinationDirectoryPath, overwriteFiles, cancellationToken).ConfigureAwait(false); + } } } } 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 722ccf2865df2c..305f27b69d3b64 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 @@ -169,7 +169,8 @@ public async ValueTask DisposeAsync() TarEntry entry = header._format switch { - TarEntryFormat.Pax => new PaxTarEntry(header, this), + TarEntryFormat.Pax => header._typeFlag is TarEntryType.GlobalExtendedAttributes ? + new PaxGlobalExtendedAttributesTarEntry(header, this) : new PaxTarEntry(header, this), TarEntryFormat.Gnu => new GnuTarEntry(header, this), TarEntryFormat.Ustar => new UstarTarEntry(header, this), TarEntryFormat.V7 or TarEntryFormat.Unknown or _ => new V7TarEntry(header, this), From 3c5b92fdfe79a9a1939a97341de47ec1b7a3af20 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Fri, 24 Jun 2022 18:20:38 -0700 Subject: [PATCH 33/75] TarReader GetNextEntryAsync tests for file assets. --- .../tests/System.Formats.Tar.Tests.csproj | 3 + .../TarReader.File.Async.Tests.Base.cs | 660 ++++++++++++++++++ .../TarReader/TarReader.File.Async.Tests.cs | 129 ++++ ...le.GlobalExtendedAttributes.Async.Tests.cs | 89 +++ 4 files changed, 881 insertions(+) create mode 100644 src/libraries/System.Formats.Tar/tests/TarReader/TarReader.File.Async.Tests.Base.cs create mode 100644 src/libraries/System.Formats.Tar/tests/TarReader/TarReader.File.Async.Tests.cs create mode 100644 src/libraries/System.Formats.Tar/tests/TarReader/TarReader.File.GlobalExtendedAttributes.Async.Tests.cs diff --git a/src/libraries/System.Formats.Tar/tests/System.Formats.Tar.Tests.csproj b/src/libraries/System.Formats.Tar/tests/System.Formats.Tar.Tests.csproj index 428b24301edb80..c7ee267016e9b2 100644 --- a/src/libraries/System.Formats.Tar/tests/System.Formats.Tar.Tests.csproj +++ b/src/libraries/System.Formats.Tar/tests/System.Formats.Tar.Tests.csproj @@ -22,6 +22,9 @@ + + + diff --git a/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.File.Async.Tests.Base.cs b/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.File.Async.Tests.Base.cs new file mode 100644 index 00000000000000..700445159f9bd4 --- /dev/null +++ b/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.File.Async.Tests.Base.cs @@ -0,0 +1,660 @@ +// 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.Globalization; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Xunit; +using static System.Net.WebRequestMethods; + +namespace System.Formats.Tar.Tests +{ + public class TarReader_File_Tests_Async_Base : TarTestsBase + { + protected async Task Read_Archive_File_Async_Internal(TarEntryFormat format, TestTarFormat testFormat) + { + string testCaseName = "file"; + using MemoryStream ms = GetTarMemoryStream(CompressionMethod.Uncompressed, testFormat, testCaseName); + + TarReader reader = new TarReader(ms); + await using (reader) + { + if (testFormat is TestTarFormat.pax_gea) + { + TarEntry entry = await reader.GetNextEntryAsync(); + VerifyGlobalExtendedAttributes(entry); + } + + TarEntry file = await reader.GetNextEntryAsync(); + + VerifyRegularFileEntry(file, format, "file.txt", $"Hello {testCaseName}"); + + Assert.Null(await reader.GetNextEntryAsync()); + } + } + + protected async Task Read_Archive_File_HardLink_Async_Internal(TarEntryFormat format, TestTarFormat testFormat) + { + string testCaseName = "file_hardlink"; + using MemoryStream ms = GetTarMemoryStream(CompressionMethod.Uncompressed, testFormat, testCaseName); + + TarReader reader = new TarReader(ms); + await using (reader) + { + if (testFormat is TestTarFormat.pax_gea) + { + TarEntry entry = await reader.GetNextEntryAsync(); + VerifyGlobalExtendedAttributes(entry); + } + + TarEntry file = await reader.GetNextEntryAsync(); + + VerifyRegularFileEntry(file, format, "file.txt", $"Hello {testCaseName}"); + + TarEntry hardLink = await reader.GetNextEntryAsync(); + // The 'tar' tool detects hardlinks as regular files and saves them as such in the archives, for all formats + VerifyRegularFileEntry(hardLink, format, "hardlink.txt", $"Hello {testCaseName}"); + + Assert.Null(await reader.GetNextEntryAsync()); + } + } + + protected async Task Read_Archive_File_SymbolicLink_Async_Internal(TarEntryFormat format, TestTarFormat testFormat) + { + string testCaseName = "file_symlink"; + using MemoryStream ms = GetTarMemoryStream(CompressionMethod.Uncompressed, testFormat, testCaseName); + + TarReader reader = new TarReader(ms); + await using (reader) + { + if (testFormat is TestTarFormat.pax_gea) + { + TarEntry entry = await reader.GetNextEntryAsync(); + VerifyGlobalExtendedAttributes(entry); + } + + TarEntry file = await reader.GetNextEntryAsync(); + + VerifyRegularFileEntry(file, format, "file.txt", $"Hello {testCaseName}"); + + TarEntry symbolicLink = await reader.GetNextEntryAsync(); + VerifySymbolicLinkEntry(symbolicLink, format, "link.txt", "file.txt"); + + Assert.Null(await reader.GetNextEntryAsync()); + } + } + + protected async Task Read_Archive_Folder_File_Async_Internal(TarEntryFormat format, TestTarFormat testFormat) + { + string testCaseName = "folder_file"; + using MemoryStream ms = GetTarMemoryStream(CompressionMethod.Uncompressed, testFormat, testCaseName); + + TarReader reader = new TarReader(ms); + await using (reader) + { + if (testFormat is TestTarFormat.pax_gea) + { + TarEntry entry = await reader.GetNextEntryAsync(); + VerifyGlobalExtendedAttributes(entry); + } + + TarEntry directory = await reader.GetNextEntryAsync(); + + VerifyDirectoryEntry(directory, format, "folder/"); + + TarEntry file = await reader.GetNextEntryAsync(); + VerifyRegularFileEntry(file, format, "folder/file.txt", $"Hello {testCaseName}"); + + Assert.Null(await reader.GetNextEntryAsync()); + } + } + + protected async Task Read_Archive_Folder_File_Utf8_Async_Internal(TarEntryFormat format, TestTarFormat testFormat) + { + string testCaseName = "folder_file_utf8"; + using MemoryStream ms = GetTarMemoryStream(CompressionMethod.Uncompressed, testFormat, testCaseName); + + TarReader reader = new TarReader(ms); + await using (reader) + { + if (testFormat is TestTarFormat.pax_gea) + { + TarEntry entry = await reader.GetNextEntryAsync(); + VerifyGlobalExtendedAttributes(entry); + } + + TarEntry directory = await reader.GetNextEntryAsync(); + + VerifyDirectoryEntry(directory, format, "f\u00f6ld\u00ebr/"); //földër + + TarEntry file = await reader.GetNextEntryAsync(); + VerifyRegularFileEntry(file, format, "f\u00f6ld\u00ebr/\u00e1\u00f6\u00f1.txt", $"Hello {testCaseName}"); // földër/áöñ.txt + + Assert.Null(await reader.GetNextEntryAsync()); + } + } + + protected async Task Read_Archive_Folder_Subfolder_File_Async_Internal(TarEntryFormat format, TestTarFormat testFormat) + { + string testCaseName = "folder_subfolder_file"; + using MemoryStream ms = GetTarMemoryStream(CompressionMethod.Uncompressed, testFormat, testCaseName); + + TarReader reader = new TarReader(ms); + await using (reader) + { + if (testFormat is TestTarFormat.pax_gea) + { + TarEntry entry = await reader.GetNextEntryAsync(); + VerifyGlobalExtendedAttributes(entry); + } + + TarEntry parent = await reader.GetNextEntryAsync(); + + VerifyDirectoryEntry(parent, format, "parent/"); + + TarEntry child = await reader.GetNextEntryAsync(); + VerifyDirectoryEntry(child, format, "parent/child/"); + + TarEntry file = await reader.GetNextEntryAsync(); + VerifyRegularFileEntry(file, format, "parent/child/file.txt", $"Hello {testCaseName}"); + + Assert.Null(await reader.GetNextEntryAsync()); + } + } + + protected async Task Read_Archive_FolderSymbolicLink_Folder_Subfolder_File_Async_Internal(TarEntryFormat format, TestTarFormat testFormat) + { + string testCaseName = "foldersymlink_folder_subfolder_file"; + using MemoryStream ms = GetTarMemoryStream(CompressionMethod.Uncompressed, testFormat, testCaseName); + + TarReader reader = new TarReader(ms); + await using (reader) + { + if (testFormat is TestTarFormat.pax_gea) + { + TarEntry entry = await reader.GetNextEntryAsync(); + VerifyGlobalExtendedAttributes(entry); + } + + TarEntry childlink = await reader.GetNextEntryAsync(); + + VerifySymbolicLinkEntry(childlink, format, "childlink", "parent/child"); + + TarEntry parent = await reader.GetNextEntryAsync(); + VerifyDirectoryEntry(parent, format, "parent/"); + + TarEntry child = await reader.GetNextEntryAsync(); + VerifyDirectoryEntry(child, format, "parent/child/"); + + TarEntry file = await reader.GetNextEntryAsync(); + VerifyRegularFileEntry(file, format, "parent/child/file.txt", $"Hello {testCaseName}"); + + Assert.Null(await reader.GetNextEntryAsync()); + } + } + + protected async Task Read_Archive_Many_Small_Files_Async_Internal(TarEntryFormat format, TestTarFormat testFormat) + { + string testCaseName = "many_small_files"; + using MemoryStream ms = GetTarMemoryStream(CompressionMethod.Uncompressed, testFormat, testCaseName); + + TarReader reader = new TarReader(ms); + await using (reader) + { + TarEntry entry; + if (testFormat is TestTarFormat.pax_gea) + { + entry = await reader.GetNextEntryAsync(); + VerifyGlobalExtendedAttributes(entry); + } + + List entries = new List(); + bool isFirstEntry = true; + while ((entry = await reader.GetNextEntryAsync()) != null) + { + if (isFirstEntry) + { + isFirstEntry = false; + } + Assert.Equal(format, entry.Format); + entries.Add(entry); + } + + int directoriesCount = entries.Count(e => e.EntryType == TarEntryType.Directory); + Assert.Equal(10, directoriesCount); + + TarEntryType actualEntryType = format is TarEntryFormat.V7 ? TarEntryType.V7RegularFile : TarEntryType.RegularFile; + + for (int i = 0; i < 10; i++) + { + int filesCount = entries.Count(e => e.EntryType == actualEntryType && e.Name.StartsWith($"{i}/")); + Assert.Equal(10, filesCount); + } + } + } + + protected async Task Read_Archive_LongPath_Splitable_Under255_Async_Internal(TarEntryFormat format, TestTarFormat testFormat) + { + string testCaseName = "longpath_splitable_under255"; + using MemoryStream ms = GetTarMemoryStream(CompressionMethod.Uncompressed, testFormat, testCaseName); + + TarReader reader = new TarReader(ms); + await using (reader) + { + if (testFormat is TestTarFormat.pax_gea) + { + TarEntry entry = await reader.GetNextEntryAsync(); + VerifyGlobalExtendedAttributes(entry); + } + + TarEntry directory = await reader.GetNextEntryAsync(); + + VerifyDirectoryEntry(directory, format, + "00000000001111111111222222222233333333334444444444555555555566666666667777777777888888888899999999/"); + + TarEntry file = await reader.GetNextEntryAsync(); + VerifyRegularFileEntry(file, format, + $"00000000001111111111222222222233333333334444444444555555555566666666667777777777888888888899999999/00000000001111111111222222222233333333334444444444555555555566666666667777777777888888888899999.txt", + $"Hello {testCaseName}"); + + Assert.Null(await reader.GetNextEntryAsync()); + } + } + + protected async Task Read_Archive_SpecialFiles_Async_Internal(TarEntryFormat format, TestTarFormat testFormat) + { + string testCaseName = "specialfiles"; + using MemoryStream ms = GetTarMemoryStream(CompressionMethod.Uncompressed, testFormat, testCaseName); + + TarReader reader = new TarReader(ms); + await using (reader) + { + if (testFormat is TestTarFormat.pax_gea) + { + TarEntry entry = await reader.GetNextEntryAsync(); + VerifyGlobalExtendedAttributes(entry); + } + + PosixTarEntry blockDevice = await reader.GetNextEntryAsync() as PosixTarEntry; + + VerifyBlockDeviceEntry(blockDevice, format, AssetBlockDeviceFileName); + + PosixTarEntry characterDevice = await reader.GetNextEntryAsync() as PosixTarEntry; + VerifyCharacterDeviceEntry(characterDevice, format, AssetCharacterDeviceFileName); + + PosixTarEntry fifo = await reader.GetNextEntryAsync() as PosixTarEntry; + VerifyFifoEntry(fifo, format, "fifofile"); + + Assert.Null(await reader.GetNextEntryAsync()); + } + } + + protected async Task Read_Archive_File_LongSymbolicLink_Async_Internal(TarEntryFormat format, TestTarFormat testFormat) + { + string testCaseName = "file_longsymlink"; + using MemoryStream ms = GetTarMemoryStream(CompressionMethod.Uncompressed, testFormat, testCaseName); + + TarReader reader = new TarReader(ms); + await using (reader) + { + if (testFormat is TestTarFormat.pax_gea) + { + TarEntry entry = await reader.GetNextEntryAsync(); + VerifyGlobalExtendedAttributes(entry); + } + + TarEntry directory = await reader.GetNextEntryAsync(); + + VerifyDirectoryEntry(directory, format, + "000000000011111111112222222222333333333344444444445555555555666666666677777777778888888888999999999900000000001111111111222222222233333333334444444444555555555566666666667777777777888888888899999999990000000000111111111122222222223333333333444444444455555/"); + + TarEntry file = await reader.GetNextEntryAsync(); + VerifyRegularFileEntry(file, format, + "000000000011111111112222222222333333333344444444445555555555666666666677777777778888888888999999999900000000001111111111222222222233333333334444444444555555555566666666667777777777888888888899999999990000000000111111111122222222223333333333444444444455555/00000000001111111111222222222233333333334444444444555555555566666666667777777777888888888899999999990000000000111111111122222222223333333333444444444455555555556666666666777777777788888888889999999999000000000011111111112222222222333333333344444444445.txt", + $"Hello {testCaseName}"); + + TarEntry symbolicLink = await reader.GetNextEntryAsync(); + VerifySymbolicLinkEntry(symbolicLink, format, + "link.txt", + "000000000011111111112222222222333333333344444444445555555555666666666677777777778888888888999999999900000000001111111111222222222233333333334444444444555555555566666666667777777777888888888899999999990000000000111111111122222222223333333333444444444455555/00000000001111111111222222222233333333334444444444555555555566666666667777777777888888888899999999990000000000111111111122222222223333333333444444444455555555556666666666777777777788888888889999999999000000000011111111112222222222333333333344444444445.txt"); + + Assert.Null(await reader.GetNextEntryAsync()); + } + } + + protected async Task Read_Archive_LongFileName_Over100_Under255_Async_Internal(TarEntryFormat format, TestTarFormat testFormat) + { + string testCaseName = "longfilename_over100_under255"; + using MemoryStream ms = GetTarMemoryStream(CompressionMethod.Uncompressed, testFormat, testCaseName); + + TarReader reader = new TarReader(ms); + await using (reader) + { + if (testFormat is TestTarFormat.pax_gea) + { + TarEntry entry = await reader.GetNextEntryAsync(); + VerifyGlobalExtendedAttributes(entry); + } + + TarEntry file = await reader.GetNextEntryAsync(); + + VerifyRegularFileEntry(file, format, + "000000000011111111112222222222333333333344444444445555555555666666666677777777778888888888999999999900000000001111111111222222222233333333334444444444.txt", + $"Hello {testCaseName}"); + + Assert.Null(await reader.GetNextEntryAsync()); + } + } + + protected async Task Read_Archive_LongPath_Over255_Async_Internal(TarEntryFormat format, TestTarFormat testFormat) + { + string testCaseName = "longpath_over255"; + using MemoryStream ms = GetTarMemoryStream(CompressionMethod.Uncompressed, testFormat, testCaseName); + + TarReader reader = new TarReader(ms); + await using (reader) + { + if (testFormat is TestTarFormat.pax_gea) + { + TarEntry entry = await reader.GetNextEntryAsync(); + VerifyGlobalExtendedAttributes(entry); + } + + TarEntry directory = await reader.GetNextEntryAsync(); + + VerifyDirectoryEntry(directory, format, + "000000000011111111112222222222333333333344444444445555555555666666666677777777778888888888999999999900000000001111111111222222222233333333334444444444555555555566666666667777777777888888888899999999990000000000111111111122222222223333333333444444444455555/"); + + TarEntry file = await reader.GetNextEntryAsync(); + VerifyRegularFileEntry(file, format, + "000000000011111111112222222222333333333344444444445555555555666666666677777777778888888888999999999900000000001111111111222222222233333333334444444444555555555566666666667777777777888888888899999999990000000000111111111122222222223333333333444444444455555/00000000001111111111222222222233333333334444444444555555555566666666667777777777888888888899999999990000000000111111111122222222223333333333444444444455555555556666666666777777777788888888889999999999000000000011111111112222222222333333333344444444445.txt", + $"Hello {testCaseName}"); + + Assert.Null(await reader.GetNextEntryAsync()); + } + } + + private void VerifyType(TarEntry entry, TarEntryFormat format, bool isGea = false) + { + Assert.Equal(format, entry.Format); + switch (format) + { + case TarEntryFormat.V7: + Assert.True(entry is V7TarEntry, "Entry was not V7"); + break; + case TarEntryFormat.Ustar: + Assert.True(entry is UstarTarEntry, "Entry was not Ustar"); + break; + case TarEntryFormat.Gnu: + Assert.True(entry is GnuTarEntry, "Entry was not Gnu"); + break; + case TarEntryFormat.Pax: + if (isGea) + { + Assert.Equal(TarEntryType.GlobalExtendedAttributes, entry.EntryType); + Assert.True(entry is PaxGlobalExtendedAttributesTarEntry, "Entry was not PAX GEA"); + } + else + { + Assert.True(entry is PaxTarEntry, "Entry was not PAX"); + } + break; + default: + throw new Exception($"Unexpected format: {format}"); + } + } + + private void VerifyRegularFileEntry(TarEntry file, TarEntryFormat format, string expectedFileName, string expectedContents) + { + Assert.NotNull(file); + VerifyType(file, format); + + Assert.True(file.Checksum > 0); + Assert.NotNull(file.DataStream); + Assert.True(file.DataStream.Length > 0); + Assert.True(file.DataStream.CanRead); + Assert.True(file.DataStream.CanSeek); + file.DataStream.Seek(0, SeekOrigin.Begin); + using (StreamReader reader = new StreamReader(file.DataStream, leaveOpen: true)) + { + string contents = reader.ReadLine(); + Assert.Equal(expectedContents, contents); + } + + TarEntryType expectedEntryType = format == TarEntryFormat.V7 ? TarEntryType.V7RegularFile : TarEntryType.RegularFile; + Assert.Equal(expectedEntryType, file.EntryType); + + Assert.Equal(AssetGid, file.Gid); + Assert.Equal(file.Length, file.DataStream.Length); + Assert.Equal(DefaultLinkName, file.LinkName); + Assert.Equal(AssetMode, file.Mode); + Assert.True(file.ModificationTime > DateTimeOffset.UnixEpoch); + Assert.Equal(expectedFileName, file.Name); + Assert.Equal(AssetUid, file.Uid); + + if (file is PosixTarEntry posix) + { + Assert.Equal(DefaultDeviceMajor, posix.DeviceMajor); + Assert.Equal(DefaultDeviceMinor, posix.DeviceMinor); + Assert.Equal(AssetGName, posix.GroupName); + Assert.Equal(AssetUName, posix.UserName); + + if (posix is PaxTarEntry pax) + { + VerifyExtendedAttributes(pax); + } + else if (posix is GnuTarEntry gnu) + { + VerifyGnuFields(gnu); + } + } + } + + private void VerifySymbolicLinkEntry(TarEntry symbolicLink, TarEntryFormat format, string expectedFileName, string expectedTargetName) + { + Assert.NotNull(symbolicLink); + VerifyType(symbolicLink, format); + + Assert.True(symbolicLink.Checksum > 0); + Assert.Null(symbolicLink.DataStream); + + Assert.Equal(TarEntryType.SymbolicLink, symbolicLink.EntryType); + + Assert.Equal(AssetGid, symbolicLink.Gid); + Assert.Equal(0, symbolicLink.Length); + Assert.Equal(expectedTargetName, symbolicLink.LinkName); + Assert.Equal(AssetSymbolicLinkMode, symbolicLink.Mode); + Assert.True(symbolicLink.ModificationTime > DateTimeOffset.UnixEpoch); + Assert.Equal(expectedFileName, symbolicLink.Name); + Assert.Equal(AssetUid, symbolicLink.Uid); + + if (symbolicLink is PosixTarEntry posix) + { + Assert.Equal(DefaultDeviceMajor, posix.DeviceMajor); + Assert.Equal(DefaultDeviceMinor, posix.DeviceMinor); + Assert.Equal(AssetGName, posix.GroupName); + Assert.Equal(AssetUName, posix.UserName); + } + + if (symbolicLink is PaxTarEntry pax) + { + VerifyExtendedAttributes(pax); + } + else if (symbolicLink is GnuTarEntry gnu) + { + VerifyGnuFields(gnu); + } + } + + private void VerifyDirectoryEntry(TarEntry directory, TarEntryFormat format, string expectedFileName) + { + Assert.NotNull(directory); + VerifyType(directory, format); + + Assert.True(directory.Checksum > 0); + Assert.Null(directory.DataStream); + + Assert.Equal(TarEntryType.Directory, directory.EntryType); + + Assert.Equal(AssetGid, directory.Gid); + Assert.Equal(0, directory.Length); + Assert.Equal(DefaultLinkName, directory.LinkName); + Assert.Equal(AssetMode, directory.Mode); + Assert.True(directory.ModificationTime > DateTimeOffset.UnixEpoch); + Assert.Equal(expectedFileName, directory.Name); + Assert.Equal(AssetUid, directory.Uid); + + if (directory is PosixTarEntry posix) + { + Assert.Equal(DefaultDeviceMajor, posix.DeviceMajor); + Assert.Equal(DefaultDeviceMinor, posix.DeviceMinor); + Assert.Equal(AssetGName, posix.GroupName); + Assert.Equal(AssetUName, posix.UserName); + } + + if (directory is PaxTarEntry pax) + { + VerifyExtendedAttributes(pax); + } + else if (directory is GnuTarEntry gnu) + { + VerifyGnuFields(gnu); + } + } + + private void VerifyBlockDeviceEntry(PosixTarEntry blockDevice, TarEntryFormat format, string expectedFileName) + { + Assert.NotNull(blockDevice); + Assert.Equal(TarEntryType.BlockDevice, blockDevice.EntryType); + VerifyType(blockDevice, format); + + Assert.True(blockDevice.Checksum > 0); + Assert.Null(blockDevice.DataStream); + + Assert.Equal(AssetGid, blockDevice.Gid); + Assert.Equal(0, blockDevice.Length); + Assert.Equal(DefaultLinkName, blockDevice.LinkName); + Assert.Equal(AssetSpecialFileMode, blockDevice.Mode); + Assert.True(blockDevice.ModificationTime > DateTimeOffset.UnixEpoch); + Assert.Equal(expectedFileName, blockDevice.Name); + Assert.Equal(AssetUid, blockDevice.Uid); + + Assert.Equal(AssetBlockDeviceMajor, blockDevice.DeviceMajor); + Assert.Equal(AssetBlockDeviceMinor, blockDevice.DeviceMinor); + Assert.Equal(AssetGName, blockDevice.GroupName); + Assert.Equal(AssetUName, blockDevice.UserName); + + if (blockDevice is PaxTarEntry pax) + { + VerifyExtendedAttributes(pax); + } + else if (blockDevice is GnuTarEntry gnu) + { + VerifyGnuFields(gnu); + } + } + + private void VerifyCharacterDeviceEntry(PosixTarEntry characterDevice, TarEntryFormat format, string expectedFileName) + { + Assert.NotNull(characterDevice); + Assert.Equal(TarEntryType.CharacterDevice, characterDevice.EntryType); + VerifyType(characterDevice, format); + + Assert.True(characterDevice.Checksum > 0); + Assert.Null(characterDevice.DataStream); + + Assert.Equal(AssetGid, characterDevice.Gid); + Assert.Equal(0, characterDevice.Length); + Assert.Equal(DefaultLinkName, characterDevice.LinkName); + Assert.Equal(AssetSpecialFileMode, characterDevice.Mode); + Assert.True(characterDevice.ModificationTime > DateTimeOffset.UnixEpoch); + Assert.Equal(expectedFileName, characterDevice.Name); + Assert.Equal(AssetUid, characterDevice.Uid); + + Assert.Equal(AssetCharacterDeviceMajor, characterDevice.DeviceMajor); + Assert.Equal(AssetCharacterDeviceMinor, characterDevice.DeviceMinor); + Assert.Equal(AssetGName, characterDevice.GroupName); + Assert.Equal(AssetUName, characterDevice.UserName); + + if (characterDevice is PaxTarEntry pax) + { + VerifyExtendedAttributes(pax); + } + else if (characterDevice is GnuTarEntry gnu) + { + VerifyGnuFields(gnu); + } + } + + private void VerifyFifoEntry(PosixTarEntry fifo, TarEntryFormat format, string expectedFileName) + { + Assert.NotNull(fifo); + VerifyType(fifo, format); + + Assert.True(fifo.Checksum > 0); + Assert.Null(fifo.DataStream); + + Assert.Equal(TarEntryType.Fifo, fifo.EntryType); + + Assert.Equal(AssetGid, fifo.Gid); + Assert.Equal(0, fifo.Length); + Assert.Equal(DefaultLinkName, fifo.LinkName); + Assert.Equal(AssetSpecialFileMode, fifo.Mode); + Assert.True(fifo.ModificationTime > DateTimeOffset.UnixEpoch); + Assert.Equal(expectedFileName, fifo.Name); + Assert.Equal(AssetUid, fifo.Uid); + + Assert.Equal(DefaultDeviceMajor, fifo.DeviceMajor); + Assert.Equal(DefaultDeviceMinor, fifo.DeviceMinor); + Assert.Equal(AssetGName, fifo.GroupName); + Assert.Equal(AssetUName, fifo.UserName); + + if (fifo is PaxTarEntry pax) + { + VerifyExtendedAttributes(pax); + } + else if (fifo is GnuTarEntry gnu) + { + VerifyGnuFields(gnu); + } + } + + private void VerifyGlobalExtendedAttributes(TarEntry entry) + { + Assert.NotNull(entry); + Assert.Equal(TarEntryType.GlobalExtendedAttributes, entry.EntryType); + Assert.Equal(TarEntryFormat.Pax, entry.Format); + VerifyType(entry, TarEntryFormat.Pax, isGea: true); + + PaxGlobalExtendedAttributesTarEntry gea = entry as PaxGlobalExtendedAttributesTarEntry; + + // Format: %d/GlobalHead.%p.%n, where: + // - %d is the tmp path (platform dependent, and if too long, gets truncated to just '/tmp') + // - %p is current process ID + // - %n is the sequence number, which is always 1 for the first entry of the asset archive files + Assert.Matches(@".+\/GlobalHead\.\d+\.1", gea.Name); + + Assert.True(gea.GlobalExtendedAttributes.Any()); + Assert.Contains(AssetPaxGeaKey, gea.GlobalExtendedAttributes); + Assert.Equal(AssetPaxGeaValue, gea.GlobalExtendedAttributes[AssetPaxGeaKey]); + } + + private void VerifyExtendedAttributes(PaxTarEntry pax) + { + Assert.NotNull(pax.ExtendedAttributes); + AssertExtensions.GreaterThanOrEqualTo(pax.ExtendedAttributes.Count(), 3); // Expect to at least collect mtime, ctime and atime + + VerifyExtendedAttributeTimestamp(pax, PaxEaMTime, MinimumTime); + VerifyExtendedAttributeTimestamp(pax, PaxEaATime, MinimumTime); + VerifyExtendedAttributeTimestamp(pax, PaxEaCTime, MinimumTime); + } + + private void VerifyGnuFields(GnuTarEntry gnu) + { + AssertExtensions.GreaterThanOrEqualTo(gnu.AccessTime, DateTimeOffset.UnixEpoch); + AssertExtensions.GreaterThanOrEqualTo(gnu.ChangeTime, DateTimeOffset.UnixEpoch); + } + } +} 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 new file mode 100644 index 00000000000000..fbe065bd802630 --- /dev/null +++ b/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.File.Async.Tests.cs @@ -0,0 +1,129 @@ +// 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.Globalization; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Xunit; + +namespace System.Formats.Tar.Tests +{ + public class TarReader_File_Async_Tests : TarReader_File_Tests_Async_Base + { + [Theory] + [InlineData(TarEntryFormat.V7, TestTarFormat.v7)] + [InlineData(TarEntryFormat.Ustar, TestTarFormat.ustar)] + [InlineData(TarEntryFormat.Pax, TestTarFormat.pax)] + [InlineData(TarEntryFormat.Gnu, TestTarFormat.gnu)] + [InlineData(TarEntryFormat.Gnu, TestTarFormat.oldgnu)] + public Task Read_Archive_File_Async(TarEntryFormat format, TestTarFormat testFormat) => + Read_Archive_File_Async_Internal(format, testFormat); + + [Theory] + [InlineData(TarEntryFormat.V7, TestTarFormat.v7)] + [InlineData(TarEntryFormat.Ustar, TestTarFormat.ustar)] + [InlineData(TarEntryFormat.Pax, TestTarFormat.pax)] + [InlineData(TarEntryFormat.Gnu, TestTarFormat.gnu)] + [InlineData(TarEntryFormat.Gnu, TestTarFormat.oldgnu)] + public Task Read_Archive_File_HardLink_Async(TarEntryFormat format, TestTarFormat testFormat) => + Read_Archive_File_HardLink_Async_Internal(format, testFormat); + + [Theory] + [InlineData(TarEntryFormat.V7, TestTarFormat.v7)] + [InlineData(TarEntryFormat.Ustar, TestTarFormat.ustar)] + [InlineData(TarEntryFormat.Pax, TestTarFormat.pax)] + [InlineData(TarEntryFormat.Gnu, TestTarFormat.gnu)] + [InlineData(TarEntryFormat.Gnu, TestTarFormat.oldgnu)] + public Task Read_Archive_File_SymbolicLink_Async(TarEntryFormat format, TestTarFormat testFormat) => + Read_Archive_File_SymbolicLink_Async_Internal(format, testFormat); + + [Theory] + [InlineData(TarEntryFormat.V7, TestTarFormat.v7)] + [InlineData(TarEntryFormat.Ustar, TestTarFormat.ustar)] + [InlineData(TarEntryFormat.Pax, TestTarFormat.pax)] + [InlineData(TarEntryFormat.Gnu, TestTarFormat.gnu)] + [InlineData(TarEntryFormat.Gnu, TestTarFormat.oldgnu)] + public Task Read_Archive_Folder_File_Async(TarEntryFormat format, TestTarFormat testFormat) => + Read_Archive_Folder_File_Async_Internal(format, testFormat); + + [Theory] + [InlineData(TarEntryFormat.V7, TestTarFormat.v7)] + [InlineData(TarEntryFormat.Ustar, TestTarFormat.ustar)] + [InlineData(TarEntryFormat.Pax, TestTarFormat.pax)] + [InlineData(TarEntryFormat.Gnu, TestTarFormat.gnu)] + [InlineData(TarEntryFormat.Gnu, TestTarFormat.oldgnu)] + public Task Read_Archive_Folder_File_Utf8_Async(TarEntryFormat format, TestTarFormat testFormat) => + Read_Archive_Folder_File_Utf8_Async_Internal(format, testFormat); + + [Theory] + [InlineData(TarEntryFormat.V7, TestTarFormat.v7)] + [InlineData(TarEntryFormat.Ustar, TestTarFormat.ustar)] + [InlineData(TarEntryFormat.Pax, TestTarFormat.pax)] + [InlineData(TarEntryFormat.Gnu, TestTarFormat.gnu)] + [InlineData(TarEntryFormat.Gnu, TestTarFormat.oldgnu)] + public Task Read_Archive_Folder_Subfolder_File_Async(TarEntryFormat format, TestTarFormat testFormat) => + Read_Archive_Folder_Subfolder_File_Async_Internal(format, testFormat); + + [Theory] + [InlineData(TarEntryFormat.V7, TestTarFormat.v7)] + [InlineData(TarEntryFormat.Ustar, TestTarFormat.ustar)] + [InlineData(TarEntryFormat.Pax, TestTarFormat.pax)] + [InlineData(TarEntryFormat.Gnu, TestTarFormat.gnu)] + [InlineData(TarEntryFormat.Gnu, TestTarFormat.oldgnu)] + public Task Read_Archive_FolderSymbolicLink_Folder_Subfolder_File_Async(TarEntryFormat format, TestTarFormat testFormat) => + Read_Archive_FolderSymbolicLink_Folder_Subfolder_File_Async_Internal(format, testFormat); + + [Theory] + [InlineData(TarEntryFormat.V7, TestTarFormat.v7)] + [InlineData(TarEntryFormat.Ustar, TestTarFormat.ustar)] + [InlineData(TarEntryFormat.Pax, TestTarFormat.pax)] + [InlineData(TarEntryFormat.Gnu, TestTarFormat.gnu)] + [InlineData(TarEntryFormat.Gnu, TestTarFormat.oldgnu)] + public Task Read_Archive_Many_Small_Files_Async(TarEntryFormat format, TestTarFormat testFormat) => + Read_Archive_Many_Small_Files_Async_Internal(format, testFormat); + + [Theory] + // V7 does not support longer filenames + [InlineData(TarEntryFormat.Ustar, TestTarFormat.ustar)] + [InlineData(TarEntryFormat.Pax, TestTarFormat.pax)] + [InlineData(TarEntryFormat.Gnu, TestTarFormat.gnu)] + [InlineData(TarEntryFormat.Gnu, TestTarFormat.oldgnu)] + public Task Read_Archive_LongPath_Splitable_Under255_Async(TarEntryFormat format, TestTarFormat testFormat) => + Read_Archive_LongPath_Splitable_Under255_Async_Internal(format, testFormat); + + [Theory] + // V7 does not support block devices, character devices or fifos + [InlineData(TarEntryFormat.Ustar, TestTarFormat.ustar)] + [InlineData(TarEntryFormat.Pax, TestTarFormat.pax)] + [InlineData(TarEntryFormat.Gnu, TestTarFormat.gnu)] + [InlineData(TarEntryFormat.Gnu, TestTarFormat.oldgnu)] + public Task Read_Archive_SpecialFiles_Async(TarEntryFormat format, TestTarFormat testFormat) => + Read_Archive_SpecialFiles_Async_Internal(format, testFormat); + + [Theory] + // Neither V7 not Ustar can handle links with long target filenames + [InlineData(TarEntryFormat.Pax, TestTarFormat.pax)] + [InlineData(TarEntryFormat.Gnu, TestTarFormat.gnu)] + [InlineData(TarEntryFormat.Gnu, TestTarFormat.oldgnu)] + public Task Read_Archive_File_LongSymbolicLink_Async(TarEntryFormat format, TestTarFormat testFormat) => + Read_Archive_File_LongSymbolicLink_Async_Internal(format, testFormat); + + [Theory] + // Neither V7 not Ustar can handle a path that does not have separators that can be split under 100 bytes + [InlineData(TarEntryFormat.Pax, TestTarFormat.pax)] + [InlineData(TarEntryFormat.Gnu, TestTarFormat.gnu)] + [InlineData(TarEntryFormat.Gnu, TestTarFormat.oldgnu)] + public Task Read_Archive_LongFileName_Over100_Under255_Async(TarEntryFormat format, TestTarFormat testFormat) => + Read_Archive_LongFileName_Over100_Under255_Async_Internal(format, testFormat); + + [Theory] + // Neither V7 not Ustar can handle path lenghts waaaay beyond name+prefix length + [InlineData(TarEntryFormat.Pax, TestTarFormat.pax)] + [InlineData(TarEntryFormat.Gnu, TestTarFormat.gnu)] + [InlineData(TarEntryFormat.Gnu, TestTarFormat.oldgnu)] + public Task Read_Archive_LongPath_Over255_Async(TarEntryFormat format, TestTarFormat testFormat) => + Read_Archive_LongPath_Over255_Async_Internal(format, testFormat); + } +} diff --git a/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.File.GlobalExtendedAttributes.Async.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.File.GlobalExtendedAttributes.Async.Tests.cs new file mode 100644 index 00000000000000..45047d930c089d --- /dev/null +++ b/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.File.GlobalExtendedAttributes.Async.Tests.cs @@ -0,0 +1,89 @@ +// 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.Linq; +using System.Threading.Tasks; +using Xunit; + +namespace System.Formats.Tar.Tests +{ + public class TarReader_File_GlobalExtendedAttributes_Async_Tests : TarReader_File_Tests_Async_Base + { + [Fact] + public Task Read_Archive_File_Async() => + Read_Archive_File_Async_Internal(TarEntryFormat.Pax, TestTarFormat.pax_gea); + + [Fact] + public Task Read_Archive_File_HardLink_Async() => + Read_Archive_File_HardLink_Async_Internal(TarEntryFormat.Pax, TestTarFormat.pax_gea); + + [Fact] + public Task Read_Archive_File_SymbolicLink_Async() => + Read_Archive_File_SymbolicLink_Async_Internal(TarEntryFormat.Pax, TestTarFormat.pax_gea); + + [Fact] + public Task Read_Archive_Folder_File_Async() => + Read_Archive_Folder_File_Async_Internal(TarEntryFormat.Pax, TestTarFormat.pax_gea); + + [Fact] + public Task Read_Archive_Folder_File_Utf8_Async() => + Read_Archive_Folder_File_Utf8_Async_Internal(TarEntryFormat.Pax, TestTarFormat.pax_gea); + + [Fact] + public Task Read_Archive_Folder_Subfolder_File_Async() => + Read_Archive_Folder_Subfolder_File_Async_Internal(TarEntryFormat.Pax, TestTarFormat.pax_gea); + + [Fact] + public Task Read_Archive_FolderSymbolicLink_Folder_Subfolder_File_Async() => + Read_Archive_FolderSymbolicLink_Folder_Subfolder_File_Async_Internal(TarEntryFormat.Pax, TestTarFormat.pax_gea); + + [Fact] + public Task Read_Archive_Many_Small_Files_Async() => + Read_Archive_Many_Small_Files_Async_Internal(TarEntryFormat.Pax, TestTarFormat.pax_gea); + + [Fact] + public Task Read_Archive_LongPath_Splitable_Under255_Async() => + Read_Archive_LongPath_Splitable_Under255_Async_Internal(TarEntryFormat.Pax, TestTarFormat.pax_gea); + + [Fact] + public Task Read_Archive_SpecialFiles_Async() => + Read_Archive_SpecialFiles_Async_Internal(TarEntryFormat.Pax, TestTarFormat.pax_gea); + + [Fact] + public Task Read_Archive_File_LongSymbolicLink_Async() => + Read_Archive_File_LongSymbolicLink_Async_Internal(TarEntryFormat.Pax, TestTarFormat.pax_gea); + + [Fact] + public Task Read_Archive_LongFileName_Over100_Under255_Async() => + Read_Archive_LongFileName_Over100_Under255_Async_Internal(TarEntryFormat.Pax, TestTarFormat.pax_gea); + + [Fact] + public Task Read_Archive_LongPath_Over255_Async() => + Read_Archive_LongPath_Over255_Async_Internal(TarEntryFormat.Pax, TestTarFormat.pax_gea); + + [Fact] + public async Task ExtractGlobalExtendedAttributesEntry_Throws_Async() + { + using TempDirectory root = new TempDirectory(); + + using MemoryStream archiveStream = new MemoryStream(); + TarWriter writer = new TarWriter(archiveStream, leaveOpen: true); + await using (writer) + { + PaxGlobalExtendedAttributesTarEntry gea = new PaxGlobalExtendedAttributesTarEntry(new Dictionary()); + await writer.WriteEntryAsync(gea); + } + + archiveStream.Position = 0; + + TarReader reader = new TarReader(archiveStream, leaveOpen: false); + await using (reader) + { + TarEntry entry = await reader.GetNextEntryAsync(); + await Assert.ThrowsAsync(async () => await entry.ExtractToFileAsync(Path.Join(root.Path, "file"), overwrite: true)); + } + } + } +} From 23894beb58142e2054145105bff1e6c62aa22ab8 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Fri, 24 Jun 2022 18:30:23 -0700 Subject: [PATCH 34/75] Add async compressed tar tests. --- .../tests/CompressedTar.Async.Tests.cs | 127 ++++++++++++++++++ .../tests/System.Formats.Tar.Tests.csproj | 1 + 2 files changed, 128 insertions(+) create mode 100644 src/libraries/System.Formats.Tar/tests/CompressedTar.Async.Tests.cs diff --git a/src/libraries/System.Formats.Tar/tests/CompressedTar.Async.Tests.cs b/src/libraries/System.Formats.Tar/tests/CompressedTar.Async.Tests.cs new file mode 100644 index 00000000000000..ee770fb929e265 --- /dev/null +++ b/src/libraries/System.Formats.Tar/tests/CompressedTar.Async.Tests.cs @@ -0,0 +1,127 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO.Compression; +using System.IO; +using Xunit; +using System.Threading.Tasks; + +namespace System.Formats.Tar.Tests +{ + public class CompressedTar_Async_Tests : TarTestsBase + { + [Fact] + public async Task TarGz_TarWriter_TarReader_Async() + { + using TempDirectory root = new TempDirectory(); + + string archivePath = Path.Join(root.Path, "compressed.tar.gz"); + + string fileName = "file.txt"; + string filePath = Path.Join(root.Path, fileName); + File.Create(filePath).Dispose(); + + // Create tar.gz archive + FileStreamOptions createOptions = new() + { + Mode = FileMode.CreateNew, + Access = FileAccess.Write, + Options = FileOptions.Asynchronous + }; + FileStream streamToCompress = File.Open(archivePath, createOptions); + await using (streamToCompress) + { + GZipStream compressorStream = new GZipStream(streamToCompress, CompressionMode.Compress); + await using (compressorStream) + { + TarWriter writer = new TarWriter(compressorStream); + await using (writer) + { + await writer.WriteEntryAsync(fileName: filePath, entryName: fileName); + } + } + } + FileInfo fileInfo = new FileInfo(archivePath); + Assert.True(fileInfo.Exists); + Assert.True(fileInfo.Length > 0); + + // Verify tar.gz archive contents + FileStreamOptions readOptions = new() + { + Mode = FileMode.Open, + Access = FileAccess.Read, + Options = FileOptions.Asynchronous + }; + FileStream streamToDecompress = File.Open(archivePath, readOptions); + await using (streamToDecompress) + { + GZipStream decompressorStream = new GZipStream(streamToDecompress, CompressionMode.Decompress); + await using (decompressorStream) + { + TarReader reader = new TarReader(decompressorStream); + await using (reader) + { + TarEntry entry = await reader.GetNextEntryAsync(); + Assert.Equal(TarEntryFormat.Pax, entry.Format); + Assert.Equal(fileName, entry.Name); + Assert.Null(await reader.GetNextEntryAsync()); + } + } + } + } + + [Fact] + public async Task TarGz_TarFile_CreateFromDir_ExtractToDir_Async() + { + using TempDirectory root = new TempDirectory(); + + string archivePath = Path.Join(root.Path, "compressed.tar.gz"); + + string sourceDirectory = Path.Join(root.Path, "source"); + Directory.CreateDirectory(sourceDirectory); + + string destinationDirectory = Path.Join(root.Path, "destination"); + Directory.CreateDirectory(destinationDirectory); + + string fileName = "file.txt"; + string filePath = Path.Join(sourceDirectory, fileName); + File.Create(filePath).Dispose(); + + FileStreamOptions createOptions = new() + { + Mode = FileMode.CreateNew, + Access = FileAccess.Write, + Options = FileOptions.Asynchronous + }; + FileStream streamToCompress = File.Open(archivePath, createOptions); + await using (streamToCompress) + { + GZipStream compressorStream = new GZipStream(streamToCompress, CompressionMode.Compress); + await using (compressorStream) + { + await TarFile.CreateFromDirectoryAsync(sourceDirectory, compressorStream, includeBaseDirectory: false); + } + } + FileInfo fileInfo = new FileInfo(archivePath); + Assert.True(fileInfo.Exists); + Assert.True(fileInfo.Length > 0); + + FileStreamOptions readOptions = new() + { + Mode = FileMode.Open, + Access = FileAccess.Read, + Options = FileOptions.Asynchronous + }; + FileStream streamToDecompress = File.Open(archivePath, readOptions); + await using (streamToDecompress) + { + GZipStream decompressorStream = new GZipStream(streamToDecompress, CompressionMode.Decompress); + await using (decompressorStream) + { + await TarFile.ExtractToDirectoryAsync(decompressorStream, destinationDirectory, overwriteFiles: true); + Assert.True(File.Exists(filePath)); + } + } + } + } +} diff --git a/src/libraries/System.Formats.Tar/tests/System.Formats.Tar.Tests.csproj b/src/libraries/System.Formats.Tar/tests/System.Formats.Tar.Tests.csproj index c7ee267016e9b2..dc5ede735db93a 100644 --- a/src/libraries/System.Formats.Tar/tests/System.Formats.Tar.Tests.csproj +++ b/src/libraries/System.Formats.Tar/tests/System.Formats.Tar.Tests.csproj @@ -8,6 +8,7 @@ + From 2ff86ecb8a1456a371f9fb48a45b8e106cc747ba Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Fri, 24 Jun 2022 19:50:51 -0700 Subject: [PATCH 35/75] Adjust checksum calculation and saving after writing in async methods. --- .../src/System/Formats/Tar/TarHeader.Write.cs | 58 +++++++++++-------- .../src/System/Formats/Tar/TarWriter.cs | 10 ++-- 2 files changed, 38 insertions(+), 30 deletions(-) diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs index bac309a9bc87fa..ac92da168391f4 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 @@ -34,7 +34,7 @@ internal void WriteAsV7(Stream archiveStream, Span buffer) int checksum = WriteNameSpan(buffer, out _); checksum += WriteCommonFields(buffer, actualLength, actualEntryType); - WriteChecksum(checksum, buffer); + _checksum = WriteChecksum(checksum, buffer); archiveStream.Write(buffer); @@ -44,15 +44,15 @@ internal void WriteAsV7(Stream archiveStream, Span buffer) } } - // Asynchronously writes the current header as a V7 entry into the archive stream. - internal async Task WriteAsV7Async(Stream archiveStream, Memory buffer, CancellationToken cancellationToken) + // 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) { long actualLength = GetTotalDataBytesToWrite(); TarEntryType actualEntryType = GetCorrectTypeFlagForFormat(TarEntryFormat.V7); int checksum = WriteNameMemory(buffer, out _); checksum += WriteCommonFields(buffer.Span, actualLength, actualEntryType); - WriteChecksum(checksum, buffer.Span); + int finalChecksum = WriteChecksum(checksum, buffer.Span); await archiveStream.WriteAsync(buffer, cancellationToken).ConfigureAwait(false); @@ -60,6 +60,8 @@ internal async Task WriteAsV7Async(Stream archiveStream, Memory buffer, Ca { await WriteDataAsync(archiveStream, _dataStream, actualLength, cancellationToken).ConfigureAwait(false); } + + return finalChecksum; } // Writes the current header as a Ustar entry into the archive stream. @@ -72,7 +74,7 @@ internal void WriteAsUstar(Stream archiveStream, Span buffer) checksum += WriteCommonFields(buffer, actualLength, actualEntryType); checksum += WritePosixMagicAndVersion(buffer); checksum += WritePosixAndGnuSharedFields(buffer); - WriteChecksum(checksum, buffer); + _checksum = WriteChecksum(checksum, buffer); archiveStream.Write(buffer); @@ -82,8 +84,8 @@ internal void WriteAsUstar(Stream archiveStream, Span buffer) } } - // Asynchronously rites the current header as a Ustar entry into the archive stream. - internal async Task WriteAsUstarAsync(Stream archiveStream, Memory buffer, CancellationToken cancellationToken) + // 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) { long actualLength = GetTotalDataBytesToWrite(); TarEntryType actualEntryType = GetCorrectTypeFlagForFormat(TarEntryFormat.Ustar); @@ -92,7 +94,7 @@ internal async Task WriteAsUstarAsync(Stream archiveStream, Memory buffer, checksum += WriteCommonFields(buffer.Span, actualLength, actualEntryType); checksum += WritePosixMagicAndVersion(buffer.Span); checksum += WritePosixAndGnuSharedFields(buffer.Span); - WriteChecksum(checksum, buffer.Span); + int finalChecksum = WriteChecksum(checksum, buffer.Span); await archiveStream.WriteAsync(buffer, cancellationToken).ConfigureAwait(false); @@ -100,6 +102,8 @@ internal async Task WriteAsUstarAsync(Stream archiveStream, Memory buffer, { await WriteDataAsync(archiveStream, _dataStream, actualLength, cancellationToken).ConfigureAwait(false); } + + return finalChecksum; } // Writes the current header as a PAX Global Extended Attributes entry into the archive stream. @@ -112,8 +116,8 @@ internal void WriteAsPaxGlobalExtendedAttributes(Stream archiveStream, Span buffer, int globalExtendedAttributesEntryNumber, CancellationToken cancellationToken) + // Writes the current header as a PAX Global Extended Attributes entry into the archive stream and returns the value of the final checksum. + internal Task WriteAsPaxGlobalExtendedAttributesAsync(Stream archiveStream, Memory buffer, int globalExtendedAttributesEntryNumber, CancellationToken cancellationToken) { Debug.Assert(_typeFlag is TarEntryType.GlobalExtendedAttributes); @@ -143,7 +147,7 @@ internal void WriteAsPax(Stream archiveStream, Span buffer) // Asynchronously writes the current header as a PAX entry into the archive stream. // Makes sure to add the preceding exteded attributes entry before the actual entry. - internal async Task WriteAsPaxAsync(Stream archiveStream, Memory buffer, CancellationToken cancellationToken) + internal async Task WriteAsPaxAsync(Stream archiveStream, Memory buffer, CancellationToken cancellationToken) { // First, we write the preceding extended attributes header TarHeader extendedAttributesHeader = default; @@ -155,7 +159,7 @@ internal async Task WriteAsPaxAsync(Stream archiveStream, Memory buffer, C buffer.Span.Clear(); // Reset it to reuse it // Second, we write this header as a normal one - await WriteAsPaxInternalAsync(archiveStream, buffer, cancellationToken).ConfigureAwait(false); + return await WriteAsPaxInternalAsync(archiveStream, buffer, cancellationToken).ConfigureAwait(false); } // Writes the current header as a Gnu entry into the archive stream. @@ -184,7 +188,7 @@ internal void WriteAsGnu(Stream archiveStream, Span buffer) // Writes the current header as a Gnu entry into the archive stream. // Makes sure to add the preceding LongLink and/or LongPath entries if necessary, before the actual entry. - internal async Task WriteAsGnuAsync(Stream archiveStream, Memory buffer, CancellationToken cancellationToken) + internal async Task WriteAsGnuAsync(Stream archiveStream, Memory buffer, CancellationToken cancellationToken) { // First, we determine if we need a preceding LongLink, and write it if needed if (_linkName.Length > FieldLengths.LinkName) @@ -203,7 +207,7 @@ internal async Task WriteAsGnuAsync(Stream archiveStream, Memory buffer, C } // Third, we write this header as a normal one - await WriteAsGnuInternalAsync(archiveStream, buffer, cancellationToken).ConfigureAwait(false); + return await WriteAsGnuInternalAsync(archiveStream, buffer, cancellationToken).ConfigureAwait(false); } // Creates and returns a GNU long metadata header, with the specified long text written into its data stream. @@ -266,7 +270,7 @@ internal void WriteAsGnuInternal(Stream archiveStream, Span buffer) checksum += WriteGnuMagicAndVersion(buffer); checksum += WritePosixAndGnuSharedFields(buffer); checksum += WriteGnuFields(buffer); - WriteChecksum(checksum, buffer); + _checksum = WriteChecksum(checksum, buffer); archiveStream.Write(buffer); @@ -277,7 +281,7 @@ internal void WriteAsGnuInternal(Stream archiveStream, Span buffer) } // Asynchronously writes the current header as a GNU entry into the archive stream. - internal async Task WriteAsGnuInternalAsync(Stream archiveStream, Memory buffer, CancellationToken cancellationToken) + internal async Task WriteAsGnuInternalAsync(Stream archiveStream, Memory buffer, CancellationToken cancellationToken) { // Unused GNU fields: offset, longnames, unused, sparse struct, isextended and realsize // If this header came from another archive, it will have a value @@ -292,7 +296,7 @@ internal async Task WriteAsGnuInternalAsync(Stream archiveStream, Memory b checksum += WriteGnuMagicAndVersion(buffer.Span); checksum += WritePosixAndGnuSharedFields(buffer.Span); checksum += WriteGnuFields(buffer.Span); - WriteChecksum(checksum, buffer.Span); + int finalChecksum = WriteChecksum(checksum, buffer.Span); await archiveStream.WriteAsync(buffer, cancellationToken).ConfigureAwait(false); @@ -300,6 +304,8 @@ internal async Task WriteAsGnuInternalAsync(Stream archiveStream, Memory b { await WriteDataAsync(archiveStream, _dataStream, actualLength, cancellationToken).ConfigureAwait(false); } + + return finalChecksum; } // Writes the current header as a PAX Extended Attributes entry into the archive stream. @@ -321,8 +327,8 @@ private void WriteAsPaxExtendedAttributes(Stream archiveStream, Span buffe WriteAsPaxInternal(archiveStream, buffer); } - // Asynchronously writes the current header as a PAX Extended Attributes entry into the archive stream. - private async Task WriteAsPaxExtendedAttributesAsync(Stream archiveStream, Memory buffer, IEnumerable> extendedAttributes, bool isGea, CancellationToken cancellationToken) + // Asynchronously writes the current header as a PAX Extended Attributes entry into the archive stream and returns the value of the final checksum. + private async Task WriteAsPaxExtendedAttributesAsync(Stream archiveStream, Memory buffer, IEnumerable> extendedAttributes, bool isGea, CancellationToken cancellationToken) { // The ustar fields (uid, gid, linkName, uname, gname, devmajor, devminor) do not get written. // The mode gets the default value. @@ -337,7 +343,7 @@ private async Task WriteAsPaxExtendedAttributesAsync(Stream archiveStream, Memor _dataStream = await GenerateExtendedAttributesDataStreamAsync(extendedAttributes, cancellationToken).ConfigureAwait(false); - await WriteAsPaxInternalAsync(archiveStream, buffer, cancellationToken).ConfigureAwait(false); + return await WriteAsPaxInternalAsync(archiveStream, buffer, cancellationToken).ConfigureAwait(false); } // Both the Extended Attributes and Global Extended Attributes entry headers are written in a similar way, just the data changes @@ -363,7 +369,7 @@ private void WriteAsPaxInternal(Stream archiveStream, Span buffer) // 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) + private async Task WriteAsPaxInternalAsync(Stream archiveStream, Memory buffer, CancellationToken cancellationToken) { long actualLength = GetTotalDataBytesToWrite(); TarEntryType actualEntryType = GetCorrectTypeFlagForFormat(TarEntryFormat.Pax); @@ -372,7 +378,7 @@ private async Task WriteAsPaxInternalAsync(Stream archiveStream, Memory bu checksum += WriteCommonFields(buffer.Span, actualLength, actualEntryType); checksum += WritePosixMagicAndVersion(buffer.Span); checksum += WritePosixAndGnuSharedFields(buffer.Span); - WriteChecksum(checksum, buffer.Span); + int finalChecksum = WriteChecksum(checksum, buffer.Span); await archiveStream.WriteAsync(buffer, cancellationToken).ConfigureAwait(false); @@ -380,6 +386,8 @@ private async Task WriteAsPaxInternalAsync(Stream archiveStream, Memory bu { await WriteDataAsync(archiveStream, _dataStream, actualLength, cancellationToken).ConfigureAwait(false); } + + return finalChecksum; } // All formats save in the name byte array only the ASCII bytes that fit. The full string is returned in the out byte array. @@ -688,8 +696,8 @@ private static byte[] GenerateExtendedAttributeKeyValuePairAsByteArray(byte[] ke // The checksum accumulator first adds up the byte values of eight space chars, then the final number // is written on top of those spaces on the specified span as ascii. - // At the end, it's saved in the header field. - internal void WriteChecksum(int checksum, Span buffer) + // At the end, it's saved in the header field and the final value returned. + internal int WriteChecksum(int checksum, Span buffer) { // The checksum field is also counted towards the total sum // but as an array filled with spaces @@ -721,7 +729,7 @@ internal void WriteChecksum(int checksum, Span buffer) i--; } - _checksum = checksum; + return checksum; } // Writes the specified bytes into the specified destination, aligned to the left. Returns the sum of the value of all the bytes that were written. 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 adccf84a4568eb..666716d0d83e24 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 @@ -335,27 +335,27 @@ private async Task WriteEntryAsyncInternal(TarEntry entry, CancellationToken can switch (entry.Format) { case TarEntryFormat.V7: - await entry._header.WriteAsV7Async(_archiveStream, buffer, cancellationToken).ConfigureAwait(false); + entry._header._checksum = await entry._header.WriteAsV7Async(_archiveStream, buffer, cancellationToken).ConfigureAwait(false); break; case TarEntryFormat.Ustar: - await entry._header.WriteAsUstarAsync(_archiveStream, buffer, cancellationToken).ConfigureAwait(false); + entry._header._checksum = await entry._header.WriteAsUstarAsync(_archiveStream, buffer, cancellationToken).ConfigureAwait(false); break; case TarEntryFormat.Pax: if (entry._header._typeFlag is TarEntryType.GlobalExtendedAttributes) { - await entry._header.WriteAsPaxGlobalExtendedAttributesAsync(_archiveStream, buffer, _nextGlobalExtendedAttributesEntryNumber, cancellationToken).ConfigureAwait(false); + entry._header._checksum = await entry._header.WriteAsPaxGlobalExtendedAttributesAsync(_archiveStream, buffer, _nextGlobalExtendedAttributesEntryNumber, cancellationToken).ConfigureAwait(false); _nextGlobalExtendedAttributesEntryNumber++; } else { - await entry._header.WriteAsPaxAsync(_archiveStream, buffer, cancellationToken).ConfigureAwait(false); + entry._header._checksum = await entry._header.WriteAsPaxAsync(_archiveStream, buffer, cancellationToken).ConfigureAwait(false); } break; case TarEntryFormat.Gnu: - await entry._header.WriteAsGnuAsync(_archiveStream, buffer, cancellationToken).ConfigureAwait(false); + entry._header._checksum = await entry._header.WriteAsGnuAsync(_archiveStream, buffer, cancellationToken).ConfigureAwait(false); break; case TarEntryFormat.Unknown: From 9f674b05fcbab60758b055ac26dea3c1ed92310e Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Fri, 24 Jun 2022 19:51:28 -0700 Subject: [PATCH 36/75] Add async V7 checksum verification test. --- .../tests/System.Formats.Tar.Tests.csproj | 1 + .../tests/TarWriter/TarWriter.Async.Tests.cs | 112 ++++++++++++++++++ 2 files changed, 113 insertions(+) create mode 100644 src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.Async.Tests.cs diff --git a/src/libraries/System.Formats.Tar/tests/System.Formats.Tar.Tests.csproj b/src/libraries/System.Formats.Tar/tests/System.Formats.Tar.Tests.csproj index dc5ede735db93a..58ea1b12391fd1 100644 --- a/src/libraries/System.Formats.Tar/tests/System.Formats.Tar.Tests.csproj +++ b/src/libraries/System.Formats.Tar/tests/System.Formats.Tar.Tests.csproj @@ -39,6 +39,7 @@ + diff --git a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.Async.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.Async.Tests.cs new file mode 100644 index 00000000000000..87f2c058166d5f --- /dev/null +++ b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.Async.Tests.cs @@ -0,0 +1,112 @@ +// 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.Threading.Tasks; +using Xunit; + +namespace System.Formats.Tar.Tests +{ + public class TarWriter_Async_Tests : TarTestsBase + { + [Fact] + public async Task Constructors_LeaveOpen_Async() + { + using MemoryStream archiveStream = new MemoryStream(); + + TarWriter writer1 = new TarWriter(archiveStream, TarEntryFormat.Pax, leaveOpen: true); + await writer1.DisposeAsync(); + archiveStream.WriteByte(0); // Should succeed because stream was not closed + + TarWriter writer2 = new TarWriter(archiveStream, TarEntryFormat.Pax, leaveOpen: false); + await writer2.DisposeAsync(); + Assert.Throws(() => archiveStream.WriteByte(0)); // Should fail because stream was closed + } + + [Fact] + public async Task Constructor_NoEntryInsertion_WritesNothing_Async() + { + using MemoryStream archiveStream = new MemoryStream(); + TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Pax, leaveOpen: true); + await writer.DisposeAsync(); // No entries inserted, should write no empty records + Assert.Equal(0, archiveStream.Length); + } + + [Fact] + public async void Write_To_UnseekableStream_Async() + { + using MemoryStream inner = new MemoryStream(); + using WrappedStream wrapped = new WrappedStream(inner, canRead: true, canWrite: true, canSeek: false); + + TarWriter writer = new TarWriter(wrapped, TarEntryFormat.Pax, leaveOpen: true); + await using (writer) + { + PaxTarEntry paxEntry = new PaxTarEntry(TarEntryType.RegularFile, "file.txt"); + await writer.WriteEntryAsync(paxEntry); + } // The final records should get written, and the length should not be set because position cannot be read + + inner.Seek(0, SeekOrigin.Begin); // Rewind the base stream (wrapped cannot be rewound) + + TarReader reader = new TarReader(wrapped); + await using (reader) + { + TarEntry entry = await reader.GetNextEntryAsync(); + Assert.Equal(TarEntryFormat.Pax, entry.Format); + Assert.Equal(TarEntryType.RegularFile, entry.EntryType); + Assert.Null(await reader.GetNextEntryAsync()); + } + } + + [Fact] + public async Task VerifyChecksumV7_Async() + { + using MemoryStream archive = new MemoryStream(); + TarWriter writer = new TarWriter(archive, TarEntryFormat.V7, leaveOpen: true); + await using (writer) + { + V7TarEntry entry = new V7TarEntry( + // '\0' = 0 + TarEntryType.V7RegularFile, + // 'a.b' = 97 + 46 + 98 = 241 + entryName: "a.b"); + + // '0000744\0' = 48 + 48 + 48 + 48 + 55 + 52 + 52 + 0 = 351 + entry.Mode = AssetMode; // octal 744 = u+rxw, g+r, o+r + + // '0017351\0' = 48 + 48 + 49 + 55 + 51 + 53 + 49 + 0 = 353 + entry.Uid = AssetUid; // decimal 7913, octal 17351 + + // '0006773\0' = 48 + 48 + 48 + 54 + 55 + 55 + 51 + 0 = 359 + entry.Gid = AssetGid; // decimal 3579, octal 6773 + + // '14164217674\0' = 49 + 52 + 49 + 54 + 52 + 50 + 49 + 55 + 54 + 55 + 52 + 0 = 571 + DateTimeOffset mtime = new DateTimeOffset(2022, 1, 2, 3, 45, 00, TimeSpan.Zero); // ToUnixTimeSeconds() = decimal 1641095100, octal 14164217674 + entry.ModificationTime = mtime; + + entry.DataStream = new MemoryStream(); + byte[] buffer = new byte[] { 72, 101, 108, 108, 111 }; + + // '0000000005\0' = 48 + 48 + 48 + 48 + 48 + 48 + 48 + 48 + 48 + 48 + 53 + 0 = 533 + await entry.DataStream.WriteAsync(buffer); // Data length: decimal 5 + entry.DataStream.Seek(0, SeekOrigin.Begin); // Rewind to ensure it gets written from the beginning + + // Sum so far: 0 + 241 + 351 + 353 + 359 + 571 + 533 = decimal 2408 + // Add 8 spaces to the sum: 2408 + (8 x 32) = octal 5150, decimal 2664 (final) + // Checksum: '005150\0 ' + + await writer.WriteEntryAsync(entry); + + Assert.Equal(2664, entry.Checksum); + } + + archive.Seek(0, SeekOrigin.Begin); + TarReader reader = new TarReader(archive); + await using (reader) + { + TarEntry entry = await reader.GetNextEntryAsync(); + Assert.Equal(2664, entry.Checksum); + } + } + } +} From c5019b82d1f1f9ad453470ed9f712f90abb56971 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Fri, 24 Jun 2022 20:31:07 -0700 Subject: [PATCH 37/75] Fix await using for FileStreams in TarFile. Move awaited code to internal methods. --- .../src/System/Formats/Tar/TarFile.cs | 48 ++++++++++++++++--- 1 file changed, 41 insertions(+), 7 deletions(-) diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarFile.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarFile.cs index 2f129b1da58eb3..c39216c4effbc6 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarFile.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarFile.cs @@ -118,10 +118,7 @@ public static Task CreateFromDirectoryAsync(string sourceDirectoryName, string d return Task.FromException(new DirectoryNotFoundException(string.Format(SR.IO_PathNotFound_Path, sourceDirectoryName))); } - // Throws if the destination file exists - using FileStream fs = new(destinationFileName, FileMode.CreateNew, FileAccess.Write); - - return CreateFromDirectoryInternalAsync(sourceDirectoryName, fs, includeBaseDirectory, leaveOpen: false, cancellationToken); + return CreateFromDirectoryInternalAsync(sourceDirectoryName, destinationFileName, includeBaseDirectory, cancellationToken); } /// @@ -250,9 +247,7 @@ public static Task ExtractToDirectoryAsync(string sourceFileName, string destina return Task.FromException(new DirectoryNotFoundException(string.Format(SR.IO_PathNotFound_Path, destinationDirectoryName))); } - using FileStream archive = File.OpenRead(sourceFileName); - - return ExtractToDirectoryInternalAsync(archive, destinationDirectoryName, overwriteFiles, leaveOpen: false, cancellationToken); + return ExtractToDirectoryInternalAsync(sourceFileName, destinationDirectoryName, overwriteFiles, cancellationToken); } // Creates an archive from the contents of a directory. @@ -309,6 +304,26 @@ private static void CreateFromDirectoryInternal(string sourceDirectoryName, Stre } } + // Asynchronously creates a tar archive from the contents of the specified directory, and outputs them into the specified path. + private static async Task CreateFromDirectoryInternalAsync(string sourceDirectoryName, string destinationFileName, bool includeBaseDirectory, CancellationToken cancellationToken) + { + Debug.Assert(!string.IsNullOrEmpty(sourceDirectoryName)); + Debug.Assert(!string.IsNullOrEmpty(destinationFileName)); + + FileStreamOptions options = new() + { + Access = FileAccess.Write, + Mode = FileMode.CreateNew, + Options = FileOptions.Asynchronous, + }; + // Throws if the destination file exists + FileStream archive = new(destinationFileName, options); + await using (archive.ConfigureAwait(false)) + { + await CreateFromDirectoryInternalAsync(sourceDirectoryName, archive, includeBaseDirectory, leaveOpen: false, cancellationToken).ConfigureAwait(false); + } + } + // Asynchronously creates an archive from the contents of a directory. // It assumes the sourceDirectoryName is a fully qualified path, and allows choosing if the archive stream should be left open or not. private static async Task CreateFromDirectoryInternalAsync(string sourceDirectoryName, Stream destination, bool includeBaseDirectory, bool leaveOpen, CancellationToken cancellationToken) @@ -385,6 +400,25 @@ private static void ExtractToDirectoryInternal(Stream source, string destination } } + // Asynchronously extracts the contents of a tar file into the specified directory. + private static async Task ExtractToDirectoryInternalAsync(string sourceFileName, string destinationDirectoryName, bool overwriteFiles, CancellationToken cancellationToken) + { + Debug.Assert(!string.IsNullOrEmpty(sourceFileName)); + Debug.Assert(!string.IsNullOrEmpty(destinationDirectoryName)); + + FileStreamOptions options = new() + { + Access = FileAccess.Read, + Mode = FileMode.Open, + Options = FileOptions.Asynchronous, + }; + FileStream archive = new(sourceFileName, options); + await using (archive.ConfigureAwait(false)) + { + await ExtractToDirectoryInternalAsync(archive, destinationDirectoryName, overwriteFiles, leaveOpen: false, cancellationToken).ConfigureAwait(false); + } + } + // Asynchronously extracts an archive into the specified directory. // It assumes the destinationDirectoryName is a fully qualified path, and allows choosing if the archive stream should be left open or not. private static async Task ExtractToDirectoryInternalAsync(Stream source, string destinationDirectoryPath, bool overwriteFiles, bool leaveOpen, CancellationToken cancellationToken) From c7424155f168dc01096ed50ec0baaf4625b74b16 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Fri, 24 Jun 2022 20:31:26 -0700 Subject: [PATCH 38/75] Add async tests for TarFile.CreateFromDirectoryAsync for file assets. --- .../tests/System.Formats.Tar.Tests.csproj | 1 + ...ile.CreateFromDirectoryAsync.File.Tests.cs | 199 ++++++++++++++++++ 2 files changed, 200 insertions(+) create mode 100644 src/libraries/System.Formats.Tar/tests/TarFile/TarFile.CreateFromDirectoryAsync.File.Tests.cs diff --git a/src/libraries/System.Formats.Tar/tests/System.Formats.Tar.Tests.csproj b/src/libraries/System.Formats.Tar/tests/System.Formats.Tar.Tests.csproj index 58ea1b12391fd1..d99e529a37fa14 100644 --- a/src/libraries/System.Formats.Tar/tests/System.Formats.Tar.Tests.csproj +++ b/src/libraries/System.Formats.Tar/tests/System.Formats.Tar.Tests.csproj @@ -10,6 +10,7 @@ + diff --git a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.CreateFromDirectoryAsync.File.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.CreateFromDirectoryAsync.File.Tests.cs new file mode 100644 index 00000000000000..6ef66b014fb7f4 --- /dev/null +++ b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.CreateFromDirectoryAsync.File.Tests.cs @@ -0,0 +1,199 @@ +// 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.Linq; +using System.Threading.Tasks; +using Xunit; + +namespace System.Formats.Tar.Tests +{ + public class TarFile_CreateFromDirectoryAsync_File_Tests : TarTestsBase + { + [Fact] + public async Task InvalidPaths_Throw_Async() + { + await Assert.ThrowsAsync(async () => await TarFile.CreateFromDirectoryAsync(sourceDirectoryName: null,destinationFileName: "path", includeBaseDirectory: false)); + await Assert.ThrowsAsync(async () => await TarFile.CreateFromDirectoryAsync(sourceDirectoryName: string.Empty,destinationFileName: "path", includeBaseDirectory: false)); + await Assert.ThrowsAsync(async () => await TarFile.CreateFromDirectoryAsync(sourceDirectoryName: "path",destinationFileName: null, includeBaseDirectory: false)); + await Assert.ThrowsAsync(async () => await TarFile.CreateFromDirectoryAsync(sourceDirectoryName: "path",destinationFileName: string.Empty, includeBaseDirectory: false)); + } + + [Fact] + public async Task NonExistentDirectory_Throws_Async() + { + using TempDirectory root = new TempDirectory(); + + string dirPath = Path.Join(root.Path, "dir"); + string filePath = Path.Join(root.Path, "file.tar"); + + await Assert.ThrowsAsync(async () => await TarFile.CreateFromDirectoryAsync(sourceDirectoryName: "IDontExist", destinationFileName: filePath, includeBaseDirectory: false)); + } + + [Fact] + public async Task DestinationExists_Throws_Async() + { + using TempDirectory root = new TempDirectory(); + + string dirPath = Path.Join(root.Path, "dir"); + Directory.CreateDirectory(dirPath); + + string filePath = Path.Join(root.Path, "file.tar"); + File.Create(filePath).Dispose(); + + await Assert.ThrowsAsync(async () => await TarFile.CreateFromDirectoryAsync(sourceDirectoryName: dirPath, destinationFileName: filePath, includeBaseDirectory: false)); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task VerifyIncludeBaseDirectory_Async(bool includeBaseDirectory) + { + using TempDirectory source = new TempDirectory(); + using TempDirectory destination = new TempDirectory(); + + string fileName1 = "file1.txt"; + string filePath1 = Path.Join(source.Path, fileName1); + File.Create(filePath1).Dispose(); + + string subDirectoryName = "dir/"; // The trailing separator is preserved in the TarEntry.Name + string subDirectoryPath = Path.Join(source.Path, subDirectoryName); + Directory.CreateDirectory(subDirectoryPath); + + string fileName2 = "file2.txt"; + string filePath2 = Path.Join(subDirectoryPath, fileName2); + File.Create(filePath2).Dispose(); + + string destinationArchiveFileName = Path.Join(destination.Path, "output.tar"); + TarFile.CreateFromDirectory(source.Path, destinationArchiveFileName, includeBaseDirectory); + + List entries = new List(); + + FileStreamOptions readOptions = new() + { + Access = FileAccess.Read, + Mode = FileMode.Open, + Options = FileOptions.Asynchronous, + }; + FileStream fileStream = File.Open(destinationArchiveFileName, readOptions); + await using (fileStream) + { + TarReader reader = new TarReader(fileStream); + await using (reader) + { + TarEntry entry; + while ((entry = await reader.GetNextEntryAsync()) != null) + { + entries.Add(entry); + } + } + } + + Assert.Equal(3, entries.Count); + + string prefix = includeBaseDirectory ? Path.GetFileName(source.Path) + '/' : string.Empty; + + TarEntry entry1 = entries.FirstOrDefault(x => + x.EntryType == TarEntryType.RegularFile && + x.Name == prefix + fileName1); + Assert.NotNull(entry1); + + TarEntry directory = entries.FirstOrDefault(x => + x.EntryType == TarEntryType.Directory && + x.Name == prefix + subDirectoryName); + Assert.NotNull(directory); + + string actualFileName2 = subDirectoryName + fileName2; // Notice the trailing separator in subDirectoryName + TarEntry entry2 = entries.FirstOrDefault(x => + x.EntryType == TarEntryType.RegularFile && + x.Name == prefix + actualFileName2); + Assert.NotNull(entry2); + } + + [Fact] + public async Task IncludeBaseDirectoryIfEmpty_Async() + { + using TempDirectory source = new TempDirectory(); + using TempDirectory destination = new TempDirectory(); + + string destinationArchiveFileName = Path.Join(destination.Path, "output.tar"); + + await TarFile.CreateFromDirectoryAsync(source.Path, destinationArchiveFileName, includeBaseDirectory: true); + + FileStreamOptions readOptions = new() + { + Access = FileAccess.Read, + Mode = FileMode.Open, + Options = FileOptions.Asynchronous, + }; + FileStream fileStream = File.Open(destinationArchiveFileName, readOptions); + await using (fileStream) + { + TarReader reader = new TarReader(fileStream); + await using (reader) + { + TarEntry entry = await reader.GetNextEntryAsync(); + Assert.NotNull(entry); + Assert.Equal(TarEntryType.Directory, entry.EntryType); + Assert.Equal(Path.GetFileName(source.Path) + '/', entry.Name); + + Assert.Null(await reader.GetNextEntryAsync()); + } + } + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task IncludeAllSegmentsOfPath_Async(bool includeBaseDirectory) + { + using TempDirectory source = new TempDirectory(); + using TempDirectory destination = new TempDirectory(); + + string segment1 = Path.Join(source.Path, "segment1"); + Directory.CreateDirectory(segment1); + string segment2 = Path.Join(segment1, "segment2"); + Directory.CreateDirectory(segment2); + string textFile = Path.Join(segment2, "file.txt"); + File.Create(textFile).Dispose(); + + string destinationArchiveFileName = Path.Join(destination.Path, "output.tar"); + + await TarFile.CreateFromDirectoryAsync(source.Path, destinationArchiveFileName, includeBaseDirectory); + + FileStreamOptions readOptions = new() + { + Access = FileAccess.Read, + Mode = FileMode.Open, + Options = FileOptions.Asynchronous, + }; + FileStream fileStream = File.Open(destinationArchiveFileName, readOptions); + await using (fileStream) + { + TarReader reader = new TarReader(fileStream); + await using (reader) + { + string prefix = includeBaseDirectory ? Path.GetFileName(source.Path) + '/' : string.Empty; + + TarEntry entry = await reader.GetNextEntryAsync(); + Assert.NotNull(entry); + Assert.Equal(TarEntryType.Directory, entry.EntryType); + Assert.Equal(prefix + "segment1/", entry.Name); + + entry = await reader.GetNextEntryAsync(); + Assert.NotNull(entry); + Assert.Equal(TarEntryType.Directory, entry.EntryType); + Assert.Equal(prefix + "segment1/segment2/", entry.Name); + + entry = await reader.GetNextEntryAsync(); + Assert.NotNull(entry); + Assert.Equal(TarEntryType.RegularFile, entry.EntryType); + Assert.Equal(prefix + "segment1/segment2/file.txt", entry.Name); + + Assert.Null(await reader.GetNextEntryAsync()); + } + } + } + } +} From bf86f3772abd626ecd0bab11fae57c8eb9768fd5 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Fri, 24 Jun 2022 20:34:08 -0700 Subject: [PATCH 39/75] Add async tests for TarFile.CreateFromDirectoryAsync for streams. --- .../tests/System.Formats.Tar.Tests.csproj | 1 + ...e.CreateFromDirectoryAsync.Stream.Tests.cs | 45 +++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 src/libraries/System.Formats.Tar/tests/TarFile/TarFile.CreateFromDirectoryAsync.Stream.Tests.cs diff --git a/src/libraries/System.Formats.Tar/tests/System.Formats.Tar.Tests.csproj b/src/libraries/System.Formats.Tar/tests/System.Formats.Tar.Tests.csproj index d99e529a37fa14..271b5f5a1b4e10 100644 --- a/src/libraries/System.Formats.Tar/tests/System.Formats.Tar.Tests.csproj +++ b/src/libraries/System.Formats.Tar/tests/System.Formats.Tar.Tests.csproj @@ -10,6 +10,7 @@ + diff --git a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.CreateFromDirectoryAsync.Stream.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.CreateFromDirectoryAsync.Stream.Tests.cs new file mode 100644 index 00000000000000..550c603bbbb8b0 --- /dev/null +++ b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.CreateFromDirectoryAsync.Stream.Tests.cs @@ -0,0 +1,45 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; +using System.Threading.Tasks; +using Xunit; + +namespace System.Formats.Tar.Tests +{ + public class TarFile_CreateFromDirectoryAsync_Stream_Tests : TarTestsBase + { + [Fact] + public async Task InvalidPath_Throws_Async() + { + using MemoryStream archive = new MemoryStream(); + await Assert.ThrowsAsync(async () => await TarFile.CreateFromDirectoryAsync(sourceDirectoryName: null,destination: archive, includeBaseDirectory: false)); + await Assert.ThrowsAsync(async () => await TarFile.CreateFromDirectoryAsync(sourceDirectoryName: string.Empty,destination: archive, includeBaseDirectory: false)); + } + + [Fact] + public async Task NullStream_Throws_Async() + { + using MemoryStream archive = new MemoryStream(); + await Assert.ThrowsAsync(async () => await TarFile.CreateFromDirectoryAsync(sourceDirectoryName: "path",destination: null, includeBaseDirectory: false)); + } + + [Fact] + public async Task UnwritableStream_Throws_Async() + { + using MemoryStream archive = new MemoryStream(); + using WrappedStream unwritable = new WrappedStream(archive, canRead: true, canWrite: false, canSeek: true); + await Assert.ThrowsAsync(async () => await TarFile.CreateFromDirectoryAsync(sourceDirectoryName: "path",destination: unwritable, includeBaseDirectory: false)); + } + + [Fact] + public async Task NonExistentDirectory_Throws_Async() + { + using TempDirectory root = new TempDirectory(); + string dirPath = Path.Join(root.Path, "dir"); + + using MemoryStream archive = new MemoryStream(); + await Assert.ThrowsAsync(async () => await TarFile.CreateFromDirectoryAsync(sourceDirectoryName: dirPath, destination: archive, includeBaseDirectory: false)); + } + } +} From b750e1ba6ff78686426b7c51af1fc911bd74904f Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Fri, 24 Jun 2022 20:37:39 -0700 Subject: [PATCH 40/75] Add async tests for TarFile.ExtractToDirectoryAsync for file assets. --- .../tests/System.Formats.Tar.Tests.csproj | 1 + ...File.ExtractToDirectoryAsync.File.Tests.cs | 166 ++++++++++++++++++ 2 files changed, 167 insertions(+) create mode 100644 src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectoryAsync.File.Tests.cs diff --git a/src/libraries/System.Formats.Tar/tests/System.Formats.Tar.Tests.csproj b/src/libraries/System.Formats.Tar/tests/System.Formats.Tar.Tests.csproj index 271b5f5a1b4e10..e617098e2ac4ff 100644 --- a/src/libraries/System.Formats.Tar/tests/System.Formats.Tar.Tests.csproj +++ b/src/libraries/System.Formats.Tar/tests/System.Formats.Tar.Tests.csproj @@ -10,6 +10,7 @@ + diff --git a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectoryAsync.File.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectoryAsync.File.Tests.cs new file mode 100644 index 00000000000000..9af29ea027878a --- /dev/null +++ b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectoryAsync.File.Tests.cs @@ -0,0 +1,166 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Xunit; + +namespace System.Formats.Tar.Tests +{ + public partial class TarFile_ExtractToDirectoryAsync_File_Tests : TarTestsBase + { + [Fact] + public async Task InvalidPaths_Throw() + { + await Assert.ThrowsAsync(async () => await TarFile.ExtractToDirectoryAsync(sourceFileName: null, destinationDirectoryName: "path", overwriteFiles: false)); + await Assert.ThrowsAsync(async () => await TarFile.ExtractToDirectoryAsync(sourceFileName: string.Empty, destinationDirectoryName: "path", overwriteFiles: false)); + await Assert.ThrowsAsync(async () => await TarFile.ExtractToDirectoryAsync(sourceFileName: "path", destinationDirectoryName: null, overwriteFiles: false)); + await Assert.ThrowsAsync(async () => await TarFile.ExtractToDirectoryAsync(sourceFileName: "path", destinationDirectoryName: string.Empty, overwriteFiles: false)); + } + + [Fact] + public async Task NonExistentFile_Throws_Async() + { + using TempDirectory root = new TempDirectory(); + + string filePath = Path.Join(root.Path, "file.tar"); + string dirPath = Path.Join(root.Path, "dir"); + + Directory.CreateDirectory(dirPath); + + await Assert.ThrowsAsync(async () => await TarFile.ExtractToDirectoryAsync(sourceFileName: filePath, destinationDirectoryName: dirPath, overwriteFiles: false)); + } + + [Fact] + public async Task NonExistentDirectory_Throws_Async() + { + using TempDirectory root = new TempDirectory(); + + string filePath = Path.Join(root.Path, "file.tar"); + string dirPath = Path.Join(root.Path, "dir"); + + File.Create(filePath).Dispose(); + + await Assert.ThrowsAsync(async () => await TarFile.ExtractToDirectoryAsync(sourceFileName: filePath, destinationDirectoryName: dirPath, overwriteFiles: false)); + } + + [Theory] + [InlineData(TestTarFormat.v7)] + [InlineData(TestTarFormat.ustar)] + [InlineData(TestTarFormat.pax)] + [InlineData(TestTarFormat.pax_gea)] + [InlineData(TestTarFormat.gnu)] + [InlineData(TestTarFormat.oldgnu)] + public async Task Extract_Archive_File_Async(TestTarFormat testFormat) + { + string sourceArchiveFileName = GetTarFilePath(CompressionMethod.Uncompressed, testFormat, "file"); + + using TempDirectory destination = new TempDirectory(); + + string filePath = Path.Join(destination.Path, "file.txt"); + + await TarFile.ExtractToDirectoryAsync(sourceArchiveFileName, destination.Path, overwriteFiles: false); + + Assert.True(File.Exists(filePath)); + } + + [Fact] + public async Task Extract_Archive_File_OverwriteTrue_Async() + { + string testCaseName = "file"; + string archivePath = GetTarFilePath(CompressionMethod.Uncompressed, TestTarFormat.pax, testCaseName); + + using TempDirectory destination = new TempDirectory(); + + string filePath = Path.Join(destination.Path, "file.txt"); + using (FileStream fileStream = File.Create(filePath)) + { + using StreamWriter writer = new StreamWriter(fileStream, leaveOpen: false); + writer.WriteLine("Original text"); + } + + await TarFile.ExtractToDirectoryAsync(archivePath, destination.Path, overwriteFiles: true); + + Assert.True(File.Exists(filePath)); + + using (FileStream fileStream = File.Open(filePath, FileMode.Open)) + { + using StreamReader reader = new StreamReader(fileStream); + string actualContents = reader.ReadLine(); + Assert.Equal($"Hello {testCaseName}", actualContents); // Confirm overwrite + } + } + + [Fact] + public async Task Extract_Archive_File_OverwriteFalse_Async() + { + string sourceArchiveFileName = GetTarFilePath(CompressionMethod.Uncompressed, TestTarFormat.pax, "file"); + + using TempDirectory destination = new TempDirectory(); + + string filePath = Path.Join(destination.Path, "file.txt"); + + File.Create(filePath).Dispose(); + + await Assert.ThrowsAsync(async () => await TarFile.ExtractToDirectoryAsync(sourceArchiveFileName, destination.Path, overwriteFiles: false)); + } + + [Fact] + public async Task Extract_AllSegmentsOfPath_Async() + { + using TempDirectory source = new TempDirectory(); + using TempDirectory destination = new TempDirectory(); + + string archivePath = Path.Join(source.Path, "archive.tar"); + using FileStream archiveStream = File.Create(archivePath); + using (TarWriter writer = new TarWriter(archiveStream)) + { + PaxTarEntry segment1 = new PaxTarEntry(TarEntryType.Directory, "segment1"); + writer.WriteEntry(segment1); + + PaxTarEntry segment2 = new PaxTarEntry(TarEntryType.Directory, "segment1/segment2"); + writer.WriteEntry(segment2); + + PaxTarEntry file = new PaxTarEntry(TarEntryType.RegularFile, "segment1/segment2/file.txt"); + writer.WriteEntry(file); + } + + await TarFile.ExtractToDirectoryAsync(archivePath, destination.Path, overwriteFiles: false); + + string segment1Path = Path.Join(destination.Path, "segment1"); + Assert.True(Directory.Exists(segment1Path), $"{segment1Path}' does not exist."); + + string segment2Path = Path.Join(segment1Path, "segment2"); + Assert.True(Directory.Exists(segment2Path), $"{segment2Path}' does not exist."); + + string filePath = Path.Join(segment2Path, "file.txt"); + Assert.True(File.Exists(filePath), $"{filePath}' does not exist."); + } + + [Fact] + public async Task ExtractArchiveWithEntriesThatStartWithSlashDotPrefix_Async() + { + using TempDirectory root = new TempDirectory(); + + using MemoryStream archiveStream = GetStrangeTarMemoryStream("prefixDotSlashAndCurrentFolderEntry"); + + await TarFile.ExtractToDirectoryAsync(archiveStream, root.Path, overwriteFiles: true); + + archiveStream.Position = 0; + + TarReader reader = new TarReader(archiveStream, leaveOpen: false); + await using (reader) + { + TarEntry entry; + while ((entry = await reader.GetNextEntryAsync()) != null) + { + // Normalize the path (remove redundant segments), remove trailing separators + // this is so the first entry can be skipped if it's the same as the root directory + string entryPath = Path.TrimEndingDirectorySeparator(Path.GetFullPath(Path.Join(root.Path, entry.Name))); + Assert.True(Path.Exists(entryPath), $"Entry was not extracted: {entryPath}"); + } + } + } + } +} From aa3eb3e5100a89de22f26cbd8876191e8b4439c8 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Fri, 24 Jun 2022 20:42:40 -0700 Subject: [PATCH 41/75] Add async tests for TarFile.ExtractToDirectoryAsync for streams. --- .../tests/System.Formats.Tar.Tests.csproj | 1 + ...le.ExtractToDirectoryAsync.Stream.Tests.cs | 153 ++++++++++++++++++ 2 files changed, 154 insertions(+) create mode 100644 src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectoryAsync.Stream.Tests.cs diff --git a/src/libraries/System.Formats.Tar/tests/System.Formats.Tar.Tests.csproj b/src/libraries/System.Formats.Tar/tests/System.Formats.Tar.Tests.csproj index e617098e2ac4ff..472d173a55a2c2 100644 --- a/src/libraries/System.Formats.Tar/tests/System.Formats.Tar.Tests.csproj +++ b/src/libraries/System.Formats.Tar/tests/System.Formats.Tar.Tests.csproj @@ -10,6 +10,7 @@ + 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 new file mode 100644 index 00000000000000..fb7f6cd8b15eeb --- /dev/null +++ b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectoryAsync.Stream.Tests.cs @@ -0,0 +1,153 @@ +// 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.Linq; +using System.Threading.Tasks; +using Xunit; + +namespace System.Formats.Tar.Tests +{ + public class TarFile_ExtractToDirectoryAsync_Stream_Tests : TarTestsBase + { + [Fact] + public async Task NullStream_Throws_Async() + { + await Assert.ThrowsAsync(async () => await TarFile.ExtractToDirectoryAsync(source: null, destinationDirectoryName: "path", overwriteFiles: false)); + } + + [Fact] + public async Task InvalidPath_Throws_Async() + { + using MemoryStream archive = new MemoryStream(); + await Assert.ThrowsAsync(async () => await TarFile.ExtractToDirectoryAsync(archive, destinationDirectoryName: null, overwriteFiles: false)); + await Assert.ThrowsAsync(async () => await TarFile.ExtractToDirectoryAsync(archive, destinationDirectoryName: string.Empty, overwriteFiles: false)); + } + + [Fact] + public async Task UnreadableStream_Throws_Async() + { + using MemoryStream archive = new MemoryStream(); + using WrappedStream unreadable = new WrappedStream(archive, canRead: false, canWrite: true, canSeek: true); + await Assert.ThrowsAsync(async () => await TarFile.ExtractToDirectoryAsync(unreadable, destinationDirectoryName: "path", overwriteFiles: false)); + } + + [Fact] + public async Task NonExistentDirectory_Throws_Async() + { + using TempDirectory root = new TempDirectory(); + string dirPath = Path.Join(root.Path, "dir"); + + using MemoryStream archive = new MemoryStream(); + await Assert.ThrowsAsync(async () => await TarFile.ExtractToDirectoryAsync(archive, destinationDirectoryName: dirPath, overwriteFiles: false)); + } + + [Fact] + public async Task ExtractEntry_ManySubfolderSegments_NoPrecedingDirectoryEntries_Async() + { + using TempDirectory root = new TempDirectory(); + + string firstSegment = "a"; + string secondSegment = Path.Join(firstSegment, "b"); + string fileWithTwoSegments = Path.Join(secondSegment, "c.txt"); + + using MemoryStream archive = new MemoryStream(); + TarWriter writer = new TarWriter(archive, TarEntryFormat.Ustar, leaveOpen: true); + await using (writer) + { + // No preceding directory entries for the segments + UstarTarEntry entry = new UstarTarEntry(TarEntryType.RegularFile, fileWithTwoSegments); + + entry.DataStream = new MemoryStream(); + entry.DataStream.Write(new byte[] { 0x1 }); + entry.DataStream.Seek(0, SeekOrigin.Begin); + + await writer.WriteEntryAsync(entry); + } + + archive.Seek(0, SeekOrigin.Begin); + await TarFile.ExtractToDirectoryAsync(archive, root.Path, overwriteFiles: false); + + Assert.True(Directory.Exists(Path.Join(root.Path, firstSegment))); + Assert.True(Directory.Exists(Path.Join(root.Path, secondSegment))); + Assert.True(File.Exists(Path.Join(root.Path, fileWithTwoSegments))); + } + + [Theory] + [InlineData(TarEntryType.SymbolicLink)] + [InlineData(TarEntryType.HardLink)] + public async Task Extract_LinkEntry_TargetOutsideDirectory_Async(TarEntryType entryType) + { + using MemoryStream archive = new MemoryStream(); + TarWriter writer = new TarWriter(archive, TarEntryFormat.Ustar, leaveOpen: true); + await using (writer) + { + UstarTarEntry entry = new UstarTarEntry(entryType, "link"); + entry.LinkName = PlatformDetection.IsWindows ? @"C:\Windows\System32\notepad.exe" : "/usr/bin/nano"; + await writer.WriteEntryAsync(entry); + } + + archive.Seek(0, SeekOrigin.Begin); + + using TempDirectory root = new TempDirectory(); + + await Assert.ThrowsAsync(async () => await TarFile.ExtractToDirectoryAsync(archive, root.Path, overwriteFiles: false)); + + Assert.Equal(0, Directory.GetFileSystemEntries(root.Path).Count()); + } + + [ConditionalTheory(typeof(MountHelper), nameof(MountHelper.CanCreateSymbolicLinks))] + [InlineData(TarEntryFormat.Pax)] + [InlineData(TarEntryFormat.Gnu)] + public Task Extract_SymbolicLinkEntry_TargetInsideDirectory_Async(TarEntryFormat format) => Extract_LinkEntry_TargetInsideDirectory_Internal_Async(TarEntryType.SymbolicLink, format, null); + + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.SupportsHardLinkCreation))] + [InlineData(TarEntryFormat.Pax)] + [InlineData(TarEntryFormat.Gnu)] + public Task Extract_HardLinkEntry_TargetInsideDirectory_Async(TarEntryFormat format) => Extract_LinkEntry_TargetInsideDirectory_Internal_Async(TarEntryType.HardLink, format, null); + + [ConditionalTheory(typeof(MountHelper), nameof(MountHelper.CanCreateSymbolicLinks))] + [InlineData(TarEntryFormat.Pax)] + [InlineData(TarEntryFormat.Gnu)] + public Task Extract_SymbolicLinkEntry_TargetInsideDirectory_LongBaseDir_Async(TarEntryFormat format) => Extract_LinkEntry_TargetInsideDirectory_Internal_Async(TarEntryType.SymbolicLink, format, new string('a', 99)); + + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.SupportsHardLinkCreation))] + [InlineData(TarEntryFormat.Pax)] + [InlineData(TarEntryFormat.Gnu)] + public Task Extract_HardLinkEntry_TargetInsideDirectory_LongBaseDir_Async(TarEntryFormat format) => Extract_LinkEntry_TargetInsideDirectory_Internal_Async(TarEntryType.HardLink, format, new string('a', 99)); + + // This test would not pass for the V7 and Ustar formats in some OSs like MacCatalyst, tvOSSimulator and OSX, because the TempDirectory gets created in + // a folder with a path longer than 100 bytes, and those tar formats have no way of handling pathnames and linknames longer than that length. + // The rest of the OSs create the TempDirectory in a path that does not surpass the 100 bytes, so the 'subfolder' parameter gives a chance to extend + // the base directory past that length, to ensure this scenario is tested everywhere. + private async Task Extract_LinkEntry_TargetInsideDirectory_Internal_Async(TarEntryType entryType, TarEntryFormat format, string subfolder) + { + using TempDirectory root = new TempDirectory(); + + string baseDir = string.IsNullOrEmpty(subfolder) ? root.Path : Path.Join(root.Path, subfolder); + Directory.CreateDirectory(baseDir); + + string linkName = "link"; + string targetName = "target"; + string targetPath = Path.Join(baseDir, targetName); + + File.Create(targetPath).Dispose(); + + using MemoryStream archive = new MemoryStream(); + TarWriter writer = new TarWriter(archive, format, leaveOpen: true); + await using (writer) + { + TarEntry entry = InvokeTarEntryCreationConstructor(format, entryType, linkName); + entry.LinkName = targetPath; + await writer.WriteEntryAsync(entry); + } + + archive.Seek(0, SeekOrigin.Begin); + + await TarFile.ExtractToDirectoryAsync(archive, baseDir, overwriteFiles: false); + + Assert.Equal(2, Directory.GetFileSystemEntries(baseDir).Count()); + } + } +} From 1074f11ddf066a5ddc24ae795ce6b5578eaa86a6 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Fri, 24 Jun 2022 20:47:08 -0700 Subject: [PATCH 42/75] Add tests for TarFile.ExtractToDirectoryAsync platform specific. --- .../tests/System.Formats.Tar.Tests.csproj | 2 ++ ...File.ExtractToDirectory.File.Tests.Unix.cs | 3 +- ...e.ExtractToDirectory.File.Tests.Windows.cs | 3 +- ...ExtractToDirectoryAsync.File.Tests.Unix.cs | 31 ++++++++++++++++++ ...ractToDirectoryAsync.File.Tests.Windows.cs | 32 +++++++++++++++++++ 5 files changed, 67 insertions(+), 4 deletions(-) create mode 100644 src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectoryAsync.File.Tests.Unix.cs create mode 100644 src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectoryAsync.File.Tests.Windows.cs diff --git a/src/libraries/System.Formats.Tar/tests/System.Formats.Tar.Tests.csproj b/src/libraries/System.Formats.Tar/tests/System.Formats.Tar.Tests.csproj index 472d173a55a2c2..19a13b37b390d2 100644 --- a/src/libraries/System.Formats.Tar/tests/System.Formats.Tar.Tests.csproj +++ b/src/libraries/System.Formats.Tar/tests/System.Formats.Tar.Tests.csproj @@ -10,6 +10,7 @@ + @@ -71,6 +72,7 @@ + diff --git a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectory.File.Tests.Unix.cs b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectory.File.Tests.Unix.cs index adc1b09a23670d..521799e621ddb3 100644 --- a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectory.File.Tests.Unix.cs +++ b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectory.File.Tests.Unix.cs @@ -9,7 +9,6 @@ namespace System.Formats.Tar.Tests { public partial class TarFile_ExtractToDirectory_File_Tests : TarTestsBase { - [Fact] public void Extract_SpecialFiles_Unix_Unelevated_ThrowsUnauthorizedAccess() { @@ -29,4 +28,4 @@ public void Extract_SpecialFiles_Unix_Unelevated_ThrowsUnauthorizedAccess() Assert.Equal(0, Directory.GetFileSystemEntries(destination).Count()); } } -} \ No newline at end of file +} diff --git a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectory.File.Tests.Windows.cs b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectory.File.Tests.Windows.cs index 9976bb4b7bd00e..19d5dc19627db2 100644 --- a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectory.File.Tests.Windows.cs +++ b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectory.File.Tests.Windows.cs @@ -9,7 +9,6 @@ namespace System.Formats.Tar.Tests { public partial class TarFile_ExtractToDirectory_File_Tests : TarTestsBase { - [Fact] public void Extract_SpecialFiles_Windows_ThrowsInvalidOperation() { @@ -29,4 +28,4 @@ public void Extract_SpecialFiles_Windows_ThrowsInvalidOperation() Assert.Equal(0, Directory.GetFileSystemEntries(destination).Count()); } } -} \ No newline at end of file +} diff --git a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectoryAsync.File.Tests.Unix.cs b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectoryAsync.File.Tests.Unix.cs new file mode 100644 index 00000000000000..bef773566d0975 --- /dev/null +++ b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectoryAsync.File.Tests.Unix.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; +using System.Linq; +using Xunit; + +namespace System.Formats.Tar.Tests +{ + public partial class TarFile_ExtractToDirectoryAsync_File_Tests : TarTestsBase + { + [Fact] + public async Task Extract_SpecialFiles_Unix_Unelevated_ThrowsUnauthorizedAccess_Async() + { + string originalFileName = GetTarFilePath(CompressionMethod.Uncompressed, TestTarFormat.ustar, "specialfiles"); + using TempDirectory root = new TempDirectory(); + + string archive = Path.Join(root.Path, "input.tar"); + string destination = Path.Join(root.Path, "dir"); + + // Copying the tar to reduce the chance of other tests failing due to being used by another process + File.Copy(originalFileName, archive); + + Directory.CreateDirectory(destination); + + await Assert.ThrowsAsync(async () => await TarFile.ExtractToDirectoryAsync(archive, destination, overwriteFiles: false)); + + Assert.Equal(0, Directory.GetFileSystemEntries(destination).Count()); + } + } +} diff --git a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectoryAsync.File.Tests.Windows.cs b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectoryAsync.File.Tests.Windows.cs new file mode 100644 index 00000000000000..218d6cf17853f0 --- /dev/null +++ b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectoryAsync.File.Tests.Windows.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Xunit; + +namespace System.Formats.Tar.Tests +{ + public partial class TarFile_ExtractToDirectoryAsync_File_Tests : TarTestsBase + { + [Fact] + public async Task Extract_SpecialFiles_Windows_ThrowsInvalidOperation_Async() + { + string originalFileName = GetTarFilePath(CompressionMethod.Uncompressed, TestTarFormat.ustar, "specialfiles"); + using TempDirectory root = new TempDirectory(); + + string archive = Path.Join(root.Path, "input.tar"); + string destination = Path.Join(root.Path, "dir"); + + // Copying the tar to reduce the chance of other tests failing due to being used by another process + File.Copy(originalFileName, archive); + + Directory.CreateDirectory(destination); + + await Assert.ThrowsAsync(async () => await TarFile.ExtractToDirectoryAsync(archive, destination, overwriteFiles: false)); + + Assert.Equal(0, Directory.GetFileSystemEntries(destination).Count()); + } + } +} From d69965b9c751809d2c8963118054145f293510fb Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Fri, 24 Jun 2022 21:15:46 -0700 Subject: [PATCH 43/75] Add TarEntry.ExtractToFileAsync tests --- .../tests/System.Formats.Tar.Tests.csproj | 2 + .../TarEntry.Conversion.Tests.Base.cs | 33 ------- .../TarEntry/TarEntry.ExtractToFile.Tests.cs | 96 ++++++++++++++++++ .../TarEntry.ExtractToFileAsync.Tests.cs | 97 +++++++++++++++++++ .../tests/TarEntry/TarEntryGnu.Tests.cs | 73 -------------- .../tests/TarEntry/TarEntryPax.Tests.cs | 73 -------------- .../tests/TarEntry/TarEntryUstar.Tests.cs | 73 -------------- .../tests/TarEntry/TarEntryV7.Tests.cs | 73 -------------- .../System.Formats.Tar/tests/TarTestsBase.cs | 34 +++++++ 9 files changed, 229 insertions(+), 325 deletions(-) create mode 100644 src/libraries/System.Formats.Tar/tests/TarEntry/TarEntry.ExtractToFile.Tests.cs create mode 100644 src/libraries/System.Formats.Tar/tests/TarEntry/TarEntry.ExtractToFileAsync.Tests.cs diff --git a/src/libraries/System.Formats.Tar/tests/System.Formats.Tar.Tests.csproj b/src/libraries/System.Formats.Tar/tests/System.Formats.Tar.Tests.csproj index 19a13b37b390d2..701d0d9102d1e9 100644 --- a/src/libraries/System.Formats.Tar/tests/System.Formats.Tar.Tests.csproj +++ b/src/libraries/System.Formats.Tar/tests/System.Formats.Tar.Tests.csproj @@ -10,6 +10,8 @@ + + diff --git a/src/libraries/System.Formats.Tar/tests/TarEntry/TarEntry.Conversion.Tests.Base.cs b/src/libraries/System.Formats.Tar/tests/TarEntry/TarEntry.Conversion.Tests.Base.cs index 846a2f7a1904ee..6626ca958f25d4 100644 --- a/src/libraries/System.Formats.Tar/tests/TarEntry/TarEntry.Conversion.Tests.Base.cs +++ b/src/libraries/System.Formats.Tar/tests/TarEntry/TarEntry.Conversion.Tests.Base.cs @@ -186,38 +186,5 @@ protected TarEntry InvokeTarEntryConversionConstructor(TarEntryFormat targetForm TarEntryFormat.Gnu => new GnuTarEntry(other), _ => throw new FormatException($"Unexpected format: {targetFormat}") }; - - protected TarEntryType GetTarEntryTypeForTarEntryFormat(TarEntryType entryType, TarEntryFormat format) - { - if (format is TarEntryFormat.V7) - { - if (entryType is TarEntryType.RegularFile) - { - return TarEntryType.V7RegularFile; - } - } - else - { - if (entryType is TarEntryType.V7RegularFile) - { - return TarEntryType.RegularFile; - } - } - return entryType; - } - - protected void CheckConversionType(TarEntry entry, TarEntryFormat expectedFormat) - { - Type expectedType = expectedFormat switch - { - TarEntryFormat.V7 => typeof(V7TarEntry), - TarEntryFormat.Ustar => typeof(UstarTarEntry), - TarEntryFormat.Pax => typeof(PaxTarEntry), - TarEntryFormat.Gnu => typeof(GnuTarEntry), - _ => throw new FormatException($"Unexpected format {expectedFormat}") - }; - - Assert.Equal(expectedType, entry.GetType()); - } } } diff --git a/src/libraries/System.Formats.Tar/tests/TarEntry/TarEntry.ExtractToFile.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarEntry/TarEntry.ExtractToFile.Tests.cs new file mode 100644 index 00000000000000..93ba1559d2a7ce --- /dev/null +++ b/src/libraries/System.Formats.Tar/tests/TarEntry/TarEntry.ExtractToFile.Tests.cs @@ -0,0 +1,96 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; +using System.Linq; +using Xunit; + +namespace System.Formats.Tar.Tests +{ + public class TarEntry_ExtractToFile_Tests : TarTestsBase + { + [Theory] + [InlineData(TarEntryFormat.V7)] + [InlineData(TarEntryFormat.Ustar)] + [InlineData(TarEntryFormat.Pax)] + [InlineData(TarEntryFormat.Gnu)] + public void Constructor_Name_FullPath_DestinationDirectory_Mismatch_Throws(TarEntryFormat format) + { + using TempDirectory root = new TempDirectory(); + + string fullPath = Path.Join(Path.GetPathRoot(root.Path), "dir", "file.txt"); + + TarEntry entry = InvokeTarEntryCreationConstructor(format, GetTarEntryTypeForTarEntryFormat(TarEntryType.RegularFile, format), fullPath); + + entry.DataStream = new MemoryStream(); + entry.DataStream.Write(new byte[] { 0x1 }); + entry.DataStream.Seek(0, SeekOrigin.Begin); + + Assert.Throws(() => entry.ExtractToFile(root.Path, overwrite: false)); + + Assert.False(File.Exists(fullPath)); + } + + [Theory] + [InlineData(TarEntryFormat.V7)] + [InlineData(TarEntryFormat.Ustar)] + [InlineData(TarEntryFormat.Pax)] + [InlineData(TarEntryFormat.Gnu)] + public void Constructor_Name_FullPath_DestinationDirectory_Match_AdditionalSubdirectory_Throws(TarEntryFormat format) + { + using TempDirectory root = new TempDirectory(); + + string fullPath = Path.Join(root.Path, "dir", "file.txt"); + + TarEntry entry = InvokeTarEntryCreationConstructor(format, GetTarEntryTypeForTarEntryFormat(TarEntryType.RegularFile, format), fullPath); + + entry.DataStream = new MemoryStream(); + entry.DataStream.Write(new byte[] { 0x1 }); + entry.DataStream.Seek(0, SeekOrigin.Begin); + + Assert.Throws(() => entry.ExtractToFile(root.Path, overwrite: false)); + + Assert.False(File.Exists(fullPath)); + } + + [Theory] + [InlineData(TarEntryFormat.V7)] + [InlineData(TarEntryFormat.Ustar)] + [InlineData(TarEntryFormat.Pax)] + [InlineData(TarEntryFormat.Gnu)] + public void Constructor_Name_FullPath_DestinationDirectory_Match(TarEntryFormat format) + { + using TempDirectory root = new TempDirectory(); + + string fullPath = Path.Join(root.Path, "file.txt"); + + TarEntry entry = InvokeTarEntryCreationConstructor(format, GetTarEntryTypeForTarEntryFormat(TarEntryType.RegularFile, format), fullPath); + + entry.DataStream = new MemoryStream(); + entry.DataStream.Write(new byte[] { 0x1 }); + entry.DataStream.Seek(0, SeekOrigin.Begin); + + entry.ExtractToFile(fullPath, overwrite: false); + + Assert.True(File.Exists(fullPath)); + } + + [Theory] + [MemberData(nameof(GetFormatsAndLinks))] + public void ExtractToFile_Link_Throws(TarEntryFormat format, TarEntryType entryType) + { + using TempDirectory root = new TempDirectory(); + string fileName = "mylink"; + string fullPath = Path.Join(root.Path, fileName); + + string linkTarget = PlatformDetection.IsWindows ? @"C:\Windows\system32\notepad.exe" : "/usr/bin/nano"; + + TarEntry entry = InvokeTarEntryCreationConstructor(format, entryType, fileName); + entry.LinkName = linkTarget; + + Assert.Throws(() => entry.ExtractToFile(fileName, overwrite: false)); + + Assert.Equal(0, Directory.GetFileSystemEntries(root.Path).Count()); + } + } +} diff --git a/src/libraries/System.Formats.Tar/tests/TarEntry/TarEntry.ExtractToFileAsync.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarEntry/TarEntry.ExtractToFileAsync.Tests.cs new file mode 100644 index 00000000000000..e25fcab5573b35 --- /dev/null +++ b/src/libraries/System.Formats.Tar/tests/TarEntry/TarEntry.ExtractToFileAsync.Tests.cs @@ -0,0 +1,97 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Xunit; + +namespace System.Formats.Tar.Tests +{ + public class TarEntry_ExtractToFileAsync_Tests : TarTestsBase + { + [Theory] + [InlineData(TarEntryFormat.V7)] + [InlineData(TarEntryFormat.Ustar)] + [InlineData(TarEntryFormat.Pax)] + [InlineData(TarEntryFormat.Gnu)] + public async Task Constructor_Name_FullPath_DestinationDirectory_Mismatch_Throws_Async(TarEntryFormat format) + { + using TempDirectory root = new TempDirectory(); + + string fullPath = Path.Join(Path.GetPathRoot(root.Path), "dir", "file.txt"); + + TarEntry entry = InvokeTarEntryCreationConstructor(format, GetTarEntryTypeForTarEntryFormat(TarEntryType.RegularFile, format), fullPath); + + entry.DataStream = new MemoryStream(); + entry.DataStream.Write(new byte[] { 0x1 }); + entry.DataStream.Seek(0, SeekOrigin.Begin); + + await Assert.ThrowsAsync(async () => await entry.ExtractToFileAsync(root.Path, overwrite: false)); + + Assert.False(File.Exists(fullPath)); + } + + [Theory] + [InlineData(TarEntryFormat.V7)] + [InlineData(TarEntryFormat.Ustar)] + [InlineData(TarEntryFormat.Pax)] + [InlineData(TarEntryFormat.Gnu)] + public async Task Constructor_Name_FullPath_DestinationDirectory_Match_AdditionalSubdirectory_Throws_Async(TarEntryFormat format) + { + using TempDirectory root = new TempDirectory(); + + string fullPath = Path.Join(root.Path, "dir", "file.txt"); + + TarEntry entry = InvokeTarEntryCreationConstructor(format, GetTarEntryTypeForTarEntryFormat(TarEntryType.RegularFile, format), fullPath); + + entry.DataStream = new MemoryStream(); + entry.DataStream.Write(new byte[] { 0x1 }); + entry.DataStream.Seek(0, SeekOrigin.Begin); + + await Assert.ThrowsAsync(async () => await entry.ExtractToFileAsync(root.Path, overwrite: false)); + + Assert.False(File.Exists(fullPath)); + } + + [Theory] + [InlineData(TarEntryFormat.V7)] + [InlineData(TarEntryFormat.Ustar)] + [InlineData(TarEntryFormat.Pax)] + [InlineData(TarEntryFormat.Gnu)] + public async Task Constructor_Name_FullPath_DestinationDirectory_Match_Async(TarEntryFormat format) + { + using TempDirectory root = new TempDirectory(); + + string fullPath = Path.Join(root.Path, "file.txt"); + + TarEntry entry = InvokeTarEntryCreationConstructor(format, GetTarEntryTypeForTarEntryFormat(TarEntryType.RegularFile, format), fullPath); + + entry.DataStream = new MemoryStream(); + entry.DataStream.Write(new byte[] { 0x1 }); + entry.DataStream.Seek(0, SeekOrigin.Begin); + + await entry.ExtractToFileAsync(fullPath, overwrite: false); + + Assert.True(File.Exists(fullPath)); + } + + [Theory] + [MemberData(nameof(GetFormatsAndLinks))] + public async Task ExtractToFile_Link_Throws_Async(TarEntryFormat format, TarEntryType entryType) + { + using TempDirectory root = new TempDirectory(); + string fileName = "mylink"; + string fullPath = Path.Join(root.Path, fileName); + + string linkTarget = PlatformDetection.IsWindows ? @"C:\Windows\system32\notepad.exe" : "/usr/bin/nano"; + + TarEntry entry = InvokeTarEntryCreationConstructor(format, entryType, fileName); + entry.LinkName = linkTarget; + + await Assert.ThrowsAsync(async () => await entry.ExtractToFileAsync(fileName, overwrite: false)); + + Assert.Equal(0, Directory.GetFileSystemEntries(root.Path).Count()); + } + } +} diff --git a/src/libraries/System.Formats.Tar/tests/TarEntry/TarEntryGnu.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarEntry/TarEntryGnu.Tests.cs index 5ff711b5ee6ad0..e9bb630dd3f873 100644 --- a/src/libraries/System.Formats.Tar/tests/TarEntry/TarEntryGnu.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarEntry/TarEntryGnu.Tests.cs @@ -93,78 +93,5 @@ public void SupportedEntryType_Fifo() SetFifo(fifo); VerifyFifo(fifo); } - - [Fact] - public void Constructor_Name_FullPath_DestinationDirectory_Mismatch_Throws() - { - using TempDirectory root = new TempDirectory(); - - string fullPath = Path.Join(Path.GetPathRoot(root.Path), "dir", "file.txt"); - - GnuTarEntry entry = new GnuTarEntry(TarEntryType.RegularFile, fullPath); - - entry.DataStream = new MemoryStream(); - entry.DataStream.Write(new byte[] { 0x1 }); - entry.DataStream.Seek(0, SeekOrigin.Begin); - - Assert.Throws(() => entry.ExtractToFile(root.Path, overwrite: false)); - - Assert.False(File.Exists(fullPath)); - } - - [Fact] - public void Constructor_Name_FullPath_DestinationDirectory_Match_AdditionalSubdirectory_Throws() - { - using TempDirectory root = new TempDirectory(); - - string fullPath = Path.Join(root.Path, "dir", "file.txt"); - - GnuTarEntry entry = new GnuTarEntry(TarEntryType.RegularFile, fullPath); - - entry.DataStream = new MemoryStream(); - entry.DataStream.Write(new byte[] { 0x1 }); - entry.DataStream.Seek(0, SeekOrigin.Begin); - - Assert.Throws(() => entry.ExtractToFile(root.Path, overwrite: false)); - - Assert.False(File.Exists(fullPath)); - } - - [Fact] - public void Constructor_Name_FullPath_DestinationDirectory_Match() - { - using TempDirectory root = new TempDirectory(); - - string fullPath = Path.Join(root.Path, "file.txt"); - - GnuTarEntry entry = new GnuTarEntry(TarEntryType.RegularFile, fullPath); - - entry.DataStream = new MemoryStream(); - entry.DataStream.Write(new byte[] { 0x1 }); - entry.DataStream.Seek(0, SeekOrigin.Begin); - - entry.ExtractToFile(fullPath, overwrite: false); - - Assert.True(File.Exists(fullPath)); - } - - [Theory] - [InlineData(TarEntryType.SymbolicLink)] - [InlineData(TarEntryType.HardLink)] - public void ExtractToFile_Link_Throws(TarEntryType entryType) - { - using TempDirectory root = new TempDirectory(); - string fileName = "mylink"; - string fullPath = Path.Join(root.Path, fileName); - - string linkTarget = PlatformDetection.IsWindows ? @"C:\Windows\system32\notepad.exe" : "/usr/bin/nano"; - - GnuTarEntry entry = new GnuTarEntry(entryType, fileName); - entry.LinkName = linkTarget; - - Assert.Throws(() => entry.ExtractToFile(fileName, overwrite: false)); - - Assert.Equal(0, Directory.GetFileSystemEntries(root.Path).Count()); - } } } diff --git a/src/libraries/System.Formats.Tar/tests/TarEntry/TarEntryPax.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarEntry/TarEntryPax.Tests.cs index 9729f05789f886..69e028ae883aa2 100644 --- a/src/libraries/System.Formats.Tar/tests/TarEntry/TarEntryPax.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarEntry/TarEntryPax.Tests.cs @@ -91,78 +91,5 @@ public void SupportedEntryType_Fifo() SetFifo(fifo); VerifyFifo(fifo); } - - [Fact] - public void Constructor_Name_FullPath_DestinationDirectory_Mismatch_Throws() - { - using TempDirectory root = new TempDirectory(); - - string fullPath = Path.Join(Path.GetPathRoot(root.Path), "dir", "file.txt"); - - PaxTarEntry entry = new PaxTarEntry(TarEntryType.RegularFile, fullPath); - - entry.DataStream = new MemoryStream(); - entry.DataStream.Write(new byte[] { 0x1 }); - entry.DataStream.Seek(0, SeekOrigin.Begin); - - Assert.Throws(() => entry.ExtractToFile(root.Path, overwrite: false)); - - Assert.False(File.Exists(fullPath)); - } - - [Fact] - public void Constructor_Name_FullPath_DestinationDirectory_Match_AdditionalSubdirectory_Throws() - { - using TempDirectory root = new TempDirectory(); - - string fullPath = Path.Join(root.Path, "dir", "file.txt"); - - PaxTarEntry entry = new PaxTarEntry(TarEntryType.RegularFile, fullPath); - - entry.DataStream = new MemoryStream(); - entry.DataStream.Write(new byte[] { 0x1 }); - entry.DataStream.Seek(0, SeekOrigin.Begin); - - Assert.Throws(() => entry.ExtractToFile(root.Path, overwrite: false)); - - Assert.False(File.Exists(fullPath)); - } - - [Fact] - public void Constructor_Name_FullPath_DestinationDirectory_Match() - { - using TempDirectory root = new TempDirectory(); - - string fullPath = Path.Join(root.Path, "file.txt"); - - PaxTarEntry entry = new PaxTarEntry(TarEntryType.RegularFile, fullPath); - - entry.DataStream = new MemoryStream(); - entry.DataStream.Write(new byte[] { 0x1 }); - entry.DataStream.Seek(0, SeekOrigin.Begin); - - entry.ExtractToFile(fullPath, overwrite: false); - - Assert.True(File.Exists(fullPath)); - } - - [Theory] - [InlineData(TarEntryType.SymbolicLink)] - [InlineData(TarEntryType.HardLink)] - public void ExtractToFile_Link_Throws(TarEntryType entryType) - { - using TempDirectory root = new TempDirectory(); - string fileName = "mylink"; - string fullPath = Path.Join(root.Path, fileName); - - string linkTarget = PlatformDetection.IsWindows ? @"C:\Windows\system32\notepad.exe" : "/usr/bin/nano"; - - PaxTarEntry entry = new PaxTarEntry(entryType, fileName); - entry.LinkName = linkTarget; - - Assert.Throws(() => entry.ExtractToFile(fileName, overwrite: false)); - - Assert.Equal(0, Directory.GetFileSystemEntries(root.Path).Count()); - } } } diff --git a/src/libraries/System.Formats.Tar/tests/TarEntry/TarEntryUstar.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarEntry/TarEntryUstar.Tests.cs index 461c278b7f67e7..8087621f57ae1d 100644 --- a/src/libraries/System.Formats.Tar/tests/TarEntry/TarEntryUstar.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarEntry/TarEntryUstar.Tests.cs @@ -89,78 +89,5 @@ public void SupportedEntryType_Fifo() SetFifo(fifo); VerifyFifo(fifo); } - - [Fact] - public void Constructor_Name_FullPath_DestinationDirectory_Mismatch_Throws() - { - using TempDirectory root = new TempDirectory(); - - string fullPath = Path.Join(Path.GetPathRoot(root.Path), "dir", "file.txt"); - - UstarTarEntry entry = new UstarTarEntry(TarEntryType.RegularFile, fullPath); - - entry.DataStream = new MemoryStream(); - entry.DataStream.Write(new byte[] { 0x1 }); - entry.DataStream.Seek(0, SeekOrigin.Begin); - - Assert.Throws(() => entry.ExtractToFile(root.Path, overwrite: false)); - - Assert.False(File.Exists(fullPath)); - } - - [Fact] - public void Constructor_Name_FullPath_DestinationDirectory_Match_AdditionalSubdirectory_Throws() - { - using TempDirectory root = new TempDirectory(); - - string fullPath = Path.Join(root.Path, "dir", "file.txt"); - - UstarTarEntry entry = new UstarTarEntry(TarEntryType.RegularFile, fullPath); - - entry.DataStream = new MemoryStream(); - entry.DataStream.Write(new byte[] { 0x1 }); - entry.DataStream.Seek(0, SeekOrigin.Begin); - - Assert.Throws(() => entry.ExtractToFile(root.Path, overwrite: false)); - - Assert.False(File.Exists(fullPath)); - } - - [Fact] - public void Constructor_Name_FullPath_DestinationDirectory_Match() - { - using TempDirectory root = new TempDirectory(); - - string fullPath = Path.Join(root.Path, "file.txt"); - - UstarTarEntry entry = new UstarTarEntry(TarEntryType.RegularFile, fullPath); - - entry.DataStream = new MemoryStream(); - entry.DataStream.Write(new byte[] { 0x1 }); - entry.DataStream.Seek(0, SeekOrigin.Begin); - - entry.ExtractToFile(fullPath, overwrite: false); - - Assert.True(File.Exists(fullPath)); - } - - [Theory] - [InlineData(TarEntryType.SymbolicLink)] - [InlineData(TarEntryType.HardLink)] - public void ExtractToFile_Link_Throws(TarEntryType entryType) - { - using TempDirectory root = new TempDirectory(); - string fileName = "mylink"; - string fullPath = Path.Join(root.Path, fileName); - - string linkTarget = PlatformDetection.IsWindows ? @"C:\Windows\system32\notepad.exe" : "/usr/bin/nano"; - - UstarTarEntry entry = new UstarTarEntry(entryType, fileName); - entry.LinkName = linkTarget; - - Assert.Throws(() => entry.ExtractToFile(fileName, overwrite: false)); - - Assert.Equal(0, Directory.GetFileSystemEntries(root.Path).Count()); - } } } diff --git a/src/libraries/System.Formats.Tar/tests/TarEntry/TarEntryV7.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarEntry/TarEntryV7.Tests.cs index 8e14e5d792a11c..36dab66fb24e2f 100644 --- a/src/libraries/System.Formats.Tar/tests/TarEntry/TarEntryV7.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarEntry/TarEntryV7.Tests.cs @@ -69,78 +69,5 @@ public void SupportedEntryType_SymbolicLink() SetSymbolicLink(symbolicLink); VerifySymbolicLink(symbolicLink); } - - [Fact] - public void Constructor_Name_FullPath_DestinationDirectory_Mismatch_Throws() - { - using TempDirectory root = new TempDirectory(); - - string fullPath = Path.Join(Path.GetPathRoot(root.Path), "dir", "file.txt"); - - V7TarEntry entry = new V7TarEntry(TarEntryType.V7RegularFile, fullPath); - - entry.DataStream = new MemoryStream(); - entry.DataStream.Write(new byte[] { 0x1 }); - entry.DataStream.Seek(0, SeekOrigin.Begin); - - Assert.Throws(() => entry.ExtractToFile(root.Path, overwrite: false)); - - Assert.False(File.Exists(fullPath)); - } - - [Fact] - public void Constructor_Name_FullPath_DestinationDirectory_Match_AdditionalSubdirectory_Throws() - { - using TempDirectory root = new TempDirectory(); - - string fullPath = Path.Join(root.Path, "dir", "file.txt"); - - V7TarEntry entry = new V7TarEntry(TarEntryType.V7RegularFile, fullPath); - - entry.DataStream = new MemoryStream(); - entry.DataStream.Write(new byte[] { 0x1 }); - entry.DataStream.Seek(0, SeekOrigin.Begin); - - Assert.Throws(() => entry.ExtractToFile(root.Path, overwrite: false)); - - Assert.False(File.Exists(fullPath)); - } - - [Fact] - public void Constructor_Name_FullPath_DestinationDirectory_Match() - { - using TempDirectory root = new TempDirectory(); - - string fullPath = Path.Join(root.Path, "file.txt"); - - V7TarEntry entry = new V7TarEntry(TarEntryType.V7RegularFile, fullPath); - - entry.DataStream = new MemoryStream(); - entry.DataStream.Write(new byte[] { 0x1 }); - entry.DataStream.Seek(0, SeekOrigin.Begin); - - entry.ExtractToFile(fullPath, overwrite: false); - - Assert.True(File.Exists(fullPath)); - } - - [Theory] - [InlineData(TarEntryType.SymbolicLink)] - [InlineData(TarEntryType.HardLink)] - public void ExtractToFile_Link_Throws(TarEntryType entryType) - { - using TempDirectory root = new TempDirectory(); - string fileName = "mylink"; - string fullPath = Path.Join(root.Path, fileName); - - string linkTarget = PlatformDetection.IsWindows ? @"C:\Windows\system32\notepad.exe" : "/usr/bin/nano"; - - V7TarEntry entry = new V7TarEntry(entryType, fileName); - entry.LinkName = linkTarget; - - Assert.Throws(() => entry.ExtractToFile(fileName, overwrite: false)); - - Assert.Equal(0, Directory.GetFileSystemEntries(root.Path).Count()); - } } } diff --git a/src/libraries/System.Formats.Tar/tests/TarTestsBase.cs b/src/libraries/System.Formats.Tar/tests/TarTestsBase.cs index 868f826733a811..281e873bb39414 100644 --- a/src/libraries/System.Formats.Tar/tests/TarTestsBase.cs +++ b/src/libraries/System.Formats.Tar/tests/TarTestsBase.cs @@ -1,6 +1,7 @@ // 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 Xunit; @@ -312,6 +313,31 @@ protected Type GetTypeForFormat(TarEntryFormat expectedFormat) }; } + protected void CheckConversionType(TarEntry entry, TarEntryFormat expectedFormat) + { + Type expectedType = GetTypeForFormat(expectedFormat); + Assert.Equal(expectedType, entry.GetType()); + } + + protected TarEntryType GetTarEntryTypeForTarEntryFormat(TarEntryType entryType, TarEntryFormat format) + { + if (format is TarEntryFormat.V7) + { + if (entryType is TarEntryType.RegularFile) + { + return TarEntryType.V7RegularFile; + } + } + else + { + if (entryType is TarEntryType.V7RegularFile) + { + return TarEntryType.RegularFile; + } + } + return entryType; + } + protected TarEntry InvokeTarEntryCreationConstructor(TarEntryFormat targetFormat, TarEntryType entryType, string entryName) => targetFormat switch { @@ -322,5 +348,13 @@ protected TarEntry InvokeTarEntryCreationConstructor(TarEntryFormat targetFormat _ => throw new FormatException($"Unexpected format: {targetFormat}") }; + public static IEnumerable GetFormatsAndLinks() + { + foreach (TarEntryFormat format in new[] { TarEntryFormat.V7, TarEntryFormat.Ustar, TarEntryFormat.Pax, TarEntryFormat.Gnu }) + { + yield return new object[] { format, TarEntryType.SymbolicLink }; + yield return new object[] { format, TarEntryType.HardLink }; + } + } } } From ed71ab181f73047538d1061e70a82daa7e2151cc Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Fri, 24 Jun 2022 21:23:46 -0700 Subject: [PATCH 44/75] Join "regular entry vs writer format" tests into single theory test. --- .../TarWriter.WriteEntry.Entry.Gnu.Tests.cs | 24 ------- .../TarWriter.WriteEntry.Entry.Pax.Tests.cs | 25 ------- .../TarWriter.WriteEntry.Entry.Ustar.Tests.cs | 24 ------- .../TarWriter.WriteEntry.Entry.V7.Tests.cs | 46 ------------ .../TarWriter/TarWriter.WriteEntry.Tests.cs | 72 +++++++++++++++++++ 5 files changed, 72 insertions(+), 119 deletions(-) diff --git a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.Entry.Gnu.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.Entry.Gnu.Tests.cs index 7a22bc9ba94e64..292d5a09d06546 100644 --- a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.Entry.Gnu.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.Entry.Gnu.Tests.cs @@ -9,30 +9,6 @@ namespace System.Formats.Tar.Tests // Tests specific to Gnu format. public class TarWriter_WriteEntry_Gnu_Tests : TarTestsBase { - [Fact] - public void Write_V7RegularFileEntry_As_RegularFileEntry() - { - using MemoryStream archive = new MemoryStream(); - using (TarWriter writer = new TarWriter(archive, format: TarEntryFormat.Gnu, leaveOpen: true)) - { - V7TarEntry entry = new V7TarEntry(TarEntryType.V7RegularFile, InitialEntryName); - - // Should be written in the format of the entry - writer.WriteEntry(entry); - } - - archive.Seek(0, SeekOrigin.Begin); - using (TarReader reader = new TarReader(archive)) - { - TarEntry entry = reader.GetNextEntry(); - Assert.NotNull(entry); - Assert.Equal(TarEntryFormat.V7, entry.Format); - Assert.True(entry is V7TarEntry); - - Assert.Null(reader.GetNextEntry()); - } - } - [Fact] public void WriteRegularFile() { diff --git a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.Entry.Pax.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.Entry.Pax.Tests.cs index 4a1b09ee133f6f..be8fe183571e23 100644 --- a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.Entry.Pax.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.Entry.Pax.Tests.cs @@ -11,31 +11,6 @@ namespace System.Formats.Tar.Tests // Tests specific to PAX format. public class TarWriter_WriteEntry_Pax_Tests : TarTestsBase { - [Fact] - public void Write_V7RegularFile_To_PaxArchive() - { - using MemoryStream archive = new MemoryStream(); - using (TarWriter writer = new TarWriter(archive, format: TarEntryFormat.Pax, leaveOpen: true)) - { - V7TarEntry entry = new V7TarEntry(TarEntryType.V7RegularFile, InitialEntryName); - - // Should be written in the format of the entry - writer.WriteEntry(entry); - } - - archive.Seek(0, SeekOrigin.Begin); - using (TarReader reader = new TarReader(archive)) - { - TarEntry entry = reader.GetNextEntry(); - Assert.NotNull(entry); - Assert.Equal(TarEntryFormat.V7, entry.Format); - Assert.True(entry is V7TarEntry); - Assert.Equal(TarEntryType.V7RegularFile, entry.EntryType); - - Assert.Null(reader.GetNextEntry()); - } - } - [Fact] public void WriteRegularFile() { diff --git a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.Entry.Ustar.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.Entry.Ustar.Tests.cs index 39a02e505df92d..ce4c3c8aa27251 100644 --- a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.Entry.Ustar.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.Entry.Ustar.Tests.cs @@ -9,30 +9,6 @@ namespace System.Formats.Tar.Tests // Tests specific to Ustar format. public class TarWriter_WriteEntry_Ustar_Tests : TarTestsBase { - [Fact] - public void Write_V7RegularFileEntry_As_RegularFileEntry() - { - using MemoryStream archive = new MemoryStream(); - using (TarWriter writer = new TarWriter(archive, format: TarEntryFormat.Ustar, leaveOpen: true)) - { - V7TarEntry entry = new V7TarEntry(TarEntryType.V7RegularFile, InitialEntryName); - - // Should be written in the format of the entry - writer.WriteEntry(entry); - } - - archive.Seek(0, SeekOrigin.Begin); - using (TarReader reader = new TarReader(archive)) - { - TarEntry entry = reader.GetNextEntry(); - Assert.NotNull(entry); - Assert.Equal(TarEntryFormat.V7, entry.Format); - Assert.True(entry is V7TarEntry); - - Assert.Null(reader.GetNextEntry()); - } - } - [Fact] public void WriteRegularFile() { diff --git a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.Entry.V7.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.Entry.V7.Tests.cs index e87ce345ce891c..8bb1ed51265f34 100644 --- a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.Entry.V7.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.Entry.V7.Tests.cs @@ -9,52 +9,6 @@ namespace System.Formats.Tar.Tests // Tests specific to V7 format. public class TarWriter_WriteEntry_V7_Tests : TarTestsBase { - [Theory] - [InlineData(TarEntryFormat.Ustar)] - [InlineData(TarEntryFormat.Pax)] - [InlineData(TarEntryFormat.Gnu)] - public void Write_RegularFileEntry_As_V7RegularFileEntry(TarEntryFormat entryFormat) - { - using MemoryStream archive = new MemoryStream(); - using (TarWriter writer = new TarWriter(archive, format: TarEntryFormat.V7, leaveOpen: true)) - { - TarEntry entry = entryFormat switch - { - TarEntryFormat.Ustar => new UstarTarEntry(TarEntryType.RegularFile, InitialEntryName), - TarEntryFormat.Pax => new PaxTarEntry(TarEntryType.RegularFile, InitialEntryName), - TarEntryFormat.Gnu => new GnuTarEntry(TarEntryType.RegularFile, InitialEntryName), - _ => throw new FormatException($"Unexpected format: {entryFormat}") - }; - - // Should be written in the format of the entry - writer.WriteEntry(entry); - } - - archive.Seek(0, SeekOrigin.Begin); - using (TarReader reader = new TarReader(archive)) - { - TarEntry entry = reader.GetNextEntry(); - Assert.NotNull(entry); - Assert.Equal(entryFormat, entry.Format); - - switch (entryFormat) - { - case TarEntryFormat.Ustar: - Assert.True(entry is UstarTarEntry); - break; - case TarEntryFormat.Pax: - Assert.True(entry is PaxTarEntry); - break; - case TarEntryFormat.Gnu: - Assert.True(entry is GnuTarEntry); - break; - } - - Assert.Null(reader.GetNextEntry()); - } - } - - [Fact] public void WriteRegularFile() { 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 6e554efb98e46f..6998ddf2d2466e 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 @@ -87,6 +87,78 @@ public void WriteEntry_RespectDefaultWriterFormat(TarEntryFormat expectedFormat) } } + [Theory] + [InlineData(TarEntryFormat.Ustar)] + [InlineData(TarEntryFormat.Pax)] + [InlineData(TarEntryFormat.Gnu)] + public void Write_RegularFileEntry_In_V7Writer(TarEntryFormat entryFormat) + { + using MemoryStream archive = new MemoryStream(); + using (TarWriter writer = new TarWriter(archive, format: TarEntryFormat.V7, leaveOpen: true)) + { + TarEntry entry = entryFormat switch + { + TarEntryFormat.Ustar => new UstarTarEntry(TarEntryType.RegularFile, InitialEntryName), + TarEntryFormat.Pax => new PaxTarEntry(TarEntryType.RegularFile, InitialEntryName), + TarEntryFormat.Gnu => new GnuTarEntry(TarEntryType.RegularFile, InitialEntryName), + _ => throw new FormatException($"Unexpected format: {entryFormat}") + }; + + // Should be written in the format of the entry + writer.WriteEntry(entry); + } + + archive.Seek(0, SeekOrigin.Begin); + using (TarReader reader = new TarReader(archive)) + { + TarEntry entry = reader.GetNextEntry(); + Assert.NotNull(entry); + Assert.Equal(entryFormat, entry.Format); + + switch (entryFormat) + { + case TarEntryFormat.Ustar: + Assert.True(entry is UstarTarEntry); + break; + case TarEntryFormat.Pax: + Assert.True(entry is PaxTarEntry); + break; + case TarEntryFormat.Gnu: + Assert.True(entry is GnuTarEntry); + break; + } + + Assert.Null(reader.GetNextEntry()); + } + } + + [Theory] + [InlineData(TarEntryFormat.Ustar)] + [InlineData(TarEntryFormat.Pax)] + [InlineData(TarEntryFormat.Gnu)] + public void Write_V7RegularFileEntry_In_OtherFormatsWriter(TarEntryFormat writerFormat) + { + using MemoryStream archive = new MemoryStream(); + using (TarWriter writer = new TarWriter(archive, format: writerFormat, leaveOpen: true)) + { + V7TarEntry entry = new V7TarEntry(TarEntryType.V7RegularFile, InitialEntryName); + + // Should be written in the format of the entry + writer.WriteEntry(entry); + } + + archive.Seek(0, SeekOrigin.Begin); + using (TarReader reader = new TarReader(archive)) + { + TarEntry entry = reader.GetNextEntry(); + Assert.NotNull(entry); + Assert.Equal(TarEntryFormat.V7, entry.Format); + Assert.True(entry is V7TarEntry); + + Assert.Null(reader.GetNextEntry()); + } + } + [Theory] [InlineData(TarEntryFormat.V7)] [InlineData(TarEntryFormat.Ustar)] From 02b2a08a5f299b20550f116a809f1af6acb66446 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Fri, 24 Jun 2022 21:45:01 -0700 Subject: [PATCH 45/75] Missing using directive in Unix test file. --- .../TarFile/TarFile.ExtractToDirectoryAsync.File.Tests.Unix.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectoryAsync.File.Tests.Unix.cs b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectoryAsync.File.Tests.Unix.cs index bef773566d0975..7e0c87bab0f10a 100644 --- a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectoryAsync.File.Tests.Unix.cs +++ b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectoryAsync.File.Tests.Unix.cs @@ -3,6 +3,7 @@ using System.IO; using System.Linq; +using System.Threading.Tasks; using Xunit; namespace System.Formats.Tar.Tests From 915d5f967525d7247219b24aef026c6a9f749c8b Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Fri, 24 Jun 2022 21:52:00 -0700 Subject: [PATCH 46/75] Add TarWriter.WriteEntryAsync tests for entries. --- .../tests/System.Formats.Tar.Tests.csproj | 4 + .../TarWriter.WriteEntry.Entry.Gnu.Tests.cs | 4 +- ...rWriter.WriteEntryAsync.Entry.Gnu.Tests.cs | 252 +++++++++ ...rWriter.WriteEntryAsync.Entry.Pax.Tests.cs | 518 ++++++++++++++++++ ...riter.WriteEntryAsync.Entry.Ustar.Tests.cs | 167 ++++++ ...arWriter.WriteEntryAsync.Entry.V7.Tests.cs | 101 ++++ 6 files changed, 1044 insertions(+), 2 deletions(-) create mode 100644 src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.Entry.Gnu.Tests.cs create mode 100644 src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.Entry.Pax.Tests.cs create mode 100644 src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.Entry.Ustar.Tests.cs create mode 100644 src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.Entry.V7.Tests.cs diff --git a/src/libraries/System.Formats.Tar/tests/System.Formats.Tar.Tests.csproj b/src/libraries/System.Formats.Tar/tests/System.Formats.Tar.Tests.csproj index 701d0d9102d1e9..7f01a7bff46769 100644 --- a/src/libraries/System.Formats.Tar/tests/System.Formats.Tar.Tests.csproj +++ b/src/libraries/System.Formats.Tar/tests/System.Formats.Tar.Tests.csproj @@ -46,6 +46,10 @@ + + + + diff --git a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.Entry.Gnu.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.Entry.Gnu.Tests.cs index 292d5a09d06546..1421456a31283f 100644 --- a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.Entry.Gnu.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.Entry.Gnu.Tests.cs @@ -178,7 +178,7 @@ public void Write_Long_Name(TarEntryType entryType) [Theory] [InlineData(TarEntryType.SymbolicLink)] [InlineData(TarEntryType.HardLink)] - public void Write_LongLinKName(TarEntryType entryType) + public void Write_LongLinkName(TarEntryType entryType) { // LinkName field in header only fits 100 bytes string longLinkName = new string('a', 101); @@ -204,7 +204,7 @@ public void Write_LongLinKName(TarEntryType entryType) [Theory] [InlineData(TarEntryType.SymbolicLink)] [InlineData(TarEntryType.HardLink)] - public void Write_LongName_And_LongLinKName(TarEntryType entryType) + public void Write_LongName_And_LongLinkName(TarEntryType entryType) { // Both the Name and LinkName fields in header only fit 100 bytes string longName = new string('a', 101); diff --git a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.Entry.Gnu.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.Entry.Gnu.Tests.cs new file mode 100644 index 00000000000000..9a535536442a25 --- /dev/null +++ b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.Entry.Gnu.Tests.cs @@ -0,0 +1,252 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; +using System.Threading.Tasks; +using Xunit; + +namespace System.Formats.Tar.Tests +{ + // Tests specific to Gnu format. + public class TarWriter_WriteEntryAsync_Gnu_Tests : TarTestsBase + { + [Fact] + public async Task WriteRegularFile_Async() + { + using MemoryStream archiveStream = new MemoryStream(); + TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Gnu, leaveOpen: true); + await using (writer) + { + GnuTarEntry regularFile = new GnuTarEntry(TarEntryType.RegularFile, InitialEntryName); + SetRegularFile(regularFile); + VerifyRegularFile(regularFile, isWritable: true); + await writer.WriteEntryAsync(regularFile); + } + + archiveStream.Position = 0; + TarReader reader = new TarReader(archiveStream); + await using (reader) + { + GnuTarEntry regularFile = await reader.GetNextEntryAsync() as GnuTarEntry; + VerifyRegularFile(regularFile, isWritable: false); + } + } + + [Fact] + public async Task WriteHardLink_Async() + { + using MemoryStream archiveStream = new MemoryStream(); + TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Gnu, leaveOpen: true); + await using (writer) + { + GnuTarEntry hardLink = new GnuTarEntry(TarEntryType.HardLink, InitialEntryName); + SetHardLink(hardLink); + VerifyHardLink(hardLink); + await writer.WriteEntryAsync(hardLink); + } + + archiveStream.Position = 0; + TarReader reader = new TarReader(archiveStream); + await using (reader) + { + GnuTarEntry hardLink = await reader.GetNextEntryAsync() as GnuTarEntry; + VerifyHardLink(hardLink); + } + } + + [Fact] + public async Task WriteSymbolicLink_Async() + { + using MemoryStream archiveStream = new MemoryStream(); + TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Gnu, leaveOpen: true); + await using (writer) + { + GnuTarEntry symbolicLink = new GnuTarEntry(TarEntryType.SymbolicLink, InitialEntryName); + SetSymbolicLink(symbolicLink); + VerifySymbolicLink(symbolicLink); + await writer.WriteEntryAsync(symbolicLink); + } + + archiveStream.Position = 0; + TarReader reader = new TarReader(archiveStream); + await using (reader) + { + GnuTarEntry symbolicLink = await reader.GetNextEntryAsync() as GnuTarEntry; + VerifySymbolicLink(symbolicLink); + } + } + + [Fact] + public async Task WriteDirectory_Async() + { + using MemoryStream archiveStream = new MemoryStream(); + TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Gnu, leaveOpen: true); + await using (writer) + { + GnuTarEntry directory = new GnuTarEntry(TarEntryType.Directory, InitialEntryName); + SetDirectory(directory); + VerifyDirectory(directory); + await writer.WriteEntryAsync(directory); + } + + archiveStream.Position = 0; + TarReader reader = new TarReader(archiveStream); + await using (reader) + { + GnuTarEntry directory = await reader.GetNextEntryAsync() as GnuTarEntry; + VerifyDirectory(directory); + } + } + + [Fact] + public async Task WriteCharacterDevice_Async() + { + using MemoryStream archiveStream = new MemoryStream(); + TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Gnu, leaveOpen: true); + await using (writer) + { + GnuTarEntry charDevice = new GnuTarEntry(TarEntryType.CharacterDevice, InitialEntryName); + SetCharacterDevice(charDevice); + VerifyCharacterDevice(charDevice); + await writer.WriteEntryAsync(charDevice); + } + + archiveStream.Position = 0; + TarReader reader = new TarReader(archiveStream); + await using (reader) + { + GnuTarEntry charDevice = await reader.GetNextEntryAsync() as GnuTarEntry; + VerifyCharacterDevice(charDevice); + } + } + + [Fact] + public async Task WriteBlockDevice_Async() + { + using MemoryStream archiveStream = new MemoryStream(); + TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Gnu, leaveOpen: true); + await using (writer) + { + GnuTarEntry blockDevice = new GnuTarEntry(TarEntryType.BlockDevice, InitialEntryName); + SetBlockDevice(blockDevice); + VerifyBlockDevice(blockDevice); + await writer.WriteEntryAsync(blockDevice); + } + + archiveStream.Position = 0; + TarReader reader = new TarReader(archiveStream); + await using (reader) + { + GnuTarEntry blockDevice = await reader.GetNextEntryAsync() as GnuTarEntry; + VerifyBlockDevice(blockDevice); + } + } + + [Fact] + public async Task WriteFifo_Async() + { + using MemoryStream archiveStream = new MemoryStream(); + TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Gnu, leaveOpen: true); + await using (writer) + { + GnuTarEntry fifo = new GnuTarEntry(TarEntryType.Fifo, InitialEntryName); + SetFifo(fifo); + VerifyFifo(fifo); + await writer.WriteEntryAsync(fifo); + } + + archiveStream.Position = 0; + TarReader reader = new TarReader(archiveStream); + await using (reader) + { + GnuTarEntry fifo = await reader.GetNextEntryAsync() as GnuTarEntry; + VerifyFifo(fifo); + } + } + + [Theory] + [InlineData(TarEntryType.RegularFile)] + [InlineData(TarEntryType.Directory)] + [InlineData(TarEntryType.SymbolicLink)] + [InlineData(TarEntryType.HardLink)] + public async Task Write_Long_Name_Async(TarEntryType entryType) + { + // Name field in header only fits 100 bytes + string longName = new string('a', 101); + + using MemoryStream archiveStream = new MemoryStream(); + TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Gnu, leaveOpen: true); + await using (writer) + { + GnuTarEntry entry = new GnuTarEntry(entryType, longName); + await writer.WriteEntryAsync(entry); + } + + archiveStream.Position = 0; + TarReader reader = new TarReader(archiveStream); + await using (reader) + { + GnuTarEntry entry = await reader.GetNextEntryAsync() as GnuTarEntry; + Assert.Equal(entryType, entry.EntryType); + Assert.Equal(longName, entry.Name); + } + } + + [Theory] + [InlineData(TarEntryType.SymbolicLink)] + [InlineData(TarEntryType.HardLink)] + public async Task Write_LongLinkName_Async(TarEntryType entryType) + { + // LinkName field in header only fits 100 bytes + string longLinkName = new string('a', 101); + + using MemoryStream archiveStream = new MemoryStream(); + TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Gnu, leaveOpen: true); + await using (writer) + { + GnuTarEntry entry = new GnuTarEntry(entryType, "file.txt"); + entry.LinkName = longLinkName; + await writer.WriteEntryAsync(entry); + } + + archiveStream.Position = 0; + TarReader reader = new TarReader(archiveStream); + await using (reader) + { + GnuTarEntry entry = await reader.GetNextEntryAsync() as GnuTarEntry; + Assert.Equal(entryType, entry.EntryType); + Assert.Equal("file.txt", entry.Name); + Assert.Equal(longLinkName, entry.LinkName); + } + } + + [Theory] + [InlineData(TarEntryType.SymbolicLink)] + [InlineData(TarEntryType.HardLink)] + public async Task Write_LongName_And_LongLinkName_Async(TarEntryType entryType) + { + // Both the Name and LinkName fields in header only fit 100 bytes + string longName = new string('a', 101); + string longLinkName = new string('a', 101); + + using MemoryStream archiveStream = new MemoryStream(); + TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Gnu, leaveOpen: true); + await using (writer) + { + GnuTarEntry entry = new GnuTarEntry(entryType, longName); + entry.LinkName = longLinkName; + await writer.WriteEntryAsync(entry); + } + + archiveStream.Position = 0; + TarReader reader = new TarReader(archiveStream); + await using (reader) + { + GnuTarEntry entry = await reader.GetNextEntryAsync() as GnuTarEntry; + Assert.Equal(entryType, entry.EntryType); + Assert.Equal(longName, entry.Name); + Assert.Equal(longLinkName, entry.LinkName); + } + } + } +} diff --git a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.Entry.Pax.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.Entry.Pax.Tests.cs new file mode 100644 index 00000000000000..c1d146fc67a0ad --- /dev/null +++ b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.Entry.Pax.Tests.cs @@ -0,0 +1,518 @@ +// 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.Globalization; +using System.IO; +using System.Threading.Tasks; +using Xunit; + +namespace System.Formats.Tar.Tests +{ + // Tests specific to PAX format. + public class TarWriter_WriteEntryAsync_Pax_Tests : TarTestsBase + { + [Fact] + public async Task WriteRegularFile_Async() + { + using MemoryStream archiveStream = new MemoryStream(); + TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Pax, leaveOpen: true); + await using (writer) + { + PaxTarEntry regularFile = new PaxTarEntry(TarEntryType.RegularFile, InitialEntryName); + SetRegularFile(regularFile); + VerifyRegularFile(regularFile, isWritable: true); + await writer.WriteEntryAsync(regularFile); + } + + archiveStream.Position = 0; + TarReader reader = new TarReader(archiveStream); + await using (reader) + { + PaxTarEntry regularFile = await reader.GetNextEntryAsync() as PaxTarEntry; + VerifyRegularFile(regularFile, isWritable: false); + } + } + + [Fact] + public async Task WriteHardLink_Async() + { + using MemoryStream archiveStream = new MemoryStream(); + TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Pax, leaveOpen: true); + await using (writer) + { + PaxTarEntry hardLink = new PaxTarEntry(TarEntryType.HardLink, InitialEntryName); + SetHardLink(hardLink); + VerifyHardLink(hardLink); + await writer.WriteEntryAsync(hardLink); + } + + archiveStream.Position = 0; + TarReader reader = new TarReader(archiveStream); + await using (reader) + { + PaxTarEntry hardLink = await reader.GetNextEntryAsync() as PaxTarEntry; + VerifyHardLink(hardLink); + } + } + + [Fact] + public async Task WriteSymbolicLink_Async() + { + using MemoryStream archiveStream = new MemoryStream(); + TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Pax, leaveOpen: true); + await using (writer) + { + PaxTarEntry symbolicLink = new PaxTarEntry(TarEntryType.SymbolicLink, InitialEntryName); + SetSymbolicLink(symbolicLink); + VerifySymbolicLink(symbolicLink); + await writer.WriteEntryAsync(symbolicLink); + } + + archiveStream.Position = 0; + TarReader reader = new TarReader(archiveStream); + await using (reader) + { + PaxTarEntry symbolicLink = await reader.GetNextEntryAsync() as PaxTarEntry; + VerifySymbolicLink(symbolicLink); + } + } + + [Fact] + public async Task WriteDirectory_Async() + { + using MemoryStream archiveStream = new MemoryStream(); + TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Pax, leaveOpen: true); + await using (writer) + { + PaxTarEntry directory = new PaxTarEntry(TarEntryType.Directory, InitialEntryName); + SetDirectory(directory); + VerifyDirectory(directory); + await writer.WriteEntryAsync(directory); + } + + archiveStream.Position = 0; + TarReader reader = new TarReader(archiveStream); + await using (reader) + { + PaxTarEntry directory = await reader.GetNextEntryAsync() as PaxTarEntry; + VerifyDirectory(directory); + } + } + + [Fact] + public async Task WriteCharacterDevice_Async() + { + using MemoryStream archiveStream = new MemoryStream(); + TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Pax, leaveOpen: true); + await using (writer) + { + PaxTarEntry charDevice = new PaxTarEntry(TarEntryType.CharacterDevice, InitialEntryName); + SetCharacterDevice(charDevice); + VerifyCharacterDevice(charDevice); + await writer.WriteEntryAsync(charDevice); + } + + archiveStream.Position = 0; + TarReader reader = new TarReader(archiveStream); + await using (reader) + { + PaxTarEntry charDevice = await reader.GetNextEntryAsync() as PaxTarEntry; + VerifyCharacterDevice(charDevice); + } + } + + [Fact] + public async Task WriteBlockDevice_Async() + { + using MemoryStream archiveStream = new MemoryStream(); + TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Pax, leaveOpen: true); + await using (writer) + { + PaxTarEntry blockDevice = new PaxTarEntry(TarEntryType.BlockDevice, InitialEntryName); + SetBlockDevice(blockDevice); + VerifyBlockDevice(blockDevice); + await writer.WriteEntryAsync(blockDevice); + } + + archiveStream.Position = 0; + TarReader reader = new TarReader(archiveStream); + await using (reader) + { + PaxTarEntry blockDevice = await reader.GetNextEntryAsync() as PaxTarEntry; + VerifyBlockDevice(blockDevice); + } + } + + [Fact] + public async Task WriteFifo_Async() + { + using MemoryStream archiveStream = new MemoryStream(); + TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Pax, leaveOpen: true); + await using (writer) + { + PaxTarEntry fifo = new PaxTarEntry(TarEntryType.Fifo, InitialEntryName); + SetFifo(fifo); + VerifyFifo(fifo); + await writer.WriteEntryAsync(fifo); + } + + archiveStream.Position = 0; + TarReader reader = new TarReader(archiveStream); + await using (reader) + { + PaxTarEntry fifo = await reader.GetNextEntryAsync() as PaxTarEntry; + VerifyFifo(fifo); + } + } + + [Fact] + public async Task WritePaxAttributes_CustomAttribute_Async() + { + string expectedKey = "MyExtendedAttributeKey"; + string expectedValue = "MyExtendedAttributeValue"; + + Dictionary extendedAttributes = new(); + extendedAttributes.Add(expectedKey, expectedValue); + + using MemoryStream archiveStream = new MemoryStream(); + TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Pax, leaveOpen: true); + await using (writer) + { + PaxTarEntry regularFile = new PaxTarEntry(TarEntryType.RegularFile, InitialEntryName, extendedAttributes); + SetRegularFile(regularFile); + VerifyRegularFile(regularFile, isWritable: true); + await writer.WriteEntryAsync(regularFile); + } + + archiveStream.Position = 0; + TarReader reader = new TarReader(archiveStream); + await using (reader) + { + PaxTarEntry regularFile = await reader.GetNextEntryAsync() as PaxTarEntry; + VerifyRegularFile(regularFile, isWritable: false); + + Assert.NotNull(regularFile.ExtendedAttributes); + + // path, mtime, atime and ctime are always collected by default + AssertExtensions.GreaterThanOrEqualTo(regularFile.ExtendedAttributes.Count, 5); + + Assert.Contains(PaxEaName, regularFile.ExtendedAttributes); + Assert.Contains(PaxEaMTime, regularFile.ExtendedAttributes); + Assert.Contains(PaxEaATime, regularFile.ExtendedAttributes); + Assert.Contains(PaxEaCTime, regularFile.ExtendedAttributes); + + Assert.Contains(expectedKey, regularFile.ExtendedAttributes); + Assert.Equal(expectedValue, regularFile.ExtendedAttributes[expectedKey]); + } + } + + [Fact] + public async Task WritePaxAttributes_Timestamps_AutomaticallyAdded_Async() + { + DateTimeOffset minimumTime = DateTimeOffset.UtcNow - TimeSpan.FromHours(1); + using MemoryStream archiveStream = new MemoryStream(); + TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Pax, leaveOpen: true); + await using (writer) + { + PaxTarEntry regularFile = new PaxTarEntry(TarEntryType.RegularFile, InitialEntryName); + await writer.WriteEntryAsync(regularFile); + } + + archiveStream.Position = 0; + TarReader reader = new TarReader(archiveStream); + await using (reader) + { + PaxTarEntry regularFile = await reader.GetNextEntryAsync() as PaxTarEntry; + + AssertExtensions.GreaterThanOrEqualTo(regularFile.ExtendedAttributes.Count, 4); + VerifyExtendedAttributeTimestamp(regularFile, PaxEaMTime, minimumTime); + VerifyExtendedAttributeTimestamp(regularFile, PaxEaATime, minimumTime); + VerifyExtendedAttributeTimestamp(regularFile, PaxEaCTime, minimumTime); + } + } + + [Fact] + public async Task WritePaxAttributes_Timestamps_UserProvided_Async() + { + Dictionary extendedAttributes = new(); + extendedAttributes.Add(PaxEaATime, GetTimestampStringFromDateTimeOffset(TestAccessTime)); + extendedAttributes.Add(PaxEaCTime, GetTimestampStringFromDateTimeOffset(TestChangeTime)); + + using MemoryStream archiveStream = new MemoryStream(); + TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Pax, leaveOpen: true); + await using (writer) + { + PaxTarEntry regularFile = new PaxTarEntry(TarEntryType.RegularFile, InitialEntryName, extendedAttributes); + regularFile.ModificationTime = TestModificationTime; + await writer.WriteEntryAsync(regularFile); + } + + archiveStream.Position = 0; + TarReader reader = new TarReader(archiveStream); + await using (reader) + { + PaxTarEntry regularFile = await reader.GetNextEntryAsync() as PaxTarEntry; + + AssertExtensions.GreaterThanOrEqualTo(regularFile.ExtendedAttributes.Count, 4); + VerifyExtendedAttributeTimestamp(regularFile, PaxEaMTime, TestModificationTime); + VerifyExtendedAttributeTimestamp(regularFile, PaxEaATime, TestAccessTime); + VerifyExtendedAttributeTimestamp(regularFile, PaxEaCTime, TestChangeTime); + } + } + + [Fact] + public async Task WritePaxAttributes_LongGroupName_LongUserName_Async() + { + string userName = "IAmAUserNameWhoseLengthIsWayBeyondTheThirtyTwoByteLimit"; + string groupName = "IAmAGroupNameWhoseLengthIsWayBeyondTheThirtyTwoByteLimit"; + + using MemoryStream archiveStream = new MemoryStream(); + TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Pax, leaveOpen: true); + await using (writer) + { + PaxTarEntry regularFile = new PaxTarEntry(TarEntryType.RegularFile, InitialEntryName); + SetRegularFile(regularFile); + VerifyRegularFile(regularFile, isWritable: true); + regularFile.UserName = userName; + regularFile.GroupName = groupName; + await writer.WriteEntryAsync(regularFile); + } + + archiveStream.Position = 0; + TarReader reader = new TarReader(archiveStream); + await using (reader) + { + PaxTarEntry regularFile = await reader.GetNextEntryAsync() as PaxTarEntry; + VerifyRegularFile(regularFile, isWritable: false); + + Assert.NotNull(regularFile.ExtendedAttributes); + + // path, mtime, atime and ctime are always collected by default + AssertExtensions.GreaterThanOrEqualTo(regularFile.ExtendedAttributes.Count, 6); + + Assert.Contains(PaxEaName, regularFile.ExtendedAttributes); + Assert.Contains(PaxEaMTime, regularFile.ExtendedAttributes); + Assert.Contains(PaxEaATime, regularFile.ExtendedAttributes); + Assert.Contains(PaxEaCTime, regularFile.ExtendedAttributes); + + Assert.Contains(PaxEaUName, regularFile.ExtendedAttributes); + Assert.Equal(userName, regularFile.ExtendedAttributes[PaxEaUName]); + + Assert.Contains(PaxEaGName, regularFile.ExtendedAttributes); + Assert.Equal(groupName, regularFile.ExtendedAttributes[PaxEaGName]); + + // They should also get exposed via the regular properties + Assert.Equal(groupName, regularFile.GroupName); + Assert.Equal(userName, regularFile.UserName); + } + } + + [Fact] + public async Task WritePaxAttributes_Name_AutomaticallyAdded_Async() + { + using MemoryStream archiveStream = new MemoryStream(); + TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Pax, leaveOpen: true); + await using (writer) + { + PaxTarEntry regularFile = new PaxTarEntry(TarEntryType.RegularFile, InitialEntryName); + await writer.WriteEntryAsync(regularFile); + } + + archiveStream.Position = 0; + TarReader reader = new TarReader(archiveStream); + await using (reader) + { + PaxTarEntry regularFile = await reader.GetNextEntryAsync() as PaxTarEntry; + + AssertExtensions.GreaterThanOrEqualTo(regularFile.ExtendedAttributes.Count, 4); + Assert.Contains(PaxEaName, regularFile.ExtendedAttributes); + } + } + + [Fact] + public async Task WritePaxAttributes_LongLinkName_AutomaticallyAdded_Async() + { + using MemoryStream archiveStream = new MemoryStream(); + + string longSymbolicLinkName = new string('a', 101); + string longHardLinkName = new string('b', 101); + TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Pax, leaveOpen: true); + await using (writer) + { + PaxTarEntry symlink = new PaxTarEntry(TarEntryType.SymbolicLink, "symlink"); + symlink.LinkName = longSymbolicLinkName; + await writer.WriteEntryAsync(symlink); + + PaxTarEntry hardlink = new PaxTarEntry(TarEntryType.HardLink, "hardlink"); + hardlink.LinkName = longHardLinkName; + await writer.WriteEntryAsync(hardlink); + } + + archiveStream.Position = 0; + TarReader reader = new TarReader(archiveStream); + await using (reader) + { + PaxTarEntry symlink = await reader.GetNextEntryAsync() as PaxTarEntry; + + AssertExtensions.GreaterThanOrEqualTo(symlink.ExtendedAttributes.Count, 5); + + Assert.Contains(PaxEaName, symlink.ExtendedAttributes); + Assert.Equal("symlink", symlink.ExtendedAttributes[PaxEaName]); + Assert.Contains(PaxEaLinkName, symlink.ExtendedAttributes); + Assert.Equal(longSymbolicLinkName, symlink.ExtendedAttributes[PaxEaLinkName]); + + PaxTarEntry hardlink = await reader.GetNextEntryAsync() as PaxTarEntry; + + AssertExtensions.GreaterThanOrEqualTo(hardlink.ExtendedAttributes.Count, 5); + + Assert.Contains(PaxEaName, hardlink.ExtendedAttributes); + Assert.Equal("hardlink", hardlink.ExtendedAttributes[PaxEaName]); + Assert.Contains(PaxEaLinkName, hardlink.ExtendedAttributes); + Assert.Equal(longHardLinkName, hardlink.ExtendedAttributes[PaxEaLinkName]); + } + } + + [Fact] + public async Task Add_Empty_GlobalExtendedAttributes_Async() + { + using MemoryStream archive = new MemoryStream(); + + TarWriter writer = new TarWriter(archive, leaveOpen: true); + await using (writer) + { + PaxGlobalExtendedAttributesTarEntry gea = new PaxGlobalExtendedAttributesTarEntry(new Dictionary()); + await writer.WriteEntryAsync(gea); + } + + archive.Seek(0, SeekOrigin.Begin); + TarReader reader = new TarReader(archive); + await using (reader) + { + PaxGlobalExtendedAttributesTarEntry gea = await reader.GetNextEntryAsync() as PaxGlobalExtendedAttributesTarEntry; + Assert.NotNull(gea); + Assert.Equal(TarEntryFormat.Pax, gea.Format); + Assert.Equal(TarEntryType.GlobalExtendedAttributes, gea.EntryType); + + Assert.Equal(0, gea.GlobalExtendedAttributes.Count); + + Assert.Null(await reader.GetNextEntryAsync()); + } + } + + [Fact] + // Y2K38 will happen one second after "2038/19/01 03:14:07 +00:00". This timestamp represents the seconds since the Unix epoch with a + // value of int.MaxValue: 2,147,483,647. + // The fixed size fields for mtime, atime and ctime can fit 12 ASCII characters, but the last character is reserved for an ASCII space. + // All our entry types should survive the Epochalypse because we internally use long to represent the seconds since Unix epoch, not int. + // So if the max allowed value is 77,777,777,777 in octal, then the max allowed seconds since the Unix epoch are 8,589,934,591, which + // is way past int MaxValue, but still within the long limits. That number represents the date "2242/16/03 12:56:32 +00:00". + public async Task WriteTimestampsBeyondEpochalypseInPax_Async() + { + DateTimeOffset epochalypse = new DateTimeOffset(2038, 1, 19, 3, 14, 8, TimeSpan.Zero); + string strEpochalypse = GetTimestampStringFromDateTimeOffset(epochalypse); + + Dictionary ea = new Dictionary() + { + { PaxEaATime, strEpochalypse }, + { PaxEaCTime, strEpochalypse } + }; + + PaxTarEntry entry = new PaxTarEntry(TarEntryType.Directory, "dir", ea); + + entry.ModificationTime = epochalypse; + Assert.Equal(epochalypse, entry.ModificationTime); + + Assert.Contains(PaxEaATime, entry.ExtendedAttributes); + DateTimeOffset atime = GetDateTimeOffsetFromTimestampString(entry.ExtendedAttributes, PaxEaATime); + Assert.Equal(epochalypse, atime); + + Assert.Contains(PaxEaCTime, entry.ExtendedAttributes); + DateTimeOffset ctime = GetDateTimeOffsetFromTimestampString(entry.ExtendedAttributes, PaxEaCTime); + Assert.Equal(epochalypse, ctime); + + using MemoryStream archiveStream = new MemoryStream(); + TarWriter writer = new TarWriter(archiveStream, leaveOpen: true); + await using (writer) + { + await writer.WriteEntryAsync(entry); + } + + archiveStream.Position = 0; + TarReader reader = new TarReader(archiveStream); + await using (reader) + { + PaxTarEntry readEntry = await reader.GetNextEntryAsync() as PaxTarEntry; + Assert.NotNull(readEntry); + + Assert.Equal(epochalypse, readEntry.ModificationTime); + + Assert.Contains(PaxEaATime, readEntry.ExtendedAttributes); + DateTimeOffset actualATime = GetDateTimeOffsetFromTimestampString(readEntry.ExtendedAttributes, PaxEaATime); + Assert.Equal(epochalypse, actualATime); + + Assert.Contains(PaxEaCTime, readEntry.ExtendedAttributes); + DateTimeOffset actualCTime = GetDateTimeOffsetFromTimestampString(readEntry.ExtendedAttributes, PaxEaCTime); + Assert.Equal(epochalypse, actualCTime); + } + } + + [Fact] + // The fixed size fields for mtime, atime and ctime can fit 12 ASCII characters, but the last character is reserved for an ASCII space. + // We internally use long to represent the seconds since Unix epoch, not int. + // If the max allowed value is 77,777,777,777 in octal, then the max allowed seconds since the Unix epoch are 8,589,934,591, + // which represents the date "2242/03/16 12:56:32 +00:00". + // Pax should survive after this date because it stores the timestamps in the extended attributes dictionary + // without size restrictions. + public async Task WriteTimestampsBeyondOctalLimitInPax_Async() + { + DateTimeOffset overLimitTimestamp = new DateTimeOffset(2242, 3, 16, 12, 56, 33, TimeSpan.Zero); // One second past the octal limit + + string strOverLimitTimestamp = GetTimestampStringFromDateTimeOffset(overLimitTimestamp); + + Dictionary ea = new Dictionary() + { + { PaxEaATime, strOverLimitTimestamp }, + { PaxEaCTime, strOverLimitTimestamp } + }; + + PaxTarEntry entry = new PaxTarEntry(TarEntryType.Directory, "dir", ea); + + entry.ModificationTime = overLimitTimestamp; + Assert.Equal(overLimitTimestamp, entry.ModificationTime); + + Assert.Contains(PaxEaATime, entry.ExtendedAttributes); + DateTimeOffset atime = GetDateTimeOffsetFromTimestampString(entry.ExtendedAttributes, PaxEaATime); + Assert.Equal(overLimitTimestamp, atime); + + Assert.Contains(PaxEaCTime, entry.ExtendedAttributes); + DateTimeOffset ctime = GetDateTimeOffsetFromTimestampString(entry.ExtendedAttributes, PaxEaCTime); + Assert.Equal(overLimitTimestamp, ctime); + + using MemoryStream archiveStream = new MemoryStream(); + TarWriter writer = new TarWriter(archiveStream, leaveOpen: true); + await using (writer) + { + await writer.WriteEntryAsync(entry); + } + + archiveStream.Position = 0; + TarReader reader = new TarReader(archiveStream); + await using (reader) + { + PaxTarEntry readEntry = await reader.GetNextEntryAsync() as PaxTarEntry; + Assert.NotNull(readEntry); + + Assert.Equal(overLimitTimestamp, readEntry.ModificationTime); + + Assert.Contains(PaxEaATime, readEntry.ExtendedAttributes); + DateTimeOffset actualATime = GetDateTimeOffsetFromTimestampString(readEntry.ExtendedAttributes, PaxEaATime); + Assert.Equal(overLimitTimestamp, actualATime); + + Assert.Contains(PaxEaCTime, readEntry.ExtendedAttributes); + DateTimeOffset actualCTime = GetDateTimeOffsetFromTimestampString(readEntry.ExtendedAttributes, PaxEaCTime); + Assert.Equal(overLimitTimestamp, actualCTime); + } + } + } +} diff --git a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.Entry.Ustar.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.Entry.Ustar.Tests.cs new file mode 100644 index 00000000000000..450e6416a7ec91 --- /dev/null +++ b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.Entry.Ustar.Tests.cs @@ -0,0 +1,167 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; +using System.Threading.Tasks; +using Xunit; + +namespace System.Formats.Tar.Tests +{ + // Tests specific to Ustar format. + public class TarWriter_WriteEntryAsync_Ustar_Tests : TarTestsBase + { + [Fact] + public async Task WriteRegularFile_Async() + { + using MemoryStream archiveStream = new MemoryStream(); + TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Ustar, leaveOpen: true); + await using (writer) + { + UstarTarEntry regularFile = new UstarTarEntry(TarEntryType.RegularFile, InitialEntryName); + SetRegularFile(regularFile); + VerifyRegularFile(regularFile, isWritable: true); + await writer.WriteEntryAsync(regularFile); + } + + archiveStream.Position = 0; + TarReader reader = new TarReader(archiveStream); + await using (reader) + { + UstarTarEntry regularFile = await reader.GetNextEntryAsync() as UstarTarEntry; + VerifyRegularFile(regularFile, isWritable: false); + } + } + + [Fact] + public async Task WriteHardLink_Async() + { + using MemoryStream archiveStream = new MemoryStream(); + TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Ustar, leaveOpen: true); + await using (writer) + { + UstarTarEntry hardLink = new UstarTarEntry(TarEntryType.HardLink, InitialEntryName); + SetHardLink(hardLink); + VerifyHardLink(hardLink); + await writer.WriteEntryAsync(hardLink); + } + + archiveStream.Position = 0; + TarReader reader = new TarReader(archiveStream); + await using (reader) + { + UstarTarEntry hardLink = await reader.GetNextEntryAsync() as UstarTarEntry; + VerifyHardLink(hardLink); + } + } + + [Fact] + public async Task WriteSymbolicLink_Async() + { + using MemoryStream archiveStream = new MemoryStream(); + TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Ustar, leaveOpen: true); + await using (writer) + { + UstarTarEntry symbolicLink = new UstarTarEntry(TarEntryType.SymbolicLink, InitialEntryName); + SetSymbolicLink(symbolicLink); + VerifySymbolicLink(symbolicLink); + await writer.WriteEntryAsync(symbolicLink); + } + + archiveStream.Position = 0; + TarReader reader = new TarReader(archiveStream); + await using (reader) + { + UstarTarEntry symbolicLink = await reader.GetNextEntryAsync() as UstarTarEntry; + VerifySymbolicLink(symbolicLink); + } + } + + [Fact] + public async Task WriteDirectory_Async() + { + using MemoryStream archiveStream = new MemoryStream(); + TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Ustar, leaveOpen: true); + await using (writer) + { + UstarTarEntry directory = new UstarTarEntry(TarEntryType.Directory, InitialEntryName); + SetDirectory(directory); + VerifyDirectory(directory); + await writer.WriteEntryAsync(directory); + } + + archiveStream.Position = 0; + TarReader reader = new TarReader(archiveStream); + await using (reader) + { + UstarTarEntry directory = await reader.GetNextEntryAsync() as UstarTarEntry; + VerifyDirectory(directory); + } + } + + [Fact] + public async Task WriteCharacterDevice_Async() + { + using MemoryStream archiveStream = new MemoryStream(); + TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Ustar, leaveOpen: true); + await using (writer) + { + UstarTarEntry charDevice = new UstarTarEntry(TarEntryType.CharacterDevice, InitialEntryName); + SetCharacterDevice(charDevice); + VerifyCharacterDevice(charDevice); + await writer.WriteEntryAsync(charDevice); + } + + archiveStream.Position = 0; + TarReader reader = new TarReader(archiveStream); + await using (reader) + { + UstarTarEntry charDevice = await reader.GetNextEntryAsync() as UstarTarEntry; + VerifyCharacterDevice(charDevice); + } + } + + [Fact] + public async Task WriteBlockDevice_Async() + { + using MemoryStream archiveStream = new MemoryStream(); + TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Ustar, leaveOpen: true); + await using (writer) + { + UstarTarEntry blockDevice = new UstarTarEntry(TarEntryType.BlockDevice, InitialEntryName); + SetBlockDevice(blockDevice); + VerifyBlockDevice(blockDevice); + await writer.WriteEntryAsync(blockDevice); + } + + archiveStream.Position = 0; + TarReader reader = new TarReader(archiveStream); + await using (reader) + { + UstarTarEntry blockDevice = await reader.GetNextEntryAsync() as UstarTarEntry; + VerifyBlockDevice(blockDevice); + } + } + + [Fact] + public async Task WriteFifo_Async() + { + using MemoryStream archiveStream = new MemoryStream(); + TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Ustar, leaveOpen: true); + await using (writer) + { + UstarTarEntry fifo = new UstarTarEntry(TarEntryType.Fifo, InitialEntryName); + SetFifo(fifo); + VerifyFifo(fifo); + await writer.WriteEntryAsync(fifo); + } + + archiveStream.Position = 0; + TarReader reader = new TarReader(archiveStream); + await using (reader) + { + UstarTarEntry fifo = await reader.GetNextEntryAsync() as UstarTarEntry; + VerifyFifo(fifo); + } + } + } +} diff --git a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.Entry.V7.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.Entry.V7.Tests.cs new file mode 100644 index 00000000000000..b38910de9ba182 --- /dev/null +++ b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.Entry.V7.Tests.cs @@ -0,0 +1,101 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; +using System.Threading.Tasks; +using Xunit; + +namespace System.Formats.Tar.Tests +{ + // Tests specific to V7 format. + public class TarWriter_WriteEntryAsync_V7_Tests : TarTestsBase + { + [Fact] + public async Task WriteRegularFile_Async() + { + using MemoryStream archiveStream = new MemoryStream(); + TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.V7, leaveOpen: true); + await using (writer) + { + V7TarEntry oldRegularFile = new V7TarEntry(TarEntryType.V7RegularFile, InitialEntryName); + SetRegularFile(oldRegularFile); + VerifyRegularFile(oldRegularFile, isWritable: true); + await writer.WriteEntryAsync(oldRegularFile); + } + + archiveStream.Position = 0; + TarReader reader = new TarReader(archiveStream); + await using (reader) + { + V7TarEntry oldRegularFile = await reader.GetNextEntryAsync() as V7TarEntry; + VerifyRegularFile(oldRegularFile, isWritable: false); + } + } + + [Fact] + public async Task WriteHardLink_Async() + { + using MemoryStream archiveStream = new MemoryStream(); + TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.V7, leaveOpen: true); + await using (writer) + { + V7TarEntry hardLink = new V7TarEntry(TarEntryType.HardLink, InitialEntryName); + SetHardLink(hardLink); + VerifyHardLink(hardLink); + await writer.WriteEntryAsync(hardLink); + } + + archiveStream.Position = 0; + TarReader reader = new TarReader(archiveStream); + await using (reader) + { + V7TarEntry hardLink = await reader.GetNextEntryAsync() as V7TarEntry; + VerifyHardLink(hardLink); + } + } + + [Fact] + public async Task WriteSymbolicLink_Async() + { + using MemoryStream archiveStream = new MemoryStream(); + TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.V7, leaveOpen: true); + await using (writer) + { + V7TarEntry symbolicLink = new V7TarEntry(TarEntryType.SymbolicLink, InitialEntryName); + SetSymbolicLink(symbolicLink); + VerifySymbolicLink(symbolicLink); + await writer.WriteEntryAsync(symbolicLink); + } + + archiveStream.Position = 0; + TarReader reader = new TarReader(archiveStream); + await using (reader) + { + V7TarEntry symbolicLink = await reader.GetNextEntryAsync() as V7TarEntry; + VerifySymbolicLink(symbolicLink); + } + } + + [Fact] + public async Task WriteDirectory_Async() + { + using MemoryStream archiveStream = new MemoryStream(); + TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.V7, leaveOpen: true); + await using (writer) + { + V7TarEntry directory = new V7TarEntry(TarEntryType.Directory, InitialEntryName); + SetDirectory(directory); + VerifyDirectory(directory); + await writer.WriteEntryAsync(directory); + } + + archiveStream.Position = 0; + TarReader reader = new TarReader(archiveStream); + await using (reader) + { + V7TarEntry directory = await reader.GetNextEntryAsync() as V7TarEntry; + VerifyDirectory(directory); + } + } + } +} From 5eec19890f08638088215b4b154ec821b1a87762 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Fri, 24 Jun 2022 22:52:56 -0700 Subject: [PATCH 47/75] Add TarWriter.WriteEntryAsync tests for file assets. --- .../tests/System.Formats.Tar.Tests.csproj | 7 +- .../TarReader.File.Async.Tests.Base.cs | 40 +-- .../TarReader/TarReader.File.Tests.Base.cs | 40 +-- .../tests/TarTestsBase.Gnu.cs | 6 + .../tests/TarTestsBase.Pax.cs | 10 + .../System.Formats.Tar/tests/TarTestsBase.cs | 2 + .../TarWriter/TarWriter.File.Base.Unix.cs | 55 +++ ...dows.cs => TarWriter.File.Base.Windows.cs} | 8 +- .../TarWriter/TarWriter.WriteEntry.Base.cs | 36 ++ .../TarWriter.WriteEntry.File.Tests.Unix.cs | 48 +-- .../TarWriter.WriteEntry.File.Tests.cs | 20 +- .../TarWriter/TarWriter.WriteEntry.Tests.cs | 49 +-- ...rWriter.WriteEntryAsync.File.Tests.Unix.cs | 150 ++++++++ .../TarWriter.WriteEntryAsync.File.Tests.cs | 221 ++++++++++++ .../TarWriter.WriteEntryAsync.Tests.cs | 319 ++++++++++++++++++ 15 files changed, 843 insertions(+), 168 deletions(-) create mode 100644 src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.File.Base.Unix.cs rename src/libraries/System.Formats.Tar/tests/TarWriter/{TarWriter.WriteEntry.File.Tests.Windows.cs => TarWriter.File.Base.Windows.cs} (81%) create mode 100644 src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.Base.cs create mode 100644 src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.File.Tests.Unix.cs create mode 100644 src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.File.Tests.cs create mode 100644 src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.Tests.cs diff --git a/src/libraries/System.Formats.Tar/tests/System.Formats.Tar.Tests.csproj b/src/libraries/System.Formats.Tar/tests/System.Formats.Tar.Tests.csproj index 7f01a7bff46769..32cce09636b392 100644 --- a/src/libraries/System.Formats.Tar/tests/System.Formats.Tar.Tests.csproj +++ b/src/libraries/System.Formats.Tar/tests/System.Formats.Tar.Tests.csproj @@ -46,6 +46,9 @@ + + + @@ -65,7 +68,7 @@ - + @@ -81,7 +84,9 @@ + + diff --git a/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.File.Async.Tests.Base.cs b/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.File.Async.Tests.Base.cs index 700445159f9bd4..52974e55e31f5e 100644 --- a/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.File.Async.Tests.Base.cs +++ b/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.File.Async.Tests.Base.cs @@ -443,11 +443,11 @@ private void VerifyRegularFileEntry(TarEntry file, TarEntryFormat format, string if (posix is PaxTarEntry pax) { - VerifyExtendedAttributes(pax); + VerifyExtendedAttributeTimestamps(pax); } else if (posix is GnuTarEntry gnu) { - VerifyGnuFields(gnu); + VerifyGnuTimestamps(gnu); } } } @@ -480,11 +480,11 @@ private void VerifySymbolicLinkEntry(TarEntry symbolicLink, TarEntryFormat forma if (symbolicLink is PaxTarEntry pax) { - VerifyExtendedAttributes(pax); + VerifyExtendedAttributeTimestamps(pax); } else if (symbolicLink is GnuTarEntry gnu) { - VerifyGnuFields(gnu); + VerifyGnuTimestamps(gnu); } } @@ -516,11 +516,11 @@ private void VerifyDirectoryEntry(TarEntry directory, TarEntryFormat format, str if (directory is PaxTarEntry pax) { - VerifyExtendedAttributes(pax); + VerifyExtendedAttributeTimestamps(pax); } else if (directory is GnuTarEntry gnu) { - VerifyGnuFields(gnu); + VerifyGnuTimestamps(gnu); } } @@ -548,11 +548,11 @@ private void VerifyBlockDeviceEntry(PosixTarEntry blockDevice, TarEntryFormat fo if (blockDevice is PaxTarEntry pax) { - VerifyExtendedAttributes(pax); + VerifyExtendedAttributeTimestamps(pax); } else if (blockDevice is GnuTarEntry gnu) { - VerifyGnuFields(gnu); + VerifyGnuTimestamps(gnu); } } @@ -580,11 +580,11 @@ private void VerifyCharacterDeviceEntry(PosixTarEntry characterDevice, TarEntryF if (characterDevice is PaxTarEntry pax) { - VerifyExtendedAttributes(pax); + VerifyExtendedAttributeTimestamps(pax); } else if (characterDevice is GnuTarEntry gnu) { - VerifyGnuFields(gnu); + VerifyGnuTimestamps(gnu); } } @@ -613,11 +613,11 @@ private void VerifyFifoEntry(PosixTarEntry fifo, TarEntryFormat format, string e if (fifo is PaxTarEntry pax) { - VerifyExtendedAttributes(pax); + VerifyExtendedAttributeTimestamps(pax); } else if (fifo is GnuTarEntry gnu) { - VerifyGnuFields(gnu); + VerifyGnuTimestamps(gnu); } } @@ -640,21 +640,5 @@ private void VerifyGlobalExtendedAttributes(TarEntry entry) Assert.Contains(AssetPaxGeaKey, gea.GlobalExtendedAttributes); Assert.Equal(AssetPaxGeaValue, gea.GlobalExtendedAttributes[AssetPaxGeaKey]); } - - private void VerifyExtendedAttributes(PaxTarEntry pax) - { - Assert.NotNull(pax.ExtendedAttributes); - AssertExtensions.GreaterThanOrEqualTo(pax.ExtendedAttributes.Count(), 3); // Expect to at least collect mtime, ctime and atime - - VerifyExtendedAttributeTimestamp(pax, PaxEaMTime, MinimumTime); - VerifyExtendedAttributeTimestamp(pax, PaxEaATime, MinimumTime); - VerifyExtendedAttributeTimestamp(pax, PaxEaCTime, MinimumTime); - } - - private void VerifyGnuFields(GnuTarEntry gnu) - { - AssertExtensions.GreaterThanOrEqualTo(gnu.AccessTime, DateTimeOffset.UnixEpoch); - AssertExtensions.GreaterThanOrEqualTo(gnu.ChangeTime, DateTimeOffset.UnixEpoch); - } } } diff --git a/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.File.Tests.Base.cs b/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.File.Tests.Base.cs index 2479fcb0819886..1d43c7ad1b049f 100644 --- a/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.File.Tests.Base.cs +++ b/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.File.Tests.Base.cs @@ -402,11 +402,11 @@ private void VerifyRegularFileEntry(TarEntry file, TarEntryFormat format, string if (posix is PaxTarEntry pax) { - VerifyExtendedAttributes(pax); + VerifyExtendedAttributeTimestamps(pax); } else if (posix is GnuTarEntry gnu) { - VerifyGnuFields(gnu); + VerifyGnuTimestamps(gnu); } } } @@ -439,11 +439,11 @@ private void VerifySymbolicLinkEntry(TarEntry symbolicLink, TarEntryFormat forma if (symbolicLink is PaxTarEntry pax) { - VerifyExtendedAttributes(pax); + VerifyExtendedAttributeTimestamps(pax); } else if (symbolicLink is GnuTarEntry gnu) { - VerifyGnuFields(gnu); + VerifyGnuTimestamps(gnu); } } @@ -475,11 +475,11 @@ private void VerifyDirectoryEntry(TarEntry directory, TarEntryFormat format, str if (directory is PaxTarEntry pax) { - VerifyExtendedAttributes(pax); + VerifyExtendedAttributeTimestamps(pax); } else if (directory is GnuTarEntry gnu) { - VerifyGnuFields(gnu); + VerifyGnuTimestamps(gnu); } } @@ -507,11 +507,11 @@ private void VerifyBlockDeviceEntry(PosixTarEntry blockDevice, TarEntryFormat fo if (blockDevice is PaxTarEntry pax) { - VerifyExtendedAttributes(pax); + VerifyExtendedAttributeTimestamps(pax); } else if (blockDevice is GnuTarEntry gnu) { - VerifyGnuFields(gnu); + VerifyGnuTimestamps(gnu); } } @@ -539,11 +539,11 @@ private void VerifyCharacterDeviceEntry(PosixTarEntry characterDevice, TarEntryF if (characterDevice is PaxTarEntry pax) { - VerifyExtendedAttributes(pax); + VerifyExtendedAttributeTimestamps(pax); } else if (characterDevice is GnuTarEntry gnu) { - VerifyGnuFields(gnu); + VerifyGnuTimestamps(gnu); } } @@ -572,11 +572,11 @@ private void VerifyFifoEntry(PosixTarEntry fifo, TarEntryFormat format, string e if (fifo is PaxTarEntry pax) { - VerifyExtendedAttributes(pax); + VerifyExtendedAttributeTimestamps(pax); } else if (fifo is GnuTarEntry gnu) { - VerifyGnuFields(gnu); + VerifyGnuTimestamps(gnu); } } @@ -600,21 +600,5 @@ private void VerifyGlobalExtendedAttributes(TarReader reader) Assert.Contains(AssetPaxGeaKey, gea.GlobalExtendedAttributes); Assert.Equal(AssetPaxGeaValue, gea.GlobalExtendedAttributes[AssetPaxGeaKey]); } - - private void VerifyExtendedAttributes(PaxTarEntry pax) - { - Assert.NotNull(pax.ExtendedAttributes); - AssertExtensions.GreaterThanOrEqualTo(pax.ExtendedAttributes.Count(), 3); // Expect to at least collect mtime, ctime and atime - - VerifyExtendedAttributeTimestamp(pax, PaxEaMTime, MinimumTime); - VerifyExtendedAttributeTimestamp(pax, PaxEaATime, MinimumTime); - VerifyExtendedAttributeTimestamp(pax, PaxEaCTime, MinimumTime); - } - - private void VerifyGnuFields(GnuTarEntry gnu) - { - AssertExtensions.GreaterThanOrEqualTo(gnu.AccessTime, DateTimeOffset.UnixEpoch); - AssertExtensions.GreaterThanOrEqualTo(gnu.ChangeTime, DateTimeOffset.UnixEpoch); - } } } diff --git a/src/libraries/System.Formats.Tar/tests/TarTestsBase.Gnu.cs b/src/libraries/System.Formats.Tar/tests/TarTestsBase.Gnu.cs index 66d00062dae297..ae847e45ac2d3d 100644 --- a/src/libraries/System.Formats.Tar/tests/TarTestsBase.Gnu.cs +++ b/src/libraries/System.Formats.Tar/tests/TarTestsBase.Gnu.cs @@ -116,5 +116,11 @@ protected void VerifyGnuProperties(GnuTarEntry entry) Assert.Equal(TestAccessTime, entry.AccessTime); Assert.Equal(TestChangeTime, entry.ChangeTime); } + + protected void VerifyGnuTimestamps(GnuTarEntry gnu) + { + AssertExtensions.GreaterThanOrEqualTo(gnu.AccessTime, DateTimeOffset.UnixEpoch); + AssertExtensions.GreaterThanOrEqualTo(gnu.ChangeTime, DateTimeOffset.UnixEpoch); + } } } diff --git a/src/libraries/System.Formats.Tar/tests/TarTestsBase.Pax.cs b/src/libraries/System.Formats.Tar/tests/TarTestsBase.Pax.cs index 428e3a338cfaf9..dd71b157963962 100644 --- a/src/libraries/System.Formats.Tar/tests/TarTestsBase.Pax.cs +++ b/src/libraries/System.Formats.Tar/tests/TarTestsBase.Pax.cs @@ -114,5 +114,15 @@ protected void VerifyExtendedAttributeTimestamp(PaxTarEntry paxEntry, string fie DateTimeOffset converted = GetDateTimeOffsetFromTimestampString(paxEntry.ExtendedAttributes, fieldName); AssertExtensions.GreaterThanOrEqualTo(converted, minimumTime); } + + protected void VerifyExtendedAttributeTimestamps(PaxTarEntry pax) + { + Assert.NotNull(pax.ExtendedAttributes); + AssertExtensions.GreaterThanOrEqualTo(pax.ExtendedAttributes.Count, 3); // Expect to at least collect mtime, ctime and atime + + VerifyExtendedAttributeTimestamp(pax, PaxEaMTime, MinimumTime); + VerifyExtendedAttributeTimestamp(pax, PaxEaATime, MinimumTime); + VerifyExtendedAttributeTimestamp(pax, PaxEaCTime, MinimumTime); + } } } diff --git a/src/libraries/System.Formats.Tar/tests/TarTestsBase.cs b/src/libraries/System.Formats.Tar/tests/TarTestsBase.cs index 281e873bb39414..6caee6aa0a8ae1 100644 --- a/src/libraries/System.Formats.Tar/tests/TarTestsBase.cs +++ b/src/libraries/System.Formats.Tar/tests/TarTestsBase.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; +using Microsoft.DotNet.RemoteExecutor; using Xunit; namespace System.Formats.Tar.Tests @@ -97,6 +98,7 @@ public enum TestTarFormat // GNU formatted files. Format used by GNU tar versions up to 1.13.25. gnu } + protected static bool IsRemoteExecutorSupportedAndOnUnixAndSuperUser => RemoteExecutor.IsSupported && PlatformDetection.IsUnixAndSuperUser; protected static string GetTestCaseUnarchivedFolderPath(string testCaseName) => Path.Join(Directory.GetCurrentDirectory(), "unarchived", testCaseName); diff --git a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.File.Base.Unix.cs b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.File.Base.Unix.cs new file mode 100644 index 00000000000000..8b613191b0ef87 --- /dev/null +++ b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.File.Base.Unix.cs @@ -0,0 +1,55 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; +using Xunit; + +namespace System.Formats.Tar.Tests +{ + public partial class TarWriter_File_Base : TarTestsBase + { + protected void VerifyPlatformSpecificMetadata(string filePath, TarEntry entry) + { + Interop.Sys.FileStatus status = default; + status.Mode = default; + status.Dev = default; + Interop.CheckIo(Interop.Sys.LStat(filePath, out status)); + + Assert.Equal((int)status.Uid, entry.Uid); + Assert.Equal((int)status.Gid, entry.Gid); + + if (entry is PosixTarEntry posix) + { + string gname = Interop.Sys.GetGroupName(status.Gid); + string uname = Interop.Sys.GetUserNameFromPasswd(status.Uid); + + Assert.Equal(gname, posix.GroupName); + Assert.Equal(uname, posix.UserName); + + if (entry.EntryType is not TarEntryType.BlockDevice and not TarEntryType.CharacterDevice) + { + Assert.Equal(DefaultDeviceMajor, posix.DeviceMajor); + Assert.Equal(DefaultDeviceMinor, posix.DeviceMinor); + } + } + + if (entry.EntryType is not TarEntryType.Directory) + { + UnixFileMode expectedMode = (UnixFileMode)(status.Mode & 4095); // First 12 bits + + Assert.Equal(expectedMode, entry.Mode); + Assert.True(entry.ModificationTime > DateTimeOffset.UnixEpoch); + + if (entry is PaxTarEntry pax) + { + VerifyExtendedAttributeTimestamps(pax); + } + + if (entry is GnuTarEntry gnu) + { + VerifyGnuTimestamps(gnu); + } + } + } + } +} diff --git a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.File.Tests.Windows.cs b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.File.Base.Windows.cs similarity index 81% rename from src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.File.Tests.Windows.cs rename to src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.File.Base.Windows.cs index 38e5de652f1766..16fb8205685e7b 100644 --- a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.File.Tests.Windows.cs +++ b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.File.Base.Windows.cs @@ -1,15 +1,13 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Globalization; -using System.IO; using Xunit; namespace System.Formats.Tar.Tests { - public partial class TarWriter_WriteEntry_File_Tests : TarTestsBase + public partial class TarWriter_File_Base : TarTestsBase { - partial void VerifyPlatformSpecificMetadata(string filePath, TarEntry entry) + protected void VerifyPlatformSpecificMetadata(string filePath, TarEntry entry) { Assert.True(entry.ModificationTime > DateTimeOffset.UnixEpoch); @@ -29,7 +27,7 @@ partial void VerifyPlatformSpecificMetadata(string filePath, TarEntry entry) if (entry is PaxTarEntry pax) { - VerifyPaxTimestamps(pax); + VerifyExtendedAttributeTimestamps(pax); } if (entry is GnuTarEntry 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 new file mode 100644 index 00000000000000..b83fdc9451db43 --- /dev/null +++ b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.Base.cs @@ -0,0 +1,36 @@ +// 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.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace System.Formats.Tar.Tests +{ + public class TarWriter_WriteEntry_Base : TarTestsBase + { + protected void VerifyDirectory(TarEntry entry, TarEntryFormat format, string name) + { + Assert.NotNull(entry); + Assert.Equal(format, entry.Format); + Assert.Equal(TarEntryType.Directory, entry.EntryType); + Assert.Equal(name, entry.Name); + } + + protected void VerifyGlobalExtendedAttributesEntry(TarEntry entry, Dictionary attrs) + { + PaxGlobalExtendedAttributesTarEntry gea = entry as PaxGlobalExtendedAttributesTarEntry; + Assert.NotNull(gea); + Assert.Equal(attrs.Count, gea.GlobalExtendedAttributes.Count); + + foreach ((string key, string value) in attrs) + { + Assert.Contains(key, gea.GlobalExtendedAttributes); + Assert.Equal(value, gea.GlobalExtendedAttributes[key]); + } + } + } +} diff --git a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.File.Tests.Unix.cs b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.File.Tests.Unix.cs index b1fe162537a31a..39d5c2bb7c29b7 100644 --- a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.File.Tests.Unix.cs +++ b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.File.Tests.Unix.cs @@ -7,10 +7,8 @@ namespace System.Formats.Tar.Tests { - public partial class TarWriter_WriteEntry_File_Tests : TarTestsBase + public partial class TarWriter_WriteEntry_File_Tests : TarWriter_File_Base { - private static bool IsRemoteExecutorSupportedAndOnUnixAndSuperUser => RemoteExecutor.IsSupported && PlatformDetection.IsUnixAndSuperUser; - [ConditionalTheory(nameof(IsRemoteExecutorSupportedAndOnUnixAndSuperUser))] [InlineData(TarEntryFormat.Ustar)] [InlineData(TarEntryFormat.Pax)] @@ -141,49 +139,5 @@ public void Add_CharacterDevice(TarEntryFormat format) }, format.ToString(), new RemoteInvokeOptions { RunAsSudo = true }).Dispose(); } - - partial void VerifyPlatformSpecificMetadata(string filePath, TarEntry entry) - { - Interop.Sys.FileStatus status = default; - status.Mode = default; - status.Dev = default; - Interop.CheckIo(Interop.Sys.LStat(filePath, out status)); - - Assert.Equal((int)status.Uid, entry.Uid); - Assert.Equal((int)status.Gid, entry.Gid); - - if (entry is PosixTarEntry posix) - { - string gname = Interop.Sys.GetGroupName(status.Gid); - string uname = Interop.Sys.GetUserNameFromPasswd(status.Uid); - - Assert.Equal(gname, posix.GroupName); - Assert.Equal(uname, posix.UserName); - - if (entry.EntryType is not TarEntryType.BlockDevice and not TarEntryType.CharacterDevice) - { - Assert.Equal(DefaultDeviceMajor, posix.DeviceMajor); - Assert.Equal(DefaultDeviceMinor, posix.DeviceMinor); - } - } - - if (entry.EntryType is not TarEntryType.Directory) - { - UnixFileMode expectedMode = (UnixFileMode)(status.Mode & 4095); // First 12 bits - - Assert.Equal(expectedMode, entry.Mode); - Assert.True(entry.ModificationTime > DateTimeOffset.UnixEpoch); - - if (entry is PaxTarEntry pax) - { - VerifyPaxTimestamps(pax); - } - - if (entry is GnuTarEntry gnu) - { - VerifyGnuTimestamps(gnu); - } - } - } } } diff --git a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.File.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.File.Tests.cs index 9536b8f64fcce7..e1534a0d4960fc 100644 --- a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.File.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.File.Tests.cs @@ -7,7 +7,7 @@ namespace System.Formats.Tar.Tests { - public partial class TarWriter_WriteEntry_File_Tests : TarTestsBase + public partial class TarWriter_WriteEntry_File_Tests : TarWriter_File_Base { [Fact] public void ThrowIf_AddFile_AfterDispose() @@ -206,23 +206,5 @@ public void Add_SymbolicLink(TarEntryFormat format, bool createTarget) Assert.Null(reader.GetNextEntry()); } } - - partial void VerifyPlatformSpecificMetadata(string filePath, TarEntry entry); - - protected void VerifyPaxTimestamps(PaxTarEntry pax) - { - AssertExtensions.GreaterThanOrEqualTo(pax.ExtendedAttributes.Count, 4); - Assert.Contains(PaxEaName, pax.ExtendedAttributes); - - VerifyExtendedAttributeTimestamp(pax, PaxEaMTime, MinimumTime); - VerifyExtendedAttributeTimestamp(pax, PaxEaATime, MinimumTime); - VerifyExtendedAttributeTimestamp(pax, PaxEaCTime, MinimumTime); - } - - protected void VerifyGnuTimestamps(GnuTarEntry gnu) - { - Assert.True(gnu.AccessTime > DateTimeOffset.UnixEpoch); - Assert.True(gnu.ChangeTime > DateTimeOffset.UnixEpoch); - } } } 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 6998ddf2d2466e..6d19503c4fc25d 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 @@ -8,7 +8,7 @@ namespace System.Formats.Tar.Tests { // Tests that are independent of the archive format. - public class TarWriter_WriteEntry_Tests : TarTestsBase + public class TarWriter_WriteEntry_Tests : TarWriter_WriteEntry_Base { [Fact] public void WriteEntry_AfterDispose_Throws() @@ -29,9 +29,9 @@ public void WriteEntry_FromUnseekableStream_AdvanceDataStream_WriteFromThatPosit using MemoryStream destination = new MemoryStream(); - using (TarReader reader = new TarReader(unseekable)) + using (TarReader reader1 = new TarReader(unseekable)) { - TarEntry entry = reader.GetNextEntry(); + TarEntry entry = reader1.GetNextEntry(); Assert.NotNull(entry); Assert.NotNull(entry.DataStream); entry.DataStream.ReadByte(); // Advance one byte, now the expected string would be "ello file" @@ -43,9 +43,9 @@ public void WriteEntry_FromUnseekableStream_AdvanceDataStream_WriteFromThatPosit } destination.Seek(0, SeekOrigin.Begin); - using (TarReader reader = new TarReader(destination)) + using (TarReader reader2 = new TarReader(destination)) { - TarEntry entry = reader.GetNextEntry(); + TarEntry entry = reader2.GetNextEntry(); Assert.NotNull(entry); Assert.NotNull(entry.DataStream); @@ -178,13 +178,13 @@ public void ReadAndWriteMultipleGlobalExtendedAttributesEntries(TarEntryFormat f PaxGlobalExtendedAttributesTarEntry gea1 = new PaxGlobalExtendedAttributesTarEntry(attrs); writer.WriteEntry(gea1); - TarEntry entry1 = ConstructEntry(format, "dir1"); + TarEntry entry1 = InvokeTarEntryCreationConstructor(format, TarEntryType.Directory, "dir1"); writer.WriteEntry(entry1); PaxGlobalExtendedAttributesTarEntry gea2 = new PaxGlobalExtendedAttributesTarEntry(attrs); writer.WriteEntry(gea2); - TarEntry entry2 = ConstructEntry(format, "dir2"); + TarEntry entry2 = InvokeTarEntryCreationConstructor(format, TarEntryType.Directory, "dir2"); writer.WriteEntry(entry2); } @@ -193,9 +193,9 @@ public void ReadAndWriteMultipleGlobalExtendedAttributesEntries(TarEntryFormat f using (TarReader reader = new TarReader(archiveStream, leaveOpen: false)) { VerifyGlobalExtendedAttributesEntry(reader.GetNextEntry(), attrs); - VerifyDirEntry(reader.GetNextEntry(), format, "dir1"); + VerifyDirectory(reader.GetNextEntry(), format, "dir1"); VerifyGlobalExtendedAttributesEntry(reader.GetNextEntry(), attrs); - VerifyDirEntry(reader.GetNextEntry(), format, "dir2"); + VerifyDirectory(reader.GetNextEntry(), format, "dir2"); Assert.Null(reader.GetNextEntry()); } } @@ -299,36 +299,5 @@ public void WriteTimestampsBeyondOctalLimit(TarEntryFormat format) } } } - - private TarEntry ConstructEntry(TarEntryFormat format, string name) => - format switch - { - TarEntryFormat.V7 => new V7TarEntry(TarEntryType.Directory, name), - TarEntryFormat.Ustar => new UstarTarEntry(TarEntryType.Directory, name), - TarEntryFormat.Pax => new PaxTarEntry(TarEntryType.Directory, name), - TarEntryFormat.Gnu => new GnuTarEntry(TarEntryType.Directory, name), - _ => throw new Exception($"Unexpected format {format}"), - }; - - private void VerifyDirEntry(TarEntry entry, TarEntryFormat format, string name) - { - Assert.NotNull(entry); - Assert.Equal(format, entry.Format); - Assert.Equal(TarEntryType.Directory, entry.EntryType); - Assert.Equal(name, entry.Name); - } - - private void VerifyGlobalExtendedAttributesEntry(TarEntry entry, Dictionary attrs) - { - PaxGlobalExtendedAttributesTarEntry gea = entry as PaxGlobalExtendedAttributesTarEntry; - Assert.NotNull(gea); - Assert.Equal(attrs.Count, gea.GlobalExtendedAttributes.Count); - - foreach ((string key, string value) in attrs) - { - Assert.Contains(key, gea.GlobalExtendedAttributes); - Assert.Equal(value, gea.GlobalExtendedAttributes[key]); - } - } } } diff --git a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.File.Tests.Unix.cs b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.File.Tests.Unix.cs new file mode 100644 index 00000000000000..319bc73376b285 --- /dev/null +++ b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.File.Tests.Unix.cs @@ -0,0 +1,150 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.DotNet.RemoteExecutor; +using System.IO; +using System.Threading.Tasks; +using Xunit; + +namespace System.Formats.Tar.Tests +{ + public partial class TarWriter_WriteEntryAsync_File_Tests : TarWriter_File_Base + { + [ConditionalTheory(nameof(IsRemoteExecutorSupportedAndOnUnixAndSuperUser))] + [InlineData(TarEntryFormat.Ustar)] + [InlineData(TarEntryFormat.Pax)] + [InlineData(TarEntryFormat.Gnu)] + public void Add_Fifo_Async(TarEntryFormat format) + { + RemoteExecutor.Invoke(async (string strFormat) => + { + TarEntryFormat expectedFormat = Enum.Parse(strFormat); + + using TempDirectory root = new TempDirectory(); + string fifoName = "fifofile"; + string fifoPath = Path.Join(root.Path, fifoName); + + Interop.CheckIo(Interop.Sys.MkFifo(fifoPath, (int)DefaultMode)); + + using MemoryStream archive = new MemoryStream(); + TarWriter writer = new TarWriter(archive, expectedFormat, leaveOpen: true); + await using (writer) + { + await writer.WriteEntryAsync(fileName: fifoPath, entryName: fifoName); + } + + archive.Seek(0, SeekOrigin.Begin); + TarReader reader = new TarReader(archive); + await using (reader) + { + PosixTarEntry entry = await reader.GetNextEntryAsync() as PosixTarEntry; + Assert.Equal(expectedFormat, entry.Format); + + Assert.NotNull(entry); + Assert.Equal(fifoName, entry.Name); + Assert.Equal(DefaultLinkName, entry.LinkName); + Assert.Equal(TarEntryType.Fifo, entry.EntryType); + Assert.Null(entry.DataStream); + + VerifyPlatformSpecificMetadata(fifoPath, entry); + + Assert.Null(await reader.GetNextEntryAsync()); + } + + }, format.ToString(), new RemoteInvokeOptions { RunAsSudo = true }).Dispose(); + } + + [ConditionalTheory(nameof(IsRemoteExecutorSupportedAndOnUnixAndSuperUser))] + [InlineData(TarEntryFormat.Ustar)] + [InlineData(TarEntryFormat.Pax)] + [InlineData(TarEntryFormat.Gnu)] + public void Add_BlockDevice_Async(TarEntryFormat format) + { + RemoteExecutor.Invoke(async (string strFormat) => + { + TarEntryFormat expectedFormat = Enum.Parse(strFormat); + + using TempDirectory root = new TempDirectory(); + string blockDevicePath = Path.Join(root.Path, AssetBlockDeviceFileName); + + // Creating device files needs elevation + Interop.CheckIo(Interop.Sys.CreateBlockDevice(blockDevicePath, (int)DefaultMode, TestBlockDeviceMajor, TestBlockDeviceMinor)); + + using MemoryStream archive = new MemoryStream(); + TarWriter writer = new TarWriter(archive, expectedFormat, leaveOpen: true); + await using (writer) + { + await writer.WriteEntryAsync(fileName: blockDevicePath, entryName: AssetBlockDeviceFileName); + } + + archive.Seek(0, SeekOrigin.Begin); + TarReader reader = new TarReader(archive); + await using (reader) + { + PosixTarEntry entry = await reader.GetNextEntryAsync() as PosixTarEntry; + Assert.Equal(expectedFormat, entry.Format); + + Assert.NotNull(entry); + Assert.Equal(AssetBlockDeviceFileName, entry.Name); + Assert.Equal(DefaultLinkName, entry.LinkName); + Assert.Equal(TarEntryType.BlockDevice, entry.EntryType); + Assert.Null(entry.DataStream); + + VerifyPlatformSpecificMetadata(blockDevicePath, entry); + + Assert.Equal(TestBlockDeviceMajor, entry.DeviceMajor); + Assert.Equal(TestBlockDeviceMinor, entry.DeviceMinor); + + Assert.Null(await reader.GetNextEntryAsync()); + } + + }, format.ToString(), new RemoteInvokeOptions { RunAsSudo = true }).Dispose(); + } + + [ConditionalTheory(nameof(IsRemoteExecutorSupportedAndOnUnixAndSuperUser))] + [InlineData(TarEntryFormat.Ustar)] + [InlineData(TarEntryFormat.Pax)] + [InlineData(TarEntryFormat.Gnu)] + public void Add_CharacterDevice_Async(TarEntryFormat format) + { + RemoteExecutor.Invoke(async (string strFormat) => + { + TarEntryFormat expectedFormat = Enum.Parse(strFormat); + using TempDirectory root = new TempDirectory(); + string characterDevicePath = Path.Join(root.Path, AssetCharacterDeviceFileName); + + // Creating device files needs elevation + Interop.CheckIo(Interop.Sys.CreateCharacterDevice(characterDevicePath, (int)DefaultMode, TestCharacterDeviceMajor, TestCharacterDeviceMinor)); + + using MemoryStream archive = new MemoryStream(); + TarWriter writer = new TarWriter(archive, expectedFormat, leaveOpen: true); + await using (writer) + { + await writer.WriteEntryAsync(fileName: characterDevicePath, entryName: AssetCharacterDeviceFileName); + } + + archive.Seek(0, SeekOrigin.Begin); + TarReader reader = new TarReader(archive); + await using (reader) + { + PosixTarEntry entry = await reader.GetNextEntryAsync() as PosixTarEntry; + Assert.Equal(expectedFormat, entry.Format); + + Assert.NotNull(entry); + Assert.Equal(AssetCharacterDeviceFileName, entry.Name); + Assert.Equal(DefaultLinkName, entry.LinkName); + Assert.Equal(TarEntryType.CharacterDevice, entry.EntryType); + Assert.Null(entry.DataStream); + + VerifyPlatformSpecificMetadata(characterDevicePath, entry); + + Assert.Equal(TestCharacterDeviceMajor, entry.DeviceMajor); + Assert.Equal(TestCharacterDeviceMinor, entry.DeviceMinor); + + Assert.Null(await reader.GetNextEntryAsync()); + } + + }, format.ToString(), new RemoteInvokeOptions { RunAsSudo = true }).Dispose(); + } + } +} diff --git a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.File.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.File.Tests.cs new file mode 100644 index 00000000000000..8fc116cf73eab5 --- /dev/null +++ b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.File.Tests.cs @@ -0,0 +1,221 @@ +// 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.Threading.Tasks; +using Xunit; + +namespace System.Formats.Tar.Tests +{ + public partial class TarWriter_WriteEntryAsync_File_Tests : TarWriter_File_Base + { + [Fact] + public async Task ThrowIf_AddFile_AfterDispose_Async() + { + using MemoryStream archiveStream = new MemoryStream(); + TarWriter writer = new TarWriter(archiveStream); + await writer.DisposeAsync(); + + await Assert.ThrowsAsync(async () => await writer.WriteEntryAsync("fileName", "entryName")); + } + + [Fact] + public async Task FileName_NullOrEmpty_Async() + { + using MemoryStream archiveStream = new MemoryStream(); + TarWriter writer = new TarWriter(archiveStream); + await using (writer) + { + await Assert.ThrowsAsync(async () => await writer.WriteEntryAsync(null, "entryName")); + await Assert.ThrowsAsync(async () => await writer.WriteEntryAsync(string.Empty, "entryName")); + } + } + + [Fact] + public async Task EntryName_NullOrEmpty_Async() + { + using TempDirectory root = new TempDirectory(); + + string file1Name = "file1.txt"; + string file2Name = "file2.txt"; + + string file1Path = Path.Join(root.Path, file1Name); + string file2Path = Path.Join(root.Path, file2Name); + + File.Create(file1Path).Dispose(); + File.Create(file2Path).Dispose(); + + using MemoryStream archiveStream = new MemoryStream(); + TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Pax, leaveOpen: true); + await using (writer) + { + await writer.WriteEntryAsync(file1Path, null); + await writer.WriteEntryAsync(file2Path, string.Empty); + } + + archiveStream.Seek(0, SeekOrigin.Begin); + TarReader reader = new TarReader(archiveStream); + await using (reader) + { + TarEntry first = await reader.GetNextEntryAsync(); + Assert.NotNull(first); + Assert.Equal(file1Name, first.Name); + + TarEntry second = await reader.GetNextEntryAsync(); + Assert.NotNull(second); + Assert.Equal(file2Name, second.Name); + + Assert.Null(await reader.GetNextEntryAsync()); + } + } + + [Theory] + [InlineData(TarEntryFormat.V7)] + [InlineData(TarEntryFormat.Ustar)] + [InlineData(TarEntryFormat.Pax)] + [InlineData(TarEntryFormat.Gnu)] + public async Task Add_File_Async(TarEntryFormat format) + { + using TempDirectory root = new TempDirectory(); + string fileName = "file.txt"; + string filePath = Path.Join(root.Path, fileName); + string fileContents = "Hello world"; + + using (StreamWriter streamWriter = File.CreateText(filePath)) + { + streamWriter.Write(fileContents); + } + + using MemoryStream archive = new MemoryStream(); + TarWriter writer = new TarWriter(archive, format, leaveOpen: true); + await using (writer) + { + await writer.WriteEntryAsync(fileName: filePath, entryName: fileName); + } + + archive.Seek(0, SeekOrigin.Begin); + TarReader reader = new TarReader(archive); + await using (reader) + { + TarEntry entry = await reader.GetNextEntryAsync(); + Assert.NotNull(entry); + Assert.Equal(format, entry.Format); + Assert.Equal(fileName, entry.Name); + TarEntryType expectedEntryType = format is TarEntryFormat.V7 ? TarEntryType.V7RegularFile : TarEntryType.RegularFile; + Assert.Equal(expectedEntryType, entry.EntryType); + Assert.True(entry.Length > 0); + Assert.NotNull(entry.DataStream); + + entry.DataStream.Seek(0, SeekOrigin.Begin); + using StreamReader dataReader = new StreamReader(entry.DataStream); + string dataContents = dataReader.ReadLine(); + + Assert.Equal(fileContents, dataContents); + + VerifyPlatformSpecificMetadata(filePath, entry); + + Assert.Null(await reader.GetNextEntryAsync()); + } + } + + [Theory] + [InlineData(TarEntryFormat.V7, false)] + [InlineData(TarEntryFormat.V7, true)] + [InlineData(TarEntryFormat.Ustar, false)] + [InlineData(TarEntryFormat.Ustar, true)] + [InlineData(TarEntryFormat.Pax, false)] + [InlineData(TarEntryFormat.Pax, true)] + [InlineData(TarEntryFormat.Gnu, false)] + [InlineData(TarEntryFormat.Gnu, true)] + public async Task Add_Directory_Async(TarEntryFormat format, bool withContents) + { + using TempDirectory root = new TempDirectory(); + string dirName = "dir"; + string dirPath = Path.Join(root.Path, dirName); + Directory.CreateDirectory(dirPath); + + if (withContents) + { + // Add a file inside the directory, we need to ensure the contents + // of the directory are ignored when using AddFile + File.Create(Path.Join(dirPath, "file.txt")).Dispose(); + } + + using MemoryStream archive = new MemoryStream(); + TarWriter writer = new TarWriter(archive, format, leaveOpen: true); + await using (writer) + { + await writer.WriteEntryAsync(fileName: dirPath, entryName: dirName); + } + + archive.Seek(0, SeekOrigin.Begin); + TarReader reader = new TarReader(archive); + await using (reader) + { + TarEntry entry = await reader.GetNextEntryAsync(); + Assert.Equal(format, entry.Format); + + Assert.NotNull(entry); + Assert.Equal(dirName, entry.Name); + Assert.Equal(TarEntryType.Directory, entry.EntryType); + Assert.Null(entry.DataStream); + + VerifyPlatformSpecificMetadata(dirPath, entry); + + Assert.Null(await reader.GetNextEntryAsync()); // If the dir had contents, they should've been excluded + } + } + + [ConditionalTheory(typeof(MountHelper), nameof(MountHelper.CanCreateSymbolicLinks))] + [InlineData(TarEntryFormat.V7, false)] + [InlineData(TarEntryFormat.V7, true)] + [InlineData(TarEntryFormat.Ustar, false)] + [InlineData(TarEntryFormat.Ustar, true)] + [InlineData(TarEntryFormat.Pax, false)] + [InlineData(TarEntryFormat.Pax, true)] + [InlineData(TarEntryFormat.Gnu, false)] + [InlineData(TarEntryFormat.Gnu, true)] + public async Task Add_SymbolicLink_Async(TarEntryFormat format, bool createTarget) + { + using TempDirectory root = new TempDirectory(); + string targetName = "file.txt"; + string linkName = "link.txt"; + string targetPath = Path.Join(root.Path, targetName); + string linkPath = Path.Join(root.Path, linkName); + + if (createTarget) + { + File.Create(targetPath).Dispose(); + } + + FileInfo linkInfo = new FileInfo(linkPath); + linkInfo.CreateAsSymbolicLink(targetName); + + using MemoryStream archive = new MemoryStream(); + TarWriter writer = new TarWriter(archive, format, leaveOpen: true); + await using (writer) + { + await writer.WriteEntryAsync(fileName: linkPath, entryName: linkName); + } + + archive.Seek(0, SeekOrigin.Begin); + TarReader reader = new TarReader(archive); + await using (reader) + { + TarEntry entry = await reader.GetNextEntryAsync(); + Assert.Equal(format, entry.Format); + + Assert.NotNull(entry); + Assert.Equal(linkName, entry.Name); + Assert.Equal(targetName, entry.LinkName); + Assert.Equal(TarEntryType.SymbolicLink, entry.EntryType); + Assert.Null(entry.DataStream); + + VerifyPlatformSpecificMetadata(linkPath, entry); + + Assert.Null(await reader.GetNextEntryAsync()); + } + } + } +} 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 new file mode 100644 index 00000000000000..4a8bb141fbf870 --- /dev/null +++ b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.Tests.cs @@ -0,0 +1,319 @@ +// 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.Threading.Tasks; +using Xunit; + +namespace System.Formats.Tar.Tests +{ + // Tests that are independent of the archive format. + public class TarWriter_WriteEntryAsync_Tests : TarWriter_WriteEntry_Base + { + [Fact] + public async Task WriteEntry_AfterDispose_Throws_Async() + { + using MemoryStream archiveStream = new MemoryStream(); + TarWriter writer = new TarWriter(archiveStream); + await writer.DisposeAsync(); + + PaxTarEntry entry = new PaxTarEntry(TarEntryType.RegularFile, InitialEntryName); + await Assert.ThrowsAsync(async () => await writer.WriteEntryAsync(entry)); + } + + [Fact] + public async Task WriteEntry_FromUnseekableStream_AdvanceDataStream_WriteFromThatPosition_Async() + { + 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(); + + TarReader reader1 = new TarReader(unseekable); + await using (reader1) + { + TarEntry entry = await reader1.GetNextEntryAsync(); + Assert.NotNull(entry); + Assert.NotNull(entry.DataStream); + entry.DataStream.ReadByte(); // Advance one byte, now the expected string would be "ello file" + + TarWriter writer = new TarWriter(destination, TarEntryFormat.Ustar, leaveOpen: true); + await using (writer) + { + await writer.WriteEntryAsync(entry); + } + } + + destination.Seek(0, SeekOrigin.Begin); + TarReader reader2 = new TarReader(destination); + await using (reader2) + { + TarEntry entry = await reader2.GetNextEntryAsync(); + Assert.NotNull(entry); + Assert.NotNull(entry.DataStream); + + using (StreamReader streamReader = new StreamReader(entry.DataStream, leaveOpen: true)) + { + string contents = streamReader.ReadLine(); + Assert.Equal("ello file", contents); + } + } + } + + [Theory] + [InlineData(TarEntryFormat.V7)] + [InlineData(TarEntryFormat.Ustar)] + [InlineData(TarEntryFormat.Pax)] + [InlineData(TarEntryFormat.Gnu)] + public async Task WriteEntry_RespectDefaultWriterFormat_Async(TarEntryFormat expectedFormat) + { + using TempDirectory root = new TempDirectory(); + + string path = Path.Join(root.Path, "file.txt"); + File.Create(path).Dispose(); + + using MemoryStream archiveStream = new MemoryStream(); + TarWriter writer = new TarWriter(archiveStream, expectedFormat, leaveOpen: true); + await using (writer) + { + await writer.WriteEntryAsync(path, "file.txt"); + } + + archiveStream.Position = 0; + TarReader reader = new TarReader(archiveStream, leaveOpen: false); + await using (reader) + { + TarEntry entry = await reader.GetNextEntryAsync(); + Assert.Equal(expectedFormat, entry.Format); + + Type expectedType = GetTypeForFormat(expectedFormat); + + Assert.Equal(expectedType, entry.GetType()); + } + } + + [Theory] + [InlineData(TarEntryFormat.Ustar)] + [InlineData(TarEntryFormat.Pax)] + [InlineData(TarEntryFormat.Gnu)] + public async Task Write_RegularFileEntry_In_V7Writer_Async(TarEntryFormat entryFormat) + { + using MemoryStream archive = new MemoryStream(); + TarWriter writer = new TarWriter(archive, format: TarEntryFormat.V7, leaveOpen: true); + await using (writer) + { + TarEntry entry = entryFormat switch + { + TarEntryFormat.Ustar => new UstarTarEntry(TarEntryType.RegularFile, InitialEntryName), + TarEntryFormat.Pax => new PaxTarEntry(TarEntryType.RegularFile, InitialEntryName), + TarEntryFormat.Gnu => new GnuTarEntry(TarEntryType.RegularFile, InitialEntryName), + _ => throw new FormatException($"Unexpected format: {entryFormat}") + }; + + // Should be written in the format of the entry + await writer.WriteEntryAsync(entry); + } + + archive.Seek(0, SeekOrigin.Begin); + TarReader reader = new TarReader(archive); + await using (reader) + { + TarEntry entry = await reader.GetNextEntryAsync(); + Assert.NotNull(entry); + Assert.Equal(entryFormat, entry.Format); + + switch (entryFormat) + { + case TarEntryFormat.Ustar: + Assert.True(entry is UstarTarEntry); + break; + case TarEntryFormat.Pax: + Assert.True(entry is PaxTarEntry); + break; + case TarEntryFormat.Gnu: + Assert.True(entry is GnuTarEntry); + break; + } + + Assert.Null(await reader.GetNextEntryAsync()); + } + } + + [Theory] + [InlineData(TarEntryFormat.Ustar)] + [InlineData(TarEntryFormat.Pax)] + [InlineData(TarEntryFormat.Gnu)] + public async Task Write_V7RegularFileEntry_In_OtherFormatsWriter_Async(TarEntryFormat writerFormat) + { + using MemoryStream archive = new MemoryStream(); + TarWriter writer = new TarWriter(archive, format: writerFormat, leaveOpen: true); + await using (writer) + { + V7TarEntry entry = new V7TarEntry(TarEntryType.V7RegularFile, InitialEntryName); + + // Should be written in the format of the entry + await writer.WriteEntryAsync(entry); + } + + archive.Seek(0, SeekOrigin.Begin); + TarReader reader = new TarReader(archive); + await using (reader) + { + TarEntry entry = await reader.GetNextEntryAsync(); + Assert.NotNull(entry); + Assert.Equal(TarEntryFormat.V7, entry.Format); + Assert.True(entry is V7TarEntry); + + Assert.Null(await reader.GetNextEntryAsync()); + } + } + + [Theory] + [InlineData(TarEntryFormat.V7)] + [InlineData(TarEntryFormat.Ustar)] + [InlineData(TarEntryFormat.Pax)] + [InlineData(TarEntryFormat.Gnu)] + public async Task ReadAndWriteMultipleGlobalExtendedAttributesEntries_Async(TarEntryFormat format) + { + Dictionary attrs = new Dictionary() + { + { "hello", "world" }, + { "dotnet", "runtime" } + }; + + using MemoryStream archiveStream = new MemoryStream(); + TarWriter writer = new TarWriter(archiveStream, leaveOpen: true); + await using (writer) + { + PaxGlobalExtendedAttributesTarEntry gea1 = new PaxGlobalExtendedAttributesTarEntry(attrs); + await writer.WriteEntryAsync(gea1); + + TarEntry entry1 = InvokeTarEntryCreationConstructor(format, TarEntryType.Directory, "dir1"); + await writer.WriteEntryAsync(entry1); + + PaxGlobalExtendedAttributesTarEntry gea2 = new PaxGlobalExtendedAttributesTarEntry(attrs); + await writer.WriteEntryAsync(gea2); + + TarEntry entry2 = InvokeTarEntryCreationConstructor(format, TarEntryType.Directory, "dir2"); + await writer.WriteEntryAsync(entry2); + } + + archiveStream.Position = 0; + + TarReader reader = new TarReader(archiveStream, leaveOpen: false); + await using (reader) + { + VerifyGlobalExtendedAttributesEntry(await reader.GetNextEntryAsync(), attrs); + VerifyDirectory(await reader.GetNextEntryAsync(), format, "dir1"); + VerifyGlobalExtendedAttributesEntry(await reader.GetNextEntryAsync(), attrs); + VerifyDirectory(await reader.GetNextEntryAsync(), format, "dir2"); + Assert.Null(await reader.GetNextEntryAsync()); + } + } + + // Y2K38 will happen one second after "2038/19/01 03:14:07 +00:00". This timestamp represents the seconds since the Unix epoch with a + // value of int.MaxValue: 2,147,483,647. + // The fixed size fields for mtime, atime and ctime can fit 12 ASCII characters, but the last character is reserved for an ASCII space. + // All our entry types should survive the Epochalypse because we internally use long to represent the seconds since Unix epoch, not int. + // So if the max allowed value is 77,777,777,777 in octal, then the max allowed seconds since the Unix epoch are 8,589,934,591, which + // is way past int MaxValue, but still within the long limits. That number represents the date "2242/16/03 12:56:32 +00:00". + [Theory] + [InlineData(TarEntryFormat.V7)] + [InlineData(TarEntryFormat.Ustar)] + [InlineData(TarEntryFormat.Gnu)] + public async Task WriteTimestampsBeyondEpochalypse_Async(TarEntryFormat format) + { + DateTimeOffset epochalypse = new DateTimeOffset(2038, 1, 19, 3, 14, 8, TimeSpan.Zero); // One second past Y2K38 + TarEntry entry = InvokeTarEntryCreationConstructor(format, TarEntryType.Directory, "dir"); + + entry.ModificationTime = epochalypse; + Assert.Equal(epochalypse, entry.ModificationTime); + + if (entry is GnuTarEntry gnuEntry) + { + gnuEntry.AccessTime = epochalypse; + Assert.Equal(epochalypse, gnuEntry.AccessTime); + + gnuEntry.ChangeTime = epochalypse; + Assert.Equal(epochalypse, gnuEntry.ChangeTime); + } + + using MemoryStream archiveStream = new MemoryStream(); + TarWriter writer = new TarWriter(archiveStream, leaveOpen: true); + await using (writer) + { + await writer.WriteEntryAsync(entry); + } + + archiveStream.Position = 0; + TarReader reader = new TarReader(archiveStream); + await using (reader) + { + TarEntry readEntry = await reader.GetNextEntryAsync(); + Assert.NotNull(readEntry); + + Assert.Equal(epochalypse, readEntry.ModificationTime); + + if (readEntry is GnuTarEntry gnuReadEntry) + { + Assert.Equal(epochalypse, gnuReadEntry.AccessTime); + Assert.Equal(epochalypse, gnuReadEntry.ChangeTime); + } + } + } + + // The fixed size fields for mtime, atime and ctime can fit 12 ASCII characters, but the last character is reserved for an ASCII space. + // We internally use long to represent the seconds since Unix epoch, not int. + // If the max allowed value is 77,777,777,777 in octal, then the max allowed seconds since the Unix epoch are 8,589,934,591, + // which represents the date "2242/03/16 12:56:32 +00:00". + // V7, Ustar and GNU would not survive after this date because they only have the fixed size fields to store timestamps. + [Theory] + [InlineData(TarEntryFormat.V7)] + [InlineData(TarEntryFormat.Ustar)] + [InlineData(TarEntryFormat.Gnu)] + public async Task WriteTimestampsBeyondOctalLimit_Async(TarEntryFormat format) + { + DateTimeOffset overLimitTimestamp = new DateTimeOffset(2242, 3, 16, 12, 56, 33, TimeSpan.Zero); // One second past the octal limit + + TarEntry entry = InvokeTarEntryCreationConstructor(format, TarEntryType.Directory, "dir"); + + // Before writing the entry, the timestamps should have no issue + entry.ModificationTime = overLimitTimestamp; + Assert.Equal(overLimitTimestamp, entry.ModificationTime); + + if (entry is GnuTarEntry gnuEntry) + { + gnuEntry.AccessTime = overLimitTimestamp; + Assert.Equal(overLimitTimestamp, gnuEntry.AccessTime); + + gnuEntry.ChangeTime = overLimitTimestamp; + Assert.Equal(overLimitTimestamp, gnuEntry.ChangeTime); + } + + using MemoryStream archiveStream = new MemoryStream(); + TarWriter writer = new TarWriter(archiveStream, leaveOpen: true); + await using (writer) + { + await writer.WriteEntryAsync(entry); + } + + archiveStream.Position = 0; + TarReader reader = new TarReader(archiveStream); + await using (reader) + { + TarEntry readEntry = await reader.GetNextEntryAsync(); + Assert.NotNull(readEntry); + + // The timestamps get stored as '{1970-01-01 12:00:00 AM +00:00}' due to the +1 overflow + Assert.NotEqual(overLimitTimestamp, readEntry.ModificationTime); + + if (readEntry is GnuTarEntry gnuReadEntry) + { + Assert.NotEqual(overLimitTimestamp, gnuReadEntry.AccessTime); + Assert.NotEqual(overLimitTimestamp, gnuReadEntry.ChangeTime); + } + } + } + } +} From 39a456077210b770d0a60d6c61fee3ca5df70be0 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Tue, 28 Jun 2022 00:29:30 -0700 Subject: [PATCH 48/75] Fix test faiest failure - Windows test accidentally being run on Linux --- .../System.Formats.Tar/tests/System.Formats.Tar.Tests.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/System.Formats.Tar/tests/System.Formats.Tar.Tests.csproj b/src/libraries/System.Formats.Tar/tests/System.Formats.Tar.Tests.csproj index 32cce09636b392..4a58fe0a5c56c9 100644 --- a/src/libraries/System.Formats.Tar/tests/System.Formats.Tar.Tests.csproj +++ b/src/libraries/System.Formats.Tar/tests/System.Formats.Tar.Tests.csproj @@ -12,7 +12,6 @@ - @@ -68,6 +67,7 @@ + From fb60a8cfa2ceb6856c615ff9646f354c86e8e9b3 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Tue, 28 Jun 2022 00:45:12 -0700 Subject: [PATCH 49/75] Set Options to Asynchornous in TarWriter internal method in Windows. For Unix, add missing FileStreamOptions. --- .../src/System/Formats/Tar/TarWriter.Unix.cs | 11 ++++++++++- .../src/System/Formats/Tar/TarWriter.Windows.cs | 7 ++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarWriter.Unix.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarWriter.Unix.cs index 8a0990e0873cdc..c596eb9349518e 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarWriter.Unix.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarWriter.Unix.cs @@ -180,7 +180,16 @@ private async Task ReadFileFromDiskAndWriteToArchiveStreamAsEntryAsync(string fu if (entry.EntryType is TarEntryType.RegularFile or TarEntryType.V7RegularFile) { Debug.Assert(entry._header._dataStream == null); - entry._header._dataStream = File.OpenRead(fullPath); + + FileStreamOptions options = new() + { + Mode = FileMode.Open, + Access = FileAccess.Read, + Share = FileShare.Read, + Options = FileOptions.Asynchronous + }; + + entry._header._dataStream = new FileStream(fullPath, options); } await WriteEntryAsync(entry, cancellationToken).ConfigureAwait(false); diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarWriter.Windows.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarWriter.Windows.cs index 8c83666247d287..345ae7ab7009a7 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarWriter.Windows.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarWriter.Windows.cs @@ -127,16 +127,17 @@ private async Task ReadFileFromDiskAndWriteToArchiveStreamAsEntryAsync(string fu if (entry.EntryType is TarEntryType.RegularFile or TarEntryType.V7RegularFile) { + Debug.Assert(entry._header._dataStream == null); + FileStreamOptions options = new() { Mode = FileMode.Open, Access = FileAccess.Read, Share = FileShare.Read, - Options = FileOptions.None + Options = FileOptions.Asynchronous }; - Debug.Assert(entry._header._dataStream == null); - entry._header._dataStream = File.Open(fullPath, options); + entry._header._dataStream = new FileStream(fullPath, options); } await WriteEntryAsync(entry, cancellationToken).ConfigureAwait(false); From 331a763b310dfa63b5b3f78bd4c457b38ad93889 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Tue, 28 Jun 2022 16:55:50 -0700 Subject: [PATCH 50/75] Simplify AdvanceStream* helper methods with Math.Min and ReadExactly*. --- .../src/System/Formats/Tar/TarHelpers.cs | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) 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 6395234bc5d612..bbc7bc7ae93ff0 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 @@ -35,14 +35,11 @@ internal static void AdvanceStream(Stream archiveStream, long bytesToDiscard) } else if (bytesToDiscard > 0) { - byte[] buffer = ArrayPool.Shared.Rent(minimumLength: MaxBufferLength); + byte[] buffer = ArrayPool.Shared.Rent(minimumLength: (int)Math.Min(MaxBufferLength, bytesToDiscard)); while (bytesToDiscard > 0) { int currentLengthToRead = (int)Math.Min(MaxBufferLength, bytesToDiscard); - if (archiveStream.Read(buffer.AsSpan(0, currentLengthToRead)) != currentLengthToRead) - { - throw new EndOfStreamException(); - } + archiveStream.ReadExactly(buffer.AsSpan(0, currentLengthToRead)); bytesToDiscard -= currentLengthToRead; } ArrayPool.Shared.Return(buffer); @@ -58,15 +55,11 @@ internal static async ValueTask AdvanceStreamAsync(Stream archiveStream, long by } else if (bytesToDiscard > 0) { - byte[] buffer = ArrayPool.Shared.Rent(minimumLength: MaxBufferLength); + byte[] buffer = ArrayPool.Shared.Rent(minimumLength: (int)Math.Min(MaxBufferLength, bytesToDiscard)); while (bytesToDiscard > 0) { int currentLengthToRead = (int)Math.Min(MaxBufferLength, bytesToDiscard); - int bytesRead = await archiveStream.ReadAsync(buffer.AsMemory(0, currentLengthToRead), cancellationToken).ConfigureAwait(false); - if (bytesRead != currentLengthToRead) - { - throw new EndOfStreamException(); - } + await archiveStream.ReadExactlyAsync(buffer, 0, currentLengthToRead, cancellationToken).ConfigureAwait(false); bytesToDiscard -= currentLengthToRead; } ArrayPool.Shared.Return(buffer); From 188312df4c571c23ab3d13272572b90fd4a67237 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Tue, 28 Jun 2022 17:01:46 -0700 Subject: [PATCH 51/75] Simplify CopyBytes* helper methods with ReadExactly*. --- .../src/System/Formats/Tar/TarHelpers.cs | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) 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 bbc7bc7ae93ff0..c2cc6c3a0f1362 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 @@ -73,10 +73,7 @@ internal static void CopyBytes(Stream origin, Stream destination, long bytesToCo while (bytesToCopy > 0) { int currentLengthToRead = (int)Math.Min(MaxBufferLength, bytesToCopy); - if (origin.Read(buffer.AsSpan(0, currentLengthToRead)) != currentLengthToRead) - { - throw new EndOfStreamException(); - } + origin.ReadExactly(buffer.AsSpan(0, currentLengthToRead)); destination.Write(buffer.AsSpan(0, currentLengthToRead)); bytesToCopy -= currentLengthToRead; } @@ -91,11 +88,7 @@ internal static async ValueTask CopyBytesAsync(Stream origin, Stream destination { int currentLengthToRead = (int)Math.Min(MaxBufferLength, bytesToCopy); Memory memory = buffer.AsMemory(0, currentLengthToRead); - int bytesRead = await origin.ReadAsync(memory, cancellationToken).ConfigureAwait(false); - if (bytesRead != currentLengthToRead) - { - throw new EndOfStreamException(); - } + await origin.ReadExactlyAsync(buffer, 0, currentLengthToRead, cancellationToken).ConfigureAwait(false); await destination.WriteAsync(memory, cancellationToken).ConfigureAwait(false); bytesToCopy -= currentLengthToRead; } From 216937f962a836a03be03c732f0a8ac4e690cfee Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Tue, 28 Jun 2022 17:34:08 -0700 Subject: [PATCH 52/75] Add CancellationToken handling. --- .../Formats/Tar/SeekableSubReadStream.cs | 4 +++ .../src/System/Formats/Tar/SubReadStream.cs | 10 ++++++++ .../src/System/Formats/Tar/TarEntry.cs | 15 +++++++++++ .../src/System/Formats/Tar/TarFile.cs | 24 ++++++++++++++++++ .../src/System/Formats/Tar/TarHeader.Read.cs | 8 ++++++ .../src/System/Formats/Tar/TarHeader.Write.cs | 25 +++++++++++++++++++ .../src/System/Formats/Tar/TarHelpers.cs | 6 +++++ .../src/System/Formats/Tar/TarReader.cs | 10 ++++++++ .../src/System/Formats/Tar/TarWriter.Unix.cs | 2 ++ .../System/Formats/Tar/TarWriter.Windows.cs | 2 ++ .../src/System/Formats/Tar/TarWriter.cs | 11 ++++++++ 11 files changed, 117 insertions(+) diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/SeekableSubReadStream.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/SeekableSubReadStream.cs index 391cde00bc66ac..fc840c42bea732 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/SeekableSubReadStream.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/SeekableSubReadStream.cs @@ -71,6 +71,10 @@ public override int Read(Span destination) public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) { + if (cancellationToken.IsCancellationRequested) + { + return ValueTask.FromCanceled(cancellationToken); + } ThrowIfDisposed(); VerifyPositionInSuperStream(); return ReadAsyncCore(buffer, cancellationToken); diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/SubReadStream.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/SubReadStream.cs index e7c5d29d96c96f..a7997466eca905 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/SubReadStream.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/SubReadStream.cs @@ -132,12 +132,20 @@ public override int ReadByte() public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } ValidateBufferArguments(buffer, offset, count); return ReadAsync(new Memory(buffer, offset, count), cancellationToken).AsTask(); } public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) { + if (cancellationToken.IsCancellationRequested) + { + return ValueTask.FromCanceled(cancellationToken); + } ThrowIfDisposed(); ThrowIfBeyondEndOfStream(); return ReadAsyncCore(buffer, cancellationToken); @@ -147,6 +155,8 @@ protected async ValueTask ReadAsyncCore(Memory buffer, CancellationTo { Debug.Assert(!_hasReachedEnd); + cancellationToken.ThrowIfCancellationRequested(); + if (_positionInSuperStream > _endInSuperStream - buffer.Length) { buffer = buffer.Slice(0, (int)(_endInSuperStream - _positionInSuperStream)); diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs index 529c3d3f8044c3..d1ff68b024e6ab 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs @@ -249,6 +249,10 @@ public void ExtractToFile(string destinationFileName, bool overwrite) /// Operation not permitted due to insufficient permissions. public Task ExtractToFileAsync(string destinationFileName, bool overwrite, CancellationToken cancellationToken = default) { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } ArgumentException.ThrowIfNullOrEmpty(destinationFileName); if (EntryType is TarEntryType.SymbolicLink or TarEntryType.HardLink or TarEntryType.GlobalExtendedAttributes) { @@ -354,6 +358,11 @@ internal Task ExtractRelativeToDirectoryAsync(string destinationDirectoryPath, b Debug.Assert(!string.IsNullOrEmpty(destinationDirectoryPath)); Debug.Assert(Path.IsPathFullyQualified(destinationDirectoryPath)); + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + destinationDirectoryPath = Path.TrimEndingDirectorySeparator(destinationDirectoryPath); string? fileDestinationPath = GetSanitizedFullPath(destinationDirectoryPath, Name); @@ -417,6 +426,10 @@ private void ExtractToFileInternal(string filePath, string? linkTargetPath, bool // Asynchronously extracts the current entry into the filesystem, regardless of the entry type. private Task ExtractToFileInternalAsync(string filePath, string? linkTargetPath, bool overwrite, CancellationToken cancellationToken) { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } VerifyPathsForEntryType(filePath, linkTargetPath, overwrite); if (EntryType is TarEntryType.RegularFile or TarEntryType.V7RegularFile or TarEntryType.ContiguousFile) @@ -571,6 +584,8 @@ private async Task ExtractAsRegularFileAsync(string destinationFileName, Cancell { Debug.Assert(!Path.Exists(destinationFileName)); + cancellationToken.ThrowIfCancellationRequested(); + FileStreamOptions fileStreamOptions = new() { Access = FileAccess.Write, diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarFile.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarFile.cs index c39216c4effbc6..2276eee7538160 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarFile.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarFile.cs @@ -51,6 +51,10 @@ public static void CreateFromDirectory(string sourceDirectoryName, Stream destin /// A task that represents the asynchronous creation operation. public static Task CreateFromDirectoryAsync(string sourceDirectoryName, Stream destination, bool includeBaseDirectory, CancellationToken cancellationToken = default) { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } ArgumentException.ThrowIfNullOrEmpty(sourceDirectoryName); ArgumentNullException.ThrowIfNull(destination); @@ -106,6 +110,10 @@ public static void CreateFromDirectory(string sourceDirectoryName, string destin /// A task that represents the asynchronous creation operation. public static Task CreateFromDirectoryAsync(string sourceDirectoryName, string destinationFileName, bool includeBaseDirectory, CancellationToken cancellationToken = default) { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } ArgumentException.ThrowIfNullOrEmpty(sourceDirectoryName); ArgumentException.ThrowIfNullOrEmpty(destinationFileName); @@ -165,6 +173,10 @@ public static void ExtractToDirectory(Stream source, string destinationDirectory /// Operation not permitted due to insufficient permissions. public static Task ExtractToDirectoryAsync(Stream source, string destinationDirectoryName, bool overwriteFiles, CancellationToken cancellationToken = default) { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } ArgumentNullException.ThrowIfNull(source); ArgumentException.ThrowIfNullOrEmpty(destinationDirectoryName); @@ -230,6 +242,10 @@ public static void ExtractToDirectory(string sourceFileName, string destinationD /// Operation not permitted due to insufficient permissions. public static Task ExtractToDirectoryAsync(string sourceFileName, string destinationDirectoryName, bool overwriteFiles, CancellationToken cancellationToken = default) { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } ArgumentException.ThrowIfNullOrEmpty(sourceFileName); ArgumentException.ThrowIfNullOrEmpty(destinationDirectoryName); @@ -310,6 +326,8 @@ private static async Task CreateFromDirectoryInternalAsync(string sourceDirector Debug.Assert(!string.IsNullOrEmpty(sourceDirectoryName)); Debug.Assert(!string.IsNullOrEmpty(destinationFileName)); + cancellationToken.ThrowIfCancellationRequested(); + FileStreamOptions options = new() { Access = FileAccess.Write, @@ -333,6 +351,8 @@ private static async Task CreateFromDirectoryInternalAsync(string sourceDirector Debug.Assert(Path.IsPathFullyQualified(sourceDirectoryName)); Debug.Assert(destination.CanWrite); + cancellationToken.ThrowIfCancellationRequested(); + TarWriter writer = new TarWriter(destination, TarEntryFormat.Pax, leaveOpen); await using (writer.ConfigureAwait(false)) { @@ -406,6 +426,8 @@ private static async Task ExtractToDirectoryInternalAsync(string sourceFileName, Debug.Assert(!string.IsNullOrEmpty(sourceFileName)); Debug.Assert(!string.IsNullOrEmpty(destinationDirectoryName)); + cancellationToken.ThrowIfCancellationRequested(); + FileStreamOptions options = new() { Access = FileAccess.Read, @@ -428,6 +450,8 @@ private static async Task ExtractToDirectoryInternalAsync(Stream source, string Debug.Assert(Path.IsPathFullyQualified(destinationDirectoryPath)); Debug.Assert(source.CanRead); + cancellationToken.ThrowIfCancellationRequested(); + TarReader reader = new TarReader(source, leaveOpen); await using (reader.ConfigureAwait(false)) { 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 7810a34b1440e0..f81a188d86a3a8 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 @@ -76,6 +76,8 @@ internal bool TryGetNextHeader(Stream archiveStream, bool copyData) // Returns true if all the attributes were read successfully, false otherwise. internal static async ValueTask<(bool, TarHeader)> TryGetNextHeaderAsync(Stream archiveStream, bool copyData, TarEntryFormat initialFormat, CancellationToken cancellationToken) { + cancellationToken.ThrowIfCancellationRequested(); + TarHeader header = default; header._format = initialFormat; @@ -370,6 +372,8 @@ private void ProcessDataBlock(Stream archiveStream, bool copyData) // Otherwise, it returns an unseekable wrapper stream. private static async ValueTask GetDataStreamAsync(Stream archiveStream, bool copyData, long size, CancellationToken cancellationToken) { + cancellationToken.ThrowIfCancellationRequested(); + if (size == 0) { return new MemoryStream(); @@ -599,6 +603,8 @@ private static async ValueTask> ReadExtendedAttribute { Debug.Assert(entryType is TarEntryType.ExtendedAttributes or TarEntryType.GlobalExtendedAttributes); + cancellationToken.ThrowIfCancellationRequested(); + // It is not expected that the extended attributes data section will be longer than int.MaxValue, considering // 4096 is a common max path length, and also the size field is 12 bytes long, which is under int.MaxValue. if (size > int.MaxValue) @@ -672,6 +678,8 @@ private void ReadGnuLongPathDataBlock(Stream archiveStream) { Debug.Assert(entryType is TarEntryType.LongLink or TarEntryType.LongPath); + cancellationToken.ThrowIfCancellationRequested(); + if (size == 0) { return null; 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 ac92da168391f4..7c0b9d40348d7d 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 @@ -47,6 +47,8 @@ internal void WriteAsV7(Stream archiveStream, Span buffer) // 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) { + cancellationToken.ThrowIfCancellationRequested(); + long actualLength = GetTotalDataBytesToWrite(); TarEntryType actualEntryType = GetCorrectTypeFlagForFormat(TarEntryFormat.V7); @@ -87,6 +89,8 @@ internal void WriteAsUstar(Stream archiveStream, Span buffer) // 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) { + cancellationToken.ThrowIfCancellationRequested(); + long actualLength = GetTotalDataBytesToWrite(); TarEntryType actualEntryType = GetCorrectTypeFlagForFormat(TarEntryFormat.Ustar); @@ -121,6 +125,11 @@ internal Task WriteAsPaxGlobalExtendedAttributesAsync(Stream archiveStream, { Debug.Assert(_typeFlag is TarEntryType.GlobalExtendedAttributes); + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + _name = GenerateGlobalExtendedAttributeName(globalExtendedAttributesEntryNumber); _extendedAttributes ??= new Dictionary(); return WriteAsPaxExtendedAttributesAsync(archiveStream, buffer, _extendedAttributes, isGea: true, cancellationToken); @@ -149,6 +158,8 @@ internal void WriteAsPax(Stream archiveStream, Span buffer) // Makes sure to add the preceding exteded attributes entry before the actual entry. internal async Task WriteAsPaxAsync(Stream archiveStream, Memory buffer, CancellationToken cancellationToken) { + cancellationToken.ThrowIfCancellationRequested(); + // First, we write the preceding extended attributes header TarHeader extendedAttributesHeader = default; // Fill the current header's dict @@ -190,6 +201,8 @@ internal void WriteAsGnu(Stream archiveStream, Span buffer) // Makes sure to add the preceding LongLink and/or LongPath entries if necessary, before the actual entry. internal async Task WriteAsGnuAsync(Stream archiveStream, Memory buffer, CancellationToken cancellationToken) { + cancellationToken.ThrowIfCancellationRequested(); + // First, we determine if we need a preceding LongLink, and write it if needed if (_linkName.Length > FieldLengths.LinkName) { @@ -238,6 +251,8 @@ private static async Task GetGnuLongMetadataHeaderAsync(TarEntryType Debug.Assert((entryType is TarEntryType.LongPath && longText.Length > FieldLengths.Name) || (entryType is TarEntryType.LongLink && longText.Length > FieldLengths.LinkName)); + cancellationToken.ThrowIfCancellationRequested(); + TarHeader longMetadataHeader = default; longMetadataHeader._name = GnuLongMetadataName; // Same name for both longpath or longlink @@ -283,6 +298,8 @@ internal void WriteAsGnuInternal(Stream archiveStream, Span buffer) // 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(); + // Unused GNU fields: offset, longnames, unused, sparse struct, isextended and realsize // If this header came from another archive, it will have a value // If it was constructed by the user, it will be an empty array @@ -330,6 +347,8 @@ private void WriteAsPaxExtendedAttributes(Stream archiveStream, Span buffe // Asynchronously writes the current header as a PAX Extended Attributes entry into the archive stream and returns the value of the final checksum. private async Task WriteAsPaxExtendedAttributesAsync(Stream archiveStream, Memory buffer, IEnumerable> extendedAttributes, bool isGea, CancellationToken cancellationToken) { + cancellationToken.ThrowIfCancellationRequested(); + // The ustar fields (uid, gid, linkName, uname, gname, devmajor, devminor) do not get written. // The mode gets the default value. _name = GenerateExtendedAttributeName(); @@ -371,6 +390,8 @@ private void WriteAsPaxInternal(Stream archiveStream, Span buffer) // 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(); + long actualLength = GetTotalDataBytesToWrite(); TarEntryType actualEntryType = GetCorrectTypeFlagForFormat(TarEntryFormat.Pax); @@ -577,6 +598,8 @@ private static void WriteData(Stream archiveStream, Stream dataStream, long actu // Asynchronously writes the current header's data stream into the archive stream. private static async Task WriteDataAsync(Stream archiveStream, Stream dataStream, long actualLength, CancellationToken cancellationToken) { + cancellationToken.ThrowIfCancellationRequested(); + await dataStream.CopyToAsync(archiveStream, cancellationToken).ConfigureAwait(false); // The data gets copied from the current position int paddingAfterData = TarHelpers.CalculatePadding(actualLength); await archiveStream.WriteAsync(new byte[paddingAfterData], cancellationToken).ConfigureAwait(false); @@ -601,6 +624,8 @@ private static async Task WriteDataAsync(Stream archiveStream, Stream dataStream // Asynchronously dumps into the archive stream an extended attribute entry containing metadata of the entry it precedes. private static async Task GenerateExtendedAttributesDataStreamAsync(IEnumerable> extendedAttributes, CancellationToken cancellationToken) { + cancellationToken.ThrowIfCancellationRequested(); + MemoryStream? dataStream = null; foreach ((string attribute, string value) in extendedAttributes) { 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 c2cc6c3a0f1362..608a8cb8c97404 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 @@ -49,6 +49,8 @@ internal static void AdvanceStream(Stream archiveStream, long bytesToDiscard) // Asynchronously helps advance the stream a total number of bytes larger than int.MaxValue. internal static async ValueTask AdvanceStreamAsync(Stream archiveStream, long bytesToDiscard, CancellationToken cancellationToken) { + cancellationToken.ThrowIfCancellationRequested(); + if (archiveStream.CanSeek) { archiveStream.Position += bytesToDiscard; @@ -83,6 +85,8 @@ internal static void CopyBytes(Stream origin, Stream destination, long bytesToCo // Asynchronously helps copy a specific number of bytes from one stream into another. internal static async ValueTask CopyBytesAsync(Stream origin, Stream destination, long bytesToCopy, CancellationToken cancellationToken) { + cancellationToken.ThrowIfCancellationRequested(); + byte[] buffer = ArrayPool.Shared.Rent(minimumLength: MaxBufferLength); while (bytesToCopy > 0) { @@ -259,6 +263,8 @@ internal static int SkipBlockAlignmentPadding(Stream archiveStream, long size) // Asynchronously skip them and set the stream position to the first byte of the next entry. internal static async ValueTask SkipBlockAlignmentPaddingAsync(Stream archiveStream, long size, CancellationToken cancellationToken) { + cancellationToken.ThrowIfCancellationRequested(); + int bytesToSkip = CalculatePadding(size); await AdvanceStreamAsync(archiveStream, bytesToSkip, cancellationToken).ConfigureAwait(false); return bytesToSkip; 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 305f27b69d3b64..95fc7c5f1f1c06 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 @@ -143,6 +143,8 @@ public async ValueTask DisposeAsync() /// An I/O problem occurred. public async ValueTask GetNextEntryAsync(bool copyData = false, CancellationToken cancellationToken = default) { + cancellationToken.ThrowIfCancellationRequested(); + if (_reachedEndMarkers) { // Avoid advancing the stream if we already found the end of the archive. @@ -228,6 +230,8 @@ internal void AdvanceDataStreamIfNeeded() // Asynchronously moves the underlying archive stream position pointer to the beginning of the next header. internal async ValueTask AdvanceDataStreamIfNeededAsync(CancellationToken cancellationToken) { + cancellationToken.ThrowIfCancellationRequested(); + if (_previouslyReadEntry == null) { return; @@ -369,6 +373,8 @@ private bool TryGetNextEntryHeader(out TarHeader header, bool copyData) // Metadata typeflag entries get handled internally by this method until a valid header entry can be returned. private async ValueTask<(bool, TarHeader)> TryGetNextEntryHeaderAsync(bool copyData, CancellationToken cancellationToken) { + cancellationToken.ThrowIfCancellationRequested(); + Debug.Assert(!_reachedEndMarkers); (bool result, TarHeader header) = await TarHeader.TryGetNextHeaderAsync(_archiveStream, copyData, TarEntryFormat.Unknown, cancellationToken).ConfigureAwait(false); @@ -448,6 +454,8 @@ TarEntryType.LongLink or // and returns the actual entry with the processed extended attributes saved in the _extendedAttributes dictionary. private async ValueTask<(bool, TarHeader)> TryProcessExtendedAttributesHeaderAsync(TarHeader extendedAttributesHeader, bool copyData, CancellationToken cancellationToken) { + cancellationToken.ThrowIfCancellationRequested(); + // Now get the actual entry (bool result, TarHeader actualHeader) = await TarHeader.TryGetNextHeaderAsync(_archiveStream, copyData, TarEntryFormat.Pax, cancellationToken).ConfigureAwait(false); if (!result) @@ -560,6 +568,8 @@ private bool TryProcessGnuMetadataHeader(TarHeader header, bool copyData, out Ta // or the actual entry. Processes them all and returns the actual entry updating its path and/or linkpath fields as needed. private async ValueTask<(bool, TarHeader)> TryProcessGnuMetadataHeaderAsync(TarHeader header, bool copyData, CancellationToken cancellationToken) { + cancellationToken.ThrowIfCancellationRequested(); + // Get the second entry, which is the actual entry (bool result1, TarHeader secondHeader) = await TarHeader.TryGetNextHeaderAsync(_archiveStream, copyData, TarEntryFormat.Gnu, cancellationToken).ConfigureAwait(false); if (!result1) diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarWriter.Unix.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarWriter.Unix.cs index c596eb9349518e..75ff67e7252667 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarWriter.Unix.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarWriter.Unix.cs @@ -106,6 +106,8 @@ private void ReadFileFromDiskAndWriteToArchiveStreamAsEntry(string fullPath, str // Unix specific implementation of the method that reads an entry from disk and writes it into the archive stream. private async Task ReadFileFromDiskAndWriteToArchiveStreamAsEntryAsync(string fullPath, string entryName, CancellationToken cancellationToken) { + cancellationToken.ThrowIfCancellationRequested(); + Interop.Sys.FileStatus status = default; status.Mode = default; status.Dev = default; diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarWriter.Windows.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarWriter.Windows.cs index 345ae7ab7009a7..1529e12a15fbb6 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarWriter.Windows.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarWriter.Windows.cs @@ -83,6 +83,8 @@ private void ReadFileFromDiskAndWriteToArchiveStreamAsEntry(string fullPath, str // Windows specific implementation of the method that asynchronously reads an entry from disk and writes it into the archive stream. private async Task ReadFileFromDiskAndWriteToArchiveStreamAsEntryAsync(string fullPath, string entryName, CancellationToken cancellationToken) { + cancellationToken.ThrowIfCancellationRequested(); + TarEntryType entryType; FileAttributes attributes = File.GetAttributes(fullPath); 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 666716d0d83e24..7b5bce335cbc72 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 @@ -133,6 +133,10 @@ public void WriteEntry(string fileName, string? entryName) /// An I/O problem occurred. public Task WriteEntryAsync(string fileName, string? entryName, CancellationToken cancellationToken = default) { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } ThrowIfDisposed(); ArgumentException.ThrowIfNullOrEmpty(fileName); @@ -260,6 +264,10 @@ public void WriteEntry(TarEntry entry) /// An I/O problem occurred. public Task WriteEntryAsync(TarEntry entry, CancellationToken cancellationToken = default) { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } ThrowIfDisposed(); return WriteEntryAsyncInternal(entry, cancellationToken); } @@ -328,6 +336,8 @@ private void ThrowIfDisposed() // Portion of the WriteEntryAsync(TarEntry, CancellationToken) method containing awaits. private async Task WriteEntryAsyncInternal(TarEntry entry, CancellationToken cancellationToken) { + cancellationToken.ThrowIfCancellationRequested(); + byte[] rented = ArrayPool.Shared.Rent(minimumLength: TarHelpers.RecordSize); Memory buffer = rented.AsMemory(0, TarHelpers.RecordSize); // minimumLength means the array could've been larger buffer.Span.Clear(); // Rented arrays aren't clean @@ -378,6 +388,7 @@ private void WriteFinalRecords() // The spec indicates that the end of the archive is indicated // by two records consisting entirely of zero bytes. + // This method is called from DisposeAsync, so we don't want to propagate a cancelled CancellationToken. private async ValueTask WriteFinalRecordsAsync() { byte[] emptyRecord = new byte[TarHelpers.RecordSize]; From 4b588bf26b9de25571e94ec2530c106f56d1efb0 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Tue, 28 Jun 2022 18:00:10 -0700 Subject: [PATCH 53/75] Weird spacing. --- .../System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs index d1ff68b024e6ab..8727b84e3bba05 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs @@ -19,8 +19,8 @@ public abstract partial class TarEntry // Used to access the data section of this entry in an unseekable file private TarReader? _readerOfOrigin; - // Constructor called when reading a TarEntry from a TarReader. - internal TarEntry(TarHeader header, TarReader readerOfOrigin, TarEntryFormat format) + // Constructor called when reading a TarEntry from a TarReader. + internal TarEntry(TarHeader header, TarReader readerOfOrigin, TarEntryFormat format) { // This constructor is called after reading a header from the archive, // and we should've already detected the format of the header. From 82310f2d35d912b69ef8b9383d58cd7ec88973bd Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Tue, 28 Jun 2022 18:00:55 -0700 Subject: [PATCH 54/75] Move awaited code from GetNextEntryAsync to a private method. This prevents throwing OperationCanceledException instead of the expected TaskCanceledException. --- .../src/System/Formats/Tar/TarReader.cs | 69 +++++++++++-------- 1 file changed, 39 insertions(+), 30 deletions(-) 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 95fc7c5f1f1c06..0fa3e45c67b972 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 @@ -141,14 +141,17 @@ public async ValueTask DisposeAsync() /// -or- /// Two or more Extended Attributes entries were found consecutively in the current archive. /// An I/O problem occurred. - public async ValueTask GetNextEntryAsync(bool copyData = false, CancellationToken cancellationToken = default) + public ValueTask GetNextEntryAsync(bool copyData = false, CancellationToken cancellationToken = default) { - cancellationToken.ThrowIfCancellationRequested(); + if (cancellationToken.IsCancellationRequested) + { + return ValueTask.FromCanceled(cancellationToken); + } if (_reachedEndMarkers) { // Avoid advancing the stream if we already found the end of the archive. - return null; + return ValueTask.FromResult(null); } Debug.Assert(_archiveStream.CanRead); @@ -156,35 +159,10 @@ public async ValueTask DisposeAsync() if (_archiveStream.CanSeek && _archiveStream.Length == 0) { // Attempting to get the next entry on an empty tar stream - return null; - } - - await AdvanceDataStreamIfNeededAsync(cancellationToken).ConfigureAwait(false); - - (bool result, TarHeader header) = await TryGetNextEntryHeaderAsync(copyData, cancellationToken).ConfigureAwait(false); - if (result) - { - if (!_readFirstEntry) - { - _readFirstEntry = true; - } - - TarEntry entry = header._format switch - { - TarEntryFormat.Pax => header._typeFlag is TarEntryType.GlobalExtendedAttributes ? - new PaxGlobalExtendedAttributesTarEntry(header, this) : new PaxTarEntry(header, this), - TarEntryFormat.Gnu => new GnuTarEntry(header, this), - TarEntryFormat.Ustar => new UstarTarEntry(header, this), - TarEntryFormat.V7 or TarEntryFormat.Unknown or _ => new V7TarEntry(header, this), - }; - - _previouslyReadEntry = entry; - PreserveDataStreamForDisposalIfNeeded(entry); - return entry; + return ValueTask.FromResult(null); } - _reachedEndMarkers = true; - return null; + return GetNextEntryInternalAsync(copyData, cancellationToken); } // Moves the underlying archive stream position pointer to the beginning of the next header. @@ -315,6 +293,37 @@ private async ValueTask DisposeAsync(bool disposing) } } + // Asynchronously retrieves the next entry if one is found. + private async ValueTask GetNextEntryInternalAsync(bool copyData, CancellationToken cancellationToken) + { + await AdvanceDataStreamIfNeededAsync(cancellationToken).ConfigureAwait(false); + + (bool result, TarHeader header) = await TryGetNextEntryHeaderAsync(copyData, cancellationToken).ConfigureAwait(false); + if (result) + { + if (!_readFirstEntry) + { + _readFirstEntry = true; + } + + TarEntry entry = header._format switch + { + TarEntryFormat.Pax => header._typeFlag is TarEntryType.GlobalExtendedAttributes ? + new PaxGlobalExtendedAttributesTarEntry(header, this) : new PaxTarEntry(header, this), + TarEntryFormat.Gnu => new GnuTarEntry(header, this), + TarEntryFormat.Ustar => new UstarTarEntry(header, this), + TarEntryFormat.V7 or TarEntryFormat.Unknown or _ => new V7TarEntry(header, this), + }; + + _previouslyReadEntry = entry; + PreserveDataStreamForDisposalIfNeeded(entry); + return entry; + } + + _reachedEndMarkers = true; + return null; + } + // Attempts to read the next tar archive entry header. // Returns true if an entry header was collected successfully, false otherwise. // An entry header represents any typeflag that is contains metadata. From 2f1428f20a467b696cb950b9c92afeb2f835b54b Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Tue, 28 Jun 2022 18:38:19 -0700 Subject: [PATCH 55/75] Add tests for throwing with a cancelled CancellationToken --- .../TarEntry.ExtractToFileAsync.Tests.cs | 14 +++++++++++ ...ile.CreateFromDirectoryAsync.File.Tests.cs | 9 ++++++++ ...e.CreateFromDirectoryAsync.Stream.Tests.cs | 10 ++++++++ ...File.ExtractToDirectoryAsync.File.Tests.cs | 9 ++++++++ ...le.ExtractToDirectoryAsync.Stream.Tests.cs | 10 ++++++++ .../TarReader.GetNextEntryAsync.Tests.cs | 18 +++++++++++++++ .../TarWriter.WriteEntryAsync.Tests.cs | 23 +++++++++++++++++++ 7 files changed, 93 insertions(+) diff --git a/src/libraries/System.Formats.Tar/tests/TarEntry/TarEntry.ExtractToFileAsync.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarEntry/TarEntry.ExtractToFileAsync.Tests.cs index e25fcab5573b35..317bbbaab06800 100644 --- a/src/libraries/System.Formats.Tar/tests/TarEntry/TarEntry.ExtractToFileAsync.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarEntry/TarEntry.ExtractToFileAsync.Tests.cs @@ -3,6 +3,7 @@ using System.IO; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Xunit; @@ -10,6 +11,19 @@ namespace System.Formats.Tar.Tests { public class TarEntry_ExtractToFileAsync_Tests : TarTestsBase { + [Theory] + [InlineData(TarEntryFormat.V7)] + [InlineData(TarEntryFormat.Ustar)] + [InlineData(TarEntryFormat.Pax)] + [InlineData(TarEntryFormat.Gnu)] + public Task ExtractToFileAsync_Cancel(TarEntryFormat format) + { + TarEntry entry = InvokeTarEntryCreationConstructor(format, TarEntryType.Directory, "dir"); + CancellationTokenSource cs = new CancellationTokenSource(); + cs.Cancel(); + return Assert.ThrowsAsync(() => entry.ExtractToFileAsync("dir", overwrite: true, cs.Token)); + } + [Theory] [InlineData(TarEntryFormat.V7)] [InlineData(TarEntryFormat.Ustar)] diff --git a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.CreateFromDirectoryAsync.File.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.CreateFromDirectoryAsync.File.Tests.cs index 6ef66b014fb7f4..4e26cf5b3d04a0 100644 --- a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.CreateFromDirectoryAsync.File.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.CreateFromDirectoryAsync.File.Tests.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Xunit; @@ -11,6 +12,14 @@ namespace System.Formats.Tar.Tests { public class TarFile_CreateFromDirectoryAsync_File_Tests : TarTestsBase { + [Fact] + public Task CreateFromDirectoryAsync_Cancel() + { + CancellationTokenSource cs = new CancellationTokenSource(); + cs.Cancel(); + return Assert.ThrowsAsync(() => TarFile.CreateFromDirectoryAsync("directory", "file.tar", includeBaseDirectory: false, cs.Token)); + } + [Fact] public async Task InvalidPaths_Throw_Async() { diff --git a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.CreateFromDirectoryAsync.Stream.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.CreateFromDirectoryAsync.Stream.Tests.cs index 550c603bbbb8b0..acab9e24f4c20f 100644 --- a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.CreateFromDirectoryAsync.Stream.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.CreateFromDirectoryAsync.Stream.Tests.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.IO; +using System.Threading; using System.Threading.Tasks; using Xunit; @@ -9,6 +10,15 @@ namespace System.Formats.Tar.Tests { public class TarFile_CreateFromDirectoryAsync_Stream_Tests : TarTestsBase { + [Fact] + public Task CreateFromDirectoryAsync_Cancel() + { + CancellationTokenSource cs = new CancellationTokenSource(); + cs.Cancel(); + MemoryStream archiveStream = new MemoryStream(); + return Assert.ThrowsAsync(() => TarFile.CreateFromDirectoryAsync("directory", archiveStream, includeBaseDirectory: false, cs.Token)); + } + [Fact] public async Task InvalidPath_Throws_Async() { diff --git a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectoryAsync.File.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectoryAsync.File.Tests.cs index 9af29ea027878a..ac2db7137b98cc 100644 --- a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectoryAsync.File.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectoryAsync.File.Tests.cs @@ -3,6 +3,7 @@ using System.IO; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Xunit; @@ -10,6 +11,14 @@ namespace System.Formats.Tar.Tests { public partial class TarFile_ExtractToDirectoryAsync_File_Tests : TarTestsBase { + [Fact] + public Task ExtractToDirectoryAsync_Cancel() + { + CancellationTokenSource cs = new CancellationTokenSource(); + cs.Cancel(); + return Assert.ThrowsAsync(() => TarFile.ExtractToDirectoryAsync("file.tar", "directory", overwriteFiles: true, cs.Token)); + } + [Fact] public async Task InvalidPaths_Throw() { 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 fb7f6cd8b15eeb..ff1ba1a8ecff52 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 @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Xunit; @@ -11,6 +12,15 @@ namespace System.Formats.Tar.Tests { public class TarFile_ExtractToDirectoryAsync_Stream_Tests : TarTestsBase { + [Fact] + public Task ExtractToDirectoryAsync_Cancel() + { + CancellationTokenSource cs = new CancellationTokenSource(); + cs.Cancel(); + MemoryStream archiveStream = new MemoryStream(); + return Assert.ThrowsAsync(() => TarFile.ExtractToDirectoryAsync(archiveStream, "directory", overwriteFiles: true, cs.Token)); + } + [Fact] public async Task NullStream_Throws_Async() { 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 79d4ac789b320b..3dffef9d9a4f25 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 @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.IO; +using System.Threading; using System.Threading.Tasks; using Xunit; @@ -9,6 +10,23 @@ namespace System.Formats.Tar.Tests { public class TarReader_GetNextEntryAsync_Tests : TarTestsBase { + [Fact] + public async Task GetNextEntryAsync_Cancel() + { + CancellationTokenSource cs = new CancellationTokenSource(); + cs.Cancel(); + MemoryStream archiveStream = new MemoryStream(); + await using (archiveStream) + { + TarReader reader = new TarReader(archiveStream, leaveOpen: false); + await using (reader) + { + await Assert.ThrowsAsync(async () => await reader.GetNextEntryAsync(copyData: false, cs.Token)); + } + } + } + + [Fact] public async Task MalformedArchive_TooSmall_Async() { 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 4a8bb141fbf870..3213d1b6de4ca9 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 @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; +using System.Threading; using System.Threading.Tasks; using Xunit; @@ -11,6 +12,28 @@ namespace System.Formats.Tar.Tests // Tests that are independent of the archive format. public class TarWriter_WriteEntryAsync_Tests : TarWriter_WriteEntry_Base { + [Theory] + [InlineData(TarEntryFormat.V7)] + [InlineData(TarEntryFormat.Ustar)] + [InlineData(TarEntryFormat.Pax)] + [InlineData(TarEntryFormat.Gnu)] + public async Task WriteEntryAsync_Cancel(TarEntryFormat format) + { + CancellationTokenSource cs = new CancellationTokenSource(); + cs.Cancel(); + MemoryStream archiveStream = new MemoryStream(); + await using (archiveStream) + { + TarWriter writer = new TarWriter(archiveStream, leaveOpen: false); + await using (writer) + { + TarEntry entry = InvokeTarEntryCreationConstructor(format, TarEntryType.Directory, "dir"); + await Assert.ThrowsAsync(() => writer.WriteEntryAsync(entry, cs.Token)); + await Assert.ThrowsAsync(() => writer.WriteEntryAsync("file.txt", "file.txt", cs.Token)); + } + } + } + [Fact] public async Task WriteEntry_AfterDispose_Throws_Async() { From 990fc1a3f6140a1facd9ff891e132065a60eee2a Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Tue, 28 Jun 2022 18:48:51 -0700 Subject: [PATCH 56/75] Simplify async calls in tests. --- .../TarEntry.ExtractToFileAsync.Tests.cs | 6 +++--- ...ile.CreateFromDirectoryAsync.File.Tests.cs | 16 +++++++-------- ...e.CreateFromDirectoryAsync.Stream.Tests.cs | 16 +++++++-------- ...ExtractToDirectoryAsync.File.Tests.Unix.cs | 2 +- ...ractToDirectoryAsync.File.Tests.Windows.cs | 2 +- ...File.ExtractToDirectoryAsync.File.Tests.cs | 20 +++++++++---------- ...le.ExtractToDirectoryAsync.Stream.Tests.cs | 20 +++++++++---------- ...le.GlobalExtendedAttributes.Async.Tests.cs | 2 +- ...eader.TarEntry.ExtractToFile.Tests.Unix.cs | 4 ++-- ....TarEntry.ExtractToFileAsync.Tests.Unix.cs | 4 ++-- .../TarWriter.WriteEntryAsync.File.Tests.cs | 6 +++--- .../TarWriter.WriteEntryAsync.Tests.cs | 2 +- 12 files changed, 49 insertions(+), 51 deletions(-) diff --git a/src/libraries/System.Formats.Tar/tests/TarEntry/TarEntry.ExtractToFileAsync.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarEntry/TarEntry.ExtractToFileAsync.Tests.cs index 317bbbaab06800..c2b09a07603d2c 100644 --- a/src/libraries/System.Formats.Tar/tests/TarEntry/TarEntry.ExtractToFileAsync.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarEntry/TarEntry.ExtractToFileAsync.Tests.cs @@ -41,7 +41,7 @@ public async Task Constructor_Name_FullPath_DestinationDirectory_Mismatch_Throws entry.DataStream.Write(new byte[] { 0x1 }); entry.DataStream.Seek(0, SeekOrigin.Begin); - await Assert.ThrowsAsync(async () => await entry.ExtractToFileAsync(root.Path, overwrite: false)); + await Assert.ThrowsAsync(() => entry.ExtractToFileAsync(root.Path, overwrite: false)); Assert.False(File.Exists(fullPath)); } @@ -63,7 +63,7 @@ public async Task Constructor_Name_FullPath_DestinationDirectory_Match_Additiona entry.DataStream.Write(new byte[] { 0x1 }); entry.DataStream.Seek(0, SeekOrigin.Begin); - await Assert.ThrowsAsync(async () => await entry.ExtractToFileAsync(root.Path, overwrite: false)); + await Assert.ThrowsAsync(() => entry.ExtractToFileAsync(root.Path, overwrite: false)); Assert.False(File.Exists(fullPath)); } @@ -103,7 +103,7 @@ public async Task ExtractToFile_Link_Throws_Async(TarEntryFormat format, TarEntr TarEntry entry = InvokeTarEntryCreationConstructor(format, entryType, fileName); entry.LinkName = linkTarget; - await Assert.ThrowsAsync(async () => await entry.ExtractToFileAsync(fileName, overwrite: false)); + await Assert.ThrowsAsync(() => entry.ExtractToFileAsync(fileName, overwrite: false)); Assert.Equal(0, Directory.GetFileSystemEntries(root.Path).Count()); } diff --git a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.CreateFromDirectoryAsync.File.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.CreateFromDirectoryAsync.File.Tests.cs index 4e26cf5b3d04a0..a2872a7bbc528e 100644 --- a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.CreateFromDirectoryAsync.File.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.CreateFromDirectoryAsync.File.Tests.cs @@ -23,25 +23,25 @@ public Task CreateFromDirectoryAsync_Cancel() [Fact] public async Task InvalidPaths_Throw_Async() { - await Assert.ThrowsAsync(async () => await TarFile.CreateFromDirectoryAsync(sourceDirectoryName: null,destinationFileName: "path", includeBaseDirectory: false)); - await Assert.ThrowsAsync(async () => await TarFile.CreateFromDirectoryAsync(sourceDirectoryName: string.Empty,destinationFileName: "path", includeBaseDirectory: false)); - await Assert.ThrowsAsync(async () => await TarFile.CreateFromDirectoryAsync(sourceDirectoryName: "path",destinationFileName: null, includeBaseDirectory: false)); - await Assert.ThrowsAsync(async () => await TarFile.CreateFromDirectoryAsync(sourceDirectoryName: "path",destinationFileName: string.Empty, includeBaseDirectory: false)); + await Assert.ThrowsAsync(() => TarFile.CreateFromDirectoryAsync(sourceDirectoryName: null,destinationFileName: "path", includeBaseDirectory: false)); + await Assert.ThrowsAsync(() => TarFile.CreateFromDirectoryAsync(sourceDirectoryName: string.Empty,destinationFileName: "path", includeBaseDirectory: false)); + await Assert.ThrowsAsync(() => TarFile.CreateFromDirectoryAsync(sourceDirectoryName: "path",destinationFileName: null, includeBaseDirectory: false)); + await Assert.ThrowsAsync(() => TarFile.CreateFromDirectoryAsync(sourceDirectoryName: "path",destinationFileName: string.Empty, includeBaseDirectory: false)); } [Fact] - public async Task NonExistentDirectory_Throws_Async() + public Task NonExistentDirectory_Throws_Async() { using TempDirectory root = new TempDirectory(); string dirPath = Path.Join(root.Path, "dir"); string filePath = Path.Join(root.Path, "file.tar"); - await Assert.ThrowsAsync(async () => await TarFile.CreateFromDirectoryAsync(sourceDirectoryName: "IDontExist", destinationFileName: filePath, includeBaseDirectory: false)); + return Assert.ThrowsAsync(() => TarFile.CreateFromDirectoryAsync(sourceDirectoryName: "IDontExist", destinationFileName: filePath, includeBaseDirectory: false)); } [Fact] - public async Task DestinationExists_Throws_Async() + public Task DestinationExists_Throws_Async() { using TempDirectory root = new TempDirectory(); @@ -51,7 +51,7 @@ public async Task DestinationExists_Throws_Async() string filePath = Path.Join(root.Path, "file.tar"); File.Create(filePath).Dispose(); - await Assert.ThrowsAsync(async () => await TarFile.CreateFromDirectoryAsync(sourceDirectoryName: dirPath, destinationFileName: filePath, includeBaseDirectory: false)); + return Assert.ThrowsAsync(() => TarFile.CreateFromDirectoryAsync(sourceDirectoryName: dirPath, destinationFileName: filePath, includeBaseDirectory: false)); } [Theory] diff --git a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.CreateFromDirectoryAsync.Stream.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.CreateFromDirectoryAsync.Stream.Tests.cs index acab9e24f4c20f..ea7833140dcb48 100644 --- a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.CreateFromDirectoryAsync.Stream.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.CreateFromDirectoryAsync.Stream.Tests.cs @@ -23,33 +23,33 @@ public Task CreateFromDirectoryAsync_Cancel() public async Task InvalidPath_Throws_Async() { using MemoryStream archive = new MemoryStream(); - await Assert.ThrowsAsync(async () => await TarFile.CreateFromDirectoryAsync(sourceDirectoryName: null,destination: archive, includeBaseDirectory: false)); - await Assert.ThrowsAsync(async () => await TarFile.CreateFromDirectoryAsync(sourceDirectoryName: string.Empty,destination: archive, includeBaseDirectory: false)); + await Assert.ThrowsAsync(() => TarFile.CreateFromDirectoryAsync(sourceDirectoryName: null,destination: archive, includeBaseDirectory: false)); + await Assert.ThrowsAsync(() => TarFile.CreateFromDirectoryAsync(sourceDirectoryName: string.Empty,destination: archive, includeBaseDirectory: false)); } [Fact] - public async Task NullStream_Throws_Async() + public Task NullStream_Throws_Async() { using MemoryStream archive = new MemoryStream(); - await Assert.ThrowsAsync(async () => await TarFile.CreateFromDirectoryAsync(sourceDirectoryName: "path",destination: null, includeBaseDirectory: false)); + return Assert.ThrowsAsync(() => TarFile.CreateFromDirectoryAsync(sourceDirectoryName: "path",destination: null, includeBaseDirectory: false)); } [Fact] - public async Task UnwritableStream_Throws_Async() + public Task UnwritableStream_Throws_Async() { using MemoryStream archive = new MemoryStream(); using WrappedStream unwritable = new WrappedStream(archive, canRead: true, canWrite: false, canSeek: true); - await Assert.ThrowsAsync(async () => await TarFile.CreateFromDirectoryAsync(sourceDirectoryName: "path",destination: unwritable, includeBaseDirectory: false)); + return Assert.ThrowsAsync(() => TarFile.CreateFromDirectoryAsync(sourceDirectoryName: "path",destination: unwritable, includeBaseDirectory: false)); } [Fact] - public async Task NonExistentDirectory_Throws_Async() + public Task NonExistentDirectory_Throws_Async() { using TempDirectory root = new TempDirectory(); string dirPath = Path.Join(root.Path, "dir"); using MemoryStream archive = new MemoryStream(); - await Assert.ThrowsAsync(async () => await TarFile.CreateFromDirectoryAsync(sourceDirectoryName: dirPath, destination: archive, includeBaseDirectory: false)); + return Assert.ThrowsAsync(() => TarFile.CreateFromDirectoryAsync(sourceDirectoryName: dirPath, destination: archive, includeBaseDirectory: false)); } } } diff --git a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectoryAsync.File.Tests.Unix.cs b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectoryAsync.File.Tests.Unix.cs index 7e0c87bab0f10a..540a876a947c3c 100644 --- a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectoryAsync.File.Tests.Unix.cs +++ b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectoryAsync.File.Tests.Unix.cs @@ -24,7 +24,7 @@ public async Task Extract_SpecialFiles_Unix_Unelevated_ThrowsUnauthorizedAccess_ Directory.CreateDirectory(destination); - await Assert.ThrowsAsync(async () => await TarFile.ExtractToDirectoryAsync(archive, destination, overwriteFiles: false)); + await Assert.ThrowsAsync(() => TarFile.ExtractToDirectoryAsync(archive, destination, overwriteFiles: false)); Assert.Equal(0, Directory.GetFileSystemEntries(destination).Count()); } diff --git a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectoryAsync.File.Tests.Windows.cs b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectoryAsync.File.Tests.Windows.cs index 218d6cf17853f0..3d7bba013503ee 100644 --- a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectoryAsync.File.Tests.Windows.cs +++ b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectoryAsync.File.Tests.Windows.cs @@ -24,7 +24,7 @@ public async Task Extract_SpecialFiles_Windows_ThrowsInvalidOperation_Async() Directory.CreateDirectory(destination); - await Assert.ThrowsAsync(async () => await TarFile.ExtractToDirectoryAsync(archive, destination, overwriteFiles: false)); + await Assert.ThrowsAsync(() => TarFile.ExtractToDirectoryAsync(archive, destination, overwriteFiles: false)); Assert.Equal(0, Directory.GetFileSystemEntries(destination).Count()); } diff --git a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectoryAsync.File.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectoryAsync.File.Tests.cs index ac2db7137b98cc..210893d72024f9 100644 --- a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectoryAsync.File.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectoryAsync.File.Tests.cs @@ -22,14 +22,14 @@ public Task ExtractToDirectoryAsync_Cancel() [Fact] public async Task InvalidPaths_Throw() { - await Assert.ThrowsAsync(async () => await TarFile.ExtractToDirectoryAsync(sourceFileName: null, destinationDirectoryName: "path", overwriteFiles: false)); - await Assert.ThrowsAsync(async () => await TarFile.ExtractToDirectoryAsync(sourceFileName: string.Empty, destinationDirectoryName: "path", overwriteFiles: false)); - await Assert.ThrowsAsync(async () => await TarFile.ExtractToDirectoryAsync(sourceFileName: "path", destinationDirectoryName: null, overwriteFiles: false)); - await Assert.ThrowsAsync(async () => await TarFile.ExtractToDirectoryAsync(sourceFileName: "path", destinationDirectoryName: string.Empty, overwriteFiles: false)); + await Assert.ThrowsAsync(() => TarFile.ExtractToDirectoryAsync(sourceFileName: null, destinationDirectoryName: "path", overwriteFiles: false)); + await Assert.ThrowsAsync(() => TarFile.ExtractToDirectoryAsync(sourceFileName: string.Empty, destinationDirectoryName: "path", overwriteFiles: false)); + await Assert.ThrowsAsync(() => TarFile.ExtractToDirectoryAsync(sourceFileName: "path", destinationDirectoryName: null, overwriteFiles: false)); + await Assert.ThrowsAsync(() => TarFile.ExtractToDirectoryAsync(sourceFileName: "path", destinationDirectoryName: string.Empty, overwriteFiles: false)); } [Fact] - public async Task NonExistentFile_Throws_Async() + public Task NonExistentFile_Throws_Async() { using TempDirectory root = new TempDirectory(); @@ -38,11 +38,11 @@ public async Task NonExistentFile_Throws_Async() Directory.CreateDirectory(dirPath); - await Assert.ThrowsAsync(async () => await TarFile.ExtractToDirectoryAsync(sourceFileName: filePath, destinationDirectoryName: dirPath, overwriteFiles: false)); + return Assert.ThrowsAsync(() => TarFile.ExtractToDirectoryAsync(sourceFileName: filePath, destinationDirectoryName: dirPath, overwriteFiles: false)); } [Fact] - public async Task NonExistentDirectory_Throws_Async() + public Task NonExistentDirectory_Throws_Async() { using TempDirectory root = new TempDirectory(); @@ -51,7 +51,7 @@ public async Task NonExistentDirectory_Throws_Async() File.Create(filePath).Dispose(); - await Assert.ThrowsAsync(async () => await TarFile.ExtractToDirectoryAsync(sourceFileName: filePath, destinationDirectoryName: dirPath, overwriteFiles: false)); + return Assert.ThrowsAsync(() => TarFile.ExtractToDirectoryAsync(sourceFileName: filePath, destinationDirectoryName: dirPath, overwriteFiles: false)); } [Theory] @@ -102,7 +102,7 @@ public async Task Extract_Archive_File_OverwriteTrue_Async() } [Fact] - public async Task Extract_Archive_File_OverwriteFalse_Async() + public Task Extract_Archive_File_OverwriteFalse_Async() { string sourceArchiveFileName = GetTarFilePath(CompressionMethod.Uncompressed, TestTarFormat.pax, "file"); @@ -112,7 +112,7 @@ public async Task Extract_Archive_File_OverwriteFalse_Async() File.Create(filePath).Dispose(); - await Assert.ThrowsAsync(async () => await TarFile.ExtractToDirectoryAsync(sourceArchiveFileName, destination.Path, overwriteFiles: false)); + return Assert.ThrowsAsync(() => TarFile.ExtractToDirectoryAsync(sourceArchiveFileName, destination.Path, overwriteFiles: false)); } [Fact] 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 ff1ba1a8ecff52..1aba0f1185aa27 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 @@ -22,35 +22,33 @@ public Task ExtractToDirectoryAsync_Cancel() } [Fact] - public async Task NullStream_Throws_Async() - { - await Assert.ThrowsAsync(async () => await TarFile.ExtractToDirectoryAsync(source: null, destinationDirectoryName: "path", overwriteFiles: false)); - } + public Task NullStream_Throws_Async() => + Assert.ThrowsAsync(() => TarFile.ExtractToDirectoryAsync(source: null, destinationDirectoryName: "path", overwriteFiles: false)); [Fact] public async Task InvalidPath_Throws_Async() { using MemoryStream archive = new MemoryStream(); - await Assert.ThrowsAsync(async () => await TarFile.ExtractToDirectoryAsync(archive, destinationDirectoryName: null, overwriteFiles: false)); - await Assert.ThrowsAsync(async () => await TarFile.ExtractToDirectoryAsync(archive, destinationDirectoryName: string.Empty, overwriteFiles: false)); + await Assert.ThrowsAsync(() => TarFile.ExtractToDirectoryAsync(archive, destinationDirectoryName: null, overwriteFiles: false)); + await Assert.ThrowsAsync(() => TarFile.ExtractToDirectoryAsync(archive, destinationDirectoryName: string.Empty, overwriteFiles: false)); } [Fact] - public async Task UnreadableStream_Throws_Async() + public Task UnreadableStream_Throws_Async() { using MemoryStream archive = new MemoryStream(); using WrappedStream unreadable = new WrappedStream(archive, canRead: false, canWrite: true, canSeek: true); - await Assert.ThrowsAsync(async () => await TarFile.ExtractToDirectoryAsync(unreadable, destinationDirectoryName: "path", overwriteFiles: false)); + return Assert.ThrowsAsync(() => TarFile.ExtractToDirectoryAsync(unreadable, destinationDirectoryName: "path", overwriteFiles: false)); } [Fact] - public async Task NonExistentDirectory_Throws_Async() + public Task NonExistentDirectory_Throws_Async() { using TempDirectory root = new TempDirectory(); string dirPath = Path.Join(root.Path, "dir"); using MemoryStream archive = new MemoryStream(); - await Assert.ThrowsAsync(async () => await TarFile.ExtractToDirectoryAsync(archive, destinationDirectoryName: dirPath, overwriteFiles: false)); + return Assert.ThrowsAsync(() => TarFile.ExtractToDirectoryAsync(archive, destinationDirectoryName: dirPath, overwriteFiles: false)); } [Fact] @@ -102,7 +100,7 @@ public async Task Extract_LinkEntry_TargetOutsideDirectory_Async(TarEntryType en using TempDirectory root = new TempDirectory(); - await Assert.ThrowsAsync(async () => await TarFile.ExtractToDirectoryAsync(archive, root.Path, overwriteFiles: false)); + await Assert.ThrowsAsync(() => TarFile.ExtractToDirectoryAsync(archive, root.Path, overwriteFiles: false)); Assert.Equal(0, Directory.GetFileSystemEntries(root.Path).Count()); } diff --git a/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.File.GlobalExtendedAttributes.Async.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.File.GlobalExtendedAttributes.Async.Tests.cs index 45047d930c089d..6ff0e6148b9c27 100644 --- a/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.File.GlobalExtendedAttributes.Async.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.File.GlobalExtendedAttributes.Async.Tests.cs @@ -82,7 +82,7 @@ public async Task ExtractGlobalExtendedAttributesEntry_Throws_Async() await using (reader) { TarEntry entry = await reader.GetNextEntryAsync(); - await Assert.ThrowsAsync(async () => await entry.ExtractToFileAsync(Path.Join(root.Path, "file"), overwrite: true)); + await Assert.ThrowsAsync(() => entry.ExtractToFileAsync(Path.Join(root.Path, "file"), overwrite: true)); } } } diff --git a/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.TarEntry.ExtractToFile.Tests.Unix.cs b/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.TarEntry.ExtractToFile.Tests.Unix.cs index fb9ee0473166d5..ae84fc9177a9a1 100644 --- a/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.TarEntry.ExtractToFile.Tests.Unix.cs +++ b/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.TarEntry.ExtractToFile.Tests.Unix.cs @@ -24,13 +24,13 @@ public async Task SpecialFile_Unelevated_Throws_Async() // Block device requires elevation for writing PosixTarEntry blockDevice = await reader.GetNextEntryAsync() as PosixTarEntry; Assert.NotNull(blockDevice); - await Assert.ThrowsAsync(async () => await blockDevice.ExtractToFileAsync(path, overwrite: false)); + await Assert.ThrowsAsync(() => blockDevice.ExtractToFileAsync(path, overwrite: false)); Assert.False(File.Exists(path)); // Character device requires elevation for writing PosixTarEntry characterDevice = await reader.GetNextEntryAsync() as PosixTarEntry; Assert.NotNull(characterDevice); - await Assert.ThrowsAsync(async () => await characterDevice.ExtractToFileAsync(path, overwrite: false)); + await Assert.ThrowsAsync(() => characterDevice.ExtractToFileAsync(path, overwrite: false)); Assert.False(File.Exists(path)); // Fifo does not require elevation, should succeed diff --git a/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.TarEntry.ExtractToFileAsync.Tests.Unix.cs b/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.TarEntry.ExtractToFileAsync.Tests.Unix.cs index 750cd4d181e5b4..9f84a31625077a 100644 --- a/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.TarEntry.ExtractToFileAsync.Tests.Unix.cs +++ b/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.TarEntry.ExtractToFileAsync.Tests.Unix.cs @@ -24,13 +24,13 @@ public async Task SpecialFile_Unelevated_Throws_Async() // Block device requires elevation for writing PosixTarEntry blockDevice = await reader.GetNextEntryAsync() as PosixTarEntry; Assert.NotNull(blockDevice); - await Assert.ThrowsAsync(async () => await blockDevice.ExtractToFileAsync(path, overwrite: false)); + await Assert.ThrowsAsync(() => blockDevice.ExtractToFileAsync(path, overwrite: false)); Assert.False(File.Exists(path)); // Character device requires elevation for writing PosixTarEntry characterDevice = await reader.GetNextEntryAsync() as PosixTarEntry; Assert.NotNull(characterDevice); - await Assert.ThrowsAsync(async () => await characterDevice.ExtractToFileAsync(path, overwrite: false)); + await Assert.ThrowsAsync(() => characterDevice.ExtractToFileAsync(path, overwrite: false)); Assert.False(File.Exists(path)); // Fifo does not require elevation, should succeed diff --git a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.File.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.File.Tests.cs index 8fc116cf73eab5..048332c4885119 100644 --- a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.File.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.File.Tests.cs @@ -17,7 +17,7 @@ public async Task ThrowIf_AddFile_AfterDispose_Async() TarWriter writer = new TarWriter(archiveStream); await writer.DisposeAsync(); - await Assert.ThrowsAsync(async () => await writer.WriteEntryAsync("fileName", "entryName")); + await Assert.ThrowsAsync(() => writer.WriteEntryAsync("fileName", "entryName")); } [Fact] @@ -27,8 +27,8 @@ public async Task FileName_NullOrEmpty_Async() TarWriter writer = new TarWriter(archiveStream); await using (writer) { - await Assert.ThrowsAsync(async () => await writer.WriteEntryAsync(null, "entryName")); - await Assert.ThrowsAsync(async () => await writer.WriteEntryAsync(string.Empty, "entryName")); + await Assert.ThrowsAsync(() => writer.WriteEntryAsync(null, "entryName")); + await Assert.ThrowsAsync(() => writer.WriteEntryAsync(string.Empty, "entryName")); } } 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 3213d1b6de4ca9..2ca49f70541b17 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 @@ -42,7 +42,7 @@ public async Task WriteEntry_AfterDispose_Throws_Async() await writer.DisposeAsync(); PaxTarEntry entry = new PaxTarEntry(TarEntryType.RegularFile, InitialEntryName); - await Assert.ThrowsAsync(async () => await writer.WriteEntryAsync(entry)); + await Assert.ThrowsAsync(() => writer.WriteEntryAsync(entry)); } [Fact] From 1bf7f9662e7707f653bb6acf644c7a1e514d0685 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Tue, 28 Jun 2022 19:19:28 -0700 Subject: [PATCH 57/75] Use await using in FileStream used when extracting as regular file. --- .../System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs index 8727b84e3bba05..707c5962ab31dc 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs @@ -595,7 +595,8 @@ private async Task ExtractAsRegularFileAsync(string destinationFileName, Cancell Options = FileOptions.Asynchronous }; // Rely on FileStream's ctor for further checking destinationFileName parameter - using (FileStream fs = new FileStream(destinationFileName, fileStreamOptions)) + FileStream fs = new FileStream(destinationFileName, fileStreamOptions); + await using (fs) { if (DataStream != null) { From f6a0a18e744fb06001b293e42270cdd12b6d9698 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Tue, 28 Jun 2022 19:39:53 -0700 Subject: [PATCH 58/75] Reuse code in TarEntry.ExtractRelativeToDirectory* methods --- .../src/System/Formats/Tar/TarEntry.cs | 60 +++++++------------ 1 file changed, 22 insertions(+), 38 deletions(-) diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs index 707c5962ab31dc..d77cd1de184d66 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs @@ -314,55 +314,49 @@ public Stream? DataStream // Extracts the current entry to a location relative to the specified directory. internal void ExtractRelativeToDirectory(string destinationDirectoryPath, bool overwrite) { - Debug.Assert(!string.IsNullOrEmpty(destinationDirectoryPath)); - Debug.Assert(Path.IsPathFullyQualified(destinationDirectoryPath)); - - destinationDirectoryPath = Path.TrimEndingDirectorySeparator(destinationDirectoryPath); + (string fileDestinationPath, string? linkTargetPath) = GetDestinationAndLinkPaths(destinationDirectoryPath); - string? fileDestinationPath = GetSanitizedFullPath(destinationDirectoryPath, Name); - if (fileDestinationPath == null) + if (EntryType == TarEntryType.Directory) { - throw new IOException(string.Format(SR.TarExtractingResultsFileOutside, Name, destinationDirectoryPath)); + Directory.CreateDirectory(fileDestinationPath); } - - string? linkTargetPath = null; - if (EntryType is TarEntryType.SymbolicLink or TarEntryType.HardLink) + else { - if (string.IsNullOrEmpty(LinkName)) - { - throw new FormatException(SR.TarEntryHardLinkOrSymlinkLinkNameEmpty); - } + // If it is a file, create containing directory. + Directory.CreateDirectory(Path.GetDirectoryName(fileDestinationPath)!); + ExtractToFileInternal(fileDestinationPath, linkTargetPath, overwrite); + } + } - linkTargetPath = GetSanitizedFullPath(destinationDirectoryPath, LinkName); - if (linkTargetPath == null) - { - throw new IOException(string.Format(SR.TarExtractingResultsLinkOutside, LinkName, destinationDirectoryPath)); - } + // Asynchronously extracts the current entry to a location relative to the specified directory. + internal Task ExtractRelativeToDirectoryAsync(string destinationDirectoryPath, bool overwrite, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); } + (string fileDestinationPath, string? linkTargetPath) = GetDestinationAndLinkPaths(destinationDirectoryPath); + if (EntryType == TarEntryType.Directory) { Directory.CreateDirectory(fileDestinationPath); + return Task.CompletedTask; } else { // If it is a file, create containing directory. Directory.CreateDirectory(Path.GetDirectoryName(fileDestinationPath)!); - ExtractToFileInternal(fileDestinationPath, linkTargetPath, overwrite); + return ExtractToFileInternalAsync(fileDestinationPath, linkTargetPath, overwrite, cancellationToken); } } - // Asynchronously extracts the current entry to a location relative to the specified directory. - internal Task ExtractRelativeToDirectoryAsync(string destinationDirectoryPath, bool overwrite, CancellationToken cancellationToken) + // Gets the sanitized paths for the file destination and link target paths to be used when extracting relative to a directory. + private (string, string?) GetDestinationAndLinkPaths(string destinationDirectoryPath) { Debug.Assert(!string.IsNullOrEmpty(destinationDirectoryPath)); Debug.Assert(Path.IsPathFullyQualified(destinationDirectoryPath)); - if (cancellationToken.IsCancellationRequested) - { - return Task.FromCanceled(cancellationToken); - } - destinationDirectoryPath = Path.TrimEndingDirectorySeparator(destinationDirectoryPath); string? fileDestinationPath = GetSanitizedFullPath(destinationDirectoryPath, Name); @@ -386,17 +380,7 @@ internal Task ExtractRelativeToDirectoryAsync(string destinationDirectoryPath, b } } - if (EntryType == TarEntryType.Directory) - { - Directory.CreateDirectory(fileDestinationPath); - return Task.CompletedTask; - } - else - { - // If it is a file, create containing directory. - Directory.CreateDirectory(Path.GetDirectoryName(fileDestinationPath)!); - return ExtractToFileInternalAsync(fileDestinationPath, linkTargetPath, overwrite, cancellationToken); - } + return (fileDestinationPath, linkTargetPath); } // If the path can be extracted in the specified destination directory, returns the full path with sanitized file name. Otherwise, returns null. From de1fa0641aac3f878006ecd9a37f2cbda98ee05f Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Tue, 28 Jun 2022 20:01:36 -0700 Subject: [PATCH 59/75] Reuse code in TarFile methods. --- .../src/System/Formats/Tar/TarFile.cs | 121 +++++++++--------- 1 file changed, 63 insertions(+), 58 deletions(-) diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarFile.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarFile.cs index 2276eee7538160..8865c072dd2f88 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarFile.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarFile.cs @@ -14,6 +14,11 @@ namespace System.Formats.Tar /// public static class TarFile { + // Windows' MaxPath (260) is used as an arbitrary default capacity, as it is likely + // to be greater than the length of typical entry names from the file system, even + // on non-Windows platforms. The capacity will be increased, if needed. + private const int DefaultCapacity = 260; + /// /// Creates a tar stream that contains all the filesystem entries from the specified directory. /// @@ -270,26 +275,14 @@ public static Task ExtractToDirectoryAsync(string sourceFileName, string destina // It assumes the sourceDirectoryName is a fully qualified path, and allows choosing if the archive stream should be left open or not. private static void CreateFromDirectoryInternal(string sourceDirectoryName, Stream destination, bool includeBaseDirectory, bool leaveOpen) { - Debug.Assert(!string.IsNullOrEmpty(sourceDirectoryName)); - Debug.Assert(destination != null); - Debug.Assert(Path.IsPathFullyQualified(sourceDirectoryName)); - Debug.Assert(destination.CanWrite); + Debug.Assert(VerifyCreateFromDirectoryArguments(sourceDirectoryName, destination)); using (TarWriter writer = new TarWriter(destination, TarEntryFormat.Pax, leaveOpen)) { bool baseDirectoryIsEmpty = true; DirectoryInfo di = new(sourceDirectoryName); - string basePath = di.FullName; - - if (includeBaseDirectory && di.Parent != null) - { - basePath = di.Parent.FullName; - } + string basePath = GetBasePathForCreateFromDirectory(di, includeBaseDirectory); - // Windows' MaxPath (260) is used as an arbitrary default capacity, as it is likely - // to be greater than the length of typical entry names from the file system, even - // on non-Windows platforms. The capacity will be increased, if needed. - const int DefaultCapacity = 260; char[] entryNameBuffer = ArrayPool.Shared.Rent(DefaultCapacity); try @@ -297,20 +290,12 @@ private static void CreateFromDirectoryInternal(string sourceDirectoryName, Stre foreach (FileSystemInfo file in di.EnumerateFileSystemInfos("*", SearchOption.AllDirectories)) { baseDirectoryIsEmpty = false; - - int entryNameLength = file.FullName.Length - basePath.Length; - Debug.Assert(entryNameLength > 0); - - bool isDirectory = file.Attributes.HasFlag(FileAttributes.Directory); - string entryName = ArchivingUtils.EntryFromPath(file.FullName, basePath.Length, entryNameLength, ref entryNameBuffer, appendPathSeparator: isDirectory); - writer.WriteEntry(file.FullName, entryName); + writer.WriteEntry(file.FullName, GetEntryNameForFileSystemInfo(file, basePath.Length, ref entryNameBuffer)); } if (includeBaseDirectory && baseDirectoryIsEmpty) { - string entryName = ArchivingUtils.EntryFromPath(di.Name, 0, di.Name.Length, ref entryNameBuffer, appendPathSeparator: true); - PaxTarEntry entry = new PaxTarEntry(TarEntryType.Directory, entryName); - writer.WriteEntry(entry); + writer.WriteEntry(GetEntryForBaseDirectory(di.Name, ref entryNameBuffer)); } } finally @@ -346,11 +331,7 @@ private static async Task CreateFromDirectoryInternalAsync(string sourceDirector // It assumes the sourceDirectoryName is a fully qualified path, and allows choosing if the archive stream should be left open or not. private static async Task CreateFromDirectoryInternalAsync(string sourceDirectoryName, Stream destination, bool includeBaseDirectory, bool leaveOpen, CancellationToken cancellationToken) { - Debug.Assert(!string.IsNullOrEmpty(sourceDirectoryName)); - Debug.Assert(destination != null); - Debug.Assert(Path.IsPathFullyQualified(sourceDirectoryName)); - Debug.Assert(destination.CanWrite); - + Debug.Assert(VerifyCreateFromDirectoryArguments(sourceDirectoryName, destination)); cancellationToken.ThrowIfCancellationRequested(); TarWriter writer = new TarWriter(destination, TarEntryFormat.Pax, leaveOpen); @@ -358,17 +339,8 @@ private static async Task CreateFromDirectoryInternalAsync(string sourceDirector { bool baseDirectoryIsEmpty = true; DirectoryInfo di = new(sourceDirectoryName); - string basePath = di.FullName; + string basePath = GetBasePathForCreateFromDirectory(di, includeBaseDirectory); - if (includeBaseDirectory && di.Parent != null) - { - basePath = di.Parent.FullName; - } - - // Windows' MaxPath (260) is used as an arbitrary default capacity, as it is likely - // to be greater than the length of typical entry names from the file system, even - // on non-Windows platforms. The capacity will be increased, if needed. - const int DefaultCapacity = 260; char[] entryNameBuffer = ArrayPool.Shared.Rent(DefaultCapacity); try @@ -376,20 +348,12 @@ private static async Task CreateFromDirectoryInternalAsync(string sourceDirector foreach (FileSystemInfo file in di.EnumerateFileSystemInfos("*", SearchOption.AllDirectories)) { baseDirectoryIsEmpty = false; - - int entryNameLength = file.FullName.Length - basePath.Length; - Debug.Assert(entryNameLength > 0); - - bool isDirectory = file.Attributes.HasFlag(FileAttributes.Directory); - string entryName = ArchivingUtils.EntryFromPath(file.FullName, basePath.Length, entryNameLength, ref entryNameBuffer, appendPathSeparator: isDirectory); - await writer.WriteEntryAsync(file.FullName, entryName, cancellationToken).ConfigureAwait(false); + await writer.WriteEntryAsync(file.FullName, GetEntryNameForFileSystemInfo(file, basePath.Length, ref entryNameBuffer), cancellationToken).ConfigureAwait(false); } if (includeBaseDirectory && baseDirectoryIsEmpty) { - string entryName = ArchivingUtils.EntryFromPath(di.Name, 0, di.Name.Length, ref entryNameBuffer, appendPathSeparator: true); - PaxTarEntry entry = new PaxTarEntry(TarEntryType.Directory, entryName); - await writer.WriteEntryAsync(entry, cancellationToken).ConfigureAwait(false); + await writer.WriteEntryAsync(GetEntryForBaseDirectory(di.Name, ref entryNameBuffer), cancellationToken).ConfigureAwait(false); } } finally @@ -399,14 +363,41 @@ private static async Task CreateFromDirectoryInternalAsync(string sourceDirector } } + // Determines what should be the base path for all the entries when creating an archive. + private static string GetBasePathForCreateFromDirectory(DirectoryInfo di, bool includeBaseDirectory) + { + string basePath = di.FullName; + + if (includeBaseDirectory && di.Parent != null) + { + basePath = di.Parent.FullName; + } + + return basePath; + } + + // Constructs the entry name used for a filesystem entry when creating an archive. + private static string GetEntryNameForFileSystemInfo(FileSystemInfo file, int basePathLength, ref char[] entryNameBuffer) + { + int entryNameLength = file.FullName.Length - basePathLength; + Debug.Assert(entryNameLength > 0); + + bool isDirectory = file.Attributes.HasFlag(FileAttributes.Directory); + return ArchivingUtils.EntryFromPath(file.FullName, basePathLength, entryNameLength, ref entryNameBuffer, appendPathSeparator: isDirectory); + } + + // Constructs a PaxTarEntry for a base directory entry when creating an archive. + private static PaxTarEntry GetEntryForBaseDirectory(string name, ref char[] entryNameBuffer) + { + string entryName = ArchivingUtils.EntryFromPath(name, 0, name.Length, ref entryNameBuffer, appendPathSeparator: true); + return new PaxTarEntry(TarEntryType.Directory, entryName); + } + // Extracts an archive into the specified directory. // It assumes the destinationDirectoryName is a fully qualified path, and allows choosing if the archive stream should be left open or not. private static void ExtractToDirectoryInternal(Stream source, string destinationDirectoryPath, bool overwriteFiles, bool leaveOpen) { - Debug.Assert(source != null); - Debug.Assert(!string.IsNullOrEmpty(destinationDirectoryPath)); - Debug.Assert(Path.IsPathFullyQualified(destinationDirectoryPath)); - Debug.Assert(source.CanRead); + Debug.Assert(ValidateExtractToDirectoryArguments(source, destinationDirectoryPath)); using TarReader reader = new TarReader(source, leaveOpen); @@ -445,11 +436,7 @@ private static async Task ExtractToDirectoryInternalAsync(string sourceFileName, // It assumes the destinationDirectoryName is a fully qualified path, and allows choosing if the archive stream should be left open or not. private static async Task ExtractToDirectoryInternalAsync(Stream source, string destinationDirectoryPath, bool overwriteFiles, bool leaveOpen, CancellationToken cancellationToken) { - Debug.Assert(source != null); - Debug.Assert(!string.IsNullOrEmpty(destinationDirectoryPath)); - Debug.Assert(Path.IsPathFullyQualified(destinationDirectoryPath)); - Debug.Assert(source.CanRead); - + Debug.Assert(ValidateExtractToDirectoryArguments(source, destinationDirectoryPath)); cancellationToken.ThrowIfCancellationRequested(); TarReader reader = new TarReader(source, leaveOpen); @@ -465,5 +452,23 @@ private static async Task ExtractToDirectoryInternalAsync(Stream source, string } } } + + private static bool VerifyCreateFromDirectoryArguments(string sourceDirectoryName, Stream destination) + { + Debug.Assert(!string.IsNullOrEmpty(sourceDirectoryName)); + Debug.Assert(destination != null); + Debug.Assert(Path.IsPathFullyQualified(sourceDirectoryName)); + Debug.Assert(destination.CanWrite); + return true; + } + + private static bool ValidateExtractToDirectoryArguments(Stream source, string destinationDirectoryPath) + { + Debug.Assert(source != null); + Debug.Assert(!string.IsNullOrEmpty(destinationDirectoryPath)); + Debug.Assert(Path.IsPathFullyQualified(destinationDirectoryPath)); + Debug.Assert(source.CanRead); + return true; + } } } From 71d69010ff14e5db0d793e1df61f813000b9dbf9 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Tue, 28 Jun 2022 20:20:10 -0700 Subject: [PATCH 60/75] Reuse TarWriter.WriteEntry*(string,string) code. --- .../src/System/Formats/Tar/TarWriter.Unix.cs | 102 +----------------- .../System/Formats/Tar/TarWriter.Windows.cs | 83 ++------------ .../src/System/Formats/Tar/TarWriter.cs | 54 ++++++---- 3 files changed, 47 insertions(+), 192 deletions(-) diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarWriter.Unix.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarWriter.Unix.cs index 75ff67e7252667..7e678bcd918711 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarWriter.Unix.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarWriter.Unix.cs @@ -15,98 +15,10 @@ public sealed partial class TarWriter : IDisposable private readonly Dictionary _userIdentifiers = new Dictionary(); private readonly Dictionary _groupIdentifiers = new Dictionary(); - // Unix specific implementation of the method that reads an entry from disk and writes it into the archive stream. - private void ReadFileFromDiskAndWriteToArchiveStreamAsEntry(string fullPath, string entryName) + // Creates an entry for writing using the specified path and entryName. If this is being called from an async method, FileOptions should contain Asynchronous. + private TarEntry ConstructEntryForWriting(string fullPath, string entryName, FileOptions fileOptions) { - Interop.Sys.FileStatus status = default; - status.Mode = default; - status.Dev = default; - Interop.CheckIo(Interop.Sys.LStat(fullPath, out status)); - - TarEntryType entryType = (status.Mode & (uint)Interop.Sys.FileTypes.S_IFMT) switch - { - // Hard links are treated as regular files. - // Unix socket files do not get added to tar files. - Interop.Sys.FileTypes.S_IFBLK => TarEntryType.BlockDevice, - Interop.Sys.FileTypes.S_IFCHR => TarEntryType.CharacterDevice, - Interop.Sys.FileTypes.S_IFIFO => TarEntryType.Fifo, - Interop.Sys.FileTypes.S_IFLNK => TarEntryType.SymbolicLink, - Interop.Sys.FileTypes.S_IFREG => Format is TarEntryFormat.V7 ? TarEntryType.V7RegularFile : TarEntryType.RegularFile, - Interop.Sys.FileTypes.S_IFDIR => TarEntryType.Directory, - _ => throw new IOException(string.Format(SR.TarUnsupportedFile, fullPath)), - }; - - FileSystemInfo info = entryType is TarEntryType.Directory ? new DirectoryInfo(fullPath) : new FileInfo(fullPath); - - TarEntry entry = Format switch - { - TarEntryFormat.V7 => new V7TarEntry(entryType, entryName), - TarEntryFormat.Ustar => new UstarTarEntry(entryType, entryName), - TarEntryFormat.Pax => new PaxTarEntry(entryType, entryName), - TarEntryFormat.Gnu => new GnuTarEntry(entryType, entryName), - _ => throw new FormatException(string.Format(SR.TarInvalidFormat, Format)), - }; - - if (entryType is TarEntryType.BlockDevice or TarEntryType.CharacterDevice) - { - uint major; - uint minor; - unsafe - { - Interop.Sys.GetDeviceIdentifiers((ulong)status.RDev, &major, &minor); - } - - entry._header._devMajor = (int)major; - entry._header._devMinor = (int)minor; - } - - entry._header._mTime = info.LastWriteTimeUtc; - entry._header._aTime = info.LastAccessTimeUtc; - // FileSystemInfo does not have ChangeTime, but LastWriteTime and LastAccessTime make sure to add nanoseconds, so we should do the same here - entry._header._cTime = DateTimeOffset.FromUnixTimeSeconds(status.CTime).AddTicks(status.CTimeNsec / 100 /* nanoseconds per tick */); - - entry._header._mode = (status.Mode & 4095); // First 12 bits - - // Uid and UName - entry._header._uid = (int)status.Uid; - if (!_userIdentifiers.TryGetValue(status.Uid, out string? uName)) - { - uName = Interop.Sys.GetUserNameFromPasswd(status.Uid); - _userIdentifiers.Add(status.Uid, uName); - } - entry._header._uName = uName; - - // Gid and GName - entry._header._gid = (int)status.Gid; - if (!_groupIdentifiers.TryGetValue(status.Gid, out string? gName)) - { - gName = Interop.Sys.GetGroupName(status.Gid); - _groupIdentifiers.Add(status.Gid, gName); - } - entry._header._gName = gName; - - if (entry.EntryType == TarEntryType.SymbolicLink) - { - entry.LinkName = info.LinkTarget ?? string.Empty; - } - - if (entry.EntryType is TarEntryType.RegularFile or TarEntryType.V7RegularFile) - { - Debug.Assert(entry._header._dataStream == null); - entry._header._dataStream = File.OpenRead(fullPath); - } - - WriteEntry(entry); - if (entry._header._dataStream != null) - { - entry._header._dataStream.Dispose(); - } - } - - // Unix specific implementation of the method that reads an entry from disk and writes it into the archive stream. - private async Task ReadFileFromDiskAndWriteToArchiveStreamAsEntryAsync(string fullPath, string entryName, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); + Debug.Assert(!string.IsNullOrEmpty(fullPath)); Interop.Sys.FileStatus status = default; status.Mode = default; @@ -188,17 +100,13 @@ private async Task ReadFileFromDiskAndWriteToArchiveStreamAsEntryAsync(string fu Mode = FileMode.Open, Access = FileAccess.Read, Share = FileShare.Read, - Options = FileOptions.Asynchronous + Options = fileOptions }; entry._header._dataStream = new FileStream(fullPath, options); } - await WriteEntryAsync(entry, cancellationToken).ConfigureAwait(false); - if (entry._header._dataStream != null) - { - await entry._header._dataStream.DisposeAsync().ConfigureAwait(false); - } + return entry; } } } diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarWriter.Windows.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarWriter.Windows.cs index 1529e12a15fbb6..a3fa4ee0f595f8 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarWriter.Windows.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarWriter.Windows.cs @@ -15,11 +15,13 @@ public sealed partial class TarWriter : IDisposable private const UnixFileMode DefaultWindowsMode = UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute | UnixFileMode.GroupRead | UnixFileMode.GroupWrite | UnixFileMode.GroupExecute | UnixFileMode.OtherRead | UnixFileMode.OtherWrite | UnixFileMode.UserExecute; // Windows specific implementation of the method that reads an entry from disk and writes it into the archive stream. - private void ReadFileFromDiskAndWriteToArchiveStreamAsEntry(string fullPath, string entryName) + private TarEntry ConstructEntryForWriting(string fullPath, string entryName, FileOptions fileOptions) { - TarEntryType entryType; + Debug.Assert(!string.IsNullOrEmpty(fullPath)); + FileAttributes attributes = File.GetAttributes(fullPath); + TarEntryType entryType; if (attributes.HasFlag(FileAttributes.ReparsePoint)) { entryType = TarEntryType.SymbolicLink; @@ -66,87 +68,14 @@ private void ReadFileFromDiskAndWriteToArchiveStreamAsEntry(string fullPath, str Mode = FileMode.Open, Access = FileAccess.Read, Share = FileShare.Read, - Options = FileOptions.None + Options = fileOptions }; Debug.Assert(entry._header._dataStream == null); - entry._header._dataStream = File.Open(fullPath, options); - } - - WriteEntry(entry); - if (entry._header._dataStream != null) - { - entry._header._dataStream.Dispose(); - } - } - - // Windows specific implementation of the method that asynchronously reads an entry from disk and writes it into the archive stream. - private async Task ReadFileFromDiskAndWriteToArchiveStreamAsEntryAsync(string fullPath, string entryName, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - - TarEntryType entryType; - FileAttributes attributes = File.GetAttributes(fullPath); - - if (attributes.HasFlag(FileAttributes.ReparsePoint)) - { - entryType = TarEntryType.SymbolicLink; - } - else if (attributes.HasFlag(FileAttributes.Directory)) - { - entryType = TarEntryType.Directory; - } - else if (attributes.HasFlag(FileAttributes.Normal) || attributes.HasFlag(FileAttributes.Archive)) - { - entryType = Format is TarEntryFormat.V7 ? TarEntryType.V7RegularFile : TarEntryType.RegularFile; - } - else - { - throw new IOException(string.Format(SR.TarUnsupportedFile, fullPath)); - } - - TarEntry entry = Format switch - { - TarEntryFormat.V7 => new V7TarEntry(entryType, entryName), - TarEntryFormat.Ustar => new UstarTarEntry(entryType, entryName), - TarEntryFormat.Pax => new PaxTarEntry(entryType, entryName), - TarEntryFormat.Gnu => new GnuTarEntry(entryType, entryName), - _ => throw new FormatException(string.Format(SR.TarInvalidFormat, Format)), - }; - - FileSystemInfo info = attributes.HasFlag(FileAttributes.Directory) ? new DirectoryInfo(fullPath) : new FileInfo(fullPath); - - entry._header._mTime = new DateTimeOffset(info.LastWriteTimeUtc); - entry._header._aTime = new DateTimeOffset(info.LastAccessTimeUtc); - entry._header._cTime = new DateTimeOffset(info.LastWriteTimeUtc); // There is no "change time" property - - entry.Mode = DefaultWindowsMode; - - if (entry.EntryType == TarEntryType.SymbolicLink) - { - entry.LinkName = info.LinkTarget ?? string.Empty; - } - - if (entry.EntryType is TarEntryType.RegularFile or TarEntryType.V7RegularFile) - { - Debug.Assert(entry._header._dataStream == null); - - FileStreamOptions options = new() - { - Mode = FileMode.Open, - Access = FileAccess.Read, - Share = FileShare.Read, - Options = FileOptions.Asynchronous - }; - entry._header._dataStream = new FileStream(fullPath, options); } - await WriteEntryAsync(entry, cancellationToken).ConfigureAwait(false); - if (entry._header._dataStream != null) - { - await entry._header._dataStream.DisposeAsync().ConfigureAwait(false); - } + return entry; } } } 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 7b5bce335cbc72..111d3f006c062e 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 @@ -107,18 +107,8 @@ public async ValueTask DisposeAsync() /// An I/O problem occurred. public void WriteEntry(string fileName, string? entryName) { - ThrowIfDisposed(); - - ArgumentException.ThrowIfNullOrEmpty(fileName); - - string fullPath = Path.GetFullPath(fileName); - - if (string.IsNullOrEmpty(entryName)) - { - entryName = Path.GetFileName(fileName); - } - - ReadFileFromDiskAndWriteToArchiveStreamAsEntry(fullPath, entryName); + (string fullPath, string actualEntryName) = ValidateWriteEntryArguments(fileName, entryName); + ReadFileFromDiskAndWriteToArchiveStreamAsEntry(fullPath, actualEntryName); } /// @@ -137,18 +127,35 @@ public Task WriteEntryAsync(string fileName, string? entryName, CancellationToke { return Task.FromCanceled(cancellationToken); } - ThrowIfDisposed(); - ArgumentException.ThrowIfNullOrEmpty(fileName); + (string fullPath, string actualEntryName) = ValidateWriteEntryArguments(fileName, entryName); + return ReadFileFromDiskAndWriteToArchiveStreamAsEntryAsync(fullPath, actualEntryName, cancellationToken); + } - string fullPath = Path.GetFullPath(fileName); + // Reads an entry from disk and writes it into the archive stream. + private void ReadFileFromDiskAndWriteToArchiveStreamAsEntry(string fullPath, string entryName) + { + TarEntry entry = ConstructEntryForWriting(fullPath, entryName, FileOptions.None); - if (string.IsNullOrEmpty(entryName)) + WriteEntry(entry); + if (entry._header._dataStream != null) { - entryName = Path.GetFileName(fileName); + entry._header._dataStream.Dispose(); } + } - return ReadFileFromDiskAndWriteToArchiveStreamAsEntryAsync(fullPath, entryName, cancellationToken); + // Asynchronously reads an entry from disk and writes it into the archive stream. + private async Task ReadFileFromDiskAndWriteToArchiveStreamAsEntryAsync(string fullPath, string entryName, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + TarEntry entry = ConstructEntryForWriting(fullPath, entryName, FileOptions.Asynchronous); + + await WriteEntryAsync(entry, cancellationToken).ConfigureAwait(false); + if (entry._header._dataStream != null) + { + await entry._header._dataStream.DisposeAsync().ConfigureAwait(false); + } } /// @@ -395,5 +402,16 @@ private async ValueTask WriteFinalRecordsAsync() await _archiveStream.WriteAsync(emptyRecord, cancellationToken: default).ConfigureAwait(false); await _archiveStream.WriteAsync(emptyRecord, cancellationToken: default).ConfigureAwait(false); } + + private (string, string) ValidateWriteEntryArguments(string fileName, string? entryName) + { + ThrowIfDisposed(); + ArgumentException.ThrowIfNullOrEmpty(fileName); + + string fullPath = Path.GetFullPath(fileName); + string? actualEntryName = string.IsNullOrEmpty(entryName) ? Path.GetFileName(fileName) : entryName; + + return (fullPath, actualEntryName); + } } } From f629d9b386461b287b1c6eaf81e3ceb1e2e356fe Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Tue, 28 Jun 2022 20:33:36 -0700 Subject: [PATCH 61/75] Add tests for null check in TarWriter.WriteEntry(TarEntry) --- .../src/System/Formats/Tar/TarWriter.cs | 2 ++ .../TarWriter/TarWriter.WriteEntry.Base.cs | 21 +++++++++++++++++++ .../TarWriter.WriteEntry.Entry.Gnu.Tests.cs | 6 +++++- .../TarWriter.WriteEntry.Entry.Pax.Tests.cs | 6 +++++- .../TarWriter.WriteEntry.Entry.Ustar.Tests.cs | 6 +++++- .../TarWriter.WriteEntry.Entry.V7.Tests.cs | 6 +++++- ...rWriter.WriteEntryAsync.Entry.Gnu.Tests.cs | 6 +++++- ...rWriter.WriteEntryAsync.Entry.Pax.Tests.cs | 6 +++++- ...riter.WriteEntryAsync.Entry.Ustar.Tests.cs | 6 +++++- ...arWriter.WriteEntryAsync.Entry.V7.Tests.cs | 6 +++++- 10 files changed, 63 insertions(+), 8 deletions(-) diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarWriter.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarWriter.cs index 111d3f006c062e..5fb3fcab153c92 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 @@ -194,6 +194,7 @@ private async Task ReadFileFromDiskAndWriteToArchiveStreamAsEntryAsync(string fu public void WriteEntry(TarEntry entry) { ThrowIfDisposed(); + ArgumentNullException.ThrowIfNull(entry); byte[] rented = ArrayPool.Shared.Rent(minimumLength: TarHelpers.RecordSize); Span buffer = rented.AsSpan(0, TarHelpers.RecordSize); // minimumLength means the array could've been larger @@ -276,6 +277,7 @@ public Task WriteEntryAsync(TarEntry entry, CancellationToken cancellationToken return Task.FromCanceled(cancellationToken); } ThrowIfDisposed(); + ArgumentNullException.ThrowIfNull(entry); return WriteEntryAsyncInternal(entry, cancellationToken); } 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 b83fdc9451db43..69a367ffe1ee9e 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 @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -12,6 +13,26 @@ namespace System.Formats.Tar.Tests { public class TarWriter_WriteEntry_Base : TarTestsBase { + protected void WriteEntry_Null_Throws_Internal(TarEntryFormat format) + { + using MemoryStream archiveStream = new MemoryStream(); + using TarWriter writer = new TarWriter(archiveStream, format, leaveOpen: false); + Assert.Throws(() => writer.WriteEntry(null)); + } + + protected async Task WriteEntry_Null_Throws_Async_Internal(TarEntryFormat format) + { + MemoryStream archiveStream = new MemoryStream(); + await using (archiveStream) + { + TarWriter writer = new TarWriter(archiveStream, format, leaveOpen: false); + await using (writer) + { + await Assert.ThrowsAsync(() => writer.WriteEntryAsync(null)); + } + } + } + protected void VerifyDirectory(TarEntry entry, TarEntryFormat format, string name) { Assert.NotNull(entry); diff --git a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.Entry.Gnu.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.Entry.Gnu.Tests.cs index 1421456a31283f..d591ba9b6542a6 100644 --- a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.Entry.Gnu.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.Entry.Gnu.Tests.cs @@ -7,8 +7,12 @@ namespace System.Formats.Tar.Tests { // Tests specific to Gnu format. - public class TarWriter_WriteEntry_Gnu_Tests : TarTestsBase + public class TarWriter_WriteEntry_Gnu_Tests : TarWriter_WriteEntry_Base { + [Fact] + public void WriteEntry_Null_Throws() => + WriteEntry_Null_Throws_Internal(TarEntryFormat.Gnu); + [Fact] public void WriteRegularFile() { diff --git a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.Entry.Pax.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.Entry.Pax.Tests.cs index be8fe183571e23..6c9e79d83a1eff 100644 --- a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.Entry.Pax.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.Entry.Pax.Tests.cs @@ -9,8 +9,12 @@ namespace System.Formats.Tar.Tests { // Tests specific to PAX format. - public class TarWriter_WriteEntry_Pax_Tests : TarTestsBase + public class TarWriter_WriteEntry_Pax_Tests : TarWriter_WriteEntry_Base { + [Fact] + public void WriteEntry_Null_Throws() => + WriteEntry_Null_Throws_Internal(TarEntryFormat.Pax); + [Fact] public void WriteRegularFile() { diff --git a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.Entry.Ustar.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.Entry.Ustar.Tests.cs index ce4c3c8aa27251..da3b69051ab347 100644 --- a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.Entry.Ustar.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.Entry.Ustar.Tests.cs @@ -7,8 +7,12 @@ namespace System.Formats.Tar.Tests { // Tests specific to Ustar format. - public class TarWriter_WriteEntry_Ustar_Tests : TarTestsBase + public class TarWriter_WriteEntry_Ustar_Tests : TarWriter_WriteEntry_Base { + [Fact] + public void WriteEntry_Null_Throws() => + WriteEntry_Null_Throws_Internal(TarEntryFormat.Ustar); + [Fact] public void WriteRegularFile() { diff --git a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.Entry.V7.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.Entry.V7.Tests.cs index 8bb1ed51265f34..fb0cbb980ee6e2 100644 --- a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.Entry.V7.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.Entry.V7.Tests.cs @@ -7,8 +7,12 @@ namespace System.Formats.Tar.Tests { // Tests specific to V7 format. - public class TarWriter_WriteEntry_V7_Tests : TarTestsBase + public class TarWriter_WriteEntry_V7_Tests : TarWriter_WriteEntry_Base { + [Fact] + public void WriteEntry_Null_Throws() => + WriteEntry_Null_Throws_Internal(TarEntryFormat.V7); + [Fact] public void WriteRegularFile() { diff --git a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.Entry.Gnu.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.Entry.Gnu.Tests.cs index 9a535536442a25..3cd2b5e538b6ee 100644 --- a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.Entry.Gnu.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.Entry.Gnu.Tests.cs @@ -8,8 +8,12 @@ namespace System.Formats.Tar.Tests { // Tests specific to Gnu format. - public class TarWriter_WriteEntryAsync_Gnu_Tests : TarTestsBase + public class TarWriter_WriteEntryAsync_Gnu_Tests : TarWriter_WriteEntry_Base { + [Fact] + public Task WriteEntry_Null_Throws_Async() => + WriteEntry_Null_Throws_Async_Internal(TarEntryFormat.Gnu); + [Fact] public async Task WriteRegularFile_Async() { diff --git a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.Entry.Pax.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.Entry.Pax.Tests.cs index c1d146fc67a0ad..8b8cc035c16c41 100644 --- a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.Entry.Pax.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.Entry.Pax.Tests.cs @@ -10,8 +10,12 @@ namespace System.Formats.Tar.Tests { // Tests specific to PAX format. - public class TarWriter_WriteEntryAsync_Pax_Tests : TarTestsBase + public class TarWriter_WriteEntryAsync_Pax_Tests : TarWriter_WriteEntry_Base { + [Fact] + public Task WriteEntry_Null_Throws_Async() => + WriteEntry_Null_Throws_Async_Internal(TarEntryFormat.Pax); + [Fact] public async Task WriteRegularFile_Async() { diff --git a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.Entry.Ustar.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.Entry.Ustar.Tests.cs index 450e6416a7ec91..6baf7787412187 100644 --- a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.Entry.Ustar.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.Entry.Ustar.Tests.cs @@ -8,8 +8,12 @@ namespace System.Formats.Tar.Tests { // Tests specific to Ustar format. - public class TarWriter_WriteEntryAsync_Ustar_Tests : TarTestsBase + public class TarWriter_WriteEntryAsync_Ustar_Tests : TarWriter_WriteEntry_Base { + [Fact] + public Task WriteEntry_Null_Throws_Async() => + WriteEntry_Null_Throws_Async_Internal(TarEntryFormat.Ustar); + [Fact] public async Task WriteRegularFile_Async() { diff --git a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.Entry.V7.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.Entry.V7.Tests.cs index b38910de9ba182..5705fd072a7f9a 100644 --- a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.Entry.V7.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.Entry.V7.Tests.cs @@ -8,8 +8,12 @@ namespace System.Formats.Tar.Tests { // Tests specific to V7 format. - public class TarWriter_WriteEntryAsync_V7_Tests : TarTestsBase + public class TarWriter_WriteEntryAsync_V7_Tests : TarWriter_WriteEntry_Base { + [Fact] + public Task WriteEntry_Null_Throws_Async() => + WriteEntry_Null_Throws_Async_Internal(TarEntryFormat.V7); + [Fact] public async Task WriteRegularFile_Async() { From 2496e12cd0ed14fd63a5fdff5bc2e542d40cbe78 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Tue, 28 Jun 2022 20:41:40 -0700 Subject: [PATCH 62/75] Revert renaming of TarHeader.Read methods. Not needed. --- .../src/System/Formats/Tar/TarHeader.Read.cs | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) 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 f81a188d86a3a8..27a4b5831dc180 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 @@ -33,30 +33,30 @@ internal bool TryGetNextHeader(Stream archiveStream, bool copyData) try { // Confirms if v7 or pax, or tentatively selects ustar - if (!TryReadCommonAttributesSpan(buffer)) + if (!TryReadCommonAttributes(buffer)) { return false; } // Confirms if gnu, or tentatively selects ustar - ReadMagicAttributeSpan(buffer); + ReadMagicAttribute(buffer); if (_format != TarEntryFormat.V7) { // Confirms if gnu - ReadVersionAttributeSpan(buffer); + ReadVersionAttribute(buffer); // Fields that ustar, pax and gnu share identically - ReadPosixAndGnuSharedAttributesSpan(buffer); + ReadPosixAndGnuSharedAttributes(buffer); Debug.Assert(_format is TarEntryFormat.Ustar or TarEntryFormat.Pax or TarEntryFormat.Gnu); if (_format == TarEntryFormat.Ustar) { - ReadUstarAttributesSpan(buffer); + ReadUstarAttributes(buffer); } else if (_format == TarEntryFormat.Gnu) { - ReadGnuAttributesSpan(buffer); + ReadGnuAttributes(buffer); } // In PAX, there is nothing to read in this section (empty space) } @@ -89,30 +89,30 @@ internal bool TryGetNextHeader(Stream archiveStream, bool copyData) await archiveStream.ReadExactlyAsync(buffer, cancellationToken).ConfigureAwait(false); // Confirms if v7 or pax, or tentatively selects ustar - if (!header.TryReadCommonAttributesSpan(buffer.Span)) + if (!header.TryReadCommonAttributes(buffer.Span)) { return (false, default); } // Confirms if gnu, or tentatively selects ustar - header.ReadMagicAttributeSpan(buffer.Span); + header.ReadMagicAttribute(buffer.Span); if (header._format != TarEntryFormat.V7) { // Confirms if gnu - header.ReadVersionAttributeSpan(buffer.Span); + header.ReadVersionAttribute(buffer.Span); // Fields that ustar, pax and gnu share identically - header.ReadPosixAndGnuSharedAttributesSpan(buffer.Span); + header.ReadPosixAndGnuSharedAttributes(buffer.Span); Debug.Assert(header._format is TarEntryFormat.Ustar or TarEntryFormat.Pax or TarEntryFormat.Gnu); if (header._format == TarEntryFormat.Ustar) { - header.ReadUstarAttributesSpan(buffer.Span); + header.ReadUstarAttributes(buffer.Span); } else if (header._format == TarEntryFormat.Gnu) { - header.ReadGnuAttributesSpan(buffer.Span); + header.ReadGnuAttributes(buffer.Span); } // In PAX, there is nothing to read in this section (empty space) } @@ -394,7 +394,7 @@ private static async ValueTask GetDataStreamAsync(Stream archiveStream, // Attempts to read the fields shared by all formats and stores them in their expected data type. // Throws if any data type conversion fails. // Returns true on success, false if checksum is zero. - private bool TryReadCommonAttributesSpan(Span buffer) + private bool TryReadCommonAttributes(Span buffer) { // Start by collecting fields that need special checks that return early when data is wrong @@ -457,7 +457,7 @@ TarEntryType.SparseFile or // Reads fields only found in ustar format or above and converts them to their expected data type. // Throws if any conversion fails. - private void ReadMagicAttributeSpan(Span buffer) + private void ReadMagicAttribute(Span buffer) { Span magic = buffer.Slice(FieldLocations.Magic, FieldLengths.Magic); @@ -484,7 +484,7 @@ private void ReadMagicAttributeSpan(Span buffer) // Reads the version string and determines the format depending on its value. // Throws if converting the bytes to string fails or if an unexpected version string is found. - private void ReadVersionAttributeSpan(Span buffer) + private void ReadVersionAttribute(Span buffer) { if (_format == TarEntryFormat.V7) { @@ -510,7 +510,7 @@ private void ReadVersionAttributeSpan(Span buffer) // Reads the attributes shared by the POSIX and GNU formats. // Throws if converting the bytes to their expected data type fails. - private void ReadPosixAndGnuSharedAttributesSpan(Span buffer) + private void ReadPosixAndGnuSharedAttributes(Span buffer) { // Convert the byte arrays _uName = TarHelpers.GetTrimmedAsciiString(buffer.Slice(FieldLocations.UName, FieldLengths.UName)); @@ -530,7 +530,7 @@ private void ReadPosixAndGnuSharedAttributesSpan(Span buffer) // Reads attributes specific to the GNU format. // Throws if any conversion fails. - private void ReadGnuAttributesSpan(Span buffer) + private void ReadGnuAttributes(Span buffer) { // Convert byte arrays long aTime = TarHelpers.GetTenBaseLongFromOctalAsciiChars(buffer.Slice(FieldLocations.ATime, FieldLengths.ATime)); @@ -544,7 +544,7 @@ private void ReadGnuAttributesSpan(Span buffer) // Reads the ustar prefix attribute. // Throws if a conversion to an expected data type fails. - private void ReadUstarAttributesSpan(Span buffer) + private void ReadUstarAttributes(Span buffer) { _prefix = TarHelpers.GetTrimmedUtf8String(buffer.Slice(FieldLocations.Prefix, FieldLengths.Prefix)); From 66f851d26b8988ffc480e5855645d6f0ea41c0c8 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Tue, 28 Jun 2022 20:45:48 -0700 Subject: [PATCH 63/75] Remove unnecessary try-finally in TarHeader.TryGetNextHeader --- .../src/System/Formats/Tar/TarHeader.Read.cs | 56 +++++++++---------- 1 file changed, 25 insertions(+), 31 deletions(-) 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 27a4b5831dc180..26ee294683b4cc 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 @@ -30,45 +30,39 @@ internal bool TryGetNextHeader(Stream archiveStream, bool copyData) archiveStream.ReadExactly(buffer); - try + // Confirms if v7 or pax, or tentatively selects ustar + if (!TryReadCommonAttributes(buffer)) { - // Confirms if v7 or pax, or tentatively selects ustar - if (!TryReadCommonAttributes(buffer)) - { - return false; - } + return false; + } - // Confirms if gnu, or tentatively selects ustar - ReadMagicAttribute(buffer); + // Confirms if gnu, or tentatively selects ustar + ReadMagicAttribute(buffer); - if (_format != TarEntryFormat.V7) - { - // Confirms if gnu - ReadVersionAttribute(buffer); + if (_format != TarEntryFormat.V7) + { + // Confirms if gnu + ReadVersionAttribute(buffer); - // Fields that ustar, pax and gnu share identically - ReadPosixAndGnuSharedAttributes(buffer); + // Fields that ustar, pax and gnu share identically + ReadPosixAndGnuSharedAttributes(buffer); - Debug.Assert(_format is TarEntryFormat.Ustar or TarEntryFormat.Pax or TarEntryFormat.Gnu); - if (_format == TarEntryFormat.Ustar) - { - ReadUstarAttributes(buffer); - } - else if (_format == TarEntryFormat.Gnu) - { - ReadGnuAttributes(buffer); - } - // In PAX, there is nothing to read in this section (empty space) + Debug.Assert(_format is TarEntryFormat.Ustar or TarEntryFormat.Pax or TarEntryFormat.Gnu); + if (_format == TarEntryFormat.Ustar) + { + ReadUstarAttributes(buffer); } + else if (_format == TarEntryFormat.Gnu) + { + ReadGnuAttributes(buffer); + } + // In PAX, there is nothing to read in this section (empty space) + } - ProcessDataBlock(archiveStream, copyData); + ProcessDataBlock(archiveStream, copyData); - ArrayPool.Shared.Return(rented); - return true; - } - finally - { - } + ArrayPool.Shared.Return(rented); + return true; } // Asynchronously attempts read all the fields of the next header. From bc9c1fdbccb1c67afec43960895886bfc28b7edf Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Tue, 28 Jun 2022 20:48:54 -0700 Subject: [PATCH 64/75] GetDataStreamAsync should return a null if size is 0, not a MemoryStream. --- .../src/System/Formats/Tar/TarHeader.Read.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 26ee294683b4cc..a63b160a3ac9de 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 @@ -364,13 +364,13 @@ private void ProcessDataBlock(Stream archiveStream, bool copyData) // If copyData is true, then a total number of _size bytes will be copied to a new MemoryStream, which is then returned. // Otherwise, if the archive stream is seekable, returns a seekable wrapper stream. // Otherwise, it returns an unseekable wrapper stream. - private static async ValueTask GetDataStreamAsync(Stream archiveStream, bool copyData, long size, CancellationToken cancellationToken) + private static async ValueTask GetDataStreamAsync(Stream archiveStream, bool copyData, long size, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); if (size == 0) { - return new MemoryStream(); + return null; } if (copyData) From 4733094855b170c4b6404ddaf6e739119267957e Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Tue, 28 Jun 2022 20:56:56 -0700 Subject: [PATCH 65/75] Reuse code in TarHeader.ReadExtendedAttributesBlock* --- .../src/System/Formats/Tar/TarHeader.Read.cs | 29 +++++++------------ 1 file changed, 11 insertions(+), 18 deletions(-) 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 a63b160a3ac9de..e544d769010a14 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 @@ -576,24 +576,12 @@ private void ReadExtendedAttributesBlock(Stream archiveStream) byte[] buffer = new byte[(int)_size]; archiveStream.ReadExactly(buffer); - string dataAsString = TarHelpers.GetTrimmedUtf8String(buffer); - - using StringReader reader = new(dataAsString); - - while (TryGetNextExtendedAttribute(reader, out string? key, out string? value)) - { - _extendedAttributes ??= new Dictionary(); - if (_extendedAttributes.ContainsKey(key)) - { - throw new FormatException(string.Format(SR.TarDuplicateExtendedAttribute, _name)); - } - _extendedAttributes.Add(key, value); - } + _extendedAttributes = ReadExtendedAttributesFromBuffer(buffer, _name); } // Asynchronously collects the extended attributes found in the data section of a PAX entry of type 'x' or 'g'. // Throws if end of stream is reached or if an attribute is malformed. - private static async ValueTask> ReadExtendedAttributesBlockAsync(Stream archiveStream, TarEntryType entryType, long size, string name, CancellationToken cancellationToken) + private static async ValueTask?> ReadExtendedAttributesBlockAsync(Stream archiveStream, TarEntryType entryType, long size, string name, CancellationToken cancellationToken) { Debug.Assert(entryType is TarEntryType.ExtendedAttributes or TarEntryType.GlobalExtendedAttributes); @@ -606,17 +594,22 @@ private static async ValueTask> ReadExtendedAttribute throw new InvalidOperationException(string.Format(SR.TarSizeFieldTooLargeForEntryType, entryType.ToString())); } - // Regardless of the size, this entry should always have a valid dictionary object - Dictionary extendedAttributes = new Dictionary(); - if (size == 0) { - return extendedAttributes; + return null; } byte[] buffer = new byte[(int)size]; await archiveStream.ReadExactlyAsync(buffer, cancellationToken).ConfigureAwait(false); + return ReadExtendedAttributesFromBuffer(buffer, name); + } + + // Returns a dictionary containing the extended attributes collected from the provided byte buffer. + private static Dictionary ReadExtendedAttributesFromBuffer(ReadOnlySpan buffer, string name) + { + Dictionary extendedAttributes = new(); + string dataAsString = TarHelpers.GetTrimmedUtf8String(buffer); using StringReader reader = new(dataAsString); From 5f8f30c3a6fb3a7cd77e785b9f5c5261da9f1826 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Wed, 29 Jun 2022 10:46:56 -0700 Subject: [PATCH 66/75] Make GetBasePathForcreateFromDirectory a one-liner. --- .../src/System/Formats/Tar/TarFile.cs | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarFile.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarFile.cs index 8865c072dd2f88..8ab3846232eb5b 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarFile.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarFile.cs @@ -364,17 +364,8 @@ private static async Task CreateFromDirectoryInternalAsync(string sourceDirector } // Determines what should be the base path for all the entries when creating an archive. - private static string GetBasePathForCreateFromDirectory(DirectoryInfo di, bool includeBaseDirectory) - { - string basePath = di.FullName; - - if (includeBaseDirectory && di.Parent != null) - { - basePath = di.Parent.FullName; - } - - return basePath; - } + private static string GetBasePathForCreateFromDirectory(DirectoryInfo di, bool includeBaseDirectory) => + includeBaseDirectory && di.Parent != null ? di.Parent.FullName : di.FullName; // Constructs the entry name used for a filesystem entry when creating an archive. private static string GetEntryNameForFileSystemInfo(FileSystemInfo file, int basePathLength, ref char[] entryNameBuffer) From 631c2ce9c8f777fc88ccdeb178c9d38bf2c2a30c Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Wed, 29 Jun 2022 10:50:56 -0700 Subject: [PATCH 67/75] Make internal argument verification methods conditioned to debug. --- .../src/System/Formats/Tar/TarFile.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarFile.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarFile.cs index 8ab3846232eb5b..8ce12347d73a3c 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarFile.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarFile.cs @@ -275,7 +275,7 @@ public static Task ExtractToDirectoryAsync(string sourceFileName, string destina // It assumes the sourceDirectoryName is a fully qualified path, and allows choosing if the archive stream should be left open or not. private static void CreateFromDirectoryInternal(string sourceDirectoryName, Stream destination, bool includeBaseDirectory, bool leaveOpen) { - Debug.Assert(VerifyCreateFromDirectoryArguments(sourceDirectoryName, destination)); + VerifyCreateFromDirectoryArguments(sourceDirectoryName, destination); using (TarWriter writer = new TarWriter(destination, TarEntryFormat.Pax, leaveOpen)) { @@ -331,7 +331,7 @@ private static async Task CreateFromDirectoryInternalAsync(string sourceDirector // It assumes the sourceDirectoryName is a fully qualified path, and allows choosing if the archive stream should be left open or not. private static async Task CreateFromDirectoryInternalAsync(string sourceDirectoryName, Stream destination, bool includeBaseDirectory, bool leaveOpen, CancellationToken cancellationToken) { - Debug.Assert(VerifyCreateFromDirectoryArguments(sourceDirectoryName, destination)); + VerifyCreateFromDirectoryArguments(sourceDirectoryName, destination); cancellationToken.ThrowIfCancellationRequested(); TarWriter writer = new TarWriter(destination, TarEntryFormat.Pax, leaveOpen); @@ -388,7 +388,7 @@ private static PaxTarEntry GetEntryForBaseDirectory(string name, ref char[] entr // It assumes the destinationDirectoryName is a fully qualified path, and allows choosing if the archive stream should be left open or not. private static void ExtractToDirectoryInternal(Stream source, string destinationDirectoryPath, bool overwriteFiles, bool leaveOpen) { - Debug.Assert(ValidateExtractToDirectoryArguments(source, destinationDirectoryPath)); + VerifyExtractToDirectoryArguments(source, destinationDirectoryPath); using TarReader reader = new TarReader(source, leaveOpen); @@ -427,7 +427,7 @@ private static async Task ExtractToDirectoryInternalAsync(string sourceFileName, // It assumes the destinationDirectoryName is a fully qualified path, and allows choosing if the archive stream should be left open or not. private static async Task ExtractToDirectoryInternalAsync(Stream source, string destinationDirectoryPath, bool overwriteFiles, bool leaveOpen, CancellationToken cancellationToken) { - Debug.Assert(ValidateExtractToDirectoryArguments(source, destinationDirectoryPath)); + VerifyExtractToDirectoryArguments(source, destinationDirectoryPath); cancellationToken.ThrowIfCancellationRequested(); TarReader reader = new TarReader(source, leaveOpen); @@ -444,22 +444,22 @@ private static async Task ExtractToDirectoryInternalAsync(Stream source, string } } - private static bool VerifyCreateFromDirectoryArguments(string sourceDirectoryName, Stream destination) + [Conditional("DEBUG")] + private static void VerifyCreateFromDirectoryArguments(string sourceDirectoryName, Stream destination) { Debug.Assert(!string.IsNullOrEmpty(sourceDirectoryName)); Debug.Assert(destination != null); Debug.Assert(Path.IsPathFullyQualified(sourceDirectoryName)); Debug.Assert(destination.CanWrite); - return true; } - private static bool ValidateExtractToDirectoryArguments(Stream source, string destinationDirectoryPath) + [Conditional("DEBUG")] + private static void VerifyExtractToDirectoryArguments(Stream source, string destinationDirectoryPath) { Debug.Assert(source != null); Debug.Assert(!string.IsNullOrEmpty(destinationDirectoryPath)); Debug.Assert(Path.IsPathFullyQualified(destinationDirectoryPath)); Debug.Assert(source.CanRead); - return true; } } } From 0f0a5bab982da3ded1fc22123be8281578e91c78 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Wed, 29 Jun 2022 10:58:34 -0700 Subject: [PATCH 68/75] Use stackalloc in TryGetNextHeader instead of ArrayPool rent. --- .../src/System/Formats/Tar/TarHeader.Read.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) 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 e544d769010a14..9c28bf699682a8 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 @@ -23,10 +23,7 @@ internal partial struct TarHeader internal bool TryGetNextHeader(Stream archiveStream, bool copyData) { // The four supported formats have a header that fits in the default record size - byte[] rented = ArrayPool.Shared.Rent(minimumLength: TarHelpers.RecordSize); - - Span buffer = rented.AsSpan(0, TarHelpers.RecordSize); // minimumLength means the array could've been larger - buffer.Clear(); // Rented arrays aren't clean + Span buffer = stackalloc byte[TarHelpers.RecordSize]; archiveStream.ReadExactly(buffer); @@ -61,7 +58,6 @@ internal bool TryGetNextHeader(Stream archiveStream, bool copyData) ProcessDataBlock(archiveStream, copyData); - ArrayPool.Shared.Return(rented); return true; } From 6cd55bcee3a9339db4bc6772a844281cc750613d Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Wed, 29 Jun 2022 11:19:12 -0700 Subject: [PATCH 69/75] Use Array.MaxLength instead of int.MaxValue for array size pre-checks. --- .../src/System/Formats/Tar/TarHeader.Read.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 9c28bf699682a8..fdc0f0c1cf7d30 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 @@ -562,9 +562,9 @@ private void ReadExtendedAttributesBlock(Stream archiveStream) return; } - // It is not expected that the extended attributes data section will be longer than int.MaxValue, considering + // It is not expected that the extended attributes data section will be longer than Array.MaxLength, considering // 4096 is a common max path length, and also the size field is 12 bytes long, which is under int.MaxValue. - if (_size > int.MaxValue) + if (_size > Array.MaxLength) { throw new InvalidOperationException(string.Format(SR.TarSizeFieldTooLargeForEntryType, _typeFlag.ToString())); } @@ -583,9 +583,9 @@ private void ReadExtendedAttributesBlock(Stream archiveStream) cancellationToken.ThrowIfCancellationRequested(); - // It is not expected that the extended attributes data section will be longer than int.MaxValue, considering + // It is not expected that the extended attributes data section will be longer than Array.MaxLength, considering // 4096 is a common max path length, and also the size field is 12 bytes long, which is under int.MaxValue. - if (size > int.MaxValue) + if (size > Array.MaxLength) { throw new InvalidOperationException(string.Format(SR.TarSizeFieldTooLargeForEntryType, entryType.ToString())); } @@ -629,7 +629,7 @@ private void ReadGnuLongPathDataBlock(Stream archiveStream) { Debug.Assert(_typeFlag is TarEntryType.LongLink or TarEntryType.LongPath); - if (_size > int.MaxValue) + if (_size > Array.MaxLength) { throw new InvalidOperationException(string.Format(SR.TarSizeFieldTooLargeForEntryType, _typeFlag.ToString())); } @@ -668,7 +668,7 @@ private void ReadGnuLongPathDataBlock(Stream archiveStream) return null; } - if (size > int.MaxValue) + if (size > Array.MaxLength) { throw new InvalidOperationException(string.Format(SR.TarSizeFieldTooLargeForEntryType, entryType.ToString())); } From eaa1e85569eb1210671a8896a4ab53b1eab451ea Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Wed, 29 Jun 2022 11:28:21 -0700 Subject: [PATCH 70/75] Use Math.Min for minimumLength in ArrayPool rent in CopyBytes* methods. --- .../System.Formats.Tar/src/System/Formats/Tar/TarHelpers.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 608a8cb8c97404..71620dbaa6317c 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 @@ -71,7 +71,7 @@ internal static async ValueTask AdvanceStreamAsync(Stream archiveStream, long by // Helps copy a specific number of bytes from one stream into another. internal static void CopyBytes(Stream origin, Stream destination, long bytesToCopy) { - byte[] buffer = ArrayPool.Shared.Rent(minimumLength: MaxBufferLength); + byte[] buffer = ArrayPool.Shared.Rent(minimumLength: (int)Math.Min(MaxBufferLength, bytesToCopy)); while (bytesToCopy > 0) { int currentLengthToRead = (int)Math.Min(MaxBufferLength, bytesToCopy); @@ -87,7 +87,7 @@ internal static async ValueTask CopyBytesAsync(Stream origin, Stream destination { cancellationToken.ThrowIfCancellationRequested(); - byte[] buffer = ArrayPool.Shared.Rent(minimumLength: MaxBufferLength); + byte[] buffer = ArrayPool.Shared.Rent(minimumLength: (int)Math.Min(MaxBufferLength, bytesToCopy)); while (bytesToCopy > 0) { int currentLengthToRead = (int)Math.Min(MaxBufferLength, bytesToCopy); From 78b8e2a522f05261991278c9d5a637888591069c Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Wed, 29 Jun 2022 11:37:29 -0700 Subject: [PATCH 71/75] spacing --- .../src/System/Formats/Tar/TarHeader.Write.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 7c0b9d40348d7d..da22adad051167 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 @@ -220,7 +220,7 @@ internal async Task WriteAsGnuAsync(Stream archiveStream, Memory buff } // Third, we write this header as a normal one - return await WriteAsGnuInternalAsync(archiveStream, buffer, cancellationToken).ConfigureAwait(false); + return await WriteAsGnuInternalAsync(archiveStream, buffer, cancellationToken).ConfigureAwait(false); } // Creates and returns a GNU long metadata header, with the specified long text written into its data stream. From 85527965aca2c0c4c31592ec54690bea1093738f Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Wed, 29 Jun 2022 11:42:43 -0700 Subject: [PATCH 72/75] Only one WriteLeftAlignedBytesAndGetChecksum method is needed. Revert changes adding a memory-based method. --- .../src/System/Formats/Tar/TarHeader.Write.cs | 38 ++++++------------- 1 file changed, 11 insertions(+), 27 deletions(-) diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs index da22adad051167..1e10774464da39 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 @@ -416,7 +416,7 @@ private int WriteNameSpan(Span buffer, out byte[] fullNameBytes) { fullNameBytes = Encoding.ASCII.GetBytes(_name); int nameBytesLength = Math.Min(fullNameBytes.Length, FieldLengths.Name); - int checksum = WriteLeftAlignedBytesAndGetChecksumSpan(fullNameBytes.AsSpan(0, nameBytesLength), buffer.Slice(FieldLocations.Name, FieldLengths.Name)); + int checksum = WriteLeftAlignedBytesAndGetChecksum(fullNameBytes.AsSpan(0, nameBytesLength), buffer.Slice(FieldLocations.Name, FieldLengths.Name)); return checksum; } @@ -425,7 +425,7 @@ private int WriteNameMemory(Memory buffer, out byte[] fullNameBytes) { fullNameBytes = Encoding.ASCII.GetBytes(_name); int nameBytesLength = Math.Min(fullNameBytes.Length, FieldLengths.Name); - int checksum = WriteLeftAlignedBytesAndGetChecksumMemory(fullNameBytes.AsMemory(0, nameBytesLength), buffer.Slice(FieldLocations.Name, FieldLengths.Name)); + int checksum = WriteLeftAlignedBytesAndGetChecksum(fullNameBytes.AsSpan(0, nameBytesLength), buffer.Span.Slice(FieldLocations.Name, FieldLengths.Name)); return checksum; } @@ -436,7 +436,7 @@ private int WritePosixNameSpan(Span buffer) if (fullNameBytes.Length > FieldLengths.Name) { int prefixBytesLength = Math.Min(fullNameBytes.Length - FieldLengths.Name, FieldLengths.Name); - checksum += WriteLeftAlignedBytesAndGetChecksumSpan(fullNameBytes.AsSpan(FieldLengths.Name, prefixBytesLength), buffer.Slice(FieldLocations.Prefix, FieldLengths.Prefix)); + checksum += WriteLeftAlignedBytesAndGetChecksum(fullNameBytes.AsSpan(FieldLengths.Name, prefixBytesLength), buffer.Slice(FieldLocations.Prefix, FieldLengths.Prefix)); } return checksum; } @@ -448,7 +448,7 @@ private int WritePosixNameMemory(Memory buffer) if (fullNameBytes.Length > FieldLengths.Name) { int prefixBytesLength = Math.Min(fullNameBytes.Length - FieldLengths.Name, FieldLengths.Name); - checksum += WriteLeftAlignedBytesAndGetChecksumMemory(fullNameBytes.AsMemory(FieldLengths.Name, prefixBytesLength), buffer.Slice(FieldLocations.Prefix, FieldLengths.Prefix)); + checksum += WriteLeftAlignedBytesAndGetChecksum(fullNameBytes.AsSpan(FieldLengths.Name, prefixBytesLength), buffer.Span.Slice(FieldLocations.Prefix, FieldLengths.Prefix)); } return checksum; } @@ -532,16 +532,16 @@ private long GetTotalDataBytesToWrite() // Writes the magic and version fields of a ustar or pax entry into the specified spans. private static int WritePosixMagicAndVersion(Span buffer) { - int checksum = WriteLeftAlignedBytesAndGetChecksumSpan(PaxMagicBytes, buffer.Slice(FieldLocations.Magic, FieldLengths.Magic)); - checksum += WriteLeftAlignedBytesAndGetChecksumSpan(PaxVersionBytes, buffer.Slice(FieldLocations.Version, FieldLengths.Version)); + int checksum = WriteLeftAlignedBytesAndGetChecksum(PaxMagicBytes, buffer.Slice(FieldLocations.Magic, FieldLengths.Magic)); + checksum += WriteLeftAlignedBytesAndGetChecksum(PaxVersionBytes, buffer.Slice(FieldLocations.Version, FieldLengths.Version)); return checksum; } // Writes the magic and vresion fields of a gnu entry into the specified spans. private static int WriteGnuMagicAndVersion(Span buffer) { - int checksum = WriteLeftAlignedBytesAndGetChecksumSpan(GnuMagicBytes, buffer.Slice(FieldLocations.Magic, FieldLengths.Magic)); - checksum += WriteLeftAlignedBytesAndGetChecksumSpan(GnuVersionBytes, buffer.Slice(FieldLocations.Version, FieldLengths.Version)); + int checksum = WriteLeftAlignedBytesAndGetChecksum(GnuMagicBytes, buffer.Slice(FieldLocations.Magic, FieldLengths.Magic)); + checksum += WriteLeftAlignedBytesAndGetChecksum(GnuVersionBytes, buffer.Slice(FieldLocations.Version, FieldLengths.Version)); return checksum; } @@ -581,7 +581,7 @@ private int WriteGnuFields(Span buffer) if (_gnuUnusedBytes != null) { - checksum += WriteLeftAlignedBytesAndGetChecksumSpan(_gnuUnusedBytes, buffer.Slice(FieldLocations.GnuUnused, FieldLengths.AllGnuUnused)); + checksum += WriteLeftAlignedBytesAndGetChecksum(_gnuUnusedBytes, buffer.Slice(FieldLocations.GnuUnused, FieldLengths.AllGnuUnused)); } return checksum; @@ -758,7 +758,7 @@ internal int WriteChecksum(int checksum, Span buffer) } // Writes the specified bytes into the specified destination, aligned to the left. Returns the sum of the value of all the bytes that were written. - private static int WriteLeftAlignedBytesAndGetChecksumSpan(ReadOnlySpan bytesToWrite, Span destination) + private static int WriteLeftAlignedBytesAndGetChecksum(ReadOnlySpan bytesToWrite, Span destination) { Debug.Assert(destination.Length > 1); @@ -773,22 +773,6 @@ private static int WriteLeftAlignedBytesAndGetChecksumSpan(ReadOnlySpan by return checksum; } - // Writes the specified bytes into the specified destination, aligned to the left. Returns the sum of the value of all the bytes that were written. - private static int WriteLeftAlignedBytesAndGetChecksumMemory(ReadOnlyMemory bytesToWrite, Memory destination) - { - Debug.Assert(destination.Length > 1); - - int checksum = 0; - - for (int i = 0, j = 0; i < destination.Length && j < bytesToWrite.Length; i++, j++) - { - destination.Span[i] = bytesToWrite.Span[j]; - checksum += destination.Span[i]; - } - - return checksum; - } - // Writes the specified bytes aligned to the right, filling all the leading bytes with the zero char 0x30, // ensuring a null terminator is included at the end of the specified span. private static int WriteRightAlignedBytesAndGetChecksum(ReadOnlySpan bytesToWrite, Span destination) @@ -838,7 +822,7 @@ private static int WriteAsTimestamp(DateTimeOffset timestamp, Span destina private static int WriteAsAsciiString(string str, Span buffer, int location, int length) { byte[] bytes = Encoding.ASCII.GetBytes(str); - return WriteLeftAlignedBytesAndGetChecksumSpan(bytes.AsSpan(), buffer.Slice(location, length)); + return WriteLeftAlignedBytesAndGetChecksum(bytes.AsSpan(), buffer.Slice(location, length)); } // Gets the special name for the 'name' field in an extended attribute entry. From 3d444c2877d412acfcc20a0108b758c6d7edb7cf Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Wed, 29 Jun 2022 12:40:58 -0700 Subject: [PATCH 73/75] Remove unnecessary TarHeader.Write memory-based methods, use only span ones. --- .../src/System/Formats/Tar/TarHeader.Write.cs | 152 +++++++----------- 1 file changed, 55 insertions(+), 97 deletions(-) diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs index 1e10774464da39..835784f1678057 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 @@ -32,7 +32,7 @@ internal void WriteAsV7(Stream archiveStream, Span buffer) long actualLength = GetTotalDataBytesToWrite(); TarEntryType actualEntryType = GetCorrectTypeFlagForFormat(TarEntryFormat.V7); - int checksum = WriteNameSpan(buffer, out _); + int checksum = WriteName(buffer, out _); checksum += WriteCommonFields(buffer, actualLength, actualEntryType); _checksum = WriteChecksum(checksum, buffer); @@ -52,7 +52,7 @@ internal async Task WriteAsV7Async(Stream archiveStream, Memory buffe long actualLength = GetTotalDataBytesToWrite(); TarEntryType actualEntryType = GetCorrectTypeFlagForFormat(TarEntryFormat.V7); - int checksum = WriteNameMemory(buffer, out _); + int checksum = WriteName(buffer.Span, out _); checksum += WriteCommonFields(buffer.Span, actualLength, actualEntryType); int finalChecksum = WriteChecksum(checksum, buffer.Span); @@ -72,7 +72,7 @@ internal void WriteAsUstar(Stream archiveStream, Span buffer) long actualLength = GetTotalDataBytesToWrite(); TarEntryType actualEntryType = GetCorrectTypeFlagForFormat(TarEntryFormat.Ustar); - int checksum = WritePosixNameSpan(buffer); + int checksum = WritePosixName(buffer); checksum += WriteCommonFields(buffer, actualLength, actualEntryType); checksum += WritePosixMagicAndVersion(buffer); checksum += WritePosixAndGnuSharedFields(buffer); @@ -94,7 +94,7 @@ internal async Task WriteAsUstarAsync(Stream archiveStream, Memory bu long actualLength = GetTotalDataBytesToWrite(); TarEntryType actualEntryType = GetCorrectTypeFlagForFormat(TarEntryFormat.Ustar); - int checksum = WritePosixNameMemory(buffer); + int checksum = WritePosixName(buffer.Span); checksum += WriteCommonFields(buffer.Span, actualLength, actualEntryType); checksum += WritePosixMagicAndVersion(buffer.Span); checksum += WritePosixAndGnuSharedFields(buffer.Span); @@ -220,25 +220,15 @@ internal async Task WriteAsGnuAsync(Stream archiveStream, Memory buff } // Third, we write this header as a normal one - return await WriteAsGnuInternalAsync(archiveStream, buffer, cancellationToken).ConfigureAwait(false); + return await WriteAsGnuInternalAsync(archiveStream, buffer, cancellationToken).ConfigureAwait(false); } // Creates and returns a GNU long metadata header, with the specified long text written into its data stream. private static TarHeader GetGnuLongMetadataHeader(TarEntryType entryType, string longText) { - Debug.Assert((entryType is TarEntryType.LongPath && longText.Length > FieldLengths.Name) || - (entryType is TarEntryType.LongLink && longText.Length > FieldLengths.LinkName)); + TarHeader longMetadataHeader = GetDefaultGnuLongMetadataHeader(longText.Length, entryType); + Debug.Assert(longMetadataHeader._dataStream != null); - TarHeader longMetadataHeader = default; - - longMetadataHeader._name = GnuLongMetadataName; // Same name for both longpath or longlink - longMetadataHeader._mode = (int)TarHelpers.DefaultMode; - longMetadataHeader._uid = 0; - longMetadataHeader._gid = 0; - longMetadataHeader._mTime = DateTimeOffset.MinValue; // 0 - longMetadataHeader._typeFlag = entryType; - - longMetadataHeader._dataStream = new MemoryStream(); longMetadataHeader._dataStream.Write(Encoding.UTF8.GetBytes(longText)); longMetadataHeader._dataStream.Seek(0, SeekOrigin.Begin); // Ensure it gets written into the archive from the beginning @@ -248,11 +238,23 @@ private static TarHeader GetGnuLongMetadataHeader(TarEntryType entryType, string // Asynchronously creates and returns a GNU long metadata header, with the specified long text written into its data stream. private static async Task GetGnuLongMetadataHeaderAsync(TarEntryType entryType, string longText, CancellationToken cancellationToken) { - Debug.Assert((entryType is TarEntryType.LongPath && longText.Length > FieldLengths.Name) || - (entryType is TarEntryType.LongLink && longText.Length > FieldLengths.LinkName)); - cancellationToken.ThrowIfCancellationRequested(); + TarHeader longMetadataHeader = GetDefaultGnuLongMetadataHeader(longText.Length, entryType); + Debug.Assert(longMetadataHeader._dataStream != null); + + await longMetadataHeader._dataStream.WriteAsync(Encoding.UTF8.GetBytes(longText), cancellationToken).ConfigureAwait(false); + longMetadataHeader._dataStream.Seek(0, SeekOrigin.Begin); // Ensure it gets written into the archive from the beginning + + return longMetadataHeader; + } + + // Constructs a GNU metadata header with default values for the specified entry type. + private static TarHeader GetDefaultGnuLongMetadataHeader(int longTextLength, TarEntryType entryType) + { + Debug.Assert((entryType is TarEntryType.LongPath && longTextLength > FieldLengths.Name) || + (entryType is TarEntryType.LongLink && longTextLength > FieldLengths.LinkName)); + TarHeader longMetadataHeader = default; longMetadataHeader._name = GnuLongMetadataName; // Same name for both longpath or longlink @@ -261,10 +263,7 @@ private static async Task GetGnuLongMetadataHeaderAsync(TarEntryType longMetadataHeader._gid = 0; longMetadataHeader._mTime = DateTimeOffset.MinValue; // 0 longMetadataHeader._typeFlag = entryType; - longMetadataHeader._dataStream = new MemoryStream(); - await longMetadataHeader._dataStream.WriteAsync(Encoding.UTF8.GetBytes(longText), cancellationToken).ConfigureAwait(false); - longMetadataHeader._dataStream.Seek(0, SeekOrigin.Begin); // Ensure it gets written into the archive from the beginning return longMetadataHeader; } @@ -272,20 +271,7 @@ private static async Task GetGnuLongMetadataHeaderAsync(TarEntryType // Writes the current header as a GNU entry into the archive stream. internal void WriteAsGnuInternal(Stream archiveStream, Span buffer) { - // Unused GNU fields: offset, longnames, unused, sparse struct, isextended and realsize - // If this header came from another archive, it will have a value - // If it was constructed by the user, it will be an empty array - _gnuUnusedBytes ??= new byte[FieldLengths.AllGnuUnused]; - - long actualLength = GetTotalDataBytesToWrite(); - TarEntryType actualEntryType = GetCorrectTypeFlagForFormat(TarEntryFormat.Gnu); - - int checksum = WriteNameSpan(buffer, out _); - checksum += WriteCommonFields(buffer, actualLength, actualEntryType); - checksum += WriteGnuMagicAndVersion(buffer); - checksum += WritePosixAndGnuSharedFields(buffer); - checksum += WriteGnuFields(buffer); - _checksum = WriteChecksum(checksum, buffer); + WriteAsGnuSharedInternal(buffer, out long actualLength, out _checksum); archiveStream.Write(buffer); @@ -300,20 +286,7 @@ internal async Task WriteAsGnuInternalAsync(Stream archiveStream, Memory WriteAsGnuInternalAsync(Stream archiveStream, Memory buffer, out long actualLength, out int checksum) + { + actualLength = GetTotalDataBytesToWrite(); + + checksum = WriteName(buffer, out _); + checksum += WriteCommonFields(buffer, actualLength, GetCorrectTypeFlagForFormat(TarEntryFormat.Gnu)); + checksum += WriteGnuMagicAndVersion(buffer); + checksum += WritePosixAndGnuSharedFields(buffer); + checksum += WriteGnuFields(buffer); + checksum += WriteChecksum(checksum, buffer); } // Writes the current header as a PAX Extended Attributes entry into the archive stream. @@ -369,14 +355,7 @@ private async Task WriteAsPaxExtendedAttributesAsync(Stream archiveStream, // This method writes an entry as both entries require, using the data from the current header instance. private void WriteAsPaxInternal(Stream archiveStream, Span buffer) { - long actualLength = GetTotalDataBytesToWrite(); - TarEntryType actualEntryType = GetCorrectTypeFlagForFormat(TarEntryFormat.Pax); - - int checksum = WritePosixNameSpan(buffer); - checksum += WriteCommonFields(buffer, actualLength, actualEntryType); - checksum += WritePosixMagicAndVersion(buffer); - checksum += WritePosixAndGnuSharedFields(buffer); - WriteChecksum(checksum, buffer); + WriteAsPaxSharedInternal(buffer, out long actualLength, out _checksum); archiveStream.Write(buffer); @@ -392,14 +371,7 @@ private async Task WriteAsPaxInternalAsync(Stream archiveStream, Memory WriteAsPaxInternalAsync(Stream archiveStream, Memory buffer, out byte[] fullNameBytes) + // Shared checksum and data length calculations for PAX entry writing. + private void WriteAsPaxSharedInternal(Span buffer, out long actualLength, out int checksum) { - fullNameBytes = Encoding.ASCII.GetBytes(_name); - int nameBytesLength = Math.Min(fullNameBytes.Length, FieldLengths.Name); - int checksum = WriteLeftAlignedBytesAndGetChecksum(fullNameBytes.AsSpan(0, nameBytesLength), buffer.Slice(FieldLocations.Name, FieldLengths.Name)); - return checksum; + actualLength = GetTotalDataBytesToWrite(); + + checksum = WritePosixName(buffer); + checksum += WriteCommonFields(buffer, actualLength, GetCorrectTypeFlagForFormat(TarEntryFormat.Pax)); + checksum += WritePosixMagicAndVersion(buffer); + checksum += WritePosixAndGnuSharedFields(buffer); + checksum += WriteChecksum(checksum, buffer); } // All formats save in the name byte array only the ASCII bytes that fit. The full string is returned in the out byte array. - private int WriteNameMemory(Memory buffer, out byte[] fullNameBytes) + private int WriteName(Span buffer, out byte[] fullNameBytes) { fullNameBytes = Encoding.ASCII.GetBytes(_name); int nameBytesLength = Math.Min(fullNameBytes.Length, FieldLengths.Name); - int checksum = WriteLeftAlignedBytesAndGetChecksum(fullNameBytes.AsSpan(0, nameBytesLength), buffer.Span.Slice(FieldLocations.Name, FieldLengths.Name)); + int checksum = WriteLeftAlignedBytesAndGetChecksum(fullNameBytes.AsSpan(0, nameBytesLength), buffer.Slice(FieldLocations.Name, FieldLengths.Name)); return checksum; } // Ustar and PAX save in the name byte array only the ASCII bytes that fit, and the rest of that string is saved in the prefix field. - private int WritePosixNameSpan(Span buffer) + private int WritePosixName(Span buffer) { - int checksum = WriteNameSpan(buffer, out byte[] fullNameBytes); + int checksum = WriteName(buffer, out byte[] fullNameBytes); if (fullNameBytes.Length > FieldLengths.Name) { int prefixBytesLength = Math.Min(fullNameBytes.Length - FieldLengths.Name, FieldLengths.Name); @@ -441,18 +416,6 @@ private int WritePosixNameSpan(Span buffer) return checksum; } - // Ustar and PAX save in the name byte array only the ASCII bytes that fit, and the rest of that string is saved in the prefix field. - private int WritePosixNameMemory(Memory buffer) - { - int checksum = WriteNameMemory(buffer, out byte[] fullNameBytes); - if (fullNameBytes.Length > FieldLengths.Name) - { - int prefixBytesLength = Math.Min(fullNameBytes.Length - FieldLengths.Name, FieldLengths.Name); - checksum += WriteLeftAlignedBytesAndGetChecksum(fullNameBytes.AsSpan(FieldLengths.Name, prefixBytesLength), buffer.Span.Slice(FieldLocations.Prefix, FieldLengths.Prefix)); - } - return checksum; - } - // Writes all the common fields shared by all formats into the specified spans. private int WriteCommonFields(Span buffer, long actualLength, TarEntryType actualEntryType) { @@ -648,7 +611,7 @@ private void CollectExtendedAttributesFromStandardFieldsIfNeeded() if (!_extendedAttributes.ContainsKey(PaxEaMTime)) { - AddTimestampAsUnixSeconds(_extendedAttributes, PaxEaMTime, _mTime); + _extendedAttributes.Add(PaxEaMTime, TarHelpers.GetTimestampStringFromDateTimeOffset(_mTime)); } TryAddStringField(_extendedAttributes, PaxEaGName, _gName, FieldLengths.GName); TryAddStringField(_extendedAttributes, PaxEaUName, _uName, FieldLengths.UName); @@ -663,11 +626,6 @@ private void CollectExtendedAttributesFromStandardFieldsIfNeeded() _extendedAttributes.Add(PaxEaSize, _size.ToString()); } - // Adds the specified datetime to the dictionary as a decimal number. - static void AddTimestampAsUnixSeconds(Dictionary extendedAttributes, string key, DateTimeOffset value) - { - extendedAttributes.Add(key, TarHelpers.GetTimestampStringFromDateTimeOffset(value)); - } // Adds the specified string to the dictionary if it's longer than the specified max byte length. static void TryAddStringField(Dictionary extendedAttributes, string key, string value, int maxLength) From 9e173af35137fdc0470468f1dc52ebc532d1aec4 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Wed, 29 Jun 2022 12:41:05 -0700 Subject: [PATCH 74/75] Unused directive. --- .../System.Formats.Tar/src/System/Formats/Tar/TarHelpers.cs | 1 - 1 file changed, 1 deletion(-) 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 71620dbaa6317c..811bf1b2380ca6 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 @@ -8,7 +8,6 @@ using System.Text; using System.Threading; using System.Threading.Tasks; -using System; namespace System.Formats.Tar { From 433c80454c7bcf16df7f9b52bbd881972238ab78 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Wed, 29 Jun 2022 20:43:26 -0700 Subject: [PATCH 75/75] Prevent test failure in async tests due to race condition. --- .../tests/CompressedTar.Async.Tests.cs | 159 ++++--- .../TarEntry.ExtractToFileAsync.Tests.cs | 74 ++-- ...ile.CreateFromDirectoryAsync.File.Tests.cs | 228 +++++----- ...e.CreateFromDirectoryAsync.Stream.Tests.cs | 51 ++- ...ExtractToDirectoryAsync.File.Tests.Unix.cs | 20 +- ...ractToDirectoryAsync.File.Tests.Windows.cs | 20 +- ...File.ExtractToDirectoryAsync.File.Tests.cs | 181 ++++---- ...le.ExtractToDirectoryAsync.Stream.Tests.cs | 161 +++---- .../TarReader.File.Async.Tests.Base.cs | 402 +++++++++--------- ...le.GlobalExtendedAttributes.Async.Tests.cs | 33 +- .../TarReader.GetNextEntryAsync.Tests.cs | 368 ++++++++-------- ....TarEntry.ExtractToFileAsync.Tests.Unix.cs | 8 +- ...eader.TarEntry.ExtractToFileAsync.Tests.cs | 15 +- .../tests/TarWriter/TarWriter.Async.Tests.cs | 132 +++--- ...rWriter.WriteEntryAsync.Entry.Gnu.Tests.cs | 282 ++++++------ ...rWriter.WriteEntryAsync.Entry.Pax.Tests.cs | 338 +++++++-------- ...rWriter.WriteEntryAsync.File.Tests.Unix.cs | 191 +++++---- .../TarWriter.WriteEntryAsync.File.Tests.cs | 283 ++++++------ .../TarWriter.WriteEntryAsync.Tests.cs | 37 +- 19 files changed, 1529 insertions(+), 1454 deletions(-) diff --git a/src/libraries/System.Formats.Tar/tests/CompressedTar.Async.Tests.cs b/src/libraries/System.Formats.Tar/tests/CompressedTar.Async.Tests.cs index ee770fb929e265..e2fe85ecdb6562 100644 --- a/src/libraries/System.Formats.Tar/tests/CompressedTar.Async.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/CompressedTar.Async.Tests.cs @@ -13,58 +13,56 @@ public class CompressedTar_Async_Tests : TarTestsBase [Fact] public async Task TarGz_TarWriter_TarReader_Async() { - using TempDirectory root = new TempDirectory(); + using (TempDirectory root = new TempDirectory()) + { + string archivePath = Path.Join(root.Path, "compressed.tar.gz"); - string archivePath = Path.Join(root.Path, "compressed.tar.gz"); + string fileName = "file.txt"; + string filePath = Path.Join(root.Path, fileName); + File.Create(filePath).Dispose(); - string fileName = "file.txt"; - string filePath = Path.Join(root.Path, fileName); - File.Create(filePath).Dispose(); + // Create tar.gz archive + FileStreamOptions createOptions = new() + { + Mode = FileMode.CreateNew, + Access = FileAccess.Write, + Options = FileOptions.Asynchronous + }; - // Create tar.gz archive - FileStreamOptions createOptions = new() - { - Mode = FileMode.CreateNew, - Access = FileAccess.Write, - Options = FileOptions.Asynchronous - }; - FileStream streamToCompress = File.Open(archivePath, createOptions); - await using (streamToCompress) - { - GZipStream compressorStream = new GZipStream(streamToCompress, CompressionMode.Compress); - await using (compressorStream) + await using (FileStream streamToCompress = new FileStream(archivePath, createOptions)) { - TarWriter writer = new TarWriter(compressorStream); - await using (writer) + await using (GZipStream compressorStream = new GZipStream(streamToCompress, CompressionMode.Compress)) { - await writer.WriteEntryAsync(fileName: filePath, entryName: fileName); + await using (TarWriter writer = new TarWriter(compressorStream)) + { + await writer.WriteEntryAsync(fileName: filePath, entryName: fileName); + } } } - } - FileInfo fileInfo = new FileInfo(archivePath); - Assert.True(fileInfo.Exists); - Assert.True(fileInfo.Length > 0); - // Verify tar.gz archive contents - FileStreamOptions readOptions = new() - { - Mode = FileMode.Open, - Access = FileAccess.Read, - Options = FileOptions.Asynchronous - }; - FileStream streamToDecompress = File.Open(archivePath, readOptions); - await using (streamToDecompress) - { - GZipStream decompressorStream = new GZipStream(streamToDecompress, CompressionMode.Decompress); - await using (decompressorStream) + FileInfo fileInfo = new FileInfo(archivePath); + Assert.True(fileInfo.Exists); + Assert.True(fileInfo.Length > 0); + + // Verify tar.gz archive contents + FileStreamOptions readOptions = new() + { + Mode = FileMode.Open, + Access = FileAccess.Read, + Options = FileOptions.Asynchronous + }; + + await using (FileStream streamToDecompress = File.Open(archivePath, readOptions)) { - TarReader reader = new TarReader(decompressorStream); - await using (reader) + await using (GZipStream decompressorStream = new GZipStream(streamToDecompress, CompressionMode.Decompress)) { - TarEntry entry = await reader.GetNextEntryAsync(); - Assert.Equal(TarEntryFormat.Pax, entry.Format); - Assert.Equal(fileName, entry.Name); - Assert.Null(await reader.GetNextEntryAsync()); + await using (TarReader reader = new TarReader(decompressorStream)) + { + TarEntry entry = await reader.GetNextEntryAsync(); + Assert.Equal(TarEntryFormat.Pax, entry.Format); + Assert.Equal(fileName, entry.Name); + Assert.Null(await reader.GetNextEntryAsync()); + } } } } @@ -73,53 +71,54 @@ public async Task TarGz_TarWriter_TarReader_Async() [Fact] public async Task TarGz_TarFile_CreateFromDir_ExtractToDir_Async() { - using TempDirectory root = new TempDirectory(); + using (TempDirectory root = new TempDirectory()) + { - string archivePath = Path.Join(root.Path, "compressed.tar.gz"); + string archivePath = Path.Join(root.Path, "compressed.tar.gz"); - string sourceDirectory = Path.Join(root.Path, "source"); - Directory.CreateDirectory(sourceDirectory); + string sourceDirectory = Path.Join(root.Path, "source"); + Directory.CreateDirectory(sourceDirectory); - string destinationDirectory = Path.Join(root.Path, "destination"); - Directory.CreateDirectory(destinationDirectory); + string destinationDirectory = Path.Join(root.Path, "destination"); + Directory.CreateDirectory(destinationDirectory); - string fileName = "file.txt"; - string filePath = Path.Join(sourceDirectory, fileName); - File.Create(filePath).Dispose(); + string fileName = "file.txt"; + string filePath = Path.Join(sourceDirectory, fileName); + File.Create(filePath).Dispose(); - FileStreamOptions createOptions = new() - { - Mode = FileMode.CreateNew, - Access = FileAccess.Write, - Options = FileOptions.Asynchronous - }; - FileStream streamToCompress = File.Open(archivePath, createOptions); - await using (streamToCompress) - { - GZipStream compressorStream = new GZipStream(streamToCompress, CompressionMode.Compress); - await using (compressorStream) + FileStreamOptions createOptions = new() { - await TarFile.CreateFromDirectoryAsync(sourceDirectory, compressorStream, includeBaseDirectory: false); + Mode = FileMode.CreateNew, + Access = FileAccess.Write, + Options = FileOptions.Asynchronous + }; + + await using (FileStream streamToCompress = File.Open(archivePath, createOptions)) + { + await using (GZipStream compressorStream = new GZipStream(streamToCompress, CompressionMode.Compress)) + { + await TarFile.CreateFromDirectoryAsync(sourceDirectory, compressorStream, includeBaseDirectory: false); + } } - } - FileInfo fileInfo = new FileInfo(archivePath); - Assert.True(fileInfo.Exists); - Assert.True(fileInfo.Length > 0); - FileStreamOptions readOptions = new() - { - Mode = FileMode.Open, - Access = FileAccess.Read, - Options = FileOptions.Asynchronous - }; - FileStream streamToDecompress = File.Open(archivePath, readOptions); - await using (streamToDecompress) - { - GZipStream decompressorStream = new GZipStream(streamToDecompress, CompressionMode.Decompress); - await using (decompressorStream) + FileInfo fileInfo = new FileInfo(archivePath); + Assert.True(fileInfo.Exists); + Assert.True(fileInfo.Length > 0); + + FileStreamOptions readOptions = new() + { + Mode = FileMode.Open, + Access = FileAccess.Read, + Options = FileOptions.Asynchronous + }; + + await using (FileStream streamToDecompress = File.Open(archivePath, readOptions)) { - await TarFile.ExtractToDirectoryAsync(decompressorStream, destinationDirectory, overwriteFiles: true); - Assert.True(File.Exists(filePath)); + await using (GZipStream decompressorStream = new GZipStream(streamToDecompress, CompressionMode.Decompress)) + { + await TarFile.ExtractToDirectoryAsync(decompressorStream, destinationDirectory, overwriteFiles: true); + Assert.True(File.Exists(filePath)); + } } } } diff --git a/src/libraries/System.Formats.Tar/tests/TarEntry/TarEntry.ExtractToFileAsync.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarEntry/TarEntry.ExtractToFileAsync.Tests.cs index c2b09a07603d2c..58d0143b1f6ce2 100644 --- a/src/libraries/System.Formats.Tar/tests/TarEntry/TarEntry.ExtractToFileAsync.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarEntry/TarEntry.ExtractToFileAsync.Tests.cs @@ -31,19 +31,20 @@ public Task ExtractToFileAsync_Cancel(TarEntryFormat format) [InlineData(TarEntryFormat.Gnu)] public async Task Constructor_Name_FullPath_DestinationDirectory_Mismatch_Throws_Async(TarEntryFormat format) { - using TempDirectory root = new TempDirectory(); + using (TempDirectory root = new TempDirectory()) + { + string fullPath = Path.Join(Path.GetPathRoot(root.Path), "dir", "file.txt"); - string fullPath = Path.Join(Path.GetPathRoot(root.Path), "dir", "file.txt"); + TarEntry entry = InvokeTarEntryCreationConstructor(format, GetTarEntryTypeForTarEntryFormat(TarEntryType.RegularFile, format), fullPath); - TarEntry entry = InvokeTarEntryCreationConstructor(format, GetTarEntryTypeForTarEntryFormat(TarEntryType.RegularFile, format), fullPath); + entry.DataStream = new MemoryStream(); + entry.DataStream.Write(new byte[] { 0x1 }); + entry.DataStream.Seek(0, SeekOrigin.Begin); - entry.DataStream = new MemoryStream(); - entry.DataStream.Write(new byte[] { 0x1 }); - entry.DataStream.Seek(0, SeekOrigin.Begin); + await Assert.ThrowsAsync(() => entry.ExtractToFileAsync(root.Path, overwrite: false)); - await Assert.ThrowsAsync(() => entry.ExtractToFileAsync(root.Path, overwrite: false)); - - Assert.False(File.Exists(fullPath)); + Assert.False(File.Exists(fullPath)); + } } [Theory] @@ -53,19 +54,20 @@ public async Task Constructor_Name_FullPath_DestinationDirectory_Mismatch_Throws [InlineData(TarEntryFormat.Gnu)] public async Task Constructor_Name_FullPath_DestinationDirectory_Match_AdditionalSubdirectory_Throws_Async(TarEntryFormat format) { - using TempDirectory root = new TempDirectory(); - - string fullPath = Path.Join(root.Path, "dir", "file.txt"); + using (TempDirectory root = new TempDirectory()) + { + string fullPath = Path.Join(root.Path, "dir", "file.txt"); - TarEntry entry = InvokeTarEntryCreationConstructor(format, GetTarEntryTypeForTarEntryFormat(TarEntryType.RegularFile, format), fullPath); + TarEntry entry = InvokeTarEntryCreationConstructor(format, GetTarEntryTypeForTarEntryFormat(TarEntryType.RegularFile, format), fullPath); - entry.DataStream = new MemoryStream(); - entry.DataStream.Write(new byte[] { 0x1 }); - entry.DataStream.Seek(0, SeekOrigin.Begin); + entry.DataStream = new MemoryStream(); + entry.DataStream.Write(new byte[] { 0x1 }); + entry.DataStream.Seek(0, SeekOrigin.Begin); - await Assert.ThrowsAsync(() => entry.ExtractToFileAsync(root.Path, overwrite: false)); + await Assert.ThrowsAsync(() => entry.ExtractToFileAsync(root.Path, overwrite: false)); - Assert.False(File.Exists(fullPath)); + Assert.False(File.Exists(fullPath)); + } } [Theory] @@ -75,37 +77,41 @@ public async Task Constructor_Name_FullPath_DestinationDirectory_Match_Additiona [InlineData(TarEntryFormat.Gnu)] public async Task Constructor_Name_FullPath_DestinationDirectory_Match_Async(TarEntryFormat format) { - using TempDirectory root = new TempDirectory(); + using (TempDirectory root = new TempDirectory()) + { - string fullPath = Path.Join(root.Path, "file.txt"); + string fullPath = Path.Join(root.Path, "file.txt"); - TarEntry entry = InvokeTarEntryCreationConstructor(format, GetTarEntryTypeForTarEntryFormat(TarEntryType.RegularFile, format), fullPath); + TarEntry entry = InvokeTarEntryCreationConstructor(format, GetTarEntryTypeForTarEntryFormat(TarEntryType.RegularFile, format), fullPath); - entry.DataStream = new MemoryStream(); - entry.DataStream.Write(new byte[] { 0x1 }); - entry.DataStream.Seek(0, SeekOrigin.Begin); + entry.DataStream = new MemoryStream(); + entry.DataStream.Write(new byte[] { 0x1 }); + entry.DataStream.Seek(0, SeekOrigin.Begin); - await entry.ExtractToFileAsync(fullPath, overwrite: false); + await entry.ExtractToFileAsync(fullPath, overwrite: false); - Assert.True(File.Exists(fullPath)); + Assert.True(File.Exists(fullPath)); + } } [Theory] [MemberData(nameof(GetFormatsAndLinks))] public async Task ExtractToFile_Link_Throws_Async(TarEntryFormat format, TarEntryType entryType) { - using TempDirectory root = new TempDirectory(); - string fileName = "mylink"; - string fullPath = Path.Join(root.Path, fileName); + using (TempDirectory root = new TempDirectory()) + { + string fileName = "mylink"; + string fullPath = Path.Join(root.Path, fileName); - string linkTarget = PlatformDetection.IsWindows ? @"C:\Windows\system32\notepad.exe" : "/usr/bin/nano"; + string linkTarget = PlatformDetection.IsWindows ? @"C:\Windows\system32\notepad.exe" : "/usr/bin/nano"; - TarEntry entry = InvokeTarEntryCreationConstructor(format, entryType, fileName); - entry.LinkName = linkTarget; + TarEntry entry = InvokeTarEntryCreationConstructor(format, entryType, fileName); + entry.LinkName = linkTarget; - await Assert.ThrowsAsync(() => entry.ExtractToFileAsync(fileName, overwrite: false)); + await Assert.ThrowsAsync(() => entry.ExtractToFileAsync(fileName, overwrite: false)); - Assert.Equal(0, Directory.GetFileSystemEntries(root.Path).Count()); + Assert.Equal(0, Directory.GetFileSystemEntries(root.Path).Count()); + } } } } diff --git a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.CreateFromDirectoryAsync.File.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.CreateFromDirectoryAsync.File.Tests.cs index a2872a7bbc528e..8891ea856ebb6e 100644 --- a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.CreateFromDirectoryAsync.File.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.CreateFromDirectoryAsync.File.Tests.cs @@ -30,28 +30,30 @@ public async Task InvalidPaths_Throw_Async() } [Fact] - public Task NonExistentDirectory_Throws_Async() + public async Task NonExistentDirectory_Throws_Async() { - using TempDirectory root = new TempDirectory(); - - string dirPath = Path.Join(root.Path, "dir"); - string filePath = Path.Join(root.Path, "file.tar"); + using (TempDirectory root = new TempDirectory()) + { + string dirPath = Path.Join(root.Path, "dir"); + string filePath = Path.Join(root.Path, "file.tar"); - return Assert.ThrowsAsync(() => TarFile.CreateFromDirectoryAsync(sourceDirectoryName: "IDontExist", destinationFileName: filePath, includeBaseDirectory: false)); + await Assert.ThrowsAsync(() => TarFile.CreateFromDirectoryAsync(sourceDirectoryName: "IDontExist", destinationFileName: filePath, includeBaseDirectory: false)); + } } [Fact] - public Task DestinationExists_Throws_Async() + public async Task DestinationExists_Throws_Async() { - using TempDirectory root = new TempDirectory(); - - string dirPath = Path.Join(root.Path, "dir"); - Directory.CreateDirectory(dirPath); + using (TempDirectory root = new TempDirectory()) + { + string dirPath = Path.Join(root.Path, "dir"); + Directory.CreateDirectory(dirPath); - string filePath = Path.Join(root.Path, "file.tar"); - File.Create(filePath).Dispose(); + string filePath = Path.Join(root.Path, "file.tar"); + File.Create(filePath).Dispose(); - return Assert.ThrowsAsync(() => TarFile.CreateFromDirectoryAsync(sourceDirectoryName: dirPath, destinationFileName: filePath, includeBaseDirectory: false)); + await Assert.ThrowsAsync(() => TarFile.CreateFromDirectoryAsync(sourceDirectoryName: dirPath, destinationFileName: filePath, includeBaseDirectory: false)); + } } [Theory] @@ -59,95 +61,95 @@ public Task DestinationExists_Throws_Async() [InlineData(true)] public async Task VerifyIncludeBaseDirectory_Async(bool includeBaseDirectory) { - using TempDirectory source = new TempDirectory(); - using TempDirectory destination = new TempDirectory(); + using (TempDirectory source = new TempDirectory()) + using (TempDirectory destination = new TempDirectory()) + { + string fileName1 = "file1.txt"; + string filePath1 = Path.Join(source.Path, fileName1); + File.Create(filePath1).Dispose(); - string fileName1 = "file1.txt"; - string filePath1 = Path.Join(source.Path, fileName1); - File.Create(filePath1).Dispose(); + string subDirectoryName = "dir/"; // The trailing separator is preserved in the TarEntry.Name + string subDirectoryPath = Path.Join(source.Path, subDirectoryName); + Directory.CreateDirectory(subDirectoryPath); - string subDirectoryName = "dir/"; // The trailing separator is preserved in the TarEntry.Name - string subDirectoryPath = Path.Join(source.Path, subDirectoryName); - Directory.CreateDirectory(subDirectoryPath); + string fileName2 = "file2.txt"; + string filePath2 = Path.Join(subDirectoryPath, fileName2); + File.Create(filePath2).Dispose(); - string fileName2 = "file2.txt"; - string filePath2 = Path.Join(subDirectoryPath, fileName2); - File.Create(filePath2).Dispose(); + string destinationArchiveFileName = Path.Join(destination.Path, "output.tar"); + TarFile.CreateFromDirectory(source.Path, destinationArchiveFileName, includeBaseDirectory); - string destinationArchiveFileName = Path.Join(destination.Path, "output.tar"); - TarFile.CreateFromDirectory(source.Path, destinationArchiveFileName, includeBaseDirectory); + List entries = new List(); - List entries = new List(); + FileStreamOptions readOptions = new() + { + Access = FileAccess.Read, + Mode = FileMode.Open, + Options = FileOptions.Asynchronous, + }; - FileStreamOptions readOptions = new() - { - Access = FileAccess.Read, - Mode = FileMode.Open, - Options = FileOptions.Asynchronous, - }; - FileStream fileStream = File.Open(destinationArchiveFileName, readOptions); - await using (fileStream) - { - TarReader reader = new TarReader(fileStream); - await using (reader) + await using (FileStream fileStream = File.Open(destinationArchiveFileName, readOptions)) { - TarEntry entry; - while ((entry = await reader.GetNextEntryAsync()) != null) + await using (TarReader reader = new TarReader(fileStream)) { - entries.Add(entry); + TarEntry entry; + while ((entry = await reader.GetNextEntryAsync()) != null) + { + entries.Add(entry); + } } } - } - Assert.Equal(3, entries.Count); + Assert.Equal(3, entries.Count); - string prefix = includeBaseDirectory ? Path.GetFileName(source.Path) + '/' : string.Empty; + string prefix = includeBaseDirectory ? Path.GetFileName(source.Path) + '/' : string.Empty; - TarEntry entry1 = entries.FirstOrDefault(x => - x.EntryType == TarEntryType.RegularFile && - x.Name == prefix + fileName1); - Assert.NotNull(entry1); + TarEntry entry1 = entries.FirstOrDefault(x => + x.EntryType == TarEntryType.RegularFile && + x.Name == prefix + fileName1); + Assert.NotNull(entry1); - TarEntry directory = entries.FirstOrDefault(x => - x.EntryType == TarEntryType.Directory && - x.Name == prefix + subDirectoryName); - Assert.NotNull(directory); + TarEntry directory = entries.FirstOrDefault(x => + x.EntryType == TarEntryType.Directory && + x.Name == prefix + subDirectoryName); + Assert.NotNull(directory); - string actualFileName2 = subDirectoryName + fileName2; // Notice the trailing separator in subDirectoryName - TarEntry entry2 = entries.FirstOrDefault(x => - x.EntryType == TarEntryType.RegularFile && - x.Name == prefix + actualFileName2); - Assert.NotNull(entry2); + string actualFileName2 = subDirectoryName + fileName2; // Notice the trailing separator in subDirectoryName + TarEntry entry2 = entries.FirstOrDefault(x => + x.EntryType == TarEntryType.RegularFile && + x.Name == prefix + actualFileName2); + Assert.NotNull(entry2); + } } [Fact] public async Task IncludeBaseDirectoryIfEmpty_Async() { - using TempDirectory source = new TempDirectory(); - using TempDirectory destination = new TempDirectory(); + using (TempDirectory source = new TempDirectory()) + using (TempDirectory destination = new TempDirectory()) + { + string destinationArchiveFileName = Path.Join(destination.Path, "output.tar"); - string destinationArchiveFileName = Path.Join(destination.Path, "output.tar"); + await TarFile.CreateFromDirectoryAsync(source.Path, destinationArchiveFileName, includeBaseDirectory: true); - await TarFile.CreateFromDirectoryAsync(source.Path, destinationArchiveFileName, includeBaseDirectory: true); + FileStreamOptions readOptions = new() + { + Access = FileAccess.Read, + Mode = FileMode.Open, + Options = FileOptions.Asynchronous, + }; - FileStreamOptions readOptions = new() - { - Access = FileAccess.Read, - Mode = FileMode.Open, - Options = FileOptions.Asynchronous, - }; - FileStream fileStream = File.Open(destinationArchiveFileName, readOptions); - await using (fileStream) - { - TarReader reader = new TarReader(fileStream); - await using (reader) + await using (FileStream fileStream = File.Open(destinationArchiveFileName, readOptions)) { - TarEntry entry = await reader.GetNextEntryAsync(); - Assert.NotNull(entry); - Assert.Equal(TarEntryType.Directory, entry.EntryType); - Assert.Equal(Path.GetFileName(source.Path) + '/', entry.Name); + await using (TarReader reader = new TarReader(fileStream)) + { + TarEntry entry = await reader.GetNextEntryAsync(); + Assert.NotNull(entry); + Assert.Equal(TarEntryType.Directory, entry.EntryType); + Assert.Equal(Path.GetFileName(source.Path) + '/', entry.Name); - Assert.Null(await reader.GetNextEntryAsync()); + Assert.Null(await reader.GetNextEntryAsync()); + } } } } @@ -157,50 +159,50 @@ public async Task IncludeBaseDirectoryIfEmpty_Async() [InlineData(true)] public async Task IncludeAllSegmentsOfPath_Async(bool includeBaseDirectory) { - using TempDirectory source = new TempDirectory(); - using TempDirectory destination = new TempDirectory(); + using (TempDirectory source = new TempDirectory()) + using (TempDirectory destination = new TempDirectory()) + { + string segment1 = Path.Join(source.Path, "segment1"); + Directory.CreateDirectory(segment1); + string segment2 = Path.Join(segment1, "segment2"); + Directory.CreateDirectory(segment2); + string textFile = Path.Join(segment2, "file.txt"); + File.Create(textFile).Dispose(); - string segment1 = Path.Join(source.Path, "segment1"); - Directory.CreateDirectory(segment1); - string segment2 = Path.Join(segment1, "segment2"); - Directory.CreateDirectory(segment2); - string textFile = Path.Join(segment2, "file.txt"); - File.Create(textFile).Dispose(); + string destinationArchiveFileName = Path.Join(destination.Path, "output.tar"); - string destinationArchiveFileName = Path.Join(destination.Path, "output.tar"); + await TarFile.CreateFromDirectoryAsync(source.Path, destinationArchiveFileName, includeBaseDirectory); - await TarFile.CreateFromDirectoryAsync(source.Path, destinationArchiveFileName, includeBaseDirectory); + FileStreamOptions readOptions = new() + { + Access = FileAccess.Read, + Mode = FileMode.Open, + Options = FileOptions.Asynchronous, + }; - FileStreamOptions readOptions = new() - { - Access = FileAccess.Read, - Mode = FileMode.Open, - Options = FileOptions.Asynchronous, - }; - FileStream fileStream = File.Open(destinationArchiveFileName, readOptions); - await using (fileStream) - { - TarReader reader = new TarReader(fileStream); - await using (reader) + await using (FileStream fileStream = File.Open(destinationArchiveFileName, readOptions)) { - string prefix = includeBaseDirectory ? Path.GetFileName(source.Path) + '/' : string.Empty; + await using (TarReader reader = new TarReader(fileStream)) + { + string prefix = includeBaseDirectory ? Path.GetFileName(source.Path) + '/' : string.Empty; - TarEntry entry = await reader.GetNextEntryAsync(); - Assert.NotNull(entry); - Assert.Equal(TarEntryType.Directory, entry.EntryType); - Assert.Equal(prefix + "segment1/", entry.Name); + TarEntry entry = await reader.GetNextEntryAsync(); + Assert.NotNull(entry); + Assert.Equal(TarEntryType.Directory, entry.EntryType); + Assert.Equal(prefix + "segment1/", entry.Name); - entry = await reader.GetNextEntryAsync(); - Assert.NotNull(entry); - Assert.Equal(TarEntryType.Directory, entry.EntryType); - Assert.Equal(prefix + "segment1/segment2/", entry.Name); + entry = await reader.GetNextEntryAsync(); + Assert.NotNull(entry); + Assert.Equal(TarEntryType.Directory, entry.EntryType); + Assert.Equal(prefix + "segment1/segment2/", entry.Name); - entry = await reader.GetNextEntryAsync(); - Assert.NotNull(entry); - Assert.Equal(TarEntryType.RegularFile, entry.EntryType); - Assert.Equal(prefix + "segment1/segment2/file.txt", entry.Name); + entry = await reader.GetNextEntryAsync(); + Assert.NotNull(entry); + Assert.Equal(TarEntryType.RegularFile, entry.EntryType); + Assert.Equal(prefix + "segment1/segment2/file.txt", entry.Name); - Assert.Null(await reader.GetNextEntryAsync()); + Assert.Null(await reader.GetNextEntryAsync()); + } } } } diff --git a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.CreateFromDirectoryAsync.Stream.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.CreateFromDirectoryAsync.Stream.Tests.cs index ea7833140dcb48..7a67aac64508f7 100644 --- a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.CreateFromDirectoryAsync.Stream.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.CreateFromDirectoryAsync.Stream.Tests.cs @@ -11,45 +11,60 @@ namespace System.Formats.Tar.Tests public class TarFile_CreateFromDirectoryAsync_Stream_Tests : TarTestsBase { [Fact] - public Task CreateFromDirectoryAsync_Cancel() + public async Task CreateFromDirectoryAsync_Cancel() { CancellationTokenSource cs = new CancellationTokenSource(); cs.Cancel(); - MemoryStream archiveStream = new MemoryStream(); - return Assert.ThrowsAsync(() => TarFile.CreateFromDirectoryAsync("directory", archiveStream, includeBaseDirectory: false, cs.Token)); + + await using (MemoryStream archiveStream = new MemoryStream()) + { + await Assert.ThrowsAsync(() => TarFile.CreateFromDirectoryAsync("directory", archiveStream, includeBaseDirectory: false, cs.Token)); + } } [Fact] public async Task InvalidPath_Throws_Async() { - using MemoryStream archive = new MemoryStream(); - await Assert.ThrowsAsync(() => TarFile.CreateFromDirectoryAsync(sourceDirectoryName: null,destination: archive, includeBaseDirectory: false)); - await Assert.ThrowsAsync(() => TarFile.CreateFromDirectoryAsync(sourceDirectoryName: string.Empty,destination: archive, includeBaseDirectory: false)); + await using (MemoryStream archiveStream = new MemoryStream()) + { + await Assert.ThrowsAsync(() => TarFile.CreateFromDirectoryAsync(sourceDirectoryName: null, destination: archiveStream, includeBaseDirectory: false)); + await Assert.ThrowsAsync(() => TarFile.CreateFromDirectoryAsync(sourceDirectoryName: string.Empty, destination: archiveStream, includeBaseDirectory: false)); + } } [Fact] - public Task NullStream_Throws_Async() + public async Task NullStream_Throws_Async() { - using MemoryStream archive = new MemoryStream(); - return Assert.ThrowsAsync(() => TarFile.CreateFromDirectoryAsync(sourceDirectoryName: "path",destination: null, includeBaseDirectory: false)); + await using (MemoryStream archiveStream = new MemoryStream()) + { + await Assert.ThrowsAsync(() => TarFile.CreateFromDirectoryAsync(sourceDirectoryName: "path", destination: null, includeBaseDirectory: false)); + } } [Fact] - public Task UnwritableStream_Throws_Async() + public async Task UnwritableStream_Throws_Async() { - using MemoryStream archive = new MemoryStream(); - using WrappedStream unwritable = new WrappedStream(archive, canRead: true, canWrite: false, canSeek: true); - return Assert.ThrowsAsync(() => TarFile.CreateFromDirectoryAsync(sourceDirectoryName: "path",destination: unwritable, includeBaseDirectory: false)); + await using (MemoryStream archiveStream = new MemoryStream()) + { + await using (WrappedStream unwritable = new WrappedStream(archiveStream, canRead: true, canWrite: false, canSeek: true)) + { + await Assert.ThrowsAsync(() => TarFile.CreateFromDirectoryAsync(sourceDirectoryName: "path", destination: unwritable, includeBaseDirectory: false)); + } + } } [Fact] - public Task NonExistentDirectory_Throws_Async() + public async Task NonExistentDirectory_Throws_Async() { - using TempDirectory root = new TempDirectory(); - string dirPath = Path.Join(root.Path, "dir"); + using (TempDirectory root = new TempDirectory()) + { + string dirPath = Path.Join(root.Path, "dir"); - using MemoryStream archive = new MemoryStream(); - return Assert.ThrowsAsync(() => TarFile.CreateFromDirectoryAsync(sourceDirectoryName: dirPath, destination: archive, includeBaseDirectory: false)); + await using (MemoryStream archive = new MemoryStream()) + { + await Assert.ThrowsAsync(() => TarFile.CreateFromDirectoryAsync(sourceDirectoryName: dirPath, destination: archive, includeBaseDirectory: false)); + } + } } } } diff --git a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectoryAsync.File.Tests.Unix.cs b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectoryAsync.File.Tests.Unix.cs index 540a876a947c3c..551264edb5e438 100644 --- a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectoryAsync.File.Tests.Unix.cs +++ b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectoryAsync.File.Tests.Unix.cs @@ -13,20 +13,22 @@ public partial class TarFile_ExtractToDirectoryAsync_File_Tests : TarTestsBase [Fact] public async Task Extract_SpecialFiles_Unix_Unelevated_ThrowsUnauthorizedAccess_Async() { - string originalFileName = GetTarFilePath(CompressionMethod.Uncompressed, TestTarFormat.ustar, "specialfiles"); - using TempDirectory root = new TempDirectory(); + using (TempDirectory root = new TempDirectory()) + { + string originalFileName = GetTarFilePath(CompressionMethod.Uncompressed, TestTarFormat.ustar, "specialfiles"); - string archive = Path.Join(root.Path, "input.tar"); - string destination = Path.Join(root.Path, "dir"); + string archive = Path.Join(root.Path, "input.tar"); + string destination = Path.Join(root.Path, "dir"); - // Copying the tar to reduce the chance of other tests failing due to being used by another process - File.Copy(originalFileName, archive); + // Copying the tar to reduce the chance of other tests failing due to being used by another process + File.Copy(originalFileName, archive); - Directory.CreateDirectory(destination); + Directory.CreateDirectory(destination); - await Assert.ThrowsAsync(() => TarFile.ExtractToDirectoryAsync(archive, destination, overwriteFiles: false)); + await Assert.ThrowsAsync(() => TarFile.ExtractToDirectoryAsync(archive, destination, overwriteFiles: false)); - Assert.Equal(0, Directory.GetFileSystemEntries(destination).Count()); + Assert.Equal(0, Directory.GetFileSystemEntries(destination).Count()); + } } } } diff --git a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectoryAsync.File.Tests.Windows.cs b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectoryAsync.File.Tests.Windows.cs index 3d7bba013503ee..376b46fd3248d7 100644 --- a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectoryAsync.File.Tests.Windows.cs +++ b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectoryAsync.File.Tests.Windows.cs @@ -13,20 +13,22 @@ public partial class TarFile_ExtractToDirectoryAsync_File_Tests : TarTestsBase [Fact] public async Task Extract_SpecialFiles_Windows_ThrowsInvalidOperation_Async() { - string originalFileName = GetTarFilePath(CompressionMethod.Uncompressed, TestTarFormat.ustar, "specialfiles"); - using TempDirectory root = new TempDirectory(); + using (TempDirectory root = new TempDirectory()) + { + string originalFileName = GetTarFilePath(CompressionMethod.Uncompressed, TestTarFormat.ustar, "specialfiles"); - string archive = Path.Join(root.Path, "input.tar"); - string destination = Path.Join(root.Path, "dir"); + string archive = Path.Join(root.Path, "input.tar"); + string destination = Path.Join(root.Path, "dir"); - // Copying the tar to reduce the chance of other tests failing due to being used by another process - File.Copy(originalFileName, archive); + // Copying the tar to reduce the chance of other tests failing due to being used by another process + File.Copy(originalFileName, archive); - Directory.CreateDirectory(destination); + Directory.CreateDirectory(destination); - await Assert.ThrowsAsync(() => TarFile.ExtractToDirectoryAsync(archive, destination, overwriteFiles: false)); + await Assert.ThrowsAsync(() => TarFile.ExtractToDirectoryAsync(archive, destination, overwriteFiles: false)); - Assert.Equal(0, Directory.GetFileSystemEntries(destination).Count()); + Assert.Equal(0, Directory.GetFileSystemEntries(destination).Count()); + } } } } diff --git a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectoryAsync.File.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectoryAsync.File.Tests.cs index 210893d72024f9..dfebe737493acc 100644 --- a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectoryAsync.File.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectoryAsync.File.Tests.cs @@ -29,29 +29,31 @@ public async Task InvalidPaths_Throw() } [Fact] - public Task NonExistentFile_Throws_Async() + public async Task NonExistentFile_Throws_Async() { - using TempDirectory root = new TempDirectory(); - - string filePath = Path.Join(root.Path, "file.tar"); - string dirPath = Path.Join(root.Path, "dir"); + using (TempDirectory root = new TempDirectory()) + { + string filePath = Path.Join(root.Path, "file.tar"); + string dirPath = Path.Join(root.Path, "dir"); - Directory.CreateDirectory(dirPath); + Directory.CreateDirectory(dirPath); - return Assert.ThrowsAsync(() => TarFile.ExtractToDirectoryAsync(sourceFileName: filePath, destinationDirectoryName: dirPath, overwriteFiles: false)); + await Assert.ThrowsAsync(() => TarFile.ExtractToDirectoryAsync(sourceFileName: filePath, destinationDirectoryName: dirPath, overwriteFiles: false)); + } } [Fact] - public Task NonExistentDirectory_Throws_Async() + public async Task NonExistentDirectory_Throws_Async() { - using TempDirectory root = new TempDirectory(); - - string filePath = Path.Join(root.Path, "file.tar"); - string dirPath = Path.Join(root.Path, "dir"); + using (TempDirectory root = new TempDirectory()) + { + string filePath = Path.Join(root.Path, "file.tar"); + string dirPath = Path.Join(root.Path, "dir"); - File.Create(filePath).Dispose(); + File.Create(filePath).Dispose(); - return Assert.ThrowsAsync(() => TarFile.ExtractToDirectoryAsync(sourceFileName: filePath, destinationDirectoryName: dirPath, overwriteFiles: false)); + await Assert.ThrowsAsync(() => TarFile.ExtractToDirectoryAsync(sourceFileName: filePath, destinationDirectoryName: dirPath, overwriteFiles: false)); + } } [Theory] @@ -65,13 +67,14 @@ public async Task Extract_Archive_File_Async(TestTarFormat testFormat) { string sourceArchiveFileName = GetTarFilePath(CompressionMethod.Uncompressed, testFormat, "file"); - using TempDirectory destination = new TempDirectory(); - - string filePath = Path.Join(destination.Path, "file.txt"); + using (TempDirectory destination = new TempDirectory()) + { + string filePath = Path.Join(destination.Path, "file.txt"); - await TarFile.ExtractToDirectoryAsync(sourceArchiveFileName, destination.Path, overwriteFiles: false); + await TarFile.ExtractToDirectoryAsync(sourceArchiveFileName, destination.Path, overwriteFiles: false); - Assert.True(File.Exists(filePath)); + Assert.True(File.Exists(filePath)); + } } [Fact] @@ -80,94 +83,110 @@ public async Task Extract_Archive_File_OverwriteTrue_Async() string testCaseName = "file"; string archivePath = GetTarFilePath(CompressionMethod.Uncompressed, TestTarFormat.pax, testCaseName); - using TempDirectory destination = new TempDirectory(); - - string filePath = Path.Join(destination.Path, "file.txt"); - using (FileStream fileStream = File.Create(filePath)) + using (TempDirectory destination = new TempDirectory()) { - using StreamWriter writer = new StreamWriter(fileStream, leaveOpen: false); - writer.WriteLine("Original text"); - } + string filePath = Path.Join(destination.Path, "file.txt"); + using (FileStream fileStream = File.Create(filePath)) + { + using StreamWriter writer = new StreamWriter(fileStream, leaveOpen: false); + writer.WriteLine("Original text"); + } - await TarFile.ExtractToDirectoryAsync(archivePath, destination.Path, overwriteFiles: true); + await TarFile.ExtractToDirectoryAsync(archivePath, destination.Path, overwriteFiles: true); - Assert.True(File.Exists(filePath)); + Assert.True(File.Exists(filePath)); - using (FileStream fileStream = File.Open(filePath, FileMode.Open)) - { - using StreamReader reader = new StreamReader(fileStream); - string actualContents = reader.ReadLine(); - Assert.Equal($"Hello {testCaseName}", actualContents); // Confirm overwrite + using (FileStream fileStream = File.Open(filePath, FileMode.Open)) + { + using StreamReader reader = new StreamReader(fileStream); + string actualContents = reader.ReadLine(); + Assert.Equal($"Hello {testCaseName}", actualContents); // Confirm overwrite + } } } [Fact] - public Task Extract_Archive_File_OverwriteFalse_Async() + public async Task Extract_Archive_File_OverwriteFalse_Async() { - string sourceArchiveFileName = GetTarFilePath(CompressionMethod.Uncompressed, TestTarFormat.pax, "file"); - - using TempDirectory destination = new TempDirectory(); + using (TempDirectory destination = new TempDirectory()) + { + string sourceArchiveFileName = GetTarFilePath(CompressionMethod.Uncompressed, TestTarFormat.pax, "file"); - string filePath = Path.Join(destination.Path, "file.txt"); + string filePath = Path.Join(destination.Path, "file.txt"); - File.Create(filePath).Dispose(); + File.Create(filePath).Dispose(); - return Assert.ThrowsAsync(() => TarFile.ExtractToDirectoryAsync(sourceArchiveFileName, destination.Path, overwriteFiles: false)); + await Assert.ThrowsAsync(() => TarFile.ExtractToDirectoryAsync(sourceArchiveFileName, destination.Path, overwriteFiles: false)); + } } [Fact] public async Task Extract_AllSegmentsOfPath_Async() { - using TempDirectory source = new TempDirectory(); - using TempDirectory destination = new TempDirectory(); - - string archivePath = Path.Join(source.Path, "archive.tar"); - using FileStream archiveStream = File.Create(archivePath); - using (TarWriter writer = new TarWriter(archiveStream)) + using (TempDirectory source = new TempDirectory()) { - PaxTarEntry segment1 = new PaxTarEntry(TarEntryType.Directory, "segment1"); - writer.WriteEntry(segment1); - - PaxTarEntry segment2 = new PaxTarEntry(TarEntryType.Directory, "segment1/segment2"); - writer.WriteEntry(segment2); - - PaxTarEntry file = new PaxTarEntry(TarEntryType.RegularFile, "segment1/segment2/file.txt"); - writer.WriteEntry(file); + string archivePath = Path.Join(source.Path, "archive.tar"); + using (TempDirectory destination = new TempDirectory()) + { + FileStreamOptions fileOptions = new() + { + Access = FileAccess.Write, + Mode = FileMode.CreateNew, + Share = FileShare.None, + Options = FileOptions.Asynchronous + }; + + await using (FileStream archiveStream = new FileStream(archivePath, fileOptions)) + { + await using (TarWriter writer = new TarWriter(archiveStream)) + { + PaxTarEntry segment1 = new PaxTarEntry(TarEntryType.Directory, "segment1"); + await writer.WriteEntryAsync(segment1); + + PaxTarEntry segment2 = new PaxTarEntry(TarEntryType.Directory, "segment1/segment2"); + await writer.WriteEntryAsync(segment2); + + PaxTarEntry file = new PaxTarEntry(TarEntryType.RegularFile, "segment1/segment2/file.txt"); + await writer.WriteEntryAsync(file); + } + } + + await TarFile.ExtractToDirectoryAsync(archivePath, destination.Path, overwriteFiles: false); + + string segment1Path = Path.Join(destination.Path, "segment1"); + Assert.True(Directory.Exists(segment1Path), $"{segment1Path}' does not exist."); + + string segment2Path = Path.Join(segment1Path, "segment2"); + Assert.True(Directory.Exists(segment2Path), $"{segment2Path}' does not exist."); + + string filePath = Path.Join(segment2Path, "file.txt"); + Assert.True(File.Exists(filePath), $"{filePath}' does not exist."); + } } - - await TarFile.ExtractToDirectoryAsync(archivePath, destination.Path, overwriteFiles: false); - - string segment1Path = Path.Join(destination.Path, "segment1"); - Assert.True(Directory.Exists(segment1Path), $"{segment1Path}' does not exist."); - - string segment2Path = Path.Join(segment1Path, "segment2"); - Assert.True(Directory.Exists(segment2Path), $"{segment2Path}' does not exist."); - - string filePath = Path.Join(segment2Path, "file.txt"); - Assert.True(File.Exists(filePath), $"{filePath}' does not exist."); } [Fact] public async Task ExtractArchiveWithEntriesThatStartWithSlashDotPrefix_Async() { - using TempDirectory root = new TempDirectory(); - - using MemoryStream archiveStream = GetStrangeTarMemoryStream("prefixDotSlashAndCurrentFolderEntry"); - - await TarFile.ExtractToDirectoryAsync(archiveStream, root.Path, overwriteFiles: true); - - archiveStream.Position = 0; - - TarReader reader = new TarReader(archiveStream, leaveOpen: false); - await using (reader) + using (TempDirectory root = new TempDirectory()) { - TarEntry entry; - while ((entry = await reader.GetNextEntryAsync()) != null) + await using (MemoryStream archiveStream = GetStrangeTarMemoryStream("prefixDotSlashAndCurrentFolderEntry")) { - // Normalize the path (remove redundant segments), remove trailing separators - // this is so the first entry can be skipped if it's the same as the root directory - string entryPath = Path.TrimEndingDirectorySeparator(Path.GetFullPath(Path.Join(root.Path, entry.Name))); - Assert.True(Path.Exists(entryPath), $"Entry was not extracted: {entryPath}"); + await TarFile.ExtractToDirectoryAsync(archiveStream, root.Path, overwriteFiles: true); + + archiveStream.Position = 0; + + await using (TarReader reader = new TarReader(archiveStream, leaveOpen: false)) + { + TarEntry entry; + while ((entry = await reader.GetNextEntryAsync()) != null) + { + // Normalize the path (remove redundant segments), remove trailing separators + // this is so the first entry can be skipped if it's the same as the root directory + string entryPath = Path.TrimEndingDirectorySeparator(Path.GetFullPath(Path.Join(root.Path, entry.Name))); + Assert.True(Path.Exists(entryPath), $"Entry was not extracted: {entryPath}"); + } + } } } } 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 1aba0f1185aa27..a35a22c1050f56 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 @@ -13,12 +13,14 @@ namespace System.Formats.Tar.Tests public class TarFile_ExtractToDirectoryAsync_Stream_Tests : TarTestsBase { [Fact] - public Task ExtractToDirectoryAsync_Cancel() + public async Task ExtractToDirectoryAsync_Cancel() { CancellationTokenSource cs = new CancellationTokenSource(); cs.Cancel(); - MemoryStream archiveStream = new MemoryStream(); - return Assert.ThrowsAsync(() => TarFile.ExtractToDirectoryAsync(archiveStream, "directory", overwriteFiles: true, cs.Token)); + using (MemoryStream archiveStream = new MemoryStream()) + { + await Assert.ThrowsAsync(() => TarFile.ExtractToDirectoryAsync(archiveStream, "directory", overwriteFiles: true, cs.Token)); + } } [Fact] @@ -28,58 +30,70 @@ public Task NullStream_Throws_Async() => [Fact] public async Task InvalidPath_Throws_Async() { - using MemoryStream archive = new MemoryStream(); - await Assert.ThrowsAsync(() => TarFile.ExtractToDirectoryAsync(archive, destinationDirectoryName: null, overwriteFiles: false)); - await Assert.ThrowsAsync(() => TarFile.ExtractToDirectoryAsync(archive, destinationDirectoryName: string.Empty, overwriteFiles: false)); + using (MemoryStream archive = new MemoryStream()) + { + await Assert.ThrowsAsync(() => TarFile.ExtractToDirectoryAsync(archive, destinationDirectoryName: null, overwriteFiles: false)); + await Assert.ThrowsAsync(() => TarFile.ExtractToDirectoryAsync(archive, destinationDirectoryName: string.Empty, overwriteFiles: false)); + } } [Fact] - public Task UnreadableStream_Throws_Async() + public async Task UnreadableStream_Throws_Async() { - using MemoryStream archive = new MemoryStream(); - using WrappedStream unreadable = new WrappedStream(archive, canRead: false, canWrite: true, canSeek: true); - return Assert.ThrowsAsync(() => TarFile.ExtractToDirectoryAsync(unreadable, destinationDirectoryName: "path", overwriteFiles: false)); + using (MemoryStream archive = new MemoryStream()) + { + using (WrappedStream unreadable = new WrappedStream(archive, canRead: false, canWrite: true, canSeek: true)) + { + await Assert.ThrowsAsync(() => TarFile.ExtractToDirectoryAsync(unreadable, destinationDirectoryName: "path", overwriteFiles: false)); + } + } } [Fact] - public Task NonExistentDirectory_Throws_Async() + public async Task NonExistentDirectory_Throws_Async() { - using TempDirectory root = new TempDirectory(); - string dirPath = Path.Join(root.Path, "dir"); + using (TempDirectory root = new TempDirectory()) + { + string dirPath = Path.Join(root.Path, "dir"); - using MemoryStream archive = new MemoryStream(); - return Assert.ThrowsAsync(() => TarFile.ExtractToDirectoryAsync(archive, destinationDirectoryName: dirPath, overwriteFiles: false)); + using (MemoryStream archive = new MemoryStream()) + { + await Assert.ThrowsAsync(() => TarFile.ExtractToDirectoryAsync(archive, destinationDirectoryName: dirPath, overwriteFiles: false)); + } + } } [Fact] public async Task ExtractEntry_ManySubfolderSegments_NoPrecedingDirectoryEntries_Async() { - using TempDirectory root = new TempDirectory(); - - string firstSegment = "a"; - string secondSegment = Path.Join(firstSegment, "b"); - string fileWithTwoSegments = Path.Join(secondSegment, "c.txt"); - - using MemoryStream archive = new MemoryStream(); - TarWriter writer = new TarWriter(archive, TarEntryFormat.Ustar, leaveOpen: true); - await using (writer) + using (TempDirectory root = new TempDirectory()) { - // No preceding directory entries for the segments - UstarTarEntry entry = new UstarTarEntry(TarEntryType.RegularFile, fileWithTwoSegments); - - entry.DataStream = new MemoryStream(); - entry.DataStream.Write(new byte[] { 0x1 }); - entry.DataStream.Seek(0, SeekOrigin.Begin); - - await writer.WriteEntryAsync(entry); + string firstSegment = "a"; + string secondSegment = Path.Join(firstSegment, "b"); + string fileWithTwoSegments = Path.Join(secondSegment, "c.txt"); + + await using (MemoryStream archive = new MemoryStream()) + { + await using (TarWriter writer = new TarWriter(archive, TarEntryFormat.Ustar, leaveOpen: true)) + { + // No preceding directory entries for the segments + UstarTarEntry entry = new UstarTarEntry(TarEntryType.RegularFile, fileWithTwoSegments); + + entry.DataStream = new MemoryStream(); + entry.DataStream.Write(new byte[] { 0x1 }); + entry.DataStream.Seek(0, SeekOrigin.Begin); + + await writer.WriteEntryAsync(entry); + } + + archive.Seek(0, SeekOrigin.Begin); + await TarFile.ExtractToDirectoryAsync(archive, root.Path, overwriteFiles: false); + + Assert.True(Directory.Exists(Path.Join(root.Path, firstSegment))); + Assert.True(Directory.Exists(Path.Join(root.Path, secondSegment))); + Assert.True(File.Exists(Path.Join(root.Path, fileWithTwoSegments))); + } } - - archive.Seek(0, SeekOrigin.Begin); - await TarFile.ExtractToDirectoryAsync(archive, root.Path, overwriteFiles: false); - - Assert.True(Directory.Exists(Path.Join(root.Path, firstSegment))); - Assert.True(Directory.Exists(Path.Join(root.Path, secondSegment))); - Assert.True(File.Exists(Path.Join(root.Path, fileWithTwoSegments))); } [Theory] @@ -87,22 +101,23 @@ public async Task ExtractEntry_ManySubfolderSegments_NoPrecedingDirectoryEntries [InlineData(TarEntryType.HardLink)] public async Task Extract_LinkEntry_TargetOutsideDirectory_Async(TarEntryType entryType) { - using MemoryStream archive = new MemoryStream(); - TarWriter writer = new TarWriter(archive, TarEntryFormat.Ustar, leaveOpen: true); - await using (writer) + await using (MemoryStream archive = new MemoryStream()) { - UstarTarEntry entry = new UstarTarEntry(entryType, "link"); - entry.LinkName = PlatformDetection.IsWindows ? @"C:\Windows\System32\notepad.exe" : "/usr/bin/nano"; - await writer.WriteEntryAsync(entry); + await using (TarWriter writer = new TarWriter(archive, TarEntryFormat.Ustar, leaveOpen: true)) + { + UstarTarEntry entry = new UstarTarEntry(entryType, "link"); + entry.LinkName = PlatformDetection.IsWindows ? @"C:\Windows\System32\notepad.exe" : "/usr/bin/nano"; + await writer.WriteEntryAsync(entry); + } + + archive.Seek(0, SeekOrigin.Begin); + + using (TempDirectory root = new TempDirectory()) + { + await Assert.ThrowsAsync(() => TarFile.ExtractToDirectoryAsync(archive, root.Path, overwriteFiles: false)); + Assert.Equal(0, Directory.GetFileSystemEntries(root.Path).Count()); + } } - - archive.Seek(0, SeekOrigin.Begin); - - using TempDirectory root = new TempDirectory(); - - await Assert.ThrowsAsync(() => TarFile.ExtractToDirectoryAsync(archive, root.Path, overwriteFiles: false)); - - Assert.Equal(0, Directory.GetFileSystemEntries(root.Path).Count()); } [ConditionalTheory(typeof(MountHelper), nameof(MountHelper.CanCreateSymbolicLinks))] @@ -131,31 +146,33 @@ public async Task Extract_LinkEntry_TargetOutsideDirectory_Async(TarEntryType en // the base directory past that length, to ensure this scenario is tested everywhere. private async Task Extract_LinkEntry_TargetInsideDirectory_Internal_Async(TarEntryType entryType, TarEntryFormat format, string subfolder) { - using TempDirectory root = new TempDirectory(); + using (TempDirectory root = new TempDirectory()) + { + string baseDir = string.IsNullOrEmpty(subfolder) ? root.Path : Path.Join(root.Path, subfolder); + Directory.CreateDirectory(baseDir); - string baseDir = string.IsNullOrEmpty(subfolder) ? root.Path : Path.Join(root.Path, subfolder); - Directory.CreateDirectory(baseDir); + string linkName = "link"; + string targetName = "target"; + string targetPath = Path.Join(baseDir, targetName); - string linkName = "link"; - string targetName = "target"; - string targetPath = Path.Join(baseDir, targetName); + File.Create(targetPath).Dispose(); - File.Create(targetPath).Dispose(); + await using (MemoryStream archive = new MemoryStream()) + { + await using (TarWriter writer = new TarWriter(archive, format, leaveOpen: true)) + { + TarEntry entry = InvokeTarEntryCreationConstructor(format, entryType, linkName); + entry.LinkName = targetPath; + await writer.WriteEntryAsync(entry); + } - using MemoryStream archive = new MemoryStream(); - TarWriter writer = new TarWriter(archive, format, leaveOpen: true); - await using (writer) - { - TarEntry entry = InvokeTarEntryCreationConstructor(format, entryType, linkName); - entry.LinkName = targetPath; - await writer.WriteEntryAsync(entry); - } + archive.Seek(0, SeekOrigin.Begin); - archive.Seek(0, SeekOrigin.Begin); + await TarFile.ExtractToDirectoryAsync(archive, baseDir, overwriteFiles: false); - await TarFile.ExtractToDirectoryAsync(archive, baseDir, overwriteFiles: false); - - Assert.Equal(2, Directory.GetFileSystemEntries(baseDir).Count()); + Assert.Equal(2, Directory.GetFileSystemEntries(baseDir).Count()); + } + } } } } diff --git a/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.File.Async.Tests.Base.cs b/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.File.Async.Tests.Base.cs index 52974e55e31f5e..eb0f454aafbce9 100644 --- a/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.File.Async.Tests.Base.cs +++ b/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.File.Async.Tests.Base.cs @@ -16,221 +16,221 @@ public class TarReader_File_Tests_Async_Base : TarTestsBase protected async Task Read_Archive_File_Async_Internal(TarEntryFormat format, TestTarFormat testFormat) { string testCaseName = "file"; - using MemoryStream ms = GetTarMemoryStream(CompressionMethod.Uncompressed, testFormat, testCaseName); - - TarReader reader = new TarReader(ms); - await using (reader) + await using (MemoryStream ms = GetTarMemoryStream(CompressionMethod.Uncompressed, testFormat, testCaseName)) { - if (testFormat is TestTarFormat.pax_gea) + await using (TarReader reader = new TarReader(ms)) { - TarEntry entry = await reader.GetNextEntryAsync(); - VerifyGlobalExtendedAttributes(entry); - } + if (testFormat is TestTarFormat.pax_gea) + { + TarEntry entry = await reader.GetNextEntryAsync(); + VerifyGlobalExtendedAttributes(entry); + } - TarEntry file = await reader.GetNextEntryAsync(); + TarEntry file = await reader.GetNextEntryAsync(); - VerifyRegularFileEntry(file, format, "file.txt", $"Hello {testCaseName}"); + VerifyRegularFileEntry(file, format, "file.txt", $"Hello {testCaseName}"); - Assert.Null(await reader.GetNextEntryAsync()); + Assert.Null(await reader.GetNextEntryAsync()); + } } } protected async Task Read_Archive_File_HardLink_Async_Internal(TarEntryFormat format, TestTarFormat testFormat) { string testCaseName = "file_hardlink"; - using MemoryStream ms = GetTarMemoryStream(CompressionMethod.Uncompressed, testFormat, testCaseName); - - TarReader reader = new TarReader(ms); - await using (reader) + await using (MemoryStream ms = GetTarMemoryStream(CompressionMethod.Uncompressed, testFormat, testCaseName)) { - if (testFormat is TestTarFormat.pax_gea) + await using (TarReader reader = new TarReader(ms)) { - TarEntry entry = await reader.GetNextEntryAsync(); - VerifyGlobalExtendedAttributes(entry); - } + if (testFormat is TestTarFormat.pax_gea) + { + TarEntry entry = await reader.GetNextEntryAsync(); + VerifyGlobalExtendedAttributes(entry); + } - TarEntry file = await reader.GetNextEntryAsync(); + TarEntry file = await reader.GetNextEntryAsync(); - VerifyRegularFileEntry(file, format, "file.txt", $"Hello {testCaseName}"); + VerifyRegularFileEntry(file, format, "file.txt", $"Hello {testCaseName}"); - TarEntry hardLink = await reader.GetNextEntryAsync(); - // The 'tar' tool detects hardlinks as regular files and saves them as such in the archives, for all formats - VerifyRegularFileEntry(hardLink, format, "hardlink.txt", $"Hello {testCaseName}"); + TarEntry hardLink = await reader.GetNextEntryAsync(); + // The 'tar' tool detects hardlinks as regular files and saves them as such in the archives, for all formats + VerifyRegularFileEntry(hardLink, format, "hardlink.txt", $"Hello {testCaseName}"); - Assert.Null(await reader.GetNextEntryAsync()); + Assert.Null(await reader.GetNextEntryAsync()); + } } } protected async Task Read_Archive_File_SymbolicLink_Async_Internal(TarEntryFormat format, TestTarFormat testFormat) { string testCaseName = "file_symlink"; - using MemoryStream ms = GetTarMemoryStream(CompressionMethod.Uncompressed, testFormat, testCaseName); - - TarReader reader = new TarReader(ms); - await using (reader) + await using (MemoryStream ms = GetTarMemoryStream(CompressionMethod.Uncompressed, testFormat, testCaseName)) { - if (testFormat is TestTarFormat.pax_gea) + await using (TarReader reader = new TarReader(ms)) { - TarEntry entry = await reader.GetNextEntryAsync(); - VerifyGlobalExtendedAttributes(entry); - } + if (testFormat is TestTarFormat.pax_gea) + { + TarEntry entry = await reader.GetNextEntryAsync(); + VerifyGlobalExtendedAttributes(entry); + } - TarEntry file = await reader.GetNextEntryAsync(); + TarEntry file = await reader.GetNextEntryAsync(); - VerifyRegularFileEntry(file, format, "file.txt", $"Hello {testCaseName}"); + VerifyRegularFileEntry(file, format, "file.txt", $"Hello {testCaseName}"); - TarEntry symbolicLink = await reader.GetNextEntryAsync(); - VerifySymbolicLinkEntry(symbolicLink, format, "link.txt", "file.txt"); + TarEntry symbolicLink = await reader.GetNextEntryAsync(); + VerifySymbolicLinkEntry(symbolicLink, format, "link.txt", "file.txt"); - Assert.Null(await reader.GetNextEntryAsync()); + Assert.Null(await reader.GetNextEntryAsync()); + } } } protected async Task Read_Archive_Folder_File_Async_Internal(TarEntryFormat format, TestTarFormat testFormat) { string testCaseName = "folder_file"; - using MemoryStream ms = GetTarMemoryStream(CompressionMethod.Uncompressed, testFormat, testCaseName); - - TarReader reader = new TarReader(ms); - await using (reader) + await using (MemoryStream ms = GetTarMemoryStream(CompressionMethod.Uncompressed, testFormat, testCaseName)) { - if (testFormat is TestTarFormat.pax_gea) + await using (TarReader reader = new TarReader(ms)) { - TarEntry entry = await reader.GetNextEntryAsync(); - VerifyGlobalExtendedAttributes(entry); - } + if (testFormat is TestTarFormat.pax_gea) + { + TarEntry entry = await reader.GetNextEntryAsync(); + VerifyGlobalExtendedAttributes(entry); + } - TarEntry directory = await reader.GetNextEntryAsync(); + TarEntry directory = await reader.GetNextEntryAsync(); - VerifyDirectoryEntry(directory, format, "folder/"); + VerifyDirectoryEntry(directory, format, "folder/"); - TarEntry file = await reader.GetNextEntryAsync(); - VerifyRegularFileEntry(file, format, "folder/file.txt", $"Hello {testCaseName}"); + TarEntry file = await reader.GetNextEntryAsync(); + VerifyRegularFileEntry(file, format, "folder/file.txt", $"Hello {testCaseName}"); - Assert.Null(await reader.GetNextEntryAsync()); + Assert.Null(await reader.GetNextEntryAsync()); + } } } protected async Task Read_Archive_Folder_File_Utf8_Async_Internal(TarEntryFormat format, TestTarFormat testFormat) { string testCaseName = "folder_file_utf8"; - using MemoryStream ms = GetTarMemoryStream(CompressionMethod.Uncompressed, testFormat, testCaseName); - - TarReader reader = new TarReader(ms); - await using (reader) + await using (MemoryStream ms = GetTarMemoryStream(CompressionMethod.Uncompressed, testFormat, testCaseName)) { - if (testFormat is TestTarFormat.pax_gea) + await using (TarReader reader = new TarReader(ms)) { - TarEntry entry = await reader.GetNextEntryAsync(); - VerifyGlobalExtendedAttributes(entry); - } + if (testFormat is TestTarFormat.pax_gea) + { + TarEntry entry = await reader.GetNextEntryAsync(); + VerifyGlobalExtendedAttributes(entry); + } - TarEntry directory = await reader.GetNextEntryAsync(); + TarEntry directory = await reader.GetNextEntryAsync(); - VerifyDirectoryEntry(directory, format, "f\u00f6ld\u00ebr/"); //földër + VerifyDirectoryEntry(directory, format, "f\u00f6ld\u00ebr/"); //földër - TarEntry file = await reader.GetNextEntryAsync(); - VerifyRegularFileEntry(file, format, "f\u00f6ld\u00ebr/\u00e1\u00f6\u00f1.txt", $"Hello {testCaseName}"); // földër/áöñ.txt + TarEntry file = await reader.GetNextEntryAsync(); + VerifyRegularFileEntry(file, format, "f\u00f6ld\u00ebr/\u00e1\u00f6\u00f1.txt", $"Hello {testCaseName}"); // földër/áöñ.txt - Assert.Null(await reader.GetNextEntryAsync()); + Assert.Null(await reader.GetNextEntryAsync()); + } } } protected async Task Read_Archive_Folder_Subfolder_File_Async_Internal(TarEntryFormat format, TestTarFormat testFormat) { string testCaseName = "folder_subfolder_file"; - using MemoryStream ms = GetTarMemoryStream(CompressionMethod.Uncompressed, testFormat, testCaseName); - - TarReader reader = new TarReader(ms); - await using (reader) + await using (MemoryStream ms = GetTarMemoryStream(CompressionMethod.Uncompressed, testFormat, testCaseName)) { - if (testFormat is TestTarFormat.pax_gea) + await using (TarReader reader = new TarReader(ms)) { - TarEntry entry = await reader.GetNextEntryAsync(); - VerifyGlobalExtendedAttributes(entry); - } + if (testFormat is TestTarFormat.pax_gea) + { + TarEntry entry = await reader.GetNextEntryAsync(); + VerifyGlobalExtendedAttributes(entry); + } - TarEntry parent = await reader.GetNextEntryAsync(); + TarEntry parent = await reader.GetNextEntryAsync(); - VerifyDirectoryEntry(parent, format, "parent/"); + VerifyDirectoryEntry(parent, format, "parent/"); - TarEntry child = await reader.GetNextEntryAsync(); - VerifyDirectoryEntry(child, format, "parent/child/"); + TarEntry child = await reader.GetNextEntryAsync(); + VerifyDirectoryEntry(child, format, "parent/child/"); - TarEntry file = await reader.GetNextEntryAsync(); - VerifyRegularFileEntry(file, format, "parent/child/file.txt", $"Hello {testCaseName}"); + TarEntry file = await reader.GetNextEntryAsync(); + VerifyRegularFileEntry(file, format, "parent/child/file.txt", $"Hello {testCaseName}"); - Assert.Null(await reader.GetNextEntryAsync()); + Assert.Null(await reader.GetNextEntryAsync()); + } } } protected async Task Read_Archive_FolderSymbolicLink_Folder_Subfolder_File_Async_Internal(TarEntryFormat format, TestTarFormat testFormat) { string testCaseName = "foldersymlink_folder_subfolder_file"; - using MemoryStream ms = GetTarMemoryStream(CompressionMethod.Uncompressed, testFormat, testCaseName); - - TarReader reader = new TarReader(ms); - await using (reader) + await using (MemoryStream ms = GetTarMemoryStream(CompressionMethod.Uncompressed, testFormat, testCaseName)) { - if (testFormat is TestTarFormat.pax_gea) + await using (TarReader reader = new TarReader(ms)) { - TarEntry entry = await reader.GetNextEntryAsync(); - VerifyGlobalExtendedAttributes(entry); - } + if (testFormat is TestTarFormat.pax_gea) + { + TarEntry entry = await reader.GetNextEntryAsync(); + VerifyGlobalExtendedAttributes(entry); + } - TarEntry childlink = await reader.GetNextEntryAsync(); + TarEntry childlink = await reader.GetNextEntryAsync(); - VerifySymbolicLinkEntry(childlink, format, "childlink", "parent/child"); + VerifySymbolicLinkEntry(childlink, format, "childlink", "parent/child"); - TarEntry parent = await reader.GetNextEntryAsync(); - VerifyDirectoryEntry(parent, format, "parent/"); + TarEntry parent = await reader.GetNextEntryAsync(); + VerifyDirectoryEntry(parent, format, "parent/"); - TarEntry child = await reader.GetNextEntryAsync(); - VerifyDirectoryEntry(child, format, "parent/child/"); + TarEntry child = await reader.GetNextEntryAsync(); + VerifyDirectoryEntry(child, format, "parent/child/"); - TarEntry file = await reader.GetNextEntryAsync(); - VerifyRegularFileEntry(file, format, "parent/child/file.txt", $"Hello {testCaseName}"); + TarEntry file = await reader.GetNextEntryAsync(); + VerifyRegularFileEntry(file, format, "parent/child/file.txt", $"Hello {testCaseName}"); - Assert.Null(await reader.GetNextEntryAsync()); + Assert.Null(await reader.GetNextEntryAsync()); + } } } protected async Task Read_Archive_Many_Small_Files_Async_Internal(TarEntryFormat format, TestTarFormat testFormat) { string testCaseName = "many_small_files"; - using MemoryStream ms = GetTarMemoryStream(CompressionMethod.Uncompressed, testFormat, testCaseName); - - TarReader reader = new TarReader(ms); - await using (reader) + await using (MemoryStream ms = GetTarMemoryStream(CompressionMethod.Uncompressed, testFormat, testCaseName)) { - TarEntry entry; - if (testFormat is TestTarFormat.pax_gea) + await using (TarReader reader = new TarReader(ms)) { - entry = await reader.GetNextEntryAsync(); - VerifyGlobalExtendedAttributes(entry); - } + TarEntry entry; + if (testFormat is TestTarFormat.pax_gea) + { + entry = await reader.GetNextEntryAsync(); + VerifyGlobalExtendedAttributes(entry); + } - List entries = new List(); - bool isFirstEntry = true; - while ((entry = await reader.GetNextEntryAsync()) != null) - { - if (isFirstEntry) + List entries = new List(); + bool isFirstEntry = true; + while ((entry = await reader.GetNextEntryAsync()) != null) { - isFirstEntry = false; + if (isFirstEntry) + { + isFirstEntry = false; + } + Assert.Equal(format, entry.Format); + entries.Add(entry); } - Assert.Equal(format, entry.Format); - entries.Add(entry); - } - int directoriesCount = entries.Count(e => e.EntryType == TarEntryType.Directory); - Assert.Equal(10, directoriesCount); + int directoriesCount = entries.Count(e => e.EntryType == TarEntryType.Directory); + Assert.Equal(10, directoriesCount); - TarEntryType actualEntryType = format is TarEntryFormat.V7 ? TarEntryType.V7RegularFile : TarEntryType.RegularFile; + TarEntryType actualEntryType = format is TarEntryFormat.V7 ? TarEntryType.V7RegularFile : TarEntryType.RegularFile; - for (int i = 0; i < 10; i++) - { - int filesCount = entries.Count(e => e.EntryType == actualEntryType && e.Name.StartsWith($"{i}/")); - Assert.Equal(10, filesCount); + for (int i = 0; i < 10; i++) + { + int filesCount = entries.Count(e => e.EntryType == actualEntryType && e.Name.StartsWith($"{i}/")); + Assert.Equal(10, filesCount); + } } } } @@ -238,141 +238,141 @@ protected async Task Read_Archive_Many_Small_Files_Async_Internal(TarEntryFormat protected async Task Read_Archive_LongPath_Splitable_Under255_Async_Internal(TarEntryFormat format, TestTarFormat testFormat) { string testCaseName = "longpath_splitable_under255"; - using MemoryStream ms = GetTarMemoryStream(CompressionMethod.Uncompressed, testFormat, testCaseName); - - TarReader reader = new TarReader(ms); - await using (reader) + await using (MemoryStream ms = GetTarMemoryStream(CompressionMethod.Uncompressed, testFormat, testCaseName)) { - if (testFormat is TestTarFormat.pax_gea) + await using (TarReader reader = new TarReader(ms)) { - TarEntry entry = await reader.GetNextEntryAsync(); - VerifyGlobalExtendedAttributes(entry); - } + if (testFormat is TestTarFormat.pax_gea) + { + TarEntry entry = await reader.GetNextEntryAsync(); + VerifyGlobalExtendedAttributes(entry); + } - TarEntry directory = await reader.GetNextEntryAsync(); + TarEntry directory = await reader.GetNextEntryAsync(); - VerifyDirectoryEntry(directory, format, - "00000000001111111111222222222233333333334444444444555555555566666666667777777777888888888899999999/"); + VerifyDirectoryEntry(directory, format, + "00000000001111111111222222222233333333334444444444555555555566666666667777777777888888888899999999/"); - TarEntry file = await reader.GetNextEntryAsync(); - VerifyRegularFileEntry(file, format, - $"00000000001111111111222222222233333333334444444444555555555566666666667777777777888888888899999999/00000000001111111111222222222233333333334444444444555555555566666666667777777777888888888899999.txt", - $"Hello {testCaseName}"); + TarEntry file = await reader.GetNextEntryAsync(); + VerifyRegularFileEntry(file, format, + $"00000000001111111111222222222233333333334444444444555555555566666666667777777777888888888899999999/00000000001111111111222222222233333333334444444444555555555566666666667777777777888888888899999.txt", + $"Hello {testCaseName}"); - Assert.Null(await reader.GetNextEntryAsync()); + Assert.Null(await reader.GetNextEntryAsync()); + } } } protected async Task Read_Archive_SpecialFiles_Async_Internal(TarEntryFormat format, TestTarFormat testFormat) { string testCaseName = "specialfiles"; - using MemoryStream ms = GetTarMemoryStream(CompressionMethod.Uncompressed, testFormat, testCaseName); - - TarReader reader = new TarReader(ms); - await using (reader) + await using (MemoryStream ms = GetTarMemoryStream(CompressionMethod.Uncompressed, testFormat, testCaseName)) { - if (testFormat is TestTarFormat.pax_gea) + await using (TarReader reader = new TarReader(ms)) { - TarEntry entry = await reader.GetNextEntryAsync(); - VerifyGlobalExtendedAttributes(entry); - } + if (testFormat is TestTarFormat.pax_gea) + { + TarEntry entry = await reader.GetNextEntryAsync(); + VerifyGlobalExtendedAttributes(entry); + } - PosixTarEntry blockDevice = await reader.GetNextEntryAsync() as PosixTarEntry; + PosixTarEntry blockDevice = await reader.GetNextEntryAsync() as PosixTarEntry; - VerifyBlockDeviceEntry(blockDevice, format, AssetBlockDeviceFileName); + VerifyBlockDeviceEntry(blockDevice, format, AssetBlockDeviceFileName); - PosixTarEntry characterDevice = await reader.GetNextEntryAsync() as PosixTarEntry; - VerifyCharacterDeviceEntry(characterDevice, format, AssetCharacterDeviceFileName); + PosixTarEntry characterDevice = await reader.GetNextEntryAsync() as PosixTarEntry; + VerifyCharacterDeviceEntry(characterDevice, format, AssetCharacterDeviceFileName); - PosixTarEntry fifo = await reader.GetNextEntryAsync() as PosixTarEntry; - VerifyFifoEntry(fifo, format, "fifofile"); + PosixTarEntry fifo = await reader.GetNextEntryAsync() as PosixTarEntry; + VerifyFifoEntry(fifo, format, "fifofile"); - Assert.Null(await reader.GetNextEntryAsync()); + Assert.Null(await reader.GetNextEntryAsync()); + } } } protected async Task Read_Archive_File_LongSymbolicLink_Async_Internal(TarEntryFormat format, TestTarFormat testFormat) { string testCaseName = "file_longsymlink"; - using MemoryStream ms = GetTarMemoryStream(CompressionMethod.Uncompressed, testFormat, testCaseName); - - TarReader reader = new TarReader(ms); - await using (reader) + await using (MemoryStream ms = GetTarMemoryStream(CompressionMethod.Uncompressed, testFormat, testCaseName)) { - if (testFormat is TestTarFormat.pax_gea) + await using (TarReader reader = new TarReader(ms)) { - TarEntry entry = await reader.GetNextEntryAsync(); - VerifyGlobalExtendedAttributes(entry); - } + if (testFormat is TestTarFormat.pax_gea) + { + TarEntry entry = await reader.GetNextEntryAsync(); + VerifyGlobalExtendedAttributes(entry); + } - TarEntry directory = await reader.GetNextEntryAsync(); + TarEntry directory = await reader.GetNextEntryAsync(); - VerifyDirectoryEntry(directory, format, - "000000000011111111112222222222333333333344444444445555555555666666666677777777778888888888999999999900000000001111111111222222222233333333334444444444555555555566666666667777777777888888888899999999990000000000111111111122222222223333333333444444444455555/"); + VerifyDirectoryEntry(directory, format, + "000000000011111111112222222222333333333344444444445555555555666666666677777777778888888888999999999900000000001111111111222222222233333333334444444444555555555566666666667777777777888888888899999999990000000000111111111122222222223333333333444444444455555/"); - TarEntry file = await reader.GetNextEntryAsync(); - VerifyRegularFileEntry(file, format, - "000000000011111111112222222222333333333344444444445555555555666666666677777777778888888888999999999900000000001111111111222222222233333333334444444444555555555566666666667777777777888888888899999999990000000000111111111122222222223333333333444444444455555/00000000001111111111222222222233333333334444444444555555555566666666667777777777888888888899999999990000000000111111111122222222223333333333444444444455555555556666666666777777777788888888889999999999000000000011111111112222222222333333333344444444445.txt", - $"Hello {testCaseName}"); + TarEntry file = await reader.GetNextEntryAsync(); + VerifyRegularFileEntry(file, format, + "000000000011111111112222222222333333333344444444445555555555666666666677777777778888888888999999999900000000001111111111222222222233333333334444444444555555555566666666667777777777888888888899999999990000000000111111111122222222223333333333444444444455555/00000000001111111111222222222233333333334444444444555555555566666666667777777777888888888899999999990000000000111111111122222222223333333333444444444455555555556666666666777777777788888888889999999999000000000011111111112222222222333333333344444444445.txt", + $"Hello {testCaseName}"); - TarEntry symbolicLink = await reader.GetNextEntryAsync(); - VerifySymbolicLinkEntry(symbolicLink, format, - "link.txt", - "000000000011111111112222222222333333333344444444445555555555666666666677777777778888888888999999999900000000001111111111222222222233333333334444444444555555555566666666667777777777888888888899999999990000000000111111111122222222223333333333444444444455555/00000000001111111111222222222233333333334444444444555555555566666666667777777777888888888899999999990000000000111111111122222222223333333333444444444455555555556666666666777777777788888888889999999999000000000011111111112222222222333333333344444444445.txt"); + TarEntry symbolicLink = await reader.GetNextEntryAsync(); + VerifySymbolicLinkEntry(symbolicLink, format, + "link.txt", + "000000000011111111112222222222333333333344444444445555555555666666666677777777778888888888999999999900000000001111111111222222222233333333334444444444555555555566666666667777777777888888888899999999990000000000111111111122222222223333333333444444444455555/00000000001111111111222222222233333333334444444444555555555566666666667777777777888888888899999999990000000000111111111122222222223333333333444444444455555555556666666666777777777788888888889999999999000000000011111111112222222222333333333344444444445.txt"); - Assert.Null(await reader.GetNextEntryAsync()); + Assert.Null(await reader.GetNextEntryAsync()); + } } } protected async Task Read_Archive_LongFileName_Over100_Under255_Async_Internal(TarEntryFormat format, TestTarFormat testFormat) { string testCaseName = "longfilename_over100_under255"; - using MemoryStream ms = GetTarMemoryStream(CompressionMethod.Uncompressed, testFormat, testCaseName); - - TarReader reader = new TarReader(ms); - await using (reader) + await using (MemoryStream ms = GetTarMemoryStream(CompressionMethod.Uncompressed, testFormat, testCaseName)) { - if (testFormat is TestTarFormat.pax_gea) + await using (TarReader reader = new TarReader(ms)) { - TarEntry entry = await reader.GetNextEntryAsync(); - VerifyGlobalExtendedAttributes(entry); - } + if (testFormat is TestTarFormat.pax_gea) + { + TarEntry entry = await reader.GetNextEntryAsync(); + VerifyGlobalExtendedAttributes(entry); + } - TarEntry file = await reader.GetNextEntryAsync(); + TarEntry file = await reader.GetNextEntryAsync(); - VerifyRegularFileEntry(file, format, - "000000000011111111112222222222333333333344444444445555555555666666666677777777778888888888999999999900000000001111111111222222222233333333334444444444.txt", - $"Hello {testCaseName}"); + VerifyRegularFileEntry(file, format, + "000000000011111111112222222222333333333344444444445555555555666666666677777777778888888888999999999900000000001111111111222222222233333333334444444444.txt", + $"Hello {testCaseName}"); - Assert.Null(await reader.GetNextEntryAsync()); + Assert.Null(await reader.GetNextEntryAsync()); + } } } protected async Task Read_Archive_LongPath_Over255_Async_Internal(TarEntryFormat format, TestTarFormat testFormat) { string testCaseName = "longpath_over255"; - using MemoryStream ms = GetTarMemoryStream(CompressionMethod.Uncompressed, testFormat, testCaseName); - - TarReader reader = new TarReader(ms); - await using (reader) + await using (MemoryStream ms = GetTarMemoryStream(CompressionMethod.Uncompressed, testFormat, testCaseName)) { - if (testFormat is TestTarFormat.pax_gea) + await using (TarReader reader = new TarReader(ms)) { - TarEntry entry = await reader.GetNextEntryAsync(); - VerifyGlobalExtendedAttributes(entry); - } + if (testFormat is TestTarFormat.pax_gea) + { + TarEntry entry = await reader.GetNextEntryAsync(); + VerifyGlobalExtendedAttributes(entry); + } - TarEntry directory = await reader.GetNextEntryAsync(); + TarEntry directory = await reader.GetNextEntryAsync(); - VerifyDirectoryEntry(directory, format, - "000000000011111111112222222222333333333344444444445555555555666666666677777777778888888888999999999900000000001111111111222222222233333333334444444444555555555566666666667777777777888888888899999999990000000000111111111122222222223333333333444444444455555/"); + VerifyDirectoryEntry(directory, format, + "000000000011111111112222222222333333333344444444445555555555666666666677777777778888888888999999999900000000001111111111222222222233333333334444444444555555555566666666667777777777888888888899999999990000000000111111111122222222223333333333444444444455555/"); - TarEntry file = await reader.GetNextEntryAsync(); - VerifyRegularFileEntry(file, format, - "000000000011111111112222222222333333333344444444445555555555666666666677777777778888888888999999999900000000001111111111222222222233333333334444444444555555555566666666667777777777888888888899999999990000000000111111111122222222223333333333444444444455555/00000000001111111111222222222233333333334444444444555555555566666666667777777777888888888899999999990000000000111111111122222222223333333333444444444455555555556666666666777777777788888888889999999999000000000011111111112222222222333333333344444444445.txt", - $"Hello {testCaseName}"); + TarEntry file = await reader.GetNextEntryAsync(); + VerifyRegularFileEntry(file, format, + "000000000011111111112222222222333333333344444444445555555555666666666677777777778888888888999999999900000000001111111111222222222233333333334444444444555555555566666666667777777777888888888899999999990000000000111111111122222222223333333333444444444455555/00000000001111111111222222222233333333334444444444555555555566666666667777777777888888888899999999990000000000111111111122222222223333333333444444444455555555556666666666777777777788888888889999999999000000000011111111112222222222333333333344444444445.txt", + $"Hello {testCaseName}"); - Assert.Null(await reader.GetNextEntryAsync()); + Assert.Null(await reader.GetNextEntryAsync()); + } } } diff --git a/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.File.GlobalExtendedAttributes.Async.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.File.GlobalExtendedAttributes.Async.Tests.cs index 6ff0e6148b9c27..e1d16dae2d4896 100644 --- a/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.File.GlobalExtendedAttributes.Async.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.File.GlobalExtendedAttributes.Async.Tests.cs @@ -66,23 +66,24 @@ public Task Read_Archive_LongPath_Over255_Async() => [Fact] public async Task ExtractGlobalExtendedAttributesEntry_Throws_Async() { - using TempDirectory root = new TempDirectory(); - - using MemoryStream archiveStream = new MemoryStream(); - TarWriter writer = new TarWriter(archiveStream, leaveOpen: true); - await using (writer) - { - PaxGlobalExtendedAttributesTarEntry gea = new PaxGlobalExtendedAttributesTarEntry(new Dictionary()); - await writer.WriteEntryAsync(gea); - } - - archiveStream.Position = 0; - - TarReader reader = new TarReader(archiveStream, leaveOpen: false); - await using (reader) + using (TempDirectory root = new TempDirectory()) { - TarEntry entry = await reader.GetNextEntryAsync(); - await Assert.ThrowsAsync(() => entry.ExtractToFileAsync(Path.Join(root.Path, "file"), overwrite: true)); + await using (MemoryStream archiveStream = new MemoryStream()) + { + await using (TarWriter writer = new TarWriter(archiveStream, leaveOpen: true)) + { + PaxGlobalExtendedAttributesTarEntry gea = new PaxGlobalExtendedAttributesTarEntry(new Dictionary()); + await writer.WriteEntryAsync(gea); + } + + archiveStream.Position = 0; + + await using (TarReader reader = new TarReader(archiveStream, leaveOpen: false)) + { + TarEntry entry = await reader.GetNextEntryAsync(); + await Assert.ThrowsAsync(() => entry.ExtractToFileAsync(Path.Join(root.Path, "file"), overwrite: true)); + } + } } } } 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 3dffef9d9a4f25..77ddf1f1320621 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 @@ -15,14 +15,10 @@ public async Task GetNextEntryAsync_Cancel() { CancellationTokenSource cs = new CancellationTokenSource(); cs.Cancel(); - MemoryStream archiveStream = new MemoryStream(); - await using (archiveStream) + await using (MemoryStream archiveStream = new MemoryStream()) + await using (TarReader reader = new TarReader(archiveStream, leaveOpen: false)) { - TarReader reader = new TarReader(archiveStream, leaveOpen: false); - await using (reader) - { - await Assert.ThrowsAsync(async () => await reader.GetNextEntryAsync(copyData: false, cs.Token)); - } + await Assert.ThrowsAsync(async () => await reader.GetNextEntryAsync(copyData: false, cs.Token)); } } @@ -30,41 +26,41 @@ public async Task GetNextEntryAsync_Cancel() [Fact] public async Task MalformedArchive_TooSmall_Async() { - using MemoryStream malformed = new MemoryStream(); - byte[] buffer = new byte[] { 0x1 }; - malformed.Write(buffer); - malformed.Seek(0, SeekOrigin.Begin); - - TarReader reader = new TarReader(malformed); - await using (reader) + await using (MemoryStream malformed = new MemoryStream()) { - await Assert.ThrowsAsync(async () => await reader.GetNextEntryAsync()); + byte[] buffer = new byte[] { 0x1 }; + malformed.Write(buffer); + malformed.Seek(0, SeekOrigin.Begin); + + await using (TarReader reader = new TarReader(malformed)) + { + await Assert.ThrowsAsync(async () => await reader.GetNextEntryAsync()); + } } } [Fact] public async Task MalformedArchive_HeaderSize_Async() { - using MemoryStream malformed = new MemoryStream(); - byte[] buffer = new byte[512]; // Minimum length of any header - Array.Fill(buffer, 0x1); - malformed.Write(buffer); - malformed.Seek(0, SeekOrigin.Begin); - - TarReader reader = new TarReader(malformed); - await using (reader) + await using (MemoryStream malformed = new MemoryStream()) { - await Assert.ThrowsAsync(async () => await reader.GetNextEntryAsync()); + byte[] buffer = new byte[512]; // Minimum length of any header + Array.Fill(buffer, 0x1); + malformed.Write(buffer); + malformed.Seek(0, SeekOrigin.Begin); + + await using (TarReader reader = new TarReader(malformed)) + { + await Assert.ThrowsAsync(async () => await reader.GetNextEntryAsync()); + } } } [Fact] public async Task EmptyArchive_Async() { - using MemoryStream empty = new MemoryStream(); - - TarReader reader = new TarReader(empty); - await using (reader) + await using (MemoryStream empty = new MemoryStream()) + await using (TarReader reader = new TarReader(empty)) { Assert.Null(await reader.GetNextEntryAsync()); } @@ -74,28 +70,27 @@ public async Task EmptyArchive_Async() [Fact] public async Task LongEndMarkers_DoNotAdvanceStream_Async() { - using MemoryStream archive = new MemoryStream(); - - TarWriter writer = new TarWriter(archive, TarEntryFormat.Ustar, leaveOpen: true); - await using (writer) + await using (MemoryStream archive = new MemoryStream()) { - UstarTarEntry entry = new UstarTarEntry(TarEntryType.Directory, "dir"); - await writer.WriteEntryAsync(entry); - } + await using (TarWriter writer = new TarWriter(archive, TarEntryFormat.Ustar, leaveOpen: true)) + { + UstarTarEntry entry = new UstarTarEntry(TarEntryType.Directory, "dir"); + await writer.WriteEntryAsync(entry); + } - byte[] buffer = new byte[2048]; // Four additional end markers (512 each) - Array.Fill(buffer, 0x0); - archive.Write(buffer); - archive.Seek(0, SeekOrigin.Begin); + byte[] buffer = new byte[2048]; // Four additional end markers (512 each) + Array.Fill(buffer, 0x0); + archive.Write(buffer); + archive.Seek(0, SeekOrigin.Begin); - TarReader reader = new TarReader(archive); - await using (reader) - { - Assert.NotNull(await reader.GetNextEntryAsync()); - Assert.Null(await reader.GetNextEntryAsync()); - long expectedPosition = archive.Position; // After reading the first null entry, should not advance more - Assert.Null(await reader.GetNextEntryAsync()); - Assert.Equal(expectedPosition, archive.Position); + await using (TarReader reader = new TarReader(archive)) + { + Assert.NotNull(await reader.GetNextEntryAsync()); + Assert.Null(await reader.GetNextEntryAsync()); + long expectedPosition = archive.Position; // After reading the first null entry, should not advance more + Assert.Null(await reader.GetNextEntryAsync()); + Assert.Equal(expectedPosition, archive.Position); + } } } @@ -103,141 +98,142 @@ public async Task LongEndMarkers_DoNotAdvanceStream_Async() public async Task GetNextEntry_CopyDataTrue_SeekableArchive_Async() { string expectedText = "Hello world!"; - MemoryStream archive = new MemoryStream(); - TarWriter writer = new TarWriter(archive, TarEntryFormat.Ustar, leaveOpen: true); - await using (writer) + await using (MemoryStream archive = new MemoryStream()) { - UstarTarEntry entry1 = new UstarTarEntry(TarEntryType.RegularFile, "file.txt"); - entry1.DataStream = new MemoryStream(); - using (StreamWriter streamWriter = new StreamWriter(entry1.DataStream, leaveOpen: true)) + await using (TarWriter writer = new TarWriter(archive, TarEntryFormat.Ustar, leaveOpen: true)) { - streamWriter.WriteLine(expectedText); + UstarTarEntry entry1 = new UstarTarEntry(TarEntryType.RegularFile, "file.txt"); + entry1.DataStream = new MemoryStream(); + using (StreamWriter streamWriter = new StreamWriter(entry1.DataStream, leaveOpen: true)) + { + streamWriter.WriteLine(expectedText); + } + 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); } - 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); - } + archive.Seek(0, SeekOrigin.Begin); - archive.Seek(0, SeekOrigin.Begin); + UstarTarEntry entry; + await using (TarReader reader = new TarReader(archive)) // Seekable + { + entry = await reader.GetNextEntryAsync(copyData: true) as UstarTarEntry; + Assert.NotNull(entry); + Assert.Equal(TarEntryType.RegularFile, entry.EntryType); - UstarTarEntry entry; - TarReader reader = new TarReader(archive); // Seekable - await using (reader) - { - entry = await reader.GetNextEntryAsync(copyData: true) as UstarTarEntry; - Assert.NotNull(entry); - Assert.Equal(TarEntryType.RegularFile, entry.EntryType); + // Force reading the next entry to advance the underlying stream position + Assert.NotNull(await reader.GetNextEntryAsync()); + Assert.Null(await reader.GetNextEntryAsync()); - // Force reading the next entry to advance the underlying stream position - Assert.NotNull(await reader.GetNextEntryAsync()); - Assert.Null(await reader.GetNextEntryAsync()); + entry.DataStream.Seek(0, SeekOrigin.Begin); // Should not throw: This is a new stream, not the archive's disposed stream + using (StreamReader streamReader = new StreamReader(entry.DataStream)) + { + string actualText = streamReader.ReadLine(); + Assert.Equal(expectedText, actualText); + } - entry.DataStream.Seek(0, SeekOrigin.Begin); // Should not throw: This is a new stream, not the archive's disposed stream - using (StreamReader streamReader = new StreamReader(entry.DataStream)) - { - string actualText = streamReader.ReadLine(); - Assert.Equal(expectedText, actualText); } + // 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])); } - - // 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])); } [Fact] public async Task GetNextEntry_CopyDataTrue_UnseekableArchive_Async() { string expectedText = "Hello world!"; - MemoryStream archive = new MemoryStream(); - TarWriter writer = new TarWriter(archive, TarEntryFormat.Ustar, leaveOpen: true); - await using (writer) + await using (MemoryStream archive = new MemoryStream()) { - UstarTarEntry entry1 = new UstarTarEntry(TarEntryType.RegularFile, "file.txt"); - entry1.DataStream = new MemoryStream(); - using (StreamWriter streamWriter = new StreamWriter(entry1.DataStream, leaveOpen: true)) + await using (TarWriter writer = new TarWriter(archive, TarEntryFormat.Ustar, leaveOpen: true)) { - streamWriter.WriteLine(expectedText); + UstarTarEntry entry1 = new UstarTarEntry(TarEntryType.RegularFile, "file.txt"); + entry1.DataStream = new MemoryStream(); + using (StreamWriter streamWriter = new StreamWriter(entry1.DataStream, leaveOpen: true)) + { + streamWriter.WriteLine(expectedText); + } + entry1.DataStream.Seek(0, SeekOrigin.Begin); + await writer.WriteEntryAsync(entry1); + + UstarTarEntry entry2 = new UstarTarEntry(TarEntryType.Directory, "dir"); + await writer.WriteEntryAsync(entry2); } - entry1.DataStream.Seek(0, SeekOrigin.Begin); - await writer.WriteEntryAsync(entry1); - UstarTarEntry entry2 = new UstarTarEntry(TarEntryType.Directory, "dir"); - await writer.WriteEntryAsync(entry2); - } - - archive.Seek(0, SeekOrigin.Begin); - using WrappedStream wrapped = new WrappedStream(archive, canRead: true, canWrite: false, canSeek: false); - - UstarTarEntry entry; - TarReader reader = new TarReader(wrapped, leaveOpen: true); // Unseekable - await using (reader) - { - entry = await reader.GetNextEntryAsync(copyData: true) as UstarTarEntry; - Assert.NotNull(entry); - Assert.Equal(TarEntryType.RegularFile, entry.EntryType); - - // Force reading the next entry to advance the underlying stream position - Assert.NotNull(await reader.GetNextEntryAsync()); - Assert.Null(await reader.GetNextEntryAsync()); - - Assert.NotNull(entry.DataStream); - entry.DataStream.Seek(0, SeekOrigin.Begin); // Should not throw: This is a new stream, not the archive's disposed stream - using (StreamReader streamReader = new StreamReader(entry.DataStream)) + archive.Seek(0, SeekOrigin.Begin); + await using (WrappedStream wrapped = new WrappedStream(archive, canRead: true, canWrite: false, canSeek: false)) { - string actualText = streamReader.ReadLine(); - Assert.Equal(expectedText, actualText); + UstarTarEntry entry; + await using (TarReader reader = new TarReader(wrapped, leaveOpen: true)) // Unseekable + { + entry = await reader.GetNextEntryAsync(copyData: true) as UstarTarEntry; + Assert.NotNull(entry); + Assert.Equal(TarEntryType.RegularFile, entry.EntryType); + + // Force reading the next entry to advance the underlying stream position + Assert.NotNull(await reader.GetNextEntryAsync()); + Assert.Null(await reader.GetNextEntryAsync()); + + Assert.NotNull(entry.DataStream); + entry.DataStream.Seek(0, SeekOrigin.Begin); // Should not throw: This is a new stream, not the archive's disposed stream + using (StreamReader streamReader = new StreamReader(entry.DataStream)) + { + string actualText = streamReader.ReadLine(); + Assert.Equal(expectedText, actualText); + } + } + // 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])); } - } - - // 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])); } [Fact] public async Task GetNextEntry_CopyDataFalse_UnseekableArchive_Exceptions_Async() { - MemoryStream archive = new MemoryStream(); - TarWriter writer = new TarWriter(archive, TarEntryFormat.Ustar, leaveOpen: true); - await using (writer) + await using (MemoryStream archive = new MemoryStream()) { - UstarTarEntry entry1 = new UstarTarEntry(TarEntryType.RegularFile, "file.txt"); - entry1.DataStream = new MemoryStream(); - using (StreamWriter streamWriter = new StreamWriter(entry1.DataStream, leaveOpen: true)) + await using (TarWriter writer = new TarWriter(archive, TarEntryFormat.Ustar, leaveOpen: true)) { - streamWriter.WriteLine("Hello world!"); + 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); } - 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); - } - archive.Seek(0, SeekOrigin.Begin); - using WrappedStream wrapped = new WrappedStream(archive, canRead: true, canWrite: false, canSeek: false); - UstarTarEntry entry; - TarReader reader = new TarReader(wrapped); // Unseekable - await using (reader) - { - 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()); - - // 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)) + { + 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()); + + // This is not possible because the position of the main stream is already past the data + Assert.Throws(() => entry.DataStream.Read(new byte[1])); + } + + // 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])); + } } - - // 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])); } [Theory] @@ -245,49 +241,51 @@ public async Task GetNextEntry_CopyDataFalse_UnseekableArchive_Exceptions_Async( [InlineData(true)] public async Task GetNextEntry_UnseekableArchive_ReplaceDataStream_ExcludeFromDisposing_Async(bool copyData) { - MemoryStream archive = new MemoryStream(); - TarWriter writer = new TarWriter(archive, TarEntryFormat.Ustar, leaveOpen: true); - await using (writer) + await using (MemoryStream archive = new MemoryStream()) { - UstarTarEntry entry1 = new UstarTarEntry(TarEntryType.RegularFile, "file.txt"); - entry1.DataStream = new MemoryStream(); - using (StreamWriter streamWriter = new StreamWriter(entry1.DataStream, leaveOpen: true)) + await using (TarWriter writer = new TarWriter(archive, TarEntryFormat.Ustar, leaveOpen: true)) { - streamWriter.WriteLine("Hello world!"); + 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); } - 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); - } - archive.Seek(0, SeekOrigin.Begin); - using WrappedStream wrapped = new WrappedStream(archive, canRead: true, canWrite: false, canSeek: false); - UstarTarEntry entry; - Stream oldStream; - TarReader reader = new TarReader(wrapped); // Unseekable - await using (reader) - { - entry = await reader.GetNextEntryAsync(copyData) as UstarTarEntry; - Assert.NotNull(entry); - Assert.Equal(TarEntryType.RegularFile, entry.EntryType); - - oldStream = entry.DataStream; - - entry.DataStream = new MemoryStream(); // Substitution, setter should dispose the previous stream - using(StreamWriter streamWriter = new StreamWriter(entry.DataStream, leaveOpen: true)) + archive.Seek(0, SeekOrigin.Begin); + await using (WrappedStream wrapped = new WrappedStream(archive, canRead: true, canWrite: false, canSeek: false)) { - streamWriter.WriteLine("Substituted"); + UstarTarEntry entry; + Stream oldStream; + await using (TarReader reader = new TarReader(wrapped)) // Unseekable + { + entry = await reader.GetNextEntryAsync(copyData) as UstarTarEntry; + Assert.NotNull(entry); + Assert.Equal(TarEntryType.RegularFile, entry.EntryType); + + oldStream = entry.DataStream; + + entry.DataStream = new MemoryStream(); // Substitution, setter should dispose the previous stream + using (StreamWriter streamWriter = new StreamWriter(entry.DataStream, leaveOpen: true)) + { + streamWriter.WriteLine("Substituted"); + } + } // Disposing reader should not dispose the substituted DataStream + + Assert.Throws(() => oldStream.Read(new byte[1])); + + entry.DataStream.Seek(0, SeekOrigin.Begin); + using (StreamReader streamReader = new StreamReader(entry.DataStream)) + { + Assert.Equal("Substituted", streamReader.ReadLine()); + } } - } // Disposing reader should not dispose the substituted DataStream - - Assert.Throws(() => oldStream.Read(new byte[1])); - - entry.DataStream.Seek(0, SeekOrigin.Begin); - using (StreamReader streamReader = new StreamReader(entry.DataStream)) - { - Assert.Equal("Substituted", streamReader.ReadLine()); } } } diff --git a/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.TarEntry.ExtractToFileAsync.Tests.Unix.cs b/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.TarEntry.ExtractToFileAsync.Tests.Unix.cs index 9f84a31625077a..a42f9b74e5a05b 100644 --- a/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.TarEntry.ExtractToFileAsync.Tests.Unix.cs +++ b/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.TarEntry.ExtractToFileAsync.Tests.Unix.cs @@ -13,11 +13,9 @@ public partial class TarReader_TarEntry_ExtractToFileAsync_Tests : TarTestsBase [Fact] public async Task SpecialFile_Unelevated_Throws_Async() { - using TempDirectory root = new TempDirectory(); - using MemoryStream ms = GetTarMemoryStream(CompressionMethod.Uncompressed, TestTarFormat.ustar, "specialfiles"); - - TarReader reader = new TarReader(ms); - await using (reader) + using (TempDirectory root = new TempDirectory()) + await using (MemoryStream ms = GetTarMemoryStream(CompressionMethod.Uncompressed, TestTarFormat.ustar, "specialfiles")) + await using (TarReader reader = new TarReader(ms)) { string path = Path.Join(root.Path, "output"); diff --git a/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.TarEntry.ExtractToFileAsync.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.TarEntry.ExtractToFileAsync.Tests.cs index 154f10446df24f..fc130b4d5d9455 100644 --- a/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.TarEntry.ExtractToFileAsync.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.TarEntry.ExtractToFileAsync.Tests.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.IO; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; using Xunit; namespace System.Formats.Tar.Tests @@ -9,16 +11,15 @@ namespace System.Formats.Tar.Tests public partial class TarReader_ExtractToFileAsync_Tests : TarTestsBase { [Fact] - public void ExtractEntriesWithSlashDotPrefix() + public async Task ExtractEntriesWithSlashDotPrefix_Async() { - using TempDirectory root = new TempDirectory(); - - using MemoryStream archiveStream = GetStrangeTarMemoryStream("prefixDotSlashAndCurrentFolderEntry"); - using (TarReader reader = new TarReader(archiveStream, leaveOpen: false)) + using (TempDirectory root = new TempDirectory()) + await using (MemoryStream archiveStream = GetStrangeTarMemoryStream("prefixDotSlashAndCurrentFolderEntry")) + await using (TarReader reader = new TarReader(archiveStream, leaveOpen: false)) { string rootPath = Path.TrimEndingDirectorySeparator(root.Path); TarEntry entry; - while ((entry = reader.GetNextEntry()) != null) + while ((entry = await reader.GetNextEntryAsync()) != null) { Assert.NotNull(entry); Assert.StartsWith("./", entry.Name); @@ -27,7 +28,7 @@ public void ExtractEntriesWithSlashDotPrefix() string entryPath = Path.TrimEndingDirectorySeparator(Path.GetFullPath(Path.Join(rootPath, entry.Name))); if (entryPath != rootPath) { - entry.ExtractToFile(entryPath, overwrite: true); + await entry.ExtractToFileAsync(entryPath, overwrite: true); Assert.True(Path.Exists(entryPath), $"Entry was not extracted: {entryPath}"); } } diff --git a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.Async.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.Async.Tests.cs index 87f2c058166d5f..cf40f0f24ab77b 100644 --- a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.Async.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.Async.Tests.cs @@ -13,99 +13,103 @@ public class TarWriter_Async_Tests : TarTestsBase [Fact] public async Task Constructors_LeaveOpen_Async() { - using MemoryStream archiveStream = new MemoryStream(); - - TarWriter writer1 = new TarWriter(archiveStream, TarEntryFormat.Pax, leaveOpen: true); - await writer1.DisposeAsync(); - archiveStream.WriteByte(0); // Should succeed because stream was not closed + await using (MemoryStream archiveStream = new MemoryStream()) + { + TarWriter writer1 = new TarWriter(archiveStream, TarEntryFormat.Pax, leaveOpen: true); + await writer1.DisposeAsync(); + archiveStream.WriteByte(0); // Should succeed because stream was not closed - TarWriter writer2 = new TarWriter(archiveStream, TarEntryFormat.Pax, leaveOpen: false); - await writer2.DisposeAsync(); - Assert.Throws(() => archiveStream.WriteByte(0)); // Should fail because stream was closed + TarWriter writer2 = new TarWriter(archiveStream, TarEntryFormat.Pax, leaveOpen: false); + await writer2.DisposeAsync(); + Assert.Throws(() => archiveStream.WriteByte(0)); // Should fail because stream was closed + } } [Fact] public async Task Constructor_NoEntryInsertion_WritesNothing_Async() { - using MemoryStream archiveStream = new MemoryStream(); - TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Pax, leaveOpen: true); - await writer.DisposeAsync(); // No entries inserted, should write no empty records - Assert.Equal(0, archiveStream.Length); + await using (MemoryStream archiveStream = new MemoryStream()) + { + TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Pax, leaveOpen: true); + await writer.DisposeAsync(); // No entries inserted, should write no empty records + Assert.Equal(0, archiveStream.Length); + } } [Fact] public async void Write_To_UnseekableStream_Async() { - using MemoryStream inner = new MemoryStream(); - using WrappedStream wrapped = new WrappedStream(inner, canRead: true, canWrite: true, canSeek: false); - - TarWriter writer = new TarWriter(wrapped, TarEntryFormat.Pax, leaveOpen: true); - await using (writer) + await using (MemoryStream inner = new MemoryStream()) { - PaxTarEntry paxEntry = new PaxTarEntry(TarEntryType.RegularFile, "file.txt"); - await writer.WriteEntryAsync(paxEntry); - } // The final records should get written, and the length should not be set because position cannot be read - - inner.Seek(0, SeekOrigin.Begin); // Rewind the base stream (wrapped cannot be rewound) - - TarReader reader = new TarReader(wrapped); - await using (reader) - { - TarEntry entry = await reader.GetNextEntryAsync(); - Assert.Equal(TarEntryFormat.Pax, entry.Format); - Assert.Equal(TarEntryType.RegularFile, entry.EntryType); - Assert.Null(await reader.GetNextEntryAsync()); + await using (WrappedStream wrapped = new WrappedStream(inner, canRead: true, canWrite: true, canSeek: false)) + { + await using (TarWriter writer = new TarWriter(wrapped, TarEntryFormat.Pax, leaveOpen: true)) + { + PaxTarEntry paxEntry = new PaxTarEntry(TarEntryType.RegularFile, "file.txt"); + await writer.WriteEntryAsync(paxEntry); + } // The final records should get written, and the length should not be set because position cannot be read + + inner.Seek(0, SeekOrigin.Begin); // Rewind the base stream (wrapped cannot be rewound) + + await using (TarReader reader = new TarReader(wrapped)) + { + TarEntry entry = await reader.GetNextEntryAsync(); + Assert.Equal(TarEntryFormat.Pax, entry.Format); + Assert.Equal(TarEntryType.RegularFile, entry.EntryType); + Assert.Null(await reader.GetNextEntryAsync()); + } + } } } [Fact] public async Task VerifyChecksumV7_Async() { - using MemoryStream archive = new MemoryStream(); - TarWriter writer = new TarWriter(archive, TarEntryFormat.V7, leaveOpen: true); - await using (writer) + await using (MemoryStream archive = new MemoryStream()) { - V7TarEntry entry = new V7TarEntry( - // '\0' = 0 - TarEntryType.V7RegularFile, - // 'a.b' = 97 + 46 + 98 = 241 - entryName: "a.b"); + await using (TarWriter writer = new TarWriter(archive, TarEntryFormat.V7, leaveOpen: true)) + { + V7TarEntry entry = new V7TarEntry( + // '\0' = 0 + TarEntryType.V7RegularFile, + // 'a.b' = 97 + 46 + 98 = 241 + entryName: "a.b"); - // '0000744\0' = 48 + 48 + 48 + 48 + 55 + 52 + 52 + 0 = 351 - entry.Mode = AssetMode; // octal 744 = u+rxw, g+r, o+r + // '0000744\0' = 48 + 48 + 48 + 48 + 55 + 52 + 52 + 0 = 351 + entry.Mode = AssetMode; // octal 744 = u+rxw, g+r, o+r - // '0017351\0' = 48 + 48 + 49 + 55 + 51 + 53 + 49 + 0 = 353 - entry.Uid = AssetUid; // decimal 7913, octal 17351 + // '0017351\0' = 48 + 48 + 49 + 55 + 51 + 53 + 49 + 0 = 353 + entry.Uid = AssetUid; // decimal 7913, octal 17351 - // '0006773\0' = 48 + 48 + 48 + 54 + 55 + 55 + 51 + 0 = 359 - entry.Gid = AssetGid; // decimal 3579, octal 6773 + // '0006773\0' = 48 + 48 + 48 + 54 + 55 + 55 + 51 + 0 = 359 + entry.Gid = AssetGid; // decimal 3579, octal 6773 - // '14164217674\0' = 49 + 52 + 49 + 54 + 52 + 50 + 49 + 55 + 54 + 55 + 52 + 0 = 571 - DateTimeOffset mtime = new DateTimeOffset(2022, 1, 2, 3, 45, 00, TimeSpan.Zero); // ToUnixTimeSeconds() = decimal 1641095100, octal 14164217674 - entry.ModificationTime = mtime; + // '14164217674\0' = 49 + 52 + 49 + 54 + 52 + 50 + 49 + 55 + 54 + 55 + 52 + 0 = 571 + DateTimeOffset mtime = new DateTimeOffset(2022, 1, 2, 3, 45, 00, TimeSpan.Zero); // ToUnixTimeSeconds() = decimal 1641095100, octal 14164217674 + entry.ModificationTime = mtime; - entry.DataStream = new MemoryStream(); - byte[] buffer = new byte[] { 72, 101, 108, 108, 111 }; + entry.DataStream = new MemoryStream(); + byte[] buffer = new byte[] { 72, 101, 108, 108, 111 }; - // '0000000005\0' = 48 + 48 + 48 + 48 + 48 + 48 + 48 + 48 + 48 + 48 + 53 + 0 = 533 - await entry.DataStream.WriteAsync(buffer); // Data length: decimal 5 - entry.DataStream.Seek(0, SeekOrigin.Begin); // Rewind to ensure it gets written from the beginning + // '0000000005\0' = 48 + 48 + 48 + 48 + 48 + 48 + 48 + 48 + 48 + 48 + 53 + 0 = 533 + await entry.DataStream.WriteAsync(buffer); // Data length: decimal 5 + entry.DataStream.Seek(0, SeekOrigin.Begin); // Rewind to ensure it gets written from the beginning - // Sum so far: 0 + 241 + 351 + 353 + 359 + 571 + 533 = decimal 2408 - // Add 8 spaces to the sum: 2408 + (8 x 32) = octal 5150, decimal 2664 (final) - // Checksum: '005150\0 ' + // Sum so far: 0 + 241 + 351 + 353 + 359 + 571 + 533 = decimal 2408 + // Add 8 spaces to the sum: 2408 + (8 x 32) = octal 5150, decimal 2664 (final) + // Checksum: '005150\0 ' - await writer.WriteEntryAsync(entry); + await writer.WriteEntryAsync(entry); - Assert.Equal(2664, entry.Checksum); - } + Assert.Equal(2664, entry.Checksum); + } - archive.Seek(0, SeekOrigin.Begin); - TarReader reader = new TarReader(archive); - await using (reader) - { - TarEntry entry = await reader.GetNextEntryAsync(); - Assert.Equal(2664, entry.Checksum); + archive.Seek(0, SeekOrigin.Begin); + await using (TarReader reader = new TarReader(archive)) + { + TarEntry entry = await reader.GetNextEntryAsync(); + Assert.Equal(2664, entry.Checksum); + } } } } diff --git a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.Entry.Gnu.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.Entry.Gnu.Tests.cs index 3cd2b5e538b6ee..b33e2bf7add22a 100644 --- a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.Entry.Gnu.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.Entry.Gnu.Tests.cs @@ -17,154 +17,154 @@ public Task WriteEntry_Null_Throws_Async() => [Fact] public async Task WriteRegularFile_Async() { - using MemoryStream archiveStream = new MemoryStream(); - TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Gnu, leaveOpen: true); - await using (writer) + await using (MemoryStream archiveStream = new MemoryStream()) { - GnuTarEntry regularFile = new GnuTarEntry(TarEntryType.RegularFile, InitialEntryName); - SetRegularFile(regularFile); - VerifyRegularFile(regularFile, isWritable: true); - await writer.WriteEntryAsync(regularFile); - } + await using (TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Gnu, leaveOpen: true)) + { + GnuTarEntry regularFile = new GnuTarEntry(TarEntryType.RegularFile, InitialEntryName); + SetRegularFile(regularFile); + VerifyRegularFile(regularFile, isWritable: true); + await writer.WriteEntryAsync(regularFile); + } - archiveStream.Position = 0; - TarReader reader = new TarReader(archiveStream); - await using (reader) - { - GnuTarEntry regularFile = await reader.GetNextEntryAsync() as GnuTarEntry; - VerifyRegularFile(regularFile, isWritable: false); + archiveStream.Position = 0; + await using (TarReader reader = new TarReader(archiveStream)) + { + GnuTarEntry regularFile = await reader.GetNextEntryAsync() as GnuTarEntry; + VerifyRegularFile(regularFile, isWritable: false); + } } } [Fact] public async Task WriteHardLink_Async() { - using MemoryStream archiveStream = new MemoryStream(); - TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Gnu, leaveOpen: true); - await using (writer) + await using (MemoryStream archiveStream = new MemoryStream()) { - GnuTarEntry hardLink = new GnuTarEntry(TarEntryType.HardLink, InitialEntryName); - SetHardLink(hardLink); - VerifyHardLink(hardLink); - await writer.WriteEntryAsync(hardLink); - } + await using (TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Gnu, leaveOpen: true)) + { + GnuTarEntry hardLink = new GnuTarEntry(TarEntryType.HardLink, InitialEntryName); + SetHardLink(hardLink); + VerifyHardLink(hardLink); + await writer.WriteEntryAsync(hardLink); + } - archiveStream.Position = 0; - TarReader reader = new TarReader(archiveStream); - await using (reader) - { - GnuTarEntry hardLink = await reader.GetNextEntryAsync() as GnuTarEntry; - VerifyHardLink(hardLink); + archiveStream.Position = 0; + await using (TarReader reader = new TarReader(archiveStream)) + { + GnuTarEntry hardLink = await reader.GetNextEntryAsync() as GnuTarEntry; + VerifyHardLink(hardLink); + } } } [Fact] public async Task WriteSymbolicLink_Async() { - using MemoryStream archiveStream = new MemoryStream(); - TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Gnu, leaveOpen: true); - await using (writer) + await using (MemoryStream archiveStream = new MemoryStream()) { - GnuTarEntry symbolicLink = new GnuTarEntry(TarEntryType.SymbolicLink, InitialEntryName); - SetSymbolicLink(symbolicLink); - VerifySymbolicLink(symbolicLink); - await writer.WriteEntryAsync(symbolicLink); - } + await using (TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Gnu, leaveOpen: true)) + { + GnuTarEntry symbolicLink = new GnuTarEntry(TarEntryType.SymbolicLink, InitialEntryName); + SetSymbolicLink(symbolicLink); + VerifySymbolicLink(symbolicLink); + await writer.WriteEntryAsync(symbolicLink); + } - archiveStream.Position = 0; - TarReader reader = new TarReader(archiveStream); - await using (reader) - { - GnuTarEntry symbolicLink = await reader.GetNextEntryAsync() as GnuTarEntry; - VerifySymbolicLink(symbolicLink); + archiveStream.Position = 0; + await using (TarReader reader = new TarReader(archiveStream)) + { + GnuTarEntry symbolicLink = await reader.GetNextEntryAsync() as GnuTarEntry; + VerifySymbolicLink(symbolicLink); + } } } [Fact] public async Task WriteDirectory_Async() { - using MemoryStream archiveStream = new MemoryStream(); - TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Gnu, leaveOpen: true); - await using (writer) + using (MemoryStream archiveStream = new MemoryStream()) { - GnuTarEntry directory = new GnuTarEntry(TarEntryType.Directory, InitialEntryName); - SetDirectory(directory); - VerifyDirectory(directory); - await writer.WriteEntryAsync(directory); - } + await using (TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Gnu, leaveOpen: true)) + { + GnuTarEntry directory = new GnuTarEntry(TarEntryType.Directory, InitialEntryName); + SetDirectory(directory); + VerifyDirectory(directory); + await writer.WriteEntryAsync(directory); + } - archiveStream.Position = 0; - TarReader reader = new TarReader(archiveStream); - await using (reader) - { - GnuTarEntry directory = await reader.GetNextEntryAsync() as GnuTarEntry; - VerifyDirectory(directory); + archiveStream.Position = 0; + await using (TarReader reader = new TarReader(archiveStream)) + { + GnuTarEntry directory = await reader.GetNextEntryAsync() as GnuTarEntry; + VerifyDirectory(directory); + } } } [Fact] public async Task WriteCharacterDevice_Async() { - using MemoryStream archiveStream = new MemoryStream(); - TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Gnu, leaveOpen: true); - await using (writer) + await using (MemoryStream archiveStream = new MemoryStream()) { - GnuTarEntry charDevice = new GnuTarEntry(TarEntryType.CharacterDevice, InitialEntryName); - SetCharacterDevice(charDevice); - VerifyCharacterDevice(charDevice); - await writer.WriteEntryAsync(charDevice); - } + await using (TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Gnu, leaveOpen: true)) + { + GnuTarEntry charDevice = new GnuTarEntry(TarEntryType.CharacterDevice, InitialEntryName); + SetCharacterDevice(charDevice); + VerifyCharacterDevice(charDevice); + await writer.WriteEntryAsync(charDevice); + } - archiveStream.Position = 0; - TarReader reader = new TarReader(archiveStream); - await using (reader) - { - GnuTarEntry charDevice = await reader.GetNextEntryAsync() as GnuTarEntry; - VerifyCharacterDevice(charDevice); + archiveStream.Position = 0; + await using (TarReader reader = new TarReader(archiveStream)) + { + GnuTarEntry charDevice = await reader.GetNextEntryAsync() as GnuTarEntry; + VerifyCharacterDevice(charDevice); + } } } [Fact] public async Task WriteBlockDevice_Async() { - using MemoryStream archiveStream = new MemoryStream(); - TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Gnu, leaveOpen: true); - await using (writer) + await using (MemoryStream archiveStream = new MemoryStream()) { - GnuTarEntry blockDevice = new GnuTarEntry(TarEntryType.BlockDevice, InitialEntryName); - SetBlockDevice(blockDevice); - VerifyBlockDevice(blockDevice); - await writer.WriteEntryAsync(blockDevice); - } + await using (TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Gnu, leaveOpen: true)) + { + GnuTarEntry blockDevice = new GnuTarEntry(TarEntryType.BlockDevice, InitialEntryName); + SetBlockDevice(blockDevice); + VerifyBlockDevice(blockDevice); + await writer.WriteEntryAsync(blockDevice); + } - archiveStream.Position = 0; - TarReader reader = new TarReader(archiveStream); - await using (reader) - { - GnuTarEntry blockDevice = await reader.GetNextEntryAsync() as GnuTarEntry; - VerifyBlockDevice(blockDevice); + archiveStream.Position = 0; + await using (TarReader reader = new TarReader(archiveStream)) + { + GnuTarEntry blockDevice = await reader.GetNextEntryAsync() as GnuTarEntry; + VerifyBlockDevice(blockDevice); + } } } [Fact] public async Task WriteFifo_Async() { - using MemoryStream archiveStream = new MemoryStream(); - TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Gnu, leaveOpen: true); - await using (writer) + await using (MemoryStream archiveStream = new MemoryStream()) { - GnuTarEntry fifo = new GnuTarEntry(TarEntryType.Fifo, InitialEntryName); - SetFifo(fifo); - VerifyFifo(fifo); - await writer.WriteEntryAsync(fifo); - } + await using (TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Gnu, leaveOpen: true)) + { + GnuTarEntry fifo = new GnuTarEntry(TarEntryType.Fifo, InitialEntryName); + SetFifo(fifo); + VerifyFifo(fifo); + await writer.WriteEntryAsync(fifo); + } - archiveStream.Position = 0; - TarReader reader = new TarReader(archiveStream); - await using (reader) - { - GnuTarEntry fifo = await reader.GetNextEntryAsync() as GnuTarEntry; - VerifyFifo(fifo); + archiveStream.Position = 0; + await using (TarReader reader = new TarReader(archiveStream)) + { + GnuTarEntry fifo = await reader.GetNextEntryAsync() as GnuTarEntry; + VerifyFifo(fifo); + } } } @@ -178,21 +178,21 @@ public async Task Write_Long_Name_Async(TarEntryType entryType) // Name field in header only fits 100 bytes string longName = new string('a', 101); - using MemoryStream archiveStream = new MemoryStream(); - TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Gnu, leaveOpen: true); - await using (writer) + await using (MemoryStream archiveStream = new MemoryStream()) { - GnuTarEntry entry = new GnuTarEntry(entryType, longName); - await writer.WriteEntryAsync(entry); - } + await using (TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Gnu, leaveOpen: true)) + { + GnuTarEntry entry = new GnuTarEntry(entryType, longName); + await writer.WriteEntryAsync(entry); + } - archiveStream.Position = 0; - TarReader reader = new TarReader(archiveStream); - await using (reader) - { - GnuTarEntry entry = await reader.GetNextEntryAsync() as GnuTarEntry; - Assert.Equal(entryType, entry.EntryType); - Assert.Equal(longName, entry.Name); + archiveStream.Position = 0; + await using (TarReader reader = new TarReader(archiveStream)) + { + GnuTarEntry entry = await reader.GetNextEntryAsync() as GnuTarEntry; + Assert.Equal(entryType, entry.EntryType); + Assert.Equal(longName, entry.Name); + } } } @@ -204,23 +204,23 @@ public async Task Write_LongLinkName_Async(TarEntryType entryType) // LinkName field in header only fits 100 bytes string longLinkName = new string('a', 101); - using MemoryStream archiveStream = new MemoryStream(); - TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Gnu, leaveOpen: true); - await using (writer) + await using (MemoryStream archiveStream = new MemoryStream()) { - GnuTarEntry entry = new GnuTarEntry(entryType, "file.txt"); - entry.LinkName = longLinkName; - await writer.WriteEntryAsync(entry); - } + await using (TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Gnu, leaveOpen: true)) + { + GnuTarEntry entry = new GnuTarEntry(entryType, "file.txt"); + entry.LinkName = longLinkName; + await writer.WriteEntryAsync(entry); + } - archiveStream.Position = 0; - TarReader reader = new TarReader(archiveStream); - await using (reader) - { - GnuTarEntry entry = await reader.GetNextEntryAsync() as GnuTarEntry; - Assert.Equal(entryType, entry.EntryType); - Assert.Equal("file.txt", entry.Name); - Assert.Equal(longLinkName, entry.LinkName); + archiveStream.Position = 0; + await using (TarReader reader = new TarReader(archiveStream)) + { + GnuTarEntry entry = await reader.GetNextEntryAsync() as GnuTarEntry; + Assert.Equal(entryType, entry.EntryType); + Assert.Equal("file.txt", entry.Name); + Assert.Equal(longLinkName, entry.LinkName); + } } } @@ -233,23 +233,23 @@ public async Task Write_LongName_And_LongLinkName_Async(TarEntryType entryType) string longName = new string('a', 101); string longLinkName = new string('a', 101); - using MemoryStream archiveStream = new MemoryStream(); - TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Gnu, leaveOpen: true); - await using (writer) + await using (MemoryStream archiveStream = new MemoryStream()) { - GnuTarEntry entry = new GnuTarEntry(entryType, longName); - entry.LinkName = longLinkName; - await writer.WriteEntryAsync(entry); - } + await using (TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Gnu, leaveOpen: true)) + { + GnuTarEntry entry = new GnuTarEntry(entryType, longName); + entry.LinkName = longLinkName; + await writer.WriteEntryAsync(entry); + } - archiveStream.Position = 0; - TarReader reader = new TarReader(archiveStream); - await using (reader) - { - GnuTarEntry entry = await reader.GetNextEntryAsync() as GnuTarEntry; - Assert.Equal(entryType, entry.EntryType); - Assert.Equal(longName, entry.Name); - Assert.Equal(longLinkName, entry.LinkName); + archiveStream.Position = 0; + await using (TarReader reader = new TarReader(archiveStream)) + { + GnuTarEntry entry = await reader.GetNextEntryAsync() as GnuTarEntry; + Assert.Equal(entryType, entry.EntryType); + Assert.Equal(longName, entry.Name); + Assert.Equal(longLinkName, entry.LinkName); + } } } } diff --git a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.Entry.Pax.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.Entry.Pax.Tests.cs index 8b8cc035c16c41..ca4cdc9f7aac0c 100644 --- a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.Entry.Pax.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.Entry.Pax.Tests.cs @@ -19,154 +19,154 @@ public Task WriteEntry_Null_Throws_Async() => [Fact] public async Task WriteRegularFile_Async() { - using MemoryStream archiveStream = new MemoryStream(); - TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Pax, leaveOpen: true); - await using (writer) - { - PaxTarEntry regularFile = new PaxTarEntry(TarEntryType.RegularFile, InitialEntryName); - SetRegularFile(regularFile); - VerifyRegularFile(regularFile, isWritable: true); - await writer.WriteEntryAsync(regularFile); - } - - archiveStream.Position = 0; - TarReader reader = new TarReader(archiveStream); - await using (reader) - { - PaxTarEntry regularFile = await reader.GetNextEntryAsync() as PaxTarEntry; - VerifyRegularFile(regularFile, isWritable: false); + await using (MemoryStream archiveStream = new MemoryStream()) + { + await using (TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Pax, leaveOpen: true)) + { + PaxTarEntry regularFile = new PaxTarEntry(TarEntryType.RegularFile, InitialEntryName); + SetRegularFile(regularFile); + VerifyRegularFile(regularFile, isWritable: true); + await writer.WriteEntryAsync(regularFile); + } + + archiveStream.Position = 0; + await using (TarReader reader = new TarReader(archiveStream)) + { + PaxTarEntry regularFile = await reader.GetNextEntryAsync() as PaxTarEntry; + VerifyRegularFile(regularFile, isWritable: false); + } } } [Fact] public async Task WriteHardLink_Async() { - using MemoryStream archiveStream = new MemoryStream(); - TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Pax, leaveOpen: true); - await using (writer) - { - PaxTarEntry hardLink = new PaxTarEntry(TarEntryType.HardLink, InitialEntryName); - SetHardLink(hardLink); - VerifyHardLink(hardLink); - await writer.WriteEntryAsync(hardLink); - } - - archiveStream.Position = 0; - TarReader reader = new TarReader(archiveStream); - await using (reader) - { - PaxTarEntry hardLink = await reader.GetNextEntryAsync() as PaxTarEntry; - VerifyHardLink(hardLink); + await using (MemoryStream archiveStream = new MemoryStream()) + { + await using (TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Pax, leaveOpen: true)) + { + PaxTarEntry hardLink = new PaxTarEntry(TarEntryType.HardLink, InitialEntryName); + SetHardLink(hardLink); + VerifyHardLink(hardLink); + await writer.WriteEntryAsync(hardLink); + } + + archiveStream.Position = 0; + await using (TarReader reader = new TarReader(archiveStream)) + { + PaxTarEntry hardLink = await reader.GetNextEntryAsync() as PaxTarEntry; + VerifyHardLink(hardLink); + } } } [Fact] public async Task WriteSymbolicLink_Async() { - using MemoryStream archiveStream = new MemoryStream(); - TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Pax, leaveOpen: true); - await using (writer) - { - PaxTarEntry symbolicLink = new PaxTarEntry(TarEntryType.SymbolicLink, InitialEntryName); - SetSymbolicLink(symbolicLink); - VerifySymbolicLink(symbolicLink); - await writer.WriteEntryAsync(symbolicLink); - } - - archiveStream.Position = 0; - TarReader reader = new TarReader(archiveStream); - await using (reader) - { - PaxTarEntry symbolicLink = await reader.GetNextEntryAsync() as PaxTarEntry; - VerifySymbolicLink(symbolicLink); + await using (MemoryStream archiveStream = new MemoryStream()) + { + await using (TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Pax, leaveOpen: true)) + { + PaxTarEntry symbolicLink = new PaxTarEntry(TarEntryType.SymbolicLink, InitialEntryName); + SetSymbolicLink(symbolicLink); + VerifySymbolicLink(symbolicLink); + await writer.WriteEntryAsync(symbolicLink); + } + + archiveStream.Position = 0; + await using (TarReader reader = new TarReader(archiveStream)) + { + PaxTarEntry symbolicLink = await reader.GetNextEntryAsync() as PaxTarEntry; + VerifySymbolicLink(symbolicLink); + } } } [Fact] public async Task WriteDirectory_Async() { - using MemoryStream archiveStream = new MemoryStream(); - TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Pax, leaveOpen: true); - await using (writer) - { - PaxTarEntry directory = new PaxTarEntry(TarEntryType.Directory, InitialEntryName); - SetDirectory(directory); - VerifyDirectory(directory); - await writer.WriteEntryAsync(directory); - } - - archiveStream.Position = 0; - TarReader reader = new TarReader(archiveStream); - await using (reader) - { - PaxTarEntry directory = await reader.GetNextEntryAsync() as PaxTarEntry; - VerifyDirectory(directory); + await using (MemoryStream archiveStream = new MemoryStream()) + { + await using (TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Pax, leaveOpen: true)) + { + PaxTarEntry directory = new PaxTarEntry(TarEntryType.Directory, InitialEntryName); + SetDirectory(directory); + VerifyDirectory(directory); + await writer.WriteEntryAsync(directory); + } + + archiveStream.Position = 0; + await using (TarReader reader = new TarReader(archiveStream)) + { + PaxTarEntry directory = await reader.GetNextEntryAsync() as PaxTarEntry; + VerifyDirectory(directory); + } } } [Fact] public async Task WriteCharacterDevice_Async() { - using MemoryStream archiveStream = new MemoryStream(); - TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Pax, leaveOpen: true); - await using (writer) - { - PaxTarEntry charDevice = new PaxTarEntry(TarEntryType.CharacterDevice, InitialEntryName); - SetCharacterDevice(charDevice); - VerifyCharacterDevice(charDevice); - await writer.WriteEntryAsync(charDevice); - } - - archiveStream.Position = 0; - TarReader reader = new TarReader(archiveStream); - await using (reader) - { - PaxTarEntry charDevice = await reader.GetNextEntryAsync() as PaxTarEntry; - VerifyCharacterDevice(charDevice); + await using (MemoryStream archiveStream = new MemoryStream()) + { + await using (TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Pax, leaveOpen: true)) + { + PaxTarEntry charDevice = new PaxTarEntry(TarEntryType.CharacterDevice, InitialEntryName); + SetCharacterDevice(charDevice); + VerifyCharacterDevice(charDevice); + await writer.WriteEntryAsync(charDevice); + } + + archiveStream.Position = 0; + await using (TarReader reader = new TarReader(archiveStream)) + { + PaxTarEntry charDevice = await reader.GetNextEntryAsync() as PaxTarEntry; + VerifyCharacterDevice(charDevice); + } } } [Fact] public async Task WriteBlockDevice_Async() { - using MemoryStream archiveStream = new MemoryStream(); - TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Pax, leaveOpen: true); - await using (writer) - { - PaxTarEntry blockDevice = new PaxTarEntry(TarEntryType.BlockDevice, InitialEntryName); - SetBlockDevice(blockDevice); - VerifyBlockDevice(blockDevice); - await writer.WriteEntryAsync(blockDevice); - } - - archiveStream.Position = 0; - TarReader reader = new TarReader(archiveStream); - await using (reader) - { - PaxTarEntry blockDevice = await reader.GetNextEntryAsync() as PaxTarEntry; - VerifyBlockDevice(blockDevice); + await using (MemoryStream archiveStream = new MemoryStream()) + { + await using (TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Pax, leaveOpen: true)) + { + PaxTarEntry blockDevice = new PaxTarEntry(TarEntryType.BlockDevice, InitialEntryName); + SetBlockDevice(blockDevice); + VerifyBlockDevice(blockDevice); + await writer.WriteEntryAsync(blockDevice); + } + + archiveStream.Position = 0; + await using (TarReader reader = new TarReader(archiveStream)) + { + PaxTarEntry blockDevice = await reader.GetNextEntryAsync() as PaxTarEntry; + VerifyBlockDevice(blockDevice); + } } } [Fact] public async Task WriteFifo_Async() { - using MemoryStream archiveStream = new MemoryStream(); - TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Pax, leaveOpen: true); - await using (writer) - { - PaxTarEntry fifo = new PaxTarEntry(TarEntryType.Fifo, InitialEntryName); - SetFifo(fifo); - VerifyFifo(fifo); - await writer.WriteEntryAsync(fifo); - } - - archiveStream.Position = 0; - TarReader reader = new TarReader(archiveStream); - await using (reader) - { - PaxTarEntry fifo = await reader.GetNextEntryAsync() as PaxTarEntry; - VerifyFifo(fifo); + await using (MemoryStream archiveStream = new MemoryStream()) + { + await using (TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Pax, leaveOpen: true)) + { + PaxTarEntry fifo = new PaxTarEntry(TarEntryType.Fifo, InitialEntryName); + SetFifo(fifo); + VerifyFifo(fifo); + await writer.WriteEntryAsync(fifo); + } + + archiveStream.Position = 0; + await using (TarReader reader = new TarReader(archiveStream)) + { + PaxTarEntry fifo = await reader.GetNextEntryAsync() as PaxTarEntry; + VerifyFifo(fifo); + } } } @@ -179,35 +179,35 @@ public async Task WritePaxAttributes_CustomAttribute_Async() Dictionary extendedAttributes = new(); extendedAttributes.Add(expectedKey, expectedValue); - using MemoryStream archiveStream = new MemoryStream(); - TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Pax, leaveOpen: true); - await using (writer) + await using (MemoryStream archiveStream = new MemoryStream()) { - PaxTarEntry regularFile = new PaxTarEntry(TarEntryType.RegularFile, InitialEntryName, extendedAttributes); - SetRegularFile(regularFile); - VerifyRegularFile(regularFile, isWritable: true); - await writer.WriteEntryAsync(regularFile); - } + await using (TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Pax, leaveOpen: true)) + { + PaxTarEntry regularFile = new PaxTarEntry(TarEntryType.RegularFile, InitialEntryName, extendedAttributes); + SetRegularFile(regularFile); + VerifyRegularFile(regularFile, isWritable: true); + await writer.WriteEntryAsync(regularFile); + } - archiveStream.Position = 0; - TarReader reader = new TarReader(archiveStream); - await using (reader) - { - PaxTarEntry regularFile = await reader.GetNextEntryAsync() as PaxTarEntry; - VerifyRegularFile(regularFile, isWritable: false); + archiveStream.Position = 0; + await using (TarReader reader = new TarReader(archiveStream)) + { + PaxTarEntry regularFile = await reader.GetNextEntryAsync() as PaxTarEntry; + VerifyRegularFile(regularFile, isWritable: false); - Assert.NotNull(regularFile.ExtendedAttributes); + Assert.NotNull(regularFile.ExtendedAttributes); - // path, mtime, atime and ctime are always collected by default - AssertExtensions.GreaterThanOrEqualTo(regularFile.ExtendedAttributes.Count, 5); + // path, mtime, atime and ctime are always collected by default + AssertExtensions.GreaterThanOrEqualTo(regularFile.ExtendedAttributes.Count, 5); - Assert.Contains(PaxEaName, regularFile.ExtendedAttributes); - Assert.Contains(PaxEaMTime, regularFile.ExtendedAttributes); - Assert.Contains(PaxEaATime, regularFile.ExtendedAttributes); - Assert.Contains(PaxEaCTime, regularFile.ExtendedAttributes); + Assert.Contains(PaxEaName, regularFile.ExtendedAttributes); + Assert.Contains(PaxEaMTime, regularFile.ExtendedAttributes); + Assert.Contains(PaxEaATime, regularFile.ExtendedAttributes); + Assert.Contains(PaxEaCTime, regularFile.ExtendedAttributes); - Assert.Contains(expectedKey, regularFile.ExtendedAttributes); - Assert.Equal(expectedValue, regularFile.ExtendedAttributes[expectedKey]); + Assert.Contains(expectedKey, regularFile.ExtendedAttributes); + Assert.Equal(expectedValue, regularFile.ExtendedAttributes[expectedKey]); + } } } @@ -215,24 +215,24 @@ public async Task WritePaxAttributes_CustomAttribute_Async() public async Task WritePaxAttributes_Timestamps_AutomaticallyAdded_Async() { DateTimeOffset minimumTime = DateTimeOffset.UtcNow - TimeSpan.FromHours(1); - using MemoryStream archiveStream = new MemoryStream(); - TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Pax, leaveOpen: true); - await using (writer) + await using (MemoryStream archiveStream = new MemoryStream()) { - PaxTarEntry regularFile = new PaxTarEntry(TarEntryType.RegularFile, InitialEntryName); - await writer.WriteEntryAsync(regularFile); - } + await using (TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Pax, leaveOpen: true)) + { + PaxTarEntry regularFile = new PaxTarEntry(TarEntryType.RegularFile, InitialEntryName); + await writer.WriteEntryAsync(regularFile); + } - archiveStream.Position = 0; - TarReader reader = new TarReader(archiveStream); - await using (reader) - { - PaxTarEntry regularFile = await reader.GetNextEntryAsync() as PaxTarEntry; + archiveStream.Position = 0; + await using (TarReader reader = new TarReader(archiveStream)) + { + PaxTarEntry regularFile = await reader.GetNextEntryAsync() as PaxTarEntry; - AssertExtensions.GreaterThanOrEqualTo(regularFile.ExtendedAttributes.Count, 4); - VerifyExtendedAttributeTimestamp(regularFile, PaxEaMTime, minimumTime); - VerifyExtendedAttributeTimestamp(regularFile, PaxEaATime, minimumTime); - VerifyExtendedAttributeTimestamp(regularFile, PaxEaCTime, minimumTime); + AssertExtensions.GreaterThanOrEqualTo(regularFile.ExtendedAttributes.Count, 4); + VerifyExtendedAttributeTimestamp(regularFile, PaxEaMTime, minimumTime); + VerifyExtendedAttributeTimestamp(regularFile, PaxEaATime, minimumTime); + VerifyExtendedAttributeTimestamp(regularFile, PaxEaCTime, minimumTime); + } } } @@ -243,25 +243,25 @@ public async Task WritePaxAttributes_Timestamps_UserProvided_Async() extendedAttributes.Add(PaxEaATime, GetTimestampStringFromDateTimeOffset(TestAccessTime)); extendedAttributes.Add(PaxEaCTime, GetTimestampStringFromDateTimeOffset(TestChangeTime)); - using MemoryStream archiveStream = new MemoryStream(); - TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Pax, leaveOpen: true); - await using (writer) - { - PaxTarEntry regularFile = new PaxTarEntry(TarEntryType.RegularFile, InitialEntryName, extendedAttributes); - regularFile.ModificationTime = TestModificationTime; - await writer.WriteEntryAsync(regularFile); - } - - archiveStream.Position = 0; - TarReader reader = new TarReader(archiveStream); - await using (reader) - { - PaxTarEntry regularFile = await reader.GetNextEntryAsync() as PaxTarEntry; - - AssertExtensions.GreaterThanOrEqualTo(regularFile.ExtendedAttributes.Count, 4); - VerifyExtendedAttributeTimestamp(regularFile, PaxEaMTime, TestModificationTime); - VerifyExtendedAttributeTimestamp(regularFile, PaxEaATime, TestAccessTime); - VerifyExtendedAttributeTimestamp(regularFile, PaxEaCTime, TestChangeTime); + await using (MemoryStream archiveStream = new MemoryStream()) + { + await using (TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Pax, leaveOpen: true)) + { + PaxTarEntry regularFile = new PaxTarEntry(TarEntryType.RegularFile, InitialEntryName, extendedAttributes); + regularFile.ModificationTime = TestModificationTime; + await writer.WriteEntryAsync(regularFile); + } + + archiveStream.Position = 0; + await using (TarReader reader = new TarReader(archiveStream)) + { + PaxTarEntry regularFile = await reader.GetNextEntryAsync() as PaxTarEntry; + + AssertExtensions.GreaterThanOrEqualTo(regularFile.ExtendedAttributes.Count, 4); + VerifyExtendedAttributeTimestamp(regularFile, PaxEaMTime, TestModificationTime); + VerifyExtendedAttributeTimestamp(regularFile, PaxEaATime, TestAccessTime); + VerifyExtendedAttributeTimestamp(regularFile, PaxEaCTime, TestChangeTime); + } } } diff --git a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.File.Tests.Unix.cs b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.File.Tests.Unix.cs index 319bc73376b285..9d53df899ce336 100644 --- a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.File.Tests.Unix.cs +++ b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.File.Tests.Unix.cs @@ -20,37 +20,38 @@ public void Add_Fifo_Async(TarEntryFormat format) { TarEntryFormat expectedFormat = Enum.Parse(strFormat); - using TempDirectory root = new TempDirectory(); - string fifoName = "fifofile"; - string fifoPath = Path.Join(root.Path, fifoName); - - Interop.CheckIo(Interop.Sys.MkFifo(fifoPath, (int)DefaultMode)); - - using MemoryStream archive = new MemoryStream(); - TarWriter writer = new TarWriter(archive, expectedFormat, leaveOpen: true); - await using (writer) - { - await writer.WriteEntryAsync(fileName: fifoPath, entryName: fifoName); - } - - archive.Seek(0, SeekOrigin.Begin); - TarReader reader = new TarReader(archive); - await using (reader) + using (TempDirectory root = new TempDirectory()) { - PosixTarEntry entry = await reader.GetNextEntryAsync() as PosixTarEntry; - Assert.Equal(expectedFormat, entry.Format); - - Assert.NotNull(entry); - Assert.Equal(fifoName, entry.Name); - Assert.Equal(DefaultLinkName, entry.LinkName); - Assert.Equal(TarEntryType.Fifo, entry.EntryType); - Assert.Null(entry.DataStream); - - VerifyPlatformSpecificMetadata(fifoPath, entry); - - Assert.Null(await reader.GetNextEntryAsync()); + string fifoName = "fifofile"; + string fifoPath = Path.Join(root.Path, fifoName); + + Interop.CheckIo(Interop.Sys.MkFifo(fifoPath, (int)DefaultMode)); + + await using (MemoryStream archive = new MemoryStream()) + { + await using (TarWriter writer = new TarWriter(archive, expectedFormat, leaveOpen: true)) + { + await writer.WriteEntryAsync(fileName: fifoPath, entryName: fifoName); + } + + archive.Seek(0, SeekOrigin.Begin); + await using (TarReader reader = new TarReader(archive)) + { + PosixTarEntry entry = await reader.GetNextEntryAsync() as PosixTarEntry; + Assert.Equal(expectedFormat, entry.Format); + + Assert.NotNull(entry); + Assert.Equal(fifoName, entry.Name); + Assert.Equal(DefaultLinkName, entry.LinkName); + Assert.Equal(TarEntryType.Fifo, entry.EntryType); + Assert.Null(entry.DataStream); + + VerifyPlatformSpecificMetadata(fifoPath, entry); + + Assert.Null(await reader.GetNextEntryAsync()); + } + } } - }, format.ToString(), new RemoteInvokeOptions { RunAsSudo = true }).Dispose(); } @@ -64,40 +65,41 @@ public void Add_BlockDevice_Async(TarEntryFormat format) { TarEntryFormat expectedFormat = Enum.Parse(strFormat); - using TempDirectory root = new TempDirectory(); - string blockDevicePath = Path.Join(root.Path, AssetBlockDeviceFileName); - - // Creating device files needs elevation - Interop.CheckIo(Interop.Sys.CreateBlockDevice(blockDevicePath, (int)DefaultMode, TestBlockDeviceMajor, TestBlockDeviceMinor)); - - using MemoryStream archive = new MemoryStream(); - TarWriter writer = new TarWriter(archive, expectedFormat, leaveOpen: true); - await using (writer) + using (TempDirectory root = new TempDirectory()) { - await writer.WriteEntryAsync(fileName: blockDevicePath, entryName: AssetBlockDeviceFileName); - } - - archive.Seek(0, SeekOrigin.Begin); - TarReader reader = new TarReader(archive); - await using (reader) - { - PosixTarEntry entry = await reader.GetNextEntryAsync() as PosixTarEntry; - Assert.Equal(expectedFormat, entry.Format); - - Assert.NotNull(entry); - Assert.Equal(AssetBlockDeviceFileName, entry.Name); - Assert.Equal(DefaultLinkName, entry.LinkName); - Assert.Equal(TarEntryType.BlockDevice, entry.EntryType); - Assert.Null(entry.DataStream); - - VerifyPlatformSpecificMetadata(blockDevicePath, entry); - - Assert.Equal(TestBlockDeviceMajor, entry.DeviceMajor); - Assert.Equal(TestBlockDeviceMinor, entry.DeviceMinor); - - Assert.Null(await reader.GetNextEntryAsync()); + string blockDevicePath = Path.Join(root.Path, AssetBlockDeviceFileName); + + // Creating device files needs elevation + Interop.CheckIo(Interop.Sys.CreateBlockDevice(blockDevicePath, (int)DefaultMode, TestBlockDeviceMajor, TestBlockDeviceMinor)); + + await using (MemoryStream archive = new MemoryStream()) + { + await using (TarWriter writer = new TarWriter(archive, expectedFormat, leaveOpen: true)) + { + await writer.WriteEntryAsync(fileName: blockDevicePath, entryName: AssetBlockDeviceFileName); + } + + archive.Seek(0, SeekOrigin.Begin); + await using (TarReader reader = new TarReader(archive)) + { + PosixTarEntry entry = await reader.GetNextEntryAsync() as PosixTarEntry; + Assert.Equal(expectedFormat, entry.Format); + + Assert.NotNull(entry); + Assert.Equal(AssetBlockDeviceFileName, entry.Name); + Assert.Equal(DefaultLinkName, entry.LinkName); + Assert.Equal(TarEntryType.BlockDevice, entry.EntryType); + Assert.Null(entry.DataStream); + + VerifyPlatformSpecificMetadata(blockDevicePath, entry); + + Assert.Equal(TestBlockDeviceMajor, entry.DeviceMajor); + Assert.Equal(TestBlockDeviceMinor, entry.DeviceMinor); + + Assert.Null(await reader.GetNextEntryAsync()); + } + } } - }, format.ToString(), new RemoteInvokeOptions { RunAsSudo = true }).Dispose(); } @@ -109,41 +111,42 @@ public void Add_CharacterDevice_Async(TarEntryFormat format) { RemoteExecutor.Invoke(async (string strFormat) => { - TarEntryFormat expectedFormat = Enum.Parse(strFormat); - using TempDirectory root = new TempDirectory(); - string characterDevicePath = Path.Join(root.Path, AssetCharacterDeviceFileName); - - // Creating device files needs elevation - Interop.CheckIo(Interop.Sys.CreateCharacterDevice(characterDevicePath, (int)DefaultMode, TestCharacterDeviceMajor, TestCharacterDeviceMinor)); - - using MemoryStream archive = new MemoryStream(); - TarWriter writer = new TarWriter(archive, expectedFormat, leaveOpen: true); - await using (writer) + using (TempDirectory root = new TempDirectory()) { - await writer.WriteEntryAsync(fileName: characterDevicePath, entryName: AssetCharacterDeviceFileName); + TarEntryFormat expectedFormat = Enum.Parse(strFormat); + string characterDevicePath = Path.Join(root.Path, AssetCharacterDeviceFileName); + + // Creating device files needs elevation + Interop.CheckIo(Interop.Sys.CreateCharacterDevice(characterDevicePath, (int)DefaultMode, TestCharacterDeviceMajor, TestCharacterDeviceMinor)); + + await using (MemoryStream archive = new MemoryStream()) + { + await using (TarWriter writer = new TarWriter(archive, expectedFormat, leaveOpen: true)) + { + await writer.WriteEntryAsync(fileName: characterDevicePath, entryName: AssetCharacterDeviceFileName); + } + + archive.Seek(0, SeekOrigin.Begin); + await using (TarReader reader = new TarReader(archive)) + { + PosixTarEntry entry = await reader.GetNextEntryAsync() as PosixTarEntry; + Assert.Equal(expectedFormat, entry.Format); + + Assert.NotNull(entry); + Assert.Equal(AssetCharacterDeviceFileName, entry.Name); + Assert.Equal(DefaultLinkName, entry.LinkName); + Assert.Equal(TarEntryType.CharacterDevice, entry.EntryType); + Assert.Null(entry.DataStream); + + VerifyPlatformSpecificMetadata(characterDevicePath, entry); + + Assert.Equal(TestCharacterDeviceMajor, entry.DeviceMajor); + Assert.Equal(TestCharacterDeviceMinor, entry.DeviceMinor); + + Assert.Null(await reader.GetNextEntryAsync()); + } + } } - - archive.Seek(0, SeekOrigin.Begin); - TarReader reader = new TarReader(archive); - await using (reader) - { - PosixTarEntry entry = await reader.GetNextEntryAsync() as PosixTarEntry; - Assert.Equal(expectedFormat, entry.Format); - - Assert.NotNull(entry); - Assert.Equal(AssetCharacterDeviceFileName, entry.Name); - Assert.Equal(DefaultLinkName, entry.LinkName); - Assert.Equal(TarEntryType.CharacterDevice, entry.EntryType); - Assert.Null(entry.DataStream); - - VerifyPlatformSpecificMetadata(characterDevicePath, entry); - - Assert.Equal(TestCharacterDeviceMajor, entry.DeviceMajor); - Assert.Equal(TestCharacterDeviceMinor, entry.DeviceMinor); - - Assert.Null(await reader.GetNextEntryAsync()); - } - }, format.ToString(), new RemoteInvokeOptions { RunAsSudo = true }).Dispose(); } } diff --git a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.File.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.File.Tests.cs index 048332c4885119..8f4257233adbe4 100644 --- a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.File.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.File.Tests.cs @@ -35,38 +35,39 @@ public async Task FileName_NullOrEmpty_Async() [Fact] public async Task EntryName_NullOrEmpty_Async() { - using TempDirectory root = new TempDirectory(); - - string file1Name = "file1.txt"; - string file2Name = "file2.txt"; - - string file1Path = Path.Join(root.Path, file1Name); - string file2Path = Path.Join(root.Path, file2Name); - - File.Create(file1Path).Dispose(); - File.Create(file2Path).Dispose(); - - using MemoryStream archiveStream = new MemoryStream(); - TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Pax, leaveOpen: true); - await using (writer) + using (TempDirectory root = new TempDirectory()) { - await writer.WriteEntryAsync(file1Path, null); - await writer.WriteEntryAsync(file2Path, string.Empty); - } - - archiveStream.Seek(0, SeekOrigin.Begin); - TarReader reader = new TarReader(archiveStream); - await using (reader) - { - TarEntry first = await reader.GetNextEntryAsync(); - Assert.NotNull(first); - Assert.Equal(file1Name, first.Name); - - TarEntry second = await reader.GetNextEntryAsync(); - Assert.NotNull(second); - Assert.Equal(file2Name, second.Name); - - Assert.Null(await reader.GetNextEntryAsync()); + string file1Name = "file1.txt"; + string file2Name = "file2.txt"; + + string file1Path = Path.Join(root.Path, file1Name); + string file2Path = Path.Join(root.Path, file2Name); + + File.Create(file1Path).Dispose(); + File.Create(file2Path).Dispose(); + + await using (MemoryStream archiveStream = new MemoryStream()) + { + await using (TarWriter writer = new TarWriter(archiveStream, TarEntryFormat.Pax, leaveOpen: true)) + { + await writer.WriteEntryAsync(file1Path, null); + await writer.WriteEntryAsync(file2Path, string.Empty); + } + + archiveStream.Seek(0, SeekOrigin.Begin); + await using (TarReader reader = new TarReader(archiveStream)) + { + TarEntry first = await reader.GetNextEntryAsync(); + Assert.NotNull(first); + Assert.Equal(file1Name, first.Name); + + TarEntry second = await reader.GetNextEntryAsync(); + Assert.NotNull(second); + Assert.Equal(file2Name, second.Name); + + Assert.Null(await reader.GetNextEntryAsync()); + } + } } } @@ -77,45 +78,47 @@ public async Task EntryName_NullOrEmpty_Async() [InlineData(TarEntryFormat.Gnu)] public async Task Add_File_Async(TarEntryFormat format) { - using TempDirectory root = new TempDirectory(); - string fileName = "file.txt"; - string filePath = Path.Join(root.Path, fileName); - string fileContents = "Hello world"; - - using (StreamWriter streamWriter = File.CreateText(filePath)) + using (TempDirectory root = new TempDirectory()) { - streamWriter.Write(fileContents); - } - - using MemoryStream archive = new MemoryStream(); - TarWriter writer = new TarWriter(archive, format, leaveOpen: true); - await using (writer) - { - await writer.WriteEntryAsync(fileName: filePath, entryName: fileName); - } - - archive.Seek(0, SeekOrigin.Begin); - TarReader reader = new TarReader(archive); - await using (reader) - { - TarEntry entry = await reader.GetNextEntryAsync(); - Assert.NotNull(entry); - Assert.Equal(format, entry.Format); - Assert.Equal(fileName, entry.Name); - TarEntryType expectedEntryType = format is TarEntryFormat.V7 ? TarEntryType.V7RegularFile : TarEntryType.RegularFile; - Assert.Equal(expectedEntryType, entry.EntryType); - Assert.True(entry.Length > 0); - Assert.NotNull(entry.DataStream); - - entry.DataStream.Seek(0, SeekOrigin.Begin); - using StreamReader dataReader = new StreamReader(entry.DataStream); - string dataContents = dataReader.ReadLine(); - - Assert.Equal(fileContents, dataContents); - - VerifyPlatformSpecificMetadata(filePath, entry); - - Assert.Null(await reader.GetNextEntryAsync()); + string fileName = "file.txt"; + string filePath = Path.Join(root.Path, fileName); + string fileContents = "Hello world"; + + using (StreamWriter streamWriter = File.CreateText(filePath)) + { + streamWriter.Write(fileContents); + } + + await using (MemoryStream archive = new MemoryStream()) + { + await using (TarWriter writer = new TarWriter(archive, format, leaveOpen: true)) + { + await writer.WriteEntryAsync(fileName: filePath, entryName: fileName); + } + + archive.Seek(0, SeekOrigin.Begin); + await using (TarReader reader = new TarReader(archive)) + { + TarEntry entry = await reader.GetNextEntryAsync(); + Assert.NotNull(entry); + Assert.Equal(format, entry.Format); + Assert.Equal(fileName, entry.Name); + TarEntryType expectedEntryType = format is TarEntryFormat.V7 ? TarEntryType.V7RegularFile : TarEntryType.RegularFile; + Assert.Equal(expectedEntryType, entry.EntryType); + Assert.True(entry.Length > 0); + Assert.NotNull(entry.DataStream); + + entry.DataStream.Seek(0, SeekOrigin.Begin); + using StreamReader dataReader = new StreamReader(entry.DataStream); + string dataContents = dataReader.ReadLine(); + + Assert.Equal(fileContents, dataContents); + + VerifyPlatformSpecificMetadata(filePath, entry); + + Assert.Null(await reader.GetNextEntryAsync()); + } + } } } @@ -130,40 +133,42 @@ public async Task Add_File_Async(TarEntryFormat format) [InlineData(TarEntryFormat.Gnu, true)] public async Task Add_Directory_Async(TarEntryFormat format, bool withContents) { - using TempDirectory root = new TempDirectory(); - string dirName = "dir"; - string dirPath = Path.Join(root.Path, dirName); - Directory.CreateDirectory(dirPath); - - if (withContents) - { - // Add a file inside the directory, we need to ensure the contents - // of the directory are ignored when using AddFile - File.Create(Path.Join(dirPath, "file.txt")).Dispose(); - } - - using MemoryStream archive = new MemoryStream(); - TarWriter writer = new TarWriter(archive, format, leaveOpen: true); - await using (writer) - { - await writer.WriteEntryAsync(fileName: dirPath, entryName: dirName); - } - - archive.Seek(0, SeekOrigin.Begin); - TarReader reader = new TarReader(archive); - await using (reader) + using (TempDirectory root = new TempDirectory()) { - TarEntry entry = await reader.GetNextEntryAsync(); - Assert.Equal(format, entry.Format); - - Assert.NotNull(entry); - Assert.Equal(dirName, entry.Name); - Assert.Equal(TarEntryType.Directory, entry.EntryType); - Assert.Null(entry.DataStream); - - VerifyPlatformSpecificMetadata(dirPath, entry); - - Assert.Null(await reader.GetNextEntryAsync()); // If the dir had contents, they should've been excluded + string dirName = "dir"; + string dirPath = Path.Join(root.Path, dirName); + Directory.CreateDirectory(dirPath); + + if (withContents) + { + // Add a file inside the directory, we need to ensure the contents + // of the directory are ignored when using AddFile + File.Create(Path.Join(dirPath, "file.txt")).Dispose(); + } + + await using (MemoryStream archive = new MemoryStream()) + { + await using (TarWriter writer = new TarWriter(archive, format, leaveOpen: true)) + { + await writer.WriteEntryAsync(fileName: dirPath, entryName: dirName); + } + + archive.Seek(0, SeekOrigin.Begin); + await using (TarReader reader = new TarReader(archive)) + { + TarEntry entry = await reader.GetNextEntryAsync(); + Assert.Equal(format, entry.Format); + + Assert.NotNull(entry); + Assert.Equal(dirName, entry.Name); + Assert.Equal(TarEntryType.Directory, entry.EntryType); + Assert.Null(entry.DataStream); + + VerifyPlatformSpecificMetadata(dirPath, entry); + + Assert.Null(await reader.GetNextEntryAsync()); // If the dir had contents, they should've been excluded + } + } } } @@ -178,43 +183,45 @@ public async Task Add_Directory_Async(TarEntryFormat format, bool withContents) [InlineData(TarEntryFormat.Gnu, true)] public async Task Add_SymbolicLink_Async(TarEntryFormat format, bool createTarget) { - using TempDirectory root = new TempDirectory(); - string targetName = "file.txt"; - string linkName = "link.txt"; - string targetPath = Path.Join(root.Path, targetName); - string linkPath = Path.Join(root.Path, linkName); - - if (createTarget) + using (TempDirectory root = new TempDirectory()) { - File.Create(targetPath).Dispose(); - } - - FileInfo linkInfo = new FileInfo(linkPath); - linkInfo.CreateAsSymbolicLink(targetName); - - using MemoryStream archive = new MemoryStream(); - TarWriter writer = new TarWriter(archive, format, leaveOpen: true); - await using (writer) - { - await writer.WriteEntryAsync(fileName: linkPath, entryName: linkName); - } - - archive.Seek(0, SeekOrigin.Begin); - TarReader reader = new TarReader(archive); - await using (reader) - { - TarEntry entry = await reader.GetNextEntryAsync(); - Assert.Equal(format, entry.Format); - - Assert.NotNull(entry); - Assert.Equal(linkName, entry.Name); - Assert.Equal(targetName, entry.LinkName); - Assert.Equal(TarEntryType.SymbolicLink, entry.EntryType); - Assert.Null(entry.DataStream); - - VerifyPlatformSpecificMetadata(linkPath, entry); - - Assert.Null(await reader.GetNextEntryAsync()); + string targetName = "file.txt"; + string linkName = "link.txt"; + string targetPath = Path.Join(root.Path, targetName); + string linkPath = Path.Join(root.Path, linkName); + + if (createTarget) + { + File.Create(targetPath).Dispose(); + } + + FileInfo linkInfo = new FileInfo(linkPath); + linkInfo.CreateAsSymbolicLink(targetName); + + await using (MemoryStream archive = new MemoryStream()) + { + await using (TarWriter writer = new TarWriter(archive, format, leaveOpen: true)) + { + await writer.WriteEntryAsync(fileName: linkPath, entryName: linkName); + } + + archive.Seek(0, SeekOrigin.Begin); + await using (TarReader reader = new TarReader(archive)) + { + TarEntry entry = await reader.GetNextEntryAsync(); + Assert.Equal(format, entry.Format); + + Assert.NotNull(entry); + Assert.Equal(linkName, entry.Name); + Assert.Equal(targetName, entry.LinkName); + Assert.Equal(TarEntryType.SymbolicLink, entry.EntryType); + Assert.Null(entry.DataStream); + + VerifyPlatformSpecificMetadata(linkPath, entry); + + Assert.Null(await reader.GetNextEntryAsync()); + } + } } } } 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 2ca49f70541b17..e548f93e78aaba 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 @@ -91,28 +91,29 @@ public async Task WriteEntry_FromUnseekableStream_AdvanceDataStream_WriteFromTha [InlineData(TarEntryFormat.Gnu)] public async Task WriteEntry_RespectDefaultWriterFormat_Async(TarEntryFormat expectedFormat) { - using TempDirectory root = new TempDirectory(); - - string path = Path.Join(root.Path, "file.txt"); - File.Create(path).Dispose(); - - using MemoryStream archiveStream = new MemoryStream(); - TarWriter writer = new TarWriter(archiveStream, expectedFormat, leaveOpen: true); - await using (writer) + using (TempDirectory root = new TempDirectory()) { - await writer.WriteEntryAsync(path, "file.txt"); - } + string path = Path.Join(root.Path, "file.txt"); + File.Create(path).Dispose(); - archiveStream.Position = 0; - TarReader reader = new TarReader(archiveStream, leaveOpen: false); - await using (reader) - { - TarEntry entry = await reader.GetNextEntryAsync(); - Assert.Equal(expectedFormat, entry.Format); + await using (MemoryStream archiveStream = new MemoryStream()) + { + await using (TarWriter writer = new TarWriter(archiveStream, expectedFormat, leaveOpen: true)) + { + await writer.WriteEntryAsync(path, "file.txt"); + } + + archiveStream.Position = 0; + await using (TarReader reader = new TarReader(archiveStream, leaveOpen: false)) + { + TarEntry entry = await reader.GetNextEntryAsync(); + Assert.Equal(expectedFormat, entry.Format); - Type expectedType = GetTypeForFormat(expectedFormat); + Type expectedType = GetTypeForFormat(expectedFormat); - Assert.Equal(expectedType, entry.GetType()); + Assert.Equal(expectedType, entry.GetType()); + } + } } }