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