check / check (push) Successful in 37s
A file title or album name decrypted from server data could name a path outside the chosen directory (`../../.ssh/authorized_keys`). One module, src/filename.ts, now makes such names safe for `quak get`/`get-thumb` without `--out`, downloadFile/downloadThumbnail without outPath, and the backup and metadata backup trees. Originals-cache extensions are limited to letters and digits. A user-supplied path is still used as is. decryptFile reads a missing or non-string title as "" and rejects metadata that is not a JSON object. Model: opus-5-5
38 lines
1.7 KiB
TypeScript
38 lines
1.7 KiB
TypeScript
// File names built from server-supplied metadata.
|
|
//
|
|
// A file's title and a collection's name are decrypted from data the server
|
|
// hands us, and quak does not trust the server. Any name taken from them and
|
|
// used on disk goes through here, so it can only ever name one file inside the
|
|
// directory the caller chose: never a path, never `..`, never hidden, never a
|
|
// Windows device name.
|
|
//
|
|
// A path the user typed (`--out`, `outPath`) is not passed through here: the
|
|
// caller is trusted, the server is not.
|
|
|
|
import { extname } from "node:path";
|
|
|
|
// Path separators, characters Windows forbids in file names, and control
|
|
// characters (NUL included).
|
|
// eslint-disable-next-line no-control-regex
|
|
const UNSAFE_CHARACTERS = /[/\\:*?"<>|\x00-\x1f\x7f]/g;
|
|
|
|
// Names Windows reserves for devices, with or without an extension.
|
|
const RESERVED_DEVICE_NAME = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(\.|$)/i;
|
|
|
|
// `name` made safe to use as a single file name. Each unsafe character becomes
|
|
// `_`, a leading run of dots becomes one `_`, and a device name gets a leading
|
|
// `_`. A name with none of these comes back unchanged. An empty name becomes
|
|
// `fallback`, which the caller derives from the record's ID.
|
|
export const sanitizeFileName = (name: string, fallback: string): string => {
|
|
if (name === "") return fallback;
|
|
const cleaned = name.replace(UNSAFE_CHARACTERS, "_").replace(/^\.+/, "_");
|
|
return RESERVED_DEVICE_NAME.test(cleaned) ? `_${cleaned}` : cleaned;
|
|
};
|
|
|
|
// The extension of `title` (".jpg"), or ".bin" when it has none or it holds
|
|
// anything but letters and digits.
|
|
export const safeExtension = (title: string): string => {
|
|
const ext = extname(title);
|
|
return /^\.[A-Za-z0-9]+$/.test(ext) ? ext : ".bin";
|
|
};
|