diff --git a/core/src/main/java/com/alibaba/fastjson2/JSONReader.java b/core/src/main/java/com/alibaba/fastjson2/JSONReader.java index 36d8713edb..bd154db5e7 100644 --- a/core/src/main/java/com/alibaba/fastjson2/JSONReader.java +++ b/core/src/main/java/com/alibaba/fastjson2/JSONReader.java @@ -1339,8 +1339,19 @@ protected JSONException char1Error(int c) { } static char char2(int c1, int c2) { - return (char) (DIGITS2[c1] * 0x10 - + DIGITS2[c2]); + return (char) (digit2(c1) * 0x10 + + digit2(c2)); + } + + /** + * Hex value of a {@code \x} escape digit. {@link JSONFactory#DIGITS2} only covers + * {@code 0}..{@code 'f'}; any character outside the table is not a hex digit either, + * so it gets the same value the table already gives to in-range non-hex characters + * (0) rather than indexing out of bounds. Characters {@code >= 0x80} arrive as + * negative {@code int}s in the byte-based readers, so this also guards the low side. + */ + private static int digit2(int c) { + return c >= 0 && c < DIGITS2.length ? DIGITS2[c] : 0; } /** diff --git a/core/src/test/java/com/alibaba/fastjson2/issues/Issue7810.java b/core/src/test/java/com/alibaba/fastjson2/issues/Issue7810.java new file mode 100644 index 0000000000..01559715ad --- /dev/null +++ b/core/src/test/java/com/alibaba/fastjson2/issues/Issue7810.java @@ -0,0 +1,71 @@ +package com.alibaba.fastjson2.issues; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONException; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Verify that {@code \x} escapes with digits outside the hex table no longer crash + * with {@link ArrayIndexOutOfBoundsException} in {@code JSONReader.char2()}. + * + *
{@code DIGITS2} only covers {@code 0}..{@code 'f'}. Any escape character past that + * (or a byte {@code >= 0x80} arriving as a negative {@code int} in the byte-based + * readers) previously indexed the array out of bounds. Such characters are not hex + * digits either, so they now decode to the same value the table already gives to + * in-range non-hex characters (0), keeping existing semantics intact. + * + * @see Issue #7810 + */ +public class Issue7810 { + @Test + public void testCharsPastTableDoNotCrash() { + // 'z' (122), 'g' (103), '{' (123) all index past DIGITS2's last entry 'f' (102) + assertEquals("\u0000", JSON.parse("\"\\xzz\"")); + assertEquals("\u0000", JSON.parse("\"\\xgg\"")); + assertEquals("\u0000", JSON.parse("\"\\x{{\"")); + } + + @Test + public void testNegativeByteDoesNotCrash() { + // byte 0xFF read as a signed byte arrives as -1 and indexes below the table + assertEquals("\u0000", JSON.parse("\"\\x\u00ff\u00ff\"")); + assertEquals("\u0000", JSON.parse("\"\\x\u00ffz\"")); + } + + @Test + public void testAllContainerPaths() { + assertEquals("\u0000", JSON.parse("[\"\\xzz\"]").toString()); + assertEquals("\u0000", JSON.parse("{\"a\":\"\\xzz\"}").get("a")); + assertEquals("\u0000", JSON.parse("{\"\\xzz\":1}").keySet().iterator().next()); + } + + @Test + public void testValidHexEscapesUnchanged() { + assertEquals("A", JSON.parse("\"\\x41\"")); + assertEquals("\u00ff", JSON.parse("\"\\xff\"")); + assertEquals("\u0000", JSON.parse("\"\\x00\"")); + } + + @Test + public void testInRangeNonHexUnchanged() { + // Characters inside the table that are not hex digits already decoded to 0 + assertEquals("\u0000", JSON.parse("\"\\x::\"")); + assertEquals("\u0000", JSON.parse("\"\\x@@\"")); + } + + @Test + public void testRegularInputStillWorks() { + assertEquals("A", JSON.parse("\"\\u0041\"")); + assertEquals("{\"a\":1}", JSON.parse("{\"a\":1}").toString()); + assertEquals("[1,2,3]", JSON.parse("[1,2,3]").toString()); + } + + @Test + public void testTruncatedEscapeStillThrows() { + // A trailing backslash with no escape char should remain a clean JSONException + assertThrows(JSONException.class, () -> JSON.parse("{\"\\")); + } +} \ No newline at end of file