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    ctxt.buf = Vec::new();
388    ctxt.buf.shrink_to_fit();
389    ctxt.last_error = None;
390    Ok(Some(doc))
391}
392
393/// A parser over a whole buffer, configured exactly as `parse_utf8` does.
394fn fresh_parser<'a>(
395    input: &'a [u8],
396    options: i32,
397    sax: &'a mut dyn SaxHandler,
398) -> Parser<'a> {
399    Parser {
400        input,
401        pos: 0,
402        line: 1,
403        col: 1,
404        options,
405        old10: (options & XML_PARSE_OLD10) != 0,
406        depth: 0,
407        ns_stack: Vec::new(),
408        sax,
409        doc: XmlDoc::with_node_capacity(
410            Some("1.0"),
411            if (options & XML_PARSE_NO_TREE) != 0 {
412                input.len() / 32
413            } else {
414                input.len() / 10
415            },
416        ),
417        stack: Vec::new(),
418        char_buf: String::new(),
419        no_tree: (options & XML_PARSE_NO_TREE) != 0,
420        recover: (options & XML_PARSE_RECOVER) != 0,
421        // libxml2 bounds entity amplification at a small multiple of the input
422        // for the same reason; without a bound, nesting is a bomb.
423        entity_budget: input.len().saturating_mul(10).max(1 << 16),
424        scratch_raw: Vec::new(),
425        scratch_sax: Vec::new(),
426        started: false,
427    }
428}
429
430/// Parse the accumulated buffer as one whole document.
431fn finish_whole(ctxt: &mut XmlPushParserCtxt) -> Result<Option<XmlDoc>, XmlError> {
432    match xml_read_memory(
433        &ctxt.buf,
434        ctxt.url.as_deref(),
435        ctxt.encoding.as_deref(),
436        ctxt.options,
437    ) {
438        Ok(doc) => {
439            ctxt.buf = Vec::new();
440            ctxt.buf.shrink_to_fit();
441            ctxt.last_error = None;
442            Ok(Some(doc))
443        }
444        Err(e) => {
445            ctxt.last_error = Some(e.clone());
446            Err(e)
447        }
448    }
449}
450
451/// `xmlReadIO` — caller-supplied read callback, no network.
452#[doc(alias = "xmlReadIO")]
453pub fn xml_read_io<F>(
454    mut read: F,
455    url: Option<&str>,
456    encoding: Option<&str>,
457    options: i32,
458) -> Result<XmlDoc, XmlError>
459where
460    F: FnMut(&mut [u8]) -> Result<usize, std::io::Error>,
461{
462    let mut buf = Vec::new();
463    let mut tmp = [0u8; 4096];
464    loop {
465        let n = read(&mut tmp).map_err(|e| XmlError::new(XML_ERR_DOCUMENT_START, e.to_string(), 0, 0))?;
466        if n == 0 {
467            break;
468        }
469        buf.extend_from_slice(&tmp[..n]);
470    }
471    xml_read_memory(&buf, url, encoding, options)
472}
473
474/// `xmlCtxtReset`.
475#[doc(alias = "xmlCtxtReset")]
476pub fn xml_ctxt_reset(ctxt: &mut XmlParserCtxt) {
477    ctxt.doc = None;
478    ctxt.last_error = None;
479}
480
481/// Parser state that survives between push chunks.
482///
483/// Everything the parser needs to carry across a chunk boundary is owned data,
484/// which is why streaming is possible at all: the descent lives in `open`, not
485/// on the call stack.
486struct PushState {
487    doc: XmlDoc,
488    ns_stack: Vec<Vec<(Option<String>, String)>>,
489    stack: Vec<NodeId>,
490    open: Vec<OpenElem>,
491    char_buf: String,
492    line: u32,
493    col: u32,
494    depth: u32,
495    /// The root's end tag has been consumed. Without this, a later chunk would
496    /// re-enter the content loop with an empty stack and parse the document's
497    /// trailing whitespace as content, adding a stray text node.
498    root_closed: bool,
499}
500
501/// An element whose start tag has been consumed and whose end tag has not.
502struct OpenElem {
503    /// The QName exactly as written, for the end-tag comparison.
504    qname: String,
505    elem: NodeId,
506}
507
508/// One attribute exactly as it was scanned, before namespaces are resolved.
509struct RawAttr {
510    qname: String,
511    value: String,
512    value_off: usize,
513    /// Byte index of the QName's colon, resolved once at scan time.
514    colon: Option<usize>,
515}
516
517impl RawAttr {
518    fn parts(&self) -> (Option<&str>, &str) {
519        match self.colon {
520            None => (None, self.qname.as_str()),
521            Some(i) => (Some(&self.qname[..i]), &self.qname[i + 1..]),
522        }
523    }
524}
525
526struct Parser<'a> {
527    input: &'a [u8],
528    pos: usize,
529    line: u32,
530    col: u32,
531    options: i32,
532    old10: bool,
533    depth: u32,
534    ns_stack: Vec<Vec<(Option<String>, String)>>,
535    sax: &'a mut dyn SaxHandler,
536    doc: XmlDoc,
537    stack: Vec<NodeId>,
538    char_buf: String,
539    scratch_raw: Vec<RawAttr>,
540    scratch_sax: Vec<SaxAttr>,
541    started: bool,
542    no_tree: bool,
543    recover: bool,
544    /// Bytes of entity expansion still permitted. Expanding nested entities
545    /// creates the billion-laughs vector, so it is bounded from the start.
546    entity_budget: usize,
547}
548
549impl<'a> Parser<'a> {
550    fn err(&self, code: i32, msg: impl Into<String>) -> XmlError {
551        XmlError::new(code, msg, self.line, self.col)
552    }
553
554    fn eof(&self) -> bool {
555        self.pos >= self.input.len()
556    }
557
558    fn peek_byte(&self) -> Option<u8> {
559        self.input.get(self.pos).copied()
560    }
561
562    fn starts_with(&self, s: &[u8]) -> bool {
563        self.input[self.pos..].starts_with(s)
564    }
565
566    fn bump_byte(&mut self) -> Option<u8> {
567        let b = self.peek_byte()?;
568        self.pos += 1;
569        if b == b'\n' {
570            self.line += 1;
571            self.col = 1;
572        } else {
573            self.col += 1;
574        }
575        Some(b)
576    }
577
578    /// Next Unicode scalar with XML 1.0 §2.11 EOL: `\r\n` / `\r` → `\n`.
579    fn peek_char(&self) -> Result<Option<char>, XmlError> {
580        // One bounds-checked load covers the end test and the byte fetch; the
581        // previous form did eof(), then re-sliced, then indexed.
582        let Some(&b0) = self.input.get(self.pos) else {
583            return Ok(None);
584        };
585        if b0 == b'\r' {
586            return Ok(Some('\n'));
587        }
588        if b0 < 0x80 {
589            return Ok(Some(b0 as char));
590        }
591        let rest = &self.input[self.pos..];
592        // A UTF-8 scalar is at most 4 bytes, so the leading one is always complete
593        // within the first 4. Validating only those keeps this O(1); validating the
594        // whole tail made a parse O(n^2) in the document length.
595        let head = &rest[..rest.len().min(4)];
596        match std::str::from_utf8(head) {
597            Ok(s) => Ok(s.chars().next()),
598            // The leading scalar decoded; the error belongs to a later one, which
599            // this call is not responsible for reporting.
600            Err(e) if e.valid_up_to() > 0 => Ok(std::str::from_utf8(&head[..e.valid_up_to()])
601                .ok()
602                .and_then(|s| s.chars().next())),
603            Err(_) => Err(XmlError::new(
604                XML_ERR_INVALID_CHAR,
605                "Invalid UTF-8",
606                self.line,
607                self.col,
608            )),
609        }
610    }
611
612    fn bump_char(&mut self) -> Result<Option<char>, XmlError> {
613        // ASCII and CR are handled without a decode and without the second
614        // peek_byte the CR test used to cost on every character.
615        match self.input.get(self.pos) {
616            None => return Ok(None),
617            Some(&b) if b == b'\r' => {
618                self.pos += 1;
619                self.col += 1;
620                if self.input.get(self.pos) == Some(&b'\n') {
621                    self.pos += 1;
622                    self.line += 1;
623                    self.col = 1;
624                }
625                return Ok(Some('\n'));
626            }
627            Some(&b) if b < 0x80 => {
628                self.pos += 1;
629                if b == b'\n' {
630                    self.line += 1;
631                    self.col = 1;
632                } else {
633                    self.col += 1;
634                }
635                return Ok(Some(b as char));
636            }
637            _ => {}
638        }
639        let c = match self.peek_char()? {
640            None => return Ok(None),
641            Some(c) => c,
642        };
643        // Advance the whole scalar at once. The byte-at-a-time loop re-ran a
644        // bounds-checked load and a newline test for every continuation byte,
645        // none of which can be a newline.
646        let n = c.len_utf8();
647        self.pos += n;
648        if c as u32 == 0x0A {
649            self.line += 1;
650            self.col = 1;
651        } else {
652            // The byte-at-a-time loop this replaces advanced col once per byte,
653            // so keep col in bytes or error positions shift on non-ASCII lines.
654            self.col += n as u32;
655        }
656        Ok(Some(c))
657    }
658
659    /// Consume required whitespace, reporting whether any was there.
660    fn require_s(&mut self) -> bool {
661        let before = self.pos;
662        let _ = self.skip_s();
663        self.pos > before
664    }
665
666    fn skip_s(&mut self) -> Result<(), XmlError> {
667        // Every XML whitespace character is ASCII, so this never needs a decode.
668        // The previous form decoded each one twice (peek, then bump).
669        while let Some(b) = self.peek_byte() {
670            if b >= 0x80 || !crate::chvalid::xml_is_blank(b as u32) {
671                break;
672            }
673            self.bump_byte();
674        }
675        Ok(())
676    }
677
678    fn expect_byte(&mut self, b: u8, code: i32, msg: &str) -> Result<(), XmlError> {
679        if self.peek_byte() != Some(b) {
680            return Err(self.err(code, msg));
681        }
682        self.bump_byte();
683        Ok(())
684    }
685
686    fn parse_name_span(&mut self) -> Result<(usize, usize), XmlError> {
687        // The first character went through peek_char AND bump_char -- two
688        // decodes -- for what is almost always one ASCII byte.
689        match self.input.get(self.pos) {
690            Some(&b) if b < 0x80 && b != b'\r' => {
691                if !xml_is_name_start_char(b as u32, self.old10) {
692                    return Err(self.err(XML_ERR_NAME_REQUIRED, "Name expected"));
693                }
694            }
695            _ => {
696                let c = self
697                    .peek_char()?
698                    .ok_or_else(|| self.err(XML_ERR_NAME_REQUIRED, "Name expected"))?;
699                if !xml_is_name_start_char(c as u32, self.old10) {
700                    return Err(self.err(XML_ERR_NAME_REQUIRED, "Name expected"));
701                }
702            }
703        }
704        // Scan the name in place and copy it out once. Name characters are
705        // overwhelmingly ASCII, and an ASCII byte needs no decode at all -- the
706        // char-at-a-time form decoded every character twice (peek, then bump)
707        // and grew the String one push at a time.
708        let start = self.pos;
709        self.bump_char()?;
710        loop {
711            let Some(b) = self.peek_byte() else { break };
712            if b < 0x80 {
713                if !xml_is_name_char(b as u32, self.old10) {
714                    break;
715                }
716                if self.pos - start >= MAX_NAME && (self.options & XML_PARSE_HUGE) == 0 {
717                    return Err(self.err(XML_ERR_NAME_REQUIRED, "Name too long"));
718                }
719                self.bump_byte();
720            } else {
721                let Some(c) = self.peek_char()? else { break };
722                if !xml_is_name_char(c as u32, self.old10) {
723                    break;
724                }
725                if self.pos - start >= MAX_NAME && (self.options & XML_PARSE_HUGE) == 0 {
726                    return Err(self.err(XML_ERR_NAME_REQUIRED, "Name too long"));
727                }
728                self.bump_char()?;
729            }
730        }
731        // Every byte in the span was accepted as part of a decoded character,
732        // so this is valid UTF-8; validate anyway rather than reach for unsafe.
733        Ok((start, self.pos))
734    }
735
736    /// The owning form. Prefer [`Parser::parse_name_span`] where the name is
737    /// only compared -- an end tag allocated a String purely to discard it.
738    fn parse_name(&mut self) -> Result<String, XmlError> {
739        let (a, b) = self.parse_name_span()?;
740        match std::str::from_utf8(&self.input[a..b]) {
741            Ok(name) => Ok(name.to_string()),
742            Err(_) => Err(self.err(XML_ERR_INVALID_CHAR, "Invalid UTF-8")),
743        }
744    }
745
746    fn split_qname(name: &str) -> Result<(Option<&str>, &str), XmlError> {
747        let mut parts = name.split(':');
748        let a = parts.next().unwrap();
749        match parts.next() {
750            None => Ok((None, a)),
751            Some(b) => {
752                if parts.next().is_some() || a.is_empty() || b.is_empty() {
753                    return Err(XmlError::new(
754                        XML_NS_ERR_QNAME,
755                        format!("Invalid QName {name}"),
756                        0,
757                        0,
758                    ));
759                }
760                Ok((Some(a), b))
761            }
762        }
763    }
764
765    fn lookup_ns(&self, prefix: Option<&str>) -> Option<String> {
766        if prefix == Some("xml") {
767            return Some(XML_NS.into());
768        }
769        if prefix == Some("xmlns") {
770            return Some(XMLNS_NS.into());
771        }
772        for frame in self.ns_stack.iter().rev() {
773            for (p, uri) in frame.iter().rev() {
774                if p.as_deref() == prefix {
775                    return Some(uri.clone());
776                }
777            }
778        }
779        None
780    }
781
782    fn uri_has_scheme(uri: &str) -> bool {
783        let bytes = uri.as_bytes();
784        if bytes.is_empty() {
785            return false;
786        }
787        if !bytes[0].is_ascii_alphabetic() {
788            return false;
789        }
790        let mut i = 1;
791        while i < bytes.len() {
792            let b = bytes[i];
793            if b == b':' {
794                return true;
795            }
796            if b.is_ascii_alphanumeric() || b == b'+' || b == b'-' || b == b'.' {
797                i += 1;
798            } else {
799                return false;
800            }
801        }
802        false
803    }
804
805    fn flush_chars(&mut self, parent: Option<NodeId>) -> Result<(), XmlError> {
806        if self.char_buf.is_empty() {
807            return Ok(());
808        }
809        if self.char_buf.len() > MAX_TEXT && (self.options & XML_PARSE_HUGE) == 0 {
810            return Err(self.err(XML_ERR_INVALID_CHAR, "Text too long"));
811        }
812        let skip_blank = (self.options & XML_PARSE_NOBLANKS) != 0
813            && self.char_buf.chars().all(|c| crate::chvalid::xml_is_blank(c as u32));
814        if !skip_blank {
815            self.sax.characters(&self.char_buf);
816            if let Some(p) = parent.filter(|_| !self.no_tree) {
817                let t = self.doc.alloc_unnamed(NodeKind::Text);
818                // Moved, not copied: the buffer is cleared immediately after,
819                // so the clone was a pure allocation plus memcpy per text node.
820                self.doc.node_mut(t).content = std::mem::take(&mut self.char_buf);
821                self.doc.xml_add_child(p, t);
822            }
823        }
824        self.char_buf.clear();
825        Ok(())
826    }
827
828    fn parse_comment(&mut self, parent: Option<NodeId>) -> Result<(), XmlError> {
829        // called after seeing "<!--"
830        let mut body = String::new();
831        loop {
832            if self.starts_with(b"-->") {
833                self.pos += 3;
834                self.col += 3;
835                break;
836            }
837            if self.eof() {
838                return Err(self.err(XML_ERR_COMMENT_NOT_FINISHED, "Comment not finished"));
839            }
840            if self.starts_with(b"--") {
841                return Err(self.err(XML_ERR_HYPHEN_IN_COMMENT, "Double hyphen in comment"));
842            }
843            let c = self.bump_char()?.unwrap();
844            if !xml_is_char(c as u32) {
845                return Err(self.err(XML_ERR_INVALID_CHAR, "Invalid character"));
846            }
847            body.push(c);
848        }
849        self.sax.comment(&body);
850        if let Some(p) = parent.filter(|_| !self.no_tree) {
851            let n = self.doc.alloc_unnamed(NodeKind::Comment);
852            self.doc.node_mut(n).content = body;
853            self.doc.xml_add_child(p, n);
854        }
855        Ok(())
856    }
857
858    fn parse_pi(&mut self, parent: Option<NodeId>, xml_decl_ok: bool) -> Result<bool, XmlError> {
859        // called after seeing "<?"
860        let target = self.parse_name()?;
861        if target.eq_ignore_ascii_case("xml") {
862            if xml_decl_ok {
863                return self.parse_xml_decl_rest().map(|_| true);
864            }
865            return Err(self.err(XML_ERR_RESERVED_XML_NAME, "Reserved PI target xml"));
866        }
867        // Namespaces in XML reserves the colon for QNames, so a PI target
868        // should be an NCName -- but C reports this and carries on, and
869        // rejecting a document libxml2 accepts is a worse trade than the three
870        // conformance cases it would win.
871        if target.contains(':') {
872            self.sax
873                .warning(&format!("colons are forbidden from PI names '{target}'
874"));
875        }
876        let data = if matches!(self.peek_byte(), Some(b) if b < 0x80 && crate::chvalid::xml_is_blank(b as u32)) {
877            self.skip_s()?;
878            let mut d = String::new();
879            loop {
880                if self.starts_with(b"?>") {
881                    self.pos += 2;
882                    self.col += 2;
883                    break;
884                }
885                if self.eof() {
886                    return Err(self.err(XML_ERR_PI_NOT_FINISHED, "PI not finished"));
887                }
888                let c = self.bump_char()?.unwrap();
889                // The character rule applies inside a PI too. A form feed in
890                // one was accepted; C stops at it.
891                if !xml_is_char(c as u32) {
892                    return Err(self.err(XML_ERR_INVALID_CHAR, "Invalid character in PI"));
893                }
894                d.push(c);
895            }
896            Some(d)
897        } else {
898            if !self.starts_with(b"?>") {
899                return Err(self.err(XML_ERR_PI_NOT_FINISHED, "PI not finished"));
900            }
901            self.pos += 2;
902            self.col += 2;
903            None
904        };
905        self.sax.processing_instruction(&target, data.as_deref());
906        if let Some(p) = parent.filter(|_| !self.no_tree) {
907            let n = self.doc.alloc(NodeKind::Pi, target);
908            self.doc.node_mut(n).content = data.unwrap_or_default();
909            self.doc.xml_add_child(p, n);
910        }
911        Ok(false)
912    }
913
914    fn parse_xml_decl_rest(&mut self) -> Result<(), XmlError> {
915        self.skip_s()?;
916        // version
917        if !self.starts_with(b"version") {
918            return Err(self.err(XML_ERR_XMLDECL_NOT_FINISHED, "XML declaration version required"));
919        }
920        self.pos += 7;
921        self.col += 7;
922        self.skip_s()?;
923        self.expect_byte(b'=', XML_ERR_EQUAL_REQUIRED, "'=' required")?;
924        self.skip_s()?;
925        let ver = self.parse_quoted()?;
926        // VersionNum ::= '1.' [0-9]+ in 1.0 5th ed; libxml2 accepts the older
927        // [a-zA-Z0-9_.:-]+ form. Either way `1.0?` is not one.
928        if ver.is_empty()
929            || !ver
930                .chars()
931                .all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '.' | ':' | '-'))
932        {
933            return Err(self.err(XML_ERR_XMLDECL_NOT_FINISHED, "Invalid XML version value"));
934        }
935        self.doc.version = ver;
936        // S is required between the version info and whatever follows it;
937        // `version="1.0"encoding="UTF-8"` was accepted.
938        let had_s = self.require_s();
939        if self.starts_with(b"encoding") {
940            if !had_s {
941                return Err(self.err(XML_ERR_SPACE_REQUIRED, "Blank needed here"));
942            }
943            self.pos += 8;
944            self.col += 8;
945            self.skip_s()?;
946            self.expect_byte(b'=', XML_ERR_EQUAL_REQUIRED, "'=' required")?;
947            self.skip_s()?;
948            let enc = self.parse_quoted()?;
949            // EncName ::= [A-Za-z] ([A-Za-z0-9._] | '-')*
950            // Any string at all was accepted, including "_UTF-8" and "".
951            let mut cs = enc.chars();
952            let ok = cs.next().is_some_and(|c| c.is_ascii_alphabetic())
953                && cs.all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-'));
954            if !ok {
955                return Err(self.err(XML_ERR_ENCODING_NAME, "Invalid XML encoding name"));
956            }
957            self.doc.encoding = Some(enc);
958            self.skip_s()?;
959        }
960        if self.starts_with(b"standalone") {
961            if !had_s {
962                return Err(self.err(XML_ERR_SPACE_REQUIRED, "Blank needed here"));
963            }
964            self.pos += 10;
965            self.col += 10;
966            self.skip_s()?;
967            self.expect_byte(b'=', XML_ERR_EQUAL_REQUIRED, "'=' required")?;
968            self.skip_s()?;
969            let st = self.parse_quoted()?;
970            self.doc.standalone = match st.as_str() {
971                "yes" => Some(true),
972                "no" => Some(false),
973                _ => return Err(self.err(XML_ERR_XMLDECL_NOT_FINISHED, "standalone must be yes or no")),
974            };
975            self.skip_s()?;
976        }
977        if !self.starts_with(b"?>") {
978            return Err(self.err(XML_ERR_XMLDECL_NOT_FINISHED, "XML declaration not finished"));
979        }
980        self.pos += 2;
981        self.col += 2;
982        Ok(())
983    }
984
985    fn parse_quoted(&mut self) -> Result<String, XmlError> {
986        let q = self.peek_byte().ok_or_else(|| self.err(XML_ERR_LITERAL_NOT_FINISHED, "Quote expected"))?;
987        if q != b'\'' && q != b'"' {
988            return Err(self.err(XML_ERR_LITERAL_NOT_FINISHED, "Quote expected"));
989        }
990        self.bump_byte();
991        let mut s = String::new();
992        loop {
993            let c = self.bump_char()?.ok_or_else(|| self.err(XML_ERR_LITERAL_NOT_FINISHED, "Unterminated literal"))?;
994            if c as u8 == q && c.is_ascii() {
995                break;
996            }
997            // The shared literal reader: ATTLIST defaults, entity values,
998            // system and public identifiers, and the XML declaration all come
999            // through here, and none of them validated. A control byte in an
1000            // ATTLIST default was injected into every element that took the
1001            // default and written back as U+FFFD; C says "invalid character in
1002            // entity value" and stops.
1003            if !xml_is_char(c as u32) {
1004                return Err(self.err(XML_ERR_INVALID_CHAR, "invalid character in literal"));
1005            }
1006            s.push(c);
1007        }
1008        Ok(s)
1009    }
1010
1011    fn parse_cdata(&mut self, parent: Option<NodeId>) -> Result<(), XmlError> {
1012        // after "<![CDATA["
1013        let mut body = String::new();
1014        loop {
1015            if self.starts_with(b"]]>") {
1016                self.pos += 3;
1017                self.col += 3;
1018                break;
1019            }
1020            if self.eof() {
1021                return Err(self.err(XML_ERR_CDATA_NOT_FINISHED, "CDATA not finished"));
1022            }
1023            let c = self.bump_char()?.unwrap();
1024            // CDATA is unparsed, not unchecked: the character rule still
1025            // applies inside it.
1026            if !xml_is_char(c as u32) {
1027                return Err(self.err(XML_ERR_INVALID_CHAR, "invalid character in CDATA"));
1028            }
1029            body.push(c);
1030        }
1031        if (self.options & XML_PARSE_NOCDATA) != 0 {
1032            self.sax.characters(&body);
1033            if let Some(p) = parent.filter(|_| !self.no_tree) {
1034                let t = self.doc.alloc_unnamed(NodeKind::Text);
1035                self.doc.node_mut(t).content = body;
1036                self.doc.xml_add_child(p, t);
1037            }
1038        } else {
1039            self.sax.cdata_block(&body);
1040            if let Some(p) = parent.filter(|_| !self.no_tree) {
1041                let t = self.doc.alloc_unnamed(NodeKind::CData);
1042                self.doc.node_mut(t).content = body;
1043                self.doc.xml_add_child(p, t);
1044            }
1045        }
1046        Ok(())
1047    }
1048
1049    fn parse_reference(&mut self) -> Result<String, XmlError> {
1050        self.expect_byte(b'&', XML_ERR_ENTITYREF_NO_NAME, "& expected")?;
1051        if self.peek_byte() == Some(b'#') {
1052            self.bump_byte();
1053            // CharRef ::= '&#' [0-9]+ ';' | '&#x' [0-9a-fA-F]+ ';'
1054            // The marker is lowercase only; `&#X58;` is not a character
1055            // reference, and we were accepting it.
1056            let hex = self.peek_byte() == Some(b'x');
1057            if hex {
1058                self.bump_byte();
1059            } else if self.peek_byte() == Some(b'X') {
1060                return Err(self.err(
1061                    XML_ERR_INVALID_DEC_CHARREF,
1062                    "CharRef: invalid decimal value",
1063                ));
1064            }
1065            let mut digits = String::new();
1066            while let Some(b) = self.peek_byte() {
1067                let ok = if hex {
1068                    b.is_ascii_hexdigit()
1069                } else {
1070                    b.is_ascii_digit()
1071                };
1072                if !ok {
1073                    break;
1074                }
1075                digits.push(b as char);
1076                self.bump_byte();
1077            }
1078            if digits.is_empty() {
1079                return Err(self.err(
1080                    if hex { XML_ERR_INVALID_HEX_CHARREF } else { XML_ERR_INVALID_DEC_CHARREF },
1081                    "Invalid character reference",
1082                ));
1083            }
1084            self.expect_byte(b';', XML_ERR_ENTITYREF_SEMICOL_MISSING, "';' required")?;
1085            let val = if hex {
1086                u32::from_str_radix(&digits, 16).map_err(|_| {
1087                    self.err(XML_ERR_INVALID_HEX_CHARREF, "Invalid hex charref")
1088                })?
1089            } else {
1090                digits.parse::<u32>().map_err(|_| {
1091                    self.err(XML_ERR_INVALID_DEC_CHARREF, "Invalid decimal charref")
1092                })?
1093            };
1094            if !xml_is_char(val) {
1095                return Err(self.err(XML_ERR_INVALID_CHARREF, "Invalid character reference"));
1096            }
1097            return Ok(char::from_u32(val).unwrap().to_string());
1098        }
1099        let name = self.parse_name()?;
1100        self.expect_byte(b';', XML_ERR_ENTITYREF_SEMICOL_MISSING, "';' required")?;
1101        match name.as_str() {
1102            "lt" => Ok("<".into()),
1103            "gt" => Ok(">".into()),
1104            "amp" => Ok("&".into()),
1105            "apos" => Ok("'".into()),
1106            "quot" => Ok("\"".into()),
1107            _ => {
1108                let raw = self
1109                    .doc
1110                    .dtd
1111                    .as_ref()
1112                    .and_then(|d| d.entities.get(&name))
1113                    .cloned();
1114                if let Some(raw) = raw {
1115                    // The replacement was returned VERBATIM, so a nested
1116                    // reference landed in the tree as literal text and came
1117                    // back out escaped: `&b;&b;` became `&amp;b;&amp;b;`.
1118                    return self.expand_entity(&name, &raw, 0);
1119                }
1120                if self.recover {
1121                    // Recovering: keep the reference as written rather than
1122                    // losing the whole document over one unknown entity.
1123                    self.sax
1124                        .error(&format!("Entity '{name}' not defined"));
1125                    return Ok(format!("&{name};"));
1126                }
1127                Err(self.err(
1128                    XML_ERR_UNDECLARED_ENTITY,
1129                    format!("Entity '{name}' not defined"),
1130                ))
1131            }
1132        }
1133    }
1134
1135    /// The declared replacement text of the reference at the cursor, as
1136    /// STORED -- character references already expanded, entity references
1137    /// still written out.
1138    ///
1139    /// That distinction is the whole point. A character reference in an entity
1140    /// value is expanded when the declaration is read, so `<!ENTITY e
1141    /// "&#60;foo/>">` really does hold a '<' and really is markup. `&lt;` is
1142    /// bypassed and stays written out, so `<!ENTITY e "&lt;AB&gt;">` holds no
1143    /// markup at all and must come out as the three characters `<AB>`.
1144    /// Deciding on the fully expanded text cannot tell those apart.
1145    fn reference_raw_value(&self) -> Option<String> {
1146        let rest = self.input.get((self.pos + 1).min(self.input.len())..)?;
1147        let end = rest.iter().position(|b| *b == b';')?;
1148        let name = std::str::from_utf8(&rest[..end]).ok()?;
1149        self.doc.dtd.as_ref()?.entities.get(name).cloned()
1150    }
1151
1152    /// Parse an entity's replacement text as content and graft the result in.
1153    ///
1154    /// The replacement is parsed in isolation, wrapped in a synthetic root, so
1155    /// its well-formedness is checked as the "Well-Formed Parsed Entity"
1156    /// constraint requires -- a bare `&` or `<` arriving by way of a character
1157    /// reference in the declaration is an error, not text.
1158    ///
1159    /// A prefix declared on the REFERENCING element is not in scope for an
1160    /// isolated parse, so an undefined-prefix failure falls back to the old
1161    /// text behaviour rather than rejecting a document libxml2 accepts.
1162    fn splice_entity(&mut self, repl: &str, parent: NodeId) -> Result<(), XmlError> {
1163        let wrapped = format!("<rusty-xml-entity>{repl}</rusty-xml-entity>");
1164        let mut null = rusty_xml_sax::NullSax;
1165        let mut sub = Parser {
1166            input: wrapped.as_bytes(),
1167            pos: 0,
1168            line: self.line,
1169            col: self.col,
1170            options: self.options,
1171            old10: self.old10,
1172            depth: self.depth + 1,
1173            ns_stack: Vec::new(),
1174            sax: &mut null,
1175            doc: XmlDoc::with_node_capacity(Some("1.0"), 8),
1176            stack: Vec::new(),
1177            char_buf: String::new(),
1178            scratch_raw: Vec::new(),
1179            scratch_sax: Vec::new(),
1180            started: false,
1181            no_tree: false,
1182            recover: self.recover,
1183            // The nested expansion draws on the SAME budget, so an entity that
1184            // splices markup cannot buy itself a fresh allowance.
1185            entity_budget: self.entity_budget,
1186        };
1187        sub.doc.dtd = self.doc.dtd.clone();
1188        let r = sub.parse_document();
1189        self.entity_budget = sub.entity_budget;
1190        match r {
1191            Ok(()) => {
1192                if let Some(root) = sub.doc.xml_doc_get_root_element() {
1193                    self.doc.xml_copy_children_from(&sub.doc, root, parent);
1194                }
1195                Ok(())
1196            }
1197            Err(e) if e.code == XML_NS_ERR_UNDEFINED_NAMESPACE => {
1198                self.char_buf.push_str(repl);
1199                self.flush_chars(Some(parent))
1200            }
1201            Err(e) => Err(e),
1202        }
1203    }
1204
1205    /// Expand an entity's replacement text, resolving references inside it.
1206    ///
1207    /// Bounded twice, because recursion here IS the billion-laughs vector: by
1208    /// nesting depth, and by a byte budget proportional to the document.
1209    fn expand_entity(&mut self, name: &str, raw: &str, depth: u32) -> Result<String, XmlError> {
1210        const MAX_ENTITY_DEPTH: u32 = 40;
1211        if depth > MAX_ENTITY_DEPTH {
1212            return Err(self.err(
1213                XML_ERR_UNDECLARED_ENTITY,
1214                format!("Entity '{name}' nested too deeply"),
1215            ));
1216        }
1217        let b = raw.as_bytes();
1218        let mut out = String::with_capacity(raw.len());
1219        let mut i = 0usize;
1220        while i < b.len() {
1221            if b[i] != b'&' {
1222                let start = i;
1223                while i < b.len() && b[i] != b'&' {
1224                    i += 1;
1225                }
1226                out.push_str(&raw[start..i]);
1227                continue;
1228            }
1229            let Some(semi) = raw[i..].find(';').map(|k| i + k) else {
1230                out.push('&');
1231                i += 1;
1232                continue;
1233            };
1234            let inner = raw[i + 1..semi].to_string();
1235            if let Some(rest) = inner.strip_prefix('#') {
1236                let (radix, digits) = match rest.strip_prefix(['x', 'X']) {
1237                    Some(h) => (16u32, h),
1238                    None => (10u32, rest),
1239                };
1240                match u32::from_str_radix(digits, radix).ok().and_then(char::from_u32) {
1241                    Some(c) => out.push(c),
1242                    None => {
1243                        return Err(self.err(
1244                            XML_ERR_INVALID_CHAR,
1245                            format!("Invalid character reference in entity '{name}'"),
1246                        ))
1247                    }
1248                }
1249                i = semi + 1;
1250                continue;
1251            }
1252            let replacement: Option<String> = match inner.as_str() {
1253                "lt" => Some("<".into()),
1254                "gt" => Some(">".into()),
1255                "amp" => Some("&".into()),
1256                "apos" => Some("'".into()),
1257                "quot" => Some('"'.to_string()),
1258                other => {
1259                    let nested = self
1260                        .doc
1261                        .dtd
1262                        .as_ref()
1263                        .and_then(|d| d.entities.get(other))
1264                        .cloned();
1265                    match nested {
1266                        Some(r) => Some(self.expand_entity(other, &r, depth + 1)?),
1267                        None => None,
1268                    }
1269                }
1270            };
1271            match replacement {
1272                Some(r) => {
1273                    if r.len() > self.entity_budget {
1274                        return Err(self.err(
1275                            XML_ERR_INTERNAL_ERROR,
1276                            "Maximum entity amplification exceeded",
1277                        ));
1278                    }
1279                    self.entity_budget -= r.len();
1280                    out.push_str(&r);
1281                }
1282                None if self.recover => out.push_str(&raw[i..=semi]),
1283                None => {
1284                    return Err(self.err(
1285                        XML_ERR_UNDECLARED_ENTITY,
1286                        format!("Entity '{inner}' not defined"),
1287                    ))
1288                }
1289            }
1290            i = semi + 1;
1291        }
1292        Ok(out)
1293    }
1294
1295    fn parse_att_value(&mut self) -> Result<(String, usize), XmlError> {
1296        let q = self.peek_byte().ok_or_else(|| {
1297            self.err(XML_ERR_ATTRIBUTE_WITHOUT_VALUE, "Attribute value expected")
1298        })?;
1299        if q != b'\'' && q != b'"' {
1300            return Err(self.err(XML_ERR_ATTRIBUTE_WITHOUT_VALUE, "Attribute value expected"));
1301        }
1302        self.bump_byte();
1303        let start = self.pos;
1304        let mut val = String::new();
1305        loop {
1306            // Same run trick as character data: most attribute values are plain
1307            // ASCII with no reference and no whitespace needing normalisation.
1308            {
1309                let rs = self.pos;
1310                let mut i = rs;
1311                while i < self.input.len() {
1312                    let b = self.input[i];
1313                    if b == q || b == b'<' || b == b'&' || b < 0x20 || b >= 0x80 {
1314                        break;
1315                    }
1316                    i += 1;
1317                }
1318                if i > rs {
1319                    if let Ok(run) = std::str::from_utf8(&self.input[rs..i]) {
1320                        // Almost every value is a single run, so this is the
1321                        // exact size and the String never grows.
1322                        if val.is_empty() {
1323                            val.reserve_exact(i - rs);
1324                        }
1325                        val.push_str(run);
1326                        self.col += (i - rs) as u32;
1327                        self.pos = i;
1328                        continue;
1329                    }
1330                }
1331            }
1332            if self.peek_byte() == Some(q) {
1333                self.bump_byte();
1334                break;
1335            }
1336            if self.eof() {
1337                return Err(self.err(XML_ERR_LITERAL_NOT_FINISHED, "Unterminated attribute"));
1338            }
1339            if self.peek_byte() == Some(b'<') {
1340                return Err(self.err(XML_ERR_LT_IN_ATTRIBUTE, "'<' in attribute value"));
1341            }
1342            if self.peek_byte() == Some(b'&') {
1343                let raw = self.reference_raw_value();
1344                let repl = self.parse_reference()?;
1345                // "No < in Attribute Values": the constraint is about the
1346                // replacement TEXT, not just what is written in the document,
1347                // so an entity carrying one is caught here and nowhere else.
1348                if raw.as_deref().is_some_and(|r| r.contains('<')) {
1349                    return Err(self.err(
1350                        XML_ERR_LT_IN_ATTRIBUTE,
1351                        "'<' in entity is not allowed in attribute values",
1352                    ));
1353                }
1354                val.push_str(&repl);
1355                continue;
1356            }
1357            let c = self.bump_char()?.unwrap();
1358            // Character data is validated; attribute values were not, so a
1359            // stray C0 control byte sailed straight through and the writer
1360            // quietly substituted U+FFFD for it on the way out -- a silently
1361            // corrupted value where C reports "invalid character in attribute
1362            // value". Found by the round-trip check: escaping it on the first
1363            // save and not the second made serialization non-idempotent.
1364            if !xml_is_char(c as u32) {
1365                return Err(self.err(
1366                    XML_ERR_INVALID_CHAR,
1367                    "invalid character in attribute value",
1368                ));
1369            }
1370            // AttValue: physical whitespace → space
1371            if c == '\n' || c == '\t' {
1372                val.push(' ');
1373            } else {
1374                val.push(c);
1375            }
1376        }
1377        Ok((val, start))
1378    }
1379
1380    fn skip_doctype(&mut self) -> Result<(), XmlError> {
1381        // after "<!DOCTYPE"
1382        self.skip_s()?;
1383        let name = self.parse_name()?;
1384        self.skip_s()?;
1385        let mut public_id = None;
1386        let mut system_id = None;
1387        if self.starts_with(b"SYSTEM") {
1388            self.pos += 6;
1389            self.col += 6;
1390            if !self.require_s() {
1391                return Err(self.err(XML_ERR_SPACE_REQUIRED, "Space required after 'SYSTEM'"));
1392            }
1393            if !matches!(self.peek_byte(), Some(b'"') | Some(b'\'')) {
1394                return Err(self.err(
1395                    XML_ERR_LITERAL_NOT_FINISHED,
1396                    "SystemLiteral \" or ' expected",
1397                ));
1398            }
1399            system_id = Some(self.parse_quoted()?);
1400        } else if self.starts_with(b"PUBLIC") {
1401            self.pos += 6;
1402            self.col += 6;
1403            if !self.require_s() {
1404                return Err(self.err(XML_ERR_SPACE_REQUIRED, "Space required after 'PUBLIC'"));
1405            }
1406            let pid = self.parse_quoted()?;
1407            // PubidLiteral is a restricted character set, not free text.
1408            if let Some(bad) = pid.chars().find(|c| !crate::dtd::is_pubid_char(*c)) {
1409                return Err(self.err(
1410                    XML_ERR_INVALID_CHAR,
1411                    format!("Invalid character 0x{:X} in public identifier", bad as u32),
1412                ));
1413            }
1414            public_id = Some(pid);
1415            // ExternalID ::= 'PUBLIC' S PubidLiteral S SystemLiteral -- the
1416            // space between the two literals is required, and `"a""b"` was
1417            // taken happily.
1418            if !self.require_s() {
1419                return Err(self.err(
1420                    XML_ERR_SPACE_REQUIRED,
1421                    "Space required after the Public Identifier",
1422                ));
1423            }
1424            if !matches!(self.peek_byte(), Some(b'"') | Some(b'\'')) {
1425                return Err(self.err(
1426                    XML_ERR_LITERAL_NOT_FINISHED,
1427                    "SystemLiteral \" or ' expected",
1428                ));
1429            }
1430            system_id = Some(self.parse_quoted()?);
1431        }
1432        self.skip_s()?;
1433        let mut int_subset = None;
1434        if self.peek_byte() == Some(b'[') {
1435            self.bump_byte();
1436            let start = self.pos;
1437            let mut depth = 1i32;
1438            let mut in_quote: Option<u8> = None;
1439            while depth > 0 {
1440                // A comment's contents are not markup, and an apostrophe in
1441                // one is not a quote. `<!--NOTE: XML doesn't specify...-->`
1442                // opened a quote that never closed, so the scan swallowed the
1443                // rest of the document and reported "Unterminated DOCTYPE" at
1444                // the last line.
1445                if in_quote.is_none() && self.starts_with(b"<!--") {
1446                    match self.input[self.pos..]
1447                        .windows(3)
1448                        .position(|w| w == b"-->")
1449                    {
1450                        Some(off) => {
1451                            for _ in 0..off + 3 {
1452                                self.bump_byte();
1453                            }
1454                            continue;
1455                        }
1456                        None => {
1457                            return Err(
1458                                self.err(XML_ERR_COMMENT_NOT_FINISHED, "Comment not finished")
1459                            );
1460                        }
1461                    }
1462                }
1463                let b = self.bump_byte().ok_or_else(|| {
1464                    self.err(XML_ERR_DOCUMENT_END, "Unterminated DOCTYPE")
1465                })?;
1466                if let Some(q) = in_quote {
1467                    if b == q {
1468                        in_quote = None;
1469                    }
1470                    continue;
1471                }
1472                match b {
1473                    b'\'' | b'"' => in_quote = Some(b),
1474                    b'[' => depth += 1,
1475                    b']' => depth -= 1,
1476                    _ => {}
1477                }
1478            }
1479            // exclude the closing ']'
1480            int_subset = Some(String::from_utf8_lossy(&self.input[start..self.pos.saturating_sub(1)]).into_owned());
1481        }
1482        self.skip_s()?;
1483        self.expect_byte(b'>', XML_ERR_GT_REQUIRED, "'>' required")?;
1484        let mut dtd = if let Some(ref subset) = int_subset {
1485            // unwrap_or_default() here discarded EVERY internal-subset
1486            // error: a malformed DTD silently became an empty one, so the
1487            // entities and ATTLIST defaults it declared just vanished and the
1488            // failure surfaced later as a bogus "entity not defined". Recovery
1489            // mode still tolerates it, because that is what recovery is for.
1490            match crate::dtd::parse_dtd_subset(subset, self.old10) {
1491                Ok(d) => d,
1492                Err(_) if self.recover => rusty_xml_tree::XmlDtd::default(),
1493                Err(e) => return Err(e),
1494            }
1495        } else {
1496            rusty_xml_tree::XmlDtd::default()
1497        };
1498        dtd.name = Some(name);
1499        dtd.public_id = public_id;
1500        dtd.system_id = system_id;
1501        dtd.int_subset = int_subset;
1502        self.doc.dtd = Some(dtd);
1503        Ok(())
1504    }
1505
1506    /// Parse a start tag and everything that belongs to it: attributes,
1507    /// namespace frame, the SAX start event and the element node.
1508    ///
1509    /// Returns the open element, or `None` if it was `<x/>` and is already
1510    /// closed. Split out of `parse_element` so the content loop can be driven
1511    /// by an explicit stack instead of by recursion.
1512    fn open_element(&mut self, parent: NodeId) -> Result<Option<OpenElem>, XmlError> {
1513        self.depth += 1;
1514        let cap = if (self.options & XML_PARSE_HUGE) != 0 {
1515            MAX_DEPTH_HUGE
1516        } else {
1517            MAX_DEPTH
1518        };
1519        if self.depth > cap {
1520            return Err(self.err(XML_ERR_INTERNAL_ERROR, "Excessive element nesting"));
1521        }
1522        self.expect_byte(b'<', XML_ERR_LT_REQUIRED, "'<' required")?;
1523        let qname = self.parse_name()?;
1524        let (prefix, local) = Self::split_qname(&qname).map_err(|mut e| {
1525            e.line = self.line;
1526            e.col = self.col;
1527            e
1528        })?;
1529
1530        // Reused across elements: a fresh Vec per element allocated once and
1531        // then grew 1-2-4-8 as the attributes were pushed.
1532        let mut raw_attrs: Vec<RawAttr> = std::mem::take(&mut self.scratch_raw);
1533        raw_attrs.clear();
1534        loop {
1535            let before_ws = self.pos;
1536            self.skip_s()?;
1537            let had_ws = self.pos > before_ws;
1538            if self.starts_with(b"/>") || self.peek_byte() == Some(b'>') {
1539                break;
1540            }
1541            // `att1="a"att2="b"` was accepted; the grammar requires S
1542            // between attributes, and C calls it an attributes construct
1543            // error.
1544            if !had_ws {
1545                return Err(self.err(
1546                    XML_ERR_SPACE_REQUIRED,
1547                    "attributes construct error",
1548                ));
1549            }
1550            let an = self.parse_name()?;
1551            self.skip_s()?;
1552            self.expect_byte(b'=', XML_ERR_EQUAL_REQUIRED, "'=' required")?;
1553            self.skip_s()?;
1554            let (value, value_off) = self.parse_att_value()?;
1555            let colon = match Self::split_qname(&an).map_err(|mut e| {
1556                e.line = self.line;
1557                e.col = self.col;
1558                e
1559            })? {
1560                (None, _) => None,
1561                (Some(pfx), _) => Some(pfx.len()),
1562            };
1563            raw_attrs.push(RawAttr {
1564                qname: an,
1565                value,
1566                value_off,
1567                colon,
1568            });
1569        }
1570        let empty = if self.starts_with(b"/>") {
1571            self.pos += 2;
1572            self.col += 2;
1573            true
1574        } else {
1575            self.expect_byte(b'>', XML_ERR_GT_REQUIRED, "'>' required")?;
1576            false
1577        };
1578
1579        let mut ns_frame: Vec<(Option<String>, String)> = Vec::new();
1580        for a in &raw_attrs {
1581            let (ap, al) = a.parts();
1582            if ap.is_none() && al == "xmlns" {
1583                if !a.value.is_empty() && !Self::uri_has_scheme(&a.value) {
1584                    let msg = format!("xmlns: URI {} is not absolute\n", a.value);
1585                    self.sax.warning(&msg);
1586                }
1587                ns_frame.push((None, a.value.clone()));
1588            } else if ap == Some("xmlns") {
1589                if !a.value.is_empty()
1590                    && !Self::uri_has_scheme(&a.value)
1591                    && (self.options & XML_PARSE_PEDANTIC) != 0
1592                {
1593                    let msg = format!("xmlns:{}: URI {} is not absolute\n", al, a.value);
1594                    self.sax.warning(&msg);
1595                }
1596                // Namespaces in XML 1.0 reserves `xml` and `xmlns` and forbids
1597                // undeclaring a prefix. None of this was checked.
1598                if al == "xml" {
1599                    if a.value != XML_NS {
1600                        return Err(self.err(
1601                            XML_NS_ERR_UNDEFINED_NAMESPACE,
1602                            "xml namespace prefix mapped to wrong URI",
1603                        ));
1604                    }
1605                } else if a.value == XML_NS {
1606                    return Err(self.err(
1607                        XML_NS_ERR_UNDEFINED_NAMESPACE,
1608                        "xml namespace URI mapped to wrong prefix",
1609                    ));
1610                }
1611                if al == "xmlns" {
1612                    return Err(self.err(
1613                        XML_NS_ERR_UNDEFINED_NAMESPACE,
1614                        "redefinition of the xmlns prefix is forbidden",
1615                    ));
1616                }
1617                if a.value == XMLNS_NS {
1618                    return Err(self.err(
1619                        XML_NS_ERR_UNDEFINED_NAMESPACE,
1620                        "reuse of the xmlns namespace name is forbidden",
1621                    ));
1622                }
1623                // Prefix undeclaring (`xmlns:p=""`) is XML 1.1 only.
1624                if a.value.is_empty() {
1625                    return Err(self.err(
1626                        XML_NS_ERR_UNDEFINED_NAMESPACE,
1627                        "Empty XML namespace is not allowed",
1628                    ));
1629                }
1630                ns_frame.push((Some(al.to_string()), a.value.clone()));
1631            }
1632        }
1633        // The frame is pushed, not copied; the stack owns it and both later
1634        // readers borrow it back from there.
1635        self.ns_stack.push(ns_frame);
1636
1637        let elem_uri = self.lookup_ns(prefix);
1638        if prefix.is_some() && elem_uri.is_none() {
1639            // Scraped markup is full of prefixes nobody declared, and an
1640            // undeclared prefix is a namespace error, not a well-formedness
1641            // one. libxml2 reports it and carries on -- exits zero -- so
1642            // refusing the document made us reject input C accepts, which is a
1643            // worse trade than any number of conformance cases. It also cost a
1644            // valid case outright: `<A.-:x/>` is a legal Name whose colon is
1645            // not a prefix at all.
1646            self.sax.error(&format!(
1647                "Namespace prefix {} is not defined",
1648                prefix.unwrap_or_default()
1649            ));
1650        }
1651
1652        let mut seen_keys: std::collections::HashSet<(Option<String>, String)> =
1653            std::collections::HashSet::new();
1654        let mut sax_attrs: Vec<SaxAttr> = std::mem::take(&mut self.scratch_sax);
1655        sax_attrs.clear();
1656        for idx in 0..raw_attrs.len() {
1657            // Own the parts first; SaxAttr needs them owned anyway, so this
1658            // costs nothing extra and releases the borrow on raw_attrs.
1659            let (ap_owned, al_owned, is_ns, value_off) = {
1660                let a = &mut raw_attrs[idx];
1661                let voff = a.value_off;
1662                match a.colon {
1663                    // Unprefixed: the local name IS the whole QName, so move it
1664                    // instead of allocating a second copy of the same bytes.
1665                    None => {
1666                        let is_ns = a.qname == "xmlns";
1667                        (None, std::mem::take(&mut a.qname), is_ns, voff)
1668                    }
1669                    Some(i) => {
1670                        let is_ns = &a.qname[..i] == "xmlns";
1671                        (
1672                            Some(a.qname[..i].to_string()),
1673                            a.qname[i + 1..].to_string(),
1674                            is_ns,
1675                            voff,
1676                        )
1677                    }
1678                }
1679            };
1680            if is_ns {
1681                continue;
1682            }
1683            let uri = if ap_owned.is_some() {
1684                let u = self.lookup_ns(ap_owned.as_deref());
1685                if u.is_none() && !self.recover {
1686                    return Err(self.err(
1687                        XML_NS_ERR_UNDEFINED_NAMESPACE,
1688                        format!("Undefined namespace prefix {}", ap_owned.clone().unwrap()),
1689                    ));
1690                }
1691                u
1692            } else {
1693                None
1694            };
1695            // The attributes already accepted ARE the "seen" set -- a separate
1696            // vector of copies was allocated per element to hold the same thing.
1697            // Linear over the accepted attributes is fine for the handful a real
1698            // element carries, but it is O(n^2) and an element with 16,000
1699            // attributes took 185 ms. Switch to a set once it could matter.
1700            if sax_attrs.len() < 32 {
1701                if sax_attrs
1702                    .iter()
1703                    .any(|s| s.uri.as_deref() == uri.as_deref() && s.local == al_owned)
1704                {
1705                    return Err(self.err(XML_ERR_ATTRIBUTE_REDEFINED, "Attribute redefined"));
1706                }
1707            } else {
1708                if seen_keys.is_empty() {
1709                    for a in sax_attrs.iter() {
1710                        seen_keys.insert((a.uri.clone(), a.local.clone()));
1711                    }
1712                }
1713                if !seen_keys.insert((uri.clone(), al_owned.clone())) {
1714                    return Err(self.err(XML_ERR_ATTRIBUTE_REDEFINED, "Attribute redefined"));
1715                }
1716            }
1717            sax_attrs.push(SaxAttr {
1718                local: al_owned,
1719                prefix: ap_owned,
1720                uri,
1721                // Moved out of raw_attrs rather than copied: one String clone
1722                // per attribute in the document.
1723                value: std::mem::take(&mut raw_attrs[idx].value),
1724                value_input_off: Some(value_off),
1725            });
1726        }
1727
1728        let frame: &[(Option<String>, String)] =
1729            self.ns_stack.last().map(Vec::as_slice).unwrap_or(&[]);
1730        self.sax.start_element_ns(
1731            local,
1732            prefix,
1733            elem_uri.as_deref(),
1734            frame,
1735            &sax_attrs,
1736            0,
1737        );
1738
1739        let elem = self.doc.alloc(NodeKind::Element, local);
1740        self.doc.node_mut(elem).prefix = prefix.map(str::to_string);
1741        self.doc.node_mut(elem).ns_uri = elem_uri;
1742        for i in 0..self.ns_stack.last().map_or(0, Vec::len) {
1743            let (p, u) = {
1744                let f = self.ns_stack.last().unwrap();
1745                (f[i].0.clone(), f[i].1.clone())
1746            };
1747            self.doc.push_ns_def(elem, p, u);
1748        }
1749        if self.no_tree {
1750            sax_attrs.clear();
1751        } else {
1752            for a in sax_attrs.drain(..) {
1753                let uri = a.uri;
1754                let aid = self.doc.add_attr_owned(elem, a.local, a.prefix, a.value);
1755                self.doc.node_mut(aid).ns_uri = uri;
1756            }
1757        }
1758        self.doc.xml_add_child(parent, elem);
1759
1760        raw_attrs.clear();
1761        sax_attrs.clear();
1762        self.scratch_raw = raw_attrs;
1763        self.scratch_sax = sax_attrs;
1764
1765        if empty {
1766            let uri = self.doc.node(elem).ns_uri.as_deref();
1767            self.sax.end_element_ns(local, prefix, uri);
1768            self.ns_stack.pop();
1769            self.depth -= 1;
1770            return Ok(None);
1771        }
1772
1773        self.stack.push(elem);
1774        Ok(Some(OpenElem { qname, elem }))
1775    }
1776
1777    /// Consume the end tag of an open element and emit its SAX end event.
1778    ///
1779    /// `local` and `prefix` are re-derived from the stored QName rather than
1780    /// carried across the call: `split_qname` borrows, so this allocates
1781    /// nothing.
1782    fn close_element(&mut self, open: &OpenElem) -> Result<(), XmlError> {
1783        let (prefix, local) = Self::split_qname(&open.qname).map_err(|mut e| {
1784            e.line = self.line;
1785            e.col = self.col;
1786            e
1787        })?;
1788        if !self.starts_with(b"</") {
1789            return Err(self.err(
1790                XML_ERR_TAG_NOT_FINISHED,
1791                format!("Premature end of data in tag {local}"),
1792            ));
1793        }
1794        self.pos += 2;
1795        self.col += 2;
1796        let (ea, eb) = self.parse_name_span()?;
1797        self.skip_s()?;
1798        self.expect_byte(b'>', XML_ERR_GT_REQUIRED, "'>' required")?;
1799        if &self.input[ea..eb] != open.qname.as_bytes() {
1800            let end_name = String::from_utf8_lossy(&self.input[ea..eb]).into_owned();
1801            let qname = &open.qname;
1802            return Err(self.err(
1803                XML_ERR_TAG_NAME_MISMATCH,
1804                format!("Opening and ending tag mismatch: {qname} and {end_name}"),
1805            ));
1806        }
1807        let uri = self.doc.node(open.elem).ns_uri.as_deref();
1808        self.sax.end_element_ns(local, prefix, uri);
1809        self.ns_stack.pop();
1810        self.stack.pop();
1811        self.depth -= 1;
1812        Ok(())
1813    }
1814
1815    /// Parse one complete element. Calls into the iterative content loop, so
1816    /// this is the only frame a document of any depth costs.
1817    fn parse_element(&mut self, parent: NodeId) -> Result<(), XmlError> {
1818        let Some(open) = self.open_element(parent)? else {
1819            return Ok(());
1820        };
1821        self.parse_content(open.elem)?;
1822        self.close_element(&open)
1823    }
1824
1825    /// Parse the content of `parent` and of every element nested inside it.
1826    ///
1827    /// This used to recurse into `parse_element`, which recursed back here, so
1828    /// document nesting consumed the call stack -- about 1.4 KB per level in
1829    /// release and 22 KB in debug, and a stack overflow aborts the process
1830    /// rather than returning an error. The element context was already heap
1831    /// state (`stack`, `ns_stack`); only the call frames were not. Now the
1832    /// descent is an explicit stack and the depth of a document costs no stack
1833    /// at all.
1834    fn parse_content(&mut self, parent: NodeId) -> Result<(), XmlError> {
1835        let mut open: Vec<OpenElem> = Vec::new();
1836        self.parse_content_inner(parent, &mut open, false, false)?;
1837        Ok(())
1838    }
1839
1840    fn parse_document(&mut self) -> Result<(), XmlError> {
1841        self.parse_prolog()?;
1842        self.parse_element(NodeId::DOCUMENT)?;
1843        self.parse_epilog()
1844    }
1845
1846    /// Rebuild a parser over a fresh buffer from saved state.
1847    fn resume(
1848        input: &'a [u8],
1849        options: i32,
1850        sax: &'a mut dyn SaxHandler,
1851        st: PushState,
1852    ) -> Self {
1853        let _ = st.root_closed;
1854        Parser {
1855            input,
1856            pos: 0,
1857            line: st.line,
1858            col: st.col,
1859            options,
1860            old10: (options & XML_PARSE_OLD10) != 0,
1861            depth: st.depth,
1862            ns_stack: st.ns_stack,
1863            sax,
1864            doc: st.doc,
1865            stack: st.stack,
1866            char_buf: st.char_buf,
1867            no_tree: (options & XML_PARSE_NO_TREE) != 0,
1868            recover: (options & XML_PARSE_RECOVER) != 0,
1869        // libxml2 bounds entity amplification at a small multiple of the input
1870        // for the same reason; without a bound, nesting is a bomb.
1871        entity_budget: input.len().saturating_mul(10).max(1 << 16),
1872            scratch_raw: Vec::new(),
1873            scratch_sax: Vec::new(),
1874            started: true,
1875        }
1876    }
1877
1878    fn suspend(self, open: Vec<OpenElem>, root_closed: bool) -> PushState {
1879        PushState {
1880            root_closed,
1881            doc: self.doc,
1882            ns_stack: self.ns_stack,
1883            stack: self.stack,
1884            open,
1885            char_buf: self.char_buf,
1886            line: self.line,
1887            col: self.col,
1888            depth: self.depth,
1889        }
1890    }
1891
1892    /// True when the remaining bytes are a proper prefix of a construct and we
1893    /// cannot tell what it is without more input.
1894    ///
1895    /// Only consulted while streaming. Character data is never "incomplete":
1896    /// the run scanner stops at `<`, `&` and `]`, and pending text is kept in
1897    /// `char_buf` rather than flushed, so more of it can simply be appended.
1898    fn incomplete_construct(&self) -> bool {
1899        let r = &self.input[self.pos..];
1900        fn has(h: &[u8], n: &[u8]) -> bool {
1901            h.len() >= n.len() && h.windows(n.len()).any(|w| w == n)
1902        }
1903        // A tag ends at the first '>' that is not inside an attribute value.
1904        fn tag_complete(r: &[u8]) -> bool {
1905            let mut quote: Option<u8> = None;
1906            for &b in &r[1..] {
1907                match quote {
1908                    Some(q) if b == q => quote = None,
1909                    Some(_) => {}
1910                    None => match b {
1911                        b'"' | 0x27 => quote = Some(b),
1912                        b'>' => return true,
1913                        _ => {}
1914                    },
1915                }
1916            }
1917            false
1918        }
1919        match r.first() {
1920            Some(b'<') => {
1921                if r.len() < 2 {
1922                    return true;
1923                }
1924                if r.starts_with(b"<!--") {
1925                    return !has(&r[4..], b"-->");
1926                }
1927                if r.starts_with(b"<![CDATA[") {
1928                    return !has(&r[9..], b"]]>");
1929                }
1930                if r.starts_with(b"<?") {
1931                    return !has(&r[2..], b"?>");
1932                }
1933                // `<!` could still become a comment, CDATA or a doctype.
1934                if r[1] == b'!' && r.len() < 9 {
1935                    return true;
1936                }
1937                !tag_complete(r)
1938            }
1939            Some(b'&') => !r.contains(&b';'),
1940            // `]` might yet become `]]>`.
1941            Some(b']') => r.len() < 3,
1942            // XML 1.0 2.11 folds CRLF to a single LF. A trailing CR gives no
1943            // way to know whether the LF follows, and guessing turned every
1944            // CRLF that landed on a chunk boundary into two newlines.
1945            Some(0x0D) => r.len() < 2,
1946            // A multi-byte character split across chunks: the lead byte says
1947            // how many continuation bytes belong to it, and without them the
1948            // scalar cannot be decoded.
1949            Some(&b0) => {
1950                let need = if b0 < 0x80 {
1951                    1
1952                } else if b0 >> 5 == 0b110 {
1953                    2
1954                } else if b0 >> 4 == 0b1110 {
1955                    3
1956                } else if b0 >> 3 == 0b11110 {
1957                    4
1958                } else {
1959                    1
1960                };
1961                r.len() < need
1962            }
1963            None => false,
1964        }
1965    }
1966
1967    /// The content loop, with the open-element stack supplied by the caller so
1968    /// it can survive between chunks.
1969    ///
1970    /// With `stop_at_eof`, running out of input is not an error: parsing stops
1971    /// at the last SAFE BOUNDARY -- the top of the loop, where we sit between
1972    /// content items rather than half way through a tag -- and returns that
1973    /// position. Pending character data stays in `char_buf` rather than being
1974    /// flushed, so a text run split across two chunks still produces one event
1975    /// and the push parser matches a whole-document parse exactly.
1976    fn parse_content_inner(
1977        &mut self,
1978        parent: NodeId,
1979        open: &mut Vec<OpenElem>,
1980        stop_at_eof: bool,
1981        stop_when_empty: bool,
1982    ) -> Result<usize, XmlError> {
1983        // The element the caller asked us to fill. When the innermost element
1984        // closes and nothing else is open, content belongs to THIS again --
1985        // falling back to the mutable `parent` would name the element that had
1986        // just been closed.
1987        let outer = parent;
1988        let mut parent = open.last().map(|f| f.elem).unwrap_or(parent);
1989        loop {
1990            let safe = self.pos;
1991            if self.eof() {
1992                if stop_at_eof {
1993                    return Ok(safe);
1994                }
1995                self.flush_chars(Some(parent))?;
1996                if let Some(o) = open.last() {
1997                    let (_, local) = Self::split_qname(&o.qname).unwrap_or((None, &o.qname));
1998                    return Err(self.err(
1999                        XML_ERR_TAG_NOT_FINISHED,
2000                        format!("Premature end of data in tag {local}"),
2001                    ));
2002                }
2003                return Ok(safe);
2004            }
2005            // Without the whole of a construct in hand we cannot tell what it
2006            // is, so stop here and wait for more input.
2007            if stop_at_eof && self.incomplete_construct() {
2008                return Ok(safe);
2009            }
2010            if self.starts_with(b"</") {
2011                self.flush_chars(Some(parent))?;
2012                // Our own end tag closes the innermost open element; when
2013                // nothing is open it belongs to the caller.
2014                match open.pop() {
2015                    Some(o) => {
2016                        self.close_element(&o)?;
2017                        // Streaming starts with the root already open, so an
2018                        // empty stack means the root just closed and the
2019                        // epilogue is the driver's job.
2020                        if stop_when_empty && open.is_empty() {
2021                            return Ok(self.pos);
2022                        }
2023                        parent = open.last().map(|f| f.elem).unwrap_or(outer);
2024                        continue;
2025                    }
2026                    None => return Ok(safe),
2027                }
2028            }
2029            if self.starts_with(b"<!--") {
2030                self.flush_chars(Some(parent))?;
2031                self.pos += 4;
2032                self.col += 4;
2033                self.parse_comment(Some(parent))?;
2034                continue;
2035            }
2036            if self.starts_with(b"<![CDATA[") {
2037                self.flush_chars(Some(parent))?;
2038                self.pos += 9;
2039                self.col += 9;
2040                self.parse_cdata(Some(parent))?;
2041                continue;
2042            }
2043            if self.starts_with(b"<?") {
2044                self.flush_chars(Some(parent))?;
2045                self.pos += 2;
2046                self.col += 2;
2047                self.parse_pi(Some(parent), false)?;
2048                continue;
2049            }
2050            let lead = self.peek_byte();
2051            if lead == Some(b'<') {
2052                self.flush_chars(Some(parent))?;
2053                if let Some(o) = self.open_element(parent)? {
2054                    parent = o.elem;
2055                    open.push(o);
2056                }
2057                continue;
2058            }
2059            if lead == Some(b'&') {
2060                // A CHARACTER reference is character data by definition -- it
2061                // cannot introduce markup -- so it belongs in the run it sits
2062                // in, not in a text node of its own.
2063                //
2064                // Flushing around it split `&#65; &#66;` into three nodes, and
2065                // the middle one was whitespace-only, so XML_PARSE_NOBLANKS
2066                // deleted it: `A B` came back as `AB`. Losing a space between
2067                // two character references is silent text corruption. It also
2068                // costs a node and an allocation per reference.
2069                //
2070                // A general entity still gets its own node: its replacement can
2071                // contain markup and is not ours to inline here.
2072                let is_charref = self.input.get(self.pos + 1) == Some(&b'#');
2073                if is_charref {
2074                    let repl = self.parse_reference()?;
2075                    self.char_buf.push_str(&repl);
2076                } else {
2077                    self.flush_chars(Some(parent))?;
2078                    let raw = self.reference_raw_value();
2079                    let repl = self.parse_reference()?;
2080                    // Replacement text containing markup has to become NODES.
2081                    // It was inserted as text and escaped on the way out, so
2082                    // `<!ENTITY e "<b>x</b>">` put the literal string
2083                    // `&lt;b&gt;x&lt;/b&gt;` in the tree: structure lost, and
2084                    // DTD validation saw character data where an element was
2085                    // declared.
2086                    // Only a DTD-declared entity whose STORED text holds
2087                    // markup is re-parsed. The predefined five produce literal
2088                    // characters, and splicing those as markup broke every
2089                    // document that so much as mentions `&lt;`.
2090                    // '&' as well as '<': `<!ENTITY e "&#38;">` stores a bare
2091                    // ampersand, and a bare ampersand in content is an error,
2092                    // not text. Re-parsing the replacement is what says so.
2093                    if raw
2094                        .as_deref()
2095                        .is_some_and(|r| r.contains('<') || r.contains('&'))
2096                        && !self.no_tree
2097                    {
2098                        self.splice_entity(raw.as_deref().unwrap(), parent)?;
2099                    } else {
2100                        self.char_buf.push_str(&repl);
2101                        self.flush_chars(Some(parent))?;
2102                    }
2103                }
2104                continue;
2105            }
2106            if self.starts_with(b"]]>") {
2107                return Err(self.err(XML_ERR_MISPLACED_CDATA_END, "Misplaced CDATA end"));
2108            }
2109            // Character data is the bulk of most documents and is almost all
2110            // ordinary ASCII. Take it in one run: one bounds test and one
2111            // push_str instead of a decode, two peeks and a push per character.
2112            {
2113                let start = self.pos;
2114                let mut i = start;
2115                while i < self.input.len() {
2116                    let b = self.input[i];
2117                    let plain = b == 0x09 || (0x20..0x80).contains(&b);
2118                    if !plain || b == b'<' || b == b'&' || b == b']' {
2119                        break;
2120                    }
2121                    i += 1;
2122                }
2123                if i > start {
2124                    // Every byte in the run is ASCII and a legal XML character.
2125                    match std::str::from_utf8(&self.input[start..i]) {
2126                        Ok(run) => {
2127                            self.char_buf.push_str(run);
2128                            self.col += (i - start) as u32;
2129                            self.pos = i;
2130                            continue;
2131                        }
2132                        Err(_) => {}
2133                    }
2134                }
2135            }
2136            let c = self.bump_char()?.unwrap();
2137            if !xml_is_char(c as u32) {
2138                return Err(self.err(XML_ERR_INVALID_CHAR, "Invalid character"));
2139            }
2140            self.char_buf.push(c);
2141        }
2142    }
2143
2144    fn parse_misc(&mut self, parent: NodeId) -> Result<(), XmlError> {
2145        loop {
2146            self.skip_s()?;
2147            if self.starts_with(b"<!--") {
2148                self.pos += 4;
2149                self.col += 4;
2150                self.parse_comment(Some(parent))?;
2151                continue;
2152            }
2153            if self.starts_with(b"<?") {
2154                self.pos += 2;
2155                self.col += 2;
2156                self.parse_pi(Some(parent), false)?;
2157                continue;
2158            }
2159            break;
2160        }
2161        Ok(())
2162    }
2163
2164    /// Everything before the root element's start tag: BOM, XML declaration,
2165    /// misc, doctype. Split out so the push parser can reach the root without
2166    /// committing to parse the whole document in one go.
2167    fn parse_prolog(&mut self) -> Result<(), XmlError> {
2168        if self.starts_with(&[0xef, 0xbb, 0xbf]) {
2169            self.pos += 3;
2170        }
2171        self.sax.set_document_locator();
2172        self.sax.start_document();
2173        self.started = true;
2174
2175        // XMLDecl must be at the start (after BOM). `<?xml-stylesheet` is a PI.
2176        if self.starts_with(b"<?xml") {
2177            let save_pos = self.pos;
2178            let save_col = self.col;
2179            let save_line = self.line;
2180            self.pos += 5;
2181            self.col += 5;
2182            match self.peek_byte() {
2183                Some(b) if b < 0x80 && crate::chvalid::xml_is_blank(b as u32) => {
2184                    self.parse_xml_decl_rest()?;
2185                }
2186                _ => {
2187                    self.pos = save_pos;
2188                    self.col = save_col;
2189                    self.line = save_line;
2190                    self.pos += 2;
2191                    self.col += 2;
2192                    self.parse_pi(Some(NodeId::DOCUMENT), false)?;
2193                }
2194            }
2195        }
2196
2197        self.parse_misc(NodeId::DOCUMENT)?;
2198        if self.starts_with(b"<!DOCTYPE") {
2199            self.pos += 9;
2200            self.col += 9;
2201            self.skip_doctype()?;
2202            self.parse_misc(NodeId::DOCUMENT)?;
2203        }
2204
2205        if self.peek_byte() != Some(b'<') {
2206            return Err(self.err(XML_ERR_DOCUMENT_EMPTY, "Document is empty"));
2207        }
2208        Ok(())
2209    }
2210
2211    /// Everything after the root element: trailing misc, then end-of-document.
2212    fn parse_epilog(&mut self) -> Result<(), XmlError> {
2213        self.parse_misc(NodeId::DOCUMENT)?;
2214        self.skip_s()?;
2215        if !self.eof() {
2216            return Err(self.err(XML_ERR_EXTRA_CONTENT, "Extra content at the end of the document"));
2217        }
2218        self.sax.end_document();
2219        Ok(())
2220    }
2221}
2222
2223fn parse_doc(
2224    buffer: &[u8],
2225    _url: Option<&str>,
2226    encoding: Option<&str>,
2227    options: i32,
2228    sax: &mut dyn SaxHandler,
2229) -> Result<XmlDoc, XmlError> {
2230    let (converted, enc_name) = crate::encoding::xml_convert_to_utf8_cow(buffer, encoding)?;
2231    parse_utf8(&converted, enc_name.as_deref(), options, sax)
2232}
2233
2234fn parse_utf8(
2235    buffer: &[u8],
2236    enc_name: Option<&str>,
2237    options: i32,
2238    sax: &mut dyn SaxHandler,
2239) -> Result<XmlDoc, XmlError> {
2240    let options = options | XML_PARSE_NONET | XML_PARSE_NO_XXE;
2241    let mut p = Parser {
2242        input: buffer,
2243        pos: 0,
2244        line: 1,
2245        col: 1,
2246        options,
2247        old10: (options & XML_PARSE_OLD10) != 0,
2248        depth: 0,
2249        ns_stack: Vec::new(),
2250        sax,
2251        // Reserving a full arena is the dominant cost of a no-tree parse --
2252        // pre-allocating a tree only to leave it empty.
2253        doc: XmlDoc::with_node_capacity(
2254            Some("1.0"),
2255            if (options & XML_PARSE_NO_TREE) != 0 {
2256                // Only element nodes are created in this mode, which measure
2257                // about one per 36 input bytes. Reserving for a full tree
2258                // wasted the arena; reserving nothing made it double instead.
2259                buffer.len() / 32
2260            } else {
2261                buffer.len() / 10
2262            },
2263        ),
2264        stack: Vec::new(),
2265        char_buf: String::new(),
2266        no_tree: (options & XML_PARSE_NO_TREE) != 0,
2267        recover: (options & XML_PARSE_RECOVER) != 0,
2268        // libxml2 bounds entity amplification at a small multiple of the input
2269        // for the same reason; without a bound, nesting is a bomb.
2270        entity_budget: buffer.len().saturating_mul(10).max(1 << 16),
2271        scratch_raw: Vec::new(),
2272        scratch_sax: Vec::new(),
2273        started: false,
2274    };
2275    match p.parse_document() {
2276        Ok(()) => {
2277            apply_dtd_defaults(&mut p.doc, buffer.len(), options)?;
2278            if p.doc.encoding.is_none() {
2279                if let Some(n) = enc_name {
2280                    if !n.eq_ignore_ascii_case("UTF-8") && !n.eq_ignore_ascii_case("US-ASCII") {
2281                        p.doc.encoding = Some(n.to_string());
2282                    }
2283                }
2284            }
2285            Ok(p.doc)
2286        }
2287        Err(e) => {
2288            if p.started {
2289                p.sax.end_document();
2290            }
2291            if (options & XML_PARSE_RECOVER) != 0 {
2292                // Hand back everything parsed before the failure. One bad byte
2293                // in a large document used to cost the caller all of it.
2294                p.sax.error(&e.message);
2295                return Ok(p.doc);
2296            }
2297            Err(e)
2298        }
2299    }
2300}
2301
2302fn apply_dtd_defaults(
2303    doc: &mut XmlDoc,
2304    input_len: usize,
2305    options: i32,
2306) -> Result<(), XmlError> {
2307    // Completing attributes from ATTLIST defaults is opt-in, as it is in C:
2308    // libxml2 does it for XML_PARSE_DTDATTR (xmllint --dtdattr) and not
2309    // otherwise -- not even for --valid. We did it unconditionally, so every
2310    // document with an ATTLIST default came back with attributes libxml2 would
2311    // not have added, which is a visible difference in the serialized output.
2312    if (options & XML_PARSE_DTDATTR) == 0 {
2313        return Ok(());
2314    }
2315    // The common cases -- no DTD, or a DTD carrying no ATTLIST default -- cost
2316    // nothing now. Testing before the clone matters: cloning the DTD copies
2317    // every entity and declaration in it.
2318    match &doc.dtd {
2319        None => return Ok(()),
2320        Some(d) => {
2321            if !d.attributes.values().any(|a| a.default_value.is_some()) {
2322                return Ok(());
2323            }
2324        }
2325    }
2326    // Defaulted attributes are an amplification vector: 13 KB with 200 ATTLIST
2327    // defaults expanded to 402,002 nodes (~74 MB) before this bound existed.
2328    // libxml2 caps entity amplification for the same reason. The budget is
2329    // generous enough that a real DTD never reaches it.
2330    // Sized from measurement, not taste: a real DTD-heavy document runs about
2331    // 0.18 defaulted attributes per input byte, while the amplification cases
2332    // run 12-30 per byte -- two orders of magnitude apart. One per input byte
2333    // sits in the gap, with a floor so small documents are never penalised and
2334    // a ceiling so a huge one cannot walk past it.
2335    let mut budget = input_len.max(65_536).min(5_000_000);
2336    let dtd = match doc.dtd.clone() {
2337        Some(d) => d,
2338        None => return Ok(()),
2339    };
2340    // Group the defaults by element name once. The previous form rescanned
2341    // every declaration for every element in the document, and allocated the
2342    // element's name each time round.
2343    let mut by_elem: std::collections::HashMap<&str, Vec<(&str, &str)>> =
2344        std::collections::HashMap::new();
2345    for ((elem, aname), ad) in &dtd.attributes {
2346        if let Some(v) = &ad.default_value {
2347            by_elem
2348                .entry(elem.as_str())
2349                .or_default()
2350                .push((aname.as_str(), v.as_str()));
2351        }
2352    }
2353    // dtd.attributes is a HashMap with a randomly seeded hasher, so without
2354    // this the defaulted attributes serialised in a DIFFERENT ORDER ON EVERY
2355    // RUN of the same binary. Any signature or digest over the saved tree --
2356    // C14N included -- has to be reproducible.
2357    for list in by_elem.values_mut() {
2358        list.sort_unstable_by(|a, b| a.0.cmp(b.0));
2359    }
2360    let n = doc.len();
2361    for i in 0..n {
2362        let id = NodeId(i as u32);
2363        if doc.kind(id) != NodeKind::Element {
2364            continue;
2365        }
2366        let Some(list) = by_elem.get(doc.name(id)) else {
2367            continue;
2368        };
2369        for (aname, v) in list.iter() {
2370            if doc.xml_get_prop(id, aname).is_none() {
2371                if budget == 0 {
2372                    return Err(XmlError::new(
2373                        XML_ERR_INTERNAL_ERROR,
2374                        "Maximum attribute-default amplification exceeded",
2375                        0,
2376                        0,
2377                    ));
2378                }
2379                budget -= 1;
2380                doc.xml_set_prop(id, aname, v);
2381            }
2382        }
2383    }
2384    Ok(())
2385}
2386
2387#[cfg(test)]
2388mod chvalid_tests {
2389    use crate::xml_is_char;
2390    use std::path::PathBuf;
2391
2392    #[test]
2393    fn xml_is_char_matches_c_bmp_dump() {
2394        let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
2395        p.pop();
2396        p.pop();
2397        p.push("corpora");
2398        p.push("xmlIsChar-bmp.bin");
2399        if !p.exists() {
2400            return;
2401        }
2402        let dump = std::fs::read(&p).expect("corpora/xmlIsChar-bmp.bin");
2403        assert_eq!(dump.len(), 65536);
2404        for i in 0u32..=0xffff {
2405            let want = dump[i as usize] != 0;
2406            let got = xml_is_char(i);
2407            assert_eq!(got, want, "xml_is_char({i:#x}) = {got}, C dump = {want}");
2408        }
2409    }
2410}