Skip to content
zit Artifact [1f2e29d7e0]
Login

Artifact [1f2e29d7e0]

Artifact 1f2e29d7e0825d44842694b5a4c44f596379a65557b297ff17800e6983c33c07:


//! The diff cache: per file, the line diff `zit annotate` made of each edge
//! of its history, so a later walk over the same edges diffs none of them
//! and reads no version whose every edge is kept. An edge is a pair of blob
//! hashes, older then newer, and what is kept is `linediff.diff` of the two
//! under annotate's options: the copy/delete/insert triples. That diff is a
//! pure function of the two contents, which the hashes name, so replaying a
//! kept edge is the same computation; names, merges and `-n` windows never
//! enter it.
//!
//! Advisory like every `.zit-cache` member, and kept as the blame cache
//! (`blamecache`) keeps its files: one member per path, named by its hash,
//! the path in the header; keyed by the same `blamecache.fingerprint`;
//! immutable once written and replaced whole by a rename; and checked block
//! by block (`blockcrc`), so a lookup checks only what it reads. Any failure
//! is a miss, a damaged file is never trusted, and the write that follows
//! starts a fresh one.
//!
//! Layout (little-endian), header then body then the body's block CRC table:
//!
//!   header: magic | version u16 | engine u32 | fingerprint u32
//!           | n_edges u32 | body_len u32 | table_crc u32 | path_len u16
//!           | path | header_crc u32
//!   body:   n_edges × (parent_len u8 | parent, zero-padded to 64
//!                     | child_len u8 | child, zero-padded to 64
//!                     | at u32 | n_triples u32), sorted by those first
//!             130 bytes, so a lookup bisects
//!           | per edge, at `at`: n_triples × (copy u32 | delete u32 | insert u32)
//!           | zero padding to a whole block

const std = @import("std");
const crc32 = @import("crc32.zig");
const blockcrc = @import("blockcrc.zig");
const repocache = @import("repocache.zig");
const linediff = @import("linediff.zig");

const magic = "ZITDIFF\x00";
/// Bump on any change to the layout: a file written under another version
/// is a miss.
pub const version: u16 = 1;
/// A file past this is not written; edges that do not fit are diffed again.
pub const max_file_bytes: usize = 4 << 20;

pub const dirname: []const u8 = repocache.cache_dirname ++ "/diff";

const fixed_len = 8 + 2 + 4 + 4 + 4 + 4 + 4 + 2; // = 32, through path_len
const path_len_at = 30;
const key_len = 2 * 65;
const edge_len = key_len + 4 + 4; // = 138
const triple_len = 12;

/// What the kept triples are a function of besides the two contents: the
/// line diff's own version and the options it ran under. A file written
/// under another is a miss. `linediff.engine_version` must be bumped when
/// the engine's output changes for any input.
pub fn engineKey(opts: linediff.Options) u32 {
    const f = opts.flags;
    var bits: u32 = 0;
    for ([_]bool{ f.strip_eol_cr, f.ignore_eol_ws, f.ignore_all_ws, opts.optimize, opts.newline_significant }, 0..) |b, i| {
        bits |= @as(u32, @intFromBool(b)) << @intCast(i);
    }
    return @as(u32, linediff.engine_version) << 8 | bits;
}

/// One edge: the diff turning blob `parent`'s lines into blob `child`'s.
pub const Edge = struct {
    parent: []const u8,
    child: []const u8,
    triples: []const linediff.Triple,
};

pub const ReadError = error{ OutOfMemory, Corrupt };

/// The member a path's cache lives in: `dirname`/hex(wyhash(path)). The
/// full path is in the header, so a collision reads as a miss.
pub fn memberPath(buf: *[dirname.len + 1 + 16]u8, path: []const u8) []const u8 {
    const h = std.hash.Wyhash.hash(0, path);
    return std.fmt.bufPrint(buf, "{s}/{x:0>16}", .{ dirname, h }) catch unreachable;
}

/// Where a file's body starts, read from its header without checking it:
/// tests use it to damage one block at a time.
pub fn bodyStart(bytes: []const u8) usize {
    return fixed_len + @as(usize, readInt(u16, bytes, path_len_at)) + 4;
}

