Skip to main content

xml_sec/
document.rs

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