diff --git a/src/libraries/System.Formats.Tar/src/Resources/Strings.resx b/src/libraries/System.Formats.Tar/src/Resources/Strings.resx index 5308e9153c9791..bfcb38c77a1351 100644 --- a/src/libraries/System.Formats.Tar/src/Resources/Strings.resx +++ b/src/libraries/System.Formats.Tar/src/Resources/Strings.resx @@ -234,6 +234,9 @@ A POSIX format was expected (Ustar or PAX), but could not be reliably determined for entry '{0}'. + + The extended attributes dictionary cannot contain the reserved key '{0}'. + The size field is negative in a tar entry. 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 11e3b77c5f8f2d..aa8b88034ac71a 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 @@ -33,8 +33,8 @@ internal GnuTarEntry(TarHeader header, TarReader readerOfOrigin) public GnuTarEntry(TarEntryType entryType, string entryName) : base(entryType, entryName, TarEntryFormat.Gnu, isGea: false) { - _header._aTime = _header._mTime; // mtime was set in base constructor - _header._cTime = _header._mTime; + _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 index 832996693624f1..996e018ed0a8a6 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/PaxGlobalExtendedAttributesTarEntry.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/PaxGlobalExtendedAttributesTarEntry.cs @@ -29,7 +29,7 @@ public PaxGlobalExtendedAttributesTarEntry(IEnumerable 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 555e4feaa27f73..6460d145d6d221 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 @@ -54,7 +54,7 @@ public PaxTarEntry(TarEntryType entryType, string entryName) { _header._prefix = string.Empty; - Debug.Assert(_header._mTime != default); + Debug.Assert(_header.MTime != default); AddNewAccessAndChangeTimestampsIfNotExist(useMTime: true); } @@ -94,9 +94,9 @@ public PaxTarEntry(TarEntryType entryType, string entryName, IEnumerable @@ -99,11 +99,11 @@ public int DeviceMinor /// is only used in Unix platforms. public string GroupName { - get => _header._gName ?? string.Empty; + get => _header.GName ?? string.Empty; set { ArgumentNullException.ThrowIfNull(value); - _header._gName = value; + _header.GName = value; } } @@ -114,11 +114,11 @@ public string GroupName /// Cannot set a null user name. public string UserName { - get => _header._uName ?? string.Empty; + get => _header.UName ?? string.Empty; set { ArgumentNullException.ThrowIfNull(value); - _header._uName = value; + _header.UName = value; } } } 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 a27df41f4c1c6a..1c514ceca90280 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 @@ -18,7 +18,7 @@ public abstract partial class TarEntry internal TarHeader _header; // Used to access the data section of this entry in an unseekable file - private TarReader? _readerOfOrigin; + internal TarReader? _readerOfOrigin; // Constructor called when reading a TarEntry from a TarReader. internal TarEntry(TarHeader header, TarReader readerOfOrigin, TarEntryFormat format) @@ -95,14 +95,14 @@ public int Gid /// The specified value is larger than . public DateTimeOffset ModificationTime { - get => _header._mTime; + get => _header.MTime; set { if (value < DateTimeOffset.UnixEpoch) { throw new ArgumentOutOfRangeException(nameof(value)); } - _header._mTime = value; + _header.MTime = value; } } @@ -110,7 +110,7 @@ public DateTimeOffset ModificationTime /// When the indicates an entry that can contain data, this property returns the length in bytes of such data. /// /// The entry type that commonly contains data is (or in the format). Other uncommon entry types that can also contain data are: , , and . - public long Length => _header._dataStream != null ? _header._dataStream.Length : _header._size; + public long Length => _header._dataStream != null ? _header._dataStream.Length : _header.Size; /// /// When the indicates a or a , this property returns the link target path of such link. @@ -120,7 +120,7 @@ public DateTimeOffset ModificationTime /// The specified value is empty. public string LinkName { - get => _header._linkName ?? string.Empty; + get => _header.LinkName ?? string.Empty; set { if (_header._typeFlag is not TarEntryType.HardLink and not TarEntryType.SymbolicLink) @@ -128,7 +128,7 @@ public string LinkName throw new InvalidOperationException(SR.TarEntryHardLinkOrSymLinkExpected); } ArgumentException.ThrowIfNullOrEmpty(value); - _header._linkName = value; + _header.LinkName = value; } } @@ -154,11 +154,11 @@ public UnixFileMode Mode /// public string Name { - get => _header._name; + get => _header.Name; set { ArgumentException.ThrowIfNullOrEmpty(value); - _header._name = value; + _header.Name = value; } } 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 ce37a74c304c80..d835e169ea5fd1 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 @@ -104,7 +104,7 @@ internal void ReplaceNormalAttributesWithExtended(Dictionary? di return; } - InitializeExtendedAttributesWithExisting(dictionaryFromExtendedAttributesHeader); + InitializeExtendedAttributesWithExisting(dictionaryFromExtendedAttributesHeader, allowReservedKeys: true); // Find all the extended attributes with known names and save them in the expected standard attribute. @@ -112,18 +112,21 @@ internal void ReplaceNormalAttributesWithExtended(Dictionary? di if (ExtendedAttributes.TryGetValue(PaxEaName, out string? paxEaName)) { _name = paxEaName; + _isPaxEaNameSynced = true; } // The 'linkName' header field only fits 100 bytes, so we always store the full linkName text to the dictionary. if (ExtendedAttributes.TryGetValue(PaxEaLinkName, out string? paxEaLinkName)) { _linkName = paxEaLinkName; + _isPaxEaLinkNameSynced = true; } // The 'mtime' header field only fits 12 bytes, so a more precise timestamp goes in the extended attributes if (TarHelpers.TryGetDateTimeOffsetFromTimestampString(ExtendedAttributes, PaxEaMTime, out DateTimeOffset mTime)) { _mTime = mTime; + _isPaxEaLinkNameSynced = true; } // The user could've stored an override in the extended attributes @@ -136,6 +139,7 @@ internal void ReplaceNormalAttributesWithExtended(Dictionary? di if (TarHelpers.TryGetStringAsBaseTenLong(ExtendedAttributes, PaxEaSize, out long size)) { _size = size; + _isPaxEaSizeSynced = true; } // The 'uid' header field only fits 8 bytes, or the user could've stored an override in the extended attributes @@ -154,12 +158,14 @@ internal void ReplaceNormalAttributesWithExtended(Dictionary? di if (ExtendedAttributes.TryGetValue(PaxEaUName, out string? paxEaUName)) { _uName = paxEaUName; + _isPaxEaUNameSynced = true; } // The 'gname' header field only fits 32 bytes if (ExtendedAttributes.TryGetValue(PaxEaGName, out string? paxEaGName)) { _gName = paxEaGName; + _isPaxEaGNameSynced = true; } // The 'devmajor' header field only fits 8 bytes, or the user could've stored an override in the extended attributes @@ -199,7 +205,7 @@ internal void ProcessDataBlock(Stream archiveStream, bool copyData) case TarEntryType.HardLink: case TarEntryType.SymbolicLink: // No data section - if (_size > 0) + if (Size > 0) { throw new InvalidDataException(string.Format(SR.TarSizeFieldTooLargeForEntryType, _typeFlag)); } @@ -216,7 +222,7 @@ internal void ProcessDataBlock(Stream archiveStream, bool copyData) _dataStream = GetDataStream(archiveStream, copyData); if (_dataStream is SeekableSubReadStream) { - TarHelpers.AdvanceStream(archiveStream, _size); + TarHelpers.AdvanceStream(archiveStream, Size); } else if (_dataStream is SubReadStream) { @@ -230,9 +236,9 @@ internal void ProcessDataBlock(Stream archiveStream, bool copyData) if (skipBlockAlignmentPadding) { - if (_size > 0) + if (Size > 0) { - TarHelpers.SkipBlockAlignmentPadding(archiveStream, _size); + TarHelpers.SkipBlockAlignmentPadding(archiveStream, Size); } if (archiveStream.CanSeek) @@ -261,7 +267,7 @@ private async Task ProcessDataBlockAsync(Stream archiveStream, bool copyData, Ca case TarEntryType.HardLink: case TarEntryType.SymbolicLink: // No data section - if (_size > 0) + if (Size > 0) { throw new InvalidDataException(string.Format(SR.TarSizeFieldTooLargeForEntryType, _typeFlag)); } @@ -275,10 +281,10 @@ private async Task ProcessDataBlockAsync(Stream archiveStream, bool copyData, Ca 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, _size, cancellationToken).ConfigureAwait(false); + _dataStream = await GetDataStreamAsync(archiveStream, copyData, Size, cancellationToken).ConfigureAwait(false); if (_dataStream is SeekableSubReadStream) { - await TarHelpers.AdvanceStreamAsync(archiveStream, _size, cancellationToken).ConfigureAwait(false); + await TarHelpers.AdvanceStreamAsync(archiveStream, Size, cancellationToken).ConfigureAwait(false); } else if (_dataStream is SubReadStream) { @@ -292,9 +298,9 @@ private async Task ProcessDataBlockAsync(Stream archiveStream, bool copyData, Ca if (skipBlockAlignmentPadding) { - if (_size > 0) + if (Size > 0) { - await TarHelpers.SkipBlockAlignmentPaddingAsync(archiveStream, _size, cancellationToken).ConfigureAwait(false); + await TarHelpers.SkipBlockAlignmentPaddingAsync(archiveStream, Size, cancellationToken).ConfigureAwait(false); } if (archiveStream.CanSeek) @@ -310,7 +316,7 @@ private async Task ProcessDataBlockAsync(Stream archiveStream, bool copyData, Ca // Otherwise, it returns an unseekable wrapper stream. private Stream? GetDataStream(Stream archiveStream, bool copyData) { - if (_size == 0) + if (Size == 0) { return null; } @@ -318,15 +324,15 @@ private async Task ProcessDataBlockAsync(Stream archiveStream, bool copyData, Ca if (copyData) { MemoryStream copiedData = new MemoryStream(); - TarHelpers.CopyBytes(archiveStream, copiedData, _size); + TarHelpers.CopyBytes(archiveStream, copiedData, Size); // Reset position pointer so the user can do the first DataStream read from the beginning copiedData.Position = 0; return copiedData; } 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); } // Asynchronously returns a stream that represents the data section of the current header. @@ -390,10 +396,10 @@ private async Task ProcessDataBlockAsync(Stream archiveStream, bool copyData, Ca typeFlag: (TarEntryType)buffer[FieldLocations.TypeFlag]) { _checksum = checksum, - _size = size, + Size = size, _uid = (int)TarHelpers.ParseOctal(buffer.Slice(FieldLocations.Uid, FieldLengths.Uid)), _gid = (int)TarHelpers.ParseOctal(buffer.Slice(FieldLocations.Gid, FieldLengths.Gid)), - _linkName = TarHelpers.GetTrimmedUtf8String(buffer.Slice(FieldLocations.LinkName, FieldLengths.LinkName)) + LinkName = TarHelpers.GetTrimmedUtf8String(buffer.Slice(FieldLocations.LinkName, FieldLengths.LinkName)) }; if (header._format == TarEntryFormat.Unknown) @@ -477,7 +483,7 @@ private void ReadVersionAttribute(Span buffer) // Check for gnu version header for mixed case if (!version.SequenceEqual(GnuVersionBytes)) { - throw new InvalidDataException(string.Format(SR.TarPosixFormatExpected, _name)); + throw new InvalidDataException(string.Format(SR.TarPosixFormatExpected, Name)); } _version = GnuVersion; @@ -495,7 +501,7 @@ private void ReadVersionAttribute(Span buffer) // Check for ustar or pax version header for mixed case if (!version.SequenceEqual(UstarVersionBytes)) { - throw new InvalidDataException(string.Format(SR.TarGnuFormatExpected, _name)); + throw new InvalidDataException(string.Format(SR.TarGnuFormatExpected, Name)); } _version = UstarVersion; @@ -517,8 +523,8 @@ private void ReadVersionAttribute(Span buffer) private void ReadPosixAndGnuSharedAttributes(Span buffer) { // Convert the byte arrays - _uName = TarHelpers.GetTrimmedAsciiString(buffer.Slice(FieldLocations.UName, FieldLengths.UName)); - _gName = TarHelpers.GetTrimmedAsciiString(buffer.Slice(FieldLocations.GName, FieldLengths.GName)); + UName = TarHelpers.GetTrimmedAsciiString(buffer.Slice(FieldLocations.UName, FieldLengths.UName)); + GName = TarHelpers.GetTrimmedAsciiString(buffer.Slice(FieldLocations.GName, FieldLengths.GName)); // DevMajor and DevMinor only have values with character devices and block devices. // For all other typeflags, the values in these fields are irrelevant. @@ -558,7 +564,7 @@ private void ReadUstarAttributes(Span buffer) { // Prefix never has a leading separator, so we add it // it should always be a forward slash for compatibility - _name = string.Format(UstarPrefixFormat, _prefix, _name); + Name = string.Format(UstarPrefixFormat, _prefix, Name); } } @@ -566,18 +572,18 @@ private void ReadUstarAttributes(Span buffer) // Throws if end of stream is reached or if an attribute is malformed. private void ReadExtendedAttributesBlock(Stream archiveStream) { - if (_size != 0) + if (Size != 0) { ValidateSize(); byte[]? buffer = null; - Span span = _size <= 256 ? + Span span = Size <= 256 ? stackalloc byte[256] : - (buffer = ArrayPool.Shared.Rent((int)_size)); - span = span.Slice(0, (int)_size); + (buffer = ArrayPool.Shared.Rent((int)Size)); + span = span.Slice(0, (int)Size); archiveStream.ReadExactly(span); - ReadExtendedAttributesFromBuffer(span, _name); + ReadExtendedAttributesFromBuffer(span, Name); if (buffer is not null) { @@ -592,14 +598,14 @@ private async ValueTask ReadExtendedAttributesBlockAsync(Stream archiveStream, C { cancellationToken.ThrowIfCancellationRequested(); - if (_size != 0) + if (Size != 0) { ValidateSize(); - byte[] buffer = ArrayPool.Shared.Rent((int)_size); - Memory memory = buffer.AsMemory(0, (int)_size); + byte[] buffer = ArrayPool.Shared.Rent((int)Size); + Memory memory = buffer.AsMemory(0, (int)Size); await archiveStream.ReadExactlyAsync(memory, cancellationToken).ConfigureAwait(false); - ReadExtendedAttributesFromBuffer(memory.Span, _name); + ReadExtendedAttributesFromBuffer(memory.Span, Name); ArrayPool.Shared.Return(buffer); } @@ -607,7 +613,7 @@ private async ValueTask ReadExtendedAttributesBlockAsync(Stream archiveStream, C private void ValidateSize() { - if ((uint)_size > (uint)Array.MaxLength) + if ((uint)Size > (uint)Array.MaxLength) { ThrowSizeFieldTooLarge(); } @@ -636,15 +642,15 @@ private void ReadExtendedAttributesFromBuffer(ReadOnlySpan buffer, string // Throws if end of stream is reached. private void ReadGnuLongPathDataBlock(Stream archiveStream) { - if (_size != 0) + if (Size != 0) { ValidateSize(); byte[]? buffer = null; - Span span = _size <= 256 ? + Span span = Size <= 256 ? stackalloc byte[256] : - (buffer = ArrayPool.Shared.Rent((int)_size)); - span = span.Slice(0, (int)_size); + (buffer = ArrayPool.Shared.Rent((int)Size)); + span = span.Slice(0, (int)Size); archiveStream.ReadExactly(span); ReadGnuLongPathDataFromBuffer(span); @@ -663,11 +669,11 @@ private async ValueTask ReadGnuLongPathDataBlockAsync(Stream archiveStream, Canc { cancellationToken.ThrowIfCancellationRequested(); - if (_size != 0) + if (Size != 0) { ValidateSize(); - byte[] buffer = ArrayPool.Shared.Rent((int)_size); - Memory memory = buffer.AsMemory(0, (int)_size); + byte[] buffer = ArrayPool.Shared.Rent((int)Size); + Memory memory = buffer.AsMemory(0, (int)Size); await archiveStream.ReadExactlyAsync(memory, cancellationToken).ConfigureAwait(false); ReadGnuLongPathDataFromBuffer(memory.Span); @@ -683,11 +689,11 @@ private void ReadGnuLongPathDataFromBuffer(ReadOnlySpan buffer) if (_typeFlag == TarEntryType.LongLink) { - _linkName = longPath; + LinkName = longPath; } else if (_typeFlag == TarEntryType.LongPath) { - _name = longPath; + Name = longPath; } } 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 43fe79ce7dc0a6..0a2632614f9ef1 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 @@ -145,7 +145,7 @@ internal void WriteAsPax(Stream archiveStream, Span buffer) // First, we write the preceding extended attributes header TarHeader extendedAttributesHeader = new(TarEntryFormat.Pax); // Fill the current header's dict - CollectExtendedAttributesFromStandardFieldsIfNeeded(); + CollectExtendedAttributesFromStandardFields(); // And pass the attributes to the preceding extended attributes header for writing extendedAttributesHeader.WriteAsPaxExtendedAttributes(archiveStream, buffer, ExtendedAttributes, isGea: false, globalExtendedAttributesEntryNumber: -1); buffer.Clear(); // Reset it to reuse it @@ -164,7 +164,7 @@ internal async Task WriteAsPaxAsync(Stream archiveStream, Memory buffer, C // First, we write the preceding extended attributes header TarHeader extendedAttributesHeader = new(TarEntryFormat.Pax); // Fill the current header's dict - CollectExtendedAttributesFromStandardFieldsIfNeeded(); + CollectExtendedAttributesFromStandardFields(); // And pass the attributes to the preceding extended attributes header for writing await extendedAttributesHeader.WriteAsPaxExtendedAttributesAsync(archiveStream, buffer, ExtendedAttributes, isGea: false, globalExtendedAttributesEntryNumber: -1, cancellationToken).ConfigureAwait(false); @@ -178,17 +178,17 @@ internal async Task WriteAsPaxAsync(Stream archiveStream, Memory buffer, C internal void WriteAsGnu(Stream archiveStream, Span buffer) { // First, we determine if we need a preceding LongLink, and write it if needed - if (_linkName?.Length > FieldLengths.LinkName) + if (LinkName?.Length > FieldLengths.LinkName) { - TarHeader longLinkHeader = GetGnuLongMetadataHeader(TarEntryType.LongLink, _linkName); + TarHeader longLinkHeader = GetGnuLongMetadataHeader(TarEntryType.LongLink, LinkName); longLinkHeader.WriteAsGnuInternal(archiveStream, buffer); buffer.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) + if (Name.Length > FieldLengths.Name) { - TarHeader longPathHeader = GetGnuLongMetadataHeader(TarEntryType.LongPath, _name); + TarHeader longPathHeader = GetGnuLongMetadataHeader(TarEntryType.LongPath, Name); longPathHeader.WriteAsGnuInternal(archiveStream, buffer); buffer.Clear(); // Reset it to reuse it } @@ -204,17 +204,17 @@ internal async Task WriteAsGnuAsync(Stream archiveStream, Memory buffer, C cancellationToken.ThrowIfCancellationRequested(); // First, we determine if we need a preceding LongLink, and write it if needed - if (_linkName?.Length > FieldLengths.LinkName) + if (LinkName?.Length > FieldLengths.LinkName) { - TarHeader longLinkHeader = GetGnuLongMetadataHeader(TarEntryType.LongLink, _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) + if (Name.Length > FieldLengths.Name) { - TarHeader longPathHeader = GetGnuLongMetadataHeader(TarEntryType.LongPath, _name); + TarHeader longPathHeader = GetGnuLongMetadataHeader(TarEntryType.LongPath, Name); await longPathHeader.WriteAsGnuInternalAsync(archiveStream, buffer, cancellationToken).ConfigureAwait(false); buffer.Span.Clear(); // Reset it to reuse it } @@ -231,11 +231,11 @@ private static TarHeader GetGnuLongMetadataHeader(TarEntryType entryType, string TarHeader longMetadataHeader = new(TarEntryFormat.Gnu); - longMetadataHeader._name = GnuLongMetadataName; // Same name for both longpath or longlink + longMetadataHeader.Name = GnuLongMetadataName; // Same name for both longpath or longlink longMetadataHeader._mode = TarHelpers.GetDefaultMode(entryType); longMetadataHeader._uid = 0; longMetadataHeader._gid = 0; - longMetadataHeader._mTime = DateTimeOffset.MinValue; // 0 + longMetadataHeader.MTime = DateTimeOffset.MinValue; // 0 longMetadataHeader._typeFlag = entryType; longMetadataHeader._dataStream = new MemoryStream(Encoding.UTF8.GetBytes(longText)); @@ -307,7 +307,7 @@ private void WriteAsPaxExtendedAttributesShared(bool isGea, int globalExtendedAt { Debug.Assert(isGea && globalExtendedAttributesEntryNumber >= 0 || !isGea && globalExtendedAttributesEntryNumber < 0); - _name = isGea ? + Name = isGea ? GenerateGlobalExtendedAttributeName(globalExtendedAttributesEntryNumber) : GenerateExtendedAttributeName(); @@ -361,7 +361,7 @@ private void WriteAsPaxSharedInternal(Span buffer, out long actualLength) // All formats save in the name byte array only the ASCII bytes that fit. private int WriteName(Span buffer) { - ReadOnlySpan src = _name.AsSpan(0, Math.Min(_name.Length, FieldLengths.Name)); + ReadOnlySpan src = Name.AsSpan(0, Math.Min(Name.Length, FieldLengths.Name)); Span dest = buffer.Slice(FieldLocations.Name, FieldLengths.Name); int encoded = Encoding.ASCII.GetBytes(src, dest); return Checksum(dest.Slice(0, encoded)); @@ -372,11 +372,11 @@ private int WritePosixName(Span buffer) { int checksum = WriteName(buffer); - if (_name.Length > FieldLengths.Name) + if (Name.Length > FieldLengths.Name) { - int prefixBytesLength = Math.Min(_name.Length - FieldLengths.Name, FieldLengths.Prefix); + int prefixBytesLength = Math.Min(Name.Length - FieldLengths.Name, FieldLengths.Prefix); Span remaining = stackalloc byte[prefixBytesLength]; - int encoded = Encoding.ASCII.GetBytes(_name.AsSpan(FieldLengths.Name, prefixBytesLength), remaining); + int encoded = Encoding.ASCII.GetBytes(Name.AsSpan(FieldLengths.Name, prefixBytesLength), remaining); Debug.Assert(encoded == remaining.Length); checksum += WriteLeftAlignedBytesAndGetChecksum(remaining, buffer.Slice(FieldLocations.Prefix, FieldLengths.Prefix)); @@ -408,22 +408,22 @@ private int WriteCommonFields(Span buffer, long actualLength, TarEntryType checksum += FormatOctal(_gid, buffer.Slice(FieldLocations.Gid, FieldLengths.Gid)); } - _size = actualLength; + Size = actualLength; - if (_size > 0) + if (Size > 0) { - checksum += FormatOctal(_size, buffer.Slice(FieldLocations.Size, FieldLengths.Size)); + checksum += FormatOctal(Size, buffer.Slice(FieldLocations.Size, FieldLengths.Size)); } - checksum += WriteAsTimestamp(_mTime, buffer.Slice(FieldLocations.MTime, FieldLengths.MTime)); + checksum += WriteAsTimestamp(MTime, buffer.Slice(FieldLocations.MTime, FieldLengths.MTime)); char typeFlagChar = (char)actualEntryType; buffer[FieldLocations.TypeFlag] = (byte)typeFlagChar; checksum += typeFlagChar; - if (!string.IsNullOrEmpty(_linkName)) + if (!string.IsNullOrEmpty(LinkName)) { - checksum += WriteAsAsciiString(_linkName, buffer.Slice(FieldLocations.LinkName, FieldLengths.LinkName)); + checksum += WriteAsAsciiString(LinkName, buffer.Slice(FieldLocations.LinkName, FieldLengths.LinkName)); } return checksum; @@ -465,14 +465,14 @@ private int WritePosixAndGnuSharedFields(Span buffer) { int checksum = 0; - if (!string.IsNullOrEmpty(_uName)) + if (!string.IsNullOrEmpty(UName)) { - checksum += WriteAsAsciiString(_uName, buffer.Slice(FieldLocations.UName, FieldLengths.UName)); + checksum += WriteAsAsciiString(UName, buffer.Slice(FieldLocations.UName, FieldLengths.UName)); } - if (!string.IsNullOrEmpty(_gName)) + if (!string.IsNullOrEmpty(GName)) { - checksum += WriteAsAsciiString(_gName, buffer.Slice(FieldLocations.GName, FieldLengths.GName)); + checksum += WriteAsAsciiString(GName, buffer.Slice(FieldLocations.GName, FieldLengths.GName)); } if (_devMajor > 0) @@ -618,34 +618,44 @@ static int CountDigits(int value) } // 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() + // extended attributes. They are always collected or updated in that dictionary, with no restrictions. + private void CollectExtendedAttributesFromStandardFields() { - ExtendedAttributes.Add(PaxEaName, _name); + if (!_isPaxEaNameSynced) + { + ExtendedAttributes[PaxEaName] = Name; + _isPaxEaNameSynced = true; + } - if (!ExtendedAttributes.ContainsKey(PaxEaMTime)) + if (!_isPaxEaMTimeSynced) { - ExtendedAttributes.Add(PaxEaMTime, TarHelpers.GetTimestampStringFromDateTimeOffset(_mTime)); + ExtendedAttributes[PaxEaMTime] = TarHelpers.GetTimestampStringFromDateTimeOffset(MTime); + _isPaxEaMTimeSynced = true; } - if (!string.IsNullOrEmpty(_gName)) + if (!_isPaxEaGNameSynced && !string.IsNullOrEmpty(GName)) { - TryAddStringField(ExtendedAttributes, PaxEaGName, _gName, FieldLengths.GName); + TryAddStringField(ExtendedAttributes, PaxEaGName, GName, FieldLengths.GName); + _isPaxEaGNameSynced = true; } - if (!string.IsNullOrEmpty(_uName)) + if (!_isPaxEaUNameSynced && !string.IsNullOrEmpty(UName)) { - TryAddStringField(ExtendedAttributes, PaxEaUName, _uName, FieldLengths.UName); + TryAddStringField(ExtendedAttributes, PaxEaUName, UName, FieldLengths.UName); + _isPaxEaUNameSynced = true; } - if (!string.IsNullOrEmpty(_linkName)) + if (!_isPaxEaLinkNameSynced && !string.IsNullOrEmpty(LinkName)) { - ExtendedAttributes.Add(PaxEaLinkName, _linkName); + ExtendedAttributes[PaxEaLinkName] = LinkName; + _isPaxEaLinkNameSynced = true; } - if (_size > 99_999_999) + Size = GetTotalDataBytesToWrite(); + if (!_isPaxEaSizeSynced && Size > 99_999_999) { - ExtendedAttributes.Add(PaxEaSize, _size.ToString()); + ExtendedAttributes[PaxEaSize] = Size.ToString(); + _isPaxEaSizeSynced = true; } // Adds the specified string to the dictionary if it's longer than the specified max byte length. @@ -653,7 +663,7 @@ static void TryAddStringField(Dictionary extendedAttributes, str { if (Encoding.UTF8.GetByteCount(value) > maxLength) { - extendedAttributes.Add(key, value); + extendedAttributes[key] = value; } } } @@ -780,10 +790,10 @@ private static int WriteAsAsciiString(string str, Span buffer) // - %f: The filename of the file, equivalent to the result of the basename utility on the translated pathname. private string GenerateExtendedAttributeName() { - ReadOnlySpan dirName = Path.GetDirectoryName(_name.AsSpan()); + ReadOnlySpan dirName = Path.GetDirectoryName(Name.AsSpan()); dirName = dirName.IsEmpty ? "." : dirName; - ReadOnlySpan fileName = Path.GetFileName(_name.AsSpan()); + ReadOnlySpan fileName = Path.GetFileName(Name.AsSpan()); fileName = fileName.IsEmpty ? "." : fileName; return _typeFlag is TarEntryType.Directory or TarEntryType.DirectoryList ? 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 65fdda022b32b6..e9a8c6e47d0f97 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 @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.IO; namespace System.Formats.Tar @@ -51,24 +52,33 @@ internal sealed partial class TarHeader internal TarEntryFormat _format; + // Booleans to determine if in a PAX tar entry, the extended attribute for a reserved + // field should get overwritten before writing this header to an archive + internal bool _isPaxEaNameSynced; + internal bool _isPaxEaSizeSynced; + internal bool _isPaxEaMTimeSynced; + internal bool _isPaxEaGNameSynced; + internal bool _isPaxEaUNameSynced; + internal bool _isPaxEaLinkNameSynced; + // Common attributes - internal string _name; + private string _name; internal int _mode; internal int _uid; internal int _gid; - internal long _size; - internal DateTimeOffset _mTime; + private long _size; + private DateTimeOffset _mTime; internal int _checksum; internal TarEntryType _typeFlag; - internal string? _linkName; + private string? _linkName; // POSIX and GNU shared attributes internal string _magic; internal string _version; - internal string? _gName; - internal string? _uName; + private string? _gName; + private string? _uName; internal int _devMajor; internal int _devMinor; @@ -90,13 +100,76 @@ internal sealed partial class TarHeader // fields have data, we store it to avoid data loss, but we don't yet expose it publicly. internal byte[]? _gnuUnusedBytes; + internal string Name + { + get => _name; + [MemberNotNull(nameof(_name))] + set + { + Debug.Assert(value != null); + _name = value; + _isPaxEaNameSynced = false; + } + } + + internal long Size + { + get => _size; + set + { + _size = value; + _isPaxEaSizeSynced = false; + } + } + + internal DateTimeOffset MTime + { + get => _mTime; + set + { + _mTime = value; + _isPaxEaMTimeSynced = false; + } + } + + internal string? LinkName + { + get => _linkName; + set + { + _linkName = value; + _isPaxEaLinkNameSynced = false; + } + } + + internal string? GName + { + get => _gName; + set + { + _gName = value; + _isPaxEaGNameSynced = false; + } + } + internal string? UName + { + get => _uName; + set + { + _uName = value; + _isPaxEaUNameSynced = false; + } + } + // Constructor called when creating an entry with default common fields. internal TarHeader(TarEntryFormat format, string name = "", int mode = 0, DateTimeOffset mTime = default, TarEntryType typeFlag = TarEntryType.RegularFile) { + Debug.Assert(name != null); + _format = format; - _name = name; + Name = name; _mode = mode; - _mTime = mTime; + MTime = mTime; _typeFlag = typeFlag; _magic = GetMagicForFormat(format); _version = GetVersionForFormat(format); @@ -105,19 +178,31 @@ internal TarHeader(TarEntryFormat format, string name = "", int mode = 0, DateTi // Constructor called when creating an entry using the common fields from another entry. // The *TarEntry constructor calling this should take care of setting any format-specific fields. internal TarHeader(TarEntryFormat format, TarEntryType typeFlag, TarHeader other) - : this(format, other._name, other._mode, other._mTime, typeFlag) + : this(format, other.Name, other._mode, other.MTime, typeFlag) { _uid = other._uid; _gid = other._gid; - _size = other._size; + Size = other.Size; _checksum = other._checksum; - _linkName = other._linkName; + LinkName = other.LinkName; _dataStream = other._dataStream; } - internal void InitializeExtendedAttributesWithExisting(IEnumerable> existing) + internal void InitializeExtendedAttributesWithExisting(IEnumerable> existing, bool allowReservedKeys) { Debug.Assert(_ea == null); + + if (!allowReservedKeys) + { + foreach ((string key, string _) in existing) + { + if (key is PaxEaName or PaxEaSize or PaxEaMTime or PaxEaGName or PaxEaUName) + { + throw new ArgumentException(string.Format(SR.TarReservedExtendedAttribute, key)); + } + } + } + _ea = new Dictionary(existing); } 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 ae815dc0073b46..f4175421d88f37 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 @@ -199,7 +199,7 @@ internal void AdvanceDataStreamIfNeeded() Debug.Assert(_previouslyReadEntry._header._endOfHeaderAndDataAndBlockAlignment > 0); _archiveStream.Position = _previouslyReadEntry._header._endOfHeaderAndDataAndBlockAlignment; } - else if (_previouslyReadEntry._header._size > 0) + 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 @@ -215,14 +215,14 @@ internal void AdvanceDataStreamIfNeeded() { // 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)) + if (dataStream.Position < (_previouslyReadEntry._header.Size - 1)) { - long bytesToSkip = _previouslyReadEntry._header._size - dataStream.Position; + long bytesToSkip = _previouslyReadEntry._header.Size - dataStream.Position; TarHelpers.AdvanceStream(_archiveStream, bytesToSkip); dataStream.HasReachedEnd = true; // Now the pointer is beyond the limit, so any read attempts should throw } } - TarHelpers.SkipBlockAlignmentPadding(_archiveStream, _previouslyReadEntry._header._size); + TarHelpers.SkipBlockAlignmentPadding(_archiveStream, _previouslyReadEntry._header.Size); } } @@ -241,7 +241,7 @@ internal async ValueTask AdvanceDataStreamIfNeededAsync(CancellationToken cancel Debug.Assert(_previouslyReadEntry._header._endOfHeaderAndDataAndBlockAlignment > 0); _archiveStream.Position = _previouslyReadEntry._header._endOfHeaderAndDataAndBlockAlignment; } - else if (_previouslyReadEntry._header._size > 0) + 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 @@ -257,14 +257,14 @@ internal async ValueTask AdvanceDataStreamIfNeededAsync(CancellationToken cancel { // 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)) + if (dataStream.Position < (_previouslyReadEntry._header.Size - 1)) { - long bytesToSkip = _previouslyReadEntry._header._size - dataStream.Position; + long bytesToSkip = _previouslyReadEntry._header.Size - dataStream.Position; await TarHelpers.AdvanceStreamAsync(_archiveStream, bytesToSkip, cancellationToken).ConfigureAwait(false); dataStream.HasReachedEnd = true; // Now the pointer is beyond the limit, so any read attempts should throw } } - await TarHelpers.SkipBlockAlignmentPaddingAsync(_archiveStream, _previouslyReadEntry._header._size, cancellationToken).ConfigureAwait(false); + await TarHelpers.SkipBlockAlignmentPaddingAsync(_archiveStream, _previouslyReadEntry._header.Size, cancellationToken).ConfigureAwait(false); } } @@ -491,18 +491,18 @@ private bool TryProcessGnuMetadataHeader(TarHeader header, bool copyData, out Ta if (header._typeFlag is TarEntryType.LongLink) { - Debug.Assert(header._linkName != null); - Debug.Assert(secondHeader._name != null); + Debug.Assert(header.LinkName != null); + Debug.Assert(secondHeader.Name != null); - thirdHeader._linkName = header._linkName; - thirdHeader._name = secondHeader._name; + 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; + Debug.Assert(header.Name != null); + Debug.Assert(secondHeader.LinkName != null); + thirdHeader.Name = header.Name; + thirdHeader.LinkName = secondHeader.LinkName; } finalHeader = thirdHeader; @@ -512,13 +512,13 @@ private bool TryProcessGnuMetadataHeader(TarHeader header, bool copyData, out Ta { if (header._typeFlag is TarEntryType.LongLink) { - Debug.Assert(header._linkName != null); - secondHeader._linkName = header._linkName; + 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; + Debug.Assert(header.Name != null); + secondHeader.Name = header.Name; } finalHeader = secondHeader; @@ -567,18 +567,18 @@ private bool TryProcessGnuMetadataHeader(TarHeader header, bool copyData, out Ta if (header._typeFlag is TarEntryType.LongLink) { - Debug.Assert(header._linkName != null); - Debug.Assert(secondHeader._name != null); + Debug.Assert(header.LinkName != null); + Debug.Assert(secondHeader.Name != null); - thirdHeader._linkName = header._linkName; - thirdHeader._name = secondHeader._name; + 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; + Debug.Assert(header.Name != null); + Debug.Assert(secondHeader.LinkName != null); + thirdHeader.Name = header.Name; + thirdHeader.LinkName = secondHeader.LinkName; } finalHeader = thirdHeader; @@ -588,13 +588,13 @@ private bool TryProcessGnuMetadataHeader(TarHeader header, bool copyData, out Ta { if (header._typeFlag is TarEntryType.LongLink) { - Debug.Assert(header._linkName != null); - secondHeader._linkName = header._linkName; + 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; + Debug.Assert(header.Name != null); + secondHeader.Name = header.Name; } finalHeader = secondHeader; 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 a691582178df6a..595140cb3f1ea6 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 @@ -62,7 +62,7 @@ private TarEntry ConstructEntryForWriting(string fullPath, string entryName, Fil entry._header._devMinor = (int)minor; } - entry._header._mTime = TarHelpers.GetDateTimeOffsetFromSecondsSinceEpoch(status.MTime); + entry._header.MTime = TarHelpers.GetDateTimeOffsetFromSecondsSinceEpoch(status.MTime); entry._header._aTime = TarHelpers.GetDateTimeOffsetFromSecondsSinceEpoch(status.ATime); entry._header._cTime = TarHelpers.GetDateTimeOffsetFromSecondsSinceEpoch(status.CTime); @@ -75,7 +75,7 @@ private TarEntry ConstructEntryForWriting(string fullPath, string entryName, Fil uName = Interop.Sys.GetUserNameFromPasswd(status.Uid); _userIdentifiers.Add(status.Uid, uName); } - entry._header._uName = uName; + entry._header.UName = uName; // Gid and GName entry._header._gid = (int)status.Gid; @@ -84,7 +84,7 @@ private TarEntry ConstructEntryForWriting(string fullPath, string entryName, Fil gName = Interop.Sys.GetGroupName(status.Gid); _groupIdentifiers.Add(status.Gid, gName); } - entry._header._gName = gName; + entry._header.GName = gName; if (entry.EntryType == TarEntryType.SymbolicLink) { 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 7452246f742ed6..b064de1eed0b87 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 @@ -50,7 +50,7 @@ private TarEntry ConstructEntryForWriting(string fullPath, string entryName, Fil FileSystemInfo info = (attributes & FileAttributes.Directory) != 0 ? new DirectoryInfo(fullPath) : new FileInfo(fullPath); - entry._header._mTime = info.LastWriteTimeUtc; + entry._header.MTime = info.LastWriteTimeUtc; entry._header._aTime = info.LastAccessTimeUtc; entry._header._cTime = info.LastWriteTimeUtc; // There is no "change time" property 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 d7b7ceceac3464..1e9e4aa2698b56 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 @@ -221,7 +221,7 @@ public void WriteEntry(TarEntry entry) { ObjectDisposedException.ThrowIf(_isDisposed, this); ArgumentNullException.ThrowIfNull(entry); - ValidateEntryLinkName(entry._header._typeFlag, entry._header._linkName); + ValidateEntryLinkName(entry._header._typeFlag, entry._header.LinkName); WriteEntryInternal(entry); } @@ -269,7 +269,7 @@ public Task WriteEntryAsync(TarEntry entry, CancellationToken cancellationToken ObjectDisposedException.ThrowIf(_isDisposed, this); ArgumentNullException.ThrowIfNull(entry); - ValidateEntryLinkName(entry._header._typeFlag, entry._header._linkName); + ValidateEntryLinkName(entry._header._typeFlag, entry._header.LinkName); return WriteEntryAsyncInternal(entry, cancellationToken); } @@ -325,7 +325,8 @@ private async Task WriteEntryAsyncInternal(TarEntry entry, CancellationToken can { TarEntryFormat.V7 => entry._header.WriteAsV7Async(_archiveStream, buffer, cancellationToken), TarEntryFormat.Ustar => entry._header.WriteAsUstarAsync(_archiveStream, buffer, cancellationToken), - TarEntryFormat.Pax when entry._header._typeFlag is TarEntryType.GlobalExtendedAttributes => entry._header.WriteAsPaxGlobalExtendedAttributesAsync(_archiveStream, buffer, _nextGlobalExtendedAttributesEntryNumber++, cancellationToken), + TarEntryFormat.Pax when entry._header._typeFlag is TarEntryType.GlobalExtendedAttributes => + entry._header.WriteAsPaxGlobalExtendedAttributesAsync(_archiveStream, buffer, _nextGlobalExtendedAttributesEntryNumber++, cancellationToken), TarEntryFormat.Pax => entry._header.WriteAsPaxAsync(_archiveStream, buffer, cancellationToken), TarEntryFormat.Gnu => entry._header.WriteAsGnuAsync(_archiveStream, buffer, cancellationToken), _ => throw new InvalidDataException(string.Format(SR.TarInvalidFormat, Format)), 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 c43c8dff343f23..c7395c6449efd7 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 @@ -47,6 +47,7 @@ + @@ -64,6 +65,7 @@ + diff --git a/src/libraries/System.Formats.Tar/tests/TarEntry/PaxTarEntry.Conversion.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarEntry/PaxTarEntry.Conversion.Tests.cs index e42f1df0ea6ea1..90d27ab19f8f22 100644 --- a/src/libraries/System.Formats.Tar/tests/TarEntry/PaxTarEntry.Conversion.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarEntry/PaxTarEntry.Conversion.Tests.cs @@ -350,5 +350,199 @@ public void Constructor_ConversionPax_BackAndForth_Fifo() => [Fact] public void Constructor_ConversionGnu_BackAndForth_Fifo() => TestConstructionConversionBackAndForth(TarEntryType.Fifo, TarEntryFormat.Pax, TarEntryFormat.Gnu); + + [Theory] + [InlineData(TarEntryType.RegularFile)] + [InlineData(TarEntryType.SymbolicLink)] + public void Constructor_Conversion_CopyAllReservedKeys(TarEntryType entryType) + { + string expectedName = "ExpectedName.txt"; + + PaxTarEntry originalEntry = new PaxTarEntry(entryType, expectedName); + originalEntry.ModificationTime = TestModificationTime; + originalEntry.GroupName = TestLongGName; + originalEntry.UserName = TestLongUName; + + if (entryType is TarEntryType.RegularFile) + { + originalEntry.DataStream = new FakeLengthStream(); // All we care is the length beyond the limit + } + else if (entryType is TarEntryType.SymbolicLink) + { + originalEntry.LinkName = TestLinkName; + } + + using MemoryStream archive = new MemoryStream(); + using (TarWriter writer = new TarWriter(archive, leaveOpen: true)) + { + writer.WriteEntry(originalEntry); // Forces writing reserved keys into extended attributes of entry + } + + // The writer copies reserved fields into the extended attributes if needed + VerifyPaxReservedKeys(originalEntry); + + PaxTarEntry convertedEntry = new PaxTarEntry(other: originalEntry); // Should not throw for finding reserved keys in dictionary + + // Using the conversion constructor should also copy the extended attributes + VerifyPaxReservedKeys(convertedEntry); + + // Verify the entry's attributes were written into the archive correctly + archive.Position = 0; + using (TarReader reader = new TarReader(archive, leaveOpen: false)) + { + PaxTarEntry readEntry = reader.GetNextEntry() as PaxTarEntry; + Assert.NotNull(readEntry); + VerifyPaxReservedKeys(readEntry); + } + + // Now update the fields so the existing dictionary keys get overwritten + + string modifiedName = "modifiedName.txt"; + DateTimeOffset modifiedMTime = new DateTimeOffset(2022, 12, 30, 23, 59, 58, TimeSpan.Zero); + string modifiedGName = $"abc{TestLongGName}abc"; + string modifiedUName = $"abc{TestLongUName}abc"; + long modifiedSize = MaxAllowedSize + 5; + string modifiedLinkName = "modifiedLinkName"; + + convertedEntry.Name = modifiedName; + convertedEntry.ModificationTime = modifiedMTime; + convertedEntry.GroupName = modifiedGName; + convertedEntry.UserName = modifiedUName; + + if (entryType is TarEntryType.RegularFile) + { + ((FakeLengthStream)convertedEntry.DataStream).ChangeLength(modifiedSize); + } + else if (entryType is TarEntryType.SymbolicLink) + { + convertedEntry.LinkName = modifiedLinkName; + } + + archive.Position = 0; + archive.SetLength(0); + using (TarWriter writer = new TarWriter(archive, leaveOpen: true)) + { + writer.WriteEntry(convertedEntry); // Forces overwriting reserved keys + } + + VerifyPaxReservedKeys(convertedEntry); + + // Last check: verify we write the converted entry's dictionary correctly + archive.Position = 0; + using (TarReader reader = new TarReader(archive, leaveOpen: false)) + { + PaxTarEntry readEntry = reader.GetNextEntry() as PaxTarEntry; + Assert.NotNull(readEntry); + VerifyPaxReservedKeys(readEntry); + } + + // Finally, dispose streams if needed + if (originalEntry.DataStream != null) + { + originalEntry.DataStream.Dispose(); + } + + if (convertedEntry.DataStream != null) + { + convertedEntry.DataStream.Dispose(); + } + } + + [Theory] + [InlineData(TarEntryType.RegularFile)] + [InlineData(TarEntryType.SymbolicLink)] + public async Task Constructor_Conversion_CopyAllReservedKeys_Async(TarEntryType entryType) + { + string expectedName = "ExpectedName.txt"; + + PaxTarEntry originalEntry = new PaxTarEntry(entryType, expectedName); + originalEntry.ModificationTime = TestModificationTime; + originalEntry.GroupName = TestLongGName; + originalEntry.UserName = TestLongUName; + + if (entryType is TarEntryType.RegularFile) + { + originalEntry.DataStream = new FakeLengthStream(); // All we care is the length beyond the limit + } + else if (entryType is TarEntryType.SymbolicLink) + { + originalEntry.LinkName = TestLinkName; + } + + await using MemoryStream archive = new MemoryStream(); + await using (TarWriter writer = new TarWriter(archive, leaveOpen: true)) + { + await writer.WriteEntryAsync(originalEntry); // Forces writing reserved keys into extended attributes of entry + } + + // The writer copies reserved fields into the extended attributes if needed + VerifyPaxReservedKeys(originalEntry); + + PaxTarEntry convertedEntry = new PaxTarEntry(other: originalEntry); // Should not throw for finding reserved keys in dictionary + + // Using the conversion constructor should also copy the extended attributes + VerifyPaxReservedKeys(convertedEntry); + + // Verify the entry's attributes were written into the archive correctly + archive.Position = 0; + await using (TarReader reader = new TarReader(archive, leaveOpen: false)) + { + PaxTarEntry readEntry = await reader.GetNextEntryAsync() as PaxTarEntry; + Assert.NotNull(readEntry); + VerifyPaxReservedKeys(readEntry); + } + + // Now update the fields so the existing dictionary keys get overwritten + + string modifiedName = "modifiedName.txt"; + DateTimeOffset modifiedMTime = new DateTimeOffset(2022, 12, 30, 23, 59, 58, TimeSpan.Zero); + string modifiedGName = $"abc{TestLongGName}abc"; + string modifiedUName = $"abc{TestLongUName}abc"; + long modifiedSize = MaxAllowedSize + 5; + string modifiedLinkName = "modifiedLinkName"; + + convertedEntry.Name = modifiedName; + convertedEntry.ModificationTime = modifiedMTime; + convertedEntry.GroupName = modifiedGName; + convertedEntry.UserName = modifiedUName; + + if (entryType is TarEntryType.RegularFile) + { + ((FakeLengthStream)convertedEntry.DataStream).ChangeLength(modifiedSize); + } + else if (entryType is TarEntryType.SymbolicLink) + { + convertedEntry.LinkName = modifiedLinkName; + } + + archive.Position = 0; + archive.SetLength(0); + await using (TarWriter writer = new TarWriter(archive, leaveOpen: true)) + { + await writer.WriteEntryAsync(convertedEntry); // Forces overwriting reserved keys + } + + VerifyPaxReservedKeys(convertedEntry); + + // Last check: verify we write the converted entry's dictionary correctly + archive.Position = 0; + await using (TarReader reader = new TarReader(archive, leaveOpen: false)) + { + PaxTarEntry readEntry = await reader.GetNextEntryAsync() as PaxTarEntry; + Assert.NotNull(readEntry); + VerifyPaxReservedKeys(readEntry); + } + + // Finally, dispose streams if needed + if (originalEntry.DataStream != null) + { + await originalEntry.DataStream.DisposeAsync(); + } + + if (convertedEntry.DataStream != null) + { + await convertedEntry.DataStream.DisposeAsync(); + } + } } } diff --git a/src/libraries/System.Formats.Tar/tests/TarEntry/PaxTarEntry.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarEntry/PaxTarEntry.Tests.cs index 0e8bcc952cea5d..f0cb0cd8a966c7 100644 --- a/src/libraries/System.Formats.Tar/tests/TarEntry/PaxTarEntry.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarEntry/PaxTarEntry.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 System.Linq; using Xunit; @@ -91,5 +92,15 @@ public void SupportedEntryType_Fifo() SetFifo(fifo); VerifyFifo(fifo); } + + [Fact] + public void ThrowIf_Dictionary_Contains_ReservedKey() + { + foreach (string reservedKey in ReservedExtendedAttributeKeyNames) + { + Dictionary dict = new Dictionary() { { reservedKey, "not allowed" } }; + Assert.Throws(() => new PaxTarEntry(TarEntryType.RegularFile, "entryName", dict)); + } + } } } diff --git a/src/libraries/System.Formats.Tar/tests/TarTestsBase.Pax.cs b/src/libraries/System.Formats.Tar/tests/TarTestsBase.Pax.cs index dd71b157963962..9f6a70f0401d03 100644 --- a/src/libraries/System.Formats.Tar/tests/TarTestsBase.Pax.cs +++ b/src/libraries/System.Formats.Tar/tests/TarTestsBase.Pax.cs @@ -124,5 +124,19 @@ protected void VerifyExtendedAttributeTimestamps(PaxTarEntry pax) VerifyExtendedAttributeTimestamp(pax, PaxEaATime, MinimumTime); VerifyExtendedAttributeTimestamp(pax, PaxEaCTime, MinimumTime); } + + protected void VerifyPaxReservedKeys(PaxTarEntry entry) + { + foreach (string reservedKey in ReservedExtendedAttributeKeyNames) + { + if ((reservedKey is PaxEaSize && entry.EntryType is not TarEntryType.RegularFile) || + (reservedKey is PaxEaLinkName && entry.EntryType is not TarEntryType.SymbolicLink)) + { + continue; + } + + Assert.Contains(reservedKey, entry.ExtendedAttributes); + } + } } } diff --git a/src/libraries/System.Formats.Tar/tests/TarTestsBase.cs b/src/libraries/System.Formats.Tar/tests/TarTestsBase.cs index ae01821d62b6d6..2b5c303d6a482c 100644 --- a/src/libraries/System.Formats.Tar/tests/TarTestsBase.cs +++ b/src/libraries/System.Formats.Tar/tests/TarTestsBase.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; +using System.IO.Enumeration; using System.Linq; using System.Runtime.CompilerServices; using Microsoft.DotNet.RemoteExecutor; @@ -32,6 +33,8 @@ public abstract partial class TarTestsBase : FileCleanupTestBase protected const UnixFileMode TestPermission3 = UserAll | UnixFileMode.OtherRead; protected const UnixFileMode TestPermission4 = UserAll | UnixFileMode.OtherExecute; + protected const long MaxAllowedSize = 99_999_999; + protected const int DefaultGid = 0; protected const int DefaultUid = 0; protected const int DefaultDeviceMajor = 0; @@ -58,6 +61,8 @@ public abstract partial class TarTestsBase : FileCleanupTestBase protected const string TestGName = "group"; protected const string TestUName = "user"; + protected readonly string TestLongGName = new string('\u00f1', 33); + protected readonly string TestLongUName = new string('\u00e4', 100); // The metadata of the entries inside the asset archives are all set to these values protected const int AssetGid = 3579; @@ -90,6 +95,8 @@ public abstract partial class TarTestsBase : FileCleanupTestBase protected const string PaxEaDevMajor = "devmajor"; protected const string PaxEaDevMinor = "devminor"; + protected static readonly string[] ReservedExtendedAttributeKeyNames = new[] { PaxEaName, PaxEaSize, PaxEaMTime, PaxEaGName, PaxEaUName }; + private static readonly string[] V7TestCaseNames = new[] { "file", @@ -178,7 +185,7 @@ public abstract partial class TarTestsBase : FileCleanupTestBase "xattrs" }; - protected enum CompressionMethod + public enum CompressionMethod { // Archiving only, no compression Uncompressed, @@ -510,6 +517,16 @@ public static IEnumerable GetFormatsAndFiles() } } + protected static TarEntryFormat GetEntryFormatForTestTarFormat(TestTarFormat testFormat) => + testFormat switch + { + TestTarFormat.v7 => TarEntryFormat.V7, + TestTarFormat.ustar => TarEntryFormat.Ustar, + TestTarFormat.pax or TestTarFormat.pax_gea => TarEntryFormat.Pax, + TestTarFormat.oldgnu or TestTarFormat.gnu => TarEntryFormat.Gnu, + _ => throw new ArgumentOutOfRangeException(nameof(testFormat)), + }; + protected static void SetUnixFileMode(string path, UnixFileMode mode) { if (!PlatformDetection.IsWindows) @@ -622,5 +639,38 @@ public static IEnumerable GetPaxAndGnuTestCaseNames() yield return new object[] { name }; } } + + protected static FileSystemEnumerable GetFileSystemInfosRecursive(string path) + { + var enumerationOptions = new EnumerationOptions { RecurseSubdirectories = true }; + return new FileSystemEnumerable(path, (ref FileSystemEntry e) => e.ToFileSystemInfo(), enumerationOptions); + } + + internal class FakeLengthStream : Stream + { + private long _length; + + public FakeLengthStream(long initialLength = MaxAllowedSize + 1) + { + _length = initialLength; + } + + public override bool CanRead => true; + public override bool CanSeek => true; + public override bool CanWrite => true; + public override long Length => _length; // Beyond allowed length to write in size field + public override long Position { get => 0; set { throw new NotImplementedException(); } } + + public override void Flush() => throw new NotImplementedException(); + public override int Read(byte[] buffer, int offset, int count) => 0; + public override long Seek(long offset, SeekOrigin origin) => throw new NotImplementedException(); + public override void SetLength(long value) => throw new NotImplementedException(); + public override void Write(byte[] buffer, int offset, int count) => throw new NotImplementedException(); + + public void ChangeLength(long newLength) + { + _length = newLength; + } + } } } diff --git a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.File.Base.cs b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.File.Base.cs new file mode 100644 index 00000000000000..dd8c0ebb57c39f --- /dev/null +++ b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.File.Base.cs @@ -0,0 +1,67 @@ +// 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; + +namespace System.Formats.Tar.Tests +{ + public partial class TarWriter_File_Base : TarTestsBase + { + // TestTarFormat, string testCaseName, CompressionMethod, bool copyData + public static IEnumerable WriteEntry_CopyArchive_Data() + { + foreach (CompressionMethod compressionMethod in Enum.GetValues()) + { + foreach (bool copyData in new bool[] { false, true }) + { + foreach (object[] testCaseName in GetV7TestCaseNames()) + { + if ((string)testCaseName[0] == "folder_file_utf8") // The legacy name field is stored in ASCII in the V7 format + { + continue; + } + + yield return new object[] { TestTarFormat.v7, testCaseName[0], compressionMethod, copyData }; + } + + foreach (object[] testCaseNameObj in GetUstarTestCaseNames()) + { + string testCaseName = (string)testCaseNameObj[0]; + if (testCaseName is "folder_file_utf8" // The legacy name field is stored in ASCII in the Ustar format + or "longpath_splitable_under255" // Folder stored under 'unarchived' runtime-assets due to NuGet not allowing very long paths + or "specialfiles") // Same but for fifos/chardevices/blockdevices + { + continue; + } + + yield return new object[] { TestTarFormat.ustar, testCaseName, compressionMethod, copyData }; + } + + foreach (object[] testCaseNameObj in GetPaxAndGnuTestCaseNames()) + { + string testCaseName = (string)testCaseNameObj[0]; + if (testCaseName is "longpath_splitable_under255" + or "specialfiles" + or "longpath_over255" // Folder stored under 'unarchived' runtime-assets due to NuGet not allowing very long paths + or "longfilename_over100_under255" // Same + or "file_longsymlink") // Same + { + continue; + } + + if (testCaseName is not "folder_file_utf8") // The legacy name field is stored in ASCII in the GNU format + { + yield return new object[] { TestTarFormat.oldgnu, testCaseName, compressionMethod, copyData }; + yield return new object[] { TestTarFormat.gnu, testCaseName, compressionMethod, copyData }; + } + + // folder_file_utf8 case name is ok to use in PAX because it allows storing the name in UTF8 in the extended attributes + yield return new object[] { TestTarFormat.pax_gea, testCaseName, compressionMethod, copyData }; + yield return new object[] { TestTarFormat.pax, testCaseName, compressionMethod, copyData }; + + } + } + } + } + } +} 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 1e81fb7b1e8a01..e7e409caf8b92e 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 @@ -495,5 +495,130 @@ public void Write_LinkEntry_EmptyLinkName_Throws(TarEntryType entryType) using TarWriter writer = new TarWriter(archiveStream, leaveOpen: false); Assert.Throws("entry", () => writer.WriteEntry(new PaxTarEntry(entryType, "link"))); } + + [Theory] + [InlineData(TarEntryType.RegularFile)] + [InlineData(TarEntryType.SymbolicLink)] + public void Verify_Write_Inserts_ReservedKeys(TarEntryType entryType) + { + string expectedName = "ExpectedName.txt"; + + PaxTarEntry entry = new PaxTarEntry(entryType, expectedName); + entry.ModificationTime = TestModificationTime; + entry.GroupName = TestLongGName; + entry.UserName = TestLongUName; + + if (entryType is TarEntryType.RegularFile) + { + entry.DataStream = new FakeLengthStream(); // All we care is the length beyond the limit + } + else if (entryType is TarEntryType.SymbolicLink) + { + entry.LinkName = TestLinkName; + } + + using MemoryStream archive = new MemoryStream(); + using (TarWriter writer = new TarWriter(archive, leaveOpen: true)) + { + writer.WriteEntry(entry); // Forces writing reserved keys into extended attributes of entry + } + + VerifyPaxReservedKeys(entry); + + archive.Position = 0; + using (TarReader reader = new TarReader(archive, leaveOpen: false)) + { + PaxTarEntry readEntry = reader.GetNextEntry() as PaxTarEntry; + Assert.NotNull(readEntry); + VerifyPaxReservedKeys(readEntry); + } + + if (entry.DataStream != null) + { + entry.DataStream.Dispose(); + } + } + + [Theory] + [InlineData(TarEntryType.RegularFile)] + [InlineData(TarEntryType.SymbolicLink)] + public void Verify_Write_Inserts_ReservedKeys_UpdatedBeforeWrite(TarEntryType entryType) + { + string modifiedName = "modifiedName.txt"; + DateTimeOffset modifiedModificationTime = new DateTimeOffset(2022, 12, 29, 23, 59, 58, TimeSpan.Zero); + string modifiedGName = $"abc{TestLongGName}abc"; + string modifiedUName = $"abc{TestLongUName}abc"; + long modifiedSize = MaxAllowedSize + 5; + string modifiedLinkName = $"abc{TestLinkName}abc"; + + PaxTarEntry originalEntry = new PaxTarEntry(entryType, "originalName.txt"); + originalEntry.ModificationTime = TestModificationTime; + originalEntry.GroupName = TestLongGName; + originalEntry.UserName = TestLongUName; + + if (entryType is TarEntryType.RegularFile) + { + originalEntry.DataStream = new FakeLengthStream(); // All we care is the length beyond the limit + } + else if (entryType is TarEntryType.SymbolicLink) + { + originalEntry.LinkName = TestLinkName; + } + + using MemoryStream archive = new MemoryStream(); + using (TarWriter writer = new TarWriter(archive, leaveOpen: true)) + { + originalEntry.Name = modifiedName; + originalEntry.ModificationTime = modifiedModificationTime; + originalEntry.GroupName = modifiedGName; + originalEntry.UserName = modifiedUName; + + if (entryType is TarEntryType.RegularFile) + { + ((FakeLengthStream)originalEntry.DataStream).ChangeLength(modifiedSize); + } + + if (entryType is TarEntryType.SymbolicLink) + { + originalEntry.LinkName = modifiedLinkName; + } + + writer.WriteEntry(originalEntry); // Forces writing reserved keys into extended attributes of entry + } + + VerifyPaxReservedKeys(originalEntry); + + archive.Position = 0; + using (TarReader reader = new TarReader(archive, leaveOpen: false)) + { + PaxTarEntry readEntry = reader.GetNextEntry() as PaxTarEntry; + Assert.NotNull(readEntry); + VerifyModifiedKeys(readEntry); + } + + if (originalEntry.DataStream != null) + { + originalEntry.DataStream.Dispose(); + } + + void VerifyModifiedKeys(PaxTarEntry paxEntry) + { + Assert.Equal(paxEntry.ExtendedAttributes[PaxEaName], modifiedName); + VerifyExtendedAttributeTimestamp(paxEntry, PaxEaMTime, modifiedModificationTime); + Assert.Equal(paxEntry.ExtendedAttributes[PaxEaGName], modifiedGName); + Assert.Equal(paxEntry.ExtendedAttributes[PaxEaUName], modifiedUName); + + if (paxEntry.EntryType is TarEntryType.RegularFile) + { + Assert.True(long.TryParse(paxEntry.ExtendedAttributes[PaxEaSize], out long result)); + Assert.Equal(result, modifiedSize); + } + + else if (paxEntry.EntryType is TarEntryType.SymbolicLink) + { + Assert.Equal(paxEntry.ExtendedAttributes[PaxEaLinkName], modifiedLinkName); + } + } + } } } diff --git a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.File.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.File.Tests.cs index e1534a0d4960fc..323d4cc7c16513 100644 --- a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.File.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.File.Tests.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.IO; +using System.IO.Compression; +using System.Linq; using Xunit; namespace System.Formats.Tar.Tests @@ -206,5 +208,81 @@ public void Add_SymbolicLink(TarEntryFormat format, bool createTarget) Assert.Null(reader.GetNextEntry()); } } + + [Theory] + [MemberData(nameof(WriteEntry_CopyArchive_Data))] + public void WriteEntry_CopyArchive(TestTarFormat testFormat, string testCaseName, CompressionMethod compressionMethod, bool copyData) + { + TarEntryFormat entryFormat = GetEntryFormatForTestTarFormat(testFormat); + string pathWithExpectedFiles = GetTestCaseUnarchivedFolderPath(testCaseName); + pathWithExpectedFiles = PathInternal.EnsureTrailingSeparator(pathWithExpectedFiles); + + using Stream fileMemoryStream = GetTarMemoryStream(compressionMethod, testFormat, testCaseName); + using Stream originArchive = compressionMethod == CompressionMethod.GZip ? new GZipStream(fileMemoryStream, CompressionMode.Decompress) : fileMemoryStream; + + VerifyCopyArchive(pathWithExpectedFiles, originArchive, entryFormat, copyData); + } + + protected void VerifyCopyArchive(string pathWithExpectedFiles, Stream originArchive, TarEntryFormat entryFormat, bool copyData) + { + TarEntry entry; + + using MemoryStream destinationArchive = new MemoryStream(); + using (TarReader reader = new TarReader(originArchive, leaveOpen: false)) + { + using (TarWriter writer = new TarWriter(destinationArchive, entryFormat, leaveOpen: true)) + { + while ((entry = reader.GetNextEntry(copyData)) != null) + { + if (entry is PaxTarEntry paxEntry) + { + paxEntry.GroupName = TestLongGName; + paxEntry.UserName = TestLongUName; + } + writer.WriteEntry(entry); + } + } + } + + destinationArchive.Position = 0; + + Dictionary expectedEntries = GetFileSystemInfosRecursive(pathWithExpectedFiles).ToDictionary(fsi => + fsi.FullName.Substring(pathWithExpectedFiles.Length).Replace(Path.DirectorySeparatorChar, '/')); + + int count = 0; + using (TarReader destinationReader = new TarReader(destinationArchive, leaveOpen: false)) + { + while ((entry = destinationReader.GetNextEntry(copyData)) != null) + { + if (entry is PaxGlobalExtendedAttributesTarEntry) // Metadata entry + { + continue; + } + string keyPath = Path.TrimEndingDirectorySeparator(entry.Name); + Assert.True(expectedEntries.TryGetValue(keyPath, out FileSystemInfo expectedFSI), $"Entry '{keyPath}' not found in FileSystemInfos dictionary."); + + // Cannot compare entry.LinkName with expectedFSI.LinkTarget because links in runtime-assets are not stored by nuget as such, but as regular files + if (entry.EntryType is TarEntryType.HardLink or TarEntryType.SymbolicLink) + { + AssertExtensions.GreaterThan(entry.LinkName.Length, 0, $"Entry LinkName length is 0."); + string expectedLinkTargetPath = Path.Join(pathWithExpectedFiles, entry.LinkName); + Assert.True(Path.Exists(expectedLinkTargetPath), $"Link target does not exist: {expectedLinkTargetPath}"); + } + else + { + Assert.Equal(string.Empty, entry.LinkName); + } + + if (entry is PaxTarEntry paxEntry) + { + Assert.Equal(TestLongGName, paxEntry.GroupName); + Assert.Equal(TestLongUName, paxEntry.UserName); + } + + count++; + } + } + Assert.Equal(expectedEntries.Count, count); + } } } diff --git a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.Entry.Pax.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.Entry.Pax.Tests.cs index b0c9d636420b52..0096b50dfdf3a3 100644 --- a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.Entry.Pax.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.Entry.Pax.Tests.cs @@ -515,5 +515,130 @@ public async Task Write_LinkEntry_EmptyLinkName_Throws_Async(TarEntryType entryT await using TarWriter writer = new TarWriter(archiveStream, leaveOpen: false); await Assert.ThrowsAsync("entry", () => writer.WriteEntryAsync(new PaxTarEntry(entryType, "link"))); } + + [Theory] + [InlineData(TarEntryType.RegularFile)] + [InlineData(TarEntryType.SymbolicLink)] + public async Task Verify_Write_Inserts_ReservedKeysAsync(TarEntryType entryType) + { + string expectedName = "ExpectedName.txt"; + + PaxTarEntry entry = new PaxTarEntry(entryType, expectedName); + entry.ModificationTime = TestModificationTime; + entry.GroupName = TestLongGName; + entry.UserName = TestLongUName; + + if (entryType is TarEntryType.RegularFile) + { + entry.DataStream = new FakeLengthStream(); // All we care is the length beyond the limit + } + else if (entryType is TarEntryType.SymbolicLink) + { + entry.LinkName = TestLinkName; + } + + await using MemoryStream archive = new MemoryStream(); + await using (TarWriter writer = new TarWriter(archive, leaveOpen: true)) + { + await writer.WriteEntryAsync(entry); // Forces writing reserved keys into extended attributes of entry + } + + VerifyPaxReservedKeys(entry); + + archive.Position = 0; + await using (TarReader reader = new TarReader(archive, leaveOpen: false)) + { + PaxTarEntry readEntry = await reader.GetNextEntryAsync() as PaxTarEntry; + Assert.NotNull(readEntry); + VerifyPaxReservedKeys(readEntry); + } + + if (entry.DataStream != null) + { + await entry.DataStream.DisposeAsync(); + } + } + + [Theory] + [InlineData(TarEntryType.RegularFile)] + [InlineData(TarEntryType.SymbolicLink)] + public async Task Verify_Write_Inserts_ReservedKeys_UpdatedBeforeWriteAsync(TarEntryType entryType) + { + string modifiedName = "modifiedName.txt"; + DateTimeOffset modifiedModificationTime = new DateTimeOffset(2022, 12, 29, 23, 59, 58, TimeSpan.Zero); + string modifiedGName = $"abc{TestLongGName}abc"; + string modifiedUName = $"abc{TestLongUName}abc"; + long modifiedSize = MaxAllowedSize + 5; + string modifiedLinkName = $"abc{TestLinkName}abc"; + + PaxTarEntry originalEntry = new PaxTarEntry(entryType, "originalName.txt"); + originalEntry.ModificationTime = TestModificationTime; + originalEntry.GroupName = TestLongGName; + originalEntry.UserName = TestLongUName; + + if (entryType is TarEntryType.RegularFile) + { + originalEntry.DataStream = new FakeLengthStream(); // All we care is the length beyond the limit + } + else if (entryType is TarEntryType.SymbolicLink) + { + originalEntry.LinkName = TestLinkName; + } + + await using MemoryStream archive = new MemoryStream(); + await using (TarWriter writer = new TarWriter(archive, leaveOpen: true)) + { + originalEntry.Name = modifiedName; + originalEntry.ModificationTime = modifiedModificationTime; + originalEntry.GroupName = modifiedGName; + originalEntry.UserName = modifiedUName; + + if (entryType is TarEntryType.RegularFile) + { + ((FakeLengthStream)originalEntry.DataStream).ChangeLength(modifiedSize); + } + + if (entryType is TarEntryType.SymbolicLink) + { + originalEntry.LinkName = modifiedLinkName; + } + + await writer.WriteEntryAsync(originalEntry); // Forces writing reserved keys into extended attributes of entry + } + + VerifyPaxReservedKeys(originalEntry); + + archive.Position = 0; + await using (TarReader reader = new TarReader(archive, leaveOpen: false)) + { + PaxTarEntry readEntry = await reader.GetNextEntryAsync() as PaxTarEntry; + Assert.NotNull(readEntry); + VerifyModifiedKeys(readEntry); + } + + if (originalEntry.DataStream != null) + { + await originalEntry.DataStream.DisposeAsync(); + } + + void VerifyModifiedKeys(PaxTarEntry paxEntry) + { + Assert.Equal(paxEntry.ExtendedAttributes[PaxEaName], modifiedName); + VerifyExtendedAttributeTimestamp(paxEntry, PaxEaMTime, modifiedModificationTime); + Assert.Equal(paxEntry.ExtendedAttributes[PaxEaGName], modifiedGName); + Assert.Equal(paxEntry.ExtendedAttributes[PaxEaUName], modifiedUName); + + if (paxEntry.EntryType is TarEntryType.RegularFile) + { + Assert.True(long.TryParse(paxEntry.ExtendedAttributes[PaxEaSize], out long result)); + Assert.Equal(result, modifiedSize); + } + + else if (paxEntry.EntryType is TarEntryType.SymbolicLink) + { + Assert.Equal(paxEntry.ExtendedAttributes[PaxEaLinkName], modifiedLinkName); + } + } + } } } diff --git a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.File.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.File.Tests.cs index b5bf3f59022bb7..c29525dcadad0d 100644 --- a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.File.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.File.Tests.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.IO; +using System.IO.Compression; +using System.Linq; using System.Threading.Tasks; using Xunit; @@ -223,5 +225,81 @@ public async Task Add_SymbolicLink_Async(TarEntryFormat format, bool createTarge } } } + + [Theory] + [MemberData(nameof(WriteEntry_CopyArchive_Data))] + public async Task WriteEntry_CopyArchiveAsync(TestTarFormat testFormat, string testCaseName, CompressionMethod compressionMethod, bool copyData) + { + TarEntryFormat entryFormat = GetEntryFormatForTestTarFormat(testFormat); + string pathWithExpectedFiles = GetTestCaseUnarchivedFolderPath(testCaseName); + pathWithExpectedFiles = PathInternal.EnsureTrailingSeparator(pathWithExpectedFiles); + + await using Stream fileMemoryStream = GetTarMemoryStream(compressionMethod, testFormat, testCaseName); + await using Stream originArchive = compressionMethod == CompressionMethod.GZip ? new GZipStream(fileMemoryStream, CompressionMode.Decompress) : fileMemoryStream; + + await VerifyCopyArchiveAsync(pathWithExpectedFiles, originArchive, entryFormat, copyData); + } + + protected async Task VerifyCopyArchiveAsync(string pathWithExpectedFiles, Stream originArchive, TarEntryFormat entryFormat, bool copyData) + { + TarEntry entry; + + await using MemoryStream destinationArchive = new MemoryStream(); + await using (TarReader reader = new TarReader(originArchive, leaveOpen: false)) + { + await using (TarWriter writer = new TarWriter(destinationArchive, entryFormat, leaveOpen: true)) + { + while ((entry = await reader.GetNextEntryAsync(copyData)) != null) + { + if (entry is PaxTarEntry paxEntry) + { + paxEntry.GroupName = TestLongGName; + paxEntry.UserName = TestLongUName; + } + await writer.WriteEntryAsync(entry); + } + } + } + + destinationArchive.Position = 0; + + Dictionary expectedEntries = GetFileSystemInfosRecursive(pathWithExpectedFiles).ToDictionary(fsi => + fsi.FullName.Substring(pathWithExpectedFiles.Length).Replace(Path.DirectorySeparatorChar, '/')); + + int count = 0; + await using (TarReader destinationReader = new TarReader(destinationArchive, leaveOpen: false)) + { + while ((entry = await destinationReader.GetNextEntryAsync(copyData)) != null) + { + if (entry is PaxGlobalExtendedAttributesTarEntry) // Metadata entry + { + continue; + } + string keyPath = Path.TrimEndingDirectorySeparator(entry.Name); + Assert.True(expectedEntries.TryGetValue(keyPath, out FileSystemInfo expectedFSI), $"Entry '{keyPath}' not found in FileSystemInfos dictionary."); + + // Cannot compare entry.LinkName with expectedFSI.LinkTarget because links in runtime-assets are not stored by nuget as such, but as regular files + if (entry.EntryType is TarEntryType.HardLink or TarEntryType.SymbolicLink) + { + AssertExtensions.GreaterThan(entry.LinkName.Length, 0, $"Entry LinkName length is 0."); + string expectedLinkTargetPath = Path.Join(pathWithExpectedFiles, entry.LinkName); + Assert.True(Path.Exists(expectedLinkTargetPath), $"Link target does not exist: {expectedLinkTargetPath}"); + } + else + { + Assert.Equal(string.Empty, entry.LinkName); + } + + if (entry is PaxTarEntry paxEntry) + { + Assert.Equal(TestLongGName, paxEntry.GroupName); + Assert.Equal(TestLongUName, paxEntry.UserName); + } + + count++; + } + } + Assert.Equal(expectedEntries.Count, count); + } } }