/// An edge's key: both hashes, each length-prefixed and zero-padded, so
/// keys compare as bytes. Null for a name that is not a hash.
fn keyOf(parent: []const u8, child: []const u8) ?[key_len]u8 {
    if (!isHash(parent) or !isHash(child)) return null;
    var k: [key_len]u8 = @splat(0);
    k[0] = @intCast(parent.len);
    @memcpy(k[1..][0..parent.len], parent);
    k[65] = @intCast(child.len);
    @memcpy(k[66..][0..child.len], child);
    return k;
}

fn isHash(h: []const u8) bool {
    return h.len == 40 or h.len == 64;
}

/// A parsed cache file over bytes it borrows. The header and the block
/// table are checked at parse; the body's blocks as they are read.
pub const File = struct {
    bytes: []const u8,
    verifier: blockcrc.Verifier,
    n_edges: u32,

    /// Null unless magic, version, engine, header CRC, block table CRC,
    /// `path` and `fp` all hold and the parts fill the file exactly.
    pub fn parse(gpa: std.mem.Allocator, bytes: []const u8, path: []const u8, fp: u32, engine: u32) error{OutOfMemory}!?File {
        if (bytes.len < fixed_len + 4 or bytes.len > max_file_bytes) return null;
        if (!std.mem.eql(u8, bytes[0..8], magic)) return null;
        if (readInt(u16, bytes, 8) != version) return null;
        const path_len = readInt(u16, bytes, path_len_at);
        const header_len = fixed_len + @as(usize, path_len) + 4;
        if (header_len > bytes.len) return null;
        if (crc32.Crc32.hash(bytes[0 .. header_len - 4]) != readInt(u32, bytes, header_len - 4)) return null;
        if (readInt(u32, bytes, 10) != engine) return null;
        if (readInt(u32, bytes, 14) != fp) return null;
        if (!std.mem.eql(u8, bytes[fixed_len..][0..path_len], path)) return null;
        const n_edges = readInt(u32, bytes, 18);
        const body_len: usize = readInt(u32, bytes, 22);
        if (body_len > bytes.len - header_len or body_len % blockcrc.block_len != 0) return null;
        if (@as(usize, n_edges) * edge_len > body_len) return null;
        const body = bytes[header_len..][0..body_len];
        const table = bytes[header_len + body_len ..];
        if (table.len != blockcrc.tableLen(body_len)) return null;
        if (crc32.Crc32.hash(table) != readInt(u32, bytes, 26)) return null;
        return .{ .bytes = bytes, .verifier = try blockcrc.Verifier.init(gpa, body, table), .n_edges = n_edges };
    }

    pub fn deinit(self: *File, gpa: std.mem.Allocator) void {
        self.verifier.deinit(gpa);
        self.* = undefined;
    }

    /// The triples kept for the edge `parent` → `child`, on `gpa`, or null
    /// when the file has none. A failed CRC or a record pointing outside
    /// the body is `Corrupt`, and every later read fails too: the file is
    /// no longer trusted.
    pub fn lookup(self: *File, gpa: std.mem.Allocator, parent: []const u8, child: []const u8) ReadError!?[]linediff.Triple {
        const want = keyOf(parent, child) orelse return null;
        var lo: usize = 0;
        var hi: usize = self.n_edges;
        while (lo < hi) {
            const mid = lo + (hi - lo) / 2;
            const rec = try self.span(mid * edge_len, edge_len);
            switch (std.mem.order(u8, rec[0..key_len], &want)) {
                .lt => lo = mid + 1,
                .gt => hi = mid,
                .eq => return try self.triplesOf(gpa, rec),
            }
        }
        return null;
    }

    /// Edge `k`, `k` below `n_edges`: its two hashes, borrowing the file,
    /// and its triples on `gpa`.
    pub fn edgeAt(self: *File, gpa: std.mem.Allocator, k: usize) ReadError!struct { key: [key_len]u8, parent: []const u8, child: []const u8, triples: []linediff.Triple } {
        std.debug.assert(k < self.n_edges);
        const rec = try self.span(k * edge_len, edge_len);
        // The length byte is checked before it bounds a slice: a CRC only
        // says the bytes are the ones written.
        for ([_]usize{ 0, 65 }) |at| if (rec[at] != 40 and rec[at] != 64) return error.Corrupt;
        return .{
            .key = rec[0..key_len].*,
            .parent = rec[1..][0..rec[0]],
            .child = rec[66..][0..rec[65]],
            .triples = try self.triplesOf(gpa, rec),
        };
    }

    fn triplesOf(self: *File, gpa: std.mem.Allocator, rec: []const u8) ReadError![]linediff.Triple {
        const at: usize = readInt(u32, rec, key_len);
        const n: usize = readInt(u32, rec, key_len + 4);
        if (n > self.verifier.body.len / triple_len) return error.Corrupt;
        const raw = try self.span(at, n * triple_len);
        const out = try gpa.alloc(linediff.Triple, n);
        for (out, 0..) |*t, i| t.* = .{
            .copy = readInt(u32, raw, i * triple_len),
            .delete = readInt(u32, raw, i * triple_len + 4),
            .insert = readInt(u32, raw, i * triple_len + 8),
        };
        return out;
    }

    fn span(self: *File, at: usize, n: usize) error{Corrupt}![]const u8 {
        if (!self.verifier.check(at, n)) return error.Corrupt;
        return self.verifier.body[at..][0..n];
    }
};

