Repository: alibaba/fastjson2
Pull request #3330 (2025-02-11 08:53:37)

bugfix:fixed concurrent modification modules exception for issue #3311
### What this PR does / why we need it?
fixed concurrent modification modules exception


### Summary of your change
final List modules = new CopyOnWriteArrayList<>();


#### Please indicate you've done the following:

- [X] Made sure tests are passing and test coverage is added if needed.
- [X] Made sure commit message follow the rule of [Conventional Commits specification](https://www.conventionalcommits.org/).
- [X] Considered the docs impact and opened a new docs issue or PR with docs changes if needed.



Patch:
diff --git a/benchmark/src/main/java/com/alibaba/fastjson2/benchmark/eishay/EishayParseUTF8Bytes.java b/benchmark/src/main/java/com/alibaba/fastjson2/benchmark/eishay/EishayParseUTF8Bytes.java
index 027ba5709..84bfcf586 100644
--- a/benchmark/src/main/java/com/alibaba/fastjson2/benchmark/eishay/EishayParseUTF8Bytes.java
+++ b/benchmark/src/main/java/com/alibaba/fastjson2/benchmark/eishay/EishayParseUTF8Bytes.java
@@ -55,6 +55,11 @@ public class EishayParseUTF8Bytes {
         bh.consume(JSON.parseObject(utf8Bytes, MediaContent.class));
     }
 
+//    @Benchmark
+    public void fastjson2_charset(Blackhole bh) {
+        bh.consume(JSON.parseObject(utf8Bytes, 0, utf8Bytes.length, StandardCharsets.ISO_8859_1, MediaContent.class));
+    }
+
     public void fastjson2_features(Blackhole bh) {
         bh.consume(JSON.parseObject(utf8Bytes, MediaContent.class, featuresContext));
     }
diff --git a/core/src/main/java/com/alibaba/fastjson2/JSONReader.java b/core/src/main/java/com/alibaba/fastjson2/JSONReader.java
index 2b8ff97d3..86430fe7c 100644
--- a/core/src/main/java/com/alibaba/fastjson2/JSONReader.java
+++ b/core/src/main/java/com/alibaba/fastjson2/JSONReader.java
@@ -3239,29 +3239,16 @@ public abstract class JSONReader
     }
 
     public static JSONReader of(byte[] utf8Bytes) {
-        Context context = createReadContext();
-        if (PREDICATE_IS_ASCII != null && PREDICATE_IS_ASCII.test(utf8Bytes)) {
-            return new JSONReaderASCII(context, null, utf8Bytes, 0, utf8Bytes.length);
-        }
-
-        return new JSONReaderUTF8(context, null, utf8Bytes, 0, utf8Bytes.length);
+        return of(utf8Bytes, 0, utf8Bytes.length, StandardCharsets.UTF_8, createReadContext());
     }
 
     @Deprecated
     public static JSONReader of(Context context, byte[] utf8Bytes) {
-        if (PREDICATE_IS_ASCII != null && PREDICATE_IS_ASCII.test(utf8Bytes)) {
-            return new JSONReaderASCII(context, null, utf8Bytes, 0, utf8Bytes.length);
-        }
-
-        return new JSONReaderUTF8(context, null, utf8Bytes, 0, utf8Bytes.length);
+        return JSONReaderUTF8.of(utf8Bytes, 0, utf8Bytes.length, context);
     }
 
     public static JSONReader of(byte[] utf8Bytes, Context context) {
-        if (PREDICATE_IS_ASCII != null && PREDICATE_IS_ASCII.test(utf8Bytes)) {
-            return new JSONReaderASCII(context, null, utf8Bytes, 0, utf8Bytes.length);
-        }
-
-        return new JSONReaderUTF8(context, null, utf8Bytes, 0, utf8Bytes.length);
+        return JSONReaderUTF8.of(utf8Bytes, 0, utf8Bytes.length, context);
     }
 
     public static JSONReader of(char[] chars) {
@@ -3357,7 +3344,7 @@ public abstract class JSONReader
         Context context = JSONFactory.createReadContext();
 
         if (charset == StandardCharsets.UTF_8) {
-            return new JSONReaderUTF8(context, null, bytes, offset, length);
+            return new JSONReaderUTF8(context, bytes, offset, length);
         }
 
         if (charset == StandardCharsets.UTF_16) {
@@ -3365,7 +3352,7 @@ public abstract class JSONReader
         }
 
         if (charset == StandardCharsets.US_ASCII || charset == StandardCharsets.ISO_8859_1) {
-            return new JSONReaderASCII(context, null, bytes, offset, length);
+            return JSONReaderASCII.of(context, null, bytes, offset, length);
         }
 
         throw new JSONException("not support charset " + charset);
@@ -3373,24 +3360,7 @@ public abstract class JSONReader
 
     public static JSONReader of(byte[] bytes, int offset, int length, Charset charset, Context context) {
         if (charset == StandardCharsets.UTF_8) {
-            if (offset == 0 && bytes.length == length) {
-                return of(bytes, context);
-            }
-
-            boolean hasNegative = true;
-            if (METHOD_HANDLE_HAS_NEGATIVE != null) {
-                try {
-                    hasNegative = (Boolean) METHOD_HANDLE_HAS_NEGATIVE.invoke(bytes, 0, bytes.length);
-                } catch (Throwable ignored) {
-                    // ignored
-                }
-            }
-
-            if (!hasNegative) {
-                return new JSONReaderASCII(context, null, bytes, offset, length);
-            }
-
-            return new JSONReaderUTF8(context, null, bytes, offset, length);
+            return JSONReaderUTF8.of(bytes, offset, length, context);
         }
 
         if (charset == StandardCharsets.UTF_16) {
@@ -3398,18 +3368,18 @@ public abstract class JSONReader
         }
 
         if (charset == StandardCharsets.US_ASCII || charset == StandardCharsets.ISO_8859_1) {
-            return new JSONReaderASCII(context, null, bytes, offset, length);
+            return JSONReaderASCII.of(context, null, bytes, offset, length);
         }
 
         throw new JSONException("not support charset " + charset);
     }
 
     public static JSONReader of(byte[] bytes, int offset, int length) {
-        return new JSONReaderUTF8(createReadContext(), null, bytes, offset, length);
+        return of(bytes, offset, length, StandardCharsets.UTF_8, createReadContext());
     }
 
     public static JSONReader of(byte[] bytes, int offset, int length, Context context) {
-        return new JSONReaderUTF8(context, null, bytes, offset, length);
+        return new JSONReaderUTF8(context, bytes, offset, length);
     }
 
     public static JSONReader of(char[] chars, int offset, int length) {
@@ -3445,7 +3415,9 @@ public abstract class JSONReader
         }
 
         if (charset == StandardCharsets.US_ASCII) {
-            return new JSONReaderASCII(context, is);
+            return JVM_VERSION == 8
+                    ? new JSONReaderASCIIJDK8(context, is)
+                    : new JSONReaderASCII(context, is);
         }
 
         return JSONReader.of(new InputStreamReader(is, charset), context);
diff --git a/core/src/main/java/com/alibaba/fastjson2/JSONReaderASCII.java b/core/src/main/java/com/alibaba/fastjson2/JSONReaderASCII.java
index a5b5cea9b..c05201fc4 100644
--- a/core/src/main/java/com/alibaba/fastjson2/JSONReaderASCII.java
+++ b/core/src/main/java/com/alibaba/fastjson2/JSONReaderASCII.java
@@ -21,7 +21,7 @@ class JSONReaderASCII
     protected int nextEscapeIndex = ESCAPE_INDEX_NOT_SET;
 
     JSONReaderASCII(Context ctx, String str, byte[] bytes, int offset, int length) {
-        super(ctx, str, bytes, offset, length);
+        super(ctx, bytes, offset, length);
         this.str = str;
         nameAscii = true;
     }
@@ -1420,7 +1420,7 @@ class JSONReaderASCII
     }
 
     @Override
