Skip to main content

base/
xml.rs

1// Bounded, namespace-aware XML subset for interchange. No DTD, entities, I/O or XInclude.
2// New native functionality; deliberately rejects constructs it cannot represent losslessly.
3use alloc::{string::String, vec::Vec};
4
5/// Expanded XML name (prefixes are not identities).
6#[derive(Clone, Debug, PartialEq, Eq)]
7pub struct Name {
8    pub ns: String,
9    pub local: String,
10}
11/// Element-only or text-only node; mixed content is rejected by this subset reader.
12#[derive(Clone, Debug, PartialEq, Eq)]
13pub struct Node {
14    pub name: Name,
15    pub attrs: Vec<(Name, String)>,
16    pub text: String,
17    pub children: Vec<Node>,
18}
19impl Node {
20    /// Test an expanded name.
21    pub fn is(&self, ns: &str, local: &str) -> bool {
22        self.name.ns == ns && self.name.local == local
23    }
24    /// Attribute lookup; the default namespace never applies to unprefixed attributes.
25    pub fn attr(&self, ns: &str, local: &str) -> Option<&str> {
26        self.attrs
27            .iter()
28            .find(|(n, _)| n.ns == ns && n.local == local)
29            .map(|(_, v)| v.as_str())
30    }
31}
32/// Syntax error, resource limit, or an intentionally unsupported XML construct.
33#[derive(Clone, Copy, Debug, PartialEq, Eq)]
34#[non_exhaustive]
35pub enum Error {
36    Syntax,
37    Limit,
38    Unsupported,
39}
40/// XML 1.0 character validity (also used before emitting document text).
41pub fn valid_char(c: char) -> bool {
42    matches!(c as u32, 9 | 10 | 13 | 0x20..=0xd7ff | 0xe000..=0xfffd | 0x10000..=0x10ffff)
43}
44/// Escape text/attribute content, including CR so it survives XML newline normalization.
45pub fn escape(s: &str) -> Result<String, Error> {
46    let mut out = String::new();
47    for c in s.chars() {
48        if !valid_char(c) {
49            return Err(Error::Syntax);
50        }
51        match c {
52            '&' => out.push_str("&amp;"),
53            '<' => out.push_str("&lt;"),
54            '>' => out.push_str("&gt;"),
55            '"' => out.push_str("&quot;"),
56            '\'' => out.push_str("&apos;"),
57            '\r' => out.push_str("&#13;"),
58            '\n' => out.push_str("&#10;"),
59            '\t' => out.push_str("&#9;"),
60            _ => out.push(c),
61        }
62    }
63    Ok(out)
64}
65fn unescape(s: &str) -> Result<String, Error> {
66    if s.contains('<') || s.contains("]]>") {
67        return Err(Error::Syntax);
68    }
69    let mut out = String::new();
70    let mut rest = s;
71    while let Some(at) = rest.find('&') {
72        out.push_str(&rest[..at]);
73        rest = &rest[at + 1..];
74        let end = rest.find(';').ok_or(Error::Syntax)?;
75        let e = &rest[..end];
76        let c = match e {
77            "amp" => '&',
78            "lt" => '<',
79            "gt" => '>',
80            "quot" => '"',
81            "apos" => '\'',
82            _ => {
83                let (digits, radix) = if let Some(d) = e.strip_prefix("#x") {
84                    (d, 16)
85                } else if let Some(d) = e.strip_prefix('#') {
86                    (d, 10)
87                } else {
88                    return Err(Error::Unsupported);
89                };
90                if digits.is_empty()
91                    || !digits.bytes().all(|b| {
92                        if radix == 16 {
93                            b.is_ascii_hexdigit()
94                        } else {
95                            b.is_ascii_digit()
96                        }
97                    })
98                {
99                    return Err(Error::Syntax);
100                }
101                char::from_u32(u32::from_str_radix(digits, radix).map_err(|_| Error::Syntax)?)
102                    .ok_or(Error::Syntax)?
103            }
104        };
105        if !valid_char(c) {
106            return Err(Error::Syntax);
107        }
108        out.push(c);
109        rest = &rest[end + 1..];
110    }
111    out.push_str(rest);
112    Ok(out)
113}
114type Namespaces = Vec<(String, String)>;
115fn expand(q: &str, namespaces: &Namespaces, attr: bool) -> Result<Name, Error> {
116    let (prefix, local) = q.split_once(':').unwrap_or(("", q));
117    if local.is_empty() || local.contains(':') {
118        return Err(Error::Syntax);
119    }
120    let ns = if prefix.is_empty() && attr {
121        ""
122    } else if prefix == "xml" {
123        "http://www.w3.org/XML/1998/namespace"
124    } else {
125        match namespaces.iter().rev().find(|(p, _)| p == prefix) {
126            Some((_, n)) => n.as_str(),
127            None if prefix.is_empty() => "",
128            _ => return Err(Error::Syntax),
129        }
130    };
131    Ok(Name {
132        ns: String::from(ns),
133        local: String::from(local),
134    })
135}
136struct Parser<'a> {
137    s: &'a str,
138    pos: usize,
139    nodes: usize,
140}
141impl<'a> Parser<'a> {
142    fn rest(&self) -> &'a str {
143        &self.s[self.pos..]
144    }
145    fn eat(&mut self, s: &str) -> bool {
146        if self.rest().starts_with(s) {
147            self.pos += s.len();
148            true
149        } else {
150            false
151        }
152    }
153    fn ws(&mut self) {
154        while self
155            .rest()
156            .as_bytes()
157            .first()
158            .is_some_and(|b| matches!(b, b' ' | b'\t' | b'\n' | b'\r'))
159        {
160            self.pos += 1;
161        }
162    }
163    fn name(&mut self) -> Result<&'a str, Error> {
164        let start = self.pos;
165        if !self
166            .rest()
167            .as_bytes()
168            .first()
169            .is_some_and(|b| b.is_ascii_alphabetic() || *b == b'_')
170        {
171            return Err(Error::Syntax);
172        }
173        while self
174            .rest()
175            .as_bytes()
176            .first()
177            .is_some_and(|b| b.is_ascii_alphanumeric() || b"_:.-".contains(b))
178        {
179            self.pos += 1;
180        }
181        Ok(&self.s[start..self.pos])
182    }
183    fn node(&mut self, ns: &Namespaces, depth: usize) -> Result<Node, Error> {
184        self.nodes += 1;
185        if depth > 48 || self.nodes > 30000 {
186            return Err(Error::Limit);
187        }
188        if !self.eat("<") {
189            return Err(Error::Syntax);
190        }
191        if self.rest().starts_with(['!', '?']) {
192            return Err(Error::Unsupported);
193        }
194        let q = self.name()?;
195        let mut scope = ns.clone();
196        let mut attrs = Vec::new();
197        let empty;
198        loop {
199            let before = self.pos;
200            self.ws();
201            if self.eat("/>") {
202                empty = true;
203                break;
204            }
205            if self.eat(">") {
206                empty = false;
207                break;
208            }
209            if before == self.pos || attrs.len() >= 64 {
210                return Err(Error::Syntax);
211            }
212            let name = self.name()?;
213            self.ws();
214            if !self.eat("=") {
215                return Err(Error::Syntax);
216            }
217            self.ws();
218            let quote = if self.eat("\"") {
219                '"'
220            } else if self.eat("'") {
221                '\''
222            } else {
223                return Err(Error::Syntax);
224            };
225            let end = self.rest().find(quote).ok_or(Error::Syntax)?;
226            let value = unescape(&self.rest()[..end].replace(['\n', '\t'], " "))?;
227            self.pos += end + 1;
228            if attrs.iter().any(|(n, _)| *n == name) {
229                return Err(Error::Syntax);
230            }
231            if name == "xmlns" || name.starts_with("xmlns:") {
232                let prefix = name.strip_prefix("xmlns:").unwrap_or("");
233                if prefix == "xmlns"
234                    || (!prefix.is_empty() && value.is_empty())
235                    || value == "http://www.w3.org/2000/xmlns/"
236                    || (prefix == "xml") != (value == "http://www.w3.org/XML/1998/namespace")
237                {
238                    return Err(Error::Syntax);
239                }
240                scope.push((String::from(prefix), value.clone()));
241                if scope.len() > 64
242                    || scope.iter().map(|(p, n)| p.len() + n.len()).sum::<usize>() > 4096
243                {
244                    return Err(Error::Limit);
245                }
246            }
247            attrs.push((name, value));
248        }
249        let mut node = Node {
250            name: expand(q, &scope, false)?,
251            attrs: Vec::new(),
252            text: String::new(),
253            children: Vec::new(),
254        };
255        for (q, v) in attrs {
256            if q == "xmlns" || q.starts_with("xmlns:") {
257                continue;
258            }
259            let n = expand(q, &scope, true)?;
260            if node.attrs.iter().any(|(a, _)| *a == n) {
261                return Err(Error::Syntax);
262            }
263            node.attrs.push((n, v));
264        }
265        if empty {
266            return Ok(node);
267        }
268        loop {
269            if self.eat("</") {
270                let end = self.name()?;
271                self.ws();
272                if end != q || !self.eat(">") {
273                    return Err(Error::Syntax);
274                }
275                break;
276            }
277            if self.rest().starts_with('<') {
278                node.children.push(self.node(&scope, depth + 1)?);
279            } else {
280                let end = self.rest().find('<').ok_or(Error::Syntax)?;
281                node.text.push_str(&unescape(&self.rest()[..end])?);
282                self.pos += end;
283            }
284        }
285        if !node.children.is_empty() {
286            if !node.text.trim_matches([' ', '\t', '\r', '\n']).is_empty() {
287                return Err(Error::Unsupported);
288            }
289            node.text.clear();
290        }
291        Ok(node)
292    }
293}
294/// Parse a bounded XML subset. DTDs, processing instructions, comments, CDATA and mixed
295/// content are explicitly rejected. No resources are ever resolved or fetched.
296pub fn parse(s: &str) -> Result<Node, Error> {
297    if s.len() > 4 * 1024 * 1024 {
298        return Err(Error::Limit);
299    }
300    if !s.chars().all(valid_char) {
301        return Err(Error::Syntax);
302    }
303    let normalized = s
304        .strip_prefix('\u{feff}')
305        .unwrap_or(s)
306        .replace("\r\n", "\n")
307        .replace('\r', "\n");
308    let mut p = Parser {
309        s: &normalized,
310        pos: 0,
311        nodes: 0,
312    };
313    if p.eat("<?xml ") {
314        let end = p.rest().find("?>").ok_or(Error::Syntax)?;
315        let decl = &p.rest()[..end];
316        let declaration = alloc::format!("<declaration {decl}/>");
317        let mut dp = Parser {
318            s: &declaration,
319            pos: 0,
320            nodes: 0,
321        };
322        let dn = dp.node(&Vec::new(), 0)?;
323        if dn.attrs.is_empty()
324            || dn.attrs[0].0.local != "version"
325            || dn.attrs[0].1 != "1.0"
326            || !dp.rest().is_empty()
327        {
328            return Err(Error::Unsupported);
329        }
330        let mut last = 0;
331        for (name, value) in &dn.attrs {
332            let order = match name.local.as_str() {
333                "version" if value == "1.0" => 1,
334                "encoding"
335                    if matches!(
336                        value.as_str(),
337                        "UTF-8" | "utf-8" | "UTF-16" | "utf-16" | "UTF-16LE" | "UTF-16BE"
338                    ) =>
339                {
340                    2
341                }
342                "standalone" if matches!(value.as_str(), "yes" | "no") => 3,
343                _ => return Err(Error::Unsupported),
344            };
345            if !name.ns.is_empty() || order <= last {
346                return Err(Error::Syntax);
347            }
348            last = order;
349        }
350        p.pos += end + 2;
351    }
352    p.ws();
353    let node = p.node(&Vec::new(), 0)?;
354    p.ws();
355    if !p.rest().is_empty() {
356        return Err(Error::Syntax);
357    }
358    Ok(node)
359}