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