diff --git a/benchmark/mime/mimetype-parse.js b/benchmark/mime/mimetype-parse.js new file mode 100644 index 000000000000..2aec20ceb062 --- /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 bb368a36aa56..7c7b18f088b5 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} 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} + */ #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 b61e6d620ab7..399e1a33d73b 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); } } }