libxml/
xpath.rs

1//! The `XPath` functionality
2
3use crate::bindings::*;
4use crate::c_helpers::*;
5use crate::readonly::RoNode;
6use crate::tree::{Document, DocumentRef, DocumentWeak, Node};
7use libc::{c_char, c_void, size_t};
8use std::cell::RefCell;
9use std::ffi::{CStr, CString};
10use std::fmt;
11use std::rc::Rc;
12use std::str;
13
14///Thinly wrapped libxml2 xpath context
15pub(crate) type ContextRef = Rc<RefCell<_Context>>;
16
17#[derive(Debug)]
18pub(crate) struct _Context(pub(crate) xmlXPathContextPtr);
19
20impl Drop for _Context {
21  ///free xpath context when it goes out of scope
22  fn drop(&mut self) {
23    unsafe {
24      xmlXPathFreeContext(self.0);
25    }
26  }
27}
28
29/// An XPath context
30#[derive(Clone)]
31pub struct Context {
32  /// Safe reference to the libxml2 context pointer
33  pub(crate) context_ptr: ContextRef,
34  ///Document contains pointer, needed for ContextPtr, so we need to borrow Document to prevent it's freeing
35  pub(crate) document: DocumentWeak,
36}
37
38///Essentially, the result of the evaluation of some xpath expression
39#[derive(Debug)]
40pub struct Object {
41  ///libxml's `ObjectPtr`
42  pub ptr: xmlXPathObjectPtr,
43  document: DocumentWeak,
44}
45
46impl Context {
47  ///create the xpath context for a document
48  pub fn new(doc: &Document) -> Result<Context, ()> {
49    let ctxtptr = unsafe { xmlXPathNewContext(doc.doc_ptr()) };
50    if ctxtptr.is_null() {
51      Err(())
52    } else {
53      Ok(Context {
54        context_ptr: Rc::new(RefCell::new(_Context(ctxtptr))),
55        document: Rc::downgrade(&doc.0),
56      })
57    }
58  }
59  pub(crate) fn new_ptr(docref: &DocumentRef) -> Result<Context, ()> {
60    let ctxtptr = unsafe { xmlXPathNewContext(docref.borrow().doc_ptr) };
61    if ctxtptr.is_null() {
62      Err(())
63    } else {
64      Ok(Context {
65        context_ptr: Rc::new(RefCell::new(_Context(ctxtptr))),
66        document: Rc::downgrade(docref),
67      })
68    }
69  }
70
71  /// Returns the raw libxml2 context pointer behind the struct
72  pub fn as_ptr(&self) -> xmlXPathContextPtr {
73    self.context_ptr.borrow().0
74  }
75
76  /// Instantiate a new Context for the Document of a given Node.
77  /// Note: the Context is root-level for that document, use `.set_context_node` to limit scope to this node
78  pub fn from_node(node: &Node) -> Result<Context, ()> {
79    let docref = node.get_docref().upgrade().unwrap();
80    Context::new_ptr(&docref)
81  }
82
83  /// Register a namespace prefix-href pair on the xpath context
84  pub fn register_namespace(&self, prefix: &str, href: &str) -> Result<(), ()> {
85    let c_prefix = CString::new(prefix).unwrap();
86    let c_href = CString::new(href).unwrap();
87    unsafe {
88      let result = xmlXPathRegisterNs(
89        self.as_ptr(),
90        c_prefix.as_bytes().as_ptr(),
91        c_href.as_bytes().as_ptr(),
92      );
93      if result != 0 {
94        Err(())
95      } else {
96        Ok(())
97      }
98    }
99  }
100
101  ///evaluate an xpath
102  pub fn evaluate(&self, xpath: &str) -> Result<Object, ()> {
103    let c_xpath = CString::new(xpath).unwrap();
104    let ptr = unsafe { xmlXPathEvalExpression(c_xpath.as_bytes().as_ptr(), self.as_ptr()) };
105    if ptr.is_null() {
106      Err(())
107    } else {
108      Ok(Object {
109        ptr,
110        document: self.document.clone(),
111      })
112    }
113  }
114
115  ///evaluate an xpath on a context Node
116  pub fn node_evaluate(&self, xpath: &str, node: &Node) -> Result<Object, ()> {
117    let c_xpath = CString::new(xpath).unwrap();
118    let ptr =
119      unsafe { xmlXPathNodeEval(node.node_ptr(), c_xpath.as_bytes().as_ptr(), self.as_ptr()) };
120    if ptr.is_null() {
121      Err(())
122    } else {
123      Ok(Object {
124        ptr,
125        document: self.document.clone(),
126      })
127    }
128  }
129
130  ///evaluate an xpath on a context RoNode
131  pub fn node_evaluate_readonly(&self, xpath: &str, node: RoNode) -> Result<Object, ()> {
132    let c_xpath = CString::new(xpath).unwrap();
133    let ptr = unsafe { xmlXPathNodeEval(node.0, c_xpath.as_bytes().as_ptr(), self.as_ptr()) };
134    if ptr.is_null() {
135      Err(())
136    } else {
137      Ok(Object {
138        ptr,
139        document: self.document.clone(),
140      })
141    }
142  }
143
144  /// localize xpath context to a specific Node
145  pub fn set_context_node(&mut self, node: &Node) -> Result<(), ()> {
146    unsafe {
147      let result = xmlXPathSetContextNode(node.node_ptr(), self.as_ptr());
148      if result != 0 {
149        return Err(());
150      }
151    }
152    Ok(())
153  }
154
155  /// find nodes via xpath, at a specified node or the document root
156  pub fn findnodes(&mut self, xpath: &str, node_opt: Option<&Node>) -> Result<Vec<Node>, ()> {
157    let evaluated = if let Some(node) = node_opt {
158      self.node_evaluate(xpath, node)?
159    } else {
160      self.evaluate(xpath)?
161    };
162    Ok(evaluated.get_nodes_as_vec())
163  }
164
165  /// find literal values via xpath, at a specified node or the document root
166  pub fn findvalues(&mut self, xpath: &str, node_opt: Option<&Node>) -> Result<Vec<String>, ()> {
167    let evaluated = if let Some(node) = node_opt {
168      self.node_evaluate(xpath, node)?
169    } else {
170      self.evaluate(xpath)?
171    };
172    Ok(evaluated.get_nodes_as_str())
173  }
174
175  /// find a literal value via xpath, at a specified node or the document root
176  pub fn findvalue(&mut self, xpath: &str, node_opt: Option<&Node>) -> Result<String, ()> {
177    let evaluated = if let Some(node) = node_opt {
178      self.node_evaluate(xpath, node)?
179    } else {
180      self.evaluate(xpath)?
181    };
182    Ok(evaluated.to_string())
183  }
184}
185
186impl Drop for Object {
187  /// free the memory allocated
188  fn drop(&mut self) {
189    unsafe {
190      xmlXPathFreeObject(self.ptr);
191    }
192  }
193}
194
195impl Object {
196  ///get the number of nodes in the result set
197  pub fn get_number_of_nodes(&self) -> usize {
198    let v = xmlXPathObjectNumberOfNodes(self.ptr);
199    if v == -1 {
200      panic!("rust-libxml: xpath: Passed in null pointer!");
201    }
202    if v == -2 {
203      // No nodes found!
204      return 0;
205    }
206    if v < -2 {
207      panic!("rust-libxml: xpath: expected non-negative number of result nodes");
208    }
209    v as usize
210  }
211
212  /// returns the result set as a vector of `Node` objects
213  pub fn get_nodes_as_vec(&self) -> Vec<Node> {
214    let n = self.get_number_of_nodes();
215    let mut vec: Vec<Node> = Vec::with_capacity(n);
216    let slice = if n > 0 {
217      xmlXPathObjectGetNodes(self.ptr, n as size_t)
218    } else {
219      Vec::new()
220    };
221    for ptr in slice {
222      if ptr.is_null() {
223        panic!("rust-libxml: xpath: found null pointer result set");
224      }
225      let node = Node::wrap(ptr, &self.document.upgrade().unwrap());
226      vec.push(node);
227    }
228    vec
229  }
230
231  /// returns the result set as a vector of `RoNode` objects
232  pub fn get_readonly_nodes_as_vec(&self) -> Vec<RoNode> {
233    let n = self.get_number_of_nodes();
234    let mut vec: Vec<RoNode> = Vec::with_capacity(n);
235    let slice = if n > 0 {
236      xmlXPathObjectGetNodes(self.ptr, n as size_t)
237    } else {
238      Vec::new()
239    };
240    for ptr in slice {
241      if ptr.is_null() {
242        panic!("rust-libxml: xpath: found null pointer result set");
243      }
244      vec.push(RoNode(ptr));
245    }
246    vec
247  }
248
249  /// returns the result set as a vector of Strings
250  pub fn get_nodes_as_str(&self) -> Vec<String> {
251    let n = self.get_number_of_nodes();
252    let mut vec: Vec<String> = Vec::with_capacity(n);
253    let slice = if n > 0 {
254      xmlXPathObjectGetNodes(self.ptr, n as size_t)
255    } else {
256      Vec::new()
257    };
258    for ptr in slice {
259      if ptr.is_null() {
260        panic!("rust-libxml: xpath: found null pointer result set");
261      }
262      let value_ptr = unsafe { xmlXPathCastNodeToString(ptr) };
263      let c_value_string = unsafe { CStr::from_ptr(value_ptr as *const c_char) };
264      let ready_str = c_value_string.to_string_lossy().into_owned();
265      bindgenFree(value_ptr as *mut c_void);
266      vec.push(ready_str);
267    }
268    vec
269  }
270
271}
272
273impl fmt::Display for Object {
274  /// use if the XPath used was meant to return a string, such as string(//foo/@attr)
275  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
276    unsafe {
277      let receiver = xmlXPathCastToString(self.ptr);
278      let c_string = CStr::from_ptr(receiver as *const c_char);
279      let rust_string = str::from_utf8(c_string.to_bytes()).unwrap().to_owned();
280      bindgenFree(receiver as *mut c_void);
281      write!(f, "{rust_string}")
282    }
283  }
284}
285
286/// Calls the binding to http://xmlsoft.org/html/libxml-xpath.html#xmlXPathCompile and return true if
287/// a non-null pointer is returned. The idea is to use this to validate an xpath independent of context.
288/// Tests describing what this validates in tests/xpath_tests.rs
289pub fn is_well_formed_xpath(xpath: &str) -> bool {
290  let c_xpath = CString::new(xpath).unwrap();
291  let xml_xpath_comp_expr_ptr = unsafe { xmlXPathCompile(c_xpath.as_bytes().as_ptr()) };
292  if xml_xpath_comp_expr_ptr.is_null() {
293    false
294  } else {
295    bindgenFree(xml_xpath_comp_expr_ptr as *mut c_void);
296    true
297  }
298}