fn readInt(comptime T: type, bytes: []const u8, at: usize) T {
    return std.mem.readInt(T, bytes[at..][0..@sizeOf(T)], .little);
}

fn writeInt(comptime T: type, bytes: []u8, at: usize, v: T) void {
    std.mem.writeInt(T, bytes[at..][0..@sizeOf(T)], v, .little);
}

const Pending = struct { key: [key_len]u8, triples: []const linediff.Triple };

fn pendingLess(_: void, a: Pending, b: Pending) bool {
    return std.mem.order(u8, &a.key, &b.key) == .lt;
}

/// A file holding `new`, then as many of `prior`'s edges as
/// `max_file_bytes` allows, each edge once. Null when none of `new` can be
/// kept. A prior edge that fails its check is dropped, never carried under
/// a fresh CRC, and so is every one after it: the file is no longer
/// trusted. Caller frees.
pub fn encode(gpa: std.mem.Allocator, path: []const u8, fp: u32, engine: u32, new: []const Edge, prior: ?*File) error{OutOfMemory}!?[]u8 {
    if (path.len > std.math.maxInt(u16)) return null;
    // Every allocation here is sized up front and freed by name, not taken
    // from an arena: an arena's trips to `gpa` depend on whether the heap
    // can grow its last chunk in place, which makes the allocation count
    // differ from one heap to another.
    const prior_n: usize = if (prior) |p| p.n_edges else 0;
    var chosen = try std.ArrayList(Pending).initCapacity(gpa, new.len + prior_n);
    defer chosen.deinit(gpa);
    var seen: std.AutoHashMapUnmanaged([key_len]u8, void) = .empty;
    defer seen.deinit(gpa);
    try seen.ensureTotalCapacity(gpa, @intCast(new.len + prior_n));
    // The prior file's triples, read one edge at a time and freed together.
    var read = try std.ArrayList([]linediff.Triple).initCapacity(gpa, prior_n);
    defer {
        for (read.items) |t| gpa.free(t);
        read.deinit(gpa);
    }
    const header_len = fixed_len + path.len + 4;
    // Bytes the body needs so far, before its padding and table.
    var need: usize = 0;
    const fits = struct {
        fn f(header: usize, body: usize) bool {
            const padded = std.mem.alignForward(usize, body, blockcrc.block_len);
            return header + padded + blockcrc.tableLen(padded) <= max_file_bytes;
        }
    }.f;
    for (new) |e| {
        const k = keyOf(e.parent, e.child) orelse continue;
        if (seen.contains(k)) continue;
        const grown = need + edge_len + e.triples.len * triple_len;
        if (!fits(header_len, grown)) continue;
        seen.putAssumeCapacity(k, {});
        chosen.appendAssumeCapacity(.{ .key = k, .triples = e.triples });
        need = grown;
    }
    if (chosen.items.len == 0) return null;
    if (prior) |p| for (0..p.n_edges) |k| {
        const e = p.edgeAt(gpa, k) catch |err| switch (err) {
            error.OutOfMemory => return error.OutOfMemory,
            error.Corrupt => break,
        };
        read.appendAssumeCapacity(e.triples);
        if (seen.contains(e.key)) continue;
        const grown = need + edge_len + e.triples.len * triple_len;
        if (!fits(header_len, grown)) continue;
        seen.putAssumeCapacity(e.key, {});
        chosen.appendAssumeCapacity(.{ .key = e.key, .triples = e.triples });
        need = grown;
    };
    std.mem.sortUnstable(Pending, chosen.items, {}, pendingLess);

    const body_len = std.mem.alignForward(usize, need, blockcrc.block_len);
    if (body_len > std.math.maxInt(u32)) return null;
    const blob = try gpa.alloc(u8, header_len + body_len + blockcrc.tableLen(body_len));
    errdefer gpa.free(blob);
    @memset(blob, 0);
    @memcpy(blob[0..8], magic);
    writeInt(u16, blob, 8, version);
    writeInt(u32, blob, 10, engine);
    writeInt(u32, blob, 14, fp);
    writeInt(u32, blob, 18, @intCast(chosen.items.len));
    writeInt(u32, blob, 22, @intCast(body_len));
    writeInt(u16, blob, path_len_at, @intCast(path.len));
    @memcpy(blob[fixed_len..][0..path.len], path);
    const body = blob[header_len..][0..body_len];
    var at: usize = chosen.items.len * edge_len;
    for (chosen.items, 0..) |e, k| {
        const rec = body[k * edge_len ..][0..edge_len];
        @memcpy(rec[0..key_len], &e.key);
        writeInt(u32, rec, key_len, @intCast(at));
        writeInt(u32, rec, key_len + 4, @intCast(e.triples.len));
        for (e.triples) |t| {
            writeInt(u32, body, at, t.copy);
            writeInt(u32, body, at + 4, t.delete);
            writeInt(u32, body, at + 8, t.insert);
            at += triple_len;
        }
    }
    std.debug.assert(at == need);
    const table = blob[header_len + body_len ..];
    blockcrc.writeTable(body, table);
    writeInt(u32, blob, 26, crc32.Crc32.hash(table));
    writeInt(u32, blob, header_len - 4, crc32.Crc32.hash(blob[0 .. header_len - 4]));
    return blob;
}

