Skip to main content

faucet_core/file_format/
xml.rs

1//! XML read/write for the file connectors (#604).
2//!
3//! The decoder is the compact element→object mapping the REST source has used
4//! since #515: each element becomes an object of its children, repeated child
5//! tags become arrays, attributes are `@name`, and text is `#text` (or the
6//! value directly when an element has only text). Namespaces are stripped to
7//! their local name, except on attributes whose local names would collide
8//! (`a:id` and `b:id` stay `@a:id` / `@b:id`).
9//!
10//! XML has **no canonical record boundary**, so one is declared
11//! ([`XmlOptions::record_element`](super::XmlOptions)). Selecting the wrong
12//! element yields no records rather than the wrong ones, which is why the
13//! decoder reports the elements it did see.
14
15use crate::error::FaucetError;
16use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event};
17use serde_json::{Map, Value};
18
19/// Parse XML bytes into records.
20///
21/// `record_element` names the repeated element. When no element of that name
22/// exists, the document root's direct children are used instead — right for the
23/// common `<rows><row/>…</rows>` shape without forcing every config to spell it
24/// out, and reported in the error when neither yields anything.
25pub 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    // Fall back to the root's children.
33    match root_children(&doc) {
34        Some(children) => Ok(children),
35        // An empty document element is zero records, not an error: a sink that
36        // wrote an empty page produces exactly this, and failing the read
37        // would turn "no data" into a pipeline failure.
38        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
46/// Every value stored under `key`, at any depth, flattened out of arrays, in
47/// document order.
48fn 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
75/// Whether the document is a single element with no children and no text.
76fn 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
87/// The document root's children, when the root wraps exactly one repeated
88/// element. Returns `None` for a root with several differently-named children,
89/// where "the records" would be a guess.
90fn 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
104/// Any writer failure, whatever `quick_xml` calls it in this version.
105fn err<E: std::fmt::Display>(e: E) -> FaucetError {
106    FaucetError::Sink(format!("xml: {e}"))
107}
108
109/// Write records as XML: `<root><record>…</record>…</root>`.
110///
111/// Scalars become text elements; a nested object becomes a nested element; an
112/// array becomes a repeated element, which is the shape [`decode`] reads back.
113pub 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
166/// A field name that is a legal XML element name.
167///
168/// JSON keys can hold spaces, slashes and leading digits; an element name
169/// cannot. Substituting `_` keeps the document well-formed and the mapping
170/// obvious, which beats failing the write on a field the user cannot rename.
171fn 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
181/// One open element: its object and its accumulated text.
182type 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
231/// Resolve `&name;` (the five XML entities) or a character reference.
232fn 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
272/// The deepest element nesting [`to_json`] accepts. Deeper documents are
273/// refused with a typed error: the resulting value is nested once per level,
274/// and walking or dropping an unbounded one would overflow the stack.
275pub const MAX_XML_DEPTH: usize = 256;
276
277/// Compact XML → JSON.
278pub 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        // RUSTSEC-2026-0194 is fixed in quick-xml 0.41+, so the duplicate-name
382        // check is back on: a repeated attribute is malformed XML, not "last wins".
383        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 &amp; B">x &lt; &#65;<![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>&nbsp;</r>").expect_err("undefined");
397        assert!(err.to_string().contains("undefined entity"), "{err}");
398        let err = to_json(b"<r>&#0;</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="&nbsp;"/>"#).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        // `<rows><row/>…</rows>` read with the default `record` name still
480        // works, rather than silently returning nothing.
481        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        // `Event::Empty` and `Event::CData` are separate arms from Start/Text.
488        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        // The second occurrence promotes the value to an array; the third
496        // takes the push branch.
497        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        // `root_children` has a distinct arm for a lone non-array child.
504        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        // Untrusted input must never panic a live pipeline. `quick_xml`'s own
552        // `check_end_names` usually rejects the mismatch first, so the message
553        // is normally its — the guard here is that the frame-stack accesses
554        // report rather than `.expect()`, whichever layer catches it.
555        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}