1#[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
9use std::cell::Cell;
10use std::collections::{HashMap, HashSet, hash_map::Entry};
11use std::sync::atomic::{AtomicU64, Ordering};
12
13use crate::xml::dom::{Document, Node, NodeId, ParseError, ParsingOptions, XmlBackend};
14use quick_xml::{
15 Reader as QuickXmlReader,
16 events::{BytesStart as QuickXmlBytesStart, Event as QuickXmlEvent},
17};
18use self_cell::self_cell;
19
20use crate::IdAttributeRegistration;
21use crate::xml::dom::{SemanticDocument, SemanticNodeId};
22
23#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24struct DocumentMetrics {
25 node_count: usize,
26 max_depth: usize,
27}
28
29static NEXT_DOCUMENT_ID: AtomicU64 = AtomicU64::new(1);
30const VALIDATION_WRAPPER_NS: &str = "urn:xml-sec:owned-document-validation";
31const VALIDATION_WRAPPER_OPEN: &str = "<xmlsec_owned_document:wrapper xmlns:xmlsec_owned_document=\"urn:xml-sec:owned-document-validation\">";
32const VALIDATION_WRAPPER_CLOSE: &str = "</xmlsec_owned_document:wrapper>";
33const VALIDATION_WRAPPER_NODE_OVERHEAD: u32 = 3;
36
37#[cfg(test)]
38pub(crate) fn selected_parser_passes() -> usize {
39 3
40}
41
42#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
44pub struct DocumentIdentity(u64);
45
46#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
48pub struct NodeIdentity {
49 document: DocumentIdentity,
50 generation: u64,
51 semantic_id: SemanticNodeId,
52}
53
54#[derive(Clone, Debug, PartialEq, Eq, Hash)]
56pub struct AttributeIdentity {
57 owner: NodeIdentity,
58 namespace: Option<String>,
59 local_name: String,
60}
61
62#[derive(Clone, Debug, PartialEq, Eq, Hash)]
64pub struct NamespaceIdentity {
65 owner: NodeIdentity,
66 prefix: String,
67 uri: String,
68}
69
70#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
72pub struct SemanticOrder {
73 node: usize,
74 phase: u8,
75 position: usize,
76}
77
78impl AttributeIdentity {
79 #[must_use]
81 pub const fn owner(&self) -> NodeIdentity {
82 self.owner
83 }
84
85 #[must_use]
87 pub fn namespace(&self) -> Option<&str> {
88 self.namespace.as_deref()
89 }
90
91 #[must_use]
93 pub fn local_name(&self) -> &str {
94 &self.local_name
95 }
96}
97
98impl NamespaceIdentity {
99 #[must_use]
101 pub const fn owner(&self) -> NodeIdentity {
102 self.owner
103 }
104
105 #[must_use]
107 pub fn prefix(&self) -> &str {
108 &self.prefix
109 }
110
111 #[must_use]
113 pub fn uri(&self) -> &str {
114 &self.uri
115 }
116}
117
118#[derive(Clone, Copy)]
119pub(crate) struct DocumentParseSettings {
120 pub(crate) backend: XmlBackend,
121 pub(crate) allow_dtd: bool,
122 pub(crate) nodes_limit: u32,
123 pub(crate) depth_limit: usize,
124 pub(crate) max_bytes: usize,
125}
126
127impl Default for DocumentParseSettings {
128 fn default() -> Self {
129 Self {
130 backend: XmlBackend::build_default(),
131 allow_dtd: false,
132 nodes_limit: crate::hard_limits::XML_DOCUMENT_NODE_CEILING,
133 depth_limit: crate::hard_limits::XML_DOCUMENT_DEPTH_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 pub(crate) const fn new_with_depth(
159 allow_dtd: bool,
160 nodes_limit: u32,
161 depth_limit: usize,
162 max_bytes: usize,
163 ) -> Self {
164 Self {
165 backend: XmlBackend::build_default(),
166 allow_dtd,
167 nodes_limit,
168 depth_limit,
169 max_bytes,
170 }
171 }
172
173 pub(crate) fn from_policy(
174 xml: &crate::policy::XmlInputPolicy,
175 resources: &crate::policy::ResourcePolicy,
176 ) -> Self {
177 Self::new_with_depth(
178 xml.allow_internal_dtd,
179 resources.effective_xml_nodes(),
180 resources.max_xml_depth,
181 resources.max_xml_document_bytes,
182 )
183 }
184}
185
186pub(crate) struct XmlParseWorkBudget {
193 #[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
194 remaining: Cell<usize>,
195 #[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
196 maximum: usize,
197}
198
199impl XmlParseWorkBudget {
200 #[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
201 pub(crate) fn from_resources(resources: &crate::policy::ResourcePolicy) -> Self {
202 Self {
203 remaining: Cell::new(resources.max_xml_parse_work_bytes),
204 maximum: resources.max_xml_parse_work_bytes,
205 }
206 }
207
208 #[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
209 pub(crate) fn charge_policy(&self, bytes: usize) -> Result<(), crate::policy::PolicyViolation> {
210 let consumed = self.maximum.saturating_sub(self.remaining.get());
211 let Some(remaining) = self.remaining.get().checked_sub(bytes) else {
212 self.remaining.set(0);
213 return Err(crate::policy::PolicyViolation::ResourceLimit {
214 resource: crate::policy::resource_name::XML_PARSE_WORK_BYTES,
215 maximum: self.maximum,
216 actual: consumed.saturating_add(bytes),
217 });
218 };
219 self.remaining.set(remaining);
220 Ok(())
221 }
222
223 #[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
224 pub(crate) fn charge(&self, bytes: usize) -> Result<(), XmlDocumentError> {
225 self.charge_policy(bytes).map_err(Into::into)
226 }
227
228 #[cfg(all(test, any(feature = "xmldsig", feature = "xmlenc")))]
229 fn with_limit(maximum: usize) -> Self {
230 Self {
231 remaining: Cell::new(maximum),
232 maximum,
233 }
234 }
235
236 #[cfg(all(test, any(feature = "xmldsig", feature = "xmlenc")))]
237 pub(crate) fn consumed(&self) -> usize {
238 self.maximum.saturating_sub(self.remaining.get())
239 }
240}
241
242fn charge_parse_work(
243 budget: Option<&XmlParseWorkBudget>,
244 bytes: usize,
245) -> Result<(), XmlDocumentError> {
246 #[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
247 if let Some(budget) = budget {
248 budget.charge(bytes)?;
249 }
250 #[cfg(not(any(feature = "xmldsig", feature = "xmlenc")))]
251 let _ = (budget, bytes);
252 Ok(())
253}
254
255fn charge_semantic_parser_work(
256 budget: Option<&XmlParseWorkBudget>,
257 bytes: usize,
258) -> Result<(), XmlDocumentError> {
259 for _ in 0..2 {
263 charge_parse_work(budget, bytes)?;
264 }
265 Ok(())
266}
267
268struct ContentReplacementEdit<'a> {
269 range: std::ops::Range<usize>,
270 replacement: &'a str,
271 self_closing: Option<SelfClosingContentEdit>,
272}
273
274struct SelfClosingContentEdit {
275 prefix_end: usize,
276 qualified_name: String,
277}
278
279#[cfg(feature = "xmldsig")]
280fn is_unescaped_base64_text(value: &str) -> bool {
281 value
282 .bytes()
283 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'+' | b'/' | b'='))
284}
285
286#[cfg(feature = "xmldsig")]
287fn validate_base64_replacements(
288 replacements: &[(NodeIdentity, String)],
289) -> Result<(), XmlDocumentError> {
290 if replacements
291 .iter()
292 .any(|(_, value)| !is_unescaped_base64_text(value))
293 {
294 return Err(XmlDocumentError::InvalidReplacement(
295 "generated base64 replacement contains XML markup".into(),
296 ));
297 }
298 Ok(())
299}
300
301impl ContentReplacementEdit<'_> {
302 fn output_len(&self) -> Option<usize> {
303 let Some(expansion) = &self.self_closing else {
304 return Some(self.replacement.len());
305 };
306 expansion
307 .prefix_end
308 .checked_add(1)
309 .and_then(|length| length.checked_add(self.replacement.len()))
310 .and_then(|length| length.checked_add(2))
311 .and_then(|length| length.checked_add(expansion.qualified_name.len()))
312 .and_then(|length| length.checked_add(1))
313 }
314
315 fn validation_output_len(&self) -> Option<usize> {
316 self.output_len()?
317 .checked_add(VALIDATION_WRAPPER_OPEN.len())?
318 .checked_add(VALIDATION_WRAPPER_CLOSE.len())
319 }
320
321 fn write_output(&self, source: &str, output: &mut String) {
322 if let Some(expansion) = &self.self_closing {
323 output.push_str(&source[self.range.start..self.range.start + expansion.prefix_end]);
324 output.push('>');
325 }
326 output.push_str(self.replacement);
327 if let Some(expansion) = &self.self_closing {
328 output.push_str("</");
329 output.push_str(&expansion.qualified_name);
330 output.push('>');
331 }
332 }
333
334 fn write_validation_output(&self, source: &str, output: &mut String) -> std::ops::Range<usize> {
335 if let Some(expansion) = &self.self_closing {
336 output.push_str(&source[self.range.start..self.range.start + expansion.prefix_end]);
337 output.push('>');
338 }
339 let wrapper_start = output.len();
340 output.push_str(VALIDATION_WRAPPER_OPEN);
341 output.push_str(self.replacement);
342 output.push_str(VALIDATION_WRAPPER_CLOSE);
343 let wrapper_range = wrapper_start..output.len();
344 if let Some(expansion) = &self.self_closing {
345 output.push_str("</");
346 output.push_str(&expansion.qualified_name);
347 output.push('>');
348 }
349 wrapper_range
350 }
351}
352
353#[derive(Debug)]
354struct DocumentIndexes {
355 order: HashMap<NodeId, usize>,
356 default_ids: HashMap<String, Option<NodeId>>,
357 attributes_by_value: HashMap<(String, String), Vec<NodeId>>,
358}
359
360#[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
361mod policy_sealed {
362 pub trait Sealed {}
363}
364
365#[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
372pub trait XmlDocumentPolicy: policy_sealed::Sealed {
373 #[doc(hidden)]
374 fn xml_input_policy(&self) -> &crate::policy::XmlInputPolicy;
375
376 #[doc(hidden)]
377 fn resource_policy(&self) -> &crate::policy::ResourcePolicy;
378}
379
380#[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
381macro_rules! impl_document_policy {
382 ($policy:ty) => {
383 impl policy_sealed::Sealed for $policy {}
384
385 impl XmlDocumentPolicy for $policy {
386 fn xml_input_policy(&self) -> &crate::policy::XmlInputPolicy {
387 &self.xml
388 }
389
390 fn resource_policy(&self) -> &crate::policy::ResourcePolicy {
391 &self.resources
392 }
393 }
394 };
395}
396
397#[cfg(feature = "xmldsig")]
398impl_document_policy!(crate::policy::SigningPolicy);
399#[cfg(feature = "xmldsig")]
400impl_document_policy!(crate::policy::VerificationPolicy);
401#[cfg(feature = "xmlenc")]
402impl_document_policy!(crate::policy::EncryptionPolicy);
403#[cfg(feature = "xmlenc")]
404impl_document_policy!(crate::policy::DecryptionPolicy);
405
406struct ParsedDocument<'input> {
407 document: Document<'input>,
408 indexes: DocumentIndexes,
409 node_count: usize,
410 max_depth: usize,
411}
412
413self_cell!(
414 struct DocumentCell {
415 owner: String,
416
417 #[covariant]
418 dependent: ParsedDocument,
419 }
420);
421
422#[derive(Debug, thiserror::Error)]
424pub enum XmlDocumentError {
425 #[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
427 #[error("XML document policy violation: {0}")]
428 Policy(#[from] crate::policy::PolicyViolation),
429
430 #[error("XML document exceeds the maximum size of {maximum} bytes: {actual} bytes")]
432 DocumentTooLarge {
433 maximum: usize,
435 actual: usize,
437 },
438 #[error("XML document exceeds the maximum element depth of {maximum}: {actual}")]
440 DocumentTooDeep {
441 maximum: usize,
443 actual: usize,
445 },
446 #[error("XML parsing error: {0}")]
448 Parse(#[from] ParseError),
449 #[error("XML identity belongs to a different document")]
451 ForeignIdentity,
452 #[error("XML identity belongs to stale generation {identity}; current generation is {current}")]
454 StaleIdentity {
455 identity: u64,
457 current: u64,
459 },
460 #[error("XML identity does not resolve to a node")]
462 MissingNode,
463 #[error("XML mutation target must be an element")]
465 TargetNotElement,
466 #[error(
468 "XML mutation target originates from an entity expansion and has no unique source range"
469 )]
470 EntityExpandedMutationTarget,
471 #[error("invalid XML replacement: {0}")]
473 InvalidReplacement(String),
474 #[error("XML document identity space is exhausted")]
476 IdentityExhausted,
477 #[error("projected XML document exceeds the maximum node count of {maximum}")]
479 ProjectedNodeLimit {
480 maximum: usize,
482 },
483}
484
485#[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
486impl XmlDocumentError {
487 pub(crate) fn into_policy_violation(
488 self,
489 settings: DocumentParseSettings,
490 ) -> Result<crate::policy::PolicyViolation, Self> {
491 let violation = match self {
492 Self::Policy(error) => error,
493 Self::DocumentTooLarge { maximum, actual } => {
494 crate::policy::PolicyViolation::ResourceLimit {
495 resource: crate::policy::resource_name::XML_DOCUMENT,
496 maximum,
497 actual,
498 }
499 }
500 Self::DocumentTooDeep { maximum, actual } => {
501 crate::policy::PolicyViolation::ResourceLimit {
502 resource: crate::policy::resource_name::XML_DEPTH,
503 maximum,
504 actual,
505 }
506 }
507 Self::Parse(ParseError::NodesLimitReached) => {
508 crate::policy::PolicyViolation::ResourceLimit {
509 resource: crate::policy::resource_name::XML_NODES,
510 maximum: settings.nodes_limit as usize,
511 actual: settings.nodes_limit as usize + 1,
512 }
513 }
514 error => return Err(error),
515 };
516 Ok(violation)
517 }
518}
519
520pub struct XmlDocument {
526 identity: DocumentIdentity,
527 generation: u64,
528 #[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
529 requires_internal_dtd: bool,
530 settings: DocumentParseSettings,
531 cell: DocumentCell,
532}
533
534#[cfg(feature = "xmlenc")]
535pub(crate) struct PreparedDocumentMutation {
536 identity: DocumentIdentity,
537 generation: u64,
538 cell: DocumentCell,
539}
540
541#[derive(Clone, Copy)]
543pub struct DocumentView<'a> {
544 identity: DocumentIdentity,
545 generation: u64,
546 parsed: &'a ParsedDocument<'a>,
547}
548
549impl XmlDocument {
550 #[must_use]
552 pub fn xml_backend(&self) -> XmlBackend {
553 self.settings.backend
554 }
555
556 pub fn parse(xml: impl AsRef<str> + Into<String>) -> Result<Self, XmlDocumentError> {
559 let settings = DocumentParseSettings::default();
560 let xml = own_bounded_xml(xml, settings.max_bytes)?;
561 Self::parse_with_settings(xml, settings)
562 }
563
564 pub fn parse_with_backend(
566 xml: impl AsRef<str> + Into<String>,
567 backend: XmlBackend,
568 ) -> Result<Self, XmlDocumentError> {
569 let settings = DocumentParseSettings::default().with_backend(backend);
570 let xml = own_bounded_xml(xml, settings.max_bytes)?;
571 Self::parse_with_settings(xml, settings)
572 }
573
574 #[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
577 pub fn parse_with_policy(
578 xml: impl AsRef<str> + Into<String>,
579 policy: &impl XmlDocumentPolicy,
580 ) -> Result<Self, XmlDocumentError> {
581 let resources = policy.resource_policy();
582 resources.validate()?;
583 let budget = XmlParseWorkBudget::from_resources(resources);
584 let xml = own_bounded_xml(xml, resources.max_xml_document_bytes)?;
585 Self::parse_with_settings_and_optional_budget(
586 xml,
587 DocumentParseSettings::from_policy(policy.xml_input_policy(), resources),
588 Some(&budget),
589 )
590 }
591
592 #[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
594 pub fn parse_with_policy_and_backend(
595 xml: impl AsRef<str> + Into<String>,
596 policy: &impl XmlDocumentPolicy,
597 backend: XmlBackend,
598 ) -> Result<Self, XmlDocumentError> {
599 let resources = policy.resource_policy();
600 resources.validate()?;
601 let budget = XmlParseWorkBudget::from_resources(resources);
602 let xml = own_bounded_xml(xml, resources.max_xml_document_bytes)?;
603 Self::parse_with_settings_and_optional_budget(
604 xml,
605 DocumentParseSettings::from_policy(policy.xml_input_policy(), resources)
606 .with_backend(backend),
607 Some(&budget),
608 )
609 }
610
611 pub(crate) fn parse_with_settings(
612 xml: String,
613 settings: DocumentParseSettings,
614 ) -> Result<Self, XmlDocumentError> {
615 Self::parse_with_settings_and_optional_budget(xml, settings, None)
616 }
617
618 fn parse_with_settings_and_optional_budget(
619 xml: String,
620 settings: DocumentParseSettings,
621 budget: Option<&XmlParseWorkBudget>,
622 ) -> Result<Self, XmlDocumentError> {
623 if xml.len() > settings.max_bytes {
624 return Err(XmlDocumentError::DocumentTooLarge {
625 maximum: settings.max_bytes,
626 actual: xml.len(),
627 });
628 }
629 preflight_document_limits(&xml, settings, budget)?;
630 #[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
631 let requires_internal_dtd = document_requires_internal_dtd(&xml, settings, budget)?;
632 let cell = build_cell_after_preflight(xml, settings, budget)?;
633 let identity = allocate_document_identity(&NEXT_DOCUMENT_ID)?;
634 Ok(Self {
635 identity,
636 generation: 0,
637 #[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
638 requires_internal_dtd,
639 settings,
640 cell,
641 })
642 }
643
644 #[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
645 pub(crate) fn parse_with_settings_and_budget(
646 xml: String,
647 settings: DocumentParseSettings,
648 budget: &XmlParseWorkBudget,
649 ) -> Result<Self, XmlDocumentError> {
650 Self::parse_with_settings_and_optional_budget(xml, settings, Some(budget))
651 }
652
653 #[must_use]
655 pub const fn identity(&self) -> DocumentIdentity {
656 self.identity
657 }
658
659 #[must_use]
661 pub const fn generation(&self) -> u64 {
662 self.generation
663 }
664
665 #[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
666 pub(crate) fn validate_xml_input_policy(
667 &self,
668 allow_internal_dtd: bool,
669 ) -> Result<(), crate::policy::PolicyViolation> {
670 if self.requires_internal_dtd && !allow_internal_dtd {
671 return Err(crate::policy::PolicyViolation::XmlInput {
672 reason: "owned document requires internal DTD support",
673 });
674 }
675 Ok(())
676 }
677
678 #[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
679 pub(crate) fn validate_operation_policy(
680 &self,
681 xml: &crate::policy::XmlInputPolicy,
682 resources: &crate::policy::ResourcePolicy,
683 ) -> Result<(), crate::policy::PolicyViolation> {
684 resources.validate()?;
685 self.validate_xml_input_policy(xml.allow_internal_dtd)?;
686 resources.validate_xml_document_len(self.as_xml().len())?;
687 self.with_view(|view| {
688 let node_count = view.node_count();
689 if node_count > resources.effective_xml_nodes() as usize {
690 return Err(crate::policy::PolicyViolation::ResourceLimit {
691 resource: crate::policy::resource_name::XML_NODES,
692 maximum: resources.effective_xml_nodes() as usize,
693 actual: node_count,
694 });
695 }
696 let max_depth = view.max_depth();
697 if max_depth > resources.max_xml_depth {
698 return Err(crate::policy::PolicyViolation::ResourceLimit {
699 resource: crate::policy::resource_name::XML_DEPTH,
700 maximum: resources.max_xml_depth,
701 actual: max_depth,
702 });
703 }
704 Ok(())
705 })
706 }
707
708 #[must_use]
710 pub fn as_xml(&self) -> &str {
711 self.cell.borrow_owner()
712 }
713
714 #[must_use]
716 pub fn into_xml(self) -> String {
717 self.cell.into_owner()
718 }
719
720 #[cfg(feature = "xmldsig")]
721 pub(crate) fn staged_copy_with_budget(
722 &self,
723 settings: DocumentParseSettings,
724 budget: &XmlParseWorkBudget,
725 ) -> Result<Self, XmlDocumentError> {
726 Self::parse_with_settings_and_budget(self.as_xml().to_owned(), settings, budget)
727 }
728
729 #[cfg(feature = "xmldsig")]
730 pub(crate) fn commit_staged(&mut self, staged: Self) -> Result<(), XmlDocumentError> {
731 self.commit_cell(staged.cell)
735 }
736
737 pub fn with_view<R>(&self, operation: impl for<'a> FnOnce(DocumentView<'a>) -> R) -> R {
739 self.cell.with_dependent(|_, parsed| {
740 operation(DocumentView {
741 identity: self.identity,
742 generation: self.generation,
743 parsed,
744 })
745 })
746 }
747
748 pub fn replace_element(
750 &mut self,
751 target: NodeIdentity,
752 replacement: &str,
753 ) -> Result<(), XmlDocumentError> {
754 let range = self.with_view(|view| {
755 let node = view.resolve_mutation_node(target)?;
756 if !node.is_element() {
757 return Err(XmlDocumentError::TargetNotElement);
758 }
759 Ok(node.range())
760 })?;
761 self.ensure_replacement_fits(&range, replacement.len(), self.settings.max_bytes)?;
762 self.validate_single_element_in_parent_context(
763 target,
764 replacement,
765 None,
766 self.settings,
767 None,
768 )?;
769 self.replace_range(range, replacement, None)
770 }
771
772 #[cfg(feature = "xmlenc")]
773 pub(crate) fn replace_element_with_budget(
774 &mut self,
775 target: NodeIdentity,
776 replacement: &str,
777 settings: DocumentParseSettings,
778 budget: &XmlParseWorkBudget,
779 ) -> Result<(), XmlDocumentError> {
780 let range = self.with_view(|view| {
781 let node = view.resolve_mutation_node(target)?;
782 if !node.is_element() {
783 return Err(XmlDocumentError::TargetNotElement);
784 }
785 Ok(node.range())
786 })?;
787 self.ensure_replacement_fits(&range, replacement.len(), settings.max_bytes)?;
788 self.validate_single_element_in_parent_context(
789 target,
790 replacement,
791 Some(settings.nodes_limit as usize),
792 settings,
793 Some(budget),
794 )?;
795 self.replace_range_with_settings(range, replacement, settings, Some(budget))
796 }
797
798 #[cfg(feature = "xmlenc")]
799 pub(crate) fn prepare_element_replacement_with_budget(
800 &self,
801 target: NodeIdentity,
802 replacement: &str,
803 settings: DocumentParseSettings,
804 budget: &XmlParseWorkBudget,
805 ) -> Result<PreparedDocumentMutation, XmlDocumentError> {
806 let range = self.with_view(|view| {
807 let node = view.resolve_mutation_node(target)?;
808 if !node.is_element() {
809 return Err(XmlDocumentError::TargetNotElement);
810 }
811 Ok(node.range())
812 })?;
813 self.ensure_replacement_fits(&range, replacement.len(), settings.max_bytes)?;
814 self.validate_single_element_in_parent_context(
815 target,
816 replacement,
817 Some(settings.nodes_limit as usize),
818 settings,
819 Some(budget),
820 )?;
821 self.prepare_range_replacement(range, replacement, settings, budget)
822 }
823
824 pub fn replace_node_with_fragment(
829 &mut self,
830 target: NodeIdentity,
831 replacement: &str,
832 ) -> Result<(), XmlDocumentError> {
833 let range = self.with_view(|view| {
834 Ok::<_, XmlDocumentError>(view.resolve_mutation_node(target)?.range())
835 })?;
836 self.ensure_replacement_fits(&range, replacement.len(), self.settings.max_bytes)?;
837 self.validate_fragment_in_parent_context(target, replacement, None, self.settings, None)?;
838 self.replace_range(range, replacement, None)
839 }
840
841 #[cfg(feature = "xmlenc")]
842 pub(crate) fn prepare_node_fragment_replacement_with_budget(
843 &self,
844 target: NodeIdentity,
845 replacement: &str,
846 settings: DocumentParseSettings,
847 budget: &XmlParseWorkBudget,
848 ) -> Result<PreparedDocumentMutation, XmlDocumentError> {
849 let range = self.with_view(|view| {
850 Ok::<_, XmlDocumentError>(view.resolve_mutation_node(target)?.range())
851 })?;
852 self.ensure_replacement_fits(&range, replacement.len(), settings.max_bytes)?;
853 self.validate_fragment_in_parent_context(
854 target,
855 replacement,
856 Some(settings.nodes_limit as usize),
857 settings,
858 Some(budget),
859 )?;
860 self.prepare_range_replacement(range, replacement, settings, budget)
861 }
862
863 pub fn replace_content(
865 &mut self,
866 target: NodeIdentity,
867 replacement: &str,
868 ) -> Result<(), XmlDocumentError> {
869 self.replace_content_inner(target, replacement, None, self.settings, None)
870 }
871
872 #[cfg(feature = "xmlenc")]
873 pub(crate) fn replace_content_with_budget(
874 &mut self,
875 target: NodeIdentity,
876 replacement: &str,
877 settings: DocumentParseSettings,
878 budget: &XmlParseWorkBudget,
879 ) -> Result<(), XmlDocumentError> {
880 self.replace_content_inner(
881 target,
882 replacement,
883 Some(settings.nodes_limit as usize),
884 settings,
885 Some(budget),
886 )
887 }
888
889 fn replace_content_inner(
890 &mut self,
891 target: NodeIdentity,
892 replacement: &str,
893 maximum: Option<usize>,
894 settings: DocumentParseSettings,
895 budget: Option<&XmlParseWorkBudget>,
896 ) -> Result<(), XmlDocumentError> {
897 let (range, self_closing_expansion, serialized_len) = self.with_view(|view| {
898 let node = view.resolve_mutation_node(target)?;
899 if !node.is_element() {
900 return Err(XmlDocumentError::TargetNotElement);
901 }
902 let range = node.range();
903 let source = &view.xml()[range.clone()];
904 let (content, qualified_name, self_closing) = element_content_range(source)?;
905 if self_closing {
906 let slash = source.rfind("/>").ok_or_else(|| {
907 XmlDocumentError::InvalidReplacement(
908 "self-closing element has no terminator".into(),
909 )
910 })?;
911 let serialized_len = slash
912 .checked_add(replacement.len())
913 .and_then(|length| length.checked_add(qualified_name.len()))
914 .and_then(|length| length.checked_add(4))
915 .ok_or(XmlDocumentError::DocumentTooLarge {
916 maximum: settings.max_bytes,
917 actual: usize::MAX,
918 })?;
919 return Ok((range, Some((slash, qualified_name)), serialized_len));
920 }
921 let range = (range.start + content.start)..(range.start + content.end);
922 Ok((range, None, replacement.len()))
923 })?;
924 self.ensure_replacement_fits(&range, serialized_len, settings.max_bytes)?;
925 let serialized_replacement = if let Some((slash, qualified_name)) = self_closing_expansion {
926 let source = &self.as_xml()[range.clone()];
927 format!("{}>{replacement}</{qualified_name}>", &source[..slash])
928 } else {
929 replacement.to_owned()
930 };
931 self.validate_content_in_element_context(target, replacement, maximum, settings, budget)?;
932 if let Some(maximum) = maximum {
933 debug_assert_eq!(maximum, settings.nodes_limit as usize);
934 self.replace_range_with_settings(range, &serialized_replacement, settings, budget)
935 } else {
936 self.replace_range(range, &serialized_replacement, budget)
937 }
938 }
939
940 pub fn replace_contents(
946 &mut self,
947 replacements: &[(NodeIdentity, String)],
948 ) -> Result<(), XmlDocumentError> {
949 self.replace_contents_inner(replacements, self.settings, false, true, None)
950 }
951
952 #[cfg(all(feature = "xmldsig", test))]
953 pub(crate) fn replace_contents_with_budget(
954 &mut self,
955 replacements: &[(NodeIdentity, String)],
956 settings: DocumentParseSettings,
957 budget: &XmlParseWorkBudget,
958 ) -> Result<(), XmlDocumentError> {
959 self.replace_contents_inner(replacements, settings, true, true, Some(budget))
960 }
961
962 #[cfg(feature = "xmldsig")]
963 pub(crate) fn replace_base64_contents_with_budget(
964 &mut self,
965 replacements: &[(NodeIdentity, String)],
966 settings: DocumentParseSettings,
967 budget: &XmlParseWorkBudget,
968 ) -> Result<(), XmlDocumentError> {
969 validate_base64_replacements(replacements)?;
970 self.replace_contents_inner(replacements, settings, true, false, Some(budget))
974 }
975
976 #[cfg(feature = "xmldsig")]
977 pub(crate) fn project_base64_contents(
978 &self,
979 replacements: &[(NodeIdentity, String)],
980 maximum_bytes: usize,
981 ) -> Result<String, XmlDocumentError> {
982 validate_base64_replacements(replacements)?;
983 let (edits, projected) =
984 self.prepare_content_replacement_edits(replacements, maximum_bytes)?;
985 Ok(self.render_content_replacement_edits(&edits, projected))
986 }
987
988 fn replace_contents_inner(
989 &mut self,
990 replacements: &[(NodeIdentity, String)],
991 settings: DocumentParseSettings,
992 policy_bounded: bool,
993 validate_fragments: bool,
994 budget: Option<&XmlParseWorkBudget>,
995 ) -> Result<(), XmlDocumentError> {
996 if replacements.is_empty() {
997 return Ok(());
998 }
999 let (edits, projected) =
1000 self.prepare_content_replacement_edits(replacements, settings.max_bytes)?;
1001
1002 if validate_fragments {
1003 self.validate_content_edits(&edits, settings, policy_bounded, budget)?;
1004 }
1005
1006 let output = self.render_content_replacement_edits(&edits, projected);
1007 if policy_bounded {
1008 self.replace_serialized_with_settings(output, settings, budget)
1009 } else {
1010 self.replace_serialized(output, budget)
1011 }
1012 }
1013
1014 fn prepare_content_replacement_edits<'a>(
1015 &self,
1016 replacements: &'a [(NodeIdentity, String)],
1017 maximum_bytes: usize,
1018 ) -> Result<(Vec<ContentReplacementEdit<'a>>, usize), XmlDocumentError> {
1019 let mut targets = HashSet::with_capacity(replacements.len());
1020 if replacements
1021 .iter()
1022 .any(|(target, _)| !targets.insert(*target))
1023 {
1024 return Err(XmlDocumentError::InvalidReplacement(
1025 "replacement targets must be unique".into(),
1026 ));
1027 }
1028 let mut edits = self.with_view(|view| {
1029 replacements
1030 .iter()
1031 .map(|(target, replacement)| {
1032 let node = view.resolve_mutation_node(*target)?;
1033 if !node.is_element() {
1034 return Err(XmlDocumentError::TargetNotElement);
1035 }
1036 let range = node.range();
1037 let source = &view.xml()[range.clone()];
1038 let (content, qualified_name, self_closing) = element_content_range(source)?;
1039 if self_closing {
1040 let slash = source.rfind("/>").ok_or_else(|| {
1041 XmlDocumentError::InvalidReplacement(
1042 "self-closing element has no terminator".into(),
1043 )
1044 })?;
1045 Ok(ContentReplacementEdit {
1046 range,
1047 replacement,
1048 self_closing: Some(SelfClosingContentEdit {
1049 prefix_end: slash,
1050 qualified_name,
1051 }),
1052 })
1053 } else {
1054 Ok(ContentReplacementEdit {
1055 range: (range.start + content.start)..(range.start + content.end),
1056 replacement,
1057 self_closing: None,
1058 })
1059 }
1060 })
1061 .collect::<Result<Vec<_>, XmlDocumentError>>()
1062 })?;
1063 edits.sort_by_key(|edit| edit.range.start);
1064 if edits
1065 .windows(2)
1066 .any(|pair| pair[0].range.end > pair[1].range.start)
1067 {
1068 return Err(XmlDocumentError::InvalidReplacement(
1069 "replacement targets overlap".into(),
1070 ));
1071 }
1072 let projected = edits.iter().try_fold(self.as_xml().len(), |length, edit| {
1073 length
1074 .checked_sub(edit.range.len())
1075 .and_then(|length| length.checked_add(edit.output_len()?))
1076 .ok_or(XmlDocumentError::DocumentTooLarge {
1077 maximum: maximum_bytes,
1078 actual: usize::MAX,
1079 })
1080 })?;
1081 if projected > maximum_bytes {
1082 return Err(XmlDocumentError::DocumentTooLarge {
1083 maximum: maximum_bytes,
1084 actual: projected,
1085 });
1086 }
1087 Ok((edits, projected))
1088 }
1089
1090 fn render_content_replacement_edits(
1091 &self,
1092 edits: &[ContentReplacementEdit<'_>],
1093 projected: usize,
1094 ) -> String {
1095 let mut output = String::with_capacity(projected);
1096 let mut cursor = 0;
1097 for edit in edits {
1098 output.push_str(&self.as_xml()[cursor..edit.range.start]);
1099 edit.write_output(self.as_xml(), &mut output);
1100 cursor = edit.range.end;
1101 }
1102 output.push_str(&self.as_xml()[cursor..]);
1103 debug_assert_eq!(output.len(), projected);
1104 output
1105 }
1106
1107 fn validate_content_edits(
1108 &self,
1109 edits: &[ContentReplacementEdit<'_>],
1110 settings: DocumentParseSettings,
1111 policy_bounded: bool,
1112 budget: Option<&XmlParseWorkBudget>,
1113 ) -> Result<(), XmlDocumentError> {
1114 let projected = edits.iter().try_fold(self.as_xml().len(), |length, edit| {
1115 length
1116 .checked_sub(edit.range.len())
1117 .and_then(|length| length.checked_add(edit.validation_output_len()?))
1118 .ok_or_else(|| {
1119 XmlDocumentError::InvalidReplacement("validation length overflow".into())
1120 })
1121 })?;
1122 let mut candidate = String::with_capacity(projected);
1123 let mut wrapper_ranges = Vec::with_capacity(edits.len());
1124 let mut cursor = 0;
1125 for edit in edits {
1126 candidate.push_str(&self.as_xml()[cursor..edit.range.start]);
1127 wrapper_ranges.push(edit.write_validation_output(self.as_xml(), &mut candidate));
1128 cursor = edit.range.end;
1129 }
1130 candidate.push_str(&self.as_xml()[cursor..]);
1131 debug_assert_eq!(candidate.len(), projected);
1132
1133 let active_nodes_limit = settings.nodes_limit as usize;
1134 let wrapper_allowance = edits.len();
1135 let validation_nodes_limit = active_nodes_limit
1136 .checked_add(wrapper_allowance)
1137 .and_then(|maximum| u32::try_from(maximum).ok())
1138 .unwrap_or(u32::MAX);
1139 let parsed = build_cell(
1140 candidate,
1141 DocumentParseSettings {
1142 nodes_limit: validation_nodes_limit,
1143 depth_limit: settings.depth_limit.saturating_add(1),
1144 max_bytes: projected,
1145 ..settings
1146 },
1147 budget,
1148 )
1149 .map_err(|error| match (policy_bounded, error) {
1150 (true, XmlDocumentError::Parse(ParseError::NodesLimitReached)) => {
1151 XmlDocumentError::ProjectedNodeLimit {
1152 maximum: active_nodes_limit,
1153 }
1154 }
1155 (_, XmlDocumentError::DocumentTooDeep { actual, .. }) => {
1156 XmlDocumentError::DocumentTooDeep {
1157 maximum: settings.depth_limit,
1158 actual: actual.saturating_sub(1),
1159 }
1160 }
1161 (_, error) => error,
1162 })?;
1163 parsed.with_dependent(|_, document| validate_wrappers(document, &wrapper_ranges))
1164 }
1165
1166 pub fn append_child(
1168 &mut self,
1169 target: NodeIdentity,
1170 child: &str,
1171 ) -> Result<(), XmlDocumentError> {
1172 self.append_child_inner(target, child, None, self.settings, true, None)
1173 }
1174
1175 #[cfg(all(feature = "xmldsig", test))]
1176 pub(crate) fn append_child_with_budget(
1177 &mut self,
1178 target: NodeIdentity,
1179 child: &str,
1180 settings: DocumentParseSettings,
1181 budget: &XmlParseWorkBudget,
1182 ) -> Result<(), XmlDocumentError> {
1183 self.append_child_inner(
1184 target,
1185 child,
1186 Some(settings.nodes_limit as usize),
1187 settings,
1188 true,
1189 Some(budget),
1190 )
1191 }
1192
1193 #[cfg(feature = "xmldsig")]
1194 pub(crate) fn append_generated_child_with_budget(
1195 &mut self,
1196 target: NodeIdentity,
1197 child: &str,
1198 settings: DocumentParseSettings,
1199 budget: &XmlParseWorkBudget,
1200 ) -> Result<(), XmlDocumentError> {
1201 self.append_child_inner(
1205 target,
1206 child,
1207 Some(settings.nodes_limit as usize),
1208 settings,
1209 false,
1210 Some(budget),
1211 )
1212 }
1213
1214 fn append_child_inner(
1215 &mut self,
1216 target: NodeIdentity,
1217 child: &str,
1218 maximum: Option<usize>,
1219 settings: DocumentParseSettings,
1220 validate_context: bool,
1221 budget: Option<&XmlParseWorkBudget>,
1222 ) -> Result<(), XmlDocumentError> {
1223 let projected = self.projected_child_append_len(target, child.len())?;
1224 if projected > settings.max_bytes {
1225 return Err(XmlDocumentError::DocumentTooLarge {
1226 maximum: settings.max_bytes,
1227 actual: projected,
1228 });
1229 }
1230 let (range, replacement) = self.with_view(|view| {
1231 let node = view.resolve_mutation_node(target)?;
1232 if !node.is_element() {
1233 return Err(XmlDocumentError::TargetNotElement);
1234 }
1235 let range = node.range();
1236 let source = &view.xml()[range.clone()];
1237 let (content, qualified_name, self_closing) = element_content_range(source)?;
1238 if self_closing {
1239 let slash = source.rfind("/>").ok_or_else(|| {
1240 XmlDocumentError::InvalidReplacement(
1241 "self-closing element has no terminator".into(),
1242 )
1243 })?;
1244 return Ok((
1245 range,
1246 format!("{}>{child}</{qualified_name}>", &source[..slash]),
1247 ));
1248 }
1249 Ok((
1250 (range.start + content.end)..(range.start + content.end),
1251 child.to_owned(),
1252 ))
1253 })?;
1254 self.ensure_replacement_fits(&range, replacement.len(), settings.max_bytes)?;
1255 if validate_context {
1256 self.validate_content_in_element_context(target, child, maximum, settings, budget)?;
1257 }
1258 if let Some(maximum) = maximum {
1259 debug_assert_eq!(maximum, settings.nodes_limit as usize);
1260 self.replace_range_with_settings(range, &replacement, settings, budget)
1261 } else {
1262 self.replace_range(range, &replacement, budget)
1263 }
1264 }
1265
1266 pub(crate) fn replace_serialized(
1267 &mut self,
1268 xml: String,
1269 budget: Option<&XmlParseWorkBudget>,
1270 ) -> Result<(), XmlDocumentError> {
1271 let next = build_cell(xml, self.settings, budget)?;
1272 self.commit_cell(next)
1273 }
1274
1275 pub(crate) fn replace_serialized_with_settings(
1276 &mut self,
1277 xml: String,
1278 settings: DocumentParseSettings,
1279 budget: Option<&XmlParseWorkBudget>,
1280 ) -> Result<(), XmlDocumentError> {
1281 let next = build_cell(xml, settings, budget).map_err(|error| match error {
1282 XmlDocumentError::Parse(ParseError::NodesLimitReached) => {
1283 XmlDocumentError::ProjectedNodeLimit {
1284 maximum: settings.nodes_limit as usize,
1285 }
1286 }
1287 error => error,
1288 })?;
1289 self.commit_cell(next)
1290 }
1291
1292 fn commit_cell(&mut self, next: DocumentCell) -> Result<(), XmlDocumentError> {
1293 let next_generation = self.generation.checked_add(1).ok_or_else(|| {
1294 XmlDocumentError::InvalidReplacement("document generation overflow".into())
1295 })?;
1296 self.cell = next;
1297 self.generation = next_generation;
1298 Ok(())
1299 }
1300
1301 #[cfg(feature = "xmldsig")]
1302 pub(crate) fn projected_content_replacement_len(
1303 &self,
1304 target: NodeIdentity,
1305 replacement_len: usize,
1306 ) -> Result<usize, XmlDocumentError> {
1307 self.with_view(|view| {
1308 let node = view.resolve_mutation_node(target)?;
1309 if !node.is_element() {
1310 return Err(XmlDocumentError::TargetNotElement);
1311 }
1312 let range = node.range();
1313 let source = &view.xml()[range.clone()];
1314 let (content, qualified_name, self_closing) = element_content_range(source)?;
1315 let removed = if self_closing {
1316 range.len()
1317 } else {
1318 content.len()
1319 };
1320 let added = if self_closing {
1321 let slash = source.rfind("/>").ok_or_else(|| {
1322 XmlDocumentError::InvalidReplacement(
1323 "self-closing element has no terminator".into(),
1324 )
1325 })?;
1326 slash
1327 .checked_add(1)
1328 .and_then(|length| length.checked_add(replacement_len))
1329 .and_then(|length| length.checked_add(2))
1330 .and_then(|length| length.checked_add(qualified_name.len()))
1331 .and_then(|length| length.checked_add(1))
1332 .ok_or_else(|| {
1333 XmlDocumentError::InvalidReplacement(
1334 "content replacement length overflow".into(),
1335 )
1336 })?
1337 } else {
1338 replacement_len
1339 };
1340 view.xml()
1341 .len()
1342 .checked_sub(removed)
1343 .and_then(|length| length.checked_add(added))
1344 .ok_or_else(|| {
1345 XmlDocumentError::InvalidReplacement(
1346 "content replacement length overflow".into(),
1347 )
1348 })
1349 })
1350 }
1351
1352 pub(crate) fn projected_child_append_len(
1353 &self,
1354 target: NodeIdentity,
1355 child_len: usize,
1356 ) -> Result<usize, XmlDocumentError> {
1357 self.with_view(|view| {
1358 let node = view.resolve_mutation_node(target)?;
1359 if !node.is_element() {
1360 return Err(XmlDocumentError::TargetNotElement);
1361 }
1362 let range = node.range();
1363 let source = &view.xml()[range.clone()];
1364 let (_, qualified_name, self_closing) = element_content_range(source)?;
1365 let (removed, added) = if self_closing {
1366 let slash = source.rfind("/>").ok_or_else(|| {
1367 XmlDocumentError::InvalidReplacement(
1368 "self-closing element has no terminator".into(),
1369 )
1370 })?;
1371 let expanded = slash
1372 .checked_add(1)
1373 .and_then(|length| length.checked_add(child_len))
1374 .and_then(|length| length.checked_add(2))
1375 .and_then(|length| length.checked_add(qualified_name.len()))
1376 .and_then(|length| length.checked_add(1))
1377 .ok_or_else(|| {
1378 XmlDocumentError::InvalidReplacement("child append length overflow".into())
1379 })?;
1380 (range.len(), expanded)
1381 } else {
1382 (0, child_len)
1383 };
1384 view.xml()
1385 .len()
1386 .checked_sub(removed)
1387 .and_then(|length| length.checked_add(added))
1388 .ok_or_else(|| {
1389 XmlDocumentError::InvalidReplacement("child append length overflow".into())
1390 })
1391 })
1392 }
1393
1394 fn replace_range(
1395 &mut self,
1396 range: std::ops::Range<usize>,
1397 replacement: &str,
1398 budget: Option<&XmlParseWorkBudget>,
1399 ) -> Result<(), XmlDocumentError> {
1400 let output = self.replaced_range(range, replacement, self.settings.max_bytes)?;
1401 self.replace_serialized(output, budget)
1402 }
1403
1404 fn replace_range_with_settings(
1405 &mut self,
1406 range: std::ops::Range<usize>,
1407 replacement: &str,
1408 settings: DocumentParseSettings,
1409 budget: Option<&XmlParseWorkBudget>,
1410 ) -> Result<(), XmlDocumentError> {
1411 let output = self.replaced_range(range, replacement, settings.max_bytes)?;
1412 self.replace_serialized_with_settings(output, settings, budget)
1413 }
1414
1415 #[cfg(feature = "xmlenc")]
1416 fn prepare_range_replacement(
1417 &self,
1418 range: std::ops::Range<usize>,
1419 replacement: &str,
1420 settings: DocumentParseSettings,
1421 budget: &XmlParseWorkBudget,
1422 ) -> Result<PreparedDocumentMutation, XmlDocumentError> {
1423 let output = self.replaced_range(range, replacement, settings.max_bytes)?;
1424 let cell = build_cell(output, settings, Some(budget)).map_err(|error| match error {
1425 XmlDocumentError::Parse(ParseError::NodesLimitReached) => {
1426 XmlDocumentError::ProjectedNodeLimit {
1427 maximum: settings.nodes_limit as usize,
1428 }
1429 }
1430 error => error,
1431 })?;
1432 Ok(PreparedDocumentMutation {
1433 identity: self.identity,
1434 generation: self.generation,
1435 cell,
1436 })
1437 }
1438
1439 #[cfg(feature = "xmlenc")]
1440 pub(crate) fn commit_prepared(
1441 &mut self,
1442 prepared: PreparedDocumentMutation,
1443 ) -> Result<(), XmlDocumentError> {
1444 if self.identity != prepared.identity {
1445 return Err(XmlDocumentError::ForeignIdentity);
1446 }
1447 if self.generation != prepared.generation {
1448 return Err(XmlDocumentError::StaleIdentity {
1449 identity: prepared.generation,
1450 current: self.generation,
1451 });
1452 }
1453 self.commit_cell(prepared.cell)
1454 }
1455
1456 fn replaced_range(
1457 &self,
1458 range: std::ops::Range<usize>,
1459 replacement: &str,
1460 maximum_bytes: usize,
1461 ) -> Result<String, XmlDocumentError> {
1462 let projected = self.ensure_replacement_fits(&range, replacement.len(), maximum_bytes)?;
1463 let mut output = String::with_capacity(projected);
1464 output.push_str(&self.as_xml()[..range.start]);
1465 output.push_str(replacement);
1466 output.push_str(&self.as_xml()[range.end..]);
1467 Ok(output)
1468 }
1469
1470 fn ensure_replacement_fits(
1471 &self,
1472 range: &std::ops::Range<usize>,
1473 replacement_len: usize,
1474 maximum_bytes: usize,
1475 ) -> Result<usize, XmlDocumentError> {
1476 let projected = self
1477 .as_xml()
1478 .len()
1479 .checked_sub(range.len())
1480 .and_then(|length| length.checked_add(replacement_len))
1481 .ok_or(XmlDocumentError::DocumentTooLarge {
1482 maximum: maximum_bytes,
1483 actual: usize::MAX,
1484 })?;
1485 if projected > maximum_bytes {
1486 return Err(XmlDocumentError::DocumentTooLarge {
1487 maximum: maximum_bytes,
1488 actual: projected,
1489 });
1490 }
1491 Ok(projected)
1492 }
1493
1494 fn validate_single_element_in_parent_context(
1495 &self,
1496 target: NodeIdentity,
1497 replacement: &str,
1498 maximum: Option<usize>,
1499 settings: DocumentParseSettings,
1500 budget: Option<&XmlParseWorkBudget>,
1501 ) -> Result<(), XmlDocumentError> {
1502 let (parsed, wrapper_range) =
1503 self.parse_fragment_in_parent_context(target, replacement, maximum, settings, budget)?;
1504 parsed.with_dependent(|_, parsed| {
1505 let wrapper = validation_wrapper(parsed, wrapper_range.clone())?;
1506 if wrapper.children().filter(Node::is_element).count() != 1
1507 || wrapper.children().any(|node| {
1508 !node.is_element()
1509 && !node.is_comment()
1510 && !node.is_pi()
1511 && node.text().is_none_or(|text| !text.trim().is_empty())
1512 })
1513 {
1514 return Err(XmlDocumentError::InvalidReplacement(
1515 "element replacement must contain exactly one element".into(),
1516 ));
1517 }
1518 Ok(())
1519 })
1520 }
1521
1522 fn validate_fragment_in_parent_context(
1523 &self,
1524 target: NodeIdentity,
1525 replacement: &str,
1526 maximum: Option<usize>,
1527 settings: DocumentParseSettings,
1528 budget: Option<&XmlParseWorkBudget>,
1529 ) -> Result<(), XmlDocumentError> {
1530 self.parse_fragment_in_parent_context(target, replacement, maximum, settings, budget)
1531 .map(|_| ())
1532 }
1533
1534 fn parse_fragment_in_parent_context(
1535 &self,
1536 target: NodeIdentity,
1537 replacement: &str,
1538 maximum: Option<usize>,
1539 settings: DocumentParseSettings,
1540 budget: Option<&XmlParseWorkBudget>,
1541 ) -> Result<(DocumentCell, std::ops::Range<usize>), XmlDocumentError> {
1542 let range =
1543 self.with_view(|view| Ok::<_, XmlDocumentError>(view.resolve_node(target)?.range()))?;
1544 self.parse_wrapped_range(range, replacement, maximum, settings, budget)
1545 }
1546
1547 fn validate_content_in_element_context(
1548 &self,
1549 target: NodeIdentity,
1550 replacement: &str,
1551 maximum: Option<usize>,
1552 settings: DocumentParseSettings,
1553 budget: Option<&XmlParseWorkBudget>,
1554 ) -> Result<(), XmlDocumentError> {
1555 let (element_range, content_range, qualified_name, self_closing) =
1556 self.with_view(|view| {
1557 let target = view.resolve_node(target)?;
1558 if !target.is_element() {
1559 return Err(XmlDocumentError::TargetNotElement);
1560 }
1561 let element_range = target.range();
1562 let source = &view.xml()[element_range.clone()];
1563 let (content, qualified_name, self_closing) = element_content_range(source)?;
1564 Ok::<_, XmlDocumentError>((
1565 element_range.clone(),
1566 (element_range.start + content.start)..(element_range.start + content.end),
1567 qualified_name,
1568 self_closing,
1569 ))
1570 })?;
1571 if !self_closing {
1572 return self
1573 .parse_wrapped_range(content_range, replacement, maximum, settings, budget)
1574 .map(|_| ());
1575 }
1576
1577 let source = &self.as_xml()[element_range.clone()];
1578 let slash = source.rfind("/>").ok_or_else(|| {
1579 XmlDocumentError::InvalidReplacement("self-closing element has no terminator".into())
1580 })?;
1581 let wrapped = wrapped_fragment(replacement);
1582 let wrapper_start = element_range.start + slash + 1;
1583 let expanded = format!("{}>{wrapped}</{qualified_name}>", &source[..slash]);
1584 self.parse_wrapped_edit(
1585 element_range,
1586 &expanded,
1587 wrapper_start..(wrapper_start + wrapped.len()),
1588 maximum,
1589 settings,
1590 budget,
1591 )
1592 .map(|_| ())
1593 }
1594
1595 fn parse_wrapped_range(
1596 &self,
1597 range: std::ops::Range<usize>,
1598 replacement: &str,
1599 maximum: Option<usize>,
1600 settings: DocumentParseSettings,
1601 budget: Option<&XmlParseWorkBudget>,
1602 ) -> Result<(DocumentCell, std::ops::Range<usize>), XmlDocumentError> {
1603 let wrapped = wrapped_fragment(replacement);
1604 let wrapper_range = range.start..(range.start + wrapped.len());
1605 self.parse_wrapped_edit(range, &wrapped, wrapper_range, maximum, settings, budget)
1606 }
1607
1608 fn parse_wrapped_edit(
1609 &self,
1610 range: std::ops::Range<usize>,
1611 replacement: &str,
1612 wrapper_range: std::ops::Range<usize>,
1613 maximum: Option<usize>,
1614 settings: DocumentParseSettings,
1615 budget: Option<&XmlParseWorkBudget>,
1616 ) -> Result<(DocumentCell, std::ops::Range<usize>), XmlDocumentError> {
1617 let projected = self
1618 .as_xml()
1619 .len()
1620 .checked_sub(range.len())
1621 .and_then(|length| length.checked_add(replacement.len()))
1622 .ok_or_else(|| {
1623 XmlDocumentError::InvalidReplacement("validation length overflow".into())
1624 })?;
1625 let mut candidate = String::with_capacity(projected);
1626 candidate.push_str(&self.as_xml()[..range.start]);
1627 candidate.push_str(replacement);
1628 candidate.push_str(&self.as_xml()[range.end..]);
1629 let active_nodes_limit = maximum
1630 .map(|maximum| maximum.min(settings.nodes_limit as usize))
1631 .map(|maximum| u32::try_from(maximum).unwrap_or(u32::MAX))
1632 .unwrap_or(settings.nodes_limit);
1633 let parsed = build_cell(
1634 candidate,
1635 DocumentParseSettings {
1636 nodes_limit: active_nodes_limit.saturating_add(VALIDATION_WRAPPER_NODE_OVERHEAD),
1637 depth_limit: settings.depth_limit.saturating_add(1),
1640 max_bytes: projected,
1641 ..settings
1642 },
1643 budget,
1644 )
1645 .map_err(|error| match (maximum, error) {
1646 (Some(_), XmlDocumentError::Parse(ParseError::NodesLimitReached)) => {
1647 XmlDocumentError::ProjectedNodeLimit {
1648 maximum: active_nodes_limit as usize,
1649 }
1650 }
1651 (_, XmlDocumentError::DocumentTooDeep { actual, .. }) => {
1652 XmlDocumentError::DocumentTooDeep {
1653 maximum: settings.depth_limit,
1654 actual: actual.saturating_sub(1),
1655 }
1656 }
1657 (_, error) => error,
1658 })?;
1659 parsed.with_dependent(|_, document| {
1660 validation_wrapper(document, wrapper_range.clone()).map(|_| ())
1661 })?;
1662 Ok((parsed, wrapper_range))
1663 }
1664}
1665
1666impl<'a> DocumentView<'a> {
1667 #[must_use]
1669 pub const fn identity(self) -> DocumentIdentity {
1670 self.identity
1671 }
1672
1673 #[must_use]
1675 pub const fn generation(self) -> u64 {
1676 self.generation
1677 }
1678
1679 #[must_use]
1681 pub fn xml(self) -> &'a str {
1682 self.parsed.document.input_text()
1683 }
1684
1685 #[must_use]
1687 pub fn root(self) -> NodeIdentity {
1688 self.node_identity(self.parsed.document.root())
1689 }
1690
1691 #[must_use]
1693 pub fn root_element(self) -> NodeIdentity {
1694 self.node_identity(self.parsed.document.root_element())
1695 }
1696
1697 #[must_use]
1699 pub fn node_count(self) -> usize {
1700 self.parsed.node_count
1701 }
1702
1703 pub(crate) fn max_depth(self) -> usize {
1704 self.parsed.max_depth
1705 }
1706
1707 #[must_use]
1709 pub fn node_for_id(
1710 self,
1711 value: &str,
1712 registrations: &[IdAttributeRegistration],
1713 ) -> Option<NodeIdentity> {
1714 let mut matches = HashSet::new();
1715 match self.parsed.indexes.default_ids.get(value) {
1716 Some(Some(node)) => {
1717 matches.insert(*node);
1718 }
1719 Some(None) => return None,
1720 None => {}
1721 }
1722 for registration in registrations {
1723 let key = (
1724 registration.attribute_local_name().to_owned(),
1725 value.to_owned(),
1726 );
1727 if let Some(nodes) = self.parsed.indexes.attributes_by_value.get(&key) {
1728 matches.extend(nodes.iter().copied().filter(|node_id| {
1729 self.parsed
1730 .document
1731 .get_node(*node_id)
1732 .is_some_and(|node| registration.matches_node(node))
1733 }));
1734 }
1735 }
1736 if matches.len() == 1 {
1737 matches
1738 .iter()
1739 .next()
1740 .map(|node| self.node_identity_by_id(*node))
1741 } else {
1742 None
1743 }
1744 }
1745
1746 #[cfg(feature = "xmldsig")]
1747 pub(crate) fn id_index(
1748 self,
1749 registrations: &[IdAttributeRegistration],
1750 ) -> HashMap<String, NodeId> {
1751 let mut candidates: HashMap<String, HashSet<NodeId>> = HashMap::new();
1752 let mut ambiguous = HashSet::new();
1753 for (value, node) in &self.parsed.indexes.default_ids {
1754 if let Some(node) = node {
1755 candidates.entry(value.clone()).or_default().insert(*node);
1756 } else {
1757 ambiguous.insert(value.clone());
1758 }
1759 }
1760 for ((attribute_name, value), nodes) in &self.parsed.indexes.attributes_by_value {
1761 for registration in registrations
1762 .iter()
1763 .filter(|registration| registration.attribute_local_name() == attribute_name)
1764 {
1765 candidates
1766 .entry(value.clone())
1767 .or_default()
1768 .extend(nodes.iter().copied().filter(|node_id| {
1769 self.parsed
1770 .document
1771 .get_node(*node_id)
1772 .is_some_and(|node| registration.matches_node(node))
1773 }));
1774 }
1775 }
1776 candidates
1777 .into_iter()
1778 .filter_map(|(value, nodes)| {
1779 if nodes.len() == 1 && !ambiguous.contains(&value) {
1780 nodes.into_iter().next().map(|node| (value, node))
1781 } else {
1782 None
1783 }
1784 })
1785 .collect()
1786 }
1787
1788 pub fn document_order(self, identity: NodeIdentity) -> Result<usize, XmlDocumentError> {
1790 self.validate_identity(identity)?;
1791 self.parsed
1792 .indexes
1793 .order
1794 .get(&self.resolve_node(identity)?.id())
1795 .copied()
1796 .ok_or(XmlDocumentError::MissingNode)
1797 }
1798
1799 pub fn node_order(self, identity: NodeIdentity) -> Result<SemanticOrder, XmlDocumentError> {
1801 Ok(SemanticOrder {
1802 node: self.document_order(identity)?,
1803 phase: 0,
1804 position: 0,
1805 })
1806 }
1807
1808 pub fn attribute_order(
1810 self,
1811 identity: &AttributeIdentity,
1812 ) -> Result<SemanticOrder, XmlDocumentError> {
1813 let owner = self.resolve_node(identity.owner)?;
1814 let position = owner
1815 .attributes()
1816 .position(|attribute| {
1817 attribute.namespace() == identity.namespace.as_deref()
1818 && attribute.name() == identity.local_name
1819 })
1820 .ok_or(XmlDocumentError::MissingNode)?;
1821 Ok(SemanticOrder {
1822 node: self.document_order(identity.owner)?,
1823 phase: 2,
1824 position,
1825 })
1826 }
1827
1828 pub fn namespace_order(
1830 self,
1831 identity: &NamespaceIdentity,
1832 ) -> Result<SemanticOrder, XmlDocumentError> {
1833 let owner = self.resolve_node(identity.owner)?;
1834 let target = (identity.prefix.as_str(), identity.uri.as_str());
1835 let mut found = false;
1836 let mut position = 0;
1837 for namespace in owner.namespaces() {
1840 let candidate = (namespace.name().unwrap_or_default(), namespace.uri());
1841 match candidate.cmp(&target) {
1842 std::cmp::Ordering::Less => position += 1,
1843 std::cmp::Ordering::Equal => found = true,
1844 std::cmp::Ordering::Greater => {}
1845 }
1846 }
1847 if !found {
1848 return Err(XmlDocumentError::MissingNode);
1849 }
1850 Ok(SemanticOrder {
1851 node: self.document_order(identity.owner)?,
1852 phase: 1,
1853 position,
1854 })
1855 }
1856
1857 pub(crate) fn document(self) -> &'a Document<'a> {
1858 &self.parsed.document
1859 }
1860
1861 pub(crate) fn node_identity(self, node: Node<'_, '_>) -> NodeIdentity {
1862 NodeIdentity {
1863 document: self.identity,
1864 generation: self.generation,
1865 semantic_id: SemanticDocument::node_id(&self.parsed.document, node),
1866 }
1867 }
1868
1869 pub(crate) fn node_identity_by_id(self, backend: NodeId) -> NodeIdentity {
1870 let node = self
1871 .parsed
1872 .document
1873 .get_node(backend)
1874 .expect("indexed backend node must remain in the retained document");
1875 self.node_identity(node)
1876 }
1877
1878 pub(crate) fn resolve_node(
1879 self,
1880 identity: NodeIdentity,
1881 ) -> Result<Node<'a, 'a>, XmlDocumentError> {
1882 self.validate_identity(identity)?;
1883 SemanticDocument::node(&self.parsed.document, identity.semantic_id)
1884 .ok_or(XmlDocumentError::MissingNode)
1885 }
1886
1887 fn resolve_mutation_node(
1888 self,
1889 identity: NodeIdentity,
1890 ) -> Result<Node<'a, 'a>, XmlDocumentError> {
1891 let node = self.resolve_node(identity)?;
1892 if !node.has_actionable_range() {
1893 return Err(XmlDocumentError::EntityExpandedMutationTarget);
1894 }
1895 Ok(node)
1896 }
1897
1898 pub fn attribute_identity(
1900 self,
1901 owner: NodeIdentity,
1902 namespace: Option<&str>,
1903 local_name: &str,
1904 ) -> Result<AttributeIdentity, XmlDocumentError> {
1905 let node = self.resolve_node(owner)?;
1906 if !node.is_element()
1907 || !node.attributes().any(|attribute| {
1908 attribute.namespace() == namespace && attribute.name() == local_name
1909 })
1910 {
1911 return Err(XmlDocumentError::MissingNode);
1912 }
1913 Ok(AttributeIdentity {
1914 owner,
1915 namespace: namespace.map(str::to_owned),
1916 local_name: local_name.to_owned(),
1917 })
1918 }
1919
1920 pub fn namespace_identities(
1922 self,
1923 owner: NodeIdentity,
1924 ) -> Result<Vec<NamespaceIdentity>, XmlDocumentError> {
1925 let node = self.resolve_node(owner)?;
1926 if !node.is_element() {
1927 return Err(XmlDocumentError::MissingNode);
1928 }
1929 let mut namespaces = node
1934 .namespaces()
1935 .map(|namespace| NamespaceIdentity {
1936 owner,
1937 prefix: namespace.name().unwrap_or_default().to_owned(),
1938 uri: namespace.uri().to_owned(),
1939 })
1940 .collect::<Vec<_>>();
1941 namespaces
1942 .sort_by(|left, right| (&left.prefix, &left.uri).cmp(&(&right.prefix, &right.uri)));
1943 Ok(namespaces)
1944 }
1945
1946 fn validate_identity(self, identity: NodeIdentity) -> Result<(), XmlDocumentError> {
1947 if identity.document != self.identity {
1948 return Err(XmlDocumentError::ForeignIdentity);
1949 }
1950 if identity.generation != self.generation {
1951 return Err(XmlDocumentError::StaleIdentity {
1952 identity: identity.generation,
1953 current: self.generation,
1954 });
1955 }
1956 Ok(())
1957 }
1958}
1959
1960fn build_cell(
1961 xml: String,
1962 settings: DocumentParseSettings,
1963 budget: Option<&XmlParseWorkBudget>,
1964) -> Result<DocumentCell, XmlDocumentError> {
1965 preflight_document_limits(&xml, settings, budget)?;
1966 build_cell_after_preflight(xml, settings, budget)
1967}
1968
1969fn build_cell_after_preflight(
1970 xml: String,
1971 settings: DocumentParseSettings,
1972 budget: Option<&XmlParseWorkBudget>,
1973) -> Result<DocumentCell, XmlDocumentError> {
1974 charge_semantic_parser_work(budget, xml.len())?;
1975 build_semantic_cell(xml, settings)
1976}
1977
1978fn build_semantic_cell(
1979 xml: String,
1980 settings: DocumentParseSettings,
1981) -> Result<DocumentCell, XmlDocumentError> {
1982 DocumentCell::try_new(xml, |source| {
1983 let (document, metrics) = parse_semantic_document(source, settings)?;
1984 let indexes = DocumentIndexes::build(&document);
1985 Ok::<_, XmlDocumentError>(ParsedDocument {
1986 document,
1987 indexes,
1988 node_count: metrics.node_count,
1989 max_depth: metrics.max_depth,
1990 })
1991 })
1992}
1993
1994pub(crate) fn parse_borrowed_with_settings_and_budget<'a>(
1995 xml: &'a str,
1996 settings: DocumentParseSettings,
1997 budget: Option<&XmlParseWorkBudget>,
1998) -> Result<Document<'a>, XmlDocumentError> {
1999 preflight_document_limits(xml, settings, budget)?;
2000 charge_semantic_parser_work(budget, xml.len())?;
2001 let (document, _) = parse_semantic_document(xml, settings)?;
2002 Ok(document)
2003}
2004
2005fn preflight_document_limits(
2006 xml: &str,
2007 settings: DocumentParseSettings,
2008 budget: Option<&XmlParseWorkBudget>,
2009) -> Result<(), XmlDocumentError> {
2010 if xml.len() > settings.max_bytes {
2013 return Err(XmlDocumentError::DocumentTooLarge {
2014 maximum: settings.max_bytes,
2015 actual: xml.len(),
2016 });
2017 }
2018 charge_parse_work(budget, xml.len())?;
2019 let mut dtd = InternalDtd::default();
2020 let mut expansion_stack = HashSet::new();
2021 let mut state = DocumentPreflightState::default();
2022 preflight_xml_fragment(
2023 xml,
2024 settings,
2025 &mut dtd,
2026 &mut expansion_stack,
2027 &mut state,
2028 budget,
2029 true,
2030 )
2031}
2032
2033#[derive(Default)]
2034struct DocumentPreflightState {
2035 depth: usize,
2036 nodes: u32,
2037 in_character_data: bool,
2038 entity_expansions: u32,
2039 entity_expansion_work: usize,
2040}
2041
2042#[derive(Default)]
2043struct InternalDtd {
2044 entities: HashMap<String, String>,
2045 attribute_defaults: HashMap<String, Vec<InternalAttributeDefault>>,
2046}
2047
2048struct InternalAttributeDefault {
2049 attribute_name: String,
2050 value: String,
2051}
2052
2053fn preflight_xml_fragment(
2054 xml: &str,
2055 settings: DocumentParseSettings,
2056 dtd: &mut InternalDtd,
2057 expansion_stack: &mut HashSet<String>,
2058 state: &mut DocumentPreflightState,
2059 budget: Option<&XmlParseWorkBudget>,
2060 collect_doctype: bool,
2061) -> Result<(), XmlDocumentError> {
2062 enum FragmentSource {
2063 Document,
2064 Entity(String),
2065 }
2066
2067 #[derive(Clone, Copy, PartialEq, Eq)]
2068 enum FragmentContext {
2069 Content,
2070 Attribute,
2071 }
2072
2073 struct FragmentFrame {
2074 source: FragmentSource,
2075 offset: usize,
2076 collect_doctype: bool,
2077 context: FragmentContext,
2078 pending_attribute_source: Vec<u8>,
2079 pending_attribute_offset: usize,
2080 }
2081
2082 enum PreflightEvent {
2083 DocType(Option<String>),
2084 GeneralRef {
2085 name: Option<String>,
2086 is_character_reference: bool,
2087 },
2088 CharacterData {
2089 xml_whitespace: bool,
2090 },
2091 Start(Vec<u8>),
2092 Empty(Vec<u8>),
2093 End,
2094 Node,
2095 Other,
2096 Done,
2097 }
2098
2099 if state.nodes == 0 {
2100 state.nodes = 1;
2102 }
2103 let mut fragments = vec![FragmentFrame {
2104 source: FragmentSource::Document,
2105 offset: 0,
2106 collect_doctype,
2107 context: FragmentContext::Content,
2108 pending_attribute_source: Vec::new(),
2109 pending_attribute_offset: 0,
2110 }];
2111 while !fragments.is_empty() {
2114 let (event, collect_doctype, context) = {
2115 let frame = fragments.last_mut().expect("fragment stack is not empty");
2116 let mut pending = general_references(
2117 &frame.pending_attribute_source[frame.pending_attribute_offset..],
2118 );
2119 if let Some(name) = pending.next() {
2120 frame.pending_attribute_offset = frame
2121 .pending_attribute_offset
2122 .saturating_add(pending.consumed());
2123 (
2124 PreflightEvent::GeneralRef {
2125 name: Some(name.to_owned()),
2126 is_character_reference: false,
2127 },
2128 false,
2129 FragmentContext::Attribute,
2130 )
2131 } else {
2132 frame.pending_attribute_source.clear();
2133 frame.pending_attribute_offset = 0;
2134 let source = match &frame.source {
2135 FragmentSource::Document => xml,
2136 FragmentSource::Entity(name) => dtd
2137 .entities
2138 .get(name)
2139 .expect("active entity replacement remains registered"),
2140 };
2141 let remaining = &source[frame.offset..];
2142 let mut reader = QuickXmlReader::from_str(remaining);
2143 reader.config_mut().check_end_names = false;
2146 reader.config_mut().allow_unmatched_ends = true;
2150 let event = match reader.read_event() {
2151 Ok(QuickXmlEvent::DocType(doctype)) => PreflightEvent::DocType(
2152 doctype.decode().ok().map(|value| value.into_owned()),
2153 ),
2154 Ok(QuickXmlEvent::GeneralRef(reference)) => {
2155 let name = reference.decode().ok().map(|value| value.into_owned());
2156 let is_character_reference =
2157 reference.resolve_char_ref().ok().flatten().is_some()
2158 || name.as_deref().is_some_and(|name| {
2159 matches!(name, "amp" | "apos" | "gt" | "lt" | "quot")
2160 });
2161 PreflightEvent::GeneralRef {
2162 name,
2163 is_character_reference,
2164 }
2165 }
2166 Ok(QuickXmlEvent::Text(text)) => PreflightEvent::CharacterData {
2167 xml_whitespace: text
2168 .as_ref()
2169 .iter()
2170 .all(|byte| matches!(byte, b' ' | b'\t' | b'\r' | b'\n')),
2171 },
2172 Ok(QuickXmlEvent::CData(_)) => PreflightEvent::CharacterData {
2173 xml_whitespace: false,
2174 },
2175 Ok(QuickXmlEvent::Start(element)) => {
2176 let source = if frame.context == FragmentContext::Content {
2177 element_attribute_reference_source(&element, dtd)
2178 } else {
2179 Vec::new()
2180 };
2181 PreflightEvent::Start(source)
2182 }
2183 Ok(QuickXmlEvent::Empty(element)) => {
2184 let source = if frame.context == FragmentContext::Content {
2185 element_attribute_reference_source(&element, dtd)
2186 } else {
2187 Vec::new()
2188 };
2189 PreflightEvent::Empty(source)
2190 }
2191 Ok(QuickXmlEvent::End(_)) => PreflightEvent::End,
2192 Ok(QuickXmlEvent::Comment(_) | QuickXmlEvent::PI(_)) => PreflightEvent::Node,
2193 Ok(QuickXmlEvent::Eof) | Err(_) => PreflightEvent::Done,
2194 Ok(_) => PreflightEvent::Other,
2195 };
2196 frame.offset = frame.offset.saturating_add(
2197 usize::try_from(reader.buffer_position()).unwrap_or(remaining.len()),
2198 );
2199 (event, frame.collect_doctype, frame.context)
2200 }
2201 };
2202
2203 match event {
2204 PreflightEvent::DocType(doctype) => {
2205 if collect_doctype
2206 && settings.allow_dtd
2207 && let Some(doctype) = doctype
2208 {
2209 collect_internal_dtd(&doctype, dtd);
2210 }
2211 state.in_character_data = false;
2212 continue;
2213 }
2214 PreflightEvent::GeneralRef {
2215 name,
2216 is_character_reference,
2217 } => {
2218 let Some(name) = name else {
2219 continue;
2220 };
2221 if is_character_reference {
2222 if context == FragmentContext::Content {
2223 observe_preflight_node(state, settings, true)?;
2224 }
2225 continue;
2226 }
2227 if expansion_stack.insert(name.clone()) {
2228 if let Some(replacement) = dtd.entities.get(&name) {
2229 state.entity_expansions = state.entity_expansions.saturating_add(1);
2230 let maximum = crate::hard_limits::XML_ENTITY_EXPANSION_CEILING;
2231 if state.entity_expansions > maximum {
2232 return Err(XmlDocumentError::Parse(
2233 ParseError::EntityExpansionLimitReached {
2234 maximum,
2235 actual: state.entity_expansions,
2236 },
2237 ));
2238 }
2239 charge_entity_expansion_work(state, budget, replacement.len())?;
2240 fragments.push(FragmentFrame {
2241 source: FragmentSource::Entity(name),
2242 offset: 0,
2243 collect_doctype: false,
2244 context,
2245 pending_attribute_source: Vec::new(),
2246 pending_attribute_offset: 0,
2247 });
2248 } else {
2249 expansion_stack.remove(&name);
2250 }
2251 }
2252 continue;
2253 }
2254 PreflightEvent::Done => {
2255 let frame = fragments.pop().expect("fragment stack is not empty");
2256 if let FragmentSource::Entity(name) = frame.source {
2257 expansion_stack.remove(&name);
2258 }
2259 continue;
2260 }
2261 PreflightEvent::CharacterData { xml_whitespace } => {
2262 if context == FragmentContext::Attribute {
2263 continue;
2264 }
2265 if state.depth == 0 && xml_whitespace {
2266 state.in_character_data = false;
2267 continue;
2268 }
2269 let starts_semantic_node = !state.in_character_data;
2270 state.in_character_data = true;
2271 if starts_semantic_node {
2272 observe_preflight_node(state, settings, false)?;
2273 }
2274 continue;
2275 }
2276 PreflightEvent::Node => {
2277 state.in_character_data = false;
2278 observe_preflight_node(state, settings, false)?;
2279 continue;
2280 }
2281 PreflightEvent::Other => {
2282 state.in_character_data = false;
2283 continue;
2284 }
2285 PreflightEvent::Start(_) | PreflightEvent::Empty(_) | PreflightEvent::End => {}
2286 }
2287
2288 if context == FragmentContext::Attribute {
2289 continue;
2290 }
2291 state.in_character_data = false;
2292 match event {
2293 PreflightEvent::Start(attribute_source) => {
2294 let frame = fragments
2295 .last_mut()
2296 .expect("active element frame remains registered");
2297 frame.pending_attribute_source = attribute_source;
2298 frame.pending_attribute_offset = 0;
2299 observe_preflight_node(state, settings, false)?;
2300 state.depth = state.depth.saturating_add(1);
2301 if state.depth > settings.depth_limit {
2302 return Err(XmlDocumentError::DocumentTooDeep {
2303 maximum: settings.depth_limit,
2304 actual: state.depth,
2305 });
2306 }
2307 }
2308 PreflightEvent::Empty(attribute_source) => {
2309 let frame = fragments
2310 .last_mut()
2311 .expect("active element frame remains registered");
2312 frame.pending_attribute_source = attribute_source;
2313 frame.pending_attribute_offset = 0;
2314 observe_preflight_node(state, settings, false)?;
2315 let actual = state.depth.saturating_add(1);
2316 if actual > settings.depth_limit {
2317 return Err(XmlDocumentError::DocumentTooDeep {
2318 maximum: settings.depth_limit,
2319 actual,
2320 });
2321 }
2322 }
2323 PreflightEvent::End => state.depth = state.depth.saturating_sub(1),
2324 _ => unreachable!("non-structural events continue above"),
2325 }
2326 }
2327 Ok(())
2328}
2329
2330fn element_attribute_reference_source(
2331 element: &QuickXmlBytesStart<'_>,
2332 dtd: &InternalDtd,
2333) -> Vec<u8> {
2334 let lexical = element.as_ref();
2335 let mut source = if lexical.contains(&b'&') {
2336 lexical.to_vec()
2337 } else {
2338 Vec::new()
2339 };
2340 if dtd.attribute_defaults.is_empty() {
2341 return source;
2342 }
2343
2344 let element_name = element.name();
2345 let Ok(element_name) = std::str::from_utf8(element_name.as_ref()) else {
2346 return source;
2347 };
2348 if let Some(defaults) = dtd.attribute_defaults.get(element_name) {
2349 let mut present_attributes: HashSet<_> = element
2350 .attributes()
2351 .flatten()
2352 .filter_map(|attribute| {
2353 std::str::from_utf8(attribute.key.as_ref())
2354 .ok()
2355 .map(ToOwned::to_owned)
2356 })
2357 .collect();
2358 for default in defaults {
2359 if present_attributes.insert(default.attribute_name.clone())
2360 && default.value.contains('&')
2361 {
2362 source.push(b' ');
2363 source.extend_from_slice(default.value.as_bytes());
2364 }
2365 }
2366 }
2367 source
2368}
2369
2370struct GeneralReferences<'a> {
2371 value: &'a [u8],
2372 offset: usize,
2373}
2374
2375impl GeneralReferences<'_> {
2376 fn consumed(&self) -> usize {
2377 self.offset
2378 }
2379}
2380
2381impl<'a> Iterator for GeneralReferences<'a> {
2382 type Item = &'a str;
2383
2384 fn next(&mut self) -> Option<Self::Item> {
2385 loop {
2386 let relative_start = self.value[self.offset..]
2387 .iter()
2388 .position(|byte| *byte == b'&')?;
2389 let start = self.offset + relative_start + 1;
2390 let Some(relative_end) = self.value[start..].iter().position(|byte| *byte == b';')
2391 else {
2392 self.offset = self.value.len();
2393 return None;
2394 };
2395 let end = start + relative_end;
2396 self.offset = end + 1;
2397 let name = &self.value[start..end];
2398 if !name.starts_with(b"#")
2399 && !matches!(name, b"amp" | b"apos" | b"gt" | b"lt" | b"quot")
2400 && let Ok(name) = std::str::from_utf8(name)
2401 {
2402 return Some(name);
2403 }
2404 }
2405 }
2406}
2407
2408fn general_references(value: &[u8]) -> GeneralReferences<'_> {
2409 GeneralReferences { value, offset: 0 }
2410}
2411
2412fn charge_entity_expansion_work(
2413 state: &mut DocumentPreflightState,
2414 budget: Option<&XmlParseWorkBudget>,
2415 bytes: usize,
2416) -> Result<(), XmlDocumentError> {
2417 charge_parse_work(budget, bytes)?;
2418 if budget.is_none() {
2419 let actual = state.entity_expansion_work.saturating_add(bytes);
2420 let maximum = crate::hard_limits::XML_ENTITY_EXPANSION_WORK_BYTE_CEILING;
2421 if actual > maximum {
2422 return Err(XmlDocumentError::Parse(
2423 ParseError::EntityExpansionWorkLimitReached { maximum, actual },
2424 ));
2425 }
2426 state.entity_expansion_work = actual;
2427 }
2428 Ok(())
2429}
2430
2431pub(crate) fn preflight_dom_limits(
2432 xml: &str,
2433 options: ParsingOptions,
2434) -> Result<ParsingOptions, ParseError> {
2435 let effective = ParsingOptions {
2436 allow_dtd: options.allow_dtd,
2437 nodes_limit: options
2438 .nodes_limit
2439 .min(crate::hard_limits::XML_DOCUMENT_NODE_CEILING),
2440 };
2441 let settings = DocumentParseSettings {
2442 backend: XmlBackend::default(),
2443 allow_dtd: effective.allow_dtd,
2444 nodes_limit: effective.nodes_limit,
2445 depth_limit: crate::hard_limits::XML_DOCUMENT_DEPTH_CEILING,
2446 max_bytes: crate::hard_limits::XML_DOCUMENT_BYTE_CEILING,
2447 };
2448 match preflight_document_limits(xml, settings, None) {
2449 Ok(()) => Ok(effective),
2450 Err(XmlDocumentError::Parse(error)) => Err(error),
2451 Err(XmlDocumentError::DocumentTooDeep { maximum, actual }) => {
2452 Err(ParseError::DepthLimitReached { maximum, actual })
2453 }
2454 Err(XmlDocumentError::DocumentTooLarge { maximum, actual }) => {
2455 Err(ParseError::ByteLimitReached { maximum, actual })
2456 }
2457 Err(error) => Err(ParseError::Backend {
2458 backend: "xml-limit-preflight",
2459 message: error.to_string(),
2460 }),
2461 }
2462}
2463
2464fn observe_preflight_node(
2465 state: &mut DocumentPreflightState,
2466 settings: DocumentParseSettings,
2467 character_data: bool,
2468) -> Result<(), XmlDocumentError> {
2469 if character_data {
2470 if state.in_character_data {
2471 return Ok(());
2472 }
2473 state.in_character_data = true;
2474 }
2475 state.nodes = state.nodes.saturating_add(1);
2476 if state.nodes > settings.nodes_limit {
2477 return Err(XmlDocumentError::Parse(ParseError::NodesLimitReached));
2478 }
2479 Ok(())
2480}
2481
2482fn collect_internal_dtd(doctype: &str, dtd: &mut InternalDtd) {
2483 let Some(subset_start) = find_unquoted_byte(doctype.as_bytes(), b'[', 0) else {
2484 return;
2485 };
2486 let Some(subset_end) = doctype.rfind(']') else {
2487 return;
2488 };
2489 let subset = &doctype[subset_start + 1..subset_end];
2490 let mut offset = 0;
2491 let bytes = subset.as_bytes();
2492 while offset < bytes.len() {
2493 if bytes[offset..].starts_with(b"<!--") {
2494 let Some(end) = find_bytes(&bytes[offset + 4..], b"-->") else {
2495 break;
2496 };
2497 offset += 4 + end + 3;
2498 } else if bytes[offset..].starts_with(b"<?") {
2499 let Some(end) = find_bytes(&bytes[offset + 2..], b"?>") else {
2500 break;
2501 };
2502 offset += 2 + end + 2;
2503 } else if bytes[offset..].starts_with(b"<!ENTITY") {
2504 let declaration_start = offset + "<!ENTITY".len();
2505 let Some(declaration_end) = find_unquoted_byte(bytes, b'>', declaration_start) else {
2506 break;
2507 };
2508 let declaration = &subset[declaration_start..declaration_end];
2509 if let Some((name, value)) = parse_internal_general_entity(declaration) {
2510 dtd.entities
2512 .entry(name.to_owned())
2513 .or_insert_with(|| normalize_internal_entity_value(value));
2514 }
2515 offset = declaration_end + 1;
2516 } else if bytes[offset..].starts_with(b"<!ATTLIST") {
2517 let declaration_start = offset + "<!ATTLIST".len();
2518 let Some(declaration_end) = find_unquoted_byte(bytes, b'>', declaration_start) else {
2519 break;
2520 };
2521 collect_internal_attribute_defaults(
2524 &subset[declaration_start..declaration_end],
2525 &mut dtd.attribute_defaults,
2526 );
2527 offset = declaration_end + 1;
2528 } else if bytes[offset..].starts_with(b"<!") {
2529 let Some(declaration_end) = find_unquoted_byte(bytes, b'>', offset + 2) else {
2530 break;
2531 };
2532 offset = declaration_end + 1;
2533 } else {
2534 offset += 1;
2535 }
2536 }
2537}
2538
2539fn collect_internal_attribute_defaults(
2540 declaration: &str,
2541 defaults: &mut HashMap<String, Vec<InternalAttributeDefault>>,
2542) {
2543 let bytes = declaration.as_bytes();
2544 let mut offset = 0;
2545 skip_dtd_whitespace(bytes, &mut offset);
2546 let Some(element_name) = consume_dtd_token(declaration, &mut offset) else {
2547 return;
2548 };
2549 let mut declarations = Vec::new();
2550 loop {
2551 skip_dtd_whitespace(bytes, &mut offset);
2552 if offset == bytes.len() {
2553 break;
2554 }
2555 let Some(attribute_name) = consume_dtd_token(declaration, &mut offset) else {
2556 break;
2557 };
2558 skip_dtd_whitespace(bytes, &mut offset);
2559 if !consume_attribute_type(declaration, &mut offset) {
2560 break;
2561 }
2562 skip_dtd_whitespace(bytes, &mut offset);
2563
2564 if consume_dtd_keyword(declaration, &mut offset, "#REQUIRED")
2565 || consume_dtd_keyword(declaration, &mut offset, "#IMPLIED")
2566 {
2567 continue;
2568 }
2569 if consume_dtd_keyword(declaration, &mut offset, "#FIXED") {
2570 skip_dtd_whitespace(bytes, &mut offset);
2571 }
2572 let Some(value) = consume_dtd_quoted_value(declaration, &mut offset) else {
2573 break;
2574 };
2575 declarations.push(InternalAttributeDefault {
2576 attribute_name: attribute_name.to_owned(),
2577 value: value.to_owned(),
2578 });
2579 }
2580
2581 if !declarations.is_empty() {
2582 defaults
2583 .entry(element_name.to_owned())
2584 .or_default()
2585 .extend(declarations);
2586 }
2587}
2588
2589fn skip_dtd_whitespace(bytes: &[u8], offset: &mut usize) {
2590 while bytes
2591 .get(*offset)
2592 .is_some_and(|byte| matches!(byte, b' ' | b'\t' | b'\r' | b'\n'))
2593 {
2594 *offset += 1;
2595 }
2596}
2597
2598fn consume_dtd_token<'a>(input: &'a str, offset: &mut usize) -> Option<&'a str> {
2599 let bytes = input.as_bytes();
2600 let start = *offset;
2601 while bytes.get(*offset).is_some_and(|byte| {
2602 !matches!(
2603 byte,
2604 b' ' | b'\t' | b'\r' | b'\n' | b'(' | b')' | b'\'' | b'"'
2605 )
2606 }) {
2607 *offset += 1;
2608 }
2609 (start != *offset).then(|| &input[start..*offset])
2610}
2611
2612fn consume_attribute_type(input: &str, offset: &mut usize) -> bool {
2613 let bytes = input.as_bytes();
2614 if consume_dtd_keyword(input, offset, "NOTATION") {
2615 skip_dtd_whitespace(bytes, offset);
2616 return consume_parenthesized_dtd_group(bytes, offset);
2617 }
2618 if bytes.get(*offset) == Some(&b'(') {
2619 return consume_parenthesized_dtd_group(bytes, offset);
2620 }
2621 consume_dtd_token(input, offset).is_some()
2622}
2623
2624fn consume_parenthesized_dtd_group(bytes: &[u8], offset: &mut usize) -> bool {
2625 if bytes.get(*offset) != Some(&b'(') {
2626 return false;
2627 }
2628 let mut depth = 0usize;
2629 while let Some(byte) = bytes.get(*offset).copied() {
2630 *offset += 1;
2631 match byte {
2632 b'(' => depth += 1,
2633 b')' => {
2634 depth -= 1;
2635 if depth == 0 {
2636 return true;
2637 }
2638 }
2639 _ => {}
2640 }
2641 }
2642 false
2643}
2644
2645fn consume_dtd_keyword(input: &str, offset: &mut usize, keyword: &str) -> bool {
2646 let remaining = &input[*offset..];
2647 if !remaining.starts_with(keyword) {
2648 return false;
2649 }
2650 let end = *offset + keyword.len();
2651 if input
2652 .as_bytes()
2653 .get(end)
2654 .is_some_and(|byte| !matches!(byte, b' ' | b'\t' | b'\r' | b'\n' | b'(' | b'\'' | b'"'))
2655 {
2656 return false;
2657 }
2658 *offset = end;
2659 true
2660}
2661
2662fn consume_dtd_quoted_value<'a>(input: &'a str, offset: &mut usize) -> Option<&'a str> {
2663 let bytes = input.as_bytes();
2664 let quote = *bytes.get(*offset)?;
2665 if !matches!(quote, b'\'' | b'"') {
2666 return None;
2667 }
2668 let start = *offset + 1;
2669 let relative_end = bytes[start..].iter().position(|byte| *byte == quote)?;
2670 let end = start + relative_end;
2671 *offset = end + 1;
2672 Some(&input[start..end])
2673}
2674
2675fn find_bytes(haystack: &[u8], needle: &[u8]) -> Option<usize> {
2676 haystack
2677 .windows(needle.len())
2678 .position(|window| window == needle)
2679}
2680
2681fn parse_internal_general_entity(declaration: &str) -> Option<(&str, &str)> {
2682 let declaration = declaration.trim_start();
2683 if declaration.starts_with('%') {
2684 return None;
2685 }
2686 let name_end = declaration.find(char::is_whitespace)?;
2687 let name = &declaration[..name_end];
2688 let definition = declaration[name_end..].trim_start();
2689 let quote = definition.as_bytes().first().copied()?;
2690 if !matches!(quote, b'\'' | b'"') {
2691 return None;
2694 }
2695 let value_end = definition.as_bytes()[1..]
2696 .iter()
2697 .position(|byte| *byte == quote)?
2698 + 1;
2699 Some((name, &definition[1..value_end]))
2700}
2701
2702fn normalize_internal_entity_value(value: &str) -> String {
2703 let mut normalized = String::with_capacity(value.len());
2707 let mut offset = 0;
2708 while let Some(relative_start) = value[offset..].find("&#") {
2709 let start = offset + relative_start;
2710 normalized.push_str(&value[offset..start]);
2711 let Some(relative_end) = value[start + 2..].find(';') else {
2712 normalized.push_str(&value[start..]);
2713 return normalized;
2714 };
2715 let end = start + 2 + relative_end;
2716 let digits = &value[start + 2..end];
2717 let (radix, digits) = digits
2718 .strip_prefix('x')
2719 .map_or((10, digits), |digits| (16, digits));
2720 let replacement = u32::from_str_radix(digits, radix)
2721 .ok()
2722 .filter(|codepoint| is_xml_character(*codepoint))
2723 .and_then(char::from_u32);
2724 if let Some(replacement) = replacement {
2725 normalized.push(replacement);
2726 } else {
2727 normalized.push_str(&value[start..=end]);
2728 }
2729 offset = end + 1;
2730 }
2731 normalized.push_str(&value[offset..]);
2732 normalized
2733}
2734
2735const fn is_xml_character(codepoint: u32) -> bool {
2736 matches!(
2737 codepoint,
2738 0x9 | 0xA | 0xD | 0x20..=0xD7FF | 0xE000..=0xFFFD | 0x10000..=0x10FFFF
2739 )
2740}
2741
2742fn find_unquoted_byte(bytes: &[u8], needle: u8, start: usize) -> Option<usize> {
2743 let mut quote = None;
2744 for (index, byte) in bytes.iter().copied().enumerate().skip(start) {
2745 match byte {
2746 b'\'' | b'"' => match quote {
2747 Some(active) if active == byte => quote = None,
2748 None => quote = Some(byte),
2749 Some(_) => {}
2750 },
2751 _ if quote.is_none() && byte == needle => return Some(index),
2752 _ => {}
2753 }
2754 }
2755 None
2756}
2757
2758fn parse_semantic_document<'a>(
2759 source: &'a str,
2760 settings: DocumentParseSettings,
2761) -> Result<(Document<'a>, DocumentMetrics), XmlDocumentError> {
2762 let document = Document::parse_after_limit_preflight_with_backend(
2763 source,
2764 ParsingOptions {
2765 allow_dtd: settings.allow_dtd,
2766 nodes_limit: settings.nodes_limit,
2767 },
2768 settings.backend,
2769 )
2770 .map_err(XmlDocumentError::Parse)?;
2771 let metrics = validate_document_metrics(&document, settings.depth_limit)?;
2772 Ok((document, metrics))
2773}
2774
2775fn validate_document_metrics(
2776 document: &Document<'_>,
2777 maximum: usize,
2778) -> Result<DocumentMetrics, XmlDocumentError> {
2779 let mut depths = Vec::new();
2783 let mut node_count = 0;
2784 let mut max_depth = 0;
2785 for node in document.descendants() {
2786 node_count += 1;
2787 let parent_depth = node
2788 .parent()
2789 .and_then(|parent| depths.get(parent.id().get_usize()))
2790 .copied()
2791 .unwrap_or(0);
2792 let depth = parent_depth + usize::from(node.is_element());
2793 let node_index = node.id().get_usize();
2794 if depths.len() <= node_index {
2795 depths.resize(node_index + 1, 0);
2796 }
2797 depths[node_index] = depth;
2798 max_depth = max_depth.max(depth);
2799 if depth > maximum {
2800 return Err(XmlDocumentError::DocumentTooDeep {
2801 maximum,
2802 actual: depth,
2803 });
2804 }
2805 }
2806 Ok(DocumentMetrics {
2807 node_count,
2808 max_depth,
2809 })
2810}
2811
2812fn own_bounded_xml(
2813 xml: impl AsRef<str> + Into<String>,
2814 maximum: usize,
2815) -> Result<String, XmlDocumentError> {
2816 let actual = xml.as_ref().len();
2817 if actual > maximum {
2818 return Err(XmlDocumentError::DocumentTooLarge { maximum, actual });
2819 }
2820 Ok(xml.into())
2821}
2822
2823fn allocate_document_identity(counter: &AtomicU64) -> Result<DocumentIdentity, XmlDocumentError> {
2824 let mut current = counter.load(Ordering::Relaxed);
2825 loop {
2826 let next = current
2827 .checked_add(1)
2828 .ok_or(XmlDocumentError::IdentityExhausted)?;
2829 match counter.compare_exchange_weak(current, next, Ordering::Relaxed, Ordering::Relaxed) {
2830 Ok(_) => return Ok(DocumentIdentity(current)),
2831 Err(observed) => current = observed,
2832 }
2833 }
2834}
2835
2836#[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
2837fn document_requires_internal_dtd(
2838 xml: &str,
2839 settings: DocumentParseSettings,
2840 budget: Option<&XmlParseWorkBudget>,
2841) -> Result<bool, XmlDocumentError> {
2842 if !settings.allow_dtd {
2846 return Ok(false);
2847 }
2848 charge_semantic_parser_work(budget, xml.len())?;
2849 Ok(Document::parse_after_limit_preflight_with_backend(
2850 xml,
2851 ParsingOptions {
2852 allow_dtd: false,
2853 nodes_limit: settings.nodes_limit,
2854 },
2855 settings.backend,
2856 )
2857 .is_err())
2858}
2859
2860fn wrapped_fragment(replacement: &str) -> String {
2861 format!("{VALIDATION_WRAPPER_OPEN}{replacement}{VALIDATION_WRAPPER_CLOSE}")
2862}
2863
2864fn validation_wrapper<'a, 'input>(
2865 parsed: &'a ParsedDocument<'input>,
2866 expected_range: std::ops::Range<usize>,
2867) -> Result<Node<'a, 'input>, XmlDocumentError> {
2868 parsed
2869 .document
2870 .descendants()
2871 .find(|node| {
2872 node.has_tag_name((VALIDATION_WRAPPER_NS, "wrapper")) && node.range() == expected_range
2873 })
2874 .ok_or_else(|| {
2875 XmlDocumentError::InvalidReplacement(
2876 "replacement escaped its structural validation boundary".into(),
2877 )
2878 })
2879}
2880
2881fn validate_wrappers(
2882 parsed: &ParsedDocument<'_>,
2883 expected_ranges: &[std::ops::Range<usize>],
2884) -> Result<(), XmlDocumentError> {
2885 let mut remaining = expected_ranges
2886 .iter()
2887 .map(|range| (range.start, range.end))
2888 .collect::<HashSet<_>>();
2889 for node in parsed
2890 .document
2891 .descendants()
2892 .filter(|node| node.has_tag_name((VALIDATION_WRAPPER_NS, "wrapper")))
2893 {
2894 let range = node.range();
2895 remaining.remove(&(range.start, range.end));
2896 }
2897 if remaining.is_empty() {
2898 Ok(())
2899 } else {
2900 Err(XmlDocumentError::InvalidReplacement(
2901 "replacement escaped its structural validation boundary".into(),
2902 ))
2903 }
2904}
2905
2906impl DocumentIndexes {
2907 fn build(document: &Document<'_>) -> Self {
2908 let mut order = HashMap::new();
2909 let mut default_ids = HashMap::new();
2910 let mut duplicate_ids = HashSet::new();
2911 let mut attributes_by_value: HashMap<(String, String), Vec<NodeId>> = HashMap::new();
2912
2913 for (position, node) in document.descendants().enumerate() {
2914 order.insert(node.id(), position);
2915 if !node.is_element() {
2916 continue;
2917 }
2918 for attribute in node.attributes() {
2919 attributes_by_value
2920 .entry((attribute.name().to_owned(), attribute.value().to_owned()))
2921 .or_default()
2922 .push(node.id());
2923 if !matches!(attribute.name(), "ID" | "Id" | "id")
2924 || duplicate_ids.contains(attribute.value())
2925 {
2926 continue;
2927 }
2928 match default_ids.entry(attribute.value().to_owned()) {
2929 Entry::Vacant(entry) => {
2930 entry.insert(Some(node.id()));
2931 }
2932 Entry::Occupied(mut entry) if entry.get() != &Some(node.id()) => {
2933 entry.insert(None);
2934 duplicate_ids.insert(attribute.value().to_owned());
2935 }
2936 Entry::Occupied(_) => {}
2937 }
2938 }
2939 }
2940 Self {
2941 order,
2942 default_ids,
2943 attributes_by_value,
2944 }
2945 }
2946}
2947
2948fn element_content_range(
2949 element: &str,
2950) -> Result<(std::ops::Range<usize>, String, bool), XmlDocumentError> {
2951 let start_end = element_opening_end(element)?;
2952 let start_tag = &element[..=start_end];
2953 let qualified_name = start_tag[1..]
2954 .split(|character: char| character.is_ascii_whitespace() || matches!(character, '/' | '>'))
2955 .next()
2956 .filter(|name| !name.is_empty())
2957 .ok_or_else(|| XmlDocumentError::InvalidReplacement("element name is missing".into()))?
2958 .to_owned();
2959 if start_tag[..start_tag.len() - 1].trim_end().ends_with('/') {
2960 return Ok((start_end..start_end, qualified_name, true));
2961 }
2962 let close = element
2963 .rfind("</")
2964 .ok_or_else(|| XmlDocumentError::InvalidReplacement("element end tag is missing".into()))?;
2965 Ok(((start_end + 1)..close, qualified_name, false))
2966}
2967
2968fn element_opening_end(element: &str) -> Result<usize, XmlDocumentError> {
2969 let mut quote = None;
2970 for (index, byte) in element.bytes().enumerate() {
2971 match byte {
2972 b'\'' | b'"' => match quote {
2973 Some(active) if active == byte => quote = None,
2974 None => quote = Some(byte),
2975 Some(_) => {}
2976 },
2977 b'>' if quote.is_none() => return Ok(index),
2978 _ => {}
2979 }
2980 }
2981 Err(XmlDocumentError::InvalidReplacement(
2982 "element start tag is incomplete".into(),
2983 ))
2984}
2985
2986#[cfg(test)]
2987mod tests {
2988 use super::*;
2989
2990 #[test]
2991 fn selected_backend_builds_the_retained_semantic_projection() {
2992 let settings = DocumentParseSettings::default();
2993 let xml =
2994 r#"<root xmlns:p="urn:test"><p:item ID="target"><![CDATA[value]]></p:item></root>"#;
2995 let retained = build_semantic_cell(xml.to_owned(), settings)
2996 .expect("selected backend semantic projection must parse");
2997 retained.with_dependent(|_, parsed| {
2998 assert!(parsed.indexes.default_ids.contains_key("target"));
2999 assert_eq!(
3000 parsed
3001 .document
3002 .root_element()
3003 .first_element_child()
3004 .expect("fixture must contain the indexed child element")
3005 .text(),
3006 Some("value")
3007 );
3008 });
3009 }
3010
3011 #[cfg(feature = "xml-backend-xmloxide")]
3012 #[test]
3013 fn xmloxide_attribute_capacity_is_independent_of_retained_node_limit() {
3014 let xml = r#"<root a="1" b="2" c="3"/>"#;
3018 let settings = DocumentParseSettings::new(false, 2, xml.len());
3019 build_cell(xml.to_owned(), settings, None)
3020 .expect("attributes must not consume the retained node allowance");
3021 }
3022
3023 #[test]
3024 fn node_preflight_rejects_wide_input_before_any_dom_pass() {
3025 let xml = "<root><first/><second/></root>";
3028 let resources = crate::policy::ResourcePolicy {
3029 max_xml_parse_work_bytes: xml.len(),
3030 ..crate::policy::ResourcePolicy::default()
3031 };
3032 let budget = XmlParseWorkBudget::from_resources(&resources);
3033 let settings = DocumentParseSettings::new(false, 3, xml.len());
3034
3035 assert!(matches!(
3036 build_cell(xml.to_owned(), settings, Some(&budget)),
3037 Err(XmlDocumentError::Parse(ParseError::NodesLimitReached))
3038 ));
3039 }
3040
3041 #[test]
3042 fn node_preflight_counts_contiguous_character_data_once() {
3043 let xml = "<root>a&<![CDATA[b]]>c</root>";
3046 let exact_settings = DocumentParseSettings::new(false, 3, xml.len());
3047
3048 build_cell(xml.to_owned(), exact_settings, None)
3049 .expect("the exact retained semantic node count must be accepted");
3050 assert!(matches!(
3051 build_cell(
3052 xml.to_owned(),
3053 DocumentParseSettings::new(false, 2, xml.len()),
3054 None,
3055 ),
3056 Err(XmlDocumentError::Parse(ParseError::NodesLimitReached))
3057 ));
3058 }
3059
3060 #[test]
3061 fn node_preflight_excludes_document_boundary_whitespace() {
3062 let xml = " \n<root/>\n ";
3065 let settings = DocumentParseSettings::new(false, 2, xml.len());
3066
3067 preflight_document_limits(xml, settings, None)
3068 .expect("document plus root element must fit the exact node ceiling");
3069 }
3070
3071 #[test]
3072 fn node_preflight_continues_after_explicit_end_tags() {
3073 let xml = "<root><first></first><second/></root>";
3076 let settings = DocumentParseSettings::new(false, 3, xml.len());
3077
3078 assert!(matches!(
3079 preflight_document_limits(xml, settings, None),
3080 Err(XmlDocumentError::Parse(ParseError::NodesLimitReached))
3081 ));
3082 }
3083
3084 #[test]
3085 fn depth_preflight_expands_nested_internal_entity_markup() {
3086 let xml = r#"<!DOCTYPE root [
3090 <!ENTITY inner "<b><c/></b>">
3091 <!ENTITY deep "<a>&inner;</a>">
3092 ]><root>&deep;</root>"#;
3093 let settings = DocumentParseSettings::new_with_depth(true, 32, 3, xml.len());
3094
3095 assert!(matches!(
3096 preflight_document_limits(xml, settings, None),
3097 Err(XmlDocumentError::DocumentTooDeep {
3098 maximum: 3,
3099 actual: 4,
3100 })
3101 ));
3102
3103 let exact_settings = DocumentParseSettings::new_with_depth(true, 32, 4, xml.len());
3104 preflight_document_limits(xml, exact_settings, None)
3105 .expect("expanded markup at the exact depth boundary must be accepted");
3106 }
3107
3108 #[test]
3109 fn depth_preflight_observes_markup_generated_by_a_character_reference() {
3110 let xml = r#"<!DOCTYPE root [
3113 <!ENTITY generated "<a><b/></a>">
3114 ]><root>&generated;</root>"#;
3115 let settings = DocumentParseSettings::new_with_depth(true, 16, 2, xml.len());
3116
3117 assert!(matches!(
3118 preflight_document_limits(xml, settings, None),
3119 Err(XmlDocumentError::DocumentTooDeep {
3120 maximum: 2,
3121 actual: 3,
3122 })
3123 ));
3124 }
3125
3126 #[test]
3127 fn entity_preflight_ignores_declarations_inside_dtd_comments() {
3128 let xml = r#"<!DOCTYPE root [
3131 <!-- <!ENTITY value "<a><b><c/></b></a>"> -->
3132 <!ENTITY value "ok">
3133 ]><root>&value;</root>"#;
3134 let settings = DocumentParseSettings::new_with_depth(true, 8, 1, xml.len());
3135
3136 preflight_document_limits(xml, settings, None)
3137 .expect("comment contents must not participate in entity expansion");
3138 build_cell(xml.to_owned(), settings, None)
3139 .expect("the real declaration contains only character data");
3140 }
3141
3142 #[test]
3143 fn entity_preflight_charges_nested_replacement_work() {
3144 let xml = r#"<!DOCTYPE root [
3147 <!ENTITY a "0123456789">
3148 <!ENTITY b "&a;&a;&a;&a;">
3149 ]><root>&b;</root>"#;
3150 let first_replacement = "&a;&a;&a;&a;".len();
3151 let nested_replacement = "0123456789".len();
3152 let maximum = xml.len() + first_replacement + nested_replacement - 1;
3153 let budget = XmlParseWorkBudget::with_limit(maximum);
3154 let settings = DocumentParseSettings::new_with_depth(true, 32, 4, xml.len());
3155
3156 assert!(matches!(
3157 build_cell(xml.to_owned(), settings, Some(&budget)),
3158 Err(XmlDocumentError::Policy(crate::policy::PolicyViolation::ResourceLimit {
3159 resource: crate::policy::resource_name::XML_PARSE_WORK_BYTES,
3160 maximum: observed_maximum,
3161 actual,
3162 })) if observed_maximum == maximum
3163 && actual == xml.len() + first_replacement + nested_replacement
3164 ));
3165 }
3166
3167 #[test]
3168 fn entity_preflight_charges_a_reference_generated_by_a_character_reference() {
3169 let xml = r#"<!DOCTYPE root [
3172 <!ENTITY nested "0123456789">
3173 <!ENTITY generated "&nested;">
3174 ]><root>&generated;</root>"#;
3175 let first_replacement = "&nested;".len();
3176 let nested_replacement = "0123456789".len();
3177 let maximum = xml.len() + first_replacement + nested_replacement - 1;
3178 let budget = XmlParseWorkBudget::with_limit(maximum);
3179 let settings = DocumentParseSettings::new_with_depth(true, 16, 2, xml.len());
3180
3181 assert!(matches!(
3182 preflight_document_limits(xml, settings, Some(&budget)),
3183 Err(XmlDocumentError::Policy(crate::policy::PolicyViolation::ResourceLimit {
3184 resource: crate::policy::resource_name::XML_PARSE_WORK_BYTES,
3185 maximum: observed_maximum,
3186 actual,
3187 })) if observed_maximum == maximum
3188 && actual == xml.len() + first_replacement + nested_replacement
3189 ));
3190 }
3191
3192 #[test]
3193 fn entity_preflight_charges_nested_attribute_replacements() {
3194 let xml = r#"<!DOCTYPE root [
3197 <!ENTITY a "0123456789">
3198 <!ENTITY b "&a;&a;&a;&a;">
3199 ]><root value="&b;"/>"#;
3200 let first_replacement = "&a;&a;&a;&a;".len();
3201 let nested_replacement = "0123456789".len();
3202 let expected_work = xml.len() + first_replacement + 4 * nested_replacement;
3203 let budget = XmlParseWorkBudget::with_limit(expected_work);
3204 let settings = DocumentParseSettings::new_with_depth(true, 8, 1, xml.len());
3205
3206 preflight_document_limits(xml, settings, Some(&budget))
3207 .expect("the exact repeated attribute expansion budget must be accepted");
3208 assert_eq!(budget.consumed(), expected_work);
3209
3210 let maximum = expected_work - 1;
3211 let budget = XmlParseWorkBudget::with_limit(maximum);
3212
3213 assert!(matches!(
3214 preflight_document_limits(xml, settings, Some(&budget)),
3215 Err(XmlDocumentError::Policy(crate::policy::PolicyViolation::ResourceLimit {
3216 resource: crate::policy::resource_name::XML_PARSE_WORK_BYTES,
3217 maximum: observed_maximum,
3218 actual,
3219 })) if observed_maximum == maximum
3220 && actual == expected_work
3221 ));
3222 }
3223
3224 #[test]
3225 fn entity_preflight_streams_dense_attribute_references_in_source_order() {
3226 let repeated = "&first;".repeat(4_096);
3230 let xml = format!(
3231 "<!DOCTYPE root [<!ENTITY first \"0123456789\"><!ENTITY last \"01234567890123456789\">]><root value=\"{repeated}&last;\"/>"
3232 );
3233 let maximum = xml.len() + 9;
3234 let budget = XmlParseWorkBudget::with_limit(maximum);
3235 let settings = DocumentParseSettings::new_with_depth(true, 8, 1, xml.len());
3236
3237 let mut scanner = general_references(b"&first;&last;");
3238 assert_eq!(scanner.next(), Some("first"));
3239 assert_eq!(scanner.next(), Some("last"));
3240 assert_eq!(scanner.next(), None);
3241
3242 assert!(matches!(
3243 preflight_document_limits(&xml, settings, Some(&budget)),
3244 Err(XmlDocumentError::Policy(crate::policy::PolicyViolation::ResourceLimit {
3245 resource: crate::policy::resource_name::XML_PARSE_WORK_BYTES,
3246 maximum: observed_maximum,
3247 actual,
3248 })) if observed_maximum == maximum && actual == xml.len() + 10
3249 ));
3250 }
3251
3252 #[test]
3253 fn entity_preflight_charges_applicable_dtd_attribute_defaults() {
3254 let omitted = r#"<!DOCTYPE root [
3258 <!ENTITY a "0123456789">
3259 <!ENTITY b "&a;&a;&a;&a;">
3260 <!ATTLIST root value CDATA "&b;">
3261 ]><root/>"#;
3262 let first_replacement = "&a;&a;&a;&a;".len();
3263 let nested_replacement = "0123456789".len();
3264 let expected_work = omitted.len() + first_replacement + 4 * nested_replacement;
3265 let settings = DocumentParseSettings::new_with_depth(true, 8, 1, omitted.len());
3266 let exact_budget = XmlParseWorkBudget::with_limit(expected_work);
3267
3268 preflight_document_limits(omitted, settings, Some(&exact_budget))
3269 .expect("the exact default-attribute expansion budget must be accepted");
3270 assert_eq!(exact_budget.consumed(), expected_work);
3271
3272 let maximum = expected_work - 1;
3273 let short_budget = XmlParseWorkBudget::with_limit(maximum);
3274 assert!(matches!(
3275 preflight_document_limits(omitted, settings, Some(&short_budget)),
3276 Err(XmlDocumentError::Policy(crate::policy::PolicyViolation::ResourceLimit {
3277 resource: crate::policy::resource_name::XML_PARSE_WORK_BYTES,
3278 maximum: observed_maximum,
3279 actual,
3280 })) if observed_maximum == maximum && actual == expected_work
3281 ));
3282
3283 let overridden = omitted.replace("<root/>", "<root value=\"literal\"/>");
3284 let source_only_budget = XmlParseWorkBudget::with_limit(overridden.len());
3285 let overridden_settings =
3286 DocumentParseSettings::new_with_depth(true, 8, 1, overridden.len());
3287 preflight_document_limits(&overridden, overridden_settings, Some(&source_only_budget))
3288 .expect("an explicit source attribute must suppress its DTD default");
3289 assert_eq!(source_only_budget.consumed(), overridden.len());
3290 }
3291
3292 #[test]
3293 fn entity_preflight_uses_bounded_heap_for_long_acyclic_chains() {
3294 const ENTITY_COUNT: usize = 4_096;
3297 let mut xml = String::from("<!DOCTYPE root [\n<!ENTITY e0 \"value\">\n");
3298 for index in 1..ENTITY_COUNT {
3299 use std::fmt::Write as _;
3300 writeln!(xml, "<!ENTITY e{index} \"&e{};\">", index - 1)
3301 .expect("write entity declaration");
3302 }
3303 use std::fmt::Write as _;
3304 write!(xml, "]><root>&e{};</root>", ENTITY_COUNT - 1).expect("write root reference");
3305 let settings = DocumentParseSettings::new_with_depth(true, 16, 1, xml.len());
3306
3307 preflight_document_limits(&xml, settings, None)
3308 .expect("long acyclic entity traversal must use bounded native stack");
3309 }
3310
3311 #[test]
3312 fn owned_parse_preflights_depth_before_dtd_provenance() {
3313 let xml = "<root><child><leaf/></child></root>";
3316 let settings = DocumentParseSettings::new_with_depth(true, 16, 2, xml.len());
3317 let budget = XmlParseWorkBudget::with_limit(xml.len());
3318
3319 assert!(matches!(
3320 XmlDocument::parse_with_settings_and_optional_budget(
3321 xml.to_owned(),
3322 settings,
3323 Some(&budget),
3324 ),
3325 Err(XmlDocumentError::DocumentTooDeep {
3326 maximum: 2,
3327 actual: 3,
3328 })
3329 ));
3330 }
3331
3332 fn nested_document(depth: usize) -> String {
3333 let mut xml = "<node>".repeat(depth);
3334 xml.push_str(&"</node>".repeat(depth));
3335 xml
3336 }
3337
3338 #[test]
3339 fn selected_backend_enforces_exact_depth_boundary() {
3340 let settings = DocumentParseSettings::new_with_depth(false, 128, 2, 4_096);
3341 let accepted = nested_document(2);
3342 build_semantic_cell(accepted, settings).expect("exact depth must parse");
3343
3344 let rejected = nested_document(3);
3345 assert!(matches!(
3346 build_semantic_cell(rejected, settings),
3347 Err(XmlDocumentError::DocumentTooDeep {
3348 maximum: 2,
3349 actual: 3,
3350 })
3351 ));
3352 }
3353
3354 #[cfg(feature = "xml-backend-xmloxide")]
3355 #[test]
3356 fn xmloxide_backend_enforces_exact_depth_boundary() {
3357 let settings = DocumentParseSettings::new_with_depth(false, 128, 2, 4_096);
3360 let accepted = nested_document(2);
3361 build_cell(accepted, settings, None).expect("exact depth must parse");
3362
3363 let rejected = nested_document(3);
3364 assert!(matches!(
3365 build_cell(rejected, settings, None),
3366 Err(XmlDocumentError::DocumentTooDeep {
3367 maximum: 2,
3368 actual: 3,
3369 })
3370 ));
3371 }
3372
3373 struct OversizedBorrowedInput<'a>(&'a str);
3374
3375 impl AsRef<str> for OversizedBorrowedInput<'_> {
3376 fn as_ref(&self) -> &str {
3377 self.0
3378 }
3379 }
3380
3381 impl From<OversizedBorrowedInput<'_>> for String {
3382 fn from(_: OversizedBorrowedInput<'_>) -> Self {
3383 panic!("oversized borrowed XML must be rejected before ownership conversion")
3384 }
3385 }
3386
3387 #[test]
3388 fn oversized_borrowed_input_is_rejected_before_ownership_conversion() {
3389 let oversized = "x".repeat(crate::hard_limits::XML_DOCUMENT_BYTE_CEILING + 1);
3392 assert!(matches!(
3393 XmlDocument::parse(OversizedBorrowedInput(&oversized)),
3394 Err(XmlDocumentError::DocumentTooLarge { .. })
3395 ));
3396
3397 #[cfg(feature = "xmldsig")]
3398 {
3399 let mut policy = crate::policy::SigningPolicy::default();
3400 policy.resources.max_xml_document_bytes = 8;
3401 assert!(matches!(
3402 XmlDocument::parse_with_policy(OversizedBorrowedInput("<root/>xx"), &policy),
3403 Err(XmlDocumentError::DocumentTooLarge {
3404 maximum: 8,
3405 actual: 9,
3406 })
3407 ));
3408 }
3409 }
3410
3411 #[test]
3412 fn views_reuse_identity_until_mutation_invalidates_generation() {
3413 let mut document =
3414 XmlDocument::parse("<root><child ID=\"target\"/></root>").expect("fixture must parse");
3415 let target = document.with_view(|view| {
3416 view.node_for_id("target", &[])
3417 .expect("ID target must resolve")
3418 });
3419 assert_eq!(document.generation(), 0);
3420 document
3421 .replace_element(target, "<replacement ID=\"target\"/>")
3422 .expect("replacement must succeed");
3423
3424 let error = document
3425 .with_view(|view| view.resolve_node(target).map(|_| ()))
3426 .expect_err("old identity must be stale");
3427 assert!(matches!(error, XmlDocumentError::StaleIdentity { .. }));
3428 assert_eq!(document.generation(), 1);
3429 }
3430
3431 #[test]
3432 fn identities_cannot_cross_documents() {
3433 let first = XmlDocument::parse("<root/>").expect("fixture must parse");
3434 let second = XmlDocument::parse("<root/>").expect("fixture must parse");
3435 let root = first.with_view(|view| view.root_element());
3436
3437 let error = second
3438 .with_view(|view| view.resolve_node(root).map(|_| ()))
3439 .expect_err("foreign identity must fail closed");
3440 assert!(matches!(error, XmlDocumentError::ForeignIdentity));
3441 }
3442
3443 #[test]
3444 fn child_append_projection_matches_both_element_forms() {
3445 for xml in ["<root></root>", "<root/>"] {
3448 let document = XmlDocument::parse(xml).expect("fixture must parse");
3449 let root = document.with_view(|view| view.root_element());
3450 let projected = document
3451 .projected_child_append_len(root, "<child/>".len())
3452 .expect("append length must project");
3453
3454 assert_eq!(projected, "<root><child/></root>".len());
3455 }
3456 }
3457
3458 #[test]
3459 fn document_identity_allocation_reports_exhaustion_without_wrapping() {
3460 let counter = AtomicU64::new(u64::MAX - 1);
3461
3462 assert_eq!(
3463 allocate_document_identity(&counter).expect("last identity must be allocated"),
3464 DocumentIdentity(u64::MAX - 1)
3465 );
3466 assert!(matches!(
3467 allocate_document_identity(&counter),
3468 Err(XmlDocumentError::IdentityExhausted)
3469 ));
3470 assert_eq!(counter.load(Ordering::Relaxed), u64::MAX);
3471 }
3472
3473 #[test]
3474 fn duplicate_and_caller_registered_ids_share_one_index() {
3475 let document = XmlDocument::parse(
3476 "<root><a ID=\"duplicate\" custom=\"selected\"/><b Id=\"duplicate\"/></root>",
3477 )
3478 .expect("fixture must parse");
3479 let registrations = [IdAttributeRegistration::global("custom")];
3480
3481 document.with_view(|view| {
3482 assert!(view.node_for_id("duplicate", &[]).is_none());
3483 assert!(view.node_for_id("selected", ®istrations).is_some());
3484 });
3485 }
3486
3487 #[test]
3488 fn duplicate_default_id_stays_ambiguous_with_caller_registration() {
3489 let document = XmlDocument::parse(
3492 "<root><a ID=\"duplicate\" custom=\"duplicate\"/><b Id=\"duplicate\"/></root>",
3493 )
3494 .expect("fixture must parse");
3495 let registrations = [IdAttributeRegistration::global("custom")];
3496
3497 document.with_view(|view| {
3498 assert!(view.node_for_id("duplicate", ®istrations).is_none());
3499 });
3500 }
3501
3502 #[test]
3503 fn content_replacement_uses_parent_namespace_context() {
3504 let mut document = XmlDocument::parse("<p:root xmlns:p=\"urn:test\"><p:old/></p:root>")
3505 .expect("fixture must parse");
3506 let root = document.with_view(|view| view.root_element());
3507 document
3508 .replace_content(root, "<p:new/>")
3509 .expect("in-scope prefix must be accepted");
3510 assert_eq!(
3511 document.as_xml(),
3512 "<p:root xmlns:p=\"urn:test\"><p:new/></p:root>"
3513 );
3514 }
3515
3516 #[cfg(feature = "xml-backend-xmloxide")]
3517 #[test]
3518 fn folded_character_data_replacement_splices_its_complete_source_range() {
3519 let mut document = XmlDocument::parse("<r>one<![CDATA[+]]>two</r>")
3522 .expect("mixed character data fixture must parse");
3523 let target = document.with_view(|view| {
3524 let text = view
3525 .document()
3526 .root_element()
3527 .first_child()
3528 .expect("fixture must contain a text node");
3529 assert_eq!(text.text(), Some("one+two"));
3530 view.node_identity(text)
3531 });
3532
3533 document
3534 .replace_node_with_fragment(target, "replacement")
3535 .expect("the complete folded text node must be replaceable");
3536
3537 assert_eq!(document.as_xml(), "<r>replacement</r>");
3538 }
3539
3540 #[test]
3541 fn validation_wrapper_does_not_consume_document_depth() {
3542 let mut document = XmlDocument::parse_with_settings(
3545 "<root><target/></root>".into(),
3546 DocumentParseSettings::new_with_depth(false, 128, 2, 4_096),
3547 )
3548 .expect("fixture must fit the exact depth ceiling");
3549 let target = document.with_view(|view| {
3550 view.node_identity(
3551 view.document()
3552 .descendants()
3553 .find(|node| node.has_tag_name("target"))
3554 .expect("target must exist"),
3555 )
3556 });
3557
3558 document
3559 .replace_content(target, "text")
3560 .expect("validation-only wrapper must not consume caller depth");
3561 assert_eq!(document.as_xml(), "<root><target>text</target></root>");
3562 }
3563
3564 #[test]
3565 fn validation_wrapper_preserves_real_depth_rejection() {
3566 let mut document = XmlDocument::parse_with_settings(
3569 "<root><target/></root>".into(),
3570 DocumentParseSettings::new_with_depth(false, 128, 2, 4_096),
3571 )
3572 .expect("fixture must fit the exact depth ceiling");
3573 let target = document.with_view(|view| {
3574 view.node_identity(
3575 view.document()
3576 .descendants()
3577 .find(|node| node.has_tag_name("target"))
3578 .expect("target must exist"),
3579 )
3580 });
3581 let before = document.as_xml().to_owned();
3582
3583 let error = document
3584 .replace_content(target, "<child/>")
3585 .expect_err("real replacement depth must remain bounded");
3586 assert!(
3587 matches!(
3588 error,
3589 XmlDocumentError::DocumentTooDeep {
3590 maximum: 2,
3591 actual: 3,
3592 }
3593 ),
3594 "unexpected error: {error:?}"
3595 );
3596 assert_eq!(document.as_xml(), before);
3597 assert_eq!(document.generation(), 0);
3598 }
3599
3600 #[test]
3601 fn batched_validation_wrappers_do_not_consume_document_depth() {
3602 let mut document = XmlDocument::parse_with_settings(
3605 "<root><first/><second/></root>".into(),
3606 DocumentParseSettings::new_with_depth(false, 128, 2, 4_096),
3607 )
3608 .expect("fixture must fit the exact depth ceiling");
3609 let (first, second) = document.with_view(|view| {
3610 let mut targets = view
3611 .document()
3612 .descendants()
3613 .filter(|node| matches!(node.tag_name().name(), "first" | "second"));
3614 (
3615 view.node_identity(targets.next().expect("first target")),
3616 view.node_identity(targets.next().expect("second target")),
3617 )
3618 });
3619
3620 document
3621 .replace_contents(&[(first, "a".into()), (second, "b".into())])
3622 .expect("validation-only wrappers must not consume caller depth");
3623 assert_eq!(
3624 document.as_xml(),
3625 "<root><first>a</first><second>b</second></root>"
3626 );
3627 }
3628
3629 #[test]
3630 fn mutations_ignore_greater_than_inside_quoted_attributes() {
3631 let mut document =
3634 XmlDocument::parse("<root marker=\">\"><old/></root>").expect("fixture must parse");
3635 let root = document.with_view(|view| view.root_element());
3636
3637 document
3638 .replace_content(root, "<first/>")
3639 .expect("quoted greater-than must not corrupt content replacement");
3640 let root = document.with_view(|view| view.root_element());
3641 document
3642 .append_child(root, "<second/>")
3643 .expect("quoted greater-than must not corrupt child append");
3644
3645 assert_eq!(
3646 document.as_xml(),
3647 "<root marker=\">\"><first/><second/></root>"
3648 );
3649 }
3650
3651 #[test]
3652 fn fragment_validation_reuses_internal_dtd_entities() {
3653 let mut document = XmlDocument::parse_with_settings(
3656 "<!DOCTYPE root [<!ENTITY custom \"replacement\">]><root><old/></root>".into(),
3657 DocumentParseSettings::new(
3658 true,
3659 crate::hard_limits::XML_DOCUMENT_NODE_CEILING,
3660 crate::hard_limits::XML_DOCUMENT_BYTE_CEILING,
3661 ),
3662 )
3663 .expect("DTD fixture must parse when explicitly enabled");
3664 let root = document.with_view(|view| view.root_element());
3665
3666 document
3667 .replace_content(root, "&custom;")
3668 .expect("declared entity must remain valid in replacement context");
3669 assert_eq!(
3670 document.as_xml(),
3671 "<!DOCTYPE root [<!ENTITY custom \"replacement\">]><root>&custom;</root>"
3672 );
3673 }
3674
3675 #[test]
3676 fn serialization_is_stable_for_unchanged_views() {
3677 let document = XmlDocument::parse("<?pi value?><root a=\"1\"><!--c--></root>")
3678 .expect("fixture must parse");
3679 let first = document.as_xml().to_owned();
3680 document.with_view(|view| assert_eq!(view.xml(), first));
3681 document.with_view(|view| assert_eq!(view.xml(), first));
3682 assert_eq!(document.as_xml(), first);
3683 }
3684
3685 #[test]
3686 fn rejected_mutation_keeps_source_and_generation_unchanged() {
3687 let mut document = XmlDocument::parse("<root><child/></root>").expect("fixture must parse");
3690 let root = document.with_view(|view| view.root_element());
3691 let before = document.as_xml().to_owned();
3692
3693 assert!(document.replace_content(root, "<unclosed>").is_err());
3694 assert_eq!(document.as_xml(), before);
3695 assert_eq!(document.generation(), 0);
3696 }
3697
3698 #[test]
3699 fn oversized_invalid_fragment_fails_at_the_document_byte_boundary() {
3700 for xml in ["<root><child/></root>", "<root/>"] {
3704 let mut document = XmlDocument::parse_with_settings(
3705 xml.into(),
3706 DocumentParseSettings::new(false, 64, 64),
3707 )
3708 .expect("bounded fixture must parse");
3709 let root = document.with_view(|view| view.root_element());
3710 let replacement = "<".repeat(1_024);
3711
3712 assert!(matches!(
3713 document.replace_content(root, &replacement),
3714 Err(XmlDocumentError::DocumentTooLarge {
3715 maximum: 64,
3716 actual,
3717 }) if actual > 64
3718 ));
3719 assert_eq!(document.as_xml(), xml);
3720 assert_eq!(document.generation(), 0);
3721 }
3722 }
3723
3724 #[cfg(feature = "xmlenc")]
3725 #[test]
3726 fn bounded_fragment_validation_uses_the_active_node_ceiling() {
3727 let document = XmlDocument::parse_with_settings(
3730 "<root><target/></root>".into(),
3731 DocumentParseSettings::new(false, 128, 4_096),
3732 )
3733 .expect("fixture must parse");
3734 let target = document.with_view(|view| {
3735 let target = view
3736 .document()
3737 .descendants()
3738 .find(|node| node.has_tag_name("target"))
3739 .expect("target must exist");
3740 view.node_identity(target)
3741 });
3742 let maximum = document.with_view(|view| view.node_count());
3743 let before = document.as_xml().to_owned();
3744 let budget = XmlParseWorkBudget::from_resources(&crate::policy::ResourcePolicy::default());
3745
3746 let replacement = document.prepare_node_fragment_replacement_with_budget(
3747 target,
3748 "<replacement><child/><child/><malformed>",
3749 DocumentParseSettings::new(false, maximum as u32, 4_096),
3750 &budget,
3751 );
3752 assert!(matches!(
3753 replacement,
3754 Err(XmlDocumentError::ProjectedNodeLimit { maximum: rejected })
3755 if rejected == maximum
3756 ));
3757 assert_eq!(document.as_xml(), before);
3758 assert_eq!(document.generation(), 0);
3759 }
3760
3761 #[test]
3762 fn duplicate_empty_content_targets_are_rejected_atomically() {
3763 let mut document =
3764 XmlDocument::parse("<root><target></target></root>").expect("fixture must parse");
3765 let target = document.with_view(|view| {
3766 let target = view
3767 .document()
3768 .descendants()
3769 .find(|node| node.has_tag_name("target"))
3770 .expect("target must exist");
3771 view.node_identity(target)
3772 });
3773
3774 let error = document
3775 .replace_contents(&[(target, "first".into()), (target, "second".into())])
3776 .expect_err("one identity cannot be replaced twice");
3777
3778 assert!(matches!(error, XmlDocumentError::InvalidReplacement(_)));
3779 assert_eq!(document.as_xml(), "<root><target></target></root>");
3780 assert_eq!(document.generation(), 0);
3781 }
3782
3783 #[cfg(feature = "xmldsig")]
3784 #[test]
3785 fn batched_content_replacements_obey_the_active_byte_ceiling() {
3786 let mut document = XmlDocument::parse_with_settings(
3789 "<root><first/><second/></root>".into(),
3790 DocumentParseSettings::new(false, 128, 4_096),
3791 )
3792 .expect("fixture must parse");
3793 let (first, second) = document.with_view(|view| {
3794 let mut targets = view
3795 .document()
3796 .descendants()
3797 .filter(|node| matches!(node.tag_name().name(), "first" | "second"));
3798 (
3799 view.node_identity(targets.next().expect("first target")),
3800 view.node_identity(targets.next().expect("second target")),
3801 )
3802 });
3803 let before = document.as_xml().to_owned();
3804 let replacements = [(first, "alpha".into()), (second, "beta".into())];
3805 let expected = "<root><first>alpha</first><second>beta</second></root>";
3806 let maximum = expected.len() - 1;
3807 let budget = XmlParseWorkBudget::from_resources(&crate::policy::ResourcePolicy::default());
3808
3809 let error = document
3810 .replace_contents_with_budget(
3811 &replacements,
3812 DocumentParseSettings::new(false, 128, maximum),
3813 &budget,
3814 )
3815 .expect_err("the active byte ceiling must reject the complete batch");
3816
3817 assert!(matches!(
3818 error,
3819 XmlDocumentError::DocumentTooLarge {
3820 maximum: observed_maximum,
3821 actual,
3822 } if observed_maximum == maximum && actual == expected.len()
3823 ));
3824 assert_eq!(document.as_xml(), before);
3825 assert_eq!(document.generation(), 0);
3826 }
3827
3828 #[test]
3829 fn batched_content_replacements_validate_one_namespaced_candidate() {
3830 let mut document = XmlDocument::parse(
3833 r#"<root xmlns:p="urn:test"><first/><second><old/></second></root>"#,
3834 )
3835 .expect("fixture must parse");
3836 let (first, second) = document.with_view(|view| {
3837 let mut targets = view
3838 .document()
3839 .descendants()
3840 .filter(|node| matches!(node.tag_name().name(), "first" | "second"));
3841 (
3842 view.node_identity(targets.next().expect("first target")),
3843 view.node_identity(targets.next().expect("second target")),
3844 )
3845 });
3846
3847 document
3848 .replace_contents(&[
3849 (first, "<p:alpha/>".into()),
3850 (second, "text<p:beta/>".into()),
3851 ])
3852 .expect("all fragments must validate in one candidate");
3853
3854 assert_eq!(
3855 document.as_xml(),
3856 r#"<root xmlns:p="urn:test"><first><p:alpha/></first><second>text<p:beta/></second></root>"#
3857 );
3858 assert_eq!(document.generation(), 1);
3859 }
3860
3861 #[cfg(feature = "xmldsig")]
3862 #[test]
3863 fn batched_content_replacements_charge_two_combined_parses() {
3864 const TARGETS: usize = 64;
3868 let source = format!("<root>{}</root>", "<item>old</item>".repeat(TARGETS));
3869 let mut document = XmlDocument::parse(&source).expect("fixture must parse");
3870 let replacements = document.with_view(|view| {
3871 view.document()
3872 .descendants()
3873 .filter(|node| node.has_tag_name("item"))
3874 .map(|node| (view.node_identity(node), "x".to_owned()))
3875 .collect::<Vec<_>>()
3876 });
3877 let committed_len = source.len() - TARGETS * "old".len() + TARGETS;
3878 let validation_len =
3879 source.len() - TARGETS * "old".len() + TARGETS * wrapped_fragment("x").len();
3880 let parser_passes = selected_parser_passes();
3881 let maximum = (validation_len + committed_len) * parser_passes;
3882 let budget = XmlParseWorkBudget::with_limit(maximum);
3883
3884 document
3885 .replace_contents_with_budget(&replacements, DocumentParseSettings::default(), &budget)
3886 .expect("the batch must fit its two combined parser passes exactly");
3887
3888 assert_eq!(budget.consumed(), maximum);
3889 assert_eq!(document.as_xml().matches("<item>x</item>").count(), TARGETS);
3890 assert_eq!(document.generation(), 1);
3891 }
3892
3893 #[test]
3894 fn parser_work_accounting_follows_the_runtime_backend() {
3895 let xml = "<root><value/></root>";
3898 for backend in [
3899 XmlBackend::Xmloxide,
3900 XmlBackend::Roxmltree,
3901 XmlBackend::Differential,
3902 ]
3903 .into_iter()
3904 .filter(|backend| backend.is_available())
3905 {
3906 let expected = xml.len() * 3;
3907 let budget = XmlParseWorkBudget::with_limit(expected);
3908 parse_borrowed_with_settings_and_budget(
3909 xml,
3910 DocumentParseSettings::default().with_backend(backend),
3911 Some(&budget),
3912 )
3913 .expect("the exact runtime parser-work allowance must fit");
3914 assert_eq!(budget.consumed(), expected, "{backend:?}");
3915 }
3916 }
3917
3918 #[test]
3919 fn batched_content_replacements_reject_one_escaped_boundary_atomically() {
3920 let mut document = XmlDocument::parse("<root><first></first><second></second></root>")
3923 .expect("fixture must parse");
3924 let (first, second) = document.with_view(|view| {
3925 let mut targets = view
3926 .document()
3927 .descendants()
3928 .filter(|node| matches!(node.tag_name().name(), "first" | "second"));
3929 (
3930 view.node_identity(targets.next().expect("first target")),
3931 view.node_identity(targets.next().expect("second target")),
3932 )
3933 });
3934 let before = document.as_xml().to_owned();
3935
3936 document
3937 .replace_contents(&[
3938 (first, "</first><attacker/><first>".into()),
3939 (second, "safe".into()),
3940 ])
3941 .expect_err("a fragment must not escape its target boundary");
3942
3943 assert_eq!(document.as_xml(), before);
3944 assert_eq!(document.generation(), 0);
3945 }
3946
3947 #[cfg(feature = "xmldsig")]
3948 #[test]
3949 fn retained_mutations_share_one_sticky_parse_work_budget() {
3950 let mut document =
3954 XmlDocument::parse("<root><first/><second/></root>").expect("fixture must parse");
3955 let first = document.with_view(|view| {
3956 view.node_identity(
3957 view.document()
3958 .descendants()
3959 .find(|node| node.has_tag_name("first"))
3960 .expect("first target"),
3961 )
3962 });
3963 let first_output = "<root><first>a</first><second/></root>";
3964 let first_validation_len = first_output
3965 .len()
3966 .checked_add(VALIDATION_WRAPPER_OPEN.len())
3967 .and_then(|length| length.checked_add(VALIDATION_WRAPPER_CLOSE.len()))
3968 .expect("fixture length must fit");
3969 let parser_passes = selected_parser_passes();
3970 let maximum = (first_validation_len + first_output.len()) * parser_passes;
3971 let budget = XmlParseWorkBudget::with_limit(maximum);
3972
3973 document
3974 .replace_contents_with_budget(
3975 &[(first, "a".into())],
3976 DocumentParseSettings::default(),
3977 &budget,
3978 )
3979 .expect("the first mutation must consume the exact allowance");
3980 assert_eq!(document.as_xml(), first_output);
3981
3982 let second = document.with_view(|view| {
3983 view.node_identity(
3984 view.document()
3985 .descendants()
3986 .find(|node| node.has_tag_name("second"))
3987 .expect("second target"),
3988 )
3989 });
3990 let before = document.as_xml().to_owned();
3991 let error = document
3992 .replace_contents_with_budget(
3993 &[(second, "b".into())],
3994 DocumentParseSettings::default(),
3995 &budget,
3996 )
3997 .expect_err("the second mutation must not receive a fresh allowance");
3998
3999 assert!(matches!(
4000 error,
4001 XmlDocumentError::Policy(crate::policy::PolicyViolation::ResourceLimit {
4002 resource: crate::policy::resource_name::XML_PARSE_WORK_BYTES,
4003 maximum: observed_maximum,
4004 actual,
4005 }) if observed_maximum == maximum && actual > maximum
4006 ));
4007 assert_eq!(document.as_xml(), before);
4008 assert_eq!(document.generation(), 1);
4009 }
4010
4011 #[cfg(feature = "xmlenc")]
4012 #[test]
4013 fn bounded_content_replacement_rejects_new_text_node_atomically() {
4014 let mut document =
4015 XmlDocument::parse("<root><target/></root>").expect("fixture must parse");
4016 let target = document.with_view(|view| {
4017 let root = view
4018 .resolve_node(view.root_element())
4019 .expect("root must resolve");
4020 view.node_identity(
4021 root.children()
4022 .find(Node::is_element)
4023 .expect("target must exist"),
4024 )
4025 });
4026 let maximum = document.with_view(|view| view.node_count());
4027 let before = document.as_xml().to_owned();
4028 let budget = XmlParseWorkBudget::from_resources(&crate::policy::ResourcePolicy::default());
4029
4030 assert!(matches!(
4031 document.replace_content_with_budget(
4032 target,
4033 "value",
4034 DocumentParseSettings::new(false, maximum as u32, 1_024),
4035 &budget,
4036 ),
4037 Err(XmlDocumentError::ProjectedNodeLimit { maximum: rejected })
4038 if rejected == maximum
4039 ));
4040 assert_eq!(document.as_xml(), before);
4041 assert_eq!(document.generation(), 0);
4042 }
4043
4044 #[test]
4045 fn entity_expanded_elements_are_not_mutation_targets() {
4046 let source = "<!DOCTYPE root [<!ENTITY pair '<a ID=\"target\"/><b/>'>]><root>&pair;</root>";
4049 let settings = DocumentParseSettings::new(true, 64, 4_096);
4050 let mut document = XmlDocument::parse_with_settings(source.into(), settings)
4051 .expect("entity fixture must parse");
4052 let target = document
4053 .with_view(|view| view.node_for_id("target", &[]))
4054 .expect("expanded element ID must resolve");
4055 let before = document.as_xml().to_owned();
4056
4057 let error = document
4058 .replace_element(target, "<replacement/>")
4059 .expect_err("entity-expanded identity must not be mutable");
4060
4061 assert!(error.to_string().contains("entity expansion"), "{error}");
4062 assert_eq!(document.as_xml(), before);
4063 assert_eq!(document.generation(), 0);
4064 }
4065
4066 #[cfg(feature = "xmldsig")]
4067 #[test]
4068 fn bounded_append_accepts_text_merged_with_existing_content() {
4069 let mut document = XmlDocument::parse_with_settings(
4072 "<root>text</root>".into(),
4073 DocumentParseSettings::new(false, 3, 1_024),
4074 )
4075 .expect("fixture must fit the exact node ceiling");
4076 let root = document.with_view(|view| view.root_element());
4077 let budget = XmlParseWorkBudget::from_resources(&crate::policy::ResourcePolicy::default());
4078
4079 document
4080 .append_child_with_budget(
4081 root,
4082 "more",
4083 DocumentParseSettings::new(false, 3, 1_024),
4084 &budget,
4085 )
4086 .expect("merged text must not consume another committed node");
4087
4088 assert_eq!(document.as_xml(), "<root>textmore</root>");
4089 assert_eq!(document.with_view(|view| view.node_count()), 3);
4090 }
4091
4092 #[cfg(feature = "xmlenc")]
4093 #[test]
4094 fn bounded_fragment_accepts_two_boundary_text_merges() {
4095 let mut document =
4098 XmlDocument::parse("<root>left<target/>right</root>").expect("fixture must parse");
4099 let target = document.with_view(|view| {
4100 let target = view
4101 .document()
4102 .descendants()
4103 .find(|node| node.has_tag_name("target"))
4104 .expect("target must exist");
4105 view.node_identity(target)
4106 });
4107 let budget = XmlParseWorkBudget::from_resources(&crate::policy::ResourcePolicy::default());
4108
4109 let prepared = document
4110 .prepare_node_fragment_replacement_with_budget(
4111 target,
4112 "middle",
4113 DocumentParseSettings::new(false, 3, 1_024),
4114 &budget,
4115 )
4116 .expect("both boundary text pairs must fit in the prepared document");
4117 document
4118 .commit_prepared(prepared)
4119 .expect("prepared replacement must commit atomically");
4120
4121 assert_eq!(document.as_xml(), "<root>leftmiddleright</root>");
4122 assert_eq!(document.with_view(|view| view.node_count()), 3);
4123 }
4124
4125 #[test]
4126 fn semantic_order_covers_namespace_attribute_and_tree_nodes() {
4127 let document =
4128 XmlDocument::parse("<root xmlns:p=\"urn:test\" p:value=\"1\"><child/></root>")
4129 .expect("fixture must parse");
4130
4131 document.with_view(|view| {
4132 let root = view.root_element();
4133 let root_node = view.resolve_node(root).expect("root must resolve");
4134 let child = view.node_identity(
4135 root_node
4136 .children()
4137 .find(Node::is_element)
4138 .expect("child must exist"),
4139 );
4140 let attribute = view
4141 .attribute_identity(root, Some("urn:test"), "value")
4142 .expect("attribute must exist");
4143 let namespace = view
4144 .namespace_identities(root)
4145 .expect("namespace axis must exist")
4146 .into_iter()
4147 .find(|namespace| namespace.prefix() == "p")
4148 .expect("p namespace must exist");
4149
4150 let root_order = view.node_order(root).expect("root order must exist");
4151 let namespace_order = view
4152 .namespace_order(&namespace)
4153 .expect("namespace order must exist");
4154 let attribute_order = view
4155 .attribute_order(&attribute)
4156 .expect("attribute order must exist");
4157 let child_order = view.node_order(child).expect("child order must exist");
4158 assert!(root_order < namespace_order);
4159 assert!(namespace_order < attribute_order);
4160 assert!(attribute_order < child_order);
4161 });
4162 }
4163
4164 #[test]
4165 fn inherited_namespace_axes_are_materialized_only_for_the_requested_owner() {
4166 let declarations = (0..128)
4170 .map(|index| format!(r#" xmlns:p{index}="urn:namespace:{index}""#))
4171 .collect::<String>();
4172 let children = (0..1_024)
4173 .map(|index| format!("<child index=\"{index}\"/>"))
4174 .collect::<String>();
4175 let document = XmlDocument::parse(format!("<root{declarations}>{children}</root>"))
4176 .expect("wide namespace fixture must parse");
4177
4178 document.with_view(|view| {
4179 let last_child = view
4180 .document()
4181 .descendants()
4182 .rfind(|node| node.has_tag_name("child"))
4183 .expect("last child must exist");
4184 let owner = view.node_identity(last_child);
4185 let namespaces = view
4186 .namespace_identities(owner)
4187 .expect("inherited namespace axis must materialize on demand");
4188 assert_eq!(namespaces.len(), 128);
4189 assert!(namespaces.windows(2).all(|pair| {
4190 view.namespace_order(&pair[0]).expect("left order")
4191 < view.namespace_order(&pair[1]).expect("right order")
4192 }));
4193 });
4194 }
4195
4196 #[test]
4197 fn mutation_invalidates_attribute_and_namespace_identities() {
4198 let mut document =
4199 XmlDocument::parse("<root xmlns:p=\"urn:test\" p:value=\"1\"><child/></root>")
4200 .expect("fixture must parse");
4201 let (root, attribute, namespace) = document.with_view(|view| {
4202 let root = view.root_element();
4203 let attribute = view
4204 .attribute_identity(root, Some("urn:test"), "value")
4205 .expect("attribute must exist");
4206 let namespace = view
4207 .namespace_identities(root)
4208 .expect("namespace axis must exist")
4209 .into_iter()
4210 .find(|namespace| namespace.prefix() == "p")
4211 .expect("p namespace must exist");
4212 (root, attribute, namespace)
4213 });
4214
4215 document
4216 .append_child(root, "<p:next/>")
4217 .expect("mutation must succeed");
4218 document.with_view(|view| {
4219 assert!(matches!(
4220 view.attribute_order(&attribute),
4221 Err(XmlDocumentError::StaleIdentity { .. })
4222 ));
4223 assert!(matches!(
4224 view.namespace_order(&namespace),
4225 Err(XmlDocumentError::StaleIdentity { .. })
4226 ));
4227 });
4228 }
4229
4230 #[test]
4231 fn content_replacement_cannot_escape_its_target_element() {
4232 let mut document = XmlDocument::parse("<outer><target><old/></target></outer>")
4233 .expect("fixture must parse");
4234 let target = document.with_view(|view| {
4235 view.node_identity(
4236 view.document()
4237 .descendants()
4238 .find(|node| node.has_tag_name("target"))
4239 .expect("target must exist"),
4240 )
4241 });
4242 let before = document.as_xml().to_owned();
4243
4244 assert!(
4245 document
4246 .replace_content(target, "</target><attacker/><target>")
4247 .is_err()
4248 );
4249 assert_eq!(document.as_xml(), before);
4250 assert_eq!(document.generation(), 0);
4251 }
4252}