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

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

commit a760ac19d7ef593db2910cda98f5a024a8a136cb
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/LocalStrings.properties    |   5 ++
 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, 274 insertions(+), 30 deletions(-)

diff --git a/java/org/apache/el/LocalStrings.properties 
b/java/org/apache/el/LocalStrings.properties
index 3915106e60..4085b91edf 100644
--- a/java/org/apache/el/LocalStrings.properties
+++ b/java/org/apache/el/LocalStrings.properties
@@ -19,10 +19,13 @@
 elSupport.coerce.nonAbstract=Unable to coerce a LambdaExpression to the 
functional interface [{0}] because the method [{1}] is not abstract
 
 error.cannotSetVariables=Cannot set variables on factory
+error.add=Cannot add [{0}] to [{1}]
+error.convert=Cannot convert [{0}] of type [{1}] to [{2}]
 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.divide=Cannot divide [{0}] by [{1}]
 error.fnMapper.method=Function [{0}] not found
 error.fnMapper.null=Expression uses functions, but no FunctionMapper was 
provided
 error.fnMapper.paramcount=Function [{0}] specifies [{1}] params, but [{2}] 
were declared
@@ -40,6 +43,7 @@ error.method.ambiguous=Unable to find unambiguous method: 
{0}.{1}({2})
 error.method.notfound=Method not found: {0}.{1}({2})
 error.method.nullParms=Parameter types cannot be null
 error.mixed=Expression cannot contain both ''#{...}'' and ''${...}'' : [{0}]
+error.mod=Cannot compute [{0}] modulo [{1}]
 error.noFunctionMapperTarget=FunctionMapper target cannot be null
 error.noVariableMapperTarget=VariableMapper target cannot be null
 error.null=Expression cannot be null
@@ -48,6 +52,7 @@ error.nullMethod=Method cannot be null
 error.parseFail=Failed to parse the expression [{0}]
 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.syntax.set=Illegal Syntax for Set Operation
 error.unreachable.base=Target Unreachable, identifier [{0}] resolved to null
 error.unreachable.property=Target Unreachable, [{0}] returned null
diff --git a/java/org/apache/el/lang/ELArithmetic.java 
b/java/org/apache/el/lang/ELArithmetic.java
index 481155d7e3..3ec9bdf102 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 b92d290f79..8e412a33ed 100644
--- a/java/org/apache/el/lang/ELSupport.java
+++ b/java/org/apache/el/lang/ELSupport.java
@@ -87,8 +87,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 {
@@ -124,12 +124,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));
     }
@@ -341,7 +349,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;
@@ -355,7 +367,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 fd9ec2c029..e5aa355feb 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 jakarta.el.ELException;
 import jakarta.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 58dd776b68..38a485bf76 100644
--- a/test/org/apache/el/lang/TestELSupport.java
+++ b/test/org/apache/el/lang/TestELSupport.java
@@ -244,6 +244,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