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