undici 6.6.2 → 6.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/README.md +49 -27
  2. package/docs/{api → docs/api}/DiagnosticsChannel.md +2 -2
  3. package/docs/{api → docs/api}/Dispatcher.md +39 -3
  4. package/docs/docs/api/Fetch.md +57 -0
  5. package/docs/{api → docs/api}/ProxyAgent.md +5 -1
  6. package/docs/docs/api/RetryAgent.md +45 -0
  7. package/docs/{api → docs/api}/RetryHandler.md +1 -1
  8. package/docs/{api → docs/api}/api-lifecycle.md +33 -4
  9. package/docs/{best-practices → docs/best-practices}/proxy.md +6 -6
  10. package/index-fetch.js +9 -8
  11. package/index.js +31 -25
  12. package/lib/api/api-request.js +1 -1
  13. package/lib/api/readable.js +12 -9
  14. package/lib/api/util.js +8 -6
  15. package/lib/core/request.js +72 -135
  16. package/lib/core/symbols.js +6 -5
  17. package/lib/core/tree.js +46 -26
  18. package/lib/core/util.js +41 -20
  19. package/lib/{agent.js → dispatcher/agent.js} +4 -4
  20. package/lib/{balanced-pool.js → dispatcher/balanced-pool.js} +3 -3
  21. package/lib/dispatcher/client-h1.js +1352 -0
  22. package/lib/dispatcher/client-h2.js +639 -0
  23. package/lib/dispatcher/client.js +611 -0
  24. package/lib/{dispatcher-base.js → dispatcher/dispatcher-base.js} +2 -2
  25. package/lib/{pool-base.js → dispatcher/pool-base.js} +3 -3
  26. package/lib/{pool-stats.js → dispatcher/pool-stats.js} +1 -1
  27. package/lib/{pool.js → dispatcher/pool.js} +4 -4
  28. package/lib/{proxy-agent.js → dispatcher/proxy-agent.js} +29 -35
  29. package/lib/dispatcher/retry-agent.js +35 -0
  30. package/lib/global.js +1 -1
  31. package/lib/handler/{RetryHandler.js → retry-handler.js} +2 -2
  32. package/lib/interceptor/{redirectInterceptor.js → redirect-interceptor.js} +1 -1
  33. package/lib/mock/mock-agent.js +2 -2
  34. package/lib/mock/mock-client.js +1 -1
  35. package/lib/mock/mock-interceptor.js +2 -2
  36. package/lib/mock/mock-pool.js +1 -1
  37. package/lib/mock/mock-utils.js +6 -4
  38. package/lib/{cache → web/cache}/cache.js +2 -4
  39. package/lib/{cache → web/cache}/cachestorage.js +1 -1
  40. package/lib/web/cache/symbols.js +5 -0
  41. package/lib/{cache → web/cache}/util.js +5 -9
  42. package/lib/{cookies → web/cookies}/parse.js +1 -1
  43. package/lib/{cookies → web/cookies}/util.js +76 -60
  44. package/lib/{eventsource → web/eventsource}/eventsource.js +2 -6
  45. package/lib/{fetch → web/fetch}/body.js +56 -175
  46. package/lib/{fetch/dataURL.js → web/fetch/data-url.js} +5 -2
  47. package/lib/{compat → web/fetch}/dispatcher-weakref.js +1 -1
  48. package/lib/{fetch → web/fetch}/file.js +7 -8
  49. package/lib/web/fetch/formdata-parser.js +488 -0
  50. package/lib/{fetch → web/fetch}/formdata.js +7 -68
  51. package/lib/{fetch → web/fetch}/headers.js +99 -71
  52. package/lib/{fetch → web/fetch}/index.js +33 -25
  53. package/lib/{fetch → web/fetch}/request.js +15 -7
  54. package/lib/{fetch → web/fetch}/response.js +3 -3
  55. package/lib/{fetch → web/fetch}/symbols.js +2 -1
  56. package/lib/{fetch → web/fetch}/util.js +171 -48
  57. package/lib/{fetch → web/fetch}/webidl.js +46 -16
  58. package/lib/{fileapi → web/fileapi}/filereader.js +1 -1
  59. package/lib/{fileapi → web/fileapi}/util.js +1 -1
  60. package/lib/{websocket → web/websocket}/connection.js +20 -10
  61. package/lib/{websocket → web/websocket}/constants.js +7 -0
  62. package/lib/{websocket → web/websocket}/events.js +1 -1
  63. package/lib/{websocket → web/websocket}/frame.js +1 -0
  64. package/lib/{websocket → web/websocket}/receiver.js +9 -16
  65. package/lib/{websocket → web/websocket}/util.js +37 -23
  66. package/lib/{websocket → web/websocket}/websocket.js +21 -9
  67. package/package.json +26 -54
  68. package/types/dispatcher.d.ts +1 -1
  69. package/types/fetch.d.ts +20 -21
  70. package/types/index.d.ts +2 -1
  71. package/types/retry-agent.d.ts +11 -0
  72. package/types/webidl.d.ts +6 -1
  73. package/docs/api/Fetch.md +0 -27
  74. package/docs/assets/lifecycle-diagram.png +0 -0
  75. package/lib/cache/symbols.js +0 -5
  76. package/lib/client.js +0 -2295
  77. package/lib/llhttp/llhttp.wasm +0 -0
  78. package/lib/llhttp/llhttp_simd.wasm +0 -0
  79. /package/docs/{api → docs/api}/Agent.md +0 -0
  80. /package/docs/{api → docs/api}/BalancedPool.md +0 -0
  81. /package/docs/{api → docs/api}/CacheStorage.md +0 -0
  82. /package/docs/{api → docs/api}/Client.md +0 -0
  83. /package/docs/{api → docs/api}/Connector.md +0 -0
  84. /package/docs/{api → docs/api}/ContentType.md +0 -0
  85. /package/docs/{api → docs/api}/Cookies.md +0 -0
  86. /package/docs/{api → docs/api}/Debug.md +0 -0
  87. /package/docs/{api → docs/api}/DispatchInterceptor.md +0 -0
  88. /package/docs/{api → docs/api}/Errors.md +0 -0
  89. /package/docs/{api → docs/api}/EventSource.md +0 -0
  90. /package/docs/{api → docs/api}/MockAgent.md +0 -0
  91. /package/docs/{api → docs/api}/MockClient.md +0 -0
  92. /package/docs/{api → docs/api}/MockErrors.md +0 -0
  93. /package/docs/{api → docs/api}/MockPool.md +0 -0
  94. /package/docs/{api → docs/api}/Pool.md +0 -0
  95. /package/docs/{api → docs/api}/PoolStats.md +0 -0
  96. /package/docs/{api → docs/api}/RedirectHandler.md +0 -0
  97. /package/docs/{api → docs/api}/Util.md +0 -0
  98. /package/docs/{api → docs/api}/WebSocket.md +0 -0
  99. /package/docs/{best-practices → docs/best-practices}/client-certificate.md +0 -0
  100. /package/docs/{best-practices → docs/best-practices}/mocking-request.md +0 -0
  101. /package/docs/{best-practices → docs/best-practices}/writing-tests.md +0 -0
  102. /package/lib/{dispatcher.js → dispatcher/dispatcher.js} +0 -0
  103. /package/lib/{node → dispatcher}/fixed-queue.js +0 -0
  104. /package/lib/handler/{DecoratorHandler.js → decorator-handler.js} +0 -0
  105. /package/lib/handler/{RedirectHandler.js → redirect-handler.js} +0 -0
  106. /package/lib/{timers.js → util/timers.js} +0 -0
  107. /package/lib/{cookies → web/cookies}/constants.js +0 -0
  108. /package/lib/{cookies → web/cookies}/index.js +0 -0
  109. /package/lib/{eventsource → web/eventsource}/eventsource-stream.js +0 -0
  110. /package/lib/{eventsource → web/eventsource}/util.js +0 -0
  111. /package/lib/{fetch → web/fetch}/LICENSE +0 -0
  112. /package/lib/{fetch → web/fetch}/constants.js +0 -0
  113. /package/lib/{fetch → web/fetch}/global.js +0 -0
  114. /package/lib/{fileapi → web/fileapi}/encoding.js +0 -0
  115. /package/lib/{fileapi → web/fileapi}/progressevent.js +0 -0
  116. /package/lib/{fileapi → web/fileapi}/symbols.js +0 -0
  117. /package/lib/{websocket → web/websocket}/symbols.js +0 -0
