Skip to content
zit Artifact [10a46a5f46]
Login

Artifact [10a46a5f46]

Artifact 10a46a5f4630f53d77be52a66513b4f506291e360fe76a948fae58c52b44e9d1:


//! Storage plan E: per-checkout working state, kept OUT of the shared log.
//! Fossil keeps checkout/working state outside the repository file (in
//! `_FOSSIL_`); this is the analog — a small record materialized atomically
//! into `.zit-checkout` beside the working tree via a write-temp + fsync +
//! rename-replace sequence (all inside std's `Dir.createFileAtomic` /
//! `File.Atomic.replace`). A crash mid-write leaves the prior record intact:
//! a reader only ever observes `filename`, never a half-written temp.
//!
//! No RFC section behind this file: native storage-engine design, the same
//! oracle as logstore.zig (storage-track-plan.md item E). Effects flow only
//! through an injected `std.Io` + `std.Io.Dir` (design-seams rule — no
//! ambient filesystem access); tests bind `std.testing.io` + a `tmpDir`.
//!
//! This state deliberately does NOT enter the log: it is per-working-copy,
//! not part of the shippable `.zit` container (plan E).

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

/// The device failed, or the record on disk is not a `.zit-checkout` this
/// reader can parse. Closed at the boundary so a CLI caller can switch
/// exhaustively.
pub const CheckoutError = error{ Storage, Corrupt };

/// `CheckoutError` plus the allocation a variable-size section read needs.
pub const SectionError = error{ Storage, Corrupt, OutOfMemory };

/// The working-state file, resolved relative to the injected checkout dir.
pub const filename: []const u8 = ".zit-checkout";
/// Per-string upper bound (a hash or branch name never approaches it).
pub const max_ck_str: usize = 255;
/// Fixed record bound: five length-prefixed strings.
pub const max_ck_payload: usize = 6 * (2 + max_ck_str); // = 1542

/// The manifest-file selectors, in canonical order: `r` the check-in artifact,
/// `u` its hash, `t` its symbolic tags. A `manifest_flags` value is a subset of
/// these, deduplicated and in this order; the empty string means "write none",
/// which is the default.
pub const manifest_selectors: []const u8 = "rut";

/// The stored `manifest_flags` meaning "stamps explicitly
/// OFF for this checkout" (`open --manifest=off`). Distinct from "", never
/// chosen, which lets the project's `manifest` setting supply the selectors
/// at the next update; this value refuses it. Not a selector letter, so a
/// reader scanning for `r`/`u`/`t` sees no stamps without knowing about it.
pub const manifest_off: []const u8 = "-";

/// The selectors a stored `manifest_flags` actually writes: none for the
/// explicit-off sentinel, the value itself otherwise.
pub fn manifestSelectors(stored: []const u8) []const u8 {
    return if (std.mem.eql(u8, stored, manifest_off)) "" else stored;
}

/// One checkout's working state. Both slices borrow the caller's buffer
/// (for `read`) or the caller's own storage (for `write`); this struct owns
/// nothing.
pub const CheckoutState = struct {
    /// The checked-out artifact hash.
    version: []const u8,
    /// The current branch name.
    branch: []const u8,
    /// The pending merge parent's artifact hash, or empty when no merge is
    /// pending. Deliberately has NO default: every construction site threads it
    /// explicitly so a reconstruction can never silently drop a pending merge.
    merge_parent: []const u8,
    /// The pending special-merge operation (cherry-pick / backout), recorded as
    /// the q-sign byte (`+` cherry-pick, `-` backout) concatenated with the
    /// target check-in's artifact hash, or empty when none is pending. A special
    /// merge and a plain merge are mutually exclusive: at most one of
    /// `merge_parent` / `special_op` is ever non-empty. Like `merge_parent`, it
    /// has NO default so no reconstruction silently drops a pending operation.
    special_op: []const u8,
    /// Which working-tree manifest files this checkout wants refreshed on every
    /// materialization: a subset of `manifest_selectors`, empty for none. Set
    /// once by `open --manifest`, then carried forward by every later `update`
    /// — so, like the two fields above, it has NO default: a reconstruction
    /// that forgets it would silently switch the files off.
    manifest_flags: []const u8,
    /// `merge --integrate`: the hash of the merged-in leaf the next
    /// commit closes; "" when none. Encoded only when set, so a record
    /// without one is byte-identical to the five-field form.
    integrate: []const u8 = "",
};

// ---------------------------------------------------------------------------
// Container v2 (consolidation DESIGN §5.1). `.zit-checkout` is a sectioned
// container holding every authoritative checkout-local record:
//
//   magic(8)="ZITCKOT\x00" | fmt_version(u16 LE)=2 | section_count(u8) |
//   reserved(u8)=0 | table: count × { tag(u8) | len(u64 LE) } |
//   payloads in table order | crc32 LE (4) over every earlier byte
//
// Section payloads are the EXISTING serialized forms of the absorbed files,
// unchanged — the container adds only framing. Sections are optional; tags in
// the table are strictly ascending (canonical order, which makes a duplicate
// unrepresentable); an unknown tag, a zero length, a nonzero reserved byte, a
// version mismatch, a table/size mismatch or a crc mismatch are all
// `error.Corrupt` — authoritative state is never best-effort parsed.
//
// The legacy single-record `.zit-checkout` begins with a u16 length ≤ 255, so
// its byte 1 is always 0x00, while the container's byte 1 is 'I': byte-1
// dispatch (§8). On a legacy read the sibling files are folded in through
// their raw bytes; once a container exists the siblings are DEAD — never
// read, deleted best-effort after every container write.
// ---------------------------------------------------------------------------

pub const container_magic: []const u8 = "ZITCKOT\x00";
/// v3 (sparse-cone RFC + worktrees): adds the `cone` and `store_location`
/// tags. Written always; v2 is read-accepted and migrates to v3 on the first
/// splice, exactly as v2 folds the legacy siblings. A v2 binary meeting a v3
/// container refuses loudly at the version gate rather than misparsing.
/// v4 (sparse-cone RFC section 4.4): adds `merge_overlay` -- the resolved
/// out-of-cone F-card set a cross-cone merge records for the next commit's
/// compose. Same fold-forward/refuse-backward discipline as v3's bump.
pub const container_version: u16 = 4;
/// The newest version whose sections this reader folds forward.
pub const container_version_min_read: u16 = 2;

/// The absorbed records, one tag each. Payload formats stay owned by their
/// modules; this file frames bytes it never interprets (except tag 1, its own).
/// `cone` (sparse-cone RFC section 3) is owned by `cone.zig`; `store_location`
/// is RESERVED for worktrees: parse-accepted, carried verbatim
/// through every splice, never interpreted here or anywhere yet.
pub const SectionTag = enum(u8) { checkout = 1, managed = 2, undo = 3, bisect = 4, cone = 5, store_location = 6, merge_overlay = 7 };

/// One splice instruction for `updateSections`: replace the tagged section's
/// payload, or remove the section entirely (`payload = null`).
pub const SectionUpdate = struct {
    tag: SectionTag,
    payload: ?[]const u8,
};

const container_header_len: usize = 12; // magic 8 + version 2 + count 1 + reserved 1
const table_entry_len: usize = 9; // tag u8 + len u64 LE
const max_sections: usize = 7;
/// Tag ceiling per container version: v2 wrote tags 1..4 only, and a v2 table
/// carrying a higher tag is corrupt, not forward-compatible.
fn tagLimit(version: u16) usize {
    std.debug.assert(version >= container_version_min_read);
    std.debug.assert(version <= container_version);
    return switch (version) {
        2 => 4,
        3 => 6,
        else => max_sections,
    };
}
const max_prefix_len: usize = container_header_len + max_sections * table_entry_len; // 75
const crc_len: usize = 4;

/// The legacy per-record files a pre-container checkout carries, indexed by
/// tag. Names are deliberate literals: this module frames payloads without
/// importing the modules that own them (the registry's generated pin test in
/// `control_paths.zig` is the drift guard, per the consolidation design §9).
fn legacyName(tag: SectionTag) ?[]const u8 {
    return switch (tag) {
        .checkout => filename,
        .managed => ".zit-managed",
        .undo => ".zit-undo",
        .bisect => ".zit-bisect",
        // v3/v4 tags: born inside the container, no legacy sibling existed.
        .cone, .store_location, .merge_overlay => null,
    };
}

const Extent = struct { offset: u64, len: u64 };

