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";111213const UNSAFE = [14 "../joint-escape.txt",15 "/tmp/koh3-joint-absolute.txt",16 "a/../../joint-nested.txt",17 "back\\slash.txt",18 "ctl\u0001name.txt",19 "Icon\r",20];2122test("Koh and Kepr drop the same unsafe paths and keep the same file list for every save (A3)", async () => {23 const fx = await makeKohBucket("joint-paths");24 const head = fx.saveIds[0] as string;25 const key = join(fx.bucketDir, fx.repo, "saves", head);26 const record = JSON.parse(await readFile(key, "utf8"));27 const hash = record.files[0].hash;28 for (const path of UNSAFE) record.files.push({ path, hash, executable: false });29 await writeFile(key, JSON.stringify(record));3031 const root = dirname(dirname(fx.repoDir));32 const home = join(root, "second-home");33 const work = join(root, "second-work");34 await mkdir(home);35 await mkdir(work);36 const koh3 = (cwd: string, args: string[]) =>37 Bun.spawnSync(["koh3", ...args], {38 cwd,39 env: { PATH: process.env.PATH, HOME: home, NO_COLOR: "1" },40 });41 expect(koh3(work, ["remote", "folder", fx.bucketDir]).exitCode).toBe(0);42 const stolen = koh3(work, ["steal", fx.repo, "--json"]);43 expect(stolen.exitCode, stolen.stderr.toString()).toBe(0);44 const dropped: string[] = JSON.parse(stolen.stdout.toString()).unsafe_paths;4546 const dataDir = join(root, "kepr-data");47 const kepr = openKeprDb(dataDir);48 await syncAll({49 bucket: openBucket({ kind: "folder", path: fx.bucketDir }),50 kepr,51 mirrorDir: join(dataDir, "mirror"),52 tarballDir: join(dataDir, "tarballs"),53 });54 expect(kepr.query("SELECT last_sync_error FROM repos").get()).toEqual({ last_sync_error: null });5556 const fileLists = (db: Database) =>57 db.query("SELECT save_id, path FROM save_files ORDER BY save_id, path").all();58 const koh = new Database(join(work, fx.repo, ".koh"));59 const mirror = openMirror(join(dataDir, "mirror"), fx.repo);60 expect(fileLists(koh)).toEqual(fileLists(mirror));61 expect(fileLists(koh).length).toBeGreaterThan(0);62 63 expect(dropped.length).toBe(UNSAFE.length);64 expect([...dropped].sort()).toEqual(quarantinedPaths(mirror));65 for (const path of UNSAFE) {66 expect(fileLists(koh)).not.toContainEqual({ save_id: head, path });67 }68 koh.close();6970 for (const outside of [71 join(work, "joint-escape.txt"),72 join(root, "joint-nested.txt"),73 "/tmp/koh3-joint-absolute.txt",74 ]) {75 expect(existsSync(outside), outside).toBe(false);76 }77});