This is an automated email from the ASF dual-hosted git repository.

rmaucher pushed a commit to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git

commit f686d90922cc0cacf353017d80374fddc14c7565
Author: opencode <[email protected]>
AuthorDate: Sun Oct 18 13:33:29 2026 +0200

    Surface EL arithmetic and comparison errors as ELException
    
    The Jakarta EL specification requires errors during arithmetic and
    comparison operator evaluation to surface as ELException, and the
    public API (ValueExpression.getValue(), ELProcessor.eval(),
    ExpressionFactory.coerceToType() etc.) only declares ELException.
    Several failure modes were instead leaking raw JDK runtime exceptions
    to callers:
    
    - coercing a non-numeric string during arithmetic leaked the
      NumberFormatException from Long.valueOf(), Double.valueOf(),
      new BigDecimal(String) and new BigInteger(String)
    - coercing NaN or Infinity for a BigDecimal operand leaked the
      NumberFormatException from new BigDecimal(double)
    - coercing a Number with an odd toString() representation for a
      BigInteger operand leaked a NumberFormatException
    - integer/BigInteger modulo by zero and BigDecimal/BigInteger
      division by zero leaked the ArithmeticException
    - BigDecimal addition and subtraction of extreme exponents leaked
      the ArithmeticException declared by BigDecimal.add()/subtract(),
      reachable from plain EL literals such as ${1e999999999 + 1}
    - comparing mutually non-comparable Comparable implementations
      leaked the ClassCastException from compareTo()
    - coercing an extreme BigDecimal to BigInteger leaked the
      ArithmeticException from BigDecimal.toBigInteger() through the
      public coercion API
    
    ELArithmetic.coerce() now wraps any NumberFormatException as an
    ELException using the existing error.convert message. The static
    add/subtract/divide/mod methods wrap ArithmeticException as an
    ELException using new error.add/error.subtract/error.divide/error.mod
    messages. ELSupport.coerceToNumber() wraps the BigDecimal and
    BigInteger coercion failures, and ELSupport.compare() wraps the
    ClassCastException from the two Comparable branches as the existing
    error.compare ELException. The original exception is preserved as
    the cause in every case so applications that were catching the raw
    types can still inspect it via getCause().
    
    Spec-defined outcomes such as 1 / 0 returning Infinity and successful
    coercions are unchanged. Adds regression tests covering all of the
    above failure modes.
---
 java/org/apache/el/Messages.properties        |   4 +
 java/org/apache/el/lang/ELArithmetic.java     |  86 +++++++++++++++------
 java/org/apache/el/lang/ELSupport.java        |  28 +++++--
 test/org/apache/el/lang/TestELArithmetic.java | 104 ++++++++++++++++++++++++++
 test/org/apache/el/lang/TestELSupport.java    |  81 ++++++++++++++++++++
 5 files changed, 273 insertions(+), 30 deletions(-)

diff --git a/java/org/apache/el/Messages.properties 
b/java/org/apache/el/Messages.properties
index b71f6ab4e3..bbf28472ac 100644
--- a/java/org/apache/el/Messages.properties
+++ b/java/org/apache/el/Messages.properties
@@ -14,14 +14,17 @@
 # limitations under the License.
 
 # General Errors
+error.add=Cannot add [{0}] to [{1}]
 error.cannotSetVariables=Cannot set variables on factory
 error.compare=Cannot compare [{0}] to [{1}]
 error.compare.null=Unable to compare null with non-null object
 error.context.null=ELContext was null
 error.convert=Cannot convert [{0}] of type [{1}] to [{2}]
 error.compare=Cannot compare [{0}] to [{1}]
+error.divide=Cannot divide [{0}] by [{1}]
 error.function=Problems calling function [{0}]
 error.invalidMethodExpression=Not a valid method expression [{0}]
+error.mod=Cannot compute [{0}] modulo [{1}]
 error.noFunctionMapperTarget=FunctionMapper target cannot be null
 error.noVariableMapperTarget=VariableMapper target cannot be null
 error.nullLocalName=Local name cannot be null
@@ -30,6 +33,7 @@ error.unreachable.base=Target Unreachable, identifier [{0}] 
resolved to null
 error.unreachable.property=Target Unreachable, [{0}] returned null
 error.resolver.unhandled=ELResolver did not handle type: [{0}] with property 
of [{1}]
 error.resolver.unhandled.null=ELResolver cannot handle a null base Object with 
identifier [{0}]
+error.subtract=Cannot subtract [{1}] from [{0}]
 error.invoke.wrongParams=The method [{0}] was called with [{1}] parameter(s) 
