功能说明
该示例解析 HTTP 响应中的 Content-Disposition 头,提取处置类型、文件名与全部参数。它同时支持普通的 filename 和 RFC 5987 的 filename*,后者用字符集与百分号编码传递非 ASCII 文件名。解析结果适合下载器、文件预览与对象存储代理使用。
关键代码
splitParameters 按分号切分,但会跟踪双引号状态,避免把引号内的分号误当分隔符。类型只接受 inline 与 attachment,其他值直接抛错。参数名统一转小写后存入 Map,引号值通过正则去掉反斜杠转义。resolveFilename 优先使用 filename*,按 charset’language’value 的格式提取并解码,解码失败时退回原始值。sanitize 只保留路径最后一段,阻止目录穿越。
使用注意
文件名来自不可信响应时,绝不能直接拼到本地路径。该函数只取 basename,仍建议再过滤控制字符与保留名。filename* 的字符集目前按 UTF-8 处理,其他编码可能解码失败并退回原文。参数重复时后出现的值覆盖先前的值。该解析器不处理多段 Content-Disposition,也不解析 multipart 正文。
验证方式
断言普通 filename 解析为 report.pdf,filename* 的百分号编码还原为中文文件名。测试包含分号的引号文件名不会被截断。传入 ../../etc/passwd 时结果只剩 passwd。再验证缺少 filename 返回 null、未知处置类型抛错、空字符串抛 TypeError。
代码示例
export function parseContentDisposition(header) {
if (typeof header !== "string" || header.trim() === "") {
throw new TypeError("header must be a non-empty string");
}
const segments = splitParameters(header);
const type = segments.shift().trim().toLowerCase();
if (type !== "inline" && type !== "attachment") {
throw new Error(`unsupported disposition type "${type}"`);
}
const parameters = new Map();
for (const segment of segments) {
const equals = segment.indexOf("=");
if (equals === -1) continue;
const name = segment.slice(0, equals).trim().toLowerCase();
if (name === "") continue;
parameters.set(name, unquote(segment.slice(equals + 1).trim()));
}
return { type, filename: resolveFilename(parameters), parameters };
}
function splitParameters(header) {
const segments = [];
let current = "";
let quoted = false;
for (const character of header) {
if (character === '"') quoted = !quoted;
if (character === ";" && !quoted) {
segments.push(current);
current = "";
continue;
}
current += character;
}
segments.push(current);
return segments;
}
function unquote(value) {
if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
return value.slice(1, -1).replace(/\\(.)/g, "$1");
}
return value;
}
function resolveFilename(parameters) {
const extended = parameters.get("filename*");
if (extended !== undefined) {
const match = /^([\w-]+)'([\w-]*)'(.+)$/.exec(extended);
if (match !== null) {
try {
return sanitize(decodeURIComponent(match[3]));
} catch {
return sanitize(extended);
}
}
}
const plain = parameters.get("filename");
return plain === undefined ? null : sanitize(plain);
}
function sanitize(filename) {
return filename.split(/[\\/]/).pop() || null;
}
console.log(parseContentDisposition('attachment; filename="report.pdf"'));
console.log(parseContentDisposition("attachment; filename*=UTF-8''%E4%B8%AD%E6%96%87.pdf"));
console.log(parseContentDisposition('inline; filename="../../etc/passwd"'));