Skip to content

Commit cdc91fe

Browse files
committed
ext/dom: Return numeric results from XPath callbacks as numbers not as string
see https://www.w3.org/TR/xpath-10/#numbers
1 parent cd2e060 commit cdc91fe

5 files changed

Lines changed: 147 additions & 0 deletions

File tree

UPGRADING

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ PHP 8.6 UPGRADE NOTES
2828
from global scope" instead of the prior readonly modification error.
2929
ReflectionProperty::isWritable() also reports these properties
3030
accurately.
31+
. Custom XPath functions registered via DOMXPath::registerPhpFunctionNS
32+
or Dom\XPath::registerPhpFunctionNS returning 'int' or 'float' numbers
33+
now provide XPath 'number' scalars. Very large integers >= 2^53 are
34+
still returned as XPath strings to not lose precision.
3135

3236
- GD:
3337
. imagesetstyle(), imagefilter() and imagecrop() filter their
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
--TEST--
2+
Returning numeric string values from Dom\XPath callback functions for large integers
3+
--EXTENSIONS--
4+
dom
5+
--SKIPIF--
6+
<?php if (PHP_INT_SIZE !== 8) die("skip 64-bit only"); ?>
7+
--FILE--
8+
<?php
9+
define('MAX_SAFE_INTEGER', 2**53-1);
10+
11+
$document = Dom\XMLDocument::createFromString('<root/>');
12+
$xpath = new Dom\XPath($document);
13+
$xpath->registerPhpFunctionNs('urn:x', 'get-int', fn(int $i): int => $i);
14+
$xpath->registerPhpFunctionNs('urn:x', 'get-float', fn(float $f): float => $f);
15+
$xpath->registerPhpFunctionNs('urn:x', 'get-max-safe-int', fn() => MAX_SAFE_INTEGER);
16+
$xpath->registerPhpFunctionNs('urn:x', 'get-negative-max-safe-int', fn() => -MAX_SAFE_INTEGER);
17+
$xpath->registerPhpFunctionNs('urn:x', 'get-large-int', fn() => MAX_SAFE_INTEGER+2);
18+
$xpath->registerPhpFunctionNs('urn:x', 'get-negative-large-int', fn() => -MAX_SAFE_INTEGER-2);
19+
$xpath->registerNamespace('x', 'urn:x');
20+
21+
var_dump($xpath->evaluate('x:get-float('.PHP_INT_MAX.')'));
22+
var_dump($xpath->evaluate('x:get-float('.PHP_INT_MIN.')'));
23+
24+
var_dump(MAX_SAFE_INTEGER);
25+
var_dump(floatval(MAX_SAFE_INTEGER));
26+
var_dump($xpath->evaluate("x:get-int(".(MAX_SAFE_INTEGER).")"));
27+
var_dump($xpath->evaluate("x:get-int(".(-MAX_SAFE_INTEGER).")"));
28+
var_dump($xpath->evaluate("x:get-max-safe-int()"));
29+
var_dump($xpath->evaluate("x:get-negative-max-safe-int()"));
30+
31+
var_dump(MAX_SAFE_INTEGER+2);
32+
// loses precision while type casting
33+
var_dump(floatval(MAX_SAFE_INTEGER+2));
34+
// loses precision while parameter passing int -> XPath number
35+
var_dump($xpath->evaluate("x:get-int(".(MAX_SAFE_INTEGER+2).")"));
36+
var_dump($xpath->evaluate("x:get-int(".(-MAX_SAFE_INTEGER-2).")"));
37+
// returns string values for integers larger than 2^53-1 to maintain precision
38+
var_dump($xpath->evaluate("x:get-large-int()"));
39+
var_dump($xpath->evaluate("x:get-negative-large-int()"));
40+
?>
41+
--EXPECT--
42+
float(9.223372036854778E+18)
43+
float(-9.223372036854778E+18)
44+
int(9007199254740991)
45+
float(9007199254740991)
46+
float(9007199254740991)
47+
float(-9007199254740991)
48+
float(9007199254740991)
49+
float(-9007199254740991)
50+
int(9007199254740993)
51+
float(9007199254740992)
52+
string(16) "9007199254740992"
53+
string(17) "-9007199254740992"
54+
string(16) "9007199254740993"
55+
string(17) "-9007199254740993"
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
--TEST--
2+
Returning scalar values from Dom\XPath callback functions
3+
--EXTENSIONS--
4+
dom
5+
--FILE--
6+
<?php
7+
$document = Dom\XMLDocument::createFromString('<root/>');
8+
$xpath = new Dom\XPath($document);
9+
$xpath->registerPhpFunctionNs('urn:x', 'get-int', fn(int $i): int => $i);
10+
$xpath->registerPhpFunctionNs('urn:x', 'get-float', fn(float $f): float => $f);
11+
$xpath->registerPhpFunctionNs('urn:x', 'get-string', fn(string $s): string => $s);
12+
$xpath->registerPhpFunctionNs('urn:x', 'get-bool', fn(bool $b): bool => $b);
13+
$xpath->registerNamespace('x', 'urn:x');
14+
15+
var_dump($xpath->evaluate('x:get-string("test")'));
16+
var_dump($xpath->evaluate('x:get-bool(1)'));
17+
var_dump($xpath->evaluate('x:get-bool(0)'));
18+
19+
var_dump($xpath->evaluate('x:get-int(41)'));
20+
var_dump($xpath->evaluate('x:get-int(41) + 1'));
21+
var_dump($xpath->evaluate('x:get-int(41)') + 1);
22+
var_dump($xpath->evaluate('x:get-float(4.2)'));
23+
var_dump($xpath->evaluate('2 * x:get-float(4.2)'));
24+
var_dump(2 * $xpath->evaluate('x:get-float(4.2)'));
25+
26+
var_dump($xpath->evaluate('x:get-int(0)'));
27+
var_dump($xpath->evaluate('x:get-int(-0)'));
28+
var_dump($xpath->evaluate('x:get-float(0)'));
29+
var_dump($xpath->evaluate('x:get-float(-0)'));
30+
31+
var_dump($xpath->evaluate('x:get-float(number("invalid"))'));
32+
var_dump($xpath->evaluate('x:get-float(1 div 0)'));
33+
34+
var_dump($xpath->evaluate('x:get-float('.PHP_FLOAT_MAX.')'));
35+
var_dump($xpath->evaluate('x:get-float('.PHP_FLOAT_MIN.')'));
36+
var_dump($xpath->evaluate('x:get-float('.PHP_FLOAT_EPSILON.')'));
37+
?>
38+
--EXPECT--
39+
string(4) "test"
40+
bool(true)
41+
bool(false)
42+
float(41)
43+
float(42)
44+
float(42)
45+
float(4.2)
46+
float(8.4)
47+
float(8.4)
48+
float(0)
49+
float(0)
50+
float(0)
51+
float(-0)
52+
float(NAN)
53+
float(INF)
54+
float(1.7976931348623E+308)
55+
float(2.2250738585072E-308)
56+
float(2.2204460492503003E-16)

