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