index.js (6375B)
1 /*! 2 * media-typer 3 * Copyright(c) 2014 Douglas Christopher Wilson 4 * MIT Licensed 5 */ 6 7 /** 8 * RegExp to match *( ";" parameter ) in RFC 2616 sec 3.7 9 * 10 * parameter = token "=" ( token | quoted-string ) 11 * token = 1*<any CHAR except CTLs or separators> 12 * separators = "(" | ")" | "<" | ">" | "@" 13 * | "," | ";" | ":" | "\" | <"> 14 * | "/" | "[" | "]" | "?" | "=" 15 * | "{" | "}" | SP | HT 16 * quoted-string = ( <"> *(qdtext | quoted-pair ) <"> ) 17 * qdtext = <any TEXT except <">> 18 * quoted-pair = "\" CHAR 19 * CHAR = <any US-ASCII character (octets 0 - 127)> 20 * TEXT = <any OCTET except CTLs, but including LWS> 21 * LWS = [CRLF] 1*( SP | HT ) 22 * CRLF = CR LF 23 * CR = <US-ASCII CR, carriage return (13)> 24 * LF = <US-ASCII LF, linefeed (10)> 25 * SP = <US-ASCII SP, space (32)> 26 * SHT = <US-ASCII HT, horizontal-tab (9)> 27 * CTL = <any US-ASCII control character (octets 0 - 31) and DEL (127)> 28 * OCTET = <any 8-bit sequence of data> 29 */ 30 var paramRegExp = /; *([!#$%&'\*\+\-\.0-9A-Z\^_`a-z\|~]+) *= *("(?:[ !\u0023-\u005b\u005d-\u007e\u0080-\u00ff]|\\[\u0020-\u007e])*"|[!#$%&'\*\+\-\.0-9A-Z\^_`a-z\|~]+) */g; 31 var textRegExp = /^[\u0020-\u007e\u0080-\u00ff]+$/ 32 var tokenRegExp = /^[!#$%&'\*\+\-\.0-9A-Z\^_`a-z\|~]+$/ 33 34 /** 35 * RegExp to match quoted-pair in RFC 2616 36 * 37 * quoted-pair = "\" CHAR 38 * CHAR = <any US-ASCII character (octets 0 - 127)> 39 */ 40 var qescRegExp = /\\([\u0000-\u007f])/g; 41 42 /** 43 * RegExp to match chars that must be quoted-pair in RFC 2616 44 */ 45 var quoteRegExp = /([\\"])/g; 46 47 /** 48 * RegExp to match type in RFC 6838 49 * 50 * type-name = restricted-name 51 * subtype-name = restricted-name 52 * restricted-name = restricted-name-first *126restricted-name-chars 53 * restricted-name-first = ALPHA / DIGIT 54 * restricted-name-chars = ALPHA / DIGIT / "!" / "#" / 55 * "$" / "&" / "-" / "^" / "_" 56 * restricted-name-chars =/ "." ; Characters before first dot always 57 * ; specify a facet name 58 * restricted-name-chars =/ "+" ; Characters after last plus always 59 * ; specify a structured syntax suffix 60 * ALPHA = %x41-5A / %x61-7A ; A-Z / a-z 61 * DIGIT = %x30-39 ; 0-9 62 */ 63 var subtypeNameRegExp = /^[A-Za-z0-9][A-Za-z0-9!#$&^_.-]{0,126}$/ 64 var typeNameRegExp = /^[A-Za-z0-9][A-Za-z0-9!#$&^_-]{0,126}$/ 65 var typeRegExp = /^ *([A-Za-z0-9][A-Za-z0-9!#$&^_-]{0,126})\/([A-Za-z0-9][A-Za-z0-9!#$&^_.+-]{0,126}) *$/; 66 67 /** 68 * Module exports. 69 */ 70 71 exports.format = format 72 exports.parse = parse 73 74 /** 75 * Format object to media type. 76 * 77 * @param {object} obj 78 * @return {string} 79 * @api public 80 */ 81 82 function format(obj) { 83 if (!obj || typeof obj !== 'object') { 84 throw new TypeError('argument obj is required') 85 } 86 87 var parameters = obj.parameters 88 var subtype = obj.subtype 89 var suffix = obj.suffix 90 var type = obj.type 91 92 if (!type || !typeNameRegExp.test(type)) { 93 throw new TypeError('invalid type') 94 } 95 96 if (!subtype || !subtypeNameRegExp.test(subtype)) { 97 throw new TypeError('invalid subtype') 98 } 99 100 // format as type/subtype 101 var string = type + '/' + subtype 102 103 // append +suffix 104 if (suffix) { 105 if (!typeNameRegExp.test(suffix)) { 106 throw new TypeError('invalid suffix') 107 } 108 109 string += '+' + suffix 110 } 111 112 // append parameters 113 if (parameters && typeof parameters === 'object') { 114 var param 115 var params = Object.keys(parameters).sort() 116 117 for (var i = 0; i < params.length; i++) { 118 param = params[i] 119 120 if (!tokenRegExp.test(param)) { 121 throw new TypeError('invalid parameter name') 122 } 123 124 string += '; ' + param + '=' + qstring(parameters[param]) 125 } 126 } 127 128 return string 129 } 130 131 /** 132 * Parse media type to object. 133 * 134 * @param {string|object} string 135 * @return {Object} 136 * @api public 137 */ 138 139 function parse(string) { 140 if (!string) { 141 throw new TypeError('argument string is required') 142 } 143 144 // support req/res-like objects as argument 145 if (typeof string === 'object') { 146 string = getcontenttype(string) 147 } 148 149 if (typeof string !== 'string') { 150 throw new TypeError('argument string is required to be a string') 151 } 152 153 var index = string.indexOf(';') 154 var type = index !== -1 155 ? string.substr(0, index) 156 : string 157 158 var key 159 var match 160 var obj = splitType(type) 161 var params = {} 162 var value 163 164 paramRegExp.lastIndex = index 165 166 while (match = paramRegExp.exec(string)) { 167 if (match.index !== index) { 168 throw new TypeError('invalid parameter format') 169 } 170 171 index += match[0].length 172 key = match[1].toLowerCase() 173 value = match[2] 174 175 if (value[0] === '"') { 176 // remove quotes and escapes 177 value = value 178 .substr(1, value.length - 2) 179 .replace(qescRegExp, '$1') 180 } 181 182 params[key] = value 183 } 184 185 if (index !== -1 && index !== string.length) { 186 throw new TypeError('invalid parameter format') 187 } 188 189 obj.parameters = params 190 191 return obj 192 } 193 194 /** 195 * Get content-type from req/res objects. 196 * 197 * @param {object} 198 * @return {Object} 199 * @api private 200 */ 201 202 function getcontenttype(obj) { 203 if (typeof obj.getHeader === 'function') { 204 // res-like 205 return obj.getHeader('content-type') 206 } 207 208 if (typeof obj.headers === 'object') { 209 // req-like 210 return obj.headers && obj.headers['content-type'] 211 } 212 } 213 214 /** 215 * Quote a string if necessary. 216 * 217 * @param {string} val 218 * @return {string} 219 * @api private 220 */ 221 222 function qstring(val) { 223 var str = String(val) 224 225 // no need to quote tokens 226 if (tokenRegExp.test(str)) { 227 return str 228 } 229 230 if (str.length > 0 && !textRegExp.test(str)) { 231 throw new TypeError('invalid parameter value') 232 } 233 234 return '"' + str.replace(quoteRegExp, '\\$1') + '"' 235 } 236 237 /** 238 * Simply "type/subtype+siffx" into parts. 239 * 240 * @param {string} string 241 * @return {Object} 242 * @api private 243 */ 244 245 function splitType(string) { 246 var match = typeRegExp.exec(string.toLowerCase()) 247 248 if (!match) { 249 throw new TypeError('invalid media type') 250 } 251 252 var type = match[1] 253 var subtype = match[2] 254 var suffix 255 256 // suffix after last + 257 var index = subtype.lastIndexOf('+') 258 if (index !== -1) { 259 suffix = subtype.substr(index + 1) 260 subtype = subtype.substr(0, index) 261 } 262 263 var obj = { 264 type: type, 265 subtype: subtype, 266 suffix: suffix 267 } 268 269 return obj 270 }