Skip to content

Commit b23c365

Browse files
committed
Fix bug in Path.toString() method with Unicode surrogate pairs handling\n\nFixed an issue where the wrong loop variable 'i' was used instead of 'j' when processing Unicode surrogate pairs in JSON field names. This caused ArrayIndexOutOfBoundsException or incorrect results when serializing JSON with emoji keys or other Unicode characters represented by surrogate pairs.\n\nAdded test cases to verify the fix works correctly with various Unicode characters including emojis.
1 parent c0f1125 commit b23c365

File tree

3 files changed

+182
-4
lines changed

3 files changed

+182
-4
lines changed

core/src/main/java/com/alibaba/fastjson2/JSONWriter.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2489,10 +2489,10 @@ public String toString() {
24892489
ascii = false;
24902490
final int uc;
24912491
if (ch < '\uDBFF' + 1) { // Character.isHighSurrogate(c)
2492-
if (name.length() - i < 2) {
2492+
if (name.length() - j < 2) {
24932493
uc = -1;
24942494
} else {
2495-
char d = name.charAt(i + 1);
2495+
char d = name.charAt(j + 1);
24962496
// d >= '\uDC00' && d < ('\uDFFF' + 1)
24972497
if (d >= '\uDC00' && d < ('\uDFFF' + 1)) { // Character.isLowSurrogate(d)
24982498
uc = ((ch << 10) + d) + (0x010000 - ('\uD800' << 10) - '\uDC00'); // Character.toCodePoint(c, d)
@@ -2603,10 +2603,10 @@ public String toString() {
26032603
ascii = false;
26042604
final int uc;
26052605
if (ch < '\uDBFF' + 1) { // Character.isHighSurrogate(c)
2606-
if (name.length() - i < 2) {
2606+
if (name.length() - j < 2) {
26072607
uc = -1;
26082608
} else {
2609-
char d = name.charAt(i + 1);
2609+
char d = name.charAt(j + 1);
26102610
// d >= '\uDC00' && d < ('\uDFFF' + 1)
26112611
if (d >= '\uDC00' && d < ('\uDFFF' + 1)) { // Character.isLowSurrogate(d)
26122612
uc = ((ch << 10) + d) + (0x010000 - ('\uD800' << 10) - '\uDC00'); // Character.toCodePoint(c, d)
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package com.alibaba.fastjson2;
2+
3+
import org.junit.jupiter.api.Test;
4+
5+
import java.util.HashMap;
6+
import java.util.Map;
7+
8+
import static org.junit.jupiter.api.Assertions.*;
9+
10+
public class JSONWriterPathBugDemoTest {
11+
/**
12+
* This test demonstrates the bug that was fixed.
13+
* Before the fix, this test would fail with an ArrayIndexOutOfBoundsException
14+
* or produce incorrect results when processing JSON field names with Unicode surrogate pairs.
15+
*
16+
* The bug was in the Path.toString() method where the wrong loop variable 'i' was used
17+
* instead of 'j' when processing characters in a string containing surrogate pairs.
18+
*/
19+
@Test
20+
public void testUnicodeSurrogatePairsPathBug() {
21+
// Create a map with keys that contain Unicode surrogate pairs
22+
Map<String, Object> map = new HashMap<>();
23+
24+
// These emojis are represented by surrogate pairs:
25+
// 😀 (grinning face) = \uD83D\uDE00
26+
// 🌍 (earth globe) = \uD83C\uDF0D
27+
// 👍 (thumbs up) = \uD83D\uDC4D
28+
29+
map.put("key😀", "value1");
30+
map.put("key🌍", "value2");
31+
map.put("key👍", "value3");
32+
33+
// Create nested objects to trigger reference path handling
34+
Map<String, Object> nested1 = new HashMap<>();
35+
Map<String, Object> nested2 = new HashMap<>();
36+
nested1.put("nested😀", nested2); // Emoji in nested key
37+
nested2.put("deepValue", "deepValue");
38+
map.put("nested", nested1);
39+
40+
try {
41+
// Serialize with reference detection enabled to trigger Path.toString()
42+
String json = JSON.toJSONString(map, JSONWriter.Feature.ReferenceDetection);
43+
System.out.println("Serialized JSON: " + json);
44+
45+
// Verify that the JSON contains our values
46+
assertTrue(json.contains("value1"), "JSON should contain value1");
47+
assertTrue(json.contains("value2"), "JSON should contain value2");
48+
assertTrue(json.contains("value3"), "JSON should contain value3");
49+
50+
System.out.println("Test passed - no exception thrown during serialization");
51+
} catch (Exception e) {
52+
fail("Exception thrown during JSON serialization with Unicode surrogate pairs: " + e.getMessage(), e);
53+
}
54+
}
55+
56+
/**
57+
* Additional test with circular references to further stress the Path.toString() method.
58+
*/
59+
@Test
60+
public void testUnicodeSurrogatePairsWithCircularReference() {
61+
// Create objects with circular references and Unicode surrogate pairs in keys
62+
Map<String, Object> parent = new HashMap<>();
63+
Map<String, Object> child = new HashMap<>();
64+
65+
// Use keys with surrogate pairs
66+
parent.put("parent😀", child);
67+
child.put("child👍", parent); // Circular reference with emoji
68+
69+
try {
70+
// Serialize with reference detection enabled
71+
String json = JSON.toJSONString(parent, JSONWriter.Feature.ReferenceDetection);
72+
System.out.println("Serialized JSON with circular reference: " + json);
73+
74+
// Should not throw any exception
75+
System.out.println("Test passed - no exception thrown during serialization with circular reference");
76+
} catch (Exception e) {
77+
fail("Exception thrown during JSON serialization with surrogate pairs and circular reference: " + e.getMessage(), e);
78+
}
79+
}
80+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package com.alibaba.fastjson2;
2+
3+
import org.junit.jupiter.api.Test;
4+
5+
import java.util.HashMap;
6+
import java.util.Map;
7+
8+
import static org.junit.jupiter.api.Assertions.*;
9+
10+
public class JSONWriterPathTest {
11+
/**
12+
* Test for reproducing the bug with Unicode surrogate pairs in JSON field names.
13+
*/
14+
@Test
15+
public void testUnicodeSurrogatePairsInPath() {
16+
// Create a map with a key that contains Unicode surrogate pairs
17+
Map<String, Object> map = new HashMap<>();
18+
// This emoji is represented by surrogate pairs: \uD83D\uDE00 (grinning face)
19+
String keyWithSurrogatePairs = "key" + "\uD83D\uDE00"; // "key😀"
20+
map.put(keyWithSurrogatePairs, "value");
21+
22+
// Also add a nested object to trigger reference path handling
23+
Map<String, Object> nested = new HashMap<>();
24+
nested.put("nestedKey", "nestedValue");
25+
map.put("nested", nested);
26+
27+
// Enable reference detection to trigger the Path.toString() code path
28+
try (JSONWriter writer = JSONWriter.of(JSONWriter.Feature.ReferenceDetection)) {
29+
// Use JSONObjectWriter directly instead of ObjectWriterCreator
30+
writer.write(map);
31+
32+
String json = writer.toString();
33+
System.out.println("Generated JSON: " + json);
34+
assertNotNull(json);
35+
// The test passes if no exception is thrown during serialization
36+
// Check that the value is present in the output
37+
assertTrue(json.contains("value"), "JSON should contain the value 'value'");
38+
} catch (Exception e) {
39+
fail("Exception thrown during JSON serialization with Unicode surrogate pairs: " + e.getMessage(), e);
40+
}
41+
}
42+
43+
/**
44+
* Additional test with more complex Unicode surrogate pairs.
45+
*/
46+
@Test
47+
public void testComplexUnicodeSurrogatePairsInPath() {
48+
Map<String, Object> map = new HashMap<>();
49+
// Using various emojis that are represented with surrogate pairs
50+
map.put("key😀", "value1"); // Grinning face
51+
map.put("key🌍", "value2"); // Earth globe
52+
map.put("key👍", "value3"); // Thumbs up
53+
54+
try (JSONWriter writer = JSONWriter.of(JSONWriter.Feature.ReferenceDetection, JSONWriter.Feature.PrettyFormat)) {
55+
// Use JSONObjectWriter directly instead of ObjectWriterCreator
56+
writer.write(map);
57+
58+
String json = writer.toString();
59+
System.out.println("Generated JSON: " + json);
60+
assertNotNull(json);
61+
// The test passes if no exception is thrown during serialization
62+
// Check that the values are present in the output
63+
assertTrue(json.contains("value1"), "JSON should contain the value 'value1'");
64+
assertTrue(json.contains("value2"), "JSON should contain the value 'value2'");
65+
assertTrue(json.contains("value3"), "JSON should contain the value 'value3'");
66+
} catch (Exception e) {
67+
fail("Exception thrown during JSON serialization with complex Unicode surrogate pairs: " + e.getMessage(), e);
68+
}
69+
}
70+
71+
/**
72+
* Test that specifically targets the bug with surrogate pairs in path handling.
73+
* This test creates a circular reference to force the Path.toString() method to be called.
74+
*/
75+
@Test
76+
public void testSurrogatePairsInPathWithCircularReference() {
77+
// Create objects with circular references
78+
Map<String, Object> parent = new HashMap<>();
79+
Map<String, Object> child = new HashMap<>();
80+
81+
// Use keys with surrogate pairs
82+
parent.put("parent😀", child);
83+
child.put("child😀", parent); // Circular reference
84+
85+
// Enable reference detection to trigger the Path.toString() code path
86+
try (JSONWriter writer = JSONWriter.of(JSONWriter.Feature.ReferenceDetection)) {
87+
// Use JSONObjectWriter directly instead of ObjectWriterCreator
88+
writer.write(parent);
89+
90+
String json = writer.toString();
91+
System.out.println("Generated JSON with circular reference: " + json);
92+
assertNotNull(json);
93+
// Should not throw any exception
94+
} catch (Exception e) {
95+
fail("Exception thrown during JSON serialization with surrogate pairs and circular reference: " + e.getMessage(), e);
96+
}
97+
}
98+
}

0 commit comments

Comments
 (0)