Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

devalue

Package Overview
Dependencies
Maintainers
2
Versions
45
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

devalue - npm Package Compare versions

Comparing version
5.6.2
to
5.6.3
+63
src/utils.test.js
import * as assert from 'uvu/assert';
import { suite } from 'uvu';
import { valid_array_indices } from './utils.js';
const test = suite('valid_array_indices');
test('returns all indices for a normal dense array', () => {
const arr = ['a', 'b', 'c'];
assert.equal(valid_array_indices(arr), ['0', '1', '2']);
});
test('returns empty array for an empty array', () => {
assert.equal(valid_array_indices([]), []);
});
test('returns populated indices for a sparse array', () => {
const arr = [, 'b', ,];
assert.equal(valid_array_indices(arr), ['1']);
});
test('strips non-numeric properties from a dense array', () => {
const arr = ['a', 'b'];
arr.foo = 'x';
arr.bar = 42;
assert.equal(valid_array_indices(arr), ['0', '1']);
});
test('strips non-numeric properties from a very sparse array', () => {
const arr = [];
arr[1_000_000] = 'x';
arr.foo = 'should be ignored';
assert.equal(valid_array_indices(arr), ['1000000']);
});
test('returns empty array when only non-numeric properties exist', () => {
const arr = [];
arr.foo = 'x';
arr.bar = 42;
assert.equal(valid_array_indices(arr), []);
});
test('handles multiple non-numeric properties after indices', () => {
const arr = [1, 2, 3];
arr.a = 'x';
arr.b = 'y';
arr.c = 'z';
assert.equal(valid_array_indices(arr), ['0', '1', '2']);
});
test('handles a single-element array with non-numeric property', () => {
const arr = ['only'];
arr.extra = true;
assert.equal(valid_array_indices(arr), ['0']);
});
test('handles array properties pretending to be indices', () => {
const arr = ['a', 'b'];
arr[-1] = 'negative index';
arr[2**32 - 1] = 'too large index';
assert.equal(valid_array_indices(arr), ['0', '1']);
})
test.run();
+2
-2
{
"name": "devalue",
"description": "Gets the job done when JSON.stringify can't",
"version": "5.6.2",
"version": "5.6.3",
"repository": "sveltejs/devalue",

@@ -35,4 +35,4 @@ "sideEffects": false,

"build": "dts-buddy",
"test": "uvu test"
"test": "uvu"
}
}

@@ -7,1 +7,2 @@ export const UNDEFINED = -1;

export const NEGATIVE_ZERO = -6;
export const SPARSE = -7;

@@ -8,2 +8,3 @@ import { decode64 } from './base64.js';

POSITIVE_INFINITY,
SPARSE,
UNDEFINED

@@ -205,2 +206,13 @@ } from './constants.js';

}
} 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 {

@@ -222,3 +234,3 @@ const array = new Array(value.length);

for (const key in value) {
for (const key of Object.keys(value)) {
if (key === '__proto__') {

@@ -225,0 +237,0 @@ throw new Error('Cannot parse an object with a `__proto__` property');

@@ -8,3 +8,4 @@ import {

stringify_key,
stringify_string
stringify_string,
valid_array_indices
} from './utils.js';

@@ -17,2 +18,3 @@ import {

POSITIVE_INFINITY,
SPARSE,
UNDEFINED

@@ -110,3 +112,12 @@ } from './constants.js';

case 'Array':
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;
str = '[';

@@ -117,8 +128,63 @@

if (i in thing) {
if (Object.hasOwn(thing, i)) {
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 {
str += HOLE;
// 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;
}
}

@@ -130,2 +196,3 @@ }

break;
}

@@ -225,3 +292,12 @@ case 'Set':

str = '["null"';
for (const key in thing) {
for (const key of Object.keys(thing)) {
if (key === '__proto__') {
throw new DevalueError(
`Cannot stringify objects with __proto__ keys`,
keys,
thing,
value
);
}
keys.push(stringify_key(key));

@@ -235,3 +311,12 @@ str += `,${stringify_string(key)},${flatten(thing[key])}`;

let started = false;
for (const key in thing) {
for (const key of Object.keys(thing)) {
if (key === '__proto__') {
throw new DevalueError(
`Cannot stringify objects with __proto__ keys`,
keys,
thing,
value
);
}
if (started) str += ',';

@@ -238,0 +323,0 @@ started = true;

@@ -9,3 +9,4 @@ import {

stringify_key,
stringify_string
stringify_string,
valid_array_indices
} from './utils.js';

@@ -135,3 +136,12 @@

for (const key in thing) {
for (const key of Object.keys(thing)) {
if (key === '__proto__') {
throw new DevalueError(
`Cannot stringify objects with __proto__ keys`,
keys,
thing,
value
);
}
keys.push(stringify_key(key));

@@ -195,8 +205,81 @@ walk(thing[key]);

case 'Array':
const members = /** @type {any[]} */ (thing).map((v, i) =>
i in thing ? stringify(v) : ''
);
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)
}
const tail = thing.length === 0 || thing.length - 1 in thing ? '' : ',';
return `[${members.join(',')}${tail}]`;
return result + tail + ']';
}

@@ -203,0 +286,0 @@ case 'Set':

@@ -119,1 +119,31 @@ /** @type {Record<string, string>} */

}
/** @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;
}

@@ -23,4 +23,4 @@ {

],
"mappings": ";;;;;iBAqBgBA,MAAMA;;;;;iBCGNC,SAASA;cCXZC,YAAYA;;;;;;;;;;;;;;iBCETC,KAAKA;;;;;iBASLC,SAASA",
"mappings": ";;;;;iBAsBgBA,MAAMA;;;;;iBCINC,SAASA;cCbZC,YAAYA;;;;;;;;;;;;;;iBCGTC,KAAKA;;;;;iBASLC,SAASA",
"ignoreList": []
}