Skip to main content

libxml_rs/xml/xpath/
eval.rs

1//! XPath 1.0 Evaluation Engine (§25).
2//!
3//! Evaluates compiled XPath expressions against XML trees using the
4//! internal Rust representation.
5//!
6//! # UPSTREAM-PARITY
7//!
8//! Full XPath 1.0 evaluation semantics: location paths, axes, node tests,
9//! predicates, functions, operators, type conversions, comparison semantics.
10//!
11//! # Courts
12//!
13//! XPATH-EVAL-*
14//!
15//! # Upstream contract
16//!
17//! Mirrors the evaluation core of upstream `xpath.c`
18//! (`SRC-LIBXML2-2.15.0-XPATH-C`, parity target libxml2 2.15.3 oracle):
19//! xmlXPathEvalExpr semantics for location paths, predicates, operators,
20//! type conversions and comparison rules.
21//!
22//! # Conceptual behavior
23//!
24//! Walks the AST with an explicit recursion guard (`push_recursion` /
25//! `pop_recursion`). Absolute paths start at the document node (the
26//! `_xmlDoc` cast to `_xmlNode`, type 9), NOT at the root element — the
27//! R-000102 fix that aligns `/` with XPath 1.0. Predicates are evaluated
28//! per-step with live context position/size; node-sets are deduplicated
29//! in document order.
30//!
31//! # Ownership & safety invariants
32//!
33//! Evaluation borrows the tree and the context: node-sets hold borrowed
34//! `_xmlNode` pointers that stay valid while the document lives; the
35//! engine never frees tree nodes. Values returned to the C ABI are
36//! converted in exports.rs and owned by the `_xmlXPathObject`. Context
37//! mutation is confined to the evaluation call (no global state).
38//!
39//! # Historical quirks & epochs
40//!
41//! R-000102: absolute paths originally evaluated from the root element
42//! (candidate bug) and were fixed to the document node. R-000159:
43//! `//book[position() <= 2]` matched one extra node until the predicate
44//! context-size semantics were aligned with the oracle. Both were sealed
45//! during the 11.1 XPath closure against the 2.15.3 oracle.
46//!
47//! # Deliberate oddities
48//!
49//! The engine evaluates predicates eagerly per step (upstream
50//! xmlXPathCompOpEval style) rather than collecting then filtering —
51//! position()/last() depend on that order, so it is deliberate.
52//!
53//! # Proving courts
54//!
55//! XPATH-EVAL-* and the XPATH differential probes compare evaluation
56//! results byte-identical against the oracle; the XSLT courts
57//! (CLI-XSLTPROC-*) run compiled templates through this engine.
58//!
59//! # Tempting simplifications that would break parity
60//!
61//! Do not defer predicate evaluation to a post-filter pass: position() /
62//! last() are order-sensitive (R-000159). Do not drop the recursion
63//! guard, and do not evaluate absolute paths from the root element
64//! (R-000102) — both are observable through xmlXPathEval results.
65
66use crate::abi::structs::_xmlNode;
67use crate::xml::xpath::ast::{Axis, BinaryOp, Expr, NameTest, NodeTest, Step};
68use crate::xml::xpath::axes;
69use crate::xml::xpath::context::XPathContext;
70use crate::xml::xpath::parser_context::{
71    free_parser_context, new_parser_context, value_pop, value_push,
72};
73use crate::xml::xpath::types::{node_string_value, string_to_number, NodeSet, XPathValue};
74use core::ffi::c_void;
75use std::os::raw::c_int;
76
77// ═══════════════════════════════════════════════════════════════════════════════
78// Evaluation
79// ═══════════════════════════════════════════════════════════════════════════════
80
81/// Evaluate an XPath expression in the given context.
82pub fn eval(ctx: &mut XPathContext, expr: &Expr) -> Result<XPathValue, String> {
83    ctx.push_recursion()?;
84
85    let result = match expr {
86        Expr::Step(step) => eval_step(ctx, step),
87        Expr::AbsolutePath(expr) => eval_absolute_path(ctx, expr),
88        Expr::RelativePath(left, right) => eval_relative_path(ctx, left, right),
89        Expr::Filter(expr, predicates) => eval_filter(ctx, expr, predicates),
90        Expr::Variable(name) => eval_variable(ctx, name),
91        Expr::StringLiteral(s) => Ok(XPathValue::String(s.clone())),
92        Expr::NumberLiteral(n) => Ok(XPathValue::Number(*n)),
93        Expr::BooleanLiteral(b) => Ok(XPathValue::Boolean(*b)),
94        Expr::FunctionCall { name, args } => eval_function_call(ctx, name, args),
95        Expr::BinaryOp { op, left, right } => eval_binary_op(ctx, op, left, right),
96        Expr::UnaryMinus(expr) => {
97            let val = eval(ctx, expr)?;
98            Ok(XPathValue::Number(-val.as_number()))
99        }
100        Expr::Union(left, right) => eval_union(ctx, left, right),
101    };
102
103    ctx.pop_recursion();
104    result
105}
106
107// ═══════════════════════════════════════════════════════════════════════════════
108// Location Path Evaluation
109// ═══════════════════════════════════════════════════════════════════════════════
110
111/// Evaluate an absolute location path: `/foo/bar`.
112fn eval_absolute_path(ctx: &mut XPathContext, expr: &Expr) -> Result<XPathValue, String> {
113    // Start from the document root
114    let doc = ctx.document;
115    if doc.is_null() {
116        return Ok(XPathValue::NodeSet(NodeSet::new()));
117    }
118
119    {
120        // The document node is the _xmlDoc cast to _xmlNode (type 9).
121        // Absolute paths like `/root/item` select relative to the
122        // document node itself, NOT the root element. This matches
123        // XPath 1.0: `/` selects the document root node.
124        let doc_node = doc as *mut _xmlNode;
125
126        // Set context to document node and evaluate the path
127        let saved_node = ctx.context_node;
128        let saved_list = ctx.context_list.clone();
129        ctx.context_node = doc_node;
130        ctx.set_context_list(vec![doc_node]);
131
132        let result = eval(ctx, expr);
133
134        ctx.context_node = saved_node;
135        ctx.context_list = saved_list;
136
137        result
138    }
139}
140
141/// Evaluate a relative location path: `foo/bar`.
142fn eval_relative_path(
143    ctx: &mut XPathContext,
144    left: &Expr,
145    right: &Expr,
146) -> Result<XPathValue, String> {
147    // Evaluate left side to get a node-set. UPSTREAM-PARITY (xpath.c
148    // xmlXPathCompOpEval, FilterExpr '/' RelativeLocationPath): a non-node-set
149    // left operand (e.g. `1/b`) is XPATH_INVALID_TYPE — "XPath error : Invalid
150    // type" — NOT a panic (Phase 16 ASan fuzz finding: `1/b` aborted the
151    // process).
152    let left_val = eval(ctx, left)?;
153    let left_ns = match left_val {
154        XPathValue::NodeSet(ns) => ns,
155        _ => return Err("Invalid type".to_string()),
156    };
157    let left_ns = left_ns.clone();
158
159    let mut result = NodeSet::new();
160
161    for node in left_ns.iter() {
162        // For each node in the left result, evaluate the right step
163        let saved_node = ctx.context_node;
164        let saved_list = ctx.context_list.clone();
165        ctx.context_node = node;
166        ctx.set_context_list(left_ns.iter().collect());
167
168        match eval(ctx, right) {
169            Ok(val) => {
170                if let XPathValue::NodeSet(ns) = val {
171                    for n in ns.iter() {
172                        result.push(n);
173                    }
174                }
175            }
176            Err(e) => {
177                ctx.context_node = saved_node;
178                ctx.context_list = saved_list;
179                return Err(e);
180            }
181        }
182
183        ctx.context_node = saved_node;
184        ctx.context_list = saved_list;
185    }
186
187    // The per-context-node concatenation is append-only and may overlap across
188    // context nodes; restore document order + uniqueness once (NodeSet::push
189    // no longer sorts/dedups per insert).
190    result.sort();
191    Ok(XPathValue::NodeSet(result))
192}
193
194/// Evaluate a single step.
195/// Evaluate one axis step, applying predicates to the traversed node-set.
196///
197/// # Safety
198///
199/// - `ctx.context_node` must be NULL or a valid `_xmlNode` that stays
200///   alive for the call; `traverse_axis` reads the node's subtree, so the
201///   whole reachable tree must be valid and stable while it runs; the
202///   predicate evaluation temporarily mutates the context fields, which
203///   must not be observed by other threads during the call.
204fn eval_step(ctx: &mut XPathContext, step: &Step) -> Result<XPathValue, String> {
205    let context_node = ctx.context_node;
206    if context_node.is_null() {
207        return Ok(XPathValue::NodeSet(NodeSet::new()));
208    }
209
210    // UPSTREAM-PARITY (xpath.c xmlXPathCompQName): a QName / prefix:* node
211    // test resolves its prefix through the context's REGISTERED namespaces
212    // (ctxt->nsHash) — never the document's in-scope declarations. An
213    // unregistered prefix raises XPATH_UNDEF_PREFIX_ERROR
214    // ("Undefined namespace prefix: prefix").
215    let node_test = resolve_step_prefix(&step.node_test, &ctx.namespaces)?;
216
217    // Traverse the axis. Attribute nodes are included for every axis step;
218    // namespace nodes only when the step is explicitly the namespace axis
219    // (upstream collects namespace nodes only on AXIS_NAMESPACE).
220    let include_namespaces = step.axis == Axis::Namespace;
221    let mut result = unsafe {
222        axes::traverse_axis(
223            context_node,
224            step.axis,
225            &node_test,
226            true, // include attributes
227            include_namespaces,
228        )
229    };
230
231    // Apply predicates
232    for predicate in &step.predicates {
233        let mut filtered = NodeSet::new();
234
235        for (i, node) in result.iter().enumerate() {
236            // Set context position and size for this node
237            let saved_node = ctx.context_node;
238            let saved_pos = ctx.context_position;
239            let saved_prox = ctx.proximity_position;
240            let saved_size = ctx.context_size;
241            let saved_list = ctx.context_list.clone();
242
243            ctx.context_node = node;
244            ctx.context_position = (i + 1) as i32;
245            // UPSTREAM-PARITY: position() reads proximityPosition — both
246            // must track the predicate position (R-000159).
247            ctx.proximity_position = (i + 1) as i32;
248            ctx.context_size = result.len() as i32;
249
250            // Evaluate predicate
251            let pred_val = eval(ctx, predicate)?;
252
253            // Predicate is true if:
254            // - It's a number and equals the context position
255            // - It's a boolean and is true
256            // - It converts to true
257            let matches = match pred_val {
258                XPathValue::Number(n) => {
259                    // Number predicate: match if n == context_position
260                    (n - (i as f64 + 1.0)).abs() < f64::EPSILON
261                        || (n.round() as i32) == (i + 1) as i32
262                }
263                _ => pred_val.as_boolean(),
264            };
265
266            if matches {
267                filtered.push(node);
268            }
269
270            ctx.context_node = saved_node;
271            ctx.context_position = saved_pos;
272            ctx.proximity_position = saved_prox;
273            ctx.context_size = saved_size;
274            ctx.context_list = saved_list;
275        }
276
277        result = filtered;
278    }
279
280    Ok(XPathValue::NodeSet(result))
281}
282
283/// Resolve a step's node-test prefix through the registered namespaces,
284/// replacing prefix-based tests with URI-based ones (upstream
285/// `xmlXPathCompQName`/`xmlXPathCompNodeTest` do this at compile time; the
286/// candidate's AST is built without the context, so the resolution happens
287/// here at the start of each step evaluation).
288///
289/// An unregistered prefix is an error (XPATH_UNDEF_PREFIX_ERROR).
290fn resolve_step_prefix(
291    node_test: &NodeTest,
292    namespaces: &std::collections::HashMap<String, String>,
293) -> Result<NodeTest, String> {
294    match node_test {
295        NodeTest::NameTest(NameTest::QName { prefix, local }) if !prefix.is_empty() => {
296            let uri = namespaces
297                .get(prefix)
298                .ok_or_else(|| format!("Undefined namespace prefix: {}", prefix))?;
299            Ok(NodeTest::NameTest(NameTest::QNameUri {
300                uri: uri.clone(),
301                local: local.clone(),
302            }))
303        }
304        NodeTest::NsWildcard(prefix) if !prefix.is_empty() => {
305            let uri = namespaces
306                .get(prefix)
307                .ok_or_else(|| format!("Undefined namespace prefix: {}", prefix))?;
308            Ok(NodeTest::NsWildcardUri(uri.clone()))
309        }
310        _ => Ok(node_test.clone()),
311    }
312}
313
314/// Evaluate a filter expression: `primary[pred1][pred2]`.
315fn eval_filter(
316    ctx: &mut XPathContext,
317    expr: &Expr,
318    predicates: &[Expr],
319) -> Result<XPathValue, String> {
320    // Evaluate the primary expression
321    let mut result = eval(ctx, expr)?;
322
323    // Apply predicates
324    let ns = match &mut result {
325        XPathValue::NodeSet(ns) => ns,
326        _ => return Ok(result), // Non-node-set can't have predicates
327    };
328
329    for predicate in predicates {
330        let mut filtered = NodeSet::new();
331        let nodes: Vec<_> = ns.iter().collect();
332
333        for (i, node) in nodes.iter().enumerate() {
334            let saved_node = ctx.context_node;
335            let saved_pos = ctx.context_position;
336            let saved_prox = ctx.proximity_position;
337            let saved_size = ctx.context_size;
338            let saved_list = ctx.context_list.clone();
339
340            ctx.context_node = *node;
341            ctx.context_position = (i + 1) as i32;
342            // UPSTREAM-PARITY: position() reads proximityPosition (R-000159).
343            ctx.proximity_position = (i + 1) as i32;
344            ctx.context_size = nodes.len() as i32;
345
346            let pred_val = eval(ctx, predicate)?;
347
348            let matches = match pred_val {
349                XPathValue::Number(n) => {
350                    (n - (i as f64 + 1.0)).abs() < f64::EPSILON
351                        || (n.round() as i32) == (i + 1) as i32
352                }
353                _ => pred_val.as_boolean(),
354            };
355
356            if matches {
357                filtered.push(*node);
358            }
359
360            ctx.context_node = saved_node;
361            ctx.context_position = saved_pos;
362            ctx.proximity_position = saved_prox;
363            ctx.context_size = saved_size;
364            ctx.context_list = saved_list;
365        }
366
367        *ns = filtered;
368    }
369
370    Ok(result)
371}
372
373// ═══════════════════════════════════════════════════════════════════════════════
374// Variable / Function Call Evaluation
375// ═══════════════════════════════════════════════════════════════════════════════
376
377/// Evaluate a variable reference.
378fn eval_variable(ctx: &mut XPathContext, name: &str) -> Result<XPathValue, String> {
379    ctx.resolve_variable(name)
380        .ok_or_else(|| format!("Undefined variable: ${}", name))
381}
382
383/// Resolve a function-call name to the qualified `{URI}local` key used by
384/// `xmlXPathRegisterFuncNS`. Unprefixed names stay as-is; a `prefix:local`
385/// name is expanded using the context's registered namespace map. Returns
386/// `None` when the prefix is undeclared or the name is unprefixed.
387fn resolve_function_qualified_name(ctx: &XPathContext, name: &str) -> Option<String> {
388    let (prefix, local) = name.split_once(':')?;
389    let uri = ctx.namespaces.get(prefix)?;
390    Some(format!("{{{}}}{}", uri, local))
391}
392
393/// Evaluate a function call.
394fn eval_function_call(
395    ctx: &mut XPathContext,
396    name: &str,
397    args: &[Expr],
398) -> Result<XPathValue, String> {
399    // Evaluate arguments first
400    let mut evaluated_args = Vec::new();
401    for arg in args {
402        evaluated_args.push(eval(ctx, arg)?);
403    }
404
405    // Look up the function. Namespaced function calls (nokogiri's
406    // `nokogiri-builtin:css-class` registered via xmlXPathRegisterFuncNS with
407    // URI `https://www.nokogiri.org/default_ns/ruby/builtins`) are stored under
408    // the `{URI}local` qualified key. The raw name (e.g. `exsl:node-set`) is
409    // tried first so the XSLT function_lookup closure (which splits on ':') and
410    // prefix-string-registered functions resolve; the qualified key falls back.
411    //
412    // UPSTREAM-PARITY (functions.c xsltXPathFunctionLookup): for a PREFIXED
413    // call whose prefix is declared, the per-context extension registry is
414    // consulted BEFORE the builtin/native tables — xslt gives "priority to
415    // context-level functions" (xmlHashLookup2 on the funcHash, which
416    // xsltRegisterExtFunction fills) over the module registry and the XSLT
417    // builtins. PHP exploits this to OVERRIDE EXSLT builtins
418    // (registerPHPFunctionNS on the EXSLT namespace); native Rust registrations
419    // (e.g. the candidate's EXSLT library) must therefore yield to a
420    // per-context C registration for the same prefix:local name.
421    let namespaced_c_first: Option<crate::xml::xpath::context::BoxedXPathFunction> = {
422        let prefixed = name
423            .split_once(':')
424            .is_some_and(|(p, _)| !p.is_empty() && ctx.namespaces.contains_key(p));
425        if prefixed {
426            ctx.function_lookup.as_ref().and_then(|lk| lk(ctx, name))
427        } else {
428            None
429        }
430    };
431    if let Some(f) = namespaced_c_first {
432        return f(ctx, &evaluated_args);
433    }
434    let mut func_ptr: Option<*const crate::xml::xpath::context::BoxedXPathFunction> = None;
435    if let Some(f) = ctx.lookup_function(name) {
436        func_ptr = Some(f as *const _);
437    } else if let Some(qname) = resolve_function_qualified_name(ctx, name) {
438        if let Some(f) = ctx.lookup_function(&qname) {
439            func_ptr = Some(f as *const _);
440        }
441    }
442    match func_ptr {
443        Some(p) => {
444            // SAFETY: `p` points into `ctx.functions`, which is alive for the
445            // duration of this call and is not mutated during evaluation.
446            let f: &crate::xml::xpath::context::BoxedXPathFunction = unsafe { &*p };
447            f(ctx, &evaluated_args)
448        }
449        None => {
450            // C-registered extension function (xmlXPathRegisterFuncLookup):
451            // consumers like nokogiri register a lookup callback that returns
452            // an xmlXPathFunction; the candidate must invoke it with the
453            // upstream parser-context protocol (name on ctxt->function, args
454            // on the value stack, result popped off the stack).
455            if ctx.func_lookup_func.is_some() {
456                if let Some(result) = invoke_c_extension_function(ctx, name, &evaluated_args) {
457                    return Ok(result);
458                }
459            }
460            // UPSTREAM-PARITY (xpath.c xmlXPathCompFunction): an unknown
461            // function reports "Unregistered function: name"; when the name
462            // carries a prefix whose namespace was never declared, the error
463            // is "Undefined namespace prefix: prefix" instead (both are
464            // XPATH_UNKNOWN_FUNC / XPATH_UNDEF_PREFIX_ERROR, delivered as
465            // "XPath error : ...").
466            let msg = match name.split_once(':') {
467                Some((prefix, _)) if !ctx.namespaces.contains_key(prefix) => {
468                    format!("Undefined namespace prefix: {}", prefix)
469                }
470                _ => format!("Unregistered function: {}", name),
471            };
472            Err(msg)
473        }
474    }
475}
476
477/// Invoke a C-registered XPath extension function through the upstream
478/// parser-context protocol.
479///
480/// UPSTREAM-PARITY (xpath.c `xmlXPathFunctionCall` / `xmlXPathCompFunction`):
481/// the function-lookup callback (`xmlXPathFuncLookupFunc`) is called with a
482/// NUL-terminated name; when it returns a function pointer, the candidate
483/// builds a fresh `xmlXPathParserContext`, sets `ctxt->function` to the
484/// function name (the invoker reads it), pushes the evaluated arguments in
485/// REVERSE order (so the first argument is on top — the function pops them in
486/// order), calls `func(pc, argc)`, and pops the result. Returns `None` when
487/// the callback is absent, returns NULL, or the C context is unavailable.
488///
489/// # Safety
490///
491/// - `ctx.func_lookup_func` / `ctx.func_lookup_data` come from
492///   `xmlXPathRegisterFuncLookup`; `ctx.c_context` must be the owning
493///   `_xmlXPathContext`.
494fn invoke_c_extension_function(
495    ctx: &mut XPathContext,
496    name: &str,
497    args: &[XPathValue],
498) -> Option<XPathValue> {
499    let lookup = ctx.func_lookup_func?;
500    let c_ctxt = ctx.c_context;
501    if c_ctxt.is_null() {
502        return None;
503    }
504
505    // UPSTREAM-PARITY (xpath.c xmlXPathCompFunction): the lookup callback
506    // receives the LOCAL name and the prefix's RESOLVED namespace URI, not
507    // the raw `prefix:local` token — xslt's xsltXPathFunctionLookup and
508    // php's php:function registration (module table keyed by
509    // {http://php.net/xsl}function) match on that. When the prefix is not
510    // declared the raw name + NULL URI are passed (consumers that key on
511    // the raw token, e.g. nokogiri's per-context lookup, keep working).
512    let (lookup_name, lookup_uri): (Vec<u8>, Option<Vec<u8>>) = match name.split_once(':') {
513        Some((prefix, local)) => match ctx.namespaces.get(prefix) {
514            Some(uri) => (local.as_bytes().to_vec(), Some(uri.as_bytes().to_vec())),
515            None => (name.as_bytes().to_vec(), None),
516        },
517        None => (name.as_bytes().to_vec(), None),
518    };
519    let mut lookup_name_nul: Vec<crate::abi::types::xmlChar> = lookup_name.clone();
520    lookup_name_nul.push(0);
521    let lookup_uri_nul: Option<Vec<crate::abi::types::xmlChar>> = lookup_uri.as_ref().map(|u| {
522        let mut v = u.clone();
523        v.push(0);
524        v
525    });
526    // SAFETY: the callback contract returns an xmlXPathFunction (fn pointer)
527    // stored as void*, or NULL when the handler does not provide the name.
528    let fp = unsafe {
529        lookup(
530            ctx.func_lookup_data,
531            lookup_name_nul.as_ptr() as *const crate::abi::types::xmlChar,
532            lookup_uri_nul.as_ref().map_or(std::ptr::null(), |v| {
533                v.as_ptr() as *const crate::abi::types::xmlChar
534            }),
535        )
536    };
537    if fp.is_null() {
538        return None;
539    }
540    let func: unsafe extern "C" fn(*mut c_void, c_int) = unsafe { std::mem::transmute(fp) };
541
542    unsafe {
543        let saved_function = (*c_ctxt).function;
544        (*c_ctxt).function = lookup_name_nul.as_ptr() as *const crate::abi::types::xmlChar;
545        let pc = new_parser_context(std::ptr::null(), c_ctxt);
546        if pc.is_null() {
547            (*c_ctxt).function = saved_function;
548            return None;
549        }
550
551        // Push the evaluated arguments in reverse order (upstream
552        // xmlXPathFunctionCall evaluates and pushes args from the last to the
553        // first, so the first argument sits on top of the stack).
554        let mut ok = true;
555        for val in args.iter().rev() {
556            let obj = crate::abi::exports_xml2::xpath_to_object_pub(val.clone());
557            if obj.is_null() {
558                ok = false;
559                break;
560            }
561            value_push(pc, obj);
562        }
563
564        let result = if ok {
565            func(pc as *mut c_void, args.len() as c_int);
566            let res = value_pop(pc);
567            if res.is_null() {
568                None
569            } else {
570                let val = crate::abi::exports_xml2::object_to_xpathvalue_pub(res);
571                crate::abi::exports_xml2::xmlXPathFreeObject(res);
572                Some(val)
573            }
574        } else {
575            None
576        };
577
578        // free_parser_context frees any remaining stack values and the
579        // context itself; the popped result was already freed above.
580        free_parser_context(pc);
581        (*c_ctxt).function = saved_function;
582        result
583    }
584}
585
586// ═══════════════════════════════════════════════════════════════════════════════
587// Operators
588// ═══════════════════════════════════════════════════════════════════════════════
589
590/// Evaluate a binary operation.
591fn eval_binary_op(
592    ctx: &mut XPathContext,
593    op: &BinaryOp,
594    left: &Expr,
595    right: &Expr,
596) -> Result<XPathValue, String> {
597    match op {
598        BinaryOp::Or => {
599            // Short-circuit: evaluate left, if true return true
600            let left_val = eval(ctx, left)?;
601            if left_val.as_boolean() {
602                return Ok(XPathValue::Boolean(true));
603            }
604            let right_val = eval(ctx, right)?;
605            Ok(XPathValue::Boolean(right_val.as_boolean()))
606        }
607        BinaryOp::And => {
608            // Short-circuit: evaluate left, if false return false
609            let left_val = eval(ctx, left)?;
610            if !left_val.as_boolean() {
611                return Ok(XPathValue::Boolean(false));
612            }
613            let right_val = eval(ctx, right)?;
614            Ok(XPathValue::Boolean(right_val.as_boolean()))
615        }
616        BinaryOp::Eq | BinaryOp::Ne => {
617            let left_val = eval(ctx, left)?;
618            let right_val = eval(ctx, right)?;
619            let eq = compare_equal(ctx, &left_val, &right_val);
620            Ok(match op {
621                BinaryOp::Eq => XPathValue::Boolean(eq),
622                BinaryOp::Ne => XPathValue::Boolean(!eq),
623                _ => unreachable!(),
624            })
625        }
626        BinaryOp::Lt | BinaryOp::Gt | BinaryOp::Le | BinaryOp::Ge => {
627            let left_val = eval(ctx, left)?;
628            let right_val = eval(ctx, right)?;
629            let cmp = compare_ordered(ctx, &left_val, &right_val);
630            let result = match op {
631                BinaryOp::Lt => cmp == std::cmp::Ordering::Less,
632                BinaryOp::Gt => cmp == std::cmp::Ordering::Greater,
633                BinaryOp::Le => cmp != std::cmp::Ordering::Greater,
634                BinaryOp::Ge => cmp != std::cmp::Ordering::Less,
635                _ => unreachable!(),
636            };
637            Ok(XPathValue::Boolean(result))
638        }
639        BinaryOp::Add => {
640            let left_val = eval(ctx, left)?;
641            let right_val = eval(ctx, right)?;
642            Ok(XPathValue::Number(
643                left_val.as_number() + right_val.as_number(),
644            ))
645        }
646        BinaryOp::Sub => {
647            let left_val = eval(ctx, left)?;
648            let right_val = eval(ctx, right)?;
649            Ok(XPathValue::Number(
650                left_val.as_number() - right_val.as_number(),
651            ))
652        }
653        BinaryOp::Mul => {
654            let left_val = eval(ctx, left)?;
655            let right_val = eval(ctx, right)?;
656            Ok(XPathValue::Number(
657                left_val.as_number() * right_val.as_number(),
658            ))
659        }
660        BinaryOp::Div => {
661            let left_val = eval(ctx, left)?;
662            let right_val = eval(ctx, right)?;
663            Ok(XPathValue::Number(
664                left_val.as_number() / right_val.as_number(),
665            ))
666        }
667        BinaryOp::Mod => {
668            let left_val = eval(ctx, left)?;
669            let right_val = eval(ctx, right)?;
670            Ok(XPathValue::Number(
671                left_val.as_number() % right_val.as_number(),
672            ))
673        }
674        BinaryOp::Union => {
675            // Union is handled at the Expr level, not BinaryOp level
676            unreachable!("Union operator should be handled by Expr::Union")
677        }
678    }
679}
680
681/// Evaluate a union expression: `left | right`.
682fn eval_union(ctx: &mut XPathContext, left: &Expr, right: &Expr) -> Result<XPathValue, String> {
683    // UPSTREAM-PARITY (xpath.c xmlXPathUnionExpr): a union of non-node-sets
684    // (e.g. `1 | 2`) is XPATH_INVALID_TYPE — "XPath error : Invalid type" —
685    // not a panic (Phase 16 ASan fuzz finding).
686    let left_val = eval(ctx, left)?;
687    let right_val = eval(ctx, right)?;
688    let mut result = match left_val {
689        XPathValue::NodeSet(ns) => ns,
690        _ => return Err("Invalid type".to_string()),
691    };
692    match right_val {
693        XPathValue::NodeSet(ns) => {
694            result.extend(&ns);
695        }
696        _ => return Err("Invalid type".to_string()),
697    }
698    result.sort();
699
700    Ok(XPathValue::NodeSet(result))
701}
702
703// ═══════════════════════════════════════════════════════════════════════════════
704// Comparison Semantics
705// ═══════════════════════════════════════════════════════════════════════════════
706
707/// Compare two XPath values for equality (XPath 1.0 §3.4).
708fn compare_equal(_ctx: &mut XPathContext, a: &XPathValue, b: &XPathValue) -> bool {
709    match (a, b) {
710        // If both are node-sets, compare by set intersection
711        (XPathValue::NodeSet(ns_a), XPathValue::NodeSet(ns_b)) => {
712            for node_a in ns_a.iter() {
713                let val_a = node_string_value(node_a);
714                for node_b in ns_b.iter() {
715                    let val_b = node_string_value(node_b);
716                    if val_a == val_b {
717                        return true;
718                    }
719                }
720            }
721            false
722        }
723        // If one is a node-set and the other is not
724        (XPathValue::NodeSet(ns), other) | (other, XPathValue::NodeSet(ns)) => {
725            for node in ns.iter() {
726                let node_str = node_string_value(node);
727                match other {
728                    XPathValue::Boolean(_) => {
729                        // Compare boolean(node-set) == other
730                        return (!ns.is_empty()) == other.as_boolean();
731                    }
732                    XPathValue::Number(_) => {
733                        let node_num = string_to_number(&node_str);
734                        if (node_num - other.as_number()).abs() < f64::EPSILON {
735                            return true;
736                        }
737                        continue;
738                    }
739                    XPathValue::String(_) => {
740                        if node_str == other.as_string() {
741                            return true;
742                        }
743                        continue;
744                    }
745                    _ => continue,
746                };
747            }
748            false
749        }
750        // Neither is a node-set
751        _ => match (a, b) {
752            (XPathValue::Boolean(_), _) | (_, XPathValue::Boolean(_)) => {
753                a.as_boolean() == b.as_boolean()
754            }
755            (XPathValue::Number(_), _) | (_, XPathValue::Number(_)) => {
756                let na = a.as_number();
757                let nb = b.as_number();
758                if na.is_nan() || nb.is_nan() {
759                    false
760                } else {
761                    (na - nb).abs() < f64::EPSILON || na == nb
762                }
763            }
764            _ => a.as_string() == b.as_string(),
765        },
766    }
767}
768
769/// Compare two XPath values for ordering (XPath 1.0 §3.4).
770fn compare_ordered(_ctx: &mut XPathContext, a: &XPathValue, b: &XPathValue) -> std::cmp::Ordering {
771    match (a, b) {
772        // If both are node-sets, compare pairwise
773        (XPathValue::NodeSet(ns_a), XPathValue::NodeSet(ns_b)) => {
774            for node_a in ns_a.iter() {
775                let num_a = string_to_number(&node_string_value(node_a));
776                for node_b in ns_b.iter() {
777                    let num_b = string_to_number(&node_string_value(node_b));
778                    if num_a < num_b {
779                        return std::cmp::Ordering::Less;
780                    }
781                    if num_a > num_b {
782                        return std::cmp::Ordering::Greater;
783                    }
784                }
785            }
786            std::cmp::Ordering::Equal
787        }
788        // If one is a node-set
789        (XPathValue::NodeSet(ns), other) | (other, XPathValue::NodeSet(ns)) => {
790            let other_num = other.as_number();
791            for node in ns.iter() {
792                let node_num = string_to_number(&node_string_value(node));
793                if node_num < other_num {
794                    return std::cmp::Ordering::Less;
795                }
796                if node_num > other_num {
797                    return std::cmp::Ordering::Greater;
798                }
799            }
800            std::cmp::Ordering::Equal
801        }
802        // Neither is a node-set: compare as numbers
803        _ => {
804            let na = a.as_number();
805            let nb = b.as_number();
806            if na.is_nan() || nb.is_nan() {
807                std::cmp::Ordering::Equal // NaN comparisons return false, so equal for ordering
808            } else if na < nb {
809                std::cmp::Ordering::Less
810            } else if na > nb {
811                std::cmp::Ordering::Greater
812            } else {
813                std::cmp::Ordering::Equal
814            }
815        }
816    }
817}
818
819// ═══════════════════════════════════════════════════════════════════════════════
820// Top-level evaluation API
821// ═══════════════════════════════════════════════════════════════════════════════
822
823/// Evaluate an XPath expression string against a document.
824///
825/// This is the main entry point for XPath evaluation.
826pub fn eval_xpath(ctx: &mut XPathContext, expression: &str) -> Result<XPathValue, String> {
827    let expr = crate::xml::xpath::parser::parse_xpath(expression).map_err(|e| e.message)?;
828    eval(ctx, &expr)
829}
830
831// ═══════════════════════════════════════════════════════════════════════════════
832// Tests
833// ═══════════════════════════════════════════════════════════════════════════════
834
835#[cfg(test)]
836mod tests {
837    use super::*;
838    use crate::xml::xpath::context::XPathContext;
839    use crate::xml::xpath::functions;
840
841    fn setup_context() -> XPathContext {
842        let mut ctx = XPathContext::new(std::ptr::null_mut());
843        // Register core functions
844        let funcs = functions::core_functions();
845        for (name, func) in funcs {
846            ctx.register_function(&name, func);
847        }
848        ctx
849    }
850
851    #[test]
852    fn test_eval_string_literal() {
853        let mut ctx = setup_context();
854        let result = eval_xpath(&mut ctx, "'hello'").unwrap();
855        assert_eq!(result.as_string(), "hello");
856    }
857
858    #[test]
859    fn test_eval_number_literal() {
860        let mut ctx = setup_context();
861        let result = eval_xpath(&mut ctx, "42").unwrap();
862        assert_eq!(result.as_number(), 42.0);
863    }
864
865    #[test]
866    fn test_eval_addition() {
867        let mut ctx = setup_context();
868        let result = eval_xpath(&mut ctx, "1 + 2").unwrap();
869        assert_eq!(result.as_number(), 3.0);
870    }
871
872    #[test]
873    fn test_eval_subtraction() {
874        let mut ctx = setup_context();
875        let result = eval_xpath(&mut ctx, "5 - 3").unwrap();
876        assert_eq!(result.as_number(), 2.0);
877    }
878
879    #[test]
880    fn test_eval_multiplication() {
881        let mut ctx = setup_context();
882        let result = eval_xpath(&mut ctx, "3 * 4").unwrap();
883        assert_eq!(result.as_number(), 12.0);
884    }
885
886    #[test]
887    fn test_eval_division() {
888        let mut ctx = setup_context();
889        let result = eval_xpath(&mut ctx, "10 div 3").unwrap();
890        assert!((result.as_number() - 3.3333333333333335).abs() < 1e-10);
891    }
892
893    #[test]
894    fn test_eval_modulo() {
895        let mut ctx = setup_context();
896        let result = eval_xpath(&mut ctx, "10 mod 3").unwrap();
897        assert_eq!(result.as_number(), 1.0);
898    }
899
900    #[test]
901    fn test_eval_equality() {
902        let mut ctx = setup_context();
903        assert!(eval_xpath(&mut ctx, "1 = 1").unwrap().as_boolean());
904        assert!(!eval_xpath(&mut ctx, "1 = 2").unwrap().as_boolean());
905        assert!(eval_xpath(&mut ctx, "1 != 2").unwrap().as_boolean());
906    }
907
908    #[test]
909    fn test_eval_comparison() {
910        let mut ctx = setup_context();
911        assert!(eval_xpath(&mut ctx, "1 < 2").unwrap().as_boolean());
912        assert!(eval_xpath(&mut ctx, "2 > 1").unwrap().as_boolean());
913        assert!(eval_xpath(&mut ctx, "1 <= 1").unwrap().as_boolean());
914        assert!(eval_xpath(&mut ctx, "2 >= 2").unwrap().as_boolean());
915    }
916
917    #[test]
918    fn test_eval_and_or() {
919        let mut ctx = setup_context();
920        assert!(eval_xpath(&mut ctx, "true() and true()")
921            .unwrap()
922            .as_boolean());
923        assert!(!eval_xpath(&mut ctx, "true() and false()")
924            .unwrap()
925            .as_boolean());
926        assert!(eval_xpath(&mut ctx, "true() or false()")
927            .unwrap()
928            .as_boolean());
929        assert!(!eval_xpath(&mut ctx, "false() or false()")
930            .unwrap()
931            .as_boolean());
932    }
933
934    #[test]
935    fn test_eval_not() {
936        let mut ctx = setup_context();
937        assert!(!eval_xpath(&mut ctx, "not(true())").unwrap().as_boolean());
938        assert!(eval_xpath(&mut ctx, "not(false())").unwrap().as_boolean());
939    }
940
941    #[test]
942    fn test_eval_boolean() {
943        let mut ctx = setup_context();
944        assert!(eval_xpath(&mut ctx, "boolean('hello')")
945            .unwrap()
946            .as_boolean());
947        assert!(!eval_xpath(&mut ctx, "boolean('')").unwrap().as_boolean());
948        assert!(!eval_xpath(&mut ctx, "boolean(0)").unwrap().as_boolean());
949        assert!(eval_xpath(&mut ctx, "boolean(1)").unwrap().as_boolean());
950    }
951
952    #[test]
953    fn test_eval_number() {
954        let mut ctx = setup_context();
955        assert_eq!(
956            eval_xpath(&mut ctx, "number('42')").unwrap().as_number(),
957            42.0
958        );
959    }
960
961    #[test]
962    fn test_eval_string() {
963        let mut ctx = setup_context();
964        assert_eq!(
965            eval_xpath(&mut ctx, "string(42)").unwrap().as_string(),
966            "42"
967        );
968    }
969
970    #[test]
971    fn test_eval_concat() {
972        let mut ctx = setup_context();
973        assert_eq!(
974            eval_xpath(&mut ctx, "concat('a', 'b', 'c')")
975                .unwrap()
976                .as_string(),
977            "abc"
978        );
979    }
980
981    #[test]
982    fn test_eval_starts_with() {
983        let mut ctx = setup_context();
984        assert!(eval_xpath(&mut ctx, "starts-with('hello', 'he')")
985            .unwrap()
986            .as_boolean());
987    }
988
989    #[test]
990    fn test_eval_contains() {
991        let mut ctx = setup_context();
992        assert!(eval_xpath(&mut ctx, "contains('hello', 'ell')")
993            .unwrap()
994            .as_boolean());
995    }
996
997    #[test]
998    fn test_eval_substring() {
999        let mut ctx = setup_context();
1000        assert_eq!(
1001            eval_xpath(&mut ctx, "substring('12345', 1, 3)")
1002                .unwrap()
1003                .as_string(),
1004            "123"
1005        );
1006        assert_eq!(
1007            eval_xpath(&mut ctx, "substring('12345', 2)")
1008                .unwrap()
1009                .as_string(),
1010            "2345"
1011        );
1012    }
1013
1014    #[test]
1015    fn test_eval_string_length() {
1016        let mut ctx = setup_context();
1017        assert_eq!(
1018            eval_xpath(&mut ctx, "string-length('hello')")
1019                .unwrap()
1020                .as_number(),
1021            5.0
1022        );
1023    }
1024
1025    #[test]
1026    fn test_eval_normalize_space() {
1027        let mut ctx = setup_context();
1028        assert_eq!(
1029            eval_xpath(&mut ctx, "normalize-space('  hello   world  ')")
1030                .unwrap()
1031                .as_string(),
1032            "hello world"
1033        );
1034    }
1035
1036    #[test]
1037    fn test_eval_floor_ceiling_round() {
1038        let mut ctx = setup_context();
1039        assert_eq!(eval_xpath(&mut ctx, "floor(3.7)").unwrap().as_number(), 3.0);
1040        assert_eq!(
1041            eval_xpath(&mut ctx, "ceiling(3.2)").unwrap().as_number(),
1042            4.0
1043        );
1044        assert_eq!(eval_xpath(&mut ctx, "round(3.5)").unwrap().as_number(), 4.0);
1045    }
1046
1047    #[test]
1048    fn test_eval_sum() {
1049        // sum() on an empty node-set should return 0
1050        let mut ctx = setup_context();
1051        ctx.document = std::ptr::null_mut();
1052        // Can't test sum directly without a node-set, but we can test it
1053        // with literal values when we add node-set construction
1054    }
1055
1056    #[test]
1057    fn test_eval_variable_not_found() {
1058        let mut ctx = setup_context();
1059        let result = eval_xpath(&mut ctx, "$undefined_var");
1060        assert!(result.is_err());
1061    }
1062
1063    #[test]
1064    fn test_eval_variable_found() {
1065        let mut ctx = setup_context();
1066        ctx.register_variable("x", XPathValue::Number(42.0));
1067        let result = eval_xpath(&mut ctx, "$x").unwrap();
1068        assert_eq!(result.as_number(), 42.0);
1069    }
1070
1071    #[test]
1072    fn test_eval_union() {
1073        // Union requires node-sets, which need a document.
1074        // Test basic union of numbers (should error since numbers aren't node-sets).
1075        // Actually, union on non-node-sets would panic at as_node_set().
1076        // For now, this is a placeholder for when we have document support.
1077    }
1078
1079    #[test]
1080    fn test_eval_operator_precedence() {
1081        let mut ctx = setup_context();
1082        // 1 + 2 * 3 should be 7 (multiplication before addition)
1083        let result = eval_xpath(&mut ctx, "1 + 2 * 3").unwrap();
1084        assert_eq!(result.as_number(), 7.0);
1085
1086        // (1 + 2) * 3 should be 9
1087        let result = eval_xpath(&mut ctx, "(1 + 2) * 3").unwrap();
1088        assert_eq!(result.as_number(), 9.0);
1089    }
1090
1091    #[test]
1092    fn test_eval_unary_minus() {
1093        let mut ctx = setup_context();
1094        let result = eval_xpath(&mut ctx, "-5").unwrap();
1095        assert_eq!(result.as_number(), -5.0);
1096
1097        let result = eval_xpath(&mut ctx, "--5").unwrap();
1098        assert_eq!(result.as_number(), 5.0);
1099    }
1100
1101    #[test]
1102    fn test_eval_true_false() {
1103        let mut ctx = setup_context();
1104        assert!(eval_xpath(&mut ctx, "true()").unwrap().as_boolean());
1105        assert!(!eval_xpath(&mut ctx, "false()").unwrap().as_boolean());
1106    }
1107
1108    #[test]
1109    fn test_eval_translate() {
1110        let mut ctx = setup_context();
1111        assert_eq!(
1112            eval_xpath(
1113                &mut ctx,
1114                "translate('hello', 'abcdefghijklmnopqrstuvwxyz', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ')"
1115            )
1116            .unwrap()
1117            .as_string(),
1118            "HELLO"
1119        );
1120    }
1121
1122    #[test]
1123    fn test_eval_empty_expression() {
1124        let mut ctx = setup_context();
1125        let result = eval_xpath(&mut ctx, "");
1126        assert!(result.is_err());
1127    }
1128}