//! Fossil interop smoke check — the one sanctioned use of the real `fossil`
//! binary. `fossil` is invoked nowhere else in this tree.
//!
//! Policy, enforced by this file's shape, not by convention:
//! - Every byte handed to fossil is zit's OWN output (`artifact.create` /
//! `delta.create` over an RFC fixture) -- never the raw RFC transcription,
//! never anything fossil produced.
//! - fossil may only CONFIRM an RFC-cited zit expectation. A scenario that
//! would require comparing zit bytes to fossil-AUTHORED bytes (e.g.
//! `fossil test-delta-create`) does not belong here.
//! - On any mismatch, print the diff and the RFC citation and exit 1. This
//! file never adjusts zit to match fossil -- a mismatch is an operator
//! decision (RFC misread vs RFC wrong), recorded in the untracked
//! ambiguities ledger, not "fixed" in place.
//!
//! Milestone-manual: this executable lives solely behind `zig build smoke`.
//! `zig build test` compiles it (catching type errors) but never runs its
//! scenarios; it is not imported by `root.zig`'s test aggregation.
const std = @import("std");
const builtin = @import("builtin");
const zit = @import("zit");
const artifact = zit.artifact;
const rfc_vectors = zit.rfc_vectors;
const delta = zit.delta;
const build_options = @import("build_options");
const net = std.Io.net;
/// The compiled `zit` binary, threaded in by build.zig (a `LazyPath`, always
/// rebuilt first). `build_options.zit_exe` is relative to the build root, which
/// is this process's cwd; S5 resolves it to an absolute path before spawning it
/// as a subprocess (`zit init/add/commit/pack/serve`). Confined to S5 — the
/// other scenarios drive only the `zit` LIBRARY, never the binary.
const zit_exe: []const u8 = build_options.zit_exe;
// ---------------------------------------------------------------------------
// RFC fixtures -- the same vectors src/artifact.zig's own tests cite.
// ---------------------------------------------------------------------------
const a1_1_manifest = rfc_vectors.artifact_a1_1_sha1_manifest;
const a1_2_technote = rfc_vectors.artifact_a1_2_sha3_technote;
const a3_manifest = rfc_vectors.artifact_a3_rcard_manifest;
/// §5/Appendix B.1 delta worked-example pair (102/151-byte sizes), reused
/// verbatim from delta.zig's own "create() then apply() round-trips the
/// target" test so S4 exercises the same RFC-cited content.
const delta_source = "The quick brown fox jumps over the lazy dog. A Fossil delta encodes byte-level differences compactly.\n";
const delta_target = "The quick brown fox jumps over the sleeping fox. A Fossil delta encodes byte-level differences compactly.\n An example delta target with appended tail.\n";
const ManifestScenario = struct {
name: []const u8,
citation: []const u8,
fixture: []const u8,
algo: artifact.HashAlgo,
};
const manifest_scenarios = [_]ManifestScenario{
.{ .name = "S1", .citation = "§A.1.1/§A.2", .fixture = a1_1_manifest, .algo = .sha1 },
.{ .name = "S2", .citation = "§A.1.2", .fixture = a1_2_technote, .algo = .sha3_256 },
.{ .name = "S3", .citation = "§A.3", .fixture = a3_manifest, .algo = .sha3_256 },
};
pub fn main(init: std.process.Init) !void {
const gpa = init.gpa;
const io = init.io;
var stdout_buffer: [4096]u8 = undefined;
var stdout_writer: std.Io.File.Writer = .init(.stdout(), io, &stdout_buffer);
const out = &stdout_writer.interface;
// Record the exact fossil the scenarios confirm against, verbatim, at the
// head of the smoke log: the version line and the PASS/FAIL results then sit
// in one block the release notes cite (readiness item S7). A null return is
// the milestone-manual skip — fossil absent, nothing to confirm against.
if (try fossilVersion(gpa, io)) |ver| {
defer gpa.free(ver);
try out.print("fossil version: {s}\n", .{std.mem.trim(u8, ver, " \t\r\n")});
} else {
try out.writeAll("fossil not found — smoke is milestone-manual\n");
try out.flush();
return;
}
var arena_state = std.heap.ArenaAllocator.init(gpa);
defer arena_state.deinit();
const arena = arena_state.allocator();
const tmp_root = try tempRoot(arena, init.environ_map.*);
std.Io.Dir.cwd().deleteTree(io, tmp_root) catch {};
try std.Io.Dir.cwd().createDirPath(io, tmp_root);
defer std.Io.Dir.cwd().deleteTree(io, tmp_root) catch {};
var env = try init.environ_map.clone(gpa);
defer env.deinit();
const home_dir = try std.fs.path.join(arena, &.{ tmp_root, "home" });
try std.Io.Dir.cwd().createDirPath(io, home_dir);
try env.put("FOSSIL_HOME", home_dir);
var all_pass = true;
for (manifest_scenarios) |sc| {
const pass = runManifestScenario(arena, gpa, io, tmp_root, &env, out, sc) catch |err| blk: {
try out.print("FAIL {s} ({s}): unexpected error running scenario: {s}\n", .{ sc.name, sc.citation, @errorName(err) });
break :blk false;
};
all_pass = all_pass and pass;
}
const s4_pass = runDeltaScenario(arena, gpa, io, tmp_root, &env, out) catch |err| blk: {
try out.print("FAIL S4 (§5/§Appendix B.1): unexpected error running scenario: {s}\n", .{@errorName(err)});
break :blk false;
};
all_pass = all_pass and s4_pass;
const s5_pass = runCloneFromRepackedScenario(arena, gpa, io, tmp_root, &env, out) catch |err| blk: {
try out.print("FAIL S5 (§7.5/§3): unexpected error running scenario: {s}\n", .{@errorName(err)});
break :blk false;
};
all_pass = all_pass and s5_pass;
const s6_pass = runOpenFromFossilServerScenario(arena, gpa, io, tmp_root, &env, out) catch |err| blk: {
try out.print("FAIL S6 (§3.7/§4): unexpected error running scenario: {s}\n", .{@errorName(err)});
break :blk false;
};
all_pass = all_pass and s6_pass;
const s7_pass = runPushToFossilServerScenario(arena, gpa, io, tmp_root, &env, out) catch |err| blk: {
try out.print("FAIL S7 (§3.3/§3.7): unexpected error running scenario: {s}\n", .{@errorName(err)});
break :blk false;
};
all_pass = all_pass and s7_pass;
const s8_pass = runPullFossilExtensionScenario(arena, gpa, io, tmp_root, &env, out) catch |err| blk: {
try out.print("FAIL S8 (§3.2/§3.7): unexpected error running scenario: {s}\n", .{@errorName(err)});
break :blk false;
};
all_pass = all_pass and s8_pass;
const s9_pass = runCloneOfShunnedScenario(arena, gpa, io, tmp_root, &env, out) catch |err| blk: {
try out.print("FAIL S9 (§9.5/§8.2): unexpected error running scenario: {s}\n", .{@errorName(err)});
break :blk false;
};
all_pass = all_pass and s9_pass;
const s10_pass = runPushOfShunnedScenario(arena, gpa, io, tmp_root, &env, out) catch |err| blk: {
try out.print("FAIL S10 (§7.7): unexpected error running scenario: {s}\n", .{@errorName(err)});
break :blk false;
};
all_pass = all_pass and s10_pass;
const s11_pass = runPushPolicyRefusalScenario(arena, gpa, io, tmp_root, &env, out) catch |err| blk: {
try out.print("FAIL S11 (push content policy): unexpected error running scenario: {s}\n", .{@errorName(err)});
break :blk false;
};
all_pass = all_pass and s11_pass;
const s12_pass = runUserConfigPullScenario(arena, gpa, io, tmp_root, &env, out) catch |err| blk: {
try out.print("FAIL S12 (config pull user): unexpected error running scenario: {s}\n", .{@errorName(err)});
break :blk false;
};
all_pass = all_pass and s12_pass;
const s13_pass = runEncodingWarningScenario(arena, gpa, io, tmp_root, &env, out) catch |err| blk: {
try out.print("FAIL S13 (commit encoding warning): unexpected error running scenario: {s}\n", .{@errorName(err)});
break :blk false;
};
all_pass = all_pass and s13_pass;
const s14_pass = runSettingsFixtureScenario(arena, gpa, io, tmp_root, &env, out) catch |err| blk: {
try out.print("FAIL S14 (.fossil-settings): unexpected error running scenario: {s}\n", .{@errorName(err)});
break :blk false;
};
all_pass = all_pass and s14_pass;
const s15_pass = runAttachmentInteropScenario(arena, gpa, io, tmp_root, &env, out) catch |err| blk: {
try out.print("FAIL S15 (attach add): unexpected error running scenario: {s}\n", .{@errorName(err)});
break :blk false;
};
all_pass = all_pass and s15_pass;
const s16_pass = runMultiCheckinPushScenario(arena, gpa, io, tmp_root, &env, out) catch |err| blk: {
try out.print("FAIL S16 (multi-check-in push): unexpected error running scenario: {s}\n", .{@errorName(err)});
break :blk false;
};
all_pass = all_pass and s16_pass;
const s17_pass = runPrivateReadScenario(arena, gpa, io, tmp_root, &env, out) catch |err| blk: {
try out.print("FAIL S17 (private read): unexpected error running scenario: {s}\n", .{@errorName(err)});
break :blk false;
};
all_pass = all_pass and s17_pass;
const s18_pass = runUnversionedScenario(arena, gpa, io, tmp_root, &env, out) catch |err| blk: {
try out.print("FAIL S18 (unversioned files): unexpected error running scenario: {s}\n", .{@errorName(err)});
break :blk false;
};
all_pass = all_pass and s18_pass;
const s19_pass = runProtectedDivertScenario(arena, gpa, io, tmp_root, &env, out) catch |err| blk: {
try out.print("FAIL S19 (protected-branch divert): unexpected error running scenario: {s}\n", .{@errorName(err)});
break :blk false;
};
all_pass = all_pass and s19_pass;
try out.flush();
if (!all_pass) std.process.exit(1);
}
/// S12 — the user-table configuration area, against a REAL fossil
/// server: `zit config pull user` adopts the peer's logins and capabilities,
/// and an ANONYMOUS attempt gets nothing and changes nothing.
///
/// This is the one lane where the admin gate can be proved rather than
/// assumed. Fossil strips `CONFIGSET_USER` from a non-administrator's group
/// mask (`configure.c`, `xfer.c`), so the boundary is fossil's, not zit's —
/// and a unit test against zit's own server could not observe it at all,
/// because zit's server has no capability model and therefore never serves
/// this area. The server is spawned WITHOUT `--localauth`, which would
/// auto-authenticate a loopback client as the default administrator and
/// quietly turn the anonymous half of this test into a second credentialed
/// one.
fn runUserConfigPullScenario(
arena: std.mem.Allocator,
gpa: std.mem.Allocator,
io: std.Io,
tmp_root: []const u8,
env: *const std.process.Environ.Map,
out: *std.Io.Writer,
) !bool {
const citation = "config pull user";
const abs_exe = try zitAbsPath(arena, io);
const base = try std.fs.path.join(arena, &.{ tmp_root, "S12" });
try std.Io.Dir.cwd().createDirPath(io, base);
const peer_repo = try std.fs.path.join(arena, &.{ base, "peer.fossil" });
const mirror = try std.fs.path.join(arena, &.{ base, "mirror" });
{
const res = try runFossil(gpa, io, &.{ "fossil", "init", peer_repo }, base, env);
defer gpa.free(res.stdout);
defer gpa.free(res.stderr);
if (!okExit(res.term)) {
try out.print("FAIL S12 ({s}): fossil init failed\n stderr: {s}\n", .{ citation, res.stderr });
return false;
}
}
// A named administrator whose capabilities this pull must reproduce.
{
const res = try runFossil(gpa, io, &.{ "fossil", "user", "new", "smokeadmin", "Smoke Admin", "sekret", "-R", peer_repo }, base, env);
defer gpa.free(res.stdout);
defer gpa.free(res.stderr);
if (!okExit(res.term)) {
try out.print("FAIL S12 ({s}): fossil user new failed\n stderr: {s}\n", .{ citation, res.stderr });
return false;
}
}
{
const res = try runFossil(gpa, io, &.{ "fossil", "user", "capabilities", "smokeadmin", "s", "-R", peer_repo }, base, env);
defer gpa.free(res.stdout);
defer gpa.free(res.stderr);
if (!okExit(res.term)) {
try out.print("FAIL S12 ({s}): fossil user capabilities failed\n stderr: {s}\n", .{ citation, res.stderr });
return false;
}
}
const port = try freshLoopbackPort(io);
const port_str = try std.fmt.allocPrint(arena, "{d}", .{port});
var server = spawnFossilServe(gpa, io, out, peer_repo, base, env, port_str) catch |err| {
try out.print("FAIL S12 ({s}): could not spawn fossil server: {s}\n", .{ citation, @errorName(err) });
return false;
};
defer stopFossilServe(io, &server);
waitForListen(io, port) catch {
try out.print("FAIL S12 ({s}): fossil server never bound 127.0.0.1:{d}\n", .{ citation, port });
return false;
};
const url = try std.fmt.allocPrint(arena, "http://127.0.0.1:{d}/", .{port});
// A mirror of the peer: the project code must match or fossil refuses the
// session outright (§9.1), so the clone is part of the fixture.
{
const res = try std.process.run(gpa, io, .{ .argv = &.{ abs_exe, "clone", url, mirror }, .cwd = .{ .path = base }, .environ_map = env });
defer gpa.free(res.stdout);
defer gpa.free(res.stderr);
if (!okExit(res.term)) {
try out.print("FAIL S12 ({s}): zit clone of the fossil peer failed\n stderr: {s}\n", .{ citation, res.stderr });
return false;
}
}
// ANONYMOUS: fossil serves no user rows, zit says so, and nothing lands.
{
const res = try std.process.run(gpa, io, .{ .argv = &.{ abs_exe, "config", "pull", "user" }, .cwd = .{ .path = mirror }, .environ_map = env });
defer gpa.free(res.stdout);
defer gpa.free(res.stderr);
if (okExit(res.term)) {
try out.print("FAIL S12 ({s}): an anonymous user pull SUCCEEDED; fossil's admin gate is not being observed\n", .{citation});
try printMismatchFooter(out, citation);
return false;
}
const listed = try std.process.run(gpa, io, .{ .argv = &.{ abs_exe, "users" }, .cwd = .{ .path = mirror }, .environ_map = env });
defer gpa.free(listed.stdout);
defer gpa.free(listed.stderr);
if (listed.stdout.len != 0) {
try out.print("FAIL S12 ({s}): a refused pull still wrote rows: {s}\n", .{ citation, listed.stdout });
return false;
}
}
// CREDENTIALED: the rows arrive, with fossil's capability string verbatim.
{
const res = try std.process.run(gpa, io, .{
.argv = &.{ abs_exe, "config", "pull", "user", "--user=smokeadmin", "--password=sekret" },
.cwd = .{ .path = mirror },
.environ_map = env,
});
defer gpa.free(res.stdout);
defer gpa.free(res.stderr);
if (!okExit(res.term)) {
try out.print("FAIL S12 ({s}): a credentialed user pull failed\n stdout: {s}\n stderr: {s}\n", .{ citation, res.stdout, res.stderr });
try printMismatchFooter(out, citation);
return false;
}
}
{
const listed = try std.process.run(gpa, io, .{ .argv = &.{ abs_exe, "users" }, .cwd = .{ .path = mirror }, .environ_map = env });
defer gpa.free(listed.stdout);
defer gpa.free(listed.stderr);
if (std.mem.indexOf(u8, listed.stdout, "smokeadmin\ts\t") == null) {
try out.print("FAIL S12 ({s}): the pulled table does not carry smokeadmin with fossil's caps\n got: {s}\n", .{ citation, listed.stdout });
try printMismatchFooter(out, citation);
return false;
}
// The hash is withheld from the default listing even though the row
// carries one — the same rule the unit lane pins, proved here over a
// password fossil itself hashed.
if (std.mem.count(u8, listed.stdout, "\t") > 3 * std.mem.count(u8, listed.stdout, "\n")) {
try out.print("FAIL S12 ({s}): the default listing has too many columns; is it printing the hash?\n got: {s}\n", .{ citation, listed.stdout });
return false;
}
}
// An authenticated push lands on the peer, and a saved login then
// authenticates a push that names no user. fossil's anonymous user cannot
// write, so a check-in reaching the peer's timeline is proof the login card
// went out and was accepted. This is the end-to-end guard for the omitted-
// URL credential drop and for the stored-login reuse: `config pull` stores
// nothing, so the first authenticated push is what stores the secret.
{
const a_path = try std.fs.path.join(arena, &.{ mirror, "a191.txt" });
try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = a_path, .data = "first\n" });
inline for (.{
.{ &[_][]const u8{ "add", "a191.txt" }, "zit add" },
.{ &[_][]const u8{ "commit", "-m", "auth-push", "--user", "u", "--no-sync" }, "zit commit" },
.{ &[_][]const u8{ "push", "--user", "smokeadmin", "--password", "sekret" }, "zit push --user (no URL, uses saved remote)" },
}) |step| {
const r = (try zitStep(gpa, io, out, abs_exe, mirror, env, step[0], step[1])) orelse return false;
gpa.free(r.stdout);
gpa.free(r.stderr);
}
{
const tl = try runFossil(gpa, io, &.{ "fossil", "timeline", "-n", "5", "-t", "ci", "-R", peer_repo }, base, env);
defer gpa.free(tl.stdout);
defer gpa.free(tl.stderr);
if (std.mem.indexOf(u8, tl.stdout, "auth-push") == null) {
try out.print("FAIL S12 ({s}): an authenticated push with no URL did not reach the fossil peer\n timeline: {s}\n", .{ citation, tl.stdout });
try printMismatchFooter(out, citation);
return false;
}
}
// The stored login now authenticates a push that names no user.
try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = a_path, .data = "second\n" });
inline for (.{
.{ &[_][]const u8{ "commit", "-m", "stored-login", "--user", "u", "--no-sync" }, "zit commit" },
.{ &[_][]const u8{"push"}, "zit push (no --user, stored login)" },
}) |step| {
const r = (try zitStep(gpa, io, out, abs_exe, mirror, env, step[0], step[1])) orelse return false;
gpa.free(r.stdout);
gpa.free(r.stderr);
}
const tl = try runFossil(gpa, io, &.{ "fossil", "timeline", "-n", "5", "-t", "ci", "-R", peer_repo }, base, env);
defer gpa.free(tl.stdout);
defer gpa.free(tl.stderr);
if (std.mem.indexOf(u8, tl.stdout, "stored-login") == null) {
try out.print("FAIL S12 ({s}): a push with no user, relying on the stored login, did not reach the fossil peer\n timeline: {s}\n", .{ citation, tl.stdout });
try printMismatchFooter(out, citation);
return false;
}
}
try out.print("PASS S12 ({s}): a credentialed 'config pull user' reproduces a fossil peer's logins and capabilities; an anonymous one is refused and writes nothing; an authenticated push and a stored-login push both land on the peer\n", .{citation});
return true;
}
// ---------------------------------------------------------------------------
// Preflight
// ---------------------------------------------------------------------------
/// The runner's `fossil version` output, captured verbatim, or null when fossil
/// is absent or errors. Doubles as the availability gate (null ⇒ skip) and the
/// version record printed at the head of the smoke log. Caller frees a non-null
/// return.
/// S15 — an attachment zit authored is an attachment fossil reads.
/// Pushed to a REAL fossil server, the A artifact crosslinks into fossil's
/// `attachment` table under the target and name zit recorded, with zit's blob
/// as its `src`, and the blob itself is in fossil's store. This is the interop
/// check the issue asks for: the format is written in one place and read by
/// the other implementation.
fn runAttachmentInteropScenario(
arena: std.mem.Allocator,
gpa: std.mem.Allocator,
io: std.Io,
tmp_root: []const u8,
env: *const std.process.Environ.Map,
out: *std.Io.Writer,
) !bool {
const citation = "attach add";
const abs_exe = try zitAbsPath(arena, io);
const base = try std.fs.path.join(arena, &.{ tmp_root, "S15" });
try std.Io.Dir.cwd().createDirPath(io, base);
const peer_repo = try std.fs.path.join(arena, &.{ base, "peer.fossil" });
const mirror = try std.fs.path.join(arena, &.{ base, "mirror" });
{
const res = try runFossil(gpa, io, &.{ "fossil", "init", peer_repo }, base, env);
defer gpa.free(res.stdout);
defer gpa.free(res.stderr);
if (!okExit(res.term)) {
try out.print("FAIL S15 ({s}): fossil init failed\n stderr: {s}\n", .{ citation, res.stderr });
return false;
}
}
// An anonymous push needs check-in capability on the `nobody` login.
{
const res = try runFossil(gpa, io, &.{ "fossil", "user", "capabilities", "nobody", "gjorzi", "-R", peer_repo }, base, env);
defer gpa.free(res.stdout);
defer gpa.free(res.stderr);
if (!okExit(res.term)) {
try out.print("FAIL S15 ({s}): fossil user capabilities failed\n stderr: {s}\n", .{ citation, res.stderr });
return false;
}
}
const port = try freshLoopbackPort(io);
const port_str = try std.fmt.allocPrint(arena, "{d}", .{port});
var server = spawnFossilServe(gpa, io, out, peer_repo, base, env, port_str) catch |err| {
try out.print("FAIL S15 ({s}): could not spawn fossil server: {s}\n", .{ citation, @errorName(err) });
return false;
};
defer stopFossilServe(io, &server);
waitForListen(io, port) catch {
try out.print("FAIL S15 ({s}): fossil server never bound 127.0.0.1:{d}\n", .{ citation, port });
return false;
};
const url = try std.fmt.allocPrint(arena, "http://127.0.0.1:{d}/", .{port});
{
const res = try std.process.run(gpa, io, .{ .argv = &.{ abs_exe, "clone", url, mirror }, .cwd = .{ .path = base }, .environ_map = env });
defer gpa.free(res.stdout);
defer gpa.free(res.stderr);
if (!okExit(res.term)) {
try out.print("FAIL S15 ({s}): zit clone of the fossil peer failed\n stderr: {s}\n", .{ citation, res.stderr });
return false;
}
}
try std.Io.Dir.cwd().writeFile(io, .{
.sub_path = try std.fs.path.join(arena, &.{ mirror, "note.txt" }),
.data = "an attachment authored by zit\n",
});
var blob_hash: []const u8 = "";
{
const res = try std.process.run(gpa, io, .{ .argv = &.{ abs_exe, "attach", "add", "Home", "note.txt", "--user", "smoke", "--comment", "interop" }, .cwd = .{ .path = mirror }, .environ_map = env });
defer gpa.free(res.stdout);
defer gpa.free(res.stderr);
if (!okExit(res.term)) {
try out.print("FAIL S15 ({s}): zit attach add failed\n stderr: {s}\n", .{ citation, res.stderr });
return false;
}
var it = std.mem.splitScalar(u8, std.mem.trim(u8, res.stdout, "\n"), '\n');
_ = it.next() orelse "";
blob_hash = try arena.dupe(u8, it.next() orelse "");
if (blob_hash.len != 64) {
try out.print("FAIL S15 ({s}): attach add did not print the blob hash on its second line\n stdout: {s}\n", .{ citation, res.stdout });
return false;
}
}
{
const res = try std.process.run(gpa, io, .{ .argv = &.{ abs_exe, "push", url }, .cwd = .{ .path = mirror }, .environ_map = env });
defer gpa.free(res.stdout);
defer gpa.free(res.stderr);
if (!okExit(res.term)) {
try out.print("FAIL S15 ({s}): zit push to the fossil server failed\n stderr: {s}\n", .{ citation, res.stderr });
return false;
}
}
stopFossilServe(io, &server);
// Fossil crosslinked it: one live attachment row for (Home, note.txt)
// whose src is the blob zit stored, and the blob is in fossil's store.
const sql = try std.fmt.allocPrint(arena, "select count(*) from attachment where target='Home' and filename='note.txt' and src='{s}' and isLatest;", .{blob_hash});
{
const res = try runFossil(gpa, io, &.{ "fossil", "sqlite3", "--readonly", sql, "-R", peer_repo }, base, env);
defer gpa.free(res.stdout);
defer gpa.free(res.stderr);
const got = std.mem.trim(u8, res.stdout, " \t\r\n");
if (!okExit(res.term) or !std.mem.eql(u8, got, "1")) {
try out.print("FAIL S15 ({s}): fossil holds no live attachment row for (Home, note.txt) with zit's blob as src\n count: {s}\n stderr: {s}\n", .{ citation, got, res.stderr });
return false;
}
}
const sql_blob = try std.fmt.allocPrint(arena, "select count(*) from blob where uuid='{s}';", .{blob_hash});
{
const res = try runFossil(gpa, io, &.{ "fossil", "sqlite3", "--readonly", sql_blob, "-R", peer_repo }, base, env);
defer gpa.free(res.stdout);
defer gpa.free(res.stderr);
const got = std.mem.trim(u8, res.stdout, " \t\r\n");
if (!okExit(res.term) or !std.mem.eql(u8, got, "1")) {
try out.print("FAIL S15 ({s}): fossil does not hold the attachment blob {s}\n stderr: {s}\n", .{ citation, blob_hash, res.stderr });
return false;
}
}
try out.print("PASS S15 ({s}): a zit-authored attachment pushed to a real fossil server crosslinks as a live attachment on its target, with zit's blob as its source\n", .{citation});
return true;
}
fn fossilVersion(gpa: std.mem.Allocator, io: std.Io) !?[]u8 {
const res = std.process.run(gpa, io, .{ .argv = &.{ "fossil", "version" } }) catch return null;
defer gpa.free(res.stderr);
if (!okExit(res.term)) {
gpa.free(res.stdout);
return null;
}
return res.stdout;
}
fn tempRoot(arena: std.mem.Allocator, env: std.process.Environ.Map) ![]const u8 {
const base = env.get("TMPDIR") orelse "/tmp";
const trimmed = std.mem.trimEnd(u8, base, "/");
return std.fs.path.join(arena, &.{ trimmed, "zit-smoke" });
}
// ---------------------------------------------------------------------------
// fossil process helper
// ---------------------------------------------------------------------------
fn okExit(term: std.process.Child.Term) bool {
return switch (term) {
.exited => |code| code == 0,
else => false,
};
}
fn runFossil(
gpa: std.mem.Allocator,
io: std.Io,
argv: []const []const u8,
cwd_path: []const u8,
env: *const std.process.Environ.Map,
) !std.process.RunResult {
return std.process.run(gpa, io, .{
.argv = argv,
.cwd = .{ .path = cwd_path },
.environ_map = env,
});
}
/// First whitespace-delimited token of `text` (the hash column of
/// `fossil sha1sum`/`sha3sum` output: "<hash> <path>\n").
fn firstToken(text: []const u8) []const u8 {
const end = std.mem.indexOfAny(u8, text, " \t\r\n") orelse text.len;
return text[0..end];
}
// ---------------------------------------------------------------------------
// Failure reporting -- never authored from fossil's side, only printed.
// ---------------------------------------------------------------------------
fn printBytesPreview(out: *std.Io.Writer, label: []const u8, bytes: []const u8) !void {
try out.print(" {s} (len {d}): ", .{ label, bytes.len });
const max = @min(bytes.len, 96);
for (bytes[0..max]) |b| try out.print("{x:0>2}", .{b});
if (bytes.len > max) try out.writeAll("...");
try out.writeAll("\n");
}
fn firstDiffOffset(a: []const u8, b: []const u8) ?usize {
const n = @min(a.len, b.len);
var i: usize = 0;
while (i < n) : (i += 1) {
if (a[i] != b[i]) return i;
}
if (a.len != b.len) return n;
return null;
}
fn printDiffWindow(out: *std.Io.Writer, zit_bytes: []const u8, fossil_bytes: []const u8) !void {
const off = firstDiffOffset(zit_bytes, fossil_bytes) orelse return;
const start = if (off > 16) off - 16 else 0;
const zit_end = @min(zit_bytes.len, off + 16);
const fossil_end = @min(fossil_bytes.len, off + 16);
try out.print(" first diff at byte {d}\n", .{off});
try out.writeAll(" zit : ");
for (zit_bytes[start..zit_end]) |b| try out.print("{x:0>2}", .{b});
try out.writeAll("\n fossil : ");
for (fossil_bytes[start..fossil_end]) |b| try out.print("{x:0>2}", .{b});
try out.writeAll("\n");
}
fn printMismatchFooter(out: *std.Io.Writer, citation: []const u8) !void {
try out.print(" RFC citation: {s}\n", .{citation});
try out.writeAll(
\\ Fossil disagrees with zit's RFC-cited expectation. Operator decides
\\ RFC-misread vs RFC-wrong; record in ambiguities.md. Never change zit
\\ to match fossil.
\\
);
}
// ---------------------------------------------------------------------------
// S1/S2/S3 -- manifest scenarios (parse -> create -> confirm via fossil)
// ---------------------------------------------------------------------------
fn runManifestScenario(
arena: std.mem.Allocator,
gpa: std.mem.Allocator,
io: std.Io,
tmp_root: []const u8,
env: *const std.process.Environ.Map,
out: *std.Io.Writer,
sc: ManifestScenario,
) !bool {
// zit's own product: the artifact fed to fossil is `create()`'s output,
// never the raw RFC transcription (§6.2 round-trip; the two happen to be
// byte-identical for S1/S3 and a strict-lexical reorder for S2, but the
// input to fossil is always what zit itself wrote).
var m = try artifact.parse(gpa, sc.fixture);
defer m.deinit();
const zit_bytes = try artifact.create(&m, gpa);
defer gpa.free(zit_bytes);
var name_buf: [64]u8 = undefined;
const zit_name = artifact.artifactName(sc.algo, zit_bytes, &name_buf);
const base = try std.fs.path.join(arena, &.{ tmp_root, sc.name });
const repo_dir = try std.fs.path.join(arena, &.{ base, "repo" });
const checkout_dir = try std.fs.path.join(arena, &.{ base, "checkout" });
try std.Io.Dir.cwd().createDirPath(io, repo_dir);
try std.Io.Dir.cwd().createDirPath(io, checkout_dir);
const manifest_path = try std.fs.path.join(arena, &.{ repo_dir, "manifest.txt" });
try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = manifest_path, .data = zit_bytes });
// fossil test-parse-manifest: repo-free syntactic accept (§4/§5). Exit is
// 0 either way -- the VERDICT TEXT is what confirms zit's parse.
{
const res = try runFossil(gpa, io, &.{ "fossil", "test-parse-manifest", manifest_path }, repo_dir, env);
defer gpa.free(res.stdout);
defer gpa.free(res.stderr);
if (std.mem.indexOf(u8, res.stdout, "reports the input is ok") == null) {
try out.print("FAIL {s} ({s}): fossil test-parse-manifest did not report the input well-formed\n", .{ sc.name, sc.citation });
try printBytesPreview(out, "zit bytes", zit_bytes);
try out.print(" fossil verdict: {s}\n", .{res.stdout});
try printMismatchFooter(out, sc.citation);
return false;
}
}
// fossil {sha1,sha3}sum: confirms zit's own artifactName computation.
{
const sum_cmd = if (sc.algo == .sha1) "sha1sum" else "sha3sum";
const res = try runFossil(gpa, io, &.{ "fossil", sum_cmd, manifest_path }, repo_dir, env);
defer gpa.free(res.stdout);
defer gpa.free(res.stderr);
const fossil_name = firstToken(res.stdout);
if (!std.mem.eql(u8, fossil_name, zit_name)) {
try out.print("FAIL {s} ({s}): fossil {s} disagrees with zit's artifactName\n", .{ sc.name, sc.citation, sum_cmd });
try out.print(" zit name: {s}\n", .{zit_name});
try out.print(" fossil name: {s}\n", .{fossil_name});
try printMismatchFooter(out, sc.citation);
return false;
}
}
// init + open + put + retrieve: confirms zit's create() output
// round-trips through fossil's own content store under the given name.
const repo_fossil = try std.fs.path.join(arena, &.{ repo_dir, "r.fossil" });
const init_argv: []const []const u8 = if (sc.algo == .sha1)
&.{ "fossil", "init", "--sha1", repo_fossil }
else
&.{ "fossil", "init", repo_fossil };
{
const res = try runFossil(gpa, io, init_argv, repo_dir, env);
defer gpa.free(res.stdout);
defer gpa.free(res.stderr);
if (!okExit(res.term)) {
try out.print("FAIL {s} ({s}): fossil init failed\n stderr: {s}\n", .{ sc.name, sc.citation, res.stderr });
try printMismatchFooter(out, sc.citation);
return false;
}
}
{
const res = try runFossil(gpa, io, &.{ "fossil", "open", repo_fossil }, checkout_dir, env);
defer gpa.free(res.stdout);
defer gpa.free(res.stderr);
if (!okExit(res.term)) {
try out.print("FAIL {s} ({s}): fossil open failed\n stderr: {s}\n", .{ sc.name, sc.citation, res.stderr });
try printMismatchFooter(out, sc.citation);
return false;
}
}
{
const res = try runFossil(gpa, io, &.{ "fossil", "test-content-put", manifest_path }, checkout_dir, env);
defer gpa.free(res.stdout);
defer gpa.free(res.stderr);
if (!okExit(res.term)) {
try out.print("FAIL {s} ({s}): fossil test-content-put failed\n stderr: {s}\n", .{ sc.name, sc.citation, res.stderr });
try printMismatchFooter(out, sc.citation);
return false;
}
}
const artifact_out_path = try std.fs.path.join(arena, &.{ checkout_dir, "out.bin" });
{
const res = try runFossil(gpa, io, &.{ "fossil", "artifact", zit_name, "out.bin" }, checkout_dir, env);
defer gpa.free(res.stdout);
defer gpa.free(res.stderr);
if (!okExit(res.term)) {
try out.print("FAIL {s} ({s}): fossil artifact could not resolve zit's name {s}\n stderr: {s}\n", .{ sc.name, sc.citation, zit_name, res.stderr });
try printMismatchFooter(out, sc.citation);
return false;
}
}
const fossil_bytes = try std.Io.Dir.cwd().readFileAlloc(io, artifact_out_path, gpa, .limited(1 << 20));
defer gpa.free(fossil_bytes);
if (!std.mem.eql(u8, zit_bytes, fossil_bytes)) {
try out.print("FAIL {s} ({s}): fossil artifact returned bytes different from zit's create() output\n", .{ sc.name, sc.citation });
try printBytesPreview(out, "zit bytes", zit_bytes);
try printBytesPreview(out, "fossil bytes", fossil_bytes);
try printDiffWindow(out, zit_bytes, fossil_bytes);
try printMismatchFooter(out, sc.citation);
return false;
}
try out.print("PASS {s} ({s}): zit's create() output confirmed by fossil (name {s})\n", .{ sc.name, sc.citation, zit_name });
return true;
}
// ---------------------------------------------------------------------------
// S4 -- delta round-trip (never compared against fossil test-delta-create;
// fossil only applies zit's own delta and confirms the reconstructed target)
// ---------------------------------------------------------------------------
fn runDeltaScenario(
arena: std.mem.Allocator,
gpa: std.mem.Allocator,
io: std.Io,
tmp_root: []const u8,
env: *const std.process.Environ.Map,
out: *std.Io.Writer,
) !bool {
const citation = "§5/§Appendix B.1";
const zit_delta = try delta.create(gpa, delta_source, delta_target);
defer gpa.free(zit_delta);
// zit's own apply() must already reconstruct the target -- fossil only
// ever confirms this, never establishes it.
const self_applied = try delta.apply(gpa, delta_source, zit_delta);
defer gpa.free(self_applied);
std.debug.assert(std.mem.eql(u8, self_applied, delta_target));
const base = try std.fs.path.join(arena, &.{ tmp_root, "S4" });
try std.Io.Dir.cwd().createDirPath(io, base);
const src_path = try std.fs.path.join(arena, &.{ base, "src.bin" });
const delta_path = try std.fs.path.join(arena, &.{ base, "zit.delta" });
const out_path = try std.fs.path.join(arena, &.{ base, "out.bin" });
try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = src_path, .data = delta_source });
try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = delta_path, .data = zit_delta });
{
const res = try runFossil(gpa, io, &.{ "fossil", "test-delta-apply", src_path, delta_path, out_path }, base, env);
defer gpa.free(res.stdout);
defer gpa.free(res.stderr);
if (!okExit(res.term)) {
try out.print("FAIL S4 ({s}): fossil test-delta-apply rejected zit's delta.create() output\n stderr: {s}\n", .{ citation, res.stderr });
try printBytesPreview(out, "zit delta", zit_delta);
try printMismatchFooter(out, citation);
return false;
}
}
const fossil_out = try std.Io.Dir.cwd().readFileAlloc(io, out_path, gpa, .limited(1 << 20));
defer gpa.free(fossil_out);
if (!std.mem.eql(u8, fossil_out, delta_target)) {
try out.print("FAIL S4 ({s}): fossil test-delta-apply produced bytes different from zit's target\n", .{citation});
try printBytesPreview(out, "zit target", delta_target);
try printBytesPreview(out, "fossil bytes", fossil_out);
try printDiffWindow(out, delta_target, fossil_out);
try printMismatchFooter(out, citation);
return false;
}
try out.print("PASS S4 ({s}): fossil test-delta-apply reconstructs zit's delta.create() target\n", .{citation});
return true;
}
// ---------------------------------------------------------------------------
// S5 -- clone from a re-packed store (§7.5, covering the §3
// serve-regenerated-delta note). The one scenario that drives the real `zit`
// BINARY: build a repo, evolve a file so its blob is stored as a delta, run
// `zit pack` so that delta is REGENERATED against our size-minimizing base, then
// serve the repacked store and have the real `fossil` client CLONE it. fossil
// hash-verifies every artifact it reconstructs on receipt, so a clone that exits
// 0 proves the regenerated delta container zit serves reconstructs byte-for-byte
// over the wire; the checked-out file is compared to zit's own committed bytes as
// the final oracle. Every byte fossil sees is zit's own output — fossil only
// confirms, never authors (the file policy at the top of this file).
// ---------------------------------------------------------------------------
/// Allocates a fresh loopback port for a child server. The reservation listener
/// closes before the child binds it; that close→child-bind race is unavoidable
/// because child servers cannot inherit the listener.
fn freshLoopbackPort(io: std.Io) !u16 {
const addr = try net.IpAddress.parseIp4("127.0.0.1", 0);
var server = try addr.listen(io, .{});
const port = server.socket.address.getPort();
server.deinit(io);
if (port == 0) return error.NoLoopbackPort;
return port;
}
/// Build the S5 file blob: 200 similar lines so `zit pack`'s best-of-N base
/// selection produces a profitable delta. `changed` mutates one line and appends
/// a tail, so the second commit's blob deltifies well against the first.
fn buildBlob(arena: std.mem.Allocator, changed: bool) ![]u8 {
var aw = std.Io.Writer.Allocating.init(arena); // arena-owned; no deinit
var i: usize = 0;
while (i < 200) : (i += 1) {
if (changed and i == 50) {
try aw.writer.print("line {d}: CHANGED CONTENT for the regenerated-delta smoke\n", .{i});
} else {
try aw.writer.print("line {d}: the quick brown fox jumps over the lazy dog number {d}\n", .{ i, i });
}
}
if (changed) try aw.writer.writeAll("line 200: appended tail exercises the delta insert op\n");
return aw.toOwnedSlice();
}
/// Absolute path to the compiled `zit` binary. `zit_exe` is relative to the
/// build root (this process's cwd); a spawned child chdirs before exec, so it
/// must be resolved first. Allocated on `arena`.
fn zitAbsPath(arena: std.mem.Allocator, io: std.Io) ![]const u8 {
const cwd_abs = try std.process.currentPathAlloc(io, arena);
return std.fs.path.resolve(arena, &.{ cwd_abs, zit_exe });
}
/// Blocking `zit <tail...>` with cwd `cwd_path` and the shared smoke env
/// (FOSSIL_HOME set). Caller owns `result.stdout`/`result.stderr`.
fn runZit(
gpa: std.mem.Allocator,
io: std.Io,
abs_exe: []const u8,
cwd_path: []const u8,
env: *const std.process.Environ.Map,
tail: []const []const u8,
) !std.process.RunResult {
var argv_buf: [8][]const u8 = undefined;
argv_buf[0] = abs_exe;
std.debug.assert(tail.len <= argv_buf.len - 1);
for (tail, argv_buf[1..][0..tail.len]) |src, *dst| dst.* = src;
return std.process.run(gpa, io, .{
.argv = argv_buf[0 .. 1 + tail.len],
.cwd = .{ .path = cwd_path },
.environ_map = env,
});
}
/// Spawn `zit serve --port <port>` detached, cwd `cwd_path` (so `discoverRoot`
/// finds `cwd_path/.zit`). stdout/stderr discarded — driven purely over the
/// socket. Caller `kill`s the returned child.
fn spawnZitServe(
io: std.Io,
abs_exe: []const u8,
cwd_path: []const u8,
env: *const std.process.Environ.Map,
port_str: []const u8,
) !std.process.Child {
return spawnZitServeArgs(io, abs_exe, cwd_path, env, &.{ abs_exe, "serve", "--port", port_str });
}
fn spawnZitServeArgs(
io: std.Io,
abs_exe: []const u8,
cwd_path: []const u8,
env: *const std.process.Environ.Map,
argv: []const []const u8,
) !std.process.Child {
_ = abs_exe;
return std.process.spawn(io, .{
.argv = argv,
.cwd = .{ .path = cwd_path },
.environ_map = env,
.stdout = .ignore,
.stderr = .ignore,
});
}
/// Bounded wait for a server to bind: retry-connect to the loopback port
/// until accepted or a wall-clock deadline elapses (never a fixed sleep). A
/// successful TCP connect proves the listener is up.
fn waitForListen(io: std.Io, port: u16) !void {
const addr = net.IpAddress.parseIp4("127.0.0.1", port) catch return error.BadTestAddress;
const deadline_ns: i96 = 15_000_000_000; // 15 s cap — orders of magnitude of slack
const start = std.Io.Clock.now(.awake, io).nanoseconds;
while (true) {
if (addr.connect(io, .{ .mode = .stream })) |stream| {
stream.close(io);
return;
} else |_| {}
if (std.Io.Clock.now(.awake, io).nanoseconds - start > deadline_ns) return error.ServerNeverBound;
}
}
/// The count parsed from `zit pack`'s "<N> re-deltaed" clause, or null if the
/// clause is absent. Used to assert S5's fixture still forces a REGENERATED
/// delta (so the scenario keeps testing serve-of-a-regenerated-delta, not just a
/// full-form clone).
fn reDeltaCount(text: []const u8) ?usize {
const idx = std.mem.indexOf(u8, text, " re-deltaed") orelse return null;
var start = idx;
while (start > 0 and std.ascii.isDigit(text[start - 1])) start -= 1;
if (start == idx) return null;
return std.fmt.parseInt(usize, text[start..idx], 10) catch null;
}
/// Run `zit <tail...>` and fail the scenario (printing stderr) unless it exits 0.
/// Returns the captured result on success (caller frees), or null on failure
/// (already freed + reported).
fn zitStep(
gpa: std.mem.Allocator,
io: std.Io,
out: *std.Io.Writer,
abs_exe: []const u8,
cwd_path: []const u8,
env: *const std.process.Environ.Map,
tail: []const []const u8,
label: []const u8,
) !?std.process.RunResult {
return zitStepIn(gpa, io, out, abs_exe, cwd_path, env, tail, label, "S5", "§7.5/§3");
}
/// `zitStep` for a named scenario: the same run-or-report-and-abort shape, with
/// the scenario name and citation supplied rather than fixed to S5's.
fn zitStepIn(
gpa: std.mem.Allocator,
io: std.Io,
out: *std.Io.Writer,
abs_exe: []const u8,
cwd_path: []const u8,
env: *const std.process.Environ.Map,
tail: []const []const u8,
label: []const u8,
scenario: []const u8,
citation: []const u8,
) !?std.process.RunResult {
const res = try runZit(gpa, io, abs_exe, cwd_path, env, tail);
if (!okExit(res.term)) {
try out.print("FAIL {s} ({s}): {s} failed\n stderr: {s}\n", .{ scenario, citation, label, res.stderr });
try printMismatchFooter(out, citation);
gpa.free(res.stdout);
gpa.free(res.stderr);
return null;
}
return res;
}
fn runCloneFromRepackedScenario(
arena: std.mem.Allocator,
gpa: std.mem.Allocator,
io: std.Io,
tmp_root: []const u8,
env: *const std.process.Environ.Map,
out: *std.Io.Writer,
) !bool {
const citation = "§7.5/§3";
const abs_exe = try zitAbsPath(arena, io);
const base = try std.fs.path.join(arena, &.{ tmp_root, "S5" });
const src_dir = try std.fs.path.join(arena, &.{ base, "src" });
try std.Io.Dir.cwd().createDirPath(io, src_dir);
// A source repo with two check-ins of an evolving file: the second blob is
// stored as a delta, giving `pack` something to re-base.
{
const r = (try zitStep(gpa, io, out, abs_exe, src_dir, env, &.{"init"}, "zit init")) orelse return false;
gpa.free(r.stdout);
gpa.free(r.stderr);
}
const blob_path = try std.fs.path.join(arena, &.{ src_dir, "big.txt" });
const seed = try buildBlob(arena, false);
try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = blob_path, .data = seed });
inline for (.{
.{ &[_][]const u8{ "add", "big.txt" }, "zit add (seed)" },
.{ &[_][]const u8{ "commit", "-m", "seed", "--user", "u" }, "zit commit (seed)" },
}) |step| {
const r = (try zitStep(gpa, io, out, abs_exe, src_dir, env, step[0], step[1])) orelse return false;
gpa.free(r.stdout);
gpa.free(r.stderr);
}
const edited = try buildBlob(arena, true);
try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = blob_path, .data = edited });
inline for (.{
.{ &[_][]const u8{ "add", "big.txt" }, "zit add (grow)" },
.{ &[_][]const u8{ "commit", "-m", "grow", "--user", "u" }, "zit commit (grow)" },
}) |step| {
const r = (try zitStep(gpa, io, out, abs_exe, src_dir, env, step[0], step[1])) orelse return false;
gpa.free(r.stdout);
gpa.free(r.stderr);
}
// Re-pack: every artifact is re-deltified against our chosen base, so a
// served delta container is REGENERATED, not the one the write path stored.
{
const r = (try zitStep(gpa, io, out, abs_exe, src_dir, env, &.{"pack"}, "zit pack")) orelse return false;
defer gpa.free(r.stdout);
defer gpa.free(r.stderr);
const n = reDeltaCount(r.stdout) orelse {
try out.print("FAIL S5 ({s}): could not parse pack's re-delta count\n pack stdout: {s}\n", .{ citation, r.stdout });
return false;
};
if (n == 0) {
try out.print("FAIL S5 ({s}): pack regenerated no delta — the fixture no longer exercises a served regenerated delta container\n pack stdout: {s}\n", .{ citation, r.stdout });
return false;
}
}
// Serve the repacked store; the real fossil client clones it over the wire.
const s5_serve_port = try freshLoopbackPort(io);
const port_str = try std.fmt.allocPrint(arena, "{d}", .{s5_serve_port});
var server = try spawnZitServe(io, abs_exe, src_dir, env, port_str);
defer server.kill(io);
waitForListen(io, s5_serve_port) catch {
try out.print("FAIL S5 ({s}): zit serve never bound 127.0.0.1:{d}\n", .{ citation, s5_serve_port });
return false;
};
// A bare-root URL: a conformant `fossil clone` POSTs to "/" (sync §4). This
// is the fossil-client-from-root case e2e explicitly defers to smoke.
const url = try std.fmt.allocPrint(arena, "http://127.0.0.1:{d}/", .{s5_serve_port});
const clone_repo = try std.fs.path.join(arena, &.{ base, "verify.fossil" });
{
const res = try runFossil(gpa, io, &.{ "fossil", "clone", url, clone_repo }, base, env);
defer gpa.free(res.stdout);
defer gpa.free(res.stderr);
if (!okExit(res.term)) {
try out.print("FAIL S5 ({s}): fossil clone from the repacked zit store failed\n stderr: {s}\n", .{ citation, res.stderr });
try printMismatchFooter(out, citation);
return false;
}
}
// Check out the clone (autosync off — the server is about to be killed) so
// fossil reconstructs the file from the artifacts it received.
const co_dir = try std.fs.path.join(arena, &.{ base, "co" });
try std.Io.Dir.cwd().createDirPath(io, co_dir);
{
const res = try runFossil(gpa, io, &.{ "fossil", "open", "--nosync", clone_repo }, co_dir, env);
defer gpa.free(res.stdout);
defer gpa.free(res.stderr);
if (!okExit(res.term)) {
try out.print("FAIL S5 ({s}): fossil open of the clone failed\n stderr: {s}\n", .{ citation, res.stderr });
try printMismatchFooter(out, citation);
return false;
}
}
// THE PROOF: fossil's reconstructed file == zit's own committed bytes. A
// regenerated delta that dropped or corrupted a byte fails here.
const co_blob = try std.fs.path.join(arena, &.{ co_dir, "big.txt" });
const fossil_bytes = try std.Io.Dir.cwd().readFileAlloc(io, co_blob, gpa, .limited(1 << 20));
defer gpa.free(fossil_bytes);
if (!std.mem.eql(u8, edited, fossil_bytes)) {
try out.print("FAIL S5 ({s}): fossil clone from the repacked store did not reconstruct zit's committed bytes\n", .{citation});
try printBytesPreview(out, "zit committed", edited);
try printBytesPreview(out, "fossil clone", fossil_bytes);
try printDiffWindow(out, edited, fossil_bytes);
try printMismatchFooter(out, citation);
return false;
}
try out.print("PASS S5 ({s}): fossil clone from a repacked zit store reconstructs zit's committed bytes byte-for-byte\n", .{citation});
return true;
}
// ---------------------------------------------------------------------------
// S6 -- clone → open against a real fossil SERVER (sync §3.7/§4, P3-F3). The
// mirror of S5: there zit served and fossil cloned; here fossil serves and the
// real `zit` binary clones AND opens, so the working tree zit materializes is
// the thing under test. This is the half of the clone story digest-set equality
// cannot see — a repository can converge perfectly on the wire and still be
// unusable if nothing turns it into files on disk.
//
// The file policy at the top of this file holds: the compared bytes are OUR OWN
// (we author `s6_payload`, hand it to `fossil add`/`fossil commit`, and compare
// it to what zit materializes). fossil is the container and the transport in
// between; it never authors a byte of the oracle. A mismatch means zit's
// artifact reconstruction or its working-tree materialization dropped or
// corrupted content that fossil carried faithfully.
// ---------------------------------------------------------------------------
/// The bytes S6 round-trips. Authored here, never by fossil: multi-line with a
/// trailing newline, so a materialization that mangled line endings or dropped
/// the tail is caught.
const s6_payload =
\\line one of the fossil-served source tree
\\line two, so a truncation is visible
\\line three
\\
;
/// Run `fossil <argv...>` and fail the scenario (printing stderr) unless it
/// exits 0. Returns true on success; the caller aborts the scenario on false.
fn fossilStep(
gpa: std.mem.Allocator,
io: std.Io,
out: *std.Io.Writer,
argv: []const []const u8,
cwd_path: []const u8,
env: *const std.process.Environ.Map,
label: []const u8,
) !bool {
const res = try runFossil(gpa, io, argv, cwd_path, env);
defer gpa.free(res.stdout);
defer gpa.free(res.stderr);
if (!okExit(res.term)) {
try out.print("FAIL S6 (§3.7/§4): {s} failed\n stderr: {s}\n", .{ label, res.stderr });
return false;
}
return true;
}
fn runOpenFromFossilServerScenario(
arena: std.mem.Allocator,
gpa: std.mem.Allocator,
io: std.Io,
tmp_root: []const u8,
env: *const std.process.Environ.Map,
out: *std.Io.Writer,
) !bool {
const citation = "§3.7/§4";
const abs_exe = try zitAbsPath(arena, io);
const base = try std.fs.path.join(arena, &.{ tmp_root, "S6" });
const co_dir = try std.fs.path.join(arena, &.{ base, "co" });
try std.Io.Dir.cwd().createDirPath(io, co_dir);
const repo_path = try std.fs.path.join(arena, &.{ base, "source.fossil" });
// A real fossil repository holding one check-in of OUR bytes.
if (!try fossilStep(gpa, io, out, &.{ "fossil", "init", repo_path }, base, env, "fossil init")) return false;
if (!try fossilStep(gpa, io, out, &.{ "fossil", "open", "--nosync", repo_path }, co_dir, env, "fossil open")) return false;
const payload_path = try std.fs.path.join(arena, &.{ co_dir, "big.txt" });
try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = payload_path, .data = s6_payload });
if (!try fossilStep(gpa, io, out, &.{ "fossil", "add", "big.txt" }, co_dir, env, "fossil add")) return false;
if (!try fossilStep(
gpa,
io,
out,
&.{ "fossil", "commit", "-m", "s6 seed", "--user-override", "u", "--no-warnings" },
co_dir,
env,
"fossil commit",
)) return false;
// Serve it with the real fossil server and clone it with the real zit
// binary over the bare-root URL (sync §4: the client posts the repo URL path
// unchanged and MUST NOT append /xfer).
const s6_serve_port = try freshLoopbackPort(io);
const port_str = try std.fmt.allocPrint(arena, "{d}", .{s6_serve_port});
var server = spawnFossilServe(gpa, io, out, repo_path, base, env, port_str) catch |err| {
try out.print("FAIL S6 ({s}): could not spawn fossil server: {s}\n", .{ citation, @errorName(err) });
return false;
};
defer stopFossilServe(io, &server);
// The same retry-connect wait S5 uses: a successful TCP connect proves the
// listener is up, whichever binary owns it.
waitForListen(io, s6_serve_port) catch {
try out.print("FAIL S6 ({s}): fossil server never bound 127.0.0.1:{d}\n", .{ citation, s6_serve_port });
return false;
};
const url = try std.fmt.allocPrint(arena, "http://127.0.0.1:{d}/", .{s6_serve_port});
{
const r = (try zitStep(gpa, io, out, abs_exe, base, env, &.{ "clone", "--no-open", url, "cloned" }, "zit clone (from fossil server)")) orelse return false;
gpa.free(r.stdout);
gpa.free(r.stderr);
}
// OPEN: the clone's checkout is still unborn, so this is what turns the
// converged repository into a working tree.
const cloned_dir = try std.fs.path.join(arena, &.{ base, "cloned" });
{
const r = (try zitStep(gpa, io, out, abs_exe, cloned_dir, env, &.{"open"}, "zit open")) orelse return false;
defer gpa.free(r.stdout);
defer gpa.free(r.stderr);
if (std.mem.indexOf(u8, r.stdout, "trunk") == null) {
try out.print("FAIL S6 ({s}): zit open did not report opening trunk\n stdout: {s}\n", .{ citation, r.stdout });
return false;
}
}
// THE PROOF: the file zit materialized == the bytes we committed through
// fossil. A dropped artifact, a mis-reconstructed delta, or a materialization
// that never wrote the file fails here.
const opened_blob = try std.fs.path.join(arena, &.{ cloned_dir, "big.txt" });
const zit_bytes = std.Io.Dir.cwd().readFileAlloc(io, opened_blob, gpa, .limited(1 << 20)) catch |err| {
try out.print("FAIL S6 ({s}): zit open left no readable big.txt in the cloned tree: {s}\n", .{ citation, @errorName(err) });
try printMismatchFooter(out, citation);
return false;
};
defer gpa.free(zit_bytes);
if (!std.mem.eql(u8, s6_payload, zit_bytes)) {
try out.print("FAIL S6 ({s}): the tree zit opened does not match the bytes committed through fossil\n", .{citation});
try printBytesPreview(out, "committed", s6_payload);
try printBytesPreview(out, "zit opened", zit_bytes);
try printDiffWindow(out, s6_payload, zit_bytes);
try printMismatchFooter(out, citation);
return false;
}
try out.print("PASS S6 ({s}): zit clone+open against a real fossil server materializes the committed bytes byte-for-byte\n", .{citation});
return true;
}
// ---------------------------------------------------------------------------
// S7/S8 -- the PUSH-direction interop pair (sync §3.3/§3.7 push, §3.2/§3.7
// pull). S5/S6 exercise only pull/clone wire (fossil pulls from zit, zit pulls
// from fossil); the push-side wire -- zit as the sync CLIENT uploading to a
// server, its igot/gimme ordering and capability negotiation -- was invisible.
// Both scenarios share the same prelude: zit authors a seed check-in, a real
// fossil repo CLONES it (so client and server share a project-code, without
// which the sync layer refuses the exchange), zit grows a second check-in the
// fossil repo lacks, then zit PUSHES that growth up to a live `fossil server`.
//
// The file policy at the top holds throughout: every compared byte is OUR OWN
// -- we author the pushed file blob and (in S8) the file blob a fossil commit
// carries. fossil is the container and the transport; it never authors a byte
// of an oracle. A mismatch means zit's push wire, or its pull-and-reconstruct,
// dropped or corrupted content fossil relayed faithfully.
// ---------------------------------------------------------------------------
/// The seed the fossil repo clones and the zit repo shares -- our own bytes.
const interop_seed_payload =
\\seed line shared by the fossil clone and the zit repo
\\
;
/// The file zit authors, commits, and PUSHES to the fossil server. Multi-line
/// with a trailing newline so a truncated or line-ending-mangled push is caught.
const push_grow_payload =
\\zit authored this line and pushed it to a fossil server
\\second line so a dropped tail is visible
\\
;
/// S8: authored here, committed THROUGH a fossil checkout (fossil is the
/// container), that zit must PULL and reconstruct. The library reader confirms
/// convergence by reading it back under its own artifact name.
const fossil_ext_payload =
\\authored here, carried by a fossil commit, pulled by zit
\\second line so a truncation is visible
\\
;
/// The prelude state both push scenarios build on.
const InteropSetup = struct {
base: []const u8,
zsrc: []const u8,
dst_fossil: []const u8,
abs_exe: []const u8,
};
/// Disable Fossil's detached backoffice worker, then spawn
/// `fossil server --port <port> <repo>` in a dedicated process group. The
/// repositories served here are disposable smoke fixtures; disabling their
/// maintenance worker keeps every process inside the test-owned group.
fn spawnFossilServe(
gpa: std.mem.Allocator,
io: std.Io,
out: *std.Io.Writer,
repo_path: []const u8,
cwd_path: []const u8,
env: *const std.process.Environ.Map,
port_str: []const u8,
) !std.process.Child {
const disable = try runFossil(
gpa,
io,
&.{ "fossil", "settings", "backoffice-disable", "1", "-R", repo_path },
cwd_path,
env,
);
defer gpa.free(disable.stdout);
defer gpa.free(disable.stderr);
if (!okExit(disable.term)) {
try out.print(
"fossil settings backoffice-disable failed ({any})\n stderr: {s}\n",
.{ disable.term, disable.stderr },
);
return error.CannotDisableFossilBackoffice;
}
return std.process.spawn(io, .{
.argv = &.{ "fossil", "server", "--port", port_str, repo_path },
.cwd = .{ .path = cwd_path },
.environ_map = env,
.stdout = .ignore,
.stderr = .ignore,
.pgid = if (builtin.os.tag == .windows) null else 0,
});
}
/// Stop a Fossil server and all request handlers forked from it. `Child.kill`
/// addresses only the original PID, which can leave a POSIX handler holding the
/// listening socket after the parent is reaped.
fn stopFossilServe(io: std.Io, server: *std.process.Child) void {
if (comptime builtin.os.tag != .windows) {
if (server.id) |pid| std.posix.kill(-pid, .KILL) catch {};
}
server.kill(io);
}
/// `zit <tail...>` step, tagged with the scenario name/citation for failure
/// reporting (the S5-scoped `zitStep` bakes in "S5"). Returns the captured
/// result on success (caller frees), or null on a non-zero exit (already freed
/// and reported).
fn zitStepTagged(
gpa: std.mem.Allocator,
io: std.Io,
out: *std.Io.Writer,
abs_exe: []const u8,
cwd_path: []const u8,
env: *const std.process.Environ.Map,
tail: []const []const u8,
tag: []const u8,
citation: []const u8,
label: []const u8,
) !?std.process.RunResult {
const res = try runZit(gpa, io, abs_exe, cwd_path, env, tail);
if (!okExit(res.term)) {
try out.print("FAIL {s} ({s}): {s} failed\n stderr: {s}\n", .{ tag, citation, label, res.stderr });
try printMismatchFooter(out, citation);
gpa.free(res.stdout);
gpa.free(res.stderr);
return null;
}
return res;
}
/// `fossil <argv...>` step, tagged with the scenario name/citation (the
/// S6-scoped `fossilStep` bakes in "S6"). True on exit 0; the caller aborts the
/// scenario on false.
fn fossilStepTagged(
gpa: std.mem.Allocator,
io: std.Io,
out: *std.Io.Writer,
argv: []const []const u8,
cwd_path: []const u8,
env: *const std.process.Environ.Map,
tag: []const u8,
citation: []const u8,
label: []const u8,
) !bool {
const res = try runFossil(gpa, io, argv, cwd_path, env);
defer gpa.free(res.stdout);
defer gpa.free(res.stderr);
if (!okExit(res.term)) {
try out.print("FAIL {s} ({s}): {s} failed\n stderr: {s}\n", .{ tag, citation, label, res.stderr });
return false;
}
return true;
}
/// The shared prelude: a zit repo and a fossil repo that share a project-code,
/// with a zit-authored `grow` check-in PUSHED from zit up to the fossil server.
/// Returns the paths on success, or null on any failure (already reported under
/// `tag`).
fn establishPushedFossil(
arena: std.mem.Allocator,
gpa: std.mem.Allocator,
io: std.Io,
tmp_root: []const u8,
env: *const std.process.Environ.Map,
out: *std.Io.Writer,
tag: []const u8,
citation: []const u8,
sub: []const u8,
) !?InteropSetup {
const abs_exe = try zitAbsPath(arena, io);
const base = try std.fs.path.join(arena, &.{ tmp_root, sub });
const zsrc = try std.fs.path.join(arena, &.{ base, "zsrc" });
const dst_fossil = try std.fs.path.join(arena, &.{ base, "dst.fossil" });
try std.Io.Dir.cwd().createDirPath(io, zsrc);
// zit authors a seed check-in.
{
const r = (try zitStepTagged(gpa, io, out, abs_exe, zsrc, env, &.{"init"}, tag, citation, "zit init")) orelse return null;
gpa.free(r.stdout);
gpa.free(r.stderr);
}
const seed_path = try std.fs.path.join(arena, &.{ zsrc, "a.txt" });
try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = seed_path, .data = interop_seed_payload });
inline for (.{
.{ &[_][]const u8{ "add", "a.txt" }, "zit add (seed)" },
.{ &[_][]const u8{ "commit", "-m", "seed", "--user", "u" }, "zit commit (seed)" },
}) |step| {
const r = (try zitStepTagged(gpa, io, out, abs_exe, zsrc, env, step[0], tag, citation, step[1])) orelse return null;
gpa.free(r.stdout);
gpa.free(r.stderr);
}
// Serve the seed; the real fossil binary CLONES it, establishing the shared
// project-code the sync layer requires before it will accept a push.
const seed_port = try freshLoopbackPort(io);
const seed_port_str = try std.fmt.allocPrint(arena, "{d}", .{seed_port});
{
var server = try spawnZitServe(io, abs_exe, zsrc, env, seed_port_str);
waitForListen(io, seed_port) catch {
server.kill(io);
try out.print("FAIL {s} ({s}): zit serve never bound 127.0.0.1:{d}\n", .{ tag, citation, seed_port });
return null;
};
const seed_url = try std.fmt.allocPrint(arena, "http://127.0.0.1:{d}/", .{seed_port});
const ok_clone = try fossilStepTagged(gpa, io, out, &.{ "fossil", "clone", seed_url, dst_fossil }, base, env, tag, citation, "fossil clone of the zit seed");
server.kill(io);
if (!ok_clone) return null;
}
// The clone saved a remote (the now-dead seed server) as the fossil repo's
// autosync target; disable autosync so a fossil-side commit (S8) cannot
// silently abort trying to reach it. Grant the anonymous loopback sync user
// check-in ('i') so the push is authorized (a bare sync request logs in as
// "nobody", whose cloned caps are read-only).
if (!try fossilStepTagged(gpa, io, out, &.{ "fossil", "settings", "autosync", "off", "-R", dst_fossil }, base, env, tag, citation, "fossil settings autosync off")) return null;
if (!try fossilStepTagged(gpa, io, out, &.{ "fossil", "user", "capabilities", "nobody", "gjorzwik", "-R", dst_fossil }, base, env, tag, citation, "fossil user capabilities nobody")) return null;
// zit grows a check-in the fossil repo lacks.
const grow_path = try std.fs.path.join(arena, &.{ zsrc, "b.txt" });
try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = grow_path, .data = push_grow_payload });
inline for (.{
.{ &[_][]const u8{ "add", "b.txt" }, "zit add (grow)" },
.{ &[_][]const u8{ "commit", "-m", "grow", "--user", "u" }, "zit commit (grow)" },
}) |step| {
const r = (try zitStepTagged(gpa, io, out, abs_exe, zsrc, env, step[0], tag, citation, step[1])) orelse return null;
gpa.free(r.stdout);
gpa.free(r.stderr);
}
// fossil serves the shared repo; zit PUSHES its growth up over the wire.
const push_port = try freshLoopbackPort(io);
const push_port_str = try std.fmt.allocPrint(arena, "{d}", .{push_port});
{
var server = spawnFossilServe(gpa, io, out, dst_fossil, base, env, push_port_str) catch |err| {
try out.print("FAIL {s} ({s}): could not spawn fossil server: {s}\n", .{ tag, citation, @errorName(err) });
return null;
};
waitForListen(io, push_port) catch {
stopFossilServe(io, &server);
try out.print("FAIL {s} ({s}): fossil server never bound 127.0.0.1:{d}\n", .{ tag, citation, push_port });
return null;
};
const push_url = try std.fmt.allocPrint(arena, "http://127.0.0.1:{d}/", .{push_port});
const push_res = try zitStepTagged(gpa, io, out, abs_exe, zsrc, env, &.{ "push", push_url }, tag, citation, "zit push to the fossil server");
stopFossilServe(io, &server);
const r = push_res orelse return null;
gpa.free(r.stdout);
gpa.free(r.stderr);
}
return .{ .base = base, .zsrc = zsrc, .dst_fossil = dst_fossil, .abs_exe = abs_exe };
}
/// S7 -- zit push -> fossil server. After the shared prelude has pushed zit's
/// grown check-in up to the fossil repo, a fresh fossil checkout of that repo
/// must reconstruct the pushed file byte-for-byte: fossil hash-verifies every
/// artifact it stores and reconstructs, so a checkout that yields our exact
/// bytes proves the push delivered the artifacts intact over the wire.
fn runPushToFossilServerScenario(
arena: std.mem.Allocator,
gpa: std.mem.Allocator,
io: std.Io,
tmp_root: []const u8,
env: *const std.process.Environ.Map,
out: *std.Io.Writer,
) !bool {
const citation = "§3.3/§3.7";
const setup = (try establishPushedFossil(arena, gpa, io, tmp_root, env, out, "S7", citation, "S7")) orelse return false;
// THE PROOF: open the pushed-to repo and read back zit's grown file. fossil
// materializes it from the artifacts the push delivered; a push that dropped
// or corrupted a byte fails here.
const co = try std.fs.path.join(arena, &.{ setup.base, "verify-co" });
try std.Io.Dir.cwd().createDirPath(io, co);
if (!try fossilStepTagged(gpa, io, out, &.{ "fossil", "open", "--nosync", setup.dst_fossil }, co, env, "S7", citation, "fossil open of the pushed repo")) return false;
const co_blob = try std.fs.path.join(arena, &.{ co, "b.txt" });
const fossil_bytes = std.Io.Dir.cwd().readFileAlloc(io, co_blob, gpa, .limited(1 << 20)) catch |err| {
try out.print("FAIL S7 ({s}): the pushed repo has no readable b.txt after checkout: {s}\n", .{ citation, @errorName(err) });
try printMismatchFooter(out, citation);
return false;
};
defer gpa.free(fossil_bytes);
if (!std.mem.eql(u8, push_grow_payload, fossil_bytes)) {
try out.print("FAIL S7 ({s}): the file fossil reconstructed after zit's push does not match zit's committed bytes\n", .{citation});
try printBytesPreview(out, "zit pushed", push_grow_payload);
try printBytesPreview(out, "fossil rebuilt", fossil_bytes);
try printDiffWindow(out, push_grow_payload, fossil_bytes);
try printMismatchFooter(out, citation);
return false;
}
try out.print("PASS S7 ({s}): fossil reconstructs zit's pushed check-in byte-for-byte from the upload\n", .{citation});
return true;
}
/// S8 -- fossil commit -> zit pull. On the repo zit pushed to in the prelude, a
/// fossil checkout commits one more file (our bytes, carried inside a
/// fossil-authored check-in). zit then PULLS that extension. Convergence is
/// asserted through the library reader: `zit.Repository` opens the pulled store
/// and returns the carried blob under its own artifact name, byte-identical to
/// what we authored -- so the pull ingested and reconstructed a check-in it did
/// not itself write.
fn runPullFossilExtensionScenario(
arena: std.mem.Allocator,
gpa: std.mem.Allocator,
io: std.Io,
tmp_root: []const u8,
env: *const std.process.Environ.Map,
out: *std.Io.Writer,
) !bool {
const citation = "§3.2/§3.7";
const setup = (try establishPushedFossil(arena, gpa, io, tmp_root, env, out, "S8", citation, "S8")) orelse return false;
// A fossil checkout extends the shared repo with one more commit wrapping
// OUR bytes. autosync is already off on the repo, so the commit cannot stall
// reaching the dead seed remote; --nosync is belt-and-suspenders.
const fco = try std.fs.path.join(arena, &.{ setup.base, "fossil-co" });
try std.Io.Dir.cwd().createDirPath(io, fco);
if (!try fossilStepTagged(gpa, io, out, &.{ "fossil", "open", "--nosync", setup.dst_fossil }, fco, env, "S8", citation, "fossil open of the shared repo")) return false;
if (!try fossilStepTagged(gpa, io, out, &.{ "fossil", "update", "trunk" }, fco, env, "S8", citation, "fossil update trunk")) return false;
const ext_path = try std.fs.path.join(arena, &.{ fco, "c.txt" });
try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = ext_path, .data = fossil_ext_payload });
if (!try fossilStepTagged(gpa, io, out, &.{ "fossil", "add", "c.txt" }, fco, env, "S8", citation, "fossil add")) return false;
if (!try fossilStepTagged(
gpa,
io,
out,
&.{ "fossil", "commit", "-m", "fossil extends", "--user-override", "u", "--no-warnings", "--nosync" },
fco,
env,
"S8",
citation,
"fossil commit",
)) return false;
// fossil serves the extended repo; zit PULLS the check-in it lacks.
const pull_port = try freshLoopbackPort(io);
const pull_port_str = try std.fmt.allocPrint(arena, "{d}", .{pull_port});
{
var server = spawnFossilServe(gpa, io, out, setup.dst_fossil, setup.base, env, pull_port_str) catch |err| {
try out.print("FAIL S8 ({s}): could not spawn fossil server: {s}\n", .{ citation, @errorName(err) });
return false;
};
waitForListen(io, pull_port) catch {
stopFossilServe(io, &server);
try out.print("FAIL S8 ({s}): fossil server never bound 127.0.0.1:{d}\n", .{ citation, pull_port });
return false;
};
const pull_url = try std.fmt.allocPrint(arena, "http://127.0.0.1:{d}/", .{pull_port});
const pull_res = try zitStepTagged(gpa, io, out, setup.abs_exe, setup.zsrc, env, &.{ "pull", pull_url }, "S8", citation, "zit pull from the fossil server");
stopFossilServe(io, &server);
const r = pull_res orelse return false;
gpa.free(r.stdout);
gpa.free(r.stderr);
}
// THE PROOF, through the library reader: the pulled store holds the
// fossil-carried blob under its own artifact name (sha3-256 over our bytes),
// and `Repository.blob` re-verifies content against that name (§7.2) before
// returning it. A pull that fetched nothing, or reconstructed the blob
// wrong, fails here.
var root = std.Io.Dir.cwd().openDir(io, setup.zsrc, .{}) catch |err| {
try out.print("FAIL S8 ({s}): could not open the pulled zit checkout root: {s}\n", .{ citation, @errorName(err) });
return false;
};
defer root.close(io);
var repo: zit.Repository = undefined;
repo.open(gpa, io, root, .{ .mode = .full }) catch |err| {
try out.print("FAIL S8 ({s}): opening the pulled zit repo via the library failed: {s}\n", .{ citation, @errorName(err) });
try printMismatchFooter(out, citation);
return false;
};
defer repo.deinit(gpa);
var name_buf: [64]u8 = undefined;
const ext_name = artifact.artifactName(.sha3_256, fossil_ext_payload, &name_buf);
const got = (repo.blob(gpa, ext_name) catch |err| {
try out.print("FAIL S8 ({s}): reading the pulled blob {s} via the library failed: {s}\n", .{ citation, ext_name, @errorName(err) });
try printMismatchFooter(out, citation);
return false;
}) orelse {
try out.print("FAIL S8 ({s}): the pull did not converge — blob {s} absent from zit's store after pull\n", .{ citation, ext_name });
try printMismatchFooter(out, citation);
return false;
};
defer gpa.free(got);
if (!std.mem.eql(u8, fossil_ext_payload, got)) {
try out.print("FAIL S8 ({s}): the pulled blob does not match the bytes committed through fossil\n", .{citation});
try printBytesPreview(out, "committed", fossil_ext_payload);
try printBytesPreview(out, "zit pulled", got);
try printDiffWindow(out, fossil_ext_payload, got);
try printMismatchFooter(out, citation);
return false;
}
try out.print("PASS S8 ({s}): zit pulls a fossil-authored check-in and the library reader confirms convergence\n", .{citation});
return true;
}
// ---------------------------------------------------------------------------
// S9 -- a real fossil CLONE from `zit serve` of a DEFER-SHUNNED store: the
// shunned artifact must be ABSENT, not CORRUPT (sync §9.5 L2042-2046, §8.2
// L1504-1506).
//
// §9.5 L2043-2045 — "A subsequent read-authorized transfer path requests igot
// for every eligible holding after the Section 3.3 shun, phantom, private, and
// peer-has filters" — is a statement about what the server sends. This scenario
// is the only place a REAL peer judges the result.
//
// `zit shun --force --defer` records the row WITHOUT rewriting the generation, so
// two things are simultaneously true in the served store: the shunned bytes are
// still on disk, and a SURVIVING frame in the same generation is a delta rooted on
// the shunned one. Both halves of the send-side filter are therefore under test:
//
// * the shunned artifact must not be announced or sent — fossil must end up
// holding it as a PHANTOM, which is fossil's own word for "absent";
// * the surviving delta must arrive in FULL form. Handing out its delta wire
// form would ship a DELTASRC this server never sends; fossil would store a
// permanently dangling delta and gimme a basis it is silently skipped on
// (§3.3 L569-572) — CORRUPT, the outcome this scenario rules out.
//
// The oracle is `fossil test-integrity`, read for two things fossil says in its
// own words: the shunned name is skipped as a PHANTOM, and the blobs it did check
// report ZERO errors. "Absent" and "corrupt" are different outcomes and only one
// is acceptable. Every byte fossil sees is zit's own output; fossil only confirms.
// ---------------------------------------------------------------------------
/// The 64-hex artifact name `zit ls` prints for `want` (its lines are
/// `HASH SP PATH`), or null when the path is not listed.
/// From a `zit ls --long` listing (`<hash> <size> <perm> <class> <name>`;
/// the bare listing is names only), the hash of `want`.
fn lsHashOf(listing: []const u8, want: []const u8) ?[]const u8 {
var it = std.mem.splitScalar(u8, listing, '\n');
while (it.next()) |line| {
const sp = std.mem.indexOfScalar(u8, line, ' ') orelse continue;
const last = std.mem.lastIndexOfScalar(u8, line, ' ') orelse continue;
if (!std.mem.eql(u8, std.mem.trim(u8, line[last + 1 ..], " \t\r"), want)) continue;
return line[0..sp];
}
return null;
}
/// The count parsed from `zit stat`'s `deltas N` line, or null if absent. Guards
/// S9's premise: with no delta frame in the store, the surviving-delta half of the
/// scenario is not being exercised at all.
fn statDeltaCount(text: []const u8) ?usize {
var it = std.mem.splitScalar(u8, text, '\n');
while (it.next()) |line| {
if (!std.mem.startsWith(u8, line, "deltas ")) continue;
return std.fmt.parseInt(usize, std.mem.trim(u8, line["deltas ".len..], " \t\r"), 10) catch null;
}
return null;
}
/// Does `text` carry a `skip phantom` line naming `hash`? That is fossil's own
/// report that it holds the name but no content — ABSENT.
fn reportsPhantom(text: []const u8, hash: []const u8) bool {
var it = std.mem.splitScalar(u8, text, '\n');
while (it.next()) |line| {
if (std.mem.indexOf(u8, line, "skip phantom") == null) continue;
if (std.mem.indexOf(u8, line, hash) != null) return true;
}
return false;
}
/// Does the clone hold `hash` in fossil's OWN shun table? The stronger form of
/// absence, and the one zit now produces.
///
/// Before the `/shun` configuration group was carried, a reference to a shunned
/// artifact left fossil holding a PHANTOM -- a name with no content, which
/// fossil treats as something to FETCH. That is the takedown-defeating state:
/// the clone would gimme those bytes from the next peer that had them. Now the
/// shun row travels with the clone, so fossil records the artifact as
/// SUPPRESSED and never asks anyone for it.
///
/// Both are "absent"; only one stays absent. Asked of the database rather than
/// of `test-integrity`'s prose because a row that exists is a fact, while the
/// absence of a phantom line is only the absence of evidence.
fn holdsShunRow(gpa: std.mem.Allocator, io: std.Io, repo_dir: []const u8, hash: []const u8, env: anytype) !bool {
const sql = try std.fmt.allocPrint(gpa, "select count(*) from shun where uuid='{s}';", .{hash});
defer gpa.free(sql);
const res = try runFossil(gpa, io, &.{ "fossil", "sqlite3", "--readonly", sql }, repo_dir, env);
defer gpa.free(res.stdout);
defer gpa.free(res.stderr);
if (!okExit(res.term)) return false;
return std.mem.indexOfScalar(u8, std.mem.trim(u8, res.stdout, " \t\r\n"), '0') == null;
}
/// Does `text` report ZERO integrity errors? Anchored on the integer immediately
/// before the word `errors`, because the substring "0 errors" is also inside
/// "10 errors", "20 errors" and "100 errors" — the exact outputs that mean CORRUPT,
/// which is the outcome this scenario exists to tell apart from ABSENT. True only
/// when at least one `N errors` count was found and every one of them read zero.
fn reportsZeroErrors(text: []const u8) bool {
const word = "errors";
var found = false;
var i: usize = 0;
while (std.mem.indexOfPos(u8, text, i, word)) |p| {
i = p + word.len;
// Whole word only: `errorsomething` is not a count.
if (i < text.len and (std.ascii.isAlphanumeric(text[i]) or text[i] == '_')) continue;
var end = p;
while (end > 0 and (text[end - 1] == ' ' or text[end - 1] == '\t')) end -= 1;
var start = end;
while (start > 0 and std.ascii.isDigit(text[start - 1])) start -= 1;
if (start == end) continue; // no count in front of this occurrence
// The digits must be their own token, not the tail of an identifier.
if (start > 0 and (std.ascii.isAlphanumeric(text[start - 1]) or text[start - 1] == '_')) continue;
const n = std.fmt.parseInt(u64, text[start..end], 10) catch continue;
if (n != 0) return false;
found = true;
}
return found;
}
test "smoke S9 oracle: a nonzero integrity count is never read as clean" {
try std.testing.expect(reportsZeroErrors("low-level database integrity ok\n0 errors\n"));
try std.testing.expect(reportsZeroErrors("skip phantom abc\n0 errors, 3 phantoms\n"));
// The defect this helper exists to prevent: "0 errors" is a substring of these.
try std.testing.expect(!reportsZeroErrors("10 errors\n"));
try std.testing.expect(!reportsZeroErrors("100 errors detected\n"));
try std.testing.expect(!reportsZeroErrors("0 errors\n20 errors\n"));
// No count at all is not a pass either.
try std.testing.expect(!reportsZeroErrors("errors\n"));
try std.testing.expect(!reportsZeroErrors("integrity check complete\n"));
}
fn runCloneOfShunnedScenario(
arena: std.mem.Allocator,
gpa: std.mem.Allocator,
io: std.Io,
tmp_root: []const u8,
env: *const std.process.Environ.Map,
out: *std.Io.Writer,
) !bool {
const citation = "§9.5/§8.2";
const abs_exe = try zitAbsPath(arena, io);
const base = try std.fs.path.join(arena, &.{ tmp_root, "S9" });
const src_dir = try std.fs.path.join(arena, &.{ base, "src" });
try std.Io.Dir.cwd().createDirPath(io, src_dir);
{
const r = (try zitStepIn(gpa, io, out, abs_exe, src_dir, env, &.{"init"}, "zit init", "S9", citation)) orelse return false;
gpa.free(r.stdout);
gpa.free(r.stderr);
}
// Two files: one to be shunned, one to prove the clone still converged on
// something. The shunned file's FIRST version is the delta basis of its
// second, which is what puts the surviving-delta half under test.
const blob_path = try std.fs.path.join(arena, &.{ src_dir, "big.txt" });
const keep_path = try std.fs.path.join(arena, &.{ src_dir, "keep.txt" });
const keep_bytes = "a file with nothing against it\n";
const seed = try buildBlob(arena, false);
try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = blob_path, .data = seed });
try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = keep_path, .data = keep_bytes });
inline for (.{
.{ &[_][]const u8{ "add", "big.txt", "keep.txt" }, "zit add (seed)" },
.{ &[_][]const u8{ "commit", "-m", "seed", "--user", "u" }, "zit commit (seed)" },
}) |st| {
const r = (try zitStepIn(gpa, io, out, abs_exe, src_dir, env, st[0], st[1], "S9", citation)) orelse return false;
gpa.free(r.stdout);
gpa.free(r.stderr);
}
// The seed blob's complete artifact name, read off the tip while the seed IS
// the tip — no abbreviation to resolve and no timeline parsing.
const doomed = blk: {
const r = (try zitStepIn(gpa, io, out, abs_exe, src_dir, env, &.{ "ls", "--long" }, "zit ls --long (seed)", "S9", citation)) orelse return false;
defer gpa.free(r.stdout);
defer gpa.free(r.stderr);
const h = lsHashOf(r.stdout, "big.txt") orelse {
try out.print("FAIL S9 ({s}): 'zit ls' did not list big.txt\n stdout: {s}\n", .{ citation, r.stdout });
return false;
};
break :blk try arena.dupe(u8, h);
};
const edited = try buildBlob(arena, true);
try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = blob_path, .data = edited });
inline for (.{
.{ &[_][]const u8{ "add", "big.txt" }, "zit add (grow)" },
.{ &[_][]const u8{ "commit", "-m", "grow", "--user", "u" }, "zit commit (grow)" },
}) |st| {
const r = (try zitStepIn(gpa, io, out, abs_exe, src_dir, env, st[0], st[1], "S9", citation)) orelse return false;
gpa.free(r.stdout);
gpa.free(r.stderr);
}
// Record the shun WITHOUT reclaiming: the bytes stay, so only the send-side
// filter can keep them off the wire.
{
const r = (try zitStepIn(gpa, io, out, abs_exe, src_dir, env, &.{ "shun", "--force", "--defer", doomed }, "zit shun --force --defer", "S9", citation)) orelse return false;
gpa.free(r.stdout);
gpa.free(r.stderr);
}
// The fixture's premise, stated rather than assumed: the store still carries
// a delta frame. With none, the surviving-delta half proves nothing.
{
const r = (try zitStepIn(gpa, io, out, abs_exe, src_dir, env, &.{"stat"}, "zit stat", "S9", citation)) orelse return false;
defer gpa.free(r.stdout);
defer gpa.free(r.stderr);
const deltas = statDeltaCount(r.stdout) orelse {
try out.print("FAIL S9 ({s}): could not parse 'zit stat' delta count\n stdout: {s}\n", .{ citation, r.stdout });
return false;
};
if (deltas == 0) {
try out.print("FAIL S9 ({s}): the store holds no delta frame — the surviving-delta half of this scenario is not being exercised\n stdout: {s}\n", .{ citation, r.stdout });
return false;
}
}
const port = try freshLoopbackPort(io);
const port_str = try std.fmt.allocPrint(arena, "{d}", .{port});
var server = try spawnZitServe(io, abs_exe, src_dir, env, port_str);
defer server.kill(io);
waitForListen(io, port) catch {
try out.print("FAIL S9 ({s}): zit serve never bound 127.0.0.1:{d}\n", .{ citation, port });
return false;
};
const url = try std.fmt.allocPrint(arena, "http://127.0.0.1:{d}/", .{port});
const clone_repo = try std.fs.path.join(arena, &.{ base, "verify.fossil" });
{
const res = try runFossil(gpa, io, &.{ "fossil", "clone", url, clone_repo }, base, env);
defer gpa.free(res.stdout);
defer gpa.free(res.stderr);
// A CORRUPT outcome shows up here first: a dangling DELTASRC aborts the
// clone rather than completing it minus one artifact.
if (!okExit(res.term)) {
try out.print("FAIL S9 ({s}): fossil clone of a defer-shunned zit store failed — the shunned artifact must be ABSENT, not a broken clone\n stdout: {s}\n stderr: {s}\n", .{ citation, res.stdout, res.stderr });
try printMismatchFooter(out, citation);
return false;
}
}
const co_dir = try std.fs.path.join(arena, &.{ base, "co" });
try std.Io.Dir.cwd().createDirPath(io, co_dir);
{
const res = try runFossil(gpa, io, &.{ "fossil", "open", "--nosync", clone_repo }, co_dir, env);
defer gpa.free(res.stdout);
defer gpa.free(res.stderr);
if (!okExit(res.term)) {
try out.print("FAIL S9 ({s}): fossil open of the clone failed\n stderr: {s}\n", .{ citation, res.stderr });
try printMismatchFooter(out, citation);
return false;
}
}
// The surviving delta arrived in a form fossil could reconstruct: the tip's
// file is zit's own committed bytes, byte-for-byte.
{
const co_blob = try std.fs.path.join(arena, &.{ co_dir, "big.txt" });
const fossil_bytes = try std.Io.Dir.cwd().readFileAlloc(io, co_blob, gpa, .limited(1 << 20));
defer gpa.free(fossil_bytes);
if (!std.mem.eql(u8, edited, fossil_bytes)) {
try out.print("FAIL S9 ({s}): the clone did not reconstruct zit's committed tip bytes — a delta whose basis was shunned did not arrive in full form\n", .{citation});
try printBytesPreview(out, "zit committed", edited);
try printBytesPreview(out, "fossil clone", fossil_bytes);
try printDiffWindow(out, edited, fossil_bytes);
try printMismatchFooter(out, citation);
return false;
}
}
// THE PROOF, in fossil's own words: the shunned artifact is ABSENT, and every
// blob fossil DID receive checks clean.
//
// Absence now has a stronger shape than it used to. Before zit carried the
// `/shun` configuration group, fossil ended up holding the name as
// a PHANTOM — absent, but absent in the way that makes fossil go looking for
// it, so the very next sync with any third party would undo the takedown.
// Now the shun row arrives with the clone and fossil records the artifact as
// suppressed instead, which is why the phantom is gone. The shun row is
// REQUIRED rather than merely accepted: the phantom is the state the
// carriage exists to eliminate, so tolerating it would leave this scenario
// unable to notice the carriage silently regressing — which is the one
// regression it is now best placed to catch.
{
const res = try runFossil(gpa, io, &.{ "fossil", "test-integrity" }, co_dir, env);
defer gpa.free(res.stdout);
defer gpa.free(res.stderr);
if (!okExit(res.term)) {
try out.print("FAIL S9 ({s}): fossil test-integrity failed on the clone\n stdout: {s}\n stderr: {s}\n", .{ citation, res.stdout, res.stderr });
try printMismatchFooter(out, citation);
return false;
}
if (!(holdsShunRow(gpa, io, co_dir, doomed, env) catch false)) {
// The phantom probe survives as a DIAGNOSTIC, not as an accepted
// outcome: it separates "the carriage regressed" from "the artifact
// was served", which are different bugs with different fixes.
const phantom_there = reportsPhantom(res.stdout, doomed) or reportsPhantom(res.stderr, doomed);
if (phantom_there) {
try out.print("FAIL S9 ({s}): the clone holds the artifact as a PHANTOM rather than a shun row — the /shun configuration group is no longer being carried, so the next sync with any peer would undo the takedown\n", .{citation});
} else {
try out.print("FAIL S9 ({s}): the clone neither shuns nor phantoms the artifact — it received content zit must never have served\n stdout: {s}\n stderr: {s}\n", .{ citation, res.stdout, res.stderr });
}
try printMismatchFooter(out, citation);
return false;
}
const clean = reportsZeroErrors(res.stdout) or reportsZeroErrors(res.stderr);
if (!clean) {
try out.print("FAIL S9 ({s}): fossil test-integrity did not report zero errors — the clone is CORRUPT, not merely missing the shunned artifact\n stdout: {s}\n stderr: {s}\n", .{ citation, res.stdout, res.stderr });
try printMismatchFooter(out, citation);
return false;
}
}
try out.print("PASS S9 ({s}): a fossil clone of a defer-shunned zit store completes, reconstructs the tip byte-for-byte, and records the shunned artifact in its OWN shun table with zero integrity errors\n", .{citation});
return true;
}
// ---------------------------------------------------------------------------
// S10 -- a real fossil PUSH into `zit serve` against a shunned store (sync
// §7.7 L1472-1475, §3.3 L553-556).
//
// S9 is the SEND direction of a shun against a real peer (fossil clones a
// defer-shunned zit store and the artifact is absent). This is the direction
// the harness had never driven at all: fossil as the sync CLIENT pushing INTO
// `zit serve`. S7 pushes the other way (zit client -> fossil server), so this
// closes the last untested corner of the four (client, server) x (zit, fossil)
// combinations, and it is the one an operator's shun depends on.
//
// WHAT THIS PINS, EXACTLY. A real fossil `push --verily` against a repository
// that has shunned and reclaimed one of its artifacts completes with no
// protocol error and lands nothing. That is the interop claim, and it holds.
//
// WHAT THIS DOES NOT PIN, STATED PLAINLY. It is NOT coverage of §7.7's
// receive-side discard: the scenario passes unchanged with the server's
// admission check deleted outright (checked with a real mutant, not assumed).
// The reason is structural rather than a harness limitation. Content transfer
// in this protocol is RECEIVER-DRIVEN: §3.3 L553-556 already excludes a shunned
// name from the receiver's phantom scan, so `zit serve` never emits a gimme for
// it, and fossil -- whose push offers `igot` and then uploads what the peer asks
// for, plus its own `unsent` artifacts -- has no reason to put the card on the
// wire. Reaching the §7.7 arm needs a peer that pushes a shunned target
// UNSOLICITED, which a conformant peer against this server never does.
//
// So §7.7 is defense in depth here, and its mutation-sensitive coverage lives
// where an unsolicited card can actually be constructed: the `src/sync_test.zig`
// §7.7 unit tests and the `src/e2e.zig` raw-POST scenario, both of which go red
// under the same mutant this one survives. This scenario is kept for the interop
// property in its PASS line and for the direction it opens, not as that proof --
// do not read its green as evidence the admission check is exercised.
//
// The store's `.zit` SIZE across the push is the oracle for "makes no repository
// mutation": `serveRound` persists only when the round wrote something
// (`overlay.writtenCount() > 0`), so a round that lands nothing appends nothing.
// Size is the one measure the shun mask cannot fool -- `zit cat` refuses the
// name whether or not the bytes came back, so it is no oracle here.
// ---------------------------------------------------------------------------
/// Byte length of the file at `path` (cwd-relative or absolute).
fn fileSizeAt(io: std.Io, path: []const u8) !u64 {
var f = try std.Io.Dir.cwd().openFile(io, path, .{});
defer f.close(io);
const st = try f.stat(io);
return st.size;
}
fn runPushOfShunnedScenario(
arena: std.mem.Allocator,
gpa: std.mem.Allocator,
io: std.Io,
tmp_root: []const u8,
env: *const std.process.Environ.Map,
out: *std.Io.Writer,
) !bool {
const citation = "§7.7";
const abs_exe = try zitAbsPath(arena, io);
const base = try std.fs.path.join(arena, &.{ tmp_root, "S10" });
const src_dir = try std.fs.path.join(arena, &.{ base, "src" });
try std.Io.Dir.cwd().createDirPath(io, src_dir);
{
const r = (try zitStepIn(gpa, io, out, abs_exe, src_dir, env, &.{"init"}, "zit init", "S10", citation)) orelse return false;
gpa.free(r.stdout);
gpa.free(r.stderr);
}
// Two files: one the operator will remove, one that must be unaffected.
const doomed_bytes = "content an operator ordered removed, which fossil will offer back\n";
const keep_bytes = "a file with nothing against it\n";
try std.Io.Dir.cwd().writeFile(io, .{
.sub_path = try std.fs.path.join(arena, &.{ src_dir, "doomed.txt" }),
.data = doomed_bytes,
});
try std.Io.Dir.cwd().writeFile(io, .{
.sub_path = try std.fs.path.join(arena, &.{ src_dir, "keep.txt" }),
.data = keep_bytes,
});
inline for (.{
.{ &[_][]const u8{ "add", "doomed.txt", "keep.txt" }, "zit add" },
.{ &[_][]const u8{ "commit", "-m", "seed", "--user", "u" }, "zit commit" },
}) |st| {
const r = (try zitStepIn(gpa, io, out, abs_exe, src_dir, env, st[0], st[1], "S10", citation)) orelse return false;
gpa.free(r.stdout);
gpa.free(r.stderr);
}
const doomed = blk: {
const r = (try zitStepIn(gpa, io, out, abs_exe, src_dir, env, &.{ "ls", "--long" }, "zit ls --long", "S10", citation)) orelse return false;
defer gpa.free(r.stdout);
defer gpa.free(r.stderr);
const h = lsHashOf(r.stdout, "doomed.txt") orelse {
try out.print("FAIL S10 ({s}): 'zit ls' did not list doomed.txt\n stdout: {s}\n", .{ citation, r.stdout });
return false;
};
break :blk try arena.dupe(u8, h);
};
// A real fossil repo clones the whole thing, so it holds the doomed artifact
// and shares zit's project code (without which the exchange is refused).
const clone_repo = try std.fs.path.join(arena, &.{ base, "peer.fossil" });
const co_dir = try std.fs.path.join(arena, &.{ base, "co" });
try std.Io.Dir.cwd().createDirPath(io, co_dir);
{
const port = try freshLoopbackPort(io);
const port_str = try std.fmt.allocPrint(arena, "{d}", .{port});
var server = try spawnZitServe(io, abs_exe, src_dir, env, port_str);
defer server.kill(io);
waitForListen(io, port) catch {
try out.print("FAIL S10 ({s}): zit serve never bound 127.0.0.1:{d}\n", .{ citation, port });
return false;
};
const url = try std.fmt.allocPrint(arena, "http://127.0.0.1:{d}/", .{port});
const res = try runFossil(gpa, io, &.{ "fossil", "clone", url, clone_repo }, base, env);
defer gpa.free(res.stdout);
defer gpa.free(res.stderr);
if (!okExit(res.term)) {
try out.print("FAIL S10 ({s}): fossil clone of the zit store failed\n stdout: {s}\n stderr: {s}\n", .{ citation, res.stdout, res.stderr });
try printMismatchFooter(out, citation);
return false;
}
}
{
const res = try runFossil(gpa, io, &.{ "fossil", "open", "--nosync", clone_repo }, co_dir, env);
defer gpa.free(res.stdout);
defer gpa.free(res.stderr);
if (!okExit(res.term)) {
try out.print("FAIL S10 ({s}): fossil open of the clone failed\n stderr: {s}\n", .{ citation, res.stderr });
try printMismatchFooter(out, citation);
return false;
}
}
// The operator removes it. `--force` reclaims, so the bytes are gone and the
// ONLY thing that can keep them out on the way back is §7.7 admission.
{
const r = (try zitStepIn(gpa, io, out, abs_exe, src_dir, env, &.{ "shun", "--force", doomed }, "zit shun --force", "S10", citation)) orelse return false;
gpa.free(r.stdout);
gpa.free(r.stderr);
}
const store_path = try std.fs.path.join(arena, &.{ src_dir, ".zit" });
const size_before = try fileSizeAt(io, store_path);
// fossil pushes back into `zit serve`. fossil uploads what its own inventory
// says the peer lacks, so the shunned artifact goes on the wire unsolicited.
{
const port = try freshLoopbackPort(io);
const port_str = try std.fmt.allocPrint(arena, "{d}", .{port});
var server = try spawnZitServe(io, abs_exe, src_dir, env, port_str);
defer server.kill(io);
waitForListen(io, port) catch {
try out.print("FAIL S10 ({s}): zit serve never bound 127.0.0.1:{d}\n", .{ citation, port });
return false;
};
const url = try std.fmt.allocPrint(arena, "http://127.0.0.1:{d}/", .{port});
// `--verily` is fossil's own "exchange extra information ... to ensure no
// content is overlooked" mode: it re-offers what its sent-flags would
// otherwise suppress, which is what puts a file card for the shunned
// artifact back on the wire. A plain `fossil push` after a clone has
// nothing marked unsent and sends no content at all.
const res = try runFossil(gpa, io, &.{ "fossil", "push", "--verily", url }, co_dir, env);
defer gpa.free(res.stdout);
defer gpa.free(res.stderr);
// "it emits no error" — fossil reports a protocol error card as a failed
// push, so a clean exit is the interoperable reading of that clause.
if (!okExit(res.term)) {
try out.print("FAIL S10 ({s}): fossil push into zit serve failed — a discarded shunned target must raise NO protocol error\n stdout: {s}\n stderr: {s}\n", .{ citation, res.stdout, res.stderr });
try printMismatchFooter(out, citation);
return false;
}
server.kill(io);
}
// THE PROOF: "makes no repository mutation". The store did not grow, so the
// artifact the operator removed did not come back through the serve door.
const size_after = try fileSizeAt(io, store_path);
if (size_after != size_before) {
try out.print("FAIL S10 ({s}): the served store GREW across a fossil push of shunned content ({d} -> {d} bytes) — the receiver re-admitted what the operator removed\n", .{ citation, size_before, size_after });
try printMismatchFooter(out, citation);
return false;
}
try out.print("PASS S10 ({s}): a real fossil push of a shunned artifact into zit serve completes with no protocol error and lands nothing\n", .{citation});
return true;
}
// S11 -- a real fossil push REFUSED by `zit serve`'s content policy.
//
// S10 drives the same direction (fossil as sync client into `zit serve`) for a
// shunned artifact, and its own comment is careful that the scenario survives
// deleting the server's admission check, because a conformant fossil never puts
// that card on the wire unsolicited. This one does NOT have that weakness: the
// violating file is ordinary new content fossil is pushing because it is new,
// so the server genuinely sees it and genuinely decides.
//
// WHAT THIS PINS. A stock fossil client pushing a check-in that violates the
// served repository's declared content policy is refused: the server stores
// nothing, and fossil surfaces the server's own refusal TEXT to the operator.
// The error card is the §11.1 r2 free-text carrier the auth and sparse-origin
// refusals already use; this proves a real fossil reads it rather than choking.
//
// WHAT THIS DOES NOT PIN, STATED PLAINLY. fossil exits ZERO after printing the
// refusal. Verified by hand against 2.28, and it is why the check below asserts
// on the store and on fossil's OUTPUT rather than on its exit status. So the
// refusal is advisory in exit-code terms to a fossil client: a script running
// `fossil push && deploy` proceeds. zit's own client exits 1. That is fossil's
// choice about a protocol-legal error card, not something this server can fix,
// and an operator relying on a policy gate needs to know it.
fn runPushPolicyRefusalScenario(
arena: std.mem.Allocator,
gpa: std.mem.Allocator,
io: std.Io,
tmp_root: []const u8,
env: *const std.process.Environ.Map,
out: *std.Io.Writer,
) !bool {
const citation = "push content policy";
const abs_exe = try zitAbsPath(arena, io);
const base = try std.fs.path.join(arena, &.{ tmp_root, "S11" });
const src_dir = try std.fs.path.join(arena, &.{ base, "src" });
try std.Io.Dir.cwd().createDirPath(io, src_dir);
{
const r = (try zitStepIn(gpa, io, out, abs_exe, src_dir, env, &.{"init"}, "zit init", "S11", citation)) orelse return false;
gpa.free(r.stdout);
gpa.free(r.stderr);
}
try std.Io.Dir.cwd().writeFile(io, .{
.sub_path = try std.fs.path.join(arena, &.{ src_dir, "seed.txt" }),
.data = "a file with nothing against it\n",
});
inline for (.{
.{ &[_][]const u8{ "add", "seed.txt" }, "zit add" },
.{ &[_][]const u8{ "commit", "-m", "seed", "--user", "u" }, "zit commit" },
// The server declares its policy: *.ok are the binaries it expects, so
// anything else carrying a NUL is refused. Undeclared would accept
// everything, which is the state of every server that never asked.
.{ &[_][]const u8{ "settings", "binary-glob", "*.ok" }, "zit settings binary-glob" },
}) |st| {
const r = (try zitStepIn(gpa, io, out, abs_exe, src_dir, env, st[0], st[1], "S11", citation)) orelse return false;
gpa.free(r.stdout);
gpa.free(r.stderr);
}
const clone_repo = try std.fs.path.join(arena, &.{ base, "peer.fossil" });
const co_dir = try std.fs.path.join(arena, &.{ base, "co" });
try std.Io.Dir.cwd().createDirPath(io, co_dir);
{
const port = try freshLoopbackPort(io);
const port_str = try std.fmt.allocPrint(arena, "{d}", .{port});
var server = try spawnZitServe(io, abs_exe, src_dir, env, port_str);
defer server.kill(io);
waitForListen(io, port) catch {
try out.print("FAIL S11 ({s}): zit serve never bound 127.0.0.1:{d}\n", .{ citation, port });
return false;
};
const url = try std.fmt.allocPrint(arena, "http://127.0.0.1:{d}/", .{port});
const res = try runFossil(gpa, io, &.{ "fossil", "clone", url, clone_repo }, base, env);
defer gpa.free(res.stdout);
defer gpa.free(res.stderr);
if (!okExit(res.term)) {
try out.print("FAIL S11 ({s}): fossil clone of the zit store failed\n stderr: {s}\n", .{ citation, res.stderr });
try printMismatchFooter(out, citation);
return false;
}
}
{
const res = try runFossil(gpa, io, &.{ "fossil", "open", "--nosync", clone_repo }, co_dir, env);
defer gpa.free(res.stdout);
defer gpa.free(res.stderr);
if (!okExit(res.term)) {
try out.print("FAIL S11 ({s}): fossil open of the clone failed\n stderr: {s}\n", .{ citation, res.stderr });
try printMismatchFooter(out, citation);
return false;
}
}
// A binary file the policy does not cover, authored and committed by fossil.
try std.Io.Dir.cwd().writeFile(io, .{
.sub_path = try std.fs.path.join(arena, &.{ co_dir, "bad.bin" }),
.data = "a\x00b\n",
});
inline for (.{
.{ &[_][]const u8{ "fossil", "add", "bad.bin" }, "fossil add" },
// --nosync: the served repository is deliberately not listening between
// the clone and the push, and fossil aborts a commit whose autosync
// cannot reach the peer. The push below is the exchange under test.
.{ &[_][]const u8{ "fossil", "commit", "-m", "binary from fossil", "--no-warnings", "--nosync" }, "fossil commit" },
}) |st| {
const res = try runFossil(gpa, io, st[0], co_dir, env);
defer gpa.free(res.stdout);
defer gpa.free(res.stderr);
if (!okExit(res.term)) {
try out.print("FAIL S11 ({s}): {s} failed\n stderr: {s}\n", .{ citation, st[1], res.stderr });
try printMismatchFooter(out, citation);
return false;
}
}
const store_path = try std.fs.path.join(arena, &.{ src_dir, ".zit" });
const size_before = try fileSizeAt(io, store_path);
{
const port = try freshLoopbackPort(io);
const port_str = try std.fmt.allocPrint(arena, "{d}", .{port});
var server = try spawnZitServe(io, abs_exe, src_dir, env, port_str);
defer server.kill(io);
waitForListen(io, port) catch {
try out.print("FAIL S11 ({s}): zit serve never bound 127.0.0.1:{d}\n", .{ citation, port });
return false;
};
const url = try std.fmt.allocPrint(arena, "http://127.0.0.1:{d}/", .{port});
const res = try runFossil(gpa, io, &.{ "fossil", "push", url }, co_dir, env);
defer gpa.free(res.stdout);
defer gpa.free(res.stderr);
// The interop claim: fossil READS the error card and shows its text.
// Asserted on output, not on exit status -- see the header note.
const combined_has = std.mem.indexOf(u8, res.stdout, "bad.bin") != null or
std.mem.indexOf(u8, res.stderr, "bad.bin") != null;
const named_rule = std.mem.indexOf(u8, res.stdout, "binary-glob") != null or
std.mem.indexOf(u8, res.stderr, "binary-glob") != null;
if (!combined_has or !named_rule) {
try out.print("FAIL S11 ({s}): fossil did not surface the server's refusal naming the file and the rule\n stdout: {s}\n stderr: {s}\n", .{ citation, res.stdout, res.stderr });
try printMismatchFooter(out, citation);
return false;
}
server.kill(io);
}
// THE PROOF: the refused round stored nothing. A policy that printed a
// refusal and kept the bytes would be worse than no policy.
const size_after = try fileSizeAt(io, store_path);
if (size_after != size_before) {
try out.print("FAIL S11 ({s}): the served store GREW across a REFUSED push ({d} -> {d} bytes)\n", .{ citation, size_before, size_after });
try printMismatchFooter(out, citation);
return false;
}
try out.print("PASS S11 ({s}): a stock fossil push violating the server's content policy is refused, the store is unchanged, and fossil surfaces the refusal naming the file and the rule\n", .{citation});
return true;
}
/// S16 — a stock fossil push carrying several check-ins in one round lands.
///
/// fossil sends a batch of commits as one round, each manifest a delta
/// against the previous one, so a delta's basis is routinely a card of the
/// same round rather than something the server already holds. Until the scan
/// learned to look inside the round, every multi-commit push from a stock
/// fossil was refused, and the workaround nobody would guess was to push one
/// check-in at a time. Single-commit rounds never exercised this, which is
/// why every other scenario here passed while a two-day narrowing was under
/// way on a forge.
fn runMultiCheckinPushScenario(
arena: std.mem.Allocator,
gpa: std.mem.Allocator,
io: std.Io,
tmp_root: []const u8,
env: *const std.process.Environ.Map,
out: *std.Io.Writer,
) !bool {
const citation = "multi-check-in push";
const abs_exe = try zitAbsPath(arena, io);
const base = try std.fs.path.join(arena, &.{ tmp_root, "S16" });
const src_dir = try std.fs.path.join(arena, &.{ base, "src" });
try std.Io.Dir.cwd().createDirPath(io, src_dir);
{
const r = (try zitStepIn(gpa, io, out, abs_exe, src_dir, env, &.{"init"}, "zit init", "S16", citation)) orelse return false;
gpa.free(r.stdout);
gpa.free(r.stderr);
}
try std.Io.Dir.cwd().writeFile(io, .{
.sub_path = try std.fs.path.join(arena, &.{ src_dir, "seed.txt" }),
.data = "seed\n",
});
inline for (.{
.{ &[_][]const u8{ "add", "seed.txt" }, "zit add" },
.{ &[_][]const u8{ "commit", "-m", "seed", "--user", "u" }, "zit commit" },
}) |st| {
const r = (try zitStepIn(gpa, io, out, abs_exe, src_dir, env, st[0], st[1], "S16", citation)) orelse return false;
gpa.free(r.stdout);
gpa.free(r.stderr);
}
const clone_repo = try std.fs.path.join(arena, &.{ base, "peer.fossil" });
const co_dir = try std.fs.path.join(arena, &.{ base, "co" });
try std.Io.Dir.cwd().createDirPath(io, co_dir);
{
const port = try freshLoopbackPort(io);
const port_str = try std.fmt.allocPrint(arena, "{d}", .{port});
var server = try spawnZitServe(io, abs_exe, src_dir, env, port_str);
defer server.kill(io);
waitForListen(io, port) catch {
try out.print("FAIL S16 ({s}): zit serve never bound 127.0.0.1:{d}\n", .{ citation, port });
return false;
};
const url = try std.fmt.allocPrint(arena, "http://127.0.0.1:{d}/", .{port});
const res = try runFossil(gpa, io, &.{ "fossil", "clone", url, clone_repo }, base, env);
defer gpa.free(res.stdout);
defer gpa.free(res.stderr);
if (!okExit(res.term)) {
try out.print("FAIL S16 ({s}): fossil clone of the zit store failed\n stderr: {s}\n", .{ citation, res.stderr });
try printMismatchFooter(out, citation);
return false;
}
}
{
const res = try runFossil(gpa, io, &.{ "fossil", "open", "--nosync", clone_repo }, co_dir, env);
defer gpa.free(res.stdout);
defer gpa.free(res.stderr);
if (!okExit(res.term)) {
try out.print("FAIL S16 ({s}): fossil open of the clone failed\n stderr: {s}\n", .{ citation, res.stderr });
try printMismatchFooter(out, citation);
return false;
}
}
// Three commits, offline, to the same file and to a new one: manifests
// chained as deltas, and file deltas chained on file deltas. --nosync
// because nothing is listening between the clone and the push.
const seed_path = try std.fs.path.join(arena, &.{ co_dir, "seed.txt" });
const other_path = try std.fs.path.join(arena, &.{ co_dir, "other.txt" });
try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = seed_path, .data = "seed\nfirst\n" });
try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = other_path, .data = "other\n" });
inline for (.{
.{ &[_][]const u8{ "fossil", "commit", "-m", "batch one", "--nosync" }, "fossil commit one" },
.{ &[_][]const u8{ "fossil", "add", "other.txt" }, "fossil add" },
.{ &[_][]const u8{ "fossil", "commit", "-m", "batch two", "--nosync" }, "fossil commit two" },
}) |st| {
const res = try runFossil(gpa, io, st[0], co_dir, env);
defer gpa.free(res.stdout);
defer gpa.free(res.stderr);
if (!okExit(res.term)) {
try out.print("FAIL S16 ({s}): {s} failed\n stderr: {s}\n", .{ citation, st[1], res.stderr });
try printMismatchFooter(out, citation);
return false;
}
}
try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = seed_path, .data = "seed\nfirst\nsecond\n" });
{
const res = try runFossil(gpa, io, &.{ "fossil", "commit", "-m", "batch three", "--nosync" }, co_dir, env);
defer gpa.free(res.stdout);
defer gpa.free(res.stderr);
if (!okExit(res.term)) {
try out.print("FAIL S16 ({s}): fossil commit three failed\n stderr: {s}\n", .{ citation, res.stderr });
try printMismatchFooter(out, citation);
return false;
}
}
{
const port = try freshLoopbackPort(io);
const port_str = try std.fmt.allocPrint(arena, "{d}", .{port});
var server = try spawnZitServe(io, abs_exe, src_dir, env, port_str);
defer server.kill(io);
waitForListen(io, port) catch {
try out.print("FAIL S16 ({s}): zit serve never bound 127.0.0.1:{d}\n", .{ citation, port });
return false;
};
const url = try std.fmt.allocPrint(arena, "http://127.0.0.1:{d}/", .{port});
const res = try runFossil(gpa, io, &.{ "fossil", "push", url }, co_dir, env);
defer gpa.free(res.stdout);
defer gpa.free(res.stderr);
const refused = std.mem.indexOf(u8, res.stdout, "Error:") != null or std.mem.indexOf(u8, res.stderr, "Error:") != null;
if (!okExit(res.term) or refused) {
try out.print("FAIL S16 ({s}): fossil push of three check-ins in one round was refused\n stdout: {s}\n stderr: {s}\n", .{ citation, res.stdout, res.stderr });
try printMismatchFooter(out, citation);
return false;
}
server.kill(io);
}
// THE PROOF: all three check-ins are in the served store, by comment.
{
const r = (try zitStepIn(gpa, io, out, abs_exe, src_dir, env, &.{ "timeline", "-n", "10" }, "zit timeline", "S16", citation)) orelse return false;
defer gpa.free(r.stdout);
defer gpa.free(r.stderr);
inline for (.{ "batch one", "batch two", "batch three" }) |msg| {
if (std.mem.indexOf(u8, r.stdout, msg) == null) {
try out.print("FAIL S16 ({s}): the served store lacks \"{s}\" after the push\n timeline: {s}\n", .{ citation, msg, r.stdout });
try printMismatchFooter(out, citation);
return false;
}
}
}
try out.print("PASS S16 ({s}): a stock fossil push of three check-ins in one round lands every one of them\n", .{citation});
return true;
}
/// S17 — a private repository is read with a login, and only with one.
///
/// fossil gates reads by capability: strip `o` from `nobody` and `anonymous`
/// and an anonymous clone is refused, while a named reader is served. zit's
/// clone and pull sent no login card at all, so a private repository on a
/// gated forge was unclonable by the account that owned it, and the server's
/// deliberate 404 (which reveals nothing) pointed at nothing. This drives the
/// three states in order: refused anonymously, cloned with --user, and pulled
/// again with no arguments on the login the clone stored.
fn runPrivateReadScenario(
arena: std.mem.Allocator,
gpa: std.mem.Allocator,
io: std.Io,
tmp_root: []const u8,
env: *const std.process.Environ.Map,
out: *std.Io.Writer,
) !bool {
const citation = "private read";
const abs_exe = try zitAbsPath(arena, io);
const base = try std.fs.path.join(arena, &.{ tmp_root, "S17" });
try std.Io.Dir.cwd().createDirPath(io, base);
const peer_repo = try std.fs.path.join(arena, &.{ base, "peer.fossil" });
const Step = struct { argv: []const []const u8, what: []const u8 };
for ([_]Step{
.{ .argv = &.{ "fossil", "init", peer_repo }, .what = "fossil init" },
.{ .argv = &.{ "fossil", "user", "new", "smokereader", "Smoke Reader", "sekret", "-R", peer_repo }, .what = "fossil user new" },
.{ .argv = &.{ "fossil", "user", "capabilities", "smokereader", "o", "-R", peer_repo }, .what = "fossil user capabilities reader" },
// The gate: neither special user may read. fossil's defaults give both
// read access, which is why every other scenario clones anonymously.
.{ .argv = &.{ "fossil", "user", "capabilities", "nobody", "", "-R", peer_repo }, .what = "fossil user capabilities nobody" },
.{ .argv = &.{ "fossil", "user", "capabilities", "anonymous", "", "-R", peer_repo }, .what = "fossil user capabilities anonymous" },
}) |step| {
const res = try runFossil(gpa, io, step.argv, base, env);
defer gpa.free(res.stdout);
defer gpa.free(res.stderr);
if (!okExit(res.term)) {
try out.print("FAIL S17 ({s}): {s} failed\n stderr: {s}\n", .{ citation, step.what, res.stderr });
return false;
}
}
const port = try freshLoopbackPort(io);
const port_str = try std.fmt.allocPrint(arena, "{d}", .{port});
var server = spawnFossilServe(gpa, io, out, peer_repo, base, env, port_str) catch |err| {
try out.print("FAIL S17 ({s}): could not spawn fossil server: {s}\n", .{ citation, @errorName(err) });
return false;
};
defer stopFossilServe(io, &server);
waitForListen(io, port) catch {
try out.print("FAIL S17 ({s}): fossil server never bound 127.0.0.1:{d}\n", .{ citation, port });
return false;
};
const url = try std.fmt.allocPrint(arena, "http://127.0.0.1:{d}/", .{port});
// ANONYMOUS: refused. The premise of the scenario; a clone that succeeds
// here proves nothing about logins.
const anon_dir = try std.fs.path.join(arena, &.{ base, "anon" });
{
const res = try std.process.run(gpa, io, .{ .argv = &.{ abs_exe, "clone", url, anon_dir }, .cwd = .{ .path = base }, .environ_map = env });
defer gpa.free(res.stdout);
defer gpa.free(res.stderr);
if (okExit(res.term)) {
try out.print("FAIL S17 ({s}): an ANONYMOUS clone of a read-gated repository succeeded; the gate is not in force\n", .{citation});
return false;
}
}
// NAMED: the clone signs from round 1 with the secret derived from the
// project code round 0 taught it, and stores the accepted login.
const mirror = try std.fs.path.join(arena, &.{ base, "mirror" });
{
const res = try std.process.run(gpa, io, .{ .argv = &.{ abs_exe, "clone", url, mirror, "--user", "smokereader", "--password", "sekret" }, .cwd = .{ .path = base }, .environ_map = env });
defer gpa.free(res.stdout);
defer gpa.free(res.stderr);
if (!okExit(res.term)) {
try out.print("FAIL S17 ({s}): a clone with --user of a read-gated repository was refused\n stdout: {s}\n stderr: {s}\n", .{ citation, res.stdout, res.stderr });
try printMismatchFooter(out, citation);
return false;
}
}
// STORED: a bare pull from the clone authenticates on the login the clone
// kept, with no user and no password given.
{
const res = try std.process.run(gpa, io, .{ .argv = &.{ abs_exe, "pull" }, .cwd = .{ .path = mirror }, .environ_map = env });
defer gpa.free(res.stdout);
defer gpa.free(res.stderr);
if (!okExit(res.term)) {
try out.print("FAIL S17 ({s}): a bare pull from the credentialed clone was refused; the stored login was not reused\n stdout: {s}\n stderr: {s}\n", .{ citation, res.stdout, res.stderr });
try printMismatchFooter(out, citation);
return false;
}
}
try out.print("PASS S17 ({s}): a read-gated fossil repository refuses an anonymous zit clone, serves one with --user, and a bare pull from that clone reuses the stored login\n", .{citation});
return true;
}
/// The first line of a tool's output, for a one-line mismatch report.
fn firstLine(text: []const u8) []const u8 {
const trimmed = std.mem.trim(u8, text, " \t\r\n");
const end = std.mem.indexOfScalar(u8, trimmed, '\n') orelse trimmed.len;
return trimmed[0..end];
}
/// S13 — fossil's commit-time ENCODING warning, and the
/// `encoding-glob` setting that exists to silence it.
///
/// This is the oracle for zit's own encoding gate. zit refuses rather than
/// prompts, so the claim being confirmed is not "zit prints what fossil
/// prints" but the premise underneath it: a UTF-16 file really does stop a
/// stock fossil commit, and declaring the glob really is what lets it through.
/// `--no-prompt` pins the answer fossil would otherwise read from a terminal
/// ('N', checkin.c), which is what makes the exit status deterministic here.
fn runEncodingWarningScenario(
arena: std.mem.Allocator,
gpa: std.mem.Allocator,
io: std.Io,
tmp_root: []const u8,
env: *const std.process.Environ.Map,
out: *std.Io.Writer,
) !bool {
const citation = "commit encoding warning";
const base = try std.fs.path.join(arena, &.{ tmp_root, "S13" });
const co_dir = try std.fs.path.join(arena, &.{ base, "co" });
try std.Io.Dir.cwd().createDirPath(io, co_dir);
const repo = try std.fs.path.join(arena, &.{ base, "repo.fossil" });
// UTF-16 LE, "hi": a byte-order mark and two 16-bit units. Not binary --
// the zero bytes are unit halves, which is exactly the distinction
// fossil's two scanners draw and zit's `classify` now draws with them.
const utf16_hi = "\xFF\xFEh\x00i\x00";
const Step = struct { argv: []const []const u8, cwd: []const u8, what: []const u8 };
for ([_]Step{
.{ .argv = &.{ "fossil", "init", repo }, .cwd = base, .what = "fossil init" },
.{ .argv = &.{ "fossil", "open", "--nosync", repo }, .cwd = co_dir, .what = "fossil open" },
.{ .argv = &.{ "fossil", "add", "wide.txt" }, .cwd = co_dir, .what = "fossil add" },
}, 0..) |step, i| {
// The file has to exist before `add` and cannot exist before `open`.
if (i == 2) try std.Io.Dir.cwd().writeFile(io, .{
.sub_path = try std.fs.path.join(arena, &.{ co_dir, "wide.txt" }),
.data = utf16_hi,
});
const res = try runFossil(gpa, io, step.argv, step.cwd, env);
defer gpa.free(res.stdout);
defer gpa.free(res.stderr);
if (!okExit(res.term)) {
try out.print("FAIL S13 ({s}): {s} failed\n stderr: {s}\n", .{ citation, step.what, res.stderr });
try printMismatchFooter(out, citation);
return false;
}
}
// Undeclared: the commit is abandoned, and the message names the class.
{
const res = try runFossil(gpa, io, &.{ "fossil", "commit", "-m", "wide", "--no-prompt", "--nosync" }, co_dir, env);
defer gpa.free(res.stdout);
defer gpa.free(res.stderr);
const said_unicode = std.mem.indexOf(u8, res.stderr, "Unicode") != null or
std.mem.indexOf(u8, res.stdout, "Unicode") != null;
if (okExit(res.term) or !said_unicode) {
try out.print(
"FAIL S13 ({s}): fossil committed a UTF-16 file with no encoding-glob, or did not name the class\n stdout: {s}\n stderr: {s}\n",
.{ citation, firstLine(res.stdout), firstLine(res.stderr) },
);
try printMismatchFooter(out, citation);
return false;
}
}
// Declared: `*` exempts every path, and the same commit goes through.
{
const res = try runFossil(gpa, io, &.{ "fossil", "settings", "encoding-glob", "*" }, co_dir, env);
defer gpa.free(res.stdout);
defer gpa.free(res.stderr);
if (!okExit(res.term)) {
try out.print("FAIL S13 ({s}): fossil settings encoding-glob failed\n stderr: {s}\n", .{ citation, res.stderr });
try printMismatchFooter(out, citation);
return false;
}
}
{
const res = try runFossil(gpa, io, &.{ "fossil", "commit", "-m", "wide", "--no-prompt", "--nosync" }, co_dir, env);
defer gpa.free(res.stdout);
defer gpa.free(res.stderr);
if (!okExit(res.term)) {
try out.print(
"FAIL S13 ({s}): fossil still refused the UTF-16 file with encoding-glob set to *\n stdout: {s}\n stderr: {s}\n",
.{ citation, firstLine(res.stdout), firstLine(res.stderr) },
);
try printMismatchFooter(out, citation);
return false;
}
}
if (!try binaryOutranksEncoding(arena, gpa, io, co_dir, env, out)) return false;
try out.print("PASS S13 ({s}): a UTF-16 file abandons a stock fossil commit naming the Unicode class, encoding-glob lets the same commit through, and a file with a NUL stays in the binary class regardless\n", .{citation});
return true;
}
/// The second half of S13: fossil's binary branch is TERMINAL. `encoding-glob`
/// is never consulted for a blob with a NUL, even set to `*`, and it is
/// `binary-glob` that exempts it. This is the oracle for the ordering in zit's
/// `ContentPolicy.fault` -- without it, declaring encoding-glob alone would
/// start refusing every PNG in a tree under a glob that cannot exempt them.
///
/// Runs in the checkout S13 already opened, with `encoding-glob` already `*`.
fn binaryOutranksEncoding(
arena: std.mem.Allocator,
gpa: std.mem.Allocator,
io: std.Io,
co_dir: []const u8,
env: *const std.process.Environ.Map,
out: *std.Io.Writer,
) !bool {
const citation = "commit encoding warning";
// A PNG header: one NUL, and bytes that are not valid UTF-8 either -- so
// both classes would claim it if binary did not win outright.
try std.Io.Dir.cwd().writeFile(io, .{
.sub_path = try std.fs.path.join(arena, &.{ co_dir, "pic.png" }),
.data = "\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\xFF\xFE\x01",
});
{
const res = try runFossil(gpa, io, &.{ "fossil", "add", "pic.png" }, co_dir, env);
defer gpa.free(res.stdout);
defer gpa.free(res.stderr);
if (!okExit(res.term)) {
try out.print("FAIL S13 ({s}): fossil add pic.png failed\n stderr: {s}\n", .{ citation, res.stderr });
try printMismatchFooter(out, citation);
return false;
}
}
{
const res = try runFossil(gpa, io, &.{ "fossil", "commit", "-m", "pic", "--no-prompt", "--nosync" }, co_dir, env);
defer gpa.free(res.stdout);
defer gpa.free(res.stderr);
const said_binary = std.mem.indexOf(u8, res.stderr, "binary") != null or
std.mem.indexOf(u8, res.stdout, "binary") != null;
if (okExit(res.term) or !said_binary) {
try out.print(
"FAIL S13 ({s}): encoding-glob '*' exempted a file with a NUL, or fossil did not name the binary class\n stdout: {s}\n stderr: {s}\n",
.{ citation, firstLine(res.stdout), firstLine(res.stderr) },
);
try printMismatchFooter(out, citation);
return false;
}
}
// binary-glob is the glob that answers for it.
{
const res = try runFossil(gpa, io, &.{ "fossil", "settings", "binary-glob", "*" }, co_dir, env);
defer gpa.free(res.stdout);
defer gpa.free(res.stderr);
if (!okExit(res.term)) {
try out.print("FAIL S13 ({s}): fossil settings binary-glob failed\n stderr: {s}\n", .{ citation, res.stderr });
try printMismatchFooter(out, citation);
return false;
}
}
{
const res = try runFossil(gpa, io, &.{ "fossil", "commit", "-m", "pic", "--no-prompt", "--nosync" }, co_dir, env);
defer gpa.free(res.stdout);
defer gpa.free(res.stderr);
if (!okExit(res.term)) {
try out.print(
"FAIL S13 ({s}): fossil still refused the NUL-carrying file with binary-glob set to *\n stdout: {s}\n stderr: {s}\n",
.{ citation, firstLine(res.stdout), firstLine(res.stderr) },
);
try printMismatchFooter(out, citation);
return false;
}
}
return true;
}
// ---------------------------------------------------------------------------
// S14 -- the full `.fossil-settings/` differential.
// ---------------------------------------------------------------------------
/// The one fixture both tools open: every versioned name fossil recognizes,
/// written with every separator form `glob.c glob_create` accepts, committed
/// through fossil, then cloned by zit over the wire. The junk that follows is
/// authored identically into both checkouts, so any difference in the three
/// comparisons below is a difference in how the two tools READ the fixture.
///
/// The comparisons, and why each is shaped the way it is:
/// - `extras` and `extras --dotfiles`: byte for byte. Both print one
/// root-relative path per line, ascending, nothing else (`fossil extras`
/// passes `C_EXTRA` alone to `status_report`, so no classify column).
/// - `clean -n`: as normalized candidate SETS. fossil prefixes each line with
/// `Removed unmanaged file: ` (checkin.c `clean_cmd`) where zit prints the
/// bare path; the two lists are otherwise the same question. There are no
/// `KEPT` lines to drop -- fossil prints those only under `--verbose`.
/// - the post-`update` tree: byte for byte, each tool's own checkout-state
/// names elided, so `manifest`/`manifest.uuid` and the `empty-dirs`
/// directory have to land in both or the listing diverges.
fn runSettingsFixtureScenario(
arena: std.mem.Allocator,
gpa: std.mem.Allocator,
io: std.Io,
tmp_root: []const u8,
env: *const std.process.Environ.Map,
out: *std.Io.Writer,
) !bool {
const citation = ".fossil-settings";
const abs_exe = try zitAbsPath(arena, io);
const base = try std.fs.path.join(arena, &.{ tmp_root, "S14" });
const fos_dir = try std.fs.path.join(arena, &.{ base, "fos" });
try std.Io.Dir.cwd().createDirPath(io, fos_dir);
const repo = try std.fs.path.join(arena, &.{ base, "repo.fossil" });
if (!try fossilStepTagged(gpa, io, out, &.{ "fossil", "init", repo }, base, env, "S14", citation, "fossil init")) return false;
if (!try fossilStepTagged(gpa, io, out, &.{ "fossil", "open", "--nosync", repo }, fos_dir, env, "S14", citation, "fossil open")) return false;
// The fixture, authored on the fossil side and added by explicit path: a
// bare `fossil add .fossil-settings` would depend on how the scan treats a
// named dot directory, which is a different item's question.
try writeSettingsFixture(arena, io, fos_dir);
var add_argv: std.ArrayList([]const u8) = .empty;
try add_argv.appendSlice(arena, &.{ "fossil", "add" });
for (fixture.settings) |s| {
try add_argv.append(arena, try std.fmt.allocPrint(arena, ".fossil-settings/{s}", .{s.name}));
}
try add_argv.append(arena, fixture.tracked_file.name);
if (!try fossilStepTagged(gpa, io, out, add_argv.items, fos_dir, env, "S14", citation, "fossil add")) return false;
if (!try fossilStepTagged(
gpa,
io,
out,
&.{ "fossil", "commit", "-m", "settings fixture", "--no-prompt", "--nosync", "--user-override", "u" },
fos_dir,
env,
"S14",
citation,
"fossil commit",
)) return false;
// zit takes the same check-in off a real fossil server.
const port = try freshLoopbackPort(io);
const port_str = try std.fmt.allocPrint(arena, "{d}", .{port});
var server = spawnFossilServe(gpa, io, out, repo, base, env, port_str) catch |err| {
try out.print("FAIL S14 ({s}): could not spawn fossil server: {s}\n", .{ citation, @errorName(err) });
return false;
};
defer stopFossilServe(io, &server);
waitForListen(io, port) catch {
try out.print("FAIL S14 ({s}): fossil server never bound 127.0.0.1:{d}\n", .{ citation, port });
return false;
};
const url = try std.fmt.allocPrint(arena, "http://127.0.0.1:{d}/", .{port});
const zit_dir = try std.fs.path.join(arena, &.{ base, "zco" });
{
const r = (try zitStepTagged(gpa, io, out, abs_exe, base, env, &.{ "clone", "--no-open", url, "zco" }, "S14", citation, "zit clone")) orelse return false;
gpa.free(r.stdout);
gpa.free(r.stderr);
}
{
const r = (try zitStepTagged(gpa, io, out, abs_exe, zit_dir, env, &.{"open"}, "S14", citation, "zit open")) orelse return false;
gpa.free(r.stdout);
gpa.free(r.stderr);
}
// One update each, with the settings already in the tree: this is the point
// `manifest` and `empty-dirs` take effect in both tools (update.c reads
// both before the update runs).
if (!try fossilStepTagged(gpa, io, out, &.{ "fossil", "update" }, fos_dir, env, "S14", citation, "fossil update")) return false;
{
const r = (try zitStepTagged(gpa, io, out, abs_exe, zit_dir, env, &.{"update"}, "S14", citation, "zit update")) orelse return false;
gpa.free(r.stdout);
gpa.free(r.stderr);
}
try writeSettingsFixtureJunk(arena, io, fos_dir);
try writeSettingsFixtureJunk(arena, io, zit_dir);
const plain = (try compareListing(arena, gpa, io, out, abs_exe, env, fos_dir, zit_dir, &.{"extras"}, "extras")) orelse return false;
const dotted = (try compareListing(arena, gpa, io, out, abs_exe, env, fos_dir, zit_dir, &.{ "extras", "--dotfiles" }, "extras --dotfiles")) orelse return false;
if (!try expectListingShape(out, plain, dotted)) return false;
if (!try compareCleanPreview(arena, gpa, io, out, abs_exe, env, fos_dir, zit_dir)) return false;
if (!try compareTrees(arena, io, out, fos_dir, zit_dir)) return false;
if (!try compareForcedAdd(arena, gpa, io, out, abs_exe, env, fos_dir, zit_dir)) return false;
try out.print("PASS S14 ({s}): a checkout carrying every .fossil-settings name reads identically in both tools -- same extras with and without --dotfiles, same clean preview, same tree after update, same adds under --force\n", .{citation});
return true;
}
/// `add` reads the two glob lists the way fossil does: a
/// directory expansion skips ignore-glob AND clean-glob matches, `--force`
/// notwithstanding (add.c hands both to vfile_scan unconditionally); a file
/// named directly is refused when ignore-glob matches it (fossil prompts, a
/// non-tty answers no) and added silently when only clean-glob does. The
/// oracle is `changes` afterwards, byte for byte.
fn compareForcedAdd(
arena: std.mem.Allocator,
gpa: std.mem.Allocator,
io: std.Io,
out: *std.Io.Writer,
abs_exe: []const u8,
env: *const std.process.Environ.Map,
fos_dir: []const u8,
zit_dir: []const u8,
) !bool {
const citation = "add glob lists";
for ([_][]const u8{ fos_dir, zit_dir }) |dir| {
for ([_][2][]const u8{ .{ "sub/obj.o", "ignore-glob under sub\n" }, .{ "sub/junk.tmp", "clean-glob under sub\n" } }) |f| {
try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = try std.fs.path.join(arena, &.{ dir, f[0] }), .data = f[1] });
}
}
const steps = [_][]const []const u8{
&.{ "add", "--force", "sub" },
&.{ "add", "x.o", "scratch.tmp" },
};
for (steps) |tail| {
var fossil_argv: std.ArrayList([]const u8) = .empty;
try fossil_argv.append(arena, "fossil");
try fossil_argv.appendSlice(arena, tail);
const label = try std.mem.join(arena, " ", tail);
const f = try runFossil(gpa, io, fossil_argv.items, fos_dir, env);
defer gpa.free(f.stdout);
defer gpa.free(f.stderr);
if (!okExit(f.term)) {
try out.print("FAIL S14 ({s}): fossil {s} failed\n stderr: {s}\n", .{ citation, label, f.stderr });
return false;
}
const z = (try zitStepTagged(gpa, io, out, abs_exe, zit_dir, env, tail, "S14", citation, label)) orelse return false;
gpa.free(z.stdout);
gpa.free(z.stderr);
}
const agreed = (try compareListing(arena, gpa, io, out, abs_exe, env, fos_dir, zit_dir, &.{"changes"}, "changes")) orelse return false;
const added = [_][]const u8{ "ADDED sub/deep.log\n", "ADDED scratch.tmp\n" };
const refused = [_][]const u8{ "x.o\n", "sub/obj.o\n", "sub/junk.tmp\n" };
for (added) |line| {
if (std.mem.indexOf(u8, agreed, line) == null) {
try out.print("FAIL S14 ({s}): both tools agreed, but `changes` omits {s}", .{ citation, line });
return false;
}
}
for (refused) |name| {
if (std.mem.indexOf(u8, agreed, name) != null) {
try out.print("FAIL S14 ({s}): both tools agreed, but `changes` lists {s}", .{ citation, name });
return false;
}
}
return true;
}
/// The S14 fixture table, shared with `src/e2e.zig` so the lane CI runs and the
/// lane fossil confirms can never build different trees.
const fixture = zit.settings_fixture;
/// Writes the fixture's `.fossil-settings/` files and its one tracked payload
/// into `dir_path`. Both checkouts get it: fossil authors it, zit receives the
/// same bytes back over the wire.
fn writeSettingsFixture(arena: std.mem.Allocator, io: std.Io, dir_path: []const u8) !void {
try std.Io.Dir.cwd().createDirPath(io, try std.fs.path.join(arena, &.{ dir_path, ".fossil-settings" }));
for (fixture.settings) |s| {
const name = try std.fmt.allocPrint(arena, ".fossil-settings/{s}", .{s.name});
try std.Io.Dir.cwd().writeFile(io, .{
.sub_path = try std.fs.path.join(arena, &.{ dir_path, name }),
.data = s.value,
});
}
try std.Io.Dir.cwd().writeFile(io, .{
.sub_path = try std.fs.path.join(arena, &.{ dir_path, fixture.tracked_file.name }),
.data = fixture.tracked_file.value,
});
}
/// The untracked files, authored identically into each checkout.
fn writeSettingsFixtureJunk(arena: std.mem.Allocator, io: std.Io, dir_path: []const u8) !void {
for (fixture.junk_dirs) |d| {
try std.Io.Dir.cwd().createDirPath(io, try std.fs.path.join(arena, &.{ dir_path, d }));
}
for (fixture.junk) |j| {
try std.Io.Dir.cwd().writeFile(io, .{
.sub_path = try std.fs.path.join(arena, &.{ dir_path, j.path }),
.data = j.data,
});
}
}
/// Runs the SAME listing verb and flags in each checkout and requires the two
/// stdouts to be equal byte for byte. Returns the agreed output (arena-owned)
/// so the caller can assert on its content, or null when the scenario failed
/// and the failure has already been printed. `label` names the comparison.
fn compareListing(
arena: std.mem.Allocator,
gpa: std.mem.Allocator,
io: std.Io,
out: *std.Io.Writer,
abs_exe: []const u8,
env: *const std.process.Environ.Map,
fos_dir: []const u8,
zit_dir: []const u8,
tail: []const []const u8,
label: []const u8,
) !?[]const u8 {
const citation = ".fossil-settings";
std.debug.assert(tail.len != 0);
var fossil_argv: std.ArrayList([]const u8) = .empty;
try fossil_argv.append(arena, "fossil");
try fossil_argv.appendSlice(arena, tail);
const f = try runFossil(gpa, io, fossil_argv.items, fos_dir, env);
defer gpa.free(f.stdout);
defer gpa.free(f.stderr);
if (!okExit(f.term)) {
try out.print("FAIL S14 ({s}): fossil {s} failed\n stderr: {s}\n", .{ citation, label, f.stderr });
return null;
}
const z = (try zitStepTagged(gpa, io, out, abs_exe, zit_dir, env, tail, "S14", citation, label)) orelse return null;
defer gpa.free(z.stdout);
defer gpa.free(z.stderr);
if (!std.mem.eql(u8, f.stdout, z.stdout)) {
try out.print("FAIL S14 ({s}): `{s}` differs between the two tools\n fossil : {s}\n zit : {s}\n", .{ citation, label, f.stdout, z.stdout });
try printDiffWindow(out, z.stdout, f.stdout);
try printMismatchFooter(out, citation);
return null;
}
return try arena.dupe(u8, f.stdout);
}
/// Agreement is only worth something if both tools agreed on the RIGHT set: two
/// empty listings would pass `compareListing` and prove nothing. These are the
/// rules the fixture declares, checked against the agreed output.
fn expectListingShape(out: *std.Io.Writer, plain: []const u8, dotted: []const u8) !bool {
const citation = ".fossil-settings";
// Listed: the plain extra, the keep-glob match (still an extra), the
// clean-glob match (listing does not exclude it), the nested one, and the
// UTF-16 file.
const listed = [_][]const u8{ "notes.txt\n", "precious.dat\n", "scratch.tmp\n", "sub/deep.log\n", "wide.wide\n" };
// Excluded: the four ignore-glob matches -- one per separator form -- the
// manifest stamps this update wrote, and fossil's reserved `.fos` name.
const excluded = [_][]const u8{ "x.o\n", "build/gen.c\n", "logs/run.log\n", "spaced name.txt\n", "manifest\n", "manifest.uuid\n", ".fos\n" };
for ([_][]const u8{ plain, dotted }, [_][]const u8{ "extras", "extras --dotfiles" }) |text, label| {
for (listed) |name| {
if (std.mem.indexOf(u8, text, name) == null) {
try out.print("FAIL S14 ({s}): both tools agreed, but `{s}` omits {s}", .{ citation, label, name });
return false;
}
}
for (excluded) |name| {
if (std.mem.indexOf(u8, text, name) != null) {
try out.print("FAIL S14 ({s}): both tools agreed, but `{s}` lists {s}", .{ citation, label, name });
return false;
}
}
}
// `dotfiles off` is the whole difference between the two runs.
if (std.mem.indexOf(u8, plain, ".hidden\n") != null or std.mem.indexOf(u8, dotted, ".hidden\n") == null) {
try out.print("FAIL S14 ({s}): --dotfiles did not change what either tool listed\n plain : {s}\n dotted : {s}\n", .{ citation, plain, dotted });
return false;
}
return true;
}
/// fossil's `clean -n` prose prefix, stripped so the two previews can be
/// compared as the candidate sets they both are (checkin.c `clean_cmd`).
const fossil_clean_prefix = "Removed unmanaged file: ";
/// `clean -n` in fossil against `clean --dry-run` in zit: same candidates,
/// different line text. Both are normalized to a sorted bare-path list first.
fn compareCleanPreview(
arena: std.mem.Allocator,
gpa: std.mem.Allocator,
io: std.Io,
out: *std.Io.Writer,
abs_exe: []const u8,
env: *const std.process.Environ.Map,
fos_dir: []const u8,
zit_dir: []const u8,
) !bool {
const citation = ".fossil-settings";
const f = try runFossil(gpa, io, &.{ "fossil", "clean", "-n" }, fos_dir, env);
defer gpa.free(f.stdout);
defer gpa.free(f.stderr);
if (!okExit(f.term)) {
try out.print("FAIL S14 ({s}): fossil clean -n failed\n stderr: {s}\n", .{ citation, f.stderr });
return false;
}
const z = (try zitStepTagged(gpa, io, out, abs_exe, zit_dir, env, &.{ "clean", "--dry-run" }, "S14", citation, "clean --dry-run")) orelse return false;
defer gpa.free(z.stdout);
defer gpa.free(z.stderr);
const f_norm = try normalizeCleanPreview(arena, f.stdout, fossil_clean_prefix);
const z_norm = try normalizeCleanPreview(arena, z.stdout, "");
if (!std.mem.eql(u8, f_norm, z_norm)) {
try out.print("FAIL S14 ({s}): the clean previews name different files\n fossil : {s}\n zit : {s}\n", .{ citation, f_norm, z_norm });
try printMismatchFooter(out, citation);
return false;
}
// The clean-glob match is a candidate under `-n` (fossil skips the
// clean-glob test in dry run); the keep-glob match never is.
if (std.mem.indexOf(u8, f_norm, "scratch.tmp\n") == null or std.mem.indexOf(u8, f_norm, "precious.dat\n") != null) {
try out.print("FAIL S14 ({s}): both previews agreed, but on the wrong set\n {s}", .{ citation, f_norm });
return false;
}
return true;
}
/// Sorted, newline-joined bare paths from a clean preview: every non-empty line
/// with `prefix` removed. Bounded by the input's own line count.
fn normalizeCleanPreview(arena: std.mem.Allocator, text: []const u8, prefix: []const u8) ![]u8 {
var lines: std.ArrayList([]const u8) = .empty;
var it = std.mem.splitScalar(u8, text, '\n');
while (it.next()) |raw| {
const line = std.mem.trimEnd(u8, raw, "\r");
if (line.len == 0) continue;
if (prefix.len != 0 and !std.mem.startsWith(u8, line, prefix)) continue;
try lines.append(arena, line[prefix.len..]);
}
std.mem.sort([]const u8, lines.items, {}, lessThanPath);
var aw = std.Io.Writer.Allocating.init(arena); // arena-owned; no deinit
for (lines.items) |line| try aw.writer.print("{s}\n", .{line});
return aw.toOwnedSlice();
}
fn lessThanPath(_: void, a: []const u8, b: []const u8) bool {
return std.mem.lessThan(u8, a, b);
}
/// Both post-update trees, rendered as one sorted path per line, and required
/// to be equal byte for byte.
fn compareTrees(
arena: std.mem.Allocator,
io: std.Io,
out: *std.Io.Writer,
fos_dir: []const u8,
zit_dir: []const u8,
) !bool {
const citation = ".fossil-settings";
const f = try listTree(arena, io, fos_dir);
const z = try listTree(arena, io, zit_dir);
if (!std.mem.eql(u8, f, z)) {
try out.print("FAIL S14 ({s}): the two trees differ after update\n fossil :\n{s} zit :\n{s}", .{ citation, f, z });
try printMismatchFooter(out, citation);
return false;
}
// The two settings that write into the tree must actually have fired, or
// the comparison above proves only that both tools did nothing.
if (std.mem.indexOf(u8, f, "manifest.uuid\n") == null or std.mem.indexOf(u8, f, "emptyd/\n") == null) {
try out.print("FAIL S14 ({s}): neither tree carries the manifest stamps and the empty-dirs directory\n{s}", .{ citation, f });
return false;
}
return true;
}
/// Every path under `root_path`, one per line, ascending, directories marked
/// with a trailing `/`. Each tool's own checkout-state names are elided so the
/// two listings are comparable at all; everything else -- including the
/// `.fos` junk both tools hide from `extras` -- is elided by the same rule in
/// both trees.
fn listTree(arena: std.mem.Allocator, io: std.Io, root_path: []const u8) ![]u8 {
var paths: std.ArrayList([]const u8) = .empty;
var budget: usize = tree_listing_max_entries;
try walkTree(arena, io, root_path, "", 0, &paths, &budget);
std.mem.sort([]const u8, paths.items, {}, lessThanPath);
var aw = std.Io.Writer.Allocating.init(arena); // arena-owned; no deinit
for (paths.items) |p| try aw.writer.print("{s}\n", .{p});
return aw.toOwnedSlice();
}
/// Hard ceilings on the walk: the fixture is a couple of dozen entries two
/// levels deep, so either limit being reached means the fixture, not the tree,
/// is wrong.
const tree_listing_max_entries: usize = 512;
const tree_listing_max_depth: usize = 8;
fn walkTree(
arena: std.mem.Allocator,
io: std.Io,
dir_path: []const u8,
prefix: []const u8,
depth: usize,
paths: *std.ArrayList([]const u8),
budget: *usize,
) !void {
if (depth >= tree_listing_max_depth) return error.TreeTooDeep;
var dir = try std.Io.Dir.cwd().openDir(io, dir_path, .{
.access_sub_paths = true,
.iterate = true,
.follow_symlinks = false,
});
defer dir.close(io);
var it = dir.iterate();
while (try it.next(io)) |entry| {
if (budget.* == 0) return error.TreeTooLarge;
budget.* -= 1;
if (depth == 0 and isCheckoutStateName(entry.name)) continue;
const rel = if (prefix.len == 0)
try arena.dupe(u8, entry.name)
else
try std.fmt.allocPrint(arena, "{s}/{s}", .{ prefix, entry.name });
if (entry.kind == .directory) {
try paths.append(arena, try std.fmt.allocPrint(arena, "{s}/", .{rel}));
try walkTree(arena, io, try std.fs.path.join(arena, &.{ dir_path, entry.name }), rel, depth + 1, paths, budget);
} else {
try paths.append(arena, rel);
}
}
}
/// Root-level names one tool owns and the other does not: fossil's checkout
/// database family (`_FOSSIL_`, `.fslckout`, the deprecated `.fos`, each with
/// its SQLite `-journal`/`-wal`/`-shm` siblings, add.c's reserved list) and
/// zit's `.zit*` container roots (`control_paths.reserved`). `.fossil-settings`
/// is tracked content and is deliberately NOT matched by the `.fos` arm.
fn isCheckoutStateName(name: []const u8) bool {
if (std.mem.eql(u8, name, ".zit") or std.mem.startsWith(u8, name, ".zit-")) return true;
if (std.mem.startsWith(u8, name, ".fslckout")) return true;
if (std.mem.startsWith(u8, name, "_FOSSIL_")) return true;
if (std.mem.eql(u8, name, ".fos") or std.mem.startsWith(u8, name, ".fos-")) return true;
return false;
}
/// S18 — `zit uv` against a stock fossil server: list, pull, push and rm,
/// under fossil's capabilities, and nothing stored in the repository.
///
/// quarry keeps unversioned files in its own storage, so zit holds none: the
/// client moves them between the server and plain files. A stock fossil
/// server is the reference for the wire: `fossil uv list` and `fossil uv cat`
/// on the server have to show what zit sent, and what zit fetched has to be
/// what `fossil uv cat` returns. Reading takes `o`, changing takes `y`.
fn runUnversionedScenario(
arena: std.mem.Allocator,
gpa: std.mem.Allocator,
io: std.Io,
tmp_root: []const u8,
env: *const std.process.Environ.Map,
out: *std.Io.Writer,
) !bool {
const citation = "unversioned files";
const abs_exe = try zitAbsPath(arena, io);
const base = try std.fs.path.join(arena, &.{ tmp_root, "S18" });
try std.Io.Dir.cwd().createDirPath(io, base);
const peer_repo = try std.fs.path.join(arena, &.{ base, "peer.fossil" });
const seed = try std.fs.path.join(arena, &.{ base, "seed.txt" });
try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = seed, .data = "first release\n" });
const Step = struct { argv: []const []const u8, what: []const u8 };
for ([_]Step{
.{ .argv = &.{ "fossil", "init", peer_repo }, .what = "fossil init" },
.{ .argv = &.{ "fossil", "uv", "add", seed, "--as", "releases/1/a.txt", "-R", peer_repo }, .what = "fossil uv add" },
.{ .argv = &.{ "fossil", "user", "new", "uvreader", "Reader", "sekret", "-R", peer_repo }, .what = "fossil user new reader" },
.{ .argv = &.{ "fossil", "user", "capabilities", "uvreader", "o", "-R", peer_repo }, .what = "fossil user capabilities reader" },
.{ .argv = &.{ "fossil", "user", "new", "uvwriter", "Writer", "sekret", "-R", peer_repo }, .what = "fossil user new writer" },
.{ .argv = &.{ "fossil", "user", "capabilities", "uvwriter", "oy", "-R", peer_repo }, .what = "fossil user capabilities writer" },
.{ .argv = &.{ "fossil", "user", "capabilities", "nobody", "", "-R", peer_repo }, .what = "fossil user capabilities nobody" },
.{ .argv = &.{ "fossil", "user", "capabilities", "anonymous", "", "-R", peer_repo }, .what = "fossil user capabilities anonymous" },
}) |step| {
const res = try runFossil(gpa, io, step.argv, base, env);
defer gpa.free(res.stdout);
defer gpa.free(res.stderr);
if (!okExit(res.term)) {
try out.print("FAIL S18 ({s}): {s} failed\n stderr: {s}\n", .{ citation, step.what, res.stderr });
return false;
}
}
const port = try freshLoopbackPort(io);
const port_str = try std.fmt.allocPrint(arena, "{d}", .{port});
var server = spawnFossilServe(gpa, io, out, peer_repo, base, env, port_str) catch |err| {
try out.print("FAIL S18 ({s}): could not spawn fossil server: {s}\n", .{ citation, @errorName(err) });
return false;
};
defer stopFossilServe(io, &server);
waitForListen(io, port) catch {
try out.print("FAIL S18 ({s}): fossil server never bound 127.0.0.1:{d}\n", .{ citation, port });
return false;
};
const url = try std.fmt.allocPrint(arena, "http://127.0.0.1:{d}/", .{port});
// The login a uv command signs with is the repository's, so the client is
// a clone of the server.
const mirror = try std.fs.path.join(arena, &.{ base, "mirror" });
{
const res = try std.process.run(gpa, io, .{ .argv = &.{ abs_exe, "clone", url, mirror, "--user", "uvreader", "--password", "sekret" }, .cwd = .{ .path = base }, .environ_map = env });
defer gpa.free(res.stdout);
defer gpa.free(res.stderr);
if (!okExit(res.term)) {
try out.print("FAIL S18 ({s}): the clone was refused\n stderr: {s}\n", .{ citation, res.stderr });
return false;
}
}
const store_path = try std.fs.path.join(arena, &.{ mirror, ".zit" });
const store_before = try std.Io.Dir.cwd().readFileAlloc(io, store_path, arena, .unlimited);
const Run = struct {
fn zit(g: std.mem.Allocator, i: std.Io, exe: []const u8, cwd: []const u8, e: *const std.process.Environ.Map, tail: []const []const u8) !std.process.RunResult {
var argv: [16][]const u8 = undefined;
argv[0] = exe;
argv[1] = "uv";
for (tail, argv[2..][0..tail.len]) |t, *d| d.* = t;
return std.process.run(g, i, .{ .argv = argv[0 .. 2 + tail.len], .cwd = .{ .path = cwd }, .environ_map = e });
}
};
const reader = [_][]const u8{ "--user", "uvreader", "--password", "sekret" };
const writer = [_][]const u8{ "--user", "uvwriter", "--password", "sekret" };
// Anonymous: no `o`, so the server shares nothing, and says nothing.
{
const res = try Run.zit(gpa, io, abs_exe, mirror, env, &.{ "list", url, "--user", "" });
defer gpa.free(res.stdout);
defer gpa.free(res.stderr);
if (okExit(res.term) or std.mem.indexOf(u8, res.stderr, "does not share") == null) {
try out.print("FAIL S18 ({s}): an anonymous list of a read-gated server was not refused in words\n stdout: {s}\n stderr: {s}\n", .{ citation, res.stdout, res.stderr });
return false;
}
}
// The reader lists and fetches; the bytes are the server's.
{
const res = try Run.zit(gpa, io, abs_exe, mirror, env, &(.{ "list", url } ++ reader));
defer gpa.free(res.stdout);
defer gpa.free(res.stderr);
if (!okExit(res.term) or !std.mem.eql(u8, res.stdout, "releases/1/a.txt\n")) {
try out.print("FAIL S18 ({s}): uv list did not show the server's one file\n stdout: {s}\n stderr: {s}\n", .{ citation, res.stdout, res.stderr });
return false;
}
}
const got_dir = try std.fs.path.join(arena, &.{ base, "got" });
{
const res = try Run.zit(gpa, io, abs_exe, mirror, env, &(.{ "pull", url, "releases/1/a.txt", "-o", got_dir } ++ reader));
defer gpa.free(res.stdout);
defer gpa.free(res.stderr);
const want = try runFossil(gpa, io, &.{ "fossil", "uv", "cat", "releases/1/a.txt", "-R", peer_repo }, base, env);
defer gpa.free(want.stdout);
defer gpa.free(want.stderr);
const got_path = try std.fs.path.join(arena, &.{ got_dir, "releases", "1", "a.txt" });
const got = std.Io.Dir.cwd().readFileAlloc(io, got_path, arena, .unlimited) catch "";
if (!okExit(res.term) or !std.mem.eql(u8, got, want.stdout)) {
try out.print("FAIL S18 ({s}): uv pull did not write the bytes fossil uv cat returns\n stderr: {s}\n", .{ citation, res.stderr });
return false;
}
}
// The reader may not change anything, and is told so.
const new_file = try std.fs.path.join(arena, &.{ base, "b.txt" });
try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = new_file, .data = "second release\n" });
{
const res = try Run.zit(gpa, io, abs_exe, mirror, env, &(.{ "push", url, new_file, "--as", "releases/2/b.txt" } ++ reader));
defer gpa.free(res.stdout);
defer gpa.free(res.stderr);
if (okExit(res.term) or std.mem.indexOf(u8, res.stderr, "not change them") == null) {
try out.print("FAIL S18 ({s}): a push without 'y' was not refused in words\n stderr: {s}\n", .{ citation, res.stderr });
return false;
}
}
// The writer pushes, and fossil serves exactly those bytes.
{
const res = try Run.zit(gpa, io, abs_exe, mirror, env, &(.{ "push", url, new_file, "--as", "releases/2/b.txt" } ++ writer));
defer gpa.free(res.stdout);
defer gpa.free(res.stderr);
const cat = try runFossil(gpa, io, &.{ "fossil", "uv", "cat", "releases/2/b.txt", "-R", peer_repo }, base, env);
defer gpa.free(cat.stdout);
defer gpa.free(cat.stderr);
if (!okExit(res.term) or !std.mem.eql(u8, cat.stdout, "second release\n")) {
try out.print("FAIL S18 ({s}): fossil uv cat does not return what zit uv push sent\n zit stderr: {s}\n fossil: {s}{s}\n", .{ citation, res.stderr, cat.stdout, cat.stderr });
return false;
}
}
// The writer deletes; fossil keeps the tombstone, and zit lists it.
{
const res = try Run.zit(gpa, io, abs_exe, mirror, env, &(.{ "rm", url, "releases/1/a.txt", "--mtime", "4102444800" } ++ writer));
defer gpa.free(res.stdout);
defer gpa.free(res.stderr);
const listed = try runFossil(gpa, io, &.{ "fossil", "uv", "list", "--all", "-R", peer_repo }, base, env);
defer gpa.free(listed.stdout);
defer gpa.free(listed.stderr);
var tomb = false;
var lines = std.mem.splitScalar(u8, listed.stdout, '\n');
while (lines.next()) |line| {
if (std.mem.indexOf(u8, line, "releases/1/a.txt") != null and std.mem.indexOf(u8, line, "deleted") != null) tomb = true;
}
if (!okExit(res.term) or !tomb) {
try out.print("FAIL S18 ({s}): fossil does not show the tombstone zit uv rm sent\n zit stderr: {s}\n fossil: {s}\n", .{ citation, res.stderr, listed.stdout });
return false;
}
const long = try Run.zit(gpa, io, abs_exe, mirror, env, &(.{ "list", url, "-l" } ++ reader));
defer gpa.free(long.stdout);
defer gpa.free(long.stderr);
if (std.mem.indexOf(u8, long.stdout, "releases/1/a.txt 4102444800 deleted 0\n") == null) {
try out.print("FAIL S18 ({s}): uv list -l does not show the tombstone\n stdout: {s}\n", .{ citation, long.stdout });
return false;
}
}
// None of it touched the repository.
const store_after = try std.Io.Dir.cwd().readFileAlloc(io, store_path, arena, .unlimited);
if (!std.mem.eql(u8, store_before, store_after)) {
try out.print("FAIL S18 ({s}): the local .zit changed; uv must store nothing\n", .{citation});
return false;
}
try out.print("PASS S18 ({s}): against a stock fossil server, zit uv lists and pulls with 'o', pushes and deletes with 'y' and is refused in words without it, fossil serves exactly what it sent, and the local .zit is untouched\n", .{citation});
return true;
}
/// A stand-in for a front such as quarry: listens on loopback, adds
/// `headers` to every request, forwards it to a `zit serve --fronted` on
/// `upstream`, and copies the reply back. One request per connection, as both
/// ends close after a reply.
const FrontProxy = struct {
io: std.Io,
listener: net.Server,
port: u16,
upstream: u16,
headers: []const u8,
stopping: std.atomic.Value(bool) = .init(false),
thread: std.Thread = undefined,
fn start(self: *FrontProxy) !void {
self.thread = try std.Thread.spawn(.{}, run, .{self});
}
fn stop(self: *FrontProxy) void {
self.stopping.store(true, .release);
const addr = net.IpAddress.parseIp4("127.0.0.1", self.port) catch unreachable;
if (addr.connect(self.io, .{ .mode = .stream })) |c| c.close(self.io) else |_| {}
self.thread.join();
self.listener.deinit(self.io);
}
fn run(self: *FrontProxy) void {
while (!self.stopping.load(.acquire)) {
const conn = self.listener.accept(self.io) catch return;
defer conn.close(self.io);
if (self.stopping.load(.acquire)) return;
self.relay(conn) catch {};
}
}
fn relay(self: *FrontProxy, conn: net.Stream) !void {
const a = std.heap.page_allocator;
var rbuf: [16 * 1024]u8 = undefined;
var r = conn.reader(self.io, &rbuf);
var head: std.ArrayListUnmanaged(u8) = .empty;
defer head.deinit(a);
var body_len: usize = 0;
while (true) {
const line = try r.interface.takeDelimiterInclusive('\n');
if (std.mem.eql(u8, line, "\r\n")) break;
const colon = std.mem.indexOfScalar(u8, line, ':') orelse 0;
if (colon != 0 and std.ascii.eqlIgnoreCase(line[0..colon], "content-length")) {
body_len = try std.fmt.parseInt(usize, std.mem.trim(u8, line[colon + 1 ..], " \r\n"), 10);
}
try head.appendSlice(a, line);
}
try head.appendSlice(a, self.headers);
try head.appendSlice(a, "\r\n");
const body = try r.interface.readAlloc(a, body_len);
defer a.free(body);
const addr = try net.IpAddress.parseIp4("127.0.0.1", self.upstream);
const up = try addr.connect(self.io, .{ .mode = .stream });
defer up.close(self.io);
var ubuf: [4096]u8 = undefined;
var uw = up.writer(self.io, &ubuf);
try uw.interface.writeAll(head.items);
try uw.interface.writeAll(body);
try uw.interface.flush();
var urbuf: [16 * 1024]u8 = undefined;
var ur = up.reader(self.io, &urbuf);
const reply = try ur.interface.allocRemaining(a, .limited(64 << 20));
defer a.free(reply);
var wbuf: [4096]u8 = undefined;
var w = conn.writer(self.io, &wbuf);
try w.interface.writeAll(reply);
try w.interface.flush();
}
};
/// S19 -- a stock fossil client pushing onto a protected branch behind a front
/// that allows a divert.
///
/// WHAT THIS PINS. The whole experience is fossil's own: a commit whose
/// autosync push is refused prints the server's text naming the attempt
/// branch, and keeps the commit; `fossil sync` then prints the server's
/// message ("Server says: ...") and pulls the move, so the checkout itself is
/// on the attempt branch afterwards; with `autosync-tries 2` fossil retries on
/// its own, the retry is the second push, and one command prints both the
/// refusal and where the work went. Trunk on the server never moves.
fn runProtectedDivertScenario(
arena: std.mem.Allocator,
gpa: std.mem.Allocator,
io: std.Io,
tmp_root: []const u8,
env: *const std.process.Environ.Map,
out: *std.Io.Writer,
) !bool {
const citation = "protected-branch divert";
const abs_exe = try zitAbsPath(arena, io);
const base = try std.fs.path.join(arena, &.{ tmp_root, "S19" });
const src_dir = try std.fs.path.join(arena, &.{ base, "src" });
try std.Io.Dir.cwd().createDirPath(io, src_dir);
{
const r = (try zitStepIn(gpa, io, out, abs_exe, src_dir, env, &.{"init"}, "zit init", "S19", citation)) orelse return false;
gpa.free(r.stdout);
gpa.free(r.stderr);
}
try std.Io.Dir.cwd().writeFile(io, .{
.sub_path = try std.fs.path.join(arena, &.{ src_dir, "seed.txt" }),
.data = "seed\n",
});
inline for (.{
.{ &[_][]const u8{ "add", "seed.txt" }, "zit add" },
.{ &[_][]const u8{ "commit", "-m", "seed", "--user", "u" }, "zit commit" },
}) |st| {
const r = (try zitStepIn(gpa, io, out, abs_exe, src_dir, env, st[0], st[1], "S19", citation)) orelse return false;
gpa.free(r.stdout);
gpa.free(r.stderr);
}
const seed_tip = blk: {
const r = (try zitStepIn(gpa, io, out, abs_exe, src_dir, env, &.{ "info", "trunk" }, "zit info trunk", "S19", citation)) orelse return false;
defer gpa.free(r.stdout);
defer gpa.free(r.stderr);
break :blk try arena.dupe(u8, r.stdout[0 .. std.mem.indexOfScalar(u8, r.stdout, '\n') orelse r.stdout.len]);
};
const serve_port = try freshLoopbackPort(io);
const serve_port_str = try std.fmt.allocPrint(arena, "{d}", .{serve_port});
var server = try spawnZitServeArgs(io, abs_exe, src_dir, env, &.{ abs_exe, "serve", "--port", serve_port_str, "--fronted" });
defer server.kill(io);
waitForListen(io, serve_port) catch {
try out.print("FAIL S19 ({s}): zit serve never bound 127.0.0.1:{d}\n", .{ citation, serve_port });
return false;
};
const any = try net.IpAddress.parseIp4("127.0.0.1", 0);
var front = FrontProxy{
.io = io,
.listener = try any.listen(io, .{}),
.port = 0,
.upstream = serve_port,
.headers = "X-Zit-Protect: trunk\r\nX-Zit-Divert-Login: jan\r\n",
};
front.port = front.listener.socket.address.getPort();
try front.start();
defer front.stop();
const url = try std.fmt.allocPrint(arena, "http://127.0.0.1:{d}/", .{front.port});
// A pull is not a push: the clone through the front is answered as ever.
const clone_repo = try std.fs.path.join(arena, &.{ base, "peer.fossil" });
const co_dir = try std.fs.path.join(arena, &.{ base, "co" });
try std.Io.Dir.cwd().createDirPath(io, co_dir);
inline for (.{
.{ &[_][]const u8{ "fossil", "clone", url, clone_repo }, base, "fossil clone" },
.{ &[_][]const u8{ "fossil", "open", "--nosync", clone_repo }, co_dir, "fossil open" },
}) |st| {
const res = try runFossil(gpa, io, st[0], if (std.mem.eql(u8, st[2], "fossil clone")) base else co_dir, env);
defer gpa.free(res.stdout);
defer gpa.free(res.stderr);
if (!okExit(res.term)) {
try out.print("FAIL S19 ({s}): {s} failed\n stderr: {s}\n", .{ citation, st[2], res.stderr });
return false;
}
}
// 1. A commit onto trunk: kept locally, its autosync push refused with
// the attempt branch named.
try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = try std.fs.path.join(arena, &.{ co_dir, "one.txt" }), .data = "one\n" });
{
const add = try runFossil(gpa, io, &.{ "fossil", "add", "one.txt" }, co_dir, env);
gpa.free(add.stdout);
gpa.free(add.stderr);
const res = try runFossil(gpa, io, &.{ "fossil", "commit", "-m", "onto trunk", "--no-warnings" }, co_dir, env);
defer gpa.free(res.stdout);
defer gpa.free(res.stderr);
const said = try std.mem.concat(arena, u8, &.{ res.stdout, res.stderr });
if (std.mem.indexOf(u8, said, "would join protected branch 'trunk'") == null or std.mem.indexOf(u8, said, "attempt/jan/") == null) {
try out.print("FAIL S19 ({s}): the commit's autosync push was not refused with the attempt branch named\n output: {s}\n", .{ citation, said });
return false;
}
}
// 2. `fossil sync`: the server's message, and the checkout on the
// attempt branch once the move is pulled.
{
const res = try runFossil(gpa, io, &.{ "fossil", "sync" }, co_dir, env);
defer gpa.free(res.stdout);
defer gpa.free(res.stderr);
const said = try std.mem.concat(arena, u8, &.{ res.stdout, res.stderr });
if (std.mem.indexOf(u8, said, "Server says: check-in ") == null or std.mem.indexOf(u8, said, "kept on branch attempt/jan/") == null) {
try out.print("FAIL S19 ({s}): fossil sync did not print where the work was kept\n output: {s}\n", .{ citation, said });
return false;
}
const cur = try runFossil(gpa, io, &.{ "fossil", "branch", "current" }, co_dir, env);
defer gpa.free(cur.stdout);
defer gpa.free(cur.stderr);
if (!std.mem.startsWith(u8, cur.stdout, "attempt/jan/")) {
try out.print("FAIL S19 ({s}): after the sync the checkout is not on the attempt branch\n fossil branch current: {s}\n", .{ citation, cur.stdout });
return false;
}
}
// 3. fossil's own retry is the second push: one command, both lines.
inline for (.{
.{ &[_][]const u8{ "fossil", "update", "trunk" }, "fossil update trunk" },
.{ &[_][]const u8{ "fossil", "settings", "autosync-tries", "2" }, "fossil settings autosync-tries" },
}) |st| {
const res = try runFossil(gpa, io, st[0], co_dir, env);
defer gpa.free(res.stdout);
defer gpa.free(res.stderr);
if (!okExit(res.term)) {
try out.print("FAIL S19 ({s}): {s} failed\n stderr: {s}\n", .{ citation, st[1], res.stderr });
return false;
}
}
try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = try std.fs.path.join(arena, &.{ co_dir, "two.txt" }), .data = "two\n" });
{
const add = try runFossil(gpa, io, &.{ "fossil", "add", "two.txt" }, co_dir, env);
gpa.free(add.stdout);
gpa.free(add.stderr);
const res = try runFossil(gpa, io, &.{ "fossil", "commit", "-m", "onto trunk again", "--no-warnings" }, co_dir, env);
defer gpa.free(res.stdout);
defer gpa.free(res.stderr);
const said = try std.mem.concat(arena, u8, &.{ res.stdout, res.stderr });
if (std.mem.indexOf(u8, said, "would join protected branch 'trunk'") == null or std.mem.indexOf(u8, said, "Server says: check-in ") == null) {
try out.print("FAIL S19 ({s}): with autosync-tries 2 one commit did not print both the refusal and where the work went\n output: {s}\n", .{ citation, said });
return false;
}
}
// THE PROOF: trunk on the server is where its owners left it.
{
const r = (try zitStepIn(gpa, io, out, abs_exe, src_dir, env, &.{ "info", "trunk" }, "zit info trunk", "S19", citation)) orelse return false;
defer gpa.free(r.stdout);
defer gpa.free(r.stderr);
if (!std.mem.startsWith(u8, r.stdout, seed_tip)) {
try out.print("FAIL S19 ({s}): trunk on the server moved\n zit info trunk: {s}\n", .{ citation, r.stdout });
return false;
}
}
try out.print("PASS S19 ({s}): a stock fossil commit onto a protected branch is refused with the way out, fossil's next sync prints where the work was kept and lands the checkout there, fossil's own retry counts as the second push, and trunk never moves\n", .{citation});
return true;
}