1use indexmap::IndexMap;
22use quick_xml::events::{BytesStart, Event};
23use quick_xml::Reader;
24use serde::{Deserialize, Serialize};
25use std::fmt;
26
27pub type Attrs = IndexMap<String, String>;
29
30#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
32pub struct XmlElement {
33 pub name: String,
35 pub attributes: Attrs,
36 pub children: Vec<XmlElement>,
37 #[serde(default, skip_serializing_if = "Option::is_none")]
38 pub text: Option<String>,
39}
40
41impl XmlElement {
42 pub fn new(name: impl Into<String>) -> Self {
43 Self {
44 name: name.into(),
45 ..Self::default()
46 }
47 }
48
49 pub fn local_name(&self) -> &str {
51 local_name(&self.name)
52 }
53
54 pub fn attr(&self, name: &str) -> Option<&str> {
55 self.attributes.get(name).map(String::as_str)
56 }
57}
58
59pub fn local_name(name: &str) -> &str {
61 match name.find(':') {
62 Some(i) => &name[i + 1..],
63 None => name,
64 }
65}
66
67#[derive(Debug, Clone, PartialEq, Eq)]
68pub struct XmlError(pub String);
69
70impl fmt::Display for XmlError {
71 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72 f.write_str(&self.0)
73 }
74}
75
76impl std::error::Error for XmlError {}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80pub enum Visit {
81 All,
83 Skip,
85 ElementsOnly,
87}
88
89pub trait XmlSink {
91 fn start(&mut self, name: &str, local: &str, attributes: Attrs, self_closing: bool) -> Visit;
94 fn text(&mut self, text: &str);
96 fn end(&mut self, name: &str);
97}
98
99fn err(e: impl fmt::Display) -> XmlError {
100 XmlError(e.to_string())
101}
102
103fn utf8(bytes: &[u8]) -> Result<&str, XmlError> {
104 std::str::from_utf8(bytes).map_err(err)
105}
106
107fn attributes(e: &BytesStart<'_>) -> Result<Attrs, XmlError> {
108 let mut attrs = Attrs::new();
109 for a in e.attributes().with_checks(false) {
111 let a = a.map_err(err)?;
112 let key = utf8(a.key.as_ref())?;
113 let value = decode_entities(utf8(&a.value)?);
114 attrs.insert(key.to_owned(), value);
115 }
116 Ok(attrs)
117}
118
119pub fn scan_xml(xml: &str, sink: &mut impl XmlSink) -> Result<bool, XmlError> {
122 let mut reader = Reader::from_str(xml);
123 let config = reader.config_mut();
124 config.trim_text(false);
125 config.check_end_names = true;
126 config.expand_empty_elements = false;
127 let mut want_text: Vec<bool> = Vec::new();
129 let mut seen_root = false;
130 let mut run = String::new();
133 loop {
134 let event = reader.read_event().map_err(err)?;
135 match &event {
136 Event::Text(t) => {
137 if want_text.last() == Some(&true) {
138 run.push_str(utf8(t)?);
139 }
140 continue;
141 }
142 Event::GeneralRef(r) => {
143 if want_text.last() == Some(&true) {
144 run.push('&');
145 run.push_str(utf8(r)?);
146 run.push(';');
147 }
148 continue;
149 }
150 _ if !run.is_empty() => {
151 sink.text(&decode_entities(&run));
152 run.clear();
153 }
154 _ => {}
155 }
156 match event {
157 Event::Start(e) => {
158 let name = utf8(e.name().into_inner())?;
159 let visit = sink.start(name, local_name(name), attributes(&e)?, false);
160 seen_root = true;
161 if visit == Visit::Skip {
162 reader.read_to_end(e.name()).map_err(err)?;
163 if want_text.is_empty() {
164 return Ok(true);
165 }
166 } else {
167 want_text.push(visit == Visit::All);
168 }
169 }
170 Event::Empty(e) => {
171 let name = utf8(e.name().into_inner())?;
172 if sink.start(name, local_name(name), attributes(&e)?, true) != Visit::Skip {
173 sink.end(name);
174 }
175 if want_text.is_empty() {
176 return Ok(true);
177 }
178 }
179 Event::End(e) => {
180 if want_text.pop().is_none() {
181 return Err(XmlError(format!(
182 "Unexpected closing tag </{}>",
183 String::from_utf8_lossy(e.name().into_inner())
184 )));
185 }
186 sink.end(utf8(e.name().into_inner())?);
187 if want_text.is_empty() {
188 return Ok(true);
189 }
190 }
191 Event::CData(t) => {
192 if want_text.last() == Some(&true) {
193 sink.text(utf8(&t)?);
194 }
195 }
196 Event::Eof => {
197 if !want_text.is_empty() {
198 return Err(XmlError(format!(
199 "Expected \"</\" at position {}",
200 reader.buffer_position()
201 )));
202 }
203 return Ok(seen_root);
204 }
205 _ => {}
207 }
208 }
209}
210
211pub fn parse_xml(xml: &str) -> Result<XmlElement, XmlError> {
213 let mut b = TreeBuilder::default();
214 if !scan_xml(xml, &mut b)? || b.root.is_none() {
215 return Err(XmlError(
216 "Failed to parse XML: no root element found".into(),
217 ));
218 }
219 Ok(b.root.unwrap_or_default())
220}
221
222#[derive(Default)]
223struct TreeBuilder {
224 root: Option<XmlElement>,
225 stack: Vec<XmlElement>,
226}
227
228impl XmlSink for TreeBuilder {
229 fn start(&mut self, name: &str, _: &str, attributes: Attrs, _: bool) -> Visit {
230 self.stack.push(XmlElement {
231 name: name.to_owned(),
232 attributes,
233 children: vec![],
234 text: None,
235 });
236 Visit::All
237 }
238
239 fn text(&mut self, text: &str) {
240 if let Some(el) = self.stack.last_mut() {
241 el.text.get_or_insert_with(String::new).push_str(text);
242 }
243 }
244
245 fn end(&mut self, _: &str) {
246 let Some(el) = self.stack.pop() else { return };
247 match self.stack.last_mut() {
248 Some(parent) => parent.children.push(el),
249 None => self.root = Some(el),
250 }
251 }
252}
253
254pub fn decode_entities(s: &str) -> String {
257 if !s.contains('&') {
258 return s.to_owned();
259 }
260 let mut out = String::with_capacity(s.len());
261 let mut rest = s;
262 while let Some(i) = rest.find('&') {
263 out.push_str(&rest[..i]);
264 rest = &rest[i..];
265 let decoded = rest.find(';').and_then(|end| {
266 let ent = &rest[1..end];
267 let ch = match ent {
268 "amp" => Some('&'),
269 "lt" => Some('<'),
270 "gt" => Some('>'),
271 "quot" => Some('"'),
272 "apos" => Some('\''),
273 _ => {
274 let code = if let Some(hex) = ent.strip_prefix("#x") {
275 (!hex.is_empty() && hex.bytes().all(|b| b.is_ascii_hexdigit()))
276 .then(|| u32::from_str_radix(hex, 16).ok())
277 .flatten()
278 } else if let Some(dec) = ent.strip_prefix('#') {
279 (!dec.is_empty() && dec.bytes().all(|b| b.is_ascii_digit()))
280 .then(|| dec.parse::<u32>().ok())
281 .flatten()
282 } else {
283 None
284 };
285 code.map(|c| char::from_u32(c).unwrap_or('\u{FFFD}'))
286 }
287 };
288 ch.map(|c| (c, end))
289 });
290 match decoded {
291 Some((c, end)) => {
292 out.push(c);
293 rest = &rest[end + 1..];
294 }
295 None => {
296 out.push('&');
297 rest = &rest[1..];
298 }
299 }
300 }
301 out.push_str(rest);
302 out
303}
304
305pub fn escape_attr(value: &str, out: &mut String) {
308 for c in value.chars() {
309 match c {
310 '&' => out.push_str("&"),
311 '<' => out.push_str("<"),
312 '"' => out.push_str("""),
313 '\n' => out.push_str(" "),
314 '\r' => out.push_str(" "),
315 '\t' => out.push_str("	"),
316 c => out.push(c),
317 }
318 }
319}
320
321pub fn escape_text(value: &str, out: &mut String) {
323 for c in value.chars() {
324 match c {
325 '&' => out.push_str("&"),
326 '<' => out.push_str("<"),
327 '>' => out.push_str(">"),
328 c => out.push(c),
329 }
330 }
331}
332
333pub fn serialize_xml(element: &XmlElement) -> String {
336 let mut out = String::from("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
337 write_element(element, 0, &mut out);
338 out.push('\n');
339 out
340}
341
342pub fn write_element(el: &XmlElement, depth: usize, out: &mut String) {
344 for _ in 0..depth {
345 out.push_str(" ");
346 }
347 out.push('<');
348 out.push_str(&el.name);
349 for (k, v) in &el.attributes {
350 out.push(' ');
351 out.push_str(k);
352 out.push_str("=\"");
353 escape_attr(v, out);
354 out.push('"');
355 }
356 if el.children.is_empty() && el.text.is_none() {
357 out.push_str("/>\n");
358 return;
359 }
360 out.push('>');
361 if let Some(t) = &el.text {
362 escape_text(t, out);
363 }
364 if !el.children.is_empty() {
365 out.push('\n');
366 for c in &el.children {
367 write_element(c, depth + 1, out);
368 }
369 for _ in 0..depth {
370 out.push_str(" ");
371 }
372 }
373 out.push_str("</");
374 out.push_str(&el.name);
375 out.push_str(">\n");
376}
377
378#[cfg(test)]
379mod tests {
380 use super::*;
381
382 #[test]
383 fn decodes_known_entities_only() {
384 assert_eq!(decode_entities("a & b <AB"), "a & b <AB");
385 assert_eq!(decode_entities(" & &;"), " & &;");
386 }
387
388 #[test]
389 fn keeps_prefixes_order_and_text() {
390 let x = r#"<?xml version="1.0"?><!-- c --><a:root z="1" b:y=""" x='2'>
391 t<![CDATA[<raw>]]><b:child/><c>x & y</c></a:root><after/>"#;
392 let el = parse_xml(x).unwrap();
393 assert_eq!(el.name, "a:root");
394 let keys: Vec<_> = el.attributes.keys().cloned().collect();
395 assert_eq!(keys, ["z", "b:y", "x"]);
396 assert_eq!(el.attr("b:y"), Some("\""));
397 assert_eq!(el.text.as_deref(), Some("\n t<raw>"));
398 assert_eq!(el.children[1].text.as_deref(), Some("x & y"));
399 }
400
401 #[test]
402 fn writes_like_ts() {
403 let el = parse_xml(r#"<r a="x "><c>1 < 2</c><e/></r>"#).unwrap();
404 assert_eq!(
405 serialize_xml(&el),
406 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<r a=\"x \">\n <c>1 < 2</c>\n <e/>\n</r>\n\n"
407 );
408 }
409
410 #[test]
411 fn errors() {
412 assert!(parse_xml("<a><b></a>").is_err());
413 assert!(parse_xml("<a>").is_err());
414 assert!(parse_xml(" ").is_err());
415 }
416
417 struct SkipB(Vec<String>);
418 impl XmlSink for SkipB {
419 fn start(&mut self, n: &str, l: &str, _: Attrs, _: bool) -> Visit {
420 self.0.push(format!("<{n}"));
421 if l == "b" {
422 Visit::Skip
423 } else {
424 Visit::ElementsOnly
425 }
426 }
427 fn text(&mut self, t: &str) {
428 self.0.push(t.to_owned());
429 }
430 fn end(&mut self, n: &str) {
431 self.0.push(format!("</{n}"));
432 }
433 }
434
435 #[test]
436 fn skip_and_elements_only() {
437 let mut s = SkipB(vec![]);
438 assert!(scan_xml("<r>t<x:b><c/>u</x:b><d/></r>", &mut s).unwrap());
439 assert_eq!(s.0, ["<r", "<x:b", "<d", "</d", "</r"]);
440 }
441
442 #[test]
443 fn entity_references_join_the_surrounding_text() {
444 let el = parse_xml("<a>x & A<!--c-->y<</a>").unwrap();
445 assert_eq!(el.text.as_deref(), Some("x & Ay<"));
446 }
447
448 #[test]
451 fn many_attributes_and_namespace_declarations_stay_linear() {
452 let n = 50_000;
453 let mut xml = String::from("<r");
454 for i in 0..n {
455 xml.push_str(&format!(" xmlns:p{i}=\"urn:{i}\" a{i}=\"{i}\""));
456 }
457 xml.push_str(" a0=\"last\"/>");
458 let started = std::time::Instant::now();
459 let el = parse_xml(&xml).unwrap();
460 assert!(started.elapsed() < std::time::Duration::from_secs(5));
461 assert_eq!(el.attributes.len(), 2 * n);
462 assert_eq!(el.attr("a0"), Some("last"));
463 assert_eq!(
464 el.attributes.get_index(1).map(|(k, _)| k.as_str()),
465 Some("a0")
466 );
467 }
468}