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 1023c0077 DoubleRange/Range accept NaN endpoints, silently producing a 
half-unbounded range, contains()/fit() fail open for every value above the 
minimum (f018).
1023c0077 is described below

commit 1023c00776aebb1b929fd873eb99e471e0dfd220
Author: Gary Gregory <[email protected]>
AuthorDate: Sat Sep 5 14:24:38 2026 -0400

    DoubleRange/Range accept NaN endpoints, silently producing a
    half-unbounded range, contains()/fit() fail open for every value above
    the minimum (f018).
---
 src/changes/changes.xml                            |  1 +
 .../java/org/apache/commons/lang3/DoubleRange.java |  3 ++
 .../java/org/apache/commons/lang3/NumberRange.java |  1 +
 src/main/java/org/apache/commons/lang3/Range.java  | 41 ++++++++++++++++++++++
 .../org/apache/commons/lang3/DoubleRangeTest.java  | 18 ++++++++++
 .../apache/commons/lang3/RangeReadObjectTest.java  | 15 ++++++++
 .../java/org/apache/commons/lang3/RangeTest.java   | 23 ++++++++++++
 7 files changed, 102 insertions(+)

diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index b8e760504..f2bc1963f 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -267,6 +267,7 @@ java.lang.NullPointerException: Cannot invoke
     <action                   type="fix" dev="ggregory" due-to="Gary 
Gregory">JavaVersion.get() throws NumberFormatException where javadoc promises 
null (f015).</action>
     <action                   type="fix" dev="ggregory" due-to="Gary 
Gregory">Match Javadoc to code in SystemProperties.getProperty() and 
SystemUtils.getEnvironmentVariable() and update inline comments (f016).</action>
     <action                   type="fix" dev="ggregory" due-to="Gary 
Gregory">Fix LockingVisitors Javadoc to match code (f017).</action>
+    <action                   type="fix" dev="ggregory" due-to="Gary 
Gregory">DoubleRange/Range accept NaN endpoints, silently producing a 
half-unbounded range, contains()/fit() fail open for every value above the 
minimum (f018).</action>
     <!-- ADD -->
     <action                   type="add" dev="ggregory" due-to="Gary 
Gregory">Add JavaVersion.JAVA_27.</action>
     <action                   type="add" dev="ggregory" due-to="Gary 
