Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions JSTests/stress/json-stringify-array-prototype-to-json.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
function shouldBe(actual, expected)
{
if (actual !== expected)
throw new Error('bad value: ' + actual);
}

// An array whose prototype was replaced can reach a toJSON through the chain, and
// SerializeJSONProperty finds it with GetV. The fast stringifier only knows about the
// global Array.prototype, so it has to check the array's own prototype.
{
let prototype = { __proto__: Array.prototype, toJSON() { return 'replaced'; } };
let array = [1, 2, 3];
Object.setPrototypeOf(array, prototype);
shouldBe(JSON.stringify(array), '"replaced"');
shouldBe(JSON.stringify({ array }), '{"array":"replaced"}');
shouldBe(JSON.stringify([array]), '["replaced"]');
shouldBe(JSON.stringify(array, null, 2), '"replaced"');
}

{
let prototype = Object.create(Array.prototype);
Object.defineProperty(prototype, 'toJSON', { value() { return this.length; }, enumerable: false });
let array = [1, 2];
Object.setPrototypeOf(array, prototype);
shouldBe(JSON.stringify(array), '2');
}

// A plain array keeps taking the fast path.
{
shouldBe(JSON.stringify([1, 2, 3]), '[1,2,3]');
shouldBe(JSON.stringify(Object.setPrototypeOf([1, 2, 3], Array.prototype)), '[1,2,3]');
let arrayWithProperty = [1];
arrayWithProperty.extra = 2;
shouldBe(JSON.stringify(arrayWithProperty), '[1]');
}

// An array with no prototype has no toJSON to find.
{
let array = [1, 2];
Object.setPrototypeOf(array, null);
shouldBe(JSON.stringify(array), '[1,2]');
}

// An own toJSON still wins over the prototype's.
{
let prototype = { __proto__: Array.prototype, toJSON() { return 'prototype'; } };
let array = [1];
Object.setPrototypeOf(array, prototype);
Object.defineProperty(array, 'toJSON', { value() { return 'own'; }, enumerable: false });
shouldBe(JSON.stringify(array), '"own"');
}

// The fast path must agree with the general stringifier, which an identity callable
// replacer forces us onto.
{
let prototype = { __proto__: Array.prototype, toJSON() { return { length: this.length }; } };
let values = [
Object.setPrototypeOf([1, 2, 3], prototype),
Object.setPrototypeOf(['string'], prototype),
[Object.setPrototypeOf([1], prototype)],
{ nested: Object.setPrototypeOf([1], prototype) },
Object.setPrototypeOf([], prototype),
];
for (let value of values) {
for (let space of [undefined, 2, '\t']) {
shouldBe(JSON.stringify(value, null, space), JSON.stringify(value, (key, x) => x, space));
}
}
}
69 changes: 69 additions & 0 deletions JSTests/stress/json-stringify-non-enumerable-to-json.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
function shouldBe(actual, expected)
{
if (actual !== expected)
throw new Error('bad value: ' + actual);
}

// SerializeJSONProperty looks up toJSON with GetV, so an own toJSON is called
// regardless of its enumerability. The fast stringifier skips DontEnum
// properties, so it has to check for toJSON before doing that.
function withNonEnumerableToJSON(object, toJSON)
{
Object.defineProperty(object, 'toJSON', { value: toJSON, enumerable: false, writable: false, configurable: false });
return object;
}

{
let object = withNonEnumerableToJSON({ uri: 'u', cid: 'c', text: 't' }, function () { return { uri: this.uri, cid: this.cid }; });
Object.freeze(object);
shouldBe(JSON.stringify(object), '{"uri":"u","cid":"c"}');
shouldBe(JSON.stringify({ record: object }), '{"record":{"uri":"u","cid":"c"}}');
shouldBe(JSON.stringify([object]), '[{"uri":"u","cid":"c"}]');
shouldBe(JSON.stringify(object, null, 2), '{\n "uri": "u",\n "cid": "c"\n}');
}

