1use std::borrow::Cow;
9#[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
10use std::cell::Cell;
11use std::collections::{HashMap, HashSet, hash_map::Entry};
12use std::sync::atomic::{AtomicU64, Ordering};
13
14use crate::xml::dom::{Document, Node, NodeId, ParseError, ParsingOptions, XmlBackend};
15use crate::xml_input as xml_sec_xml_input;
16use self_cell::self_cell;
17
18use crate::IdAttributeRegistration;
19use crate::xml::dom::{SemanticDocument, SemanticNodeId};
20
21#[derive(Clone, Copy, Debug, PartialEq, Eq)]
22struct DocumentMetrics {
23 node_count: usize,
24 max_depth: usize,
25}
26
27static NEXT_DOCUMENT_ID: AtomicU64 = AtomicU64::new(1);
28const VALIDATION_WRAPPER_NS: &str = "urn:xml-sec:owned-document-validation";
29const VALIDATION_WRAPPER_OPEN: &str = "<xmlsec_owned_document:wrapper xmlns:xmlsec_owned_document=\"urn:xml-sec:owned-document-validation\">";
30const VALIDATION_WRAPPER_CLOSE: &str = "</xmlsec_owned_document:wrapper>";
31const VALIDATION_WRAPPER_NODE_OVERHEAD: u32 = 3;
34
35#[cfg(test)]
36pub(crate) fn selected_parser_passes() -> usize {
37 3
38}
39
40#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
42pub struct DocumentIdentity(u64);
43
44#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
46pub struct NodeIdentity {
47 document: DocumentIdentity,
48 generation: u64,
49 semantic_id: SemanticNodeId,
50}
51
52#[derive(Clone, Debug, PartialEq, Eq, Hash)]
54pub struct AttributeIdentity {
55 owner: NodeIdentity,
56 namespace: Option<String>,
57 local_name: String,
58}
59
60#[derive(Clone, Debug, PartialEq, Eq, Hash)]
62pub struct NamespaceIdentity {
63 owner: NodeIdentity,
64 prefix: String,
65 uri: String,
66}
67
68#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
70pub struct SemanticOrder {
71 node: usize,
72 phase: u8,
73 position: usize,
74}
75
76impl AttributeIdentity {
77 #[must_use]
79 pub const fn owner(&self) -> NodeIdentity {
80 self.owner
81 }
82
83 #[must_use]
85 pub fn namespace(&self) -> Option<&str> {
86 self.namespace.as_deref()
87 }
88
89 #[must_use]
91 pub fn local_name(&self) -> &str {
92 &self.local_name
93 }
94}
95
96impl NamespaceIdentity {
97 #[must_use]
99 pub const fn owner(&self) -> NodeIdentity {
100 self.owner
101 }
102
103 #[must_use]
105 pub fn prefix(&self) -> &str {
106 &self.prefix
107 }
108
109 #[must_use]
111 pub fn uri(&self) -> &str {
112 &self.uri
113 }
114}
115
116#[derive(Clone, Copy)]
117pub(crate) struct DocumentParseSettings {
118 pub(crate) backend: XmlBackend,
119 pub(crate) allow_dtd: bool,
120 pub(crate) nodes_limit: u32,
121 pub(crate) depth_limit: usize,
122 pub(crate) namespace_bindings_limit: usize,
123 pub(crate) max_bytes: usize,
124}
125
126impl Default for DocumentParseSettings {
127 fn default() -> Self {
128 Self {
129 backend: XmlBackend::build_default(),
130 allow_dtd: false,
131 nodes_limit: crate::hard_limits::XML_DOCUMENT_NODE_CEILING,
132 depth_limit: crate::hard_limits::XML_DOCUMENT_DEPTH_CEILING,
133 namespace_bindings_limit: crate::hard_limits::XML_NAMESPACE_BINDING_CEILING,
134 max_bytes: crate::hard_limits::XML_DOCUMENT_BYTE_CEILING,
135 }
136 }
137}
138
139impl DocumentParseSettings {
140 pub(crate) const fn with_backend(mut self, backend: XmlBackend) -> Self {
141 self.backend = backend;
142 self
143 }
144}
145
146#[cfg(any(feature = "xmldsig", feature = "xmlenc", test))]
147impl DocumentParseSettings {
148 #[cfg(test)]
149 pub(crate) const fn new(allow_dtd: bool, nodes_limit: u32, max_bytes: usize) -> Self {
150 Self::new_with_depth(
151 allow_dtd,
152 nodes_limit,
153 crate::hard_limits::XML_DOCUMENT_DEPTH_CEILING,
154 max_bytes,
155 )
156 }
157
158 #[cfg(test)]
159 pub(crate) const fn new_with_depth(
160 allow_dtd: bool,
161 nodes_limit: u32,
162 depth_limit: usize,
163 max_bytes: usize,
164 ) -> Self {
165 Self {
166 backend: XmlBackend::build_default(),
167 allow_dtd,
168 nodes_limit,
169 depth_limit,
170 namespace_bindings_limit: crate::hard_limits::XML_NAMESPACE_BINDING_CEILING,
171 max_bytes,
172 }
173 }
174
175 pub(crate) fn from_policy(
176 xml: &crate::policy::XmlInputPolicy,
177 resources: &crate::policy::ResourcePolicy,
178 ) -> Self {
179 Self {
180 allow_dtd: xml.allow_internal_dtd,
181 nodes_limit: resources.effective_xml_nodes(),
182 depth_limit: resources.max_xml_depth,
183 namespace_bindings_limit: resources.max_xml_namespace_bindings,
184 max_bytes: resources.max_xml_document_bytes,
185 ..Self::default()
186 }
187 }
188
189 #[cfg(feature = "xmldsig")]
190 pub(crate) fn for_transform_output(resources: &crate::policy::ResourcePolicy) -> Self {
191 Self {
192 allow_dtd: false,
193 nodes_limit: resources.effective_xml_nodes(),
194 depth_limit: resources.max_xml_depth,
195 namespace_bindings_limit: resources.max_xml_namespace_bindings,
196 max_bytes: resources.max_xml_document_bytes,
197 ..Self::default()
198 }
199 }
200}
201
202pub(crate) struct XmlParseWorkBudget {
209 #[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
210 remaining: Cell<usize>,
211 #[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
212 maximum: usize,
213}
214
215impl XmlParseWorkBudget {
216 #[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
217 pub(crate) fn from_resources(resources: &crate::policy::ResourcePolicy) -> Self {
218 Self {
219 remaining: Cell::new(resources.max_xml_parse_work_bytes),
220 maximum: resources.max_xml_parse_work_bytes,
221 }
222 }
223
224 #[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
225 pub(crate) fn charge_policy(&self, bytes: usize) -> Result<(), crate::policy::PolicyViolation> {
226 let consumed = self.maximum.saturating_sub(self.remaining.get());
227 let Some(remaining) = self.remaining.get().checked_sub(bytes) else {
228 self.remaining.set(0);
229 return Err(crate::policy::PolicyViolation::ResourceLimit {
230 resource: crate::policy::resource_name::XML_PARSE_WORK_BYTES,
231 maximum: self.maximum,
232 actual: consumed.saturating_add(bytes),
233 });
234 };
235 self.remaining.set(remaining);
236 Ok(())
237 }
238
239 #[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
240 pub(crate) fn charge(&self, bytes: usize) -> Result<(), XmlDocumentError> {
241 self.charge_policy(bytes).map_err(Into::into)
242 }
243
244 #[cfg(all(test, any(feature = "xmldsig", feature = "xmlenc")))]
245 fn with_limit(maximum: usize) -> Self {
246 Self {
247 remaining: Cell::new(maximum),
248 maximum,
249 }
250 }
251
252 #[cfg(all(test, any(feature = "xmldsig", feature = "xmlenc")))]
253 pub(crate) fn consumed(&self) -> usize {
254 self.maximum.saturating_sub(self.remaining.get())
255 }
256}
257
258fn charge_parse_work(
259 budget: Option<&XmlParseWorkBudget>,
260 bytes: usize,
261) -> Result<(), XmlDocumentError> {
262 #[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
263 if let Some(budget) = budget {
264 budget.charge(bytes)?;
265 }
266 #[cfg(not(any(feature = "xmldsig", feature = "xmlenc")))]
267 let _ = (budget, bytes);
268 Ok(())
269}
270
271fn charge_semantic_parser_work(
272 budget: Option<&XmlParseWorkBudget>,
273 bytes: usize,
274) -> Result<(), XmlDocumentError> {
275 for _ in 0..2 {
279 charge_parse_work(budget, bytes)?;
280 }
281 Ok(())
282}
283
284struct ContentReplacementEdit<'a> {
285 range: std::ops::Range<usize>,
286 replacement: &'a str,
287 self_closing: Option<SelfClosingContentEdit>,
288}
289
290struct SelfClosingContentEdit {
291 prefix_end: usize,
292 qualified_name: String,
293}
294
295#[cfg(feature = "xmldsig")]
296fn is_unescaped_base64_text(value: &str) -> bool {
297 value
298 .bytes()
299 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'+' | b'/' | b'='))
300}
301
302#[cfg(feature = "xmldsig")]
303fn validate_base64_replacements(
304 replacements: &[(NodeIdentity, String)],
305) -> Result<(), XmlDocumentError> {
306 if replacements
307 .iter()
308 .any(|(_, value)| !is_unescaped_base64_text(value))
309 {
310 return Err(XmlDocumentError::InvalidReplacement(
311 "generated base64 replacement contains XML markup".into(),
312 ));
313 }
314 Ok(())
315}
316
317impl ContentReplacementEdit<'_> {
318 fn output_len(&self) -> Option<usize> {
319 let Some(expansion) = &self.self_closing else {
320 return Some(self.replacement.len());
321 };
322 expansion
323 .prefix_end
324 .checked_add(1)
325 .and_then(|length| length.checked_add(self.replacement.len()))
326 .and_then(|length| length.checked_add(2))
327 .and_then(|length| length.checked_add(expansion.qualified_name.len()))
328 .and_then(|length| length.checked_add(1))
329 }
330
331 fn validation_output_len(&self) -> Option<usize> {
332 self.output_len()?
333 .checked_add(VALIDATION_WRAPPER_OPEN.len())?
334 .checked_add(VALIDATION_WRAPPER_CLOSE.len())
335 }
336
337 fn write_output(&self, source: &str, output: &mut String) {
338 if let Some(expansion) = &self.self_closing {
339 output.push_str(&source[self.range.start..self.range.start + expansion.prefix_end]);
340 output.push('>');
341 }
342 output.push_str(self.replacement);
343 if let Some(expansion) = &self.self_closing {
344 output.push_str("</");
345 output.push_str(&expansion.qualified_name);
346 output.push('>');
347 }
348 }
349
350 fn write_validation_output(&self, source: &str, output: &mut String) -> std::ops::Range<usize> {
351 if let Some(expansion) = &self.self_closing {
352 output.push_str(&source[self.range.start..self.range.start + expansion.prefix_end]);
353 output.push('>');
354 }
355 let wrapper_start = output.len();
356 output.push_str(VALIDATION_WRAPPER_OPEN);
357 output.push_str(self.replacement);
358 output.push_str(VALIDATION_WRAPPER_CLOSE);
359 let wrapper_range = wrapper_start..output.len();
360 if let Some(expansion) = &self.self_closing {
361 output.push_str("</");
362 output.push_str(&expansion.qualified_name);
363 output.push('>');
364 }
365 wrapper_range
366 }
367}
368
369#[derive(Debug)]
370struct DocumentIndexes {
371 order: HashMap<NodeId, usize>,
372 default_ids: HashMap<String, Option<NodeId>>,
373 attributes_by_value: HashMap<(String, String), Vec<NodeId>>,
374}
375
376#[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
377mod policy_sealed {
378 pub trait Sealed {}
379}
380
381#[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
388pub trait XmlDocumentPolicy: policy_sealed::Sealed {
389 #[doc(hidden)]
390 fn xml_input_policy(&self) -> &crate::policy::XmlInputPolicy;
391
392 #[doc(hidden)]
393 fn resource_policy(&self) -> &crate::policy::ResourcePolicy;
394}
395
396#[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
397macro_rules! impl_document_policy {
398 ($policy:ty) => {
399 impl policy_sealed::Sealed for $policy {}
400
401 impl XmlDocumentPolicy for $policy {
402 fn xml_input_policy(&self) -> &crate::policy::XmlInputPolicy {
403 &self.xml
404 }
405
406 fn resource_policy(&self) -> &crate::policy::ResourcePolicy {
407 &self.resources
408 }
409 }
410 };
411}
412
413#[cfg(feature = "xmldsig")]
414impl_document_policy!(crate::policy::SigningPolicy);
415#[cfg(feature = "xmldsig")]
416impl_document_policy!(crate::policy::VerificationPolicy);
417#[cfg(feature = "xmlenc")]
418impl_document_policy!(crate::policy::EncryptionPolicy);
419#[cfg(feature = "xmlenc")]
420impl_document_policy!(crate::policy::DecryptionPolicy);
421
422struct ParsedDocument<'input> {
423 document: Document<'input>,
424 indexes: DocumentIndexes,
425 node_count: usize,
426 #[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
427 max_depth: usize,
428 #[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
429 peak_namespace_bindings: usize,
430}
431
432self_cell!(
433 struct DocumentCell {
434 owner: String,
435
436 #[covariant]
437 dependent: ParsedDocument,
438 }
439);
440
441#[derive(Debug, thiserror::Error)]
443pub enum XmlDocumentError {
444 #[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
446 #[error("XML document policy violation: {0}")]
447 Policy(#[from] crate::policy::PolicyViolation),
448
449 #[error("XML document exceeds the maximum size of {maximum} bytes: {actual} bytes")]
451 DocumentTooLarge {
452 maximum: usize,
454 actual: usize,
456 },
457 #[error("XML encoding error: {0}")]
459 Encoding(#[from] xml_sec_xml_input::Error),
460 #[error("XML document exceeds the maximum element depth of {maximum}: {actual}")]
462 DocumentTooDeep {
463 maximum: usize,
465 actual: usize,
467 },
468 #[error("XML parsing error: {0}")]
470 Parse(#[from] ParseError),
471 #[error("XML identity belongs to a different document")]
473 ForeignIdentity,
474 #[error("XML identity belongs to stale generation {identity}; current generation is {current}")]
476 StaleIdentity {
477 identity: u64,
479 current: u64,
481 },
482 #[error("XML identity does not resolve to a node")]
484 MissingNode,
485 #[error("XML mutation target must be an element")]
487 TargetNotElement,
488 #[error(
490 "XML mutation target originates from an entity expansion and has no unique source range"
491 )]
492 EntityExpandedMutationTarget,
493 #[error("invalid XML replacement: {0}")]
495 InvalidReplacement(String),
496 #[error("XML document identity space is exhausted")]
498 IdentityExhausted,
499 #[error("projected XML document exceeds the maximum node count of {maximum}")]
501 ProjectedNodeLimit {
502 maximum: usize,
504 },
505}
506
507#[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
508impl XmlDocumentError {
509 pub(crate) fn into_policy_violation(
510 self,
511 settings: DocumentParseSettings,
512 ) -> Result<crate::policy::PolicyViolation, Self> {
513 let violation = match self {
514 Self::Policy(error) => error,
515 Self::DocumentTooLarge { maximum, actual } => {
516 crate::policy::PolicyViolation::ResourceLimit {
517 resource: crate::policy::resource_name::XML_DOCUMENT,
518 maximum,
519 actual,
520 }
521 }
522 Self::DocumentTooDeep { maximum, actual } => {
523 crate::policy::PolicyViolation::ResourceLimit {
524 resource: crate::policy::resource_name::XML_DEPTH,
525 maximum,
526 actual,
527 }
528 }
529 Self::Parse(ParseError::NodesLimitReached) => {
530 crate::policy::PolicyViolation::ResourceLimit {
531 resource: crate::policy::resource_name::XML_NODES,
532 maximum: settings.nodes_limit as usize,
533 actual: settings.nodes_limit as usize + 1,
534 }
535 }
536 Self::Parse(ParseError::NamespaceBindingLimitReached { maximum, actual }) => {
537 crate::policy::PolicyViolation::ResourceLimit {
538 resource: crate::policy::resource_name::XML_NAMESPACE_BINDINGS,
539 maximum,
540 actual,
541 }
542 }
543 error => return Err(error),
544 };
545 Ok(violation)
546 }
547}
548
549pub struct XmlDocument {
555 identity: DocumentIdentity,
556 generation: u64,
557 #[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
558 requires_internal_dtd: bool,
559 settings: DocumentParseSettings,
560 cell: DocumentCell,
561}
562
563#[cfg(feature = "xmlenc")]
564pub(crate) struct PreparedDocumentMutation {
565 identity: DocumentIdentity,
566 generation: u64,
567 cell: DocumentCell,
568}
569
570#[derive(Clone, Copy)]
572pub struct DocumentView<'a> {
573 identity: DocumentIdentity,
574 generation: u64,
575 parsed: &'a ParsedDocument<'a>,
576}
577
578impl XmlDocument {
579 #[must_use]
581 pub fn xml_backend(&self) -> XmlBackend {
582 self.settings.backend
583 }
584
585 pub fn parse(xml: impl AsRef<str> + Into<String>) -> Result<Self, XmlDocumentError> {
588 let settings = DocumentParseSettings::default();
589 let xml = own_bounded_xml(xml, settings.max_bytes)?;
590 Self::parse_with_settings(xml, settings)
591 }
592
593 pub fn parse_bytes(bytes: &[u8]) -> Result<Self, XmlDocumentError> {
598 let settings = DocumentParseSettings::default();
599 Self::parse_with_settings(decode_owned_xml(bytes, settings.max_bytes, None)?, settings)
600 }
601
602 pub fn parse_bytes_with_backend(
604 bytes: &[u8],
605 backend: XmlBackend,
606 ) -> Result<Self, XmlDocumentError> {
607 let settings = DocumentParseSettings::default().with_backend(backend);
608 Self::parse_with_settings(decode_owned_xml(bytes, settings.max_bytes, None)?, settings)
609 }
610
611 pub fn parse_with_backend(
613 xml: impl AsRef<str> + Into<String>,
614 backend: XmlBackend,
615 ) -> Result<Self, XmlDocumentError> {
616 let settings = DocumentParseSettings::default().with_backend(backend);
617 let xml = own_bounded_xml(xml, settings.max_bytes)?;
618 Self::parse_with_settings(xml, settings)
619 }
620
621 #[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
624 pub fn parse_with_policy(
625 xml: impl AsRef<str> + Into<String>,
626 policy: &impl XmlDocumentPolicy,
627 ) -> Result<Self, XmlDocumentError> {
628 let resources = policy.resource_policy();
629 resources.validate()?;
630 let budget = XmlParseWorkBudget::from_resources(resources);
631 let xml = own_bounded_xml(xml, resources.max_xml_document_bytes)?;
632 Self::parse_with_settings_and_optional_budget(
633 xml,
634 DocumentParseSettings::from_policy(policy.xml_input_policy(), resources),
635 Some(&budget),
636 )
637 }
638
639 #[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
641 pub fn parse_bytes_with_policy(
642 bytes: &[u8],
643 policy: &impl XmlDocumentPolicy,
644 ) -> Result<Self, XmlDocumentError> {
645 let resources = policy.resource_policy();
646 resources.validate()?;
647 let budget = XmlParseWorkBudget::from_resources(resources);
648 let xml = decode_owned_xml(bytes, resources.max_xml_document_bytes, Some(&budget))?;
649 Self::parse_with_settings_and_optional_budget(
650 xml,
651 DocumentParseSettings::from_policy(policy.xml_input_policy(), resources),
652 Some(&budget),
653 )
654 }
655
656 #[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
658 pub fn parse_with_policy_and_backend(
659 xml: impl AsRef<str> + Into<String>,
660 policy: &impl XmlDocumentPolicy,
661 backend: XmlBackend,
662 ) -> Result<Self, XmlDocumentError> {
663 let resources = policy.resource_policy();
664 resources.validate()?;
665 let budget = XmlParseWorkBudget::from_resources(resources);
666 let xml = own_bounded_xml(xml, resources.max_xml_document_bytes)?;
667 Self::parse_with_settings_and_optional_budget(
668 xml,
669 DocumentParseSettings::from_policy(policy.xml_input_policy(), resources)
670 .with_backend(backend),
671 Some(&budget),
672 )
673 }
674
675 #[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
677 pub fn parse_bytes_with_policy_and_backend(
678 bytes: &[u8],
679 policy: &impl XmlDocumentPolicy,
680 backend: XmlBackend,
681 ) -> Result<Self, XmlDocumentError> {
682 let resources = policy.resource_policy();
683 resources.validate()?;
684 let budget = XmlParseWorkBudget::from_resources(resources);
685 let xml = decode_owned_xml(bytes, resources.max_xml_document_bytes, Some(&budget))?;
686 Self::parse_with_settings_and_optional_budget(
687 xml,
688 DocumentParseSettings::from_policy(policy.xml_input_policy(), resources)
689 .with_backend(backend),
690 Some(&budget),
691 )
692 }
693
694 pub(crate) fn parse_with_settings(
695 xml: String,
696 settings: DocumentParseSettings,
697 ) -> Result<Self, XmlDocumentError> {
698 Self::parse_with_settings_and_optional_budget(xml, settings, None)
699 }
700
701 fn parse_with_settings_and_optional_budget(
702 xml: String,
703 settings: DocumentParseSettings,
704 budget: Option<&XmlParseWorkBudget>,
705 ) -> Result<Self, XmlDocumentError> {
706 if xml.len() > settings.max_bytes {
707 return Err(XmlDocumentError::DocumentTooLarge {
708 maximum: settings.max_bytes,
709 actual: xml.len(),
710 });
711 }
712 let peak_namespace_bindings = preflight_document_limits(&xml, settings, budget)?;
713 #[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
714 let requires_internal_dtd = document_requires_internal_dtd(&xml, settings, budget)?;
715 let cell = build_cell_after_preflight(xml, settings, budget, peak_namespace_bindings)?;
716 let identity = allocate_document_identity(&NEXT_DOCUMENT_ID)?;
717 Ok(Self {
718 identity,
719 generation: 0,
720 #[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
721 requires_internal_dtd,
722 settings,
723 cell,
724 })
725 }
726
727 #[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
728 pub(crate) fn parse_with_settings_and_budget(
729 xml: String,
730 settings: DocumentParseSettings,
731 budget: &XmlParseWorkBudget,
732 ) -> Result<Self, XmlDocumentError> {
733 Self::parse_with_settings_and_optional_budget(xml, settings, Some(budget))
734 }
735
736 #[must_use]
738 pub const fn identity(&self) -> DocumentIdentity {
739 self.identity
740 }
741
742 #[must_use]
744 pub const fn generation(&self) -> u64 {
745 self.generation
746 }
747
748 #[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
749 pub(crate) fn validate_xml_input_policy(
750 &self,
751 allow_internal_dtd: bool,
752 ) -> Result<(), crate::policy::PolicyViolation> {
753 if self.requires_internal_dtd && !allow_internal_dtd {
754 return Err(crate::policy::PolicyViolation::XmlInput {
755 reason: "owned document requires internal DTD support",
756 });
757 }
758 Ok(())
759 }
760
761 #[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
762 pub(crate) fn validate_operation_policy(
763 &self,
764 xml: &crate::policy::XmlInputPolicy,
765 resources: &crate::policy::ResourcePolicy,
766 ) -> Result<(), crate::policy::PolicyViolation> {
767 resources.validate()?;
768 self.validate_xml_input_policy(xml.allow_internal_dtd)?;
769 resources.validate_xml_document_len(self.as_xml().len())?;
770 self.with_view(|view| {
771 let actual = view.parsed.peak_namespace_bindings;
774 if actual > resources.max_xml_namespace_bindings {
775 return Err(crate::policy::PolicyViolation::ResourceLimit {
776 resource: crate::policy::resource_name::XML_NAMESPACE_BINDINGS,
777 maximum: resources.max_xml_namespace_bindings,
778 actual,
779 });
780 }
781 let node_count = view.node_count();
782 if node_count > resources.effective_xml_nodes() as usize {
783 return Err(crate::policy::PolicyViolation::ResourceLimit {
784 resource: crate::policy::resource_name::XML_NODES,
785 maximum: resources.effective_xml_nodes() as usize,
786 actual: node_count,
787 });
788 }
789 let max_depth = view.max_depth();
790 if max_depth > resources.max_xml_depth {
791 return Err(crate::policy::PolicyViolation::ResourceLimit {
792 resource: crate::policy::resource_name::XML_DEPTH,
793 maximum: resources.max_xml_depth,
794 actual: max_depth,
795 });
796 }
797 Ok(())
798 })
799 }
800
801 #[must_use]
803 pub fn as_xml(&self) -> &str {
804 self.cell.borrow_owner()
805 }
806
807 #[must_use]
809 pub fn into_xml(self) -> String {
810 self.cell.into_owner()
811 }
812
813 #[cfg(feature = "xmldsig")]
814 pub(crate) fn staged_copy_with_budget(
815 &self,
816 settings: DocumentParseSettings,
817 budget: &XmlParseWorkBudget,
818 ) -> Result<Self, XmlDocumentError> {
819 Self::parse_with_settings_and_budget(self.as_xml().to_owned(), settings, budget)
820 }
821
822 #[cfg(feature = "xmldsig")]
823 pub(crate) fn commit_staged(&mut self, staged: Self) -> Result<(), XmlDocumentError> {
824 self.commit_cell(staged.cell)
828 }
829
830 pub fn with_view<R>(&self, operation: impl for<'a> FnOnce(DocumentView<'a>) -> R) -> R {
832 self.cell.with_dependent(|_, parsed| {
833 operation(DocumentView {
834 identity: self.identity,
835 generation: self.generation,
836 parsed,
837 })
838 })
839 }
840
841 pub fn replace_element(
843 &mut self,
844 target: NodeIdentity,
845 replacement: &str,
846 ) -> Result<(), XmlDocumentError> {
847 let range = self.with_view(|view| {
848 let node = view.resolve_mutation_node(target)?;
849 if !node.is_element() {
850 return Err(XmlDocumentError::TargetNotElement);
851 }
852 Ok(node.range())
853 })?;
854 self.ensure_replacement_fits(&range, replacement.len(), self.settings.max_bytes)?;
855 self.validate_single_element_in_parent_context(
856 target,
857 replacement,
858 None,
859 self.settings,
860 None,
861 )?;
862 self.replace_range(range, replacement, None)
863 }
864
865 #[cfg(feature = "xmlenc")]
866 pub(crate) fn replace_element_with_budget(
867 &mut self,
868 target: NodeIdentity,
869 replacement: &str,
870 settings: DocumentParseSettings,
871 budget: &XmlParseWorkBudget,
872 ) -> Result<(), XmlDocumentError> {
873 let range = self.with_view(|view| {
874 let node = view.resolve_mutation_node(target)?;
875 if !node.is_element() {
876 return Err(XmlDocumentError::TargetNotElement);
877 }
878 Ok(node.range())
879 })?;
880 self.ensure_replacement_fits(&range, replacement.len(), settings.max_bytes)?;
881 self.validate_single_element_in_parent_context(
882 target,
883 replacement,
884 Some(settings.nodes_limit as usize),
885 settings,
886 Some(budget),
887 )?;
888 self.replace_range_with_settings(range, replacement, settings, Some(budget))
889 }
890
891 #[cfg(feature = "xmlenc")]
892 pub(crate) fn prepare_element_replacement_with_budget(
893 &self,
894 target: NodeIdentity,
895 replacement: &str,
896 settings: DocumentParseSettings,
897 budget: &XmlParseWorkBudget,
898 ) -> Result<PreparedDocumentMutation, XmlDocumentError> {
899 let range = self.with_view(|view| {
900 let node = view.resolve_mutation_node(target)?;
901 if !node.is_element() {
902 return Err(XmlDocumentError::TargetNotElement);
903 }
904 Ok(node.range())
905 })?;
906 self.ensure_replacement_fits(&range, replacement.len(), settings.max_bytes)?;
907 self.validate_single_element_in_parent_context(
908 target,
909 replacement,
910 Some(settings.nodes_limit as usize),
911 settings,
912 Some(budget),
913 )?;
914 self.prepare_range_replacement(range, replacement, settings, budget)
915 }
916
917 pub fn replace_node_with_fragment(
922 &mut self,
923 target: NodeIdentity,
924 replacement: &str,
925 ) -> Result<(), XmlDocumentError> {
926 let range = self.with_view(|view| {
927 Ok::<_, XmlDocumentError>(view.resolve_mutation_node(target)?.range())
928 })?;
929 self.ensure_replacement_fits(&range, replacement.len(), self.settings.max_bytes)?;
930 self.validate_fragment_in_parent_context(target, replacement, None, self.settings, None)?;
931 self.replace_range(range, replacement, None)
932 }
933
934 #[cfg(feature = "xmlenc")]
935 pub(crate) fn prepare_node_fragment_replacement_with_budget(
936 &self,
937 target: NodeIdentity,
938 replacement: &str,
939 settings: DocumentParseSettings,
940 budget: &XmlParseWorkBudget,
941 ) -> Result<PreparedDocumentMutation, XmlDocumentError> {
942 let range = self.with_view(|view| {
943 Ok::<_, XmlDocumentError>(view.resolve_mutation_node(target)?.range())
944 })?;
945 self.ensure_replacement_fits(&range, replacement.len(), settings.max_bytes)?;
946 self.validate_fragment_in_parent_context(
947 target,
948 replacement,
949 Some(settings.nodes_limit as usize),
950 settings,
951 Some(budget),
952 )?;
953 self.prepare_range_replacement(range, replacement, settings, budget)
954 }
955
956 pub fn replace_content(
958 &mut self,
959 target: NodeIdentity,
960 replacement: &str,
961 ) -> Result<(), XmlDocumentError> {
962 self.replace_content_inner(target, replacement, None, self.settings, None)
963 }
964
965 #[cfg(feature = "xmlenc")]
966 pub(crate) fn replace_content_with_budget(
967 &mut self,
968 target: NodeIdentity,
969 replacement: &str,
970 settings: DocumentParseSettings,
971 budget: &XmlParseWorkBudget,
972 ) -> Result<(), XmlDocumentError> {
973 self.replace_content_inner(
974 target,
975 replacement,
976 Some(settings.nodes_limit as usize),
977 settings,
978 Some(budget),
979 )
980 }
981
982 fn replace_content_inner(
983 &mut self,
984 target: NodeIdentity,
985 replacement: &str,
986 maximum: Option<usize>,
987 settings: DocumentParseSettings,
988 budget: Option<&XmlParseWorkBudget>,
989 ) -> Result<(), XmlDocumentError> {
990 let (range, self_closing_expansion, serialized_len) = self.with_view(|view| {
991 let node = view.resolve_mutation_node(target)?;
992 if !node.is_element() {
993 return Err(XmlDocumentError::TargetNotElement);
994 }
995 let range = node.range();
996 let source = &view.xml()[range.clone()];
997 let (content, qualified_name, self_closing) = element_content_range(source)?;
998 if self_closing {
999 let slash = source.rfind("/>").ok_or_else(|| {
1000 XmlDocumentError::InvalidReplacement(
1001 "self-closing element has no terminator".into(),
1002 )
1003 })?;
1004 let serialized_len = slash
1005 .checked_add(replacement.len())
1006 .and_then(|length| length.checked_add(qualified_name.len()))
1007 .and_then(|length| length.checked_add(4))
1008 .ok_or(XmlDocumentError::DocumentTooLarge {
1009 maximum: settings.max_bytes,
1010 actual: usize::MAX,
1011 })?;
1012 return Ok((range, Some((slash, qualified_name)), serialized_len));
1013 }
1014 let range = (range.start + content.start)..(range.start + content.end);
1015 Ok((range, None, replacement.len()))
1016 })?;
1017 self.ensure_replacement_fits(&range, serialized_len, settings.max_bytes)?;
1018 let serialized_replacement = if let Some((slash, qualified_name)) = self_closing_expansion {
1019 let source = &self.as_xml()[range.clone()];
1020 format!("{}>{replacement}</{qualified_name}>", &source[..slash])
1021 } else {
1022 replacement.to_owned()
1023 };
1024 self.validate_content_in_element_context(target, replacement, maximum, settings, budget)?;
1025 if let Some(maximum) = maximum {
1026 debug_assert_eq!(maximum, settings.nodes_limit as usize);
1027 self.replace_range_with_settings(range, &serialized_replacement, settings, budget)
1028 } else {
1029 self.replace_range(range, &serialized_replacement, budget)
1030 }
1031 }
1032
1033 pub fn replace_contents(
1039 &mut self,
1040 replacements: &[(NodeIdentity, String)],
1041 ) -> Result<(), XmlDocumentError> {
1042 self.replace_contents_inner(replacements, self.settings, false, true, None)
1043 }
1044
1045 #[cfg(all(feature = "xmldsig", test))]
1046 pub(crate) fn replace_contents_with_budget(
1047 &mut self,
1048 replacements: &[(NodeIdentity, String)],
1049 settings: DocumentParseSettings,
1050 budget: &XmlParseWorkBudget,
1051 ) -> Result<(), XmlDocumentError> {
1052 self.replace_contents_inner(replacements, settings, true, true, Some(budget))
1053 }
1054
1055 #[cfg(feature = "xmldsig")]
1056 pub(crate) fn replace_base64_contents_with_budget(
1057 &mut self,
1058 replacements: &[(NodeIdentity, String)],
1059 settings: DocumentParseSettings,
1060 budget: &XmlParseWorkBudget,
1061 ) -> Result<(), XmlDocumentError> {
1062 validate_base64_replacements(replacements)?;
1063 self.replace_contents_inner(replacements, settings, true, false, Some(budget))
1067 }
1068
1069 #[cfg(feature = "xmldsig")]
1070 pub(crate) fn project_base64_contents(
1071 &self,
1072 replacements: &[(NodeIdentity, String)],
1073 maximum_bytes: usize,
1074 ) -> Result<String, XmlDocumentError> {
1075 validate_base64_replacements(replacements)?;
1076 let (edits, projected) =
1077 self.prepare_content_replacement_edits(replacements, maximum_bytes)?;
1078 Ok(self.render_content_replacement_edits(&edits, projected))
1079 }
1080
1081 fn replace_contents_inner(
1082 &mut self,
1083 replacements: &[(NodeIdentity, String)],
1084 settings: DocumentParseSettings,
1085 policy_bounded: bool,
1086 validate_fragments: bool,
1087 budget: Option<&XmlParseWorkBudget>,
1088 ) -> Result<(), XmlDocumentError> {
1089 if replacements.is_empty() {
1090 return Ok(());
1091 }
1092 let (edits, projected) =
1093 self.prepare_content_replacement_edits(replacements, settings.max_bytes)?;
1094
1095 if validate_fragments {
1096 self.validate_content_edits(&edits, settings, policy_bounded, budget)?;
1097 }
1098
1099 let output = self.render_content_replacement_edits(&edits, projected);
1100 if policy_bounded {
1101 self.replace_serialized_with_settings(output, settings, budget)
1102 } else {
1103 self.replace_serialized(output, budget)
1104 }
1105 }
1106
1107 fn prepare_content_replacement_edits<'a>(
1108 &self,
1109 replacements: &'a [(NodeIdentity, String)],
1110 maximum_bytes: usize,
1111 ) -> Result<(Vec<ContentReplacementEdit<'a>>, usize), XmlDocumentError> {
1112 let mut targets = HashSet::with_capacity(replacements.len());
1113 if replacements
1114 .iter()
1115 .any(|(target, _)| !targets.insert(*target))
1116 {
1117 return Err(XmlDocumentError::InvalidReplacement(
1118 "replacement targets must be unique".into(),
1119 ));
1120 }
1121 let mut edits = self.with_view(|view| {
1122 replacements
1123 .iter()
1124 .map(|(target, replacement)| {
1125 let node = view.resolve_mutation_node(*target)?;
1126 if !node.is_element() {
1127 return Err(XmlDocumentError::TargetNotElement);
1128 }
1129 let range = node.range();
1130 let source = &view.xml()[range.clone()];
1131 let (content, qualified_name, self_closing) = element_content_range(source)?;
1132 if self_closing {
1133 let slash = source.rfind("/>").ok_or_else(|| {
1134 XmlDocumentError::InvalidReplacement(
1135 "self-closing element has no terminator".into(),
1136 )
1137 })?;
1138 Ok(ContentReplacementEdit {
1139 range,
1140 replacement,
1141 self_closing: Some(SelfClosingContentEdit {
1142 prefix_end: slash,
1143 qualified_name,
1144 }),
1145 })
1146 } else {
1147 Ok(ContentReplacementEdit {
1148 range: (range.start + content.start)..(range.start + content.end),
1149 replacement,
1150 self_closing: None,
1151 })
1152 }
1153 })
1154 .collect::<Result<Vec<_>, XmlDocumentError>>()
1155 })?;
1156 edits.sort_by_key(|edit| edit.range.start);
1157 if edits
1158 .windows(2)
1159 .any(|pair| pair[0].range.end > pair[1].range.start)
1160 {
1161 return Err(XmlDocumentError::InvalidReplacement(
1162 "replacement targets overlap".into(),
1163 ));
1164 }
1165 let projected = edits.iter().try_fold(self.as_xml().len(), |length, edit| {
1166 length
1167 .checked_sub(edit.range.len())
1168 .and_then(|length| length.checked_add(edit.output_len()?))
1169 .ok_or(XmlDocumentError::DocumentTooLarge {
1170 maximum: maximum_bytes,
1171 actual: usize::MAX,
1172 })
1173 })?;
1174 if projected > maximum_bytes {
1175 return Err(XmlDocumentError::DocumentTooLarge {
1176 maximum: maximum_bytes,
1177 actual: projected,
1178 });
1179 }
1180 Ok((edits, projected))
1181 }
1182
1183 fn render_content_replacement_edits(
1184 &self,
1185 edits: &[ContentReplacementEdit<'_>],
1186 projected: usize,
1187 ) -> String {
1188 let mut output = String::with_capacity(projected);
1189 let mut cursor = 0;
1190 for edit in edits {
1191 output.push_str(&self.as_xml()[cursor..edit.range.start]);
1192 edit.write_output(self.as_xml(), &mut output);
1193 cursor = edit.range.end;
1194 }
1195 output.push_str(&self.as_xml()[cursor..]);
1196 debug_assert_eq!(output.len(), projected);
1197 output
1198 }
1199
1200 fn validate_content_edits(
1201 &self,
1202 edits: &[ContentReplacementEdit<'_>],
1203 settings: DocumentParseSettings,
1204 policy_bounded: bool,
1205 budget: Option<&XmlParseWorkBudget>,
1206 ) -> Result<(), XmlDocumentError> {
1207 let projected = edits.iter().try_fold(self.as_xml().len(), |length, edit| {
1208 length
1209 .checked_sub(edit.range.len())
1210 .and_then(|length| length.checked_add(edit.validation_output_len()?))
1211 .ok_or_else(|| {
1212 XmlDocumentError::InvalidReplacement("validation length overflow".into())
1213 })
1214 })?;
1215 let mut candidate = String::with_capacity(projected);
1216 let mut wrapper_ranges = Vec::with_capacity(edits.len());
1217 let mut cursor = 0;
1218 for edit in edits {
1219 candidate.push_str(&self.as_xml()[cursor..edit.range.start]);
1220 wrapper_ranges.push(edit.write_validation_output(self.as_xml(), &mut candidate));
1221 cursor = edit.range.end;
1222 }
1223 candidate.push_str(&self.as_xml()[cursor..]);
1224 debug_assert_eq!(candidate.len(), projected);
1225
1226 let active_nodes_limit = settings.nodes_limit as usize;
1227 let wrapper_allowance = edits.len();
1228 let validation_nodes_limit = active_nodes_limit
1229 .checked_add(wrapper_allowance)
1230 .and_then(|maximum| u32::try_from(maximum).ok())
1231 .unwrap_or(u32::MAX);
1232 let parsed = build_cell(
1233 candidate,
1234 DocumentParseSettings {
1235 nodes_limit: validation_nodes_limit,
1236 depth_limit: settings.depth_limit.saturating_add(1),
1237 namespace_bindings_limit: settings.namespace_bindings_limit.saturating_add(1),
1240 max_bytes: projected,
1241 ..settings
1242 },
1243 budget,
1244 )
1245 .map_err(|error| match (policy_bounded, error) {
1246 (true, XmlDocumentError::Parse(ParseError::NodesLimitReached)) => {
1247 XmlDocumentError::ProjectedNodeLimit {
1248 maximum: active_nodes_limit,
1249 }
1250 }
1251 (_, XmlDocumentError::DocumentTooDeep { actual, .. }) => {
1252 XmlDocumentError::DocumentTooDeep {
1253 maximum: settings.depth_limit,
1254 actual: actual.saturating_sub(1),
1255 }
1256 }
1257 (_, error) => strip_validation_wrapper_namespace_binding(error, settings),
1258 })?;
1259 parsed.with_dependent(|_, document| validate_wrappers(document, &wrapper_ranges))
1260 }
1261
1262 pub fn append_child(
1264 &mut self,
1265 target: NodeIdentity,
1266 child: &str,
1267 ) -> Result<(), XmlDocumentError> {
1268 self.append_child_inner(target, child, None, self.settings, true, None)
1269 }
1270
1271 #[cfg(all(feature = "xmldsig", test))]
1272 pub(crate) fn append_child_with_budget(
1273 &mut self,
1274 target: NodeIdentity,
1275 child: &str,
1276 settings: DocumentParseSettings,
1277 budget: &XmlParseWorkBudget,
1278 ) -> Result<(), XmlDocumentError> {
1279 self.append_child_inner(
1280 target,
1281 child,
1282 Some(settings.nodes_limit as usize),
1283 settings,
1284 true,
1285 Some(budget),
1286 )
1287 }
1288
1289 #[cfg(feature = "xmldsig")]
1290 pub(crate) fn append_generated_child_with_budget(
1291 &mut self,
1292 target: NodeIdentity,
1293 child: &str,
1294 settings: DocumentParseSettings,
1295 budget: &XmlParseWorkBudget,
1296 ) -> Result<(), XmlDocumentError> {
1297 self.append_child_inner(
1301 target,
1302 child,
1303 Some(settings.nodes_limit as usize),
1304 settings,
1305 false,
1306 Some(budget),
1307 )
1308 }
1309
1310 fn append_child_inner(
1311 &mut self,
1312 target: NodeIdentity,
1313 child: &str,
1314 maximum: Option<usize>,
1315 settings: DocumentParseSettings,
1316 validate_context: bool,
1317 budget: Option<&XmlParseWorkBudget>,
1318 ) -> Result<(), XmlDocumentError> {
1319 let projected = self.projected_child_append_len(target, child.len())?;
1320 if projected > settings.max_bytes {
1321 return Err(XmlDocumentError::DocumentTooLarge {
1322 maximum: settings.max_bytes,
1323 actual: projected,
1324 });
1325 }
1326 let (range, replacement) = self.with_view(|view| {
1327 let node = view.resolve_mutation_node(target)?;
1328 if !node.is_element() {
1329 return Err(XmlDocumentError::TargetNotElement);
1330 }
1331 let range = node.range();
1332 let source = &view.xml()[range.clone()];
1333 let (content, qualified_name, self_closing) = element_content_range(source)?;
1334 if self_closing {
1335 let slash = source.rfind("/>").ok_or_else(|| {
1336 XmlDocumentError::InvalidReplacement(
1337 "self-closing element has no terminator".into(),
1338 )
1339 })?;
1340 return Ok((
1341 range,
1342 format!("{}>{child}</{qualified_name}>", &source[..slash]),
1343 ));
1344 }
1345 Ok((
1346 (range.start + content.end)..(range.start + content.end),
1347 child.to_owned(),
1348 ))
1349 })?;
1350 self.ensure_replacement_fits(&range, replacement.len(), settings.max_bytes)?;
1351 if validate_context {
1352 self.validate_content_in_element_context(target, child, maximum, settings, budget)?;
1353 }
1354 if let Some(maximum) = maximum {
1355 debug_assert_eq!(maximum, settings.nodes_limit as usize);
1356 self.replace_range_with_settings(range, &replacement, settings, budget)
1357 } else {
1358 self.replace_range(range, &replacement, budget)
1359 }
1360 }
1361
1362 pub(crate) fn replace_serialized(
1363 &mut self,
1364 xml: String,
1365 budget: Option<&XmlParseWorkBudget>,
1366 ) -> Result<(), XmlDocumentError> {
1367 let next = build_cell(xml, self.settings, budget)?;
1368 self.commit_cell(next)
1369 }
1370
1371 pub(crate) fn replace_serialized_with_settings(
1372 &mut self,
1373 xml: String,
1374 settings: DocumentParseSettings,
1375 budget: Option<&XmlParseWorkBudget>,
1376 ) -> Result<(), XmlDocumentError> {
1377 let next = build_cell(xml, settings, budget).map_err(|error| match error {
1378 XmlDocumentError::Parse(ParseError::NodesLimitReached) => {
1379 XmlDocumentError::ProjectedNodeLimit {
1380 maximum: settings.nodes_limit as usize,
1381 }
1382 }
1383 error => error,
1384 })?;
1385 self.commit_cell(next)
1386 }
1387
1388 fn commit_cell(&mut self, next: DocumentCell) -> Result<(), XmlDocumentError> {
1389 let next_generation = self.generation.checked_add(1).ok_or_else(|| {
1390 XmlDocumentError::InvalidReplacement("document generation overflow".into())
1391 })?;
1392 self.cell = next;
1393 self.generation = next_generation;
1394 Ok(())
1395 }
1396
1397 #[cfg(feature = "xmldsig")]
1398 pub(crate) fn projected_content_replacement_len(
1399 &self,
1400 target: NodeIdentity,
1401 replacement_len: usize,
1402 ) -> Result<usize, XmlDocumentError> {
1403 self.with_view(|view| {
1404 let node = view.resolve_mutation_node(target)?;
1405 if !node.is_element() {
1406 return Err(XmlDocumentError::TargetNotElement);
1407 }
1408 let range = node.range();
1409 let source = &view.xml()[range.clone()];
1410 let (content, qualified_name, self_closing) = element_content_range(source)?;
1411 let removed = if self_closing {
1412 range.len()
1413 } else {
1414 content.len()
1415 };
1416 let added = if self_closing {
1417 let slash = source.rfind("/>").ok_or_else(|| {
1418 XmlDocumentError::InvalidReplacement(
1419 "self-closing element has no terminator".into(),
1420 )
1421 })?;
1422 slash
1423 .checked_add(1)
1424 .and_then(|length| length.checked_add(replacement_len))
1425 .and_then(|length| length.checked_add(2))
1426 .and_then(|length| length.checked_add(qualified_name.len()))
1427 .and_then(|length| length.checked_add(1))
1428 .ok_or_else(|| {
1429 XmlDocumentError::InvalidReplacement(
1430 "content replacement length overflow".into(),
1431 )
1432 })?
1433 } else {
1434 replacement_len
1435 };
1436 view.xml()
1437 .len()
1438 .checked_sub(removed)
1439 .and_then(|length| length.checked_add(added))
1440 .ok_or_else(|| {
1441 XmlDocumentError::InvalidReplacement(
1442 "content replacement length overflow".into(),
1443 )
1444 })
1445 })
1446 }
1447
1448 pub(crate) fn projected_child_append_len(
1449 &self,
1450 target: NodeIdentity,
1451 child_len: usize,
1452 ) -> Result<usize, XmlDocumentError> {
1453 self.with_view(|view| {
1454 let node = view.resolve_mutation_node(target)?;
1455 if !node.is_element() {
1456 return Err(XmlDocumentError::TargetNotElement);
1457 }
1458 let range = node.range();
1459 let source = &view.xml()[range.clone()];
1460 let (_, qualified_name, self_closing) = element_content_range(source)?;
1461 let (removed, added) = if self_closing {
1462 let slash = source.rfind("/>").ok_or_else(|| {
1463 XmlDocumentError::InvalidReplacement(
1464 "self-closing element has no terminator".into(),
1465 )
1466 })?;
1467 let expanded = slash
1468 .checked_add(1)
1469 .and_then(|length| length.checked_add(child_len))
1470 .and_then(|length| length.checked_add(2))
1471 .and_then(|length| length.checked_add(qualified_name.len()))
1472 .and_then(|length| length.checked_add(1))
1473 .ok_or_else(|| {
1474 XmlDocumentError::InvalidReplacement("child append length overflow".into())
1475 })?;
1476 (range.len(), expanded)
1477 } else {
1478 (0, child_len)
1479 };
1480 view.xml()
1481 .len()
1482 .checked_sub(removed)
1483 .and_then(|length| length.checked_add(added))
1484 .ok_or_else(|| {
1485 XmlDocumentError::InvalidReplacement("child append length overflow".into())
1486 })
1487 })
1488 }
1489
1490 fn replace_range(
1491 &mut self,
1492 range: std::ops::Range<usize>,
1493 replacement: &str,
1494 budget: Option<&XmlParseWorkBudget>,
1495 ) -> Result<(), XmlDocumentError> {
1496 let output = self.replaced_range(range, replacement, self.settings.max_bytes)?;
1497 self.replace_serialized(output, budget)
1498 }
1499
1500 fn replace_range_with_settings(
1501 &mut self,
1502 range: std::ops::Range<usize>,
1503 replacement: &str,
1504 settings: DocumentParseSettings,
1505 budget: Option<&XmlParseWorkBudget>,
1506 ) -> Result<(), XmlDocumentError> {
1507 let output = self.replaced_range(range, replacement, settings.max_bytes)?;
1508 self.replace_serialized_with_settings(output, settings, budget)
1509 }
1510
1511 #[cfg(feature = "xmlenc")]
1512 fn prepare_range_replacement(
1513 &self,
1514 range: std::ops::Range<usize>,
1515 replacement: &str,
1516 settings: DocumentParseSettings,
1517 budget: &XmlParseWorkBudget,
1518 ) -> Result<PreparedDocumentMutation, XmlDocumentError> {
1519 let output = self.replaced_range(range, replacement, settings.max_bytes)?;
1520 let cell = build_cell(output, settings, Some(budget)).map_err(|error| match error {
1521 XmlDocumentError::Parse(ParseError::NodesLimitReached) => {
1522 XmlDocumentError::ProjectedNodeLimit {
1523 maximum: settings.nodes_limit as usize,
1524 }
1525 }
1526 error => error,
1527 })?;
1528 Ok(PreparedDocumentMutation {
1529 identity: self.identity,
1530 generation: self.generation,
1531 cell,
1532 })
1533 }
1534
1535 #[cfg(feature = "xmlenc")]
1536 pub(crate) fn commit_prepared(
1537 &mut self,
1538 prepared: PreparedDocumentMutation,
1539 ) -> Result<(), XmlDocumentError> {
1540 if self.identity != prepared.identity {
1541 return Err(XmlDocumentError::ForeignIdentity);
1542 }
1543 if self.generation != prepared.generation {
1544 return Err(XmlDocumentError::StaleIdentity {
1545 identity: prepared.generation,
1546 current: self.generation,
1547 });
1548 }
1549 self.commit_cell(prepared.cell)
1550 }
1551
1552 fn replaced_range(
1553 &self,
1554 range: std::ops::Range<usize>,
1555 replacement: &str,
1556 maximum_bytes: usize,
1557 ) -> Result<String, XmlDocumentError> {
1558 let projected = self.ensure_replacement_fits(&range, replacement.len(), maximum_bytes)?;
1559 let mut output = String::with_capacity(projected);
1560 output.push_str(&self.as_xml()[..range.start]);
1561 output.push_str(replacement);
1562 output.push_str(&self.as_xml()[range.end..]);
1563 Ok(output)
1564 }
1565
1566 fn ensure_replacement_fits(
1567 &self,
1568 range: &std::ops::Range<usize>,
1569 replacement_len: usize,
1570 maximum_bytes: usize,
1571 ) -> Result<usize, XmlDocumentError> {
1572 let projected = self
1573 .as_xml()
1574 .len()
1575 .checked_sub(range.len())
1576 .and_then(|length| length.checked_add(replacement_len))
1577 .ok_or(XmlDocumentError::DocumentTooLarge {
1578 maximum: maximum_bytes,
1579 actual: usize::MAX,
1580 })?;
1581 if projected > maximum_bytes {
1582 return Err(XmlDocumentError::DocumentTooLarge {
1583 maximum: maximum_bytes,
1584 actual: projected,
1585 });
1586 }
1587 Ok(projected)
1588 }
1589
1590 fn validate_single_element_in_parent_context(
1591 &self,
1592 target: NodeIdentity,
1593 replacement: &str,
1594 maximum: Option<usize>,
1595 settings: DocumentParseSettings,
1596 budget: Option<&XmlParseWorkBudget>,
1597 ) -> Result<(), XmlDocumentError> {
1598 let (parsed, wrapper_range) =
1599 self.parse_fragment_in_parent_context(target, replacement, maximum, settings, budget)?;
1600 parsed.with_dependent(|_, parsed| {
1601 let wrapper = validation_wrapper(parsed, wrapper_range.clone())?;
1602 if wrapper.children().filter(Node::is_element).count() != 1
1603 || wrapper.children().any(|node| {
1604 !node.is_element()
1605 && !node.is_comment()
1606 && !node.is_pi()
1607 && node.text().is_none_or(|text| !text.trim().is_empty())
1608 })
1609 {
1610 return Err(XmlDocumentError::InvalidReplacement(
1611 "element replacement must contain exactly one element".into(),
1612 ));
1613 }
1614 Ok(())
1615 })
1616 }
1617
1618 fn validate_fragment_in_parent_context(
1619 &self,
1620 target: NodeIdentity,
1621 replacement: &str,
1622 maximum: Option<usize>,
1623 settings: DocumentParseSettings,
1624 budget: Option<&XmlParseWorkBudget>,
1625 ) -> Result<(), XmlDocumentError> {
1626 self.parse_fragment_in_parent_context(target, replacement, maximum, settings, budget)
1627 .map(|_| ())
1628 }
1629
1630 fn parse_fragment_in_parent_context(
1631 &self,
1632 target: NodeIdentity,
1633 replacement: &str,
1634 maximum: Option<usize>,
1635 settings: DocumentParseSettings,
1636 budget: Option<&XmlParseWorkBudget>,
1637 ) -> Result<(DocumentCell, std::ops::Range<usize>), XmlDocumentError> {
1638 let range =
1639 self.with_view(|view| Ok::<_, XmlDocumentError>(view.resolve_node(target)?.range()))?;
1640 self.parse_wrapped_range(range, replacement, maximum, settings, budget)
1641 }
1642
1643 fn validate_content_in_element_context(
1644 &self,
1645 target: NodeIdentity,
1646 replacement: &str,
1647 maximum: Option<usize>,
1648 settings: DocumentParseSettings,
1649 budget: Option<&XmlParseWorkBudget>,
1650 ) -> Result<(), XmlDocumentError> {
1651 let (element_range, content_range, qualified_name, self_closing) =
1652 self.with_view(|view| {
1653 let target = view.resolve_node(target)?;
1654 if !target.is_element() {
1655 return Err(XmlDocumentError::TargetNotElement);
1656 }
1657 let element_range = target.range();
1658 let source = &view.xml()[element_range.clone()];
1659 let (content, qualified_name, self_closing) = element_content_range(source)?;
1660 Ok::<_, XmlDocumentError>((
1661 element_range.clone(),
1662 (element_range.start + content.start)..(element_range.start + content.end),
1663 qualified_name,
1664 self_closing,
1665 ))
1666 })?;
1667 if !self_closing {
1668 return self
1669 .parse_wrapped_range(content_range, replacement, maximum, settings, budget)
1670 .map(|_| ());
1671 }
1672
1673 let source = &self.as_xml()[element_range.clone()];
1674 let slash = source.rfind("/>").ok_or_else(|| {
1675 XmlDocumentError::InvalidReplacement("self-closing element has no terminator".into())
1676 })?;
1677 let wrapped = wrapped_fragment(replacement);
1678 let wrapper_start = element_range.start + slash + 1;
1679 let expanded = format!("{}>{wrapped}</{qualified_name}>", &source[..slash]);
1680 self.parse_wrapped_edit(
1681 element_range,
1682 &expanded,
1683 wrapper_start..(wrapper_start + wrapped.len()),
1684 maximum,
1685 settings,
1686 budget,
1687 )
1688 .map(|_| ())
1689 }
1690
1691 fn parse_wrapped_range(
1692 &self,
1693 range: std::ops::Range<usize>,
1694 replacement: &str,
1695 maximum: Option<usize>,
1696 settings: DocumentParseSettings,
1697 budget: Option<&XmlParseWorkBudget>,
1698 ) -> Result<(DocumentCell, std::ops::Range<usize>), XmlDocumentError> {
1699 let wrapped = wrapped_fragment(replacement);
1700 let wrapper_range = range.start..(range.start + wrapped.len());
1701 self.parse_wrapped_edit(range, &wrapped, wrapper_range, maximum, settings, budget)
1702 }
1703
1704 fn parse_wrapped_edit(
1705 &self,
1706 range: std::ops::Range<usize>,
1707 replacement: &str,
1708 wrapper_range: std::ops::Range<usize>,
1709 maximum: Option<usize>,
1710 settings: DocumentParseSettings,
1711 budget: Option<&XmlParseWorkBudget>,
1712 ) -> Result<(DocumentCell, std::ops::Range<usize>), XmlDocumentError> {
1713 let projected = self
1714 .as_xml()
1715 .len()
1716 .checked_sub(range.len())
1717 .and_then(|length| length.checked_add(replacement.len()))
1718 .ok_or_else(|| {
1719 XmlDocumentError::InvalidReplacement("validation length overflow".into())
1720 })?;
1721 let mut candidate = String::with_capacity(projected);
1722 candidate.push_str(&self.as_xml()[..range.start]);
1723 candidate.push_str(replacement);
1724 candidate.push_str(&self.as_xml()[range.end..]);
1725 let active_nodes_limit = maximum
1726 .map(|maximum| maximum.min(settings.nodes_limit as usize))
1727 .map(|maximum| u32::try_from(maximum).unwrap_or(u32::MAX))
1728 .unwrap_or(settings.nodes_limit);
1729 let parsed = build_cell(
1730 candidate,
1731 DocumentParseSettings {
1732 nodes_limit: active_nodes_limit.saturating_add(VALIDATION_WRAPPER_NODE_OVERHEAD),
1733 depth_limit: settings.depth_limit.saturating_add(1),
1736 namespace_bindings_limit: settings.namespace_bindings_limit.saturating_add(1),
1737 max_bytes: projected,
1738 ..settings
1739 },
1740 budget,
1741 )
1742 .map_err(|error| match (maximum, error) {
1743 (Some(_), XmlDocumentError::Parse(ParseError::NodesLimitReached)) => {
1744 XmlDocumentError::ProjectedNodeLimit {
1745 maximum: active_nodes_limit as usize,
1746 }
1747 }
1748 (_, XmlDocumentError::DocumentTooDeep { actual, .. }) => {
1749 XmlDocumentError::DocumentTooDeep {
1750 maximum: settings.depth_limit,
1751 actual: actual.saturating_sub(1),
1752 }
1753 }
1754 (_, error) => strip_validation_wrapper_namespace_binding(error, settings),
1755 })?;
1756 parsed.with_dependent(|_, document| {
1757 validation_wrapper(document, wrapper_range.clone()).map(|_| ())
1758 })?;
1759 Ok((parsed, wrapper_range))
1760 }
1761}
1762
1763impl<'a> DocumentView<'a> {
1764 #[must_use]
1766 pub const fn identity(self) -> DocumentIdentity {
1767 self.identity
1768 }
1769
1770 #[must_use]
1772 pub const fn generation(self) -> u64 {
1773 self.generation
1774 }
1775
1776 #[must_use]
1778 pub fn xml(self) -> &'a str {
1779 self.parsed.document.input_text()
1780 }
1781
1782 #[must_use]
1784 pub fn root(self) -> NodeIdentity {
1785 self.node_identity(self.parsed.document.root())
1786 }
1787
1788 #[must_use]
1790 pub fn root_element(self) -> NodeIdentity {
1791 self.node_identity(self.parsed.document.root_element())
1792 }
1793
1794 #[must_use]
1796 pub fn node_count(self) -> usize {
1797 self.parsed.node_count
1798 }
1799
1800 #[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
1801 pub(crate) fn max_depth(self) -> usize {
1802 self.parsed.max_depth
1803 }
1804
1805 #[must_use]
1807 pub fn node_for_id(
1808 self,
1809 value: &str,
1810 registrations: &[IdAttributeRegistration],
1811 ) -> Option<NodeIdentity> {
1812 let mut matches = HashSet::new();
1813 match self.parsed.indexes.default_ids.get(value) {
1814 Some(Some(node)) => {
1815 matches.insert(*node);
1816 }
1817 Some(None) => return None,
1818 None => {}
1819 }
1820 for registration in registrations {
1821 let key = (
1822 registration.attribute_local_name().to_owned(),
1823 value.to_owned(),
1824 );
1825 if let Some(nodes) = self.parsed.indexes.attributes_by_value.get(&key) {
1826 matches.extend(nodes.iter().copied().filter(|node_id| {
1827 self.parsed
1828 .document
1829 .get_node(*node_id)
1830 .is_some_and(|node| registration.matches_node(node))
1831 }));
1832 }
1833 }
1834 if matches.len() == 1 {
1835 matches
1836 .iter()
1837 .next()
1838 .map(|node| self.node_identity_by_id(*node))
1839 } else {
1840 None
1841 }
1842 }
1843
1844 #[cfg(feature = "xmldsig")]
1845 pub(crate) fn id_index(
1846 self,
1847 registrations: &[IdAttributeRegistration],
1848 ) -> HashMap<String, NodeId> {
1849 let mut candidates: HashMap<String, HashSet<NodeId>> = HashMap::new();
1850 let mut ambiguous = HashSet::new();
1851 for (value, node) in &self.parsed.indexes.default_ids {
1852 if let Some(node) = node {
1853 candidates.entry(value.clone()).or_default().insert(*node);
1854 } else {
1855 ambiguous.insert(value.clone());
1856 }
1857 }
1858 for ((attribute_name, value), nodes) in &self.parsed.indexes.attributes_by_value {
1859 for registration in registrations
1860 .iter()
1861 .filter(|registration| registration.attribute_local_name() == attribute_name)
1862 {
1863 candidates
1864 .entry(value.clone())
1865 .or_default()
1866 .extend(nodes.iter().copied().filter(|node_id| {
1867 self.parsed
1868 .document
1869 .get_node(*node_id)
1870 .is_some_and(|node| registration.matches_node(node))
1871 }));
1872 }
1873 }
1874 candidates
1875 .into_iter()
1876 .filter_map(|(value, nodes)| {
1877 if nodes.len() == 1 && !ambiguous.contains(&value) {
1878 nodes.into_iter().next().map(|node| (value, node))
1879 } else {
1880 None
1881 }
1882 })
1883 .collect()
1884 }
1885
1886 pub fn document_order(self, identity: NodeIdentity) -> Result<usize, XmlDocumentError> {
1888 self.validate_identity(identity)?;
1889 self.parsed
1890 .indexes
1891 .order
1892 .get(&self.resolve_node(identity)?.id())
1893 .copied()
1894 .ok_or(XmlDocumentError::MissingNode)
1895 }
1896
1897 pub fn node_order(self, identity: NodeIdentity) -> Result<SemanticOrder, XmlDocumentError> {
1899 Ok(SemanticOrder {
1900 node: self.document_order(identity)?,
1901 phase: 0,
1902 position: 0,
1903 })
1904 }
1905
1906 pub fn attribute_order(
1908 self,
1909 identity: &AttributeIdentity,
1910 ) -> Result<SemanticOrder, XmlDocumentError> {
1911 let owner = self.resolve_node(identity.owner)?;
1912 let position = owner
1913 .attributes()
1914 .position(|attribute| {
1915 attribute.namespace() == identity.namespace.as_deref()
1916 && attribute.name() == identity.local_name
1917 })
1918 .ok_or(XmlDocumentError::MissingNode)?;
1919 Ok(SemanticOrder {
1920 node: self.document_order(identity.owner)?,
1921 phase: 2,
1922 position,
1923 })
1924 }
1925
1926 pub fn namespace_order(
1928 self,
1929 identity: &NamespaceIdentity,
1930 ) -> Result<SemanticOrder, XmlDocumentError> {
1931 let owner = self.resolve_node(identity.owner)?;
1932 let target = (identity.prefix.as_str(), identity.uri.as_str());
1933 let mut found = false;
1934 let mut position = 0;
1935 for namespace in owner.namespaces() {
1938 let candidate = (namespace.name().unwrap_or_default(), namespace.uri());
1939 match candidate.cmp(&target) {
1940 std::cmp::Ordering::Less => position += 1,
1941 std::cmp::Ordering::Equal => found = true,
1942 std::cmp::Ordering::Greater => {}
1943 }
1944 }
1945 if !found {
1946 return Err(XmlDocumentError::MissingNode);
1947 }
1948 Ok(SemanticOrder {
1949 node: self.document_order(identity.owner)?,
1950 phase: 1,
1951 position,
1952 })
1953 }
1954
1955 pub(crate) fn document(self) -> &'a Document<'a> {
1956 &self.parsed.document
1957 }
1958
1959 pub(crate) fn node_identity(self, node: Node<'_, '_>) -> NodeIdentity {
1960 NodeIdentity {
1961 document: self.identity,
1962 generation: self.generation,
1963 semantic_id: SemanticDocument::node_id(&self.parsed.document, node),
1964 }
1965 }
1966
1967 pub(crate) fn node_identity_by_id(self, backend: NodeId) -> NodeIdentity {
1968 let node = self
1969 .parsed
1970 .document
1971 .get_node(backend)
1972 .expect("indexed backend node must remain in the retained document");
1973 self.node_identity(node)
1974 }
1975
1976 pub(crate) fn resolve_node(
1977 self,
1978 identity: NodeIdentity,
1979 ) -> Result<Node<'a, 'a>, XmlDocumentError> {
1980 self.validate_identity(identity)?;
1981 SemanticDocument::node(&self.parsed.document, identity.semantic_id)
1982 .ok_or(XmlDocumentError::MissingNode)
1983 }
1984
1985 fn resolve_mutation_node(
1986 self,
1987 identity: NodeIdentity,
1988 ) -> Result<Node<'a, 'a>, XmlDocumentError> {
1989 let node = self.resolve_node(identity)?;
1990 if !node.has_actionable_range() {
1991 return Err(XmlDocumentError::EntityExpandedMutationTarget);
1992 }
1993 Ok(node)
1994 }
1995
1996 pub fn attribute_identity(
1998 self,
1999 owner: NodeIdentity,
2000 namespace: Option<&str>,
2001 local_name: &str,
2002 ) -> Result<AttributeIdentity, XmlDocumentError> {
2003 let node = self.resolve_node(owner)?;
2004 if !node.is_element()
2005 || !node.attributes().any(|attribute| {
2006 attribute.namespace() == namespace && attribute.name() == local_name
2007 })
2008 {
2009 return Err(XmlDocumentError::MissingNode);
2010 }
2011 Ok(AttributeIdentity {
2012 owner,
2013 namespace: namespace.map(str::to_owned),
2014 local_name: local_name.to_owned(),
2015 })
2016 }
2017
2018 pub fn namespace_identities(
2020 self,
2021 owner: NodeIdentity,
2022 ) -> Result<Vec<NamespaceIdentity>, XmlDocumentError> {
2023 let node = self.resolve_node(owner)?;
2024 if !node.is_element() {
2025 return Err(XmlDocumentError::MissingNode);
2026 }
2027 let mut namespaces = node
2032 .namespaces()
2033 .map(|namespace| NamespaceIdentity {
2034 owner,
2035 prefix: namespace.name().unwrap_or_default().to_owned(),
2036 uri: namespace.uri().to_owned(),
2037 })
2038 .collect::<Vec<_>>();
2039 namespaces
2040 .sort_by(|left, right| (&left.prefix, &left.uri).cmp(&(&right.prefix, &right.uri)));
2041 Ok(namespaces)
2042 }
2043
2044 fn validate_identity(self, identity: NodeIdentity) -> Result<(), XmlDocumentError> {
2045 if identity.document != self.identity {
2046 return Err(XmlDocumentError::ForeignIdentity);
2047 }
2048 if identity.generation != self.generation {
2049 return Err(XmlDocumentError::StaleIdentity {
2050 identity: identity.generation,
2051 current: self.generation,
2052 });
2053 }
2054 Ok(())
2055 }
2056}
2057
2058fn build_cell(
2059 xml: String,
2060 settings: DocumentParseSettings,
2061 budget: Option<&XmlParseWorkBudget>,
2062) -> Result<DocumentCell, XmlDocumentError> {
2063 let peak_namespace_bindings = preflight_document_limits(&xml, settings, budget)?;
2064 build_cell_after_preflight(xml, settings, budget, peak_namespace_bindings)
2065}
2066
2067fn build_cell_after_preflight(
2068 xml: String,
2069 settings: DocumentParseSettings,
2070 budget: Option<&XmlParseWorkBudget>,
2071 peak_namespace_bindings: usize,
2072) -> Result<DocumentCell, XmlDocumentError> {
2073 charge_semantic_parser_work(budget, xml.len())?;
2074 build_semantic_cell(xml, settings, peak_namespace_bindings)
2075}
2076
2077fn build_semantic_cell(
2078 xml: String,
2079 settings: DocumentParseSettings,
2080 peak_namespace_bindings: usize,
2081) -> Result<DocumentCell, XmlDocumentError> {
2082 #[cfg(not(any(feature = "xmldsig", feature = "xmlenc")))]
2083 let _ = peak_namespace_bindings;
2084 DocumentCell::try_new(xml, |source| {
2085 let (document, metrics) = parse_semantic_document(source, settings)?;
2086 let indexes = DocumentIndexes::build(&document);
2087 Ok::<_, XmlDocumentError>(ParsedDocument {
2088 document,
2089 indexes,
2090 node_count: metrics.node_count,
2091 #[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
2092 max_depth: metrics.max_depth,
2093 #[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
2094 peak_namespace_bindings,
2095 })
2096 })
2097}
2098
2099pub(crate) fn parse_borrowed_with_settings_and_budget<'a>(
2100 xml: &'a str,
2101 settings: DocumentParseSettings,
2102 budget: Option<&XmlParseWorkBudget>,
2103) -> Result<Document<'a>, XmlDocumentError> {
2104 preflight_document_limits(xml, settings, budget)?;
2105 charge_semantic_parser_work(budget, xml.len())?;
2106 let (document, _) = parse_semantic_document(xml, settings)?;
2107 Ok(document)
2108}
2109
2110fn preflight_document_limits(
2111 xml: &str,
2112 settings: DocumentParseSettings,
2113 budget: Option<&XmlParseWorkBudget>,
2114) -> Result<usize, XmlDocumentError> {
2115 if xml.len() > settings.max_bytes {
2118 return Err(XmlDocumentError::DocumentTooLarge {
2119 maximum: settings.max_bytes,
2120 actual: xml.len(),
2121 });
2122 }
2123 charge_parse_work(budget, xml.len())?;
2124 let mut dtd = InternalDtd::default();
2125 let mut expansion_stack = HashSet::new();
2126 let mut state = DocumentPreflightState::default();
2127 preflight_xml_fragment(
2128 xml,
2129 settings,
2130 &mut dtd,
2131 &mut expansion_stack,
2132 &mut state,
2133 budget,
2134 true,
2135 )?;
2136 Ok(state.peak_namespace_bindings)
2137}
2138
2139#[derive(Default)]
2140struct DocumentPreflightState {
2141 depth: usize,
2142 nodes: u32,
2143 in_character_data: bool,
2144 entity_expansions: u32,
2145 entity_expansion_work: usize,
2146 active_namespace_bindings: HashSet<String>,
2147 peak_namespace_bindings: usize,
2148 namespace_scopes: Vec<Vec<(String, bool)>>,
2149}
2150
2151#[derive(Default)]
2152struct InternalDtd {
2153 entities: HashMap<String, String>,
2154 attribute_defaults: HashMap<String, Vec<InternalAttributeDefault>>,
2155}
2156
2157struct InternalAttributeDefault {
2158 attribute_name: String,
2159 value: String,
2160}
2161
2162#[derive(Clone, Copy, PartialEq, Eq)]
2163enum FragmentContext {
2164 Content,
2165 Attribute,
2166}
2167
2168enum PreflightEvent {
2169 DocType(Option<String>),
2170 GeneralRef {
2171 name: Option<String>,
2172 is_character_reference: bool,
2173 },
2174 CharacterData {
2175 xml_whitespace: bool,
2176 },
2177 Start {
2178 attribute_source: Vec<u8>,
2179 namespace_declarations: Vec<NamespaceDeclaration>,
2180 },
2181 Empty {
2182 attribute_source: Vec<u8>,
2183 namespace_declarations: Vec<NamespaceDeclaration>,
2184 },
2185 End,
2186 Node,
2187 Markup,
2188 Done,
2189}
2190
2191fn preflight_xml_fragment(
2192 xml: &str,
2193 settings: DocumentParseSettings,
2194 dtd: &mut InternalDtd,
2195 expansion_stack: &mut HashSet<String>,
2196 state: &mut DocumentPreflightState,
2197 budget: Option<&XmlParseWorkBudget>,
2198 collect_doctype: bool,
2199) -> Result<(), XmlDocumentError> {
2200 enum FragmentSource {
2201 Document,
2202 Entity(String),
2203 }
2204
2205 struct FragmentFrame {
2206 source: FragmentSource,
2207 offset: usize,
2208 collect_doctype: bool,
2209 context: FragmentContext,
2210 pending_attribute_source: Vec<u8>,
2211 pending_attribute_offset: usize,
2212 }
2213
2214 if state.nodes == 0 {
2215 state.nodes = 1;
2217 }
2218 let mut fragments = vec![FragmentFrame {
2219 source: FragmentSource::Document,
2220 offset: 0,
2221 collect_doctype,
2222 context: FragmentContext::Content,
2223 pending_attribute_source: Vec::new(),
2224 pending_attribute_offset: 0,
2225 }];
2226 while !fragments.is_empty() {
2229 let (event, collect_doctype, context) = {
2230 let frame = fragments.last_mut().expect("fragment stack is not empty");
2231 let mut pending = general_references(
2232 &frame.pending_attribute_source[frame.pending_attribute_offset..],
2233 );
2234 if let Some(name) = pending.next() {
2235 frame.pending_attribute_offset = frame
2236 .pending_attribute_offset
2237 .saturating_add(pending.consumed());
2238 (
2242 PreflightEvent::GeneralRef {
2243 name: Some(name.to_owned()),
2244 is_character_reference: false,
2245 },
2246 false,
2247 FragmentContext::Attribute,
2248 )
2249 } else {
2250 frame.pending_attribute_source.clear();
2251 frame.pending_attribute_offset = 0;
2252 let source = match &frame.source {
2253 FragmentSource::Document => xml,
2254 FragmentSource::Entity(name) => dtd
2255 .entities
2256 .get(name)
2257 .expect("active entity replacement remains registered"),
2258 };
2259 let remaining = &source[frame.offset..];
2260 let (event, consumed) =
2261 scan_preflight_event(remaining, frame.context, dtd, state, budget)?;
2262 frame.offset = frame.offset.saturating_add(consumed);
2263 (event, frame.collect_doctype, frame.context)
2264 }
2265 };
2266
2267 match event {
2268 PreflightEvent::DocType(doctype) => {
2269 if collect_doctype
2270 && settings.allow_dtd
2271 && let Some(doctype) = doctype
2272 {
2273 collect_internal_dtd(&doctype, dtd);
2274 }
2275 state.in_character_data = false;
2276 continue;
2277 }
2278 PreflightEvent::GeneralRef {
2279 name,
2280 is_character_reference,
2281 } => {
2282 let Some(name) = name else {
2283 continue;
2284 };
2285 if is_character_reference {
2286 if context == FragmentContext::Content {
2287 observe_preflight_node(state, settings, true)?;
2288 }
2289 continue;
2290 }
2291 if expansion_stack.insert(name.clone()) {
2292 if let Some(replacement) = dtd.entities.get(&name) {
2293 state.entity_expansions = state.entity_expansions.saturating_add(1);
2294 let maximum = crate::hard_limits::XML_ENTITY_EXPANSION_CEILING;
2295 if state.entity_expansions > maximum {
2296 return Err(XmlDocumentError::Parse(
2297 ParseError::EntityExpansionLimitReached {
2298 maximum,
2299 actual: state.entity_expansions,
2300 },
2301 ));
2302 }
2303 charge_entity_expansion_work(state, budget, replacement.len())?;
2304 fragments.push(FragmentFrame {
2305 source: FragmentSource::Entity(name),
2306 offset: 0,
2307 collect_doctype: false,
2308 context,
2309 pending_attribute_source: Vec::new(),
2310 pending_attribute_offset: 0,
2311 });
2312 } else {
2313 expansion_stack.remove(&name);
2314 }
2315 }
2316 continue;
2317 }
2318 PreflightEvent::Done => {
2319 let frame = fragments.pop().expect("fragment stack is not empty");
2320 if let FragmentSource::Entity(name) = frame.source {
2321 expansion_stack.remove(&name);
2322 }
2323 continue;
2324 }
2325 PreflightEvent::CharacterData { xml_whitespace } => {
2326 if context == FragmentContext::Attribute {
2327 continue;
2328 }
2329 if state.depth == 0 && xml_whitespace {
2330 state.in_character_data = false;
2331 continue;
2332 }
2333 let starts_semantic_node = !state.in_character_data;
2334 state.in_character_data = true;
2335 if starts_semantic_node {
2336 observe_preflight_node(state, settings, false)?;
2337 }
2338 continue;
2339 }
2340 PreflightEvent::Node => {
2341 state.in_character_data = false;
2342 observe_preflight_node(state, settings, false)?;
2343 continue;
2344 }
2345 PreflightEvent::Markup => {
2346 state.in_character_data = false;
2347 continue;
2348 }
2349 PreflightEvent::Start { .. } | PreflightEvent::Empty { .. } | PreflightEvent::End => {}
2350 }
2351
2352 if context == FragmentContext::Attribute {
2353 continue;
2354 }
2355 state.in_character_data = false;
2356 match event {
2357 PreflightEvent::Start {
2358 attribute_source,
2359 namespace_declarations,
2360 } => {
2361 let frame = fragments
2362 .last_mut()
2363 .expect("active element frame remains registered");
2364 frame.pending_attribute_source = attribute_source;
2365 frame.pending_attribute_offset = 0;
2366 let changes = apply_preflight_namespace_declarations(
2367 state,
2368 namespace_declarations,
2369 settings.namespace_bindings_limit,
2370 )?;
2371 state.namespace_scopes.push(changes);
2372 observe_preflight_node(state, settings, false)?;
2373 state.depth = state.depth.saturating_add(1);
2374 if state.depth > settings.depth_limit {
2375 return Err(XmlDocumentError::DocumentTooDeep {
2376 maximum: settings.depth_limit,
2377 actual: state.depth,
2378 });
2379 }
2380 }
2381 PreflightEvent::Empty {
2382 attribute_source,
2383 namespace_declarations,
2384 } => {
2385 let frame = fragments
2386 .last_mut()
2387 .expect("active element frame remains registered");
2388 frame.pending_attribute_source = attribute_source;
2389 frame.pending_attribute_offset = 0;
2390 let changes = apply_preflight_namespace_declarations(
2391 state,
2392 namespace_declarations,
2393 settings.namespace_bindings_limit,
2394 )?;
2395 observe_preflight_node(state, settings, false)?;
2396 let actual = state.depth.saturating_add(1);
2397 if actual > settings.depth_limit {
2398 return Err(XmlDocumentError::DocumentTooDeep {
2399 maximum: settings.depth_limit,
2400 actual,
2401 });
2402 }
2403 restore_preflight_namespace_bindings(&mut state.active_namespace_bindings, changes);
2404 }
2405 PreflightEvent::End => {
2406 state.depth = state.depth.saturating_sub(1);
2407 if let Some(changes) = state.namespace_scopes.pop() {
2408 restore_preflight_namespace_bindings(
2409 &mut state.active_namespace_bindings,
2410 changes,
2411 );
2412 }
2413 }
2414 _ => unreachable!("non-structural events continue above"),
2415 }
2416 }
2417 Ok(())
2418}
2419
2420fn scan_preflight_event(
2421 remaining: &str,
2422 context: FragmentContext,
2423 dtd: &InternalDtd,
2424 state: &mut DocumentPreflightState,
2425 budget: Option<&XmlParseWorkBudget>,
2426) -> Result<(PreflightEvent, usize), XmlDocumentError> {
2427 if remaining.is_empty() {
2428 return Ok((PreflightEvent::Done, 0));
2429 }
2430 if context == FragmentContext::Attribute {
2431 let mut references = general_references(remaining.as_bytes());
2432 return Ok(references
2433 .next()
2434 .map_or((PreflightEvent::Done, remaining.len()), |name| {
2435 let consumed = references.consumed();
2436 (
2437 PreflightEvent::GeneralRef {
2438 is_character_reference: is_character_reference(name),
2439 name: Some(name.to_owned()),
2440 },
2441 consumed,
2442 )
2443 }));
2444 }
2445 if remaining.starts_with("<!--") {
2446 return Ok(find_bytes(&remaining.as_bytes()[4..], b"-->")
2447 .map_or((PreflightEvent::Done, remaining.len()), |end| {
2448 (PreflightEvent::Node, 4 + end + 3)
2449 }));
2450 }
2451 if remaining.starts_with("<![CDATA[") {
2452 return Ok(find_bytes(&remaining.as_bytes()[9..], b"]]>").map_or(
2453 (PreflightEvent::Done, remaining.len()),
2454 |end| {
2455 (
2456 PreflightEvent::CharacterData {
2457 xml_whitespace: false,
2458 },
2459 9 + end + 3,
2460 )
2461 },
2462 ));
2463 }
2464 if remaining.starts_with("<?") {
2465 return Ok(find_bytes(&remaining.as_bytes()[2..], b"?>").map_or(
2466 (PreflightEvent::Done, remaining.len()),
2467 |end| {
2468 let event = if remaining
2469 .get(2..5)
2470 .is_some_and(|target| target.eq_ignore_ascii_case("xml"))
2471 && remaining
2472 .as_bytes()
2473 .get(5)
2474 .is_some_and(u8::is_ascii_whitespace)
2475 {
2476 PreflightEvent::Markup
2477 } else {
2478 PreflightEvent::Node
2479 };
2480 (event, 2 + end + 2)
2481 },
2482 ));
2483 }
2484 if remaining.starts_with("<!DOCTYPE") {
2485 return Ok(find_doctype_end(remaining).map_or(
2486 (PreflightEvent::Done, remaining.len()),
2487 |end| {
2488 (
2489 PreflightEvent::DocType(Some(remaining[..end].to_owned())),
2490 end,
2491 )
2492 },
2493 ));
2494 }
2495 if remaining.starts_with("</") {
2496 return Ok(find_unquoted_byte(remaining.as_bytes(), b'>', 2)
2497 .map_or((PreflightEvent::Done, remaining.len()), |end| {
2498 (PreflightEvent::End, end + 1)
2499 }));
2500 }
2501 if remaining.starts_with('<') {
2502 let Some(end) = find_unquoted_byte(remaining.as_bytes(), b'>', 1) else {
2503 return Ok((PreflightEvent::Done, remaining.len()));
2504 };
2505 let opening = &remaining[..=end];
2506 let (element_name, attributes) = opening_tag_attributes(opening);
2507 let attribute_source =
2508 element_attribute_reference_source(opening, element_name, &attributes, dtd);
2509 let namespace_declarations =
2510 namespace_declarations(element_name, &attributes, dtd, state, budget)?;
2511 let event = if opening[..opening.len() - 1].trim_end().ends_with('/') {
2512 PreflightEvent::Empty {
2513 attribute_source,
2514 namespace_declarations,
2515 }
2516 } else {
2517 PreflightEvent::Start {
2518 attribute_source,
2519 namespace_declarations,
2520 }
2521 };
2522 return Ok((event, end + 1));
2523 }
2524 if let Some(reference) = remaining.strip_prefix('&') {
2525 if let Some(end) = reference.find([';', '<', '&'])
2531 && reference.as_bytes()[end] == b';'
2532 {
2533 let name = &reference[..end];
2534 return Ok((
2535 PreflightEvent::GeneralRef {
2536 name: Some(name.to_owned()),
2537 is_character_reference: is_character_reference(name),
2538 },
2539 end + 2,
2540 ));
2541 }
2542 return Ok((
2543 PreflightEvent::CharacterData {
2544 xml_whitespace: false,
2545 },
2546 1,
2547 ));
2548 }
2549 let end = remaining.find(['<', '&']).unwrap_or(remaining.len());
2550 let text = &remaining[..end];
2551 Ok((
2552 PreflightEvent::CharacterData {
2553 xml_whitespace: text
2554 .chars()
2555 .all(|character| matches!(character, ' ' | '\t' | '\r' | '\n')),
2556 },
2557 end,
2558 ))
2559}
2560
2561fn is_character_reference(name: &str) -> bool {
2562 name.starts_with('#') || matches!(name, "amp" | "apos" | "gt" | "lt" | "quot")
2563}
2564
2565fn find_doctype_end(doctype: &str) -> Option<usize> {
2566 let mut quote = None;
2567 let mut subset_depth = 0usize;
2568 let bytes = doctype.as_bytes();
2569 let mut offset = 0;
2570 while offset < bytes.len() {
2571 if quote.is_none() && bytes[offset..].starts_with(b"<!--") {
2572 let end = bytes[offset + 4..]
2573 .windows(3)
2574 .position(|window| window == b"-->")?;
2575 offset += 4 + end + 3;
2576 continue;
2577 }
2578 if quote.is_none() && bytes[offset..].starts_with(b"<?") {
2579 let end = bytes[offset + 2..]
2580 .windows(2)
2581 .position(|window| window == b"?>")?;
2582 offset += 2 + end + 2;
2583 continue;
2584 }
2585 let byte = bytes[offset];
2586 match (quote, byte) {
2587 (None, b'\'' | b'"') => quote = Some(byte),
2588 (Some(delimiter), current) if delimiter == current => quote = None,
2589 (None, b'[') => subset_depth = subset_depth.saturating_add(1),
2590 (None, b']') => subset_depth = subset_depth.saturating_sub(1),
2591 (None, b'>') if subset_depth == 0 => return Some(offset + 1),
2592 _ => {}
2593 }
2594 offset += 1;
2595 }
2596 None
2597}
2598
2599struct NamespaceDeclaration {
2600 prefix: String,
2601 is_undeclaration: bool,
2602}
2603
2604fn namespace_declarations(
2605 element_name: &str,
2606 attributes: &[(&str, &str)],
2607 dtd: &InternalDtd,
2608 state: &mut DocumentPreflightState,
2609 budget: Option<&XmlParseWorkBudget>,
2610) -> Result<Vec<NamespaceDeclaration>, XmlDocumentError> {
2611 let mut declarations = Vec::new();
2612 for &(name, value) in attributes {
2613 let prefix = if name == "xmlns" {
2614 ""
2615 } else {
2616 let Some(prefix) = name.strip_prefix("xmlns:") else {
2617 continue;
2618 };
2619 prefix
2620 };
2621 declarations.push(NamespaceDeclaration {
2622 prefix: prefix.to_owned(),
2623 is_undeclaration: namespace_value_expands_to_empty(value, dtd, state, budget)?,
2624 });
2625 }
2626 if let Some(defaults) = dtd.attribute_defaults.get(element_name) {
2627 for default in defaults {
2628 if attributes
2629 .iter()
2630 .any(|(name, _)| *name == default.attribute_name)
2631 {
2632 continue;
2633 }
2634 let prefix = if default.attribute_name == "xmlns" {
2635 ""
2636 } else if let Some(prefix) = default.attribute_name.strip_prefix("xmlns:") {
2637 prefix
2638 } else {
2639 continue;
2640 };
2641 declarations.push(NamespaceDeclaration {
2642 prefix: prefix.to_owned(),
2643 is_undeclaration: namespace_value_expands_to_empty(
2644 &default.value,
2645 dtd,
2646 state,
2647 budget,
2648 )?,
2649 });
2650 }
2651 }
2652 Ok(declarations)
2653}
2654
2655fn namespace_value_expands_to_empty<'a>(
2656 value: &'a str,
2657 dtd: &'a InternalDtd,
2658 state: &mut DocumentPreflightState,
2659 budget: Option<&XmlParseWorkBudget>,
2660) -> Result<bool, XmlDocumentError> {
2661 if value.is_empty() {
2666 return Ok(true);
2667 }
2668 if !value.starts_with('&') {
2669 return Ok(false);
2670 }
2671 let mut pending = vec![value];
2672 let mut expansions = 0u32;
2673 while let Some(remaining) = pending.pop() {
2674 if remaining.is_empty() {
2675 continue;
2676 }
2677 let Some(reference) = remaining.strip_prefix('&') else {
2678 return Ok(false);
2679 };
2680 let Some(end) = reference.find(';') else {
2681 return Ok(false);
2682 };
2683 let name = &reference[..end];
2684 if name.starts_with('#') || matches!(name, "amp" | "lt" | "gt" | "apos" | "quot") {
2688 return Ok(false);
2689 }
2690 let Some(replacement) = dtd.entities.get(name) else {
2691 return Ok(false);
2692 };
2693 expansions = expansions.saturating_add(1);
2694 if expansions > crate::hard_limits::XML_ENTITY_EXPANSION_CEILING {
2695 return Ok(false);
2696 }
2697 charge_entity_expansion_work(state, budget, replacement.len())?;
2698 pending.push(&reference[end + 1..]);
2699 pending.push(replacement);
2700 }
2701 Ok(true)
2702}
2703
2704fn apply_preflight_namespace_declarations(
2705 state: &mut DocumentPreflightState,
2706 declarations: Vec<NamespaceDeclaration>,
2707 maximum: usize,
2708) -> Result<Vec<(String, bool)>, XmlDocumentError> {
2709 let mut changes = Vec::with_capacity(declarations.len());
2710 for declaration in declarations {
2711 let was_active = if declaration.is_undeclaration {
2712 state.active_namespace_bindings.remove(&declaration.prefix)
2713 } else {
2714 !state
2715 .active_namespace_bindings
2716 .insert(declaration.prefix.clone())
2717 };
2718 changes.push((declaration.prefix, was_active));
2719 }
2720 let actual = state.active_namespace_bindings.len();
2721 state.peak_namespace_bindings = state.peak_namespace_bindings.max(actual);
2722 if actual > maximum {
2723 return Err(XmlDocumentError::Parse(
2724 ParseError::NamespaceBindingLimitReached { maximum, actual },
2725 ));
2726 }
2727 Ok(changes)
2728}
2729
2730fn restore_preflight_namespace_bindings(
2731 active: &mut HashSet<String>,
2732 changes: Vec<(String, bool)>,
2733) {
2734 for (prefix, was_active) in changes.into_iter().rev() {
2735 if was_active {
2736 active.insert(prefix);
2737 } else {
2738 active.remove(&prefix);
2739 }
2740 }
2741}
2742
2743fn element_attribute_reference_source(
2744 opening: &str,
2745 element_name: &str,
2746 attributes: &[(&str, &str)],
2747 dtd: &InternalDtd,
2748) -> Vec<u8> {
2749 let mut source = if opening.contains('&') {
2750 opening.as_bytes().to_vec()
2751 } else {
2752 Vec::new()
2753 };
2754 if dtd.attribute_defaults.is_empty() {
2755 return source;
2756 }
2757
2758 if let Some(defaults) = dtd.attribute_defaults.get(element_name) {
2759 let mut present_attributes: HashSet<_> = attributes.iter().map(|(name, _)| *name).collect();
2760 for default in defaults {
2761 if present_attributes.insert(default.attribute_name.as_str())
2762 && default.value.contains('&')
2763 {
2764 source.push(b' ');
2765 source.extend_from_slice(default.value.as_bytes());
2766 }
2767 }
2768 }
2769 source
2770}
2771
2772fn opening_tag_attributes(opening: &str) -> (&str, Vec<(&str, &str)>) {
2773 let bytes = opening.as_bytes();
2774 let mut offset = 1usize;
2775 let name_start = offset;
2776 while bytes
2777 .get(offset)
2778 .is_some_and(|byte| !matches!(byte, b' ' | b'\t' | b'\r' | b'\n' | b'/' | b'>'))
2779 {
2780 offset += 1;
2781 }
2782 let element_name = &opening[name_start..offset];
2783 let mut attributes = Vec::new();
2784 while offset < bytes.len() {
2785 while bytes
2786 .get(offset)
2787 .is_some_and(|byte| matches!(byte, b' ' | b'\t' | b'\r' | b'\n'))
2788 {
2789 offset += 1;
2790 }
2791 if bytes
2792 .get(offset)
2793 .is_none_or(|byte| matches!(byte, b'/' | b'>'))
2794 {
2795 break;
2796 }
2797 let start = offset;
2798 while bytes
2799 .get(offset)
2800 .is_some_and(|byte| !matches!(byte, b' ' | b'\t' | b'\r' | b'\n' | b'='))
2801 {
2802 offset += 1;
2803 }
2804 if start == offset {
2805 break;
2806 }
2807 let name = &opening[start..offset];
2808 while bytes
2809 .get(offset)
2810 .is_some_and(|byte| matches!(byte, b' ' | b'\t' | b'\r' | b'\n'))
2811 {
2812 offset += 1;
2813 }
2814 if bytes.get(offset) != Some(&b'=') {
2815 break;
2816 }
2817 offset += 1;
2818 while bytes
2819 .get(offset)
2820 .is_some_and(|byte| matches!(byte, b' ' | b'\t' | b'\r' | b'\n'))
2821 {
2822 offset += 1;
2823 }
2824 let Some(delimiter @ (b'\'' | b'"')) = bytes.get(offset).copied() else {
2825 break;
2826 };
2827 offset += 1;
2828 let value_start = offset;
2829 while bytes.get(offset).is_some_and(|byte| *byte != delimiter) {
2830 offset += 1;
2831 }
2832 let value = &opening[value_start..offset];
2833 attributes.push((name, value));
2834 offset = offset.saturating_add(1);
2835 }
2836 (element_name, attributes)
2837}
2838
2839struct GeneralReferences<'a> {
2840 value: &'a [u8],
2841 offset: usize,
2842}
2843
2844impl GeneralReferences<'_> {
2845 fn consumed(&self) -> usize {
2846 self.offset
2847 }
2848}
2849
2850impl<'a> Iterator for GeneralReferences<'a> {
2851 type Item = &'a str;
2852
2853 fn next(&mut self) -> Option<Self::Item> {
2854 loop {
2855 let relative_start = self.value[self.offset..]
2856 .iter()
2857 .position(|byte| *byte == b'&')?;
2858 let start = self.offset + relative_start + 1;
2859 let Some(relative_end) = self.value[start..].iter().position(|byte| *byte == b';')
2860 else {
2861 self.offset = self.value.len();
2862 return None;
2863 };
2864 let end = start + relative_end;
2865 self.offset = end + 1;
2866 let name = &self.value[start..end];
2867 if !name.starts_with(b"#")
2868 && !matches!(name, b"amp" | b"apos" | b"gt" | b"lt" | b"quot")
2869 && let Ok(name) = std::str::from_utf8(name)
2870 {
2871 return Some(name);
2872 }
2873 }
2874 }
2875}
2876
2877fn general_references(value: &[u8]) -> GeneralReferences<'_> {
2878 GeneralReferences { value, offset: 0 }
2879}
2880
2881fn charge_entity_expansion_work(
2882 state: &mut DocumentPreflightState,
2883 budget: Option<&XmlParseWorkBudget>,
2884 bytes: usize,
2885) -> Result<(), XmlDocumentError> {
2886 charge_parse_work(budget, bytes)?;
2887 if budget.is_none() {
2888 let actual = state.entity_expansion_work.saturating_add(bytes);
2889 let maximum = crate::hard_limits::XML_ENTITY_EXPANSION_WORK_BYTE_CEILING;
2890 if actual > maximum {
2891 return Err(XmlDocumentError::Parse(
2892 ParseError::EntityExpansionWorkLimitReached { maximum, actual },
2893 ));
2894 }
2895 state.entity_expansion_work = actual;
2896 }
2897 Ok(())
2898}
2899
2900pub(crate) fn preflight_dom_limits(
2901 xml: &str,
2902 options: ParsingOptions,
2903) -> Result<ParsingOptions, ParseError> {
2904 let effective = ParsingOptions {
2905 allow_dtd: options.allow_dtd,
2906 nodes_limit: options
2907 .nodes_limit
2908 .min(crate::hard_limits::XML_DOCUMENT_NODE_CEILING),
2909 };
2910 let settings = DocumentParseSettings {
2911 backend: XmlBackend::default(),
2912 allow_dtd: effective.allow_dtd,
2913 nodes_limit: effective.nodes_limit,
2914 depth_limit: crate::hard_limits::XML_DOCUMENT_DEPTH_CEILING,
2915 namespace_bindings_limit: crate::hard_limits::XML_NAMESPACE_BINDING_CEILING,
2916 max_bytes: crate::hard_limits::XML_DOCUMENT_BYTE_CEILING,
2917 };
2918 match preflight_document_limits(xml, settings, None) {
2919 Ok(_) => Ok(effective),
2920 Err(XmlDocumentError::Parse(error)) => Err(error),
2921 Err(XmlDocumentError::DocumentTooDeep { maximum, actual }) => {
2922 Err(ParseError::DepthLimitReached { maximum, actual })
2923 }
2924 Err(XmlDocumentError::DocumentTooLarge { maximum, actual }) => {
2925 Err(ParseError::ByteLimitReached { maximum, actual })
2926 }
2927 Err(error) => Err(ParseError::Backend {
2928 backend: "xml-limit-preflight",
2929 message: error.to_string(),
2930 }),
2931 }
2932}
2933
2934fn observe_preflight_node(
2935 state: &mut DocumentPreflightState,
2936 settings: DocumentParseSettings,
2937 character_data: bool,
2938) -> Result<(), XmlDocumentError> {
2939 if character_data {
2940 if state.in_character_data {
2941 return Ok(());
2942 }
2943 state.in_character_data = true;
2944 }
2945 state.nodes = state.nodes.saturating_add(1);
2946 if state.nodes > settings.nodes_limit {
2947 return Err(XmlDocumentError::Parse(ParseError::NodesLimitReached));
2948 }
2949 Ok(())
2950}
2951
2952fn collect_internal_dtd(doctype: &str, dtd: &mut InternalDtd) {
2953 let Some(subset_start) = find_unquoted_byte(doctype.as_bytes(), b'[', 0) else {
2954 return;
2955 };
2956 let Some(subset_end) = doctype.rfind(']') else {
2957 return;
2958 };
2959 let subset = &doctype[subset_start + 1..subset_end];
2960 let mut offset = 0;
2961 let bytes = subset.as_bytes();
2962 while offset < bytes.len() {
2963 if bytes[offset..].starts_with(b"<!--") {
2964 let Some(end) = find_bytes(&bytes[offset + 4..], b"-->") else {
2965 break;
2966 };
2967 offset += 4 + end + 3;
2968 } else if bytes[offset..].starts_with(b"<?") {
2969 let Some(end) = find_bytes(&bytes[offset + 2..], b"?>") else {
2970 break;
2971 };
2972 offset += 2 + end + 2;
2973 } else if bytes[offset..].starts_with(b"<!ENTITY") {
2974 let declaration_start = offset + "<!ENTITY".len();
2975 let Some(declaration_end) = find_unquoted_byte(bytes, b'>', declaration_start) else {
2976 break;
2977 };
2978 let declaration = &subset[declaration_start..declaration_end];
2979 if let Some((name, value)) = parse_internal_general_entity(declaration) {
2980 dtd.entities
2982 .entry(name.to_owned())
2983 .or_insert_with(|| normalize_internal_entity_value(value));
2984 }
2985 offset = declaration_end + 1;
2986 } else if bytes[offset..].starts_with(b"<!ATTLIST") {
2987 let declaration_start = offset + "<!ATTLIST".len();
2988 let Some(declaration_end) = find_unquoted_byte(bytes, b'>', declaration_start) else {
2989 break;
2990 };
2991 collect_internal_attribute_defaults(
2994 &subset[declaration_start..declaration_end],
2995 &mut dtd.attribute_defaults,
2996 );
2997 offset = declaration_end + 1;
2998 } else if bytes[offset..].starts_with(b"<!") {
2999 let Some(declaration_end) = find_unquoted_byte(bytes, b'>', offset + 2) else {
3000 break;
3001 };
3002 offset = declaration_end + 1;
3003 } else {
3004 offset += 1;
3005 }
3006 }
3007}
3008
3009fn collect_internal_attribute_defaults(
3010 declaration: &str,
3011 defaults: &mut HashMap<String, Vec<InternalAttributeDefault>>,
3012) {
3013 let bytes = declaration.as_bytes();
3014 let mut offset = 0;
3015 skip_dtd_whitespace(bytes, &mut offset);
3016 let Some(element_name) = consume_dtd_token(declaration, &mut offset) else {
3017 return;
3018 };
3019 let mut declarations = Vec::new();
3020 loop {
3021 skip_dtd_whitespace(bytes, &mut offset);
3022 if offset == bytes.len() {
3023 break;
3024 }
3025 let Some(attribute_name) = consume_dtd_token(declaration, &mut offset) else {
3026 break;
3027 };
3028 skip_dtd_whitespace(bytes, &mut offset);
3029 if !consume_attribute_type(declaration, &mut offset) {
3030 break;
3031 }
3032 skip_dtd_whitespace(bytes, &mut offset);
3033
3034 if consume_dtd_keyword(declaration, &mut offset, "#REQUIRED")
3035 || consume_dtd_keyword(declaration, &mut offset, "#IMPLIED")
3036 {
3037 continue;
3038 }
3039 if consume_dtd_keyword(declaration, &mut offset, "#FIXED") {
3040 skip_dtd_whitespace(bytes, &mut offset);
3041 }
3042 let Some(value) = consume_dtd_quoted_value(declaration, &mut offset) else {
3043 break;
3044 };
3045 declarations.push(InternalAttributeDefault {
3046 attribute_name: attribute_name.to_owned(),
3047 value: value.to_owned(),
3048 });
3049 }
3050
3051 if !declarations.is_empty() {
3052 defaults
3053 .entry(element_name.to_owned())
3054 .or_default()
3055 .extend(declarations);
3056 }
3057}
3058
3059fn skip_dtd_whitespace(bytes: &[u8], offset: &mut usize) {
3060 while bytes
3061 .get(*offset)
3062 .is_some_and(|byte| matches!(byte, b' ' | b'\t' | b'\r' | b'\n'))
3063 {
3064 *offset += 1;
3065 }
3066}
3067
3068fn consume_dtd_token<'a>(input: &'a str, offset: &mut usize) -> Option<&'a str> {
3069 let bytes = input.as_bytes();
3070 let start = *offset;
3071 while bytes.get(*offset).is_some_and(|byte| {
3072 !matches!(
3073 byte,
3074 b' ' | b'\t' | b'\r' | b'\n' | b'(' | b')' | b'\'' | b'"'
3075 )
3076 }) {
3077 *offset += 1;
3078 }
3079 (start != *offset).then(|| &input[start..*offset])
3080}
3081
3082fn consume_attribute_type(input: &str, offset: &mut usize) -> bool {
3083 let bytes = input.as_bytes();
3084 if consume_dtd_keyword(input, offset, "NOTATION") {
3085 skip_dtd_whitespace(bytes, offset);
3086 return consume_parenthesized_dtd_group(bytes, offset);
3087 }
3088 if bytes.get(*offset) == Some(&b'(') {
3089 return consume_parenthesized_dtd_group(bytes, offset);
3090 }
3091 consume_dtd_token(input, offset).is_some()
3092}
3093
3094fn consume_parenthesized_dtd_group(bytes: &[u8], offset: &mut usize) -> bool {
3095 if bytes.get(*offset) != Some(&b'(') {
3096 return false;
3097 }
3098 let mut depth = 0usize;
3099 while let Some(byte) = bytes.get(*offset).copied() {
3100 *offset += 1;
3101 match byte {
3102 b'(' => depth += 1,
3103 b')' => {
3104 depth -= 1;
3105 if depth == 0 {
3106 return true;
3107 }
3108 }
3109 _ => {}
3110 }
3111 }
3112 false
3113}
3114
3115fn consume_dtd_keyword(input: &str, offset: &mut usize, keyword: &str) -> bool {
3116 let remaining = &input[*offset..];
3117 if !remaining.starts_with(keyword) {
3118 return false;
3119 }
3120 let end = *offset + keyword.len();
3121 if input
3122 .as_bytes()
3123 .get(end)
3124 .is_some_and(|byte| !matches!(byte, b' ' | b'\t' | b'\r' | b'\n' | b'(' | b'\'' | b'"'))
3125 {
3126 return false;
3127 }
3128 *offset = end;
3129 true
3130}
3131
3132fn consume_dtd_quoted_value<'a>(input: &'a str, offset: &mut usize) -> Option<&'a str> {
3133 let bytes = input.as_bytes();
3134 let quote = *bytes.get(*offset)?;
3135 if !matches!(quote, b'\'' | b'"') {
3136 return None;
3137 }
3138 let start = *offset + 1;
3139 let relative_end = bytes[start..].iter().position(|byte| *byte == quote)?;
3140 let end = start + relative_end;
3141 *offset = end + 1;
3142 Some(&input[start..end])
3143}
3144
3145fn find_bytes(haystack: &[u8], needle: &[u8]) -> Option<usize> {
3146 haystack
3147 .windows(needle.len())
3148 .position(|window| window == needle)
3149}
3150
3151fn parse_internal_general_entity(declaration: &str) -> Option<(&str, &str)> {
3152 let bytes = declaration.as_bytes();
3153 let mut offset = 0usize;
3154 skip_dtd_whitespace(bytes, &mut offset);
3155 if bytes.get(offset) == Some(&b'%') {
3156 return None;
3157 }
3158 let name = consume_dtd_token(declaration, &mut offset)?;
3162 let separator = offset;
3163 skip_dtd_whitespace(bytes, &mut offset);
3164 if offset == separator {
3165 return None;
3166 }
3167 let definition = &declaration[offset..];
3168 let quote = definition.as_bytes().first().copied()?;
3169 if !matches!(quote, b'\'' | b'"') {
3170 return None;
3173 }
3174 let value_end = definition.as_bytes()[1..]
3175 .iter()
3176 .position(|byte| *byte == quote)?
3177 + 1;
3178 Some((name, &definition[1..value_end]))
3179}
3180
3181fn normalize_internal_entity_value(value: &str) -> String {
3182 let mut normalized = String::with_capacity(value.len());
3186 let mut offset = 0;
3187 while let Some(relative_start) = value[offset..].find("&#") {
3188 let start = offset + relative_start;
3189 normalized.push_str(&value[offset..start]);
3190 let Some(relative_end) = value[start + 2..].find(';') else {
3191 normalized.push_str(&value[start..]);
3192 return normalized;
3193 };
3194 let end = start + 2 + relative_end;
3195 let digits = &value[start + 2..end];
3196 let (radix, digits) = digits
3197 .strip_prefix('x')
3198 .map_or((10, digits), |digits| (16, digits));
3199 let replacement = u32::from_str_radix(digits, radix)
3200 .ok()
3201 .filter(|codepoint| is_xml_character(*codepoint))
3202 .and_then(char::from_u32);
3203 if let Some(replacement) = replacement {
3204 normalized.push(replacement);
3205 } else {
3206 normalized.push_str(&value[start..=end]);
3207 }
3208 offset = end + 1;
3209 }
3210 normalized.push_str(&value[offset..]);
3211 normalized
3212}
3213
3214const fn is_xml_character(codepoint: u32) -> bool {
3215 matches!(
3216 codepoint,
3217 0x9 | 0xA | 0xD | 0x20..=0xD7FF | 0xE000..=0xFFFD | 0x10000..=0x10FFFF
3218 )
3219}
3220
3221fn find_unquoted_byte(bytes: &[u8], needle: u8, start: usize) -> Option<usize> {
3222 let mut quote = None;
3223 for (index, byte) in bytes.iter().copied().enumerate().skip(start) {
3224 match byte {
3225 b'\'' | b'"' => match quote {
3226 Some(active) if active == byte => quote = None,
3227 None => quote = Some(byte),
3228 Some(_) => {}
3229 },
3230 _ if quote.is_none() && byte == needle => return Some(index),
3231 _ => {}
3232 }
3233 }
3234 None
3235}
3236
3237fn parse_semantic_document<'a>(
3238 source: &'a str,
3239 settings: DocumentParseSettings,
3240) -> Result<(Document<'a>, DocumentMetrics), XmlDocumentError> {
3241 let document = Document::parse_after_limit_preflight_with_backend(
3242 source,
3243 ParsingOptions {
3244 allow_dtd: settings.allow_dtd,
3245 nodes_limit: settings.nodes_limit,
3246 },
3247 settings.backend,
3248 )
3249 .map_err(XmlDocumentError::Parse)?;
3250 let metrics = validate_document_metrics(&document, settings.depth_limit)?;
3251 Ok((document, metrics))
3252}
3253
3254fn validate_document_metrics(
3255 document: &Document<'_>,
3256 maximum: usize,
3257) -> Result<DocumentMetrics, XmlDocumentError> {
3258 let mut depths = Vec::new();
3262 let mut node_count = 0;
3263 let mut max_depth = 0;
3264 for node in document.descendants() {
3265 node_count += 1;
3266 let parent_depth = node
3267 .parent()
3268 .and_then(|parent| depths.get(parent.id().get_usize()))
3269 .copied()
3270 .unwrap_or(0);
3271 let depth = parent_depth + usize::from(node.is_element());
3272 let node_index = node.id().get_usize();
3273 if depths.len() <= node_index {
3274 depths.resize(node_index + 1, 0);
3275 }
3276 depths[node_index] = depth;
3277 max_depth = max_depth.max(depth);
3278 if depth > maximum {
3279 return Err(XmlDocumentError::DocumentTooDeep {
3280 maximum,
3281 actual: depth,
3282 });
3283 }
3284 }
3285 Ok(DocumentMetrics {
3286 node_count,
3287 max_depth,
3288 })
3289}
3290
3291fn own_bounded_xml(
3292 xml: impl AsRef<str> + Into<String>,
3293 maximum: usize,
3294) -> Result<String, XmlDocumentError> {
3295 let actual = xml.as_ref().len();
3296 if actual > maximum {
3297 return Err(XmlDocumentError::DocumentTooLarge { maximum, actual });
3298 }
3299 Ok(xml.into())
3300}
3301
3302fn decode_owned_xml(
3303 bytes: &[u8],
3304 maximum: usize,
3305 budget: Option<&XmlParseWorkBudget>,
3306) -> Result<String, XmlDocumentError> {
3307 decode_xml_with_budget(bytes, maximum, budget).map(Cow::into_owned)
3308}
3309
3310pub(crate) fn decode_xml_with_budget<'a>(
3311 bytes: &'a [u8],
3312 maximum: usize,
3313 budget: Option<&XmlParseWorkBudget>,
3314) -> Result<Cow<'a, str>, XmlDocumentError> {
3315 if bytes.len() > maximum {
3316 return Err(XmlDocumentError::DocumentTooLarge {
3317 maximum,
3318 actual: bytes.len(),
3319 });
3320 }
3321 charge_parse_work(budget, bytes.len())?;
3325 xml_sec_xml_input::decode_xml_bounded(bytes, None, maximum).map_err(|error| match error {
3326 xml_sec_xml_input::Error::DecodedLimit { actual, .. } => {
3327 XmlDocumentError::DocumentTooLarge { maximum, actual }
3328 }
3329 error => XmlDocumentError::Encoding(error),
3330 })
3331}
3332
3333fn allocate_document_identity(counter: &AtomicU64) -> Result<DocumentIdentity, XmlDocumentError> {
3334 let mut current = counter.load(Ordering::Relaxed);
3335 loop {
3336 let next = current
3337 .checked_add(1)
3338 .ok_or(XmlDocumentError::IdentityExhausted)?;
3339 match counter.compare_exchange_weak(current, next, Ordering::Relaxed, Ordering::Relaxed) {
3340 Ok(_) => return Ok(DocumentIdentity(current)),
3341 Err(observed) => current = observed,
3342 }
3343 }
3344}
3345
3346#[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
3347fn document_requires_internal_dtd(
3348 xml: &str,
3349 settings: DocumentParseSettings,
3350 budget: Option<&XmlParseWorkBudget>,
3351) -> Result<bool, XmlDocumentError> {
3352 if !settings.allow_dtd {
3356 return Ok(false);
3357 }
3358 charge_semantic_parser_work(budget, xml.len())?;
3359 Ok(Document::parse_after_limit_preflight_with_backend(
3360 xml,
3361 ParsingOptions {
3362 allow_dtd: false,
3363 nodes_limit: settings.nodes_limit,
3364 },
3365 settings.backend,
3366 )
3367 .is_err())
3368}
3369
3370fn wrapped_fragment(replacement: &str) -> String {
3371 format!("{VALIDATION_WRAPPER_OPEN}{replacement}{VALIDATION_WRAPPER_CLOSE}")
3372}
3373
3374fn strip_validation_wrapper_namespace_binding(
3375 error: XmlDocumentError,
3376 settings: DocumentParseSettings,
3377) -> XmlDocumentError {
3378 match error {
3379 XmlDocumentError::Parse(ParseError::NamespaceBindingLimitReached { actual, .. }) => {
3380 XmlDocumentError::Parse(ParseError::NamespaceBindingLimitReached {
3382 maximum: settings.namespace_bindings_limit,
3383 actual: actual.saturating_sub(1),
3384 })
3385 }
3386 error => error,
3387 }
3388}
3389
3390fn validation_wrapper<'a, 'input>(
3391 parsed: &'a ParsedDocument<'input>,
3392 expected_range: std::ops::Range<usize>,
3393) -> Result<Node<'a, 'input>, XmlDocumentError> {
3394 parsed
3395 .document
3396 .descendants()
3397 .find(|node| {
3398 node.has_tag_name((VALIDATION_WRAPPER_NS, "wrapper")) && node.range() == expected_range
3399 })
3400 .ok_or_else(|| {
3401 XmlDocumentError::InvalidReplacement(
3402 "replacement escaped its structural validation boundary".into(),
3403 )
3404 })
3405}
3406
3407fn validate_wrappers(
3408 parsed: &ParsedDocument<'_>,
3409 expected_ranges: &[std::ops::Range<usize>],
3410) -> Result<(), XmlDocumentError> {
3411 let mut remaining = expected_ranges
3412 .iter()
3413 .map(|range| (range.start, range.end))
3414 .collect::<HashSet<_>>();
3415 for node in parsed
3416 .document
3417 .descendants()
3418 .filter(|node| node.has_tag_name((VALIDATION_WRAPPER_NS, "wrapper")))
3419 {
3420 let range = node.range();
3421 remaining.remove(&(range.start, range.end));
3422 }
3423 if remaining.is_empty() {
3424 Ok(())
3425 } else {
3426 Err(XmlDocumentError::InvalidReplacement(
3427 "replacement escaped its structural validation boundary".into(),
3428 ))
3429 }
3430}
3431
3432impl DocumentIndexes {
3433 fn build(document: &Document<'_>) -> Self {
3434 let mut order = HashMap::new();
3435 let mut default_ids = HashMap::new();
3436 let mut duplicate_ids = HashSet::new();
3437 let mut attributes_by_value: HashMap<(String, String), Vec<NodeId>> = HashMap::new();
3438
3439 for (position, node) in document.descendants().enumerate() {
3440 order.insert(node.id(), position);
3441 if !node.is_element() {
3442 continue;
3443 }
3444 for attribute in node.attributes() {
3445 attributes_by_value
3446 .entry((attribute.name().to_owned(), attribute.value().to_owned()))
3447 .or_default()
3448 .push(node.id());
3449 if !matches!(attribute.name(), "ID" | "Id" | "id")
3450 || duplicate_ids.contains(attribute.value())
3451 {
3452 continue;
3453 }
3454 match default_ids.entry(attribute.value().to_owned()) {
3455 Entry::Vacant(entry) => {
3456 entry.insert(Some(node.id()));
3457 }
3458 Entry::Occupied(mut entry) if entry.get() != &Some(node.id()) => {
3459 entry.insert(None);
3460 duplicate_ids.insert(attribute.value().to_owned());
3461 }
3462 Entry::Occupied(_) => {}
3463 }
3464 }
3465 }
3466 Self {
3467 order,
3468 default_ids,
3469 attributes_by_value,
3470 }
3471 }
3472}
3473
3474fn element_content_range(
3475 element: &str,
3476) -> Result<(std::ops::Range<usize>, String, bool), XmlDocumentError> {
3477 let start_end = element_opening_end(element)?;
3478 let start_tag = &element[..=start_end];
3479 let qualified_name = start_tag[1..]
3480 .split(|character: char| character.is_ascii_whitespace() || matches!(character, '/' | '>'))
3481 .next()
3482 .filter(|name| !name.is_empty())
3483 .ok_or_else(|| XmlDocumentError::InvalidReplacement("element name is missing".into()))?
3484 .to_owned();
3485 if start_tag[..start_tag.len() - 1].trim_end().ends_with('/') {
3486 return Ok((start_end..start_end, qualified_name, true));
3487 }
3488 let close = element
3489 .rfind("</")
3490 .ok_or_else(|| XmlDocumentError::InvalidReplacement("element end tag is missing".into()))?;
3491 Ok(((start_end + 1)..close, qualified_name, false))
3492}
3493
3494fn element_opening_end(element: &str) -> Result<usize, XmlDocumentError> {
3495 let mut quote = None;
3496 for (index, byte) in element.bytes().enumerate() {
3497 match byte {
3498 b'\'' | b'"' => match quote {
3499 Some(active) if active == byte => quote = None,
3500 None => quote = Some(byte),
3501 Some(_) => {}
3502 },
3503 b'>' if quote.is_none() => return Ok(index),
3504 _ => {}
3505 }
3506 }
3507 Err(XmlDocumentError::InvalidReplacement(
3508 "element start tag is incomplete".into(),
3509 ))
3510}
3511
3512#[cfg(test)]
3513mod tests {
3514 use super::*;
3515
3516 #[test]
3517 #[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
3518 fn namespace_preflight_error_is_a_policy_violation() {
3519 let settings = DocumentParseSettings {
3521 namespace_bindings_limit: 1,
3522 ..Default::default()
3523 };
3524 let error =
3525 preflight_document_limits(r#"<root xmlns:a="urn:a" xmlns:b="urn:b"/>"#, settings, None)
3526 .expect_err("namespace ceiling must be enforced");
3527 assert!(matches!(
3528 error.into_policy_violation(settings),
3529 Ok(crate::policy::PolicyViolation::ResourceLimit {
3530 resource: crate::policy::resource_name::XML_NAMESPACE_BINDINGS,
3531 maximum: 1,
3532 actual: 2,
3533 })
3534 ));
3535 }
3536
3537 #[test]
3538 #[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
3539 fn review_owned_document_enforces_namespace_policy() {
3540 let document = XmlDocument::parse(r#"<root xmlns:a="urn:a" xmlns:b="urn:b"/>"#)
3542 .expect("permissive parse accepts both namespace bindings");
3543 let resources = crate::policy::ResourcePolicy {
3544 max_xml_namespace_bindings: 1,
3545 ..Default::default()
3546 };
3547 assert!(matches!(
3548 document
3549 .validate_operation_policy(&crate::policy::XmlInputPolicy::default(), &resources),
3550 Err(crate::policy::PolicyViolation::ResourceLimit {
3551 resource: crate::policy::resource_name::XML_NAMESPACE_BINDINGS,
3552 actual: 2,
3553 maximum: 1
3554 })
3555 ));
3556 }
3557
3558 #[test]
3559 #[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
3560 fn namespace_policy_tracks_peak_scope_and_mutations() {
3561 let mut document = XmlDocument::parse(
3564 r#"<root><a xmlns:p="urn:p" ID="target"/><b xmlns:q="urn:q"/></root>"#,
3565 )
3566 .expect("disjoint namespace scopes parse");
3567 let resources = crate::policy::ResourcePolicy {
3568 max_xml_namespace_bindings: 1,
3569 ..Default::default()
3570 };
3571 let policy = crate::policy::XmlInputPolicy::default();
3572 document
3573 .validate_operation_policy(&policy, &resources)
3574 .expect("sibling scopes fit a one-binding limit");
3575 let target =
3576 document.with_view(|view| view.node_for_id("target", &[]).expect("target is indexed"));
3577 document
3578 .replace_element(
3579 target,
3580 r#"<a xmlns:p="urn:p" xmlns:q="urn:q" ID="target"/>"#,
3581 )
3582 .expect("replacement fits the original parse settings");
3583 assert!(matches!(
3584 document.validate_operation_policy(&policy, &resources),
3585 Err(crate::policy::PolicyViolation::ResourceLimit { actual: 2, .. })
3586 ));
3587 let target = document.with_view(|view| {
3588 view.node_for_id("target", &[])
3589 .expect("replacement target is indexed")
3590 });
3591 document
3592 .replace_element(target, "<a/>")
3593 .expect("bindings can be removed");
3594 document
3595 .validate_operation_policy(&policy, &resources)
3596 .expect("removed namespace bindings do not remain in the peak metric");
3597 }
3598
3599 #[test]
3600 fn byte_input_decoding_is_identical_for_every_semantic_backend() {
3601 let latin1 = b"<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?><root>caf\xe9</root>";
3604 for backend in XmlBackend::available() {
3605 let document = XmlDocument::parse_bytes_with_backend(latin1, backend)
3606 .expect("declared Latin-1 XML must parse with every backend");
3607 assert!(document.as_xml().contains("encoding=\"UTF-8\""));
3608 document.with_view(|view| {
3609 assert_eq!(view.document().root_element().text(), Some("café"));
3610 });
3611 }
3612 }
3613
3614 #[test]
3615 fn every_semantic_backend_rejects_the_same_encoding_conflict() {
3616 let mut bytes = vec![0xff, 0xfe];
3619 bytes.extend(
3620 "<?xml version=\"1.0\" encoding=\"UTF-16BE\"?><root/>"
3621 .encode_utf16()
3622 .flat_map(u16::to_le_bytes),
3623 );
3624 for backend in XmlBackend::available() {
3625 assert!(matches!(
3626 XmlDocument::parse_bytes_with_backend(&bytes, backend),
3627 Err(XmlDocumentError::Encoding(
3628 xml_sec_xml_input::Error::ConflictingEncoding(_)
3629 ))
3630 ));
3631 }
3632 }
3633
3634 #[cfg(feature = "xmldsig")]
3635 #[test]
3636 fn byte_decoding_work_is_rejected_before_encoding_validation() {
3637 let bytes = [0xff, 0xfe, 0];
3640 let mut policy = crate::policy::VerificationPolicy::default();
3641 policy.resources.max_xml_parse_work_bytes = 0;
3642 let assert_work_error = |result: Result<XmlDocument, XmlDocumentError>| {
3643 assert!(matches!(
3644 result,
3645 Err(XmlDocumentError::Policy(
3646 crate::policy::PolicyViolation::ResourceLimit {
3647 resource: crate::policy::resource_name::XML_PARSE_WORK_BYTES,
3648 maximum: 0,
3649 actual: 3,
3650 }
3651 ))
3652 ));
3653 };
3654 assert_work_error(XmlDocument::parse_bytes_with_policy(&bytes, &policy));
3655 for backend in XmlBackend::available() {
3656 assert_work_error(XmlDocument::parse_bytes_with_policy_and_backend(
3657 &bytes, &policy, backend,
3658 ));
3659 }
3660 }
3661
3662 #[cfg(feature = "xmldsig")]
3663 #[test]
3664 fn byte_decoding_and_parsing_share_one_work_allowance() {
3665 let xml = "<r/>";
3668 let mut bytes = vec![0xff, 0xfe];
3669 bytes.extend(xml.encode_utf16().flat_map(u16::to_le_bytes));
3670 let required = bytes.len() + 3 * xml.len();
3671 for backend in XmlBackend::available() {
3672 let mut policy = crate::policy::VerificationPolicy::default();
3673 policy.resources.max_xml_parse_work_bytes = required - 1;
3674 assert!(matches!(
3675 XmlDocument::parse_bytes_with_policy_and_backend(&bytes, &policy, backend),
3676 Err(XmlDocumentError::Policy(crate::policy::PolicyViolation::ResourceLimit {
3677 resource: crate::policy::resource_name::XML_PARSE_WORK_BYTES,
3678 maximum, actual,
3679 })) if maximum == required - 1 && actual == required
3680 ));
3681 policy.resources.max_xml_parse_work_bytes = required;
3682 assert_eq!(
3683 XmlDocument::parse_bytes_with_policy_and_backend(&bytes, &policy, backend)
3684 .expect("exact aggregate work must suffice")
3685 .as_xml(),
3686 xml
3687 );
3688 }
3689 }
3690
3691 #[test]
3692 fn selected_backend_builds_the_retained_semantic_projection() {
3693 let settings = DocumentParseSettings::default();
3694 let xml =
3695 r#"<root xmlns:p="urn:test"><p:item ID="target"><![CDATA[value]]></p:item></root>"#;
3696 let retained = build_semantic_cell(xml.to_owned(), settings, 1)
3697 .expect("selected backend semantic projection must parse");
3698 retained.with_dependent(|_, parsed| {
3699 assert!(parsed.indexes.default_ids.contains_key("target"));
3700 assert_eq!(
3701 parsed
3702 .document
3703 .root_element()
3704 .first_element_child()
3705 .expect("fixture must contain the indexed child element")
3706 .text(),
3707 Some("value")
3708 );
3709 });
3710 }
3711
3712 #[cfg(feature = "xml-backend-xmloxide")]
3713 #[test]
3714 fn xmloxide_attribute_capacity_is_independent_of_retained_node_limit() {
3715 let xml = r#"<root a="1" b="2" c="3"/>"#;
3719 let settings = DocumentParseSettings::new(false, 2, xml.len());
3720 build_cell(xml.to_owned(), settings, None)
3721 .expect("attributes must not consume the retained node allowance");
3722 }
3723
3724 #[test]
3725 fn node_preflight_rejects_wide_input_before_any_dom_pass() {
3726 let xml = "<root><first/><second/></root>";
3729 let resources = crate::policy::ResourcePolicy {
3730 max_xml_parse_work_bytes: xml.len(),
3731 ..crate::policy::ResourcePolicy::default()
3732 };
3733 let budget = XmlParseWorkBudget::from_resources(&resources);
3734 let settings = DocumentParseSettings::new(false, 3, xml.len());
3735
3736 assert!(matches!(
3737 build_cell(xml.to_owned(), settings, Some(&budget)),
3738 Err(XmlDocumentError::Parse(ParseError::NodesLimitReached))
3739 ));
3740 }
3741
3742 #[test]
3743 fn namespace_predefined_references_do_not_expand_dtd_declarations() {
3744 let mut dtd = InternalDtd::default();
3747 dtd.entities.insert("amp".into(), "&#38;".into());
3748 for value in [
3749 "&", "&", "&", "<", ">", """, "'",
3750 ] {
3751 let mut state = DocumentPreflightState::default();
3752 let budget = XmlParseWorkBudget::with_limit(0);
3753 assert!(
3754 !namespace_value_expands_to_empty(value, &dtd, &mut state, Some(&budget))
3755 .expect("character references require no DTD expansion")
3756 );
3757 assert_eq!(state.entity_expansion_work, 0);
3758 }
3759 }
3760
3761 #[test]
3762 fn namespace_entity_chain_consumes_parse_work_budget() {
3763 let xml = r#"<!DOCTYPE root [<!ENTITY first "&second;"><!ENTITY second "">]><root xmlns="&first;"/>"#;
3766 let resources = crate::policy::ResourcePolicy {
3767 max_xml_parse_work_bytes: xml.len() + "&second;".len() - 1,
3768 ..crate::policy::ResourcePolicy::default()
3769 };
3770 let budget = XmlParseWorkBudget::from_resources(&resources);
3771 let settings = DocumentParseSettings::new(true, 8, xml.len());
3772
3773 assert!(matches!(
3774 preflight_document_limits(xml, settings, Some(&budget)),
3775 Err(XmlDocumentError::Policy(
3776 crate::policy::PolicyViolation::ResourceLimit {
3777 resource: crate::policy::resource_name::XML_PARSE_WORK_BYTES,
3778 ..
3779 }
3780 ))
3781 ));
3782 }
3783
3784 #[test]
3785 fn internal_entity_names_use_xml_whitespace_delimiters_only() {
3786 assert_eq!(
3790 parse_internal_general_entity(" \u{1680} 'value'"),
3791 Some(("\u{1680}", "value"))
3792 );
3793 }
3794
3795 #[test]
3796 fn node_preflight_counts_contiguous_character_data_once() {
3797 let xml = "<root>a&<![CDATA[b]]>c</root>";
3800 let exact_settings = DocumentParseSettings::new(false, 3, xml.len());
3801
3802 build_cell(xml.to_owned(), exact_settings, None)
3803 .expect("the exact retained semantic node count must be accepted");
3804 assert!(matches!(
3805 build_cell(
3806 xml.to_owned(),
3807 DocumentParseSettings::new(false, 2, xml.len()),
3808 None,
3809 ),
3810 Err(XmlDocumentError::Parse(ParseError::NodesLimitReached))
3811 ));
3812 }
3813
3814 #[test]
3815 fn node_preflight_excludes_document_boundary_whitespace() {
3816 let xml = " \n<root/>\n ";
3819 let settings = DocumentParseSettings::new(false, 2, xml.len());
3820
3821 preflight_document_limits(xml, settings, None)
3822 .expect("document plus root element must fit the exact node ceiling");
3823 }
3824
3825 #[test]
3826 fn node_preflight_continues_after_explicit_end_tags() {
3827 let xml = "<root><first></first><second/></root>";
3830 let settings = DocumentParseSettings::new(false, 3, xml.len());
3831
3832 assert!(matches!(
3833 preflight_document_limits(xml, settings, None),
3834 Err(XmlDocumentError::Parse(ParseError::NodesLimitReached))
3835 ));
3836 }
3837
3838 #[test]
3839 fn depth_preflight_expands_nested_internal_entity_markup() {
3840 let xml = r#"<!DOCTYPE root [
3844 <!ENTITY inner "<b><c/></b>">
3845 <!ENTITY deep "<a>&inner;</a>">
3846 ]><root>&deep;</root>"#;
3847 let settings = DocumentParseSettings::new_with_depth(true, 32, 3, xml.len());
3848
3849 assert!(matches!(
3850 preflight_document_limits(xml, settings, None),
3851 Err(XmlDocumentError::DocumentTooDeep {
3852 maximum: 3,
3853 actual: 4,
3854 })
3855 ));
3856
3857 let exact_settings = DocumentParseSettings::new_with_depth(true, 32, 4, xml.len());
3858 preflight_document_limits(xml, exact_settings, None)
3859 .expect("expanded markup at the exact depth boundary must be accepted");
3860 }
3861
3862 #[test]
3863 fn namespace_preflight_counts_bindings_inside_entity_replacements() {
3864 let maximum = crate::hard_limits::XML_NAMESPACE_BINDING_CEILING;
3867 let declarations = (0..=maximum)
3868 .map(|index| format!(r#" xmlns:p{index}="urn:{index}""#))
3869 .collect::<String>();
3870 let xml = format!(
3871 r#"<!DOCTYPE root [<!ENTITY expanded '<child{declarations}/>'>]><root>&expanded;</root>"#
3872 );
3873 let settings = DocumentParseSettings::new_with_depth(true, 16, 4, xml.len());
3874
3875 assert!(matches!(
3876 preflight_document_limits(&xml, settings, None),
3877 Err(XmlDocumentError::Parse(ParseError::NamespaceBindingLimitReached {
3878 maximum: observed_maximum,
3879 actual,
3880 })) if observed_maximum == maximum && actual == maximum + 1
3881 ));
3882 }
3883
3884 #[test]
3885 fn namespace_preflight_accepts_entity_replacement_at_exact_ceiling() {
3886 let maximum = crate::hard_limits::XML_NAMESPACE_BINDING_CEILING;
3887 let declarations = (0..maximum)
3888 .map(|index| format!(r#" xmlns:p{index}="urn:{index}""#))
3889 .collect::<String>();
3890 let xml = format!(
3891 r#"<!DOCTYPE root [<!ENTITY expanded '<child{declarations}/>'>]><root>&expanded;</root>"#
3892 );
3893 let settings = DocumentParseSettings::new_with_depth(true, 16, 4, xml.len());
3894
3895 preflight_document_limits(&xml, settings, None)
3896 .expect("the exact expanded namespace-binding boundary is accepted");
3897 }
3898
3899 #[test]
3900 fn namespace_preflight_expands_undeclarations_before_counting_bindings() {
3901 let maximum = crate::hard_limits::XML_NAMESPACE_BINDING_CEILING;
3904 let declarations = (0..maximum)
3905 .map(|index| format!(r#" xmlns:p{index}="urn:{index}""#))
3906 .collect::<String>();
3907 let xml = format!(
3908 r#"<!DOCTYPE root [<!ENTITY empty ''>]><root{declarations}><child xmlns="∅"/></root>"#
3909 );
3910 let settings = DocumentParseSettings::new_with_depth(true, 16, 4, xml.len());
3911
3912 preflight_document_limits(&xml, settings, None)
3913 .expect("an entity-expanded empty default namespace is an undeclaration");
3914 }
3915
3916 #[test]
3917 fn namespace_preflight_counts_literal_active_bindings() {
3918 let maximum = crate::hard_limits::XML_NAMESPACE_BINDING_CEILING;
3919 let declarations = (0..=maximum)
3920 .map(|index| format!(r#" xmlns:p{index}="urn:{index}""#))
3921 .collect::<String>();
3922 let xml = format!("<root{declarations}/>");
3923 let settings = DocumentParseSettings::new(false, 4, xml.len());
3924
3925 assert!(matches!(
3926 preflight_document_limits(&xml, settings, None),
3927 Err(XmlDocumentError::Parse(ParseError::NamespaceBindingLimitReached {
3928 maximum: observed_maximum,
3929 actual,
3930 })) if observed_maximum == maximum && actual == maximum + 1
3931 ));
3932 }
3933
3934 #[test]
3935 fn namespace_preflight_enforces_compiled_resource_policy() {
3936 let xml = r#"<root xmlns:first="urn:first" xmlns:second="urn:second"/>"#;
3937 let resources = crate::policy::ResourcePolicy {
3938 max_xml_namespace_bindings: 1,
3939 ..crate::policy::ResourcePolicy::default()
3940 };
3941 let settings = DocumentParseSettings::from_policy(
3942 &crate::policy::XmlInputPolicy::default(),
3943 &resources,
3944 );
3945
3946 assert!(matches!(
3947 preflight_document_limits(xml, settings, None),
3948 Err(XmlDocumentError::Parse(
3949 ParseError::NamespaceBindingLimitReached {
3950 maximum: 1,
3951 actual: 2,
3952 }
3953 ))
3954 ));
3955 }
3956
3957 #[test]
3958 fn namespace_shadowing_does_not_inflate_active_binding_count() {
3959 let mut xml = String::new();
3963 for depth in 0..220 {
3964 xml.push_str("<n");
3965 for prefix in 0..5 {
3966 xml.push_str(&format!(r#" xmlns:p{prefix}="urn:{depth}:{prefix}""#));
3967 }
3968 xml.push('>');
3969 }
3970 xml.push_str(&"</n>".repeat(220));
3971 let settings = DocumentParseSettings::new_with_depth(false, 2_000, 256, xml.len());
3972
3973 preflight_document_limits(&xml, settings, None)
3974 .expect("five shadowed prefixes remain five active bindings");
3975 }
3976
3977 #[test]
3978 fn namespace_preflight_releases_distinct_empty_sibling_bindings() {
3979 let maximum = crate::hard_limits::XML_NAMESPACE_BINDING_CEILING;
3983 let siblings = (0..=maximum)
3984 .map(|index| format!(r#"<child xmlns:p{index}="urn:{index}"/>"#))
3985 .collect::<String>();
3986 let xml = format!("<root>{siblings}</root>");
3987 let settings = DocumentParseSettings::new(false, 2_000, xml.len());
3988
3989 preflight_document_limits(&xml, settings, None)
3990 .expect("bindings on completed empty siblings are no longer active");
3991 }
3992
3993 #[test]
3994 fn namespace_preflight_restores_outer_binding_after_shadowing() {
3995 let maximum = crate::hard_limits::XML_NAMESPACE_BINDING_CEILING;
3999 let declarations = (0..maximum)
4000 .map(|index| format!(r#" xmlns:p{index}="urn:outer:{index}""#))
4001 .collect::<String>();
4002 let xml = format!(
4003 r#"<root{declarations}><shadow xmlns:p0="urn:inner"/><overflow xmlns:extra="urn:extra"/></root>"#
4004 );
4005 let settings = DocumentParseSettings::new(false, 2_000, xml.len());
4006
4007 assert!(matches!(
4008 preflight_document_limits(&xml, settings, None),
4009 Err(XmlDocumentError::Parse(ParseError::NamespaceBindingLimitReached {
4010 maximum: observed_maximum,
4011 actual,
4012 })) if observed_maximum == maximum && actual == maximum + 1
4013 ));
4014 }
4015
4016 #[test]
4017 fn depth_preflight_observes_markup_generated_by_a_character_reference() {
4018 let xml = r#"<!DOCTYPE root [
4021 <!ENTITY generated "<a><b/></a>">
4022 ]><root>&generated;</root>"#;
4023 let settings = DocumentParseSettings::new_with_depth(true, 16, 2, xml.len());
4024
4025 assert!(matches!(
4026 preflight_document_limits(xml, settings, None),
4027 Err(XmlDocumentError::DocumentTooDeep {
4028 maximum: 2,
4029 actual: 3,
4030 })
4031 ));
4032 }
4033
4034 #[test]
4035 fn depth_preflight_does_not_stop_at_generated_ampersands() {
4036 for replacement in ["&<a><b/></a>", "&<a><b/>&</a>"] {
4039 let xml = format!(
4040 "<!DOCTYPE root [<!ENTITY generated \"{replacement}\">]><root>&generated;</root>"
4041 );
4042 let settings = DocumentParseSettings::new_with_depth(true, 16, 2, xml.len());
4043 assert!(matches!(
4044 preflight_document_limits(&xml, settings, None),
4045 Err(XmlDocumentError::DocumentTooDeep {
4046 maximum: 2,
4047 actual: 3
4048 })
4049 ));
4050 }
4051 }
4052
4053 #[test]
4054 fn entity_preflight_ignores_declarations_inside_dtd_comments() {
4055 let xml = r#"<!DOCTYPE root [
4058 <!-- <!ENTITY value "<a><b><c/></b></a>"> -->
4059 <!ENTITY value "ok">
4060 ]><root>&value;</root>"#;
4061 let settings = DocumentParseSettings::new_with_depth(true, 8, 1, xml.len());
4062
4063 preflight_document_limits(xml, settings, None)
4064 .expect("comment contents must not participate in entity expansion");
4065 build_cell(xml.to_owned(), settings, None)
4066 .expect("the real declaration contains only character data");
4067 }
4068
4069 #[test]
4070 fn doctype_protected_regions_cannot_hide_document_nodes_from_preflight() {
4071 for protected in ["<!-- [ -->", "<?target [ ?>"] {
4074 let xml = format!("<!DOCTYPE root [{protected}]><root><child/></root>");
4075 let settings = DocumentParseSettings::new_with_depth(true, 1, 4, xml.len());
4076
4077 assert!(matches!(
4078 preflight_document_limits(&xml, settings, None),
4079 Err(XmlDocumentError::Parse(ParseError::NodesLimitReached))
4080 ));
4081 }
4082 }
4083
4084 #[test]
4085 fn entity_preflight_charges_nested_replacement_work() {
4086 let xml = r#"<!DOCTYPE root [
4089 <!ENTITY a "0123456789">
4090 <!ENTITY b "&a;&a;&a;&a;">
4091 ]><root>&b;</root>"#;
4092 let first_replacement = "&a;&a;&a;&a;".len();
4093 let nested_replacement = "0123456789".len();
4094 let maximum = xml.len() + first_replacement + nested_replacement - 1;
4095 let budget = XmlParseWorkBudget::with_limit(maximum);
4096 let settings = DocumentParseSettings::new_with_depth(true, 32, 4, xml.len());
4097
4098 assert!(matches!(
4099 build_cell(xml.to_owned(), settings, Some(&budget)),
4100 Err(XmlDocumentError::Policy(crate::policy::PolicyViolation::ResourceLimit {
4101 resource: crate::policy::resource_name::XML_PARSE_WORK_BYTES,
4102 maximum: observed_maximum,
4103 actual,
4104 })) if observed_maximum == maximum
4105 && actual == xml.len() + first_replacement + nested_replacement
4106 ));
4107 }
4108
4109 #[test]
4110 fn entity_preflight_charges_a_reference_generated_by_a_character_reference() {
4111 let xml = r#"<!DOCTYPE root [
4114 <!ENTITY nested "0123456789">
4115 <!ENTITY generated "&nested;">
4116 ]><root>&generated;</root>"#;
4117 let first_replacement = "&nested;".len();
4118 let nested_replacement = "0123456789".len();
4119 let maximum = xml.len() + first_replacement + nested_replacement - 1;
4120 let budget = XmlParseWorkBudget::with_limit(maximum);
4121 let settings = DocumentParseSettings::new_with_depth(true, 16, 2, xml.len());
4122
4123 assert!(matches!(
4124 preflight_document_limits(xml, settings, Some(&budget)),
4125 Err(XmlDocumentError::Policy(crate::policy::PolicyViolation::ResourceLimit {
4126 resource: crate::policy::resource_name::XML_PARSE_WORK_BYTES,
4127 maximum: observed_maximum,
4128 actual,
4129 })) if observed_maximum == maximum
4130 && actual == xml.len() + first_replacement + nested_replacement
4131 ));
4132 }
4133
4134 #[test]
4135 fn entity_preflight_charges_nested_attribute_replacements() {
4136 let xml = r#"<!DOCTYPE root [
4139 <!ENTITY a "0123456789">
4140 <!ENTITY b "&a;&a;&a;&a;">
4141 ]><root value="&b;"/>"#;
4142 let first_replacement = "&a;&a;&a;&a;".len();
4143 let nested_replacement = "0123456789".len();
4144 let expected_work = xml.len() + first_replacement + 4 * nested_replacement;
4145 let budget = XmlParseWorkBudget::with_limit(expected_work);
4146 let settings = DocumentParseSettings::new_with_depth(true, 8, 1, xml.len());
4147
4148 preflight_document_limits(xml, settings, Some(&budget))
4149 .expect("the exact repeated attribute expansion budget must be accepted");
4150 assert_eq!(budget.consumed(), expected_work);
4151
4152 let maximum = expected_work - 1;
4153 let budget = XmlParseWorkBudget::with_limit(maximum);
4154
4155 assert!(matches!(
4156 preflight_document_limits(xml, settings, Some(&budget)),
4157 Err(XmlDocumentError::Policy(crate::policy::PolicyViolation::ResourceLimit {
4158 resource: crate::policy::resource_name::XML_PARSE_WORK_BYTES,
4159 maximum: observed_maximum,
4160 actual,
4161 })) if observed_maximum == maximum
4162 && actual == expected_work
4163 ));
4164 }
4165
4166 #[test]
4167 fn attribute_preflight_exempts_normatively_redeclared_predefined_entities() {
4168 let references = "&".repeat(
4172 usize::try_from(crate::hard_limits::XML_ENTITY_EXPANSION_CEILING)
4173 .expect("entity ceiling fits usize")
4174 + 1,
4175 );
4176 let xml =
4177 format!(r#"<!DOCTYPE root [<!ENTITY amp "&#38;">]><root value="{references}"/>"#);
4178 let settings = DocumentParseSettings::new_with_depth(true, 8, 1, xml.len());
4179 let budget = XmlParseWorkBudget::with_limit(xml.len());
4180
4181 preflight_document_limits(&xml, settings, Some(&budget))
4182 .expect("predefined attribute references do not consume entity expansion budget");
4183 assert_eq!(budget.consumed(), xml.len());
4184 }
4185
4186 #[test]
4187 fn entity_preflight_streams_dense_attribute_references_in_source_order() {
4188 let repeated = "&first;".repeat(4_096);
4192 let xml = format!(
4193 "<!DOCTYPE root [<!ENTITY first \"0123456789\"><!ENTITY last \"01234567890123456789\">]><root value=\"{repeated}&last;\"/>"
4194 );
4195 let maximum = xml.len() + 9;
4196 let budget = XmlParseWorkBudget::with_limit(maximum);
4197 let settings = DocumentParseSettings::new_with_depth(true, 8, 1, xml.len());
4198
4199 let mut scanner = general_references(b"&first;&last;");
4200 assert_eq!(scanner.next(), Some("first"));
4201 assert_eq!(scanner.next(), Some("last"));
4202 assert_eq!(scanner.next(), None);
4203
4204 assert!(matches!(
4205 preflight_document_limits(&xml, settings, Some(&budget)),
4206 Err(XmlDocumentError::Policy(crate::policy::PolicyViolation::ResourceLimit {
4207 resource: crate::policy::resource_name::XML_PARSE_WORK_BYTES,
4208 maximum: observed_maximum,
4209 actual,
4210 })) if observed_maximum == maximum && actual == xml.len() + 10
4211 ));
4212 }
4213
4214 #[test]
4215 fn entity_preflight_charges_applicable_dtd_attribute_defaults() {
4216 let omitted = r#"<!DOCTYPE root [
4220 <!ENTITY a "0123456789">
4221 <!ENTITY b "&a;&a;&a;&a;">
4222 <!ATTLIST root value CDATA "&b;">
4223 ]><root/>"#;
4224 let first_replacement = "&a;&a;&a;&a;".len();
4225 let nested_replacement = "0123456789".len();
4226 let expected_work = omitted.len() + first_replacement + 4 * nested_replacement;
4227 let settings = DocumentParseSettings::new_with_depth(true, 8, 1, omitted.len());
4228 let exact_budget = XmlParseWorkBudget::with_limit(expected_work);
4229
4230 preflight_document_limits(omitted, settings, Some(&exact_budget))
4231 .expect("the exact default-attribute expansion budget must be accepted");
4232 assert_eq!(exact_budget.consumed(), expected_work);
4233
4234 let maximum = expected_work - 1;
4235 let short_budget = XmlParseWorkBudget::with_limit(maximum);
4236 assert!(matches!(
4237 preflight_document_limits(omitted, settings, Some(&short_budget)),
4238 Err(XmlDocumentError::Policy(crate::policy::PolicyViolation::ResourceLimit {
4239 resource: crate::policy::resource_name::XML_PARSE_WORK_BYTES,
4240 maximum: observed_maximum,
4241 actual,
4242 })) if observed_maximum == maximum && actual == expected_work
4243 ));
4244
4245 let overridden = omitted.replace("<root/>", "<root value=\"literal\"/>");
4246 let source_only_budget = XmlParseWorkBudget::with_limit(overridden.len());
4247 let overridden_settings =
4248 DocumentParseSettings::new_with_depth(true, 8, 1, overridden.len());
4249 preflight_document_limits(&overridden, overridden_settings, Some(&source_only_budget))
4250 .expect("an explicit source attribute must suppress its DTD default");
4251 assert_eq!(source_only_budget.consumed(), overridden.len());
4252 }
4253
4254 #[test]
4255 fn entity_preflight_uses_bounded_heap_for_long_acyclic_chains() {
4256 const ENTITY_COUNT: usize = 4_096;
4259 let mut xml = String::from("<!DOCTYPE root [\n<!ENTITY e0 \"value\">\n");
4260 for index in 1..ENTITY_COUNT {
4261 use std::fmt::Write as _;
4262 writeln!(xml, "<!ENTITY e{index} \"&e{};\">", index - 1)
4263 .expect("write entity declaration");
4264 }
4265 use std::fmt::Write as _;
4266 write!(xml, "]><root>&e{};</root>", ENTITY_COUNT - 1).expect("write root reference");
4267 let settings = DocumentParseSettings::new_with_depth(true, 16, 1, xml.len());
4268
4269 preflight_document_limits(&xml, settings, None)
4270 .expect("long acyclic entity traversal must use bounded native stack");
4271 }
4272
4273 #[test]
4274 fn owned_parse_preflights_depth_before_dtd_provenance() {
4275 let xml = "<root><child><leaf/></child></root>";
4278 let settings = DocumentParseSettings::new_with_depth(true, 16, 2, xml.len());
4279 let budget = XmlParseWorkBudget::with_limit(xml.len());
4280
4281 assert!(matches!(
4282 XmlDocument::parse_with_settings_and_optional_budget(
4283 xml.to_owned(),
4284 settings,
4285 Some(&budget),
4286 ),
4287 Err(XmlDocumentError::DocumentTooDeep {
4288 maximum: 2,
4289 actual: 3,
4290 })
4291 ));
4292 }
4293
4294 fn nested_document(depth: usize) -> String {
4295 let mut xml = "<node>".repeat(depth);
4296 xml.push_str(&"</node>".repeat(depth));
4297 xml
4298 }
4299
4300 #[test]
4301 fn selected_backend_enforces_exact_depth_boundary() {
4302 let settings = DocumentParseSettings::new_with_depth(false, 128, 2, 4_096);
4303 let accepted = nested_document(2);
4304 build_semantic_cell(accepted, settings, 0).expect("exact depth must parse");
4305
4306 let rejected = nested_document(3);
4307 assert!(matches!(
4308 build_semantic_cell(rejected, settings, 0),
4309 Err(XmlDocumentError::DocumentTooDeep {
4310 maximum: 2,
4311 actual: 3,
4312 })
4313 ));
4314 }
4315
4316 #[cfg(feature = "xml-backend-xmloxide")]
4317 #[test]
4318 fn xmloxide_backend_enforces_exact_depth_boundary() {
4319 let settings = DocumentParseSettings::new_with_depth(false, 128, 2, 4_096);
4322 let accepted = nested_document(2);
4323 build_cell(accepted, settings, None).expect("exact depth must parse");
4324
4325 let rejected = nested_document(3);
4326 assert!(matches!(
4327 build_cell(rejected, settings, None),
4328 Err(XmlDocumentError::DocumentTooDeep {
4329 maximum: 2,
4330 actual: 3,
4331 })
4332 ));
4333 }
4334
4335 struct OversizedBorrowedInput<'a>(&'a str);
4336
4337 impl AsRef<str> for OversizedBorrowedInput<'_> {
4338 fn as_ref(&self) -> &str {
4339 self.0
4340 }
4341 }
4342
4343 impl From<OversizedBorrowedInput<'_>> for String {
4344 fn from(_: OversizedBorrowedInput<'_>) -> Self {
4345 panic!("oversized borrowed XML must be rejected before ownership conversion")
4346 }
4347 }
4348
4349 #[test]
4350 fn oversized_borrowed_input_is_rejected_before_ownership_conversion() {
4351 let oversized = "x".repeat(crate::hard_limits::XML_DOCUMENT_BYTE_CEILING + 1);
4354 assert!(matches!(
4355 XmlDocument::parse(OversizedBorrowedInput(&oversized)),
4356 Err(XmlDocumentError::DocumentTooLarge { .. })
4357 ));
4358
4359 #[cfg(feature = "xmldsig")]
4360 {
4361 let mut policy = crate::policy::SigningPolicy::default();
4362 policy.resources.max_xml_document_bytes = 8;
4363 assert!(matches!(
4364 XmlDocument::parse_with_policy(OversizedBorrowedInput("<root/>xx"), &policy),
4365 Err(XmlDocumentError::DocumentTooLarge {
4366 maximum: 8,
4367 actual: 9,
4368 })
4369 ));
4370 }
4371 }
4372
4373 #[test]
4374 fn views_reuse_identity_until_mutation_invalidates_generation() {
4375 let mut document =
4376 XmlDocument::parse("<root><child ID=\"target\"/></root>").expect("fixture must parse");
4377 let target = document.with_view(|view| {
4378 view.node_for_id("target", &[])
4379 .expect("ID target must resolve")
4380 });
4381 assert_eq!(document.generation(), 0);
4382 document
4383 .replace_element(target, "<replacement ID=\"target\"/>")
4384 .expect("replacement must succeed");
4385
4386 let error = document
4387 .with_view(|view| view.resolve_node(target).map(|_| ()))
4388 .expect_err("old identity must be stale");
4389 assert!(matches!(error, XmlDocumentError::StaleIdentity { .. }));
4390 assert_eq!(document.generation(), 1);
4391 }
4392
4393 #[test]
4394 fn identities_cannot_cross_documents() {
4395 let first = XmlDocument::parse("<root/>").expect("fixture must parse");
4396 let second = XmlDocument::parse("<root/>").expect("fixture must parse");
4397 let root = first.with_view(|view| view.root_element());
4398
4399 let error = second
4400 .with_view(|view| view.resolve_node(root).map(|_| ()))
4401 .expect_err("foreign identity must fail closed");
4402 assert!(matches!(error, XmlDocumentError::ForeignIdentity));
4403 }
4404
4405 #[test]
4406 fn child_append_projection_matches_both_element_forms() {
4407 for xml in ["<root></root>", "<root/>"] {
4410 let document = XmlDocument::parse(xml).expect("fixture must parse");
4411 let root = document.with_view(|view| view.root_element());
4412 let projected = document
4413 .projected_child_append_len(root, "<child/>".len())
4414 .expect("append length must project");
4415
4416 assert_eq!(projected, "<root><child/></root>".len());
4417 }
4418 }
4419
4420 #[test]
4421 fn document_identity_allocation_reports_exhaustion_without_wrapping() {
4422 let counter = AtomicU64::new(u64::MAX - 1);
4423
4424 assert_eq!(
4425 allocate_document_identity(&counter).expect("last identity must be allocated"),
4426 DocumentIdentity(u64::MAX - 1)
4427 );
4428 assert!(matches!(
4429 allocate_document_identity(&counter),
4430 Err(XmlDocumentError::IdentityExhausted)
4431 ));
4432 assert_eq!(counter.load(Ordering::Relaxed), u64::MAX);
4433 }
4434
4435 #[test]
4436 fn duplicate_and_caller_registered_ids_share_one_index() {
4437 let document = XmlDocument::parse(
4438 "<root><a ID=\"duplicate\" custom=\"selected\"/><b Id=\"duplicate\"/></root>",
4439 )
4440 .expect("fixture must parse");
4441 let registrations = [IdAttributeRegistration::global("custom")];
4442
4443 document.with_view(|view| {
4444 assert!(view.node_for_id("duplicate", &[]).is_none());
4445 assert!(view.node_for_id("selected", ®istrations).is_some());
4446 });
4447 }
4448
4449 #[test]
4450 fn duplicate_default_id_stays_ambiguous_with_caller_registration() {
4451 let document = XmlDocument::parse(
4454 "<root><a ID=\"duplicate\" custom=\"duplicate\"/><b Id=\"duplicate\"/></root>",
4455 )
4456 .expect("fixture must parse");
4457 let registrations = [IdAttributeRegistration::global("custom")];
4458
4459 document.with_view(|view| {
4460 assert!(view.node_for_id("duplicate", ®istrations).is_none());
4461 });
4462 }
4463
4464 #[test]
4465 fn content_replacement_uses_parent_namespace_context() {
4466 let mut document = XmlDocument::parse("<p:root xmlns:p=\"urn:test\"><p:old/></p:root>")
4467 .expect("fixture must parse");
4468 let root = document.with_view(|view| view.root_element());
4469 document
4470 .replace_content(root, "<p:new/>")
4471 .expect("in-scope prefix must be accepted");
4472 assert_eq!(
4473 document.as_xml(),
4474 "<p:root xmlns:p=\"urn:test\"><p:new/></p:root>"
4475 );
4476 }
4477
4478 #[cfg(feature = "xml-backend-xmloxide")]
4479 #[test]
4480 fn folded_character_data_replacement_splices_its_complete_source_range() {
4481 let mut document = XmlDocument::parse("<r>one<![CDATA[+]]>two</r>")
4484 .expect("mixed character data fixture must parse");
4485 let target = document.with_view(|view| {
4486 let text = view
4487 .document()
4488 .root_element()
4489 .first_child()
4490 .expect("fixture must contain a text node");
4491 assert_eq!(text.text(), Some("one+two"));
4492 view.node_identity(text)
4493 });
4494
4495 document
4496 .replace_node_with_fragment(target, "replacement")
4497 .expect("the complete folded text node must be replaceable");
4498
4499 assert_eq!(document.as_xml(), "<r>replacement</r>");
4500 }
4501
4502 #[test]
4503 fn validation_wrapper_does_not_consume_document_depth() {
4504 let mut document = XmlDocument::parse_with_settings(
4507 "<root><target/></root>".into(),
4508 DocumentParseSettings::new_with_depth(false, 128, 2, 4_096),
4509 )
4510 .expect("fixture must fit the exact depth ceiling");
4511 let target = document.with_view(|view| {
4512 view.node_identity(
4513 view.document()
4514 .descendants()
4515 .find(|node| node.has_tag_name("target"))
4516 .expect("target must exist"),
4517 )
4518 });
4519
4520 document
4521 .replace_content(target, "text")
4522 .expect("validation-only wrapper must not consume caller depth");
4523 assert_eq!(document.as_xml(), "<root><target>text</target></root>");
4524 }
4525
4526 #[test]
4527 fn validation_wrapper_does_not_consume_namespace_allowance() {
4528 for batch in [false, true] {
4530 let mut document = XmlDocument::parse_with_settings(
4531 "<root/>".into(),
4532 DocumentParseSettings {
4533 namespace_bindings_limit: 0,
4534 ..DocumentParseSettings::default()
4535 },
4536 )
4537 .expect("namespace-free input");
4538 let root = document.with_view(|view| view.root_element());
4539 if batch {
4540 document
4541 .replace_contents(&[(root, "text".into())])
4542 .expect("namespace-free batch");
4543 } else {
4544 document
4545 .replace_content(root, "text")
4546 .expect("namespace-free replacement");
4547 }
4548 assert_eq!(document.as_xml(), "<root>text</root>");
4549 let root = document.with_view(|view| view.root_element());
4550 assert!(
4551 document
4552 .replace_content(root, "<p:e xmlns:p='urn:p'/>")
4553 .is_err()
4554 );
4555 assert_eq!(document.as_xml(), "<root>text</root>");
4556 }
4557 }
4558
4559 #[test]
4560 #[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
4561 fn validation_wrapper_namespace_error_reports_document_limit() {
4562 let settings = DocumentParseSettings {
4564 namespace_bindings_limit: 1,
4565 ..DocumentParseSettings::default()
4566 };
4567 for batch in [false, true] {
4568 let mut document = XmlDocument::parse_with_settings("<root/>".into(), settings)
4569 .expect("namespace-free input");
4570 let root = document.with_view(|view| view.root_element());
4571 let replacement = "<child xmlns:a='urn:a' xmlns:b='urn:b'/>";
4572 let error = if batch {
4573 document.replace_contents(&[(root, replacement.into())])
4574 } else {
4575 document.replace_content(root, replacement)
4576 }
4577 .expect_err("two bindings exceed the configured ceiling");
4578 assert!(matches!(
4579 error.into_policy_violation(settings),
4580 Ok(crate::policy::PolicyViolation::ResourceLimit {
4581 resource: crate::policy::resource_name::XML_NAMESPACE_BINDINGS,
4582 maximum: 1,
4583 actual: 2,
4584 })
4585 ));
4586 assert_eq!(document.as_xml(), "<root/>");
4587 assert_eq!(document.generation(), 0);
4588 }
4589 }
4590
4591 #[test]
4592 fn validation_wrapper_preserves_real_depth_rejection() {
4593 let mut document = XmlDocument::parse_with_settings(
4596 "<root><target/></root>".into(),
4597 DocumentParseSettings::new_with_depth(false, 128, 2, 4_096),
4598 )
4599 .expect("fixture must fit the exact depth ceiling");
4600 let target = document.with_view(|view| {
4601 view.node_identity(
4602 view.document()
4603 .descendants()
4604 .find(|node| node.has_tag_name("target"))
4605 .expect("target must exist"),
4606 )
4607 });
4608 let before = document.as_xml().to_owned();
4609
4610 let error = document
4611 .replace_content(target, "<child/>")
4612 .expect_err("real replacement depth must remain bounded");
4613 assert!(
4614 matches!(
4615 error,
4616 XmlDocumentError::DocumentTooDeep {
4617 maximum: 2,
4618 actual: 3,
4619 }
4620 ),
4621 "unexpected error: {error:?}"
4622 );
4623 assert_eq!(document.as_xml(), before);
4624 assert_eq!(document.generation(), 0);
4625 }
4626
4627 #[test]
4628 fn batched_validation_wrappers_do_not_consume_document_depth() {
4629 let mut document = XmlDocument::parse_with_settings(
4632 "<root><first/><second/></root>".into(),
4633 DocumentParseSettings::new_with_depth(false, 128, 2, 4_096),
4634 )
4635 .expect("fixture must fit the exact depth ceiling");
4636 let (first, second) = document.with_view(|view| {
4637 let mut targets = view
4638 .document()
4639 .descendants()
4640 .filter(|node| matches!(node.tag_name().name(), "first" | "second"));
4641 (
4642 view.node_identity(targets.next().expect("first target")),
4643 view.node_identity(targets.next().expect("second target")),
4644 )
4645 });
4646
4647 document
4648 .replace_contents(&[(first, "a".into()), (second, "b".into())])
4649 .expect("validation-only wrappers must not consume caller depth");
4650 assert_eq!(
4651 document.as_xml(),
4652 "<root><first>a</first><second>b</second></root>"
4653 );
4654 }
4655
4656 #[test]
4657 fn mutations_ignore_greater_than_inside_quoted_attributes() {
4658 let mut document =
4661 XmlDocument::parse("<root marker=\">\"><old/></root>").expect("fixture must parse");
4662 let root = document.with_view(|view| view.root_element());
4663
4664 document
4665 .replace_content(root, "<first/>")
4666 .expect("quoted greater-than must not corrupt content replacement");
4667 let root = document.with_view(|view| view.root_element());
4668 document
4669 .append_child(root, "<second/>")
4670 .expect("quoted greater-than must not corrupt child append");
4671
4672 assert_eq!(
4673 document.as_xml(),
4674 "<root marker=\">\"><first/><second/></root>"
4675 );
4676 }
4677
4678 #[test]
4679 fn fragment_validation_reuses_internal_dtd_entities() {
4680 let mut document = XmlDocument::parse_with_settings(
4683 "<!DOCTYPE root [<!ENTITY custom \"replacement\">]><root><old/></root>".into(),
4684 DocumentParseSettings::new(
4685 true,
4686 crate::hard_limits::XML_DOCUMENT_NODE_CEILING,
4687 crate::hard_limits::XML_DOCUMENT_BYTE_CEILING,
4688 ),
4689 )
4690 .expect("DTD fixture must parse when explicitly enabled");
4691 let root = document.with_view(|view| view.root_element());
4692
4693 document
4694 .replace_content(root, "&custom;")
4695 .expect("declared entity must remain valid in replacement context");
4696 assert_eq!(
4697 document.as_xml(),
4698 "<!DOCTYPE root [<!ENTITY custom \"replacement\">]><root>&custom;</root>"
4699 );
4700 }
4701
4702 #[test]
4703 fn serialization_is_stable_for_unchanged_views() {
4704 let document = XmlDocument::parse("<?pi value?><root a=\"1\"><!--c--></root>")
4705 .expect("fixture must parse");
4706 let first = document.as_xml().to_owned();
4707 document.with_view(|view| assert_eq!(view.xml(), first));
4708 document.with_view(|view| assert_eq!(view.xml(), first));
4709 assert_eq!(document.as_xml(), first);
4710 }
4711
4712 #[test]
4713 fn rejected_mutation_keeps_source_and_generation_unchanged() {
4714 let mut document = XmlDocument::parse("<root><child/></root>").expect("fixture must parse");
4717 let root = document.with_view(|view| view.root_element());
4718 let before = document.as_xml().to_owned();
4719
4720 assert!(document.replace_content(root, "<unclosed>").is_err());
4721 assert_eq!(document.as_xml(), before);
4722 assert_eq!(document.generation(), 0);
4723 }
4724
4725 #[test]
4726 fn oversized_invalid_fragment_fails_at_the_document_byte_boundary() {
4727 for xml in ["<root><child/></root>", "<root/>"] {
4731 let mut document = XmlDocument::parse_with_settings(
4732 xml.into(),
4733 DocumentParseSettings::new(false, 64, 64),
4734 )
4735 .expect("bounded fixture must parse");
4736 let root = document.with_view(|view| view.root_element());
4737 let replacement = "<".repeat(1_024);
4738
4739 assert!(matches!(
4740 document.replace_content(root, &replacement),
4741 Err(XmlDocumentError::DocumentTooLarge {
4742 maximum: 64,
4743 actual,
4744 }) if actual > 64
4745 ));
4746 assert_eq!(document.as_xml(), xml);
4747 assert_eq!(document.generation(), 0);
4748 }
4749 }
4750
4751 #[cfg(feature = "xmlenc")]
4752 #[test]
4753 fn bounded_fragment_validation_uses_the_active_node_ceiling() {
4754 let document = XmlDocument::parse_with_settings(
4757 "<root><target/></root>".into(),
4758 DocumentParseSettings::new(false, 128, 4_096),
4759 )
4760 .expect("fixture must parse");
4761 let target = document.with_view(|view| {
4762 let target = view
4763 .document()
4764 .descendants()
4765 .find(|node| node.has_tag_name("target"))
4766 .expect("target must exist");
4767 view.node_identity(target)
4768 });
4769 let maximum = document.with_view(|view| view.node_count());
4770 let before = document.as_xml().to_owned();
4771 let budget = XmlParseWorkBudget::from_resources(&crate::policy::ResourcePolicy::default());
4772
4773 let replacement = document.prepare_node_fragment_replacement_with_budget(
4774 target,
4775 "<replacement><child/><child/><malformed>",
4776 DocumentParseSettings::new(false, maximum as u32, 4_096),
4777 &budget,
4778 );
4779 assert!(matches!(
4780 replacement,
4781 Err(XmlDocumentError::ProjectedNodeLimit { maximum: rejected })
4782 if rejected == maximum
4783 ));
4784 assert_eq!(document.as_xml(), before);
4785 assert_eq!(document.generation(), 0);
4786 }
4787
4788 #[test]
4789 fn duplicate_empty_content_targets_are_rejected_atomically() {
4790 let mut document =
4791 XmlDocument::parse("<root><target></target></root>").expect("fixture must parse");
4792 let target = document.with_view(|view| {
4793 let target = view
4794 .document()
4795 .descendants()
4796 .find(|node| node.has_tag_name("target"))
4797 .expect("target must exist");
4798 view.node_identity(target)
4799 });
4800
4801 let error = document
4802 .replace_contents(&[(target, "first".into()), (target, "second".into())])
4803 .expect_err("one identity cannot be replaced twice");
4804
4805 assert!(matches!(error, XmlDocumentError::InvalidReplacement(_)));
4806 assert_eq!(document.as_xml(), "<root><target></target></root>");
4807 assert_eq!(document.generation(), 0);
4808 }
4809
4810 #[cfg(feature = "xmldsig")]
4811 #[test]
4812 fn batched_content_replacements_obey_the_active_byte_ceiling() {
4813 let mut document = XmlDocument::parse_with_settings(
4816 "<root><first/><second/></root>".into(),
4817 DocumentParseSettings::new(false, 128, 4_096),
4818 )
4819 .expect("fixture must parse");
4820 let (first, second) = document.with_view(|view| {
4821 let mut targets = view
4822 .document()
4823 .descendants()
4824 .filter(|node| matches!(node.tag_name().name(), "first" | "second"));
4825 (
4826 view.node_identity(targets.next().expect("first target")),
4827 view.node_identity(targets.next().expect("second target")),
4828 )
4829 });
4830 let before = document.as_xml().to_owned();
4831 let replacements = [(first, "alpha".into()), (second, "beta".into())];
4832 let expected = "<root><first>alpha</first><second>beta</second></root>";
4833 let maximum = expected.len() - 1;
4834 let budget = XmlParseWorkBudget::from_resources(&crate::policy::ResourcePolicy::default());
4835
4836 let error = document
4837 .replace_contents_with_budget(
4838 &replacements,
4839 DocumentParseSettings::new(false, 128, maximum),
4840 &budget,
4841 )
4842 .expect_err("the active byte ceiling must reject the complete batch");
4843
4844 assert!(matches!(
4845 error,
4846 XmlDocumentError::DocumentTooLarge {
4847 maximum: observed_maximum,
4848 actual,
4849 } if observed_maximum == maximum && actual == expected.len()
4850 ));
4851 assert_eq!(document.as_xml(), before);
4852 assert_eq!(document.generation(), 0);
4853 }
4854
4855 #[test]
4856 fn batched_content_replacements_validate_one_namespaced_candidate() {
4857 let mut document = XmlDocument::parse(
4860 r#"<root xmlns:p="urn:test"><first/><second><old/></second></root>"#,
4861 )
4862 .expect("fixture must parse");
4863 let (first, second) = document.with_view(|view| {
4864 let mut targets = view
4865 .document()
4866 .descendants()
4867 .filter(|node| matches!(node.tag_name().name(), "first" | "second"));
4868 (
4869 view.node_identity(targets.next().expect("first target")),
4870 view.node_identity(targets.next().expect("second target")),
4871 )
4872 });
4873
4874 document
4875 .replace_contents(&[
4876 (first, "<p:alpha/>".into()),
4877 (second, "text<p:beta/>".into()),
4878 ])
4879 .expect("all fragments must validate in one candidate");
4880
4881 assert_eq!(
4882 document.as_xml(),
4883 r#"<root xmlns:p="urn:test"><first><p:alpha/></first><second>text<p:beta/></second></root>"#
4884 );
4885 assert_eq!(document.generation(), 1);
4886 }
4887
4888 #[cfg(feature = "xmldsig")]
4889 #[test]
4890 fn batched_content_replacements_charge_two_combined_parses() {
4891 const TARGETS: usize = 64;
4895 let source = format!("<root>{}</root>", "<item>old</item>".repeat(TARGETS));
4896 let mut document = XmlDocument::parse(&source).expect("fixture must parse");
4897 let replacements = document.with_view(|view| {
4898 view.document()
4899 .descendants()
4900 .filter(|node| node.has_tag_name("item"))
4901 .map(|node| (view.node_identity(node), "x".to_owned()))
4902 .collect::<Vec<_>>()
4903 });
4904 let committed_len = source.len() - TARGETS * "old".len() + TARGETS;
4905 let validation_len =
4906 source.len() - TARGETS * "old".len() + TARGETS * wrapped_fragment("x").len();
4907 let parser_passes = selected_parser_passes();
4908 let maximum = (validation_len + committed_len) * parser_passes;
4909 let budget = XmlParseWorkBudget::with_limit(maximum);
4910
4911 document
4912 .replace_contents_with_budget(&replacements, DocumentParseSettings::default(), &budget)
4913 .expect("the batch must fit its two combined parser passes exactly");
4914
4915 assert_eq!(budget.consumed(), maximum);
4916 assert_eq!(document.as_xml().matches("<item>x</item>").count(), TARGETS);
4917 assert_eq!(document.generation(), 1);
4918 }
4919
4920 #[test]
4921 fn parser_work_accounting_follows_the_runtime_backend() {
4922 let xml = "<root><value/></root>";
4925 for backend in [
4926 XmlBackend::Xmloxide,
4927 XmlBackend::Roxmltree,
4928 XmlBackend::Differential,
4929 ]
4930 .into_iter()
4931 .filter(|backend| backend.is_available())
4932 {
4933 let expected = xml.len() * 3;
4934 let budget = XmlParseWorkBudget::with_limit(expected);
4935 parse_borrowed_with_settings_and_budget(
4936 xml,
4937 DocumentParseSettings::default().with_backend(backend),
4938 Some(&budget),
4939 )
4940 .expect("the exact runtime parser-work allowance must fit");
4941 assert_eq!(budget.consumed(), expected, "{backend:?}");
4942 }
4943 }
4944
4945 #[test]
4946 fn batched_content_replacements_reject_one_escaped_boundary_atomically() {
4947 let mut document = XmlDocument::parse("<root><first></first><second></second></root>")
4950 .expect("fixture must parse");
4951 let (first, second) = document.with_view(|view| {
4952 let mut targets = view
4953 .document()
4954 .descendants()
4955 .filter(|node| matches!(node.tag_name().name(), "first" | "second"));
4956 (
4957 view.node_identity(targets.next().expect("first target")),
4958 view.node_identity(targets.next().expect("second target")),
4959 )
4960 });
4961 let before = document.as_xml().to_owned();
4962
4963 document
4964 .replace_contents(&[
4965 (first, "</first><attacker/><first>".into()),
4966 (second, "safe".into()),
4967 ])
4968 .expect_err("a fragment must not escape its target boundary");
4969
4970 assert_eq!(document.as_xml(), before);
4971 assert_eq!(document.generation(), 0);
4972 }
4973
4974 #[cfg(feature = "xmldsig")]
4975 #[test]
4976 fn retained_mutations_share_one_sticky_parse_work_budget() {
4977 let mut document =
4981 XmlDocument::parse("<root><first/><second/></root>").expect("fixture must parse");
4982 let first = document.with_view(|view| {
4983 view.node_identity(
4984 view.document()
4985 .descendants()
4986 .find(|node| node.has_tag_name("first"))
4987 .expect("first target"),
4988 )
4989 });
4990 let first_output = "<root><first>a</first><second/></root>";
4991 let first_validation_len = first_output
4992 .len()
4993 .checked_add(VALIDATION_WRAPPER_OPEN.len())
4994 .and_then(|length| length.checked_add(VALIDATION_WRAPPER_CLOSE.len()))
4995 .expect("fixture length must fit");
4996 let parser_passes = selected_parser_passes();
4997 let maximum = (first_validation_len + first_output.len()) * parser_passes;
4998 let budget = XmlParseWorkBudget::with_limit(maximum);
4999
5000 document
5001 .replace_contents_with_budget(
5002 &[(first, "a".into())],
5003 DocumentParseSettings::default(),
5004 &budget,
5005 )
5006 .expect("the first mutation must consume the exact allowance");
5007 assert_eq!(document.as_xml(), first_output);
5008
5009 let second = document.with_view(|view| {
5010 view.node_identity(
5011 view.document()
5012 .descendants()
5013 .find(|node| node.has_tag_name("second"))
5014 .expect("second target"),
5015 )
5016 });
5017 let before = document.as_xml().to_owned();
5018 let error = document
5019 .replace_contents_with_budget(
5020 &[(second, "b".into())],
5021 DocumentParseSettings::default(),
5022 &budget,
5023 )
5024 .expect_err("the second mutation must not receive a fresh allowance");
5025
5026 assert!(matches!(
5027 error,
5028 XmlDocumentError::Policy(crate::policy::PolicyViolation::ResourceLimit {
5029 resource: crate::policy::resource_name::XML_PARSE_WORK_BYTES,
5030 maximum: observed_maximum,
5031 actual,
5032 }) if observed_maximum == maximum && actual > maximum
5033 ));
5034 assert_eq!(document.as_xml(), before);
5035 assert_eq!(document.generation(), 1);
5036 }
5037
5038 #[cfg(feature = "xmlenc")]
5039 #[test]
5040 fn bounded_content_replacement_rejects_new_text_node_atomically() {
5041 let mut document =
5042 XmlDocument::parse("<root><target/></root>").expect("fixture must parse");
5043 let target = document.with_view(|view| {
5044 let root = view
5045 .resolve_node(view.root_element())
5046 .expect("root must resolve");
5047 view.node_identity(
5048 root.children()
5049 .find(Node::is_element)
5050 .expect("target must exist"),
5051 )
5052 });
5053 let maximum = document.with_view(|view| view.node_count());
5054 let before = document.as_xml().to_owned();
5055 let budget = XmlParseWorkBudget::from_resources(&crate::policy::ResourcePolicy::default());
5056
5057 assert!(matches!(
5058 document.replace_content_with_budget(
5059 target,
5060 "value",
5061 DocumentParseSettings::new(false, maximum as u32, 1_024),
5062 &budget,
5063 ),
5064 Err(XmlDocumentError::ProjectedNodeLimit { maximum: rejected })
5065 if rejected == maximum
5066 ));
5067 assert_eq!(document.as_xml(), before);
5068 assert_eq!(document.generation(), 0);
5069 }
5070
5071 #[test]
5072 fn entity_expanded_elements_are_not_mutation_targets() {
5073 let source = "<!DOCTYPE root [<!ENTITY pair '<a ID=\"target\"/><b/>'>]><root>&pair;</root>";
5076 let settings = DocumentParseSettings::new(true, 64, 4_096);
5077 let mut document = XmlDocument::parse_with_settings(source.into(), settings)
5078 .expect("entity fixture must parse");
5079 let target = document
5080 .with_view(|view| view.node_for_id("target", &[]))
5081 .expect("expanded element ID must resolve");
5082 let before = document.as_xml().to_owned();
5083
5084 let error = document
5085 .replace_element(target, "<replacement/>")
5086 .expect_err("entity-expanded identity must not be mutable");
5087
5088 assert!(error.to_string().contains("entity expansion"), "{error}");
5089 assert_eq!(document.as_xml(), before);
5090 assert_eq!(document.generation(), 0);
5091 }
5092
5093 #[cfg(feature = "xmldsig")]
5094 #[test]
5095 fn bounded_append_accepts_text_merged_with_existing_content() {
5096 let mut document = XmlDocument::parse_with_settings(
5099 "<root>text</root>".into(),
5100 DocumentParseSettings::new(false, 3, 1_024),
5101 )
5102 .expect("fixture must fit the exact node ceiling");
5103 let root = document.with_view(|view| view.root_element());
5104 let budget = XmlParseWorkBudget::from_resources(&crate::policy::ResourcePolicy::default());
5105
5106 document
5107 .append_child_with_budget(
5108 root,
5109 "more",
5110 DocumentParseSettings::new(false, 3, 1_024),
5111 &budget,
5112 )
5113 .expect("merged text must not consume another committed node");
5114
5115 assert_eq!(document.as_xml(), "<root>textmore</root>");
5116 assert_eq!(document.with_view(|view| view.node_count()), 3);
5117 }
5118
5119 #[cfg(feature = "xmlenc")]
5120 #[test]
5121 fn bounded_fragment_accepts_two_boundary_text_merges() {
5122 let mut document =
5125 XmlDocument::parse("<root>left<target/>right</root>").expect("fixture must parse");
5126 let target = document.with_view(|view| {
5127 let target = view
5128 .document()
5129 .descendants()
5130 .find(|node| node.has_tag_name("target"))
5131 .expect("target must exist");
5132 view.node_identity(target)
5133 });
5134 let budget = XmlParseWorkBudget::from_resources(&crate::policy::ResourcePolicy::default());
5135
5136 let prepared = document
5137 .prepare_node_fragment_replacement_with_budget(
5138 target,
5139 "middle",
5140 DocumentParseSettings::new(false, 3, 1_024),
5141 &budget,
5142 )
5143 .expect("both boundary text pairs must fit in the prepared document");
5144 document
5145 .commit_prepared(prepared)
5146 .expect("prepared replacement must commit atomically");
5147
5148 assert_eq!(document.as_xml(), "<root>leftmiddleright</root>");
5149 assert_eq!(document.with_view(|view| view.node_count()), 3);
5150 }
5151
5152 #[test]
5153 fn semantic_order_covers_namespace_attribute_and_tree_nodes() {
5154 let document =
5155 XmlDocument::parse("<root xmlns:p=\"urn:test\" p:value=\"1\"><child/></root>")
5156 .expect("fixture must parse");
5157
5158 document.with_view(|view| {
5159 let root = view.root_element();
5160 let root_node = view.resolve_node(root).expect("root must resolve");
5161 let child = view.node_identity(
5162 root_node
5163 .children()
5164 .find(Node::is_element)
5165 .expect("child must exist"),
5166 );
5167 let attribute = view
5168 .attribute_identity(root, Some("urn:test"), "value")
5169 .expect("attribute must exist");
5170 let namespace = view
5171 .namespace_identities(root)
5172 .expect("namespace axis must exist")
5173 .into_iter()
5174 .find(|namespace| namespace.prefix() == "p")
5175 .expect("p namespace must exist");
5176
5177 let root_order = view.node_order(root).expect("root order must exist");
5178 let namespace_order = view
5179 .namespace_order(&namespace)
5180 .expect("namespace order must exist");
5181 let attribute_order = view
5182 .attribute_order(&attribute)
5183 .expect("attribute order must exist");
5184 let child_order = view.node_order(child).expect("child order must exist");
5185 assert!(root_order < namespace_order);
5186 assert!(namespace_order < attribute_order);
5187 assert!(attribute_order < child_order);
5188 });
5189 }
5190
5191 #[test]
5192 fn inherited_namespace_axes_are_materialized_only_for_the_requested_owner() {
5193 let declarations = (0..128)
5197 .map(|index| format!(r#" xmlns:p{index}="urn:namespace:{index}""#))
5198 .collect::<String>();
5199 let children = (0..1_024)
5200 .map(|index| format!("<child index=\"{index}\"/>"))
5201 .collect::<String>();
5202 let document = XmlDocument::parse(format!("<root{declarations}>{children}</root>"))
5203 .expect("wide namespace fixture must parse");
5204
5205 document.with_view(|view| {
5206 let last_child = view
5207 .document()
5208 .descendants()
5209 .rfind(|node| node.has_tag_name("child"))
5210 .expect("last child must exist");
5211 let owner = view.node_identity(last_child);
5212 let namespaces = view
5213 .namespace_identities(owner)
5214 .expect("inherited namespace axis must materialize on demand");
5215 assert_eq!(namespaces.len(), 128);
5216 assert!(namespaces.windows(2).all(|pair| {
5217 view.namespace_order(&pair[0]).expect("left order")
5218 < view.namespace_order(&pair[1]).expect("right order")
5219 }));
5220 });
5221 }
5222
5223 #[test]
5224 fn mutation_invalidates_attribute_and_namespace_identities() {
5225 let mut document =
5226 XmlDocument::parse("<root xmlns:p=\"urn:test\" p:value=\"1\"><child/></root>")
5227 .expect("fixture must parse");
5228 let (root, attribute, namespace) = document.with_view(|view| {
5229 let root = view.root_element();
5230 let attribute = view
5231 .attribute_identity(root, Some("urn:test"), "value")
5232 .expect("attribute must exist");
5233 let namespace = view
5234 .namespace_identities(root)
5235 .expect("namespace axis must exist")
5236 .into_iter()
5237 .find(|namespace| namespace.prefix() == "p")
5238 .expect("p namespace must exist");
5239 (root, attribute, namespace)
5240 });
5241
5242 document
5243 .append_child(root, "<p:next/>")
5244 .expect("mutation must succeed");
5245 document.with_view(|view| {
5246 assert!(matches!(
5247 view.attribute_order(&attribute),
5248 Err(XmlDocumentError::StaleIdentity { .. })
5249 ));
5250 assert!(matches!(
5251 view.namespace_order(&namespace),
5252 Err(XmlDocumentError::StaleIdentity { .. })
5253 ));
5254 });
5255 }
5256
5257 #[test]
5258 fn content_replacement_cannot_escape_its_target_element() {
5259 let mut document = XmlDocument::parse("<outer><target><old/></target></outer>")
5260 .expect("fixture must parse");
5261 let target = document.with_view(|view| {
5262 view.node_identity(
5263 view.document()
5264 .descendants()
5265 .find(|node| node.has_tag_name("target"))
5266 .expect("target must exist"),
5267 )
5268 });
5269 let before = document.as_xml().to_owned();
5270
5271 assert!(
5272 document
5273 .replace_content(target, "</target><attacker/><target>")
5274 .is_err()
5275 );
5276 assert_eq!(document.as_xml(), before);
5277 assert_eq!(document.generation(), 0);
5278 }
5279}