diff --git a/core/src/main/java/com/alibaba/fastjson2/internal/asm/ASMUtils.java b/core/src/main/java/com/alibaba/fastjson2/internal/asm/ASMUtils.java index b7a025f108..4553c8bc8d 100644 --- a/core/src/main/java/com/alibaba/fastjson2/internal/asm/ASMUtils.java +++ b/core/src/main/java/com/alibaba/fastjson2/internal/asm/ASMUtils.java @@ -221,6 +221,39 @@ public final class ASMUtils { descMapping.put(FieldReader[].class, DESC_FIELD_READER_ARRAY); } + /** + * Sanitizes a class simple name so that it can be embedded in a generated class name. + * Array types like {@code "Class[]"} become {@code "ClassArray"}, and any other character + * that is not valid in a class name is replaced with an underscore. + * + * @param simpleName the simple name to sanitize, may be null + * @return a name safe to use in a generated class name, never null + */ + public static String sanitizeClassName(String simpleName) { + if (simpleName == null || simpleName.isEmpty()) { + return ""; + } + if (simpleName.endsWith("[]")) { + return sanitizeClassName(simpleName.substring(0, simpleName.length() - 2)) + "Array"; + } + StringBuilder buf = null; + for (int i = 0; i < simpleName.length(); i++) { + char c = simpleName.charAt(i); + if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_') { + if (buf != null) { + buf.append(c); + } + } else { + if (buf == null) { + buf = new StringBuilder(simpleName.length()); + buf.append(simpleName, 0, i); + } + buf.append('_'); + } + } + return buf != null ? buf.toString() : simpleName; + } + public static String type(Class clazz) { String type = typeMapping.get(clazz); if (type != null) { diff --git a/core/src/main/java/com/alibaba/fastjson2/reader/ObjectReaderCreatorASM.java b/core/src/main/java/com/alibaba/fastjson2/reader/ObjectReaderCreatorASM.java index 8ea085ee78..4e49d50986 100644 --- a/core/src/main/java/com/alibaba/fastjson2/reader/ObjectReaderCreatorASM.java +++ b/core/src/main/java/com/alibaba/fastjson2/reader/ObjectReaderCreatorASM.java @@ -4173,7 +4173,7 @@ public ObjectReadContext( this.fieldNameCharLengthMin = charLenMin; this.fieldNameCharLengthMax = charLenMax; - String className = "ORG_" + seed.incrementAndGet() + "_" + fieldReaders.length + (objectClass == null ? "" : "_" + objectClass.getSimpleName()); + String className = "ORG_" + seed.incrementAndGet() + "_" + fieldReaders.length + (objectClass == null ? "" : "_" + sanitizeClassName(objectClass.getSimpleName())); Package pkg = ObjectReaderCreatorASM.class.getPackage(); if (pkg != null) { @@ -4237,7 +4237,7 @@ private Function createValueConsumer0( String className = (bytes ? "VBACG_" : "VCACG_") + seed.incrementAndGet() + "_" + fieldReaderArray.length - + "_" + objectClass.getSimpleName(); + + "_" + sanitizeClassName(objectClass.getSimpleName()); String classNameType; String classNameFull; diff --git a/core/src/main/java/com/alibaba/fastjson2/writer/ObjectWriterCreatorASM.java b/core/src/main/java/com/alibaba/fastjson2/writer/ObjectWriterCreatorASM.java index a21257a14b..70c29b0ebf 100644 --- a/core/src/main/java/com/alibaba/fastjson2/writer/ObjectWriterCreatorASM.java +++ b/core/src/main/java/com/alibaba/fastjson2/writer/ObjectWriterCreatorASM.java @@ -471,7 +471,7 @@ private ObjectWriterAdapter jitWriter( ClassWriter cw = new ClassWriter(null); - String className = "OWG_" + seed.incrementAndGet() + "_" + fieldWriters.size() + (objectClass == null ? "" : ("_" + objectClass.getSimpleName())); + String className = "OWG_" + seed.incrementAndGet() + "_" + fieldWriters.size() + (objectClass == null ? "" : ("_" + sanitizeClassName(objectClass.getSimpleName()))); String classNameType; String classNameFull; @@ -4636,7 +4636,7 @@ private FieldWriter jitFieldWriterList( ClassWriter cw = new ClassWriter(null); - String className = "OWF_" + seed.incrementAndGet() + "_" + fieldWriters.size() + "_" + itemClass.getSimpleName(); + String className = "OWF_" + seed.incrementAndGet() + "_" + fieldWriters.size() + "_" + sanitizeClassName(itemClass.getSimpleName()); String classNameType; String classNameFull; diff --git a/core/src/test/java/com/alibaba/fastjson2/issues_4000/Issue4009Test.java b/core/src/test/java/com/alibaba/fastjson2/issues_4000/Issue4009Test.java new file mode 100644 index 0000000000..0e423816c6 --- /dev/null +++ b/core/src/test/java/com/alibaba/fastjson2/issues_4000/Issue4009Test.java @@ -0,0 +1,96 @@ +package com.alibaba.fastjson2.issues_4000; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONReader; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class Issue4009Test { + @Test + public void testClassArrayField() { + // Test that a class with a Class[] field can be serialized without ClassFormatError + BeanWithClassArray bean = new BeanWithClassArray(); + bean.setClasses(new Class[]{String.class, Integer.class}); + bean.setName("test"); + + // This should not throw ClassFormatError + Object json = JSON.toJSON(bean); + assertNotNull(json); + + String jsonString = JSON.toJSONString(bean); + assertNotNull(jsonString); + assertTrue(jsonString.contains("test")); + + // the reader generates class names the same way, so deserialization must work too + // (SupportClassForName is required only because the field type is Class[]) + BeanWithClassArray parsed = JSON.parseObject( + jsonString, BeanWithClassArray.class, JSONReader.Feature.SupportClassForName); + assertNotNull(parsed); + assertEquals("test", parsed.getName()); + assertArrayEquals(new Class[]{String.class, Integer.class}, parsed.getClasses()); + } + + @Test + public void testClassArrayFieldNull() { + BeanWithClassArray bean = new BeanWithClassArray(); + bean.setClasses(null); + bean.setName("test"); + + Object json = JSON.toJSON(bean); + assertNotNull(json); + } + + @Test + public void test2DArrayField() { + // Test multi-dimensional arrays + BeanWith2DArray bean = new BeanWith2DArray(); + bean.setMatrix(new int[][]{{1, 2}, {3, 4}}); + + Object json = JSON.toJSON(bean); + assertNotNull(json); + + String jsonString = JSON.toJSONString(bean); + assertNotNull(jsonString); + + BeanWith2DArray parsed = JSON.parseObject(jsonString, BeanWith2DArray.class); + assertNotNull(parsed); + assertArrayEquals(new int[][]{{1, 2}, {3, 4}}, parsed.getMatrix()); + } + + public static class BeanWithClassArray { + private String name; + private Class[] classes; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Class[] getClasses() { + return classes; + } + + public void setClasses(Class[] classes) { + this.classes = classes; + } + } + + public static class BeanWith2DArray { + private int[][] matrix; + + public int[][] getMatrix() { + return matrix; + } + + public void setMatrix(int[][] matrix) { + this.matrix = matrix; + } + } +} diff --git a/core/src/test/java/com/alibaba/fastjson2/util/ASMUtilsTest.java b/core/src/test/java/com/alibaba/fastjson2/util/ASMUtilsTest.java index 78fc889238..2e2f905d05 100644 --- a/core/src/test/java/com/alibaba/fastjson2/util/ASMUtilsTest.java +++ b/core/src/test/java/com/alibaba/fastjson2/util/ASMUtilsTest.java @@ -7,7 +7,9 @@ import java.time.format.DateTimeParseException; import static com.alibaba.fastjson2.internal.asm.ASMUtils.lookupParameterNames; +import static com.alibaba.fastjson2.internal.asm.ASMUtils.sanitizeClassName; import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; @Tag("util") public class ASMUtilsTest { @@ -48,4 +50,32 @@ public void test1() throws Exception { ) ); } + + @Test + public void sanitizeClassNameNullOrEmpty() { + assertEquals("", sanitizeClassName(null)); + assertEquals("", sanitizeClassName("")); + } + + @Test + public void sanitizeClassNameUnchanged() { + assertEquals("String", sanitizeClassName("String")); + assertEquals("MyClass", sanitizeClassName("MyClass")); + assertEquals("MyClass2", sanitizeClassName("MyClass2")); + assertEquals("My_Class", sanitizeClassName("My_Class")); + } + + @Test + public void sanitizeClassNameArray() { + assertEquals("ClassArray", sanitizeClassName("Class[]")); + assertEquals("StringArray", sanitizeClassName("String[]")); + assertEquals("intArrayArray", sanitizeClassName("int[][]")); + } + + @Test + public void sanitizeClassNameSpecialChars() { + assertEquals("My_Class", sanitizeClassName("My$Class")); + assertEquals("My_Class_2", sanitizeClassName("My-Class-2")); + assertEquals("My_ClassArray", sanitizeClassName("My$Class[]")); + } }