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 595cc7744f358..d834ab2f10462 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 @@ -273,7 +273,7 @@ private bool TryReadCommonAttributes(Span buffer) _mode = TarHelpers.GetTenBaseNumberFromOctalAsciiChars(buffer.Slice(FieldLocations.Mode, FieldLengths.Mode)); _uid = TarHelpers.GetTenBaseNumberFromOctalAsciiChars(buffer.Slice(FieldLocations.Uid, FieldLengths.Uid)); _gid = TarHelpers.GetTenBaseNumberFromOctalAsciiChars(buffer.Slice(FieldLocations.Gid, FieldLengths.Gid)); - int mTime = TarHelpers.GetTenBaseNumberFromOctalAsciiChars(buffer.Slice(FieldLocations.MTime, FieldLengths.MTime)); + long mTime = TarHelpers.GetTenBaseLongFromOctalAsciiChars(buffer.Slice(FieldLocations.MTime, FieldLengths.MTime)); _mTime = TarHelpers.GetDateTimeOffsetFromSecondsSinceEpoch(mTime); _typeFlag = (TarEntryType)buffer[FieldLocations.TypeFlag]; _linkName = TarHelpers.GetTrimmedUtf8String(buffer.Slice(FieldLocations.LinkName, FieldLengths.LinkName)); @@ -383,10 +383,10 @@ private void ReadPosixAndGnuSharedAttributes(Span buffer) private void ReadGnuAttributes(Span buffer) { // Convert byte arrays - int aTime = TarHelpers.GetTenBaseNumberFromOctalAsciiChars(buffer.Slice(FieldLocations.ATime, FieldLengths.ATime)); + long aTime = TarHelpers.GetTenBaseLongFromOctalAsciiChars(buffer.Slice(FieldLocations.ATime, FieldLengths.ATime)); _aTime = TarHelpers.GetDateTimeOffsetFromSecondsSinceEpoch(aTime); - int cTime = TarHelpers.GetTenBaseNumberFromOctalAsciiChars(buffer.Slice(FieldLocations.CTime, FieldLengths.CTime)); + long cTime = TarHelpers.GetTenBaseLongFromOctalAsciiChars(buffer.Slice(FieldLocations.CTime, FieldLengths.CTime)); _cTime = TarHelpers.GetDateTimeOffsetFromSecondsSinceEpoch(cTime); // 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 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 db2db1157a03e..b6ac2470a1e44 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 @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Diagnostics; -using System.Globalization; using System.IO; using System.Text; @@ -390,20 +389,6 @@ private void CollectExtendedAttributesFromStandardFieldsIfNeeded() _extendedAttributes ??= new Dictionary(); _extendedAttributes.Add(PaxEaName, _name); - bool containsATime = _extendedAttributes.ContainsKey(PaxEaATime); - bool containsCTime = _extendedAttributes.ContainsKey(PaxEaATime); - if (!containsATime || !containsCTime) - { - DateTimeOffset now = DateTimeOffset.UtcNow; - if (!containsATime) - { - AddTimestampAsUnixSeconds(_extendedAttributes, PaxEaATime, now); - } - if (!containsCTime) - { - AddTimestampAsUnixSeconds(_extendedAttributes, PaxEaCTime, now); - } - } if (!_extendedAttributes.ContainsKey(PaxEaMTime)) { AddTimestampAsUnixSeconds(_extendedAttributes, PaxEaMTime, _mTime); diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHelpers.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHelpers.cs index 924106cf3bcbf..62bc6250f85d5 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHelpers.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHelpers.cs @@ -119,20 +119,20 @@ internal static DateTimeOffset GetDateTimeOffsetFromSecondsSinceEpoch(long secon new DateTimeOffset((secondsSinceUnixEpoch * TimeSpan.TicksPerSecond) + DateTime.UnixEpoch.Ticks, TimeSpan.Zero); // Converts the specified number of seconds that have passed since the Unix Epoch to a DateTimeOffset. - internal static DateTimeOffset GetDateTimeOffsetFromSecondsSinceEpoch(double secondsSinceUnixEpoch) => + private static DateTimeOffset GetDateTimeOffsetFromSecondsSinceEpoch(decimal secondsSinceUnixEpoch) => new DateTimeOffset((long)(secondsSinceUnixEpoch * TimeSpan.TicksPerSecond) + DateTime.UnixEpoch.Ticks, TimeSpan.Zero); // Converts the specified DateTimeOffset to the number of seconds that have passed since the Unix Epoch. - internal static double GetSecondsSinceEpochFromDateTimeOffset(DateTimeOffset dateTimeOffset) => - ((double)(dateTimeOffset.UtcDateTime - DateTime.UnixEpoch).Ticks) / TimeSpan.TicksPerSecond; + private static decimal GetSecondsSinceEpochFromDateTimeOffset(DateTimeOffset dateTimeOffset) => + ((decimal)(dateTimeOffset.UtcDateTime - DateTime.UnixEpoch).Ticks) / TimeSpan.TicksPerSecond; - // If the specified fieldName is found in the provided dictionary and it is a valid double number, returns true and sets the value in 'dateTimeOffset'. + // If the specified fieldName is found in the provided dictionary and it is a valid decimal number, returns true and sets the value in 'dateTimeOffset'. internal static bool TryGetDateTimeOffsetFromTimestampString(Dictionary? dict, string fieldName, out DateTimeOffset dateTimeOffset) { dateTimeOffset = default; if (dict != null && dict.TryGetValue(fieldName, out string? value) && - double.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out double secondsSinceEpoch)) + decimal.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out decimal secondsSinceEpoch)) { dateTimeOffset = GetDateTimeOffsetFromSecondsSinceEpoch(secondsSinceEpoch); return true; @@ -143,8 +143,10 @@ internal static bool TryGetDateTimeOffsetFromTimestampString(Dictionary buffer) return string.IsNullOrEmpty(str) ? 0 : Convert.ToInt32(str, fromBase: 8); } + // Receives a byte array that represents an ASCII string containing a number in octal base. + // Converts the array to an octal base number, then transforms it to ten base and returns it. + internal static long GetTenBaseLongFromOctalAsciiChars(Span buffer) + { + string str = GetTrimmedAsciiString(buffer); + return string.IsNullOrEmpty(str) ? 0 : Convert.ToInt64(str, fromBase: 8); + } + // Returns the string contained in the specified buffer of bytes, // in the specified encoding, removing the trailing null or space chars. private static string GetTrimmedString(ReadOnlySpan buffer, Encoding encoding) 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 225dac08e6496..11fdf8d362911 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 @@ -58,9 +58,10 @@ partial void ReadFileFromDiskAndWriteToArchiveStreamAsEntry(string fullPath, str entry._header._devMinor = (int)minor; } - entry._header._mTime = TarHelpers.GetDateTimeOffsetFromSecondsSinceEpoch(status.MTime); - entry._header._aTime = TarHelpers.GetDateTimeOffsetFromSecondsSinceEpoch(status.ATime); - entry._header._cTime = TarHelpers.GetDateTimeOffsetFromSecondsSinceEpoch(status.CTime); + entry._header._mTime = info.LastWriteTimeUtc; + entry._header._aTime = info.LastAccessTimeUtc; + // FileSystemInfo does not have ChangeTime, but LastWriteTime and LastAccessTime make sure to add nanoseconds, so we should do the same here + entry._header._cTime = DateTimeOffset.FromUnixTimeSeconds(status.CTime).AddTicks(status.CTimeNsec / 100 /* nanoseconds per tick */); entry._header._mode = (status.Mode & 4095); // First 12 bits 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 bee4ac9b7c319..c3f5a6ca7a136 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 @@ -46,9 +46,9 @@ partial void ReadFileFromDiskAndWriteToArchiveStreamAsEntry(string fullPath, str FileSystemInfo info = attributes.HasFlag(FileAttributes.Directory) ? new DirectoryInfo(fullPath) : new FileInfo(fullPath); - entry._header._mTime = new DateTimeOffset(info.LastWriteTimeUtc); - entry._header._aTime = new DateTimeOffset(info.LastAccessTimeUtc); - entry._header._cTime = new DateTimeOffset(info.LastWriteTimeUtc); // There is no "change time" property + entry._header._mTime = info.LastWriteTimeUtc; + entry._header._aTime = info.LastAccessTimeUtc; + entry._header._cTime = info.LastWriteTimeUtc; // There is no "change time" property entry.Mode = DefaultWindowsMode; 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 608701114e616..8125e45708eb3 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 @@ -61,6 +61,89 @@ public class PaxTarEntry_Conversion_Tests : TarTestsConversionBase [Fact] public void Constructor_ConversionFromGnu_CharacterDevice() => TestConstructionConversion(TarEntryType.CharacterDevice, TarEntryFormat.Gnu, TarEntryFormat.Pax); + [Theory] + [InlineData(TarEntryFormat.V7)] + [InlineData(TarEntryFormat.Ustar)] + [InlineData(TarEntryFormat.Pax)] + [InlineData(TarEntryFormat.Gnu)] + public void Constructor_ConversionFromV7_Write(TarEntryFormat originalEntryFormat) + { + string name = "file.txt"; + string contents = "Hello world"; + + TarEntry originalEntry = InvokeTarEntryCreationConstructor(originalEntryFormat, GetTarEntryTypeForTarEntryFormat(TarEntryType.RegularFile, originalEntryFormat), name); + + using MemoryStream dataStream = new MemoryStream(); + using (StreamWriter streamWriter = new StreamWriter(dataStream, leaveOpen: true)) + { + streamWriter.WriteLine(contents); + } + dataStream.Position = 0; + originalEntry.DataStream = dataStream; + + DateTimeOffset expectedATime; + DateTimeOffset expectedCTime; + + if (originalEntryFormat is TarEntryFormat.Pax or TarEntryFormat.Gnu) + { + // The constructor should've set the atime and ctime automatically to the same value of mtime + expectedATime = originalEntry.ModificationTime; + expectedCTime = originalEntry.ModificationTime; + } + else + { + // ustar and v7 do not have atime and ctime, so the expected values of atime and ctime should be + // larger than mtime, because the conversion constructor sets those values automatically + DateTimeOffset now = DateTimeOffset.UtcNow; + expectedATime = now; + expectedCTime = now; + } + + TarEntry convertedEntry = InvokeTarEntryConversionConstructor(TarEntryFormat.Pax, originalEntry); + + using MemoryStream archiveStream = new MemoryStream(); + using (TarWriter writer = new TarWriter(archiveStream, leaveOpen: true)) + { + writer.WriteEntry(convertedEntry); + } + + archiveStream.Position = 0; + using (TarReader reader = new TarReader(archiveStream)) + { + PaxTarEntry paxEntry = reader.GetNextEntry() as PaxTarEntry; + Assert.NotNull(paxEntry); + Assert.Equal(TarEntryFormat.Pax, paxEntry.Format); + Assert.Equal(TarEntryType.RegularFile, paxEntry.EntryType); + Assert.Equal(name, paxEntry.Name); + + Assert.NotNull(paxEntry.DataStream); + + using (StreamReader streamReader = new StreamReader(paxEntry.DataStream, leaveOpen: true)) + { + Assert.Equal(contents, streamReader.ReadLine()); + } + + // atime and ctime should've been added automatically in the conversion constructor + // and should not be equal to the value of mtime, which was set on the original entry constructor + + Assert.Contains(PaxEaATime, paxEntry.ExtendedAttributes); + Assert.Contains(PaxEaCTime, paxEntry.ExtendedAttributes); + DateTimeOffset atime = GetDateTimeOffsetFromTimestampString(paxEntry.ExtendedAttributes[PaxEaATime]); + DateTimeOffset ctime = GetDateTimeOffsetFromTimestampString(paxEntry.ExtendedAttributes[PaxEaCTime]); + + if (originalEntryFormat is TarEntryFormat.Pax or TarEntryFormat.Gnu) + { + Assert.Equal(expectedATime, atime); + Assert.Equal(expectedCTime, ctime); + } + else + { + AssertExtensions.GreaterThanOrEqualTo(atime, expectedATime); + AssertExtensions.GreaterThanOrEqualTo(ctime, expectedCTime); + } + } + } + [Theory] [InlineData(TarEntryFormat.V7)] [InlineData(TarEntryFormat.Ustar)] @@ -72,7 +155,7 @@ public void Constructor_ConversionFromV7_From_UnseekableTarReader(TarEntryFormat using WrappedStream wrappedSource = new WrappedStream(source, canRead: true, canWrite: false, canSeek: false); using TarReader sourceReader = new TarReader(wrappedSource, leaveOpen: true); - V7TarEntry v7Entry = sourceReader.GetNextEntry(copyData: false) as V7TarEntry; + V7TarEntry v7Entry = sourceReader.GetNextEntry(copyData: false) as V7TarEntry; // Preserve the connection to the unseekable stream PaxTarEntry paxEntry = new PaxTarEntry(other: v7Entry); // Convert, and avoid advancing wrappedSource position using MemoryStream destination = new MemoryStream(); diff --git a/src/libraries/System.Formats.Tar/tests/TarEntry/TarEntry.Conversion.Tests.Base.cs b/src/libraries/System.Formats.Tar/tests/TarEntry/TarEntry.Conversion.Tests.Base.cs index 2a4a4fe28ca8d..846a2f7a1904e 100644 --- a/src/libraries/System.Formats.Tar/tests/TarEntry/TarEntry.Conversion.Tests.Base.cs +++ b/src/libraries/System.Formats.Tar/tests/TarEntry/TarEntry.Conversion.Tests.Base.cs @@ -76,14 +76,14 @@ private TarEntry GetFirstEntry(MemoryStream dataStream, TarEntryType entryType, PaxTarEntry paxEntry = firstEntry as PaxTarEntry; Assert.Contains("atime", paxEntry.ExtendedAttributes); Assert.Contains("ctime", paxEntry.ExtendedAttributes); - CompareDateTimeOffsets(firstEntry.ModificationTime, GetDateTimeOffsetFromTimestampString(paxEntry.ExtendedAttributes, "atime")); - CompareDateTimeOffsets(firstEntry.ModificationTime, GetDateTimeOffsetFromTimestampString(paxEntry.ExtendedAttributes, "ctime")); + Assert.Equal(firstEntry.ModificationTime, GetDateTimeOffsetFromTimestampString(paxEntry.ExtendedAttributes, "atime")); + Assert.Equal(firstEntry.ModificationTime, GetDateTimeOffsetFromTimestampString(paxEntry.ExtendedAttributes, "ctime")); } else if (format is TarEntryFormat.Gnu) { GnuTarEntry gnuEntry = firstEntry as GnuTarEntry; - CompareDateTimeOffsets(firstEntry.ModificationTime, gnuEntry.AccessTime); - CompareDateTimeOffsets(firstEntry.ModificationTime, gnuEntry.ChangeTime); + Assert.Equal(firstEntry.ModificationTime, gnuEntry.AccessTime); + Assert.Equal(firstEntry.ModificationTime, gnuEntry.ChangeTime); } return firstEntry; @@ -123,18 +123,18 @@ private TarEntry ConvertAndVerifyEntry(TarEntry originalEntry, TarEntryType entr if (formatToConvert is TarEntryFormat.Pax) { PaxTarEntry paxEntry = convertedEntry as PaxTarEntry; - DateTimeOffset actualAccessTime = GetDateTimeOffsetFromTimestampString(paxEntry.ExtendedAttributes, "atime"); - DateTimeOffset actualChangeTime = GetDateTimeOffsetFromTimestampString(paxEntry.ExtendedAttributes, "atime"); + DateTimeOffset actualAccessTime = GetDateTimeOffsetFromTimestampString(paxEntry.ExtendedAttributes, PaxEaATime); + DateTimeOffset actualChangeTime = GetDateTimeOffsetFromTimestampString(paxEntry.ExtendedAttributes, PaxEaCTime); if (originalEntry.Format is TarEntryFormat.Pax or TarEntryFormat.Gnu) { GetExpectedTimestampsFromOriginalPaxOrGnu(originalEntry, out DateTimeOffset expectedATime, out DateTimeOffset expectedCTime); - CompareDateTimeOffsets(expectedATime, actualAccessTime); - CompareDateTimeOffsets(expectedCTime, actualChangeTime); + Assert.Equal(expectedATime, actualAccessTime); + Assert.Equal(expectedCTime, actualChangeTime); } else if (originalEntry.Format is TarEntryFormat.Ustar or TarEntryFormat.V7) { - CompareDateTimeOffsets(initialNow, actualAccessTime); - CompareDateTimeOffsets(initialNow, actualChangeTime); + AssertExtensions.GreaterThanOrEqualTo(actualAccessTime, initialNow); + AssertExtensions.GreaterThanOrEqualTo(actualChangeTime, initialNow); } } @@ -144,13 +144,13 @@ private TarEntry ConvertAndVerifyEntry(TarEntry originalEntry, TarEntryType entr if (originalEntry.Format is TarEntryFormat.Pax or TarEntryFormat.Gnu) { GetExpectedTimestampsFromOriginalPaxOrGnu(originalEntry, out DateTimeOffset expectedATime, out DateTimeOffset expectedCTime); - CompareDateTimeOffsets(expectedATime, gnuEntry.AccessTime); - CompareDateTimeOffsets(expectedCTime, gnuEntry.ChangeTime); + AssertExtensions.GreaterThanOrEqualTo(gnuEntry.AccessTime, expectedATime); + AssertExtensions.GreaterThanOrEqualTo(gnuEntry.ChangeTime, expectedCTime); } else if (originalEntry.Format is TarEntryFormat.Ustar or TarEntryFormat.V7) { - CompareDateTimeOffsets(initialNow, gnuEntry.AccessTime); - CompareDateTimeOffsets(initialNow, gnuEntry.ChangeTime); + AssertExtensions.GreaterThanOrEqualTo(gnuEntry.AccessTime, initialNow); + AssertExtensions.GreaterThanOrEqualTo(gnuEntry.ChangeTime, initialNow); } } diff --git a/src/libraries/System.Formats.Tar/tests/TarTestsBase.Pax.cs b/src/libraries/System.Formats.Tar/tests/TarTestsBase.Pax.cs index bd0068f460258..428e3a338cfaf 100644 --- a/src/libraries/System.Formats.Tar/tests/TarTestsBase.Pax.cs +++ b/src/libraries/System.Formats.Tar/tests/TarTestsBase.Pax.cs @@ -85,28 +85,28 @@ protected void VerifyFifo(PaxTarEntry fifo) VerifyPosixFifo(fifo); } - private DateTimeOffset GetDateTimeOffsetFromSecondsSinceEpoch(double secondsSinceUnixEpoch) => + private DateTimeOffset GetDateTimeOffsetFromSecondsSinceEpoch(decimal secondsSinceUnixEpoch) => new DateTimeOffset((long)(secondsSinceUnixEpoch * TimeSpan.TicksPerSecond) + DateTime.UnixEpoch.Ticks, TimeSpan.Zero); - private double GetSecondsSinceEpochFromDateTimeOffset(DateTimeOffset value) => - ((double)(value.UtcDateTime - DateTime.UnixEpoch).Ticks) / TimeSpan.TicksPerSecond; + private decimal GetSecondsSinceEpochFromDateTimeOffset(DateTimeOffset value) => + ((decimal)(value.UtcDateTime - DateTime.UnixEpoch).Ticks) / TimeSpan.TicksPerSecond; protected DateTimeOffset GetDateTimeOffsetFromTimestampString(IReadOnlyDictionary ea, string fieldName) { - Assert.True(ea.TryGetValue(fieldName, out string value), $"Extended attributes did not contain field '{fieldName}'"); - - // As regular header fields, timestamps are saved as integer numbers that fit in 12 bytes - // But as extended attributes, they should always be saved as doubles with decimal precision - Assert.Contains(".", value); + Assert.Contains(fieldName, ea); + return GetDateTimeOffsetFromTimestampString(ea[fieldName]); + } - Assert.True(double.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out double secondsSinceEpoch), $"Extended attributes field '{fieldName}' is not a valid double."); + protected DateTimeOffset GetDateTimeOffsetFromTimestampString(string strNumber) + { + Assert.True(decimal.TryParse(strNumber, NumberStyles.Any, CultureInfo.InvariantCulture, out decimal secondsSinceEpoch)); return GetDateTimeOffsetFromSecondsSinceEpoch(secondsSinceEpoch); } protected string GetTimestampStringFromDateTimeOffset(DateTimeOffset timestamp) { - double secondsSinceEpoch = GetSecondsSinceEpochFromDateTimeOffset(timestamp); - return secondsSinceEpoch.ToString("F9", CultureInfo.InvariantCulture); + decimal secondsSinceEpoch = GetSecondsSinceEpochFromDateTimeOffset(timestamp); + return secondsSinceEpoch.ToString("G", CultureInfo.InvariantCulture); } protected void VerifyExtendedAttributeTimestamp(PaxTarEntry paxEntry, string fieldName, DateTimeOffset minimumTime) diff --git a/src/libraries/System.Formats.Tar/tests/TarTestsBase.cs b/src/libraries/System.Formats.Tar/tests/TarTestsBase.cs index 88d1433cd0489..27998a5e634ab 100644 --- a/src/libraries/System.Formats.Tar/tests/TarTestsBase.cs +++ b/src/libraries/System.Formats.Tar/tests/TarTestsBase.cs @@ -300,18 +300,6 @@ protected void VerifyDataStream(TarEntry entry, bool isFromWriter) } } - // Compares date, hour, minutes, seconds and offset from two DateTimeOffset instances. - // Milliseconds and smaller units are ignored, since this comparer is used for when converting - // to and from double (Unix Epoch) and some precision is lost. - protected void CompareDateTimeOffsets(DateTimeOffset expected, DateTimeOffset actual) - { - Assert.Equal(expected.Date, actual.Date); - Assert.Equal(expected.Hour, actual.Hour); - Assert.Equal(expected.Minute, actual.Minute); - Assert.Equal(expected.Second, actual.Second); - Assert.Equal(expected.Offset, actual.Offset); - } - protected Type GetTypeForFormat(TarEntryFormat expectedFormat) { return expectedFormat switch diff --git a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.Entry.Pax.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.Entry.Pax.Tests.cs index 04dbf264f8853..4a1b09ee133f6 100644 --- a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.Entry.Pax.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.Entry.Pax.Tests.cs @@ -395,5 +395,116 @@ public void Add_Empty_GlobalExtendedAttributes() Assert.Null(reader.GetNextEntry()); } } + + [Fact] + // Y2K38 will happen one second after "2038/19/01 03:14:07 +00:00". This timestamp represents the seconds since the Unix epoch with a + // value of int.MaxValue: 2,147,483,647. + // The fixed size fields for mtime, atime and ctime can fit 12 ASCII characters, but the last character is reserved for an ASCII space. + // All our entry types should survive the Epochalypse because we internally use long to represent the seconds since Unix epoch, not int. + // So if the max allowed value is 77,777,777,777 in octal, then the max allowed seconds since the Unix epoch are 8,589,934,591, which + // is way past int MaxValue, but still within the long limits. That number represents the date "2242/16/03 12:56:32 +00:00". + public void WriteTimestampsBeyondEpochalypseInPax() + { + DateTimeOffset epochalypse = new DateTimeOffset(2038, 1, 19, 3, 14, 8, TimeSpan.Zero); + string strEpochalypse = GetTimestampStringFromDateTimeOffset(epochalypse); + + Dictionary ea = new Dictionary() + { + { PaxEaATime, strEpochalypse }, + { PaxEaCTime, strEpochalypse } + }; + + PaxTarEntry entry = new PaxTarEntry(TarEntryType.Directory, "dir", ea); + + entry.ModificationTime = epochalypse; + Assert.Equal(epochalypse, entry.ModificationTime); + + Assert.Contains(PaxEaATime, entry.ExtendedAttributes); + DateTimeOffset atime = GetDateTimeOffsetFromTimestampString(entry.ExtendedAttributes, PaxEaATime); + Assert.Equal(epochalypse, atime); + + Assert.Contains(PaxEaCTime, entry.ExtendedAttributes); + DateTimeOffset ctime = GetDateTimeOffsetFromTimestampString(entry.ExtendedAttributes, PaxEaCTime); + Assert.Equal(epochalypse, ctime); + + using MemoryStream archiveStream = new MemoryStream(); + using (TarWriter writer = new TarWriter(archiveStream, leaveOpen: true)) + { + writer.WriteEntry(entry); + } + + archiveStream.Position = 0; + using (TarReader reader = new TarReader(archiveStream)) + { + PaxTarEntry readEntry = reader.GetNextEntry() as PaxTarEntry; + Assert.NotNull(readEntry); + + Assert.Equal(epochalypse, readEntry.ModificationTime); + + Assert.Contains(PaxEaATime, readEntry.ExtendedAttributes); + DateTimeOffset actualATime = GetDateTimeOffsetFromTimestampString(readEntry.ExtendedAttributes, PaxEaATime); + Assert.Equal(epochalypse, actualATime); + + Assert.Contains(PaxEaCTime, readEntry.ExtendedAttributes); + DateTimeOffset actualCTime = GetDateTimeOffsetFromTimestampString(readEntry.ExtendedAttributes, PaxEaCTime); + Assert.Equal(epochalypse, actualCTime); + } + } + + [Fact] + // The fixed size fields for mtime, atime and ctime can fit 12 ASCII characters, but the last character is reserved for an ASCII space. + // We internally use long to represent the seconds since Unix epoch, not int. + // If the max allowed value is 77,777,777,777 in octal, then the max allowed seconds since the Unix epoch are 8,589,934,591, + // which represents the date "2242/03/16 12:56:32 +00:00". + // Pax should survive after this date because it stores the timestamps in the extended attributes dictionary + // without size restrictions. + public void WriteTimestampsBeyondOctalLimitInPax() + { + DateTimeOffset overLimitTimestamp = new DateTimeOffset(2242, 3, 16, 12, 56, 33, TimeSpan.Zero); // One second past the octal limit + + string strOverLimitTimestamp = GetTimestampStringFromDateTimeOffset(overLimitTimestamp); + + Dictionary ea = new Dictionary() + { + { PaxEaATime, strOverLimitTimestamp }, + { PaxEaCTime, strOverLimitTimestamp } + }; + + PaxTarEntry entry = new PaxTarEntry(TarEntryType.Directory, "dir", ea); + + entry.ModificationTime = overLimitTimestamp; + Assert.Equal(overLimitTimestamp, entry.ModificationTime); + + Assert.Contains(PaxEaATime, entry.ExtendedAttributes); + DateTimeOffset atime = GetDateTimeOffsetFromTimestampString(entry.ExtendedAttributes, PaxEaATime); + Assert.Equal(overLimitTimestamp, atime); + + Assert.Contains(PaxEaCTime, entry.ExtendedAttributes); + DateTimeOffset ctime = GetDateTimeOffsetFromTimestampString(entry.ExtendedAttributes, PaxEaCTime); + Assert.Equal(overLimitTimestamp, ctime); + + using MemoryStream archiveStream = new MemoryStream(); + using (TarWriter writer = new TarWriter(archiveStream, leaveOpen: true)) + { + writer.WriteEntry(entry); + } + + archiveStream.Position = 0; + using (TarReader reader = new TarReader(archiveStream)) + { + PaxTarEntry readEntry = reader.GetNextEntry() as PaxTarEntry; + Assert.NotNull(readEntry); + + Assert.Equal(overLimitTimestamp, readEntry.ModificationTime); + + Assert.Contains(PaxEaATime, readEntry.ExtendedAttributes); + DateTimeOffset actualATime = GetDateTimeOffsetFromTimestampString(readEntry.ExtendedAttributes, PaxEaATime); + Assert.Equal(overLimitTimestamp, actualATime); + + Assert.Contains(PaxEaCTime, readEntry.ExtendedAttributes); + DateTimeOffset actualCTime = GetDateTimeOffsetFromTimestampString(readEntry.ExtendedAttributes, PaxEaCTime); + Assert.Equal(overLimitTimestamp, actualCTime); + } + } } } diff --git a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.File.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.File.Tests.cs index d9a94c6e7b670..9536b8f64fcce 100644 --- a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.File.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.File.Tests.cs @@ -65,7 +65,6 @@ public void EntryName_NullOrEmpty() } } - [ActiveIssue("https://github.com/dotnet/runtime/issues/69474", TestPlatforms.Linux)] [Theory] [InlineData(TarEntryFormat.V7)] [InlineData(TarEntryFormat.Ustar)] diff --git a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.Tests.cs index 2fad0f26f0449..6e554efb98e46 100644 --- a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.Tests.cs @@ -128,6 +128,106 @@ public void ReadAndWriteMultipleGlobalExtendedAttributesEntries(TarEntryFormat f } } + // Y2K38 will happen one second after "2038/19/01 03:14:07 +00:00". This timestamp represents the seconds since the Unix epoch with a + // value of int.MaxValue: 2,147,483,647. + // The fixed size fields for mtime, atime and ctime can fit 12 ASCII characters, but the last character is reserved for an ASCII space. + // All our entry types should survive the Epochalypse because we internally use long to represent the seconds since Unix epoch, not int. + // So if the max allowed value is 77,777,777,777 in octal, then the max allowed seconds since the Unix epoch are 8,589,934,591, which + // is way past int MaxValue, but still within the long limits. That number represents the date "2242/16/03 12:56:32 +00:00". + [Theory] + [InlineData(TarEntryFormat.V7)] + [InlineData(TarEntryFormat.Ustar)] + [InlineData(TarEntryFormat.Gnu)] + public void WriteTimestampsBeyondEpochalypse(TarEntryFormat format) + { + DateTimeOffset epochalypse = new DateTimeOffset(2038, 1, 19, 3, 14, 8, TimeSpan.Zero); // One second past Y2K38 + TarEntry entry = InvokeTarEntryCreationConstructor(format, TarEntryType.Directory, "dir"); + + entry.ModificationTime = epochalypse; + Assert.Equal(epochalypse, entry.ModificationTime); + + if (entry is GnuTarEntry gnuEntry) + { + gnuEntry.AccessTime = epochalypse; + Assert.Equal(epochalypse, gnuEntry.AccessTime); + + gnuEntry.ChangeTime = epochalypse; + Assert.Equal(epochalypse, gnuEntry.ChangeTime); + } + + using MemoryStream archiveStream = new MemoryStream(); + using (TarWriter writer = new TarWriter(archiveStream, leaveOpen: true)) + { + writer.WriteEntry(entry); + } + + archiveStream.Position = 0; + using (TarReader reader = new TarReader(archiveStream)) + { + TarEntry readEntry = reader.GetNextEntry(); + Assert.NotNull(readEntry); + + Assert.Equal(epochalypse, readEntry.ModificationTime); + + if (readEntry is GnuTarEntry gnuReadEntry) + { + Assert.Equal(epochalypse, gnuReadEntry.AccessTime); + Assert.Equal(epochalypse, gnuReadEntry.ChangeTime); + } + } + } + + // The fixed size fields for mtime, atime and ctime can fit 12 ASCII characters, but the last character is reserved for an ASCII space. + // We internally use long to represent the seconds since Unix epoch, not int. + // If the max allowed value is 77,777,777,777 in octal, then the max allowed seconds since the Unix epoch are 8,589,934,591, + // which represents the date "2242/03/16 12:56:32 +00:00". + // V7, Ustar and GNU would not survive after this date because they only have the fixed size fields to store timestamps. + [Theory] + [InlineData(TarEntryFormat.V7)] + [InlineData(TarEntryFormat.Ustar)] + [InlineData(TarEntryFormat.Gnu)] + public void WriteTimestampsBeyondOctalLimit(TarEntryFormat format) + { + DateTimeOffset overLimitTimestamp = new DateTimeOffset(2242, 3, 16, 12, 56, 33, TimeSpan.Zero); // One second past the octal limit + + TarEntry entry = InvokeTarEntryCreationConstructor(format, TarEntryType.Directory, "dir"); + + // Before writing the entry, the timestamps should have no issue + entry.ModificationTime = overLimitTimestamp; + Assert.Equal(overLimitTimestamp, entry.ModificationTime); + + if (entry is GnuTarEntry gnuEntry) + { + gnuEntry.AccessTime = overLimitTimestamp; + Assert.Equal(overLimitTimestamp, gnuEntry.AccessTime); + + gnuEntry.ChangeTime = overLimitTimestamp; + Assert.Equal(overLimitTimestamp, gnuEntry.ChangeTime); + } + + using MemoryStream archiveStream = new MemoryStream(); + using (TarWriter writer = new TarWriter(archiveStream, leaveOpen: true)) + { + writer.WriteEntry(entry); + } + + archiveStream.Position = 0; + using (TarReader reader = new TarReader(archiveStream)) + { + TarEntry readEntry = reader.GetNextEntry(); + Assert.NotNull(readEntry); + + // The timestamps get stored as '{1970-01-01 12:00:00 AM +00:00}' due to the +1 overflow + Assert.NotEqual(overLimitTimestamp, readEntry.ModificationTime); + + if (readEntry is GnuTarEntry gnuReadEntry) + { + Assert.NotEqual(overLimitTimestamp, gnuReadEntry.AccessTime); + Assert.NotEqual(overLimitTimestamp, gnuReadEntry.ChangeTime); + } + } + } + private TarEntry ConstructEntry(TarEntryFormat format, string name) => format switch {