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