You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by pa...@apache.org on 2026/08/04 00:44:45 UTC

(groovy) branch master updated (9e72f5fe8b -> 3fd4b4b661)

This is an automated email from the ASF dual-hosted git repository.

paulk-asert pushed a change to branch master
in repository https://gitbox.apache.org/repos/asf/groovy.git


    from 9e72f5fe8b GROOVY-12230: STC: String/GString conditional not accepted where String is expected
     new fd2b826cfb GROOVY-12231: validate the four hex digits of a json \u escape
     new 3fd4b4b661 GROOVY-12231: validate the four hex digits of a json \u escape (tweaks)

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../org/apache/groovy/json/internal/CharBuf.java   | 19 +++--
 .../json/JsonSlurperMalformedStringTest.groovy     | 82 ++++++++++++++++++++++
 2 files changed, 96 insertions(+), 5 deletions(-)


(groovy) 01/02: GROOVY-12231: validate the four hex digits of a json \u escape

Posted by pa...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

paulk-asert pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/groovy.git

commit fd2b826cfbb751406ba316bb4d31fa010574213b
Author: Kartik Kenchi <ne...@gmail.com>
AuthorDate: Mon Aug 3 20:08:23 2026 +0530

    GROOVY-12231: validate the four hex digits of a json \u escape
---
 .../org/apache/groovy/json/internal/CharBuf.java   | 19 +++--
 .../JsonSlurperMalformedUnicodeEscapeTest.groovy   | 98 ++++++++++++++++++++++
 2 files changed, 112 insertions(+), 5 deletions(-)

