Skip to content

Commit d3f7e01

Browse files
authored
Implement Tar Global Extended Attributes API changes (#70869)
* ref: Global Extended Attributes API changes * src: Global Extended Attributes API changes * tests: Verify Global Extended Attributes API changes * Address suggestions * Address debug suggestion. * Improve the decision of reducing the size of the GEA path. * Use Path.GetTempPath for the GEA path. * Rename field tracking GEA entry number. Co-authored-by: carlossanlop <[email protected]>
1 parent b1839d3 commit d3f7e01

23 files changed

+986
-626
lines changed

src/libraries/System.Formats.Tar/ref/System.Formats.Tar.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ public GnuTarEntry(System.Formats.Tar.TarEntryType entryType, string entryName)
1313
public System.DateTimeOffset AccessTime { get { throw null; } set { } }
1414
public System.DateTimeOffset ChangeTime { get { throw null; } set { } }
1515
}
16+
public sealed partial class PaxGlobalExtendedAttributesTarEntry : System.Formats.Tar.PosixTarEntry
17+
{
18+
public PaxGlobalExtendedAttributesTarEntry(System.Collections.Generic.IEnumerable<System.Collections.Generic.KeyValuePair<string, string>> globalExtendedAttributes) { }
19+
public System.Collections.Generic.IReadOnlyDictionary<string, string> GlobalExtendedAttributes { get { throw null; } }
20+
}
1621
public sealed partial class PaxTarEntry : System.Formats.Tar.PosixTarEntry
1722
{
1823
public PaxTarEntry(System.Formats.Tar.TarEntry other) { }
@@ -101,14 +106,13 @@ public enum TarFileMode
101106
public sealed partial class TarReader : System.IDisposable
102107
{
103108
public TarReader(System.IO.Stream archiveStream, bool leaveOpen = false) { }
104-
public System.Collections.Generic.IReadOnlyDictionary<string, string>? GlobalExtendedAttributes { get { throw null; } }
105109
public void Dispose() { }
106110
public System.Formats.Tar.TarEntry? GetNextEntry(bool copyData = false) { throw null; }
107111
}
108112
public sealed partial class TarWriter : System.IDisposable
109113
{
110114
public TarWriter(System.IO.Stream archiveStream) { }
111-
public TarWriter(System.IO.Stream archiveStream, System.Collections.Generic.IEnumerable<System.Collections.Generic.KeyValuePair<string, string>>? globalExtendedAttributes = null, bool leaveOpen = false) { }
115+
public TarWriter(System.IO.Stream archiveStream, bool leaveOpen = false) { }
112116
public TarWriter(System.IO.Stream archiveStream, System.Formats.Tar.TarEntryFormat format = System.Formats.Tar.TarEntryFormat.Pax, bool leaveOpen = false) { }
113117
public System.Formats.Tar.TarEntryFormat Format { get { throw null; } }
114118
public void Dispose() { }

src/libraries/System.Formats.Tar/src/Resources/Strings.resx

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -186,12 +186,12 @@
186186
<data name="SetLengthRequiresSeekingAndWriting" xml:space="preserve">
187187
<value>SetLength requires a stream that supports seeking and writing.</value>
188188
</data>
189+
<data name="TarCannotConvertPaxGlobalExtendedAttributesEntry" xml:space="preserve">
190+
<value>Cannot convert a PaxGlobalExtendedAttributesEntry into another format.</value>
191+
</data>
189192
<data name="TarDuplicateExtendedAttribute" xml:space="preserve">
190193
<value>The entry '{0}' has a duplicate extended attribute.</value>
191194
</data>
192-
<data name="TarEntriesInDifferentFormats" xml:space="preserve">
193-
<value>An entry in '{0}' format was found in an archive where other entries of format '{1}' have been found.</value>
194-
</data>
195195
<data name="TarEntryBlockOrCharacterExpected" xml:space="preserve">
196196
<value>Cannot set the 'DeviceMajor' or 'DeviceMinor' fields on an entry that does not represent a block or character device.</value>
197197
</data>
@@ -240,9 +240,6 @@
240240
<data name="TarSymbolicLinkTargetNotExists" xml:space="preserve">
241241
<value>Cannot create the symbolic link '{0}' because the specified target '{1}' does not exist.</value>
242242
</data>
243-
<data name="TarTooManyGlobalExtendedAttributesEntries" xml:space="preserve">
244-
<value>The archive has more than one global extended attributes entry.</value>
245-
</data>
246243
<data name="TarUnexpectedMetadataEntry" xml:space="preserve">
247244
<value>A metadata entry of type '{0}' was unexpectedly found after a metadata entry of type '{1}'.</value>
248245
</data>

src/libraries/System.Formats.Tar/src/System.Formats.Tar.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
<Compile Include="System\Formats\Tar\TarEntryFormat.cs" />
2525
<Compile Include="System\Formats\Tar\UstarTarEntry.cs" />
2626
<Compile Include="System\Formats\Tar\GnuTarEntry.cs" />
27+
<Compile Include="System\Formats\Tar\PaxGlobalExtendedAttributesTarEntry.cs" />
2728
<Compile Include="System\Formats\Tar\PaxTarEntry.cs" />
2829
<Compile Include="System\Formats\Tar\TarEntryType.cs" />
2930
<Compile Include="System\Formats\Tar\TarFile.cs" />

src/libraries/System.Formats.Tar/src/System/Formats/Tar/GnuTarEntry.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ internal GnuTarEntry(TarHeader header, TarReader readerOfOrigin)
2929
/// </list>
3030
/// </remarks>
3131
public GnuTarEntry(TarEntryType entryType, string entryName)
32-
: base(entryType, entryName, TarEntryFormat.Gnu)
32+
: base(entryType, entryName, TarEntryFormat.Gnu, isGea: false)
3333
{
3434
_header._aTime = _header._mTime; // mtime was set in base constructor
3535
_header._cTime = _header._mTime;
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Collections.Generic;
5+
using System.Collections.ObjectModel;
6+
using System.Diagnostics;
7+
8+
namespace System.Formats.Tar
9+
{
10+
/// <summary>
11+
/// Represents a Global Extended Attributes TAR entry from an archive of the PAX format.
12+
/// </summary>
13+
public sealed class PaxGlobalExtendedAttributesTarEntry : PosixTarEntry
14+
{
15+
private ReadOnlyDictionary<string, string>? _readOnlyGlobalExtendedAttributes;
16+
17+
// Constructor used when reading an existing archive.
18+
internal PaxGlobalExtendedAttributesTarEntry(TarHeader header, TarReader readerOfOrigin)
19+
: base(header, readerOfOrigin, TarEntryFormat.Pax)
20+
{
21+
}
22+
23+
/// <summary>
24+
/// Initializes a new <see cref="PaxGlobalExtendedAttributesTarEntry"/> instance with the specified Global Extended Attributes enumeration.
25+
/// </summary>
26+
/// <param name="globalExtendedAttributes">An enumeration of string key-value pairs that represents the metadata to include as Global Extended Attributes.</param>
27+
/// <exception cref="ArgumentNullException"><paramref name="globalExtendedAttributes"/> is <see langword="null"/>.</exception>
28+
public PaxGlobalExtendedAttributesTarEntry(IEnumerable<KeyValuePair<string, string>> globalExtendedAttributes)
29+
: base(TarEntryType.GlobalExtendedAttributes, TarHeader.GlobalHeadFormatPrefix, TarEntryFormat.Pax, isGea: true)
30+
{
31+
ArgumentNullException.ThrowIfNull(globalExtendedAttributes);
32+
_header._extendedAttributes = new Dictionary<string, string>(globalExtendedAttributes);
33+
}
34+
35+
/// <summary>
36+
/// Returns the global extended attributes stored in this entry.
37+
/// </summary>
38+
public IReadOnlyDictionary<string, string> GlobalExtendedAttributes
39+
{
40+
get
41+
{
42+
_header._extendedAttributes ??= new Dictionary<string, string>();
43+
return _readOnlyGlobalExtendedAttributes ??= _header._extendedAttributes.AsReadOnly();
44+
}
45+
}
46+
47+
// Determines if the current instance's entry type supports setting a data stream.
48+
internal override bool IsDataStreamSetterSupported() => false;
49+
}
50+
}

src/libraries/System.Formats.Tar/src/System/Formats/Tar/PaxTarEntry.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ internal PaxTarEntry(TarHeader header, TarReader readerOfOrigin)
4848
/// </list>
4949
/// </remarks>
5050
public PaxTarEntry(TarEntryType entryType, string entryName)
51-
: base(entryType, entryName, TarEntryFormat.Pax)
51+
: base(entryType, entryName, TarEntryFormat.Pax, isGea: false)
5252
{
5353
_header._prefix = string.Empty;
5454
_header._extendedAttributes = new Dictionary<string, string>();
@@ -87,7 +87,7 @@ public PaxTarEntry(TarEntryType entryType, string entryName)
8787
/// </list>
8888
/// </remarks>
8989
public PaxTarEntry(TarEntryType entryType, string entryName, IEnumerable<KeyValuePair<string, string>> extendedAttributes)
90-
: base(entryType, entryName, TarEntryFormat.Pax)
90+
: base(entryType, entryName, TarEntryFormat.Pax, isGea: false)
9191
{
9292
ArgumentNullException.ThrowIfNull(extendedAttributes);
9393

src/libraries/System.Formats.Tar/src/System/Formats/Tar/PosixTarEntry.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ internal PosixTarEntry(TarHeader header, TarReader readerOfOrigin, TarEntryForma
1919
}
2020

2121
// Constructor called when the user creates a TarEntry instance from scratch.
22-
internal PosixTarEntry(TarEntryType entryType, string entryName, TarEntryFormat format)
23-
: base(entryType, entryName, format)
22+
internal PosixTarEntry(TarEntryType entryType, string entryName, TarEntryFormat format, bool isGea)
23+
: base(entryType, entryName, format, isGea)
2424
{
2525
_header._uName = string.Empty;
2626
_header._gName = string.Empty;

src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,16 @@ internal TarEntry(TarHeader header, TarReader readerOfOrigin, TarEntryFormat for
2929
}
3030

3131
// Constructor called when the user creates a TarEntry instance from scratch.
32-
internal TarEntry(TarEntryType entryType, string entryName, TarEntryFormat format)
32+
internal TarEntry(TarEntryType entryType, string entryName, TarEntryFormat format, bool isGea)
3333
{
3434
ArgumentException.ThrowIfNullOrEmpty(entryName);
35-
TarHelpers.ThrowIfEntryTypeNotSupported(entryType, format);
35+
36+
Debug.Assert(!isGea || entryType is TarEntryType.GlobalExtendedAttributes);
37+
38+
if (!isGea)
39+
{
40+
TarHelpers.ThrowIfEntryTypeNotSupported(entryType, format);
41+
}
3642

3743
_header = default;
3844
_header._format = format;
@@ -48,6 +54,11 @@ internal TarEntry(TarEntryType entryType, string entryName, TarEntryFormat forma
4854
// Constructor called when converting an entry to the selected format.
4955
internal TarEntry(TarEntry other, TarEntryFormat format)
5056
{
57+
if (other is PaxGlobalExtendedAttributesTarEntry)
58+
{
59+
throw new InvalidOperationException(SR.TarCannotConvertPaxGlobalExtendedAttributesEntry);
60+
}
61+
5162
TarEntryType compatibleEntryType;
5263
if (other.Format is TarEntryFormat.V7 && other.EntryType is TarEntryType.V7RegularFile && format is TarEntryFormat.Ustar or TarEntryFormat.Pax or TarEntryFormat.Gnu)
5364
{
@@ -208,7 +219,7 @@ public int Uid
208219
/// <exception cref="UnauthorizedAccessException">Operation not permitted due to insufficient permissions.</exception>
209220
public void ExtractToFile(string destinationFileName, bool overwrite)
210221
{
211-
if (EntryType is TarEntryType.SymbolicLink or TarEntryType.HardLink)
222+
if (EntryType is TarEntryType.SymbolicLink or TarEntryType.HardLink or TarEntryType.GlobalExtendedAttributes)
212223
{
213224
throw new InvalidOperationException(string.Format(SR.TarEntryTypeNotSupportedForExtracting, EntryType));
214225
}

src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarFile.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,10 @@ private static void ExtractToDirectoryInternal(Stream source, string destination
255255
TarEntry? entry;
256256
while ((entry = reader.GetNextEntry()) != null)
257257
{
258-
entry.ExtractRelativeToDirectory(destinationDirectoryPath, overwriteFiles);
258+
if (entry is not PaxGlobalExtendedAttributesTarEntry)
259+
{
260+
entry.ExtractRelativeToDirectory(destinationDirectoryPath, overwriteFiles);
261+
}
259262
}
260263
}
261264
}

src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs

Lines changed: 25 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -22,28 +22,9 @@ internal partial struct TarHeader
2222
// "{dirName}/PaxHeaders.{processId}/{fileName}{trailingSeparator}"
2323
private const string PaxHeadersFormat = "{0}/PaxHeaders.{1}/{2}{3}";
2424

25-
// Global Extended Attribute entries have a special format in the Name field:
26-
// "{tmpFolder}/GlobalHead.{processId}.1"
27-
private const string GlobalHeadFormat = "{0}/GlobalHead.{1}.1";
28-
2925
// Predefined text for the Name field of a GNU long metadata entry. Applies for both LongPath ('L') and LongLink ('K').
3026
private const string GnuLongMetadataName = "././@LongLink";
3127

32-
// Creates a PAX Global Extended Attributes header and writes it into the specified archive stream.
33-
internal static void WriteGlobalExtendedAttributesHeader(Stream archiveStream, Span<byte> buffer, IEnumerable<KeyValuePair<string, string>> globalExtendedAttributes)
34-
{
35-
TarHeader geaHeader = default;
36-
geaHeader._name = GenerateGlobalExtendedAttributeName();
37-
geaHeader._mode = (int)TarHelpers.DefaultMode;
38-
geaHeader._typeFlag = TarEntryType.GlobalExtendedAttributes;
39-
geaHeader._linkName = string.Empty;
40-
geaHeader._magic = string.Empty;
41-
geaHeader._version = string.Empty;
42-
geaHeader._gName = string.Empty;
43-
geaHeader._uName = string.Empty;
44-
geaHeader.WriteAsPaxExtendedAttributes(archiveStream, buffer, globalExtendedAttributes, isGea: true);
45-
}
46-
4728
// Writes the current header as a V7 entry into the archive stream.
4829
internal void WriteAsV7(Stream archiveStream, Span<byte> buffer)
4930
{
@@ -82,14 +63,27 @@ internal void WriteAsUstar(Stream archiveStream, Span<byte> buffer)
8263
}
8364
}
8465

66+
// Writes the current header as a PAX Global Extended Attributes entry into the archive stream.
67+
internal void WriteAsPaxGlobalExtendedAttributes(Stream archiveStream, Span<byte> buffer, int globalExtendedAttributesEntryNumber)
68+
{
69+
Debug.Assert(_typeFlag is TarEntryType.GlobalExtendedAttributes);
70+
71+
_name = GenerateGlobalExtendedAttributeName(globalExtendedAttributesEntryNumber);
72+
_extendedAttributes ??= new Dictionary<string, string>();
73+
WriteAsPaxExtendedAttributes(archiveStream, buffer, _extendedAttributes, isGea: true);
74+
}
75+
8576
// Writes the current header as a PAX entry into the archive stream.
86-
// Makes sure to add the preceding exteded attributes entry before the actual entry.
77+
// Makes sure to add the preceding extended attributes entry before the actual entry.
8778
internal void WriteAsPax(Stream archiveStream, Span<byte> buffer)
8879
{
80+
Debug.Assert(_typeFlag is not TarEntryType.GlobalExtendedAttributes);
81+
8982
// First, we write the preceding extended attributes header
9083
TarHeader extendedAttributesHeader = default;
9184
// Fill the current header's dict
9285
CollectExtendedAttributesFromStandardFieldsIfNeeded();
86+
// And pass the attributes to the preceding extended attributes header for writing
9387
Debug.Assert(_extendedAttributes != null);
9488
extendedAttributesHeader.WriteAsPaxExtendedAttributes(archiveStream, buffer, _extendedAttributes, isGea: false);
9589

@@ -611,30 +605,30 @@ private string GenerateExtendedAttributeName()
611605
}
612606

613607
// Gets the special name for the 'name' field in a global extended attribute entry.
614-
// Format: "%d/GlobalHead.%p/%f"
608+
// Format: "%d/GlobalHead.%p/%n"
615609
// - %d: The path of the $TMPDIR variable, if found. Otherwise, the value is '/tmp'.
616610
// - %p: The current process ID.
617611
// - %n: The sequence number of the global extended header record of the archive, starting at 1. In our case, since we only generate one, the value is always 1.
618612
// If the path of $TMPDIR makes the final string too long to fit in the 'name' field,
619613
// then the TMPDIR='/tmp' is used.
620-
private static string GenerateGlobalExtendedAttributeName()
614+
private static string GenerateGlobalExtendedAttributeName(int globalExtendedAttributesEntryNumber)
621615
{
622-
string? tmpDir = Environment.GetEnvironmentVariable("TMPDIR");
623-
if (string.IsNullOrWhiteSpace(tmpDir))
624-
{
625-
tmpDir = "/tmp";
626-
}
627-
else if (Path.EndsInDirectorySeparator(tmpDir))
616+
Debug.Assert(globalExtendedAttributesEntryNumber >= 1);
617+
618+
string tmpDir = Path.GetTempPath();
619+
if (Path.EndsInDirectorySeparator(tmpDir))
628620
{
629621
tmpDir = Path.TrimEndingDirectorySeparator(tmpDir);
630622
}
631623
int processId = Environment.ProcessId;
632624

633-
string result = string.Format(GlobalHeadFormat, tmpDir, processId);
634-
if (result.Length >= FieldLengths.Name)
625+
string result = string.Format(GlobalHeadFormatPrefix, tmpDir, processId);
626+
string suffix = $".{globalExtendedAttributesEntryNumber}"; // GEA sequence number
627+
if (result.Length + suffix.Length >= FieldLengths.Name)
635628
{
636-
result = string.Format(GlobalHeadFormat, "/tmp", processId);
629+
result = string.Format(GlobalHeadFormatPrefix, "/tmp", processId);
637630
}
631+
result += suffix;
638632

639633
return result;
640634
}

0 commit comments

Comments
 (0)