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