/// A path's cache as found on disk: the mapping and the parse over it.
pub const Mapped = struct {
    io: std.Io,
    mm: std.Io.File.MemoryMap,
    file: File,

    pub fn deinit(self: *Mapped, gpa: std.mem.Allocator) void {
        self.file.deinit(gpa);
        self.mm.destroy(self.io);
        self.* = undefined;
    }
};

/// `path`'s cache under `dir`, mapped read-only, or null on any miss.
pub fn open(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir, path: []const u8, fp: u32, engine: u32) ?Mapped {
    var name_buf: [dirname.len + 1 + 16]u8 = undefined;
    var f = dir.openFile(io, memberPath(&name_buf, path), .{}) catch return null;
    defer f.close(io); // a mapping outlives its descriptor
    const st = f.stat(io) catch return null;
    const size = std.math.cast(usize, st.size) orelse return null;
    if (size == 0 or size > max_file_bytes) return null;
    var mm = f.createMemoryMap(io, .{ .len = size, .protection = .{ .read = true }, .populate = false }) catch return null;
    const file = (File.parse(gpa, mm.memory, path, fp, engine) catch null) orelse {
        mm.destroy(io);
        return null;
    };
    return .{ .io = io, .mm = mm, .file = file };
}

/// Write `new` into `path`'s cache, carrying `prior`'s edges over; pass
/// null for `prior` after a read found the file damaged. Atomic (temp +
/// fsync + rename) and best-effort: a read-only store or filesystem, or any
/// other failure, leaves the cache as it was.
pub fn store(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir, path: []const u8, fp: u32, engine: u32, new: []const Edge, prior: ?*File) void {
    const blob = (encode(gpa, path, fp, engine, new, prior) catch return) orelse return;
    defer gpa.free(blob);
    if (!repocache.ensureCacheDir(io, dir)) return;
    dir.createDir(io, dirname, .default_dir) catch |e| switch (e) {
        error.PathAlreadyExists => {},
        else => return,
    };
    var name_buf: [dirname.len + 1 + 16]u8 = undefined;
    var af = dir.createFileAtomic(io, memberPath(&name_buf, path), .{ .replace = true }) catch return;
    defer af.deinit(io);
    af.file.writePositionalAll(io, blob, 0) catch return;
    af.file.sync(io) catch return;
    af.replace(io) catch return;
}