-    public final String readString() {
+    public String readString() {
         if (ch == '"' || ch == '\'') {
             final byte[] bytes = this.bytes;
             final byte quote = (byte) ch;
@@ -1513,7 +1513,7 @@ class JSONReaderASCII
         return readStringNotMatch();
     }
 
-    private int readEscaped(byte[] bytes, int offset, byte quote, char[] buf) {
+    protected final int readEscaped(byte[] bytes, int offset, byte quote, char[] buf) {
         for (int i = 0; ; ++i) {
             char c = (char) (bytes[offset] & 0xff);
             if (c == '\\') {
@@ -1559,4 +1559,8 @@ class JSONReaderASCII
         }
         return offset;
     }
+
+    public static JSONReaderASCII of(Context ctx, String str, byte[] bytes, int offset, int length) {
+        return new JSONReaderASCII(ctx, str, bytes, offset, length);
+    }
 }
diff --git a/core/src/main/java/com/alibaba/fastjson2/JSONReaderASCIIJDK8.java b/core/src/main/java/com/alibaba/fastjson2/JSONReaderASCIIJDK8.java
new file mode 100644
index 000000000..9d9ff68d9
--- /dev/null
+++ b/core/src/main/java/com/alibaba/fastjson2/JSONReaderASCIIJDK8.java
@@ -0,0 +1,157 @@
+package com.alibaba.fastjson2;
+
+import com.alibaba.fastjson2.util.TypeUtils;
+
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+
+import static com.alibaba.fastjson2.JSONReaderJSONB.check3;
+import static com.alibaba.fastjson2.util.IOUtils.getLongLE;
+import static com.alibaba.fastjson2.util.IOUtils.hexDigit4;
+
+final class JSONReaderASCIIJDK8
+        extends JSONReaderASCII {
+    JSONReaderASCIIJDK8(Context ctx, String str, byte[] bytes, int offset, int length) {
+        super(ctx, str, bytes, offset, length);
+    }
+
+    JSONReaderASCIIJDK8(Context ctx, InputStream is) {
+        super(ctx, is);
+    }
+
+    @Override
+    public String readString() {
+        if (ch == '"' || ch == '\'') {
+            final byte[] bytes = this.bytes;
+            char quote = this.ch;
+            int valueLength;
+            int offset = this.offset;
+            final int start = offset, end = this.end;
+            final long byteVectorQuote = quote == '\'' ? 0x2727_2727_2727_2727L : 0x2222_2222_2222_2222L;
+            valueEscape = false;
+
+            {
+                int i = 0;
+                int upperBound = offset + ((end - offset) & ~7);
+                while (offset < upperBound) {
+                    long v = getLongLE(bytes, offset);
+                    if ((v & 0xFF00FF00FF00FF00L) != 0 || containsSlashOrQuote(v, byteVectorQuote)) {
+                        break;
+                    }
+
+                    offset += 8;
+                    i += 8;
+                }
+                // ...
+
+                for (; ; ++i) {
+                    if (offset >= end) {
+                        throw new JSONException("invalid escape character EOI");
+                    }
+
+                    int c = bytes[offset] & 0xFF;
+                    if (c == '\\') {
+                        valueEscape = true;
+                        c = bytes[offset + 1];
+                        offset += (c == 'u' ? 6 : (c == 'x' ? 4 : 2));
+                        continue;
+                    }
+
+                    if (c == quote) {
+                        valueLength = i;
+                        break;
+                    }
+                    offset++;
+                }
+            }
+
+            String str;
+            if (valueEscape) {
+                char[] chars = new char[valueLength];
+                offset = start;
+                for (int i = 0; ; ++i) {
+                    int ch = bytes[offset];
+                    if (ch == '\\') {
+                        ch = bytes[++offset];
+                        switch (ch) {
+                            case 'u': {
+                                ch = hexDigit4(bytes, check3(offset + 1, end));
+                                offset += 4;
+                                break;
+                            }
+                            case 'x': {
+                                ch = char2(bytes[offset + 1], bytes[offset + 2]);
+                                offset += 2;
+                                break;
+                            }
+                            case '\\':
+                            case '"':
+                                break;
+                            case 'b':
+                                ch = '\b';
+                                break;
+                            case 't':
+                                ch = '\t';
+                                break;
+                            case 'n':
+                                ch = '\n';
+                                break;
+                            case 'f':
+                                ch = '\f';
+                                break;
+                            case 'r':
+                                ch = '\r';
+                                break;
+                            default:
+                                ch = char1(ch);
+                                break;
+                        }
+                        chars[i] = (char) ch;
+                        offset++;
+                    } else if (ch == quote) {
+                        break;
+                    } else {
+                        chars[i] = (char) ch;
+                        offset++;
+                    }
+                }
+
+                str = new String(chars);
+            } else {
+                int strlen = offset - start;
+                if (strlen == 1) {
+                    str = TypeUtils.toString((char) (bytes[start] & 0xff));
+                } else if (strlen == 2) {
+                    str = TypeUtils.toString(
+                            (char) (bytes[start] & 0xff),
+                            (char) (bytes[start + 1] & 0xff)
+                    );
+                } else {
+                    str = new String(bytes, start, offset - start, StandardCharsets.US_ASCII);
+                }
+            }
+
+            if ((context.features & Feature.TrimString.mask) != 0) {
+                str = str.trim();
+            }
+
+            int ch = ++offset == end ? EOI : bytes[offset++];
+            while (ch <= ' ' && (1L << ch & SPACE) != 0) {
+                ch = offset == end ? EOI : bytes[offset++];
+            }
+
+            if (comma = ch == ',') {
+                ch = offset == end ? EOI : bytes[offset++];
+                while (ch <= ' ' && (1L << ch & SPACE) != 0) {
+                    ch = offset == end ? EOI : bytes[offset++];
+                }
+            }
+
+            this.ch = (char) ch;
+            this.offset = offset;
+            return str;
+        }
+
+        return readStringNotMatch();
+    }
+}
diff --git a/core/src/main/java/com/alibaba/fastjson2/JSONReaderUTF8.java b/core/src/main/java/com/alibaba/fastjson2/JSONReaderUTF8.java
index 04af023d3..276dfb861 100644
--- a/core/src/main/java/com/alibaba/fastjson2/JSONReaderUTF8.java
+++ b/core/src/main/java/com/alibaba/fastjson2/JSONReaderUTF8.java
@@ -105,7 +105,7 @@ class JSONReaderUTF8
         }
     }
 
-    JSONReaderUTF8(Context ctx, String str, byte[] bytes, int offset, int length) {
+    JSONReaderUTF8(Context ctx, byte[] bytes, int offset, int length) {
         super(ctx, false, true);
 
         this.bytes = bytes;
@@ -7226,4 +7226,21 @@ class JSONReaderUTF8
             }
         }
     }
