Artifact a9a6ac57a7c2bc2ef66eff1a043d0c58a5c69aa9c9d8f835c51fed3f48a8d456:
- File
src/repository.zig
— part of check-in
[b503efb9f2]
at
2026-07-19 21:36:07
on branch iss7
— timeline: read fossil-legacy check-in manifests instead of dropping them
Repository.manifest parsed check-in manifests through the strict reference parser, which rejects fossil's own 2008 cvs2fossil-era manifests: their F cards carry a dangling trailing space after the hash, where the §4.2 perm token (1*tchar) permits none. On a large fossil-origin repository this made `zit timeline` stop at the first such check-in and exit non-zero, silently losing that check-in and every older one — a functional loss of history, surfaced as a misleading "input/output error".
Parse through parseForImport, the same read-path interop the diff baseline already relies on: the strict parser still runs first and stays the sole grammar oracle; the trailing-separator tolerance engages only when an otherwise well-framed, Z-valid artifact is rejected. OwnedManifest now also owns the normalized copy the manifest borrows when that tolerance fires. (user: dmestas, size: 34063) [annotate] [blame] [check-ins using] [more...]
//! zit — the `Repository` facade: one deep module for opening a repository and //! answering the derived and content questions every read verb asks. //! //! The north star names this the public repo surface. It folds the open //! sequence that each read verb used to hand-wire — open `.zit`, take the O(1) //! `.zit-cache` fast path (repo-state §1 memo) or derive the crosslinked //! `RepoState` from the log, close the file — behind `open`, and serves the //! derived queries (`resolve`, `branchOf`, `state`, `currentCheckoutRid`) plus //! the lazily-opened content path (`manifest`, `blob`) that `ls`/`cat` lean on. //! //! Determinism seam (design-seams rule): this module touches no clock, RNG, //! ambient cwd, or network. Discovery of the checkout root — which reads the //! process cwd — stays in the CLI; `open` operates on an already-discovered //! `root_dir` and the injected `std.Io`. The log is reopened lazily only when a //! content request arrives, so view-only verbs keep the O(1) cache fast path. //! //! Pinning: `state` (inside the loaded `RepoView`) borrows the view's own //! `MemStore`, and a lazily-opened `LogStore` borrows this struct's inline //! `FileStorage`. `open` therefore fills the caller's `Repository` in place //! (`self: *Repository`); a filled `Repository` must not be moved. const std = @import("std"); const artifact = @import("artifact.zig"); const repostate = @import("repostate.zig"); const logstore = @import("logstore.zig"); const storage = @import("storage.zig"); const checkout = @import("checkout.zig"); const repoview = @import("repoview.zig"); const repoimage = @import("repoimage.zig"); const repocache = @import("repocache.zig"); /// The repository file, beside `.zit-checkout` in the checkout root. Shared with /// the CLI (re-exported from root.zig) so the one name has a single home. pub const repo_filename: []const u8 = ".zit"; /// The lazily-opened content channel: the reopened `.zit` handle, the /// `FileStorage` binding over it (pinned — the `LogStore` borrows it), and the /// scanned read-only log. Absent until the first `manifest`/`blob`/`logStore`. const Content = struct { file: std.Io.File, fs: storage.FileStorage, log: logstore.LogStore, }; pub const Repository = struct { io: std.Io, /// The checkout root, discovered and owned by the caller (the caller closes /// it after `deinit`). Borrowed here to reopen `.zit` for lazy content. root_dir: std.Io.Dir, /// The crosslinked derived view, loaded in place by `open`. `state` borrows /// `&view.store`, so this whole struct is caller-pinned. view: repoview.RepoView, /// The content channel, opened lazily on the first content request. Held /// inline (pinned) because the `LogStore` borrows `&content.?.fs`. content: ?Content, /// The derived-query identity space: a `RepoState` rid (the view's store). pub const Rid = repostate.Rid; /// Options for `open`. `bypass_cache` skips the O(1) `.zit-cache` fast path /// and forces the authoritative full `openReadOnly` scan + derive (the /// `--no-cache` escape hatch); the derived view still rewrites a fresh cache /// afterward. pub const OpenOptions = struct { bypass_cache: bool = false, /// Which cache representation to serve. Defaults to `.full` (derive the /// complete `RepoState` — every query answerable), so every existing /// caller keeps today's behavior. The hot read verbs (`leaves` and the /// mtime-ordered listings) pass `.fast_read` to take the zero-parse /// image path, which answers their freed queries without re-deriving; /// the cold verbs (`info` ancestry, `branch`, `tag`, `ls`, `cat`) stay /// `.full` because they read adjacency and the full tag tables. mode: repocache.LoadMode = .full, }; /// Every failure the content-and-open surface can surface, as a single /// closed set the CLI switches exhaustively. `NotARepo`: no `.zit` under the /// given root. `CannotRead`: an I/O error opening/reading it. `Corrupt`: a /// structurally invalid log or a stored artifact whose content does not /// match its name (artifact §7.2). `OutOfMemory`: allocation failure. pub const OpenError = error{ NotARepo, CannotRead, Corrupt, OutOfMemory }; /// Name resolution failure (repo-state §8.3), forwarded unchanged from /// `RepoState.resolve` so the CLI keeps one exhaustive switch. /// `OutOfMemory` rides along because settling an equal-`event.mtime` tie by /// DAG ancestry allocates (repo-state `MtimeMaxPick`). pub const ResolveError = error{ NotFound, Ambiguous, OutOfMemory }; /// Discoverless open: `root_dir` is the already-found checkout root. Opens /// `.zit`, serves the view from the `.zit-cache` fast path or derives it /// from the log, then closes the file (the view is self-contained). On /// success the caller owns the value and must `deinit` it; on any error /// nothing is retained and `deinit` must not be called. pub fn open( self: *Repository, gpa: std.mem.Allocator, io: std.Io, root_dir: std.Io.Dir, opts: OpenOptions, ) OpenError!void { self.io = io; self.root_dir = root_dir; self.content = null; var file = root_dir.openFile(io, repo_filename, .{}) catch |e| return openFileError(e); defer file.close(io); const st = file.stat(io) catch return error.CannotRead; var fs = storage.FileStorage.init(io, file, st.size); // Fast path (repo-state §1 memo): a `.zit-cache` still valid against the // live log's tail serves the whole view in O(1), no full scan. A miss // is never an error — fall through to the authoritative derive. if (!opts.bypass_cache and repocache.tryLoad(gpa, io, root_dir, fs.storageFile(), &self.view, opts.mode)) return; var log = logstore.LogStore.openReadOnly(gpa, fs.storageFile()) catch |e| return openReadOnlyError(e); defer log.deinit(gpa); // Derive from the log, capturing the crosslink records, then materialize // the zero-parse image from the fresh derived state and rewrite the // cache atomically (best-effort — the derived view is correct either // way; a failed image serialize just stores an empty image section, so // the next `.fast_read` falls back to a derive rather than erroring). var body: std.ArrayList(u8) = .empty; defer body.deinit(gpa); self.view.loadCapturing(gpa, &log, &body) catch |e| return loadError(e); if (log.logKey()) |key| { var image: std.ArrayList(u8) = .empty; defer image.deinit(gpa); repoimage.serialize(gpa, &self.view.state, self.view.main_branch, &image) catch image.clearRetainingCapacity(); repocache.store(gpa, io, root_dir, key, self.view.main_branch, body.items, image.items); } else |_| {} } /// Frees the derived view and closes the lazy content channel if it was /// opened. Does NOT close `root_dir` — the caller that discovered it owns /// its lifetime. Call only on a `Repository` that `open` returned `ok`. pub fn deinit(self: *Repository, gpa: std.mem.Allocator) void { if (self.content) |*c| { c.log.deinit(gpa); c.file.close(self.io); } self.view.deinit(gpa); self.* = undefined; } /// Eagerly materialize the `.zit-cache` for the repository at `root_dir` so /// the first read verb after a fresh clone takes the zero-parse image fast /// path (repo-state §7.1 precomputed leaf table) instead of paying the full /// cold derive. It runs `open`'s own read-miss path — derive from the log, /// serialize the image, store the cache — purely for that documented side /// effect, then discards the view; there is no new derivation path, so /// nothing is added to the derivation oracle DST already proves. /// /// Best-effort by construction (repo-state §1 memo / §7.3 rebuild fallback): /// a `.zit-cache` is disposable — deleting it never changes an answer — so /// any failure to open, derive, or write is swallowed. A caller that cannot /// warm the cache (unreadable `.zit`, OOM, a read-only directory) is left /// exactly as before: correct, with the first read deriving lazily. Warming a /// repository that already has a valid cache is a cheap hit-and-discard. pub fn warmCache(gpa: std.mem.Allocator, io: std.Io, root_dir: std.Io.Dir) void { var repo: Repository = undefined; // The miss path inside `open` writes the cache (image + records); on any // error nothing is retained, so there is nothing to deinit. repo.open(gpa, io, root_dir, .{}) catch return; repo.deinit(gpa); } /// The derived `RepoState` — the leaf table, branch/tag derivations, event /// times, and ancestry (repo-state §6-9). The view already hides the derive; /// exposing the query surface directly keeps this facade from becoming a /// shallow pass-through over a dozen `RepoState` methods. pub fn state(self: *const Repository) *const repostate.RepoState { return &self.view.state; } /// The repo-state §6.1 default-branch name (config `main-branch` when set, /// else `"trunk"`) — the fallback `branchOf` threads. pub fn mainBranch(self: *const Repository) []const u8 { return self.view.main_branch; } /// The lowercase-hex artifact name of a derived rid — what a verb prints and /// compares against the current checkout. pub fn hashOf(self: *Repository, rid: Rid) []const u8 { return self.view.hashOf(rid); } /// The derived rid for a lowercase-hex artifact name, or null when the name /// was never interned in this view. pub fn lookup(self: *Repository, hash: []const u8) ?Rid { return self.view.lookup(hash); } /// Resolve a symbolic name / id / prefix to a rid via the repo-state §8.3 /// procedure (recency-based, §8.2 — closed-ness and leaf-ness never filter /// the candidate). Errors forwarded unchanged so the CLI switches once. pub fn resolve(self: *const Repository, name: []const u8, want: repostate.RepoState.NameType) ResolveError!Rid { return self.view.state.resolve(name, want); } /// The active-tag branch of `rid` (repo-state §6.1), config `main-branch` as /// the fallback. Folds the fallback so callers never pass it — precondition: /// `rid` bears an event (`state.mtimeOf(rid) != null`). pub fn branchOf(self: *const Repository, rid: Rid) []const u8 { return self.view.state.branchOf(rid, self.view.main_branch); } /// The active display-override value of tag `name` on `rid` (repo-state /// §5.2), or `fallback` when no override is live. A `zit amend` control /// artifact's singleton override lands in `tagxref` on the target rid at the /// next read (repo-state §4/§5), so the render sites read it here without any /// override-precedence logic of their own. General over the tag name — one /// accessor serves the comment/user/date overrides. pub fn override(self: *const Repository, rid: Rid, name: []const u8, fallback: []const u8) []const u8 { return self.view.state.activeTagValue(rid, name) orelse fallback; } /// Merge base of `a` and `b` over full ancestry: the common ancestor with the /// greatest event mtime, ties broken by the least artifact hash, or null when /// they share no ancestor (repo-state `mergeBaseOf`; ambiguities.md /// `merge-base`). A thin pass-through over the one general-purpose query. pub fn mergeBaseOf(self: *const Repository, gpa: std.mem.Allocator, a: Rid, b: Rid) error{OutOfMemory}!?Rid { return self.view.state.mergeBaseOf(gpa, a, b); } /// Full-ancestry membership: whether `ancestor` is in the full ancestry of /// `descendant` (§3.3, merge parents included, self-inclusive). mtime- /// independent, unlike `mergeBaseOf` — the authoritative up-to-date test the /// `merge` verb keys the B-merge-16 refusal on. A thin pass-through. pub fn isAncestorOf(self: *const Repository, gpa: std.mem.Allocator, ancestor: Rid, descendant: Rid) error{OutOfMemory}!bool { return self.view.state.isAncestorOf(gpa, ancestor, descendant); } /// The full ancestry of `rid` as a slice (§3.3, merge parents included, /// self-inclusive), the general-purpose query `annotate` builds its version /// chain over. A thin pass-through over the one shared ancestry walk — the /// verb owns no ancestry traversal of its own. Caller frees the slice. pub fn ancestorsOf(self: *const Repository, gpa: std.mem.Allocator, rid: Rid) error{OutOfMemory}![]Rid { return self.view.state.ancestorsOf(gpa, rid); } /// The full ancestry of `rid` (§3.3, merge parents included, self-inclusive) /// linearized into the annotate engine's processing order — a topological /// order in which every parent precedes its child (oldest first), so /// same-mtime and non-monotone parent/child pairs never invert. A thin /// pass-through over the one shared linearization; the verb owns no ancestry /// ordering of its own. Caller frees the slice. pub fn ancestryLinearized(self: *const Repository, gpa: std.mem.Allocator, rid: Rid) error{OutOfMemory}![]Rid { return self.view.state.ancestryLinearized(gpa, rid); } /// The bisect candidate span and next probe over full ancestry (repo-state /// `bisectSpan`; ambiguities.md `bisect-midpoint`): candidate set = /// ancestors(bad-anchor) minus every good's ancestry minus skipped; probe = /// lower median of untested candidates by ascending mtime, ties least hash; /// culprit = mtime-earliest marked-bad. Refuses `InvertedMarks` / /// `UnrelatedHistories`. A thin pass-through over the one general-purpose /// query — the bisect verb owns no ancestry walk of its own. pub fn bisectSpan( self: *const Repository, gpa: std.mem.Allocator, bad: []const Rid, good: []const Rid, skipped: []const Rid, ) repostate.RepoState.BisectError!repostate.RepoState.BisectOutcome { return self.view.state.bisectSpan(gpa, bad, good, skipped); } /// The current checkout's crosslinked rid, or null when there is nothing to /// mark: no `.zit-checkout`, an unborn checkout, a version this view never /// interned, or an interned-but-eventless phantom (repo-state §7.5). The /// `mtimeOf != null` guard is the postcondition `branchOf`'s callers lean on /// — a returned rid always bears an event. pub fn currentCheckoutRid(self: *Repository) ?Rid { var ck_buf: [checkout.max_ck_payload + 1]u8 = undefined; const ck = (checkout.read(self.io, self.root_dir, &ck_buf) catch return null) orelse return null; if (ck.version.len == 0) return null; const rid = self.view.lookup(ck.version) orelse return null; if (self.view.state.mtimeOf(rid) == null) return null; return rid; } /// The lazily-opened read-only log for content reads. Reopens `.zit` (closed /// after the view derive) on first use and memoizes it for the repository's /// lifetime; subsequent calls return the same store. pub fn logStore(self: *Repository, gpa: std.mem.Allocator) OpenError!*logstore.LogStore { if (self.content) |*c| return &c.log; var file = self.root_dir.openFile(self.io, repo_filename, .{}) catch |e| return openFileError(e); errdefer file.close(self.io); const st = file.stat(self.io) catch return error.CannotRead; self.content = .{ .file = file, .fs = storage.FileStorage.init(self.io, file, st.size), .log = undefined, }; self.content.?.log = logstore.LogStore.openReadOnly(gpa, self.content.?.fs.storageFile()) catch |e| { self.content = null; // discard the half-built channel; errdefer closes `file` return openReadOnlyError(e); }; return &self.content.?.log; } /// The uncompressed content of the artifact named `name` (a lowercase-hex /// id), or null when no artifact of that name is stored. The bytes are /// verified to hash to `name` (artifact §7.2, hash-over-uncompressed) — a /// content/name mismatch or an undecodable frame is `Corrupt`. Caller frees /// the returned slice. pub fn blob(self: *Repository, gpa: std.mem.Allocator, name: []const u8) OpenError!?[]u8 { var digest_buf: [logstore.max_hash_len]u8 = undefined; const digest = decodeName(name, &digest_buf) orelse return null; // undecodable ⇒ never stored const log = try self.logStore(gpa); const rid = log.lookup(digest) orelse return null; const bytes = (log.content(gpa, rid) catch |e| return contentError(e)) orelse return null; errdefer gpa.free(bytes); if (!hashMatches(digest, bytes)) return error.Corrupt; // §7.2 return bytes; } /// Resolving an artifact-name prefix: the open set plus `Ambiguous` (a /// prefix matching more than one stored artifact — repo-state §8.3 item 8, /// never silently narrowed). A prefix matching nothing is `null`, not an /// error — the not-found family the CLI renders as a plain refusal. pub const ResolveArtifactError = OpenError || error{Ambiguous}; /// Resolve a lowercase-hex artifact-name prefix (full or abbreviated) to /// the unique full lowercase-hex name of the stored artifact it addresses, /// written into `out` and returned as a sub-slice. Queries the LogStore — /// the one store holding EVERY stored artifact, file blobs included (the /// derived `repostate` interns only check-in/parent phantoms, which carry /// no content), so a blob prefix resolves here where a check-in-scoped name /// lookup would miss it. `null` = no match (not-found); `error.Ambiguous` = /// the §8.3-item-8 multi-match refusal. Hides the LogStore rid space from /// the caller, who sees only hashes. Precondition on `prefix` is the /// LogStore's: 4..64 lowercase-hex bytes (the CLI canonicalizes first). pub fn resolveArtifactPrefix( self: *Repository, gpa: std.mem.Allocator, prefix: []const u8, out: *[2 * logstore.max_hash_len]u8, ) ResolveArtifactError!?[]const u8 { const log = try self.logStore(gpa); const maybe_rid = log.resolveHexPrefix(prefix) catch |e| switch (e) { error.Ambiguous => return error.Ambiguous, }; const rid = maybe_rid orelse return null; const raw = log.hashOf(rid); std.debug.assert(raw.len <= logstore.max_hash_len); const lower = "0123456789abcdef"; for (raw, 0..) |b, i| { out[i * 2] = lower[b >> 4]; out[i * 2 + 1] = lower[b & 0x0f]; } return out[0 .. raw.len * 2]; } /// A parsed manifest bundled with the uncompressed bytes its cards borrow — /// the parser slices its source rather than duping it, so that source must /// outlive the manifest. `bytes` is the reconstructed artifact (always /// owned); `normalized` is non-null only when the read-path legacy /// trailing-separator tolerance fired, in which case the manifest's cards /// borrow `normalized` instead of `bytes` (§4.2 read-interop). `deinit` /// frees all held allocations. pub const OwnedManifest = struct { bytes: []u8, normalized: ?[]u8 = null, manifest: artifact.Manifest, pub fn deinit(self: *OwnedManifest, gpa: std.mem.Allocator) void { self.manifest.deinit(); if (self.normalized) |n| gpa.free(n); gpa.free(self.bytes); self.* = undefined; } }; /// The parsed manifest of the check-in `rid`, or null when its content is /// missing. `Corrupt` when the stored bytes are not a parseable artifact. /// Parses through `parseForImport`, not the strict reference `parse`, so a /// fossil-origin check-in carrying the legacy dangling F-card separator /// (fossil's own 2008 cvs2fossil history) reads instead of being rejected — /// the same read-path interop the diff baseline already relies on. Caller /// `deinit`s the returned `OwnedManifest`. pub fn manifest(self: *Repository, gpa: std.mem.Allocator, rid: Rid) OpenError!?OwnedManifest { const bytes = (try self.blob(gpa, self.view.hashOf(rid))) orelse return null; errdefer gpa.free(bytes); const im = artifact.parseForImport(gpa, bytes) catch |e| switch (e) { error.OutOfMemory => return error.OutOfMemory, else => return error.Corrupt, }; return .{ .bytes = bytes, .normalized = im.normalized, .manifest = im.manifest }; } /// Every failure `files` can surface: the open/content set plus the three /// §8 enumeration faults. `NotManifest`: the resolved artifact is not a /// check-in manifest (artifact §5.1). `BaselineUnresolvable` (§8.3): a delta /// check-in whose B-card baseline cannot be located, is not a manifest, or /// is unparseable — the hard-failure discipline §8.3 mandates for the /// file-enumeration entry points. `BaselineIsDelta` (§4.7.3): the resolved /// baseline is itself a delta manifest (delta-of-delta, rejected). pub const FilesError = OpenError || error{ NotManifest, BaselineUnresolvable, BaselineIsDelta }; /// The effective file set of one check-in, retaining every manifest whose /// F cards the walk borrows. A baseline check-in keeps only its own manifest /// (`baseline` null); a delta check-in additionally keeps the eagerly-read /// baseline, since the §8.2 merge walk borrows both card lists. Build it with /// `files`, read it with `iterator`, free it with `deinit`. pub const FileList = struct { target: OwnedManifest, baseline: ?OwnedManifest = null, /// The two pre-sorted (§8.1) F-card lists the §8.2 merge walk consumes: /// `base_files` is the baseline's set (the target's own set for a /// baseline check-in), `delta_files` the delta's authored cards (empty /// for a baseline check-in). Both borrow the retained manifests above, /// so they stay valid exactly as long as this `FileList`. base_files: []const artifact.FCard, delta_files: []const artifact.FCard, pub fn deinit(self: *FileList, gpa: std.mem.Allocator) void { if (self.baseline) |*b| b.deinit(gpa); self.target.deinit(gpa); self.* = undefined; } /// A fresh §8.2 merge walk over the retained card lists — allocation-free /// and repeatable, so a caller may enumerate more than once. pub fn iterator(self: *const FileList) EffectiveFiles { return .{ .walk = .{ .baseline = self.base_files, .delta = self.delta_files } }; } }; /// A §8.2 merge walk narrowed to the effective set: `next` yields each merged /// F card in canonical filename order (§4.5.3), suppressing the delete markers /// the raw walk emits verbatim (§8.2 step 3 / tail, §4.7.6) so every yielded /// card carries a hash. pub const EffectiveFiles = struct { walk: artifact.MergeWalk, pub fn next(self: *EffectiveFiles) ?artifact.FCard { while (self.walk.next()) |f| { if (f.hash != null) return f; // §4.7.6: a hash-less card is a delete marker } return null; } }; /// The effective file set of the check-in `rid` (artifact §8.2): its own F /// cards for a baseline manifest, or the §8.2 merge of the eagerly-resolved /// baseline with the delta's cards for a delta manifest. Returns null when the /// check-in's own content is missing (an interned phantom). The §8.3/§4.7.3 /// baseline policy stays in `artifact.enumerate`: this pre-reads only the one /// baseline the B card names and hands `enumerate` a resolver over it. Caller /// `deinit`s the returned `FileList`. pub fn files(self: *Repository, gpa: std.mem.Allocator, rid: Rid) FilesError!?FileList { var target = (try self.manifest(gpa, rid)) orelse return null; errdefer target.deinit(gpa); if (target.manifest.kind != .manifest) return error.NotManifest; // §5.1 // Eagerly resolve the one baseline the B card names (a delta manifest // only). A located, parseable check-in manifest becomes the resolver's // view; anything else leaves it null, so `enumerate` raises // BaselineUnresolvable under the §8.3 hard-failure discipline. var baseline: ?OwnedManifest = null; errdefer if (baseline) |*b| b.deinit(gpa); var view: ?artifact.ManifestFiles = null; if (target.manifest.b) |b| { if (try self.blob(gpa, b.hash)) |bytes| { if (parseBaseline(gpa, bytes)) |bm| { baseline = bm; view = manifestFilesOf(&baseline.?.manifest); } else |e| switch (e) { error.OutOfMemory => { gpa.free(bytes); return error.OutOfMemory; }, error.NotManifest => gpa.free(bytes), // §8.3: not a manifest ⇒ unresolvable } } } var eager = EagerBaseline{ .hash = if (target.manifest.b) |b| b.hash else null, .view = view, }; const walk = artifact.enumerate(manifestFilesOf(&target.manifest), .{ .ctx = &eager, .resolveFn = EagerBaseline.resolve, }) catch |e| switch (e) { error.BaselineUnresolvable => return error.BaselineUnresolvable, // §8.3 error.BaselineIsDelta => return error.BaselineIsDelta, // §4.7.3 }; // Postcondition: a delta check-in retained its baseline (else `enumerate` // would have raised BaselineUnresolvable above); a baseline check-in did // not read one. std.debug.assert((target.manifest.b != null) == (baseline != null)); return FileList{ .target = target, .baseline = baseline, .base_files = walk.baseline, .delta_files = walk.delta, }; } /// The effective F card for `name` in the check-in `rid` (artifact §8.4 /// single-name lookup), or null when `name` is not in the effective set — /// either never present, or a delta delete marker (§4.7.6) suppresses it. /// Exact-name only: §8.4's best-match (closest-name) form is deliberately /// not offered, so it is never a candidate to suppress here. /// /// §8.4's discipline — search the delta's own cards first, a delta delete /// marker reports not-found without consulting the baseline, and the /// baseline is searched only when the name is absent from the delta — is /// exactly the §8.2 merge the `files` walk already performs. This narrows /// that walk to one name rather than re-deriving the rules, so the §8.3/ /// §4.7.3 baseline faults surface unchanged. The returned card's slices are /// duplicated into `gpa` (independent of the walk's scratch, which is freed /// before returning); release it with `freeFileCard`. pub fn fileAt(self: *Repository, gpa: std.mem.Allocator, rid: Rid, name: []const u8) FilesError!?artifact.FCard { var list = (try self.files(gpa, rid)) orelse return null; defer list.deinit(gpa); var it = list.iterator(); while (it.next()) |f| { std.debug.assert(f.hash != null); // §4.7.6: the walk suppresses delete markers if (std.mem.eql(u8, f.filename, name)) return try dupFileCard(gpa, f); } return null; } /// Release a card returned by `fileAt` — its present slices were each /// duplicated into `gpa`. A no-op-safe companion: every field that was /// null in the source stays null and is skipped. pub fn freeFileCard(gpa: std.mem.Allocator, f: artifact.FCard) void { gpa.free(f.filename); if (f.hash) |h| gpa.free(h); if (f.perm) |p| gpa.free(p); if (f.old_name) |o| gpa.free(o); } }; /// Duplicate every present slice of `f` into `gpa`, yielding a card whose bytes /// outlive the merge walk's scratch. Partial-failure-safe: an OOM after any dup /// frees the slices already taken (`errdefer`), so no leak escapes. `mode` is a /// value, copied for free. Freed by `Repository.freeFileCard`. fn dupFileCard(gpa: std.mem.Allocator, f: artifact.FCard) error{OutOfMemory}!artifact.FCard { const filename = try gpa.dupe(u8, f.filename); errdefer gpa.free(filename); const hash = if (f.hash) |h| try gpa.dupe(u8, h) else null; errdefer if (hash) |h| gpa.free(h); const perm = if (f.perm) |p| try gpa.dupe(u8, p) else null; errdefer if (perm) |p| gpa.free(p); const old_name = if (f.old_name) |o| try gpa.dupe(u8, o) else null; return .{ .filename = filename, .hash = hash, .perm = perm, .old_name = old_name, .mode = f.mode }; } /// The §8.2 `ManifestFiles` view of a parsed manifest: its B-card hash (null for /// a baseline manifest, §8-p1) and its own F-card list (sorted at parse time). fn manifestFilesOf(m: *const artifact.Manifest) artifact.ManifestFiles { return .{ .baseline_hash = if (m.b) |b| b.hash else null, .files = m.f, }; } /// Parse `bytes` (borrowed) as the eagerly-read baseline of a delta check-in /// (§8.3). A successful parse of a check-in manifest becomes an `OwnedManifest` /// that takes ownership of `bytes`; a parse OOM propagates; any other outcome — /// a syntactically invalid artifact or one that is not a manifest — is /// `NotManifest`, the §8.3 "located artifact is not a manifest" case (the caller /// frees `bytes` and lets `enumerate` raise `BaselineUnresolvable`). fn parseBaseline(gpa: std.mem.Allocator, bytes: []u8) error{ OutOfMemory, NotManifest }!Repository.OwnedManifest { var m = artifact.parse(gpa, bytes) catch |e| switch (e) { error.OutOfMemory => return error.OutOfMemory, else => return error.NotManifest, }; if (m.kind != .manifest) { m.deinit(); return error.NotManifest; } return .{ .bytes = bytes, .manifest = m }; } /// The §8.3 baseline resolver for `files`: the baseline was already located and /// parsed (eagerly, exactly once), so `resolve` just hands back the pre-built /// view — null when the baseline was unresolvable, which `enumerate` turns into /// `BaselineUnresolvable`. `enumerate` calls it only for a delta manifest, always /// with that manifest's single B-card hash — asserted here as negative space. const EagerBaseline = struct { hash: ?[]const u8, view: ?artifact.ManifestFiles, fn resolve(ctx: *anyopaque, hash: []const u8) ?artifact.ManifestFiles { const self: *EagerBaseline = @ptrCast(@alignCast(ctx)); std.debug.assert(self.hash != null and std.mem.eql(u8, hash, self.hash.?)); return self.view; } }; /// Maps a `root_dir.openFile` failure: a missing `.zit` is "not a repository", /// every other open error is an I/O read failure. fn openFileError(e: anyerror) Repository.OpenError { return switch (e) { error.FileNotFound => error.NotARepo, error.OutOfMemory => error.OutOfMemory, else => error.CannotRead, }; } /// Maps `LogStore.openReadOnly`'s closed error set into the facade's set. fn openReadOnlyError(e: logstore.OpenError) Repository.OpenError { return switch (e) { error.OutOfMemory => error.OutOfMemory, error.Corrupt => error.Corrupt, error.Storage => error.CannotRead, }; } /// Maps `RepoView.load`'s closed error set into the facade's set. fn loadError(e: repoview.RepoView.LoadError) Repository.OpenError { return switch (e) { error.OutOfMemory => error.OutOfMemory, error.Corrupt => error.Corrupt, error.Storage => error.CannotRead, }; } /// Maps `LogStore.content`'s closed error set into the facade's set. fn contentError(e: logstore.ContentError) Repository.OpenError { return switch (e) { error.OutOfMemory => error.OutOfMemory, error.Corrupt => error.Corrupt, error.Storage => error.CannotRead, }; } /// Decode a lowercase-hex artifact name to its raw digest into `out`. Returns /// null for anything that cannot be a stored name — an odd length, a length /// past `max_hash_len*2`, or a non-hex digit — so the caller treats it as "not /// present" rather than trapping on wire input. fn decodeName(name: []const u8, out: *[logstore.max_hash_len]u8) ?[]const u8 { if (name.len == 0 or name.len % 2 != 0) return null; const bytes = name.len / 2; if (bytes > logstore.max_hash_len) return null; _ = std.fmt.hexToBytes(out[0..bytes], name) catch return null; return out[0..bytes]; } /// Whether `content` hashes to `digest` under the algorithm the digest length /// selects (artifact §7.2: 20 bytes ⇒ sha1, 32 bytes ⇒ sha3-256). A digest of /// any other length is not a name this build mints, so it cannot match. fn hashMatches(digest: []const u8, content: []const u8) bool { switch (digest.len) { 20 => { var out: [20]u8 = undefined; std.crypto.hash.Sha1.hash(content, &out, .{}); return std.mem.eql(u8, &out, digest); }, 32 => { var out: [32]u8 = undefined; std.crypto.hash.sha3.Sha3_256.hash(content, &out, .{}); return std.mem.eql(u8, &out, digest); }, else => return false, } }