From a7ff45c828ddbc173e3e7dcd7605752e4da1e5c8 Mon Sep 17 00:00:00 2001 From: Sebastian Mandrean Date: Tue, 28 Apr 2026 13:11:54 +0200 Subject: [PATCH 1/3] test(java): reproduce boxed long record pool corruption --- .../RecordSerializersTest.java | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/java/fory-latest-jdk-tests/src/test/java/org/apache/fory/integration_tests/RecordSerializersTest.java b/java/fory-latest-jdk-tests/src/test/java/org/apache/fory/integration_tests/RecordSerializersTest.java index ff54a6ad36..531f49486b 100644 --- a/java/fory-latest-jdk-tests/src/test/java/org/apache/fory/integration_tests/RecordSerializersTest.java +++ b/java/fory-latest-jdk-tests/src/test/java/org/apache/fory/integration_tests/RecordSerializersTest.java @@ -28,7 +28,10 @@ import java.util.Arrays; import java.util.List; import org.apache.fory.Fory; +import org.apache.fory.ThreadSafeFory; import org.apache.fory.config.CompatibleMode; +import org.apache.fory.config.ForyBuilder; +import org.apache.fory.config.Language; import org.apache.fory.context.MetaReadContext; import org.apache.fory.context.MetaWriteContext; import org.apache.fory.test.bean.Struct; @@ -42,6 +45,8 @@ public class RecordSerializersTest { public record Foo(int f1, String f2, List f3, char f4) {} + public record NumberCompressedPayload(Long longValue, String stringValue) {} + @Test public void testIsRecord() { Assert.assertTrue(RecordUtils.isRecord(Foo.class)); @@ -80,6 +85,37 @@ public void testSimpleRecord(boolean codegen) { Assert.assertEquals(fory.deserialize(fory.serialize(foo)), foo); } + @Test + public void testNumberCompressedBoxedLongRecordRoundTripAcrossPools() { + ThreadSafeFory writer = newNumberCompressedPool(); + ThreadSafeFory reader = newNumberCompressedPool(); + + NumberCompressedPayload payload = + new NumberCompressedPayload(123_456_789L, "longer string with multibyte: \u00ff\u00fe"); + + byte[] bytes = writer.serialize(payload); + Assert.assertEquals(reader.deserialize(bytes), payload); + } + + private static ThreadSafeFory newNumberCompressedPool() { + return newNumberCompressedBuilder().buildThreadSafeForyPool(4); + } + + private static ForyBuilder newNumberCompressedBuilder() { + return Fory.builder() + .withLanguage(Language.JAVA) + .withCodegen(true) + .withAsyncCompilation(false) + .requireClassRegistration(false) + .suppressClassRegistrationWarnings(true) + .withDeserializeUnknownClass(true) + .withRefTracking(true) + .withCompatibleMode(CompatibleMode.COMPATIBLE) + .withStringCompressed(true) + .withNumberCompressed(true) + .withRefCopy(true); + } + @Test(dataProvider = "codegen") public void testSimpleRecordMetaShared(boolean codegen) { Fory fory = From 2425d5123ec07c02f40dc284972160034dc17c59 Mon Sep 17 00:00:00 2001 From: Sebastian Mandrean Date: Tue, 28 Apr 2026 13:17:42 +0200 Subject: [PATCH 2/3] fix(java): honor record field encoding in generated decode --- .../org/apache/fory/builder/BaseObjectCodecBuilder.java | 6 ++---- .../src/main/java/org/apache/fory/codegen/Expression.java | 7 +++++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java b/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java index e7aee5ee69..a05527725b 100644 --- a/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java +++ b/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java @@ -1856,17 +1856,15 @@ protected Expression deserializeForNullableField( } else { if (typeRef.isPrimitive() && !nullable) { // Only skip null check if BOTH: local type is primitive AND sender didn't write null flag - Expression value = deserializeForNotNull(buffer, typeRef, null); + Expression value = deserializeForNotNullForField(buffer, descriptor, null); // Should put value expr ahead to avoid generated code in wrong scope. return new ListExpression(value, callback.apply(value)); } - // Pass local field type so readNullable can use default value for primitives when null - Class localFieldType = typeRef.isPrimitive() ? typeRef.getRawType() : null; return readNullableField( buffer, descriptor, callback, - () -> deserializeForNotNull(buffer, typeRef, null), + () -> deserializeForNotNullForField(buffer, descriptor, null), nullable); } } diff --git a/java/fory-core/src/main/java/org/apache/fory/codegen/Expression.java b/java/fory-core/src/main/java/org/apache/fory/codegen/Expression.java index 25ce0b294f..e1311a4c83 100644 --- a/java/fory-core/src/main/java/org/apache/fory/codegen/Expression.java +++ b/java/fory-core/src/main/java/org/apache/fory/codegen/Expression.java @@ -56,6 +56,7 @@ import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Locale; import java.util.stream.Collectors; import java.util.stream.Stream; import org.apache.fory.memory.Platform; @@ -430,7 +431,8 @@ public ExprCode doGenCode(CodegenContext ctx) { return new ExprCode( FalseLiteral, new LiteralValue(javaType, "Float.NEGATIVE_INFINITY")); } else { - return new ExprCode(FalseLiteral, new LiteralValue(javaType, String.format("%fF", f))); + return new ExprCode( + FalseLiteral, new LiteralValue(javaType, String.format(Locale.ROOT, "%fF", f))); } } else if (javaType == Double.class) { Double d = (Double) value; @@ -443,7 +445,8 @@ public ExprCode doGenCode(CodegenContext ctx) { return new ExprCode( FalseLiteral, new LiteralValue(javaType, "Double.NEGATIVE_INFINITY")); } else { - return new ExprCode(FalseLiteral, new LiteralValue(javaType, String.format("%fD", d))); + return new ExprCode( + FalseLiteral, new LiteralValue(javaType, String.format(Locale.ROOT, "%fD", d))); } } else if (javaType == Byte.class) { return new ExprCode( From efe6e64b046b2b8cef258a2533e983e1c4451a70 Mon Sep 17 00:00:00 2001 From: Sebastian Mandrean Date: Tue, 28 Apr 2026 13:33:23 +0200 Subject: [PATCH 3/3] test(java): cover boxed primitive compatible record corruption --- .../RecordSerializersTest.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/java/fory-latest-jdk-tests/src/test/java/org/apache/fory/integration_tests/RecordSerializersTest.java b/java/fory-latest-jdk-tests/src/test/java/org/apache/fory/integration_tests/RecordSerializersTest.java index 531f49486b..71a4f515e6 100644 --- a/java/fory-latest-jdk-tests/src/test/java/org/apache/fory/integration_tests/RecordSerializersTest.java +++ b/java/fory-latest-jdk-tests/src/test/java/org/apache/fory/integration_tests/RecordSerializersTest.java @@ -22,6 +22,7 @@ import static org.apache.fory.collection.Collections.ofArrayList; import static org.apache.fory.collection.Collections.ofHashMap; +import java.io.Serializable; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.ArrayList; @@ -47,6 +48,9 @@ public record Foo(int f1, String f2, List f3, char f4) {} public record NumberCompressedPayload(Long longValue, String stringValue) {} + public record BoxedPrimitiveRecord(String lensId, Long from, Long to, Integer type) + implements Serializable {} + @Test public void testIsRecord() { Assert.assertTrue(RecordUtils.isRecord(Foo.class)); @@ -97,6 +101,25 @@ public void testNumberCompressedBoxedLongRecordRoundTripAcrossPools() { Assert.assertEquals(reader.deserialize(bytes), payload); } + @Test + public void testCompatibleCodegenBoxedPrimitiveRecordRoundTrip() { + Fory fory = + Fory.builder() + .withLanguage(Language.JAVA) + .requireClassRegistration(false) + .withCompatibleMode(CompatibleMode.COMPATIBLE) + .withClassVersionCheck(true) + .withCodegen(true) + .build(); + BoxedPrimitiveRecord record = + new BoxedPrimitiveRecord( + "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee:11111111-2222-3333-4444-555555555555", + 123456789012345L, + 98765432109876L, + 146); + Assert.assertEquals(fory.deserialize(fory.serialize(record)), record); + } + private static ThreadSafeFory newNumberCompressedPool() { return newNumberCompressedBuilder().buildThreadSafeForyPool(4); }