/// A validated container prefix: which sections exist and where. Constructed
/// only by `parsePrefix`, which has already proven the table is canonical and
/// exactly accounts for the file size — so extents derived here are backed by
/// real bytes, never by a declared length alone.
const Table = struct {
    lens: [max_sections]u64, // indexed by tag-1; 0 = absent (a real section is never empty)
    payload_start: u64,
    total_len: u64,

    fn extent(self: *const Table, tag: SectionTag) ?Extent {
        var offset: u64 = self.payload_start;
        for (self.lens, 1..) |len, t| {
            if (t == @intFromEnum(tag)) {
                if (len == 0) return null;
                return .{ .offset = offset, .len = len };
            }
            offset += len;
        }
        unreachable; // every SectionTag value indexes the loop
    }
};

/// Byte-1 dispatch (§8): the container's second byte is 'I'; the legacy
/// record's second byte is the high byte of a u16 length ≤ 255, always 0x00.
fn isContainer(prefix: []const u8) bool {
    return prefix.len >= 2 and prefix[1] == container_magic[1];
}

/// Validates a container's header + section table against the stat'd file
/// size. Closed parsing: every violation is `error.Corrupt`, and the byte
/// accounting must be EXACT — the table's sections plus framing must equal the
/// file size, so neither a truncated payload nor trailing garbage survives to
/// the crc check.
fn parsePrefix(prefix: []const u8, file_size: u64) error{Corrupt}!Table {
    if (prefix.len < container_header_len) return error.Corrupt;
    if (!std.mem.eql(u8, prefix[0..container_magic.len], container_magic)) return error.Corrupt;
    const version = std.mem.readInt(u16, prefix[8..10], .little);
    if (version < container_version_min_read) return error.Corrupt;
    if (version > container_version) return error.Corrupt;
    const tag_limit = tagLimit(version);
    const count: usize = prefix[10];
    if (count == 0 or count > tag_limit) return error.Corrupt; // a writer never emits an empty container
    if (prefix[11] != 0) return error.Corrupt;
    const table_end = container_header_len + count * table_entry_len;
    if (prefix.len < table_end) return error.Corrupt;

    var table: Table = .{
        .lens = @splat(0),
        .payload_start = table_end,
        .total_len = 0,
    };
    var payload_sum: u64 = 0;
    var prev_tag: u8 = 0;
    for (0..count) |i| {
        const entry = prefix[container_header_len + i * table_entry_len ..][0..table_entry_len];
        const tag = entry[0];
        // Strictly ascending known tags: canonical order, and a duplicate or
        // unknown tag is unrepresentable rather than merely rejected.
        if (tag <= prev_tag or tag > tag_limit) return error.Corrupt;
        prev_tag = tag;
        const len = std.mem.readInt(u64, entry[1..9], .little);
        if (len == 0) return error.Corrupt; // absent is "not in the table", never a zero-length section
        if (len > file_size or payload_sum > file_size - len) return error.Corrupt;
        payload_sum += len;
        table.lens[tag - 1] = len;
    }
    const framing: u64 = @intCast(table_end + crc_len);
    if (payload_sum > file_size or file_size - payload_sum != framing) return error.Corrupt;
    table.total_len = file_size;
    return table;
}

/// One streaming pass over a validated container: computes the whole-file crc
/// (over every byte before the trailing 4) and, when `want` is set, copies
/// that extent's bytes into `dest` as they stream past. The copy is returned
/// to the caller ONLY if the trailing crc matches — no section is trusted
/// before the whole file proves intact.
fn verifyStream(io: std.Io, file: std.Io.File, table: Table, want: ?Extent, dest: []u8) CheckoutError!void {
    if (want) |w| std.debug.assert(dest.len >= w.len);
    var crc = crc32.Crc32.init();
    const body_len = table.total_len - crc_len;
    var pos: u64 = 0;
    var chunk: [4096]u8 = undefined;
    while (pos < body_len) {
        const take: usize = @intCast(@min(chunk.len, body_len - pos));
        const n = file.readPositionalAll(io, chunk[0..take], pos) catch return error.Storage;
        if (n != take) return error.Storage; // size came from stat; a short read is device trouble, not framing
        crc.update(chunk[0..take]);
        if (want) |w| {
            const lo = @max(pos, w.offset);
            const hi = @min(pos + take, w.offset + w.len);
            if (lo < hi) {
                const src = chunk[@intCast(lo - pos)..@intCast(hi - pos)];
                @memcpy(dest[@intCast(lo - w.offset)..][0..src.len], src);
            }
        }
        pos += take;
    }
    var stored: [crc_len]u8 = undefined;
    const n = file.readPositionalAll(io, &stored, body_len) catch return error.Storage;
    if (n != crc_len) return error.Storage;
    if (std.mem.readInt(u32, &stored, .little) != crc.final()) return error.Corrupt;
}

/// Reads one section's raw payload, dispatching on the file generation:
/// container reads stream-verify the whole-file crc while extracting exactly
/// the requested extent; a legacy checkout reads the corresponding sibling
/// file raw (the payload formats are identical, so the sibling's bytes ARE the
/// section). Returns null when the section (or the whole file) is absent.
/// The caller owns the returned bytes.
/// Where this checkout's store lives. Null means "beside the
/// checkout", which is every checkout `init` or `clone` produces; a detached
/// worktree carries the store root's absolute path in its `store_location`
/// section, and this is the ONE place that path is interpreted, so a
/// checkout whose pointer dangles fails identically everywhere:
/// `error.Storage`, the same answer as a deleted `.zit`.
///
/// The caller owns closing the returned directory handle. Reads the section
/// with a fixed buffer — a path beyond the OS maximum could not open anyway.
pub fn storeDirOf(io: std.Io, root_dir: std.Io.Dir) error{ Storage, Corrupt }!?std.Io.Dir {
    var buf: [std.fs.max_path_bytes + 256]u8 = undefined;
    var fixed = std.heap.FixedBufferAllocator.init(&buf);
    const payload = readSectionAlloc(fixed.allocator(), io, root_dir, .store_location) catch |e| switch (e) {
        // A pointer too long to be a real path is a corrupt section, not OOM.
        error.OutOfMemory => return error.Corrupt,
        error.Storage => return error.Storage,
        error.Corrupt => return error.Corrupt,
    } orelse return null;
    if (payload.len == 0 or payload[0] != '/') return error.Corrupt;
    if (std.mem.indexOfScalar(u8, payload, 0) != null) return error.Corrupt;
    return std.Io.Dir.cwd().openDir(io, payload, .{}) catch return error.Storage;
}

pub fn readSectionAlloc(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir, tag: SectionTag) SectionError!?[]u8 {
    var file = dir.openFile(io, filename, .{}) catch |e| switch (e) {
        error.FileNotFound => return legacyReadAlloc(gpa, io, dir, tag),
        else => return error.Storage,
    };
    defer file.close(io);
    const st = file.stat(io) catch return error.Storage;
    var prefix: [max_prefix_len]u8 = undefined;
    const pn: usize = @intCast(@min(prefix.len, st.size));
    const got = file.readPositionalAll(io, prefix[0..pn], 0) catch return error.Storage;
    if (got != pn) return error.Storage;
    if (!isContainer(prefix[0..pn])) {
        // Pre-container checkout: the requested record lives in its own file.
        // For tag 1 that file is this one; re-read it raw below either way.
        return legacyReadAlloc(gpa, io, dir, tag);
    }
    const table = try parsePrefix(prefix[0..pn], st.size);
    const ext = table.extent(tag) orelse {
        // Absence is also a statement the crc must back before it is trusted.
        try verifyStream(io, file, table, null, &.{});
        return null;
    };
    const len = std.math.cast(usize, ext.len) orelse return error.OutOfMemory;
    const buf = gpa.alloc(u8, len) catch return error.OutOfMemory;
    errdefer gpa.free(buf);
    try verifyStream(io, file, table, ext, buf);
    return buf;
}

/// The legacy-generation read behind `readSectionAlloc`: the tagged record's
/// own file, whole, sized from stat (the journal has no format-level size cap
/// by design — see `undo.zig`'s `readJournalFile` rationale, which this
/// inherits). Absent file = absent section.
fn legacyReadAlloc(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir, tag: SectionTag) SectionError!?[]u8 {
    const name = legacyName(tag) orelse return null;
    var file = dir.openFile(io, name, .{}) catch |e| switch (e) {
        error.FileNotFound => return null,
        else => return error.Storage,
    };
    defer file.close(io);
    const st = file.stat(io) catch return error.Storage;
    const size = std.math.cast(usize, st.size) orelse return error.OutOfMemory;
    const buf = gpa.alloc(u8, size) catch return error.OutOfMemory;
    errdefer gpa.free(buf);
    const n = file.readPositionalAll(io, buf, 0) catch return error.Storage;
    if (n != size) return error.Storage;
    return buf;
}

