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