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;
35pub const XML_PARSE_NO_SYS_CATALOG: i32 = 1 << 25;
36pub const XML_PARSE_CATALOG_PI: i32 = 1 << 26;
37pub const XML_PARSE_SKIP_IDS: i32 = 1 << 27;
38
39const XML_NS: &str = "http://www.w3.org/XML/1998/namespace";
40const XMLNS_NS: &str = "http://www.w3.org/2000/xmlns/";
41
42const MAX_DEPTH: u32 = 256;
43const MAX_NAME: usize = 50_000;
44const MAX_TEXT: usize = 10_000_000;
45
46/// Safe defaults: no network, no XXE.
47pub fn default_parse_options() -> i32 {
48    XML_PARSE_NONET | XML_PARSE_NO_XXE
49}
50
51/// `xmlInitParser` — no process-global ctor in Rust.
52#[doc(alias = "xmlInitParser")]
53pub fn xml_init_parser() {}
54
55/// `xmlCleanupParser` — no-op.
56#[doc(alias = "xmlCleanupParser")]
57pub fn xml_cleanup_parser() {}
58
59/// Parser context (`xmlParserCtxt`).
60#[derive(Debug, Default)]
61pub struct XmlParserCtxt {
62    pub options: i32,
63    pub last_error: Option<XmlError>,
64    pub doc: Option<XmlDoc>,
65}
66
67/// `xmlNewParserCtxt`.
68#[doc(alias = "xmlNewParserCtxt")]
69pub fn xml_new_parser_ctxt() -> XmlParserCtxt {
70    XmlParserCtxt {
71        options: default_parse_options(),
72        last_error: None,
73        doc: None,
74    }
75}
76
77/// `xmlCtxtUseOptions`.
78#[doc(alias = "xmlCtxtUseOptions")]
79pub fn xml_ctxt_use_options(ctxt: &mut XmlParserCtxt, options: i32) -> i32 {
80    ctxt.options = options | XML_PARSE_NONET | XML_PARSE_NO_XXE;
81    0
82}
83
84/// `xmlCtxtSetOptions`.
85#[doc(alias = "xmlCtxtSetOptions")]
86pub fn xml_ctxt_set_options(ctxt: &mut XmlParserCtxt, options: i32) -> i32 {
87    xml_ctxt_use_options(ctxt, options)
88}
89
90/// `xmlCtxtGetOptions`.
91#[doc(alias = "xmlCtxtGetOptions")]
92pub fn xml_ctxt_get_options(ctxt: &XmlParserCtxt) -> i32 {
93    ctxt.options
94}
95
96/// `xmlCtxtGetLastError`.
97#[doc(alias = "xmlCtxtGetLastError")]
98pub fn xml_ctxt_get_last_error(ctxt: &XmlParserCtxt) -> Option<&XmlError> {
99    ctxt.last_error.as_ref()
100}
101
102/// `xmlCtxtGetDocument`.
103#[doc(alias = "xmlCtxtGetDocument")]
104pub fn xml_ctxt_get_document(ctxt: &XmlParserCtxt) -> Option<&XmlDoc> {
105    ctxt.doc.as_ref()
106}
107
108/// `xmlReadMemory`.
109#[doc(alias = "xmlReadMemory")]
110pub fn xml_read_memory(
111    buffer: &[u8],
112    url: Option<&str>,
113    encoding: Option<&str>,
114    options: i32,
115) -> Result<XmlDoc, XmlError> {
116    let mut rec = rusty_xml_sax::SaxRecorder::new();
117    parse_doc(buffer, url, encoding, options, &mut rec)
118}
119
120/// `xmlReadDoc`.
121#[doc(alias = "xmlReadDoc")]
122pub fn xml_read_doc(
123    cur: &str,
124    url: Option<&str>,
125    encoding: Option<&str>,
126    options: i32,
127) -> Result<XmlDoc, XmlError> {
128    xml_read_memory(cur.as_bytes(), url, encoding, options)
129}
130
131/// `xmlReadFile`.
132#[doc(alias = "xmlReadFile")]
133pub fn xml_read_file(
134    filename: &str,
135    encoding: Option<&str>,
136    options: i32,
137) -> Result<XmlDoc, XmlError> {
138    let bytes = std::fs::read(filename).map_err(|e| {
139        XmlError::new(XML_ERR_DOCUMENT_START, e.to_string(), 0, 0)
140    })?;
141    xml_read_memory(&bytes, Some(filename), encoding, options)
142}
143
144/// `xmlCtxtReadMemory`.
145#[doc(alias = "xmlCtxtReadMemory")]
146pub fn xml_ctxt_read_memory(
147    ctxt: &mut XmlParserCtxt,
148    buffer: &[u8],
149    url: Option<&str>,
150    encoding: Option<&str>,
151    options: i32,
152) -> Result<XmlDoc, XmlError> {
153    let opts = if options != 0 { options } else { ctxt.options };
154    match xml_read_memory(buffer, url, encoding, opts) {
155        Ok(doc) => {
156            ctxt.doc = Some(doc.clone());
157            ctxt.last_error = None;
158            Ok(doc)
159        }
160        Err(e) => {
161            ctxt.last_error = Some(e.clone());
162            Err(e)
163        }
164    }
165}
166
167/// Parse and record SAX events (for the event-exact gate).
168pub fn xml_sax_parse_memory(
169    buffer: &[u8],
170    options: i32,
171    sax: &mut dyn SaxHandler,
172) -> Result<XmlDoc, XmlError> {
173    parse_doc(buffer, None, None, options, sax)
174}
175
176/// Push parser context (`xmlCreatePushParserCtxt`).
177pub struct XmlPushParserCtxt {
178    buf: Vec<u8>,
179    options: i32,
180    url: Option<String>,
181    encoding: Option<String>,
182    doc: Option<XmlDoc>,
183    last_error: Option<XmlError>,
184}
185
186/// `xmlCreatePushParserCtxt`.
187#[doc(alias = "xmlCreatePushParserCtxt")]
188pub fn xml_create_push_parser_ctxt(
189    chunk: &[u8],
190    url: Option<&str>,
191    encoding: Option<&str>,
192    options: i32,
193) -> XmlPushParserCtxt {
194    XmlPushParserCtxt {
195        buf: chunk.to_vec(),
196        options: options | XML_PARSE_NONET | XML_PARSE_NO_XXE,
197        url: url.map(str::to_string),
198        encoding: encoding.map(str::to_string),
199        doc: None,
200        last_error: None,
201    }
202}
203
204/// `xmlParseChunk`. `terminate != 0` finishes the document.
205#[doc(alias = "xmlParseChunk")]
206pub fn xml_parse_chunk(
207    ctxt: &mut XmlPushParserCtxt,
208    chunk: &[u8],
209    terminate: i32,
210) -> Result<Option<XmlDoc>, XmlError> {
211    ctxt.buf.extend_from_slice(chunk);
212    if terminate == 0 {
213        return Ok(None);
214    }
215    match xml_read_memory(
216        &ctxt.buf,
217        ctxt.url.as_deref(),
218        ctxt.encoding.as_deref(),
219        ctxt.options,
220    ) {
221        Ok(doc) => {
222            ctxt.doc = Some(doc.clone());
223            Ok(Some(doc))
224        }
225        Err(e) => {
226            ctxt.last_error = Some(e.clone());
227            Err(e)
228        }
229    }
230}
231
232/// `xmlReadIO` — caller-supplied read callback, no network.
233#[doc(alias = "xmlReadIO")]
234pub fn xml_read_io<F>(
235    mut read: F,
236    url: Option<&str>,
237    encoding: Option<&str>,
238    options: i32,
239) -> Result<XmlDoc, XmlError>
240where
241    F: FnMut(&mut [u8]) -> Result<usize, std::io::Error>,
242{
243    let mut buf = Vec::new();
244    let mut tmp = [0u8; 4096];
245    loop {
246        let n = read(&mut tmp).map_err(|e| XmlError::new(XML_ERR_DOCUMENT_START, e.to_string(), 0, 0))?;
247        if n == 0 {
248            break;
249        }
250        buf.extend_from_slice(&tmp[..n]);
251    }
252    xml_read_memory(&buf, url, encoding, options)
253}
254
255/// `xmlCtxtReset`.
256#[doc(alias = "xmlCtxtReset")]
257pub fn xml_ctxt_reset(ctxt: &mut XmlParserCtxt) {
258    ctxt.doc = None;
259    ctxt.last_error = None;
260}
261
262struct Parser<'a> {
263    input: &'a [u8],
264    pos: usize,
265    line: u32,
266    col: u32,
267    options: i32,
268    old10: bool,
269    depth: u32,
270    ns_stack: Vec<Vec<(Option<String>, String)>>,
271    sax: &'a mut dyn SaxHandler,
272    doc: XmlDoc,
273    stack: Vec<NodeId>,
274    char_buf: String,
275    started: bool,
276}
277
278impl<'a> Parser<'a> {
279    fn err(&self, code: i32, msg: impl Into<String>) -> XmlError {
280        XmlError::new(code, msg, self.line, self.col)
281    }
282
283    fn eof(&self) -> bool {
284        self.pos >= self.input.len()
285    }
286
287    fn peek_byte(&self) -> Option<u8> {
288        self.input.get(self.pos).copied()
289    }
290
291    fn starts_with(&self, s: &[u8]) -> bool {
292        self.input[self.pos..].starts_with(s)
293    }
294
295    fn bump_byte(&mut self) -> Option<u8> {
296        let b = self.peek_byte()?;
297        self.pos += 1;
298        if b == b'\n' {
299            self.line += 1;
300            self.col = 1;
301        } else {
302            self.col += 1;
303        }
304        Some(b)
305    }
306
307    /// Next Unicode scalar with XML 1.0 §2.11 EOL: `\r\n` / `\r` → `\n`.
308    fn peek_char(&self) -> Result<Option<char>, XmlError> {
309        if self.eof() {
310            return Ok(None);
311        }
312        let rest = &self.input[self.pos..];
313        if rest[0] == b'\r' {
314            return Ok(Some('\n'));
315        }
316        let s = std::str::from_utf8(rest).map_err(|_| {
317            XmlError::new(XML_ERR_INVALID_CHAR, "Invalid UTF-8", self.line, self.col)
318        })?;
319        Ok(s.chars().next())
320    }
321
322    fn bump_char(&mut self) -> Result<Option<char>, XmlError> {
323        let c = match self.peek_char()? {
324            None => return Ok(None),
325            Some(c) => c,
326        };
327        if self.peek_byte() == Some(b'\r') {
328            self.bump_byte();
329            if self.peek_byte() == Some(b'\n') {
330                self.bump_byte();
331            }
332            return Ok(Some('\n'));
333        }
334        let n = c.len_utf8();
335        for _ in 0..n {
336            self.bump_byte();
337        }
338        Ok(Some(c))
339    }
340
341    fn skip_s(&mut self) -> Result<(), XmlError> {
342        while let Some(c) = self.peek_char()? {
343            if crate::chvalid::xml_is_blank(c as u32) {
344                self.bump_char()?;
345            } else {
346                break;
347            }
348        }
349        Ok(())
350    }
351
352    fn expect_byte(&mut self, b: u8, code: i32, msg: &str) -> Result<(), XmlError> {
353        if self.peek_byte() != Some(b) {
354            return Err(self.err(code, msg));
355        }
356        self.bump_byte();
357        Ok(())
358    }
359
360    fn parse_name(&mut self) -> Result<String, XmlError> {
361        let c = self.peek_char()?.ok_or_else(|| self.err(XML_ERR_NAME_REQUIRED, "Name expected"))?;
362        if !xml_is_name_start_char(c as u32, self.old10) {
363            return Err(self.err(XML_ERR_NAME_REQUIRED, "Name expected"));
364        }
365        let mut s = String::new();
366        s.push(self.bump_char()?.unwrap());
367        while let Some(c) = self.peek_char()? {
368            if xml_is_name_char(c as u32, self.old10) {
369                if s.len() >= MAX_NAME && (self.options & XML_PARSE_HUGE) == 0 {
370                    return Err(self.err(XML_ERR_NAME_REQUIRED, "Name too long"));
371                }
372                s.push(self.bump_char()?.unwrap());
373            } else {
374                break;
375            }
376        }
377        Ok(s)
378    }
379
380    fn split_qname(name: &str) -> Result<(Option<String>, String), XmlError> {
381        let mut parts = name.split(':');
382        let a = parts.next().unwrap();
383        match parts.next() {
384            None => Ok((None, a.to_string())),
385            Some(b) => {
386                if parts.next().is_some() || a.is_empty() || b.is_empty() {
387                    return Err(XmlError::new(
388                        XML_NS_ERR_QNAME,
389                        format!("Invalid QName {name}"),
390                        0,
391                        0,
392                    ));
393                }
394                Ok((Some(a.to_string()), b.to_string()))
395            }
396        }
397    }
398
399    fn lookup_ns(&self, prefix: Option<&str>) -> Option<String> {
400        if prefix == Some("xml") {
401            return Some(XML_NS.into());
402        }
403        if prefix == Some("xmlns") {
404            return Some(XMLNS_NS.into());
405        }
406        for frame in self.ns_stack.iter().rev() {
407            for (p, uri) in frame.iter().rev() {
408                if p.as_deref() == prefix {
409                    return Some(uri.clone());
410                }
411            }
412        }
413        None
414    }
415
416    fn uri_has_scheme(uri: &str) -> bool {
417        let bytes = uri.as_bytes();
418        if bytes.is_empty() {
419            return false;
420        }
421        if !bytes[0].is_ascii_alphabetic() {
422            return false;
423        }
424        let mut i = 1;
425        while i < bytes.len() {
426            let b = bytes[i];
427            if b == b':' {
428                return true;
429            }
430            if b.is_ascii_alphanumeric() || b == b'+' || b == b'-' || b == b'.' {
431                i += 1;
432            } else {
433                return false;
434            }
435        }
436        false
437    }
438
439    fn flush_chars(&mut self, parent: Option<NodeId>) -> Result<(), XmlError> {
440        if self.char_buf.is_empty() {
441            return Ok(());
442        }
443        if self.char_buf.len() > MAX_TEXT && (self.options & XML_PARSE_HUGE) == 0 {
444            return Err(self.err(XML_ERR_INVALID_CHAR, "Text too long"));
445        }
446        let skip_blank = (self.options & XML_PARSE_NOBLANKS) != 0
447            && self.char_buf.chars().all(|c| crate::chvalid::xml_is_blank(c as u32));
448        if !skip_blank {
449            self.sax.characters(&self.char_buf);
450            if let Some(p) = parent {
451                let t = self.doc.alloc(NodeKind::Text, "#text");
452                self.doc.node_mut(t).content = self.char_buf.clone();
453                self.doc.xml_add_child(p, t);
454            }
455        }
456        self.char_buf.clear();
457        Ok(())
458    }
459
460    fn parse_comment(&mut self, parent: Option<NodeId>) -> Result<(), XmlError> {
461        // called after seeing "<!--"
462        let mut body = String::new();
463        loop {
464            if self.starts_with(b"-->") {
465                self.pos += 3;
466                self.col += 3;
467                break;
468            }
469            if self.eof() {
470                return Err(self.err(XML_ERR_COMMENT_NOT_FINISHED, "Comment not finished"));
471            }
472            if self.starts_with(b"--") {
473                return Err(self.err(XML_ERR_HYPHEN_IN_COMMENT, "Double hyphen in comment"));
474            }
475            let c = self.bump_char()?.unwrap();
476            if !xml_is_char(c as u32) {
477                return Err(self.err(XML_ERR_INVALID_CHAR, "Invalid character"));
478            }
479            body.push(c);
480        }
481        self.sax.comment(&body);
482        if let Some(p) = parent {
483            let n = self.doc.alloc(NodeKind::Comment, "#comment");
484            self.doc.node_mut(n).content = body;
485            self.doc.xml_add_child(p, n);
486        }
487        Ok(())
488    }
489
490    fn parse_pi(&mut self, parent: Option<NodeId>, xml_decl_ok: bool) -> Result<bool, XmlError> {
491        // called after seeing "<?"
492        let target = self.parse_name()?;
493        if target.eq_ignore_ascii_case("xml") {
494            if xml_decl_ok {
495                return self.parse_xml_decl_rest().map(|_| true);
496            }
497            return Err(self.err(XML_ERR_RESERVED_XML_NAME, "Reserved PI target xml"));
498        }
499        let data = if matches!(self.peek_char()?, Some(c) if crate::chvalid::xml_is_blank(c as u32)) {
500            self.skip_s()?;
501            let mut d = String::new();
502            loop {
503                if self.starts_with(b"?>") {
504                    self.pos += 2;
505                    self.col += 2;
506                    break;
507                }
508                if self.eof() {
509                    return Err(self.err(XML_ERR_PI_NOT_FINISHED, "PI not finished"));
510                }
511                d.push(self.bump_char()?.unwrap());
512            }
513            Some(d)
514        } else {
515            if !self.starts_with(b"?>") {
516                return Err(self.err(XML_ERR_PI_NOT_FINISHED, "PI not finished"));
517            }
518            self.pos += 2;
519            self.col += 2;
520            None
521        };
522        self.sax.processing_instruction(&target, data.as_deref());
523        if let Some(p) = parent {
524            let n = self.doc.alloc(NodeKind::Pi, target);
525            self.doc.node_mut(n).content = data.unwrap_or_default();
526            self.doc.xml_add_child(p, n);
527        }
528        Ok(false)
529    }
530
531    fn parse_xml_decl_rest(&mut self) -> Result<(), XmlError> {
532        self.skip_s()?;
533        // version
534        if !self.starts_with(b"version") {
535            return Err(self.err(XML_ERR_XMLDECL_NOT_FINISHED, "XML declaration version required"));
536        }
537        self.pos += 7;
538        self.col += 7;
539        self.skip_s()?;
540        self.expect_byte(b'=', XML_ERR_EQUAL_REQUIRED, "'=' required")?;
541        self.skip_s()?;
542        let ver = self.parse_quoted()?;
543        self.doc.version = ver;
544        self.skip_s()?;
545        if self.starts_with(b"encoding") {
546            self.pos += 8;
547            self.col += 8;
548            self.skip_s()?;
549            self.expect_byte(b'=', XML_ERR_EQUAL_REQUIRED, "'=' required")?;
550            self.skip_s()?;
551            let enc = self.parse_quoted()?;
552            self.doc.encoding = Some(enc);
553            self.skip_s()?;
554        }
555        if self.starts_with(b"standalone") {
556            self.pos += 10;
557            self.col += 10;
558            self.skip_s()?;
559            self.expect_byte(b'=', XML_ERR_EQUAL_REQUIRED, "'=' required")?;
560            self.skip_s()?;
561            let st = self.parse_quoted()?;
562            self.doc.standalone = match st.as_str() {
563                "yes" => Some(true),
564                "no" => Some(false),
565                _ => return Err(self.err(XML_ERR_XMLDECL_NOT_FINISHED, "standalone must be yes or no")),
566            };
567            self.skip_s()?;
568        }
569        if !self.starts_with(b"?>") {
570            return Err(self.err(XML_ERR_XMLDECL_NOT_FINISHED, "XML declaration not finished"));
571        }
572        self.pos += 2;
573        self.col += 2;
574        Ok(())
575    }
576
577    fn parse_quoted(&mut self) -> Result<String, XmlError> {
578        let q = self.peek_byte().ok_or_else(|| self.err(XML_ERR_LITERAL_NOT_FINISHED, "Quote expected"))?;
579        if q != b'\'' && q != b'"' {
580            return Err(self.err(XML_ERR_LITERAL_NOT_FINISHED, "Quote expected"));
581        }
582        self.bump_byte();
583        let mut s = String::new();
584        loop {
585            let c = self.bump_char()?.ok_or_else(|| self.err(XML_ERR_LITERAL_NOT_FINISHED, "Unterminated literal"))?;
586            if c as u8 == q && c.is_ascii() {
587                break;
588            }
589            s.push(c);
590        }
591        Ok(s)
592    }
593
594    fn parse_cdata(&mut self, parent: Option<NodeId>) -> Result<(), XmlError> {
595        // after "<![CDATA["
596        let mut body = String::new();
597        loop {
598            if self.starts_with(b"]]>") {
599                self.pos += 3;
600                self.col += 3;
601                break;
602            }
603            if self.eof() {
604                return Err(self.err(XML_ERR_CDATA_NOT_FINISHED, "CDATA not finished"));
605            }
606            body.push(self.bump_char()?.unwrap());
607        }
608        if (self.options & XML_PARSE_NOCDATA) != 0 {
609            self.sax.characters(&body);
610            if let Some(p) = parent {
611                let t = self.doc.alloc(NodeKind::Text, "#text");
612                self.doc.node_mut(t).content = body;
613                self.doc.xml_add_child(p, t);
614            }
615        } else {
616            self.sax.cdata_block(&body);
617            if let Some(p) = parent {
618                let t = self.doc.alloc(NodeKind::CData, "#cdata-section");
619                self.doc.node_mut(t).content = body;
620                self.doc.xml_add_child(p, t);
621            }
622        }
623        Ok(())
624    }
625
626    fn parse_reference(&mut self) -> Result<String, XmlError> {
627        self.expect_byte(b'&', XML_ERR_ENTITYREF_NO_NAME, "& expected")?;
628        if self.peek_byte() == Some(b'#') {
629            self.bump_byte();
630            let hex = self.peek_byte() == Some(b'x') || self.peek_byte() == Some(b'X');
631            if hex {
632                self.bump_byte();
633            }
634            let mut digits = String::new();
635            while let Some(b) = self.peek_byte() {
636                let ok = if hex {
637                    b.is_ascii_hexdigit()
638                } else {
639                    b.is_ascii_digit()
640                };
641                if !ok {
642                    break;
643                }
644                digits.push(b as char);
645                self.bump_byte();
646            }
647            if digits.is_empty() {
648                return Err(self.err(
649                    if hex { XML_ERR_INVALID_HEX_CHARREF } else { XML_ERR_INVALID_DEC_CHARREF },
650                    "Invalid character reference",
651                ));
652            }
653            self.expect_byte(b';', XML_ERR_ENTITYREF_SEMICOL_MISSING, "';' required")?;
654            let val = if hex {
655                u32::from_str_radix(&digits, 16).map_err(|_| {
656                    self.err(XML_ERR_INVALID_HEX_CHARREF, "Invalid hex charref")
657                })?
658            } else {
659                digits.parse::<u32>().map_err(|_| {
660                    self.err(XML_ERR_INVALID_DEC_CHARREF, "Invalid decimal charref")
661                })?
662            };
663            if !xml_is_char(val) {
664                return Err(self.err(XML_ERR_INVALID_CHARREF, "Invalid character reference"));
665            }
666            return Ok(char::from_u32(val).unwrap().to_string());
667        }
668        let name = self.parse_name()?;
669        self.expect_byte(b';', XML_ERR_ENTITYREF_SEMICOL_MISSING, "';' required")?;
670        match name.as_str() {
671            "lt" => Ok("<".into()),
672            "gt" => Ok(">".into()),
673            "amp" => Ok("&".into()),
674            "apos" => Ok("'".into()),
675            "quot" => Ok("\"".into()),
676            _ => {
677                if let Some(dtd) = &self.doc.dtd {
678                    if let Some(repl) = dtd.entities.get(&name) {
679                        return Ok(repl.clone());
680                    }
681                }
682                Err(self.err(
683                    XML_ERR_UNDECLARED_ENTITY,
684                    format!("Entity '{name}' not defined"),
685                ))
686            }
687        }
688    }
689
690    fn parse_att_value(&mut self) -> Result<(String, usize), XmlError> {
691        let q = self.peek_byte().ok_or_else(|| {
692            self.err(XML_ERR_ATTRIBUTE_WITHOUT_VALUE, "Attribute value expected")
693        })?;
694        if q != b'\'' && q != b'"' {
695            return Err(self.err(XML_ERR_ATTRIBUTE_WITHOUT_VALUE, "Attribute value expected"));
696        }
697        self.bump_byte();
698        let start = self.pos;
699        let mut val = String::new();
700        loop {
701            if self.peek_byte() == Some(q) {
702                self.bump_byte();
703                break;
704            }
705            if self.eof() {
706                return Err(self.err(XML_ERR_LITERAL_NOT_FINISHED, "Unterminated attribute"));
707            }
708            if self.peek_byte() == Some(b'<') {
709                return Err(self.err(XML_ERR_LT_IN_ATTRIBUTE, "'<' in attribute value"));
710            }
711            if self.peek_byte() == Some(b'&') {
712                val.push_str(&self.parse_reference()?);
713                continue;
714            }
715            let c = self.bump_char()?.unwrap();
716            // AttValue: physical whitespace → space
717            if c == '\n' || c == '\t' {
718                val.push(' ');
719            } else {
720                val.push(c);
721            }
722        }
723        Ok((val, start))
724    }
725
726    fn skip_doctype(&mut self) -> Result<(), XmlError> {
727        // after "<!DOCTYPE"
728        self.skip_s()?;
729        let name = self.parse_name()?;
730        self.skip_s()?;
731        let mut public_id = None;
732        let mut system_id = None;
733        if self.starts_with(b"SYSTEM") {
734            self.pos += 6;
735            self.col += 6;
736            self.skip_s()?;
737            system_id = Some(self.parse_quoted()?);
738        } else if self.starts_with(b"PUBLIC") {
739            self.pos += 6;
740            self.col += 6;
741            self.skip_s()?;
742            public_id = Some(self.parse_quoted()?);
743            self.skip_s()?;
744            system_id = Some(self.parse_quoted()?);
745        }
746        self.skip_s()?;
747        let mut int_subset = None;
748        if self.peek_byte() == Some(b'[') {
749            self.bump_byte();
750            let start = self.pos;
751            let mut depth = 1i32;
752            let mut in_quote: Option<u8> = None;
753            while depth > 0 {
754                let b = self.bump_byte().ok_or_else(|| {
755                    self.err(XML_ERR_DOCUMENT_END, "Unterminated DOCTYPE")
756                })?;
757                if let Some(q) = in_quote {
758                    if b == q {
759                        in_quote = None;
760                    }
761                    continue;
762                }
763                match b {
764                    b'\'' | b'"' => in_quote = Some(b),
765                    b'[' => depth += 1,
766                    b']' => depth -= 1,
767                    _ => {}
768                }
769            }
770            // exclude the closing ']'
771            int_subset = Some(String::from_utf8_lossy(&self.input[start..self.pos.saturating_sub(1)]).into_owned());
772        }
773        self.skip_s()?;
774        self.expect_byte(b'>', XML_ERR_GT_REQUIRED, "'>' required")?;
775        let mut dtd = if let Some(ref subset) = int_subset {
776            crate::dtd::parse_dtd_subset(subset).unwrap_or_default()
777        } else {
778            rusty_xml_tree::XmlDtd::default()
779        };
780        dtd.name = Some(name);
781        dtd.public_id = public_id;
782        dtd.system_id = system_id;
783        dtd.int_subset = int_subset;
784        self.doc.dtd = Some(dtd);
785        Ok(())
786    }
787
788    fn parse_element(&mut self, parent: NodeId) -> Result<(), XmlError> {
789        self.depth += 1;
790        if self.depth > MAX_DEPTH && (self.options & XML_PARSE_HUGE) == 0 {
791            return Err(self.err(XML_ERR_INTERNAL_ERROR, "Excessive element nesting"));
792        }
793        self.expect_byte(b'<', XML_ERR_LT_REQUIRED, "'<' required")?;
794        let qname = self.parse_name()?;
795        let (prefix, local) = Self::split_qname(&qname).map_err(|mut e| {
796            e.line = self.line;
797            e.col = self.col;
798            e
799        })?;
800
801        struct RawAttr {
802            qname: String,
803            value: String,
804            value_off: usize,
805        }
806        let mut raw_attrs: Vec<RawAttr> = Vec::new();
807        loop {
808            self.skip_s()?;
809            if self.starts_with(b"/>") || self.peek_byte() == Some(b'>') {
810                break;
811            }
812            let an = self.parse_name()?;
813            self.skip_s()?;
814            self.expect_byte(b'=', XML_ERR_EQUAL_REQUIRED, "'=' required")?;
815            self.skip_s()?;
816            let (value, value_off) = self.parse_att_value()?;
817            raw_attrs.push(RawAttr {
818                qname: an,
819                value,
820                value_off,
821            });
822        }
823        let empty = if self.starts_with(b"/>") {
824            self.pos += 2;
825            self.col += 2;
826            true
827        } else {
828            self.expect_byte(b'>', XML_ERR_GT_REQUIRED, "'>' required")?;
829            false
830        };
831
832        let mut ns_frame: Vec<(Option<String>, String)> = Vec::new();
833        for a in &raw_attrs {
834            let (ap, al) = Self::split_qname(&a.qname).map_err(|mut e| {
835                e.line = self.line;
836                e.col = self.col;
837                e
838            })?;
839            if ap.is_none() && al == "xmlns" {
840                if !a.value.is_empty() && !Self::uri_has_scheme(&a.value) {
841                    let msg = format!("xmlns: URI {} is not absolute\n", a.value);
842                    self.sax.warning(&msg);
843                }
844                ns_frame.push((None, a.value.clone()));
845            } else if ap.as_deref() == Some("xmlns") {
846                if !a.value.is_empty()
847                    && !Self::uri_has_scheme(&a.value)
848                    && (self.options & XML_PARSE_PEDANTIC) != 0
849                {
850                    let msg = format!("xmlns:{}: URI {} is not absolute\n", al, a.value);
851                    self.sax.warning(&msg);
852                }
853                ns_frame.push((Some(al), a.value.clone()));
854            }
855        }
856        self.ns_stack.push(ns_frame.clone());
857
858        let elem_uri = self.lookup_ns(prefix.as_deref());
859        if prefix.is_some() && elem_uri.is_none() {
860            return Err(self.err(
861                XML_NS_ERR_UNDEFINED_NAMESPACE,
862                format!("Undefined namespace prefix {}", prefix.as_deref().unwrap()),
863            ));
864        }
865
866        let mut sax_attrs: Vec<SaxAttr> = Vec::new();
867        let mut seen: Vec<(Option<String>, String)> = Vec::new();
868        for a in &raw_attrs {
869            let (ap, al) = Self::split_qname(&a.qname).unwrap();
870            if (ap.is_none() && al == "xmlns") || ap.as_deref() == Some("xmlns") {
871                continue;
872            }
873            let uri = if ap.is_some() {
874                let u = self.lookup_ns(ap.as_deref());
875                if u.is_none() {
876                    return Err(self.err(
877                        XML_NS_ERR_UNDEFINED_NAMESPACE,
878                        format!("Undefined namespace prefix {}", ap.as_deref().unwrap()),
879                    ));
880                }
881                u
882            } else {
883                None
884            };
885            let key = (uri.clone(), al.clone());
886            if seen.iter().any(|s| s == &key) {
887                return Err(self.err(XML_ERR_ATTRIBUTE_REDEFINED, "Attribute redefined"));
888            }
889            seen.push(key);
890            sax_attrs.push(SaxAttr {
891                local: al,
892                prefix: ap,
893                uri,
894                value: a.value.clone(),
895                value_input_off: Some(a.value_off),
896            });
897        }
898
899        self.sax.start_element_ns(
900            &local,
901            prefix.as_deref(),
902            elem_uri.as_deref(),
903            &ns_frame,
904            &sax_attrs,
905            0,
906        );
907
908        let elem = self.doc.alloc(NodeKind::Element, local.clone());
909        self.doc.node_mut(elem).prefix = prefix.clone();
910        self.doc.node_mut(elem).ns_uri = elem_uri.clone();
911        for (p, u) in &ns_frame {
912            self.doc.push_ns_def(elem, p.clone(), u.clone());
913        }
914        for a in &sax_attrs {
915            let aid = self.doc.add_attr(elem, &a.local, a.prefix.as_deref(), &a.value);
916            self.doc.node_mut(aid).ns_uri = a.uri.clone();
917        }
918        self.doc.xml_add_child(parent, elem);
919
920        if empty {
921            self.sax.end_element_ns(&local, prefix.as_deref(), elem_uri.as_deref());
922            self.ns_stack.pop();
923            self.depth -= 1;
924            return Ok(());
925        }
926
927        self.stack.push(elem);
928        self.parse_content(elem)?;
929        if !self.starts_with(b"</") {
930            return Err(self.err(
931                XML_ERR_TAG_NOT_FINISHED,
932                format!("Premature end of data in tag {local}"),
933            ));
934        }
935        self.pos += 2;
936        self.col += 2;
937        let end_name = self.parse_name()?;
938        self.skip_s()?;
939        self.expect_byte(b'>', XML_ERR_GT_REQUIRED, "'>' required")?;
940        if end_name != qname {
941            return Err(self.err(
942                XML_ERR_TAG_NAME_MISMATCH,
943                format!("Opening and ending tag mismatch: {qname} and {end_name}"),
944            ));
945        }
946        self.sax.end_element_ns(&local, prefix.as_deref(), elem_uri.as_deref());
947        self.ns_stack.pop();
948        self.stack.pop();
949        self.depth -= 1;
950        Ok(())
951    }
952
953    fn parse_content(&mut self, parent: NodeId) -> Result<(), XmlError> {
954        loop {
955            if self.eof() {
956                self.flush_chars(Some(parent))?;
957                return Ok(());
958            }
959            if self.starts_with(b"</") {
960                self.flush_chars(Some(parent))?;
961                return Ok(());
962            }
963            if self.starts_with(b"<!--") {
964                self.flush_chars(Some(parent))?;
965                self.pos += 4;
966                self.col += 4;
967                self.parse_comment(Some(parent))?;
968                continue;
969            }
970            if self.starts_with(b"<![CDATA[") {
971                self.flush_chars(Some(parent))?;
972                self.pos += 9;
973                self.col += 9;
974                self.parse_cdata(Some(parent))?;
975                continue;
976            }
977            if self.starts_with(b"<?") {
978                self.flush_chars(Some(parent))?;
979                self.pos += 2;
980                self.col += 2;
981                self.parse_pi(Some(parent), false)?;
982                continue;
983            }
984            if self.peek_byte() == Some(b'<') {
985                self.flush_chars(Some(parent))?;
986                self.parse_element(parent)?;
987                continue;
988            }
989            if self.peek_byte() == Some(b'&') {
990                self.flush_chars(Some(parent))?;
991                let repl = self.parse_reference()?;
992                self.char_buf.push_str(&repl);
993                self.flush_chars(Some(parent))?;
994                continue;
995            }
996            if self.starts_with(b"]]>") {
997                return Err(self.err(XML_ERR_MISPLACED_CDATA_END, "Misplaced CDATA end"));
998            }
999            let c = self.bump_char()?.unwrap();
1000            if !xml_is_char(c as u32) {
1001                return Err(self.err(XML_ERR_INVALID_CHAR, "Invalid character"));
1002            }
1003            self.char_buf.push(c);
1004        }
1005    }
1006
1007    fn parse_misc(&mut self, parent: NodeId) -> Result<(), XmlError> {
1008        loop {
1009            self.skip_s()?;
1010            if self.starts_with(b"<!--") {
1011                self.pos += 4;
1012                self.col += 4;
1013                self.parse_comment(Some(parent))?;
1014                continue;
1015            }
1016            if self.starts_with(b"<?") {
1017                self.pos += 2;
1018                self.col += 2;
1019                self.parse_pi(Some(parent), false)?;
1020                continue;
1021            }
1022            break;
1023        }
1024        Ok(())
1025    }
1026
1027    fn parse_document(&mut self) -> Result<(), XmlError> {
1028        if self.starts_with(&[0xef, 0xbb, 0xbf]) {
1029            self.pos += 3;
1030        }
1031        self.sax.set_document_locator();
1032        self.sax.start_document();
1033        self.started = true;
1034
1035        // XMLDecl must be at the start (after BOM). `<?xml-stylesheet` is a PI.
1036        if self.starts_with(b"<?xml") {
1037            let save_pos = self.pos;
1038            let save_col = self.col;
1039            let save_line = self.line;
1040            self.pos += 5;
1041            self.col += 5;
1042            match self.peek_char()? {
1043                Some(c) if crate::chvalid::xml_is_blank(c as u32) => {
1044                    self.parse_xml_decl_rest()?;
1045                }
1046                _ => {
1047                    self.pos = save_pos;
1048                    self.col = save_col;
1049                    self.line = save_line;
1050                    self.pos += 2;
1051                    self.col += 2;
1052                    self.parse_pi(Some(NodeId::DOCUMENT), false)?;
1053                }
1054            }
1055        }
1056
1057        self.parse_misc(NodeId::DOCUMENT)?;
1058        if self.starts_with(b"<!DOCTYPE") {
1059            self.pos += 9;
1060            self.col += 9;
1061            self.skip_doctype()?;
1062            self.parse_misc(NodeId::DOCUMENT)?;
1063        }
1064
1065        if self.peek_byte() != Some(b'<') {
1066            return Err(self.err(XML_ERR_DOCUMENT_EMPTY, "Document is empty"));
1067        }
1068        self.parse_element(NodeId::DOCUMENT)?;
1069        self.parse_misc(NodeId::DOCUMENT)?;
1070        self.skip_s()?;
1071        if !self.eof() {
1072            return Err(self.err(XML_ERR_EXTRA_CONTENT, "Extra content at the end of the document"));
1073        }
1074        self.sax.end_document();
1075        Ok(())
1076    }
1077}
1078
1079fn parse_doc(
1080    buffer: &[u8],
1081    _url: Option<&str>,
1082    encoding: Option<&str>,
1083    options: i32,
1084    sax: &mut dyn SaxHandler,
1085) -> Result<XmlDoc, XmlError> {
1086    let (converted, enc_name) = crate::encoding::xml_convert_to_utf8(buffer, encoding)?;
1087    parse_utf8(&converted, enc_name.as_deref(), options, sax)
1088}
1089
1090fn parse_utf8(
1091    buffer: &[u8],
1092    enc_name: Option<&str>,
1093    options: i32,
1094    sax: &mut dyn SaxHandler,
1095) -> Result<XmlDoc, XmlError> {
1096    let options = options | XML_PARSE_NONET | XML_PARSE_NO_XXE;
1097    let mut p = Parser {
1098        input: buffer,
1099        pos: 0,
1100        line: 1,
1101        col: 1,
1102        options,
1103        old10: (options & XML_PARSE_OLD10) != 0,
1104        depth: 0,
1105        ns_stack: Vec::new(),
1106        sax,
1107        doc: XmlDoc::xml_new_doc(Some("1.0")),
1108        stack: Vec::new(),
1109        char_buf: String::new(),
1110        started: false,
1111    };
1112    match p.parse_document() {
1113        Ok(()) => {
1114            apply_dtd_defaults(&mut p.doc);
1115            if p.doc.encoding.is_none() {
1116                if let Some(n) = enc_name {
1117                    if !n.eq_ignore_ascii_case("UTF-8") && !n.eq_ignore_ascii_case("US-ASCII") {
1118                        p.doc.encoding = Some(n.to_string());
1119                    }
1120                }
1121            }
1122            Ok(p.doc)
1123        }
1124        Err(e) => {
1125            if p.started {
1126                p.sax.end_document();
1127            }
1128            Err(e)
1129        }
1130    }
1131}
1132
1133fn apply_dtd_defaults(doc: &mut XmlDoc) {
1134    let Some(dtd) = doc.dtd.clone() else { return };
1135    let n = doc.len();
1136    for i in 0..n {
1137        let id = NodeId(i as u32);
1138        if doc.kind(id) != NodeKind::Element {
1139            continue;
1140        }
1141        let name = doc.name(id).to_string();
1142        for ((elem, aname), ad) in &dtd.attributes {
1143            if elem != &name {
1144                continue;
1145            }
1146            if doc.xml_get_prop(id, aname).is_some() {
1147                continue;
1148            }
1149            if let Some(v) = &ad.default_value {
1150                doc.xml_set_prop(id, aname, v);
1151            }
1152        }
1153    }
1154}
1155
1156#[cfg(test)]
1157mod chvalid_tests {
1158    use crate::xml_is_char;
1159    use std::path::PathBuf;
1160
1161    #[test]
1162    fn xml_is_char_matches_c_bmp_dump() {
1163        let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
1164        p.pop();
1165        p.pop();
1166        p.push("corpora");
1167        p.push("xmlIsChar-bmp.bin");
1168        if !p.exists() {
1169            return;
1170        }
1171        let dump = std::fs::read(&p).expect("corpora/xmlIsChar-bmp.bin");
1172        assert_eq!(dump.len(), 65536);
1173        for i in 0u32..=0xffff {
1174            let want = dump[i as usize] != 0;
1175            let got = xml_is_char(i);
1176            assert_eq!(got, want, "xml_is_char({i:#x}) = {got}, C dump = {want}");
1177        }
1178    }
1179}