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

chaokunyang pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fory.git


The following commit(s) were added to refs/heads/main by this push:
     new 0855c18f1 fix(java): validate subclass serializer layer counts (#3676)
0855c18f1 is described below

commit 0855c18f13534fbf55936e7db043f78a6875af5a
Author: Shawn Yang <[email protected]>
AuthorDate: Wed May 13 13:41:22 2026 +0800

    fix(java): validate subclass serializer layer counts (#3676)
    
    ## Why?
    
    
    
    ## What does this PR do?
    
    
    
    ## Related issues
    
    
    
    ## AI Contribution Checklist
    
    
    
    - [ ] Substantial AI assistance was used in this PR: `yes` / `no`
    - [ ] If `yes`, I included a completed [AI Contribution
    
Checklist](https://github.com/apache/fory/blob/main/AI_POLICY.md#9-contributor-checklist-for-ai-assisted-prs)
    in this PR description and the required `AI Usage Disclosure`.
    - [ ] If `yes`, my PR description includes the required `ai_review`
    summary and screenshot evidence of the final clean AI review results
    from both fresh reviewers on the current PR diff or current HEAD after
    the latest code changes.
    
    
    
    ## Does this PR introduce any user-facing change?
    
    
    
    - [ ] Does this PR introduce any public API change?
    - [ ] Does this PR introduce any binary protocol compatibility change?
    
    ## Benchmark
---
 docs/guide/java/static-generated-serializers.md    | 20 ++++++++
 docs/specification/java_serialization_spec.md      | 54 ++++++++++++++++++++++
 .../annotation/processing/ForyStructProcessor.java | 24 ++++------
 .../processing/ForyStructProcessorTest.java        | 40 ++++++++++++++++
 .../java/org/apache/fory/annotation/ForyDebug.java | 37 +++++++++++++++
 .../org/apache/fory/annotation/ForyStruct.java     |  8 ----
 .../fory/builder/StaticCompatibleCodecBuilder.java |  6 ++-
 .../fory/serializer/ExceptionSerializers.java      | 23 +++++++++
 .../collection/ChildContainerSerializers.java      | 47 +++++++++++++------
 .../builder/StaticCompatibleCodecBuilderTest.java  | 51 ++++++++++++++++++++
 .../fory/serializer/ExceptionSerializersTest.java  | 25 ++++++++++
 .../collection/ChildContainerSerializersTest.java  | 22 +++++++++
 12 files changed, 316 insertions(+), 41 deletions(-)

diff --git a/docs/guide/java/static-generated-serializers.md 
b/docs/guide/java/static-generated-serializers.md
index ec101027c..428b3eff3 100644
--- a/docs/guide/java/static-generated-serializers.md
+++ b/docs/guide/java/static-generated-serializers.md
@@ -84,6 +84,26 @@ The processor generates serializer classes in the same Java 
package as the annot
 For a static nested type such as `Outer.Inner`, the generated top-level 
classes are
 `Outer$Inner__ForySerializer__` and `Outer$Inner__ForyNativeSerializer__`.
 
+## Field Debug Tracing
+
+Add `@ForyDebug` next to `@ForyStruct` when you need generated serializers to 
include field-level
+debug tracing hooks. The generated code prints those traces only when
+`ENABLE_FORY_DEBUG_OUTPUT=1`.
+
+```java
+import org.apache.fory.annotation.ForyDebug;
+import org.apache.fory.annotation.ForyStruct;
+
+@ForyStruct
+@ForyDebug
+public class DebugOrder {
+  public long id;
+  public String note;
+
+  public DebugOrder() {}
+}
+```
+
 ## Runtime Use
 
 Fory uses static generated serializers when they are available on:
diff --git a/docs/specification/java_serialization_spec.md 
b/docs/specification/java_serialization_spec.md
index 7906a329b..1cba88ebc 100644
--- a/docs/specification/java_serialization_spec.md
+++ b/docs/specification/java_serialization_spec.md
@@ -430,6 +430,23 @@ per-element null flags (if HAS_NULL).
 If `IS_SAME_TYPE` is not set, each element is written with its own class info 
and data (and
 optionally ref flags).
 
+#### Child collection subclasses
+
+Optimized serializers for subclasses of supported JDK collection 
implementations write subclass
+field layers before element payloads:
+
+```
+| varuint32_small7: length | [comparator_ref] | varuint32_small7: 
num_class_layers |
+| class_layer_fields... | [elements_header | elem_class_info | elements...] |
+```
+
+- `comparator_ref` is present only for sorted-set and priority-queue 
subclasses.
+- `num_class_layers` is the exact number of subclass-owned field layers 
written after the collection
+  header and before the element payload.
+- Readers must reject a payload whose `num_class_layers` does not match the 
local serializer's layer
+  count. These serializers do not carry per-layer class identity in the value 
payload, so mismatched
+  layers cannot be skipped safely.
+
 ### Maps
 
 Maps encode entry count and then a sequence of chunks. Each chunk groups 
entries that share key
@@ -471,6 +488,23 @@ These chunks always represent exactly one entry.
 - `1`: Java-serialized empty `EnumMap` payload. Android uses this mode when an 
empty map has no
   public key from which to derive the enum class. Readers on Android and JVM 
must accept both modes.
 
+#### Child map subclasses
+
+Optimized serializers for subclasses of supported JDK map implementations 
write subclass field
+layers before map entry chunks:
+
+```
+| varuint32_small7: size | [comparator_ref] | varuint32_small7: 
num_class_layers |
+| class_layer_fields... | [chunk_1 | chunk_2 | ...] |
+```
+
+- `comparator_ref` is present only for sorted-map subclasses.
+- `num_class_layers` is the exact number of subclass-owned field layers 
written after the map header
+  and before the entry chunks.
+- Readers must reject a payload whose `num_class_layers` does not match the 
local serializer's layer
+  count. These serializers do not carry per-layer class identity in the value 
payload, so mismatched
+  layers cannot be skipped safely.
+
 ### JDK collection/map wrappers and views
 
 Java native mode may use specialized serializers for JDK collection/map 
wrappers and views. These
@@ -514,6 +548,26 @@ serialization:
 - For each field, the serializer uses field metadata (nullable, trackingRef, 
polymorphic) to decide
   whether to write ref flags and/or type meta before the field value.
 
+### Throwable values
+
+`Throwable` subclasses use a specialized payload that preserves stack trace, 
cause, message,
+suppressed exceptions, and subclass-owned fields:
+
+```
+| stack_trace_ref | cause_ref | message_string_ref |
+| varuint32: suppressed_count | suppressed_ref... |
+| varuint32: extra_field_count | extra_field_name/value... |
+| varuint32_small7: num_class_layers | class_layer_fields... |
+```
+
+- `extra_field_count` is reserved for serializer-owned extension fields and is 
currently written as
+  zero.
+- `num_class_layers` is the exact number of `Throwable` subclass field layers 
written after the
+  built-in Throwable state.
+- Readers must reject a payload whose `num_class_layers` does not match the 
local serializer's layer
+  count. The Throwable value payload does not carry per-layer class identity, 
so mismatched layers
+  cannot be skipped safely.
+
 ### Extensions (EXT)
 
 Extension types are encoded by their registered serializer. Type meta is still 
written before the
diff --git 
a/java/fory-annotation-processor/src/main/java/org/apache/fory/annotation/processing/ForyStructProcessor.java
 
b/java/fory-annotation-processor/src/main/java/org/apache/fory/annotation/processing/ForyStructProcessor.java
index d6827c5d3..7eaa246a4 100644
--- 
a/java/fory-annotation-processor/src/main/java/org/apache/fory/annotation/processing/ForyStructProcessor.java
+++ 
b/java/fory-annotation-processor/src/main/java/org/apache/fory/annotation/processing/ForyStructProcessor.java
@@ -58,12 +58,16 @@ import javax.lang.model.util.Elements;
 import javax.tools.Diagnostic;
 import javax.tools.JavaFileObject;
 
-@SupportedAnnotationTypes("org.apache.fory.annotation.ForyStruct")
+@SupportedAnnotationTypes({
+  "org.apache.fory.annotation.ForyStruct",
+  "org.apache.fory.annotation.ForyDebug"
+})
 public final class ForyStructProcessor extends AbstractProcessor {
   private static final String ARRAY_TYPE = 
"org.apache.fory.annotation.ArrayType";
   private static final String BFLOAT16_TYPE = 
"org.apache.fory.annotation.BFloat16Type";
   private static final String EXPOSE = "org.apache.fory.annotation.Expose";
   private static final String FLOAT16_TYPE = 
"org.apache.fory.annotation.Float16Type";
+  private static final String FORY_DEBUG = 
"org.apache.fory.annotation.ForyDebug";
   private static final String FORY_FIELD = 
"org.apache.fory.annotation.ForyField";
   private static final String FORY_STRUCT = 
"org.apache.fory.annotation.ForyStruct";
   private static final String IGNORE = "org.apache.fory.annotation.Ignore";
@@ -231,25 +235,13 @@ public final class ForyStructProcessor extends 
AbstractProcessor {
         canonicalName(type.asType()),
         serializerName,
         record,
-        foryStructDebug(type),
+        isForyDebugEnabled(type),
         sourceFields,
         recordConstructorFields);
   }
 
-  private boolean foryStructDebug(TypeElement type) {
-    AnnotationMirror mirror = annotationMirror(type, FORY_STRUCT);
-    if (mirror == null) {
-      return false;
-    }
-    Map<? extends ExecutableElement, ? extends AnnotationValue> values =
-        elements.getElementValuesWithDefaults(mirror);
-    for (Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> 
entry :
-        values.entrySet()) {
-      if (entry.getKey().getSimpleName().contentEquals("debug")) {
-        return Boolean.TRUE.equals(entry.getValue().getValue());
-      }
-    }
-    return false;
+  private boolean isForyDebugEnabled(TypeElement type) {
+    return annotationMirror(type, FORY_DEBUG) != null;
   }
 
   private void validateForyFieldId(
diff --git 
a/java/fory-annotation-processor/src/test/java/org/apache/fory/annotation/processing/ForyStructProcessorTest.java
 
b/java/fory-annotation-processor/src/test/java/org/apache/fory/annotation/processing/ForyStructProcessorTest.java
index a24bc316b..dab9795c2 100644
--- 
a/java/fory-annotation-processor/src/test/java/org/apache/fory/annotation/processing/ForyStructProcessorTest.java
+++ 
b/java/fory-annotation-processor/src/test/java/org/apache/fory/annotation/processing/ForyStructProcessorTest.java
@@ -107,6 +107,46 @@ public class ForyStructProcessorTest {
     }
   }
 
+  @Test
+  public void testForyDebugAnnotationEmitsGeneratedFieldTracing() throws 
Exception {
+    CompilationResult result =
+        compile(
+            "test.DebugStruct",
+            "package test;\n"
+                + "import org.apache.fory.annotation.ForyDebug;\n"
+                + "import org.apache.fory.annotation.ForyStruct;\n"
+                + "@ForyStruct\n"
+                + "@ForyDebug\n"
+                + "public class DebugStruct {\n"
+                + "  public int id;\n"
+                + "  public String name;\n"
+                + "  public DebugStruct() {}\n"
+                + "}\n");
+    Assert.assertTrue(result.success, result.diagnostics());
+    String generatedSource =
+        
result.generatedSource("test/DebugStruct__ForyNativeSerializer__.java");
+    Assert.assertTrue(
+        generatedSource.contains("private static final boolean 
FORY_STRUCT_DEBUG = true;"));
+    Assert.assertTrue(generatedSource.contains("debugWriteField(\""));
+    Assert.assertTrue(generatedSource.contains("debugReadField(\""));
+    Assert.assertTrue(generatedSource.contains("debugRemoteReadField(\""));
+  }
+
+  @Test
+  public void testForyStructDebugParameterIsRejected() throws Exception {
+    CompilationResult result =
+        compile(
+            "test.LegacyDebugStruct",
+            "package test;\n"
+                + "import org.apache.fory.annotation.ForyStruct;\n"
+                + "@ForyStruct(debug = true) public class LegacyDebugStruct 
{\n"
+                + "  public int id;\n"
+                + "  public LegacyDebugStruct() {}\n"
+                + "}\n");
+    Assert.assertFalse(result.success);
+    Assert.assertTrue(result.diagnostics().contains("debug"), 
result.diagnostics());
+  }
+
   @Test
   public void testPrivateFieldUsesAccessibleAccessors() throws Exception {
     CompilationResult result =
diff --git 
a/java/fory-core/src/main/java/org/apache/fory/annotation/ForyDebug.java 
b/java/fory-core/src/main/java/org/apache/fory/annotation/ForyDebug.java
new file mode 100644
index 000000000..b9aab3c6d
--- /dev/null
+++ b/java/fory-core/src/main/java/org/apache/fory/annotation/ForyDebug.java
@@ -0,0 +1,37 @@
+/*
+ * 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 org.apache.fory.annotation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Enables generated serializer field-level debug tracing for a {@link 
ForyStruct} type.
+ *
+ * <p>The generated code still prints only when {@code 
ENABLE_FORY_DEBUG_OUTPUT=1}; this annotation
+ * controls whether field tracing code is emitted for the annotated struct.
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface ForyDebug {}
diff --git 
a/java/fory-core/src/main/java/org/apache/fory/annotation/ForyStruct.java 
b/java/fory-core/src/main/java/org/apache/fory/annotation/ForyStruct.java
index c92d41cc6..a72fb9ff2 100644
--- a/java/fory-core/src/main/java/org/apache/fory/annotation/ForyStruct.java
+++ b/java/fory-core/src/main/java/org/apache/fory/annotation/ForyStruct.java
@@ -58,12 +58,4 @@ public @interface ForyStruct {
    * struct encoding even when compatible metadata is otherwise enabled.
    */
   Evolution evolution() default Evolution.INHERIT;
-
-  /**
-   * Emit generated serializer field-level debug tracing.
-   *
-   * <p>The generated code still prints only when {@code 
ENABLE_FORY_DEBUG_OUTPUT=1}; this option
-   * controls whether field tracing code is emitted for this struct.
-   */
-  boolean debug() default false;
 }
diff --git 
a/java/fory-core/src/main/java/org/apache/fory/builder/StaticCompatibleCodecBuilder.java
 
b/java/fory-core/src/main/java/org/apache/fory/builder/StaticCompatibleCodecBuilder.java
index 9bf3f7682..0fbd26f42 100644
--- 
a/java/fory-core/src/main/java/org/apache/fory/builder/StaticCompatibleCodecBuilder.java
+++ 
b/java/fory-core/src/main/java/org/apache/fory/builder/StaticCompatibleCodecBuilder.java
@@ -24,6 +24,7 @@ import static org.apache.fory.type.TypeUtils.OBJECT_TYPE;
 import java.util.Collections;
 import java.util.List;
 import org.apache.fory.Fory;
+import org.apache.fory.annotation.ForyDebug;
 import org.apache.fory.annotation.ForyStruct;
 import org.apache.fory.builder.Generated.GeneratedStaticCompatibleSerializer;
 import org.apache.fory.codegen.Code;
@@ -68,8 +69,9 @@ public final class StaticCompatibleCodecBuilder extends 
ObjectCodecBuilder {
         !fory.getConfig().checkClassVersion(),
         "Class version check should be disabled when compatible mode is 
enabled.");
     localDescriptors = 
Collections.unmodifiableList(Descriptor.getDescriptors(beanClass));
-    ForyStruct foryStruct = beanClass.getAnnotation(ForyStruct.class);
-    debug = foryStruct != null && foryStruct.debug();
+    debug =
+        beanClass.isAnnotationPresent(ForyStruct.class)
+            && beanClass.isAnnotationPresent(ForyDebug.class);
   }
 
   @Override
diff --git 
a/java/fory-core/src/main/java/org/apache/fory/serializer/ExceptionSerializers.java
 
b/java/fory-core/src/main/java/org/apache/fory/serializer/ExceptionSerializers.java
index 5efb771ff..dc22b47ed 100644
--- 
a/java/fory-core/src/main/java/org/apache/fory/serializer/ExceptionSerializers.java
+++ 
b/java/fory-core/src/main/java/org/apache/fory/serializer/ExceptionSerializers.java
@@ -100,6 +100,7 @@ public final class ExceptionSerializers {
         writeContext.writeRef(suppressedException);
       }
       buffer.writeVarUInt32(0);
+      writeNumClassLayers(buffer, slotsSerializers);
       for (Serializer slotsSerializer : slotsSerializers) {
         slotsSerializer.write(writeContext, value);
       }
@@ -214,6 +215,10 @@ public final class ExceptionSerializers {
         readContext.readRef();
       }
     }
+
+    private void writeNumClassLayers(MemoryBuffer buffer, Serializer[] 
slotsSerializers) {
+      buffer.writeVarUInt32Small7(slotsSerializers.length);
+    }
   }
 
   public static final class StackTraceElementSerializer extends 
Serializer<StackTraceElement> {
@@ -425,6 +430,7 @@ public final class ExceptionSerializers {
 
   private static void readAndSetFields(
       ReadContext readContext, Object target, Serializer[] slotsSerializers, 
Config config) {
+    readAndCheckNumClassLayers(readContext, target.getClass(), 
slotsSerializers.length);
     for (Serializer slotsSerializer : slotsSerializers) {
       if (slotsSerializer instanceof CompatibleLayerSerializer) {
         CompatibleLayerSerializer compatibleSerializer =
@@ -439,6 +445,23 @@ public final class ExceptionSerializers {
     }
   }
 
+  private static void readAndCheckNumClassLayers(
+      ReadContext readContext, Class<?> type, int expectedNumClassLayers) {
+    MemoryBuffer buffer = readContext.getBuffer();
+    int numClassLayers = buffer.readVarUInt32Small7();
+    if (numClassLayers != expectedNumClassLayers) {
+      // Layer payloads do not carry per-layer class identity here, so 
mismatches cannot be skipped
+      // safely. Fail before consuming field payloads.
+      throw new ForyException(
+          "Class layer count mismatch for Throwable type "
+              + type.getName()
+              + ": expected "
+              + expectedNumClassLayers
+              + ", got "
+              + numClassLayers);
+    }
+  }
+
   private static void readAndSkipLayerClassMeta(ReadContext readContext) {
     MemoryBuffer buffer = readContext.getBuffer();
     MetaReadContext metaReadContext = readContext.getMetaReadContext();
diff --git 
a/java/fory-core/src/main/java/org/apache/fory/serializer/collection/ChildContainerSerializers.java
 
b/java/fory-core/src/main/java/org/apache/fory/serializer/collection/ChildContainerSerializers.java
index b78172dc1..4a80478a3 100644
--- 
a/java/fory-core/src/main/java/org/apache/fory/serializer/collection/ChildContainerSerializers.java
+++ 
b/java/fory-core/src/main/java/org/apache/fory/serializer/collection/ChildContainerSerializers.java
@@ -176,9 +176,7 @@ public class ChildContainerSerializers {
     public Collection onCollectionWrite(WriteContext writeContext, T value) {
       MemoryBuffer buffer = writeContext.getBuffer();
       buffer.writeVarUInt32Small7(value.size());
-      for (Serializer slotsSerializer : slotsSerializers) {
-        slotsSerializer.write(writeContext, value);
-      }
+      writeChildFields(writeContext, value, slotsSerializers);
       return value;
     }
 
@@ -245,9 +243,7 @@ public class ChildContainerSerializers {
       MemoryBuffer buffer = writeContext.getBuffer();
       buffer.writeVarUInt32Small7(value.size());
       writeContext.writeRef(value.comparator());
-      for (Serializer slotsSerializer : slotsSerializers) {
-        slotsSerializer.write(writeContext, value);
-      }
+      writeChildFields(writeContext, value, slotsSerializers);
       return value;
     }
 
@@ -293,9 +289,7 @@ public class ChildContainerSerializers {
       MemoryBuffer buffer = writeContext.getBuffer();
       buffer.writeVarUInt32Small7(value.size());
       writeContext.writeRef(value.comparator());
-      for (Serializer slotsSerializer : slotsSerializers) {
-        slotsSerializer.write(writeContext, value);
-      }
+      writeChildFields(writeContext, value, slotsSerializers);
       return value;
     }
 
@@ -352,9 +346,7 @@ public class ChildContainerSerializers {
     public Map onMapWrite(WriteContext writeContext, T value) {
       MemoryBuffer buffer = writeContext.getBuffer();
       buffer.writeVarUInt32Small7(value.size());
-      for (Serializer slotsSerializer : slotsSerializers) {
-        slotsSerializer.write(writeContext, value);
-      }
+      writeChildFields(writeContext, value, slotsSerializers);
       return value;
     }
 
@@ -405,9 +397,7 @@ public class ChildContainerSerializers {
       MemoryBuffer buffer = writeContext.getBuffer();
       buffer.writeVarUInt32Small7(value.size());
       writeContext.writeRef(value.comparator());
-      for (Serializer slotsSerializer : slotsSerializers) {
-        slotsSerializer.write(writeContext, value);
-      }
+      writeChildFields(writeContext, value, slotsSerializers);
       return value;
     }
 
@@ -651,6 +641,7 @@ public class ChildContainerSerializers {
       TypeResolver typeResolver,
       Object collection,
       Serializer[] slotsSerializers) {
+    readAndCheckNumClassLayers(readContext, collection.getClass(), 
slotsSerializers.length);
     for (Serializer slotsSerializer : slotsSerializers) {
       if (slotsSerializer instanceof CompatibleLayerSerializer) {
         CompatibleLayerSerializer compatibleSerializer =
@@ -667,6 +658,32 @@ public class ChildContainerSerializers {
     }
   }
 
+  private static void writeChildFields(
+      WriteContext writeContext, Object collection, Serializer[] 
slotsSerializers) {
+    MemoryBuffer buffer = writeContext.getBuffer();
+    buffer.writeVarUInt32Small7(slotsSerializers.length);
+    for (Serializer slotsSerializer : slotsSerializers) {
+      slotsSerializer.write(writeContext, collection);
+    }
+  }
+
+  private static void readAndCheckNumClassLayers(
+      ReadContext readContext, Class<?> type, int expectedNumClassLayers) {
+    MemoryBuffer buffer = readContext.getBuffer();
+    int numClassLayers = buffer.readVarUInt32Small7();
+    if (numClassLayers != expectedNumClassLayers) {
+      // Layer payloads do not carry per-layer class identity here, so 
mismatches cannot be skipped
+      // safely. Fail before consuming field payloads.
+      throw new ForyException(
+          "Class layer count mismatch for child container type "
+              + type.getName()
+              + ": expected "
+              + expectedNumClassLayers
+              + ", got "
+              + numClassLayers);
+    }
+  }
+
   /**
    * Read and skip the layer class meta from buffer. This is used to skip over 
the class definition
    * that was written by CompatibleLayerSerializer.writeLayerClassMeta(). For
diff --git 
a/java/fory-core/src/test/java/org/apache/fory/builder/StaticCompatibleCodecBuilderTest.java
 
b/java/fory-core/src/test/java/org/apache/fory/builder/StaticCompatibleCodecBuilderTest.java
index 23a9731b3..8289a6f5a 100644
--- 
a/java/fory-core/src/test/java/org/apache/fory/builder/StaticCompatibleCodecBuilderTest.java
+++ 
b/java/fory-core/src/test/java/org/apache/fory/builder/StaticCompatibleCodecBuilderTest.java
@@ -47,6 +47,7 @@ import org.apache.fory.context.MetaReadContext;
 import org.apache.fory.context.MetaWriteContext;
 import org.apache.fory.meta.TypeDef;
 import org.apache.fory.platform.GraalvmSupport;
+import org.apache.fory.reflect.TypeRef;
 import org.apache.fory.resolver.TypeResolver;
 import org.apache.fory.serializer.Serializer;
 import org.mockito.MockedStatic;
@@ -62,6 +63,47 @@ public class StaticCompatibleCodecBuilderTest {
     return new Object[][] {{false}, {true}};
   }
 
+  @Test
+  public void testForyDebugAnnotationEnablesStaticCompatibleTracing() throws 
Exception {
+    CompilationResult debugResult =
+        compile(
+            "test.StaticCompatibleDebugPayload",
+            "package test;\n"
+                + "import org.apache.fory.annotation.ForyDebug;\n"
+                + "import org.apache.fory.annotation.ForyStruct;\n"
+                + "@ForyStruct\n"
+                + "@ForyDebug\n"
+                + "public class StaticCompatibleDebugPayload {\n"
+                + "  public int id;\n"
+                + "  public StaticCompatibleDebugPayload() {}\n"
+                + "}\n");
+    CompilationResult plainResult =
+        compile(
+            "test.StaticCompatiblePlainPayload",
+            "package test;\n"
+                + "import org.apache.fory.annotation.ForyStruct;\n"
+                + "@ForyStruct\n"
+                + "public class StaticCompatiblePlainPayload {\n"
+                + "  public int id;\n"
+                + "  public StaticCompatiblePlainPayload() {}\n"
+                + "}\n");
+    Assert.assertTrue(debugResult.success, debugResult.diagnostics());
+    Assert.assertTrue(plainResult.success, plainResult.diagnostics());
+    try (URLClassLoader debugLoader = debugResult.classLoader();
+        URLClassLoader plainLoader = plainResult.classLoader()) {
+      assertStaticCompatibleDebugTracing(
+          debugLoader,
+          debugLoader.loadClass("test.StaticCompatibleDebugPayload"),
+          true,
+          "debug-reader");
+      assertStaticCompatibleDebugTracing(
+          plainLoader,
+          plainLoader.loadClass("test.StaticCompatiblePlainPayload"),
+          false,
+          "plain-reader");
+    }
+  }
+
   @Test(dataProvider = "xlangModes")
   public void testStaticCompatibleSerializerReadsRuntimeRemoteTypeDef(boolean 
xlang)
       throws Exception {
@@ -394,6 +436,15 @@ public class StaticCompatibleCodecBuilderTest {
     return fory;
   }
 
+  private static void assertStaticCompatibleDebugTracing(
+      ClassLoader classLoader, Class<?> type, boolean expectedDebug, String 
role) {
+    Fory fory = compatibleFory(classLoader, type, false, role);
+    TypeDef typeDef = TypeDef.buildTypeDef(fory.getTypeResolver(), type);
+    String generatedSource =
+        new StaticCompatibleCodecBuilder(TypeRef.of(type), fory, 
typeDef).genCode();
+    Assert.assertEquals(generatedSource.contains("debugRemoteReadField(\""), 
expectedDebug);
+  }
+
   @SuppressWarnings("unchecked")
   private static Class<Object> cast(Class<?> type) {
     return (Class<Object>) type;
diff --git 
a/java/fory-core/src/test/java/org/apache/fory/serializer/ExceptionSerializersTest.java
 
b/java/fory-core/src/test/java/org/apache/fory/serializer/ExceptionSerializersTest.java
index ff1b4cd39..2b0505e07 100644
--- 
a/java/fory-core/src/test/java/org/apache/fory/serializer/ExceptionSerializersTest.java
+++ 
b/java/fory-core/src/test/java/org/apache/fory/serializer/ExceptionSerializersTest.java
@@ -26,6 +26,7 @@ import org.apache.fory.Fory;
 import org.apache.fory.ForyTestBase;
 import org.apache.fory.context.ReadContext;
 import org.apache.fory.context.WriteContext;
+import org.apache.fory.exception.ForyException;
 import org.apache.fory.memory.MemoryBuffer;
 import org.apache.fory.memory.MemoryUtils;
 import org.apache.fory.reflect.ReflectionUtils;
@@ -174,6 +175,7 @@ public class ExceptionSerializersTest extends ForyTestBase {
     writeContext.writeStringRef(value.getMessage());
     payload.writeVarUInt32(0);
     payload.writeVarUInt32(0);
+    payload.writeVarUInt32Small7(2);
 
     payload.readerIndex(0);
     ReadContext readContext = fory.getReadContext();
@@ -233,6 +235,29 @@ public class ExceptionSerializersTest extends ForyTestBase 
{
     Assert.assertEquals(copy.getSuppressed()[0].getMessage(), 
"suppressed-custom");
   }
 
+  @Test
+  @SuppressWarnings("unchecked")
+  public void testThrowableRejectsMismatchedClassLayerCount() {
+    Fory fory = 
builder().withRefTracking(false).withCodegen(false).withCompatible(true).build();
+    MemoryBuffer payload = MemoryUtils.buffer(128);
+    WriteContext writeContext = fory.getWriteContext();
+    writeContext.prepare(payload, null);
+    writeContext.writeRef(null);
+    writeContext.writeRef(null);
+    writeContext.writeStringRef("layer-count");
+    payload.writeVarUInt32(0);
+    payload.writeVarUInt32(0);
+    payload.writeVarUInt32Small7(0);
+
+    payload.readerIndex(0);
+    ReadContext readContext = fory.getReadContext();
+    readContext.prepare(payload, null, false);
+    Serializer<CustomException> serializer =
+        (Serializer<CustomException>) 
fory.getTypeResolver().getSerializer(CustomException.class);
+
+    Assert.assertThrows(ForyException.class, () -> 
serializer.read(readContext));
+  }
+
   private static RuntimeException buildTryWithResourcesException() {
     try {
       try (FailingCloseable ignored = new FailingCloseable()) {
diff --git 
a/java/fory-core/src/test/java/org/apache/fory/serializer/collection/ChildContainerSerializersTest.java
 
b/java/fory-core/src/test/java/org/apache/fory/serializer/collection/ChildContainerSerializersTest.java
index 96d22fc8a..8ec956085 100644
--- 
a/java/fory-core/src/test/java/org/apache/fory/serializer/collection/ChildContainerSerializersTest.java
+++ 
b/java/fory-core/src/test/java/org/apache/fory/serializer/collection/ChildContainerSerializersTest.java
@@ -48,6 +48,11 @@ import lombok.EqualsAndHashCode;
 import lombok.NoArgsConstructor;
 import org.apache.fory.Fory;
 import org.apache.fory.ForyTestBase;
+import org.apache.fory.context.ReadContext;
+import org.apache.fory.exception.ForyException;
+import org.apache.fory.memory.MemoryBuffer;
+import org.apache.fory.memory.MemoryUtils;
+import org.apache.fory.serializer.Serializer;
 import org.apache.fory.test.bean.Cyclic;
 import org.testng.Assert;
 import org.testng.annotations.DataProvider;
@@ -128,6 +133,23 @@ public class ChildContainerSerializersTest extends 
ForyTestBase {
     }
   }
 
+  @Test
+  @SuppressWarnings("unchecked")
+  public void testChildCollectionRejectsMismatchedClassLayerCount() {
+    Fory fory = 
builder().withRefTracking(false).withCodegen(false).withCompatible(true).build();
+    MemoryBuffer payload = MemoryUtils.buffer(32);
+    payload.writeVarUInt32Small7(0);
+    payload.writeVarUInt32Small7(0);
+
+    payload.readerIndex(0);
+    ReadContext readContext = fory.getReadContext();
+    readContext.prepare(payload, null, false);
+    Serializer<ChildArrayList> serializer =
+        (Serializer<ChildArrayList>) 
fory.getTypeResolver().getSerializer(ChildArrayList.class);
+
+    Assert.assertThrows(ForyException.class, () -> 
serializer.read(readContext));
+  }
+
   @Test(dataProvider = "foryCopyConfig")
   public void testChildCollectionCopy(Fory fory) {
     List<Object> data = ImmutableList.of(1, true, "test", Cyclic.create(true));


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to