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

garydgregory pushed a commit to branch 1.X
in repository https://gitbox.apache.org/repos/asf/commons-beanutils.git


The following commit(s) were added to refs/heads/1.X by this push:
     new 3c0f9a9b Add tests to avoid breaking the binary format of serialized 
objects.
3c0f9a9b is described below

commit 3c0f9a9b8683a843d9379202b2be2aa42a3b9f84
Author: Gary Gregory <[email protected]>
AuthorDate: Mon Jul 27 20:03:06 2026 +0000

    Add tests to avoid breaking the binary format of serialized objects.
---
 .../beanutils/DynaPropertySerializationTest.java   | 141 +++++++++++++++++++++
 1 file changed, 141 insertions(+)

diff --git 
a/src/test/java/org/apache/commons/beanutils/DynaPropertySerializationTest.java 
b/src/test/java/org/apache/commons/beanutils/DynaPropertySerializationTest.java
index 65387bed..a8dff23a 100644
--- 
a/src/test/java/org/apache/commons/beanutils/DynaPropertySerializationTest.java
+++ 
b/src/test/java/org/apache/commons/beanutils/DynaPropertySerializationTest.java
@@ -23,6 +23,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertNotSame;
 import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
@@ -30,6 +31,7 @@ import java.io.IOException;
 import java.io.ObjectInputStream;
 import java.io.ObjectOutputStream;
 import java.io.StreamCorruptedException;
