Add global pax support to TarArchive

This commit is contained in:
Adam Hathcock
2026-04-20 08:26:11 +01:00
parent 823afd4fd5
commit 2a62f4dfe2
12 changed files with 375 additions and 91 deletions

View File

@@ -21,10 +21,14 @@ Primary references:
- `Tar.XZ` is now documented as read-only (`Writer API = N/A`) in `docs/FORMATS.md`.
- Local PAX extended headers (`x`) are now implemented on the read path for selected keys.
- Global PAX extended headers (`g`) are now implemented on the read path for selected keys.
- Tar tests now include local PAX coverage for reader/archive sync and async paths.
- Tar tests now include global PAX coverage for reader/archive sync and async paths.
- `TarWriterOptions.HeaderFormat` is now honored in sync and async file and directory write paths.
- Tar tests now cover `USTAR` and `GNU_TAR_LONG_LINK`, including USTAR long-name failure scenarios.
- Symlink coverage now includes `TarWithSymlink.tar.gz` for reader sync and async paths.
- Sparse handling remains explicitly unsupported.
- Non-modeled PAX keys remain explicitly unsupported.
## Claimed vs Actual Support
@@ -50,9 +54,9 @@ Recommended action:
## Read-Path Gaps
### Local PAX headers are implemented; global PAX is still pending
### Local and global PAX headers are implemented for selected keys
Local POSIX PAX extended headers (`x`) are now supported on the read path.
Local (`x`) and global (`g`) POSIX PAX extended headers are now supported on the read path.
Supported keys in the current implementation:
@@ -66,17 +70,20 @@ Supported keys in the current implementation:
Remaining gap:
- global PAX extended headers (`g`) are still not semantically implemented
- non-modeled PAX keys are still ignored
- PAX sparse extensions are still unsupported
Recommended action:
- keep local PAX key support documented as implemented
- implement global PAX (`g`) separately when cross-entry metadata state is added
- keep supported-key boundaries documented and test-covered
- keep unsupported-key behavior explicit in docs
### Sparse files are not semantically implemented
`EntryType` defines `SparseFile`, but the read path does not contain sparse map handling or sparse reconstruction logic.
PAX sparse extensions are also unsupported (for example `GNU.sparse.*` and similar sparse metadata keys).
Evidence:
- `src/SharpCompress/Common/Tar/Headers/EntryType.cs`
@@ -89,26 +96,25 @@ Impact:
Recommended action:
- document sparse support as unsupported or partial
- add explicit tests if future support is added
- keep sparse support explicitly documented as unsupported
- add sparse fixtures and tests only when sparse reconstruction is implemented
### Global extended headers are not semantically implemented
### Non-modeled PAX keys are still unsupported
`EntryType` defines `GlobalExtendedHeader`, but no semantic handling exists in the read pipeline.
PAX parsing is intentionally limited to modeled keys (`path`, `linkpath`, `size`, `mtime`, `uid`, `gid`, `mode`).
Evidence:
Not currently modeled/supported:
- `TarHeader.Read` does not special-case `GlobalExtendedHeader`
- `TarEntry` does not surface a global-header model
- no tests cover this case
Impact:
- global metadata records are not applied in a defined way
- `uname`
- `gname`
- `atime`
- `ctime`
- device-specific values and vendor keys
Recommended action:
- document as unsupported until explicit behavior exists
- keep unsupported-key behavior documented as ignored
- add support only when there is a consumer-facing object model for it
### Device and FIFO semantics are not surfaced
@@ -215,12 +221,12 @@ Tar tests now cover:
- long-name failure in USTAR mode
- long-name success in GNU mode through sync and async writer paths
### No tests for sparse or global PAX headers
### No tests for sparse tar semantics
Local PAX coverage now exists, but there is still no evidence of coverage for:
Local and global PAX coverage now exists, but there is still no evidence of coverage for:
- global extended headers
- sparse tar entries
- sparse PAX extensions
Impact:
@@ -255,7 +261,7 @@ Missing implementation-specific details include:
- GNU long-name and long-link support
- USTAR prefix handling
- oldgnu numeric quirk handling
- partial PAX support boundaries (local supported, global pending)
- partial PAX support boundaries (selected local/global keys supported)
- missing sparse support
- reader vs archive behavior differences for compressed tar
- file-size requirements for writing from non-seekable sources
@@ -281,15 +287,10 @@ Recommended action:
### Priority 1
- Implement and document global PAX (`g`) support
- Decide and document the support position for sparse files
- Decide and document support boundaries for non-modeled PAX keys (`uname`, `gname`, vendor keys)
### Priority 2
- Add negative writer tests for unsupported wrapper compressions
- Evaluate whether sync and async archive open contracts should match exactly
- Improve metadata round-trip behavior only if there is a consumer need
- Evaluate whether non-modeled PAX keys should remain ignored or be surfaced in a future metadata API
## Summary

