This is an automated email from the ASF dual-hosted git repository.
garydgregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-lang.git
The following commit(s) were added to refs/heads/master by this push:
new eb560776a A serialized Fraction can't store a bad cached hashCode.
(#1634)
eb560776a is described below
commit eb560776a3430a600dacce67239e69420a607501
Author: Gary Gregory <[email protected]>
AuthorDate: Tue May 5 17:45:39 2026 -0400
A serialized Fraction can't store a bad cached hashCode. (#1634)
---
src/main/java/org/apache/commons/lang3/Range.java | 6 +-
.../org/apache/commons/lang3/math/Fraction.java | 25 +++++++-
.../apache/commons/lang3/RangeReadObjectTest.java | 2 +-
.../commons/lang3/SerializationUtilsTest.java | 9 ++-
.../commons/lang3/math/FractionReadObjectTest.java | 70 ++++++++++++++++++++++
5 files changed, 105 insertions(+), 7 deletions(-)
diff --git a/src/main/java/org/apache/commons/lang3/Range.java
b/src/main/java/org/apache/commons/lang3/Range.java
index 5200b4ee5..2c1743956 100644
--- a/src/main/java/org/apache/commons/lang3/Range.java
+++ b/src/main/java/org/apache/commons/lang3/Range.java
@@ -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();
diff --git a/src/main/java/org/apache/commons/lang3/math/Fraction.java
b/src/main/java/org/apache/commons/lang3/math/Fraction.java
index b88082640..b0a9476f4 100644
--- a/src/main/java/org/apache/commons/lang3/math/Fraction.java
+++ b/src/main/java/org/apache/commons/lang3/math/Fraction.java
@@ -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;
@@ -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.
*
@@ -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);
}
/**
@@ -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>
diff --git a/src/test/java/org/apache/commons/lang3/RangeReadObjectTest.java
b/src/test/java/org/apache/commons/lang3/RangeReadObjectTest.java
index 9976e94a1..866270b59 100644
--- a/src/test/java/org/apache/commons/lang3/RangeReadObjectTest.java
+++ b/src/test/java/org/apache/commons/lang3/RangeReadObjectTest.java
@@ -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 {
diff --git a/src/test/java/org/apache/commons/lang3/SerializationUtilsTest.java
b/src/test/java/org/apache/commons/lang3/SerializationUtilsTest.java
index 6fe4d24ce..0702f41f1 100644
--- a/src/test/java/org/apache/commons/lang3/SerializationUtilsTest.java
+++ b/src/test/java/org/apache/commons/lang3/SerializationUtilsTest.java
@@ -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();
@@ -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;
diff --git
a/src/test/java/org/apache/commons/lang3/math/FractionReadObjectTest.java
b/src/test/java/org/apache/commons/lang3/math/FractionReadObjectTest.java
new file mode 100644
index 000000000..ecbc944c6
--- /dev/null
+++ b/src/test/java/org/apache/commons/lang3/math/FractionReadObjectTest.java
@@ -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);
+
+ }
+}