// --- tests -----------------------------------------------------------------

const testing = std.testing;

const repeat = @import("repeat.zig").repeat;
const hash_a = repeat("a", 64);
const hash_b = repeat("b", 64);
const hash_c = repeat("c", 40);
const engine_now = engineKey(.{});

const t1 = [_]linediff.Triple{ .{ .copy = 2, .delete = 1, .insert = 0 }, .{ .copy = 0, .delete = 0, .insert = 3 } };
const t2 = [_]linediff.Triple{.{ .copy = 7, .delete = 0, .insert = 0 }};

fn expectEdge(f: *File, parent: []const u8, child: []const u8, want: ?[]const linediff.Triple) !void {
    const got = try f.lookup(testing.allocator, parent, child);
    defer if (got) |g| testing.allocator.free(g);
    if (want) |w| try testing.expectEqualSlices(linediff.Triple, w, got.?) else try testing.expect(got == null);
}

test "diffcache: edges round-trip by their ordered pair, an empty diff included" {
    const gpa = testing.allocator;
    const edges = [_]Edge{
        .{ .parent = hash_b, .child = hash_a, .triples = &t1 },
        .{ .parent = hash_a, .child = hash_c, .triples = &t2 },
        .{ .parent = hash_c, .child = hash_a, .triples = &.{} },
        .{ .parent = hash_b, .child = hash_a, .triples = &t2 }, // the first one's pair again
    };
    const blob = (try encode(gpa, "src/a.c", 7, engine_now, &edges, null)).?;
    defer gpa.free(blob);
    var f = (try File.parse(gpa, blob, "src/a.c", 7, engine_now)).?;
    defer f.deinit(gpa);
    try testing.expectEqual(@as(u32, 3), f.n_edges);
    try expectEdge(&f, hash_b, hash_a, &t1);
    try expectEdge(&f, hash_a, hash_c, &t2);
    try expectEdge(&f, hash_c, hash_a, &.{});
    try expectEdge(&f, hash_a, hash_b, null);
    try expectEdge(&f, "short", hash_a, null);
}

test "diffcache: a rewrite keeps the prior edges, the new ones first under the bound" {
    const gpa = testing.allocator;
    const first = (try encode(gpa, "p", 1, engine_now, &.{.{ .parent = hash_a, .child = hash_b, .triples = &t1 }}, null)).?;
    defer gpa.free(first);
    var f = (try File.parse(gpa, first, "p", 1, engine_now)).?;
    defer f.deinit(gpa);
    const second = (try encode(gpa, "p", 1, engine_now, &.{.{ .parent = hash_b, .child = hash_c, .triples = &t2 }}, &f)).?;
    defer gpa.free(second);
    var g = (try File.parse(gpa, second, "p", 1, engine_now)).?;
    defer g.deinit(gpa);
    try expectEdge(&g, hash_a, hash_b, &t1);
    try expectEdge(&g, hash_b, hash_c, &t2);
    // An edge too big for the bound is not written; a prior edge that no
    // longer fits beside the new ones is dropped.
    const huge = try gpa.alloc(linediff.Triple, max_file_bytes / triple_len);
    defer gpa.free(huge);
    @memset(huge, .{ .copy = 1, .delete = 0, .insert = 0 });
    try testing.expect((try encode(gpa, "p", 1, engine_now, &.{.{ .parent = hash_a, .child = hash_c, .triples = huge }}, null)) == null);
    const big = huge[0 .. huge.len * 5 / 8];
    const held = (try encode(gpa, "p", 1, engine_now, &.{.{ .parent = hash_a, .child = hash_b, .triples = big }}, null)).?;
    defer gpa.free(held);
    var hf = (try File.parse(gpa, held, "p", 1, engine_now)).?;
    defer hf.deinit(gpa);
    const third = (try encode(gpa, "p", 1, engine_now, &.{.{ .parent = hash_a, .child = hash_c, .triples = big }}, &hf)).?;
    defer gpa.free(third);
    try testing.expect(third.len <= max_file_bytes);
    var h = (try File.parse(gpa, third, "p", 1, engine_now)).?;
    defer h.deinit(gpa);
    try testing.expectEqual(@as(u32, 1), h.n_edges);
    try expectEdge(&h, hash_a, hash_c, big);
}

