You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
222 lines
4.7 KiB
222 lines
4.7 KiB
/*! |
|
* content-type |
|
* Copyright(c) 2015 Douglas Christopher Wilson |
|
* MIT Licensed |
|
*/ |
|
|
|
'use strict' |
|
|
|
/** |
|
* 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 ) |
|
*/ |
|
var PARAM_REGEXP = /; *([!#$%&'*+.^_`|~0-9A-Za-z-]+) *= *("(?:[\u000b\u0020\u0021\u0023-\u005b\u005d-\u007e\u0080-\u00ff]|\\[\u000b\u0020-\u00ff])*"|[!#$%&'*+.^_`|~0-9A-Za-z-]+) */g |
|
var TEXT_REGEXP = /^[\u000b\u0020-\u007e\u0080-\u00ff]+$/ |
|
var TOKEN_REGEXP = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/ |
|
|
|
/** |
|
* RegExp to match quoted-pair in RFC 7230 sec 3.2.6 |
|
* |
|
* quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text ) |
|
* obs-text = %x80-FF |
|
*/ |
|
var QESC_REGEXP = /\\([\u000b\u0020-\u00ff])/g |
|
|
|
/** |
|
* RegExp to match chars that must be quoted-pair in RFC 7230 sec 3.2.6 |
|
*/ |
|
var QUOTE_REGEXP = /([\\"])/g |
|
|
|
/** |
|
* RegExp to match type in RFC 7231 sec 3.1.1.1 |
|
* |
|
* media-type = type "/" subtype |
|
* type = token |
|
* subtype = token |
|
*/ |
|
var TYPE_REGEXP = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+\/[!#$%&'*+.^_`|~0-9A-Za-z-]+$/ |
|
|
|
/** |
|
* Module exports. |
|
* @public |
|
*/ |
|
|
|
exports.format = format |
|
exports.parse = parse |
|
|
|
/** |
|
* Format object to media type. |
|
* |
|
* @param {object} obj |
|
* @return {string} |
|
* @public |
|
*/ |
|
|
|
function format (obj) { |
|
if (!obj || typeof obj !== 'object') { |
|
throw new TypeError('argument obj is required') |
|
} |
|
|
|
var parameters = obj.parameters |
|
var type = obj.type |
|
|
|
if (!type || !TYPE_REGEXP.test(type)) { |
|
throw new TypeError('invalid type') |
|
} |
|
|
|
var string = type |
|
|
|
// 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 (!TOKEN_REGEXP.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} |
|
* @public |
|
*/ |
|
|
|
function parse (string) { |
|
if (!string) { |
|
throw new TypeError('argument string is required') |
|
} |
|
|
|
// support req/res-like objects as argument |
|
var header = typeof string === 'object' |
|
? getcontenttype(string) |
|
: string |
|
|
|
if (typeof header !== 'string') { |
|
throw new TypeError('argument string is required to be a string') |
|
} |
|
|
|
var index = header.indexOf(';') |
|
var type = index !== -1 |
|
? header.substr(0, index).trim() |
|
: header.trim() |
|
|
|
if (!TYPE_REGEXP.test(type)) { |
|
throw new TypeError('invalid media type') |
|
} |
|
|
|
var obj = new ContentType(type.toLowerCase()) |
|
|
|
// parse parameters |
|
if (index !== -1) { |
|
var key |
|
var match |
|
var value |
|
|
|
PARAM_REGEXP.lastIndex = index |
|
|
|
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 (value[0] === '"') { |
|
// remove quotes and escapes |
|
value = value |
|
.substr(1, value.length - 2) |
|
.replace(QESC_REGEXP, '$1') |
|
} |
|
|
|
obj.parameters[key] = value |
|
} |
|
|
|
if (index !== header.length) { |
|
throw new TypeError('invalid parameter format') |
|
} |
|
} |
|
|
|
return obj |
|
} |
|
|
|
/** |
|
* Get content-type from req/res objects. |
|
* |
|
* @param {object} |
|
* @return {Object} |
|
* @private |
|
*/ |
|
|
|
function getcontenttype (obj) { |
|
var header |
|
|
|
if (typeof obj.getHeader === 'function') { |
|
// res-like |
|
header = obj.getHeader('content-type') |
|
} else if (typeof obj.headers === 'object') { |
|
// req-like |
|
header = obj.headers && obj.headers['content-type'] |
|
} |
|
|
|
if (typeof header !== 'string') { |
|
throw new TypeError('content-type header is missing from object') |
|
} |
|
|
|
return header |
|
} |
|
|
|
/** |
|
* Quote a string if necessary. |
|
* |
|
* @param {string} val |
|
* @return {string} |
|
* @private |
|
*/ |
|
|
|
function qstring (val) { |
|
var str = String(val) |
|
|
|
// no need to quote tokens |
|
if (TOKEN_REGEXP.test(str)) { |
|
return str |
|
} |
|
|
|
if (str.length > 0 && !TEXT_REGEXP.test(str)) { |
|
throw new TypeError('invalid parameter value') |
|
} |
|
|
|
return '"' + str.replace(QUOTE_REGEXP, '\\$1') + '"' |
|
} |
|
|
|
/** |
|
* Class to represent a content type. |
|
* @private |
|
*/ |
|
function ContentType (type) { |
|
this.parameters = Object.create(null) |
|
this.type = type |
|
}
|
|
|