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;
19  
20  import static org.junit.jupiter.api.Assertions.assertEquals;
21  import static org.junit.jupiter.api.Assertions.assertNotSame;
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.Serializable;
32  import java.util.Base64;
33  import java.util.Collections;
34  import java.util.Comparator;
35  import java.util.Objects;
36  
37  import org.apache.commons.lang3.reflect.FieldUtils;
38  import org.junit.jupiter.api.Test;
39  
40  /**
41   * Tests range invariants and hash code reconstruction during deserialization.
42   */
43  class RangeReadObjectTest {
44  
45      private static final class ChangingHashEndpoint extends IdentityEndpoint {
46          private static final long serialVersionUID = 1L;
47          private transient int hash = 123;
48  
49          ChangingHashEndpoint(final int value) {
50              super(value);
51          }
52  
53          @Override
54          public boolean equals(final Object other) {
55              return this == other;
56          }
57  
58          @Override
59          public int hashCode() {
60              return hash;
61          }
62      }
63  
64      private static class IdentityEndpoint implements Serializable, Comparable<IdentityEndpoint> {
65          private static final long serialVersionUID = 1L;
66          private final int value;
67  
68          IdentityEndpoint(final int value) {
69              this.value = value;
70          }
71  
72          @Override
73          public int compareTo(final IdentityEndpoint other) {
74              return Integer.compare(value, other.value);
75          }
76      }
77  
78      /**
79       * Standin class used only to drive {@link ObjectOutputStream#writeObject(Object)} into emitting a stream that matches the wire format of {@link Range} but
80       * with caller-controlled field values. The class name and serialVersionUID are spoofed in the stream below via a custom {@link ObjectOutputStream} subclass
81       * so the stream reads back as a {@code Range}.
82       */
83      private static final class RangeForge implements Serializable {
84  
85          private static final long serialVersionUID = 1L; // matches Range.serialVersionUID
86          private final Object comparator;
87          private final Object maximum;
88          private final Object minimum;
89  
90          RangeForge(final Object comparator, final Object minimum, final Object maximum) {
91              this.comparator = comparator;
92              this.minimum = minimum;
93              this.maximum = maximum;
94          }
95      }
96  
97      private static Object deserialize(final byte[] bytes) throws IOException, ClassNotFoundException {
98          try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes))) {
99              return ois.readObject();
100         }
101     }
102 
103     /**
104      * Serializes a {@link RangeForge} but rewrites the class descriptor name to "org.apache.commons.lang3.Range" so the resulting bytes deserialize as a
105      * {@link Range}. Because the field set, types, order, and serialVersionUID all match, default deserialization assigns each forged value to the
106      * corresponding Range field via reflection (bypassing the constructor).
107      */
108     private static byte[] forgeRangeStream(final Object comparator, final Object minimum, final Object maximum) throws IOException {
109         // Build the legitimate-shape bytes via RangeForge, then rewrite the embedded class name.
110         final ByteArrayOutputStream baos = new ByteArrayOutputStream();
111         try (ObjectOutputStream oos = new ObjectOutputStream(baos) {
112 
113             @Override
114             protected void writeClassDescriptor(final java.io.ObjectStreamClass desc) throws IOException {
115                 if (desc.getName().equals(RangeForge.class.getName())) {
116                     // Emit a descriptor whose name is Range but whose field layout still matches RangeForge.
117                     final java.io.ObjectStreamClass spoofed = java.io.ObjectStreamClass.lookup(Range.class);
118                     super.writeClassDescriptor(spoofed);
119                 } else {
120                     super.writeClassDescriptor(desc);
121                 }
122             }
123         }) {
124             oos.writeObject(new RangeForge(comparator, minimum, maximum));
125         }
126         return baos.toByteArray();
127     }
128 
129     @Test
130     void testCachedHashCodeRecomputed() throws Exception {
131         final Range<Integer> range = Range.of(1, 100);
132         FieldUtils.writeDeclaredField(range, "hashCode", 0xDEADBEEF, true);
133         final Range<Integer> copy = SerializationUtils.roundtrip(range);
134         assertEquals(Range.of(1, 100).hashCode(), copy.hashCode());
135         assertEquals(range, copy);
136     }
137 
138     @Test
139     void testCloneIdentityHashEndpoints() {
140         final Range<IdentityEndpoint> original = Range.of(new IdentityEndpoint(1), new IdentityEndpoint(2));
141         final Range<IdentityEndpoint> copy = SerializationUtils.clone(original);
142         assertNotSame(original.getMinimum(), copy.getMinimum());
143         assertNotSame(original.getMaximum(), copy.getMaximum());
144         assertEquals(0, original.getMinimum().compareTo(copy.getMinimum()));
145         assertEquals(0, original.getMaximum().compareTo(copy.getMaximum()));
146         assertEquals(Objects.hash(copy.getMinimum(), copy.getMaximum()), copy.hashCode());
147     }
148 
149     @Test
150     void testComparatorNullViaForgedStream() throws Exception {
151         final Integer min = Integer.valueOf(1);
152         final Integer max = Integer.valueOf(10);
153         final byte[] forged = forgeRangeStream(null, min, max);
154         assertThrows(InvalidObjectException.class, () -> deserialize(forged));
155     }
156 
157     @Test
158     void testDeserializeVersion320() {
159         // Streams generated using Commons Lang 3.20.0, with endpoints 1 and 2.
160         final Range<?>[] expected = {Range.of(1, 2), IntegerRange.of(1, 2), LongRange.of(1, 2), DoubleRange.of(1, 2)};
161         final String[] streams = {
162             "rO0ABXNyAB5vcmcuYXBhY2hlLmNvbW1vbnMubGFuZzMuUmFuZ2UAAAAAAAAAAQIAA0wACmNvbXBhcmF0b3J0ABZMamF2YS91dGls" +
163                 "L0NvbXBhcmF0b3I7TAAHbWF4aW11bXQAEkxqYXZhL2xhbmcvT2JqZWN0O0wAB21pbmltdW1xAH4AAnhwfnIAM29yZy5hcGFjaGUu" +
164                 "Y29tbW9ucy5sYW5nMy5SYW5nZSRDb21wYXJhYmxlQ29tcGFyYXRvcgAAAAAAAAAAEgAAeHIADmphdmEubGFuZy5FbnVtAAAAAAAA" +
165                 "AAASAAB4cHQACElOU1RBTkNFc3IAEWphdmEubGFuZy5JbnRlZ2VyEuKgpPeBhzgCAAFJAAV2YWx1ZXhyABBqYXZhLmxhbmcuTnVt" +
166                 "YmVyhqyVHQuU4IsCAAB4cAAAAAJzcQB+AAgAAAAB",
167             "rO0ABXNyACVvcmcuYXBhY2hlLmNvbW1vbnMubGFuZzMuSW50ZWdlclJhbmdlAAAAAAAAAAECAAB4cgAkb3JnLmFwYWNoZS5jb21t" +
168                 "b25zLmxhbmczLk51bWJlclJhbmdlAAAAAAAAAAECAAB4cgAeb3JnLmFwYWNoZS5jb21tb25zLmxhbmczLlJhbmdlAAAAAAAAAAEC" +
169                 "AANMAApjb21wYXJhdG9ydAAWTGphdmEvdXRpbC9Db21wYXJhdG9yO0wAB21heGltdW10ABJMamF2YS9sYW5nL09iamVjdDtMAAdt" +
170                 "aW5pbXVtcQB+AAR4cH5yADNvcmcuYXBhY2hlLmNvbW1vbnMubGFuZzMuUmFuZ2UkQ29tcGFyYWJsZUNvbXBhcmF0b3IAAAAAAAAA" +
171                 "ABIAAHhyAA5qYXZhLmxhbmcuRW51bQAAAAAAAAAAEgAAeHB0AAhJTlNUQU5DRXNyABFqYXZhLmxhbmcuSW50ZWdlchLioKT3gYc4" +
172                 "AgABSQAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAAACc3EAfgAKAAAAAQ==",
173             "rO0ABXNyACJvcmcuYXBhY2hlLmNvbW1vbnMubGFuZzMuTG9uZ1JhbmdlAAAAAAAAAAECAAB4cgAkb3JnLmFwYWNoZS5jb21tb25z" +
174                 "LmxhbmczLk51bWJlclJhbmdlAAAAAAAAAAECAAB4cgAeb3JnLmFwYWNoZS5jb21tb25zLmxhbmczLlJhbmdlAAAAAAAAAAECAANM" +
175                 "AApjb21wYXJhdG9ydAAWTGphdmEvdXRpbC9Db21wYXJhdG9yO0wAB21heGltdW10ABJMamF2YS9sYW5nL09iamVjdDtMAAdtaW5p" +
176                 "bXVtcQB+AAR4cH5yADNvcmcuYXBhY2hlLmNvbW1vbnMubGFuZzMuUmFuZ2UkQ29tcGFyYWJsZUNvbXBhcmF0b3IAAAAAAAAAABIA" +
177                 "AHhyAA5qYXZhLmxhbmcuRW51bQAAAAAAAAAAEgAAeHB0AAhJTlNUQU5DRXNyAA5qYXZhLmxhbmcuTG9uZzuL5JDMjyPfAgABSgAF" +
178                 "dmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAAAAAAAAAnNxAH4ACgAAAAAAAAAB",
179             "rO0ABXNyACRvcmcuYXBhY2hlLmNvbW1vbnMubGFuZzMuRG91YmxlUmFuZ2UAAAAAAAAAAQIAAHhyACRvcmcuYXBhY2hlLmNvbW1v" +
180                 "bnMubGFuZzMuTnVtYmVyUmFuZ2UAAAAAAAAAAQIAAHhyAB5vcmcuYXBhY2hlLmNvbW1vbnMubGFuZzMuUmFuZ2UAAAAAAAAAAQIA" +
181                 "A0wACmNvbXBhcmF0b3J0ABZMamF2YS91dGlsL0NvbXBhcmF0b3I7TAAHbWF4aW11bXQAEkxqYXZhL2xhbmcvT2JqZWN0O0wAB21p" +
182                 "bmltdW1xAH4ABHhwfnIAM29yZy5hcGFjaGUuY29tbW9ucy5sYW5nMy5SYW5nZSRDb21wYXJhYmxlQ29tcGFyYXRvcgAAAAAAAAAA" +
183                 "EgAAeHIADmphdmEubGFuZy5FbnVtAAAAAAAAAAASAAB4cHQACElOU1RBTkNFc3IAEGphdmEubGFuZy5Eb3VibGWAs8JKKWv7BAIA" +
184                 "AUQABXZhbHVleHIAEGphdmEubGFuZy5OdW1iZXKGrJUdC5TgiwIAAHhwQAAAAAAAAABzcQB+AAo/8AAAAAAAAA=="
185         };
186         for (int i = 0; i < streams.length; i++) {
187             final Range<?> actual = SerializationUtils.deserialize(Base64.getDecoder().decode(streams[i]));
188             assertEquals(expected[i].getClass(), actual.getClass());
189             assertEquals(expected[i], actual);
190             assertEquals(expected[i].hashCode(), actual.hashCode());
191         }
192     }
193 
194     @Test
195     void testForgedReversedComparatorBreaksContains() throws Exception {
196         final Range<Integer> reference = Range.of(Integer.valueOf(1), Integer.valueOf(10));
197         final Range<Integer> forged = Range.of(Integer.valueOf(1), Integer.valueOf(10));
198         final Comparator<Integer> reversed = Collections.reverseOrder();
199         FieldUtils.writeDeclaredField(forged, "comparator", reversed, true);
200         assertThrows(InvalidObjectException.class, () -> deserialize(SerializationUtils.serialize(forged)));
201         assertThrows(SerializationException.class, () -> SerializationUtils.deserialize(SerializationUtils.serialize(forged)));
202         assertThrows(SerializationException.class, () -> SerializationUtils.roundtrip(forged));
203         assertTrue(reference.contains(Integer.valueOf(5)));
204     }
205 
206     @Test
207     void testMaximumNullViaForgedStream() throws Exception {
208         final Integer min = Integer.valueOf(1);
209         final Object comparator = Range.of(Integer.valueOf(1), Integer.valueOf(2)).getComparator();
210         final byte[] forged = forgeRangeStream(comparator, min, null);
211         assertThrows(InvalidObjectException.class, () -> deserialize(forged));
212     }
213 
214     @Test
215     void testMinimumNullViaForgedStream() throws Exception {
216         final Integer max = Integer.valueOf(10);
217         // comparator must be non-null here so we isolate the minimum-null gap.
218         // We use ComparableComparator.INSTANCE via deserialization round-trip of a real Range.
219         final Object comparator = Range.of(Integer.valueOf(1), Integer.valueOf(2)).getComparator();
220         final byte[] forged = forgeRangeStream(comparator, null, max);
221         assertThrows(InvalidObjectException.class, () -> deserialize(forged));
222     }
223 
224 
225     @Test
226     void testNaNEndpointViaForgedStream() throws Exception {
227         final Double min = Double.valueOf(5.0);
228         final Double max = Double.valueOf(Double.NaN);
229         final Object comparator = Range.of(Integer.valueOf(1), Integer.valueOf(2)).getComparator();
230         final byte[] forged = forgeRangeStream(comparator, min, max);
231         assertThrows(InvalidObjectException.class, () -> deserialize(forged));
232     }
233 
234     @Test
235     void testRoundTripChangingHashEndpoints() {
236         final Range<ChangingHashEndpoint> original = Range.of(new ChangingHashEndpoint(1), new ChangingHashEndpoint(2));
237         final Range<ChangingHashEndpoint> copy = SerializationUtils.roundtrip(original);
238         assertEquals(123, original.getMinimum().hashCode());
239         assertEquals(0, copy.getMinimum().hashCode());
240         assertEquals(Objects.hash(copy.getMinimum(), copy.getMaximum()), copy.hashCode());
241     }
242 
243     @Test
244     void testRoundTripPreservesCorrectHashCode() throws Exception {
245         final Range<String> range = Range.of("apple", "mango");
246         final Range<String> roundtrip = SerializationUtils.roundtrip(range);
247         assertEquals(range.hashCode(), roundtrip.hashCode(), "Round-trip serialization must preserve the correct hashCode");
248         assertEquals(range, roundtrip);
249     }
250 
251 }