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
564 .same_document_id_fragment(fragment)
565 .ok_or_else(|| TransformError::UnsupportedUri(format!("#{fragment}")))?;
566 self.resolve_id(id, budget, with_comments)
567 }
568 }
569
570 fn same_document_id_fragment<'uri>(&self, fragment: &'uri str) -> Option<(&'uri str, bool)> {
571 if let Some(id) = parse_xpointer_id_fragment(fragment) {
572 if id.is_empty() {
575 return None;
576 }
577 return Some((id, true));
578 }
579 if fragment.starts_with("xpointer(") {
580 return None;
581 }
582 match self.same_document_id_semantics {
583 SameDocumentIdSemantics::Specification if !is_xml_ncname(fragment) => {
584 return None;
585 }
586 SameDocumentIdSemantics::XmlSecBarename if fragment.contains('\'') => {
587 return None;
588 }
589 SameDocumentIdSemantics::Specification
590 | SameDocumentIdSemantics::XmlSecBarename
591 | SameDocumentIdSemantics::XmlSecVisa3d => {}
592 }
593 Some((fragment, false))
594 }
595
596 fn resolve_id(
598 &self,
599 id: &str,
600 budget: Option<&NodeSetMaterializationBudget>,
601 with_comments: bool,
602 ) -> Result<TransformData<'a>, TransformError> {
603 match self.id_index.node(self.doc, id) {
604 Some(element) => {
605 let nodes = if let Some(view) = self.view {
606 NodeSet::subtree_from_view(view, element, with_comments, budget)?
607 } else if with_comments {
608 match budget {
609 Some(budget) => NodeSet::subtree_with_budget(element, budget)?,
610 None => NodeSet::subtree(element)?,
611 }
612 } else {
613 NodeSet::subtree_without_comments_with_budget(element, budget)?
614 };
615 Ok(TransformData::NodeSet(nodes))
616 }
617 None => Err(TransformError::ElementNotFound(id.to_string())),
618 }
619 }
620
621 pub fn has_id(&self, id: &str) -> bool {
623 self.id_index.contains(id)
624 }
625
626 pub fn node_for_id(&self, id: &str) -> Option<Node<'a, 'a>> {
631 self.id_index.node(self.doc, id)
632 }
633
634 pub fn node_for_same_document_reference(
637 &self,
638 uri: &str,
639 ) -> Result<Option<Node<'a, 'a>>, TransformError> {
640 if uri.is_empty() {
641 return Ok(Some(self.doc.root_element()));
642 }
643 Ok(self
644 .node_id_for_same_document_reference(uri)?
645 .and_then(|id| self.doc.get_node(id)))
646 }
647
648 #[cfg(feature = "xmlenc")]
651 pub(crate) fn same_document_reference_targets(&self, uri: &str, target: NodeId) -> bool {
652 if uri.is_empty() {
653 return self.doc.root_element().id() == target;
654 }
655 let Some(fragment) = uri.strip_prefix('#') else {
656 return false;
657 };
658 if fragment.is_empty() || fragment == "xpointer(/)" {
659 return false;
660 }
661 let Some((id, _)) = self.same_document_id_fragment(fragment) else {
662 return false;
663 };
664 self.id_index.node_id(id) == Some(target)
665 }
666
667 pub(crate) fn node_id_for_same_document_reference(
670 &self,
671 uri: &str,
672 ) -> Result<Option<NodeId>, TransformError> {
673 let fragment = uri
674 .strip_prefix('#')
675 .ok_or_else(|| TransformError::UnsupportedUri(uri.to_owned()))?;
676 if fragment.is_empty() || fragment == "xpointer(/)" {
677 return Err(TransformError::UnsupportedUri(uri.to_owned()));
678 }
679 let (id, _) = self
680 .same_document_id_fragment(fragment)
681 .ok_or_else(|| TransformError::UnsupportedUri(uri.to_owned()))?;
682 Ok(self.id_index.node_id(id))
683 }
684
685 pub(crate) fn node_for_node_id(&self, id: NodeId) -> Option<Node<'a, 'a>> {
686 self.doc.get_node(id)
687 }
688
689 pub fn id_count(&self) -> usize {
691 self.id_index.len()
692 }
693}
694
695fn map_xml_base_resolution_error(error: XmlBaseResolutionError) -> TransformError {
696 match error {
697 XmlBaseResolutionError::Components { maximum, actual } => transform_resource_limit(
698 crate::policy::resource_name::XML_BASE_COMPONENTS,
699 maximum,
700 actual,
701 ),
702 XmlBaseResolutionError::Bytes { maximum, actual } => transform_resource_limit(
703 crate::policy::resource_name::XML_BASE_RESOLUTION_BYTES,
704 maximum,
705 actual,
706 ),
707 }
708}
709
710pub(crate) fn parse_xpointer_id_fragment(fragment: &str) -> Option<&str> {
713 let inner = fragment.strip_prefix("xpointer(id(")?.strip_suffix("))")?;
714
715 if let Some(stripped) = inner.strip_prefix('\'').and_then(|s| s.strip_suffix('\'')) {
718 Some(stripped)
719 } else if let Some(stripped) = inner.strip_prefix('"').and_then(|s| s.strip_suffix('"')) {
720 Some(stripped)
721 } else {
722 None
723 }
724}
725
726#[cfg(test)]
727#[allow(clippy::unwrap_used)]
728mod tests {
729 use super::super::types::NodeSet;
730 use super::*;
731
732 #[test]
733 fn empty_same_document_node_reference_selects_the_document_element() {
734 let document = Document::parse("<root><child/></root>").unwrap();
737 let resolver = UriReferenceResolver::new(&document);
738
739 assert_eq!(
740 resolver
741 .node_for_same_document_reference("")
742 .unwrap()
743 .unwrap(),
744 document.root_element()
745 );
746 }
747
748 #[test]
749 fn empty_uri_returns_whole_document() {
750 let xml = "<root><child>text</child></root>";
751 let doc = Document::parse(xml).unwrap();
752 let resolver = UriReferenceResolver::new(&doc);
753
754 let data = resolver.dereference("").unwrap();
755 let node_set = data.into_node_set().unwrap();
756
757 let root = doc.root_element();
759 assert!(node_set.contains(root));
760 let child = root.first_child().unwrap();
761 assert!(node_set.contains(child));
762 }
763
764 #[test]
765 fn empty_uri_excludes_comments() {
766 let xml = "<root><!-- comment --><child/></root>";
767 let doc = Document::parse(xml).unwrap();
768 let resolver = UriReferenceResolver::new(&doc);
769
770 let data = resolver.dereference("").unwrap();
771 let node_set = data.into_node_set().unwrap();
772
773 for node in doc.descendants() {
775 if node.is_comment() {
776 assert!(
777 !node_set.contains(node),
778 "comment should be excluded for empty URI"
779 );
780 }
781 }
782 assert!(node_set.contains(doc.root_element()));
784 }
785
786 #[test]
787 fn fragment_uri_resolves_by_id_attr() {
788 let xml = r#"<root><item ID="abc">content</item><item ID="def">other</item></root>"#;
789 let doc = Document::parse(xml).unwrap();
790 let resolver = UriReferenceResolver::new(&doc);
791
792 let data = resolver.dereference("#abc").unwrap();
793 let node_set = data.into_node_set().unwrap();
794
795 let abc_elem = doc
797 .descendants()
798 .find(|n| n.attribute("ID") == Some("abc"))
799 .unwrap();
800 assert!(node_set.contains(abc_elem));
801
802 let text_child = abc_elem.first_child().unwrap();
804 assert!(node_set.contains(text_child));
805
806 assert!(!node_set.contains(doc.root_element()));
808
809 let def_elem = doc
811 .descendants()
812 .find(|n| n.attribute("ID") == Some("def"))
813 .unwrap();
814 assert!(!node_set.contains(def_elem));
815 }
816
817 #[test]
818 fn fragment_uri_resolves_lowercase_id() {
819 let xml = r#"<root><item id="lower">text</item></root>"#;
820 let doc = Document::parse(xml).unwrap();
821 let resolver = UriReferenceResolver::new(&doc);
822
823 let data = resolver.dereference("#lower").unwrap();
824 let node_set = data.into_node_set().unwrap();
825
826 let elem = doc
827 .descendants()
828 .find(|n| n.attribute("id") == Some("lower"))
829 .unwrap();
830 assert!(node_set.contains(elem));
831 }
832
833 #[test]
834 fn fragment_uri_resolves_mixed_case_id() {
835 let xml = r#"<root><ds:Signature Id="sig1" xmlns:ds="http://www.w3.org/2000/09/xmldsig#"/></root>"#;
836 let doc = Document::parse(xml).unwrap();
837 let resolver = UriReferenceResolver::new(&doc);
838
839 assert!(resolver.has_id("sig1"));
840 let data = resolver.dereference("#sig1").unwrap();
841 assert!(data.into_node_set().is_ok());
842 }
843
844 #[test]
845 fn fragment_uri_not_found() {
846 let xml = "<root><child>text</child></root>";
847 let doc = Document::parse(xml).unwrap();
848 let resolver = UriReferenceResolver::new(&doc);
849
850 let result = resolver.dereference("#nonexistent");
851 assert!(result.is_err());
852 match result.unwrap_err() {
853 TransformError::ElementNotFound(id) => assert_eq!(id, "nonexistent"),
854 other => panic!("expected ElementNotFound, got: {other:?}"),
855 }
856 }
857
858 #[test]
859 fn unsupported_external_uri() {
860 let xml = "<root/>";
861 let doc = Document::parse(xml).unwrap();
862 let resolver = UriReferenceResolver::new(&doc);
863
864 let result = resolver.dereference("http://example.com/doc.xml");
865 assert!(result.is_err());
866 match result.unwrap_err() {
867 TransformError::UnsupportedUri(uri) => {
868 assert_eq!(uri, "http://example.com/doc.xml")
869 }
870 other => panic!("expected UnsupportedUri, got: {other:?}"),
871 }
872 }
873
874 #[test]
875 fn unsupported_xpointer_expression() {
876 let xml = "<root/>";
879 let doc = Document::parse(xml).unwrap();
880 let resolver = UriReferenceResolver::new(&doc);
881
882 let result = resolver.dereference("#xpointer(foo())");
883 assert!(result.is_err());
884 match result.unwrap_err() {
885 TransformError::UnsupportedUri(uri) => {
886 assert_eq!(uri, "#xpointer(foo())")
887 }
888 other => panic!("expected UnsupportedUri, got: {other:?}"),
889 }
890
891 let result = resolver.dereference("#xpointer(//element)");
893 assert!(result.is_err());
894 assert!(matches!(
895 result.unwrap_err(),
896 TransformError::UnsupportedUri(_)
897 ));
898 }
899
900 #[test]
901 fn empty_fragment_rejected() {
902 let xml = "<root/>";
904 let doc = Document::parse(xml).unwrap();
905 let resolver = UriReferenceResolver::new(&doc);
906
907 let result = resolver.dereference("#");
908 assert!(result.is_err());
909 match result.unwrap_err() {
910 TransformError::UnsupportedUri(uri) => assert_eq!(uri, "#"),
911 other => panic!("expected UnsupportedUri, got: {other:?}"),
912 }
913 }
914
915 #[test]
916 fn foreign_document_node_rejected() {
917 let xml1 = "<root><child/></root>";
919 let xml2 = "<other><item/></other>";
920 let doc1 = Document::parse(xml1).unwrap();
921 let doc2 = Document::parse(xml2).unwrap();
922
923 let node_set = NodeSet::entire_document_without_comments(&doc1).unwrap();
924
925 let foreign_node = doc2.root_element();
927 assert!(
928 !node_set.contains(foreign_node),
929 "foreign document node should be rejected"
930 );
931
932 let own_node = doc1.root_element();
934 assert!(node_set.contains(own_node));
935 }
936
937 #[test]
938 fn custom_id_attr_name() {
939 let xml = r#"<root><elem myid="custom1">data</elem></root>"#;
942 let doc = Document::parse(xml).unwrap();
943
944 let resolver_default = UriReferenceResolver::new(&doc);
946 assert!(!resolver_default.has_id("custom1"));
947
948 let resolver_custom = UriReferenceResolver::with_id_attrs(&doc, &["myid"]);
950 assert!(resolver_custom.has_id("custom1"));
951
952 let data = resolver_custom.dereference("#custom1").unwrap();
953 assert!(data.into_node_set().is_ok());
954 }
955
956 #[test]
957 fn absolute_external_uri_uses_normalized_resource_identity() {
958 let xml = r#"<root xml:base="https://base.example/ignored/">
961 <reference URI="https://example.test/a/../data.bin"/>
962 </root>"#;
963 let doc = Document::parse(xml).unwrap();
964 let reference = doc
965 .descendants()
966 .find(|node| node.has_tag_name("reference"))
967 .unwrap();
968 let resources = HashMap::from([(
969 "https://example.test/data.bin".to_owned(),
970 b"payload".to_vec(),
971 )]);
972 let budget = NodeSetMaterializationBudget::default();
973 let xml_base_budget = XmlBaseResolutionBudget::default();
974 let resolver = UriReferenceResolver::new(&doc).with_external_resources(&resources);
975
976 let data = resolver
977 .dereference_from_with_budget(
978 reference.attribute("URI").unwrap(),
979 reference,
980 &budget,
981 &xml_base_budget,
982 )
983 .unwrap();
984
985 assert_eq!(data.into_binary().unwrap(), b"payload");
986 }
987
988 #[test]
989 fn absolute_external_uri_does_not_consume_xml_base_components() {
990 let xml = r#"<root xml:base="ignored/"><reference URI="https://example.test/a/../data.bin"/></root>"#;
993 let doc = Document::parse(xml).unwrap();
994 let reference = doc
995 .descendants()
996 .find(|node| node.has_tag_name("reference"))
997 .unwrap();
998 let resources = HashMap::from([(
999 "https://example.test/data.bin".to_owned(),
1000 b"payload".to_vec(),
1001 )]);
1002 let resolver = UriReferenceResolver::new(&doc).with_external_resources(&resources);
1003
1004 let data = resolver
1005 .dereference_from_with_budget(
1006 reference.attribute("URI").unwrap(),
1007 reference,
1008 &NodeSetMaterializationBudget::default(),
1009 &XmlBaseResolutionBudget::with_limits(0, 1_024),
1010 )
1011 .expect("absolute references must bypass inherited XML Base traversal");
1012
1013 assert_eq!(data.into_binary().unwrap(), b"payload");
1014 }
1015
1016 #[test]
1017 fn external_uri_without_xml_base_uses_normalized_resource_identity() {
1018 let xml = r#"<root><reference URI="https://example.test/a/../data.bin"/></root>"#;
1021 let doc = Document::parse(xml).unwrap();
1022 let reference = doc
1023 .descendants()
1024 .find(|node| node.has_tag_name("reference"))
1025 .unwrap();
1026 let resources = HashMap::from([(
1027 "https://example.test/data.bin".to_owned(),
1028 b"payload".to_vec(),
1029 )]);
1030 let budget = NodeSetMaterializationBudget::default();
1031 let xml_base_budget = XmlBaseResolutionBudget::default();
1032 let resolver = UriReferenceResolver::new(&doc).with_external_resources(&resources);
1033
1034 let data = resolver
1035 .dereference_from_with_budget(
1036 reference.attribute("URI").unwrap(),
1037 reference,
1038 &budget,
1039 &xml_base_budget,
1040 )
1041 .expect("the normalized resource key must resolve without xml:base");
1042
1043 assert_eq!(data.into_binary().unwrap(), b"payload");
1044 }
1045
1046 #[test]
1047 fn pathless_relative_xml_base_preserves_relative_resource_identity() {
1048 let xml = r#"<root xml:base="?old">
1051 <reference URI="data.bin"/>
1052 </root>"#;
1053 let doc = Document::parse(xml).unwrap();
1054 let reference = doc
1055 .descendants()
1056 .find(|node| node.has_tag_name("reference"))
1057 .unwrap();
1058 let resources = HashMap::from([("data.bin".to_owned(), b"payload".to_vec())]);
1059 let budget = NodeSetMaterializationBudget::default();
1060 let xml_base_budget = XmlBaseResolutionBudget::default();
1061 let resolver = UriReferenceResolver::new(&doc).with_external_resources(&resources);
1062
1063 let data = resolver
1064 .dereference_from_with_budget(
1065 reference.attribute("URI").unwrap(),
1066 reference,
1067 &budget,
1068 &xml_base_budget,
1069 )
1070 .unwrap();
1071
1072 assert_eq!(data.into_binary().unwrap(), b"payload");
1073 }
1074
1075 #[test]
1076 fn relative_xml_base_normalizes_absolute_external_path() {
1077 let xml = r#"<root xml:base="a/b">
1080 <reference URI="/x/../data.bin"/>
1081 </root>"#;
1082 let doc = Document::parse(xml).unwrap();
1083 let reference = doc
1084 .descendants()
1085 .find(|node| node.has_tag_name("reference"))
1086 .unwrap();
1087 let resources = HashMap::from([("/data.bin".to_owned(), b"payload".to_vec())]);
1088 let budget = NodeSetMaterializationBudget::default();
1089 let xml_base_budget = XmlBaseResolutionBudget::default();
1090 let resolver = UriReferenceResolver::new(&doc).with_external_resources(&resources);
1091
1092 let data = resolver
1093 .dereference_from_with_budget(
1094 reference.attribute("URI").unwrap(),
1095 reference,
1096 &budget,
1097 &xml_base_budget,
1098 )
1099 .unwrap();
1100
1101 assert_eq!(data.into_binary().unwrap(), b"payload");
1102 }
1103
1104 #[test]
1105 fn network_path_xml_base_preserves_external_resource_authority() {
1106 let xml = r#"<root xml:base="//cdn.example/a/b/">
1109 <reference URI="/x/../data.bin"/>
1110 </root>"#;
1111 let doc = Document::parse(xml).unwrap();
1112 let reference = doc
1113 .descendants()
1114 .find(|node| node.has_tag_name("reference"))
1115 .unwrap();
1116 let resources = HashMap::from([("//cdn.example/data.bin".to_owned(), b"payload".to_vec())]);
1117 let budget = NodeSetMaterializationBudget::default();
1118 let xml_base_budget = XmlBaseResolutionBudget::default();
1119 let resolver = UriReferenceResolver::new(&doc).with_external_resources(&resources);
1120
1121 let data = resolver
1122 .dereference_from_with_budget(
1123 reference.attribute("URI").unwrap(),
1124 reference,
1125 &budget,
1126 &xml_base_budget,
1127 )
1128 .unwrap();
1129
1130 assert_eq!(data.into_binary().unwrap(), b"payload");
1131 }
1132
1133 #[test]
1134 fn unicode_external_uri_resolves_without_panicking() {
1135 let xml = r#"<root xml:base="https://example.test/base/">
1138 <reference URI="é?x"/>
1139 </root>"#;
1140 let doc = Document::parse(xml).unwrap();
1141 let reference = doc
1142 .descendants()
1143 .find(|node| node.has_tag_name("reference"))
1144 .unwrap();
1145 let resources = HashMap::from([(
1146 "https://example.test/base/é?x".to_owned(),
1147 b"payload".to_vec(),
1148 )]);
1149 let budget = NodeSetMaterializationBudget::default();
1150 let xml_base_budget = XmlBaseResolutionBudget::default();
1151 let resolver = UriReferenceResolver::new(&doc).with_external_resources(&resources);
1152
1153 let data = resolver
1154 .dereference_from_with_budget(
1155 reference.attribute("URI").unwrap(),
1156 reference,
1157 &budget,
1158 &xml_base_budget,
1159 )
1160 .unwrap();
1161
1162 assert_eq!(data.into_binary().unwrap(), b"payload");
1163 }
1164
1165 #[test]
1166 fn absolute_rootless_external_uri_discards_leading_parent_segment() {
1167 let xml = r#"<root xml:base="https://example.test/base/">
1168 <reference URI="urn:../payload"/>
1169 </root>"#;
1170 let doc = Document::parse(xml).unwrap();
1171 let reference = doc
1172 .descendants()
1173 .find(|node| node.has_tag_name("reference"))
1174 .unwrap();
1175 let resources = HashMap::from([("urn:payload".to_owned(), b"payload".to_vec())]);
1176 let budget = NodeSetMaterializationBudget::default();
1177 let xml_base_budget = XmlBaseResolutionBudget::default();
1178 let resolver = UriReferenceResolver::new(&doc).with_external_resources(&resources);
1179
1180 let data = resolver
1181 .dereference_from_with_budget(
1182 reference.attribute("URI").unwrap(),
1183 reference,
1184 &budget,
1185 &xml_base_budget,
1186 )
1187 .unwrap();
1188
1189 assert_eq!(data.into_binary().unwrap(), b"payload");
1190 }
1191
1192 #[test]
1193 fn namespaced_id_attr_found_by_local_name() {
1194 let xml =
1196 r#"<root><elem wsu:Id="ts1" xmlns:wsu="http://example.com/wsu">data</elem></root>"#;
1197 let doc = Document::parse(xml).unwrap();
1198
1199 let resolver = UriReferenceResolver::new(&doc);
1200 assert!(resolver.has_id("ts1"));
1201 }
1202
1203 #[test]
1204 fn id_count_reports_unique_ids() {
1205 let xml = r#"<root ID="r1"><a ID="a1"/><b Id="b1"/><c id="c1"/></root>"#;
1206 let doc = Document::parse(xml).unwrap();
1207 let resolver = UriReferenceResolver::new(&doc);
1208
1209 assert_eq!(resolver.id_count(), 4);
1211 }
1212
1213 #[test]
1214 fn duplicate_ids_are_rejected() {
1215 let xml = r#"<root><a ID="dup">first</a><b ID="dup">second</b></root>"#;
1218 let doc = Document::parse(xml).unwrap();
1219 let resolver = UriReferenceResolver::new(&doc);
1220
1221 assert!(!resolver.has_id("dup"));
1223 let result = resolver.dereference("#dup");
1224 assert!(result.is_err());
1225 assert!(matches!(
1226 result.unwrap_err(),
1227 TransformError::ElementNotFound(_)
1228 ));
1229 }
1230
1231 #[test]
1232 fn triple_duplicate_ids_stay_rejected() {
1233 let xml = r#"<root><a ID="dup">1</a><b ID="dup">2</b><c ID="dup">3</c></root>"#;
1236 let doc = Document::parse(xml).unwrap();
1237 let resolver = UriReferenceResolver::new(&doc);
1238
1239 assert!(!resolver.has_id("dup"));
1240 assert!(resolver.dereference("#dup").is_err());
1241 }
1242
1243 #[test]
1244 fn node_set_exclude_subtree() {
1245 let xml = r#"<root><keep>yes</keep><remove><deep>no</deep></remove></root>"#;
1246 let doc = Document::parse(xml).unwrap();
1247 let resolver = UriReferenceResolver::new(&doc);
1248
1249 let data = resolver.dereference("").unwrap();
1250 let mut node_set = data.into_node_set().unwrap();
1251
1252 let remove_elem = doc
1254 .descendants()
1255 .find(|n| n.is_element() && n.has_tag_name("remove"))
1256 .unwrap();
1257 node_set.exclude_subtree(remove_elem);
1258
1259 let keep_elem = doc
1261 .descendants()
1262 .find(|n| n.is_element() && n.has_tag_name("keep"))
1263 .unwrap();
1264 assert!(node_set.contains(keep_elem));
1265
1266 assert!(!node_set.contains(remove_elem));
1268 let deep_elem = doc
1269 .descendants()
1270 .find(|n| n.is_element() && n.has_tag_name("deep"))
1271 .unwrap();
1272 assert!(!node_set.contains(deep_elem));
1273 }
1274
1275 #[test]
1276 fn bare_name_subtree_excludes_comments() {
1277 let xml = r#"<root><item ID="x"><!-- comment --><child/></item></root>"#;
1279 let doc = Document::parse(xml).unwrap();
1280 let resolver = UriReferenceResolver::new(&doc);
1281
1282 let data = resolver.dereference("#x").unwrap();
1283 let node_set = data.into_node_set().unwrap();
1284
1285 for node in doc.descendants() {
1286 if node.is_comment() {
1287 assert!(
1288 !node_set.contains(node),
1289 "comment must be excluded from #id"
1290 );
1291 }
1292 }
1293 }
1294
1295 #[test]
1296 fn xpointer_root_returns_whole_document_with_comments() {
1297 let xml = "<root><!-- comment --><child/></root>";
1298 let doc = Document::parse(xml).unwrap();
1299 let resolver = UriReferenceResolver::new(&doc);
1300
1301 let data = resolver.dereference("#xpointer(/)").unwrap();
1302 let node_set = data.into_node_set().unwrap();
1303
1304 for node in doc.descendants() {
1306 if node.is_comment() {
1307 assert!(
1308 node_set.contains(node),
1309 "comment should be included for #xpointer(/)"
1310 );
1311 }
1312 }
1313 assert!(node_set.contains(doc.root_element()));
1314 }
1315
1316 #[test]
1317 fn xpointer_id_single_quotes() {
1318 let xml = r#"<root><item ID="abc"><!-- retained -->content</item></root>"#;
1320 let doc = Document::parse(xml).unwrap();
1321 let resolver = UriReferenceResolver::new(&doc);
1322
1323 let data = resolver.dereference("#xpointer(id('abc'))").unwrap();
1324 let node_set = data.into_node_set().unwrap();
1325
1326 let elem = doc
1327 .descendants()
1328 .find(|n| n.attribute("ID") == Some("abc"))
1329 .unwrap();
1330 assert!(node_set.contains(elem));
1331 assert!(
1332 elem.children()
1333 .any(|node| node.is_comment() && node_set.contains(node))
1334 );
1335 }
1336
1337 #[test]
1338 fn xpointer_id_double_quotes() {
1339 let xml = r#"<root><item ID="xyz">content</item></root>"#;
1340 let doc = Document::parse(xml).unwrap();
1341 let resolver = UriReferenceResolver::new(&doc);
1342
1343 let data = resolver.dereference(r#"#xpointer(id("xyz"))"#).unwrap();
1344 let node_set = data.into_node_set().unwrap();
1345
1346 let elem = doc
1347 .descendants()
1348 .find(|n| n.attribute("ID") == Some("xyz"))
1349 .unwrap();
1350 assert!(node_set.contains(elem));
1351 }
1352
1353 #[test]
1354 fn xpointer_id_not_found() {
1355 let xml = "<root/>";
1356 let doc = Document::parse(xml).unwrap();
1357 let resolver = UriReferenceResolver::new(&doc);
1358
1359 let result = resolver.dereference("#xpointer(id('missing'))");
1360 assert!(result.is_err());
1361 match result.unwrap_err() {
1362 TransformError::ElementNotFound(id) => assert_eq!(id, "missing"),
1363 other => panic!("expected ElementNotFound, got: {other:?}"),
1364 }
1365 }
1366
1367 #[test]
1368 fn xpointer_id_empty_value_rejected() {
1369 let xml = "<root/>";
1371 let doc = Document::parse(xml).unwrap();
1372 let resolver = UriReferenceResolver::new(&doc);
1373
1374 let result = resolver.dereference("#xpointer(id(''))");
1375 assert!(result.is_err());
1376 assert!(matches!(
1377 result.unwrap_err(),
1378 TransformError::UnsupportedUri(_)
1379 ));
1380 }
1381
1382 #[test]
1383 fn bare_fragment_rejects_non_ncname_without_visa3d_compatibility() {
1384 let xml = r#"<root><item ID="12345">content</item></root>"#;
1388 let doc = Document::parse(xml).unwrap();
1389 let resolver = UriReferenceResolver::new(&doc);
1390
1391 assert!(matches!(
1392 resolver.dereference("#12345"),
1393 Err(TransformError::UnsupportedUri(uri)) if uri == "#12345"
1394 ));
1395 }
1396
1397 #[test]
1398 fn visa3d_compatibility_resolves_non_ncname_id_directly() {
1399 let xml = r#"<root><item ID="12345">content</item></root>"#;
1402 let doc = Document::parse(xml).unwrap();
1403 let resolver = UriReferenceResolver::new(&doc)
1404 .with_same_document_id_semantics(SameDocumentIdSemantics::XmlSecVisa3d);
1405
1406 assert!(resolver.dereference("#12345").is_ok());
1407 }
1408
1409 #[test]
1410 fn xmlsec_barename_compatibility_matches_donor_literal_and_comment_semantics() {
1411 let xml = r#"<root><item ID="12345"><!-- excluded -->numeric</item><item ID="visa'3d">quoted</item></root>"#;
1415 let doc = Document::parse(xml).unwrap();
1416 let resolver = UriReferenceResolver::new(&doc)
1417 .with_same_document_id_semantics(SameDocumentIdSemantics::XmlSecBarename);
1418
1419 let nodes = resolver
1420 .dereference("#12345")
1421 .unwrap()
1422 .into_node_set()
1423 .unwrap();
1424 assert!(
1425 doc.descendants()
1426 .filter(|node| node.is_comment())
1427 .all(|node| !nodes.contains(node))
1428 );
1429 assert!(matches!(
1430 resolver.dereference("#visa'3d"),
1431 Err(TransformError::UnsupportedUri(uri)) if uri == "#visa'3d"
1432 ));
1433 }
1434
1435 #[test]
1436 fn parse_xpointer_id_variants() {
1437 assert_eq!(
1439 super::parse_xpointer_id_fragment("xpointer(id('foo'))"),
1440 Some("foo")
1441 );
1442 assert_eq!(
1443 super::parse_xpointer_id_fragment(r#"xpointer(id("bar"))"#),
1444 Some("bar")
1445 );
1446
1447 assert_eq!(super::parse_xpointer_id_fragment("xpointer(/)"), None);
1449 assert_eq!(super::parse_xpointer_id_fragment("xpointer(id(foo))"), None); assert_eq!(super::parse_xpointer_id_fragment("not-xpointer"), None);
1451 assert_eq!(super::parse_xpointer_id_fragment(""), None);
1452
1453 assert_eq!(super::parse_xpointer_id_fragment("xpointer(id('))"), None);
1455 assert_eq!(
1456 super::parse_xpointer_id_fragment(r#"xpointer(id("))"#),
1457 None
1458 );
1459 }
1460
1461 #[test]
1462 fn same_document_reference_nodes_reject_non_id_fragments() {
1463 let document = Document::parse(r#"<root><item ID="target"/></root>"#).unwrap();
1464 let resolver = UriReferenceResolver::new(&document);
1465 for uri in [
1466 "#target",
1467 "#xpointer(id('target'))",
1468 r#"#xpointer(id("target"))"#,
1469 ] {
1470 assert!(
1471 resolver
1472 .node_id_for_same_document_reference(uri)
1473 .unwrap()
1474 .is_some()
1475 );
1476 }
1477 for uri in [
1478 "",
1479 "target",
1480 "#",
1481 "#xpointer(/)",
1482 "#xpointer(id(''))",
1483 "#xpointer(id(target))",
1484 ] {
1485 assert!(
1486 resolver.node_id_for_same_document_reference(uri).is_err(),
1487 "{uri}"
1488 );
1489 }
1490 }
1491
1492 #[test]
1493 fn same_element_multiple_id_attrs_not_duplicate() {
1494 let xml = r#"<root><item ID="x" Id="x">data</item></root>"#;
1498 let doc = Document::parse(xml).unwrap();
1499 let resolver = UriReferenceResolver::new(&doc);
1500
1501 assert!(resolver.has_id("x"));
1502 assert!(resolver.dereference("#x").is_ok());
1503 }
1504
1505 #[test]
1506 fn saml_style_document() {
1507 let xml = r#"<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
1509 xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
1510 ID="_resp1">
1511 <saml:Assertion ID="_assert1">
1512 <saml:Subject>user@example.com</saml:Subject>
1513 </saml:Assertion>
1514 <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#" Id="sig1">
1515 <ds:SignedInfo/>
1516 </ds:Signature>
1517 </samlp:Response>"#;
1518
1519 let doc = Document::parse(xml).unwrap();
1520 let resolver = UriReferenceResolver::new(&doc);
1521
1522 assert!(resolver.has_id("_resp1"));
1524 assert!(resolver.has_id("_assert1"));
1525 assert!(resolver.has_id("sig1"));
1526 assert_eq!(resolver.id_count(), 3);
1527
1528 let data = resolver.dereference("#_assert1").unwrap();
1530 let node_set = data.into_node_set().unwrap();
1531
1532 let assertion = doc
1534 .descendants()
1535 .find(|n| n.attribute("ID") == Some("_assert1"))
1536 .unwrap();
1537 assert!(node_set.contains(assertion));
1538
1539 let subject = assertion
1541 .children()
1542 .find(|n| n.is_element() && n.has_tag_name("Subject"))
1543 .unwrap();
1544 assert!(node_set.contains(subject));
1545
1546 assert!(!node_set.contains(doc.root_element()));
1548 }
1549}