parse-nested-form-data 1.0.0 → 1.1.0

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.
@@ -1,393 +1,602 @@
1
- 'use strict';
2
-
3
- Object.defineProperty(exports, '__esModule', { value: true });
4
-
5
- var webFile = require('@remix-run/web-file');
6
-
7
1
  /**
8
2
  * @name parse-nested-form-data
9
3
  * @license MIT license.
10
4
  * @copyright (c) 2022 Christian Schurr
11
5
  * @author Christian Schurr <chris@schurr.dev>
12
6
  */
13
-
7
+ Object.defineProperties(exports, {
8
+ __esModule: { value: true },
9
+ [Symbol.toStringTag]: { value: "Module" }
10
+ });
11
+ // src/errors.ts
14
12
  /**
15
- * Thrown when a path is used multiple times or has missmatching path parts.
16
- *
17
- * @example
18
- * ```ts
19
- * const formData = new FormData()
20
- * formData.append('a[0]', 'b')
21
- * formData.append('a[0]', 'c')
22
- * parseFormData(formData)
23
- * // throws DuplicateKeyError('a[0]')
24
- * ```
25
- *
26
- * @example
27
- * ```ts
28
- * const formData = new FormData()
29
- * formData.append('a', 'b')
30
- * formData.append('a', 'c')
31
- * parseFormData(formData)
32
- * // throws DuplicateKeyError('a')
33
- *
34
- * @example
35
- * ```ts
36
- * const formData = new FormData()
37
- * formData.append('a', 'b')
38
- * formData.append('a[]', 'c')
39
- * parseFormData(formData)
40
- * // throws DuplicateKeyError('a[]')
41
- * ```
42
- *
43
- */
44
- class DuplicateKeyError extends Error {
45
- constructor(key) {
46
- super(`Duplicate key at path part ${key}`);
47
- this.key = void 0;
48
- this.key = key;
49
- }
13
+ * Thrown when a path is used multiple times or has mismatching path parts.
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * const formData = new FormData()
18
+ * formData.append('a[0]', 'b')
19
+ * formData.append('a[0]', 'c')
20
+ * parseFormData(formData)
21
+ * // throws DuplicateKeyError('a[0]')
22
+ * ```
23
+ *
24
+ * @example
25
+ * ```ts
26
+ * const formData = new FormData()
27
+ * formData.append('a', 'b')
28
+ * formData.append('a', 'c')
29
+ * parseFormData(formData)
30
+ * // throws DuplicateKeyError('a')
31
+ * ```
32
+ *
33
+ * @example
34
+ * ```ts
35
+ * const formData = new FormData()
36
+ * formData.append('a', 'b')
37
+ * formData.append('a[]', 'c')
38
+ * parseFormData(formData)
39
+ * // throws DuplicateKeyError('a[]')
40
+ * ```
41
+ *
42
+ */
43
+ var DuplicateKeyError = class extends Error {
44
+ constructor(key) {
45
+ super(`Duplicate key at path part ${key}`);
46
+ this.name = "DuplicateKeyError";
47
+ this.key = key;
48
+ }
49
+ };
50
+ /**
51
+ * Thrown when an array is used at the same path with an order parameter and
52
+ * without an order parameter.
53
+ *
54
+ * @example
55
+ * ```ts
56
+ * const formData = new FormData()
57
+ * formData.append('a[0]', 'a')
58
+ * formData.append('a[]', 'b')
59
+ * parseFormData(formData)
60
+ * // => throws `MixedArrayError(a[])`
61
+ * ```
62
+ *
63
+ * @example
64
+ * ```ts
65
+ * const formData = new FormData()
66
+ * formData.append('a[]', 'a')
67
+ * formData.append('a[0]', 'b')
68
+ * parseFormData(formData)
69
+ * // => throws `MixedArrayError(a[0])`
70
+ * ```
71
+ */
72
+ var MixedArrayError = class extends Error {
73
+ constructor(key) {
74
+ super(`Mixed array at path part ${key}`);
75
+ this.name = "MixedArrayError";
76
+ this.key = key;
77
+ }
78
+ };
79
+ /**
80
+ * Cuts a path to 100 characters plus `...`, so a huge path from a client
81
+ * doesn't end up in full in logs or error responses.
82
+ */
83
+ function shortKey(key) {
84
+ return key.length > 100 ? `${key.slice(0, 100)}...` : key;
50
85
  }
51
86
  /**
52
- * Thrown when an array is used at the same path with an order parameter and
53
- * without an order parameter.
54
- *
55
- * @example
56
- * ```ts
57
- * const formData = new FormData()
58
- * formData.append('a[0]', 'a')
59
- * formData.append('a[]', 'b')
60
- * parseFormData(formData)
61
- * // => throws `MixedArrayError(a[])`
62
- * ```
63
- *
64
- * @example
65
- * ```ts
66
- * const formData = new FormData()
67
- * formData.append('a[]', 'a')
68
- * formData.append('a[0]', 'b')
69
- * parseFormData(formData)
70
- * // => throws `MixedArrayError(a[0])`
71
- * ```
72
- */
73
- class MixedArrayError extends Error {
74
- constructor(key) {
75
- super(`Mixed array at path part ${key}`);
76
- this.key = void 0;
77
- this.key = key;
78
- }
87
+ * Thrown when an object path part is `__proto__`, `constructor` or
88
+ * `prototype`, which could modify `Object.prototype`. The names are rejected
89
+ * at any position in the path. Entries with other names on `Object.prototype`
90
+ * (`toString`, ...) are removed instead (see `parseFormData`).
91
+ * `key` is the path up to the forbidden part, cut to 100 characters plus `...`
92
+ * when it's longer.
93
+ *
94
+ * @example
95
+ * ```ts
96
+ * const formData = new FormData()
97
+ * formData.append('__proto__.polluted', 'yes')
98
+ * parseFormData(formData)
99
+ * // throws ForbiddenKeyError('__proto__')
100
+ * ```
101
+ */
102
+ var ForbiddenKeyError = class extends Error {
103
+ constructor(key) {
104
+ key = shortKey(key);
105
+ super(`Forbidden key at path part ${key}`);
106
+ this.name = "ForbiddenKeyError";
107
+ this.key = key;
108
+ }
109
+ };
110
+ /**
111
+ * Thrown when a path has more path parts than the `depth` option allows.
112
+ * `key` is the path up to the first path part over the limit, cut to
113
+ * 100 characters plus `...` when it's longer.
114
+ * Within one entry the depth is checked first, so a path that's too deep throws
115
+ * `DepthLimitError` even if it would also throw another error, like a
116
+ * `ForbiddenKeyError` for `__proto__`.
117
+ *
118
+ * @example
119
+ * ```ts
120
+ * const formData = new FormData()
121
+ * formData.append('a.b[0].c', 'x')
122
+ * parseFormData(formData, {depth: 2})
123
+ * // throws DepthLimitError('a.b[0]')
124
+ * ```
125
+ */
126
+ var DepthLimitError = class extends Error {
127
+ constructor(key) {
128
+ key = shortKey(key);
129
+ super(`Depth limit exceeded at path part ${key}`);
130
+ this.name = "DepthLimitError";
131
+ this.key = key;
132
+ }
133
+ };
134
+ /**
135
+ * Thrown when an array index in a path is larger than `4294967294`, the
136
+ * largest index a JavaScript array can hold.
137
+ * `key` is the path up to that array part, cut to 100 characters plus `...`
138
+ * when it's longer.
139
+ *
140
+ * @example
141
+ * ```ts
142
+ * const formData = new FormData()
143
+ * formData.append('a[4294967295]', 'b')
144
+ * parseFormData(formData)
145
+ * // throws InvalidPathError('a[4294967295]')
146
+ * ```
147
+ */
148
+ var InvalidPathError = class extends Error {
149
+ constructor(key) {
150
+ key = shortKey(key);
151
+ super(`Invalid path at path part ${key}`);
152
+ this.name = "InvalidPathError";
153
+ this.key = key;
154
+ }
155
+ };
156
+ // src/path-parts.ts
157
+ /**
158
+ * Finds the `]` that ends the bracket part starting at the `[` at `open`.
159
+ * Returns -1 if this `[` is not a bracket part but a normal character of the key.
160
+ *
161
+ * Brackets can be nested: in `a[b[c]]` the last `]` belongs to
162
+ * the first `[`.
163
+ *
164
+ * Special cases:
165
+ * - Some `[` are never closed. The part then ends at the last `]`:
166
+ * `a[[0]` => key `[0`.
167
+ * - If there is no `]` after the `[` at all, the `[` is a normal character:
168
+ * `a[0` => key `a[0`.
169
+ * - The same goes for a `]` without a `[`: `a]b` => key `a]b`, and
170
+ * `a[b]]` => key `]` inside `b`.
171
+ *
172
+ * The scan stops at the returned `]`. The caller continues from there, so no
173
+ * character of the path is scanned twice.
174
+ */
175
+ function closeOf(path, open, lastClose) {
176
+ if (open > lastClose) return -1;
177
+ let nesting = 0;
178
+ for (let idx = open; idx < lastClose; idx++) if (path[idx] === "[") nesting++;
179
+ else if (path[idx] === "]" && --nesting === 0) return idx;
180
+ return lastClose;
79
181
  }
80
- function isJsonObject(val) {
81
- return typeof val === 'object' && !Array.isArray(val) && val !== null && !(val instanceof webFile.File);
182
+ /**
183
+ *
184
+ * Transforms a FormData path into an array of `PathPart`s.
185
+ *
186
+ * @param path - the path to extract the path parts from
187
+ * @param depth - the maximum number of path parts (default: `Infinity`)
188
+ * @returns {Array<PathPart>} the extracted path parts
189
+ * @throws `DepthLimitError` if the path has more path parts than `depth`, at
190
+ * the first part over the limit
191
+ *
192
+ * @example
193
+ * ```ts
194
+ * const path = 'a[0].b'
195
+ * const result = extractPathParts(path)
196
+ * // => [{path: 'a', type: 'object', default: [], pathToPart: 'a'},
197
+ * // {path: '0', type: 'array', default: {}, pathToPart: 'a[0]'},
198
+ * // {path: 'b', type: 'object', default: {}, pathToPart: 'a[0].b'}]
199
+ * ```
200
+ *
201
+ * @example
202
+ * ```ts
203
+ * const path = 'a.b'
204
+ * const result = extractPathParts(path)
205
+ * // => [{path: 'a', type: 'object', default: {}, pathToPart: 'a'},
206
+ * // {path: 'b', type: 'object', default: {}, pathToPart: 'a.b'}]
207
+ * ```
208
+ *
209
+ * @example
210
+ * ```ts
211
+ * const path = 'a[b][0]'
212
+ * const result = extractPathParts(path)
213
+ * // => [{path: 'a', type: 'object', default: {}, pathToPart: 'a'},
214
+ * // {path: 'b', type: 'object', default: [], pathToPart: 'a[b]'},
215
+ * // {path: '0', type: 'array', default: {}, pathToPart: 'a[b][0]'}]
216
+ * ```
217
+ *
218
+ * @example
219
+ * ```ts
220
+ * const path = 'a[][0]'
221
+ * const result = extractPathParts(path)
222
+ * // => [{path: 'a', type: 'object', default: [], pathToPart: 'a'},
223
+ * // {path: '', type: 'array', default: [], pathToPart: 'a[]'},
224
+ * // {path: '0', type: 'array', default: {}, pathToPart: 'a[][0]'}]
225
+ * ```
226
+ *
227
+ *
228
+ */
229
+ function extractPathParts(path, depth = Infinity) {
230
+ const lastClose = path.lastIndexOf("]");
231
+ const parts = [];
232
+ const addPart = (key, type, end) => {
233
+ if (key === "" && type === "object") return;
234
+ if (parts.length === depth) throw new DepthLimitError(path.slice(0, end));
235
+ parts.push({
236
+ path: key,
237
+ type,
238
+ default: {},
239
+ pathToPart: path.slice(0, end)
240
+ });
241
+ };
242
+ let start = 0;
243
+ for (let idx = 0; idx < path.length; idx++) {
244
+ const close = path[idx] === "[" ? closeOf(path, idx, lastClose) : -1;
245
+ if (path[idx] !== "." && close < idx) continue;
246
+ addPart(path.slice(start, idx), "object", idx);
247
+ if (close > idx) {
248
+ const key = path.slice(idx + 1, close);
249
+ addPart(key, /^\d*$/.test(key) ? "array" : "object", close + 1);
250
+ idx = close;
251
+ }
252
+ start = idx + 1;
253
+ }
254
+ addPart(path.slice(start), "object", path.length);
255
+ parts.forEach((part, idx) => {
256
+ if (parts[idx + 1]?.type === "array") part.default = [];
257
+ });
258
+ return parts;
82
259
  }
83
-
260
+ // src/transform.ts
84
261
  /**
85
- * Default Transformer for `parseFormData`.
86
- *
87
- * Transforms a FormData Entry into a path and a `JsonLeafValue`.
88
- *
89
- * - `path` starts with `+` -> transform value to `number`
90
- * - `path` starts with `&` -> transform value to `boolean`
91
- * - `path` starts with `-` -> transform value to `null`
92
- *
93
- * @example
94
- * ```ts
95
- * const entry = ['a[0]', 'b']
96
- * const result = defaultTransform(entry)
97
- * // => {path: 'a[0]', value: 'b'}
98
- * ```
99
- *
100
- * @example
101
- * ```ts
102
- * const entry = ['+a[0]', '1']
103
- * const result = defaultTransform(entry)
104
- * // => {path: 'a[0]', value: 1}
105
- * ```
106
- *
107
- * @example
108
- * ```ts
109
- * const entry = ['&a[0]', 'true']
110
- * const result = defaultTransform(entry)
111
- * // => {path: 'a[0]', value: true}
112
- * ```
113
- *
114
- * @example
115
- * ```ts
116
- * const entry = ['-a[0]', 'null']
117
- * const result = defaultTransform(entry)
118
- * // => {path: 'a[0]', value: null}
119
- * ```
120
- *
121
- * @example
122
- * ```ts
123
- * const entry = ['a[0]', new File([''], 'file.txt')]
124
- * const result = defaultTransform(entry)
125
- * // => {path: 'a[0]', value: File}
126
- * ```
127
- *
128
- *
129
- * @param entry [path, value]: the FormData entry
130
- * @returns the path and the transformed value
131
- */
262
+ * Default Transformer for `parseFormData`.
263
+ *
264
+ * Transforms a FormData Entry into a path and a `JsonLeafValue`.
265
+ *
266
+ * - `path` starts with `+` -> transform value to `number`
267
+ * - `path` starts with `&` -> transform value to `boolean`
268
+ * - `path` starts with `-` -> transform value to `null`
269
+ *
270
+ * @example
271
+ * ```ts
272
+ * const entry = ['a[0]', 'b']
273
+ * const result = defaultTransform(entry)
274
+ * // => {path: 'a[0]', value: 'b'}
275
+ * ```
276
+ *
277
+ * @example
278
+ * ```ts
279
+ * const entry = ['+a[0]', '1']
280
+ * const result = defaultTransform(entry)
281
+ * // => {path: 'a[0]', value: 1}
282
+ * ```
283
+ *
284
+ * @example
285
+ * ```ts
286
+ * const entry = ['&a[0]', 'true']
287
+ * const result = defaultTransform(entry)
288
+ * // => {path: 'a[0]', value: true}
289
+ * ```
290
+ *
291
+ * @example
292
+ * ```ts
293
+ * const entry = ['-a[0]', 'null']
294
+ * const result = defaultTransform(entry)
295
+ * // => {path: 'a[0]', value: null}
296
+ * ```
297
+ *
298
+ * @example
299
+ * ```ts
300
+ * const entry = ['a[0]', new File([''], 'file.txt')]
301
+ * const result = defaultTransform(entry)
302
+ * // => {path: 'a[0]', value: File}
303
+ * ```
304
+ *
305
+ *
306
+ * @param entry [path, value]: the FormData entry
307
+ * @returns the path and the transformed value
308
+ */
132
309
  function defaultTransform(entry) {
133
- let path = entry[0];
134
- let value = entry[1];
135
- if (path.startsWith('+')) {
136
- path = path.slice(1);
137
- value = Number(value);
138
- } else if (path.startsWith('&')) {
139
- path = path.slice(1);
140
- value = value === 'on' || value === 'true' || Boolean(Number(value));
141
- } else if (path.startsWith('-')) {
142
- path = path.slice(1);
143
- value = null;
144
- }
145
- return {
146
- path,
147
- value
148
- };
310
+ let path = entry[0];
311
+ let value = entry[1];
312
+ if (path.startsWith("+")) {
313
+ path = path.slice(1);
314
+ value = Number(value);
315
+ } else if (path.startsWith("&")) {
316
+ path = path.slice(1);
317
+ value = value === "on" || value === "true" || Boolean(Number(value));
318
+ } else if (path.startsWith("-")) {
319
+ path = path.slice(1);
320
+ value = null;
321
+ }
322
+ return {
323
+ path,
324
+ value
325
+ };
326
+ }
327
+ // src/index.ts
328
+ const FORBIDDEN_OBJECT_KEYS = /* @__PURE__ */ new Set([
329
+ "__proto__",
330
+ "constructor",
331
+ "prototype"
332
+ ]);
333
+ const MAX_ARRAY_INDEX = 4294967294;
334
+ function isPrototypeKey(key) {
335
+ return Object.getOwnPropertyDescriptor(Object.prototype, key) !== void 0;
149
336
  }
150
337
  /**
151
- *
152
- * Transforms a FormData path into an array of `PathPart`s.
153
- *
154
- * @param path - the path to extract the path parts from
155
- * @returns {Array<PathPart>} the extracted path parts
156
- *
157
- * @example
158
- * ```ts
159
- * const path = 'a[0].b'
160
- * const result = extractPathParts(path)
161
- * // => [{path: 'a', type: 'object, default: [], pathToPart: 'a'},
162
- * // {path: '0', type: 'array', default: {}, pathToPart: 'a[0]'},
163
- * // {path: 'b', type: 'object', default: {}, pathToPart: 'a[0].b'}]
164
- * ```
165
- *
166
- * @example
167
- * ```ts
168
- * const path = 'a.b'
169
- * const result = extractPathParts(path)
170
- * // => [{path: 'a', type: 'object, default: {}, pathToPart: 'a'},
171
- * // {path: 'b', type: 'object', default: {}, pathToPart: 'a.b'}]
172
- * ```
173
- *
174
- * @example
175
- * ```ts
176
- * const path = 'a[][0]'
177
- * const result = extractPathParts(path)
178
- * // => [{path: 'a', type: 'object, default: [], pathToPart: 'a'},
179
- * // {path: '', type: 'array', default: [], pathToPart: 'a[]'},
180
- * // {path: '0', type: 'array', default: {}, pathToPart: 'a[][0]'}]
181
- * ```
182
- *
183
- *
184
- */
185
- function extractPathParts(path) {
186
- const re = /((?<array>\d*)\]|(?<pathPart>[^.[]+))(?<nextType>\[|\.|$)/g;
187
- return Array.from(path.matchAll(re)).map(match => {
188
- // self casted RegexExpMatchArray to custom
189
- const typedMatch = match;
190
- const {
191
- array,
192
- pathPart,
193
- nextType
194
- } = typedMatch.groups;
195
- const type = array === undefined ? 'object' : 'array';
196
- const nextDefault = nextType === '[' ? [] : {};
197
- return {
198
- path: array ?? pathPart,
199
- type,
200
- default: nextDefault,
201
- pathToPart: path.slice(0, typedMatch.index + typedMatch[1].length)
202
- };
203
- });
338
+ * Detects `File` and `Blob` values by their `Symbol.toStringTag`, which works
339
+ * for native instances, polyfills (e.g. `@remix-run/web-file`) and values from
340
+ * other realms, where `instanceof` would fail.
341
+ */
342
+ function isFileLike(val) {
343
+ const tag = Object.prototype.toString.call(val);
344
+ return tag === "[object File]" || tag === "[object Blob]";
345
+ }
346
+ function isJsonObject(val) {
347
+ return typeof val === "object" && !Array.isArray(val) && val !== null && !isFileLike(val);
204
348
  }
205
-
206
349
  /**
207
- *
208
- * Returns the value accessed via `pathPart` in the `currentPathObject`
209
- * and a setter function to set the value in the `currentPathObject` via the
210
- * provided `pathPart`.
211
- *
212
- * @param pathPart - the path part to get the setter and getter for
213
- * @param currentPathObject - the object at the current path (before the path part)
214
- * @param arraysWithOrder - a set of arrays that have an order
215
- * @returns the setter and getter for the path part
216
- *
217
- * @example
218
- * ```ts
219
- * const pathPart = {path: 'a', type: 'object', default: {}, pathToPart: 'a'}
220
- * const currentPathObject = {}
221
- * const arraysWithOrder = new Set()
222
- * const [value, setValue] = getSetterAndGetter(pathPart, currentPathObject, arraysWithOrder)
223
- * setValue('b')
224
- * // => currentPathObject = {a: 'b'}
225
- * ```
226
- *
227
- * @example
228
- * ```ts
229
- * const pathPart = {path: '0', type: 'array', default: [], pathToPart: 'a[0]'}
230
- * const currentPathObject = {a: []}
231
- * const arraysWithOrder = new Set()
232
- * const [value, setValue] = getSetterAndGetter(pathPart, currentPathObject, arraysWithOrder)
233
- * setValue('b')
234
- * // => currentPathObject = {a: ['b']}
235
- * ```
236
- *
237
- */
350
+ *
351
+ * Returns the value accessed via `pathPart` in the `currentPathObject`,
352
+ * a setter function to set the value in the `currentPathObject` via the
353
+ * provided `pathPart`, and the key (object key or array index) it uses.
354
+ *
355
+ * @param pathPart - the path part to get the value, setter and key for
356
+ * @param currentPathObject - the object at the current path (before the path part)
357
+ * @param arraysWithOrder - a set of arrays that have an order
358
+ * @returns the current value, a setter for it, and the key (object key or
359
+ * array index) in `currentPathObject`
360
+ *
361
+ * @example
362
+ * ```ts
363
+ * const pathPart = {path: 'a', type: 'object', default: {}, pathToPart: 'a'}
364
+ * const currentPathObject = {}
365
+ * const arraysWithOrder = new Set()
366
+ * const [value, setValue, key] = handlePathPart(pathPart, currentPathObject, arraysWithOrder)
367
+ * setValue('b')
368
+ * // => currentPathObject = {a: 'b'}, key = 'a'
369
+ * ```
370
+ *
371
+ * @example
372
+ * ```ts
373
+ * const pathPart = {path: '0', type: 'array', default: [], pathToPart: 'a[0]'}
374
+ * const currentPathObject = {a: []}
375
+ * const arraysWithOrder = new Set()
376
+ * const [value, setValue, key] = handlePathPart(pathPart, currentPathObject.a, arraysWithOrder)
377
+ * setValue('b')
378
+ * // => currentPathObject = {a: ['b']}, key = 0
379
+ * ```
380
+ *
381
+ */
238
382
  function handlePathPart(pathPart, currentPathObject, arraysWithOrder) {
239
- if (pathPart.type === 'object') {
240
- if (Array.isArray(currentPathObject)) {
241
- throw new DuplicateKeyError(pathPart.pathToPart);
242
- }
243
- const currentObject = currentPathObject;
244
- return [currentObject[pathPart.path], val => currentObject[pathPart.path] = val];
245
- }
246
- if (!Array.isArray(currentPathObject)) {
247
- throw new DuplicateKeyError(pathPart.pathToPart);
248
- }
249
- const currentArray = currentPathObject;
250
- const isOrdered = pathPart.path !== '';
251
- const isOrderedArray = arraysWithOrder.has(currentArray);
252
- if (isOrdered) {
253
- arraysWithOrder.add(currentArray);
254
- }
255
- if (!isOrdered && isOrderedArray || isOrdered && !isOrderedArray && currentArray.length > 0) {
256
- throw new MixedArrayError(pathPart.pathToPart);
257
- }
258
- const order = isOrdered ? Number(pathPart.path) : currentArray.length;
259
- return [currentArray[order], val => currentArray[order] = val];
383
+ if (pathPart.type === "object") {
384
+ const key = pathPart.path;
385
+ if (FORBIDDEN_OBJECT_KEYS.has(key)) throw new ForbiddenKeyError(pathPart.pathToPart);
386
+ if (Array.isArray(currentPathObject)) throw new DuplicateKeyError(pathPart.pathToPart);
387
+ const currentObject = currentPathObject;
388
+ return [
389
+ Object.prototype.hasOwnProperty.call(currentObject, key) ? currentObject[key] : void 0,
390
+ (val) => {
391
+ if (!isPrototypeKey(key)) {
392
+ currentObject[key] = val;
393
+ return;
394
+ }
395
+ Object.defineProperty(currentObject, key, {
396
+ value: val,
397
+ writable: true,
398
+ enumerable: true,
399
+ configurable: true
400
+ });
401
+ },
402
+ key
403
+ ];
404
+ }
405
+ const isOrdered = pathPart.path !== "";
406
+ if (isOrdered && Number(pathPart.path) > MAX_ARRAY_INDEX) throw new InvalidPathError(pathPart.pathToPart);
407
+ if (!Array.isArray(currentPathObject)) throw new DuplicateKeyError(pathPart.pathToPart);
408
+ const currentArray = currentPathObject;
409
+ const isOrderedArray = arraysWithOrder.has(currentArray);
410
+ if (isOrdered) arraysWithOrder.add(currentArray);
411
+ if (!isOrdered && isOrderedArray || isOrdered && !isOrderedArray && currentArray.length > 0) throw new MixedArrayError(pathPart.pathToPart);
412
+ const order = isOrdered ? Number(pathPart.path) : currentArray.length;
413
+ return [
414
+ currentArray[order],
415
+ (val) => currentArray[order] = val,
416
+ order
417
+ ];
260
418
  }
261
-
262
419
  /**
263
- *
264
- * Parses a FormData object to a JSON object. This is done by parsing the `name`
265
- * attribute of each `FormDataEntryValue` and then inserting the value at the
266
- * path. Also by default the start of the path is used to transform the value.
267
- *
268
- *
269
- * In front of the whole `key`:
270
- * - `+` => parse to `Number`
271
- * - `-` => set value to `null`
272
- * - `&` => parse to `Boolean`
273
- *
274
- * - `.` between path parts => nest into `objects`
275
- * - `[\d*]` after path part => push to array in order `\d` or push to end if `[]`
276
- *
277
-
278
- *
279
- * @example
280
- * ```ts
281
- * const formData = new FormData()
282
- * formData.append('+a', '1')
283
- * formData.append('&b', 'true')
284
- * formData.append('-c', 'null')
285
- * formData.append('d', 'foo')
286
- * parseFormData(formData, defaultTransform)
287
- * // => {a: 1, b: true, c: null, d: 'foo'}
288
- * ```
289
- *
290
- * @example
291
- * ```ts
292
- * const formData = new FormData()
293
- * formData.append('a.b', 'foo')
294
- * parseFormData(formData)
295
- * // => {a: {b: 'foo'}}
296
- * ```
297
- *
298
- * @example
299
- * ```ts
300
- * const formData = new FormData()
301
- * formData.append('a[0]', 'foo')
302
- * formData.append('a[1]', 'bar')
303
- * parseFormData(formData)
304
- * // => {a: ['foo', 'bar']}
305
- * ```
306
- *
307
- * @example
308
- * ```ts
309
- * const formData = new FormData()
310
- * formData.append('a[]', 'foo')
311
- * formData.append('a[]', 'bar')
312
- * parseFormData(formData)
313
- * // => {a: ['foo', 'bar']}
314
- * ```
315
- *
316
- * @example
317
- * ```ts
318
- * const formData = new FormData()
319
- * formData.append('a[0]', 'foo')
320
- * parseFormData(formData, {transformEntry: (path, value) => {path, value: value + 'bar'}})
321
- * // => {a: ['foobar']}
322
- * ```
323
- *
324
- * @example
325
- * ```ts
326
- * const formData = new FormData()
327
- * formData.append('a[0]', 'foo')
328
- * formData.append('a[1]', '')
329
- * parseFormData(formData, {removeEmptyString: true})
330
- * // => {a: ['foo']}
331
- * ```
332
- *
333
- * @param {Iterable<[string, string | File]>} formData - an iterator of an [`path`, `value`] tuple
334
- * - `path` := `^(\+|\-|\&)?([^\.]+?(\[\d*\])*)(\.[^\.]+?(\[\d*\])*)*$` (e.g. `+a[][1].b`)
335
- * - `value` := `string` or `File`
336
- * @param {ParseFormDataOptions} options - options for parsing the form data
337
- * - `transformEntry` - a function to transform the path and the value before
338
- * inserting the value at the path in the resulting object (default: `defaultTransform`)
339
- * - `removeEmptyString` - if `true` removes all entries where the value is an empty string
340
- * @returns {JsonObject} the parsed JSON object
341
- * @throws `DuplicateKeyError` if
342
- * - a path part is an object and the path part is already defined as an object
343
- * - a path part is an array and the path part is already defined as an array
344
- * @throws `MixedArrayError` if at a specific path part an unordered array is
345
- * defined and at a later path part an ordered array is defined or vice versa
346
- * - e.g. `a[0]` and `a[]`
347
- * - e.g. `a[]` and `a[0]`
348
- */
349
- function parseFormData(formData, _temp) {
350
- let {
351
- removeEmptyString = false,
352
- transformEntry = defaultTransform
353
- } = _temp === void 0 ? {} : _temp;
354
- const result = {};
355
-
356
- // all arrays we need to squash (in place) later
357
- const arraysWithOrder = new Set();
358
- for (const entry of Array.from(formData)) {
359
- if (removeEmptyString && entry[1] === '') continue;
360
- const {
361
- path,
362
- value
363
- } = transformEntry(entry, defaultTransform);
364
- const pathParts = extractPathParts(path);
365
- let currentPathObject = result;
366
- pathParts.forEach((pathPart, idx) => {
367
- const [nextPathValue, setNextPathValue] = handlePathPart(pathPart, currentPathObject, arraysWithOrder);
368
- if (pathParts.length - 1 === idx) {
369
- if (nextPathValue !== undefined) {
370
- throw new DuplicateKeyError(pathPart.pathToPart);
371
- }
372
- setNextPathValue(value);
373
- } else {
374
- if (nextPathValue !== undefined && !isJsonObject(nextPathValue) && !Array.isArray(nextPathValue)) {
375
- throw new DuplicateKeyError(pathPart.pathToPart);
376
- }
377
- const nextPathObject = nextPathValue ?? pathPart.default;
378
- currentPathObject = nextPathObject;
379
- setNextPathValue(nextPathObject);
380
- }
381
- });
382
- }
383
- for (const orderedArray of Array.from(arraysWithOrder)) {
384
- // replace array with a squashed array
385
- // array.flat(0) will remove all empty slots (e.g. [0, , 1] => [0, 1])
386
- orderedArray.splice(0, orderedArray.length, ...orderedArray.flat(0));
387
- }
388
- return result;
420
+ *
421
+ * Parses a FormData object to a JSON object. This is done by parsing the `name`
422
+ * attribute of each `FormDataEntryValue` and then inserting the value at the
423
+ * path. Also by default the start of the path is used to transform the value.
424
+ *
425
+ *
426
+ * In front of the whole `key`:
427
+ * - `+` => parse to `Number`
428
+ * - `-` => set value to `null`
429
+ * - `&` => parse to `Boolean`
430
+ *
431
+ * - `.` between path parts => nest into `objects`
432
+ * - `[key]` after path part => nest into `objects`, same as `.key`; the text
433
+ * in brackets is the key as it is (`a[b.c]` => key `b.c`)
434
+ * - `[\d*]` after path part => push to array in order `\d` or push to end if `[]`
435
+ *
436
+ * @example
437
+ * ```ts
438
+ * const formData = new FormData()
439
+ * formData.append('+a', '1')
440
+ * formData.append('&b', 'true')
441
+ * formData.append('-c', 'null')
442
+ * formData.append('d', 'foo')
443
+ * parseFormData(formData)
444
+ * // => {a: 1, b: true, c: null, d: 'foo'}
445
+ * ```
446
+ *
447
+ * @example
448
+ * ```ts
449
+ * const formData = new FormData()
450
+ * formData.append('a.b', 'foo')
451
+ * parseFormData(formData)
452
+ * // => {a: {b: 'foo'}}
453
+ * ```
454
+ *
455
+ * @example
456
+ * ```ts
457
+ * const formData = new FormData()
458
+ * formData.append('a[b]', 'foo')
459
+ * formData.append('a.c', 'bar')
460
+ * parseFormData(formData)
461
+ * // => {a: {b: 'foo', c: 'bar'}}
462
+ * ```
463
+ *
464
+ * @example
465
+ * ```ts
466
+ * const formData = new FormData()
467
+ * formData.append('a[0]', 'foo')
468
+ * formData.append('a[1]', 'bar')
469
+ * parseFormData(formData)
470
+ * // => {a: ['foo', 'bar']}
471
+ * ```
472
+ *
473
+ * @example
474
+ * ```ts
475
+ * const formData = new FormData()
476
+ * formData.append('a[]', 'foo')
477
+ * formData.append('a[]', 'bar')
478
+ * parseFormData(formData)
479
+ * // => {a: ['foo', 'bar']}
480
+ * ```
481
+ *
482
+ * @example
483
+ * ```ts
484
+ * const formData = new FormData()
485
+ * formData.append('a[0]', 'foo')
486
+ * parseFormData(formData, {
487
+ * transformEntry: ([path, value]) => ({
488
+ * path,
489
+ * value: typeof value === 'string' ? value + 'bar' : value,
490
+ * }),
491
+ * })
492
+ * // => {a: ['foobar']}
493
+ * ```
494
+ *
495
+ * @example
496
+ * ```ts
497
+ * const formData = new FormData()
498
+ * formData.append('a[0]', 'foo')
499
+ * formData.append('a[1]', '')
500
+ * parseFormData(formData, {removeEmptyString: true})
501
+ * // => {a: ['foo']}
502
+ * ```
503
+ *
504
+ * @param {Iterable<[string, string | File]>} formData - an iterator of an [`path`, `value`] tuple
505
+ * - `path` := an optional prefix (`+`, `-`, `&`), then path parts: keys after
506
+ * `.` or in brackets (`[key]`), and array parts `[]` or `[\d+]` (e.g.
507
+ * `+a[][1].b[c]`)
508
+ * - `value` := `string` or `File` (native or polyfilled `File` values are kept
509
+ * as they are)
510
+ * @param {ParseFormDataOptions} options - options for parsing the form data
511
+ * - `transformEntry` - a function to transform the path and the value before
512
+ * inserting the value at the path in the resulting object (default: `defaultTransform`)
513
+ * - `removeEmptyString` - if `true` removes all entries where the value is an empty string
514
+ * - `depth` - the maximum number of path parts in a path, counting every part
515
+ * (default: `Infinity`)
516
+ * @returns {JsonObject} the parsed JSON object
517
+ * @throws `TypeError` if `depth` isn't a positive integer or `Infinity`
518
+ * @throws `DepthLimitError` if a path has more path parts than `depth`
519
+ * - e.g. `a.b[0]` with `depth: 2` => `DepthLimitError('a.b[0]')`
520
+ * @throws `DuplicateKeyError` if
521
+ * - two entries set a value at the same path
522
+ * - e.g. `a` and `a`, `a[0]` and `a[0]`, `a.b` and `a[b]`
523
+ * - an entry uses a path part as an object, array or value and another entry
524
+ * uses it differently
525
+ * - e.g. `a` and `a.b`, `a.b` and `a[]`, `a[]` and `a.b`
526
+ * - the path starts with an array part
527
+ * - e.g. `[0]` or `[]`
528
+ * @throws `MixedArrayError` if at a specific path part an unordered array is
529
+ * defined and at a later path part an ordered array is defined or vice versa
530
+ * - e.g. `a[0]` and `a[]`
531
+ * - e.g. `a[]` and `a[0]`
532
+ * @throws `ForbiddenKeyError` if an object path part is `__proto__`,
533
+ * `constructor` or `prototype`
534
+ * - e.g. `__proto__.polluted`
535
+ * @throws `InvalidPathError` if an array index is larger than `4294967294`
536
+ * - e.g. `a[4294967295]`
537
+ *
538
+ * Every error's `key` is the path after `transformEntry`, not the field name
539
+ * (e.g. `+a.b.c` with `depth: 2` => `DepthLimitError('a.b.c')`).
540
+ *
541
+ * An entry with any other object path part named like a property on
542
+ * `Object.prototype` (`toString`, `hasOwnProperty`, ...) is removed from the
543
+ * result. It's parsed like any other entry first, so it causes the same errors
544
+ * as an unused name: an error doesn't tell the client which properties exist
545
+ * on the server's `Object.prototype`.
546
+ * - e.g. `user.toString` => `{}`
547
+ * - e.g. `user[toString]` => `{}`
548
+ */
549
+ function parseFormData(formData, { removeEmptyString = false, transformEntry = defaultTransform, depth = Infinity } = {}) {
550
+ if (depth !== Infinity && !(Number.isInteger(depth) && depth > 0)) throw new TypeError(`depth must be a positive integer or Infinity, got ${String(depth)}`);
551
+ const result = {};
552
+ const arraysWithOrder = /* @__PURE__ */ new Set();
553
+ const createdByRemoved = [];
554
+ for (const entry of Array.from(formData)) {
555
+ if (removeEmptyString && entry[1] === "") continue;
556
+ const { path, value } = transformEntry(entry, defaultTransform);
557
+ const pathParts = extractPathParts(path, depth);
558
+ const isRemoved = pathParts.some((pathPart) => pathPart.type === "object" && isPrototypeKey(pathPart.path));
559
+ let currentPathObject = result;
560
+ pathParts.forEach((pathPart, idx) => {
561
+ const [nextPathValue, setNextPathValue, key] = handlePathPart(pathPart, currentPathObject, arraysWithOrder);
562
+ const parent = currentPathObject;
563
+ if (pathParts.length - 1 === idx) {
564
+ if (nextPathValue !== void 0) throw new DuplicateKeyError(pathPart.pathToPart);
565
+ setNextPathValue(value);
566
+ if (isRemoved) createdByRemoved.push([
567
+ parent,
568
+ key,
569
+ void 0
570
+ ]);
571
+ } else {
572
+ if (nextPathValue !== void 0 && !isJsonObject(nextPathValue) && !Array.isArray(nextPathValue)) throw new DuplicateKeyError(pathPart.pathToPart);
573
+ const nextPathObject = nextPathValue ?? pathPart.default;
574
+ currentPathObject = nextPathObject;
575
+ setNextPathValue(nextPathObject);
576
+ if (isRemoved && nextPathValue === void 0) createdByRemoved.push([
577
+ parent,
578
+ key,
579
+ nextPathObject
580
+ ]);
581
+ }
582
+ });
583
+ }
584
+ for (let idx = createdByRemoved.length - 1; idx >= 0; idx--) {
585
+ const [parent, key, container] = createdByRemoved[idx];
586
+ if (container && Object.keys(container).length > 0) continue;
587
+ Reflect.deleteProperty(parent, key);
588
+ if (Array.isArray(parent)) arraysWithOrder.add(parent);
589
+ }
590
+ for (const orderedArray of Array.from(arraysWithOrder)) {
591
+ const values = Object.values(orderedArray);
592
+ orderedArray.length = 0;
593
+ for (const value of values) orderedArray.push(value);
594
+ }
595
+ return result;
389
596
  }
390
-
597
+ exports.DepthLimitError = DepthLimitError;
391
598
  exports.DuplicateKeyError = DuplicateKeyError;
599
+ exports.ForbiddenKeyError = ForbiddenKeyError;
600
+ exports.InvalidPathError = InvalidPathError;
392
601
  exports.MixedArrayError = MixedArrayError;
393
602
  exports.parseFormData = parseFormData;