@@ -0,0 +1,488 @@
1
+ 'use strict'
2
+
3
+ const { webidl } = require('./webidl')
4
+ const { utf8DecodeBytes } = require('./util')
5
+ const { HTTP_TOKEN_CODEPOINTS, isomorphicDecode } = require('./data-url')
6
+ const { isFileLike, File: UndiciFile } = require('./file')
7
+ const { makeEntry } = require('./formdata')
8
+ const assert = require('node:assert')
9
+ const { isAscii, File: NodeFile } = require('node:buffer')
10
+
11
+ const File = globalThis.File ?? NodeFile ?? UndiciFile
12
+
13
+ const formDataNameBuffer = Buffer.from('form-data; name="')
14
+ const filenameBuffer = Buffer.from('; filename')
15
+ const dd = Buffer.from('--')
16
+ const ddcrlf = Buffer.from('--\r\n')
17
+
18
+ /**
19
+ * @see https://andreubotella.github.io/multipart-form-data/#multipart-form-data-boundary
20
+ * @param {string} boundary
21
+ */
22
+ function validateBoundary (boundary) {
23
+ const length = boundary.length
24
+
25
+ // - its length is greater or equal to 27 and lesser or equal to 70, and
26
+ if (length < 27 || length > 70) {
27
+ return false
28
+ }
29
+
30
+ // - it is composed by bytes in the ranges 0x30 to 0x39, 0x41 to 0x5A, or
31
+ // 0x61 to 0x7A, inclusive (ASCII alphanumeric), or which are 0x27 ('),
32
+ // 0x2D (-) or 0x5F (_).
33
+ for (let i = 0; i < boundary.length; i++) {
34
+ const cp = boundary.charCodeAt(i)
35
+
36
+ if (!(
37
+ (cp >= 0x30 && cp <= 0x39) ||
38
+ (cp >= 0x41 && cp <= 0x5a) ||
39
+ (cp >= 0x61 && cp <= 0x7a) ||
40
+ cp === 0x27 ||
41
+ cp === 0x2d ||
42
+ cp === 0x5f
43
+ )) {
44
+ return false
45
+ }
46
+ }
47
+
48
+ return true
49
+ }
50
+
51
+ /**
52
+ * @see https://andreubotella.github.io/multipart-form-data/#escape-a-multipart-form-data-name
53
+ * @param {string} name
54
+ * @param {string} [encoding='utf-8']
55
+ * @param {boolean} [isFilename=false]
56
+ */
57
+ function escapeFormDataName (name, encoding = 'utf-8', isFilename = false) {
58
+ // 1. If isFilename is true:
59
+ if (isFilename) {
60
+ // 1.1. Set name to the result of converting name into a scalar value string.
61
+ name = webidl.converters.USVString(name)
62
+ } else {
63
+ // 2. Otherwise:
64
+
65
+ // 2.1. Assert: name is a scalar value string.
66
+ assert(name === webidl.converters.USVString(name))
67
+
68
+ // 2.2. Replace every occurrence of U+000D (CR) not followed by U+000A (LF),
69
+ // and every occurrence of U+000A (LF) not preceded by U+000D (CR), in
70
+ // name, by a string consisting of U+000D (CR) and U+000A (LF).
71
+ name = name.replace(/\r\n?|\r?\n/g, '\r\n')
72
+ }
73
+
74
+ // 3. Let encoded be the result of encoding name with encoding.
75
+ assert(Buffer.isEncoding(encoding))
76
+
77
+ // 4. Replace every 0x0A (LF) bytes in encoded with the byte sequence `%0A`,
78
+ // 0x0D (CR) with `%0D` and 0x22 (") with `%22`.
79
+ name = name
80
+ .replace(/\n/g, '%0A')
81
+ .replace(/\r/g, '%0D')
82
+ .replace(/"/g, '%22')
83
+
84
+ // 5. Return encoded.
85
+ return Buffer.from(name, encoding) // encoded
86
+ }
87
+
88
+ /**
89
+ * @see https://andreubotella.github.io/multipart-form-data/#multipart-form-data-parser
90
+ * @param {Buffer} input
91
+ * @param {ReturnType<import('./data-url')['parseMIMEType']>} mimeType
92
+ */
93
+ function multipartFormDataParser (input, mimeType) {
94
+ // 1. Assert: mimeType’s essence is "multipart/form-data".
95
+ assert(mimeType !== 'failure' && mimeType.essence === 'multipart/form-data')
96
+
97
+ // 2. If mimeType’s parameters["boundary"] does not exist, return failure.
98
+ // Otherwise, let boundary be the result of UTF-8 decoding mimeType’s
99
+ // parameters["boundary"].
100
+ if (!mimeType.parameters.has('boundary')) {
101
+ return 'failure'
102
+ }
103
+
104
+ const boundary = Buffer.from(`--${mimeType.parameters.get('boundary')}`, 'utf8')
105
+
106
+ // 3. Let entry list be an empty entry list.
107
+ const entryList = []
108
+
109
+ // 4. Let position be a pointer to a byte in input, initially pointing at
110
+ // the first byte.
111
+ const position = { position: 0 }
112
+
113
+ // Note: undici addition, allow \r\n before the body.
114
+ if (input[0] === 0x0d && input[1] === 0x0a) {
115
+ position.position += 2
116
+ }
117
+
118
+ // 5. While true:
119
+ while (true) {
120
+ // 5.1. If position points to a sequence of bytes starting with 0x2D 0x2D
121
+ // (`--`) followed by boundary, advance position by 2 + the length of
122
+ // boundary. Otherwise, return failure.
123
+ // Note: boundary is padded with 2 dashes already, no need to add 2.
124
+ if (input.subarray(position.position, position.position + boundary.length).equals(boundary)) {
125
+ position.position += boundary.length
126
+ } else {
127
+ return 'failure'
128
+ }
129
+
130
+ // 5.2. If position points to the sequence of bytes 0x2D 0x2D 0x0D 0x0A
131
+ // (`--` followed by CR LF) followed by the end of input, return entry list.
132
+ // Note: a body does NOT need to end with CRLF. It can end with --.
133
+ if (
134
+ (position.position === input.length - 2 && bufferStartsWith(input, dd, position)) ||
135
+ (position.position === input.length - 4 && bufferStartsWith(input, ddcrlf, position))
136
+ ) {
137
+ return entryList
138
+ }
139
+
140
+ // 5.3. If position does not point to a sequence of bytes starting with 0x0D
141
+ // 0x0A (CR LF), return failure.
142
+ if (input[position.position] !== 0x0d || input[position.position + 1] !== 0x0a) {
143
+ return 'failure'
144
+ }
145
+
146
+ // 5.4. Advance position by 2. (This skips past the newline.)
147
+ position.position += 2
148
+
149
+ // 5.5. Let name, filename and contentType be the result of parsing
150
+ // multipart/form-data headers on input and position, if the result
151
+ // is not failure. Otherwise, return failure.
152
+ const result = parseMultipartFormDataHeaders(input, position)
153
+
154
+ if (result === 'failure') {
155
+ return 'failure'
156
+ }
157
+
158
+ let { name, filename, contentType, encoding } = result
159
+
160
+ // 5.6. Advance position by 2. (This skips past the empty line that marks
161
+ // the end of the headers.)
162
+ position.position += 2
163
+
164
+ // 5.7. Let body be the empty byte sequence.
165
+ let body
166
+
167
+ // 5.8. Body loop: While position is not past the end of input:
168
+ // TODO: the steps here are completely wrong
169
+ {
170
+ const boundaryIndex = input.indexOf(boundary.subarray(2), position.position)
171
+
172
+ if (boundaryIndex === -1) {
173
+ return 'failure'
174
+ }
175
+
176
+ body = input.subarray(position.position, boundaryIndex - 4)
177
+
178
+ position.position += body.length
179
+
180
+ // Note: position must be advanced by the body's length before being
181
+ // decoded, otherwise the parsing will fail.
182
+ if (encoding === 'base64') {
183
+ body = Buffer.from(body.toString(), 'base64')
184
+ }
185
+ }
186
+
187
+ // 5.9. If position does not point to a sequence of bytes starting with
188
+ // 0x0D 0x0A (CR LF), return failure. Otherwise, advance position by 2.
189
+ if (input[position.position] !== 0x0d || input[position.position + 1] !== 0x0a) {
190
+ return 'failure'
191
+ } else {
192
+ position.position += 2
193
+ }
194
+
195
+ // 5.10. If filename is not null:
196
+ let value
197
+
198
+ if (filename !== null) {
199
+ // 5.10.1. If contentType is null, set contentType to "text/plain".
200
+ contentType ??= 'text/plain'
201
+
202
+ // 5.10.2. If contentType is not an ASCII string, set contentType to the empty string.
203
+ if (!isAscii(Buffer.from(contentType))) {
204
+ contentType = ''
205
+ }
206
+
207
+ // 5.10.3. Let value be a new File object with name filename, type contentType, and body body.
208
+ value = new File([body], filename, { type: contentType })
209
+ } else {
210
+ // 5.11. Otherwise:
211
+
212
+ // 5.11.1. Let value be the UTF-8 decoding without BOM of body.
213
+ value = utf8DecodeBytes(Buffer.from(body))
214
+ }
215
+
216
+ // 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))
219
+
220
+ // 5.13. Create an entry with name and value, and append it to entry list.
221
+ entryList.push(makeEntry(name, value, filename))
222
+ }
223
+ }
224
+
225
+ /**
226
+ * @see https://andreubotella.github.io/multipart-form-data/#parse-multipart-form-data-headers
227
+ * @param {Buffer} input
228
+ * @param {{ position: number }} position
229
+ */
230
+ function parseMultipartFormDataHeaders (input, position) {
231
+ // 1. Let name, filename and contentType be null.
232
+ let name = null
233
+ let filename = null
234
+ let contentType = null
235
+ let encoding = null
236
+
237
+ // 2. While true:
238
+ while (true) {
239
+ // 2.1. If position points to a sequence of bytes starting with 0x0D 0x0A (CR LF):
240
+ if (input[position.position] === 0x0d && input[position.position + 1] === 0x0a) {
241
+ // 2.1.1. If name is null, return failure.
242
+ if (name === null) {
243
+ return 'failure'
244
+ }
245
+
246
+ // 2.1.2. Return name, filename and contentType.
247
+ return { name, filename, contentType, encoding }
248
+ }
249
+
250
+ // 2.2. Let header name be the result of collecting a sequence of bytes that are
251
+ // not 0x0A (LF), 0x0D (CR) or 0x3A (:), given position.
252
+ let headerName = collectASequenceOfBytes(
253
+ (char) => char !== 0x0a && char !== 0x0d && char !== 0x3a,
254
+ input,
255
+ position
256
+ )
257
+
258
+ // 2.3. Remove any HTTP tab or space bytes from the start or end of header name.
259
+ headerName = removeChars(headerName, true, true, (char) => char === 0x9 || char === 0x20)
260
+
261
+ // 2.4. If header name does not match the field-name token production, return failure.
262
+ if (!HTTP_TOKEN_CODEPOINTS.test(headerName.toString())) {
263
+ return 'failure'
264
+ }
265
+
266
+ // 2.5. If the byte at position is not 0x3A (:), return failure.
267
+ if (input[position.position] !== 0x3a) {
268
+ return 'failure'
269
+ }
270
+
271
+ // 2.6. Advance position by 1.
272
+ position.position++
273
+
274
+ // 2.7. Collect a sequence of bytes that are HTTP tab or space bytes given position.
275
+ // (Do nothing with those bytes.)
276
+ collectASequenceOfBytes(
277
+ (char) => char === 0x20 || char === 0x09,
278
+ input,
279
+ position
280
+ )
281
+
282
+ // 2.8. Byte-lowercase header name and switch on the result:
283
+ switch (new TextDecoder().decode(headerName).toLowerCase()) {
284
+ case 'content-disposition': {
285
+ // 1. Set name and filename to null.
286
+ name = filename = null
287
+
288
+ // 2. If position does not point to a sequence of bytes starting with
289
+ // `form-data; name="`, return failure.
290
+ if (!bufferStartsWith(input, formDataNameBuffer, position)) {
291
+ return 'failure'
292
+ }
293
+
294
+ // 3. Advance position so it points at the byte after the next 0x22 (")
295
+ // byte (the one in the sequence of bytes matched above).
296
+ position.position += 17
297
+
298
+ // 4. Set name to the result of parsing a multipart/form-data name given
299
+ // input and position, if the result is not failure. Otherwise, return
300
+ // failure.
301
+ name = parseMultipartFormDataName(input, position)
302
+
303
+ if (name === null) {
304
+ return 'failure'
305
+ }
306
+
307
+ // 5. If position points to a sequence of bytes starting with `; filename="`:
308
+ if (bufferStartsWith(input, filenameBuffer, position)) {
309
+ // Note: undici also handles filename*
310
+ let check = position.position + filenameBuffer.length
311
+
312
+ if (input[check] === 0x2a) {
313
+ position.position += 1
314
+ check += 1
315
+ }
316
+
317
+ if (input[check] !== 0x3d || input[check + 1] !== 0x22) { // ="
318
+ return 'failure'
319
+ }
320
+
321
+ // 1. Advance position so it points at the byte after the next 0x22 (") byte
322
+ // (the one in the sequence of bytes matched above).
323
+ position.position += 12
324
+
325
+ // 2. Set filename to the result of parsing a multipart/form-data name given
326
+ // input and position, if the result is not failure. Otherwise, return failure.
327
+ filename = parseMultipartFormDataName(input, position)
328
+
329
+ if (filename === null) {
330
+ return 'failure'
331
+ }
332
+ }
333
+
334
+ break
335
+ }
336
+ case 'content-type': {
337
+ // 1. Let header value be the result of collecting a sequence of bytes that are
338
+ // not 0x0A (LF) or 0x0D (CR), given position.
339
+ let headerValue = collectASequenceOfBytes(
340
+ (char) => char !== 0x0a && char !== 0x0d,
341
+ input,
342
+ position
343
+ )
344
+
345
+ // 2. Remove any HTTP tab or space bytes from the end of header value.
346
+ headerValue = removeChars(headerValue, false, true, (char) => char === 0x9 || char === 0x20)
347
+
348
+ // 3. Set contentType to the isomorphic decoding of header value.
349
+ contentType = isomorphicDecode(headerValue)
350
+
351
+ break
352
+ }
353
+ case 'content-transfer-encoding': {
354
+ let headerValue = collectASequenceOfBytes(
355
+ (char) => char !== 0x0a && char !== 0x0d,
356
+ input,
357
+ position
358
+ )
359
+
360
+ headerValue = removeChars(headerValue, false, true, (char) => char === 0x9 || char === 0x20)
361
+
362
+ encoding = isomorphicDecode(headerValue)
363
+
364
+ break
365
+ }
366
+ default: {
367
+ // Collect a sequence of bytes that are not 0x0A (LF) or 0x0D (CR), given position.
368
+ // (Do nothing with those bytes.)
369
+ collectASequenceOfBytes(
370
+ (char) => char !== 0x0a && char !== 0x0d,
371
+ input,
372
+ position
373
+ )
374
+ }
375
+ }
376
+
377
+ // 2.9. If position does not point to a sequence of bytes starting with 0x0D 0x0A
378
+ // (CR LF), return failure. Otherwise, advance position by 2 (past the newline).
379
+ if (input[position.position] !== 0x0d && input[position.position + 1] !== 0x0a) {
380
+ return 'failure'
381
+ } else {
382
+ position.position += 2
383
+ }
384
+ }
385
+ }
386
+
387
+ /**
388
+ * @see https://andreubotella.github.io/multipart-form-data/#parse-a-multipart-form-data-name
389
+ * @param {Buffer} input
390
+ * @param {{ position: number }} position
391
+ */
392
+ function parseMultipartFormDataName (input, position) {
393
+ // 1. Assert: The byte at (position - 1) is 0x22 (").
394
+ assert(input[position.position - 1] === 0x22)
395
+
396
+ // 2. Let name be the result of collecting a sequence of bytes that are not 0x0A (LF), 0x0D (CR) or 0x22 ("), given position.
397
+ /** @type {string | Buffer} */
398
+ let name = collectASequenceOfBytes(
399
+ (char) => char !== 0x0a && char !== 0x0d && char !== 0x22,
400
+ input,
401
+ position
402
+ )
403
+
404
+ // 3. If the byte at position is not 0x22 ("), return failure. Otherwise, advance position by 1.
405
+ if (input[position.position] !== 0x22) {
406
+ return null // name could be 'failure'
407
+ } else {
408
+ position.position++
409
+ }
410
+
411
+ // 4. Replace any occurrence of the following subsequences in name with the given byte:
412
+ // - `%0A`: 0x0A (LF)
413
+ // - `%0D`: 0x0D (CR)
414
+ // - `%22`: 0x22 (")
415
+ name = new TextDecoder().decode(name)
416
+ .replace(/%0A/ig, '\n')
417
+ .replace(/%0D/ig, '\r')
418
+ .replace(/%22/g, '"')
419
+
420
+ // 5. Return the UTF-8 decoding without BOM of name.
421
+ return name
422
+ }
423
+
424
+ /**
425
+ * @param {(char: number) => boolean} condition
426
+ * @param {Buffer} input
427
+ * @param {{ position: number }} position
428
+ */
429
+ function collectASequenceOfBytes (condition, input, position) {
430
+ const result = []
431
+ let index = 0
432
+
433
+ while (position.position < input.length && condition(input[position.position])) {
434
+ result[index++] = input[position.position]
435
+
436
+ position.position++
437
+ }
438
+
439
+ return Buffer.from(result, result.length)
440
+ }
441
+
442
+ /**
443
+ * @param {Buffer} buf
444
+ * @param {boolean} leading
445
+ * @param {boolean} trailing
446
+ * @param {(charCode: number) => boolean} predicate
447
+ * @returns {Buffer}
448
+ */
449
+ function removeChars (buf, leading, trailing, predicate) {
450
+ let lead = 0
451
+ let trail = buf.length - 1
452
+
453
+ if (leading) {
454
+ while (lead < buf.length && predicate(buf[lead])) lead++
455
+ }
456
+
457
+ if (trailing) {
458
+ while (trail > 0 && predicate(buf[trail])) trail--
459
+ }
460
+
461
+ return lead === 0 && trail === buf.length - 1 ? buf : buf.subarray(lead, trail + 1)
462
+ }
463
+
464
+ /**
465
+ * Checks if {@param buffer} starts with {@param start}
466
+ * @param {Buffer} buffer
467
+ * @param {Buffer} start
468
+ * @param {{ position: number }} position
469
+ */
470
+ function bufferStartsWith (buffer, start, position) {
471
+ if (buffer.length < start.length) {
472
+ return false
473
+ }
474
+
475
+ for (let i = 0; i < start.length; i++) {
476
+ if (start[i] !== buffer[position.position + i]) {
477
+ return false
478
+ }
479
+ }
480
+
481
+ return true
482
+ }
483
+
484
+ module.exports = {
485
+ multipartFormDataParser,
486
+ validateBoundary,
487
+ escapeFormDataName
488
+ }
@@ -1,8 +1,8 @@
1
1
  'use strict'
2
2
 
3
- const { isBlobLike, toUSVString, makeIterator } = require('./util')
3
+ const { isBlobLike, iteratorMixin } = require('./util')
4
4
  const { kState } = require('./symbols')
5
- const { kEnumerableProperty } = require('../core/util')
5
+ const { kEnumerableProperty } = require('../../core/util')
6
6
  const { File: UndiciFile, FileLike, isFileLike } = require('./file')
7
7
  const { webidl } = require('./webidl')
8
8
  const { File: NativeFile } = require('node:buffer')
@@ -133,7 +133,7 @@ class FormData {
133
133
  ? webidl.converters.Blob(value, { strict: false })
134
134
  : webidl.converters.USVString(value)
135
135
  filename = arguments.length === 3
136
- ? toUSVString(filename)
136
+ ? webidl.converters.USVString(filename)
137
137
  : undefined
138
138
 
139
139
  // 2. Let entry be the result of creating an entry with name, value, and
@@ -154,62 +154,9 @@ class FormData {
154
154
  this[kState].push(entry)
155
155
  }
156
156
  }
157
-
158
- entries () {
159
- webidl.brandCheck(this, FormData)
160
-
161
- return makeIterator(
162
- () => this[kState],
163
- 'FormData',
164
- 'key+value',
165
- 'name', 'value'
166
- )
167
- }
168
-
169
- keys () {
170
- webidl.brandCheck(this, FormData)
171
-
172
- return makeIterator(
173
- () => this[kState],
174
- 'FormData',
175
- 'key',
176
- 'name', 'value'
177
- )
178
- }
179
-
180
- values () {
181
- webidl.brandCheck(this, FormData)
182
-
183
- return makeIterator(
184
- () => this[kState],
185
- 'FormData',
186
- 'value',
187
- 'name', 'value'
188
- )
189
- }
190
-
191
- /**
192
- * @param {(value: string, key: string, self: FormData) => void} callbackFn
193
- * @param {unknown} thisArg
194
- */
195
- forEach (callbackFn, thisArg = globalThis) {
196
- webidl.brandCheck(this, FormData)
197
-
198
- webidl.argumentLengthCheck(arguments, 1, { header: 'FormData.forEach' })
199
-
200
- if (typeof callbackFn !== 'function') {
201
- throw new TypeError(
202
- "Failed to execute 'forEach' on 'FormData': parameter 1 is not of type 'Function'."
203
- )
204
- }
205
-
206
- for (const [key, value] of this) {
207
- callbackFn.call(thisArg, value, key, this)
208
- }
209
- }
210
157
  }
