Skip to content

Commit c96e01b

Browse files
authored
System.Formats.Tar: default to no ctime/atime. (#115778)
* System.Formats.Tar: default to no ctime/atime. atime and ctime are not well supported on non-PAX formats. Even for PAX formats, tools do not include these timestamps by default because they are of limited use. This makes .NET to also default to not include these timestamps. * Add back attribute lists in the XML docs. * Copy atime/ctime. * Move atime/ctime copy back to GnuTarEntry ctor. * Test conversion with atime/ctime set.
1 parent 08e6c2e commit c96e01b

16 files changed

+209
-281
lines changed

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

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Diagnostics;
5+
46
namespace System.Formats.Tar
57
{
68
/// <summary>
@@ -34,10 +36,7 @@ internal GnuTarEntry(TarHeader header, TarReader readerOfOrigin)
3436
/// <para><paramref name="entryType"/> is not supported in the specified format.</para></exception>
3537
public GnuTarEntry(TarEntryType entryType, string entryName)
3638
: base(entryType, entryName, TarEntryFormat.Gnu, isGea: false)
37-
{
38-
_header._aTime = default;
39-
_header._cTime = default;
40-
}
39+
{ }
4140

4241
/// <summary>
4342
/// Initializes a new <see cref="GnuTarEntry"/> instance by converting the specified <paramref name="other"/> entry into the GNU format.
@@ -51,21 +50,17 @@ public GnuTarEntry(TarEntryType entryType, string entryName)
5150
public GnuTarEntry(TarEntry other)
5251
: base(other, TarEntryFormat.Gnu)
5352
{
53+
// Some tools don't accept Gnu entries that have an atime/ctime.
54+
// We only copy atime/ctime for round-tripping between GnuTarEntries and clear it for other formats.
5455
if (other is GnuTarEntry gnuOther)
5556
{
5657
_header._aTime = gnuOther.AccessTime;
5758
_header._cTime = gnuOther.ChangeTime;
58-
_header._gnuUnusedBytes = other._header._gnuUnusedBytes;
5959
}
6060
else
6161
{
62-
// 'other' was V7, Ustar (those formats do not have atime or ctime),
63-
// or even PAX (which could contain atime and ctime in the ExtendedAttributes), but
64-
// to avoid creating a GNU entry that might be incompatible with other tools,
65-
// we avoid setting the atime and ctime fields. The user would have to set them manually
66-
// if they are really needed.
67-
_header._aTime = default;
68-
_header._cTime = default;
62+
Debug.Assert(_header._aTime == default);
63+
Debug.Assert(_header._cTime == default);
6964
}
7065
}
7166

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public PaxGlobalExtendedAttributesTarEntry(IEnumerable<KeyValuePair<string, stri
2828
: base(TarEntryType.GlobalExtendedAttributes, nameof(PaxGlobalExtendedAttributesTarEntry), TarEntryFormat.Pax, isGea: true) // Name == name of type for lack of a better temporary name until the entry is written
2929
{
3030
ArgumentNullException.ThrowIfNull(globalExtendedAttributes);
31-
_header.InitializeExtendedAttributesWithExisting(globalExtendedAttributes);
31+
_header.AddExtendedAttributes(globalExtendedAttributes);
3232
}
3333

3434
/// <summary>

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

Lines changed: 19 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ internal PaxTarEntry(TarHeader header, TarReader readerOfOrigin)
2121
}
2222

2323
/// <summary>
24-
/// Initializes a new <see cref="PaxTarEntry"/> instance with the specified entry type, entry name, and the default extended attributes.
24+
/// Initializes a new <see cref="PaxTarEntry"/> instance with the specified entry type and entry name.
2525
/// </summary>
2626
/// <param name="entryType">The type of the entry.</param>
2727
/// <param name="entryName">A string with the path and file name of this entry.</param>
@@ -30,20 +30,7 @@ internal PaxTarEntry(TarHeader header, TarReader readerOfOrigin)
3030
/// <item>In all platforms: <see cref="TarEntryType.Directory"/>, <see cref="TarEntryType.HardLink"/>, <see cref="TarEntryType.SymbolicLink"/>, <see cref="TarEntryType.RegularFile"/>.</item>
3131
/// <item>In Unix platforms only: <see cref="TarEntryType.BlockDevice"/>, <see cref="TarEntryType.CharacterDevice"/> and <see cref="TarEntryType.Fifo"/>.</item>
3232
/// </list>
33-
/// <para>Use the <see cref="PaxTarEntry(TarEntryType, string, IEnumerable{KeyValuePair{string, string}})"/> constructor to include additional extended attributes when creating the entry.</para>
34-
/// <para>The following entries are always found in the Extended Attributes dictionary of any PAX entry:</para>
35-
/// <list type="bullet">
36-
/// <item>Modification time, under the name <c>mtime</c>, as a <see cref="double"/> number.</item>
37-
/// <item>Access time, under the name <c>atime</c>, as a <see cref="double"/> number.</item>
38-
/// <item>Change time, under the name <c>ctime</c>, as a <see cref="double"/> number.</item>
39-
/// <item>Path, under the name <c>path</c>, as a string.</item>
40-
/// </list>
41-
/// <para>The following entries are only found in the Extended Attributes dictionary of a PAX entry if certain conditions are met:</para>
42-
/// <list type="bullet">
43-
/// <item>Group name, under the name <c>gname</c>, as a string, if it is larger than 32 bytes.</item>
44-
/// <item>User name, under the name <c>uname</c>, as a string, if it is larger than 32 bytes.</item>
45-
/// <item>File length, under the name <c>size</c>, as an <see cref="int"/>, if the string representation of the number is larger than 12 bytes.</item>
46-
/// </list>
33+
/// <para>Use the <see cref="PaxTarEntry(TarEntryType, string, IEnumerable{KeyValuePair{string, string}})"/> constructor to include extended attributes when creating the entry.</para>
4734
/// </remarks>
4835
/// <exception cref="ArgumentNullException"><paramref name="entryName"/> is <see langword="null"/>.</exception>
4936
/// <exception cref="ArgumentException"><para><paramref name="entryName"/> is empty.</para>
@@ -53,13 +40,10 @@ public PaxTarEntry(TarEntryType entryType, string entryName)
5340
: base(entryType, entryName, TarEntryFormat.Pax, isGea: false)
5441
{
5542
_header._prefix = string.Empty;
56-
57-
Debug.Assert(_header._mTime != default);
58-
AddNewAccessAndChangeTimestampsIfNotExist(useMTime: true);
5943
}
6044

6145
/// <summary>
62-
/// Initializes a new <see cref="PaxTarEntry"/> instance with the specified entry type, entry name and Extended Attributes enumeration.
46+
/// Initializes a new <see cref="PaxTarEntry"/> instance with the specified entry type, entry name and extended attributes.
6347
/// </summary>
6448
/// <param name="entryType">The type of the entry.</param>
6549
/// <param name="entryName">A string with the path and file name of this entry.</param>
@@ -69,19 +53,11 @@ public PaxTarEntry(TarEntryType entryType, string entryName)
6953
/// <item>In all platforms: <see cref="TarEntryType.Directory"/>, <see cref="TarEntryType.HardLink"/>, <see cref="TarEntryType.SymbolicLink"/>, <see cref="TarEntryType.RegularFile"/>.</item>
7054
/// <item>In Unix platforms only: <see cref="TarEntryType.BlockDevice"/>, <see cref="TarEntryType.CharacterDevice"/> and <see cref="TarEntryType.Fifo"/>.</item>
7155
/// </list>
72-
/// The specified <paramref name="extendedAttributes"/> get appended to the default attributes, unless the specified enumeration overrides any of them.
73-
/// <para>The following entries are always found in the Extended Attributes dictionary of any PAX entry:</para>
56+
/// The specified <paramref name="extendedAttributes"/> are additional attributes to be used for the entry.
57+
/// <para>It may include PAX attributes like:</para>
7458
/// <list type="bullet">
75-
/// <item>Modification time, under the name <c>mtime</c>, as a <see cref="double"/> number.</item>
7659
/// <item>Access time, under the name <c>atime</c>, as a <see cref="double"/> number.</item>
7760
/// <item>Change time, under the name <c>ctime</c>, as a <see cref="double"/> number.</item>
78-
/// <item>Path, under the name <c>path</c>, as a string.</item>
79-
/// </list>
80-
/// <para>The following entries are only found in the Extended Attributes dictionary of a PAX entry if certain conditions are met:</para>
81-
/// <list type="bullet">
82-
/// <item>Group name, under the name <c>gname</c>, as a string, if it is larger than 32 bytes.</item>
83-
/// <item>User name, under the name <c>uname</c>, as a string, if it is larger than 32 bytes.</item>
84-
/// <item>File length, under the name <c>size</c>, as an <see cref="int"/>, if the string representation of the number is larger than 12 bytes.</item>
8561
/// </list>
8662
/// </remarks>
8763
/// <exception cref="ArgumentNullException"><paramref name="extendedAttributes"/> or <paramref name="entryName"/> is <see langword="null"/>.</exception>
@@ -94,10 +70,7 @@ public PaxTarEntry(TarEntryType entryType, string entryName, IEnumerable<KeyValu
9470
ArgumentNullException.ThrowIfNull(extendedAttributes);
9571

9672
_header._prefix = string.Empty;
97-
_header.InitializeExtendedAttributesWithExisting(extendedAttributes);
98-
99-
Debug.Assert(_header._mTime != default);
100-
AddNewAccessAndChangeTimestampsIfNotExist(useMTime: true);
73+
_header.AddExtendedAttributes(extendedAttributes);
10174
}
10275

10376
/// <summary>
@@ -119,72 +92,39 @@ public PaxTarEntry(TarEntry other)
11992

12093
if (other is PaxTarEntry paxOther)
12194
{
122-
_header.InitializeExtendedAttributesWithExisting(paxOther.ExtendedAttributes);
95+
_header.AddExtendedAttributes(paxOther.ExtendedAttributes);
12396
}
124-
else
97+
else if (other is GnuTarEntry gnuOther)
12598
{
126-
if (other is GnuTarEntry gnuOther)
99+
if (gnuOther.AccessTime != default)
100+
{
101+
_header.ExtendedAttributes[TarHeader.PaxEaATime] = TarHelpers.GetTimestampStringFromDateTimeOffset(gnuOther.AccessTime);
102+
}
103+
if (gnuOther.ChangeTime != default)
127104
{
128-
if (gnuOther.AccessTime != default)
129-
{
130-
_header.ExtendedAttributes[TarHeader.PaxEaATime] = TarHelpers.GetTimestampStringFromDateTimeOffset(gnuOther.AccessTime);
131-
}
132-
if (gnuOther.ChangeTime != default)
133-
{
134-
_header.ExtendedAttributes[TarHeader.PaxEaCTime] = TarHelpers.GetTimestampStringFromDateTimeOffset(gnuOther.ChangeTime);
135-
}
105+
_header.ExtendedAttributes[TarHeader.PaxEaCTime] = TarHelpers.GetTimestampStringFromDateTimeOffset(gnuOther.ChangeTime);
136106
}
137107
}
138-
139-
AddNewAccessAndChangeTimestampsIfNotExist(useMTime: false);
140108
}
141109

142110
/// <summary>
143111
/// Returns the extended attributes for this entry.
144112
/// </summary>
145-
/// <remarks>The extended attributes are specified when constructing an entry. Use <see cref="PaxTarEntry(TarEntryType, string, IEnumerable{KeyValuePair{string, string}})"/> to append your own enumeration of extended attributes to the current entry on top of the default ones. Use <see cref="PaxTarEntry(TarEntryType, string)"/> to only use the default extended attributes.
146-
/// <para>The following entries are always found in the Extended Attributes dictionary of any PAX entry:</para>
113+
/// <remarks>The extended attributes are specified when constructing an entry and updated with additional attributes when the entry is written. Use <see cref="PaxTarEntry(TarEntryType, string, IEnumerable{KeyValuePair{string, string}})"/> to append custom extended attributes.
114+
/// <para>The following common PAX attributes may be included:</para>
147115
/// <list type="bullet">
148116
/// <item>Modification time, under the name <c>mtime</c>, as a <see cref="double"/> number.</item>
149117
/// <item>Access time, under the name <c>atime</c>, as a <see cref="double"/> number.</item>
150118
/// <item>Change time, under the name <c>ctime</c>, as a <see cref="double"/> number.</item>
151119
/// <item>Path, under the name <c>path</c>, as a string.</item>
152-
/// </list>
153-
/// <para>The following entries are only found in the Extended Attributes dictionary of a PAX entry if certain conditions are met:</para>
154-
/// <list type="bullet">
155-
/// <item>Group name, under the name <c>gname</c>, as a string, if it is larger than 32 bytes.</item>
156-
/// <item>User name, under the name <c>uname</c>, as a string, if it is larger than 32 bytes.</item>
157-
/// <item>File length, under the name <c>size</c>, as an <see cref="int"/>, if the string representation of the number is larger than 12 bytes.</item>
120+
/// <item>Group name, under the name <c>gname</c>, as a string.</item>
121+
/// <item>User name, under the name <c>uname</c>, as a string.</item>
122+
/// <item>File length, under the name <c>size</c>, as an <see cref="int"/>.</item>
158123
/// </list>
159124
/// </remarks>
160125
public IReadOnlyDictionary<string, string> ExtendedAttributes => _readOnlyExtendedAttributes ??= _header.ExtendedAttributes.AsReadOnly();
161126

162127
// Determines if the current instance's entry type supports setting a data stream.
163128
internal override bool IsDataStreamSetterSupported() => EntryType == TarEntryType.RegularFile;
164-
165-
// Checks if the extended attributes dictionary contains 'atime' and 'ctime'.
166-
// If any of them is not found, it is added with the value of either the current entry's 'mtime',
167-
// or 'DateTimeOffset.UtcNow', depending on the value of 'useMTime'.
168-
private void AddNewAccessAndChangeTimestampsIfNotExist(bool useMTime)
169-
{
170-
Debug.Assert(!useMTime || (useMTime && _header._mTime != default));
171-
bool containsATime = _header.ExtendedAttributes.ContainsKey(TarHeader.PaxEaATime);
172-
bool containsCTime = _header.ExtendedAttributes.ContainsKey(TarHeader.PaxEaCTime);
173-
174-
if (!containsATime || !containsCTime)
175-
{
176-
string secondsFromEpochString = TarHelpers.GetTimestampStringFromDateTimeOffset(useMTime ? _header._mTime : DateTimeOffset.UtcNow);
177-
178-
if (!containsATime)
179-
{
180-
_header.ExtendedAttributes[TarHeader.PaxEaATime] = secondsFromEpochString;
181-
}
182-
183-
if (!containsCTime)
184-
{
185-
_header.ExtendedAttributes[TarHeader.PaxEaCTime] = secondsFromEpochString;
186-
}
187-
}
188-
}
189129
}
190130
}

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

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@ namespace System.Formats.Tar
1515
// Reads the header attributes from a tar archive entry.
1616
internal sealed partial class TarHeader
1717
{
18-
private readonly byte[] ArrayOf12NullBytes = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
19-
2018
// Attempts to retrieve the next header from the specified tar archive stream.
2119
// Throws if end of stream is reached or if any data type conversion fails.
2220
// Returns a valid TarHeader object if the attributes were read successfully, null otherwise.
@@ -106,7 +104,7 @@ internal void ReplaceNormalAttributesWithExtended(Dictionary<string, string>? di
106104
return;
107105
}
108106

109-
InitializeExtendedAttributesWithExisting(dictionaryFromExtendedAttributesHeader);
107+
AddExtendedAttributes(dictionaryFromExtendedAttributesHeader);
110108

111109
// Find all the extended attributes with known names and save them in the expected standard attribute.
112110

@@ -388,7 +386,7 @@ private async Task ProcessDataBlockAsync(Stream archiveStream, bool copyData, Ca
388386
TarHeader header = new(initialFormat,
389387
name: TarHelpers.GetTrimmedUtf8String(buffer.Slice(FieldLocations.Name, FieldLengths.Name)),
390388
mode: TarHelpers.ParseNumeric<int>(buffer.Slice(FieldLocations.Mode, FieldLengths.Mode)),
391-
mTime: TarHelpers.GetDateTimeOffsetFromSecondsSinceEpoch(TarHelpers.ParseNumeric<long>(buffer.Slice(FieldLocations.MTime, FieldLengths.MTime))),
389+
mTime: ParseAsTimestamp(buffer.Slice(FieldLocations.MTime, FieldLengths.MTime)),
392390
typeFlag: (TarEntryType)buffer[FieldLocations.TypeFlag])
393391
{
394392
_checksum = checksum,
@@ -538,21 +536,24 @@ private void ReadPosixAndGnuSharedAttributes(ReadOnlySpan<byte> buffer)
538536
// Throws if any conversion fails.
539537
private void ReadGnuAttributes(ReadOnlySpan<byte> buffer)
540538
{
541-
// Convert byte arrays
542-
ReadOnlySpan<byte> aTimeBuffer = buffer.Slice(FieldLocations.ATime, FieldLengths.ATime);
543-
if (!aTimeBuffer.SequenceEqual(ArrayOf12NullBytes)) // null values are ignored
544-
{
545-
long aTime = TarHelpers.ParseNumeric<long>(aTimeBuffer);
546-
_aTime = TarHelpers.GetDateTimeOffsetFromSecondsSinceEpoch(aTime);
547-
}
548-
ReadOnlySpan<byte> cTimeBuffer = buffer.Slice(FieldLocations.CTime, FieldLengths.CTime);
549-
if (!cTimeBuffer.SequenceEqual(ArrayOf12NullBytes)) // An all nulls buffer is interpreted as MinValue
539+
_aTime = ParseAsTimestamp(buffer.Slice(FieldLocations.ATime, FieldLengths.ATime));
540+
541+
_cTime = ParseAsTimestamp(buffer.Slice(FieldLocations.CTime, FieldLengths.CTime));
542+
543+
// TODO: Read the bytes of the currently unsupported GNU fields, in case user wants to write this entry into another GNU archive, they need to be preserved. https://github.com/dotnet/runtime/issues/68230
544+
}
545+
546+
private static DateTimeOffset ParseAsTimestamp(ReadOnlySpan<byte> buffer)
547+
{
548+
// When all bytes are zero, the timestamp is not initialized, and we map it to default.
549+
bool allZeros = !buffer.ContainsAnyExcept((byte)0);
550+
if (allZeros)
550551
{
551-
long cTime = TarHelpers.ParseNumeric<long>(cTimeBuffer);
552-
_cTime = TarHelpers.GetDateTimeOffsetFromSecondsSinceEpoch(cTime);
552+
return default(DateTimeOffset);
553553
}
554554

555-
// TODO: Read the bytes of the currently unsupported GNU fields, in case user wants to write this entry into another GNU archive, they need to be preserved. https://github.com/dotnet/runtime/issues/68230
555+
long time = TarHelpers.ParseNumeric<long>(buffer);
556+
return TarHelpers.GetDateTimeOffsetFromSecondsSinceEpoch(time);
556557
}
557558

558559
// Reads the ustar prefix attribute.

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

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -790,19 +790,8 @@ private int WriteGnuFields(Span<byte> buffer)
790790

791791
if (_typeFlag is not TarEntryType.LongLink and not TarEntryType.LongPath)
792792
{
793-
if (_aTime != default)
794-
{
795-
checksum += WriteAsTimestamp(_aTime, buffer.Slice(FieldLocations.ATime, FieldLengths.ATime));
796-
}
797-
if (_cTime != default)
798-
{
799-
checksum += WriteAsTimestamp(_cTime, buffer.Slice(FieldLocations.CTime, FieldLengths.CTime));
800-
}
801-
}
802-
803-
if (_gnuUnusedBytes != null)
804-
{
805-
checksum += WriteLeftAlignedBytesAndGetChecksum(_gnuUnusedBytes, buffer.Slice(FieldLocations.GnuUnused, FieldLengths.AllGnuUnused));
793+
checksum += WriteAsTimestamp(_aTime, buffer.Slice(FieldLocations.ATime, FieldLengths.ATime));
794+
checksum += WriteAsTimestamp(_cTime, buffer.Slice(FieldLocations.CTime, FieldLengths.CTime));
806795
}
807796

808797
return checksum;
@@ -1179,6 +1168,12 @@ private static int FormatOctal(long value, Span<byte> destination)
11791168
// Writes the specified DateTimeOffset's Unix time seconds, and returns its checksum.
11801169
private int WriteAsTimestamp(DateTimeOffset timestamp, Span<byte> destination)
11811170
{
1171+
// For 'default' we leave the buffer zero-ed to indicate: "no timestamp".
1172+
if (timestamp == default)
1173+
{
1174+
return 0;
1175+
}
1176+
11821177
long unixTimeSeconds = timestamp.ToUnixTimeSeconds();
11831178
return FormatNumeric(unixTimeSeconds, destination);
11841179
}

0 commit comments

Comments
 (0)