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