Skip to content

Commit d41f8ff

Browse files
bmeckTrottaduh95
authored andcommittedNov 10, 2022
util: add MIME utilities (#21128)
Co-authored-by: Rich Trott <rtrott@gmail.com> Co-authored-by: Antoine du Hamel <duhamelantoine1995@gmail.com> PR-URL: #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>
1 parent 9ed9aa8 commit d41f8ff

16 files changed

+5123
-17
lines changed
 

‎doc/api/errors.md

+6
Original file line numberDiff line numberDiff line change
@@ -1967,6 +1967,12 @@ An invalid HTTP token was supplied.
19671967

19681968
An IP address is not valid.
19691969

1970+
<a id="ERR_INVALID_MIME_SYNTAX"></a>
1971+
1972+
### `ERR_INVALID_MIME_SYNTAX`
1973+
1974+
The syntax of a MIME is not valid.
1975+
19701976
<a id="ERR_INVALID_MODULE"></a>
19711977

19721978
### `ERR_INVALID_MODULE`

‎doc/api/util.md

+353
Original file line numberDiff line numberDiff line change
@@ -1024,6 +1024,355 @@ Otherwise, returns `false`.
10241024
See [`assert.deepStrictEqual()`][] for more information about deep strict
10251025
equality.
10261026

1027+
## Class: `util.MIMEType`
1028+
1029+
<!-- YAML
1030+
added: REPLACEME
1031+
-->
1032+
1033+
> Stability: 1 - Experimental
1034+
1035+
An implementation of [the MIMEType class](https://bmeck.github.io/node-proposal-mime-api/).
1036+
1037+
In accordance with browser conventions, all properties of `MIMEType` objects
1038+
are implemented as getters and setters on the class prototype, rather than as
1039+
data properties on the object itself.
1040+
1041+
A MIME string is a structured string containing multiple meaningful
1042+
components. When parsed, a `MIMEType` object is returned containing
1043+
properties for each of these components.
1044+
1045+
### Constructor: `new MIMEType(input)`
1046+
1047+
* `input` {string} The input MIME to parse
1048+
1049+
Creates a new `MIMEType` object by parsing the `input`.
1050+
1051+
```mjs
1052+
import { MIMEType } from 'node:util';
1053+
1054+
const myMIME = new MIMEType('text/plain');
1055+
```
1056+
1057+
```cjs
1058+
const { MIMEType } = require('node:util');
1059+
1060+
const myMIME = new MIMEType('text/plain');
1061+
```
1062+
1063+
A `TypeError` will be thrown if the `input` is not a valid MIME. Note
1064+
that an effort will be made to coerce the given values into strings. For
1065+
instance:
1066+
1067+
```mjs
1068+
import { MIMEType } from 'node:util';
1069+
const myMIME = new MIMEType({ toString: () => 'text/plain' });
1070+
console.log(String(myMIME));
1071+
// Prints: text/plain
1072+
```
1073+
1074+
```cjs
1075+
const { MIMEType } = require('node:util');
1076+
const myMIME = new MIMEType({ toString: () => 'text/plain' });
1077+
console.log(String(myMIME));
1078+
// Prints: text/plain
1079+
```
1080+
1081+
#### `mime.type`
1082+
1083+
* {string}
1084+
1085+
Gets and sets the type portion of the MIME.
1086+
1087+
```mjs
1088+
import { MIMEType } from 'node:util';
1089+
1090+
const myMIME = new MIMEType('text/javascript');
1091+
console.log(myMIME.type);
1092+
// Prints: text
1093+
myMIME.type = 'application';
1094+
console.log(myMIME.type);
1095+
// Prints: application
1096+
console.log(String(myMIME));
1097+
// Prints: application/javascript
1098+
```
1099+
1100+
```cjs
1101+
const { MIMEType } = require('node:util');
1102+
1103+
const myMIME = new MIMEType('text/javascript');
1104+
console.log(myMIME.type);
1105+
// Prints: text
1106+
myMIME.type = 'application';
1107+
console.log(myMIME.type);
1108+
// Prints: application
1109+
console.log(String(myMIME));
1110+
// Prints: application/javascript/javascript
1111+
```
1112+
1113+
#### `mime.subtype`
1114+
1115+
* {string}
1116+
1117+
Gets and sets the subtype portion of the MIME.
1118+
1119+
```mjs
1120+
import { MIMEType } from 'node:util';
1121+
1122+
const myMIME = new MIMEType('text/ecmascript');
1123+
console.log(myMIME.subtype);
1124+
// Prints: ecmascript
1125+
myMIME.subtype = 'javascript';
1126+
console.log(myMIME.subtype);
1127+
// Prints: javascript
1128+
console.log(String(myMIME));
1129+
// Prints: text/javascript
1130+
```
1131+
1132+
```cjs
1133+
const { MIMEType } = require('node:util');
1134+
1135+
const myMIME = new MIMEType('text/ecmascript');
1136+
console.log(myMIME.subtype);
1137+
// Prints: ecmascript
1138+
myMIME.subtype = 'javascript';
1139+
console.log(myMIME.subtype);
1140+
// Prints: javascript
1141+
console.log(String(myMIME));
1142+
// Prints: text/javascript
1143+
```
1144+
1145+
#### `mime.essence`
1146+
1147+
* {string}
1148+
1149+
Gets the essence of the MIME. This property is read only.
1150+
Use `mime.type` or `mime.subtype` to alter the MIME.
1151+
1152+
```mjs
1153+
import { MIMEType } from 'node:util';
1154+
1155+
const myMIME = new MIMEType('text/javascript;key=value');
1156+
console.log(myMIME.essence);
1157+
// Prints: text/javascript
1158+
myMIME.type = 'application';
1159+
console.log(myMIME.essence);
1160+
// Prints: application/javascript
1161+
console.log(String(myMIME));
1162+
// Prints: application/javascript;key=value
1163+
```
1164+
1165+
```cjs
1166+
const { MIMEType } = require('node:util');
1167+
1168+
const myMIME = new MIMEType('text/javascript;key=value');
1169+
console.log(myMIME.essence);
1170+
// Prints: text/javascript
1171+
myMIME.type = 'application';
1172+
console.log(myMIME.essence);
1173+
// Prints: application/javascript
1174+
console.log(String(myMIME));
1175+
// Prints: application/javascript;key=value
1176+
```
1177+
1178+
#### `mime.params`
1179+
1180+
* {MIMEParams}
1181+
1182+
Gets the [`MIMEParams`][] object representing the
1183+
parameters of the MIME. This property is read-only. See
1184+
[`MIMEParams`][] documentation for details.
1185+
1186+
#### `mime.toString()`
1187+
1188+
* Returns: {string}
1189+
1190+
The `toString()` method on the `MIMEType` object returns the serialized MIME.
1191+
1192+
Because of the need for standard compliance, this method does not allow users
1193+
to customize the serialization process of the MIME.
1194+
1195+
#### `mime.toJSON()`
1196+
1197+
* Returns: {string}
1198+
1199+
Alias for [`mime.toString()`][].
1200+
1201+
This method is automatically called when an `MIMEType` object is serialized
1202+
with [`JSON.stringify()`][].
1203+
1204+
```mjs
1205+
import { MIMEType } from 'node:util';
1206+
1207+
const myMIMES = [
1208+
new MIMEType('image/png'),
1209+
new MIMEType('image/gif'),
1210+
];
1211+
console.log(JSON.stringify(myMIMES));
1212+
// Prints: ["image/png", "image/gif"]
1213+
```
1214+
1215+
```cjs
1216+
const { MIMEType } = require('node:util');
1217+
1218+
const myMIMES = [
1219+
new MIMEType('image/png'),
1220+
new MIMEType('image/gif'),
1221+
];
1222+
console.log(JSON.stringify(myMIMES));
1223+
// Prints: ["image/png", "image/gif"]
1224+
```
1225+
1226+
### Class: `util.MIMEParams`
1227+
1228+
<!-- YAML
1229+
added: REPLACEME
1230+
-->
1231+
1232+
The `MIMEParams` API provides read and write access to the parameters of a
1233+
`MIMEType`.
1234+
1235+
#### Constructor: `new MIMEParams()`
1236+
1237+
Creates a new `MIMEParams` object by with empty parameters
1238+
1239+
```mjs
1240+
import { MIMEParams } from 'node:util';
1241+
1242+
const myParams = new MIMEParams();
1243+
```
1244+
1245+
```cjs
1246+
const { MIMEParams } = require('node:util');
1247+
1248+
const myParams = new MIMEParams();
1249+
```
1250+
1251+
#### `mimeParams.delete(name)`
1252+
1253+
* `name` {string}
1254+
1255+
Remove all name-value pairs whose name is `name`.
1256+
1257+
#### `mimeParams.entries()`
1258+
1259+
* Returns: {Iterator}
1260+
1261+
Returns an iterator over each of the name-value pairs in the parameters.
1262+
Each item of the iterator is a JavaScript `Array`. The first item of the array
1263+
is the `name`, the second item of the array is the `value`.
1264+
1265+
#### `mimeParams.get(name)`
1266+
1267+
* `name` {string}
1268+
* Returns: {string} or `null` if there is no name-value pair with the given
1269+
`name`.
1270+
1271+
Returns the value of the first name-value pair whose name is `name`. If there
1272+
are no such pairs, `null` is returned.
1273+
1274+
#### `mimeParams.has(name)`
1275+
1276+
* `name` {string}
1277+
* Returns: {boolean}
1278+
1279+
Returns `true` if there is at least one name-value pair whose name is `name`.
1280+
1281+
#### `mimeParams.keys()`
1282+
1283+
* Returns: {Iterator}
1284+
1285+
Returns an iterator over the names of each name-value pair.
1286+
1287+
```mjs
1288+
import { MIMEType } from 'node:util';
1289+
1290+
const { params } = new MIMEType('text/plain;foo=0;bar=1');
1291+
for (const name of params.keys()) {
1292+
console.log(name);
1293+
}
1294+
// Prints:
1295+
// foo
1296+
// bar
1297+
```
1298+
1299+
```cjs
1300+
const { MIMEType } = require('node:util');
1301+
1302+
const { params } = new MIMEType('text/plain;foo=0;bar=1');
1303+
for (const name of params.keys()) {
1304+
console.log(name);
1305+
}
1306+
// Prints:
1307+
// foo
1308+
// bar
1309+
```
1310+
1311+
#### `mimeParams.set(name, value)`
1312+
1313+
* `name` {string}
1314+
* `value` {string}
1315+
1316+
Sets the value in the `MIMEParams` object associated with `name` to
1317+
`value`. If there are any pre-existing name-value pairs whose names are `name`,
1318+
set the first such pair's value to `value`.
1319+
1320+
```mjs
1321+
import { MIMEType } from 'node:util';
1322+
1323+
const { params } = new MIMEType('text/plain;foo=0;bar=1');
1324+
params.set('foo', 'def');
1325+
params.set('baz', 'xyz');
1326+
console.log(params.toString());
1327+
// Prints: foo=def&bar=1&baz=xyz
1328+
```
1329+
1330+
```cjs
1331+
const { MIMEType } = require('node:util');
1332+
1333+
const { params } = new MIMEType('text/plain;foo=0;bar=1');
1334+
params.set('foo', 'def');
1335+
params.set('baz', 'xyz');
1336+
console.log(params.toString());
1337+
// Prints: foo=def&bar=1&baz=xyz
1338+
```
1339+
1340+
#### `mimeParams.values()`
1341+
1342+
* Returns: {Iterator}
1343+
1344+
Returns an iterator over the values of each name-value pair.
1345+
1346+
#### `mimeParams[@@iterator]()`
1347+
1348+
* Returns: {Iterator}
1349+
1350+
Alias for [`mimeParams.entries()`][].
1351+
1352+
```mjs
1353+
import { MIMEType } from 'node:util';
1354+
1355+
const { params } = new MIMEType('text/plain;foo=bar;xyz=baz');
1356+
for (const [name, value] of params) {
1357+
console.log(name, value);
1358+
}
1359+
// Prints:
1360+
// foo bar
1361+
// xyz baz
1362+
```
1363+
1364+
```cjs
1365+
const { MIMEType } = require('node:util');
1366+
1367+
const { params } = new MIMEType('text/plain;foo=bar;xyz=baz');
1368+
for (const [name, value] of params) {
1369+
console.log(name, value);
1370+
}
1371+
// Prints:
1372+
// foo bar
1373+
// xyz baz
1374+
```
1375+
10271376
## `util.parseArgs([config])`
10281377

10291378
<!-- YAML
@@ -2903,6 +3252,8 @@ util.log('Timestamped message.');
29033252
[`Int16Array`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Int16Array
29043253
[`Int32Array`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Int32Array
29053254
[`Int8Array`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Int8Array
3255+
[`JSON.stringify()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify
3256+
[`MIMEparams`]: #class-utilmimeparams
29063257
[`Map`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map
29073258
[`Object.assign()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
29083259
[`Object.freeze()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze
@@ -2920,6 +3271,8 @@ util.log('Timestamped message.');
29203271
[`WebAssembly.Module`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/Module
29213272
[`assert.deepStrictEqual()`]: assert.md#assertdeepstrictequalactual-expected-message
29223273
[`console.error()`]: console.md#consoleerrordata-args
3274+
[`mime.toString()`]: #mimetostring
3275+
[`mimeParams.entries()`]: #mimeparamsentries
29233276
[`napi_create_external()`]: n-api.md#napi_create_external
29243277
[`target` and `handler`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy#Terminology
29253278
[`tty.hasColors()`]: tty.md#writestreamhascolorscount-env

‎doc/contributing/primordials.md

+4-13
Original file line numberDiff line numberDiff line change
@@ -639,6 +639,9 @@ RegExp.prototype.exec = () => null;
639639
// Core
640640
console.log(RegExpPrototypeTest(/o/, 'foo')); // false
641641
console.log(RegExpPrototypeExec(/o/, 'foo') !== null); // true
642+
643+
console.log(RegExpPrototypeSymbolSearch(/o/, 'foo')); // -1
644+
console.log(SafeStringPrototypeSearch('foo', /o/)); // 1
642645
```
643646

644647
#### Don't trust `RegExp` flags
@@ -668,19 +671,7 @@ Object.defineProperty(RegExp.prototype, 'global', { value: false });
668671

669672
// Core
670673
console.log(RegExpPrototypeSymbolReplace(/o/g, 'foo', 'a')); // 'fao'
671-
672-
const regex = /o/g;
673-
ObjectDefineProperties(regex, {
674-
dotAll: { value: false },
675-
exec: { value: undefined },
676-
flags: { value: 'g' },
677-
global: { value: true },
678-
ignoreCase: { value: false },
679-
multiline: { value: false },
680-
unicode: { value: false },
681-
sticky: { value: false },
682-
});
683-
console.log(RegExpPrototypeSymbolReplace(regex, 'foo', 'a')); // 'faa'
674+
console.log(RegExpPrototypeSymbolReplace(hardenRegExp(/o/g), 'foo', 'a')); // 'faa'
684675
```
685676

686677
### Defining object own properties

‎lib/internal/errors.js

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

‎lib/internal/mime.js

+367
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,367 @@
1+
'use strict';
2+
3+
const {
4+
FunctionPrototypeCall,
5+
ObjectDefineProperty,
6+
RegExpPrototypeExec,
7+
SafeMap,
8+
SafeStringPrototypeSearch,
9+
StringPrototypeCharAt,
10+
StringPrototypeIndexOf,
11+
StringPrototypeSlice,
12+
StringPrototypeToLowerCase,
13+
SymbolIterator,
14+
} = primordials;
15+
const {
16+
ERR_INVALID_MIME_SYNTAX,
17+
} = require('internal/errors').codes;
18+
19+
const NOT_HTTP_TOKEN_CODE_POINT = /[^!#$%&'*+\-.^_`|~A-Za-z0-9]/g;
20+
const NOT_HTTP_QUOTED_STRING_CODE_POINT = /[^\t\u0020-~\u0080-\u00FF]/g;
21+
22+
const END_BEGINNING_WHITESPACE = /[^\r\n\t ]|$/;
23+
const START_ENDING_WHITESPACE = /[\r\n\t ]*$/;
24+
25+
function toASCIILower(str) {
26+
let result = '';
27+
for (let i = 0; i < str.length; i++) {
28+
const char = str[i];
29+
30+
result += char >= 'A' && char <= 'Z' ?
31+
StringPrototypeToLowerCase(char) :
32+
char;
33+
}
34+
return result;
35+
}
36+
37+
const SOLIDUS = '/';
38+
const SEMICOLON = ';';
39+
function parseTypeAndSubtype(str) {
40+
// Skip only HTTP whitespace from start
41+
let position = SafeStringPrototypeSearch(str, END_BEGINNING_WHITESPACE);
42+
// read until '/'
43+
const typeEnd = StringPrototypeIndexOf(str, SOLIDUS, position);
44+
const trimmedType = typeEnd === -1 ?
45+
StringPrototypeSlice(str, position) :
46+
StringPrototypeSlice(str, position, typeEnd);
47+
const invalidTypeIndex = SafeStringPrototypeSearch(trimmedType,
48+
NOT_HTTP_TOKEN_CODE_POINT);
49+
if (trimmedType === '' || invalidTypeIndex !== -1 || typeEnd === -1) {
50+
throw new ERR_INVALID_MIME_SYNTAX('type', str, invalidTypeIndex);
51+
}
52+
// skip type and '/'
53+
position = typeEnd + 1;
54+
const type = toASCIILower(trimmedType);
55+
// read until ';'
56+
const subtypeEnd = StringPrototypeIndexOf(str, SEMICOLON, position);
57+
const rawSubtype = subtypeEnd === -1 ?
58+
StringPrototypeSlice(str, position) :
59+
StringPrototypeSlice(str, position, subtypeEnd);
60+
position += rawSubtype.length;
61+
if (subtypeEnd !== -1) {
62+
// skip ';'
63+
position += 1;
64+
}
65+
const trimmedSubtype = StringPrototypeSlice(
66+
rawSubtype,
67+
0,
68+
SafeStringPrototypeSearch(rawSubtype, START_ENDING_WHITESPACE));
69+
const invalidSubtypeIndex = SafeStringPrototypeSearch(trimmedSubtype,
70+
NOT_HTTP_TOKEN_CODE_POINT);
71+
if (trimmedSubtype === '' || invalidSubtypeIndex !== -1) {
72+
throw new ERR_INVALID_MIME_SYNTAX('subtype', str, trimmedSubtype);
73+
}
74+
const subtype = toASCIILower(trimmedSubtype);
75+
return {
76+
__proto__: null,
77+
type,
78+
subtype,
79+
parametersStringIndex: position,
80+
};
81+
}
82+
83+
const EQUALS_SEMICOLON_OR_END = /[;=]|$/;
84+
const QUOTED_VALUE_PATTERN = /^(?:([\\]$)|[\\][\s\S]|[^"])*(?:(")|$)/u;
85+
86+
function removeBackslashes(str) {
87+
let ret = '';
88+
// We stop at str.length - 1 because we want to look ahead one character.
89+
let i;
90+
for (i = 0; i < str.length - 1; i++) {
91+
const c = str[i];
92+
if (c === '\\') {
93+
i++;
94+
ret += str[i];
95+
} else {
96+
ret += c;
97+
}
98+
}
99+
// We add the last character if we didn't skip to it.
100+
if (i === str.length - 1) {
101+
ret += str[i];
102+
}
103+
return ret;
104+
}
105+
106+
107+
function escapeQuoteOrSolidus(str) {
108+
let result = '';
109+
for (let i = 0; i < str.length; i++) {
110+
const char = str[i];
111+
result += (char === '"' || char === '\\') ? `\\${char}` : char;
112+
}
113+
return result;
114+
}
115+
116+
const encode = (value) => {
117+
if (value.length === 0) return '""';
118+
const encode = SafeStringPrototypeSearch(value, NOT_HTTP_TOKEN_CODE_POINT) !== -1;
119+
if (!encode) return value;
120+
const escaped = escapeQuoteOrSolidus(value);
121+
return `"${escaped}"`;
122+
};
123+
124+
class MIMEParams {
125+
#data = new SafeMap();
126+
127+
delete(name) {
128+
this.#data.delete(name);
129+
}
130+
131+
get(name) {
132+
const data = this.#data;
133+
if (data.has(name)) {
134+
return data.get(name);
135+
}
136+
return null;
137+
}
138+
139+
has(name) {
140+
return this.#data.has(name);
141+
}
142+
143+
set(name, value) {
144+
const data = this.#data;
145+
name = `${name}`;
146+
value = `${value}`;
147+
const invalidNameIndex = SafeStringPrototypeSearch(name, NOT_HTTP_TOKEN_CODE_POINT);
148+
if (name.length === 0 || invalidNameIndex !== -1) {
149+
throw new ERR_INVALID_MIME_SYNTAX(
150+
'parameter name',
151+
name,
152+
invalidNameIndex,
153+
);
154+
}
155+
const invalidValueIndex = SafeStringPrototypeSearch(
156+
value,
157+
NOT_HTTP_QUOTED_STRING_CODE_POINT);
158+
if (invalidValueIndex !== -1) {
159+
throw new ERR_INVALID_MIME_SYNTAX(
160+
'parameter value',
161+
value,
162+
invalidValueIndex,
163+
);
164+
}
165+
data.set(name, value);
166+
}
167+
168+
*entries() {
169+
yield* this.#data.entries();
170+
}
171+
172+
*keys() {
173+
yield* this.#data.keys();
174+
}
175+
176+
*values() {
177+
yield* this.#data.values();
178+
}
179+
180+
toString() {
181+
let ret = '';
182+
for (const { 0: key, 1: value } of this.#data) {
183+
const encoded = encode(value);
184+
// Ensure they are separated
185+
if (ret.length) ret += ';';
186+
ret += `${key}=${encoded}`;
187+
}
188+
return ret;
189+
}
190+
191+
// Used to act as a friendly class to stringifying stuff
192+
// not meant to be exposed to users, could inject invalid values
193+
static parseParametersString(str, position, params) {
194+
const paramsMap = params.#data;
195+
const endOfSource = SafeStringPrototypeSearch(
196+
StringPrototypeSlice(str, position),
197+
START_ENDING_WHITESPACE
198+
) + position;
199+
while (position < endOfSource) {
200+
// Skip any whitespace before parameter
201+
position += SafeStringPrototypeSearch(
202+
StringPrototypeSlice(str, position),
203+
END_BEGINNING_WHITESPACE
204+
);
205+
// Read until ';' or '='
206+
const afterParameterName = SafeStringPrototypeSearch(
207+
StringPrototypeSlice(str, position),
208+
EQUALS_SEMICOLON_OR_END
209+
) + position;
210+
const parameterString = toASCIILower(
211+
StringPrototypeSlice(str, position, afterParameterName)
212+
);
213+
position = afterParameterName;
214+
// If we found a terminating character
215+
if (position < endOfSource) {
216+
// Safe to use because we never do special actions for surrogate pairs
217+
const char = StringPrototypeCharAt(str, position);
218+
// Skip the terminating character
219+
position += 1;
220+
// Ignore parameters without values
221+
if (char === ';') {
222+
continue;
223+
}
224+
}
225+
// If we are at end of the string, it cannot have a value
226+
if (position >= endOfSource) break;
227+
// Safe to use because we never do special actions for surrogate pairs
228+
const char = StringPrototypeCharAt(str, position);
229+
let parameterValue = null;
230+
if (char === '"') {
231+
// Handle quoted-string form of values
232+
// skip '"'
233+
position += 1;
234+
// Find matching closing '"' or end of string
235+
// use $1 to see if we terminated on unmatched '\'
236+
// use $2 to see if we terminated on a matching '"'
237+
// so we can skip the last char in either case
238+
const insideMatch = RegExpPrototypeExec(
239+
QUOTED_VALUE_PATTERN,
240+
StringPrototypeSlice(str, position));
241+
position += insideMatch[0].length;
242+
// Skip including last character if an unmatched '\' or '"' during
243+
// unescape
244+
const inside = insideMatch[1] || insideMatch[2] ?
245+
StringPrototypeSlice(insideMatch[0], 0, -1) :
246+
insideMatch[0];
247+
// Unescape '\' quoted characters
248+
parameterValue = removeBackslashes(inside);
249+
// If we did have an unmatched '\' add it back to the end
250+
if (insideMatch[1]) parameterValue += '\\';
251+
} else {
252+
// Handle the normal parameter value form
253+
const valueEnd = StringPrototypeIndexOf(str, SEMICOLON, position);
254+
const rawValue = valueEnd === -1 ?
255+
StringPrototypeSlice(str, position) :
256+
StringPrototypeSlice(str, position, valueEnd);
257+
position += rawValue.length;
258+
const trimmedValue = StringPrototypeSlice(
259+
rawValue,
260+
0,
261+
SafeStringPrototypeSearch(rawValue, START_ENDING_WHITESPACE)
262+
);
263+
// Ignore parameters without values
264+
if (trimmedValue === '') continue;
265+
parameterValue = trimmedValue;
266+
}
267+
if (
268+
parameterString !== '' &&
269+
SafeStringPrototypeSearch(parameterString,
270+
NOT_HTTP_TOKEN_CODE_POINT) === -1 &&
271+
SafeStringPrototypeSearch(parameterValue,
272+
NOT_HTTP_QUOTED_STRING_CODE_POINT) === -1 &&
273+
params.has(parameterString) === false
274+
) {
275+
paramsMap.set(parameterString, parameterValue);
276+
}
277+
position++;
278+
}
279+
return paramsMap;
280+
}
281+
}
282+
const MIMEParamsStringify = MIMEParams.prototype.toString;
283+
ObjectDefineProperty(MIMEParams.prototype, SymbolIterator, {
284+
__proto__: null,
285+
configurable: true,
286+
value: MIMEParams.prototype.entries,
287+
writable: true,
288+
});
289+
ObjectDefineProperty(MIMEParams.prototype, 'toJSON', {
290+
__proto__: null,
291+
configurable: true,
292+
value: MIMEParamsStringify,
293+
writable: true,
294+
});
295+
296+
const { parseParametersString } = MIMEParams;
297+
delete MIMEParams.parseParametersString;
298+
299+
class MIMEType {
300+
#type;
301+
#subtype;
302+
#parameters;
303+
constructor(string) {
304+
string = `${string}`;
305+
const data = parseTypeAndSubtype(string);
306+
this.#type = data.type;
307+
this.#subtype = data.subtype;
308+
this.#parameters = new MIMEParams();
309+
parseParametersString(
310+
string,
311+
data.parametersStringIndex,
312+
this.#parameters,
313+
);
314+
}
315+
316+
get type() {
317+
return this.#type;
318+
}
319+
320+
set type(v) {
321+
v = `${v}`;
322+
const invalidTypeIndex = SafeStringPrototypeSearch(v, NOT_HTTP_TOKEN_CODE_POINT);
323+
if (v.length === 0 || invalidTypeIndex !== -1) {
324+
throw new ERR_INVALID_MIME_SYNTAX('type', v, invalidTypeIndex);
325+
}
326+
this.#type = toASCIILower(v);
327+
}
328+
329+
get subtype() {
330+
return this.#subtype;
331+
}
332+
333+
set subtype(v) {
334+
v = `${v}`;
335+
const invalidSubtypeIndex = SafeStringPrototypeSearch(v, NOT_HTTP_TOKEN_CODE_POINT);
336+
if (v.length === 0 || invalidSubtypeIndex !== -1) {
337+
throw new ERR_INVALID_MIME_SYNTAX('subtype', v, invalidSubtypeIndex);
338+
}
339+
this.#subtype = toASCIILower(v);
340+
}
341+
342+
get essence() {
343+
return `${this.#type}/${this.#subtype}`;
344+
}
345+
346+
get params() {
347+
return this.#parameters;
348+
}
349+
350+
toString() {
351+
let ret = `${this.#type}/${this.#subtype}`;
352+
const paramStr = FunctionPrototypeCall(MIMEParamsStringify, this.#parameters);
353+
if (paramStr.length) ret += `;${paramStr}`;
354+
return ret;
355+
}
356+
}
357+
ObjectDefineProperty(MIMEType.prototype, 'toJSON', {
358+
__proto__: null,
359+
configurable: true,
360+
value: MIMEType.prototype.toString,
361+
writable: true,
362+
});
363+
364+
module.exports = {
365+
MIMEParams,
366+
MIMEType,
367+
};

‎lib/internal/per_context/primordials.js

+147
Original file line numberDiff line numberDiff line change
@@ -266,12 +266,36 @@ const {
266266
FinalizationRegistry,
267267
FunctionPrototypeCall,
268268
Map,
269+
ObjectDefineProperties,
270+
ObjectDefineProperty,
269271
ObjectFreeze,
270272
ObjectSetPrototypeOf,
271273
Promise,
272274
PromisePrototypeThen,
275+
ReflectApply,
276+
ReflectConstruct,
277+
ReflectSet,
278+
ReflectGet,
279+
RegExp,
280+
RegExpPrototype,
281+
RegExpPrototypeExec,
282+
RegExpPrototypeGetDotAll,
283+
RegExpPrototypeGetFlags,
284+
RegExpPrototypeGetGlobal,
285+
RegExpPrototypeGetHasIndices,
286+
RegExpPrototypeGetIgnoreCase,
287+
RegExpPrototypeGetMultiline,
288+
RegExpPrototypeGetSource,
289+
RegExpPrototypeGetSticky,
290+
RegExpPrototypeGetUnicode,
273291
Set,
274292
SymbolIterator,
293+
SymbolMatch,
294+
SymbolMatchAll,
295+
SymbolReplace,
296+
SymbolSearch,
297+
SymbolSpecies,
298+
SymbolSplit,
275299
WeakMap,
276300
WeakRef,
277301
WeakSet,
@@ -490,5 +514,128 @@ primordials.SafePromiseRace = (promises, mapFn) =>
490514
);
491515

492516

517+
const {
518+
exec: OriginalRegExpPrototypeExec,
519+
[SymbolMatch]: OriginalRegExpPrototypeSymbolMatch,
520+
[SymbolMatchAll]: OriginalRegExpPrototypeSymbolMatchAll,
521+
[SymbolReplace]: OriginalRegExpPrototypeSymbolReplace,
522+
[SymbolSearch]: OriginalRegExpPrototypeSymbolSearch,
523+
[SymbolSplit]: OriginalRegExpPrototypeSymbolSplit,
524+
} = RegExpPrototype;
525+
526+
class RegExpLikeForStringSplitting {
527+
#regex;
528+
constructor() {
529+
this.#regex = ReflectConstruct(RegExp, arguments);
530+
}
531+
532+
get lastIndex() {
533+
return ReflectGet(this.#regex, 'lastIndex');
534+
}
535+
set lastIndex(value) {
536+
ReflectSet(this.#regex, 'lastIndex', value);
537+
}
538+
539+
exec() {
540+
return ReflectApply(OriginalRegExpPrototypeExec, this.#regex, arguments);
541+
}
542+
}
543+
ObjectSetPrototypeOf(RegExpLikeForStringSplitting.prototype, null);
544+
545+
primordials.hardenRegExp = function hardenRegExp(pattern) {
546+
ObjectDefineProperties(pattern, {
547+
[SymbolMatch]: {
548+
__proto__: null,
549+
configurable: true,
550+
value: OriginalRegExpPrototypeSymbolMatch,
551+
},
552+
[SymbolMatchAll]: {
553+
__proto__: null,
554+
configurable: true,
555+
value: OriginalRegExpPrototypeSymbolMatchAll,
556+
},
557+
[SymbolReplace]: {
558+
__proto__: null,
559+
configurable: true,
560+
value: OriginalRegExpPrototypeSymbolReplace,
561+
},
562+
[SymbolSearch]: {
563+
__proto__: null,
564+
configurable: true,
565+
value: OriginalRegExpPrototypeSymbolSearch,
566+
},
567+
[SymbolSplit]: {
568+
__proto__: null,
569+
configurable: true,
570+
value: OriginalRegExpPrototypeSymbolSplit,
571+
},
572+
constructor: {
573+
__proto__: null,
574+
configurable: true,
575+
value: {
576+
[SymbolSpecies]: RegExpLikeForStringSplitting,
577+
}
578+
},
579+
dotAll: {
580+
__proto__: null,
581+
configurable: true,
582+
value: RegExpPrototypeGetDotAll(pattern),
583+
},
584+
exec: {
585+
__proto__: null,
586+
configurable: true,
587+
value: OriginalRegExpPrototypeExec,
588+
},
589+
global: {
590+
__proto__: null,
591+
configurable: true,
592+
value: RegExpPrototypeGetGlobal(pattern),
593+
},
594+
hasIndices: {
595+
__proto__: null,
596+
configurable: true,
597+
value: RegExpPrototypeGetHasIndices(pattern),
598+
},
599+
ignoreCase: {
600+
__proto__: null,
601+
configurable: true,
602+
value: RegExpPrototypeGetIgnoreCase(pattern),
603+
},
604+
multiline: {
605+
__proto__: null,
606+
configurable: true,
607+
value: RegExpPrototypeGetMultiline(pattern),
608+
},
609+
source: {
610+
__proto__: null,
611+
configurable: true,
612+
value: RegExpPrototypeGetSource(pattern),
613+
},
614+
sticky: {
615+
__proto__: null,
616+
configurable: true,
617+
value: RegExpPrototypeGetSticky(pattern),
618+
},
619+
unicode: {
620+
__proto__: null,
621+
configurable: true,
622+
value: RegExpPrototypeGetUnicode(pattern),
623+
},
624+
});
625+
ObjectDefineProperty(pattern, 'flags', {
626+
__proto__: null,
627+
configurable: true,
628+
value: RegExpPrototypeGetFlags(pattern),
629+
});
630+
return pattern;
631+
};
632+
633+
634+
primordials.SafeStringPrototypeSearch = (str, regexp) => {
635+
regexp.lastIndex = 0;
636+
const match = RegExpPrototypeExec(regexp, str);
637+
return match ? match.index : -1;
638+
};
639+
493640
ObjectSetPrototypeOf(primordials, null);
494641
ObjectFreeze(primordials);

‎lib/util.js

+3
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ const {
6868
validateNumber,
6969
} = require('internal/validators');
7070
const { TextDecoder, TextEncoder } = require('internal/encoding');
71+
const { MIMEType, MIMEParams } = require('internal/mime');
7172
const { isBuffer } = require('buffer').Buffer;
7273
const types = require('internal/util/types');
7374

@@ -385,6 +386,8 @@ module.exports = {
385386
isFunction,
386387
isPrimitive,
387388
log,
389+
MIMEType,
390+
MIMEParams,
388391
parseArgs,
389392
promisify,
390393
stripVTControlCharacters,

‎test/fixtures/mime-whatwg-generated.js

+3,533
Large diffs are not rendered by default.

‎test/fixtures/mime-whatwg.js

+392
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,392 @@
1+
'use strict';
2+
3+
// TODO: Incorporate this with the other WPT tests if it makes sense to do that.
4+
5+
/* The following tests are copied from WPT. Modifications to them should be
6+
upstreamed first. Refs:
7+
https://github.com/w3c/web-platform-tests/blob/88b75886e/url/urltestdata.json
8+
License: http://www.w3.org/Consortium/Legal/2008/04-testsuite-copyright.html
9+
*/
10+
module.exports = [
11+
"Basics",
12+
{
13+
"input": "text/html;charset=gbk",
14+
"output": "text/html;charset=gbk",
15+
"navigable": true,
16+
"encoding": "GBK"
17+
},
18+
{
19+
"input": "TEXT/HTML;CHARSET=GBK",
20+
"output": "text/html;charset=GBK",
21+
"navigable": true,
22+
"encoding": "GBK"
23+
},
24+
"Legacy comment syntax",
25+
{
26+
"input": "text/html;charset=gbk(",
27+
"output": "text/html;charset=\"gbk(\"",
28+
"navigable": true,
29+
"encoding": null
30+
},
31+
{
32+
"input": "text/html;x=(;charset=gbk",
33+
"output": "text/html;x=\"(\";charset=gbk",
34+
"navigable": true,
35+
"encoding": "GBK"
36+
},
37+
"Duplicate parameter",
38+
{
39+
"input": "text/html;charset=gbk;charset=windows-1255",
40+
"output": "text/html;charset=gbk",
41+
"navigable": true,
42+
"encoding": "GBK"
43+
},
44+
{
45+
"input": "text/html;charset=();charset=GBK",
46+
"output": "text/html;charset=\"()\"",
47+
"navigable": true,
48+
"encoding": null
49+
},
50+
"Spaces",
51+
{
52+
"input": "text/html;charset =gbk",
53+
"output": "text/html",
54+
"navigable": true,
55+
"encoding": null
56+
},
57+
{
58+
"input": "text/html ;charset=gbk",
59+
"output": "text/html;charset=gbk",
60+
"navigable": true,
61+
"encoding": "GBK"
62+
},
63+
{
64+
"input": "text/html; charset=gbk",
65+
"output": "text/html;charset=gbk",
66+
"navigable": true,
67+
"encoding": "GBK"
68+
},
69+
{
70+
"input": "text/html;charset= gbk",
71+
"output": "text/html;charset=\" gbk\"",
72+
"navigable": true,
73+
"encoding": "GBK"
74+
},
75+
{
76+
"input": "text/html;charset= \"gbk\"",
77+
"output": "text/html;charset=\" \\\"gbk\\\"\"",
78+
"navigable": true,
79+
"encoding": null
80+
},
81+
"0x0B and 0x0C",
82+
{
83+
"input": "text/html;charset=\u000Bgbk",
84+
"output": "text/html",
85+
"navigable": true,
86+
"encoding": null
87+
},
88+
{
89+
"input": "text/html;charset=\u000Cgbk",
90+
"output": "text/html",
91+
"navigable": true,
92+
"encoding": null
93+
},
94+
{
95+
"input": "text/html;\u000Bcharset=gbk",
96+
"output": "text/html",
97+
"navigable": true,
98+
"encoding": null
99+
},
100+
{
101+
"input": "text/html;\u000Ccharset=gbk",
102+
"output": "text/html",
103+
"navigable": true,
104+
"encoding": null
105+
},
106+
"Single quotes are a token, not a delimiter",
107+
{
108+
"input": "text/html;charset='gbk'",
109+
"output": "text/html;charset='gbk'",
110+
"navigable": true,
111+
"encoding": null
112+
},
113+
{
114+
"input": "text/html;charset='gbk",
115+
"output": "text/html;charset='gbk",
116+
"navigable": true,
117+
"encoding": null
118+
},
119+
{
120+
"input": "text/html;charset=gbk'",
121+
"output": "text/html;charset=gbk'",
122+
"navigable": true,
123+
"encoding": null
124+
},
125+
{
126+
"input": "text/html;charset=';charset=GBK",
127+
"output": "text/html;charset='",
128+
"navigable": true,
129+
"encoding": null
130+
},
131+
"Invalid parameters",
132+
{
133+
"input": "text/html;test;charset=gbk",
134+
"output": "text/html;charset=gbk",
135+
"navigable": true,
136+
"encoding": "GBK"
137+
},
138+
{
139+
"input": "text/html;test=;charset=gbk",
140+
"output": "text/html;charset=gbk",
141+
"navigable": true,
142+
"encoding": "GBK"
143+
},
144+
{
145+
"input": "text/html;';charset=gbk",
146+
"output": "text/html;charset=gbk",
147+
"navigable": true,
148+
"encoding": "GBK"
149+
},
150+
{
151+
"input": "text/html;\";charset=gbk",
152+
"output": "text/html;charset=gbk",
153+
"navigable": true,
154+
"encoding": "GBK"
155+
},
156+
{
157+
"input": "text/html ; ; charset=gbk",
158+
"output": "text/html;charset=gbk",
159+
"navigable": true,
160+
"encoding": "GBK"
161+
},
162+
{
163+
"input": "text/html;;;;charset=gbk",
164+
"output": "text/html;charset=gbk",
165+
"navigable": true,
166+
"encoding": "GBK"
167+
},
168+
{
169+
"input": "text/html;charset= \"\u007F;charset=GBK",
170+
"output": "text/html;charset=GBK",
171+
"navigable": true,
172+
"encoding": "GBK"
173+
},
174+
{
175+
"input": "text/html;charset=\"\u007F;charset=foo\";charset=GBK",
176+
"output": "text/html;charset=GBK",
177+
"navigable": true,
178+
"encoding": "GBK"
179+
},
180+
"Double quotes",
181+
{
182+
"input": "text/html;charset=\"gbk\"",
183+
"output": "text/html;charset=gbk",
184+
"navigable": true,
185+
"encoding": "GBK"
186+
},
187+
{
188+
"input": "text/html;charset=\"gbk",
189+
"output": "text/html;charset=gbk",
190+
"navigable": true,
191+
"encoding": "GBK"
192+
},
193+
{
194+
"input": "text/html;charset=gbk\"",
195+
"output": "text/html;charset=\"gbk\\\"\"",
196+
"navigable": true,
197+
"encoding": null
198+
},
199+
{
200+
"input": "text/html;charset=\" gbk\"",
201+
"output": "text/html;charset=\" gbk\"",
202+
"navigable": true,
203+
"encoding": "GBK"
204+
},
205+
{
206+
"input": "text/html;charset=\"gbk \"",
207+
"output": "text/html;charset=\"gbk \"",
208+
"navigable": true,
209+
"encoding": "GBK"
210+
},
211+
{
212+
"input": "text/html;charset=\"\\ gbk\"",
213+
"output": "text/html;charset=\" gbk\"",
214+
"navigable": true,
215+
"encoding": "GBK"
216+
},
217+
{
218+
"input": "text/html;charset=\"\\g\\b\\k\"",
219+
"output": "text/html;charset=gbk",
220+
"navigable": true,
221+
"encoding": "GBK"
222+
},
223+
{
224+
"input": "text/html;charset=\"gbk\"x",
225+
"output": "text/html;charset=gbk",
226+
"navigable": true,
227+
"encoding": "GBK"
228+
},
229+
{
230+
"input": "text/html;charset=\"\";charset=GBK",
231+
"output": "text/html;charset=\"\"",
232+
"navigable": true,
233+
"encoding": null
234+
},
235+
{
236+
"input": "text/html;charset=\";charset=GBK",
237+
"output": "text/html;charset=\";charset=GBK\"",
238+
"navigable": true,
239+
"encoding": null
240+
},
241+
"Unexpected code points",
242+
{
243+
"input": "text/html;charset={gbk}",
244+
"output": "text/html;charset=\"{gbk}\"",
245+
"navigable": true,
246+
"encoding": null
247+
},
248+
"Parameter name longer than 127",
249+
{
250+
"input": "text/html;0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789=x;charset=gbk",
251+
"output": "text/html;0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789=x;charset=gbk",
252+
"navigable": true,
253+
"encoding": "GBK"
254+
},
255+
"type/subtype longer than 127",
256+
{
257+
"input": "0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789/0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789",
258+
"output": "0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789/0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789"
259+
},
260+
"Valid",
261+
{
262+
"input": "!#$%&'*+-.^_`|~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz/!#$%&'*+-.^_`|~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz;!#$%&'*+-.^_`|~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz=!#$%&'*+-.^_`|~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
263+
"output": "!#$%&'*+-.^_`|~0123456789abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz/!#$%&'*+-.^_`|~0123456789abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz;!#$%&'*+-.^_`|~0123456789abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz=!#$%&'*+-.^_`|~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
264+
},
265+
{
266+
"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\"",
267+
"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\""
268+
},
269+
"End-of-file handling",
270+
{
271+
"input": "x/x;test",
272+
"output": "x/x"
273+
},
274+
{
275+
"input": "x/x;test=\"\\",
276+
"output": "x/x;test=\"\\\\\""
277+
},
278+
"Whitespace (not handled by generated-mime-types.json or above)",
279+
{
280+
"input": "x/x;x= ",
281+
"output": "x/x"
282+
},
283+
{
284+
"input": "x/x;x=\t",
285+
"output": "x/x"
286+
},
287+
{
288+
"input": "x/x\n\r\t ;x=x",
289+
"output": "x/x;x=x"
290+
},
291+
{
292+
"input": "\n\r\t x/x;x=x\n\r\t ",
293+
"output": "x/x;x=x"
294+
},
295+
{
296+
"input": "x/x;\n\r\t x=x\n\r\t ;x=y",
297+
"output": "x/x;x=x"
298+
},
299+
"Latin1",
300+
{
301+
"input": "text/html;test=\u00FF;charset=gbk",
302+
"output": "text/html;test=\"\u00FF\";charset=gbk",
303+
"navigable": true,
304+
"encoding": "GBK"
305+
},
306+
">Latin1",
307+
{
308+
"input": "x/x;test=\uFFFD;x=x",
309+
"output": "x/x;x=x"
310+
},
311+
"Failure",
312+
{
313+
"input": "\u000Bx/x",
314+
"output": null
315+
},
316+
{
317+
"input": "\u000Cx/x",
318+
"output": null
319+
},
320+
{
321+
"input": "x/x\u000B",
322+
"output": null
323+
},
324+
{
325+
"input": "x/x\u000C",
326+
"output": null
327+
},
328+
{
329+
"input": "",
330+
"output": null
331+
},
332+
{
333+
"input": "\t",
334+
"output": null
335+
},
336+
{
337+
"input": "/",
338+
"output": null
339+
},
340+
{
341+
"input": "bogus",
342+
"output": null
343+
},
344+
{
345+
"input": "bogus/",
346+
"output": null
347+
},
348+
{
349+
"input": "bogus/ ",
350+
"output": null
351+
},
352+
{
353+
"input": "bogus/bogus/;",
354+
"output": null
355+
},
356+
{
357+
"input": "</>",
358+
"output": null
359+
},
360+
{
361+
"input": "(/)",
362+
"output": null
363+
},
364+
{
365+
"input": "ÿ/ÿ",
366+
"output": null
367+
},
368+
{
369+
"input": "text/html(;doesnot=matter",
370+
"output": null
371+
},
372+
{
373+
"input": "{/}",
374+
"output": null
375+
},
376+
{
377+
"input": "\u0100/\u0100",
378+
"output": null
379+
},
380+
{
381+
"input": "text /html",
382+
"output": null
383+
},
384+
{
385+
"input": "text/ html",
386+
"output": null
387+
},
388+
{
389+
"input": "\"text/html\"",
390+
"output": null
391+
}
392+
];

‎test/parallel/test-bootstrap-modules.js

+1
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ const expectedModules = new Set([
7272
'NativeModule internal/histogram',
7373
'NativeModule internal/idna',
7474
'NativeModule internal/linkedlist',
75+
'NativeModule internal/mime',
7576
'NativeModule internal/modules/cjs/helpers',
7677
'NativeModule internal/modules/cjs/loader',
7778
'NativeModule internal/modules/esm/assert',

‎test/parallel/test-eslint-avoid-prototype-pollution.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ new RuleTester({
164164
},
165165
{
166166
code: 'RegExpPrototypeSymbolSearch(/some regex/, "some string")',
167-
errors: [{ message: /looks up the "exec" property/ }],
167+
errors: [{ message: /SafeStringPrototypeSearch/ }],
168168
},
169169
{
170170
code: 'StringPrototypeMatch("some string", /some regex/)',
@@ -204,7 +204,7 @@ new RuleTester({
204204
},
205205
{
206206
code: 'StringPrototypeSearch("some string", /some regex/)',
207-
errors: [{ message: /looks up the Symbol\.search property/ }],
207+
errors: [{ message: /SafeStringPrototypeSearch/ }],
208208
},
209209
{
210210
code: 'StringPrototypeSplit("some string", /some regex/)',

‎test/parallel/test-mime-api.js

+160
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
'use strict';
2+
3+
require('../common');
4+
const assert = require('assert');
5+
const { MIMEType, MIMEParams } = require('util');
6+
7+
8+
const WHITESPACES = '\t\n\f\r ';
9+
const NOT_HTTP_TOKEN_CODE_POINT = ',';
10+
const NOT_HTTP_QUOTED_STRING_CODE_POINT = '\n';
11+
12+
const mime = new MIMEType('application/ecmascript; ');
13+
const mime_descriptors = Object.getOwnPropertyDescriptors(mime);
14+
const mime_proto = Object.getPrototypeOf(mime);
15+
const mime_impersonator = Object.create(mime_proto);
16+
for (const key of Object.keys(mime_descriptors)) {
17+
const descriptor = mime_descriptors[key];
18+
if (descriptor.get) {
19+
assert.throws(descriptor.get.call(mime_impersonator), /Invalid receiver/i);
20+
}
21+
if (descriptor.set) {
22+
assert.throws(descriptor.set.call(mime_impersonator, 'x'), /Invalid receiver/i);
23+
}
24+
}
25+
26+
27+
assert.strictEqual(
28+
JSON.stringify(mime),
29+
JSON.stringify('application/ecmascript'));
30+
assert.strictEqual(`${mime}`, 'application/ecmascript');
31+
assert.strictEqual(mime.essence, 'application/ecmascript');
32+
assert.strictEqual(mime.type, 'application');
33+
assert.strictEqual(mime.subtype, 'ecmascript');
34+
assert.ok(mime.params);
35+
assert.deepStrictEqual([], [...mime.params]);
36+
assert.strictEqual(mime.params.has('not found'), false);
37+
assert.strictEqual(mime.params.get('not found'), null);
38+
assert.strictEqual(mime.params.delete('not found'), undefined);
39+
40+
41+
mime.type = 'text';
42+
assert.strictEqual(mime.type, 'text');
43+
assert.strictEqual(JSON.stringify(mime), JSON.stringify('text/ecmascript'));
44+
assert.strictEqual(`${mime}`, 'text/ecmascript');
45+
assert.strictEqual(mime.essence, 'text/ecmascript');
46+
47+
assert.throws(() => {
48+
mime.type = `${WHITESPACES}text`;
49+
}, /ERR_INVALID_MIME_SYNTAX/);
50+
51+
assert.throws(() => mime.type = '', /type/i);
52+
assert.throws(() => mime.type = '/', /type/i);
53+
assert.throws(() => mime.type = 'x/', /type/i);
54+
assert.throws(() => mime.type = '/x', /type/i);
55+
assert.throws(() => mime.type = NOT_HTTP_TOKEN_CODE_POINT, /type/i);
56+
assert.throws(() => mime.type = `${NOT_HTTP_TOKEN_CODE_POINT}/`, /type/i);
57+
assert.throws(() => mime.type = `/${NOT_HTTP_TOKEN_CODE_POINT}`, /type/i);
58+
59+
60+
mime.subtype = 'javascript';
61+
assert.strictEqual(mime.type, 'text');
62+
assert.strictEqual(JSON.stringify(mime), JSON.stringify('text/javascript'));
63+
assert.strictEqual(`${mime}`, 'text/javascript');
64+
assert.strictEqual(mime.essence, 'text/javascript');
65+
assert.strictEqual(`${mime.params}`, '');
66+
assert.strictEqual(`${new MIMEParams()}`, '');
67+
assert.strictEqual(`${new MIMEParams(mime.params)}`, '');
68+
assert.strictEqual(`${new MIMEParams(`${mime.params}`)}`, '');
69+
70+
assert.throws(() => {
71+
mime.subtype = `javascript${WHITESPACES}`;
72+
}, /ERR_INVALID_MIME_SYNTAX/);
73+
74+
assert.throws(() => mime.subtype = '', /subtype/i);
75+
assert.throws(() => mime.subtype = ';', /subtype/i);
76+
assert.throws(() => mime.subtype = 'x;', /subtype/i);
77+
assert.throws(() => mime.subtype = ';x', /subtype/i);
78+
assert.throws(() => mime.subtype = NOT_HTTP_TOKEN_CODE_POINT, /subtype/i);
79+
assert.throws(
80+
() => mime.subtype = `${NOT_HTTP_TOKEN_CODE_POINT};`,
81+
/subtype/i);
82+
assert.throws(
83+
() => mime.subtype = `;${NOT_HTTP_TOKEN_CODE_POINT}`,
84+
/subtype/i);
85+
86+
87+
const params = mime.params;
88+
params.set('charset', 'utf-8');
89+
assert.strictEqual(params.has('charset'), true);
90+
assert.strictEqual(params.get('charset'), 'utf-8');
91+
assert.deepStrictEqual([...params], [['charset', 'utf-8']]);
92+
assert.strictEqual(
93+
JSON.stringify(mime),
94+
JSON.stringify('text/javascript;charset=utf-8'));
95+
assert.strictEqual(`${mime}`, 'text/javascript;charset=utf-8');
96+
assert.strictEqual(mime.essence, 'text/javascript');
97+
assert.strictEqual(`${mime.params}`, 'charset=utf-8');
98+
assert.strictEqual(`${new MIMEParams(mime.params)}`, '');
99+
assert.strictEqual(`${new MIMEParams(`${mime.params}`)}`, '');
100+
101+
params.set('goal', 'module');
102+
assert.strictEqual(params.has('goal'), true);
103+
assert.strictEqual(params.get('goal'), 'module');
104+
assert.deepStrictEqual([...params], [['charset', 'utf-8'], ['goal', 'module']]);
105+
assert.strictEqual(
106+
JSON.stringify(mime),
107+
JSON.stringify('text/javascript;charset=utf-8;goal=module'));
108+
assert.strictEqual(`${mime}`, 'text/javascript;charset=utf-8;goal=module');
109+
assert.strictEqual(mime.essence, 'text/javascript');
110+
assert.strictEqual(`${mime.params}`, 'charset=utf-8;goal=module');
111+
assert.strictEqual(`${new MIMEParams(mime.params)}`, '');
112+
assert.strictEqual(`${new MIMEParams(`${mime.params}`)}`, '');
113+
114+
assert.throws(() => {
115+
params.set(`${WHITESPACES}goal`, 'module');
116+
}, /ERR_INVALID_MIME_SYNTAX/);
117+
118+
params.set('charset', 'iso-8859-1');
119+
assert.strictEqual(params.has('charset'), true);
120+
assert.strictEqual(params.get('charset'), 'iso-8859-1');
121+
assert.deepStrictEqual(
122+
[...params],
123+
[['charset', 'iso-8859-1'], ['goal', 'module']]);
124+
assert.strictEqual(
125+
JSON.stringify(mime),
126+
JSON.stringify('text/javascript;charset=iso-8859-1;goal=module'));
127+
assert.strictEqual(`${mime}`, 'text/javascript;charset=iso-8859-1;goal=module');
128+
assert.strictEqual(mime.essence, 'text/javascript');
129+
130+
params.delete('charset');
131+
assert.strictEqual(params.has('charset'), false);
132+
assert.strictEqual(params.get('charset'), null);
133+
assert.deepStrictEqual([...params], [['goal', 'module']]);
134+
assert.strictEqual(
135+
JSON.stringify(mime),
136+
JSON.stringify('text/javascript;goal=module'));
137+
assert.strictEqual(`${mime}`, 'text/javascript;goal=module');
138+
assert.strictEqual(mime.essence, 'text/javascript');
139+
140+
params.set('x', '');
141+
assert.strictEqual(params.has('x'), true);
142+
assert.strictEqual(params.get('x'), '');
143+
assert.deepStrictEqual([...params], [['goal', 'module'], ['x', '']]);
144+
assert.strictEqual(
145+
JSON.stringify(mime),
146+
JSON.stringify('text/javascript;goal=module;x=""'));
147+
assert.strictEqual(`${mime}`, 'text/javascript;goal=module;x=""');
148+
assert.strictEqual(mime.essence, 'text/javascript');
149+
150+
assert.throws(() => params.set('', 'x'), /parameter name/i);
151+
assert.throws(() => params.set('=', 'x'), /parameter name/i);
152+
assert.throws(() => params.set('x=', 'x'), /parameter name/i);
153+
assert.throws(() => params.set('=x', 'x'), /parameter name/i);
154+
assert.throws(() => params.set(`${NOT_HTTP_TOKEN_CODE_POINT}=`, 'x'), /parameter name/i);
155+
assert.throws(() => params.set(`${NOT_HTTP_TOKEN_CODE_POINT}x`, 'x'), /parameter name/i);
156+
assert.throws(() => params.set(`x${NOT_HTTP_TOKEN_CODE_POINT}`, 'x'), /parameter name/i);
157+
158+
assert.throws(() => params.set('x', `${NOT_HTTP_QUOTED_STRING_CODE_POINT};`), /parameter value/i);
159+
assert.throws(() => params.set('x', `${NOT_HTTP_QUOTED_STRING_CODE_POINT}x`), /parameter value/i);
160+
assert.throws(() => params.set('x', `x${NOT_HTTP_QUOTED_STRING_CODE_POINT}`), /parameter value/i);

‎test/parallel/test-mime-whatwg.js

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
'use strict';
2+
3+
require('../common');
4+
const assert = require('assert');
5+
const { MIMEType } = require('util');
6+
const fixtures = require('../common/fixtures');
7+
8+
function test(mimes) {
9+
for (const entry of mimes) {
10+
if (typeof entry === 'string') continue;
11+
const { input, output } = entry;
12+
if (output === null) {
13+
assert.throws(() => new MIMEType(input), /ERR_INVALID_MIME_SYNTAX/i);
14+
} else {
15+
const str = `${new MIMEType(input)}`;
16+
assert.strictEqual(str, output);
17+
}
18+
}
19+
}
20+
21+
// These come from https://github.com/web-platform-tests/wpt/tree/master/mimesniff/mime-types/resources
22+
test(require(fixtures.path('./mime-whatwg.js')));
23+
test(require(fixtures.path('./mime-whatwg-generated.js')));
+119
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
// Flags: --expose-internals
2+
'use strict';
3+
4+
const { mustNotCall } = require('../common');
5+
const assert = require('assert');
6+
7+
const {
8+
RegExpPrototypeSymbolReplace,
9+
RegExpPrototypeSymbolSearch,
10+
RegExpPrototypeSymbolSplit,
11+
SafeStringPrototypeSearch,
12+
hardenRegExp,
13+
} = require('internal/test/binding').primordials;
14+
15+
16+
Object.defineProperties(RegExp.prototype, {
17+
[Symbol.match]: {
18+
get: mustNotCall('get %RegExp.prototype%[@@match]'),
19+
set: mustNotCall('set %RegExp.prototype%[@@match]'),
20+
},
21+
[Symbol.matchAll]: {
22+
get: mustNotCall('get %RegExp.prototype%[@@matchAll]'),
23+
set: mustNotCall('set %RegExp.prototype%[@@matchAll]'),
24+
},
25+
[Symbol.replace]: {
26+
get: mustNotCall('get %RegExp.prototype%[@@replace]'),
27+
set: mustNotCall('set %RegExp.prototype%[@@replace]'),
28+
},
29+
[Symbol.search]: {
30+
get: mustNotCall('get %RegExp.prototype%[@@search]'),
31+
set: mustNotCall('set %RegExp.prototype%[@@search]'),
32+
},
33+
[Symbol.split]: {
34+
get: mustNotCall('get %RegExp.prototype%[@@split]'),
35+
set: mustNotCall('set %RegExp.prototype%[@@split]'),
36+
},
37+
dotAll: {
38+
get: mustNotCall('get %RegExp.prototype%.dotAll'),
39+
set: mustNotCall('set %RegExp.prototype%.dotAll'),
40+
},
41+
exec: {
42+
get: mustNotCall('get %RegExp.prototype%.exec'),
43+
set: mustNotCall('set %RegExp.prototype%.exec'),
44+
},
45+
flags: {
46+
get: mustNotCall('get %RegExp.prototype%.flags'),
47+
set: mustNotCall('set %RegExp.prototype%.flags'),
48+
},
49+
global: {
50+
get: mustNotCall('get %RegExp.prototype%.global'),
51+
set: mustNotCall('set %RegExp.prototype%.global'),
52+
},
53+
hasIndices: {
54+
get: mustNotCall('get %RegExp.prototype%.hasIndices'),
55+
set: mustNotCall('set %RegExp.prototype%.hasIndices'),
56+
},
57+
ignoreCase: {
58+
get: mustNotCall('get %RegExp.prototype%.ignoreCase'),
59+
set: mustNotCall('set %RegExp.prototype%.ignoreCase'),
60+
},
61+
multiline: {
62+
get: mustNotCall('get %RegExp.prototype%.multiline'),
63+
set: mustNotCall('set %RegExp.prototype%.multiline'),
64+
},
65+
source: {
66+
get: mustNotCall('get %RegExp.prototype%.source'),
67+
set: mustNotCall('set %RegExp.prototype%.source'),
68+
},
69+
sticky: {
70+
get: mustNotCall('get %RegExp.prototype%.sticky'),
71+
set: mustNotCall('set %RegExp.prototype%.sticky'),
72+
},
73+
test: {
74+
get: mustNotCall('get %RegExp.prototype%.test'),
75+
set: mustNotCall('set %RegExp.prototype%.test'),
76+
},
77+
toString: {
78+
get: mustNotCall('get %RegExp.prototype%.toString'),
79+
set: mustNotCall('set %RegExp.prototype%.toString'),
80+
},
81+
unicode: {
82+
get: mustNotCall('get %RegExp.prototype%.unicode'),
83+
set: mustNotCall('set %RegExp.prototype%.unicode'),
84+
},
85+
});
86+
87+
hardenRegExp(hardenRegExp(/1/));
88+
89+
// IMO there are no valid use cases in node core to use RegExpPrototypeSymbolMatch
90+
// or RegExpPrototypeSymbolMatchAll, they are inherently unsafe.
91+
92+
{
93+
const myRegex = hardenRegExp(/a/);
94+
assert.strictEqual(RegExpPrototypeSymbolReplace(myRegex, 'baar', 'e'), 'bear');
95+
}
96+
{
97+
const myRegex = hardenRegExp(/a/g);
98+
assert.strictEqual(RegExpPrototypeSymbolReplace(myRegex, 'baar', 'e'), 'beer');
99+
}
100+
{
101+
const myRegex = hardenRegExp(/a/);
102+
assert.strictEqual(RegExpPrototypeSymbolSearch(myRegex, 'baar'), 1);
103+
}
104+
{
105+
const myRegex = /a/;
106+
assert.strictEqual(SafeStringPrototypeSearch('baar', myRegex), 1);
107+
}
108+
{
109+
const myRegex = hardenRegExp(/a/);
110+
assert.deepStrictEqual(RegExpPrototypeSymbolSplit(myRegex, 'baar', 0), []);
111+
}
112+
{
113+
const myRegex = hardenRegExp(/a/);
114+
assert.deepStrictEqual(RegExpPrototypeSymbolSplit(myRegex, 'baar', 1), ['b']);
115+
}
116+
{
117+
const myRegex = hardenRegExp(/a/);
118+
assert.deepStrictEqual(RegExpPrototypeSymbolSplit(myRegex, 'baar'), ['b', '', 'r']);
119+
}

‎tools/doc/type-parser.mjs

+2
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,8 @@ const customTypesMap = {
217217
'URL': 'url.html#the-whatwg-url-api',
218218
'URLSearchParams': 'url.html#class-urlsearchparams',
219219

220+
'MIMEParams': 'util.html#class-utilmimeparams',
221+
220222
'vm.Module': 'vm.html#class-vmmodule',
221223
'vm.Script': 'vm.html#class-vmscript',
222224
'vm.SourceTextModule': 'vm.html#class-vmsourcetextmodule',

‎tools/eslint-rules/avoid-prototype-pollution.js

+7-2
Original file line numberDiff line numberDiff line change
@@ -135,17 +135,22 @@ module.exports = {
135135
}],
136136
});
137137
},
138-
[CallExpression(/^RegExpPrototypeSymbol(Match|MatchAll|Search)$/)](node) {
138+
[CallExpression(/^RegExpPrototypeSymbol(Match|MatchAll)$/)](node) {
139139
context.report({
140140
node,
141141
message: node.callee.name + ' looks up the "exec" property of `this` value',
142142
});
143143
},
144+
[CallExpression(/^(RegExpPrototypeSymbol|StringPrototype)Search$/)](node) {
145+
context.report({
146+
node,
147+
message: node.callee.name + ' is unsafe, use SafeStringPrototypeSearch instead',
148+
});
149+
},
144150
...createUnsafeStringMethodReport(context, '%String.prototype.match%', 'Symbol.match'),
145151
...createUnsafeStringMethodReport(context, '%String.prototype.matchAll%', 'Symbol.matchAll'),
146152
...createUnsafeStringMethodOnRegexReport(context, '%String.prototype.replace%', 'Symbol.replace'),
147153
...createUnsafeStringMethodOnRegexReport(context, '%String.prototype.replaceAll%', 'Symbol.replace'),
148-
...createUnsafeStringMethodReport(context, '%String.prototype.search%', 'Symbol.search'),
149154
...createUnsafeStringMethodOnRegexReport(context, '%String.prototype.split%', 'Symbol.split'),
150155

151156
'NewExpression[callee.name="Proxy"][arguments.1.type="ObjectExpression"]'(node) {

0 commit comments

Comments
 (0)
Please sign in to comment.