Skip to main content

libxml_rs/xml/xpath/
context.rs

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