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