/** * Target-profile aware filename sanitizer. * * Pure string in / string out. No filesystem access, no I/O. Use this * at the boundary where a user-supplied filename meets a storage * destination — local FS, ZIP archive, S3 key, URL path, SMB share — * to defuse the canonical 14-class footgun before it reaches that * destination. * * ## Threat model * * 1. Path injection — `..`, NUL bytes, `\` on POSIX, absolute paths. * 2. Windows reserved names — `CON`, `PRN`, `AUX`, `NUL`, * `COM1-9`, `LPT1-9`, with or without an extension. * 3. Windows reserved chars — `< > : " / \ | ? *` plus ASCII 0-31. * 4. Trailing `.` and ` ` on Windows / SMB. * 5. Unicode normalization drift — same display, different bytes → * "two files with the same name" sync ghosts. * 6. Bidi override spoofing — U+202E reversing `harmless.exe.txt` * into `harmless.txt.exe`. * 7. URL `+` ambiguity in S3 presigned URLs. * 8. ZIP general-purpose-flag bit 11 (UTF-8 filename) optional in * pre-2006 readers. * 9-14. Length caps, leading/trailing whitespace + controls, * `.DS_Store`-style hidden noise, etc. * * ## Always-on transforms (every profile) * * - NFC normalize (`String.prototype.normalize('NFC')`). * - Reject NUL — hard fail, not strip. Silent strip enables a * classic truncation bypass (`safe.txt\0.exe` → `safe.txt`). * - Strip bidi overrides (`U+202A..U+202E`, `U+2066..U+2069`). * - Trim leading/trailing whitespace and ASCII control chars. * * ## Non-goals (i18n boundary policy, ) * * - No transliteration. * - No locale-aware slugging. * - No script-specific segmentation. * * @module */ /** * One of seven storage destinations the sanitizer knows how to defang * for. Pick the most restrictive that covers your write site: * `'macos-smb'` is the safe default for "I don't know where these * files end up but they're going to a real filesystem somewhere." */ export type FilenameProfile = 'posix' | 'windows' | 'macos-smb' | 'zip' | 'url-path' | 's3-key' | 'opaque'; export interface SanitizeFilenameOptions { /** Target-destination profile. */ readonly profile: FilenameProfile; /** * Override the per-profile length cap. Useful when leaving headroom * for a collision suffix — e.g. `maxBytes: 240` on a `posix` write * site that wants 15 bytes of slack for `-1`, `-2`, … */ readonly maxBytes?: number; /** * Required when `profile === 'opaque'`. The opaque profile replaces * the entire input with `${opaqueId}.${ext}` (extension preserved * from the input when present). */ readonly opaqueId?: string; } /** * Sanitize a filename for a target-destination profile. Pure: same * input + options always returns the same output, no I/O. * * Returns the sanitized name. Throws {@link FilenameSanitizationError} * when the input cannot be made safe at all (NUL byte, empty after * normalization, missing `opaqueId` for the opaque profile, * `..` segment that would fall out of any reasonable target). */ export declare function sanitizeFilename(name: string, opts: SanitizeFilenameOptions): string;