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 quick_xml::{
14    Reader as QuickXmlReader,
15    events::{BytesStart as QuickXmlBytesStart, Event as QuickXmlEvent},
16};
17use roxmltree::{Document, Node, NodeId, ParsingOptions};
18use self_cell::self_cell;
19
20use crate::IdAttributeRegistration;
21
22#[derive(Clone, Copy, Debug, PartialEq, Eq)]
23struct DocumentMetrics {
24    node_count: usize,
25    max_depth: usize,
26}
27
28trait XmlParserBackend {
29    fn validate_after_bounded_projection(
30        xml: &str,
31        settings: DocumentParseSettings,
32        bounded: DocumentMetrics,
33        budget: Option<&XmlParseWorkBudget>,
34    ) -> Result<DocumentMetrics, XmlDocumentError>;
35}
36
37#[cfg(not(feature = "xml-backend-xmloxide"))]
38struct RoxmltreeParser;
39
40#[cfg(not(feature = "xml-backend-xmloxide"))]
41impl XmlParserBackend for RoxmltreeParser {
42    fn validate_after_bounded_projection(
43        _xml: &str,
44        _settings: DocumentParseSettings,
45        bounded: DocumentMetrics,
46        _budget: Option<&XmlParseWorkBudget>,
47    ) -> Result<DocumentMetrics, XmlDocumentError> {
48        // The retained semantic projection below is itself the roxmltree
49        // validation pass, so this adapter must not parse the document twice.
50        Ok(bounded)
51    }
52}
53
54#[cfg(feature = "xml-backend-xmloxide")]
55struct XmloxideParser;
56
57#[cfg(feature = "xml-backend-xmloxide")]
58impl XmlParserBackend for XmloxideParser {
59    fn validate_after_bounded_projection(
60        xml: &str,
61        settings: DocumentParseSettings,
62        bounded: DocumentMetrics,
63        budget: Option<&XmlParseWorkBudget>,
64    ) -> Result<DocumentMetrics, XmlDocumentError> {
65        // xmloxide 0.5 has no aggregate node-count parser option. Requiring a
66        // bounded retained projection here ensures wide input is rejected
67        // before xmloxide can allocate its tree.
68        charge_parse_work(budget, xml.len())?;
69        let options = xmloxide_parse_options(settings);
70        xmloxide::parser::parse_str_with_options(xml, &options).map_err(|error| {
71            if error.message.contains("maximum nesting depth exceeded") {
72                XmlDocumentError::DocumentTooDeep {
73                    maximum: settings.depth_limit,
74                    actual: settings.depth_limit.saturating_add(1),
75                }
76            } else {
77                XmlDocumentError::BackendParse {
78                    backend: "xmloxide",
79                    message: error.to_string(),
80                }
81            }
82        })?;
83        // Aggregate node policy is defined over the shared retained semantic
84        // projection. xmloxide can expose a different lexical node count for
85        // contiguous entity/CDATA text, so it must not replace those metrics.
86        Ok(bounded)
87    }
88}
89
90#[cfg(feature = "xml-backend-xmloxide")]
91fn xmloxide_parse_options(settings: DocumentParseSettings) -> xmloxide::parser::ParseOptions {
92    let maximum_depth = u32::try_from(settings.depth_limit).unwrap_or(u32::MAX);
93    // Retained semantic nodes and source bytes are different resources from
94    // xmloxide's per-element and post-expansion lexical limits. Disable those
95    // backend-specific caps so selecting a parser cannot narrow the shared
96    // document contract; the bounded preflight remains authoritative.
97    xmloxide::parser::ParseOptions::default()
98        .max_depth(maximum_depth)
99        .max_attributes(u32::MAX)
100        .max_attribute_length(usize::MAX)
101        .max_text_length(usize::MAX)
102        .max_name_length(usize::MAX)
103        .max_entity_expansions(u32::MAX)
104}
105
106#[cfg(feature = "xml-backend-xmloxide")]
107type SelectedXmlParser = XmloxideParser;
108#[cfg(not(feature = "xml-backend-xmloxide"))]
109type SelectedXmlParser = RoxmltreeParser;
110
111static NEXT_DOCUMENT_ID: AtomicU64 = AtomicU64::new(1);
112const VALIDATION_WRAPPER_NS: &str = "urn:xml-sec:owned-document-validation";
113const VALIDATION_WRAPPER_OPEN: &str = "<xmlsec_owned_document:wrapper xmlns:xmlsec_owned_document=\"urn:xml-sec:owned-document-validation\">";
114const VALIDATION_WRAPPER_CLOSE: &str = "</xmlsec_owned_document:wrapper>";
115// Validation adds one wrapper element and can prevent text-node merging at
116// both replacement boundaries. The committed candidate uses the real ceiling.
117const VALIDATION_WRAPPER_NODE_OVERHEAD: u32 = 3;
118
119#[cfg(test)]
120fn selected_parser_passes() -> usize {
121    2 + usize::from(cfg!(feature = "xml-backend-xmloxide"))
122}
123
124/// Process-local provenance of one owned XML document.
125#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
126pub struct DocumentIdentity(u64);
127
128/// Identity of one tree node in one immutable document generation.
129#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
130pub struct NodeIdentity {
131    document: DocumentIdentity,
132    generation: u64,
133    backend: NodeId,
134}
135
136/// Identity of one XPath attribute node and its owning element.
137#[derive(Clone, Debug, PartialEq, Eq, Hash)]
138pub struct AttributeIdentity {
139    owner: NodeIdentity,
140    namespace: Option<String>,
141    local_name: String,
142}
143
144/// Identity of one XPath namespace node and its owning element.
145#[derive(Clone, Debug, PartialEq, Eq, Hash)]
146pub struct NamespaceIdentity {
147    owner: NodeIdentity,
148    prefix: String,
149    uri: String,
150}
151
152/// Deterministic total order for tree, namespace, and attribute identities.
153#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
154pub struct SemanticOrder {
155    node: usize,
156    phase: u8,
157    position: usize,
158}
159
160impl AttributeIdentity {
161    /// Return the element that owns this XPath attribute node.
162    #[must_use]
163    pub const fn owner(&self) -> NodeIdentity {
164        self.owner
165    }
166
167    /// Return the attribute namespace URI.
168    #[must_use]
169    pub fn namespace(&self) -> Option<&str> {
170        self.namespace.as_deref()
171    }
172
173    /// Return the attribute local name.
174    #[must_use]
175    pub fn local_name(&self) -> &str {
176        &self.local_name
177    }
178}
179
180impl NamespaceIdentity {
181    /// Return the element that owns this XPath namespace node.
182    #[must_use]
183    pub const fn owner(&self) -> NodeIdentity {
184        self.owner
185    }
186
187    /// Return the namespace prefix, or an empty string for the default namespace.
188    #[must_use]
189    pub fn prefix(&self) -> &str {
190        &self.prefix
191    }
192
193    /// Return the namespace URI.
194    #[must_use]
195    pub fn uri(&self) -> &str {
196        &self.uri
197    }
198}
199
200#[derive(Clone, Copy)]
201pub(crate) struct DocumentParseSettings {
202    pub(crate) allow_dtd: bool,
203    pub(crate) nodes_limit: u32,
204    pub(crate) depth_limit: usize,
205    pub(crate) max_bytes: usize,
206}
207
208impl Default for DocumentParseSettings {
209    fn default() -> Self {
210        Self {
211            allow_dtd: false,
212            nodes_limit: crate::hard_limits::XML_DOCUMENT_NODE_CEILING,
213            depth_limit: crate::hard_limits::XML_DOCUMENT_DEPTH_CEILING,
214            max_bytes: crate::hard_limits::XML_DOCUMENT_BYTE_CEILING,
215        }
216    }
217}
218
219#[cfg(any(feature = "xmldsig", feature = "xmlenc", test))]
220impl DocumentParseSettings {
221    #[cfg(test)]
222    pub(crate) const fn new(allow_dtd: bool, nodes_limit: u32, max_bytes: usize) -> Self {
223        Self::new_with_depth(
224            allow_dtd,
225            nodes_limit,
226            crate::hard_limits::XML_DOCUMENT_DEPTH_CEILING,
227            max_bytes,
228        )
229    }
230
231    pub(crate) const fn new_with_depth(
232        allow_dtd: bool,
233        nodes_limit: u32,
234        depth_limit: usize,
235        max_bytes: usize,
236    ) -> Self {
237        Self {
238            allow_dtd,
239            nodes_limit,
240            depth_limit,
241            max_bytes,
242        }
243    }
244
245    pub(crate) fn from_policy(
246        xml: &crate::policy::XmlInputPolicy,
247        resources: &crate::policy::ResourcePolicy,
248    ) -> Self {
249        Self::new_with_depth(
250            xml.allow_internal_dtd,
251            resources.effective_xml_nodes(),
252            resources.max_xml_depth,
253            resources.max_xml_document_bytes,
254        )
255    }
256}
257
258/// Monotonic parser-work allowance shared by one XML Security operation.
259///
260/// Every byte handed to the XML parser is charged before parsing, including
261/// structural-validation candidates, staged copies, retries, and committed
262/// document generations. Failed work remains charged so nested helpers cannot
263/// reset or reuse the allowance.
264pub(crate) struct XmlParseWorkBudget {
265    #[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
266    remaining: Cell<usize>,
267    #[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
268    maximum: usize,
269}
270
271impl XmlParseWorkBudget {
272    #[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
273    pub(crate) fn from_resources(resources: &crate::policy::ResourcePolicy) -> Self {
274        Self {
275            remaining: Cell::new(resources.max_xml_parse_work_bytes),
276            maximum: resources.max_xml_parse_work_bytes,
277        }
278    }
279
280    #[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
281    pub(crate) fn charge_policy(&self, bytes: usize) -> Result<(), crate::policy::PolicyViolation> {
282        let consumed = self.maximum.saturating_sub(self.remaining.get());
283        let Some(remaining) = self.remaining.get().checked_sub(bytes) else {
284            self.remaining.set(0);
285            return Err(crate::policy::PolicyViolation::ResourceLimit {
286                resource: crate::policy::resource_name::XML_PARSE_WORK_BYTES,
287                maximum: self.maximum,
288                actual: consumed.saturating_add(bytes),
289            });
290        };
291        self.remaining.set(remaining);
292        Ok(())
293    }
294
295    #[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
296    pub(crate) fn charge(&self, bytes: usize) -> Result<(), XmlDocumentError> {
297        self.charge_policy(bytes).map_err(Into::into)
298    }
299
300    #[cfg(all(test, any(feature = "xmldsig", feature = "xmlenc")))]
301    fn with_limit(maximum: usize) -> Self {
302        Self {
303            remaining: Cell::new(maximum),
304            maximum,
305        }
306    }
307
308    #[cfg(all(test, any(feature = "xmldsig", feature = "xmlenc")))]
309    pub(crate) fn consumed(&self) -> usize {
310        self.maximum.saturating_sub(self.remaining.get())
311    }
312}
313
314fn charge_parse_work(
315    budget: Option<&XmlParseWorkBudget>,
316    bytes: usize,
317) -> Result<(), XmlDocumentError> {
318    #[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
319    if let Some(budget) = budget {
320        budget.charge(bytes)?;
321    }
322    #[cfg(not(any(feature = "xmldsig", feature = "xmlenc")))]
323    let _ = (budget, bytes);
324    Ok(())
325}
326
327struct ContentReplacementEdit<'a> {
328    range: std::ops::Range<usize>,
329    replacement: &'a str,
330    self_closing: Option<SelfClosingContentEdit>,
331}
332
333struct SelfClosingContentEdit {
334    prefix_end: usize,
335    qualified_name: String,
336}
337
338#[cfg(feature = "xmldsig")]
339fn is_unescaped_base64_text(value: &str) -> bool {
340    value
341        .bytes()
342        .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'+' | b'/' | b'='))
343}
344
345#[cfg(feature = "xmldsig")]
346fn validate_base64_replacements(
347    replacements: &[(NodeIdentity, String)],
348) -> Result<(), XmlDocumentError> {
349    if replacements
350        .iter()
351        .any(|(_, value)| !is_unescaped_base64_text(value))
352    {
353        return Err(XmlDocumentError::InvalidReplacement(
354            "generated base64 replacement contains XML markup".into(),
355        ));
356    }
357    Ok(())
358}
359
360impl ContentReplacementEdit<'_> {
361    fn output_len(&self) -> Option<usize> {
362        let Some(expansion) = &self.self_closing else {
363            return Some(self.replacement.len());
364        };
365        expansion
366            .prefix_end
367            .checked_add(1)
368            .and_then(|length| length.checked_add(self.replacement.len()))
369            .and_then(|length| length.checked_add(2))
370            .and_then(|length| length.checked_add(expansion.qualified_name.len()))
371            .and_then(|length| length.checked_add(1))
372    }
373
374    fn validation_output_len(&self) -> Option<usize> {
375        self.output_len()?
376            .checked_add(VALIDATION_WRAPPER_OPEN.len())?
377            .checked_add(VALIDATION_WRAPPER_CLOSE.len())
378    }
379
380    fn write_output(&self, source: &str, output: &mut String) {
381        if let Some(expansion) = &self.self_closing {
382            output.push_str(&source[self.range.start..self.range.start + expansion.prefix_end]);
383            output.push('>');
384        }
385        output.push_str(self.replacement);
386        if let Some(expansion) = &self.self_closing {
387            output.push_str("</");
388            output.push_str(&expansion.qualified_name);
389            output.push('>');
390        }
391    }
392
393    fn write_validation_output(&self, source: &str, output: &mut String) -> std::ops::Range<usize> {
394        if let Some(expansion) = &self.self_closing {
395            output.push_str(&source[self.range.start..self.range.start + expansion.prefix_end]);
396            output.push('>');
397        }
398        let wrapper_start = output.len();
399        output.push_str(VALIDATION_WRAPPER_OPEN);
400        output.push_str(self.replacement);
401        output.push_str(VALIDATION_WRAPPER_CLOSE);
402        let wrapper_range = wrapper_start..output.len();
403        if let Some(expansion) = &self.self_closing {
404            output.push_str("</");
405            output.push_str(&expansion.qualified_name);
406            output.push('>');
407        }
408        wrapper_range
409    }
410}
411
412#[derive(Debug)]
413struct DocumentIndexes {
414    order: HashMap<NodeId, usize>,
415    default_ids: HashMap<String, Option<NodeId>>,
416    attributes_by_value: HashMap<(String, String), Vec<NodeId>>,
417}
418
419#[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
420mod policy_sealed {
421    pub trait Sealed {}
422}
423
424/// Operation policy snapshots that can configure owned-document parsing.
425///
426/// This sealed trait keeps XML parser policy derived from the same immutable
427/// signing, verification, encryption, or decryption snapshot used by the
428/// subsequent operation. Applications cannot create a second parser-only
429/// configuration path that drifts from operation enforcement.
430#[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
431pub trait XmlDocumentPolicy: policy_sealed::Sealed {
432    #[doc(hidden)]
433    fn xml_input_policy(&self) -> &crate::policy::XmlInputPolicy;
434
435    #[doc(hidden)]
436    fn resource_policy(&self) -> &crate::policy::ResourcePolicy;
437}
438
439#[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
440macro_rules! impl_document_policy {
441    ($policy:ty) => {
442        impl policy_sealed::Sealed for $policy {}
443
444        impl XmlDocumentPolicy for $policy {
445            fn xml_input_policy(&self) -> &crate::policy::XmlInputPolicy {
446                &self.xml
447            }
448
449            fn resource_policy(&self) -> &crate::policy::ResourcePolicy {
450                &self.resources
451            }
452        }
453    };
454}
455
456#[cfg(feature = "xmldsig")]
457impl_document_policy!(crate::policy::SigningPolicy);
458#[cfg(feature = "xmldsig")]
459impl_document_policy!(crate::policy::VerificationPolicy);
460#[cfg(feature = "xmlenc")]
461impl_document_policy!(crate::policy::EncryptionPolicy);
462#[cfg(feature = "xmlenc")]
463impl_document_policy!(crate::policy::DecryptionPolicy);
464
465struct ParsedDocument<'input> {
466    document: Document<'input>,
467    indexes: DocumentIndexes,
468    node_count: usize,
469    max_depth: usize,
470}
471
472impl ParsedDocument<'_> {
473    const fn metrics(&self) -> DocumentMetrics {
474        DocumentMetrics {
475            node_count: self.node_count,
476            max_depth: self.max_depth,
477        }
478    }
479}
480
481self_cell!(
482    struct DocumentCell {
483        owner: String,
484
485        #[covariant]
486        dependent: ParsedDocument,
487    }
488);
489
490/// Errors from owned document parsing, identity validation, and mutation.
491#[derive(Debug, thiserror::Error)]
492pub enum XmlDocumentError {
493    /// The immutable operation policy contains invalid resource limits.
494    #[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
495    #[error("XML document policy violation: {0}")]
496    Policy(#[from] crate::policy::PolicyViolation),
497
498    /// The input exceeds the active XML document byte limit.
499    #[error("XML document exceeds the maximum size of {maximum} bytes: {actual} bytes")]
500    DocumentTooLarge {
501        /// Maximum accepted byte length.
502        maximum: usize,
503        /// Actual byte length.
504        actual: usize,
505    },
506    /// The input exceeds the active XML element nesting limit.
507    #[error("XML document exceeds the maximum element depth of {maximum}: {actual}")]
508    DocumentTooDeep {
509        /// Maximum accepted element nesting depth.
510        maximum: usize,
511        /// First observed element nesting depth beyond the limit.
512        actual: usize,
513    },
514    /// The XML parser rejected the document.
515    #[error("XML parsing error: {0}")]
516    Parse(#[from] roxmltree::Error),
517    /// The selected parser backend rejected the document.
518    #[error("XML parsing error from {backend}: {message}")]
519    BackendParse {
520        /// Compile-time selected parser backend.
521        backend: &'static str,
522        /// Stable human-readable parser diagnostic.
523        message: String,
524    },
525    /// An identity belongs to another document.
526    #[error("XML identity belongs to a different document")]
527    ForeignIdentity,
528    /// An identity belongs to an earlier document generation.
529    #[error("XML identity belongs to stale generation {identity}; current generation is {current}")]
530    StaleIdentity {
531        /// Generation captured by the identity.
532        identity: u64,
533        /// Current document generation.
534        current: u64,
535    },
536    /// The node no longer exists in the current parsed tree.
537    #[error("XML identity does not resolve to a node")]
538    MissingNode,
539    /// A mutation target is not an element.
540    #[error("XML mutation target must be an element")]
541    TargetNotElement,
542    /// Replacement content does not satisfy the requested structural shape.
543    #[error("invalid XML replacement: {0}")]
544    InvalidReplacement(String),
545    /// Process-local document provenance space is exhausted.
546    #[error("XML document identity space is exhausted")]
547    IdentityExhausted,
548    /// A projected mutation exceeds the caller's active node ceiling.
549    #[error("projected XML document exceeds the maximum node count of {maximum}")]
550    ProjectedNodeLimit {
551        /// Maximum accepted parser node count.
552        maximum: usize,
553    },
554}
555
556#[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
557impl XmlDocumentError {
558    pub(crate) fn into_policy_violation(
559        self,
560        settings: DocumentParseSettings,
561    ) -> Result<crate::policy::PolicyViolation, Self> {
562        let violation = match self {
563            Self::Policy(error) => error,
564            Self::DocumentTooLarge { maximum, actual } => {
565                crate::policy::PolicyViolation::ResourceLimit {
566                    resource: crate::policy::resource_name::XML_DOCUMENT,
567                    maximum,
568                    actual,
569                }
570            }
571            Self::DocumentTooDeep { maximum, actual } => {
572                crate::policy::PolicyViolation::ResourceLimit {
573                    resource: crate::policy::resource_name::XML_DEPTH,
574                    maximum,
575                    actual,
576                }
577            }
578            Self::Parse(roxmltree::Error::NodesLimitReached) => {
579                crate::policy::PolicyViolation::ResourceLimit {
580                    resource: crate::policy::resource_name::XML_NODES,
581                    maximum: settings.nodes_limit as usize,
582                    actual: settings.nodes_limit as usize + 1,
583                }
584            }
585            error => return Err(error),
586        };
587        Ok(violation)
588    }
589}
590
591/// Reusable owned XML document.
592///
593/// The parsed view is retained across read-only operations. Every successful
594/// mutation increments [`Self::generation`] and invalidates all identities
595/// captured from previous views.
596pub struct XmlDocument {
597    identity: DocumentIdentity,
598    generation: u64,
599    #[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
600    requires_internal_dtd: bool,
601    settings: DocumentParseSettings,
602    cell: DocumentCell,
603}
604
605/// Borrowed semantic view of one immutable [`XmlDocument`] generation.
606#[derive(Clone, Copy)]
607pub struct DocumentView<'a> {
608    identity: DocumentIdentity,
609    generation: u64,
610    parsed: &'a ParsedDocument<'a>,
611}
612
613impl XmlDocument {
614    /// Parse and own an XML document using conservative XML input defaults.
615    /// Borrowed input is size-checked before it is copied into owned storage.
616    pub fn parse(xml: impl AsRef<str> + Into<String>) -> Result<Self, XmlDocumentError> {
617        let settings = DocumentParseSettings::default();
618        let xml = own_bounded_xml(xml, settings.max_bytes)?;
619        Self::parse_with_settings(xml, settings)
620    }
621
622    /// Parse and own XML under the same immutable policy used by an operation.
623    /// The policy's byte ceiling is checked before borrowed input is copied.
624    #[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
625    pub fn parse_with_policy(
626        xml: impl AsRef<str> + Into<String>,
627        policy: &impl XmlDocumentPolicy,
628    ) -> Result<Self, XmlDocumentError> {
629        let resources = policy.resource_policy();
630        resources.validate()?;
631        let budget = XmlParseWorkBudget::from_resources(resources);
632        let xml = own_bounded_xml(xml, resources.max_xml_document_bytes)?;
633        Self::parse_with_settings_and_optional_budget(
634            xml,
635            DocumentParseSettings::from_policy(policy.xml_input_policy(), resources),
636            Some(&budget),
637        )
638    }
639
640    pub(crate) fn parse_with_settings(
641        xml: String,
642        settings: DocumentParseSettings,
643    ) -> Result<Self, XmlDocumentError> {
644        Self::parse_with_settings_and_optional_budget(xml, settings, None)
645    }
646
647    fn parse_with_settings_and_optional_budget(
648        xml: String,
649        settings: DocumentParseSettings,
650        budget: Option<&XmlParseWorkBudget>,
651    ) -> Result<Self, XmlDocumentError> {
652        if xml.len() > settings.max_bytes {
653            return Err(XmlDocumentError::DocumentTooLarge {
654                maximum: settings.max_bytes,
655                actual: xml.len(),
656            });
657        }
658        preflight_document_limits(&xml, settings, budget)?;
659        #[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
660        let requires_internal_dtd = document_requires_internal_dtd(&xml, settings, budget)?;
661        let cell = build_cell_after_preflight(xml, settings, budget)?;
662        let identity = allocate_document_identity(&NEXT_DOCUMENT_ID)?;
663        Ok(Self {
664            identity,
665            generation: 0,
666            #[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
667            requires_internal_dtd,
668            settings,
669            cell,
670        })
671    }
672
673    #[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
674    pub(crate) fn parse_with_settings_and_budget(
675        xml: String,
676        settings: DocumentParseSettings,
677        budget: &XmlParseWorkBudget,
678    ) -> Result<Self, XmlDocumentError> {
679        Self::parse_with_settings_and_optional_budget(xml, settings, Some(budget))
680    }
681
682    /// Return this document's stable provenance identity.
683    #[must_use]
684    pub const fn identity(&self) -> DocumentIdentity {
685        self.identity
686    }
687
688    /// Return the current mutation generation.
689    #[must_use]
690    pub const fn generation(&self) -> u64 {
691        self.generation
692    }
693
694    #[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
695    pub(crate) fn validate_xml_input_policy(
696        &self,
697        allow_internal_dtd: bool,
698    ) -> Result<(), crate::policy::PolicyViolation> {
699        if self.requires_internal_dtd && !allow_internal_dtd {
700            return Err(crate::policy::PolicyViolation::XmlInput {
701                reason: "owned document requires internal DTD support",
702            });
703        }
704        Ok(())
705    }
706
707    #[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
708    pub(crate) fn validate_operation_policy(
709        &self,
710        xml: &crate::policy::XmlInputPolicy,
711        resources: &crate::policy::ResourcePolicy,
712    ) -> Result<(), crate::policy::PolicyViolation> {
713        resources.validate()?;
714        self.validate_xml_input_policy(xml.allow_internal_dtd)?;
715        resources.validate_xml_document_len(self.as_xml().len())?;
716        self.with_view(|view| {
717            let node_count = view.node_count();
718            if node_count > resources.effective_xml_nodes() as usize {
719                return Err(crate::policy::PolicyViolation::ResourceLimit {
720                    resource: crate::policy::resource_name::XML_NODES,
721                    maximum: resources.effective_xml_nodes() as usize,
722                    actual: node_count,
723                });
724            }
725            let max_depth = view.max_depth();
726            if max_depth > resources.max_xml_depth {
727                return Err(crate::policy::PolicyViolation::ResourceLimit {
728                    resource: crate::policy::resource_name::XML_DEPTH,
729                    maximum: resources.max_xml_depth,
730                    actual: max_depth,
731                });
732            }
733            Ok(())
734        })
735    }
736
737    /// Return the deterministic serialized representation of this generation.
738    #[must_use]
739    pub fn as_xml(&self) -> &str {
740        self.cell.borrow_owner()
741    }
742
743    /// Consume the document and return its deterministic serialization.
744    #[must_use]
745    pub fn into_xml(self) -> String {
746        self.cell.into_owner()
747    }
748
749    #[cfg(feature = "xmldsig")]
750    pub(crate) fn staged_copy_with_budget(
751        &self,
752        settings: DocumentParseSettings,
753        budget: &XmlParseWorkBudget,
754    ) -> Result<Self, XmlDocumentError> {
755        Self::parse_with_settings_and_budget(self.as_xml().to_owned(), settings, budget)
756    }
757
758    #[cfg(feature = "xmldsig")]
759    pub(crate) fn commit_staged(&mut self, staged: Self) -> Result<(), XmlDocumentError> {
760        // Signing mutates and validates every staged generation under the active
761        // policy. Moving that retained cell preserves atomicity without parsing
762        // the complete signed document once more merely to commit it.
763        self.commit_cell(staged.cell)
764    }
765
766    /// Borrow the retained parsed view without reparsing.
767    pub fn with_view<R>(&self, operation: impl for<'a> FnOnce(DocumentView<'a>) -> R) -> R {
768        self.cell.with_dependent(|_, parsed| {
769            operation(DocumentView {
770                identity: self.identity,
771                generation: self.generation,
772                parsed,
773            })
774        })
775    }
776
777    /// Replace one complete element with a well-formed element fragment.
778    pub fn replace_element(
779        &mut self,
780        target: NodeIdentity,
781        replacement: &str,
782    ) -> Result<(), XmlDocumentError> {
783        let range = self.with_view(|view| {
784            let node = view.resolve_node(target)?;
785            if !node.is_element() {
786                return Err(XmlDocumentError::TargetNotElement);
787            }
788            Ok(node.range())
789        })?;
790        self.ensure_replacement_fits(&range, replacement.len(), self.settings.max_bytes)?;
791        self.validate_single_element_in_parent_context(
792            target,
793            replacement,
794            None,
795            self.settings,
796            None,
797        )?;
798        self.replace_range(range, replacement, None)
799    }
800
801    #[cfg(feature = "xmlenc")]
802    pub(crate) fn replace_element_with_budget(
803        &mut self,
804        target: NodeIdentity,
805        replacement: &str,
806        settings: DocumentParseSettings,
807        budget: &XmlParseWorkBudget,
808    ) -> Result<(), XmlDocumentError> {
809        let range = self.with_view(|view| {
810            let node = view.resolve_node(target)?;
811            if !node.is_element() {
812                return Err(XmlDocumentError::TargetNotElement);
813            }
814            Ok(node.range())
815        })?;
816        self.ensure_replacement_fits(&range, replacement.len(), settings.max_bytes)?;
817        self.validate_single_element_in_parent_context(
818            target,
819            replacement,
820            Some(settings.nodes_limit as usize),
821            settings,
822            Some(budget),
823        )?;
824        self.replace_range_with_settings(range, replacement, settings, Some(budget))
825    }
826
827    /// Replace one complete node with an XML fragment valid in its parent context.
828    ///
829    /// This operation is intended for XMLEnc Content replacement, where one
830    /// `EncryptedData` element can expand into multiple sibling nodes.
831    pub fn replace_node_with_fragment(
832        &mut self,
833        target: NodeIdentity,
834        replacement: &str,
835    ) -> Result<(), XmlDocumentError> {
836        let range =
837            self.with_view(|view| Ok::<_, XmlDocumentError>(view.resolve_node(target)?.range()))?;
838        self.ensure_replacement_fits(&range, replacement.len(), self.settings.max_bytes)?;
839        self.validate_fragment_in_parent_context(target, replacement, None, self.settings, None)?;
840        self.replace_range(range, replacement, None)
841    }
842
843    #[cfg(feature = "xmlenc")]
844    pub(crate) fn replace_node_with_fragment_with_budget(
845        &mut self,
846        target: NodeIdentity,
847        replacement: &str,
848        settings: DocumentParseSettings,
849        budget: &XmlParseWorkBudget,
850    ) -> Result<(), XmlDocumentError> {
851        let range =
852            self.with_view(|view| Ok::<_, XmlDocumentError>(view.resolve_node(target)?.range()))?;
853        self.ensure_replacement_fits(&range, replacement.len(), settings.max_bytes)?;
854        self.validate_fragment_in_parent_context(
855            target,
856            replacement,
857            Some(settings.nodes_limit as usize),
858            settings,
859            Some(budget),
860        )?;
861        self.replace_range_with_settings(range, replacement, settings, Some(budget))
862    }
863
864    /// Replace all children of one element with a well-formed XML fragment.
865    pub fn replace_content(
866        &mut self,
867        target: NodeIdentity,
868        replacement: &str,
869    ) -> Result<(), XmlDocumentError> {
870        self.replace_content_inner(target, replacement, None, self.settings, None)
871    }
872
873    #[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
874    pub(crate) fn replace_content_with_budget(
875        &mut self,
876        target: NodeIdentity,
877        replacement: &str,
878        settings: DocumentParseSettings,
879        budget: &XmlParseWorkBudget,
880    ) -> Result<(), XmlDocumentError> {
881        self.replace_content_inner(
882            target,
883            replacement,
884            Some(settings.nodes_limit as usize),
885            settings,
886            Some(budget),
887        )
888    }
889
890    fn replace_content_inner(
891        &mut self,
892        target: NodeIdentity,
893        replacement: &str,
894        maximum: Option<usize>,
895        settings: DocumentParseSettings,
896        budget: Option<&XmlParseWorkBudget>,
897    ) -> Result<(), XmlDocumentError> {
898        let (range, self_closing_expansion, serialized_len) = self.with_view(|view| {
899            let node = view.resolve_node(target)?;
900            if !node.is_element() {
901                return Err(XmlDocumentError::TargetNotElement);
902            }
903            let range = node.range();
904            let source = &view.xml()[range.clone()];
905            let (content, qualified_name, self_closing) = element_content_range(source)?;
906            if self_closing {
907                let slash = source.rfind("/>").ok_or_else(|| {
908                    XmlDocumentError::InvalidReplacement(
909                        "self-closing element has no terminator".into(),
910                    )
911                })?;
912                let serialized_len = slash
913                    .checked_add(replacement.len())
914                    .and_then(|length| length.checked_add(qualified_name.len()))
915                    .and_then(|length| length.checked_add(4))
916                    .ok_or(XmlDocumentError::DocumentTooLarge {
917                        maximum: settings.max_bytes,
918                        actual: usize::MAX,
919                    })?;
920                return Ok((range, Some((slash, qualified_name)), serialized_len));
921            }
922            let range = (range.start + content.start)..(range.start + content.end);
923            Ok((range, None, replacement.len()))
924        })?;
925        self.ensure_replacement_fits(&range, serialized_len, settings.max_bytes)?;
926        let serialized_replacement = if let Some((slash, qualified_name)) = self_closing_expansion {
927            let source = &self.as_xml()[range.clone()];
928            format!("{}>{replacement}</{qualified_name}>", &source[..slash])
929        } else {
930            replacement.to_owned()
931        };
932        self.validate_content_in_element_context(target, replacement, maximum, settings, budget)?;
933        if let Some(maximum) = maximum {
934            debug_assert_eq!(maximum, settings.nodes_limit as usize);
935            self.replace_range_with_settings(range, &serialized_replacement, settings, budget)
936        } else {
937            self.replace_range(range, &serialized_replacement, budget)
938        }
939    }
940
941    /// Replace the children of several non-overlapping elements atomically.
942    ///
943    /// All identities must belong to the current generation. Every boundary is
944    /// checked in one validation candidate, then the final candidate is parsed
945    /// and either commits as one new generation or leaves the document unchanged.
946    pub fn replace_contents(
947        &mut self,
948        replacements: &[(NodeIdentity, String)],
949    ) -> Result<(), XmlDocumentError> {
950        self.replace_contents_inner(replacements, self.settings, false, true, None)
951    }
952
953    #[cfg(all(feature = "xmldsig", test))]
954    pub(crate) fn replace_contents_with_budget(
955        &mut self,
956        replacements: &[(NodeIdentity, String)],
957        settings: DocumentParseSettings,
958        budget: &XmlParseWorkBudget,
959    ) -> Result<(), XmlDocumentError> {
960        self.replace_contents_inner(replacements, settings, true, true, Some(budget))
961    }
962
963    #[cfg(feature = "xmldsig")]
964    pub(crate) fn replace_base64_contents_with_budget(
965        &mut self,
966        replacements: &[(NodeIdentity, String)],
967        settings: DocumentParseSettings,
968        budget: &XmlParseWorkBudget,
969    ) -> Result<(), XmlDocumentError> {
970        validate_base64_replacements(replacements)?;
971        // Base64 cannot alter XML structure, so the committed candidate parse
972        // is also the boundary validation. Generic caller fragments retain the
973        // separate contextual validation path above.
974        self.replace_contents_inner(replacements, settings, true, false, Some(budget))
975    }
976
977    #[cfg(feature = "xmldsig")]
978    pub(crate) fn project_base64_contents(
979        &self,
980        replacements: &[(NodeIdentity, String)],
981        maximum_bytes: usize,
982    ) -> Result<String, XmlDocumentError> {
983        validate_base64_replacements(replacements)?;
984        let (edits, projected) =
985            self.prepare_content_replacement_edits(replacements, maximum_bytes)?;
986        Ok(self.render_content_replacement_edits(&edits, projected))
987    }
988
989    fn replace_contents_inner(
990        &mut self,
991        replacements: &[(NodeIdentity, String)],
992        settings: DocumentParseSettings,
993        policy_bounded: bool,
994        validate_fragments: bool,
995        budget: Option<&XmlParseWorkBudget>,
996    ) -> Result<(), XmlDocumentError> {
997        if replacements.is_empty() {
998            return Ok(());
999        }
1000        let (edits, projected) =
1001            self.prepare_content_replacement_edits(replacements, settings.max_bytes)?;
1002
1003        if validate_fragments {
1004            self.validate_content_edits(&edits, settings, policy_bounded, budget)?;
1005        }
1006
1007        let output = self.render_content_replacement_edits(&edits, projected);
1008        if policy_bounded {
1009            self.replace_serialized_with_settings(output, settings, budget)
1010        } else {
1011            self.replace_serialized(output, budget)
1012        }
1013    }
1014
1015    fn prepare_content_replacement_edits<'a>(
1016        &self,
1017        replacements: &'a [(NodeIdentity, String)],
1018        maximum_bytes: usize,
1019    ) -> Result<(Vec<ContentReplacementEdit<'a>>, usize), XmlDocumentError> {
1020        let mut targets = HashSet::with_capacity(replacements.len());
1021        if replacements
1022            .iter()
1023            .any(|(target, _)| !targets.insert(*target))
1024        {
1025            return Err(XmlDocumentError::InvalidReplacement(
1026                "replacement targets must be unique".into(),
1027            ));
1028        }
1029        let mut edits = self.with_view(|view| {
1030            replacements
1031                .iter()
1032                .map(|(target, replacement)| {
1033                    let node = view.resolve_node(*target)?;
1034                    if !node.is_element() {
1035                        return Err(XmlDocumentError::TargetNotElement);
1036                    }
1037                    let range = node.range();
1038                    let source = &view.xml()[range.clone()];
1039                    let (content, qualified_name, self_closing) = element_content_range(source)?;
1040                    if self_closing {
1041                        let slash = source.rfind("/>").ok_or_else(|| {
1042                            XmlDocumentError::InvalidReplacement(
1043                                "self-closing element has no terminator".into(),
1044                            )
1045                        })?;
1046                        Ok(ContentReplacementEdit {
1047                            range,
1048                            replacement,
1049                            self_closing: Some(SelfClosingContentEdit {
1050                                prefix_end: slash,
1051                                qualified_name,
1052                            }),
1053                        })
1054                    } else {
1055                        Ok(ContentReplacementEdit {
1056                            range: (range.start + content.start)..(range.start + content.end),
1057                            replacement,
1058                            self_closing: None,
1059                        })
1060                    }
1061                })
1062                .collect::<Result<Vec<_>, XmlDocumentError>>()
1063        })?;
1064        edits.sort_by_key(|edit| edit.range.start);
1065        if edits
1066            .windows(2)
1067            .any(|pair| pair[0].range.end > pair[1].range.start)
1068        {
1069            return Err(XmlDocumentError::InvalidReplacement(
1070                "replacement targets overlap".into(),
1071            ));
1072        }
1073        let projected = edits.iter().try_fold(self.as_xml().len(), |length, edit| {
1074            length
1075                .checked_sub(edit.range.len())
1076                .and_then(|length| length.checked_add(edit.output_len()?))
1077                .ok_or(XmlDocumentError::DocumentTooLarge {
1078                    maximum: maximum_bytes,
1079                    actual: usize::MAX,
1080                })
1081        })?;
1082        if projected > maximum_bytes {
1083            return Err(XmlDocumentError::DocumentTooLarge {
1084                maximum: maximum_bytes,
1085                actual: projected,
1086            });
1087        }
1088        Ok((edits, projected))
1089    }
1090
1091    fn render_content_replacement_edits(
1092        &self,
1093        edits: &[ContentReplacementEdit<'_>],
1094        projected: usize,
1095    ) -> String {
1096        let mut output = String::with_capacity(projected);
1097        let mut cursor = 0;
1098        for edit in edits {
1099            output.push_str(&self.as_xml()[cursor..edit.range.start]);
1100            edit.write_output(self.as_xml(), &mut output);
1101            cursor = edit.range.end;
1102        }
1103        output.push_str(&self.as_xml()[cursor..]);
1104        debug_assert_eq!(output.len(), projected);
1105        output
1106    }
1107
1108    fn validate_content_edits(
1109        &self,
1110        edits: &[ContentReplacementEdit<'_>],
1111        settings: DocumentParseSettings,
1112        policy_bounded: bool,
1113        budget: Option<&XmlParseWorkBudget>,
1114    ) -> Result<(), XmlDocumentError> {
1115        let projected = edits.iter().try_fold(self.as_xml().len(), |length, edit| {
1116            length
1117                .checked_sub(edit.range.len())
1118                .and_then(|length| length.checked_add(edit.validation_output_len()?))
1119                .ok_or_else(|| {
1120                    XmlDocumentError::InvalidReplacement("validation length overflow".into())
1121                })
1122        })?;
1123        let mut candidate = String::with_capacity(projected);
1124        let mut wrapper_ranges = Vec::with_capacity(edits.len());
1125        let mut cursor = 0;
1126        for edit in edits {
1127            candidate.push_str(&self.as_xml()[cursor..edit.range.start]);
1128            wrapper_ranges.push(edit.write_validation_output(self.as_xml(), &mut candidate));
1129            cursor = edit.range.end;
1130        }
1131        candidate.push_str(&self.as_xml()[cursor..]);
1132        debug_assert_eq!(candidate.len(), projected);
1133
1134        let active_nodes_limit = settings.nodes_limit as usize;
1135        let wrapper_allowance = edits.len();
1136        let validation_nodes_limit = active_nodes_limit
1137            .checked_add(wrapper_allowance)
1138            .and_then(|maximum| u32::try_from(maximum).ok())
1139            .unwrap_or(u32::MAX);
1140        let parsed = build_cell(
1141            candidate,
1142            DocumentParseSettings {
1143                nodes_limit: validation_nodes_limit,
1144                depth_limit: settings.depth_limit.saturating_add(1),
1145                max_bytes: projected,
1146                ..settings
1147            },
1148            budget,
1149        )
1150        .map_err(|error| match (policy_bounded, error) {
1151            (true, XmlDocumentError::Parse(roxmltree::Error::NodesLimitReached)) => {
1152                XmlDocumentError::ProjectedNodeLimit {
1153                    maximum: active_nodes_limit,
1154                }
1155            }
1156            (_, XmlDocumentError::DocumentTooDeep { actual, .. }) => {
1157                XmlDocumentError::DocumentTooDeep {
1158                    maximum: settings.depth_limit,
1159                    actual: actual.saturating_sub(1),
1160                }
1161            }
1162            (_, error) => error,
1163        })?;
1164        parsed.with_dependent(|_, document| validate_wrappers(document, &wrapper_ranges))
1165    }
1166
1167    /// Append a well-formed XML fragment as the last child of an element.
1168    pub fn append_child(
1169        &mut self,
1170        target: NodeIdentity,
1171        child: &str,
1172    ) -> Result<(), XmlDocumentError> {
1173        self.append_child_inner(target, child, None, self.settings, true, None)
1174    }
1175
1176    #[cfg(all(feature = "xmldsig", test))]
1177    pub(crate) fn append_child_with_budget(
1178        &mut self,
1179        target: NodeIdentity,
1180        child: &str,
1181        settings: DocumentParseSettings,
1182        budget: &XmlParseWorkBudget,
1183    ) -> Result<(), XmlDocumentError> {
1184        self.append_child_inner(
1185            target,
1186            child,
1187            Some(settings.nodes_limit as usize),
1188            settings,
1189            true,
1190            Some(budget),
1191        )
1192    }
1193
1194    #[cfg(feature = "xmldsig")]
1195    pub(crate) fn append_generated_child_with_budget(
1196        &mut self,
1197        target: NodeIdentity,
1198        child: &str,
1199        settings: DocumentParseSettings,
1200        budget: &XmlParseWorkBudget,
1201    ) -> Result<(), XmlDocumentError> {
1202        // SignatureBuilder serializes this fragment with quick-xml. Parsing the
1203        // final candidate once validates both the generated child and its
1204        // document context without a redundant wrapper-document pass.
1205        self.append_child_inner(
1206            target,
1207            child,
1208            Some(settings.nodes_limit as usize),
1209            settings,
1210            false,
1211            Some(budget),
1212        )
1213    }
1214
1215    fn append_child_inner(
1216        &mut self,
1217        target: NodeIdentity,
1218        child: &str,
1219        maximum: Option<usize>,
1220        settings: DocumentParseSettings,
1221        validate_context: bool,
1222        budget: Option<&XmlParseWorkBudget>,
1223    ) -> Result<(), XmlDocumentError> {
1224        let projected = self.projected_child_append_len(target, child.len())?;
1225        if projected > settings.max_bytes {
1226            return Err(XmlDocumentError::DocumentTooLarge {
1227                maximum: settings.max_bytes,
1228                actual: projected,
1229            });
1230        }
1231        let (range, replacement) = self.with_view(|view| {
1232            let node = view.resolve_node(target)?;
1233            if !node.is_element() {
1234                return Err(XmlDocumentError::TargetNotElement);
1235            }
1236            let range = node.range();
1237            let source = &view.xml()[range.clone()];
1238            let (content, qualified_name, self_closing) = element_content_range(source)?;
1239            if self_closing {
1240                let slash = source.rfind("/>").ok_or_else(|| {
1241                    XmlDocumentError::InvalidReplacement(
1242                        "self-closing element has no terminator".into(),
1243                    )
1244                })?;
1245                return Ok((
1246                    range,
1247                    format!("{}>{child}</{qualified_name}>", &source[..slash]),
1248                ));
1249            }
1250            Ok((
1251                (range.start + content.end)..(range.start + content.end),
1252                child.to_owned(),
1253            ))
1254        })?;
1255        self.ensure_replacement_fits(&range, replacement.len(), settings.max_bytes)?;
1256        if validate_context {
1257            self.validate_content_in_element_context(target, child, maximum, settings, budget)?;
1258        }
1259        if let Some(maximum) = maximum {
1260            debug_assert_eq!(maximum, settings.nodes_limit as usize);
1261            self.replace_range_with_settings(range, &replacement, settings, budget)
1262        } else {
1263            self.replace_range(range, &replacement, budget)
1264        }
1265    }
1266
1267    pub(crate) fn replace_serialized(
1268        &mut self,
1269        xml: String,
1270        budget: Option<&XmlParseWorkBudget>,
1271    ) -> Result<(), XmlDocumentError> {
1272        let next = build_cell(xml, self.settings, budget)?;
1273        self.commit_cell(next)
1274    }
1275
1276    pub(crate) fn replace_serialized_with_settings(
1277        &mut self,
1278        xml: String,
1279        settings: DocumentParseSettings,
1280        budget: Option<&XmlParseWorkBudget>,
1281    ) -> Result<(), XmlDocumentError> {
1282        let next = build_cell(xml, settings, budget).map_err(|error| match error {
1283            XmlDocumentError::Parse(roxmltree::Error::NodesLimitReached) => {
1284                XmlDocumentError::ProjectedNodeLimit {
1285                    maximum: settings.nodes_limit as usize,
1286                }
1287            }
1288            error => error,
1289        })?;
1290        self.commit_cell(next)
1291    }
1292
1293    fn commit_cell(&mut self, next: DocumentCell) -> Result<(), XmlDocumentError> {
1294        let next_generation = self.generation.checked_add(1).ok_or_else(|| {
1295            XmlDocumentError::InvalidReplacement("document generation overflow".into())
1296        })?;
1297        self.cell = next;
1298        self.generation = next_generation;
1299        Ok(())
1300    }
1301
1302    #[cfg(feature = "xmldsig")]
1303    pub(crate) fn projected_content_replacement_len(
1304        &self,
1305        target: NodeIdentity,
1306        replacement_len: usize,
1307    ) -> Result<usize, XmlDocumentError> {
1308        self.with_view(|view| {
1309            let node = view.resolve_node(target)?;
1310            if !node.is_element() {
1311                return Err(XmlDocumentError::TargetNotElement);
1312            }
1313            let range = node.range();
1314            let source = &view.xml()[range.clone()];
1315            let (content, qualified_name, self_closing) = element_content_range(source)?;
1316            let removed = if self_closing {
1317                range.len()
1318            } else {
1319                content.len()
1320            };
1321            let added = if self_closing {
1322                let slash = source.rfind("/>").ok_or_else(|| {
1323                    XmlDocumentError::InvalidReplacement(
1324                        "self-closing element has no terminator".into(),
1325                    )
1326                })?;
1327                slash
1328                    .checked_add(1)
1329                    .and_then(|length| length.checked_add(replacement_len))
1330                    .and_then(|length| length.checked_add(2))
1331                    .and_then(|length| length.checked_add(qualified_name.len()))
1332                    .and_then(|length| length.checked_add(1))
1333                    .ok_or_else(|| {
1334                        XmlDocumentError::InvalidReplacement(
1335                            "content replacement length overflow".into(),
1336                        )
1337                    })?
1338            } else {
1339                replacement_len
1340            };
1341            view.xml()
1342                .len()
1343                .checked_sub(removed)
1344                .and_then(|length| length.checked_add(added))
1345                .ok_or_else(|| {
1346                    XmlDocumentError::InvalidReplacement(
1347                        "content replacement length overflow".into(),
1348                    )
1349                })
1350        })
1351    }
1352
1353    pub(crate) fn projected_child_append_len(
1354        &self,
1355        target: NodeIdentity,
1356        child_len: usize,
1357    ) -> Result<usize, XmlDocumentError> {
1358        self.with_view(|view| {
1359            let node = view.resolve_node(target)?;
1360            if !node.is_element() {
1361                return Err(XmlDocumentError::TargetNotElement);
1362            }
1363            let range = node.range();
1364            let source = &view.xml()[range.clone()];
1365            let (_, qualified_name, self_closing) = element_content_range(source)?;
1366            let (removed, added) = if self_closing {
1367                let slash = source.rfind("/>").ok_or_else(|| {
1368                    XmlDocumentError::InvalidReplacement(
1369                        "self-closing element has no terminator".into(),
1370                    )
1371                })?;
1372                let expanded = slash
1373                    .checked_add(1)
1374                    .and_then(|length| length.checked_add(child_len))
1375                    .and_then(|length| length.checked_add(2))
1376                    .and_then(|length| length.checked_add(qualified_name.len()))
1377                    .and_then(|length| length.checked_add(1))
1378                    .ok_or_else(|| {
1379                        XmlDocumentError::InvalidReplacement("child append length overflow".into())
1380                    })?;
1381                (range.len(), expanded)
1382            } else {
1383                (0, child_len)
1384            };
1385            view.xml()
1386                .len()
1387                .checked_sub(removed)
1388                .and_then(|length| length.checked_add(added))
1389                .ok_or_else(|| {
1390                    XmlDocumentError::InvalidReplacement("child append length overflow".into())
1391                })
1392        })
1393    }
1394
1395    fn replace_range(
1396        &mut self,
1397        range: std::ops::Range<usize>,
1398        replacement: &str,
1399        budget: Option<&XmlParseWorkBudget>,
1400    ) -> Result<(), XmlDocumentError> {
1401        let output = self.replaced_range(range, replacement, self.settings.max_bytes)?;
1402        self.replace_serialized(output, budget)
1403    }
1404
1405    fn replace_range_with_settings(
1406        &mut self,
1407        range: std::ops::Range<usize>,
1408        replacement: &str,
1409        settings: DocumentParseSettings,
1410        budget: Option<&XmlParseWorkBudget>,
1411    ) -> Result<(), XmlDocumentError> {
1412        let output = self.replaced_range(range, replacement, settings.max_bytes)?;
1413        self.replace_serialized_with_settings(output, settings, budget)
1414    }
1415
1416    fn replaced_range(
1417        &self,
1418        range: std::ops::Range<usize>,
1419        replacement: &str,
1420        maximum_bytes: usize,
1421    ) -> Result<String, XmlDocumentError> {
1422        let projected = self.ensure_replacement_fits(&range, replacement.len(), maximum_bytes)?;
1423        let mut output = String::with_capacity(projected);
1424        output.push_str(&self.as_xml()[..range.start]);
1425        output.push_str(replacement);
1426        output.push_str(&self.as_xml()[range.end..]);
1427        Ok(output)
1428    }
1429
1430    fn ensure_replacement_fits(
1431        &self,
1432        range: &std::ops::Range<usize>,
1433        replacement_len: usize,
1434        maximum_bytes: usize,
1435    ) -> Result<usize, XmlDocumentError> {
1436        let projected = self
1437            .as_xml()
1438            .len()
1439            .checked_sub(range.len())
1440            .and_then(|length| length.checked_add(replacement_len))
1441            .ok_or(XmlDocumentError::DocumentTooLarge {
1442                maximum: maximum_bytes,
1443                actual: usize::MAX,
1444            })?;
1445        if projected > maximum_bytes {
1446            return Err(XmlDocumentError::DocumentTooLarge {
1447                maximum: maximum_bytes,
1448                actual: projected,
1449            });
1450        }
1451        Ok(projected)
1452    }
1453
1454    fn validate_single_element_in_parent_context(
1455        &self,
1456        target: NodeIdentity,
1457        replacement: &str,
1458        maximum: Option<usize>,
1459        settings: DocumentParseSettings,
1460        budget: Option<&XmlParseWorkBudget>,
1461    ) -> Result<(), XmlDocumentError> {
1462        let (parsed, wrapper_range) =
1463            self.parse_fragment_in_parent_context(target, replacement, maximum, settings, budget)?;
1464        parsed.with_dependent(|_, parsed| {
1465            let wrapper = validation_wrapper(parsed, wrapper_range.clone())?;
1466            if wrapper.children().filter(Node::is_element).count() != 1
1467                || wrapper.children().any(|node| {
1468                    !node.is_element()
1469                        && !node.is_comment()
1470                        && !node.is_pi()
1471                        && node.text().is_none_or(|text| !text.trim().is_empty())
1472                })
1473            {
1474                return Err(XmlDocumentError::InvalidReplacement(
1475                    "element replacement must contain exactly one element".into(),
1476                ));
1477            }
1478            Ok(())
1479        })
1480    }
1481
1482    fn validate_fragment_in_parent_context(
1483        &self,
1484        target: NodeIdentity,
1485        replacement: &str,
1486        maximum: Option<usize>,
1487        settings: DocumentParseSettings,
1488        budget: Option<&XmlParseWorkBudget>,
1489    ) -> Result<(), XmlDocumentError> {
1490        self.parse_fragment_in_parent_context(target, replacement, maximum, settings, budget)
1491            .map(|_| ())
1492    }
1493
1494    fn parse_fragment_in_parent_context(
1495        &self,
1496        target: NodeIdentity,
1497        replacement: &str,
1498        maximum: Option<usize>,
1499        settings: DocumentParseSettings,
1500        budget: Option<&XmlParseWorkBudget>,
1501    ) -> Result<(DocumentCell, std::ops::Range<usize>), XmlDocumentError> {
1502        let range =
1503            self.with_view(|view| Ok::<_, XmlDocumentError>(view.resolve_node(target)?.range()))?;
1504        self.parse_wrapped_range(range, replacement, maximum, settings, budget)
1505    }
1506
1507    fn validate_content_in_element_context(
1508        &self,
1509        target: NodeIdentity,
1510        replacement: &str,
1511        maximum: Option<usize>,
1512        settings: DocumentParseSettings,
1513        budget: Option<&XmlParseWorkBudget>,
1514    ) -> Result<(), XmlDocumentError> {
1515        let (element_range, content_range, qualified_name, self_closing) =
1516            self.with_view(|view| {
1517                let target = view.resolve_node(target)?;
1518                if !target.is_element() {
1519                    return Err(XmlDocumentError::TargetNotElement);
1520                }
1521                let element_range = target.range();
1522                let source = &view.xml()[element_range.clone()];
1523                let (content, qualified_name, self_closing) = element_content_range(source)?;
1524                Ok::<_, XmlDocumentError>((
1525                    element_range.clone(),
1526                    (element_range.start + content.start)..(element_range.start + content.end),
1527                    qualified_name,
1528                    self_closing,
1529                ))
1530            })?;
1531        if !self_closing {
1532            return self
1533                .parse_wrapped_range(content_range, replacement, maximum, settings, budget)
1534                .map(|_| ());
1535        }
1536
1537        let source = &self.as_xml()[element_range.clone()];
1538        let slash = source.rfind("/>").ok_or_else(|| {
1539            XmlDocumentError::InvalidReplacement("self-closing element has no terminator".into())
1540        })?;
1541        let wrapped = wrapped_fragment(replacement);
1542        let wrapper_start = element_range.start + slash + 1;
1543        let expanded = format!("{}>{wrapped}</{qualified_name}>", &source[..slash]);
1544        self.parse_wrapped_edit(
1545            element_range,
1546            &expanded,
1547            wrapper_start..(wrapper_start + wrapped.len()),
1548            maximum,
1549            settings,
1550            budget,
1551        )
1552        .map(|_| ())
1553    }
1554
1555    fn parse_wrapped_range(
1556        &self,
1557        range: std::ops::Range<usize>,
1558        replacement: &str,
1559        maximum: Option<usize>,
1560        settings: DocumentParseSettings,
1561        budget: Option<&XmlParseWorkBudget>,
1562    ) -> Result<(DocumentCell, std::ops::Range<usize>), XmlDocumentError> {
1563        let wrapped = wrapped_fragment(replacement);
1564        let wrapper_range = range.start..(range.start + wrapped.len());
1565        self.parse_wrapped_edit(range, &wrapped, wrapper_range, maximum, settings, budget)
1566    }
1567
1568    fn parse_wrapped_edit(
1569        &self,
1570        range: std::ops::Range<usize>,
1571        replacement: &str,
1572        wrapper_range: std::ops::Range<usize>,
1573        maximum: Option<usize>,
1574        settings: DocumentParseSettings,
1575        budget: Option<&XmlParseWorkBudget>,
1576    ) -> Result<(DocumentCell, std::ops::Range<usize>), XmlDocumentError> {
1577        let projected = self
1578            .as_xml()
1579            .len()
1580            .checked_sub(range.len())
1581            .and_then(|length| length.checked_add(replacement.len()))
1582            .ok_or_else(|| {
1583                XmlDocumentError::InvalidReplacement("validation length overflow".into())
1584            })?;
1585        let mut candidate = String::with_capacity(projected);
1586        candidate.push_str(&self.as_xml()[..range.start]);
1587        candidate.push_str(replacement);
1588        candidate.push_str(&self.as_xml()[range.end..]);
1589        let active_nodes_limit = maximum
1590            .map(|maximum| maximum.min(settings.nodes_limit as usize))
1591            .map(|maximum| u32::try_from(maximum).unwrap_or(u32::MAX))
1592            .unwrap_or(settings.nodes_limit);
1593        let parsed = build_cell(
1594            candidate,
1595            DocumentParseSettings {
1596                nodes_limit: active_nodes_limit.saturating_add(VALIDATION_WRAPPER_NODE_OVERHEAD),
1597                // Wrapper markup is validation scaffolding, not document input.
1598                // The committed candidate is checked against the real ceiling.
1599                depth_limit: settings.depth_limit.saturating_add(1),
1600                max_bytes: projected,
1601                ..settings
1602            },
1603            budget,
1604        )
1605        .map_err(|error| match (maximum, error) {
1606            (Some(_), XmlDocumentError::Parse(roxmltree::Error::NodesLimitReached)) => {
1607                XmlDocumentError::ProjectedNodeLimit {
1608                    maximum: active_nodes_limit as usize,
1609                }
1610            }
1611            (_, XmlDocumentError::DocumentTooDeep { actual, .. }) => {
1612                XmlDocumentError::DocumentTooDeep {
1613                    maximum: settings.depth_limit,
1614                    actual: actual.saturating_sub(1),
1615                }
1616            }
1617            (_, error) => error,
1618        })?;
1619        parsed.with_dependent(|_, document| {
1620            validation_wrapper(document, wrapper_range.clone()).map(|_| ())
1621        })?;
1622        Ok((parsed, wrapper_range))
1623    }
1624}
1625
1626impl<'a> DocumentView<'a> {
1627    /// Return the owning document identity.
1628    #[must_use]
1629    pub const fn identity(self) -> DocumentIdentity {
1630        self.identity
1631    }
1632
1633    /// Return the immutable generation represented by this view.
1634    #[must_use]
1635    pub const fn generation(self) -> u64 {
1636        self.generation
1637    }
1638
1639    /// Return the serialized XML backing this parsed view.
1640    #[must_use]
1641    pub fn xml(self) -> &'a str {
1642        self.parsed.document.input_text()
1643    }
1644
1645    /// Return the document root identity.
1646    #[must_use]
1647    pub fn root(self) -> NodeIdentity {
1648        self.node_identity(self.parsed.document.root())
1649    }
1650
1651    /// Return the root element identity.
1652    #[must_use]
1653    pub fn root_element(self) -> NodeIdentity {
1654        self.node_identity(self.parsed.document.root_element())
1655    }
1656
1657    /// Return the number of parser nodes in this generation.
1658    #[must_use]
1659    pub fn node_count(self) -> usize {
1660        self.parsed.node_count
1661    }
1662
1663    pub(crate) fn max_depth(self) -> usize {
1664        self.parsed.max_depth
1665    }
1666
1667    /// Resolve an ID using standard spellings plus caller registrations.
1668    #[must_use]
1669    pub fn node_for_id(
1670        self,
1671        value: &str,
1672        registrations: &[IdAttributeRegistration],
1673    ) -> Option<NodeIdentity> {
1674        let mut matches = HashSet::new();
1675        match self.parsed.indexes.default_ids.get(value) {
1676            Some(Some(node)) => {
1677                matches.insert(*node);
1678            }
1679            Some(None) => return None,
1680            None => {}
1681        }
1682        for registration in registrations {
1683            let key = (
1684                registration.attribute_local_name().to_owned(),
1685                value.to_owned(),
1686            );
1687            if let Some(nodes) = self.parsed.indexes.attributes_by_value.get(&key) {
1688                matches.extend(nodes.iter().copied().filter(|node_id| {
1689                    self.parsed
1690                        .document
1691                        .get_node(*node_id)
1692                        .is_some_and(|node| registration.matches_node(node))
1693                }));
1694            }
1695        }
1696        if matches.len() == 1 {
1697            matches
1698                .iter()
1699                .next()
1700                .map(|node| self.node_identity_by_id(*node))
1701        } else {
1702            None
1703        }
1704    }
1705
1706    #[cfg(feature = "xmldsig")]
1707    pub(crate) fn id_index(
1708        self,
1709        registrations: &[IdAttributeRegistration],
1710    ) -> HashMap<String, NodeId> {
1711        let mut candidates: HashMap<String, HashSet<NodeId>> = HashMap::new();
1712        let mut ambiguous = HashSet::new();
1713        for (value, node) in &self.parsed.indexes.default_ids {
1714            if let Some(node) = node {
1715                candidates.entry(value.clone()).or_default().insert(*node);
1716            } else {
1717                ambiguous.insert(value.clone());
1718            }
1719        }
1720        for ((attribute_name, value), nodes) in &self.parsed.indexes.attributes_by_value {
1721            for registration in registrations
1722                .iter()
1723                .filter(|registration| registration.attribute_local_name() == attribute_name)
1724            {
1725                candidates
1726                    .entry(value.clone())
1727                    .or_default()
1728                    .extend(nodes.iter().copied().filter(|node_id| {
1729                        self.parsed
1730                            .document
1731                            .get_node(*node_id)
1732                            .is_some_and(|node| registration.matches_node(node))
1733                    }));
1734            }
1735        }
1736        candidates
1737            .into_iter()
1738            .filter_map(|(value, nodes)| {
1739                if nodes.len() == 1 && !ambiguous.contains(&value) {
1740                    nodes.into_iter().next().map(|node| (value, node))
1741                } else {
1742                    None
1743                }
1744            })
1745            .collect()
1746    }
1747
1748    /// Return deterministic document order for a current tree-node identity.
1749    pub fn document_order(self, identity: NodeIdentity) -> Result<usize, XmlDocumentError> {
1750        self.validate_identity(identity)?;
1751        self.parsed
1752            .indexes
1753            .order
1754            .get(&identity.backend)
1755            .copied()
1756            .ok_or(XmlDocumentError::MissingNode)
1757    }
1758
1759    /// Return a total order key for a current tree node.
1760    pub fn node_order(self, identity: NodeIdentity) -> Result<SemanticOrder, XmlDocumentError> {
1761        Ok(SemanticOrder {
1762            node: self.document_order(identity)?,
1763            phase: 0,
1764            position: 0,
1765        })
1766    }
1767
1768    /// Return a total order key for a current attribute node.
1769    pub fn attribute_order(
1770        self,
1771        identity: &AttributeIdentity,
1772    ) -> Result<SemanticOrder, XmlDocumentError> {
1773        let owner = self.resolve_node(identity.owner)?;
1774        let position = owner
1775            .attributes()
1776            .position(|attribute| {
1777                attribute.namespace() == identity.namespace.as_deref()
1778                    && attribute.name() == identity.local_name
1779            })
1780            .ok_or(XmlDocumentError::MissingNode)?;
1781        Ok(SemanticOrder {
1782            node: self.document_order(identity.owner)?,
1783            phase: 2,
1784            position,
1785        })
1786    }
1787
1788    /// Return a total order key for a current namespace node.
1789    pub fn namespace_order(
1790        self,
1791        identity: &NamespaceIdentity,
1792    ) -> Result<SemanticOrder, XmlDocumentError> {
1793        let owner = self.resolve_node(identity.owner)?;
1794        let target = (identity.prefix.as_str(), identity.uri.as_str());
1795        let mut found = false;
1796        let mut position = 0;
1797        // This is the same complete in-scope axis consumed by
1798        // namespace_identities(), not only declarations written on `owner`.
1799        for namespace in owner.namespaces() {
1800            let candidate = (namespace.name().unwrap_or_default(), namespace.uri());
1801            match candidate.cmp(&target) {
1802                std::cmp::Ordering::Less => position += 1,
1803                std::cmp::Ordering::Equal => found = true,
1804                std::cmp::Ordering::Greater => {}
1805            }
1806        }
1807        if !found {
1808            return Err(XmlDocumentError::MissingNode);
1809        }
1810        Ok(SemanticOrder {
1811            node: self.document_order(identity.owner)?,
1812            phase: 1,
1813            position,
1814        })
1815    }
1816
1817    pub(crate) fn document(self) -> &'a Document<'a> {
1818        &self.parsed.document
1819    }
1820
1821    pub(crate) fn node_identity(self, node: Node<'_, '_>) -> NodeIdentity {
1822        self.node_identity_by_id(node.id())
1823    }
1824
1825    pub(crate) fn node_identity_by_id(self, backend: NodeId) -> NodeIdentity {
1826        NodeIdentity {
1827            document: self.identity,
1828            generation: self.generation,
1829            backend,
1830        }
1831    }
1832
1833    pub(crate) fn resolve_node(
1834        self,
1835        identity: NodeIdentity,
1836    ) -> Result<Node<'a, 'a>, XmlDocumentError> {
1837        self.validate_identity(identity)?;
1838        self.parsed
1839            .document
1840            .get_node(identity.backend)
1841            .ok_or(XmlDocumentError::MissingNode)
1842    }
1843
1844    /// Identify an attribute by expanded name on a current owner element.
1845    pub fn attribute_identity(
1846        self,
1847        owner: NodeIdentity,
1848        namespace: Option<&str>,
1849        local_name: &str,
1850    ) -> Result<AttributeIdentity, XmlDocumentError> {
1851        let node = self.resolve_node(owner)?;
1852        if !node.is_element()
1853            || !node.attributes().any(|attribute| {
1854                attribute.namespace() == namespace && attribute.name() == local_name
1855            })
1856        {
1857            return Err(XmlDocumentError::MissingNode);
1858        }
1859        Ok(AttributeIdentity {
1860            owner,
1861            namespace: namespace.map(str::to_owned),
1862            local_name: local_name.to_owned(),
1863        })
1864    }
1865
1866    /// Return the in-scope XPath namespace nodes owned by one current element.
1867    pub fn namespace_identities(
1868        self,
1869        owner: NodeIdentity,
1870    ) -> Result<Vec<NamespaceIdentity>, XmlDocumentError> {
1871        let node = self.resolve_node(owner)?;
1872        if !node.is_element() {
1873            return Err(XmlDocumentError::MissingNode);
1874        }
1875        // roxmltree stores each element's complete in-scope namespace axis as
1876        // a compact range of shared namespace indices. This includes inherited
1877        // bindings with prefix shadowing already applied; walking ancestors
1878        // here would duplicate backend resolution and could diverge from it.
1879        let mut namespaces = node
1880            .namespaces()
1881            .map(|namespace| NamespaceIdentity {
1882                owner,
1883                prefix: namespace.name().unwrap_or_default().to_owned(),
1884                uri: namespace.uri().to_owned(),
1885            })
1886            .collect::<Vec<_>>();
1887        namespaces
1888            .sort_by(|left, right| (&left.prefix, &left.uri).cmp(&(&right.prefix, &right.uri)));
1889        Ok(namespaces)
1890    }
1891
1892    fn validate_identity(self, identity: NodeIdentity) -> Result<(), XmlDocumentError> {
1893        if identity.document != self.identity {
1894            return Err(XmlDocumentError::ForeignIdentity);
1895        }
1896        if identity.generation != self.generation {
1897            return Err(XmlDocumentError::StaleIdentity {
1898                identity: identity.generation,
1899                current: self.generation,
1900            });
1901        }
1902        Ok(())
1903    }
1904}
1905
1906fn build_cell(
1907    xml: String,
1908    settings: DocumentParseSettings,
1909    budget: Option<&XmlParseWorkBudget>,
1910) -> Result<DocumentCell, XmlDocumentError> {
1911    if xml.len() > settings.max_bytes {
1912        return Err(XmlDocumentError::DocumentTooLarge {
1913            maximum: settings.max_bytes,
1914            actual: xml.len(),
1915        });
1916    }
1917    preflight_document_limits(&xml, settings, budget)?;
1918    build_cell_after_preflight(xml, settings, budget)
1919}
1920
1921fn build_cell_after_preflight(
1922    xml: String,
1923    settings: DocumentParseSettings,
1924    budget: Option<&XmlParseWorkBudget>,
1925) -> Result<DocumentCell, XmlDocumentError> {
1926    charge_parse_work(budget, xml.len())?;
1927    let cell = build_roxmltree_cell(xml, settings)?;
1928    cell.with_dependent(|source, parsed| {
1929        SelectedXmlParser::validate_after_bounded_projection(
1930            source,
1931            settings,
1932            parsed.metrics(),
1933            budget,
1934        )
1935        .map(|_| ())
1936    })?;
1937    Ok(cell)
1938}
1939
1940fn build_roxmltree_cell(
1941    xml: String,
1942    settings: DocumentParseSettings,
1943) -> Result<DocumentCell, XmlDocumentError> {
1944    DocumentCell::try_new(xml, |source| {
1945        let (document, metrics) = parse_roxmltree_projection(source, settings)?;
1946        let indexes = DocumentIndexes::build(&document);
1947        Ok::<_, XmlDocumentError>(ParsedDocument {
1948            document,
1949            indexes,
1950            node_count: metrics.node_count,
1951            max_depth: metrics.max_depth,
1952        })
1953    })
1954}
1955
1956pub(crate) fn parse_borrowed_with_settings_and_budget<'a>(
1957    xml: &'a str,
1958    settings: DocumentParseSettings,
1959    budget: Option<&XmlParseWorkBudget>,
1960) -> Result<Document<'a>, XmlDocumentError> {
1961    if xml.len() > settings.max_bytes {
1962        return Err(XmlDocumentError::DocumentTooLarge {
1963            maximum: settings.max_bytes,
1964            actual: xml.len(),
1965        });
1966    }
1967    preflight_document_limits(xml, settings, budget)?;
1968    charge_parse_work(budget, xml.len())?;
1969    let (document, metrics) = parse_roxmltree_projection(xml, settings)?;
1970    SelectedXmlParser::validate_after_bounded_projection(xml, settings, metrics, budget)?;
1971    Ok(document)
1972}
1973
1974fn preflight_document_limits(
1975    xml: &str,
1976    settings: DocumentParseSettings,
1977    budget: Option<&XmlParseWorkBudget>,
1978) -> Result<(), XmlDocumentError> {
1979    // This bounded lexical pass is the first parser stage for every entry
1980    // point, before either backend is allowed to construct a DOM.
1981    charge_parse_work(budget, xml.len())?;
1982    let mut dtd = InternalDtd::default();
1983    let mut expansion_stack = HashSet::new();
1984    let mut state = DocumentPreflightState::default();
1985    preflight_xml_fragment(
1986        xml,
1987        settings,
1988        &mut dtd,
1989        &mut expansion_stack,
1990        &mut state,
1991        budget,
1992        true,
1993    )
1994}
1995
1996#[derive(Default)]
1997struct DocumentPreflightState {
1998    depth: usize,
1999    nodes: u32,
2000    in_character_data: bool,
2001    #[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
2002    entity_expansion_work: usize,
2003}
2004
2005#[derive(Default)]
2006struct InternalDtd {
2007    entities: HashMap<String, String>,
2008    attribute_defaults: HashMap<String, Vec<InternalAttributeDefault>>,
2009}
2010
2011struct InternalAttributeDefault {
2012    attribute_name: String,
2013    value: String,
2014}
2015
2016fn preflight_xml_fragment(
2017    xml: &str,
2018    settings: DocumentParseSettings,
2019    dtd: &mut InternalDtd,
2020    expansion_stack: &mut HashSet<String>,
2021    state: &mut DocumentPreflightState,
2022    budget: Option<&XmlParseWorkBudget>,
2023    collect_doctype: bool,
2024) -> Result<(), XmlDocumentError> {
2025    enum FragmentSource {
2026        Document,
2027        Entity(String),
2028    }
2029
2030    #[derive(Clone, Copy, PartialEq, Eq)]
2031    enum FragmentContext {
2032        Content,
2033        Attribute,
2034    }
2035
2036    struct FragmentFrame {
2037        source: FragmentSource,
2038        offset: usize,
2039        collect_doctype: bool,
2040        context: FragmentContext,
2041        pending_attribute_source: Vec<u8>,
2042        pending_attribute_offset: usize,
2043    }
2044
2045    enum PreflightEvent {
2046        DocType(Option<String>),
2047        GeneralRef {
2048            name: Option<String>,
2049            is_character_reference: bool,
2050        },
2051        CharacterData {
2052            xml_whitespace: bool,
2053        },
2054        Start(Vec<u8>),
2055        Empty(Vec<u8>),
2056        End,
2057        Node,
2058        Other,
2059        Done,
2060    }
2061
2062    if state.nodes == 0 {
2063        // roxmltree exposes its document root as the first semantic node.
2064        state.nodes = 1;
2065    }
2066    let mut fragments = vec![FragmentFrame {
2067        source: FragmentSource::Document,
2068        offset: 0,
2069        collect_doctype,
2070        context: FragmentContext::Content,
2071        pending_attribute_source: Vec::new(),
2072        pending_attribute_offset: 0,
2073    }];
2074    // Syntax diagnostics and exact parser precedence belong to the selected
2075    // DOM pipeline; this pass stops at malformed syntax after bounding its prefix.
2076    while !fragments.is_empty() {
2077        let (event, collect_doctype, context) = {
2078            let frame = fragments.last_mut().expect("fragment stack is not empty");
2079            let mut pending = general_references(
2080                &frame.pending_attribute_source[frame.pending_attribute_offset..],
2081            );
2082            if let Some(name) = pending.next() {
2083                frame.pending_attribute_offset = frame
2084                    .pending_attribute_offset
2085                    .saturating_add(pending.consumed());
2086                (
2087                    PreflightEvent::GeneralRef {
2088                        name: Some(name.to_owned()),
2089                        is_character_reference: false,
2090                    },
2091                    false,
2092                    FragmentContext::Attribute,
2093                )
2094            } else {
2095                frame.pending_attribute_source.clear();
2096                frame.pending_attribute_offset = 0;
2097                let source = match &frame.source {
2098                    FragmentSource::Document => xml,
2099                    FragmentSource::Entity(name) => dtd
2100                        .entities
2101                        .get(name)
2102                        .expect("active entity replacement remains registered"),
2103                };
2104                let remaining = &source[frame.offset..];
2105                let mut reader = QuickXmlReader::from_str(remaining);
2106                // This pass observes lexical events one at a time and deliberately
2107                // leaves structural diagnostics to the selected DOM parser.
2108                reader.config_mut().check_end_names = false;
2109                // A fresh reader has no opening-tag state for an End event at
2110                // this slice boundary. Emit it so our manual depth state and all
2111                // later events remain visible; the DOM still rejects bad pairs.
2112                reader.config_mut().allow_unmatched_ends = true;
2113                let event = match reader.read_event() {
2114                    Ok(QuickXmlEvent::DocType(doctype)) => PreflightEvent::DocType(
2115                        doctype.decode().ok().map(|value| value.into_owned()),
2116                    ),
2117                    Ok(QuickXmlEvent::GeneralRef(reference)) => {
2118                        let name = reference.decode().ok().map(|value| value.into_owned());
2119                        let is_character_reference =
2120                            reference.resolve_char_ref().ok().flatten().is_some()
2121                                || name.as_deref().is_some_and(|name| {
2122                                    matches!(name, "amp" | "apos" | "gt" | "lt" | "quot")
2123                                });
2124                        PreflightEvent::GeneralRef {
2125                            name,
2126                            is_character_reference,
2127                        }
2128                    }
2129                    Ok(QuickXmlEvent::Text(text)) => PreflightEvent::CharacterData {
2130                        xml_whitespace: text
2131                            .as_ref()
2132                            .iter()
2133                            .all(|byte| matches!(byte, b' ' | b'\t' | b'\r' | b'\n')),
2134                    },
2135                    Ok(QuickXmlEvent::CData(_)) => PreflightEvent::CharacterData {
2136                        xml_whitespace: false,
2137                    },
2138                    Ok(QuickXmlEvent::Start(element)) => {
2139                        let source = if frame.context == FragmentContext::Content {
2140                            element_attribute_reference_source(&element, dtd)
2141                        } else {
2142                            Vec::new()
2143                        };
2144                        PreflightEvent::Start(source)
2145                    }
2146                    Ok(QuickXmlEvent::Empty(element)) => {
2147                        let source = if frame.context == FragmentContext::Content {
2148                            element_attribute_reference_source(&element, dtd)
2149                        } else {
2150                            Vec::new()
2151                        };
2152                        PreflightEvent::Empty(source)
2153                    }
2154                    Ok(QuickXmlEvent::End(_)) => PreflightEvent::End,
2155                    Ok(QuickXmlEvent::Comment(_) | QuickXmlEvent::PI(_)) => PreflightEvent::Node,
2156                    Ok(QuickXmlEvent::Eof) | Err(_) => PreflightEvent::Done,
2157                    Ok(_) => PreflightEvent::Other,
2158                };
2159                frame.offset = frame.offset.saturating_add(
2160                    usize::try_from(reader.buffer_position()).unwrap_or(remaining.len()),
2161                );
2162                (event, frame.collect_doctype, frame.context)
2163            }
2164        };
2165
2166        match event {
2167            PreflightEvent::DocType(doctype) => {
2168                if collect_doctype
2169                    && settings.allow_dtd
2170                    && let Some(doctype) = doctype
2171                {
2172                    collect_internal_dtd(&doctype, dtd);
2173                }
2174                state.in_character_data = false;
2175                continue;
2176            }
2177            PreflightEvent::GeneralRef {
2178                name,
2179                is_character_reference,
2180            } => {
2181                let Some(name) = name else {
2182                    continue;
2183                };
2184                if is_character_reference {
2185                    if context == FragmentContext::Content {
2186                        observe_preflight_node(state, settings, true)?;
2187                    }
2188                    continue;
2189                }
2190                if expansion_stack.insert(name.clone()) {
2191                    if let Some(replacement) = dtd.entities.get(&name) {
2192                        charge_entity_expansion_work(state, budget, replacement.len())?;
2193                        fragments.push(FragmentFrame {
2194                            source: FragmentSource::Entity(name),
2195                            offset: 0,
2196                            collect_doctype: false,
2197                            context,
2198                            pending_attribute_source: Vec::new(),
2199                            pending_attribute_offset: 0,
2200                        });
2201                    } else {
2202                        expansion_stack.remove(&name);
2203                    }
2204                }
2205                continue;
2206            }
2207            PreflightEvent::Done => {
2208                let frame = fragments.pop().expect("fragment stack is not empty");
2209                if let FragmentSource::Entity(name) = frame.source {
2210                    expansion_stack.remove(&name);
2211                }
2212                continue;
2213            }
2214            PreflightEvent::CharacterData { xml_whitespace } => {
2215                if context == FragmentContext::Attribute {
2216                    continue;
2217                }
2218                if state.depth == 0 && xml_whitespace {
2219                    state.in_character_data = false;
2220                    continue;
2221                }
2222                let starts_semantic_node = !state.in_character_data;
2223                state.in_character_data = true;
2224                if starts_semantic_node {
2225                    observe_preflight_node(state, settings, false)?;
2226                }
2227                continue;
2228            }
2229            PreflightEvent::Node => {
2230                state.in_character_data = false;
2231                observe_preflight_node(state, settings, false)?;
2232                continue;
2233            }
2234            PreflightEvent::Other => {
2235                state.in_character_data = false;
2236                continue;
2237            }
2238            PreflightEvent::Start(_) | PreflightEvent::Empty(_) | PreflightEvent::End => {}
2239        }
2240
2241        if context == FragmentContext::Attribute {
2242            continue;
2243        }
2244        state.in_character_data = false;
2245        match event {
2246            PreflightEvent::Start(attribute_source) => {
2247                let frame = fragments
2248                    .last_mut()
2249                    .expect("active element frame remains registered");
2250                frame.pending_attribute_source = attribute_source;
2251                frame.pending_attribute_offset = 0;
2252                observe_preflight_node(state, settings, false)?;
2253                state.depth = state.depth.saturating_add(1);
2254                if state.depth > settings.depth_limit {
2255                    return Err(XmlDocumentError::DocumentTooDeep {
2256                        maximum: settings.depth_limit,
2257                        actual: state.depth,
2258                    });
2259                }
2260            }
2261            PreflightEvent::Empty(attribute_source) => {
2262                let frame = fragments
2263                    .last_mut()
2264                    .expect("active element frame remains registered");
2265                frame.pending_attribute_source = attribute_source;
2266                frame.pending_attribute_offset = 0;
2267                observe_preflight_node(state, settings, false)?;
2268                let actual = state.depth.saturating_add(1);
2269                if actual > settings.depth_limit {
2270                    return Err(XmlDocumentError::DocumentTooDeep {
2271                        maximum: settings.depth_limit,
2272                        actual,
2273                    });
2274                }
2275            }
2276            PreflightEvent::End => state.depth = state.depth.saturating_sub(1),
2277            _ => unreachable!("non-structural events continue above"),
2278        }
2279    }
2280    Ok(())
2281}
2282
2283fn element_attribute_reference_source(
2284    element: &QuickXmlBytesStart<'_>,
2285    dtd: &InternalDtd,
2286) -> Vec<u8> {
2287    let lexical = element.as_ref();
2288    let mut source = if lexical.contains(&b'&') {
2289        lexical.to_vec()
2290    } else {
2291        Vec::new()
2292    };
2293    if dtd.attribute_defaults.is_empty() {
2294        return source;
2295    }
2296
2297    let element_name = element.name();
2298    let Ok(element_name) = std::str::from_utf8(element_name.as_ref()) else {
2299        return source;
2300    };
2301    if let Some(defaults) = dtd.attribute_defaults.get(element_name) {
2302        let mut present_attributes: HashSet<_> = element
2303            .attributes()
2304            .flatten()
2305            .filter_map(|attribute| {
2306                std::str::from_utf8(attribute.key.as_ref())
2307                    .ok()
2308                    .map(ToOwned::to_owned)
2309            })
2310            .collect();
2311        for default in defaults {
2312            if present_attributes.insert(default.attribute_name.clone())
2313                && default.value.contains('&')
2314            {
2315                source.push(b' ');
2316                source.extend_from_slice(default.value.as_bytes());
2317            }
2318        }
2319    }
2320    source
2321}
2322
2323struct GeneralReferences<'a> {
2324    value: &'a [u8],
2325    offset: usize,
2326}
2327
2328impl GeneralReferences<'_> {
2329    fn consumed(&self) -> usize {
2330        self.offset
2331    }
2332}
2333
2334impl<'a> Iterator for GeneralReferences<'a> {
2335    type Item = &'a str;
2336
2337    fn next(&mut self) -> Option<Self::Item> {
2338        loop {
2339            let relative_start = self.value[self.offset..]
2340                .iter()
2341                .position(|byte| *byte == b'&')?;
2342            let start = self.offset + relative_start + 1;
2343            let Some(relative_end) = self.value[start..].iter().position(|byte| *byte == b';')
2344            else {
2345                self.offset = self.value.len();
2346                return None;
2347            };
2348            let end = start + relative_end;
2349            self.offset = end + 1;
2350            let name = &self.value[start..end];
2351            if !name.starts_with(b"#")
2352                && !matches!(name, b"amp" | b"apos" | b"gt" | b"lt" | b"quot")
2353                && let Ok(name) = std::str::from_utf8(name)
2354            {
2355                return Some(name);
2356            }
2357        }
2358    }
2359}
2360
2361fn general_references(value: &[u8]) -> GeneralReferences<'_> {
2362    GeneralReferences { value, offset: 0 }
2363}
2364
2365fn charge_entity_expansion_work(
2366    state: &mut DocumentPreflightState,
2367    budget: Option<&XmlParseWorkBudget>,
2368    bytes: usize,
2369) -> Result<(), XmlDocumentError> {
2370    charge_parse_work(budget, bytes)?;
2371    #[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
2372    if budget.is_none() {
2373        let actual = state.entity_expansion_work.saturating_add(bytes);
2374        let maximum = crate::hard_limits::XML_PARSE_WORK_BYTE_CEILING;
2375        if actual > maximum {
2376            return Err(crate::policy::PolicyViolation::ResourceLimit {
2377                resource: crate::policy::resource_name::XML_PARSE_WORK_BYTES,
2378                maximum,
2379                actual,
2380            }
2381            .into());
2382        }
2383        state.entity_expansion_work = actual;
2384    }
2385    #[cfg(not(any(feature = "xmldsig", feature = "xmlenc")))]
2386    let _ = state;
2387    Ok(())
2388}
2389
2390fn observe_preflight_node(
2391    state: &mut DocumentPreflightState,
2392    settings: DocumentParseSettings,
2393    character_data: bool,
2394) -> Result<(), XmlDocumentError> {
2395    if character_data {
2396        if state.in_character_data {
2397            return Ok(());
2398        }
2399        state.in_character_data = true;
2400    }
2401    state.nodes = state.nodes.saturating_add(1);
2402    if state.nodes > settings.nodes_limit {
2403        return Err(XmlDocumentError::Parse(roxmltree::Error::NodesLimitReached));
2404    }
2405    Ok(())
2406}
2407
2408fn collect_internal_dtd(doctype: &str, dtd: &mut InternalDtd) {
2409    let Some(subset_start) = find_unquoted_byte(doctype.as_bytes(), b'[', 0) else {
2410        return;
2411    };
2412    let Some(subset_end) = doctype.rfind(']') else {
2413        return;
2414    };
2415    let subset = &doctype[subset_start + 1..subset_end];
2416    let mut offset = 0;
2417    let bytes = subset.as_bytes();
2418    while offset < bytes.len() {
2419        if bytes[offset..].starts_with(b"<!--") {
2420            let Some(end) = find_bytes(&bytes[offset + 4..], b"-->") else {
2421                break;
2422            };
2423            offset += 4 + end + 3;
2424        } else if bytes[offset..].starts_with(b"<?") {
2425            let Some(end) = find_bytes(&bytes[offset + 2..], b"?>") else {
2426                break;
2427            };
2428            offset += 2 + end + 2;
2429        } else if bytes[offset..].starts_with(b"<!ENTITY") {
2430            let declaration_start = offset + "<!ENTITY".len();
2431            let Some(declaration_end) = find_unquoted_byte(bytes, b'>', declaration_start) else {
2432                break;
2433            };
2434            let declaration = &subset[declaration_start..declaration_end];
2435            if let Some((name, value)) = parse_internal_general_entity(declaration) {
2436                // roxmltree resolves the first declaration with a matching name.
2437                dtd.entities
2438                    .entry(name.to_owned())
2439                    .or_insert_with(|| value.to_owned());
2440            }
2441            offset = declaration_end + 1;
2442        } else if bytes[offset..].starts_with(b"<!ATTLIST") {
2443            let declaration_start = offset + "<!ATTLIST".len();
2444            let Some(declaration_end) = find_unquoted_byte(bytes, b'>', declaration_start) else {
2445                break;
2446            };
2447            // Parser-created default values bypass lexical start-tag attributes,
2448            // so retain their references for the same iterative work accounting.
2449            collect_internal_attribute_defaults(
2450                &subset[declaration_start..declaration_end],
2451                &mut dtd.attribute_defaults,
2452            );
2453            offset = declaration_end + 1;
2454        } else if bytes[offset..].starts_with(b"<!") {
2455            let Some(declaration_end) = find_unquoted_byte(bytes, b'>', offset + 2) else {
2456                break;
2457            };
2458            offset = declaration_end + 1;
2459        } else {
2460            offset += 1;
2461        }
2462    }
2463}
2464
2465fn collect_internal_attribute_defaults(
2466    declaration: &str,
2467    defaults: &mut HashMap<String, Vec<InternalAttributeDefault>>,
2468) {
2469    let bytes = declaration.as_bytes();
2470    let mut offset = 0;
2471    skip_dtd_whitespace(bytes, &mut offset);
2472    let Some(element_name) = consume_dtd_token(declaration, &mut offset) else {
2473        return;
2474    };
2475    let mut declarations = Vec::new();
2476    loop {
2477        skip_dtd_whitespace(bytes, &mut offset);
2478        if offset == bytes.len() {
2479            break;
2480        }
2481        let Some(attribute_name) = consume_dtd_token(declaration, &mut offset) else {
2482            break;
2483        };
2484        skip_dtd_whitespace(bytes, &mut offset);
2485        if !consume_attribute_type(declaration, &mut offset) {
2486            break;
2487        }
2488        skip_dtd_whitespace(bytes, &mut offset);
2489
2490        if consume_dtd_keyword(declaration, &mut offset, "#REQUIRED")
2491            || consume_dtd_keyword(declaration, &mut offset, "#IMPLIED")
2492        {
2493            continue;
2494        }
2495        if consume_dtd_keyword(declaration, &mut offset, "#FIXED") {
2496            skip_dtd_whitespace(bytes, &mut offset);
2497        }
2498        let Some(value) = consume_dtd_quoted_value(declaration, &mut offset) else {
2499            break;
2500        };
2501        declarations.push(InternalAttributeDefault {
2502            attribute_name: attribute_name.to_owned(),
2503            value: value.to_owned(),
2504        });
2505    }
2506
2507    if !declarations.is_empty() {
2508        defaults
2509            .entry(element_name.to_owned())
2510            .or_default()
2511            .extend(declarations);
2512    }
2513}
2514
2515fn skip_dtd_whitespace(bytes: &[u8], offset: &mut usize) {
2516    while bytes
2517        .get(*offset)
2518        .is_some_and(|byte| matches!(byte, b' ' | b'\t' | b'\r' | b'\n'))
2519    {
2520        *offset += 1;
2521    }
2522}
2523
2524fn consume_dtd_token<'a>(input: &'a str, offset: &mut usize) -> Option<&'a str> {
2525    let bytes = input.as_bytes();
2526    let start = *offset;
2527    while bytes.get(*offset).is_some_and(|byte| {
2528        !matches!(
2529            byte,
2530            b' ' | b'\t' | b'\r' | b'\n' | b'(' | b')' | b'\'' | b'"'
2531        )
2532    }) {
2533        *offset += 1;
2534    }
2535    (start != *offset).then(|| &input[start..*offset])
2536}
2537
2538fn consume_attribute_type(input: &str, offset: &mut usize) -> bool {
2539    let bytes = input.as_bytes();
2540    if consume_dtd_keyword(input, offset, "NOTATION") {
2541        skip_dtd_whitespace(bytes, offset);
2542        return consume_parenthesized_dtd_group(bytes, offset);
2543    }
2544    if bytes.get(*offset) == Some(&b'(') {
2545        return consume_parenthesized_dtd_group(bytes, offset);
2546    }
2547    consume_dtd_token(input, offset).is_some()
2548}
2549
2550fn consume_parenthesized_dtd_group(bytes: &[u8], offset: &mut usize) -> bool {
2551    if bytes.get(*offset) != Some(&b'(') {
2552        return false;
2553    }
2554    let mut depth = 0usize;
2555    while let Some(byte) = bytes.get(*offset).copied() {
2556        *offset += 1;
2557        match byte {
2558            b'(' => depth += 1,
2559            b')' => {
2560                depth -= 1;
2561                if depth == 0 {
2562                    return true;
2563                }
2564            }
2565            _ => {}
2566        }
2567    }
2568    false
2569}
2570
2571fn consume_dtd_keyword(input: &str, offset: &mut usize, keyword: &str) -> bool {
2572    let remaining = &input[*offset..];
2573    if !remaining.starts_with(keyword) {
2574        return false;
2575    }
2576    let end = *offset + keyword.len();
2577    if input
2578        .as_bytes()
2579        .get(end)
2580        .is_some_and(|byte| !matches!(byte, b' ' | b'\t' | b'\r' | b'\n' | b'(' | b'\'' | b'"'))
2581    {
2582        return false;
2583    }
2584    *offset = end;
2585    true
2586}
2587
2588fn consume_dtd_quoted_value<'a>(input: &'a str, offset: &mut usize) -> Option<&'a str> {
2589    let bytes = input.as_bytes();
2590    let quote = *bytes.get(*offset)?;
2591    if !matches!(quote, b'\'' | b'"') {
2592        return None;
2593    }
2594    let start = *offset + 1;
2595    let relative_end = bytes[start..].iter().position(|byte| *byte == quote)?;
2596    let end = start + relative_end;
2597    *offset = end + 1;
2598    Some(&input[start..end])
2599}
2600
2601fn find_bytes(haystack: &[u8], needle: &[u8]) -> Option<usize> {
2602    haystack
2603        .windows(needle.len())
2604        .position(|window| window == needle)
2605}
2606
2607fn parse_internal_general_entity(declaration: &str) -> Option<(&str, &str)> {
2608    let declaration = declaration.trim_start();
2609    if declaration.starts_with('%') {
2610        return None;
2611    }
2612    let name_end = declaration.find(char::is_whitespace)?;
2613    let name = &declaration[..name_end];
2614    let definition = declaration[name_end..].trim_start();
2615    let quote = definition.as_bytes().first().copied()?;
2616    if !matches!(quote, b'\'' | b'"') {
2617        // External entities have no replacement text without an explicit
2618        // resolver, which this crate never installs.
2619        return None;
2620    }
2621    let value_end = definition.as_bytes()[1..]
2622        .iter()
2623        .position(|byte| *byte == quote)?
2624        + 1;
2625    Some((name, &definition[1..value_end]))
2626}
2627
2628fn find_unquoted_byte(bytes: &[u8], needle: u8, start: usize) -> Option<usize> {
2629    let mut quote = None;
2630    for (index, byte) in bytes.iter().copied().enumerate().skip(start) {
2631        match byte {
2632            b'\'' | b'"' => match quote {
2633                Some(active) if active == byte => quote = None,
2634                None => quote = Some(byte),
2635                Some(_) => {}
2636            },
2637            _ if quote.is_none() && byte == needle => return Some(index),
2638            _ => {}
2639        }
2640    }
2641    None
2642}
2643
2644fn parse_roxmltree_projection<'a>(
2645    source: &'a str,
2646    settings: DocumentParseSettings,
2647) -> Result<(Document<'a>, DocumentMetrics), XmlDocumentError> {
2648    let document = Document::parse_with_options(
2649        source,
2650        ParsingOptions {
2651            allow_dtd: settings.allow_dtd,
2652            nodes_limit: settings.nodes_limit,
2653            entity_resolver: None,
2654        },
2655    )
2656    .map_err(XmlDocumentError::Parse)?;
2657    let metrics = validate_document_metrics(&document, settings.depth_limit)?;
2658    Ok((document, metrics))
2659}
2660
2661fn validate_document_metrics(
2662    document: &Document<'_>,
2663    maximum: usize,
2664) -> Result<DocumentMetrics, XmlDocumentError> {
2665    // Node IDs follow document order, so each parent's depth has already been
2666    // recorded. This keeps the compatibility path linear instead of walking
2667    // every ancestor chain independently.
2668    let mut depths = Vec::new();
2669    let mut node_count = 0;
2670    let mut max_depth = 0;
2671    for node in document.descendants() {
2672        node_count += 1;
2673        let parent_depth = node
2674            .parent()
2675            .and_then(|parent| depths.get(parent.id().get_usize()))
2676            .copied()
2677            .unwrap_or(0);
2678        let depth = parent_depth + usize::from(node.is_element());
2679        let node_index = node.id().get_usize();
2680        if depths.len() <= node_index {
2681            depths.resize(node_index + 1, 0);
2682        }
2683        depths[node_index] = depth;
2684        max_depth = max_depth.max(depth);
2685        if depth > maximum {
2686            return Err(XmlDocumentError::DocumentTooDeep {
2687                maximum,
2688                actual: depth,
2689            });
2690        }
2691    }
2692    Ok(DocumentMetrics {
2693        node_count,
2694        max_depth,
2695    })
2696}
2697
2698fn own_bounded_xml(
2699    xml: impl AsRef<str> + Into<String>,
2700    maximum: usize,
2701) -> Result<String, XmlDocumentError> {
2702    let actual = xml.as_ref().len();
2703    if actual > maximum {
2704        return Err(XmlDocumentError::DocumentTooLarge { maximum, actual });
2705    }
2706    Ok(xml.into())
2707}
2708
2709fn allocate_document_identity(counter: &AtomicU64) -> Result<DocumentIdentity, XmlDocumentError> {
2710    let mut current = counter.load(Ordering::Relaxed);
2711    loop {
2712        let next = current
2713            .checked_add(1)
2714            .ok_or(XmlDocumentError::IdentityExhausted)?;
2715        match counter.compare_exchange_weak(current, next, Ordering::Relaxed, Ordering::Relaxed) {
2716            Ok(_) => return Ok(DocumentIdentity(current)),
2717            Err(observed) => current = observed,
2718        }
2719    }
2720}
2721
2722#[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
2723fn document_requires_internal_dtd(
2724    xml: &str,
2725    settings: DocumentParseSettings,
2726    budget: Option<&XmlParseWorkBudget>,
2727) -> Result<bool, XmlDocumentError> {
2728    // Parse provenance records what the source actually requires, not merely
2729    // whether its creator used a permissive policy. This lets a later strict
2730    // operation accept ordinary XML while rejecting DTD-dependent documents.
2731    if !settings.allow_dtd {
2732        return Ok(false);
2733    }
2734    charge_parse_work(budget, xml.len())?;
2735    Ok(Document::parse_with_options(
2736        xml,
2737        ParsingOptions {
2738            allow_dtd: false,
2739            nodes_limit: settings.nodes_limit,
2740            entity_resolver: None,
2741        },
2742    )
2743    .is_err())
2744}
2745
2746fn wrapped_fragment(replacement: &str) -> String {
2747    format!("{VALIDATION_WRAPPER_OPEN}{replacement}{VALIDATION_WRAPPER_CLOSE}")
2748}
2749
2750fn validation_wrapper<'a, 'input>(
2751    parsed: &'a ParsedDocument<'input>,
2752    expected_range: std::ops::Range<usize>,
2753) -> Result<Node<'a, 'input>, XmlDocumentError> {
2754    parsed
2755        .document
2756        .descendants()
2757        .find(|node| {
2758            node.has_tag_name((VALIDATION_WRAPPER_NS, "wrapper")) && node.range() == expected_range
2759        })
2760        .ok_or_else(|| {
2761            XmlDocumentError::InvalidReplacement(
2762                "replacement escaped its structural validation boundary".into(),
2763            )
2764        })
2765}
2766
2767fn validate_wrappers(
2768    parsed: &ParsedDocument<'_>,
2769    expected_ranges: &[std::ops::Range<usize>],
2770) -> Result<(), XmlDocumentError> {
2771    let mut remaining = expected_ranges
2772        .iter()
2773        .map(|range| (range.start, range.end))
2774        .collect::<HashSet<_>>();
2775    for node in parsed
2776        .document
2777        .descendants()
2778        .filter(|node| node.has_tag_name((VALIDATION_WRAPPER_NS, "wrapper")))
2779    {
2780        let range = node.range();
2781        remaining.remove(&(range.start, range.end));
2782    }
2783    if remaining.is_empty() {
2784        Ok(())
2785    } else {
2786        Err(XmlDocumentError::InvalidReplacement(
2787            "replacement escaped its structural validation boundary".into(),
2788        ))
2789    }
2790}
2791
2792impl DocumentIndexes {
2793    fn build(document: &Document<'_>) -> Self {
2794        let mut order = HashMap::new();
2795        let mut default_ids = HashMap::new();
2796        let mut duplicate_ids = HashSet::new();
2797        let mut attributes_by_value: HashMap<(String, String), Vec<NodeId>> = HashMap::new();
2798
2799        for (position, node) in document.descendants().enumerate() {
2800            order.insert(node.id(), position);
2801            if !node.is_element() {
2802                continue;
2803            }
2804            for attribute in node.attributes() {
2805                attributes_by_value
2806                    .entry((attribute.name().to_owned(), attribute.value().to_owned()))
2807                    .or_default()
2808                    .push(node.id());
2809                if !matches!(attribute.name(), "ID" | "Id" | "id")
2810                    || duplicate_ids.contains(attribute.value())
2811                {
2812                    continue;
2813                }
2814                match default_ids.entry(attribute.value().to_owned()) {
2815                    Entry::Vacant(entry) => {
2816                        entry.insert(Some(node.id()));
2817                    }
2818                    Entry::Occupied(mut entry) if entry.get() != &Some(node.id()) => {
2819                        entry.insert(None);
2820                        duplicate_ids.insert(attribute.value().to_owned());
2821                    }
2822                    Entry::Occupied(_) => {}
2823                }
2824            }
2825        }
2826        Self {
2827            order,
2828            default_ids,
2829            attributes_by_value,
2830        }
2831    }
2832}
2833
2834fn element_content_range(
2835    element: &str,
2836) -> Result<(std::ops::Range<usize>, String, bool), XmlDocumentError> {
2837    let start_end = element_opening_end(element)?;
2838    let start_tag = &element[..=start_end];
2839    let qualified_name = start_tag[1..]
2840        .split(|character: char| character.is_ascii_whitespace() || matches!(character, '/' | '>'))
2841        .next()
2842        .filter(|name| !name.is_empty())
2843        .ok_or_else(|| XmlDocumentError::InvalidReplacement("element name is missing".into()))?
2844        .to_owned();
2845    if start_tag[..start_tag.len() - 1].trim_end().ends_with('/') {
2846        return Ok((start_end..start_end, qualified_name, true));
2847    }
2848    let close = element
2849        .rfind("</")
2850        .ok_or_else(|| XmlDocumentError::InvalidReplacement("element end tag is missing".into()))?;
2851    Ok(((start_end + 1)..close, qualified_name, false))
2852}
2853
2854fn element_opening_end(element: &str) -> Result<usize, XmlDocumentError> {
2855    let mut quote = None;
2856    for (index, byte) in element.bytes().enumerate() {
2857        match byte {
2858            b'\'' | b'"' => match quote {
2859                Some(active) if active == byte => quote = None,
2860                None => quote = Some(byte),
2861                Some(_) => {}
2862            },
2863            b'>' if quote.is_none() => return Ok(index),
2864            _ => {}
2865        }
2866    }
2867    Err(XmlDocumentError::InvalidReplacement(
2868        "element start tag is incomplete".into(),
2869    ))
2870}
2871
2872#[cfg(test)]
2873mod tests {
2874    use super::*;
2875
2876    #[cfg(feature = "xml-backend-xmloxide")]
2877    #[test]
2878    fn xmloxide_accepts_the_retained_semantic_projection() {
2879        // The retained roxmltree projection owns shared metrics and indexes;
2880        // the selected backend adds strict syntax validation without replacing
2881        // those semantics with a backend-specific node model.
2882        let settings = DocumentParseSettings::default();
2883        let xml =
2884            r#"<root xmlns:p="urn:test"><p:item ID="target"><![CDATA[value]]></p:item></root>"#;
2885        let retained = build_roxmltree_cell(xml.to_owned(), settings)
2886            .expect("roxmltree semantic projection must parse");
2887        retained.with_dependent(|source, parsed| {
2888            XmloxideParser::validate_after_bounded_projection(
2889                source,
2890                settings,
2891                parsed.metrics(),
2892                None,
2893            )
2894            .expect("xmloxide must accept the fixture");
2895            assert!(parsed.indexes.default_ids.contains_key("target"));
2896        });
2897    }
2898
2899    #[cfg(feature = "xml-backend-xmloxide")]
2900    #[test]
2901    fn xmloxide_attribute_capacity_is_independent_of_retained_node_limit() {
2902        // Retained nodes and per-element attributes are different resources.
2903        // A tight node limit must not make the selected backend reject an
2904        // otherwise bounded element that the semantic projection accepts.
2905        let xml = r#"<root a="1" b="2" c="3"/>"#;
2906        let settings = DocumentParseSettings::new(false, 2, xml.len());
2907        let options = xmloxide_parse_options(settings);
2908
2909        assert_eq!(options.max_attributes, u32::MAX);
2910        assert_eq!(options.max_attribute_length, usize::MAX);
2911        assert_eq!(options.max_text_length, usize::MAX);
2912        assert_eq!(options.max_name_length, usize::MAX);
2913        assert_eq!(options.max_entity_expansions, u32::MAX);
2914        build_cell(xml.to_owned(), settings, None)
2915            .expect("attributes must not consume the retained node allowance");
2916    }
2917
2918    #[test]
2919    fn node_preflight_rejects_wide_input_before_any_dom_pass() {
2920        // The allowance covers only the streaming preflight. Reaching either
2921        // DOM would exhaust parser work before it could report the node bound.
2922        let xml = "<root><first/><second/></root>";
2923        let resources = crate::policy::ResourcePolicy {
2924            max_xml_parse_work_bytes: xml.len(),
2925            ..crate::policy::ResourcePolicy::default()
2926        };
2927        let budget = XmlParseWorkBudget::from_resources(&resources);
2928        let settings = DocumentParseSettings::new(false, 3, xml.len());
2929
2930        assert!(matches!(
2931            build_cell(xml.to_owned(), settings, Some(&budget)),
2932            Err(XmlDocumentError::Parse(roxmltree::Error::NodesLimitReached))
2933        ));
2934    }
2935
2936    #[test]
2937    fn node_preflight_counts_contiguous_character_data_once() {
2938        // Entity references and CDATA split the lexical event stream, but
2939        // roxmltree retains the contiguous character data as one text node.
2940        let xml = "<root>a&amp;<![CDATA[b]]>&#99;</root>";
2941        let exact_settings = DocumentParseSettings::new(false, 3, xml.len());
2942
2943        build_cell(xml.to_owned(), exact_settings, None)
2944            .expect("the exact retained semantic node count must be accepted");
2945        assert!(matches!(
2946            build_cell(
2947                xml.to_owned(),
2948                DocumentParseSettings::new(false, 2, xml.len()),
2949                None,
2950            ),
2951            Err(XmlDocumentError::Parse(roxmltree::Error::NodesLimitReached))
2952        ));
2953    }
2954
2955    #[test]
2956    fn node_preflight_excludes_document_boundary_whitespace() {
2957        // XML Misc whitespace outside the document element is not retained as
2958        // a roxmltree text node and must not consume semantic-node policy.
2959        let xml = " \n<root/>\n ";
2960        let settings = DocumentParseSettings::new(false, 2, xml.len());
2961
2962        preflight_document_limits(xml, settings, None)
2963            .expect("document plus root element must fit the exact node ceiling");
2964    }
2965
2966    #[test]
2967    fn node_preflight_continues_after_explicit_end_tags() {
2968        // The lexical reader is recreated at each event boundary. A closing tag
2969        // must still be observed so later siblings cannot bypass node policy.
2970        let xml = "<root><first></first><second/></root>";
2971        let settings = DocumentParseSettings::new(false, 3, xml.len());
2972
2973        assert!(matches!(
2974            preflight_document_limits(xml, settings, None),
2975            Err(XmlDocumentError::Parse(roxmltree::Error::NodesLimitReached))
2976        ));
2977    }
2978
2979    #[test]
2980    fn depth_preflight_expands_nested_internal_entity_markup() {
2981        // Entity replacement markup contributes real retained element depth.
2982        // The streaming guard must reject it before either DOM parser allocates
2983        // the expanded tree, including references nested inside replacements.
2984        let xml = r#"<!DOCTYPE root [
2985            <!ENTITY inner "<b><c/></b>">
2986            <!ENTITY deep "<a>&inner;</a>">
2987        ]><root>&deep;</root>"#;
2988        let settings = DocumentParseSettings::new_with_depth(true, 32, 3, xml.len());
2989
2990        assert!(matches!(
2991            preflight_document_limits(xml, settings, None),
2992            Err(XmlDocumentError::DocumentTooDeep {
2993                maximum: 3,
2994                actual: 4,
2995            })
2996        ));
2997
2998        let exact_settings = DocumentParseSettings::new_with_depth(true, 32, 4, xml.len());
2999        preflight_document_limits(xml, exact_settings, None)
3000            .expect("expanded markup at the exact depth boundary must be accepted");
3001    }
3002
3003    #[test]
3004    fn entity_preflight_ignores_declarations_inside_dtd_comments() {
3005        // Comment text is not a declaration. Treating it as one would let a
3006        // harmless reference acquire attacker-controlled phantom markup.
3007        let xml = r#"<!DOCTYPE root [
3008            <!-- <!ENTITY value "<a><b><c/></b></a>"> -->
3009            <!ENTITY value "ok">
3010        ]><root>&value;</root>"#;
3011        let settings = DocumentParseSettings::new_with_depth(true, 8, 1, xml.len());
3012
3013        preflight_document_limits(xml, settings, None)
3014            .expect("comment contents must not participate in entity expansion");
3015        build_cell(xml.to_owned(), settings, None)
3016            .expect("the real declaration contains only character data");
3017    }
3018
3019    #[test]
3020    fn entity_preflight_charges_nested_replacement_work() {
3021        // Repeated nested references must exhaust parser work during the
3022        // streaming pass, before either DOM parser receives the document.
3023        let xml = r#"<!DOCTYPE root [
3024            <!ENTITY a "0123456789">
3025            <!ENTITY b "&a;&a;&a;&a;">
3026        ]><root>&b;</root>"#;
3027        let first_replacement = "&a;&a;&a;&a;".len();
3028        let nested_replacement = "0123456789".len();
3029        let maximum = xml.len() + first_replacement + nested_replacement - 1;
3030        let budget = XmlParseWorkBudget::with_limit(maximum);
3031        let settings = DocumentParseSettings::new_with_depth(true, 32, 4, xml.len());
3032
3033        assert!(matches!(
3034            build_cell(xml.to_owned(), settings, Some(&budget)),
3035            Err(XmlDocumentError::Policy(crate::policy::PolicyViolation::ResourceLimit {
3036                resource: crate::policy::resource_name::XML_PARSE_WORK_BYTES,
3037                maximum: observed_maximum,
3038                actual,
3039            })) if observed_maximum == maximum
3040                && actual == xml.len() + first_replacement + nested_replacement
3041        ));
3042    }
3043
3044    #[test]
3045    fn entity_preflight_charges_nested_attribute_replacements() {
3046        // Attribute values are part of parser expansion work even though their
3047        // entity references are contained inside one lexical start-tag event.
3048        let xml = r#"<!DOCTYPE root [
3049            <!ENTITY a "0123456789">
3050            <!ENTITY b "&a;&a;&a;&a;">
3051        ]><root value="&b;"/>"#;
3052        let first_replacement = "&a;&a;&a;&a;".len();
3053        let nested_replacement = "0123456789".len();
3054        let expected_work = xml.len() + first_replacement + 4 * nested_replacement;
3055        let budget = XmlParseWorkBudget::with_limit(expected_work);
3056        let settings = DocumentParseSettings::new_with_depth(true, 8, 1, xml.len());
3057
3058        preflight_document_limits(xml, settings, Some(&budget))
3059            .expect("the exact repeated attribute expansion budget must be accepted");
3060        assert_eq!(budget.consumed(), expected_work);
3061
3062        let maximum = expected_work - 1;
3063        let budget = XmlParseWorkBudget::with_limit(maximum);
3064
3065        assert!(matches!(
3066            preflight_document_limits(xml, settings, Some(&budget)),
3067            Err(XmlDocumentError::Policy(crate::policy::PolicyViolation::ResourceLimit {
3068                resource: crate::policy::resource_name::XML_PARSE_WORK_BYTES,
3069                maximum: observed_maximum,
3070                actual,
3071            })) if observed_maximum == maximum
3072                && actual == expected_work
3073        ));
3074    }
3075
3076    #[test]
3077    fn entity_preflight_streams_dense_attribute_references_in_source_order() {
3078        // A dense attribute must fail on the first expansion that exhausts the
3079        // budget. Buffering every reference and popping in reverse both delays
3080        // enforcement and exposes the transient allocation amplification.
3081        let repeated = "&first;".repeat(4_096);
3082        let xml = format!(
3083            "<!DOCTYPE root [<!ENTITY first \"0123456789\"><!ENTITY last \"01234567890123456789\">]><root value=\"{repeated}&last;\"/>"
3084        );
3085        let maximum = xml.len() + 9;
3086        let budget = XmlParseWorkBudget::with_limit(maximum);
3087        let settings = DocumentParseSettings::new_with_depth(true, 8, 1, xml.len());
3088
3089        let mut scanner = general_references(b"&first;&last;");
3090        assert_eq!(scanner.next(), Some("first"));
3091        assert_eq!(scanner.next(), Some("last"));
3092        assert_eq!(scanner.next(), None);
3093
3094        assert!(matches!(
3095            preflight_document_limits(&xml, settings, Some(&budget)),
3096            Err(XmlDocumentError::Policy(crate::policy::PolicyViolation::ResourceLimit {
3097                resource: crate::policy::resource_name::XML_PARSE_WORK_BYTES,
3098                maximum: observed_maximum,
3099                actual,
3100            })) if observed_maximum == maximum && actual == xml.len() + 10
3101        ));
3102    }
3103
3104    #[test]
3105    fn entity_preflight_charges_applicable_dtd_attribute_defaults() {
3106        // Parser-created DTD defaults bypass lexical start-tag attributes when
3107        // the source element omits that name. Their references must use the same
3108        // budget as literal values without charging an overridden default.
3109        let omitted = r#"<!DOCTYPE root [
3110            <!ENTITY a "0123456789">
3111            <!ENTITY b "&a;&a;&a;&a;">
3112            <!ATTLIST root value CDATA "&b;">
3113        ]><root/>"#;
3114        let first_replacement = "&a;&a;&a;&a;".len();
3115        let nested_replacement = "0123456789".len();
3116        let expected_work = omitted.len() + first_replacement + 4 * nested_replacement;
3117        let settings = DocumentParseSettings::new_with_depth(true, 8, 1, omitted.len());
3118        let exact_budget = XmlParseWorkBudget::with_limit(expected_work);
3119
3120        preflight_document_limits(omitted, settings, Some(&exact_budget))
3121            .expect("the exact default-attribute expansion budget must be accepted");
3122        assert_eq!(exact_budget.consumed(), expected_work);
3123
3124        let maximum = expected_work - 1;
3125        let short_budget = XmlParseWorkBudget::with_limit(maximum);
3126        assert!(matches!(
3127            preflight_document_limits(omitted, settings, Some(&short_budget)),
3128            Err(XmlDocumentError::Policy(crate::policy::PolicyViolation::ResourceLimit {
3129                resource: crate::policy::resource_name::XML_PARSE_WORK_BYTES,
3130                maximum: observed_maximum,
3131                actual,
3132            })) if observed_maximum == maximum && actual == expected_work
3133        ));
3134
3135        let overridden = omitted.replace("<root/>", "<root value=\"literal\"/>");
3136        let source_only_budget = XmlParseWorkBudget::with_limit(overridden.len());
3137        let overridden_settings =
3138            DocumentParseSettings::new_with_depth(true, 8, 1, overridden.len());
3139        preflight_document_limits(&overridden, overridden_settings, Some(&source_only_budget))
3140            .expect("an explicit source attribute must suppress its DTD default");
3141        assert_eq!(source_only_budget.consumed(), overridden.len());
3142    }
3143
3144    #[test]
3145    fn entity_preflight_uses_bounded_heap_for_long_acyclic_chains() {
3146        // Entity indirection is independent of element depth. A legal acyclic
3147        // chain must not consume one native stack frame per replacement.
3148        const ENTITY_COUNT: usize = 4_096;
3149        let mut xml = String::from("<!DOCTYPE root [\n<!ENTITY e0 \"value\">\n");
3150        for index in 1..ENTITY_COUNT {
3151            use std::fmt::Write as _;
3152            writeln!(xml, "<!ENTITY e{index} \"&e{};\">", index - 1)
3153                .expect("write entity declaration");
3154        }
3155        use std::fmt::Write as _;
3156        write!(xml, "]><root>&e{};</root>", ENTITY_COUNT - 1).expect("write root reference");
3157        let settings = DocumentParseSettings::new_with_depth(true, 16, 1, xml.len());
3158
3159        preflight_document_limits(&xml, settings, None)
3160            .expect("long acyclic entity traversal must use bounded native stack");
3161    }
3162
3163    #[test]
3164    fn owned_parse_preflights_depth_before_dtd_provenance() {
3165        // Enabling internal DTD syntax must not move the provenance probe ahead
3166        // of the allocation-free depth boundary for an ordinary document.
3167        let xml = "<root><child><leaf/></child></root>";
3168        let settings = DocumentParseSettings::new_with_depth(true, 16, 2, xml.len());
3169        let budget = XmlParseWorkBudget::with_limit(xml.len());
3170
3171        assert!(matches!(
3172            XmlDocument::parse_with_settings_and_optional_budget(
3173                xml.to_owned(),
3174                settings,
3175                Some(&budget),
3176            ),
3177            Err(XmlDocumentError::DocumentTooDeep {
3178                maximum: 2,
3179                actual: 3,
3180            })
3181        ));
3182    }
3183
3184    fn nested_document(depth: usize) -> String {
3185        let mut xml = "<node>".repeat(depth);
3186        xml.push_str(&"</node>".repeat(depth));
3187        xml
3188    }
3189
3190    #[test]
3191    fn roxmltree_backend_enforces_exact_depth_boundary() {
3192        // The compatibility backend must enforce the same typed nesting
3193        // policy as the default parser rather than relying on backend defaults.
3194        let settings = DocumentParseSettings::new_with_depth(false, 128, 2, 4_096);
3195        let accepted = nested_document(2);
3196        build_roxmltree_cell(accepted, settings).expect("exact depth must parse");
3197
3198        let rejected = nested_document(3);
3199        assert!(matches!(
3200            build_roxmltree_cell(rejected, settings),
3201            Err(XmlDocumentError::DocumentTooDeep {
3202                maximum: 2,
3203                actual: 3,
3204            })
3205        ));
3206    }
3207
3208    #[cfg(feature = "xml-backend-xmloxide")]
3209    #[test]
3210    fn xmloxide_backend_enforces_exact_depth_boundary() {
3211        // The default backend rejects before constructing an oversized tree,
3212        // while exposing the same backend-neutral document error.
3213        let settings = DocumentParseSettings::new_with_depth(false, 128, 2, 4_096);
3214        let accepted = nested_document(2);
3215        build_cell(accepted, settings, None).expect("exact depth must parse");
3216
3217        let rejected = nested_document(3);
3218        assert!(matches!(
3219            build_cell(rejected, settings, None),
3220            Err(XmlDocumentError::DocumentTooDeep {
3221                maximum: 2,
3222                actual: 3,
3223            })
3224        ));
3225    }
3226
3227    struct OversizedBorrowedInput<'a>(&'a str);
3228
3229    impl AsRef<str> for OversizedBorrowedInput<'_> {
3230        fn as_ref(&self) -> &str {
3231            self.0
3232        }
3233    }
3234
3235    impl From<OversizedBorrowedInput<'_>> for String {
3236        fn from(_: OversizedBorrowedInput<'_>) -> Self {
3237            panic!("oversized borrowed XML must be rejected before ownership conversion")
3238        }
3239    }
3240
3241    #[test]
3242    fn oversized_borrowed_input_is_rejected_before_ownership_conversion() {
3243        // Borrowed request bodies can be arbitrarily large. Both constructors
3244        // must inspect their size before cloning them into the owned document.
3245        let oversized = "x".repeat(crate::hard_limits::XML_DOCUMENT_BYTE_CEILING + 1);
3246        assert!(matches!(
3247            XmlDocument::parse(OversizedBorrowedInput(&oversized)),
3248            Err(XmlDocumentError::DocumentTooLarge { .. })
3249        ));
3250
3251        #[cfg(feature = "xmldsig")]
3252        {
3253            let mut policy = crate::policy::SigningPolicy::default();
3254            policy.resources.max_xml_document_bytes = 8;
3255            assert!(matches!(
3256                XmlDocument::parse_with_policy(OversizedBorrowedInput("<root/>xx"), &policy),
3257                Err(XmlDocumentError::DocumentTooLarge {
3258                    maximum: 8,
3259                    actual: 9,
3260                })
3261            ));
3262        }
3263    }
3264
3265    #[test]
3266    fn views_reuse_identity_until_mutation_invalidates_generation() {
3267        let mut document =
3268            XmlDocument::parse("<root><child ID=\"target\"/></root>").expect("fixture must parse");
3269        let target = document.with_view(|view| {
3270            view.node_for_id("target", &[])
3271                .expect("ID target must resolve")
3272        });
3273        assert_eq!(document.generation(), 0);
3274        document
3275            .replace_element(target, "<replacement ID=\"target\"/>")
3276            .expect("replacement must succeed");
3277
3278        let error = document
3279            .with_view(|view| view.resolve_node(target).map(|_| ()))
3280            .expect_err("old identity must be stale");
3281        assert!(matches!(error, XmlDocumentError::StaleIdentity { .. }));
3282        assert_eq!(document.generation(), 1);
3283    }
3284
3285    #[test]
3286    fn identities_cannot_cross_documents() {
3287        let first = XmlDocument::parse("<root/>").expect("fixture must parse");
3288        let second = XmlDocument::parse("<root/>").expect("fixture must parse");
3289        let root = first.with_view(|view| view.root_element());
3290
3291        let error = second
3292            .with_view(|view| view.resolve_node(root).map(|_| ()))
3293            .expect_err("foreign identity must fail closed");
3294        assert!(matches!(error, XmlDocumentError::ForeignIdentity));
3295    }
3296
3297    #[test]
3298    fn child_append_projection_matches_both_element_forms() {
3299        // Signing must be able to enforce a tighter operation policy before
3300        // append_child allocates either form of the replacement document.
3301        for xml in ["<root></root>", "<root/>"] {
3302            let document = XmlDocument::parse(xml).expect("fixture must parse");
3303            let root = document.with_view(|view| view.root_element());
3304            let projected = document
3305                .projected_child_append_len(root, "<child/>".len())
3306                .expect("append length must project");
3307
3308            assert_eq!(projected, "<root><child/></root>".len());
3309        }
3310    }
3311
3312    #[test]
3313    fn document_identity_allocation_reports_exhaustion_without_wrapping() {
3314        let counter = AtomicU64::new(u64::MAX - 1);
3315
3316        assert_eq!(
3317            allocate_document_identity(&counter).expect("last identity must be allocated"),
3318            DocumentIdentity(u64::MAX - 1)
3319        );
3320        assert!(matches!(
3321            allocate_document_identity(&counter),
3322            Err(XmlDocumentError::IdentityExhausted)
3323        ));
3324        assert_eq!(counter.load(Ordering::Relaxed), u64::MAX);
3325    }
3326
3327    #[test]
3328    fn duplicate_and_caller_registered_ids_share_one_index() {
3329        let document = XmlDocument::parse(
3330            "<root><a ID=\"duplicate\" custom=\"selected\"/><b Id=\"duplicate\"/></root>",
3331        )
3332        .expect("fixture must parse");
3333        let registrations = [IdAttributeRegistration::global("custom")];
3334
3335        document.with_view(|view| {
3336            assert!(view.node_for_id("duplicate", &[]).is_none());
3337            assert!(view.node_for_id("selected", &registrations).is_some());
3338        });
3339    }
3340
3341    #[test]
3342    fn duplicate_default_id_stays_ambiguous_with_caller_registration() {
3343        // A caller registration must not turn an already ambiguous standard ID
3344        // into a unique match by selecting only one of the duplicate elements.
3345        let document = XmlDocument::parse(
3346            "<root><a ID=\"duplicate\" custom=\"duplicate\"/><b Id=\"duplicate\"/></root>",
3347        )
3348        .expect("fixture must parse");
3349        let registrations = [IdAttributeRegistration::global("custom")];
3350
3351        document.with_view(|view| {
3352            assert!(view.node_for_id("duplicate", &registrations).is_none());
3353        });
3354    }
3355
3356    #[test]
3357    fn content_replacement_uses_parent_namespace_context() {
3358        let mut document = XmlDocument::parse("<p:root xmlns:p=\"urn:test\"><p:old/></p:root>")
3359            .expect("fixture must parse");
3360        let root = document.with_view(|view| view.root_element());
3361        document
3362            .replace_content(root, "<p:new/>")
3363            .expect("in-scope prefix must be accepted");
3364        assert_eq!(
3365            document.as_xml(),
3366            "<p:root xmlns:p=\"urn:test\"><p:new/></p:root>"
3367        );
3368    }
3369
3370    #[test]
3371    fn validation_wrapper_does_not_consume_document_depth() {
3372        // The synthetic wrapper exists only while validating the replacement;
3373        // a result at the real document depth ceiling must remain accepted.
3374        let mut document = XmlDocument::parse_with_settings(
3375            "<root><target/></root>".into(),
3376            DocumentParseSettings::new_with_depth(false, 128, 2, 4_096),
3377        )
3378        .expect("fixture must fit the exact depth ceiling");
3379        let target = document.with_view(|view| {
3380            view.node_identity(
3381                view.document()
3382                    .descendants()
3383                    .find(|node| node.has_tag_name("target"))
3384                    .expect("target must exist"),
3385            )
3386        });
3387
3388        document
3389            .replace_content(target, "text")
3390            .expect("validation-only wrapper must not consume caller depth");
3391        assert_eq!(document.as_xml(), "<root><target>text</target></root>");
3392    }
3393
3394    #[test]
3395    fn validation_wrapper_preserves_real_depth_rejection() {
3396        // Exempting validation scaffolding must not exempt an inserted element
3397        // that makes the committed document exceed the configured depth.
3398        let mut document = XmlDocument::parse_with_settings(
3399            "<root><target/></root>".into(),
3400            DocumentParseSettings::new_with_depth(false, 128, 2, 4_096),
3401        )
3402        .expect("fixture must fit the exact depth ceiling");
3403        let target = document.with_view(|view| {
3404            view.node_identity(
3405                view.document()
3406                    .descendants()
3407                    .find(|node| node.has_tag_name("target"))
3408                    .expect("target must exist"),
3409            )
3410        });
3411        let before = document.as_xml().to_owned();
3412
3413        let error = document
3414            .replace_content(target, "<child/>")
3415            .expect_err("real replacement depth must remain bounded");
3416        assert!(
3417            matches!(
3418                error,
3419                XmlDocumentError::DocumentTooDeep {
3420                    maximum: 2,
3421                    actual: 3,
3422                }
3423            ),
3424            "unexpected error: {error:?}"
3425        );
3426        assert_eq!(document.as_xml(), before);
3427        assert_eq!(document.generation(), 0);
3428    }
3429
3430    #[test]
3431    fn batched_validation_wrappers_do_not_consume_document_depth() {
3432        // The combined validator inserts one wrapper per target, but sibling
3433        // scaffolding must not reduce the active depth available to either edit.
3434        let mut document = XmlDocument::parse_with_settings(
3435            "<root><first/><second/></root>".into(),
3436            DocumentParseSettings::new_with_depth(false, 128, 2, 4_096),
3437        )
3438        .expect("fixture must fit the exact depth ceiling");
3439        let (first, second) = document.with_view(|view| {
3440            let mut targets = view
3441                .document()
3442                .descendants()
3443                .filter(|node| matches!(node.tag_name().name(), "first" | "second"));
3444            (
3445                view.node_identity(targets.next().expect("first target")),
3446                view.node_identity(targets.next().expect("second target")),
3447            )
3448        });
3449
3450        document
3451            .replace_contents(&[(first, "a".into()), (second, "b".into())])
3452            .expect("validation-only wrappers must not consume caller depth");
3453        assert_eq!(
3454            document.as_xml(),
3455            "<root><first>a</first><second>b</second></root>"
3456        );
3457    }
3458
3459    #[test]
3460    fn mutations_ignore_greater_than_inside_quoted_attributes() {
3461        // The opening-tag boundary is the unquoted '>', not a character in an
3462        // attribute value; both content replacement and append use that boundary.
3463        let mut document =
3464            XmlDocument::parse("<root marker=\">\"><old/></root>").expect("fixture must parse");
3465        let root = document.with_view(|view| view.root_element());
3466
3467        document
3468            .replace_content(root, "<first/>")
3469            .expect("quoted greater-than must not corrupt content replacement");
3470        let root = document.with_view(|view| view.root_element());
3471        document
3472            .append_child(root, "<second/>")
3473            .expect("quoted greater-than must not corrupt child append");
3474
3475        assert_eq!(
3476            document.as_xml(),
3477            "<root marker=\">\"><first/><second/></root>"
3478        );
3479    }
3480
3481    #[test]
3482    fn fragment_validation_reuses_internal_dtd_entities() {
3483        // Validation must run in the original document context so an entity
3484        // declared by its internal subset remains available during mutation.
3485        let mut document = XmlDocument::parse_with_settings(
3486            "<!DOCTYPE root [<!ENTITY custom \"replacement\">]><root><old/></root>".into(),
3487            DocumentParseSettings::new(
3488                true,
3489                crate::hard_limits::XML_DOCUMENT_NODE_CEILING,
3490                crate::hard_limits::XML_DOCUMENT_BYTE_CEILING,
3491            ),
3492        )
3493        .expect("DTD fixture must parse when explicitly enabled");
3494        let root = document.with_view(|view| view.root_element());
3495
3496        document
3497            .replace_content(root, "&custom;")
3498            .expect("declared entity must remain valid in replacement context");
3499        assert_eq!(
3500            document.as_xml(),
3501            "<!DOCTYPE root [<!ENTITY custom \"replacement\">]><root>&custom;</root>"
3502        );
3503    }
3504
3505    #[test]
3506    fn serialization_is_stable_for_unchanged_views() {
3507        let document = XmlDocument::parse("<?pi value?><root a=\"1\"><!--c--></root>")
3508            .expect("fixture must parse");
3509        let first = document.as_xml().to_owned();
3510        document.with_view(|view| assert_eq!(view.xml(), first));
3511        document.with_view(|view| assert_eq!(view.xml(), first));
3512        assert_eq!(document.as_xml(), first);
3513    }
3514
3515    #[test]
3516    fn rejected_mutation_keeps_source_and_generation_unchanged() {
3517        // Failed fragment parsing must be transactional: callers can safely
3518        // retry without observing a partially changed document or stale IDs.
3519        let mut document = XmlDocument::parse("<root><child/></root>").expect("fixture must parse");
3520        let root = document.with_view(|view| view.root_element());
3521        let before = document.as_xml().to_owned();
3522
3523        assert!(document.replace_content(root, "<unclosed>").is_err());
3524        assert_eq!(document.as_xml(), before);
3525        assert_eq!(document.generation(), 0);
3526    }
3527
3528    #[test]
3529    fn oversized_invalid_fragment_fails_at_the_document_byte_boundary() {
3530        // Resource rejection must happen before wrapper parsing, so malformed
3531        // attacker input cannot force allocations beyond the document ceiling.
3532        // Cover both direct content replacement and self-closing expansion.
3533        for xml in ["<root><child/></root>", "<root/>"] {
3534            let mut document = XmlDocument::parse_with_settings(
3535                xml.into(),
3536                DocumentParseSettings::new(false, 64, 64),
3537            )
3538            .expect("bounded fixture must parse");
3539            let root = document.with_view(|view| view.root_element());
3540            let replacement = "<".repeat(1_024);
3541
3542            assert!(matches!(
3543                document.replace_content(root, &replacement),
3544                Err(XmlDocumentError::DocumentTooLarge {
3545                    maximum: 64,
3546                    actual,
3547                }) if actual > 64
3548            ));
3549            assert_eq!(document.as_xml(), xml);
3550            assert_eq!(document.generation(), 0);
3551        }
3552    }
3553
3554    #[cfg(feature = "xmlenc")]
3555    #[test]
3556    fn bounded_fragment_validation_uses_the_active_node_ceiling() {
3557        // The validation wrapper must not parse attacker-controlled plaintext
3558        // under a broader document-creation ceiling before the operation limit.
3559        let mut document = XmlDocument::parse_with_settings(
3560            "<root><target/></root>".into(),
3561            DocumentParseSettings::new(false, 128, 4_096),
3562        )
3563        .expect("fixture must parse");
3564        let target = document.with_view(|view| {
3565            let target = view
3566                .document()
3567                .descendants()
3568                .find(|node| node.has_tag_name("target"))
3569                .expect("target must exist");
3570            view.node_identity(target)
3571        });
3572        let maximum = document.with_view(|view| view.node_count());
3573        let before = document.as_xml().to_owned();
3574        let budget = XmlParseWorkBudget::from_resources(&crate::policy::ResourcePolicy::default());
3575
3576        assert!(matches!(
3577            document.replace_node_with_fragment_with_budget(
3578                target,
3579                "<replacement><child/><child/><malformed>",
3580                DocumentParseSettings::new(false, maximum as u32, 4_096),
3581                &budget,
3582            ),
3583            Err(XmlDocumentError::ProjectedNodeLimit { maximum: rejected })
3584                if rejected == maximum
3585        ));
3586        assert_eq!(document.as_xml(), before);
3587        assert_eq!(document.generation(), 0);
3588    }
3589
3590    #[test]
3591    fn duplicate_empty_content_targets_are_rejected_atomically() {
3592        let mut document =
3593            XmlDocument::parse("<root><target></target></root>").expect("fixture must parse");
3594        let target = document.with_view(|view| {
3595            let target = view
3596                .document()
3597                .descendants()
3598                .find(|node| node.has_tag_name("target"))
3599                .expect("target must exist");
3600            view.node_identity(target)
3601        });
3602
3603        let error = document
3604            .replace_contents(&[(target, "first".into()), (target, "second".into())])
3605            .expect_err("one identity cannot be replaced twice");
3606
3607        assert!(matches!(error, XmlDocumentError::InvalidReplacement(_)));
3608        assert_eq!(document.as_xml(), "<root><target></target></root>");
3609        assert_eq!(document.generation(), 0);
3610    }
3611
3612    #[cfg(feature = "xmldsig")]
3613    #[test]
3614    fn batched_content_replacements_obey_the_active_byte_ceiling() {
3615        // A retained document can have broader parse settings than the current
3616        // signing operation; the complete batch must use the active ceiling.
3617        let mut document = XmlDocument::parse_with_settings(
3618            "<root><first/><second/></root>".into(),
3619            DocumentParseSettings::new(false, 128, 4_096),
3620        )
3621        .expect("fixture must parse");
3622        let (first, second) = document.with_view(|view| {
3623            let mut targets = view
3624                .document()
3625                .descendants()
3626                .filter(|node| matches!(node.tag_name().name(), "first" | "second"));
3627            (
3628                view.node_identity(targets.next().expect("first target")),
3629                view.node_identity(targets.next().expect("second target")),
3630            )
3631        });
3632        let before = document.as_xml().to_owned();
3633        let replacements = [(first, "alpha".into()), (second, "beta".into())];
3634        let expected = "<root><first>alpha</first><second>beta</second></root>";
3635        let maximum = expected.len() - 1;
3636        let budget = XmlParseWorkBudget::from_resources(&crate::policy::ResourcePolicy::default());
3637
3638        let error = document
3639            .replace_contents_with_budget(
3640                &replacements,
3641                DocumentParseSettings::new(false, 128, maximum),
3642                &budget,
3643            )
3644            .expect_err("the active byte ceiling must reject the complete batch");
3645
3646        assert!(matches!(
3647            error,
3648            XmlDocumentError::DocumentTooLarge {
3649                maximum: observed_maximum,
3650                actual,
3651            } if observed_maximum == maximum && actual == expected.len()
3652        ));
3653        assert_eq!(document.as_xml(), before);
3654        assert_eq!(document.generation(), 0);
3655    }
3656
3657    #[test]
3658    fn batched_content_replacements_validate_one_namespaced_candidate() {
3659        // The combined validator must preserve each target's parent namespace
3660        // context across both self-closing expansion and ordinary replacement.
3661        let mut document = XmlDocument::parse(
3662            r#"<root xmlns:p="urn:test"><first/><second><old/></second></root>"#,
3663        )
3664        .expect("fixture must parse");
3665        let (first, second) = document.with_view(|view| {
3666            let mut targets = view
3667                .document()
3668                .descendants()
3669                .filter(|node| matches!(node.tag_name().name(), "first" | "second"));
3670            (
3671                view.node_identity(targets.next().expect("first target")),
3672                view.node_identity(targets.next().expect("second target")),
3673            )
3674        });
3675
3676        document
3677            .replace_contents(&[
3678                (first, "<p:alpha/>".into()),
3679                (second, "text<p:beta/>".into()),
3680            ])
3681            .expect("all fragments must validate in one candidate");
3682
3683        assert_eq!(
3684            document.as_xml(),
3685            r#"<root xmlns:p="urn:test"><first><p:alpha/></first><second>text<p:beta/></second></root>"#
3686        );
3687        assert_eq!(document.generation(), 1);
3688    }
3689
3690    #[cfg(feature = "xmldsig")]
3691    #[test]
3692    fn batched_content_replacements_charge_two_combined_parses() {
3693        // A full 64-reference signing batch must pay for one combined
3694        // structural-validation candidate and one committed generation, not
3695        // one full-document parse per independent DigestValue replacement.
3696        const TARGETS: usize = 64;
3697        let source = format!("<root>{}</root>", "<item>old</item>".repeat(TARGETS));
3698        let mut document = XmlDocument::parse(&source).expect("fixture must parse");
3699        let replacements = document.with_view(|view| {
3700            view.document()
3701                .descendants()
3702                .filter(|node| node.has_tag_name("item"))
3703                .map(|node| (view.node_identity(node), "x".to_owned()))
3704                .collect::<Vec<_>>()
3705        });
3706        let committed_len = source.len() - TARGETS * "old".len() + TARGETS;
3707        let validation_len =
3708            source.len() - TARGETS * "old".len() + TARGETS * wrapped_fragment("x").len();
3709        let parser_passes = selected_parser_passes();
3710        let maximum = (validation_len + committed_len) * parser_passes;
3711        let budget = XmlParseWorkBudget::with_limit(maximum);
3712
3713        document
3714            .replace_contents_with_budget(&replacements, DocumentParseSettings::default(), &budget)
3715            .expect("the batch must fit its two combined parser passes exactly");
3716
3717        assert_eq!(budget.consumed(), maximum);
3718        assert_eq!(document.as_xml().matches("<item>x</item>").count(), TARGETS);
3719        assert_eq!(document.generation(), 1);
3720    }
3721
3722    #[test]
3723    fn batched_content_replacements_reject_one_escaped_boundary_atomically() {
3724        // One fragment escaping its target must reject the whole batch even
3725        // when every other replacement is structurally valid.
3726        let mut document = XmlDocument::parse("<root><first></first><second></second></root>")
3727            .expect("fixture must parse");
3728        let (first, second) = document.with_view(|view| {
3729            let mut targets = view
3730                .document()
3731                .descendants()
3732                .filter(|node| matches!(node.tag_name().name(), "first" | "second"));
3733            (
3734                view.node_identity(targets.next().expect("first target")),
3735                view.node_identity(targets.next().expect("second target")),
3736            )
3737        });
3738        let before = document.as_xml().to_owned();
3739
3740        document
3741            .replace_contents(&[
3742                (first, "</first><attacker/><first>".into()),
3743                (second, "safe".into()),
3744            ])
3745            .expect_err("a fragment must not escape its target boundary");
3746
3747        assert_eq!(document.as_xml(), before);
3748        assert_eq!(document.generation(), 0);
3749    }
3750
3751    #[cfg(feature = "xmldsig")]
3752    #[test]
3753    fn retained_mutations_share_one_sticky_parse_work_budget() {
3754        // Each mutation performs one wrapped validation parse and one committed
3755        // document parse. A later mutation must inherit the consumed allowance,
3756        // and a rejected charge must exhaust it rather than permit retries.
3757        let mut document =
3758            XmlDocument::parse("<root><first/><second/></root>").expect("fixture must parse");
3759        let first = document.with_view(|view| {
3760            view.node_identity(
3761                view.document()
3762                    .descendants()
3763                    .find(|node| node.has_tag_name("first"))
3764                    .expect("first target"),
3765            )
3766        });
3767        let first_output = "<root><first>a</first><second/></root>";
3768        let first_validation_len = first_output
3769            .len()
3770            .checked_add(VALIDATION_WRAPPER_OPEN.len())
3771            .and_then(|length| length.checked_add(VALIDATION_WRAPPER_CLOSE.len()))
3772            .expect("fixture length must fit");
3773        let parser_passes = selected_parser_passes();
3774        let maximum = (first_validation_len + first_output.len()) * parser_passes;
3775        let budget = XmlParseWorkBudget::with_limit(maximum);
3776
3777        document
3778            .replace_contents_with_budget(
3779                &[(first, "a".into())],
3780                DocumentParseSettings::default(),
3781                &budget,
3782            )
3783            .expect("the first mutation must consume the exact allowance");
3784        assert_eq!(document.as_xml(), first_output);
3785
3786        let second = document.with_view(|view| {
3787            view.node_identity(
3788                view.document()
3789                    .descendants()
3790                    .find(|node| node.has_tag_name("second"))
3791                    .expect("second target"),
3792            )
3793        });
3794        let before = document.as_xml().to_owned();
3795        let error = document
3796            .replace_contents_with_budget(
3797                &[(second, "b".into())],
3798                DocumentParseSettings::default(),
3799                &budget,
3800            )
3801            .expect_err("the second mutation must not receive a fresh allowance");
3802
3803        assert!(matches!(
3804            error,
3805            XmlDocumentError::Policy(crate::policy::PolicyViolation::ResourceLimit {
3806                resource: crate::policy::resource_name::XML_PARSE_WORK_BYTES,
3807                maximum: observed_maximum,
3808                actual,
3809            }) if observed_maximum == maximum && actual > maximum
3810        ));
3811        assert_eq!(document.as_xml(), before);
3812        assert_eq!(document.generation(), 1);
3813    }
3814
3815    #[cfg(feature = "xmldsig")]
3816    #[test]
3817    fn bounded_content_replacement_rejects_new_text_node_atomically() {
3818        let mut document =
3819            XmlDocument::parse("<root><target/></root>").expect("fixture must parse");
3820        let target = document.with_view(|view| {
3821            let root = view
3822                .resolve_node(view.root_element())
3823                .expect("root must resolve");
3824            view.node_identity(
3825                root.children()
3826                    .find(Node::is_element)
3827                    .expect("target must exist"),
3828            )
3829        });
3830        let maximum = document.with_view(|view| view.node_count());
3831        let before = document.as_xml().to_owned();
3832        let budget = XmlParseWorkBudget::from_resources(&crate::policy::ResourcePolicy::default());
3833
3834        assert!(matches!(
3835            document.replace_content_with_budget(
3836                target,
3837                "value",
3838                DocumentParseSettings::new(false, maximum as u32, 1_024),
3839                &budget,
3840            ),
3841            Err(XmlDocumentError::ProjectedNodeLimit { maximum: rejected })
3842                if rejected == maximum
3843        ));
3844        assert_eq!(document.as_xml(), before);
3845        assert_eq!(document.generation(), 0);
3846    }
3847
3848    #[cfg(feature = "xmldsig")]
3849    #[test]
3850    fn bounded_append_accepts_text_merged_with_existing_content() {
3851        // Wrapper validation temporarily separates adjacent text, but the
3852        // committed document merges it and therefore stays at the node ceiling.
3853        let mut document = XmlDocument::parse_with_settings(
3854            "<root>text</root>".into(),
3855            DocumentParseSettings::new(false, 3, 1_024),
3856        )
3857        .expect("fixture must fit the exact node ceiling");
3858        let root = document.with_view(|view| view.root_element());
3859        let budget = XmlParseWorkBudget::from_resources(&crate::policy::ResourcePolicy::default());
3860
3861        document
3862            .append_child_with_budget(
3863                root,
3864                "more",
3865                DocumentParseSettings::new(false, 3, 1_024),
3866                &budget,
3867            )
3868            .expect("merged text must not consume another committed node");
3869
3870        assert_eq!(document.as_xml(), "<root>textmore</root>");
3871        assert_eq!(document.with_view(|view| view.node_count()), 3);
3872    }
3873
3874    #[cfg(feature = "xmlenc")]
3875    #[test]
3876    fn bounded_fragment_accepts_two_boundary_text_merges() {
3877        // A wrapper can prevent text merging on both sides of a replacement.
3878        // The final candidate still contains one text node and fits exactly.
3879        let mut document =
3880            XmlDocument::parse("<root>left<target/>right</root>").expect("fixture must parse");
3881        let target = document.with_view(|view| {
3882            let target = view
3883                .document()
3884                .descendants()
3885                .find(|node| node.has_tag_name("target"))
3886                .expect("target must exist");
3887            view.node_identity(target)
3888        });
3889        let budget = XmlParseWorkBudget::from_resources(&crate::policy::ResourcePolicy::default());
3890
3891        document
3892            .replace_node_with_fragment_with_budget(
3893                target,
3894                "middle",
3895                DocumentParseSettings::new(false, 3, 1_024),
3896                &budget,
3897            )
3898            .expect("both boundary text pairs must merge in the committed document");
3899
3900        assert_eq!(document.as_xml(), "<root>leftmiddleright</root>");
3901        assert_eq!(document.with_view(|view| view.node_count()), 3);
3902    }
3903
3904    #[test]
3905    fn semantic_order_covers_namespace_attribute_and_tree_nodes() {
3906        let document =
3907            XmlDocument::parse("<root xmlns:p=\"urn:test\" p:value=\"1\"><child/></root>")
3908                .expect("fixture must parse");
3909
3910        document.with_view(|view| {
3911            let root = view.root_element();
3912            let root_node = view.resolve_node(root).expect("root must resolve");
3913            let child = view.node_identity(
3914                root_node
3915                    .children()
3916                    .find(Node::is_element)
3917                    .expect("child must exist"),
3918            );
3919            let attribute = view
3920                .attribute_identity(root, Some("urn:test"), "value")
3921                .expect("attribute must exist");
3922            let namespace = view
3923                .namespace_identities(root)
3924                .expect("namespace axis must exist")
3925                .into_iter()
3926                .find(|namespace| namespace.prefix() == "p")
3927                .expect("p namespace must exist");
3928
3929            let root_order = view.node_order(root).expect("root order must exist");
3930            let namespace_order = view
3931                .namespace_order(&namespace)
3932                .expect("namespace order must exist");
3933            let attribute_order = view
3934                .attribute_order(&attribute)
3935                .expect("attribute order must exist");
3936            let child_order = view.node_order(child).expect("child order must exist");
3937            assert!(root_order < namespace_order);
3938            assert!(namespace_order < attribute_order);
3939            assert!(attribute_order < child_order);
3940        });
3941    }
3942
3943    #[test]
3944    fn inherited_namespace_axes_are_materialized_only_for_the_requested_owner() {
3945        // A wide namespace scope inherited by many descendants must remain in
3946        // roxmltree's structural representation instead of being cloned into an
3947        // O(elements * namespaces) eager document index.
3948        let declarations = (0..128)
3949            .map(|index| format!(r#" xmlns:p{index}="urn:namespace:{index}""#))
3950            .collect::<String>();
3951        let children = (0..1_024)
3952            .map(|index| format!("<child index=\"{index}\"/>"))
3953            .collect::<String>();
3954        let document = XmlDocument::parse(format!("<root{declarations}>{children}</root>"))
3955            .expect("wide namespace fixture must parse");
3956
3957        document.with_view(|view| {
3958            let last_child = view
3959                .document()
3960                .descendants()
3961                .rfind(|node| node.has_tag_name("child"))
3962                .expect("last child must exist");
3963            let owner = view.node_identity(last_child);
3964            let namespaces = view
3965                .namespace_identities(owner)
3966                .expect("inherited namespace axis must materialize on demand");
3967            assert_eq!(namespaces.len(), 128);
3968            assert!(namespaces.windows(2).all(|pair| {
3969                view.namespace_order(&pair[0]).expect("left order")
3970                    < view.namespace_order(&pair[1]).expect("right order")
3971            }));
3972        });
3973    }
3974
3975    #[test]
3976    fn mutation_invalidates_attribute_and_namespace_identities() {
3977        let mut document =
3978            XmlDocument::parse("<root xmlns:p=\"urn:test\" p:value=\"1\"><child/></root>")
3979                .expect("fixture must parse");
3980        let (root, attribute, namespace) = document.with_view(|view| {
3981            let root = view.root_element();
3982            let attribute = view
3983                .attribute_identity(root, Some("urn:test"), "value")
3984                .expect("attribute must exist");
3985            let namespace = view
3986                .namespace_identities(root)
3987                .expect("namespace axis must exist")
3988                .into_iter()
3989                .find(|namespace| namespace.prefix() == "p")
3990                .expect("p namespace must exist");
3991            (root, attribute, namespace)
3992        });
3993
3994        document
3995            .append_child(root, "<p:next/>")
3996            .expect("mutation must succeed");
3997        document.with_view(|view| {
3998            assert!(matches!(
3999                view.attribute_order(&attribute),
4000                Err(XmlDocumentError::StaleIdentity { .. })
4001            ));
4002            assert!(matches!(
4003                view.namespace_order(&namespace),
4004                Err(XmlDocumentError::StaleIdentity { .. })
4005            ));
4006        });
4007    }
4008
4009    #[test]
4010    fn content_replacement_cannot_escape_its_target_element() {
4011        let mut document = XmlDocument::parse("<outer><target><old/></target></outer>")
4012            .expect("fixture must parse");
4013        let target = document.with_view(|view| {
4014            view.node_identity(
4015                view.document()
4016                    .descendants()
4017                    .find(|node| node.has_tag_name("target"))
4018                    .expect("target must exist"),
4019            )
4020        });
4021        let before = document.as_xml().to_owned();
4022
4023        assert!(
4024            document
4025                .replace_content(target, "</target><attacker/><target>")
4026                .is_err()
4027        );
4028        assert_eq!(document.as_xml(), before);
4029        assert_eq!(document.generation(), 0);
4030    }
4031}