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]
