Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/main/java/org/apache/commons/lang3/Range.java
Original file line number Diff line number Diff line change
Expand Up @@ -535,11 +535,13 @@ public boolean isStartedBy(final T element) {
}

/**
* See {@link Serializable}.
* Validates the cached hashCode after deserialization. Throws a {@link InvalidObjectException} when the stored hashCode does not match the canonical hash
* of the deserialized minimum/maximum.
*
* @param in See {@link Serializable}.
* @throws IOException See {@link Serializable}.
* @throws IOException See {@link Serializable}.
* @throws ClassNotFoundException See {@link Serializable}.
* @throws InvalidObjectException If the hashCode doesn't match the minimum and maximum.
*/
private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
Expand Down
25 changes: 24 additions & 1 deletion src/main/java/org/apache/commons/lang3/math/Fraction.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
*/
package org.apache.commons.lang3.math;

import java.io.IOException;
import java.io.InvalidObjectException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.math.BigInteger;
import java.util.Objects;
Expand Down Expand Up @@ -402,6 +405,10 @@ private static int greatestCommonDivisor(int u, int v) {
return -u * (1 << k); // gcd is u*2^k
}

private static int hash(final int value1, final int value2) {
return Objects.hash(value1, value2);
}

/**
* Multiplies two integers, checking for overflow.
*
Expand Down Expand Up @@ -489,7 +496,7 @@ private static int subAndCheck(final int x, final int y) {
private Fraction(final int numerator, final int denominator) {
this.numerator = numerator;
this.denominator = denominator;
this.hashCode = Objects.hash(denominator, numerator);
this.hashCode = hash(denominator, numerator);
}

/**
Expand Down Expand Up @@ -837,6 +844,22 @@ public Fraction pow(final int power) {
return f.pow(power / 2).multiplyBy(this);
}

/**
* Validates the cached hashCode after deserialization. Throws a {@link InvalidObjectException} when the stored hashCode does not match the canonical hash
* of the deserialized numerator/denominator.
*
* @param in See {@link Serializable}.
* @throws IOException See {@link Serializable}.
* @throws ClassNotFoundException See {@link Serializable}.
* @throws InvalidObjectException If the hashCode doesn't match the denominator and numerator.
*/
private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
if (hashCode != hash(denominator, numerator)) {
throw new InvalidObjectException("Fraction hashCode does not match numerator/denominator.");
}
}

/**
* Reduce the fraction to the smallest values for the numerator and denominator, returning the result.
* <p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
import org.junit.jupiter.api.Test;

/**
* Tests that a serialized Range can't store a bad cached hashCode.
* Tests that a serialized {@link Range} can't store a bad cached hashCode.
*/
class RangeReadObjectTest {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,15 +61,17 @@ interface SerializableSupplier<T> extends Supplier<T>, Serializable {
/**
* Tests {@link SerializationUtils}.
*/
class SerializationUtilsTest extends AbstractLangTest {
public class SerializationUtilsTest extends AbstractLangTest {

static final String CLASS_NOT_FOUND_MESSAGE = "ClassNotFoundSerialization.readObject fake exception";

protected static final String SERIALIZE_IO_EXCEPTION_MESSAGE = "Anonymous OutputStream I/O exception";

static byte[] intToBytes(final int v) {
public static byte[] intToBytes(final int v) {
return new byte[] { (byte) (v >>> 24), (byte) (v >>> 16), (byte) (v >>> 8), (byte) v };
}
static byte[] replaceLastInt(final byte[] src, final int from, final int to) {

public static byte[] replaceLastInt(final byte[] src, final int from, final int to) {
final byte[] fromB = intToBytes(from);
final byte[] toB = intToBytes(to);
final byte[] out = src.clone();
Expand All @@ -85,6 +87,7 @@ static byte[] replaceLastInt(final byte[] src, final int from, final int to) {
fail("No legitimate int in stream, serialization must keep hashCode in default field set");
return null;
}

private String iString;

private Integer iInteger;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* 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.math;

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 java.io.InvalidObjectException;
import java.util.HashMap;
import java.util.Map;

import org.apache.commons.lang3.SerializationException;
import org.apache.commons.lang3.SerializationUtils;
import org.apache.commons.lang3.SerializationUtilsTest;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.junit.jupiter.api.Test;

/**
* Tests that a serialized {@link Fraction} can't store a bad cached hashCode.
*/
public class FractionReadObjectTest {

@Test
public void testBadHashCodeStreamIsRejected() throws Exception {
final Fraction fraction = Fraction.getFraction(3, 7);
final byte[] bytes = SerializationUtils.serialize(fraction);
final int hashCode = (Integer) FieldUtils.readDeclaredField(fraction, "hashCode", true);
final byte[] edited = SerializationUtilsTest.replaceLastInt(bytes, hashCode, 0xCAFEBABE);
final SerializationException ex = assertThrows(SerializationException.class, () -> SerializationUtils.deserialize(edited),
"Bad hashCode in stream must be rejected with InvalidObjectException");
assertInstanceOf(InvalidObjectException.class, ex.getCause());
assertEquals("java.io.InvalidObjectException: Fraction hashCode does not match numerator/denominator.", ex.getMessage());

}

@Test
public void testHashMapLookupAfterRoundTrip() throws Exception {
final Fraction fraction = Fraction.getFraction(1, 4);
final byte[] bytes = SerializationUtils.serialize(fraction);
final Fraction deserialized = SerializationUtils.deserialize(bytes);
final Map<Fraction, String> map = new HashMap<>();
map.put(fraction, "quarter");
assertEquals("quarter", map.get(deserialized), "HashMap lookup must work after deserialization");
}

@Test
public void testRoundTripPreservesHashCode() throws Exception {
final Fraction fraction = Fraction.getFraction(1, 4);
final Fraction roundtrip = SerializationUtils.roundtrip(fraction);
assertEquals(fraction.hashCode(), roundtrip.hashCode(), "Round-trip serialization must preserve the correct hashCode");
assertEquals(fraction, roundtrip);

}
}
Loading