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.math;
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  
24  import java.io.ByteArrayInputStream;
25  import java.io.InvalidObjectException;
26  import java.io.ObjectInputStream;
27  import java.util.HashMap;
28  import java.util.Map;
29  import java.util.Objects;
30  
31  import org.apache.commons.lang3.SerializationException;
32  import org.apache.commons.lang3.SerializationUtils;
33  import org.apache.commons.lang3.SerializationUtilsTest;
34  import org.apache.commons.lang3.reflect.FieldUtils;
35  import org.junit.jupiter.api.Test;
36  
37  /**
38   * Tests that a serialized {@link Fraction} can't store a bad cached hashCode.
39   */
40  public class FractionReadObjectTest {
41  
42      private static Object deserialize(final byte[] bytes) throws Exception {
43          try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes))) {
44              return ois.readObject();
45          }
46      }
47  
48      private static void setInt(final Object target, final String name, final int value) throws Exception {
49          FieldUtils.writeDeclaredField(target, name, value, true);
50      }
51  
52      @Test
53      public void testBadHashCodeStreamIsRejected() throws Exception {
54          final Fraction fraction = Fraction.getFraction(3, 7);
55          final byte[] bytes = SerializationUtils.serialize(fraction);
56          final int hashCode = (Integer) FieldUtils.readDeclaredField(fraction, "hashCode", true);
57          final byte[] edited = SerializationUtilsTest.replaceLastInt(bytes, hashCode, 0xCAFEBABE);
58          final SerializationException ex = assertThrows(SerializationException.class, () -> SerializationUtils.deserialize(edited),
59                  "Bad hashCode in stream must be rejected with InvalidObjectException");
60          assertInstanceOf(InvalidObjectException.class, ex.getCause());
61          assertEquals("java.io.InvalidObjectException: Fraction hashCode does not match numerator/denominator.", ex.getMessage());
62      }
63  
64      /**
65       * Forged stream: numerator=0, denominator=0, hashCode=hash(0,0). The public factory refuses denominator 0 (negative control); readObject does not, so the
66       * forged Fraction(0,0) deserializes and divides by zero on {@code intValue()}.
67       */
68      @Test
69      void testForgedZeroDenominatorDividesByZero() throws Exception {
70          // Negative control: no public factory can build a zero-denominator Fraction.
71          assertThrows(ArithmeticException.class, () -> Fraction.getFraction(0, 0));
72          final Fraction seed = Fraction.getFraction(1, 2);
73          setInt(seed, "numerator", 0);
74          setInt(seed, "denominator", 0);
75          setInt(seed, "hashCode", Objects.hash(Integer.valueOf(0), Integer.valueOf(0)));
76          assertThrows(ArithmeticException.class, () -> deserialize(SerializationUtils.serialize(seed)));
77          assertThrows(ArithmeticException.class, () -> SerializationUtils.roundtrip(seed));
78      }
79  
80      @Test
81      public void testHashMapLookupAfterRoundTrip() throws Exception {
82          final Fraction fraction = Fraction.getFraction(1, 4);
83          final byte[] bytes = SerializationUtils.serialize(fraction);
84          final Fraction deserialized = SerializationUtils.deserialize(bytes);
85          final Map<Fraction, String> map = new HashMap<>();
86          map.put(fraction, "quarter");
87          assertEquals("quarter", map.get(deserialized), "HashMap lookup must work after deserialization");
88      }
89  
90      @Test
91      public void testRoundTripPreservesHashCode() throws Exception {
92          final Fraction fraction = Fraction.getFraction(1, 4);
93          final Fraction roundtrip = SerializationUtils.roundtrip(fraction);
94          assertEquals(fraction.hashCode(), roundtrip.hashCode(), "Round-trip serialization must preserve the correct hashCode");
95          assertEquals(fraction, roundtrip);
96      }
97  }