// File names built from server-supplied metadata. // // quak does not trust the server. A file's title and a collection's name are // decrypted from data the server hands us, and a hostile server (or a // compromised account) can set them to anything. quak uses them to name files // on disk: `quak get` without `--out`, `downloadFile` without `outPath`, the // backup's symlink and collection directories, and the extension of every file // in the originals cache. Each of those goes through `sanitizeFileName` or // `safeExtension`, so a title can only ever name one file inside the directory // the caller chose. // // A path the user supplies (`--out`, `outPath`) is never sanitized: the caller // is trusted, the server is not. import { describe, expect, it } from "vitest"; import { safeExtension, sanitizeFileName } from "../../src/filename.js"; const FALLBACK = "file-42"; describe("sanitizeFileName", () => { it("passes a normal title through unchanged", () => { expect(sanitizeFileName("IMG_0001.HEIC", FALLBACK)).toBe( "IMG_0001.HEIC", ); expect(sanitizeFileName("Holiday 2024 (1).jpg", FALLBACK)).toBe( "Holiday 2024 (1).jpg", ); expect(sanitizeFileName("café.jpg", FALLBACK)).toBe("café.jpg"); }); it("cannot climb out of the directory with ../", () => { // Without sanitizing, this would overwrite the user's SSH keys. expect(sanitizeFileName("../../.ssh/authorized_keys", FALLBACK)).toBe( "__.._.ssh_authorized_keys", ); expect(sanitizeFileName("..", FALLBACK)).toBe("_"); expect(sanitizeFileName("..\\..\\x", FALLBACK)).toBe("__.._x"); }); it("cannot name an absolute path", () => { expect(sanitizeFileName("/etc/passwd", FALLBACK)).toBe("_etc_passwd"); expect(sanitizeFileName("C:\\Windows\\x.dll", FALLBACK)).toBe( "C__Windows_x.dll", ); }); it("replaces embedded separators, so the name stays one file", () => { expect(sanitizeFileName("a/b\\c.jpg", FALLBACK)).toBe("a_b_c.jpg"); }); it("replaces NUL and other control characters", () => { // A NUL truncates the path in C code and makes Node's fs throw. expect(sanitizeFileName("evil\0.jpg", FALLBACK)).toBe("evil_.jpg"); expect(sanitizeFileName("line\nbreak\x7f.jpg", FALLBACK)).toBe( "line_break_.jpg", ); }); it("does not produce a hidden file", () => { expect(sanitizeFileName(".bashrc", FALLBACK)).toBe("_bashrc"); }); it("does not produce a Windows device name", () => { expect(sanitizeFileName("CON", FALLBACK)).toBe("_CON"); expect(sanitizeFileName("nul.txt", FALLBACK)).toBe("_nul.txt"); expect(sanitizeFileName("LPT1", FALLBACK)).toBe("_LPT1"); // Only the exact names are reserved. expect(sanitizeFileName("console.jpg", FALLBACK)).toBe("console.jpg"); }); it("falls back to the given name for an empty title", () => { expect(sanitizeFileName("", FALLBACK)).toBe(FALLBACK); }); }); describe("safeExtension", () => { it("keeps a normal extension", () => { expect(safeExtension("IMG_0001.HEIC")).toBe(".HEIC"); expect(safeExtension("clip.mp4")).toBe(".mp4"); }); it("uses .bin when there is no extension", () => { expect(safeExtension("")).toBe(".bin"); expect(safeExtension("README")).toBe(".bin"); }); it("uses .bin when the extension holds anything but letters and digits", () => { expect(safeExtension("x.j\\..\\pg")).toBe(".bin"); expect(safeExtension("x.jp g")).toBe(".bin"); expect(safeExtension("x.jpg\0")).toBe(".bin"); }); });