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 (consumer-registered / extension) functions
479 // first — this preserves the upstream override contract (a consumer
480 // may register a same-named function that shadows a built-in).
481 if self.functions.contains_key(name) {
482 return self.functions.get(name);
483 }
484
485 // Core XPath 1.0 built-ins (static, allocation-free — Phase 16.5.9).
486 // The ordinary context no longer copies ~27 core functions into its
487 // own map at creation.
488 if let Some(f) = crate::xml::xpath::functions::lookup_core_function(name) {
489 return Some(f);
490 }
491
492 // Namespaced fallback (XSLT extension functions, EXSLT, ...): the
493 // resolved closure is memoised into the registry.
494 if let Some(lookup) = &self.function_lookup {
495 if let Some(func) = lookup(self, name) {
496 self.functions.insert(name.to_string(), func);
497 return self.functions.get(name);
498 }
499 }
500
501 // C-registered extension functions (xmlXPathRegisterFuncLookup) are
502 // resolved and invoked through the parser-context protocol in
503 // eval_function_call — they are NOT resolved here (a bare callback
504 // call with a non-NUL-terminated name crashes C consumers like
505 // nokogiri's handler lookup).
506 None
507 }
508
509 /// Register an extension function.
510 ///
511 /// The function is stored in the local `functions` map under `name`.
512 /// It will be found by [`lookup_function`](Self::lookup_function) before
513 /// any C callback is consulted. Accepts both fn pointers and capturing
514 /// closures.
515 pub fn register_function<F>(&mut self, name: &str, func: F)
516 where
517 F: Fn(&mut XPathContext, &[XPathValue]) -> Result<XPathValue, String>
518 + Send
519 + Sync
520 + 'static,
521 {
522 self.functions.insert(name.to_string(), Box::new(func));
523 }
524
525 /// Register a variable binding.
526 ///
527 /// The variable is stored in the local `variables` map under `name`.
528 /// It will be found by [`resolve_variable`](Self::resolve_variable) before
529 /// any C callback is consulted.
530 pub fn register_variable(&mut self, name: &str, value: XPathValue) {
531 self.variables.insert(name.to_string(), value);
532 }
533
534 /// Remove a variable binding from the context's variable hash.
535 ///
536 /// Used to unwind local XSLT variable scopes when a variable is popped
537 /// from the transform variable stack.
538 pub fn unregister_variable(&mut self, name: &str) {
539 self.variables.remove(name);
540 }
541
542 /// Register a namespace binding.
543 ///
544 /// Maps `prefix` to `uri` in the local `namespaces` map.
545 /// An empty prefix registers the default namespace.
546 pub fn register_namespace(&mut self, prefix: &str, uri: &str) {
547 self.namespaces.insert(prefix.to_string(), uri.to_string());
548 }
549
550 /// Record an error message.
551 ///
552 /// Overwrites any previously recorded error. Use `clear_error` to reset.
553 pub fn set_error(&mut self, msg: &str) {
554 self.error = Some(msg.to_string());
555 }
556
557 /// Clear any recorded error.
558 pub fn clear_error(&mut self) {
559 self.error = None;
560 }
561
562 /// Push onto the recursion stack.
563 ///
564 /// Increments `recursion_depth`. If the depth exceeds a reasonable limit
565 /// (currently 1000), returns `Err` with an overflow message.
566 ///
567 /// Callers should invoke this before recursing into expression evaluation
568 /// and call [`pop_recursion`](Self::pop_recursion) after returning.
569 pub fn push_recursion(&mut self) -> Result<(), String> {
570 const MAX_RECURSION_DEPTH: u32 = 1000;
571 if self.recursion_depth >= MAX_RECURSION_DEPTH {
572 return Err(
573 "XPath evaluation recursion depth exceeded (infinite recursion?)".to_string(),
574 );
575 }
576 self.recursion_depth += 1;
577 Ok(())
578 }
579
580 /// Pop from the recursion stack.
581 ///
582 /// Decrements `recursion_depth`. Must be called after a corresponding
583 /// [`push_recursion`](Self::push_recursion).
584 ///
585 /// # Panics
586 ///
587 /// Panics if `recursion_depth` is already 0 (indicating unbalanced
588 /// push/pop calls).
589 pub fn pop_recursion(&mut self) {
590 assert!(
591 self.recursion_depth > 0,
592 "unbalanced pop_recursion: recursion_depth is already 0"
593 );
594 self.recursion_depth -= 1;
595 }
596
597 /// Returns `true` if a context node is set (non-null).
598 pub const fn has_context_node(&self) -> bool {
599 !self.context_node.is_null()
600 }
601
602 /// Reset the context to its initial state, keeping the document pointer.
603 ///
604 /// Clears the context node, context list, error, and recursion depth.
605 /// Variable, namespace, and function bindings are preserved.
606 pub fn reset(&mut self) {
607 self.context_node = std::ptr::null_mut();
608 self.context_position = 1;
609 self.context_size = 1;
610 self.error = None;
611 self.proximity_position = 1;
612 self.context_list.clear();
613 self.recursion_depth = 0;
614 }
615
616 /// Returns the current proximity position (1-based).
617 ///
618 /// Equivalent to the XPath `position()` function.
619 pub const fn position(&self) -> i32 {
620 self.proximity_position
621 }
622
623 /// Returns the context size.
624 ///
625 /// Equivalent to the XPath `last()` function.
626 pub const fn last(&self) -> i32 {
627 self.context_size
628 }
629
630 /// Advance the proximity position by one.
631 ///
632 /// Called when iterating over the context list during predicate
633 /// evaluation.
634 pub const fn advance_position(&mut self) {
635 self.proximity_position += 1;
636 self.context_position = self.proximity_position;
637 }
638
639 /// Rewind the proximity position to 1.
640 pub const fn reset_position(&mut self) {
641 self.proximity_position = 1;
642 self.context_position = 1;
643 }
644}
645
646impl Default for XPathContext {
647 /// Create a default context with a null document pointer.
648 ///
649 /// This is useful when you need a context for testing or when the
650 /// document will be set later via [`set_context_node`](Self::set_context_node).
651 fn default() -> Self {
652 Self::new(std::ptr::null_mut())
653 }
654}
655
656// ═══════════════════════════════════════════════════════════════════════════════
657// Tests
658// ═══════════════════════════════════════════════════════════════════════════════
659
660#[cfg(test)]
661mod tests {
662 use super::*;
663
664 use crate::xml::xpath::types::NodeSet;
665
666 // ── Helpers ──────────────────────────────────────────────────────────
667
668 /// Create a minimal _xmlDoc for testing.
669 ///
670 /// SAFETY: The caller is responsible for freeing the allocated doc.
671 unsafe fn create_test_doc() -> *mut _xmlDoc {
672 // Allocate zeroed memory for a minimal document.
673 let layout = std::alloc::Layout::new::<_xmlDoc>();
674 let ptr = std::alloc::alloc_zeroed(layout) as *mut _xmlDoc;
675 assert!(!ptr.is_null(), "failed to allocate test document");
676 ptr
677 }
678
679 /// Create a minimal _xmlNode for testing.
680 ///
681 /// SAFETY: The caller is responsible for freeing the allocated node.
682 unsafe fn create_test_node() -> *mut _xmlNode {
683 let layout = std::alloc::Layout::new::<_xmlNode>();
684 let ptr = std::alloc::alloc_zeroed(layout) as *mut _xmlNode;
685 assert!(!ptr.is_null(), "failed to allocate test node");
686 ptr
687 }
688
689 /// SAFETY: Frees a test document allocated with `create_test_doc`.
690 unsafe fn free_test_doc(doc: *mut _xmlDoc) {
691 if !doc.is_null() {
692 let layout = std::alloc::Layout::new::<_xmlDoc>();
693 std::alloc::dealloc(doc as *mut u8, layout);
694 }
695 }
696
697 /// SAFETY: Frees a test node allocated with `create_test_node`.
698 unsafe fn free_test_node(node: *mut _xmlNode) {
699 if !node.is_null() {
700 let layout = std::alloc::Layout::new::<_xmlNode>();
701 std::alloc::dealloc(node as *mut u8, layout);
702 }
703 }
704
705 // ── Construction ─────────────────────────────────────────────────────
706
707 #[test]
708 fn test_new_context() {
709 let ctx = XPathContext::new(std::ptr::null_mut());
710 assert!(ctx.document.is_null());
711 assert!(ctx.context_node.is_null());
712 assert_eq!(ctx.context_position, 1);
713 assert_eq!(ctx.context_size, 1);
714 assert!(ctx.variables.is_empty());
715 assert!(ctx.namespaces.is_empty());
716 assert!(ctx.functions.is_empty());
717 assert!(ctx.error.is_none());
718 assert_eq!(ctx.proximity_position, 1);
719 assert!(ctx.context_list.is_empty());
720 assert_eq!(ctx.recursion_depth, 0);
721 assert!(ctx.var_lookup_func.is_none());
722 assert!(ctx.var_lookup_data.is_null());
723 assert!(ctx.func_lookup_func.is_none());
724 assert!(ctx.func_lookup_data.is_null());
725 }
726
727 #[test]
728 fn test_default_context() {
729 let ctx = XPathContext::default();
730 assert!(ctx.document.is_null());
731 assert_eq!(ctx.context_position, 1);
732 }
733
734 /// Create a context with a document and check it is recorded.
735 ///
736 /// # Safety
737 ///
738 /// - `doc` is a valid, aligned `_xmlDoc` allocated by `create_test_doc`
739 /// and freed with `free_test_doc` exactly once; it is only stored as
740 /// a pointer, never dereferenced, during the test.
741 #[test]
742 fn test_new_with_doc() {
743 unsafe {
744 let doc = create_test_doc();
745 let ctx = XPathContext::new(doc);
746 assert_eq!(ctx.document, doc);
747 free_test_doc(doc);
748 }
749 }
750
751 // ── set_context_node ─────────────────────────────────────────────────
752
753 /// Set a non-NULL context node and verify position state.
754 ///
755 /// # Safety
756 ///
757 /// - `node` is a valid, aligned `_xmlNode` allocated by
758 /// `create_test_node` and freed with `free_test_node` exactly once;
759 /// the context only stores and compares the pointer.
760 #[test]
761 fn test_set_context_node_non_null() {
762 unsafe {
763 let node = create_test_node();
764 let mut ctx = XPathContext::new(std::ptr::null_mut());
765 ctx.set_context_node(node);
766
767 assert_eq!(ctx.context_node, node);
768 assert_eq!(ctx.context_position, 1);
769 assert_eq!(ctx.context_size, 1);
770 assert_eq!(ctx.proximity_position, 1);
771 assert_eq!(ctx.context_list.len(), 1);
772 assert_eq!(ctx.context_list[0], node);
773
774 free_test_node(node);
775 }
776 }
777
778 #[test]
779 fn test_set_context_node_null() {
780 let mut ctx = XPathContext::new(std::ptr::null_mut());
781 // Set a non-null node first.
782 // SAFETY: We use a dangling pointer as a sentinel — it won't be dereferenced.
783 let sentinel = std::ptr::dangling_mut::<_xmlNode>();
784 ctx.context_list = vec![sentinel];
785 ctx.context_position = 5;
786 ctx.context_size = 5;
787 ctx.proximity_position = 5;
788
789 // Now set to null — should reset everything.
790 ctx.set_context_node(std::ptr::null_mut());
791 assert!(ctx.context_node.is_null());
792 assert!(ctx.context_list.is_empty());
793 assert_eq!(ctx.context_position, 1);
794 assert_eq!(ctx.context_size, 1);
795 assert_eq!(ctx.proximity_position, 1);
796 }
797
798 // ── set_context_list ─────────────────────────────────────────────────
799
800 /// Set a context list and verify size and position initialization.
801 ///
802 /// # Safety
803 ///
804 /// - `node1`/`node2` are valid, aligned `_xmlNode`s allocated by
805 /// `create_test_node` and freed with `free_test_node`; the context
806 /// stores the pointers in a `Vec` without dereferencing them.
807 #[test]
808 fn test_set_context_list() {
809 unsafe {
810 let node1 = create_test_node();
811 let node2 = create_test_node();
812 let nodes = vec![node1, node2];
813
814 let mut ctx = XPathContext::new(std::ptr::null_mut());
815 ctx.set_context_list(nodes.clone());
816
817 assert_eq!(ctx.context_list.len(), 2);
818 assert_eq!(ctx.context_size, 2);
819 assert_eq!(ctx.context_position, 1);
820 assert_eq!(ctx.proximity_position, 1);
821
822 free_test_node(node1);
823 free_test_node(node2);
824 }
825 }
826
827 #[test]
828 fn test_set_context_list_empty() {
829 let mut ctx = XPathContext::new(std::ptr::null_mut());
830 ctx.set_context_list(vec![]);
831
832 assert!(ctx.context_list.is_empty());
833 assert_eq!(ctx.context_size, 0);
834 assert_eq!(ctx.context_position, 1);
835 }
836
837 // ── Variables ────────────────────────────────────────────────────────
838
839 #[test]
840 fn test_register_and_resolve_variable() {
841 let mut ctx = XPathContext::new(std::ptr::null_mut());
842 ctx.register_variable("foo", XPathValue::String("bar".to_string()));
843
844 let result = ctx.resolve_variable("foo");
845 assert!(result.is_some());
846 assert_eq!(result.unwrap().as_string(), "bar");
847 }
848
849 #[test]
850 fn test_resolve_unknown_variable() {
851 let ctx = XPathContext::new(std::ptr::null_mut());
852 assert!(ctx.resolve_variable("nonexistent").is_none());
853 }
854 #[allow(clippy::approx_constant)]
855 #[test]
856 fn test_register_variable_number() {
857 let mut ctx = XPathContext::new(std::ptr::null_mut());
858 ctx.register_variable("pi", XPathValue::Number(3.14159));
859
860 let result = ctx.resolve_variable("pi");
861 assert!(result.is_some());
862 let val = result.unwrap();
863 assert!((val.as_number() - 3.14159).abs() < 1e-10);
864 }
865
866 #[test]
867 fn test_register_variable_boolean() {
868 let mut ctx = XPathContext::new(std::ptr::null_mut());
869 ctx.register_variable("flag", XPathValue::Boolean(true));
870
871 let result = ctx.resolve_variable("flag");
872 assert!(result.is_some());
873 assert!(result.unwrap().as_boolean());
874 }
875
876 #[test]
877 fn test_register_variable_nodeset() {
878 let mut ctx = XPathContext::new(std::ptr::null_mut());
879 let ns = NodeSet::new();
880 ctx.register_variable("nodes", XPathValue::NodeSet(ns));
881
882 let result = ctx.resolve_variable("nodes");
883 assert!(result.is_some());
884 assert!(matches!(result.unwrap(), XPathValue::NodeSet(_)));
885 }
886
887 #[test]
888 fn test_variable_overwrite() {
889 let mut ctx = XPathContext::new(std::ptr::null_mut());
890 ctx.register_variable("x", XPathValue::Number(1.0));
891 ctx.register_variable("x", XPathValue::Number(2.0));
892
893 let result = ctx.resolve_variable("x");
894 assert!(result.is_some());
895 assert!((result.unwrap().as_number() - 2.0).abs() < 1e-10);
896 }
897
898 // ── Namespaces ───────────────────────────────────────────────────────
899
900 #[test]
901 fn test_register_and_resolve_namespace() {
902 let mut ctx = XPathContext::new(std::ptr::null_mut());
903 ctx.register_namespace("xslt", "http://www.w3.org/1999/XSL/Transform");
904
905 let result = ctx.resolve_namespace("xslt");
906 assert!(result.is_some());
907 assert_eq!(result.unwrap(), "http://www.w3.org/1999/XSL/Transform");
908 }
909
910 #[test]
911 fn test_resolve_unknown_namespace() {
912 let ctx = XPathContext::new(std::ptr::null_mut());
913 // With no context node and no bindings, this should return None.
914 assert!(ctx.resolve_namespace("unknown").is_none());
915 }
916
917 #[test]
918 fn test_register_default_namespace() {
919 let mut ctx = XPathContext::new(std::ptr::null_mut());
920 ctx.register_namespace("", "http://example.com/default");
921
922 let result = ctx.resolve_namespace("");
923 assert!(result.is_some());
924 assert_eq!(result.unwrap(), "http://example.com/default");
925 }
926
927 #[test]
928 fn test_namespace_overwrite() {
929 let mut ctx = XPathContext::new(std::ptr::null_mut());
930 ctx.register_namespace("a", "http://example.com/1");
931 ctx.register_namespace("a", "http://example.com/2");
932
933 let result = ctx.resolve_namespace("a");
934 assert_eq!(result.unwrap(), "http://example.com/2");
935 }
936
937 // ── Functions ────────────────────────────────────────────────────────
938
939 #[test]
940 fn test_register_and_lookup_function() {
941 fn test_func(_ctx: &mut XPathContext, _args: &[XPathValue]) -> Result<XPathValue, String> {
942 Ok(XPathValue::String("test".to_string()))
943 }
944
945 let mut ctx = XPathContext::new(std::ptr::null_mut());
946 ctx.register_function("test:func", test_func);
947
948 let result = ctx.lookup_function("test:func");
949 assert!(result.is_some());
950 }
951
952 #[test]
953 fn test_lookup_unknown_function() {
954 let mut ctx = XPathContext::new(std::ptr::null_mut());
955 assert!(ctx.lookup_function("nonexistent").is_none());
956 }
957
958 #[test]
959 fn test_function_overwrite() {
960 fn func_a(_ctx: &mut XPathContext, _args: &[XPathValue]) -> Result<XPathValue, String> {
961 Ok(XPathValue::String("a".to_string()))
962 }
963 fn func_b(_ctx: &mut XPathContext, _args: &[XPathValue]) -> Result<XPathValue, String> {
964 Ok(XPathValue::String("b".to_string()))
965 }
966
967 let mut ctx = XPathContext::new(std::ptr::null_mut());
968 ctx.register_function("f", func_a);
969 ctx.register_function("f", func_b);
970
971 let result = ctx.lookup_function("f");
972 assert!(result.is_some());
973
974 // The overwritten function should be func_b.
975 if let Some(f) = result {
976 let mut tmp_ctx = XPathContext::new(std::ptr::null_mut());
977 let value = f(&mut tmp_ctx, &[]).unwrap();
978 assert_eq!(value.as_string(), "b");
979 }
980 }
981
982 // ── Error handling ───────────────────────────────────────────────────
983
984 #[test]
985 fn test_set_and_get_error() {
986 let mut ctx = XPathContext::new(std::ptr::null_mut());
987 assert!(ctx.error.is_none());
988
989 ctx.set_error("something went wrong");
990 assert_eq!(ctx.error.as_deref(), Some("something went wrong"));
991 }
992
993 #[test]
994 fn test_clear_error() {
995 let mut ctx = XPathContext::new(std::ptr::null_mut());
996 ctx.set_error("an error");
997 assert!(ctx.error.is_some());
998
999 ctx.clear_error();
1000 assert!(ctx.error.is_none());
1001 }
1002
1003 #[test]
1004 fn test_error_overwrite() {
1005 let mut ctx = XPathContext::new(std::ptr::null_mut());
1006 ctx.set_error("first error");
1007 ctx.set_error("second error");
1008 assert_eq!(ctx.error.as_deref(), Some("second error"));
1009 }
1010
1011 // ── Recursion depth ──────────────────────────────────────────────────
1012
1013 #[test]
1014 fn test_push_pop_recursion() {
1015 let mut ctx = XPathContext::new(std::ptr::null_mut());
1016 assert_eq!(ctx.recursion_depth, 0);
1017
1018 assert!(ctx.push_recursion().is_ok());
1019 assert_eq!(ctx.recursion_depth, 1);
1020
1021 ctx.pop_recursion();
1022 assert_eq!(ctx.recursion_depth, 0);
1023 }
1024
1025 #[test]
1026 fn test_recursion_depth_limit() {
1027 let mut ctx = XPathContext::new(std::ptr::null_mut());
1028
1029 // Push to the limit (1000).
1030 for _ in 0..1000 {
1031 assert!(ctx.push_recursion().is_ok());
1032 }
1033 assert_eq!(ctx.recursion_depth, 1000);
1034
1035 // The next push should fail.
1036 let result = ctx.push_recursion();
1037 assert!(result.is_err());
1038 assert!(result.unwrap_err().contains("recursion depth exceeded"));
1039
1040 // Pop back down.
1041 for _ in 0..1000 {
1042 ctx.pop_recursion();
1043 }
1044 assert_eq!(ctx.recursion_depth, 0);
1045 }
1046
1047 #[test]
1048 #[should_panic(expected = "unbalanced pop_recursion")]
1049 fn test_pop_recursion_underflow() {
1050 let mut ctx = XPathContext::new(std::ptr::null_mut());
1051 ctx.pop_recursion(); // depth is 0 — should panic
1052 }
1053
1054 #[test]
1055 fn test_recursion_nesting() {
1056 let mut ctx = XPathContext::new(std::ptr::null_mut());
1057
1058 // Simulate nested evaluation.
1059 assert!(ctx.push_recursion().is_ok());
1060 assert!(ctx.push_recursion().is_ok());
1061 assert!(ctx.push_recursion().is_ok());
1062 assert_eq!(ctx.recursion_depth, 3);
1063
1064 ctx.pop_recursion();
1065 assert_eq!(ctx.recursion_depth, 2);
1066
1067 ctx.pop_recursion();
1068 assert_eq!(ctx.recursion_depth, 1);
1069
1070 ctx.pop_recursion();
1071 assert_eq!(ctx.recursion_depth, 0);
1072 }
1073
1074 // ── Position / Size ──────────────────────────────────────────────────
1075
1076 #[test]
1077 fn test_position_and_last() {
1078 let ctx = XPathContext::new(std::ptr::null_mut());
1079 assert_eq!(ctx.position(), 1);
1080 assert_eq!(ctx.last(), 1);
1081 }
1082
1083 #[test]
1084 fn test_advance_position() {
1085 let mut ctx = XPathContext::new(std::ptr::null_mut());
1086 ctx.advance_position();
1087 assert_eq!(ctx.position(), 2);
1088 assert_eq!(ctx.proximity_position, 2);
1089 assert_eq!(ctx.context_position, 2);
1090 }
1091
1092 #[test]
1093 fn test_reset_position() {
1094 let mut ctx = XPathContext::new(std::ptr::null_mut());
1095 ctx.advance_position();
1096 ctx.advance_position();
1097 ctx.advance_position();
1098 assert_eq!(ctx.position(), 4);
1099
1100 ctx.reset_position();
1101 assert_eq!(ctx.position(), 1);
1102 assert_eq!(ctx.context_position, 1);
1103 }
1104
1105 /// Advance the position across a three-node context list.
1106 ///
1107 /// # Safety
1108 ///
1109 /// - The three nodes are valid, aligned `_xmlNode`s allocated by
1110 /// `create_test_node` and freed with `free_test_node`; the context
1111 /// only stores and counts the pointers.
1112 #[test]
1113 fn test_position_with_context_list() {
1114 unsafe {
1115 let node1 = create_test_node();
1116 let node2 = create_test_node();
1117 let node3 = create_test_node();
1118 let nodes = vec![node1, node2, node3];
1119
1120 let mut ctx = XPathContext::new(std::ptr::null_mut());
1121 ctx.set_context_list(nodes);
1122
1123 assert_eq!(ctx.last(), 3);
1124 assert_eq!(ctx.position(), 1);
1125
1126 ctx.advance_position();
1127 assert_eq!(ctx.position(), 2);
1128
1129 ctx.advance_position();
1130 assert_eq!(ctx.position(), 3);
1131
1132 free_test_node(node1);
1133 free_test_node(node2);
1134 free_test_node(node3);
1135 }
1136 }
1137
1138 // ── has_context_node ─────────────────────────────────────────────────
1139
1140 /// Check `has_context_node` before and after setting a node.
1141 ///
1142 /// # Safety
1143 ///
1144 /// - `node` is a valid, aligned `_xmlNode` allocated by
1145 /// `create_test_node` and freed with `free_test_node` exactly once;
1146 /// `has_context_node` only checks the stored pointer for NULL.
1147 #[test]
1148 fn test_has_context_node() {
1149 let mut ctx = XPathContext::new(std::ptr::null_mut());
1150 assert!(!ctx.has_context_node());
1151
1152 unsafe {
1153 let node = create_test_node();
1154 ctx.set_context_node(node);
1155 assert!(ctx.has_context_node());
1156 free_test_node(node);
1157 }
1158 }
1159
1160 // ── reset ────────────────────────────────────────────────────────────
1161
1162 #[test]
1163 fn test_reset() {
1164 let mut ctx = XPathContext::new(std::ptr::null_mut());
1165
1166 // Set up some state.
1167 ctx.set_error("test error");
1168 ctx.proximity_position = 5;
1169 ctx.context_position = 5;
1170 ctx.context_size = 10;
1171 ctx.recursion_depth = 3;
1172 {
1173 let sentinel = std::ptr::dangling_mut::<_xmlNode>();
1174 ctx.context_list = vec![sentinel];
1175 }
1176
1177 // Register some bindings — these should survive reset.
1178 ctx.register_variable("x", XPathValue::Number(42.0));
1179 ctx.register_namespace("p", "http://example.com/ns");
1180 fn dummy(_: &mut XPathContext, _: &[XPathValue]) -> Result<XPathValue, String> {
1181 Ok(XPathValue::Boolean(true))
1182 }
1183 ctx.register_function("f", dummy);
1184
1185 ctx.reset();
1186
1187 // Context node and position should be reset.
1188 assert!(ctx.context_node.is_null());
1189 assert_eq!(ctx.context_position, 1);
1190 assert_eq!(ctx.context_size, 1);
1191 assert_eq!(ctx.proximity_position, 1);
1192 assert!(ctx.error.is_none());
1193 assert!(ctx.context_list.is_empty());
1194 assert_eq!(ctx.recursion_depth, 0);
1195
1196 // Bindings should be preserved.
1197 assert!(ctx.resolve_variable("x").is_some());
1198 assert!(ctx.resolve_namespace("p").is_some());
1199 assert!(ctx.lookup_function("f").is_some());
1200 }
1201
1202 // ── C callback fields ────────────────────────────────────────────────
1203
1204 #[test]
1205 fn test_callback_fields_default_to_none() {
1206 let ctx = XPathContext::new(std::ptr::null_mut());
1207 assert!(ctx.var_lookup_func.is_none());
1208 assert!(ctx.var_lookup_data.is_null());
1209 assert!(ctx.func_lookup_func.is_none());
1210 assert!(ctx.func_lookup_data.is_null());
1211 }
1212
1213 #[test]
1214 fn test_set_callback_fields() {
1215 let mut ctx = XPathContext::new(std::ptr::null_mut());
1216
1217 /// A no-op variable-lookup callback returning NULL.
1218 ///
1219 /// # Safety
1220 ///
1221 /// - The callback is never invoked by this test; if installed it
1222 /// must be a valid function pointer, and `data`/`ns`/`name`
1223 /// would need to be valid pointers if it were called.
1224 unsafe extern "C" fn dummy_var_lookup(
1225 _data: *mut c_void,
1226 _ns: *const xmlChar,
1227 _name: *const xmlChar,
1228 ) -> *mut _xmlXPathObject {
1229 std::ptr::null_mut()
1230 }
1231
1232 /// A no-op function-lookup callback returning NULL.
1233 ///
1234 /// # Safety
1235 ///
1236 /// - The callback is never invoked by this test; if installed it
1237 /// must be a valid function pointer, and `data`/`ns`/`name`
1238 /// would need to be valid pointers if it were called.
1239 unsafe extern "C" fn dummy_func_lookup(
1240 _data: *mut c_void,
1241 _ns: *const xmlChar,
1242 _name: *const xmlChar,
1243 ) -> *mut c_void {
1244 std::ptr::null_mut()
1245 }
1246
1247 let data_ptr = &mut 42u32 as *mut u32 as *mut c_void;
1248
1249 ctx.var_lookup_func = Some(dummy_var_lookup);
1250 ctx.var_lookup_data = data_ptr;
1251 ctx.func_lookup_func = Some(dummy_func_lookup);
1252 ctx.func_lookup_data = data_ptr;
1253
1254 assert!(ctx.var_lookup_func.is_some());
1255 assert!(!ctx.var_lookup_data.is_null());
1256 assert!(ctx.func_lookup_func.is_some());
1257 assert!(!ctx.func_lookup_data.is_null());
1258 }
1259
1260 // ── Clone ────────────────────────────────────────────────────────────
1261
1262 #[test]
1263 fn test_context_clone() {
1264 let mut ctx = XPathContext::new(std::ptr::null_mut());
1265 ctx.register_variable("x", XPathValue::Number(10.0));
1266 ctx.register_namespace("ns", "http://example.com/ns");
1267 ctx.set_error("clone test");
1268
1269 let cloned = ctx.clone();
1270 assert_eq!(cloned.document, ctx.document);
1271 assert_eq!(cloned.context_node, ctx.context_node);
1272 assert_eq!(cloned.context_position, ctx.context_position);
1273 assert_eq!(cloned.context_size, ctx.context_size);
1274 assert_eq!(cloned.error, ctx.error);
1275
1276 // Verify the clone has independent state.
1277 let var = cloned.resolve_variable("x");
1278 assert!(var.is_some());
1279 assert!((var.unwrap().as_number() - 10.0).abs() < 1e-10);
1280
1281 let ns = cloned.resolve_namespace("ns");
1282 assert!(ns.is_some());
1283 assert_eq!(ns.unwrap(), "http://example.com/ns");
1284 }
1285
1286 // ── Debug ────────────────────────────────────────────────────────────
1287
1288 #[test]
1289 fn test_context_debug_format() {
1290 let ctx = XPathContext::new(std::ptr::null_mut());
1291 let debug_str = format!("{:?}", ctx);
1292 assert!(debug_str.contains("context_position"));
1293 assert!(debug_str.contains("context_size"));
1294 assert!(debug_str.contains("recursion_depth"));
1295 }
1296
1297 // ── Edge cases ───────────────────────────────────────────────────────
1298
1299 #[test]
1300 fn test_context_size_zero() {
1301 let mut ctx = XPathContext::new(std::ptr::null_mut());
1302 ctx.set_context_list(vec![]);
1303 assert_eq!(ctx.last(), 0);
1304 assert_eq!(ctx.position(), 1);
1305 }
1306
1307 #[test]
1308 fn test_multiple_advancements() {
1309 let mut ctx = XPathContext::new(std::ptr::null_mut());
1310 for i in 1..=10 {
1311 assert_eq!(ctx.position(), i);
1312 ctx.advance_position();
1313 }
1314 assert_eq!(ctx.position(), 11);
1315 }
1316
1317 #[test]
1318 fn test_register_multiple_variables() {
1319 let mut ctx = XPathContext::new(std::ptr::null_mut());
1320 ctx.register_variable("a", XPathValue::Number(1.0));
1321 ctx.register_variable("b", XPathValue::String("two".to_string()));
1322 ctx.register_variable("c", XPathValue::Boolean(true));
1323
1324 assert_eq!(ctx.variables.len(), 3);
1325 assert_eq!(ctx.resolve_variable("a").unwrap().as_number(), 1.0);
1326 assert_eq!(ctx.resolve_variable("b").unwrap().as_string(), "two");
1327 assert!(ctx.resolve_variable("c").unwrap().as_boolean());
1328 }
1329
1330 #[test]
1331 fn test_register_multiple_namespaces() {
1332 let mut ctx = XPathContext::new(std::ptr::null_mut());
1333 ctx.register_namespace("a", "http://example.com/a");
1334 ctx.register_namespace("b", "http://example.com/b");
1335 ctx.register_namespace("c", "http://example.com/c");
1336
1337 assert_eq!(ctx.namespaces.len(), 3);
1338 assert_eq!(ctx.resolve_namespace("a").unwrap(), "http://example.com/a");
1339 assert_eq!(ctx.resolve_namespace("b").unwrap(), "http://example.com/b");
1340 assert_eq!(ctx.resolve_namespace("c").unwrap(), "http://example.com/c");
1341 }
1342
1343 #[test]
1344 fn test_register_multiple_functions() {
1345 fn f1(_: &mut XPathContext, _: &[XPathValue]) -> Result<XPathValue, String> {
1346 Ok(XPathValue::Number(1.0))
1347 }
1348 fn f2(_: &mut XPathContext, _: &[XPathValue]) -> Result<XPathValue, String> {
1349 Ok(XPathValue::Number(2.0))
1350 }
1351 fn f3(_: &mut XPathContext, _: &[XPathValue]) -> Result<XPathValue, String> {
1352 Ok(XPathValue::Number(3.0))
1353 }
1354
1355 let mut ctx = XPathContext::new(std::ptr::null_mut());
1356 ctx.register_function("f1", f1);
1357 ctx.register_function("f2", f2);
1358 ctx.register_function("f3", f3);
1359
1360 assert_eq!(ctx.functions.len(), 3);
1361 }
1362}