View File

@@ -270,6 +270,7 @@ Tar header parsing is implemented in `TarHeader.Read` and `TarHeader.ReadAsync`.
| GNU long name (`L`) | Yes |
| GNU long link (`K`) | Yes |
| PAX local extended header (`x`) | Yes (selected keys) |
| PAX global extended header (`g`) | Yes (selected keys) |
| USTAR prefix reconstruction | Yes |
| Binary size field parsing | Yes |
| oldgnu uid/gid numeric quirk parsing | Yes |
@@ -318,6 +319,22 @@ Currently supported keys:
Unknown PAX keys are ignored.
### PAX Global Header Reads
SharpCompress consumes global PAX extended headers (`g`) and applies supported key overrides to subsequent entries.
Supported keys match local PAX support:
- `path`
- `linkpath`
- `size`
- `mtime`
- `uid`
- `gid`
- `mode`
Global metadata is overridden by local per-entry metadata when both are present.
### Name Reconstruction
For USTAR headers, if the magic field is `ustar` and the prefix field is populated, SharpCompress reconstructs the entry name as `prefix + "/" + name`.
@@ -376,7 +393,7 @@ Async tar support is provided by:
- `TarHeader.ReadAsync`
- `TarHeader.WriteAsync`
The async implementations generally mirror the sync implementations while using async header parsing, decompression, and stream copy paths. The most important current exception is `TarWriterOptions.HeaderFormat`, which is not consistently honored outside the synchronous file write path.
The async implementations generally mirror the sync implementations while using async header parsing, decompression, and stream copy paths.
## Known Limitations
@@ -394,10 +411,8 @@ This section documents current implementation limits, not desired future behavio
### Read limitations or partial support
- No PAX global extended header (`g`) support
- Local PAX support is limited to selected keys (`path`, `linkpath`, `size`, `mtime`, `uid`, `gid`, `mode`)
- PAX support is limited to selected keys (`path`, `linkpath`, `size`, `mtime`, `uid`, `gid`, `mode`)
- No semantic sparse file handling beyond recognizing the entry type enum value
- No semantic global extended header handling beyond recognizing the entry type enum value
- No special device or FIFO object model beyond the raw entry type information available internally
### Archive behavior limitations
@@ -433,6 +448,8 @@ Representative tar test archives in `tests/TestArchives/Archives/`:
- `very long filename.tar`
- `ustar with long names.tar`
- `Tar.LongPathsWithLongNameExtension.tar`
- `Tar.PaxGlobalHeader.tar`
- `Tar.PaxGlobalHeader.Link.tar`
- `Tar.Empty.tar`
- `TarCorrupted.tar`
- `TarWithSymlink.tar.gz`

View File