Gregory">Add SystemUtils.IS_JAVA_27.</action>
diff --git a/src/main/java/org/apache/commons/lang3/DoubleRange.java 
b/src/main/java/org/apache/commons/lang3/DoubleRange.java
index 13aa5e372..da1185835 100644
--- a/src/main/java/org/apache/commons/lang3/DoubleRange.java
+++ b/src/main/java/org/apache/commons/lang3/DoubleRange.java
@@ -44,6 +44,7 @@ public final class DoubleRange extends NumberRange<Double> {
      * @param fromInclusive The first value that defines the edge of the 
range, inclusive.
      * @param toInclusive The second value that defines the edge of the range, 
inclusive.
      * @return The range object, not null.
+     * @throws IllegalArgumentException if either value is NaN.
      */
     public static DoubleRange of(final double fromInclusive, final double 
toInclusive) {
         return of(Double.valueOf(fromInclusive), Double.valueOf(toInclusive));
@@ -64,6 +65,7 @@ public static DoubleRange of(final double fromInclusive, 
final double toInclusiv
      * @param toInclusive The second value that defines the edge of the range, 
inclusive.
      * @return The range object, not null.
      * @throws NullPointerException if either element is null.
+     * @throws IllegalArgumentException if either element is NaN.
      */
     public static DoubleRange of(final Double fromInclusive, final Double 
toInclusive) {
         return new DoubleRange(fromInclusive, toInclusive);
@@ -76,6 +78,7 @@ public static DoubleRange of(final Double fromInclusive, 
final Double toInclusiv
      * @param number2 The second element, not null.
      * @throws NullPointerException when element1 is null.
      * @throws NullPointerException when element2 is null.
+     * @throws IllegalArgumentException when element1 or element2 is NaN.
      */
     private DoubleRange(final Double number1, final Double number2) {
         super(number1, number2, null);
diff --git a/src/main/java/org/apache/commons/lang3/NumberRange.java 
b/src/main/java/org/apache/commons/lang3/NumberRange.java
index f1200cfe7..1e46992f2 100644
--- a/src/main/java/org/apache/commons/lang3/NumberRange.java
+++ b/src/main/java/org/apache/commons/lang3/NumberRange.java
@@ -40,6 +40,7 @@ public class NumberRange<N extends Number> extends Range<N> {
      * @param comp The comparator to be used, null for natural ordering.
      * @throws NullPointerException when element1 is null.
      * @throws NullPointerException when element2 is null.
+     * @throws IllegalArgumentException when element1 or element2 is a 
floating-point NaN.
      */
     public NumberRange(final N number1, final N number2, final Comparator<N> 
comp) {
         super(number1, number2, comp);
diff --git a/src/main/java/org/apache/commons/lang3/Range.java 
b/src/main/java/org/apache/commons/lang3/Range.java
index 0256bcc07..bf2c456b8 100644
--- a/src/main/java/org/apache/commons/lang3/Range.java
+++ b/src/main/java/org/apache/commons/lang3/Range.java
@@ -78,6 +78,7 @@ public int compare(final Object obj1, final Object obj2) {
      * @throws NullPointerException when fromInclusive is null.
      * @throws NullPointerException when toInclusive is null.
      * @throws ClassCastException if the elements are not {@link Comparable}.
+     * @throws IllegalArgumentException if either element is a floating-point 
NaN.
      * @deprecated Use {@link #of(Comparable, Comparable)}.
      */
     @Deprecated
@@ -102,6 +103,7 @@ public static <T extends Comparable<? super T>> Range<T> 
between(final T fromInc
      * @throws NullPointerException when fromInclusive is null.
      * @throws NullPointerException when toInclusive is null.
      * @throws ClassCastException if using natural ordering and the elements 
are not {@link Comparable}.
+     * @throws IllegalArgumentException if either element is a floating-point 
NaN.
      * @deprecated Use {@link #of(Object, Object, Comparator)}.
      */
     @Deprecated
@@ -125,6 +127,7 @@ private static int hash(final Object value1, final Object 
value2) {
      * @return The range object, not null.
      * @throws NullPointerException if the element is null.
      * @throws ClassCastException if the element is not {@link Comparable}.
+     * @throws IllegalArgumentException if the element is a floating-point NaN.
      */
     public static <T extends Comparable<? super T>> Range<T> is(final T 
element) {
         return of(element, element, null);
@@ -143,11 +146,25 @@ public static <T extends Comparable<? super T>> Range<T> 
is(final T element) {
      * @return The range object, not null.
      * @throws NullPointerException if the element is null.
      * @throws ClassCastException if using natural ordering and the elements 
are not {@link Comparable}.
+     * @throws IllegalArgumentException if the element is a floating-point NaN.
      */
     public static <T> Range<T> is(final T element, final Comparator<T> 
comparator) {
         return of(element, element, comparator);
     }
 
+    /**
+     * Tests whether the element is a floating-point NaN. A NaN endpoint sorts 
above every value under the natural
+     * total order ({@link Double#compareTo(Double)} / {@link 
Float#compareTo(Float)}), silently producing a
+     * half-unbounded range whose {@code contains}/{@code fit} accept every 
value above the minimum.
+     *
+     * @param element The element to test, may be null.
+     * @return Whether the element is a floating-point NaN.
+     */
+    private static boolean isNaN(final Object element) {
+        return element instanceof Double && ((Double) element).isNaN()
+                || element instanceof Float && ((Float) element).isNaN();
+    }
+
     /**
      * Creates a range with the specified minimum and maximum values (both 
inclusive).
      *
@@ -163,6 +180,7 @@ public static <T> Range<T> is(final T element, final 
Comparator<T> comparator) {
      * @return The range object, not null.
      * @throws NullPointerException if either element is null.
      * @throws ClassCastException if the elements are not {@link Comparable}.
+     * @throws IllegalArgumentException if either element is a floating-point 
NaN.
      * @since 3.13.0
      */
     public static <T extends Comparable<? super T>> Range<T> of(final T 
fromInclusive, final T toInclusive) {
@@ -186,12 +204,27 @@ public static <T extends Comparable<? super T>> Range<T> 
of(final T fromInclusiv
      * @throws NullPointerException when fromInclusive is null.
      * @throws NullPointerException when toInclusive is null.
      * @throws ClassCastException if using natural ordering and the elements 
are not {@link Comparable}.
+     * @throws IllegalArgumentException if either element is a floating-point 
NaN.
      * @since 3.13.0
      */
     public static <T> Range<T> of(final T fromInclusive, final T toInclusive, 
final Comparator<T> comparator) {
         return new Range<>(fromInclusive, toInclusive, comparator);
     }
 
+    /**
+     * Validates that a floating-point endpoint is not NaN, mirroring the 
fail-closed posture of
+     * {@link Validate#notNaN(double, String, Object...)}.
+     *
+     * @param element The endpoint to validate.
+     * @param name The parameter name for the exception message.
+     * @throws IllegalArgumentException if the endpoint is a floating-point 
NaN.
+     */
+    private static void requireNotNaN(final Object element, final String name) 
{
+        if (isNaN(element)) {
+            throw new IllegalArgumentException(name + " must not be NaN");
+        }
+    }
+
     /**
      * The ordering scheme used in this range.
      */
@@ -225,11 +258,14 @@ public static <T> Range<T> of(final T fromInclusive, 
final T toInclusive, final
      * @param comp  The comparator to be used, null for natural ordering.
      * @throws NullPointerException when element1 is null.
      * @throws NullPointerException when element2 is null.
+     * @throws IllegalArgumentException when element1 or element2 is a 
floating-point NaN.
      */
     @SuppressWarnings("unchecked")
     Range(final T element1, final T element2, final Comparator<T> comp) {
         Objects.requireNonNull(element1, "element1");
         Objects.requireNonNull(element2, "element2");
+        requireNotNaN(element1, "element1");
+        requireNotNaN(element2, "element2");
         if (comp == null) {
             this.comparator = ComparableComparator.INSTANCE;
         } else {
@@ -553,6 +589,11 @@ private void readObject(final ObjectInputStream in) throws 
IOException, ClassNot
         SerializationUtils.requireNonNull(maximum, "maximum null");
         SerializationUtils.requireNonNull(minimum, "minimum null");
         SerializationUtils.requireNonNull(comparator, "comparator null");
+        // Mirror the constructor's NaN endpoint rejection: a crafted stream 
cannot smuggle in the degenerate
+        // half-unbounded range that construction refuses.
+        if (isNaN(minimum) || isNaN(maximum)) {
+            throw new InvalidObjectException("Range minimum/maximum must not 
be NaN.");
+        }
         if (comparator.compare(minimum, maximum) > 0) {
             throw new InvalidObjectException("Range minimum is greater than 
maximum under the comparator.");
         }
diff --git a/src/test/java/org/apache/commons/lang3/DoubleRangeTest.java 
b/src/test/java/org/apache/commons/lang3/DoubleRangeTest.java
index 64c0c2611..ab74c8b59 100644
--- a/src/test/java/org/apache/commons/lang3/DoubleRangeTest.java
+++ b/src/test/java/org/apache/commons/lang3/DoubleRangeTest.java
@@ -385,6 +385,24 @@ void testOfWithContains() {
         assertNullPointerException(() -> of(null, null));
     }
 
+    /**
+     * A NaN endpoint sorts above every double under Double.compareTo's total 
order, so it used to construct a
+     * half-unbounded range whose contains()/fit() accepted every value above 
the minimum. Construction must fail
+     * closed instead (mirroring Validate.notNaN).
+     */
+    @Test
+    void testNaNEndpointsRejected() {
+        assertIllegalArgumentException(() -> of(5.0, Double.NaN));
+        assertIllegalArgumentException(() -> of(Double.NaN, 5.0));
+        assertIllegalArgumentException(() -> of(Double.NaN, Double.NaN));
+        assertIllegalArgumentException(() -> of(Double.valueOf(5.0), 
Double.valueOf(Double.NaN)));
+        // computed-NaN routes, not just the literal
+        assertIllegalArgumentException(() -> of(0.0, 0.0 / 0.0));
+        assertIllegalArgumentException(() -> of(1.0, 0.0 * 
Double.POSITIVE_INFINITY));
+        // infinities remain legal endpoints
+        assertTrue(of(0.0, 
Double.POSITIVE_INFINITY).contains(Double.MAX_VALUE));
+    }
+
     @Test
     void testRangeOfChars() {
         final DoubleRange chars = of('a', 'z');
diff --git a/src/test/java/org/apache/commons/lang3/RangeReadObjectTest.java 
b/src/test/java/org/apache/commons/lang3/RangeReadObjectTest.java
index 88188f629..e2a0cdc07 100644
--- a/src/test/java/org/apache/commons/lang3/RangeReadObjectTest.java
+++ b/src/test/java/org/apache/commons/lang3/RangeReadObjectTest.java
@@ -165,6 +165,21 @@ void testMinimumNullViaForgedStream() throws Exception {
         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);
+        assertThrows(InvalidObjectException.class, () -> deserialize(forged));
+    }
+
     @Test
     void testRoundTripPreservesCorrectHashCode() throws Exception {
         final Range<String> range = Range.of("apple", "mango");
diff --git a/src/test/java/org/apache/commons/lang3/RangeTest.java 
b/src/test/java/org/apache/commons/lang3/RangeTest.java
index 8d69e1e07..611a89a52 100644
--- a/src/test/java/org/apache/commons/lang3/RangeTest.java
+++ b/src/test/java/org/apache/commons/lang3/RangeTest.java
@@ -346,6 +346,29 @@ void testIsWithCompare() {
         assertTrue(ri.contains(11), "should contain 11");
     }
 
+    @Test
+    void testNaNEndpointsRejected() {
+        // A NaN endpoint sorts above every value under 
Double/Float.compareTo's total order, silently
+        // constructing a half-unbounded range: contains()/fit() then accept 
every value above the minimum.
+        // Construction fails closed instead (mirroring Validate.notNaN); 
non-NaN behavior is unchanged.
+        assertIllegalArgumentException(() -> Range.of(5.0, Double.NaN));
+        assertIllegalArgumentException(() -> Range.of(Double.NaN, 5.0));
+        assertIllegalArgumentException(() -> Range.of(5.0f, Float.NaN));
+        assertIllegalArgumentException(() -> Range.of(Float.NaN, 5.0f));
+        assertIllegalArgumentException(() -> Range.between(5.0, Double.NaN));
+        assertIllegalArgumentException(() -> Range.between(5.0f, Float.NaN));
+        assertIllegalArgumentException(() -> Range.is(Double.NaN));
+        assertIllegalArgumentException(() -> Range.is(Float.NaN));
+        // rejected under a custom comparator as well: no comparator gives a 
NaN endpoint a sound meaning
+        assertIllegalArgumentException(() -> Range.of(5.0, Double.NaN, 
Comparator.naturalOrder()));
+        // NumberRange over Float/Double shares the same root constructor
+        assertIllegalArgumentException(() -> new NumberRange<>(5.0f, 
Float.NaN, null));
+        assertIllegalArgumentException(() -> new NumberRange<>(Double.NaN, 
5.0, null));
+        assertNotNull(new NumberRange<>(5.0f, 6.0f, null));
+        // infinities remain legal endpoints
+        assertTrue(Range.of(0.0f, 
Float.POSITIVE_INFINITY).contains(Float.MAX_VALUE));
+    }
+
     @Test
     void testOfWithCompare() {
         // all integers are equal

Reply via email to