/// Where a carried-forward section's bytes come from during a splice.
const CarrySource = union(enum) {
    omit,
    /// Caller-supplied replacement payload.
    payload: []const u8,
    /// Copy out of the (crc-verified) old container.
    old: Extent,
    /// Legacy fold: stream the sibling file's raw bytes in (§8). The handle is
    /// opened during resolution and closed by the splice's cleanup.
    sibling: struct { file: std.Io.File, len: u64 },
};

/// Read-splice-write for the authoritative container. Resolves every
/// section's source (caller update > old container > legacy sibling), then
/// materializes the new container through the same write-temp + fsync +
/// rename-replace sequence every authoritative writer uses. After a
/// successful rename the legacy sibling files are dead and are deleted
/// best-effort; removing the last section deletes the container itself, so
/// "no checkout state" stays spelled "no file".
///
/// An existing container's crc is verified BEFORE any of its sections are
/// carried forward; an existing legacy record is gated through `parseCk`
/// before adoption. Allocation-free: bounded stack buffers and streamed
/// copies only, which is what lets `write`/`read` keep their signatures.
pub fn updateSections(io: std.Io, dir: std.Io.Dir, updates: []const SectionUpdate) CheckoutError!void {
    // Caller contract, checked pairwise (which also bounds the list: five or
    // more updates cannot all name distinct tags). No assert names the slice
    // parameter itself — the release_assert gate's rule. An EMPTY update list
    // is well-defined: a pure rewrite, which on a legacy checkout is exactly
    // "migrate to the container".
    for (updates, 0..) |u, i| {
        for (updates[i + 1 ..]) |v| std.debug.assert(u.tag != v.tag);
        if (u.payload) |p| std.debug.assert(p.len > 0); // empty ≠ absent; absence is payload = null
    }

    const old_file: ?std.Io.File = dir.openFile(io, filename, .{}) catch |e| switch (e) {
        error.FileNotFound => null,
        else => return error.Storage,
    };
    defer if (old_file) |f| f.close(io);

    // Old-generation state: exactly one of these is set when the file exists.
    var old_table: ?Table = null;
    var legacy_ck_buf: [max_ck_payload + 1]u8 = undefined;
    var legacy_ck: ?[]const u8 = null;

    if (old_file) |f| {
        const st = f.stat(io) catch return error.Storage;
        var prefix: [max_prefix_len]u8 = undefined;
        const pn: usize = @intCast(@min(prefix.len, st.size));
        const got = f.readPositionalAll(io, prefix[0..pn], 0) catch return error.Storage;
        if (got != pn) return error.Storage;
        if (isContainer(prefix[0..pn])) {
            const table = try parsePrefix(prefix[0..pn], st.size);
            try verifyStream(io, f, table, null, &.{}); // trust nothing the crc has not backed
            old_table = table;
        } else {
            if (st.size > max_ck_payload) return error.Corrupt;
            const n = f.readPositionalAll(io, legacy_ck_buf[0..@as(usize, @intCast(st.size))], 0) catch return error.Storage;
            if (n != st.size) return error.Storage;
            _ = try parseCk(legacy_ck_buf[0..n]); // structural gate before adoption
            legacy_ck = legacy_ck_buf[0..n];
        }
    }

    // Resolve each tag's source. Sibling handles opened here are closed by the
    // deferred sweep below whether the splice commits or fails.
    var sources: [max_sections]CarrySource = @splat(.omit);
    defer for (&sources) |*s| {
        if (s.* == .sibling) s.sibling.file.close(io);
    };
    resolve: for (1..max_sections + 1) |t| {
        const tag: SectionTag = @enumFromInt(t);
        for (updates) |u| {
            if (u.tag == tag) {
                if (u.payload) |p| sources[t - 1] = .{ .payload = p };
                continue :resolve;
            }
        }
        if (old_table) |table| {
            // Container generation: the siblings are dead (§8) — carry only
            // what the container itself holds.
            if (table.extent(tag)) |ext| sources[t - 1] = .{ .old = ext };
            continue :resolve;
        }
        if (tag == .checkout) {
            if (legacy_ck) |ck| sources[t - 1] = .{ .payload = ck };
            continue :resolve;
        }
        const sibling_name = legacyName(tag) orelse continue :resolve;
        var sf = dir.openFile(io, sibling_name, .{}) catch |e| switch (e) {
            error.FileNotFound => continue :resolve,
            else => return error.Storage,
        };
        const st = sf.stat(io) catch {
            sf.close(io);
            return error.Storage;
        };
        if (st.size == 0) {
            // A zero-byte record is corrupt in every absorbed format; refuse
            // to launder it into a valid-looking absence mid-migration.
            sf.close(io);
            return error.Corrupt;
        }
        sources[t - 1] = .{ .sibling = .{ .file = sf, .len = st.size } };
    }

    var count: usize = 0;
    for (sources) |s| {
        if (s != .omit) count += 1;
    }
    if (count == 0) {
        dir.deleteFile(io, filename) catch |e| switch (e) {
            error.FileNotFound => {},
            else => return error.Storage,
        };
        deleteLegacySiblings(io, dir);
        return;
    }

    var af = dir.createFileAtomic(io, filename, .{ .replace = true }) catch return error.Storage;
    defer af.deinit(io);
    var crc = crc32.Crc32.init();
    var pos: u64 = 0;

    var pbuf: [max_prefix_len]u8 = undefined;
    @memcpy(pbuf[0..container_magic.len], container_magic);
    std.mem.writeInt(u16, pbuf[8..10], container_version, .little);
    pbuf[10] = @intCast(count);
    pbuf[11] = 0;
    var off: usize = container_header_len;
    for (sources, 1..) |s, t| {
        const len: u64 = switch (s) {
            .omit => continue,
            .payload => |p| p.len,
            .old => |ext| ext.len,
            .sibling => |sib| sib.len,
        };
        pbuf[off] = @intCast(t);
        std.mem.writeInt(u64, pbuf[off + 1 ..][0..8], len, .little);
        off += table_entry_len;
    }
    try appendChunk(io, af.file, &crc, &pos, pbuf[0..off]);

    for (sources) |s| switch (s) {
        .omit => {},
        .payload => |p| try appendChunk(io, af.file, &crc, &pos, p),
        .old => |ext| try appendCopy(io, af.file, &crc, &pos, old_file.?, ext.offset, ext.len),
        .sibling => |sib| try appendCopy(io, af.file, &crc, &pos, sib.file, 0, sib.len),
    };

    var trailer: [crc_len]u8 = undefined;
    std.mem.writeInt(u32, &trailer, crc.final(), .little);
    af.file.writePositionalAll(io, &trailer, pos) catch return error.Storage;
    af.file.sync(io) catch return error.Storage; // fsync data before the rename
    af.replace(io) catch return error.Storage; // atomic rename into place

    // The container is durable; whatever the siblings say is now dead (§8).
    // A crash before these deletes leaves both, and the byte-1 dispatch above
    // keeps ignoring the siblings — the deletes are cleanup, not correctness.
    deleteLegacySiblings(io, dir);
}

fn deleteLegacySiblings(io: std.Io, dir: std.Io.Dir) void {
    dir.deleteFile(io, legacyName(.managed).?) catch {};
    dir.deleteFile(io, legacyName(.undo).?) catch {};
    dir.deleteFile(io, legacyName(.bisect).?) catch {};
}

fn appendChunk(io: std.Io, file: std.Io.File, crc: *crc32.Crc32, pos: *u64, bytes: []const u8) CheckoutError!void {
    file.writePositionalAll(io, bytes, pos.*) catch return error.Storage;
    crc.update(bytes);
    pos.* += bytes.len;
}

fn appendCopy(io: std.Io, dst: std.Io.File, crc: *crc32.Crc32, pos: *u64, src: std.Io.File, src_offset: u64, len: u64) CheckoutError!void {
    var chunk: [4096]u8 = undefined;
    var copied: u64 = 0;
    while (copied < len) {
        const take: usize = @intCast(@min(chunk.len, len - copied));
        const n = src.readPositionalAll(io, chunk[0..take], src_offset + copied) catch return error.Storage;
        if (n != take) return error.Storage; // len came from a verified table or stat
        try appendChunk(io, dst, crc, pos, chunk[0..take]);
        copied += take;
    }
}

