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