util: add MIME utilities (#21128)

Co-authored-by: Rich Trott <rtrott@gmail.com>
Co-authored-by: Antoine du Hamel <duhamelantoine1995@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/21128
Reviewed-By: Gus Caplan <me@gus.host>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Robert Nagy <ronagy@icloud.com>
Reviewed-By: Jacob Smith <jacob@frende.me>
Reviewed-By: Michaël Zasso <targos@protonmail.com>
Reviewed-By: Geoffrey Booth <webadmin@geoffreybooth.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
This commit is contained in:
Bradley Farias 2022-10-18 21:26:16 -05:00 committed by GitHub
parent 1f7e8d609a
commit 87cdf7d412
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 5123 additions and 17 deletions

View File

@ -1967,6 +1967,12 @@ An invalid HTTP token was supplied.
An IP address is not valid.
<a id="ERR_INVALID_MIME_SYNTAX"></a>
### `ERR_INVALID_MIME_SYNTAX`
The syntax of a MIME is not valid.
<a id="ERR_INVALID_MODULE"></a>
### `ERR_INVALID_MODULE`

View File

@ -1024,6 +1024,355 @@ Otherwise, returns `false`.
See [`assert.deepStrictEqual()`][] for more information about deep strict
equality.
## Class: `util.MIMEType`
<!-- YAML
added: REPLACEME
-->
> Stability: 1 - Experimental
An implementation of [the MIMEType class](https://bmeck.github.io/node-proposal-mime-api/).
In accordance with browser conventions, all properties of `MIMEType` objects
are implemented as getters and setters on the class prototype, rather than as
data properties on the object itself.
A MIME string is a structured string containing multiple meaningful
components. When parsed, a `MIMEType` object is returned containing
properties for each of these components.
### Constructor: `new MIMEType(input)`
* `input` {string} The input MIME to parse
Creates a new `MIMEType` object by parsing the `input`.
```mjs
import { MIMEType } from 'node:util';
const myMIME = new MIMEType('text/plain');
```
```cjs
const { MIMEType } = require('node:util');
const myMIME = new MIMEType('text/plain');
```
A `TypeError` will be thrown if the `input` is not a valid MIME. Note
that an effort will be made to coerce the given values into strings. For
instance:
```mjs
import { MIMEType } from 'node:util';
const myMIME = new MIMEType({ toString: () => 'text/plain' });
console.log(String(myMIME));
// Prints: text/plain
```
```cjs
const { MIMEType } = require('node:util');
const myMIME = new MIMEType({ toString: () => 'text/plain' });
console.log(String(myMIME));
// Prints: text/plain
```
#### `mime.type`
* {string}
Gets and sets the type portion of the MIME.
```mjs
import { MIMEType } from 'node:util';
const myMIME = new MIMEType('text/javascript');
console.log(myMIME.type);
// Prints: text
myMIME.type = 'application';
console.log(myMIME.type);
// Prints: application
console.log(String(myMIME));
// Prints: application/javascript
```
```cjs
const { MIMEType } = require('node:util');
const myMIME = new MIMEType('text/javascript');
console.log(myMIME.type);
// Prints: text
myMIME.type = 'application';
console.log(myMIME.type);
// Prints: application
console.log(String(myMIME));
// Prints: application/javascript/javascript
```
#### `mime.subtype`
* {string}
Gets and sets the subtype portion of the MIME.
```mjs
import { MIMEType } from 'node:util';
const myMIME = new MIMEType('text/ecmascript');
console.log(myMIME.subtype);
// Prints: ecmascript
myMIME.subtype = 'javascript';
console.log(myMIME.subtype);
// Prints: javascript
console.log(String(myMIME));
// Prints: text/javascript
```
```cjs
const { MIMEType } = require('node:util');
const myMIME = new MIMEType('text/ecmascript');
console.log(myMIME.subtype);
// Prints: ecmascript
myMIME.subtype = 'javascript';
console.log(myMIME.subtype);
// Prints: javascript
console.log(String(myMIME));
// Prints: text/javascript
```
#### `mime.essence`
* {string}
Gets the essence of the MIME. This property is read only.
Use `mime.type` or `mime.subtype` to alter the MIME.
```mjs
import { MIMEType } from 'node:util';
const myMIME = new MIMEType('text/javascript;key=value');
console.log(myMIME.essence);
// Prints: text/javascript
myMIME.type = 'application';
console.log(myMIME.essence);
// Prints: application/javascript
console.log(String(myMIME));
// Prints: application/javascript;key=value
```
```cjs
const { MIMEType } = require('node:util');
const myMIME = new MIMEType('text/javascript;key=value');
console.log(myMIME.essence);
// Prints: text/javascript
myMIME.type = 'application';
console.log(myMIME.essence);
// Prints: application/javascript
console.log(String(myMIME));
// Prints: application/javascript;key=value
```
#### `mime.params`
* {MIMEParams}
Gets the [`MIMEParams`][] object representing the
parameters of the MIME. This property is read-only. See
[`MIMEParams`][] documentation for details.
#### `mime.toString()`
* Returns: {string}
The `toString()` method on the `MIMEType` object returns the serialized MIME.
Because of the need for standard compliance, this method does not allow users
to customize the serialization process of the MIME.
#### `mime.toJSON()`
* Returns: {string}
Alias for [`mime.toString()`][].
This method is automatically called when an `MIMEType` object is serialized
with [`JSON.stringify()`][].
```mjs
import { MIMEType } from 'node:util';
const myMIMES = [
new MIMEType('image/png'),
new MIMEType('image/gif'),
];
console.log(JSON.stringify(myMIMES));
// Prints: ["image/png", "image/gif"]
```
```cjs
const { MIMEType } = require('node:util');
const myMIMES = [
new MIMEType('image/png'),
new MIMEType('image/gif'),
];
console.log(JSON.stringify(myMIMES));
// Prints: ["image/png", "image/gif"]
```
### Class: `util.MIMEParams`
<!-- YAML
added: REPLACEME
-->
The `MIMEParams` API provides read and write access to the parameters of a
`MIMEType`.
#### Constructor: `new MIMEParams()`
Creates a new `MIMEParams` object by with empty parameters
```mjs
import { MIMEParams } from 'node:util';
const myParams = new MIMEParams();
```
```cjs
const { MIMEParams } = require('node:util');
const myParams = new MIMEParams();
```
#### `mimeParams.delete(name)`
* `name` {string}
Remove all name-value pairs whose name is `name`.
#### `mimeParams.entries()`
* Returns: {Iterator}
Returns an iterator over each of the name-value pairs in the parameters.
Each item of the iterator is a JavaScript `Array`. The first item of the array
is the `name`, the second item of the array is the `value`.
#### `mimeParams.get(name)`
* `name` {string}
* Returns: {string} or `null` if there is no name-value pair with the given
`name`.
Returns the value of the first name-value pair whose name is `name`. If there
are no such pairs, `null` is returned.
#### `mimeParams.has(name)`
* `name` {string}
* Returns: {boolean}
Returns `true` if there is at least one name-value pair whose name is `name`.
#### `mimeParams.keys()`
* Returns: {Iterator}
Returns an iterator over the names of each name-value pair.
```mjs
import { MIMEType } from 'node:util';
const { params } = new MIMEType('text/plain;foo=0;bar=1');
for (const name of params.keys()) {
console.log(name);
}
// Prints:
// foo
// bar
```
```cjs
const { MIMEType } = require('node:util');
const { params } = new MIMEType('text/plain;foo=0;bar=1');
for (const name of params.keys()) {
console.log(name);
}
// Prints:
// foo
// bar
```
#### `mimeParams.set(name, value)`
* `name` {string}
* `value` {string}
Sets the value in the `MIMEParams` object associated with `name` to
`value`. If there are any pre-existing name-value pairs whose names are `name`,
set the first such pair's value to `value`.
```mjs
import { MIMEType } from 'node:util';
const { params } = new MIMEType('text/plain;foo=0;bar=1');
params.set('foo', 'def');
params.set('baz', 'xyz');
console.log(params.toString());
// Prints: foo=def&bar=1&baz=xyz
```
```cjs
const { MIMEType } = require('node:util');
const { params } = new MIMEType('text/plain;foo=0;bar=1');
params.set('foo', 'def');
params.set('baz', 'xyz');
console.log(params.toString());
// Prints: foo=def&bar=1&baz=xyz
```
#### `mimeParams.values()`
* Returns: {Iterator}
Returns an iterator over the values of each name-value pair.
#### `mimeParams[@@iterator]()`
* Returns: {Iterator}
Alias for [`mimeParams.entries()`][].
```mjs
import { MIMEType } from 'node:util';
const { params } = new MIMEType('text/plain;foo=bar;xyz=baz');
for (const [name, value] of params) {
console.log(name, value);
}
// Prints:
// foo bar
// xyz baz
```
```cjs
const { MIMEType } = require('node:util');
const { params } = new MIMEType('text/plain;foo=bar;xyz=baz');
for (const [name, value] of params) {
console.log(name, value);
}
// Prints:
// foo bar
// xyz baz
```
## `util.parseArgs([config])`
<!-- YAML
@ -2903,6 +3252,8 @@ util.log('Timestamped message.');
[`Int16Array`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Int16Array
[`Int32Array`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Int32Array
[`Int8Array`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Int8Array
[`JSON.stringify()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify
[`MIMEparams`]: #class-utilmimeparams
[`Map`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map
[`Object.assign()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
[`Object.freeze()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze
@ -2920,6 +3271,8 @@ util.log('Timestamped message.');
[`WebAssembly.Module`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/Module
[`assert.deepStrictEqual()`]: assert.md#assertdeepstrictequalactual-expected-message
[`console.error()`]: console.md#consoleerrordata-args
[`mime.toString()`]: #mimetostring
[`mimeParams.entries()`]: #mimeparamsentries
[`napi_create_external()`]: n-api.md#napi_create_external
[`target` and `handler`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy#Terminology
[`tty.hasColors()`]: tty.md#writestreamhascolorscount-env

View File

@ -639,6 +639,9 @@ RegExp.prototype.exec = () => null;
// Core
console.log(RegExpPrototypeTest(/o/, 'foo')); // false
console.log(RegExpPrototypeExec(/o/, 'foo') !== null); // true
console.log(RegExpPrototypeSymbolSearch(/o/, 'foo')); // -1
console.log(SafeStringPrototypeSearch('foo', /o/)); // 1
```
#### Don't trust `RegExp` flags
@ -668,19 +671,7 @@ Object.defineProperty(RegExp.prototype, 'global', { value: false });
// Core
console.log(RegExpPrototypeSymbolReplace(/o/g, 'foo', 'a')); // 'fao'
const regex = /o/g;
ObjectDefineProperties(regex, {
dotAll: { value: false },
exec: { value: undefined },
flags: { value: 'g' },
global: { value: true },
ignoreCase: { value: false },
multiline: { value: false },
unicode: { value: false },
sticky: { value: false },
});
console.log(RegExpPrototypeSymbolReplace(regex, 'foo', 'a')); // 'faa'
console.log(RegExpPrototypeSymbolReplace(hardenRegExp(/o/g), 'foo', 'a')); // 'faa'
```
### Defining object own properties

View File

@ -1310,6 +1310,10 @@ E('ERR_INVALID_FILE_URL_PATH', 'File URL path %s', TypeError);
E('ERR_INVALID_HANDLE_TYPE', 'This handle type cannot be sent', TypeError);
E('ERR_INVALID_HTTP_TOKEN', '%s must be a valid HTTP token ["%s"]', TypeError);
E('ERR_INVALID_IP_ADDRESS', 'Invalid IP address: %s', TypeError);
E('ERR_INVALID_MIME_SYNTAX', (production, str, invalidIndex) => {
const msg = invalidIndex !== -1 ? ` at ${invalidIndex}` : '';
return `The MIME syntax for a ${production} in "${str}" is invalid` + msg;
}, TypeError);
E('ERR_INVALID_MODULE_SPECIFIER', (request, reason, base = undefined) => {
return `Invalid module "${request}" ${reason}${base ?
` imported from ${base}` : ''}`;

367
lib/internal/mime.js Normal file
View File

@ -0,0 +1,367 @@
'use strict';
const {
FunctionPrototypeCall,
ObjectDefineProperty,
RegExpPrototypeExec,
SafeMap,
SafeStringPrototypeSearch,
StringPrototypeCharAt,
StringPrototypeIndexOf,
StringPrototypeSlice,
StringPrototypeToLowerCase,
SymbolIterator,
} = primordials;
const {
ERR_INVALID_MIME_SYNTAX,
} = require('internal/errors').codes;
const NOT_HTTP_TOKEN_CODE_POINT = /[^!#$%&'*+\-.^_`|~A-Za-z0-9]/g;
const NOT_HTTP_QUOTED_STRING_CODE_POINT = /[^\t\u0020-~\u0080-\u00FF]/g;
const END_BEGINNING_WHITESPACE = /[^\r\n\t ]|$/;
const START_ENDING_WHITESPACE = /[\r\n\t ]*$/;
function toASCIILower(str) {
let result = '';
for (let i = 0; i < str.length; i++) {
const char = str[i];
result += char >= 'A' && char <= 'Z' ?
StringPrototypeToLowerCase(char) :
char;
}
return result;
}
const SOLIDUS = '/';
const SEMICOLON = ';';
function parseTypeAndSubtype(str) {
// Skip only HTTP whitespace from start
let position = SafeStringPrototypeSearch(str, END_BEGINNING_WHITESPACE);
// read until '/'
const typeEnd = StringPrototypeIndexOf(str, SOLIDUS, position);
const trimmedType = typeEnd === -1 ?
StringPrototypeSlice(str, position) :
StringPrototypeSlice(str, position, typeEnd);
const invalidTypeIndex = SafeStringPrototypeSearch(trimmedType,
NOT_HTTP_TOKEN_CODE_POINT);
if (trimmedType === '' || invalidTypeIndex !== -1 || typeEnd === -1) {
throw new ERR_INVALID_MIME_SYNTAX('type', str, invalidTypeIndex);
}
// skip type and '/'
position = typeEnd + 1;
const type = toASCIILower(trimmedType);
// read until ';'
const subtypeEnd = StringPrototypeIndexOf(str, SEMICOLON, position);
const rawSubtype = subtypeEnd === -1 ?
StringPrototypeSlice(str, position) :
StringPrototypeSlice(str, position, subtypeEnd);
position += rawSubtype.length;
if (subtypeEnd !== -1) {
// skip ';'
position += 1;
}
const trimmedSubtype = StringPrototypeSlice(
rawSubtype,
0,
SafeStringPrototypeSearch(rawSubtype, START_ENDING_WHITESPACE));
const invalidSubtypeIndex = SafeStringPrototypeSearch(trimmedSubtype,
NOT_HTTP_TOKEN_CODE_POINT);
if (trimmedSubtype === '' || invalidSubtypeIndex !== -1) {
throw new ERR_INVALID_MIME_SYNTAX('subtype', str, trimmedSubtype);
}
const subtype = toASCIILower(trimmedSubtype);
return {
__proto__: null,
type,
subtype,
parametersStringIndex: position,
};
}
const EQUALS_SEMICOLON_OR_END = /[;=]|$/;
const QUOTED_VALUE_PATTERN = /^(?:([\\]$)|[\\][\s\S]|[^"])*(?:(")|$)/u;
function removeBackslashes(str) {
let ret = '';
// We stop at str.length - 1 because we want to look ahead one character.
let i;
for (i = 0; i < str.length - 1; i++) {
const c = str[i];
if (c === '\\') {
i++;
ret += str[i];
} else {
ret += c;
}
}
// We add the last character if we didn't skip to it.
if (i === str.length - 1) {
ret += str[i];
}
return ret;
}
function escapeQuoteOrSolidus(str) {
let result = '';
for (let i = 0; i < str.length; i++) {
const char = str[i];
result += (char === '"' || char === '\\') ? `\\${char}` : char;
}
return result;
}
const encode = (value) => {
if (value.length === 0) return '""';
const encode = SafeStringPrototypeSearch(value, NOT_HTTP_TOKEN_CODE_POINT) !== -1;
if (!encode) return value;
const escaped = escapeQuoteOrSolidus(value);
return `"${escaped}"`;
};
class MIMEParams {
#data = new SafeMap();
delete(name) {
this.#data.delete(name);
}
get(name) {
const data = this.#data;
if (data.has(name)) {
return data.get(name);
}
return null;
}
has(name) {
return this.#data.has(name);
}
set(name, value) {
const data = this.#data;
name = `${name}`;
value = `${value}`;
const invalidNameIndex = SafeStringPrototypeSearch(name, NOT_HTTP_TOKEN_CODE_POINT);
if (name.length === 0 || invalidNameIndex !== -1) {
throw new ERR_INVALID_MIME_SYNTAX(
'parameter name',
name,
invalidNameIndex,
);
}
const invalidValueIndex = SafeStringPrototypeSearch(
value,
NOT_HTTP_QUOTED_STRING_CODE_POINT);
if (invalidValueIndex !== -1) {
throw new ERR_INVALID_MIME_SYNTAX(
'parameter value',
value,
invalidValueIndex,
);
}
data.set(name, value);
}
*entries() {
yield* this.#data.entries();
}
*keys() {
yield* this.#data.keys();
}
*values() {
yield* this.#data.values();
}
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;
}
// Used to act as a friendly class to stringifying stuff
// 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) {
// 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 &&
params.has(parameterString) === false
) {
paramsMap.set(parameterString, parameterValue);
}
position++;
}
return paramsMap;
}
}
const MIMEParamsStringify = MIMEParams.prototype.toString;
ObjectDefineProperty(MIMEParams.prototype, SymbolIterator, {
__proto__: null,
configurable: true,
value: MIMEParams.prototype.entries,
writable: true,
});
ObjectDefineProperty(MIMEParams.prototype, 'toJSON', {
__proto__: null,
configurable: true,
value: MIMEParamsStringify,
writable: true,
});
const { parseParametersString } = MIMEParams;
delete MIMEParams.parseParametersString;
class MIMEType {
#type;
#subtype;
#parameters;
constructor(string) {
string = `${string}`;
const data = parseTypeAndSubtype(string);
this.#type = data.type;
this.#subtype = data.subtype;
this.#parameters = new MIMEParams();
parseParametersString(
string,
data.parametersStringIndex,
this.#parameters,
);
}
get type() {
return this.#type;
}
set type(v) {
v = `${v}`;
const invalidTypeIndex = SafeStringPrototypeSearch(v, NOT_HTTP_TOKEN_CODE_POINT);
if (v.length === 0 || invalidTypeIndex !== -1) {
throw new ERR_INVALID_MIME_SYNTAX('type', v, invalidTypeIndex);
}
this.#type = toASCIILower(v);
}
get subtype() {
return this.#subtype;
}
set subtype(v) {
v = `${v}`;
const invalidSubtypeIndex = SafeStringPrototypeSearch(v, NOT_HTTP_TOKEN_CODE_POINT);
if (v.length === 0 || invalidSubtypeIndex !== -1) {
throw new ERR_INVALID_MIME_SYNTAX('subtype', v, invalidSubtypeIndex);
}
this.#subtype = toASCIILower(v);
}
get essence() {
return `${this.#type}/${this.#subtype}`;
}
get params() {
return this.#parameters;
}
toString() {
let ret = `${this.#type}/${this.#subtype}`;
const paramStr = FunctionPrototypeCall(MIMEParamsStringify, this.#parameters);
if (paramStr.length) ret += `;${paramStr}`;
return ret;
}
}
ObjectDefineProperty(MIMEType.prototype, 'toJSON', {
__proto__: null,
configurable: true,
value: MIMEType.prototype.toString,
writable: true,
});
module.exports = {
MIMEParams,
MIMEType,
};

View File

@ -266,12 +266,36 @@ const {
FinalizationRegistry,
FunctionPrototypeCall,
Map,
ObjectDefineProperties,
ObjectDefineProperty,
ObjectFreeze,
ObjectSetPrototypeOf,
Promise,
PromisePrototypeThen,
ReflectApply,
ReflectConstruct,
ReflectSet,
ReflectGet,
RegExp,
RegExpPrototype,
RegExpPrototypeExec,
RegExpPrototypeGetDotAll,
RegExpPrototypeGetFlags,
RegExpPrototypeGetGlobal,
RegExpPrototypeGetHasIndices,
RegExpPrototypeGetIgnoreCase,
RegExpPrototypeGetMultiline,
RegExpPrototypeGetSource,
RegExpPrototypeGetSticky,
RegExpPrototypeGetUnicode,
Set,
SymbolIterator,
SymbolMatch,
SymbolMatchAll,
SymbolReplace,
SymbolSearch,
SymbolSpecies,
SymbolSplit,
WeakMap,
WeakRef,
WeakSet,
@ -490,5 +514,128 @@ primordials.SafePromiseRace = (promises, mapFn) =>
);
const {
exec: OriginalRegExpPrototypeExec,
[SymbolMatch]: OriginalRegExpPrototypeSymbolMatch,
[SymbolMatchAll]: OriginalRegExpPrototypeSymbolMatchAll,
[SymbolReplace]: OriginalRegExpPrototypeSymbolReplace,
[SymbolSearch]: OriginalRegExpPrototypeSymbolSearch,
[SymbolSplit]: OriginalRegExpPrototypeSymbolSplit,
} = RegExpPrototype;
class RegExpLikeForStringSplitting {
#regex;
constructor() {
this.#regex = ReflectConstruct(RegExp, arguments);
}
get lastIndex() {
return ReflectGet(this.#regex, 'lastIndex');
}
set lastIndex(value) {
ReflectSet(this.#regex, 'lastIndex', value);
}
exec() {
return ReflectApply(OriginalRegExpPrototypeExec, this.#regex, arguments);
}
}
ObjectSetPrototypeOf(RegExpLikeForStringSplitting.prototype, null);
primordials.hardenRegExp = function hardenRegExp(pattern) {
ObjectDefineProperties(pattern, {
[SymbolMatch]: {
__proto__: null,
configurable: true,
value: OriginalRegExpPrototypeSymbolMatch,
},
[SymbolMatchAll]: {
__proto__: null,
configurable: true,
value: OriginalRegExpPrototypeSymbolMatchAll,
},
[SymbolReplace]: {
__proto__: null,
configurable: true,
value: OriginalRegExpPrototypeSymbolReplace,
},
[SymbolSearch]: {
__proto__: null,
configurable: true,
value: OriginalRegExpPrototypeSymbolSearch,
},
[SymbolSplit]: {
__proto__: null,
configurable: true,
value: OriginalRegExpPrototypeSymbolSplit,
},
constructor: {
__proto__: null,
configurable: true,
value: {
[SymbolSpecies]: RegExpLikeForStringSplitting,
}
},
dotAll: {
__proto__: null,
configurable: true,
value: RegExpPrototypeGetDotAll(pattern),
},
exec: {
__proto__: null,
configurable: true,
value: OriginalRegExpPrototypeExec,
},
global: {
__proto__: null,
configurable: true,
value: RegExpPrototypeGetGlobal(pattern),
},
hasIndices: {
__proto__: null,
configurable: true,
value: RegExpPrototypeGetHasIndices(pattern),
},
ignoreCase: {
__proto__: null,
configurable: true,
value: RegExpPrototypeGetIgnoreCase(pattern),
},
multiline: {
__proto__: null,
configurable: true,
value: RegExpPrototypeGetMultiline(pattern),
},
source: {
__proto__: null,
configurable: true,
value: RegExpPrototypeGetSource(pattern),
},
sticky: {
__proto__: null,
configurable: true,
value: RegExpPrototypeGetSticky(pattern),
},
unicode: {
__proto__: null,
configurable: true,
value: RegExpPrototypeGetUnicode(pattern),
},
});
ObjectDefineProperty(pattern, 'flags', {
__proto__: null,
configurable: true,
value: RegExpPrototypeGetFlags(pattern),
});
return pattern;
};
primordials.SafeStringPrototypeSearch = (str, regexp) => {
regexp.lastIndex = 0;
const match = RegExpPrototypeExec(regexp, str);
return match ? match.index : -1;
};
ObjectSetPrototypeOf(primordials, null);
ObjectFreeze(primordials);

View File

@ -68,6 +68,7 @@ const {
validateNumber,
} = require('internal/validators');
const { TextDecoder, TextEncoder } = require('internal/encoding');
const { MIMEType, MIMEParams } = require('internal/mime');
const { isBuffer } = require('buffer').Buffer;
const types = require('internal/util/types');
@ -385,6 +386,8 @@ module.exports = {
isFunction,
isPrimitive,
log,
MIMEType,
MIMEParams,
parseArgs,
promisify,
stripVTControlCharacters,

3533
test/fixtures/mime-whatwg-generated.js vendored Normal file

File diff suppressed because it is too large Load Diff

392
test/fixtures/mime-whatwg.js vendored Normal file
View File

@ -0,0 +1,392 @@
'use strict';
// TODO: Incorporate this with the other WPT tests if it makes sense to do that.
/* The following tests are copied from WPT. Modifications to them should be
upstreamed first. Refs:
https://github.com/w3c/web-platform-tests/blob/88b75886e/url/urltestdata.json
License: http://www.w3.org/Consortium/Legal/2008/04-testsuite-copyright.html
*/
module.exports = [
"Basics",
{
"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"
},
"Legacy comment syntax",
{
"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"
},
"Duplicate parameter",
{
"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
},
"Spaces",
{
"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
},
"0x0B and 0x0C",
{
"input": "text/html;charset=\u000Bgbk",
"output": "text/html",
"navigable": true,
"encoding": null
},
{
"input": "text/html;charset=\u000Cgbk",
"output": "text/html",
"navigable": true,
"encoding": null
},
{
"input": "text/html;\u000Bcharset=gbk",
"output": "text/html",
"navigable": true,
"encoding": null
},
{
"input": "text/html;\u000Ccharset=gbk",
"output": "text/html",
"navigable": true,
"encoding": null
},
"Single quotes are a token, not a delimiter",
{
"input": "text/html;charset='gbk'",
"output": "text/html;charset='gbk'",
"navigable": true,
"encoding": null
},
{
"input": "text/html;charset='gbk",
"output": "text/html;charset='gbk",
"navigable": true,
"encoding": null
},
{
"input": "text/html;charset=gbk'",
"output": "text/html;charset=gbk'",
"navigable": true,
"encoding": null
},
{
"input": "text/html;charset=';charset=GBK",
"output": "text/html;charset='",
"navigable": true,
"encoding": null
},
"Invalid parameters",
{
"input": "text/html;test;charset=gbk",
"output": "text/html;charset=gbk",
"navigable": true,
"encoding": "GBK"
},
{
"input": "text/html;test=;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": "GBK"
},
{
"input": "text/html;;;;charset=gbk",
"output": "text/html;charset=gbk",
"navigable": true,
"encoding": "GBK"
},
{
"input": "text/html;charset= \"\u007F;charset=GBK",
"output": "text/html;charset=GBK",
"navigable": true,
"encoding": "GBK"
},
{
"input": "text/html;charset=\"\u007F;charset=foo\";charset=GBK",
"output": "text/html;charset=GBK",
"navigable": true,
"encoding": "GBK"
},
"Double quotes",
{
"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;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=\"\\g\\b\\k\"",
"output": "text/html;charset=gbk",
"navigable": true,
"encoding": "GBK"
},
{
"input": "text/html;charset=\"gbk\"x",
"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=\";charset=GBK",
"output": "text/html;charset=\";charset=GBK\"",
"navigable": true,
"encoding": null
},
"Unexpected code points",
{
"input": "text/html;charset={gbk}",
"output": "text/html;charset=\"{gbk}\"",
"navigable": true,
"encoding": null
},
"Parameter name longer than 127",
{
"input": "text/html;0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789=x;charset=gbk",
"output": "text/html;0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789=x;charset=gbk",
"navigable": true,
"encoding": "GBK"
},
"type/subtype longer than 127",
{
"input": "0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789/0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789",
"output": "0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789/0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789"
},
"Valid",
{
"input": "!#$%&'*+-.^_`|~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz/!#$%&'*+-.^_`|~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz;!#$%&'*+-.^_`|~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz=!#$%&'*+-.^_`|~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
"output": "!#$%&'*+-.^_`|~0123456789abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz/!#$%&'*+-.^_`|~0123456789abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz;!#$%&'*+-.^_`|~0123456789abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz=!#$%&'*+-.^_`|~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
},
{
"input": "x/x;x=\"\t !\\\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\u0080\u0081\u0082\u0083\u0084\u0085\u0086\u0087\u0088\u0089\u008A\u008B\u008C\u008D\u008E\u008F\u0090\u0091\u0092\u0093\u0094\u0095\u0096\u0097\u0098\u0099\u009A\u009B\u009C\u009D\u009E\u009F\u00A0\u00A1\u00A2\u00A3\u00A4\u00A5\u00A6\u00A7\u00A8\u00A9\u00AA\u00AB\u00AC\u00AD\u00AE\u00AF\u00B0\u00B1\u00B2\u00B3\u00B4\u00B5\u00B6\u00B7\u00B8\u00B9\u00BA\u00BB\u00BC\u00BD\u00BE\u00BF\u00C0\u00C1\u00C2\u00C3\u00C4\u00C5\u00C6\u00C7\u00C8\u00C9\u00CA\u00CB\u00CC\u00CD\u00CE\u00CF\u00D0\u00D1\u00D2\u00D3\u00D4\u00D5\u00D6\u00D7\u00D8\u00D9\u00DA\u00DB\u00DC\u00DD\u00DE\u00DF\u00E0\u00E1\u00E2\u00E3\u00E4\u00E5\u00E6\u00E7\u00E8\u00E9\u00EA\u00EB\u00EC\u00ED\u00EE\u00EF\u00F0\u00F1\u00F2\u00F3\u00F4\u00F5\u00F6\u00F7\u00F8\u00F9\u00FA\u00FB\u00FC\u00FD\u00FE\u00FF\"",
"output": "x/x;x=\"\t !\\\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\u0080\u0081\u0082\u0083\u0084\u0085\u0086\u0087\u0088\u0089\u008A\u008B\u008C\u008D\u008E\u008F\u0090\u0091\u0092\u0093\u0094\u0095\u0096\u0097\u0098\u0099\u009A\u009B\u009C\u009D\u009E\u009F\u00A0\u00A1\u00A2\u00A3\u00A4\u00A5\u00A6\u00A7\u00A8\u00A9\u00AA\u00AB\u00AC\u00AD\u00AE\u00AF\u00B0\u00B1\u00B2\u00B3\u00B4\u00B5\u00B6\u00B7\u00B8\u00B9\u00BA\u00BB\u00BC\u00BD\u00BE\u00BF\u00C0\u00C1\u00C2\u00C3\u00C4\u00C5\u00C6\u00C7\u00C8\u00C9\u00CA\u00CB\u00CC\u00CD\u00CE\u00CF\u00D0\u00D1\u00D2\u00D3\u00D4\u00D5\u00D6\u00D7\u00D8\u00D9\u00DA\u00DB\u00DC\u00DD\u00DE\u00DF\u00E0\u00E1\u00E2\u00E3\u00E4\u00E5\u00E6\u00E7\u00E8\u00E9\u00EA\u00EB\u00EC\u00ED\u00EE\u00EF\u00F0\u00F1\u00F2\u00F3\u00F4\u00F5\u00F6\u00F7\u00F8\u00F9\u00FA\u00FB\u00FC\u00FD\u00FE\u00FF\""
},
"End-of-file handling",
{
"input": "x/x;test",
"output": "x/x"
},
{
"input": "x/x;test=\"\\",
"output": "x/x;test=\"\\\\\""
},
"Whitespace (not handled by generated-mime-types.json or above)",
{
"input": "x/x;x= ",
"output": "x/x"
},
{
"input": "x/x;x=\t",
"output": "x/x"
},
{
"input": "x/x\n\r\t ;x=x",
"output": "x/x;x=x"
},
{
"input": "\n\r\t x/x;x=x\n\r\t ",
"output": "x/x;x=x"
},
{
"input": "x/x;\n\r\t x=x\n\r\t ;x=y",
"output": "x/x;x=x"
},
"Latin1",
{
"input": "text/html;test=\u00FF;charset=gbk",
"output": "text/html;test=\"\u00FF\";charset=gbk",
"navigable": true,
"encoding": "GBK"
},
">Latin1",
{
"input": "x/x;test=\uFFFD;x=x",
"output": "x/x;x=x"
},
"Failure",
{
"input": "\u000Bx/x",
"output": null
},
{
"input": "\u000Cx/x",
"output": null
},
{
"input": "x/x\u000B",
"output": null
},
{
"input": "x/x\u000C",
"output": null
},
{
"input": "",
"output": null
},
{
"input": "\t",
"output": null
},
{
"input": "/",
"output": null
},
{
"input": "bogus",
"output": null
},
{
"input": "bogus/",
"output": null
},
{
"input": "bogus/ ",
"output": null
},
{
"input": "bogus/bogus/;",
"output": null
},
{
"input": "</>",
"output": null
},
{
"input": "(/)",
"output": null
},
{
"input": "ÿ/ÿ",
"output": null
},
{
"input": "text/html(;doesnot=matter",
"output": null
},
{
"input": "{/}",
"output": null
},
{
"input": "\u0100/\u0100",
"output": null
},
{
"input": "text /html",
"output": null
},
{
"input": "text/ html",
"output": null
},
{
"input": "\"text/html\"",
"output": null
}
];

View File

@ -72,6 +72,7 @@ const expectedModules = new Set([
'NativeModule internal/histogram',
'NativeModule internal/idna',
'NativeModule internal/linkedlist',
'NativeModule internal/mime',
'NativeModule internal/modules/cjs/helpers',
'NativeModule internal/modules/cjs/loader',
'NativeModule internal/modules/esm/assert',

View File

@ -164,7 +164,7 @@ new RuleTester({
},
{
code: 'RegExpPrototypeSymbolSearch(/some regex/, "some string")',
errors: [{ message: /looks up the "exec" property/ }],
errors: [{ message: /SafeStringPrototypeSearch/ }],
},
{
code: 'StringPrototypeMatch("some string", /some regex/)',
@ -204,7 +204,7 @@ new RuleTester({
},
{
code: 'StringPrototypeSearch("some string", /some regex/)',
errors: [{ message: /looks up the Symbol\.search property/ }],
errors: [{ message: /SafeStringPrototypeSearch/ }],
},
{
code: 'StringPrototypeSplit("some string", /some regex/)',

View File

@ -0,0 +1,160 @@
'use strict';
require('../common');
const assert = require('assert');
const { MIMEType, MIMEParams } = require('util');
const WHITESPACES = '\t\n\f\r ';
const NOT_HTTP_TOKEN_CODE_POINT = ',';
const NOT_HTTP_QUOTED_STRING_CODE_POINT = '\n';
const mime = new MIMEType('application/ecmascript; ');
const mime_descriptors = Object.getOwnPropertyDescriptors(mime);
const mime_proto = Object.getPrototypeOf(mime);
const mime_impersonator = Object.create(mime_proto);
for (const key of Object.keys(mime_descriptors)) {
const descriptor = mime_descriptors[key];
if (descriptor.get) {
assert.throws(descriptor.get.call(mime_impersonator), /Invalid receiver/i);
}
if (descriptor.set) {
assert.throws(descriptor.set.call(mime_impersonator, 'x'), /Invalid receiver/i);
}
}
assert.strictEqual(
JSON.stringify(mime),
JSON.stringify('application/ecmascript'));
assert.strictEqual(`${mime}`, 'application/ecmascript');
assert.strictEqual(mime.essence, 'application/ecmascript');
assert.strictEqual(mime.type, 'application');
assert.strictEqual(mime.subtype, 'ecmascript');
assert.ok(mime.params);
assert.deepStrictEqual([], [...mime.params]);
assert.strictEqual(mime.params.has('not found'), false);
assert.strictEqual(mime.params.get('not found'), null);
assert.strictEqual(mime.params.delete('not found'), undefined);
mime.type = 'text';
assert.strictEqual(mime.type, 'text');
assert.strictEqual(JSON.stringify(mime), JSON.stringify('text/ecmascript'));
assert.strictEqual(`${mime}`, 'text/ecmascript');
assert.strictEqual(mime.essence, 'text/ecmascript');
assert.throws(() => {
mime.type = `${WHITESPACES}text`;
}, /ERR_INVALID_MIME_SYNTAX/);
assert.throws(() => mime.type = '', /type/i);
assert.throws(() => mime.type = '/', /type/i);
assert.throws(() => mime.type = 'x/', /type/i);
assert.throws(() => mime.type = '/x', /type/i);
assert.throws(() => mime.type = NOT_HTTP_TOKEN_CODE_POINT, /type/i);
assert.throws(() => mime.type = `${NOT_HTTP_TOKEN_CODE_POINT}/`, /type/i);
assert.throws(() => mime.type = `/${NOT_HTTP_TOKEN_CODE_POINT}`, /type/i);
mime.subtype = 'javascript';
assert.strictEqual(mime.type, 'text');
assert.strictEqual(JSON.stringify(mime), JSON.stringify('text/javascript'));
assert.strictEqual(`${mime}`, 'text/javascript');
assert.strictEqual(mime.essence, 'text/javascript');
assert.strictEqual(`${mime.params}`, '');
assert.strictEqual(`${new MIMEParams()}`, '');
assert.strictEqual(`${new MIMEParams(mime.params)}`, '');
assert.strictEqual(`${new MIMEParams(`${mime.params}`)}`, '');
assert.throws(() => {
mime.subtype = `javascript${WHITESPACES}`;
}, /ERR_INVALID_MIME_SYNTAX/);
assert.throws(() => mime.subtype = '', /subtype/i);
assert.throws(() => mime.subtype = ';', /subtype/i);
assert.throws(() => mime.subtype = 'x;', /subtype/i);
assert.throws(() => mime.subtype = ';x', /subtype/i);
assert.throws(() => mime.subtype = NOT_HTTP_TOKEN_CODE_POINT, /subtype/i);
assert.throws(
() => mime.subtype = `${NOT_HTTP_TOKEN_CODE_POINT};`,
/subtype/i);
assert.throws(
() => mime.subtype = `;${NOT_HTTP_TOKEN_CODE_POINT}`,
/subtype/i);
const params = mime.params;
params.set('charset', 'utf-8');
assert.strictEqual(params.has('charset'), true);
assert.strictEqual(params.get('charset'), 'utf-8');
assert.deepStrictEqual([...params], [['charset', 'utf-8']]);
assert.strictEqual(
JSON.stringify(mime),
JSON.stringify('text/javascript;charset=utf-8'));
assert.strictEqual(`${mime}`, 'text/javascript;charset=utf-8');
assert.strictEqual(mime.essence, 'text/javascript');
assert.strictEqual(`${mime.params}`, 'charset=utf-8');
assert.strictEqual(`${new MIMEParams(mime.params)}`, '');
assert.strictEqual(`${new MIMEParams(`${mime.params}`)}`, '');
params.set('goal', 'module');
assert.strictEqual(params.has('goal'), true);
assert.strictEqual(params.get('goal'), 'module');
assert.deepStrictEqual([...params], [['charset', 'utf-8'], ['goal', 'module']]);
assert.strictEqual(
JSON.stringify(mime),
JSON.stringify('text/javascript;charset=utf-8;goal=module'));
assert.strictEqual(`${mime}`, 'text/javascript;charset=utf-8;goal=module');
assert.strictEqual(mime.essence, 'text/javascript');
assert.strictEqual(`${mime.params}`, 'charset=utf-8;goal=module');
assert.strictEqual(`${new MIMEParams(mime.params)}`, '');
assert.strictEqual(`${new MIMEParams(`${mime.params}`)}`, '');
assert.throws(() => {
params.set(`${WHITESPACES}goal`, 'module');
}, /ERR_INVALID_MIME_SYNTAX/);
params.set('charset', 'iso-8859-1');
assert.strictEqual(params.has('charset'), true);
assert.strictEqual(params.get('charset'), 'iso-8859-1');
assert.deepStrictEqual(
[...params],
[['charset', 'iso-8859-1'], ['goal', 'module']]);
assert.strictEqual(
JSON.stringify(mime),
JSON.stringify('text/javascript;charset=iso-8859-1;goal=module'));
assert.strictEqual(`${mime}`, 'text/javascript;charset=iso-8859-1;goal=module');
assert.strictEqual(mime.essence, 'text/javascript');
params.delete('charset');
assert.strictEqual(params.has('charset'), false);
assert.strictEqual(params.get('charset'), null);
assert.deepStrictEqual([...params], [['goal', 'module']]);
assert.strictEqual(
JSON.stringify(mime),
JSON.stringify('text/javascript;goal=module'));
assert.strictEqual(`${mime}`, 'text/javascript;goal=module');
assert.strictEqual(mime.essence, 'text/javascript');
params.set('x', '');
assert.strictEqual(params.has('x'), true);
assert.strictEqual(params.get('x'), '');
assert.deepStrictEqual([...params], [['goal', 'module'], ['x', '']]);
assert.strictEqual(
JSON.stringify(mime),
JSON.stringify('text/javascript;goal=module;x=""'));
assert.strictEqual(`${mime}`, 'text/javascript;goal=module;x=""');
assert.strictEqual(mime.essence, 'text/javascript');
assert.throws(() => params.set('', 'x'), /parameter name/i);
assert.throws(() => params.set('=', 'x'), /parameter name/i);
assert.throws(() => params.set('x=', 'x'), /parameter name/i);
assert.throws(() => params.set('=x', 'x'), /parameter name/i);
assert.throws(() => params.set(`${NOT_HTTP_TOKEN_CODE_POINT}=`, 'x'), /parameter name/i);
assert.throws(() => params.set(`${NOT_HTTP_TOKEN_CODE_POINT}x`, 'x'), /parameter name/i);
assert.throws(() => params.set(`x${NOT_HTTP_TOKEN_CODE_POINT}`, 'x'), /parameter name/i);
assert.throws(() => params.set('x', `${NOT_HTTP_QUOTED_STRING_CODE_POINT};`), /parameter value/i);
assert.throws(() => params.set('x', `${NOT_HTTP_QUOTED_STRING_CODE_POINT}x`), /parameter value/i);
assert.throws(() => params.set('x', `x${NOT_HTTP_QUOTED_STRING_CODE_POINT}`), /parameter value/i);

View File

@ -0,0 +1,23 @@
'use strict';
require('../common');
const assert = require('assert');
const { MIMEType } = require('util');
const fixtures = require('../common/fixtures');
function test(mimes) {
for (const entry of mimes) {
if (typeof entry === 'string') continue;
const { input, output } = entry;
if (output === null) {
assert.throws(() => new MIMEType(input), /ERR_INVALID_MIME_SYNTAX/i);
} else {
const str = `${new MIMEType(input)}`;
assert.strictEqual(str, output);
}
}
}
// These come from https://github.com/web-platform-tests/wpt/tree/master/mimesniff/mime-types/resources
test(require(fixtures.path('./mime-whatwg.js')));
test(require(fixtures.path('./mime-whatwg-generated.js')));

View File

@ -0,0 +1,119 @@
// Flags: --expose-internals
'use strict';
const { mustNotCall } = require('../common');
const assert = require('assert');
const {
RegExpPrototypeSymbolReplace,
RegExpPrototypeSymbolSearch,
RegExpPrototypeSymbolSplit,
SafeStringPrototypeSearch,
hardenRegExp,
} = require('internal/test/binding').primordials;
Object.defineProperties(RegExp.prototype, {
[Symbol.match]: {
get: mustNotCall('get %RegExp.prototype%[@@match]'),
set: mustNotCall('set %RegExp.prototype%[@@match]'),
},
[Symbol.matchAll]: {
get: mustNotCall('get %RegExp.prototype%[@@matchAll]'),
set: mustNotCall('set %RegExp.prototype%[@@matchAll]'),
},
[Symbol.replace]: {
get: mustNotCall('get %RegExp.prototype%[@@replace]'),
set: mustNotCall('set %RegExp.prototype%[@@replace]'),
},
[Symbol.search]: {
get: mustNotCall('get %RegExp.prototype%[@@search]'),
set: mustNotCall('set %RegExp.prototype%[@@search]'),
},
[Symbol.split]: {
get: mustNotCall('get %RegExp.prototype%[@@split]'),
set: mustNotCall('set %RegExp.prototype%[@@split]'),
},
dotAll: {
get: mustNotCall('get %RegExp.prototype%.dotAll'),
set: mustNotCall('set %RegExp.prototype%.dotAll'),
},
exec: {
get: mustNotCall('get %RegExp.prototype%.exec'),
set: mustNotCall('set %RegExp.prototype%.exec'),
},
flags: {
get: mustNotCall('get %RegExp.prototype%.flags'),
set: mustNotCall('set %RegExp.prototype%.flags'),
},
global: {
get: mustNotCall('get %RegExp.prototype%.global'),
set: mustNotCall('set %RegExp.prototype%.global'),
},
hasIndices: {
get: mustNotCall('get %RegExp.prototype%.hasIndices'),
set: mustNotCall('set %RegExp.prototype%.hasIndices'),
},
ignoreCase: {
get: mustNotCall('get %RegExp.prototype%.ignoreCase'),
set: mustNotCall('set %RegExp.prototype%.ignoreCase'),
},
multiline: {
get: mustNotCall('get %RegExp.prototype%.multiline'),
set: mustNotCall('set %RegExp.prototype%.multiline'),
},
source: {
get: mustNotCall('get %RegExp.prototype%.source'),
set: mustNotCall('set %RegExp.prototype%.source'),
},
sticky: {
get: mustNotCall('get %RegExp.prototype%.sticky'),
set: mustNotCall('set %RegExp.prototype%.sticky'),
},
test: {
get: mustNotCall('get %RegExp.prototype%.test'),
set: mustNotCall('set %RegExp.prototype%.test'),
},
toString: {
get: mustNotCall('get %RegExp.prototype%.toString'),
set: mustNotCall('set %RegExp.prototype%.toString'),
},
unicode: {
get: mustNotCall('get %RegExp.prototype%.unicode'),
set: mustNotCall('set %RegExp.prototype%.unicode'),
},
});
hardenRegExp(hardenRegExp(/1/));
// IMO there are no valid use cases in node core to use RegExpPrototypeSymbolMatch
// or RegExpPrototypeSymbolMatchAll, they are inherently unsafe.
{
const myRegex = hardenRegExp(/a/);
assert.strictEqual(RegExpPrototypeSymbolReplace(myRegex, 'baar', 'e'), 'bear');
}
{
const myRegex = hardenRegExp(/a/g);
assert.strictEqual(RegExpPrototypeSymbolReplace(myRegex, 'baar', 'e'), 'beer');
}
{
const myRegex = hardenRegExp(/a/);
assert.strictEqual(RegExpPrototypeSymbolSearch(myRegex, 'baar'), 1);
}
{
const myRegex = /a/;
assert.strictEqual(SafeStringPrototypeSearch('baar', myRegex), 1);
}
{
const myRegex = hardenRegExp(/a/);
assert.deepStrictEqual(RegExpPrototypeSymbolSplit(myRegex, 'baar', 0), []);
}
{
const myRegex = hardenRegExp(/a/);
assert.deepStrictEqual(RegExpPrototypeSymbolSplit(myRegex, 'baar', 1), ['b']);
}
{
const myRegex = hardenRegExp(/a/);
assert.deepStrictEqual(RegExpPrototypeSymbolSplit(myRegex, 'baar'), ['b', '', 'r']);
}

View File

@ -217,6 +217,8 @@ const customTypesMap = {
'URL': 'url.html#the-whatwg-url-api',
'URLSearchParams': 'url.html#class-urlsearchparams',
'MIMEParams': 'util.html#class-utilmimeparams',
'vm.Module': 'vm.html#class-vmmodule',
'vm.Script': 'vm.html#class-vmscript',
'vm.SourceTextModule': 'vm.html#class-vmsourcetextmodule',

View File

@ -135,17 +135,22 @@ module.exports = {
}],
});
},
[CallExpression(/^RegExpPrototypeSymbol(Match|MatchAll|Search)$/)](node) {
[CallExpression(/^RegExpPrototypeSymbol(Match|MatchAll)$/)](node) {
context.report({
node,
message: node.callee.name + ' looks up the "exec" property of `this` value',
});
},
[CallExpression(/^(RegExpPrototypeSymbol|StringPrototype)Search$/)](node) {
context.report({
node,
message: node.callee.name + ' is unsafe, use SafeStringPrototypeSearch instead',
});
},
...createUnsafeStringMethodReport(context, '%String.prototype.match%', 'Symbol.match'),
...createUnsafeStringMethodReport(context, '%String.prototype.matchAll%', 'Symbol.matchAll'),
...createUnsafeStringMethodOnRegexReport(context, '%String.prototype.replace%', 'Symbol.replace'),
...createUnsafeStringMethodOnRegexReport(context, '%String.prototype.replaceAll%', 'Symbol.replace'),
...createUnsafeStringMethodReport(context, '%String.prototype.search%', 'Symbol.search'),
...createUnsafeStringMethodOnRegexReport(context, '%String.prototype.split%', 'Symbol.split'),
'NewExpression[callee.name="Proxy"][arguments.1.type="ObjectExpression"]'(node) {