/// Materializes `state` atomically as the container's checkout section,
/// carrying every other section forward (read-splice-write; §5.1). A legacy
/// checkout is migrated to the container by this first write. Data is fsync'd
/// before the rename so the record is durable the instant it becomes visible;
/// a crash mid-write leaves the prior generation intact.
pub fn write(io: std.Io, dir: std.Io.Dir, state: CheckoutState) CheckoutError!void {
    std.debug.assert(state.version.len <= max_ck_str);
    std.debug.assert(state.branch.len <= max_ck_str);
    std.debug.assert(state.merge_parent.len <= max_ck_str);
    std.debug.assert(state.special_op.len <= max_ck_str);
    std.debug.assert(state.manifest_flags.len <= max_ck_str);

    var buf: [max_ck_payload]u8 = undefined;
    const bytes = encodeCk(&buf, state);
    return updateSections(io, dir, &.{.{ .tag = .checkout, .payload = bytes }});
}

/// Reads and parses the checkout record into `out_buf`, which must hold at
/// least one byte past the record bound (`> max_ck_payload`): that extra byte
/// is what lets an oversized legacy file be detected rather than silently
/// truncated to a valid-looking maximal prefix. Byte-1 dispatch: a container
/// stream-verifies its whole-file crc while extracting the checkout section
/// (still allocation-free — the section is bounded); a legacy record parses
/// as before. Returns null when no checkout file exists, or when a container
/// exists but carries no checkout section (`FileNotFound` is absence, not an
/// error, plan E). A malformed record is `error.Corrupt`; any other I/O
/// failure is `error.Storage`. The result's slices borrow `out_buf`.
pub fn read(io: std.Io, dir: std.Io.Dir, out_buf: []u8) CheckoutError!?CheckoutState {
    std.debug.assert(out_buf.len > max_ck_payload);
    var file = dir.openFile(io, filename, .{}) catch |e| switch (e) {
        error.FileNotFound => return null,
        else => return error.Storage,
    };
    defer file.close(io);
    const st = file.stat(io) catch return error.Storage;
    var prefix: [max_prefix_len]u8 = undefined;
    const pn: usize = @intCast(@min(prefix.len, st.size));
    const got = file.readPositionalAll(io, prefix[0..pn], 0) catch return error.Storage;
    if (got != pn) return error.Storage;

    if (isContainer(prefix[0..pn])) {
        const table = try parsePrefix(prefix[0..pn], st.size);
        const ext = table.extent(.checkout) orelse {
            try verifyStream(io, file, table, null, &.{}); // absence is trusted only past the crc
            return null;
        };
        if (ext.len > max_ck_payload) return error.Corrupt;
        try verifyStream(io, file, table, ext, out_buf);
        return try parseCk(out_buf[0..@intCast(ext.len)]);
    }

    const n = file.readPositionalAll(io, out_buf, 0) catch return error.Storage;
    // No valid record exceeds max_ck_payload bytes. A read past it means the
    // file is longer than any record this reader emits — reject it, never parse
    // a truncated prefix (an exactly-maximal record leaves the extra byte free).
    if (n > max_ck_payload) return error.Corrupt;
    return try parseCk(out_buf[0..n]);
}

/// Encodes `s` into `buf` as `[version_len u16 LE][version][branch_len u16
/// LE][branch][merge_parent_len u16 LE][merge_parent][special_op_len u16 LE]
/// [special_op][manifest_flags_len u16 LE][manifest_flags]` and returns the
/// written slice. Five TLVs are ALWAYS emitted (an empty field is `[00 00]`).
/// Fixed bounds, no unbounded copy. Public so a caller composing a
/// multi-section `updateSections` splice (undo's restore, update's commit
/// point) can supply the checkout payload alongside its siblings.
pub fn encodeCk(buf: []u8, s: CheckoutState) []const u8 {
    std.debug.assert(buf.len >= max_ck_payload);
    std.debug.assert(s.version.len <= max_ck_str);
    std.debug.assert(s.branch.len <= max_ck_str);
    std.debug.assert(s.merge_parent.len <= max_ck_str);
    std.debug.assert(s.special_op.len <= max_ck_str);
    std.debug.assert(s.manifest_flags.len <= max_ck_str);
    const vl: usize = s.version.len;
    const bl: usize = s.branch.len;
    const ml: usize = s.merge_parent.len;
    const sl: usize = s.special_op.len;
    const fl: usize = s.manifest_flags.len;

    std.mem.writeInt(u16, buf[0..2], @intCast(vl), .little);
    @memcpy(buf[2..][0..vl], s.version);
    std.mem.writeInt(u16, buf[2 + vl ..][0..2], @intCast(bl), .little);
    @memcpy(buf[4 + vl ..][0..bl], s.branch);
    std.mem.writeInt(u16, buf[4 + vl + bl ..][0..2], @intCast(ml), .little);
    @memcpy(buf[6 + vl + bl ..][0..ml], s.merge_parent);
    std.mem.writeInt(u16, buf[6 + vl + bl + ml ..][0..2], @intCast(sl), .little);
    @memcpy(buf[8 + vl + bl + ml ..][0..sl], s.special_op);
    std.mem.writeInt(u16, buf[8 + vl + bl + ml + sl ..][0..2], @intCast(fl), .little);
    @memcpy(buf[10 + vl + bl + ml + sl ..][0..fl], s.manifest_flags);
    const base = 10 + vl + bl + ml + sl + fl;
    if (s.integrate.len == 0) return buf[0..base];
    std.debug.assert(s.integrate.len <= max_ck_str);
    const il: usize = s.integrate.len;
    std.mem.writeInt(u16, buf[base..][0..2], @intCast(il), .little);
    @memcpy(buf[base + 2 ..][0..il], s.integrate);
    return buf[0 .. base + 2 + il];
}

/// Parses a `encodeCk` record. Every length is bounds-checked against the
/// remaining slice before use, each length is capped at `max_ck_str`, and
/// total consumption must be exact (trailing bytes are corruption, never
/// silently dropped). All wire-derived, validated before use.
fn parseCk(payload: []const u8) error{Corrupt}!CheckoutState {
    var pos: usize = 0;
    if (payload.len - pos < 2) return error.Corrupt;
    const vlen: usize = std.mem.readInt(u16, payload[pos..][0..2], .little);
    pos += 2;
    if (vlen > max_ck_str) return error.Corrupt;
    if (payload.len - pos < vlen) return error.Corrupt;
    const version = payload[pos..][0..vlen];
    pos += vlen;

    if (payload.len - pos < 2) return error.Corrupt;
    const blen: usize = std.mem.readInt(u16, payload[pos..][0..2], .little);
    pos += 2;
    if (blen > max_ck_str) return error.Corrupt;
    if (payload.len - pos < blen) return error.Corrupt;
    const branch = payload[pos..][0..blen];
    pos += blen;

    // Third TLV (merge_parent) is read WHEN PRESENT. A legacy two-field record
    // ends exactly here (`pos == payload.len`): it parses as "no pending merge"
    // — the pending merge errors out of existence, no versioning byte needed.
    var merge_parent: []const u8 = "";
    if (pos != payload.len) {
        if (payload.len - pos < 2) return error.Corrupt;
        const mlen: usize = std.mem.readInt(u16, payload[pos..][0..2], .little);
        pos += 2;
        if (mlen > max_ck_str) return error.Corrupt;
        if (payload.len - pos < mlen) return error.Corrupt;
        merge_parent = payload[pos..][0..mlen];
        pos += mlen;
    }

    // Fourth TLV (special_op) is likewise read WHEN PRESENT. A three-field
    // record written before special_op existed ends here and parses as "no
    // pending operation" — same errors-out-of-existence tolerance as above.
    var special_op: []const u8 = "";
    if (pos != payload.len) {
        if (payload.len - pos < 2) return error.Corrupt;
        const slen: usize = std.mem.readInt(u16, payload[pos..][0..2], .little);
        pos += 2;
        if (slen > max_ck_str) return error.Corrupt;
        if (payload.len - pos < slen) return error.Corrupt;
        special_op = payload[pos..][0..slen];
        pos += slen;
    }

    // Fifth TLV (manifest_flags), same when-present tolerance: a record written
    // before the manifest opt-in existed ends here and parses as "write no
    // manifest files" — which is exactly what such a checkout was doing.
    var manifest_flags: []const u8 = "";
    if (pos != payload.len) {
        if (payload.len - pos < 2) return error.Corrupt;
        const flen: usize = std.mem.readInt(u16, payload[pos..][0..2], .little);
        pos += 2;
        if (flen > max_ck_str) return error.Corrupt;
        if (payload.len - pos < flen) return error.Corrupt;
        manifest_flags = payload[pos..][0..flen];
        pos += flen;
    }
    var integrate: []const u8 = "";
    if (pos != payload.len) {
        if (payload.len - pos < 2) return error.Corrupt;
        const ilen: usize = std.mem.readInt(u16, payload[pos..][0..2], .little);
        pos += 2;
        if (ilen > max_ck_str) return error.Corrupt;
        if (payload.len - pos < ilen) return error.Corrupt;
        integrate = payload[pos..][0..ilen];
        pos += ilen;
    }

    if (pos != payload.len) return error.Corrupt; // no trailing bytes
    return .{
        .version = version,
        .branch = branch,
        .merge_parent = merge_parent,
        .special_op = special_op,
        .manifest_flags = manifest_flags,
        .integrate = integrate,
    };
}

