Artifact a4fb31ceaad48b2bc09d9a542e4aa191c62b82262ecdb7ff343e1c45e6d9fdf4:
- File
src/pack.zig
— part of check-in
[ce40b2526e]
at
2026-07-29 16:54:16
on branch shun-implementation
— shun S0: the durable shun table in the log
FrameKind.shun carries a tombstone keyed by the shunned artifact's whole digest (sync SS3.3 L518-525: persistent, repository-local, keyed by complete artifact name). Never interned, so lookup() still means 'the content frame for this hash'; scan rebuilds the set. putShun is idempotent and returns BadKey for any width but 20 or 32 -- checked, not asserted, so a ReleaseFast build cannot append a durable un-openable frame.
pack carries the table forward. Without that the repo's own compaction verb silently drops every tombstone, which would make this slice's durability claim false the first time anyone packs. The pack FILTER is still S2.
storestat and the benchmark restate FrameKind's universe; both now fold from the enum so a future kind fails to compile rather than vanish from the byte accounting. (user: dmestas, size: 30913) [annotate] [blame] [check-ins using]
//! The `zit pack` re-deltification pass — the fossil `extra_deltification` //! analog (storage-delta-base-selection.md §6). A whole-store generational //! rewrite that reconstructs every content artifact and re-stores it through //! the `pickBase` best-of-N policy (logstore.zig), so a source frame's stored //! encoding — including a verbatim wire delta whose sender chose the base — is //! never trusted at rest. This is the pure store-to-store core, reached both by //! the CLI wiring in main.zig (`runPack`) and by the DST/fuzz harnesses that //! exercise the transform in-process; the CLI-layer temp-file/rename plumbing //! stays in main.zig. //! //! In-module file: it reaches `logstore`/`artifact` through relative sibling //! imports (the same way logstore.zig reaches delta/container), not through the //! public `zit` module — a relative import here shares the one module-local copy //! of each dependency rather than pulling a second into a foreign root. const std = @import("std"); const logstore = @import("logstore.zig"); const artifact = @import("artifact.zig"); /// The binary digest width the pack hex helpers round-trip (sha256/sha3-256). const digest_len = 32; /// pack's closed error set: memory, a destination write fault, or a source /// artifact whose reconstructed content does not hash to its name (§7.7) or /// whose frame is otherwise corrupt. pub const PackError = error{ OutOfMemory, Storage, Corrupt }; /// One repack's tally: the artifact count (== the source's content frames), how /// many landed as deltas in the new log, and the on-disk sizes before/after. pub const PackStats = struct { artifacts: usize, deltas: usize, bytes_before: u64, bytes_after: u64, }; /// A source artifact's ordered, bounded base-candidate hints for the repack — a /// check-in manifest's lineage parent (§6.1) or a file blob's same-path /// predecessors (§6.2, nearest first), fed to `putArtifactPacked` ahead of the /// chronological window. Inline fixed buffer (≤ `max_base_candidates`), so the /// hint map holds no per-entry allocation and every fill is bounded. const PriorityHints = struct { buf: [logstore.max_base_candidates]logstore.Rid = undefined, len: u8 = 0, /// The recorded hints in priority order. fn slice(self: *const PriorityHints) []const logstore.Rid { return self.buf[0..self.len]; } /// Append `rid` unless the buffer is full or already holds it (bounded dedup /// — a base offered by two lanes, or a path listing the same blob twice, /// costs no slot). Priority order is preserved: earlier pushes rank higher. fn push(self: *PriorityHints, rid: logstore.Rid) void { std.debug.assert(rid >= 1); // a valid log rid is 1-based if (self.len >= self.buf.len) return; for (self.buf[0..self.len]) |r| if (r == rid) return; self.buf[self.len] = rid; self.len += 1; } }; /// Repack every artifact from `src` into the fresh, empty `dst` (same /// `hash_kind`), choosing each frame's delta base by OUR size-minimizing policy /// instead of trusting the source's stored encoding (§6.1, the fossil /// `extra_deltification` analog). Every content artifact — including a verbatim /// wire delta (`putDeltaContainer`), decoded to full bytes first — is run /// through `pickBase` against its priority hints (a check-in manifest's lineage /// parent §6.1; a file blob's same-path predecessors §6.2, fossil's mlink analog) /// plus the chronological window of already-packed frames, so the sender's base /// choice stops being trusted at rest. Iteration is source offset order, which /// guarantees a chosen base is already present in `dst` before its dependents /// (candidates are always earlier frames, and `delta_of` is a byte offset in the /// NEW file, assigned at write time). The live config survives as exactly one /// frame; superseded config records are dropped (§E). The repository shun table /// survives whole, in persisted order — dropping it would make the set's /// durability claim false the first time anyone packs. Two integrity gates /// bracket the writes: each SOURCE frame is §7.7-verified before it is /// re-encoded, and — MANDATORY (§3) — after the final write every PACKED /// artifact is reconstructed through its NEW chain and re-hashed; any mismatch /// aborts before `commit`, leaving `dst` a discarded temp and `src` untouched. /// `dst` MUST be a just-opened empty log; `src` is never written. Deterministic: /// fixed offset-order iteration and a deterministic `pickBase` make the packed /// bytes a pure function of the source content — so `pack∘pack` reproduces the /// same log. pub fn packLog(gpa: std.mem.Allocator, src: *logstore.LogStore, dst: *logstore.LogStore) PackError!PackStats { // §6.1 base-availability precondition (load-bearing): `dst` MUST be a // just-opened empty log. If it already held frames, the hint translation // `dst.lookup(src.hashOf(sb))` below could resolve a pre-existing unrelated // frame and silently mis-base a re-delta. Assert it before the first write. std.debug.assert(dst.entryCount() == 0); // The live config survives as exactly one frame; every superseded config // record is dropped (the whole of pack's "dead frame" reclaim, §E). if (try readSrcConfig(gpa, src)) |oc_val| { var owned = oc_val; defer owned.deinit(gpa); dst.putConfig(owned.config) catch |e| return storagePutError(e); } // The repository shun table carries across verbatim, in persisted order. // Shun rows are the second never-interned frame kind, so — exactly like the // config record above — they are in neither `ridsInPersistedOrder` nor any // content path below, and a pack that did not re-emit them would silently // DROP the whole set. That is fail-OPEN on the one predicate the set exists // to make durable: the tombstones vanish while the artifacts they name may // survive the repack. Preserving is not the §5 pack FILTER (dropping // shunned content is a separate, later decision) — it is what makes the set // durable across the repo's own compaction at all. for (src.shunList()) |row| { dst.putShun(row.hash, row.reason, row.epoch) catch |e| return storageShunError(e); } // Artifacts in offset order: every candidate base a frame picks precedes it // in the source, so it is already written into `dst` (its NEW offset known) // before the dependent that names it. const rids = try src.ridsInPersistedOrder(gpa); defer gpa.free(rids); // Priority base hints (source rid → ordered source base rids), from a first // pass over the manifests: a check-in manifest against its primary parent // manifest (§6.1), plus each file blob's same-path predecessors — the §6.2 // mlink analog. These are `pickBase`'s highest-priority candidates; the // chronological window (added by `putArtifactPacked`) fills the rest. Absent ⇒ // the window alone decides. var hints: std.AutoHashMapUnmanaged(logstore.Rid, PriorityHints) = .empty; defer hints.deinit(gpa); try deriveCandidateHints(gpa, src, rids, &hints); var cache = logstore.PlaintextCache.init(); defer cache.deinit(gpa); var deltas: usize = 0; for (rids) |src_rid| { // Non-phantom by construction (ridsInPersistedOrder filters phantoms), // so content is never null. A verbatim wire delta is decoded to full // bytes here, then re-deltified like everything else — its stored base is // not preserved. const content = (try srcContent(gpa, src, src_rid, &cache)) orelse continue; defer gpa.free(content); const digest = src.hashOf(src_rid); // §7.7 L1332/L1336-1338 — the reconstructed bytes MUST hash to the // artifact's name, recomputed BEFORE any frame is written. A lying frame // aborts the pack with the original store untouched. var name_buf: [2 * logstore.max_hash_len]u8 = undefined; const name = digestToWireName(digest, &name_buf); if (!artifact.verify(name, content)) return error.Corrupt; // Priority base hints translated to their NEW dst rids (every hint is an // earlier-offset frame, so it is already written into `dst`). An unheld // hint is dropped. Absent/all-unheld ⇒ the chronological window alone // decides. `putArtifactPacked` owns best-of-N + the accept threshold + // chain-depth policy — pack only offers the ordered hints and the §6.1 // monotonic floor. var pri_buf: [logstore.max_base_candidates]logstore.Rid = undefined; var pri_len: usize = 0; if (hints.get(src_rid)) |ph| { for (ph.slice()) |sb| { if (pri_len >= pri_buf.len) break; if (dst.lookup(src.hashOf(sb))) |db| { pri_buf[pri_len] = db; pri_len += 1; } } } // §6.1 monotonic-shrink guard: the new encoding is written only if it is // strictly smaller than the existing source frame; otherwise pack keeps // that frame verbatim (a compact wire delta fossil chose well is never // replaced by a larger re-encoding). This makes pack non-worsening // frame-by-frame with ONE acknowledged exception: `copySrcFrame`'s §3.3 // deep-chain fallback re-stores a KEPT delta as a full zlib frame when its // base's dst depth reaches the read cap, which can exceed the source // delta's span — reachable only once chains approach `max_delta_chain`. const floor_span = src.frameSpanOf(src_rid); const new_rid = switch (dst.putArtifactPacked(digest, content, pri_buf[0..pri_len], floor_span) catch |e| return storagePutError(e)) { .rewrote => |rid| rid, .kept => try copySrcFrame(gpa, src, dst, src_rid, content), }; if (dst.kindOf(new_rid) == .delta) deltas += 1; } // MANDATORY invariant sweep (§3): reconstruct EVERY packed artifact through // its NEW (possibly re-based) delta chain and re-hash it; a mismatch aborts // before the commit, so the source is never replaced by a store that fails // round-trip. `dst` is a discarded temp on this path. try packVerifyPacked(gpa, dst); dst.commit() catch return error.Storage; // fsync the new log before the caller renames it return .{ .artifacts = rids.len, .deltas = deltas, .bytes_before = src.size(), .bytes_after = dst.size(), }; } /// The §3 post-write invariant sweep: reconstruct every content artifact in the /// freshly written `dst` through its (possibly re-based) delta chain and confirm /// it hashes to its own name. A single mismatch is `error.Corrupt`, aborting the /// pack before its atomic rename — the source store is never replaced by a pack /// that fails byte-exact round-trip. A shared `PlaintextCache` bounds the /// re-reconstruction cost over deep chains. fn packVerifyPacked(gpa: std.mem.Allocator, dst: *logstore.LogStore) PackError!void { var cache = logstore.PlaintextCache.init(); defer cache.deinit(gpa); const rids = try dst.ridsInPersistedOrder(gpa); defer gpa.free(rids); for (rids) |rid| { const content = (try srcContent(gpa, dst, rid, &cache)) orelse continue; defer gpa.free(content); var name_buf: [2 * logstore.max_hash_len]u8 = undefined; const name = digestToWireName(dst.hashOf(rid), &name_buf); if (!artifact.verify(name, content)) return error.Corrupt; } } /// `src.readConfig` narrowed to pack's error set (a config frame that will not /// decode is `Corrupt`, mirroring §7.7's abort-on-corrupt discipline). fn readSrcConfig(gpa: std.mem.Allocator, src: *logstore.LogStore) PackError!?logstore.OwnedConfig { return src.readConfig(gpa) catch |e| switch (e) { error.OutOfMemory => error.OutOfMemory, error.Storage => error.Storage, error.Corrupt => error.Corrupt, }; } /// `contentCached` narrowed to pack's error set. fn srcContent(gpa: std.mem.Allocator, src: *logstore.LogStore, rid: logstore.Rid, cache: *logstore.PlaintextCache) PackError!?[]u8 { return src.contentCached(gpa, rid, cache) catch |e| switch (e) { error.OutOfMemory => error.OutOfMemory, error.Storage => error.Storage, error.Corrupt => error.Corrupt, }; } /// Re-store `src_rid`'s EXISTING frame into `dst` verbatim — the §6.1 "kept" /// path, taken when no re-encoding beat the source frame's on-disk size. A full /// zlib container is copied byte-for-byte; a wire delta is copied byte-for-byte /// and re-pointed at its base's NEW dst frame (same base content, so the delta /// still reconstructs the same artifact); a raw frame re-stores its plaintext. /// Preserves fossil's compact base choice unbeaten where pack's re-deltify could /// not improve it. Offset order guarantees a delta's base is already packed; a /// base unexpectedly absent falls back to a full zlib store (§3.3 — never a /// dangling delta) rather than corrupt the packed store. fn copySrcFrame( gpa: std.mem.Allocator, src: *logstore.LogStore, dst: *logstore.LogStore, src_rid: logstore.Rid, content: []const u8, ) PackError!logstore.Rid { std.debug.assert(src_rid >= 1); // a valid 1-based source log rid std.debug.assert(content.len <= logstore.max_payload); // reconstructed artifact bytes const digest = src.hashOf(src_rid); const maybe_wire = src.wire(gpa, src_rid) catch |e| switch (e) { error.OutOfMemory => return error.OutOfMemory, error.Storage => return error.Storage, error.Corrupt => return error.Corrupt, }; // A `.raw` frame carries no stored container: re-store the plaintext as raw. var w = maybe_wire orelse return dst.put(digest, content) catch |e| return storagePutError(e); defer w.deinit(gpa); switch (w) { .full => |c| return dst.putContainer(digest, c) catch |e| return storagePutError(e), .delta => |d| { const base_rid = dst.lookup(d.baseHash()) orelse return dst.putZlib(digest, content) catch |e| return storagePutError(e); // Chain-depth safety: a verbatim copy re-links to the base's NEW rid, // whose dst depth can EXCEED the source's — pack may re-deltify a // source-full base into a delta (depth up to `delta_chain_write_max`), // so a kept delta stacked on it lands deeper than it was at the source. // Copy only while the base stays below the READ bound so the packed // chain still resolves within `max_delta_chain`; a base at/over the // bound falls back to full form, exactly as the clone persist path does // for a retained foreign chain (§3.3). Without this the post-write // invariant sweep (§3) would reject an over-deep chain as corrupt. if (dst.depthOf(base_rid) >= logstore.max_delta_chain) { return dst.putZlib(digest, content) catch |e| return storagePutError(e); } return dst.putDeltaContainer(digest, base_rid, d.container) catch |e| return storagePutError(e); }, } } /// Map a `putArtifact`/`putConfig` failure into pack's error set. fn storagePutError(e: logstore.PutError) PackError { return switch (e) { error.OutOfMemory => error.OutOfMemory, error.Storage => error.Storage, }; } /// Map a `putShun` failure into pack's error set. Every row re-emitted here came /// out of `src.shunList()`, so it has already satisfied both of `putShun`'s /// input rules — `scan` rejects a shun frame whose key is not a complete digest, /// and `parseShun` caps the reason at `max_shun_reason` — which makes `BadKey` /// and `ReasonTooLong` reachable only if the source's set disagrees with the /// rules its own reader enforces. That is source corruption, and it fails the /// repack rather than quietly dropping a tombstone. fn storageShunError(e: logstore.ShunError) PackError { return switch (e) { error.OutOfMemory => error.OutOfMemory, error.Storage => error.Storage, error.BadKey, error.ReasonTooLong => error.Corrupt, }; } /// Walk the source manifests once, filling `hints` with each artifact's ordered /// base candidates: /// * a check-in manifest → its primary parent manifest (§6.1 lineage); /// * a file blob → its same-path predecessors, nearest first (§6.2, fossil's /// mlink-biased `N_NEIGHBOR` analog — rebuild.c:501,508). /// Same-path knowledge is derived HERE, inside pack (the manifest-parsing layer), /// and handed to `pickBase` as plain `[]Rid`: no path concept leaks into the /// logstore policy. Every recorded predecessor precedes its blob in offset order /// (the two-pass offset sort guarantees it), so a chosen base is always already /// written into `dst` before its dependent. A non-manifest / unparseable artifact /// contributes no lineage hint. Only memory/storage/corrupt frame faults escape. fn deriveCandidateHints( gpa: std.mem.Allocator, src: *logstore.LogStore, rids: []const logstore.Rid, hints: *std.AutoHashMapUnmanaged(logstore.Rid, PriorityHints), ) PackError!void { // Fresh map in, filled here: the caller hands a just-initialized `.empty` // hint map so every entry below is this walk's own. std.debug.assert(hints.count() == 0); // path → the distinct file-blob rids ever stored at that path, accumulated in // manifest-walk order (re-sorted to offset order in pass 2). Keys are gpa-owned // dups of the manifest arena's filename bytes; freed with the map below. var paths: std.StringHashMapUnmanaged(std.ArrayListUnmanaged(logstore.Rid)) = .empty; defer { var it = paths.iterator(); while (it.next()) |e| { e.value_ptr.deinit(gpa); gpa.free(e.key_ptr.*); } paths.deinit(gpa); } // Pass 1: manifest → lineage parent, and accumulate path → blob rids. var cache = logstore.PlaintextCache.init(); defer cache.deinit(gpa); for (rids) |m_rid| { const bytes = (try srcContent(gpa, src, m_rid, &cache)) orelse continue; defer gpa.free(bytes); // Only a check-in manifest carries F/P lineage; a blob (or a control // artifact) fails the parse or is a different kind and contributes none. // Read path: pack reads stored (fossil-origin) manifests, so it tolerates // the legacy F-card trailing separator. var loaded = artifact.parseForImport(gpa, bytes) catch |e| switch (e) { error.OutOfMemory => return error.OutOfMemory, else => continue, }; defer loaded.deinit(gpa); const m = &loaded.manifest; if (m.kind != .manifest) continue; // manifest → primary parent manifest (§6.1). The parent frame precedes // this one in offset order, so it is packed before it. if (primaryParentRid(src, m)) |parent_rid| { try pushHint(gpa, hints, m_rid, parent_rid); } // §6.2: record each F-card's blob under its path. A delete F-card (no // hash), an unresolvable hash, or a phantom blob contributes nothing — // a phantom is skipped so pass 2's offset sort never queries a frameless // rid. for (m.f) |fc| { const bh = fc.hash orelse continue; const digest = hexToDigest(bh) orelse continue; const blob_rid = src.lookup(&digest) orelse continue; if (src.isPhantom(blob_rid)) continue; try recordPathBlob(gpa, &paths, fc.filename, blob_rid); } } // Pass 2: per path, sort its versions by offset and give each blob its nearest // same-path predecessors (offset below), nearest first — ahead of whatever the // chronological window later adds. var vit = paths.valueIterator(); while (vit.next()) |list| { std.mem.sort(logstore.Rid, list.items, src, blobOffsetLessThan); // A path that reverts to a byte-identical earlier blob (A→B→A) records the // SAME content-addressed rid more than once, non-consecutively — // `recordPathBlob` dedups only the consecutive run, so a revert slips a // duplicate past it. The offset sort makes those duplicates adjacent (an // identical rid has an identical offset), so one linear pass compacts // `versions` to its DISTINCT rids. Without it the predecessor walk below // would offer a blob as its OWN same-path base: equal offsets fail the // strict offset-below invariant (:362) and `pushHint`'s `rid != base` // assert (:379) — a blob is never its own predecessor. const versions = dedupSortedRids(list.items); for (versions, 0..) |blob_rid, k| { // Nearest predecessor first: k-1, k-2, … (all offset-below `blob_rid`). // Bounded to `max_base_candidates`: only that many hints are ever kept // (`PriorityHints.push` no-ops past the buffer), so a deep same-path // history costs O(N·5), never O(N²). var j: usize = k; var taken: usize = 0; while (j > 0 and taken < logstore.max_base_candidates) { j -= 1; taken += 1; // Load-bearing invariant: an offset-sorted `versions` puts every // predecessor (index below `k`) strictly earlier in the log than // `blob_rid`, so a chosen base is always already packed into `dst` // before its dependent. Assert it at the point of use. std.debug.assert(src.offsetOf(versions[j]) < src.offsetOf(blob_rid)); try pushHint(gpa, hints, blob_rid, versions[j]); } } } } /// Append `base` to `rid`'s ordered hint list (creating it on first use). The /// list is bounded and self-deduping (`PriorityHints.push`), so this is a no-op /// once the buffer is full — the loops that call it need no separate cap. fn pushHint( gpa: std.mem.Allocator, hints: *std.AutoHashMapUnmanaged(logstore.Rid, PriorityHints), rid: logstore.Rid, base: logstore.Rid, ) PackError!void { std.debug.assert(rid >= 1 and base >= 1); // both are valid 1-based log rids std.debug.assert(rid != base); // a frame is never its own base hint const gop = hints.getOrPut(gpa, rid) catch return error.OutOfMemory; if (!gop.found_existing) gop.value_ptr.* = .{}; gop.value_ptr.push(base); } /// Record `blob_rid` under `path` in `paths`, owning a dup of the filename bytes /// on first sight of a path. A file unchanged across consecutive check-ins /// references the same blob; that run is deduped here (only a change from the /// last-recorded version appends), and `PriorityHints.push` dedups any residual /// repeat. Only OOM escapes. fn recordPathBlob( gpa: std.mem.Allocator, paths: *std.StringHashMapUnmanaged(std.ArrayListUnmanaged(logstore.Rid)), path: []const u8, blob_rid: logstore.Rid, ) PackError!void { std.debug.assert(path.len > 0); // an F-card filename is never empty std.debug.assert(blob_rid >= 1); // a valid 1-based log rid const gop = paths.getOrPut(gpa, path) catch return error.OutOfMemory; if (!gop.found_existing) { // The slot's key is still the caller's borrowed `path`. Own it before any // later free can touch it; on a dupe OOM, drop the half-inserted slot so // the map's teardown never frees borrowed bytes or reads an undefined list. const owned = gpa.dupe(u8, path) catch { std.debug.assert(paths.remove(path)); return error.OutOfMemory; }; gop.key_ptr.* = owned; gop.value_ptr.* = .empty; } const list = gop.value_ptr; if (list.items.len > 0 and list.items[list.items.len - 1] == blob_rid) return; list.append(gpa, blob_rid) catch return error.OutOfMemory; } /// Compact an offset-sorted rid slice to its DISTINCT rids IN PLACE, returning /// the leading distinct prefix. A same-path history that reverts to a /// byte-identical earlier blob (A→B→A) records one content-addressed rid more /// than once; the offset sort makes those duplicates adjacent (an identical rid /// has an identical offset), so a single linear pass drops them. Keeps the pass-2 /// predecessor walk from ever offering a blob as its own same-path base — a blob /// is never its own predecessor. fn dedupSortedRids(sorted: []logstore.Rid) []logstore.Rid { if (sorted.len == 0) return sorted; var w: usize = 1; // count of distinct rids kept in the leading prefix var i: usize = 1; while (i < sorted.len) : (i += 1) { std.debug.assert(sorted[i] >= 1); // a valid 1-based log rid if (sorted[i] != sorted[w - 1]) { sorted[w] = sorted[i]; w += 1; } } return sorted[0..w]; } /// Offset order over two non-phantom blob rids — pass 2's sort key. Ascending /// offset means earlier-written first, so a blob's predecessors (positions below /// it) are exactly the same-path versions already packed when it is reached. fn blobOffsetLessThan(src: *logstore.LogStore, a: logstore.Rid, b: logstore.Rid) bool { // Contract: two non-phantom blob rids. `offsetOf` re-asserts range and // non-phantom internally; state the boundary contract here explicitly. std.debug.assert(a >= 1 and b >= 1); std.debug.assert(!src.isPhantom(a) and !src.isPhantom(b)); return src.offsetOf(a) < src.offsetOf(b); } /// The log rid of a manifest's primary (first) parent, or null when it is a /// root or the parent is not held. `parents` is the space-separated P-card /// argument (artifact §4.7.13); the first token is the merge-primary parent. fn primaryParentRid(src: *logstore.LogStore, m: *const artifact.Manifest) ?logstore.Rid { if (m.p.len == 0) return null; var it = m.p[0].hashes(); const first = it.next() orelse return null; const digest = hexToDigest(first) orelse return null; return src.lookup(&digest); } /// Decodes a 64-hex artifact name into its 32-byte binary digest — the log-frame /// key. Returns null for a non-64-hex string, so a caller maps it to a corrupt /// diagnostic rather than trapping. Byte-identical to main.zig's `hexToDigest`; /// pack keeps its own copy so it needs no import above the store layer. fn hexToDigest(hex: []const u8) ?[digest_len]u8 { if (hex.len != digest_len * 2) return null; var d: [digest_len]u8 = undefined; _ = std.fmt.hexToBytes(&d, hex) catch return null; return d; } /// Lowercase-hex-encodes a raw binary `digest` into `out`, returning the used /// prefix — the log-frame key's wire NAME (sync §3.3). Byte-identical to /// main.zig's `digestToWireName`. fn digestToWireName(digest: []const u8, out: *[2 * digest_len]u8) []const u8 { std.debug.assert(digest.len <= digest_len); const hexdigits = "0123456789abcdef"; for (digest, 0..) |b, i| { out[2 * i] = hexdigits[b >> 4]; out[2 * i + 1] = hexdigits[b & 0x0f]; } return out[0 .. 2 * digest.len]; } // --------------------------------------------------------------------------- // Tests. The broad pack-transform coverage (content equality, re-deltification, // config survival) lives with the CLI wiring in cmd/sync.zig; what belongs HERE // is the property that has no other home — that pack preserves the frame kinds // it does not rewrite. // --------------------------------------------------------------------------- const storage = @import("storage.zig"); test "pack: the repository shun table survives a repack" { // Shun rows are never interned, so they are in neither `readSrcConfig` nor // `ridsInPersistedOrder` — the two sources `packLog` rebuilds `dst` from. // A pack that did not re-emit them would silently DROP the entire durable // set: `zit pack` writes a new generation and swaps it in, so the // tombstones would be gone for good while the artifacts they name may // survive the repack. Fail-open on exactly the predicate the set exists to // make durable, with no error and no warning. const gpa = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const io = std.testing.io; const first = "s" ** 32; const second = "t" ** 20; // the other complete-name width const sfile = try tmp.dir.createFile(io, "src", .{ .read = true }); defer sfile.close(io); var sfs = storage.FileStorage.init(io, sfile, 0); var src = try logstore.LogStore.open(gpa, sfs.storageFile(), 0); defer src.deinit(gpa); // A real content-addressed artifact: `packLog` §7.7-verifies every source // frame, so the store must hold a genuinely named one for the shun rows to // be preserved ALONGSIDE content rather than in a degenerate empty pack. const held = "held content that the pack will reconstruct and re-hash"; var held_digest: [digest_len]u8 = undefined; std.crypto.hash.sha3.Sha3_256.hash(held, &held_digest, .{}); _ = try src.putArtifact(&held_digest, held, null); try src.putShun(first, "takedown notice 4711", 1753000000); try src.putShun(second, "", 1753000001); try src.commit(); const dfile = try tmp.dir.createFile(io, "packed", .{ .read = true, .truncate = true }); defer dfile.close(io); var dfs = storage.FileStorage.init(io, dfile, 0); var dst = try logstore.LogStore.open(gpa, dfs.storageFile(), src.hash_kind); defer dst.deinit(gpa); _ = try packLog(gpa, &src, &dst); // Present in the packed generation, in persisted order, fields intact. try std.testing.expect(dst.isShunned(first)); try std.testing.expect(dst.isShunned(second)); const list = dst.shunList(); try std.testing.expectEqual(@as(usize, 2), list.len); try std.testing.expectEqualStrings(first, list[0].hash); try std.testing.expectEqualStrings("takedown notice 4711", list[0].reason); try std.testing.expectEqual(@as(i64, 1753000000), list[0].epoch); try std.testing.expectEqualStrings(second, list[1].hash); try std.testing.expectEqual(@as(i64, 1753000001), list[1].epoch); // ...and still there once the packed file is reopened from its bytes. var rfs = storage.FileStorage.init(io, dfile, dst.size()); var reopened = try logstore.LogStore.open(gpa, rfs.storageFile(), 0); defer reopened.deinit(gpa); try std.testing.expectEqual(@as(usize, 2), reopened.shunList().len); try std.testing.expect(reopened.isShunned(first)); // A tombstone is still not content: it takes no rid in the packed log. try std.testing.expect(reopened.lookup(first) == null); }