contentDisposition.ts
Raw
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 */
9export 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