From 8e8c27c5873fb2084934fdca3e0726bcd5086092 Mon Sep 17 00:00:00 2001 From: Tony Germano Date: Thu, 29 Apr 2021 22:53:05 -0400 Subject: [PATCH] fix for stringify array replacer mozilla/rhino#725 - Per spec, Numbers in an array replacer should be converted to strings prior to adding to the property list - Duplicates should be removed during construction of the property list - Changed some variables declared as List to Collection instead because that is sufficient for how they are being used - Moved List to array conversion from happening in every iteration of the jo method to being called once in the stringify method - Convert strings items representing integers to their Integer value for correct property lookup --- src/org/mozilla/javascript/NativeJSON.java | 38 +++++++++++++------ .../json-stringify-array-replacer.jstest | 28 ++++++++++++++ 2 files changed, 54 insertions(+), 12 deletions(-) create mode 100644 testsrc/jstests/json-stringify-array-replacer.jstest diff --git a/src/org/mozilla/javascript/NativeJSON.java b/src/org/mozilla/javascript/NativeJSON.java index 4ce2fc5131..de02b8055b 100644 --- a/src/org/mozilla/javascript/NativeJSON.java +++ b/src/org/mozilla/javascript/NativeJSON.java @@ -11,8 +11,8 @@ import java.util.Arrays; import java.util.Collection; import java.util.Iterator; +import java.util.LinkedHashSet; import java.util.LinkedList; -import java.util.List; import java.util.Map; import java.util.Stack; import org.mozilla.javascript.json.JsonParser; @@ -200,7 +200,7 @@ private static class StringifyState { String indent, String gap, Callable replacer, - List propertyList) { + Object[] propertyList) { this.cx = cx; this.scope = scope; @@ -214,7 +214,7 @@ private static class StringifyState { String indent; String gap; Callable replacer; - List propertyList; + Object[] propertyList; Context cx; Scriptable scope; @@ -225,22 +225,36 @@ public static Object stringify( String indent = ""; String gap = ""; - List propertyList = null; + Object[] propertyList = null; Callable replacerFunction = null; if (replacer instanceof Callable) { replacerFunction = (Callable) replacer; } else if (replacer instanceof NativeArray) { - propertyList = new LinkedList(); + LinkedHashSet propertySet = new LinkedHashSet(); NativeArray replacerArray = (NativeArray) replacer; for (int i : replacerArray.getIndexIds()) { Object v = replacerArray.get(i, replacerArray); - if (v instanceof String || v instanceof Number) { - propertyList.add(v); - } else if (v instanceof NativeString || v instanceof NativeNumber) { - propertyList.add(ScriptRuntime.toString(v)); + if (v instanceof String) { + propertySet.add(v); + } else if (v instanceof Number + || v instanceof NativeString + || v instanceof NativeNumber) { + // TODO: This should also apply to subclasses of NativeString and NativeNumber + // once the class, extends, and super keywords are implemented + propertySet.add(ScriptRuntime.toString(v)); } } + // After items have been converted to strings and duplicates removed, transform to an + // array and convert indexed keys back to Integers as required for later processing + propertyList = new Object[propertySet.size()]; + int i = 0; + for (Object prop : propertySet) { + ScriptRuntime.StringIdOrIndex idOrIndex = ScriptRuntime.toStringIdOrIndex(cx, prop); + // This will always be a String or Integer + propertyList[i++] = + (idOrIndex.stringId == null) ? idOrIndex.index : idOrIndex.stringId; + } } if (space instanceof NativeNumber) { @@ -413,12 +427,12 @@ private static String jo(Scriptable value, StringifyState state) { state.indent = state.indent + state.gap; Object[] k = null; if (state.propertyList != null) { - k = state.propertyList.toArray(); + k = state.propertyList; } else { k = value.getIds(); } - List partial = new LinkedList(); + Collection partial = new LinkedList(); for (Object p : k) { Object strP = str(p, value, state); @@ -463,7 +477,7 @@ private static String ja(Scriptable value, StringifyState state) { String stepback = state.indent; state.indent = state.indent + state.gap; - List partial = new LinkedList(); + Collection partial = new LinkedList(); if (unwrapped != null) { Object[] elements = null; diff --git a/testsrc/jstests/json-stringify-array-replacer.jstest b/testsrc/jstests/json-stringify-array-replacer.jstest new file mode 100644 index 0000000000..f16bad8840 --- /dev/null +++ b/testsrc/jstests/json-stringify-array-replacer.jstest @@ -0,0 +1,28 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +// This test is a stand-in for the following until they are run by the Test262SuiteTest. +// https://github.com/tc39/test262/blob/70bc32edab22b44db9d671ce505db8842ae200b6/test/built-ins/JSON/stringify/replacer-array-duplicates.js +// https://github.com/tc39/test262/blob/70bc32edab22b44db9d671ce505db8842ae200b6/test/built-ins/JSON/stringify/replacer-array-number.js + + +function assertEqual(actual, expected) { + if (actual !== expected) throw 'expected: <' + expected + '> but found <' + actual + '>'; +} + +var obj = {"0":1, "1":2, "2":3, "name":4}; +var actual = JSON.stringify(obj, ["0", "1", "name", "name"]); +var expected = JSON.stringify({"0":1, "1":2, "name":4}); +assertEqual(expected, actual); + +var obj = {"2":"b","3":"c","1":"a"}; +var expected = JSON.stringify({"1":"a","2":"b","3":"c"}); + +var actual = JSON.stringify(obj, ["1","2","3"]); +assertEqual(expected, actual); + +var actual = JSON.stringify(obj, [1,2,3]); +assertEqual(expected, actual); + +"success"