UNPKG

content-disposition

Version:

Create and parse Content-Disposition header

389 lines 13.2 kB
"use strict"; /*! * content-disposition * Copyright(c) 2014-2017 Douglas Christopher Wilson * MIT Licensed */ Object.defineProperty(exports, "__esModule", { value: true }); exports.create = create; exports.parse = parse; exports.decodeExtended = decodeExtended; exports.format = format; exports.encodeExtended = encodeExtended; /** * Null object perf optimization. Faster than `Object.create(null)` and `{ __proto__: null }`. */ const NullObject = /* @__PURE__ */ (() => { const C = function () { }; C.prototype = Object.create(null); return C; })(); /** * Create an attachment Content-Disposition header. */ function create(filename, options) { const type = options?.type || 'attachment'; const parameters = createParameters(filename, options?.fallback); return format({ type, parameters }, { extended: false }); } const SP = 32; // " " const HTAB = 9; // "\t" const SEMI = 59; // ";" const EQ = 61; // "=" const DQUOTE = 34; // '"' const BSLASH = 92; // "\\" const ASTERISK = 42; // "*" /** * Parse Content-Disposition header string. */ function parse(header, options) { const len = header.length; const multipart = options?.multipart === true; const extended = options?.extended !== false; let index = skipOWS(header, 0, header.length); const typeStart = index; index = parseToken(header, index, len); const typeEnd = trailingOWS(header, typeStart, index); const type = header.slice(typeStart, typeEnd).toLowerCase(); const parameters = new NullObject(); parameter: while (index < len) { index = skipOWS(header, index + 1, len); // Skip over semicolon. const keyStart = index; while (index < len) { const char = header.charCodeAt(index); if (char === SEMI) continue parameter; if (char === EQ) { const keyEnd = trailingOWS(header, keyStart, index); const key = header.slice(keyStart, keyEnd).toLowerCase(); index = skipOWS(header, index + 1, len); if (index < len && header.charCodeAt(index) === DQUOTE) { index++; let value = ''; if (multipart) { while (index < len) { const code = header.charCodeAt(index++); if (code === DQUOTE) { index = parseToken(header, index, len); if (parameters[key] === undefined) parameters[key] = value; break; } if (code === /* % */ 37 && index + 1 < len) { const code2 = header.charCodeAt(index); const code3 = header.charCodeAt(index + 1); if (code2 === 50 /* "2" */ && code3 === 50 /* "2" */) { value += '"'; index += 2; continue; } if (code2 === 48 /* "0" */) { if (code3 === 100 /* "d" */ || code3 === 68 /* "D" */) { value += '\r'; index += 2; continue; } if (code3 === 97 /* "a" */ || code3 === 65 /* "A" */) { value += '\n'; index += 2; continue; } } } value += String.fromCharCode(code); } } else { while (index < len) { const code = header.charCodeAt(index++); if (code === DQUOTE) { index = parseToken(header, index, len); if (parameters[key] === undefined) parameters[key] = value; break; } if (code === BSLASH && index < len) { value += header[index++]; continue; } value += String.fromCharCode(code); } } continue parameter; } const valueStart = index; index = parseToken(header, index, len); const valueEnd = trailingOWS(header, valueStart, index); const value = header.slice(valueStart, valueEnd); if (extended && key.charCodeAt(key.length - 1) === ASTERISK) { const normalizedKey = key.slice(0, -1); const decoded = decodeExtended(value); if (decoded !== undefined) { parameters[normalizedKey] = decoded; continue parameter; } } if (parameters[key] === undefined) parameters[key] = value; continue parameter; } index++; } } return { type, parameters }; } /** * RegExp to match non attr-char, *after* encodeURIComponent (i.e. not including "%") */ const ENCODE_URL_ATTR_CHAR_REGEXP = /[\x00-\x20"'()*,/:;<=>?@[\\\]{}\x7f]/g; // eslint-disable-line no-control-regex /** * RegExp to match non-US-ASCII characters. */ const NON_ASCII_REGEXP = /[^\x20-\x7e]/g; /** * RegExp to match chars that must be quoted-pair in RFC 2616 */ const QUOTE_REGEXP = /[\\"]/g; /** * Match chars that can't be used in the filename for compatibility. */ const INVALID_FILENAME_REGEXP = /%[0-9A-Fa-f]{2}/; /** * RegExp for various RFC 2616 grammar * * parameter = token "=" ( token | quoted-string ) * token = 1*<any CHAR except CTLs or separators> * separators = "(" | ")" | "<" | ">" | "@" * | "," | ";" | ":" | "\" | <"> * | "/" | "[" | "]" | "?" | "=" * | "{" | "}" | SP | HT * quoted-string = ( <"> *(qdtext | quoted-pair ) <"> ) * qdtext = <any TEXT except <">> * quoted-pair = "\" CHAR * CHAR = <any US-ASCII character (octets 0 - 127)> * TEXT = <any OCTET except CTLs, but including LWS> * LWS = [CRLF] 1*( SP | HT ) * CRLF = CR LF * CR = <US-ASCII CR, carriage return (13)> * LF = <US-ASCII LF, linefeed (10)> * SP = <US-ASCII SP, space (32)> * HT = <US-ASCII HT, horizontal-tab (9)> * CTL = <any US-ASCII control character (octets 0 - 31) and DEL (127)> * OCTET = <any 8-bit sequence of data> */ const TEXT_REGEXP = /^[\x20-\x7e\x80-\xff]+$/; const ASCII_TEXT_REGEXP = /^[\x20-\x7e]+$/; const TOKEN_REGEXP = /^[!#$%&'*+.0-9A-Z^_`a-z|~-]+$/; /** * Create parameters object from filename and fallback. */ function createParameters(filename, fallback = true) { if (filename === undefined) return; if (typeof fallback === 'string') { if (!ASCII_TEXT_REGEXP.test(fallback)) { throw new TypeError('Fallback must be valid US-ASCII: ' + fallback); } if (fallback === filename) return { filename }; return { filename: fallback, 'filename*': encodeExtended(filename) }; } // Use `filename` when possible, otherwise use `filename*`. if (ASCII_TEXT_REGEXP.test(filename) && !INVALID_FILENAME_REGEXP.test(filename)) { return { filename }; } if (fallback === false) { return { 'filename*': encodeExtended(filename) }; } return { filename: getAscii(filename), 'filename*': encodeExtended(filename), }; } /** * Decode a RFC 8187 field value (gracefully). */ function decodeExtended(str) { const charsetEnd = str.indexOf("'"); if (charsetEnd <= 0) { return undefined; } const languageEnd = str.indexOf("'", charsetEnd + 1); if (languageEnd === -1) { return undefined; } const charset = str.slice(0, charsetEnd).toLowerCase(); const encoded = str.slice(languageEnd + 1); switch (charset) { case 'iso-8859-1': { return decodeHexEscapes(encoded); } case 'utf-8': case 'utf8': { return tryDecodeURIComponent(encoded); } } return undefined; } /** * Decode URI component but return `undefined` on error. */ function tryDecodeURIComponent(str) { try { return decodeURIComponent(str); } catch { return undefined; } } /** * Parse a token starting at the provided index. */ function parseToken(str, index, len) { while (index < len) { const char = str.charCodeAt(index); if (char === SEMI) break; index++; } return index; } /** * Skip RFC 2616 linear whitespace (space / tab). */ function skipOWS(str, index, len) { while (index < len) { const char = str.charCodeAt(index); if (char !== SP && char !== HTAB) break; index++; } return index; } /** * Skip RFC 2616 linear whitespace (space / tab) from the end of a string. */ function trailingOWS(str, start, end) { while (end > start) { const char = str.charCodeAt(end - 1); if (char !== SP && char !== HTAB) break; end--; } return end; } /** * Format object to Content-Disposition header. */ function format(obj, options) { const { type, parameters } = obj; const multipart = options?.multipart === true; const extended = options?.extended !== false; if (!type || !TOKEN_REGEXP.test(type)) { throw new TypeError('Invalid type: ' + type); } let result = type; if (parameters) { for (const param of Object.keys(parameters)) { const value = parameters[param]; if (!TOKEN_REGEXP.test(param)) { throw new TypeError('Invalid parameter name: ' + param); } if (multipart) { result += '; ' + param + '=' + qmultipart(value); continue; } if (TOKEN_REGEXP.test(value)) { result += '; ' + param + '=' + value; continue; } if (TEXT_REGEXP.test(value)) { result += '; ' + param + '=' + qstring(value); continue; } if (!extended) { throw new TypeError('Invalid parameter value: ' + value); } result += '; ' + param + '*=' + encodeExtended(value); } } return result; } /** * Get US-ASCII version of string. */ function getAscii(val) { // simple Unicode -> US-ASCII transformation return val.replace(NON_ASCII_REGEXP, '?'); } /** * Percent encode a single character. */ function pencode(char) { return '%' + char.charCodeAt(0).toString(16).toUpperCase(); } /** * Quote a string for HTTP. */ function qstring(str) { return '"' + str.replace(QUOTE_REGEXP, '\\$&') + '"'; } /** * Quote a string for multipart/form-data. * * @see https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#multipart-form-data */ function qmultipart(str) { return '"' + str.replace(/[\n\r"]/g, multipartEscape) + '"'; } /** * Escape a multipart/form-data string character. */ function multipartEscape(char) { if (char === '\n') return '%0A'; if (char === '\r') return '%0D'; return '%22'; } /** * Encode a Unicode string for HTTP (RFC 5987). */ function encodeExtended(str) { const encoded = encodeURIComponent(str).replace(ENCODE_URL_ATTR_CHAR_REGEXP, pencode); return "UTF-8''" + encoded; } /** * Check if a character is a hex digit [0-9A-Fa-f] */ function isHexDigit(char) { const code = char.charCodeAt(0); return ((code >= 48 && code <= 57) || // 0-9 (code >= 65 && code <= 70) || // A-F (code >= 97 && code <= 102) // a-f ); } /** * Decode hex escapes in a string (e.g., %20 -> space) */ function decodeHexEscapes(str) { const firstEscape = str.indexOf('%'); if (firstEscape === -1) return str; let result = str.slice(0, firstEscape); for (let idx = firstEscape; idx < str.length; idx++) { if (str[idx] === '%' && idx + 2 < str.length && isHexDigit(str[idx + 1]) && isHexDigit(str[idx + 2])) { result += String.fromCharCode(Number.parseInt(str[idx + 1] + str[idx + 2], 16)); idx += 2; } else { result += str[idx]; } } return result; } //# sourceMappingURL=index.js.map