/** * Build a safe `Content-Disposition` header value. * * The display filename is restricted to a printable ASCII subset so it can * never break out of the quoted-string form (no `"`, `\`, CR, LF, NUL), * and a UTF-8 `filename*` parameter is added per RFC 5987 so unicode names * still come through to the client when possible. */ export function contentDisposition( type: "inline" | "attachment", name: string, ): string { // Drop path components and control chars; collapse anything not // printable-ASCII-and-safe-in-a-quoted-string into "_". const base = name.split(/[/\\]/).pop() ?? ""; // biome-ignore lint/suspicious/noControlCharactersInRegex: control characters are exactly what we want to strip from HTTP header values const stripped = base.replace(/[\x00-\x1f\x7f"\\]/g, "_"); const ascii = stripped.length > 0 ? stripped : "file"; // biome-ignore lint/suspicious/noControlCharactersInRegex: control characters are exactly what we want to strip from HTTP header values const utf8 = encodeURIComponent(base.replace(/[\x00-\x1f\x7f]/g, "_")) // RFC 5987 disallows `'` in filename* value-chars (it is the // separator between charset, language, and value); encodeURIComponent // does not escape it, so do it explicitly. .replace(/'/g, "%27"); return `${type}; filename="${ascii}"; filename*=UTF-8''${utf8}`; }