Skip to content

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.gz could plant a link → /etc/passwd for downstream rewriters to follow. (TarExtract_SymlinkEntry_SkippedNotFollowed pins this.)
  • PAX long paths — TarReader handles 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 ZipExtractorTests pass unchanged — back-compat preserved
  • No new NuGet dependencies
  • Staging: real .tar.gz deploy 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

Merge request reports

Loading