UNPKG

nstdlib-nightly

Version:

Node.js standard library converted to runtime-agnostic ES modules.

410 lines (376 loc) 11.4 kB
// Source: https://github.com/nodejs/node/blob/65eff1eb/lib/internal/mime.js import { codes as __codes__ } from "nstdlib/lib/internal/errors"; const { ERR_INVALID_MIME_SYNTAX } = __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" ? String.prototype.toLowerCase.call(char) : char; } return result; } const SOLIDUS = "/"; const SEMICOLON = ";"; function parseTypeAndSubtype(str) { // Skip only HTTP whitespace from start let position = String.prototype.search.call(str, END_BEGINNING_WHITESPACE); // read until '/' const typeEnd = String.prototype.indexOf.call(str, SOLIDUS, position); const trimmedType = typeEnd === -1 ? String.prototype.slice.call(str, position) : String.prototype.slice.call(str, position, typeEnd); const invalidTypeIndex = String.prototype.search.call( 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 = String.prototype.indexOf.call(str, SEMICOLON, position); const rawSubtype = subtypeEnd === -1 ? String.prototype.slice.call(str, position) : String.prototype.slice.call(str, position, subtypeEnd); position += rawSubtype.length; if (subtypeEnd !== -1) { // skip ';' position += 1; } const trimmedSubtype = String.prototype.slice.call( rawSubtype, 0, String.prototype.search.call(rawSubtype, START_ENDING_WHITESPACE), ); const invalidSubtypeIndex = String.prototype.search.call( 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 = String.prototype.search.call(value, NOT_HTTP_TOKEN_CODE_POINT) !== -1; if (!encode) return value; const escaped = ((...args) => globalThis.escapeQuoteOrSolidus(...args))( value, ); return `"${escaped}"`; }; class MIMEParams { #data = new Map(); // 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 = String.prototype.search.call( name, NOT_HTTP_TOKEN_CODE_POINT, ); if (name.length === 0 || invalidNameIndex !== -1) { throw new ERR_INVALID_MIME_SYNTAX( "parameter name", name, invalidNameIndex, ); } const invalidValueIndex = String.prototype.search.call( 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 = String.prototype.search.call( String.prototype.slice.call(str, position), START_ENDING_WHITESPACE, ) + position; while (position < endOfSource) { // Skip any whitespace before parameter position += String.prototype.search.call( String.prototype.slice.call(str, position), END_BEGINNING_WHITESPACE, ); // Read until ';' or '=' const afterParameterName = String.prototype.search.call( String.prototype.slice.call(str, position), EQUALS_SEMICOLON_OR_END, ) + position; const parameterString = toASCIILower( String.prototype.slice.call(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 = String.prototype.charAt.call(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 = String.prototype.charAt.call(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 = RegExp.prototype.exec.call( QUOTED_VALUE_PATTERN, String.prototype.slice.call(str, position), ); position += insideMatch[0].length; // Skip including last character if an unmatched '\' or '"' during // unescape const inside = insideMatch[1] || insideMatch[2] ? String.prototype.slice.call(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 = String.prototype.indexOf.call( str, SEMICOLON, position, ); const rawValue = valueEnd === -1 ? String.prototype.slice.call(str, position) : String.prototype.slice.call(str, position, valueEnd); position += rawValue.length; const trimmedValue = String.prototype.slice.call( rawValue, 0, String.prototype.search.call(rawValue, START_ENDING_WHITESPACE), ); // Ignore parameters without values if (trimmedValue === "") continue; parameterValue = trimmedValue; } if ( parameterString !== "" && String.prototype.search.call( parameterString, NOT_HTTP_TOKEN_CODE_POINT, ) === -1 && String.prototype.search.call( 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; Object.defineProperty(MIMEParams.prototype, Symbol.iterator, { __proto__: null, configurable: true, value: MIMEParams.prototype.entries, writable: true, }); Object.defineProperty(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( String.prototype.slice.call(string, data[2]), ); } get type() { return this.#type; } set type(v) { v = `${v}`; const invalidTypeIndex = String.prototype.search.call( 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 = String.prototype.search.call( 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 = Function.prototype.call.call( MIMEParamsStringify, this.#parameters, ); if (paramStr.length) ret += `;${paramStr}`; return ret; } } Object.defineProperty(MIMEType.prototype, "toJSON", { __proto__: null, configurable: true, value: MIMEType.prototype.toString, writable: true, }); export { MIMEParams }; export { MIMEType };