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