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_external_subset(&text, false)?;
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, old10: bool) -> Result<XmlDtd, XmlError> {
22    parse_subset(src, true, old10)
23}
24
25/// Parse an external subset, where conditional sections are legal and a
26/// parameter entity may supply part of a declaration.
27pub fn parse_external_subset(src: &str, old10: bool) -> Result<XmlDtd, XmlError> {
28    parse_subset(src, false, old10)
29}
30
31fn parse_subset(src: &str, internal: bool, old10: bool) -> Result<XmlDtd, XmlError> {
32    let expanded = expand_pe(src, internal)?;
33    let mut dtd = XmlDtd::default();
34    dtd.int_subset = Some(src.to_string());
35    let mut p = DtdParser {
36        src: expanded.as_str(),
37        pos: 0,
38        dtd: &mut dtd,
39        internal,
40        old10,
41    };
42    p.parse_markup()?;
43    // A parameter entity reference in the subset means the declarations may be
44    // incomplete, which changes "Entity Declared" from a well-formedness
45    // constraint into a validity one.
46    dtd.has_parameter_entity_refs = expanded != src
47        || src.chars().zip(src.chars().skip(1)).any(|(a, b)| {
48            a == '%' && crate::chvalid::xml_is_name_start_char(b as u32, false)
49        });
50    check_entity_graph(&dtd)?;
51    Ok(dtd)
52}
53
54fn expand_pe(src: &str, internal: bool) -> Result<String, XmlError> {
55    // Multi-pass PE expansion so `%percent;` can invent new PE names.
56    let mut cur = src.to_string();
57    for _ in 0..16 {
58        let mut pes: std::collections::HashMap<String, String> = std::collections::HashMap::new();
59        harvest_pe(&cur, &mut pes);
60        let next = subst_pe(&cur, &pes, internal)?;
61        if next == cur {
62            return Ok(cur);
63        }
64        cur = next;
65    }
66    Ok(cur)
67}
68
69fn harvest_pe(src: &str, pes: &mut std::collections::HashMap<String, String>) {
70    let bytes = src.as_bytes();
71    let mut i = 0;
72    while i + 8 < bytes.len() {
73        if bytes[i] == b'<' && bytes.get(i..i + 9) == Some(b"<!ENTITY ") {
74            i += 9;
75            while i < bytes.len() && bytes[i].is_ascii_whitespace() {
76                i += 1;
77            }
78            if i < bytes.len() && bytes[i] == b'%' {
79                i += 1;
80                while i < bytes.len() && bytes[i].is_ascii_whitespace() {
81                    i += 1;
82                }
83                let start = i;
84                while i < bytes.len() && !bytes[i].is_ascii_whitespace() && bytes[i] != b'"' && bytes[i] != b'\'' {
85                    i += 1;
86                }
87                let name = src[start..i].to_string();
88                while i < bytes.len() && bytes[i].is_ascii_whitespace() {
89                    i += 1;
90                }
91                if i < bytes.len() && (bytes[i] == b'"' || bytes[i] == b'\'') {
92                    let q = bytes[i];
93                    i += 1;
94                    let vs = i;
95                    while i < bytes.len() && bytes[i] != q {
96                        i += 1;
97                    }
98                    // The harvest pass is a pre-scan; a malformed value is
99                    // reported later, by the declaration parser proper.
100                    let Ok(val) = decode_charrefs(&src[vs..i]) else {
101                        continue;
102                    };
103                    pes.insert(name, val);
104                }
105            }
106        } else {
107            i += 1;
108        }
109    }
110}
111
112fn subst_pe(
113    src: &str,
114    pes: &std::collections::HashMap<String, String>,
115    internal: bool,
116) -> Result<String, XmlError> {
117    let mut out = String::new();
118    let mut chars = src.chars().peekable();
119    let mut in_comment = false;
120    // In the internal subset a parameter entity reference may only occur where
121    // a markup declaration can occur -- never inside one. `<!ELEMENT %pe;` and
122    // `<!ENTITY foo "%e;">` were both expanded happily.
123    let mut in_decl = false;
124    // A PE reference is recognized inside an EntityValue but NOT inside an
125    // attribute default, where `%` is an ordinary character. `<!ATTLIST d a
126    // CDATA "%e;">` is a valid document and must stay one.
127    let mut decl_is_entity = false;
128    let mut in_quote: Option<char> = None;
129    while let Some(c) = chars.next() {
130        if in_comment {
131            out.push(c);
132            if c == '-' && chars.peek() == Some(&'-') {
133                out.push(chars.next().unwrap());
134                if chars.peek() == Some(&'>') {
135                    out.push(chars.next().unwrap());
136                    in_comment = false;
137                }
138            }
139            continue;
140        }
141        if c == '>' && in_decl {
142            in_decl = false;
143            out.push(c);
144            continue;
145        }
146        if in_decl {
147            match in_quote {
148                Some(q) if c == q => in_quote = None,
149                None if c == '"' || c == '\'' => in_quote = Some(c),
150                _ => {}
151            }
152        }
153        // A processing instruction is markup too, and a parameter entity may
154        // not supply part of one in the internal subset. `<?music %pe;` where
155        // the entity carries the `?>` was going through.
156        if c == '<' && chars.peek() == Some(&'?') {
157            in_decl = true;
158            decl_is_entity = false;
159            in_quote = None;
160            out.push(c);
161            out.push(chars.next().unwrap());
162            continue;
163        }
164        if c == '?' && in_decl && chars.peek() == Some(&'>') {
165            in_decl = false;
166            out.push(c);
167            out.push(chars.next().unwrap());
168            continue;
169        }
170        if c == '<' && chars.peek() == Some(&'!') {
171            in_decl = true;
172            decl_is_entity = false;
173            in_quote = None;
174            out.push(c);
175            out.push(chars.next().unwrap());
176            if chars.peek() == Some(&'-') {
177                out.push(chars.next().unwrap());
178                if chars.peek() == Some(&'-') {
179                    out.push(chars.next().unwrap());
180                    in_comment = true;
181                    in_decl = false;
182                }
183            } else {
184                // Which declaration this is decides whether a `%` inside its
185                // literals is a reference at all.
186                let rest: String = chars.clone().take(6).collect();
187                decl_is_entity = rest.starts_with("ENTITY");
188            }
189            continue;
190        }
191        if c == '%' {
192            // `%` followed by anything that cannot start a Name is the PE
193            // marker of an `<!ENTITY % name ...>` declaration, not a reference.
194            let is_ref = chars
195                .peek()
196                .is_some_and(|n| crate::chvalid::xml_is_name_start_char(*n as u32, false));
197            if !is_ref {
198                // PEReference ::= '%' Name ';' -- `%;` has no name at all.
199                if chars.peek() == Some(&';') {
200                    return Err(XmlError::new(
201                        crate::error::XML_ERR_ENTITYREF_NO_NAME,
202                        "PEReference: no name",
203                        0,
204                        0,
205                    ));
206                }
207                out.push('%');
208                continue;
209            }
210            // Inside an attribute default the `%` is literal; leave it alone.
211            if in_decl && in_quote.is_some() && !decl_is_entity {
212                out.push('%');
213                continue;
214            }
215            if internal && in_decl {
216                return Err(XmlError::new(
217                    crate::error::XML_ERR_ENTITYREF_NO_NAME,
218                    "PEReferences forbidden in internal subset",
219                    0,
220                    0,
221                ));
222            }
223            let mut name = String::new();
224            let mut terminated = false;
225            while let Some(&n) = chars.peek() {
226                if n == ';' {
227                    chars.next();
228                    terminated = true;
229                    break;
230                }
231                if !crate::chvalid::xml_is_name_char(n as u32, false) {
232                    break;
233                }
234                name.push(n);
235                chars.next();
236            }
237            // `%paaa` and `%paaa ;` were both accepted. The semicolon is not
238            // optional, and no whitespace may come before it.
239            if !terminated {
240                return Err(XmlError::new(
241                    crate::error::XML_ERR_ENTITYREF_SEMICOL_MISSING,
242                    "PEReference: expecting ';'",
243                    0,
244                    0,
245                ));
246            }
247            if let Some(v) = pes.get(&name) {
248                out.push_str(v);
249            } else {
250                out.push('%');
251                out.push_str(&name);
252                out.push(';');
253            }
254            continue;
255        }
256        out.push(c);
257    }
258    Ok(out)
259}
260
261/// Decode character references in an entity value or attribute default.
262///
263/// EntityValue forbids a bare `&`: it must begin a character or entity
264/// reference. Nothing checked that, so `&49;` was kept as literal text and
265/// `&#002f;` -- digits followed by a non-digit -- was silently left alone
266/// instead of being reported as an invalid decimal value.
267///
268/// General entity references are kept verbatim; they are expanded at the point
269/// of use, not here.
270fn decode_charrefs(s: &str) -> Result<String, &'static str> {
271    let mut out = String::new();
272    let mut it = s.chars().peekable();
273    while let Some(c) = it.next() {
274        if c != '&' {
275            out.push(c);
276            continue;
277        }
278        if it.peek() == Some(&'#') {
279            it.next();
280            let hex = matches!(it.peek(), Some('x') | Some('X'));
281            let upper_x = it.peek() == Some(&'X');
282            if hex {
283                it.next();
284            }
285            let mut digits = String::new();
286            while let Some(&d) = it.peek() {
287                if hex && d.is_ascii_hexdigit() || !hex && d.is_ascii_digit() {
288                    digits.push(d);
289                    it.next();
290                } else {
291                    break;
292                }
293            }
294            // `&#X41;` -- the production spells the marker lowercase only.
295            if upper_x || digits.is_empty() || it.next() != Some(';') {
296                return Err(if hex {
297                    "CharRef: invalid hexadecimal value"
298                } else {
299                    "CharRef: invalid decimal value"
300                });
301            }
302            let radix = if hex { 16 } else { 10 };
303            let v = u32::from_str_radix(&digits, radix)
304                .map_err(|_| "CharRef: value out of range")?;
305            match char::from_u32(v).filter(|ch| crate::chvalid::xml_is_char(*ch as u32)) {
306                Some(ch) => out.push(ch),
307                None => return Err("CharRef: invalid XML character"),
308            }
309            continue;
310        }
311        // A general entity reference: keep it, but it must be well formed.
312        let mut name = String::new();
313        while let Some(&d) = it.peek() {
314            if crate::chvalid::xml_is_name_char(d as u32, false) {
315                name.push(d);
316                it.next();
317            } else {
318                break;
319            }
320        }
321        if name.is_empty() || it.next() != Some(';') {
322            return Err("EntityValue: '&' forbidden except for entities references");
323        }
324        out.push('&');
325        out.push_str(&name);
326        out.push(';');
327    }
328    Ok(out)
329}
330
331struct DtdParser<'a> {
332    src: &'a str,
333    pos: usize,
334    dtd: &'a mut XmlDtd,
335    /// The internal subset carries rules the external one does not: no
336    /// conditional sections, and a parameter entity may not supply part of a
337    /// declaration.
338    internal: bool,
339    /// XML 1.0 before the 5th edition: the narrower name character classes.
340    /// The DTD parser had no idea this option existed, so a name illegal
341    /// under the old rules sailed through in a declaration -- and a PI
342    /// target inside the subset was never checked at all.
343    old10: bool,
344}
345
346impl<'a> DtdParser<'a> {
347    fn rest(&self) -> &'a str {
348        &self.src[self.pos..]
349    }
350    /// Whitespace only. Where the grammar says S it means S, not "whatever
351    /// happens to be in the way" -- skip_ws_and_comments swallows the SGML
352    /// `-- comment --` form and PIs, which is exactly how a malformed
353    /// declaration slipped past.
354    fn skip_ws(&mut self) {
355        let r = self.rest();
356        let trimmed = r.trim_start_matches([' ', '\t', '\r', '\n']);
357        self.pos += r.len() - trimmed.len();
358    }
359
360    fn skip_ws_and_comments(&mut self) -> Result<(), XmlError> {
361        loop {
362            let r = self.rest();
363            let trimmed = r.trim_start();
364            let n = r.len() - trimmed.len();
365            self.pos += n;
366            if self.rest().starts_with("<!--") {
367                if let Some(e) = self.rest().find("-->") {
368                    self.pos += e + 3;
369                    continue;
370                }
371            }
372            if self.rest().starts_with("<?") {
373                // An XML declaration is only legal at the very start of the
374                // document. Inside the internal subset it is a PI whose target
375                // is reserved, and this loop skipped every PI without looking.
376                let after = &self.rest()[2..];
377                // .get(..3), not [..3]: a byte index that lands inside a
378                // multi-byte character panics, and a PI target is arbitrary
379                // text. The suite hit this on the first run.
380                let is_xml_decl = after
381                    .get(..3)
382                    .is_some_and(|k| k.eq_ignore_ascii_case("xml"))
383                    && after[3..]
384                        .chars()
385                        .next()
386                        .is_none_or(|c| c.is_whitespace() || c == '?');
387                if is_xml_decl {
388                    return Err(self.err(
389                        "XML declaration allowed only at the start of the document",
390                    ));
391                }
392                // A PI inside the subset was skipped without a glance at its
393                // target. That is where the suite puts its illegal-name cases
394                // -- roughly three hundred of them.
395                let after = &self.rest()[2..];
396                let mut target_len = 0usize;
397                for (i, ch) in after.char_indices() {
398                    let ok = if i == 0 {
399                        crate::chvalid::xml_is_name_start_char(ch as u32, self.old10)
400                    } else {
401                        crate::chvalid::xml_is_name_char(ch as u32, self.old10)
402                    };
403                    if !ok {
404                        break;
405                    }
406                    target_len = i + ch.len_utf8();
407                }
408                if target_len == 0 {
409                    return Err(self.err("xmlParsePI : no target name"));
410                }
411                // The target has to END there too. `<?_` followed by a
412                // character that is not a name character is not a PI with the
413                // target `_`; it is a PI with an illegal character in its
414                // target, which is what the suite is testing.
415                let tail = &after[target_len..];
416                let ends_cleanly = tail.is_empty()
417                    || tail.starts_with("?>")
418                    || tail.chars().next().is_some_and(char::is_whitespace);
419                if !ends_cleanly {
420                    return Err(self.err("xmlParsePI : invalid character in target name"));
421                }
422                if let Some(e) = self.rest().find("?>") {
423                    self.pos += e + 2;
424                    continue;
425                }
426            }
427            break;
428        }
429        Ok(())
430    }
431    fn parse_markup(&mut self) -> Result<(), XmlError> {
432        loop {
433            self.skip_ws_and_comments()?;
434            if self.pos >= self.src.len() {
435                break;
436            }
437            if self.rest().starts_with("<!ELEMENT") {
438                self.parse_element()?;
439            } else if self.rest().starts_with("<!ATTLIST") {
440                self.parse_attlist()?;
441            } else if self.rest().starts_with("<!ENTITY") {
442                self.parse_entity()?;
443            } else if self.rest().starts_with("<!NOTATION") {
444                self.parse_notation()?;
445            } else if self.rest().starts_with("<![") {
446                // INCLUDE and IGNORE sections are external-subset only.
447                if self.internal {
448                    return Err(self.err("Content error in the internal subset"));
449                }
450                self.skip_cond()?;
451            } else if self.rest().starts_with('<') {
452                // Anything else beginning with '<' is not a markup
453                // declaration, and skipping it accepted `<ELEMENT ...>` with
454                // the bang missing, `<!Attlist ...>` and `<!notation ...>`
455                // with the keyword miscased, and every other near-miss.
456                return Err(self.err("Content error in the internal subset"));
457            } else if self.rest().starts_with('%') {
458                // A well-formed PE reference was already substituted, so a '%'
459                // still sitting at markup level is not one -- `% foo;` with a
460                // space is not a reference, it is garbage between declarations.
461                return Err(self.err("PEReference: expecting ';'"));
462            } else {
463                self.pos += self.rest().chars().next().unwrap().len_utf8();
464            }
465        }
466        Ok(())
467    }
468    fn skip_decl(&mut self) -> Result<(), XmlError> {
469        if let Some(i) = self.rest().find('>') {
470            self.pos += i + 1;
471            Ok(())
472        } else {
473            self.pos = self.src.len();
474            Ok(())
475        }
476    }
477    fn skip_cond(&mut self) -> Result<(), XmlError> {
478        let mut depth = 0i32;
479        let bytes = self.rest().as_bytes();
480        let mut i = 0;
481        while i < bytes.len() {
482            if bytes[i] == b'<' && bytes.get(i..i + 3) == Some(b"<![") {
483                depth += 1;
484                i += 3;
485                continue;
486            }
487            if bytes[i] == b']' && bytes.get(i..i + 3) == Some(b"]]>") {
488                depth -= 1;
489                i += 3;
490                if depth == 0 {
491                    self.pos += i;
492                    return Ok(());
493                }
494                continue;
495            }
496            i += 1;
497        }
498        self.pos = self.src.len();
499        Ok(())
500    }
501    fn bump(&mut self, n: usize) {
502        self.pos += n;
503    }
504    fn parse_name(&mut self) -> String {
505        // A misplaced XML declaration is reported by the markup loop; here we
506        // only need the position advanced.
507        let _ = self.skip_ws_and_comments();
508        let r = self.rest();
509        let mut n = 0;
510        // Names here were ASCII-only, so `<!ELEMENT เจมส์ (#PCDATA)>` -- a
511        // perfectly valid declaration -- came back empty and the document was
512        // rejected. The document body accepted the same name happily; only the
513        // DTD disagreed.
514        for (i, c) in r.char_indices() {
515            let ok = if i == 0 {
516                crate::chvalid::xml_is_name_start_char(c as u32, self.old10)
517            } else {
518                crate::chvalid::xml_is_name_char(c as u32, self.old10)
519            };
520            if !ok {
521                break;
522            }
523            n = i + c.len_utf8();
524        }
525        let s = r[..n].to_string();
526        self.bump(n);
527        s
528    }
529    /// Read a quoted literal from the internal subset.
530    ///
531    /// This returned a bare String and so could not report anything. An
532    /// ATTLIST default or entity value holding a C0 control byte was
533    /// therefore accepted, copied into every element that took the default,
534    /// and written back out as U+FFFD -- a value the document never
535    /// contained. C stops at the declaration with "invalid character in
536    /// entity value". Found by the fuzz round-trip check, which saw the
537    /// first save escape the character and the second not.
538    fn parse_quoted(&mut self) -> Result<String, XmlError> {
539        self.skip_ws_and_comments()?;
540        let r = self.rest();
541        if r.starts_with('"') || r.starts_with('\'') {
542            let q = r.as_bytes()[0] as char;
543            self.bump(1);
544            if let Some(e) = self.rest().find(q) {
545                let s = decode_charrefs(&self.rest()[..e]).map_err(|m| self.err(m))?;
546                self.bump(e + 1);
547                if let Some(bad) =
548                    s.chars().find(|c| !crate::chvalid::xml_is_char(*c as u32))
549                {
550                    return Err(XmlError::new(
551                        crate::error::XML_ERR_INVALID_CHAR,
552                        format!("invalid character 0x{:X} in entity value", bad as u32),
553                        0,
554                        0,
555                    ));
556                }
557                return Ok(s);
558            }
559        }
560        Ok(String::new())
561    }
562    fn parse_element(&mut self) -> Result<(), XmlError> {
563        self.bump("<!ELEMENT".len());
564        if !self.require_ws() {
565            return Err(self.err("Space required after '<!ELEMENT'"));
566        }
567        let name = self.parse_name();
568        if name.is_empty() {
569            return Err(self.err("Element name expected"));
570        }
571        if !self.require_ws() {
572            return Err(self.err("Space required after the element name"));
573        }
574        let decl = if self.rest().starts_with("EMPTY") {
575            self.bump(5);
576            ElementDecl::Empty
577        } else if self.rest().starts_with("ANY") {
578            self.bump(3);
579            ElementDecl::Any
580        } else if self.rest().starts_with('(') {
581            let mut spec = self.take_until_gt_paren();
582            // take_until_gt_paren stops at the closing paren, so a trailing
583            // occurrence indicator is still in the stream. It is part of the
584            // content spec and Mixed content is not valid without it.
585            if let Some(q @ ('?' | '*' | '+')) = self.rest().chars().next() {
586                self.bump(1);
587                spec.push(q);
588            }
589            // The content model was never checked, only scanned for '#PCDATA'
590            // and split on '|'. Everything else was accepted: `(a & b)`,
591            // `(a b)`, `(a|b,c)` mixing connectors, `(doc*?)`, `()`. That is
592            // 73 conformance cases, and the unchecked loop behind it was the
593            // 32 GB allocation.
594            let mixed = validate_contentspec(&spec).map_err(|e| self.err(e))?;
595            if mixed {
596                let mut names = Vec::new();
597                for part in spec.split('|') {
598                    let t = part.trim().trim_matches(|c: char| c == '(' || c == ')' || c == '*');
599                    if t != "#PCDATA" && !t.is_empty() {
600                        names.push(t.to_string());
601                    }
602                }
603                ElementDecl::Mixed(names)
604            } else {
605                ElementDecl::Children(spec)
606            }
607        } else {
608            return Err(self.err("xmlParseElementDecl: 'EMPTY', 'ANY' or '(' expected"));
609        };
610        if self.dtd.elements.contains_key(&name) {
611            self.dtd.duplicate_elements.push(name.clone());
612        }
613        self.dtd.elements.insert(name, decl);
614        self.expect_decl_end("Element")
615    }
616    fn take_until_gt_paren(&mut self) -> String {
617        let r = self.rest();
618        let mut depth = 0i32;
619        let mut i = 0;
620        for (off, c) in r.char_indices() {
621            match c {
622                '(' => depth += 1,
623                ')' => {
624                    depth -= 1;
625                    if depth == 0 {
626                        i = off + 1;
627                        break;
628                    }
629                }
630                '>' if depth == 0 => {
631                    i = off;
632                    break;
633                }
634                _ => {}
635            }
636            i = off + c.len_utf8();
637        }
638        let s = r[..i].to_string();
639        self.bump(i);
640        s
641    }
642    fn parse_attlist(&mut self) -> Result<(), XmlError> {
643        self.bump("<!ATTLIST".len());
644        if !self.require_ws() {
645            return Err(self.err("Space required after '<!ATTLIST'"));
646        }
647        let elem = self.parse_name();
648        if elem.is_empty() {
649            return Err(self.err("Element name expected in ATTLIST"));
650        }
651        loop {
652            // AttDef ::= S Name S AttType S DefaultDecl -- every one of those
653            // S is required, and none of them was checked.
654            let had_ws = self.require_ws();
655            self.skip_ws_and_comments()?;
656            if self.rest().starts_with('>') {
657                self.bump(1);
658                break;
659            }
660            if self.pos >= self.src.len() {
661                return Err(self.err("xmlParseAttributeListDecl: not terminated"));
662            }
663            if !had_ws {
664                return Err(self.err("Space required after the attribute name"));
665            }
666            let aname = self.parse_name();
667            if aname.is_empty() {
668                return Err(self.err("Attribute name expected"));
669            }
670            if !self.require_ws() {
671                return Err(self.err("Space required after the attribute name"));
672            }
673            self.skip_ws_and_comments()?;
674            let mut enumerated = Vec::new();
675            let att_type = if self.rest().starts_with('(') {
676                let spec = self.take_until_gt_paren();
677                // Enumeration ::= '(' S? Nmtoken (S? '|' S? Nmtoken)* S? ')'
678                // Only '|' separates. `(foo,bar)` used to be accepted because
679                // this split on '|' and shrugged at whatever else was inside.
680                let body = spec.trim();
681                if !body.starts_with('(') || !body.ends_with(')') {
682                    return Err(self.err("')' required to finish ATTLIST enumeration"));
683                }
684                for part in body[1..body.len() - 1].split('|') {
685                    let t = part.trim();
686                    if t.is_empty() || !t.chars().all(|c| crate::chvalid::xml_is_name_char(c as u32, false)) {
687                        return Err(self.err("')' required to finish ATTLIST enumeration"));
688                    }
689                    enumerated.push(t.to_string());
690                }
691                "ENUMERATION".into()
692            } else {
693                let t = self.parse_name();
694                // AttType is a closed set. `NAME` is not in it, and was taken
695                // as a perfectly good type.
696                const TYPES: &[&str] = &[
697                    "CDATA", "ID", "IDREF", "IDREFS", "ENTITY", "ENTITIES", "NMTOKEN",
698                    "NMTOKENS", "NOTATION",
699                ];
700                if !TYPES.contains(&t.as_str()) {
701                    return Err(self.err("'(' required to start ATTLIST enumeration"));
702                }
703                if t == "NOTATION" {
704                    if !self.require_ws() {
705                        return Err(self.err("Space required after 'NOTATION'"));
706                    }
707                    if !self.rest().starts_with('(') {
708                        return Err(self.err("'(' required to start ATTLIST enumeration"));
709                    }
710                    let spec = self.take_until_gt_paren();
711                    for part in spec.trim().trim_matches(['(', ')']).split('|') {
712                        let n = part.trim();
713                        // NotationType ::= 'NOTATION' S '(' S? Name (S? '|' S?
714                        // Name)* S? ')' -- every entry is a Name, and an empty
715                        // or malformed one was quietly dropped.
716                        let ok = !n.is_empty()
717                            && n.chars().enumerate().all(|(i, c)| {
718                                if i == 0 {
719                                    crate::chvalid::xml_is_name_start_char(c as u32, self.old10)
720                                } else {
721                                    crate::chvalid::xml_is_name_char(c as u32, self.old10)
722                                }
723                            });
724                        if !ok {
725                            return Err(self.err("Name expected in NOTATION declaration"));
726                        }
727                        enumerated.push(n.to_string());
728                    }
729                }
730                t
731            };
732            if !self.require_ws() {
733                return Err(self.err("Space required after the attribute type"));
734            }
735            self.skip_ws_and_comments()?;
736            let (default, default_value) = if self.rest().starts_with("#REQUIRED") {
737                self.bump(9);
738                (AttrDefault::Required, None)
739            } else if self.rest().starts_with("#IMPLIED") {
740                self.bump(8);
741                (AttrDefault::Implied, None)
742            } else if self.rest().starts_with("#FIXED") {
743                self.bump(6);
744                if !self.require_ws() {
745                    return Err(self.err("Space required after '#FIXED'"));
746                }
747                if !self.at_quote() {
748                    return Err(self.err("AttValue: \" or ' expected"));
749                }
750                (AttrDefault::Fixed, Some(self.parse_quoted()?))
751            } else {
752                // A default value is an AttValue, which is quoted. `v1` bare
753                // was accepted and silently became an empty string.
754                if !self.at_quote() {
755                    return Err(self.err("AttValue: \" or ' expected"));
756                }
757                (AttrDefault::Value, Some(self.parse_quoted()?))
758            };
759            // "When more than one definition is provided for the same
760            // attribute of a given element type, the FIRST declaration is
761            // binding and later declarations are ignored." We were inserting
762            // into a map, so the last one won and a later #REQUIRED overrode
763            // an earlier default.
764            // Attribute-value normalization applies to a default too, and to
765            // the LITERAL whitespace only: a tab written out becomes a space,
766            // a character-referenced one stays a tab. Entity values and system
767            // identifiers get no such treatment, so this belongs here and not
768            // in the shared literal reader.
769            let default_value = default_value.map(|v: String| {
770                v.chars()
771                    .map(|c| if matches!(c, '\t' | '\n' | '\r') { ' ' } else { c })
772                    .collect::<String>()
773            });
774            // In an attribute default the references ARE expanded, so the
775            // entity has to be declared already -- unlike an EntityValue,
776            // where they are bypassed and a forward reference is legal. The
777            // graph check runs after the whole subset and so could not tell
778            // the two apart; this runs in declaration order and can.
779            if let Some(v) = default_value.as_deref() {
780                const PREDEFINED: &[&str] = &["lt", "gt", "amp", "apos", "quot"];
781                for r in entity_refs_in(v) {
782                    if !PREDEFINED.contains(&r.as_str()) && !self.dtd.entities.contains_key(&r) {
783                        return Err(self.err(&format!("Entity '{r}' not defined")));
784                    }
785                }
786            }
787            self.dtd.attributes.entry((elem.clone(), aname)).or_insert(
788                AttrDecl {
789                    att_type,
790                    default,
791                    default_value,
792                    enumerated,
793                },
794            );
795        }
796        Ok(())
797    }
798    /// `NotationDecl ::= '<!NOTATION' S Name S (ExternalID | PublicID) S? '>'`
799    ///
800    /// This went to skip_decl, which took everything up to the next '>' and
801    /// asked no questions: a missing space, a missing name, a public
802    /// identifier holding characters the production forbids, all accepted.
803    fn parse_notation(&mut self) -> Result<(), XmlError> {
804        self.bump("<!NOTATION".len());
805        if !self.require_ws() {
806            return Err(self.err("Space required after '<!NOTATION'"));
807        }
808        let name = self.parse_name();
809        if name.is_empty() {
810            return Err(self.err("Notation name expected"));
811        }
812        if !self.require_ws() {
813            return Err(self.err("Space required after the notation name"));
814        }
815        if name.contains(':') {
816            self.dtd
817                .namespace_errors
818                .push(format!("colons are forbidden from notation names '{name}'"));
819        }
820        self.dtd.notations.insert(name.clone());
821        let public = if self.rest().starts_with("PUBLIC") {
822            true
823        } else if self.rest().starts_with("SYSTEM") {
824            false
825        } else {
826            return Err(self.err("'PUBLIC' or 'SYSTEM' expected in NOTATION"));
827        };
828        self.bump(6);
829        if !self.require_ws() {
830            return Err(self.err("Space required after the external ID keyword"));
831        }
832        if !self.at_quote() {
833            return Err(self.err("Unfinished System or Public ID \" or ' expected"));
834        }
835        let first = self.parse_quoted()?;
836        if public {
837            if let Some(bad) = first.chars().find(|c| !is_pubid_char(*c)) {
838                return Err(self.err(&format!(
839                    "Invalid character 0x{:X} in public identifier",
840                    bad as u32
841                )));
842            }
843            // PublicID (notation only) may stop after the public identifier;
844            // ExternalID continues with a system literal.
845            let before = self.pos;
846            self.skip_ws();
847            if self.at_quote() {
848                if self.pos == before {
849                    return Err(self.err("Space required after the Public Identifier"));
850                }
851                self.parse_quoted()?;
852            }
853        }
854        self.expect_decl_end("Notation")
855    }
856
857    fn parse_entity(&mut self) -> Result<(), XmlError> {
858        self.bump("<!ENTITY".len());
859        if !self.require_ws() {
860            return Err(self.err("Space required after '<!ENTITY'"));
861        }
862        let pe = self.rest().starts_with('%');
863        if pe {
864            self.bump(1);
865            if !self.require_ws() {
866                return Err(self.err("Space required after '%'"));
867            }
868        }
869        let name = self.parse_name();
870        if name.is_empty() {
871            return Err(self.err("Entity name expected"));
872        }
873        // EntityDecl requires S between the name and the definition. Without
874        // this, `<!ENTITY foo"some text">` was accepted.
875        // An entity name is an NCName -- the colon belongs to QNames. Not
876        // fatal, as C is not: it is a namespace error and gets reported.
877        if name.contains(':') {
878            self.dtd
879                .namespace_errors
880                .push(format!("colons are forbidden from entities names '{name}'"));
881        }
882        if !self.require_ws() {
883            return Err(self.err("Space required after the entity name"));
884        }
885        if self.rest().starts_with("SYSTEM") || self.rest().starts_with("PUBLIC") {
886            let public = self.rest().starts_with("PUBLIC");
887            self.bump(6);
888            if !self.require_ws() {
889                return Err(self.err("Space required after the external ID keyword"));
890            }
891            if public {
892                // ExternalID ::= 'PUBLIC' S PubidLiteral S SystemLiteral --
893                // two literals, with space between them. One was accepted, and
894                // so was `"whatever""e.ent"` with no space.
895                let pid = self.parse_quoted()?;
896                if let Some(bad) = pid.chars().find(|c| !is_pubid_char(*c)) {
897                    return Err(self.err(&format!(
898                        "Invalid character 0x{:X} in public identifier",
899                        bad as u32
900                    )));
901                }
902                if !self.require_ws() {
903                    return Err(self.err("Space required after the Public Identifier"));
904                }
905                if !self.at_quote() {
906                    return Err(self.err("SystemLiteral expected"));
907                }
908            }
909            // parse_quoted returns an empty string rather than an error when
910            // it is not looking at a quote, so `<!ENTITY p SYSTEM >` with no
911            // literal at all went through as an entity with no system id.
912            if !self.at_quote() {
913                return Err(self.err("SystemLiteral \" or ' expected"));
914            }
915            self.parse_quoted()?;
916            // NDataDecl is the only thing allowed to follow, and it needs the
917            // space before it. Measure BEFORE skipping, or the skip eats the
918            // very thing being checked for.
919            let ws_before_ndata = {
920                let before = self.pos;
921                self.skip_ws();
922                self.pos > before
923            };
924            self.skip_ws_and_comments()?;
925            if self.rest().starts_with("NDATA") {
926                // A parameter entity is always parsed; NDATA is for unparsed
927                // general entities only.
928                if pe {
929                    return Err(self.err("xmlParseEntityDecl: entity not terminated"));
930                }
931                if !ws_before_ndata {
932                    return Err(self.err("Space required before 'NDATA'"));
933                }
934                self.bump(5);
935                if !self.require_ws() {
936                    return Err(self.err("Space required after 'NDATA'"));
937                }
938                let notation = self.parse_name();
939                if notation.is_empty() {
940                    return Err(self.err("Notation name expected after 'NDATA'"));
941                }
942                self.dtd.ndata_notations.push(notation);
943                // An NDATA entity is unparsed, and only an unparsed entity may
944                // be the value of an ENTITY attribute.
945                self.dtd.unparsed_entities.insert(name.clone());
946                self.skip_ws();
947            }
948            return self.expect_decl_end("entity");
949        }
950        if !self.at_quote() {
951            return Err(self.err("Entity value expected"));
952        }
953        let val = self.parse_quoted()?;
954        // "If the same entity is declared more than once, the first
955        // declaration encountered is binding." We inserted into a map, so the
956        // last won -- and a document whose second declaration is deliberately
957        // junk was rejected on the strength of a declaration it never uses.
958        if pe {
959            self.dtd.parameter_entities.entry(name).or_insert(val);
960        } else {
961            self.dtd.entities.entry(name).or_insert(val);
962        }
963        self.skip_ws();
964        self.expect_decl_end("entity")
965    }
966
967    /// Position of the parser as a line and column, so an error points at the
968    /// declaration rather than at 0:0.
969    fn line_col(&self) -> (u32, u32) {
970        let mut line = 1u32;
971        let mut col = 1u32;
972        for c in self.src[..self.pos.min(self.src.len())].chars() {
973            if c == '\n' {
974                line += 1;
975                col = 1;
976            } else {
977                col += 1;
978            }
979        }
980        (line, col)
981    }
982
983    fn err(&self, msg: &str) -> XmlError {
984        let (line, col) = self.line_col();
985        XmlError::new(crate::error::XML_ERR_SPACE_REQUIRED, msg, line, col)
986    }
987
988    /// Consume required whitespace, reporting whether any was there.
989    fn require_ws(&mut self) -> bool {
990        let before = self.pos;
991        self.skip_ws();
992        self.pos > before || self.pos >= self.src.len()
993    }
994
995    fn at_quote(&self) -> bool {
996        self.rest().starts_with('"') || self.rest().starts_with('\'')
997    }
998
999    /// A declaration ends at '>' and nothing else. It used to fall through to
1000    /// skip_decl(), which swallowed whatever was in the way -- including the
1001    /// SGML `-- comment --` form that XML does not have.
1002    fn expect_decl_end(&mut self, what: &str) -> Result<(), XmlError> {
1003        self.skip_ws();
1004        if self.rest().starts_with('>') {
1005            self.bump(1);
1006            Ok(())
1007        } else {
1008            Err(self.err(&format!("xmlParse{what}Decl: not terminated")))
1009        }
1010    }
1011}
1012
1013/// Merge `src` into `dst` (external subset onto internal).
1014pub fn merge_dtd(dst: &mut XmlDtd, src: XmlDtd) {
1015    dst.entities.extend(src.entities);
1016    dst.unparsed_entities.extend(src.unparsed_entities);
1017    dst.duplicate_elements.extend(src.duplicate_elements);
1018    dst.notations.extend(src.notations);
1019    dst.namespace_errors.extend(src.namespace_errors);
1020    dst.ndata_notations.extend(src.ndata_notations);
1021    dst.has_parameter_entity_refs |= src.has_parameter_entity_refs;
1022    dst.parameter_entities.extend(src.parameter_entities);
1023    dst.elements.extend(src.elements);
1024    dst.attributes.extend(src.attributes);
1025    if dst.public_id.is_none() {
1026        dst.public_id = src.public_id;
1027    }
1028    if dst.system_id.is_none() {
1029        dst.system_id = src.system_id;
1030    }
1031}
1032
1033/// Validate a content specification against XML 1.0 productions 46-51.
1034///
1035/// Returns `Ok(true)` for Mixed content, `Ok(false)` for a children model.
1036///
1037/// ```text
1038/// Mixed    ::= '(' S? '#PCDATA' (S? '|' S? Name)* S? ')*'
1039///            | '(' S? '#PCDATA' S? ')'
1040/// children ::= (choice | seq) ('?' | '*' | '+')?
1041/// cp       ::= (Name | choice | seq) ('?' | '*' | '+')?
1042/// choice   ::= '(' S? cp ( S? '|' S? cp )+ S? ')'
1043/// seq      ::= '(' S? cp ( S? ',' S? cp )* S? ')'
1044/// ```
1045///
1046/// The two rules that catch most malformed models: a group may not mix `,` and
1047/// `|` at the same level, and Mixed content that names elements must close
1048/// with `)*`.
1049fn validate_contentspec(spec: &str) -> Result<bool, &'static str> {
1050    let mut p = SpecParser {
1051        b: spec.as_bytes(),
1052        i: 0,
1053        depth: 0,
1054    };
1055    p.ws();
1056    if !p.eat(b'(') {
1057        return Err("ContentDecl : '(' expected");
1058    }
1059    p.ws();
1060    if p.b[p.i..].starts_with(b"#PCDATA") {
1061        p.i += 7;
1062        let mut named = false;
1063        loop {
1064            p.ws();
1065            if p.eat(b')') {
1066                break;
1067            }
1068            if !p.eat(b'|') {
1069                return Err("ContentDecl : ',' '|' or ')' expected");
1070            }
1071            p.ws();
1072            p.name()?;
1073            named = true;
1074        }
1075        // `(#PCDATA|a)` without the star is not a legal Mixed model.
1076        let star = p.eat(b'*');
1077        if named && !star {
1078            return Err("Element content model is not finished with ')*'");
1079        }
1080        p.ws();
1081        return if p.i == p.b.len() {
1082            Ok(true)
1083        } else {
1084            Err("trailing content after the content model")
1085        };
1086    }
1087    // A children model: rewind to the '(' and read it as a group.
1088    p.i = 0;
1089    p.ws();
1090    p.group()?;
1091    p.quant();
1092    p.ws();
1093    if p.i != p.b.len() {
1094        return Err("ContentDecl : garbage after the content model");
1095    }
1096    Ok(false)
1097}
1098
1099struct SpecParser<'a> {
1100    b: &'a [u8],
1101    i: usize,
1102    depth: u32,
1103}
1104
1105impl SpecParser<'_> {
1106    fn ws(&mut self) {
1107        while matches!(self.b.get(self.i), Some(b' ' | b'\t' | b'\r' | b'\n')) {
1108            self.i += 1;
1109        }
1110    }
1111    fn peek(&self) -> Option<u8> {
1112        self.b.get(self.i).copied()
1113    }
1114    fn eat(&mut self, c: u8) -> bool {
1115        if self.peek() == Some(c) {
1116            self.i += 1;
1117            true
1118        } else {
1119            false
1120        }
1121    }
1122    fn quant(&mut self) {
1123        if matches!(self.peek(), Some(b'?' | b'*' | b'+')) {
1124            self.i += 1;
1125        }
1126    }
1127    fn name(&mut self) -> Result<(), &'static str> {
1128        let start = self.i;
1129        // Names here are ASCII in practice, but a UTF-8 name must not be cut
1130        // mid-character, so decode properly.
1131        let rest = match std::str::from_utf8(&self.b[self.i..]) {
1132            Ok(r) => r,
1133            Err(_) => return Err("invalid UTF-8 in the content model"),
1134        };
1135        let mut chars = rest.char_indices();
1136        match chars.next() {
1137            Some((_, c)) if crate::chvalid::xml_is_name_start_char(c as u32, false) => {
1138                self.i += c.len_utf8();
1139            }
1140            _ => return Err("Name expected in the content model"),
1141        }
1142        for (off, c) in chars {
1143            if !crate::chvalid::xml_is_name_char(c as u32, false) {
1144                self.i = start + off;
1145                return Ok(());
1146            }
1147            self.i = start + off + c.len_utf8();
1148        }
1149        Ok(())
1150    }
1151    /// choice or seq. Which one is decided by the first separator, and the
1152    /// group must then use only that one.
1153    fn group(&mut self) -> Result<(), &'static str> {
1154        // `((((((...` must not recurse the stack away.
1155        self.depth += 1;
1156        if self.depth > 256 {
1157            return Err("content model nested too deeply");
1158        }
1159        if !self.eat(b'(') {
1160            return Err("ContentDecl : '(' expected");
1161        }
1162        self.ws();
1163        self.cp()?;
1164        self.ws();
1165        let sep = match self.peek() {
1166            Some(b')') => {
1167                self.i += 1;
1168                self.depth -= 1;
1169                return Ok(());
1170            }
1171            Some(c @ (b'|' | b',')) => c,
1172            _ => return Err("ContentDecl : ',' '|' or ')' expected"),
1173        };
1174        loop {
1175            if !self.eat(sep) {
1176                // A different connector at the same level: `(a|b,c)`.
1177                return Err("ContentDecl : ',' '|' or ')' expected");
1178            }
1179            self.ws();
1180            self.cp()?;
1181            self.ws();
1182            match self.peek() {
1183                Some(b')') => {
1184                    self.i += 1;
1185                    self.depth -= 1;
1186                    return Ok(());
1187                }
1188                Some(c) if c == sep => continue,
1189                _ => return Err("ContentDecl : ',' '|' or ')' expected"),
1190            }
1191        }
1192    }
1193    fn cp(&mut self) -> Result<(), &'static str> {
1194        if self.peek() == Some(b'(') {
1195            self.group()?;
1196        } else {
1197            self.name()?;
1198        }
1199        self.quant();
1200        Ok(())
1201    }
1202}
1203
1204/// PubidChar ::= #x20 | #xD | #xA | [a-zA-Z0-9] | [-'()+,./:=?;!*#@$_%]
1205///
1206/// A public identifier is a restricted character set, not free text. Nothing
1207/// checked it, so `<!NOTATION n PUBLIC "a^b">` was accepted.
1208pub fn is_pubid_char(c: char) -> bool {
1209    matches!(c, ' ' | '\r' | '\n')
1210        || c.is_ascii_alphanumeric()
1211        || matches!(
1212            c,
1213            '-' | '\'' | '(' | ')' | '+' | ',' | '.' | '/' | ':'
1214                | '=' | '?' | ';' | '!' | '*' | '#' | '@' | '$' | '_' | '%'
1215        )
1216}
1217
1218/// Well-formedness constraints on the entity graph, checked once the whole
1219/// subset is parsed.
1220///
1221/// `decode_charrefs` keeps general entity references verbatim, because they are
1222/// expanded at the point of use. Nothing then looked at them, so an entity
1223/// value or an ATTLIST default could reference an entity that was never
1224/// declared, or one declared NDATA (which may not be referenced at all), or
1225/// itself by way of a cycle. All three were accepted silently, and the cycle
1226/// only surfaced later as a depth-limit error pointing at the wrong entity.
1227fn check_entity_graph(dtd: &XmlDtd) -> Result<(), XmlError> {
1228    const PREDEFINED: &[&str] = &["lt", "gt", "amp", "apos", "quot"];
1229    let err = |m: String| XmlError::new(crate::error::XML_ERR_UNDECLARED_ENTITY, m, 0, 0);
1230
1231    // Every reference in a literal must name a declared, parsed entity.
1232    let mut refs: std::collections::HashMap<&str, Vec<String>> = Default::default();
1233    let literals = dtd
1234        .entities
1235        .iter()
1236        .map(|(k, v)| (k.as_str(), v.as_str()))
1237        .chain(
1238            dtd.attributes
1239                .iter()
1240                .filter_map(|((_, a), d)| d.default_value.as_deref().map(|v| (a.as_str(), v))),
1241        );
1242    for (owner, text) in literals {
1243        for name in entity_refs_in(text) {
1244            if PREDEFINED.contains(&name.as_str()) {
1245                continue;
1246            }
1247            if dtd.unparsed_entities.contains(&name) {
1248                return Err(err(format!("Entity reference to unparsed entity {name}")));
1249            }
1250            if !dtd.entities.contains_key(&name) {
1251                return Err(err(format!("Entity '{name}' not defined")));
1252            }
1253            refs.entry(owner).or_default().push(name);
1254        }
1255    }
1256
1257    // A cycle in the reference graph is a well-formedness error, not something
1258    // to discover by running out of depth.
1259    for start in dtd.entities.keys() {
1260        let mut seen = std::collections::HashSet::new();
1261        let mut stack = vec![start.as_str()];
1262        while let Some(cur) = stack.pop() {
1263            if !seen.insert(cur) {
1264                continue;
1265            }
1266            for next in refs.get(cur).into_iter().flatten() {
1267                if next == start {
1268                    return Err(err("Detected an entity reference loop".into()));
1269                }
1270                stack.push(next.as_str());
1271            }
1272        }
1273    }
1274    Ok(())
1275}
1276
1277/// The general entity references in a literal, as `&name;` occurrences.
1278fn entity_refs_in(text: &str) -> Vec<String> {
1279    let mut out = Vec::new();
1280    // Inside a CDATA section an ampersand is an ampersand. Scanning straight
1281    // through one reported `<!ENTITY e "<![CDATA[&foo;]]>">` as referencing an
1282    // undeclared entity that it does not reference at all.
1283    let mut rest = text;
1284    let mut scan = String::new();
1285    while let Some(i) = rest.find("<![CDATA[") {
1286        scan.push_str(&rest[..i]);
1287        rest = &rest[i + 9..];
1288        match rest.find("]]>") {
1289            Some(e) => rest = &rest[e + 3..],
1290            None => {
1291                rest = "";
1292                break;
1293            }
1294        }
1295    }
1296    scan.push_str(rest);
1297    let text = scan.as_str();
1298    let mut it = text.chars().peekable();
1299    while let Some(c) = it.next() {
1300        if c != '&' || it.peek() == Some(&'#') {
1301            continue;
1302        }
1303        let mut name = String::new();
1304        while let Some(&d) = it.peek() {
1305            if crate::chvalid::xml_is_name_char(d as u32, false) {
1306                name.push(d);
1307                it.next();
1308            } else {
1309                break;
1310            }
1311        }
1312        if !name.is_empty() && it.peek() == Some(&';') {
1313            it.next();
1314            out.push(name);
1315        }
1316    }
1317    out
1318}