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