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