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 40eb5e16d2531..a3b63226f060c 100644 --- a/src/libraries/System.Formats.Tar/ref/System.Formats.Tar.cs +++ b/src/libraries/System.Formats.Tar/ref/System.Formats.Tar.cs @@ -13,6 +13,11 @@ public GnuTarEntry(System.Formats.Tar.TarEntryType entryType, string entryName) public System.DateTimeOffset AccessTime { get { throw null; } set { } } public System.DateTimeOffset ChangeTime { get { throw null; } set { } } } + public sealed partial class PaxGlobalExtendedAttributesTarEntry : System.Formats.Tar.PosixTarEntry + { + public PaxGlobalExtendedAttributesTarEntry(System.Collections.Generic.IEnumerable> globalExtendedAttributes) { } + public System.Collections.Generic.IReadOnlyDictionary GlobalExtendedAttributes { get { throw null; } } + } public sealed partial class PaxTarEntry : System.Formats.Tar.PosixTarEntry { public PaxTarEntry(System.Formats.Tar.TarEntry other) { } @@ -101,14 +106,13 @@ public enum TarFileMode public sealed partial class TarReader : System.IDisposable { public TarReader(System.IO.Stream archiveStream, bool leaveOpen = false) { } - public System.Collections.Generic.IReadOnlyDictionary? GlobalExtendedAttributes { get { throw null; } } public void Dispose() { } public System.Formats.Tar.TarEntry? GetNextEntry(bool copyData = false) { throw null; } } public sealed partial class TarWriter : System.IDisposable { public TarWriter(System.IO.Stream archiveStream) { } - public TarWriter(System.IO.Stream archiveStream, System.Collections.Generic.IEnumerable>? globalExtendedAttributes = null, bool leaveOpen = false) { } + 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() { } diff --git a/src/libraries/System.Formats.Tar/src/Resources/Strings.resx b/src/libraries/System.Formats.Tar/src/Resources/Strings.resx index 3140ec6831a83..59404900a6563 100644 --- a/src/libraries/System.Formats.Tar/src/Resources/Strings.resx +++ b/src/libraries/System.Formats.Tar/src/Resources/Strings.resx @@ -186,12 +186,12 @@ SetLength requires a stream that supports seeking and writing. + + Cannot convert a PaxGlobalExtendedAttributesEntry into another format. + The entry '{0}' has a duplicate extended attribute. - - An entry in '{0}' format was found in an archive where other entries of format '{1}' have been found. - Cannot set the 'DeviceMajor' or 'DeviceMinor' fields on an entry that does not represent a block or character device. @@ -240,9 +240,6 @@ Cannot create the symbolic link '{0}' because the specified target '{1}' does not exist. - - The archive has more than one global extended attributes entry. - A metadata entry of type '{0}' was unexpectedly found after a metadata entry of type '{1}'. diff --git a/src/libraries/System.Formats.Tar/src/System.Formats.Tar.csproj b/src/libraries/System.Formats.Tar/src/System.Formats.Tar.csproj index 65aa346b4907f..867940f737573 100644 --- a/src/libraries/System.Formats.Tar/src/System.Formats.Tar.csproj +++ b/src/libraries/System.Formats.Tar/src/System.Formats.Tar.csproj @@ -24,6 +24,7 @@ + diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/GnuTarEntry.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/GnuTarEntry.cs index ef0efb6906bbd..4c13fddfa9ea6 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/GnuTarEntry.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/GnuTarEntry.cs @@ -29,7 +29,7 @@ internal GnuTarEntry(TarHeader header, TarReader readerOfOrigin) /// /// public GnuTarEntry(TarEntryType entryType, string entryName) - : base(entryType, entryName, TarEntryFormat.Gnu) + : base(entryType, entryName, TarEntryFormat.Gnu, isGea: false) { _header._aTime = _header._mTime; // mtime was set in base constructor _header._cTime = _header._mTime; diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/PaxGlobalExtendedAttributesTarEntry.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/PaxGlobalExtendedAttributesTarEntry.cs new file mode 100644 index 0000000000000..7b7c493b171c4 --- /dev/null +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/PaxGlobalExtendedAttributesTarEntry.cs @@ -0,0 +1,50 @@ +// 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.Collections.ObjectModel; +using System.Diagnostics; + +namespace System.Formats.Tar +{ + /// + /// Represents a Global Extended Attributes TAR entry from an archive of the PAX format. + /// + public sealed class PaxGlobalExtendedAttributesTarEntry : PosixTarEntry + { + private ReadOnlyDictionary? _readOnlyGlobalExtendedAttributes; + + // Constructor used when reading an existing archive. + internal PaxGlobalExtendedAttributesTarEntry(TarHeader header, TarReader readerOfOrigin) + : base(header, readerOfOrigin, TarEntryFormat.Pax) + { + } + + /// + /// Initializes a new instance with the specified Global Extended Attributes enumeration. + /// + /// An enumeration of string key-value pairs that represents the metadata to include as Global Extended Attributes. + /// is . + public PaxGlobalExtendedAttributesTarEntry(IEnumerable> globalExtendedAttributes) + : base(TarEntryType.GlobalExtendedAttributes, TarHeader.GlobalHeadFormatPrefix, TarEntryFormat.Pax, isGea: true) + { + ArgumentNullException.ThrowIfNull(globalExtendedAttributes); + _header._extendedAttributes = new Dictionary(globalExtendedAttributes); + } + + /// + /// Returns the global extended attributes stored in this entry. + /// + public IReadOnlyDictionary GlobalExtendedAttributes + { + get + { + _header._extendedAttributes ??= new Dictionary(); + return _readOnlyGlobalExtendedAttributes ??= _header._extendedAttributes.AsReadOnly(); + } + } + + // Determines if the current instance's entry type supports setting a data stream. + internal override bool IsDataStreamSetterSupported() => false; + } +} diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/PaxTarEntry.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/PaxTarEntry.cs index 8a823fc1ee629..9c80a45e2647e 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/PaxTarEntry.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/PaxTarEntry.cs @@ -48,7 +48,7 @@ internal PaxTarEntry(TarHeader header, TarReader readerOfOrigin) /// /// public PaxTarEntry(TarEntryType entryType, string entryName) - : base(entryType, entryName, TarEntryFormat.Pax) + : base(entryType, entryName, TarEntryFormat.Pax, isGea: false) { _header._prefix = string.Empty; _header._extendedAttributes = new Dictionary(); @@ -87,7 +87,7 @@ public PaxTarEntry(TarEntryType entryType, string entryName) /// /// public PaxTarEntry(TarEntryType entryType, string entryName, IEnumerable> extendedAttributes) - : base(entryType, entryName, TarEntryFormat.Pax) + : base(entryType, entryName, TarEntryFormat.Pax, isGea: false) { ArgumentNullException.ThrowIfNull(extendedAttributes); diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/PosixTarEntry.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/PosixTarEntry.cs index a86e0653cb35f..0a45494f4fe23 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/PosixTarEntry.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/PosixTarEntry.cs @@ -19,8 +19,8 @@ internal PosixTarEntry(TarHeader header, TarReader readerOfOrigin, TarEntryForma } // Constructor called when the user creates a TarEntry instance from scratch. - internal PosixTarEntry(TarEntryType entryType, string entryName, TarEntryFormat format) - : base(entryType, entryName, format) + internal PosixTarEntry(TarEntryType entryType, string entryName, TarEntryFormat format, bool isGea) + : base(entryType, entryName, format, isGea) { _header._uName = string.Empty; _header._gName = string.Empty; 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 50f22e300f2f4..cb811686c54e8 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 @@ -29,10 +29,16 @@ internal TarEntry(TarHeader header, TarReader readerOfOrigin, TarEntryFormat for } // Constructor called when the user creates a TarEntry instance from scratch. - internal TarEntry(TarEntryType entryType, string entryName, TarEntryFormat format) + internal TarEntry(TarEntryType entryType, string entryName, TarEntryFormat format, bool isGea) { ArgumentException.ThrowIfNullOrEmpty(entryName); - TarHelpers.ThrowIfEntryTypeNotSupported(entryType, format); + + Debug.Assert(!isGea || entryType is TarEntryType.GlobalExtendedAttributes); + + if (!isGea) + { + TarHelpers.ThrowIfEntryTypeNotSupported(entryType, format); + } _header = default; _header._format = format; @@ -48,6 +54,11 @@ internal TarEntry(TarEntryType entryType, string entryName, TarEntryFormat forma // Constructor called when converting an entry to the selected format. internal TarEntry(TarEntry other, TarEntryFormat format) { + if (other is PaxGlobalExtendedAttributesTarEntry) + { + throw new InvalidOperationException(SR.TarCannotConvertPaxGlobalExtendedAttributesEntry); + } + TarEntryType compatibleEntryType; if (other.Format is TarEntryFormat.V7 && other.EntryType is TarEntryType.V7RegularFile && format is TarEntryFormat.Ustar or TarEntryFormat.Pax or TarEntryFormat.Gnu) { @@ -208,7 +219,7 @@ public int Uid /// Operation not permitted due to insufficient permissions. public void ExtractToFile(string destinationFileName, bool overwrite) { - if (EntryType is TarEntryType.SymbolicLink or TarEntryType.HardLink) + if (EntryType is TarEntryType.SymbolicLink or TarEntryType.HardLink or TarEntryType.GlobalExtendedAttributes) { throw 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 66e87920fe20d..aa30d728ab6a2 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 @@ -255,7 +255,10 @@ private static void ExtractToDirectoryInternal(Stream source, string destination TarEntry? entry; while ((entry = reader.GetNextEntry()) != null) { - entry.ExtractRelativeToDirectory(destinationDirectoryPath, overwriteFiles); + if (entry is not PaxGlobalExtendedAttributesTarEntry) + { + entry.ExtractRelativeToDirectory(destinationDirectoryPath, overwriteFiles); + } } } } 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 44d6dac5ad19c..db2db1157a03e 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 @@ -22,28 +22,9 @@ internal partial struct TarHeader // "{dirName}/PaxHeaders.{processId}/{fileName}{trailingSeparator}" private const string PaxHeadersFormat = "{0}/PaxHeaders.{1}/{2}{3}"; - // Global Extended Attribute entries have a special format in the Name field: - // "{tmpFolder}/GlobalHead.{processId}.1" - private const string GlobalHeadFormat = "{0}/GlobalHead.{1}.1"; - // Predefined text for the Name field of a GNU long metadata entry. Applies for both LongPath ('L') and LongLink ('K'). private const string GnuLongMetadataName = "././@LongLink"; - // Creates a PAX Global Extended Attributes header and writes it into the specified archive stream. - internal static void WriteGlobalExtendedAttributesHeader(Stream archiveStream, Span buffer, IEnumerable> globalExtendedAttributes) - { - TarHeader geaHeader = default; - geaHeader._name = GenerateGlobalExtendedAttributeName(); - geaHeader._mode = (int)TarHelpers.DefaultMode; - geaHeader._typeFlag = TarEntryType.GlobalExtendedAttributes; - geaHeader._linkName = string.Empty; - geaHeader._magic = string.Empty; - geaHeader._version = string.Empty; - geaHeader._gName = string.Empty; - geaHeader._uName = string.Empty; - geaHeader.WriteAsPaxExtendedAttributes(archiveStream, buffer, globalExtendedAttributes, isGea: true); - } - // Writes the current header as a V7 entry into the archive stream. internal void WriteAsV7(Stream archiveStream, Span buffer) { @@ -82,14 +63,27 @@ 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); + } + // 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. + // Makes sure to add the preceding extended attributes entry before the actual entry. internal void WriteAsPax(Stream archiveStream, Span buffer) { + Debug.Assert(_typeFlag is not TarEntryType.GlobalExtendedAttributes); + // First, we write the preceding extended attributes header TarHeader extendedAttributesHeader = default; // Fill the current header's dict CollectExtendedAttributesFromStandardFieldsIfNeeded(); + // And pass the attributes to the preceding extended attributes header for writing Debug.Assert(_extendedAttributes != null); extendedAttributesHeader.WriteAsPaxExtendedAttributes(archiveStream, buffer, _extendedAttributes, isGea: false); @@ -611,30 +605,30 @@ private string GenerateExtendedAttributeName() } // Gets the special name for the 'name' field in a global extended attribute entry. - // Format: "%d/GlobalHead.%p/%f" + // Format: "%d/GlobalHead.%p/%n" // - %d: The path of the $TMPDIR variable, if found. Otherwise, the value is '/tmp'. // - %p: The current process ID. // - %n: The sequence number of the global extended header record of the archive, starting at 1. In our case, since we only generate one, the value is always 1. // If the path of $TMPDIR makes the final string too long to fit in the 'name' field, // then the TMPDIR='/tmp' is used. - private static string GenerateGlobalExtendedAttributeName() + private static string GenerateGlobalExtendedAttributeName(int globalExtendedAttributesEntryNumber) { - string? tmpDir = Environment.GetEnvironmentVariable("TMPDIR"); - if (string.IsNullOrWhiteSpace(tmpDir)) - { - tmpDir = "/tmp"; - } - else if (Path.EndsInDirectorySeparator(tmpDir)) + Debug.Assert(globalExtendedAttributesEntryNumber >= 1); + + string tmpDir = Path.GetTempPath(); + if (Path.EndsInDirectorySeparator(tmpDir)) { tmpDir = Path.TrimEndingDirectorySeparator(tmpDir); } int processId = Environment.ProcessId; - string result = string.Format(GlobalHeadFormat, tmpDir, processId); - if (result.Length >= FieldLengths.Name) + string result = string.Format(GlobalHeadFormatPrefix, tmpDir, processId); + string suffix = $".{globalExtendedAttributesEntryNumber}"; // GEA sequence number + if (result.Length + suffix.Length >= FieldLengths.Name) { - result = string.Format(GlobalHeadFormat, "/tmp", processId); + result = string.Format(GlobalHeadFormatPrefix, "/tmp", processId); } + result += suffix; return result; } diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.cs index 79a209e64846a..ae54a00dcf4d2 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.cs @@ -38,6 +38,11 @@ internal partial struct TarHeader private const string PaxEaDevMajor = "devmajor"; private const string PaxEaDevMinor = "devminor"; + // Global Extended Attribute entries have a special format in the Name field: + // "{tmpFolder}/GlobalHead.{processId}.{GEAEntryNumber}" + // Excludes ".{GEAEntryNumber}" because the number gets added on write. + internal const string GlobalHeadFormatPrefix = "{0}/GlobalHead.{1}"; + internal Stream? _dataStream; // Position in the stream where the data ends in this header. 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 a38600662ef5b..924106cf3bcbf 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 @@ -252,9 +252,10 @@ TarEntryType.HardLink or TarEntryType.RegularFile or TarEntryType.SymbolicLink) { + // GlobalExtendedAttributes is handled via PaxGlobalExtendedAttributesEntry + // Not supported for writing - internally autogenerated: // - ExtendedAttributes - // - GlobalExtendedAttributes return; } break; 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 da254f351216e..3855bd8d317fe 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 @@ -40,19 +40,11 @@ public TarReader(Stream archiveStream, bool leaveOpen = false) _leaveOpen = leaveOpen; _previouslyReadEntry = null; - GlobalExtendedAttributes = null; _isDisposed = false; _readFirstEntry = false; _reachedEndMarkers = false; } - /// - /// If the archive format is , returns a read-only dictionary containing the string key-value pairs of the Global Extended Attributes in the first entry of the archive. - /// If there is no Global Extended Attributes entry at the beginning of the archive, this returns an empty read-only dictionary. - /// If the first entry has not been read by calling , this returns . - /// - public IReadOnlyDictionary? GlobalExtendedAttributes { get; private set; } - /// /// Disposes the current instance, and disposes the streams of all the entries that were read from the archive. /// @@ -114,7 +106,8 @@ public void Dispose() 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), @@ -221,37 +214,6 @@ private bool TryGetNextEntryHeader(out TarHeader header, bool copyData) return false; } - // Special case: First header. Collect GEA from data section, then get next entry. - if (header._typeFlag is TarEntryType.GlobalExtendedAttributes) - { - if (GlobalExtendedAttributes != null) - { - // We can only have one extended attributes entry. - throw new FormatException(SR.TarTooManyGlobalExtendedAttributesEntries); - } - - GlobalExtendedAttributes = header._extendedAttributes?.AsReadOnly(); - - header = default; - header._format = TarEntryFormat.Pax; - try - { - if (!header.TryGetNextHeader(_archiveStream, copyData)) - { - return false; - } - } - catch (EndOfStreamException) - { - // Edge case: The only entry in the archive was a Global Extended Attributes entry - return false; - } - if (header._typeFlag == TarEntryType.GlobalExtendedAttributes) - { - throw new FormatException(SR.TarTooManyGlobalExtendedAttributesEntries); - } - } - // If a metadata typeflag entry is retrieved, handle it here, then read the next entry // PAX metadata @@ -287,6 +249,9 @@ 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. private bool TryProcessExtendedAttributesHeader(TarHeader extendedAttributesHeader, bool copyData, out TarHeader actualHeader) { actualHeader = default; @@ -298,26 +263,26 @@ private bool TryProcessExtendedAttributesHeader(TarHeader extendedAttributesHead return false; } - // Should never read a GEA entry at this point - if (actualHeader._typeFlag == TarEntryType.GlobalExtendedAttributes) - { - throw new FormatException(SR.TarTooManyGlobalExtendedAttributesEntries); - } - - // Can't have two extended attribute metadata entries in a row - if (actualHeader._typeFlag is TarEntryType.ExtendedAttributes) + // 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) { - throw new FormatException(string.Format(SR.TarUnexpectedMetadataEntry, TarEntryType.ExtendedAttributes, TarEntryType.ExtendedAttributes)); + throw new FormatException(string.Format(SR.TarUnexpectedMetadataEntry, actualHeader._typeFlag, TarEntryType.ExtendedAttributes)); } Debug.Assert(extendedAttributesHeader._extendedAttributes != null); - // Replace all the standard attributes with the extended attributes ones, + // Replace all the attributes representing standard fields with the extended ones, if any actualHeader.ReplaceNormalAttributesWithExtended(extendedAttributesHeader._extendedAttributes); 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. private bool TryProcessGnuMetadataHeader(TarHeader header, bool copyData, out TarHeader finalHeader) { finalHeader = default; 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 bb630a186c869..c3ad156d0fb1a 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,12 +13,11 @@ namespace System.Formats.Tar /// public sealed partial class TarWriter : IDisposable { - private bool _wroteGEA; private bool _wroteEntries; private bool _isDisposed; private readonly bool _leaveOpen; private readonly Stream _archiveStream; - private readonly IEnumerable>? _globalExtendedAttributes; + private int _nextGlobalExtendedAttributesEntryNumber; /// /// Initializes a instance that can write tar entries to the specified stream and closes the upon disposal of this instance. @@ -31,15 +30,13 @@ public TarWriter(Stream archiveStream) } /// - /// Initializes a instance that can write tar entries to the specified stream, optionally leave the stream open upon disposal of this instance, and can optionally add a Global Extended Attributes entry at the beginning of the archive. When using this constructor, the format of the resulting archive is . + /// Initializes a instance that can write tar entries to the specified stream and optionally leaves the stream open upon disposal of this instance. When using this constructor, the format of the resulting archive is . /// /// The stream to write to. - /// An optional enumeration of string key-value pairs that represent Global Extended Attributes metadata that should apply to all subsquent entries. If , then no Global Extended Attributes entry is written. If an empty instance is passed, a Global Extended Attributes entry is written with default values. /// to dispose the when this instance is disposed; to leave the stream open. - public TarWriter(Stream archiveStream, IEnumerable>? globalExtendedAttributes = null, bool leaveOpen = false) + public TarWriter(Stream archiveStream, bool leaveOpen = false) : this(archiveStream, TarEntryFormat.Pax, leaveOpen) { - _globalExtendedAttributes = globalExtendedAttributes; } /// @@ -73,8 +70,7 @@ public TarWriter(Stream archiveStream, TarEntryFormat format = TarEntryFormat.Pa _leaveOpen = leaveOpen; _isDisposed = false; _wroteEntries = false; - _wroteGEA = false; - _globalExtendedAttributes = null; + _nextGlobalExtendedAttributesEntryNumber = 1; } /// @@ -120,11 +116,6 @@ public void WriteEntry(string fileName, string? entryName) entryName = Path.GetFileName(fileName); } - if (Format is TarEntryFormat.Pax) - { - WriteGlobalExtendedAttributesEntryIfNeeded(); - } - ReadFileFromDiskAndWriteToArchiveStreamAsEntry(fullPath, entryName); } @@ -176,8 +167,6 @@ public void WriteEntry(TarEntry entry) { ThrowIfDisposed(); - WriteGlobalExtendedAttributesEntryIfNeeded(); - byte[] rented = ArrayPool.Shared.Rent(minimumLength: TarHelpers.RecordSize); Span buffer = rented.AsSpan(0, TarHelpers.RecordSize); // minimumLength means the array could've been larger buffer.Clear(); // Rented arrays aren't clean @@ -192,7 +181,15 @@ public void WriteEntry(TarEntry entry) entry._header.WriteAsUstar(_archiveStream, buffer); break; case TarEntryFormat.Pax: - entry._header.WriteAsPax(_archiveStream, buffer); + if (entry._header._typeFlag is TarEntryType.GlobalExtendedAttributes) + { + entry._header.WriteAsPaxGlobalExtendedAttributes(_archiveStream, buffer, _nextGlobalExtendedAttributesEntryNumber); + _nextGlobalExtendedAttributesEntryNumber++; + } + else + { + entry._header.WriteAsPax(_archiveStream, buffer); + } break; case TarEntryFormat.Gnu: entry._header.WriteAsGnu(_archiveStream, buffer); @@ -254,8 +251,6 @@ private void Dispose(bool disposing) { try { - WriteGlobalExtendedAttributesEntryIfNeeded(); - if (_wroteEntries) { WriteFinalRecords(); @@ -283,36 +278,6 @@ private void ThrowIfDisposed() } } - // Writes a Global Extended Attributes entry at the beginning of the archive. - private void WriteGlobalExtendedAttributesEntryIfNeeded() - { - Debug.Assert(!_isDisposed); - - if (_wroteGEA || Format != TarEntryFormat.Pax) - { - return; - } - - Debug.Assert(!_wroteEntries); // The GEA entry can only be the first entry - - if (_globalExtendedAttributes != null) - { - byte[] rented = ArrayPool.Shared.Rent(minimumLength: TarHelpers.RecordSize); - try - { - Span buffer = rented.AsSpan(0, TarHelpers.RecordSize); - buffer.Clear(); // Rented arrays aren't clean - // Write the GEA entry regardless if it has values or not - TarHeader.WriteGlobalExtendedAttributesHeader(_archiveStream, buffer, _globalExtendedAttributes); - } - finally - { - ArrayPool.Shared.Return(rented); - } - } - _wroteGEA = true; - } - // The spec indicates that the end of the archive is indicated // by two records consisting entirely of zero bytes. private void WriteFinalRecords() diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/UstarTarEntry.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/UstarTarEntry.cs index afe1f3e4a1fc5..10c5aad7325ad 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/UstarTarEntry.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/UstarTarEntry.cs @@ -28,7 +28,7 @@ internal UstarTarEntry(TarHeader header, TarReader readerOfOrigin) /// /// public UstarTarEntry(TarEntryType entryType, string entryName) - : base(entryType, entryName, TarEntryFormat.Ustar) + : base(entryType, entryName, TarEntryFormat.Ustar, isGea: false) { _header._prefix = string.Empty; } diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/V7TarEntry.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/V7TarEntry.cs index 2ee03fe1b829e..fd049c05d90c9 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/V7TarEntry.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/V7TarEntry.cs @@ -23,7 +23,7 @@ internal V7TarEntry(TarHeader header, TarReader readerOfOrigin) /// The entry type is not supported for creating an entry. /// When creating an instance using the constructor, only the following entry types are supported: , , and . public V7TarEntry(TarEntryType entryType, string entryName) - : base(entryType, entryName, TarEntryFormat.V7) + : base(entryType, entryName, TarEntryFormat.V7, isGea: false) { } 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 09981cbf5e909..ffcac5e244b4c 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,8 @@ + + diff --git a/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.File.GlobalExtendedAttributes.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.File.GlobalExtendedAttributes.Tests.cs new file mode 100644 index 0000000000000..2661c542ae4c0 --- /dev/null +++ b/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.File.GlobalExtendedAttributes.Tests.cs @@ -0,0 +1,86 @@ +// 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 Xunit; + +namespace System.Formats.Tar.Tests +{ + public class TarReader_File_GlobalExtendedAttributes_Tests : TarReader_File_Tests_Base + { + [Fact] + public void Read_Archive_File() => + Read_Archive_File_Internal(TarEntryFormat.Pax, TestTarFormat.pax_gea); + + [Fact] + public void Read_Archive_File_HardLink() => + Read_Archive_File_HardLink_Internal(TarEntryFormat.Pax, TestTarFormat.pax_gea); + + [Fact] + public void Read_Archive_File_SymbolicLink() => + Read_Archive_File_SymbolicLink_Internal(TarEntryFormat.Pax, TestTarFormat.pax_gea); + + [Fact] + public void Read_Archive_Folder_File() => + Read_Archive_Folder_File_Internal(TarEntryFormat.Pax, TestTarFormat.pax_gea); + + [Fact] + public void Read_Archive_Folder_File_Utf8() => + Read_Archive_Folder_File_Utf8_Internal(TarEntryFormat.Pax, TestTarFormat.pax_gea); + + [Fact] + public void Read_Archive_Folder_Subfolder_File() => + Read_Archive_Folder_Subfolder_File_Internal(TarEntryFormat.Pax, TestTarFormat.pax_gea); + + [Fact] + public void Read_Archive_FolderSymbolicLink_Folder_Subfolder_File() => + Read_Archive_FolderSymbolicLink_Folder_Subfolder_File_Internal(TarEntryFormat.Pax, TestTarFormat.pax_gea); + + [Fact] + public void Read_Archive_Many_Small_Files() => + Read_Archive_Many_Small_Files_Internal(TarEntryFormat.Pax, TestTarFormat.pax_gea); + + [Fact] + public void Read_Archive_LongPath_Splitable_Under255() => + Read_Archive_LongPath_Splitable_Under255_Internal(TarEntryFormat.Pax, TestTarFormat.pax_gea); + + [Fact] + public void Read_Archive_SpecialFiles() => + Read_Archive_SpecialFiles_Internal(TarEntryFormat.Pax, TestTarFormat.pax_gea); + + [Fact] + public void Read_Archive_File_LongSymbolicLink() => + Read_Archive_File_LongSymbolicLink_Internal(TarEntryFormat.Pax, TestTarFormat.pax_gea); + + [Fact] + public void Read_Archive_LongFileName_Over100_Under255() => + Read_Archive_LongFileName_Over100_Under255_Internal(TarEntryFormat.Pax, TestTarFormat.pax_gea); + + [Fact] + public void Read_Archive_LongPath_Over255() => + Read_Archive_LongPath_Over255_Internal(TarEntryFormat.Pax, TestTarFormat.pax_gea); + + [Fact] + public void ExtractGlobalExtendedAttributesEntry_Throws() + { + using TempDirectory root = new TempDirectory(); + + using MemoryStream archiveStream = new MemoryStream(); + using (TarWriter writer = new TarWriter(archiveStream, leaveOpen: true)) + { + PaxGlobalExtendedAttributesTarEntry gea = new PaxGlobalExtendedAttributesTarEntry(new Dictionary()); + writer.WriteEntry(gea); + } + + archiveStream.Position = 0; + + using (TarReader reader = new TarReader(archiveStream, leaveOpen: false)) + { + TarEntry entry = reader.GetNextEntry(); + Assert.Throws(() => entry.ExtractToFile(Path.Join(root.Path, "file"), overwrite: true)); + } + } + } +} diff --git a/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.File.Tests.Base.cs b/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.File.Tests.Base.cs new file mode 100644 index 0000000000000..2479fcb081988 --- /dev/null +++ b/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.File.Tests.Base.cs @@ -0,0 +1,620 @@ +// 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 Xunit; +using static System.Net.WebRequestMethods; + +namespace System.Formats.Tar.Tests +{ + public class TarReader_File_Tests_Base : TarTestsBase + { + protected void Read_Archive_File_Internal(TarEntryFormat format, TestTarFormat testFormat) + { + string testCaseName = "file"; + using MemoryStream ms = GetTarMemoryStream(CompressionMethod.Uncompressed, testFormat, testCaseName); + + using TarReader reader = new TarReader(ms); + + if (testFormat is TestTarFormat.pax_gea) + { + VerifyGlobalExtendedAttributes(reader); + } + + TarEntry file = reader.GetNextEntry(); + + VerifyRegularFileEntry(file, format, "file.txt", $"Hello {testCaseName}"); + + Assert.Null(reader.GetNextEntry()); + } + + protected void Read_Archive_File_HardLink_Internal(TarEntryFormat format, TestTarFormat testFormat) + { + string testCaseName = "file_hardlink"; + using MemoryStream ms = GetTarMemoryStream(CompressionMethod.Uncompressed, testFormat, testCaseName); + + using TarReader reader = new TarReader(ms); + + if (testFormat is TestTarFormat.pax_gea) + { + VerifyGlobalExtendedAttributes(reader); + } + + TarEntry file = reader.GetNextEntry(); + + VerifyRegularFileEntry(file, format, "file.txt", $"Hello {testCaseName}"); + + TarEntry hardLink = reader.GetNextEntry(); + // 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(reader.GetNextEntry()); + } + + protected void Read_Archive_File_SymbolicLink_Internal(TarEntryFormat format, TestTarFormat testFormat) + { + string testCaseName = "file_symlink"; + using MemoryStream ms = GetTarMemoryStream(CompressionMethod.Uncompressed, testFormat, testCaseName); + + using TarReader reader = new TarReader(ms); + + if (testFormat is TestTarFormat.pax_gea) + { + VerifyGlobalExtendedAttributes(reader); + } + + TarEntry file = reader.GetNextEntry(); + + VerifyRegularFileEntry(file, format, "file.txt", $"Hello {testCaseName}"); + + TarEntry symbolicLink = reader.GetNextEntry(); + VerifySymbolicLinkEntry(symbolicLink, format, "link.txt", "file.txt"); + + Assert.Null(reader.GetNextEntry()); + } + + protected void Read_Archive_Folder_File_Internal(TarEntryFormat format, TestTarFormat testFormat) + { + string testCaseName = "folder_file"; + using MemoryStream ms = GetTarMemoryStream(CompressionMethod.Uncompressed, testFormat, testCaseName); + + using TarReader reader = new TarReader(ms); + + if (testFormat is TestTarFormat.pax_gea) + { + VerifyGlobalExtendedAttributes(reader); + } + + TarEntry directory = reader.GetNextEntry(); + + VerifyDirectoryEntry(directory, format, "folder/"); + + TarEntry file = reader.GetNextEntry(); + VerifyRegularFileEntry(file, format, "folder/file.txt", $"Hello {testCaseName}"); + + Assert.Null(reader.GetNextEntry()); + } + + protected void Read_Archive_Folder_File_Utf8_Internal(TarEntryFormat format, TestTarFormat testFormat) + { + string testCaseName = "folder_file_utf8"; + using MemoryStream ms = GetTarMemoryStream(CompressionMethod.Uncompressed, testFormat, testCaseName); + + using TarReader reader = new TarReader(ms); + + if (testFormat is TestTarFormat.pax_gea) + { + VerifyGlobalExtendedAttributes(reader); + } + + TarEntry directory = reader.GetNextEntry(); + + VerifyDirectoryEntry(directory, format, "f\u00f6ld\u00ebr/"); //földër + + TarEntry file = reader.GetNextEntry(); + VerifyRegularFileEntry(file, format, "f\u00f6ld\u00ebr/\u00e1\u00f6\u00f1.txt", $"Hello {testCaseName}"); // földër/áöñ.txt + + Assert.Null(reader.GetNextEntry()); + } + + protected void Read_Archive_Folder_Subfolder_File_Internal(TarEntryFormat format, TestTarFormat testFormat) + { + string testCaseName = "folder_subfolder_file"; + using MemoryStream ms = GetTarMemoryStream(CompressionMethod.Uncompressed, testFormat, testCaseName); + + using TarReader reader = new TarReader(ms); + + if (testFormat is TestTarFormat.pax_gea) + { + VerifyGlobalExtendedAttributes(reader); + } + + TarEntry parent = reader.GetNextEntry(); + + VerifyDirectoryEntry(parent, format, "parent/"); + + TarEntry child = reader.GetNextEntry(); + VerifyDirectoryEntry(child, format, "parent/child/"); + + TarEntry file = reader.GetNextEntry(); + VerifyRegularFileEntry(file, format, "parent/child/file.txt", $"Hello {testCaseName}"); + + Assert.Null(reader.GetNextEntry()); + } + + protected void Read_Archive_FolderSymbolicLink_Folder_Subfolder_File_Internal(TarEntryFormat format, TestTarFormat testFormat) + { + string testCaseName = "foldersymlink_folder_subfolder_file"; + using MemoryStream ms = GetTarMemoryStream(CompressionMethod.Uncompressed, testFormat, testCaseName); + + using TarReader reader = new TarReader(ms); + + if (testFormat is TestTarFormat.pax_gea) + { + VerifyGlobalExtendedAttributes(reader); + } + + TarEntry childlink = reader.GetNextEntry(); + + VerifySymbolicLinkEntry(childlink, format, "childlink", "parent/child"); + + TarEntry parent = reader.GetNextEntry(); + VerifyDirectoryEntry(parent, format, "parent/"); + + TarEntry child = reader.GetNextEntry(); + VerifyDirectoryEntry(child, format, "parent/child/"); + + TarEntry file = reader.GetNextEntry(); + VerifyRegularFileEntry(file, format, "parent/child/file.txt", $"Hello {testCaseName}"); + + Assert.Null(reader.GetNextEntry()); + } + + protected void Read_Archive_Many_Small_Files_Internal(TarEntryFormat format, TestTarFormat testFormat) + { + string testCaseName = "many_small_files"; + using MemoryStream ms = GetTarMemoryStream(CompressionMethod.Uncompressed, testFormat, testCaseName); + + using TarReader reader = new TarReader(ms); + + if (testFormat is TestTarFormat.pax_gea) + { + VerifyGlobalExtendedAttributes(reader); + } + + List entries = new List(); + TarEntry entry; + bool isFirstEntry = true; + while ((entry = reader.GetNextEntry()) != 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 void Read_Archive_LongPath_Splitable_Under255_Internal(TarEntryFormat format, TestTarFormat testFormat) + { + string testCaseName = "longpath_splitable_under255"; + using MemoryStream ms = GetTarMemoryStream(CompressionMethod.Uncompressed, testFormat, testCaseName); + + using TarReader reader = new TarReader(ms); + + if (testFormat is TestTarFormat.pax_gea) + { + VerifyGlobalExtendedAttributes(reader); + } + + TarEntry directory = reader.GetNextEntry(); + + VerifyDirectoryEntry(directory, format, + "00000000001111111111222222222233333333334444444444555555555566666666667777777777888888888899999999/"); + + TarEntry file = reader.GetNextEntry(); + VerifyRegularFileEntry(file, format, + $"00000000001111111111222222222233333333334444444444555555555566666666667777777777888888888899999999/00000000001111111111222222222233333333334444444444555555555566666666667777777777888888888899999.txt", + $"Hello {testCaseName}"); + + Assert.Null(reader.GetNextEntry()); + } + + protected void Read_Archive_SpecialFiles_Internal(TarEntryFormat format, TestTarFormat testFormat) + { + string testCaseName = "specialfiles"; + using MemoryStream ms = GetTarMemoryStream(CompressionMethod.Uncompressed, testFormat, testCaseName); + + using TarReader reader = new TarReader(ms); + + if (testFormat is TestTarFormat.pax_gea) + { + VerifyGlobalExtendedAttributes(reader); + } + + PosixTarEntry blockDevice = reader.GetNextEntry() as PosixTarEntry; + + VerifyBlockDeviceEntry(blockDevice, format, AssetBlockDeviceFileName); + + PosixTarEntry characterDevice = reader.GetNextEntry() as PosixTarEntry; + VerifyCharacterDeviceEntry(characterDevice, format, AssetCharacterDeviceFileName); + + PosixTarEntry fifo = reader.GetNextEntry() as PosixTarEntry; + VerifyFifoEntry(fifo, format, "fifofile"); + + Assert.Null(reader.GetNextEntry()); + } + + protected void Read_Archive_File_LongSymbolicLink_Internal(TarEntryFormat format, TestTarFormat testFormat) + { + string testCaseName = "file_longsymlink"; + using MemoryStream ms = GetTarMemoryStream(CompressionMethod.Uncompressed, testFormat, testCaseName); + + using TarReader reader = new TarReader(ms); + + if (testFormat is TestTarFormat.pax_gea) + { + VerifyGlobalExtendedAttributes(reader); + } + + TarEntry directory = reader.GetNextEntry(); + + VerifyDirectoryEntry(directory, format, + "000000000011111111112222222222333333333344444444445555555555666666666677777777778888888888999999999900000000001111111111222222222233333333334444444444555555555566666666667777777777888888888899999999990000000000111111111122222222223333333333444444444455555/"); + + TarEntry file = reader.GetNextEntry(); + VerifyRegularFileEntry(file, format, + "000000000011111111112222222222333333333344444444445555555555666666666677777777778888888888999999999900000000001111111111222222222233333333334444444444555555555566666666667777777777888888888899999999990000000000111111111122222222223333333333444444444455555/00000000001111111111222222222233333333334444444444555555555566666666667777777777888888888899999999990000000000111111111122222222223333333333444444444455555555556666666666777777777788888888889999999999000000000011111111112222222222333333333344444444445.txt", + $"Hello {testCaseName}"); + + TarEntry symbolicLink = reader.GetNextEntry(); + VerifySymbolicLinkEntry(symbolicLink, format, + "link.txt", + "000000000011111111112222222222333333333344444444445555555555666666666677777777778888888888999999999900000000001111111111222222222233333333334444444444555555555566666666667777777777888888888899999999990000000000111111111122222222223333333333444444444455555/00000000001111111111222222222233333333334444444444555555555566666666667777777777888888888899999999990000000000111111111122222222223333333333444444444455555555556666666666777777777788888888889999999999000000000011111111112222222222333333333344444444445.txt"); + + Assert.Null(reader.GetNextEntry()); + } + + protected void Read_Archive_LongFileName_Over100_Under255_Internal(TarEntryFormat format, TestTarFormat testFormat) + { + string testCaseName = "longfilename_over100_under255"; + using MemoryStream ms = GetTarMemoryStream(CompressionMethod.Uncompressed, testFormat, testCaseName); + + using TarReader reader = new TarReader(ms); + + if (testFormat is TestTarFormat.pax_gea) + { + VerifyGlobalExtendedAttributes(reader); + } + + TarEntry file = reader.GetNextEntry(); + + VerifyRegularFileEntry(file, format, + "000000000011111111112222222222333333333344444444445555555555666666666677777777778888888888999999999900000000001111111111222222222233333333334444444444.txt", + $"Hello {testCaseName}"); + + Assert.Null(reader.GetNextEntry()); + } + + protected void Read_Archive_LongPath_Over255_Internal(TarEntryFormat format, TestTarFormat testFormat) + { + string testCaseName = "longpath_over255"; + using MemoryStream ms = GetTarMemoryStream(CompressionMethod.Uncompressed, testFormat, testCaseName); + + using TarReader reader = new TarReader(ms); + + if (testFormat is TestTarFormat.pax_gea) + { + VerifyGlobalExtendedAttributes(reader); + } + + TarEntry directory = reader.GetNextEntry(); + + VerifyDirectoryEntry(directory, format, + "000000000011111111112222222222333333333344444444445555555555666666666677777777778888888888999999999900000000001111111111222222222233333333334444444444555555555566666666667777777777888888888899999999990000000000111111111122222222223333333333444444444455555/"); + + TarEntry file = reader.GetNextEntry(); + VerifyRegularFileEntry(file, format, + "000000000011111111112222222222333333333344444444445555555555666666666677777777778888888888999999999900000000001111111111222222222233333333334444444444555555555566666666667777777777888888888899999999990000000000111111111122222222223333333333444444444455555/00000000001111111111222222222233333333334444444444555555555566666666667777777777888888888899999999990000000000111111111122222222223333333333444444444455555555556666666666777777777788888888889999999999000000000011111111112222222222333333333344444444445.txt", + $"Hello {testCaseName}"); + + Assert.Null(reader.GetNextEntry()); + } + + 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.True(entry is PaxGlobalExtendedAttributesTarEntry, "Entry was not PaxGea"); + } + 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(TarReader reader) + { + TarEntry entry = reader.GetNextEntry(); + 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.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.File.Tests.cs index 818e4485bf867..20c4e814decb5 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 @@ -9,26 +9,16 @@ namespace System.Formats.Tar.Tests { - public class TarReader_File_Tests : TarTestsBase + 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)] - public void Read_Archive_File(TarEntryFormat format, TestTarFormat testFormat) - { - string testCaseName = "file"; - using MemoryStream ms = GetTarMemoryStream(CompressionMethod.Uncompressed, testFormat, testCaseName); - - using TarReader reader = new TarReader(ms); - - TarEntry file = reader.GetNextEntry(); - VerifyRegularFileEntry(file, format, "file.txt", $"Hello {testCaseName}"); - - Assert.Null(reader.GetNextEntry()); - } + //[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); [Theory] [InlineData(TarEntryFormat.V7, TestTarFormat.v7)] @@ -36,22 +26,8 @@ public void Read_Archive_File(TarEntryFormat format, TestTarFormat testFormat) [InlineData(TarEntryFormat.Pax, TestTarFormat.pax)] [InlineData(TarEntryFormat.Gnu, TestTarFormat.gnu)] [InlineData(TarEntryFormat.Gnu, TestTarFormat.oldgnu)] - public void Read_Archive_File_HardLink(TarEntryFormat format, TestTarFormat testFormat) - { - string testCaseName = "file_hardlink"; - using MemoryStream ms = GetTarMemoryStream(CompressionMethod.Uncompressed, testFormat, testCaseName); - - using TarReader reader = new TarReader(ms); - - TarEntry file = reader.GetNextEntry(); - VerifyRegularFileEntry(file, format, "file.txt", $"Hello {testCaseName}"); - - // The 'tar' unix tool detects hardlinks as regular files and saves them as such in the archives, for all formats - TarEntry hardLink = reader.GetNextEntry(); - VerifyRegularFileEntry(hardLink, format, "hardlink.txt", $"Hello {testCaseName}"); - - Assert.Null(reader.GetNextEntry()); - } + public void Read_Archive_File_HardLink(TarEntryFormat format, TestTarFormat testFormat) => + Read_Archive_File_HardLink_Internal(format, testFormat); [Theory] [InlineData(TarEntryFormat.V7, TestTarFormat.v7)] @@ -59,21 +35,8 @@ public void Read_Archive_File_HardLink(TarEntryFormat format, TestTarFormat test [InlineData(TarEntryFormat.Pax, TestTarFormat.pax)] [InlineData(TarEntryFormat.Gnu, TestTarFormat.gnu)] [InlineData(TarEntryFormat.Gnu, TestTarFormat.oldgnu)] - public void Read_Archive_File_SymbolicLink(TarEntryFormat format, TestTarFormat testFormat) - { - string testCaseName = "file_symlink"; - using MemoryStream ms = GetTarMemoryStream(CompressionMethod.Uncompressed, testFormat, testCaseName); - - using TarReader reader = new TarReader(ms); - - TarEntry file = reader.GetNextEntry(); - VerifyRegularFileEntry(file, format, "file.txt", $"Hello {testCaseName}"); - - TarEntry symbolicLink = reader.GetNextEntry(); - VerifySymbolicLinkEntry(symbolicLink, format, "link.txt", "file.txt"); - - Assert.Null(reader.GetNextEntry()); - } + public void Read_Archive_File_SymbolicLink(TarEntryFormat format, TestTarFormat testFormat) => + Read_Archive_File_SymbolicLink_Internal(format, testFormat); [Theory] [InlineData(TarEntryFormat.V7, TestTarFormat.v7)] @@ -81,21 +44,8 @@ public void Read_Archive_File_SymbolicLink(TarEntryFormat format, TestTarFormat [InlineData(TarEntryFormat.Pax, TestTarFormat.pax)] [InlineData(TarEntryFormat.Gnu, TestTarFormat.gnu)] [InlineData(TarEntryFormat.Gnu, TestTarFormat.oldgnu)] - public void Read_Archive_Folder_File(TarEntryFormat format, TestTarFormat testFormat) - { - string testCaseName = "folder_file"; - using MemoryStream ms = GetTarMemoryStream(CompressionMethod.Uncompressed, testFormat, testCaseName); - - using TarReader reader = new TarReader(ms); - - TarEntry directory = reader.GetNextEntry(); - VerifyDirectoryEntry(directory, format, "folder/"); - - TarEntry file = reader.GetNextEntry(); - VerifyRegularFileEntry(file, format, "folder/file.txt", $"Hello {testCaseName}"); - - Assert.Null(reader.GetNextEntry()); - } + public void Read_Archive_Folder_File(TarEntryFormat format, TestTarFormat testFormat) => + Read_Archive_Folder_File_Internal(format, testFormat); [Theory] [InlineData(TarEntryFormat.V7, TestTarFormat.v7)] @@ -103,21 +53,8 @@ public void Read_Archive_Folder_File(TarEntryFormat format, TestTarFormat testFo [InlineData(TarEntryFormat.Pax, TestTarFormat.pax)] [InlineData(TarEntryFormat.Gnu, TestTarFormat.gnu)] [InlineData(TarEntryFormat.Gnu, TestTarFormat.oldgnu)] - public void Read_Archive_Folder_File_Utf8(TarEntryFormat format, TestTarFormat testFormat) - { - string testCaseName = "folder_file_utf8"; - using MemoryStream ms = GetTarMemoryStream(CompressionMethod.Uncompressed, testFormat, testCaseName); - - using TarReader reader = new TarReader(ms); - - TarEntry directory = reader.GetNextEntry(); - VerifyDirectoryEntry(directory, format, "földër/"); - - TarEntry file = reader.GetNextEntry(); - VerifyRegularFileEntry(file, format, "földër/áöñ.txt", $"Hello {testCaseName}"); - - Assert.Null(reader.GetNextEntry()); - } + public void Read_Archive_Folder_File_Utf8(TarEntryFormat format, TestTarFormat testFormat) => + Read_Archive_Folder_File_Utf8_Internal(format, testFormat); [Theory] [InlineData(TarEntryFormat.V7, TestTarFormat.v7)] @@ -125,24 +62,8 @@ public void Read_Archive_Folder_File_Utf8(TarEntryFormat format, TestTarFormat t [InlineData(TarEntryFormat.Pax, TestTarFormat.pax)] [InlineData(TarEntryFormat.Gnu, TestTarFormat.gnu)] [InlineData(TarEntryFormat.Gnu, TestTarFormat.oldgnu)] - public void Read_Archive_Folder_Subfolder_File(TarEntryFormat format, TestTarFormat testFormat) - { - string testCaseName = "folder_subfolder_file"; - using MemoryStream ms = GetTarMemoryStream(CompressionMethod.Uncompressed, testFormat, testCaseName); - - using TarReader reader = new TarReader(ms); - - TarEntry parent = reader.GetNextEntry(); - VerifyDirectoryEntry(parent, format, "parent/"); - - TarEntry child = reader.GetNextEntry(); - VerifyDirectoryEntry(child, format, "parent/child/"); - - TarEntry file = reader.GetNextEntry(); - VerifyRegularFileEntry(file, format, "parent/child/file.txt", $"Hello {testCaseName}"); - - Assert.Null(reader.GetNextEntry()); - } + public void Read_Archive_Folder_Subfolder_File(TarEntryFormat format, TestTarFormat testFormat) => + Read_Archive_Folder_Subfolder_File_Internal(format, testFormat); [Theory] [InlineData(TarEntryFormat.V7, TestTarFormat.v7)] @@ -150,27 +71,8 @@ public void Read_Archive_Folder_Subfolder_File(TarEntryFormat format, TestTarFor [InlineData(TarEntryFormat.Pax, TestTarFormat.pax)] [InlineData(TarEntryFormat.Gnu, TestTarFormat.gnu)] [InlineData(TarEntryFormat.Gnu, TestTarFormat.oldgnu)] - public void Read_Archive_FolderSymbolicLink_Folder_Subfolder_File(TarEntryFormat format, TestTarFormat testFormat) - { - string testCaseName = "foldersymlink_folder_subfolder_file"; - using MemoryStream ms = GetTarMemoryStream(CompressionMethod.Uncompressed, testFormat, testCaseName); - - using TarReader reader = new TarReader(ms); - - TarEntry childlink = reader.GetNextEntry(); - VerifySymbolicLinkEntry(childlink, format, "childlink", "parent/child"); - - TarEntry parent = reader.GetNextEntry(); - VerifyDirectoryEntry(parent, format, "parent/"); - - TarEntry child = reader.GetNextEntry(); - VerifyDirectoryEntry(child, format, "parent/child/"); - - TarEntry file = reader.GetNextEntry(); - VerifyRegularFileEntry(file, format, "parent/child/file.txt", $"Hello {testCaseName}"); - - Assert.Null(reader.GetNextEntry()); - } + public void Read_Archive_FolderSymbolicLink_Folder_Subfolder_File(TarEntryFormat format, TestTarFormat testFormat) => + Read_Archive_FolderSymbolicLink_Folder_Subfolder_File_Internal(format, testFormat); [Theory] [InlineData(TarEntryFormat.V7, TestTarFormat.v7)] @@ -178,31 +80,8 @@ public void Read_Archive_FolderSymbolicLink_Folder_Subfolder_File(TarEntryFormat [InlineData(TarEntryFormat.Pax, TestTarFormat.pax)] [InlineData(TarEntryFormat.Gnu, TestTarFormat.gnu)] [InlineData(TarEntryFormat.Gnu, TestTarFormat.oldgnu)] - public void Read_Archive_Many_Small_Files(TarEntryFormat format, TestTarFormat testFormat) - { - string testCaseName = "many_small_files"; - using MemoryStream ms = GetTarMemoryStream(CompressionMethod.Uncompressed, testFormat, testCaseName); - - using TarReader reader = new TarReader(ms); - - List entries = new List(); - TarEntry entry; - while ((entry = reader.GetNextEntry()) != null) - { - Assert.Equal(format, entry.Format); - entries.Add(entry); - } - - int directoriesCount = entries.Count(e => e.EntryType == TarEntryType.Directory); - Assert.Equal(10, directoriesCount); - - TarEntryType regularFileEntryType = format == TarEntryFormat.V7 ? TarEntryType.V7RegularFile : TarEntryType.RegularFile; - for (int i = 0; i < 10; i++) - { - int filesCount = entries.Count(e => e.EntryType == regularFileEntryType && e.Name.StartsWith($"{i}/")); - Assert.Equal(10, filesCount); - } - } + public void Read_Archive_Many_Small_Files(TarEntryFormat format, TestTarFormat testFormat) => + Read_Archive_Many_Small_Files_Internal(format, testFormat); [Theory] // V7 does not support longer filenames @@ -210,23 +89,8 @@ public void Read_Archive_Many_Small_Files(TarEntryFormat format, TestTarFormat t [InlineData(TarEntryFormat.Pax, TestTarFormat.pax)] [InlineData(TarEntryFormat.Gnu, TestTarFormat.gnu)] [InlineData(TarEntryFormat.Gnu, TestTarFormat.oldgnu)] - public void Read_Archive_LongPath_Splitable_Under255(TarEntryFormat format, TestTarFormat testFormat) - { - string testCaseName = "longpath_splitable_under255"; - using MemoryStream ms = GetTarMemoryStream(CompressionMethod.Uncompressed, testFormat, testCaseName); - - using TarReader reader = new TarReader(ms); - - TarEntry directory = reader.GetNextEntry(); - VerifyDirectoryEntry(directory, format, "00000000001111111111222222222233333333334444444444555555555566666666667777777777888888888899999999/"); - - TarEntry file = reader.GetNextEntry(); - VerifyRegularFileEntry(file, format, - $"00000000001111111111222222222233333333334444444444555555555566666666667777777777888888888899999999/00000000001111111111222222222233333333334444444444555555555566666666667777777777888888888899999.txt", - $"Hello {testCaseName}"); - - Assert.Null(reader.GetNextEntry()); - } + public void Read_Archive_LongPath_Splitable_Under255(TarEntryFormat format, TestTarFormat testFormat) => + Read_Archive_LongPath_Splitable_Under255_Internal(format, testFormat); [Theory] // V7 does not support block devices, character devices or fifos @@ -234,335 +98,31 @@ public void Read_Archive_LongPath_Splitable_Under255(TarEntryFormat format, Test [InlineData(TarEntryFormat.Pax, TestTarFormat.pax)] [InlineData(TarEntryFormat.Gnu, TestTarFormat.gnu)] [InlineData(TarEntryFormat.Gnu, TestTarFormat.oldgnu)] - public void Read_Archive_SpecialFiles(TarEntryFormat format, TestTarFormat testFormat) - { - string testCaseName = "specialfiles"; - using MemoryStream ms = GetTarMemoryStream(CompressionMethod.Uncompressed, testFormat, testCaseName); - - using TarReader reader = new TarReader(ms); - - PosixTarEntry blockDevice = reader.GetNextEntry() as PosixTarEntry; - VerifyBlockDeviceEntry(blockDevice, format, AssetBlockDeviceFileName); - - PosixTarEntry characterDevice = reader.GetNextEntry() as PosixTarEntry; - VerifyCharacterDeviceEntry(characterDevice, format, AssetCharacterDeviceFileName); - - PosixTarEntry fifo = reader.GetNextEntry() as PosixTarEntry; - VerifyFifoEntry(fifo, format, "fifofile"); - - Assert.Null(reader.GetNextEntry()); - } + public void Read_Archive_SpecialFiles(TarEntryFormat format, TestTarFormat testFormat) => + Read_Archive_SpecialFiles_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 void Read_Archive_File_LongSymbolicLink(TarEntryFormat format, TestTarFormat testFormat) - { - string testCaseName = "file_longsymlink"; - using MemoryStream ms = GetTarMemoryStream(CompressionMethod.Uncompressed, testFormat, testCaseName); - - using TarReader reader = new TarReader(ms); - - TarEntry directory = reader.GetNextEntry(); - VerifyDirectoryEntry(directory, format, - "000000000011111111112222222222333333333344444444445555555555666666666677777777778888888888999999999900000000001111111111222222222233333333334444444444555555555566666666667777777777888888888899999999990000000000111111111122222222223333333333444444444455555/"); - - TarEntry file = reader.GetNextEntry(); - VerifyRegularFileEntry(file, format, - "000000000011111111112222222222333333333344444444445555555555666666666677777777778888888888999999999900000000001111111111222222222233333333334444444444555555555566666666667777777777888888888899999999990000000000111111111122222222223333333333444444444455555/00000000001111111111222222222233333333334444444444555555555566666666667777777777888888888899999999990000000000111111111122222222223333333333444444444455555555556666666666777777777788888888889999999999000000000011111111112222222222333333333344444444445.txt", - $"Hello {testCaseName}"); - - TarEntry symbolicLink = reader.GetNextEntry(); - VerifySymbolicLinkEntry(symbolicLink, format, - "link.txt", - "000000000011111111112222222222333333333344444444445555555555666666666677777777778888888888999999999900000000001111111111222222222233333333334444444444555555555566666666667777777777888888888899999999990000000000111111111122222222223333333333444444444455555/00000000001111111111222222222233333333334444444444555555555566666666667777777777888888888899999999990000000000111111111122222222223333333333444444444455555555556666666666777777777788888888889999999999000000000011111111112222222222333333333344444444445.txt"); - - Assert.Null(reader.GetNextEntry()); - } + public void Read_Archive_File_LongSymbolicLink(TarEntryFormat format, TestTarFormat testFormat) => + Read_Archive_File_LongSymbolicLink_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 void Read_Archive_LongFileName_Over100_Under255(TarEntryFormat format, TestTarFormat testFormat) - { - string testCaseName = "longfilename_over100_under255"; - using MemoryStream ms = GetTarMemoryStream(CompressionMethod.Uncompressed, testFormat, testCaseName); - - using TarReader reader = new TarReader(ms); - - TarEntry file = reader.GetNextEntry(); - VerifyRegularFileEntry(file, format, "000000000011111111112222222222333333333344444444445555555555666666666677777777778888888888999999999900000000001111111111222222222233333333334444444444.txt", $"Hello {testCaseName}"); - - Assert.Null(reader.GetNextEntry()); - } + public void Read_Archive_LongFileName_Over100_Under255(TarEntryFormat format, TestTarFormat testFormat) => + Read_Archive_LongFileName_Over100_Under255_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 void Read_Archive_LongPath_Over255(TarEntryFormat format, TestTarFormat testFormat) - { - string testCaseName = "longpath_over255"; - using MemoryStream ms = GetTarMemoryStream(CompressionMethod.Uncompressed, testFormat, testCaseName); - - using TarReader reader = new TarReader(ms); - - TarEntry directory = reader.GetNextEntry(); - VerifyDirectoryEntry(directory, format, - "000000000011111111112222222222333333333344444444445555555555666666666677777777778888888888999999999900000000001111111111222222222233333333334444444444555555555566666666667777777777888888888899999999990000000000111111111122222222223333333333444444444455555/"); - - TarEntry file = reader.GetNextEntry(); - VerifyRegularFileEntry(file, format, - "000000000011111111112222222222333333333344444444445555555555666666666677777777778888888888999999999900000000001111111111222222222233333333334444444444555555555566666666667777777777888888888899999999990000000000111111111122222222223333333333444444444455555/00000000001111111111222222222233333333334444444444555555555566666666667777777777888888888899999999990000000000111111111122222222223333333333444444444455555555556666666666777777777788888888889999999999000000000011111111112222222222333333333344444444445.txt", - $"Hello {testCaseName}"); - - Assert.Null(reader.GetNextEntry()); - } - - protected void VerifyRegularFileEntry(TarEntry file, TarEntryFormat format, string expectedFileName, string expectedContents) - { - Assert.NotNull(file); - Assert.Equal(format, 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); - } - } - } - - protected void VerifySymbolicLinkEntry(TarEntry symbolicLink, TarEntryFormat format, string expectedFileName, string expectedTargetName) - { - Assert.NotNull(symbolicLink); - Assert.Equal(format, 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); - } - } - - protected void VerifyDirectoryEntry(TarEntry directory, TarEntryFormat format, string expectedFileName) - { - Assert.NotNull(directory); - Assert.Equal(format, 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); - } - } - - protected void VerifyBlockDeviceEntry(PosixTarEntry blockDevice, TarEntryFormat format, string expectedFileName) - { - Assert.NotNull(blockDevice); - Assert.Equal(TarEntryType.BlockDevice, blockDevice.EntryType); - Assert.Equal(format, 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); - - // TODO: Figure out why the numbers don't match https://github.com/dotnet/runtime/issues/68230 - // Assert.Equal(AssetBlockDeviceMajor, blockDevice.DeviceMajor); - // Assert.Equal(AssetBlockDeviceMinor, blockDevice.DeviceMinor); - // Remove these two temporary checks when the above is fixed - Assert.True(blockDevice.DeviceMajor > 0); - Assert.True(blockDevice.DeviceMinor > 0); - 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); - } - } - - protected void VerifyCharacterDeviceEntry(PosixTarEntry characterDevice, TarEntryFormat format, string expectedFileName) - { - Assert.NotNull(characterDevice); - Assert.Equal(TarEntryType.CharacterDevice, characterDevice.EntryType); - Assert.Equal(format, 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); - - // TODO: Figure out why the numbers don't match https://github.com/dotnet/runtime/issues/68230 - //Assert.Equal(AssetBlockDeviceMajor, characterDevice.DeviceMajor); - //Assert.Equal(AssetBlockDeviceMinor, characterDevice.DeviceMinor); - // Remove these two temporary checks when the above is fixed - Assert.True(characterDevice.DeviceMajor > 0); - Assert.True(characterDevice.DeviceMinor > 0); - 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); - } - } - - protected void VerifyFifoEntry(PosixTarEntry fifo, TarEntryFormat format, string expectedFileName) - { - Assert.NotNull(fifo); - Assert.Equal(format, 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 VerifyExtendedAttributes(PaxTarEntry pax) - { - Assert.NotNull(pax.ExtendedAttributes); - Assert.Equal(TarEntryFormat.Pax, pax.Format); - 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) - { - Assert.Equal(TarEntryFormat.Gnu, gnu.Format); - AssertExtensions.GreaterThanOrEqualTo(gnu.AccessTime, DateTimeOffset.UnixEpoch); - AssertExtensions.GreaterThanOrEqualTo(gnu.ChangeTime, DateTimeOffset.UnixEpoch); - } + public void Read_Archive_LongPath_Over255(TarEntryFormat format, TestTarFormat testFormat) => + Read_Archive_LongPath_Over255_Internal(format, testFormat); } } diff --git a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.Tests.cs index dbaeff9419a89..3e080ab37bbd2 100644 --- a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.Tests.cs @@ -50,11 +50,8 @@ public void Constructor_Format() using TarWriter writerGnu = new TarWriter(archiveStream, TarEntryFormat.Gnu, leaveOpen: true); Assert.Equal(TarEntryFormat.Gnu, writerGnu.Format); - using TarWriter writerNullGeaDefaultPax = new TarWriter(archiveStream, leaveOpen: true, globalExtendedAttributes: null); - Assert.Equal(TarEntryFormat.Pax, writerNullGeaDefaultPax.Format); - - using TarWriter writerValidGeaDefaultPax = new TarWriter(archiveStream, leaveOpen: true, globalExtendedAttributes: new Dictionary()); - Assert.Equal(TarEntryFormat.Pax, writerValidGeaDefaultPax.Format); + using TarWriter writerNoFormat = new TarWriter(archiveStream, leaveOpen: true); + Assert.Equal(TarEntryFormat.Pax, writerNoFormat.Format); Assert.Throws(() => new TarWriter(archiveStream, TarEntryFormat.Unknown)); Assert.Throws(() => new TarWriter(archiveStream, (TarEntryFormat)int.MinValue)); diff --git a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.Entry.Pax.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.Entry.Pax.Tests.cs index 739ec41049896..04dbf264f8853 100644 --- a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.Entry.Pax.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.Entry.Pax.Tests.cs @@ -12,7 +12,7 @@ namespace System.Formats.Tar.Tests public class TarWriter_WriteEntry_Pax_Tests : TarTestsBase { [Fact] - public void Write_V7RegularFileEntry_As_RegularFileEntry() + public void Write_V7RegularFile_To_PaxArchive() { using MemoryStream archive = new MemoryStream(); using (TarWriter writer = new TarWriter(archive, format: TarEntryFormat.Pax, leaveOpen: true)) @@ -30,6 +30,7 @@ public void Write_V7RegularFileEntry_As_RegularFileEntry() Assert.NotNull(entry); Assert.Equal(TarEntryFormat.V7, entry.Format); Assert.True(entry is V7TarEntry); + Assert.Equal(TarEntryType.V7RegularFile, entry.EntryType); Assert.Null(reader.GetNextEntry()); } @@ -369,5 +370,30 @@ public void WritePaxAttributes_LongLinkName_AutomaticallyAdded() Assert.Equal(longHardLinkName, hardlink.ExtendedAttributes[PaxEaLinkName]); } } + + [Fact] + public void Add_Empty_GlobalExtendedAttributes() + { + using MemoryStream archive = new MemoryStream(); + + using (TarWriter writer = new TarWriter(archive, leaveOpen: true)) + { + PaxGlobalExtendedAttributesTarEntry gea = new PaxGlobalExtendedAttributesTarEntry(new Dictionary()); + writer.WriteEntry(gea); + } + + archive.Seek(0, SeekOrigin.Begin); + using (TarReader reader = new TarReader(archive)) + { + PaxGlobalExtendedAttributesTarEntry gea = reader.GetNextEntry() as PaxGlobalExtendedAttributesTarEntry; + Assert.NotNull(gea); + Assert.Equal(TarEntryFormat.Pax, gea.Format); + Assert.Equal(TarEntryType.GlobalExtendedAttributes, gea.EntryType); + + Assert.Equal(0, gea.GlobalExtendedAttributes.Count); + + Assert.Null(reader.GetNextEntry()); + } + } } } diff --git a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.Tests.cs index 32c2ec66a9000..2fad0f26f0449 100644 --- a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.Tests.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.Generic; using System.IO; using Xunit; @@ -85,5 +86,77 @@ public void WriteEntry_RespectDefaultWriterFormat(TarEntryFormat expectedFormat) Assert.Equal(expectedType, entry.GetType()); } } + + [Theory] + [InlineData(TarEntryFormat.V7)] + [InlineData(TarEntryFormat.Ustar)] + [InlineData(TarEntryFormat.Pax)] + [InlineData(TarEntryFormat.Gnu)] + public void ReadAndWriteMultipleGlobalExtendedAttributesEntries(TarEntryFormat format) + { + Dictionary attrs = new Dictionary() + { + { "hello", "world" }, + { "dotnet", "runtime" } + }; + + using MemoryStream archiveStream = new MemoryStream(); + using (TarWriter writer = new TarWriter(archiveStream, leaveOpen: true)) + { + PaxGlobalExtendedAttributesTarEntry gea1 = new PaxGlobalExtendedAttributesTarEntry(attrs); + writer.WriteEntry(gea1); + + TarEntry entry1 = ConstructEntry(format, "dir1"); + writer.WriteEntry(entry1); + + PaxGlobalExtendedAttributesTarEntry gea2 = new PaxGlobalExtendedAttributesTarEntry(attrs); + writer.WriteEntry(gea2); + + TarEntry entry2 = ConstructEntry(format, "dir2"); + writer.WriteEntry(entry2); + } + + archiveStream.Position = 0; + + using (TarReader reader = new TarReader(archiveStream, leaveOpen: false)) + { + VerifyGlobalExtendedAttributesEntry(reader.GetNextEntry(), attrs); + VerifyDirEntry(reader.GetNextEntry(), format, "dir1"); + VerifyGlobalExtendedAttributesEntry(reader.GetNextEntry(), attrs); + VerifyDirEntry(reader.GetNextEntry(), format, "dir2"); + Assert.Null(reader.GetNextEntry()); + } + } + + private TarEntry ConstructEntry(TarEntryFormat format, string name) => + format switch + { + TarEntryFormat.V7 => new V7TarEntry(TarEntryType.Directory, name), + TarEntryFormat.Ustar => new UstarTarEntry(TarEntryType.Directory, name), + TarEntryFormat.Pax => new PaxTarEntry(TarEntryType.Directory, name), + TarEntryFormat.Gnu => new GnuTarEntry(TarEntryType.Directory, name), + _ => throw new Exception($"Unexpected format {format}"), + }; + + private void VerifyDirEntry(TarEntry entry, TarEntryFormat format, string name) + { + Assert.NotNull(entry); + Assert.Equal(format, entry.Format); + Assert.Equal(TarEntryType.Directory, entry.EntryType); + Assert.Equal(name, entry.Name); + } + + private void VerifyGlobalExtendedAttributesEntry(TarEntry entry, Dictionary attrs) + { + PaxGlobalExtendedAttributesTarEntry gea = entry as PaxGlobalExtendedAttributesTarEntry; + Assert.NotNull(gea); + Assert.Equal(attrs.Count, gea.GlobalExtendedAttributes.Count); + + foreach ((string key, string value) in attrs) + { + Assert.Contains(key, gea.GlobalExtendedAttributes); + Assert.Equal(value, gea.GlobalExtendedAttributes[key]); + } + } } }