211
158
 
212
- FormData.prototype[Symbol.iterator] = FormData.prototype.entries
159
+ iteratorMixin('FormData', FormData, kState, 'name', 'value')
213
160
 
214
161
  Object.defineProperties(FormData.prototype, {
215
162
  append: kEnumerableProperty,
@@ -218,11 +165,6 @@ Object.defineProperties(FormData.prototype, {
218
165
  getAll: kEnumerableProperty,
219
166
  has: kEnumerableProperty,
220
167
  set: kEnumerableProperty,
221
- entries: kEnumerableProperty,
222
- keys: kEnumerableProperty,
223
- values: kEnumerableProperty,
224
- forEach: kEnumerableProperty,
225
- [Symbol.iterator]: { enumerable: false },
226
168
  [Symbol.toStringTag]: {
227
169
  value: 'FormData',
228
170
  configurable: true
@@ -238,15 +180,12 @@ Object.defineProperties(FormData.prototype, {
238
180
  */
239
181
  function makeEntry (name, value, filename) {
240
182
  // 1. Set name to the result of converting name into a scalar value string.
241
- // "To convert a string into a scalar value string, replace any surrogates
242
- // with U+FFFD."
243
- // see: https://nodejs.org/dist/latest-v20.x/docs/api/buffer.html#buftostringencoding-start-end
244
- name = Buffer.from(name).toString('utf8')
183
+ // Note: This operation was done by the webidl converter USVString.
245
184
 
246
185
  // 2. If value is a string, then set value to the result of converting
247
186
  // value into a scalar value string.
248
187
  if (typeof value === 'string') {
249
- value = Buffer.from(value).toString('utf8')
188
+ // Note: This operation was done by the webidl converter USVString.
250
189
  } else {
251
190
  // 3. Otherwise:
252
191
 
@@ -277,4 +216,4 @@ function makeEntry (name, value, filename) {
277
216
  return { name, value }
278
217
  }
279
218
 
280
- module.exports = { FormData }
219
+ module.exports = { FormData, makeEntry }