import { Database } from "bun:sqlite"; import { expect, test } from "bun:test"; import { existsSync } from "node:fs"; import { mkdir, readFile, writeFile } from "node:fs/promises"; import { dirname, join } from "node:path"; import { openBucket } from "../src/bucket/reader.ts"; import { syncAll } from "../src/bucket/sync.ts"; import { openMirror, quarantinedPaths } from "../src/mirror/store.ts"; import { openKeprDb } from "../src/server/db.ts"; import { makeKohBucket } from "./fixture.ts"; // ยง5.5 is a contract between the repos: every path either side drops, the other drops too. const UNSAFE = [ "../joint-escape.txt", "/tmp/koh-joint-absolute.txt", "a/../../joint-nested.txt", "back\\slash.txt", "ctl\u0001name.txt", "Icon\r", "c1-\u0085", // JSON.stringify writes this as an unpaired `\ud800` escape. "lone-\ud800", // Its X becomes the byte 0xff below: a name that is not UTF-8 at all. "raw-X-byte", ]; // Strips that would reach `src/` if either side read their illegal paths loosely. const ILLEGAL_STRIPS = ["src//", "", "src\ud800"]; test("Koh and Kepr drop the same unsafe paths and keep the same file list for every save (A3)", async () => { const fx = await makeKohBucket("joint-paths"); const head = fx.saveIds[0] as string; const key = join(fx.bucketDir, fx.repo, "saves", head); const record = JSON.parse(await readFile(key, "utf8")); const hash = record.files[0].hash; for (const path of UNSAFE) record.files.push({ path, hash, executable: false }); const bytes = Buffer.from(JSON.stringify(record)); bytes[bytes.indexOf("raw-X-byte") + 4] = 0xff; await writeFile(key, bytes); for (const [i, path] of ILLEGAL_STRIPS.entries()) { const id = `${i}`.repeat(12); const strip = { id, kind: "strip", save_id: "", path, reason: "", created_at: 1, machine: "x" }; await writeFile(join(fx.bucketDir, fx.repo, "events", id), JSON.stringify(strip)); } const root = dirname(dirname(fx.repoDir)); const home = join(root, "second-home"); const work = join(root, "second-work"); await mkdir(home); await mkdir(work); const runKoh = (cwd: string, args: string[]) => Bun.spawnSync(["koh", ...args], { cwd, env: { PATH: process.env.PATH, HOME: home, NO_COLOR: "1" }, }); expect(runKoh(work, ["remote", "folder", fx.bucketDir]).exitCode).toBe(0); const stolen = runKoh(work, ["steal", fx.repo, "--json"]); expect(stolen.exitCode, stolen.stderr.toString()).toBe(0); const dropped: string[] = JSON.parse(stolen.stdout.toString()).unsafe_paths; const dataDir = join(root, "kepr-data"); const kepr = openKeprDb(dataDir); await syncAll({ bucket: openBucket({ kind: "folder", path: fx.bucketDir }), kepr, mirrorDir: join(dataDir, "mirror"), }); expect(kepr.query("SELECT last_sync_error FROM repos").get()).toEqual({ last_sync_error: null }); const fileLists = (db: Database) => db.query("SELECT save_id, path FROM save_files ORDER BY save_id, path").all(); const koh = new Database(join(work, fx.repo, ".koh")); const mirror = openMirror(join(dataDir, "mirror"), fx.repo); expect(fileLists(koh)).toEqual(fileLists(mirror)); expect(fileLists(koh)).toContainEqual({ save_id: head, path: "src/main.ts" }); // Same verdict and the same escaped name on both sides, name for name. expect(dropped.length).toBe(UNSAFE.length); expect([...dropped].sort()).toEqual(quarantinedPaths(mirror)); for (const path of UNSAFE) { expect(fileLists(koh)).not.toContainEqual({ save_id: head, path }); } koh.close(); for (const outside of [ join(work, "joint-escape.txt"), join(root, "joint-nested.txt"), "/tmp/koh-joint-absolute.txt", ]) { expect(existsSync(outside), outside).toBe(false); } });