Skip to main content

libxml_rs/xml/xpath/
context.rs

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