test "diffcache: another path, fingerprint, engine, version, a truncation or a header flip is a miss" {
    const gpa = testing.allocator;
    const blob = (try encode(gpa, "p", 9, engine_now, &.{.{ .parent = hash_a, .child = hash_b, .triples = &t1 }}, null)).?;
    defer gpa.free(blob);
    try testing.expect((try File.parse(gpa, blob, "q", 9, engine_now)) == null);
    try testing.expect((try File.parse(gpa, blob, "p", 8, engine_now)) == null);
    try testing.expect((try File.parse(gpa, blob, "p", 9, engineKey(.{ .optimize = false }))) == null);
    try testing.expect((try File.parse(gpa, blob, "p", 9, engine_now + (1 << 8))) == null);
    try testing.expect((try File.parse(gpa, blob[0 .. blob.len - 1], "p", 9, engine_now)) == null);
    const copy = try gpa.dupe(u8, blob);
    defer gpa.free(copy);
    // A format-version bump: a valid file under the old number is refused.
    writeInt(u16, copy, 8, version + 1);
    writeInt(u32, copy, fixed_len + 1, crc32.Crc32.hash(copy[0 .. fixed_len + 1]));
    try testing.expect((try File.parse(gpa, copy, "p", 9, engine_now)) == null);
    @memcpy(copy, blob);
    for (0..fixed_len + 1 + 4) |i| {
        copy[i] ^= 0x40;
        try testing.expect((try File.parse(gpa, copy, "p", 9, engine_now)) == null);
        copy[i] ^= 0x40;
    }
}

test "diffcache: a flipped byte in any block fails the lookup that reads it, and is never carried" {
    const gpa = testing.allocator;
    // Enough edges that the index and the triples span several blocks.
    var names: [80][64]u8 = undefined;
    var edges: [80]Edge = undefined;
    for (&names, &edges, 0..) |*n, *e, i| {
        _ = std.fmt.bufPrint(n, "{x:0>64}", .{i + 1}) catch unreachable;
        e.* = .{ .parent = n, .child = hash_a, .triples = &t1 };
    }
    const blob = (try encode(gpa, "p", 1, engine_now, &edges, null)).?;
    defer gpa.free(blob);
    const start = bodyStart(blob);
    const body_len: usize = readInt(u32, blob, 22);
    try testing.expect(body_len / blockcrc.block_len >= 3);
    const copy = try gpa.dupe(u8, blob);
    defer gpa.free(copy);
    for (0..body_len / blockcrc.block_len) |k| {
        @memcpy(copy, blob);
        copy[start + k * blockcrc.block_len + 3] ^= 1;
        var f = (try File.parse(gpa, copy, "p", 1, engine_now)).?;
        defer f.deinit(gpa);
        var failed = false;
        for (edges) |e| {
            const got = f.lookup(gpa, e.parent, e.child) catch |err| switch (err) {
                error.Corrupt => {
                    failed = true;
                    break;
                },
                else => return err,
            };
            gpa.free(got.?);
        }
        try testing.expect(failed);
        // A rewrite over the damaged file carries nothing it cannot check.
        const re = (try encode(gpa, "p", 1, engine_now, &.{.{ .parent = hash_b, .child = hash_c, .triples = &t2 }}, &f)).?;
        defer gpa.free(re);
        var r = (try File.parse(gpa, re, "p", 1, engine_now)).?;
        defer r.deinit(gpa);
        try testing.expect(r.n_edges <= 1);
        try expectEdge(&r, hash_b, hash_c, &t2);
    }
}

/// Recompute every CRC of `blob` after a test edited its body, so only the
/// edit is wrong.
fn reseal(blob: []u8) void {
    const start = bodyStart(blob);
    const body_len: usize = readInt(u32, blob, 22);
    const table = blob[start + body_len ..];
    blockcrc.writeTable(blob[start..][0..body_len], table);
    writeInt(u32, blob, 26, crc32.Crc32.hash(table));
    writeInt(u32, blob, start - 4, crc32.Crc32.hash(blob[0 .. start - 4]));
}

