[6a3a178] | 1 | /*!
|
---|
| 2 | * content-type
|
---|
| 3 | * Copyright(c) 2015 Douglas Christopher Wilson
|
---|
| 4 | * MIT Licensed
|
---|
| 5 | */
|
---|
| 6 |
|
---|
| 7 | 'use strict'
|
---|
| 8 |
|
---|
| 9 | /**
|
---|
| 10 | * RegExp to match *( ";" parameter ) in RFC 7231 sec 3.1.1.1
|
---|
| 11 | *
|
---|
| 12 | * parameter = token "=" ( token / quoted-string )
|
---|
| 13 | * token = 1*tchar
|
---|
| 14 | * tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*"
|
---|
| 15 | * / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~"
|
---|
| 16 | * / DIGIT / ALPHA
|
---|
| 17 | * ; any VCHAR, except delimiters
|
---|
| 18 | * quoted-string = DQUOTE *( qdtext / quoted-pair ) DQUOTE
|
---|
| 19 | * qdtext = HTAB / SP / %x21 / %x23-5B / %x5D-7E / obs-text
|
---|
| 20 | * obs-text = %x80-FF
|
---|
| 21 | * quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text )
|
---|
| 22 | */
|
---|
| 23 | var PARAM_REGEXP = /; *([!#$%&'*+.^_`|~0-9A-Za-z-]+) *= *("(?:[\u000b\u0020\u0021\u0023-\u005b\u005d-\u007e\u0080-\u00ff]|\\[\u000b\u0020-\u00ff])*"|[!#$%&'*+.^_`|~0-9A-Za-z-]+) */g
|
---|
| 24 | var TEXT_REGEXP = /^[\u000b\u0020-\u007e\u0080-\u00ff]+$/
|
---|
| 25 | var TOKEN_REGEXP = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/
|
---|
| 26 |
|
---|
| 27 | /**
|
---|
| 28 | * RegExp to match quoted-pair in RFC 7230 sec 3.2.6
|
---|
| 29 | *
|
---|
| 30 | * quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text )
|
---|
| 31 | * obs-text = %x80-FF
|
---|
| 32 | */
|
---|
| 33 | var QESC_REGEXP = /\\([\u000b\u0020-\u00ff])/g
|
---|
| 34 |
|
---|
| 35 | /**
|
---|
| 36 | * RegExp to match chars that must be quoted-pair in RFC 7230 sec 3.2.6
|
---|
| 37 | */
|
---|
| 38 | var QUOTE_REGEXP = /([\\"])/g
|
---|
| 39 |
|
---|
| 40 | /**
|
---|
| 41 | * RegExp to match type in RFC 7231 sec 3.1.1.1
|
---|
| 42 | *
|
---|
| 43 | * media-type = type "/" subtype
|
---|
| 44 | * type = token
|
---|
| 45 | * subtype = token
|
---|
| 46 | */
|
---|
| 47 | var TYPE_REGEXP = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+\/[!#$%&'*+.^_`|~0-9A-Za-z-]+$/
|
---|
| 48 |
|
---|
| 49 | /**
|
---|
| 50 | * Module exports.
|
---|
| 51 | * @public
|
---|
| 52 | */
|
---|
| 53 |
|
---|
| 54 | exports.format = format
|
---|
| 55 | exports.parse = parse
|
---|
| 56 |
|
---|
| 57 | /**
|
---|
| 58 | * Format object to media type.
|
---|
| 59 | *
|
---|
| 60 | * @param {object} obj
|
---|
| 61 | * @return {string}
|
---|
| 62 | * @public
|
---|
| 63 | */
|
---|
| 64 |
|
---|
| 65 | function format (obj) {
|
---|
| 66 | if (!obj || typeof obj !== 'object') {
|
---|
| 67 | throw new TypeError('argument obj is required')
|
---|
| 68 | }
|
---|
| 69 |
|
---|
| 70 | var parameters = obj.parameters
|
---|
| 71 | var type = obj.type
|
---|
| 72 |
|
---|
| 73 | if (!type || !TYPE_REGEXP.test(type)) {
|
---|
| 74 | throw new TypeError('invalid type')
|
---|
| 75 | }
|
---|
| 76 |
|
---|
| 77 | var string = type
|
---|
| 78 |
|
---|
| 79 | // append parameters
|
---|
| 80 | if (parameters && typeof parameters === 'object') {
|
---|
| 81 | var param
|
---|
| 82 | var params = Object.keys(parameters).sort()
|
---|
| 83 |
|
---|
| 84 | for (var i = 0; i < params.length; i++) {
|
---|
| 85 | param = params[i]
|
---|
| 86 |
|
---|
| 87 | if (!TOKEN_REGEXP.test(param)) {
|
---|
| 88 | throw new TypeError('invalid parameter name')
|
---|
| 89 | }
|
---|
| 90 |
|
---|
| 91 | string += '; ' + param + '=' + qstring(parameters[param])
|
---|
| 92 | }
|
---|
| 93 | }
|
---|
| 94 |
|
---|
| 95 | return string
|
---|
| 96 | }
|
---|
| 97 |
|
---|
| 98 | /**
|
---|
| 99 | * Parse media type to object.
|
---|
| 100 | *
|
---|
| 101 | * @param {string|object} string
|
---|
| 102 | * @return {Object}
|
---|
| 103 | * @public
|
---|
| 104 | */
|
---|
| 105 |
|
---|
| 106 | function parse (string) {
|
---|
| 107 | if (!string) {
|
---|
| 108 | throw new TypeError('argument string is required')
|
---|
| 109 | }
|
---|
| 110 |
|
---|
| 111 | // support req/res-like objects as argument
|
---|
| 112 | var header = typeof string === 'object'
|
---|
| 113 | ? getcontenttype(string)
|
---|
| 114 | : string
|
---|
| 115 |
|
---|
| 116 | if (typeof header !== 'string') {
|
---|
| 117 | throw new TypeError('argument string is required to be a string')
|
---|
| 118 | }
|
---|
| 119 |
|
---|
| 120 | var index = header.indexOf(';')
|
---|
| 121 | var type = index !== -1
|
---|
| 122 | ? header.substr(0, index).trim()
|
---|
| 123 | : header.trim()
|
---|
| 124 |
|
---|
| 125 | if (!TYPE_REGEXP.test(type)) {
|
---|
| 126 | throw new TypeError('invalid media type')
|
---|
| 127 | }
|
---|
| 128 |
|
---|
| 129 | var obj = new ContentType(type.toLowerCase())
|
---|
| 130 |
|
---|
| 131 | // parse parameters
|
---|
| 132 | if (index !== -1) {
|
---|
| 133 | var key
|
---|
| 134 | var match
|
---|
| 135 | var value
|
---|
| 136 |
|
---|
| 137 | PARAM_REGEXP.lastIndex = index
|
---|
| 138 |
|
---|
| 139 | while ((match = PARAM_REGEXP.exec(header))) {
|
---|
| 140 | if (match.index !== index) {
|
---|
| 141 | throw new TypeError('invalid parameter format')
|
---|
| 142 | }
|
---|
| 143 |
|
---|
| 144 | index += match[0].length
|
---|
| 145 | key = match[1].toLowerCase()
|
---|
| 146 | value = match[2]
|
---|
| 147 |
|
---|
| 148 | if (value[0] === '"') {
|
---|
| 149 | // remove quotes and escapes
|
---|
| 150 | value = value
|
---|
| 151 | .substr(1, value.length - 2)
|
---|
| 152 | .replace(QESC_REGEXP, '$1')
|
---|
| 153 | }
|
---|
| 154 |
|
---|
| 155 | obj.parameters[key] = value
|
---|
| 156 | }
|
---|
| 157 |
|
---|
| 158 | if (index !== header.length) {
|
---|
| 159 | throw new TypeError('invalid parameter format')
|
---|
| 160 | }
|
---|
| 161 | }
|
---|
| 162 |
|
---|
| 163 | return obj
|
---|
| 164 | }
|
---|
| 165 |
|
---|
| 166 | /**
|
---|
| 167 | * Get content-type from req/res objects.
|
---|
| 168 | *
|
---|
| 169 | * @param {object}
|
---|
| 170 | * @return {Object}
|
---|
| 171 | * @private
|
---|
| 172 | */
|
---|
| 173 |
|
---|
| 174 | function getcontenttype (obj) {
|
---|
| 175 | var header
|
---|
| 176 |
|
---|
| 177 | if (typeof obj.getHeader === 'function') {
|
---|
| 178 | // res-like
|
---|
| 179 | header = obj.getHeader('content-type')
|
---|
| 180 | } else if (typeof obj.headers === 'object') {
|
---|
| 181 | // req-like
|
---|
| 182 | header = obj.headers && obj.headers['content-type']
|
---|
| 183 | }
|
---|
| 184 |
|
---|
| 185 | if (typeof header !== 'string') {
|
---|
| 186 | throw new TypeError('content-type header is missing from object')
|
---|
| 187 | }
|
---|
| 188 |
|
---|
| 189 | return header
|
---|
| 190 | }
|
---|
| 191 |
|
---|
| 192 | /**
|
---|
| 193 | * Quote a string if necessary.
|
---|
| 194 | *
|
---|
| 195 | * @param {string} val
|
---|
| 196 | * @return {string}
|
---|
| 197 | * @private
|
---|
| 198 | */
|
---|
| 199 |
|
---|
| 200 | function qstring (val) {
|
---|
| 201 | var str = String(val)
|
---|
| 202 |
|
---|
| 203 | // no need to quote tokens
|
---|
| 204 | if (TOKEN_REGEXP.test(str)) {
|
---|
| 205 | return str
|
---|
| 206 | }
|
---|
| 207 |
|
---|
| 208 | if (str.length > 0 && !TEXT_REGEXP.test(str)) {
|
---|
| 209 | throw new TypeError('invalid parameter value')
|
---|
| 210 | }
|
---|
| 211 |
|
---|
| 212 | return '"' + str.replace(QUOTE_REGEXP, '\\$1') + '"'
|
---|
| 213 | }
|
---|
| 214 |
|
---|
| 215 | /**
|
---|
| 216 | * Class to represent a content type.
|
---|
| 217 | * @private
|
---|
| 218 | */
|
---|
| 219 | function ContentType (type) {
|
---|
| 220 | this.parameters = Object.create(null)
|
---|
| 221 | this.type = type
|
---|
| 222 | }
|
---|