@@ -200,9 +200,13 @@ internal sealed partial class TarHeader
.ConfigureAwait(false);
}
internal async ValueTask<bool> ReadAsync(AsyncBinaryReader reader)
internal async ValueTask<bool> ReadAsync(
AsyncBinaryReader reader,
PaxMetadata? globalPaxMetadata = null
)
{
var pendingMetadata = new PendingTarMetadata();
globalPaxMetadata ??= new PaxMetadata();
var pendingMetadata = globalPaxMetadata.Clone();
byte[] buffer;
EntryType entryType;
@@ -239,6 +243,13 @@ internal sealed partial class TarHeader
continue;
}
if (entryType == EntryType.GlobalExtendedHeader)
{
await ReadPaxMetadataAsync(reader, buffer, globalPaxMetadata).ConfigureAwait(false);
pendingMetadata = globalPaxMetadata.Clone();
continue;
}
break;
}
@@ -278,7 +289,7 @@ internal sealed partial class TarHeader
}
}
ApplyPendingMetadata(pendingMetadata);
pendingMetadata.ApplyTo(this);
if (entryType == EntryType.Directory)
{
@@ -329,7 +340,7 @@ internal sealed partial class TarHeader
private async ValueTask ReadPaxMetadataAsync(
AsyncBinaryReader reader,
byte[] buffer,
PendingTarMetadata pendingMetadata
PaxMetadata pendingMetadata
)
{
var payload = await ReadMetadataPayloadAsync(

View File

@@ -43,7 +43,7 @@ internal sealed partial class TarHeader
private const int MAX_LONG_NAME_SIZE = 32768;
private const int MAX_PAX_HEADER_SIZE = 65536;
private sealed class PendingTarMetadata
internal sealed class PaxMetadata
{
internal string? Name { get; set; }
internal string? LinkName { get; set; }
@@ -52,6 +52,56 @@ internal sealed partial class TarHeader
internal long? GroupId { get; set; }
internal long? Size { get; set; }
internal DateTime? LastModifiedTime { get; set; }
internal PaxMetadata Clone() =>
new()
{
Name = Name,
LinkName = LinkName,
Mode = Mode,
UserId = UserId,
GroupId = GroupId,
Size = Size,
LastModifiedTime = LastModifiedTime,
};
internal void ApplyTo(TarHeader header)
{
if (Name is not null)
{
header.Name = Name;
}
if (LinkName is not null)
{
header.LinkName = LinkName;
}
if (Size.HasValue)
{
header.Size = Size.Value;
}
if (LastModifiedTime.HasValue)
{
header.LastModifiedTime = LastModifiedTime.Value;
}
if (Mode.HasValue)
{
header.Mode = Mode.Value;
}
if (UserId.HasValue)
{
header.UserId = UserId.Value;
}
if (GroupId.HasValue)
{
header.GroupId = GroupId.Value;
}
}
}
internal void Write(Stream output)
@@ -246,9 +296,10 @@ internal sealed partial class TarHeader
output.Write(stackalloc byte[numPaddingBytes]);
}
internal bool Read(BinaryReader reader)
internal bool Read(BinaryReader reader, PaxMetadata? globalPaxMetadata = null)
{
var pendingMetadata = new PendingTarMetadata();
globalPaxMetadata ??= new PaxMetadata();
var pendingMetadata = globalPaxMetadata.Clone();
byte[] buffer;
EntryType entryType;
@@ -283,6 +334,13 @@ internal sealed partial class TarHeader
continue;
}
if (entryType == EntryType.GlobalExtendedHeader)
{
ReadPaxMetadata(reader, buffer, globalPaxMetadata);
pendingMetadata = globalPaxMetadata.Clone();
continue;
}
break;
}
@@ -322,7 +380,7 @@ internal sealed partial class TarHeader
}
}
ApplyPendingMetadata(pendingMetadata);
pendingMetadata.ApplyTo(this);
if (entryType == EntryType.Directory)
{
@@ -343,11 +401,7 @@ internal sealed partial class TarHeader
return ArchiveEncoding.Decode(nameBytes, 0, nameBytes.Length).TrimNulls();
}
private void ReadPaxMetadata(
BinaryReader reader,
byte[] buffer,
PendingTarMetadata pendingMetadata
)
private void ReadPaxMetadata(BinaryReader reader, byte[] buffer, PaxMetadata pendingMetadata)
{
var payload = ReadMetadataPayload(reader, buffer, MAX_PAX_HEADER_SIZE, "PAX header");
ParsePaxRecords(payload, pendingMetadata);
@@ -403,7 +457,7 @@ internal sealed partial class TarHeader
return remainder == 0 ? 0 : BLOCK_SIZE - remainder;
}
private static void ParsePaxRecords(byte[] payload, PendingTarMetadata pendingMetadata)
private static void ParsePaxRecords(byte[] payload, PaxMetadata pendingMetadata)
{
var index = 0;
while (index < payload.Length)
@@ -473,11 +527,7 @@ internal sealed partial class TarHeader
return value;
}
private static void ApplyPaxKeyValue(
PendingTarMetadata pendingMetadata,
string key,
string value
)
private static void ApplyPaxKeyValue(PaxMetadata pendingMetadata, string key, string value)
{
switch (key)
{
@@ -579,44 +629,6 @@ internal sealed partial class TarHeader
}
}
private void ApplyPendingMetadata(PendingTarMetadata pendingMetadata)
{
if (pendingMetadata.Name is not null)
{
Name = pendingMetadata.Name;
}
if (pendingMetadata.LinkName is not null)
{
LinkName = pendingMetadata.LinkName;
}
if (pendingMetadata.Size.HasValue)
{
Size = pendingMetadata.Size.Value;
}
if (pendingMetadata.LastModifiedTime.HasValue)
{
LastModifiedTime = pendingMetadata.LastModifiedTime.Value;
}
if (pendingMetadata.Mode.HasValue)
{
Mode = pendingMetadata.Mode.Value;
}
if (pendingMetadata.UserId.HasValue)
{
UserId = pendingMetadata.UserId.Value;
}
if (pendingMetadata.GroupId.HasValue)
{
GroupId = pendingMetadata.GroupId.Value;
}
}
private static EntryType ReadEntryType(byte[] buffer) => (EntryType)buffer[156];
private long ReadSize(byte[] buffer)

