From 01161bc5d3e0236620a263ef38d5c9350ae11cd8 Mon Sep 17 00:00:00 2001 From: Carlos Fuentes Date: Fri, 10 Feb 2023 14:04:56 +0100 Subject: [PATCH 01/14] chore(util): first attempt of performance improve on MIMEType class --- benchmark/util/mime-parser.js | 97 +++++++++ lib/internal/mime.js | 362 +++++++++++++++++++++++----------- 2 files changed, 349 insertions(+), 110 deletions(-) create mode 100644 benchmark/util/mime-parser.js diff --git a/benchmark/util/mime-parser.js b/benchmark/util/mime-parser.js new file mode 100644 index 000000000000..62bfc32bc2ed --- /dev/null +++ b/benchmark/util/mime-parser.js @@ -0,0 +1,97 @@ +'use strict'; + +const common = require('../common.js'); + +const inputs = [ + { + input: 'text/html;charset=gbk', + output: 'text/html;charset=gbk', + navigable: true, + encoding: 'GBK', + }, + { + input: 'TEXT/HTML;CHARSET=GBK', + output: 'text/html;charset=GBK', + navigable: true, + encoding: 'GBK', + }, + { + input: 'text/html;charset=gbk(', + output: 'text/html;charset="gbk("', + navigable: true, + encoding: null, + }, + { + input: 'text/html;x=(;charset=gbk', + output: 'text/html;x="(";charset=gbk', + navigable: true, + encoding: 'GBK', + }, + { + input: 'text/html;charset=gbk;charset=windows-1255', + output: 'text/html;charset=gbk', + navigable: true, + encoding: 'GBK', + }, + { + input: 'text/html;charset=();charset=GBK', + output: 'text/html;charset="()"', + navigable: true, + encoding: null, + }, + { + input: 'text/html;charset =gbk', + output: 'text/html', + navigable: true, + encoding: null, + }, + { + input: 'text/html ;charset=gbk', + output: 'text/html;charset=gbk', + navigable: true, + encoding: 'GBK', + }, + { + input: 'text/html; charset=gbk', + output: 'text/html;charset=gbk', + navigable: true, + encoding: 'GBK', + }, + { + input: 'text/html;charset= gbk', + output: 'text/html;charset=" gbk"', + navigable: true, + encoding: 'GBK', + }, + { + input: 'text/html;charset= "gbk"', + output: 'text/html;charset=" \\"gbk\\""', + navigable: true, + encoding: null, + }, +]; + +const strings = [ + 'text/html;charset=gbk', + 'text/html;charset=GBK', + 'text/html;charset=gbk;charset=windows-1255', + 'text/html; charset=gbk', + 'text/html;charset= "gbk"', + 'text/html ;charset=gbk', + 'application/json; charset="utf-8"', + 'text/html;x=(;charset=gbk', +]; + +const bench = common.createBenchmark(main, { + strings, + n: [1e5], +}); + +function main({ strings: string, n }) { + const { MIMEType } = require('util'); + + bench.start(); + new MIMEType(string); + bench.end(n); + // assert.ok(noDead === undefined || noDead.length > 0); +} diff --git a/lib/internal/mime.js b/lib/internal/mime.js index 4dbfcb3d9b07..9b5fb2ef1b4c 100644 --- a/lib/internal/mime.js +++ b/lib/internal/mime.js @@ -1,5 +1,4 @@ 'use strict'; - const { FunctionPrototypeCall, ObjectDefineProperty, @@ -7,9 +6,13 @@ const { SafeMap, SafeStringPrototypeSearch, StringPrototypeCharAt, + StringPrototypeTrimEnd, + StringPrototypeTrim, + StringPrototypeCharCodeAt, StringPrototypeIndexOf, StringPrototypeSlice, StringPrototypeToLowerCase, + RegExpPrototypeTest, SymbolIterator, } = primordials; const { @@ -21,7 +24,11 @@ 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 ]*$/; +const WHITESPACE_REGEX = /[\r\n\t ]/; +const ASCII_LOWER_LOWERBOUND = StringPrototypeCharCodeAt('A'); +const ASCII_LOWER_UPPERBOUND = StringPrototypeCharCodeAt('Z'); +// TODO: evaluate later function toASCIILower(str) { let result = ''; for (let i = 0; i < str.length; i++) { @@ -38,20 +45,22 @@ const SOLIDUS = '/'; const SEMICOLON = ';'; function parseTypeAndSubtype(str) { // Skip only HTTP whitespace from start - let position = SafeStringPrototypeSearch(str, END_BEGINNING_WHITESPACE); + str = StringPrototypeTrim(str); + // ### CURRENT + let position = 0; // read until '/' - const typeEnd = StringPrototypeIndexOf(str, SOLIDUS, position); - const trimmedType = typeEnd === -1 ? + const typeEnd = StringPrototypeIndexOf(str, SOLIDUS); + const mimetype = 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); + if (mimetype === '' || typeEnd === -1 || RegExpPrototypeTest( + NOT_HTTP_TOKEN_CODE_POINT, + mimetype)) { + throw new ERR_INVALID_MIME_SYNTAX('type', str, typeEnd); } // skip type and '/' position = typeEnd + 1; - const type = toASCIILower(trimmedType); + const type = toASCIILower(mimetype); // read until ';' const subtypeEnd = StringPrototypeIndexOf(str, SEMICOLON, position); const rawSubtype = subtypeEnd === -1 ? @@ -62,47 +71,118 @@ function parseTypeAndSubtype(str) { // 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) { + // const trimmedSubtype = StringPrototypeSlice( + // rawSubtype, + // 0, + // SafeStringPrototypeSearch(rawSubtype, START_ENDING_WHITESPACE)); + const trimmedSubtype = StringPrototypeTrimEnd(rawSubtype); + // const invalidSubtypeIndex = SafeStringPrototypeSearch(trimmedSubtype, + // NOT_HTTP_TOKEN_CODE_POINT); + if (trimmedSubtype === '' || RegExpPrototypeTest(NOT_HTTP_TOKEN_CODE_POINT, + trimmedSubtype)) { throw new ERR_INVALID_MIME_SYNTAX('subtype', str, trimmedSubtype); } const subtype = toASCIILower(trimmedSubtype); + // // ### NEW + // const marker = { position: 0 }; + // // read until '/' + // const type = moveUntilIterative(str, isSolidus, marker, true); + // if (type === '' || RegExpPrototypeTest( NOT_HTTP_TOKEN_CODE_POINT, + // type)) { + // throw new ERR_INVALID_MIME_SYNTAX('type', str, invalidTypeIndex); + // } + // // skip type and '/' + // marker.position += 1; + + // // read until ';' + // const subtype = moveUntilIterative(str, isSemicolon, marker, true) + // const trimmedSubtype = StringPrototypeTrimEnd(subtype); + // if (trimmedSubtype === '' || RegExpPrototypeTest(NOT_HTTP_TOKEN_CODE_POINT, + // trimmedSubtype)) { + // throw new ERR_INVALID_MIME_SYNTAX('subtype', str, trimmedSubtype); + // } + + // marker.position += 1; + return { __proto__: null, type, subtype, parametersStringIndex: position, + // parametersStringIndex: marker.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]; +// TESTING AREA +function isSemicolonOrEquals(char) { + return char === SEMICOLON || char === '='; +} + +function isQuoteOrBackslash(char) { + return char === '"' || char === '\\'; +} + +function isNotWhiteSpace(char) { + return !WHITESPACE_REGEX.test(char); +} + +function moveUntilIterative(str, predicate, marker, { toASCII, removeBackslashes } = { toASCII: false, removeBackslashes: false }) { + const start = marker.position; + const length = str.length; + let index = start; + let result = ''; + + while(predicate(str[index]) === false && index < length) { + let char = str[index]; + if (removeBackslashes && char === '\\') { + // jump to the next char + index++ + char = str[index]; + } + + if (toASCII) { + const asCharCode = StringPrototypeCharCodeAt(char) + result += asCharCode >= ASCII_LOWER_LOWERBOUND && asCharCode <= ASCII_LOWER_UPPERBOUND ? + StringPrototypeToLowerCase(char) : + char; } else { - ret += c; + result += char; } + + index++; + } - // We add the last character if we didn't skip to it. - if (i === str.length - 1) { - ret += str[i]; - } - return ret; + + marker.position = index; + + return result; } +// END-TESTING AREA + +// 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 = ''; @@ -115,8 +195,8 @@ 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 isValid = RegExpPrototypeTest(NOT_HTTP_TOKEN_CODE_POINT, value); + if (isValid) return value; const escaped = escapeQuoteOrSolidus(value); return `"${escaped}"`; }; @@ -130,10 +210,7 @@ class MIMEParams { get(name) { const data = this.#data; - if (data.has(name)) { - return data.get(name); - } - return null; + return data.get(name) ?? null; } has(name) { @@ -144,18 +221,18 @@ class MIMEParams { const data = this.#data; name = `${name}`; value = `${value}`; - const invalidNameIndex = SafeStringPrototypeSearch(name, NOT_HTTP_TOKEN_CODE_POINT); - if (name.length === 0 || invalidNameIndex !== -1) { + const invalidNameIndex = RegExpPrototypeTest(NOT_HTTP_TOKEN_CODE_POINT, name); + if (name.length === 0 || invalidNameIndex) { throw new ERR_INVALID_MIME_SYNTAX( 'parameter name', name, invalidNameIndex, ); } - const invalidValueIndex = SafeStringPrototypeSearch( - value, - NOT_HTTP_QUOTED_STRING_CODE_POINT); - if (invalidValueIndex !== -1) { + const invalidValueIndex = RegExpPrototypeTest( + NOT_HTTP_QUOTED_STRING_CODE_POINT, + value); + if (invalidValueIndex) { throw new ERR_INVALID_MIME_SYNTAX( 'parameter value', value, @@ -192,92 +269,153 @@ class MIMEParams { // not meant to be exposed to users, could inject invalid values static parseParametersString(str, position, params) { const paramsMap = params.#data; - const endOfSource = SafeStringPrototypeSearch( - StringPrototypeSlice(str, position), - START_ENDING_WHITESPACE, - ) + position; - while (position < endOfSource) { + const endOfSource = str.length; + const marker = { position }; + while (marker.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; + moveUntilIterative(str, isNotWhiteSpace, marker) + const parameterName = moveUntilIterative(str, isSemicolonOrEquals, marker, { toASCII: true }); + // If we are at end of the string, it cannot have a value + if (marker.position > endOfSource) break; + else { // Ignore parameters without values - if (char === ';') { + if (str[marker.position] === ';') { continue; + } else { + // Skip the terminating character + marker.position++; } } - // 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); + const nextChar = str[marker.position]; let parameterValue = null; - if (char === '"') { + if (nextChar === '"') { // Handle quoted-string form of values // skip '"' - position += 1; + marker.position++; // 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); + parameterValue = moveUntilIterative(str, isQuoteOrBackslash, marker, { removeBackslashes: true }); // If we did have an unmatched '\' add it back to the end - if (insideMatch[1]) parameterValue += '\\'; + if (parameterValue[marker.position] === '\\') parameterValue += '\\'; } else { // Handle the normal parameter value form - const valueEnd = StringPrototypeIndexOf(str, SEMICOLON, position); + const valueEnd = StringPrototypeIndexOf(str, SEMICOLON, marker.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), - ); + StringPrototypeSlice(str, marker.position) : + StringPrototypeSlice(str, marker.position, valueEnd); + + const trimmedValue = StringPrototypeTrimEnd(rawValue); + marker.position += trimmedValue.length; // 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 && - params.has(parameterString) === false + parameterName !== '' && + params.has(parameterName) === false && + RegExpPrototypeTest(NOT_HTTP_TOKEN_CODE_POINT, + parameterName) === false && + RegExpPrototypeTest(NOT_HTTP_QUOTED_STRING_CODE_POINT, + parameterName) === false ) { - paramsMap.set(parameterString, parameterValue); + paramsMap.set(parameterName, parameterValue); } - position++; + + marker.position++; } return paramsMap; } + // static parseParametersString(str, position, params) { + // const paramsMap = params.#data; + // 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 !== '' && + // RegExpPrototypeTest(NOT_HTTP_TOKEN_CODE_POINT, + // parameterString) === false && + // RegExpPrototypeExec(NOT_HTTP_QUOTED_STRING_CODE_POINT, + // parameterString) === false && + // params.has(parameterString) === false + // ) { + // paramsMap.set(parameterString, parameterValue); + // } + // position++; + // } + // return paramsMap; + // } } const MIMEParamsStringify = MIMEParams.prototype.toString; ObjectDefineProperty(MIMEParams.prototype, SymbolIterator, { @@ -300,12 +438,14 @@ class MIMEType { #type; #subtype; #parameters; + #essence; constructor(string) { string = `${string}`; const data = parseTypeAndSubtype(string); this.#type = data.type; this.#subtype = data.subtype; this.#parameters = new MIMEParams(); + this.#essence = `${this.#type}/${this.#subtype}`; parseParametersString( string, data.parametersStringIndex, @@ -319,11 +459,12 @@ class MIMEType { set type(v) { v = `${v}`; - const invalidTypeIndex = SafeStringPrototypeSearch(v, NOT_HTTP_TOKEN_CODE_POINT); - if (v.length === 0 || invalidTypeIndex !== -1) { + const invalidTypeIndex = RegExpPrototypeTest(NOT_HTTP_TOKEN_CODE_POINT, v); + if (v.length === 0 || invalidTypeIndex) { throw new ERR_INVALID_MIME_SYNTAX('type', v, invalidTypeIndex); } this.#type = toASCIILower(v); + this.#essence = `${this.#type}/${this.#subtype}`; } get subtype() { @@ -332,15 +473,16 @@ class MIMEType { set subtype(v) { v = `${v}`; - const invalidSubtypeIndex = SafeStringPrototypeSearch(v, NOT_HTTP_TOKEN_CODE_POINT); - if (v.length === 0 || invalidSubtypeIndex !== -1) { + const invalidSubtypeIndex = RegExpPrototypeTest(NOT_HTTP_TOKEN_CODE_POINT, v); + if (v.length === 0 || invalidSubtypeIndex) { throw new ERR_INVALID_MIME_SYNTAX('subtype', v, invalidSubtypeIndex); } this.#subtype = toASCIILower(v); + this.#essence = `${this.#type}/${this.#subtype}`; } get essence() { - return `${this.#type}/${this.#subtype}`; + return this.#essence; } get params() { @@ -348,7 +490,7 @@ class MIMEType { } toString() { - let ret = `${this.#type}/${this.#subtype}`; + let ret = this.#essence; const paramStr = FunctionPrototypeCall(MIMEParamsStringify, this.#parameters); if (paramStr.length) ret += `;${paramStr}`; return ret; From 26564da996f9febf6fb6e4cae9b40bb034055bd6 Mon Sep 17 00:00:00 2001 From: Carlos Fuentes Date: Wed, 15 Feb 2023 10:39:25 +0100 Subject: [PATCH 02/14] refactor: small set of adjustments --- lib/internal/mime.js | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/lib/internal/mime.js b/lib/internal/mime.js index 9b5fb2ef1b4c..6bdde50fb773 100644 --- a/lib/internal/mime.js +++ b/lib/internal/mime.js @@ -43,6 +43,9 @@ function toASCIILower(str) { const SOLIDUS = '/'; const SEMICOLON = ';'; +const EQUALS = '='; +const BACKSLASH = '\\'; +const QUOTE = '"'; function parseTypeAndSubtype(str) { // Skip only HTTP whitespace from start str = StringPrototypeTrim(str); @@ -118,18 +121,18 @@ const QUOTED_VALUE_PATTERN = /^(?:([\\]$)|[\\][\s\S]|[^"])*(?:(")|$)/u; // TESTING AREA function isSemicolonOrEquals(char) { - return char === SEMICOLON || char === '='; + return char === SEMICOLON || char === EQUALS; } function isQuoteOrBackslash(char) { - return char === '"' || char === '\\'; + return char === QUOTE || char === BACKSLASH; } function isNotWhiteSpace(char) { - return !WHITESPACE_REGEX.test(char); + return !RegExpPrototypeTest(WHITESPACE_REGEX, char); } -function moveUntilIterative(str, predicate, marker, { toASCII, removeBackslashes } = { toASCII: false, removeBackslashes: false }) { +function moveUntil(str, predicate, marker, { toASCII, removeBackslashes } = { toASCII: false, removeBackslashes: false }) { const start = marker.position; const length = str.length; let index = start; @@ -209,7 +212,6 @@ class MIMEParams { } get(name) { - const data = this.#data; return data.get(name) ?? null; } @@ -273,8 +275,8 @@ class MIMEParams { const marker = { position }; while (marker.position < endOfSource) { // Skip any whitespace before parameter - moveUntilIterative(str, isNotWhiteSpace, marker) - const parameterName = moveUntilIterative(str, isSemicolonOrEquals, marker, { toASCII: true }); + moveUntil(str, isNotWhiteSpace, marker) + const parameterName = moveUntil(str, isSemicolonOrEquals, marker, { toASCII: true }); // If we are at end of the string, it cannot have a value if (marker.position > endOfSource) break; else { @@ -296,7 +298,7 @@ class MIMEParams { marker.position++; // Find matching closing '"' or end of string // Unescape '\' quoted characters - parameterValue = moveUntilIterative(str, isQuoteOrBackslash, marker, { removeBackslashes: true }); + parameterValue = moveUntil(str, isQuoteOrBackslash, marker, { removeBackslashes: true }); // If we did have an unmatched '\' add it back to the end if (parameterValue[marker.position] === '\\') parameterValue += '\\'; } else { @@ -315,11 +317,11 @@ class MIMEParams { if ( parameterName !== '' && - params.has(parameterName) === false && RegExpPrototypeTest(NOT_HTTP_TOKEN_CODE_POINT, parameterName) === false && RegExpPrototypeTest(NOT_HTTP_QUOTED_STRING_CODE_POINT, - parameterName) === false + parameterName) === false && + params.has(parameterName) === false ) { paramsMap.set(parameterName, parameterValue); } @@ -441,14 +443,14 @@ class MIMEType { #essence; constructor(string) { string = `${string}`; - const data = parseTypeAndSubtype(string); - this.#type = data.type; - this.#subtype = data.subtype; + const { type, subtype, parametersStringIndex } = parseTypeAndSubtype(string); + this.#type = type; + this.#subtype = subtype; this.#parameters = new MIMEParams(); this.#essence = `${this.#type}/${this.#subtype}`; parseParametersString( string, - data.parametersStringIndex, + parametersStringIndex, this.#parameters, ); } From c34812c5a03f7f62d2dae4570267a1d676b2e8a6 Mon Sep 17 00:00:00 2001 From: Carlos Fuentes Date: Mon, 20 Feb 2023 16:27:06 +0100 Subject: [PATCH 03/14] refactor: cleanup implementation --- lib/internal/mime.js | 221 ++++++++++--------------------------------- 1 file changed, 50 insertions(+), 171 deletions(-) diff --git a/lib/internal/mime.js b/lib/internal/mime.js index 6bdde50fb773..bb863b8bbcf7 100644 --- a/lib/internal/mime.js +++ b/lib/internal/mime.js @@ -1,11 +1,8 @@ 'use strict'; const { - FunctionPrototypeCall, ObjectDefineProperty, - RegExpPrototypeExec, SafeMap, SafeStringPrototypeSearch, - StringPrototypeCharAt, StringPrototypeTrimEnd, StringPrototypeTrim, StringPrototypeCharCodeAt, @@ -22,8 +19,6 @@ const { 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 ]*$/; const WHITESPACE_REGEX = /[\r\n\t ]/; const ASCII_LOWER_LOWERBOUND = StringPrototypeCharCodeAt('A'); const ASCII_LOWER_UPPERBOUND = StringPrototypeCharCodeAt('Z'); @@ -41,6 +36,18 @@ function toASCIILower(str) { return result; } +function stringifyMIMETypeParams(data) { + let result = ''; + for (const { 0: key, 1: value } of data) { + const encoded = encode(value); + // Ensure they are separated + if (result.length > 0) result += ';'; + result += `${key}=${encoded}`; + } + + return result; +} + const SOLIDUS = '/'; const SEMICOLON = ';'; const EQUALS = '='; @@ -49,7 +56,6 @@ const QUOTE = '"'; function parseTypeAndSubtype(str) { // Skip only HTTP whitespace from start str = StringPrototypeTrim(str); - // ### CURRENT let position = 0; // read until '/' const typeEnd = StringPrototypeIndexOf(str, SOLIDUS); @@ -74,51 +80,21 @@ function parseTypeAndSubtype(str) { // skip ';' position += 1; } - // const trimmedSubtype = StringPrototypeSlice( - // rawSubtype, - // 0, - // SafeStringPrototypeSearch(rawSubtype, START_ENDING_WHITESPACE)); const trimmedSubtype = StringPrototypeTrimEnd(rawSubtype); - // const invalidSubtypeIndex = SafeStringPrototypeSearch(trimmedSubtype, - // NOT_HTTP_TOKEN_CODE_POINT); if (trimmedSubtype === '' || RegExpPrototypeTest(NOT_HTTP_TOKEN_CODE_POINT, trimmedSubtype)) { throw new ERR_INVALID_MIME_SYNTAX('subtype', str, trimmedSubtype); } const subtype = toASCIILower(trimmedSubtype); - // // ### NEW - // const marker = { position: 0 }; - // // read until '/' - // const type = moveUntilIterative(str, isSolidus, marker, true); - // if (type === '' || RegExpPrototypeTest( NOT_HTTP_TOKEN_CODE_POINT, - // type)) { - // throw new ERR_INVALID_MIME_SYNTAX('type', str, invalidTypeIndex); - // } - // // skip type and '/' - // marker.position += 1; - - // // read until ';' - // const subtype = moveUntilIterative(str, isSemicolon, marker, true) - // const trimmedSubtype = StringPrototypeTrimEnd(subtype); - // if (trimmedSubtype === '' || RegExpPrototypeTest(NOT_HTTP_TOKEN_CODE_POINT, - // trimmedSubtype)) { - // throw new ERR_INVALID_MIME_SYNTAX('subtype', str, trimmedSubtype); - // } - - // marker.position += 1; return { __proto__: null, type, subtype, parametersStringIndex: position, - // parametersStringIndex: marker.position, }; } -const EQUALS_SEMICOLON_OR_END = /[;=]|$/; -const QUOTED_VALUE_PATTERN = /^(?:([\\]$)|[\\][\s\S]|[^"])*(?:(")|$)/u; - // TESTING AREA function isSemicolonOrEquals(char) { return char === SEMICOLON || char === EQUALS; @@ -164,29 +140,6 @@ function moveUntil(str, predicate, marker, { toASCII, removeBackslashes } = { to return result; } -// END-TESTING AREA - -// 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++) { @@ -199,7 +152,7 @@ function escapeQuoteOrSolidus(str) { const encode = (value) => { if (value.length === 0) return '""'; const isValid = RegExpPrototypeTest(NOT_HTTP_TOKEN_CODE_POINT, value); - if (isValid) return value; + if (!isValid) return value; const escaped = escapeQuoteOrSolidus(value); return `"${escaped}"`; }; @@ -212,7 +165,7 @@ class MIMEParams { } get(name) { - return data.get(name) ?? null; + return this.#data.get(name) ?? null; } has(name) { @@ -223,24 +176,33 @@ class MIMEParams { const data = this.#data; name = `${name}`; value = `${value}`; - const invalidNameIndex = RegExpPrototypeTest(NOT_HTTP_TOKEN_CODE_POINT, name); - if (name.length === 0 || invalidNameIndex) { + let invalidNameIndex = -1; + + // String.prototype.search is needed if we want to + // explicitly indicate to the end-user where the error is located + if (name.length === 0 || + (invalidNameIndex = SafeStringPrototypeSearch(name, NOT_HTTP_TOKEN_CODE_POINT) + , invalidNameIndex) !== -1 + ) { throw new ERR_INVALID_MIME_SYNTAX( 'parameter name', name, invalidNameIndex, ); } - const invalidValueIndex = RegExpPrototypeTest( - NOT_HTTP_QUOTED_STRING_CODE_POINT, - value); - if (invalidValueIndex) { + + let invalidValueIndex = -1; + if (value.length > 0 && + (invalidValueIndex = SafeStringPrototypeSearch(value, NOT_HTTP_QUOTED_STRING_CODE_POINT), + invalidValueIndex) !== -1 + ) { throw new ERR_INVALID_MIME_SYNTAX( 'parameter value', value, invalidValueIndex, ); } + data.set(name, value); } @@ -257,14 +219,18 @@ class MIMEParams { } toString() { - 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; + return stringifyMIMETypeParams(this.#data); + // 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}`; + // } + + // console.log('called mimeparams', ret) + // console.log('called mimeparams from', new Error()) + // return ret; } // Used to act as a friendly class to stringifying stuff @@ -320,7 +286,7 @@ class MIMEParams { RegExpPrototypeTest(NOT_HTTP_TOKEN_CODE_POINT, parameterName) === false && RegExpPrototypeTest(NOT_HTTP_QUOTED_STRING_CODE_POINT, - parameterName) === false && + parameterValue) === false && params.has(parameterName) === false ) { paramsMap.set(parameterName, parameterValue); @@ -330,95 +296,8 @@ class MIMEParams { } return paramsMap; } - // static parseParametersString(str, position, params) { - // const paramsMap = params.#data; - // 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 !== '' && - // RegExpPrototypeTest(NOT_HTTP_TOKEN_CODE_POINT, - // parameterString) === false && - // RegExpPrototypeExec(NOT_HTTP_QUOTED_STRING_CODE_POINT, - // parameterString) === false && - // params.has(parameterString) === false - // ) { - // paramsMap.set(parameterString, parameterValue); - // } - // position++; - // } - // return paramsMap; - // } } + const MIMEParamsStringify = MIMEParams.prototype.toString; ObjectDefineProperty(MIMEParams.prototype, SymbolIterator, { __proto__: null, @@ -461,8 +340,8 @@ class MIMEType { set type(v) { v = `${v}`; - const invalidTypeIndex = RegExpPrototypeTest(NOT_HTTP_TOKEN_CODE_POINT, v); - if (v.length === 0 || invalidTypeIndex) { + 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); @@ -475,9 +354,9 @@ class MIMEType { set subtype(v) { v = `${v}`; - const invalidSubtypeIndex = RegExpPrototypeTest(NOT_HTTP_TOKEN_CODE_POINT, v); - if (v.length === 0 || invalidSubtypeIndex) { - throw new ERR_INVALID_MIME_SYNTAX('subtype', v, invalidSubtypeIndex); + const invalidTypeIndex = SafeStringPrototypeSearch(v, NOT_HTTP_TOKEN_CODE_POINT); + if (v.length === 0 || invalidTypeIndex !== -1) { + throw new ERR_INVALID_MIME_SYNTAX('subtype', v, invalidTypeIndex); } this.#subtype = toASCIILower(v); this.#essence = `${this.#type}/${this.#subtype}`; @@ -493,8 +372,8 @@ class MIMEType { toString() { let ret = this.#essence; - const paramStr = FunctionPrototypeCall(MIMEParamsStringify, this.#parameters); - if (paramStr.length) ret += `;${paramStr}`; + const paramStr = stringifyMIMETypeParams(this.#parameters.entries()); + if (paramStr.length > 0) ret += `;${paramStr}`; return ret; } } From a7c03ad51010850b6a81e34dc07a9dd1886b2333 Mon Sep 17 00:00:00 2001 From: Carlos Fuentes Date: Mon, 20 Feb 2023 17:12:05 +0100 Subject: [PATCH 04/14] fix: smaller adjustments --- lib/internal/mime.js | 45 +++++++++++++++----------------------------- 1 file changed, 15 insertions(+), 30 deletions(-) diff --git a/lib/internal/mime.js b/lib/internal/mime.js index bb863b8bbcf7..72407f4094e0 100644 --- a/lib/internal/mime.js +++ b/lib/internal/mime.js @@ -1,5 +1,6 @@ 'use strict'; const { + FunctionPrototypeCall, ObjectDefineProperty, SafeMap, SafeStringPrototypeSearch, @@ -16,14 +17,13 @@ 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 NOT_HTTP_TOKEN_CODE_POINT = /[^!#$%&'*+\-.^_`|~A-Za-z0-9]/; +const NOT_HTTP_QUOTED_STRING_CODE_POINT = /[^\t\u0020-~\u0080-\u00FF]/; const WHITESPACE_REGEX = /[\r\n\t ]/; const ASCII_LOWER_LOWERBOUND = StringPrototypeCharCodeAt('A'); const ASCII_LOWER_UPPERBOUND = StringPrototypeCharCodeAt('Z'); -// TODO: evaluate later function toASCIILower(str) { let result = ''; for (let i = 0; i < str.length; i++) { @@ -36,18 +36,6 @@ function toASCIILower(str) { return result; } -function stringifyMIMETypeParams(data) { - let result = ''; - for (const { 0: key, 1: value } of data) { - const encoded = encode(value); - // Ensure they are separated - if (result.length > 0) result += ';'; - result += `${key}=${encoded}`; - } - - return result; -} - const SOLIDUS = '/'; const SEMICOLON = ';'; const EQUALS = '='; @@ -86,7 +74,6 @@ function parseTypeAndSubtype(str) { throw new ERR_INVALID_MIME_SYNTAX('subtype', str, trimmedSubtype); } const subtype = toASCIILower(trimmedSubtype); - return { __proto__: null, type, @@ -95,7 +82,6 @@ function parseTypeAndSubtype(str) { }; } -// TESTING AREA function isSemicolonOrEquals(char) { return char === SEMICOLON || char === EQUALS; } @@ -219,18 +205,15 @@ class MIMEParams { } toString() { - return stringifyMIMETypeParams(this.#data); - // 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}`; - // } - - // console.log('called mimeparams', ret) - // console.log('called mimeparams from', new Error()) - // return ret; + let ret = ''; + for (const { 0: key, 1: value } of this.#data) { + const encoded = encode(value); + // Ensure they are separated + if (ret.length > 0) ret += ';'; + ret += `${key}=${encoded}`; + } + + return ret; } // Used to act as a friendly class to stringifying stuff @@ -276,6 +259,7 @@ class MIMEParams { const trimmedValue = StringPrototypeTrimEnd(rawValue); marker.position += trimmedValue.length; + // Ignore parameters without values if (trimmedValue === '') continue; parameterValue = trimmedValue; @@ -294,6 +278,7 @@ class MIMEParams { marker.position++; } + return paramsMap; } } @@ -372,7 +357,7 @@ class MIMEType { toString() { let ret = this.#essence; - const paramStr = stringifyMIMETypeParams(this.#parameters.entries()); + const paramStr = FunctionPrototypeCall(MIMEParamsStringify, this.#parameters); if (paramStr.length > 0) ret += `;${paramStr}`; return ret; } From 1b30086399edc45a439baa660452982968b16462 Mon Sep 17 00:00:00 2001 From: Carlos Fuentes Date: Wed, 22 Feb 2023 11:03:10 +0100 Subject: [PATCH 05/14] refactor: leftovers and linting --- benchmark/util/mime-parser.js | 70 ----------------------------------- lib/internal/mime.js | 62 ++++++++++++++++++------------- 2 files changed, 36 insertions(+), 96 deletions(-) diff --git a/benchmark/util/mime-parser.js b/benchmark/util/mime-parser.js index 62bfc32bc2ed..54a46d7717c9 100644 --- a/benchmark/util/mime-parser.js +++ b/benchmark/util/mime-parser.js @@ -2,75 +2,6 @@ const common = require('../common.js'); -const inputs = [ - { - input: 'text/html;charset=gbk', - output: 'text/html;charset=gbk', - navigable: true, - encoding: 'GBK', - }, - { - input: 'TEXT/HTML;CHARSET=GBK', - output: 'text/html;charset=GBK', - navigable: true, - encoding: 'GBK', - }, - { - input: 'text/html;charset=gbk(', - output: 'text/html;charset="gbk("', - navigable: true, - encoding: null, - }, - { - input: 'text/html;x=(;charset=gbk', - output: 'text/html;x="(";charset=gbk', - navigable: true, - encoding: 'GBK', - }, - { - input: 'text/html;charset=gbk;charset=windows-1255', - output: 'text/html;charset=gbk', - navigable: true, - encoding: 'GBK', - }, - { - input: 'text/html;charset=();charset=GBK', - output: 'text/html;charset="()"', - navigable: true, - encoding: null, - }, - { - input: 'text/html;charset =gbk', - output: 'text/html', - navigable: true, - encoding: null, - }, - { - input: 'text/html ;charset=gbk', - output: 'text/html;charset=gbk', - navigable: true, - encoding: 'GBK', - }, - { - input: 'text/html; charset=gbk', - output: 'text/html;charset=gbk', - navigable: true, - encoding: 'GBK', - }, - { - input: 'text/html;charset= gbk', - output: 'text/html;charset=" gbk"', - navigable: true, - encoding: 'GBK', - }, - { - input: 'text/html;charset= "gbk"', - output: 'text/html;charset=" \\"gbk\\""', - navigable: true, - encoding: null, - }, -]; - const strings = [ 'text/html;charset=gbk', 'text/html;charset=GBK', @@ -93,5 +24,4 @@ function main({ strings: string, n }) { bench.start(); new MIMEType(string); bench.end(n); - // assert.ok(noDead === undefined || noDead.length > 0); } diff --git a/lib/internal/mime.js b/lib/internal/mime.js index 72407f4094e0..f41c33bb3c67 100644 --- a/lib/internal/mime.js +++ b/lib/internal/mime.js @@ -69,8 +69,9 @@ function parseTypeAndSubtype(str) { position += 1; } const trimmedSubtype = StringPrototypeTrimEnd(rawSubtype); - if (trimmedSubtype === '' || RegExpPrototypeTest(NOT_HTTP_TOKEN_CODE_POINT, - trimmedSubtype)) { + if (trimmedSubtype === '' || + RegExpPrototypeTest(NOT_HTTP_TOKEN_CODE_POINT, + trimmedSubtype)) { throw new ERR_INVALID_MIME_SYNTAX('subtype', str, trimmedSubtype); } const subtype = toASCIILower(trimmedSubtype); @@ -94,31 +95,38 @@ function isNotWhiteSpace(char) { return !RegExpPrototypeTest(WHITESPACE_REGEX, char); } -function moveUntil(str, predicate, marker, { toASCII, removeBackslashes } = { toASCII: false, removeBackslashes: false }) { +function moveUntil(str, predicate, marker, { + toASCII, + removeBackslashes, +} = { + toASCII: false, + removeBackslashes: false, +}, +) { const start = marker.position; const length = str.length; let index = start; let result = ''; - while(predicate(str[index]) === false && index < length) { + while (predicate(str[index]) === false && index < length) { let char = str[index]; if (removeBackslashes && char === '\\') { - // jump to the next char - index++ + // Jump to the next char + index++; char = str[index]; } if (toASCII) { - const asCharCode = StringPrototypeCharCodeAt(char) - result += asCharCode >= ASCII_LOWER_LOWERBOUND && asCharCode <= ASCII_LOWER_UPPERBOUND ? - StringPrototypeToLowerCase(char) : - char; + const asCharCode = StringPrototypeCharCodeAt(char); + result += asCharCode >= ASCII_LOWER_LOWERBOUND && + asCharCode <= ASCII_LOWER_UPPERBOUND ? + StringPrototypeToLowerCase(char) : + char; } else { result += char; } index++; - } marker.position = index; @@ -180,7 +188,7 @@ class MIMEParams { let invalidValueIndex = -1; if (value.length > 0 && (invalidValueIndex = SafeStringPrototypeSearch(value, NOT_HTTP_QUOTED_STRING_CODE_POINT), - invalidValueIndex) !== -1 + invalidValueIndex) !== -1 ) { throw new ERR_INVALID_MIME_SYNTAX( 'parameter value', @@ -224,20 +232,18 @@ class MIMEParams { const marker = { position }; while (marker.position < endOfSource) { // Skip any whitespace before parameter - moveUntil(str, isNotWhiteSpace, marker) + moveUntil(str, isNotWhiteSpace, marker); const parameterName = moveUntil(str, isSemicolonOrEquals, marker, { toASCII: true }); // If we are at end of the string, it cannot have a value if (marker.position > endOfSource) break; - else { - // Ignore parameters without values - if (str[marker.position] === ';') { - continue; - } else { - // Skip the terminating character - marker.position++; - } + // Ignore parameters without values + else if (str[marker.position] === ';') { + continue; + } else { + // Skip the terminating character + marker.position++; } - + // Safe to use because we never do special actions for surrogate pairs const nextChar = str[marker.position]; let parameterValue = null; @@ -267,10 +273,14 @@ class MIMEParams { if ( parameterName !== '' && - RegExpPrototypeTest(NOT_HTTP_TOKEN_CODE_POINT, - parameterName) === false && - RegExpPrototypeTest(NOT_HTTP_QUOTED_STRING_CODE_POINT, - parameterValue) === false && + RegExpPrototypeTest( + NOT_HTTP_TOKEN_CODE_POINT, + parameterName, + ) === false && + RegExpPrototypeTest( + NOT_HTTP_QUOTED_STRING_CODE_POINT, + parameterValue, + ) === false && params.has(parameterName) === false ) { paramsMap.set(parameterName, parameterValue); From c7eb5e3a35dfd76934d224df8e4260f0609940c4 Mon Sep 17 00:00:00 2001 From: Carlos Fuentes Date: Wed, 22 Feb 2023 14:27:23 +0100 Subject: [PATCH 06/14] refactor: use charcode instead of string check --- lib/internal/mime.js | 74 +++++++++++++++++++++++++++----------------- 1 file changed, 45 insertions(+), 29 deletions(-) diff --git a/lib/internal/mime.js b/lib/internal/mime.js index f41c33bb3c67..a13533a0250b 100644 --- a/lib/internal/mime.js +++ b/lib/internal/mime.js @@ -23,24 +23,28 @@ const NOT_HTTP_QUOTED_STRING_CODE_POINT = /[^\t\u0020-~\u0080-\u00FF]/; const WHITESPACE_REGEX = /[\r\n\t ]/; const ASCII_LOWER_LOWERBOUND = StringPrototypeCharCodeAt('A'); const ASCII_LOWER_UPPERBOUND = StringPrototypeCharCodeAt('Z'); +const SEMICOLON_CHARCODE = StringPrototypeCharCodeAt(';'); +const EQUALS_CHARCODE = StringPrototypeCharCodeAt('='); +const BACKSLASH_CHARCODE = StringPrototypeCharCodeAt('\\'); +const QUOTE_CHARCODE = StringPrototypeCharCodeAt('"'); +const SOLIDUS = '/'; +const SEMICOLON = ';'; function toASCIILower(str) { let result = ''; for (let i = 0; i < str.length; i++) { const char = str[i]; + const charCode = StringPrototypeCharCodeAt(str[i], 0); - result += char >= 'A' && char <= 'Z' ? - StringPrototypeToLowerCase(char) : - char; + result += charCode >= ASCII_LOWER_LOWERBOUND && + charCode <= ASCII_LOWER_UPPERBOUND ? + StringPrototypeToLowerCase(charCode) : + char; } return result; } -const SOLIDUS = '/'; -const SEMICOLON = ';'; -const EQUALS = '='; -const BACKSLASH = '\\'; -const QUOTE = '"'; + function parseTypeAndSubtype(str) { // Skip only HTTP whitespace from start str = StringPrototypeTrim(str); @@ -48,9 +52,9 @@ function parseTypeAndSubtype(str) { // read until '/' const typeEnd = StringPrototypeIndexOf(str, SOLIDUS); const mimetype = typeEnd === -1 ? - StringPrototypeSlice(str, position) : + str : StringPrototypeSlice(str, position, typeEnd); - if (mimetype === '' || typeEnd === -1 || RegExpPrototypeTest( + if (typeEnd === -1 || mimetype.length === 0 || RegExpPrototypeTest( NOT_HTTP_TOKEN_CODE_POINT, mimetype)) { throw new ERR_INVALID_MIME_SYNTAX('type', str, typeEnd); @@ -61,7 +65,7 @@ function parseTypeAndSubtype(str) { // read until ';' const subtypeEnd = StringPrototypeIndexOf(str, SEMICOLON, position); const rawSubtype = subtypeEnd === -1 ? - StringPrototypeSlice(str, position) : + str : StringPrototypeSlice(str, position, subtypeEnd); position += rawSubtype.length; if (subtypeEnd !== -1) { @@ -69,7 +73,7 @@ function parseTypeAndSubtype(str) { position += 1; } const trimmedSubtype = StringPrototypeTrimEnd(rawSubtype); - if (trimmedSubtype === '' || + if (trimmedSubtype.length === 0 || RegExpPrototypeTest(NOT_HTTP_TOKEN_CODE_POINT, trimmedSubtype)) { throw new ERR_INVALID_MIME_SYNTAX('subtype', str, trimmedSubtype); @@ -84,11 +88,13 @@ function parseTypeAndSubtype(str) { } function isSemicolonOrEquals(char) { - return char === SEMICOLON || char === EQUALS; + const code = StringPrototypeCharCodeAt(char, 0) + return code === SEMICOLON_CHARCODE || code === EQUALS_CHARCODE; } function isQuoteOrBackslash(char) { - return char === QUOTE || char === BACKSLASH; + const code = StringPrototypeCharCodeAt(char, 0) + return code === QUOTE_CHARCODE || code === BACKSLASH_CHARCODE; } function isNotWhiteSpace(char) { @@ -107,17 +113,25 @@ function moveUntil(str, predicate, marker, { const length = str.length; let index = start; let result = ''; - - while (predicate(str[index]) === false && index < length) { - let char = str[index]; - if (removeBackslashes && char === '\\') { + let char = str[index]; + + while (predicate(char) === false && index < length) { + let asCharCode; + let controlCharCode; + // We lazily assign it if needed, as not all the iterations + // might require it + if (removeBackslashes && ( + asCharCode = StringPrototypeCharCodeAt(char, 0) + ) === BACKSLASH_CHARCODE + ) { // Jump to the next char index++; char = str[index]; + controlCharCode = StringPrototypeCharCodeAt(char, 0); } if (toASCII) { - const asCharCode = StringPrototypeCharCodeAt(char); + asCharCode = controlCharCode != null ? controlCharCode : asCharCode || StringPrototypeCharCodeAt(char, 0) result += asCharCode >= ASCII_LOWER_LOWERBOUND && asCharCode <= ASCII_LOWER_UPPERBOUND ? StringPrototypeToLowerCase(char) : @@ -127,6 +141,7 @@ function moveUntil(str, predicate, marker, { } index++; + char = str[index]; } marker.position = index; @@ -138,7 +153,8 @@ function escapeQuoteOrSolidus(str) { let result = ''; for (let i = 0; i < str.length; i++) { const char = str[i]; - result += (char === '"' || char === '\\') ? `\\${char}` : char; + const code = StringPrototypeCharCodeAt(char, 0) + result += (code === QUOTE_CHARCODE || char === BACKSLASH_CHARCODE) ? `\\${char}` : char; } return result; } @@ -237,7 +253,7 @@ class MIMEParams { // If we are at end of the string, it cannot have a value if (marker.position > endOfSource) break; // Ignore parameters without values - else if (str[marker.position] === ';') { + else if (StringPrototypeCharCodeAt(str, marker.position) === SEMICOLON_CHARCODE) { continue; } else { // Skip the terminating character @@ -245,9 +261,9 @@ class MIMEParams { } // Safe to use because we never do special actions for surrogate pairs - const nextChar = str[marker.position]; - let parameterValue = null; - if (nextChar === '"') { + const nextChar = StringPrototypeCharCodeAt(str, marker.position); + let parameterValue = ''; + if (nextChar === QUOTE_CHARCODE) { // Handle quoted-string form of values // skip '"' marker.position++; @@ -255,7 +271,7 @@ class MIMEParams { // Unescape '\' quoted characters parameterValue = moveUntil(str, isQuoteOrBackslash, marker, { removeBackslashes: true }); // If we did have an unmatched '\' add it back to the end - if (parameterValue[marker.position] === '\\') parameterValue += '\\'; + if (StringPrototypeCharCodeAt(parameterValue, marker.position) === '\\') parameterValue += '\\'; } else { // Handle the normal parameter value form const valueEnd = StringPrototypeIndexOf(str, SEMICOLON, marker.position); @@ -267,12 +283,12 @@ class MIMEParams { marker.position += trimmedValue.length; // Ignore parameters without values - if (trimmedValue === '') continue; + if (trimmedValue.length === 0) continue; parameterValue = trimmedValue; } if ( - parameterName !== '' && + parameterName.length !== 0 && RegExpPrototypeTest( NOT_HTTP_TOKEN_CODE_POINT, parameterName, @@ -336,7 +352,7 @@ class MIMEType { set type(v) { v = `${v}`; const invalidTypeIndex = SafeStringPrototypeSearch(v, NOT_HTTP_TOKEN_CODE_POINT); - if (v.length === 0 || invalidTypeIndex !== -1) { + if (invalidTypeIndex !== -1 || v.length === 0) { throw new ERR_INVALID_MIME_SYNTAX('type', v, invalidTypeIndex); } this.#type = toASCIILower(v); @@ -350,7 +366,7 @@ class MIMEType { set subtype(v) { v = `${v}`; const invalidTypeIndex = SafeStringPrototypeSearch(v, NOT_HTTP_TOKEN_CODE_POINT); - if (v.length === 0 || invalidTypeIndex !== -1) { + if (invalidTypeIndex !== -1 || v.length === 0) { throw new ERR_INVALID_MIME_SYNTAX('subtype', v, invalidTypeIndex); } this.#subtype = toASCIILower(v); From c6896c30d50d5c05355833f537ba307af4d9c8e5 Mon Sep 17 00:00:00 2001 From: Carlos Fuentes Date: Fri, 24 Feb 2023 11:08:01 +0100 Subject: [PATCH 07/14] refactor: remove else branching --- lib/internal/mime.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/internal/mime.js b/lib/internal/mime.js index a13533a0250b..af02b1bda5c7 100644 --- a/lib/internal/mime.js +++ b/lib/internal/mime.js @@ -253,13 +253,13 @@ class MIMEParams { // If we are at end of the string, it cannot have a value if (marker.position > endOfSource) break; // Ignore parameters without values - else if (StringPrototypeCharCodeAt(str, marker.position) === SEMICOLON_CHARCODE) { + if (StringPrototypeCharCodeAt(str, marker.position) === SEMICOLON_CHARCODE) { continue; - } else { - // Skip the terminating character - marker.position++; } + // Skip the terminating character + marker.position++; + // Safe to use because we never do special actions for surrogate pairs const nextChar = StringPrototypeCharCodeAt(str, marker.position); let parameterValue = ''; From e94628ea8f8ff41b170eb6580131c45535d4ee52 Mon Sep 17 00:00:00 2001 From: Carlos Fuentes Date: Wed, 1 Mar 2023 23:31:39 +0100 Subject: [PATCH 08/14] fix: bad encoding --- lib/internal/mime.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/internal/mime.js b/lib/internal/mime.js index af02b1bda5c7..bd5b45b803cf 100644 --- a/lib/internal/mime.js +++ b/lib/internal/mime.js @@ -38,7 +38,7 @@ function toASCIILower(str) { result += charCode >= ASCII_LOWER_LOWERBOUND && charCode <= ASCII_LOWER_UPPERBOUND ? - StringPrototypeToLowerCase(charCode) : + StringPrototypeToLowerCase(char) : char; } return result; @@ -151,6 +151,7 @@ function moveUntil(str, predicate, marker, { function escapeQuoteOrSolidus(str) { let result = ''; + for (let i = 0; i < str.length; i++) { const char = str[i]; const code = StringPrototypeCharCodeAt(char, 0) From 8d8a023fd5d7dab62a19bd210c1936f69767a705 Mon Sep 17 00:00:00 2001 From: Carlos Fuentes Date: Fri, 3 Mar 2023 10:26:06 +0100 Subject: [PATCH 09/14] fix: linting issues --- lib/internal/mime.js | 18 ++++++++++-------- test/parallel/test-mime-whatwg.js | 2 +- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/lib/internal/mime.js b/lib/internal/mime.js index bd5b45b803cf..596b6da3c9d3 100644 --- a/lib/internal/mime.js +++ b/lib/internal/mime.js @@ -38,8 +38,8 @@ function toASCIILower(str) { result += charCode >= ASCII_LOWER_LOWERBOUND && charCode <= ASCII_LOWER_UPPERBOUND ? - StringPrototypeToLowerCase(char) : - char; + StringPrototypeToLowerCase(char) : + char; } return result; } @@ -88,12 +88,12 @@ function parseTypeAndSubtype(str) { } function isSemicolonOrEquals(char) { - const code = StringPrototypeCharCodeAt(char, 0) + const code = StringPrototypeCharCodeAt(char, 0); return code === SEMICOLON_CHARCODE || code === EQUALS_CHARCODE; } function isQuoteOrBackslash(char) { - const code = StringPrototypeCharCodeAt(char, 0) + const code = StringPrototypeCharCodeAt(char, 0); return code === QUOTE_CHARCODE || code === BACKSLASH_CHARCODE; } @@ -121,8 +121,8 @@ function moveUntil(str, predicate, marker, { // We lazily assign it if needed, as not all the iterations // might require it if (removeBackslashes && ( - asCharCode = StringPrototypeCharCodeAt(char, 0) - ) === BACKSLASH_CHARCODE + asCharCode = StringPrototypeCharCodeAt(char, 0) + ) === BACKSLASH_CHARCODE ) { // Jump to the next char index++; @@ -131,7 +131,9 @@ function moveUntil(str, predicate, marker, { } if (toASCII) { - asCharCode = controlCharCode != null ? controlCharCode : asCharCode || StringPrototypeCharCodeAt(char, 0) + asCharCode = controlCharCode != null ? + controlCharCode : + asCharCode || StringPrototypeCharCodeAt(char, 0); result += asCharCode >= ASCII_LOWER_LOWERBOUND && asCharCode <= ASCII_LOWER_UPPERBOUND ? StringPrototypeToLowerCase(char) : @@ -154,7 +156,7 @@ function escapeQuoteOrSolidus(str) { for (let i = 0; i < str.length; i++) { const char = str[i]; - const code = StringPrototypeCharCodeAt(char, 0) + const code = StringPrototypeCharCodeAt(char, 0); result += (code === QUOTE_CHARCODE || char === BACKSLASH_CHARCODE) ? `\\${char}` : char; } return result; diff --git a/test/parallel/test-mime-whatwg.js b/test/parallel/test-mime-whatwg.js index b61e6d620ab7..e19ee3ca12bf 100644 --- a/test/parallel/test-mime-whatwg.js +++ b/test/parallel/test-mime-whatwg.js @@ -9,7 +9,7 @@ function test(mimes) { for (const entry of mimes) { if (typeof entry === 'string') continue; const { input, output } = entry; - if (output === null) { + if (output == null) { assert.throws(() => new MIMEType(input), /ERR_INVALID_MIME_SYNTAX/i); } else { const str = `${new MIMEType(input)}`; From 3951cf527bd848efbeafd9b2e4256496777eeffb Mon Sep 17 00:00:00 2001 From: Carlos Fuentes Date: Wed, 8 Mar 2023 11:05:16 +0100 Subject: [PATCH 10/14] refactor: revert to string#search --- lib/internal/mime.js | 40 ++++++++++++++++++---------------------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/lib/internal/mime.js b/lib/internal/mime.js index 596b6da3c9d3..32af6cd4ed1f 100644 --- a/lib/internal/mime.js +++ b/lib/internal/mime.js @@ -10,7 +10,6 @@ const { StringPrototypeIndexOf, StringPrototypeSlice, StringPrototypeToLowerCase, - RegExpPrototypeTest, SymbolIterator, } = primordials; const { @@ -54,9 +53,8 @@ function parseTypeAndSubtype(str) { const mimetype = typeEnd === -1 ? str : StringPrototypeSlice(str, position, typeEnd); - if (typeEnd === -1 || mimetype.length === 0 || RegExpPrototypeTest( - NOT_HTTP_TOKEN_CODE_POINT, - mimetype)) { + if (typeEnd === -1 || mimetype.length === 0 || SafeStringPrototypeSearch( + mimetype, NOT_HTTP_TOKEN_CODE_POINT) !== -1) { throw new ERR_INVALID_MIME_SYNTAX('type', str, typeEnd); } // skip type and '/' @@ -74,8 +72,8 @@ function parseTypeAndSubtype(str) { } const trimmedSubtype = StringPrototypeTrimEnd(rawSubtype); if (trimmedSubtype.length === 0 || - RegExpPrototypeTest(NOT_HTTP_TOKEN_CODE_POINT, - trimmedSubtype)) { + SafeStringPrototypeSearch(trimmedSubtype, + NOT_HTTP_TOKEN_CODE_POINT) !== -1) { throw new ERR_INVALID_MIME_SYNTAX('subtype', str, trimmedSubtype); } const subtype = toASCIILower(trimmedSubtype); @@ -97,10 +95,6 @@ function isQuoteOrBackslash(char) { return code === QUOTE_CHARCODE || code === BACKSLASH_CHARCODE; } -function isNotWhiteSpace(char) { - return !RegExpPrototypeTest(WHITESPACE_REGEX, char); -} - function moveUntil(str, predicate, marker, { toASCII, removeBackslashes, @@ -115,7 +109,7 @@ function moveUntil(str, predicate, marker, { let result = ''; let char = str[index]; - while (predicate(char) === false && index < length) { + while (index < length && predicate(char) === false) { let asCharCode; let controlCharCode; // We lazily assign it if needed, as not all the iterations @@ -164,8 +158,8 @@ function escapeQuoteOrSolidus(str) { const encode = (value) => { if (value.length === 0) return '""'; - const isValid = RegExpPrototypeTest(NOT_HTTP_TOKEN_CODE_POINT, value); - if (!isValid) return value; + const shouldEncode = SafeStringPrototypeSearch(value, NOT_HTTP_TOKEN_CODE_POINT) !== -1; + if (!shouldEncode) return value; const escaped = escapeQuoteOrSolidus(value); return `"${escaped}"`; }; @@ -236,7 +230,7 @@ class MIMEParams { for (const { 0: key, 1: value } of this.#data) { const encoded = encode(value); // Ensure they are separated - if (ret.length > 0) ret += ';'; + if (ret.length !== 0) ret += ';'; ret += `${key}=${encoded}`; } @@ -251,7 +245,10 @@ class MIMEParams { const marker = { position }; while (marker.position < endOfSource) { // Skip any whitespace before parameter - moveUntil(str, isNotWhiteSpace, marker); + const endOfWhitespace = SafeStringPrototypeSearch(str, WHITESPACE_REGEX); + + marker.position = endOfWhitespace > 0 ? endOfWhitespace + 1 : marker.position; + const parameterName = moveUntil(str, isSemicolonOrEquals, marker, { toASCII: true }); // If we are at end of the string, it cannot have a value if (marker.position > endOfSource) break; @@ -263,7 +260,6 @@ class MIMEParams { // Skip the terminating character marker.position++; - // Safe to use because we never do special actions for surrogate pairs const nextChar = StringPrototypeCharCodeAt(str, marker.position); let parameterValue = ''; if (nextChar === QUOTE_CHARCODE) { @@ -292,14 +288,14 @@ class MIMEParams { if ( parameterName.length !== 0 && - RegExpPrototypeTest( - NOT_HTTP_TOKEN_CODE_POINT, + SafeStringPrototypeSearch( parameterName, - ) === false && - RegExpPrototypeTest( - NOT_HTTP_QUOTED_STRING_CODE_POINT, + NOT_HTTP_TOKEN_CODE_POINT, + ) === -1 && + SafeStringPrototypeSearch( parameterValue, - ) === false && + NOT_HTTP_QUOTED_STRING_CODE_POINT, + ) === -1 && params.has(parameterName) === false ) { paramsMap.set(parameterName, parameterValue); From e2eca5b118630c239a8dfb18552aad973484b570 Mon Sep 17 00:00:00 2001 From: Carlos Fuentes Date: Wed, 22 Mar 2023 12:59:02 +0100 Subject: [PATCH 11/14] refactor: take explicit steps from standard --- lib/internal/mime.js | 202 +++++++++++++++++++++++++++++++------------ 1 file changed, 147 insertions(+), 55 deletions(-) diff --git a/lib/internal/mime.js b/lib/internal/mime.js index 32af6cd4ed1f..4447a1393638 100644 --- a/lib/internal/mime.js +++ b/lib/internal/mime.js @@ -19,21 +19,22 @@ const { const NOT_HTTP_TOKEN_CODE_POINT = /[^!#$%&'*+\-.^_`|~A-Za-z0-9]/; const NOT_HTTP_QUOTED_STRING_CODE_POINT = /[^\t\u0020-~\u0080-\u00FF]/; -const WHITESPACE_REGEX = /[\r\n\t ]/; +const SOLIDUS = '/'; +const WHITESPACE_REGEX = /(^[\r\n\t ])/; +const SEMICOLON = ';'; +const BACKSLASH = '\\'; +const QUOTE = '"'; const ASCII_LOWER_LOWERBOUND = StringPrototypeCharCodeAt('A'); const ASCII_LOWER_UPPERBOUND = StringPrototypeCharCodeAt('Z'); -const SEMICOLON_CHARCODE = StringPrototypeCharCodeAt(';'); +const SEMICOLON_CHARCODE = StringPrototypeCharCodeAt(SEMICOLON); const EQUALS_CHARCODE = StringPrototypeCharCodeAt('='); -const BACKSLASH_CHARCODE = StringPrototypeCharCodeAt('\\'); -const QUOTE_CHARCODE = StringPrototypeCharCodeAt('"'); -const SOLIDUS = '/'; -const SEMICOLON = ';'; - +const BACKSLASH_CHARCODE = StringPrototypeCharCodeAt(BACKSLASH); +const QUOTE_CHARCODE = StringPrototypeCharCodeAt(QUOTE); function toASCIILower(str) { let result = ''; for (let i = 0; i < str.length; i++) { const char = str[i]; - const charCode = StringPrototypeCharCodeAt(str[i], 0); + const charCode = StringPrototypeCharCodeAt(char, 0); result += charCode >= ASCII_LOWER_LOWERBOUND && charCode <= ASCII_LOWER_UPPERBOUND ? @@ -43,40 +44,112 @@ function toASCIILower(str) { return result; } +// https://fetch.spec.whatwg.org/#collect-an-http-quoted-string +function collectHTTPQuotedString(str, marker) { + const limit = str.length; + let index = marker.position; + // #2 Let value be the empty string. + let value = ''; + + // #4 Advance position by 1. + index++; + + // #5 While true + while (true) { + let nextDelimiter = StringPrototypeIndexOf(str, BACKSLASH, index); + + nextDelimiter = nextDelimiter !== -1 ? nextDelimiter : StringPrototypeIndexOf(str, QUOTE, index); + + // #5-1 Append the result of collecting a sequence of code points + // that are not U+0022 (") or U+005C (\) from input, given position, to value. + + const collectedPoints = nextDelimiter === -1 ? StringPrototypeSlice(str, index) : + StringPrototypeSlice(str, index, nextDelimiter); + + index += collectedPoints.length; + value += collectedPoints; + + // #5-2 If position is past the end of input, then break. + if (index > limit) break; + + // #5-3 Let quoteOrBackslash be the code point at position within input. + const quoteOrBackslash = StringPrototypeCharCodeAt(str, index); + + // #5-4 Advance position by 1. + index++; + + // #5-5 If quoteOrBackslash is U+005C (\), then: + if (quoteOrBackslash === BACKSLASH_CHARCODE) { + // #5-5-1 If position is past the end of input, then append U+005C (\) to value and break. + if (index > limit) { + value += BACKSLASH; + break; + } + + // #5-5-2 Append the code point at position within input to value. + value += str[index]; + + // #5-5-3 AAdvance position by 1. + index++; + } + + // #5-6 Otherwise + // #5-6-1 Assert: quoteOrBackslash is U+0022 ("). + // #5-6-2 Break + if (quoteOrBackslash === QUOTE_CHARCODE) break; + } + + marker.position = index; + + // #6 If the extract-value flag is set, then return value + return value; +} + function parseTypeAndSubtype(str) { - // Skip only HTTP whitespace from start - str = StringPrototypeTrim(str); + const strLength = str.length; + // #2 Let position be a position variable for input, initially pointg at + // the start of the input let position = 0; - // read until '/' + + // #3 Let type be the result of collecting a sequence of code points that are + // not U+002F (/ [SOLIDUS]) from input, given position. const typeEnd = StringPrototypeIndexOf(str, SOLIDUS); - const mimetype = typeEnd === -1 ? + const preType = typeEnd === -1 ? str : StringPrototypeSlice(str, position, typeEnd); - if (typeEnd === -1 || mimetype.length === 0 || SafeStringPrototypeSearch( - mimetype, NOT_HTTP_TOKEN_CODE_POINT) !== -1) { + + // #4 If type is the empty string or does not solely + // contain HTTP token code points, then return failure. + // #5 If position is past the end of input, then return failure. + if (typeEnd === -1 || preType.length === 0 || typeEnd > strLength || SafeStringPrototypeSearch( + preType, NOT_HTTP_TOKEN_CODE_POINT) !== -1) { throw new ERR_INVALID_MIME_SYNTAX('type', str, typeEnd); } - // skip type and '/' + + // #6 Advance position by 1. (This skips past U+002F (/).) position = typeEnd + 1; - const type = toASCIILower(mimetype); - // read until ';' - const subtypeEnd = StringPrototypeIndexOf(str, SEMICOLON, position); - const rawSubtype = subtypeEnd === -1 ? - str : - StringPrototypeSlice(str, position, subtypeEnd); + + // #7 Let subtype be the result of collecting a sequence of code points + // that are not U+003B (;) from input, given position. + let subtypeEnd = StringPrototypeIndexOf(str, SEMICOLON, position); + subtypeEnd = subtypeEnd === -1 ? strLength : subtypeEnd; + const rawSubtype = StringPrototypeSlice(str, position, subtypeEnd); position += rawSubtype.length; - if (subtypeEnd !== -1) { - // skip ';' - position += 1; - } - const trimmedSubtype = StringPrototypeTrimEnd(rawSubtype); - if (trimmedSubtype.length === 0 || - SafeStringPrototypeSearch(trimmedSubtype, + + // #8 Remove any trailing HTTP whitespace from subtype. + const preSubType = StringPrototypeTrimEnd(rawSubtype); + + // #9 If subtype is the empty string or does not solely contain + // HTTP token code points, then return failure. + if (preSubType.length === 0 || + SafeStringPrototypeSearch(preSubType, NOT_HTTP_TOKEN_CODE_POINT) !== -1) { - throw new ERR_INVALID_MIME_SYNTAX('subtype', str, trimmedSubtype); + throw new ERR_INVALID_MIME_SYNTAX('subtype', str, subtypeEnd); } - const subtype = toASCIILower(trimmedSubtype); + + const type = toASCIILower(preType); + const subtype = toASCIILower(preSubType); return { __proto__: null, type, @@ -90,11 +163,6 @@ function isSemicolonOrEquals(char) { return code === SEMICOLON_CHARCODE || code === EQUALS_CHARCODE; } -function isQuoteOrBackslash(char) { - const code = StringPrototypeCharCodeAt(char, 0); - return code === QUOTE_CHARCODE || code === BACKSLASH_CHARCODE; -} - function moveUntil(str, predicate, marker, { toASCII, removeBackslashes, @@ -243,49 +311,71 @@ class MIMEParams { const paramsMap = params.#data; const endOfSource = str.length; const marker = { position }; + + // #11 While position is not past the end of input: while (marker.position < endOfSource) { - // Skip any whitespace before parameter - const endOfWhitespace = SafeStringPrototypeSearch(str, WHITESPACE_REGEX); - marker.position = endOfWhitespace > 0 ? endOfWhitespace + 1 : marker.position; + // #11-2 Advance position by 1. (This skips past U+003B (; [SEMICOLON]).) + marker.position++; + + // #11-3 Collect a sequence of code points that are HTTP whitespace from input given position. + const sliced = StringPrototypeSlice(str, marker.position); + const endOfWhitespace = SafeStringPrototypeSearch(sliced, WHITESPACE_REGEX); + marker.position += endOfWhitespace + 1; const parameterName = moveUntil(str, isSemicolonOrEquals, marker, { toASCII: true }); - // If we are at end of the string, it cannot have a value + + // #11-6 If position is past the end of input, then break. if (marker.position > endOfSource) break; - // Ignore parameters without values + // #11-5-1 If the code point at position within input is U+003B (;), then continue. if (StringPrototypeCharCodeAt(str, marker.position) === SEMICOLON_CHARCODE) { continue; } - // Skip the terminating character + // #11-5-2 Advance position by 1. (This skips past U+003D (=).) marker.position++; const nextChar = StringPrototypeCharCodeAt(str, marker.position); - let parameterValue = ''; + + // #11-7 Let parameterValue be null. + let parameterValue = null; + // #11-8 If the code point at position within input is U+0022 ("), then: if (nextChar === QUOTE_CHARCODE) { - // Handle quoted-string form of values - // skip '"' - marker.position++; - // Find matching closing '"' or end of string + // #11-8-1 If the code point at position within input is U+0022 ("), then: // Unescape '\' quoted characters - parameterValue = moveUntil(str, isQuoteOrBackslash, marker, { removeBackslashes: true }); - // If we did have an unmatched '\' add it back to the end - if (StringPrototypeCharCodeAt(parameterValue, marker.position) === '\\') parameterValue += '\\'; + parameterValue = collectHTTPQuotedString(str, marker); + + // #11-8-1 Collect a sequence of code points that are not U+003B (;) from input, given position. + const remaining = StringPrototypeIndexOf(str, SEMICOLON, marker.position); + marker.position += remaining !== -1 ? remaining - marker.position : 0; } else { - // Handle the normal parameter value form + // #11-9-1 Set parameterValue to the result of collecting a sequence of code points + // that are not U+003B (;) from input, given position. const valueEnd = StringPrototypeIndexOf(str, SEMICOLON, marker.position); const rawValue = valueEnd === -1 ? StringPrototypeSlice(str, marker.position) : StringPrototypeSlice(str, marker.position, valueEnd); + // #11-9-2 Remove any trailing HTTP whitespace from parameterValue. const trimmedValue = StringPrototypeTrimEnd(rawValue); marker.position += trimmedValue.length; - // Ignore parameters without values + // #11-9-3 If parameterValue is the empty string, then continue. if (trimmedValue.length === 0) continue; + parameterValue = trimmedValue; } + /** + * #11-10 if: + * + * parameterName is not the empty string + * parameterName solely contains HTTP token code points + * parameterValue solely contains HTTP quoted-string token code points + * mimeTypes parameters[parameterName] does not exist + * + * then set mimeType's parameters[parameterName] to parameterValue. + */ if ( parameterName.length !== 0 && SafeStringPrototypeSearch( @@ -300,8 +390,6 @@ class MIMEParams { ) { paramsMap.set(parameterName, parameterValue); } - - marker.position++; } return paramsMap; @@ -331,12 +419,16 @@ class MIMEType { #parameters; #essence; constructor(string) { - string = `${string}`; + // #1 Remove any leading and trailing HTTP whitespace from input + string = StringPrototypeTrim(`${string}`); + + // #10 Let mimeType be a new MIME type record whose type is type, + // in ASCII lowercase, and subtype is subtype, in ASCII lowercase. const { type, subtype, parametersStringIndex } = parseTypeAndSubtype(string); this.#type = type; this.#subtype = subtype; - this.#parameters = new MIMEParams(); this.#essence = `${this.#type}/${this.#subtype}`; + this.#parameters = new MIMEParams(); parseParametersString( string, parametersStringIndex, From aca6158bdfe28e097889cfe04989faa4e007a898 Mon Sep 17 00:00:00 2001 From: Carlos Fuentes Date: Wed, 28 Jun 2023 13:30:55 +0200 Subject: [PATCH 12/14] fix: bad parsing of several inputs --- lib/internal/mime.js | 55 ++++++++++++++++++++++---------------------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/lib/internal/mime.js b/lib/internal/mime.js index 4447a1393638..7912ca36684e 100644 --- a/lib/internal/mime.js +++ b/lib/internal/mime.js @@ -20,7 +20,7 @@ const NOT_HTTP_TOKEN_CODE_POINT = /[^!#$%&'*+\-.^_`|~A-Za-z0-9]/; const NOT_HTTP_QUOTED_STRING_CODE_POINT = /[^\t\u0020-~\u0080-\u00FF]/; const SOLIDUS = '/'; -const WHITESPACE_REGEX = /(^[\r\n\t ])/; +const WHITESPACE_REGEX = /[^\r\n\t ]/; const SEMICOLON = ';'; const BACKSLASH = '\\'; const QUOTE = '"'; @@ -60,12 +60,8 @@ function collectHTTPQuotedString(str, marker) { nextDelimiter = nextDelimiter !== -1 ? nextDelimiter : StringPrototypeIndexOf(str, QUOTE, index); - // #5-1 Append the result of collecting a sequence of code points - // that are not U+0022 (") or U+005C (\) from input, given position, to value. - - const collectedPoints = nextDelimiter === -1 ? StringPrototypeSlice(str, index) : - StringPrototypeSlice(str, index, nextDelimiter); - + // #5-1 Append the result of collecting a sequence of code points that are not U+0022 (") or U+005C (\) from input, given position, to value. + const collectedPoints = nextDelimiter === -1 ? StringPrototypeSlice(str, index) : StringPrototypeSlice(str, index, nextDelimiter); index += collectedPoints.length; value += collectedPoints; @@ -73,30 +69,31 @@ function collectHTTPQuotedString(str, marker) { if (index > limit) break; // #5-3 Let quoteOrBackslash be the code point at position within input. - const quoteOrBackslash = StringPrototypeCharCodeAt(str, index); + let quoteOrBackslash = str[index]; + let quoteOrBackslashAsCode = quoteOrBackslash == null ? 0 : StringPrototypeCharCodeAt(quoteOrBackslash, 0); // #5-4 Advance position by 1. index++; // #5-5 If quoteOrBackslash is U+005C (\), then: - if (quoteOrBackslash === BACKSLASH_CHARCODE) { + if (quoteOrBackslashAsCode === BACKSLASH_CHARCODE) { // #5-5-1 If position is past the end of input, then append U+005C (\) to value and break. - if (index > limit) { + if (index >= limit) { value += BACKSLASH; break; - } + }; // #5-5-2 Append the code point at position within input to value. value += str[index]; - // #5-5-3 AAdvance position by 1. + // #5-5-3 Advance position by 1. index++; + } else if (quoteOrBackslashAsCode === QUOTE_CHARCODE) { + // #5-6 Otherwise + // #5-6-1 Assert: quoteOrBackslash is U+0022 ("). + // #5-6-2 Break + break; } - - // #5-6 Otherwise - // #5-6-1 Assert: quoteOrBackslash is U+0022 ("). - // #5-6-2 Break - if (quoteOrBackslash === QUOTE_CHARCODE) break; } marker.position = index; @@ -106,7 +103,7 @@ function collectHTTPQuotedString(str, marker) { } -function parseTypeAndSubtype(str) { +function parseTypeAndSubtype(str) { const strLength = str.length; // #2 Let position be a position variable for input, initially pointg at // the start of the input @@ -120,7 +117,7 @@ function parseTypeAndSubtype(str) { StringPrototypeSlice(str, position, typeEnd); // #4 If type is the empty string or does not solely - // contain HTTP token code points, then return failure. + // contain HTTP token code points, then return failure. // #5 If position is past the end of input, then return failure. if (typeEnd === -1 || preType.length === 0 || typeEnd > strLength || SafeStringPrototypeSearch( preType, NOT_HTTP_TOKEN_CODE_POINT) !== -1) { @@ -163,6 +160,11 @@ function isSemicolonOrEquals(char) { return code === SEMICOLON_CHARCODE || code === EQUALS_CHARCODE; } +function isQuoteOrBackslash(char) { + const code = StringPrototypeCharCodeAt(char, 0); + return code === QUOTE_CHARCODE || code === BACKSLASH_CHARCODE; +} + function moveUntil(str, predicate, marker, { toASCII, removeBackslashes, @@ -310,7 +312,7 @@ class MIMEParams { static parseParametersString(str, position, params) { const paramsMap = params.#data; const endOfSource = str.length; - const marker = { position }; + const marker = { position }; // #11 While position is not past the end of input: while (marker.position < endOfSource) { @@ -321,7 +323,7 @@ class MIMEParams { // #11-3 Collect a sequence of code points that are HTTP whitespace from input given position. const sliced = StringPrototypeSlice(str, marker.position); const endOfWhitespace = SafeStringPrototypeSearch(sliced, WHITESPACE_REGEX); - marker.position += endOfWhitespace + 1; + marker.position += endOfWhitespace; const parameterName = moveUntil(str, isSemicolonOrEquals, marker, { toASCII: true }); @@ -346,11 +348,10 @@ class MIMEParams { parameterValue = collectHTTPQuotedString(str, marker); // #11-8-1 Collect a sequence of code points that are not U+003B (;) from input, given position. - const remaining = StringPrototypeIndexOf(str, SEMICOLON, marker.position); + let remaining = StringPrototypeIndexOf(str, SEMICOLON, marker.position); marker.position += remaining !== -1 ? remaining - marker.position : 0; } else { - // #11-9-1 Set parameterValue to the result of collecting a sequence of code points - // that are not U+003B (;) from input, given position. + // #11-9-1 Set parameterValue to the result of collecting a sequence of code points that are not U+003B (;) from input, given position. const valueEnd = StringPrototypeIndexOf(str, SEMICOLON, marker.position); const rawValue = valueEnd === -1 ? StringPrototypeSlice(str, marker.position) : @@ -372,9 +373,9 @@ class MIMEParams { * parameterName is not the empty string * parameterName solely contains HTTP token code points * parameterValue solely contains HTTP quoted-string token code points - * mimeTypes parameters[parameterName] does not exist - * - * then set mimeType's parameters[parameterName] to parameterValue. + * mimeType’s parameters[parameterName] does not exist + * + * then set mimeType’s parameters[parameterName] to parameterValue. */ if ( parameterName.length !== 0 && From 3394c74d1d370f6fac50196d9bc4ea08cc0ef049 Mon Sep 17 00:00:00 2001 From: Carlos Fuentes Date: Wed, 28 Jun 2023 13:38:06 +0200 Subject: [PATCH 13/14] fix: linter issues --- lib/internal/mime.js | 38 ++++++++++++++++++-------------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/lib/internal/mime.js b/lib/internal/mime.js index 7912ca36684e..cb4a8f2a5a7a 100644 --- a/lib/internal/mime.js +++ b/lib/internal/mime.js @@ -60,8 +60,10 @@ function collectHTTPQuotedString(str, marker) { nextDelimiter = nextDelimiter !== -1 ? nextDelimiter : StringPrototypeIndexOf(str, QUOTE, index); - // #5-1 Append the result of collecting a sequence of code points that are not U+0022 (") or U+005C (\) from input, given position, to value. - const collectedPoints = nextDelimiter === -1 ? StringPrototypeSlice(str, index) : StringPrototypeSlice(str, index, nextDelimiter); + // #5-1 Append the result of collecting a sequence of code points + // that are not U+0022 (") or U+005C (\) from input, given position, to value. + const collectedPoints = nextDelimiter === -1 ? + StringPrototypeSlice(str, index) : StringPrototypeSlice(str, index, nextDelimiter); index += collectedPoints.length; value += collectedPoints; @@ -69,8 +71,10 @@ function collectHTTPQuotedString(str, marker) { if (index > limit) break; // #5-3 Let quoteOrBackslash be the code point at position within input. - let quoteOrBackslash = str[index]; - let quoteOrBackslashAsCode = quoteOrBackslash == null ? 0 : StringPrototypeCharCodeAt(quoteOrBackslash, 0); + const quoteOrBackslash = str[index]; + const quoteOrBackslashAsCode = quoteOrBackslash == null ? + 0 : + StringPrototypeCharCodeAt(quoteOrBackslash, 0); // #5-4 Advance position by 1. index++; @@ -81,7 +85,7 @@ function collectHTTPQuotedString(str, marker) { if (index >= limit) { value += BACKSLASH; break; - }; + } // #5-5-2 Append the code point at position within input to value. value += str[index]; @@ -102,8 +106,7 @@ function collectHTTPQuotedString(str, marker) { return value; } - -function parseTypeAndSubtype(str) { +function parseTypeAndSubtype(str) { const strLength = str.length; // #2 Let position be a position variable for input, initially pointg at // the start of the input @@ -117,7 +120,7 @@ function parseTypeAndSubtype(str) { StringPrototypeSlice(str, position, typeEnd); // #4 If type is the empty string or does not solely - // contain HTTP token code points, then return failure. + // contain HTTP token code points, then return failure. // #5 If position is past the end of input, then return failure. if (typeEnd === -1 || preType.length === 0 || typeEnd > strLength || SafeStringPrototypeSearch( preType, NOT_HTTP_TOKEN_CODE_POINT) !== -1) { @@ -160,11 +163,6 @@ function isSemicolonOrEquals(char) { return code === SEMICOLON_CHARCODE || code === EQUALS_CHARCODE; } -function isQuoteOrBackslash(char) { - const code = StringPrototypeCharCodeAt(char, 0); - return code === QUOTE_CHARCODE || code === BACKSLASH_CHARCODE; -} - function moveUntil(str, predicate, marker, { toASCII, removeBackslashes, @@ -312,7 +310,7 @@ class MIMEParams { static parseParametersString(str, position, params) { const paramsMap = params.#data; const endOfSource = str.length; - const marker = { position }; + const marker = { position }; // #11 While position is not past the end of input: while (marker.position < endOfSource) { @@ -348,10 +346,12 @@ class MIMEParams { parameterValue = collectHTTPQuotedString(str, marker); // #11-8-1 Collect a sequence of code points that are not U+003B (;) from input, given position. - let remaining = StringPrototypeIndexOf(str, SEMICOLON, marker.position); + const remaining = StringPrototypeIndexOf(str, SEMICOLON, marker.position); marker.position += remaining !== -1 ? remaining - marker.position : 0; } else { - // #11-9-1 Set parameterValue to the result of collecting a sequence of code points that are not U+003B (;) from input, given position. + // #11-9-1 Set parameterValue to the result of + // collecting a sequence of code points that are not U+003B (;) from input, + // given position. const valueEnd = StringPrototypeIndexOf(str, SEMICOLON, marker.position); const rawValue = valueEnd === -1 ? StringPrototypeSlice(str, marker.position) : @@ -369,13 +369,11 @@ class MIMEParams { /** * #11-10 if: - * * parameterName is not the empty string * parameterName solely contains HTTP token code points * parameterValue solely contains HTTP quoted-string token code points - * mimeType’s parameters[parameterName] does not exist - * - * then set mimeType’s parameters[parameterName] to parameterValue. + * mimeType parameters[parameterName] does not exist + * then set mimeType parameters[parameterName] to parameterValue. */ if ( parameterName.length !== 0 && From 46d27aef45574fbfc8e146fc12c2d3189d6b3d9b Mon Sep 17 00:00:00 2001 From: Carlos Fuentes Date: Fri, 24 Nov 2023 12:22:28 +0100 Subject: [PATCH 14/14] refactor: preserve commit --- lib/internal/mime.js | 30 ++++-------------------------- 1 file changed, 4 insertions(+), 26 deletions(-) diff --git a/lib/internal/mime.js b/lib/internal/mime.js index 9e5944aa90fe..cb4a8f2a5a7a 100644 --- a/lib/internal/mime.js +++ b/lib/internal/mime.js @@ -154,8 +154,8 @@ function parseTypeAndSubtype(str) { __proto__: null, type, subtype, - position, - ]; + parametersStringIndex: position, + }; } function isSemicolonOrEquals(char) { @@ -234,24 +234,8 @@ const encode = (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; - #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); } @@ -260,12 +244,10 @@ class MIMEParams { } has(name) { - this.#parse(); return this.#data.has(name); } set(name, value) { - this.#parse(); const data = this.#data; name = `${name}`; value = `${value}`; @@ -300,22 +282,18 @@ class MIMEParams { } *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); @@ -431,8 +409,8 @@ ObjectDefineProperty(MIMEParams.prototype, 'toJSON', { writable: true, }); -const { instantiateMimeParams } = MIMEParams; -delete MIMEParams.instantiateMimeParams; +const { parseParametersString } = MIMEParams; +delete MIMEParams.parseParametersString; class MIMEType { #type;