From bc3245853dd249d5c8e1769e52d13c5b399c2e9d Mon Sep 17 00:00:00 2001 From: Joe Afflerbach Date: Fri, 29 May 2026 14:27:52 +0200 Subject: [PATCH] ext/dom: Return numeric results from XPath callbacks as numbers not as string see https://www.w3.org/TR/xpath-10/#numbers --- UPGRADING | 4 ++ .../return_numeric_strings_from_xpath.phpt | 61 +++++++++++++++++++ .../modern/xml/return_scalars_from_xpath.phpt | 49 +++++++++++++++ ext/dom/xpath_callbacks.c | 10 +++ ...registerPHPFunctionNS-numeric-results.phpt | 22 +++++++ 5 files changed, 146 insertions(+) create mode 100644 ext/dom/tests/modern/xml/return_numeric_strings_from_xpath.phpt create mode 100644 ext/dom/tests/modern/xml/return_scalars_from_xpath.phpt create mode 100644 ext/xsl/tests/xsltprocessor_registerPHPFunctionNS-numeric-results.phpt diff --git a/UPGRADING b/UPGRADING index a3b22d8c0560..60090399f002 100644 --- a/UPGRADING +++ b/UPGRADING @@ -28,6 +28,10 @@ PHP 8.6 UPGRADE NOTES from global scope" instead of the prior readonly modification error. ReflectionProperty::isWritable() also reports these properties accurately. + . Custom XPath functions registered via DOMXPath::registerPhpFunctionNS + or Dom\XPath::registerPhpFunctionNS returning 'int' or 'float' numbers + now provide XPath 'number' scalars. Very large integers >= 2^53 are + still returned as XPath strings to not lose precision. - GD: . imagesetstyle(), imagefilter() and imagecrop() filter their diff --git a/ext/dom/tests/modern/xml/return_numeric_strings_from_xpath.phpt b/ext/dom/tests/modern/xml/return_numeric_strings_from_xpath.phpt new file mode 100644 index 000000000000..44659642a6a9 --- /dev/null +++ b/ext/dom/tests/modern/xml/return_numeric_strings_from_xpath.phpt @@ -0,0 +1,61 @@ +--TEST-- +Returning numeric string values from Dom\XPath callback functions for large integers +--EXTENSIONS-- +dom +--SKIPIF-- + +--FILE-- +'); +$xpath = new Dom\XPath($document); +$xpath->registerPhpFunctionNs('urn:x', 'get-int', fn(int $i): int => $i); +$xpath->registerPhpFunctionNs('urn:x', 'get-float', fn(float $f): float => $f); +$xpath->registerPhpFunctionNs('urn:x', 'get-max-safe-int', fn() => MAX_SAFE_INTEGER); +$xpath->registerPhpFunctionNs('urn:x', 'get-negative-max-safe-int', fn() => -MAX_SAFE_INTEGER); +$xpath->registerPhpFunctionNs('urn:x', 'get-large-int', fn() => MAX_SAFE_INTEGER+2); +$xpath->registerPhpFunctionNs('urn:x', 'get-negative-large-int', fn() => -MAX_SAFE_INTEGER-2); +$xpath->registerNamespace('x', 'urn:x'); + +var_dump($xpath->evaluate('x:get-float('.PHP_INT_MAX.')')); +var_dump($xpath->evaluate('x:get-float('.PHP_INT_MIN.')')); +var_dump($xpath->evaluate('x:get-float('.PHP_FLOAT_MAX.')')); +var_dump($xpath->evaluate('x:get-float('.PHP_FLOAT_MIN.')')); +var_dump($xpath->evaluate('x:get-float('.PHP_FLOAT_EPSILON.')')); + +var_dump(MAX_SAFE_INTEGER); +var_dump(floatval(MAX_SAFE_INTEGER)); +var_dump($xpath->evaluate("x:get-int(".(MAX_SAFE_INTEGER).")")); +var_dump($xpath->evaluate("x:get-int(".(-MAX_SAFE_INTEGER).")")); +var_dump($xpath->evaluate("x:get-max-safe-int()")); +var_dump($xpath->evaluate("x:get-negative-max-safe-int()")); + +var_dump(MAX_SAFE_INTEGER+2); +// loses precision while type casting +var_dump(floatval(MAX_SAFE_INTEGER+2)); +// loses precision while parameter passing int -> XPath number +var_dump($xpath->evaluate("x:get-int(".(MAX_SAFE_INTEGER+2).")")); +var_dump($xpath->evaluate("x:get-int(".(-MAX_SAFE_INTEGER-2).")")); +// returns string values for integers larger than 2^53-1 to maintain precision +var_dump($xpath->evaluate("x:get-large-int()")); +var_dump($xpath->evaluate("x:get-negative-large-int()")); +?> +--EXPECT-- +float(9.223372036854778E+18) +float(-9.223372036854778E+18) +float(1.7976931348623E+308) +float(2.2250738585072E-308) +float(2.2204460492503003E-16) +int(9007199254740991) +float(9007199254740991) +float(9007199254740991) +float(-9007199254740991) +float(9007199254740991) +float(-9007199254740991) +int(9007199254740993) +float(9007199254740992) +string(16) "9007199254740992" +string(17) "-9007199254740992" +string(16) "9007199254740993" +string(17) "-9007199254740993" diff --git a/ext/dom/tests/modern/xml/return_scalars_from_xpath.phpt b/ext/dom/tests/modern/xml/return_scalars_from_xpath.phpt new file mode 100644 index 000000000000..c5514fcc2880 --- /dev/null +++ b/ext/dom/tests/modern/xml/return_scalars_from_xpath.phpt @@ -0,0 +1,49 @@ +--TEST-- +Returning scalar values from Dom\XPath callback functions +--EXTENSIONS-- +dom +--FILE-- +'); +$xpath = new Dom\XPath($document); +$xpath->registerPhpFunctionNs('urn:x', 'get-int', fn(int $i): int => $i); +$xpath->registerPhpFunctionNs('urn:x', 'get-float', fn(float $f): float => $f); +$xpath->registerPhpFunctionNs('urn:x', 'get-string', fn(string $s): string => $s); +$xpath->registerPhpFunctionNs('urn:x', 'get-bool', fn(bool $b): bool => $b); +$xpath->registerNamespace('x', 'urn:x'); + +var_dump($xpath->evaluate('x:get-string("test")')); +var_dump($xpath->evaluate('x:get-bool(1)')); +var_dump($xpath->evaluate('x:get-bool(0)')); + +var_dump($xpath->evaluate('x:get-int(41)')); +var_dump($xpath->evaluate('x:get-int(41) + 1')); +var_dump($xpath->evaluate('x:get-int(41)') + 1); +var_dump($xpath->evaluate('x:get-float(4.2)')); +var_dump($xpath->evaluate('2 * x:get-float(4.2)')); +var_dump(2 * $xpath->evaluate('x:get-float(4.2)')); + +var_dump($xpath->evaluate('x:get-int(0)')); +var_dump($xpath->evaluate('x:get-int(-0)')); +var_dump($xpath->evaluate('x:get-float(0)')); +var_dump($xpath->evaluate('x:get-float(-0)')); + +var_dump($xpath->evaluate('x:get-float(number("invalid"))')); +var_dump($xpath->evaluate('x:get-float(1 div 0)')); +?> +--EXPECT-- +string(4) "test" +bool(true) +bool(false) +float(41) +float(42) +float(42) +float(4.2) +float(8.4) +float(8.4) +float(0) +float(0) +float(0) +float(-0) +float(NAN) +float(INF) diff --git a/ext/dom/xpath_callbacks.c b/ext/dom/xpath_callbacks.c index 90395dc15f2e..e3fb5331a797 100644 --- a/ext/dom/xpath_callbacks.c +++ b/ext/dom/xpath_callbacks.c @@ -25,6 +25,11 @@ #include "internal_helpers.h" #include +#ifndef DBL_MANT_DIG +# define DBL_MANT_DIG 53 +#endif +#define MAX_SAFE_INTEGER ((1LL << DBL_MANT_DIG) - 1) + static void xpath_callbacks_entry_dtor(zval *zv) { zend_fcall_info_cache *fcc = Z_PTR_P(zv); @@ -438,6 +443,11 @@ static zend_result php_dom_xpath_callback_dispatch(php_dom_xpath_callbacks *xpat valuePush(ctxt, xmlXPathNewNodeSet(nodep)); } else if (Z_TYPE(callback_retval) == IS_FALSE || Z_TYPE(callback_retval) == IS_TRUE) { valuePush(ctxt, xmlXPathNewBoolean(Z_TYPE(callback_retval) == IS_TRUE)); + } else if (Z_TYPE(callback_retval) == IS_LONG && + Z_LVAL(callback_retval) >= -MAX_SAFE_INTEGER && Z_LVAL(callback_retval) <= MAX_SAFE_INTEGER) { + valuePush(ctxt, xmlXPathNewFloat(Z_LVAL(callback_retval))); + } else if (Z_TYPE(callback_retval) == IS_DOUBLE) { + valuePush(ctxt, xmlXPathNewFloat(Z_DVAL(callback_retval))); } else if (Z_TYPE(callback_retval) == IS_OBJECT) { zend_type_error("Only objects that are instances of DOM nodes can be converted to an XPath expression"); zval_ptr_dtor(&callback_retval); diff --git a/ext/xsl/tests/xsltprocessor_registerPHPFunctionNS-numeric-results.phpt b/ext/xsl/tests/xsltprocessor_registerPHPFunctionNS-numeric-results.phpt new file mode 100644 index 000000000000..34059a74c347 --- /dev/null +++ b/ext/xsl/tests/xsltprocessor_registerPHPFunctionNS-numeric-results.phpt @@ -0,0 +1,22 @@ +--TEST-- +Returning numeric values from XSLTProcessor callbacks +--EXTENSIONS-- +xsl +--FILE-- +'); +$xslt =<< + +count: + +END; +$stylesheet = Dom\XMLDocument::createFromString($xslt); +$xsltProcessor = new XSLTProcessor; +$xsltProcessor->importStylesheet($stylesheet); +$xsltProcessor->registerPHPFunctionNS('urn:my.ns', 'count', fn(array $arg1) => count($arg1)); +var_dump($xsltProcessor->transformToXML($document)); +?> +--EXPECT-- +string(8) "count: 4"