Commit 6812e92dea0 for nodejs
commit 6812e92dea0c5fb502d9e382e8cfc059f77e02b4
Author: James M Snell <jasnell@gmail.com>
Date: Mon Oct 5 00:47:29 2026 -0500
util: make mime parsing faster
Signed-off-by: James M Snell <jasnell@gmail.com>
Assisted-by: Opencode
PR-URL: https://github.com/nodejs/node/pull/66369
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
diff --git a/benchmark/mime/mimetype-parse.js b/benchmark/mime/mimetype-parse.js
new file mode 100644
index 00000000000..2aec20ceb06
--- /dev/null
+++ b/benchmark/mime/mimetype-parse.js
@@ -0,0 +1,53 @@
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const { MIMEType } = require('util');
+
+const bench = common.createBenchmark(main, {
+ n: [1e5],
+ value: [
+ 'application/json',
+ 'text/html; charset=utf-8',
+ 'multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW',
+ 'text/plain; charset="utf-8"; foo="b\\"ar"; x=y',
+ 'not a mime type',
+ ],
+ operation: ['parse', 'essence', 'params.get'],
+});
+
+function main({ n, value, operation }) {
+ const length = 1024;
+ const array = [];
+ let fn;
+ switch (operation) {
+ case 'parse':
+ fn = () => MIMEType.parse(value);
+ break;
+ case 'essence':
+ fn = () => MIMEType.parse(value)?.essence ?? null;
+ break;
+ case 'params.get':
+ fn = () => MIMEType.parse(value)?.params.get('charset') ?? null;
+ break;
+ default:
+ throw new Error(`Unsupported operation ${operation}`);
+ }
+
+ // Warm up.
+ for (let i = 0; i < length; ++i) {
+ array.push(fn());
+ }
+
+ bench.start();
+ for (let i = 0; i < n; ++i) {
+ array[i % length] = fn();
+ }
+ bench.end(n);
+
+ // Verify the entries to prevent dead code elimination from making
+ // the benchmark invalid.
+ for (let i = 0; i < length; ++i) {
+ assert.notStrictEqual(array[i], undefined);
+ }
+}
diff --git a/lib/internal/mime.js b/lib/internal/mime.js
index bb368a36aa5..7c7b18f088b 100644
--- a/lib/internal/mime.js
+++ b/lib/internal/mime.js
@@ -3,115 +3,297 @@
const {
FunctionPrototypeCall,
ObjectDefineProperty,
- RegExpPrototypeExec,
SafeMap,
- SafeStringPrototypeSearch,
- StringPrototypeCharAt,
- StringPrototypeIndexOf,
+ StringPrototypeCharCodeAt,
StringPrototypeSlice,
StringPrototypeToLowerCase,
Symbol,
SymbolIterator,
+ Uint8Array,
} = primordials;
const {
ERR_ILLEGAL_CONSTRUCTOR,
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 ]*$/;
+// Lookup table for the code point classes used by the MIME Sniffing
+// standard. Only code points <= 0xFF can be in either class.
+// https://mimesniff.spec.whatwg.org/#http-token-code-point
+// https://mimesniff.spec.whatwg.org/#http-quoted-string-token-code-point
+const kHTTPToken = 1;
+const kHTTPQuotedStringToken = 2;
+const codePointClass = new Uint8Array(256);
+{
+ codePointClass[0x09] = kHTTPQuotedStringToken;
+ for (let c = 0x20; c <= 0x7E; c++) codePointClass[c] = kHTTPQuotedStringToken;
+ for (let c = 0x80; c <= 0xFF; c++) codePointClass[c] = kHTTPQuotedStringToken;
+ const tokens = "!#$%&'*+-.^_`|~0123456789" +
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
+ for (let i = 0; i < tokens.length; i++) {
+ codePointClass[StringPrototypeCharCodeAt(tokens, i)] |= kHTTPToken;
+ }
+}
const kNoThrow = Symbol('kNoThrow');
+// https://fetch.spec.whatwg.org/#http-whitespace
+function isHTTPWhitespace(c) {
+ return c === 0x20 || c === 0x09 || c === 0x0A || c === 0x0D;
+}
+
+/**
+ * Returns the offset from `start` of the first code point in
+ * `str[start, end)` that is not in `cls`, or -1 if they all are.
+ * @param {string} str
+ * @param {number} start
+ * @param {number} end
+ * @param {number} cls
+ * @returns {number}
+ */
+function findInvalid(str, start, end, cls) {
+ for (let i = start; i < end; i++) {
+ const c = StringPrototypeCharCodeAt(str, i);
+ if (c > 0xFF || (codePointClass[c] & cls) === 0) return i - start;
+ }
+ return -1;
+}
+
function toASCIILower(str) {
- // eslint-disable-next-line no-control-regex
- if (!/[^\x00-\x7f]/.test(str)) return StringPrototypeToLowerCase(str);
- let result = '';
+ let hasUpper = false;
for (let i = 0; i < str.length; i++) {
- const char = str[i];
-
- result += char >= 'A' && char <= 'Z' ?
- StringPrototypeToLowerCase(char) :
- char;
+ const c = StringPrototypeCharCodeAt(str, i);
+ if (c > 0x7F) {
+ let result = '';
+ for (let j = 0; j < str.length; j++) {
+ const char = str[j];
+ result += char >= 'A' && char <= 'Z' ?
+ StringPrototypeToLowerCase(char) :
+ char;
+ }
+ return result;
+ }
+ if (c <= 0x5A && c >= 0x41) hasUpper = true;
}
- return result;
+ // Returning the input unchanged when possible avoids an allocation and
+ // keeps its cached hash for Map lookups.
+ return hasUpper ? StringPrototypeToLowerCase(str) : str;
}
-const SOLIDUS = '/';
-const SEMICOLON = ';';
-
-function parseTypeAndSubtype(str, noThrow = null) {
- // 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) {
- if (noThrow === kNoThrow) return null;
- throw new ERR_INVALID_MIME_SYNTAX('type', str, invalidTypeIndex);
+// Results of parseTypeAndSubtype(). Kept in module state to avoid allocating
+// a result object per parse; they are consumed synchronously by the caller.
+let parsedType = '';
+let parsedSubtype = '';
+let parsedParamsStart = 0;
+let parsedParamsEnd = 0;
+let failedProduction = '';
+let failedIndex = -1;
+
+/**
+ * Parses the type and subtype of a MIME type string and locates its
+ * parameters. Returns false on failure, with failedProduction and
+ * failedIndex describing the error.
+ * @see https://mimesniff.spec.whatwg.org/#parse-a-mime-type
+ * @param {string} str
+ * @returns {boolean}
+ */
+function parseTypeAndSubtype(str) {
+ const length = str.length;
+
+ // Skip leading HTTP whitespace.
+ let position = 0;
+ while (position < length &&
+ isHTTPWhitespace(StringPrototypeCharCodeAt(str, position))) {
+ position++;
}
- // 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;
+
+ // Collect the type, up to '/'.
+ const typeStart = position;
+ let invalidTypeIndex = -1;
+ let typeHasUpper = false;
+ for (; position < length; position++) {
+ const c = StringPrototypeCharCodeAt(str, position);
+ if (c === 0x2F /* / */) break;
+ if (c > 0xFF || (codePointClass[c] & kHTTPToken) === 0) {
+ if (invalidTypeIndex === -1) invalidTypeIndex = position - typeStart;
+ } else if (c <= 0x5A && c >= 0x41) {
+ typeHasUpper = true;
+ }
}
- const trimmedSubtype = StringPrototypeSlice(
- rawSubtype,
- 0,
- SafeStringPrototypeSearch(rawSubtype, START_ENDING_WHITESPACE));
- const invalidSubtypeIndex = SafeStringPrototypeSearch(trimmedSubtype,
- NOT_HTTP_TOKEN_CODE_POINT);
- if (trimmedSubtype === '' || invalidSubtypeIndex !== -1) {
- if (noThrow === kNoThrow) return null;
- throw new ERR_INVALID_MIME_SYNTAX('subtype', str, invalidSubtypeIndex);
+ if (position === typeStart || invalidTypeIndex !== -1 || position >= length) {
+ failedProduction = 'type';
+ failedIndex = invalidTypeIndex;
+ return false;
}
- const subtype = toASCIILower(trimmedSubtype);
- return [
- type,
- subtype,
- position,
- ];
+ const typeEnd = position;
+
+ // Skip '/' and collect the subtype, up to ';'. In the same pass, find the
+ // first non-token code point and where the trailing HTTP whitespace starts.
+ position++;
+ const subtypeStart = position;
+ let subtypeEnd = position;
+ let firstNonToken = -1;
+ let subtypeHasUpper = false;
+ for (; position < length; position++) {
+ const c = StringPrototypeCharCodeAt(str, position);
+ if (c === 0x3B /* ; */) break;
+ if (c <= 0xFF && (codePointClass[c] & kHTTPToken) !== 0) {
+ if (c <= 0x5A && c >= 0x41) subtypeHasUpper = true;
+ } else if (firstNonToken === -1) {
+ firstNonToken = position;
+ }
+ if (!isHTTPWhitespace(c)) subtypeEnd = position + 1;
+ }
+ const subtypeRawEnd = position;
+ // Whitespace is not a token code point, so a non-token code point at or
+ // after subtypeEnd is part of the trailing whitespace, which is removed.
+ const invalidSubtypeIndex = firstNonToken !== -1 && firstNonToken < subtypeEnd ?
+ firstNonToken - subtypeStart : -1;
+ if (subtypeEnd === subtypeStart || invalidSubtypeIndex !== -1) {
+ failedProduction = 'subtype';
+ failedIndex = invalidSubtypeIndex;
+ return false;
+ }
+
+ // Parameters are everything after the ';' up to the trailing whitespace.
+ let paramsEnd = length;
+ while (paramsEnd > subtypeRawEnd &&
+ isHTTPWhitespace(StringPrototypeCharCodeAt(str, paramsEnd - 1))) {
+ paramsEnd--;
+ }
+
+ const type = StringPrototypeSlice(str, typeStart, typeEnd);
+ const subtype = StringPrototypeSlice(str, subtypeStart, subtypeEnd);
+ // Both are ASCII-only at this point, so toLowerCase() is ASCII lowercase.
+ parsedType = typeHasUpper ? StringPrototypeToLowerCase(type) : type;
+ parsedSubtype = subtypeHasUpper ? StringPrototypeToLowerCase(subtype) : subtype;
+ parsedParamsStart = subtypeRawEnd + 1;
+ parsedParamsEnd = paramsEnd;
+ return true;
}
-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];
+/**
+ * Parses MIME type parameters from `str[position, end)` into `params`.
+ * `position` is just past the ';' that follows the subtype, and `end`
+ * excludes trailing HTTP whitespace. This is step 11 of
+ * https://mimesniff.spec.whatwg.org/#parse-a-mime-type
+ * @param {string} str
+ * @param {number} position
+ * @param {number} end
+ * @param {SafeMap<string, string>} params
+ */
+function parseParameters(str, position, end, params) {
+ while (position < end) {
+ // Skip HTTP whitespace.
+ while (position < end &&
+ isHTTPWhitespace(StringPrototypeCharCodeAt(str, position))) {
+ position++;
+ }
+
+ // Collect the parameter name, up to ';' or '='.
+ const nameStart = position;
+ let nameIsToken = true;
+ let nameHasUpper = false;
+ for (; position < end; position++) {
+ const c = StringPrototypeCharCodeAt(str, position);
+ if (c === 0x3B /* ; */ || c === 0x3D /* = */) break;
+ if (c > 0xFF || (codePointClass[c] & kHTTPToken) === 0) {
+ nameIsToken = false;
+ } else if (c <= 0x5A && c >= 0x41) {
+ nameHasUpper = true;
+ }
+ }
+ const nameEnd = position;
+
+ if (position < end) {
+ // Parameters without a value are ignored.
+ if (StringPrototypeCharCodeAt(str, position) === 0x3B /* ; */) {
+ position++;
+ continue;
+ }
+ // Skip '='.
+ position++;
+ }
+ if (position >= end) break;
+
+ let value;
+ if (StringPrototypeCharCodeAt(str, position) === 0x22 /* " */) {
+ // Collect an HTTP quoted string with the extract-value flag.
+ // https://fetch.spec.whatwg.org/#collect-an-http-quoted-string
+ position++;
+ value = '';
+ let chunkStart = position;
+ while (true) {
+ while (position < end) {
+ const c = StringPrototypeCharCodeAt(str, position);
+ if (c === 0x22 /* " */ || c === 0x5C /* \ */) break;
+ position++;
+ }
+ if (position >= end) {
+ value += StringPrototypeSlice(str, chunkStart, position);
+ break;
+ }
+ const quoteOrBackslash = StringPrototypeCharCodeAt(str, position);
+ value += StringPrototypeSlice(str, chunkStart, position);
+ position++;
+ if (quoteOrBackslash === 0x5C /* \ */) {
+ if (position >= end) {
+ value += '\\';
+ break;
+ }
+ // The escaped code point starts the next chunk.
+ chunkStart = position;
+ position++;
+ } else {
+ break;
+ }
+ }
+ // Skip anything else up to the next ';'.
+ while (position < end &&
+ StringPrototypeCharCodeAt(str, position) !== 0x3B /* ; */) {
+ position++;
+ }
+ if (findInvalid(value, 0, value.length, kHTTPQuotedStringToken) !== -1) {
+ position++;
+ continue;
+ }
} else {
- ret += c;
+ // Collect the value up to ';', validating it and finding where its
+ // trailing HTTP whitespace starts in the same pass. CR and LF are not
+ // quoted-string token code points, but trailing ones are removed, so
+ // the value is only invalid if a bad code point comes before valueEnd.
+ const valueStart = position;
+ let valueEnd = position;
+ let firstInvalid = -1;
+ for (; position < end; position++) {
+ const c = StringPrototypeCharCodeAt(str, position);
+ if (c === 0x3B /* ; */) break;
+ if (firstInvalid === -1 &&
+ (c > 0xFF || (codePointClass[c] & kHTTPQuotedStringToken) === 0)) {
+ firstInvalid = position;
+ }
+ if (!isHTTPWhitespace(c)) valueEnd = position + 1;
+ }
+ // Parameters with an empty or invalid value are ignored.
+ if (valueEnd === valueStart ||
+ (firstInvalid !== -1 && firstInvalid < valueEnd)) {
+ position++;
+ continue;
+ }
+ value = StringPrototypeSlice(str, valueStart, valueEnd);
}
+
+ if (nameEnd !== nameStart && nameIsToken) {
+ // The name is ASCII-only, so toLowerCase() is ASCII lowercase.
+ const name = nameHasUpper ?
+ StringPrototypeToLowerCase(StringPrototypeSlice(str, nameStart, nameEnd)) :
+ StringPrototypeSlice(str, nameStart, nameEnd);
+ if (!params.has(name)) params.set(name, value);
+ }
+ // Skip ';'.
+ position++;
}
- // 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++) {
@@ -123,28 +305,29 @@ function escapeQuoteOrSolidus(str) {
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}"`;
+ if (findInvalid(value, 0, value.length, kHTTPToken) === -1) return value;
+ return `"${escapeQuoteOrSolidus(value)}"`;
};
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;
+ // Parsing is deferred until the parameters are first accessed, as most
+ // users only need the type and subtype. Until then #data is null and
+ // #string[#start, #end) holds the unparsed parameters.
+ #data = null;
#string = null;
+ #start = 0;
+ #end = 0;
/**
* Used to instantiate a MIMEParams object within the MIMEType class and
* to allow it to be parsed lazily.
* @returns {MIMEParams}
*/
- static instantiateMimeParams(str) {
+ static instantiateMimeParams(str, start, end) {
const instance = new MIMEParams();
instance.#string = str;
- instance.#processed = false;
+ instance.#start = start;
+ instance.#end = end;
return instance;
}
@@ -153,31 +336,23 @@ class MIMEParams {
* @returns {void}
*/
delete(name) {
- this.#parse();
- this.#data.delete(toASCIILower(`${name}`));
+ this.#parse().delete(toASCIILower(`${name}`));
}
get(name) {
- this.#parse();
- const data = this.#data;
- name = toASCIILower(`${name}`);
- if (data.has(name)) {
- return data.get(name);
- }
- return null;
+ const value = this.#parse().get(toASCIILower(`${name}`));
+ return value === undefined ? null : value;
}
has(name) {
- this.#parse();
- return this.#data.has(toASCIILower(`${name}`));
+ return this.#parse().has(toASCIILower(`${name}`));
}
set(name, value) {
- this.#parse();
- const data = this.#data;
+ const data = this.#parse();
name = toASCIILower(`${name}`);
value = `${value}`;
- const invalidNameIndex = SafeStringPrototypeSearch(name, NOT_HTTP_TOKEN_CODE_POINT);
+ const invalidNameIndex = findInvalid(name, 0, name.length, kHTTPToken);
if (name.length === 0 || invalidNameIndex !== -1) {
throw new ERR_INVALID_MIME_SYNTAX(
'parameter name',
@@ -185,9 +360,8 @@ class MIMEParams {
invalidNameIndex,
);
}
- const invalidValueIndex = SafeStringPrototypeSearch(
- value,
- NOT_HTTP_QUOTED_STRING_CODE_POINT);
+ const invalidValueIndex = findInvalid(value, 0, value.length,
+ kHTTPQuotedStringToken);
if (invalidValueIndex !== -1) {
throw new ERR_INVALID_MIME_SYNTAX(
'parameter value',
@@ -199,24 +373,20 @@ class MIMEParams {
}
*entries() {
- this.#parse();
- yield* this.#data.entries();
+ yield* this.#parse().entries();
}
*keys() {
- this.#parse();
- yield* this.#data.keys();
+ yield* this.#parse().keys();
}
*values() {
- this.#parse();
- yield* this.#data.values();
+ yield* this.#parse().values();
}
toString() {
- this.#parse();
let ret = '';
- for (const { 0: key, 1: value } of this.#data) {
+ for (const { 0: key, 1: value } of this.#parse()) {
const encoded = encode(value);
// Ensure they are separated
if (ret.length) ret += ';';
@@ -225,99 +395,20 @@ class MIMEParams {
return ret;
}
- // Used to act as a friendly class to stringifying stuff
- // not meant to be exposed to users, could inject invalid values
+ /**
+ * Parses the deferred parameter string on first use.
+ * @returns {SafeMap<string, string>}
+ */
#parse() {
- if (this.#processed) return; // already parsed
- const paramsMap = this.#data;
- let position = 0;
+ let data = this.#data;
+ if (data !== null) return data;
+ data = this.#data = new SafeMap();
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++;
+ if (str !== null) {
+ this.#string = null;
+ parseParameters(str, this.#start, this.#end, data);
}
- this.#data = paramsMap;
- this.#processed = true;
+ return data;
}
}
const MIMEParamsStringify = MIMEParams.prototype.toString;
@@ -342,23 +433,34 @@ class MIMEType {
#subtype;
#parameters;
constructor(string, noThrowSymbol = null) {
+ // MIMEType.parse() has already parsed the string successfully.
+ if (noThrowSymbol === kNoThrow) {
+ this.#init(string);
+ return;
+ }
string = `${string}`;
- // noThrowSymbol can be null or kNoThrow, but not any other value
- if (noThrowSymbol != null && noThrowSymbol !== kNoThrow) {
+ if (noThrowSymbol != null) {
throw new ERR_ILLEGAL_CONSTRUCTOR();
}
- const data = parseTypeAndSubtype(string, noThrowSymbol);
- if (data != null) {
- this.#type = data[0];
- this.#subtype = data[1];
- this.#parameters = instantiateMimeParams(StringPrototypeSlice(string, data[2]));
+ if (!parseTypeAndSubtype(string)) {
+ throw new ERR_INVALID_MIME_SYNTAX(failedProduction, string, failedIndex);
}
+ this.#init(string);
+ }
+
+ // Consumes the results of a successful parseTypeAndSubtype(string).
+ #init(string) {
+ this.#type = parsedType;
+ this.#subtype = parsedSubtype;
+ this.#parameters = instantiateMimeParams(string, parsedParamsStart,
+ parsedParamsEnd);
}
// Like the constructor, but returns null instead of throwing on invalid input.
static parse(string) {
- const mt = new MIMEType(string, kNoThrow);
- return mt.type ? mt : null;
+ string = `${string}`;
+ if (!parseTypeAndSubtype(string)) return null;
+ return new MIMEType(string, kNoThrow);
}
get type() {
@@ -367,7 +469,7 @@ class MIMEType {
set type(v) {
v = `${v}`;
- const invalidTypeIndex = SafeStringPrototypeSearch(v, NOT_HTTP_TOKEN_CODE_POINT);
+ const invalidTypeIndex = findInvalid(v, 0, v.length, kHTTPToken);
if (v.length === 0 || invalidTypeIndex !== -1) {
throw new ERR_INVALID_MIME_SYNTAX('type', v, invalidTypeIndex);
}
@@ -380,7 +482,7 @@ class MIMEType {
set subtype(v) {
v = `${v}`;
- const invalidSubtypeIndex = SafeStringPrototypeSearch(v, NOT_HTTP_TOKEN_CODE_POINT);
+ const invalidSubtypeIndex = findInvalid(v, 0, v.length, kHTTPToken);
if (v.length === 0 || invalidSubtypeIndex !== -1) {
throw new ERR_INVALID_MIME_SYNTAX('subtype', v, invalidSubtypeIndex);
}
diff --git a/test/parallel/test-mime-whatwg.js b/test/parallel/test-mime-whatwg.js
index b61e6d620ab..399e1a33d73 100644
--- a/test/parallel/test-mime-whatwg.js
+++ b/test/parallel/test-mime-whatwg.js
@@ -11,9 +11,11 @@ function test(mimes) {
const { input, output } = entry;
if (output === null) {
assert.throws(() => new MIMEType(input), /ERR_INVALID_MIME_SYNTAX/i);
+ assert.strictEqual(MIMEType.parse(input), null);
} else {
const str = `${new MIMEType(input)}`;
assert.strictEqual(str, output);
+ assert.strictEqual(`${MIMEType.parse(input)}`, output);
}
}
}