Skip to content

Commit 4c1714b

Browse files
authored
perf: improve parsing form-data (#2944)
* perf: improve parsing form-data * apply suggestions from code review * apply suggestions from code review
1 parent 0b5a451 commit 4c1714b

File tree

2 files changed

+40
-14
lines changed

2 files changed

+40
-14
lines changed

lib/core/util.js

+11-1
Original file line numberDiff line numberDiff line change
@@ -440,7 +440,8 @@ function addAbortListener (signal, listener) {
440440
return () => signal.removeListener('abort', listener)
441441
}
442442

443-
const hasToWellFormed = !!String.prototype.toWellFormed
443+
const hasToWellFormed = typeof String.prototype.toWellFormed === 'function'
444+
const hasIsWellFormed = typeof String.prototype.isWellFormed === 'function'
444445

445446
/**
446447
* @param {string} val
@@ -449,6 +450,14 @@ function toUSVString (val) {
449450
return hasToWellFormed ? `${val}`.toWellFormed() : nodeUtil.toUSVString(val)
450451
}
451452

453+
/**
454+
* @param {string} val
455+
*/
456+
// TODO: move this to webidl
457+
function isUSVString (val) {
458+
return hasIsWellFormed ? `${val}`.isWellFormed() : toUSVString(val) === `${val}`
459+
}
460+
452461
/**
453462
* @see https://tools.ietf.org/html/rfc7230#section-3.2.6
454463
* @param {number} c
@@ -538,6 +547,7 @@ module.exports = {
538547
isErrored,
539548
isReadable,
540549
toUSVString,
550+
isUSVString,
541551
isReadableAborted,
542552
isBlobLike,
543553
parseOrigin,

lib/web/fetch/formdata-parser.js

+29-13
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
'use strict'
22

3-
const { webidl } = require('./webidl')
3+
const { toUSVString, isUSVString, bufferToLowerCasedHeaderName } = require('../../core/util')
44
const { utf8DecodeBytes } = require('./util')
55
const { HTTP_TOKEN_CODEPOINTS, isomorphicDecode } = require('./data-url')
66
const { isFileLike, File: UndiciFile } = require('./file')
77
const { makeEntry } = require('./formdata')
88
const assert = require('node:assert')
9-
const { isAscii, File: NodeFile } = require('node:buffer')
9+
const { File: NodeFile } = require('node:buffer')
1010

1111
const File = globalThis.File ?? NodeFile ?? UndiciFile
1212

@@ -15,6 +15,18 @@ const filenameBuffer = Buffer.from('; filename')
1515
const dd = Buffer.from('--')
1616
const ddcrlf = Buffer.from('--\r\n')
1717

18+
/**
19+
* @param {string} chars
20+
*/
21+
function isAsciiString (chars) {
22+
for (let i = 0; i < chars.length; ++i) {
23+
if ((chars.charCodeAt(i) & ~0x7F) !== 0) {
24+
return false
25+
}
26+
}
27+
return true
28+
}
29+
1830
/**
1931
* @see https://andreubotella.github.io/multipart-form-data/#multipart-form-data-boundary
2032
* @param {string} boundary
@@ -30,7 +42,7 @@ function validateBoundary (boundary) {
3042
// - it is composed by bytes in the ranges 0x30 to 0x39, 0x41 to 0x5A, or
3143
// 0x61 to 0x7A, inclusive (ASCII alphanumeric), or which are 0x27 ('),
3244
// 0x2D (-) or 0x5F (_).
33-
for (let i = 0; i < boundary.length; i++) {
45+
for (let i = 0; i < length; ++i) {
3446
const cp = boundary.charCodeAt(i)
3547

3648
if (!(
@@ -58,12 +70,12 @@ function escapeFormDataName (name, encoding = 'utf-8', isFilename = false) {
5870
// 1. If isFilename is true:
5971
if (isFilename) {
6072
// 1.1. Set name to the result of converting name into a scalar value string.
61-
name = webidl.converters.USVString(name)
73+
name = toUSVString(name)
6274
} else {
6375
// 2. Otherwise:
6476

6577
// 2.1. Assert: name is a scalar value string.
66-
assert(name === webidl.converters.USVString(name))
78+
assert(isUSVString(name))
6779

6880
// 2.2. Replace every occurrence of U+000D (CR) not followed by U+000A (LF),
6981
// and every occurrence of U+000A (LF) not preceded by U+000D (CR), in
@@ -94,14 +106,16 @@ function multipartFormDataParser (input, mimeType) {
94106
// 1. Assert: mimeType’s essence is "multipart/form-data".
95107
assert(mimeType !== 'failure' && mimeType.essence === 'multipart/form-data')
96108

109+
const boundaryString = mimeType.parameters.get('boundary')
110+
97111
// 2. If mimeType’s parameters["boundary"] does not exist, return failure.
98112
// Otherwise, let boundary be the result of UTF-8 decoding mimeType’s
99113
// parameters["boundary"].
100-
if (!mimeType.parameters.has('boundary')) {
114+
if (boundaryString === undefined) {
101115
return 'failure'
102116
}
103117

104-
const boundary = Buffer.from(`--${mimeType.parameters.get('boundary')}`, 'utf8')
118+
const boundary = Buffer.from(`--${boundaryString}`, 'utf8')
105119

106120
// 3. Let entry list be an empty entry list.
107121
const entryList = []
@@ -200,7 +214,10 @@ function multipartFormDataParser (input, mimeType) {
200214
contentType ??= 'text/plain'
201215

202216
// 5.10.2. If contentType is not an ASCII string, set contentType to the empty string.
203-
if (!isAscii(Buffer.from(contentType))) {
217+
218+
// Note: `buffer.isAscii` can be used at zero-cost, but converting a string to a buffer is a high overhead.
219+
// Content-Type is a relatively small string, so it is faster to use `String#charCodeAt`.
220+
if (!isAsciiString(contentType)) {
204221
contentType = ''
205222
}
206223

@@ -214,8 +231,8 @@ function multipartFormDataParser (input, mimeType) {
214231
}
215232

216233
// 5.12. Assert: name is a scalar value string and value is either a scalar value string or a File object.
217-
assert(name === webidl.converters.USVString(name))
218-
assert((typeof value === 'string' && value === webidl.converters.USVString(value)) || isFileLike(value))
234+
assert(isUSVString(name))
235+
assert((typeof value === 'string' && isUSVString(value)) || isFileLike(value))
219236

220237
// 5.13. Create an entry with name and value, and append it to entry list.
221238
entryList.push(makeEntry(name, value, filename))
@@ -280,7 +297,7 @@ function parseMultipartFormDataHeaders (input, position) {
280297
)
281298

282299
// 2.8. Byte-lowercase header name and switch on the result:
283-
switch (new TextDecoder().decode(headerName).toLowerCase()) {
300+
switch (bufferToLowerCasedHeaderName(headerName)) {
284301
case 'content-disposition': {
285302
// 1. Set name and filename to null.
286303
name = filename = null
@@ -428,10 +445,9 @@ function parseMultipartFormDataName (input, position) {
428445
*/
429446
function collectASequenceOfBytes (condition, input, position) {
430447
const result = []
431-
let index = 0
432448

433449
while (position.position < input.length && condition(input[position.position])) {
434-
result[index++] = input[position.position]
450+
result.push(input[position.position])
435451

436452
position.position++
437453
}

0 commit comments

Comments
 (0)