1use std::sync::Arc;
2
3use quick_xml::escape::resolve_predefined_entity;
4use quick_xml::events::{BytesStart, Event};
5use quick_xml::Reader;
6use tatara_lisp_eval::ffi::Arity;
7use tatara_lisp_eval::{EvalError, Interpreter, MapKey, Value};
8
9use crate::errors::raise_kind;
10
11pub const MALFORMED: &str = "xml_malformed";
12pub const REFUSED: &str = "xml_refused";
13pub const MAX_DEPTH: usize = 1024;
14
15pub fn install_xml_stdlib<H: 'static>(interp: &mut Interpreter<H>) {
16 interp.register_fn(
17 "xml_parse",
18 Arity::Exact(1),
19 |args: &[Value], _h: &mut H, span| match &args[0] {
20 Value::Str(s) => parse(s).map_err(|f| f.raise(span)),
21 other => Err(EvalError::native_fn(
22 "xml_parse",
23 format!("expected a string of XML text, got {}", other.type_name()),
24 span,
25 )),
26 },
27 );
28 interp.register_fn(
29 "xml_render",
30 Arity::Exact(1),
31 |args: &[Value], _h: &mut H, span| {
32 render(&args[0])
33 .map(|s| Value::Str(Arc::from(s)))
34 .map_err(|f| f.raise(span))
35 },
36 );
37}
38
39#[derive(Debug, Clone, PartialEq, Eq)]
40pub enum Fault {
41 Malformed(String),
42 Refused(String),
43}
44
45impl Fault {
46 fn raise(self, span: tatara_lisp::Span) -> EvalError {
47 match self {
48 Fault::Malformed(m) => raise_kind(MALFORMED, m, span),
49 Fault::Refused(m) => raise_kind(REFUSED, m, span),
50 }
51 }
52}
53
54struct Open {
55 name: String,
56 attributes: Vec<(String, String)>,
57 children: Vec<Value>,
58 texts: Vec<String>,
59}
60
61impl Open {
62 fn close(self) -> Value {
63 let text = if self.children.is_empty() {
64 self.texts.concat()
65 } else {
66 self.texts
67 .into_iter()
68 .filter(|t| !t.chars().all(char::is_whitespace))
69 .collect::<String>()
70 };
71 element(self.name, self.attributes, self.children, text)
72 }
73}
74
75fn key(name: &str) -> MapKey {
76 MapKey::Keyword(Arc::from(name))
77}
78
79fn element(
80 name: String,
81 attributes: Vec<(String, String)>,
82 children: Vec<Value>,
83 text: String,
84) -> Value {
85 Value::map([
86 (key("name"), Value::Str(Arc::from(name))),
87 (
88 key("attributes"),
89 Value::map(
90 attributes
91 .into_iter()
92 .map(|(k, v)| (MapKey::Str(Arc::from(k)), Value::Str(Arc::from(v)))),
93 ),
94 ),
95 (key("children"), Value::list(children)),
96 (key("text"), Value::Str(Arc::from(text))),
97 ])
98}
99
100fn position(src: &str, byte: u64) -> String {
101 let end = usize::try_from(byte).unwrap_or(src.len()).min(src.len());
102 let before = src.get(..end).unwrap_or(src);
103 let line = before.matches('\n').count() + 1;
104 let col = before.rsplit('\n').next().map_or(0, |l| l.chars().count()) + 1;
105 format!("line {line}, column {col}")
106}
107
108fn entity_refused(name: &str) -> Fault {
109 Fault::Refused(format!(
110 "entity &{name}; is not expanded: blue reads only the five predefined entities and character references, never a DTD entity"
111 ))
112}
113
114fn resolve_ref(name: &str) -> Result<String, Fault> {
115 if let Some(num) = name.strip_prefix('#') {
116 let code = match num.strip_prefix('x') {
117 Some(hex) => u32::from_str_radix(hex, 16).ok(),
118 None => num.parse::<u32>().ok(),
119 };
120 return code
121 .and_then(char::from_u32)
122 .filter(|c| legal_char(*c))
123 .map(String::from)
124 .ok_or_else(|| {
125 Fault::Malformed(format!("&{name}; is not a legal character reference"))
126 });
127 }
128 resolve_predefined_entity(name)
129 .map(str::to_owned)
130 .ok_or_else(|| entity_refused(name))
131}
132
133fn legal_char(c: char) -> bool {
134 matches!(c, '\t' | '\n' | '\r') || (c >= ' ' && c != '\u{FFFE}' && c != '\u{FFFF}')
135}
136
137fn open(start: &BytesStart<'_>, src: &str, at: u64) -> Result<Open, Fault> {
138 let name = std::str::from_utf8(start.name().as_ref())
139 .map_err(|e| Fault::Malformed(e.to_string()))?
140 .to_owned();
141 let mut attributes: Vec<(String, String)> = Vec::new();
142 for attr in start.attributes() {
143 let attr = attr.map_err(|e| Fault::Malformed(format!("{e} at {}", position(src, at))))?;
144 let k = std::str::from_utf8(attr.key.as_ref())
145 .map_err(|e| Fault::Malformed(e.to_string()))?
146 .to_owned();
147 let mut unknown: Option<String> = None;
148 let v = attr
149 .unescape_value_with(|ent| {
150 let hit = resolve_predefined_entity(ent);
151 if hit.is_none() {
152 unknown = Some(ent.to_owned());
153 }
154 hit
155 })
156 .map_err(|e| match unknown.take() {
157 Some(ent) => entity_refused(&ent),
158 None => Fault::Malformed(format!("{e} at {}", position(src, at))),
159 })?
160 .into_owned();
161 attributes.push((k, v));
162 }
163 Ok(Open {
164 name,
165 attributes,
166 children: Vec::new(),
167 texts: Vec::new(),
168 })
169}
170
171fn doctype_declares_entity(body: &str) -> bool {
172 body.contains("<!ENTITY")
173}
174
175pub fn parse(src: &str) -> Result<Value, Fault> {
176 let mut reader = Reader::from_str(src);
177 reader.config_mut().check_end_names = true;
178 reader.config_mut().expand_empty_elements = false;
179 reader.config_mut().check_comments = true;
180 let mut stack: Vec<Open> = Vec::new();
181 let mut root: Option<Value> = None;
182 loop {
183 let at = reader.buffer_position();
184 let event = reader.read_event().map_err(|e| {
185 Fault::Malformed(format!("{e} at {}", position(src, reader.error_position())))
186 })?;
187 match event {
188 Event::Start(start) => {
189 if root.is_some() && stack.is_empty() {
190 return Err(Fault::Malformed(format!(
191 "a second root element at {}",
192 position(src, at)
193 )));
194 }
195 if stack.len() >= MAX_DEPTH {
196 return Err(Fault::Refused(format!(
197 "elements nest deeper than {MAX_DEPTH} at {}",
198 position(src, at)
199 )));
200 }
201 stack.push(open(&start, src, at)?);
202 }
203 Event::Empty(start) => {
204 if root.is_some() && stack.is_empty() {
205 return Err(Fault::Malformed(format!(
206 "a second root element at {}",
207 position(src, at)
208 )));
209 }
210 let done = open(&start, src, at)?.close();
211 match stack.last_mut() {
212 Some(parent) => parent.children.push(done),
213 None => root = Some(done),
214 }
215 }
216 Event::End(_) => {
217 let done = stack
218 .pop()
219 .ok_or_else(|| {
220 Fault::Malformed(format!(
221 "an end tag with no start at {}",
222 position(src, at)
223 ))
224 })?
225 .close();
226 match stack.last_mut() {
227 Some(parent) => parent.children.push(done),
228 None => root = Some(done),
229 }
230 }
231 Event::Text(t) => {
232 let text = t
233 .decode()
234 .map_err(|e| Fault::Malformed(e.to_string()))?
235 .into_owned();
236 match stack.last_mut() {
237 Some(el) => el.texts.push(text),
238 None if text.chars().all(char::is_whitespace) => {}
239 None => {
240 return Err(Fault::Malformed(format!(
241 "text outside the root element at {}",
242 position(src, at)
243 )))
244 }
245 }
246 }
247 Event::CData(c) => {
248 let text = c
249 .decode()
250 .map_err(|e| Fault::Malformed(e.to_string()))?
251 .into_owned();
252 match stack.last_mut() {
253 Some(el) => el.texts.push(text),
254 None => {
255 return Err(Fault::Malformed(format!(
256 "CDATA outside the root element at {}",
257 position(src, at)
258 )))
259 }
260 }
261 }
262 Event::GeneralRef(r) => {
263 let name = r.decode().map_err(|e| Fault::Malformed(e.to_string()))?;
264 let text = resolve_ref(&name)?;
265 match stack.last_mut() {
266 Some(el) => el.texts.push(text),
267 None => {
268 return Err(Fault::Malformed(format!(
269 "a reference outside the root element at {}",
270 position(src, at)
271 )))
272 }
273 }
274 }
275 Event::DocType(d) => {
276 let body = d.decode().map_err(|e| Fault::Malformed(e.to_string()))?;
277 if root.is_some() || !stack.is_empty() {
278 return Err(Fault::Malformed(format!(
279 "a DOCTYPE after the root element began at {}",
280 position(src, at)
281 )));
282 }
283 if doctype_declares_entity(&body) {
284 return Err(Fault::Refused(
285 "the DOCTYPE declares an entity: blue never expands or fetches DTD entities"
286 .to_owned(),
287 ));
288 }
289 }
290 Event::Comment(_) | Event::Decl(_) | Event::PI(_) => {}
291 Event::Eof => break,
292 }
293 }
294 if let Some(unclosed) = stack.last() {
295 return Err(Fault::Malformed(format!(
296 "<{}> is never closed",
297 unclosed.name
298 )));
299 }
300 root.ok_or_else(|| Fault::Malformed("the document has no root element".to_owned()))
301}
302
303fn field<'a>(el: &'a Value, name: &str) -> Option<&'a Value> {
304 match el {
305 Value::Map(m) => m
306 .iter()
307 .find(|(k, _)| matches!(k, MapKey::Keyword(s) | MapKey::Str(s) if &**s == name))
308 .map(|(_, v)| v),
309 _ => None,
310 }
311}
312
313fn scalar(v: &Value) -> Option<String> {
314 match v {
315 Value::Str(s) | Value::Symbol(s) | Value::Keyword(s) => Some(s.to_string()),
316 Value::Int(n) => Some(n.to_string()),
317 Value::Float(n) => Some(n.to_string()),
318 Value::Bool(b) => Some(b.to_string()),
319 _ => None,
320 }
321}
322
323fn map_key_text(k: &MapKey) -> Option<String> {
324 match k {
325 MapKey::Str(s) | MapKey::Symbol(s) | MapKey::Keyword(s) => Some(s.to_string()),
326 _ => None,
327 }
328}
329
330fn name_char(c: char, first: bool) -> bool {
331 c == '_'
332 || c == ':'
333 || c.is_alphabetic()
334 || (!first && (c == '-' || c == '.' || c.is_numeric()))
335 || (!c.is_ascii() && !c.is_whitespace() && !c.is_control())
336}
337
338fn checked_name(name: &str, what: &str) -> Result<(), Fault> {
339 let mut chars = name.chars();
340 let ok = chars.next().is_some_and(|c| name_char(c, true)) && chars.all(|c| name_char(c, false));
341 if ok {
342 Ok(())
343 } else {
344 Err(Fault::Refused(format!(
345 "{what} {name:?} is not an XML name"
346 )))
347 }
348}
349
350fn escape_into(out: &mut String, text: &str, in_attribute: bool) -> Result<(), Fault> {
351 for c in text.chars() {
352 match c {
353 '&' => out.push_str("&"),
354 '<' => out.push_str("<"),
355 '>' => out.push_str(">"),
356 '"' if in_attribute => out.push_str("""),
357 '\t' if in_attribute => out.push_str("	"),
358 '\n' if in_attribute => out.push_str(" "),
359 '\r' => out.push_str(" "),
360 c if legal_char(c) => out.push(c),
361 c => {
362 return Err(Fault::Refused(format!(
363 "U+{:04X} cannot appear in an XML 1.0 document",
364 u32::from(c)
365 )))
366 }
367 }
368 }
369 Ok(())
370}
371
372fn render_into(out: &mut String, el: &Value, depth: usize) -> Result<(), Fault> {
373 if depth >= MAX_DEPTH {
374 return Err(Fault::Refused(format!(
375 "elements nest deeper than {MAX_DEPTH}"
376 )));
377 }
378 let name = field(el, "name").and_then(scalar).ok_or_else(|| {
379 Fault::Refused(format!(
380 "an element is a map with a name; got {}",
381 el.type_name()
382 ))
383 })?;
384 checked_name(&name, "element name")?;
385 out.push('<');
386 out.push_str(&name);
387 match field(el, "attributes") {
388 None | Some(Value::Nil) => {}
389 Some(Value::Map(attrs)) => {
390 let mut pairs: Vec<(String, &Value)> = Vec::new();
391 for (k, v) in attrs.iter() {
392 let k = map_key_text(k)
393 .ok_or_else(|| Fault::Refused("an attribute name is text".to_owned()))?;
394 pairs.push((k, v));
395 }
396 pairs.sort_by(|a, b| a.0.cmp(&b.0));
397 for (k, v) in pairs {
398 checked_name(&k, "attribute name")?;
399 let v = scalar(v).ok_or_else(|| {
400 Fault::Refused(format!("attribute {k} is not text: {}", v.type_name()))
401 })?;
402 out.push(' ');
403 out.push_str(&k);
404 out.push_str("=\"");
405 escape_into(out, &v, true)?;
406 out.push('"');
407 }
408 }
409 Some(other) => {
410 return Err(Fault::Refused(format!(
411 "attributes is a map; got {}",
412 other.type_name()
413 )))
414 }
415 }
416 let text = match field(el, "text") {
417 None | Some(Value::Nil) => String::new(),
418 Some(v) => scalar(v)
419 .ok_or_else(|| Fault::Refused(format!("text is text; got {}", v.type_name())))?,
420 };
421 let children: Vec<Value> = match field(el, "children") {
422 None | Some(Value::Nil) => Vec::new(),
423 Some(Value::List(xs)) => xs.iter().cloned().collect(),
424 Some(other) => {
425 return Err(Fault::Refused(format!(
426 "children is a list; got {}",
427 other.type_name()
428 )))
429 }
430 };
431 if text.is_empty() && children.is_empty() {
432 out.push_str("/>");
433 return Ok(());
434 }
435 out.push('>');
436 escape_into(out, &text, false)?;
437 for child in &children {
438 render_into(out, child, depth + 1)?;
439 }
440 out.push_str("</");
441 out.push_str(&name);
442 out.push('>');
443 Ok(())
444}
445
446pub fn render(el: &Value) -> Result<String, Fault> {
447 let mut out = String::new();
448 render_into(&mut out, el, 0)?;
449 Ok(out)
450}
451
452#[cfg(test)]
453mod tests {
454 use super::*;
455
456 fn get<'a>(el: &'a Value, k: &str) -> &'a Value {
457 field(el, k).unwrap_or_else(|| panic!("no {k} in {el:?}"))
458 }
459
460 fn text(el: &Value) -> String {
461 scalar(get(el, "text")).unwrap_or_default()
462 }
463
464 fn kids(el: &Value) -> Vec<Value> {
465 match get(el, "children") {
466 Value::List(xs) => xs.iter().cloned().collect(),
467 other => panic!("children {other:?}"),
468 }
469 }
470
471 #[test]
472 fn an_empty_document_is_malformed_not_nil() {
473 assert!(matches!(parse(""), Err(Fault::Malformed(_))));
474 assert!(matches!(parse(" \n"), Err(Fault::Malformed(_))));
475 }
476
477 #[test]
478 fn a_parsed_document_renders_and_parses_back_to_itself() {
479 let src = "<?xml version=\"1.0\"?><!DOCTYPE r><r a=\"1 & <2> "q"\" ns:b=\"x y\"><c>t & u</c><c/><![CDATA[<raw>&]]></r>";
480 let once = render(&parse(src).expect("parses")).expect("renders");
481 let again = render(&parse(&once).expect("reparses")).expect("renders again");
482 assert_eq!(once, again);
483 assert_eq!(
484 once,
485 "<r a=\"1 & <2> "q"\" ns:b=\"x y\"><raw>&<c>t & u</c><c/></r>"
486 );
487 }
488
489 #[test]
490 fn values_are_unescaped_and_prefixes_kept() {
491 let el = parse("<x:r xmlns:x=\"urn:example\" a=\"<AB\">1 > 0</x:r>")
492 .expect("parses");
493 assert_eq!(scalar(get(&el, "name")).as_deref(), Some("x:r"));
494 let Value::Map(attrs) = get(&el, "attributes") else {
495 panic!()
496 };
497 assert_eq!(
498 attrs
499 .get(&MapKey::Str(Arc::from("a")))
500 .and_then(scalar)
501 .as_deref(),
502 Some("<AB")
503 );
504 assert_eq!(
505 attrs
506 .get(&MapKey::Str(Arc::from("xmlns:x")))
507 .and_then(scalar)
508 .as_deref(),
509 Some("urn:example")
510 );
511 assert_eq!(text(&el), "1 > 0");
512 }
513
514 #[test]
515 fn whitespace_between_elements_is_not_text() {
516 let el = parse("<a>\n <b> x </b>\n <c/>\n</a>").expect("parses");
517 assert_eq!(text(&el), "");
518 assert_eq!(kids(&el).len(), 2);
519 assert_eq!(text(&kids(&el)[0]), " x ");
520 }
521
522 #[test]
523 fn malformed_documents_are_refused_by_kind() {
524 for bad in [
525 "<a>",
526 "<a></b>",
527 "<a><b></a></b>",
528 "</a>",
529 "<a/><b/>",
530 "text<a/>",
531 "<a x=1/>",
532 "<a x=\"1\" x=\"2\"/>",
533 "<a>�</a>",
534 ] {
535 assert!(
536 matches!(parse(bad), Err(Fault::Malformed(_))),
537 "{bad}: {:?}",
538 parse(bad)
539 );
540 }
541 }
542
543 #[test]
544 fn external_and_internal_entities_are_never_expanded() {
545 let xxe = "<?xml version=\"1.0\"?><!DOCTYPE r [<!ENTITY x SYSTEM \"file:///etc/passwd\">]><r>&x;</r>";
546 assert!(matches!(parse(xxe), Err(Fault::Refused(_))));
547 let laughs = "<!DOCTYPE r [<!ENTITY a \"ha\"><!ENTITY b \"&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;\">]><r>&b;</r>";
548 assert!(matches!(parse(laughs), Err(Fault::Refused(_))));
549 assert!(matches!(parse("<r>&x;</r>"), Err(Fault::Refused(_))));
550 assert!(matches!(parse("<r a=\"&x;\"/>"), Err(Fault::Refused(_))));
551 let unreferenced = "<!DOCTYPE r [<!ENTITY x SYSTEM \"file:///etc/passwd\">]><r/>";
552 assert!(matches!(parse(unreferenced), Err(Fault::Refused(_))));
553 let declared_only =
554 "<!DOCTYPE issues [<!ELEMENT issues (issue*)><!ATTLIST issues v CDATA \"\">]><issues/>";
555 assert!(parse(declared_only).is_ok());
556 }
557
558 #[test]
559 fn nesting_beyond_the_bound_is_refused() {
560 let deep = format!(
561 "{}{}",
562 "<a>".repeat(MAX_DEPTH + 1),
563 "</a>".repeat(MAX_DEPTH + 1)
564 );
565 assert!(matches!(parse(&deep), Err(Fault::Refused(_))));
566 let ok = format!("{}{}", "<a>".repeat(MAX_DEPTH), "</a>".repeat(MAX_DEPTH));
567 assert!(parse(&ok).is_ok());
568 }
569
570 #[test]
571 fn faults_name_their_line_and_column() {
572 let Err(Fault::Malformed(m)) = parse("<a>\n\n x <b></a>") else {
573 panic!()
574 };
575 assert!(m.contains("line 3, column"), "{m}");
576 let Err(Fault::Malformed(m)) = parse("<a/>\n <b/>") else {
577 panic!()
578 };
579 assert!(m.ends_with("line 2, column 3"), "{m}");
580 let Err(Fault::Malformed(m)) = parse("<a/>\n<!DOCTYPE a>") else {
581 panic!()
582 };
583 assert!(m.contains("DOCTYPE"), "{m}");
584 assert!(parse("\n <r/>\n").is_ok());
585 }
586
587 #[test]
588 fn render_is_bounded_like_parse_and_checks_names() {
589 let mut el = element("a".into(), vec![], vec![], String::new());
590 for _ in 1..MAX_DEPTH {
591 el = element("a".into(), vec![], vec![el], String::new());
592 }
593 let text = render(&el).expect("the deepest document parse accepts renders");
594 assert!(parse(&text).is_ok());
595 let deeper = element("a".into(), vec![], vec![el], String::new());
596 assert!(matches!(render(&deeper), Err(Fault::Refused(_))));
597 for ok in ["a", "a1", "a-b.c", "_x", "x:y", "\u{e9}t\u{e9}"] {
598 let el = element(ok.into(), vec![], vec![], String::new());
599 assert!(render(&el).is_ok(), "{ok}");
600 }
601 for bad in ["1a", "-a", ".a", "a b", "a\u{2003}b", "a\u{85}b", ""] {
602 let el = element(bad.into(), vec![], vec![], String::new());
603 assert!(matches!(render(&el), Err(Fault::Refused(_))), "{bad:?}");
604 }
605 let kinds = Value::map([
606 (key("name"), Value::Str(Arc::from("r"))),
607 (
608 key("attributes"),
609 Value::map([
610 (MapKey::Str(Arc::from("b")), Value::Bool(true)),
611 (MapKey::Str(Arc::from("f")), Value::Float(1.5)),
612 (MapKey::Str(Arc::from("n")), Value::Int(7)),
613 ]),
614 ),
615 ]);
616 assert_eq!(
617 render(&kinds).expect("renders"),
618 "<r b=\"true\" f=\"1.5\" n=\"7\"/>"
619 );
620 }
621
622 #[test]
623 fn render_escapes_and_refuses_what_xml_cannot_hold() {
624 let el = element(
625 "r".into(),
626 vec![("q".into(), "a\"<&>\t".into())],
627 vec![],
628 "x < y & z".into(),
629 );
630 assert_eq!(
631 render(&el).expect("renders"),
632 "<r q=\"a"<&>	\">x < y & z</r>"
633 );
634 let plain = element("r".into(), vec![], vec![], "a\"b\n\tc".into());
635 assert_eq!(render(&plain).expect("renders"), "<r>a\"b\n\tc</r>");
636 let bad = element("r x".into(), vec![], vec![], String::new());
637 assert!(matches!(render(&bad), Err(Fault::Refused(_))));
638 let ctl = element("r".into(), vec![], vec![], "\u{1}".into());
639 assert!(matches!(render(&ctl), Err(Fault::Refused(_))));
640 assert!(matches!(render(&Value::Int(1)), Err(Fault::Refused(_))));
641 }
642
643 #[test]
644 fn the_primitives_raise_typed_kinds_in_blue() {
645 let err = crate::run(r#"xml_parse("<a>")"#).expect_err("must raise");
646 assert!(err.to_string().contains(MALFORMED), "{err}");
647 let err = crate::run(r#"xml_parse("<r>&x;</r>")"#).expect_err("must raise");
648 assert!(err.to_string().contains(REFUSED), "{err}");
649 let v = crate::run(r#"xml_render(xml_parse("<a b=\"1\"><c/></a>"))"#)
650 .expect("runs")
651 .value;
652 assert_eq!(scalar(&v).as_deref(), Some("<a b=\"1\"><c/></a>"));
653 }
654}