+
+    public static JSONReaderUTF8 of(byte[] bytes, int off, int len, JSONReader.Context context) {
+        if (METHOD_HANDLE_HAS_NEGATIVE != null) {
+            try {
+                boolean hasNegative = (Boolean) METHOD_HANDLE_HAS_NEGATIVE.invoke(bytes, off, len);
+                if (!hasNegative) {
+                    return new JSONReaderASCII(context, null, bytes, off, len);
+                }
+            } catch (Throwable ignored) {
+                // ignored
+            }
+        } else if (IOUtils.isASCII(bytes, off, len)) {
+            return new JSONReaderASCIIJDK8(context, null, bytes, off, len);
+        }
+
+        return new JSONReaderUTF8(context, bytes, off, len);
+    }
 }
diff --git a/core/src/main/java/com/alibaba/fastjson2/reader/ObjectReaderProvider.java b/core/src/main/java/com/alibaba/fastjson2/reader/ObjectReaderProvider.java
index 8523ad95a..347e2363a 100644
--- a/core/src/main/java/com/alibaba/fastjson2/reader/ObjectReaderProvider.java
+++ b/core/src/main/java/com/alibaba/fastjson2/reader/ObjectReaderProvider.java
@@ -19,6 +19,7 @@ import java.lang.reflect.*;
 import java.util.*;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.Consumer;
 import java.util.function.Function;
