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 7f5bbbf0a Fraction.getFraction(String) throws undeclared 
ArithmeticException on crafted numeric strings ('9999999999.5', '1/0', 
mixed-number overflow) (f021).
7f5bbbf0a is described below

commit 7f5bbbf0a2d201194fcbd7eb8aeb15c5c691420f
Author: Gary Gregory <[email protected]>
AuthorDate: Sat Sep 5 14:52:20 2026 -0400

    Fraction.getFraction(String) throws undeclared ArithmeticException on
    crafted numeric strings ('9999999999.5', '1/0', mixed-number overflow)
    (f021).
---
 src/changes/changes.xml                            |  1 +
 .../org/apache/commons/lang3/math/Fraction.java    | 58 ++++++++++++++++------
 .../apache/commons/lang3/math/FractionTest.java    | 20 ++++++++
 3 files changed, 65 insertions(+), 14 deletions(-)

diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 7d5882e15..846bae61c 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -270,6 +270,7 @@ java.lang.NullPointerException: Cannot invoke
     <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>
     <action                   type="fix" dev="ggregory" due-to="Gary 
Gregory">BitField long-mask fields silently mis-answer int/short/byte 
accessors: shifts masked mod 32, and sign extension makes above-bit-31 flags 
read as set on any negative holder (f019).</action>
     <action                   type="fix" dev="ggregory" due-to="Gary 
