Skip to main content

libxml_rs/xml/xpath/
functions.rs

1//! XPath 1.0 Core Function Library (§25).
2//!
3//! Implements all XPath 1.0 core functions as specified in §4 of the
4//! XPath 1.0 Recommendation.
5//!
6//! # UPSTREAM-PARITY
7//!
8//! All functions follow the XPath 1.0 specification and libxml2
9//! observable behavior, including edge cases and historical quirks.
10//!
11//! # Courts
12//!
13//! XPATH-FUNCTIONS-*
14//!
15//! # Upstream contract
16//!
17//! Mirrors the core-function library of upstream `xpath.c`
18//! (`SRC-LIBXML2-2.15.0-XPATH-C`, parity target libxml2 2.15.3 oracle):
19//! the 25 XPath 1.0 §4 functions (node-set, string, boolean, number
20//! groups) with libxml2 observable edge cases.
21//!
22//! # Conceptual behavior
23//!
24//! Implements each function over already-evaluated `XPathValue` arguments
25//! with the upstream argument-count and coercion behavior: node-set
26//! functions (last, position, count, id, local-name, namespace-uri, name),
27//! string functions (string, concat, starts-with, substring, translate,
28//! ...), boolean functions and number functions (number, sum, floor,
29//! ceiling, round). number()/string() route through the R-000166 number
30//! formatter (1e9/1e-5 scientific threshold, DBL_DIG=15 fraction digits).
31//!
32//! # Ownership & safety invariants
33//!
34//! Functions return owned `XPathValue`s; node-set arguments are borrowed
35//! views over the tree (valid for the call). No function stores or caches
36//! argument pointers — values are copied at the boundary, so the registry
37//! is safe to share.
38//!
39//! # Historical quirks & epochs
40//!
41//! R-000114 (attribute string-value must be the attribute content, not
42//! empty) and R-000166 (full double-precision value-of printing) were
43//! fixed against the 2.15.3 oracle; the number() corpus (967/967 cases)
44//! locks the formatting epoch. The E-008 stable libxslt epoch means any
45//! function-level divergence is a candidate bug, not an epoch difference.
46//!
47//! # Deliberate oddities
48//!
49//! round()/floor()/ceiling() reproduce libxml2 IEEE-754 handling
50//! (including negative zero and NaN propagation) rather than Rust
51//! rounding helpers, which differ on ties and sign.
52//!
53//! # Proving courts
54//!
55//! XPATH-FUNCTIONS-* differential probes and the 967/967 number() corpus
56//! compare results byte-identical against the oracle; the XSLT courts
57//! (CLI-XSLTPROC-0014/0015/0017) exercise value-of/format-number through
58//! these functions.
59//!
60//! # Tempting simplifications that would break parity
61//!
62//! Do not delegate number formatting to Rust float formatting: the
63//! scientific threshold, digit counts and exponent padding are
64//! oracle-observable (R-000166). Do not coerce arguments more eagerly
65//! than upstream (e.g. empty node-sets to string) — R-000114 proved the
66//! string-value rules are observable.
67
68use crate::xml::xpath::context::XPathContext;
69use crate::xml::xpath::types::{node_string_value, string_to_number, NodeSet, XPathValue};
70use std::collections::HashMap;
71
72/// Type alias for XPath functions.
73///
74/// Functions receive already-evaluated arguments as `XPathValue` slices.
75pub type XPathFunction = fn(&mut XPathContext, &[XPathValue]) -> Result<XPathValue, String>;
76
77/// Get all registered XPath core functions.
78pub fn core_functions() -> HashMap<String, XPathFunction> {
79    let mut funcs: HashMap<String, XPathFunction> = HashMap::new();
80
81    // Node set functions (§4.1)
82    funcs.insert("last".into(), fn_last);
83    funcs.insert("position".into(), fn_position);
84    funcs.insert("count".into(), fn_count);
85    funcs.insert("id".into(), fn_id);
86    funcs.insert("local-name".into(), fn_local_name);
87    funcs.insert("namespace-uri".into(), fn_namespace_uri);
88    funcs.insert("name".into(), fn_name);
89
90    // String functions (§4.2)
91    funcs.insert("string".into(), fn_string);
92    funcs.insert("concat".into(), fn_concat);
93    funcs.insert("starts-with".into(), fn_starts_with);
94    funcs.insert("contains".into(), fn_contains);
95    funcs.insert("substring-before".into(), fn_substring_before);
96    funcs.insert("substring-after".into(), fn_substring_after);
97    funcs.insert("substring".into(), fn_substring);
98    funcs.insert("string-length".into(), fn_string_length);
99    funcs.insert("normalize-space".into(), fn_normalize_space);
100    funcs.insert("translate".into(), fn_translate);
101
102    // Boolean functions (§4.3)
103    funcs.insert("boolean".into(), fn_boolean);
104    funcs.insert("not".into(), fn_not);
105    funcs.insert("true".into(), fn_true);
106    funcs.insert("false".into(), fn_false);
107    funcs.insert("lang".into(), fn_lang);
108
109    // Number functions (§4.4)
110    funcs.insert("number".into(), fn_number);
111    funcs.insert("sum".into(), fn_sum);
112    funcs.insert("floor".into(), fn_floor);
113    funcs.insert("ceiling".into(), fn_ceiling);
114    funcs.insert("round".into(), fn_round);
115
116    funcs
117}
118
119// ═══════════════════════════════════════════════════════════════════════════════
120// Helper: extract typed arguments
121// ═══════════════════════════════════════════════════════════════════════════════
122
123fn get_string_arg(args: &[XPathValue], index: usize) -> String {
124    if index < args.len() {
125        args[index].as_string()
126    } else {
127        String::new()
128    }
129}
130
131fn get_number_arg(args: &[XPathValue], index: usize) -> f64 {
132    if index < args.len() {
133        args[index].as_number()
134    } else {
135        f64::NAN
136    }
137}
138
139fn get_boolean_arg(args: &[XPathValue], index: usize) -> bool {
140    if index < args.len() {
141        args[index].as_boolean()
142    } else {
143        false
144    }
145}
146
147fn get_node_set_arg(args: &[XPathValue], index: usize) -> NodeSet {
148    if index < args.len() {
149        match &args[index] {
150            XPathValue::NodeSet(ns) => ns.clone(),
151            _ => NodeSet::new(),
152        }
153    } else {
154        NodeSet::new()
155    }
156}
157
158fn get_first_node(
159    ctx: &XPathContext,
160    args: &[XPathValue],
161    index: usize,
162) -> Option<*mut crate::abi::structs::_xmlNode> {
163    if index < args.len() {
164        match &args[index] {
165            XPathValue::NodeSet(ns) => ns.first(),
166            _ => None,
167        }
168    } else {
169        // Default to context node
170        Some(ctx.context_node)
171    }
172}
173
174// ═══════════════════════════════════════════════════════════════════════════════
175// Node Set Functions (§4.1)
176// ═══════════════════════════════════════════════════════════════════════════════
177
178/// last() — context size.
179const fn fn_last(ctx: &mut XPathContext, _args: &[XPathValue]) -> Result<XPathValue, String> {
180    Ok(XPathValue::Number(ctx.last() as f64))
181}
182
183/// position() — context position.
184const fn fn_position(ctx: &mut XPathContext, _args: &[XPathValue]) -> Result<XPathValue, String> {
185    Ok(XPathValue::Number(ctx.position() as f64))
186}
187
188/// count(node-set) — number of nodes in node-set.
189fn fn_count(_ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
190    let ns = get_node_set_arg(args, 0);
191    Ok(XPathValue::Number(ns.len() as f64))
192}
193
194/// id(object) — select elements by ID.
195const fn fn_id(_ctx: &mut XPathContext, _args: &[XPathValue]) -> Result<XPathValue, String> {
196    // id() is complex: requires DTD validation to know which attributes are ID.
197    // For now, return empty node-set.
198    Ok(XPathValue::NodeSet(NodeSet::new()))
199}
200
201/// local-name(node-set?) — local part of name.
202///
203/// # Safety
204///
205/// - The node returned by `get_first_node` must be NULL or a valid
206///   `_xmlNode` that stays alive for the call; its `name` field must be
207///   NULL or a valid NUL-terminated string.
208fn fn_local_name(ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
209    let node = get_first_node(ctx, args, 0);
210    if let Some(node) = node {
211        unsafe {
212            let name = crate::xml::string::xmlstr_to_string((*node).name);
213            // Strip prefix if present
214            if let Some(pos) = name.find(':') {
215                Ok(XPathValue::String(name[pos + 1..].to_string()))
216            } else {
217                Ok(XPathValue::String(name))
218            }
219        }
220    } else {
221        Ok(XPathValue::String(String::new()))
222    }
223}
224
225/// namespace-uri(node-set?) — namespace URI of node.
226///
227/// # Safety
228///
229/// - The node returned by `get_first_node` must be NULL or a valid
230///   `_xmlNode` that stays alive for the call; its `ns` pointer, when
231///   non-NULL, must point to a valid `_xmlNs` whose `href` is NULL or a
232///   valid NUL-terminated string.
233fn fn_namespace_uri(ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
234    let node = get_first_node(ctx, args, 0);
235    if let Some(node) = node {
236        unsafe {
237            if let Some(ns) = (*node).ns.as_ref() {
238                let uri = crate::xml::string::xmlstr_to_string(ns.href);
239                Ok(XPathValue::String(uri))
240            } else {
241                Ok(XPathValue::String(String::new()))
242            }
243        }
244    } else {
245        Ok(XPathValue::String(String::new()))
246    }
247}
248
249/// name(node-set?) — QName of the first node in document order.
250///
251/// Element/attribute nodes bound to a prefixed namespace return
252/// `prefix:local`; everything else follows the local-name rule.
253///
254/// # Safety
255///
256/// - The node returned by `get_first_node` must be NULL or a valid
257///   `_xmlNode` that stays alive for the call; its `name` and `ns` fields
258///   must be valid.
259fn fn_name(ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
260    let node = get_first_node(ctx, args, 0);
261    if let Some(node) = node {
262        unsafe {
263            use crate::abi::types::xmlElementType as ET;
264            let t = (*node).type_;
265            let name = crate::xml::string::xmlstr_to_string((*node).name);
266            if (t == ET::XML_ELEMENT_NODE as i32 || t == ET::XML_ATTRIBUTE_NODE as i32)
267                && !name.is_empty()
268                && !(*node).ns.is_null()
269                && !(*(*node).ns).prefix.is_null()
270            {
271                let prefix = crate::xml::string::xmlstr_to_string((*(*node).ns).prefix);
272                Ok(XPathValue::String(format!("{prefix}:{name}")))
273            } else {
274                Ok(XPathValue::String(name))
275            }
276        }
277    } else {
278        Ok(XPathValue::String(String::new()))
279    }
280}
281
282// ═══════════════════════════════════════════════════════════════════════════════
283// String Functions (§4.2)
284// ═══════════════════════════════════════════════════════════════════════════════
285
286/// string(object?) — convert to string.
287fn fn_string(ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
288    if args.is_empty() {
289        // Default: context node's string value
290        Ok(XPathValue::String(node_string_value(ctx.context_node)))
291    } else {
292        Ok(XPathValue::String(args[0].as_string()))
293    }
294}
295
296/// concat(string, string, ...) — concatenate strings.
297fn fn_concat(_ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
298    let mut result = String::new();
299    for arg in args {
300        result.push_str(&arg.as_string());
301    }
302    Ok(XPathValue::String(result))
303}
304
305/// starts-with(string1, string2) — check prefix.
306fn fn_starts_with(_ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
307    let s1 = get_string_arg(args, 0);
308    let s2 = get_string_arg(args, 1);
309    Ok(XPathValue::Boolean(s1.starts_with(&s2)))
310}
311
312/// contains(string1, string2) — check substring.
313fn fn_contains(_ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
314    let s1 = get_string_arg(args, 0);
315    let s2 = get_string_arg(args, 1);
316    Ok(XPathValue::Boolean(s1.contains(&s2)))
317}
318
319/// substring-before(string1, string2) — before first occurrence.
320fn fn_substring_before(_ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
321    let s1 = get_string_arg(args, 0);
322    let s2 = get_string_arg(args, 1);
323    if let Some(pos) = s1.find(&s2) {
324        Ok(XPathValue::String(s1[..pos].to_string()))
325    } else {
326        Ok(XPathValue::String(String::new()))
327    }
328}
329
330/// substring-after(string1, string2) — after first occurrence.
331fn fn_substring_after(_ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
332    let s1 = get_string_arg(args, 0);
333    let s2 = get_string_arg(args, 1);
334    if let Some(pos) = s1.find(&s2) {
335        Ok(XPathValue::String(s1[pos + s2.len()..].to_string()))
336    } else {
337        Ok(XPathValue::String(String::new()))
338    }
339}
340
341/// substring(string, number, number?) — substring extraction.
342///
343/// UPSTREAM-PARITY (XPath 1.0 §4.2): substring operates on CHARACTERS
344/// (Unicode code points), and a character at 1-based position P is included
345/// when `P >= round(start)` and `P < round(start) + round(length)`. Byte
346/// slicing is wrong for multibyte strings (bug26384: xsl:key over a
347/// Cyrillic value panicked on a non-char-boundary slice).
348fn fn_substring(_ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
349    let s = get_string_arg(args, 0);
350    let start = get_number_arg(args, 1);
351    let has_length = args.len() >= 3;
352    let length = if has_length {
353        get_number_arg(args, 2)
354    } else {
355        f64::MAX
356    };
357
358    let start_r = start.round();
359    let end_r = start_r + length.round();
360
361    let mut out = String::new();
362    for (i, c) in s.chars().enumerate() {
363        let p = (i + 1) as f64;
364        if p >= start_r && p < end_r {
365            out.push(c);
366        }
367    }
368    Ok(XPathValue::String(out))
369}
370
371/// string-length(string?) — length of string.
372///
373/// UPSTREAM-PARITY (XPath 1.0 §4.2): string-length counts CHARACTERS (Unicode
374/// code points), not bytes.
375fn fn_string_length(ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
376    let s = if args.is_empty() {
377        node_string_value(ctx.context_node)
378    } else {
379        get_string_arg(args, 0)
380    };
381    Ok(XPathValue::Number(s.chars().count() as f64))
382}
383
384/// normalize-space(string?) — normalize whitespace.
385fn fn_normalize_space(ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
386    let s = if args.is_empty() {
387        node_string_value(ctx.context_node)
388    } else {
389        get_string_arg(args, 0)
390    };
391    let normalized: Vec<&str> = s.split_whitespace().collect();
392    Ok(XPathValue::String(normalized.join(" ")))
393}
394
395/// translate(string1, string2, string3) — character translation.
396fn fn_translate(_ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
397    let s = get_string_arg(args, 0);
398    let from = get_string_arg(args, 1);
399    let to = get_string_arg(args, 2);
400
401    let result: String = s
402        .chars()
403        .map(|c| {
404            // UPSTREAM-PARITY (XPath 1.0 §4.2): translate maps by CHARACTER
405            // position within the "from" string (byte offsets are wrong for
406            // multibyte "from" strings).
407            if let Some(pos) = from.chars().position(|x| x == c) {
408                let to_chars: Vec<char> = to.chars().collect();
409                if pos < to_chars.len() {
410                    to_chars[pos]
411                } else {
412                    '\0' // Remove character
413                }
414            } else {
415                c
416            }
417        })
418        .filter(|&c| c != '\0')
419        .collect();
420
421    Ok(XPathValue::String(result))
422}
423
424// ═══════════════════════════════════════════════════════════════════════════════
425// Boolean Functions (§4.3)
426// ═══════════════════════════════════════════════════════════════════════════════
427
428/// boolean(object) — convert to boolean.
429fn fn_boolean(_ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
430    Ok(XPathValue::Boolean(get_boolean_arg(args, 0)))
431}
432
433/// not(boolean) — logical NOT.
434fn fn_not(_ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
435    Ok(XPathValue::Boolean(!get_boolean_arg(args, 0)))
436}
437
438/// true() — constant true.
439const fn fn_true(_ctx: &mut XPathContext, _args: &[XPathValue]) -> Result<XPathValue, String> {
440    Ok(XPathValue::Boolean(true))
441}
442
443/// false() — constant false.
444const fn fn_false(_ctx: &mut XPathContext, _args: &[XPathValue]) -> Result<XPathValue, String> {
445    Ok(XPathValue::Boolean(false))
446}
447
448/// lang(string) — language test.
449///
450/// # Safety
451///
452/// - `ctx.context_node` must be NULL or a valid `_xmlNode`; the walk up
453///   the `parent` chain and through each node's `properties` must only
454///   touch valid nodes and attributes whose `name` and child `content`
455///   fields are NULL or valid NUL-terminated strings; the chain must stay
456///   alive and acyclic for the duration of the call.
457fn fn_lang(ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
458    let lang = get_string_arg(args, 0);
459    let mut node = ctx.context_node;
460    unsafe {
461        while !node.is_null() {
462            let mut prop = (*node).properties;
463            while !prop.is_null() {
464                let attr_name = crate::xml::string::xmlstr_to_string((*prop).name);
465                if attr_name == "lang" || attr_name == "xml:lang" {
466                    // Attribute value is stored in children (text node's content)
467                    if !(*prop).children.is_null() {
468                        let attr_val =
469                            crate::xml::string::xmlstr_to_string((*(*prop).children).content);
470                        if attr_val.to_lowercase() == lang.to_lowercase()
471                            || attr_val
472                                .to_lowercase()
473                                .starts_with(&format!("{}-", lang.to_lowercase()))
474                        {
475                            return Ok(XPathValue::Boolean(true));
476                        }
477                    }
478                }
479                prop = (*prop).next;
480            }
481            node = (*node).parent;
482        }
483    }
484    Ok(XPathValue::Boolean(false))
485}
486
487// ═══════════════════════════════════════════════════════════════════════════════
488// Number Functions (§4.4)
489// ═══════════════════════════════════════════════════════════════════════════════
490
491/// number(object?) — convert to number.
492fn fn_number(ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
493    if args.is_empty() {
494        Ok(XPathValue::Number(string_to_number(&node_string_value(
495            ctx.context_node,
496        ))))
497    } else {
498        Ok(XPathValue::Number(get_number_arg(args, 0)))
499    }
500}
501
502/// sum(node-set) — sum of string->number conversions.
503fn fn_sum(_ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
504    let ns = get_node_set_arg(args, 0);
505    let mut total = 0.0;
506    for node in ns.iter() {
507        let s = node_string_value(node);
508        total += string_to_number(&s);
509    }
510    Ok(XPathValue::Number(total))
511}
512
513/// floor(number) — largest integer <= value.
514fn fn_floor(_ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
515    let n = get_number_arg(args, 0);
516    Ok(XPathValue::Number(n.floor()))
517}
518
519/// ceiling(number) — smallest integer >= value.
520fn fn_ceiling(_ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
521    let n = get_number_arg(args, 0);
522    Ok(XPathValue::Number(n.ceil()))
523}
524
525/// round(number) — round to nearest integer.
526///
527/// UPSTREAM-PARITY: XPath 1.0 rounds towards positive infinity for .5 cases.
528/// Rust's f64::round() rounds half away from zero, which differs for negative .5 values.
529/// See XPath 1.0 §4.4.
530fn fn_round(_ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
531    let n = get_number_arg(args, 0);
532    if n.is_nan() || n.is_infinite() || n == 0.0 {
533        return Ok(XPathValue::Number(n));
534    }
535    // Rust's f64::round() uses "round half away from zero"
536    // XPath 1.0 uses "round half towards positive infinity"
537    // These differ for negative numbers with .5 fractional part
538    let rust_rounded = n.round();
539    let result = if n.is_sign_negative() && (n - rust_rounded).abs() == 0.5 {
540        // XPath: move towards positive infinity (i.e., add 1.0 to the Rust result)
541        rust_rounded + 1.0
542    } else {
543        rust_rounded
544    };
545    Ok(XPathValue::Number(result))
546}
547
548// ═══════════════════════════════════════════════════════════════════════════════
549// Tests
550// ═══════════════════════════════════════════════════════════════════════════════
551
552#[cfg(test)]
553mod tests {
554    use super::*;
555
556    #[test]
557    fn test_true_false() {
558        let mut ctx = XPathContext::new(std::ptr::null_mut());
559        assert!(fn_true(&mut ctx, &[]).unwrap().as_boolean());
560        assert!(!fn_false(&mut ctx, &[]).unwrap().as_boolean());
561    }
562
563    #[test]
564    fn test_boolean_conversion() {
565        let mut ctx = XPathContext::new(std::ptr::null_mut());
566        assert!(fn_boolean(&mut ctx, &[XPathValue::Boolean(true)])
567            .unwrap()
568            .as_boolean());
569        assert!(!fn_boolean(&mut ctx, &[XPathValue::Boolean(false)])
570            .unwrap()
571            .as_boolean());
572    }
573
574    #[test]
575    fn test_not() {
576        let mut ctx = XPathContext::new(std::ptr::null_mut());
577        assert!(!fn_not(&mut ctx, &[XPathValue::Boolean(true)])
578            .unwrap()
579            .as_boolean());
580        assert!(fn_not(&mut ctx, &[XPathValue::Boolean(false)])
581            .unwrap()
582            .as_boolean());
583    }
584
585    #[test]
586    fn test_number_round() {
587        let mut ctx = XPathContext::new(std::ptr::null_mut());
588        assert_eq!(
589            fn_floor(&mut ctx, &[XPathValue::Number(3.7)])
590                .unwrap()
591                .as_number(),
592            3.0
593        );
594        assert_eq!(
595            fn_ceiling(&mut ctx, &[XPathValue::Number(3.2)])
596                .unwrap()
597                .as_number(),
598            4.0
599        );
600        assert_eq!(
601            fn_round(&mut ctx, &[XPathValue::Number(3.5)])
602                .unwrap()
603                .as_number(),
604            4.0
605        );
606        assert_eq!(
607            fn_round(&mut ctx, &[XPathValue::Number(-3.5)])
608                .unwrap()
609                .as_number(),
610            -3.0
611        );
612    }
613
614    #[test]
615    fn test_string_functions() {
616        let mut ctx = XPathContext::new(std::ptr::null_mut());
617        assert_eq!(
618            fn_concat(
619                &mut ctx,
620                &[
621                    XPathValue::String("a".into()),
622                    XPathValue::String("b".into()),
623                    XPathValue::String("c".into())
624                ]
625            )
626            .unwrap()
627            .as_string(),
628            "abc"
629        );
630        assert!(fn_starts_with(
631            &mut ctx,
632            &[
633                XPathValue::String("hello".into()),
634                XPathValue::String("he".into())
635            ]
636        )
637        .unwrap()
638        .as_boolean());
639        assert!(!fn_starts_with(
640            &mut ctx,
641            &[
642                XPathValue::String("hello".into()),
643                XPathValue::String("x".into())
644            ]
645        )
646        .unwrap()
647        .as_boolean());
648        assert!(fn_contains(
649            &mut ctx,
650            &[
651                XPathValue::String("hello".into()),
652                XPathValue::String("ell".into())
653            ]
654        )
655        .unwrap()
656        .as_boolean());
657        assert_eq!(
658            fn_string_length(&mut ctx, &[XPathValue::String("hello".into())])
659                .unwrap()
660                .as_number(),
661            5.0
662        );
663    }
664
665    #[test]
666    fn test_core_functions_registered() {
667        let funcs = core_functions();
668        assert!(funcs.contains_key("last"));
669        assert!(funcs.contains_key("position"));
670        assert!(funcs.contains_key("count"));
671        assert!(funcs.contains_key("string"));
672        assert!(funcs.contains_key("concat"));
673        assert!(funcs.contains_key("boolean"));
674        assert!(funcs.contains_key("not"));
675        assert!(funcs.contains_key("number"));
676        assert!(funcs.contains_key("sum"));
677        assert!(funcs.contains_key("floor"));
678        assert!(funcs.contains_key("ceiling"));
679        assert!(funcs.contains_key("round"));
680        assert!(funcs.contains_key("name"));
681        assert!(funcs.contains_key("local-name"));
682        assert_eq!(funcs.len(), 27);
683        assert!(funcs.contains_key("namespace-uri"));
684    }
685}