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(crate) fn id_registrations(&self) -> &[crate::IdAttributeRegistration] {
266 &self.id_registrations
267 }
268 pub fn new(doc: &'a Document<'a>) -> Self {
270 Self::with_id_attrs(doc, &[])
271 }
272
273 pub fn with_id_attrs(doc: &'a Document<'a>, extra_attrs: &[&str]) -> Self {
282 let id_registrations = extra_attrs
283 .iter()
284 .map(|name| crate::IdAttributeRegistration::global(*name))
285 .collect::<Vec<_>>();
286 Self {
287 doc,
288 view: None,
289 resource_identity: None,
290 id_index: ResolverIdIndex::Borrowed(XmlIdIndex::with_registrations(
291 doc,
292 &id_registrations,
293 )),
294 id_registrations,
295 external_resources: None,
296 external_resource_budget: Rc::new(ExternalResourceBudget::default()),
297 same_document_id_semantics: SameDocumentIdSemantics::Specification,
298 }
299 }
300
301 pub fn with_id_registrations(
303 doc: &'a Document<'a>,
304 registrations: &[crate::IdAttributeRegistration],
305 ) -> Self {
306 let id_registrations = registrations.to_vec();
307 Self {
308 doc,
309 view: None,
310 resource_identity: None,
311 id_index: ResolverIdIndex::Borrowed(XmlIdIndex::with_registrations(
312 doc,
313 &id_registrations,
314 )),
315 id_registrations,
316 external_resources: None,
317 external_resource_budget: Rc::new(ExternalResourceBudget::default()),
318 same_document_id_semantics: SameDocumentIdSemantics::Specification,
319 }
320 }
321
322 pub(crate) fn with_document_view(
323 view: crate::DocumentView<'a>,
324 registrations: &[crate::IdAttributeRegistration],
325 ) -> Self {
326 let id_registrations = registrations.to_vec();
327 Self {
328 doc: view.document(),
329 view: Some(view),
330 resource_identity: None,
331 id_index: ResolverIdIndex::Retained(view.id_index(&id_registrations)),
332 id_registrations,
333 external_resources: None,
334 external_resource_budget: Rc::new(ExternalResourceBudget::default()),
335 same_document_id_semantics: SameDocumentIdSemantics::Specification,
336 }
337 }
338
339 pub(crate) fn with_same_document_id_semantics(
340 mut self,
341 semantics: SameDocumentIdSemantics,
342 ) -> Self {
343 self.same_document_id_semantics = semantics;
344 self
345 }
346
347 pub(crate) fn for_document_view<'b>(
350 &self,
351 view: crate::DocumentView<'b>,
352 ) -> UriReferenceResolver<'b>
353 where
354 'a: 'b,
355 {
356 UriReferenceResolver {
357 doc: view.document(),
358 view: Some(view),
359 resource_identity: None,
360 id_index: ResolverIdIndex::Retained(view.id_index(&self.id_registrations)),
361 id_registrations: self.id_registrations.clone(),
362 external_resources: self.external_resources,
363 external_resource_budget: Rc::clone(&self.external_resource_budget),
364 same_document_id_semantics: self.same_document_id_semantics,
365 }
366 }
367
368 pub(crate) fn for_external_document_view<'b>(
374 &self,
375 view: crate::DocumentView<'b>,
376 resource_identity: &str,
377 ) -> UriReferenceResolver<'b>
378 where
379 'a: 'b,
380 {
381 let mut resolver = self.for_document_view(view);
382 resolver.resource_identity = Some(resource_identity.to_owned());
383 resolver
384 }
385
386 pub(crate) fn traversal_document_identity(&self) -> TraversalDocumentIdentity {
387 if let Some(identity) = &self.resource_identity {
388 TraversalDocumentIdentity::External(identity.clone())
389 } else if let Some(view) = self.view {
390 TraversalDocumentIdentity::Owned(view.identity())
391 } else {
392 TraversalDocumentIdentity::Borrowed(self.doc as *const Document<'_> as usize)
393 }
394 }
395
396 pub fn with_external_resources(mut self, resources: &'a HashMap<String, Vec<u8>>) -> Self {
402 self.external_resources = Some(resources);
403 self
404 }
405
406 pub(crate) fn with_external_resource_limits(
407 mut self,
408 max_resource_bytes: usize,
409 max_total_bytes: usize,
410 ) -> Self {
411 self.external_resource_budget = Rc::new(ExternalResourceBudget::with_limits(
412 max_resource_bytes,
413 max_total_bytes,
414 ));
415 self
416 }
417
418 pub(crate) fn external_resource(&self, uri: &str) -> Result<Option<&'a [u8]>, TransformError> {
419 let Some(bytes) = self
420 .external_resources
421 .and_then(|resources| resources.get(uri))
422 else {
423 return Ok(None);
424 };
425 self.external_resource_budget.charge(bytes.len())?;
426 Ok(Some(bytes))
427 }
428
429 pub(crate) fn external_resource_identity(
430 &self,
431 uri: &str,
432 ) -> Option<crate::operation::OperationResourceIdentity> {
433 self.external_resources
434 .and_then(|resources| resources.get(uri))
435 .map(|bytes| crate::operation::OperationResourceIdentity::external(uri, bytes))
436 }
437
438 pub(crate) fn external_resource_set_identity(
439 &self,
440 ) -> crate::operation::OperationResourceIdentity {
441 use sha2::Digest;
442
443 let mut entries = self
444 .external_resources
445 .into_iter()
446 .flat_map(HashMap::iter)
447 .collect::<Vec<_>>();
448 entries.sort_unstable_by_key(|(uri, _)| *uri);
449 let mut hasher = sha2::Sha256::new();
450 for (uri, bytes) in entries {
451 hasher.update(uri.len().to_be_bytes());
452 hasher.update(uri.as_bytes());
453 hasher.update(bytes.len().to_be_bytes());
454 hasher.update(bytes);
455 }
456 crate::operation::OperationResourceIdentity::External {
457 uri: "caller-owned-external-resource-set".to_owned(),
458 fingerprint: hasher.finalize().into(),
459 }
460 }
461
462 pub fn dereference(&self, uri: &str) -> Result<TransformData<'a>, TransformError> {
475 self.dereference_with_optional_budget(uri, None)
476 }
477
478 pub(crate) fn dereference_with_budget(
479 &self,
480 uri: &str,
481 budget: &NodeSetMaterializationBudget,
482 ) -> Result<TransformData<'a>, TransformError> {
483 self.dereference_with_optional_budget(uri, Some(budget))
484 }
485
486 pub(crate) fn dereference_from_with_budget(
487 &self,
488 uri: &str,
489 origin: Node<'_, '_>,
490 budget: &NodeSetMaterializationBudget,
491 xml_base_budget: &XmlBaseResolutionBudget,
492 ) -> Result<TransformData<'a>, TransformError> {
493 if uri.is_empty() || uri.starts_with('#') {
496 return self.dereference_with_budget(uri, budget);
497 }
498 let resolved = resolve_uri_from_node_with_budget(origin, uri, xml_base_budget)
499 .map_err(map_xml_base_resolution_error)?;
500 self.dereference_with_budget(&resolved, budget)
501 }
502
503 fn dereference_with_optional_budget(
504 &self,
505 uri: &str,
506 budget: Option<&NodeSetMaterializationBudget>,
507 ) -> Result<TransformData<'a>, TransformError> {
508 if uri.is_empty() {
509 let nodes = match self.view {
513 Some(view) => NodeSet::entire_document_without_comments_from_view(view, budget)?,
514 None => match budget {
515 Some(budget) => {
516 NodeSet::entire_document_without_comments_with_budget(self.doc, budget)?
517 }
518 None => NodeSet::entire_document_without_comments(self.doc)?,
519 },
520 };
521 Ok(TransformData::NodeSet(nodes))
522 } else if let Some(fragment) = uri.strip_prefix('#') {
523 self.dereference_fragment(fragment, budget)
524 } else {
525 self.external_resource(uri)?
526 .map(|bytes| TransformData::Binary(bytes.to_vec()))
527 .ok_or_else(|| TransformError::UnsupportedUri(uri.to_string()))
528 }
529 }
530
531 fn dereference_fragment(
538 &self,
539 fragment: &str,
540 budget: Option<&NodeSetMaterializationBudget>,
541 ) -> Result<TransformData<'a>, TransformError> {
542 let normalized =
543 Self::normalize_fragment(fragment, budget, self.same_document_id_semantics)?;
544 let fragment = normalized.as_ref();
545 if fragment.is_empty() {
546 return Err(TransformError::UnsupportedUri("#".to_string()));
548 }
549
550 if is_xpointer_root(fragment) {
551 let nodes = match self.view {
555 Some(view) => NodeSet::entire_document_with_comments_from_view(view, budget)?,
556 None => match budget {
557 Some(budget) => {
558 NodeSet::entire_document_with_comments_with_budget(self.doc, budget)?
559 }
560 None => NodeSet::entire_document_with_comments(self.doc)?,
561 },
562 };
563 Ok(TransformData::NodeSet(nodes))
564 } else {
565 let (id, with_comments) = self
566 .same_document_id_fragment(fragment)
567 .ok_or_else(|| TransformError::UnsupportedUri(format!("#{fragment}")))?;
568 self.resolve_id(id, budget, with_comments)
569 }
570 }
571
572 fn same_document_id_fragment<'uri>(&self, fragment: &'uri str) -> Option<(&'uri str, bool)> {
573 Self::id_fragment(fragment, self.same_document_id_semantics)
574 }
575
576 pub(crate) fn id_fragment(
577 fragment: &str,
578 semantics: SameDocumentIdSemantics,
579 ) -> Option<(&str, bool)> {
580 if fragment.is_empty() {
581 return None;
582 }
583 if let Some(id) = parse_xpointer_id_fragment(fragment) {
584 if id.is_empty() {
587 return None;
588 }
589 return Some((id, true));
590 }
591 if fragment.starts_with("xpointer(") {
592 return None;
593 }
594 match semantics {
595 SameDocumentIdSemantics::Specification if !is_xml_ncname(fragment) => {
596 return None;
597 }
598 SameDocumentIdSemantics::XmlSecBarename if fragment.contains('\'') => {
599 return None;
600 }
601 SameDocumentIdSemantics::Specification
602 | SameDocumentIdSemantics::XmlSecBarename
603 | SameDocumentIdSemantics::XmlSecVisa3d => {}
604 }
605 Some((fragment, false))
606 }
607
608 fn resolve_id(
610 &self,
611 id: &str,
612 budget: Option<&NodeSetMaterializationBudget>,
613 with_comments: bool,
614 ) -> Result<TransformData<'a>, TransformError> {
615 match self.id_index.node(self.doc, id) {
616 Some(element) => {
617 let nodes = if let Some(view) = self.view {
618 NodeSet::subtree_from_view(view, element, with_comments, budget)?
619 } else if with_comments {
620 match budget {
621 Some(budget) => NodeSet::subtree_with_budget(element, budget)?,
622 None => NodeSet::subtree(element)?,
623 }
624 } else {
625 NodeSet::subtree_without_comments_with_budget(element, budget)?
626 };
627 Ok(TransformData::NodeSet(nodes))
628 }
629 None => Err(TransformError::ElementNotFound(id.to_string())),
630 }
631 }
632
633 pub fn has_id(&self, id: &str) -> bool {
635 self.id_index.contains(id)
636 }
637
638 pub fn node_for_id(&self, id: &str) -> Option<Node<'a, 'a>> {
643 self.id_index.node(self.doc, id)
644 }
645
646 pub fn node_for_same_document_reference(
649 &self,
650 uri: &str,
651 ) -> Result<Option<Node<'a, 'a>>, TransformError> {
652 self.node_for_same_document_reference_with_budget(uri, None)
653 }
654
655 pub(crate) fn node_for_same_document_reference_with_budget(
656 &self,
657 uri: &str,
658 budget: Option<&NodeSetMaterializationBudget>,
659 ) -> Result<Option<Node<'a, 'a>>, TransformError> {
660 if uri.is_empty() {
661 return Ok(Some(self.doc.root_element()));
662 }
663 let target = self.node_id_for_reference_resource(uri, budget)?;
664 if target == Some(self.doc.root().id()) {
665 return Err(TransformError::UnsupportedUri(uri.to_owned()));
666 }
667 Ok(target.and_then(|id| self.doc.get_node(id)))
668 }
669
670 #[cfg(feature = "xmlenc")]
673 pub(crate) fn same_document_reference_targets(&self, uri: &str, target: NodeId) -> bool {
674 if uri.is_empty() {
675 return self.doc.root_element().id() == target;
676 }
677 let Some(fragment) = uri.strip_prefix('#') else {
678 return false;
679 };
680 let Ok(normalized) =
681 Self::normalize_fragment(fragment, None, self.same_document_id_semantics)
682 else {
683 return false;
684 };
685 let fragment = normalized.as_ref();
686 if fragment.is_empty() || is_xpointer_root(fragment) {
687 return false;
688 }
689 let Some((id, _)) = self.same_document_id_fragment(fragment) else {
690 return false;
691 };
692 self.id_index.node_id(id) == Some(target)
693 }
694
695 pub(crate) fn node_id_for_same_document_reference(
698 &self,
699 uri: &str,
700 ) -> Result<Option<NodeId>, TransformError> {
701 let target = self.node_id_for_reference_resource(uri, None)?;
702 if uri.is_empty() || target == Some(self.doc.root().id()) {
703 return Err(TransformError::UnsupportedUri(uri.to_owned()));
704 }
705 Ok(target)
706 }
707
708 pub(crate) fn node_id_for_reference_resource(
711 &self,
712 uri: &str,
713 budget: Option<&NodeSetMaterializationBudget>,
714 ) -> Result<Option<NodeId>, TransformError> {
715 if uri.is_empty() {
716 return Ok(Some(self.doc.root().id()));
717 }
718 let fragment = uri
719 .strip_prefix('#')
720 .ok_or_else(|| TransformError::UnsupportedUri(uri.to_owned()))?;
721 let normalized =
722 Self::normalize_fragment(fragment, budget, self.same_document_id_semantics)?;
723 let fragment = normalized.as_ref();
724 if fragment.is_empty() {
725 return Err(TransformError::UnsupportedUri(uri.to_owned()));
726 }
727 if is_xpointer_root(fragment) {
728 return Ok(Some(self.doc.root().id()));
729 }
730 let (id, _) = self
731 .same_document_id_fragment(fragment)
732 .ok_or_else(|| TransformError::UnsupportedUri(uri.to_owned()))?;
733 Ok(self.id_index.node_id(id))
734 }
735
736 pub(crate) fn node_for_node_id(&self, id: NodeId) -> Option<Node<'a, 'a>> {
737 self.doc.get_node(id)
738 }
739
740 pub fn id_count(&self) -> usize {
742 self.id_index.len()
743 }
744
745 pub(crate) fn normalize_fragment<'uri>(
746 fragment: &'uri str,
747 budget: Option<&NodeSetMaterializationBudget>,
748 semantics: SameDocumentIdSemantics,
749 ) -> Result<std::borrow::Cow<'uri, str>, TransformError> {
750 use std::borrow::Cow;
751 let invalid = || TransformError::UnsupportedUri("invalid same-document fragment".into());
752 if fragment.len() > crate::hard_limits::XML_DOCUMENT_BYTE_CEILING {
753 return Err(invalid());
754 }
755 let mut result = Cow::Borrowed(fragment);
756 if fragment.contains('%')
762 && (semantics == SameDocumentIdSemantics::Specification
763 || has_encoded_xpointer_prefix(fragment))
764 {
765 if let Some(budget) = budget {
766 budget.reserve_workspace(fragment.len())?;
767 }
768 let mut decoded = Vec::with_capacity(fragment.len());
769 let bytes = fragment.as_bytes();
770 let mut position = 0;
771 while position < bytes.len() {
772 if bytes[position] == b'%' {
773 let high = bytes
774 .get(position + 1)
775 .and_then(|byte| hex_digit(*byte))
776 .ok_or_else(invalid)?;
777 let low = bytes
778 .get(position + 2)
779 .and_then(|byte| hex_digit(*byte))
780 .ok_or_else(invalid)?;
781 decoded.push(high * 16 + low);
782 position += 3;
783 } else {
784 decoded.push(bytes[position]);
785 position += 1;
786 }
787 }
788 result = Cow::Owned(String::from_utf8(decoded).map_err(|_| invalid())?);
789 }
790 if result.starts_with("xpointer(") {
791 let bytes = result.as_bytes();
794 let mut depth = 0usize;
795 let mut position = "xpointer".len();
796 while position < bytes.len() {
797 match bytes[position] {
798 b'^' => {
799 if !matches!(bytes.get(position + 1), Some(b'(' | b')' | b'^')) {
800 return Err(invalid());
801 }
802 position += 2;
803 continue;
804 }
805 b'(' => depth += 1,
806 b')' => {
807 if depth == 0 {
808 return Err(invalid());
809 }
810 depth -= 1;
811 if depth == 0 && position + 1 != bytes.len() {
812 return Err(invalid());
813 }
814 }
815 _ => {}
816 }
817 position += 1;
818 }
819 if depth != 0 {
820 return Err(invalid());
821 }
822 if result.contains('^') {
823 if matches!(result, Cow::Borrowed(_))
824 && let Some(budget) = budget
825 {
826 budget.reserve_workspace(result.len())?;
827 }
828 let mut escaped = false;
829 result.to_mut().retain(|character| {
830 if escaped {
831 escaped = false;
832 true
833 } else if character == '^' {
834 escaped = true;
835 false
836 } else {
837 true
838 }
839 });
840 }
841 }
842 Ok(result)
843 }
844}
845
846fn hex_digit(byte: u8) -> Option<u8> {
847 match byte {
848 b'0'..=b'9' => Some(byte - b'0'),
849 b'a'..=b'f' => Some(byte - b'a' + 10),
850 b'A'..=b'F' => Some(byte - b'A' + 10),
851 _ => None,
852 }
853}
854
855fn has_encoded_xpointer_prefix(fragment: &str) -> bool {
856 let mut bytes = fragment.bytes();
858 for expected in b"xpointer(" {
859 let Some(mut actual) = bytes.next() else {
860 return false;
861 };
862 if actual == b'%' {
863 let Some(high) = bytes.next().and_then(hex_digit) else {
864 return false;
865 };
866 let Some(low) = bytes.next().and_then(hex_digit) else {
867 return false;
868 };
869 actual = high * 16 + low;
870 }
871 if actual != *expected {
872 return false;
873 }
874 }
875 true
876}
877
878fn is_xpointer_root(fragment: &str) -> bool {
879 fragment
880 .strip_prefix("xpointer(")
881 .and_then(|value| value.strip_suffix(')'))
882 .is_some_and(|expression| xml_space_trim(expression) == "/")
883}
884
885pub(crate) fn map_xml_base_resolution_error(error: XmlBaseResolutionError) -> TransformError {
886 match error {
887 XmlBaseResolutionError::Components { maximum, actual } => transform_resource_limit(
888 crate::policy::resource_name::XML_BASE_COMPONENTS,
889 maximum,
890 actual,
891 ),
892 XmlBaseResolutionError::Bytes { maximum, actual } => transform_resource_limit(
893 crate::policy::resource_name::XML_BASE_RESOLUTION_BYTES,
894 maximum,
895 actual,
896 ),
897 }
898}
899
900pub(crate) fn parse_xpointer_id_fragment(fragment: &str) -> Option<&str> {
903 let expression = fragment.strip_prefix("xpointer(")?.strip_suffix(')')?;
907 let argument = xml_space_trim(expression)
908 .strip_prefix("id")?
909 .trim_start_matches(is_xml_space)
910 .strip_prefix('(')?;
911 let literal = xml_space_trim(argument.strip_suffix(')')?);
912 let quote = literal.as_bytes().first().copied()?;
913 if !matches!(quote, b'\'' | b'"')
914 || literal.len() < 2
915 || literal.as_bytes().last() != Some("e)
916 {
917 return None;
918 }
919 let value = &literal[1..literal.len() - 1];
920 if value.as_bytes().contains("e) {
921 return None;
922 }
923 Some(value)
924}
925
926fn is_xml_space(character: char) -> bool {
927 matches!(character, ' ' | '\t' | '\r' | '\n')
928}
929
930fn xml_space_trim(value: &str) -> &str {
931 value.trim_matches(is_xml_space)
932}
933
934#[cfg(test)]
935#[allow(clippy::unwrap_used)]
936mod tests {
937 use super::super::types::NodeSet;
938 use super::*;
939
940 #[test]
941 fn escaped_fragments_preserve_target_and_comment_semantics() {
942 let document =
945 Document::parse(r#"<root><item ID="café"><!--kept--></item><item ID="a(b)"/></root>"#)
946 .unwrap();
947 let resolver = UriReferenceResolver::new(&document);
948 let plain = resolver
949 .dereference("#caf%C3%A9")
950 .unwrap()
951 .into_node_set()
952 .unwrap();
953 let pointer = resolver
954 .dereference("#xpointer(id('caf%C3%A9'))")
955 .unwrap()
956 .into_node_set()
957 .unwrap();
958 let comment = document
959 .descendants()
960 .find(|node| node.is_comment())
961 .unwrap();
962 assert!(!plain.contains(comment));
963 assert!(pointer.contains(comment));
964 assert!(resolver.dereference("#xpointer(id('a^(b^)'))").is_ok());
965 assert!(resolver.dereference("#xpointer( / )").is_ok());
966 for uri in [
967 "#caf%",
968 "#caf%FF",
969 "#xpointer(id('a^x'))",
970 "#xpointer(id('a(b'))",
971 "#xpointer(id('a') or 'b')",
972 ] {
973 assert!(resolver.dereference(uri).is_err(), "{uri}");
974 }
975 }
976
977 #[test]
978 fn fragment_decoding_reserves_workspace_before_allocation() {
979 let budget = NodeSetMaterializationBudget::with_limit(0);
982 assert!(
983 UriReferenceResolver::normalize_fragment(
984 "target",
985 Some(&budget),
986 SameDocumentIdSemantics::Specification
987 )
988 .is_ok()
989 );
990 assert!(matches!(
991 UriReferenceResolver::normalize_fragment(
992 "tar%67et",
993 Some(&budget),
994 SameDocumentIdSemantics::Specification
995 ),
996 Err(TransformError::Policy(_))
997 ));
998 assert!(matches!(
999 UriReferenceResolver::normalize_fragment(
1000 "xpointer(id('a^(b^)'))",
1001 Some(&budget),
1002 SameDocumentIdSemantics::Specification
1003 ),
1004 Err(TransformError::Policy(_))
1005 ));
1006 }
1007
1008 #[test]
1009 fn xpointer_id_requires_one_complete_literal() {
1010 assert_eq!(parse_xpointer_id_fragment("xpointer(id('a' or 'b'))"), None);
1013 assert_eq!(
1014 parse_xpointer_id_fragment("xpointer(id(\"a\" or \"b\"))"),
1015 None
1016 );
1017 assert_eq!(
1018 parse_xpointer_id_fragment("xpointer( id ( 'target' ) )"),
1019 Some("target")
1020 );
1021 }
1022
1023 #[test]
1024 fn empty_same_document_node_reference_selects_the_document_element() {
1025 let document = Document::parse("<root><child/></root>").unwrap();
1028 let resolver = UriReferenceResolver::new(&document);
1029
1030 assert_eq!(
1031 resolver
1032 .node_for_same_document_reference("")
1033 .unwrap()
1034 .unwrap(),
1035 document.root_element()
1036 );
1037 }
1038
1039 #[test]
1040 fn empty_uri_returns_whole_document() {
1041 let xml = "<root><child>text</child></root>";
1042 let doc = Document::parse(xml).unwrap();
1043 let resolver = UriReferenceResolver::new(&doc);
1044
1045 let data = resolver.dereference("").unwrap();
1046 let node_set = data.into_node_set().unwrap();
1047
1048 let root = doc.root_element();
1050 assert!(node_set.contains(root));
1051 let child = root.first_child().unwrap();
1052 assert!(node_set.contains(child));
1053 }
1054
1055 #[test]
1056 fn empty_uri_excludes_comments() {
1057 let xml = "<root><!-- comment --><child/></root>";
1058 let doc = Document::parse(xml).unwrap();
1059 let resolver = UriReferenceResolver::new(&doc);
1060
1061 let data = resolver.dereference("").unwrap();
1062 let node_set = data.into_node_set().unwrap();
1063
1064 for node in doc.descendants() {
1066 if node.is_comment() {
1067 assert!(
1068 !node_set.contains(node),
1069 "comment should be excluded for empty URI"
1070 );
1071 }
1072 }
1073 assert!(node_set.contains(doc.root_element()));
1075 }
1076
1077 #[test]
1078 fn fragment_uri_resolves_by_id_attr() {
1079 let xml = r#"<root><item ID="abc">content</item><item ID="def">other</item></root>"#;
1080 let doc = Document::parse(xml).unwrap();
1081 let resolver = UriReferenceResolver::new(&doc);
1082
1083 let data = resolver.dereference("#abc").unwrap();
1084 let node_set = data.into_node_set().unwrap();
1085
1086 let abc_elem = doc
1088 .descendants()
1089 .find(|n| n.attribute("ID") == Some("abc"))
1090 .unwrap();
1091 assert!(node_set.contains(abc_elem));
1092
1093 let text_child = abc_elem.first_child().unwrap();
1095 assert!(node_set.contains(text_child));
1096
1097 assert!(!node_set.contains(doc.root_element()));
1099
1100 let def_elem = doc
1102 .descendants()
1103 .find(|n| n.attribute("ID") == Some("def"))
1104 .unwrap();
1105 assert!(!node_set.contains(def_elem));
1106 }
1107
1108 #[test]
1109 fn fragment_uri_resolves_lowercase_id() {
1110 let xml = r#"<root><item id="lower">text</item></root>"#;
1111 let doc = Document::parse(xml).unwrap();
1112 let resolver = UriReferenceResolver::new(&doc);
1113
1114 let data = resolver.dereference("#lower").unwrap();
1115 let node_set = data.into_node_set().unwrap();
1116
1117 let elem = doc
1118 .descendants()
1119 .find(|n| n.attribute("id") == Some("lower"))
1120 .unwrap();
1121 assert!(node_set.contains(elem));
1122 }
1123
1124 #[test]
1125 fn fragment_uri_resolves_mixed_case_id() {
1126 let xml = r#"<root><ds:Signature Id="sig1" xmlns:ds="http://www.w3.org/2000/09/xmldsig#"/></root>"#;
1127 let doc = Document::parse(xml).unwrap();
1128 let resolver = UriReferenceResolver::new(&doc);
1129
1130 assert!(resolver.has_id("sig1"));
1131 let data = resolver.dereference("#sig1").unwrap();
1132 assert!(data.into_node_set().is_ok());
1133 }
1134
1135 #[test]
1136 fn fragment_uri_not_found() {
1137 let xml = "<root><child>text</child></root>";
1138 let doc = Document::parse(xml).unwrap();
1139 let resolver = UriReferenceResolver::new(&doc);
1140
1141 let result = resolver.dereference("#nonexistent");
1142 assert!(result.is_err());
1143 match result.unwrap_err() {
1144 TransformError::ElementNotFound(id) => assert_eq!(id, "nonexistent"),
1145 other => panic!("expected ElementNotFound, got: {other:?}"),
1146 }
1147 }
1148
1149 #[test]
1150 fn unsupported_external_uri() {
1151 let xml = "<root/>";
1152 let doc = Document::parse(xml).unwrap();
1153 let resolver = UriReferenceResolver::new(&doc);
1154
1155 let result = resolver.dereference("http://example.com/doc.xml");
1156 assert!(result.is_err());
1157 match result.unwrap_err() {
1158 TransformError::UnsupportedUri(uri) => {
1159 assert_eq!(uri, "http://example.com/doc.xml")
1160 }
1161 other => panic!("expected UnsupportedUri, got: {other:?}"),
1162 }
1163 }
1164
1165 #[test]
1166 fn unsupported_xpointer_expression() {
1167 let xml = "<root/>";
1170 let doc = Document::parse(xml).unwrap();
1171 let resolver = UriReferenceResolver::new(&doc);
1172
1173 let result = resolver.dereference("#xpointer(foo())");
1174 assert!(result.is_err());
1175 match result.unwrap_err() {
1176 TransformError::UnsupportedUri(uri) => {
1177 assert_eq!(uri, "#xpointer(foo())")
1178 }
1179 other => panic!("expected UnsupportedUri, got: {other:?}"),
1180 }
1181
1182 let result = resolver.dereference("#xpointer(//element)");
1184 assert!(result.is_err());
1185 assert!(matches!(
1186 result.unwrap_err(),
1187 TransformError::UnsupportedUri(_)
1188 ));
1189 }
1190
1191 #[test]
1192 fn empty_fragment_rejected() {
1193 let xml = "<root/>";
1195 let doc = Document::parse(xml).unwrap();
1196 let resolver = UriReferenceResolver::new(&doc);
1197
1198 let result = resolver.dereference("#");
1199 assert!(result.is_err());
1200 match result.unwrap_err() {
1201 TransformError::UnsupportedUri(uri) => assert_eq!(uri, "#"),
1202 other => panic!("expected UnsupportedUri, got: {other:?}"),
1203 }
1204 }
1205
1206 #[test]
1207 fn foreign_document_node_rejected() {
1208 let xml1 = "<root><child/></root>";
1210 let xml2 = "<other><item/></other>";
1211 let doc1 = Document::parse(xml1).unwrap();
1212 let doc2 = Document::parse(xml2).unwrap();
1213
1214 let node_set = NodeSet::entire_document_without_comments(&doc1).unwrap();
1215
1216 let foreign_node = doc2.root_element();
1218 assert!(
1219 !node_set.contains(foreign_node),
1220 "foreign document node should be rejected"
1221 );
1222
1223 let own_node = doc1.root_element();
1225 assert!(node_set.contains(own_node));
1226 }
1227
1228 #[test]
1229 fn custom_id_attr_name() {
1230 let xml = r#"<root><elem myid="custom1">data</elem></root>"#;
1233 let doc = Document::parse(xml).unwrap();
1234
1235 let resolver_default = UriReferenceResolver::new(&doc);
1237 assert!(!resolver_default.has_id("custom1"));
1238
1239 let resolver_custom = UriReferenceResolver::with_id_attrs(&doc, &["myid"]);
1241 assert!(resolver_custom.has_id("custom1"));
1242
1243 let data = resolver_custom.dereference("#custom1").unwrap();
1244 assert!(data.into_node_set().is_ok());
1245 }
1246
1247 #[test]
1248 fn absolute_external_uri_uses_normalized_resource_identity() {
1249 let xml = r#"<root xml:base="https://base.example/ignored/">
1252 <reference URI="https://example.test/a/../data.bin"/>
1253 </root>"#;
1254 let doc = Document::parse(xml).unwrap();
1255 let reference = doc
1256 .descendants()
1257 .find(|node| node.has_tag_name("reference"))
1258 .unwrap();
1259 let resources = HashMap::from([(
1260 "https://example.test/data.bin".to_owned(),
1261 b"payload".to_vec(),
1262 )]);
1263 let budget = NodeSetMaterializationBudget::default();
1264 let xml_base_budget = XmlBaseResolutionBudget::default();
1265 let resolver = UriReferenceResolver::new(&doc).with_external_resources(&resources);
1266
1267 let data = resolver
1268 .dereference_from_with_budget(
1269 reference.attribute("URI").unwrap(),
1270 reference,
1271 &budget,
1272 &xml_base_budget,
1273 )
1274 .unwrap();
1275
1276 assert_eq!(data.into_binary().unwrap(), b"payload");
1277 }
1278
1279 #[test]
1280 fn absolute_external_uri_does_not_consume_xml_base_components() {
1281 let xml = r#"<root xml:base="ignored/"><reference URI="https://example.test/a/../data.bin"/></root>"#;
1284 let doc = Document::parse(xml).unwrap();
1285 let reference = doc
1286 .descendants()
1287 .find(|node| node.has_tag_name("reference"))
1288 .unwrap();
1289 let resources = HashMap::from([(
1290 "https://example.test/data.bin".to_owned(),
1291 b"payload".to_vec(),
1292 )]);
1293 let resolver = UriReferenceResolver::new(&doc).with_external_resources(&resources);
1294
1295 let data = resolver
1296 .dereference_from_with_budget(
1297 reference.attribute("URI").unwrap(),
1298 reference,
1299 &NodeSetMaterializationBudget::default(),
1300 &XmlBaseResolutionBudget::with_limits(0, 1_024),
1301 )
1302 .expect("absolute references must bypass inherited XML Base traversal");
1303
1304 assert_eq!(data.into_binary().unwrap(), b"payload");
1305 }
1306
1307 #[test]
1308 fn external_uri_without_xml_base_uses_normalized_resource_identity() {
1309 let xml = r#"<root><reference URI="https://example.test/a/../data.bin"/></root>"#;
1312 let doc = Document::parse(xml).unwrap();
1313 let reference = doc
1314 .descendants()
1315 .find(|node| node.has_tag_name("reference"))
1316 .unwrap();
1317 let resources = HashMap::from([(
1318 "https://example.test/data.bin".to_owned(),
1319 b"payload".to_vec(),
1320 )]);
1321 let budget = NodeSetMaterializationBudget::default();
1322 let xml_base_budget = XmlBaseResolutionBudget::default();
1323 let resolver = UriReferenceResolver::new(&doc).with_external_resources(&resources);
1324
1325 let data = resolver
1326 .dereference_from_with_budget(
1327 reference.attribute("URI").unwrap(),
1328 reference,
1329 &budget,
1330 &xml_base_budget,
1331 )
1332 .expect("the normalized resource key must resolve without xml:base");
1333
1334 assert_eq!(data.into_binary().unwrap(), b"payload");
1335 }
1336
1337 #[test]
1338 fn pathless_relative_xml_base_preserves_relative_resource_identity() {
1339 let xml = r#"<root xml:base="?old">
1342 <reference URI="data.bin"/>
1343 </root>"#;
1344 let doc = Document::parse(xml).unwrap();
1345 let reference = doc
1346 .descendants()
1347 .find(|node| node.has_tag_name("reference"))
1348 .unwrap();
1349 let resources = HashMap::from([("data.bin".to_owned(), b"payload".to_vec())]);
1350 let budget = NodeSetMaterializationBudget::default();
1351 let xml_base_budget = XmlBaseResolutionBudget::default();
1352 let resolver = UriReferenceResolver::new(&doc).with_external_resources(&resources);
1353
1354 let data = resolver
1355 .dereference_from_with_budget(
1356 reference.attribute("URI").unwrap(),
1357 reference,
1358 &budget,
1359 &xml_base_budget,
1360 )
1361 .unwrap();
1362
1363 assert_eq!(data.into_binary().unwrap(), b"payload");
1364 }
1365
1366 #[test]
1367 fn relative_xml_base_normalizes_absolute_external_path() {
1368 let xml = r#"<root xml:base="a/b">
1371 <reference URI="/x/../data.bin"/>
1372 </root>"#;
1373 let doc = Document::parse(xml).unwrap();
1374 let reference = doc
1375 .descendants()
1376 .find(|node| node.has_tag_name("reference"))
1377 .unwrap();
1378 let resources = HashMap::from([("/data.bin".to_owned(), b"payload".to_vec())]);
1379 let budget = NodeSetMaterializationBudget::default();
1380 let xml_base_budget = XmlBaseResolutionBudget::default();
1381 let resolver = UriReferenceResolver::new(&doc).with_external_resources(&resources);
1382
1383 let data = resolver
1384 .dereference_from_with_budget(
1385 reference.attribute("URI").unwrap(),
1386 reference,
1387 &budget,
1388 &xml_base_budget,
1389 )
1390 .unwrap();
1391
1392 assert_eq!(data.into_binary().unwrap(), b"payload");
1393 }
1394
1395 #[test]
1396 fn network_path_xml_base_preserves_external_resource_authority() {
1397 let xml = r#"<root xml:base="//cdn.example/a/b/">
1400 <reference URI="/x/../data.bin"/>
1401 </root>"#;
1402 let doc = Document::parse(xml).unwrap();
1403 let reference = doc
1404 .descendants()
1405 .find(|node| node.has_tag_name("reference"))
1406 .unwrap();
1407 let resources = HashMap::from([("//cdn.example/data.bin".to_owned(), b"payload".to_vec())]);
1408 let budget = NodeSetMaterializationBudget::default();
1409 let xml_base_budget = XmlBaseResolutionBudget::default();
1410 let resolver = UriReferenceResolver::new(&doc).with_external_resources(&resources);
1411
1412 let data = resolver
1413 .dereference_from_with_budget(
1414 reference.attribute("URI").unwrap(),
1415 reference,
1416 &budget,
1417 &xml_base_budget,
1418 )
1419 .unwrap();
1420
1421 assert_eq!(data.into_binary().unwrap(), b"payload");
1422 }
1423
1424 #[test]
1425 fn unicode_external_uri_resolves_without_panicking() {
1426 let xml = r#"<root xml:base="https://example.test/base/">
1429 <reference URI="é?x"/>
1430 </root>"#;
1431 let doc = Document::parse(xml).unwrap();
1432 let reference = doc
1433 .descendants()
1434 .find(|node| node.has_tag_name("reference"))
1435 .unwrap();
1436 let resources = HashMap::from([(
1437 "https://example.test/base/é?x".to_owned(),
1438 b"payload".to_vec(),
1439 )]);
1440 let budget = NodeSetMaterializationBudget::default();
1441 let xml_base_budget = XmlBaseResolutionBudget::default();
1442 let resolver = UriReferenceResolver::new(&doc).with_external_resources(&resources);
1443
1444 let data = resolver
1445 .dereference_from_with_budget(
1446 reference.attribute("URI").unwrap(),
1447 reference,
1448 &budget,
1449 &xml_base_budget,
1450 )
1451 .unwrap();
1452
1453 assert_eq!(data.into_binary().unwrap(), b"payload");
1454 }
1455
1456 #[test]
1457 fn absolute_rootless_external_uri_discards_leading_parent_segment() {
1458 let xml = r#"<root xml:base="https://example.test/base/">
1459 <reference URI="urn:../payload"/>
1460 </root>"#;
1461 let doc = Document::parse(xml).unwrap();
1462 let reference = doc
1463 .descendants()
1464 .find(|node| node.has_tag_name("reference"))
1465 .unwrap();
1466 let resources = HashMap::from([("urn:payload".to_owned(), b"payload".to_vec())]);
1467 let budget = NodeSetMaterializationBudget::default();
1468 let xml_base_budget = XmlBaseResolutionBudget::default();
1469 let resolver = UriReferenceResolver::new(&doc).with_external_resources(&resources);
1470
1471 let data = resolver
1472 .dereference_from_with_budget(
1473 reference.attribute("URI").unwrap(),
1474 reference,
1475 &budget,
1476 &xml_base_budget,
1477 )
1478 .unwrap();
1479
1480 assert_eq!(data.into_binary().unwrap(), b"payload");
1481 }
1482
1483 #[test]
1484 fn namespaced_id_attr_found_by_local_name() {
1485 let xml =
1487 r#"<root><elem wsu:Id="ts1" xmlns:wsu="http://example.com/wsu">data</elem></root>"#;
1488 let doc = Document::parse(xml).unwrap();
1489
1490 let resolver = UriReferenceResolver::new(&doc);
1491 assert!(resolver.has_id("ts1"));
1492 }
1493
1494 #[test]
1495 fn id_count_reports_unique_ids() {
1496 let xml = r#"<root ID="r1"><a ID="a1"/><b Id="b1"/><c id="c1"/></root>"#;
1497 let doc = Document::parse(xml).unwrap();
1498 let resolver = UriReferenceResolver::new(&doc);
1499
1500 assert_eq!(resolver.id_count(), 4);
1502 }
1503
1504 #[test]
1505 fn duplicate_ids_are_rejected() {
1506 let xml = r#"<root><a ID="dup">first</a><b ID="dup">second</b></root>"#;
1509 let doc = Document::parse(xml).unwrap();
1510 let resolver = UriReferenceResolver::new(&doc);
1511
1512 assert!(!resolver.has_id("dup"));
1514 let result = resolver.dereference("#dup");
1515 assert!(result.is_err());
1516 assert!(matches!(
1517 result.unwrap_err(),
1518 TransformError::ElementNotFound(_)
1519 ));
1520 }
1521
1522 #[test]
1523 fn triple_duplicate_ids_stay_rejected() {
1524 let xml = r#"<root><a ID="dup">1</a><b ID="dup">2</b><c ID="dup">3</c></root>"#;
1527 let doc = Document::parse(xml).unwrap();
1528 let resolver = UriReferenceResolver::new(&doc);
1529
1530 assert!(!resolver.has_id("dup"));
1531 assert!(resolver.dereference("#dup").is_err());
1532 }
1533
1534 #[test]
1535 fn node_set_exclude_subtree() {
1536 let xml = r#"<root><keep>yes</keep><remove><deep>no</deep></remove></root>"#;
1537 let doc = Document::parse(xml).unwrap();
1538 let resolver = UriReferenceResolver::new(&doc);
1539
1540 let data = resolver.dereference("").unwrap();
1541 let mut node_set = data.into_node_set().unwrap();
1542
1543 let remove_elem = doc
1545 .descendants()
1546 .find(|n| n.is_element() && n.has_tag_name("remove"))
1547 .unwrap();
1548 node_set.exclude_subtree(remove_elem);
1549
1550 let keep_elem = doc
1552 .descendants()
1553 .find(|n| n.is_element() && n.has_tag_name("keep"))
1554 .unwrap();
1555 assert!(node_set.contains(keep_elem));
1556
1557 assert!(!node_set.contains(remove_elem));
1559 let deep_elem = doc
1560 .descendants()
1561 .find(|n| n.is_element() && n.has_tag_name("deep"))
1562 .unwrap();
1563 assert!(!node_set.contains(deep_elem));
1564 }
1565
1566 #[test]
1567 fn bare_name_subtree_excludes_comments() {
1568 let xml = r#"<root><item ID="x"><!-- comment --><child/></item></root>"#;
1570 let doc = Document::parse(xml).unwrap();
1571 let resolver = UriReferenceResolver::new(&doc);
1572
1573 let data = resolver.dereference("#x").unwrap();
1574 let node_set = data.into_node_set().unwrap();
1575
1576 for node in doc.descendants() {
1577 if node.is_comment() {
1578 assert!(
1579 !node_set.contains(node),
1580 "comment must be excluded from #id"
1581 );
1582 }
1583 }
1584 }
1585
1586 #[test]
1587 fn xpointer_root_returns_whole_document_with_comments() {
1588 let xml = "<root><!-- comment --><child/></root>";
1589 let doc = Document::parse(xml).unwrap();
1590 let resolver = UriReferenceResolver::new(&doc);
1591
1592 let data = resolver.dereference("#xpointer(/)").unwrap();
1593 let node_set = data.into_node_set().unwrap();
1594
1595 for node in doc.descendants() {
1597 if node.is_comment() {
1598 assert!(
1599 node_set.contains(node),
1600 "comment should be included for #xpointer(/)"
1601 );
1602 }
1603 }
1604 assert!(node_set.contains(doc.root_element()));
1605 }
1606
1607 #[test]
1608 fn xpointer_id_single_quotes() {
1609 let xml = r#"<root><item ID="abc"><!-- retained -->content</item></root>"#;
1611 let doc = Document::parse(xml).unwrap();
1612 let resolver = UriReferenceResolver::new(&doc);
1613
1614 let data = resolver.dereference("#xpointer(id('abc'))").unwrap();
1615 let node_set = data.into_node_set().unwrap();
1616
1617 let elem = doc
1618 .descendants()
1619 .find(|n| n.attribute("ID") == Some("abc"))
1620 .unwrap();
1621 assert!(node_set.contains(elem));
1622 assert!(
1623 elem.children()
1624 .any(|node| node.is_comment() && node_set.contains(node))
1625 );
1626 }
1627
1628 #[test]
1629 fn xpointer_id_double_quotes() {
1630 let xml = r#"<root><item ID="xyz">content</item></root>"#;
1631 let doc = Document::parse(xml).unwrap();
1632 let resolver = UriReferenceResolver::new(&doc);
1633
1634 let data = resolver.dereference(r#"#xpointer(id("xyz"))"#).unwrap();
1635 let node_set = data.into_node_set().unwrap();
1636
1637 let elem = doc
1638 .descendants()
1639 .find(|n| n.attribute("ID") == Some("xyz"))
1640 .unwrap();
1641 assert!(node_set.contains(elem));
1642 }
1643
1644 #[test]
1645 fn xpointer_id_not_found() {
1646 let xml = "<root/>";
1647 let doc = Document::parse(xml).unwrap();
1648 let resolver = UriReferenceResolver::new(&doc);
1649
1650 let result = resolver.dereference("#xpointer(id('missing'))");
1651 assert!(result.is_err());
1652 match result.unwrap_err() {
1653 TransformError::ElementNotFound(id) => assert_eq!(id, "missing"),
1654 other => panic!("expected ElementNotFound, got: {other:?}"),
1655 }
1656 }
1657
1658 #[test]
1659 fn xpointer_id_empty_value_rejected() {
1660 let xml = "<root/>";
1662 let doc = Document::parse(xml).unwrap();
1663 let resolver = UriReferenceResolver::new(&doc);
1664
1665 let result = resolver.dereference("#xpointer(id(''))");
1666 assert!(result.is_err());
1667 assert!(matches!(
1668 result.unwrap_err(),
1669 TransformError::UnsupportedUri(_)
1670 ));
1671 }
1672
1673 #[test]
1674 fn bare_fragment_rejects_non_ncname_without_visa3d_compatibility() {
1675 let xml = r#"<root><item ID="12345">content</item></root>"#;
1679 let doc = Document::parse(xml).unwrap();
1680 let resolver = UriReferenceResolver::new(&doc);
1681
1682 assert!(matches!(
1683 resolver.dereference("#12345"),
1684 Err(TransformError::UnsupportedUri(uri)) if uri == "#12345"
1685 ));
1686 }
1687
1688 #[test]
1689 fn visa3d_compatibility_resolves_non_ncname_id_directly() {
1690 let xml = r#"<root><item ID="12345">content</item></root>"#;
1693 let doc = Document::parse(xml).unwrap();
1694 let resolver = UriReferenceResolver::new(&doc)
1695 .with_same_document_id_semantics(SameDocumentIdSemantics::XmlSecVisa3d);
1696
1697 assert!(resolver.dereference("#12345").is_ok());
1698 }
1699
1700 #[test]
1701 fn xmlsec_barename_compatibility_matches_donor_literal_and_comment_semantics() {
1702 let xml = r#"<root><item ID="12345"><!-- excluded -->numeric</item><item ID="visa'3d">quoted</item></root>"#;
1706 let doc = Document::parse(xml).unwrap();
1707 let resolver = UriReferenceResolver::new(&doc)
1708 .with_same_document_id_semantics(SameDocumentIdSemantics::XmlSecBarename);
1709
1710 let nodes = resolver
1711 .dereference("#12345")
1712 .unwrap()
1713 .into_node_set()
1714 .unwrap();
1715 assert!(
1716 doc.descendants()
1717 .filter(|node| node.is_comment())
1718 .all(|node| !nodes.contains(node))
1719 );
1720 assert!(matches!(
1721 resolver.dereference("#visa'3d"),
1722 Err(TransformError::UnsupportedUri(uri)) if uri == "#visa'3d"
1723 ));
1724 }
1725
1726 #[test]
1727 fn explicit_xpointer_decoding_is_independent_of_barename_mode() {
1728 let document = Document::parse("<root><item ID='café'/></root>").unwrap();
1730 for semantics in [
1731 SameDocumentIdSemantics::Specification,
1732 SameDocumentIdSemantics::XmlSecBarename,
1733 SameDocumentIdSemantics::XmlSecVisa3d,
1734 ] {
1735 let resolver =
1736 UriReferenceResolver::new(&document).with_same_document_id_semantics(semantics);
1737 for uri in [
1738 "#xpointer(id('caf%C3%A9'))",
1739 "#%78pointer%28id('caf%C3%A9')%29",
1740 ] {
1741 assert!(resolver.dereference(uri).is_ok(), "{semantics:?}: {uri}");
1742 }
1743 }
1744 }
1745
1746 #[test]
1747 fn parse_xpointer_id_variants() {
1748 assert_eq!(
1750 super::parse_xpointer_id_fragment("xpointer(id('foo'))"),
1751 Some("foo")
1752 );
1753 assert_eq!(
1754 super::parse_xpointer_id_fragment(r#"xpointer(id("bar"))"#),
1755 Some("bar")
1756 );
1757
1758 assert_eq!(super::parse_xpointer_id_fragment("xpointer(/)"), None);
1760 assert_eq!(super::parse_xpointer_id_fragment("xpointer(id(foo))"), None); assert_eq!(super::parse_xpointer_id_fragment("not-xpointer"), None);
1762 assert_eq!(super::parse_xpointer_id_fragment(""), None);
1763
1764 assert_eq!(super::parse_xpointer_id_fragment("xpointer(id('))"), None);
1766 assert_eq!(
1767 super::parse_xpointer_id_fragment(r#"xpointer(id("))"#),
1768 None
1769 );
1770 }
1771
1772 #[test]
1773 fn same_document_reference_nodes_reject_non_id_fragments() {
1774 let document = Document::parse(r#"<root><item ID="target"/></root>"#).unwrap();
1775 let resolver = UriReferenceResolver::new(&document);
1776 for uri in [
1777 "#target",
1778 "#xpointer(id('target'))",
1779 r#"#xpointer(id("target"))"#,
1780 ] {
1781 assert!(
1782 resolver
1783 .node_id_for_same_document_reference(uri)
1784 .unwrap()
1785 .is_some()
1786 );
1787 }
1788 for uri in [
1789 "",
1790 "target",
1791 "#",
1792 "#xpointer(/)",
1793 "#xpointer(id(''))",
1794 "#xpointer(id(target))",
1795 ] {
1796 assert!(
1797 resolver.node_id_for_same_document_reference(uri).is_err(),
1798 "{uri}"
1799 );
1800 }
1801 }
1802
1803 #[test]
1804 fn same_element_multiple_id_attrs_not_duplicate() {
1805 let xml = r#"<root><item ID="x" Id="x">data</item></root>"#;
1809 let doc = Document::parse(xml).unwrap();
1810 let resolver = UriReferenceResolver::new(&doc);
1811
1812 assert!(resolver.has_id("x"));
1813 assert!(resolver.dereference("#x").is_ok());
1814 }
1815
1816 #[test]
1817 fn saml_style_document() {
1818 let xml = r#"<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
1820 xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
1821 ID="_resp1">
1822 <saml:Assertion ID="_assert1">
1823 <saml:Subject>user@example.com</saml:Subject>
1824 </saml:Assertion>
1825 <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#" Id="sig1">
1826 <ds:SignedInfo/>
1827 </ds:Signature>
1828 </samlp:Response>"#;
1829
1830 let doc = Document::parse(xml).unwrap();
1831 let resolver = UriReferenceResolver::new(&doc);
1832
1833 assert!(resolver.has_id("_resp1"));
1835 assert!(resolver.has_id("_assert1"));
1836 assert!(resolver.has_id("sig1"));
1837 assert_eq!(resolver.id_count(), 3);
1838
1839 let data = resolver.dereference("#_assert1").unwrap();
1841 let node_set = data.into_node_set().unwrap();
1842
1843 let assertion = doc
1845 .descendants()
1846 .find(|n| n.attribute("ID") == Some("_assert1"))
1847 .unwrap();
1848 assert!(node_set.contains(assertion));
1849
1850 let subject = assertion
1852 .children()
1853 .find(|n| n.is_element() && n.has_tag_name("Subject"))
1854 .unwrap();
1855 assert!(node_set.contains(subject));
1856
1857 assert!(!node_set.contains(doc.root_element()));
1859 }
1860}