| 'use strict'; |
| |
| const { |
| FunctionPrototypeCall, |
| ObjectDefineProperty, |
| RegExpPrototypeExec, |
| SafeMap, |
| SafeStringPrototypeSearch, |
| StringPrototypeCharAt, |
| StringPrototypeIndexOf, |
| StringPrototypeSlice, |
| StringPrototypeToLowerCase, |
| SymbolIterator, |
| } = primordials; |
| const { |
| ERR_INVALID_MIME_SYNTAX, |
| } = require('internal/errors').codes; |
| |
| const NOT_HTTP_TOKEN_CODE_POINT = /[^!#$%&'*+\-.^_`|~A-Za-z0-9]/g; |
| const NOT_HTTP_QUOTED_STRING_CODE_POINT = /[^\t\u0020-~\u0080-\u00FF]/g; |
| |
| const END_BEGINNING_WHITESPACE = /[^\r\n\t ]|$/; |
| const START_ENDING_WHITESPACE = /[\r\n\t ]*$/; |
| |
| function toASCIILower(str) { |
| let result = ''; |
| for (let i = 0; i < str.length; i++) { |
| const char = str[i]; |
| |
| result += char >= 'A' && char <= 'Z' ? |
| StringPrototypeToLowerCase(char) : |
| char; |
| } |
| return result; |
| } |
| |
| const SOLIDUS = '/'; |
| const SEMICOLON = ';'; |
| |
| function parseTypeAndSubtype(str) { |
| // Skip only HTTP whitespace from start |
| let position = SafeStringPrototypeSearch(str, END_BEGINNING_WHITESPACE); |
| // read until '/' |
| const typeEnd = StringPrototypeIndexOf(str, SOLIDUS, position); |
| const trimmedType = typeEnd === -1 ? |
| StringPrototypeSlice(str, position) : |
| StringPrototypeSlice(str, position, typeEnd); |
| const invalidTypeIndex = SafeStringPrototypeSearch(trimmedType, |
| NOT_HTTP_TOKEN_CODE_POINT); |
| if (trimmedType === '' || invalidTypeIndex !== -1 || typeEnd === -1) { |
| throw new ERR_INVALID_MIME_SYNTAX('type', str, invalidTypeIndex); |
| } |
| // skip type and '/' |
| position = typeEnd + 1; |
| const type = toASCIILower(trimmedType); |
| // read until ';' |
| const subtypeEnd = StringPrototypeIndexOf(str, SEMICOLON, position); |
| const rawSubtype = subtypeEnd === -1 ? |
| StringPrototypeSlice(str, position) : |
| StringPrototypeSlice(str, position, subtypeEnd); |
| position += rawSubtype.length; |
| if (subtypeEnd !== -1) { |
| // skip ';' |
| position += 1; |
| } |
| const trimmedSubtype = StringPrototypeSlice( |
| rawSubtype, |
| 0, |
| SafeStringPrototypeSearch(rawSubtype, START_ENDING_WHITESPACE)); |
| const invalidSubtypeIndex = SafeStringPrototypeSearch(trimmedSubtype, |
| NOT_HTTP_TOKEN_CODE_POINT); |
| if (trimmedSubtype === '' || invalidSubtypeIndex !== -1) { |
| throw new ERR_INVALID_MIME_SYNTAX('subtype', str, invalidSubtypeIndex); |
| } |
| const subtype = toASCIILower(trimmedSubtype); |
| return [ |
| type, |
| subtype, |
| position, |
| ]; |
| } |
| |
| const EQUALS_SEMICOLON_OR_END = /[;=]|$/; |
| const QUOTED_VALUE_PATTERN = /^(?:([\\]$)|[\\][\s\S]|[^"])*(?:(")|$)/u; |
| |
| function removeBackslashes(str) { |
| let ret = ''; |
| // We stop at str.length - 1 because we want to look ahead one character. |
| let i; |
| for (i = 0; i < str.length - 1; i++) { |
| const c = str[i]; |
| if (c === '\\') { |
| i++; |
| ret += str[i]; |
| } else { |
| ret += c; |
| } |
| } |
| // We add the last character if we didn't skip to it. |
| if (i === str.length - 1) { |
| ret += str[i]; |
| } |
| return ret; |
| } |
| |
| |
| function escapeQuoteOrSolidus(str) { |
| let result = ''; |
| for (let i = 0; i < str.length; i++) { |
| const char = str[i]; |
| result += (char === '"' || char === '\\') ? `\\${char}` : char; |
| } |
| return result; |
| } |
| |
| const encode = (value) => { |
| if (value.length === 0) return '""'; |
| const encode = SafeStringPrototypeSearch(value, NOT_HTTP_TOKEN_CODE_POINT) !== -1; |
| if (!encode) return value; |
| const escaped = escapeQuoteOrSolidus(value); |
| return `"${escaped}"`; |
| }; |
| |
| class MIMEParams { |
| #data = new SafeMap(); |
| // We set the flag the MIMEParams instance as processed on initialization |
| // to defer the parsing of a potentially large string. |
| #processed = true; |
| #string = null; |
| |
| /** |
| * Used to instantiate a MIMEParams object within the MIMEType class and |
| * to allow it to be parsed lazily. |
| */ |
| static instantiateMimeParams(str) { |
| const instance = new MIMEParams(); |
| instance.#string = str; |
| instance.#processed = false; |
| return instance; |
| } |
| |
| delete(name) { |
| this.#parse(); |
| this.#data.delete(name); |
| } |
| |
| get(name) { |
| this.#parse(); |
| const data = this.#data; |
| if (data.has(name)) { |
| return data.get(name); |
| } |
| return null; |
| } |
| |
| has(name) { |
| this.#parse(); |
| return this.#data.has(name); |
| } |
| |
| set(name, value) { |
| this.#parse(); |
| const data = this.#data; |
| name = `${name}`; |
| value = `${value}`; |
| const invalidNameIndex = SafeStringPrototypeSearch(name, NOT_HTTP_TOKEN_CODE_POINT); |
| if (name.length === 0 || invalidNameIndex !== -1) { |
| throw new ERR_INVALID_MIME_SYNTAX( |
| 'parameter name', |
| name, |
| invalidNameIndex, |
| ); |
| } |
| const invalidValueIndex = SafeStringPrototypeSearch( |
| value, |
| NOT_HTTP_QUOTED_STRING_CODE_POINT); |
| if (invalidValueIndex !== -1) { |
| throw new ERR_INVALID_MIME_SYNTAX( |
| 'parameter value', |
| value, |
| invalidValueIndex, |
| ); |
| } |
| data.set(name, value); |
| } |
| |
| *entries() { |
| this.#parse(); |
| yield* this.#data.entries(); |
| } |
| |
| *keys() { |
| this.#parse(); |
| yield* this.#data.keys(); |
| } |
| |
| *values() { |
| this.#parse(); |
| yield* this.#data.values(); |
| } |
| |
| toString() { |
| this.#parse(); |
| let ret = ''; |
| for (const { 0: key, 1: value } of this.#data) { |
| const encoded = encode(value); |
| // Ensure they are separated |
| if (ret.length) ret += ';'; |
| ret += `${key}=${encoded}`; |
| } |
| return ret; |
| } |
| |
| // Used to act as a friendly class to stringifying stuff |
| // not meant to be exposed to users, could inject invalid values |
| #parse() { |
| if (this.#processed) return; // already parsed |
| const paramsMap = this.#data; |
| let position = 0; |
| const str = this.#string; |
| const endOfSource = SafeStringPrototypeSearch( |
| StringPrototypeSlice(str, position), |
| START_ENDING_WHITESPACE, |
| ) + position; |
| while (position < endOfSource) { |
| // Skip any whitespace before parameter |
| position += SafeStringPrototypeSearch( |
| StringPrototypeSlice(str, position), |
| END_BEGINNING_WHITESPACE, |
| ); |
| // Read until ';' or '=' |
| const afterParameterName = SafeStringPrototypeSearch( |
| StringPrototypeSlice(str, position), |
| EQUALS_SEMICOLON_OR_END, |
| ) + position; |
| const parameterString = toASCIILower( |
| StringPrototypeSlice(str, position, afterParameterName), |
| ); |
| position = afterParameterName; |
| // If we found a terminating character |
| if (position < endOfSource) { |
| // Safe to use because we never do special actions for surrogate pairs |
| const char = StringPrototypeCharAt(str, position); |
| // Skip the terminating character |
| position += 1; |
| // Ignore parameters without values |
| if (char === ';') { |
| continue; |
| } |
| } |
| // If we are at end of the string, it cannot have a value |
| if (position >= endOfSource) break; |
| // Safe to use because we never do special actions for surrogate pairs |
| const char = StringPrototypeCharAt(str, position); |
| let parameterValue = null; |
| if (char === '"') { |
| // Handle quoted-string form of values |
| // skip '"' |
| position += 1; |
| // Find matching closing '"' or end of string |
| // use $1 to see if we terminated on unmatched '\' |
| // use $2 to see if we terminated on a matching '"' |
| // so we can skip the last char in either case |
| const insideMatch = RegExpPrototypeExec( |
| QUOTED_VALUE_PATTERN, |
| StringPrototypeSlice(str, position)); |
| position += insideMatch[0].length; |
| // Skip including last character if an unmatched '\' or '"' during |
| // unescape |
| const inside = insideMatch[1] || insideMatch[2] ? |
| StringPrototypeSlice(insideMatch[0], 0, -1) : |
| insideMatch[0]; |
| // Unescape '\' quoted characters |
| parameterValue = removeBackslashes(inside); |
| // If we did have an unmatched '\' add it back to the end |
| if (insideMatch[1]) parameterValue += '\\'; |
| } else { |
| // Handle the normal parameter value form |
| const valueEnd = StringPrototypeIndexOf(str, SEMICOLON, position); |
| const rawValue = valueEnd === -1 ? |
| StringPrototypeSlice(str, position) : |
| StringPrototypeSlice(str, position, valueEnd); |
| position += rawValue.length; |
| const trimmedValue = StringPrototypeSlice( |
| rawValue, |
| 0, |
| SafeStringPrototypeSearch(rawValue, START_ENDING_WHITESPACE), |
| ); |
| // Ignore parameters without values |
| if (trimmedValue === '') continue; |
| parameterValue = trimmedValue; |
| } |
| if ( |
| parameterString !== '' && |
| SafeStringPrototypeSearch(parameterString, |
| NOT_HTTP_TOKEN_CODE_POINT) === -1 && |
| SafeStringPrototypeSearch(parameterValue, |
| NOT_HTTP_QUOTED_STRING_CODE_POINT) === -1 && |
| paramsMap.has(parameterString) === false |
| ) { |
| paramsMap.set(parameterString, parameterValue); |
| } |
| position++; |
| } |
| this.#data = paramsMap; |
| this.#processed = true; |
| } |
| } |
| const MIMEParamsStringify = MIMEParams.prototype.toString; |
| ObjectDefineProperty(MIMEParams.prototype, SymbolIterator, { |
| __proto__: null, |
| configurable: true, |
| value: MIMEParams.prototype.entries, |
| writable: true, |
| }); |
| ObjectDefineProperty(MIMEParams.prototype, 'toJSON', { |
| __proto__: null, |
| configurable: true, |
| value: MIMEParamsStringify, |
| writable: true, |
| }); |
| |
| const { instantiateMimeParams } = MIMEParams; |
| delete MIMEParams.instantiateMimeParams; |
| |
| class MIMEType { |
| #type; |
| #subtype; |
| #parameters; |
| constructor(string) { |
| string = `${string}`; |
| const data = parseTypeAndSubtype(string); |
| this.#type = data[0]; |
| this.#subtype = data[1]; |
| this.#parameters = instantiateMimeParams(StringPrototypeSlice(string, data[2])); |
| } |
| |
| get type() { |
| return this.#type; |
| } |
| |
| set type(v) { |
| v = `${v}`; |
| const invalidTypeIndex = SafeStringPrototypeSearch(v, NOT_HTTP_TOKEN_CODE_POINT); |
| if (v.length === 0 || invalidTypeIndex !== -1) { |
| throw new ERR_INVALID_MIME_SYNTAX('type', v, invalidTypeIndex); |
| } |
| this.#type = toASCIILower(v); |
| } |
| |
| get subtype() { |
| return this.#subtype; |
| } |
| |
| set subtype(v) { |
| v = `${v}`; |
| const invalidSubtypeIndex = SafeStringPrototypeSearch(v, NOT_HTTP_TOKEN_CODE_POINT); |
| if (v.length === 0 || invalidSubtypeIndex !== -1) { |
| throw new ERR_INVALID_MIME_SYNTAX('subtype', v, invalidSubtypeIndex); |
| } |
| this.#subtype = toASCIILower(v); |
| } |
| |
| get essence() { |
| return `${this.#type}/${this.#subtype}`; |
| } |
| |
| get params() { |
| return this.#parameters; |
| } |
| |
| toString() { |
| let ret = `${this.#type}/${this.#subtype}`; |
| const paramStr = FunctionPrototypeCall(MIMEParamsStringify, this.#parameters); |
| if (paramStr.length) ret += `;${paramStr}`; |
| return ret; |
| } |
| } |
| ObjectDefineProperty(MIMEType.prototype, 'toJSON', { |
| __proto__: null, |
| configurable: true, |
| value: MIMEType.prototype.toString, |
| writable: true, |
| }); |
| |
| module.exports = { |
| MIMEParams, |
| MIMEType, |
| }; |