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

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


The following commit(s) were added to refs/heads/master by this push:
     new bcf09d227 FluentBitSet.readObject(ObjectInputStream) should validate 
the same (#1694)
bcf09d227 is described below

commit bcf09d227765e788e09344ef5173b6c2e8fb4870
Author: Gary Gregory <[email protected]>
AuthorDate: Mon Jun 8 16:33:13 2026 -0400

    FluentBitSet.readObject(ObjectInputStream) should validate the same (#1694)
    
    invariants as constructors
---
 .../apache/commons/lang3/util/FluentBitSet.java    |  20 +++
 .../lang3/util/FluentBitSetReadObjectTest.java     | 200 +++++++++++++++++++++
 2 files changed, 220 insertions(+)

diff --git a/src/main/java/org/apache/commons/lang3/util/FluentBitSet.java 
b/src/main/java/org/apache/commons/lang3/util/FluentBitSet.java
index 314cf0a6e..374da9fff 100644
--- a/src/main/java/org/apache/commons/lang3/util/FluentBitSet.java
+++ b/src/main/java/org/apache/commons/lang3/util/FluentBitSet.java
@@ -16,11 +16,17 @@
  */
 package org.apache.commons.lang3.util;
 
+import java.io.IOException;
+import java.io.InvalidObjectException;
+import java.io.NotActiveException;
+import java.io.ObjectInputStream;
 import java.io.Serializable;
 import java.util.BitSet;
 import java.util.Objects;
 import java.util.stream.IntStream;
 
+import org.apache.commons.lang3.SerializationUtils;
+
 /**
  * A fluent {@link BitSet} with additional operations.
  * <p>
@@ -416,6 +422,20 @@ public int previousSetBit(final int fromIndex) {
         return bitSet.previousSetBit(fromIndex);
     }
 
+    /**
+     * Reads and restores the state of the object.
+     *
+     * @param in the source stream.
+     * @throws ClassNotFoundException if the class of a serialized object 
could not be found.
+     * @throws IOException            if an I/O error occurs.
+     * @throws NotActiveException     if the stream is not currently reading 
objects.
+     * @throws InvalidObjectException if {@code bitSet} is {@code null}.
+     */
+    private void readObject(final ObjectInputStream in) throws IOException, 
ClassNotFoundException {
+        in.defaultReadObject();
+        SerializationUtils.requireNonNull(bitSet, "bitSet null");
+    }
+
     /**
      * Sets the bit at the specified indexes to {@code true}.
      *
diff --git 
a/src/test/java/org/apache/commons/lang3/util/FluentBitSetReadObjectTest.java 
b/src/test/java/org/apache/commons/lang3/util/FluentBitSetReadObjectTest.java
new file mode 100644
index 000000000..fd378e1be
--- /dev/null
+++ 
b/src/test/java/org/apache/commons/lang3/util/FluentBitSetReadObjectTest.java
@@ -0,0 +1,200 @@
+/*
+ * 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
+ *
+ *      https://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.commons.lang3.util;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InvalidObjectException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.ObjectStreamClass;
+import java.io.Serializable;
+import java.util.BitSet;
+
+import org.apache.commons.lang3.SerializationException;
+import org.apache.commons.lang3.SerializationUtils;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests {@link FluentBitSet#readObject(ObjectInputStream)}.
+ * <p>
+ * Covers normal round-trip serialization as well as rejection of a forged 
stream in which the {@code bitSet} field is {@code null}.
+ * </p>
+ */
+class FluentBitSetReadObjectTest {
+
+    /**
+     * Forge class: mirrors the field layout of {@link FluentBitSet} (same 
field name, type, and {@code serialVersionUID}) but declares {@code bitSet} as
+     * non-final so it can be set to {@code null}, which the real constructor 
rejects.
+     */
+    private static final class FluentBitSetForge implements Serializable {
+
+        private static final long serialVersionUID = 1L; // must match 
FluentBitSet.serialVersionUID
+
+        // Non-final so we can construct an instance with bitSet == null.
+        @SuppressWarnings("unused")
+        private final BitSet bitSet;
+
+        FluentBitSetForge(final BitSet bitSet) {
+            this.bitSet = bitSet;
+        }
+    }
+
+    /**
+     * Deserializes an object from the given byte array using a plain {@link 
ObjectInputStream}.
+     */
+    private static Object deserialize(final byte[] bytes) throws IOException, 
ClassNotFoundException {
+        try (ObjectInputStream ois = new ObjectInputStream(new 
ByteArrayInputStream(bytes))) {
+            return ois.readObject();
+        }
+    }
+
+    /**
+     * Builds a serialized stream that looks like a {@link FluentBitSet} 
(matching class name and {@code serialVersionUID}) but carries {@code bitSet == 
null}.
+     */
+    private static byte[] forgeNullBitSetStream() throws IOException {
+        final ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        try (ObjectOutputStream oos = new ObjectOutputStream(baos) {
+
+            @Override
+            protected void writeClassDescriptor(final ObjectStreamClass desc) 
throws IOException {
+                if (desc.getName().equals(FluentBitSetForge.class.getName())) {
+                    // Emit the descriptor for FluentBitSet so the stream 
deserializes as that class.
+                    
super.writeClassDescriptor(ObjectStreamClass.lookup(FluentBitSet.class));
+                } else {
+                    super.writeClassDescriptor(desc);
+                }
+            }
+        }) {
+            oos.writeObject(new FluentBitSetForge(null));
+        }
+        return baos.toByteArray();
+    }
+
+    /**
+     * Tests that a deserialized {@link FluentBitSet} remains fully functional 
(bits can be read and modified) after round-trip serialization.
+     */
+    @Test
+    void testDeserializedInstanceIsMutable() {
+        final FluentBitSet original = new FluentBitSet().set(5);
+        final FluentBitSet roundtrip = SerializationUtils.roundtrip(original);
+        assertTrue(roundtrip.get(5));
+        roundtrip.set(6);
+        assertTrue(roundtrip.get(6), "Deserialized instance must remain 
mutable");
+        // The original must not be affected.
+        assertInstanceOf(FluentBitSet.class, roundtrip);
+        assertTrue(original.get(5));
+    }
+
+    /**
+     * Tests that a forged stream with {@code bitSet == null} is rejected with 
{@link InvalidObjectException} when deserialized directly via
+     * {@link ObjectInputStream}.
+     */
+    @Test
+    void testNullBitSetRejectedByObjectInputStream() throws Exception {
+        final byte[] forged = forgeNullBitSetStream();
+        final Exception ex = assertThrows(InvalidObjectException.class, () -> 
deserialize(forged));
+        assertTrue(ex.getMessage().contains("bitSet null"));
+    }
+
+    /**
+     * Tests that a forged stream with {@code bitSet == null} is rejected with 
{@link SerializationException} wrapping an {@link InvalidObjectException} when
+     * deserialized via {@link SerializationUtils#deserialize(byte[])}.
+     */
+    @Test
+    void testNullBitSetRejectedBySerializationUtils() throws Exception {
+        final byte[] forged = forgeNullBitSetStream();
+        final SerializationException ex = 
assertThrows(SerializationException.class, () -> 
SerializationUtils.deserialize(forged));
+        assertInstanceOf(InvalidObjectException.class, ex.getCause());
+        assertTrue(ex.getCause().getMessage().contains("bitSet null"));
+    }
+
+    /**
+     * Tests that round-trip serialization of an empty {@link FluentBitSet} 
produces an equal, empty instance.
+     */
+    @Test
+    void testRoundTripEmptyBitSet() {
+        final FluentBitSet original = new FluentBitSet();
+        final FluentBitSet roundtrip = SerializationUtils.roundtrip(original);
+        assertEquals(original, roundtrip);
+        assertTrue(roundtrip.isEmpty());
+    }
+
+    /**
+     * Tests that round-trip serialization of a {@link FluentBitSet} with 
scattered bits set preserves all bit values.
+     */
+    @Test
+    void testRoundTripPreservesBits() {
+        final FluentBitSet original = new FluentBitSet().set(1, 3, 5, 7, 100);
+        final FluentBitSet roundtrip = SerializationUtils.roundtrip(original);
+        assertEquals(original, roundtrip);
+        assertEquals(original.bitSet(), roundtrip.bitSet());
+    }
+
+    /**
+     * Tests that round-trip serialization preserves the hash code of a {@link 
FluentBitSet}.
+     */
+    @Test
+    void testRoundTripPreservesHashCode() {
+        final FluentBitSet original = new FluentBitSet().set(2, 4, 8, 16);
+        assertEquals(original.hashCode(), 
SerializationUtils.roundtrip(original).hashCode());
+    }
+
+    /**
+     * Tests that a {@link FluentBitSet} wrapping a {@link BitSet} that was 
constructed via {@link BitSet#valueOf(long[])} survives a round-trip 
serialization.
+     */
+    @Test
+    void testRoundTripWithBitSetValueOf() {
+        final BitSet bs = BitSet.valueOf(new long[] { 0b1010_1010L });
+        final FluentBitSet original = new FluentBitSet(bs);
+        assertEquals(original, SerializationUtils.roundtrip(original));
+    }
+
+    /**
+     * Tests that round-trip serialization of a {@link FluentBitSet} with a 
large bit index preserves the full contents.
+     */
+    @Test
+    void testRoundTripWithHighBitIndex() {
+        final FluentBitSet original = new FluentBitSet(256).set(0, 127, 255);
+        final FluentBitSet roundtrip = SerializationUtils.roundtrip(original);
+        assertEquals(original, roundtrip);
+        assertTrue(roundtrip.get(0));
+        assertTrue(roundtrip.get(127));
+        assertTrue(roundtrip.get(255));
+    }
+
+    /**
+     * Tests that two independently deserialized instances of the same {@link 
FluentBitSet} are equal, ensuring readObject leaves the object in a consistent
+     * state.
+     */
+    @Test
+    void testTwoDeserializedInstancesAreEqual() {
+        final FluentBitSet original = new FluentBitSet().set(10, 20, 30);
+        final byte[] bytes = SerializationUtils.serialize(original);
+        final FluentBitSet first = SerializationUtils.deserialize(bytes);
+        final FluentBitSet second = SerializationUtils.deserialize(bytes);
+        assertEquals(first, second);
+        assertEquals(first.bitSet(), second.bitSet());
+    }
+}

Reply via email to