/*!
|
* 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
|
}
|