nstdlib-nightly
Version:
Node.js standard library converted to runtime-agnostic ES modules.
410 lines (376 loc) • 11.4 kB
JavaScript
// 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 };