From f3c83d83495f5f86c2f88a30f909e7e2dbbd974b Mon Sep 17 00:00:00 2001 From: boulea7 Date: Fri, 2 Oct 2026 18:18:04 +0800 Subject: [PATCH] Fix JSONML text double decoding with keepStrings --- src/main/java/org/json/JSONML.java | 2 +- src/test/java/org/json/junit/JSONMLTest.java | 92 ++++++++++++++++++++ 2 files changed, 93 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/json/JSONML.java b/src/main/java/org/json/JSONML.java index 6ec997061..5caf733f6 100644 --- a/src/main/java/org/json/JSONML.java +++ b/src/main/java/org/json/JSONML.java @@ -274,7 +274,7 @@ private static Object parse( if (token instanceof String) { String strToken = (String) token; if (config.isKeepStrings()) { - value = XML.unescape(strToken); + value = strToken; } else { value = XML.stringToValue(strToken); } diff --git a/src/test/java/org/json/junit/JSONMLTest.java b/src/test/java/org/json/junit/JSONMLTest.java index 93a6821d8..9c6d9bc05 100644 --- a/src/test/java/org/json/junit/JSONMLTest.java +++ b/src/test/java/org/json/junit/JSONMLTest.java @@ -724,6 +724,98 @@ public void testToJSONArray_reversibility() { assertNotEquals(revertedXml, originalXml); } + /** + * Entity-looking text is decoded once, including when strings are kept. + */ + @Test + public void shouldDecodeEntitiesOnceWhenKeepingStringsInJSONArray() { + final String[][] cases = { + {"&lt;", "<"}, + {"&amp;", "&"}, + {"&#65;", "A"}, + {"&lt;", "<"}, + {"<>&"'AB", "<>&\"'AB"} + }; + for (String[] testCase : cases) { + final String xml = "

" + testCase[0] + "

"; + final JSONArray json = JSONML.toJSONArray(xml, true); + assertEquals(xml, testCase[1], json.getJSONObject(1).get("title")); + assertEquals(xml, testCase[1], json.get(2)); + assertEquals(xml, testCase[1], JSONML.toJSONArray(xml, false).get(2)); + assertTrue(xml, json.similar(JSONML.toJSONArray(new XMLTokener(JSONML.toString(json)), + JSONMLParserConfiguration.KEEP_STRINGS))); + } + } + + /** + * Object form preserves the same decoded attribute and text values. + */ + @Test + public void shouldDecodeEntitiesOnceWhenKeepingStringsInJSONObject() { + final String xml = "

&lt;

"; + final JSONObject json = JSONML.toJSONObject(xml, + JSONMLParserConfiguration.ORIGINAL.withKeepStrings(true)); + assertEquals("<", json.get("title")); + assertEquals("<", json.getJSONArray("childNodes").get(0)); + assertEquals("<", JSONML.toJSONObject(xml).getJSONArray("childNodes").get(0)); + assertTrue(json.similar(JSONML.toJSONObject(new XMLTokener(JSONML.toString(json)), true))); + } + + /** + * Keeping strings disables type conversion without disabling entity decoding. + */ + @Test + public void shouldPreserveValueTypesWhenKeepingStrings() { + final String[] values = {"42", "true", "false", "null"}; + final String[] content = {"42", "true", "false", "null"}; + final Object[] typedValues = {Integer.valueOf(42), Boolean.TRUE, Boolean.FALSE, JSONObject.NULL}; + for (int i = 0; i < values.length; i++) { + final String xml = "

" + content[i] + "

"; + final JSONArray array = JSONML.toJSONArray(new XMLTokener(xml), true); + final JSONObject object = JSONML.toJSONObject(xml, true); + assertEquals(values[i], array.getJSONObject(1).get("value")); + assertEquals(values[i], array.get(2)); + assertEquals(values[i], object.get("value")); + assertEquals(values[i], object.getJSONArray("childNodes").get(0)); + final JSONArray typedArray = JSONML.toJSONArray(xml); + final JSONObject typedObject = JSONML.toJSONObject(xml); + assertEquals(typedValues[i], typedArray.getJSONObject(1).get("value")); + assertEquals(typedValues[i], typedArray.get(2)); + assertEquals(typedValues[i], typedObject.get("value")); + assertEquals(typedValues[i], typedObject.getJSONArray("childNodes").get(0)); + } + } + + /** + * CDATA entity syntax remains literal content. + */ + @Test + public void shouldPreserveCdataWhenKeepingStrings() { + final String content = "< A & 42 true null"; + final String xml = "

"; + assertEquals(content, JSONML.toJSONArray(xml, true).get(1)); + assertEquals(content, JSONML.toJSONObject(xml, true).getJSONArray("childNodes").get(0)); + } + + /** + * Escaped invalid character references are text, while raw references remain invalid. + */ + @Test + public void shouldPreserveEscapedInvalidCharacterReferences() { + final String[] references = {"#0", "#x110000"}; + for (String reference : references) { + final String expected = "&" + reference + ";"; + final String xml = "

&" + reference + ";

"; + assertEquals(expected, JSONML.toJSONArray(xml, true).get(1)); + assertEquals(expected, JSONML.toJSONObject(xml, true).getJSONArray("childNodes").get(0)); + assertEquals(expected, JSONML.toJSONArray(xml).get(1)); + assertEquals(expected, JSONML.toJSONObject(xml).getJSONArray("childNodes").get(0)); + final String invalidXml = "

" + expected + "

"; + assertThrows(JSONException.class, () -> JSONML.toJSONArray(invalidXml, true)); + assertThrows(JSONException.class, () -> JSONML.toJSONObject(invalidXml, true)); + } + } + /** * JSON string cannot be reverted to original xml when type guessing is used. * When we force all the values as string, the original text comes back.