|
6 | 6 | is_plain_object, |
7 | 7 | is_primitive, |
8 | 8 | stringify_key, |
9 | | - stringify_string |
| 9 | + stringify_string, |
| 10 | + valid_array_indices |
10 | 11 | } from './utils.js'; |
11 | 12 |
|
12 | 13 | const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_$'; |
@@ -199,12 +200,85 @@ export function uneval(value, replacer) { |
199 | 200 | case 'URLSearchParams': |
200 | 201 | return `new URLSearchParams(${stringify_string(thing.toString())})`; |
201 | 202 |
|
202 | | - case 'Array': |
203 | | - const members = /** @type {any[]} */ (thing).map((v, i) => |
204 | | - i in thing ? stringify(v) : '' |
205 | | - ); |
| 203 | + case 'Array': { |
| 204 | + // For dense arrays (no holes), we iterate normally. |
| 205 | + // When we encounter the first hole, we call Object.keys |
| 206 | + // to determine the sparseness, then decide between: |
| 207 | + // - Array literal with holes: [,"a",,] (default) |
| 208 | + // - Object.assign: Object.assign(Array(n),{...}) (for very sparse arrays) |
| 209 | + // Only the Object.assign path avoids iterating every slot, which |
| 210 | + // is what protects against the DoS of e.g. `arr[1000000] = 1`. |
| 211 | + let has_holes = false; |
| 212 | + |
| 213 | + let result = '['; |
| 214 | + |
| 215 | + for (let i = 0; i < thing.length; i += 1) { |
| 216 | + if (i > 0) result += ','; |
| 217 | + |
| 218 | + if (Object.hasOwn(thing, i)) { |
| 219 | + result += stringify(thing[i]); |
| 220 | + } else if (!has_holes) { |
| 221 | + // Decide between array literal and Object.assign. |
| 222 | + // |
| 223 | + // Array literal: holes are consecutive commas. |
| 224 | + // For example, [, "a", ,] is written as [,"a",,]. |
| 225 | + // Each hole costs 1 char (a comma). |
| 226 | + // |
| 227 | + // Object.assign: populated indices are listed explicitly. |
| 228 | + // For example, [, "a", ,] would be written as |
| 229 | + // Object.assign(Array(3),{1:"a"}). This avoids paying |
| 230 | + // per-hole, but has a large fixed overhead for the |
| 231 | + // "Object.assign(Array(n),{...})" wrapper, and each |
| 232 | + // element costs extra chars for its index and colon. |
| 233 | + // |
| 234 | + // The serialized values are the same size either way, so |
| 235 | + // the choice comes down to the structural overhead: |
| 236 | + // |
| 237 | + // Array literal overhead: |
| 238 | + // 1 char per element or hole (comma separators) |
| 239 | + // + 2 chars for "[" and "]" |
| 240 | + // = L + 2 |
| 241 | + // |
| 242 | + // Object.assign overhead: |
| 243 | + // "Object.assign(Array(" — 20 chars |
| 244 | + // + length — d chars |
| 245 | + // + "),{" — 3 chars |
| 246 | + // + for each populated element: |
| 247 | + // index + ":" + "," — (d + 2) chars |
| 248 | + // + "})" — 2 chars |
| 249 | + // = (25 + d) + P * (d + 2) |
| 250 | + // |
| 251 | + // where L is the array length, P is the number of |
| 252 | + // populated elements, and d is the number of digits |
| 253 | + // in L (an upper bound on the digits in any index). |
| 254 | + // |
| 255 | + // Object.assign is cheaper when: |
| 256 | + // (25 + d) + P * (d + 2) < L + 2 |
| 257 | + const populated_keys = valid_array_indices(/** @type {any[]} */ (thing)); |
| 258 | + const population = populated_keys.length; |
| 259 | + const d = String(thing.length).length; |
| 260 | + |
| 261 | + const hole_cost = thing.length + 2; |
| 262 | + const sparse_cost = (25 + d) + population * (d + 2); |
| 263 | + |
| 264 | + if (hole_cost > sparse_cost) { |
| 265 | + const entries = populated_keys |
| 266 | + .map((k) => `${k}:${stringify(thing[k])}`) |
| 267 | + .join(','); |
| 268 | + return `Object.assign(Array(${thing.length}),{${entries}})`; |
| 269 | + } |
| 270 | + |
| 271 | + // Re-process this index as a hole in the array literal |
| 272 | + has_holes = true; |
| 273 | + i -= 1; |
| 274 | + } |
| 275 | + // else: already decided on array literal, hole is just an empty slot |
| 276 | + // (the comma separator is all we need — no content for this position) |
| 277 | + } |
| 278 | + |
206 | 279 | const tail = thing.length === 0 || thing.length - 1 in thing ? '' : ','; |
207 | | - return `[${members.join(',')}${tail}]`; |
| 280 | + return result + tail + ']'; |
| 281 | + } |
208 | 282 |
|
209 | 283 | case 'Set': |
210 | 284 | case 'Map': |
|
0 commit comments