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