// ---------------------------------------------------------------------------
// Tests (storage-track-plan.md item E; no RFC section behind this file).
// ---------------------------------------------------------------------------

test "checkout plan E: write then read round-trips version branch merge_parent and special_op" {
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    const io = std.testing.io;

    const state: CheckoutState = .{ .version = "a" ** 64, .branch = "trunk", .merge_parent = "b" ** 64, .special_op = "", .manifest_flags = "" };
    try write(io, tmp.dir, state);

    var buf: [max_ck_payload + 1]u8 = undefined;
    const got = (try read(io, tmp.dir, &buf)).?;
    try std.testing.expectEqualStrings(state.version, got.version);
    try std.testing.expectEqualStrings(state.branch, got.branch);
    try std.testing.expectEqualStrings(state.merge_parent, got.merge_parent);
    try std.testing.expectEqualStrings("", got.special_op);

    // A pending special-merge op round-trips independently of merge_parent.
    const cp: CheckoutState = .{ .version = "a" ** 64, .branch = "trunk", .merge_parent = "", .special_op = "+" ++ "d" ** 64, .manifest_flags = "" };
    try write(io, tmp.dir, cp);
    const got2 = (try read(io, tmp.dir, &buf)).?;
    try std.testing.expectEqualStrings("", got2.merge_parent);
    try std.testing.expectEqualStrings("+" ++ "d" ** 64, got2.special_op);

    // The manifest opt-in round-trips independently of every other field.
    const mf: CheckoutState = .{ .version = "a" ** 64, .branch = "trunk", .merge_parent = "", .special_op = "", .manifest_flags = "rut" };
    try write(io, tmp.dir, mf);
    const got3 = (try read(io, tmp.dir, &buf)).?;
    try std.testing.expectEqualStrings("rut", got3.manifest_flags);
}

test "checkout plan E: an empty merge_parent and special_op round-trip as no pending op" {
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    const io = std.testing.io;

    try write(io, tmp.dir, .{ .version = "a" ** 64, .branch = "trunk", .merge_parent = "", .special_op = "", .manifest_flags = "" });

    var buf: [max_ck_payload + 1]u8 = undefined;
    const got = (try read(io, tmp.dir, &buf)).?;
    try std.testing.expectEqualStrings("a" ** 64, got.version);
    try std.testing.expectEqualStrings("trunk", got.branch);
    try std.testing.expectEqualStrings("", got.merge_parent);
    try std.testing.expectEqualStrings("", got.special_op);
}

test "checkout plan E: legacy two-field and three-field records parse with empty trailing fields" {
    // A record written before merge_parent existed carries only version+branch
    // and ends after the branch. parseCk must yield merge_parent/special_op ==
    // "" rather than reject it — errors-out-of-existence, §E.
    const legacy2 = [_]u8{ 0x01, 0x00, 'v', 0x05, 0x00, 't', 'r', 'u', 'n', 'k' };
    const got2 = try parseCk(&legacy2);
    try std.testing.expectEqualStrings("v", got2.version);
    try std.testing.expectEqualStrings("trunk", got2.branch);
    try std.testing.expectEqualStrings("", got2.merge_parent);
    try std.testing.expectEqualStrings("", got2.special_op);
    try std.testing.expectEqualStrings("", got2.manifest_flags);

    // A three-field record (version+branch+merge_parent, no special_op) parses
    // with special_op == "".
    const legacy3 = [_]u8{ 0x01, 0x00, 'v', 0x05, 0x00, 't', 'r', 'u', 'n', 'k', 0x01, 0x00, 'm' };
    const got3 = try parseCk(&legacy3);
    try std.testing.expectEqualStrings("m", got3.merge_parent);
    try std.testing.expectEqualStrings("", got3.special_op);
    try std.testing.expectEqualStrings("", got3.manifest_flags);

    // A four-field record (no manifest_flags) parses as "write no manifest
    // files" — the behaviour such a checkout already had.
    const legacy4 = [_]u8{ 0x01, 0x00, 'v', 0x05, 0x00, 't', 'r', 'u', 'n', 'k', 0x00, 0x00, 0x00, 0x00 };
    const got4 = try parseCk(&legacy4);
    try std.testing.expectEqualStrings("v", got4.version);
    try std.testing.expectEqualStrings("", got4.manifest_flags);
}

test "checkout plan E: a second write atomically replaces the state (last wins)" {
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    const io = std.testing.io;

    try write(io, tmp.dir, .{ .version = "1" ** 40, .branch = "feature", .merge_parent = "", .special_op = "", .manifest_flags = "" });
    try write(io, tmp.dir, .{ .version = "2" ** 40, .branch = "release", .merge_parent = "", .special_op = "", .manifest_flags = "" });

    var buf: [max_ck_payload + 1]u8 = undefined;
    const got = (try read(io, tmp.dir, &buf)).?;
    try std.testing.expectEqualStrings("2" ** 40, got.version);
    try std.testing.expectEqualStrings("release", got.branch);
}

test "checkout plan E: read of an absent checkout file returns null" {
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    const io = std.testing.io;

    var buf: [max_ck_payload + 1]u8 = undefined;
    try std.testing.expectEqual(@as(?CheckoutState, null), try read(io, tmp.dir, &buf));
}

test "checkout plan E: a partial write that never materializes leaves the prior state intact" {
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    const io = std.testing.io;

    try write(io, tmp.dir, .{ .version = "old" ** 8, .branch = "trunk", .merge_parent = "", .special_op = "", .manifest_flags = "" });

    // Begin a fresh atomic file, write bytes, then drop it WITHOUT replace:
    // deinit deletes the temp, and a reader only ever sees `filename`.
    {
        var af = try tmp.dir.createFileAtomic(io, filename, .{ .replace = true });
        defer af.deinit(io);
        var b: [max_ck_payload]u8 = undefined;
        const bytes = encodeCk(&b, .{ .version = "new" ** 8, .branch = "feature", .merge_parent = "", .special_op = "", .manifest_flags = "" });
        try af.file.writePositionalAll(io, bytes, 0);
        // no af.replace(io): the temp is abandoned
    }

    var buf: [max_ck_payload + 1]u8 = undefined;
    const got = (try read(io, tmp.dir, &buf)).?;
    try std.testing.expectEqualStrings("old" ** 8, got.version);
    try std.testing.expectEqualStrings("trunk", got.branch);
}

