View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      https://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.commons.lang3.util;
19  
20  import static org.junit.jupiter.api.Assertions.assertEquals;
21  import static org.junit.jupiter.api.Assertions.assertInstanceOf;
22  import static org.junit.jupiter.api.Assertions.assertThrows;
23  import static org.junit.jupiter.api.Assertions.assertTrue;
24  
25  import java.io.ByteArrayInputStream;
26  import java.io.ByteArrayOutputStream;
27  import java.io.IOException;
28  import java.io.InvalidObjectException;
29  import java.io.ObjectInputStream;
30  import java.io.ObjectOutputStream;
31  import java.io.ObjectStreamClass;
32  import java.io.Serializable;
33  import java.util.BitSet;
34  
35  import org.apache.commons.lang3.SerializationException;
36  import org.apache.commons.lang3.SerializationUtils;
37  import org.junit.jupiter.api.Test;
38  
39  /**
40   * Tests {@link FluentBitSet#readObject(ObjectInputStream)}.
41   * <p>
42   * Covers normal round-trip serialization as well as rejection of a forged stream in which the {@code bitSet} field is {@code null}.
43   * </p>
44   */
45  class FluentBitSetReadObjectTest {
46  
47      /**
48       * Forge class: mirrors the field layout of {@link FluentBitSet} (same field name, type, and {@code serialVersionUID}) but declares {@code bitSet} as
49       * non-final so it can be set to {@code null}, which the real constructor rejects.
50       */
51      private static final class FluentBitSetForge implements Serializable {
52  
53          private static final long serialVersionUID = 1L; // must match FluentBitSet.serialVersionUID
54  
55          // Non-final so we can construct an instance with bitSet == null.
56          @SuppressWarnings("unused")
57          private final BitSet bitSet;
58  
59          FluentBitSetForge(final BitSet bitSet) {
60              this.bitSet = bitSet;
61          }
62      }
63  
64      /**
65       * Deserializes an object from the given byte array using a plain {@link ObjectInputStream}.
66       */
67      private static Object deserialize(final byte[] bytes) throws IOException, ClassNotFoundException {
68          try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes))) {
69              return ois.readObject();
70          }
71      }
72  
73      /**
74       * Builds a serialized stream that looks like a {@link FluentBitSet} (matching class name and {@code serialVersionUID}) but carries {@code bitSet == null}.
75       */
76      private static byte[] forgeNullBitSetStream() throws IOException {
77          final ByteArrayOutputStream baos = new ByteArrayOutputStream();
78          try (ObjectOutputStream oos = new ObjectOutputStream(baos) {
79  
80              @Override
81              protected void writeClassDescriptor(final ObjectStreamClass desc) throws IOException {
82                  if (desc.getName().equals(FluentBitSetForge.class.getName())) {
83                      // Emit the descriptor for FluentBitSet so the stream deserializes as that class.
84                      super.writeClassDescriptor(ObjectStreamClass.lookup(FluentBitSet.class));
85                  } else {
86                      super.writeClassDescriptor(desc);
87                  }
88              }
89          }) {
90              oos.writeObject(new FluentBitSetForge(null));
91          }
92          return baos.toByteArray();
93      }
94  
95      /**
96       * Tests that a deserialized {@link FluentBitSet} remains fully functional (bits can be read and modified) after round-trip serialization.
97       */
98      @Test
99      void testDeserializedInstanceIsMutable() {
100         final FluentBitSet original = new FluentBitSet().set(5);
101         final FluentBitSet roundtrip = SerializationUtils.roundtrip(original);
102         assertTrue(roundtrip.get(5));
103         roundtrip.set(6);
104         assertTrue(roundtrip.get(6), "Deserialized instance must remain mutable");
105         // The original must not be affected.
106         assertInstanceOf(FluentBitSet.class, roundtrip);
107         assertTrue(original.get(5));
108     }
109 
110     /**
111      * Tests that a forged stream with {@code bitSet == null} is rejected with {@link InvalidObjectException} when deserialized directly via
112      * {@link ObjectInputStream}.
113      */
114     @Test
115     void testNullBitSetRejectedByObjectInputStream() throws Exception {
116         final byte[] forged = forgeNullBitSetStream();
117         final Exception ex = assertThrows(InvalidObjectException.class, () -> deserialize(forged));
118         assertTrue(ex.getMessage().contains("bitSet null"));
119     }
120 
121     /**
122      * Tests that a forged stream with {@code bitSet == null} is rejected with {@link SerializationException} wrapping an {@link InvalidObjectException} when
123      * deserialized via {@link SerializationUtils#deserialize(byte[])}.
124      */
125     @Test
126     void testNullBitSetRejectedBySerializationUtils() throws Exception {
127         final byte[] forged = forgeNullBitSetStream();
128         final SerializationException ex = assertThrows(SerializationException.class, () -> SerializationUtils.deserialize(forged));
129         assertInstanceOf(InvalidObjectException.class, ex.getCause());
130         assertTrue(ex.getCause().getMessage().contains("bitSet null"));
131     }
132 
133     /**
134      * Tests that round-trip serialization of an empty {@link FluentBitSet} produces an equal, empty instance.
135      */
136     @Test
137     void testRoundTripEmptyBitSet() {
138         final FluentBitSet original = new FluentBitSet();
139         final FluentBitSet roundtrip = SerializationUtils.roundtrip(original);
140         assertEquals(original, roundtrip);
141         assertTrue(roundtrip.isEmpty());
142     }
143 
144     /**
145      * Tests that round-trip serialization of a {@link FluentBitSet} with scattered bits set preserves all bit values.
146      */
147     @Test
148     void testRoundTripPreservesBits() {
149         final FluentBitSet original = new FluentBitSet().set(1, 3, 5, 7, 100);
150         final FluentBitSet roundtrip = SerializationUtils.roundtrip(original);
151         assertEquals(original, roundtrip);
152         assertEquals(original.bitSet(), roundtrip.bitSet());
153     }
154 
155     /**
156      * Tests that round-trip serialization preserves the hash code of a {@link FluentBitSet}.
157      */
158     @Test
159     void testRoundTripPreservesHashCode() {
160         final FluentBitSet original = new FluentBitSet().set(2, 4, 8, 16);
161         assertEquals(original.hashCode(), SerializationUtils.roundtrip(original).hashCode());
162     }
163 
164     /**
165      * Tests that a {@link FluentBitSet} wrapping a {@link BitSet} that was constructed via {@link BitSet#valueOf(long[])} survives a round-trip serialization.
166      */
167     @Test
168     void testRoundTripWithBitSetValueOf() {
169         final BitSet bs = BitSet.valueOf(new long[] { 0b1010_1010L });
170         final FluentBitSet original = new FluentBitSet(bs);
171         assertEquals(original, SerializationUtils.roundtrip(original));
172     }
173 
174     /**
175      * Tests that round-trip serialization of a {@link FluentBitSet} with a large bit index preserves the full contents.
176      */
177     @Test
178     void testRoundTripWithHighBitIndex() {
179         final FluentBitSet original = new FluentBitSet(256).set(0, 127, 255);
180         final FluentBitSet roundtrip = SerializationUtils.roundtrip(original);
181         assertEquals(original, roundtrip);
182         assertTrue(roundtrip.get(0));
183         assertTrue(roundtrip.get(127));
184         assertTrue(roundtrip.get(255));
185     }
186 
187     /**
188      * Tests that two independently deserialized instances of the same {@link FluentBitSet} are equal, ensuring readObject leaves the object in a consistent
189      * state.
190      */
191     @Test
192     void testTwoDeserializedInstancesAreEqual() {
193         final FluentBitSet original = new FluentBitSet().set(10, 20, 30);
194         final byte[] bytes = SerializationUtils.serialize(original);
195         final FluentBitSet first = SerializationUtils.deserialize(bytes);
196         final FluentBitSet second = SerializationUtils.deserialize(bytes);
197         assertEquals(first, second);
198         assertEquals(first.bitSet(), second.bitSet());
199     }
200 }