Skip to main content

rusty_xml_parser/
parse.rs

1//! UTF-8 well-formed document parser. No DTD / HTML / XInclude / recovery (M1).
2
3use rusty_xml_sax::{SaxAttr, SaxHandler};
4use rusty_xml_tree::{NodeId, NodeKind, XmlDoc};
5
6use crate::chvalid::{xml_is_char, xml_is_name_char, xml_is_name_start_char};
7use crate::error::*;
8
9/// libxml2 `xmlParserOption` bits (numeric identity).
10pub const XML_PARSE_RECOVER: i32 = 1 << 0;
11pub const XML_PARSE_NOENT: i32 = 1 << 1;
12pub const XML_PARSE_DTDLOAD: i32 = 1 << 2;
13pub const XML_PARSE_DTDATTR: i32 = 1 << 3;
14pub const XML_PARSE_DTDVALID: i32 = 1 << 4;
15pub const XML_PARSE_NOERROR: i32 = 1 << 5;
16pub const XML_PARSE_NOWARNING: i32 = 1 << 6;
17pub const XML_PARSE_PEDANTIC: i32 = 1 << 7;
18pub const XML_PARSE_NOBLANKS: i32 = 1 << 8;
19pub const XML_PARSE_SAX1: i32 = 1 << 9;
20pub const XML_PARSE_XINCLUDE: i32 = 1 << 10;
21pub const XML_PARSE_NONET: i32 = 1 << 11;
22pub const XML_PARSE_NODICT: i32 = 1 << 12;
23pub const XML_PARSE_NSCLEAN: i32 = 1 << 13;
24pub const XML_PARSE_NOCDATA: i32 = 1 << 14;
25pub const XML_PARSE_NOXINCNODE: i32 = 1 << 15;
26pub const XML_PARSE_COMPACT: i32 = 1 << 16;
27pub const XML_PARSE_OLD10: i32 = 1 << 17;
28pub const XML_PARSE_NOBASEFIX: i32 = 1 << 18;
29pub const XML_PARSE_HUGE: i32 = 1 << 19;
30pub const XML_PARSE_OLDSAX: i32 = 1 << 20;
31pub const XML_PARSE_IGNORE_ENC: i32 = 1 << 21;
32pub const XML_PARSE_BIG_LINES: i32 = 1 << 22;
33pub const XML_PARSE_NO_XXE: i32 = 1 << 23;
34pub const XML_PARSE_UNZIP: i32 = 1 << 24;
35
36/// Deliver SAX events without building a document tree.
37/// **A rusty_xml extension, not a libxml2 flag.**
38///
39/// Every entry point here materialises the whole document, including the ones
40/// whose job is streaming: `xml_sax_parse_memory` built a full tree and then
41/// discarded it, and `xml_reader_for_memory` builds a tree and walks it with a
42/// cursor. A consumer that only wants text -- an indexer, a document converter
43/// -- paid for a DOM it never touched.
44///
45/// With this set, character data, CDATA, comments, processing instructions and
46/// attributes create no nodes. The SAX event stream is unchanged and complete;
47/// the returned [`XmlDoc`] holds only the element skeleton and should be
48/// ignored.
49pub const XML_PARSE_NO_TREE: i32 = 1 << 30;
50pub const XML_PARSE_NO_SYS_CATALOG: i32 = 1 << 25;
51pub const XML_PARSE_CATALOG_PI: i32 = 1 << 26;
52pub const XML_PARSE_SKIP_IDS: i32 = 1 << 27;
53
54const XML_NS: &str = "http://www.w3.org/XML/1998/namespace";
55const XMLNS_NS: &str = "http://www.w3.org/2000/xmlns/";
56
57/// Element nesting limit.
58///
59/// This was 64 because the parser recursed and the cap had to sit below the
60/// stack limit -- about 1.4 KB per level in release and 22 KB in debug, so a
61/// deeper document aborted the process instead of returning an error. The
62/// content loop is iterative now and document depth costs no stack, so the cap
63/// is a POLICY limit again rather than a crash guard.
64///
65/// 5000 matches what libxml2 permits by default and is beyond any real markup;
66/// `XML_PARSE_HUGE` lifts it, which it can now do safely.
67const MAX_DEPTH: u32 = 5_000;
68
69/// The ceiling `XML_PARSE_HUGE` raises the nesting limit to. Bounded rather
70/// than unlimited so a hostile document cannot make the arena grow without end.
71const MAX_DEPTH_HUGE: u32 = 1_000_000;
72
73/// `XML_PARSE_HUGE` deliberately does **not** lift the nesting limit.
74///
75/// A depth cap only protects you when the cap is BELOW the stack limit. The
76/// element parser is recursive descent, and a debug build overflows around 95
77/// levels, so any cap above that is not a limit at all -- the process aborts
78/// before the check fires, and a stack overflow cannot be caught. Raising the
79/// cap to 256 or 512 therefore did not give callers deeper documents, it gave
80/// them a crash instead of an error.
81///
82/// HUGE still lifts the other limits it guards (name length, text length).
83/// Genuinely deeper nesting requires an iterative parser; that is the fix, and
84/// a larger constant is not a substitute for it.
85
86const MAX_NAME: usize = 50_000;
87const MAX_TEXT: usize = 10_000_000;
88
89/// Safe defaults: no network, no XXE.
90pub fn default_parse_options() -> i32 {
91    XML_PARSE_NONET | XML_PARSE_NO_XXE
92}
93
94/// `xmlInitParser` — no process-global ctor in Rust.
95#[doc(alias = "xmlInitParser")]
96pub fn xml_init_parser() {}
97
98/// `xmlCleanupParser` — no-op.
99#[doc(alias = "xmlCleanupParser")]
100pub fn xml_cleanup_parser() {}
101
102/// Parser context (`xmlParserCtxt`).
103#[derive(Debug, Default)]
104pub struct XmlParserCtxt {
105    pub options: i32,
106    pub last_error: Option<XmlError>,
107    pub doc: Option<XmlDoc>,
108}
109
110/// `xmlNewParserCtxt`.
111#[doc(alias = "xmlNewParserCtxt")]
112pub fn xml_new_parser_ctxt() -> XmlParserCtxt {
113    XmlParserCtxt {
114        options: default_parse_options(),
115        last_error: None,
116        doc: None,
117    }
118}
119
120/// `xmlCtxtUseOptions`.
121#[doc(alias = "xmlCtxtUseOptions")]
122pub fn xml_ctxt_use_options(ctxt: &mut XmlParserCtxt, options: i32) -> i32 {
123    ctxt.options = options | XML_PARSE_NONET | XML_PARSE_NO_XXE;
124    0
125}
126
127/// `xmlCtxtSetOptions`.
128#[doc(alias = "xmlCtxtSetOptions")]
129pub fn xml_ctxt_set_options(ctxt: &mut XmlParserCtxt, options: i32) -> i32 {
130    xml_ctxt_use_options(ctxt, options)
131}
132
133/// `xmlCtxtGetOptions`.
134#[doc(alias = "xmlCtxtGetOptions")]
135pub fn xml_ctxt_get_options(ctxt: &XmlParserCtxt) -> i32 {
136    ctxt.options
137}
138
139/// `xmlCtxtGetLastError`.
140#[doc(alias = "xmlCtxtGetLastError")]
141pub fn xml_ctxt_get_last_error(ctxt: &XmlParserCtxt) -> Option<&XmlError> {
142    ctxt.last_error.as_ref()
143}
144
145/// `xmlCtxtGetDocument`.
146#[doc(alias = "xmlCtxtGetDocument")]
147pub fn xml_ctxt_get_document(ctxt: &XmlParserCtxt) -> Option<&XmlDoc> {
148    ctxt.doc.as_ref()
149}
150
151/// `xmlReadMemory`.
152#[doc(alias = "xmlReadMemory")]
153pub fn xml_read_memory(
154    buffer: &[u8],
155    url: Option<&str>,
156    encoding: Option<&str>,
157    options: i32,
158) -> Result<XmlDoc, XmlError> {
159    let mut sink = rusty_xml_sax::NullSax;
160    parse_doc(buffer, url, encoding, options, &mut sink)
161}
162
163/// `xmlReadDoc`.
164#[doc(alias = "xmlReadDoc")]
165pub fn xml_read_doc(
166    cur: &str,
167    url: Option<&str>,
168    encoding: Option<&str>,
169    options: i32,
170) -> Result<XmlDoc, XmlError> {
171    xml_read_memory(cur.as_bytes(), url, encoding, options)
172}
173
174/// `xmlReadFile`.
175#[doc(alias = "xmlReadFile")]
176pub fn xml_read_file(
177    filename: &str,
178    encoding: Option<&str>,
179    options: i32,
180) -> Result<XmlDoc, XmlError> {
181    let bytes = std::fs::read(filename).map_err(|e| {
182        XmlError::new(XML_ERR_DOCUMENT_START, e.to_string(), 0, 0)
183    })?;
184    xml_read_memory(&bytes, Some(filename), encoding, options)
185}
186
187/// `xmlCtxtReadMemory`.
188#[doc(alias = "xmlCtxtReadMemory")]
189pub fn xml_ctxt_read_memory(
190    ctxt: &mut XmlParserCtxt,
191    buffer: &[u8],
192    url: Option<&str>,
193    encoding: Option<&str>,
194    options: i32,
195) -> Result<XmlDoc, XmlError> {
196    let opts = if options != 0 { options } else { ctxt.options };
197    match xml_read_memory(buffer, url, encoding, opts) {
198        Ok(doc) => {
199            ctxt.doc = Some(doc.clone());
200            ctxt.last_error = None;
201            Ok(doc)
202        }
203        Err(e) => {
204            ctxt.last_error = Some(e.clone());
205            Err(e)
206        }
207    }
208}
209
210/// Parse and record SAX events (for the event-exact gate).
211pub fn xml_sax_parse_memory(
212    buffer: &[u8],
213    options: i32,
214    sax: &mut dyn SaxHandler,
215) -> Result<XmlDoc, XmlError> {
216    parse_doc(buffer, None, None, options, sax)
217}
218
219/// Push parser context (`xmlCreatePushParserCtxt`).
220pub struct XmlPushParserCtxt {
221    buf: Vec<u8>,
222    options: i32,
223    url: Option<String>,
224    encoding: Option<String>,
225    last_error: Option<XmlError>,
226    /// Parser state between chunks. `None` until the prolog and the root's
227    /// start tag have been seen, because until then there is nothing to resume.
228    state: Option<PushState>,
229    consumed: usize,
230    /// Set when the document needs an encoding conversion or has a BOM. The
231    /// streaming path works on raw bytes and cannot convert as it goes, so
232    /// those documents are buffered whole, as they were before streaming.
233    no_stream: bool,
234}
235
236impl XmlPushParserCtxt {
237    /// The last error, if the most recent chunk failed to parse.
238    pub fn last_error(&self) -> Option<&XmlError> {
239        self.last_error.as_ref()
240    }
241
242    /// Bytes buffered but not yet parsed.
243    ///
244    /// Once streaming starts this is only the unparsed tail, not the document
245    /// seen so far -- that is the whole point of the push parser.
246    pub fn buffered(&self) -> usize {
247        self.buf.len()
248    }
249
250    /// Total bytes parsed and released so far.
251    pub fn consumed(&self) -> usize {
252        self.consumed
253    }
254}
255
256/// `xmlCreatePushParserCtxt`.
257#[doc(alias = "xmlCreatePushParserCtxt")]
258pub fn xml_create_push_parser_ctxt(
259    chunk: &[u8],
260    url: Option<&str>,
261    encoding: Option<&str>,
262    options: i32,
263) -> XmlPushParserCtxt {
264    XmlPushParserCtxt {
265        buf: chunk.to_vec(),
266        options: options | XML_PARSE_NONET | XML_PARSE_NO_XXE,
267        url: url.map(str::to_string),
268        encoding: encoding.map(str::to_string),
269        last_error: None,
270        state: None,
271        consumed: 0,
272        no_stream: false,
273    }
274}
275
276/// `xmlParseChunk`. `terminate != 0` finishes the document.
277#[doc(alias = "xmlParseChunk")]
278pub fn xml_parse_chunk(
279    ctxt: &mut XmlPushParserCtxt,
280    chunk: &[u8],
281    terminate: i32,
282) -> Result<Option<XmlDoc>, XmlError> {
283    ctxt.buf.extend_from_slice(chunk);
284    let terminate = terminate != 0;
285    let opts = ctxt.options;
286
287    // Phase 1 -- prolog. Nothing can stream until the root's start tag is in
288    // hand, so buffer until it parses. The prolog is small, and re-parsing it
289    // per chunk is cheap; the body, which is not small, is never re-parsed.
290    if ctxt.state.is_none() && !ctxt.no_stream {
291        // Conversion is stateful and the streaming path hands raw bytes to the
292        // parser, so anything that is not already plain UTF-8 is buffered.
293        match crate::encoding::xml_convert_to_utf8_cow(&ctxt.buf, ctxt.encoding.as_deref()) {
294            Ok((std::borrow::Cow::Borrowed(b), _)) if b.len() == ctxt.buf.len() => {}
295            _ => {
296                ctxt.no_stream = true;
297            }
298        }
299    }
300    if ctxt.state.is_none() && !ctxt.no_stream {
301        let mut sink = rusty_xml_sax::NullSax;
302        let started = {
303            let mut p = fresh_parser(&ctxt.buf, opts, &mut sink);
304            match p.parse_prolog().and_then(|()| p.open_element(NodeId::DOCUMENT)) {
305                Ok(Some(root)) => {
306                    let at = p.pos;
307                    Some((p.suspend(vec![root], false), at))
308                }
309                // `<root/>`, or not enough input yet, or a real error. All three
310                // are handled by parsing the buffer whole -- which for the empty
311                // root is correct and for an error reports it at the right time.
312                _ => None,
313            }
314        };
315        match started {
316            Some((st, at)) => {
317                ctxt.state = Some(st);
318                ctxt.buf.drain(..at);
319                ctxt.consumed += at;
320            }
321            None => {
322                if !terminate {
323                    return Ok(None);
324                }
325                return finish_whole(ctxt);
326            }
327        }
328    }
329
330    if ctxt.no_stream {
331        if !terminate {
332            return Ok(None);
333        }
334        return finish_whole(ctxt);
335    }
336
337    // Phase 2 -- content, streamed. Parse as far as the buffer allows, then
338    // release what was consumed: peak memory becomes the tree plus the
339    // unparsed tail rather than the tree plus the whole document.
340    let mut sink = rusty_xml_sax::NullSax;
341    let mut st = ctxt.state.take().expect("state is present past the prolog");
342    let mut open = std::mem::take(&mut st.open);
343    let was_closed = st.root_closed;
344    let mut p = Parser::resume(&ctxt.buf, opts, &mut sink, st);
345
346    // Once the root has closed, everything left is epilogue; re-entering the
347    // content loop would parse trailing whitespace as document content.
348    let safe = if was_closed {
349        0
350    } else {
351        match p.parse_content_inner(NodeId::DOCUMENT, &mut open, !terminate, true) {
352            Ok(at) => at,
353            Err(e) => {
354                ctxt.last_error = Some(e.clone());
355                return Err(e);
356            }
357        }
358    };
359    let root_closed = was_closed || open.is_empty();
360
361    if !terminate {
362        let at = safe.min(ctxt.buf.len());
363        ctxt.state = Some(p.suspend(open, root_closed));
364        ctxt.buf.drain(..at);
365        ctxt.buf.shrink_to_fit();
366        ctxt.consumed += at;
367        return Ok(None);
368    }
369
370    if let Some(o) = open.last() {
371        let (_, local) = Parser::split_qname(&o.qname).unwrap_or((None, &o.qname));
372        let e = p.err(
373            XML_ERR_TAG_NOT_FINISHED,
374            format!("Premature end of data in tag {local}"),
375        );
376        ctxt.last_error = Some(e.clone());
377        return Err(e);
378    }
379
380    if let Err(e) = p.parse_epilog() {
381        ctxt.last_error = Some(e.clone());
382        return Err(e);
383    }
384    let total = ctxt.consumed + ctxt.buf.len();
385    let mut doc = p.suspend(open, true).doc;
386    apply_dtd_defaults(&mut doc, total, ctxt.options)?;
387    normalize_tokenized_attrs(&mut doc);
388    ctxt.buf = Vec::new();
389    ctxt.buf.shrink_to_fit();
390    ctxt.last_error = None;
391    Ok(Some(doc))
392}
393
394/// A parser over a whole buffer, configured exactly as `parse_utf8` does.
395fn fresh_parser<'a>(
396    input: &'a [u8],
397    options: i32,
398    sax: &'a mut dyn SaxHandler,
399) -> Parser<'a> {
400    Parser {
401        input,
402        pos: 0,
403        line: 1,
404        col: 1,
405        options,
406        old10: (options & XML_PARSE_OLD10) != 0,
407        depth: 0,
408        ns_stack: Vec::new(),
409        sax,
410        doc: XmlDoc::with_node_capacity(
411            Some("1.0"),
412            if (options & XML_PARSE_NO_TREE) != 0 {
413                input.len() / 32
414            } else {
415                input.len() / 10
416            },
417        ),
418        stack: Vec::new(),
419        char_buf: String::new(),
420        char_buf_from_reference: false,
421        no_tree: (options & XML_PARSE_NO_TREE) != 0,
422        recover: (options & XML_PARSE_RECOVER) != 0,
423        // libxml2 bounds entity amplification at a small multiple of the input
424        // for the same reason; without a bound, nesting is a bomb.
425        entity_budget: input.len().saturating_mul(10).max(1 << 16),
426        scratch_raw: Vec::new(),
427        scratch_sax: Vec::new(),
428        started: false,
429    }
430}
431
432/// Parse the accumulated buffer as one whole document.
433fn finish_whole(ctxt: &mut XmlPushParserCtxt) -> Result<Option<XmlDoc>, XmlError> {
434    match xml_read_memory(
435        &ctxt.buf,
436        ctxt.url.as_deref(),
437        ctxt.encoding.as_deref(),
438        ctxt.options,
439    ) {
440        Ok(doc) => {
441            ctxt.buf = Vec::new();
442            ctxt.buf.shrink_to_fit();
443            ctxt.last_error = None;
444            Ok(Some(doc))
445        }
446        Err(e) => {
447            ctxt.last_error = Some(e.clone());
448            Err(e)
449        }
450    }
451}
452
453/// `xmlReadIO` — caller-supplied read callback, no network.
454#[doc(alias = "xmlReadIO")]
455pub fn xml_read_io<F>(
456    mut read: F,
457    url: Option<&str>,
458    encoding: Option<&str>,
459    options: i32,
460) -> Result<XmlDoc, XmlError>
461where
462    F: FnMut(&mut [u8]) -> Result<usize, std::io::Error>,
463{
464    let mut buf = Vec::new();
465    let mut tmp = [0u8; 4096];
466    loop {
467        let n = read(&mut tmp).map_err(|e| XmlError::new(XML_ERR_DOCUMENT_START, e.to_string(), 0, 0))?;
468        if n == 0 {
469            break;
470        }
471        buf.extend_from_slice(&tmp[..n]);
472    }
473    xml_read_memory(&buf, url, encoding, options)
474}
475
476/// `xmlCtxtReset`.
477#[doc(alias = "xmlCtxtReset")]
478pub fn xml_ctxt_reset(ctxt: &mut XmlParserCtxt) {
479    ctxt.doc = None;
480    ctxt.last_error = None;
481}
482
483/// Parser state that survives between push chunks.
484///
485/// Everything the parser needs to carry across a chunk boundary is owned data,
486/// which is why streaming is possible at all: the descent lives in `open`, not
487/// on the call stack.
488struct PushState {
489    doc: XmlDoc,
490    ns_stack: Vec<Vec<(Option<String>, String)>>,
491    stack: Vec<NodeId>,
492    open: Vec<OpenElem>,
493    char_buf: String,
494    line: u32,
495    col: u32,
496    depth: u32,
497    /// The root's end tag has been consumed. Without this, a later chunk would
498    /// re-enter the content loop with an empty stack and parse the document's
499    /// trailing whitespace as content, adding a stray text node.
500    root_closed: bool,
501}
502
503/// An element whose start tag has been consumed and whose end tag has not.
504struct OpenElem {
505    /// The QName exactly as written, for the end-tag comparison.
506    qname: String,
507    elem: NodeId,
508}
509
510/// One attribute exactly as it was scanned, before namespaces are resolved.
511struct RawAttr {
512    qname: String,
513    value: String,
514    value_off: usize,
515    /// Byte index of the QName's colon, resolved once at scan time.
516    colon: Option<usize>,
517}
518
519impl RawAttr {
520    fn parts(&self) -> (Option<&str>, &str) {
521        match self.colon {
522            None => (None, self.qname.as_str()),
523            Some(i) => (Some(&self.qname[..i]), &self.qname[i + 1..]),
524        }
525    }
526}
527
528struct Parser<'a> {
529    input: &'a [u8],
530    pos: usize,
531    line: u32,
532    col: u32,
533    options: i32,
534    old10: bool,
535    depth: u32,
536    ns_stack: Vec<Vec<(Option<String>, String)>>,
537    sax: &'a mut dyn SaxHandler,
538    doc: XmlDoc,
539    stack: Vec<NodeId>,
540    char_buf: String,
541    scratch_raw: Vec<RawAttr>,
542    scratch_sax: Vec<SaxAttr>,
543    started: bool,
544    no_tree: bool,
545    recover: bool,
546    /// Whether anything now in `char_buf` arrived by way of a character or
547    /// entity reference. Referenced text is never ignorable whitespace, and
548    /// the text itself cannot say so.
549    char_buf_from_reference: bool,
550    /// Bytes of entity expansion still permitted. Expanding nested entities
551    /// creates the billion-laughs vector, so it is bounded from the start.
552    entity_budget: usize,
553}
554
555impl<'a> Parser<'a> {
556    /// A name carrying a colon that does not form a QName is legal XML but a
557    /// namespace error, and C reports it as "Failed to parse QName".
558    fn check_qname(&mut self, name: &str) {
559        let mut it = name.split(':');
560        let a = it.next().unwrap_or("");
561        if let Some(b) = it.next() {
562            if it.next().is_some() || a.is_empty() || b.is_empty() {
563                let msg = format!("Failed to parse QName '{name}'");
564                self.ns_error(&msg);
565            }
566        }
567    }
568
569    /// Report a namespace error without failing the parse.
570    ///
571    /// Namespace violations are not well-formedness errors. C logs them and
572    /// carries on, and a caller that cares can read `doc.namespace_errors`.
573    fn ns_error(&mut self, msg: &str) {
574        self.sax.error(msg);
575        self.doc.namespace_errors.push(msg.to_string());
576    }
577
578    fn err(&self, code: i32, msg: impl Into<String>) -> XmlError {
579        XmlError::new(code, msg, self.line, self.col)
580    }
581
582    fn eof(&self) -> bool {
583        self.pos >= self.input.len()
584    }
585
586    fn peek_byte(&self) -> Option<u8> {
587        self.input.get(self.pos).copied()
588    }
589
590    fn starts_with(&self, s: &[u8]) -> bool {
591        self.input[self.pos..].starts_with(s)
592    }
593
594    fn bump_byte(&mut self) -> Option<u8> {
595        let b = self.peek_byte()?;
596        self.pos += 1;
597        if b == b'\n' {
598            self.line += 1;
599            self.col = 1;
600        } else {
601            self.col += 1;
602        }
603        Some(b)
604    }
605
606    /// Next Unicode scalar with XML 1.0 §2.11 EOL: `\r\n` / `\r` → `\n`.
607    fn peek_char(&self) -> Result<Option<char>, XmlError> {
608        // One bounds-checked load covers the end test and the byte fetch; the
609        // previous form did eof(), then re-sliced, then indexed.
610        let Some(&b0) = self.input.get(self.pos) else {
611            return Ok(None);
612        };
613        if b0 == b'\r' {
614            return Ok(Some('\n'));
615        }
616        if b0 < 0x80 {
617            return Ok(Some(b0 as char));
618        }
619        let rest = &self.input[self.pos..];
620        // A UTF-8 scalar is at most 4 bytes, so the leading one is always complete
621        // within the first 4. Validating only those keeps this O(1); validating the
622        // whole tail made a parse O(n^2) in the document length.
623        let head = &rest[..rest.len().min(4)];
624        match std::str::from_utf8(head) {
625            Ok(s) => Ok(s.chars().next()),
626            // The leading scalar decoded; the error belongs to a later one, which
627            // this call is not responsible for reporting.
628            Err(e) if e.valid_up_to() > 0 => Ok(std::str::from_utf8(&head[..e.valid_up_to()])
629                .ok()
630                .and_then(|s| s.chars().next())),
631            Err(_) => Err(XmlError::new(
632                XML_ERR_INVALID_CHAR,
633                "Invalid UTF-8",
634                self.line,
635                self.col,
636            )),
637        }
638    }
639
640    fn bump_char(&mut self) -> Result<Option<char>, XmlError> {
641        // ASCII and CR are handled without a decode and without the second
642        // peek_byte the CR test used to cost on every character.
643        match self.input.get(self.pos) {
644            None => return Ok(None),
645            Some(&b) if b == b'\r' => {
646                self.pos += 1;
647                self.col += 1;
648                if self.input.get(self.pos) == Some(&b'\n') {
649                    self.pos += 1;
650                    self.line += 1;
651                    self.col = 1;
652                }
653                return Ok(Some('\n'));
654            }
655            Some(&b) if b < 0x80 => {
656                self.pos += 1;
657                if b == b'\n' {
658                    self.line += 1;
659                    self.col = 1;
660                } else {
661                    self.col += 1;
662                }
663                return Ok(Some(b as char));
664            }
665            _ => {}
666        }
667        let c = match self.peek_char()? {
668            None => return Ok(None),
669            Some(c) => c,
670        };
671        // Advance the whole scalar at once. The byte-at-a-time loop re-ran a
672        // bounds-checked load and a newline test for every continuation byte,
673        // none of which can be a newline.
674        let n = c.len_utf8();
675        self.pos += n;
676        if c as u32 == 0x0A {
677            self.line += 1;
678            self.col = 1;
679        } else {
680            // The byte-at-a-time loop this replaces advanced col once per byte,
681            // so keep col in bytes or error positions shift on non-ASCII lines.
682            self.col += n as u32;
683        }
684        Ok(Some(c))
685    }
686
687    /// Consume required whitespace, reporting whether any was there.
688    fn require_s(&mut self) -> bool {
689        let before = self.pos;
690        let _ = self.skip_s();
691        self.pos > before
692    }
693
694    fn skip_s(&mut self) -> Result<(), XmlError> {
695        // Every XML whitespace character is ASCII, so this never needs a decode.
696        // The previous form decoded each one twice (peek, then bump).
697        while let Some(b) = self.peek_byte() {
698            if b >= 0x80 || !crate::chvalid::xml_is_blank(b as u32) {
699                break;
700            }
701            self.bump_byte();
702        }
703        Ok(())
704    }
705
706    fn expect_byte(&mut self, b: u8, code: i32, msg: &str) -> Result<(), XmlError> {
707        if self.peek_byte() != Some(b) {
708            return Err(self.err(code, msg));
709        }
710        self.bump_byte();
711        Ok(())
712    }
713
714    fn parse_name_span(&mut self) -> Result<(usize, usize), XmlError> {
715        // The first character went through peek_char AND bump_char -- two
716        // decodes -- for what is almost always one ASCII byte.
717        match self.input.get(self.pos) {
718            Some(&b) if b < 0x80 && b != b'\r' => {
719                if !xml_is_name_start_char(b as u32, self.old10) {
720                    return Err(self.err(XML_ERR_NAME_REQUIRED, "Name expected"));
721                }
722            }
723            _ => {
724                let c = self
725                    .peek_char()?
726                    .ok_or_else(|| self.err(XML_ERR_NAME_REQUIRED, "Name expected"))?;
727                if !xml_is_name_start_char(c as u32, self.old10) {
728                    return Err(self.err(XML_ERR_NAME_REQUIRED, "Name expected"));
729                }
730            }
731        }
732        // Scan the name in place and copy it out once. Name characters are
733        // overwhelmingly ASCII, and an ASCII byte needs no decode at all -- the
734        // char-at-a-time form decoded every character twice (peek, then bump)
735        // and grew the String one push at a time.
736        let start = self.pos;
737        self.bump_char()?;
738        loop {
739            let Some(b) = self.peek_byte() else { break };
740            if b < 0x80 {
741                if !xml_is_name_char(b as u32, self.old10) {
742                    break;
743                }
744                if self.pos - start >= MAX_NAME && (self.options & XML_PARSE_HUGE) == 0 {
745                    return Err(self.err(XML_ERR_NAME_REQUIRED, "Name too long"));
746                }
747                self.bump_byte();
748            } else {
749                let Some(c) = self.peek_char()? else { break };
750                if !xml_is_name_char(c as u32, self.old10) {
751                    break;
752                }
753                if self.pos - start >= MAX_NAME && (self.options & XML_PARSE_HUGE) == 0 {
754                    return Err(self.err(XML_ERR_NAME_REQUIRED, "Name too long"));
755                }
756                self.bump_char()?;
757            }
758        }
759        // Every byte in the span was accepted as part of a decoded character,
760        // so this is valid UTF-8; validate anyway rather than reach for unsafe.
761        Ok((start, self.pos))
762    }
763
764    /// The owning form. Prefer [`Parser::parse_name_span`] where the name is
765    /// only compared -- an end tag allocated a String purely to discard it.
766    fn parse_name(&mut self) -> Result<String, XmlError> {
767        let (a, b) = self.parse_name_span()?;
768        match std::str::from_utf8(&self.input[a..b]) {
769            Ok(name) => Ok(name.to_string()),
770            Err(_) => Err(self.err(XML_ERR_INVALID_CHAR, "Invalid UTF-8")),
771        }
772    }
773
774    fn split_qname(name: &str) -> Result<(Option<&str>, &str), XmlError> {
775        let mut parts = name.split(':');
776        let a = parts.next().unwrap();
777        match parts.next() {
778            None => Ok((None, a)),
779            Some(b) => {
780                // A colon that does not form a QName is not a prefix marker --
781                // it is just a colon, and the colon is a perfectly ordinary
782                // XML 1.0 name character. `:`, `:x`, `x:` and `a.-:x` are all
783                // legal Names; we were rejecting the documents that use them,
784                // and the suite has whole tests of exactly that shape.
785                //
786                // libxml2 does the same: it reports the namespace problem and
787                // treats the whole thing as an unprefixed name.
788                if parts.next().is_some() || a.is_empty() || b.is_empty() {
789                    return Ok((None, name));
790                }
791                Ok((Some(a), b))
792            }
793        }
794    }
795
796    fn lookup_ns(&self, prefix: Option<&str>) -> Option<String> {
797        if prefix == Some("xml") {
798            return Some(XML_NS.into());
799        }
800        if prefix == Some("xmlns") {
801            return Some(XMLNS_NS.into());
802        }
803        for frame in self.ns_stack.iter().rev() {
804            for (p, uri) in frame.iter().rev() {
805                if p.as_deref() == prefix {
806                    return Some(uri.clone());
807                }
808            }
809        }
810        None
811    }
812
813    fn uri_has_scheme(uri: &str) -> bool {
814        let bytes = uri.as_bytes();
815        if bytes.is_empty() {
816            return false;
817        }
818        if !bytes[0].is_ascii_alphabetic() {
819            return false;
820        }
821        let mut i = 1;
822        while i < bytes.len() {
823            let b = bytes[i];
824            if b == b':' {
825                return true;
826            }
827            if b.is_ascii_alphanumeric() || b == b'+' || b == b'-' || b == b'.' {
828                i += 1;
829            } else {
830                return false;
831            }
832        }
833        false
834    }
835
836    fn flush_chars(&mut self, parent: Option<NodeId>) -> Result<(), XmlError> {
837        if self.char_buf.is_empty() {
838            return Ok(());
839        }
840        if self.char_buf.len() > MAX_TEXT && (self.options & XML_PARSE_HUGE) == 0 {
841            return Err(self.err(XML_ERR_INVALID_CHAR, "Text too long"));
842        }
843        let skip_blank = (self.options & XML_PARSE_NOBLANKS) != 0
844            && self.char_buf.chars().all(|c| crate::chvalid::xml_is_blank(c as u32));
845        if !skip_blank {
846            self.sax.characters(&self.char_buf);
847            if let Some(p) = parent.filter(|_| !self.no_tree) {
848                let t = self.doc.alloc_unnamed(NodeKind::Text);
849                // Moved, not copied: the buffer is cleared immediately after,
850                // so the clone was a pure allocation plus memcpy per text node.
851                self.doc.node_mut(t).content = std::mem::take(&mut self.char_buf);
852                self.doc.xml_add_child(p, t);
853                if self.char_buf_from_reference {
854                    self.doc.reference_text.insert(t);
855                }
856            }
857        }
858        self.char_buf.clear();
859        self.char_buf_from_reference = false;
860        Ok(())
861    }
862
863    fn parse_comment(&mut self, parent: Option<NodeId>) -> Result<(), XmlError> {
864        // called after seeing "<!--"
865        let mut body = String::new();
866        loop {
867            if self.starts_with(b"-->") {
868                self.pos += 3;
869                self.col += 3;
870                break;
871            }
872            if self.eof() {
873                return Err(self.err(XML_ERR_COMMENT_NOT_FINISHED, "Comment not finished"));
874            }
875            if self.starts_with(b"--") {
876                return Err(self.err(XML_ERR_HYPHEN_IN_COMMENT, "Double hyphen in comment"));
877            }
878            let c = self.bump_char()?.unwrap();
879            if !xml_is_char(c as u32) {
880                return Err(self.err(XML_ERR_INVALID_CHAR, "Invalid character"));
881            }
882            body.push(c);
883        }
884        self.sax.comment(&body);
885        if let Some(p) = parent.filter(|_| !self.no_tree) {
886            let n = self.doc.alloc_unnamed(NodeKind::Comment);
887            self.doc.node_mut(n).content = body;
888            self.doc.xml_add_child(p, n);
889        }
890        Ok(())
891    }
892
893    fn parse_pi(&mut self, parent: Option<NodeId>, xml_decl_ok: bool) -> Result<bool, XmlError> {
894        // called after seeing "<?"
895        let target = self.parse_name()?;
896        if target.eq_ignore_ascii_case("xml") {
897            if xml_decl_ok {
898                return self.parse_xml_decl_rest().map(|_| true);
899            }
900            return Err(self.err(XML_ERR_RESERVED_XML_NAME, "Reserved PI target xml"));
901        }
902        // Namespaces in XML reserves the colon for QNames, so a PI target
903        // should be an NCName -- but C reports this and carries on, and
904        // rejecting a document libxml2 accepts is a worse trade than the three
905        // conformance cases it would win.
906        if target.contains(':') {
907            let msg = format!("colons are forbidden from PI names '{target}'");
908            self.ns_error(&msg);
909        }
910        let data = if matches!(self.peek_byte(), Some(b) if b < 0x80 && crate::chvalid::xml_is_blank(b as u32)) {
911            self.skip_s()?;
912            let mut d = String::new();
913            loop {
914                if self.starts_with(b"?>") {
915                    self.pos += 2;
916                    self.col += 2;
917                    break;
918                }
919                if self.eof() {
920                    return Err(self.err(XML_ERR_PI_NOT_FINISHED, "PI not finished"));
921                }
922                let c = self.bump_char()?.unwrap();
923                // The character rule applies inside a PI too. A form feed in
924                // one was accepted; C stops at it.
925                if !xml_is_char(c as u32) {
926                    return Err(self.err(XML_ERR_INVALID_CHAR, "Invalid character in PI"));
927                }
928                d.push(c);
929            }
930            Some(d)
931        } else {
932            if !self.starts_with(b"?>") {
933                return Err(self.err(XML_ERR_PI_NOT_FINISHED, "PI not finished"));
934            }
935            self.pos += 2;
936            self.col += 2;
937            None
938        };
939        self.sax.processing_instruction(&target, data.as_deref());
940        if let Some(p) = parent.filter(|_| !self.no_tree) {
941            let n = self.doc.alloc(NodeKind::Pi, target);
942            self.doc.node_mut(n).content = data.unwrap_or_default();
943            self.doc.xml_add_child(p, n);
944        }
945        Ok(false)
946    }
947
948    fn parse_xml_decl_rest(&mut self) -> Result<(), XmlError> {
949        self.skip_s()?;
950        // version
951        if !self.starts_with(b"version") {
952            return Err(self.err(XML_ERR_XMLDECL_NOT_FINISHED, "XML declaration version required"));
953        }
954        self.pos += 7;
955        self.col += 7;
956        self.skip_s()?;
957        self.expect_byte(b'=', XML_ERR_EQUAL_REQUIRED, "'=' required")?;
958        self.skip_s()?;
959        let ver = self.parse_quoted()?;
960        // VersionNum ::= '1.' [0-9]+ in 1.0 5th ed; libxml2 accepts the older
961        // [a-zA-Z0-9_.:-]+ form. Either way `1.0?` is not one.
962        if ver.is_empty()
963            || !ver
964                .chars()
965                .all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '.' | ':' | '-'))
966        {
967            return Err(self.err(XML_ERR_XMLDECL_NOT_FINISHED, "Invalid XML version value"));
968        }
969        self.doc.version = ver;
970        // S is required between the version info and whatever follows it;
971        // `version="1.0"encoding="UTF-8"` was accepted.
972        let had_s = self.require_s();
973        if self.starts_with(b"encoding") {
974            if !had_s {
975                return Err(self.err(XML_ERR_SPACE_REQUIRED, "Blank needed here"));
976            }
977            self.pos += 8;
978            self.col += 8;
979            self.skip_s()?;
980            self.expect_byte(b'=', XML_ERR_EQUAL_REQUIRED, "'=' required")?;
981            self.skip_s()?;
982            let enc = self.parse_quoted()?;
983            // EncName ::= [A-Za-z] ([A-Za-z0-9._] | '-')*
984            // Any string at all was accepted, including "_UTF-8" and "".
985            let mut cs = enc.chars();
986            let ok = cs.next().is_some_and(|c| c.is_ascii_alphabetic())
987                && cs.all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-'));
988            if !ok {
989                return Err(self.err(XML_ERR_ENCODING_NAME, "Invalid XML encoding name"));
990            }
991            self.doc.encoding = Some(enc);
992            self.skip_s()?;
993        }
994        if self.starts_with(b"standalone") {
995            if !had_s {
996                return Err(self.err(XML_ERR_SPACE_REQUIRED, "Blank needed here"));
997            }
998            self.pos += 10;
999            self.col += 10;
1000            self.skip_s()?;
1001            self.expect_byte(b'=', XML_ERR_EQUAL_REQUIRED, "'=' required")?;
1002            self.skip_s()?;
1003            let st = self.parse_quoted()?;
1004            self.doc.standalone = match st.as_str() {
1005                "yes" => Some(true),
1006                "no" => Some(false),
1007                _ => return Err(self.err(XML_ERR_XMLDECL_NOT_FINISHED, "standalone must be yes or no")),
1008            };
1009            self.skip_s()?;
1010        }
1011        if !self.starts_with(b"?>") {
1012            return Err(self.err(XML_ERR_XMLDECL_NOT_FINISHED, "XML declaration not finished"));
1013        }
1014        self.pos += 2;
1015        self.col += 2;
1016        Ok(())
1017    }
1018
1019    fn parse_quoted(&mut self) -> Result<String, XmlError> {
1020        let q = self.peek_byte().ok_or_else(|| self.err(XML_ERR_LITERAL_NOT_FINISHED, "Quote expected"))?;
1021        if q != b'\'' && q != b'"' {
1022            return Err(self.err(XML_ERR_LITERAL_NOT_FINISHED, "Quote expected"));
1023        }
1024        self.bump_byte();
1025        let mut s = String::new();
1026        loop {
1027            let c = self.bump_char()?.ok_or_else(|| self.err(XML_ERR_LITERAL_NOT_FINISHED, "Unterminated literal"))?;
1028            if c as u8 == q && c.is_ascii() {
1029                break;
1030            }
1031            // The shared literal reader: ATTLIST defaults, entity values,
1032            // system and public identifiers, and the XML declaration all come
1033            // through here, and none of them validated. A control byte in an
1034            // ATTLIST default was injected into every element that took the
1035            // default and written back as U+FFFD; C says "invalid character in
1036            // entity value" and stops.
1037            if !xml_is_char(c as u32) {
1038                return Err(self.err(XML_ERR_INVALID_CHAR, "invalid character in literal"));
1039            }
1040            s.push(c);
1041        }
1042        Ok(s)
1043    }
1044
1045    fn parse_cdata(&mut self, parent: Option<NodeId>) -> Result<(), XmlError> {
1046        // after "<![CDATA["
1047        let mut body = String::new();
1048        loop {
1049            if self.starts_with(b"]]>") {
1050                self.pos += 3;
1051                self.col += 3;
1052                break;
1053            }
1054            if self.eof() {
1055                return Err(self.err(XML_ERR_CDATA_NOT_FINISHED, "CDATA not finished"));
1056            }
1057            let c = self.bump_char()?.unwrap();
1058            // CDATA is unparsed, not unchecked: the character rule still
1059            // applies inside it.
1060            if !xml_is_char(c as u32) {
1061                return Err(self.err(XML_ERR_INVALID_CHAR, "invalid character in CDATA"));
1062            }
1063            body.push(c);
1064        }
1065        if (self.options & XML_PARSE_NOCDATA) != 0 {
1066            self.sax.characters(&body);
1067            if let Some(p) = parent.filter(|_| !self.no_tree) {
1068                let t = self.doc.alloc_unnamed(NodeKind::Text);
1069                self.doc.node_mut(t).content = body;
1070                self.doc.xml_add_child(p, t);
1071            }
1072        } else {
1073            self.sax.cdata_block(&body);
1074            if let Some(p) = parent.filter(|_| !self.no_tree) {
1075                let t = self.doc.alloc_unnamed(NodeKind::CData);
1076                self.doc.node_mut(t).content = body;
1077                self.doc.xml_add_child(p, t);
1078            }
1079        }
1080        Ok(())
1081    }
1082
1083    fn parse_reference(&mut self) -> Result<String, XmlError> {
1084        self.expect_byte(b'&', XML_ERR_ENTITYREF_NO_NAME, "& expected")?;
1085        if self.peek_byte() == Some(b'#') {
1086            self.bump_byte();
1087            // CharRef ::= '&#' [0-9]+ ';' | '&#x' [0-9a-fA-F]+ ';'
1088            // The marker is lowercase only; `&#X58;` is not a character
1089            // reference, and we were accepting it.
1090            let hex = self.peek_byte() == Some(b'x');
1091            if hex {
1092                self.bump_byte();
1093            } else if self.peek_byte() == Some(b'X') {
1094                return Err(self.err(
1095                    XML_ERR_INVALID_DEC_CHARREF,
1096                    "CharRef: invalid decimal value",
1097                ));
1098            }
1099            let mut digits = String::new();
1100            while let Some(b) = self.peek_byte() {
1101                let ok = if hex {
1102                    b.is_ascii_hexdigit()
1103                } else {
1104                    b.is_ascii_digit()
1105                };
1106                if !ok {
1107                    break;
1108                }
1109                digits.push(b as char);
1110                self.bump_byte();
1111            }
1112            if digits.is_empty() {
1113                return Err(self.err(
1114                    if hex { XML_ERR_INVALID_HEX_CHARREF } else { XML_ERR_INVALID_DEC_CHARREF },
1115                    "Invalid character reference",
1116                ));
1117            }
1118            self.expect_byte(b';', XML_ERR_ENTITYREF_SEMICOL_MISSING, "';' required")?;
1119            let val = if hex {
1120                u32::from_str_radix(&digits, 16).map_err(|_| {
1121                    self.err(XML_ERR_INVALID_HEX_CHARREF, "Invalid hex charref")
1122                })?
1123            } else {
1124                digits.parse::<u32>().map_err(|_| {
1125                    self.err(XML_ERR_INVALID_DEC_CHARREF, "Invalid decimal charref")
1126                })?
1127            };
1128            if !xml_is_char(val) {
1129                return Err(self.err(XML_ERR_INVALID_CHARREF, "Invalid character reference"));
1130            }
1131            return Ok(char::from_u32(val).unwrap().to_string());
1132        }
1133        let name = self.parse_name()?;
1134        self.expect_byte(b';', XML_ERR_ENTITYREF_SEMICOL_MISSING, "';' required")?;
1135        match name.as_str() {
1136            "lt" => Ok("<".into()),
1137            "gt" => Ok(">".into()),
1138            "amp" => Ok("&".into()),
1139            "apos" => Ok("'".into()),
1140            "quot" => Ok("\"".into()),
1141            _ => {
1142                let raw = self
1143                    .doc
1144                    .dtd
1145                    .as_ref()
1146                    .and_then(|d| d.entities.get(&name))
1147                    .cloned();
1148                if let Some(raw) = raw {
1149                    // The replacement was returned VERBATIM, so a nested
1150                    // reference landed in the tree as literal text and came
1151                    // back out escaped: `&b;&b;` became `&amp;b;&amp;b;`.
1152                    return self.expand_entity(&name, &raw, 0);
1153                }
1154                // XML 1.0 4.1: when the subset used a parameter entity
1155                // reference the declarations may be incomplete, so an
1156                // unresolvable entity is a VALIDITY error rather than a
1157                // well-formedness one. Killing the parse there refused
1158                // documents libxml2 reads, and the reference is recorded so
1159                // the validator can still report it.
1160                let subset_incomplete = self
1161                    .doc
1162                    .dtd
1163                    .as_ref()
1164                    .is_some_and(|d| d.has_parameter_entity_refs);
1165                if self.recover || subset_incomplete {
1166                    self.sax
1167                        .error(&format!("Entity '{name}' not defined"));
1168                    self.doc.undeclared_entity_refs.push(name.clone());
1169                    return Ok(format!("&{name};"));
1170                }
1171                Err(self.err(
1172                    XML_ERR_UNDECLARED_ENTITY,
1173                    format!("Entity '{name}' not defined"),
1174                ))
1175            }
1176        }
1177    }
1178
1179    /// The declared replacement text of the reference at the cursor, as
1180    /// STORED -- character references already expanded, entity references
1181    /// still written out.
1182    ///
1183    /// That distinction is the whole point. A character reference in an entity
1184    /// value is expanded when the declaration is read, so `<!ENTITY e
1185    /// "&#60;foo/>">` really does hold a '<' and really is markup. `&lt;` is
1186    /// bypassed and stays written out, so `<!ENTITY e "&lt;AB&gt;">` holds no
1187    /// markup at all and must come out as the three characters `<AB>`.
1188    /// Deciding on the fully expanded text cannot tell those apart.
1189    fn reference_raw_value(&self) -> Option<String> {
1190        let rest = self.input.get((self.pos + 1).min(self.input.len())..)?;
1191        let end = rest.iter().position(|b| *b == b';')?;
1192        let name = std::str::from_utf8(&rest[..end]).ok()?;
1193        self.doc.dtd.as_ref()?.entities.get(name).cloned()
1194    }
1195
1196    /// Step over `&name;` without expanding it.
1197    fn consume_reference(&mut self) -> Result<(), XmlError> {
1198        self.expect_byte(b'&', XML_ERR_ENTITYREF_NO_NAME, "& expected")?;
1199        let _ = self.parse_name()?;
1200        self.expect_byte(b';', XML_ERR_ENTITYREF_SEMICOL_MISSING, "';' required")?;
1201        Ok(())
1202    }
1203
1204    /// Parse an entity's replacement text as content and graft the result in.
1205    ///
1206    /// The replacement is parsed in isolation, wrapped in a synthetic root, so
1207    /// its well-formedness is checked as the "Well-Formed Parsed Entity"
1208    /// constraint requires -- a bare `&` or `<` arriving by way of a character
1209    /// reference in the declaration is an error, not text.
1210    ///
1211    /// A prefix declared on the REFERENCING element is not in scope for an
1212    /// isolated parse, so an undefined-prefix failure falls back to the old
1213    /// text behaviour rather than rejecting a document libxml2 accepts.
1214    fn splice_entity(&mut self, repl: &str, parent: NodeId) -> Result<(), XmlError> {
1215        // The splice path bypasses expand_entity, which is where the
1216        // amplification budget was charged -- so a bomb whose replacement
1217        // contains '&' stopped being charged at all and expanded freely. It is
1218        // charged here instead, and the sub-parser inherits and returns what
1219        // is left so nesting draws on one pool.
1220        self.entity_budget = match self.entity_budget.checked_sub(repl.len()) {
1221            Some(n) => n,
1222            None => {
1223                return Err(self.err(
1224                    XML_ERR_UNDECLARED_ENTITY,
1225                    "Entity expansion budget exceeded",
1226                ));
1227            }
1228        };
1229        let wrapped = format!("<rusty-xml-entity>{repl}</rusty-xml-entity>");
1230        let mut null = rusty_xml_sax::NullSax;
1231        let mut sub = Parser {
1232            input: wrapped.as_bytes(),
1233            pos: 0,
1234            line: self.line,
1235            col: self.col,
1236            options: self.options,
1237            old10: self.old10,
1238            depth: self.depth + 1,
1239            ns_stack: Vec::new(),
1240            sax: &mut null,
1241            doc: XmlDoc::with_node_capacity(Some("1.0"), 8),
1242            stack: Vec::new(),
1243            char_buf: String::new(),
1244        char_buf_from_reference: false,
1245            scratch_raw: Vec::new(),
1246            scratch_sax: Vec::new(),
1247            started: false,
1248            no_tree: false,
1249            recover: self.recover,
1250            // The nested expansion draws on the SAME budget, so an entity that
1251            // splices markup cannot buy itself a fresh allowance.
1252            entity_budget: self.entity_budget,
1253        };
1254        sub.doc.dtd = self.doc.dtd.clone();
1255        let r = sub.parse_document();
1256        self.entity_budget = sub.entity_budget;
1257        match r {
1258            Ok(()) => {
1259                if let Some(root) = sub.doc.xml_doc_get_root_element() {
1260                    self.doc.xml_copy_children_from(&sub.doc, root, parent);
1261                }
1262                Ok(())
1263            }
1264            Err(e) if e.code == XML_NS_ERR_UNDEFINED_NAMESPACE => {
1265                self.char_buf.push_str(repl);
1266                self.flush_chars(Some(parent))
1267            }
1268            Err(e) => Err(e),
1269        }
1270    }
1271
1272    /// Expand an entity's replacement text, resolving references inside it.
1273    ///
1274    /// Bounded twice, because recursion here IS the billion-laughs vector: by
1275    /// nesting depth, and by a byte budget proportional to the document.
1276    fn expand_entity(&mut self, name: &str, raw: &str, depth: u32) -> Result<String, XmlError> {
1277        const MAX_ENTITY_DEPTH: u32 = 40;
1278        if depth > MAX_ENTITY_DEPTH {
1279            return Err(self.err(
1280                XML_ERR_UNDECLARED_ENTITY,
1281                format!("Entity '{name}' nested too deeply"),
1282            ));
1283        }
1284        let b = raw.as_bytes();
1285        let mut out = String::with_capacity(raw.len());
1286        let mut i = 0usize;
1287        while i < b.len() {
1288            if b[i] != b'&' {
1289                let start = i;
1290                while i < b.len() && b[i] != b'&' {
1291                    i += 1;
1292                }
1293                out.push_str(&raw[start..i]);
1294                continue;
1295            }
1296            let Some(semi) = raw[i..].find(';').map(|k| i + k) else {
1297                out.push('&');
1298                i += 1;
1299                continue;
1300            };
1301            let inner = raw[i + 1..semi].to_string();
1302            if let Some(rest) = inner.strip_prefix('#') {
1303                let (radix, digits) = match rest.strip_prefix(['x', 'X']) {
1304                    Some(h) => (16u32, h),
1305                    None => (10u32, rest),
1306                };
1307                match u32::from_str_radix(digits, radix).ok().and_then(char::from_u32) {
1308                    Some(c) => out.push(c),
1309                    None => {
1310                        return Err(self.err(
1311                            XML_ERR_INVALID_CHAR,
1312                            format!("Invalid character reference in entity '{name}'"),
1313                        ))
1314                    }
1315                }
1316                i = semi + 1;
1317                continue;
1318            }
1319            let replacement: Option<String> = match inner.as_str() {
1320                "lt" => Some("<".into()),
1321                "gt" => Some(">".into()),
1322                "amp" => Some("&".into()),
1323                "apos" => Some("'".into()),
1324                "quot" => Some('"'.to_string()),
1325                other => {
1326                    let nested = self
1327                        .doc
1328                        .dtd
1329                        .as_ref()
1330                        .and_then(|d| d.entities.get(other))
1331                        .cloned();
1332                    match nested {
1333                        Some(r) => Some(self.expand_entity(other, &r, depth + 1)?),
1334                        None => None,
1335                    }
1336                }
1337            };
1338            match replacement {
1339                Some(r) => {
1340                    if r.len() > self.entity_budget {
1341                        return Err(self.err(
1342                            XML_ERR_INTERNAL_ERROR,
1343                            "Maximum entity amplification exceeded",
1344                        ));
1345                    }
1346                    self.entity_budget -= r.len();
1347                    out.push_str(&r);
1348                }
1349                None if self.recover => out.push_str(&raw[i..=semi]),
1350                None => {
1351                    return Err(self.err(
1352                        XML_ERR_UNDECLARED_ENTITY,
1353                        format!("Entity '{inner}' not defined"),
1354                    ))
1355                }
1356            }
1357            i = semi + 1;
1358        }
1359        Ok(out)
1360    }
1361
1362    fn parse_att_value(&mut self) -> Result<(String, usize), XmlError> {
1363        let q = self.peek_byte().ok_or_else(|| {
1364            self.err(XML_ERR_ATTRIBUTE_WITHOUT_VALUE, "Attribute value expected")
1365        })?;
1366        if q != b'\'' && q != b'"' {
1367            return Err(self.err(XML_ERR_ATTRIBUTE_WITHOUT_VALUE, "Attribute value expected"));
1368        }
1369        self.bump_byte();
1370        let start = self.pos;
1371        let mut val = String::new();
1372        loop {
1373            // Same run trick as character data: most attribute values are plain
1374            // ASCII with no reference and no whitespace needing normalisation.
1375            {
1376                let rs = self.pos;
1377                let mut i = rs;
1378                while i < self.input.len() {
1379                    let b = self.input[i];
1380                    if b == q || b == b'<' || b == b'&' || b < 0x20 || b >= 0x80 {
1381                        break;
1382                    }
1383                    i += 1;
1384                }
1385                if i > rs {
1386                    if let Ok(run) = std::str::from_utf8(&self.input[rs..i]) {
1387                        // Almost every value is a single run, so this is the
1388                        // exact size and the String never grows.
1389                        if val.is_empty() {
1390                            val.reserve_exact(i - rs);
1391                        }
1392                        val.push_str(run);
1393                        self.col += (i - rs) as u32;
1394                        self.pos = i;
1395                        continue;
1396                    }
1397                }
1398            }
1399            if self.peek_byte() == Some(q) {
1400                self.bump_byte();
1401                break;
1402            }
1403            if self.eof() {
1404                return Err(self.err(XML_ERR_LITERAL_NOT_FINISHED, "Unterminated attribute"));
1405            }
1406            if self.peek_byte() == Some(b'<') {
1407                return Err(self.err(XML_ERR_LT_IN_ATTRIBUTE, "'<' in attribute value"));
1408            }
1409            if self.peek_byte() == Some(b'&') {
1410                let raw = self.reference_raw_value();
1411                let repl = self.parse_reference()?;
1412                // "No < in Attribute Values": the constraint is about the
1413                // replacement TEXT, not just what is written in the document,
1414                // so an entity carrying one is caught here and nowhere else.
1415                // The EXPANDED text, not just the stored one: `<!ENTITY a
1416                // "&b;">` where b holds a '<' carries it in just the same way,
1417                // and only the expansion shows that.
1418                // Only for a DECLARED entity: `&lt;` is the sanctioned way
1419                // to put a '<' in an attribute value, and rejecting that broke
1420                // a valid document.
1421                if raw.is_some() && (raw.as_deref().unwrap().contains('<') || repl.contains('<')) {
1422                    return Err(self.err(
1423                        XML_ERR_LT_IN_ATTRIBUTE,
1424                        "'<' in entity is not allowed in attribute values",
1425                    ));
1426                }
1427                // `<!ENTITY e "&#38;">` stores a bare ampersand, and a bare
1428                // ampersand is no more legal in an attribute value than in
1429                // content -- it has to begin a reference.
1430                if raw.as_deref().is_some_and(|r| has_bare_ampersand(r)) {
1431                    return Err(self.err(
1432                        XML_ERR_ENTITYREF_NO_NAME,
1433                        "entity reference in attribute value is not well formed",
1434                    ));
1435                }
1436                val.push_str(&repl);
1437                continue;
1438            }
1439            let c = self.bump_char()?.unwrap();
1440            // Character data is validated; attribute values were not, so a
1441            // stray C0 control byte sailed straight through and the writer
1442            // quietly substituted U+FFFD for it on the way out -- a silently
1443            // corrupted value where C reports "invalid character in attribute
1444            // value". Found by the round-trip check: escaping it on the first
1445            // save and not the second made serialization non-idempotent.
1446            if !xml_is_char(c as u32) {
1447                return Err(self.err(
1448                    XML_ERR_INVALID_CHAR,
1449                    "invalid character in attribute value",
1450                ));
1451            }
1452            // AttValue: physical whitespace → space
1453            if c == '\n' || c == '\t' {
1454                val.push(' ');
1455            } else {
1456                val.push(c);
1457            }
1458        }
1459        Ok((val, start))
1460    }
1461
1462    fn skip_doctype(&mut self) -> Result<(), XmlError> {
1463        // after "<!DOCTYPE"
1464        self.skip_s()?;
1465        let name = self.parse_name()?;
1466        self.skip_s()?;
1467        let mut public_id = None;
1468        let mut system_id = None;
1469        if self.starts_with(b"SYSTEM") {
1470            self.pos += 6;
1471            self.col += 6;
1472            if !self.require_s() {
1473                return Err(self.err(XML_ERR_SPACE_REQUIRED, "Space required after 'SYSTEM'"));
1474            }
1475            if !matches!(self.peek_byte(), Some(b'"') | Some(b'\'')) {
1476                return Err(self.err(
1477                    XML_ERR_LITERAL_NOT_FINISHED,
1478                    "SystemLiteral \" or ' expected",
1479                ));
1480            }
1481            system_id = Some(self.parse_quoted()?);
1482        } else if self.starts_with(b"PUBLIC") {
1483            self.pos += 6;
1484            self.col += 6;
1485            if !self.require_s() {
1486                return Err(self.err(XML_ERR_SPACE_REQUIRED, "Space required after 'PUBLIC'"));
1487            }
1488            let pid = self.parse_quoted()?;
1489            // PubidLiteral is a restricted character set, not free text.
1490            if let Some(bad) = pid.chars().find(|c| !crate::dtd::is_pubid_char(*c)) {
1491                return Err(self.err(
1492                    XML_ERR_INVALID_CHAR,
1493                    format!("Invalid character 0x{:X} in public identifier", bad as u32),
1494                ));
1495            }
1496            public_id = Some(pid);
1497            // ExternalID ::= 'PUBLIC' S PubidLiteral S SystemLiteral -- the
1498            // space between the two literals is required, and `"a""b"` was
1499            // taken happily.
1500            if !self.require_s() {
1501                return Err(self.err(
1502                    XML_ERR_SPACE_REQUIRED,
1503                    "Space required after the Public Identifier",
1504                ));
1505            }
1506            if !matches!(self.peek_byte(), Some(b'"') | Some(b'\'')) {
1507                return Err(self.err(
1508                    XML_ERR_LITERAL_NOT_FINISHED,
1509                    "SystemLiteral \" or ' expected",
1510                ));
1511            }
1512            system_id = Some(self.parse_quoted()?);
1513        }
1514        self.skip_s()?;
1515        let mut int_subset = None;
1516        if self.peek_byte() == Some(b'[') {
1517            self.bump_byte();
1518            let start = self.pos;
1519            let mut depth = 1i32;
1520            let mut in_quote: Option<u8> = None;
1521            while depth > 0 {
1522                // A comment's contents are not markup, and an apostrophe in
1523                // one is not a quote. `<!--NOTE: XML doesn't specify...-->`
1524                // opened a quote that never closed, so the scan swallowed the
1525                // rest of the document and reported "Unterminated DOCTYPE" at
1526                // the last line.
1527                if in_quote.is_none() && self.starts_with(b"<!--") {
1528                    match self.input[self.pos..]
1529                        .windows(3)
1530                        .position(|w| w == b"-->")
1531                    {
1532                        Some(off) => {
1533                            for _ in 0..off + 3 {
1534                                self.bump_byte();
1535                            }
1536                            continue;
1537                        }
1538                        None => {
1539                            return Err(
1540                                self.err(XML_ERR_COMMENT_NOT_FINISHED, "Comment not finished")
1541                            );
1542                        }
1543                    }
1544                }
1545                let b = self.bump_byte().ok_or_else(|| {
1546                    self.err(XML_ERR_DOCUMENT_END, "Unterminated DOCTYPE")
1547                })?;
1548                if let Some(q) = in_quote {
1549                    if b == q {
1550                        in_quote = None;
1551                    }
1552                    continue;
1553                }
1554                match b {
1555                    b'\'' | b'"' => in_quote = Some(b),
1556                    b'[' => depth += 1,
1557                    b']' => depth -= 1,
1558                    _ => {}
1559                }
1560            }
1561            // exclude the closing ']'
1562            int_subset = Some(String::from_utf8_lossy(&self.input[start..self.pos.saturating_sub(1)]).into_owned());
1563        }
1564        self.skip_s()?;
1565        self.expect_byte(b'>', XML_ERR_GT_REQUIRED, "'>' required")?;
1566        let mut dtd = if let Some(ref subset) = int_subset {
1567            // unwrap_or_default() here discarded EVERY internal-subset
1568            // error: a malformed DTD silently became an empty one, so the
1569            // entities and ATTLIST defaults it declared just vanished and the
1570            // failure surfaced later as a bogus "entity not defined". Recovery
1571            // mode still tolerates it, because that is what recovery is for.
1572            match crate::dtd::parse_dtd_subset(subset, self.old10) {
1573                Ok(d) => d,
1574                Err(_) if self.recover => rusty_xml_tree::XmlDtd::default(),
1575                Err(e) => return Err(e),
1576            }
1577        } else {
1578            rusty_xml_tree::XmlDtd::default()
1579        };
1580        let subset_ns_errors = std::mem::take(&mut dtd.namespace_errors);
1581        self.doc.namespace_errors.extend(subset_ns_errors);
1582        dtd.name = Some(name);
1583        dtd.public_id = public_id;
1584        dtd.system_id = system_id;
1585        dtd.int_subset = int_subset;
1586        self.doc.dtd = Some(dtd);
1587        Ok(())
1588    }
1589
1590    /// Parse a start tag and everything that belongs to it: attributes,
1591    /// namespace frame, the SAX start event and the element node.
1592    ///
1593    /// Returns the open element, or `None` if it was `<x/>` and is already
1594    /// closed. Split out of `parse_element` so the content loop can be driven
1595    /// by an explicit stack instead of by recursion.
1596    fn open_element(&mut self, parent: NodeId) -> Result<Option<OpenElem>, XmlError> {
1597        self.depth += 1;
1598        let cap = if (self.options & XML_PARSE_HUGE) != 0 {
1599            MAX_DEPTH_HUGE
1600        } else {
1601            MAX_DEPTH
1602        };
1603        if self.depth > cap {
1604            return Err(self.err(XML_ERR_INTERNAL_ERROR, "Excessive element nesting"));
1605        }
1606        self.expect_byte(b'<', XML_ERR_LT_REQUIRED, "'<' required")?;
1607        let qname = self.parse_name()?;
1608        self.check_qname(&qname);
1609        // `<xmlns:foo/>`: xmlns is reserved and is not an element prefix.
1610        if qname.starts_with("xmlns:") {
1611            self.ns_error("Elements must not have the prefix xmlns");
1612        }
1613        let (prefix, local) = Self::split_qname(&qname).map_err(|mut e| {
1614            e.line = self.line;
1615            e.col = self.col;
1616            e
1617        })?;
1618
1619        // Reused across elements: a fresh Vec per element allocated once and
1620        // then grew 1-2-4-8 as the attributes were pushed.
1621        let mut raw_attrs: Vec<RawAttr> = std::mem::take(&mut self.scratch_raw);
1622        raw_attrs.clear();
1623        loop {
1624            let before_ws = self.pos;
1625            self.skip_s()?;
1626            let had_ws = self.pos > before_ws;
1627            if self.starts_with(b"/>") || self.peek_byte() == Some(b'>') {
1628                break;
1629            }
1630            // `att1="a"att2="b"` was accepted; the grammar requires S
1631            // between attributes, and C calls it an attributes construct
1632            // error.
1633            if !had_ws {
1634                return Err(self.err(
1635                    XML_ERR_SPACE_REQUIRED,
1636                    "attributes construct error",
1637                ));
1638            }
1639            let an = self.parse_name()?;
1640            self.skip_s()?;
1641            self.expect_byte(b'=', XML_ERR_EQUAL_REQUIRED, "'=' required")?;
1642            self.skip_s()?;
1643            let (value, value_off) = self.parse_att_value()?;
1644            self.check_qname(&an);
1645            let colon = match Self::split_qname(&an).map_err(|mut e| {
1646                e.line = self.line;
1647                e.col = self.col;
1648                e
1649            })? {
1650                (None, _) => None,
1651                (Some(pfx), _) => Some(pfx.len()),
1652            };
1653            raw_attrs.push(RawAttr {
1654                qname: an,
1655                value,
1656                value_off,
1657                colon,
1658            });
1659        }
1660        let empty = if self.starts_with(b"/>") {
1661            self.pos += 2;
1662            self.col += 2;
1663            true
1664        } else {
1665            self.expect_byte(b'>', XML_ERR_GT_REQUIRED, "'>' required")?;
1666            false
1667        };
1668
1669        let mut ns_frame: Vec<(Option<String>, String)> = Vec::new();
1670        // A namespace declaration is an attribute, and if the DTD declares it
1671        // as a tokenized type its value is normalized before it means
1672        // anything. Namespace declarations never reach the attribute chain, so
1673        // the post-parse normalization pass could not see them -- and two
1674        // prefixes could bind to URIs that are equal only after normalization
1675        // without ever being noticed as equal.
1676        let normalize_ns = |p: &Parser, name: &str, value: &str| -> String {
1677            let tokenized = p.doc.dtd.as_ref().is_some_and(|d| {
1678                d.attributes
1679                    .get(&(qname.to_string(), name.to_string()))
1680                    .is_some_and(|a| a.att_type != "CDATA")
1681            });
1682            if tokenized {
1683                value.split(' ').filter(|t| !t.is_empty()).collect::<Vec<_>>().join(" ")
1684            } else {
1685                value.to_string()
1686            }
1687        };
1688        for a in &raw_attrs {
1689            let (ap, al) = a.parts();
1690            if ap.is_none() && al == "xmlns" {
1691                if !a.value.is_empty() && !Self::uri_has_scheme(&a.value) {
1692                    let msg = format!("xmlns: URI {} is not absolute\n", a.value);
1693                    self.sax.warning(&msg);
1694                }
1695                // The reserved namespaces may not be bound as the DEFAULT
1696                // either. Only the prefixed form was being checked.
1697                if a.value == XML_NS {
1698                    self.ns_error("xml namespace URI cannot be the default namespace");
1699                } else if a.value == XMLNS_NS {
1700                    self.ns_error("xmlns namespace URI cannot be the default namespace");
1701                }
1702                ns_frame.push((None, normalize_ns(self, &a.qname, &a.value)));
1703            } else if ap == Some("xmlns") {
1704                if !a.value.is_empty()
1705                    && !Self::uri_has_scheme(&a.value)
1706                    && (self.options & XML_PARSE_PEDANTIC) != 0
1707                {
1708                    let msg = format!("xmlns:{}: URI {} is not absolute\n", al, a.value);
1709                    self.sax.warning(&msg);
1710                }
1711                // Namespaces in XML 1.0 reserves `xml` and `xmlns` and
1712                // forbids undeclaring a prefix. None of it is a
1713                // WELL-FORMEDNESS error: libxml2 parses the document and logs
1714                // a namespace error, and its own conformance harness scores
1715                // these tests by requiring exactly that -- the parse must
1716                // succeed AND an error must have been reported. Returning Err
1717                // here refused documents C reads, and failed the tests it was
1718                // meant to pass.
1719                if al == "xml" {
1720                    if a.value != XML_NS {
1721                        self.ns_error("xml namespace prefix mapped to wrong URI");
1722                    }
1723                } else if a.value == XML_NS {
1724                    self.ns_error("xml namespace URI mapped to wrong prefix");
1725                }
1726                if al == "xmlns" {
1727                    self.ns_error("redefinition of the xmlns prefix is forbidden");
1728                }
1729                if a.value == XMLNS_NS {
1730                    self.ns_error("reuse of the xmlns namespace name is forbidden");
1731                }
1732                // Prefix undeclaring (`xmlns:p=""`) is XML 1.1 only.
1733                if a.value.is_empty() {
1734                    self.ns_error("Empty XML namespace is not allowed");
1735                }
1736                ns_frame.push((Some(al.to_string()), normalize_ns(self, &a.qname, &a.value)));
1737            }
1738        }
1739        // The frame is pushed, not copied; the stack owns it and both later
1740        // readers borrow it back from there.
1741        self.ns_stack.push(ns_frame);
1742
1743        let elem_uri = self.lookup_ns(prefix);
1744        if prefix.is_some() && elem_uri.is_none() {
1745            // Scraped markup is full of prefixes nobody declared, and an
1746            // undeclared prefix is a namespace error, not a well-formedness
1747            // one. libxml2 reports it and carries on -- exits zero -- so
1748            // refusing the document made us reject input C accepts, which is a
1749            // worse trade than any number of conformance cases. It also cost a
1750            // valid case outright: `<A.-:x/>` is a legal Name whose colon is
1751            // not a prefix at all.
1752            let msg = format!("Namespace prefix {} is not defined", prefix.unwrap_or_default());
1753            self.ns_error(&msg);
1754        }
1755
1756        let mut seen_keys: std::collections::HashSet<(Option<String>, String)> =
1757            std::collections::HashSet::new();
1758        let mut sax_attrs: Vec<SaxAttr> = std::mem::take(&mut self.scratch_sax);
1759        sax_attrs.clear();
1760        for idx in 0..raw_attrs.len() {
1761            // Own the parts first; SaxAttr needs them owned anyway, so this
1762            // costs nothing extra and releases the borrow on raw_attrs.
1763            let (ap_owned, al_owned, is_ns, value_off) = {
1764                let a = &mut raw_attrs[idx];
1765                let voff = a.value_off;
1766                match a.colon {
1767                    // Unprefixed: the local name IS the whole QName, so move it
1768                    // instead of allocating a second copy of the same bytes.
1769                    None => {
1770                        let is_ns = a.qname == "xmlns";
1771                        (None, std::mem::take(&mut a.qname), is_ns, voff)
1772                    }
1773                    Some(i) => {
1774                        let is_ns = &a.qname[..i] == "xmlns";
1775                        (
1776                            Some(a.qname[..i].to_string()),
1777                            a.qname[i + 1..].to_string(),
1778                            is_ns,
1779                            voff,
1780                        )
1781                    }
1782                }
1783            };
1784            if is_ns {
1785                continue;
1786            }
1787            let uri = if ap_owned.is_some() {
1788                let u = self.lookup_ns(ap_owned.as_deref());
1789                if u.is_none() {
1790                    // Non-fatal, like the element case and like C: an unbound
1791                    // prefix is a namespace error, and rejecting refused
1792                    // documents libxml2 reads.
1793                    let msg = format!(
1794                        "Namespace prefix {} for {} on ... is not defined",
1795                        ap_owned.clone().unwrap_or_default(),
1796                        al_owned
1797                    );
1798                    self.ns_error(&msg);
1799                }
1800                u
1801            } else {
1802                None
1803            };
1804            // The attributes already accepted ARE the "seen" set -- a separate
1805            // vector of copies was allocated per element to hold the same thing.
1806            // Linear over the accepted attributes is fine for the handful a real
1807            // element carries, but it is O(n^2) and an element with 16,000
1808            // attributes took 185 ms. Switch to a set once it could matter.
1809            // Two kinds of duplicate, and they are not the same error.
1810            // The same QName twice is a well-formedness violation and fatal.
1811            // Two DIFFERENT prefixes bound to one URI, with the same local
1812            // name, only collide after namespace expansion -- that is a
1813            // namespace error, which C reports and carries on from. Treating
1814            // both as fatal refused documents libxml2 reads.
1815            if sax_attrs
1816                .iter()
1817                .any(|s| s.prefix == ap_owned && s.local == al_owned)
1818            {
1819                return Err(self.err(XML_ERR_ATTRIBUTE_REDEFINED, "Attribute redefined"));
1820            }
1821            if sax_attrs.len() < 32 {
1822                if uri.is_some()
1823                    && sax_attrs
1824                        .iter()
1825                        .any(|s| s.uri.as_deref() == uri.as_deref() && s.local == al_owned)
1826                {
1827                    self.ns_error("Attribute redefined after namespace expansion");
1828                }
1829            } else {
1830                if seen_keys.is_empty() {
1831                    for a in sax_attrs.iter() {
1832                        seen_keys.insert((a.uri.clone(), a.local.clone()));
1833                    }
1834                }
1835                if !seen_keys.insert((uri.clone(), al_owned.clone())) && uri.is_some() {
1836                    self.ns_error("Attribute redefined after namespace expansion");
1837                }
1838            }
1839            sax_attrs.push(SaxAttr {
1840                local: al_owned,
1841                prefix: ap_owned,
1842                uri,
1843                // Moved out of raw_attrs rather than copied: one String clone
1844                // per attribute in the document.
1845                value: std::mem::take(&mut raw_attrs[idx].value),
1846                value_input_off: Some(value_off),
1847            });
1848        }
1849
1850        let frame: &[(Option<String>, String)] =
1851            self.ns_stack.last().map(Vec::as_slice).unwrap_or(&[]);
1852        self.sax.start_element_ns(
1853            local,
1854            prefix,
1855            elem_uri.as_deref(),
1856            frame,
1857            &sax_attrs,
1858            0,
1859        );
1860
1861        let elem = self.doc.alloc(NodeKind::Element, local);
1862        self.doc.node_mut(elem).prefix = prefix.map(str::to_string);
1863        self.doc.node_mut(elem).ns_uri = elem_uri;
1864        for i in 0..self.ns_stack.last().map_or(0, Vec::len) {
1865            let (p, u) = {
1866                let f = self.ns_stack.last().unwrap();
1867                (f[i].0.clone(), f[i].1.clone())
1868            };
1869            self.doc.push_ns_def(elem, p, u);
1870        }
1871        if self.no_tree {
1872            sax_attrs.clear();
1873        } else {
1874            for a in sax_attrs.drain(..) {
1875                let uri = a.uri;
1876                let aid = self.doc.add_attr_owned(elem, a.local, a.prefix, a.value);
1877                self.doc.node_mut(aid).ns_uri = uri;
1878            }
1879        }
1880        self.doc.xml_add_child(parent, elem);
1881
1882        raw_attrs.clear();
1883        sax_attrs.clear();
1884        self.scratch_raw = raw_attrs;
1885        self.scratch_sax = sax_attrs;
1886
1887        if empty {
1888            let uri = self.doc.node(elem).ns_uri.as_deref();
1889            self.sax.end_element_ns(local, prefix, uri);
1890            self.ns_stack.pop();
1891            self.depth -= 1;
1892            return Ok(None);
1893        }
1894
1895        self.stack.push(elem);
1896        Ok(Some(OpenElem { qname, elem }))
1897    }
1898
1899    /// Consume the end tag of an open element and emit its SAX end event.
1900    ///
1901    /// `local` and `prefix` are re-derived from the stored QName rather than
1902    /// carried across the call: `split_qname` borrows, so this allocates
1903    /// nothing.
1904    fn close_element(&mut self, open: &OpenElem) -> Result<(), XmlError> {
1905        let (prefix, local) = Self::split_qname(&open.qname).map_err(|mut e| {
1906            e.line = self.line;
1907            e.col = self.col;
1908            e
1909        })?;
1910        if !self.starts_with(b"</") {
1911            return Err(self.err(
1912                XML_ERR_TAG_NOT_FINISHED,
1913                format!("Premature end of data in tag {local}"),
1914            ));
1915        }
1916        self.pos += 2;
1917        self.col += 2;
1918        let (ea, eb) = self.parse_name_span()?;
1919        self.skip_s()?;
1920        self.expect_byte(b'>', XML_ERR_GT_REQUIRED, "'>' required")?;
1921        if &self.input[ea..eb] != open.qname.as_bytes() {
1922            let end_name = String::from_utf8_lossy(&self.input[ea..eb]).into_owned();
1923            let qname = &open.qname;
1924            return Err(self.err(
1925                XML_ERR_TAG_NAME_MISMATCH,
1926                format!("Opening and ending tag mismatch: {qname} and {end_name}"),
1927            ));
1928        }
1929        let uri = self.doc.node(open.elem).ns_uri.as_deref();
1930        self.sax.end_element_ns(local, prefix, uri);
1931        self.ns_stack.pop();
1932        self.stack.pop();
1933        self.depth -= 1;
1934        Ok(())
1935    }
1936
1937    /// Parse one complete element. Calls into the iterative content loop, so
1938    /// this is the only frame a document of any depth costs.
1939    fn parse_element(&mut self, parent: NodeId) -> Result<(), XmlError> {
1940        let Some(open) = self.open_element(parent)? else {
1941            return Ok(());
1942        };
1943        self.parse_content(open.elem)?;
1944        self.close_element(&open)
1945    }
1946
1947    /// Parse the content of `parent` and of every element nested inside it.
1948    ///
1949    /// This used to recurse into `parse_element`, which recursed back here, so
1950    /// document nesting consumed the call stack -- about 1.4 KB per level in
1951    /// release and 22 KB in debug, and a stack overflow aborts the process
1952    /// rather than returning an error. The element context was already heap
1953    /// state (`stack`, `ns_stack`); only the call frames were not. Now the
1954    /// descent is an explicit stack and the depth of a document costs no stack
1955    /// at all.
1956    fn parse_content(&mut self, parent: NodeId) -> Result<(), XmlError> {
1957        let mut open: Vec<OpenElem> = Vec::new();
1958        self.parse_content_inner(parent, &mut open, false, false)?;
1959        Ok(())
1960    }
1961
1962    fn parse_document(&mut self) -> Result<(), XmlError> {
1963        self.parse_prolog()?;
1964        self.parse_element(NodeId::DOCUMENT)?;
1965        self.parse_epilog()
1966    }
1967
1968    /// Rebuild a parser over a fresh buffer from saved state.
1969    fn resume(
1970        input: &'a [u8],
1971        options: i32,
1972        sax: &'a mut dyn SaxHandler,
1973        st: PushState,
1974    ) -> Self {
1975        let _ = st.root_closed;
1976        Parser {
1977            input,
1978            pos: 0,
1979            line: st.line,
1980            col: st.col,
1981            options,
1982            old10: (options & XML_PARSE_OLD10) != 0,
1983            depth: st.depth,
1984            ns_stack: st.ns_stack,
1985            sax,
1986            doc: st.doc,
1987            stack: st.stack,
1988            char_buf: st.char_buf,
1989            char_buf_from_reference: false,
1990            no_tree: (options & XML_PARSE_NO_TREE) != 0,
1991            recover: (options & XML_PARSE_RECOVER) != 0,
1992        // libxml2 bounds entity amplification at a small multiple of the input
1993        // for the same reason; without a bound, nesting is a bomb.
1994        entity_budget: input.len().saturating_mul(10).max(1 << 16),
1995            scratch_raw: Vec::new(),
1996            scratch_sax: Vec::new(),
1997            started: true,
1998        }
1999    }
2000
2001    fn suspend(self, open: Vec<OpenElem>, root_closed: bool) -> PushState {
2002        PushState {
2003            root_closed,
2004            doc: self.doc,
2005            ns_stack: self.ns_stack,
2006            stack: self.stack,
2007            open,
2008            char_buf: self.char_buf,
2009            line: self.line,
2010            col: self.col,
2011            depth: self.depth,
2012        }
2013    }
2014
2015    /// True when the remaining bytes are a proper prefix of a construct and we
2016    /// cannot tell what it is without more input.
2017    ///
2018    /// Only consulted while streaming. Character data is never "incomplete":
2019    /// the run scanner stops at `<`, `&` and `]`, and pending text is kept in
2020    /// `char_buf` rather than flushed, so more of it can simply be appended.
2021    fn incomplete_construct(&self) -> bool {
2022        let r = &self.input[self.pos..];
2023        fn has(h: &[u8], n: &[u8]) -> bool {
2024            h.len() >= n.len() && h.windows(n.len()).any(|w| w == n)
2025        }
2026        // A tag ends at the first '>' that is not inside an attribute value.
2027        fn tag_complete(r: &[u8]) -> bool {
2028            let mut quote: Option<u8> = None;
2029            for &b in &r[1..] {
2030                match quote {
2031                    Some(q) if b == q => quote = None,
2032                    Some(_) => {}
2033                    None => match b {
2034                        b'"' | 0x27 => quote = Some(b),
2035                        b'>' => return true,
2036                        _ => {}
2037                    },
2038                }
2039            }
2040            false
2041        }
2042        match r.first() {
2043            Some(b'<') => {
2044                if r.len() < 2 {
2045                    return true;
2046                }
2047                if r.starts_with(b"<!--") {
2048                    return !has(&r[4..], b"-->");
2049                }
2050                if r.starts_with(b"<![CDATA[") {
2051                    return !has(&r[9..], b"]]>");
2052                }
2053                if r.starts_with(b"<?") {
2054                    return !has(&r[2..], b"?>");
2055                }
2056                // `<!` could still become a comment, CDATA or a doctype.
2057                if r[1] == b'!' && r.len() < 9 {
2058                    return true;
2059                }
2060                !tag_complete(r)
2061            }
2062            Some(b'&') => !r.contains(&b';'),
2063            // `]` might yet become `]]>`.
2064            Some(b']') => r.len() < 3,
2065            // XML 1.0 2.11 folds CRLF to a single LF. A trailing CR gives no
2066            // way to know whether the LF follows, and guessing turned every
2067            // CRLF that landed on a chunk boundary into two newlines.
2068            Some(0x0D) => r.len() < 2,
2069            // A multi-byte character split across chunks: the lead byte says
2070            // how many continuation bytes belong to it, and without them the
2071            // scalar cannot be decoded.
2072            Some(&b0) => {
2073                let need = if b0 < 0x80 {
2074                    1
2075                } else if b0 >> 5 == 0b110 {
2076                    2
2077                } else if b0 >> 4 == 0b1110 {
2078                    3
2079                } else if b0 >> 3 == 0b11110 {
2080                    4
2081                } else {
2082                    1
2083                };
2084                r.len() < need
2085            }
2086            None => false,
2087        }
2088    }
2089
2090    /// The content loop, with the open-element stack supplied by the caller so
2091    /// it can survive between chunks.
2092    ///
2093    /// With `stop_at_eof`, running out of input is not an error: parsing stops
2094    /// at the last SAFE BOUNDARY -- the top of the loop, where we sit between
2095    /// content items rather than half way through a tag -- and returns that
2096    /// position. Pending character data stays in `char_buf` rather than being
2097    /// flushed, so a text run split across two chunks still produces one event
2098    /// and the push parser matches a whole-document parse exactly.
2099    fn parse_content_inner(
2100        &mut self,
2101        parent: NodeId,
2102        open: &mut Vec<OpenElem>,
2103        stop_at_eof: bool,
2104        stop_when_empty: bool,
2105    ) -> Result<usize, XmlError> {
2106        // The element the caller asked us to fill. When the innermost element
2107        // closes and nothing else is open, content belongs to THIS again --
2108        // falling back to the mutable `parent` would name the element that had
2109        // just been closed.
2110        let outer = parent;
2111        let mut parent = open.last().map(|f| f.elem).unwrap_or(parent);
2112        loop {
2113            let safe = self.pos;
2114            if self.eof() {
2115                if stop_at_eof {
2116                    return Ok(safe);
2117                }
2118                self.flush_chars(Some(parent))?;
2119                if let Some(o) = open.last() {
2120                    let (_, local) = Self::split_qname(&o.qname).unwrap_or((None, &o.qname));
2121                    return Err(self.err(
2122                        XML_ERR_TAG_NOT_FINISHED,
2123                        format!("Premature end of data in tag {local}"),
2124                    ));
2125                }
2126                return Ok(safe);
2127            }
2128            // Without the whole of a construct in hand we cannot tell what it
2129            // is, so stop here and wait for more input.
2130            if stop_at_eof && self.incomplete_construct() {
2131                return Ok(safe);
2132            }
2133            if self.starts_with(b"</") {
2134                self.flush_chars(Some(parent))?;
2135                // Our own end tag closes the innermost open element; when
2136                // nothing is open it belongs to the caller.
2137                match open.pop() {
2138                    Some(o) => {
2139                        self.close_element(&o)?;
2140                        // Streaming starts with the root already open, so an
2141                        // empty stack means the root just closed and the
2142                        // epilogue is the driver's job.
2143                        if stop_when_empty && open.is_empty() {
2144                            return Ok(self.pos);
2145                        }
2146                        parent = open.last().map(|f| f.elem).unwrap_or(outer);
2147                        continue;
2148                    }
2149                    None => return Ok(safe),
2150                }
2151            }
2152            if self.starts_with(b"<!--") {
2153                self.flush_chars(Some(parent))?;
2154                self.pos += 4;
2155                self.col += 4;
2156                self.parse_comment(Some(parent))?;
2157                continue;
2158            }
2159            if self.starts_with(b"<![CDATA[") {
2160                self.flush_chars(Some(parent))?;
2161                self.pos += 9;
2162                self.col += 9;
2163                self.parse_cdata(Some(parent))?;
2164                continue;
2165            }
2166            if self.starts_with(b"<?") {
2167                self.flush_chars(Some(parent))?;
2168                self.pos += 2;
2169                self.col += 2;
2170                self.parse_pi(Some(parent), false)?;
2171                continue;
2172            }
2173            let lead = self.peek_byte();
2174            if lead == Some(b'<') {
2175                self.flush_chars(Some(parent))?;
2176                if let Some(o) = self.open_element(parent)? {
2177                    parent = o.elem;
2178                    open.push(o);
2179                }
2180                continue;
2181            }
2182            if lead == Some(b'&') {
2183                // A CHARACTER reference is character data by definition -- it
2184                // cannot introduce markup -- so it belongs in the run it sits
2185                // in, not in a text node of its own.
2186                //
2187                // Flushing around it split `&#65; &#66;` into three nodes, and
2188                // the middle one was whitespace-only, so XML_PARSE_NOBLANKS
2189                // deleted it: `A B` came back as `AB`. Losing a space between
2190                // two character references is silent text corruption. It also
2191                // costs a node and an allocation per reference.
2192                //
2193                // A general entity still gets its own node: its replacement can
2194                // contain markup and is not ours to inline here.
2195                let is_charref = self.input.get(self.pos + 1) == Some(&b'#');
2196                if is_charref {
2197                    let repl = self.parse_reference()?;
2198                    self.char_buf.push_str(&repl);
2199                    self.char_buf_from_reference = true;
2200                } else {
2201                    self.flush_chars(Some(parent))?;
2202                    let raw = self.reference_raw_value();
2203                    // When the replacement is going to be re-parsed anyway,
2204                    // expanding it first is both wasted work and wrong: the
2205                    // expander does not know about CDATA, so `<!ENTITY e
2206                    // "<![CDATA[&foo;]]>">` had it chasing an entity that the
2207                    // section makes literal text.
2208                    let will_splice = raw
2209                        .as_deref()
2210                        .is_some_and(|r| r.contains('<') || r.contains('&'))
2211                        && !self.no_tree;
2212                    let repl = if will_splice {
2213                        self.consume_reference()?;
2214                        String::new()
2215                    } else {
2216                        self.parse_reference()?
2217                    };
2218                    // Replacement text containing markup has to become NODES.
2219                    // It was inserted as text and escaped on the way out, so
2220                    // `<!ENTITY e "<b>x</b>">` put the literal string
2221                    // `&lt;b&gt;x&lt;/b&gt;` in the tree: structure lost, and
2222                    // DTD validation saw character data where an element was
2223                    // declared.
2224                    // Only a DTD-declared entity whose STORED text holds
2225                    // markup is re-parsed. The predefined five produce literal
2226                    // characters, and splicing those as markup broke every
2227                    // document that so much as mentions `&lt;`.
2228                    // '&' as well as '<': `<!ENTITY e "&#38;">` stores a bare
2229                    // ampersand, and a bare ampersand in content is an error,
2230                    // not text. Re-parsing the replacement is what says so.
2231                    // The element contained a reference, whatever it expanded
2232                    // to -- including nothing, which leaves no node to see.
2233                    if !self.no_tree {
2234                        self.doc.elements_with_entity_refs.insert(parent);
2235                    }
2236                    if will_splice {
2237                        self.splice_entity(raw.as_deref().unwrap(), parent)?;
2238                    } else {
2239                        // NOT marked as referenced text. An entity whose value
2240                        // IS whitespace contributes ignorable whitespace --
2241                        // `<!ENTITY space " ">` and `<!ENTITY space "&#32;">`
2242                        // are both valid in element-only content, because the
2243                        // reference is replaced by its content. It is a
2244                        // character reference standing in the DOCUMENT that is
2245                        // character data, and that case is marked where it is
2246                        // read, or by the sub-parse when an entity's
2247                        // replacement text contains one literally.
2248                        self.char_buf.push_str(&repl);
2249                        self.flush_chars(Some(parent))?;
2250                    }
2251                }
2252                continue;
2253            }
2254            if self.starts_with(b"]]>") {
2255                return Err(self.err(XML_ERR_MISPLACED_CDATA_END, "Misplaced CDATA end"));
2256            }
2257            // Character data is the bulk of most documents and is almost all
2258            // ordinary ASCII. Take it in one run: one bounds test and one
2259            // push_str instead of a decode, two peeks and a push per character.
2260            {
2261                let start = self.pos;
2262                let mut i = start;
2263                while i < self.input.len() {
2264                    let b = self.input[i];
2265                    let plain = b == 0x09 || (0x20..0x80).contains(&b);
2266                    if !plain || b == b'<' || b == b'&' || b == b']' {
2267                        break;
2268                    }
2269                    i += 1;
2270                }
2271                if i > start {
2272                    // Every byte in the run is ASCII and a legal XML character.
2273                    match std::str::from_utf8(&self.input[start..i]) {
2274                        Ok(run) => {
2275                            self.char_buf.push_str(run);
2276                            self.col += (i - start) as u32;
2277                            self.pos = i;
2278                            continue;
2279                        }
2280                        Err(_) => {}
2281                    }
2282                }
2283            }
2284            let c = self.bump_char()?.unwrap();
2285            if !xml_is_char(c as u32) {
2286                return Err(self.err(XML_ERR_INVALID_CHAR, "Invalid character"));
2287            }
2288            self.char_buf.push(c);
2289        }
2290    }
2291
2292    fn parse_misc(&mut self, parent: NodeId) -> Result<(), XmlError> {
2293        loop {
2294            self.skip_s()?;
2295            if self.starts_with(b"<!--") {
2296                self.pos += 4;
2297                self.col += 4;
2298                self.parse_comment(Some(parent))?;
2299                continue;
2300            }
2301            if self.starts_with(b"<?") {
2302                self.pos += 2;
2303                self.col += 2;
2304                self.parse_pi(Some(parent), false)?;
2305                continue;
2306            }
2307            break;
2308        }
2309        Ok(())
2310    }
2311
2312    /// Everything before the root element's start tag: BOM, XML declaration,
2313    /// misc, doctype. Split out so the push parser can reach the root without
2314    /// committing to parse the whole document in one go.
2315    fn parse_prolog(&mut self) -> Result<(), XmlError> {
2316        if self.starts_with(&[0xef, 0xbb, 0xbf]) {
2317            self.pos += 3;
2318        }
2319        self.sax.set_document_locator();
2320        self.sax.start_document();
2321        self.started = true;
2322
2323        // XMLDecl must be at the start (after BOM). `<?xml-stylesheet` is a PI.
2324        if self.starts_with(b"<?xml") {
2325            let save_pos = self.pos;
2326            let save_col = self.col;
2327            let save_line = self.line;
2328            self.pos += 5;
2329            self.col += 5;
2330            match self.peek_byte() {
2331                Some(b) if b < 0x80 && crate::chvalid::xml_is_blank(b as u32) => {
2332                    self.parse_xml_decl_rest()?;
2333                }
2334                _ => {
2335                    self.pos = save_pos;
2336                    self.col = save_col;
2337                    self.line = save_line;
2338                    self.pos += 2;
2339                    self.col += 2;
2340                    self.parse_pi(Some(NodeId::DOCUMENT), false)?;
2341                }
2342            }
2343        }
2344
2345        self.parse_misc(NodeId::DOCUMENT)?;
2346        if self.starts_with(b"<!DOCTYPE") {
2347            self.pos += 9;
2348            self.col += 9;
2349            self.skip_doctype()?;
2350            self.parse_misc(NodeId::DOCUMENT)?;
2351        }
2352
2353        if self.peek_byte() != Some(b'<') {
2354            return Err(self.err(XML_ERR_DOCUMENT_EMPTY, "Document is empty"));
2355        }
2356        Ok(())
2357    }
2358
2359    /// Everything after the root element: trailing misc, then end-of-document.
2360    fn parse_epilog(&mut self) -> Result<(), XmlError> {
2361        self.parse_misc(NodeId::DOCUMENT)?;
2362        self.skip_s()?;
2363        if !self.eof() {
2364            return Err(self.err(XML_ERR_EXTRA_CONTENT, "Extra content at the end of the document"));
2365        }
2366        self.sax.end_document();
2367        Ok(())
2368    }
2369}
2370
2371fn parse_doc(
2372    buffer: &[u8],
2373    _url: Option<&str>,
2374    encoding: Option<&str>,
2375    options: i32,
2376    sax: &mut dyn SaxHandler,
2377) -> Result<XmlDoc, XmlError> {
2378    let (converted, enc_name) = crate::encoding::xml_convert_to_utf8_cow(buffer, encoding)?;
2379    parse_utf8(&converted, enc_name.as_deref(), options, sax)
2380}
2381
2382fn parse_utf8(
2383    buffer: &[u8],
2384    enc_name: Option<&str>,
2385    options: i32,
2386    sax: &mut dyn SaxHandler,
2387) -> Result<XmlDoc, XmlError> {
2388    let options = options | XML_PARSE_NONET | XML_PARSE_NO_XXE;
2389    let mut p = Parser {
2390        input: buffer,
2391        pos: 0,
2392        line: 1,
2393        col: 1,
2394        options,
2395        old10: (options & XML_PARSE_OLD10) != 0,
2396        depth: 0,
2397        ns_stack: Vec::new(),
2398        sax,
2399        // Reserving a full arena is the dominant cost of a no-tree parse --
2400        // pre-allocating a tree only to leave it empty.
2401        doc: XmlDoc::with_node_capacity(
2402            Some("1.0"),
2403            if (options & XML_PARSE_NO_TREE) != 0 {
2404                // Only element nodes are created in this mode, which measure
2405                // about one per 36 input bytes. Reserving for a full tree
2406                // wasted the arena; reserving nothing made it double instead.
2407                buffer.len() / 32
2408            } else {
2409                buffer.len() / 10
2410            },
2411        ),
2412        stack: Vec::new(),
2413        char_buf: String::new(),
2414        char_buf_from_reference: false,
2415        no_tree: (options & XML_PARSE_NO_TREE) != 0,
2416        recover: (options & XML_PARSE_RECOVER) != 0,
2417        // libxml2 bounds entity amplification at a small multiple of the input
2418        // for the same reason; without a bound, nesting is a bomb.
2419        entity_budget: buffer.len().saturating_mul(10).max(1 << 16),
2420        scratch_raw: Vec::new(),
2421        scratch_sax: Vec::new(),
2422        started: false,
2423    };
2424    match p.parse_document() {
2425        Ok(()) => {
2426            apply_dtd_defaults(&mut p.doc, buffer.len(), options)?;
2427            normalize_tokenized_attrs(&mut p.doc);
2428            match (&p.doc.encoding, enc_name) {
2429                (None, Some(n)) => {
2430                    if !n.eq_ignore_ascii_case("UTF-8") && !n.eq_ignore_ascii_case("US-ASCII") {
2431                        p.doc.encoding = Some(n.to_string());
2432                    }
2433                }
2434                (Some(declared), Some(detected)) => {
2435                    // A byte-order mark is evidence and a declaration is a
2436                    // claim; when they disagree the document is broken, and we
2437                    // were saying nothing at all. C reports it and carries on,
2438                    // so we report it and carry on -- a UTF-16 file declaring
2439                    // utf-8 is exactly the kind of thing that bites the next
2440                    // program along.
2441                    if !encodings_agree(declared, detected) {
2442                        let msg = format!(
2443                            "Encoding '{declared}' doesn't match auto-detected '{detected}'"
2444                        );
2445                        p.sax.error(&msg);
2446                        p.doc.warnings.push(msg);
2447                    }
2448                }
2449                _ => {}
2450            }
2451            Ok(p.doc)
2452        }
2453        Err(e) => {
2454            if p.started {
2455                p.sax.end_document();
2456            }
2457            if (options & XML_PARSE_RECOVER) != 0 {
2458                // Hand back everything parsed before the failure. One bad byte
2459                // in a large document used to cost the caller all of it.
2460                p.sax.error(&e.message);
2461                return Ok(p.doc);
2462            }
2463            Err(e)
2464        }
2465    }
2466}
2467
2468fn apply_dtd_defaults(
2469    doc: &mut XmlDoc,
2470    input_len: usize,
2471    options: i32,
2472) -> Result<(), XmlError> {
2473    // Completing attributes from ATTLIST defaults is opt-in, as it is in C:
2474    // libxml2 does it for XML_PARSE_DTDATTR (xmllint --dtdattr) and not
2475    // otherwise -- not even for --valid. We did it unconditionally, so every
2476    // document with an ATTLIST default came back with attributes libxml2 would
2477    // not have added, which is a visible difference in the serialized output.
2478    if (options & XML_PARSE_DTDATTR) == 0 {
2479        return Ok(());
2480    }
2481    // The common cases -- no DTD, or a DTD carrying no ATTLIST default -- cost
2482    // nothing now. Testing before the clone matters: cloning the DTD copies
2483    // every entity and declaration in it.
2484    match &doc.dtd {
2485        None => return Ok(()),
2486        Some(d) => {
2487            if !d.attributes.values().any(|a| a.default_value.is_some()) {
2488                return Ok(());
2489            }
2490        }
2491    }
2492    // Defaulted attributes are an amplification vector: 13 KB with 200 ATTLIST
2493    // defaults expanded to 402,002 nodes (~74 MB) before this bound existed.
2494    // libxml2 caps entity amplification for the same reason. The budget is
2495    // generous enough that a real DTD never reaches it.
2496    // Sized from measurement, not taste: a real DTD-heavy document runs about
2497    // 0.18 defaulted attributes per input byte, while the amplification cases
2498    // run 12-30 per byte -- two orders of magnitude apart. One per input byte
2499    // sits in the gap, with a floor so small documents are never penalised and
2500    // a ceiling so a huge one cannot walk past it.
2501    let mut budget = input_len.max(65_536).min(5_000_000);
2502    let dtd = match doc.dtd.clone() {
2503        Some(d) => d,
2504        None => return Ok(()),
2505    };
2506    // Group the defaults by element name once. The previous form rescanned
2507    // every declaration for every element in the document, and allocated the
2508    // element's name each time round.
2509    let mut by_elem: std::collections::HashMap<&str, Vec<(&str, &str)>> =
2510        std::collections::HashMap::new();
2511    for ((elem, aname), ad) in &dtd.attributes {
2512        if let Some(v) = &ad.default_value {
2513            by_elem
2514                .entry(elem.as_str())
2515                .or_default()
2516                .push((aname.as_str(), v.as_str()));
2517        }
2518    }
2519    // dtd.attributes is a HashMap with a randomly seeded hasher, so without
2520    // this the defaulted attributes serialised in a DIFFERENT ORDER ON EVERY
2521    // RUN of the same binary. Any signature or digest over the saved tree --
2522    // C14N included -- has to be reproducible.
2523    for list in by_elem.values_mut() {
2524        list.sort_unstable_by(|a, b| a.0.cmp(b.0));
2525    }
2526    let n = doc.len();
2527    for i in 0..n {
2528        let id = NodeId(i as u32);
2529        if doc.kind(id) != NodeKind::Element {
2530            continue;
2531        }
2532        let Some(list) = by_elem.get(doc.name(id)) else {
2533            continue;
2534        };
2535        for (aname, v) in list.iter() {
2536            if doc.xml_get_prop(id, aname).is_none() {
2537                if budget == 0 {
2538                    return Err(XmlError::new(
2539                        XML_ERR_INTERNAL_ERROR,
2540                        "Maximum attribute-default amplification exceeded",
2541                        0,
2542                        0,
2543                    ));
2544                }
2545                budget -= 1;
2546                doc.xml_set_prop(id, aname, v);
2547            }
2548        }
2549    }
2550    Ok(())
2551}
2552
2553#[cfg(test)]
2554mod chvalid_tests {
2555    use crate::xml_is_char;
2556    use std::path::PathBuf;
2557
2558    #[test]
2559    fn xml_is_char_matches_c_bmp_dump() {
2560        let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
2561        p.pop();
2562        p.pop();
2563        p.push("corpora");
2564        p.push("xmlIsChar-bmp.bin");
2565        if !p.exists() {
2566            return;
2567        }
2568        let dump = std::fs::read(&p).expect("corpora/xmlIsChar-bmp.bin");
2569        assert_eq!(dump.len(), 65536);
2570        for i in 0u32..=0xffff {
2571            let want = dump[i as usize] != 0;
2572            let got = xml_is_char(i);
2573            assert_eq!(got, want, "xml_is_char({i:#x}) = {got}, C dump = {want}");
2574        }
2575    }
2576}
2577
2578/// Does this text hold an ampersand that does not begin a reference?
2579///
2580/// Entity replacement text is inserted into an attribute value verbatim, so a
2581/// bare ampersand in it is exactly as illegal there as it is in content.
2582fn has_bare_ampersand(s: &str) -> bool {
2583    let mut it = s.chars().peekable();
2584    while let Some(c) = it.next() {
2585        if c != '&' {
2586            continue;
2587        }
2588        if it.peek() == Some(&'#') {
2589            it.next();
2590            let hex = it.peek() == Some(&'x');
2591            if hex {
2592                it.next();
2593            }
2594            let mut any = false;
2595            while let Some(&d) = it.peek() {
2596                if (hex && d.is_ascii_hexdigit()) || (!hex && d.is_ascii_digit()) {
2597                    any = true;
2598                    it.next();
2599                } else {
2600                    break;
2601                }
2602            }
2603            if !any || it.next() != Some(';') {
2604                return true;
2605            }
2606            continue;
2607        }
2608        let mut any = false;
2609        while let Some(&d) = it.peek() {
2610            if crate::chvalid::xml_is_name_char(d as u32, false) {
2611                any = true;
2612                it.next();
2613            } else {
2614                break;
2615            }
2616        }
2617        if !any || it.next() != Some(';') {
2618            return true;
2619        }
2620    }
2621    false
2622}
2623
2624/// Attribute-value normalization for the tokenized types (XML 1.0 3.3.3).
2625///
2626/// A value whose declared type is anything but CDATA has its leading and
2627/// trailing space discarded and its internal runs collapsed to one space each.
2628/// We did none of it, so `id="  x  y  "` stayed `  x  y  ` where C reports
2629/// `x y` -- a different value for the same document, on any document with a
2630/// DTD that declares a non-CDATA attribute.
2631///
2632/// Not gated on XML_PARSE_DTDATTR: this is normalization, not defaulting, and
2633/// libxml2 does it whether or not you ask for defaults.
2634fn normalize_tokenized_attrs(doc: &mut XmlDoc) {
2635    let Some(dtd) = doc.dtd.as_ref() else { return };
2636    // The common case is a DTD with no tokenized attribute at all.
2637    if !dtd.attributes.values().any(|a| a.att_type != "CDATA") {
2638        return;
2639    }
2640    let tokenized: std::collections::HashSet<(String, String)> = dtd
2641        .attributes
2642        .iter()
2643        .filter(|(_, a)| a.att_type != "CDATA")
2644        .map(|((e, a), _)| (e.clone(), a.clone()))
2645        .collect();
2646
2647    let mut stack = vec![NodeId::DOCUMENT];
2648    let mut edits: Vec<(NodeId, String)> = Vec::new();
2649    while let Some(id) = stack.pop() {
2650        let mut c = doc.first_child(id);
2651        while let Some(x) = c {
2652            if doc.kind(x) == NodeKind::Element {
2653                let elem = doc.qname(x);
2654                let mut a = doc.first_attr(x);
2655                while let Some(at) = a {
2656                    if tokenized.contains(&(elem.clone(), doc.qname(at))) {
2657                        let v = doc.content(at);
2658                        // Space-separated, not whitespace-separated. XML 1.0
2659                        // 3.3.3 turns LITERAL tab/newline into a space during
2660                        // normalization, but a character reference contributes
2661                        // its character unchanged -- so a referenced tab is
2662                        // still a tab here and belongs to the token around it.
2663                        // Splitting on it merged two tokens out of one.
2664                        let norm = v
2665                            .split(' ')
2666                            .filter(|t| !t.is_empty())
2667                            .collect::<Vec<_>>()
2668                            .join(" ");
2669                        if norm != v {
2670                            edits.push((at, norm));
2671                        }
2672                    }
2673                    a = doc.next_sibling(at);
2674                }
2675                stack.push(x);
2676            }
2677            c = doc.next_sibling(x);
2678        }
2679    }
2680    for (at, v) in edits {
2681        doc.node_mut(at).content = v;
2682    }
2683}
2684
2685/// Do a declared encoding and an auto-detected one describe the same thing?
2686///
2687/// Only the family matters: a byte-order mark can say UTF-16BE where the
2688/// declaration says UTF-16, and those agree. UTF-8 against UTF-16 does not.
2689fn encodings_agree(declared: &str, detected: &str) -> bool {
2690    fn family(n: &str) -> &'static str {
2691        let n = n.to_ascii_uppercase();
2692        if n.starts_with("UTF-16") || n.starts_with("UTF16") {
2693            "16"
2694        } else if n.starts_with("UTF-32") || n.starts_with("UTF32") {
2695            "32"
2696        } else if n.starts_with("UTF-8") || n.starts_with("UTF8") || n == "US-ASCII" {
2697            "8"
2698        } else {
2699            "other"
2700        }
2701    }
2702    let (d, a) = (family(declared), family(detected));
2703    d == a || d == "other" || a == "other"
2704}