test "checkout plan E: parseCk rejects malformed checkout records" {
    // TLV bounds: one rejection per condition (storage-track-plan.md item E).
    var over: [4 + 300]u8 = undefined;
    std.mem.writeInt(u16, over[0..2], 300, .little); // version_len > max_ck_str
    const cases = [_][]const u8{
        &[_]u8{0x00}, // truncated version_len (1 of 2 bytes)
        over[0..2], // version_len exceeds max_ck_str
        &[_]u8{ 0x05, 0x00, 'a', 'b' }, // version_len 5 exceeds remaining
        &[_]u8{ 0x01, 0x00, 'v', 0x02 }, // truncated branch_len after version
        &[_]u8{ 0x01, 0x00, 'v', 0x00, 0x00, 0x09 }, // truncated merge_parent_len after branch
        &[_]u8{ 0x01, 0x00, 'v', 0x00, 0x00, 0x05, 0x00, 'a' }, // merge_parent_len 5 exceeds remaining
        &[_]u8{ 0x01, 0x00, 'v', 0x00, 0x00, 0x00, 0x00, 0x09 }, // truncated special_op_len after merge_parent
        &[_]u8{ 0x01, 0x00, 'v', 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 'a' }, // special_op_len 5 exceeds remaining
        &[_]u8{ 0x01, 0x00, 'v', 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x09 }, // truncated manifest_flags_len after special_op
        &[_]u8{ 0x01, 0x00, 'v', 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 'a' }, // manifest_flags_len 5 exceeds remaining
        &[_]u8{ 0x01, 0x00, 'v', 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x09 }, // trailing byte after manifest_flags
    };
    for (cases) |c| try std.testing.expectError(error.Corrupt, parseCk(c));

    // Whole-file variant: garbage in `filename` surfaces as Corrupt.
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    const io = std.testing.io;
    {
        var f = try tmp.dir.createFile(io, filename, .{});
        defer f.close(io);
        try f.writePositionalAll(io, &[_]u8{ 0xff, 0xff, 0xaa }, 0); // version_len 0xffff
    }
    var buf: [max_ck_payload + 1]u8 = undefined;
    try std.testing.expectError(error.Corrupt, read(io, tmp.dir, &buf));
}

test "checkout plan E: a file one byte past a maximal record is rejected, not truncated" {
    const io = std.testing.io;

    // The unique maximal record consumes exactly max_ck_payload bytes
    // (four length-prefixed 255-byte strings). It must still round-trip.
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try write(io, tmp.dir, .{ .version = "v" ** max_ck_str, .branch = "b" ** max_ck_str, .merge_parent = "m" ** max_ck_str, .special_op = "s" ** max_ck_str, .manifest_flags = "f" ** max_ck_str, .integrate = "i" ** max_ck_str });
    var okbuf: [max_ck_payload + 1]u8 = undefined;
    const ok = (try read(io, tmp.dir, &okbuf)).?;
    try std.testing.expectEqual(@as(usize, max_ck_str), ok.version.len);
    try std.testing.expectEqual(@as(usize, max_ck_str), ok.branch.len);
    try std.testing.expectEqual(@as(usize, max_ck_str), ok.merge_parent.len);
    try std.testing.expectEqual(@as(usize, max_ck_str), ok.special_op.len);
    try std.testing.expectEqual(@as(usize, max_ck_str), ok.manifest_flags.len);
    try std.testing.expectEqual(@as(usize, max_ck_str), ok.integrate.len);

    // The same maximal record with one trailing byte is a longer-than-any-record
    // file: read must reject it rather than parse the maximal prefix and drop
    // the excess.
    var tmp2 = std.testing.tmpDir(.{});
    defer tmp2.cleanup();
    var rec: [max_ck_payload]u8 = undefined;
    const maximal = encodeCk(&rec, .{ .version = "v" ** max_ck_str, .branch = "b" ** max_ck_str, .merge_parent = "m" ** max_ck_str, .special_op = "s" ** max_ck_str, .manifest_flags = "f" ** max_ck_str, .integrate = "i" ** max_ck_str });
    try std.testing.expectEqual(@as(usize, max_ck_payload), maximal.len);
    {
        var f = try tmp2.dir.createFile(io, filename, .{});
        defer f.close(io);
        try f.writePositionalAll(io, maximal, 0);
        try f.writePositionalAll(io, &[_]u8{0x00}, @intCast(maximal.len));
    }
    var buf: [max_ck_payload + 1]u8 = undefined;
    try std.testing.expectError(error.Corrupt, read(io, tmp2.dir, &buf));
}

// ---------------------------------------------------------------------------
// Container v2 tests (consolidation DESIGN §5.1). Two invariants, modelled on
// the repocache suite: a valid container round-trips every section
// byte-exactly, and every framing violation is error.Corrupt — never partial
// trust.
// ---------------------------------------------------------------------------

fn writeRawTestFile(io: std.Io, dir: std.Io.Dir, name: []const u8, bytes: []const u8) !void {
    var f = try dir.createFile(io, name, .{ .truncate = true });
    defer f.close(io);
    try f.writePositionalAll(io, bytes, 0);
}

fn testFileExists(io: std.Io, dir: std.Io.Dir, name: []const u8) bool {
    var f = dir.openFile(io, name, .{}) catch return false;
    f.close(io);
    return true;
}

test "container: updateSections round-trips every section byte-exactly" {
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    const io = std.testing.io;
    const gpa = std.testing.allocator;

    const state: CheckoutState = .{ .version = "a" ** 64, .branch = "trunk", .merge_parent = "", .special_op = "", .manifest_flags = "" };
    try write(io, tmp.dir, state);
    const managed_payload = "\x01\x00\x00\x00" ++ "\x05\x00" ++ "a.txt"; // any bytes: the container frames, never interprets
    const undo_payload = "UNDO-JOURNAL-BYTES";
    const bisect_payload = "BISECT-RECORD";
    try updateSections(io, tmp.dir, &.{
        .{ .tag = .managed, .payload = managed_payload },
        .{ .tag = .undo, .payload = undo_payload },
        .{ .tag = .bisect, .payload = bisect_payload },
    });

    // The file is a container (byte-1 dispatch) and every section reads back
    // byte-for-byte, including the checkout carried across the second splice.
    const raw = try tmp.dir.readFileAlloc(io, filename, gpa, .limited(1 << 16));
    defer gpa.free(raw);
    try std.testing.expect(raw.len > container_magic.len);
    try std.testing.expectEqualSlices(u8, container_magic, raw[0..container_magic.len]);
    try std.testing.expect(isContainer(raw));

    var ckbuf: [max_ck_payload + 1]u8 = undefined;
    const got = (try read(io, tmp.dir, &ckbuf)).?;
    try std.testing.expectEqualStrings(state.version, got.version);
    try std.testing.expectEqualStrings(state.branch, got.branch);

    inline for (.{ .{ SectionTag.managed, managed_payload }, .{ SectionTag.undo, undo_payload }, .{ SectionTag.bisect, bisect_payload } }) |pair| {
        const bytes = (try readSectionAlloc(gpa, io, tmp.dir, pair[0])).?;
        defer gpa.free(bytes);
        try std.testing.expectEqualSlices(u8, pair[1], bytes);
    }

    // Replacing one section leaves the others byte-identical.
    try updateSections(io, tmp.dir, &.{.{ .tag = .undo, .payload = "SWAPPED" }});
    const undo2 = (try readSectionAlloc(gpa, io, tmp.dir, .undo)).?;
    defer gpa.free(undo2);
    try std.testing.expectEqualStrings("SWAPPED", undo2);
    const managed2 = (try readSectionAlloc(gpa, io, tmp.dir, .managed)).?;
    defer gpa.free(managed2);
    try std.testing.expectEqualSlices(u8, managed_payload, managed2);
    const got2 = (try read(io, tmp.dir, &ckbuf)).?;
    try std.testing.expectEqualStrings(state.version, got2.version);
}

test "container: absent file and absent section both read as null" {
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    const io = std.testing.io;
    const gpa = std.testing.allocator;

    try std.testing.expectEqual(@as(?[]u8, null), try readSectionAlloc(gpa, io, tmp.dir, .undo));

    try write(io, tmp.dir, .{ .version = "v", .branch = "b", .merge_parent = "", .special_op = "", .manifest_flags = "" });
    try std.testing.expectEqual(@as(?[]u8, null), try readSectionAlloc(gpa, io, tmp.dir, .bisect));
}

test "container: removing the last section deletes the file" {
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    const io = std.testing.io;

    try write(io, tmp.dir, .{ .version = "v", .branch = "b", .merge_parent = "", .special_op = "", .manifest_flags = "" });
    try updateSections(io, tmp.dir, &.{.{ .tag = .checkout, .payload = null }});
    var buf: [max_ck_payload + 1]u8 = undefined;
    try std.testing.expectEqual(@as(?CheckoutState, null), try read(io, tmp.dir, &buf));
    // Not just "no checkout section" — the file itself is gone.
    try std.testing.expect(!testFileExists(io, tmp.dir, filename));
}

