Skip to main content

xml_sec/
document.rs

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