Skip to main content

rusty_xml_valid/
lib.rs

1//! DTD validation, C14N, RelaxNG, XML Schema, Schematron.
2
3#![forbid(unsafe_code)]
4
5mod c14n;
6mod rng;
7mod xsd;
8mod schematron;
9
10use rusty_xml_tree::{AttrDefault, ElementDecl, NodeId, NodeKind, XmlDoc, XmlDtd};
11
12pub use c14n::*;
13pub use rng::*;
14pub use xsd::*;
15pub use schematron::*;
16
17/// `xmlValidateDocument` against the document's attached DTD.
18#[doc(alias = "xmlValidateDocument")]
19pub fn xml_validate_document(doc: &XmlDoc) -> Result<(), String> {
20    let dtd = doc.dtd.as_ref().ok_or("no DTD")?;
21    xml_validate_dtd(doc, dtd)
22}
23
24/// `xmlValidateDtd`.
25#[doc(alias = "xmlValidateDtd")]
26pub fn xml_validate_dtd(doc: &XmlDoc, dtd: &XmlDtd) -> Result<(), String> {
27    let root = doc
28        .xml_doc_get_root_element()
29        .ok_or("document has no root")?;
30    if let Some(n) = &dtd.name {
31        // The DOCTYPE names a QName, so `<!DOCTYPE xml:foo>` must be compared
32        // against `xml:foo` and not against the local part alone.
33        if doc.qname(root) != *n {
34            return Err(format!("root element {} does not match DOCTYPE {n}", doc.qname(root)));
35        }
36    }
37    // Constraints on the DECLARATIONS themselves, before any instance of them
38    // is looked at. None of these was checked, so a declaration could promise
39    // something no document could satisfy.
40    // An entity the parse could not resolve is a validity error when the
41    // subset was incomplete -- it stopped being a well-formedness one, but it
42    // did not stop being an error.
43    if let Some(e) = doc.undeclared_entity_refs.first() {
44        return Err(format!("Entity '{e}' not defined"));
45    }
46    // "Notation Declared": a notation named by an NDATA annotation or by a
47    // NOTATION attribute type has to have been declared. We were parsing
48    // <!NOTATION> and throwing the name away, so nothing could tell.
49    for n in &dtd.ndata_notations {
50        if !dtd.notations.contains(n) {
51            return Err(format!("Notation {n} is not declared"));
52        }
53    }
54    for ((elem, aname), ad) in &dtd.attributes {
55        if ad.att_type != "NOTATION" {
56            continue;
57        }
58        for n in &ad.enumerated {
59            if !dtd.notations.contains(n) {
60                return Err(format!(
61                    "Notation {n} in attribute {aname} of {elem} is not declared"
62                ));
63            }
64        }
65    }
66    // "Unique Element Type Declaration": no element type may be declared more
67    // than once. A HashMap cannot say so on its own, so the parser records it.
68    if let Some(dup) = dtd.duplicate_elements.first() {
69        return Err(format!("Redefinition of element {dup}"));
70    }
71    // "No Duplicate Types": a mixed content model may not name the same
72    // element twice.
73    for (elem, decl) in &dtd.elements {
74        if let ElementDecl::Mixed(names) = decl {
75            let mut seen = std::collections::HashSet::new();
76            for n in names {
77                if !seen.insert(n) {
78                    return Err(format!(
79                        "Definition of {elem} has duplicate references of {n}"
80                    ));
81                }
82            }
83        }
84    }
85    for ((elem, aname), ad) in &dtd.attributes {
86        // "No Duplicate Tokens": an enumeration may not repeat a value.
87        let mut seen = std::collections::HashSet::new();
88        for e in &ad.enumerated {
89            if !seen.insert(e) {
90                return Err(format!(
91                    "attribute {aname} of {elem}: enumeration value token {e} duplicated"
92                ));
93            }
94        }
95        // "ID Attribute Default": an ID attribute must be #IMPLIED or
96        // #REQUIRED -- it cannot carry a default value, since two elements
97        // taking the default would share an ID.
98        if ad.att_type == "ID"
99            && matches!(ad.default, AttrDefault::Value | AttrDefault::Fixed)
100        {
101            return Err(format!(
102                "ID attribute {aname} of {elem} is not valid, must be #IMPLIED or #REQUIRED"
103            ));
104        }
105        // "No Notation on Empty Element": an element declared EMPTY cannot
106        // carry a NOTATION attribute, since it can never have content for the
107        // notation to describe.
108        if ad.att_type == "NOTATION" && matches!(dtd.elements.get(elem), Some(ElementDecl::Empty)) {
109            return Err(format!(
110                "NOTATION attribute type declared for EMPTY element {elem}"
111            ));
112        }
113        // "Attribute Default Value Syntactically Correct": the default has to
114        // satisfy the type it is declared with.
115        let Some(def) = ad.default_value.as_deref() else {
116            continue;
117        };
118        let ok = match ad.att_type.as_str() {
119            "ID" | "IDREF" | "ENTITY" => is_name(def),
120            "IDREFS" | "ENTITIES" => {
121                def.split_space().next().is_some()
122                    && def.split_space().all(is_name)
123            }
124            "NMTOKEN" => is_nmtoken(def),
125            "NMTOKENS" => {
126                def.split_space().next().is_some()
127                    && def.split_space().all(is_nmtoken)
128            }
129            _ => true,
130        };
131        if !ok {
132            return Err(format!("invalid default value for attribute {aname} of {elem}"));
133        }
134        if !ad.enumerated.is_empty() && !ad.enumerated.iter().any(|e| e == def) {
135            return Err(format!("invalid default value for attribute {aname} of {elem}"));
136        }
137    }
138
139    // Walk iteratively: validation used to recurse per element, which is the
140    // same stack cliff the parser, the writer and C14N all had.
141    let mut ids: std::collections::HashMap<String, ()> = Default::default();
142    let mut idrefs: Vec<String> = Vec::new();
143    let mut stack = vec![root];
144    while let Some(id) = stack.pop() {
145        validate_element(doc, id, dtd, &mut ids, &mut idrefs)?;
146        let mut c = doc.last_child(id);
147        while let Some(x) = c {
148            if doc.kind(x) == NodeKind::Element {
149                stack.push(x);
150            }
151            c = doc.prev_sibling(x);
152        }
153    }
154    // IDREF VC: every referenced ID must be declared somewhere in the
155    // document. This was never checked at all.
156    for r in &idrefs {
157        if !ids.contains_key(r) {
158            return Err(format!("IDREF attribute references unknown ID \"{r}\""));
159        }
160    }
161    Ok(())
162}
163
164/// Split a list-typed attribute value on #x20, and only on #x20.
165///
166/// The tokenized list types are `Nmtoken (#x20 Nmtoken)*`. Literal tab and
167/// newline become spaces during attribute-value normalization, but a character
168/// reference contributes its character unchanged -- so `abc&#9;xyz` is ONE
169/// token holding a tab, and not a valid Nmtoken at all. Splitting on any
170/// whitespace turned it into two tokens that both looked fine.
171trait SplitSpace {
172    fn split_space(&self) -> std::iter::Filter<std::str::Split<'_, char>, fn(&&str) -> bool>;
173}
174
175impl SplitSpace for str {
176    fn split_space(&self) -> std::iter::Filter<std::str::Split<'_, char>, fn(&&str) -> bool> {
177        fn non_empty(s: &&str) -> bool {
178            !s.is_empty()
179        }
180        self.split(' ').filter(non_empty as fn(&&str) -> bool)
181    }
182}
183
184/// A Name, as the ID / IDREF validity constraints require.
185fn is_name(v: &str) -> bool {
186    let mut cs = v.chars();
187    match cs.next() {
188        Some(c) if rusty_xml_parser::chvalid::xml_is_name_start_char(c as u32, false) => {}
189        _ => return false,
190    }
191    cs.all(|c| rusty_xml_parser::chvalid::xml_is_name_char(c as u32, false))
192}
193
194/// An Nmtoken: like a Name but with no restriction on the first character.
195fn is_nmtoken(v: &str) -> bool {
196    !v.is_empty()
197        && v.chars()
198            .all(|c| rusty_xml_parser::chvalid::xml_is_name_char(c as u32, false))
199}
200
201fn validate_element(
202    doc: &XmlDoc,
203    id: NodeId,
204    dtd: &XmlDtd,
205    ids: &mut std::collections::HashMap<String, ()>,
206    idrefs: &mut Vec<String>,
207) -> Result<(), String> {
208    if doc.kind(id) != NodeKind::Element {
209        return Ok(());
210    }
211    // Declarations name QNames, so an element declared `<!ELEMENT xml:foo>` is
212    // looked up as `xml:foo`. Using the local part alone reported a declared
213    // element as undeclared.
214    let name = doc.qname(id);
215    // "Element Valid": an element with no declaration is invalid, and nothing
216    // said so. A DTD that declares nothing at all is not a validating DTD, so
217    // only complain when there are declarations to be missing from.
218    // Not gated on the DTD declaring anything: validating against a subset
219    // that declares no elements should fail for every element, which is what C
220    // reports. Requiring a non-empty map let a DTD of pure entity declarations
221    // validate any document at all.
222    if !dtd.elements.contains_key(&name) {
223        return Err(format!("No declaration for element {name}"));
224    }
225    if let Some(decl) = dtd.elements.get(&name) {
226        match decl {
227            ElementDecl::Empty => {
228                // An entity reference is content even when it expands to
229                // nothing: `<foo>&empty;</foo>` leaves no node behind, so the
230                // child list alone cannot see it.
231                if doc.first_child(id).is_some()
232                    || doc.elements_with_entity_refs.contains(&id)
233                {
234                    return Err(format!("element {name} must be EMPTY"));
235                }
236            }
237            ElementDecl::Any => {}
238            ElementDecl::Mixed(_) => {
239                let mut c = doc.first_child(id);
240                while let Some(x) = c {
241                    match doc.kind(x) {
242                        NodeKind::Element => {
243                            if let ElementDecl::Mixed(allowed) = decl {
244                                if !allowed.is_empty() && !allowed.iter().any(|n| *n == doc.qname(x)) {
245                                    return Err(format!("element {} not allowed in mixed {name}", doc.qname(x)));
246                                }
247                            }
248                        }
249                        NodeKind::Text | NodeKind::CData | NodeKind::Comment | NodeKind::Pi => {}
250                        _ => {}
251                    }
252                    c = doc.next_sibling(x);
253                }
254            }
255            ElementDecl::Children(spec) => {
256                let kids: Vec<String> = {
257                    let mut v = Vec::new();
258                    let mut c = doc.first_child(id);
259                    while let Some(x) = c {
260                        if doc.kind(x) == NodeKind::Element {
261                            v.push(doc.qname(x));
262                        } else if doc.kind(x) == NodeKind::Text
263                            && (!doc.xml_is_blank_node(x) || doc.reference_text.contains(&x))
264                        {
265                            // Whitespace that arrived as `&#32;` is character
266                            // data, not the ignorable indentation beside it,
267                            // and only the parser can tell them apart.
268                            return Err(format!("character data not allowed in {name}"));
269                        } else if doc.kind(x) == NodeKind::CData {
270                            // A CDATA section is character data whatever is in
271                            // it. Whitespace inside one is never the ignorable
272                            // kind, so an empty `<![CDATA[]]>` still breaks an
273                            // element-only content model -- and we were only
274                            // looking at Text nodes.
275                            return Err(format!("character data not allowed in {name}"));
276                        }
277                        c = doc.next_sibling(x);
278                    }
279                    v
280                };
281                if !match_children_spec(spec, &kids) {
282                    return Err(format!("content of {name} does not match {spec}"));
283                }
284            }
285        }
286    }
287    for ((elem, aname), ad) in &dtd.attributes {
288        if elem != &name {
289            continue;
290        }
291        // An ATTLIST declares a QName, so `xml:lang` is looked up as
292        // `xml:lang`. xml_get_prop is xmlGetProp -- it matches unprefixed
293        // attributes only -- so every prefixed declared attribute looked
294        // absent, and a #REQUIRED one was reported missing on a document that
295        // plainly had it.
296        let have = {
297            let mut found = None;
298            let mut a = doc.first_attr(id);
299            while let Some(x) = a {
300                if doc.qname(x) == *aname {
301                    found = Some(doc.content(x).to_string());
302                    break;
303                }
304                a = doc.next_sibling(x);
305            }
306            found
307        };
308        match ad.default {
309            AttrDefault::Required if have.is_none() => {
310                return Err(format!("attribute {aname} of {name} is required"));
311            }
312            AttrDefault::Fixed => {
313                if let (Some(v), Some(fix)) = (&have, &ad.default_value) {
314                    if v != fix {
315                        return Err(format!("attribute {aname} must be {fix}"));
316                    }
317                }
318            }
319            _ => {}
320        }
321        if let Some(v) = &have {
322            if !ad.enumerated.is_empty() && !ad.enumerated.iter().any(|e| e == v) {
323                return Err(format!("attribute {aname} value not in enumeration"));
324            }
325            // The tokenized types carry validity constraints on their VALUES,
326            // and not one of them was enforced -- the ID branch said
327            // "uniqueness checked loosely", which meant not at all.
328            match ad.att_type.as_str() {
329                "ID" | "IDREF" => {
330                    // Namespaces in XML erratum NE05: an ID or IDREF value is
331                    // an NCName, so a colon in one is a validity error. This
332                    // is stricter than libxml2, which does not check it -- but
333                    // it is a VALIDITY constraint, so it costs nothing at
334                    // parse time and refuses no document anyone can read.
335                    if v.contains(':') {
336                        return Err(format!(
337                            "Value {v} for attribute {aname} of {name} is not an NCName"
338                        ));
339                    }
340                    if !is_name(v) {
341                        return Err(format!(
342                            "Syntax of value for attribute {aname} of {name} is not valid"
343                        ));
344                    }
345                    if ad.att_type == "ID" {
346                        if ids.insert(v.clone(), ()).is_some() {
347                            return Err(format!("ID {v} already defined"));
348                        }
349                    } else {
350                        idrefs.push(v.clone());
351                    }
352                }
353                "IDREFS" => {
354                    let mut any = false;
355                    for part in v.split_space() {
356                        any = true;
357                        if part.contains(':') {
358                            return Err(format!(
359                                "Value {part} for attribute {aname} of {name} is not an NCName"
360                            ));
361                        }
362                        if !is_name(part) {
363                            return Err(format!(
364                                "Syntax of value for attribute {aname} of {name} is not valid"
365                            ));
366                        }
367                        idrefs.push(part.to_string());
368                    }
369                    if !any {
370                        return Err(format!(
371                            "Syntax of value for attribute {aname} of {name} is not valid"
372                        ));
373                    }
374                }
375                "NMTOKEN" => {
376                    if !is_nmtoken(v) {
377                        return Err(format!(
378                            "Syntax of value for attribute {aname} of {name} is not valid"
379                        ));
380                    }
381                }
382                "NMTOKENS" => {
383                    if v.split_space().next().is_none()
384                        || !v.split_space().all(is_nmtoken)
385                    {
386                        return Err(format!(
387                            "Syntax of value for attribute {aname} of {name} is not valid"
388                        ));
389                    }
390                }
391                "ENTITY" | "ENTITIES" => {
392                    for part in v.split_space() {
393                        if !is_name(part) {
394                            return Err(format!(
395                                "Syntax of value for attribute {aname} of {name} is not valid"
396                            ));
397                        }
398                        if !dtd.unparsed_entities.contains(part) {
399                            return Err(format!(
400                                "ENTITY attribute {aname} references an unknown entity \"{part}\""
401                            ));
402                        }
403                    }
404                }
405                _ => {}
406            }
407        }
408    }
409    // "Attribute Value Type": every attribute an element carries must be
410    // declared for that element type. Nothing checked, so `xml:space` on an
411    // element whose ATTLIST never mentions it was fine by us.
412    // A namespace declaration is an attribute as far as validity is concerned:
413    // it has to be declared, and a #FIXED default still binds its value. We
414    // keep ns declarations off the attribute chain, so nothing looked at them
415    // at all.
416    for (pre, uri) in doc.ns_defs(id) {
417        let q = match pre {
418            Some(p) => format!("xmlns:{p}"),
419            None => "xmlns".to_string(),
420        };
421        match dtd.attributes.get(&(name.clone(), q.clone())) {
422            None => return Err(format!("No declaration for attribute {q} of element {name}")),
423            Some(ad) => {
424                if ad.default == AttrDefault::Fixed {
425                    if let Some(fix) = ad.default_value.as_deref() {
426                        if uri != fix {
427                            return Err(format!(
428                                "Value for attribute {q} of {name} is different from default {fix}"
429                            ));
430                        }
431                    }
432                }
433            }
434        }
435    }
436    {
437        let mut a = doc.first_attr(id);
438        while let Some(x) = a {
439            let q = doc.qname(x);
440            if !dtd.attributes.contains_key(&(name.clone(), q.clone())) {
441                return Err(format!("No declaration for attribute {q} of element {name}"));
442            }
443            a = doc.next_sibling(x);
444        }
445    }
446    // "One ID per Element Type": an element type may carry at most one ID
447    // attribute, however the declarations are spread across ATTLISTs.
448    let id_attrs = dtd
449        .attributes
450        .iter()
451        .filter(|((e, _), ad)| e == &name && ad.att_type == "ID")
452        .count();
453    if id_attrs > 1 {
454        return Err(format!("Element {name} has {id_attrs} ID attributes"));
455    }
456    Ok(())
457}
458
459fn match_children_spec(spec: &str, kids: &[String]) -> bool {
460    let toks = tokenize_content(spec);
461    match_seq(&toks, kids, 0).contains(&kids.len())
462}
463
464#[derive(Clone, Debug)]
465enum Tok {
466    Name(String),
467    Seq(Vec<Tok>),
468    Choice(Vec<Tok>),
469    Star,
470    Plus,
471    Q,
472}
473
474fn tokenize_content(spec: &str) -> Vec<Tok> {
475    // Very small content-model parser: names, ',', '|', '*+?', parentheses.
476    let p = spec.trim();
477    fn parse_choice<'a>(p: &mut &'a str) -> Vec<Tok> {
478        let mut alts = vec![Tok::Seq(parse_seq(p))];
479        loop {
480            skip(p);
481            if p.starts_with('|') {
482                *p = &p[1..];
483                alts.push(Tok::Seq(parse_seq(p)));
484            } else {
485                break;
486            }
487        }
488        alts
489    }
490    fn parse_seq<'a>(p: &mut &'a str) -> Vec<Tok> {
491        let mut v = Vec::new();
492        loop {
493            skip(p);
494            if p.is_empty() || p.starts_with('|') || p.starts_with(')') {
495                break;
496            }
497            if p.starts_with(',') {
498                *p = &p[1..];
499                continue;
500            }
501            // A particle that consumes nothing is the end of what we can
502            // read, not a reason to try again.
503            //
504            // `<!ELEMENT doc (a & b)?>` used SGML's "and" connector: `&` is
505            // not a name character, so take_name returned "" and the position
506            // never moved. This loop pushed an empty Name forever -- 32 bytes
507            // of DTD grew a Vec until the process died asking for 32 GB. Any
508            // document with a DTD could do it to anything that validates.
509            let before = p.len();
510            let particle = parse_particle(p);
511            if p.len() == before {
512                break;
513            }
514            v.push(particle);
515        }
516        v
517    }
518    fn parse_particle<'a>(p: &mut &'a str) -> Tok {
519        skip(p);
520        let mut inner = if p.starts_with('(') {
521            *p = &p[1..];
522            let c = parse_choice(p);
523            skip(p);
524            if p.starts_with(')') {
525                *p = &p[1..];
526            }
527            if c.len() == 1 {
528                Tok::Seq(match c.into_iter().next().unwrap() {
529                    Tok::Seq(s) => s,
530                    other => vec![other],
531                })
532            } else {
533                Tok::Choice(c)
534            }
535        } else {
536            let name = take_name(p);
537            Tok::Name(name)
538        };
539        skip(p);
540        inner = match p.chars().next() {
541            Some('*') => {
542                *p = &p[1..];
543                Tok::Seq(vec![inner, Tok::Star])
544            }
545            Some('+') => {
546                *p = &p[1..];
547                Tok::Seq(vec![inner, Tok::Plus])
548            }
549            Some('?') => {
550                *p = &p[1..];
551                Tok::Seq(vec![inner, Tok::Q])
552            }
553            _ => inner,
554        };
555        inner
556    }
557    fn take_name<'a>(p: &mut &'a str) -> String {
558        let bytes = p.as_bytes();
559        let mut i = 0;
560        while i < bytes.len() {
561            let c = bytes[i] as char;
562            if c.is_ascii_alphanumeric() || "-._:".contains(c) {
563                i += 1;
564            } else {
565                break;
566            }
567        }
568        let s = p[..i].to_string();
569        *p = &p[i..];
570        s
571    }
572    fn skip(p: &mut &str) {
573        *p = p.trim_start();
574    }
575    let mut tmp = p;
576    parse_choice(&mut tmp)
577}
578
579fn match_seq(toks: &[Tok], kids: &[String], i: usize) -> Vec<usize> {
580    if toks.is_empty() {
581        return vec![i];
582    }
583    match &toks[0] {
584        Tok::Star => {
585            let rest = &toks[1..];
586            // Star applies to previous — encoded as Seq(inner, Star). Handle Seq instead.
587            match_seq(rest, kids, i)
588        }
589        Tok::Plus | Tok::Q => match_seq(&toks[1..], kids, i),
590        Tok::Name(n) => {
591            if i < kids.len() && &kids[i] == n {
592                match_seq(&toks[1..], kids, i + 1)
593            } else {
594                vec![]
595            }
596        }
597        Tok::Seq(inner) => {
598            let (body, quant) = split_quant(inner);
599            apply_quant(body, quant, &toks[1..], kids, i)
600        }
601        Tok::Choice(alts) => {
602            let mut out = Vec::new();
603            for a in alts {
604                let one = match_seq(&[a.clone()], kids, i);
605                for pos in one {
606                    out.extend(match_seq(&toks[1..], kids, pos));
607                }
608            }
609            out.sort();
610            out.dedup();
611            out
612        }
613    }
614}
615
616enum Quant {
617    One,
618    Q,
619    Star,
620    Plus,
621}
622
623fn split_quant(inner: &[Tok]) -> (&[Tok], Quant) {
624    if inner.len() >= 2 {
625        match inner.last() {
626            Some(Tok::Star) => return (&inner[..inner.len() - 1], Quant::Star),
627            Some(Tok::Plus) => return (&inner[..inner.len() - 1], Quant::Plus),
628            Some(Tok::Q) => return (&inner[..inner.len() - 1], Quant::Q),
629            _ => {}
630        }
631    }
632    (inner, Quant::One)
633}
634
635fn apply_quant(body: &[Tok], q: Quant, rest: &[Tok], kids: &[String], i: usize) -> Vec<usize> {
636    match q {
637        Quant::One => {
638            let mut out = Vec::new();
639            for p in match_seq(body, kids, i) {
640                out.extend(match_seq(rest, kids, p));
641            }
642            out
643        }
644        Quant::Q => {
645            let mut out = match_seq(rest, kids, i);
646            for p in match_seq(body, kids, i) {
647                out.extend(match_seq(rest, kids, p));
648            }
649            out.sort();
650            out.dedup();
651            out
652        }
653        Quant::Star => {
654            let mut out = match_seq(rest, kids, i);
655            let mut frontier = vec![i];
656            while let Some(p) = frontier.pop() {
657                for n in match_seq(body, kids, p) {
658                    if n > p {
659                        out.extend(match_seq(rest, kids, n));
660                        frontier.push(n);
661                    }
662                }
663            }
664            out.sort();
665            out.dedup();
666            out
667        }
668        Quant::Plus => {
669            let mut out = Vec::new();
670            for p in match_seq(body, kids, i) {
671                out.extend(apply_quant(body, Quant::Star, rest, kids, p));
672            }
673            out.sort();
674            out.dedup();
675            out
676        }
677    }
678}