kohpaths.test.ts

3.6 KB · raw

1import { Database } from "bun:sqlite";2import { expect, test } from "bun:test";3import { existsSync } from "node:fs";4import { mkdir, readFile, writeFile } from "node:fs/promises";5import { dirname, join } from "node:path";6import { openBucket } from "../src/bucket/reader.ts";7import { syncAll } from "../src/bucket/sync.ts";8import { openMirror, quarantinedPaths } from "../src/mirror/store.ts";9import { openKeprDb } from "../src/server/db.ts";10import { makeKohBucket } from "./fixture.ts";1112// §5.5 is a contract between the repos: every path either side drops, the other drops too.13const UNSAFE = [14	"../joint-escape.txt",15	"/tmp/koh-joint-absolute.txt",16	"a/../../joint-nested.txt",17	"back\\slash.txt",18	"ctl\u0001name.txt",19	"Icon\r",20	"c1-\u0085",21	// JSON.stringify writes this as an unpaired `\ud800` escape.22	"lone-\ud800",23	// Its X becomes the byte 0xff below: a name that is not UTF-8 at all.24	"raw-X-byte",25];26// Strips that would reach `src/` if either side read their illegal paths loosely.27const ILLEGAL_STRIPS = ["src//", "", "src\ud800"];2829test("Koh and Kepr drop the same unsafe paths and keep the same file list for every save (A3)", async () => {30	const fx = await makeKohBucket("joint-paths");31	const head = fx.saveIds[0] as string;32	const key = join(fx.bucketDir, fx.repo, "saves", head);33	const record = JSON.parse(await readFile(key, "utf8"));34	const hash = record.files[0].hash;35	for (const path of UNSAFE) record.files.push({ path, hash, executable: false });36	const bytes = Buffer.from(JSON.stringify(record));37	bytes[bytes.indexOf("raw-X-byte") + 4] = 0xff;38	await writeFile(key, bytes);39	for (const [i, path] of ILLEGAL_STRIPS.entries()) {40		const id = `${i}`.repeat(12);41		const strip = { id, kind: "strip", save_id: "", path, reason: "", created_at: 1, machine: "x" };42		await writeFile(join(fx.bucketDir, fx.repo, "events", id), JSON.stringify(strip));43	}4445	const root = dirname(dirname(fx.repoDir));46	const home = join(root, "second-home");47	const work = join(root, "second-work");48	await mkdir(home);49	await mkdir(work);50	const runKoh = (cwd: string, args: string[]) =>51		Bun.spawnSync(["koh", ...args], {52			cwd,53			env: { PATH: process.env.PATH, HOME: home, NO_COLOR: "1" },54		});55	expect(runKoh(work, ["remote", "folder", fx.bucketDir]).exitCode).toBe(0);56	const stolen = runKoh(work, ["steal", fx.repo, "--json"]);57	expect(stolen.exitCode, stolen.stderr.toString()).toBe(0);58	const dropped: string[] = JSON.parse(stolen.stdout.toString()).unsafe_paths;5960	const dataDir = join(root, "kepr-data");61	const kepr = openKeprDb(dataDir);62	await syncAll({63		bucket: openBucket({ kind: "folder", path: fx.bucketDir }),64		kepr,65		mirrorDir: join(dataDir, "mirror"),66	});67	expect(kepr.query("SELECT last_sync_error FROM repos").get()).toEqual({ last_sync_error: null });6869	const fileLists = (db: Database) =>70		db.query("SELECT save_id, path FROM save_files ORDER BY save_id, path").all();71	const koh = new Database(join(work, fx.repo, ".koh"));72	const mirror = openMirror(join(dataDir, "mirror"), fx.repo);73	expect(fileLists(koh)).toEqual(fileLists(mirror));74	expect(fileLists(koh)).toContainEqual({ save_id: head, path: "src/main.ts" });75	// Same verdict and the same escaped name on both sides, name for name.76	expect(dropped.length).toBe(UNSAFE.length);77	expect([...dropped].sort()).toEqual(quarantinedPaths(mirror));78	for (const path of UNSAFE) {79		expect(fileLists(koh)).not.toContainEqual({ save_id: head, path });80	}81	koh.close();8283	for (const outside of [84		join(work, "joint-escape.txt"),85		join(root, "joint-nested.txt"),86		"/tmp/koh-joint-absolute.txt",87	]) {88		expect(existsSync(outside), outside).toBe(false);89	}90});