1use crate::c14n::scratch::{Accounted, Bytes as _, Native, Scratch, Text as _};
16use std::cell::Cell;
17use std::collections::HashMap;
18use std::rc::Rc;
19
20use crate::xml::dom::{Document, Node, NodeId};
21
22use crate::c14n::xml_base::{
23 XmlBaseResolutionBudget, XmlBaseResolutionError, resolve_uri_from_node_with_budget,
24};
25use crate::policy::SameDocumentIdSemantics;
26use crate::xml::{XmlIdIndex, is_xml_ncname};
27
28use super::types::{
29 NodeSet, NodeSetMaterializationBudget, TransformData, TransformError, transform_resource_limit,
30};
31
32pub(crate) enum ReferenceInput<'a> {
34 Xml {
35 root: Node<'a, 'a>,
36 with_comments: bool,
37 },
38 Binary {
39 bytes: &'a [u8],
40 uri: &'a str,
41 },
42}
43
44pub(crate) enum NormalizedFragment<'uri, B> {
45 Borrowed(&'uri str),
46 Owned(B),
47}
48
49impl<B: std::ops::Deref<Target = [u8]>> NormalizedFragment<'_, B> {
50 pub(crate) fn view(&self, scratch: &impl Scratch) -> Result<&str, TransformError> {
51 match self {
52 Self::Borrowed(text) => Ok(text),
53 Self::Owned(bytes) => {
54 scratch.charge(bytes.len())?;
55 std::str::from_utf8(bytes).map_err(|_| invalid_fragment())
56 }
57 }
58 }
59}
60
61fn invalid_fragment() -> TransformError {
62 TransformError::InvalidFragment
63}
64
65pub(crate) enum UriFailureKind {
66 MissingId,
67 Unsupported,
68}
69
70pub(crate) trait UriScratch: Scratch {
71 fn diagnostic(&self, kind: UriFailureKind, parts: &[&str]) -> TransformError;
72}
73
74impl UriScratch for Native {
75 fn diagnostic(&self, kind: UriFailureKind, parts: &[&str]) -> TransformError {
76 match kind {
77 UriFailureKind::MissingId => TransformError::ElementNotFound(parts.concat()),
78 UriFailureKind::Unsupported => TransformError::UnsupportedUri(parts.concat()),
79 }
80 }
81}
82
83impl<W: structured_asn1::memory::Work> UriScratch for Accounted<'_, W> {
84 fn diagnostic(&self, kind: UriFailureKind, parts: &[&str]) -> TransformError {
85 match self.text(parts) {
86 Ok(text) => match kind {
87 UriFailureKind::MissingId => {
88 TransformError::AccountedElementNotFound(text.into_owned())
89 }
90 UriFailureKind::Unsupported => {
91 TransformError::AccountedUnsupportedUri(text.into_owned())
92 }
93 },
94 Err(cause) => cause.into(),
95 }
96 }
97}
98
99enum FragmentFailure<'uri> {
100 MissingId(&'uri str),
101 Unsupported(&'uri str),
102}
103
104impl FragmentFailure<'_> {
105 fn diagnostic(self, scratch: &impl UriScratch) -> TransformError {
106 match self {
107 Self::MissingId(id) => scratch.diagnostic(UriFailureKind::MissingId, &[id]),
108 Self::Unsupported(fragment) => {
109 scratch.diagnostic(UriFailureKind::Unsupported, &["#", fragment])
110 }
111 }
112 }
113}
114
115pub(crate) fn validate_signing_reference_uri(
117 uri: &str,
118 policy: &crate::policy::SigningPolicy,
119) -> Result<(), crate::policy::PolicyViolation> {
120 if !policy.uris.references.allows(uri) {
121 return Err(crate::policy::PolicyViolation::Uri {
122 operation: "signing",
123 reason: "signing reference URI class is not permitted",
124 });
125 }
126 Ok(())
127}
128
129pub(crate) fn validate_signing_reference_request(
131 uri: &str,
132 has_external_resources: bool,
133) -> Result<(), crate::policy::PolicyViolation> {
134 if !uri.is_empty() && !uri.starts_with('#') && !has_external_resources {
135 return Err(crate::policy::PolicyViolation::Uri {
136 operation: "signing",
137 reason: "external signing references require request-scoped resource bytes",
138 });
139 }
140 Ok(())
141}
142
143struct ExternalResourceBudget {
144 remaining_total_bytes: Cell<usize>,
145 max_resource_bytes: usize,
146 max_total_bytes: usize,
147}
148
149pub(crate) enum ExternalResourceMapError {
150 Policy(crate::policy::PolicyViolation),
151 TotalLengthOverflow,
152}
153
154pub(crate) fn validate_external_resource_map(
155 resources: &HashMap<String, Vec<u8>>,
156 max_resource_bytes: usize,
157 max_total_bytes: usize,
158) -> Result<(), ExternalResourceMapError> {
159 let mut total = 0usize;
160 for bytes in resources.values() {
161 if bytes.len() > max_resource_bytes {
162 return Err(ExternalResourceMapError::Policy(
163 crate::policy::PolicyViolation::ResourceLimit {
164 resource: crate::policy::resource_name::EXTERNAL_RESOURCE_BYTES,
165 maximum: max_resource_bytes,
166 actual: bytes.len(),
167 },
168 ));
169 }
170 total = total
171 .checked_add(bytes.len())
172 .ok_or(ExternalResourceMapError::TotalLengthOverflow)?;
173 }
174 if total > max_total_bytes {
175 return Err(ExternalResourceMapError::Policy(
176 crate::policy::PolicyViolation::ResourceLimit {
177 resource: crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
178 maximum: max_total_bytes,
179 actual: total,
180 },
181 ));
182 }
183 Ok(())
184}
185
186pub(crate) struct ExternalResourceContext<'a> {
192 resources: Option<&'a HashMap<String, Vec<u8>>>,
193 budget: Rc<ExternalResourceBudget>,
194}
195
196impl<'a> ExternalResourceContext<'a> {
197 pub(crate) fn new(
198 resources: Option<&'a HashMap<String, Vec<u8>>>,
199 max_resource_bytes: usize,
200 max_total_bytes: usize,
201 ) -> Self {
202 Self {
203 resources,
204 budget: Rc::new(ExternalResourceBudget::with_limits(
205 max_resource_bytes,
206 max_total_bytes,
207 )),
208 }
209 }
210
211 pub(crate) fn is_configured(&self) -> bool {
212 self.resources.is_some()
213 }
214
215 pub(crate) fn bind<'doc>(
216 &'doc self,
217 doc: &'doc Document<'doc>,
218 registrations: &[crate::IdAttributeRegistration],
219 same_document_id_semantics: SameDocumentIdSemantics,
220 ) -> UriReferenceResolver<'doc>
221 where
222 'a: 'doc,
223 {
224 let mut resolver = UriReferenceResolver::with_id_registrations(doc, registrations)
225 .with_same_document_id_semantics(same_document_id_semantics);
226 resolver.external_resources = self.resources;
227 resolver.external_resource_budget = Rc::clone(&self.budget);
228 resolver
229 }
230}
231
232impl Default for ExternalResourceBudget {
233 fn default() -> Self {
234 Self::with_limits(
235 crate::hard_limits::EXTERNAL_RESOURCE_BYTE_CEILING,
236 crate::hard_limits::EXTERNAL_RESOURCE_TOTAL_BYTE_CEILING,
237 )
238 }
239}
240
241impl ExternalResourceBudget {
242 fn with_limits(max_resource_bytes: usize, max_total_bytes: usize) -> Self {
243 Self {
244 remaining_total_bytes: Cell::new(max_total_bytes),
245 max_resource_bytes,
246 max_total_bytes,
247 }
248 }
249
250 fn charge(&self, bytes: usize) -> Result<(), TransformError> {
251 if bytes > self.max_resource_bytes {
252 return Err(transform_resource_limit(
253 crate::policy::resource_name::EXTERNAL_RESOURCE_BYTES,
254 self.max_resource_bytes,
255 bytes,
256 ));
257 }
258 let remaining = self.remaining_total_bytes.get();
259 let Some(next) = remaining.checked_sub(bytes) else {
260 self.remaining_total_bytes.set(0);
261 return Err(transform_resource_limit(
262 crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
263 self.max_total_bytes,
264 self.max_total_bytes
265 .saturating_add(bytes.saturating_sub(remaining)),
266 ));
267 };
268 self.remaining_total_bytes.set(next);
269 Ok(())
270 }
271}
272
273pub struct UriReferenceResolver<'a> {
296 doc: &'a Document<'a>,
297 view: Option<crate::DocumentView<'a>>,
298 resource_identity: Option<String>,
299 id_registrations: Vec<crate::IdAttributeRegistration>,
300 id_index: ResolverIdIndex<'a>,
301 external_resources: Option<&'a HashMap<String, Vec<u8>>>,
302 external_resource_budget: Rc<ExternalResourceBudget>,
303 same_document_id_semantics: SameDocumentIdSemantics,
304}
305
306enum ResolverIdIndex<'a> {
307 Borrowed(XmlIdIndex<'a>),
308 Retained(HashMap<String, NodeId>),
309}
310
311#[derive(Clone, Debug, PartialEq, Eq, Hash)]
312pub(crate) enum TraversalDocumentIdentity {
313 Owned(crate::DocumentIdentity),
314 External(String),
315 Borrowed(usize),
316}
317
318impl<'a> ResolverIdIndex<'a> {
319 fn node(&self, document: &'a Document<'a>, id: &str) -> Option<Node<'a, 'a>> {
320 match self {
321 Self::Borrowed(index) => index.node(id),
322 Self::Retained(index) => index.get(id).and_then(|node| document.get_node(*node)),
323 }
324 }
325
326 fn contains(&self, id: &str) -> bool {
327 match self {
328 Self::Borrowed(index) => index.contains(id),
329 Self::Retained(index) => index.contains_key(id),
330 }
331 }
332
333 fn node_id(&self, id: &str) -> Option<NodeId> {
334 match self {
335 Self::Borrowed(index) => index.node_id(id),
336 Self::Retained(index) => index.get(id).copied(),
337 }
338 }
339
340 fn len(&self) -> usize {
341 match self {
342 Self::Borrowed(index) => index.len(),
343 Self::Retained(index) => index.len(),
344 }
345 }
346}
347
348impl<'a> UriReferenceResolver<'a> {
349 pub(crate) fn id_registrations(&self) -> &[crate::IdAttributeRegistration] {
350 &self.id_registrations
351 }
352 pub fn new(doc: &'a Document<'a>) -> Self {
354 Self::with_id_attrs(doc, &[])
355 }
356
357 pub fn with_id_attrs(doc: &'a Document<'a>, extra_attrs: &[&str]) -> Self {
366 let id_registrations = extra_attrs
367 .iter()
368 .map(|name| crate::IdAttributeRegistration::global(*name))
369 .collect::<Vec<_>>();
370 Self {
371 doc,
372 view: None,
373 resource_identity: None,
374 id_index: ResolverIdIndex::Borrowed(XmlIdIndex::with_registrations(
375 doc,
376 &id_registrations,
377 )),
378 id_registrations,
379 external_resources: None,
380 external_resource_budget: Rc::new(ExternalResourceBudget::default()),
381 same_document_id_semantics: SameDocumentIdSemantics::Specification,
382 }
383 }
384
385 pub fn with_id_registrations(
387 doc: &'a Document<'a>,
388 registrations: &[crate::IdAttributeRegistration],
389 ) -> Self {
390 let id_registrations = registrations.to_vec();
391 Self {
392 doc,
393 view: None,
394 resource_identity: None,
395 id_index: ResolverIdIndex::Borrowed(XmlIdIndex::with_registrations(
396 doc,
397 &id_registrations,
398 )),
399 id_registrations,
400 external_resources: None,
401 external_resource_budget: Rc::new(ExternalResourceBudget::default()),
402 same_document_id_semantics: SameDocumentIdSemantics::Specification,
403 }
404 }
405
406 pub(crate) fn with_document_view(
407 view: crate::DocumentView<'a>,
408 registrations: &[crate::IdAttributeRegistration],
409 ) -> Self {
410 let id_registrations = registrations.to_vec();
411 Self {
412 doc: view.document(),
413 view: Some(view),
414 resource_identity: None,
415 id_index: ResolverIdIndex::Retained(view.id_index(&id_registrations)),
416 id_registrations,
417 external_resources: None,
418 external_resource_budget: Rc::new(ExternalResourceBudget::default()),
419 same_document_id_semantics: SameDocumentIdSemantics::Specification,
420 }
421 }
422
423 pub(crate) fn with_same_document_id_semantics(
424 mut self,
425 semantics: SameDocumentIdSemantics,
426 ) -> Self {
427 self.same_document_id_semantics = semantics;
428 self
429 }
430
431 pub(crate) fn for_document_view<'b>(
434 &self,
435 view: crate::DocumentView<'b>,
436 ) -> UriReferenceResolver<'b>
437 where
438 'a: 'b,
439 {
440 UriReferenceResolver {
441 doc: view.document(),
442 view: Some(view),
443 resource_identity: None,
444 id_index: ResolverIdIndex::Retained(view.id_index(&self.id_registrations)),
445 id_registrations: self.id_registrations.clone(),
446 external_resources: self.external_resources,
447 external_resource_budget: Rc::clone(&self.external_resource_budget),
448 same_document_id_semantics: self.same_document_id_semantics,
449 }
450 }
451
452 pub(crate) fn for_external_document_view<'b>(
458 &self,
459 view: crate::DocumentView<'b>,
460 resource_identity: &str,
461 ) -> UriReferenceResolver<'b>
462 where
463 'a: 'b,
464 {
465 let mut resolver = self.for_document_view(view);
466 resolver.resource_identity = Some(resource_identity.to_owned());
467 resolver
468 }
469
470 pub(crate) fn traversal_document_identity(&self) -> TraversalDocumentIdentity {
471 if let Some(identity) = &self.resource_identity {
472 TraversalDocumentIdentity::External(identity.clone())
473 } else if let Some(view) = self.view {
474 TraversalDocumentIdentity::Owned(view.identity())
475 } else {
476 TraversalDocumentIdentity::Borrowed(self.doc as *const Document<'_> as usize)
477 }
478 }
479
480 pub fn with_external_resources(mut self, resources: &'a HashMap<String, Vec<u8>>) -> Self {
486 self.external_resources = Some(resources);
487 self
488 }
489
490 pub(crate) fn with_external_resource_limits(
491 mut self,
492 max_resource_bytes: usize,
493 max_total_bytes: usize,
494 ) -> Self {
495 self.external_resource_budget = Rc::new(ExternalResourceBudget::with_limits(
496 max_resource_bytes,
497 max_total_bytes,
498 ));
499 self
500 }
501
502 pub(crate) fn external_resource(&self, uri: &str) -> Result<Option<&'a [u8]>, TransformError> {
503 let Some(bytes) = self
504 .external_resources
505 .and_then(|resources| resources.get(uri))
506 else {
507 return Ok(None);
508 };
509 self.external_resource_budget.charge(bytes.len())?;
510 Ok(Some(bytes))
511 }
512
513 fn external_resource_input(
514 &self,
515 uri: &str,
516 ) -> Result<Option<ReferenceInput<'a>>, TransformError> {
517 let Some((uri, bytes)) = self
518 .external_resources
519 .and_then(|resources| resources.get_key_value(uri))
520 else {
521 return Ok(None);
522 };
523 self.external_resource_budget.charge(bytes.len())?;
524 Ok(Some(ReferenceInput::Binary { bytes, uri }))
525 }
526
527 pub(crate) fn external_resource_identity(
528 &self,
529 uri: &str,
530 ) -> Option<crate::operation::OperationResourceIdentity> {
531 self.external_resources
532 .and_then(|resources| resources.get(uri))
533 .map(|bytes| crate::operation::OperationResourceIdentity::external(uri, bytes))
534 }
535
536 pub(crate) fn external_resource_set_identity(
537 &self,
538 ) -> crate::operation::OperationResourceIdentity {
539 use sha2::Digest;
540
541 let mut entries = self
542 .external_resources
543 .into_iter()
544 .flat_map(HashMap::iter)
545 .collect::<Vec<_>>();
546 entries.sort_unstable_by_key(|(uri, _)| *uri);
547 let mut hasher = sha2::Sha256::new();
548 for (uri, bytes) in entries {
549 hasher.update(uri.len().to_be_bytes());
550 hasher.update(uri.as_bytes());
551 hasher.update(bytes.len().to_be_bytes());
552 hasher.update(bytes);
553 }
554 crate::operation::OperationResourceIdentity::External {
555 uri: "caller-owned-external-resource-set".to_owned(),
556 fingerprint: hasher.finalize().into(),
557 }
558 }
559
560 pub fn dereference(&self, uri: &str) -> Result<TransformData<'a>, TransformError> {
573 self.dereference_with_optional_budget(uri, None)
574 }
575
576 pub(crate) fn dereference_with_budget(
577 &self,
578 uri: &str,
579 budget: &NodeSetMaterializationBudget,
580 ) -> Result<TransformData<'a>, TransformError> {
581 self.dereference_with_optional_budget(uri, Some(budget))
582 }
583
584 pub(crate) fn dereference_from_with_budget(
585 &self,
586 uri: &str,
587 origin: Node<'_, '_>,
588 budget: &NodeSetMaterializationBudget,
589 xml_base_budget: &XmlBaseResolutionBudget,
590 ) -> Result<TransformData<'a>, TransformError> {
591 if uri.is_empty() || uri.starts_with('#') {
594 return self.dereference_with_budget(uri, budget);
595 }
596 let resolved = resolve_uri_from_node_with_budget(origin, uri, xml_base_budget)
597 .map_err(map_xml_base_resolution_error)?;
598 self.dereference_with_budget(&resolved, budget)
599 }
600
601 pub(crate) fn replay_input_from_with_budget<S: UriScratch>(
604 &self,
605 uri: &str,
606 origin: Node<'_, '_>,
607 budget: &NodeSetMaterializationBudget,
608 xml_base_budget: &XmlBaseResolutionBudget,
609 scratch: &S,
610 ) -> Result<Option<ReferenceInput<'a>>, TransformError> {
611 if uri.is_empty() {
612 return Ok(Some(ReferenceInput::Xml {
613 root: self.doc.root(),
614 with_comments: false,
615 }));
616 }
617 if let Some(fragment) = uri.strip_prefix('#') {
618 let normalized = Self::normalize_fragment_in(
619 fragment,
620 Some(budget),
621 self.same_document_id_semantics,
622 scratch,
623 )?;
624 let fragment = normalized.view(scratch)?;
625 scratch.charge(fragment.len())?;
626 return self
627 .normalized_fragment_input(fragment)
628 .map(Some)
629 .map_err(|cause| cause.diagnostic(scratch));
630 }
631 let resolved = crate::c14n::xml_base::resolve_uri_from_node_in(
632 origin,
633 uri,
634 None,
635 xml_base_budget,
636 scratch,
637 )
638 .map_err(map_xml_base_execution_error)?;
639 let resolved = resolved.view().map_err(map_xml_base_execution_error)?;
640 scratch
641 .charge(resolved.len())
642 .map_err(map_xml_base_execution_error)?;
643 self.external_resource_input(resolved)
644 }
645
646 fn dereference_with_optional_budget(
647 &self,
648 uri: &str,
649 budget: Option<&NodeSetMaterializationBudget>,
650 ) -> Result<TransformData<'a>, TransformError> {
651 if uri.is_empty() {
652 let nodes = match self.view {
656 Some(view) => NodeSet::entire_document_without_comments_from_view(view, budget)?,
657 None => match budget {
658 Some(budget) => {
659 NodeSet::entire_document_without_comments_with_budget(self.doc, budget)?
660 }
661 None => NodeSet::entire_document_without_comments(self.doc)?,
662 },
663 };
664 Ok(TransformData::NodeSet(nodes))
665 } else if let Some(fragment) = uri.strip_prefix('#') {
666 self.dereference_fragment(fragment, budget)
667 } else {
668 self.external_resource_input(uri)?
669 .map(|input| match input {
670 ReferenceInput::Binary { bytes, uri } => TransformData::Resource { bytes, uri },
671 ReferenceInput::Xml { .. } => unreachable!("external resources are octets"),
672 })
673 .ok_or_else(|| TransformError::UnsupportedUri(uri.to_string()))
674 }
675 }
676
677 fn dereference_fragment(
684 &self,
685 fragment: &str,
686 budget: Option<&NodeSetMaterializationBudget>,
687 ) -> Result<TransformData<'a>, TransformError> {
688 let normalized =
689 Self::normalize_fragment(fragment, budget, self.same_document_id_semantics)?;
690 let fragment = normalized.as_ref();
691 self.dereference_normalized_fragment(fragment, budget)
692 }
693
694 fn dereference_normalized_fragment(
695 &self,
696 fragment: &str,
697 budget: Option<&NodeSetMaterializationBudget>,
698 ) -> Result<TransformData<'a>, TransformError> {
699 let ReferenceInput::Xml {
700 root,
701 with_comments,
702 } = self
703 .normalized_fragment_input(fragment)
704 .map_err(|cause| cause.diagnostic(&Native))?
705 else {
706 unreachable!("a same-document fragment resolves to XML");
707 };
708 NodeSet::projected(root, with_comments, budget).map(TransformData::NodeSet)
709 }
710
711 fn normalized_fragment_input<'uri>(
712 &self,
713 fragment: &'uri str,
714 ) -> Result<ReferenceInput<'a>, FragmentFailure<'uri>> {
715 if fragment.is_empty() {
716 return Err(FragmentFailure::Unsupported(fragment));
718 }
719
720 if is_xpointer_root(fragment) {
721 Ok(ReferenceInput::Xml {
725 root: self.doc.root(),
726 with_comments: true,
727 })
728 } else {
729 let (id, with_comments) = self
730 .same_document_id_fragment(fragment)
731 .ok_or(FragmentFailure::Unsupported(fragment))?;
732 self.id_index
733 .node(self.doc, id)
734 .map(|root| ReferenceInput::Xml {
735 root,
736 with_comments,
737 })
738 .ok_or(FragmentFailure::MissingId(id))
739 }
740 }
741
742 fn same_document_id_fragment<'uri>(&self, fragment: &'uri str) -> Option<(&'uri str, bool)> {
743 Self::id_fragment(fragment, self.same_document_id_semantics)
744 }
745
746 pub(crate) fn id_fragment(
747 fragment: &str,
748 semantics: SameDocumentIdSemantics,
749 ) -> Option<(&str, bool)> {
750 if fragment.is_empty() {
751 return None;
752 }
753 if let Some(id) = parse_xpointer_id_fragment(fragment) {
754 if id.is_empty() {
757 return None;
758 }
759 return Some((id, true));
760 }
761 if fragment.starts_with("xpointer(") {
762 return None;
763 }
764 match semantics {
765 SameDocumentIdSemantics::Specification if !is_xml_ncname(fragment) => {
766 return None;
767 }
768 SameDocumentIdSemantics::XmlSecBarename if fragment.contains('\'') => {
769 return None;
770 }
771 SameDocumentIdSemantics::Specification
772 | SameDocumentIdSemantics::XmlSecBarename
773 | SameDocumentIdSemantics::XmlSecVisa3d => {}
774 }
775 Some((fragment, false))
776 }
777
778 pub fn has_id(&self, id: &str) -> bool {
780 self.id_index.contains(id)
781 }
782
783 pub fn node_for_id(&self, id: &str) -> Option<Node<'a, 'a>> {
788 self.id_index.node(self.doc, id)
789 }
790
791 pub fn node_for_same_document_reference(
794 &self,
795 uri: &str,
796 ) -> Result<Option<Node<'a, 'a>>, TransformError> {
797 self.node_for_same_document_reference_with_budget(uri, None)
798 }
799
800 pub(crate) fn node_for_same_document_reference_with_budget(
801 &self,
802 uri: &str,
803 budget: Option<&NodeSetMaterializationBudget>,
804 ) -> Result<Option<Node<'a, 'a>>, TransformError> {
805 self.node_for_same_document_reference_in(uri, budget, &Native)
806 }
807
808 pub(crate) fn node_for_same_document_reference_in(
809 &self,
810 uri: &str,
811 budget: Option<&NodeSetMaterializationBudget>,
812 scratch: &impl UriScratch,
813 ) -> Result<Option<Node<'a, 'a>>, TransformError> {
814 if uri.is_empty() {
815 return Ok(Some(self.doc.root_element()));
816 }
817 let target = self.node_id_for_reference_resource_in(uri, budget, scratch)?;
818 if target == Some(self.doc.root().id()) {
819 return Err(scratch.diagnostic(UriFailureKind::Unsupported, &[uri]));
820 }
821 Ok(target.and_then(|id| self.doc.get_node(id)))
822 }
823
824 pub(crate) fn bare_name_node_in(
829 &self,
830 uri: &str,
831 budget: Option<&NodeSetMaterializationBudget>,
832 scratch: &impl UriScratch,
833 ) -> Result<Option<Option<Node<'a, 'a>>>, TransformError> {
834 let Some(fragment) = uri.strip_prefix('#') else {
835 return Ok(None);
836 };
837 let normalized = match Self::normalize_fragment_in(
838 fragment,
839 budget,
840 self.same_document_id_semantics,
841 scratch,
842 ) {
843 Ok(normalized) => normalized,
844 Err(TransformError::InvalidFragment) => return Ok(None),
845 Err(error) => return Err(error),
846 };
847 let fragment = normalized.view(scratch)?;
848 scratch.charge(fragment.len())?;
849 Ok(match self.same_document_id_fragment(fragment) {
853 Some((id, false)) => Some(self.id_index.node(self.doc, id)),
854 Some((_, true)) | None => None,
855 })
856 }
857
858 #[cfg(feature = "xmlenc")]
861 pub(crate) fn same_document_reference_targets(&self, uri: &str, target: NodeId) -> bool {
862 if uri.is_empty() {
863 return self.doc.root_element().id() == target;
864 }
865 let Some(fragment) = uri.strip_prefix('#') else {
866 return false;
867 };
868 let Ok(normalized) =
869 Self::normalize_fragment(fragment, None, self.same_document_id_semantics)
870 else {
871 return false;
872 };
873 let fragment = normalized.as_ref();
874 if fragment.is_empty() || is_xpointer_root(fragment) {
875 return false;
876 }
877 let Some((id, _)) = self.same_document_id_fragment(fragment) else {
878 return false;
879 };
880 self.id_index.node_id(id) == Some(target)
881 }
882
883 pub(crate) fn node_id_for_same_document_reference(
886 &self,
887 uri: &str,
888 ) -> Result<Option<NodeId>, TransformError> {
889 let target = self.node_id_for_reference_resource(uri, None)?;
890 if uri.is_empty() || target == Some(self.doc.root().id()) {
891 return Err(TransformError::UnsupportedUri(uri.to_owned()));
892 }
893 Ok(target)
894 }
895
896 pub(crate) fn node_id_for_reference_resource(
899 &self,
900 uri: &str,
901 budget: Option<&NodeSetMaterializationBudget>,
902 ) -> Result<Option<NodeId>, TransformError> {
903 self.node_id_for_reference_resource_in(uri, budget, &Native)
904 }
905
906 fn node_id_for_reference_resource_in(
907 &self,
908 uri: &str,
909 budget: Option<&NodeSetMaterializationBudget>,
910 scratch: &impl UriScratch,
911 ) -> Result<Option<NodeId>, TransformError> {
912 if uri.is_empty() {
913 return Ok(Some(self.doc.root().id()));
914 }
915 let fragment = uri
916 .strip_prefix('#')
917 .ok_or_else(|| scratch.diagnostic(UriFailureKind::Unsupported, &[uri]))?;
918 let normalized = Self::normalize_fragment_in(
919 fragment,
920 budget,
921 self.same_document_id_semantics,
922 scratch,
923 )?;
924 let fragment = normalized.view(scratch)?;
925 scratch.charge(fragment.len())?;
926 if fragment.is_empty() {
927 return Err(scratch.diagnostic(UriFailureKind::Unsupported, &[uri]));
928 }
929 if is_xpointer_root(fragment) {
930 return Ok(Some(self.doc.root().id()));
931 }
932 let (id, _) = self
933 .same_document_id_fragment(fragment)
934 .ok_or_else(|| scratch.diagnostic(UriFailureKind::Unsupported, &[uri]))?;
935 Ok(self.id_index.node_id(id))
936 }
937
938 pub(crate) fn node_for_node_id(&self, id: NodeId) -> Option<Node<'a, 'a>> {
939 self.doc.get_node(id)
940 }
941
942 pub fn id_count(&self) -> usize {
944 self.id_index.len()
945 }
946
947 pub(crate) fn normalize_fragment<'uri>(
948 fragment: &'uri str,
949 budget: Option<&NodeSetMaterializationBudget>,
950 semantics: SameDocumentIdSemantics,
951 ) -> Result<std::borrow::Cow<'uri, str>, TransformError> {
952 match Self::normalize_fragment_in(fragment, budget, semantics, &Native)? {
953 NormalizedFragment::Borrowed(text) => Ok(std::borrow::Cow::Borrowed(text)),
954 NormalizedFragment::Owned(bytes) => Ok(std::borrow::Cow::Owned(
955 String::from_utf8(bytes).map_err(|_| invalid_fragment())?,
956 )),
957 }
958 }
959
960 pub(crate) fn normalize_fragment_in<'uri, S: Scratch>(
961 fragment: &'uri str,
962 budget: Option<&NodeSetMaterializationBudget>,
963 semantics: SameDocumentIdSemantics,
964 scratch: &S,
965 ) -> Result<NormalizedFragment<'uri, S::Bytes>, TransformError> {
966 let invalid = invalid_fragment;
967 if fragment.len() > crate::hard_limits::XML_DOCUMENT_BYTE_CEILING {
968 return Err(invalid());
969 }
970 scratch.charge(fragment.len())?;
971 let mut result = NormalizedFragment::Borrowed(fragment);
972 if fragment.contains('%')
978 && (semantics == SameDocumentIdSemantics::Specification
979 || has_encoded_xpointer_prefix(fragment))
980 {
981 if let Some(budget) = budget {
982 budget.reserve_workspace(fragment.len())?;
983 }
984 let mut decoded = scratch.bytes();
985 decoded.reserve(fragment.len())?;
986 scratch.charge(fragment.len())?;
987 let bytes = fragment.as_bytes();
988 let mut position = 0;
989 let mut chunk = [0; 1024];
990 let mut used = 0;
991 while position < bytes.len() {
992 let byte = if bytes[position] == b'%' {
993 let high = bytes
994 .get(position + 1)
995 .and_then(|byte| hex_digit(*byte))
996 .ok_or_else(invalid)?;
997 let low = bytes
998 .get(position + 2)
999 .and_then(|byte| hex_digit(*byte))
1000 .ok_or_else(invalid)?;
1001 position += 3;
1002 high * 16 + low
1003 } else {
1004 let byte = bytes[position];
1005 position += 1;
1006 byte
1007 };
1008 chunk[used] = byte;
1009 used += 1;
1010 if used == chunk.len() {
1011 decoded.append(&chunk)?;
1012 used = 0;
1013 }
1014 }
1015 decoded.append(&chunk[..used])?;
1016 result = NormalizedFragment::Owned(decoded);
1017 }
1018 let text = result.view(scratch)?;
1019 if text.starts_with("xpointer(") {
1020 scratch.charge(text.len())?;
1023 let bytes = text.as_bytes();
1024 let mut depth = 0usize;
1025 let mut position = "xpointer".len();
1026 while position < bytes.len() {
1027 match bytes[position] {
1028 b'^' => {
1029 if !matches!(bytes.get(position + 1), Some(b'(' | b')' | b'^')) {
1030 return Err(invalid());
1031 }
1032 position += 2;
1033 continue;
1034 }
1035 b'(' => depth += 1,
1036 b')' => {
1037 if depth == 0 {
1038 return Err(invalid());
1039 }
1040 depth -= 1;
1041 if depth == 0 && position + 1 != bytes.len() {
1042 return Err(invalid());
1043 }
1044 }
1045 _ => {}
1046 }
1047 position += 1;
1048 }
1049 if depth != 0 {
1050 return Err(invalid());
1051 }
1052 scratch.charge(text.len())?;
1053 if text.contains('^') {
1054 if let NormalizedFragment::Borrowed(text) = result {
1055 if let Some(budget) = budget {
1056 budget.reserve_workspace(text.len())?;
1057 }
1058 let mut bytes = scratch.bytes();
1059 bytes.reserve(text.len())?;
1060 bytes.append(text.as_bytes())?;
1061 result = NormalizedFragment::Owned(bytes);
1062 }
1063 let NormalizedFragment::Owned(ref mut bytes) = result else {
1064 unreachable!()
1065 };
1066 scratch.charge(bytes.len())?;
1067 let mut escaped = false;
1068 let mut write = 0;
1069 for read in 0..bytes.len() {
1070 let character = bytes[read];
1071 if escaped {
1072 escaped = false;
1073 } else if character == b'^' {
1074 escaped = true;
1075 continue;
1076 }
1077 bytes[write] = character;
1078 write += 1;
1079 }
1080 bytes.truncate(write);
1081 }
1082 }
1083 Ok(result)
1084 }
1085}
1086
1087fn hex_digit(byte: u8) -> Option<u8> {
1088 match byte {
1089 b'0'..=b'9' => Some(byte - b'0'),
1090 b'a'..=b'f' => Some(byte - b'a' + 10),
1091 b'A'..=b'F' => Some(byte - b'A' + 10),
1092 _ => None,
1093 }
1094}
1095
1096fn has_encoded_xpointer_prefix(fragment: &str) -> bool {
1097 let mut bytes = fragment.bytes();
1099 for expected in b"xpointer(" {
1100 let Some(mut actual) = bytes.next() else {
1101 return false;
1102 };
1103 if actual == b'%' {
1104 let Some(high) = bytes.next().and_then(hex_digit) else {
1105 return false;
1106 };
1107 let Some(low) = bytes.next().and_then(hex_digit) else {
1108 return false;
1109 };
1110 actual = high * 16 + low;
1111 }
1112 if actual != *expected {
1113 return false;
1114 }
1115 }
1116 true
1117}
1118
1119fn is_xpointer_root(fragment: &str) -> bool {
1120 fragment
1121 .strip_prefix("xpointer(")
1122 .and_then(|value| value.strip_suffix(')'))
1123 .is_some_and(|expression| xml_space_trim(expression) == "/")
1124}
1125
1126pub(crate) fn map_xml_base_execution_error(error: crate::c14n::C14nError) -> TransformError {
1127 match error {
1128 crate::c14n::C14nError::XmlBaseComponentsTooLarge { max, actual } => {
1129 map_xml_base_resolution_error(XmlBaseResolutionError::Components {
1130 maximum: max,
1131 actual,
1132 })
1133 }
1134 crate::c14n::C14nError::XmlBaseResolutionTooLarge { max_bytes, actual } => {
1135 map_xml_base_resolution_error(XmlBaseResolutionError::Bytes {
1136 maximum: max_bytes,
1137 actual,
1138 })
1139 }
1140 error => error.into(),
1141 }
1142}
1143
1144pub(crate) fn map_xml_base_resolution_error(error: XmlBaseResolutionError) -> TransformError {
1145 match error {
1146 XmlBaseResolutionError::Components { maximum, actual } => transform_resource_limit(
1147 crate::policy::resource_name::XML_BASE_COMPONENTS,
1148 maximum,
1149 actual,
1150 ),
1151 XmlBaseResolutionError::Bytes { maximum, actual } => transform_resource_limit(
1152 crate::policy::resource_name::XML_BASE_RESOLUTION_BYTES,
1153 maximum,
1154 actual,
1155 ),
1156 }
1157}
1158
1159pub(crate) fn parse_xpointer_id_fragment(fragment: &str) -> Option<&str> {
1162 let expression = fragment.strip_prefix("xpointer(")?.strip_suffix(')')?;
1166 let argument = xml_space_trim(expression)
1167 .strip_prefix("id")?
1168 .trim_start_matches(is_xml_space)
1169 .strip_prefix('(')?;
1170 let literal = xml_space_trim(argument.strip_suffix(')')?);
1171 let quote = literal.as_bytes().first().copied()?;
1172 if !matches!(quote, b'\'' | b'"')
1173 || literal.len() < 2
1174 || literal.as_bytes().last() != Some("e)
1175 {
1176 return None;
1177 }
1178 let value = &literal[1..literal.len() - 1];
1179 if value.as_bytes().contains("e) {
1180 return None;
1181 }
1182 Some(value)
1183}
1184
1185fn is_xml_space(character: char) -> bool {
1186 matches!(character, ' ' | '\t' | '\r' | '\n')
1187}
1188
1189fn xml_space_trim(value: &str) -> &str {
1190 value.trim_matches(is_xml_space)
1191}
1192
1193#[cfg(test)]
1194#[allow(clippy::unwrap_used)]
1195mod tests {
1196 use super::super::types::NodeSet;
1197 use super::*;
1198
1199 #[test]
1200 fn fragment_current_execution_admits_work_and_capacity() {
1201 use crate::c14n::scratch::Accounted;
1204 use structured_asn1::memory::{Memory, MemoryAccount};
1205
1206 let work = Memory::new(std::sync::Arc::new(MemoryAccount::new(1 << 20, 1 << 20)));
1207 for (source, expected) in [
1208 ("target", "target"),
1209 ("caf%C3%A9", "café"),
1210 ("xpointer(id('a^(b^)'))", "xpointer(id('a(b)'))"),
1211 ("%78pointer(id('a%5E(b%5E)'))", "xpointer(id('a(b)'))"),
1212 ] {
1213 let normalized = UriReferenceResolver::normalize_fragment_in(
1214 source,
1215 None,
1216 SameDocumentIdSemantics::Specification,
1217 &Accounted(&work),
1218 )
1219 .unwrap();
1220 assert_eq!(normalized.view(&Accounted(&work)).unwrap(), expected);
1221 drop(normalized);
1222 assert_eq!(work.used(), 0);
1223 }
1224 let tiny = Memory::new(std::sync::Arc::new(MemoryAccount::new(1, 1 << 20)));
1225 assert!(
1226 UriReferenceResolver::normalize_fragment_in(
1227 "tar%67et",
1228 None,
1229 SameDocumentIdSemantics::Specification,
1230 &Accounted(&tiny),
1231 )
1232 .is_err()
1233 );
1234 let zero = Memory::new(std::sync::Arc::new(MemoryAccount::new(1 << 20, 0)));
1235 assert!(
1236 UriReferenceResolver::normalize_fragment_in(
1237 "target",
1238 None,
1239 SameDocumentIdSemantics::Specification,
1240 &Accounted(&zero),
1241 )
1242 .is_err()
1243 );
1244 }
1245
1246 #[test]
1247 fn fragment_query_local_and_aggregate_boundaries_agree() {
1248 use crate::c14n::scratch::Accounted;
1252 use structured_asn1::{
1253 Encoding, Limits, Parser,
1254 memory::{Memory, MemoryAccount},
1255 };
1256 let source = "%78pointer(id('caf%C3%A9%5E(x%5E)'))";
1257 let run = |local: u64, aggregate: usize| {
1258 let memory = Memory::new(std::sync::Arc::new(MemoryAccount::new(4096, aggregate)));
1259 let parser = Parser::new(
1260 Encoding::Der,
1261 Limits {
1262 max_work: local,
1263 ..Limits::default()
1264 },
1265 memory.clone(),
1266 );
1267 let reader = parser.read(&[], 0).unwrap();
1268 let work = reader.execution();
1269 let before = memory.work_used();
1270 let result = (|| {
1271 let fragment = UriReferenceResolver::normalize_fragment_in(
1272 source,
1273 None,
1274 SameDocumentIdSemantics::Specification,
1275 &Accounted(&work),
1276 )?;
1277 assert_eq!(fragment.view(&Accounted(&work))?, "xpointer(id('café(x)'))");
1278 Ok::<_, TransformError>(())
1279 })();
1280 assert_eq!(memory.used(), 0);
1281 (result, memory.work_used() - before)
1282 };
1283 let (result, cost) = run(1 << 20, 1 << 20);
1284 result.unwrap();
1285 assert!(cost > source.len());
1286 for (local, aggregate, success) in [
1287 (cost as u64, cost, true),
1288 (cost as u64 - 1, cost, false),
1289 (cost as u64, cost - 1, false),
1290 (0, cost, false),
1291 (cost as u64, 0, false),
1292 ] {
1293 let (result, _) = run(local, aggregate);
1294 if success {
1295 result.unwrap();
1296 } else {
1297 assert!(matches!(
1298 result,
1299 Err(TransformError::C14n(
1300 crate::c14n::C14nError::SharedExecution(
1301 structured_asn1::memory::MemoryError::WorkExceeded
1302 )
1303 ))
1304 ));
1305 }
1306 }
1307 }
1308
1309 #[test]
1310 fn escaped_fragments_preserve_target_and_comment_semantics() {
1311 let document =
1314 Document::parse(r#"<root><item ID="café"><!--kept--></item><item ID="a(b)"/></root>"#)
1315 .unwrap();
1316 let resolver = UriReferenceResolver::new(&document);
1317 let plain = resolver
1318 .dereference("#caf%C3%A9")
1319 .unwrap()
1320 .into_node_set()
1321 .unwrap();
1322 let pointer = resolver
1323 .dereference("#xpointer(id('caf%C3%A9'))")
1324 .unwrap()
1325 .into_node_set()
1326 .unwrap();
1327 let comment = document
1328 .descendants()
1329 .find(|node| node.is_comment())
1330 .unwrap();
1331 assert!(!plain.contains(comment));
1332 assert!(pointer.contains(comment));
1333 assert!(resolver.dereference("#xpointer(id('a^(b^)'))").is_ok());
1334 assert!(resolver.dereference("#xpointer( / )").is_ok());
1335 for uri in [
1336 "#caf%",
1337 "#caf%FF",
1338 "#xpointer(id('a^x'))",
1339 "#xpointer(id('a(b'))",
1340 "#xpointer(id('a') or 'b')",
1341 ] {
1342 assert!(resolver.dereference(uri).is_err(), "{uri}");
1343 }
1344 }
1345
1346 #[test]
1347 fn fragment_decoding_reserves_workspace_before_allocation() {
1348 let budget = NodeSetMaterializationBudget::with_limit(0);
1351 assert!(
1352 UriReferenceResolver::normalize_fragment(
1353 "target",
1354 Some(&budget),
1355 SameDocumentIdSemantics::Specification
1356 )
1357 .is_ok()
1358 );
1359 assert!(matches!(
1360 UriReferenceResolver::normalize_fragment(
1361 "tar%67et",
1362 Some(&budget),
1363 SameDocumentIdSemantics::Specification
1364 ),
1365 Err(TransformError::Policy(_))
1366 ));
1367 assert!(matches!(
1368 UriReferenceResolver::normalize_fragment(
1369 "xpointer(id('a^(b^)'))",
1370 Some(&budget),
1371 SameDocumentIdSemantics::Specification
1372 ),
1373 Err(TransformError::Policy(_))
1374 ));
1375 }
1376
1377 #[test]
1378 fn xpointer_id_requires_one_complete_literal() {
1379 assert_eq!(parse_xpointer_id_fragment("xpointer(id('a' or 'b'))"), None);
1382 assert_eq!(
1383 parse_xpointer_id_fragment("xpointer(id(\"a\" or \"b\"))"),
1384 None
1385 );
1386 assert_eq!(
1387 parse_xpointer_id_fragment("xpointer( id ( 'target' ) )"),
1388 Some("target")
1389 );
1390 }
1391
1392 #[test]
1393 fn empty_same_document_node_reference_selects_the_document_element() {
1394 let document = Document::parse("<root><child/></root>").unwrap();
1397 let resolver = UriReferenceResolver::new(&document);
1398
1399 assert_eq!(
1400 resolver
1401 .node_for_same_document_reference("")
1402 .unwrap()
1403 .unwrap(),
1404 document.root_element()
1405 );
1406 }
1407
1408 #[test]
1409 fn empty_uri_returns_whole_document() {
1410 let xml = "<root><child>text</child></root>";
1411 let doc = Document::parse(xml).unwrap();
1412 let resolver = UriReferenceResolver::new(&doc);
1413
1414 let data = resolver.dereference("").unwrap();
1415 let node_set = data.into_node_set().unwrap();
1416
1417 let root = doc.root_element();
1419 assert!(node_set.contains(root));
1420 let child = root.first_child().unwrap();
1421 assert!(node_set.contains(child));
1422 }
1423
1424 #[test]
1425 fn empty_uri_excludes_comments() {
1426 let xml = "<root><!-- comment --><child/></root>";
1427 let doc = Document::parse(xml).unwrap();
1428 let resolver = UriReferenceResolver::new(&doc);
1429
1430 let data = resolver.dereference("").unwrap();
1431 let node_set = data.into_node_set().unwrap();
1432
1433 for node in doc.descendants() {
1435 if node.is_comment() {
1436 assert!(
1437 !node_set.contains(node),
1438 "comment should be excluded for empty URI"
1439 );
1440 }
1441 }
1442 assert!(node_set.contains(doc.root_element()));
1444 }
1445
1446 #[test]
1447 fn fragment_uri_resolves_by_id_attr() {
1448 let xml = r#"<root><item ID="abc">content</item><item ID="def">other</item></root>"#;
1449 let doc = Document::parse(xml).unwrap();
1450 let resolver = UriReferenceResolver::new(&doc);
1451
1452 let data = resolver.dereference("#abc").unwrap();
1453 let node_set = data.into_node_set().unwrap();
1454
1455 let abc_elem = doc
1457 .descendants()
1458 .find(|n| n.attribute("ID") == Some("abc"))
1459 .unwrap();
1460 assert!(node_set.contains(abc_elem));
1461
1462 let text_child = abc_elem.first_child().unwrap();
1464 assert!(node_set.contains(text_child));
1465
1466 assert!(!node_set.contains(doc.root_element()));
1468
1469 let def_elem = doc
1471 .descendants()
1472 .find(|n| n.attribute("ID") == Some("def"))
1473 .unwrap();
1474 assert!(!node_set.contains(def_elem));
1475 }
1476
1477 #[test]
1478 fn fragment_uri_resolves_lowercase_id() {
1479 let xml = r#"<root><item id="lower">text</item></root>"#;
1480 let doc = Document::parse(xml).unwrap();
1481 let resolver = UriReferenceResolver::new(&doc);
1482
1483 let data = resolver.dereference("#lower").unwrap();
1484 let node_set = data.into_node_set().unwrap();
1485
1486 let elem = doc
1487 .descendants()
1488 .find(|n| n.attribute("id") == Some("lower"))
1489 .unwrap();
1490 assert!(node_set.contains(elem));
1491 }
1492
1493 #[test]
1494 fn fragment_uri_resolves_mixed_case_id() {
1495 let xml = r#"<root><ds:Signature Id="sig1" xmlns:ds="http://www.w3.org/2000/09/xmldsig#"/></root>"#;
1496 let doc = Document::parse(xml).unwrap();
1497 let resolver = UriReferenceResolver::new(&doc);
1498
1499 assert!(resolver.has_id("sig1"));
1500 let data = resolver.dereference("#sig1").unwrap();
1501 assert!(data.into_node_set().is_ok());
1502 }
1503
1504 #[test]
1505 fn fragment_uri_not_found() {
1506 let xml = "<root><child>text</child></root>";
1507 let doc = Document::parse(xml).unwrap();
1508 let resolver = UriReferenceResolver::new(&doc);
1509
1510 let result = resolver.dereference("#nonexistent");
1511 assert!(result.is_err());
1512 match result.unwrap_err() {
1513 TransformError::ElementNotFound(id) => assert_eq!(id, "nonexistent"),
1514 other => panic!("expected ElementNotFound, got: {other:?}"),
1515 }
1516 }
1517
1518 #[test]
1519 fn missing_fragment_diagnostic_obeys_current_capacity() {
1520 use crate::c14n::scratch::Accounted;
1523 use structured_asn1::memory::{Memory, MemoryAccount, MemoryError};
1524
1525 let doc = Document::parse("<root/>").unwrap();
1526 let resolver = UriReferenceResolver::new(&doc);
1527 let uri = format!("#{}", "x".repeat(65536));
1528 let work = Memory::new(std::sync::Arc::new(MemoryAccount::new(1024, 1 << 22)));
1529 let result = resolver.replay_input_from_with_budget(
1530 &uri,
1531 doc.root_element(),
1532 &NodeSetMaterializationBudget::default(),
1533 &XmlBaseResolutionBudget::default(),
1534 &Accounted(&work),
1535 );
1536 assert!(matches!(
1537 result,
1538 Err(TransformError::C14n(
1539 crate::c14n::C14nError::SharedExecution(MemoryError::LimitExceeded)
1540 ))
1541 ));
1542 assert_eq!(work.used(), 0);
1543 }
1544
1545 #[test]
1546 fn rejected_fragment_diagnostics_obey_current_capacity() {
1547 use structured_asn1::memory::{Memory, MemoryAccount, MemoryError};
1550 let doc = Document::parse("<root/>").unwrap();
1551 let resolver = UriReferenceResolver::new(&doc);
1552 let uri = format!("#xpointer('{}')", "x".repeat(65536));
1553 let work = Memory::new(std::sync::Arc::new(MemoryAccount::new(1024, 1 << 22)));
1554 assert!(matches!(
1555 resolver.replay_input_from_with_budget(
1556 &uri,
1557 doc.root_element(),
1558 &NodeSetMaterializationBudget::default(),
1559 &XmlBaseResolutionBudget::default(),
1560 &Accounted(&work),
1561 ),
1562 Err(TransformError::C14n(
1563 crate::c14n::C14nError::SharedExecution(MemoryError::LimitExceeded)
1564 ))
1565 ));
1566 assert_eq!(work.used(), 0);
1567 let work = Memory::new(std::sync::Arc::new(MemoryAccount::new(0, 1024)));
1568 assert!(matches!(
1569 resolver.replay_input_from_with_budget(
1570 "#xpointer(^)",
1571 doc.root_element(),
1572 &NodeSetMaterializationBudget::default(),
1573 &XmlBaseResolutionBudget::default(),
1574 &Accounted(&work),
1575 ),
1576 Err(TransformError::InvalidFragment)
1577 ));
1578 assert_eq!(work.used(), 0);
1579 }
1580
1581 #[test]
1582 fn unsupported_external_uri() {
1583 let xml = "<root/>";
1584 let doc = Document::parse(xml).unwrap();
1585 let resolver = UriReferenceResolver::new(&doc);
1586
1587 let result = resolver.dereference("http://example.com/doc.xml");
1588 assert!(result.is_err());
1589 match result.unwrap_err() {
1590 TransformError::UnsupportedUri(uri) => {
1591 assert_eq!(uri, "http://example.com/doc.xml")
1592 }
1593 other => panic!("expected UnsupportedUri, got: {other:?}"),
1594 }
1595 }
1596
1597 #[test]
1598 fn unsupported_xpointer_expression() {
1599 let xml = "<root/>";
1602 let doc = Document::parse(xml).unwrap();
1603 let resolver = UriReferenceResolver::new(&doc);
1604
1605 let result = resolver.dereference("#xpointer(foo())");
1606 assert!(result.is_err());
1607 match result.unwrap_err() {
1608 TransformError::UnsupportedUri(uri) => {
1609 assert_eq!(uri, "#xpointer(foo())")
1610 }
1611 other => panic!("expected UnsupportedUri, got: {other:?}"),
1612 }
1613
1614 let result = resolver.dereference("#xpointer(//element)");
1616 assert!(result.is_err());
1617 assert!(matches!(
1618 result.unwrap_err(),
1619 TransformError::UnsupportedUri(_)
1620 ));
1621 }
1622
1623 #[test]
1624 fn empty_fragment_rejected() {
1625 let xml = "<root/>";
1627 let doc = Document::parse(xml).unwrap();
1628 let resolver = UriReferenceResolver::new(&doc);
1629
1630 let result = resolver.dereference("#");
1631 assert!(result.is_err());
1632 match result.unwrap_err() {
1633 TransformError::UnsupportedUri(uri) => assert_eq!(uri, "#"),
1634 other => panic!("expected UnsupportedUri, got: {other:?}"),
1635 }
1636 }
1637
1638 #[test]
1639 fn foreign_document_node_rejected() {
1640 let xml1 = "<root><child/></root>";
1642 let xml2 = "<other><item/></other>";
1643 let doc1 = Document::parse(xml1).unwrap();
1644 let doc2 = Document::parse(xml2).unwrap();
1645
1646 let node_set = NodeSet::entire_document_without_comments(&doc1).unwrap();
1647
1648 let foreign_node = doc2.root_element();
1650 assert!(
1651 !node_set.contains(foreign_node),
1652 "foreign document node should be rejected"
1653 );
1654
1655 let own_node = doc1.root_element();
1657 assert!(node_set.contains(own_node));
1658 }
1659
1660 #[test]
1661 fn custom_id_attr_name() {
1662 let xml = r#"<root><elem myid="custom1">data</elem></root>"#;
1665 let doc = Document::parse(xml).unwrap();
1666
1667 let resolver_default = UriReferenceResolver::new(&doc);
1669 assert!(!resolver_default.has_id("custom1"));
1670
1671 let resolver_custom = UriReferenceResolver::with_id_attrs(&doc, &["myid"]);
1673 assert!(resolver_custom.has_id("custom1"));
1674
1675 let data = resolver_custom.dereference("#custom1").unwrap();
1676 assert!(data.into_node_set().is_ok());
1677 }
1678
1679 #[test]
1680 fn absolute_external_uri_uses_normalized_resource_identity() {
1681 let xml = r#"<root xml:base="https://base.example/ignored/">
1684 <reference URI="https://example.test/a/../data.bin"/>
1685 </root>"#;
1686 let doc = Document::parse(xml).unwrap();
1687 let reference = doc
1688 .descendants()
1689 .find(|node| node.has_tag_name("reference"))
1690 .unwrap();
1691 let resources = HashMap::from([(
1692 "https://example.test/data.bin".to_owned(),
1693 b"payload".to_vec(),
1694 )]);
1695 let budget = NodeSetMaterializationBudget::default();
1696 let xml_base_budget = XmlBaseResolutionBudget::default();
1697 let resolver = UriReferenceResolver::new(&doc).with_external_resources(&resources);
1698
1699 let data = resolver
1700 .dereference_from_with_budget(
1701 reference.attribute("URI").unwrap(),
1702 reference,
1703 &budget,
1704 &xml_base_budget,
1705 )
1706 .unwrap();
1707
1708 assert_eq!(data.into_binary().unwrap(), b"payload");
1709 }
1710
1711 #[test]
1712 fn absolute_external_uri_does_not_consume_xml_base_components() {
1713 let xml = r#"<root xml:base="ignored/"><reference URI="https://example.test/a/../data.bin"/></root>"#;
1716 let doc = Document::parse(xml).unwrap();
1717 let reference = doc
1718 .descendants()
1719 .find(|node| node.has_tag_name("reference"))
1720 .unwrap();
1721 let resources = HashMap::from([(
1722 "https://example.test/data.bin".to_owned(),
1723 b"payload".to_vec(),
1724 )]);
1725 let resolver = UriReferenceResolver::new(&doc).with_external_resources(&resources);
1726
1727 let data = resolver
1728 .dereference_from_with_budget(
1729 reference.attribute("URI").unwrap(),
1730 reference,
1731 &NodeSetMaterializationBudget::default(),
1732 &XmlBaseResolutionBudget::with_limits(0, 1_024),
1733 )
1734 .expect("absolute references must bypass inherited XML Base traversal");
1735
1736 assert_eq!(data.into_binary().unwrap(), b"payload");
1737 }
1738
1739 #[test]
1740 fn external_uri_without_xml_base_uses_normalized_resource_identity() {
1741 let xml = r#"<root><reference URI="https://example.test/a/../data.bin"/></root>"#;
1744 let doc = Document::parse(xml).unwrap();
1745 let reference = doc
1746 .descendants()
1747 .find(|node| node.has_tag_name("reference"))
1748 .unwrap();
1749 let resources = HashMap::from([(
1750 "https://example.test/data.bin".to_owned(),
1751 b"payload".to_vec(),
1752 )]);
1753 let budget = NodeSetMaterializationBudget::default();
1754 let xml_base_budget = XmlBaseResolutionBudget::default();
1755 let resolver = UriReferenceResolver::new(&doc).with_external_resources(&resources);
1756
1757 let data = resolver
1758 .dereference_from_with_budget(
1759 reference.attribute("URI").unwrap(),
1760 reference,
1761 &budget,
1762 &xml_base_budget,
1763 )
1764 .expect("the normalized resource key must resolve without xml:base");
1765
1766 assert_eq!(data.into_binary().unwrap(), b"payload");
1767 }
1768
1769 #[test]
1770 fn pathless_relative_xml_base_preserves_relative_resource_identity() {
1771 let xml = r#"<root xml:base="?old">
1774 <reference URI="data.bin"/>
1775 </root>"#;
1776 let doc = Document::parse(xml).unwrap();
1777 let reference = doc
1778 .descendants()
1779 .find(|node| node.has_tag_name("reference"))
1780 .unwrap();
1781 let resources = HashMap::from([("data.bin".to_owned(), b"payload".to_vec())]);
1782 let budget = NodeSetMaterializationBudget::default();
1783 let xml_base_budget = XmlBaseResolutionBudget::default();
1784 let resolver = UriReferenceResolver::new(&doc).with_external_resources(&resources);
1785
1786 let data = resolver
1787 .dereference_from_with_budget(
1788 reference.attribute("URI").unwrap(),
1789 reference,
1790 &budget,
1791 &xml_base_budget,
1792 )
1793 .unwrap();
1794
1795 assert_eq!(data.into_binary().unwrap(), b"payload");
1796 }
1797
1798 #[test]
1799 fn relative_xml_base_normalizes_absolute_external_path() {
1800 let xml = r#"<root xml:base="a/b">
1803 <reference URI="/x/../data.bin"/>
1804 </root>"#;
1805 let doc = Document::parse(xml).unwrap();
1806 let reference = doc
1807 .descendants()
1808 .find(|node| node.has_tag_name("reference"))
1809 .unwrap();
1810 let resources = HashMap::from([("/data.bin".to_owned(), b"payload".to_vec())]);
1811 let budget = NodeSetMaterializationBudget::default();
1812 let xml_base_budget = XmlBaseResolutionBudget::default();
1813 let resolver = UriReferenceResolver::new(&doc).with_external_resources(&resources);
1814
1815 let data = resolver
1816 .dereference_from_with_budget(
1817 reference.attribute("URI").unwrap(),
1818 reference,
1819 &budget,
1820 &xml_base_budget,
1821 )
1822 .unwrap();
1823
1824 assert_eq!(data.into_binary().unwrap(), b"payload");
1825 }
1826
1827 #[test]
1828 fn network_path_xml_base_preserves_external_resource_authority() {
1829 let xml = r#"<root xml:base="//cdn.example/a/b/">
1832 <reference URI="/x/../data.bin"/>
1833 </root>"#;
1834 let doc = Document::parse(xml).unwrap();
1835 let reference = doc
1836 .descendants()
1837 .find(|node| node.has_tag_name("reference"))
1838 .unwrap();
1839 let resources = HashMap::from([("//cdn.example/data.bin".to_owned(), b"payload".to_vec())]);
1840 let budget = NodeSetMaterializationBudget::default();
1841 let xml_base_budget = XmlBaseResolutionBudget::default();
1842 let resolver = UriReferenceResolver::new(&doc).with_external_resources(&resources);
1843
1844 let data = resolver
1845 .dereference_from_with_budget(
1846 reference.attribute("URI").unwrap(),
1847 reference,
1848 &budget,
1849 &xml_base_budget,
1850 )
1851 .unwrap();
1852
1853 assert_eq!(data.into_binary().unwrap(), b"payload");
1854 }
1855
1856 #[test]
1857 fn unicode_external_uri_resolves_without_panicking() {
1858 let xml = r#"<root xml:base="https://example.test/base/">
1861 <reference URI="é?x"/>
1862 </root>"#;
1863 let doc = Document::parse(xml).unwrap();
1864 let reference = doc
1865 .descendants()
1866 .find(|node| node.has_tag_name("reference"))
1867 .unwrap();
1868 let resources = HashMap::from([(
1869 "https://example.test/base/é?x".to_owned(),
1870 b"payload".to_vec(),
1871 )]);
1872 let budget = NodeSetMaterializationBudget::default();
1873 let xml_base_budget = XmlBaseResolutionBudget::default();
1874 let resolver = UriReferenceResolver::new(&doc).with_external_resources(&resources);
1875
1876 let data = resolver
1877 .dereference_from_with_budget(
1878 reference.attribute("URI").unwrap(),
1879 reference,
1880 &budget,
1881 &xml_base_budget,
1882 )
1883 .unwrap();
1884
1885 assert_eq!(data.into_binary().unwrap(), b"payload");
1886 }
1887
1888 #[test]
1889 fn absolute_rootless_external_uri_discards_leading_parent_segment() {
1890 let xml = r#"<root xml:base="https://example.test/base/">
1891 <reference URI="urn:../payload"/>
1892 </root>"#;
1893 let doc = Document::parse(xml).unwrap();
1894 let reference = doc
1895 .descendants()
1896 .find(|node| node.has_tag_name("reference"))
1897 .unwrap();
1898 let resources = HashMap::from([("urn:payload".to_owned(), b"payload".to_vec())]);
1899 let budget = NodeSetMaterializationBudget::default();
1900 let xml_base_budget = XmlBaseResolutionBudget::default();
1901 let resolver = UriReferenceResolver::new(&doc).with_external_resources(&resources);
1902
1903 let data = resolver
1904 .dereference_from_with_budget(
1905 reference.attribute("URI").unwrap(),
1906 reference,
1907 &budget,
1908 &xml_base_budget,
1909 )
1910 .unwrap();
1911
1912 assert_eq!(data.into_binary().unwrap(), b"payload");
1913 }
1914
1915 #[test]
1916 fn namespaced_id_attr_found_by_local_name() {
1917 let xml =
1919 r#"<root><elem wsu:Id="ts1" xmlns:wsu="http://example.com/wsu">data</elem></root>"#;
1920 let doc = Document::parse(xml).unwrap();
1921
1922 let resolver = UriReferenceResolver::new(&doc);
1923 assert!(resolver.has_id("ts1"));
1924 }
1925
1926 #[test]
1927 fn id_count_reports_unique_ids() {
1928 let xml = r#"<root ID="r1"><a ID="a1"/><b Id="b1"/><c id="c1"/></root>"#;
1929 let doc = Document::parse(xml).unwrap();
1930 let resolver = UriReferenceResolver::new(&doc);
1931
1932 assert_eq!(resolver.id_count(), 4);
1934 }
1935
1936 #[test]
1937 fn duplicate_ids_are_rejected() {
1938 let xml = r#"<root><a ID="dup">first</a><b ID="dup">second</b></root>"#;
1941 let doc = Document::parse(xml).unwrap();
1942 let resolver = UriReferenceResolver::new(&doc);
1943
1944 assert!(!resolver.has_id("dup"));
1946 let result = resolver.dereference("#dup");
1947 assert!(result.is_err());
1948 assert!(matches!(
1949 result.unwrap_err(),
1950 TransformError::ElementNotFound(_)
1951 ));
1952 }
1953
1954 #[test]
1955 fn triple_duplicate_ids_stay_rejected() {
1956 let xml = r#"<root><a ID="dup">1</a><b ID="dup">2</b><c ID="dup">3</c></root>"#;
1959 let doc = Document::parse(xml).unwrap();
1960 let resolver = UriReferenceResolver::new(&doc);
1961
1962 assert!(!resolver.has_id("dup"));
1963 assert!(resolver.dereference("#dup").is_err());
1964 }
1965
1966 #[test]
1967 fn node_set_exclude_subtree() {
1968 let xml = r#"<root><keep>yes</keep><remove><deep>no</deep></remove></root>"#;
1969 let doc = Document::parse(xml).unwrap();
1970 let resolver = UriReferenceResolver::new(&doc);
1971
1972 let data = resolver.dereference("").unwrap();
1973 let mut node_set = data.into_node_set().unwrap();
1974
1975 let remove_elem = doc
1977 .descendants()
1978 .find(|n| n.is_element() && n.has_tag_name("remove"))
1979 .unwrap();
1980 node_set.exclude_subtree(remove_elem);
1981
1982 let keep_elem = doc
1984 .descendants()
1985 .find(|n| n.is_element() && n.has_tag_name("keep"))
1986 .unwrap();
1987 assert!(node_set.contains(keep_elem));
1988
1989 assert!(!node_set.contains(remove_elem));
1991 let deep_elem = doc
1992 .descendants()
1993 .find(|n| n.is_element() && n.has_tag_name("deep"))
1994 .unwrap();
1995 assert!(!node_set.contains(deep_elem));
1996 }
1997
1998 #[test]
1999 fn bare_name_subtree_excludes_comments() {
2000 let xml = r#"<root><item ID="x"><!-- comment --><child/></item></root>"#;
2002 let doc = Document::parse(xml).unwrap();
2003 let resolver = UriReferenceResolver::new(&doc);
2004
2005 let data = resolver.dereference("#x").unwrap();
2006 let node_set = data.into_node_set().unwrap();
2007
2008 for node in doc.descendants() {
2009 if node.is_comment() {
2010 assert!(
2011 !node_set.contains(node),
2012 "comment must be excluded from #id"
2013 );
2014 }
2015 }
2016 }
2017
2018 #[test]
2019 fn xpointer_root_returns_whole_document_with_comments() {
2020 let xml = "<root><!-- comment --><child/></root>";
2021 let doc = Document::parse(xml).unwrap();
2022 let resolver = UriReferenceResolver::new(&doc);
2023
2024 let data = resolver.dereference("#xpointer(/)").unwrap();
2025 let node_set = data.into_node_set().unwrap();
2026
2027 for node in doc.descendants() {
2029 if node.is_comment() {
2030 assert!(
2031 node_set.contains(node),
2032 "comment should be included for #xpointer(/)"
2033 );
2034 }
2035 }
2036 assert!(node_set.contains(doc.root_element()));
2037 }
2038
2039 #[test]
2040 fn xpointer_id_single_quotes() {
2041 let xml = r#"<root><item ID="abc"><!-- retained -->content</item></root>"#;
2043 let doc = Document::parse(xml).unwrap();
2044 let resolver = UriReferenceResolver::new(&doc);
2045
2046 let data = resolver.dereference("#xpointer(id('abc'))").unwrap();
2047 let node_set = data.into_node_set().unwrap();
2048
2049 let elem = doc
2050 .descendants()
2051 .find(|n| n.attribute("ID") == Some("abc"))
2052 .unwrap();
2053 assert!(node_set.contains(elem));
2054 assert!(
2055 elem.children()
2056 .any(|node| node.is_comment() && node_set.contains(node))
2057 );
2058 }
2059
2060 #[test]
2061 fn xpointer_id_double_quotes() {
2062 let xml = r#"<root><item ID="xyz">content</item></root>"#;
2063 let doc = Document::parse(xml).unwrap();
2064 let resolver = UriReferenceResolver::new(&doc);
2065
2066 let data = resolver.dereference(r#"#xpointer(id("xyz"))"#).unwrap();
2067 let node_set = data.into_node_set().unwrap();
2068
2069 let elem = doc
2070 .descendants()
2071 .find(|n| n.attribute("ID") == Some("xyz"))
2072 .unwrap();
2073 assert!(node_set.contains(elem));
2074 }
2075
2076 #[test]
2077 fn xpointer_id_not_found() {
2078 let xml = "<root/>";
2079 let doc = Document::parse(xml).unwrap();
2080 let resolver = UriReferenceResolver::new(&doc);
2081
2082 let result = resolver.dereference("#xpointer(id('missing'))");
2083 assert!(result.is_err());
2084 match result.unwrap_err() {
2085 TransformError::ElementNotFound(id) => assert_eq!(id, "missing"),
2086 other => panic!("expected ElementNotFound, got: {other:?}"),
2087 }
2088 }
2089
2090 #[test]
2091 fn xpointer_id_empty_value_rejected() {
2092 let xml = "<root/>";
2094 let doc = Document::parse(xml).unwrap();
2095 let resolver = UriReferenceResolver::new(&doc);
2096
2097 let result = resolver.dereference("#xpointer(id(''))");
2098 assert!(result.is_err());
2099 assert!(matches!(
2100 result.unwrap_err(),
2101 TransformError::UnsupportedUri(_)
2102 ));
2103 }
2104
2105 #[test]
2106 fn bare_fragment_rejects_non_ncname_without_visa3d_compatibility() {
2107 let xml = r#"<root><item ID="12345">content</item></root>"#;
2111 let doc = Document::parse(xml).unwrap();
2112 let resolver = UriReferenceResolver::new(&doc);
2113
2114 assert!(matches!(
2115 resolver.dereference("#12345"),
2116 Err(TransformError::UnsupportedUri(uri)) if uri == "#12345"
2117 ));
2118 }
2119
2120 #[test]
2121 fn visa3d_compatibility_resolves_non_ncname_id_directly() {
2122 let xml = r#"<root><item ID="12345">content</item></root>"#;
2125 let doc = Document::parse(xml).unwrap();
2126 let resolver = UriReferenceResolver::new(&doc)
2127 .with_same_document_id_semantics(SameDocumentIdSemantics::XmlSecVisa3d);
2128
2129 assert!(resolver.dereference("#12345").is_ok());
2130 }
2131
2132 #[test]
2133 fn xmlsec_barename_compatibility_matches_donor_literal_and_comment_semantics() {
2134 let xml = r#"<root><item ID="12345"><!-- excluded -->numeric</item><item ID="visa'3d">quoted</item></root>"#;
2138 let doc = Document::parse(xml).unwrap();
2139 let resolver = UriReferenceResolver::new(&doc)
2140 .with_same_document_id_semantics(SameDocumentIdSemantics::XmlSecBarename);
2141
2142 let nodes = resolver
2143 .dereference("#12345")
2144 .unwrap()
2145 .into_node_set()
2146 .unwrap();
2147 assert!(
2148 doc.descendants()
2149 .filter(|node| node.is_comment())
2150 .all(|node| !nodes.contains(node))
2151 );
2152 assert!(matches!(
2153 resolver.dereference("#visa'3d"),
2154 Err(TransformError::UnsupportedUri(uri)) if uri == "#visa'3d"
2155 ));
2156 }
2157
2158 #[test]
2159 fn explicit_xpointer_decoding_is_independent_of_barename_mode() {
2160 let document = Document::parse("<root><item ID='café'/></root>").unwrap();
2162 for semantics in [
2163 SameDocumentIdSemantics::Specification,
2164 SameDocumentIdSemantics::XmlSecBarename,
2165 SameDocumentIdSemantics::XmlSecVisa3d,
2166 ] {
2167 let resolver =
2168 UriReferenceResolver::new(&document).with_same_document_id_semantics(semantics);
2169 for uri in [
2170 "#xpointer(id('caf%C3%A9'))",
2171 "#%78pointer%28id('caf%C3%A9')%29",
2172 ] {
2173 assert!(resolver.dereference(uri).is_ok(), "{semantics:?}: {uri}");
2174 }
2175 }
2176 }
2177
2178 #[test]
2179 fn parse_xpointer_id_variants() {
2180 assert_eq!(
2182 super::parse_xpointer_id_fragment("xpointer(id('foo'))"),
2183 Some("foo")
2184 );
2185 assert_eq!(
2186 super::parse_xpointer_id_fragment(r#"xpointer(id("bar"))"#),
2187 Some("bar")
2188 );
2189
2190 assert_eq!(super::parse_xpointer_id_fragment("xpointer(/)"), None);
2192 assert_eq!(super::parse_xpointer_id_fragment("xpointer(id(foo))"), None); assert_eq!(super::parse_xpointer_id_fragment("not-xpointer"), None);
2194 assert_eq!(super::parse_xpointer_id_fragment(""), None);
2195
2196 assert_eq!(super::parse_xpointer_id_fragment("xpointer(id('))"), None);
2198 assert_eq!(
2199 super::parse_xpointer_id_fragment(r#"xpointer(id("))"#),
2200 None
2201 );
2202 }
2203
2204 #[test]
2205 fn same_document_reference_nodes_reject_non_id_fragments() {
2206 let document = Document::parse(r#"<root><item ID="target"/></root>"#).unwrap();
2207 let resolver = UriReferenceResolver::new(&document);
2208 for uri in [
2209 "#target",
2210 "#xpointer(id('target'))",
2211 r#"#xpointer(id("target"))"#,
2212 ] {
2213 assert!(
2214 resolver
2215 .node_id_for_same_document_reference(uri)
2216 .unwrap()
2217 .is_some()
2218 );
2219 }
2220 for uri in [
2221 "",
2222 "target",
2223 "#",
2224 "#xpointer(/)",
2225 "#xpointer(id(''))",
2226 "#xpointer(id(target))",
2227 ] {
2228 assert!(
2229 resolver.node_id_for_same_document_reference(uri).is_err(),
2230 "{uri}"
2231 );
2232 }
2233 }
2234
2235 #[test]
2236 fn same_element_multiple_id_attrs_not_duplicate() {
2237 let xml = r#"<root><item ID="x" Id="x">data</item></root>"#;
2241 let doc = Document::parse(xml).unwrap();
2242 let resolver = UriReferenceResolver::new(&doc);
2243
2244 assert!(resolver.has_id("x"));
2245 assert!(resolver.dereference("#x").is_ok());
2246 }
2247
2248 #[test]
2249 fn saml_style_document() {
2250 let xml = r#"<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
2252 xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
2253 ID="_resp1">
2254 <saml:Assertion ID="_assert1">
2255 <saml:Subject>user@example.com</saml:Subject>
2256 </saml:Assertion>
2257 <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#" Id="sig1">
2258 <ds:SignedInfo/>
2259 </ds:Signature>
2260 </samlp:Response>"#;
2261
2262 let doc = Document::parse(xml).unwrap();
2263 let resolver = UriReferenceResolver::new(&doc);
2264
2265 assert!(resolver.has_id("_resp1"));
2267 assert!(resolver.has_id("_assert1"));
2268 assert!(resolver.has_id("sig1"));
2269 assert_eq!(resolver.id_count(), 3);
2270
2271 let data = resolver.dereference("#_assert1").unwrap();
2273 let node_set = data.into_node_set().unwrap();
2274
2275 let assertion = doc
2277 .descendants()
2278 .find(|n| n.attribute("ID") == Some("_assert1"))
2279 .unwrap();
2280 assert!(node_set.contains(assertion));
2281
2282 let subject = assertion
2284 .children()
2285 .find(|n| n.is_element() && n.has_tag_name("Subject"))
2286 .unwrap();
2287 assert!(node_set.contains(subject));
2288
2289 assert!(!node_set.contains(doc.root_element()));
2291 }
2292}