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