when it expected [{2}]
 error.invoke.tooFewParams=The method [{0}] was called with [{1}] parameter(s) 
when it expected at least [{2}]
 
diff --git a/java/org/apache/el/lang/ELArithmetic.java 
b/java/org/apache/el/lang/ELArithmetic.java
index c35eac969c..c47a502427 100644
--- a/java/org/apache/el/lang/ELArithmetic.java
+++ b/java/org/apache/el/lang/ELArithmetic.java
@@ -303,7 +303,11 @@ public abstract class ELArithmetic {
      *
      * @param obj0 The first operand
      * @param obj1 The second operand
+     *
      * @return The result of the addition
+     *
+     * @throws ELException if the operands cannot be coerced to numbers or if 
the addition cannot be computed, for
+     *                     example overflow with extreme BigDecimal values
      */
     public static Number add(final Object obj0, final Object obj1) {
         final ELArithmetic delegate = findDelegate(obj0, obj1);
@@ -311,10 +315,14 @@ public abstract class ELArithmetic {
             return Long.valueOf(0);
         }
 
-        Number num0 = delegate.coerce(obj0);
-        Number num1 = delegate.coerce(obj1);
+        try {
+            Number num0 = delegate.coerce(obj0);
+            Number num1 = delegate.coerce(obj1);
 
-        return delegate.add(num0, num1);
+            return delegate.add(num0, num1);
+        } catch (ArithmeticException ae) {
+            throw new ELException(MessageFactory.get("error.add", obj0, obj1), 
ae);
+        }
     }
 
     /**
@@ -322,7 +330,11 @@ public abstract class ELArithmetic {
      *
      * @param obj0 The dividend
      * @param obj1 The divisor
+     *
      * @return The result of the modulo operation
+     *
+     * @throws ELException if the operands cannot be coerced to numbers or if 
the modulo cannot be computed, for
+     *                     example division by zero with integer operands
      */
     public static Number mod(final Object obj0, final Object obj1) {
         if (obj0 == null && obj1 == null) {
@@ -340,10 +352,14 @@ public abstract class ELArithmetic {
             delegate = LONG;
         }
 
-        Number num0 = delegate.coerce(obj0);
-        Number num1 = delegate.coerce(obj1);
+        try {
+            Number num0 = delegate.coerce(obj0);
+            Number num1 = delegate.coerce(obj1);
 
-        return delegate.mod(num0, num1);
+            return delegate.mod(num0, num1);
+        } catch (ArithmeticException ae) {
+            throw new ELException(MessageFactory.get("error.mod", obj0, obj1), 
ae);
+        }
     }
 
     /**
@@ -351,7 +367,11 @@ public abstract class ELArithmetic {
      *
      * @param obj0 The minuend
      * @param obj1 The subtrahend
+     *
      * @return The result of the subtraction
+     *
+     * @throws ELException if the operands cannot be coerced to numbers or if 
the subtraction cannot be computed, for
+     *                     example overflow with extreme BigDecimal values
      */
     public static Number subtract(final Object obj0, final Object obj1) {
         final ELArithmetic delegate = findDelegate(obj0, obj1);
@@ -359,10 +379,14 @@ public abstract class ELArithmetic {
             return Long.valueOf(0);
         }
 
-        Number num0 = delegate.coerce(obj0);
-        Number num1 = delegate.coerce(obj1);
+        try {
+            Number num0 = delegate.coerce(obj0);
+            Number num1 = delegate.coerce(obj1);
 
-        return delegate.subtract(num0, num1);
+            return delegate.subtract(num0, num1);
+        } catch (ArithmeticException ae) {
+            throw new ELException(MessageFactory.get("error.subtract", obj0, 
obj1), ae);
+        }
     }
 
     /**
@@ -370,7 +394,11 @@ public abstract class ELArithmetic {
      *
      * @param obj0 The dividend
      * @param obj1 The divisor
+     *
      * @return The result of the division
+     *
+     * @throws ELException if the operands cannot be coerced to numbers or if 
the division cannot be computed, for
+     *                     example division by zero with BigDecimal or 
BigInteger operands
      */
     public static Number divide(final Object obj0, final Object obj1) {
         if (obj0 == null && obj1 == null) {
@@ -386,10 +414,14 @@ public abstract class ELArithmetic {
             delegate = DOUBLE;
         }
 
-        Number num0 = delegate.coerce(obj0);
-        Number num1 = delegate.coerce(obj1);
+        try {
+            Number num0 = delegate.coerce(obj0);
+            Number num1 = delegate.coerce(obj1);
 
-        return delegate.divide(num0, num1);
+            return delegate.divide(num0, num1);
+        } catch (ArithmeticException ae) {
+            throw new ELException(MessageFactory.get("error.divide", obj0, 
obj1), ae);
+        }
     }
 
     /**
@@ -507,21 +539,27 @@ public abstract class ELArithmetic {
      * Coerce an object to a number.
      *
      * @param obj The object to coerce
+     *
      * @return The coerced number
+     *
+     * @throws ELException if the object cannot be coerced to a number
      */
     protected final Number coerce(final Object obj) {
-
-        if (isNumber(obj)) {
-            return coerce((Number) obj);
-        }
-        if (obj == null || "".equals(obj)) {
-            return coerce(ZERO);
-        }
-        if (obj instanceof String) {
-            return coerce((String) obj);
-        }
-        if (obj instanceof Character) {
-            return coerce(Short.valueOf((short) ((Character) 
obj).charValue()));
+        try {
+            if (isNumber(obj)) {
+                return coerce((Number) obj);
+            }
+            if (obj == null || "".equals(obj)) {
+                return coerce(ZERO);
+            }
+            if (obj instanceof String) {
+                return coerce((String) obj);
+            }
+            if (obj instanceof Character) {
+                return coerce(Short.valueOf((short) ((Character) 
obj).charValue()));
+            }
+        } catch (NumberFormatException nfe) {
+            throw new ELException(MessageFactory.get("error.convert", obj, 
obj.getClass(), "Number"), nfe);
         }
 
         throw new ELException(MessageFactory.get("error.convert", obj, 
obj.getClass(), "Number"));
diff --git a/java/org/apache/el/lang/ELSupport.java 
b/java/org/apache/el/lang/ELSupport.java
index a2c292dfca..1399de3608 100644
--- a/java/org/apache/el/lang/ELSupport.java
+++ b/java/org/apache/el/lang/ELSupport.java
@@ -82,8 +82,8 @@ public class ELSupport {
      *
      * @return -1, 0, or 1 if this object is less than, equal to, or greater 
than val.
      *
-     * @throws ELException          if neither object is Comparable
-     * @throws ClassCastException   if the objects are not mutually comparable
+     * @throws ELException          if the objects cannot be compared, 
including because they are not mutually
+     *                              comparable
      * @throws NullPointerException if either object is null
      */
     public static int compare(final ELContext ctx, final Object obj0, final 
Object obj1) throws ELException {
@@ -119,12 +119,20 @@ public class ELSupport {
         if (obj0 instanceof Comparable<?>) {
             @SuppressWarnings("unchecked") // checked above
             final Comparable<Object> comparable = (Comparable<Object>) obj0;
-            return comparable.compareTo(obj1);
+            try {
+                return comparable.compareTo(obj1);
+            } catch (ClassCastException cce) {
+                throw new ELException(MessageFactory.get("error.compare", 
obj0, obj1), cce);
+            }
         }
         if (obj1 instanceof Comparable<?>) {
             @SuppressWarnings("unchecked") // checked above
             final Comparable<Object> comparable = (Comparable<Object>) obj1;
-            return -comparable.compareTo(obj0);
+            try {
+                return -comparable.compareTo(obj0);
+            } catch (ClassCastException cce) {
+                throw new ELException(MessageFactory.get("error.compare", 
obj0, obj1), cce);
+            }
         }
         throw new ELException(MessageFactory.get("error.compare", obj0, obj1));
     }
@@ -336,7 +344,11 @@ public class ELSupport {
         }
         if (BigInteger.class.equals(type)) {
             if (number instanceof BigDecimal) {
-                return ((BigDecimal) number).toBigInteger();
+                try {
+                    return ((BigDecimal) number).toBigInteger();
+                } catch (ArithmeticException ae) {
+                    throw new ELException(MessageFactory.get("error.convert", 
number, number.getClass(), type), ae);
+                }
             }
             if (number instanceof BigInteger) {
                 return number;
@@ -350,7 +362,11 @@ public class ELSupport {
             if (number instanceof BigInteger) {
                 return new BigDecimal((BigInteger) number);
             }
-            return new BigDecimal(number.doubleValue());
+            try {
+                return new BigDecimal(number.doubleValue());
+            } catch (NumberFormatException nfe) {
+                throw new ELException(MessageFactory.get("error.convert", 
number, number.getClass(), type), nfe);
+            }
         }
         if (Byte.TYPE == type || Byte.class.equals(type)) {
             return Byte.valueOf(number.byteValue());
diff --git a/test/org/apache/el/lang/TestELArithmetic.java 
b/test/org/apache/el/lang/TestELArithmetic.java
index 14fc27aacc..72b1b99af7 100644
--- a/test/org/apache/el/lang/TestELArithmetic.java
+++ b/test/org/apache/el/lang/TestELArithmetic.java
@@ -19,6 +19,7 @@ package org.apache.el.lang;
 import java.math.BigDecimal;
 import java.math.BigInteger;
 
+import javax.el.ELException;
 import javax.el.ELProcessor;
 
 import org.junit.Assert;
@@ -145,4 +146,107 @@ public class TestELArithmetic {
     public void testBug47371doubleString2() throws Exception {
         Assert.assertEquals(Double.valueOf(2), ELArithmetic.add("1.", "1"));
     }
+
+    @Test
+    public void testNonNumericStringCoercedThrowsELException() {
+        // A non-numeric String coerced during arithmetic must surface as 
ELException,
+        // not as a raw NumberFormatException (the delegate String coercions 
use
+        // Long.valueOf/Double.valueOf/new BigDecimal/new BigInteger).
+        Object[] badOperands = { "abc", "1E", "0x1f", "1.2.3" };
+        for (Object operand : badOperands) {
+            assertELException("add", () -> ELArithmetic.add(operand, 
Long.valueOf(1)));
+            assertELException("subtract", () -> 
ELArithmetic.subtract(Long.valueOf(1), operand));
+            assertELException("multiply", () -> ELArithmetic.multiply(operand, 
Long.valueOf(1)));
+        }
+    }
+
+    @Test
+    public void testIntegerModByZeroThrowsELException() {
+        // Cast to Object to select the static Object-based method
+        assertELException("mod", () -> ELArithmetic.mod((Object) 
Long.valueOf(1), (Object) Long.valueOf(0)));
+    }
+
+    @Test
+    public void testBigIntegerModByZeroThrowsELException() {
+        assertELException("mod",
+                () -> ELArithmetic.mod((Object) BigInteger.valueOf(1), 
(Object) BigInteger.valueOf(0)));
+    }
+
+    @Test
+    public void testBigDecimalDivByZeroThrowsELException() {
+        assertELException("divide", () -> ELArithmetic.divide((Object) 
BigDecimal.ONE, (Object) BigDecimal.ZERO));
+    }
+
+    @Test
+    public void testBigIntegerDivByZeroThrowsELException() {
+        assertELException("divide",
+                () -> ELArithmetic.divide((Object) BigInteger.ONE, (Object) 
BigInteger.ZERO));
+    }
+
+    @Test
+    public void testNonFiniteNumberWithBigDecimalThrowsELException() {
+        // The BigDecimal delegate Number coercion uses new BigDecimal(double) 
which
+        // rejects NaN and Infinity. The failure must surface as ELException.
+        assertELException("add", () -> ELArithmetic.add((Object) 
BigDecimal.ONE, (Object) Double.NaN));
+        assertELException("add",
+                () -> ELArithmetic.add((Object) BigDecimal.ONE, (Object) 
Double.POSITIVE_INFINITY));
+        assertELException("divide",
+                () -> ELArithmetic.divide((Object) BigDecimal.ONE, (Object) 
Double.NEGATIVE_INFINITY));
+    }
+
+    @Test
+    public void testOddNumberToStringCoercionThrowsELException() {
+        // The BigInteger delegate Number coercion uses new 
BigInteger(toString()). The
+        // failure must surface as ELException.
+        assertELException("add",
+                () -> ELArithmetic.add((Object) new TesterNumberOddToString(), 
(Object) BigInteger.ONE));
+    }
+
+    @Test
+    public void testBigDecimalOverflowThrowsELException() {
+        // BigDecimal.add()/subtract() declare ArithmeticException for 
overflow with
+        // extreme exponents. The failure must surface as ELException.
+        BigDecimal extreme = new BigDecimal("1e999999999");
+        assertELException("add", () -> ELArithmetic.add((Object) extreme, 
(Object) Long.valueOf(1)));
+        assertELException("subtract", () -> ELArithmetic.subtract((Object) 
extreme, (Object) Long.valueOf(1)));
+    }
+
+    private static class TesterNumberOddToString extends Number {
+
+        private static final long serialVersionUID = 1L;
+
+        @Override
+        public int intValue() {
+            return 1;
+        }
+
+        @Override
+        public long longValue() {
+            return 1L;
+        }
+
+        @Override
+        public float floatValue() {
+            return 1.5f;
+        }
+
+        @Override
+        public double doubleValue() {
+            return 1.5;
+        }
+
+        @Override
+        public String toString() {
+            return "1.5";
+        }
+    }
+
+    private static void assertELException(String op, Runnable runnable) {
+        try {
+            runnable.run();
+            Assert.fail(op + " with an invalid operand should throw 
ELException");
+        } catch (ELException e) {
+            // Expected
+        }
+    }
 }
diff --git a/test/org/apache/el/lang/TestELSupport.java 
b/test/org/apache/el/lang/TestELSupport.java
index 528ed50379..355824c34d 100644
--- a/test/org/apache/el/lang/TestELSupport.java
+++ b/test/org/apache/el/lang/TestELSupport.java
@@ -240,6 +240,87 @@ public class TestELSupport {
         Assert.assertNull("Result: " + result, result);
     }
 
+    @Test
+    public void testCompareNonMutuallyComparable01() {
+        // The ClassCastException raised by obj0's compareTo() must be wrapped
+        try {
+            ELSupport.compare(null, new TesterComparableA(), new 
TesterComparableB());
+            Assert.fail("An ELException was expected");
+        } catch (ELException e) {
+            Assert.assertTrue("Unexpected cause: " + e.getCause(), 
e.getCause() instanceof ClassCastException);
+        }
+    }
+
+    @Test
+    public void testCompareNonMutuallyComparable02() {
+        // obj0 is not Comparable, so obj1's compareTo() is used. The resulting
+        // ClassCastException must be wrapped
+        try {
+            ELSupport.compare(null, new Object(), new TesterComparableB());
+            Assert.fail("An ELException was expected");
+        } catch (ELException e) {
+            Assert.assertTrue("Unexpected cause: " + e.getCause(), 
e.getCause() instanceof ClassCastException);
+        }
+    }
+
+    @Test
+    public void testCompareBigDecimalWithNonFiniteThrowsELException() {
+        // Coercing NaN to BigDecimal uses new BigDecimal(double) which 
rejects the
+        // value. The failure must surface as ELException.
+        try {
+            ELSupport.compare(null, BigDecimal.ONE, Double.NaN);
+            Assert.fail("An ELException was expected");
+        } catch (ELException e) {
+            Assert.assertTrue("Unexpected cause: " + e.getCause(), 
e.getCause() instanceof NumberFormatException);
+        }
+    }
+
+    @Test
+    public void testEqualsBigDecimalWithNonFiniteThrowsELException() {
+        try {
+            ELSupport.equals(null, BigDecimal.ONE, Double.NaN);
+            Assert.fail("An ELException was expected");
+        } catch (ELException e) {
+            Assert.assertTrue("Unexpected cause: " + e.getCause(), 
e.getCause() instanceof NumberFormatException);
+        }
+    }
+
+    @Test
+    public void testCoerceToTypeNonFiniteToBigDecimalThrowsELException() {
+        try {
+            ELSupport.coerceToType(null, Double.POSITIVE_INFINITY, 
BigDecimal.class);
+            Assert.fail("An ELException was expected");
+        } catch (ELException e) {
+            Assert.assertTrue("Unexpected cause: " + e.getCause(), 
e.getCause() instanceof NumberFormatException);
+        }
+    }
+
+    @Test
+    public void 
testCoerceToTypeExtremeBigDecimalToBigIntegerThrowsELException() {
+        // BigDecimal.toBigInteger() declares ArithmeticException for overflow 
with
+        // extreme exponents. The failure must surface as ELException.
+        try {
+            ELSupport.coerceToType(null, new BigDecimal("1e999999999"), 
BigInteger.class);
+            Assert.fail("An ELException was expected");
+        } catch (ELException e) {
+            Assert.assertTrue("Unexpected cause: " + e.getCause(), 
e.getCause() instanceof ArithmeticException);
+        }
+    }
+
+    private static class TesterComparableA implements 
Comparable<TesterComparableA> {
+        @Override
+        public int compareTo(TesterComparableA o) {
+            return 0;
+        }
+    }
+
+    private static class TesterComparableB implements 
Comparable<TesterComparableB> {
+        @Override
+        public int compareTo(TesterComparableB o) {
+            return 0;
+        }
+    }
+
     private static void testIsSame(Object value) {
         Assert.assertEquals(value, ELSupport.coerceToNumber(null, value, 
value.getClass()));
     }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to