test "diffcache: a record whose hash length byte is out of range, every CRC right, is a miss or Corrupt" {
    const gpa = testing.allocator;
    const blob = (try encode(gpa, "p", 1, engine_now, &.{.{ .parent = hash_a, .child = hash_b, .triples = &t1 }}, null)).?;
    defer gpa.free(blob);
    const start = bodyStart(blob);
    for ([_]usize{ 0, 65 }) |at| for ([_]u8{ 0, 39, 65, 255 }) |len| {
        const copy = try gpa.dupe(u8, blob);
        defer gpa.free(copy);
        copy[start + at] = len;
        reseal(copy);
        var f = (try File.parse(gpa, copy, "p", 1, engine_now)).?;
        defer f.deinit(gpa);
        try expectEdge(&f, hash_a, hash_b, null);
        try testing.expectError(error.Corrupt, f.edgeAt(gpa, 0));
        // Carried into a rewrite, it is dropped.
        const re = (try encode(gpa, "p", 1, engine_now, &.{.{ .parent = hash_b, .child = hash_c, .triples = &t2 }}, &f)).?;
        defer gpa.free(re);
        var r = (try File.parse(gpa, re, "p", 1, engine_now)).?;
        defer r.deinit(gpa);
        try testing.expectEqual(@as(u32, 1), r.n_edges);
    };
}

fn codecNoLeak(a: std.mem.Allocator) !void {
    const first = (try encode(a, "p", 3, engine_now, &.{.{ .parent = hash_a, .child = hash_b, .triples = &t1 }}, null)).?;
    defer a.free(first);
    var f = (try File.parse(a, first, "p", 3, engine_now)).?;
    defer f.deinit(a);
    const second = (try encode(a, "p", 3, engine_now, &.{.{ .parent = hash_c, .child = hash_b, .triples = &t2 }}, &f)).?;
    defer a.free(second);
    var g = (try File.parse(a, second, "p", 3, engine_now)).?;
    defer g.deinit(a);
    const got = (try g.lookup(a, hash_a, hash_b)).?;
    defer a.free(got);
    try testing.expectEqual(@as(usize, 2), got.len);
}

test "diffcache: the codec is leak-free under checkAllAllocationFailures" {
    try testing.checkAllAllocationFailures(testing.allocator, codecNoLeak, .{});
}

test "diffcache: store then open round-trips under a directory, and another path's file is a miss" {
    const gpa = testing.allocator;
    const io = testing.io;
    var tmp = testing.tmpDir(.{});
    defer tmp.cleanup();
    try testing.expect(open(gpa, io, tmp.dir, "a.txt", 1, engine_now) == null);
    store(gpa, io, tmp.dir, "a.txt", 1, engine_now, &.{.{ .parent = hash_a, .child = hash_b, .triples = &t1 }}, null);
    var m = open(gpa, io, tmp.dir, "a.txt", 1, engine_now).?;
    try expectEdge(&m.file, hash_a, hash_b, &t1);
    store(gpa, io, tmp.dir, "a.txt", 1, engine_now, &.{.{ .parent = hash_b, .child = hash_c, .triples = &t2 }}, &m.file);
    m.deinit(gpa);
    var m2 = open(gpa, io, tmp.dir, "a.txt", 1, engine_now).?;
    defer m2.deinit(gpa);
    try testing.expectEqual(@as(u32, 2), m2.file.n_edges);
    try testing.expect(open(gpa, io, tmp.dir, "a.txt", 2, engine_now) == null);
    store(gpa, io, tmp.dir, "b.txt", 1, engine_now, &.{.{ .parent = hash_a, .child = hash_b, .triples = &t1 }}, null);
    var a_buf: [dirname.len + 1 + 16]u8 = undefined;
    var b_buf: [dirname.len + 1 + 16]u8 = undefined;
    try tmp.dir.rename(memberPath(&b_buf, "b.txt"), tmp.dir, memberPath(&a_buf, "a.txt"), io);
    try testing.expect(open(gpa, io, tmp.dir, "a.txt", 1, engine_now) == null);
}