Gunnar

nordisk/Skidbladnir

.nornir/archive-guide.md

# archive — guide

> **Class:** `guide` · audience: a techie packing or reading bundles. The *what* is in
> [`archive-design.md`](archive-design.md); the *why* in [`archive-reasoning.md`](archive-reasoning.md).

## Pick an engine

```rust
use skidbladnir::archive::{Archive, TarZstd};
let eng = TarZstd::new();          // always available, always offline
```

For the REAL znippy-core engine (seal/read/verify/unpack via `../znippy`, so the bundle is the one
canonical `.znippy` — `znippy` CLI / DuckDB readable), build with `--features znippy` and swap the
constructor — the trait seam means nothing else changes:

```rust
# #[cfg(feature = "znippy")]
let eng = skidbladnir::archive::ZnippyArchive::new();
```

## Pack a staging tree

```rust
// staging_root holds your files under their relative paths (see pack-guide.md)
let manifest = eng.pack(&staging_root, "20260704", &bundle_path)?;   // Archive::pack
assert!(manifest.verify_root());                                     // self-check
// a sidecar <bundle_path>.manifest.json is written beside the bundle
```

`manifest.entries` is one `EntryDigest { path, sha256, len }` per file; `manifest.root_sha256` is
the hash over all of them. This manifest is what the far side verifies.

## List and read without extracting everything

```rust
for path in eng.list(&bundle_path)? { println!("{path}"); }
let bytes = eng.read_entry(&bundle_path, "bin/app")?;   // znippy: get_file random-access; tar: temp-extract
```

## Verify a received bundle

```rust
let manifest = skidbladnir::archive::load_manifest(&bundle_path)?;
let bad = eng.verify_entries(&bundle_path, &manifest)?;   // Vec<String> of bad paths
assert!(bad.is_empty(), "tampered or incomplete: {bad:?}");
```

Prefer the `fetch` wrapper for the full receipt gate (manifest root + optional out-of-band SHA +
per-entry) — see [`move-guide.md`](move-guide.md).

## Ship only what changed (znippy delta)

With `--features znippy`, pack a patch against the previous manifest and apply it on the far side:

```rust
# #[cfg(feature = "znippy")] {
let patch = eng.delta(&old_manifest, &new_staging, "20260705")?;   // ships only changed blobs
println!("shipped {} bytes, reused {} entries", patch.shipped_bytes(), patch.reused());
let new_manifest = eng.apply(&old_bundle, &patch, &new_bundle)?;   // re-seals a real znippy bundle; same content root as a fresh pack
# }
```

On the default `TarZstd` engine `delta`/`apply` bail with a clear "use the znippy feature" message
(`archive.rs:143`) — they are not silently faked.

## Gotchas

- The manifest is a **sidecar** file (`<bundle>.manifest.json`), not inside the archive. Ship it
  alongside the bundle, or regenerate it with `pack`.
- `TarZstd::new()` maps level 0 to zstd level 3; pass a level explicitly for a different ratio.
- znippy `read_entry` (`get_file`) bounds blob length against file size before allocating — a
  corrupt/hostile index cannot OOM you — and blake3-verifies every chunk. Extraction rejects
  `..`/absolute paths (zip-slip), and a tampered byte fails verification loudly.