Skip to main content

libxml_rs/xml/xpath/
context.rs

1//! XPath 1.0 Evaluation Context (§25).
2//!
3//! The evaluation context holds the state required to evaluate an XPath
4//! expression: the current document, context node, context position/size,
5//! variable bindings, namespace declarations, registered extension functions,
6//! and recursion-depth tracking.
7//!
8//! # UPSTREAM-PARITY
9//!
10//! Mirrors `xmlXPathContext` from libxml2 with additional Rust-side state
11//! for variable/function resolution and safe recursion guards.
12//!
13//! # Courts
14//!
15//! XPATH-CONTEXT-*
16//!
17//! # Upstream contract
18//!
19//! Mirrors `xmlXPathContext` (xpath.c / xpathInternals.h,
20//! `SRC-LIBXML2-2.15.0-XPATH-C`, parity target libxml2 2.15.3 oracle):
21//! context node/document, position/size, variable and function registries,
22//! namespace scope, the C var/function lookup hooks, and the opLimit/
23//! opCount fields (R-000128 fixed their widths in the C mirror).
24//!
25//! # Conceptual behavior
26//!
27//! Holds the state an evaluation needs: current document, context node,
28//! context position/size, variable bindings, namespace declarations,
29//! registered extension functions and recursion-depth tracking. The
30//! C function bridge (R-000162) synthesizes an `xmlXPathParserContext`
31//! around the value stack, pushes evaluated args, invokes the registered
32//! C function and converts its result back — including the namespaced
33//! function_lookup fallback the XSLT engine uses for prefix:local calls.
34//!
35//! # Ownership & safety invariants
36//!
37//! Callback user-data pointers (`var_lookup_data` / `func_lookup_data`)
38//! are stored verbatim and passed back — the caller keeps them alive
39//! (OWNERSHIP_ATLAS §6). A `VarLookupFunc` returning an
40//! `_xmlXPathObject` transfers ownership to the caller. Context state is
41//! single-threaded per evaluation; the recursion guard bounds nesting.
42//!
43//! # Historical quirks & epochs
44//!
45//! R-000162: the C XPath function registry was a stub that always errored
46//! ('C extension function cannot be called') until the 11.1-L callback
47//! audit built the parser-context bridge — registered functions now run
48//! with oracle-verified semantics. The recursion guard mirrors upstream
49//! depth handling introduced in the hardening epochs (SEC-0001 lineage).
50//!
51//! # Deliberate oddities
52//!
53//! The synthesized parser context is an internal adapter, not the full
54//! upstream xmlXPathParserContext: only the value-stack operations that
55//! C extension functions observe are modeled.
56//!
57//! # Proving courts
58//!
59//! XPATH-CONTEXT-* and CALLBACK-001 (courts/suites/data-abi/callback-
60//! family-probe.c) verify registered C function invocation byte-identical
61//! against the oracle; cargo test covers variable/function resolution.
62//!
63//! # Tempting simplifications that would break parity
64//!
65//! Do not drop the C-callback bridge back to a Rust-only registry: XSLT
66//! extension functions and C consumers register raw function pointers
67//! through xmlXPathRegisterFunc/xmlXPathRegisterFuncNS and observe them
68//! firing (R-000162).
69//! Do not remove the recursion guard — deep expressions must fail like
70//! the oracle, not overflow the stack.
71
72use crate::abi::structs::{_xmlDoc, _xmlNode, _xmlXPathContext, _xmlXPathObject};
73use crate::abi::types::xmlChar;
74use crate::xml::xpath::types::XPathValue;
75use std::collections::HashMap;
76use std::os::raw::c_void;
77
78/// First 8 bytes of every internal `XPathContext` allocation. C-interop
79/// helpers that receive a `_xmlXPathContext` whose `extra` slot may hold
80/// either the internal Rust context (engine rule) or a transform context
81/// (upstream libxslt stores the `xsltTransformContext` there — php reads
82/// `ctxt->context->extra` directly) inspect this to tell the two apart.
83///
84/// NOTE: `XPathContext` is `repr(Rust)` so field order is NOT declaration
85/// order — always test with [`has_signature`], which reads the field at its
86/// compiler-chosen offset via `offset_of!`.
87pub const XPATH_CONTEXT_SIGNATURE: u64 = 0x5850_4354_5854_5844;
88
89/// True when `ptr` points at an internal `XPathContext` allocation (the
90/// engine stores one in `_xmlXPathContext.extra`). False when it points at
91/// an upstream-layout `xsltTransformContext` in the same slot (the XSLT
92/// extension-function mirror bridge) or at anything else.
93pub const fn has_signature(ptr: *const c_void) -> bool {
94    if ptr.is_null() {
95        return false;
96    }
97    let field = unsafe {
98        (ptr as *const u8)
99            .add(core::mem::offset_of!(XPathContext, signature))
100            .cast::<u64>()
101    };
102    // The Box allocation is aligned to the struct's alignment and the field
103    // offset is a multiple of u64's alignment, so a plain read is safe.
104    let value = unsafe { *field };
105    value == XPATH_CONTEXT_SIGNATURE
106}
107
108// ═══════════════════════════════════════════════════════════════════════════════
109// Type Aliases
110// ═══════════════════════════════════════════════════════════════════════════════
111
112/// XPath extension function signature.
113///
114/// Registered extension functions receive a mutable reference to the current
115/// evaluation context and a slice of already-evaluated argument values.
116/// They return an `XPathValue` on success or an error string on failure.
117pub type XPathFunction = fn(&mut XPathContext, &[XPathValue]) -> Result<XPathValue, String>;
118
119/// A boxed, capture-capable XPath function implementation.
120///
121/// Plain function pointers coerce into this via boxing; capturing closures
122/// (e.g. EXSLT `func:function` bodies) can also be stored. Used for the
123/// extension-function registry and the EXSLT registry.
124pub type BoxedXPathFunction =
125    Box<dyn Fn(&mut XPathContext, &[XPathValue]) -> Result<XPathValue, String> + Send + Sync>;
126
127/// A namespaced function-lookup fallback: consulted by `lookup_function`
128/// after the exact-name registry misses, so engines (XSLT) can resolve
129/// `prefix:local(...)` calls against their own extension registries.
130pub type FunctionLookupFn =
131    Box<dyn Fn(&XPathContext, &str) -> Option<BoxedXPathFunction> + Send + Sync>;
132
133/// C callback for variable lookup.
134///
135/// SAFETY: This is called from C ABI boundaries (e.g. when libxml2's XPath
136/// evaluator invokes the variable lookup hook). The implementation must not
137/// panic and must handle null pointers gracefully.
138///
139/// * `data` — user-supplied data pointer (the `var_lookup_data` field).
140/// * `ns`   — namespace URI of the variable (may be null for no namespace).
141/// * `name` — local part of the variable name.
142///
143/// Returns a pointer to an `_xmlXPathObject` that the caller takes ownership
144/// of, or null if the variable is not found.
145pub type VarLookupFunc =
146    unsafe extern "C" fn(*mut c_void, *const xmlChar, *const xmlChar) -> *mut _xmlXPathObject;
147
148/// C callback for function lookup.
149///
150/// SAFETY: Called from C ABI boundaries. The implementation must not panic
151/// and must handle null pointers gracefully.
152///
153/// * `data` — user-supplied data pointer (the `func_lookup_data` field).
154/// * `ns`   — namespace URI of the function (may be null for no namespace).
155/// * `name` — local part of the function name.
156///
157/// Returns an opaque pointer to a function implementation, or null if the
158/// function is not found. The interpretation of the returned pointer is
159/// defined by the caller that registered the callback.
160pub type FuncLookupFunc =
161    unsafe extern "C" fn(*mut c_void, *const xmlChar, *const xmlChar) -> *mut c_void;
162
163// ═══════════════════════════════════════════════════════════════════════════════
164// XPathContext
165// ═══════════════════════════════════════════════════════════════════════════════
166
167/// XPath 1.0 evaluation context.
168///
169/// Carries all state required to evaluate an XPath expression:
170///
171/// * **Document and node** — the current XML document and context node.
172/// * **Context position/size** — for `position()` and `last()`.
173/// * **Variable bindings** — in-scope XPath variables.
174/// * **Namespace bindings** — prefix-to-URI mappings.
175/// * **Extension functions** — registered extension functions.
176/// * **Recursion guard** — depth counter to prevent infinite recursion.
177/// * **C callbacks** — hooks for variable and function lookup from the C ABI.
178///
179/// # Lifetime / Safety
180///
181/// The context borrows raw pointers to the document and nodes. It is the
182/// caller's responsibility to ensure those pointers remain valid for the
183/// duration of evaluation. The context does **not** own the document tree.
184pub struct XPathContext {
185    /// Signature marker: always `XPATH_CONTEXT_SIGNATURE`. Lets C-interop
186    /// helpers distinguish an internal Rust context (stored in
187    /// `_xmlXPathContext.extra`) from an upstream-style transform context in
188    /// the same slot (see `transform_context_from_parser`).
189    pub signature: u64,
190
191    /// The current XML document.
192    pub document: *mut _xmlDoc,
193
194    /// The current context node.
195    pub context_node: *mut _xmlNode,
196
197    /// Position of the context node within the context list (1-based).
198    pub context_position: i32,
199
200    /// Size of the context list.
201    pub context_size: i32,
202
203    /// Bound variables (name → value).
204    pub variables: HashMap<String, XPathValue>,
205
206    /// Namespace bindings (prefix → URI).
207    pub namespaces: HashMap<String, String>,
208
209    /// Registered extension functions (name → function).
210    pub functions: HashMap<String, BoxedXPathFunction>,
211
212    /// Namespaced function-lookup fallback (e.g. the XSLT extension
213    /// function registry), consulted after `functions`.
214    pub function_lookup: Option<FunctionLookupFn>,
215
216    /// Last error message, if any.
217    pub error: Option<String>,
218
219    /// Current proximity position (for `last()` / `position()`).
220    pub proximity_position: i32,
221
222    /// The context list for `position()` / `last()`.
223    pub context_list: Vec<*mut _xmlNode>,
224
225    /// Recursion depth counter (to prevent infinite recursion).
226    pub recursion_depth: u32,
227
228    /// C callback for variable lookup.
229    pub var_lookup_func: Option<VarLookupFunc>,
230
231    /// Opaque data pointer passed to `var_lookup_func`.
232    pub var_lookup_data: *mut c_void,
233
234    /// C callback for function lookup.
235    pub func_lookup_func: Option<FuncLookupFunc>,
236
237    /// Opaque data pointer passed to `func_lookup_func`.
238    pub func_lookup_data: *mut c_void,
239
240    /// The C-visible `_xmlXPathContext` this internal context belongs to
241    /// (set by `xmlXPathNewContext`); needed to invoke C-registered
242    /// extension functions through the `xmlXPathParserContext` protocol.
243    pub c_context: *mut _xmlXPathContext,
244}
245
246impl std::fmt::Debug for XPathContext {
247    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
248        // The extension-function map holds boxed callables and cannot be
249        // formatted; report the registered names instead.
250        let names: Vec<&String> = self.functions.keys().collect();
251        f.debug_struct("XPathContext")
252            .field("document", &self.document)
253            .field("context_node", &self.context_node)
254            .field("context_position", &self.context_position)
255            .field("context_size", &self.context_size)
256            .field("variables", &self.variables)
257            .field("namespaces", &self.namespaces)
258            .field("functions", &names)
259            .field("error", &self.error)
260            .field("recursion_depth", &self.recursion_depth)
261            .finish()
262    }
263}
264
265impl Clone for XPathContext {
266    fn clone(&self) -> Self {
267        // Extension functions are not cloned (boxed callables cannot be
268        // duplicated); the clone carries the same state otherwise.
269        let mut cloned = XPathContext::new(self.document);
270        cloned.context_node = self.context_node;
271        cloned.context_position = self.context_position;
272        cloned.context_size = self.context_size;
273        cloned.variables = self.variables.clone();
274        cloned.namespaces = self.namespaces.clone();
275        cloned.error = self.error.clone();
276        cloned.proximity_position = self.proximity_position;
277        cloned.context_list = self.context_list.clone();
278        cloned.recursion_depth = self.recursion_depth;
279        cloned.var_lookup_func = self.var_lookup_func;
280        cloned.var_lookup_data = self.var_lookup_data;
281        cloned.func_lookup_func = self.func_lookup_func;
282        cloned.func_lookup_data = self.func_lookup_data;
283        cloned.c_context = self.c_context;
284        cloned
285    }
286}
287
288impl XPathContext {
289    /// Create a new XPath evaluation context for the given document.
290    ///
291    /// The context is initialised with:
292    /// * The document pointer set to `doc`.
293    /// * No context node (`null`).
294    /// * Context position = 1, context size = 1 (defaults per XPath 1.0).
295    /// * Empty variable, namespace, and function tables.
296    /// * No error.
297    /// * Proximity position = 1.
298    /// * Empty context list.
299    /// * Recursion depth = 0.
300    /// * No C callbacks registered.
301    /// * Callback data pointers set to null.
302    pub fn new(doc: *mut _xmlDoc) -> Self {
303        Self {
304            signature: XPATH_CONTEXT_SIGNATURE,
305            document: doc,
306            context_node: std::ptr::null_mut(),
307            context_position: 1,
308            context_size: 1,
309            variables: HashMap::new(),
310            namespaces: HashMap::new(),
311            functions: HashMap::new(),
312            function_lookup: None,
313            error: None,
314            proximity_position: 1,
315            context_list: Vec::new(),
316            recursion_depth: 0,
317            var_lookup_func: None,
318            var_lookup_data: std::ptr::null_mut(),
319            func_lookup_func: None,
320            func_lookup_data: std::ptr::null_mut(),
321            c_context: std::ptr::null_mut(),
322        }
323    }
324
325    /// Set the context node and update context position / size.
326    ///
327    /// If `node` is non-null, the context list is set to a single-element
328    /// list containing only that node, and both `context_position` and
329    /// `context_size` are set to 1.
330    ///
331    /// If `node` is null, the context list is cleared and both
332    /// `context_position` and `context_size` are set to 1.
333    pub fn set_context_node(&mut self, node: *mut _xmlNode) {
334        self.context_node = node;
335        if node.is_null() {
336            self.context_list.clear();
337            self.context_position = 1;
338            self.context_size = 1;
339            self.proximity_position = 1;
340        } else {
341            self.context_list = vec![node];
342            self.context_position = 1;
343            self.context_size = 1;
344            self.proximity_position = 1;
345        }
346    }
347
348    /// Set the context list for `position()` / `last()`.
349    ///
350    /// Updates `context_list`, `context_size`, and resets
351    /// `context_position` and `proximity_position` to 1.
352    ///
353    /// The context node is not changed by this call; use
354    /// [`set_context_node`](Self::set_context_node) to update it.
355    pub fn set_context_list(&mut self, nodes: Vec<*mut _xmlNode>) {
356        self.context_size = nodes.len() as i32;
357        self.context_list = nodes;
358        self.context_position = 1;
359        self.proximity_position = 1;
360    }
361
362    /// Look up a variable by name.
363    ///
364    /// Checks the local `variables` map first. If the variable is not found
365    /// there, and a `var_lookup_func` callback is registered, the callback
366    /// is invoked with the variable name and its namespace (currently passed
367    /// as null since our Rust-side variables have no namespace component).
368    ///
369    /// Returns `None` if the variable is not bound.
370    ///
371    /// # Note
372    ///
373    /// When the C callback path is used, the returned `_xmlXPathObject` is
374    /// converted into an `XPathValue`. Currently this path is a placeholder;
375    /// a full implementation would call into `xmlXPathObject` conversion
376    /// routines.
377    pub fn resolve_variable(&self, name: &str) -> Option<XPathValue> {
378        // Check local Rust-side variables first.
379        if let Some(value) = self.variables.get(name) {
380            return Some(value.clone());
381        }
382
383        // Fall back to the C callback if registered.
384        if let Some(lookup) = self.var_lookup_func {
385            // Convert the name to a C string (xmlChar*).
386            let c_name: Vec<xmlChar> = name.bytes().collect();
387            // SAFETY: We call the C callback with the user-provided data pointer.
388            // The callback must not panic and must handle null inputs gracefully.
389            let result = unsafe { lookup(self.var_lookup_data, std::ptr::null(), c_name.as_ptr()) };
390            if !result.is_null() {
391                // TODO: Convert _xmlXPathObject to XPathValue.
392                // For now, free the object and return a placeholder.
393                // In a full implementation this would inspect result.type_
394                // and extract the appropriate value.
395                {
396                    // We cannot easily convert without more ABI support.
397                    // Return None for now — the C callback path is for
398                    // interop scenarios where the caller handles conversion.
399                    let _ = result; // would free with xmlXPathFreeObject
400                }
401            }
402        }
403
404        None
405    }
406
407    /// Look up a namespace URI by prefix.
408    ///
409    /// Checks the local `namespaces` map first. If the prefix is not found
410    /// there, it falls back to scanning the namespace definitions on the
411    /// context node (`nsDef` chain) and its ancestors.
412    ///
413    /// Returns `None` if the prefix is not bound.
414    pub fn resolve_namespace(&self, prefix: &str) -> Option<String> {
415        // Check local bindings first.
416        if let Some(uri) = self.namespaces.get(prefix) {
417            return Some(uri.clone());
418        }
419
420        // Fall back to scanning the node's namespace definitions.
421        // Walk up the ancestor chain looking for nsDef declarations.
422        let mut current = self.context_node;
423        while !current.is_null() {
424            // SAFETY: We dereference raw pointers up the parent chain.
425            // The caller guarantees these pointers remain valid.
426            unsafe {
427                let mut ns = (*current).nsDef;
428                while !ns.is_null() {
429                    let ns_prefix = (*ns).prefix;
430                    let ns_href = (*ns).href;
431
432                    // Compare prefix.
433                    let prefix_matches = if ns_prefix.is_null() {
434                        // Default namespace (no prefix) — only matches
435                        // if the caller is asking for the default namespace.
436                        prefix.is_empty()
437                    } else {
438                        // Read the prefix as a C string and compare.
439                        let mut len = 0;
440                        while *ns_prefix.add(len) != 0 {
441                            len += 1;
442                        }
443                        let slice = std::slice::from_raw_parts(ns_prefix, len);
444                        slice == prefix.as_bytes()
445                    };
446
447                    if prefix_matches {
448                        // Read the href as a Rust String.
449                        let mut len = 0;
450                        while *ns_href.add(len) != 0 {
451                            len += 1;
452                        }
453                        let slice = std::slice::from_raw_parts(ns_href, len);
454                        return Some(String::from_utf8_lossy(slice).into_owned());
455                    }
456
457                    ns = (*ns).next;
458                }
459            }
460
461            // Move to parent.
462            // SAFETY: The node tree is valid for the lifetime of the context.
463            unsafe {
464                current = (*current).parent;
465            }
466        }
467
468        None
469    }
470
471    /// Look up a registered extension function by name.
472    ///
473    /// Checks the local `functions` map first. If not found, and a
474    /// `func_lookup_func` callback is registered, the callback is invoked.
475    ///
476    /// Returns `None` if no such function is registered.
477    pub fn lookup_function(&mut self, name: &str) -> Option<&BoxedXPathFunction> {
478        // Check local Rust-side functions first.
479        if self.functions.contains_key(name) {
480            return self.functions.get(name);
481        }
482
483        // Namespaced fallback (XSLT extension functions, EXSLT, ...): the
484        // resolved closure is memoised into the registry.
485        if let Some(lookup) = &self.function_lookup {
486            if let Some(func) = lookup(self, name) {
487                self.functions.insert(name.to_string(), func);
488                return self.functions.get(name);
489            }
490        }
491
492        // C-registered extension functions (xmlXPathRegisterFuncLookup) are
493        // resolved and invoked through the parser-context protocol in
494        // eval_function_call — they are NOT resolved here (a bare callback
495        // call with a non-NUL-terminated name crashes C consumers like
496        // nokogiri's handler lookup).
497        None
498    }
499
500    /// Register an extension function.
501    ///
502    /// The function is stored in the local `functions` map under `name`.
503    /// It will be found by [`lookup_function`](Self::lookup_function) before
504    /// any C callback is consulted. Accepts both fn pointers and capturing
505    /// closures.
506    pub fn register_function<F>(&mut self, name: &str, func: F)
507    where
508        F: Fn(&mut XPathContext, &[XPathValue]) -> Result<XPathValue, String>
509            + Send
510            + Sync
511            + 'static,
512    {
513        self.functions.insert(name.to_string(), Box::new(func));
514    }
515
516    /// Register a variable binding.
517    ///
518    /// The variable is stored in the local `variables` map under `name`.
519    /// It will be found by [`resolve_variable`](Self::resolve_variable) before
520    /// any C callback is consulted.
521    pub fn register_variable(&mut self, name: &str, value: XPathValue) {
522        self.variables.insert(name.to_string(), value);
523    }
524
525    /// Remove a variable binding from the context's variable hash.
526    ///
527    /// Used to unwind local XSLT variable scopes when a variable is popped
528    /// from the transform variable stack.
529    pub fn unregister_variable(&mut self, name: &str) {
530        self.variables.remove(name);
531    }
532
533    /// Register a namespace binding.
534    ///
535    /// Maps `prefix` to `uri` in the local `namespaces` map.
536    /// An empty prefix registers the default namespace.
537    pub fn register_namespace(&mut self, prefix: &str, uri: &str) {
538        self.namespaces.insert(prefix.to_string(), uri.to_string());
539    }
540
541    /// Record an error message.
542    ///
543    /// Overwrites any previously recorded error. Use `clear_error` to reset.
544    pub fn set_error(&mut self, msg: &str) {
545        self.error = Some(msg.to_string());
546    }
547
548    /// Clear any recorded error.
549    pub fn clear_error(&mut self) {
550        self.error = None;
551    }
552
553    /// Push onto the recursion stack.
554    ///
555    /// Increments `recursion_depth`. If the depth exceeds a reasonable limit
556    /// (currently 1000), returns `Err` with an overflow message.
557    ///
558    /// Callers should invoke this before recursing into expression evaluation
559    /// and call [`pop_recursion`](Self::pop_recursion) after returning.
560    pub fn push_recursion(&mut self) -> Result<(), String> {
561        const MAX_RECURSION_DEPTH: u32 = 1000;
562        if self.recursion_depth >= MAX_RECURSION_DEPTH {
563            return Err(
564                "XPath evaluation recursion depth exceeded (infinite recursion?)".to_string(),
565            );
566        }
567        self.recursion_depth += 1;
568        Ok(())
569    }
570
571    /// Pop from the recursion stack.
572    ///
573    /// Decrements `recursion_depth`. Must be called after a corresponding
574    /// [`push_recursion`](Self::push_recursion).
575    ///
576    /// # Panics
577    ///
578    /// Panics if `recursion_depth` is already 0 (indicating unbalanced
579    /// push/pop calls).
580    pub fn pop_recursion(&mut self) {
581        assert!(
582            self.recursion_depth > 0,
583            "unbalanced pop_recursion: recursion_depth is already 0"
584        );
585        self.recursion_depth -= 1;
586    }
587
588    /// Returns `true` if a context node is set (non-null).
589    pub const fn has_context_node(&self) -> bool {
590        !self.context_node.is_null()
591    }
592
593    /// Reset the context to its initial state, keeping the document pointer.
594    ///
595    /// Clears the context node, context list, error, and recursion depth.
596    /// Variable, namespace, and function bindings are preserved.
597    pub fn reset(&mut self) {
598        self.context_node = std::ptr::null_mut();
599        self.context_position = 1;
600        self.context_size = 1;
601        self.error = None;
602        self.proximity_position = 1;
603        self.context_list.clear();
604        self.recursion_depth = 0;
605    }
606
607    /// Returns the current proximity position (1-based).
608    ///
609    /// Equivalent to the XPath `position()` function.
610    pub const fn position(&self) -> i32 {
611        self.proximity_position
612    }
613
614    /// Returns the context size.
615    ///
616    /// Equivalent to the XPath `last()` function.
617    pub const fn last(&self) -> i32 {
618        self.context_size
619    }
620
621    /// Advance the proximity position by one.
622    ///
623    /// Called when iterating over the context list during predicate
624    /// evaluation.
625    pub const fn advance_position(&mut self) {
626        self.proximity_position += 1;
627        self.context_position = self.proximity_position;
628    }
629
630    /// Rewind the proximity position to 1.
631    pub const fn reset_position(&mut self) {
632        self.proximity_position = 1;
633        self.context_position = 1;
634    }
635}
636
637impl Default for XPathContext {
638    /// Create a default context with a null document pointer.
639    ///
640    /// This is useful when you need a context for testing or when the
641    /// document will be set later via [`set_context_node`](Self::set_context_node).
642    fn default() -> Self {
643        Self::new(std::ptr::null_mut())
644    }
645}
646
647// ═══════════════════════════════════════════════════════════════════════════════
648// Tests
649// ═══════════════════════════════════════════════════════════════════════════════
650
651#[cfg(test)]
652mod tests {
653    use super::*;
654
655    use crate::xml::xpath::types::NodeSet;
656
657    // ── Helpers ──────────────────────────────────────────────────────────
658
659    /// Create a minimal _xmlDoc for testing.
660    ///
661    /// SAFETY: The caller is responsible for freeing the allocated doc.
662    unsafe fn create_test_doc() -> *mut _xmlDoc {
663        // Allocate zeroed memory for a minimal document.
664        let layout = std::alloc::Layout::new::<_xmlDoc>();
665        let ptr = std::alloc::alloc_zeroed(layout) as *mut _xmlDoc;
666        assert!(!ptr.is_null(), "failed to allocate test document");
667        ptr
668    }
669
670    /// Create a minimal _xmlNode for testing.
671    ///
672    /// SAFETY: The caller is responsible for freeing the allocated node.
673    unsafe fn create_test_node() -> *mut _xmlNode {
674        let layout = std::alloc::Layout::new::<_xmlNode>();
675        let ptr = std::alloc::alloc_zeroed(layout) as *mut _xmlNode;
676        assert!(!ptr.is_null(), "failed to allocate test node");
677        ptr
678    }
679
680    /// SAFETY: Frees a test document allocated with `create_test_doc`.
681    unsafe fn free_test_doc(doc: *mut _xmlDoc) {
682        if !doc.is_null() {
683            let layout = std::alloc::Layout::new::<_xmlDoc>();
684            std::alloc::dealloc(doc as *mut u8, layout);
685        }
686    }
687
688    /// SAFETY: Frees a test node allocated with `create_test_node`.
689    unsafe fn free_test_node(node: *mut _xmlNode) {
690        if !node.is_null() {
691            let layout = std::alloc::Layout::new::<_xmlNode>();
692            std::alloc::dealloc(node as *mut u8, layout);
693        }
694    }
695
696    // ── Construction ─────────────────────────────────────────────────────
697
698    #[test]
699    fn test_new_context() {
700        let ctx = XPathContext::new(std::ptr::null_mut());
701        assert!(ctx.document.is_null());
702        assert!(ctx.context_node.is_null());
703        assert_eq!(ctx.context_position, 1);
704        assert_eq!(ctx.context_size, 1);
705        assert!(ctx.variables.is_empty());
706        assert!(ctx.namespaces.is_empty());
707        assert!(ctx.functions.is_empty());
708        assert!(ctx.error.is_none());
709        assert_eq!(ctx.proximity_position, 1);
710        assert!(ctx.context_list.is_empty());
711        assert_eq!(ctx.recursion_depth, 0);
712        assert!(ctx.var_lookup_func.is_none());
713        assert!(ctx.var_lookup_data.is_null());
714        assert!(ctx.func_lookup_func.is_none());
715        assert!(ctx.func_lookup_data.is_null());
716    }
717
718    #[test]
719    fn test_default_context() {
720        let ctx = XPathContext::default();
721        assert!(ctx.document.is_null());
722        assert_eq!(ctx.context_position, 1);
723    }
724
725    /// Create a context with a document and check it is recorded.
726    ///
727    /// # Safety
728    ///
729    /// - `doc` is a valid, aligned `_xmlDoc` allocated by `create_test_doc`
730    ///   and freed with `free_test_doc` exactly once; it is only stored as
731    ///   a pointer, never dereferenced, during the test.
732    #[test]
733    fn test_new_with_doc() {
734        unsafe {
735            let doc = create_test_doc();
736            let ctx = XPathContext::new(doc);
737            assert_eq!(ctx.document, doc);
738            free_test_doc(doc);
739        }
740    }
741
742    // ── set_context_node ─────────────────────────────────────────────────
743
744    /// Set a non-NULL context node and verify position state.
745    ///
746    /// # Safety
747    ///
748    /// - `node` is a valid, aligned `_xmlNode` allocated by
749    ///   `create_test_node` and freed with `free_test_node` exactly once;
750    ///   the context only stores and compares the pointer.
751    #[test]
752    fn test_set_context_node_non_null() {
753        unsafe {
754            let node = create_test_node();
755            let mut ctx = XPathContext::new(std::ptr::null_mut());
756            ctx.set_context_node(node);
757
758            assert_eq!(ctx.context_node, node);
759            assert_eq!(ctx.context_position, 1);
760            assert_eq!(ctx.context_size, 1);
761            assert_eq!(ctx.proximity_position, 1);
762            assert_eq!(ctx.context_list.len(), 1);
763            assert_eq!(ctx.context_list[0], node);
764
765            free_test_node(node);
766        }
767    }
768
769    #[test]
770    fn test_set_context_node_null() {
771        let mut ctx = XPathContext::new(std::ptr::null_mut());
772        // Set a non-null node first.
773        // SAFETY: We use a dangling pointer as a sentinel — it won't be dereferenced.
774        let sentinel = std::ptr::dangling_mut::<_xmlNode>();
775        ctx.context_list = vec![sentinel];
776        ctx.context_position = 5;
777        ctx.context_size = 5;
778        ctx.proximity_position = 5;
779
780        // Now set to null — should reset everything.
781        ctx.set_context_node(std::ptr::null_mut());
782        assert!(ctx.context_node.is_null());
783        assert!(ctx.context_list.is_empty());
784        assert_eq!(ctx.context_position, 1);
785        assert_eq!(ctx.context_size, 1);
786        assert_eq!(ctx.proximity_position, 1);
787    }
788
789    // ── set_context_list ─────────────────────────────────────────────────
790
791    /// Set a context list and verify size and position initialization.
792    ///
793    /// # Safety
794    ///
795    /// - `node1`/`node2` are valid, aligned `_xmlNode`s allocated by
796    ///   `create_test_node` and freed with `free_test_node`; the context
797    ///   stores the pointers in a `Vec` without dereferencing them.
798    #[test]
799    fn test_set_context_list() {
800        unsafe {
801            let node1 = create_test_node();
802            let node2 = create_test_node();
803            let nodes = vec![node1, node2];
804
805            let mut ctx = XPathContext::new(std::ptr::null_mut());
806            ctx.set_context_list(nodes.clone());
807
808            assert_eq!(ctx.context_list.len(), 2);
809            assert_eq!(ctx.context_size, 2);
810            assert_eq!(ctx.context_position, 1);
811            assert_eq!(ctx.proximity_position, 1);
812
813            free_test_node(node1);
814            free_test_node(node2);
815        }
816    }
817
818    #[test]
819    fn test_set_context_list_empty() {
820        let mut ctx = XPathContext::new(std::ptr::null_mut());
821        ctx.set_context_list(vec![]);
822
823        assert!(ctx.context_list.is_empty());
824        assert_eq!(ctx.context_size, 0);
825        assert_eq!(ctx.context_position, 1);
826    }
827
828    // ── Variables ────────────────────────────────────────────────────────
829
830    #[test]
831    fn test_register_and_resolve_variable() {
832        let mut ctx = XPathContext::new(std::ptr::null_mut());
833        ctx.register_variable("foo", XPathValue::String("bar".to_string()));
834
835        let result = ctx.resolve_variable("foo");
836        assert!(result.is_some());
837        assert_eq!(result.unwrap().as_string(), "bar");
838    }
839
840    #[test]
841    fn test_resolve_unknown_variable() {
842        let ctx = XPathContext::new(std::ptr::null_mut());
843        assert!(ctx.resolve_variable("nonexistent").is_none());
844    }
845    #[allow(clippy::approx_constant)]
846    #[test]
847    fn test_register_variable_number() {
848        let mut ctx = XPathContext::new(std::ptr::null_mut());
849        ctx.register_variable("pi", XPathValue::Number(3.14159));
850
851        let result = ctx.resolve_variable("pi");
852        assert!(result.is_some());
853        let val = result.unwrap();
854        assert!((val.as_number() - 3.14159).abs() < 1e-10);
855    }
856
857    #[test]
858    fn test_register_variable_boolean() {
859        let mut ctx = XPathContext::new(std::ptr::null_mut());
860        ctx.register_variable("flag", XPathValue::Boolean(true));
861
862        let result = ctx.resolve_variable("flag");
863        assert!(result.is_some());
864        assert!(result.unwrap().as_boolean());
865    }
866
867    #[test]
868    fn test_register_variable_nodeset() {
869        let mut ctx = XPathContext::new(std::ptr::null_mut());
870        let ns = NodeSet::new();
871        ctx.register_variable("nodes", XPathValue::NodeSet(ns));
872
873        let result = ctx.resolve_variable("nodes");
874        assert!(result.is_some());
875        assert!(matches!(result.unwrap(), XPathValue::NodeSet(_)));
876    }
877
878    #[test]
879    fn test_variable_overwrite() {
880        let mut ctx = XPathContext::new(std::ptr::null_mut());
881        ctx.register_variable("x", XPathValue::Number(1.0));
882        ctx.register_variable("x", XPathValue::Number(2.0));
883
884        let result = ctx.resolve_variable("x");
885        assert!(result.is_some());
886        assert!((result.unwrap().as_number() - 2.0).abs() < 1e-10);
887    }
888
889    // ── Namespaces ───────────────────────────────────────────────────────
890
891    #[test]
892    fn test_register_and_resolve_namespace() {
893        let mut ctx = XPathContext::new(std::ptr::null_mut());
894        ctx.register_namespace("xslt", "http://www.w3.org/1999/XSL/Transform");
895
896        let result = ctx.resolve_namespace("xslt");
897        assert!(result.is_some());
898        assert_eq!(result.unwrap(), "http://www.w3.org/1999/XSL/Transform");
899    }
900
901    #[test]
902    fn test_resolve_unknown_namespace() {
903        let ctx = XPathContext::new(std::ptr::null_mut());
904        // With no context node and no bindings, this should return None.
905        assert!(ctx.resolve_namespace("unknown").is_none());
906    }
907
908    #[test]
909    fn test_register_default_namespace() {
910        let mut ctx = XPathContext::new(std::ptr::null_mut());
911        ctx.register_namespace("", "http://example.com/default");
912
913        let result = ctx.resolve_namespace("");
914        assert!(result.is_some());
915        assert_eq!(result.unwrap(), "http://example.com/default");
916    }
917
918    #[test]
919    fn test_namespace_overwrite() {
920        let mut ctx = XPathContext::new(std::ptr::null_mut());
921        ctx.register_namespace("a", "http://example.com/1");
922        ctx.register_namespace("a", "http://example.com/2");
923
924        let result = ctx.resolve_namespace("a");
925        assert_eq!(result.unwrap(), "http://example.com/2");
926    }
927
928    // ── Functions ────────────────────────────────────────────────────────
929
930    #[test]
931    fn test_register_and_lookup_function() {
932        fn test_func(_ctx: &mut XPathContext, _args: &[XPathValue]) -> Result<XPathValue, String> {
933            Ok(XPathValue::String("test".to_string()))
934        }
935
936        let mut ctx = XPathContext::new(std::ptr::null_mut());
937        ctx.register_function("test:func", test_func);
938
939        let result = ctx.lookup_function("test:func");
940        assert!(result.is_some());
941    }
942
943    #[test]
944    fn test_lookup_unknown_function() {
945        let mut ctx = XPathContext::new(std::ptr::null_mut());
946        assert!(ctx.lookup_function("nonexistent").is_none());
947    }
948
949    #[test]
950    fn test_function_overwrite() {
951        fn func_a(_ctx: &mut XPathContext, _args: &[XPathValue]) -> Result<XPathValue, String> {
952            Ok(XPathValue::String("a".to_string()))
953        }
954        fn func_b(_ctx: &mut XPathContext, _args: &[XPathValue]) -> Result<XPathValue, String> {
955            Ok(XPathValue::String("b".to_string()))
956        }
957
958        let mut ctx = XPathContext::new(std::ptr::null_mut());
959        ctx.register_function("f", func_a);
960        ctx.register_function("f", func_b);
961
962        let result = ctx.lookup_function("f");
963        assert!(result.is_some());
964
965        // The overwritten function should be func_b.
966        if let Some(f) = result {
967            let mut tmp_ctx = XPathContext::new(std::ptr::null_mut());
968            let value = f(&mut tmp_ctx, &[]).unwrap();
969            assert_eq!(value.as_string(), "b");
970        }
971    }
972
973    // ── Error handling ───────────────────────────────────────────────────
974
975    #[test]
976    fn test_set_and_get_error() {
977        let mut ctx = XPathContext::new(std::ptr::null_mut());
978        assert!(ctx.error.is_none());
979
980        ctx.set_error("something went wrong");
981        assert_eq!(ctx.error.as_deref(), Some("something went wrong"));
982    }
983
984    #[test]
985    fn test_clear_error() {
986        let mut ctx = XPathContext::new(std::ptr::null_mut());
987        ctx.set_error("an error");
988        assert!(ctx.error.is_some());
989
990        ctx.clear_error();
991        assert!(ctx.error.is_none());
992    }
993
994    #[test]
995    fn test_error_overwrite() {
996        let mut ctx = XPathContext::new(std::ptr::null_mut());
997        ctx.set_error("first error");
998        ctx.set_error("second error");
999        assert_eq!(ctx.error.as_deref(), Some("second error"));
1000    }
1001
1002    // ── Recursion depth ──────────────────────────────────────────────────
1003
1004    #[test]
1005    fn test_push_pop_recursion() {
1006        let mut ctx = XPathContext::new(std::ptr::null_mut());
1007        assert_eq!(ctx.recursion_depth, 0);
1008
1009        assert!(ctx.push_recursion().is_ok());
1010        assert_eq!(ctx.recursion_depth, 1);
1011
1012        ctx.pop_recursion();
1013        assert_eq!(ctx.recursion_depth, 0);
1014    }
1015
1016    #[test]
1017    fn test_recursion_depth_limit() {
1018        let mut ctx = XPathContext::new(std::ptr::null_mut());
1019
1020        // Push to the limit (1000).
1021        for _ in 0..1000 {
1022            assert!(ctx.push_recursion().is_ok());
1023        }
1024        assert_eq!(ctx.recursion_depth, 1000);
1025
1026        // The next push should fail.
1027        let result = ctx.push_recursion();
1028        assert!(result.is_err());
1029        assert!(result.unwrap_err().contains("recursion depth exceeded"));
1030
1031        // Pop back down.
1032        for _ in 0..1000 {
1033            ctx.pop_recursion();
1034        }
1035        assert_eq!(ctx.recursion_depth, 0);
1036    }
1037
1038    #[test]
1039    #[should_panic(expected = "unbalanced pop_recursion")]
1040    fn test_pop_recursion_underflow() {
1041        let mut ctx = XPathContext::new(std::ptr::null_mut());
1042        ctx.pop_recursion(); // depth is 0 — should panic
1043    }
1044
1045    #[test]
1046    fn test_recursion_nesting() {
1047        let mut ctx = XPathContext::new(std::ptr::null_mut());
1048
1049        // Simulate nested evaluation.
1050        assert!(ctx.push_recursion().is_ok());
1051        assert!(ctx.push_recursion().is_ok());
1052        assert!(ctx.push_recursion().is_ok());
1053        assert_eq!(ctx.recursion_depth, 3);
1054
1055        ctx.pop_recursion();
1056        assert_eq!(ctx.recursion_depth, 2);
1057
1058        ctx.pop_recursion();
1059        assert_eq!(ctx.recursion_depth, 1);
1060
1061        ctx.pop_recursion();
1062        assert_eq!(ctx.recursion_depth, 0);
1063    }
1064
1065    // ── Position / Size ──────────────────────────────────────────────────
1066
1067    #[test]
1068    fn test_position_and_last() {
1069        let ctx = XPathContext::new(std::ptr::null_mut());
1070        assert_eq!(ctx.position(), 1);
1071        assert_eq!(ctx.last(), 1);
1072    }
1073
1074    #[test]
1075    fn test_advance_position() {
1076        let mut ctx = XPathContext::new(std::ptr::null_mut());
1077        ctx.advance_position();
1078        assert_eq!(ctx.position(), 2);
1079        assert_eq!(ctx.proximity_position, 2);
1080        assert_eq!(ctx.context_position, 2);
1081    }
1082
1083    #[test]
1084    fn test_reset_position() {
1085        let mut ctx = XPathContext::new(std::ptr::null_mut());
1086        ctx.advance_position();
1087        ctx.advance_position();
1088        ctx.advance_position();
1089        assert_eq!(ctx.position(), 4);
1090
1091        ctx.reset_position();
1092        assert_eq!(ctx.position(), 1);
1093        assert_eq!(ctx.context_position, 1);
1094    }
1095
1096    /// Advance the position across a three-node context list.
1097    ///
1098    /// # Safety
1099    ///
1100    /// - The three nodes are valid, aligned `_xmlNode`s allocated by
1101    ///   `create_test_node` and freed with `free_test_node`; the context
1102    ///   only stores and counts the pointers.
1103    #[test]
1104    fn test_position_with_context_list() {
1105        unsafe {
1106            let node1 = create_test_node();
1107            let node2 = create_test_node();
1108            let node3 = create_test_node();
1109            let nodes = vec![node1, node2, node3];
1110
1111            let mut ctx = XPathContext::new(std::ptr::null_mut());
1112            ctx.set_context_list(nodes);
1113
1114            assert_eq!(ctx.last(), 3);
1115            assert_eq!(ctx.position(), 1);
1116
1117            ctx.advance_position();
1118            assert_eq!(ctx.position(), 2);
1119
1120            ctx.advance_position();
1121            assert_eq!(ctx.position(), 3);
1122
1123            free_test_node(node1);
1124            free_test_node(node2);
1125            free_test_node(node3);
1126        }
1127    }
1128
1129    // ── has_context_node ─────────────────────────────────────────────────
1130
1131    /// Check `has_context_node` before and after setting a node.
1132    ///
1133    /// # Safety
1134    ///
1135    /// - `node` is a valid, aligned `_xmlNode` allocated by
1136    ///   `create_test_node` and freed with `free_test_node` exactly once;
1137    ///   `has_context_node` only checks the stored pointer for NULL.
1138    #[test]
1139    fn test_has_context_node() {
1140        let mut ctx = XPathContext::new(std::ptr::null_mut());
1141        assert!(!ctx.has_context_node());
1142
1143        unsafe {
1144            let node = create_test_node();
1145            ctx.set_context_node(node);
1146            assert!(ctx.has_context_node());
1147            free_test_node(node);
1148        }
1149    }
1150
1151    // ── reset ────────────────────────────────────────────────────────────
1152
1153    #[test]
1154    fn test_reset() {
1155        let mut ctx = XPathContext::new(std::ptr::null_mut());
1156
1157        // Set up some state.
1158        ctx.set_error("test error");
1159        ctx.proximity_position = 5;
1160        ctx.context_position = 5;
1161        ctx.context_size = 10;
1162        ctx.recursion_depth = 3;
1163        {
1164            let sentinel = std::ptr::dangling_mut::<_xmlNode>();
1165            ctx.context_list = vec![sentinel];
1166        }
1167
1168        // Register some bindings — these should survive reset.
1169        ctx.register_variable("x", XPathValue::Number(42.0));
1170        ctx.register_namespace("p", "http://example.com/ns");
1171        fn dummy(_: &mut XPathContext, _: &[XPathValue]) -> Result<XPathValue, String> {
1172            Ok(XPathValue::Boolean(true))
1173        }
1174        ctx.register_function("f", dummy);
1175
1176        ctx.reset();
1177
1178        // Context node and position should be reset.
1179        assert!(ctx.context_node.is_null());
1180        assert_eq!(ctx.context_position, 1);
1181        assert_eq!(ctx.context_size, 1);
1182        assert_eq!(ctx.proximity_position, 1);
1183        assert!(ctx.error.is_none());
1184        assert!(ctx.context_list.is_empty());
1185        assert_eq!(ctx.recursion_depth, 0);
1186
1187        // Bindings should be preserved.
1188        assert!(ctx.resolve_variable("x").is_some());
1189        assert!(ctx.resolve_namespace("p").is_some());
1190        assert!(ctx.lookup_function("f").is_some());
1191    }
1192
1193    // ── C callback fields ────────────────────────────────────────────────
1194
1195    #[test]
1196    fn test_callback_fields_default_to_none() {
1197        let ctx = XPathContext::new(std::ptr::null_mut());
1198        assert!(ctx.var_lookup_func.is_none());
1199        assert!(ctx.var_lookup_data.is_null());
1200        assert!(ctx.func_lookup_func.is_none());
1201        assert!(ctx.func_lookup_data.is_null());
1202    }
1203
1204    #[test]
1205    fn test_set_callback_fields() {
1206        let mut ctx = XPathContext::new(std::ptr::null_mut());
1207
1208        /// A no-op variable-lookup callback returning NULL.
1209        ///
1210        /// # Safety
1211        ///
1212        /// - The callback is never invoked by this test; if installed it
1213        ///   must be a valid function pointer, and `data`/`ns`/`name`
1214        ///   would need to be valid pointers if it were called.
1215        unsafe extern "C" fn dummy_var_lookup(
1216            _data: *mut c_void,
1217            _ns: *const xmlChar,
1218            _name: *const xmlChar,
1219        ) -> *mut _xmlXPathObject {
1220            std::ptr::null_mut()
1221        }
1222
1223        /// A no-op function-lookup callback returning NULL.
1224        ///
1225        /// # Safety
1226        ///
1227        /// - The callback is never invoked by this test; if installed it
1228        ///   must be a valid function pointer, and `data`/`ns`/`name`
1229        ///   would need to be valid pointers if it were called.
1230        unsafe extern "C" fn dummy_func_lookup(
1231            _data: *mut c_void,
1232            _ns: *const xmlChar,
1233            _name: *const xmlChar,
1234        ) -> *mut c_void {
1235            std::ptr::null_mut()
1236        }
1237
1238        let data_ptr = &mut 42u32 as *mut u32 as *mut c_void;
1239
1240        ctx.var_lookup_func = Some(dummy_var_lookup);
1241        ctx.var_lookup_data = data_ptr;
1242        ctx.func_lookup_func = Some(dummy_func_lookup);
1243        ctx.func_lookup_data = data_ptr;
1244
1245        assert!(ctx.var_lookup_func.is_some());
1246        assert!(!ctx.var_lookup_data.is_null());
1247        assert!(ctx.func_lookup_func.is_some());
1248        assert!(!ctx.func_lookup_data.is_null());
1249    }
1250
1251    // ── Clone ────────────────────────────────────────────────────────────
1252
1253    #[test]
1254    fn test_context_clone() {
1255        let mut ctx = XPathContext::new(std::ptr::null_mut());
1256        ctx.register_variable("x", XPathValue::Number(10.0));
1257        ctx.register_namespace("ns", "http://example.com/ns");
1258        ctx.set_error("clone test");
1259
1260        let cloned = ctx.clone();
1261        assert_eq!(cloned.document, ctx.document);
1262        assert_eq!(cloned.context_node, ctx.context_node);
1263        assert_eq!(cloned.context_position, ctx.context_position);
1264        assert_eq!(cloned.context_size, ctx.context_size);
1265        assert_eq!(cloned.error, ctx.error);
1266
1267        // Verify the clone has independent state.
1268        let var = cloned.resolve_variable("x");
1269        assert!(var.is_some());
1270        assert!((var.unwrap().as_number() - 10.0).abs() < 1e-10);
1271
1272        let ns = cloned.resolve_namespace("ns");
1273        assert!(ns.is_some());
1274        assert_eq!(ns.unwrap(), "http://example.com/ns");
1275    }
1276
1277    // ── Debug ────────────────────────────────────────────────────────────
1278
1279    #[test]
1280    fn test_context_debug_format() {
1281        let ctx = XPathContext::new(std::ptr::null_mut());
1282        let debug_str = format!("{:?}", ctx);
1283        assert!(debug_str.contains("context_position"));
1284        assert!(debug_str.contains("context_size"));
1285        assert!(debug_str.contains("recursion_depth"));
1286    }
1287
1288    // ── Edge cases ───────────────────────────────────────────────────────
1289
1290    #[test]
1291    fn test_context_size_zero() {
1292        let mut ctx = XPathContext::new(std::ptr::null_mut());
1293        ctx.set_context_list(vec![]);
1294        assert_eq!(ctx.last(), 0);
1295        assert_eq!(ctx.position(), 1);
1296    }
1297
1298    #[test]
1299    fn test_multiple_advancements() {
1300        let mut ctx = XPathContext::new(std::ptr::null_mut());
1301        for i in 1..=10 {
1302            assert_eq!(ctx.position(), i);
1303            ctx.advance_position();
1304        }
1305        assert_eq!(ctx.position(), 11);
1306    }
1307
1308    #[test]
1309    fn test_register_multiple_variables() {
1310        let mut ctx = XPathContext::new(std::ptr::null_mut());
1311        ctx.register_variable("a", XPathValue::Number(1.0));
1312        ctx.register_variable("b", XPathValue::String("two".to_string()));
1313        ctx.register_variable("c", XPathValue::Boolean(true));
1314
1315        assert_eq!(ctx.variables.len(), 3);
1316        assert_eq!(ctx.resolve_variable("a").unwrap().as_number(), 1.0);
1317        assert_eq!(ctx.resolve_variable("b").unwrap().as_string(), "two");
1318        assert!(ctx.resolve_variable("c").unwrap().as_boolean());
1319    }
1320
1321    #[test]
1322    fn test_register_multiple_namespaces() {
1323        let mut ctx = XPathContext::new(std::ptr::null_mut());
1324        ctx.register_namespace("a", "http://example.com/a");
1325        ctx.register_namespace("b", "http://example.com/b");
1326        ctx.register_namespace("c", "http://example.com/c");
1327
1328        assert_eq!(ctx.namespaces.len(), 3);
1329        assert_eq!(ctx.resolve_namespace("a").unwrap(), "http://example.com/a");
1330        assert_eq!(ctx.resolve_namespace("b").unwrap(), "http://example.com/b");
1331        assert_eq!(ctx.resolve_namespace("c").unwrap(), "http://example.com/c");
1332    }
1333
1334    #[test]
1335    fn test_register_multiple_functions() {
1336        fn f1(_: &mut XPathContext, _: &[XPathValue]) -> Result<XPathValue, String> {
1337            Ok(XPathValue::Number(1.0))
1338        }
1339        fn f2(_: &mut XPathContext, _: &[XPathValue]) -> Result<XPathValue, String> {
1340            Ok(XPathValue::Number(2.0))
1341        }
1342        fn f3(_: &mut XPathContext, _: &[XPathValue]) -> Result<XPathValue, String> {
1343            Ok(XPathValue::Number(3.0))
1344        }
1345
1346        let mut ctx = XPathContext::new(std::ptr::null_mut());
1347        ctx.register_function("f1", f1);
1348        ctx.register_function("f2", f2);
1349        ctx.register_function("f3", f3);
1350
1351        assert_eq!(ctx.functions.len(), 3);
1352    }
1353}