test "container: every framing violation is Corrupt, never partial trust" {
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    const io = std.testing.io;
    const gpa = std.testing.allocator;

    try write(io, tmp.dir, .{ .version = "v" ** 64, .branch = "trunk", .merge_parent = "", .special_op = "", .manifest_flags = "" });
    try updateSections(io, tmp.dir, &.{.{ .tag = .undo, .payload = "JOURNAL" }});
    const good = try tmp.dir.readFileAlloc(io, filename, gpa, .limited(1 << 16));
    defer gpa.free(good);

    const Case = struct { name: []const u8, mutate: *const fn (bytes: []u8) void };
    const cases = [_]Case{
        .{ .name = "wrong magic", .mutate = struct {
            fn f(b: []u8) void {
                b[0] = 'X';
            }
        }.f },
        .{ .name = "wrong version", .mutate = struct {
            fn f(b: []u8) void {
                b[8] = 99;
            }
        }.f },
        .{ .name = "nonzero reserved", .mutate = struct {
            fn f(b: []u8) void {
                b[11] = 1;
            }
        }.f },
        .{ .name = "zero section count", .mutate = struct {
            fn f(b: []u8) void {
                b[10] = 0;
            }
        }.f },
        .{ .name = "oversized section count", .mutate = struct {
            fn f(b: []u8) void {
                b[10] = 5;
            }
        }.f },
        .{
            .name = "unknown tag",
            .mutate = struct {
                fn f(b: []u8) void {
                    b[container_header_len] = 7; // first table entry's tag
                }
            }.f,
        },
        .{
            .name = "duplicate tag (breaks strict ascent)",
            .mutate = struct {
                fn f(b: []u8) void {
                    b[container_header_len + table_entry_len] = b[container_header_len]; // second tag = first tag
                }
            }.f,
        },
        .{ .name = "unordered tags", .mutate = struct {
            fn f(b: []u8) void {
                std.mem.swap(u8, &b[container_header_len], &b[container_header_len + table_entry_len]);
            }
        }.f },
        .{
            .name = "zero-length section",
            .mutate = struct {
                fn f(b: []u8) void {
                    @memset(b[container_header_len + 1 ..][0..8], 0); // first entry's len = 0
                }
            }.f,
        },
        .{
            .name = "table len exceeds file",
            .mutate = struct {
                fn f(b: []u8) void {
                    b[container_header_len + 1 + 7] = 0xff; // first entry's len high byte
                }
            }.f,
        },
        .{ .name = "flipped payload byte (crc)", .mutate = struct {
            fn f(b: []u8) void {
                b[b.len - crc_len - 1] ^= 0xff;
            }
        }.f },
        .{ .name = "flipped crc byte", .mutate = struct {
            fn f(b: []u8) void {
                b[b.len - 1] ^= 0xff;
            }
        }.f },
    };

    var ckbuf: [max_ck_payload + 1]u8 = undefined;
    for (cases) |case| {
        const bad = try gpa.dupe(u8, good);
        defer gpa.free(bad);
        case.mutate(bad);
        try writeRawTestFile(io, tmp.dir, filename, bad);
        // Both readers reject; neither returns data or "absent".
        try std.testing.expectError(error.Corrupt, read(io, tmp.dir, &ckbuf));
        try std.testing.expectError(error.Corrupt, readSectionAlloc(gpa, io, tmp.dir, .undo));
        // And a splice refuses to carry anything forward from it.
        try std.testing.expectError(error.Corrupt, updateSections(io, tmp.dir, &.{.{ .tag = .bisect, .payload = "B" }}));
    }

    // Truncation and trailing garbage break the exact byte accounting.
    try writeRawTestFile(io, tmp.dir, filename, good[0 .. good.len - 1]);
    try std.testing.expectError(error.Corrupt, read(io, tmp.dir, &ckbuf));
    const extended = try gpa.alloc(u8, good.len + 1);
    defer gpa.free(extended);
    @memcpy(extended[0..good.len], good);
    extended[good.len] = 0;
    try writeRawTestFile(io, tmp.dir, filename, extended);
    try std.testing.expectError(error.Corrupt, read(io, tmp.dir, &ckbuf));
    try std.testing.expectError(error.Corrupt, readSectionAlloc(gpa, io, tmp.dir, .undo));
}

test "container migration: a legacy checkout folds its siblings in on first write, then the siblings are dead" {
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    const io = std.testing.io;
    const gpa = std.testing.allocator;

    // A pre-container checkout: legacy TLV record + three sibling files.
    var lbuf: [max_ck_payload]u8 = undefined;
    const legacy_rec = encodeCk(&lbuf, .{ .version = "1" ** 64, .branch = "trunk", .merge_parent = "", .special_op = "", .manifest_flags = "" });
    try writeRawTestFile(io, tmp.dir, filename, legacy_rec);
    try writeRawTestFile(io, tmp.dir, legacyName(.managed).?, "MANAGED-IMAGE");
    try writeRawTestFile(io, tmp.dir, legacyName(.undo).?, "UNDO-IMAGE");
    try writeRawTestFile(io, tmp.dir, legacyName(.bisect).?, "BISECT-IMAGE");

    // Legacy reads work before any write: the siblings ARE the sections.
    {
        const m = (try readSectionAlloc(gpa, io, tmp.dir, .managed)).?;
        defer gpa.free(m);
        try std.testing.expectEqualStrings("MANAGED-IMAGE", m);
    }

    // First write migrates: container materialized, every sibling folded in
    // byte-exactly, then deleted.
    try write(io, tmp.dir, .{ .version = "2" ** 64, .branch = "feature", .merge_parent = "", .special_op = "", .manifest_flags = "" });
    const raw = try tmp.dir.readFileAlloc(io, filename, gpa, .limited(1 << 16));
    defer gpa.free(raw);
    try std.testing.expect(isContainer(raw));
    inline for (.{ .{ SectionTag.managed, "MANAGED-IMAGE" }, .{ SectionTag.undo, "UNDO-IMAGE" }, .{ SectionTag.bisect, "BISECT-IMAGE" } }) |pair| {
        const bytes = (try readSectionAlloc(gpa, io, tmp.dir, pair[0])).?;
        defer gpa.free(bytes);
        try std.testing.expectEqualStrings(pair[1], bytes);
        // The sibling file is gone.
        try std.testing.expect(!testFileExists(io, tmp.dir, legacyName(pair[0]).?));
    }
    var ckbuf: [max_ck_payload + 1]u8 = undefined;
    const got = (try read(io, tmp.dir, &ckbuf)).?;
    try std.testing.expectEqualStrings("2" ** 64, got.version);

    // Once a container exists a (re-planted) sibling is dead: never read.
    try writeRawTestFile(io, tmp.dir, legacyName(.undo).?, "STALE-NEVER-READ");
    const u = (try readSectionAlloc(gpa, io, tmp.dir, .undo)).?;
    defer gpa.free(u);
    try std.testing.expectEqualStrings("UNDO-IMAGE", u);
    // ...and the next write sweeps it away.
    try updateSections(io, tmp.dir, &.{.{ .tag = .bisect, .payload = "B2" }});
    try std.testing.expect(!testFileExists(io, tmp.dir, legacyName(.undo).?));
}

test "container migration: migration is idempotent and preserves bytes across repeated writes" {
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    const io = std.testing.io;
    const gpa = std.testing.allocator;

    var lbuf: [max_ck_payload]u8 = undefined;
    const legacy_rec = encodeCk(&lbuf, .{ .version = "1" ** 64, .branch = "trunk", .merge_parent = "", .special_op = "", .manifest_flags = "" });
    try writeRawTestFile(io, tmp.dir, filename, legacy_rec);
    try writeRawTestFile(io, tmp.dir, legacyName(.undo).?, "UNDO-IMAGE");

    // The same logical write applied twice (as a crash-replay would) yields
    // the same container bytes.
    try updateSections(io, tmp.dir, &.{.{ .tag = .bisect, .payload = "B" }});
    const first = try tmp.dir.readFileAlloc(io, filename, gpa, .limited(1 << 16));
    defer gpa.free(first);
    try updateSections(io, tmp.dir, &.{.{ .tag = .bisect, .payload = "B" }});
    const second = try tmp.dir.readFileAlloc(io, filename, gpa, .limited(1 << 16));
    defer gpa.free(second);
    try std.testing.expectEqualSlices(u8, first, second);
}

