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