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 d8edb5b5f03602..f37ee088b800fa 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
@@ -46,12 +46,13 @@ public PaxTarEntry(TarEntryType entryType, string entryName)
/// The type of the entry.
/// A string with the path and file name of this entry.
/// An enumeration of string key-value pairs that represents the metadata to include in the Extended Attributes entry that precedes the current entry.
- /// When creating an instance using the constructor, only the following entry types are supported:
+ /// When creating an instance using the constructor, only the following entry types are supported:
///
/// In all platforms: , , , .
/// In Unix platforms only: , and .
///
- /// The specified are additional attributes to be used for the entry.
+ /// The specified are additional attributes to be used for the entry. If any of the provided extended attributes correspond to standard entry properties (such as path, mtime, uid, gid, uname, gname, linkpath, devmajor, or devminor), those values are applied to the corresponding properties. The parameter always takes precedence over a path extended attribute if both are specified.
+ /// Setting a property after construction will update the corresponding extended attribute.
/// It may include PAX attributes like:
///
/// Access time, under the name atime, as a number.
@@ -68,7 +69,11 @@ public PaxTarEntry(TarEntryType entryType, string entryName, IEnumerable
@@ -90,7 +95,7 @@ public PaxTarEntry(TarEntry other)
if (other is PaxTarEntry paxOther)
{
- _header.AddExtendedAttributes(paxOther.ExtendedAttributes);
+ _header.ReplaceNormalAttributesWithExtended(paxOther.ExtendedAttributes);
}
else if (other is GnuTarEntry gnuOther)
{
@@ -108,7 +113,8 @@ public PaxTarEntry(TarEntry other)
///
/// Returns the extended attributes for this entry.
///
- /// The extended attributes are specified when constructing an entry and updated with additional attributes when the entry is written. Use to append custom extended attributes.
+ /// The extended attributes are specified when constructing an entry. All provided extended attributes are preserved, including those whose values fit within the standard header fields.
+ /// Setting properties such as , , , , , , , , or will update the corresponding extended attribute to keep properties and extended attributes synchronized.
/// The following common PAX attributes may be included:
///
/// Modification time, under the name mtime, as a number.
@@ -120,7 +126,7 @@ public PaxTarEntry(TarEntry other)
/// File length, under the name size, as an .
///
///
- public IReadOnlyDictionary ExtendedAttributes => field ??= _header.ExtendedAttributes.AsReadOnly();
+ public IReadOnlyDictionary ExtendedAttributes => field ??= _header.GetPopulatedExtendedAttributes().AsReadOnly();
// Determines if the current instance's entry type supports setting a data stream.
internal override bool IsDataStreamSetterSupported() => EntryType == TarEntryType.RegularFile;
diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/PosixTarEntry.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/PosixTarEntry.cs
index 72d1396f83beed..0187aadd7b9865 100644
--- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/PosixTarEntry.cs
+++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/PosixTarEntry.cs
@@ -48,7 +48,7 @@ internal PosixTarEntry(TarEntry other, TarEntryFormat format)
///
/// When the current entry represents a character device or a block device, the major number identifies the driver associated with the device.
///
- /// Character and block devices are Unix-specific entry types.
+ /// Character and block devices are Unix-specific entry types. For PAX entries, setting this property updates the corresponding devmajor extended attribute in .
/// The entry does not represent a block device or a character device.
/// The value is negative, or larger than 2097151 when using or .
public int DeviceMajor
@@ -64,17 +64,18 @@ public int DeviceMajor
ArgumentOutOfRangeException.ThrowIfNegative(value);
if (FormatIsOctalOnly)
{
- ArgumentOutOfRangeException.ThrowIfGreaterThan(value, 0x1FFFFF); // 7777777 in octal
+ ArgumentOutOfRangeException.ThrowIfGreaterThan(value, TarHeader.Octal8ByteFieldMaxValue);
}
_header._devMajor = value;
+ _header.SyncNumericExtendedAttribute(TarHeader.PaxEaDevMajor, value, TarHeader.Octal8ByteFieldMaxValue);
}
}
///
/// When the current entry represents a character device or a block device, the minor number is used by the driver to distinguish individual devices it controls.
///
- /// Character and block devices are Unix-specific entry types.
+ /// Character and block devices are Unix-specific entry types. For PAX entries, setting this property updates the corresponding devminor extended attribute in .
/// The entry does not represent a block device or a character device.
/// The value is negative, or larger than 2097151 when using or .
public int DeviceMinor
@@ -90,10 +91,11 @@ public int DeviceMinor
ArgumentOutOfRangeException.ThrowIfNegative(value);
if (FormatIsOctalOnly)
{
- ArgumentOutOfRangeException.ThrowIfGreaterThan(value, 0x1FFFFF); // 7777777 in octal
+ ArgumentOutOfRangeException.ThrowIfGreaterThan(value, TarHeader.Octal8ByteFieldMaxValue);
}
_header._devMinor = value;
+ _header.SyncNumericExtendedAttribute(TarHeader.PaxEaDevMinor, value, TarHeader.Octal8ByteFieldMaxValue);
}
}
@@ -101,7 +103,7 @@ public int DeviceMinor
/// Represents the name of the group that owns this entry.
///
/// Cannot set a null group name.
- /// is only used in Unix platforms.
+ /// is only used in Unix platforms. For PAX entries, setting this property updates the corresponding gname extended attribute in .
public string GroupName
{
get => _header._gName ?? string.Empty;
@@ -109,13 +111,14 @@ public string GroupName
{
ArgumentNullException.ThrowIfNull(value);
_header._gName = value;
+ _header.SyncStringExtendedAttribute(TarHeader.PaxEaGName, value, FieldLengths.GName);
}
}
///
/// Represents the name of the user that owns this entry.
///
- /// is only used in Unix platforms.
+ /// is only used in Unix platforms. For PAX entries, setting this property updates the corresponding uname extended attribute in .
/// Cannot set a null user name.
public string UserName
{
@@ -124,6 +127,7 @@ public string UserName
{
ArgumentNullException.ThrowIfNull(value);
_header._uName = value;
+ _header.SyncStringExtendedAttribute(TarHeader.PaxEaUName, value, FieldLengths.UName);
}
}
}
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 6e8382552e4d4b..50d0543dea5aa8 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
@@ -84,17 +84,21 @@ internal TarEntry(TarEntry other, TarEntryFormat format)
///
/// The ID of the group that owns the file represented by this entry.
///
- /// This field is only supported in Unix platforms.
+ /// This field is only supported in Unix platforms. For PAX entries, setting this property updates the corresponding gid extended attribute in .
public int Gid
{
get => _header._gid;
- set => _header._gid = value;
+ set
+ {
+ _header._gid = value;
+ _header.SyncNumericExtendedAttribute(TarHeader.PaxEaGid, value, TarHeader.Octal8ByteFieldMaxValue);
+ }
}
///
/// A timestamps that represents the last time the contents of the file represented by this entry were modified.
///
- /// In Unix platforms, this timestamp is commonly known as mtime.
+ /// In Unix platforms, this timestamp is commonly known as mtime. For PAX entries, setting this property updates the corresponding mtime extended attribute in .
/// The specified value is larger than when using or .
public DateTimeOffset ModificationTime
{
@@ -106,6 +110,7 @@ public DateTimeOffset ModificationTime
ArgumentOutOfRangeException.ThrowIfLessThan(value, DateTimeOffset.UnixEpoch);
}
_header._mTime = value;
+ _header.SyncTimestampExtendedAttribute(TarHeader.PaxEaMTime, value);
}
}
@@ -118,6 +123,7 @@ public DateTimeOffset ModificationTime
///
/// When the indicates a or a , this property returns the link target path of such link.
///
+ /// For PAX entries, setting this property updates the corresponding linkpath extended attribute in .
/// The entry type is not or .
/// The specified value is .
/// The specified value is empty.
@@ -132,6 +138,7 @@ public string LinkName
}
ArgumentException.ThrowIfNullOrEmpty(value);
_header._linkName = value;
+ _header.SyncStringExtendedAttribute(TarHeader.PaxEaLinkName, value);
}
}
@@ -157,6 +164,7 @@ public UnixFileMode Mode
///
/// Represents the name of the entry, which includes the relative path and the filename.
///
+ /// For PAX entries, setting this property updates the corresponding path extended attribute in .
public string Name
{
get => _header._name;
@@ -164,17 +172,22 @@ public string Name
{
ArgumentException.ThrowIfNullOrEmpty(value);
_header._name = value;
+ _header.SyncStringExtendedAttribute(TarHeader.PaxEaName, value);
}
}
///
/// The ID of the user that owns the file represented by this entry.
///
- /// This field is only supported in Unix platforms.
+ /// This field is only supported in Unix platforms. For PAX entries, setting this property updates the corresponding uid extended attribute in .
public int Uid
{
get => _header._uid;
- set => _header._uid = value;
+ set
+ {
+ _header._uid = value;
+ _header.SyncNumericExtendedAttribute(TarHeader.PaxEaUid, value, TarHeader.Octal8ByteFieldMaxValue);
+ }
}
///
@@ -591,10 +604,10 @@ private FileStreamOptions CreateFileStreamOptions(bool isAsync)
if (!OperatingSystem.IsWindows())
{
- const UnixFileMode OwnershipPermissions =
- UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute |
- UnixFileMode.GroupRead | UnixFileMode.GroupWrite | UnixFileMode.GroupExecute |
- UnixFileMode.OtherRead | UnixFileMode.OtherWrite | UnixFileMode.OtherExecute;
+ const UnixFileMode OwnershipPermissions =
+ UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute |
+ UnixFileMode.GroupRead | UnixFileMode.GroupWrite | UnixFileMode.GroupExecute |
+ UnixFileMode.OtherRead | UnixFileMode.OtherWrite | UnixFileMode.OtherExecute;
// Restore permissions.
// For security, limit to ownership permissions, and respect umask (through UnixCreateMode).
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 c39df542aa88d0..feae728117ab8a 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
@@ -103,16 +103,24 @@ internal sealed partial class TarHeader
// If any of the dictionary entries use the name of a standard attribute, that attribute's value gets replaced with the one from the dictionary.
// Unlike the historic header, numeric values in extended attributes are stored using decimal, not octal.
// Throws if any conversion from string to the expected data type fails.
- internal void ReplaceNormalAttributesWithExtended(Dictionary? dictionaryFromExtendedAttributesHeader)
+ internal void ReplaceNormalAttributesWithExtended(IEnumerable>? extendedAttributes)
{
- if (dictionaryFromExtendedAttributesHeader == null || dictionaryFromExtendedAttributesHeader.Count == 0)
+ if (extendedAttributes == null)
{
return;
}
- AddExtendedAttributes(dictionaryFromExtendedAttributesHeader);
+ AddExtendedAttributes(extendedAttributes);
- // Find all the extended attributes with known names and save them in the expected standard attribute.
+ if (_ea == null || _ea.Count == 0)
+ {
+ // no extended attributes were added, so we can skip the rest of the processing
+ return;
+ }
+
+ // Find all the extended attributes with known names and save them
+ // in the expected standard attribute. Extended attributes are preserved
+ // as-is to avoid data loss during roundtripping.
// The 'name' header field only fits 100 bytes, so we always store the full name text to the dictionary.
if (ExtendedAttributes.TryGetValue(PaxEaName, out string? paxEaName))
@@ -121,7 +129,9 @@ internal void ReplaceNormalAttributesWithExtended(Dictionary? di
}
// 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))
+ // Only apply to link entries to avoid setting _linkName on non-link entry types.
+ if (_typeFlag is TarEntryType.HardLink or TarEntryType.SymbolicLink &&
+ ExtendedAttributes.TryGetValue(PaxEaLinkName, out string? paxEaLinkName))
{
_linkName = paxEaLinkName;
}
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 8a4ee54bac4f9f..06c9d8ea52a459 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
@@ -17,8 +17,8 @@ namespace System.Formats.Tar
// Writes header attributes of a tar archive entry.
internal sealed partial class TarHeader
{
- private const long Octal12ByteFieldMaxValue = (1L << (3 * 11)) - 1; // Max value of 11 octal digits.
- private const int Octal8ByteFieldMaxValue = (1 << (3 * 7)) - 1; // Max value of 7 octal digits.
+ internal const long Octal12ByteFieldMaxValue = (1L << (3 * 11)) - 1; // Max value of 11 octal digits.
+ internal const int Octal8ByteFieldMaxValue = (1 << (3 * 7)) - 1; // Max value of 7 octal digits.
private static ReadOnlySpan UstarMagicBytes => "ustar\0"u8;
private static ReadOnlySpan UstarVersionBytes => "00"u8;
@@ -942,75 +942,14 @@ static int CountDigits(int value)
// extended attributes. They get collected and saved in that dictionary, with no restrictions.
private void CollectExtendedAttributesFromStandardFieldsIfNeeded()
{
- ExtendedAttributes[PaxEaName] = _name;
- ExtendedAttributes[PaxEaMTime] = TarHelpers.GetTimestampStringFromDateTimeOffset(_mTime);
-
- TryAddStringField(ExtendedAttributes, PaxEaGName, _gName, FieldLengths.GName);
- TryAddStringField(ExtendedAttributes, PaxEaUName, _uName, FieldLengths.UName);
-
- if (!string.IsNullOrEmpty(_linkName))
- {
- Debug.Assert(_typeFlag is TarEntryType.SymbolicLink or TarEntryType.HardLink);
- ExtendedAttributes[PaxEaLinkName] = _linkName;
- }
-
- if (_size > Octal12ByteFieldMaxValue)
- {
- ExtendedAttributes[PaxEaSize] = _size.ToString();
- }
- else
- {
- ExtendedAttributes.Remove(PaxEaSize);
- }
-
- if (_uid > Octal8ByteFieldMaxValue)
- {
- ExtendedAttributes[PaxEaUid] = _uid.ToString();
- }
- else
- {
- ExtendedAttributes.Remove(PaxEaUid);
- }
-
- if (_gid > Octal8ByteFieldMaxValue)
- {
- ExtendedAttributes[PaxEaGid] = _gid.ToString();
- }
- else
- {
- ExtendedAttributes.Remove(PaxEaGid);
- }
-
- if (_devMajor > Octal8ByteFieldMaxValue)
- {
- ExtendedAttributes[PaxEaDevMajor] = _devMajor.ToString();
- }
- else
- {
- ExtendedAttributes.Remove(PaxEaDevMajor);
- }
-
- if (_devMinor > Octal8ByteFieldMaxValue)
- {
- ExtendedAttributes[PaxEaDevMinor] = _devMinor.ToString();
- }
- else
- {
- ExtendedAttributes.Remove(PaxEaDevMinor);
- }
+ CollectExtendedAttributesFromStandardFieldsIfNeeded(_ea ??= new Dictionary());
+ }
- // Sets the specified string to the dictionary if it's longer than the specified max byte length; otherwise, remove it.
- static void TryAddStringField(Dictionary extendedAttributes, string key, string? value, int maxLength)
- {
- if (string.IsNullOrEmpty(value) || GetUtf8TextLength(value) <= maxLength)
- {
- extendedAttributes.Remove(key);
- }
- else
- {
- extendedAttributes[key] = value;
- }
- }
+ // At write time, we both add and remove entries to ensure the EA dictionary
+ // is fully normalized. Delegates to the shared helper with removeIfUnneeded: true.
+ private void CollectExtendedAttributesFromStandardFieldsIfNeeded(Dictionary ea)
+ {
+ AddOrUpdateStandardFieldExtendedAttributes(ea, removeIfUnneeded: true);
}
// The checksum accumulator first adds up the byte values of eight space chars, then the final number
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 35da5b566ac37f..593f227c00de63 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
@@ -25,19 +25,19 @@ internal sealed partial class TarHeader
private const string GnuVersion = " \0";
// Names of PAX extended attributes commonly found fields
- private const string PaxEaName = "path";
- private const string PaxEaLinkName = "linkpath";
- private const string PaxEaMode = "mode";
- private const string PaxEaGName = "gname";
- private const string PaxEaUName = "uname";
- private const string PaxEaGid = "gid";
- private const string PaxEaUid = "uid";
+ internal const string PaxEaName = "path";
+ internal const string PaxEaLinkName = "linkpath";
+ internal const string PaxEaMode = "mode";
+ internal const string PaxEaGName = "gname";
+ internal const string PaxEaUName = "uname";
+ internal const string PaxEaGid = "gid";
+ internal const string PaxEaUid = "uid";
internal const string PaxEaATime = "atime";
internal const string PaxEaCTime = "ctime";
- private const string PaxEaMTime = "mtime";
- private const string PaxEaSize = "size";
- private const string PaxEaDevMajor = "devmajor";
- private const string PaxEaDevMinor = "devminor";
+ internal const string PaxEaMTime = "mtime";
+ internal const string PaxEaSize = "size";
+ internal const string PaxEaDevMajor = "devmajor";
+ internal const string PaxEaDevMinor = "devminor";
internal Stream? _dataStream;
internal long _dataOffset;
@@ -152,5 +152,118 @@ internal void AddExtendedAttributes(IEnumerable> ex
// if the archive stream is seekable. Otherwise, -1.
private static void SetDataOffset(TarHeader header, Stream archiveStream) =>
header._dataOffset = archiveStream.CanSeek ? archiveStream.Position : -1;
+
+ // Synchronizes the extended attributes dictionary with the value of a property.
+ // Only updates if the format is PAX and the ExtendedAttributes dictionary has been initialized.
+ // When maxUtf8ByteLength is 0 (default), the value is always added to EA if non-empty.
+ // This is intentional for "path" and "linkpath" which always belong in extended attributes.
+ internal void SyncStringExtendedAttribute(string key, string? value, int maxUtf8ByteLength = 0)
+ {
+ if (_format == TarEntryFormat.Pax && _ea is not null)
+ {
+ if (!string.IsNullOrEmpty(value) && GetUtf8TextLength(value) > maxUtf8ByteLength)
+ {
+ _ea[key] = value;
+ }
+ else
+ {
+ _ea.Remove(key);
+ }
+ }
+ }
+
+ // Synchronizes the extended attributes dictionary with a timestamp property.
+ // Only updates if the format is PAX and the ExtendedAttributes dictionary has been initialized.
+ internal void SyncTimestampExtendedAttribute(string key, DateTimeOffset value)
+ {
+ if (_format == TarEntryFormat.Pax && _ea is not null)
+ {
+ _ea[key] = TarHelpers.GetTimestampStringFromDateTimeOffset(value);
+ }
+ }
+
+ // Synchronizes the extended attributes dictionary with a numeric property.
+ // Only updates if the format is PAX and the ExtendedAttributes dictionary has been initialized.
+ // Uses the same logic as CollectExtendedAttributesFromStandardFieldsIfNeeded to determine
+ // whether to add or remove the attribute.
+ internal void SyncNumericExtendedAttribute(string key, int value, int maxNonextendedValue)
+ {
+ if (_format == TarEntryFormat.Pax && _ea is not null)
+ {
+ if (value > maxNonextendedValue)
+ {
+ _ea[key] = value.ToString();
+ }
+ else
+ {
+ _ea.Remove(key);
+ }
+ }
+ }
+
+ internal Dictionary GetPopulatedExtendedAttributes()
+ {
+ PopulateExtendedAttributesFromStandardFields(ExtendedAttributes);
+ return ExtendedAttributes;
+ }
+
+ // Ensures standard fields are present in the extended attributes dictionary
+ // without removing any existing entries. Used for populating the EA dictionary
+ // for read access. Delegates to the shared helper with removeIfUnneeded: false.
+ private void PopulateExtendedAttributesFromStandardFields(Dictionary ea)
+ {
+ AddOrUpdateStandardFieldExtendedAttributes(ea, removeIfUnneeded: false);
+ }
+
+ // Shared helper that adds standard header field values to extended attributes.
+ // When removeIfUnneeded is true (write-time), entries that fit in standard fields
+ // are removed from the dictionary. When false (read-time/populate), only entries
+ // that exceed standard field capacity are added — existing keys are never removed.
+ private void AddOrUpdateStandardFieldExtendedAttributes(Dictionary ea, bool removeIfUnneeded)
+ {
+ ea[PaxEaName] = _name;
+ ea[PaxEaMTime] = TarHelpers.GetTimestampStringFromDateTimeOffset(_mTime);
+
+ AddOrRemoveStringField(ea, PaxEaGName, _gName, FieldLengths.GName, removeIfUnneeded);
+ AddOrRemoveStringField(ea, PaxEaUName, _uName, FieldLengths.UName, removeIfUnneeded);
+
+ if (!string.IsNullOrEmpty(_linkName))
+ {
+ // The LinkName is stored unconditionally (not doing so might
+ // break users depending on existing behavior).
+ Debug.Assert(_typeFlag is TarEntryType.SymbolicLink or TarEntryType.HardLink);
+ ea[PaxEaLinkName] = _linkName;
+ }
+
+ AddOrRemoveNumericField(ea, PaxEaSize, _size, Octal12ByteFieldMaxValue, removeIfUnneeded);
+ AddOrRemoveNumericField(ea, PaxEaUid, _uid, Octal8ByteFieldMaxValue, removeIfUnneeded);
+ AddOrRemoveNumericField(ea, PaxEaGid, _gid, Octal8ByteFieldMaxValue, removeIfUnneeded);
+ AddOrRemoveNumericField(ea, PaxEaDevMajor, _devMajor, Octal8ByteFieldMaxValue, removeIfUnneeded);
+ AddOrRemoveNumericField(ea, PaxEaDevMinor, _devMinor, Octal8ByteFieldMaxValue, removeIfUnneeded);
+
+ static void AddOrRemoveStringField(Dictionary ea, string key, string? value, int maxUtf8ByteLength, bool removeIfUnneeded)
+ {
+ if (!string.IsNullOrEmpty(value) && GetUtf8TextLength(value) > maxUtf8ByteLength)
+ {
+ ea[key] = value;
+ }
+ else if (removeIfUnneeded)
+ {
+ ea.Remove(key);
+ }
+ }
+
+ static void AddOrRemoveNumericField(Dictionary ea, string key, long value, long maxNonextendedValue, bool removeIfUnneeded)
+ {
+ if (value > maxNonextendedValue)
+ {
+ ea[key] = value.ToString();
+ }
+ else if (removeIfUnneeded)
+ {
+ ea.Remove(key);
+ }
+ }
+ }
}
}
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 8f85711e7eaf3f..11085c750251fc 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
@@ -31,6 +31,7 @@
+
diff --git a/src/libraries/System.Formats.Tar/tests/TarEntry/PaxTarEntry.ExtendedAttributes.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarEntry/PaxTarEntry.ExtendedAttributes.Tests.cs
new file mode 100644
index 00000000000000..f0c0c99900f905
--- /dev/null
+++ b/src/libraries/System.Formats.Tar/tests/TarEntry/PaxTarEntry.ExtendedAttributes.Tests.cs
@@ -0,0 +1,296 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+using System.IO;
+using Xunit;
+
+namespace System.Formats.Tar.Tests
+{
+ public class PaxTarEntry_ExtendedAttributes_Tests : TarTestsBase
+ {
+ // helper to create getters and setters for test data
+ static (Func getter, Action setter) Accessors(Func getter, Action setter) =>
+ (entry => getter(entry), (entry, value) => setter(entry, (T)value));
+
+ public static IEnumerable