1use std::cell::Cell;
16use std::collections::HashMap;
17use std::rc::Rc;
18
19use crate::xml::dom::{Document, Node, NodeId};
20
21use crate::c14n::xml_base::{
22 XmlBaseResolutionBudget, XmlBaseResolutionError, resolve_uri_from_node_with_budget,
23};
24use crate::policy::SameDocumentIdSemantics;
25use crate::xml::{XmlIdIndex, is_xml_ncname};
26
27use super::types::{
28 NodeSet, NodeSetMaterializationBudget, TransformData, TransformError, transform_resource_limit,
29};
30
31pub(crate) fn validate_signing_reference_uri(
33 uri: &str,
34 policy: &crate::policy::SigningPolicy,
35) -> Result<(), crate::policy::PolicyViolation> {
36 if !policy.uris.references.allows(uri) {
37 return Err(crate::policy::PolicyViolation::Uri {
38 operation: "signing",
39 reason: "signing reference URI class is not permitted",
40 });
41 }
42 Ok(())
43}
44
45pub(crate) fn validate_signing_reference_request(
47 uri: &str,
48 has_external_resources: bool,
49) -> Result<(), crate::policy::PolicyViolation> {
50 if !uri.is_empty() && !uri.starts_with('#') && !has_external_resources {
51 return Err(crate::policy::PolicyViolation::Uri {
52 operation: "signing",
53 reason: "external signing references require request-scoped resource bytes",
54 });
55 }
56 Ok(())
57}
58
59struct ExternalResourceBudget {
60 remaining_total_bytes: Cell<usize>,
61 max_resource_bytes: usize,
62 max_total_bytes: usize,
63}
64
65pub(crate) enum ExternalResourceMapError {
66 Policy(crate::policy::PolicyViolation),
67 TotalLengthOverflow,
68}
69
70pub(crate) fn validate_external_resource_map(
71 resources: &HashMap<String, Vec<u8>>,
72 max_resource_bytes: usize,
73 max_total_bytes: usize,
74) -> Result<(), ExternalResourceMapError> {
75 let mut total = 0usize;
76 for bytes in resources.values() {
77 if bytes.len() > max_resource_bytes {
78 return Err(ExternalResourceMapError::Policy(
79 crate::policy::PolicyViolation::ResourceLimit {
80 resource: crate::policy::resource_name::EXTERNAL_RESOURCE_BYTES,
81 maximum: max_resource_bytes,
82 actual: bytes.len(),
83 },
84 ));
85 }
86 total = total
87 .checked_add(bytes.len())
88 .ok_or(ExternalResourceMapError::TotalLengthOverflow)?;
89 }
90 if total > max_total_bytes {
91 return Err(ExternalResourceMapError::Policy(
92 crate::policy::PolicyViolation::ResourceLimit {
93 resource: crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
94 maximum: max_total_bytes,
95 actual: total,
96 },
97 ));
98 }
99 Ok(())
100}
101
102pub(crate) struct ExternalResourceContext<'a> {
108 resources: Option<&'a HashMap<String, Vec<u8>>>,
109 budget: Rc<ExternalResourceBudget>,
110}
111
112impl<'a> ExternalResourceContext<'a> {
113 pub(crate) fn new(
114 resources: Option<&'a HashMap<String, Vec<u8>>>,
115 max_resource_bytes: usize,
116 max_total_bytes: usize,
117 ) -> Self {
118 Self {
119 resources,
120 budget: Rc::new(ExternalResourceBudget::with_limits(
121 max_resource_bytes,
122 max_total_bytes,
123 )),
124 }
125 }
126
127 pub(crate) fn is_configured(&self) -> bool {
128 self.resources.is_some()
129 }
130
131 pub(crate) fn bind<'doc>(
132 &'doc self,
133 doc: &'doc Document<'doc>,
134 registrations: &[crate::IdAttributeRegistration],
135 same_document_id_semantics: SameDocumentIdSemantics,
136 ) -> UriReferenceResolver<'doc>
137 where
138 'a: 'doc,
139 {
140 let mut resolver = UriReferenceResolver::with_id_registrations(doc, registrations)
141 .with_same_document_id_semantics(same_document_id_semantics);
142 resolver.external_resources = self.resources;
143 resolver.external_resource_budget = Rc::clone(&self.budget);
144 resolver
145 }
146}
147
148impl Default for ExternalResourceBudget {
149 fn default() -> Self {
150 Self::with_limits(
151 crate::hard_limits::EXTERNAL_RESOURCE_BYTE_CEILING,
152 crate::hard_limits::EXTERNAL_RESOURCE_TOTAL_BYTE_CEILING,
153 )
154 }
155}
156
157impl ExternalResourceBudget {
158 fn with_limits(max_resource_bytes: usize, max_total_bytes: usize) -> Self {
159 Self {
160 remaining_total_bytes: Cell::new(max_total_bytes),
161 max_resource_bytes,
162 max_total_bytes,
163 }
164 }
165
166 fn charge(&self, bytes: usize) -> Result<(), TransformError> {
167 if bytes > self.max_resource_bytes {
168 return Err(transform_resource_limit(
169 crate::policy::resource_name::EXTERNAL_RESOURCE_BYTES,
170 self.max_resource_bytes,
171 bytes,
172 ));
173 }
174 let remaining = self.remaining_total_bytes.get();
175 let Some(next) = remaining.checked_sub(bytes) else {
176 self.remaining_total_bytes.set(0);
177 return Err(transform_resource_limit(
178 crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
179 self.max_total_bytes,
180 self.max_total_bytes
181 .saturating_add(bytes.saturating_sub(remaining)),
182 ));
183 };
184 self.remaining_total_bytes.set(next);
185 Ok(())
186 }
187}
188
189pub struct UriReferenceResolver<'a> {
212 doc: &'a Document<'a>,
213 view: Option<crate::DocumentView<'a>>,
214 resource_identity: Option<String>,
215 id_registrations: Vec<crate::IdAttributeRegistration>,
216 id_index: ResolverIdIndex<'a>,
217 external_resources: Option<&'a HashMap<String, Vec<u8>>>,
218 external_resource_budget: Rc<ExternalResourceBudget>,
219 same_document_id_semantics: SameDocumentIdSemantics,
220}
221
222enum ResolverIdIndex<'a> {
223 Borrowed(XmlIdIndex<'a>),
224 Retained(HashMap<String, NodeId>),
225}
226
227#[derive(Clone, Debug, PartialEq, Eq, Hash)]
228pub(crate) enum TraversalDocumentIdentity {
229 Owned(crate::DocumentIdentity),
230 External(String),
231 Borrowed(usize),
232}
233
234impl<'a> ResolverIdIndex<'a> {
235 fn node(&self, document: &'a Document<'a>, id: &str) -> Option<Node<'a, 'a>> {
236 match self {
237 Self::Borrowed(index) => index.node(id),
238 Self::Retained(index) => index.get(id).and_then(|node| document.get_node(*node)),
239 }
240 }
241
242 fn contains(&self, id: &str) -> bool {
243 match self {
244 Self::Borrowed(index) => index.contains(id),
245 Self::Retained(index) => index.contains_key(id),
246 }
247 }
248
249 fn node_id(&self, id: &str) -> Option<NodeId> {
250 match self {
251 Self::Borrowed(index) => index.node_id(id),
252 Self::Retained(index) => index.get(id).copied(),
253 }
254 }
255
256 fn len(&self) -> usize {
257 match self {
258 Self::Borrowed(index) => index.len(),
259 Self::Retained(index) => index.len(),
260 }
261 }
262}
263
264impl<'a> UriReferenceResolver<'a> {
265 pub fn new(doc: &'a Document<'a>) -> Self {
267 Self::with_id_attrs(doc, &[])
268 }
269
270 pub fn with_id_attrs(doc: &'a Document<'a>, extra_attrs: &[&str]) -> Self {
279 let id_registrations = extra_attrs
280 .iter()
281 .map(|name| crate::IdAttributeRegistration::global(*name))
282 .collect::<Vec<_>>();
283 Self {
284 doc,
285 view: None,
286 resource_identity: None,
287 id_index: ResolverIdIndex::Borrowed(XmlIdIndex::with_registrations(
288 doc,
289 &id_registrations,
290 )),
291 id_registrations,
292 external_resources: None,
293 external_resource_budget: Rc::new(ExternalResourceBudget::default()),
294 same_document_id_semantics: SameDocumentIdSemantics::Specification,
295 }
296 }
297
298 pub fn with_id_registrations(
300 doc: &'a Document<'a>,
301 registrations: &[crate::IdAttributeRegistration],
302 ) -> Self {
303 let id_registrations = registrations.to_vec();
304 Self {
305 doc,
306 view: None,
307 resource_identity: None,
308 id_index: ResolverIdIndex::Borrowed(XmlIdIndex::with_registrations(
309 doc,
310 &id_registrations,
311 )),
312 id_registrations,
313 external_resources: None,
314 external_resource_budget: Rc::new(ExternalResourceBudget::default()),
315 same_document_id_semantics: SameDocumentIdSemantics::Specification,
316 }
317 }
318
319 pub(crate) fn with_document_view(
320 view: crate::DocumentView<'a>,
321 registrations: &[crate::IdAttributeRegistration],
322 ) -> Self {
323 let id_registrations = registrations.to_vec();
324 Self {
325 doc: view.document(),
326 view: Some(view),
327 resource_identity: None,
328 id_index: ResolverIdIndex::Retained(view.id_index(&id_registrations)),
329 id_registrations,
330 external_resources: None,
331 external_resource_budget: Rc::new(ExternalResourceBudget::default()),
332 same_document_id_semantics: SameDocumentIdSemantics::Specification,
333 }
334 }
335
336 pub(crate) fn with_same_document_id_semantics(
337 mut self,
338 semantics: SameDocumentIdSemantics,
339 ) -> Self {
340 self.same_document_id_semantics = semantics;
341 self
342 }
343
344 pub(crate) fn for_document_view<'b>(
347 &self,
348 view: crate::DocumentView<'b>,
349 ) -> UriReferenceResolver<'b>
350 where
351 'a: 'b,
352 {
353 UriReferenceResolver {
354 doc: view.document(),
355 view: Some(view),
356 resource_identity: None,
357 id_index: ResolverIdIndex::Retained(view.id_index(&self.id_registrations)),
358 id_registrations: self.id_registrations.clone(),
359 external_resources: self.external_resources,
360 external_resource_budget: Rc::clone(&self.external_resource_budget),
361 same_document_id_semantics: self.same_document_id_semantics,
362 }
363 }
364
365 pub(crate) fn for_external_document_view<'b>(
371 &self,
372 view: crate::DocumentView<'b>,
373 resource_identity: &str,
374 ) -> UriReferenceResolver<'b>
375 where
376 'a: 'b,
377 {
378 let mut resolver = self.for_document_view(view);
379 resolver.resource_identity = Some(resource_identity.to_owned());
380 resolver
381 }
382
383 pub(crate) fn traversal_document_identity(&self) -> TraversalDocumentIdentity {
384 if let Some(identity) = &self.resource_identity {
385 TraversalDocumentIdentity::External(identity.clone())
386 } else if let Some(view) = self.view {
387 TraversalDocumentIdentity::Owned(view.identity())
388 } else {
389 TraversalDocumentIdentity::Borrowed(self.doc as *const Document<'_> as usize)
390 }
391 }
392
393 pub fn with_external_resources(mut self, resources: &'a HashMap<String, Vec<u8>>) -> Self {
399 self.external_resources = Some(resources);
400 self
401 }
402
403 pub(crate) fn with_external_resource_limits(
404 mut self,
405 max_resource_bytes: usize,
406 max_total_bytes: usize,
407 ) -> Self {
408 self.external_resource_budget = Rc::new(ExternalResourceBudget::with_limits(
409 max_resource_bytes,
410 max_total_bytes,
411 ));
412 self
413 }
414
415 pub(crate) fn external_resource(&self, uri: &str) -> Result<Option<&'a [u8]>, TransformError> {
416 let Some(bytes) = self
417 .external_resources
418 .and_then(|resources| resources.get(uri))
419 else {
420 return Ok(None);
421 };
422 self.external_resource_budget.charge(bytes.len())?;
423 Ok(Some(bytes))
424 }
425
426 pub(crate) fn external_resource_identity(
427 &self,
428 uri: &str,
429 ) -> Option<crate::operation::OperationResourceIdentity> {
430 self.external_resources
431 .and_then(|resources| resources.get(uri))
432 .map(|bytes| crate::operation::OperationResourceIdentity::external(uri, bytes))
433 }
434
435 pub(crate) fn external_resource_set_identity(
436 &self,
437 ) -> crate::operation::OperationResourceIdentity {
438 use sha2::Digest;
439
440 let mut entries = self
441 .external_resources
442 .into_iter()
443 .flat_map(HashMap::iter)
444 .collect::<Vec<_>>();
445 entries.sort_unstable_by_key(|(uri, _)| *uri);
446 let mut hasher = sha2::Sha256::new();
447 for (uri, bytes) in entries {
448 hasher.update(uri.len().to_be_bytes());
449 hasher.update(uri.as_bytes());
450 hasher.update(bytes.len().to_be_bytes());
451 hasher.update(bytes);
452 }
453 crate::operation::OperationResourceIdentity::External {
454 uri: "caller-owned-external-resource-set".to_owned(),
455 fingerprint: hasher.finalize().into(),
456 }
457 }
458
459 pub fn dereference(&self, uri: &str) -> Result<TransformData<'a>, TransformError> {
472 self.dereference_with_optional_budget(uri, None)
473 }
474
475 pub(crate) fn dereference_with_budget(
476 &self,
477 uri: &str,
478 budget: &NodeSetMaterializationBudget,
479 ) -> Result<TransformData<'a>, TransformError> {
480 self.dereference_with_optional_budget(uri, Some(budget))
481 }
482
483 pub(crate) fn dereference_from_with_budget(
484 &self,
485 uri: &str,
486 origin: Node<'_, '_>,
487 budget: &NodeSetMaterializationBudget,
488 xml_base_budget: &XmlBaseResolutionBudget,
489 ) -> Result<TransformData<'a>, TransformError> {
490 if uri.is_empty() || uri.starts_with('#') {
493 return self.dereference_with_budget(uri, budget);
494 }
495 let resolved = resolve_uri_from_node_with_budget(origin, uri, xml_base_budget)
496 .map_err(map_xml_base_resolution_error)?;
497 self.dereference_with_budget(&resolved, budget)
498 }
499
500 fn dereference_with_optional_budget(
501 &self,
502 uri: &str,
503 budget: Option<&NodeSetMaterializationBudget>,
504 ) -> Result<TransformData<'a>, TransformError> {
505 if uri.is_empty() {
506 let nodes = match self.view {
510 Some(view) => NodeSet::entire_document_without_comments_from_view(view, budget)?,
511 None => match budget {
512 Some(budget) => {
513 NodeSet::entire_document_without_comments_with_budget(self.doc, budget)?
514 }
515 None => NodeSet::entire_document_without_comments(self.doc)?,
516 },
517 };
518 Ok(TransformData::NodeSet(nodes))
519 } else if let Some(fragment) = uri.strip_prefix('#') {
520 self.dereference_fragment(fragment, budget)
525 } else {
526 self.external_resource(uri)?
527 .map(|bytes| TransformData::Binary(bytes.to_vec()))
528 .ok_or_else(|| TransformError::UnsupportedUri(uri.to_string()))
529 }
530 }
531
532 fn dereference_fragment(
539 &self,
540 fragment: &str,
541 budget: Option<&NodeSetMaterializationBudget>,
542 ) -> Result<TransformData<'a>, TransformError> {
543 if fragment.is_empty() {
544 return Err(TransformError::UnsupportedUri("#".to_string()));
546 }
547
548 if fragment == "xpointer(/)" {
549 let nodes = match self.view {
553 Some(view) => NodeSet::entire_document_with_comments_from_view(view, budget)?,
554 None => match budget {
555 Some(budget) => {
556 NodeSet::entire_document_with_comments_with_budget(self.doc, budget)?
557 }
558 None => NodeSet::entire_document_with_comments(self.doc)?,
559 },
560 };
561 Ok(TransformData::NodeSet(nodes))
562 } else {
563 let (id, with_comments) = self.same_document_id_fragment(fragment)?;
564 self.resolve_id(id, budget, with_comments)
565 }
566 }
567
568 fn same_document_id_fragment<'uri>(
569 &self,
570 fragment: &'uri str,
571 ) -> Result<(&'uri str, bool), TransformError> {
572 if let Some(id) = parse_xpointer_id_fragment(fragment) {
573 if id.is_empty() {
576 return Err(TransformError::UnsupportedUri(format!("#{fragment}")));
577 }
578 return Ok((id, true));
579 }
580 if fragment.starts_with("xpointer(") {
581 return Err(TransformError::UnsupportedUri(format!("#{fragment}")));
582 }
583 match self.same_document_id_semantics {
584 SameDocumentIdSemantics::Specification if !is_xml_ncname(fragment) => {
585 return Err(TransformError::UnsupportedUri(format!("#{fragment}")));
586 }
587 SameDocumentIdSemantics::XmlSecBarename if fragment.contains('\'') => {
588 return Err(TransformError::UnsupportedUri(format!("#{fragment}")));
589 }
590 SameDocumentIdSemantics::Specification
591 | SameDocumentIdSemantics::XmlSecBarename
592 | SameDocumentIdSemantics::XmlSecVisa3d => {}
593 }
594 Ok((fragment, false))
595 }
596
597 fn resolve_id(
599 &self,
600 id: &str,
601 budget: Option<&NodeSetMaterializationBudget>,
602 with_comments: bool,
603 ) -> Result<TransformData<'a>, TransformError> {
604 match self.id_index.node(self.doc, id) {
605 Some(element) => {
606 let nodes = if let Some(view) = self.view {
607 NodeSet::subtree_from_view(view, element, with_comments, budget)?
608 } else if with_comments {
609 match budget {
610 Some(budget) => NodeSet::subtree_with_budget(element, budget)?,
611 None => NodeSet::subtree(element)?,
612 }
613 } else {
614 NodeSet::subtree_without_comments_with_budget(element, budget)?
615 };
616 Ok(TransformData::NodeSet(nodes))
617 }
618 None => Err(TransformError::ElementNotFound(id.to_string())),
619 }
620 }
621
622 pub fn has_id(&self, id: &str) -> bool {
624 self.id_index.contains(id)
625 }
626
627 pub fn node_for_id(&self, id: &str) -> Option<Node<'a, 'a>> {
632 self.id_index.node(self.doc, id)
633 }
634
635 pub fn node_for_same_document_reference(
638 &self,
639 uri: &str,
640 ) -> Result<Option<Node<'a, 'a>>, TransformError> {
641 if uri.is_empty() {
642 return Ok(Some(self.doc.root_element()));
643 }
644 Ok(self
645 .node_id_for_same_document_reference(uri)?
646 .and_then(|id| self.doc.get_node(id)))
647 }
648
649 pub(crate) fn node_id_for_same_document_reference(
652 &self,
653 uri: &str,
654 ) -> Result<Option<NodeId>, TransformError> {
655 let fragment = uri
656 .strip_prefix('#')
657 .ok_or_else(|| TransformError::UnsupportedUri(uri.to_owned()))?;
658 if fragment.is_empty() || fragment == "xpointer(/)" {
659 return Err(TransformError::UnsupportedUri(uri.to_owned()));
660 }
661 let (id, _) = self.same_document_id_fragment(fragment)?;
662 Ok(self.id_index.node_id(id))
663 }
664
665 pub(crate) fn node_for_node_id(&self, id: NodeId) -> Option<Node<'a, 'a>> {
666 self.doc.get_node(id)
667 }
668
669 pub fn id_count(&self) -> usize {
671 self.id_index.len()
672 }
673}
674
675fn map_xml_base_resolution_error(error: XmlBaseResolutionError) -> TransformError {
676 match error {
677 XmlBaseResolutionError::Components { maximum, actual } => transform_resource_limit(
678 crate::policy::resource_name::XML_BASE_COMPONENTS,
679 maximum,
680 actual,
681 ),
682 XmlBaseResolutionError::Bytes { maximum, actual } => transform_resource_limit(
683 crate::policy::resource_name::XML_BASE_RESOLUTION_BYTES,
684 maximum,
685 actual,
686 ),
687 }
688}
689
690pub(crate) fn parse_xpointer_id_fragment(fragment: &str) -> Option<&str> {
693 let inner = fragment.strip_prefix("xpointer(id(")?.strip_suffix("))")?;
694
695 if let Some(stripped) = inner.strip_prefix('\'').and_then(|s| s.strip_suffix('\'')) {
698 Some(stripped)
699 } else if let Some(stripped) = inner.strip_prefix('"').and_then(|s| s.strip_suffix('"')) {
700 Some(stripped)
701 } else {
702 None
703 }
704}
705
706#[cfg(test)]
707#[allow(clippy::unwrap_used)]
708mod tests {
709 use super::super::types::NodeSet;
710 use super::*;
711
712 #[test]
713 fn empty_same_document_node_reference_selects_the_document_element() {
714 let document = Document::parse("<root><child/></root>").unwrap();
717 let resolver = UriReferenceResolver::new(&document);
718
719 assert_eq!(
720 resolver
721 .node_for_same_document_reference("")
722 .unwrap()
723 .unwrap(),
724 document.root_element()
725 );
726 }
727
728 #[test]
729 fn empty_uri_returns_whole_document() {
730 let xml = "<root><child>text</child></root>";
731 let doc = Document::parse(xml).unwrap();
732 let resolver = UriReferenceResolver::new(&doc);
733
734 let data = resolver.dereference("").unwrap();
735 let node_set = data.into_node_set().unwrap();
736
737 let root = doc.root_element();
739 assert!(node_set.contains(root));
740 let child = root.first_child().unwrap();
741 assert!(node_set.contains(child));
742 }
743
744 #[test]
745 fn empty_uri_excludes_comments() {
746 let xml = "<root><!-- comment --><child/></root>";
747 let doc = Document::parse(xml).unwrap();
748 let resolver = UriReferenceResolver::new(&doc);
749
750 let data = resolver.dereference("").unwrap();
751 let node_set = data.into_node_set().unwrap();
752
753 for node in doc.descendants() {
755 if node.is_comment() {
756 assert!(
757 !node_set.contains(node),
758 "comment should be excluded for empty URI"
759 );
760 }
761 }
762 assert!(node_set.contains(doc.root_element()));
764 }
765
766 #[test]
767 fn fragment_uri_resolves_by_id_attr() {
768 let xml = r#"<root><item ID="abc">content</item><item ID="def">other</item></root>"#;
769 let doc = Document::parse(xml).unwrap();
770 let resolver = UriReferenceResolver::new(&doc);
771
772 let data = resolver.dereference("#abc").unwrap();
773 let node_set = data.into_node_set().unwrap();
774
775 let abc_elem = doc
777 .descendants()
778 .find(|n| n.attribute("ID") == Some("abc"))
779 .unwrap();
780 assert!(node_set.contains(abc_elem));
781
782 let text_child = abc_elem.first_child().unwrap();
784 assert!(node_set.contains(text_child));
785
786 assert!(!node_set.contains(doc.root_element()));
788
789 let def_elem = doc
791 .descendants()
792 .find(|n| n.attribute("ID") == Some("def"))
793 .unwrap();
794 assert!(!node_set.contains(def_elem));
795 }
796
797 #[test]
798 fn fragment_uri_resolves_lowercase_id() {
799 let xml = r#"<root><item id="lower">text</item></root>"#;
800 let doc = Document::parse(xml).unwrap();
801 let resolver = UriReferenceResolver::new(&doc);
802
803 let data = resolver.dereference("#lower").unwrap();
804 let node_set = data.into_node_set().unwrap();
805
806 let elem = doc
807 .descendants()
808 .find(|n| n.attribute("id") == Some("lower"))
809 .unwrap();
810 assert!(node_set.contains(elem));
811 }
812
813 #[test]
814 fn fragment_uri_resolves_mixed_case_id() {
815 let xml = r#"<root><ds:Signature Id="sig1" xmlns:ds="http://www.w3.org/2000/09/xmldsig#"/></root>"#;
816 let doc = Document::parse(xml).unwrap();
817 let resolver = UriReferenceResolver::new(&doc);
818
819 assert!(resolver.has_id("sig1"));
820 let data = resolver.dereference("#sig1").unwrap();
821 assert!(data.into_node_set().is_ok());
822 }
823
824 #[test]
825 fn fragment_uri_not_found() {
826 let xml = "<root><child>text</child></root>";
827 let doc = Document::parse(xml).unwrap();
828 let resolver = UriReferenceResolver::new(&doc);
829
830 let result = resolver.dereference("#nonexistent");
831 assert!(result.is_err());
832 match result.unwrap_err() {
833 TransformError::ElementNotFound(id) => assert_eq!(id, "nonexistent"),
834 other => panic!("expected ElementNotFound, got: {other:?}"),
835 }
836 }
837
838 #[test]
839 fn unsupported_external_uri() {
840 let xml = "<root/>";
841 let doc = Document::parse(xml).unwrap();
842 let resolver = UriReferenceResolver::new(&doc);
843
844 let result = resolver.dereference("http://example.com/doc.xml");
845 assert!(result.is_err());
846 match result.unwrap_err() {
847 TransformError::UnsupportedUri(uri) => {
848 assert_eq!(uri, "http://example.com/doc.xml")
849 }
850 other => panic!("expected UnsupportedUri, got: {other:?}"),
851 }
852 }
853
854 #[test]
855 fn unsupported_xpointer_expression() {
856 let xml = "<root/>";
859 let doc = Document::parse(xml).unwrap();
860 let resolver = UriReferenceResolver::new(&doc);
861
862 let result = resolver.dereference("#xpointer(foo())");
863 assert!(result.is_err());
864 match result.unwrap_err() {
865 TransformError::UnsupportedUri(uri) => {
866 assert_eq!(uri, "#xpointer(foo())")
867 }
868 other => panic!("expected UnsupportedUri, got: {other:?}"),
869 }
870
871 let result = resolver.dereference("#xpointer(//element)");
873 assert!(result.is_err());
874 assert!(matches!(
875 result.unwrap_err(),
876 TransformError::UnsupportedUri(_)
877 ));
878 }
879
880 #[test]
881 fn empty_fragment_rejected() {
882 let xml = "<root/>";
884 let doc = Document::parse(xml).unwrap();
885 let resolver = UriReferenceResolver::new(&doc);
886
887 let result = resolver.dereference("#");
888 assert!(result.is_err());
889 match result.unwrap_err() {
890 TransformError::UnsupportedUri(uri) => assert_eq!(uri, "#"),
891 other => panic!("expected UnsupportedUri, got: {other:?}"),
892 }
893 }
894
895 #[test]
896 fn foreign_document_node_rejected() {
897 let xml1 = "<root><child/></root>";
899 let xml2 = "<other><item/></other>";
900 let doc1 = Document::parse(xml1).unwrap();
901 let doc2 = Document::parse(xml2).unwrap();
902
903 let node_set = NodeSet::entire_document_without_comments(&doc1).unwrap();
904
905 let foreign_node = doc2.root_element();
907 assert!(
908 !node_set.contains(foreign_node),
909 "foreign document node should be rejected"
910 );
911
912 let own_node = doc1.root_element();
914 assert!(node_set.contains(own_node));
915 }
916
917 #[test]
918 fn custom_id_attr_name() {
919 let xml = r#"<root><elem myid="custom1">data</elem></root>"#;
922 let doc = Document::parse(xml).unwrap();
923
924 let resolver_default = UriReferenceResolver::new(&doc);
926 assert!(!resolver_default.has_id("custom1"));
927
928 let resolver_custom = UriReferenceResolver::with_id_attrs(&doc, &["myid"]);
930 assert!(resolver_custom.has_id("custom1"));
931
932 let data = resolver_custom.dereference("#custom1").unwrap();
933 assert!(data.into_node_set().is_ok());
934 }
935
936 #[test]
937 fn absolute_external_uri_uses_normalized_resource_identity() {
938 let xml = r#"<root xml:base="https://base.example/ignored/">
941 <reference URI="https://example.test/a/../data.bin"/>
942 </root>"#;
943 let doc = Document::parse(xml).unwrap();
944 let reference = doc
945 .descendants()
946 .find(|node| node.has_tag_name("reference"))
947 .unwrap();
948 let resources = HashMap::from([(
949 "https://example.test/data.bin".to_owned(),
950 b"payload".to_vec(),
951 )]);
952 let budget = NodeSetMaterializationBudget::default();
953 let xml_base_budget = XmlBaseResolutionBudget::default();
954 let resolver = UriReferenceResolver::new(&doc).with_external_resources(&resources);
955
956 let data = resolver
957 .dereference_from_with_budget(
958 reference.attribute("URI").unwrap(),
959 reference,
960 &budget,
961 &xml_base_budget,
962 )
963 .unwrap();
964
965 assert_eq!(data.into_binary().unwrap(), b"payload");
966 }
967
968 #[test]
969 fn absolute_external_uri_does_not_consume_xml_base_components() {
970 let xml = r#"<root xml:base="ignored/"><reference URI="https://example.test/a/../data.bin"/></root>"#;
973 let doc = Document::parse(xml).unwrap();
974 let reference = doc
975 .descendants()
976 .find(|node| node.has_tag_name("reference"))
977 .unwrap();
978 let resources = HashMap::from([(
979 "https://example.test/data.bin".to_owned(),
980 b"payload".to_vec(),
981 )]);
982 let resolver = UriReferenceResolver::new(&doc).with_external_resources(&resources);
983
984 let data = resolver
985 .dereference_from_with_budget(
986 reference.attribute("URI").unwrap(),
987 reference,
988 &NodeSetMaterializationBudget::default(),
989 &XmlBaseResolutionBudget::with_limits(0, 1_024),
990 )
991 .expect("absolute references must bypass inherited XML Base traversal");
992
993 assert_eq!(data.into_binary().unwrap(), b"payload");
994 }
995
996 #[test]
997 fn external_uri_without_xml_base_uses_normalized_resource_identity() {
998 let xml = r#"<root><reference URI="https://example.test/a/../data.bin"/></root>"#;
1001 let doc = Document::parse(xml).unwrap();
1002 let reference = doc
1003 .descendants()
1004 .find(|node| node.has_tag_name("reference"))
1005 .unwrap();
1006 let resources = HashMap::from([(
1007 "https://example.test/data.bin".to_owned(),
1008 b"payload".to_vec(),
1009 )]);
1010 let budget = NodeSetMaterializationBudget::default();
1011 let xml_base_budget = XmlBaseResolutionBudget::default();
1012 let resolver = UriReferenceResolver::new(&doc).with_external_resources(&resources);
1013
1014 let data = resolver
1015 .dereference_from_with_budget(
1016 reference.attribute("URI").unwrap(),
1017 reference,
1018 &budget,
1019 &xml_base_budget,
1020 )
1021 .expect("the normalized resource key must resolve without xml:base");
1022
1023 assert_eq!(data.into_binary().unwrap(), b"payload");
1024 }
1025
1026 #[test]
1027 fn pathless_relative_xml_base_preserves_relative_resource_identity() {
1028 let xml = r#"<root xml:base="?old">
1031 <reference URI="data.bin"/>
1032 </root>"#;
1033 let doc = Document::parse(xml).unwrap();
1034 let reference = doc
1035 .descendants()
1036 .find(|node| node.has_tag_name("reference"))
1037 .unwrap();
1038 let resources = HashMap::from([("data.bin".to_owned(), b"payload".to_vec())]);
1039 let budget = NodeSetMaterializationBudget::default();
1040 let xml_base_budget = XmlBaseResolutionBudget::default();
1041 let resolver = UriReferenceResolver::new(&doc).with_external_resources(&resources);
1042
1043 let data = resolver
1044 .dereference_from_with_budget(
1045 reference.attribute("URI").unwrap(),
1046 reference,
1047 &budget,
1048 &xml_base_budget,
1049 )
1050 .unwrap();
1051
1052 assert_eq!(data.into_binary().unwrap(), b"payload");
1053 }
1054
1055 #[test]
1056 fn relative_xml_base_normalizes_absolute_external_path() {
1057 let xml = r#"<root xml:base="a/b">
1060 <reference URI="/x/../data.bin"/>
1061 </root>"#;
1062 let doc = Document::parse(xml).unwrap();
1063 let reference = doc
1064 .descendants()
1065 .find(|node| node.has_tag_name("reference"))
1066 .unwrap();
1067 let resources = HashMap::from([("/data.bin".to_owned(), b"payload".to_vec())]);
1068 let budget = NodeSetMaterializationBudget::default();
1069 let xml_base_budget = XmlBaseResolutionBudget::default();
1070 let resolver = UriReferenceResolver::new(&doc).with_external_resources(&resources);
1071
1072 let data = resolver
1073 .dereference_from_with_budget(
1074 reference.attribute("URI").unwrap(),
1075 reference,
1076 &budget,
1077 &xml_base_budget,
1078 )
1079 .unwrap();
1080
1081 assert_eq!(data.into_binary().unwrap(), b"payload");
1082 }
1083
1084 #[test]
1085 fn network_path_xml_base_preserves_external_resource_authority() {
1086 let xml = r#"<root xml:base="//cdn.example/a/b/">
1089 <reference URI="/x/../data.bin"/>
1090 </root>"#;
1091 let doc = Document::parse(xml).unwrap();
1092 let reference = doc
1093 .descendants()
1094 .find(|node| node.has_tag_name("reference"))
1095 .unwrap();
1096 let resources = HashMap::from([("//cdn.example/data.bin".to_owned(), b"payload".to_vec())]);
1097 let budget = NodeSetMaterializationBudget::default();
1098 let xml_base_budget = XmlBaseResolutionBudget::default();
1099 let resolver = UriReferenceResolver::new(&doc).with_external_resources(&resources);
1100
1101 let data = resolver
1102 .dereference_from_with_budget(
1103 reference.attribute("URI").unwrap(),
1104 reference,
1105 &budget,
1106 &xml_base_budget,
1107 )
1108 .unwrap();
1109
1110 assert_eq!(data.into_binary().unwrap(), b"payload");
1111 }
1112
1113 #[test]
1114 fn unicode_external_uri_resolves_without_panicking() {
1115 let xml = r#"<root xml:base="https://example.test/base/">
1118 <reference URI="é?x"/>
1119 </root>"#;
1120 let doc = Document::parse(xml).unwrap();
1121 let reference = doc
1122 .descendants()
1123 .find(|node| node.has_tag_name("reference"))
1124 .unwrap();
1125 let resources = HashMap::from([(
1126 "https://example.test/base/é?x".to_owned(),
1127 b"payload".to_vec(),
1128 )]);
1129 let budget = NodeSetMaterializationBudget::default();
1130 let xml_base_budget = XmlBaseResolutionBudget::default();
1131 let resolver = UriReferenceResolver::new(&doc).with_external_resources(&resources);
1132
1133 let data = resolver
1134 .dereference_from_with_budget(
1135 reference.attribute("URI").unwrap(),
1136 reference,
1137 &budget,
1138 &xml_base_budget,
1139 )
1140 .unwrap();
1141
1142 assert_eq!(data.into_binary().unwrap(), b"payload");
1143 }
1144
1145 #[test]
1146 fn absolute_rootless_external_uri_discards_leading_parent_segment() {
1147 let xml = r#"<root xml:base="https://example.test/base/">
1148 <reference URI="urn:../payload"/>
1149 </root>"#;
1150 let doc = Document::parse(xml).unwrap();
1151 let reference = doc
1152 .descendants()
1153 .find(|node| node.has_tag_name("reference"))
1154 .unwrap();
1155 let resources = HashMap::from([("urn:payload".to_owned(), b"payload".to_vec())]);
1156 let budget = NodeSetMaterializationBudget::default();
1157 let xml_base_budget = XmlBaseResolutionBudget::default();
1158 let resolver = UriReferenceResolver::new(&doc).with_external_resources(&resources);
1159
1160 let data = resolver
1161 .dereference_from_with_budget(
1162 reference.attribute("URI").unwrap(),
1163 reference,
1164 &budget,
1165 &xml_base_budget,
1166 )
1167 .unwrap();
1168
1169 assert_eq!(data.into_binary().unwrap(), b"payload");
1170 }
1171
1172 #[test]
1173 fn namespaced_id_attr_found_by_local_name() {
1174 let xml =
1176 r#"<root><elem wsu:Id="ts1" xmlns:wsu="http://example.com/wsu">data</elem></root>"#;
1177 let doc = Document::parse(xml).unwrap();
1178
1179 let resolver = UriReferenceResolver::new(&doc);
1180 assert!(resolver.has_id("ts1"));
1181 }
1182
1183 #[test]
1184 fn id_count_reports_unique_ids() {
1185 let xml = r#"<root ID="r1"><a ID="a1"/><b Id="b1"/><c id="c1"/></root>"#;
1186 let doc = Document::parse(xml).unwrap();
1187 let resolver = UriReferenceResolver::new(&doc);
1188
1189 assert_eq!(resolver.id_count(), 4);
1191 }
1192
1193 #[test]
1194 fn duplicate_ids_are_rejected() {
1195 let xml = r#"<root><a ID="dup">first</a><b ID="dup">second</b></root>"#;
1198 let doc = Document::parse(xml).unwrap();
1199 let resolver = UriReferenceResolver::new(&doc);
1200
1201 assert!(!resolver.has_id("dup"));
1203 let result = resolver.dereference("#dup");
1204 assert!(result.is_err());
1205 assert!(matches!(
1206 result.unwrap_err(),
1207 TransformError::ElementNotFound(_)
1208 ));
1209 }
1210
1211 #[test]
1212 fn triple_duplicate_ids_stay_rejected() {
1213 let xml = r#"<root><a ID="dup">1</a><b ID="dup">2</b><c ID="dup">3</c></root>"#;
1216 let doc = Document::parse(xml).unwrap();
1217 let resolver = UriReferenceResolver::new(&doc);
1218
1219 assert!(!resolver.has_id("dup"));
1220 assert!(resolver.dereference("#dup").is_err());
1221 }
1222
1223 #[test]
1224 fn node_set_exclude_subtree() {
1225 let xml = r#"<root><keep>yes</keep><remove><deep>no</deep></remove></root>"#;
1226 let doc = Document::parse(xml).unwrap();
1227 let resolver = UriReferenceResolver::new(&doc);
1228
1229 let data = resolver.dereference("").unwrap();
1230 let mut node_set = data.into_node_set().unwrap();
1231
1232 let remove_elem = doc
1234 .descendants()
1235 .find(|n| n.is_element() && n.has_tag_name("remove"))
1236 .unwrap();
1237 node_set.exclude_subtree(remove_elem);
1238
1239 let keep_elem = doc
1241 .descendants()
1242 .find(|n| n.is_element() && n.has_tag_name("keep"))
1243 .unwrap();
1244 assert!(node_set.contains(keep_elem));
1245
1246 assert!(!node_set.contains(remove_elem));
1248 let deep_elem = doc
1249 .descendants()
1250 .find(|n| n.is_element() && n.has_tag_name("deep"))
1251 .unwrap();
1252 assert!(!node_set.contains(deep_elem));
1253 }
1254
1255 #[test]
1256 fn bare_name_subtree_excludes_comments() {
1257 let xml = r#"<root><item ID="x"><!-- comment --><child/></item></root>"#;
1259 let doc = Document::parse(xml).unwrap();
1260 let resolver = UriReferenceResolver::new(&doc);
1261
1262 let data = resolver.dereference("#x").unwrap();
1263 let node_set = data.into_node_set().unwrap();
1264
1265 for node in doc.descendants() {
1266 if node.is_comment() {
1267 assert!(
1268 !node_set.contains(node),
1269 "comment must be excluded from #id"
1270 );
1271 }
1272 }
1273 }
1274
1275 #[test]
1276 fn xpointer_root_returns_whole_document_with_comments() {
1277 let xml = "<root><!-- comment --><child/></root>";
1278 let doc = Document::parse(xml).unwrap();
1279 let resolver = UriReferenceResolver::new(&doc);
1280
1281 let data = resolver.dereference("#xpointer(/)").unwrap();
1282 let node_set = data.into_node_set().unwrap();
1283
1284 for node in doc.descendants() {
1286 if node.is_comment() {
1287 assert!(
1288 node_set.contains(node),
1289 "comment should be included for #xpointer(/)"
1290 );
1291 }
1292 }
1293 assert!(node_set.contains(doc.root_element()));
1294 }
1295
1296 #[test]
1297 fn xpointer_id_single_quotes() {
1298 let xml = r#"<root><item ID="abc"><!-- retained -->content</item></root>"#;
1300 let doc = Document::parse(xml).unwrap();
1301 let resolver = UriReferenceResolver::new(&doc);
1302
1303 let data = resolver.dereference("#xpointer(id('abc'))").unwrap();
1304 let node_set = data.into_node_set().unwrap();
1305
1306 let elem = doc
1307 .descendants()
1308 .find(|n| n.attribute("ID") == Some("abc"))
1309 .unwrap();
1310 assert!(node_set.contains(elem));
1311 assert!(
1312 elem.children()
1313 .any(|node| node.is_comment() && node_set.contains(node))
1314 );
1315 }
1316
1317 #[test]
1318 fn xpointer_id_double_quotes() {
1319 let xml = r#"<root><item ID="xyz">content</item></root>"#;
1320 let doc = Document::parse(xml).unwrap();
1321 let resolver = UriReferenceResolver::new(&doc);
1322
1323 let data = resolver.dereference(r#"#xpointer(id("xyz"))"#).unwrap();
1324 let node_set = data.into_node_set().unwrap();
1325
1326 let elem = doc
1327 .descendants()
1328 .find(|n| n.attribute("ID") == Some("xyz"))
1329 .unwrap();
1330 assert!(node_set.contains(elem));
1331 }
1332
1333 #[test]
1334 fn xpointer_id_not_found() {
1335 let xml = "<root/>";
1336 let doc = Document::parse(xml).unwrap();
1337 let resolver = UriReferenceResolver::new(&doc);
1338
1339 let result = resolver.dereference("#xpointer(id('missing'))");
1340 assert!(result.is_err());
1341 match result.unwrap_err() {
1342 TransformError::ElementNotFound(id) => assert_eq!(id, "missing"),
1343 other => panic!("expected ElementNotFound, got: {other:?}"),
1344 }
1345 }
1346
1347 #[test]
1348 fn xpointer_id_empty_value_rejected() {
1349 let xml = "<root/>";
1351 let doc = Document::parse(xml).unwrap();
1352 let resolver = UriReferenceResolver::new(&doc);
1353
1354 let result = resolver.dereference("#xpointer(id(''))");
1355 assert!(result.is_err());
1356 assert!(matches!(
1357 result.unwrap_err(),
1358 TransformError::UnsupportedUri(_)
1359 ));
1360 }
1361
1362 #[test]
1363 fn bare_fragment_rejects_non_ncname_without_visa3d_compatibility() {
1364 let xml = r#"<root><item ID="12345">content</item></root>"#;
1368 let doc = Document::parse(xml).unwrap();
1369 let resolver = UriReferenceResolver::new(&doc);
1370
1371 assert!(matches!(
1372 resolver.dereference("#12345"),
1373 Err(TransformError::UnsupportedUri(uri)) if uri == "#12345"
1374 ));
1375 }
1376
1377 #[test]
1378 fn visa3d_compatibility_resolves_non_ncname_id_directly() {
1379 let xml = r#"<root><item ID="12345">content</item></root>"#;
1382 let doc = Document::parse(xml).unwrap();
1383 let resolver = UriReferenceResolver::new(&doc)
1384 .with_same_document_id_semantics(SameDocumentIdSemantics::XmlSecVisa3d);
1385
1386 assert!(resolver.dereference("#12345").is_ok());
1387 }
1388
1389 #[test]
1390 fn xmlsec_barename_compatibility_matches_donor_literal_and_comment_semantics() {
1391 let xml = r#"<root><item ID="12345"><!-- excluded -->numeric</item><item ID="visa'3d">quoted</item></root>"#;
1395 let doc = Document::parse(xml).unwrap();
1396 let resolver = UriReferenceResolver::new(&doc)
1397 .with_same_document_id_semantics(SameDocumentIdSemantics::XmlSecBarename);
1398
1399 let nodes = resolver
1400 .dereference("#12345")
1401 .unwrap()
1402 .into_node_set()
1403 .unwrap();
1404 assert!(
1405 doc.descendants()
1406 .filter(|node| node.is_comment())
1407 .all(|node| !nodes.contains(node))
1408 );
1409 assert!(matches!(
1410 resolver.dereference("#visa'3d"),
1411 Err(TransformError::UnsupportedUri(uri)) if uri == "#visa'3d"
1412 ));
1413 }
1414
1415 #[test]
1416 fn parse_xpointer_id_variants() {
1417 assert_eq!(
1419 super::parse_xpointer_id_fragment("xpointer(id('foo'))"),
1420 Some("foo")
1421 );
1422 assert_eq!(
1423 super::parse_xpointer_id_fragment(r#"xpointer(id("bar"))"#),
1424 Some("bar")
1425 );
1426
1427 assert_eq!(super::parse_xpointer_id_fragment("xpointer(/)"), None);
1429 assert_eq!(super::parse_xpointer_id_fragment("xpointer(id(foo))"), None); assert_eq!(super::parse_xpointer_id_fragment("not-xpointer"), None);
1431 assert_eq!(super::parse_xpointer_id_fragment(""), None);
1432
1433 assert_eq!(super::parse_xpointer_id_fragment("xpointer(id('))"), None);
1435 assert_eq!(
1436 super::parse_xpointer_id_fragment(r#"xpointer(id("))"#),
1437 None
1438 );
1439 }
1440
1441 #[test]
1442 fn same_document_reference_nodes_reject_non_id_fragments() {
1443 let document = Document::parse(r#"<root><item ID="target"/></root>"#).unwrap();
1444 let resolver = UriReferenceResolver::new(&document);
1445 for uri in [
1446 "#target",
1447 "#xpointer(id('target'))",
1448 r#"#xpointer(id("target"))"#,
1449 ] {
1450 assert!(
1451 resolver
1452 .node_id_for_same_document_reference(uri)
1453 .unwrap()
1454 .is_some()
1455 );
1456 }
1457 for uri in [
1458 "",
1459 "target",
1460 "#",
1461 "#xpointer(/)",
1462 "#xpointer(id(''))",
1463 "#xpointer(id(target))",
1464 ] {
1465 assert!(
1466 resolver.node_id_for_same_document_reference(uri).is_err(),
1467 "{uri}"
1468 );
1469 }
1470 }
1471
1472 #[test]
1473 fn same_element_multiple_id_attrs_not_duplicate() {
1474 let xml = r#"<root><item ID="x" Id="x">data</item></root>"#;
1478 let doc = Document::parse(xml).unwrap();
1479 let resolver = UriReferenceResolver::new(&doc);
1480
1481 assert!(resolver.has_id("x"));
1482 assert!(resolver.dereference("#x").is_ok());
1483 }
1484
1485 #[test]
1486 fn saml_style_document() {
1487 let xml = r#"<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
1489 xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
1490 ID="_resp1">
1491 <saml:Assertion ID="_assert1">
1492 <saml:Subject>user@example.com</saml:Subject>
1493 </saml:Assertion>
1494 <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#" Id="sig1">
1495 <ds:SignedInfo/>
1496 </ds:Signature>
1497 </samlp:Response>"#;
1498
1499 let doc = Document::parse(xml).unwrap();
1500 let resolver = UriReferenceResolver::new(&doc);
1501
1502 assert!(resolver.has_id("_resp1"));
1504 assert!(resolver.has_id("_assert1"));
1505 assert!(resolver.has_id("sig1"));
1506 assert_eq!(resolver.id_count(), 3);
1507
1508 let data = resolver.dereference("#_assert1").unwrap();
1510 let node_set = data.into_node_set().unwrap();
1511
1512 let assertion = doc
1514 .descendants()
1515 .find(|n| n.attribute("ID") == Some("_assert1"))
1516 .unwrap();
1517 assert!(node_set.contains(assertion));
1518
1519 let subject = assertion
1521 .children()
1522 .find(|n| n.is_element() && n.has_tag_name("Subject"))
1523 .unwrap();
1524 assert!(node_set.contains(subject));
1525
1526 assert!(!node_set.contains(doc.root_element()));
1528 }
1529}