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 fa2470369 Fix Range serialization compatibility and hash
reconstruction (#1802)
fa2470369 is described below
commit fa2470369bc43d1806f92685d81f49b8d828160d
Author: Gary Gregory <[email protected]>
AuthorDate: Fri Sep 25 15:30:51 2026 -0400
Fix Range serialization compatibility and hash reconstruction (#1802)
Recompute the transient hash cache after deserialization while retaining
endpoint and comparator validation. Restore serialVersionUID 1L to
preserve compatibility with 3.20.0 serialized ranges.
Add regression tests for changing endpoint hashes and legacy streams.
---
RELEASE-NOTES.txt | 4 +-
src/changes/changes.xml | 3 +-
src/main/java/org/apache/commons/lang3/Range.java | 17 +--
.../apache/commons/lang3/RangeReadObjectTest.java | 154 ++++++++++++++-------
4 files changed, 117 insertions(+), 61 deletions(-)
diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt
index 9456a9634..5141daec9 100644
--- a/RELEASE-NOTES.txt
+++ b/RELEASE-NOTES.txt
@@ -151,7 +151,7 @@ Fixed Bugs:
* LANG-1823: LocaleUtils.toLocale cannot parse valid JDK Locale string
containing '#' #1630. Thanks to ????, Gary Gregory.
* LANG-879: LocaleUtils test fails with new Locale "ja_JP_JP_#u-ca-japanese"
of JDK7. Thanks to Matthew T. Adams, Gary Gregory.
* Fix typo in SystemProperties.JDK_XML_ENTITY_REPLACEMENT_LIMIT
(#1631). Thanks to Omkhar Arasaratnam, Gary Gregory.
-* Harden Range.readObject() to reject bad cached hash code (#1633).
Thanks to Omkhar Arasaratnam, Gary Gregory.
+* Recompute Range cached hash codes on deserialization while
retaining endpoint and comparator validation (#1633). Thanks to Omkhar
Arasaratnam, Gary Gregory.
* Harden Fraction.readObject() to reject bad cached hash code
(#1634). Thanks to Omkhar Arasaratnam, Gary Gregory.
* NumberUtils.createNumber(String): Float shortcut can bypass exact
decimal parsing (#1635). Thanks to Omkhar Arasaratnam, Gary Gregory.
* StringUtils.joins() for primitive types can throw OOME before
index check (#1636). Thanks to Omkhar Arasaratnam, Gary Gregory.
@@ -306,6 +306,8 @@ Fixed Bugs:
* Fix NumberUtils.createNumber() and isCreatable() for positive
Long literals with explicit '+' sign (#1796). Thanks to gaurav kumar pandey,
Gary Gregory.
* Fix StrBuilder.lastIndexOf(String) start index for an empty
search (#1759). Thanks to alhudz, Gary Gregory.
* Fix int overflow in Conversion count and position bounds guards
(#1779). Thanks to alhudz, Gary Gregory.
+* Preserve serialization compatibility with 3.20.0 for Range,
IntegerRange, LongRange, and DoubleRange by retaining Range serialVersionUID 1L.
+ Streams written by 3.21.0 RC1 with UID 2L require reading with
RC1 and exporting endpoint and comparator values before rebuilding ranges with
this release.
Changes:
* Bump org.apache.commons:commons-parent from 92 to 105 #1498.
Thanks to Gary Gregory, Dependabot.
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index d2347420c..389a29481 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -137,7 +137,7 @@ The <action> type attribute can be add,update,fix,remove.
<action issue="LANG-1823" type="fix" dev="ggregory" due-to="尹茂椿萱, Gary
Gregory">LocaleUtils.toLocale cannot parse valid JDK Locale string containing
'#' #1630.</action>
<action issue="LANG-879" type="fix" dev="ggregory" due-to="Matthew T.
Adams, Gary Gregory">LocaleUtils test fails with new Locale
"ja_JP_JP_#u-ca-japanese" of JDK7.</action>
<action type="fix" dev="ggregory" due-to="Omkhar
Arasaratnam, Gary Gregory">Fix typo in
SystemProperties.JDK_XML_ENTITY_REPLACEMENT_LIMIT (#1631).</action>
- <action type="fix" dev="ggregory" due-to="Omkhar
Arasaratnam, Gary Gregory">Harden Range.readObject() to reject bad cached hash
code (#1633).</action>
+ <action type="fix" dev="ggregory" due-to="Omkhar
Arasaratnam, Gary Gregory">Recompute Range cached hash codes on deserialization
while retaining endpoint and comparator validation (#1633).</action>
<action type="fix" dev="ggregory" due-to="Omkhar
Arasaratnam, Gary Gregory">Harden Fraction.readObject() to reject bad cached
hash code (#1634).</action>
<action type="fix" dev="ggregory" due-to="Omkhar
Arasaratnam, Gary Gregory">NumberUtils.createNumber(String): Float shortcut can
bypass exact decimal parsing (#1635).</action>
<action type="fix" dev="ggregory" due-to="Omkhar
Arasaratnam, Gary Gregory">StringUtils.joins() for primitive types can throw
OOME before index check (#1636).</action>
@@ -316,6 +316,7 @@ The <action> type attribute can be add,update,fix,remove.
<action type="add" dev="ggregory" due-to="Gary Gregory,
gaurav kumar pandey">Fix TypeUtils.isAssignable() for wildcards with multiple
upper bounds (#1782).</action>
<action type="add" dev="ggregory" due-to="Gary
Gregory">Add JavaVersion.JAVA_28.</action>
<action type="add" dev="ggregory" due-to="Gary
Gregory">Add SystemUtils.IS_JAVA_28.</action>
+ <action type="fix" dev="ggregory" due-to="Gary
Gregory">Preserve serialization compatibility with 3.20.0 for Range,
IntegerRange, LongRange, and DoubleRange by retaining Range serialVersionUID
1L. Streams written by 3.21.0 RC1 with UID 2L require reading with RC1 and
exporting endpoint and comparator values before rebuilding ranges with this
release.</action>
<!-- UPDATE -->
<action type="update" dev="ggregory" due-to="Gary
Gregory, Dependabot">Bump org.apache.commons:commons-parent from 92 to 105
#1498.</action>
<action type="update" dev="ggregory" due-to="Gary
Gregory">[test] Bump org.apache.commons:commons-text from 1.14.0 to
1.15.0.</action>
diff --git a/src/main/java/org/apache/commons/lang3/Range.java
b/src/main/java/org/apache/commons/lang3/Range.java
index 953ab5b1c..b1ca03e71 100644
--- a/src/main/java/org/apache/commons/lang3/Range.java
+++ b/src/main/java/org/apache/commons/lang3/Range.java
@@ -37,7 +37,6 @@
*
* @param <T> The type of range values.
* @since 3.0
- * @since 3.21.0 {@code serialVersionUID} changed from {@code 1L} to {@code
2L}.
*/
public class Range<T> implements Serializable {
@@ -62,9 +61,8 @@ public int compare(final Object obj1, final Object obj2) {
* Serialization version.
*
* @see java.io.Serializable
- * @since 3.21.0 {@code serialVersionUID} changed from {@code 1L} to
{@value}.
*/
- private static final long serialVersionUID = 2L;
+ private static final long serialVersionUID = 1L;
/**
* Creates a range with the specified minimum and maximum values (both
inclusive).
@@ -257,7 +255,7 @@ private static void requireNotNaN(final Object element,
final String name) {
/**
* Cached output hashCode (class is immutable).
*/
- private final int hashCode;
+ private transient int hashCode;
/**
* The maximum value in this range (inclusive).
@@ -613,21 +611,15 @@ public boolean isStartedBy(final T element) {
}
/**
- * 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.
+ * Validates the endpoints and comparator and recomputes the cached hash
code after deserialization.
*
* @param in See {@link Serializable}.
* @throws IOException Thrown as described in {@link Serializable}.
* @throws ClassNotFoundException Thrown as described in {@link
Serializable}.
- * @throws InvalidObjectException Thrown if the hashCode doesn't match the
minimum and maximum.
+ * @throws InvalidObjectException Thrown if the endpoints or comparator
violate the range invariants.
*/
private void readObject(final ObjectInputStream in) throws IOException,
ClassNotFoundException {
in.defaultReadObject();
- // Reject streams whose cached hashCode does not match the canonical
hash of the deserialized minimum/maximum: a crafted stream cannot supply a
forged
- // value.
- if (hashCode != hash(minimum, maximum)) {
- throw new InvalidObjectException("Range hashCode does not match
minimum/maximum.");
- }
SerializationUtils.requireNonNull(maximum, "maximum null");
SerializationUtils.requireNonNull(minimum, "minimum null");
SerializationUtils.requireNonNull(comparator, "comparator null");
@@ -639,6 +631,7 @@ private void readObject(final ObjectInputStream in) throws
IOException, ClassNot
if (comparator.compare(minimum, maximum) > 0) {
throw new InvalidObjectException("Range minimum is greater than
maximum under the comparator.");
}
+ hashCode = hash(minimum, maximum);
}
/**
diff --git a/src/test/java/org/apache/commons/lang3/RangeReadObjectTest.java
b/src/test/java/org/apache/commons/lang3/RangeReadObjectTest.java
index e2a0cdc07..90e32ab00 100644
--- a/src/test/java/org/apache/commons/lang3/RangeReadObjectTest.java
+++ b/src/test/java/org/apache/commons/lang3/RangeReadObjectTest.java
@@ -18,7 +18,7 @@
package org.apache.commons.lang3;
import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -29,6 +29,7 @@
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
+import java.util.Base64;
import java.util.Collections;
import java.util.Comparator;
import java.util.Objects;
@@ -37,10 +38,43 @@
import org.junit.jupiter.api.Test;
/**
- * Tests that a serialized {@link Range} can't store a bad cached hashCode.
+ * Tests range invariants and hash code reconstruction during deserialization.
*/
class RangeReadObjectTest {
+ private static final class ChangingHashEndpoint extends IdentityEndpoint {
+ private static final long serialVersionUID = 1L;
+ private transient int hash = 123;
+
+ ChangingHashEndpoint(final int value) {
+ super(value);
+ }
+
+ @Override
+ public boolean equals(final Object other) {
+ return this == other;
+ }
+
+ @Override
+ public int hashCode() {
+ return hash;
+ }
+ }
+
+ private static class IdentityEndpoint implements Serializable,
Comparable<IdentityEndpoint> {
+ private static final long serialVersionUID = 1L;
+ private final int value;
+
+ IdentityEndpoint(final int value) {
+ this.value = value;
+ }
+
+ @Override
+ public int compareTo(final IdentityEndpoint other) {
+ return Integer.compare(value, other.value);
+ }
+ }
+
/**
* Standin class used only to drive {@link
ObjectOutputStream#writeObject(Object)} into emitting a stream that matches the
wire format of {@link Range} but
* with caller-controlled field values. The class name and
serialVersionUID are spoofed in the stream below via a custom {@link
ObjectOutputStream} subclass
@@ -48,17 +82,15 @@ class RangeReadObjectTest {
*/
private static final class RangeForge implements Serializable {
- private static final long serialVersionUID = 2L; // matches
Range.serialVersionUID
+ private static final long serialVersionUID = 1L; // matches
Range.serialVersionUID
private final Object comparator;
- private final int hashCode;
private final Object maximum;
private final Object minimum;
- RangeForge(final Object comparator, final Object minimum, final Object
maximum, final int hashCode) {
+ RangeForge(final Object comparator, final Object minimum, final Object
maximum) {
this.comparator = comparator;
this.minimum = minimum;
this.maximum = maximum;
- this.hashCode = hashCode;
}
}
@@ -73,7 +105,7 @@ private static Object deserialize(final byte[] bytes) throws
IOException, ClassN
* {@link Range}. Because the field set, types, order, and
serialVersionUID all match, default deserialization assigns each forged value
to the
* corresponding Range field via reflection (bypassing the constructor).
*/
- private static byte[] forgeRangeStream(final Object comparator, final
Object minimum, final Object maximum, final int hashCode) throws IOException {
+ private static byte[] forgeRangeStream(final Object comparator, final
Object minimum, final Object maximum) throws IOException {
// Build the legitimate-shape bytes via RangeForge, then rewrite the
embedded class name.
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (ObjectOutputStream oos = new ObjectOutputStream(baos) {
@@ -89,42 +121,76 @@ protected void writeClassDescriptor(final
java.io.ObjectStreamClass desc) throws
}
}
}) {
- oos.writeObject(new RangeForge(comparator, minimum, maximum,
hashCode));
+ oos.writeObject(new RangeForge(comparator, minimum, maximum));
}
return baos.toByteArray();
}
@Test
- void testBadHashCodeRejected() throws Exception {
+ void testCachedHashCodeRecomputed() throws Exception {
final Range<Integer> range = Range.of(1, 100);
- final byte[] bytes = SerializationUtils.serialize(range);
- // Locate the legitimate hashCode int in the serialized stream and
overwrite it.
- final int hashCode = (Integer) FieldUtils.readDeclaredField(range,
"hashCode", true);
- final byte[] edited = SerializationUtilsTest.replaceLastInt(bytes,
hashCode, 0xDEADBEEF);
- 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: Range hashCode does not
match minimum/maximum.", ex.getMessage());
+ FieldUtils.writeDeclaredField(range, "hashCode", 0xDEADBEEF, true);
+ final Range<Integer> copy = SerializationUtils.roundtrip(range);
+ assertEquals(Range.of(1, 100).hashCode(), copy.hashCode());
+ assertEquals(range, copy);
+ }
+
+ @Test
+ void testCloneIdentityHashEndpoints() {
+ final Range<IdentityEndpoint> original = Range.of(new
IdentityEndpoint(1), new IdentityEndpoint(2));
+ final Range<IdentityEndpoint> copy =
SerializationUtils.clone(original);
+ assertNotSame(original.getMinimum(), copy.getMinimum());
+ assertNotSame(original.getMaximum(), copy.getMaximum());
+ assertEquals(0, original.getMinimum().compareTo(copy.getMinimum()));
+ assertEquals(0, original.getMaximum().compareTo(copy.getMaximum()));
+ assertEquals(Objects.hash(copy.getMinimum(), copy.getMaximum()),
copy.hashCode());
}
- /**
- * Forged stream with {@code comparator == null}; F-004 hashCode check
passes because we set hashCode canonically; {@code contains()} then NPEs on
- * {@code comparator.compare(...)}.
- */
@Test
void testComparatorNullViaForgedStream() throws Exception {
final Integer min = Integer.valueOf(1);
final Integer max = Integer.valueOf(10);
- final int canonicalHash = Objects.hash(min, max);
- final byte[] forged = forgeRangeStream(null, min, max, canonicalHash);
+ final byte[] forged = forgeRangeStream(null, min, max);
assertThrows(InvalidObjectException.class, () -> deserialize(forged));
}
- /**
- * Forged stream: minimum=1, maximum=10, hashCode=hash(1,10) (all
legitimate), but comparator replaced with a reversed ordering. The hashCode
gate passes
- * (comparator excluded from the hash) and the null gates pass (comparator
is non-null); the ordering invariant is the only one violated. The deserialized
- * Range still reports endpoints [1,10] but {@code contains(5)} returns
false because it trusts the reversed comparator.
- */
+ @Test
+ void testDeserializeVersion320() {
+ // Streams generated using Commons Lang 3.20.0, with endpoints 1 and 2.
+ final Range<?>[] expected = {Range.of(1, 2), IntegerRange.of(1, 2),
LongRange.of(1, 2), DoubleRange.of(1, 2)};
+ final String[] streams = {
+
"rO0ABXNyAB5vcmcuYXBhY2hlLmNvbW1vbnMubGFuZzMuUmFuZ2UAAAAAAAAAAQIAA0wACmNvbXBhcmF0b3J0ABZMamF2YS91dGls"
+
+
"L0NvbXBhcmF0b3I7TAAHbWF4aW11bXQAEkxqYXZhL2xhbmcvT2JqZWN0O0wAB21pbmltdW1xAH4AAnhwfnIAM29yZy5hcGFjaGUu"
+
+
"Y29tbW9ucy5sYW5nMy5SYW5nZSRDb21wYXJhYmxlQ29tcGFyYXRvcgAAAAAAAAAAEgAAeHIADmphdmEubGFuZy5FbnVtAAAAAAAA"
+
+
"AAASAAB4cHQACElOU1RBTkNFc3IAEWphdmEubGFuZy5JbnRlZ2VyEuKgpPeBhzgCAAFJAAV2YWx1ZXhyABBqYXZhLmxhbmcuTnVt"
+
+ "YmVyhqyVHQuU4IsCAAB4cAAAAAJzcQB+AAgAAAAB",
+
"rO0ABXNyACVvcmcuYXBhY2hlLmNvbW1vbnMubGFuZzMuSW50ZWdlclJhbmdlAAAAAAAAAAECAAB4cgAkb3JnLmFwYWNoZS5jb21t"
+
+
"b25zLmxhbmczLk51bWJlclJhbmdlAAAAAAAAAAECAAB4cgAeb3JnLmFwYWNoZS5jb21tb25zLmxhbmczLlJhbmdlAAAAAAAAAAEC"
+
+
"AANMAApjb21wYXJhdG9ydAAWTGphdmEvdXRpbC9Db21wYXJhdG9yO0wAB21heGltdW10ABJMamF2YS9sYW5nL09iamVjdDtMAAdt"
+
+
"aW5pbXVtcQB+AAR4cH5yADNvcmcuYXBhY2hlLmNvbW1vbnMubGFuZzMuUmFuZ2UkQ29tcGFyYWJsZUNvbXBhcmF0b3IAAAAAAAAA"
+
+
"ABIAAHhyAA5qYXZhLmxhbmcuRW51bQAAAAAAAAAAEgAAeHB0AAhJTlNUQU5DRXNyABFqYXZhLmxhbmcuSW50ZWdlchLioKT3gYc4"
+
+
"AgABSQAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAAACc3EAfgAKAAAAAQ==",
+
"rO0ABXNyACJvcmcuYXBhY2hlLmNvbW1vbnMubGFuZzMuTG9uZ1JhbmdlAAAAAAAAAAECAAB4cgAkb3JnLmFwYWNoZS5jb21tb25z"
+
+
"LmxhbmczLk51bWJlclJhbmdlAAAAAAAAAAECAAB4cgAeb3JnLmFwYWNoZS5jb21tb25zLmxhbmczLlJhbmdlAAAAAAAAAAECAANM"
+
+
"AApjb21wYXJhdG9ydAAWTGphdmEvdXRpbC9Db21wYXJhdG9yO0wAB21heGltdW10ABJMamF2YS9sYW5nL09iamVjdDtMAAdtaW5p"
+
+
"bXVtcQB+AAR4cH5yADNvcmcuYXBhY2hlLmNvbW1vbnMubGFuZzMuUmFuZ2UkQ29tcGFyYWJsZUNvbXBhcmF0b3IAAAAAAAAAABIA"
+
+
"AHhyAA5qYXZhLmxhbmcuRW51bQAAAAAAAAAAEgAAeHB0AAhJTlNUQU5DRXNyAA5qYXZhLmxhbmcuTG9uZzuL5JDMjyPfAgABSgAF"
+
+
"dmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAAAAAAAAAnNxAH4ACgAAAAAAAAAB",
+
"rO0ABXNyACRvcmcuYXBhY2hlLmNvbW1vbnMubGFuZzMuRG91YmxlUmFuZ2UAAAAAAAAAAQIAAHhyACRvcmcuYXBhY2hlLmNvbW1v"
+
+
"bnMubGFuZzMuTnVtYmVyUmFuZ2UAAAAAAAAAAQIAAHhyAB5vcmcuYXBhY2hlLmNvbW1vbnMubGFuZzMuUmFuZ2UAAAAAAAAAAQIA"
+
+
"A0wACmNvbXBhcmF0b3J0ABZMamF2YS91dGlsL0NvbXBhcmF0b3I7TAAHbWF4aW11bXQAEkxqYXZhL2xhbmcvT2JqZWN0O0wAB21p"
+
+
"bmltdW1xAH4ABHhwfnIAM29yZy5hcGFjaGUuY29tbW9ucy5sYW5nMy5SYW5nZSRDb21wYXJhYmxlQ29tcGFyYXRvcgAAAAAAAAAA"
+
+
"EgAAeHIADmphdmEubGFuZy5FbnVtAAAAAAAAAAASAAB4cHQACElOU1RBTkNFc3IAEGphdmEubGFuZy5Eb3VibGWAs8JKKWv7BAIA"
+
+
"AUQABXZhbHVleHIAEGphdmEubGFuZy5OdW1iZXKGrJUdC5TgiwIAAHhwQAAAAAAAAABzcQB+AAo/8AAAAAAAAA=="
+ };
+ for (int i = 0; i < streams.length; i++) {
+ final Range<?> actual =
SerializationUtils.deserialize(Base64.getDecoder().decode(streams[i]));
+ assertEquals(expected[i].getClass(), actual.getClass());
+ assertEquals(expected[i], actual);
+ assertEquals(expected[i].hashCode(), actual.hashCode());
+ }
+ }
+
@Test
void testForgedReversedComparatorBreaksContains() throws Exception {
final Range<Integer> reference = Range.of(Integer.valueOf(1),
Integer.valueOf(10));
@@ -137,49 +203,43 @@ void testForgedReversedComparatorBreaksContains() throws
Exception {
assertTrue(reference.contains(Integer.valueOf(5)));
}
- /**
- * Forged stream with {@code maximum == null}; symmetric to F-061b.
- */
@Test
void testMaximumNullViaForgedStream() throws Exception {
final Integer min = Integer.valueOf(1);
- final int canonicalHash = Objects.hash(min, (Object) null);
final Object comparator = Range.of(Integer.valueOf(1),
Integer.valueOf(2)).getComparator();
- final byte[] forged = forgeRangeStream(comparator, min, null,
canonicalHash);
+ final byte[] forged = forgeRangeStream(comparator, min, null);
assertThrows(InvalidObjectException.class, () -> deserialize(forged));
}
- /**
- * Forged stream with {@code minimum == null}; {@code Objects.hash(null,
max)} is a valid int, so the F-004 check passes. {@code contains()} NPEs
- * because {@code comparator.compare(element, null)} unboxes null (or, for
ComparableComparator, calls {@code element.compareTo(null)} which is an
- * NPE-by-contract).
- */
@Test
void testMinimumNullViaForgedStream() throws Exception {
final Integer max = Integer.valueOf(10);
- final int canonicalHash = Objects.hash((Object) null, max);
// comparator must be non-null here so we isolate the minimum-null gap.
// We use ComparableComparator.INSTANCE via deserialization round-trip
of a real Range.
final Object comparator = Range.of(Integer.valueOf(1),
Integer.valueOf(2)).getComparator();
- final byte[] forged = forgeRangeStream(comparator, null, max,
canonicalHash);
+ final byte[] forged = forgeRangeStream(comparator, null, max);
assertThrows(InvalidObjectException.class, () -> deserialize(forged));
}
- /**
- * Forged stream with a NaN maximum and a canonically matching hashCode:
the hashCode gate passes and the
- * comparator ordering gate passes (NaN sorts above everything under
Double.compareTo), so only the NaN
- * endpoint gate stands between the stream and a half-unbounded fail-open
range.
- */
+
@Test
void testNaNEndpointViaForgedStream() throws Exception {
final Double min = Double.valueOf(5.0);
final Double max = Double.valueOf(Double.NaN);
- final int canonicalHash = Objects.hash(min, max);
final Object comparator = Range.of(Integer.valueOf(1),
Integer.valueOf(2)).getComparator();
- final byte[] forged = forgeRangeStream(comparator, min, max,
canonicalHash);
+ final byte[] forged = forgeRangeStream(comparator, min, max);
assertThrows(InvalidObjectException.class, () -> deserialize(forged));
}
+ @Test
+ void testRoundTripChangingHashEndpoints() {
+ final Range<ChangingHashEndpoint> original = Range.of(new
ChangingHashEndpoint(1), new ChangingHashEndpoint(2));
+ final Range<ChangingHashEndpoint> copy =
SerializationUtils.roundtrip(original);
+ assertEquals(123, original.getMinimum().hashCode());
+ assertEquals(0, copy.getMinimum().hashCode());
+ assertEquals(Objects.hash(copy.getMinimum(), copy.getMaximum()),
copy.hashCode());
+ }
+
@Test
void testRoundTripPreservesCorrectHashCode() throws Exception {
final Range<String> range = Range.of("apple", "mango");