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 (consumer-registered / extension) functions
479        // first — this preserves the upstream override contract (a consumer
480        // may register a same-named function that shadows a built-in).
481        if self.functions.contains_key(name) {
482            return self.functions.get(name);
483        }
484
485        // Core XPath 1.0 built-ins (static, allocation-free — Phase 16.5.9).
486        // The ordinary context no longer copies ~27 core functions into its
487        // own map at creation.
488        if let Some(f) = crate::xml::xpath::functions::lookup_core_function(name) {
489            return Some(f);
490        }
491
492        // Namespaced fallback (XSLT extension functions, EXSLT, ...): the
493        // resolved closure is memoised into the registry.
494        if let Some(lookup) = &self.function_lookup {
495            if let Some(func) = lookup(self, name) {
496                self.functions.insert(name.to_string(), func);
497                return self.functions.get(name);
498            }
499        }
500
501        // C-registered extension functions (xmlXPathRegisterFuncLookup) are
502        // resolved and invoked through the parser-context protocol in
503        // eval_function_call — they are NOT resolved here (a bare callback
504        // call with a non-NUL-terminated name crashes C consumers like
505        // nokogiri's handler lookup).
506        None
507    }
508
509    /// Register an extension function.
510    ///
511    /// The function is stored in the local `functions` map under `name`.
512    /// It will be found by [`lookup_function`](Self::lookup_function) before
513    /// any C callback is consulted. Accepts both fn pointers and capturing
514    /// closures.
515    pub fn register_function<F>(&mut self, name: &str, func: F)
516    where
517        F: Fn(&mut XPathContext, &[XPathValue]) -> Result<XPathValue, String>
518            + Send
519            + Sync
520            + 'static,
521    {
522        self.functions.insert(name.to_string(), Box::new(func));
523    }
524
525    /// Register a variable binding.
526    ///
527    /// The variable is stored in the local `variables` map under `name`.
528    /// It will be found by [`resolve_variable`](Self::resolve_variable) before
529    /// any C callback is consulted.
530    pub fn register_variable(&mut self, name: &str, value: XPathValue) {
531        self.variables.insert(name.to_string(), value);
532    }
533
534    /// Remove a variable binding from the context's variable hash.
535    ///
536    /// Used to unwind local XSLT variable scopes when a variable is popped
537    /// from the transform variable stack.
538    pub fn unregister_variable(&mut self, name: &str) {
539        self.variables.remove(name);
540    }
541
542    /// Register a namespace binding.
543    ///
544    /// Maps `prefix` to `uri` in the local `namespaces` map.
545    /// An empty prefix registers the default namespace.
546    pub fn register_namespace(&mut self, prefix: &str, uri: &str) {
547        self.namespaces.insert(prefix.to_string(), uri.to_string());
548    }
549
550    /// Record an error message.
551    ///
552    /// Overwrites any previously recorded error. Use `clear_error` to reset.
553    pub fn set_error(&mut self, msg: &str) {
554        self.error = Some(msg.to_string());
555    }
556
557    /// Clear any recorded error.
558    pub fn clear_error(&mut self) {
559        self.error = None;
560    }
561
562    /// Push onto the recursion stack.
563    ///
564    /// Increments `recursion_depth`. If the depth exceeds a reasonable limit
565    /// (currently 1000), returns `Err` with an overflow message.
566    ///
567    /// Callers should invoke this before recursing into expression evaluation
568    /// and call [`pop_recursion`](Self::pop_recursion) after returning.
569    pub fn push_recursion(&mut self) -> Result<(), String> {
570        const MAX_RECURSION_DEPTH: u32 = 1000;
571        if self.recursion_depth >= MAX_RECURSION_DEPTH {
572            return Err(
573                "XPath evaluation recursion depth exceeded (infinite recursion?)".to_string(),
574            );
575        }
576        self.recursion_depth += 1;
577        Ok(())
578    }
579
580    /// Pop from the recursion stack.
581    ///
582    /// Decrements `recursion_depth`. Must be called after a corresponding
583    /// [`push_recursion`](Self::push_recursion).
584    ///
585    /// # Panics
586    ///
587    /// Panics if `recursion_depth` is already 0 (indicating unbalanced
588    /// push/pop calls).
589    pub fn pop_recursion(&mut self) {
590        assert!(
591            self.recursion_depth > 0,
592            "unbalanced pop_recursion: recursion_depth is already 0"
593        );
594        self.recursion_depth -= 1;
595    }
596
597    /// Returns `true` if a context node is set (non-null).
598    pub const fn has_context_node(&self) -> bool {
599        !self.context_node.is_null()
600    }
601
602    /// Reset the context to its initial state, keeping the document pointer.
603    ///
604    /// Clears the context node, context list, error, and recursion depth.
605    /// Variable, namespace, and function bindings are preserved.
606    pub fn reset(&mut self) {
607        self.context_node = std::ptr::null_mut();
608        self.context_position = 1;
609        self.context_size = 1;
610        self.error = None;
611        self.proximity_position = 1;
612        self.context_list.clear();
613        self.recursion_depth = 0;
614    }
615
616    /// Returns the current proximity position (1-based).
617    ///
618    /// Equivalent to the XPath `position()` function.
619    pub const fn position(&self) -> i32 {
620        self.proximity_position
621    }
622
623    /// Returns the context size.
624    ///
625    /// Equivalent to the XPath `last()` function.
626    pub const fn last(&self) -> i32 {
627        self.context_size
628    }
629
630    /// Advance the proximity position by one.
631    ///
632    /// Called when iterating over the context list during predicate
633    /// evaluation.
634    pub const fn advance_position(&mut self) {
635        self.proximity_position += 1;
636        self.context_position = self.proximity_position;
637    }
638
639    /// Rewind the proximity position to 1.
640    pub const fn reset_position(&mut self) {
641        self.proximity_position = 1;
642        self.context_position = 1;
643    }
644}
645
646impl Default for XPathContext {
647    /// Create a default context with a null document pointer.
648    ///
649    /// This is useful when you need a context for testing or when the
650    /// document will be set later via [`set_context_node`](Self::set_context_node).
651    fn default() -> Self {
652        Self::new(std::ptr::null_mut())
653    }
654}
655
656// ═══════════════════════════════════════════════════════════════════════════════
657// Tests
658// ═══════════════════════════════════════════════════════════════════════════════
659
660#[cfg(test)]
661mod tests {
662    use super::*;
663
664    use crate::xml::xpath::types::NodeSet;
665
666    // ── Helpers ──────────────────────────────────────────────────────────
667
668    /// Create a minimal _xmlDoc for testing.
669    ///
670    /// SAFETY: The caller is responsible for freeing the allocated doc.
671    unsafe fn create_test_doc() -> *mut _xmlDoc {
672        // Allocate zeroed memory for a minimal document.
673        let layout = std::alloc::Layout::new::<_xmlDoc>();
674        let ptr = std::alloc::alloc_zeroed(layout) as *mut _xmlDoc;
675        assert!(!ptr.is_null(), "failed to allocate test document");
676        ptr
677    }
678
679    /// Create a minimal _xmlNode for testing.
680    ///
681    /// SAFETY: The caller is responsible for freeing the allocated node.
682    unsafe fn create_test_node() -> *mut _xmlNode {
683        let layout = std::alloc::Layout::new::<_xmlNode>();
684        let ptr = std::alloc::alloc_zeroed(layout) as *mut _xmlNode;
685        assert!(!ptr.is_null(), "failed to allocate test node");
686        ptr
687    }
688
689    /// SAFETY: Frees a test document allocated with `create_test_doc`.
690    unsafe fn free_test_doc(doc: *mut _xmlDoc) {
691        if !doc.is_null() {
692            let layout = std::alloc::Layout::new::<_xmlDoc>();
693            std::alloc::dealloc(doc as *mut u8, layout);
694        }
695    }
696
697    /// SAFETY: Frees a test node allocated with `create_test_node`.
698    unsafe fn free_test_node(node: *mut _xmlNode) {
699        if !node.is_null() {
700            let layout = std::alloc::Layout::new::<_xmlNode>();
701            std::alloc::dealloc(node as *mut u8, layout);
702        }
703    }
704
705    // ── Construction ─────────────────────────────────────────────────────
706
707    #[test]
708    fn test_new_context() {
709        let ctx = XPathContext::new(std::ptr::null_mut());
710        assert!(ctx.document.is_null());
711        assert!(ctx.context_node.is_null());
712        assert_eq!(ctx.context_position, 1);
713        assert_eq!(ctx.context_size, 1);
714        assert!(ctx.variables.is_empty());
715        assert!(ctx.namespaces.is_empty());
716        assert!(ctx.functions.is_empty());
717        assert!(ctx.error.is_none());
718        assert_eq!(ctx.proximity_position, 1);
719        assert!(ctx.context_list.is_empty());
720        assert_eq!(ctx.recursion_depth, 0);
721        assert!(ctx.var_lookup_func.is_none());
722        assert!(ctx.var_lookup_data.is_null());
723        assert!(ctx.func_lookup_func.is_none());
724        assert!(ctx.func_lookup_data.is_null());
725    }
726
727    #[test]
728    fn test_default_context() {
729        let ctx = XPathContext::default();
730        assert!(ctx.document.is_null());
731        assert_eq!(ctx.context_position, 1);
732    }
733
734    /// Create a context with a document and check it is recorded.
735    ///
736    /// # Safety
737    ///
738    /// - `doc` is a valid, aligned `_xmlDoc` allocated by `create_test_doc`
739    ///   and freed with `free_test_doc` exactly once; it is only stored as
740    ///   a pointer, never dereferenced, during the test.
741    #[test]
742    fn test_new_with_doc() {
743        unsafe {
744            let doc = create_test_doc();
745            let ctx = XPathContext::new(doc);
746            assert_eq!(ctx.document, doc);
747            free_test_doc(doc);
748        }
749    }
750
751    // ── set_context_node ─────────────────────────────────────────────────
752
753    /// Set a non-NULL context node and verify position state.
754    ///
755    /// # Safety
756    ///
757    /// - `node` is a valid, aligned `_xmlNode` allocated by
758    ///   `create_test_node` and freed with `free_test_node` exactly once;
759    ///   the context only stores and compares the pointer.
760    #[test]
761    fn test_set_context_node_non_null() {
762        unsafe {
763            let node = create_test_node();
764            let mut ctx = XPathContext::new(std::ptr::null_mut());
765            ctx.set_context_node(node);
766
767            assert_eq!(ctx.context_node, node);
768            assert_eq!(ctx.context_position, 1);
769            assert_eq!(ctx.context_size, 1);
770            assert_eq!(ctx.proximity_position, 1);
771            assert_eq!(ctx.context_list.len(), 1);
772            assert_eq!(ctx.context_list[0], node);
773
774            free_test_node(node);
775        }
776    }
777
778    #[test]
779    fn test_set_context_node_null() {
780        let mut ctx = XPathContext::new(std::ptr::null_mut());
781        // Set a non-null node first.
782        // SAFETY: We use a dangling pointer as a sentinel — it won't be dereferenced.
783        let sentinel = std::ptr::dangling_mut::<_xmlNode>();
784        ctx.context_list = vec![sentinel];
785        ctx.context_position = 5;
786        ctx.context_size = 5;
787        ctx.proximity_position = 5;
788
789        // Now set to null — should reset everything.
790        ctx.set_context_node(std::ptr::null_mut());
791        assert!(ctx.context_node.is_null());
792        assert!(ctx.context_list.is_empty());
793        assert_eq!(ctx.context_position, 1);
794        assert_eq!(ctx.context_size, 1);
795        assert_eq!(ctx.proximity_position, 1);
796    }
797
798    // ── set_context_list ─────────────────────────────────────────────────
799
800    /// Set a context list and verify size and position initialization.
801    ///
802    /// # Safety
803    ///
804    /// - `node1`/`node2` are valid, aligned `_xmlNode`s allocated by
805    ///   `create_test_node` and freed with `free_test_node`; the context
806    ///   stores the pointers in a `Vec` without dereferencing them.
807    #[test]
808    fn test_set_context_list() {
809        unsafe {
810            let node1 = create_test_node();
811            let node2 = create_test_node();
812            let nodes = vec![node1, node2];
813
814            let mut ctx = XPathContext::new(std::ptr::null_mut());
815            ctx.set_context_list(nodes.clone());
816
817            assert_eq!(ctx.context_list.len(), 2);
818            assert_eq!(ctx.context_size, 2);
819            assert_eq!(ctx.context_position, 1);
820            assert_eq!(ctx.proximity_position, 1);
821
822            free_test_node(node1);
823            free_test_node(node2);
824        }
825    }
826
827    #[test]
828    fn test_set_context_list_empty() {
829        let mut ctx = XPathContext::new(std::ptr::null_mut());
830        ctx.set_context_list(vec![]);
831
832        assert!(ctx.context_list.is_empty());
833        assert_eq!(ctx.context_size, 0);
834        assert_eq!(ctx.context_position, 1);
835    }
836
837    // ── Variables ────────────────────────────────────────────────────────
838
839    #[test]
840    fn test_register_and_resolve_variable() {
841        let mut ctx = XPathContext::new(std::ptr::null_mut());
842        ctx.register_variable("foo", XPathValue::String("bar".to_string()));
843
844        let result = ctx.resolve_variable("foo");
845        assert!(result.is_some());
846        assert_eq!(result.unwrap().as_string(), "bar");
847    }
848
849    #[test]
850    fn test_resolve_unknown_variable() {
851        let ctx = XPathContext::new(std::ptr::null_mut());
852        assert!(ctx.resolve_variable("nonexistent").is_none());
853    }
854    #[allow(clippy::approx_constant)]
855    #[test]
856    fn test_register_variable_number() {
857        let mut ctx = XPathContext::new(std::ptr::null_mut());
858        ctx.register_variable("pi", XPathValue::Number(3.14159));
859
860        let result = ctx.resolve_variable("pi");
861        assert!(result.is_some());
862        let val = result.unwrap();
863        assert!((val.as_number() - 3.14159).abs() < 1e-10);
864    }
865
866    #[test]
867    fn test_register_variable_boolean() {
868        let mut ctx = XPathContext::new(std::ptr::null_mut());
869        ctx.register_variable("flag", XPathValue::Boolean(true));
870
871        let result = ctx.resolve_variable("flag");
872        assert!(result.is_some());
873        assert!(result.unwrap().as_boolean());
874    }
875
876    #[test]
877    fn test_register_variable_nodeset() {
878        let mut ctx = XPathContext::new(std::ptr::null_mut());
879        let ns = NodeSet::new();
880        ctx.register_variable("nodes", XPathValue::NodeSet(ns));
881
882        let result = ctx.resolve_variable("nodes");
883        assert!(result.is_some());
884        assert!(matches!(result.unwrap(), XPathValue::NodeSet(_)));
885    }
886
887    #[test]
888    fn test_variable_overwrite() {
889        let mut ctx = XPathContext::new(std::ptr::null_mut());
890        ctx.register_variable("x", XPathValue::Number(1.0));
891        ctx.register_variable("x", XPathValue::Number(2.0));
892
893        let result = ctx.resolve_variable("x");
894        assert!(result.is_some());
895        assert!((result.unwrap().as_number() - 2.0).abs() < 1e-10);
896    }
897
898    // ── Namespaces ───────────────────────────────────────────────────────
899
900    #[test]
901    fn test_register_and_resolve_namespace() {
902        let mut ctx = XPathContext::new(std::ptr::null_mut());
903        ctx.register_namespace("xslt", "http://www.w3.org/1999/XSL/Transform");
904
905        let result = ctx.resolve_namespace("xslt");
906        assert!(result.is_some());
907        assert_eq!(result.unwrap(), "http://www.w3.org/1999/XSL/Transform");
908    }
909
910    #[test]
911    fn test_resolve_unknown_namespace() {
912        let ctx = XPathContext::new(std::ptr::null_mut());
913        // With no context node and no bindings, this should return None.
914        assert!(ctx.resolve_namespace("unknown").is_none());
915    }
916
917    #[test]
918    fn test_register_default_namespace() {
919        let mut ctx = XPathContext::new(std::ptr::null_mut());
920        ctx.register_namespace("", "http://example.com/default");
921
922        let result = ctx.resolve_namespace("");
923        assert!(result.is_some());
924        assert_eq!(result.unwrap(), "http://example.com/default");
925    }
926
927    #[test]
928    fn test_namespace_overwrite() {
929        let mut ctx = XPathContext::new(std::ptr::null_mut());
930        ctx.register_namespace("a", "http://example.com/1");
931        ctx.register_namespace("a", "http://example.com/2");
932
933        let result = ctx.resolve_namespace("a");
934        assert_eq!(result.unwrap(), "http://example.com/2");
935    }
936
937    // ── Functions ────────────────────────────────────────────────────────
938
939    #[test]
940    fn test_register_and_lookup_function() {
941        fn test_func(_ctx: &mut XPathContext, _args: &[XPathValue]) -> Result<XPathValue, String> {
942            Ok(XPathValue::String("test".to_string()))
943        }
944
945        let mut ctx = XPathContext::new(std::ptr::null_mut());
946        ctx.register_function("test:func", test_func);
947
948        let result = ctx.lookup_function("test:func");
949        assert!(result.is_some());
950    }
951
952    #[test]
953    fn test_lookup_unknown_function() {
954        let mut ctx = XPathContext::new(std::ptr::null_mut());
955        assert!(ctx.lookup_function("nonexistent").is_none());
956    }
957
958    #[test]
959    fn test_function_overwrite() {
960        fn func_a(_ctx: &mut XPathContext, _args: &[XPathValue]) -> Result<XPathValue, String> {
961            Ok(XPathValue::String("a".to_string()))
962        }
963        fn func_b(_ctx: &mut XPathContext, _args: &[XPathValue]) -> Result<XPathValue, String> {
964            Ok(XPathValue::String("b".to_string()))
965        }
966
967        let mut ctx = XPathContext::new(std::ptr::null_mut());
968        ctx.register_function("f", func_a);
969        ctx.register_function("f", func_b);
970
971        let result = ctx.lookup_function("f");
972        assert!(result.is_some());
973
974        // The overwritten function should be func_b.
975        if let Some(f) = result {
976            let mut tmp_ctx = XPathContext::new(std::ptr::null_mut());
977            let value = f(&mut tmp_ctx, &[]).unwrap();
978            assert_eq!(value.as_string(), "b");
979        }
980    }
981
982    // ── Error handling ───────────────────────────────────────────────────
983
984    #[test]
985    fn test_set_and_get_error() {
986        let mut ctx = XPathContext::new(std::ptr::null_mut());
987        assert!(ctx.error.is_none());
988
989        ctx.set_error("something went wrong");
990        assert_eq!(ctx.error.as_deref(), Some("something went wrong"));
991    }
992
993    #[test]
994    fn test_clear_error() {
995        let mut ctx = XPathContext::new(std::ptr::null_mut());
996        ctx.set_error("an error");
997        assert!(ctx.error.is_some());
998
999        ctx.clear_error();
1000        assert!(ctx.error.is_none());
1001    }
1002
1003    #[test]
1004    fn test_error_overwrite() {
1005        let mut ctx = XPathContext::new(std::ptr::null_mut());
1006        ctx.set_error("first error");
1007        ctx.set_error("second error");
1008        assert_eq!(ctx.error.as_deref(), Some("second error"));
1009    }
1010
1011    // ── Recursion depth ──────────────────────────────────────────────────
1012
1013    #[test]
1014    fn test_push_pop_recursion() {
1015        let mut ctx = XPathContext::new(std::ptr::null_mut());
1016        assert_eq!(ctx.recursion_depth, 0);
1017
1018        assert!(ctx.push_recursion().is_ok());
1019        assert_eq!(ctx.recursion_depth, 1);
1020
1021        ctx.pop_recursion();
1022        assert_eq!(ctx.recursion_depth, 0);
1023    }
1024
1025    #[test]
1026    fn test_recursion_depth_limit() {
1027        let mut ctx = XPathContext::new(std::ptr::null_mut());
1028
1029        // Push to the limit (1000).
1030        for _ in 0..1000 {
1031            assert!(ctx.push_recursion().is_ok());
1032        }
1033        assert_eq!(ctx.recursion_depth, 1000);
1034
1035        // The next push should fail.
1036        let result = ctx.push_recursion();
1037        assert!(result.is_err());
1038        assert!(result.unwrap_err().contains("recursion depth exceeded"));
1039
1040        // Pop back down.
1041        for _ in 0..1000 {
1042            ctx.pop_recursion();
1043        }
1044        assert_eq!(ctx.recursion_depth, 0);
1045    }
1046
1047    #[test]
1048    #[should_panic(expected = "unbalanced pop_recursion")]
1049    fn test_pop_recursion_underflow() {
1050        let mut ctx = XPathContext::new(std::ptr::null_mut());
1051        ctx.pop_recursion(); // depth is 0 — should panic
1052    }
1053
1054    #[test]
1055    fn test_recursion_nesting() {
1056        let mut ctx = XPathContext::new(std::ptr::null_mut());
1057
1058        // Simulate nested evaluation.
1059        assert!(ctx.push_recursion().is_ok());
1060        assert!(ctx.push_recursion().is_ok());
1061        assert!(ctx.push_recursion().is_ok());
1062        assert_eq!(ctx.recursion_depth, 3);
1063
1064        ctx.pop_recursion();
1065        assert_eq!(ctx.recursion_depth, 2);
1066
1067        ctx.pop_recursion();
1068        assert_eq!(ctx.recursion_depth, 1);
1069
1070        ctx.pop_recursion();
1071        assert_eq!(ctx.recursion_depth, 0);
1072    }
1073
1074    // ── Position / Size ──────────────────────────────────────────────────
1075
1076    #[test]
1077    fn test_position_and_last() {
1078        let ctx = XPathContext::new(std::ptr::null_mut());
1079        assert_eq!(ctx.position(), 1);
1080        assert_eq!(ctx.last(), 1);
1081    }
1082
1083    #[test]
1084    fn test_advance_position() {
1085        let mut ctx = XPathContext::new(std::ptr::null_mut());
1086        ctx.advance_position();
1087        assert_eq!(ctx.position(), 2);
1088        assert_eq!(ctx.proximity_position, 2);
1089        assert_eq!(ctx.context_position, 2);
1090    }
1091
1092    #[test]
1093    fn test_reset_position() {
1094        let mut ctx = XPathContext::new(std::ptr::null_mut());
1095        ctx.advance_position();
1096        ctx.advance_position();
1097        ctx.advance_position();
1098        assert_eq!(ctx.position(), 4);
1099
1100        ctx.reset_position();
1101        assert_eq!(ctx.position(), 1);
1102        assert_eq!(ctx.context_position, 1);
1103    }
1104
1105    /// Advance the position across a three-node context list.
1106    ///
1107    /// # Safety
1108    ///
1109    /// - The three nodes are valid, aligned `_xmlNode`s allocated by
1110    ///   `create_test_node` and freed with `free_test_node`; the context
1111    ///   only stores and counts the pointers.
1112    #[test]
1113    fn test_position_with_context_list() {
1114        unsafe {
1115            let node1 = create_test_node();
1116            let node2 = create_test_node();
1117            let node3 = create_test_node();
1118            let nodes = vec![node1, node2, node3];
1119
1120            let mut ctx = XPathContext::new(std::ptr::null_mut());
1121            ctx.set_context_list(nodes);
1122
1123            assert_eq!(ctx.last(), 3);
1124            assert_eq!(ctx.position(), 1);
1125
1126            ctx.advance_position();
1127            assert_eq!(ctx.position(), 2);
1128
1129            ctx.advance_position();
1130            assert_eq!(ctx.position(), 3);
1131
1132            free_test_node(node1);
1133            free_test_node(node2);
1134            free_test_node(node3);
1135        }
1136    }
1137
1138    // ── has_context_node ─────────────────────────────────────────────────
1139
1140    /// Check `has_context_node` before and after setting a node.
1141    ///
1142    /// # Safety
1143    ///
1144    /// - `node` is a valid, aligned `_xmlNode` allocated by
1145    ///   `create_test_node` and freed with `free_test_node` exactly once;
1146    ///   `has_context_node` only checks the stored pointer for NULL.
1147    #[test]
1148    fn test_has_context_node() {
1149        let mut ctx = XPathContext::new(std::ptr::null_mut());
1150        assert!(!ctx.has_context_node());
1151
1152        unsafe {
1153            let node = create_test_node();
1154            ctx.set_context_node(node);
1155            assert!(ctx.has_context_node());
1156            free_test_node(node);
1157        }
1158    }
1159
1160    // ── reset ────────────────────────────────────────────────────────────
1161
1162    #[test]
1163    fn test_reset() {
1164        let mut ctx = XPathContext::new(std::ptr::null_mut());
1165
1166        // Set up some state.
1167        ctx.set_error("test error");
1168        ctx.proximity_position = 5;
1169        ctx.context_position = 5;
1170        ctx.context_size = 10;
1171        ctx.recursion_depth = 3;
1172        {
1173            let sentinel = std::ptr::dangling_mut::<_xmlNode>();
1174            ctx.context_list = vec![sentinel];
1175        }
1176
1177        // Register some bindings — these should survive reset.
1178        ctx.register_variable("x", XPathValue::Number(42.0));
1179        ctx.register_namespace("p", "http://example.com/ns");
1180        fn dummy(_: &mut XPathContext, _: &[XPathValue]) -> Result<XPathValue, String> {
1181            Ok(XPathValue::Boolean(true))
1182        }
1183        ctx.register_function("f", dummy);
1184
1185        ctx.reset();
1186
1187        // Context node and position should be reset.
1188        assert!(ctx.context_node.is_null());
1189        assert_eq!(ctx.context_position, 1);
1190        assert_eq!(ctx.context_size, 1);
1191        assert_eq!(ctx.proximity_position, 1);
1192        assert!(ctx.error.is_none());
1193        assert!(ctx.context_list.is_empty());
1194        assert_eq!(ctx.recursion_depth, 0);
1195
1196        // Bindings should be preserved.
1197        assert!(ctx.resolve_variable("x").is_some());
1198        assert!(ctx.resolve_namespace("p").is_some());
1199        assert!(ctx.lookup_function("f").is_some());
1200    }
1201
1202    // ── C callback fields ────────────────────────────────────────────────
1203
1204    #[test]
1205    fn test_callback_fields_default_to_none() {
1206        let ctx = XPathContext::new(std::ptr::null_mut());
1207        assert!(ctx.var_lookup_func.is_none());
1208        assert!(ctx.var_lookup_data.is_null());
1209        assert!(ctx.func_lookup_func.is_none());
1210        assert!(ctx.func_lookup_data.is_null());
1211    }
1212
1213    #[test]
1214    fn test_set_callback_fields() {
1215        let mut ctx = XPathContext::new(std::ptr::null_mut());
1216
1217        /// A no-op variable-lookup callback returning NULL.
1218        ///
1219        /// # Safety
1220        ///
1221        /// - The callback is never invoked by this test; if installed it
1222        ///   must be a valid function pointer, and `data`/`ns`/`name`
1223        ///   would need to be valid pointers if it were called.
1224        unsafe extern "C" fn dummy_var_lookup(
1225            _data: *mut c_void,
1226            _ns: *const xmlChar,
1227            _name: *const xmlChar,
1228        ) -> *mut _xmlXPathObject {
1229            std::ptr::null_mut()
1230        }
1231
1232        /// A no-op function-lookup callback returning NULL.
1233        ///
1234        /// # Safety
1235        ///
1236        /// - The callback is never invoked by this test; if installed it
1237        ///   must be a valid function pointer, and `data`/`ns`/`name`
1238        ///   would need to be valid pointers if it were called.
1239        unsafe extern "C" fn dummy_func_lookup(
1240            _data: *mut c_void,
1241            _ns: *const xmlChar,
1242            _name: *const xmlChar,
1243        ) -> *mut c_void {
1244            std::ptr::null_mut()
1245        }
1246
1247        let data_ptr = &mut 42u32 as *mut u32 as *mut c_void;
1248
1249        ctx.var_lookup_func = Some(dummy_var_lookup);
1250        ctx.var_lookup_data = data_ptr;
1251        ctx.func_lookup_func = Some(dummy_func_lookup);
1252        ctx.func_lookup_data = data_ptr;
1253
1254        assert!(ctx.var_lookup_func.is_some());
1255        assert!(!ctx.var_lookup_data.is_null());
1256        assert!(ctx.func_lookup_func.is_some());
1257        assert!(!ctx.func_lookup_data.is_null());
1258    }
1259
1260    // ── Clone ────────────────────────────────────────────────────────────
1261
1262    #[test]
1263    fn test_context_clone() {
1264        let mut ctx = XPathContext::new(std::ptr::null_mut());
1265        ctx.register_variable("x", XPathValue::Number(10.0));
1266        ctx.register_namespace("ns", "http://example.com/ns");
1267        ctx.set_error("clone test");
1268
1269        let cloned = ctx.clone();
1270        assert_eq!(cloned.document, ctx.document);
1271        assert_eq!(cloned.context_node, ctx.context_node);
1272        assert_eq!(cloned.context_position, ctx.context_position);
1273        assert_eq!(cloned.context_size, ctx.context_size);
1274        assert_eq!(cloned.error, ctx.error);
1275
1276        // Verify the clone has independent state.
1277        let var = cloned.resolve_variable("x");
1278        assert!(var.is_some());
1279        assert!((var.unwrap().as_number() - 10.0).abs() < 1e-10);
1280
1281        let ns = cloned.resolve_namespace("ns");
1282        assert!(ns.is_some());
1283        assert_eq!(ns.unwrap(), "http://example.com/ns");
1284    }
1285
1286    // ── Debug ────────────────────────────────────────────────────────────
1287
1288    #[test]
1289    fn test_context_debug_format() {
1290        let ctx = XPathContext::new(std::ptr::null_mut());
1291        let debug_str = format!("{:?}", ctx);
1292        assert!(debug_str.contains("context_position"));
1293        assert!(debug_str.contains("context_size"));
1294        assert!(debug_str.contains("recursion_depth"));
1295    }
1296
1297    // ── Edge cases ───────────────────────────────────────────────────────
1298
1299    #[test]
1300    fn test_context_size_zero() {
1301        let mut ctx = XPathContext::new(std::ptr::null_mut());
1302        ctx.set_context_list(vec![]);
1303        assert_eq!(ctx.last(), 0);
1304        assert_eq!(ctx.position(), 1);
1305    }
1306
1307    #[test]
1308    fn test_multiple_advancements() {
1309        let mut ctx = XPathContext::new(std::ptr::null_mut());
1310        for i in 1..=10 {
1311            assert_eq!(ctx.position(), i);
1312            ctx.advance_position();
1313        }
1314        assert_eq!(ctx.position(), 11);
1315    }
1316
1317    #[test]
1318    fn test_register_multiple_variables() {
1319        let mut ctx = XPathContext::new(std::ptr::null_mut());
1320        ctx.register_variable("a", XPathValue::Number(1.0));
1321        ctx.register_variable("b", XPathValue::String("two".to_string()));
1322        ctx.register_variable("c", XPathValue::Boolean(true));
1323
1324        assert_eq!(ctx.variables.len(), 3);
1325        assert_eq!(ctx.resolve_variable("a").unwrap().as_number(), 1.0);
1326        assert_eq!(ctx.resolve_variable("b").unwrap().as_string(), "two");
1327        assert!(ctx.resolve_variable("c").unwrap().as_boolean());
1328    }
1329
1330    #[test]
1331    fn test_register_multiple_namespaces() {
1332        let mut ctx = XPathContext::new(std::ptr::null_mut());
1333        ctx.register_namespace("a", "http://example.com/a");
1334        ctx.register_namespace("b", "http://example.com/b");
1335        ctx.register_namespace("c", "http://example.com/c");
1336
1337        assert_eq!(ctx.namespaces.len(), 3);
1338        assert_eq!(ctx.resolve_namespace("a").unwrap(), "http://example.com/a");
1339        assert_eq!(ctx.resolve_namespace("b").unwrap(), "http://example.com/b");
1340        assert_eq!(ctx.resolve_namespace("c").unwrap(), "http://example.com/c");
1341    }
1342
1343    #[test]
1344    fn test_register_multiple_functions() {
1345        fn f1(_: &mut XPathContext, _: &[XPathValue]) -> Result<XPathValue, String> {
1346            Ok(XPathValue::Number(1.0))
1347        }
1348        fn f2(_: &mut XPathContext, _: &[XPathValue]) -> Result<XPathValue, String> {
1349            Ok(XPathValue::Number(2.0))
1350        }
1351        fn f3(_: &mut XPathContext, _: &[XPathValue]) -> Result<XPathValue, String> {
1352            Ok(XPathValue::Number(3.0))
1353        }
1354
1355        let mut ctx = XPathContext::new(std::ptr::null_mut());
1356        ctx.register_function("f1", f1);
1357        ctx.register_function("f2", f2);
1358        ctx.register_function("f3", f3);
1359
1360        assert_eq!(ctx.functions.len(), 3);
1361    }
1362}