#![forbid(unsafe_code)]
use std::collections::HashSet;
use std::sync::Arc;
use sup_xml_tree::dom::{Document, DocumentBuilder, Node, NodeKind};
use crate::entity_resolver::{EntityResolver, ResolveError};
use crate::error::{ErrorDomain, ErrorLevel, Result, XmlError};
use crate::options::ParseOptions;
use crate::parser::parse_str;
pub const XINCLUDE_NS: &str = "http://www.w3.org/2001/XInclude";
#[derive(Clone)]
pub struct XIncludeOptions {
pub resolver: Option<Arc<dyn EntityResolver>>,
pub max_depth: u32,
pub max_total_bytes: u64,
}
impl Default for XIncludeOptions {
fn default() -> Self {
Self::new()
}
}
impl XIncludeOptions {
pub fn new() -> Self {
Self {
resolver: None,
max_depth: 16,
max_total_bytes: 10 * 1024 * 1024,
}
}
}
pub fn process_xincludes(
doc: &Document,
opts: &XIncludeOptions,
) -> Result<Document> {
let b = DocumentBuilder::new();
let mut state = State {
opts: opts.clone(),
hrefs: HashSet::new(),
depth: 0,
total_bytes: 0,
};
b.set_version(doc.version.clone());
b.set_encoding(doc.encoding.clone());
b.set_standalone(doc.standalone);
let src_root = doc.root();
if src_root.kind == NodeKind::Element && is_xinclude_element_named(src_root.name()) {
return Err(validation_err(
"xi:include is not allowed as the document root element",
));
}
let dst_root = copy_subtree(&b, src_root, &mut state)?;
b.set_root(dst_root);
Ok(b.build())
}
struct State {
opts: XIncludeOptions,
hrefs: HashSet<String>,
depth: u32,
total_bytes: u64,
}
fn copy_subtree<'a>(
b: &'a DocumentBuilder,
src: &Node<'_>,
state: &mut State,
) -> Result<&'a Node<'a>> {
match src.kind {
NodeKind::Element => {
let name = b.alloc_str(src.name());
let el = b.new_element(name);
for attr in src.attributes() {
let aname = b.alloc_str(attr.name());
let aval = b.alloc_str(attr.value());
let new_attr = b.new_attribute(aname, aval);
b.append_attribute(el, new_attr);
}
copy_children_into(b, el, src, state)?;
Ok(el)
}
NodeKind::Text => {
let content = b.alloc_str(src.content());
Ok(b.new_text(content))
}
NodeKind::CData => {
let content = b.alloc_str(src.content());
Ok(b.new_cdata(content))
}
NodeKind::Comment => {
let content = b.alloc_str(src.content());
Ok(b.new_comment(content))
}
NodeKind::Pi => {
let target = b.alloc_str(src.name());
let content = src.content_opt().map(|c| &*b.alloc_str(c));
Ok(b.new_pi(target, content))
}
NodeKind::EntityRef => {
let name = b.alloc_str(src.name());
let content = b.alloc_str(src.content());
Ok(b.new_entity_ref(name, content))
}
NodeKind::Attribute => unreachable!("Attribute kind never appears on a Node"),
NodeKind::Document => unreachable!("Document kind never appears on a Node"),
NodeKind::DocumentFragment => unreachable!(
"DocumentFragment is a compat-shim transient; XInclude does not walk into one"
),
NodeKind::DtdDecl => unreachable!(
"DtdDecl is an internal-subset child; XInclude copies element subtrees only"
),
NodeKind::Dtd => unreachable!(
"Dtd is a document-level internal-subset node; XInclude copies element subtrees only"
),
}
}
fn copy_children_into<'a>(
b: &'a DocumentBuilder,
dst_parent: &'a Node<'a>,
src_parent: &Node<'_>,
state: &mut State,
) -> Result<()> {
for child in src_parent.children() {
if child.kind == NodeKind::Element && is_xinclude_element(child) {
let replacements = resolve_include(b, child, state)?;
for n in replacements {
b.append_child(dst_parent, n);
}
} else {
let new_child = copy_subtree(b, child, state)?;
b.append_child(dst_parent, new_child);
}
}
Ok(())
}
fn is_xinclude_element(elem: &Node<'_>) -> bool {
if let Some(ns) = elem.namespace.get() {
if ns.href() == XINCLUDE_NS {
let local = local_name(elem.name());
return local == "include";
}
}
is_xinclude_element_named_with_attrs(elem)
}
fn is_xinclude_element_named(name: &str) -> bool {
name == "xi:include"
}
fn is_xinclude_element_named_with_attrs(elem: &Node<'_>) -> bool {
let name = elem.name();
let local = local_name(name);
if local != "include" {
return false;
}
if elem.attributes().any(|a| {
(a.name() == "xmlns" || a.name().starts_with("xmlns:"))
&& a.value() == XINCLUDE_NS
}) {
return true;
}
name == "xi:include"
}
fn is_xi_fallback(elem: &Node<'_>) -> bool {
let local = local_name(elem.name());
local == "fallback"
}
#[inline]
fn local_name(qname: &str) -> &str {
qname.rsplit_once(':').map(|(_, l)| l).unwrap_or(qname)
}
#[derive(Default)]
struct XiAttrs {
href: Option<String>,
parse: XiParseMode,
xpointer: Option<String>,
}
#[derive(Default, Clone, Copy, PartialEq, Eq)]
enum XiParseMode {
#[default]
Xml,
Text,
}
fn parse_xi_include_attrs(elem: &Node<'_>) -> Result<XiAttrs> {
let mut out = XiAttrs::default();
for attr in elem.attributes() {
let local = local_name(attr.name());
match local {
"href" => out.href = Some(attr.value().to_string()),
"parse" => {
out.parse = match attr.value() {
"xml" => XiParseMode::Xml,
"text" => XiParseMode::Text,
other => {
return Err(validation_err(format!(
"xi:include parse={other:?} is not supported \
(only \"xml\" and \"text\")"
)))
}
};
}
"xpointer" => {
out.xpointer = Some(attr.value().to_string());
}
_ => {}
}
}
Ok(out)
}
fn resolve_include<'a>(
b: &'a DocumentBuilder,
include_elem: &Node<'_>,
state: &mut State,
) -> Result<Vec<&'a Node<'a>>> {
state.depth += 1;
if state.depth > state.opts.max_depth {
state.depth -= 1;
return Err(validation_err(format!(
"XInclude depth limit ({}) exceeded — possible recursion",
state.opts.max_depth
)));
}
let primary = resolve_include_inner(b, include_elem, state);
state.depth -= 1;
match primary {
Ok(nodes) => Ok(nodes),
Err(primary_err) => {
let fallback_elem = include_elem.children().find(|c| {
c.kind == NodeKind::Element && is_xi_fallback(c)
});
match fallback_elem {
Some(fb) => {
let tmp = b.new_element(b.alloc_str("__xi_fallback_tmp__"));
copy_children_into(b, tmp, fb, state)?;
let mut out = Vec::new();
let mut cur = tmp.first_child.get();
while let Some(c) = cur {
let next = c.next_sibling.get();
b.detach(c);
out.push(c);
cur = next;
}
Ok(out)
}
None => Err(primary_err),
}
}
}
}
fn resolve_include_inner<'a>(
b: &'a DocumentBuilder,
include_elem: &Node<'_>,
state: &mut State,
) -> Result<Vec<&'a Node<'a>>> {
let attrs = parse_xi_include_attrs(include_elem)?;
let href = attrs.href.ok_or_else(|| {
validation_err(
"xi:include without href is not supported in v1 (use \
href to reference an external resource)",
)
})?;
if state.hrefs.contains(&href) {
return Err(validation_err(format!(
"XInclude cycle detected: {href:?} is already in the \
include chain"
)));
}
let resolver = state
.opts
.resolver
.as_ref()
.cloned()
.ok_or_else(|| {
io_err(format!(
"xi:include {href:?} cannot be resolved — no \
resolver configured (set XIncludeOptions::resolver)"
))
})?;
let bytes = resolver
.resolve(None, &href, None)
.map_err(|e| match e {
ResolveError::Refused(msg) => io_err(format!(
"xi:include {href:?} refused by resolver: {msg}"
)),
ResolveError::Io(io) => io_err(format!(
"xi:include {href:?} I/O error: {io}"
)),
ResolveError::Other(other) => io_err(format!(
"xi:include {href:?} resolver error: {other}"
)),
})?;
let added = bytes.len() as u64;
if state.total_bytes.saturating_add(added) > state.opts.max_total_bytes {
return Err(validation_err(format!(
"XInclude byte budget ({}) exceeded by {href:?}",
state.opts.max_total_bytes
)));
}
state.total_bytes += added;
match attrs.parse {
XiParseMode::Xml => {
let text = std::str::from_utf8(&bytes).map_err(|e| {
XmlError::new(
ErrorDomain::Encoding,
ErrorLevel::Fatal,
format!("xi:include {href:?} bytes are not UTF-8: {e}"),
)
})?;
let sub_doc = parse_str(text, &ParseOptions::default())?;
state.hrefs.insert(href.clone());
let result: Result<Vec<&Node<'_>>> = (|| {
let targets: Vec<&Node<'_>> = match &attrs.xpointer {
Some(xp) => resolve_xpointer(&sub_doc, xp, &href)?,
None => vec![sub_doc.root()],
};
let mut out: Vec<&Node<'_>> = Vec::with_capacity(targets.len());
for tgt in targets {
if is_xinclude_element(tgt) {
out.extend(resolve_include(b, tgt, state)?);
} else {
out.push(copy_subtree(b, tgt, state)?);
}
}
Ok(out)
})();
state.hrefs.remove(&href);
result
}
XiParseMode::Text => {
let text = String::from_utf8_lossy(&bytes).into_owned();
let alloc = b.alloc_str(&text);
Ok(vec![b.new_text(alloc)])
}
}
}
pub fn resolve_xpointer<'a>(
sub_doc: &'a sup_xml_tree::dom::Document,
expr: &str,
href: &str,
) -> Result<Vec<&'a Node<'a>>> {
let bad = |msg: String| validation_err(format!(
"xi:include xpointer={expr:?} ({href}): {msg}"
));
if let Some(rest) = expr.strip_prefix("xpointer(") {
let inner = rest.strip_suffix(')').ok_or_else(|| {
bad("missing closing `)` after xpointer scheme".to_string())
})?;
let result = crate::xpath::xpath_eval(sub_doc, inner)
.map_err(|e| bad(format!("XPath evaluation failed: {e}")))?;
let ids = match result {
crate::xpath::XPathValue::NodeSet(ns) if !ns.is_empty() => ns,
crate::xpath::XPathValue::NodeSet(_) => {
return Err(bad("XPath returned an empty nodeset".to_string()));
}
_ => return Err(bad(
"XPath must return a nodeset for xi:include splicing".to_string()
)),
};
let idx = crate::xpath::context::DocIndex::build(sub_doc);
let mut out: Vec<&Node<'_>> = Vec::with_capacity(ids.len());
for id in ids {
if let Some(n) = node_id_to_arena(&idx, id) {
out.push(n);
}
}
if out.is_empty() {
return Err(bad(
"XPath matched only non-element nodes (text, attributes, \
etc.) — XInclude can only splice element / document \
subtrees".to_string()
));
}
return Ok(out);
}
if let Some(rest) = expr.strip_prefix("element(") {
let inner = rest.strip_suffix(')').ok_or_else(|| {
bad("missing closing `)` after element scheme".to_string())
})?;
let trimmed = inner.trim();
let (start, seq): (Option<&str>, &str) = if trimmed.starts_with('/') {
(None, trimmed)
} else {
match trimmed.find('/') {
Some(slash) => (Some(&trimmed[..slash]), &trimmed[slash..]),
None => (Some(trimmed), ""),
}
};
let (anchor, seq_after_root): (&Node<'_>, &str) = match start {
None => {
let after_slash = seq.trim_start_matches('/');
let (first, rest) = match after_slash.find('/') {
Some(i) => (&after_slash[..i], &after_slash[i + 1..]),
None => (after_slash, ""),
};
if !first.is_empty() {
let n: usize = first.parse().map_err(|_| {
bad(format!("ChildSequence root step {first:?} is not a positive integer"))
})?;
if n != 1 {
return Err(bad(format!(
"ChildSequence root step must be 1 (only one root \
element exists); got {n}"
)));
}
}
(sub_doc.root(), rest)
}
Some(name) => {
let n = find_by_id(sub_doc, name).ok_or_else(|| {
bad(format!("no element with id={name:?}"))
})?;
(n, seq.trim_start_matches('/'))
}
};
let target = walk_child_sequence(anchor, seq_after_root, &bad)?;
return Ok(vec![target]);
}
if !expr.contains('(') && !expr.contains('/') && !expr.is_empty() {
let n = find_by_id(sub_doc, expr).ok_or_else(|| {
bad(format!("no element with id={expr:?}"))
})?;
return Ok(vec![n]);
}
Err(bad(format!(
"unrecognized xpointer form — supported: \
`xpointer(EXPR)`, `element(/N/...)`, `element(NAME[/N/...])`, \
or bare `NAME`"
)))
}
fn walk_child_sequence<'a, F>(
anchor: &'a Node<'a>,
seq: &str,
bad: &F,
) -> Result<&'a Node<'a>>
where
F: Fn(String) -> XmlError,
{
let mut current = anchor;
let mut remaining = seq.trim_start_matches('/');
while !remaining.is_empty() {
let (head, tail) = match remaining.find('/') {
Some(i) => (&remaining[..i], &remaining[i + 1..]),
None => (remaining, ""),
};
let n: usize = head.parse().map_err(|_| {
bad(format!("ChildSequence step {head:?} is not a positive integer"))
})?;
if n == 0 {
return Err(bad(
"ChildSequence steps are 1-based; got 0".to_string(),
));
}
let mut next_node: Option<&Node<'_>> = None;
let mut seen = 0usize;
for child in current.children() {
if child.is_element() {
seen += 1;
if seen == n {
next_node = Some(child);
break;
}
}
}
current = next_node.ok_or_else(|| {
bad(format!(
"ChildSequence step {n} out of range — only {seen} element \
child(ren) under <{}>",
current.name()
))
})?;
remaining = tail;
}
Ok(current)
}
fn find_by_id<'a>(
doc: &'a sup_xml_tree::dom::Document,
id: &str,
) -> Option<&'a Node<'a>> {
fn walk<'a>(n: &'a Node<'a>, id: &str) -> Option<&'a Node<'a>> {
if n.is_element() {
for a in n.attributes() {
let nm = a.name();
if (nm == "id" || nm == "xml:id" || nm.ends_with(":id"))
&& a.value() == id
{
return Some(n);
}
}
}
for c in n.children() {
if let Some(found) = walk(c, id) {
return Some(found);
}
}
None
}
walk(doc.root(), id)
}
fn node_id_to_arena<'doc>(
idx: &crate::xpath::context::DocIndex<'doc>,
id: crate::xpath::NodeId,
) -> Option<&'doc Node<'doc>> {
use crate::xpath::context::INodeKind;
match &idx.nodes.get(id)?.kind {
INodeKind::Element(n) => Some(n),
_ => None,
}
}
fn io_err(msg: String) -> XmlError {
XmlError::new(ErrorDomain::Io, ErrorLevel::Fatal, msg)
}
fn validation_err(msg: impl Into<String>) -> XmlError {
XmlError::new(ErrorDomain::Validation, ErrorLevel::Fatal, msg)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::entity_resolver::InMemoryResolver;
use std::collections::HashMap;
use std::sync::Arc;
fn opts_with_resolver(map: HashMap<String, Vec<u8>>) -> XIncludeOptions {
let mut r = InMemoryResolver::new();
for (sys, bytes) in map {
r = r.with_system(&sys, bytes);
}
XIncludeOptions {
resolver: Some(Arc::new(r)),
..XIncludeOptions::new()
}
}
fn parse(xml: &str) -> Document {
parse_str(xml, &ParseOptions::default()).expect("parse")
}
#[test]
fn xinclude_xml_replaces_include_with_referenced_subtree() {
let mut docs = HashMap::new();
docs.insert("part.xml".to_string(), b"<chunk>hello</chunk>".to_vec());
let xml = r#"<root xmlns:xi="http://www.w3.org/2001/XInclude"><xi:include href="part.xml"/></root>"#;
let doc = parse(xml);
let out = process_xincludes(&doc, &opts_with_resolver(docs)).unwrap();
let root = out.root();
assert_eq!(root.name(), "root");
let kids: Vec<_> = root.children().collect();
assert_eq!(kids.len(), 1);
let chunk = kids[0];
assert_eq!(chunk.kind, NodeKind::Element);
assert_eq!(chunk.name(), "chunk");
assert_eq!(chunk.text_content(), Some("hello"));
}
#[test]
fn xinclude_text_includes_raw_bytes_as_text_node() {
let mut docs = HashMap::new();
docs.insert(
"readme.txt".to_string(),
b"raw <text> with & specials".to_vec(),
);
let xml = r#"<root xmlns:xi="http://www.w3.org/2001/XInclude"><xi:include href="readme.txt" parse="text"/></root>"#;
let doc = parse(xml);
let out = process_xincludes(&doc, &opts_with_resolver(docs)).unwrap();
let root = out.root();
let kids: Vec<_> = root.children().collect();
assert_eq!(kids.len(), 1);
assert_eq!(kids[0].kind, NodeKind::Text);
assert_eq!(kids[0].content(), "raw <text> with & specials");
}
#[test]
fn xinclude_fallback_used_when_resolve_fails() {
let xml = r#"<root xmlns:xi="http://www.w3.org/2001/XInclude">
<xi:include href="missing.xml">
<xi:fallback><default>fallback content</default></xi:fallback>
</xi:include>
</root>"#;
let doc = parse(xml);
let out = process_xincludes(&doc, &opts_with_resolver(HashMap::new()))
.unwrap();
let root = out.root();
let has_default = root.children().any(|c| {
c.kind == NodeKind::Element && c.name() == "default"
});
assert!(
has_default,
"fallback content should replace the failed include"
);
}
#[test]
fn xinclude_no_resolver_errors_on_include() {
let xml = r#"<root xmlns:xi="http://www.w3.org/2001/XInclude"><xi:include href="x.xml"/></root>"#;
let doc = parse(xml);
let opts = XIncludeOptions::new();
let err = process_xincludes(&doc, &opts).expect_err("no resolver");
assert!(err.message.contains("no resolver"), "got: {}", err.message);
}
#[test]
fn xinclude_recursive_processing() {
let mut docs = HashMap::new();
docs.insert(
"outer.xml".to_string(),
br#"<wrap xmlns:xi="http://www.w3.org/2001/XInclude"><xi:include href="inner.xml"/></wrap>"#.to_vec(),
);
docs.insert("inner.xml".to_string(), b"<leaf>x</leaf>".to_vec());
let xml = r#"<root xmlns:xi="http://www.w3.org/2001/XInclude"><xi:include href="outer.xml"/></root>"#;
let doc = parse(xml);
let out = process_xincludes(&doc, &opts_with_resolver(docs)).unwrap();
let root = out.root();
let wrap = root.children().next().unwrap();
assert_eq!(wrap.name(), "wrap");
let leaf = wrap.children().next().unwrap();
assert_eq!(leaf.name(), "leaf");
assert_eq!(leaf.text_content(), Some("x"));
}
#[test]
fn xinclude_cycle_detected() {
let mut docs = HashMap::new();
docs.insert(
"a.xml".to_string(),
br#"<a xmlns:xi="http://www.w3.org/2001/XInclude"><xi:include href="a.xml"/></a>"#.to_vec(),
);
let xml = r#"<root xmlns:xi="http://www.w3.org/2001/XInclude"><xi:include href="a.xml"/></root>"#;
let doc = parse(xml);
let err = process_xincludes(&doc, &opts_with_resolver(docs))
.expect_err("cycle");
assert!(
err.message.contains("cycle") || err.message.contains("depth"),
"got: {}",
err.message
);
}
#[test]
fn xinclude_max_depth_enforced() {
let mut docs = HashMap::new();
for (i, next) in [(1, 2), (2, 3), (3, 4), (4, 5), (5, 6)] {
docs.insert(
format!("d{i}.xml"),
format!(
r#"<l xmlns:xi="http://www.w3.org/2001/XInclude"><xi:include href="d{next}.xml"/></l>"#
)
.into_bytes(),
);
}
let xml = r#"<root xmlns:xi="http://www.w3.org/2001/XInclude"><xi:include href="d1.xml"/></root>"#;
let doc = parse(xml);
let opts = XIncludeOptions {
max_depth: 2,
..opts_with_resolver(docs)
};
let err = process_xincludes(&doc, &opts).expect_err("depth limit");
assert!(err.message.contains("depth"), "got: {}", err.message);
}
#[test]
fn xinclude_no_xi_elements_is_noop() {
let xml = "<r><a/><b/></r>";
let doc = parse(xml);
let out = process_xincludes(&doc, &XIncludeOptions::new()).unwrap();
let root = out.root();
assert_eq!(root.name(), "r");
let kids: Vec<&str> = root.children().map(|c| c.name()).collect();
assert_eq!(kids, vec!["a", "b"]);
}
#[test]
fn xinclude_unsupported_parse_mode_errors() {
let xml = r#"<root xmlns:xi="http://www.w3.org/2001/XInclude"><xi:include href="x" parse="binary"/></root>"#;
let doc = parse(xml);
let err = process_xincludes(&doc, &opts_with_resolver(HashMap::new()))
.expect_err("bad parse mode");
assert!(err.message.contains("parse"), "got: {}", err.message);
}
#[test]
fn xinclude_preserves_surrounding_siblings() {
let mut docs = HashMap::new();
docs.insert("p.xml".to_string(), b"<inc/>".to_vec());
let xml = r#"<root xmlns:xi="http://www.w3.org/2001/XInclude">
<before/>
<xi:include href="p.xml"/>
<after/>
</root>"#;
let doc = parse(xml);
let out = process_xincludes(&doc, &opts_with_resolver(docs)).unwrap();
let names: Vec<&str> = out
.root()
.children()
.filter(|c| c.kind == NodeKind::Element)
.map(|c| c.name())
.collect();
assert_eq!(names, vec!["before", "inc", "after"]);
}
#[test]
fn xinclude_copies_attributes_on_other_elements() {
let mut docs = HashMap::new();
docs.insert("p.xml".to_string(), b"<i/>".to_vec());
let xml = r#"<root xmlns:xi="http://www.w3.org/2001/XInclude" id="r" class="c"><xi:include href="p.xml"/></root>"#;
let doc = parse(xml);
let out = process_xincludes(&doc, &opts_with_resolver(docs)).unwrap();
let root = out.root();
let attrs: Vec<(&str, &str)> = root
.attributes()
.filter(|a| a.name() != "xmlns:xi")
.map(|a| (a.name(), a.value()))
.collect();
assert!(attrs.iter().any(|&(n, v)| n == "id" && v == "r"));
assert!(attrs.iter().any(|&(n, v)| n == "class" && v == "c"));
}
#[test]
fn xinclude_returns_independent_document() {
let mut docs = HashMap::new();
docs.insert("p.xml".to_string(), b"<inc>hi</inc>".to_vec());
let xml = r#"<root xmlns:xi="http://www.w3.org/2001/XInclude"><xi:include href="p.xml"/></root>"#;
let opts = opts_with_resolver(docs);
let out = {
let doc = parse(xml);
process_xincludes(&doc, &opts).unwrap()
};
assert_eq!(out.root().name(), "root");
let inc = out.root().children().next().unwrap();
assert_eq!(inc.name(), "inc");
assert_eq!(inc.text_content(), Some("hi"));
}
#[test]
fn xinclude_xml_decl_fields_preserved() {
let mut docs = HashMap::new();
docs.insert("p.xml".to_string(), b"<x/>".to_vec());
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>"#;
let doc = parse(xml);
let out = process_xincludes(&doc, &opts_with_resolver(docs)).unwrap();
assert_eq!(out.version, "1.1");
assert_eq!(out.encoding, "UTF-8");
assert_eq!(out.standalone, Some(true));
}
#[test]
fn xinclude_text_mode_does_not_parse_xml() {
let mut docs = HashMap::new();
docs.insert(
"snippet.txt".to_string(),
b"<not-parsed/>".to_vec(),
);
let xml = r#"<root xmlns:xi="http://www.w3.org/2001/XInclude"><xi:include href="snippet.txt" parse="text"/></root>"#;
let doc = parse(xml);
let out = process_xincludes(&doc, &opts_with_resolver(docs)).unwrap();
let kids: Vec<_> = out.root().children().collect();
assert_eq!(kids.len(), 1);
assert_eq!(kids[0].kind, NodeKind::Text);
assert_eq!(kids[0].content(), "<not-parsed/>");
}
#[test]
fn xinclude_fallback_with_nested_include() {
let mut docs = HashMap::new();
docs.insert("ok.xml".to_string(), b"<from-fallback/>".to_vec());
let xml = r#"<root xmlns:xi="http://www.w3.org/2001/XInclude">
<xi:include href="missing.xml">
<xi:fallback><xi:include href="ok.xml"/></xi:fallback>
</xi:include>
</root>"#;
let doc = parse(xml);
let out = process_xincludes(&doc, &opts_with_resolver(docs)).unwrap();
let has_inner = out
.root()
.children()
.any(|c| c.kind == NodeKind::Element && c.name() == "from-fallback");
assert!(
has_inner,
"xi:include nested in fallback should be expanded"
);
}
#[test]
fn xinclude_xpointer_xpath_selects_subtree() {
let mut docs = HashMap::new();
docs.insert(
"part.xml".to_string(),
b"<a><b><c>match</c></b><b><c>skip</c></b></a>".to_vec(),
);
let xml = r#"<root xmlns:xi="http://www.w3.org/2001/XInclude">
<xi:include href="part.xml" xpointer="xpointer(/a/b[1]/c)"/>
</root>"#;
let doc = parse(xml);
let out = process_xincludes(&doc, &opts_with_resolver(docs)).unwrap();
let included: Vec<_> = out.root().children()
.filter(|c| c.kind == NodeKind::Element)
.collect();
assert_eq!(included.len(), 1);
assert_eq!(included[0].name(), "c");
assert_eq!(included[0].text_content(), Some("match"));
}
#[test]
fn xinclude_xpointer_element_child_sequence() {
let mut docs = HashMap::new();
docs.insert(
"part.xml".to_string(),
b"<a><b id='b1'/><b id='b2'><c id='c1'/><c id='c2'/></b></a>".to_vec(),
);
let xml = r#"<root xmlns:xi="http://www.w3.org/2001/XInclude">
<xi:include href="part.xml" xpointer="element(/1/2/2)"/>
</root>"#;
let doc = parse(xml);
let out = process_xincludes(&doc, &opts_with_resolver(docs)).unwrap();
let included: Vec<_> = out.root().children()
.filter(|c| c.kind == NodeKind::Element)
.collect();
assert_eq!(included.len(), 1);
assert_eq!(included[0].name(), "c");
let id_attr = included[0].attributes()
.find(|a| a.name() == "id")
.map(|a| a.value());
assert_eq!(id_attr, Some("c2"));
}
#[test]
fn xinclude_xpointer_element_fragment_id() {
let mut docs = HashMap::new();
docs.insert(
"part.xml".to_string(),
b"<a><b id='hit'><inner/></b><b id='miss'/></a>".to_vec(),
);
let xml = r#"<root xmlns:xi="http://www.w3.org/2001/XInclude">
<xi:include href="part.xml" xpointer="element(hit)"/>
</root>"#;
let doc = parse(xml);
let out = process_xincludes(&doc, &opts_with_resolver(docs)).unwrap();
let included: Vec<_> = out.root().children()
.filter(|c| c.kind == NodeKind::Element)
.collect();
assert_eq!(included.len(), 1);
assert_eq!(included[0].name(), "b");
let has_inner = included[0].children()
.any(|c| c.is_element() && c.name() == "inner");
assert!(has_inner);
}
#[test]
fn xinclude_xpointer_bare_name_is_id_lookup() {
let mut docs = HashMap::new();
docs.insert(
"part.xml".to_string(),
b"<a><b id='target'>hit</b><b id='other'/></a>".to_vec(),
);
let xml = r#"<root xmlns:xi="http://www.w3.org/2001/XInclude">
<xi:include href="part.xml" xpointer="target"/>
</root>"#;
let doc = parse(xml);
let out = process_xincludes(&doc, &opts_with_resolver(docs)).unwrap();
let included: Vec<_> = out.root().children()
.filter(|c| c.kind == NodeKind::Element)
.collect();
assert_eq!(included.len(), 1);
assert_eq!(included[0].text_content(), Some("hit"));
}
#[test]
fn xinclude_xpointer_no_match_is_error() {
let mut docs = HashMap::new();
docs.insert(
"part.xml".to_string(),
b"<a><b/></a>".to_vec(),
);
let xml = r#"<root xmlns:xi="http://www.w3.org/2001/XInclude">
<xi:include href="part.xml" xpointer="xpointer(/no-such-node)"/>
</root>"#;
let doc = parse(xml);
let result = process_xincludes(&doc, &opts_with_resolver(docs));
assert!(result.is_err(),
"xpointer matching nothing should error so fallback can fire");
}
}