//! 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;
var arena_state = std.heap.ArenaAllocator.init(gpa);
defer arena_state.deinit();
const arena = arena_state.allocator();
const prior_n: usize = if (prior) |p| p.n_edges else 0;
var chosen = try std.ArrayList(Pending).initCapacity(arena, new.len + prior_n);
var seen: std.AutoHashMapUnmanaged([key_len]u8, void) = .empty;
try seen.ensureTotalCapacity(arena, @intCast(new.len + prior_n));
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(arena, k) catch |err| switch (err) {
error.OutOfMemory => return error.OutOfMemory,
error.Corrupt => break,
};
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);
}