index.js (4809B)
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 }