From 94854a1dfafdf7ecf168a60593240ecce13e7634 Mon Sep 17 00:00:00 2001
From: dong0713 <417851790@qq.com>
Date: Mon, 20 Jul 2026 17:42:02 +0800
Subject: [PATCH] Fix XML.unescape rejecting valid whitespace numeric character
references
XML.mustEscape() is shared by both XML.escape() (serialization) and
XMLTokener.unescapeEntity() (deserialization). While the method's Javadoc
and comment quote the W3C XML 1.0 valid-character range
(#x9 | #xA | #xD | [#x20-#xD7FF] | ...), the implementation only checked
[#x20-#xD7FF] in its range clause, omitting #x9/#xA/#xD.
Although the ISO-control clause excluded those three codepoints, the
negated range clause still marked them as 'must escape', so unescapeEntity()
threw JSONException for the XML-allowed control characters TAB, LF and CR.
This broke XML.unescape("
") and XML.toJSONObject("
"),
both of which worked in v20251224 and regressed after #1045 (v20260522).
Align the range clause with the W3C spec by explicitly allowing #x9, #xA
and #xD, matching the comment that was already documented.
Fixes #1059
---
src/main/java/org/json/XML.java | 10 ++++--
src/test/java/org/json/junit/XMLTest.java | 37 +++++++++++++++++++++++
2 files changed, 45 insertions(+), 2 deletions(-)
diff --git a/src/main/java/org/json/XML.java b/src/main/java/org/json/XML.java
index 32475876c..f5846cc1e 100644
--- a/src/main/java/org/json/XML.java
+++ b/src/main/java/org/json/XML.java
@@ -172,8 +172,14 @@ static boolean mustEscape(int cp) {
&& cp != 0xA
&& cp != 0xD
) || !(
- // valid the range of acceptable characters that aren't control
- (cp >= 0x20 && cp <= 0xD7FF)
+ // Valid character range per W3C XML 1.0 spec:
+ // #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]
+ // Previously omitted #x9/#xA/#xD, causing unescape("
") etc.
+ // to reject valid LF/TAB/CR as illegal (see #1059)
+ cp == 0x9
+ || cp == 0xA
+ || cp == 0xD
+ || (cp >= 0x20 && cp <= 0xD7FF)
|| (cp >= 0xE000 && cp <= 0xFFFD)
|| (cp >= 0x10000 && cp <= 0x10FFFF)
)
diff --git a/src/test/java/org/json/junit/XMLTest.java b/src/test/java/org/json/junit/XMLTest.java
index 589536fd2..44fdcccb4 100644
--- a/src/test/java/org/json/junit/XMLTest.java
+++ b/src/test/java/org/json/junit/XMLTest.java
@@ -1537,6 +1537,43 @@ public void testValidUppercaseHexEntity() {
assertEquals("A", jsonObject.getString("a"));
}
+ /**
+ * Tests that valid XML numeric character references for whitespace
+ * control characters (TAB, LF, CR) are correctly unescaped. These
+ * codepoints are explicitly allowed by the XML 1.0 spec
+ * (https://www.w3.org/TR/REC-xml/#charsets) but were previously rejected
+ * as invalid. See issue #1059.
+ */
+ @Test
+ public void testValidWhitespaceNumericEntityUnescape() {
+ // decimal references for the three allowed control characters
+ assertEquals("\t", XML.unescape(" "));
+ assertEquals("\n", XML.unescape("
"));
+ assertEquals("\r", XML.unescape("
"));
+ // hex references for the same codepoints
+ assertEquals("\t", XML.unescape(" "));
+ assertEquals("\n", XML.unescape("
"));
+ assertEquals("\r", XML.unescape("
"));
+ }
+
+ /**
+ * Tests that {@code XML.toJSONObject} accepts numeric character references
+ * for the XML-allowed control characters (TAB, LF, CR) without throwing.
+ * Regression test for #1059, where {@code XML.toJSONObject("
")}
+ * threw JSONException in versions after 20251224.
+ */
+ @Test
+ public void testValidWhitespaceNumericEntityToJSONObject() {
+ // LF reference should round-trip through toJSONObject without throwing
+ JSONObject jsonObject = XML.toJSONObject("
");
+ // the value is the LF character (possibly trimmed by the JSON path,
+ // but the call must not throw)
+ assertTrue(jsonObject.has("a"));
+ // TAB and CR references also accepted
+ XML.toJSONObject(" ");
+ XML.toJSONObject("
");
+ }
+
}