1use crate::error::FaucetError;
16use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event};
17use serde_json::{Map, Value};
18
19pub fn decode(bytes: &[u8], record_element: &str) -> Result<Vec<Value>, FaucetError> {
26 let doc = to_json(bytes)?;
27 let mut out = Vec::new();
28 collect(&doc, record_element, &mut out);
29 if !out.is_empty() {
30 return Ok(out);
31 }
32 match root_children(&doc) {
34 Some(children) => Ok(children),
35 None if is_empty_document(&doc) => Ok(Vec::new()),
39 None => Err(FaucetError::Source(format!(
40 "xml: no <{record_element}> elements, and the document root has no repeated child to \
41 use instead — set `xml.record_element` to the element that delimits one record"
42 ))),
43 }
44}
45
46fn collect(v: &Value, key: &str, out: &mut Vec<Value>) {
49 enum Work<'a> {
50 Visit(&'a Value),
51 Emit(&'a Value),
52 }
53 let mut pending = vec![Work::Visit(v)];
54 while let Some(work) = pending.pop() {
55 match work {
56 Work::Emit(Value::Array(items)) => out.extend(items.iter().cloned()),
57 Work::Emit(other) => out.push(other.clone()),
58 Work::Visit(Value::Object(map)) => {
59 for (k, child) in map.iter().rev() {
60 pending.push(if k == key {
61 Work::Emit(child)
62 } else {
63 Work::Visit(child)
64 });
65 }
66 }
67 Work::Visit(Value::Array(items)) => {
68 pending.extend(items.iter().rev().map(Work::Visit));
69 }
70 Work::Visit(_) => {}
71 }
72 }
73}
74
75fn is_empty_document(doc: &Value) -> bool {
77 let Some(root) = doc.as_object().and_then(|m| m.values().next()) else {
78 return true;
79 };
80 match root {
81 Value::String(s) => s.trim().is_empty(),
82 Value::Object(map) => map.is_empty(),
83 _ => false,
84 }
85}
86
87fn root_children(doc: &Value) -> Option<Vec<Value>> {
91 let root = doc.as_object()?.values().next()?;
92 let map = root.as_object()?;
93 let real: Vec<(&String, &Value)> = map
94 .iter()
95 .filter(|(k, _)| !k.starts_with('@') && *k != "#text")
96 .collect();
97 match real.as_slice() {
98 [(_, Value::Array(items))] => Some(items.to_vec()),
99 [(_, single)] => Some(vec![(*single).clone()]),
100 _ => None,
101 }
102}
103
104fn err<E: std::fmt::Display>(e: E) -> FaucetError {
106 FaucetError::Sink(format!("xml: {e}"))
107}
108
109pub fn encode(
114 records: &[Value],
115 root_element: &str,
116 record_element: &str,
117) -> Result<Vec<u8>, FaucetError> {
118 let mut w = quick_xml::Writer::new(Vec::new());
119 w.write_event(Event::Start(BytesStart::new(root_element)))
120 .map_err(err)?;
121 for r in records {
122 w.write_event(Event::Start(BytesStart::new(record_element)))
123 .map_err(err)?;
124 write_value(&mut w, r)?;
125 w.write_event(Event::End(BytesEnd::new(record_element)))
126 .map_err(err)?;
127 }
128 w.write_event(Event::End(BytesEnd::new(root_element)))
129 .map_err(err)?;
130 Ok(w.into_inner())
131}
132
133fn write_value(w: &mut quick_xml::Writer<Vec<u8>>, v: &Value) -> Result<(), FaucetError> {
134 match v {
135 Value::Object(map) => {
136 for (k, child) in map {
137 let name = sanitize(k);
138 match child {
139 Value::Array(items) => {
140 for item in items {
141 w.write_event(Event::Start(BytesStart::new(&name)))
142 .map_err(err)?;
143 write_value(w, item)?;
144 w.write_event(Event::End(BytesEnd::new(&name)))
145 .map_err(err)?;
146 }
147 }
148 other => {
149 w.write_event(Event::Start(BytesStart::new(&name)))
150 .map_err(err)?;
151 write_value(w, other)?;
152 w.write_event(Event::End(BytesEnd::new(&name)))
153 .map_err(err)?;
154 }
155 }
156 }
157 Ok(())
158 }
159 Value::Null => Ok(()),
160 other => w
161 .write_event(Event::Text(BytesText::new(&super::cell_text(other))))
162 .map_err(err),
163 }
164}
165
166fn sanitize(key: &str) -> String {
172 let mut out = String::with_capacity(key.len());
173 for (i, c) in key.chars().enumerate() {
174 let ok = c.is_alphanumeric() || c == '_' || c == '-' || c == '.';
175 let ok = ok && !(i == 0 && (c.is_numeric() || c == '-' || c == '.'));
176 out.push(if ok { c } else { '_' });
177 }
178 if out.is_empty() { "field".into() } else { out }
179}
180
181type Frame = (Map<String, Value>, String);
183
184fn unbalanced(detail: &str) -> FaucetError {
185 FaucetError::Source(format!(
186 "xml: malformed XML: {detail}; the document has more closing than opening tags"
187 ))
188}
189
190fn top<'a>(stack: &'a mut [Frame], detail: &str) -> Result<&'a mut Frame, FaucetError> {
191 stack.last_mut().ok_or_else(|| unbalanced(detail))
192}
193
194fn pop(stack: &mut Vec<Frame>, detail: &str) -> Result<Frame, FaucetError> {
195 stack.pop().ok_or_else(|| unbalanced(detail))
196}
197
198fn attrs(e: &BytesStart) -> Result<Map<String, Value>, FaucetError> {
199 let mut parsed: Vec<(String, String)> = Vec::new();
200 for a in e.attributes() {
201 let a = a.map_err(|e| FaucetError::Source(format!("xml: malformed attribute: {e}")))?;
202 let v = a
203 .normalized_value(quick_xml::XmlVersion::Implicit1_0)
204 .map_err(|e| {
205 FaucetError::Source(format!("xml: attribute `{}`: {e}", a.key.as_ref()))
206 })?;
207 let qname = a.key.as_ref().to_string();
208 parsed.push((qname, v.into_owned()));
209 }
210 let mut seen: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
211 for (q, _) in &parsed {
212 *seen.entry(local(q)).or_default() += 1;
213 }
214 let mut m = Map::new();
215 for (q, v) in parsed {
216 let short = local(&q);
217 let key = if seen.get(&short).copied().unwrap_or(0) > 1 {
218 format!("@{q}")
219 } else {
220 format!("@{short}")
221 };
222 m.insert(key, Value::String(v));
223 }
224 Ok(m)
225}
226
227fn local(name: &str) -> String {
228 name.rsplit(':').next().unwrap_or_default().to_string()
229}
230
231fn reference(r: &quick_xml::events::BytesRef<'_>) -> Result<String, FaucetError> {
233 if let Some(ch) = r.resolve_char_ref().map_err(|e| {
234 FaucetError::Source(format!(
235 "xml: invalid character reference `&{};`: {e}",
236 &**r
237 ))
238 })? {
239 return Ok(ch.to_string());
240 }
241 quick_xml::escape::resolve_predefined_entity(r)
242 .map(str::to_string)
243 .ok_or_else(|| FaucetError::Source(format!("xml: undefined entity reference `&{};`", &**r)))
244}
245
246fn insert_child(parent: &mut Map<String, Value>, key: String, val: Value) {
247 match parent.get_mut(&key) {
248 Some(Value::Array(arr)) => arr.push(val),
249 Some(existing) => {
250 let prev = existing.take();
251 parent.insert(key, Value::Array(vec![prev, val]));
252 }
253 None => {
254 parent.insert(key, val);
255 }
256 }
257}
258
259fn finish(obj: Map<String, Value>, text: String) -> Value {
260 let trimmed = text.trim();
261 if obj.is_empty() {
262 Value::String(trimmed.to_string())
263 } else {
264 let mut obj = obj;
265 if !trimmed.is_empty() {
266 obj.insert("#text".to_string(), Value::String(trimmed.to_string()));
267 }
268 Value::Object(obj)
269 }
270}
271
272pub const MAX_XML_DEPTH: usize = 256;
276
277pub fn to_json(bytes: &[u8]) -> Result<Value, FaucetError> {
279 let text = std::str::from_utf8(bytes)
280 .map_err(|e| FaucetError::Source(format!("xml: not UTF-8: {e}")))?;
281 let mut reader = quick_xml::Reader::from_str(text);
282 let mut stack: Vec<Frame> = vec![(Map::new(), String::new())];
283 let mut names: Vec<String> = Vec::new();
284
285 loop {
286 match reader
287 .read_event()
288 .map_err(|e| FaucetError::Source(format!("xml: {e}")))?
289 {
290 Event::Eof => break,
291 Event::Start(e) => {
292 if names.len() >= MAX_XML_DEPTH {
293 return Err(FaucetError::Source(format!(
294 "xml: elements are nested more than {MAX_XML_DEPTH} levels deep"
295 )));
296 }
297 names.push(local(e.name().as_ref()));
298 stack.push((attrs(&e)?, String::new()));
299 }
300 Event::Empty(e) => {
301 let name = local(e.name().as_ref());
302 let val = finish(attrs(&e)?, String::new());
303 let t = top(&mut stack, "element after the document root closed")?;
304 insert_child(&mut t.0, name, val);
305 }
306 Event::Text(t) => {
307 top(&mut stack, "text after the document root closed")?
308 .1
309 .push_str(&t.xml10_content());
310 }
311 Event::CData(t) => {
312 top(&mut stack, "CDATA after the document root closed")?
313 .1
314 .push_str(&t.xml10_content());
315 }
316 Event::GeneralRef(r) => {
317 let s = reference(&r)?;
318 top(&mut stack, "text after the document root closed")?
319 .1
320 .push_str(&s);
321 }
322 Event::End(_) => {
323 let (obj, text) = pop(&mut stack, "unmatched closing tag")?;
324 let name = names
325 .pop()
326 .ok_or_else(|| unbalanced("unmatched closing tag"))?;
327 let val = finish(obj, text);
328 let parent = top(&mut stack, "unmatched closing tag")?;
329 insert_child(&mut parent.0, name, val);
330 }
331 _ => {}
332 }
333 }
334 if let Some(open) = names.last() {
335 return Err(FaucetError::Source(format!(
336 "xml: document ended inside <{open}> — the input is truncated or not closed"
337 )));
338 }
339 let (root, _) = stack
340 .pop()
341 .ok_or_else(|| unbalanced("document root closed twice"))?;
342 Ok(Value::Object(root))
343}
344
345#[cfg(test)]
346mod tests {
347 use super::*;
348 use serde_json::json;
349
350 #[test]
351 fn a_document_cut_between_records_is_an_error() {
352 let xml = br#"<records><record><a>1</a></record><record><a>2"#;
353 let err = decode(xml, "record").unwrap_err().to_string();
354 assert!(err.contains("ended inside <a>"), "{err}");
355 let err = decode(b"<records><record><a>1</a></record>", "record")
356 .unwrap_err()
357 .to_string();
358 assert!(err.contains("ended inside <records>"), "{err}");
359 }
360
361 #[test]
362 fn namespaced_attributes_with_one_local_name_keep_their_prefix() {
363 let xml = br#"<rows xmlns:a="u:a" xmlns:b="u:b"><row a:id="1" b:id="2" n="x"/></rows>"#;
364 assert_eq!(
365 decode(xml, "row").expect("decode"),
366 vec![json!({"@a:id": "1", "@b:id": "2", "@n": "x"})]
367 );
368 }
369
370 #[test]
371 fn a_named_record_element_selects_the_records() {
372 let xml = br#"<rows><row><id>1</id><n>a</n></row><row><id>2</id><n>b</n></row></rows>"#;
373 assert_eq!(
374 decode(xml, "row").expect("decode"),
375 vec![json!({"id": "1", "n": "a"}), json!({"id": "2", "n": "b"})]
376 );
377 }
378
379 #[test]
380 fn many_attributes_parse_and_a_duplicate_name_is_refused() {
381 let many: String = (0..50_000).map(|i| format!(" a{i}=\"{i}\"")).collect();
384 let xml = format!("<rows><row{many} id=\"1\"/></rows>");
385 let rows = decode(xml.as_bytes(), "row").expect("decode");
386 assert_eq!(rows[0]["@a49999"], json!("49999"));
387 let err = decode(br#"<rows><row id="1" id="2"/></rows>"#, "row").expect_err("dup");
388 assert!(err.to_string().contains("malformed attribute"), "{err}");
389 }
390
391 #[test]
392 fn references_resolve_and_undefined_entities_fail() {
393 let v = to_json(br#"<r><v a="A & B">x < A<![CDATA[&z]]></v></r>"#).unwrap();
394 assert_eq!(v["r"]["v"]["@a"], json!("A & B"));
395 assert_eq!(v["r"]["v"]["#text"], json!("x < A&z"));
396 let err = to_json(b"<r> </r>").expect_err("undefined");
397 assert!(err.to_string().contains("undefined entity"), "{err}");
398 let err = to_json(b"<r>�</r>").expect_err("bad char ref");
399 assert!(
400 err.to_string().contains("invalid character reference"),
401 "{err}"
402 );
403 let err = to_json(br#"<r a=" "/>"#).expect_err("attr");
404 assert!(err.to_string().contains("attribute `a`"), "{err}");
405 }
406
407 fn nested(depth: usize) -> Vec<u8> {
408 format!("{}x{}", "<a>".repeat(depth), "</a>".repeat(depth)).into_bytes()
409 }
410
411 #[test]
412 fn a_hostile_nesting_depth_is_refused_rather_than_overflowing_the_stack() {
413 let err = decode(&nested(250_000), "row").expect_err("too deep");
414 assert!(err.to_string().contains("nested more than"), "{err}");
415 }
416
417 #[test]
418 fn nesting_up_to_the_limit_still_decodes() {
419 let v = to_json(&nested(MAX_XML_DEPTH)).expect("at the limit");
420 let mut cur = &v;
421 for _ in 0..MAX_XML_DEPTH {
422 cur = &cur["a"];
423 }
424 assert_eq!(cur, &json!("x"));
425 assert!(to_json(&nested(MAX_XML_DEPTH + 1)).is_err());
426 }
427
428 fn collect_recursive(v: &Value, key: &str, out: &mut Vec<Value>) {
429 match v {
430 Value::Object(map) => {
431 for (k, child) in map {
432 if k == key {
433 match child {
434 Value::Array(items) => out.extend(items.iter().cloned()),
435 other => out.push(other.clone()),
436 }
437 } else {
438 collect_recursive(child, key, out);
439 }
440 }
441 }
442 Value::Array(items) => items.iter().for_each(|i| collect_recursive(i, key, out)),
443 _ => {}
444 }
445 }
446
447 #[test]
448 fn the_iterative_walk_matches_a_recursive_one() {
449 let xml = br#"<r><row><id>1</id></row><g><row><id>2</id><row><id>2.1</id></row></row><h><row><id>3</id></row></h></g><row><id>4</id></row><z><row><id>5</id></row></z></r>"#;
450 let doc = to_json(xml).expect("json");
451 let mut iterative = Vec::new();
452 collect(&doc, "row", &mut iterative);
453 let mut recursive = Vec::new();
454 collect_recursive(&doc, "row", &mut recursive);
455 assert_eq!(iterative.len(), 5);
456 assert_eq!(iterative, recursive);
457 }
458
459 #[test]
460 fn a_single_record_is_still_a_list_of_one() {
461 let xml = br#"<rows><row><id>1</id></row></rows>"#;
462 assert_eq!(
463 decode(xml, "row").expect("decode"),
464 vec![json!({"id": "1"})]
465 );
466 }
467
468 #[test]
469 fn the_record_element_is_found_at_any_depth() {
470 let xml = br#"<env><body><rows><row><id>1</id></row></rows></body></env>"#;
471 assert_eq!(
472 decode(xml, "row").expect("decode"),
473 vec![json!({"id": "1"})]
474 );
475 }
476
477 #[test]
478 fn without_a_match_the_roots_children_are_used() {
479 let xml = br#"<rows><row><id>1</id></row><row><id>2</id></row></rows>"#;
482 assert_eq!(decode(xml, "record").expect("decode").len(), 2);
483 }
484
485 #[test]
486 fn a_self_closing_element_and_cdata_decode() {
487 let v = to_json(br#"<r><e/><c><![CDATA[raw <>&]]></c><!-- ignored --></r>"#).expect("json");
489 assert_eq!(v["r"]["e"], json!(""));
490 assert_eq!(v["r"]["c"], json!("raw <>&"));
491 }
492
493 #[test]
494 fn three_repeated_elements_accumulate_into_one_array() {
495 let recs = decode(br#"<rs><r>a</r><r>b</r><r>c</r></rs>"#, "r").expect("decode");
498 assert_eq!(recs, vec![json!("a"), json!("b"), json!("c")]);
499 }
500
501 #[test]
502 fn a_root_wrapping_a_single_child_still_yields_one_record() {
503 let recs = decode(br#"<rows><row><id>1</id></row></rows>"#, "nope").expect("decode");
505 assert_eq!(recs, vec![json!({"id": "1"})]);
506 }
507
508 #[test]
509 fn emptiness_is_judged_on_the_root_shape() {
510 assert!(is_empty_document(&json!({})));
511 assert!(is_empty_document(&json!({"r": ""})));
512 assert!(is_empty_document(&json!({"r": " "})));
513 assert!(is_empty_document(&json!({"r": {}})));
514 assert!(!is_empty_document(&json!({"r": "text"})));
515 assert!(!is_empty_document(&json!({"r": {"a": 1}})));
516 assert!(!is_empty_document(&json!({"r": [1]})));
517 }
518
519 #[test]
520 fn the_writer_error_helper_is_prefixed() {
521 assert_eq!(err("boom").to_string(), "Sink error: xml: boom");
522 }
523
524 #[test]
525 fn an_empty_document_reads_back_as_zero_records_not_an_error() {
526 let bytes = encode(&[], "records", "record").expect("encode");
527 assert_eq!(
528 String::from_utf8(bytes.clone()).unwrap(),
529 "<records></records>"
530 );
531 assert!(decode(&bytes, "record").expect("decode").is_empty());
532 }
533
534 #[test]
535 fn an_ambiguous_root_is_an_error_naming_the_option() {
536 let xml = br#"<doc><a>1</a><b>2</b></doc>"#;
537 let err = decode(xml, "record").expect_err("ambiguous");
538 assert!(err.to_string().contains("xml.record_element"), "{err}");
539 }
540
541 #[test]
542 fn attributes_text_and_namespaces_map_compactly() {
543 let xml = br#"<r><x ns:k="v">t</x></r>"#;
544 let v = to_json(xml).expect("json");
545 assert_eq!(v["r"]["x"]["@k"], json!("v"));
546 assert_eq!(v["r"]["x"]["#text"], json!("t"));
547 }
548
549 #[test]
550 fn unbalanced_input_is_a_typed_error_not_a_panic() {
551 let err = to_json(b"<a></a></a>").expect_err("unbalanced");
556 assert!(matches!(err, FaucetError::Source(_)), "{err}");
557 assert!(unbalanced("x").to_string().contains("malformed XML"));
558 }
559
560 #[test]
561 fn encode_round_trips_through_decode() {
562 let recs = vec![json!({"id": "1", "n": "a"}), json!({"id": "2", "n": "b"})];
563 let bytes = encode(&recs, "records", "record").expect("encode");
564 assert_eq!(
565 String::from_utf8(bytes.clone()).unwrap(),
566 "<records><record><id>1</id><n>a</n></record><record><id>2</id><n>b</n></record></records>"
567 );
568 assert_eq!(decode(&bytes, "record").expect("decode"), recs);
569 }
570
571 #[test]
572 fn an_array_field_becomes_a_repeated_element() {
573 let bytes = encode(&[json!({"t": ["a", "b"]})], "rs", "r").expect("encode");
574 assert_eq!(
575 String::from_utf8(bytes).unwrap(),
576 "<rs><r><t>a</t><t>b</t></r></rs>"
577 );
578 }
579
580 #[test]
581 fn a_key_that_is_not_a_legal_element_name_is_substituted_not_rejected() {
582 let bytes = encode(&[json!({"a b/c": 1, "2x": 2})], "rs", "r").expect("encode");
583 let s = String::from_utf8(bytes).unwrap();
584 assert!(s.contains("<a_b_c>1</a_b_c>"), "{s}");
585 assert!(s.contains("<_x>2</_x>"), "{s}");
586 }
587
588 #[test]
589 fn a_null_field_writes_an_empty_element() {
590 let bytes = encode(&[json!({"a": null})], "rs", "r").expect("encode");
591 assert_eq!(String::from_utf8(bytes).unwrap(), "<rs><r><a></a></r></rs>");
592 }
593}