diff --git a/subprojects/groovy-json/src/main/java/org/apache/groovy/json/internal/CharBuf.java b/subprojects/groovy-json/src/main/java/org/apache/groovy/json/internal/CharBuf.java
index 2e6cf7e2f7..3bc3eb1adb 100644
--- a/subprojects/groovy-json/src/main/java/org/apache/groovy/json/internal/CharBuf.java
+++ b/subprojects/groovy-json/src/main/java/org/apache/groovy/json/internal/CharBuf.java
@@ -906,12 +906,14 @@ public class CharBuf extends Writer implements CharSequence {
                             break;
 
                         case 'u':
-                            if (index + 4 < to) {
-                                String hex = new String(chars, index + 1, 4);
-                                char unicode = (char) Integer.parseInt(hex, 16);
-                                buffer[location++] = unicode;
-                                index += 4;
+                            if (index + 4 >= to) {
+                                throw new JsonException("Unable to decode string");
                             }
+                            buffer[location++] = (char) (hexToNibble(chars[index + 1]) << 12
+                                    | hexToNibble(chars[index + 2]) << 8
+                                    | hexToNibble(chars[index + 3]) << 4
+                                    | hexToNibble(chars[index + 4]));
+                            index += 4;
                             break;
 
                         default:
@@ -930,4 +932,11 @@ public class CharBuf extends Writer implements CharSequence {
 
         return this;
     }
+
+    private static int hexToNibble(final char c) {
+        if (c >= '0' && c <= '9') return c - '0';
+        if (c >= 'A' && c <= 'F') return c - 'A' + 10;
+        if (c >= 'a' && c <= 'f') return c - 'a' + 10;
+        throw new JsonException("Unable to decode string");
+    }
 }
diff --git a/subprojects/groovy-json/src/test/groovy/groovy/json/JsonSlurperMalformedUnicodeEscapeTest.groovy b/subprojects/groovy-json/src/test/groovy/groovy/json/JsonSlurperMalformedUnicodeEscapeTest.groovy
new file mode 100644
index 0000000000..bfc18c1581
--- /dev/null
+++ b/subprojects/groovy-json/src/test/groovy/groovy/json/JsonSlurperMalformedUnicodeEscapeTest.groovy
@@ -0,0 +1,98 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package groovy.json
+
+import org.junit.jupiter.api.Test
+
+import static groovy.test.GroovyAssert.shouldFail
+import static org.junit.jupiter.api.Assertions.assertEquals
+
+/**
+ * RFC 8259 requires a {@code \\u} escape to carry exactly four hexadecimal digits, and
+ * {@link JsonTokenType#STRING} validates exactly that for the lexer, so {@link JsonSlurperClassic}
+ * rejects a malformed escape. {@link org.apache.groovy.json.internal.CharBuf#decodeJsonString}, the
+ * decoder shared by every {@link JsonParserType}, did not: it read the digits with
+ * {@code Integer.parseInt(hex, 16)}, which also accepts a leading sign and non-ASCII Unicode digits,
+ * and skipped the escape altogether when fewer than four characters remained. So the same document
+ * that the lexer refused was accepted by {@link JsonSlurper} and decoded to text the document never
+ * specified: {@code "\\u12"} to the literal {@code 12}, {@code "\\u+041"} to {@code A}.
+ */
+class JsonSlurperMalformedUnicodeEscapeTest {
+
+    // The Arabic-Indic digits for 1234; Integer.parseInt accepts these as hex, a JSON parser must not.
+    private static final String ARABIC_INDIC_DIGITS = '١٢٣٤'
+
+    private static final Map<String, String> MALFORMED = [
+            'truncated by the closing quote': '{"k":"\\u12"}',
+            'no digits at all'              : '{"k":"\\u"}',
+            'leading plus sign'             : '{"k":"\\u+041"}',
+            'leading minus sign'            : '{"k":"\\u-001"}',
+            'non-ASCII digits'              : '{"k":"\\u' + ARABIC_INDIC_DIGITS + '"}',
+            'not hexadecimal at all'        : '{"k":"\\uZZZZ"}',
+    ]
+
+    @Test
+    void testEveryParserTypeRejectsMalformedUnicodeEscape() {
+        JsonParserType.values().each { type ->
+            MALFORMED.each { description, doc ->
+                shouldFail(JsonException) {
+                    // toJson forces the lazy overlay parsers to materialize (decode) their values.
+                    JsonOutput.toJson(new JsonSlurper().setType(type).parseText(doc))
+                }
+            }
+        }
+    }
+
+    @Test
+    void testClassicSlurperRejectsMalformedUnicodeEscape() {
+        MALFORMED.each { description, doc ->
+            shouldFail(RuntimeException) {
+                new JsonSlurperClassic().parseText(doc)
+            }
+        }
+    }
+
+    @Test
+    void testValidUnicodeEscapesStillDecodeForEveryParserType() {
+        JsonParserType.values().each { type ->
+            def slurper = new JsonSlurper().setType(type)
+            assertValidEscapes("parser type $type") { String doc -> slurper.parseText(doc).k }
+        }
+    }
+
+    @Test
+    void testValidUnicodeEscapesStillDecodeForClassicSlurper() {
+        def slurper = new JsonSlurperClassic()
+        assertValidEscapes('JsonSlurperClassic') { String doc -> slurper.parseText(doc).k }
+    }
+
+    private static void assertValidEscapes(String context, Closure<String> parse) {
+        assertEquals('A', parse('{"k":"\\u0041"}'), "basic escape for $context")
+        assertEquals(0xE9, (int) parse('{"k":"\\u00e9"}').charAt(0), "lower-case digits for $context")
+        assertEquals(0xE9, (int) parse('{"k":"\\u00E9"}').charAt(0), "upper-case digits for $context")
+        assertEquals(0x0000, (int) parse('{"k":"\\u0000"}').charAt(0), "lowest code unit for $context")
+        assertEquals(0xFFFF, (int) parse('{"k":"\\uffff"}').charAt(0), "highest code unit for $context")
+
+        // a surrogate pair is two escapes, and ordinary text may follow the fourth digit
+        String pair = parse('{"k":"\\ud83d\\ude00ef"}')
+        assertEquals(4, pair.length(), "pair length for $context")
+        assertEquals(0x1F600, pair.codePointAt(0), "pair code point for $context")
+        assertEquals('ef', pair.substring(2), "text after escape for $context")
+    }
+}


(groovy) 02/02: GROOVY-12231: validate the four hex digits of a json \u escape (tweaks)

Posted by pa...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

paulk-asert pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/groovy.git

commit 3fd4b4b66166426026de0cae300f297df4d4be4b
Author: Paul King <pa...@asert.com.au>
AuthorDate: Tue Aug 4 09:33:07 2026 +1000

    GROOVY-12231: validate the four hex digits of a json \u escape (tweaks)
---
 .../json/JsonSlurperMalformedStringTest.groovy     | 82 ++++++++++++++++++
 .../JsonSlurperMalformedUnicodeEscapeTest.groovy   | 98 ----------------------
 2 files changed, 82 insertions(+), 98 deletions(-)

diff --git a/subprojects/groovy-json/src/test/groovy/groovy/json/JsonSlurperMalformedStringTest.groovy b/subprojects/groovy-json/src/test/groovy/groovy/json/JsonSlurperMalformedStringTest.groovy
index 1434da5dba..5dc44817b4 100644
--- a/subprojects/groovy-json/src/test/groovy/groovy/json/JsonSlurperMalformedStringTest.groovy
+++ b/subprojects/groovy-json/src/test/groovy/groovy/json/JsonSlurperMalformedStringTest.groovy
@@ -31,12 +31,33 @@ import static org.junit.jupiter.api.Assertions.assertEquals
  * {@link JsonSlurper} that surfaced as a raw {@link ArrayIndexOutOfBoundsException} instead of the
  * documented {@link JsonException}, so a caller guarding untrusted input with {@code catch
  * (JsonException)} would not catch it.
+ * <p>
+ * The same decoder was also lenient about the four hexadecimal digits that RFC 8259 requires a
+ * {@code \\u} escape to carry. {@link JsonTokenType#STRING} validates exactly that for the lexer, so
+ * {@link JsonSlurperClassic} rejects a malformed escape; the decoder shared by every
+ * {@link JsonParserType} did not. It read the digits with {@code Integer.parseInt(hex, 16)}, which
+ * also accepts a leading sign and non-ASCII Unicode digits, and skipped the escape altogether when
+ * fewer than four characters remained. So the same document that the lexer refused was accepted by
+ * {@link JsonSlurper} and decoded to text the document never specified: {@code "\\u12"} to the
+ * literal {@code 12}, {@code "\\u+041"} to {@code A}.
  */
 class JsonSlurperMalformedStringTest {
 
     // Each ends in a lone (odd) trailing backslash with no closing quote.
     private static final List<String> DANGLING = ['"\\', '{"a":"x\\']
 
+    // The Arabic-Indic digits for 1234; Integer.parseInt accepts these as hex, a JSON parser must not.
+    private static final String ARABIC_INDIC_DIGITS = '١٢٣٤'
+
+    private static final Map<String, String> MALFORMED_ESCAPE = [
+            'truncated by the closing quote': '{"k":"\\u12"}',
+            'no digits at all'              : '{"k":"\\u"}',
+            'leading plus sign'             : '{"k":"\\u+041"}',
+            'leading minus sign'            : '{"k":"\\u-001"}',
+            'non-ASCII digits'              : '{"k":"\\u' + ARABIC_INDIC_DIGITS + '"}',
+            'not hexadecimal at all'        : '{"k":"\\uZZZZ"}',
+    ]
+
     @Test
     void testDefaultParserRejectsDanglingEscape() {
         // The default (CHAR_BUFFER) parser decodes strings eagerly, so this used to throw
@@ -76,4 +97,65 @@ class JsonSlurperMalformedStringTest {
             assertEquals('A', slurper.parseText('{"k":"\\u0041"}').k, "unicode escape for $type")
         }
     }
+
+    @Test
+    void testEveryParserTypeRejectsMalformedUnicodeEscape() {
+        JsonParserType.values().each { type ->
+            MALFORMED_ESCAPE.each { description, doc ->
+                assertRejected("parser type $type", description) {
+                    // toJson forces the lazy overlay parsers to materialize (decode) their values.
+                    JsonOutput.toJson(new JsonSlurper().setType(type).parseText(doc))
+                }
+            }
+        }
+    }
+
+    @Test
+    void testClassicSlurperRejectsMalformedUnicodeEscape() {
+        MALFORMED_ESCAPE.each { description, doc ->
+            assertRejected('JsonSlurperClassic', description) {
+                new JsonSlurperClassic().parseText(doc)
+            }
+        }
+    }
+
+    @Test
+    void testValidUnicodeEscapesStillDecodeForEveryParserType() {
+        JsonParserType.values().each { type ->
+            def slurper = new JsonSlurper().setType(type)
+            assertValidUnicodeEscapes("parser type $type") { String doc -> slurper.parseText(doc).k }
+        }
+    }
+
+    @Test
+    void testValidUnicodeEscapesStillDecodeForClassicSlurper() {
+        def slurper = new JsonSlurperClassic()
+        assertValidUnicodeEscapes('JsonSlurperClassic') { String doc -> slurper.parseText(doc).k }
+    }
+
+    private static void assertRejected(String context, String description, Closure parse) {
+        def thrown = null
+        try {
+            parse()
+        } catch (Throwable t) {
+            thrown = t
+        }
+        assert thrown instanceof JsonException,
+                "$context must reject a \\u escape $description, but got " +
+                        (thrown == null ? 'no failure' : thrown.getClass().name)
+    }
+
+    private static void assertValidUnicodeEscapes(String context, Closure<String> parse) {
+        assertEquals('A', parse('{"k":"\\u0041"}'), "basic escape for $context")
+        assertEquals(0xE9, (int) parse('{"k":"\\u00e9"}').charAt(0), "lower-case digits for $context")
+        assertEquals(0xE9, (int) parse('{"k":"\\u00E9"}').charAt(0), "upper-case digits for $context")
+        assertEquals(0x0000, (int) parse('{"k":"\\u0000"}').charAt(0), "lowest code unit for $context")
+        assertEquals(0xFFFF, (int) parse('{"k":"\\uffff"}').charAt(0), "highest code unit for $context")
+
+        // a surrogate pair is two escapes, and ordinary text may follow the fourth digit
+        String pair = parse('{"k":"\\ud83d\\ude00ef"}')
+        assertEquals(4, pair.length(), "pair length for $context")
+        assertEquals(0x1F600, pair.codePointAt(0), "pair code point for $context")
+        assertEquals('ef', pair.substring(2), "text after escape for $context")
+    }
 }
diff --git a/subprojects/groovy-json/src/test/groovy/groovy/json/JsonSlurperMalformedUnicodeEscapeTest.groovy b/subprojects/groovy-json/src/test/groovy/groovy/json/JsonSlurperMalformedUnicodeEscapeTest.groovy
deleted file mode 100644
index bfc18c1581..0000000000
--- a/subprojects/groovy-json/src/test/groovy/groovy/json/JsonSlurperMalformedUnicodeEscapeTest.groovy
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- *  Licensed to the Apache Software Foundation (ASF) under one
- *  or more contributor license agreements.  See the NOTICE file
- *  distributed with this work for additional information
- *  regarding copyright ownership.  The ASF licenses this file
- *  to you under the Apache License, Version 2.0 (the
- *  "License"); you may not use this file except in compliance
- *  with the License.  You may obtain a copy of the License at
- *
- *    http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing,
- *  software distributed under the License is distributed on an
- *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- *  KIND, either express or implied.  See the License for the
- *  specific language governing permissions and limitations
- *  under the License.
- */
-package groovy.json
-
-import org.junit.jupiter.api.Test
-
-import static groovy.test.GroovyAssert.shouldFail
-import static org.junit.jupiter.api.Assertions.assertEquals
-
-/**
- * RFC 8259 requires a {@code \\u} escape to carry exactly four hexadecimal digits, and
- * {@link JsonTokenType#STRING} validates exactly that for the lexer, so {@link JsonSlurperClassic}
- * rejects a malformed escape. {@link org.apache.groovy.json.internal.CharBuf#decodeJsonString}, the
- * decoder shared by every {@link JsonParserType}, did not: it read the digits with
- * {@code Integer.parseInt(hex, 16)}, which also accepts a leading sign and non-ASCII Unicode digits,
- * and skipped the escape altogether when fewer than four characters remained. So the same document
- * that the lexer refused was accepted by {@link JsonSlurper} and decoded to text the document never
- * specified: {@code "\\u12"} to the literal {@code 12}, {@code "\\u+041"} to {@code A}.
- */
-class JsonSlurperMalformedUnicodeEscapeTest {
-
-    // The Arabic-Indic digits for 1234; Integer.parseInt accepts these as hex, a JSON parser must not.
-    private static final String ARABIC_INDIC_DIGITS = '١٢٣٤'
-
-    private static final Map<String, String> MALFORMED = [
-            'truncated by the closing quote': '{"k":"\\u12"}',
-            'no digits at all'              : '{"k":"\\u"}',
-            'leading plus sign'             : '{"k":"\\u+041"}',
-            'leading minus sign'            : '{"k":"\\u-001"}',
-            'non-ASCII digits'              : '{"k":"\\u' + ARABIC_INDIC_DIGITS + '"}',
-            'not hexadecimal at all'        : '{"k":"\\uZZZZ"}',
-    ]
-
-    @Test
-    void testEveryParserTypeRejectsMalformedUnicodeEscape() {
-        JsonParserType.values().each { type ->
-            MALFORMED.each { description, doc ->
-                shouldFail(JsonException) {
-                    // toJson forces the lazy overlay parsers to materialize (decode) their values.
-                    JsonOutput.toJson(new JsonSlurper().setType(type).parseText(doc))
-                }
-            }
-        }
-    }
-
-    @Test
-    void testClassicSlurperRejectsMalformedUnicodeEscape() {
-        MALFORMED.each { description, doc ->
-            shouldFail(RuntimeException) {
-                new JsonSlurperClassic().parseText(doc)
-            }
-        }
-    }
-
-    @Test
-    void testValidUnicodeEscapesStillDecodeForEveryParserType() {
-        JsonParserType.values().each { type ->
-            def slurper = new JsonSlurper().setType(type)
-            assertValidEscapes("parser type $type") { String doc -> slurper.parseText(doc).k }
-        }
-    }
-
-    @Test
-    void testValidUnicodeEscapesStillDecodeForClassicSlurper() {
-        def slurper = new JsonSlurperClassic()
-        assertValidEscapes('JsonSlurperClassic') { String doc -> slurper.parseText(doc).k }
-    }
-
-    private static void assertValidEscapes(String context, Closure<String> parse) {
-        assertEquals('A', parse('{"k":"\\u0041"}'), "basic escape for $context")
-        assertEquals(0xE9, (int) parse('{"k":"\\u00e9"}').charAt(0), "lower-case digits for $context")
-        assertEquals(0xE9, (int) parse('{"k":"\\u00E9"}').charAt(0), "upper-case digits for $context")
-        assertEquals(0x0000, (int) parse('{"k":"\\u0000"}').charAt(0), "lowest code unit for $context")
-        assertEquals(0xFFFF, (int) parse('{"k":"\\uffff"}').charAt(0), "highest code unit for $context")
-
-        // a surrogate pair is two escapes, and ordinary text may follow the fourth digit
-        String pair = parse('{"k":"\\ud83d\\ude00ef"}')
-        assertEquals(4, pair.length(), "pair length for $context")
-        assertEquals(0x1F600, pair.codePointAt(0), "pair code point for $context")
-        assertEquals('ef', pair.substring(2), "text after escape for $context")
-    }
-}