View File

@@ -19,13 +19,15 @@ internal static partial class TarHeaderFactory
using var reader = new AsyncBinaryReader(stream, leaveOpen: true);
#endif
var globalPaxMetadata = new TarHeader.PaxMetadata();
while (true)
{
TarHeader? header = null;
try
{
header = new TarHeader(archiveEncoding);
if (!await header.ReadAsync(reader).ConfigureAwait(false))
if (!await header.ReadAsync(reader, globalPaxMetadata).ConfigureAwait(false))
{
yield break;
}

View File

@@ -13,6 +13,7 @@ internal static partial class TarHeaderFactory
IArchiveEncoding archiveEncoding
)
{
var globalPaxMetadata = new TarHeader.PaxMetadata();
while (true)
{
TarHeader? header = null;
@@ -21,7 +22,7 @@ internal static partial class TarHeaderFactory
var reader = new BinaryReader(stream, archiveEncoding.Default, leaveOpen: false);
header = new TarHeader(archiveEncoding);
if (!header.Read(reader))
if (!header.Read(reader, globalPaxMetadata))
{
yield break;
}

View File

@@ -314,4 +314,62 @@ public class TarArchiveAsyncTests : ArchiveTests
Assert.Equal("pax/link-entry", entry.Key);
Assert.Equal("pax/target-entry", entry.LinkTarget);
}
[Fact]
public async ValueTask Tar_PaxGlobalHeader_Archive_Async()
{
var archivePath = Path.Combine(TEST_ARCHIVES_PATH, "Tar.PaxGlobalHeader.tar");
await using var archive = await TarArchive.OpenAsyncArchive(
new AsyncOnlyStream(File.OpenRead(archivePath))
);
var globalTime = DateTimeOffset.FromUnixTimeSeconds(1700000100).LocalDateTime;
var localOverrideTime = DateTimeOffset.FromUnixTimeSeconds(1700000200).LocalDateTime;
var firstEntry = (TarArchiveEntry)
await archive.EntriesAsync.SingleAsync(entry => entry.Key == "global-one.txt");
Assert.Equal(4000, firstEntry.UserID);
Assert.Equal(5000, firstEntry.GroupId);
Assert.Equal(Convert.ToInt64("640", 8), firstEntry.Mode);
Assert.Equal(globalTime, firstEntry.LastModifiedTime);
var secondEntry = (TarArchiveEntry)
await archive.EntriesAsync.SingleAsync(entry =>
entry.Key == "global-local-override.txt"
);
Assert.Equal(4010, secondEntry.UserID);
Assert.Equal(5010, secondEntry.GroupId);
Assert.Equal(Convert.ToInt64("600", 8), secondEntry.Mode);
Assert.Equal(localOverrideTime, secondEntry.LastModifiedTime);
var thirdEntry = (TarArchiveEntry)
await archive.EntriesAsync.SingleAsync(entry => entry.Key == "global-three.txt");
Assert.Equal(4000, thirdEntry.UserID);
Assert.Equal(5000, thirdEntry.GroupId);
Assert.Equal(Convert.ToInt64("640", 8), thirdEntry.Mode);
Assert.Equal(globalTime, thirdEntry.LastModifiedTime);
}
[Fact]
public async ValueTask Tar_PaxGlobalHeader_Link_Archive_Async()
{
var archivePath = Path.Combine(TEST_ARCHIVES_PATH, "Tar.PaxGlobalHeader.Link.tar");
await using var archive = await TarArchive.OpenAsyncArchive(
new AsyncOnlyStream(File.OpenRead(archivePath))
);
var globalLink = (TarArchiveEntry)
await archive.EntriesAsync.SingleAsync(entry => entry.Key == "global-link");
Assert.Equal("global-target", globalLink.LinkTarget);
Assert.Equal(4100, globalLink.UserID);
Assert.Equal(5100, globalLink.GroupId);
Assert.Equal(Convert.ToInt64("777", 8), globalLink.Mode);
var localOverrideLink = (TarArchiveEntry)
await archive.EntriesAsync.SingleAsync(entry => entry.Key == "local-link-override");
Assert.Equal("local-target", localOverrideLink.LinkTarget);
Assert.Equal(4100, localOverrideLink.UserID);
Assert.Equal(5100, localOverrideLink.GroupId);
Assert.Equal(Convert.ToInt64("777", 8), localOverrideLink.Mode);
}
}

View File

@@ -209,6 +209,58 @@ public class TarArchiveTests : ArchiveTests
Assert.Equal("pax/target-entry", entry.LinkTarget);
}
[Fact]
public void Tar_PaxGlobalHeader_Archive()
{
var archivePath = Path.Combine(TEST_ARCHIVES_PATH, "Tar.PaxGlobalHeader.tar");
using var archive = TarArchive.OpenArchive(archivePath);
var globalTime = DateTimeOffset.FromUnixTimeSeconds(1700000100).LocalDateTime;
var localOverrideTime = DateTimeOffset.FromUnixTimeSeconds(1700000200).LocalDateTime;
var firstEntry = (TarArchiveEntry)
archive.Entries.Single(entry => entry.Key == "global-one.txt");
Assert.Equal(4000, firstEntry.UserID);
Assert.Equal(5000, firstEntry.GroupId);
Assert.Equal(Convert.ToInt64("640", 8), firstEntry.Mode);
Assert.Equal(globalTime, firstEntry.LastModifiedTime);
var secondEntry = (TarArchiveEntry)
archive.Entries.Single(entry => entry.Key == "global-local-override.txt");
Assert.Equal(4010, secondEntry.UserID);
Assert.Equal(5010, secondEntry.GroupId);
Assert.Equal(Convert.ToInt64("600", 8), secondEntry.Mode);
Assert.Equal(localOverrideTime, secondEntry.LastModifiedTime);
var thirdEntry = (TarArchiveEntry)
archive.Entries.Single(entry => entry.Key == "global-three.txt");
Assert.Equal(4000, thirdEntry.UserID);
Assert.Equal(5000, thirdEntry.GroupId);
Assert.Equal(Convert.ToInt64("640", 8), thirdEntry.Mode);
Assert.Equal(globalTime, thirdEntry.LastModifiedTime);
}
[Fact]
public void Tar_PaxGlobalHeader_Link_Archive()
{
var archivePath = Path.Combine(TEST_ARCHIVES_PATH, "Tar.PaxGlobalHeader.Link.tar");
using var archive = TarArchive.OpenArchive(archivePath);
var globalLink = (TarArchiveEntry)
archive.Entries.Single(entry => entry.Key == "global-link");
Assert.Equal("global-target", globalLink.LinkTarget);
Assert.Equal(4100, globalLink.UserID);
Assert.Equal(5100, globalLink.GroupId);
Assert.Equal(Convert.ToInt64("777", 8), globalLink.Mode);
var localOverrideLink = (TarArchiveEntry)
archive.Entries.Single(entry => entry.Key == "local-link-override");
Assert.Equal("local-target", localOverrideLink.LinkTarget);
Assert.Equal(4100, localOverrideLink.UserID);
Assert.Equal(5100, localOverrideLink.GroupId);
Assert.Equal(Convert.ToInt64("777", 8), localOverrideLink.Mode);
}
[Fact]
public void Tar_Create_New()
{

View File

@@ -203,6 +203,71 @@ public class TarReaderAsyncTests : ReaderTests
Assert.False(await reader.MoveToNextEntryAsync());
}
[Fact]
public async ValueTask Tar_PaxGlobalHeader_Reader_Async()
{
var archivePath = Path.Combine(TEST_ARCHIVES_PATH, "Tar.PaxGlobalHeader.tar");
using Stream stream = File.OpenRead(archivePath);
await using var reader = await TarReader.OpenAsyncReader(stream);
var globalTime = DateTimeOffset.FromUnixTimeSeconds(1700000100).LocalDateTime;
var localOverrideTime = DateTimeOffset.FromUnixTimeSeconds(1700000200).LocalDateTime;
Assert.True(await reader.MoveToNextEntryAsync());
var firstEntry = (TarEntry)reader.Entry;
Assert.Equal("global-one.txt", firstEntry.Key);
Assert.Equal(4000, firstEntry.UserID);
Assert.Equal(5000, firstEntry.GroupId);
Assert.Equal(Convert.ToInt64("640", 8), firstEntry.Mode);
Assert.Equal(globalTime, firstEntry.LastModifiedTime);
Assert.True(await reader.MoveToNextEntryAsync());
var secondEntry = (TarEntry)reader.Entry;
Assert.Equal("global-local-override.txt", secondEntry.Key);
Assert.Equal(4010, secondEntry.UserID);
Assert.Equal(5010, secondEntry.GroupId);
Assert.Equal(Convert.ToInt64("600", 8), secondEntry.Mode);
Assert.Equal(localOverrideTime, secondEntry.LastModifiedTime);
Assert.True(await reader.MoveToNextEntryAsync());
var thirdEntry = (TarEntry)reader.Entry;
Assert.Equal("global-three.txt", thirdEntry.Key);
Assert.Equal(4000, thirdEntry.UserID);
Assert.Equal(5000, thirdEntry.GroupId);
Assert.Equal(Convert.ToInt64("640", 8), thirdEntry.Mode);
Assert.Equal(globalTime, thirdEntry.LastModifiedTime);
Assert.False(await reader.MoveToNextEntryAsync());
}
[Fact]
public async ValueTask Tar_PaxGlobalHeader_Link_Reader_Async()
{
var archivePath = Path.Combine(TEST_ARCHIVES_PATH, "Tar.PaxGlobalHeader.Link.tar");
using Stream stream = File.OpenRead(archivePath);
await using var reader = await TarReader.OpenAsyncReader(stream);
Assert.True(await reader.MoveToNextEntryAsync());
var firstEntry = (TarEntry)reader.Entry;
Assert.Equal("global-link", firstEntry.Key);
Assert.Equal("global-target", firstEntry.LinkTarget);
Assert.Equal(4100, firstEntry.UserID);
Assert.Equal(5100, firstEntry.GroupId);
Assert.Equal(Convert.ToInt64("777", 8), firstEntry.Mode);
Assert.True(await reader.MoveToNextEntryAsync());
var secondEntry = (TarEntry)reader.Entry;
Assert.Equal("local-link-override", secondEntry.Key);
Assert.Equal("local-target", secondEntry.LinkTarget);
Assert.Equal(4100, secondEntry.UserID);
Assert.Equal(5100, secondEntry.GroupId);
Assert.Equal(Convert.ToInt64("777", 8), secondEntry.Mode);
Assert.False(await reader.MoveToNextEntryAsync());
}
[Fact]
public async ValueTask Tar_WithSymlink_Reader_SurfacesLinkTargets_Async()
{

View File

@@ -223,6 +223,71 @@ public class TarReaderTests : ReaderTests
Assert.False(reader.MoveToNextEntry());
}
[Fact]
public void Tar_PaxGlobalHeader_Reader()
{
var archivePath = Path.Combine(TEST_ARCHIVES_PATH, "Tar.PaxGlobalHeader.tar");
using Stream stream = File.OpenRead(archivePath);
using var reader = TarReader.OpenReader(stream);
var globalTime = DateTimeOffset.FromUnixTimeSeconds(1700000100).LocalDateTime;
var localOverrideTime = DateTimeOffset.FromUnixTimeSeconds(1700000200).LocalDateTime;
Assert.True(reader.MoveToNextEntry());
var firstEntry = (TarEntry)reader.Entry;
Assert.Equal("global-one.txt", firstEntry.Key);
Assert.Equal(4000, firstEntry.UserID);
Assert.Equal(5000, firstEntry.GroupId);
Assert.Equal(Convert.ToInt64("640", 8), firstEntry.Mode);
Assert.Equal(globalTime, firstEntry.LastModifiedTime);
Assert.True(reader.MoveToNextEntry());
var secondEntry = (TarEntry)reader.Entry;
Assert.Equal("global-local-override.txt", secondEntry.Key);
Assert.Equal(4010, secondEntry.UserID);
Assert.Equal(5010, secondEntry.GroupId);
Assert.Equal(Convert.ToInt64("600", 8), secondEntry.Mode);
Assert.Equal(localOverrideTime, secondEntry.LastModifiedTime);
Assert.True(reader.MoveToNextEntry());
var thirdEntry = (TarEntry)reader.Entry;
Assert.Equal("global-three.txt", thirdEntry.Key);
Assert.Equal(4000, thirdEntry.UserID);
Assert.Equal(5000, thirdEntry.GroupId);
Assert.Equal(Convert.ToInt64("640", 8), thirdEntry.Mode);
Assert.Equal(globalTime, thirdEntry.LastModifiedTime);
Assert.False(reader.MoveToNextEntry());
}
[Fact]
public void Tar_PaxGlobalHeader_Link_Reader()
{
var archivePath = Path.Combine(TEST_ARCHIVES_PATH, "Tar.PaxGlobalHeader.Link.tar");
using Stream stream = File.OpenRead(archivePath);
using var reader = TarReader.OpenReader(stream);
Assert.True(reader.MoveToNextEntry());
var firstEntry = (TarEntry)reader.Entry;
Assert.Equal("global-link", firstEntry.Key);
Assert.Equal("global-target", firstEntry.LinkTarget);
Assert.Equal(4100, firstEntry.UserID);
Assert.Equal(5100, firstEntry.GroupId);
Assert.Equal(Convert.ToInt64("777", 8), firstEntry.Mode);
Assert.True(reader.MoveToNextEntry());
var secondEntry = (TarEntry)reader.Entry;
Assert.Equal("local-link-override", secondEntry.Key);
Assert.Equal("local-target", secondEntry.LinkTarget);
Assert.Equal(4100, secondEntry.UserID);
Assert.Equal(5100, secondEntry.GroupId);
Assert.Equal(Convert.ToInt64("777", 8), secondEntry.Mode);
Assert.False(reader.MoveToNextEntry());
}
[Fact]
public void Tar_WithSymlink_Reader_SurfacesLinkTargets()
{

Binary file not shown.