FluentBitSetReadObjectTest.java

/*
 * 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());
    }
}