1#![forbid(unsafe_code)]
2
3use std::collections::HashSet;
33use std::sync::Arc;
34
35use sup_xml_tree::dom::{Document, DocumentBuilder, Node, NodeKind};
36
37use crate::entity_resolver::{EntityResolver, ResolveError};
38use crate::error::{ErrorDomain, ErrorLevel, Result, XmlError};
39use crate::options::ParseOptions;
40use crate::parser::parse_str;
41
42pub const XINCLUDE_NS: &str = "http://www.w3.org/2001/XInclude";
44
45#[derive(Clone)]
47pub struct XIncludeOptions {
48 pub resolver: Option<Arc<dyn EntityResolver>>,
52 pub max_depth: u32,
56 pub max_total_bytes: u64,
60}
61
62impl Default for XIncludeOptions {
63 fn default() -> Self {
64 Self::new()
65 }
66}
67
68impl XIncludeOptions {
69 pub fn new() -> Self {
73 Self {
74 resolver: None,
75 max_depth: 16,
76 max_total_bytes: 10 * 1024 * 1024,
77 }
78 }
79}
80
81pub fn process_xincludes(
100 doc: &Document,
101 opts: &XIncludeOptions,
102) -> Result<Document> {
103 let b = DocumentBuilder::new();
104 let mut state = State {
105 opts: opts.clone(),
106 hrefs: HashSet::new(),
107 depth: 0,
108 total_bytes: 0,
109 };
110
111 b.set_version(doc.version.clone());
113 b.set_encoding(doc.encoding.clone());
114 b.set_standalone(doc.standalone);
115
116 let src_root = doc.root();
117 if src_root.kind == NodeKind::Element && is_xinclude_element_named(src_root.name()) {
124 return Err(validation_err(
125 "xi:include is not allowed as the document root element",
126 ));
127 }
128
129 let dst_root = copy_subtree(&b, src_root, &mut state)?;
130 b.set_root(dst_root);
131 Ok(b.build())
132}
133
134struct State {
137 opts: XIncludeOptions,
138 hrefs: HashSet<String>,
141 depth: u32,
142 total_bytes: u64,
143}
144
145fn copy_subtree<'a>(
156 b: &'a DocumentBuilder,
157 src: &Node<'_>,
158 state: &mut State,
159) -> Result<&'a Node<'a>> {
160 match src.kind {
161 NodeKind::Element => {
162 let name = b.alloc_str(src.name());
163 let el = b.new_element(name);
164 for attr in src.attributes() {
165 let aname = b.alloc_str(attr.name());
166 let aval = b.alloc_str(attr.value());
167 let new_attr = b.new_attribute(aname, aval);
168 b.append_attribute(el, new_attr);
169 }
170 copy_children_into(b, el, src, state)?;
171 Ok(el)
172 }
173 NodeKind::Text => {
174 let content = b.alloc_str(src.content());
175 Ok(b.new_text(content))
176 }
177 NodeKind::CData => {
178 let content = b.alloc_str(src.content());
179 Ok(b.new_cdata(content))
180 }
181 NodeKind::Comment => {
182 let content = b.alloc_str(src.content());
183 Ok(b.new_comment(content))
184 }
185 NodeKind::Pi => {
186 let target = b.alloc_str(src.name());
187 let content = src.content_opt().map(|c| &*b.alloc_str(c));
188 Ok(b.new_pi(target, content))
189 }
190 NodeKind::EntityRef => {
191 let name = b.alloc_str(src.name());
195 let content = b.alloc_str(src.content());
196 Ok(b.new_entity_ref(name, content))
197 }
198 NodeKind::Attribute => unreachable!("Attribute kind never appears on a Node"),
200 NodeKind::Document => unreachable!("Document kind never appears on a Node"),
201 NodeKind::DocumentFragment => unreachable!(
202 "DocumentFragment is a compat-shim transient; XInclude does not walk into one"
203 ),
204 NodeKind::DtdDecl => unreachable!(
205 "DtdDecl is an internal-subset child; XInclude copies element subtrees only"
206 ),
207 NodeKind::Dtd => unreachable!(
208 "Dtd is a document-level internal-subset node; XInclude copies element subtrees only"
209 ),
210 }
211}
212
213fn copy_children_into<'a>(
216 b: &'a DocumentBuilder,
217 dst_parent: &'a Node<'a>,
218 src_parent: &Node<'_>,
219 state: &mut State,
220) -> Result<()> {
221 for child in src_parent.children() {
222 if child.kind == NodeKind::Element && is_xinclude_element(child) {
223 let replacements = resolve_include(b, child, state)?;
225 for n in replacements {
226 b.append_child(dst_parent, n);
227 }
228 } else {
229 let new_child = copy_subtree(b, child, state)?;
230 b.append_child(dst_parent, new_child);
231 }
232 }
233 Ok(())
234}
235
236fn is_xinclude_element(elem: &Node<'_>) -> bool {
239 if let Some(ns) = elem.namespace.get() {
241 if ns.href() == XINCLUDE_NS {
242 let local = local_name(elem.name());
243 return local == "include";
244 }
245 }
246 is_xinclude_element_named_with_attrs(elem)
249}
250
251fn is_xinclude_element_named(name: &str) -> bool {
254 name == "xi:include"
255}
256
257fn is_xinclude_element_named_with_attrs(elem: &Node<'_>) -> bool {
261 let name = elem.name();
262 let local = local_name(name);
263 if local != "include" {
264 return false;
265 }
266 if elem.attributes().any(|a| {
272 (a.name() == "xmlns" || a.name().starts_with("xmlns:"))
273 && a.value() == XINCLUDE_NS
274 }) {
275 return true;
276 }
277 name == "xi:include"
281}
282
283fn is_xi_fallback(elem: &Node<'_>) -> bool {
284 let local = local_name(elem.name());
285 local == "fallback"
286}
287
288#[inline]
289fn local_name(qname: &str) -> &str {
290 qname.rsplit_once(':').map(|(_, l)| l).unwrap_or(qname)
291}
292
293#[derive(Default)]
296struct XiAttrs {
297 href: Option<String>,
298 parse: XiParseMode,
299 xpointer: Option<String>,
305}
306
307#[derive(Default, Clone, Copy, PartialEq, Eq)]
308enum XiParseMode {
309 #[default]
310 Xml,
311 Text,
312}
313
314fn parse_xi_include_attrs(elem: &Node<'_>) -> Result<XiAttrs> {
315 let mut out = XiAttrs::default();
316 for attr in elem.attributes() {
317 let local = local_name(attr.name());
318 match local {
319 "href" => out.href = Some(attr.value().to_string()),
320 "parse" => {
321 out.parse = match attr.value() {
322 "xml" => XiParseMode::Xml,
323 "text" => XiParseMode::Text,
324 other => {
325 return Err(validation_err(format!(
326 "xi:include parse={other:?} is not supported \
327 (only \"xml\" and \"text\")"
328 )))
329 }
330 };
331 }
332 "xpointer" => {
333 out.xpointer = Some(attr.value().to_string());
334 }
335 _ => {}
338 }
339 }
340 Ok(out)
341}
342
343fn resolve_include<'a>(
351 b: &'a DocumentBuilder,
352 include_elem: &Node<'_>,
353 state: &mut State,
354) -> Result<Vec<&'a Node<'a>>> {
355 state.depth += 1;
356 if state.depth > state.opts.max_depth {
357 state.depth -= 1;
358 return Err(validation_err(format!(
359 "XInclude depth limit ({}) exceeded — possible recursion",
360 state.opts.max_depth
361 )));
362 }
363
364 let primary = resolve_include_inner(b, include_elem, state);
365 state.depth -= 1;
366
367 match primary {
368 Ok(nodes) => Ok(nodes),
369 Err(primary_err) => {
370 let fallback_elem = include_elem.children().find(|c| {
372 c.kind == NodeKind::Element && is_xi_fallback(c)
373 });
374 match fallback_elem {
375 Some(fb) => {
376 let tmp = b.new_element(b.alloc_str("__xi_fallback_tmp__"));
384 copy_children_into(b, tmp, fb, state)?;
385 let mut out = Vec::new();
392 let mut cur = tmp.first_child.get();
393 while let Some(c) = cur {
394 let next = c.next_sibling.get();
395 b.detach(c);
396 out.push(c);
397 cur = next;
398 }
399 Ok(out)
400 }
401 None => Err(primary_err),
402 }
403 }
404 }
405}
406
407fn resolve_include_inner<'a>(
408 b: &'a DocumentBuilder,
409 include_elem: &Node<'_>,
410 state: &mut State,
411) -> Result<Vec<&'a Node<'a>>> {
412 let attrs = parse_xi_include_attrs(include_elem)?;
413
414 let href = attrs.href.ok_or_else(|| {
415 validation_err(
416 "xi:include without href is not supported in v1 (use \
417 href to reference an external resource)",
418 )
419 })?;
420
421 if state.hrefs.contains(&href) {
422 return Err(validation_err(format!(
423 "XInclude cycle detected: {href:?} is already in the \
424 include chain"
425 )));
426 }
427
428 let resolver = state
429 .opts
430 .resolver
431 .as_ref()
432 .cloned()
433 .ok_or_else(|| {
434 io_err(format!(
435 "xi:include {href:?} cannot be resolved — no \
436 resolver configured (set XIncludeOptions::resolver)"
437 ))
438 })?;
439
440 let bytes = resolver
441 .resolve(None, &href, None)
442 .map_err(|e| match e {
443 ResolveError::Refused(msg) => io_err(format!(
444 "xi:include {href:?} refused by resolver: {msg}"
445 )),
446 ResolveError::Io(io) => io_err(format!(
447 "xi:include {href:?} I/O error: {io}"
448 )),
449 ResolveError::Other(other) => io_err(format!(
450 "xi:include {href:?} resolver error: {other}"
451 )),
452 })?;
453
454 let added = bytes.len() as u64;
455 if state.total_bytes.saturating_add(added) > state.opts.max_total_bytes {
456 return Err(validation_err(format!(
457 "XInclude byte budget ({}) exceeded by {href:?}",
458 state.opts.max_total_bytes
459 )));
460 }
461 state.total_bytes += added;
462
463 match attrs.parse {
464 XiParseMode::Xml => {
465 let text = std::str::from_utf8(&bytes).map_err(|e| {
466 XmlError::new(
467 ErrorDomain::Encoding,
468 ErrorLevel::Fatal,
469 format!("xi:include {href:?} bytes are not UTF-8: {e}"),
470 )
471 })?;
472 let sub_doc = parse_str(text, &ParseOptions::default())?;
476
477 state.hrefs.insert(href.clone());
478
479 let result: Result<Vec<&Node<'_>>> = (|| {
485 let targets: Vec<&Node<'_>> = match &attrs.xpointer {
486 Some(xp) => resolve_xpointer(&sub_doc, xp, &href)?,
487 None => vec![sub_doc.root()],
488 };
489 let mut out: Vec<&Node<'_>> = Vec::with_capacity(targets.len());
490 for tgt in targets {
491 if is_xinclude_element(tgt) {
497 out.extend(resolve_include(b, tgt, state)?);
498 } else {
499 out.push(copy_subtree(b, tgt, state)?);
500 }
501 }
502 Ok(out)
503 })();
504
505 state.hrefs.remove(&href);
506 result
507 }
508 XiParseMode::Text => {
509 let text = String::from_utf8_lossy(&bytes).into_owned();
510 let alloc = b.alloc_str(&text);
511 Ok(vec![b.new_text(alloc)])
512 }
513 }
514}
515
516pub fn resolve_xpointer<'a>(
540 sub_doc: &'a sup_xml_tree::dom::Document,
541 expr: &str,
542 href: &str,
543) -> Result<Vec<&'a Node<'a>>> {
544 let bad = |msg: String| validation_err(format!(
545 "xi:include xpointer={expr:?} ({href}): {msg}"
546 ));
547
548 if let Some(rest) = expr.strip_prefix("xpointer(") {
550 let inner = rest.strip_suffix(')').ok_or_else(|| {
551 bad("missing closing `)` after xpointer scheme".to_string())
552 })?;
553 let result = crate::xpath::xpath_eval(sub_doc, inner)
554 .map_err(|e| bad(format!("XPath evaluation failed: {e}")))?;
555 let ids = match result {
556 crate::xpath::XPathValue::NodeSet(ns) if !ns.is_empty() => ns,
557 crate::xpath::XPathValue::NodeSet(_) => {
558 return Err(bad("XPath returned an empty nodeset".to_string()));
559 }
560 _ => return Err(bad(
561 "XPath must return a nodeset for xi:include splicing".to_string()
562 )),
563 };
564 let idx = crate::xpath::context::DocIndex::build(sub_doc);
569 let mut out: Vec<&Node<'_>> = Vec::with_capacity(ids.len());
570 for id in ids {
571 if let Some(n) = node_id_to_arena(&idx, id) {
572 out.push(n);
573 }
574 }
575 if out.is_empty() {
576 return Err(bad(
577 "XPath matched only non-element nodes (text, attributes, \
578 etc.) — XInclude can only splice element / document \
579 subtrees".to_string()
580 ));
581 }
582 return Ok(out);
583 }
584
585 if let Some(rest) = expr.strip_prefix("element(") {
587 let inner = rest.strip_suffix(')').ok_or_else(|| {
588 bad("missing closing `)` after element scheme".to_string())
589 })?;
590 let trimmed = inner.trim();
594 let (start, seq): (Option<&str>, &str) = if trimmed.starts_with('/') {
595 (None, trimmed)
596 } else {
597 match trimmed.find('/') {
599 Some(slash) => (Some(&trimmed[..slash]), &trimmed[slash..]),
600 None => (Some(trimmed), ""),
601 }
602 };
603 let (anchor, seq_after_root): (&Node<'_>, &str) = match start {
610 None => {
611 let after_slash = seq.trim_start_matches('/');
614 let (first, rest) = match after_slash.find('/') {
615 Some(i) => (&after_slash[..i], &after_slash[i + 1..]),
616 None => (after_slash, ""),
617 };
618 if !first.is_empty() {
619 let n: usize = first.parse().map_err(|_| {
620 bad(format!("ChildSequence root step {first:?} is not a positive integer"))
621 })?;
622 if n != 1 {
623 return Err(bad(format!(
624 "ChildSequence root step must be 1 (only one root \
625 element exists); got {n}"
626 )));
627 }
628 }
629 (sub_doc.root(), rest)
630 }
631 Some(name) => {
632 let n = find_by_id(sub_doc, name).ok_or_else(|| {
633 bad(format!("no element with id={name:?}"))
634 })?;
635 (n, seq.trim_start_matches('/'))
636 }
637 };
638 let target = walk_child_sequence(anchor, seq_after_root, &bad)?;
639 return Ok(vec![target]);
640 }
641
642 if !expr.contains('(') && !expr.contains('/') && !expr.is_empty() {
644 let n = find_by_id(sub_doc, expr).ok_or_else(|| {
645 bad(format!("no element with id={expr:?}"))
646 })?;
647 return Ok(vec![n]);
648 }
649
650 Err(bad(format!(
651 "unrecognized xpointer form — supported: \
652 `xpointer(EXPR)`, `element(/N/...)`, `element(NAME[/N/...])`, \
653 or bare `NAME`"
654 )))
655}
656
657fn walk_child_sequence<'a, F>(
663 anchor: &'a Node<'a>,
664 seq: &str,
665 bad: &F,
666) -> Result<&'a Node<'a>>
667where
668 F: Fn(String) -> XmlError,
669{
670 let mut current = anchor;
671 let mut remaining = seq.trim_start_matches('/');
672 while !remaining.is_empty() {
673 let (head, tail) = match remaining.find('/') {
674 Some(i) => (&remaining[..i], &remaining[i + 1..]),
675 None => (remaining, ""),
676 };
677 let n: usize = head.parse().map_err(|_| {
678 bad(format!("ChildSequence step {head:?} is not a positive integer"))
679 })?;
680 if n == 0 {
681 return Err(bad(
682 "ChildSequence steps are 1-based; got 0".to_string(),
683 ));
684 }
685 let mut next_node: Option<&Node<'_>> = None;
686 let mut seen = 0usize;
687 for child in current.children() {
688 if child.is_element() {
689 seen += 1;
690 if seen == n {
691 next_node = Some(child);
692 break;
693 }
694 }
695 }
696 current = next_node.ok_or_else(|| {
697 bad(format!(
698 "ChildSequence step {n} out of range — only {seen} element \
699 child(ren) under <{}>",
700 current.name()
701 ))
702 })?;
703 remaining = tail;
704 }
705 Ok(current)
706}
707
708fn find_by_id<'a>(
713 doc: &'a sup_xml_tree::dom::Document,
714 id: &str,
715) -> Option<&'a Node<'a>> {
716 fn walk<'a>(n: &'a Node<'a>, id: &str) -> Option<&'a Node<'a>> {
717 if n.is_element() {
718 for a in n.attributes() {
719 let nm = a.name();
720 if (nm == "id" || nm == "xml:id" || nm.ends_with(":id"))
721 && a.value() == id
722 {
723 return Some(n);
724 }
725 }
726 }
727 for c in n.children() {
728 if let Some(found) = walk(c, id) {
729 return Some(found);
730 }
731 }
732 None
733 }
734 walk(doc.root(), id)
735}
736
737fn node_id_to_arena<'doc>(
741 idx: &crate::xpath::context::DocIndex<'doc>,
742 id: crate::xpath::NodeId,
743) -> Option<&'doc Node<'doc>> {
744 use crate::xpath::context::INodeKind;
745 match &idx.nodes.get(id)?.kind {
746 INodeKind::Element(n) => Some(n),
747 _ => None,
750 }
751}
752
753fn io_err(msg: String) -> XmlError {
756 XmlError::new(ErrorDomain::Io, ErrorLevel::Fatal, msg)
757}
758
759fn validation_err(msg: impl Into<String>) -> XmlError {
760 XmlError::new(ErrorDomain::Validation, ErrorLevel::Fatal, msg)
761}
762
763#[cfg(test)]
766mod tests {
767 use super::*;
768 use crate::entity_resolver::InMemoryResolver;
769 use std::collections::HashMap;
770 use std::sync::Arc;
771
772 fn opts_with_resolver(map: HashMap<String, Vec<u8>>) -> XIncludeOptions {
773 let mut r = InMemoryResolver::new();
774 for (sys, bytes) in map {
775 r = r.with_system(&sys, bytes);
776 }
777 XIncludeOptions {
778 resolver: Some(Arc::new(r)),
779 ..XIncludeOptions::new()
780 }
781 }
782
783 fn parse(xml: &str) -> Document {
784 parse_str(xml, &ParseOptions::default()).expect("parse")
785 }
786
787 #[test]
788 fn xinclude_xml_replaces_include_with_referenced_subtree() {
789 let mut docs = HashMap::new();
790 docs.insert("part.xml".to_string(), b"<chunk>hello</chunk>".to_vec());
791 let xml = r#"<root xmlns:xi="http://www.w3.org/2001/XInclude"><xi:include href="part.xml"/></root>"#;
792 let doc = parse(xml);
793 let out = process_xincludes(&doc, &opts_with_resolver(docs)).unwrap();
794 let root = out.root();
795 assert_eq!(root.name(), "root");
796 let kids: Vec<_> = root.children().collect();
798 assert_eq!(kids.len(), 1);
799 let chunk = kids[0];
800 assert_eq!(chunk.kind, NodeKind::Element);
801 assert_eq!(chunk.name(), "chunk");
802 assert_eq!(chunk.text_content(), Some("hello"));
803 }
804
805 #[test]
806 fn xinclude_text_includes_raw_bytes_as_text_node() {
807 let mut docs = HashMap::new();
808 docs.insert(
809 "readme.txt".to_string(),
810 b"raw <text> with & specials".to_vec(),
811 );
812 let xml = r#"<root xmlns:xi="http://www.w3.org/2001/XInclude"><xi:include href="readme.txt" parse="text"/></root>"#;
813 let doc = parse(xml);
814 let out = process_xincludes(&doc, &opts_with_resolver(docs)).unwrap();
815 let root = out.root();
816 let kids: Vec<_> = root.children().collect();
817 assert_eq!(kids.len(), 1);
818 assert_eq!(kids[0].kind, NodeKind::Text);
819 assert_eq!(kids[0].content(), "raw <text> with & specials");
820 }
821
822 #[test]
823 fn xinclude_fallback_used_when_resolve_fails() {
824 let xml = r#"<root xmlns:xi="http://www.w3.org/2001/XInclude">
825 <xi:include href="missing.xml">
826 <xi:fallback><default>fallback content</default></xi:fallback>
827 </xi:include>
828 </root>"#;
829 let doc = parse(xml);
830 let out = process_xincludes(&doc, &opts_with_resolver(HashMap::new()))
832 .unwrap();
833 let root = out.root();
834 let has_default = root.children().any(|c| {
835 c.kind == NodeKind::Element && c.name() == "default"
836 });
837 assert!(
838 has_default,
839 "fallback content should replace the failed include"
840 );
841 }
842
843 #[test]
844 fn xinclude_no_resolver_errors_on_include() {
845 let xml = r#"<root xmlns:xi="http://www.w3.org/2001/XInclude"><xi:include href="x.xml"/></root>"#;
846 let doc = parse(xml);
847 let opts = XIncludeOptions::new();
848 let err = process_xincludes(&doc, &opts).expect_err("no resolver");
849 assert!(err.message.contains("no resolver"), "got: {}", err.message);
850 }
851
852 #[test]
853 fn xinclude_recursive_processing() {
854 let mut docs = HashMap::new();
855 docs.insert(
856 "outer.xml".to_string(),
857 br#"<wrap xmlns:xi="http://www.w3.org/2001/XInclude"><xi:include href="inner.xml"/></wrap>"#.to_vec(),
858 );
859 docs.insert("inner.xml".to_string(), b"<leaf>x</leaf>".to_vec());
860 let xml = r#"<root xmlns:xi="http://www.w3.org/2001/XInclude"><xi:include href="outer.xml"/></root>"#;
861 let doc = parse(xml);
862 let out = process_xincludes(&doc, &opts_with_resolver(docs)).unwrap();
863 let root = out.root();
865 let wrap = root.children().next().unwrap();
866 assert_eq!(wrap.name(), "wrap");
867 let leaf = wrap.children().next().unwrap();
868 assert_eq!(leaf.name(), "leaf");
869 assert_eq!(leaf.text_content(), Some("x"));
870 }
871
872 #[test]
873 fn xinclude_cycle_detected() {
874 let mut docs = HashMap::new();
875 docs.insert(
876 "a.xml".to_string(),
877 br#"<a xmlns:xi="http://www.w3.org/2001/XInclude"><xi:include href="a.xml"/></a>"#.to_vec(),
878 );
879 let xml = r#"<root xmlns:xi="http://www.w3.org/2001/XInclude"><xi:include href="a.xml"/></root>"#;
880 let doc = parse(xml);
881 let err = process_xincludes(&doc, &opts_with_resolver(docs))
882 .expect_err("cycle");
883 assert!(
884 err.message.contains("cycle") || err.message.contains("depth"),
885 "got: {}",
886 err.message
887 );
888 }
889
890 #[test]
891 fn xinclude_max_depth_enforced() {
892 let mut docs = HashMap::new();
893 for (i, next) in [(1, 2), (2, 3), (3, 4), (4, 5), (5, 6)] {
894 docs.insert(
895 format!("d{i}.xml"),
896 format!(
897 r#"<l xmlns:xi="http://www.w3.org/2001/XInclude"><xi:include href="d{next}.xml"/></l>"#
898 )
899 .into_bytes(),
900 );
901 }
902 let xml = r#"<root xmlns:xi="http://www.w3.org/2001/XInclude"><xi:include href="d1.xml"/></root>"#;
903 let doc = parse(xml);
904 let opts = XIncludeOptions {
905 max_depth: 2,
906 ..opts_with_resolver(docs)
907 };
908 let err = process_xincludes(&doc, &opts).expect_err("depth limit");
909 assert!(err.message.contains("depth"), "got: {}", err.message);
910 }
911
912 #[test]
913 fn xinclude_no_xi_elements_is_noop() {
914 let xml = "<r><a/><b/></r>";
915 let doc = parse(xml);
916 let out = process_xincludes(&doc, &XIncludeOptions::new()).unwrap();
917 let root = out.root();
919 assert_eq!(root.name(), "r");
920 let kids: Vec<&str> = root.children().map(|c| c.name()).collect();
921 assert_eq!(kids, vec!["a", "b"]);
922 }
923
924 #[test]
925 fn xinclude_unsupported_parse_mode_errors() {
926 let xml = r#"<root xmlns:xi="http://www.w3.org/2001/XInclude"><xi:include href="x" parse="binary"/></root>"#;
927 let doc = parse(xml);
928 let err = process_xincludes(&doc, &opts_with_resolver(HashMap::new()))
929 .expect_err("bad parse mode");
930 assert!(err.message.contains("parse"), "got: {}", err.message);
931 }
932
933 #[test]
936 fn xinclude_preserves_surrounding_siblings() {
937 let mut docs = HashMap::new();
939 docs.insert("p.xml".to_string(), b"<inc/>".to_vec());
940 let xml = r#"<root xmlns:xi="http://www.w3.org/2001/XInclude">
941 <before/>
942 <xi:include href="p.xml"/>
943 <after/>
944 </root>"#;
945 let doc = parse(xml);
946 let out = process_xincludes(&doc, &opts_with_resolver(docs)).unwrap();
947 let names: Vec<&str> = out
948 .root()
949 .children()
950 .filter(|c| c.kind == NodeKind::Element)
951 .map(|c| c.name())
952 .collect();
953 assert_eq!(names, vec!["before", "inc", "after"]);
954 }
955
956 #[test]
957 fn xinclude_copies_attributes_on_other_elements() {
958 let mut docs = HashMap::new();
960 docs.insert("p.xml".to_string(), b"<i/>".to_vec());
961 let xml = r#"<root xmlns:xi="http://www.w3.org/2001/XInclude" id="r" class="c"><xi:include href="p.xml"/></root>"#;
962 let doc = parse(xml);
963 let out = process_xincludes(&doc, &opts_with_resolver(docs)).unwrap();
964 let root = out.root();
965 let attrs: Vec<(&str, &str)> = root
966 .attributes()
967 .filter(|a| a.name() != "xmlns:xi")
968 .map(|a| (a.name(), a.value()))
969 .collect();
970 assert!(attrs.iter().any(|&(n, v)| n == "id" && v == "r"));
973 assert!(attrs.iter().any(|&(n, v)| n == "class" && v == "c"));
974 }
975
976 #[test]
977 fn xinclude_returns_independent_document() {
978 let mut docs = HashMap::new();
981 docs.insert("p.xml".to_string(), b"<inc>hi</inc>".to_vec());
982 let xml = r#"<root xmlns:xi="http://www.w3.org/2001/XInclude"><xi:include href="p.xml"/></root>"#;
983 let opts = opts_with_resolver(docs);
984 let out = {
985 let doc = parse(xml);
986 process_xincludes(&doc, &opts).unwrap()
987 };
989 assert_eq!(out.root().name(), "root");
990 let inc = out.root().children().next().unwrap();
991 assert_eq!(inc.name(), "inc");
992 assert_eq!(inc.text_content(), Some("hi"));
993 }
994
995 #[test]
996 fn xinclude_xml_decl_fields_preserved() {
997 let mut docs = HashMap::new();
998 docs.insert("p.xml".to_string(), b"<x/>".to_vec());
999 let xml = r#"<?xml version="1.1" encoding="UTF-8" standalone="yes"?><root xmlns:xi="http://www.w3.org/2001/XInclude"><xi:include href="p.xml"/></root>"#;
1000 let doc = parse(xml);
1001 let out = process_xincludes(&doc, &opts_with_resolver(docs)).unwrap();
1002 assert_eq!(out.version, "1.1");
1003 assert_eq!(out.encoding, "UTF-8");
1004 assert_eq!(out.standalone, Some(true));
1005 }
1006
1007 #[test]
1008 fn xinclude_text_mode_does_not_parse_xml() {
1009 let mut docs = HashMap::new();
1012 docs.insert(
1013 "snippet.txt".to_string(),
1014 b"<not-parsed/>".to_vec(),
1015 );
1016 let xml = r#"<root xmlns:xi="http://www.w3.org/2001/XInclude"><xi:include href="snippet.txt" parse="text"/></root>"#;
1017 let doc = parse(xml);
1018 let out = process_xincludes(&doc, &opts_with_resolver(docs)).unwrap();
1019 let kids: Vec<_> = out.root().children().collect();
1020 assert_eq!(kids.len(), 1);
1021 assert_eq!(kids[0].kind, NodeKind::Text);
1022 assert_eq!(kids[0].content(), "<not-parsed/>");
1023 }
1024
1025 #[test]
1026 fn xinclude_fallback_with_nested_include() {
1027 let mut docs = HashMap::new();
1030 docs.insert("ok.xml".to_string(), b"<from-fallback/>".to_vec());
1031 let xml = r#"<root xmlns:xi="http://www.w3.org/2001/XInclude">
1032 <xi:include href="missing.xml">
1033 <xi:fallback><xi:include href="ok.xml"/></xi:fallback>
1034 </xi:include>
1035 </root>"#;
1036 let doc = parse(xml);
1037 let out = process_xincludes(&doc, &opts_with_resolver(docs)).unwrap();
1038 let has_inner = out
1039 .root()
1040 .children()
1041 .any(|c| c.kind == NodeKind::Element && c.name() == "from-fallback");
1042 assert!(
1043 has_inner,
1044 "xi:include nested in fallback should be expanded"
1045 );
1046 }
1047
1048 #[test]
1054 fn xinclude_xpointer_xpath_selects_subtree() {
1055 let mut docs = HashMap::new();
1056 docs.insert(
1057 "part.xml".to_string(),
1058 b"<a><b><c>match</c></b><b><c>skip</c></b></a>".to_vec(),
1059 );
1060 let xml = r#"<root xmlns:xi="http://www.w3.org/2001/XInclude">
1061 <xi:include href="part.xml" xpointer="xpointer(/a/b[1]/c)"/>
1062 </root>"#;
1063 let doc = parse(xml);
1064 let out = process_xincludes(&doc, &opts_with_resolver(docs)).unwrap();
1065 let included: Vec<_> = out.root().children()
1066 .filter(|c| c.kind == NodeKind::Element)
1067 .collect();
1068 assert_eq!(included.len(), 1);
1069 assert_eq!(included[0].name(), "c");
1070 assert_eq!(included[0].text_content(), Some("match"));
1071 }
1072
1073 #[test]
1076 fn xinclude_xpointer_element_child_sequence() {
1077 let mut docs = HashMap::new();
1078 docs.insert(
1079 "part.xml".to_string(),
1080 b"<a><b id='b1'/><b id='b2'><c id='c1'/><c id='c2'/></b></a>".to_vec(),
1081 );
1082 let xml = r#"<root xmlns:xi="http://www.w3.org/2001/XInclude">
1083 <xi:include href="part.xml" xpointer="element(/1/2/2)"/>
1084 </root>"#;
1085 let doc = parse(xml);
1086 let out = process_xincludes(&doc, &opts_with_resolver(docs)).unwrap();
1087 let included: Vec<_> = out.root().children()
1089 .filter(|c| c.kind == NodeKind::Element)
1090 .collect();
1091 assert_eq!(included.len(), 1);
1092 assert_eq!(included[0].name(), "c");
1093 let id_attr = included[0].attributes()
1094 .find(|a| a.name() == "id")
1095 .map(|a| a.value());
1096 assert_eq!(id_attr, Some("c2"));
1097 }
1098
1099 #[test]
1101 fn xinclude_xpointer_element_fragment_id() {
1102 let mut docs = HashMap::new();
1103 docs.insert(
1104 "part.xml".to_string(),
1105 b"<a><b id='hit'><inner/></b><b id='miss'/></a>".to_vec(),
1106 );
1107 let xml = r#"<root xmlns:xi="http://www.w3.org/2001/XInclude">
1108 <xi:include href="part.xml" xpointer="element(hit)"/>
1109 </root>"#;
1110 let doc = parse(xml);
1111 let out = process_xincludes(&doc, &opts_with_resolver(docs)).unwrap();
1112 let included: Vec<_> = out.root().children()
1113 .filter(|c| c.kind == NodeKind::Element)
1114 .collect();
1115 assert_eq!(included.len(), 1);
1116 assert_eq!(included[0].name(), "b");
1117 let has_inner = included[0].children()
1119 .any(|c| c.is_element() && c.name() == "inner");
1120 assert!(has_inner);
1121 }
1122
1123 #[test]
1125 fn xinclude_xpointer_bare_name_is_id_lookup() {
1126 let mut docs = HashMap::new();
1127 docs.insert(
1128 "part.xml".to_string(),
1129 b"<a><b id='target'>hit</b><b id='other'/></a>".to_vec(),
1130 );
1131 let xml = r#"<root xmlns:xi="http://www.w3.org/2001/XInclude">
1132 <xi:include href="part.xml" xpointer="target"/>
1133 </root>"#;
1134 let doc = parse(xml);
1135 let out = process_xincludes(&doc, &opts_with_resolver(docs)).unwrap();
1136 let included: Vec<_> = out.root().children()
1137 .filter(|c| c.kind == NodeKind::Element)
1138 .collect();
1139 assert_eq!(included.len(), 1);
1140 assert_eq!(included[0].text_content(), Some("hit"));
1141 }
1142
1143 #[test]
1146 fn xinclude_xpointer_no_match_is_error() {
1147 let mut docs = HashMap::new();
1148 docs.insert(
1149 "part.xml".to_string(),
1150 b"<a><b/></a>".to_vec(),
1151 );
1152 let xml = r#"<root xmlns:xi="http://www.w3.org/2001/XInclude">
1153 <xi:include href="part.xml" xpointer="xpointer(/no-such-node)"/>
1154 </root>"#;
1155 let doc = parse(xml);
1156 let result = process_xincludes(&doc, &opts_with_resolver(docs));
1157 assert!(result.is_err(),
1158 "xpointer matching nothing should error so fallback can fire");
1159 }
1160}