ext/dom/xpath_callbacks.c

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@
2525
#include "internal_helpers.h"
2626
#include <libxml/parserInternals.h>
2727

28+
#ifndef DBL_MANT_DIG
29+
# define DBL_MANT_DIG 53
30+
#endif
31+
#define MAX_SAFE_INTEGER ((1LL << DBL_MANT_DIG) - 1)
32+
2833
static void xpath_callbacks_entry_dtor(zval *zv)
2934
{
3035
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
438443
valuePush(ctxt, xmlXPathNewNodeSet(nodep));
439444
} else if (Z_TYPE(callback_retval) == IS_FALSE || Z_TYPE(callback_retval) == IS_TRUE) {
440445
valuePush(ctxt, xmlXPathNewBoolean(Z_TYPE(callback_retval) == IS_TRUE));
446+
} else if (Z_TYPE(callback_retval) == IS_LONG &&
447+
Z_LVAL(callback_retval) >= -MAX_SAFE_INTEGER && Z_LVAL(callback_retval) <= MAX_SAFE_INTEGER) {
448+
valuePush(ctxt, xmlXPathNewFloat(Z_LVAL(callback_retval)));
449+
} else if (Z_TYPE(callback_retval) == IS_DOUBLE) {
450+
valuePush(ctxt, xmlXPathNewFloat(Z_DVAL(callback_retval)));
441451
} else if (Z_TYPE(callback_retval) == IS_OBJECT) {
442452
zend_type_error("Only objects that are instances of DOM nodes can be converted to an XPath expression");
443453
zval_ptr_dtor(&callback_retval);
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
--TEST--
2+
Returning numeric values from XSLTProcessor callbacks
3+
--EXTENSIONS--
4+
xsl
5+
--FILE--
6+
<?php
7+
$document = Dom\XMLDocument::createFromString('<root><a/><b/><c/><d/></root>');
8+
$xslt =<<<END
9+
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
10+
version="1.0" xmlns:my="urn:my.ns">
11+
<xsl:output method="text" omit-xml-declaration="yes"/>
12+
<xsl:template match="/root">count: <xsl:value-of select="my:count(*)"/></xsl:template>
13+
</xsl:stylesheet>
14+
END;
15+
$stylesheet = Dom\XMLDocument::createFromString($xslt);
16+
$xsltProcessor = new XSLTProcessor;
17+
$xsltProcessor->importStylesheet($stylesheet);
18+
$xsltProcessor->registerPHPFunctionNS('urn:my.ns', 'count', fn(array $arg1) => count($arg1));
19+
var_dump($xsltProcessor->transformToXML($document));
20+
?>
21+
--EXPECT--
22+
string(8) "count: 4"

0 commit comments

Comments
 (0)