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