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