Gregory">ClassUtils.getClass() name normalization (whitespace stripping + 
dot-to-dollar rewriting) defeats host-side class-name string checks 
(f020).</action>
+    <action                   type="fix" dev="ggregory" due-to="Gary 
Gregory">Fraction.getFraction(String) throws undeclared ArithmeticException on 
crafted numeric strings ('9999999999.5', '1/0', mixed-number overflow) 
(f021).</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/math/Fraction.java 
b/src/main/java/org/apache/commons/lang3/math/Fraction.java
index 5fc936a28..49b313a1e 100644
--- a/src/main/java/org/apache/commons/lang3/math/Fraction.java
+++ b/src/main/java/org/apache/commons/lang3/math/Fraction.java
@@ -245,39 +245,51 @@ public static Fraction getFraction(final int whole, final 
int numerator, final i
      * </p>
      * <ol>
      * <li>{@code double} String containing a dot</li>
-     * <li>'X Y/Z'</li>
-     * <li>'Y/Z'</li>
-     * <li>'X' (a simple whole number)</li>
+     * <li>{@code "X Y/Z"}</li>
+     * <li>{@code "Y/Z"}</li>
+     * <li>{@code "X"} (a simple whole number)</li>
      * </ol>
      * <p>
-     * and a .
+     * and a {@code .}
      * </p>
      *
      * @param str The string to parse, must not be {@code null}
      * @return The new {@link Fraction} instance
-     * @throws NullPointerException  if the string is {@code null}
-     * @throws NumberFormatException if the number format is invalid
+     * @throws NullPointerException  Thrown if the string is {@code null}
+     * @throws NumberFormatException Thrown if the number format is invalid, 
or if the string is well-formed but its value cannot be represented as a
+     *                               {@code Fraction}: a zero denominator such 
as {@code "1/0"}, a value outside the range of an {@code int} such as
+     *                               {@code "9999999999.5"}, or a mixed number 
whose combined numerator overflows. For those unrepresentable values, the causal
+     *                               {@link ArithmeticException} is preserved 
as the {@link Throwable#getCause() cause}.
      */
-    public static Fraction getFraction(String str) {
+    public static Fraction getFraction(final String str) {
         Objects.requireNonNull(str, "str");
         // parse double format
         int pos = str.indexOf('.');
         if (pos >= 0) {
-            return getFraction(Double.parseDouble(str));
+            final double value = Double.parseDouble(str);
+            try {
+                return getFraction(value);
+            } catch (final ArithmeticException e) {
+                throw toNumberFormatException(str, e);
+            }
         }
 
         // parse X Y/Z format
         pos = str.indexOf(' ');
         if (pos > 0) {
             final int whole = Integer.parseInt(str.substring(0, pos));
-            str = str.substring(pos + 1);
-            pos = str.indexOf('/');
+            final String remainder = str.substring(pos + 1);
+            pos = remainder.indexOf('/');
             if (pos < 0) {
                 throw new NumberFormatException("The fraction could not be 
parsed as the format X Y/Z");
             }
-            final int numer = Integer.parseInt(str.substring(0, pos));
-            final int denom = Integer.parseInt(str.substring(pos + 1));
-            return getFraction(whole, numer, denom);
+            final int numer = Integer.parseInt(remainder.substring(0, pos));
+            final int denom = Integer.parseInt(remainder.substring(pos + 1));
+            try {
+                return getFraction(whole, numer, denom);
+            } catch (final ArithmeticException e) {
+                throw toNumberFormatException(str, e);
+            }
         }
 
         // parse Y/Z format
@@ -288,7 +300,11 @@ public static Fraction getFraction(String str) {
         }
         final int numer = Integer.parseInt(str.substring(0, pos));
         final int denom = Integer.parseInt(str.substring(pos + 1));
-        return getFraction(numer, denom);
+        try {
+            return getFraction(numer, denom);
+        } catch (final ArithmeticException e) {
+            throw toNumberFormatException(str, e);
+        }
     }
 
     /**
@@ -436,6 +452,20 @@ private static int mulPosAndCheck(final int x, final int 
y) {
         return (int) m;
     }
 
+    /**
+     * Converts an {@link ArithmeticException} raised while parsing a string 
into the {@link NumberFormatException} that
+     * {@link #getFraction(String)} documents, preserving the original 
exception as the cause.
+     *
+     * @param str The string being parsed.
+     * @param cause The arithmetic failure: a zero denominator or a value 
outside the range of an {@code int}.
+     * @return The exception for the caller to throw, never {@code null}.
+     */
+    private static NumberFormatException toNumberFormatException(final String 
str, final ArithmeticException cause) {
+        final NumberFormatException nfe = new NumberFormatException("The 
fraction could not be parsed from '" + str + "': " + cause.getMessage());
+        nfe.initCause(cause);
+        return nfe;
+    }
+
     /**
      * The numerator number part of the fraction (the three in three sevenths).
      */
diff --git a/src/test/java/org/apache/commons/lang3/math/FractionTest.java 
b/src/test/java/org/apache/commons/lang3/math/FractionTest.java
index 86bb7b120..e2f583a78 100644
--- a/src/test/java/org/apache/commons/lang3/math/FractionTest.java
+++ b/src/test/java/org/apache/commons/lang3/math/FractionTest.java
@@ -609,6 +609,26 @@ void testFactory_String_proper() {
         assertThrows(NumberFormatException.class, () -> Fraction.getFraction(" 
"));
     }
 
+    /**
+     * Tests that string contents that are well-formed but unrepresentable 
throw the documented NumberFormatException
+     * (with the ArithmeticException preserved as the cause) instead of an 
undeclared ArithmeticException.
+     */
+    @Test
+    void testFactory_String_unrepresentableThrowsNumberFormatException() {
+        // double delegation path: out of int range and non-convergent values
+        NumberFormatException e = assertThrows(NumberFormatException.class, () 
-> Fraction.getFraction("9999999999.5"));
+        assertTrue(e.getCause() instanceof ArithmeticException);
+        assertThrows(NumberFormatException.class, () -> 
Fraction.getFraction("2147483648.5"));
+        // Y/Z path: zero denominator and negation overflow
+        e = assertThrows(NumberFormatException.class, () -> 
Fraction.getFraction("1/0"));
+        assertTrue(e.getCause() instanceof ArithmeticException);
+        assertThrows(NumberFormatException.class, () -> 
Fraction.getFraction("1/-2147483648"));
+        // X Y/Z path: zero denominator and combined-numerator overflow
+        e = assertThrows(NumberFormatException.class, () -> 
Fraction.getFraction("1 2/0"));
+        assertTrue(e.getCause() instanceof ArithmeticException);
+        assertThrows(NumberFormatException.class, () -> 
Fraction.getFraction("2147483647 1/2"));
+    }
+
     @Test
     void testGets() {
         Fraction f;

Reply via email to