From 0dfd62b85c43294278ba2e77ea6c4805ad8d8c44 Mon Sep 17 00:00:00 2001 From: rootvector2 Date: Sat, 13 Jun 2026 12:41:35 +0530 Subject: [PATCH] restrict xpath string-to-number conversion to the number grammar Double.parseDouble/Double.valueOf in InfoSetUtil.doubleValue and number accept Java literals (leading +, exponents, d/f suffixes, hex, Infinity/NaN) that the XPath 1.0 number grammar excludes and which XPath requires to be NaN; gate both on the Number production so e.g. number('1e3') is NaN instead of 1000. --- .../apache/commons/jxpath/ri/InfoSetUtil.java | 36 +++++++++++++------ .../jxpath/ri/compiler/CoreFunctionTest.java | 18 ++++++++++ 2 files changed, 44 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/apache/commons/jxpath/ri/InfoSetUtil.java b/src/main/java/org/apache/commons/jxpath/ri/InfoSetUtil.java index 5b19b0180..a5f337034 100644 --- a/src/main/java/org/apache/commons/jxpath/ri/InfoSetUtil.java +++ b/src/main/java/org/apache/commons/jxpath/ri/InfoSetUtil.java @@ -17,6 +17,8 @@ package org.apache.commons.jxpath.ri; +import java.util.regex.Pattern; + import org.apache.commons.jxpath.Pointer; import org.apache.commons.jxpath.ri.model.NodePointer; import org.apache.commons.jxpath.ri.model.VariablePointer; @@ -30,6 +32,27 @@ public class InfoSetUtil { private static final Double ONE = Double.valueOf(1); private static final Double NOT_A_NUMBER = Double.valueOf(Double.NaN); + /** + * The lexical space a string may occupy to be converted to a number per the XPath 1.0 Number production: optional surrounding whitespace and an optional + * leading minus around {@code Digits ('.' Digits?)? | '.' Digits}. {@link Double#parseDouble(String)} additionally accepts Java-only forms (a leading + * {@code +}, exponents, {@code d}/{@code f} type suffixes, hexadecimal floats and the {@code Infinity}/{@code NaN} words), none of which are XPath numbers + * and all of which must convert to NaN. + */ + private static final Pattern XPATH_NUMBER = Pattern.compile("[ \t\r\n]*-?(?:[0-9]+(?:\\.[0-9]*)?|\\.[0-9]+)[ \t\r\n]*"); + + /** + * Converts a string to a double using XPath rules, returning NaN for any string outside the XPath number lexical space. + * + * @param string value to convert + * @return double value or NaN + */ + private static double parseNumber(final String string) { + if (XPATH_NUMBER.matcher(string).matches()) { + return Double.parseDouble(string.trim()); + } + return Double.NaN; + } + /** * Converts the supplied object to boolean. * @@ -81,11 +104,7 @@ public static double doubleValue(final Object object) { if (object.equals("")) { return 0.0; } - try { - return Double.parseDouble((String) object); - } catch (final NumberFormatException ex) { - return Double.NaN; - } + return parseNumber((String) object); } if (object instanceof NodePointer) { return doubleValue(((NodePointer) object).getValue()); @@ -112,11 +131,8 @@ public static Number number(final Object object) { return ((Boolean) object).booleanValue() ? ONE : ZERO; } if (object instanceof String) { - try { - return Double.valueOf((String) object); - } catch (final NumberFormatException ex) { - return NOT_A_NUMBER; - } + final double value = parseNumber((String) object); + return Double.isNaN(value) ? NOT_A_NUMBER : Double.valueOf(value); } if (object instanceof EvalContext) { final EvalContext ctx = (EvalContext) object; diff --git a/src/test/java/org/apache/commons/jxpath/ri/compiler/CoreFunctionTest.java b/src/test/java/org/apache/commons/jxpath/ri/compiler/CoreFunctionTest.java index 251a0a437..db68bde51 100644 --- a/src/test/java/org/apache/commons/jxpath/ri/compiler/CoreFunctionTest.java +++ b/src/test/java/org/apache/commons/jxpath/ri/compiler/CoreFunctionTest.java @@ -114,6 +114,24 @@ void testCoreFunctions() { assertXPathValue(context, "round(2 div 0)", Double.valueOf(Double.POSITIVE_INFINITY)); } + @Test + void testNumberConversionIsXPathConformant() { + // Java number literals that are outside the XPath 1.0 number grammar must convert to NaN. + assertXPathValue(context, "number('1e3')", Double.valueOf(Double.NaN)); + assertXPathValue(context, "number('5d')", Double.valueOf(Double.NaN)); + assertXPathValue(context, "number('5f')", Double.valueOf(Double.NaN)); + assertXPathValue(context, "number('+5')", Double.valueOf(Double.NaN)); + assertXPathValue(context, "number('Infinity')", Double.valueOf(Double.NaN)); + // Valid XPath numbers still convert. + assertXPathValue(context, "number('1')", Double.valueOf(1)); + assertXPathValue(context, "number('1.5')", Double.valueOf(1.5)); + assertXPathValue(context, "number('-.5')", Double.valueOf(-0.5)); + assertXPathValue(context, "number(' 42 ')", Double.valueOf(42)); + // doubleValue() agrees: a non-XPath number is NaN, so numeric comparisons are false. + assertXPathValue(context, "'5d' >= 5", Boolean.FALSE); + assertXPathValue(context, "'1e3' = 1000", Boolean.FALSE); + } + @Test void testExtendedKeyFunction() { context.setKeyManager(new ExtendedKeyManager() {