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 loop {
131 let event = reader.read_event().map_err(err)?;
132 match event {
133 Event::Start(e) => {
134 let name = utf8(e.name().into_inner())?;
135 let visit = sink.start(name, local_name(name), attributes(&e)?, false);
136 seen_root = true;
137 if visit == Visit::Skip {
138 reader.read_to_end(e.name()).map_err(err)?;
139 if want_text.is_empty() {
140 return Ok(true);
141 }
142 } else {
143 want_text.push(visit == Visit::All);
144 }
145 }
146 Event::Empty(e) => {
147 let name = utf8(e.name().into_inner())?;
148 if sink.start(name, local_name(name), attributes(&e)?, true) != Visit::Skip {
149 sink.end(name);
150 }
151 if want_text.is_empty() {
152 return Ok(true);
153 }
154 }
155 Event::End(e) => {
156 if want_text.pop().is_none() {
157 return Err(XmlError(format!(
158 "Unexpected closing tag </{}>",
159 String::from_utf8_lossy(e.name().into_inner())
160 )));
161 }
162 sink.end(utf8(e.name().into_inner())?);
163 if want_text.is_empty() {
164 return Ok(true);
165 }
166 }
167 Event::Text(t) => {
168 if want_text.last() == Some(&true) {
169 sink.text(&decode_entities(utf8(&t)?));
170 }
171 }
172 Event::CData(t) => {
173 if want_text.last() == Some(&true) {
174 sink.text(utf8(&t)?);
175 }
176 }
177 Event::Eof => {
178 if !want_text.is_empty() {
179 return Err(XmlError(format!(
180 "Expected \"</\" at position {}",
181 reader.buffer_position()
182 )));
183 }
184 return Ok(seen_root);
185 }
186 _ => {}
188 }
189 }
190}
191
192pub fn parse_xml(xml: &str) -> Result<XmlElement, XmlError> {
194 let mut b = TreeBuilder::default();
195 if !scan_xml(xml, &mut b)? || b.root.is_none() {
196 return Err(XmlError(
197 "Failed to parse XML: no root element found".into(),
198 ));
199 }
200 Ok(b.root.unwrap_or_default())
201}
202
203#[derive(Default)]
204struct TreeBuilder {
205 root: Option<XmlElement>,
206 stack: Vec<XmlElement>,
207}
208
209impl XmlSink for TreeBuilder {
210 fn start(&mut self, name: &str, _: &str, attributes: Attrs, _: bool) -> Visit {
211 self.stack.push(XmlElement {
212 name: name.to_owned(),
213 attributes,
214 children: vec![],
215 text: None,
216 });
217 Visit::All
218 }
219
220 fn text(&mut self, text: &str) {
221 if let Some(el) = self.stack.last_mut() {
222 el.text.get_or_insert_with(String::new).push_str(text);
223 }
224 }
225
226 fn end(&mut self, _: &str) {
227 let Some(el) = self.stack.pop() else { return };
228 match self.stack.last_mut() {
229 Some(parent) => parent.children.push(el),
230 None => self.root = Some(el),
231 }
232 }
233}
234
235pub fn decode_entities(s: &str) -> String {
238 if !s.contains('&') {
239 return s.to_owned();
240 }
241 let mut out = String::with_capacity(s.len());
242 let mut rest = s;
243 while let Some(i) = rest.find('&') {
244 out.push_str(&rest[..i]);
245 rest = &rest[i..];
246 let decoded = rest.find(';').and_then(|end| {
247 let ent = &rest[1..end];
248 let ch = match ent {
249 "amp" => Some('&'),
250 "lt" => Some('<'),
251 "gt" => Some('>'),
252 "quot" => Some('"'),
253 "apos" => Some('\''),
254 _ => {
255 let code = if let Some(hex) = ent.strip_prefix("#x") {
256 (!hex.is_empty() && hex.bytes().all(|b| b.is_ascii_hexdigit()))
257 .then(|| u32::from_str_radix(hex, 16).ok())
258 .flatten()
259 } else if let Some(dec) = ent.strip_prefix('#') {
260 (!dec.is_empty() && dec.bytes().all(|b| b.is_ascii_digit()))
261 .then(|| dec.parse::<u32>().ok())
262 .flatten()
263 } else {
264 None
265 };
266 code.map(|c| char::from_u32(c).unwrap_or('\u{FFFD}'))
267 }
268 };
269 ch.map(|c| (c, end))
270 });
271 match decoded {
272 Some((c, end)) => {
273 out.push(c);
274 rest = &rest[end + 1..];
275 }
276 None => {
277 out.push('&');
278 rest = &rest[1..];
279 }
280 }
281 }
282 out.push_str(rest);
283 out
284}
285
286pub fn escape_attr(value: &str, out: &mut String) {
289 for c in value.chars() {
290 match c {
291 '&' => out.push_str("&"),
292 '<' => out.push_str("<"),
293 '"' => out.push_str("""),
294 '\n' => out.push_str(" "),
295 '\r' => out.push_str(" "),
296 '\t' => out.push_str("	"),
297 c => out.push(c),
298 }
299 }
300}
301
302pub fn escape_text(value: &str, out: &mut String) {
304 for c in value.chars() {
305 match c {
306 '&' => out.push_str("&"),
307 '<' => out.push_str("<"),
308 '>' => out.push_str(">"),
309 c => out.push(c),
310 }
311 }
312}
313
314pub fn serialize_xml(element: &XmlElement) -> String {
317 let mut out = String::from("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
318 write_element(element, 0, &mut out);
319 out.push('\n');
320 out
321}
322
323pub fn write_element(el: &XmlElement, depth: usize, out: &mut String) {
325 for _ in 0..depth {
326 out.push_str(" ");
327 }
328 out.push('<');
329 out.push_str(&el.name);
330 for (k, v) in &el.attributes {
331 out.push(' ');
332 out.push_str(k);
333 out.push_str("=\"");
334 escape_attr(v, out);
335 out.push('"');
336 }
337 if el.children.is_empty() && el.text.is_none() {
338 out.push_str("/>\n");
339 return;
340 }
341 out.push('>');
342 if let Some(t) = &el.text {
343 escape_text(t, out);
344 }
345 if !el.children.is_empty() {
346 out.push('\n');
347 for c in &el.children {
348 write_element(c, depth + 1, out);
349 }
350 for _ in 0..depth {
351 out.push_str(" ");
352 }
353 }
354 out.push_str("</");
355 out.push_str(&el.name);
356 out.push_str(">\n");
357}
358
359#[cfg(test)]
360mod tests {
361 use super::*;
362
363 #[test]
364 fn decodes_known_entities_only() {
365 assert_eq!(decode_entities("a & b <AB"), "a & b <AB");
366 assert_eq!(decode_entities(" & &;"), " & &;");
367 }
368
369 #[test]
370 fn keeps_prefixes_order_and_text() {
371 let x = r#"<?xml version="1.0"?><!-- c --><a:root z="1" b:y=""" x='2'>
372 t<![CDATA[<raw>]]><b:child/><c>x & y</c></a:root><after/>"#;
373 let el = parse_xml(x).unwrap();
374 assert_eq!(el.name, "a:root");
375 let keys: Vec<_> = el.attributes.keys().cloned().collect();
376 assert_eq!(keys, ["z", "b:y", "x"]);
377 assert_eq!(el.attr("b:y"), Some("\""));
378 assert_eq!(el.text.as_deref(), Some("\n t<raw>"));
379 assert_eq!(el.children[1].text.as_deref(), Some("x & y"));
380 }
381
382 #[test]
383 fn writes_like_ts() {
384 let el = parse_xml(r#"<r a="x "><c>1 < 2</c><e/></r>"#).unwrap();
385 assert_eq!(
386 serialize_xml(&el),
387 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<r a=\"x \">\n <c>1 < 2</c>\n <e/>\n</r>\n\n"
388 );
389 }
390
391 #[test]
392 fn errors() {
393 assert!(parse_xml("<a><b></a>").is_err());
394 assert!(parse_xml("<a>").is_err());
395 assert!(parse_xml(" ").is_err());
396 }
397
398 struct SkipB(Vec<String>);
399 impl XmlSink for SkipB {
400 fn start(&mut self, n: &str, l: &str, _: Attrs, _: bool) -> Visit {
401 self.0.push(format!("<{n}"));
402 if l == "b" {
403 Visit::Skip
404 } else {
405 Visit::ElementsOnly
406 }
407 }
408 fn text(&mut self, t: &str) {
409 self.0.push(t.to_owned());
410 }
411 fn end(&mut self, n: &str) {
412 self.0.push(format!("</{n}"));
413 }
414 }
415
416 #[test]
417 fn skip_and_elements_only() {
418 let mut s = SkipB(vec![]);
419 assert!(scan_xml("<r>t<x:b><c/>u</x:b><d/></r>", &mut s).unwrap());
420 assert_eq!(s.0, ["<r", "<x:b", "<d", "</d", "</r"]);
421 }
422}