test "container migration: a corrupt legacy record refuses to migrate" {
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    const io = std.testing.io;

    try writeRawTestFile(io, tmp.dir, filename, &[_]u8{ 0xff, 0x00, 0xaa }); // version_len 0xff, no bytes behind it
    try std.testing.expectError(error.Corrupt, updateSections(io, tmp.dir, &.{.{ .tag = .undo, .payload = "J" }}));

    // A zero-byte sibling is corrupt in every absorbed format: refuse to
    // launder it into a valid-looking absence.
    var tmp2 = std.testing.tmpDir(.{});
    defer tmp2.cleanup();
    var lbuf: [max_ck_payload]u8 = undefined;
    const legacy_rec = encodeCk(&lbuf, .{ .version = "v", .branch = "b", .merge_parent = "", .special_op = "", .manifest_flags = "" });
    try writeRawTestFile(io, tmp2.dir, filename, legacy_rec);
    try writeRawTestFile(io, tmp2.dir, legacyName(.managed).?, "");
    try std.testing.expectError(error.Corrupt, updateSections(io, tmp2.dir, &.{.{ .tag = .undo, .payload = "J" }}));
}

test "container v3: a v2 container reads and migrates to v3 on first splice" {
    const io = std.testing.io;
    const gpa = std.testing.allocator;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    // Hand-build a v2 container: header(version=2) + one checkout section +
    // crc. Byte layout per the v2 shipped format; this test IS the pin that
    // v3 readers keep folding it.
    var ckbuf: [max_ck_payload]u8 = undefined;
    const ck = encodeCk(&ckbuf, .{ .version = "v2v", .branch = "trunk", .merge_parent = "", .special_op = "", .manifest_flags = "" });
    var v2: std.ArrayList(u8) = .empty;
    defer v2.deinit(gpa);
    try v2.appendSlice(gpa, container_magic);
    try v2.appendSlice(gpa, &.{ 2, 0 }); // version u16 LE = 2
    try v2.appendSlice(gpa, &.{ 1, 0 }); // count, reserved
    try v2.append(gpa, 1); // tag: checkout
    var len8: [8]u8 = undefined;
    std.mem.writeInt(u64, &len8, ck.len, .little);
    try v2.appendSlice(gpa, &len8);
    try v2.appendSlice(gpa, ck);
    var crc = crc32.Crc32.init();
    crc.update(v2.items);
    var crc4: [4]u8 = undefined;
    std.mem.writeInt(u32, &crc4, crc.final(), .little);
    try v2.appendSlice(gpa, &crc4);
    try writeRawTestFile(io, tmp.dir, filename, v2.items);

    // Reads fold the v2 generation.
    const got = (try readSectionAlloc(gpa, io, tmp.dir, .checkout)).?;
    defer gpa.free(got);
    try std.testing.expectEqualSlices(u8, ck, got);

    // A splice rewrites as v3, carrying the v2 section byte-exactly.
    try updateSections(io, tmp.dir, &.{.{ .tag = .cone, .payload = "CONE" }});
    var file = try tmp.dir.openFile(io, filename, .{});
    defer file.close(io);
    var head: [10]u8 = undefined;
    _ = try file.readPositionalAll(io, &head, 0);
    try std.testing.expectEqual(container_version, std.mem.readInt(u16, head[8..10], .little));
    const carried = (try readSectionAlloc(gpa, io, tmp.dir, .checkout)).?;
    defer gpa.free(carried);
    try std.testing.expectEqualSlices(u8, ck, carried);
    const cone_payload = (try readSectionAlloc(gpa, io, tmp.dir, .cone)).?;
    defer gpa.free(cone_payload);
    try std.testing.expectEqualSlices(u8, "CONE", cone_payload);
}

test "container v3: version negative space — v2 with a v3 tag, and a future version, both refuse" {
    const io = std.testing.io;
    const gpa = std.testing.allocator;

    const Case = struct { version: u8, tag: u8 };
    // An older table may not smuggle a newer tag; a future container
    // version refuses loudly rather than misparses. Same gate an old
    // binary hits on meeting any newer version.
    for ([_]Case{ .{ .version = 2, .tag = 5 }, .{ .version = 3, .tag = 7 }, .{ .version = 5, .tag = 1 } }) |case_| {
        var tmp = std.testing.tmpDir(.{});
        defer tmp.cleanup();
        var bytes: std.ArrayList(u8) = .empty;
        defer bytes.deinit(gpa);
        try bytes.appendSlice(gpa, container_magic);
        try bytes.appendSlice(gpa, &.{ case_.version, 0, 1, 0 });
        try bytes.append(gpa, case_.tag);
        var len8: [8]u8 = undefined;
        std.mem.writeInt(u64, &len8, 1, .little);
        try bytes.appendSlice(gpa, &len8);
        try bytes.append(gpa, 'X');
        var crc = crc32.Crc32.init();
        crc.update(bytes.items);
        var crc4: [4]u8 = undefined;
        std.mem.writeInt(u32, &crc4, crc.final(), .little);
        try bytes.appendSlice(gpa, &crc4);
        try writeRawTestFile(io, tmp.dir, filename, bytes.items);
        try std.testing.expectError(error.Corrupt, readSectionAlloc(gpa, io, tmp.dir, .checkout));
    }
}

test "container v3: store_location is carried verbatim through splices it does not name" {
    const io = std.testing.io;
    const gpa = std.testing.allocator;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    // Reserve the worktree seat: the payload is opaque here and must survive
    // unrelated splices byte-identically.
    const opaque_loc = "/somewhere/else/.zit\x00binary\xffgoo";
    try updateSections(io, tmp.dir, &.{.{ .tag = .store_location, .payload = opaque_loc }});
    try updateSections(io, tmp.dir, &.{.{ .tag = .undo, .payload = "JOURNAL" }});
    try updateSections(io, tmp.dir, &.{.{ .tag = .undo, .payload = null }});
    const got = (try readSectionAlloc(gpa, io, tmp.dir, .store_location)).?;
    defer gpa.free(got);
    try std.testing.expectEqualSlices(u8, opaque_loc, got);
}

// Seeded deterministic fuzz: parseCk on adversarial bytes yields a value or
// error.Corrupt, never a crash; and parseCk(encodeCk(x)) == x for any bounded
// pair.
test "checkout plan E fuzz: parseCk never crashes and round-trips encodeCk" {
    var prng = std.Random.DefaultPrng.init(0xC4EC);
    const random = prng.random();
    var iter: usize = 0;
    while (iter < 400) : (iter += 1) {
        // Round-trip: parseCk(encodeCk(x)) == x for random bounded strings.
        var vbuf: [max_ck_str]u8 = undefined;
        var bbuf: [max_ck_str]u8 = undefined;
        var mbuf: [max_ck_str]u8 = undefined;
        var sbuf: [max_ck_str]u8 = undefined;
        var fbuf: [max_ck_str]u8 = undefined;
        const vn = random.uintAtMost(usize, max_ck_str);
        const bn = random.uintAtMost(usize, max_ck_str);
        const mn = random.uintAtMost(usize, max_ck_str);
        const sn = random.uintAtMost(usize, max_ck_str);
        const fn_ = random.uintAtMost(usize, max_ck_str);
        random.bytes(vbuf[0..vn]); // wire data embeds NUL legally
        random.bytes(bbuf[0..bn]);
        random.bytes(mbuf[0..mn]);
        random.bytes(sbuf[0..sn]);
        random.bytes(fbuf[0..fn_]);
        var enc: [max_ck_payload]u8 = undefined;
        const bytes = encodeCk(&enc, .{ .version = vbuf[0..vn], .branch = bbuf[0..bn], .merge_parent = mbuf[0..mn], .special_op = sbuf[0..sn], .manifest_flags = fbuf[0..fn_] });
        const rt = try parseCk(bytes);
        try std.testing.expectEqualSlices(u8, vbuf[0..vn], rt.version);
        try std.testing.expectEqualSlices(u8, bbuf[0..bn], rt.branch);
        try std.testing.expectEqualSlices(u8, mbuf[0..mn], rt.merge_parent);
        try std.testing.expectEqualSlices(u8, sbuf[0..sn], rt.special_op);
        try std.testing.expectEqualSlices(u8, fbuf[0..fn_], rt.manifest_flags);

        // Garbage: any random byte string is a value or Corrupt, never a crash.
        var garbage: [64]u8 = undefined;
        const gn = random.uintAtMost(usize, garbage.len);
        random.bytes(garbage[0..gn]);
        if (parseCk(garbage[0..gn])) |_| {} else |err| switch (err) {
            error.Corrupt => {},
        }
    }
}