Artifact bc1a94f35675065cee6bf29e9236b443e2a1879ec2f9b65ef0141b1042a77219:
- File
src/control_paths.zig
— part of check-in
[581db6e5ba]
at
2026-07-31 01:41:43
on branch sub-manifest
— control_paths: one worktree-path admission policy
Two layers decided independently what a reserved path is. update carried its own validateWorktreePath — an absolute/backslash/dot-dot check plus a hand-rolled case-insensitive comparison against the .zit family — while extras carried isControlName, which tested only the first /-delimited component of a path. They agreed by coincidence rather than construction, and the extras form admitted `.zit-managed/entries` because it asked about the component instead of the root.
control_paths now owns both questions. validateWorktreePath and isReservedRoot are the single definitions; update delegates to the first and extras to the second. isControlName takes the full root-relative key that add and move already hand it, so a path rooted in a control entry is refused at any depth rather than only at depth zero. firstComponent goes with its last caller.
The update test that pinned the old list is now a case table naming what is reserved and what merely resembles it, including the case-insensitive spellings the previous ad-hoc comparison happened to get right. (user: dmestas, size: 3682) [annotate] [blame] [check-ins using]
const std = @import("std"); pub const WorktreePathError = error{ InvalidPath, ReservedPath, }; /// Valid relative working-tree paths use `/` separators, contain no empty or /// dot components, and never claim a reserved root. Import and update share /// this check so neither can admit a path the other would later reject. pub fn validateWorktreePath(path: []const u8) WorktreePathError!void { if (path.len == 0 or std.fs.path.isAbsolute(path)) return error.InvalidPath; if (std.mem.indexOfScalar(u8, path, '\\') != null) return error.InvalidPath; var components = std.mem.splitScalar(u8, path, '/'); const root = components.next() orelse return error.InvalidPath; if (root.len == 0 or std.mem.eql(u8, root, ".") or std.mem.eql(u8, root, "..")) return error.InvalidPath; if (isReservedRoot(path)) return error.ReservedPath; while (components.next()) |component| { if (component.len == 0 or std.mem.eql(u8, component, ".") or std.mem.eql(u8, component, "..")) return error.InvalidPath; } } /// Root-level checkout metadata names that imported and materialized paths /// must never own. Inspects only the first slash-delimited path component. pub fn isReservedRoot(path: []const u8) bool { const slash = std.mem.indexOfScalar(u8, path, '/') orelse path.len; const root = path[0..slash]; const names = [_][]const u8{ ".zit", ".zit-checkout", ".zit-managed", ".zit-undo", ".zit-bisect", ".zit-cache", ".zit-pack-tmp", ".zit-import-map.jsonl", "refs", }; for (names) |reserved| if (asciiEqlIgnoreCase(root, reserved)) return true; return asciiStartsWithIgnoreCase(root, ".zit-update-txn-"); } fn asciiEqlIgnoreCase(a: []const u8, b: []const u8) bool { if (a.len != b.len) return false; for (a, b) |left, right| { if (std.ascii.toLower(left) != std.ascii.toLower(right)) return false; } return true; } fn asciiStartsWithIgnoreCase(value: []const u8, prefix: []const u8) bool { return value.len >= prefix.len and asciiEqlIgnoreCase(value[0..prefix.len], prefix); } test "control path policy reserves complete roots case-insensitively" { const Case = struct { path: []const u8, reserved: bool }; const cases = [_]Case{ .{ .path = ".zit", .reserved = true }, .{ .path = ".ZiT/object", .reserved = true }, .{ .path = ".zit-checkout", .reserved = true }, .{ .path = ".ZiT-Checkout/state", .reserved = true }, .{ .path = ".zit-managed/entries", .reserved = true }, .{ .path = ".zit-undo/transaction", .reserved = true }, .{ .path = ".zit-bisect/state", .reserved = true }, .{ .path = ".zit-cache/data", .reserved = true }, .{ .path = ".zit-pack-tmp/scratch", .reserved = true }, .{ .path = ".zit-import-map.jsonl/sidecar", .reserved = true }, .{ .path = "ReFs/heads/main", .reserved = true }, .{ .path = ".zit-update-txn-user/file", .reserved = true }, .{ .path = ".ZiT-UpDaTe-TxN-user/file", .reserved = true }, .{ .path = "", .reserved = false }, .{ .path = "zit", .reserved = false }, .{ .path = ".zit-other/data", .reserved = false }, .{ .path = ".zit-cacheable/data", .reserved = false }, .{ .path = ".zit-checkout-user/file", .reserved = false }, .{ .path = ".zit-update-txn", .reserved = false }, .{ .path = ".zit-update-txns/user", .reserved = false }, .{ .path = "refs-old/heads/main", .reserved = false }, .{ .path = "sub/.zit/object", .reserved = false }, }; for (cases) |case_| try std.testing.expectEqual(case_.reserved, isReservedRoot(case_.path)); }