diff --git a/src/libraries/System.Formats.Tar/System.Formats.Tar.sln b/src/libraries/System.Formats.Tar/System.Formats.Tar.sln index 74f1435574175f..a2fb3b5cbf01e0 100644 --- a/src/libraries/System.Formats.Tar/System.Formats.Tar.sln +++ b/src/libraries/System.Formats.Tar/System.Formats.Tar.sln @@ -1,4 +1,8 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36105.17 +MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StreamConformanceTests", "..\Common\tests\StreamConformanceTests\StreamConformanceTests.csproj", "{BE259E6E-B4F5-47DC-93D5-204297098A8C}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestUtilities", "..\Common\tests\TestUtilities\TestUtilities.csproj", "{45972587-B4BF-4F09-94DC-20E2D460FAA8}" @@ -39,11 +43,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{55A8C7E4-925 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "gen", "gen", "{0345BAA8-92BC-4499-B550-21AC44910FD2}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "gen", "tools\gen", "{07E13495-DC86-43BF-9E64-2CEA381D892D}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "gen", "gen", "{07E13495-DC86-43BF-9E64-2CEA381D892D}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "tools\src", "{3CB7A441-325E-41C9-B0D3-30D29CC21E82}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{3CB7A441-325E-41C9-B0D3-30D29CC21E82}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ref", "tools\ref", "{F845BA28-9AFC-4B52-8ED1-A4302AEB5C11}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ref", "ref", "{F845BA28-9AFC-4B52-8ED1-A4302AEB5C11}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tools", "tools", "{4A77449F-D456-4E19-B31C-7E0E3702680B}" EndProject @@ -124,25 +128,29 @@ Global GlobalSection(NestedProjects) = preSolution {BE259E6E-B4F5-47DC-93D5-204297098A8C} = {6CF0D830-3EE9-44B1-B548-EA8750AD7B3E} {45972587-B4BF-4F09-94DC-20E2D460FAA8} = {6CF0D830-3EE9-44B1-B548-EA8750AD7B3E} - {D2788A26-CDAE-4388-AE4B-A36B0E6DFF9D} = {6CF0D830-3EE9-44B1-B548-EA8750AD7B3E} - {6FD1E284-7B50-4077-B73A-5B31CB0E3577} = {6CF0D830-3EE9-44B1-B548-EA8750AD7B3E} {E0B882C6-2082-45F2-806E-568461A61975} = {9BE8AFF4-D37B-49AF-AFD3-A15E514AC8AE} - {A00011A0-E609-4A49-B893-EBFC72C98707} = {9BE8AFF4-D37B-49AF-AFD3-A15E514AC8AE} {9F751C2B-56DD-4604-A3F3-568627F8C006} = {55A8C7E4-925C-4F21-B68B-CEFC19137A4B} + {D2788A26-CDAE-4388-AE4B-A36B0E6DFF9D} = {6CF0D830-3EE9-44B1-B548-EA8750AD7B3E} + {6FD1E284-7B50-4077-B73A-5B31CB0E3577} = {6CF0D830-3EE9-44B1-B548-EA8750AD7B3E} {00477EA4-C3E5-48A9-8CA8-8CCF689E0DB4} = {0345BAA8-92BC-4499-B550-21AC44910FD2} {CD3A1327-8C67-4370-AC34-033065330F3F} = {0345BAA8-92BC-4499-B550-21AC44910FD2} {E89FEF3E-E0B9-41C4-A51C-9759AD1A3B69} = {0345BAA8-92BC-4499-B550-21AC44910FD2} {50E6D5FD-0E06-4D07-966E-C28E5448A1D3} = {0345BAA8-92BC-4499-B550-21AC44910FD2} + {A00011A0-E609-4A49-B893-EBFC72C98707} = {9BE8AFF4-D37B-49AF-AFD3-A15E514AC8AE} {AFEE875F-22C7-46AE-B28F-AF5C05CA0BA5} = {07E13495-DC86-43BF-9E64-2CEA381D892D} {AABA2E7B-6B45-4DD7-9C33-5F0FCDA1193F} = {07E13495-DC86-43BF-9E64-2CEA381D892D} - {07E13495-DC86-43BF-9E64-2CEA381D892D} = {4A77449F-D456-4E19-B31C-7E0E3702680B} {1720175E-27DA-4004-ABF2-DD47A338A3DB} = {3CB7A441-325E-41C9-B0D3-30D29CC21E82} {EB826FA8-035F-4DEF-8767-CBF94446916B} = {3CB7A441-325E-41C9-B0D3-30D29CC21E82} - {3CB7A441-325E-41C9-B0D3-30D29CC21E82} = {4A77449F-D456-4E19-B31C-7E0E3702680B} {B2F7C18F-2333-432B-AC5E-9AD672582DF3} = {F845BA28-9AFC-4B52-8ED1-A4302AEB5C11} + {07E13495-DC86-43BF-9E64-2CEA381D892D} = {4A77449F-D456-4E19-B31C-7E0E3702680B} + {3CB7A441-325E-41C9-B0D3-30D29CC21E82} = {4A77449F-D456-4E19-B31C-7E0E3702680B} {F845BA28-9AFC-4B52-8ED1-A4302AEB5C11} = {4A77449F-D456-4E19-B31C-7E0E3702680B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {F9B8DA67-C83B-466D-907C-9541CDBDCFEF} EndGlobalSection + GlobalSection(SharedMSBuildProjectFiles) = preSolution + ..\..\tools\illink\src\ILLink.Shared\ILLink.Shared.projitems*{aaba2e7b-6b45-4dd7-9c33-5f0fcda1193f}*SharedItemsImports = 5 + ..\..\tools\illink\src\ILLink.Shared\ILLink.Shared.projitems*{eb826fa8-035f-4def-8767-cbf94446916b}*SharedItemsImports = 5 + EndGlobalSection EndGlobal diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/GnuTarEntry.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/GnuTarEntry.cs index ee3bad50c74506..3a2a416704eb8d 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/GnuTarEntry.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/GnuTarEntry.cs @@ -20,7 +20,9 @@ internal GnuTarEntry(TarHeader header, TarReader readerOfOrigin) /// /// The type of the entry. /// A string with the path and file name of this entry. - /// When creating an instance using the constructor, only the following entry types are supported: + /// + /// When creating an instance of using this constructor, the and properties are set to , which in the entry header atime and ctime fields is written as null bytes. This ensures compatibility with other tools that are unable to read the atime and ctime in entries, as these two fields are not POSIX compatible because other formats expect the prefix field in the same header location where writes atime and ctime. + /// When creating an instance using the constructor, only the following entry types are supported: /// /// In all platforms: , , , . /// In Unix platforms only: , and . @@ -33,13 +35,16 @@ internal GnuTarEntry(TarHeader header, TarReader readerOfOrigin) public GnuTarEntry(TarEntryType entryType, string entryName) : base(entryType, entryName, TarEntryFormat.Gnu, isGea: false) { - _header._aTime = _header._mTime; // mtime was set in base constructor - _header._cTime = _header._mTime; + _header._aTime = default; + _header._cTime = default; } /// /// Initializes a new instance by converting the specified entry into the GNU format. /// + /// + /// When creating an instance of using this constructor, if is or , then the and properties are set to the same values set in . But if is of any other format, then and are set to , which in the entry header atime and ctime fields is written as null bytes. This ensures compatibility with other tools that are unable to read the atime and ctime in entries, as these two fields are not POSIX compatible because other formats expect the prefix field in the same header location where writes atime and ctime. + /// /// is a and cannot be converted. /// -or- /// The entry type of is not supported for conversion to the GNU format. @@ -54,45 +59,23 @@ public GnuTarEntry(TarEntry other) } else { - bool changedATime = false; - bool changedCTime = false; - - if (other is PaxTarEntry paxOther) - { - changedATime = TarHelpers.TryGetDateTimeOffsetFromTimestampString(paxOther._header.ExtendedAttributes, TarHeader.PaxEaATime, out DateTimeOffset aTime); - if (changedATime) - { - _header._aTime = aTime; - } - - changedCTime = TarHelpers.TryGetDateTimeOffsetFromTimestampString(paxOther._header.ExtendedAttributes, TarHeader.PaxEaCTime, out DateTimeOffset cTime); - if (changedCTime) - { - _header._cTime = cTime; - } - } - - // Either 'other' was V7 or Ustar (those formats do not have atime or ctime), - // or 'other' was PAX and at least one of the timestamps was not found in the extended attributes - if (!changedATime || !changedCTime) - { - DateTimeOffset now = DateTimeOffset.UtcNow; - if (!changedATime) - { - _header._aTime = now; - } - if (!changedCTime) - { - _header._cTime = now; - } - } + // 'other' was V7, Ustar (those formats do not have atime or ctime), + // or even PAX (which could contain atime and ctime in the ExtendedAttributes), but + // to avoid creating a GNU entry that might be incompatible with other tools, + // we avoid setting the atime and ctime fields. The user would have to set them manually + // if they are really needed. + _header._aTime = default; + _header._cTime = default; } } /// - /// A timestamp that represents the last time the file represented by this entry was accessed. + /// A timestamp that represents the last time the file represented by this entry was accessed. Setting a value for this property is not recommended because most TAR reading tools do not support it. /// - /// In Unix platforms, this timestamp is commonly known as atime. + /// + /// In Unix platforms, this timestamp is commonly known as atime. + /// Setting the value of this property to a value other than may cause problems with other tools that read TAR files, because the format writes the atime field where other formats would normally read and write the prefix field in the header. You should only set this property to something other than if this entry will be read by tools that know how to correctly interpret the atime field of the format. + /// public DateTimeOffset AccessTime { get => _header._aTime; @@ -103,9 +86,12 @@ public DateTimeOffset AccessTime } /// - /// A timestamp that represents the last time the metadata of the file represented by this entry was changed. + /// A timestamp that represents the last time the metadata of the file represented by this entry was changed. Setting a value for this property is not recommended because most TAR reading tools do not support it. /// - /// In Unix platforms, this timestamp is commonly known as ctime. + /// + /// In Unix platforms, this timestamp is commonly known as ctime. + /// Setting the value of this property to a value other than may cause problems with other tools that read TAR files, because the format writes the ctime field where other formats would normally read and write the prefix field in the header. You should only set this property to something other than if this entry will be read by tools that know how to correctly interpret the ctime field of the format. + /// public DateTimeOffset ChangeTime { get => _header._cTime; diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/PaxTarEntry.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/PaxTarEntry.cs index 555e4feaa27f73..a062993fef0473 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 @@ -103,9 +103,12 @@ public PaxTarEntry(TarEntryType entryType, string entryName, IEnumerable /// Initializes a new instance by converting the specified entry into the PAX format. /// - /// is a and cannot be converted. + /// + /// is a and cannot be converted. /// -or- - /// The entry type of is not supported for conversion to the PAX format. + /// The entry type of is not supported for conversion to the PAX format. + /// + /// When converting a to using this constructor, the and values will get transfered to the dictionary only if their values are not (which is ). public PaxTarEntry(TarEntry other) : base(other, TarEntryFormat.Pax) { @@ -122,8 +125,14 @@ public PaxTarEntry(TarEntry other) { if (other is GnuTarEntry gnuOther) { - _header.ExtendedAttributes[TarHeader.PaxEaATime] = TarHelpers.GetTimestampStringFromDateTimeOffset(gnuOther.AccessTime); - _header.ExtendedAttributes[TarHeader.PaxEaCTime] = TarHelpers.GetTimestampStringFromDateTimeOffset(gnuOther.ChangeTime); + if (gnuOther.AccessTime != default) + { + _header.ExtendedAttributes[TarHeader.PaxEaATime] = TarHelpers.GetTimestampStringFromDateTimeOffset(gnuOther.AccessTime); + } + if (gnuOther.ChangeTime != default) + { + _header.ExtendedAttributes[TarHeader.PaxEaCTime] = TarHelpers.GetTimestampStringFromDateTimeOffset(gnuOther.ChangeTime); + } } } 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 99ee9b667f410c..e15d7a1add2dbf 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 @@ -15,6 +15,8 @@ namespace System.Formats.Tar // Reads the header attributes from a tar archive entry. internal sealed partial class TarHeader { + private readonly byte[] ArrayOf12NullBytes = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + // Attempts to retrieve the next header from the specified tar archive stream. // Throws if end of stream is reached or if any data type conversion fails. // Returns a valid TarHeader object if the attributes were read successfully, null otherwise. @@ -537,11 +539,18 @@ private void ReadPosixAndGnuSharedAttributes(ReadOnlySpan buffer) private void ReadGnuAttributes(ReadOnlySpan buffer) { // Convert byte arrays - long aTime = TarHelpers.ParseNumeric(buffer.Slice(FieldLocations.ATime, FieldLengths.ATime)); - _aTime = TarHelpers.GetDateTimeOffsetFromSecondsSinceEpoch(aTime); - - long cTime = TarHelpers.ParseNumeric(buffer.Slice(FieldLocations.CTime, FieldLengths.CTime)); - _cTime = TarHelpers.GetDateTimeOffsetFromSecondsSinceEpoch(cTime); + ReadOnlySpan aTimeBuffer = buffer.Slice(FieldLocations.ATime, FieldLengths.ATime); + if (!aTimeBuffer.SequenceEqual(ArrayOf12NullBytes)) // null values are ignored + { + long aTime = TarHelpers.ParseNumeric(aTimeBuffer); + _aTime = TarHelpers.GetDateTimeOffsetFromSecondsSinceEpoch(aTime); + } + ReadOnlySpan cTimeBuffer = buffer.Slice(FieldLocations.CTime, FieldLengths.CTime); + if (!cTimeBuffer.SequenceEqual(ArrayOf12NullBytes)) // An all nulls buffer is interpreted as MinValue + { + long cTime = TarHelpers.ParseNumeric(cTimeBuffer); + _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 dd7e15753cf0bb..421cf210efdd08 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 @@ -29,6 +29,9 @@ internal sealed partial class TarHeader private const string GnuLongMetadataName = "././@LongLink"; private const string ArgNameEntry = "entry"; + private const int RootUidGid = 0; + private const string RootUNameGName = "root"; + // Writes the entry in the order required to be able to obtain the seekable data stream size. private void WriteWithSeekableDataStream(TarEntryFormat format, Stream archiveStream, Span buffer) { @@ -358,9 +361,9 @@ internal async Task WriteAsPaxAsync(Stream archiveStream, Memory buffer, C // .NET strings do not include a null terminator by default, need to add it manually and also consider it for the length. private bool IsLinkNameTooLongForRegularField() => _linkName != null && (Encoding.UTF8.GetByteCount(_linkName) + 1) > FieldLengths.LinkName; - // Checks if the name string is too long to fit in the regular header field. + // Checks if the name string is too long to fit in the regular header field (excluding null char). // .NET strings do not include a null terminator by default, need to add it manually and also consider it for the length. - private bool IsNameTooLongForRegularField() => (Encoding.UTF8.GetByteCount(_name) + 1) > FieldLengths.Name; + private bool IsNameTooLongForRegularField() => (Encoding.UTF8.GetByteCount(_name)) > FieldLengths.Name; // Writes the current header as a Gnu entry into the archive stream. // Makes sure to add the preceding LongLink and/or LongPath entries if necessary, before the actual entry. @@ -446,17 +449,17 @@ private TarHeader GetGnuLongLinkMetadataHeader() { Debug.Assert(_linkName != null); MemoryStream dataStream = GetLongMetadataStream(_linkName); - return GetGnuLongMetadataHeader(dataStream, TarEntryType.LongLink, _uid, _gid, _uName, _gName); + return GetGnuLongMetadataHeader(dataStream, TarEntryType.LongLink); } private TarHeader GetGnuLongPathMetadataHeader() { MemoryStream dataStream = GetLongMetadataStream(_name); - return GetGnuLongMetadataHeader(dataStream, TarEntryType.LongPath, _uid, _gid, _uName, _gName); + return GetGnuLongMetadataHeader(dataStream, TarEntryType.LongPath); } // Creates and returns a GNU long metadata header, with the specified long text written into its data stream (seekable). - private static TarHeader GetGnuLongMetadataHeader(MemoryStream dataStream, TarEntryType entryType, int mainEntryUid, int mainEntryGid, string? mainEntryUname, string? mainEntryGname) + private static TarHeader GetGnuLongMetadataHeader(MemoryStream dataStream, TarEntryType entryType) { Debug.Assert(entryType is TarEntryType.LongPath or TarEntryType.LongLink); @@ -464,15 +467,15 @@ private static TarHeader GetGnuLongMetadataHeader(MemoryStream dataStream, TarEn { _name = GnuLongMetadataName, // Same name for both longpath or longlink _mode = TarHelpers.GetDefaultMode(entryType), - _uid = mainEntryUid, - _gid = mainEntryGid, - _mTime = DateTimeOffset.UnixEpoch, // 0 + _uid = RootUidGid, + _gid = RootUidGid, + _mTime = DateTimeOffset.UnixEpoch, // Stores as series of 0 characters _typeFlag = entryType, _dataStream = dataStream, - _uName = mainEntryUname, - _gName = mainEntryGname, - _aTime = DateTimeOffset.UnixEpoch, // 0 - _cTime = DateTimeOffset.UnixEpoch, // 0 + _uName = RootUNameGName, + _gName = RootUNameGName, + _aTime = default, // LongLink/LongPath entries store these as nulls + _cTime = default, // LongLink/LongPath entries store these as nulls }; } @@ -783,8 +786,19 @@ private int WritePosixAndGnuSharedFields(Span buffer) // Saves the gnu-specific fields into the specified spans. private int WriteGnuFields(Span buffer) { - int checksum = WriteAsTimestamp(_aTime, buffer.Slice(FieldLocations.ATime, FieldLengths.ATime)); - checksum += WriteAsTimestamp(_cTime, buffer.Slice(FieldLocations.CTime, FieldLengths.CTime)); + int checksum = 0; + + if (_typeFlag is not TarEntryType.LongLink and not TarEntryType.LongPath) + { + if (_aTime != default) + { + checksum += WriteAsTimestamp(_aTime, buffer.Slice(FieldLocations.ATime, FieldLengths.ATime)); + } + if (_cTime != default) + { + checksum += WriteAsTimestamp(_cTime, buffer.Slice(FieldLocations.CTime, FieldLengths.CTime)); + } + } if (_gnuUnusedBytes != null) { @@ -1031,7 +1045,7 @@ private static int WriteChecksum(int checksum, Span buffer) destination[^2] = (byte)'\0'; int i = destination.Length - 3; - int j = converted.Length - 1; + int j = converted.Length - 2; // Skip the null terminator in 'converted' while (i >= 0) { 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 de3aeb31cb8920..fb0b76faad2870 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 @@ -63,8 +63,11 @@ private TarEntry ConstructEntryForWriting(string fullPath, string entryName, Fil } entry._header._mTime = TarHelpers.GetDateTimeOffsetFromSecondsSinceEpoch(status.MTime); - entry._header._aTime = TarHelpers.GetDateTimeOffsetFromSecondsSinceEpoch(status.ATime); - entry._header._cTime = TarHelpers.GetDateTimeOffsetFromSecondsSinceEpoch(status.CTime); + // We do not set atime and ctime by default because many external tools are unable to read GNU entries + // that have these fields set to non-zero values. This is because the GNU format writes atime and ctime in the same + // location where other formats expect the prefix field to be written. + // If the user wants to set atime and ctime, they can do so by constructing the entry manually from the file and + // then setting the values. // This mask only keeps the least significant 12 bits valid for UnixFileModes entry._header._mode = status.Mode & (int)TarHelpers.ValidUnixFileModes; 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 29905beef85e0e..9443c7cd7a09a9 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 @@ -51,8 +51,11 @@ private TarEntry ConstructEntryForWriting(string fullPath, string entryName, Fil FileSystemInfo info = (attributes & FileAttributes.Directory) != 0 ? new DirectoryInfo(fullPath) : new FileInfo(fullPath); entry._header._mTime = info.LastWriteTimeUtc; - entry._header._aTime = info.LastAccessTimeUtc; - entry._header._cTime = info.LastWriteTimeUtc; // There is no "change time" property + // We do not set atime and ctime by default because many external tools are unable to read GNU entries + // that have these fields set to non-zero values. This is because the GNU format writes atime and ctime in the same + // location where other formats expect the prefix field to be written. + // If the user wants to set atime and ctime, they can do so by constructing the entry manually from the file and + // then setting the values. entry.Mode = DefaultWindowsMode; diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarWriter.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarWriter.cs index a3e1bd82d25918..321c4923aefaf9 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarWriter.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarWriter.cs @@ -27,6 +27,7 @@ public sealed partial class TarWriter : IDisposable, IAsyncDisposable /// When using this constructor, is used as the default format of the entries written to the archive using the method. /// is . /// does not support writing. + /// The format is the default format as it is the most flexible and POSIX compatible. This is the only format with which reads and stores atime and ctime when creating entries from filesystem entries. public TarWriter(Stream archiveStream) : this(archiveStream, TarEntryFormat.Pax, leaveOpen: false) { @@ -39,6 +40,7 @@ public TarWriter(Stream archiveStream) /// to dispose the when this instance is disposed; to leave the stream open. /// is . /// is unwritable. + /// The format is the default format as it is the most flexible and POSIX compatible. This is the only format with which reads and stores atime and ctime when creating entries from filesystem entries. public TarWriter(Stream archiveStream, bool leaveOpen = false) : this(archiveStream, TarEntryFormat.Pax, leaveOpen) { @@ -56,6 +58,7 @@ public TarWriter(Stream archiveStream, bool leaveOpen = false) /// is . /// is unwritable. /// is either , or not one of the other enum values. + /// The format is the default for the other constructors. This is the recommended format as it is the most flexible and POSIX compatible. This is the only format with which reads and stores atime and ctime when creating entries from filesystem entries. public TarWriter(Stream archiveStream, TarEntryFormat format = TarEntryFormat.Pax, bool leaveOpen = false) { ArgumentNullException.ThrowIfNull(archiveStream); @@ -133,6 +136,10 @@ public async ValueTask DisposeAsync() /// The archive stream is disposed. /// or is or empty. /// An I/O problem occurred. + /// + /// The entry will be created using the format specified in the constructor, or will use if other constructors are used. + /// If the format is , the atime and ctime from the file will be stored in the dictionary. If the format is , this method will not set a value for and because most TAR tools do not support these fields for this format. + /// public void WriteEntry(string fileName, string? entryName) { (string fullPath, string actualEntryName) = ValidateWriteEntryArguments(fileName, entryName); @@ -149,6 +156,10 @@ public void WriteEntry(string fileName, string? entryName) /// The archive stream is disposed. /// or is or empty. /// An I/O problem occurred. + /// + /// The entry will be created using the format specified in the constructor, or will use if other constructors are used. + /// If the format is , the atime and ctime from the file will be stored in the dictionary. If the format is , this method will not set a value for and because most TAR tools do not support these fields for this format. + /// public Task WriteEntryAsync(string fileName, string? entryName, CancellationToken cancellationToken = default) { if (cancellationToken.IsCancellationRequested) @@ -217,6 +228,10 @@ private async Task ReadFileFromDiskAndWriteToArchiveStreamAsEntryAsync(string fu /// The archive stream is disposed. /// is . /// An I/O problem occurred. + /// + /// When writing a using this method, if and/or are set, they will be preserved in the archive. These fields are unsupported by most TAR tools, so to ensure the archive is readable by other tools, make sure to set and to or . + /// To ensure an entry preserves the atime and ctime values and it is readable by other tools, it is recommended to convert the entry to instead. In that format, the two values get stored in the . The format is used as the default format by as it is the most flexible and POSIX compatible. + /// public void WriteEntry(TarEntry entry) { ObjectDisposedException.ThrowIf(_isDisposed, this); diff --git a/src/libraries/System.Formats.Tar/tests/TarEntry/GnuTarEntry.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarEntry/GnuTarEntry.Tests.cs index bc80d89becbc06..52ea9dd532afed 100644 --- a/src/libraries/System.Formats.Tar/tests/TarEntry/GnuTarEntry.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarEntry/GnuTarEntry.Tests.cs @@ -14,931 +14,947 @@ namespace System.Formats.Tar.Tests { public class GnuTarEntry_Tests : TarTestsBase -{ - [Fact] - public void Constructor_InvalidEntryName() { - Assert.Throws(() => new GnuTarEntry(TarEntryType.RegularFile, entryName: null)); - Assert.Throws(() => new GnuTarEntry(TarEntryType.RegularFile, entryName: string.Empty)); - } - - [Fact] - public void Constructor_UnsupportedEntryTypes() - { - Assert.Throws(() => new GnuTarEntry((TarEntryType)byte.MaxValue, InitialEntryName)); - - Assert.Throws(() => new GnuTarEntry(TarEntryType.ExtendedAttributes, InitialEntryName)); - Assert.Throws(() => new GnuTarEntry(TarEntryType.GlobalExtendedAttributes, InitialEntryName)); - Assert.Throws(() => new GnuTarEntry(TarEntryType.V7RegularFile, InitialEntryName)); - - // These are specific to GNU, but currently the user cannot create them manually - Assert.Throws(() => new GnuTarEntry(TarEntryType.ContiguousFile, InitialEntryName)); - Assert.Throws(() => new GnuTarEntry(TarEntryType.DirectoryList, InitialEntryName)); - Assert.Throws(() => new GnuTarEntry(TarEntryType.MultiVolume, InitialEntryName)); - Assert.Throws(() => new GnuTarEntry(TarEntryType.RenamedOrSymlinked, InitialEntryName)); - Assert.Throws(() => new GnuTarEntry(TarEntryType.SparseFile, InitialEntryName)); - Assert.Throws(() => new GnuTarEntry(TarEntryType.TapeVolume, InitialEntryName)); - - // The user should not create these entries manually - Assert.Throws(() => new GnuTarEntry(TarEntryType.LongLink, InitialEntryName)); - Assert.Throws(() => new GnuTarEntry(TarEntryType.LongPath, InitialEntryName)); - } - - [Fact] - public void SupportedEntryType_RegularFile() - { - GnuTarEntry regularFile = new GnuTarEntry(TarEntryType.RegularFile, InitialEntryName); - SetRegularFile(regularFile); - VerifyRegularFile(regularFile, isWritable: true); - } - - [Fact] - public void SupportedEntryType_Directory() - { - GnuTarEntry directory = new GnuTarEntry(TarEntryType.Directory, InitialEntryName); - SetDirectory(directory); - VerifyDirectory(directory); - } - - [Fact] - public void SupportedEntryType_HardLink() - { - GnuTarEntry hardLink = new GnuTarEntry(TarEntryType.HardLink, InitialEntryName); - SetHardLink(hardLink); - VerifyHardLink(hardLink); - } - - [Fact] - public void SupportedEntryType_SymbolicLink() - { - GnuTarEntry symbolicLink = new GnuTarEntry(TarEntryType.SymbolicLink, InitialEntryName); - SetSymbolicLink(symbolicLink); - VerifySymbolicLink(symbolicLink); - } + [Fact] + public void Constructor_InvalidEntryName() + { + Assert.Throws(() => new GnuTarEntry(TarEntryType.RegularFile, entryName: null)); + Assert.Throws(() => new GnuTarEntry(TarEntryType.RegularFile, entryName: string.Empty)); + } - [Fact] - public void SupportedEntryType_BlockDevice() - { - GnuTarEntry blockDevice = new GnuTarEntry(TarEntryType.BlockDevice, InitialEntryName); - SetBlockDevice(blockDevice); - VerifyBlockDevice(blockDevice); - } + [Fact] + public void Constructor_UnsupportedEntryTypes() + { + Assert.Throws(() => new GnuTarEntry((TarEntryType)byte.MaxValue, InitialEntryName)); - [Fact] - public void SupportedEntryType_CharacterDevice() - { - GnuTarEntry characterDevice = new GnuTarEntry(TarEntryType.CharacterDevice, InitialEntryName); - SetCharacterDevice(characterDevice); - VerifyCharacterDevice(characterDevice); - } + Assert.Throws(() => new GnuTarEntry(TarEntryType.ExtendedAttributes, InitialEntryName)); + Assert.Throws(() => new GnuTarEntry(TarEntryType.GlobalExtendedAttributes, InitialEntryName)); + Assert.Throws(() => new GnuTarEntry(TarEntryType.V7RegularFile, InitialEntryName)); - [Fact] - public void SupportedEntryType_Fifo() - { - GnuTarEntry fifo = new GnuTarEntry(TarEntryType.Fifo, InitialEntryName); - SetFifo(fifo); - VerifyFifo(fifo); - } + // These are specific to GNU, but currently the user cannot create them manually + Assert.Throws(() => new GnuTarEntry(TarEntryType.ContiguousFile, InitialEntryName)); + Assert.Throws(() => new GnuTarEntry(TarEntryType.DirectoryList, InitialEntryName)); + Assert.Throws(() => new GnuTarEntry(TarEntryType.MultiVolume, InitialEntryName)); + Assert.Throws(() => new GnuTarEntry(TarEntryType.RenamedOrSymlinked, InitialEntryName)); + Assert.Throws(() => new GnuTarEntry(TarEntryType.SparseFile, InitialEntryName)); + Assert.Throws(() => new GnuTarEntry(TarEntryType.TapeVolume, InitialEntryName)); - [Theory] - [InlineData(false)] - [InlineData(true)] - public void DataOffset_RegularFile(bool canSeek) - { - using MemoryStream ms = new(); - using (TarWriter writer = new(ms, leaveOpen: true)) - { - GnuTarEntry entry = new GnuTarEntry(TarEntryType.RegularFile, InitialEntryName); - entry.DataStream = new MemoryStream(); - entry.DataStream.WriteByte(ExpectedOffsetDataSingleByte); - entry.DataStream.Position = 0; - writer.WriteEntry(entry); + // The user should not create these entries manually + Assert.Throws(() => new GnuTarEntry(TarEntryType.LongLink, InitialEntryName)); + Assert.Throws(() => new GnuTarEntry(TarEntryType.LongPath, InitialEntryName)); } - ms.Position = 0; - - using Stream streamToRead = new WrappedStream(ms, canWrite: true, canRead: true, canSeek: canSeek); - using TarReader reader = new(streamToRead); - TarEntry actualEntry = reader.GetNextEntry(); - Assert.NotNull(actualEntry); - // Then it writes the actual regular file entry, containing: - // * 512 bytes of the regular tar header - // Totalling 2560. - // The regular file data section starts on the next byte. - long expectedDataOffset = canSeek ? 512 : -1; - Assert.Equal(expectedDataOffset, actualEntry.DataOffset); - - if (canSeek) + [Fact] + public void SupportedEntryType_RegularFile() { - ms.Position = actualEntry.DataOffset; - byte actualData = (byte)ms.ReadByte(); - Assert.Equal(ExpectedOffsetDataSingleByte, actualData); + GnuTarEntry regularFile = new GnuTarEntry(TarEntryType.RegularFile, InitialEntryName); + SetRegularFile(regularFile); + VerifyRegularFile(regularFile, isWritable: true); } - } - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task DataOffset_RegularFile_Async(bool canSeek) - { - await using MemoryStream ms = new(); - await using (TarWriter writer = new(ms, leaveOpen: true)) + [Fact] + public void SupportedEntryType_Directory() { - GnuTarEntry entry = new GnuTarEntry(TarEntryType.RegularFile, InitialEntryName); - entry.DataStream = new MemoryStream(); - entry.DataStream.WriteByte(ExpectedOffsetDataSingleByte); - entry.DataStream.Position = 0; - await writer.WriteEntryAsync(entry); + GnuTarEntry directory = new GnuTarEntry(TarEntryType.Directory, InitialEntryName); + SetDirectory(directory); + VerifyDirectory(directory); } - ms.Position = 0; - - await using Stream streamToRead = new WrappedStream(ms, canWrite: true, canRead: true, canSeek: canSeek); - await using TarReader reader = new(streamToRead); - TarEntry actualEntry = await reader.GetNextEntryAsync(); - Assert.NotNull(actualEntry); - - // Then it writes the actual regular file entry, containing: - // * 512 bytes of the regular tar header - // Totalling 2560. - // The regular file data section starts on the next byte. - long expectedDataOffset = canSeek ? 512 : -1; - Assert.Equal(expectedDataOffset, actualEntry.DataOffset); - if (canSeek) + [Fact] + public void SupportedEntryType_HardLink() { - ms.Position = actualEntry.DataOffset; - byte actualData = (byte)ms.ReadByte(); - Assert.Equal(ExpectedOffsetDataSingleByte, actualData); + GnuTarEntry hardLink = new GnuTarEntry(TarEntryType.HardLink, InitialEntryName); + SetHardLink(hardLink); + VerifyHardLink(hardLink); } - } - [Theory] - [InlineData(false)] - [InlineData(true)] - public void DataOffset_RegularFile_LongPath(bool canSeek) - { - using MemoryStream ms = new(); - using (TarWriter writer = new(ms, leaveOpen: true)) + [Fact] + public void SupportedEntryType_SymbolicLink() { - string veryLongName = new string('a', 1234); // Forces using a GNU LongPath entry - GnuTarEntry entry = new GnuTarEntry(TarEntryType.RegularFile, veryLongName); - entry.DataStream = new MemoryStream(); - entry.DataStream.WriteByte(ExpectedOffsetDataSingleByte); - entry.DataStream.Position = 0; - writer.WriteEntry(entry); + GnuTarEntry symbolicLink = new GnuTarEntry(TarEntryType.SymbolicLink, InitialEntryName); + SetSymbolicLink(symbolicLink); + VerifySymbolicLink(symbolicLink); } - ms.Position = 0; - - using Stream streamToRead = new WrappedStream(ms, canWrite: true, canRead: true, canSeek: canSeek); - using TarReader reader = new(streamToRead); - TarEntry actualEntry = reader.GetNextEntry(); - Assert.NotNull(actualEntry); - - // GNU first writes the long path entry, containing: - // * 512 bytes of the regular tar header - // * 1234 bytes for the data section containing the full long path - // * 302 bytes of padding - // Then it writes the actual regular file entry, containing: - // * 512 bytes of the regular tar header - // Totalling 2560. - // The regular file data section starts on the next byte. - long expectedDataOffset = canSeek ? 2560 : -1; - Assert.Equal(expectedDataOffset, actualEntry.DataOffset); - } - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task DataOffset_RegularFile_LongPath_Async(bool canSeek) - { - await using MemoryStream ms = new(); - await using (TarWriter writer = new(ms, leaveOpen: true)) + [Fact] + public void SupportedEntryType_BlockDevice() { - string veryLongName = new string('a', 1234); // Forces using a GNU LongPath entry - GnuTarEntry entry = new GnuTarEntry(TarEntryType.RegularFile, veryLongName); - entry.DataStream = new MemoryStream(); - entry.DataStream.WriteByte(ExpectedOffsetDataSingleByte); - entry.DataStream.Position = 0; - await writer.WriteEntryAsync(entry); + GnuTarEntry blockDevice = new GnuTarEntry(TarEntryType.BlockDevice, InitialEntryName); + SetBlockDevice(blockDevice); + VerifyBlockDevice(blockDevice); } - ms.Position = 0; - - await using Stream streamToRead = new WrappedStream(ms, canWrite: true, canRead: true, canSeek: canSeek); - await using TarReader reader = new(streamToRead); - TarEntry actualEntry = await reader.GetNextEntryAsync(); - Assert.NotNull(actualEntry); - - // GNU first writes the long path entry, containing: - // * 512 bytes of the regular tar header - // * 1234 bytes for the data section containing the full long path - // * 302 bytes of padding - // Then it writes the actual regular file entry, containing: - // * 512 bytes of the regular tar header - // Totalling 2560. - // The regular file data section starts on the next byte. - long expectedDataOffset = canSeek ? 2560 : -1; - Assert.Equal(expectedDataOffset, actualEntry.DataOffset); - } - [Theory] - [InlineData(false)] - [InlineData(true)] - public void DataOffset_RegularFile_LongLink(bool canSeek) - { - using MemoryStream ms = new(); - using (TarWriter writer = new(ms, leaveOpen: true)) + [Fact] + public void SupportedEntryType_CharacterDevice() { - GnuTarEntry entry = new GnuTarEntry(TarEntryType.SymbolicLink, InitialEntryName); - entry.LinkName = new string('a', 1234); // Forces using a GNU LongLink entry - writer.WriteEntry(entry); + GnuTarEntry characterDevice = new GnuTarEntry(TarEntryType.CharacterDevice, InitialEntryName); + SetCharacterDevice(characterDevice); + VerifyCharacterDevice(characterDevice); } - ms.Position = 0; - - using Stream streamToRead = new WrappedStream(ms, canWrite: true, canRead: true, canSeek: canSeek); - using TarReader reader = new(streamToRead); - TarEntry actualEntry = reader.GetNextEntry(); - Assert.NotNull(actualEntry); - - // GNU first writes the long link entry, containing: - // * 512 bytes of the regular tar header - // * 1234 bytes for the data section containing the full long link - // * 302 bytes of padding - // Then it writes the actual regular file entry, containing: - // * 512 bytes of the regular tar header - // Totalling 2560. - // The regular file data section starts on the next byte. - long expectedDataOffset = canSeek ? 2560 : -1; - Assert.Equal(expectedDataOffset, actualEntry.DataOffset); - } - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task DataOffset_RegularFile_LongLink_Async(bool canSeek) - { - await using MemoryStream ms = new(); - await using (TarWriter writer = new(ms, leaveOpen: true)) + + [Fact] + public void SupportedEntryType_Fifo() { - GnuTarEntry entry = new GnuTarEntry(TarEntryType.SymbolicLink, InitialEntryName); - entry.LinkName = new string('b', 1234); // Forces using a GNU LongLink entry - await writer.WriteEntryAsync(entry); + GnuTarEntry fifo = new GnuTarEntry(TarEntryType.Fifo, InitialEntryName); + SetFifo(fifo); + VerifyFifo(fifo); } - ms.Position = 0; - - await using Stream streamToRead = new WrappedStream(ms, canWrite: true, canRead: true, canSeek: canSeek); - await using TarReader reader = new(streamToRead); - TarEntry actualEntry = await reader.GetNextEntryAsync(); - Assert.NotNull(actualEntry); - - // GNU first writes the long link entry, containing: - // * 512 bytes of the regular tar header - // * 1234 bytes for the data section containing the full long link - // * 302 bytes of padding - // Then it writes the actual regular file entry, containing: - // * 512 bytes of the regular tar header - // Totalling 2560. - // The regular file data section starts on the next byte. - long expectedDataOffset = canSeek ? 2560 : -1; - Assert.Equal(expectedDataOffset, actualEntry.DataOffset); - } - [Theory] - [InlineData(false)] - [InlineData(true)] - public void DataOffset_RegularFile_LongLink_LongPath(bool canSeek) - { - using MemoryStream ms = new(); - using (TarWriter writer = new(ms, leaveOpen: true)) + [Theory] + [InlineData(false)] + [InlineData(true)] + public void DataOffset_RegularFile(bool canSeek) { - string veryLongName = new string('a', 1234); // Forces using a GNU LongPath entry - GnuTarEntry entry = new GnuTarEntry(TarEntryType.SymbolicLink, veryLongName); - entry.LinkName = new string('b', 1234); // Forces using a GNU LongLink entry - writer.WriteEntry(entry); + using MemoryStream ms = new(); + using (TarWriter writer = new(ms, leaveOpen: true)) + { + GnuTarEntry entry = new GnuTarEntry(TarEntryType.RegularFile, InitialEntryName); + entry.DataStream = new MemoryStream(); + entry.DataStream.WriteByte(ExpectedOffsetDataSingleByte); + entry.DataStream.Position = 0; + writer.WriteEntry(entry); + } + ms.Position = 0; + + using Stream streamToRead = new WrappedStream(ms, canWrite: true, canRead: true, canSeek: canSeek); + using TarReader reader = new(streamToRead); + TarEntry actualEntry = reader.GetNextEntry(); + Assert.NotNull(actualEntry); + + // Then it writes the actual regular file entry, containing: + // * 512 bytes of the regular tar header + // Totalling 2560. + // The regular file data section starts on the next byte. + long expectedDataOffset = canSeek ? 512 : -1; + Assert.Equal(expectedDataOffset, actualEntry.DataOffset); + + if (canSeek) + { + ms.Position = actualEntry.DataOffset; + byte actualData = (byte)ms.ReadByte(); + Assert.Equal(ExpectedOffsetDataSingleByte, actualData); + } } - ms.Position = 0; - - using Stream streamToRead = new WrappedStream(ms, canWrite: true, canRead: true, canSeek: canSeek); - using TarReader reader = new(streamToRead); - TarEntry actualEntry = reader.GetNextEntry(); - Assert.NotNull(actualEntry); - - // GNU first writes the long link and long path entries, containing: - // * 512 bytes of the regular long link tar header - // * 1234 bytes for the data section containing the full long link - // * 302 bytes of padding - // * 512 bytes of the regular long path tar header - // * 1234 bytes for the data section containing the full long path - // * 302 bytes of padding - // Then it writes the actual entry, containing: - // * 512 bytes of the regular tar header - // Totalling 4608. - // The data section starts on the next byte. - long expectedDataOffset = canSeek ? 4608 : -1; - Assert.Equal(expectedDataOffset, actualEntry.DataOffset); - } - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task DataOffset_RegularFile_LongLink_LongPath_Async(bool canSeek) - { - await using MemoryStream ms = new(); - await using (TarWriter writer = new(ms, leaveOpen: true)) + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task DataOffset_RegularFile_Async(bool canSeek) { - string veryLongName = new string('a', 1234); // Forces using a GNU LongPath entry - GnuTarEntry entry = new GnuTarEntry(TarEntryType.SymbolicLink, veryLongName); - entry.LinkName = new string('b', 1234); // Forces using a GNU LongLink entry - await writer.WriteEntryAsync(entry); - } - ms.Position = 0; - - await using Stream streamToRead = new WrappedStream(ms, canWrite: true, canRead: true, canSeek: canSeek); - await using TarReader reader = new(streamToRead); - TarEntry actualEntry = await reader.GetNextEntryAsync(); - Assert.NotNull(actualEntry); - - // GNU first writes the long link and long path entries, containing: - // * 512 bytes of the regular long link tar header - // * 1234 bytes for the data section containing the full long link - // * 302 bytes of padding - // * 512 bytes of the regular long path tar header - // * 1234 bytes for the data section containing the full long path - // * 302 bytes of padding - // Then it writes the actual entry, containing: - // * 512 bytes of the regular tar header - // Totalling 4608. - // The data section starts on the next byte. - long expectedDataOffset = canSeek ? 4608 : -1; - Assert.Equal(expectedDataOffset, actualEntry.DataOffset); - } + await using MemoryStream ms = new(); + await using (TarWriter writer = new(ms, leaveOpen: true)) + { + GnuTarEntry entry = new GnuTarEntry(TarEntryType.RegularFile, InitialEntryName); + entry.DataStream = new MemoryStream(); + entry.DataStream.WriteByte(ExpectedOffsetDataSingleByte); + entry.DataStream.Position = 0; + await writer.WriteEntryAsync(entry); + } + ms.Position = 0; - [Fact] - public void DataOffset_BeforeAndAfterArchive() - { - GnuTarEntry entry = new GnuTarEntry(TarEntryType.RegularFile, InitialEntryName); - Assert.Equal(-1, entry.DataOffset); - entry.DataStream = new MemoryStream(); - entry.DataStream.WriteByte(ExpectedOffsetDataSingleByte); - entry.DataStream.Position = 0; // The data stream is written to the archive from the current position - - using MemoryStream ms = new(); - using TarWriter writer = new(ms); - writer.WriteEntry(entry); - Assert.Equal(512, entry.DataOffset); - - // Write it again, the offset should now point to the second written entry - // First entry 512 (header) + 1 (data) + 511 (padding) - // Second entry 512 (header) - // 512 + 512 + 512 = 1536 - writer.WriteEntry(entry); - Assert.Equal(1536, entry.DataOffset); - } + await using Stream streamToRead = new WrappedStream(ms, canWrite: true, canRead: true, canSeek: canSeek); + await using TarReader reader = new(streamToRead); + TarEntry actualEntry = await reader.GetNextEntryAsync(); + Assert.NotNull(actualEntry); - [Fact] - public async Task DataOffset_BeforeAndAfterArchive_Async() - { - GnuTarEntry entry = new GnuTarEntry(TarEntryType.RegularFile, InitialEntryName); - Assert.Equal(-1, entry.DataOffset); - - entry.DataStream = new MemoryStream(); - entry.DataStream.WriteByte(ExpectedOffsetDataSingleByte); - entry.DataStream.Position = 0; // The data stream is written to the archive from the current position - - await using MemoryStream ms = new(); - await using TarWriter writer = new(ms); - await writer.WriteEntryAsync(entry); - Assert.Equal(512, entry.DataOffset); - - // Write it again, the offset should now point to the second written entry - // First entry 512 (header) + 1 (data) + 511 (padding) - // Second entry 512 (header) - // 512 + 512 + 512 = 1536 - await writer.WriteEntryAsync(entry); - Assert.Equal(1536, entry.DataOffset); - } + // Then it writes the actual regular file entry, containing: + // * 512 bytes of the regular tar header + // Totalling 2560. + // The regular file data section starts on the next byte. + long expectedDataOffset = canSeek ? 512 : -1; + Assert.Equal(expectedDataOffset, actualEntry.DataOffset); - [Fact] - public void DataOffset_UnseekableDataStream() - { - using MemoryStream ms = new(); - using (TarWriter writer = new(ms, leaveOpen: true)) + if (canSeek) + { + ms.Position = actualEntry.DataOffset; + byte actualData = (byte)ms.ReadByte(); + Assert.Equal(ExpectedOffsetDataSingleByte, actualData); + } + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void DataOffset_RegularFile_LongPath(bool canSeek) + { + using MemoryStream ms = new(); + using (TarWriter writer = new(ms, leaveOpen: true)) + { + string veryLongName = new string('a', 1234); // Forces using a GNU LongPath entry + GnuTarEntry entry = new GnuTarEntry(TarEntryType.RegularFile, veryLongName); + entry.DataStream = new MemoryStream(); + entry.DataStream.WriteByte(ExpectedOffsetDataSingleByte); + entry.DataStream.Position = 0; + writer.WriteEntry(entry); + } + ms.Position = 0; + + using Stream streamToRead = new WrappedStream(ms, canWrite: true, canRead: true, canSeek: canSeek); + using TarReader reader = new(streamToRead); + TarEntry actualEntry = reader.GetNextEntry(); + Assert.NotNull(actualEntry); + + // GNU first writes the long path entry, containing: + // * 512 bytes of the regular tar header + // * 1234 bytes for the data section containing the full long path + // * 302 bytes of padding + // Then it writes the actual regular file entry, containing: + // * 512 bytes of the regular tar header + // Totalling 2560. + // The regular file data section starts on the next byte. + long expectedDataOffset = canSeek ? 2560 : -1; + Assert.Equal(expectedDataOffset, actualEntry.DataOffset); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task DataOffset_RegularFile_LongPath_Async(bool canSeek) + { + await using MemoryStream ms = new(); + await using (TarWriter writer = new(ms, leaveOpen: true)) + { + string veryLongName = new string('a', 1234); // Forces using a GNU LongPath entry + GnuTarEntry entry = new GnuTarEntry(TarEntryType.RegularFile, veryLongName); + entry.DataStream = new MemoryStream(); + entry.DataStream.WriteByte(ExpectedOffsetDataSingleByte); + entry.DataStream.Position = 0; + await writer.WriteEntryAsync(entry); + } + ms.Position = 0; + + await using Stream streamToRead = new WrappedStream(ms, canWrite: true, canRead: true, canSeek: canSeek); + await using TarReader reader = new(streamToRead); + TarEntry actualEntry = await reader.GetNextEntryAsync(); + Assert.NotNull(actualEntry); + + // GNU first writes the long path entry, containing: + // * 512 bytes of the regular tar header + // * 1234 bytes for the data section containing the full long path + // * 302 bytes of padding + // Then it writes the actual regular file entry, containing: + // * 512 bytes of the regular tar header + // Totalling 2560. + // The regular file data section starts on the next byte. + long expectedDataOffset = canSeek ? 2560 : -1; + Assert.Equal(expectedDataOffset, actualEntry.DataOffset); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void DataOffset_RegularFile_LongLink(bool canSeek) + { + using MemoryStream ms = new(); + using (TarWriter writer = new(ms, leaveOpen: true)) + { + GnuTarEntry entry = new GnuTarEntry(TarEntryType.SymbolicLink, InitialEntryName); + entry.LinkName = new string('a', 1234); // Forces using a GNU LongLink entry + writer.WriteEntry(entry); + } + ms.Position = 0; + + using Stream streamToRead = new WrappedStream(ms, canWrite: true, canRead: true, canSeek: canSeek); + using TarReader reader = new(streamToRead); + TarEntry actualEntry = reader.GetNextEntry(); + Assert.NotNull(actualEntry); + + // GNU first writes the long link entry, containing: + // * 512 bytes of the regular tar header + // * 1234 bytes for the data section containing the full long link + // * 302 bytes of padding + // Then it writes the actual regular file entry, containing: + // * 512 bytes of the regular tar header + // Totalling 2560. + // The regular file data section starts on the next byte. + long expectedDataOffset = canSeek ? 2560 : -1; + Assert.Equal(expectedDataOffset, actualEntry.DataOffset); + } + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task DataOffset_RegularFile_LongLink_Async(bool canSeek) + { + await using MemoryStream ms = new(); + await using (TarWriter writer = new(ms, leaveOpen: true)) + { + GnuTarEntry entry = new GnuTarEntry(TarEntryType.SymbolicLink, InitialEntryName); + entry.LinkName = new string('b', 1234); // Forces using a GNU LongLink entry + await writer.WriteEntryAsync(entry); + } + ms.Position = 0; + + await using Stream streamToRead = new WrappedStream(ms, canWrite: true, canRead: true, canSeek: canSeek); + await using TarReader reader = new(streamToRead); + TarEntry actualEntry = await reader.GetNextEntryAsync(); + Assert.NotNull(actualEntry); + + // GNU first writes the long link entry, containing: + // * 512 bytes of the regular tar header + // * 1234 bytes for the data section containing the full long link + // * 302 bytes of padding + // Then it writes the actual regular file entry, containing: + // * 512 bytes of the regular tar header + // Totalling 2560. + // The regular file data section starts on the next byte. + long expectedDataOffset = canSeek ? 2560 : -1; + Assert.Equal(expectedDataOffset, actualEntry.DataOffset); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void DataOffset_RegularFile_LongLink_LongPath(bool canSeek) + { + using MemoryStream ms = new(); + using (TarWriter writer = new(ms, leaveOpen: true)) + { + string veryLongName = new string('a', 1234); // Forces using a GNU LongPath entry + GnuTarEntry entry = new GnuTarEntry(TarEntryType.SymbolicLink, veryLongName); + entry.LinkName = new string('b', 1234); // Forces using a GNU LongLink entry + writer.WriteEntry(entry); + } + ms.Position = 0; + + using Stream streamToRead = new WrappedStream(ms, canWrite: true, canRead: true, canSeek: canSeek); + using TarReader reader = new(streamToRead); + TarEntry actualEntry = reader.GetNextEntry(); + Assert.NotNull(actualEntry); + + // GNU first writes the long link and long path entries, containing: + // * 512 bytes of the regular long link tar header + // * 1234 bytes for the data section containing the full long link + // * 302 bytes of padding + // * 512 bytes of the regular long path tar header + // * 1234 bytes for the data section containing the full long path + // * 302 bytes of padding + // Then it writes the actual entry, containing: + // * 512 bytes of the regular tar header + // Totalling 4608. + // The data section starts on the next byte. + long expectedDataOffset = canSeek ? 4608 : -1; + Assert.Equal(expectedDataOffset, actualEntry.DataOffset); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task DataOffset_RegularFile_LongLink_LongPath_Async(bool canSeek) + { + await using MemoryStream ms = new(); + await using (TarWriter writer = new(ms, leaveOpen: true)) + { + string veryLongName = new string('a', 1234); // Forces using a GNU LongPath entry + GnuTarEntry entry = new GnuTarEntry(TarEntryType.SymbolicLink, veryLongName); + entry.LinkName = new string('b', 1234); // Forces using a GNU LongLink entry + await writer.WriteEntryAsync(entry); + } + ms.Position = 0; + + await using Stream streamToRead = new WrappedStream(ms, canWrite: true, canRead: true, canSeek: canSeek); + await using TarReader reader = new(streamToRead); + TarEntry actualEntry = await reader.GetNextEntryAsync(); + Assert.NotNull(actualEntry); + + // GNU first writes the long link and long path entries, containing: + // * 512 bytes of the regular long link tar header + // * 1234 bytes for the data section containing the full long link + // * 302 bytes of padding + // * 512 bytes of the regular long path tar header + // * 1234 bytes for the data section containing the full long path + // * 302 bytes of padding + // Then it writes the actual entry, containing: + // * 512 bytes of the regular tar header + // Totalling 4608. + // The data section starts on the next byte. + long expectedDataOffset = canSeek ? 4608 : -1; + Assert.Equal(expectedDataOffset, actualEntry.DataOffset); + } + + [Fact] + public void DataOffset_BeforeAndAfterArchive() { GnuTarEntry entry = new GnuTarEntry(TarEntryType.RegularFile, InitialEntryName); Assert.Equal(-1, entry.DataOffset); + entry.DataStream = new MemoryStream(); + entry.DataStream.WriteByte(ExpectedOffsetDataSingleByte); + entry.DataStream.Position = 0; // The data stream is written to the archive from the current position - using MemoryStream dataStream = new(); - dataStream.WriteByte(ExpectedOffsetDataSingleByte); - dataStream.Position = 0; - using WrappedStream wds = new(dataStream, canWrite: true, canRead: true, canSeek: false); - entry.DataStream = wds; + using MemoryStream ms = new(); + using TarWriter writer = new(ms); + writer.WriteEntry(entry); + Assert.Equal(512, entry.DataOffset); + // Write it again, the offset should now point to the second written entry + // First entry 512 (header) + 1 (data) + 511 (padding) + // Second entry 512 (header) + // 512 + 512 + 512 = 1536 writer.WriteEntry(entry); + Assert.Equal(1536, entry.DataOffset); } - ms.Position = 0; - using TarReader reader = new(ms); - TarEntry actualEntry = reader.GetNextEntry(); - Assert.NotNull(actualEntry); - // Gnu header length is 512, data starts in the next position - Assert.Equal(512, actualEntry.DataOffset); - } - - [Fact] - public async Task DataOffset_UnseekableDataStream_Async() - { - await using MemoryStream ms = new(); - await using (TarWriter writer = new(ms, leaveOpen: true)) + [Fact] + public async Task DataOffset_BeforeAndAfterArchive_Async() { GnuTarEntry entry = new GnuTarEntry(TarEntryType.RegularFile, InitialEntryName); Assert.Equal(-1, entry.DataOffset); - await using MemoryStream dataStream = new(); - dataStream.WriteByte(ExpectedOffsetDataSingleByte); - dataStream.Position = 0; - await using WrappedStream wds = new(dataStream, canWrite: true, canRead: true, canSeek: false); - entry.DataStream = wds; + entry.DataStream = new MemoryStream(); + entry.DataStream.WriteByte(ExpectedOffsetDataSingleByte); + entry.DataStream.Position = 0; // The data stream is written to the archive from the current position + await using MemoryStream ms = new(); + await using TarWriter writer = new(ms); await writer.WriteEntryAsync(entry); - } - ms.Position = 0; + Assert.Equal(512, entry.DataOffset); - await using TarReader reader = new(ms); - TarEntry actualEntry = await reader.GetNextEntryAsync(); - Assert.NotNull(actualEntry); - // Gnu header length is 512, data starts in the next position - Assert.Equal(512, actualEntry.DataOffset); - } - - [Theory] - [InlineData(false)] - [InlineData(true)] - public void DataOffset_LongPath_LongLink_SecondEntry(bool canSeek) - { - string veryLongPathName = new string('a', 1234); // Forces using a GNU LongPath entry - string veryLongLinkName = new string('b', 1234); // Forces using a GNU LongLink entry - - using MemoryStream ms = new(); - using (TarWriter writer = new(ms, leaveOpen: true)) - { - GnuTarEntry entry1 = new GnuTarEntry(TarEntryType.SymbolicLink, veryLongPathName); - entry1.LinkName = veryLongLinkName; - writer.WriteEntry(entry1); - - GnuTarEntry entry2 = new GnuTarEntry(TarEntryType.SymbolicLink, veryLongPathName); - entry2.LinkName = veryLongLinkName; - writer.WriteEntry(entry2); - } - ms.Position = 0; - - using Stream streamToRead = new WrappedStream(ms, canWrite: true, canRead: true, canSeek: canSeek); - using TarReader reader = new(streamToRead); - TarEntry firstEntry = reader.GetNextEntry(); - Assert.NotNull(firstEntry); - // GNU first writes the long link and long path entries, containing: - // * 512 bytes of the regular long link tar header - // * 1234 bytes for the data section containing the full long link - // * 302 bytes of padding - // * 512 bytes of the regular long path tar header - // * 1234 bytes for the data section containing the full long path - // * 302 bytes of padding - // Then it writes the actual regular file entry, containing: - // * 512 bytes of the regular tar header - // Totalling 4608. - // The regular file data section starts on the next byte. - long firstExpectedDataOffset = canSeek ? 4608 : -1; - Assert.Equal(firstExpectedDataOffset, firstEntry.DataOffset); - - TarEntry secondEntry = reader.GetNextEntry(); - Assert.NotNull(secondEntry); - // First entry (including its long link and long path entries) end at 4608 (no padding, empty, as symlink has no data) - // Second entry (including its long link and long path entries) data section starts one byte after 4608 + 4608 = 9216 - long secondExpectedDataOffset = canSeek ? 9216 : -1; - Assert.Equal(secondExpectedDataOffset, secondEntry.DataOffset); - } + // Write it again, the offset should now point to the second written entry + // First entry 512 (header) + 1 (data) + 511 (padding) + // Second entry 512 (header) + // 512 + 512 + 512 = 1536 + await writer.WriteEntryAsync(entry); + Assert.Equal(1536, entry.DataOffset); + } - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task DataOffset_LongPath_LongLink_SecondEntry_Async(bool canSeek) - { - string veryLongPathName = new string('a', 1234); // Forces using a GNU LongPath entry - string veryLongLinkName = new string('b', 1234); // Forces using a GNU LongLink entry - - await using MemoryStream ms = new(); - await using (TarWriter writer = new(ms, leaveOpen: true)) - { - GnuTarEntry entry1 = new GnuTarEntry(TarEntryType.SymbolicLink, veryLongPathName); - entry1.LinkName = veryLongLinkName; - await writer.WriteEntryAsync(entry1); - - GnuTarEntry entry2 = new GnuTarEntry(TarEntryType.SymbolicLink, veryLongPathName); - entry2.LinkName = veryLongLinkName; - await writer.WriteEntryAsync(entry2); - } - ms.Position = 0; - - await using Stream streamToRead = new WrappedStream(ms, canWrite: true, canRead: true, canSeek: canSeek); - await using TarReader reader = new(streamToRead); - TarEntry firstEntry = await reader.GetNextEntryAsync(); - Assert.NotNull(firstEntry); - // GNU first writes the long link and long path entries, containing: - // * 512 bytes of the regular long link tar header - // * 1234 bytes for the data section containing the full long link - // * 302 bytes of padding - // * 512 bytes of the regular long path tar header - // * 1234 bytes for the data section containing the full long path - // * 302 bytes of padding - // Then it writes the actual regular file entry, containing: - // * 512 bytes of the regular tar header - // Totalling 4608. - // The regular file data section starts on the next byte. - long firstExpectedDataOffset = canSeek ? 4608 : -1; - Assert.Equal(firstExpectedDataOffset, firstEntry.DataOffset); - - TarEntry secondEntry = await reader.GetNextEntryAsync(); - Assert.NotNull(secondEntry); - // First entry (including its long link and long path entries) end at 4608 (no padding, empty, as symlink has no data) - // Second entry (including its long link and long path entries) data section starts one byte after 4608 + 4608 = 9216 - long secondExpectedDataOffset = canSeek ? 9216 : -1; - Assert.Equal(secondExpectedDataOffset, secondEntry.DataOffset); - } + [Fact] + public void DataOffset_UnseekableDataStream() + { + using MemoryStream ms = new(); + using (TarWriter writer = new(ms, leaveOpen: true)) + { + GnuTarEntry entry = new GnuTarEntry(TarEntryType.RegularFile, InitialEntryName); + Assert.Equal(-1, entry.DataOffset); - [Fact] - public void UnusedBytesInSizeFieldShouldBeZeroChars() - { - // The GNU format sets the unused bytes in the size field to "0" characters. + using MemoryStream dataStream = new(); + dataStream.WriteByte(ExpectedOffsetDataSingleByte); + dataStream.Position = 0; + using WrappedStream wds = new(dataStream, canWrite: true, canRead: true, canSeek: false); + entry.DataStream = wds; - using MemoryStream ms = new(); + writer.WriteEntry(entry); + } + ms.Position = 0; - using (TarWriter writer = new(ms, TarEntryFormat.Gnu, leaveOpen: true)) - { - // Start with a regular file entry without data - GnuTarEntry entry = new GnuTarEntry(TarEntryType.RegularFile, InitialEntryName); - writer.WriteEntry(entry); + using TarReader reader = new(ms); + TarEntry actualEntry = reader.GetNextEntry(); + Assert.NotNull(actualEntry); + // Gnu header length is 512, data starts in the next position + Assert.Equal(512, actualEntry.DataOffset); } - ms.Position = 0; - using (TarReader reader = new(ms, leaveOpen: true)) + [Fact] + public async Task DataOffset_UnseekableDataStream_Async() { - GnuTarEntry entry = reader.GetNextEntry() as GnuTarEntry; - Assert.NotNull(entry); - Assert.Equal(0, entry.Length); - } - ms.Position = 0; - ValidateUnusedBytesInSizeField(ms, 0); + await using MemoryStream ms = new(); + await using (TarWriter writer = new(ms, leaveOpen: true)) + { + GnuTarEntry entry = new GnuTarEntry(TarEntryType.RegularFile, InitialEntryName); + Assert.Equal(-1, entry.DataOffset); - ms.SetLength(0); // Reset the stream - byte[] data = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 }; // 8 bytes of data means a size of 10 in octal - using (TarWriter writer = new(ms, TarEntryFormat.Gnu, leaveOpen: true)) - { - // Start with a regular file entry with data - GnuTarEntry entry = new GnuTarEntry(TarEntryType.RegularFile, InitialEntryName); - entry.DataStream = new MemoryStream(data); - writer.WriteEntry(entry); + await using MemoryStream dataStream = new(); + dataStream.WriteByte(ExpectedOffsetDataSingleByte); + dataStream.Position = 0; + await using WrappedStream wds = new(dataStream, canWrite: true, canRead: true, canSeek: false); + entry.DataStream = wds; + + await writer.WriteEntryAsync(entry); + } + ms.Position = 0; + + await using TarReader reader = new(ms); + TarEntry actualEntry = await reader.GetNextEntryAsync(); + Assert.NotNull(actualEntry); + // Gnu header length is 512, data starts in the next position + Assert.Equal(512, actualEntry.DataOffset); } - ms.Position = 0; - using (TarReader reader = new(ms, leaveOpen: true)) + [Theory] + [InlineData(false)] + [InlineData(true)] + public void DataOffset_LongPath_LongLink_SecondEntry(bool canSeek) { - GnuTarEntry entry = reader.GetNextEntry() as GnuTarEntry; - Assert.NotNull(entry); - Assert.Equal(data.Length, entry.Length); - } - ms.Position = 0; - ValidateUnusedBytesInSizeField(ms, data.Length); - } + string veryLongPathName = new string('a', 1234); // Forces using a GNU LongPath entry + string veryLongLinkName = new string('b', 1234); // Forces using a GNU LongLink entry - [Fact] - public async Task UnusedBytesInSizeFieldShouldBeZeroChars_Async() - { - // The GNU format sets the unused bytes in the size field to "0" characters. + using MemoryStream ms = new(); + using (TarWriter writer = new(ms, leaveOpen: true)) + { + GnuTarEntry entry1 = new GnuTarEntry(TarEntryType.SymbolicLink, veryLongPathName); + entry1.LinkName = veryLongLinkName; + writer.WriteEntry(entry1); - await using MemoryStream ms = new(); + GnuTarEntry entry2 = new GnuTarEntry(TarEntryType.SymbolicLink, veryLongPathName); + entry2.LinkName = veryLongLinkName; + writer.WriteEntry(entry2); + } + ms.Position = 0; + + using Stream streamToRead = new WrappedStream(ms, canWrite: true, canRead: true, canSeek: canSeek); + using TarReader reader = new(streamToRead); + TarEntry firstEntry = reader.GetNextEntry(); + Assert.NotNull(firstEntry); + // GNU first writes the long link and long path entries, containing: + // * 512 bytes of the regular long link tar header + // * 1234 bytes for the data section containing the full long link + // * 302 bytes of padding + // * 512 bytes of the regular long path tar header + // * 1234 bytes for the data section containing the full long path + // * 302 bytes of padding + // Then it writes the actual regular file entry, containing: + // * 512 bytes of the regular tar header + // Totalling 4608. + // The regular file data section starts on the next byte. + long firstExpectedDataOffset = canSeek ? 4608 : -1; + Assert.Equal(firstExpectedDataOffset, firstEntry.DataOffset); + + TarEntry secondEntry = reader.GetNextEntry(); + Assert.NotNull(secondEntry); + // First entry (including its long link and long path entries) end at 4608 (no padding, empty, as symlink has no data) + // Second entry (including its long link and long path entries) data section starts one byte after 4608 + 4608 = 9216 + long secondExpectedDataOffset = canSeek ? 9216 : -1; + Assert.Equal(secondExpectedDataOffset, secondEntry.DataOffset); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task DataOffset_LongPath_LongLink_SecondEntry_Async(bool canSeek) + { + string veryLongPathName = new string('a', 1234); // Forces using a GNU LongPath entry + string veryLongLinkName = new string('b', 1234); // Forces using a GNU LongLink entry + + await using MemoryStream ms = new(); + await using (TarWriter writer = new(ms, leaveOpen: true)) + { + GnuTarEntry entry1 = new GnuTarEntry(TarEntryType.SymbolicLink, veryLongPathName); + entry1.LinkName = veryLongLinkName; + await writer.WriteEntryAsync(entry1); - await using (TarWriter writer = new(ms, TarEntryFormat.Gnu, leaveOpen: true)) - { - // Start with a regular file entry without data - GnuTarEntry entry = new GnuTarEntry(TarEntryType.RegularFile, InitialEntryName); - await writer.WriteEntryAsync(entry); - } - ms.Position = 0; + GnuTarEntry entry2 = new GnuTarEntry(TarEntryType.SymbolicLink, veryLongPathName); + entry2.LinkName = veryLongLinkName; + await writer.WriteEntryAsync(entry2); + } + ms.Position = 0; + + await using Stream streamToRead = new WrappedStream(ms, canWrite: true, canRead: true, canSeek: canSeek); + await using TarReader reader = new(streamToRead); + TarEntry firstEntry = await reader.GetNextEntryAsync(); + Assert.NotNull(firstEntry); + // GNU first writes the long link and long path entries, containing: + // * 512 bytes of the regular long link tar header + // * 1234 bytes for the data section containing the full long link + // * 302 bytes of padding + // * 512 bytes of the regular long path tar header + // * 1234 bytes for the data section containing the full long path + // * 302 bytes of padding + // Then it writes the actual regular file entry, containing: + // * 512 bytes of the regular tar header + // Totalling 4608. + // The regular file data section starts on the next byte. + long firstExpectedDataOffset = canSeek ? 4608 : -1; + Assert.Equal(firstExpectedDataOffset, firstEntry.DataOffset); + + TarEntry secondEntry = await reader.GetNextEntryAsync(); + Assert.NotNull(secondEntry); + // First entry (including its long link and long path entries) end at 4608 (no padding, empty, as symlink has no data) + // Second entry (including its long link and long path entries) data section starts one byte after 4608 + 4608 = 9216 + long secondExpectedDataOffset = canSeek ? 9216 : -1; + Assert.Equal(secondExpectedDataOffset, secondEntry.DataOffset); + } + + [Fact] + public void UnusedBytesInSizeFieldShouldBeZeroChars() + { + // The GNU format sets the unused bytes in the size field to "0" characters. + + using MemoryStream ms = new(); + + using (TarWriter writer = new(ms, TarEntryFormat.Gnu, leaveOpen: true)) + { + // Start with a regular file entry without data + GnuTarEntry entry = new GnuTarEntry(TarEntryType.RegularFile, InitialEntryName); + writer.WriteEntry(entry); + } + ms.Position = 0; - await using (TarReader reader = new(ms, leaveOpen: true)) - { - GnuTarEntry entry = await reader.GetNextEntryAsync() as GnuTarEntry; - Assert.NotNull(entry); - Assert.Equal(0, entry.Length); - } - ms.Position = 0; - ValidateUnusedBytesInSizeField(ms, 0); + using (TarReader reader = new(ms, leaveOpen: true)) + { + GnuTarEntry entry = reader.GetNextEntry() as GnuTarEntry; + Assert.NotNull(entry); + Assert.Equal(0, entry.Length); + } + ms.Position = 0; + ValidateUnusedBytesInSizeField(ms, 0); - ms.SetLength(0); // Reset the stream - byte[] data = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 }; // 8 bytes of data means a size of 10 in octal - await using (TarWriter writer = new(ms, TarEntryFormat.Gnu, leaveOpen: true)) - { - // Start with a regular file entry with data - GnuTarEntry entry = new GnuTarEntry(TarEntryType.RegularFile, InitialEntryName); - entry.DataStream = new MemoryStream(data); - await writer.WriteEntryAsync(entry); - } + ms.SetLength(0); // Reset the stream + byte[] data = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 }; // 8 bytes of data means a size of 10 in octal + using (TarWriter writer = new(ms, TarEntryFormat.Gnu, leaveOpen: true)) + { + // Start with a regular file entry with data + GnuTarEntry entry = new GnuTarEntry(TarEntryType.RegularFile, InitialEntryName); + entry.DataStream = new MemoryStream(data); + writer.WriteEntry(entry); + } - ms.Position = 0; - await using (TarReader reader = new(ms, leaveOpen: true)) - { - GnuTarEntry entry = await reader.GetNextEntryAsync() as GnuTarEntry; - Assert.NotNull(entry); - Assert.Equal(data.Length, entry.Length); + ms.Position = 0; + using (TarReader reader = new(ms, leaveOpen: true)) + { + GnuTarEntry entry = reader.GetNextEntry() as GnuTarEntry; + Assert.NotNull(entry); + Assert.Equal(data.Length, entry.Length); + } + ms.Position = 0; + ValidateUnusedBytesInSizeField(ms, data.Length); } - ms.Position = 0; - ValidateUnusedBytesInSizeField(ms, data.Length); - } - private void ValidateUnusedBytesInSizeField(MemoryStream ms, long size) - { - // internally, the unused bytes in the size field should be "0" characters, - // and the rest should be the octal value of the size field - - // name, mode, uid, gid, - // size - int sizeStart = 100 + 8 + 8 + 8; - byte[] buffer = new byte[12]; // The size field is 12 bytes in length - - ms.Seek(sizeStart, SeekOrigin.Begin); - ms.Read(buffer); - - // Convert the base 10 value of size to base 8 - string octalSize = Convert.ToString(size, 8).PadLeft(11, '0'); - byte[] octalSizeBytes = Encoding.ASCII.GetBytes(octalSize); - // The last byte should be a null character - Assert.Equal(octalSizeBytes, buffer.Take(octalSizeBytes.Length).ToArray()); - } - - public static IEnumerable NameAndLink_TestData() - { - // Name and link have a max length of 100. Anything longer goes into LongPath or LongLink entries in GNU. - yield return new object[] { InitialEntryName, InitialEntryName }; // Short name and short link - yield return new object[] { InitialEntryName, new string('b', 101) }; // Short name and long link - yield return new object[] { new string('a', 101), InitialEntryName }; // Long name and short link - yield return new object[] { new string('a', 101), new string('b', 101) }; // Long name and long link - } + [Fact] + public async Task UnusedBytesInSizeFieldShouldBeZeroChars_Async() + { + // The GNU format sets the unused bytes in the size field to "0" characters. - private GnuTarEntry CreateEntryForLongLinkLongPathChecks(string name, string linkName) - { - // A SymbolicLink entry can test both LongLink and LongPath entries if - // the length of either string is longer than what fits in the header. - return new GnuTarEntry(TarEntryType.SymbolicLink, name) - { - LinkName = linkName, - ModificationTime = DateTimeOffset.UnixEpoch, - AccessTime = DateTimeOffset.UnixEpoch, - ChangeTime = DateTimeOffset.UnixEpoch, - Uid = 0, - Gid = 0, - Mode = 0, - UserName = TestUName, - GroupName = TestGName - }; - } + await using MemoryStream ms = new(); - private void ValidateEntryForRegularEntryInLongLinkAndLongPathChecks(GnuTarEntry entry, string name, string linkName) - { - Assert.NotNull(entry); - Assert.Equal(DateTimeOffset.UnixEpoch, entry.ModificationTime); - Assert.Equal(DateTimeOffset.UnixEpoch, entry.AccessTime); - Assert.Equal(DateTimeOffset.UnixEpoch, entry.ChangeTime); - Assert.Equal(name, entry.Name); - Assert.Equal(linkName, entry.LinkName); - Assert.Equal(0, entry.Uid); // Should be '0' chars in the long header - Assert.Equal(0, entry.Gid); // Should be '0' chars in the long header - Assert.Equal(UnixFileMode.None, entry.Mode); // Should be '0' chars in the long header - Assert.Equal(TestUName, entry.UserName); - Assert.Equal(TestGName, entry.GroupName); - Assert.Equal(0, entry.Length); // No data in the main entry - } + await using (TarWriter writer = new(ms, TarEntryFormat.Gnu, leaveOpen: true)) + { + // Start with a regular file entry without data + GnuTarEntry entry = new GnuTarEntry(TarEntryType.RegularFile, InitialEntryName); + await writer.WriteEntryAsync(entry); + } + ms.Position = 0; - [Theory] - [MemberData(nameof(NameAndLink_TestData))] - public void Check_LongLink_AndLongPath_Metadata(string name, string linkName) - { - // The GNU format sets the mtime, atime and ctime to nulls in headers when they are set to the unix epoch. - // Also the uid and gid should be '0' in the long entries headers. - // Also the uname and gname in the long entry headers should be set to those of the main entry. + await using (TarReader reader = new(ms, leaveOpen: true)) + { + GnuTarEntry entry = await reader.GetNextEntryAsync() as GnuTarEntry; + Assert.NotNull(entry); + Assert.Equal(0, entry.Length); + } + ms.Position = 0; + ValidateUnusedBytesInSizeField(ms, 0); - using MemoryStream ms = new(); + ms.SetLength(0); // Reset the stream + byte[] data = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 }; // 8 bytes of data means a size of 10 in octal + await using (TarWriter writer = new(ms, TarEntryFormat.Gnu, leaveOpen: true)) + { + // Start with a regular file entry with data + GnuTarEntry entry = new GnuTarEntry(TarEntryType.RegularFile, InitialEntryName); + entry.DataStream = new MemoryStream(data); + await writer.WriteEntryAsync(entry); + } - using (TarWriter writer = new(ms, TarEntryFormat.Gnu, leaveOpen: true)) - { - GnuTarEntry entry = CreateEntryForLongLinkLongPathChecks(name, linkName); - writer.WriteEntry(entry); + ms.Position = 0; + await using (TarReader reader = new(ms, leaveOpen: true)) + { + GnuTarEntry entry = await reader.GetNextEntryAsync() as GnuTarEntry; + Assert.NotNull(entry); + Assert.Equal(data.Length, entry.Length); + } + ms.Position = 0; + ValidateUnusedBytesInSizeField(ms, data.Length); } - ms.Position = 0; - using (TarReader reader = new(ms, leaveOpen: true)) + private void ValidateUnusedBytesInSizeField(MemoryStream ms, long size) { - GnuTarEntry entry = reader.GetNextEntry() as GnuTarEntry; - ValidateEntryForRegularEntryInLongLinkAndLongPathChecks(entry, name, linkName); - } + // internally, the unused bytes in the size field should be "0" characters, + // and the rest should be the octal value of the size field - ValidateLongEntryBytes(ms, name, linkName); - } + // name, mode, uid, gid, + // size + int sizeStart = 100 + 8 + 8 + 8; + byte[] buffer = new byte[12]; // The size field is 12 bytes in length - [Theory] - [MemberData(nameof(NameAndLink_TestData))] - public async Task Check_LongLink_AndLongPath_Metadata_Async(string name, string linkName) - { - // The GNU format sets the mtime, atime and ctime to nulls in headers when they are set to the unix epoch. - // Also the uid and gid should be '0' in the long entries headers. - // Also the uname and gname in the long entry headers should be set to those of the main entry. + ms.Seek(sizeStart, SeekOrigin.Begin); + ms.Read(buffer); - await using MemoryStream ms = new(); + // Convert the base 10 value of size to base 8 + string octalSize = Convert.ToString(size, 8).PadLeft(11, '0'); + byte[] octalSizeBytes = Encoding.ASCII.GetBytes(octalSize); + // The last byte should be a null character + Assert.Equal(octalSizeBytes, buffer.Take(octalSizeBytes.Length).ToArray()); + } - await using (TarWriter writer = new(ms, TarEntryFormat.Gnu, leaveOpen: true)) + public static IEnumerable NameAndLink_TestData() { - GnuTarEntry entry = CreateEntryForLongLinkLongPathChecks(name, linkName); - await writer.WriteEntryAsync(entry); + // Name and link have a max length of 100. Anything longer goes into LongPath or LongLink entries in GNU. + yield return new object[] { InitialEntryName, InitialEntryName }; // Short name and short link + yield return new object[] { InitialEntryName, new string('b', 101) }; // Short name and long link + yield return new object[] { new string('a', 101), InitialEntryName }; // Long name and short link + yield return new object[] { new string('a', 101), new string('b', 101) }; // Long name and long link } - ms.Position = 0; - await using (TarReader reader = new(ms, leaveOpen: true)) + private GnuTarEntry CreateEntryForLongLinkLongPathChecks(string name, string linkName) { - GnuTarEntry entry = await reader.GetNextEntryAsync() as GnuTarEntry; - ValidateEntryForRegularEntryInLongLinkAndLongPathChecks(entry, name, linkName); + // A SymbolicLink entry can test both LongLink and LongPath entries if + // the length of either string is longer than what fits in the header. + return new GnuTarEntry(TarEntryType.SymbolicLink, name) + { + LinkName = linkName, + ModificationTime = TestModificationTime, + AccessTime = TestAccessTime, // This should only be set in the main entry + ChangeTime = TestChangeTime, // This should only be set in the main entry + Uid = TestUid, // This should only be set in the main entry + Gid = TestGid, // This should only be set in the main entry + Mode = TestMode, // This should only be set in the main entry + UserName = TestUName, // This should only be set in the main entry + GroupName = TestGName // This should only be set in the main entry + }; } - ValidateLongEntryBytes(ms, name, linkName); - } - - private void ValidateLongEntryBytes(MemoryStream ms, string name, string linkName) - { - bool isLongPath = name.Length >= 100; - bool isLongLink = linkName.Length >= 100; + private void ValidateEntryForRegularEntryInLongLinkAndLongPathChecks(GnuTarEntry entry, string name, string linkName) + { + Assert.NotNull(entry); + Assert.Equal(TestModificationTime, entry.ModificationTime); + Assert.Equal(TestAccessTime, entry.AccessTime); // This should be different in the long entry header + Assert.Equal(TestChangeTime, entry.ChangeTime); // This should be different in the long entry header + Assert.Equal(name, entry.Name); + Assert.Equal(linkName, entry.LinkName); + Assert.Equal(TestUid, entry.Uid); // This should be different in the long entry header + Assert.Equal(TestGid, entry.Gid); // This should be different in the long entry header + Assert.Equal(TestMode, entry.Mode); // This should be different in the long entry header + Assert.Equal(TestUName, entry.UserName); // This should be different in the long entry header + Assert.Equal(TestGName, entry.GroupName); // This should be different in the long entry header + Assert.Equal(0, entry.Length); // No data in the main entry + } + + [Theory] + [MemberData(nameof(NameAndLink_TestData))] + public void Check_LongLink_AndLongPath_Metadata(string name, string linkName) + { + // The GNU format sets the mtime, atime and ctime to nulls in headers when they are set to the unix epoch. + // Also the uid and gid should be '0' in the long entries headers. + // Also the uname and gname in the long entry headers should be set to those of the main entry. + + using MemoryStream ms = new(); + + using (TarWriter writer = new(ms, TarEntryFormat.Gnu, leaveOpen: true)) + { + GnuTarEntry entry = CreateEntryForLongLinkLongPathChecks(name, linkName); + writer.WriteEntry(entry); + } + ms.Position = 0; - ms.Position = 0; + using (TarReader reader = new(ms, leaveOpen: true)) + { + GnuTarEntry entry = reader.GetNextEntry() as GnuTarEntry; + ValidateEntryForRegularEntryInLongLinkAndLongPathChecks(entry, name, linkName); + } - long nextEntryStart = 0; - long reportedSize; + ValidateLongEntryBytes(ms, name, linkName); + } - if (isLongLink) + [Theory] + [MemberData(nameof(NameAndLink_TestData))] + public async Task Check_LongLink_AndLongPath_Metadata_Async(string name, string linkName) { - reportedSize = CheckHeaderMetadataAndGetReportedSize(ms, nextEntryStart, isLongLinkOrLongPath: true); - CheckDataContainsExpectedString(ms, nextEntryStart + 512, reportedSize, linkName, shouldTrim: false); // Skip to the data section - nextEntryStart += 512 + 512; // Skip the current header and the long link entry - Assert.True(linkName.Length < 512, "Do not test paths longer than a 512 byte block"); + // The GNU format sets the mtime, atime and ctime to nulls in headers when they are set to MinValue + // Also the uid and gid should be '0' in the long entries headers, and uname and gname in the long entry headers should be set to root. + + await using MemoryStream ms = new(); + + await using (TarWriter writer = new(ms, TarEntryFormat.Gnu, leaveOpen: true)) + { + GnuTarEntry entry = CreateEntryForLongLinkLongPathChecks(name, linkName); + await writer.WriteEntryAsync(entry); + } + ms.Position = 0; + + await using (TarReader reader = new(ms, leaveOpen: true)) + { + GnuTarEntry entry = await reader.GetNextEntryAsync() as GnuTarEntry; + ValidateEntryForRegularEntryInLongLinkAndLongPathChecks(entry, name, linkName); + } + + ValidateLongEntryBytes(ms, name, linkName); } - if (isLongPath) + private void ValidateLongEntryBytes(MemoryStream ms, string name, string linkName) { - reportedSize = CheckHeaderMetadataAndGetReportedSize(ms, nextEntryStart, isLongLinkOrLongPath: true); - CheckDataContainsExpectedString(ms, nextEntryStart + 512, reportedSize, name, shouldTrim: false); // Skip to the data section - nextEntryStart += 512 + 512; // Skip the current header and the long path entry - Assert.True(name.Length < 512, "Do not test paths longer than a 512 byte block"); - } + bool isLongPath = name.Length >= 100; + bool isLongLink = linkName.Length >= 100; - CheckHeaderMetadataAndGetReportedSize(ms, nextEntryStart, isLongLinkOrLongPath: false); - } + ms.Position = 0; - private long CheckHeaderMetadataAndGetReportedSize(MemoryStream ms, long nextEntryStart, bool isLongLinkOrLongPath) - { - // internally, mtime, atime and ctime should be nulls - // and if the entry is a long path or long link, the entry's data length should be - // equal to the string plus a null character - - // name mode uid gid size mtime checksum typeflag linkname magic uname gname devmajor devminor atime ctime - // 100 8 8 8 12 12 8 1 100 8 32 32 8 8 12 12 - long nameStart = nextEntryStart; - long modeStart = nameStart + 100; - long uidStart = modeStart + 8; - long gidStart = uidStart + 8; - long sizeStart = gidStart + 8; - long mTimeStart = sizeStart + 12; - long checksumStart = mTimeStart + 12; - long typeflagStart = checksumStart + 8; - long linkNameStart = typeflagStart + 1; - long magicStart = linkNameStart + 100; - long uNameStart = magicStart + 8; - long gNameStart = uNameStart + 32; - long devMajorStart = gNameStart + 32; - long devMinorStart = devMajorStart + 8; - long aTimeStart = devMinorStart + 8; - long cTimeStart = aTimeStart + 12; - - byte[] buffer = new byte[12]; // size, mtime, atime, ctime all are 12 bytes in length - - if (isLongLinkOrLongPath) - { - CheckBytesAreZeros(ms, buffer.AsSpan(0, 8), uidStart); - CheckBytesAreZeros(ms, buffer.AsSpan(0, 8), gidStart); - } - else - { - CheckBytesAreZeros(ms, buffer.AsSpan(0, 8), modeStart); - } - CheckBytesAreZeros(ms, buffer, mTimeStart); - CheckBytesAreZeros(ms, buffer, aTimeStart); - CheckBytesAreZeros(ms, buffer, cTimeStart); - CheckDataContainsExpectedString(ms, uNameStart, 32, TestUName, shouldTrim: true); - CheckDataContainsExpectedString(ms, gNameStart, 32, TestGName, shouldTrim: true); - - ms.Seek(sizeStart, SeekOrigin.Begin); - ms.Read(buffer); - return ParseNumeric(buffer); - } + long nextEntryStart = 0; + long reportedSize; - private void CheckBytesAreSpecificChar(MemoryStream ms, Span buffer, long dataStart, byte charToCheck) - { - ms.Seek(dataStart, SeekOrigin.Begin); - ms.Read(buffer); - foreach (byte b in buffer.Slice(0, buffer.Length - 1)) // The last byte should be a null character - { - Assert.Equal(charToCheck, b); - } - Assert.Equal(0, buffer[^1]); // The last byte should be a null character - } + if (isLongLink) + { + reportedSize = CheckHeaderMetadataAndGetReportedSize(ms, nextEntryStart, isLongLinkOrLongPath: true); + CheckDataContainsExpectedString(ms, nextEntryStart + 512, reportedSize, linkName, shouldTrim: false); // Skip to the data section + nextEntryStart += 512 + 512; // Skip the current header and the long link entry + Assert.True(linkName.Length < 512, "Do not test paths longer than a 512 byte block"); + } - private void CheckBytesAreNulls(MemoryStream ms, Span buffer, long dataStart) => CheckBytesAreSpecificChar(ms, buffer, dataStart, 0); // null char + if (isLongPath) + { + reportedSize = CheckHeaderMetadataAndGetReportedSize(ms, nextEntryStart, isLongLinkOrLongPath: true); + CheckDataContainsExpectedString(ms, nextEntryStart + 512, reportedSize, name, shouldTrim: false); // Skip to the data section + nextEntryStart += 512 + 512; // Skip the current header and the long path entry + Assert.True(name.Length < 512, "Do not test paths longer than a 512 byte block"); + } - private void CheckBytesAreZeros(MemoryStream ms, Span buffer, long dataStart) => CheckBytesAreSpecificChar(ms, buffer, dataStart, 0x30); // '0' char + CheckHeaderMetadataAndGetReportedSize(ms, nextEntryStart, isLongLinkOrLongPath: false); + } + + private long CheckHeaderMetadataAndGetReportedSize(MemoryStream ms, long nextEntryStart, bool isLongLinkOrLongPath) + { + // internally, mtime, atime and ctime should be nulls + // and if the entry is a long path or long link, the entry's data length should be + // equal to the string plus a null character + + // name mode uid gid size mtime checksum typeflag linkname magic uname gname devmajor devminor atime ctime + // 100 8 8 8 12 12 8 1 100 8 32 32 8 8 12 12 + long nameStart = nextEntryStart; + long modeStart = nameStart + 100; + long uidStart = modeStart + 8; + long gidStart = uidStart + 8; + long sizeStart = gidStart + 8; + long mTimeStart = sizeStart + 12; + long checksumStart = mTimeStart + 12; + long typeflagStart = checksumStart + 8; + long linkNameStart = typeflagStart + 1; + long magicStart = linkNameStart + 100; + long uNameStart = magicStart + 8; + long gNameStart = uNameStart + 32; + long devMajorStart = gNameStart + 32; + long devMinorStart = devMajorStart + 8; + long aTimeStart = devMinorStart + 8; + long cTimeStart = aTimeStart + 12; + + Span buffer = stackalloc byte[12]; // size, mtime, atime, ctime all are 12 bytes in length (max length to check) + + if (isLongLinkOrLongPath) + { + CheckBytesAreNulls(ms, buffer, aTimeStart); // no atime + CheckBytesAreNulls(ms, buffer, cTimeStart); // no ctime + CheckBytesAreZeros(ms, buffer.Slice(0, 8), uidStart); // uid 0 + CheckBytesAreZeros(ms, buffer.Slice(0, 8), gidStart); // uid 0 + Span expectedOctalModeBytes = Encoding.ASCII.GetBytes("0000644\0"); // 644 is the default mode set in LongLink/LongPath + CheckBytesAreSpecificSequence(ms, buffer.Slice(0, 8), modeStart, expectedOctalModeBytes); + CheckDataContainsExpectedString(ms, uNameStart, 32, RootUNameGName, shouldTrim: true); + CheckDataContainsExpectedString(ms, gNameStart, 32, RootUNameGName, shouldTrim: true); + CheckBytesAreZeros(ms, buffer, mTimeStart); + } + else + { + Span expectedOctalUidBytes = Encoding.ASCII.GetBytes(Convert.ToString(TestUid, 8).PadLeft(7, '0') + '\0'); + Span expectedOctalGidBytes = Encoding.ASCII.GetBytes(Convert.ToString(TestGid, 8).PadLeft(7, '0') + '\0'); + Span expectedOctalModeBytes = Encoding.ASCII.GetBytes(Convert.ToString((int)TestMode, 8).PadLeft(7, '0') + '\0'); + CheckBytesAreSpecificSequence(ms, buffer.Slice(0, 8), uidStart, expectedOctalUidBytes); + CheckBytesAreSpecificSequence(ms, buffer.Slice(0, 8), gidStart, expectedOctalGidBytes); + CheckBytesAreSpecificSequence(ms, buffer.Slice(0, 8), modeStart, expectedOctalModeBytes); + CheckDataContainsExpectedString(ms, uNameStart, 32, TestUName, shouldTrim: true); + CheckDataContainsExpectedString(ms, gNameStart, 32, TestGName, shouldTrim: true); + } - private void CheckDataContainsExpectedString(MemoryStream ms, long dataStart, long actualDataLength, string expectedString, bool shouldTrim) - { - ms.Seek(dataStart, SeekOrigin.Begin); - byte[] buffer = new byte[actualDataLength]; - ms.Read(buffer); + ms.Seek(sizeStart, SeekOrigin.Begin); + ms.Read(buffer); + return ParseNumeric(buffer); + } - if (shouldTrim) + private void CheckBytesAreSpecificChar(MemoryStream ms, Span buffer, long dataStart, byte charToCheck) { - string actualString = Encoding.ASCII.GetString(TrimEndingNullsAndSpaces(buffer)); - Assert.Equal(expectedString, actualString); + ms.Seek(dataStart, SeekOrigin.Begin); + ms.Read(buffer); + Span expectedSequence = stackalloc byte[buffer.Length - 1]; + expectedSequence.Fill(charToCheck); + AssertExtensions.SequenceEqual(expectedSequence, buffer.Slice(0, buffer.Length - 1)); + Assert.Equal(0, buffer[^1]); // The last byte should be a null character } - else + + private void CheckBytesAreSpecificSequence(MemoryStream ms, Span buffer, long dataStart, ReadOnlySpan expectedSequence) { - string actualString = Encoding.ASCII.GetString(buffer); - Assert.Equal(expectedString, actualString[..^1]); // The last byte should be a null character + ms.Seek(dataStart, SeekOrigin.Begin); + ms.Read(buffer); + Assert.Equal(expectedSequence.Length, buffer.Length); + AssertExtensions.SequenceEqual(expectedSequence, buffer.Slice(0, expectedSequence.Length)); + Assert.Equal(0, buffer[expectedSequence.Length - 1]); // The last byte should be a null character } - } - private static T ParseNumeric(ReadOnlySpan buffer) where T : struct, INumber, IBinaryInteger - { - byte leadingByte = buffer[0]; - if (leadingByte == 0xff) + private void CheckBytesAreNulls(MemoryStream ms, Span buffer, long dataStart) => CheckBytesAreSpecificChar(ms, buffer, dataStart, 0); // null char + + private void CheckBytesAreZeros(MemoryStream ms, Span buffer, long dataStart) => CheckBytesAreSpecificChar(ms, buffer, dataStart, 0x30); // '0' char + + private void CheckDataContainsExpectedString(MemoryStream ms, long dataStart, long actualDataLength, string expectedString, bool shouldTrim) { - return T.ReadBigEndian(buffer, isUnsigned: false); + ms.Seek(dataStart, SeekOrigin.Begin); + byte[] buffer = new byte[actualDataLength]; + ms.Read(buffer); + + if (shouldTrim) + { + string actualString = Encoding.ASCII.GetString(TrimEndingNullsAndSpaces(buffer)); + Assert.Equal(expectedString, actualString); + } + else + { + string actualString = Encoding.ASCII.GetString(buffer); + Assert.Equal(expectedString, actualString[..^1]); // The last byte should be a null character + } } - else if (leadingByte == 0x80) + + private static T ParseNumeric(ReadOnlySpan buffer) where T : struct, INumber, IBinaryInteger { - return T.ReadBigEndian(buffer.Slice(1), isUnsigned: true); + byte leadingByte = buffer[0]; + if (leadingByte == 0xff) + { + return T.ReadBigEndian(buffer, isUnsigned: false); + } + else if (leadingByte == 0x80) + { + return T.ReadBigEndian(buffer.Slice(1), isUnsigned: true); + } + else + { + return ParseOctal(buffer); + } } - else + + private static T ParseOctal(ReadOnlySpan buffer) where T : struct, INumber { - return ParseOctal(buffer); - } - } + buffer = TrimEndingNullsAndSpaces(buffer); + buffer = TrimLeadingNullsAndSpaces(buffer); - private static T ParseOctal(ReadOnlySpan buffer) where T : struct, INumber - { - buffer = TrimEndingNullsAndSpaces(buffer); - buffer = TrimLeadingNullsAndSpaces(buffer); + if (buffer.Length == 0) + { + return T.Zero; + } - if (buffer.Length == 0) - { - return T.Zero; + T octalFactor = T.CreateTruncating(8u); + T value = T.Zero; + foreach (byte b in buffer) + { + uint digit = (uint)(b - '0'); + if (digit >= 8) + { + throw new InvalidDataException(SR.Format(SR.TarInvalidNumber)); + } + + value = checked((value * octalFactor) + T.CreateTruncating(digit)); + } + + return value; } - T octalFactor = T.CreateTruncating(8u); - T value = T.Zero; - foreach (byte b in buffer) + private static ReadOnlySpan TrimEndingNullsAndSpaces(ReadOnlySpan buffer) { - uint digit = (uint)(b - '0'); - if (digit >= 8) + int trimmedLength = buffer.Length; + while (trimmedLength > 0 && buffer[trimmedLength - 1] is 0 or 32) { - throw new InvalidDataException(SR.Format(SR.TarInvalidNumber)); + trimmedLength--; } - value = checked((value * octalFactor) + T.CreateTruncating(digit)); + return buffer.Slice(0, trimmedLength); } - return value; - } - - private static ReadOnlySpan TrimEndingNullsAndSpaces(ReadOnlySpan buffer) - { - int trimmedLength = buffer.Length; - while (trimmedLength > 0 && buffer[trimmedLength - 1] is 0 or 32) + private static ReadOnlySpan TrimLeadingNullsAndSpaces(ReadOnlySpan buffer) { - trimmedLength--; - } - - return buffer.Slice(0, trimmedLength); - } + int newStart = 0; + while (newStart < buffer.Length && buffer[newStart] is 0 or 32) + { + newStart++; + } - private static ReadOnlySpan TrimLeadingNullsAndSpaces(ReadOnlySpan buffer) - { - int newStart = 0; - while (newStart < buffer.Length && buffer[newStart] is 0 or 32) - { - newStart++; + return buffer.Slice(newStart); } - - return buffer.Slice(newStart); } } -} diff --git a/src/libraries/System.Formats.Tar/tests/TarEntry/PaxTarEntry.Conversion.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarEntry/PaxTarEntry.Conversion.Tests.cs index e42f1df0ea6ea1..eb80726dbe063c 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 @@ -77,6 +77,8 @@ public void Constructor_ConversionFromPaxGEA_ToAny_Throw() [InlineData(TarEntryFormat.Gnu)] public void Constructor_ConversionFromV7_Write(TarEntryFormat originalEntryFormat) { + DateTimeOffset initialNow = DateTimeOffset.UtcNow; + string name = "file.txt"; string contents = "Hello world"; @@ -90,22 +92,28 @@ public void Constructor_ConversionFromV7_Write(TarEntryFormat originalEntryForma dataStream.Position = 0; originalEntry.DataStream = dataStream; - DateTimeOffset expectedATime; - DateTimeOffset expectedCTime; + DateTimeOffset expectedATime = default; + DateTimeOffset expectedCTime = default; - if (originalEntryFormat is TarEntryFormat.Pax or TarEntryFormat.Gnu) + if (originalEntry is GnuTarEntry gnuEntry) { - // The constructor should've set the atime and ctime automatically to the same value of mtime - expectedATime = originalEntry.ModificationTime; - expectedCTime = originalEntry.ModificationTime; + Assert.Equal(default, gnuEntry.AccessTime); + Assert.Equal(default, gnuEntry.ChangeTime); + // Change them to mtime + gnuEntry.AccessTime = gnuEntry.ModificationTime; + gnuEntry.ChangeTime = gnuEntry.ModificationTime; + + expectedATime = gnuEntry.ModificationTime; + expectedCTime = gnuEntry.ModificationTime; } - else + else if (originalEntry is PaxTarEntry paxEntry) { - // 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; + expectedATime = GetDateTimeOffsetFromTimestampString(paxEntry.ExtendedAttributes, PaxEaATime); + expectedCTime = GetDateTimeOffsetFromTimestampString(paxEntry.ExtendedAttributes, PaxEaCTime); + + Assert.Equal(paxEntry.ModificationTime, expectedATime); + Assert.Equal(paxEntry.ModificationTime, expectedCTime); + // Can't change them, it's a read-only dictionary } TarEntry convertedEntry = InvokeTarEntryConversionConstructor(TarEntryFormat.Pax, originalEntry); @@ -145,11 +153,6 @@ public void Constructor_ConversionFromV7_Write(TarEntryFormat originalEntryForma Assert.Equal(expectedATime, atime); Assert.Equal(expectedCTime, ctime); } - else - { - AssertExtensions.GreaterThanOrEqualTo(atime, expectedATime); - AssertExtensions.GreaterThanOrEqualTo(ctime, expectedCTime); - } } } 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 9fa5e57d460c19..fef2da357e8234 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 @@ -1,20 +1,15 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; -using System.Collections.Generic; using System.IO; -using System.Linq; -using System.Linq.Expressions; -using System.Text; -using System.Threading.Tasks; -using Microsoft.Diagnostics.Runtime.ICorDebug; using Xunit; namespace System.Formats.Tar.Tests { public class TarTestsConversionBase : TarTestsBase { + private readonly TimeSpan _oneSecond = TimeSpan.FromSeconds(1); + protected void TestConstructionConversion( TarEntryType originalEntryType, TarEntryFormat firstFormat, @@ -82,8 +77,8 @@ private TarEntry GetFirstEntry(MemoryStream dataStream, TarEntryType entryType, else if (format is TarEntryFormat.Gnu) { GnuTarEntry gnuEntry = firstEntry as GnuTarEntry; - Assert.Equal(firstEntry.ModificationTime, gnuEntry.AccessTime); - Assert.Equal(firstEntry.ModificationTime, gnuEntry.ChangeTime); + Assert.Equal(default, gnuEntry.AccessTime); + Assert.Equal(default, gnuEntry.ChangeTime); } return firstEntry; @@ -123,16 +118,54 @@ private TarEntry ConvertAndVerifyEntry(TarEntry originalEntry, TarEntryType entr if (formatToConvert is TarEntryFormat.Pax) { PaxTarEntry paxEntry = convertedEntry as PaxTarEntry; - DateTimeOffset actualAccessTime = GetDateTimeOffsetFromTimestampString(paxEntry.ExtendedAttributes, PaxEaATime); - DateTimeOffset actualChangeTime = GetDateTimeOffsetFromTimestampString(paxEntry.ExtendedAttributes, PaxEaCTime); - if (originalEntry.Format is TarEntryFormat.Pax or TarEntryFormat.Gnu) + if (originalEntry.Format is TarEntryFormat.Gnu) + { + GnuTarEntry gnuEntry = originalEntry as GnuTarEntry; + + DateTimeOffset expectedATime = gnuEntry.AccessTime; + DateTimeOffset expectedCTime = gnuEntry.ChangeTime; + + DateTimeOffset actualAccessTime = GetDateTimeOffsetFromTimestampString(paxEntry.ExtendedAttributes, PaxEaATime); + DateTimeOffset actualChangeTime = GetDateTimeOffsetFromTimestampString(paxEntry.ExtendedAttributes, PaxEaCTime); + + if (expectedATime == default) + { + AssertExtensions.GreaterThanOrEqualTo(actualAccessTime, paxEntry.ModificationTime); + } + else + { + expectedATime = expectedATime - _oneSecond; + AssertExtensions.GreaterThanOrEqualTo(expectedATime, actualAccessTime); + } + + if (expectedCTime == default) + { + AssertExtensions.GreaterThanOrEqualTo(actualChangeTime, paxEntry.ModificationTime); + } + else + { + expectedCTime = expectedCTime - _oneSecond; + AssertExtensions.GreaterThanOrEqualTo(expectedCTime, actualChangeTime); + } + } + else if (originalEntry.Format is TarEntryFormat.Pax) { - GetExpectedTimestampsFromOriginalPaxOrGnu(originalEntry, out DateTimeOffset expectedATime, out DateTimeOffset expectedCTime); - Assert.Equal(expectedATime, actualAccessTime); - Assert.Equal(expectedCTime, actualChangeTime); + PaxTarEntry originalPaxEntry = originalEntry as PaxTarEntry; + + DateTimeOffset expectedATime = GetDateTimeOffsetFromTimestampString(originalPaxEntry.ExtendedAttributes, PaxEaATime) - _oneSecond; + DateTimeOffset expectedCTime = GetDateTimeOffsetFromTimestampString(originalPaxEntry.ExtendedAttributes, PaxEaCTime) - _oneSecond; + + DateTimeOffset actualAccessTime = GetDateTimeOffsetFromTimestampString(paxEntry.ExtendedAttributes, PaxEaATime); + DateTimeOffset actualChangeTime = GetDateTimeOffsetFromTimestampString(paxEntry.ExtendedAttributes, PaxEaCTime); + + AssertExtensions.GreaterThanOrEqualTo(actualAccessTime, expectedATime); + AssertExtensions.GreaterThanOrEqualTo(actualChangeTime, expectedCTime); } else if (originalEntry.Format is TarEntryFormat.Ustar or TarEntryFormat.V7) { + DateTimeOffset actualAccessTime = GetDateTimeOffsetFromTimestampString(paxEntry.ExtendedAttributes, PaxEaATime); + DateTimeOffset actualChangeTime = GetDateTimeOffsetFromTimestampString(paxEntry.ExtendedAttributes, PaxEaCTime); + AssertExtensions.GreaterThanOrEqualTo(actualAccessTime, initialNow); AssertExtensions.GreaterThanOrEqualTo(actualChangeTime, initialNow); } @@ -143,30 +176,31 @@ private TarEntry ConvertAndVerifyEntry(TarEntry originalEntry, TarEntryType entr GnuTarEntry gnuEntry = convertedEntry as GnuTarEntry; if (originalEntry.Format is TarEntryFormat.Pax or TarEntryFormat.Gnu) { - GetExpectedTimestampsFromOriginalPaxOrGnu(originalEntry, out DateTimeOffset expectedATime, out DateTimeOffset expectedCTime); + GetExpectedTimestampsFromOriginalPaxOrGnu(originalEntry, formatToConvert, out DateTimeOffset expectedATime, out DateTimeOffset expectedCTime); AssertExtensions.GreaterThanOrEqualTo(gnuEntry.AccessTime, expectedATime); AssertExtensions.GreaterThanOrEqualTo(gnuEntry.ChangeTime, expectedCTime); } else if (originalEntry.Format is TarEntryFormat.Ustar or TarEntryFormat.V7) { - AssertExtensions.GreaterThanOrEqualTo(gnuEntry.AccessTime, initialNow); - AssertExtensions.GreaterThanOrEqualTo(gnuEntry.ChangeTime, initialNow); + Assert.Equal(default, gnuEntry.AccessTime); + Assert.Equal(default, gnuEntry.ChangeTime); } } return convertedEntry; } - private void GetExpectedTimestampsFromOriginalPaxOrGnu(TarEntry originalEntry, out DateTimeOffset expectedATime, out DateTimeOffset expectedCTime) + private void GetExpectedTimestampsFromOriginalPaxOrGnu(TarEntry originalEntry, TarEntryFormat formatToConvert, out DateTimeOffset expectedATime, out DateTimeOffset expectedCTime) { Assert.True(originalEntry.Format is TarEntryFormat.Gnu or TarEntryFormat.Pax); if (originalEntry.Format is TarEntryFormat.Pax) { PaxTarEntry originalPaxEntry = originalEntry as PaxTarEntry; - Assert.Contains("atime", originalPaxEntry.ExtendedAttributes); - Assert.Contains("ctime", originalPaxEntry.ExtendedAttributes); - expectedATime = GetDateTimeOffsetFromTimestampString(originalPaxEntry.ExtendedAttributes, "atime"); - expectedCTime = GetDateTimeOffsetFromTimestampString(originalPaxEntry.ExtendedAttributes, "ctime"); + Assert.Contains("atime", originalPaxEntry.ExtendedAttributes); // We are verifying that the original had an atime and ctime set + Assert.Contains("ctime", originalPaxEntry.ExtendedAttributes); // and that when converting to GNU we are _not_ preserving them + // And that instead, we are setting them to MinValue + expectedATime = formatToConvert is TarEntryFormat.Gnu ? default : GetDateTimeOffsetFromTimestampString(originalPaxEntry.ExtendedAttributes, "atime"); + expectedCTime = formatToConvert is TarEntryFormat.Gnu ? default : GetDateTimeOffsetFromTimestampString(originalPaxEntry.ExtendedAttributes, "ctime"); } else { diff --git a/src/libraries/System.Formats.Tar/tests/TarTestsBase.Gnu.cs b/src/libraries/System.Formats.Tar/tests/TarTestsBase.Gnu.cs index 325c6c916f6058..96cfabac17d4d1 100644 --- a/src/libraries/System.Formats.Tar/tests/TarTestsBase.Gnu.cs +++ b/src/libraries/System.Formats.Tar/tests/TarTestsBase.Gnu.cs @@ -56,33 +56,10 @@ protected void SetFifo(GnuTarEntry fifo) protected void SetGnuProperties(GnuTarEntry entry) { - // The octal format limits the representable range. - bool formatIsOctalOnly = entry.Format is not TarEntryFormat.Pax and not TarEntryFormat.Gnu; - - DateTimeOffset approxNow = DateTimeOffset.UtcNow.Subtract(TimeSpan.FromHours(6)); - - // ATime: Verify the default value was approximately "now" - Assert.True(entry.AccessTime > approxNow); - if (formatIsOctalOnly) - { - Assert.Throws(() => entry.AccessTime = DateTimeOffset.MinValue); - } - else - { - entry.AccessTime = DateTimeOffset.MinValue; - } + Assert.Equal(default, entry.AccessTime); entry.AccessTime = TestAccessTime; - // CTime: Verify the default value was approximately "now" - Assert.True(entry.ChangeTime > approxNow); - if (formatIsOctalOnly) - { - Assert.Throws(() => entry.ChangeTime = DateTimeOffset.MinValue); - } - else - { - entry.ChangeTime = DateTimeOffset.MinValue; - } + Assert.Equal(default, entry.ChangeTime); entry.ChangeTime = TestChangeTime; } @@ -136,8 +113,8 @@ protected void VerifyGnuProperties(GnuTarEntry entry) protected void VerifyGnuTimestamps(GnuTarEntry gnu) { - AssertExtensions.GreaterThanOrEqualTo(gnu.AccessTime, DateTimeOffset.UnixEpoch); - AssertExtensions.GreaterThanOrEqualTo(gnu.ChangeTime, DateTimeOffset.UnixEpoch); + Assert.Equal(default, gnu.AccessTime); + Assert.Equal(default, gnu.ChangeTime); } } } diff --git a/src/libraries/System.Formats.Tar/tests/TarTestsBase.Pax.cs b/src/libraries/System.Formats.Tar/tests/TarTestsBase.Pax.cs index c1776753512cfd..adfbea8fdaa02b 100644 --- a/src/libraries/System.Formats.Tar/tests/TarTestsBase.Pax.cs +++ b/src/libraries/System.Formats.Tar/tests/TarTestsBase.Pax.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.Globalization; using System.IO; -using System.Runtime.CompilerServices; using Xunit; namespace System.Formats.Tar.Tests diff --git a/src/libraries/System.Formats.Tar/tests/TarTestsBase.cs b/src/libraries/System.Formats.Tar/tests/TarTestsBase.cs index 5412d03f6f29a5..e8df0e4a9a3482 100644 --- a/src/libraries/System.Formats.Tar/tests/TarTestsBase.cs +++ b/src/libraries/System.Formats.Tar/tests/TarTestsBase.cs @@ -60,6 +60,8 @@ public abstract partial class TarTestsBase : FileCleanupTestBase protected const string TestGName = "group"; protected const string TestUName = "user"; + protected const int RootUidGid = 0; + protected const string RootUNameGName = "root"; // The metadata of the entries inside the asset archives are all set to these values protected const int AssetGid = 3579; @@ -255,7 +257,7 @@ protected static string GetStrangeTarFilePath(string testCaseName) => protected static MemoryStream GetStrangeTarMemoryStream(string testCaseName) => GetMemoryStream(GetStrangeTarFilePath(testCaseName)); - private static MemoryStream GetMemoryStream(string path) + protected static MemoryStream GetMemoryStream(string path) { MemoryStream ms = new(); using (FileStream fs = File.OpenRead(path)) @@ -340,7 +342,7 @@ protected void SetCommonProperties(TarEntry entry, bool isDirectory = false) } else { - entry.ModificationTime = DateTimeOffset.MinValue; + entry.ModificationTime = default; } entry.ModificationTime = TestModificationTime; @@ -724,7 +726,7 @@ internal static IEnumerable GetNamesNonAsciiTestData(NameCapabilities ma // this is 256 but is supported because prefix is not required to end in separator. yield return Repeat(OneByteCharacter, 155) + Separator + Repeat(OneByteCharacter, 100); - // non-ascii prefix + name + // non-ascii prefix + name yield return Repeat(TwoBytesCharacter, 155 / 2) + Separator + Repeat(OneByteCharacter, 100); yield return Repeat(FourBytesCharacter, 155 / 4) + Separator + Repeat(OneByteCharacter, 100); @@ -798,6 +800,26 @@ internal static IEnumerable GetTooLongNamesTestData(NameCapabilities max } } + internal static int GetChecksum(byte[] value) + { + int count = 0; + foreach (byte c in value) + { + count += c; + } + return count; + } + + internal static int GetChecksum(string value) + { + int count = 0; + foreach (char c in value) + { + count += c; + } + return count; + } + internal static string Repeat(char c, int count) { return new string(c, count); diff --git a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.Tests.cs index 1dbd3fc9fdf7bb..647160421dffa9 100644 --- a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.Tests.cs @@ -10,7 +10,6 @@ namespace System.Formats.Tar.Tests public partial class TarWriter_Tests : TarTestsBase { private readonly DateTimeOffset TimestampForChecksum = new DateTimeOffset(2022, 1, 2, 3, 45, 00, TimeSpan.Zero); - private readonly DateTimeOffset UnixEpochTimestampForChecksum = DateTimeOffset.UnixEpoch; [Fact] public void Constructors_NullStream() @@ -289,24 +288,26 @@ private int GetNameChecksum(TarEntryFormat format, bool longPath, out string ent { // 'a.b' = 97 + 46 + 98 = 241 entryName = "a.b"; - expectedChecksum += 241; + expectedChecksum += GetChecksum(entryName); } else { - entryName = new string('a', 100); - expectedChecksum += 9700; // 100 * 97 = 9700 (first 100 bytes go into 'name' field) + entryName = new string('a', 100); // 100 * 97 = 9700 (first 100 bytes go into 'name' field) + expectedChecksum += GetChecksum(entryName); // V7 does not support name fields larger than 100 + string extraNameForPrefix = string.Empty; if (format is not TarEntryFormat.V7) { - entryName += "/" + new string('a', 50); + extraNameForPrefix = new string('a', 50); // 50 * 97 = 4850 + entryName += "/" + extraNameForPrefix; } // Gnu and Pax writes first 100 bytes in 'name' field, then the full name is written in a metadata entry that precedes this one. if (format is TarEntryFormat.Ustar) { - // Ustar can write the directory into prefix. - expectedChecksum += 4850; // 50 * 97 = 4850 + // Ustar can write the directory into prefix (separator excluded). + expectedChecksum += GetChecksum(extraNameForPrefix); } } return expectedChecksum; @@ -319,16 +320,17 @@ private int GetLinkChecksum(TarEntryFormat format, bool longLink, out string lin { // 'a.b' = 97 + 46 + 98 = 241 linkName = "a.b"; - expectedChecksum += 241; + expectedChecksum += GetChecksum(linkName); } else { linkName = new string('a', 100); // 100 * 97 = 9700 (first 100 bytes go into 'linkName' field) - expectedChecksum += 9700; + expectedChecksum += GetChecksum(linkName); - // V7 and Ustar does not support name fields larger than 100 + // V7 and Ustar do not support name fields larger than 100. // Pax and Gnu write first 100 bytes in 'linkName' field, then the full link name is written in the - // preceding metadata entry (extended attributes for PAX, LongLink for GNU). + // preceding metadata entry (extended attributes for PAX, LongLink for GNU), meaning the linkname + // won't be part of the current entry's checksum. if (format is not TarEntryFormat.V7 and not TarEntryFormat.Ustar) { linkName += "/" + new string('a', 50); @@ -341,49 +343,55 @@ private int GetLinkChecksum(TarEntryFormat format, bool longLink, out string lin private int GetChecksumForCommonFields(TarEntry entry, TarEntryType entryType) { // Add 8 spaces to the sum: (8 x 32) = 256 - int expectedChecksum = 256; + int expectedChecksum = GetChecksum(" "); // '0000744\0' = 48 + 48 + 48 + 48 + 55 + 52 + 52 + 0 = 351 entry.Mode = AssetMode; // decimal 484 (octal 744) => u+rxw, g+r, o+r - expectedChecksum += 351; + string modeOctal = Convert.ToString((int)AssetMode, 8).PadLeft(7, '0'); + expectedChecksum += GetChecksum(modeOctal); // '0017351\0' = 48 + 48 + 49 + 55 + 51 + 53 + 49 + 0 = 353 entry.Uid = AssetUid; // 7913 (octal 17351) - expectedChecksum += 353; + string uidOctal = Convert.ToString(AssetUid, 8).PadLeft(7, '0'); + expectedChecksum += GetChecksum(uidOctal); // '0006773\0' = 48 + 48 + 48 + 54 + 55 + 55 + 51 + 0 = 359 entry.Gid = AssetGid; // 3579 (octal 6773) - expectedChecksum += 359; + string gidOctal = Convert.ToString(AssetGid, 8).PadLeft(7, '0'); + expectedChecksum += GetChecksum(gidOctal); // '14164217674\0' = 49 + 52 + 49 + 54 + 52 + 50 + 49 + 55 + 54 + 55 + 52 + 0 = 571 DateTimeOffset mtime = TimestampForChecksum; // ToUnixTimeSeconds() = 1641095100 (octal 14164217674) entry.ModificationTime = mtime; - expectedChecksum += 571; + expectedChecksum += GetChecksum(Convert.ToString(mtime.ToUnixTimeSeconds(), 8).PadLeft(11, '0')); if (entryType is TarEntryType.RegularFile or TarEntryType.V7RegularFile) { entry.DataStream = new MemoryStream(); byte[] buffer = [ 72, 101, 108, 108, 111 ]; // values don't matter, only length (5) - // '0000000005\0' = 48 + 48 + 48 + 48 + 48 + 48 + 48 + 48 + 48 + 48 + 53 + 0 = 533 + // '00000000005\0' = 48 + 48 + 48 + 48 + 48 + 48 + 48 + 48 + 48 + 48 + 53 + 0 = 533 entry.DataStream.Write(buffer); entry.DataStream.Seek(0, SeekOrigin.Begin); // Rewind to ensure it gets written from the beginning - expectedChecksum += 533; + expectedChecksum += GetChecksum(Convert.ToString(buffer.Length, 8).PadLeft(11, '0')); } else { - // '0000000000\0' = 48 + 48 + 48 + 48 + 48 + 48 + 48 + 48 + 48 + 48 + 48 + 0 = 528 - expectedChecksum += 528; + // '00000000000\0' = 48 + 48 + 48 + 48 + 48 + 48 + 48 + 48 + 48 + 48 + 48 + 0 = 528 + expectedChecksum += GetChecksum(new string('0', 11)); } + // Some examples: // If V7 regular file: '\0' = 0 // If Ustar/Pax/Gnu regular file: '0' = 48 + // If symbolic link: '2' = 50 // If block device: '4' = 52 expectedChecksum += (byte)entryType; // Checksum so far: 256 + 351 + 353 + 359 + 571 = decimal 1890 // If V7RegularFile: 1890 + 533 + 0 = 2423 (octal 4567) => '004567\0' // If RegularFile: 1890 + 533 + 48 = 2471 (octal 4647) => '004647\0' + // If SymbolicLink: 1890 + 528 + 50 = 2468 (octal 4644) => '004644\0' // If BlockDevice: 1890 + 0 + 52 = 1942 (octal 3626) => '003626\0' return expectedChecksum; } @@ -396,16 +404,16 @@ private int GetChecksumForFormatSpecificFields(TarEntry entry, TarEntryFormat fo case TarEntryFormat.Ustar: case TarEntryFormat.Pax: // Magic: 'ustar\0' = 117 + 115 + 116 + 97 + 114 + 0 = 559 - checksum += 559; + checksum += GetChecksum("ustar"); // Version: '00' = 48 + 48 = 96 - checksum += 96; + checksum += GetChecksum("00"); // Total: 655 break; case TarEntryFormat.Gnu: // Magic: 'ustar ' = 117 + 115 + 116 + 97 + 114 + 32 = 591 - checksum += 591; + checksum += GetChecksum("ustar "); // Version: ' \0' = 32 + 0 = 32 - checksum += 32; + checksum += GetChecksum(" "); // Total: 623 break; } @@ -414,20 +422,20 @@ private int GetChecksumForFormatSpecificFields(TarEntry entry, TarEntryFormat fo { // 'user' = 117 + 115 + 101 + 114 = 447 posixEntry.UserName = TestUName; - checksum += 447; + checksum += GetChecksum(TestUName); // 'group' = 103 + 114 + 111 + 117 + 112 = 557 posixEntry.GroupName = TestGName; - checksum += 557; + checksum += GetChecksum(TestGName); // Total: 1004 if (posixEntry.EntryType is TarEntryType.BlockDevice) { // '0000075\0' = 48 + 48 + 48 + 48 + 48 + 55 + 53 + 0 = 348 posixEntry.DeviceMajor = TestBlockDeviceMajor; // 61 (octal 75) - checksum += 348; + checksum += GetChecksum(Convert.ToString(TestBlockDeviceMajor, 8).PadLeft(7, '0')); // '0000101\0' = 48 + 48 + 48 + 48 + 49 + 48 + 49 + 0 = 338 posixEntry.DeviceMinor = TestBlockDeviceMinor; // 65 (octal 101) - checksum += 338; + checksum += GetChecksum(Convert.ToString(TestBlockDeviceMinor, 8).PadLeft(7, '0')); // Total: 686 } @@ -440,21 +448,22 @@ private int GetChecksumForFormatSpecificFields(TarEntry entry, TarEntryFormat fo gnuEntry.ChangeTime = expectedTimestampToTest; checksum += expectedTimestampChecksumToTest; - // Total: UnixEpoch = 1056, otherwise = 1142 + // Total: UnixEpoch = 0, otherwise = 1142 void GetTimestampToTest(bool testEpoch, out DateTimeOffset expectedTimestampToTest, out int expectedTimestampChecksumToTest) { if (!testEpoch) { - expectedTimestampChecksumToTest = 571; + expectedTimestampChecksumToTest = GetChecksum(Convert.ToString(TimestampForChecksum.ToUnixTimeSeconds(), 8).PadLeft(11, '0')); // '14164217674\0' = 49 + 52 + 49 + 54 + 52 + 50 + 49 + 55 + 54 + 55 + 52 + 0 = 571 expectedTimestampToTest = TimestampForChecksum; // ToUnixTimeSeconds() = decimal 1641095100, octal 14164217674; + return; } - expectedTimestampChecksumToTest = 528; - // '00000000000\0' = 0 - expectedTimestampToTest = UnixEpochTimestampForChecksum; // ToUnixTimeSeconds() = decimal 0, octal 0; + expectedTimestampChecksumToTest = 0; + // '\0\0\0\0\0\0\0\0\0\0\0\0' = 0 + expectedTimestampToTest = default; } } } @@ -464,11 +473,12 @@ void GetTimestampToTest(bool testEpoch, out DateTimeOffset expectedTimestampToTe // Ustar RegularFile: 655 + 1004 = 1659 // Pax RegularFile: 655 + 1004 = 1659 // Gnu RegularFile: 623 + 1004 + 1142 = 2769 + // Gnu SymbolicLink: 623 + 1004 + 0 = 1627 // Ustar BlockDevice: 655 + 1004 + 686 = 2345 // Pax BlockDevice: 655 + 1004 + 686 = 2345 // Gnu BlockDevice: // No epoch: 623 + 1004 + 686 + 1142 = 3455 - // Epoch: 623 + 1004 + 686 + 1056 = 3369 + // Epoch: 0 return checksum; } } diff --git a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.Base.cs b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.Base.cs index d8d4018c40b017..6e04c67b3e963d 100644 --- a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.Base.cs +++ b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.Base.cs @@ -108,7 +108,7 @@ private static IEnumerable GetWriteTimeStamps(TarEntryFormat for if (!formatIsOctalOnly) { // Min value property. - yield return DateTimeOffset.MinValue; // This is not representable with the octal format. + yield return default; // This is not representable with the octal format. // One second past what a 12-byte field can store with octal representation yield return DateTimeOffset.UnixEpoch + new TimeSpan((0x1FFFFFFFF + 1) * TimeSpan.TicksPerSecond);