{
shouldBe(JSON.stringify(withNonEnumerableToJSON({ a: 1 }, function () { return 'replaced'; })), '"replaced"');
shouldBe(JSON.stringify(withNonEnumerableToJSON({ a: 1 }, function () { return undefined; })), undefined);
shouldBe(JSON.stringify(withNonEnumerableToJSON({ a: 1 }, function (key) { return key; })), '""');
shouldBe(JSON.stringify({ key: withNonEnumerableToJSON({ a: 1 }, function (key) { return key; }) }), '{"key":"key"}');
}

// The fast stringifier bails out on the DynamicBuffer path too.
{
let array = [];
for (let i = 0; i < 2000; ++i)
array.push(withNonEnumerableToJSON({ index: i, name: 'name' + i }, function () { return this.index; }));
let expected = '[' + array.map((_, i) => i).join(',') + ']';
shouldBe(JSON.stringify(array), expected);
}

// An own toJSON that is not callable is serialized as a normal property.
{
shouldBe(JSON.stringify(withNonEnumerableToJSON({ a: 1 }, 42)), '{"a":1}');
shouldBe(JSON.stringify({ a: 1, toJSON: 42 }), '{"a":1,"toJSON":42}');
}

// An enumerable own toJSON keeps working.
{
shouldBe(JSON.stringify({ a: 1, toJSON() { return 'enumerable'; } }), '"enumerable"');
shouldBe(JSON.stringify(Object.freeze({ a: 1, toJSON() { return 'frozen'; } })), '"frozen"');
}

// The fast path must agree with the general stringifier, which an identity
// callable replacer forces us onto.
{
let values = [
withNonEnumerableToJSON({ a: 1 }, function () { return { b: 2 }; }),
withNonEnumerableToJSON({ a: 1 }, function () { return 'string'; }),
withNonEnumerableToJSON({ a: 1 }, 'not callable'),
{ nested: withNonEnumerableToJSON({ a: 1 }, function () { return [1, 2]; }) },
[withNonEnumerableToJSON({ a: 1 }, function () { return null; })],
withNonEnumerableToJSON({ 'キー': '値' }, function () { return this['キー']; }),
];
for (let value of values) {
for (let space of [undefined, 2, '\t']) {
shouldBe(JSON.stringify(value, null, space), JSON.stringify(value, (key, x) => x, space));
}
}
}
16 changes: 16 additions & 0 deletions Source/JavaScriptCore/runtime/JSONObject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1424,6 +1424,12 @@ void FastStringifier<CharType, bufferMode>::append(JSValue value)
++m_depth;
const unsigned newLineAndIndent = hasGap == HasGap::Yes ? newLineAndIndentSize() : 0;
structure.forEachProperty(m_vm, [&](const auto& entry) -> bool {
// SerializeJSONProperty looks up toJSON with GetV, so an own toJSON applies even when
// it is not enumerable. It has to be found before DontEnum properties are skipped.
if (entry.key() == m_vm.propertyNames->toJSON) [[unlikely]] {
recordFailure("object has toJSON"_s);
return false;
}
Comment thread
robobun marked this conversation as resolved.
Resolved
if (entry.attributes() & PropertyAttribute::DontEnum)
return true;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
auto& name = *entry.key();
Expand Down Expand Up @@ -1538,6 +1544,16 @@ void FastStringifier<CharType, bufferMode>::append(JSValue value)
}
auto& structure = *array.structure();
if (!m_globalObject.isOriginalArrayStructure(&structure)) [[unlikely]] {
// An original array structure has Array.prototype, which mayHaveToJSON checked above.
// Any other prototype can carry a toJSON, so it has to go to the general stringifier.
if (structure.hasPolyProto()) [[unlikely]] {
recordFailure("hasPolyProto"_s);
return;
}
if (structure.storedPrototype() != m_globalObject.arrayPrototype()) [[unlikely]] {
recordFailure("non-standard array prototype"_s);
return;
}
structure.forEachProperty(m_vm, [&](const PropertyTableEntry& entry) -> bool {
if (entry.key() == m_vm.propertyNames->toJSON) [[unlikely]] {
recordFailure("array has toJSON"_s);
Expand Down
Loading