/*! * media-typer * Copyright(c) 2014 Douglas Christopher Wilson * MIT Licensed */ /** * RegExp to match *( ";" parameter ) in RFC 2616 sec 3.7 * * parameter = token "=" ( token | quoted-string ) * token = 1* * separators = "(" | ")" | "<" | ">" | "@" * | "," | ";" | ":" | "\" | <"> * | "/" | "[" | "]" | "?" | "=" * | "{" | "}" | SP | HT * quoted-string = ( <"> *(qdtext | quoted-pair ) <"> ) * qdtext = > * quoted-pair = "\" CHAR * CHAR = * TEXT = * LWS = [CRLF] 1*( SP | HT ) * CRLF = CR LF * CR = * LF = * SP = * SHT = * CTL = * OCTET = */ var paramRegExp = /; *([!#$%&'\*\+\-\.0-9A-Z\^_`a-z\|~]+) *= *("(?:[ !\u0023-\u005b\u005d-\u007e\u0080-\u00ff]|\\[\u0020-\u007e])*"|[!#$%&'\*\+\-\.0-9A-Z\^_`a-z\|~]+) */g; var textRegExp = /^[\u0020-\u007e\u0080-\u00ff]+$/ var tokenRegExp = /^[!#$%&'\*\+\-\.0-9A-Z\^_`a-z\|~]+$/ /** * RegExp to match quoted-pair in RFC 2616 * * quoted-pair = "\" CHAR * CHAR = */ var qescRegExp = /\\([\u0000-\u007f])/g; /** * RegExp to match chars that must be quoted-pair in RFC 2616 */ var quoteRegExp = /([\\"])/g; /** * RegExp to match type in RFC 6838 * * type-name = restricted-name * subtype-name = restricted-name * restricted-name = restricted-name-first *126restricted-name-chars * restricted-name-first = ALPHA / DIGIT * restricted-name-chars = ALPHA / DIGIT / "!" / "#" / * "$" / "&" / "-" / "^" / "_" * restricted-name-chars =/ "." ; Characters before first dot always * ; specify a facet name * restricted-name-chars =/ "+" ; Characters after last plus always * ; specify a structured syntax suffix * ALPHA = %x41-5A / %x61-7A ; A-Z / a-z * DIGIT = %x30-39 ; 0-9 */ var subtypeNameRegExp = /^[A-Za-z0-9][A-Za-z0-9!#$&^_.-]{0,126}$/ var typeNameRegExp = /^[A-Za-z0-9][A-Za-z0-9!#$&^_-]{0,126}$/ var typeRegExp = /^ *([A-Za-z0-9][A-Za-z0-9!#$&^_-]{0,126})\/([A-Za-z0-9][A-Za-z0-9!#$&^_.+-]{0,126}) *$/; /** * Module exports. */ exports.format = format exports.parse = parse /** * Format object to media type. * * @param {object} obj * @return {string} * @api public */ function format(obj) { if (!obj || typeof obj !== 'object') { throw new TypeError('argument obj is required') } var parameters = obj.parameters var subtype = obj.subtype var suffix = obj.suffix var type = obj.type if (!type || !typeNameRegExp.test(type)) { throw new TypeError('invalid type') } if (!subtype || !subtypeNameRegExp.test(subtype)) { throw new TypeError('invalid subtype') } // format as type/subtype var string = type + '/' + subtype // append +suffix if (suffix) { if (!typeNameRegExp.test(suffix)) { throw new TypeError('invalid suffix') } string += '+' + suffix } // append parameters if (parameters && typeof parameters === 'object') { var param var params = Object.keys(parameters).sort() for (var i = 0; i < params.length; i++) { param = params[i] if (!tokenRegExp.test(param)) { throw new TypeError('invalid parameter name') } string += '; ' + param + '=' + qstring(parameters[param]) } } return string } /** * Parse media type to object. * * @param {string|object} string * @return {Object} * @api public */ function parse(string) { if (!string) { throw new TypeError('argument string is required') } // support req/res-like objects as argument if (typeof string === 'object') { string = getcontenttype(string) } if (typeof string !== 'string') { throw new TypeError('argument string is required to be a string') } var index = string.indexOf(';') var type = index !== -1 ? string.substr(0, index) : string var key var match var obj = splitType(type) var params = {} var value paramRegExp.lastIndex = index while (match = paramRegExp.exec(string)) { 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 .substr(1, value.length - 2) .replace(qescRegExp, '$1') } params[key] = value } if (index !== -1 && index !== string.length) { throw new TypeError('invalid parameter format') } obj.parameters = params return obj } /** * Get content-type from req/res objects. * * @param {object} * @return {Object} * @api private */ function getcontenttype(obj) { if (typeof obj.getHeader === 'function') { // res-like return obj.getHeader('content-type') } if (typeof obj.headers === 'object') { // req-like return obj.headers && obj.headers['content-type'] } } /** * Quote a string if necessary. * * @param {string} val * @return {string} * @api private */ function qstring(val) { var str = String(val) // no need to quote tokens if (tokenRegExp.test(str)) { return str } if (str.length > 0 && !textRegExp.test(str)) { throw new TypeError('invalid parameter value') } return '"' + str.replace(quoteRegExp, '\\$1') + '"' } /** * Simply "type/subtype+siffx" into parts. * * @param {string} string * @return {Object} * @api private */ function splitType(string) { var match = typeRegExp.exec(string.toLowerCase()) if (!match) { throw new TypeError('invalid media type') } var type = match[1] var subtype = match[2] var suffix // suffix after last + var index = subtype.lastIndexOf('+') if (index !== -1) { suffix = subtype.substr(index + 1) subtype = subtype.substr(0, index) } var obj = { type: type, subtype: subtype, suffix: suffix } return obj }