1use std::borrow::Cow;
2
3use libxml::{
4 tree::{Document, Node, NodeType},
5 xpath::Context,
6};
7use rustc_hash::FxHashMap as HashMap;
8
9use crate::common::error::Result;
10
11pub const XMLNS_NS: &str = "http://www.w3.org/2000/xmlns/";
12pub const XML_NS: &str = "http://www.w3.org/XML/1998/namespace";
13
14pub fn parse_chunk(markup: &str) -> std::result::Result<Document, String> {
44 let options = libxml::parser::ParserOptions {
45 recover: false,
46 no_net: true,
47 no_def_dtd: true,
48 ..libxml::parser::ParserOptions::default()
49 };
50 libxml::parser::Parser::default()
51 .parse_string_with_options(markup, options)
52 .map_err(|e| match e {
53 libxml::parser::XmlParseError::DocumentTooLarge => String::from("markup too large to parse"),
54 _ => ill_formed_markup_hint(),
55 })
56}
57
58fn ill_formed_markup_hint() -> String {
73 String::from(
74 "not well-formed XML — check for an unclosed or mismatched tag, a bare `&` \
75 (write `&`), or an HTML entity such as ` ` that XML does not define \
76 (write the numeric form, ` `)",
77 )
78}
79
80const FRAGMENT_WRAPPER: &str = "_lxfragment";
83
84pub fn parse_fragment(markup: &str) -> std::result::Result<ParsedFragment, String> {
111 if let Ok(doc) = parse_chunk(markup)
113 && let Some(root) = doc.get_root_element()
114 {
115 return Ok(ParsedFragment { doc, nodes: vec![root] });
116 }
117 let doc = parse_chunk(&format!(
120 "<{FRAGMENT_WRAPPER}>{markup}</{FRAGMENT_WRAPPER}>"
121 ))?;
122 let root = doc
123 .get_root_element()
124 .ok_or_else(|| String::from("markup parsed to an empty document"))?;
125 let nodes = root.get_child_nodes();
126 Ok(ParsedFragment { doc, nodes })
127}
128
129pub fn is_parse_artifact(node: &Node) -> bool {
141 match node.get_type() {
142 Some(NodeType::DocumentNode) => true,
143 Some(NodeType::ElementNode) => node.get_name() == FRAGMENT_WRAPPER,
144 _ => false,
145 }
146}
147
148#[derive(Clone)]
158pub struct ParsedFragment {
159 doc: Document,
161 nodes: Vec<Node>,
162}
163
164impl std::fmt::Debug for ParsedFragment {
168 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
169 f.debug_struct("ParsedFragment")
170 .field("nodes", &self.nodes.len())
171 .field(
172 "names",
173 &self.nodes.iter().map(Node::get_name).collect::<Vec<_>>(),
174 )
175 .finish()
176 }
177}
178
179impl ParsedFragment {
180 pub fn nodes(&self) -> Vec<Node> { self.nodes.clone() }
182 pub fn len(&self) -> usize { self.nodes.len() }
184 pub fn is_empty(&self) -> bool { self.nodes.is_empty() }
185 pub fn document(&self) -> &Document { &self.doc }
187}
188
189pub struct XPath {
190 context: Context,
191}
192
193impl XPath {
195 pub fn new(doc: &Document, _mappings: HashMap<String, String>) -> Self {
196 let context = Context::new(doc).unwrap();
197 XPath { context }
198 }
199
200 pub fn register_namespace(&mut self, codeprefix: &str, namespace: &str) -> Result<()> {
201 match self.context.register_namespace(codeprefix, namespace) {
202 Ok(()) => {},
203 Err(_) => {
204 let message = s!(
205 "Failed to register an XPath namespace: prefix {:?} and href {:?}",
206 codeprefix,
207 namespace
208 );
209 Error!("expected", "XPath", message);
210 },
211 };
212 Ok(())
213 }
214
215 pub fn findnodes(&mut self, xpath: &str, node: Option<&Node>) -> Vec<Node> {
216 match self.context.findnodes(xpath, node) {
217 Ok(nodes) => nodes,
218 Err(e) => {
219 let message = s!(
220 "XPath {xpath:?} failed (context node: {}): {e:?}",
221 node.is_some()
222 );
223 let err = || {
224 Error!("xpath", "findnodes", message);
225 Ok(())
226 };
227 err().ok();
228 Vec::new()
234 },
235 }
236 }
237
238 pub fn findvalues(&mut self, xpath: &str, node: Option<&Node>) -> Vec<String> {
239 match self.context.findvalues(xpath, node) {
240 Ok(vals) => vals,
241 Err(e) => {
242 let message = s!(
243 "XPath {xpath:?} failed (context node: {}): {e:?}",
244 node.is_some()
245 );
246 let err = || {
247 Error!("xpath", "findvalues", message);
248 Ok(())
249 };
250 err().ok();
251 Vec::new()
252 },
253 }
254 }
255
256 pub fn findvalue(&mut self, xpath: &str, node: Option<&Node>) -> String {
257 self.context.findvalue(xpath, node).unwrap_or_default()
258 }
259}
260
261pub fn get_next_element(node_in: &Node) -> Option<Node> {
265 let mut node = Cow::Borrowed(node_in);
266 while let Some(next) = node.get_next_sibling() {
267 if next.get_type() == Some(NodeType::ElementNode) {
268 return Some(next);
269 } else {
270 node = Cow::Owned(next);
271 }
272 }
273 None
274}
275pub fn get_prev_element(node_in: &Node) -> Option<Node> {
277 let mut node = Cow::Borrowed(node_in);
278 while let Some(next) = node.get_prev_sibling() {
279 if next.get_type() == Some(NodeType::ElementNode) {
280 return Some(next);
281 } else {
282 node = Cow::Owned(next);
283 }
284 }
285 None
286}
287pub fn detached_root(node: &Node) -> Option<Node> {
294 let mut cur = node.clone();
295 loop {
296 match cur.get_parent() {
297 None => return Some(cur),
298 Some(p)
299 if matches!(
300 p.get_type(),
301 Some(NodeType::DocumentNode) | Some(NodeType::DocumentFragNode)
302 ) =>
303 {
304 return None;
305 },
306 Some(p) => cur = p,
307 }
308 }
309}
310
311pub fn element_nodes(node: &Node) -> Vec<Node> {
313 node
314 .get_child_nodes()
315 .into_iter()
316 .filter(|n| matches!(n.get_type(), Some(NodeType::ElementNode)))
317 .collect()
318}
319
320pub fn content_nodes(node: &Node) -> Vec<Node> {
322 node
323 .get_child_nodes()
324 .into_iter()
325 .filter(|n| {
326 matches!(
327 n.get_type(),
328 Some(NodeType::ElementNode) | Some(NodeType::TextNode)
329 )
330 })
331 .collect()
332}
333
334pub fn closest_element(node: &Node) -> Option<Node> {
335 if node.get_type() == Some(NodeType::ElementNode) {
336 return Some(node.clone());
337 }
338 let mut current = node.clone();
344 while let Some(parent) = current.get_parent() {
345 if parent.get_type() == Some(NodeType::ElementNode) {
346 return Some(parent);
347 }
348 current = parent;
349 }
350 None
351}
352
353pub fn is_descendant_or_self(child: &Node, parent: &Node) -> bool {
355 let mut p = Some(child);
356 let mut parent_opt;
357 while let Some(p_node) = p {
358 if p_node == parent {
360 return true;
361 }
362 match p_node.get_parent() {
363 Some(parent_node) => {
364 parent_opt = Some(parent_node);
365 p = parent_opt.as_ref();
366 },
367 _ => {
368 break;
369 },
370 }
371 }
372 false
373}
374
375#[cfg(test)]
376mod tests {
377 use libxml::tree::Document;
378
379 use super::*;
380
381 #[test]
382 fn namespace_constants() {
383 assert_eq!(XML_NS, "http://www.w3.org/XML/1998/namespace");
384 assert_eq!(XMLNS_NS, "http://www.w3.org/2000/xmlns/");
385 }
386
387 #[test]
393 fn closest_element_terminates_without_an_element_ancestor() {
394 let doc = Document::new().unwrap();
395 let mut doc_node = doc.as_node();
396 let mut stray = Node::new_text("stray", &doc).unwrap();
397 doc_node.add_child(&mut stray).unwrap();
398
399 assert_eq!(stray.get_type(), Some(NodeType::TextNode));
400 assert_ne!(
401 doc_node.get_type(),
402 Some(NodeType::ElementNode),
403 "the parent must be a non-element for this to exercise the walk"
404 );
405 assert!(
406 closest_element(&stray).is_none(),
407 "no element ancestor exists, so the walk must end at the document"
408 );
409 }
410
411 fn build_tree() -> (Document, Node) {
412 let mut doc = Document::new().unwrap();
413 let mut root = Node::new("root", None, &doc).unwrap();
414 doc.set_root_element(&root);
415 let mut a = Node::new("a", None, &doc).unwrap();
418 let mut t1 = Node::new_text("text1", &doc).unwrap();
419 let mut b = Node::new("b", None, &doc).unwrap();
420 let mut t2 = Node::new_text("text2", &doc).unwrap();
421 let mut c = Node::new("c", None, &doc).unwrap();
422 root.add_child(&mut a).unwrap();
423 root.add_child(&mut t1).unwrap();
424 root.add_child(&mut b).unwrap();
425 root.add_child(&mut t2).unwrap();
426 root.add_child(&mut c).unwrap();
427 (doc, root)
428 }
429
430 #[test]
431 fn element_nodes_skips_text() {
432 let (_doc, root) = build_tree();
433 let children = element_nodes(&root);
434 assert_eq!(children.len(), 3);
435 assert_eq!(children[0].get_name(), "a");
436 assert_eq!(children[1].get_name(), "b");
437 assert_eq!(children[2].get_name(), "c");
438 }
439
440 #[test]
441 fn content_nodes_includes_text() {
442 let (_doc, root) = build_tree();
443 let children = content_nodes(&root);
444 assert_eq!(children.len(), 5, "3 elements + 2 text nodes");
445 }
446
447 #[test]
448 fn get_next_element_skips_text() {
449 let (_doc, root) = build_tree();
450 let a = element_nodes(&root)[0].clone();
451 let next = get_next_element(&a).expect("a has a next element");
452 assert_eq!(
453 next.get_name(),
454 "b",
455 "<a> next element must be <b>, skipping the text node"
456 );
457 }
458
459 #[test]
460 fn get_next_element_none_at_end() {
461 let (_doc, root) = build_tree();
462 let c = element_nodes(&root)[2].clone();
463 assert!(get_next_element(&c).is_none(), "last element has no next");
464 }
465
466 #[test]
467 fn get_prev_element_skips_text() {
468 let (_doc, root) = build_tree();
469 let b = element_nodes(&root)[1].clone();
470 let prev = get_prev_element(&b).expect("b has a prev element");
471 assert_eq!(
472 prev.get_name(),
473 "a",
474 "<b> prev element must be <a>, skipping text"
475 );
476 }
477
478 #[test]
479 fn get_prev_element_none_at_start() {
480 let (_doc, root) = build_tree();
481 let a = element_nodes(&root)[0].clone();
482 assert!(get_prev_element(&a).is_none(), "first element has no prev");
483 }
484
485 #[test]
486 fn is_descendant_or_self_true_for_self() {
487 let (_doc, root) = build_tree();
488 assert!(is_descendant_or_self(&root, &root));
489 }
490
491 #[test]
492 fn is_descendant_or_self_true_for_child() {
493 let (_doc, root) = build_tree();
494 let a = element_nodes(&root)[0].clone();
495 assert!(is_descendant_or_self(&a, &root));
496 }
497
498 #[test]
499 fn is_descendant_or_self_false_for_sibling() {
500 let (_doc, root) = build_tree();
501 let kids = element_nodes(&root);
502 assert!(
503 !is_descendant_or_self(&kids[0], &kids[1]),
504 "a is not a descendant of b"
505 );
506 }
507}
508
509#[cfg(test)]
510mod parse_chunk_tests {
511 use super::*;
515
516 #[test]
517 fn a_single_well_formed_root_parses() {
518 let doc = parse_chunk(r#"<p xmlns="http://www.w3.org/1999/xhtml">hi <b>bold</b></p>"#)
519 .expect("single-root xhtml should parse");
520 let root = doc.get_root_element().expect("parsed chunk has a root");
521 assert_eq!(root.get_name(), "p");
522 assert_eq!(root.get_attribute("class"), None);
523 }
524
525 #[test]
526 fn a_multi_root_fragment_is_rejected() {
527 assert!(parse_chunk("<b>a</b> <i>b</i>").is_err());
531 assert!(parse_chunk("bare text").is_err());
532 assert!(parse_chunk("").is_err());
533 }
534
535 #[test]
536 fn an_undefined_html_entity_is_rejected_not_crashed() {
537 assert!(parse_chunk("<p>a b</p>").is_err());
542 assert!(parse_chunk("<p>a b</p>").is_ok());
544 }
545}
546
547#[cfg(test)]
548mod parse_fragment_tests {
549 use super::*;
552
553 #[test]
554 fn a_single_root_still_yields_exactly_one_node() {
555 let f = parse_fragment(r#"<p xmlns="http://www.w3.org/1999/xhtml">hi</p>"#).unwrap();
556 assert_eq!(f.len(), 1);
557 assert_eq!(f.nodes()[0].get_name(), "p");
558 }
559
560 #[test]
561 fn sibling_roots_are_kept_whole() {
562 let f = parse_fragment("<b>a</b><i>b</i>").unwrap();
565 assert_eq!(f.len(), 2, "both siblings must survive");
566 assert_eq!(f.nodes()[0].get_name(), "b");
567 assert_eq!(f.nodes()[1].get_name(), "i");
568 }
569
570 #[test]
571 fn bare_text_is_a_legitimate_fragment() {
572 let f = parse_fragment("just text").unwrap();
573 assert_eq!(f.len(), 1);
574 assert_eq!(f.nodes()[0].get_content(), "just text");
575 }
576
577 #[test]
578 fn malformed_markup_is_still_rejected_not_salvaged() {
579 assert!(parse_fragment("<p>unclosed").is_err(), "unclosed element");
582 assert!(parse_fragment("<p>a & b</p>").is_err(), "bare ampersand");
583 assert!(
584 parse_fragment("<p>a b</p>").is_err(),
585 "undeclared entity"
586 );
587 }
588
589 #[test]
590 fn empty_markup_parses_to_no_nodes_rather_than_failing() {
591 let f = parse_fragment("").expect("empty markup is not a parse failure");
596 assert!(f.is_empty());
597 assert_eq!(f.len(), 0);
598 }
599
600 #[test]
601 fn a_rejection_says_what_to_look_for_not_got_a_null_pointer() {
602 for markup in ["<p>unclosed", "<p>a & b</p>", "<p>a b</p>"] {
607 let err = parse_fragment(markup).expect_err("markup should be rejected");
608 assert!(
609 !err.to_lowercase().contains("null pointer"),
610 "leaked the useless libxml error for {markup:?}: {err}"
611 );
612 assert!(
613 err.contains("unclosed") && err.contains("&") && err.contains(" "),
614 "rejection must point at the likely causes for {markup:?}: {err}"
615 );
616 }
617 }
618
619 #[test]
620 fn the_wrapper_never_enters_the_result() {
621 let f = parse_fragment("<b>a</b><i>b</i>").unwrap();
622 assert!(
623 !f.nodes().iter().any(|n| n.get_name() == FRAGMENT_WRAPPER),
624 "the throwaway wrapper must not be handed back"
625 );
626 }
627}