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        if doc.name(root) != n.as_str() {
32            return Err(format!("root element {} does not match DOCTYPE {n}", doc.name(root)));
33        }
34    }
35    validate_element(doc, root, dtd)?;
36    Ok(())
37}
38
39fn validate_element(doc: &XmlDoc, id: NodeId, dtd: &XmlDtd) -> Result<(), String> {
40    if doc.kind(id) != NodeKind::Element {
41        return Ok(());
42    }
43    let name = doc.name(id).to_string();
44    if let Some(decl) = dtd.elements.get(&name) {
45        match decl {
46            ElementDecl::Empty => {
47                if doc.first_child(id).is_some() {
48                    return Err(format!("element {name} must be EMPTY"));
49                }
50            }
51            ElementDecl::Any => {}
52            ElementDecl::Mixed(_) => {
53                let mut c = doc.first_child(id);
54                while let Some(x) = c {
55                    match doc.kind(x) {
56                        NodeKind::Element => {
57                            if let ElementDecl::Mixed(allowed) = decl {
58                                if !allowed.is_empty() && !allowed.iter().any(|n| n == doc.name(x)) {
59                                    return Err(format!("element {} not allowed in mixed {name}", doc.name(x)));
60                                }
61                            }
62                            validate_element(doc, x, dtd)?;
63                        }
64                        NodeKind::Text | NodeKind::CData | NodeKind::Comment | NodeKind::Pi => {}
65                        _ => {}
66                    }
67                    c = doc.next_sibling(x);
68                }
69            }
70            ElementDecl::Children(spec) => {
71                let kids: Vec<String> = {
72                    let mut v = Vec::new();
73                    let mut c = doc.first_child(id);
74                    while let Some(x) = c {
75                        if doc.kind(x) == NodeKind::Element {
76                            v.push(doc.name(x).to_string());
77                            validate_element(doc, x, dtd)?;
78                        } else if doc.kind(x) == NodeKind::Text && !doc.xml_is_blank_node(x) {
79                            return Err(format!("character data not allowed in {name}"));
80                        }
81                        c = doc.next_sibling(x);
82                    }
83                    v
84                };
85                if !match_children_spec(spec, &kids) {
86                    return Err(format!("content of {name} does not match {spec}"));
87                }
88            }
89        }
90    }
91    for ((elem, aname), ad) in &dtd.attributes {
92        if elem != &name {
93            continue;
94        }
95        let have = doc.xml_get_prop(id, aname);
96        match ad.default {
97            AttrDefault::Required if have.is_none() => {
98                return Err(format!("attribute {aname} of {name} is required"));
99            }
100            AttrDefault::Fixed => {
101                if let (Some(v), Some(fix)) = (&have, &ad.default_value) {
102                    if v != fix {
103                        return Err(format!("attribute {aname} must be {fix}"));
104                    }
105                }
106            }
107            _ => {}
108        }
109        if let Some(v) = &have {
110            if !ad.enumerated.is_empty() && !ad.enumerated.iter().any(|e| e == v) {
111                return Err(format!("attribute {aname} value not in enumeration"));
112            }
113            if ad.att_type == "ID" {
114                // uniqueness checked loosely
115            }
116        }
117    }
118    Ok(())
119}
120
121fn match_children_spec(spec: &str, kids: &[String]) -> bool {
122    let toks = tokenize_content(spec);
123    match_seq(&toks, kids, 0).contains(&kids.len())
124}
125
126#[derive(Clone, Debug)]
127enum Tok {
128    Name(String),
129    Seq(Vec<Tok>),
130    Choice(Vec<Tok>),
131    Star,
132    Plus,
133    Q,
134}
135
136fn tokenize_content(spec: &str) -> Vec<Tok> {
137    // Very small content-model parser: names, ',', '|', '*+?', parentheses.
138    let p = spec.trim();
139    fn parse_choice<'a>(p: &mut &'a str) -> Vec<Tok> {
140        let mut alts = vec![Tok::Seq(parse_seq(p))];
141        loop {
142            skip(p);
143            if p.starts_with('|') {
144                *p = &p[1..];
145                alts.push(Tok::Seq(parse_seq(p)));
146            } else {
147                break;
148            }
149        }
150        alts
151    }
152    fn parse_seq<'a>(p: &mut &'a str) -> Vec<Tok> {
153        let mut v = Vec::new();
154        loop {
155            skip(p);
156            if p.is_empty() || p.starts_with('|') || p.starts_with(')') {
157                break;
158            }
159            if p.starts_with(',') {
160                *p = &p[1..];
161                continue;
162            }
163            v.push(parse_particle(p));
164        }
165        v
166    }
167    fn parse_particle<'a>(p: &mut &'a str) -> Tok {
168        skip(p);
169        let mut inner = if p.starts_with('(') {
170            *p = &p[1..];
171            let c = parse_choice(p);
172            skip(p);
173            if p.starts_with(')') {
174                *p = &p[1..];
175            }
176            if c.len() == 1 {
177                Tok::Seq(match c.into_iter().next().unwrap() {
178                    Tok::Seq(s) => s,
179                    other => vec![other],
180                })
181            } else {
182                Tok::Choice(c)
183            }
184        } else {
185            let name = take_name(p);
186            Tok::Name(name)
187        };
188        skip(p);
189        inner = match p.chars().next() {
190            Some('*') => {
191                *p = &p[1..];
192                Tok::Seq(vec![inner, Tok::Star])
193            }
194            Some('+') => {
195                *p = &p[1..];
196                Tok::Seq(vec![inner, Tok::Plus])
197            }
198            Some('?') => {
199                *p = &p[1..];
200                Tok::Seq(vec![inner, Tok::Q])
201            }
202            _ => inner,
203        };
204        inner
205    }
206    fn take_name<'a>(p: &mut &'a str) -> String {
207        let bytes = p.as_bytes();
208        let mut i = 0;
209        while i < bytes.len() {
210            let c = bytes[i] as char;
211            if c.is_ascii_alphanumeric() || "-._:".contains(c) {
212                i += 1;
213            } else {
214                break;
215            }
216        }
217        let s = p[..i].to_string();
218        *p = &p[i..];
219        s
220    }
221    fn skip(p: &mut &str) {
222        *p = p.trim_start();
223    }
224    let mut tmp = p;
225    parse_choice(&mut tmp)
226}
227
228fn match_seq(toks: &[Tok], kids: &[String], i: usize) -> Vec<usize> {
229    if toks.is_empty() {
230        return vec![i];
231    }
232    match &toks[0] {
233        Tok::Star => {
234            let rest = &toks[1..];
235            // Star applies to previous — encoded as Seq(inner, Star). Handle Seq instead.
236            match_seq(rest, kids, i)
237        }
238        Tok::Plus | Tok::Q => match_seq(&toks[1..], kids, i),
239        Tok::Name(n) => {
240            if i < kids.len() && &kids[i] == n {
241                match_seq(&toks[1..], kids, i + 1)
242            } else {
243                vec![]
244            }
245        }
246        Tok::Seq(inner) => {
247            let (body, quant) = split_quant(inner);
248            apply_quant(body, quant, &toks[1..], kids, i)
249        }
250        Tok::Choice(alts) => {
251            let mut out = Vec::new();
252            for a in alts {
253                let one = match_seq(&[a.clone()], kids, i);
254                for pos in one {
255                    out.extend(match_seq(&toks[1..], kids, pos));
256                }
257            }
258            out.sort();
259            out.dedup();
260            out
261        }
262    }
263}
264
265enum Quant {
266    One,
267    Q,
268    Star,
269    Plus,
270}
271
272fn split_quant(inner: &[Tok]) -> (&[Tok], Quant) {
273    if inner.len() >= 2 {
274        match inner.last() {
275            Some(Tok::Star) => return (&inner[..inner.len() - 1], Quant::Star),
276            Some(Tok::Plus) => return (&inner[..inner.len() - 1], Quant::Plus),
277            Some(Tok::Q) => return (&inner[..inner.len() - 1], Quant::Q),
278            _ => {}
279        }
280    }
281    (inner, Quant::One)
282}
283
284fn apply_quant(body: &[Tok], q: Quant, rest: &[Tok], kids: &[String], i: usize) -> Vec<usize> {
285    match q {
286        Quant::One => {
287            let mut out = Vec::new();
288            for p in match_seq(body, kids, i) {
289                out.extend(match_seq(rest, kids, p));
290            }
291            out
292        }
293        Quant::Q => {
294            let mut out = match_seq(rest, kids, i);
295            for p in match_seq(body, kids, i) {
296                out.extend(match_seq(rest, kids, p));
297            }
298            out.sort();
299            out.dedup();
300            out
301        }
302        Quant::Star => {
303            let mut out = match_seq(rest, kids, i);
304            let mut frontier = vec![i];
305            while let Some(p) = frontier.pop() {
306                for n in match_seq(body, kids, p) {
307                    if n > p {
308                        out.extend(match_seq(rest, kids, n));
309                        frontier.push(n);
310                    }
311                }
312            }
313            out.sort();
314            out.dedup();
315            out
316        }
317        Quant::Plus => {
318            let mut out = Vec::new();
319            for p in match_seq(body, kids, i) {
320                out.extend(apply_quant(body, Quant::Star, rest, kids, p));
321            }
322            out.sort();
323            out.dedup();
324            out
325        }
326    }
327}