// biome-ignore lint/suspicious/noControlCharactersInRegex: const ENCODE_URL_ATTR_CHAR_REGEXP = /[\x00-\x20"'()*,/:;<=>?@[\\\]{}\x7f]/g; const HEX_ESCAPE_REGEXP = /%[0-9A-Fa-f]{2}/; const HEX_ESCAPE_REPLACE_REGEXP = /%([0-9A-Fa-f]{2})/g; const NON_LATIN1_REGEXP = /[^\x20-\x7e\xa0-\xff]/g; // biome-ignore lint/suspicious/noControlCharactersInRegex: const QESC_REGEXP = /\\([\u0000-\u007f])/g; const QUOTE_REGEXP = /([\\"])/g; const PARAM_REGEXP = // biome-ignore lint/suspicious/noControlCharactersInRegex: /;[\x09\x20]*([!#$%&'*+.0-9A-Z^_`a-z|~-]+)[\x09\x20]*=[\x09\x20]*("(?:[\x20!\x23-\x5b\x5d-\x7e\x80-\xff]|\\[\x20-\x7e])*"|[!#$%&'*+.0-9A-Z^_`a-z|~-]+)[\x09\x20]*/g; const TEXT_REGEXP = /^[\x20-\x7e\x80-\xff]+$/; const TOKEN_REGEXP = /^[!#$%&'*+.0-9A-Z^_`a-z|~-]+$/; const EXT_VALUE_REGEXP = /^([A-Za-z0-9!#$%&+\-^_`{}~]+)'(?:[A-Za-z]{2,3}(?:-[A-Za-z]{3}){0,3}|[A-Za-z]{4,8}|)'((?:%[0-9A-Fa-f]{2}|[A-Za-z0-9!#$&+.^_`|~-])+)$/; // biome-ignore lint/suspicious/noControlCharactersInRegex: const DISPOSITION_TYPE_REGEXP = /^([!#$%&'*+.0-9A-Z^_`a-z|~-]+)[\x09\x20]*(?:$|;)/; const getlatin1 = (val) => { // simple Unicode -> ISO-8859-1 transformation return String(val).replace(NON_LATIN1_REGEXP, '?'); }; export class ContentDisposition { constructor(type, parameters) { this.type = type; this.parameters = parameters; } } const qstring = (val) => `"${String(val).replace(QUOTE_REGEXP, '\\$1')}"`; const pencode = (char) => `%${String(char).charCodeAt(0).toString(16).toUpperCase()}`; function ustring(val) { const str = String(val); // percent encode as UTF-8 const encoded = encodeURIComponent(str).replace(ENCODE_URL_ATTR_CHAR_REGEXP, pencode); return `UTF-8''${encoded}`; } const basename = (str) => str.slice(str.lastIndexOf('/') + 1); function format({ parameters, type }) { if (!type || typeof type !== 'string' || !TOKEN_REGEXP.test(type)) { throw new TypeError('invalid type'); } // start with normalized type let string = String(type).toLowerCase(); // append parameters if (parameters && typeof parameters === 'object') { const params = Object.keys(parameters).sort(); for (const param of params) { const val = param.slice(-1) === '*' ? ustring(parameters[param]) : qstring(parameters[param]); string += `; ${param}=${val}`; } } return string; } function createParams(filename, fallback) { if (filename === undefined) return {}; const params = {}; // fallback defaults to true if (!fallback) fallback = true; if (typeof fallback === 'string' && NON_LATIN1_REGEXP.test(fallback)) { throw new TypeError('fallback must be ISO-8859-1 string'); } // restrict to file base name const name = basename(filename); // determine if name is suitable for quoted string const isQuotedString = TEXT_REGEXP.test(name); // generate fallback name const fallbackName = typeof fallback !== 'string' ? fallback && getlatin1(name) : basename(fallback); const hasFallback = typeof fallbackName === 'string' && fallbackName !== name; // set extended filename parameter if (hasFallback || !isQuotedString || HEX_ESCAPE_REGEXP.test(name)) { params['filename*'] = name; } // set filename parameter if (isQuotedString || hasFallback) { params.filename = hasFallback ? fallbackName : name; } return params; } const pdecode = (_str, hex) => String.fromCharCode(Number.parseInt(hex, 16)); /** * Create an attachment Content-Disposition header. * * @param filename file name * @param options */ export function contentDisposition(filename, options = {}) { // format into string return format(new ContentDisposition(options.type || 'attachment', createParams(filename, options.fallback))); } function decodefield(str) { const match = EXT_VALUE_REGEXP.exec(str); if (!match) throw new TypeError('invalid extended field value'); const charset = match[1].toLowerCase(); const encoded = match[2]; let value; switch (charset) { case 'iso-8859-1': value = getlatin1(encoded.replace(HEX_ESCAPE_REPLACE_REGEXP, pdecode)); break; case 'utf-8': try { value = decodeURIComponent(encoded); } catch { throw new TypeError('invalid encoded utf-8'); } break; default: throw new TypeError('unsupported charset in extended field'); } return value; } /** * Parse Content-Disposition header string. * @param header string */ export function parse(header) { let match = DISPOSITION_TYPE_REGEXP.exec(header); if (!match) throw new TypeError('invalid type format'); // normalize type let index = match[0].length; const type = match[1].toLowerCase(); let key; const names = []; const params = {}; let value; // calculate index to start at index = PARAM_REGEXP.lastIndex = match[0].slice(-1) === ';' ? index - 1 : index; // match parameters while ((match = PARAM_REGEXP.exec(header))) { if (match.index !== index) throw new TypeError('invalid parameter format'); index += match[0].length; key = match[1].toLowerCase(); value = match[2]; if (names.indexOf(key) !== -1) { throw new TypeError('invalid duplicate parameter'); } names.push(key); if (key.indexOf('*') + 1 === key.length) { // decode extended value key = key.slice(0, -1); value = decodefield(value); // overwrite existing value params[key] = value; continue; } if (typeof params[key] === 'string') continue; if (value[0] === '"') { value = value.slice(1, value.length - 1).replace(QESC_REGEXP, '$1'); } params[key] = value; } if (index !== -1 && index !== header.length) { throw new TypeError('invalid parameter format'); } return new ContentDisposition(type, params); } //# sourceMappingURL=index.js.map