Multi-format package extraction (.tar, .tar.gz, .tgz)
Summary
Extends G1.4 ExtractPackageStep with format dispatch. Operator's .nupkg/.zip workflow is byte-identical; .tar/.tar.gz/.tgz now extract through the same safety story (zip-slip, per-entry + total size caps, fail-closed).
Zero new NuGet dependencies — System.Formats.Tar is .NET 9 native, gzip via System.IO.Compression.
Format dispatch matrix
| Extension | Extractor | Engine |
|---|---|---|
.zip, .nupkg
|
ZipPackageExtractor |
System.IO.Compression.ZipArchive (existing) |
.tar |
TarPackageExtractor |
System.Formats.Tar.TarReader |
.tar.gz, .tgz
|
TarGzPackageExtractor |
GZipStream → TarReader
|
.7z |
SevenZipUnsupportedExtractor |
Recognised, deferred — clear error message naming alternatives |
| unknown | (registry returns null → step throws) | Operator gets list of supported formats |
What's in the box
| Layer | File |
|---|---|
| Interface |
src/Squid.Calamari/Commands/Package/IPackageExtractor.cs (new) |
| Shared safety primitives |
src/Squid.Calamari/Commands/Package/ArchiveSafety.cs (new — extracted from ZipExtractor) |
| Dispatcher |
src/Squid.Calamari/Commands/Package/PackageExtractorRegistry.cs (new) |
| Tar + tar.gz + 7z |
src/Squid.Calamari/Commands/Package/TarPackageExtractor.cs (new) |
| Zip refactored |
src/Squid.Calamari/Commands/Package/ZipExtractor.cs (static API preserved + ZipPackageExtractor wrapper added) |
| Step uses registry | src/Squid.Calamari/Commands/Package/ExtractPackageStep.cs |
| Tests |
TarPackageExtractorTests.cs (15), PackageExtractorRegistryTests.cs (8), step tests +3 |
Hostile-archive safety (uniform across formats)
All extractors use ArchiveSafety primitives — single source of truth for safety:
-
Zip-slip / tar-slip / absolute-path rejection via
ResolveSafeEntryPath -
Per-entry size cap =
EncodingPreservingFileIO.ResolveMaxFileSizeMB()(T3 env-var-tunable, 50 MB default) - Total archive cap = 10× per-entry — catches zip-bomb pattern across formats
- Fail-closed on first violation — whole extract aborts, no partial state
Tar-specific safety additions
-
Symlinks / hardlinks / device files — SKIPPED with structured console warning. Without this, a malicious
.tar.gzcould plant alink → /etc/passwdfor downstream rewriters to follow. (TarExtract_SymlinkEntry_SkippedNotFollowedpins this.) -
PAX long paths —
TarReaderhandles POSIX-1.2001 extended headers natively. No manual header reassembly.
Test plan
-
Squid.Calamari.Tests: 374/374 (was 335; +39) -
Squid.UnitTests: 5625/5625 (unchanged) -
Solution build: 0 errors -
Pre-merge audit GREEN on 7/7 invariants -
All 11 existing G1.4 ZipExtractorTestspass unchanged — back-compat preserved -
No new NuGet dependencies -
Staging: real .tar.gzdeploy with PreDeploy / PostDeploy conventions inside
Non-breaking guarantee
| Surface | Status |
|---|---|
Existing .nupkg / .zip deploys |
Byte-identical behaviour |
ZipExtractor.Extract(string, string) static API |
Preserved (G1.4 tests reference it directly) |
Wire literal Squid.Action.Package.OriginalPath
|
Unchanged |
| Standalone-script deploys | Zero impact |
.7z operators |
Get clear "convert to .zip/.nupkg/.tar/.tar.gz" message instead of generic "unsupported format" |
| SquidWeb | Untouched |