//! 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.
pub const ResolveError = error{ NotFound, Ambiguous };
/// 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 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;
}
/// A parsed manifest bundled with the uncompressed bytes its cards borrow —
/// `artifact.parse` slices the source rather than duping it, so the bytes
/// must outlive the manifest. `deinit` frees both.
pub const OwnedManifest = struct {
bytes: []u8,
manifest: artifact.Manifest,
pub fn deinit(self: *OwnedManifest, gpa: std.mem.Allocator) void {
self.manifest.deinit();
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.
/// 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 m = artifact.parse(gpa, bytes) catch |e| switch (e) {
error.OutOfMemory => return error.OutOfMemory,
else => return error.Corrupt,
};
return .{ .bytes = bytes, .manifest = m };
}
/// 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,
}
}