UNPKG

fast-content-type-parse

Version:

Parse HTTP Content-Type header according to RFC 7231

170 lines (134 loc) 3.9 kB
'use strict' const NullObject = function NullObject () { } NullObject.prototype = Object.create(null) /** * RegExp to match *( ";" parameter ) in RFC 7231 sec 3.1.1.1 * * parameter = token "=" ( token / quoted-string ) * token = 1*tchar * tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" * / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" * / DIGIT / ALPHA * ; any VCHAR, except delimiters * quoted-string = DQUOTE *( qdtext / quoted-pair ) DQUOTE * qdtext = HTAB / SP / %x21 / %x23-5B / %x5D-7E / obs-text * obs-text = %x80-FF * quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text ) */ const paramRE = /; *([!#$%&'*+.^\w`|~-]+)=("(?:[\v\u0020\u0021\u0023-\u005b\u005d-\u007e\u0080-\u00ff]|\\[\v\u0020-\u00ff])*"|[!#$%&'*+.^\w`|~-]+) */gu /** * RegExp to match quoted-pair in RFC 7230 sec 3.2.6 * * quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text ) * obs-text = %x80-FF */ const quotedPairRE = /\\([\v\u0020-\u00ff])/gu /** * RegExp to match type in RFC 7231 sec 3.1.1.1 * * media-type = type "/" subtype * type = token * subtype = token */ const mediaTypeRE = /^[!#$%&'*+.^\w|~-]+\/[!#$%&'*+.^\w|~-]+$/u // default ContentType to prevent repeated object creation const defaultContentType = { type: '', parameters: new NullObject() } Object.freeze(defaultContentType.parameters) Object.freeze(defaultContentType) /** * Parse media type to object. * * @param {string|object} header * @return {Object} * @public */ function parse (header) { if (typeof header !== 'string') { throw new TypeError('argument header is required and must be a string') } let index = header.indexOf(';') const type = index !== -1 ? header.slice(0, index).trim() : header.trim() if (mediaTypeRE.test(type) === false) { throw new TypeError('invalid media type') } const result = { type: type.toLowerCase(), parameters: new NullObject() } // parse parameters if (index === -1) { return result } let key let match let value paramRE.lastIndex = index while ((match = paramRE.exec(header))) { if (match.index !== index) { throw new TypeError('invalid parameter format') } index += match[0].length key = match[1].toLowerCase() value = match[2] if (value[0] === '"') { // remove quotes and escapes value = value .slice(1, value.length - 1) quotedPairRE.test(value) && (value = value.replace(quotedPairRE, '$1')) } result.parameters[key] = value } if (index !== header.length) { throw new TypeError('invalid parameter format') } return result } function safeParse (header) { if (typeof header !== 'string') { return defaultContentType } let index = header.indexOf(';') const type = index !== -1 ? header.slice(0, index).trim() : header.trim() if (mediaTypeRE.test(type) === false) { return defaultContentType } const result = { type: type.toLowerCase(), parameters: new NullObject() } // parse parameters if (index === -1) { return result } let key let match let value paramRE.lastIndex = index while ((match = paramRE.exec(header))) { if (match.index !== index) { return defaultContentType } index += match[0].length key = match[1].toLowerCase() value = match[2] if (value[0] === '"') { // remove quotes and escapes value = value .slice(1, value.length - 1) quotedPairRE.test(value) && (value = value.replace(quotedPairRE, '$1')) } result.parameters[key] = value } if (index !== header.length) { return defaultContentType } return result } module.exports.default = { parse, safeParse } module.exports.parse = parse module.exports.safeParse = safeParse module.exports.defaultContentType = defaultContentType