Skip to main content

rusty_xml_parser/
dtd.rs

1//! DTD subset parser (internal + caller-supplied external). No network.
2
3use rusty_xml_tree::{AttrDecl, AttrDefault, ElementDecl, XmlDtd};
4use crate::error::XmlError;
5
6/// `xmlParseDTD` — parse a DTD from memory (caller already loaded the bytes).
7#[doc(alias = "xmlParseDTD")]
8pub fn xml_parse_dtd(
9    buffer: &[u8],
10    public_id: Option<&str>,
11    system_id: Option<&str>,
12) -> Result<XmlDtd, XmlError> {
13    let text = String::from_utf8_lossy(buffer);
14    let mut dtd = parse_dtd_subset(&text)?;
15    dtd.public_id = public_id.map(str::to_string);
16    dtd.system_id = system_id.map(str::to_string);
17    Ok(dtd)
18}
19
20/// Parse a DTD internal/external subset into declarations.
21pub fn parse_dtd_subset(src: &str) -> Result<XmlDtd, XmlError> {
22    let expanded = expand_pe(src);
23    let mut dtd = XmlDtd::default();
24    dtd.int_subset = Some(src.to_string());
25    let mut p = DtdParser {
26        src: expanded.as_str(),
27        pos: 0,
28        dtd: &mut dtd,
29    };
30    p.parse_markup()?;
31    Ok(dtd)
32}
33
34fn expand_pe(src: &str) -> String {
35    // Multi-pass PE expansion so `%percent;` can invent new PE names.
36    let mut cur = src.to_string();
37    for _ in 0..16 {
38        let mut pes: std::collections::HashMap<String, String> = std::collections::HashMap::new();
39        harvest_pe(&cur, &mut pes);
40        let next = subst_pe(&cur, &pes);
41        if next == cur {
42            return cur;
43        }
44        cur = next;
45    }
46    cur
47}
48
49fn harvest_pe(src: &str, pes: &mut std::collections::HashMap<String, String>) {
50    let bytes = src.as_bytes();
51    let mut i = 0;
52    while i + 8 < bytes.len() {
53        if bytes[i] == b'<' && bytes.get(i..i + 9) == Some(b"<!ENTITY ") {
54            i += 9;
55            while i < bytes.len() && bytes[i].is_ascii_whitespace() {
56                i += 1;
57            }
58            if i < bytes.len() && bytes[i] == b'%' {
59                i += 1;
60                while i < bytes.len() && bytes[i].is_ascii_whitespace() {
61                    i += 1;
62                }
63                let start = i;
64                while i < bytes.len() && !bytes[i].is_ascii_whitespace() && bytes[i] != b'"' && bytes[i] != b'\'' {
65                    i += 1;
66                }
67                let name = src[start..i].to_string();
68                while i < bytes.len() && bytes[i].is_ascii_whitespace() {
69                    i += 1;
70                }
71                if i < bytes.len() && (bytes[i] == b'"' || bytes[i] == b'\'') {
72                    let q = bytes[i];
73                    i += 1;
74                    let vs = i;
75                    while i < bytes.len() && bytes[i] != q {
76                        i += 1;
77                    }
78                    let val = decode_charrefs(&src[vs..i]);
79                    pes.insert(name, val);
80                }
81            }
82        } else {
83            i += 1;
84        }
85    }
86}
87
88fn subst_pe(src: &str, pes: &std::collections::HashMap<String, String>) -> String {
89    let mut out = String::new();
90    let mut chars = src.chars().peekable();
91    let mut in_comment = false;
92    while let Some(c) = chars.next() {
93        if in_comment {
94            out.push(c);
95            if c == '-' && chars.peek() == Some(&'-') {
96                out.push(chars.next().unwrap());
97                if chars.peek() == Some(&'>') {
98                    out.push(chars.next().unwrap());
99                    in_comment = false;
100                }
101            }
102            continue;
103        }
104        if c == '<' && chars.peek() == Some(&'!') {
105            out.push(c);
106            out.push(chars.next().unwrap());
107            if chars.peek() == Some(&'-') {
108                out.push(chars.next().unwrap());
109                if chars.peek() == Some(&'-') {
110                    out.push(chars.next().unwrap());
111                    in_comment = true;
112                }
113            }
114            continue;
115        }
116        if c == '%' {
117            let mut name = String::new();
118            while let Some(&n) = chars.peek() {
119                if n == ';' {
120                    chars.next();
121                    break;
122                }
123                if n.is_ascii_whitespace() || n == '"' || n == '\'' {
124                    break;
125                }
126                name.push(n);
127                chars.next();
128            }
129            if let Some(v) = pes.get(&name) {
130                out.push_str(v);
131            } else {
132                out.push('%');
133                out.push_str(&name);
134                if !name.is_empty() {
135                    out.push(';');
136                }
137            }
138            continue;
139        }
140        out.push(c);
141    }
142    out
143}
144
145fn decode_charrefs(s: &str) -> String {
146    let mut out = String::new();
147    let mut rest = s;
148    while let Some(i) = rest.find("&#") {
149        out.push_str(&rest[..i]);
150        let after = &rest[i + 2..];
151        if let Some(hex) = after.strip_prefix('x').or_else(|| after.strip_prefix('X')) {
152            if let Some(end) = hex.find(';') {
153                if let Ok(v) = u32::from_str_radix(&hex[..end], 16) {
154                    if let Some(ch) = char::from_u32(v) {
155                        out.push(ch);
156                        rest = &hex[end + 1..];
157                        continue;
158                    }
159                }
160            }
161        } else if let Some(end) = after.find(';') {
162            if let Ok(v) = after[..end].parse::<u32>() {
163                if let Some(ch) = char::from_u32(v) {
164                    out.push(ch);
165                    rest = &after[end + 1..];
166                    continue;
167                }
168            }
169        }
170        out.push_str("&#");
171        rest = after;
172    }
173    out.push_str(rest);
174    out
175}
176
177struct DtdParser<'a> {
178    src: &'a str,
179    pos: usize,
180    dtd: &'a mut XmlDtd,
181}
182
183impl<'a> DtdParser<'a> {
184    fn rest(&self) -> &'a str {
185        &self.src[self.pos..]
186    }
187    /// Whitespace only. Where the grammar says S it means S, not "whatever
188    /// happens to be in the way" -- skip_ws_and_comments swallows the SGML
189    /// `-- comment --` form and PIs, which is exactly how a malformed
190    /// declaration slipped past.
191    fn skip_ws(&mut self) {
192        let r = self.rest();
193        let trimmed = r.trim_start_matches([' ', '\t', '\r', '\n']);
194        self.pos += r.len() - trimmed.len();
195    }
196
197    fn skip_ws_and_comments(&mut self) {
198        loop {
199            let r = self.rest();
200            let trimmed = r.trim_start();
201            let n = r.len() - trimmed.len();
202            self.pos += n;
203            if self.rest().starts_with("<!--") {
204                if let Some(e) = self.rest().find("-->") {
205                    self.pos += e + 3;
206                    continue;
207                }
208            }
209            if self.rest().starts_with("<?") {
210                if let Some(e) = self.rest().find("?>") {
211                    self.pos += e + 2;
212                    continue;
213                }
214            }
215            break;
216        }
217    }
218    fn parse_markup(&mut self) -> Result<(), XmlError> {
219        loop {
220            self.skip_ws_and_comments();
221            if self.pos >= self.src.len() {
222                break;
223            }
224            if self.rest().starts_with("<!ELEMENT") {
225                self.parse_element()?;
226            } else if self.rest().starts_with("<!ATTLIST") {
227                self.parse_attlist()?;
228            } else if self.rest().starts_with("<!ENTITY") {
229                self.parse_entity()?;
230            } else if self.rest().starts_with("<!NOTATION") {
231                self.skip_decl()?;
232            } else if self.rest().starts_with("<![") {
233                self.skip_cond()?;
234            } else if self.rest().starts_with('<') {
235                self.skip_decl()?;
236            } else {
237                self.pos += self.rest().chars().next().unwrap().len_utf8();
238            }
239        }
240        Ok(())
241    }
242    fn skip_decl(&mut self) -> Result<(), XmlError> {
243        if let Some(i) = self.rest().find('>') {
244            self.pos += i + 1;
245            Ok(())
246        } else {
247            self.pos = self.src.len();
248            Ok(())
249        }
250    }
251    fn skip_cond(&mut self) -> Result<(), XmlError> {
252        let mut depth = 0i32;
253        let bytes = self.rest().as_bytes();
254        let mut i = 0;
255        while i < bytes.len() {
256            if bytes[i] == b'<' && bytes.get(i..i + 3) == Some(b"<![") {
257                depth += 1;
258                i += 3;
259                continue;
260            }
261            if bytes[i] == b']' && bytes.get(i..i + 3) == Some(b"]]>") {
262                depth -= 1;
263                i += 3;
264                if depth == 0 {
265                    self.pos += i;
266                    return Ok(());
267                }
268                continue;
269            }
270            i += 1;
271        }
272        self.pos = self.src.len();
273        Ok(())
274    }
275    fn bump(&mut self, n: usize) {
276        self.pos += n;
277    }
278    fn parse_name(&mut self) -> String {
279        self.skip_ws_and_comments();
280        let r = self.rest();
281        let mut n = 0;
282        for (i, c) in r.char_indices() {
283            if i == 0 {
284                if !(c.is_ascii_alphabetic() || c == '_' || c == ':') {
285                    break;
286                }
287            } else if !(c.is_ascii_alphanumeric() || "-._:".contains(c)) {
288                n = i;
289                break;
290            }
291            n = i + c.len_utf8();
292        }
293        let s = r[..n].to_string();
294        self.bump(n);
295        s
296    }
297    /// Read a quoted literal from the internal subset.
298    ///
299    /// This returned a bare String and so could not report anything. An
300    /// ATTLIST default or entity value holding a C0 control byte was
301    /// therefore accepted, copied into every element that took the default,
302    /// and written back out as U+FFFD -- a value the document never
303    /// contained. C stops at the declaration with "invalid character in
304    /// entity value". Found by the fuzz round-trip check, which saw the
305    /// first save escape the character and the second not.
306    fn parse_quoted(&mut self) -> Result<String, XmlError> {
307        self.skip_ws_and_comments();
308        let r = self.rest();
309        if r.starts_with('"') || r.starts_with('\'') {
310            let q = r.as_bytes()[0] as char;
311            self.bump(1);
312            if let Some(e) = self.rest().find(q) {
313                let s = decode_charrefs(&self.rest()[..e]);
314                self.bump(e + 1);
315                if let Some(bad) =
316                    s.chars().find(|c| !crate::chvalid::xml_is_char(*c as u32))
317                {
318                    return Err(XmlError::new(
319                        crate::error::XML_ERR_INVALID_CHAR,
320                        format!("invalid character 0x{:X} in entity value", bad as u32),
321                        0,
322                        0,
323                    ));
324                }
325                return Ok(s);
326            }
327        }
328        Ok(String::new())
329    }
330    fn parse_element(&mut self) -> Result<(), XmlError> {
331        self.bump("<!ELEMENT".len());
332        let name = self.parse_name();
333        self.skip_ws_and_comments();
334        let decl = if self.rest().starts_with("EMPTY") {
335            self.bump(5);
336            ElementDecl::Empty
337        } else if self.rest().starts_with("ANY") {
338            self.bump(3);
339            ElementDecl::Any
340        } else if self.rest().starts_with('(') {
341            let spec = self.take_until_gt_paren();
342            if spec.contains("#PCDATA") {
343                let mut names = Vec::new();
344                for part in spec.split('|') {
345                    let t = part.trim().trim_matches(|c: char| c == '(' || c == ')' || c == '*');
346                    if t != "#PCDATA" && !t.is_empty() {
347                        names.push(t.to_string());
348                    }
349                }
350                ElementDecl::Mixed(names)
351            } else {
352                ElementDecl::Children(spec)
353            }
354        } else {
355            self.skip_decl()?;
356            return Ok(());
357        };
358        self.dtd.elements.insert(name, decl);
359        self.skip_ws_and_comments();
360        if self.rest().starts_with('>') {
361            self.bump(1);
362        } else {
363            self.skip_decl()?;
364        }
365        Ok(())
366    }
367    fn take_until_gt_paren(&mut self) -> String {
368        let r = self.rest();
369        let mut depth = 0i32;
370        let mut i = 0;
371        for (off, c) in r.char_indices() {
372            match c {
373                '(' => depth += 1,
374                ')' => {
375                    depth -= 1;
376                    if depth == 0 {
377                        i = off + 1;
378                        break;
379                    }
380                }
381                '>' if depth == 0 => {
382                    i = off;
383                    break;
384                }
385                _ => {}
386            }
387            i = off + c.len_utf8();
388        }
389        let s = r[..i].to_string();
390        self.bump(i);
391        s
392    }
393    fn parse_attlist(&mut self) -> Result<(), XmlError> {
394        self.bump("<!ATTLIST".len());
395        let elem = self.parse_name();
396        loop {
397            self.skip_ws_and_comments();
398            if self.rest().starts_with('>') {
399                self.bump(1);
400                break;
401            }
402            if self.pos >= self.src.len() {
403                break;
404            }
405            let aname = self.parse_name();
406            if aname.is_empty() {
407                self.skip_decl()?;
408                break;
409            }
410            self.skip_ws_and_comments();
411            let mut enumerated = Vec::new();
412            let att_type = if self.rest().starts_with('(') {
413                let spec = self.take_until_gt_paren();
414                // Enumeration ::= '(' S? Nmtoken (S? '|' S? Nmtoken)* S? ')'
415                // Only '|' separates. `(foo,bar)` used to be accepted because
416                // this split on '|' and shrugged at whatever else was inside.
417                let body = spec.trim();
418                if !body.starts_with('(') || !body.ends_with(')') {
419                    return Err(self.err("')' required to finish ATTLIST enumeration"));
420                }
421                for part in body[1..body.len() - 1].split('|') {
422                    let t = part.trim();
423                    if t.is_empty() || !t.chars().all(|c| crate::chvalid::xml_is_name_char(c as u32, false)) {
424                        return Err(self.err("')' required to finish ATTLIST enumeration"));
425                    }
426                    enumerated.push(t.to_string());
427                }
428                "ENUMERATION".into()
429            } else {
430                let t = self.parse_name();
431                // AttType is a closed set. `NAME` is not in it, and was taken
432                // as a perfectly good type.
433                const TYPES: &[&str] = &[
434                    "CDATA", "ID", "IDREF", "IDREFS", "ENTITY", "ENTITIES", "NMTOKEN",
435                    "NMTOKENS", "NOTATION",
436                ];
437                if !TYPES.contains(&t.as_str()) {
438                    return Err(self.err("'(' required to start ATTLIST enumeration"));
439                }
440                if t == "NOTATION" {
441                    self.skip_ws();
442                    if !self.rest().starts_with('(') {
443                        return Err(self.err("'(' required to start ATTLIST enumeration"));
444                    }
445                    let spec = self.take_until_gt_paren();
446                    for part in spec.trim().trim_matches(['(', ')']).split('|') {
447                        let n = part.trim();
448                        if !n.is_empty() {
449                            enumerated.push(n.to_string());
450                        }
451                    }
452                }
453                t
454            };
455            self.skip_ws_and_comments();
456            let (default, default_value) = if self.rest().starts_with("#REQUIRED") {
457                self.bump(9);
458                (AttrDefault::Required, None)
459            } else if self.rest().starts_with("#IMPLIED") {
460                self.bump(8);
461                (AttrDefault::Implied, None)
462            } else if self.rest().starts_with("#FIXED") {
463                self.bump(6);
464                if !self.require_ws() {
465                    return Err(self.err("Space required after '#FIXED'"));
466                }
467                if !self.at_quote() {
468                    return Err(self.err("AttValue: \" or ' expected"));
469                }
470                (AttrDefault::Fixed, Some(self.parse_quoted()?))
471            } else {
472                // A default value is an AttValue, which is quoted. `v1` bare
473                // was accepted and silently became an empty string.
474                if !self.at_quote() {
475                    return Err(self.err("AttValue: \" or ' expected"));
476                }
477                (AttrDefault::Value, Some(self.parse_quoted()?))
478            };
479            self.dtd.attributes.insert(
480                (elem.clone(), aname),
481                AttrDecl {
482                    att_type,
483                    default,
484                    default_value,
485                    enumerated,
486                },
487            );
488        }
489        Ok(())
490    }
491    fn parse_entity(&mut self) -> Result<(), XmlError> {
492        self.bump("<!ENTITY".len());
493        if !self.require_ws() {
494            return Err(self.err("Space required after '<!ENTITY'"));
495        }
496        let pe = self.rest().starts_with('%');
497        if pe {
498            self.bump(1);
499            if !self.require_ws() {
500                return Err(self.err("Space required after '%'"));
501            }
502        }
503        let name = self.parse_name();
504        if name.is_empty() {
505            return Err(self.err("Entity name expected"));
506        }
507        // EntityDecl requires S between the name and the definition. Without
508        // this, `<!ENTITY foo"some text">` was accepted.
509        if !self.require_ws() {
510            return Err(self.err("Space required after the entity name"));
511        }
512        if self.rest().starts_with("SYSTEM") || self.rest().starts_with("PUBLIC") {
513            let public = self.rest().starts_with("PUBLIC");
514            self.bump(6);
515            if !self.require_ws() {
516                return Err(self.err("Space required after the external ID keyword"));
517            }
518            if public {
519                // ExternalID ::= 'PUBLIC' S PubidLiteral S SystemLiteral --
520                // two literals, with space between them. One was accepted, and
521                // so was `"whatever""e.ent"` with no space.
522                self.parse_quoted()?;
523                if !self.require_ws() {
524                    return Err(self.err("Space required after the Public Identifier"));
525                }
526                if !self.at_quote() {
527                    return Err(self.err("SystemLiteral expected"));
528                }
529            }
530            self.parse_quoted()?;
531            self.skip_ws_and_comments();
532            // NDataDecl is the only thing allowed to follow.
533            if self.rest().starts_with("NDATA") {
534                self.bump(5);
535                if !self.require_ws() {
536                    return Err(self.err("Space required after 'NDATA'"));
537                }
538                if self.parse_name().is_empty() {
539                    return Err(self.err("Notation name expected after 'NDATA'"));
540                }
541                self.skip_ws();
542            }
543            return self.expect_decl_end("entity");
544        }
545        if !self.at_quote() {
546            return Err(self.err("Entity value expected"));
547        }
548        let val = self.parse_quoted()?;
549        if pe {
550            self.dtd.parameter_entities.insert(name, val);
551        } else {
552            self.dtd.entities.insert(name, val);
553        }
554        self.skip_ws();
555        self.expect_decl_end("entity")
556    }
557
558    /// Position of the parser as a line and column, so an error points at the
559    /// declaration rather than at 0:0.
560    fn line_col(&self) -> (u32, u32) {
561        let mut line = 1u32;
562        let mut col = 1u32;
563        for c in self.src[..self.pos.min(self.src.len())].chars() {
564            if c == '\n' {
565                line += 1;
566                col = 1;
567            } else {
568                col += 1;
569            }
570        }
571        (line, col)
572    }
573
574    fn err(&self, msg: &str) -> XmlError {
575        let (line, col) = self.line_col();
576        XmlError::new(crate::error::XML_ERR_SPACE_REQUIRED, msg, line, col)
577    }
578
579    /// Consume required whitespace, reporting whether any was there.
580    fn require_ws(&mut self) -> bool {
581        let before = self.pos;
582        self.skip_ws();
583        self.pos > before || self.pos >= self.src.len()
584    }
585
586    fn at_quote(&self) -> bool {
587        self.rest().starts_with('"') || self.rest().starts_with('\'')
588    }
589
590    /// A declaration ends at '>' and nothing else. It used to fall through to
591    /// skip_decl(), which swallowed whatever was in the way -- including the
592    /// SGML `-- comment --` form that XML does not have.
593    fn expect_decl_end(&mut self, what: &str) -> Result<(), XmlError> {
594        self.skip_ws();
595        if self.rest().starts_with('>') {
596            self.bump(1);
597            Ok(())
598        } else {
599            Err(self.err(&format!("xmlParse{what}Decl: not terminated")))
600        }
601    }
602}
603
604/// Merge `src` into `dst` (external subset onto internal).
605pub fn merge_dtd(dst: &mut XmlDtd, src: XmlDtd) {
606    dst.entities.extend(src.entities);
607    dst.parameter_entities.extend(src.parameter_entities);
608    dst.elements.extend(src.elements);
609    dst.attributes.extend(src.attributes);
610    if dst.public_id.is_none() {
611        dst.public_id = src.public_id;
612    }
613    if dst.system_id.is_none() {
614        dst.system_id = src.system_id;
615    }
616}