complete
This commit is contained in:
-1
@@ -4,4 +4,3 @@ export const NAN = -3;
|
||||
export const POSITIVE_INFINITY = -4;
|
||||
export const NEGATIVE_INFINITY = -5;
|
||||
export const NEGATIVE_ZERO = -6;
|
||||
export const SPARSE = -7;
|
||||
|
||||
+1
-13
@@ -5,7 +5,6 @@ import {
|
||||
NEGATIVE_INFINITY,
|
||||
NEGATIVE_ZERO,
|
||||
POSITIVE_INFINITY,
|
||||
SPARSE,
|
||||
UNDEFINED
|
||||
} from './constants.js';
|
||||
|
||||
@@ -202,17 +201,6 @@ export function unflatten(parsed, revivers) {
|
||||
default:
|
||||
throw new Error(`Unknown type ${type}`);
|
||||
}
|
||||
} else if (value[0] === SPARSE) {
|
||||
// Sparse array encoding: [SPARSE, length, idx, val, idx, val, ...]
|
||||
const len = value[1];
|
||||
|
||||
const array = new Array(len);
|
||||
hydrated[index] = array;
|
||||
|
||||
for (let i = 2; i < value.length; i += 2) {
|
||||
const idx = value[i];
|
||||
array[idx] = hydrate(value[i + 1]);
|
||||
}
|
||||
} else {
|
||||
const array = new Array(value.length);
|
||||
hydrated[index] = array;
|
||||
@@ -229,7 +217,7 @@ export function unflatten(parsed, revivers) {
|
||||
const object = {};
|
||||
hydrated[index] = object;
|
||||
|
||||
for (const key of Object.keys(value)) {
|
||||
for (const key in value) {
|
||||
if (key === '__proto__') {
|
||||
throw new Error('Cannot parse an object with a `__proto__` property');
|
||||
}
|
||||
|
||||
+6
-91
@@ -5,8 +5,7 @@ import {
|
||||
is_plain_object,
|
||||
is_primitive,
|
||||
stringify_key,
|
||||
stringify_string,
|
||||
valid_array_indices
|
||||
stringify_string
|
||||
} from './utils.js';
|
||||
import {
|
||||
HOLE,
|
||||
@@ -14,7 +13,6 @@ import {
|
||||
NEGATIVE_INFINITY,
|
||||
NEGATIVE_ZERO,
|
||||
POSITIVE_INFINITY,
|
||||
SPARSE,
|
||||
UNDEFINED
|
||||
} from './constants.js';
|
||||
import { encode64 } from './base64.js';
|
||||
@@ -107,89 +105,24 @@ export function stringify(value, reducers) {
|
||||
: `["RegExp",${stringify_string(source)}]`;
|
||||
break;
|
||||
|
||||
case 'Array': {
|
||||
// For dense arrays (no holes), we iterate normally.
|
||||
// When we encounter the first hole, we call Object.keys
|
||||
// to determine the sparseness, then decide between:
|
||||
// - HOLE encoding: [-2, val, -2, ...] (default)
|
||||
// - Sparse encoding: [-7, length, idx, val, ...] (for very sparse arrays)
|
||||
// Only the sparse path avoids iterating every slot, which
|
||||
// is what protects against the DoS of e.g. `arr[1000000] = 1`.
|
||||
let mostly_dense = false;
|
||||
|
||||
case 'Array':
|
||||
str = '[';
|
||||
|
||||
for (let i = 0; i < thing.length; i += 1) {
|
||||
if (i > 0) str += ',';
|
||||
|
||||
if (Object.hasOwn(thing, i)) {
|
||||
if (i in thing) {
|
||||
keys.push(`[${i}]`);
|
||||
str += flatten(thing[i]);
|
||||
keys.pop();
|
||||
} else if (mostly_dense) {
|
||||
// Use dense encoding. The heuristic guarantees the
|
||||
// array is only mildly sparse, so iterating over every
|
||||
// slot is fine.
|
||||
str += HOLE;
|
||||
} else {
|
||||
// Decide between HOLE encoding and sparse encoding.
|
||||
//
|
||||
// HOLE encoding: each hole is serialized as the HOLE
|
||||
// sentinel (-2). For example, [, "a", ,] becomes
|
||||
// [-2, 0, -2]. Each hole costs 3 chars ("-2" + comma).
|
||||
//
|
||||
// Sparse encoding: lists only populated indices.
|
||||
// For example, [, "a", ,] becomes [-7, 3, 1, 0] — the
|
||||
// -7 sentinel, the array length (3), then index-value
|
||||
// pairs. This avoids paying per-hole, but each element
|
||||
// costs extra chars to write its index.
|
||||
//
|
||||
// The values are the same size either way, so the
|
||||
// choice comes down to structural overhead:
|
||||
//
|
||||
// HOLE overhead:
|
||||
// 3 chars per hole ("-2" + comma)
|
||||
// = (L - P) * 3
|
||||
//
|
||||
// Sparse overhead:
|
||||
// "-7," — 3 chars (sparse sentinel + comma)
|
||||
// + length + "," — (d + 1) chars (array length + comma)
|
||||
// + per element: index + "," — (d + 1) chars
|
||||
// = (4 + d) + P * (d + 1)
|
||||
//
|
||||
// where L is the array length, P is the number of
|
||||
// populated elements, and d is the number of digits
|
||||
// in L (an upper bound on the digits in any index).
|
||||
//
|
||||
// Sparse encoding is cheaper when:
|
||||
// (4 + d) + P * (d + 1) < (L - P) * 3
|
||||
const populated_keys = valid_array_indices(/** @type {any[]} */ (thing));
|
||||
const population = populated_keys.length;
|
||||
const d = String(thing.length).length;
|
||||
|
||||
const hole_cost = (thing.length - population) * 3;
|
||||
const sparse_cost = 4 + d + population * (d + 1);
|
||||
|
||||
if (hole_cost > sparse_cost) {
|
||||
str = '[' + SPARSE + ',' + thing.length;
|
||||
for (let j = 0; j < populated_keys.length; j++) {
|
||||
const key = populated_keys[j];
|
||||
keys.push(`[${key}]`);
|
||||
str += ',' + key + ',' + flatten(thing[key]);
|
||||
keys.pop();
|
||||
}
|
||||
break;
|
||||
} else {
|
||||
mostly_dense = true;
|
||||
str += HOLE;
|
||||
}
|
||||
str += HOLE;
|
||||
}
|
||||
}
|
||||
|
||||
str += ']';
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'Set':
|
||||
str = '["Set"';
|
||||
@@ -284,16 +217,7 @@ export function stringify(value, reducers) {
|
||||
|
||||
if (Object.getPrototypeOf(thing) === null) {
|
||||
str = '["null"';
|
||||
for (const key of Object.keys(thing)) {
|
||||
if (key === '__proto__') {
|
||||
throw new DevalueError(
|
||||
`Cannot stringify objects with __proto__ keys`,
|
||||
keys,
|
||||
thing,
|
||||
value
|
||||
);
|
||||
}
|
||||
|
||||
for (const key in thing) {
|
||||
keys.push(stringify_key(key));
|
||||
str += `,${stringify_string(key)},${flatten(thing[key])}`;
|
||||
keys.pop();
|
||||
@@ -302,16 +226,7 @@ export function stringify(value, reducers) {
|
||||
} else {
|
||||
str = '{';
|
||||
let started = false;
|
||||
for (const key of Object.keys(thing)) {
|
||||
if (key === '__proto__') {
|
||||
throw new DevalueError(
|
||||
`Cannot stringify objects with __proto__ keys`,
|
||||
keys,
|
||||
thing,
|
||||
value
|
||||
);
|
||||
}
|
||||
|
||||
for (const key in thing) {
|
||||
if (started) str += ',';
|
||||
started = true;
|
||||
keys.push(stringify_key(key));
|
||||
|
||||
+7
-90
@@ -6,8 +6,7 @@ import {
|
||||
is_plain_object,
|
||||
is_primitive,
|
||||
stringify_key,
|
||||
stringify_string,
|
||||
valid_array_indices
|
||||
stringify_string
|
||||
} from './utils.js';
|
||||
|
||||
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_$';
|
||||
@@ -132,16 +131,7 @@ export function uneval(value, replacer) {
|
||||
);
|
||||
}
|
||||
|
||||
for (const key of Object.keys(thing)) {
|
||||
if (key === '__proto__') {
|
||||
throw new DevalueError(
|
||||
`Cannot stringify objects with __proto__ keys`,
|
||||
keys,
|
||||
thing,
|
||||
value
|
||||
);
|
||||
}
|
||||
|
||||
for (const key in thing) {
|
||||
keys.push(stringify_key(key));
|
||||
walk(thing[key]);
|
||||
keys.pop();
|
||||
@@ -200,85 +190,12 @@ export function uneval(value, replacer) {
|
||||
case 'URLSearchParams':
|
||||
return `new URLSearchParams(${stringify_string(thing.toString())})`;
|
||||
|
||||
case 'Array': {
|
||||
// For dense arrays (no holes), we iterate normally.
|
||||
// When we encounter the first hole, we call Object.keys
|
||||
// to determine the sparseness, then decide between:
|
||||
// - Array literal with holes: [,"a",,] (default)
|
||||
// - Object.assign: Object.assign(Array(n),{...}) (for very sparse arrays)
|
||||
// Only the Object.assign path avoids iterating every slot, which
|
||||
// is what protects against the DoS of e.g. `arr[1000000] = 1`.
|
||||
let has_holes = false;
|
||||
|
||||
let result = '[';
|
||||
|
||||
for (let i = 0; i < thing.length; i += 1) {
|
||||
if (i > 0) result += ',';
|
||||
|
||||
if (Object.hasOwn(thing, i)) {
|
||||
result += stringify(thing[i]);
|
||||
} else if (!has_holes) {
|
||||
// Decide between array literal and Object.assign.
|
||||
//
|
||||
// Array literal: holes are consecutive commas.
|
||||
// For example, [, "a", ,] is written as [,"a",,].
|
||||
// Each hole costs 1 char (a comma).
|
||||
//
|
||||
// Object.assign: populated indices are listed explicitly.
|
||||
// For example, [, "a", ,] would be written as
|
||||
// Object.assign(Array(3),{1:"a"}). This avoids paying
|
||||
// per-hole, but has a large fixed overhead for the
|
||||
// "Object.assign(Array(n),{...})" wrapper, and each
|
||||
// element costs extra chars for its index and colon.
|
||||
//
|
||||
// The serialized values are the same size either way, so
|
||||
// the choice comes down to the structural overhead:
|
||||
//
|
||||
// Array literal overhead:
|
||||
// 1 char per element or hole (comma separators)
|
||||
// + 2 chars for "[" and "]"
|
||||
// = L + 2
|
||||
//
|
||||
// Object.assign overhead:
|
||||
// "Object.assign(Array(" — 20 chars
|
||||
// + length — d chars
|
||||
// + "),{" — 3 chars
|
||||
// + for each populated element:
|
||||
// index + ":" + "," — (d + 2) chars
|
||||
// + "})" — 2 chars
|
||||
// = (25 + d) + P * (d + 2)
|
||||
//
|
||||
// where L is the array length, P is the number of
|
||||
// populated elements, and d is the number of digits
|
||||
// in L (an upper bound on the digits in any index).
|
||||
//
|
||||
// Object.assign is cheaper when:
|
||||
// (25 + d) + P * (d + 2) < L + 2
|
||||
const populated_keys = valid_array_indices(/** @type {any[]} */ (thing));
|
||||
const population = populated_keys.length;
|
||||
const d = String(thing.length).length;
|
||||
|
||||
const hole_cost = thing.length + 2;
|
||||
const sparse_cost = (25 + d) + population * (d + 2);
|
||||
|
||||
if (hole_cost > sparse_cost) {
|
||||
const entries = populated_keys
|
||||
.map((k) => `${k}:${stringify(thing[k])}`)
|
||||
.join(',');
|
||||
return `Object.assign(Array(${thing.length}),{${entries}})`;
|
||||
}
|
||||
|
||||
// Re-process this index as a hole in the array literal
|
||||
has_holes = true;
|
||||
i -= 1;
|
||||
}
|
||||
// else: already decided on array literal, hole is just an empty slot
|
||||
// (the comma separator is all we need — no content for this position)
|
||||
}
|
||||
|
||||
case 'Array':
|
||||
const members = /** @type {any[]} */ (thing).map((v, i) =>
|
||||
i in thing ? stringify(v) : ''
|
||||
);
|
||||
const tail = thing.length === 0 || thing.length - 1 in thing ? '' : ',';
|
||||
return result + tail + ']';
|
||||
}
|
||||
return `[${members.join(',')}${tail}]`;
|
||||
|
||||
case 'Set':
|
||||
case 'Map':
|
||||
|
||||
-30
@@ -116,33 +116,3 @@ const is_identifier = /^[a-zA-Z_$][a-zA-Z_$0-9]*$/;
|
||||
export function stringify_key(key) {
|
||||
return is_identifier.test(key) ? '.' + key : '[' + JSON.stringify(key) + ']';
|
||||
}
|
||||
|
||||
/** @param {string} s */
|
||||
function is_valid_array_index(s) {
|
||||
if (s.length === 0) return false;
|
||||
if (s.length > 1 && s.charCodeAt(0) === 48) return false; // leading zero
|
||||
for (let i = 0; i < s.length; i++) {
|
||||
const c = s.charCodeAt(i);
|
||||
if (c < 48 || c > 57) return false;
|
||||
}
|
||||
// by this point we know it's a string of digits, but it has to be within the range of valid array indices
|
||||
const n = +s;
|
||||
if (n >= 2 ** 32 - 1) return false;
|
||||
if (n < 0) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the populated indices of an array.
|
||||
* @param {unknown[]} array
|
||||
*/
|
||||
export function valid_array_indices(array) {
|
||||
const keys = Object.keys(array);
|
||||
for (var i = keys.length - 1; i >= 0; i--) {
|
||||
if (is_valid_array_index(keys[i])) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
keys.length = i + 1;
|
||||
return keys;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user