+import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
@@ -340,4 +342,143 @@ class DynaPropertySerializationTest {
                 "Expected StreamCorruptedException for unrecognised primitive 
contentType constant");
         assertInstanceOf(StreamCorruptedException.class, thrown, "Root cause 
must be StreamCorruptedException");
     }
+
+    /**
+     * Returns the byte-offset of the first occurrence of {@code needle} 
inside {@code haystack}, or {@code -1} if not found.
+     */
+    private static int findSequence(final byte[] haystack, final byte[] 
needle) {
+        outer: for (int i = 0; i <= haystack.length - needle.length; i++) {
+            for (int j = 0; j < needle.length; j++) {
+                if (haystack[i + j] != needle[j]) {
+                    continue outer;
+                }
+            }
+            return i;
+        }
+        return -1;
+    }
+
+    /**
+     * Proves that {@link DynaProperty#writeObject(ObjectOutputStream)} calls 
{@code writeAnyClass} (which encodes the {@code type} field) <em>before</em>
+     * {@code defaultWriteObject} (which encodes the {@code name} field) for 
<strong>primitive types</strong>.
+     * <p>
+     * Strategy: serialise two {@link DynaProperty} instances that have an 
identical {@code name} but different primitive types. Because the name is 
identical,
+     * both byte streams are the same from the class-descriptor through to the 
end of the default-field data. The streams diverge only at the
+     * {@code writeAnyClass} output (the primitive-type integer constant). If 
that divergence point is located <em>before</em> the name bytes in the stream, 
it
+     * proves that {@code writeAnyClass} is called first.
+     * </p>
+     */
+    @Test
+    void testWireFormatPrimitiveTypeDataPrecedesNameField() throws Exception {
+        final String sharedName = "sharedPrimitiveName";
+        // Boolean.TYPE encodes as writeBoolean(true) + 
writeInt(BOOLEAN_TYPE=1).
+        // Byte.TYPE encodes as writeBoolean(true) + writeInt(BYTE_TYPE=2).
+        // The two streams are byte-for-byte identical except at the 
primitive-type int.
+        final byte[] boolBytes = serialize(new DynaProperty(sharedName, 
Boolean.TYPE));
+        final byte[] byteBytes = serialize(new DynaProperty(sharedName, 
Byte.TYPE));
+        // Locate the shared name in the boolean-type stream.
+        final byte[] nameBytes = sharedName.getBytes(StandardCharsets.UTF_8);
+        final int namePosition = findSequence(boolBytes, nameBytes);
+        assertTrue(namePosition > 0, "Property name must be present in the 
serialized stream");
+        // The name must occupy the same position in both streams (identical 
name, identical class).
+        assertEquals(namePosition, findSequence(byteBytes, nameBytes), "Name 
must be at the same byte position in both streams");
+        // Find the first byte where the two streams diverge: this is inside 
the
+        // writeAnyClass output (the primitive-type integer constant differs: 
1 vs 2).
+        int firstDiff = -1;
+        for (int i = 0; i < boolBytes.length; i++) {
+            if (boolBytes[i] != byteBytes[i]) {
+                firstDiff = i;
+                break;
+            }
+        }
+        assertTrue(firstDiff >= 0, "Streams must diverge at the primitive-type 
constant");
+        // CRITICAL assertion: the divergence point (type data from 
writeAnyClass) must
+        // come BEFORE the name field (from defaultWriteObject).
+        assertTrue(firstDiff < namePosition, "writeAnyClass must be invoked 
before defaultWriteObject: " + "type-data divergence at byte " + firstDiff
+                + " must precede name field at byte " + namePosition);
+        // Sanity-check: both properties still round-trip correctly.
+        assertRoundTrip(new DynaProperty(sharedName, Boolean.TYPE));
+        assertRoundTrip(new DynaProperty(sharedName, Byte.TYPE));
+    }
+
+    /**
+     * Same proof as {@link 
#testWireFormatPrimitiveTypeDataPrecedesNameField()} but for <strong>object 
(non-primitive) types</strong>.
+     * <p>
+     * For object types {@code writeAnyClass} emits {@code 
writeBoolean(false)} followed by {@code writeObject(clazz)}. Two properties 
with the same name but
+     * different object types ({@code String.class} vs {@code Integer.class}) 
differ in the class object written by {@code writeAnyClass}; the shared name is
+     * written later by {@code defaultWriteObject}.
+     * </p>
+     */
+    @Test
+    void testWireFormatObjectTypeDataPrecedesNameField() throws Exception {
+        final String sharedName = "sharedObjectName";
+        // writeAnyClass for object type: writeBoolean(false) + 
writeObject(clazz).
+        // String.class and Integer.class are serialised differently, so 
streams diverge
+        // at the class-object position (inside writeAnyClass output).
+        final byte[] stringTypeBytes = serialize(new DynaProperty(sharedName, 
String.class));
+        final byte[] integerTypeBytes = serialize(new DynaProperty(sharedName, 
Integer.class));
+        // Find the shared name in both streams.
+        final byte[] nameBytes = sharedName.getBytes(StandardCharsets.UTF_8);
+        final int namePositionInString = findSequence(stringTypeBytes, 
nameBytes);
+        final int namePositionInInteger = findSequence(integerTypeBytes, 
nameBytes);
+        assertTrue(namePositionInString > 0, "Name must be present in the 
String-type stream");
+        assertTrue(namePositionInInteger > 0, "Name must be present in the 
Integer-type stream");
+        // Find the first byte where the streams diverge (inside writeAnyClass 
output,
+        // where the serialised form of String.class differs from 
Integer.class).
+        int firstDiff = -1;
+        for (int i = 0; i < stringTypeBytes.length; i++) {
+            if (stringTypeBytes[i] != integerTypeBytes[i]) {
+                firstDiff = i;
+                break;
+            }
+        }
+        assertTrue(firstDiff >= 0, "Streams must diverge where the class 
objects differ");
+        // The divergence (type class data from writeAnyClass) must precede 
the name
+        // (from defaultWriteObject) in BOTH streams.
+        assertTrue(firstDiff < namePositionInString, "writeAnyClass must be 
invoked before defaultWriteObject (String-type stream): "
+                + "type divergence at byte " + firstDiff + " must precede name 
at byte " + namePositionInString);
+        assertTrue(firstDiff < namePositionInInteger, "writeAnyClass must be 
invoked before defaultWriteObject (Integer-type stream): "
+                + "type divergence at byte " + firstDiff + " must precede name 
at byte " + namePositionInInteger);
+        assertRoundTrip(new DynaProperty(sharedName, String.class));
+        assertRoundTrip(new DynaProperty(sharedName, Integer.class));
+    }
+
+    /**
+     * Proves that for <strong>indexed and mapped properties</strong> the 
second {@code writeAnyClass} call (for {@code contentType}) also appears in the 
byte
+     * stream <em>before</em> the {@code name} field written by {@code 
defaultWriteObject}.
+     * <p>
+     * Two mapped properties share the same name and the same {@code 
Map.class} type but differ in their {@code contentType} ({@code Boolean.TYPE} vs
+     * {@code Byte.TYPE}). Because the type ({@code Map.class}) is identical, 
the streams are the same through the first {@code writeAnyClass} call. They
+     * diverge at the second {@code writeAnyClass} call (contentType 
constant), which must still precede the name.
+     * </p>
+     */
+    @Test
+    void testWireFormatContentTypeDataPrecedesNameFieldForMappedProperty() 
throws Exception {
+        final String sharedName = "sharedMappedName";
+        // Both use Map.class as the type (identical first writeAnyClass 
output).
+        // They differ only in contentType: Boolean.TYPE (constant 1) vs 
Byte.TYPE (constant 2).
+        final byte[] boolContentBytes = serialize(new DynaProperty(sharedName, 
Map.class, Boolean.TYPE));
+        final byte[] byteContentBytes = serialize(new DynaProperty(sharedName, 
Map.class, Byte.TYPE));
+        final byte[] nameBytes = sharedName.getBytes(StandardCharsets.UTF_8);
+        final int namePosition = findSequence(boolContentBytes, nameBytes);
+        assertTrue(namePosition > 0, "Name must be present in the serialized 
stream");
+        assertEquals(namePosition, findSequence(byteContentBytes, nameBytes), 
"Name must be at the same byte position in both streams");
+        // The streams are identical up to (and including) the type encoding 
of Map.class.
+        // They diverge at the contentType constant written by the second 
writeAnyClass call.
+        int firstDiff = -1;
+        for (int i = 0; i < boolContentBytes.length; i++) {
+            if (boolContentBytes[i] != byteContentBytes[i]) {
+                firstDiff = i;
+                break;
+            }
+        }
+        assertTrue(firstDiff >= 0, "Streams must diverge at the contentType 
primitive-type constant");
+        // The second writeAnyClass output (contentType) must still precede 
the name
+        // (defaultWriteObject), confirming that both writeAnyClass calls 
happen before
+        // defaultWriteObject is invoked.
+        assertTrue(firstDiff < namePosition, "The second writeAnyClass call 
(contentType) must precede defaultWriteObject: "
+                + "content-type divergence at byte " + firstDiff + " must 
precede name at byte " + namePosition);
+        assertRoundTrip(new DynaProperty(sharedName, Map.class, Boolean.TYPE));
+        assertRoundTrip(new DynaProperty(sharedName, Map.class, Byte.TYPE));
+    }
 }

Reply via email to