1'use strict'; 2 3const { 4 FunctionPrototypeCall, 5 ObjectDefineProperty, 6 RegExpPrototypeExec, 7 SafeMap, 8 SafeStringPrototypeSearch, 9 StringPrototypeCharAt, 10 StringPrototypeIndexOf, 11 StringPrototypeSlice, 12 StringPrototypeToLowerCase, 13 SymbolIterator, 14} = primordials; 15const { 16 ERR_INVALID_MIME_SYNTAX, 17} = require('internal/errors').codes; 18 19const NOT_HTTP_TOKEN_CODE_POINT = /[^!#$%&'*+\-.^_`|~A-Za-z0-9]/g; 20const NOT_HTTP_QUOTED_STRING_CODE_POINT = /[^\t\u0020-~\u0080-\u00FF]/g; 21 22const END_BEGINNING_WHITESPACE = /[^\r\n\t ]|$/; 23const START_ENDING_WHITESPACE = /[\r\n\t ]*$/; 24 25function toASCIILower(str) { 26 let result = ''; 27 for (let i = 0; i < str.length; i++) { 28 const char = str[i]; 29 30 result += char >= 'A' && char <= 'Z' ? 31 StringPrototypeToLowerCase(char) : 32 char; 33 } 34 return result; 35} 36 37const SOLIDUS = '/'; 38const SEMICOLON = ';'; 39function parseTypeAndSubtype(str) { 40 // Skip only HTTP whitespace from start 41 let position = SafeStringPrototypeSearch(str, END_BEGINNING_WHITESPACE); 42 // read until '/' 43 const typeEnd = StringPrototypeIndexOf(str, SOLIDUS, position); 44 const trimmedType = typeEnd === -1 ? 45 StringPrototypeSlice(str, position) : 46 StringPrototypeSlice(str, position, typeEnd); 47 const invalidTypeIndex = SafeStringPrototypeSearch(trimmedType, 48 NOT_HTTP_TOKEN_CODE_POINT); 49 if (trimmedType === '' || invalidTypeIndex !== -1 || typeEnd === -1) { 50 throw new ERR_INVALID_MIME_SYNTAX('type', str, invalidTypeIndex); 51 } 52 // skip type and '/' 53 position = typeEnd + 1; 54 const type = toASCIILower(trimmedType); 55 // read until ';' 56 const subtypeEnd = StringPrototypeIndexOf(str, SEMICOLON, position); 57 const rawSubtype = subtypeEnd === -1 ? 58 StringPrototypeSlice(str, position) : 59 StringPrototypeSlice(str, position, subtypeEnd); 60 position += rawSubtype.length; 61 if (subtypeEnd !== -1) { 62 // skip ';' 63 position += 1; 64 } 65 const trimmedSubtype = StringPrototypeSlice( 66 rawSubtype, 67 0, 68 SafeStringPrototypeSearch(rawSubtype, START_ENDING_WHITESPACE)); 69 const invalidSubtypeIndex = SafeStringPrototypeSearch(trimmedSubtype, 70 NOT_HTTP_TOKEN_CODE_POINT); 71 if (trimmedSubtype === '' || invalidSubtypeIndex !== -1) { 72 throw new ERR_INVALID_MIME_SYNTAX('subtype', str, trimmedSubtype); 73 } 74 const subtype = toASCIILower(trimmedSubtype); 75 return { 76 __proto__: null, 77 type, 78 subtype, 79 parametersStringIndex: position, 80 }; 81} 82 83const EQUALS_SEMICOLON_OR_END = /[;=]|$/; 84const QUOTED_VALUE_PATTERN = /^(?:([\\]$)|[\\][\s\S]|[^"])*(?:(")|$)/u; 85 86function removeBackslashes(str) { 87 let ret = ''; 88 // We stop at str.length - 1 because we want to look ahead one character. 89 let i; 90 for (i = 0; i < str.length - 1; i++) { 91 const c = str[i]; 92 if (c === '\\') { 93 i++; 94 ret += str[i]; 95 } else { 96 ret += c; 97 } 98 } 99 // We add the last character if we didn't skip to it. 100 if (i === str.length - 1) { 101 ret += str[i]; 102 } 103 return ret; 104} 105 106 107function escapeQuoteOrSolidus(str) { 108 let result = ''; 109 for (let i = 0; i < str.length; i++) { 110 const char = str[i]; 111 result += (char === '"' || char === '\\') ? `\\${char}` : char; 112 } 113 return result; 114} 115 116const encode = (value) => { 117 if (value.length === 0) return '""'; 118 const encode = SafeStringPrototypeSearch(value, NOT_HTTP_TOKEN_CODE_POINT) !== -1; 119 if (!encode) return value; 120 const escaped = escapeQuoteOrSolidus(value); 121 return `"${escaped}"`; 122}; 123 124class MIMEParams { 125 #data = new SafeMap(); 126 127 delete(name) { 128 this.#data.delete(name); 129 } 130 131 get(name) { 132 const data = this.#data; 133 if (data.has(name)) { 134 return data.get(name); 135 } 136 return null; 137 } 138 139 has(name) { 140 return this.#data.has(name); 141 } 142 143 set(name, value) { 144 const data = this.#data; 145 name = `${name}`; 146 value = `${value}`; 147 const invalidNameIndex = SafeStringPrototypeSearch(name, NOT_HTTP_TOKEN_CODE_POINT); 148 if (name.length === 0 || invalidNameIndex !== -1) { 149 throw new ERR_INVALID_MIME_SYNTAX( 150 'parameter name', 151 name, 152 invalidNameIndex, 153 ); 154 } 155 const invalidValueIndex = SafeStringPrototypeSearch( 156 value, 157 NOT_HTTP_QUOTED_STRING_CODE_POINT); 158 if (invalidValueIndex !== -1) { 159 throw new ERR_INVALID_MIME_SYNTAX( 160 'parameter value', 161 value, 162 invalidValueIndex, 163 ); 164 } 165 data.set(name, value); 166 } 167 168 *entries() { 169 yield* this.#data.entries(); 170 } 171 172 *keys() { 173 yield* this.#data.keys(); 174 } 175 176 *values() { 177 yield* this.#data.values(); 178 } 179 180 toString() { 181 let ret = ''; 182 for (const { 0: key, 1: value } of this.#data) { 183 const encoded = encode(value); 184 // Ensure they are separated 185 if (ret.length) ret += ';'; 186 ret += `${key}=${encoded}`; 187 } 188 return ret; 189 } 190 191 // Used to act as a friendly class to stringifying stuff 192 // not meant to be exposed to users, could inject invalid values 193 static parseParametersString(str, position, params) { 194 const paramsMap = params.#data; 195 const endOfSource = SafeStringPrototypeSearch( 196 StringPrototypeSlice(str, position), 197 START_ENDING_WHITESPACE, 198 ) + position; 199 while (position < endOfSource) { 200 // Skip any whitespace before parameter 201 position += SafeStringPrototypeSearch( 202 StringPrototypeSlice(str, position), 203 END_BEGINNING_WHITESPACE, 204 ); 205 // Read until ';' or '=' 206 const afterParameterName = SafeStringPrototypeSearch( 207 StringPrototypeSlice(str, position), 208 EQUALS_SEMICOLON_OR_END, 209 ) + position; 210 const parameterString = toASCIILower( 211 StringPrototypeSlice(str, position, afterParameterName), 212 ); 213 position = afterParameterName; 214 // If we found a terminating character 215 if (position < endOfSource) { 216 // Safe to use because we never do special actions for surrogate pairs 217 const char = StringPrototypeCharAt(str, position); 218 // Skip the terminating character 219 position += 1; 220 // Ignore parameters without values 221 if (char === ';') { 222 continue; 223 } 224 } 225 // If we are at end of the string, it cannot have a value 226 if (position >= endOfSource) break; 227 // Safe to use because we never do special actions for surrogate pairs 228 const char = StringPrototypeCharAt(str, position); 229 let parameterValue = null; 230 if (char === '"') { 231 // Handle quoted-string form of values 232 // skip '"' 233 position += 1; 234 // Find matching closing '"' or end of string 235 // use $1 to see if we terminated on unmatched '\' 236 // use $2 to see if we terminated on a matching '"' 237 // so we can skip the last char in either case 238 const insideMatch = RegExpPrototypeExec( 239 QUOTED_VALUE_PATTERN, 240 StringPrototypeSlice(str, position)); 241 position += insideMatch[0].length; 242 // Skip including last character if an unmatched '\' or '"' during 243 // unescape 244 const inside = insideMatch[1] || insideMatch[2] ? 245 StringPrototypeSlice(insideMatch[0], 0, -1) : 246 insideMatch[0]; 247 // Unescape '\' quoted characters 248 parameterValue = removeBackslashes(inside); 249 // If we did have an unmatched '\' add it back to the end 250 if (insideMatch[1]) parameterValue += '\\'; 251 } else { 252 // Handle the normal parameter value form 253 const valueEnd = StringPrototypeIndexOf(str, SEMICOLON, position); 254 const rawValue = valueEnd === -1 ? 255 StringPrototypeSlice(str, position) : 256 StringPrototypeSlice(str, position, valueEnd); 257 position += rawValue.length; 258 const trimmedValue = StringPrototypeSlice( 259 rawValue, 260 0, 261 SafeStringPrototypeSearch(rawValue, START_ENDING_WHITESPACE), 262 ); 263 // Ignore parameters without values 264 if (trimmedValue === '') continue; 265 parameterValue = trimmedValue; 266 } 267 if ( 268 parameterString !== '' && 269 SafeStringPrototypeSearch(parameterString, 270 NOT_HTTP_TOKEN_CODE_POINT) === -1 && 271 SafeStringPrototypeSearch(parameterValue, 272 NOT_HTTP_QUOTED_STRING_CODE_POINT) === -1 && 273 params.has(parameterString) === false 274 ) { 275 paramsMap.set(parameterString, parameterValue); 276 } 277 position++; 278 } 279 return paramsMap; 280 } 281} 282const MIMEParamsStringify = MIMEParams.prototype.toString; 283ObjectDefineProperty(MIMEParams.prototype, SymbolIterator, { 284 __proto__: null, 285 configurable: true, 286 value: MIMEParams.prototype.entries, 287 writable: true, 288}); 289ObjectDefineProperty(MIMEParams.prototype, 'toJSON', { 290 __proto__: null, 291 configurable: true, 292 value: MIMEParamsStringify, 293 writable: true, 294}); 295 296const { parseParametersString } = MIMEParams; 297delete MIMEParams.parseParametersString; 298 299class MIMEType { 300 #type; 301 #subtype; 302 #parameters; 303 constructor(string) { 304 string = `${string}`; 305 const data = parseTypeAndSubtype(string); 306 this.#type = data.type; 307 this.#subtype = data.subtype; 308 this.#parameters = new MIMEParams(); 309 parseParametersString( 310 string, 311 data.parametersStringIndex, 312 this.#parameters, 313 ); 314 } 315 316 get type() { 317 return this.#type; 318 } 319 320 set type(v) { 321 v = `${v}`; 322 const invalidTypeIndex = SafeStringPrototypeSearch(v, NOT_HTTP_TOKEN_CODE_POINT); 323 if (v.length === 0 || invalidTypeIndex !== -1) { 324 throw new ERR_INVALID_MIME_SYNTAX('type', v, invalidTypeIndex); 325 } 326 this.#type = toASCIILower(v); 327 } 328 329 get subtype() { 330 return this.#subtype; 331 } 332 333 set subtype(v) { 334 v = `${v}`; 335 const invalidSubtypeIndex = SafeStringPrototypeSearch(v, NOT_HTTP_TOKEN_CODE_POINT); 336 if (v.length === 0 || invalidSubtypeIndex !== -1) { 337 throw new ERR_INVALID_MIME_SYNTAX('subtype', v, invalidSubtypeIndex); 338 } 339 this.#subtype = toASCIILower(v); 340 } 341 342 get essence() { 343 return `${this.#type}/${this.#subtype}`; 344 } 345 346 get params() { 347 return this.#parameters; 348 } 349 350 toString() { 351 let ret = `${this.#type}/${this.#subtype}`; 352 const paramStr = FunctionPrototypeCall(MIMEParamsStringify, this.#parameters); 353 if (paramStr.length) ret += `;${paramStr}`; 354 return ret; 355 } 356} 357ObjectDefineProperty(MIMEType.prototype, 'toJSON', { 358 __proto__: null, 359 configurable: true, 360 value: MIMEType.prototype.toString, 361 writable: true, 362}); 363 364module.exports = { 365 MIMEParams, 366 MIMEType, 367}; 368