| // Copyright 2012 the V8 project authors. All rights reserved. |
| // Redistribution and use in source and binary forms, with or without |
| // modification, are permitted provided that the following conditions are |
| // met: |
| // |
| // * Redistributions of source code must retain the above copyright |
| // notice, this list of conditions and the following disclaimer. |
| // * Redistributions in binary form must reproduce the above |
| // copyright notice, this list of conditions and the following |
| // disclaimer in the documentation and/or other materials provided |
| // with the distribution. |
| // * Neither the name of Google Inc. nor the names of its |
| // contributors may be used to endorse or promote products derived |
| // from this software without specific prior written permission. |
| // |
| // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| |
| |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| // JSON.stringify |
| |
| |
| function testStringify(expected, object) { |
| // Test fast case that bails out to slow case. |
| assertEquals(expected, JSON.stringify(object)); |
| // Test slow case. |
| assertEquals(expected, JSON.stringify(object, (key, value) => value)); |
| // Test gap. |
| assertEquals(JSON.stringify(object, null, "="), |
| JSON.stringify(object, (key, value) => value, "=")); |
| } |
| |
| |
| // Test serializing a proxy, a function proxy, and objects that contain them. |
| |
| var handler1 = { |
| get: function(target, name) { |
| return name.toUpperCase(); |
| }, |
| ownKeys: function() { |
| return ['a', 'b', 'c']; |
| }, |
| getOwnPropertyDescriptor: function() { |
| return { enumerable: true, configurable: true }; |
| } |
| } |
| |
| var proxy1 = new Proxy({}, handler1); |
| testStringify('{"a":"A","b":"B","c":"C"}', proxy1); |
| |
| var proxy_fun = new Proxy(() => {}, handler1); |
| assertTrue(typeof(proxy_fun) === 'function'); |
| testStringify(undefined, proxy_fun); |
| testStringify('[1,null]', [1, proxy_fun]); |
| |
| handler1.apply = function() { return 666; }; |
| testStringify(undefined, proxy_fun); |
| testStringify('[1,null]', [1, proxy_fun]); |
| |
| var parent1a = { b: proxy1 }; |
| testStringify('{"b":{"a":"A","b":"B","c":"C"}}', parent1a); |
| testStringify('{"b":{"a":"A","b":"B","c":"C"}}', parent1a); |
| |
| var parent1b = { a: 123, b: proxy1, c: true }; |
| testStringify('{"a":123,"b":{"a":"A","b":"B","c":"C"},"c":true}', parent1b); |
| |
| var parent1c = [123, proxy1, true]; |
| testStringify('[123,{"a":"A","b":"B","c":"C"},true]', parent1c); |
| |
| |
| // Proxy with side effect. |
| |
| var handler2 = { |
| get: function(target, name) { |
| delete parent2.c; |
| return name.toUpperCase(); |
| }, |
| ownKeys: function() { |
| return ['a', 'b', 'c']; |
| }, |
| getOwnPropertyDescriptor: function() { |
| return { enumerable: true, configurable: true }; |
| } |
| } |
| |
| var proxy2 = new Proxy({}, handler2); |
| var parent2 = { a: "delete", b: proxy2, c: "remove" }; |
| var expected2 = '{"a":"delete","b":{"a":"A","b":"B","c":"C"}}'; |
| assertEquals(expected2, JSON.stringify(parent2)); |
| parent2.c = "remove"; // Revert side effect. |
| assertEquals(expected2, JSON.stringify(parent2, undefined, 0)); |
| |
| |
| // Proxy with a get function that uses the receiver argument. |
| |
| var handler3 = { |
| get: function(target, name, receiver) { |
| if (name == 'valueOf' || name === Symbol.toPrimitive) { |
| return function() { return "proxy" }; |
| }; |
| if (typeof name !== 'symbol') return name + "(" + receiver + ")"; |
| }, |
| ownKeys: function() { |
| return ['a', 'b', 'c']; |
| }, |
| getOwnPropertyDescriptor: function() { |
| return { enumerable: true, configurable: true }; |
| } |
| } |
| |
| var proxy3 = new Proxy({}, handler3); |
| var parent3 = { x: 123, y: proxy3 } |
| testStringify('{"x":123,"y":{"a":"a(proxy)","b":"b(proxy)","c":"c(proxy)"}}', |
| parent3); |
| |
| |
| // Empty proxy. |
| |
| var handler4 = { |
| get: function(target, name) { |
| return 0; |
| }, |
| has: function() { |
| return true; |
| }, |
| getOwnPropertyDescriptor: function(target, name) { |
| return { enumerable: false }; |
| } |
| } |
| |
| var proxy4 = new Proxy({}, handler4); |
| testStringify('{}', proxy4); |
| testStringify('{"a":{}}', { a: proxy4 }); |
| |
| |
| // Proxy that provides a toJSON function that uses this. |
| |
| var handler5 = { |
| get: function(target, name) { |
| if (name == 'z') return 97000; |
| return function(key) { return key.charCodeAt(0) + this.z; }; |
| }, |
| ownKeys: function(target) { |
| return ['toJSON', 'z']; |
| }, |
| has: function() { |
| return true; |
| }, |
| getOwnPropertyDescriptor: function(target, name) { |
| return { enumerable: true }; |
| } |
| } |
| |
| var proxy5 = new Proxy({}, handler5); |
| testStringify('{"a":97097}', { a: proxy5 }); |
| |
| |
| // Proxy that provides a toJSON function that returns undefined. |
| |
| var handler6 = { |
| get: function(target, name) { |
| return function(key) { return undefined; }; |
| }, |
| ownKeys: function(target) { |
| return ['toJSON']; |
| }, |
| has: function() { |
| return true; |
| }, |
| getOwnPropertyDescriptor: function(target, name) { |
| return { enumerable: true }; |
| } |
| } |
| |
| var proxy6 = new Proxy({}, handler6); |
| testStringify('[1,null,true]', [1, proxy6, true]); |
| testStringify('{"a":1,"c":true}', {a: 1, b: proxy6, c: true}); |
| |
| |
| // Object containing a proxy that changes the parent's properties. |
| |
| var handler7 = { |
| get: function(target, name) { |
| delete parent7.a; |
| delete parent7.c; |
| parent7.e = "5"; |
| return name.toUpperCase(); |
| }, |
| ownKeys: function() { |
| return ['a', 'b', 'c']; |
| }, |
| getOwnPropertyDescriptor: function() { |
| return { enumerable: true, configurable: true }; |
| } |
| } |
| |
| var proxy7 = new Proxy({}, handler7); |
| var parent7 = { a: "1", b: proxy7, c: "3", d: "4" }; |
| assertEquals('{"a":"1","b":{"a":"A","b":"B","c":"C"},"d":"4"}', |
| JSON.stringify(parent7)); |
| assertEquals('{"b":{"a":"A","b":"B","c":"C"},"d":"4","e":"5"}', |
| JSON.stringify(parent7)); |
| |
| |
| // (Proxy handler to log trap calls) |
| |
| var log = []; |
| var logger = {}; |
| var handler = new Proxy({}, logger); |
| |
| logger.get = function(t, trap, r) { |
| return function() { |
| log.push([trap, ...arguments]); |
| return Reflect[trap](...arguments); |
| } |
| }; |
| |
| |
| // Object is a callable proxy |
| |
| log.length = 0; |
| var target = () => 42; |
| var proxy = new Proxy(target, handler); |
| assertTrue(typeof proxy === 'function'); |
| |
| assertEquals(undefined, JSON.stringify(proxy)); |
| assertEquals(1, log.length) |
| for (var i in log) assertSame(target, log[i][1]); |
| |
| assertEquals(["get", target, "toJSON", proxy], log[0]); |
| |
| |
| // Object is a non-callable non-arraylike proxy |
| |
| log.length = 0; |
| var target = {foo: 42} |
| var proxy = new Proxy(target, handler); |
| assertFalse(Array.isArray(proxy)); |
| |
| assertEquals('{"foo":42}', JSON.stringify(proxy)); |
| assertEquals(4, log.length) |
| for (var i in log) assertSame(target, log[i][1]); |
| |
| assertEquals( |
| ["get", target, "toJSON", proxy], log[0]); |
| assertEquals( |
| ["ownKeys", target], log[1]); // EnumerableOwnNames |
| assertEquals( |
| ["getOwnPropertyDescriptor", target, "foo"], log[2]); // EnumerableOwnNames |
| assertEquals( |
| ["get", target, "foo", proxy], log[3]); |
| |
| |
| // Object is an arraylike proxy |
| |
| log.length = 0; |
| var target = [42]; |
| var proxy = new Proxy(target, handler); |
| assertTrue(Array.isArray(proxy)); |
| |
| assertEquals('[42]', JSON.stringify(proxy)); |
| assertEquals(3, log.length) |
| for (var i in log) assertSame(target, log[i][1]); |
| |
| assertEquals(["get", target, "toJSON", proxy], log[0]); |
| assertEquals(["get", target, "length", proxy], log[1]); |
| assertEquals(["get", target, "0", proxy], log[2]); |
| |
| |
| // Replacer is a callable proxy |
| |
| log.length = 0; |
| var object = {0: "foo", 1: 666}; |
| var target = (key, val) => key == "1" ? val + 42 : val; |
| var proxy = new Proxy(target, handler); |
| assertTrue(typeof proxy === 'function'); |
| |
| assertEquals('{"0":"foo","1":708}', JSON.stringify(object, proxy)); |
| assertEquals(3, log.length) |
| for (var i in log) assertSame(target, log[i][1]); |
| |
| assertEquals(4, log[0].length) |
| assertEquals("apply", log[0][0]); |
| assertEquals("", log[0][3][0]); |
| assertEquals({0: "foo", 1: 666}, log[0][3][1]); |
| assertEquals(4, log[1].length) |
| assertEquals("apply", log[1][0]); |
| assertEquals(["0", "foo"], log[1][3]); |
| assertEquals(4, log[2].length) |
| assertEquals("apply", log[2][0]); |
| assertEquals(["1", 666], log[2][3]); |
| |
| |
| // Replacer is an arraylike proxy |
| |
| log.length = 0; |
| var object = {0: "foo", 1: 666}; |
| var target = [0]; |
| var proxy = new Proxy(target, handler); |
| assertTrue(Array.isArray(proxy)); |
| |
| assertEquals('{"0":"foo"}', JSON.stringify(object, proxy)); |
| assertEquals(2, log.length) |
| for (var i in log) assertSame(target, log[i][1]); |
| |
| assertEquals(["get", target, "length", proxy], log[0]); |
| assertEquals(["get", target, "0", proxy], log[1]); |
| |
| |
| // Replacer is an arraylike proxy and object is an array |
| |
| log.length = 0; |
| var object = ["foo", 42]; |
| var target = [0]; |
| var proxy = new Proxy(target, handler); |
| assertTrue(Array.isArray(proxy)); |
| |
| assertEquals('["foo",42]', JSON.stringify(object, proxy)); |
| assertEquals(2, log.length); |
| for (var i in log) assertSame(target, log[i][1]); |
| |
| assertEquals(["get", target, "length", proxy], log[0]); |
| assertEquals(["get", target, "0", proxy], log[1]); |
| |
| |
| // Replacer is an arraylike proxy with a non-trivial length |
| |
| var getTrap = function(t, key) { |
| if (key === "length") return {[Symbol.toPrimitive]() {return 42}}; |
| if (key === "41") return "foo"; |
| if (key === "42") return "bar"; |
| }; |
| var target = []; |
| var proxy = new Proxy(target, {get: getTrap}); |
| assertTrue(Array.isArray(proxy)); |
| var object = {foo: true, bar: 666}; |
| assertEquals('{"foo":true}', JSON.stringify(object, proxy)); |
| |
| |
| // Replacer is an arraylike proxy with a bogus length |
| |
| var getTrap = function(t, key) { |
| if (key === "length") return Symbol(); |
| if (key === "41") return "foo"; |
| if (key === "42") return "bar"; |
| }; |
| var target = []; |
| var proxy = new Proxy(target, {get: getTrap}); |
| assertTrue(Array.isArray(proxy)); |
| var object = {foo: true, bar: 666}; |
| assertThrows(() => JSON.stringify(object, proxy), TypeError); |
| |
| |
| // Replacer returns a non-callable non-arraylike proxy |
| |
| log.length = 0; |
| var object = ["foo", 42]; |
| var target = {baz: 5}; |
| var proxy = new Proxy(target, handler); |
| var replacer = (key, val) => key === "1" ? proxy : val; |
| |
| assertEquals('["foo",{"baz":5}]', JSON.stringify(object, replacer)); |
| assertEquals(3, log.length); |
| for (var i in log) assertSame(target, log[i][1]); |
| |
| assertEquals(["ownKeys", target], log[0]); |
| assertEquals(["getOwnPropertyDescriptor", target, "baz"], log[1]); |
| |
| |
| // Replacer returns an arraylike proxy |
| |
| log.length = 0; |
| var object = ["foo", 42]; |
| var target = ["bar"]; |
| var proxy = new Proxy(target, handler); |
| var replacer = (key, val) => key === "1" ? proxy : val; |
| |
| assertEquals('["foo",["bar"]]', JSON.stringify(object, replacer)); |
| assertEquals(2, log.length); |
| for (var i in log) assertSame(target, log[i][1]); |
| |
| assertEquals(["get", target, "length", proxy], log[0]); |
| assertEquals(["get", target, "0", proxy], log[1]); |
| |
| |
| // Replacer returns an arraylike proxy with a non-trivial length |
| |
| var getTrap = function(t, key) { |
| if (key === "length") return {[Symbol.toPrimitive]() {return 3}}; |
| if (key === "2") return "baz"; |
| if (key === "3") return "bar"; |
| }; |
| var target = []; |
| var proxy = new Proxy(target, {get: getTrap}); |
| var replacer = (key, val) => key === "goo" ? proxy : val; |
| var object = {foo: true, goo: false}; |
| assertEquals('{"foo":true,"goo":[null,null,"baz"]}', |
| JSON.stringify(object, replacer)); |
| |
| |
| // Replacer returns an arraylike proxy with a bogus length |
| |
| var getTrap = function(t, key) { |
| if (key === "length") return Symbol(); |
| if (key === "2") return "baz"; |
| if (key === "3") return "bar"; |
| }; |
| var target = []; |
| var proxy = new Proxy(target, {get: getTrap}); |
| var replacer = (key, val) => key === "goo" ? proxy : val; |
| var object = {foo: true, goo: false}; |
| assertThrows(() => JSON.stringify(object, replacer), TypeError); |
| |
| |
| // Replacer returns a callable proxy |
| |
| log.length = 0; |
| var target = () => 666; |
| var proxy = new Proxy(target, handler); |
| var replacer = (key, val) => key === "1" ? proxy : val; |
| |
| assertEquals('["foo",null]', JSON.stringify(["foo", 42], replacer)); |
| assertEquals(0, log.length); |
| |
| assertEquals('{"0":"foo"}', JSON.stringify({0: "foo", 1: 42}, replacer)); |
| assertEquals(0, log.length); |
| |
| |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| // JSON.parse |
| |
| |
| // Reviver is a callable proxy |
| |
| log.length = 0; |
| var target = () => 42; |
| var proxy = new Proxy(target, handler); |
| assertTrue(typeof proxy === "function"); |
| |
| assertEquals(42, JSON.parse("[true, false]", proxy)); |
| assertEquals(3, log.length); |
| for (var i in log) assertSame(target, log[i][1]); |
| |
| assertEquals(4, log[0].length); |
| assertEquals("apply", log[0][0]); |
| assertEquals(["0", true], log[0][3]); |
| assertEquals(4, log[1].length); |
| assertEquals("apply", log[1][0]); |
| assertEquals(["1", false], log[1][3]); |
| assertEquals(4, log[2].length); |
| assertEquals("apply", log[2][0]); |
| assertEquals(["", [42, 42]], log[2][3]); |
| |
| |
| // Reviver plants a non-arraylike proxy into a yet-to-be-visited property |
| |
| log.length = 0; |
| var target = {baz: 42}; |
| var proxy = new Proxy(target, handler); |
| var reviver = function(p, v) { |
| if (p === "baz") return 5; |
| if (p === "foo") this.bar = proxy; |
| return v; |
| } |
| |
| assertEquals({foo: 0, bar: proxy}, JSON.parse('{"foo":0,"bar":1}', reviver)); |
| assertEquals(4, log.length); |
| for (var i in log) assertSame(target, log[i][1]); |
| |
| assertEquals(["ownKeys", target], log[0]); |
| assertEquals(["getOwnPropertyDescriptor", target, "baz"], log[1]); |
| assertEquals(["get", target, "baz", proxy], log[2]); |
| assertEquals(["defineProperty", target, "baz", |
| {value: 5, configurable: true, writable: true, enumerable: true}], log[3]); |
| |
| |
| // Reviver plants an arraylike proxy into a yet-to-be-visited property |
| |
| log.length = 0; |
| var target = [42]; |
| var proxy = new Proxy(target, handler); |
| assertTrue(Array.isArray(proxy)); |
| var reviver = function(p, v) { |
| if (p === "0") return undefined; |
| if (p === "foo") this.bar = proxy; |
| return v; |
| } |
| |
| var result = JSON.parse('{"foo":0,"bar":1}', reviver); |
| assertEquals({foo: 0, bar: proxy}, result); |
| assertSame(result.bar, proxy); |
| assertEquals(3, log.length); |
| for (var i in log) assertSame(target, log[i][1]); |
| |
| assertEquals(["get", target, "length", proxy], log[0]); |
| assertEquals(["get", target, "0", proxy], log[1]); |
| assertEquals(["deleteProperty", target, "0"], log[2]); |
| |
| proxy = new Proxy([], { |
| get: function(target, property) { |
| if (property == "length") return 7; |
| return 0; |
| }, |
| }); |
| assertEquals('[[0,0,0,0,0,0,0]]', JSON.stringify([proxy])); |
| |
| proxy = new Proxy([], { |
| get: function(target, property) { |
| if (property == "length") return 1E40; |
| return 0; |
| }, |
| }); |
| assertThrows(() => JSON.stringify([proxy]), RangeError); |
| |
| log = []; |
| proxy = new Proxy({}, { |
| ownKeys: function() { |
| log.push("ownKeys"); |
| return ["0", "a", "b"]; |
| }, |
| get: function(target, property) { |
| log.push("get " + property); |
| return property.toUpperCase(); |
| }, |
| getOwnPropertyDescriptor: function(target, property) { |
| log.push("descriptor " + property); |
| return {enumerable: true, configurable: true}; |
| }, |
| isExtensible: assertUnreachable, |
| has: assertUnreachable, |
| getPrototypeOf: assertUnreachable, |
| setPrototypeOf: assertUnreachable, |
| preventExtensions: assertUnreachable, |
| setPrototypeOf: assertUnreachable, |
| defineProperty: assertUnreachable, |
| set: assertUnreachable, |
| deleteProperty: assertUnreachable, |
| apply: assertUnreachable, |
| construct: assertUnreachable, |
| }); |
| |
| assertEquals('[{"0":"0","a":"A","b":"B"}]', JSON.stringify([proxy])); |
| assertEquals(['get toJSON', |
| 'ownKeys', |
| 'descriptor 0', |
| 'descriptor a', |
| 'descriptor b', |
| 'get 0', |
| 'get a', |
| 'get b'], log); |