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