1use std::collections::hash_map::Entry;
15use std::collections::{HashMap, HashSet};
16
17use roxmltree::{Document, Node, NodeId};
18
19use super::types::{NodeSet, NodeSetMaterializationBudget, TransformData, TransformError};
20
21const DEFAULT_ID_ATTRS: &[&str] = &["ID", "Id", "id"];
28
29pub struct UriReferenceResolver<'a> {
51 doc: &'a Document<'a>,
52 id_map: HashMap<&'a str, Node<'a, 'a>>,
54}
55
56impl<'a> UriReferenceResolver<'a> {
57 pub fn new(doc: &'a Document<'a>) -> Self {
59 Self::with_id_attrs(doc, DEFAULT_ID_ATTRS)
60 }
61
62 pub fn with_id_attrs(doc: &'a Document<'a>, extra_attrs: &[&str]) -> Self {
73 let mut id_map = HashMap::new();
74 let mut duplicate_ids: HashSet<&'a str> = HashSet::new();
77
78 let mut attr_names: Vec<&str> = DEFAULT_ID_ATTRS.to_vec();
80 for name in extra_attrs {
81 if !attr_names.contains(name) {
82 attr_names.push(name);
83 }
84 }
85
86 for node in doc.descendants() {
88 if node.is_element() {
89 for attr_name in &attr_names {
90 if let Some(value) = node.attribute(*attr_name) {
91 if duplicate_ids.contains(value) {
93 continue;
94 }
95
96 match id_map.entry(value) {
101 Entry::Vacant(v) => {
102 v.insert(node);
103 }
104 Entry::Occupied(o) => {
105 if o.get().id() != node.id() {
110 o.remove();
111 duplicate_ids.insert(value);
112 }
113 }
114 }
115 }
116 }
117 }
118 }
119
120 Self { doc, id_map }
121 }
122
123 pub fn dereference(&self, uri: &str) -> Result<TransformData<'a>, TransformError> {
135 self.dereference_with_optional_budget(uri, None)
136 }
137
138 pub(crate) fn dereference_with_budget(
139 &self,
140 uri: &str,
141 budget: &NodeSetMaterializationBudget,
142 ) -> Result<TransformData<'a>, TransformError> {
143 self.dereference_with_optional_budget(uri, Some(budget))
144 }
145
146 fn dereference_with_optional_budget(
147 &self,
148 uri: &str,
149 budget: Option<&NodeSetMaterializationBudget>,
150 ) -> Result<TransformData<'a>, TransformError> {
151 if uri.is_empty() {
152 let nodes = match budget {
156 Some(budget) => {
157 NodeSet::entire_document_without_comments_with_budget(self.doc, budget)?
158 }
159 None => NodeSet::entire_document_without_comments(self.doc)?,
160 };
161 Ok(TransformData::NodeSet(nodes))
162 } else if let Some(fragment) = uri.strip_prefix('#') {
163 self.dereference_fragment(fragment, budget)
168 } else {
169 Err(TransformError::UnsupportedUri(uri.to_string()))
170 }
171 }
172
173 fn dereference_fragment(
180 &self,
181 fragment: &str,
182 budget: Option<&NodeSetMaterializationBudget>,
183 ) -> Result<TransformData<'a>, TransformError> {
184 if fragment.is_empty() {
185 return Err(TransformError::UnsupportedUri("#".to_string()));
187 }
188
189 if fragment == "xpointer(/)" {
190 let nodes = match budget {
194 Some(budget) => {
195 NodeSet::entire_document_with_comments_with_budget(self.doc, budget)?
196 }
197 None => NodeSet::entire_document_with_comments(self.doc)?,
198 };
199 Ok(TransformData::NodeSet(nodes))
200 } else if let Some(id) = parse_xpointer_id_fragment(fragment) {
201 if id.is_empty() {
204 return Err(TransformError::UnsupportedUri(format!("#{fragment}")));
205 }
206 self.resolve_id(id, budget)
207 } else if fragment.starts_with("xpointer(") {
208 Err(TransformError::UnsupportedUri(format!("#{fragment}")))
210 } else {
211 self.resolve_id(fragment, budget)
213 }
214 }
215
216 fn resolve_id(
218 &self,
219 id: &str,
220 budget: Option<&NodeSetMaterializationBudget>,
221 ) -> Result<TransformData<'a>, TransformError> {
222 match self.id_map.get(id) {
223 Some(&element) => {
224 let nodes = match budget {
225 Some(budget) => NodeSet::subtree_with_budget(element, budget)?,
226 None => NodeSet::subtree(element)?,
227 };
228 Ok(TransformData::NodeSet(nodes))
229 }
230 None => Err(TransformError::ElementNotFound(id.to_string())),
231 }
232 }
233
234 pub fn has_id(&self, id: &str) -> bool {
236 self.id_map.contains_key(id)
237 }
238
239 pub(crate) fn node_id_for_id(&self, id: &str) -> Option<NodeId> {
244 self.id_map.get(id).map(|node| node.id())
245 }
246
247 pub fn id_count(&self) -> usize {
249 self.id_map.len()
250 }
251}
252
253pub(crate) fn parse_xpointer_id_fragment(fragment: &str) -> Option<&str> {
256 let inner = fragment.strip_prefix("xpointer(id(")?.strip_suffix("))")?;
257
258 if let Some(stripped) = inner.strip_prefix('\'').and_then(|s| s.strip_suffix('\'')) {
261 Some(stripped)
262 } else if let Some(stripped) = inner.strip_prefix('"').and_then(|s| s.strip_suffix('"')) {
263 Some(stripped)
264 } else {
265 None
266 }
267}
268
269#[cfg(test)]
270#[allow(clippy::unwrap_used)]
271mod tests {
272 use super::super::types::NodeSet;
273 use super::*;
274
275 #[test]
276 fn empty_uri_returns_whole_document() {
277 let xml = "<root><child>text</child></root>";
278 let doc = Document::parse(xml).unwrap();
279 let resolver = UriReferenceResolver::new(&doc);
280
281 let data = resolver.dereference("").unwrap();
282 let node_set = data.into_node_set().unwrap();
283
284 let root = doc.root_element();
286 assert!(node_set.contains(root));
287 let child = root.first_child().unwrap();
288 assert!(node_set.contains(child));
289 }
290
291 #[test]
292 fn empty_uri_excludes_comments() {
293 let xml = "<root><!-- comment --><child/></root>";
294 let doc = Document::parse(xml).unwrap();
295 let resolver = UriReferenceResolver::new(&doc);
296
297 let data = resolver.dereference("").unwrap();
298 let node_set = data.into_node_set().unwrap();
299
300 for node in doc.descendants() {
302 if node.is_comment() {
303 assert!(
304 !node_set.contains(node),
305 "comment should be excluded for empty URI"
306 );
307 }
308 }
309 assert!(node_set.contains(doc.root_element()));
311 }
312
313 #[test]
314 fn fragment_uri_resolves_by_id_attr() {
315 let xml = r#"<root><item ID="abc">content</item><item ID="def">other</item></root>"#;
316 let doc = Document::parse(xml).unwrap();
317 let resolver = UriReferenceResolver::new(&doc);
318
319 let data = resolver.dereference("#abc").unwrap();
320 let node_set = data.into_node_set().unwrap();
321
322 let abc_elem = doc
324 .descendants()
325 .find(|n| n.attribute("ID") == Some("abc"))
326 .unwrap();
327 assert!(node_set.contains(abc_elem));
328
329 let text_child = abc_elem.first_child().unwrap();
331 assert!(node_set.contains(text_child));
332
333 assert!(!node_set.contains(doc.root_element()));
335
336 let def_elem = doc
338 .descendants()
339 .find(|n| n.attribute("ID") == Some("def"))
340 .unwrap();
341 assert!(!node_set.contains(def_elem));
342 }
343
344 #[test]
345 fn fragment_uri_resolves_lowercase_id() {
346 let xml = r#"<root><item id="lower">text</item></root>"#;
347 let doc = Document::parse(xml).unwrap();
348 let resolver = UriReferenceResolver::new(&doc);
349
350 let data = resolver.dereference("#lower").unwrap();
351 let node_set = data.into_node_set().unwrap();
352
353 let elem = doc
354 .descendants()
355 .find(|n| n.attribute("id") == Some("lower"))
356 .unwrap();
357 assert!(node_set.contains(elem));
358 }
359
360 #[test]
361 fn fragment_uri_resolves_mixed_case_id() {
362 let xml = r#"<root><ds:Signature Id="sig1" xmlns:ds="http://www.w3.org/2000/09/xmldsig#"/></root>"#;
363 let doc = Document::parse(xml).unwrap();
364 let resolver = UriReferenceResolver::new(&doc);
365
366 assert!(resolver.has_id("sig1"));
367 let data = resolver.dereference("#sig1").unwrap();
368 assert!(data.into_node_set().is_ok());
369 }
370
371 #[test]
372 fn fragment_uri_not_found() {
373 let xml = "<root><child>text</child></root>";
374 let doc = Document::parse(xml).unwrap();
375 let resolver = UriReferenceResolver::new(&doc);
376
377 let result = resolver.dereference("#nonexistent");
378 assert!(result.is_err());
379 match result.unwrap_err() {
380 TransformError::ElementNotFound(id) => assert_eq!(id, "nonexistent"),
381 other => panic!("expected ElementNotFound, got: {other:?}"),
382 }
383 }
384
385 #[test]
386 fn unsupported_external_uri() {
387 let xml = "<root/>";
388 let doc = Document::parse(xml).unwrap();
389 let resolver = UriReferenceResolver::new(&doc);
390
391 let result = resolver.dereference("http://example.com/doc.xml");
392 assert!(result.is_err());
393 match result.unwrap_err() {
394 TransformError::UnsupportedUri(uri) => {
395 assert_eq!(uri, "http://example.com/doc.xml")
396 }
397 other => panic!("expected UnsupportedUri, got: {other:?}"),
398 }
399 }
400
401 #[test]
402 fn unsupported_xpointer_expression() {
403 let xml = "<root/>";
406 let doc = Document::parse(xml).unwrap();
407 let resolver = UriReferenceResolver::new(&doc);
408
409 let result = resolver.dereference("#xpointer(foo())");
410 assert!(result.is_err());
411 match result.unwrap_err() {
412 TransformError::UnsupportedUri(uri) => {
413 assert_eq!(uri, "#xpointer(foo())")
414 }
415 other => panic!("expected UnsupportedUri, got: {other:?}"),
416 }
417
418 let result = resolver.dereference("#xpointer(//element)");
420 assert!(result.is_err());
421 assert!(matches!(
422 result.unwrap_err(),
423 TransformError::UnsupportedUri(_)
424 ));
425 }
426
427 #[test]
428 fn empty_fragment_rejected() {
429 let xml = "<root/>";
431 let doc = Document::parse(xml).unwrap();
432 let resolver = UriReferenceResolver::new(&doc);
433
434 let result = resolver.dereference("#");
435 assert!(result.is_err());
436 match result.unwrap_err() {
437 TransformError::UnsupportedUri(uri) => assert_eq!(uri, "#"),
438 other => panic!("expected UnsupportedUri, got: {other:?}"),
439 }
440 }
441
442 #[test]
443 fn foreign_document_node_rejected() {
444 let xml1 = "<root><child/></root>";
446 let xml2 = "<other><item/></other>";
447 let doc1 = Document::parse(xml1).unwrap();
448 let doc2 = Document::parse(xml2).unwrap();
449
450 let node_set = NodeSet::entire_document_without_comments(&doc1).unwrap();
451
452 let foreign_node = doc2.root_element();
454 assert!(
455 !node_set.contains(foreign_node),
456 "foreign document node should be rejected"
457 );
458
459 let own_node = doc1.root_element();
461 assert!(node_set.contains(own_node));
462 }
463
464 #[test]
465 fn custom_id_attr_name() {
466 let xml = r#"<root><elem myid="custom1">data</elem></root>"#;
469 let doc = Document::parse(xml).unwrap();
470
471 let resolver_default = UriReferenceResolver::new(&doc);
473 assert!(!resolver_default.has_id("custom1"));
474
475 let resolver_custom = UriReferenceResolver::with_id_attrs(&doc, &["myid"]);
477 assert!(resolver_custom.has_id("custom1"));
478
479 let data = resolver_custom.dereference("#custom1").unwrap();
480 assert!(data.into_node_set().is_ok());
481 }
482
483 #[test]
484 fn namespaced_id_attr_found_by_local_name() {
485 let xml =
487 r#"<root><elem wsu:Id="ts1" xmlns:wsu="http://example.com/wsu">data</elem></root>"#;
488 let doc = Document::parse(xml).unwrap();
489
490 let resolver = UriReferenceResolver::new(&doc);
491 assert!(resolver.has_id("ts1"));
492 }
493
494 #[test]
495 fn id_count_reports_unique_ids() {
496 let xml = r#"<root ID="r1"><a ID="a1"/><b Id="b1"/><c id="c1"/></root>"#;
497 let doc = Document::parse(xml).unwrap();
498 let resolver = UriReferenceResolver::new(&doc);
499
500 assert_eq!(resolver.id_count(), 4);
502 }
503
504 #[test]
505 fn duplicate_ids_are_rejected() {
506 let xml = r#"<root><a ID="dup">first</a><b ID="dup">second</b></root>"#;
509 let doc = Document::parse(xml).unwrap();
510 let resolver = UriReferenceResolver::new(&doc);
511
512 assert!(!resolver.has_id("dup"));
514 let result = resolver.dereference("#dup");
515 assert!(result.is_err());
516 assert!(matches!(
517 result.unwrap_err(),
518 TransformError::ElementNotFound(_)
519 ));
520 }
521
522 #[test]
523 fn triple_duplicate_ids_stay_rejected() {
524 let xml = r#"<root><a ID="dup">1</a><b ID="dup">2</b><c ID="dup">3</c></root>"#;
527 let doc = Document::parse(xml).unwrap();
528 let resolver = UriReferenceResolver::new(&doc);
529
530 assert!(!resolver.has_id("dup"));
531 assert!(resolver.dereference("#dup").is_err());
532 }
533
534 #[test]
535 fn node_set_exclude_subtree() {
536 let xml = r#"<root><keep>yes</keep><remove><deep>no</deep></remove></root>"#;
537 let doc = Document::parse(xml).unwrap();
538 let resolver = UriReferenceResolver::new(&doc);
539
540 let data = resolver.dereference("").unwrap();
541 let mut node_set = data.into_node_set().unwrap();
542
543 let remove_elem = doc
545 .descendants()
546 .find(|n| n.is_element() && n.has_tag_name("remove"))
547 .unwrap();
548 node_set.exclude_subtree(remove_elem);
549
550 let keep_elem = doc
552 .descendants()
553 .find(|n| n.is_element() && n.has_tag_name("keep"))
554 .unwrap();
555 assert!(node_set.contains(keep_elem));
556
557 assert!(!node_set.contains(remove_elem));
559 let deep_elem = doc
560 .descendants()
561 .find(|n| n.is_element() && n.has_tag_name("deep"))
562 .unwrap();
563 assert!(!node_set.contains(deep_elem));
564 }
565
566 #[test]
567 fn subtree_includes_comments() {
568 let xml = r#"<root><item ID="x"><!-- comment --><child/></item></root>"#;
570 let doc = Document::parse(xml).unwrap();
571 let resolver = UriReferenceResolver::new(&doc);
572
573 let data = resolver.dereference("#x").unwrap();
574 let node_set = data.into_node_set().unwrap();
575
576 for node in doc.descendants() {
577 if node.is_comment() {
578 assert!(
579 node_set.contains(node),
580 "comment should be included in #id subtree"
581 );
582 }
583 }
584 }
585
586 #[test]
587 fn xpointer_root_returns_whole_document_with_comments() {
588 let xml = "<root><!-- comment --><child/></root>";
589 let doc = Document::parse(xml).unwrap();
590 let resolver = UriReferenceResolver::new(&doc);
591
592 let data = resolver.dereference("#xpointer(/)").unwrap();
593 let node_set = data.into_node_set().unwrap();
594
595 for node in doc.descendants() {
597 if node.is_comment() {
598 assert!(
599 node_set.contains(node),
600 "comment should be included for #xpointer(/)"
601 );
602 }
603 }
604 assert!(node_set.contains(doc.root_element()));
605 }
606
607 #[test]
608 fn xpointer_id_single_quotes() {
609 let xml = r#"<root><item ID="abc">content</item></root>"#;
610 let doc = Document::parse(xml).unwrap();
611 let resolver = UriReferenceResolver::new(&doc);
612
613 let data = resolver.dereference("#xpointer(id('abc'))").unwrap();
614 let node_set = data.into_node_set().unwrap();
615
616 let elem = doc
617 .descendants()
618 .find(|n| n.attribute("ID") == Some("abc"))
619 .unwrap();
620 assert!(node_set.contains(elem));
621 }
622
623 #[test]
624 fn xpointer_id_double_quotes() {
625 let xml = r#"<root><item ID="xyz">content</item></root>"#;
626 let doc = Document::parse(xml).unwrap();
627 let resolver = UriReferenceResolver::new(&doc);
628
629 let data = resolver.dereference(r#"#xpointer(id("xyz"))"#).unwrap();
630 let node_set = data.into_node_set().unwrap();
631
632 let elem = doc
633 .descendants()
634 .find(|n| n.attribute("ID") == Some("xyz"))
635 .unwrap();
636 assert!(node_set.contains(elem));
637 }
638
639 #[test]
640 fn xpointer_id_not_found() {
641 let xml = "<root/>";
642 let doc = Document::parse(xml).unwrap();
643 let resolver = UriReferenceResolver::new(&doc);
644
645 let result = resolver.dereference("#xpointer(id('missing'))");
646 assert!(result.is_err());
647 match result.unwrap_err() {
648 TransformError::ElementNotFound(id) => assert_eq!(id, "missing"),
649 other => panic!("expected ElementNotFound, got: {other:?}"),
650 }
651 }
652
653 #[test]
654 fn xpointer_id_empty_value_rejected() {
655 let xml = "<root/>";
657 let doc = Document::parse(xml).unwrap();
658 let resolver = UriReferenceResolver::new(&doc);
659
660 let result = resolver.dereference("#xpointer(id(''))");
661 assert!(result.is_err());
662 assert!(matches!(
663 result.unwrap_err(),
664 TransformError::UnsupportedUri(_)
665 ));
666 }
667
668 #[test]
669 fn parse_xpointer_id_variants() {
670 assert_eq!(
672 super::parse_xpointer_id_fragment("xpointer(id('foo'))"),
673 Some("foo")
674 );
675 assert_eq!(
676 super::parse_xpointer_id_fragment(r#"xpointer(id("bar"))"#),
677 Some("bar")
678 );
679
680 assert_eq!(super::parse_xpointer_id_fragment("xpointer(/)"), None);
682 assert_eq!(super::parse_xpointer_id_fragment("xpointer(id(foo))"), None); assert_eq!(super::parse_xpointer_id_fragment("not-xpointer"), None);
684 assert_eq!(super::parse_xpointer_id_fragment(""), None);
685
686 assert_eq!(super::parse_xpointer_id_fragment("xpointer(id('))"), None);
688 assert_eq!(
689 super::parse_xpointer_id_fragment(r#"xpointer(id("))"#),
690 None
691 );
692 }
693
694 #[test]
695 fn same_element_multiple_id_attrs_not_duplicate() {
696 let xml = r#"<root><item ID="x" Id="x">data</item></root>"#;
700 let doc = Document::parse(xml).unwrap();
701 let resolver = UriReferenceResolver::new(&doc);
702
703 assert!(resolver.has_id("x"));
704 assert!(resolver.dereference("#x").is_ok());
705 }
706
707 #[test]
708 fn saml_style_document() {
709 let xml = r#"<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
711 xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
712 ID="_resp1">
713 <saml:Assertion ID="_assert1">
714 <saml:Subject>user@example.com</saml:Subject>
715 </saml:Assertion>
716 <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#" Id="sig1">
717 <ds:SignedInfo/>
718 </ds:Signature>
719 </samlp:Response>"#;
720
721 let doc = Document::parse(xml).unwrap();
722 let resolver = UriReferenceResolver::new(&doc);
723
724 assert!(resolver.has_id("_resp1"));
726 assert!(resolver.has_id("_assert1"));
727 assert!(resolver.has_id("sig1"));
728 assert_eq!(resolver.id_count(), 3);
729
730 let data = resolver.dereference("#_assert1").unwrap();
732 let node_set = data.into_node_set().unwrap();
733
734 let assertion = doc
736 .descendants()
737 .find(|n| n.attribute("ID") == Some("_assert1"))
738 .unwrap();
739 assert!(node_set.contains(assertion));
740
741 let subject = assertion
743 .children()
744 .find(|n| n.is_element() && n.has_tag_name("Subject"))
745 .unwrap();
746 assert!(node_set.contains(subject));
747
748 assert!(!node_set.contains(doc.root_element()));
750 }
751}