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;