mirror of
https://github.com/adamhathcock/sharpcompress.git
synced 2026-07-08 18:16:30 +00:00
Add global pax support to TarArchive
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
BIN
tests/TestArchives/Archives/Tar.PaxGlobalHeader.Link.tar
Normal file
BIN
tests/TestArchives/Archives/Tar.PaxGlobalHeader.Link.tar
Normal file
Binary file not shown.
BIN
tests/TestArchives/Archives/Tar.PaxGlobalHeader.tar
Normal file
BIN
tests/TestArchives/Archives/Tar.PaxGlobalHeader.tar
Normal file
Binary file not shown.
Reference in New Issue
Block a user