@@ -156,7 +157,7 @@ public class ObjectReaderProvider
     private final ConcurrentMap<Type, Map<Type, Function>> typeConverts = new ConcurrentHashMap<>();
 
     final ObjectReaderCreator creator;
-    final List<ObjectReaderModule> modules = new ArrayList<>();
+    final List<ObjectReaderModule> modules = new CopyOnWriteArrayList<>();
 
     boolean disableReferenceDetect = JSONFactory.isDisableReferenceDetect();
     boolean disableArrayMapping = JSONFactory.isDisableArrayMapping();
diff --git a/core/src/main/java/com/alibaba/fastjson2/util/IOUtils.java b/core/src/main/java/com/alibaba/fastjson2/util/IOUtils.java
index 5236a6534..c36d502fa 100644
--- a/core/src/main/java/com/alibaba/fastjson2/util/IOUtils.java
+++ b/core/src/main/java/com/alibaba/fastjson2/util/IOUtils.java
@@ -836,7 +836,7 @@ public class IOUtils {
         final int div2 = div / 1000;
         final int rem1 = nano - div * 1000;
 
-        putIntLE(bytes, off, DIGITS_K_32[div2] & 0xffffff00 | '.');
+        putIntLE(bytes, off, DIGITS_K_32[div2 & 0x3ff] & 0xffffff00 | '.');
         off += 4;
 
         int v;
@@ -1960,9 +1960,7 @@ public class IOUtils {
             15 = 0b0000_1111 => m = ((x + 6) & 0x10); (m >> 1) - (m >> 4) => 7 + 0x30 + (x & 0xF) => F
          */
         int m = (i + 0x06060606) & 0x10101010;
-        return ((m >> 1) - (m >> 4))
-                + 0x30303030
-                + i;
+        return ((m * 7) >> 4) + 0x30303030 + i;
     }
 
     public static long utf16Hex4U(long i) {
@@ -1993,8 +1991,7 @@ public class IOUtils {
 
     private static int reverseBytesExpand(int i) {
         // i = Integer.reverseBytes(Integer.expand(i, 0xF0F0F0F0));
-        i = ((i & 0xF000) >> 12) | (i & 0xF00) | ((i & 0xF0) << 12) | ((i & 0xF) << 24);
-        return i;
+        return ((i & 0xF000) >> 12) | (i & 0xF00) | ((i & 0xF0) << 12) | ((i & 0xF) << 24);
     }
 
     private static long utf16ReverseBytesExpand(long i) {


Test patch:
diff --git a/core/src/test/java/com/alibaba/fastjson2/JSONReaderTest1.java b/core/src/test/java/com/alibaba/fastjson2/JSONReaderTest1.java
index d1f8da17e..63fb2b6fc 100644
--- a/core/src/test/java/com/alibaba/fastjson2/JSONReaderTest1.java
+++ b/core/src/test/java/com/alibaba/fastjson2/JSONReaderTest1.java
@@ -1763,7 +1763,7 @@ public class JSONReaderTest1 {
                 assertEquals(name0, name1);
 
                 byte[] ut8Bytes = new String(chars).getBytes(StandardCharsets.UTF_8);
-                JSONReaderUTF8 utf8Reader = new JSONReaderUTF8(JSONFactory.createReadContext(), null, ut8Bytes, 0, ut8Bytes.length);
+                JSONReaderUTF8 utf8Reader = new JSONReaderUTF8(JSONFactory.createReadContext(), ut8Bytes, 0, ut8Bytes.length);
                 assertTrue(utf8Reader.nextIfObjectStart());
                 String name2 = utf8Reader.readFieldName();
                 assertEquals(name0, name2);
@@ -2023,7 +2023,7 @@ public class JSONReaderTest1 {
             String json = JSON.toJSONString(JSONObject.of(s1, s1));
             byte[] bytes = json.getBytes(StandardCharsets.UTF_8);
             JSONReader.Context ctx = JSONFactory.createReadContext();
-            JSONReaderUTF8 jsonReader = new JSONReaderUTF8(ctx, s1, bytes, 0, bytes.length);
+            JSONReaderUTF8 jsonReader = new JSONReaderUTF8(ctx, bytes, 0, bytes.length);
             JSONObject object = (JSONObject) jsonReader.readObject();
             Object v1 = object.get(s1);
             assertEquals(s1, v1);
@@ -2035,7 +2035,7 @@ public class JSONReaderTest1 {
                 String json = JSON.toJSONString(JSONObject.of(s2, s2));
                 byte[] bytes = json.getBytes(StandardCharsets.UTF_8);
                 JSONReader.Context ctx = JSONFactory.createReadContext();
-                JSONReaderUTF8 jsonReader = new JSONReaderUTF8(ctx, s2, bytes, 0, bytes.length);
+                JSONReaderUTF8 jsonReader = new JSONReaderUTF8(ctx, bytes, 0, bytes.length);
                 JSONObject object = (JSONObject) jsonReader.readObject();
                 Object v1 = object.get(s2);
                 assertEquals(s2, v1);
diff --git a/core/src/test/java/com/alibaba/fastjson2/TestUtils.java b/core/src/test/java/com/alibaba/fastjson2/TestUtils.java
index 9160d7815..4ee475333 100644
--- a/core/src/test/java/com/alibaba/fastjson2/TestUtils.java
+++ b/core/src/test/java/com/alibaba/fastjson2/TestUtils.java
@@ -150,7 +150,7 @@ public class TestUtils {
         byte[] utf8Bytes = str.getBytes(StandardCharsets.UTF_8);
         byte[] utf16Bytes = str.getBytes(StandardCharsets.UTF_16);
         return new JSONReader[]{
-                new JSONReaderUTF8(JSONFactory.createReadContext(), null, utf8Bytes, 0, utf8Bytes.length),
+                new JSONReaderUTF8(JSONFactory.createReadContext(), utf8Bytes, 0, utf8Bytes.length),
                 new JSONReaderUTF16(JSONFactory.createReadContext(), utf16Bytes, 0, utf16Bytes.length)
         };
     }
@@ -159,7 +159,7 @@ public class TestUtils {
         byte[] utf8Bytes = str.getBytes(StandardCharsets.UTF_8);
         byte[] utf16Bytes = str.getBytes(StandardCharsets.UTF_16);
         return new JSONReader[]{
-                new JSONReaderUTF8(JSONFactory.createReadContext(), null, utf8Bytes, 0, utf8Bytes.length),
+                new JSONReaderUTF8(JSONFactory.createReadContext(), utf8Bytes, 0, utf8Bytes.length),
                 new JSONReaderUTF16(JSONFactory.createReadContext(), utf16Bytes, 0, utf16Bytes.length),
                 new JSONReaderASCII(JSONFactory.createReadContext(), null, utf8Bytes, 0, utf8Bytes.length)
         };