Skip to content
zit Artifact [beb6e26b27]
Login

Artifact [beb6e26b27]

Artifact beb6e26b27b0d0d7b85d1f8e52f0e29458e503eab6aae7b63358af7e26f78324:


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-vstat",    ".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));
}