2022-10-19 02:26:16 +00:00
|
|
|
'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 = ';';
|
2023-10-08 15:43:16 +00:00
|
|
|
|
2022-10-19 02:26:16 +00:00
|
|
|
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) {
|
2023-12-25 04:29:00 +00:00
|
|
|
throw new ERR_INVALID_MIME_SYNTAX('subtype', str, invalidSubtypeIndex);
|
2022-10-19 02:26:16 +00:00
|
|
|
}
|
|
|
|
const subtype = toASCIILower(trimmedSubtype);
|
2023-10-08 15:43:16 +00:00
|
|
|
return [
|
2022-10-19 02:26:16 +00:00
|
|
|
type,
|
|
|
|
subtype,
|
2023-10-08 15:43:16 +00:00
|
|
|
position,
|
|
|
|
];
|
2022-10-19 02:26:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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();
|
2023-10-08 15:43:16 +00:00
|
|
|
// 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;
|
|
|
|
}
|
2022-10-19 02:26:16 +00:00
|
|
|
|
|
|
|
delete(name) {
|
2023-10-08 15:43:16 +00:00
|
|
|
this.#parse();
|
2022-10-19 02:26:16 +00:00
|
|
|
this.#data.delete(name);
|
|
|
|
}
|
|
|
|
|
|
|
|
get(name) {
|
2023-10-08 15:43:16 +00:00
|
|
|
this.#parse();
|
2022-10-19 02:26:16 +00:00
|
|
|
const data = this.#data;
|
|
|
|
if (data.has(name)) {
|
|
|
|
return data.get(name);
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
has(name) {
|
2023-10-08 15:43:16 +00:00
|
|
|
this.#parse();
|
2022-10-19 02:26:16 +00:00
|
|
|
return this.#data.has(name);
|
|
|
|
}
|
|
|
|
|
|
|
|
set(name, value) {
|
2023-10-08 15:43:16 +00:00
|
|
|
this.#parse();
|
2022-10-19 02:26:16 +00:00
|
|
|
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() {
|
2023-10-08 15:43:16 +00:00
|
|
|
this.#parse();
|
2022-10-19 02:26:16 +00:00
|
|
|
yield* this.#data.entries();
|
|
|
|
}
|
|
|
|
|
|
|
|
*keys() {
|
2023-10-08 15:43:16 +00:00
|
|
|
this.#parse();
|
2022-10-19 02:26:16 +00:00
|
|
|
yield* this.#data.keys();
|
|
|
|
}
|
|
|
|
|
|
|
|
*values() {
|
2023-10-08 15:43:16 +00:00
|
|
|
this.#parse();
|
2022-10-19 02:26:16 +00:00
|
|
|
yield* this.#data.values();
|
|
|
|
}
|
|
|
|
|
|
|
|
toString() {
|
2023-10-08 15:43:16 +00:00
|
|
|
this.#parse();
|
2022-10-19 02:26:16 +00:00
|
|
|
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
|
2023-10-08 15:43:16 +00:00
|
|
|
#parse() {
|
|
|
|
if (this.#processed) return; // already parsed
|
|
|
|
const paramsMap = this.#data;
|
|
|
|
let position = 0;
|
|
|
|
const str = this.#string;
|
2022-10-19 02:26:16 +00:00
|
|
|
const endOfSource = SafeStringPrototypeSearch(
|
|
|
|
StringPrototypeSlice(str, position),
|
2023-02-14 17:45:16 +00:00
|
|
|
START_ENDING_WHITESPACE,
|
2022-10-19 02:26:16 +00:00
|
|
|
) + position;
|
|
|
|
while (position < endOfSource) {
|
|
|
|
// Skip any whitespace before parameter
|
|
|
|
position += SafeStringPrototypeSearch(
|
|
|
|
StringPrototypeSlice(str, position),
|
2023-02-14 17:45:16 +00:00
|
|
|
END_BEGINNING_WHITESPACE,
|
2022-10-19 02:26:16 +00:00
|
|
|
);
|
|
|
|
// Read until ';' or '='
|
|
|
|
const afterParameterName = SafeStringPrototypeSearch(
|
|
|
|
StringPrototypeSlice(str, position),
|
2023-02-14 17:45:16 +00:00
|
|
|
EQUALS_SEMICOLON_OR_END,
|
2022-10-19 02:26:16 +00:00
|
|
|
) + position;
|
|
|
|
const parameterString = toASCIILower(
|
2023-02-14 17:45:16 +00:00
|
|
|
StringPrototypeSlice(str, position, afterParameterName),
|
2022-10-19 02:26:16 +00:00
|
|
|
);
|
|
|
|
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,
|
2023-02-14 17:45:16 +00:00
|
|
|
SafeStringPrototypeSearch(rawValue, START_ENDING_WHITESPACE),
|
2022-10-19 02:26:16 +00:00
|
|
|
);
|
|
|
|
// 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 &&
|
2023-10-08 15:43:16 +00:00
|
|
|
paramsMap.has(parameterString) === false
|
2022-10-19 02:26:16 +00:00
|
|
|
) {
|
|
|
|
paramsMap.set(parameterString, parameterValue);
|
|
|
|
}
|
|
|
|
position++;
|
|
|
|
}
|
2023-10-08 15:43:16 +00:00
|
|
|
this.#data = paramsMap;
|
|
|
|
this.#processed = true;
|
2022-10-19 02:26:16 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
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,
|
|
|
|
});
|
|
|
|
|
2023-10-08 15:43:16 +00:00
|
|
|
const { instantiateMimeParams } = MIMEParams;
|
|
|
|
delete MIMEParams.instantiateMimeParams;
|
2022-10-19 02:26:16 +00:00
|
|
|
|
|
|
|
class MIMEType {
|
|
|
|
#type;
|
|
|
|
#subtype;
|
|
|
|
#parameters;
|
|
|
|
constructor(string) {
|
|
|
|
string = `${string}`;
|
|
|
|
const data = parseTypeAndSubtype(string);
|
2023-10-08 15:43:16 +00:00
|
|
|
this.#type = data[0];
|
|
|
|
this.#subtype = data[1];
|
|
|
|
this.#parameters = instantiateMimeParams(StringPrototypeSlice(string, data[2]));
|
2022-10-19 02:26:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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,
|
|
|
|
};
|