This is an automated email from the ASF dual-hosted git repository. rvesse pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/jena.git
commit c9720833ffe545f1197ccbbd1aece16f646cf1b2 Author: Rob Vesse <[email protected]> AuthorDate: Mon Sep 28 11:46:15 2026 +0100 Avoid unnecessary math computation Various built-in SPARQL function libraries provide access to a range of math computations. However some of these can result in generating very large outputs that won't fit into the target type. This commit adds a new MathLimits static helper with pre-computation validation functions that are called to check whether a given computation should be computed and throw an error if its output would be excessive. --- .../apache/jena/sparql/function/MathLimits.java | 118 +++++++++++++++++++++ .../jena/sparql/function/library/Math_exp.java | 2 + .../jena/sparql/function/library/Math_exp10.java | 6 +- .../jena/sparql/function/library/Math_pow.java | 22 ++-- .../function/library/leviathan/cartesian.java | 6 ++ .../sparql/function/library/leviathan/cube.java | 5 +- .../jena/sparql/function/library/leviathan/e.java | 2 + .../function/library/leviathan/factorial.java | 4 +- .../sparql/function/library/leviathan/pow.java | 5 +- .../function/library/leviathan/pythagoras.java | 3 + .../sparql/function/library/leviathan/root.java | 2 + .../jena/sparql/function/library/leviathan/sq.java | 5 +- .../sparql/function/library/leviathan/ten.java | 2 + .../optimize/TestTransformConstantFolding.java | 5 + .../org/apache/jena/sparql/expr/LibTestExpr.java | 9 +- .../jena/sparql/expr/TestLeviathanFunctions.java | 38 ++++++- .../function/library/TS_LibraryFunctions.java | 1 + .../function/library/TestFnFunctionsNumeric.java | 23 ++++ .../sparql/function/library/TestMathLimits.java | 48 +++++++++ 19 files changed, 284 insertions(+), 22 deletions(-) diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/function/MathLimits.java b/jena-arq/src/main/java/org/apache/jena/sparql/function/MathLimits.java new file mode 100644 index 0000000000..efe497d722 --- /dev/null +++ b/jena-arq/src/main/java/org/apache/jena/sparql/function/MathLimits.java @@ -0,0 +1,118 @@ +package org.apache.jena.sparql.function; + +import org.apache.jena.sparql.expr.ExprEvalException; + +import java.math.BigInteger; + +/** + * Static limits that control what's permitted for some math computations accessible via the various SPARQL function + * libraries + */ +public class MathLimits { + + private MathLimits() { + + } + + /** + * Default maximum digits used for {@link #MAX_EXPONENT_DIGITS} and {@link #MAX_FACTORIAL_DIGITS} + */ + public static final int DEFAULT_MAX_DIGITS = 64; + /** + * Configurable maximum number of digits for an exponent calculation + */ + public static int MAX_EXPONENT_DIGITS = DEFAULT_MAX_DIGITS; + /** + * Configurable maximum number of digits for a factorial calculation + */ + public static int MAX_FACTORIAL_DIGITS = DEFAULT_MAX_DIGITS; + + /** + * Calculates how many digits raising {@code a} to the power of {@code b} would require. If the number of digits is + * very large then throw an expression evaluation error to prevent execution. + * + * @param a A, base number to raise by the exponent + * @param b B, exponent to raise by + * @see #preValidateExponentCalculation(double, double) + */ + public static void preValidateExponentCalculation(BigInteger a, BigInteger b) { + double aDbl = a.doubleValue(); + if (infiniteOrNaN(aDbl)) { + throw baseOutOfRange(); + } + double bDbl = b.doubleValue(); + if (infiniteOrNaN(bDbl)) { + throw exponentOutOfRange(); + } + preValidateExponentCalculation(aDbl, bDbl); + } + + private static ExprEvalException tooManyExponentDigits() { + return new ExprEvalException("Calculating this exponent would produce too many digits"); + } + + private static ExprEvalException baseOutOfRange() { + return new ExprEvalException("Cannot calculate exponent when base is out of double range"); + } + + private static ExprEvalException exponentOutOfRange() { + return new ExprEvalException("Cannot calculate exponent when base is outside double range"); + } + + private static boolean infiniteOrNaN(double aDbl) { + return Double.isInfinite(aDbl) || Double.isNaN(aDbl); + } + + /** + * Calculates how many digits raising {@code a} to the power of {@code b} would require. If the number of digits is + * very large then throw an expression evaluation error to prevent execution. + * <p> + * This is based upon the following mathematical formula, for {@code a^b} then Number of Digits is + * {@code floor(b * log10(a)) + 1}. + * </p> + * + * @param a A, base number to raise by exponent + * @param b B, exponent to raise by + * @throws ExprEvalException Thrown if the calculation would require too many digits + */ + public static void preValidateExponentCalculation(double a, double b) { + double logA = Math.log10(a); + if (infiniteOrNaN(logA)) { + throw exponentOutOfRange(); + } + double numDigits = Math.floor(b * Math.log10(a)) + 1.0d; + if (numDigits > MAX_EXPONENT_DIGITS) { + throw tooManyExponentDigits(); + } + } + + /** + * Calculates the Stirling approximation for the factorial of a number and uses it to validate whether calculating a + * factorial would produce too many digits throwing an {@link ExprEvalException} if it would. + * <p> + * Based on Python example from <a href="https://www.johndcook.com/blog/2015/10/06/number-of-digits-in-n/">Number of + * Digits in n!</a> + * <pre> + * def stirling(n): + * return floor( ((n+0.5)*log(n) - n + 0.5*log(2*pi))/log(10) ) + 1 + * </pre> + * </p> + * <p> + * Note this method assumes the caller has already validated that the given input is non-negative. + * </p> + * + * @param n Number to compute the factorial of + * @throws ExprEvalException Thrown if calculating the factorial would produce too many digits + */ + public static void preValidateFactorial(BigInteger n) { + double nDbl = n.doubleValue(); + if (infiniteOrNaN(nDbl)) { + throw new ExprEvalException("Cannot calculate factorial when n is outside double range"); + } + double nAdj = nDbl + 0.5; + double numDigits = Math.floor(((nAdj * Math.log(nDbl)) - (nAdj * Math.log(2 * Math.PI))) / Math.log(10)) + 1d; + if (numDigits > MAX_FACTORIAL_DIGITS) { + throw new ExprEvalException("Calculating this factorial would produce too many digits"); + } + } +} diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/function/library/Math_exp.java b/jena-arq/src/main/java/org/apache/jena/sparql/function/library/Math_exp.java index 554cf45e55..b996b65f39 100644 --- a/jena-arq/src/main/java/org/apache/jena/sparql/function/library/Math_exp.java +++ b/jena-arq/src/main/java/org/apache/jena/sparql/function/library/Math_exp.java @@ -23,12 +23,14 @@ package org.apache.jena.sparql.function.library; import org.apache.jena.sparql.expr.NodeValue; import org.apache.jena.sparql.function.FunctionBase1; +import org.apache.jena.sparql.function.MathLimits; // Returns the value of e^x. public class Math_exp extends FunctionBase1 { @Override public NodeValue exec(NodeValue v) { + MathLimits.preValidateExponentCalculation(Math.E, v.getDouble()); return NodeValue.makeDouble(Math.exp(v.getDouble())); } diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/function/library/Math_exp10.java b/jena-arq/src/main/java/org/apache/jena/sparql/function/library/Math_exp10.java index 70f9f87389..c9270157b2 100644 --- a/jena-arq/src/main/java/org/apache/jena/sparql/function/library/Math_exp10.java +++ b/jena-arq/src/main/java/org/apache/jena/sparql/function/library/Math_exp10.java @@ -27,6 +27,7 @@ import org.apache.jena.sparql.ARQInternalErrorException; import org.apache.jena.sparql.expr.NodeValue; import org.apache.jena.sparql.expr.nodevalue.XSDFuncOp; import org.apache.jena.sparql.function.FunctionBase1; +import org.apache.jena.sparql.function.MathLimits; // Returns the value of natural log(x) public class Math_exp10 extends FunctionBase1 { @@ -37,13 +38,16 @@ public class Math_exp10 extends FunctionBase1 { { case OP_INTEGER: int x = v.getInteger().intValue(); - if ( x >= 0 ) + if ( x >= 0 ) { + MathLimits.preValidateExponentCalculation(BigInteger.TEN, v.getInteger()); return NodeValue.makeInteger(BigInteger.TEN.pow(x)); + } // Anything else -> double //$FALL-THROUGH$ case OP_DECIMAL: case OP_FLOAT: case OP_DOUBLE: + MathLimits.preValidateExponentCalculation(10, v.getDouble()); return NodeValue.makeDouble(Math.pow(10, v.getDouble())); default: throw new ARQInternalErrorException("Unrecognized numeric operation : "+ v); diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/function/library/Math_pow.java b/jena-arq/src/main/java/org/apache/jena/sparql/function/library/Math_pow.java index b5d0d5761c..e549aaf5f8 100644 --- a/jena-arq/src/main/java/org/apache/jena/sparql/function/library/Math_pow.java +++ b/jena-arq/src/main/java/org/apache/jena/sparql/function/library/Math_pow.java @@ -27,6 +27,7 @@ import org.apache.jena.sparql.ARQInternalErrorException; import org.apache.jena.sparql.expr.NodeValue; import org.apache.jena.sparql.expr.nodevalue.XSDFuncOp; import org.apache.jena.sparql.function.FunctionBase2; +import org.apache.jena.sparql.function.MathLimits; // math:pow($x as xs:double?, $y as xs:numeric) as xs:double? // except pow(integer,+ve integer) is an integer @@ -34,13 +35,14 @@ public class Math_pow extends FunctionBase2 { @Override public NodeValue exec(NodeValue v1, NodeValue v2) { - switch (XSDFuncOp.classifyNumeric("pow", v1, v2)) - { + switch (XSDFuncOp.classifyNumeric("pow", v1, v2)) { case OP_INTEGER: BigInteger x = v1.getInteger(); int y = v2.getInteger().intValue(); - if ( y >= 0 ) - return NodeValue.makeInteger( x.pow(y)); + if (y >= 0) { + MathLimits.preValidateExponentCalculation(x, v2.getInteger()); + return NodeValue.makeInteger(x.pow(y)); + } // Anything else -> double //$FALL-THROUGH$ case OP_DECIMAL: @@ -48,15 +50,17 @@ public class Math_pow extends FunctionBase2 { case OP_DOUBLE: double d1 = v1.getDouble(); double d2 = v2.getDouble(); - if ( d1 == 1 && d2 == Double.POSITIVE_INFINITY ) { - if ( v1.isInteger() ) + if (d1 == 1 && d2 == Double.POSITIVE_INFINITY) { + if (v1.isInteger()) { return NodeValue.nvONE; - else + } else { return NodeValue.makeDouble(1); + } } - return NodeValue.makeDouble( Math.pow(v1.getDouble(), v2.getDouble()) ); + MathLimits.preValidateExponentCalculation(d1, d2); + return NodeValue.makeDouble(Math.pow(v1.getDouble(), v2.getDouble())); default: - throw new ARQInternalErrorException("Unrecognized numeric operation : "+ v1); + throw new ARQInternalErrorException("Unrecognized numeric operation : " + v1); } } diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/function/library/leviathan/cartesian.java b/jena-arq/src/main/java/org/apache/jena/sparql/function/library/leviathan/cartesian.java index d5dee55f6d..e63cb4f366 100644 --- a/jena-arq/src/main/java/org/apache/jena/sparql/function/library/leviathan/cartesian.java +++ b/jena-arq/src/main/java/org/apache/jena/sparql/function/library/leviathan/cartesian.java @@ -29,6 +29,7 @@ import org.apache.jena.sparql.expr.ExprEvalException; import org.apache.jena.sparql.expr.ExprList; import org.apache.jena.sparql.expr.NodeValue; import org.apache.jena.sparql.function.FunctionBase; +import org.apache.jena.sparql.function.MathLimits; public class cartesian extends FunctionBase { @@ -42,6 +43,8 @@ public class cartesian extends FunctionBase { double dX = args.get(0).getDouble() - args.get(2).getDouble(); double dY = args.get(1).getDouble() - args.get(3).getDouble(); + MathLimits.preValidateExponentCalculation(dX, 2); + MathLimits.preValidateExponentCalculation(dY, 2); return NodeValue.makeDouble(Math.sqrt(Math.pow(dX, 2) + Math.pow(dY, 2))); } case 6: { @@ -49,6 +52,9 @@ public class cartesian extends FunctionBase { double dY = args.get(1).getDouble() - args.get(4).getDouble(); double dZ = args.get(2).getDouble() - args.get(5).getDouble(); + MathLimits.preValidateExponentCalculation(dX, 2); + MathLimits.preValidateExponentCalculation(dY, 2); + MathLimits.preValidateExponentCalculation(dZ, 2); return NodeValue.makeDouble(Math.sqrt(Math.pow(dX, 2) + Math.pow(dY, 2) + Math.pow(dZ, 2))); } default: diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/function/library/leviathan/cube.java b/jena-arq/src/main/java/org/apache/jena/sparql/function/library/leviathan/cube.java index c4aee9f2f2..136afa9d7d 100644 --- a/jena-arq/src/main/java/org/apache/jena/sparql/function/library/leviathan/cube.java +++ b/jena-arq/src/main/java/org/apache/jena/sparql/function/library/leviathan/cube.java @@ -27,6 +27,7 @@ import org.apache.jena.sparql.ARQInternalErrorException; import org.apache.jena.sparql.expr.NodeValue; import org.apache.jena.sparql.expr.nodevalue.XSDFuncOp; import org.apache.jena.sparql.function.FunctionBase1; +import org.apache.jena.sparql.function.MathLimits; public class cube extends FunctionBase1 { @@ -36,13 +37,15 @@ public class cube extends FunctionBase1 { { case OP_INTEGER: BigInteger i = v.getInteger(); + MathLimits.preValidateExponentCalculation(i, BigInteger.valueOf(3)); return NodeValue.makeInteger( i.pow(3) ); case OP_DECIMAL: double dec = v.getDecimal().doubleValue(); + MathLimits.preValidateExponentCalculation(dec, 3d); return NodeValue.makeDecimal( Math.pow(dec, 3d)); case OP_FLOAT: - // TODO Should cubing a float keep it a float? case OP_DOUBLE: + MathLimits.preValidateExponentCalculation(v.getDouble(), 3d); return NodeValue.makeDouble( Math.pow(v.getDouble(), 3d) ); default: throw new ARQInternalErrorException("Unrecognized numeric operation : "+v); diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/function/library/leviathan/e.java b/jena-arq/src/main/java/org/apache/jena/sparql/function/library/leviathan/e.java index 66abdf252f..18ca67e3f9 100644 --- a/jena-arq/src/main/java/org/apache/jena/sparql/function/library/leviathan/e.java +++ b/jena-arq/src/main/java/org/apache/jena/sparql/function/library/leviathan/e.java @@ -23,11 +23,13 @@ package org.apache.jena.sparql.function.library.leviathan; import org.apache.jena.sparql.expr.NodeValue; import org.apache.jena.sparql.function.FunctionBase1; +import org.apache.jena.sparql.function.MathLimits; public class e extends FunctionBase1 { @Override public NodeValue exec(NodeValue v) { + MathLimits.preValidateExponentCalculation(Math.E, v.getDouble()); return NodeValue.makeDouble(Math.exp(v.getDouble())); } diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/function/library/leviathan/factorial.java b/jena-arq/src/main/java/org/apache/jena/sparql/function/library/leviathan/factorial.java index d8692e215a..17935d498b 100644 --- a/jena-arq/src/main/java/org/apache/jena/sparql/function/library/leviathan/factorial.java +++ b/jena-arq/src/main/java/org/apache/jena/sparql/function/library/leviathan/factorial.java @@ -26,6 +26,7 @@ import java.math.BigInteger; import org.apache.jena.sparql.expr.ExprEvalException; import org.apache.jena.sparql.expr.NodeValue; import org.apache.jena.sparql.function.FunctionBase1; +import org.apache.jena.sparql.function.MathLimits; public class factorial extends FunctionBase1 { @@ -43,13 +44,14 @@ public class factorial extends FunctionBase1 { case 1: BigInteger res = i.add(BigInteger.ZERO); i = i.subtract(BigInteger.ONE); + MathLimits.preValidateFactorial(i); while (i.compareTo(BigInteger.ZERO) != 0) { res = res.multiply(i); i = i.subtract(BigInteger.ONE); } return NodeValue.makeInteger(res); default: - throw new ExprEvalException("Unexpecte comparison result"); + throw new ExprEvalException("Unexpected comparison result"); } } diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/function/library/leviathan/pow.java b/jena-arq/src/main/java/org/apache/jena/sparql/function/library/leviathan/pow.java index b32a4e3369..38d84a818c 100644 --- a/jena-arq/src/main/java/org/apache/jena/sparql/function/library/leviathan/pow.java +++ b/jena-arq/src/main/java/org/apache/jena/sparql/function/library/leviathan/pow.java @@ -27,6 +27,7 @@ import org.apache.jena.sparql.ARQInternalErrorException; import org.apache.jena.sparql.expr.NodeValue; import org.apache.jena.sparql.expr.nodevalue.XSDFuncOp; import org.apache.jena.sparql.function.FunctionBase2; +import org.apache.jena.sparql.function.MathLimits; public class pow extends FunctionBase2 { @@ -36,13 +37,15 @@ public class pow extends FunctionBase2 { { case OP_INTEGER: BigInteger i = v1.getInteger(); + MathLimits.preValidateExponentCalculation(i, v2.getInteger()); return NodeValue.makeInteger( i.pow(v2.getInteger().intValue()) ); case OP_DECIMAL: double dec = v1.getDecimal().doubleValue(); + MathLimits.preValidateExponentCalculation(dec, v2.getDouble()); return NodeValue.makeDecimal( Math.pow(dec, v2.getDouble())); case OP_FLOAT: - // TODO Should raising a float to a power keep it a float? case OP_DOUBLE: + MathLimits.preValidateExponentCalculation(v1.getDouble(), v2.getDouble()); return NodeValue.makeDouble( Math.pow(v1.getDouble(), v2.getDouble()) ); default: throw new ARQInternalErrorException("Unrecognized numeric operation : "+ v1); diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/function/library/leviathan/pythagoras.java b/jena-arq/src/main/java/org/apache/jena/sparql/function/library/leviathan/pythagoras.java index da691f9538..044419ed9f 100644 --- a/jena-arq/src/main/java/org/apache/jena/sparql/function/library/leviathan/pythagoras.java +++ b/jena-arq/src/main/java/org/apache/jena/sparql/function/library/leviathan/pythagoras.java @@ -23,6 +23,7 @@ package org.apache.jena.sparql.function.library.leviathan; import org.apache.jena.sparql.expr.NodeValue; import org.apache.jena.sparql.function.FunctionBase2; +import org.apache.jena.sparql.function.MathLimits; public class pythagoras extends FunctionBase2 { @@ -31,6 +32,8 @@ public class pythagoras extends FunctionBase2 { double a = v1.getDouble(); double b = v2.getDouble(); + MathLimits.preValidateExponentCalculation(a, 2); + MathLimits.preValidateExponentCalculation(b, 2); return NodeValue.makeDouble(Math.sqrt(Math.pow(a, 2) + Math.pow(b, 2))); } diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/function/library/leviathan/root.java b/jena-arq/src/main/java/org/apache/jena/sparql/function/library/leviathan/root.java index e133217c72..71636461f6 100644 --- a/jena-arq/src/main/java/org/apache/jena/sparql/function/library/leviathan/root.java +++ b/jena-arq/src/main/java/org/apache/jena/sparql/function/library/leviathan/root.java @@ -23,6 +23,7 @@ package org.apache.jena.sparql.function.library.leviathan; import org.apache.jena.sparql.expr.NodeValue; import org.apache.jena.sparql.function.FunctionBase2; +import org.apache.jena.sparql.function.MathLimits; public class root extends FunctionBase2 { @@ -31,6 +32,7 @@ public class root extends FunctionBase2 { double value = v1.getDouble(); double root = v2.getDouble(); + MathLimits.preValidateExponentCalculation(value, 1d / root); return NodeValue.makeDouble(Math.pow(value, 1d / root)); } diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/function/library/leviathan/sq.java b/jena-arq/src/main/java/org/apache/jena/sparql/function/library/leviathan/sq.java index 077f623cb5..d318869112 100644 --- a/jena-arq/src/main/java/org/apache/jena/sparql/function/library/leviathan/sq.java +++ b/jena-arq/src/main/java/org/apache/jena/sparql/function/library/leviathan/sq.java @@ -27,6 +27,7 @@ import org.apache.jena.sparql.ARQInternalErrorException; import org.apache.jena.sparql.expr.NodeValue; import org.apache.jena.sparql.expr.nodevalue.XSDFuncOp; import org.apache.jena.sparql.function.FunctionBase1; +import org.apache.jena.sparql.function.MathLimits; public class sq extends FunctionBase1 { @@ -36,13 +37,15 @@ public class sq extends FunctionBase1 { { case OP_INTEGER: BigInteger i = v.getInteger(); + MathLimits.preValidateExponentCalculation(i, BigInteger.valueOf(2)); return NodeValue.makeInteger( i.pow(2) ); case OP_DECIMAL: double dec = v.getDecimal().doubleValue(); + MathLimits.preValidateExponentCalculation(dec, 2d); return NodeValue.makeDecimal( Math.pow(dec, 2d)); case OP_FLOAT: - // TODO Should squaring a float keep it a float? case OP_DOUBLE: + MathLimits.preValidateExponentCalculation(v.getDouble(), 2d); return NodeValue.makeDouble( Math.pow(v.getDouble(), 2d) ); default: throw new ARQInternalErrorException("Unrecognized numeric operation : "+v); diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/function/library/leviathan/ten.java b/jena-arq/src/main/java/org/apache/jena/sparql/function/library/leviathan/ten.java index 9afcf11015..bf88885922 100644 --- a/jena-arq/src/main/java/org/apache/jena/sparql/function/library/leviathan/ten.java +++ b/jena-arq/src/main/java/org/apache/jena/sparql/function/library/leviathan/ten.java @@ -23,11 +23,13 @@ package org.apache.jena.sparql.function.library.leviathan; import org.apache.jena.sparql.expr.NodeValue; import org.apache.jena.sparql.function.FunctionBase1; +import org.apache.jena.sparql.function.MathLimits; public class ten extends FunctionBase1 { @Override public NodeValue exec(NodeValue v) { + MathLimits.preValidateExponentCalculation(10, v.getDouble()); return NodeValue.makeDouble(Math.pow(10d, v.getDouble())); } diff --git a/jena-arq/src/test/java/org/apache/jena/sparql/algebra/optimize/TestTransformConstantFolding.java b/jena-arq/src/test/java/org/apache/jena/sparql/algebra/optimize/TestTransformConstantFolding.java index 5cf9044355..c77b377068 100644 --- a/jena-arq/src/test/java/org/apache/jena/sparql/algebra/optimize/TestTransformConstantFolding.java +++ b/jena-arq/src/test/java/org/apache/jena/sparql/algebra/optimize/TestTransformConstantFolding.java @@ -174,4 +174,9 @@ public class TestTransformConstantFolding { public void constant_fold_leftjoin_01() { test("(leftjoin (table unit) (table unit) (+ 1 2))", "(leftjoin (table unit) (table unit) (exprlist 3))", transform); } + + @Test + public void constant_fold_math_beyond_limits() { + testNoTransform("(extend (?x (math:pow 2 2000000000)) (table unit))", transform); + } } diff --git a/jena-arq/src/test/java/org/apache/jena/sparql/expr/LibTestExpr.java b/jena-arq/src/test/java/org/apache/jena/sparql/expr/LibTestExpr.java index a40ef65e44..ce4ad057ed 100644 --- a/jena-arq/src/test/java/org/apache/jena/sparql/expr/LibTestExpr.java +++ b/jena-arq/src/test/java/org/apache/jena/sparql/expr/LibTestExpr.java @@ -21,9 +21,6 @@ package org.apache.jena.sparql.expr; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - import java.util.function.Predicate; import org.apache.jena.graph.Node; @@ -39,6 +36,8 @@ import org.apache.jena.sparql.util.Context; import org.apache.jena.sparql.util.ExprUtils; import org.apache.jena.sparql.util.NodeFactoryExtra; +import static org.junit.jupiter.api.Assertions.*; + public class LibTestExpr { private static PrefixMapping pmap = new PrefixMappingImpl(); @@ -169,8 +168,8 @@ public class LibTestExpr { } - public static void testError(String exprString) { + public static <T extends Exception> T testError(String exprString, Class<T> expectedException) { Expr expr = ExprUtils.parse(exprString, pmap); - expr.eval(null, new FunctionEnvBase()); + return assertThrows(expectedException, () -> expr.eval(null, new FunctionEnvBase())); } } diff --git a/jena-arq/src/test/java/org/apache/jena/sparql/expr/TestLeviathanFunctions.java b/jena-arq/src/test/java/org/apache/jena/sparql/expr/TestLeviathanFunctions.java index 45161d3fd3..e0efa3665c 100644 --- a/jena-arq/src/test/java/org/apache/jena/sparql/expr/TestLeviathanFunctions.java +++ b/jena-arq/src/test/java/org/apache/jena/sparql/expr/TestLeviathanFunctions.java @@ -24,14 +24,21 @@ package org.apache.jena.sparql.expr; import static org.apache.jena.sparql.expr.LibTestExpr.test; import static org.apache.jena.sparql.expr.LibTestExpr.testDouble; import static org.apache.jena.sparql.expr.LibTestExpr.testError; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.assertThrows; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Strings; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.apache.jena.sparql.util.NodeFactoryExtra; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; public class TestLeviathanFunctions { @@ -116,12 +123,12 @@ public class TestLeviathanFunctions { @Test public void factorial_05() { - assertThrows(ExprEvalException.class, ()->testError("lfn:factorial(-1)")); + testError("lfn:factorial(-1)", ExprEvalException.class); } @Test public void factorial_06() { - assertThrows(ExprEvalException.class, ()->testError("lfn:factorial(5.4)")); + testError("lfn:factorial(5.4)", ExprEvalException.class); } @Test @@ -230,4 +237,29 @@ public class TestLeviathanFunctions { public void acos_01() { testDouble("lfn:radians-to-degrees(lfn:cos-1(lfn:cos(lfn:degrees-to-radians(60))))", "60", DELTA); } + + public static Stream<Arguments> outOfRangeInputs() { + return Stream.of(Arguments.of("lfn:factorial(2000000000)"), + Arguments.of("lfn:pow(100, 100)"), + Arguments.of("lfn:sq(100.0e100)"), + Arguments.of("lfn:cube(100.0e100)"), + Arguments.of("lfn:ten(2000000000)"), + Arguments.of("lfn:cartesian(0, 0, 1, 2000000000)"), + Arguments.of("lfn:cartesian(0, 0, 2000000000, 1)"), + Arguments.of("lfn:cartesian(0, 0, 0, 1, 1, 2000000000)"), + Arguments.of("lfn:cartesian(0, 0, 0, 1, 2000000000, 1)"), + Arguments.of("lfn:cartesian(0, 0, 0, 2000000000, 1, 1)"), + Arguments.of("lfn:pythagoras(2.0e128, 2)"), + Arguments.of("lfn:root(2" + StringUtils.repeat('0', 64) + ", 1)"), + Arguments.of("lfn:e(2000000000)") + ); + } + + @ParameterizedTest + @MethodSource("outOfRangeInputs") + public void givenOutOfRangeInputsInExpr_whenExecuting_thenErrors(String expr) { + ExprEvalException error = testError(expr, ExprEvalException.class); + assertNotNull(error); + assertTrue(Strings.CI.containsAny(error.getMessage(), "too many digits", "outside double range")); + } } diff --git a/jena-arq/src/test/java/org/apache/jena/sparql/function/library/TS_LibraryFunctions.java b/jena-arq/src/test/java/org/apache/jena/sparql/function/library/TS_LibraryFunctions.java index 75bbe27a64..b6eb8c7a12 100644 --- a/jena-arq/src/test/java/org/apache/jena/sparql/function/library/TS_LibraryFunctions.java +++ b/jena-arq/src/test/java/org/apache/jena/sparql/function/library/TS_LibraryFunctions.java @@ -38,6 +38,7 @@ import org.apache.jena.sparql.expr.NodeValue; , TestFnFunctionsOther.class , TestFnFunctionsFormat.class , TestFnFunctionsCollation.class + , TestMathLimits.class }) public class TS_LibraryFunctions { // Expected warnings off. diff --git a/jena-arq/src/test/java/org/apache/jena/sparql/function/library/TestFnFunctionsNumeric.java b/jena-arq/src/test/java/org/apache/jena/sparql/function/library/TestFnFunctionsNumeric.java index 5c08e9f8e4..821d512daf 100644 --- a/jena-arq/src/test/java/org/apache/jena/sparql/function/library/TestFnFunctionsNumeric.java +++ b/jena-arq/src/test/java/org/apache/jena/sparql/function/library/TestFnFunctionsNumeric.java @@ -22,11 +22,18 @@ package org.apache.jena.sparql.function.library; import static org.apache.jena.sparql.expr.LibTestExpr.test; +import static org.apache.jena.sparql.expr.LibTestExpr.testError; +import org.apache.jena.sparql.expr.ExprEvalException; import org.junit.jupiter.api.Test; import org.apache.jena.sparql.expr.NodeValue; import org.apache.jena.sys.JenaSystem; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; public class TestFnFunctionsNumeric { @@ -54,4 +61,20 @@ public class TestFnFunctionsNumeric { @Test public void exprRoundHalfEven_07() { test("fn:round-half-to-even(35612.25, -2)", NodeValue.makeDecimal(35600)); } // counter-intuitive -- would fail if float/double not translated to decimal @Test public void exprRoundHalfEven_08() { test("fn:round-half-to-even('150.015'^^xsd:float, 2)", NodeValue.makeFloat((float)150.01)); } + + + public static Stream<Arguments> outOfRangeInputs() { + return Stream.of(Arguments.of("math:pow(2,2000000000)"), + Arguments.of("math:pow(2000000000, 16)"), + Arguments.of("math:pow(100,100)"), + Arguments.of("math:pow(2, 2.0e16)"), + Arguments.of("math:exp10(2000000000)"), + Arguments.of("math:exp(2000000000)")); + } + + @ParameterizedTest + @MethodSource("outOfRangeInputs") + public void givenOutOfRangeInputsInExpr_whenExecuting_thenError(String expr) { + testError(expr, ExprEvalException.class); + } } diff --git a/jena-arq/src/test/java/org/apache/jena/sparql/function/library/TestMathLimits.java b/jena-arq/src/test/java/org/apache/jena/sparql/function/library/TestMathLimits.java new file mode 100644 index 0000000000..9b058aa6a9 --- /dev/null +++ b/jena-arq/src/test/java/org/apache/jena/sparql/function/library/TestMathLimits.java @@ -0,0 +1,48 @@ +package org.apache.jena.sparql.function.library; + +import org.apache.jena.sparql.expr.ExprEvalException; +import org.apache.jena.sparql.function.MathLimits; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.math.BigInteger; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class TestMathLimits { + + public static Stream<Arguments> bigIntegerExponents() { + return Stream.of(Arguments.of(BigInteger.valueOf(2), BigInteger.valueOf(16), true), + Arguments.of(BigInteger.valueOf(100), BigInteger.valueOf(100), false), + Arguments.of(BigInteger.valueOf(2), BigInteger.valueOf(2_000_000_000), false), + Arguments.of(BigInteger.valueOf(2_000_000_000), BigInteger.valueOf(16), false)); + } + + @ParameterizedTest + @MethodSource("bigIntegerExponents") + public void givenBigIntegers_whenPreValidatingExponent_thenAsExpected(BigInteger a, BigInteger b, boolean valid) { + if (valid) { + MathLimits.preValidateExponentCalculation(a, b); + } else { + assertThrows(ExprEvalException.class, () -> MathLimits.preValidateExponentCalculation(a, b)); + } + } + + public static Stream<Arguments> factorials() { + return Stream.of(Arguments.of(BigInteger.valueOf(10), true), + Arguments.of(BigInteger.valueOf(63), true), + Arguments.of(BigInteger.valueOf(2_000_000_000), false)); + } + + @ParameterizedTest + @MethodSource("factorials") + public void givenFactorialInputs_whenPreValidating_thenAsExpected(BigInteger n, boolean valid) { + if (valid) { + MathLimits.preValidateFactorial(n); + } else { + assertThrows(ExprEvalException.class, () -> MathLimits.preValidateFactorial(n)); + } + } +}
