Skip to main content

config_disassembler/xml/builders/
merge_xml_elements.rs

1//! Merge multiple XML elements into one.
2//!
3//! The work is done by the owned (`*_owned`) functions, which move every value
4//! from the parsed shards into the merged tree. Reassemble calls those directly,
5//! so it never holds a second full copy of the document while merging or
6//! reordering. The borrowing [`merge_xml_elements`] / [`reorder_root_keys`] keep
7//! the public API and clone their input once before delegating.
8//!
9//! Key order must match the original clone-based implementation byte for byte.
10//! Each branch below makes the same `Map` calls in the same order -- including
11//! `remove` + `insert` in the array branch, which with serde_json's
12//! `preserve_order` is a *swap*-remove that moves keys. The test module keeps the
13//! original implementation as a reference and checks the two agree.
14
15use serde_json::{Map, Value};
16
17use crate::xml::types::XmlElement;
18
19fn merge_element_content(target: &mut Map<String, Value>, source: Map<String, Value>) {
20    for (key, value) in source {
21        match value {
22            Value::Array(items) => merge_array_value(target, key, items),
23            Value::Object(map) => merge_object_value(target, key, map),
24            primitive => merge_primitive_value(target, key, primitive),
25        }
26    }
27}
28
29fn merge_array_value(target: &mut Map<String, Value>, key: String, items: Vec<Value>) {
30    if !target.contains_key(&key) {
31        target.insert(key, Value::Array(items));
32    } else if let Some(Value::Array(arr)) = target.get_mut(&key) {
33        arr.extend(items);
34    } else {
35        let existing = target.remove(&key).unwrap();
36        let mut combined = vec![existing];
37        combined.extend(items);
38        target.insert(key, Value::Array(combined));
39    }
40}
41
42fn merge_object_value(target: &mut Map<String, Value>, key: String, map: Map<String, Value>) {
43    match target.get_mut(&key) {
44        Some(Value::Array(arr)) => arr.push(Value::Object(map)),
45        // Replacing the value of an existing key keeps the key's position, like
46        // inserting over an existing key does.
47        Some(existing) => {
48            let previous = std::mem::take(existing);
49            *existing = Value::Array(vec![previous, Value::Object(map)]);
50        }
51        None => {
52            target.insert(key, Value::Object(map));
53        }
54    }
55}
56
57fn merge_primitive_value(target: &mut Map<String, Value>, key: String, value: Value) {
58    if !target.contains_key(&key) {
59        target.insert(key, value);
60    }
61}
62
63fn default_xml_declaration() -> Value {
64    let mut decl = Map::new();
65    decl.insert("@version".to_string(), Value::String("1.0".to_string()));
66    decl.insert("@encoding".to_string(), Value::String("UTF-8".to_string()));
67    Value::Object(decl)
68}
69
70fn build_final_xml_element(
71    declaration: Option<Value>,
72    root_key: &str,
73    content: Map<String, Value>,
74) -> XmlElement {
75    let mut result = Map::new();
76    let decl = declaration.unwrap_or_else(default_xml_declaration);
77    result.insert("?xml".to_string(), decl);
78    result.insert(root_key.to_string(), Value::Object(content));
79    Value::Object(result)
80}
81
82/// Reorder the root element's child keys to match the given order.
83/// Keys not in `key_order` are appended at the end.
84///
85/// Returns `None` when `element` isn't an object with a non-`?xml` root key whose
86/// value is an object. See [`reorder_root_keys_owned`] to avoid the copy.
87pub fn reorder_root_keys(element: &XmlElement, key_order: &[String]) -> Option<XmlElement> {
88    let obj = element.as_object()?;
89    let root_key = obj.keys().find(|k| *k != "?xml")?;
90    obj.get(root_key)?.as_object()?;
91    Some(reorder_root_keys_owned(element.clone(), key_order))
92}
93
94/// Owned version of [`reorder_root_keys`]: moves the root's children into their
95/// new order instead of cloning them. An `element` without the expected shape is
96/// returned unchanged (the case where [`reorder_root_keys`] returns `None`).
97pub fn reorder_root_keys_owned(element: XmlElement, key_order: &[String]) -> XmlElement {
98    let mut obj = match element {
99        Value::Object(obj) => obj,
100        other => return other,
101    };
102    let Some(root_key) = obj.keys().find(|k| *k != "?xml").cloned() else {
103        return Value::Object(obj);
104    };
105    let mut root_content = match obj.get_mut(&root_key) {
106        Some(Value::Object(content)) => std::mem::take(content),
107        _ => return Value::Object(obj),
108    };
109
110    let mut reordered = Map::new();
111    for key in key_order {
112        // A key listed twice keeps its first position (re-inserting an existing
113        // key never moves it), so only the first occurrence needs taking.
114        if !reordered.contains_key(key) {
115            if let Some(value) = root_content.get_mut(key) {
116                reordered.insert(key.clone(), std::mem::take(value));
117            }
118        }
119    }
120    for (key, value) in root_content {
121        if !reordered.contains_key(&key) {
122            reordered.insert(key, value);
123        }
124    }
125
126    let mut result = Map::new();
127    if let Some(decl) = obj.get_mut("?xml") {
128        result.insert("?xml".to_string(), std::mem::take(decl));
129    }
130    result.insert(root_key, Value::Object(reordered));
131    Value::Object(result)
132}
133
134/// Merge multiple XML elements into one.
135///
136/// Scans elements for the first non-`?xml` root key; tolerates leading entries
137/// that are empty or declaration-only (e.g. a partially-written disassembled file),
138/// which previously caused the whole merge to return `None` and the caller to
139/// emit an empty `<root>` document. See [`merge_xml_elements_owned`] to avoid the copy.
140pub fn merge_xml_elements(elements: &[XmlElement]) -> Option<XmlElement> {
141    merge_xml_elements_owned(elements.to_vec())
142}
143
144/// Owned version of [`merge_xml_elements`]: consumes the parsed elements and
145/// moves their content into the merged tree instead of cloning it.
146pub fn merge_xml_elements_owned(elements: Vec<XmlElement>) -> Option<XmlElement> {
147    if elements.is_empty() {
148        log::error!("No elements to merge.");
149        return None;
150    }
151
152    let root_key = elements
153        .iter()
154        .find_map(|el| el.as_object()?.keys().find(|k| *k != "?xml").cloned())?;
155
156    let mut declaration: Option<Value> = None;
157    let mut merged_content = Map::new();
158    for element in elements {
159        let Value::Object(mut obj) = element else {
160            continue;
161        };
162        // First element carrying a declaration wins.
163        if declaration.is_none() {
164            if let Some(decl) = obj.get_mut("?xml") {
165                declaration = Some(std::mem::take(decl));
166            }
167        }
168        if let Some(Value::Object(content)) = obj.get_mut(&root_key) {
169            merge_element_content(&mut merged_content, std::mem::take(content));
170        }
171    }
172
173    Some(build_final_xml_element(
174        declaration,
175        &root_key,
176        merged_content,
177    ))
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183    use serde_json::json;
184
185    // ---- reference implementation -------------------------------------------
186    // The original clone-based merge / reorder, kept as the oracle for the owned
187    // versions above.
188    mod reference {
189        use serde_json::{Map, Value};
190
191        fn merge_element_content(target: &mut Map<String, Value>, source: &Map<String, Value>) {
192            for (key, value) in source {
193                if value.is_array() {
194                    merge_array_value(target, key, value.as_array().unwrap());
195                } else if value.is_object() {
196                    merge_object_value(target, key, value.as_object().unwrap());
197                } else if !target.contains_key(key) {
198                    target.insert(key.to_string(), value.clone());
199                }
200            }
201        }
202
203        fn merge_array_value(target: &mut Map<String, Value>, key: &str, value: &[Value]) {
204            if !target.contains_key(key) {
205                target.insert(key.to_string(), Value::Array(value.to_vec()));
206            } else if let Some(Value::Array(arr)) = target.get_mut(key) {
207                arr.extend(value.iter().cloned());
208            } else {
209                let existing = target.remove(key).unwrap();
210                target.insert(
211                    key.to_string(),
212                    Value::Array(
213                        [vec![existing], value.to_vec()]
214                            .into_iter()
215                            .flatten()
216                            .collect(),
217                    ),
218                );
219            }
220        }
221
222        fn merge_object_value(
223            target: &mut Map<String, Value>,
224            key: &str,
225            value: &Map<String, Value>,
226        ) {
227            if let Some(Value::Array(arr)) = target.get_mut(key) {
228                arr.push(Value::Object(value.clone()));
229            } else if let Some(existing) = target.get(key) {
230                let existing = existing.clone();
231                target.insert(
232                    key.to_string(),
233                    Value::Array(vec![existing, Value::Object(value.clone())]),
234                );
235            } else {
236                target.insert(key.to_string(), Value::Object(value.clone()));
237            }
238        }
239
240        pub fn merge(elements: &[Value]) -> Option<Value> {
241            if elements.is_empty() {
242                return None;
243            }
244            let root_key = elements
245                .iter()
246                .find_map(|el| el.as_object()?.keys().find(|k| *k != "?xml").cloned())?;
247            let declaration = elements.iter().find_map(|el| el.as_object()?.get("?xml"));
248            let mut merged = Map::new();
249            for element in elements {
250                if let Some(content) = element
251                    .as_object()
252                    .and_then(|o| o.get(&root_key))
253                    .and_then(Value::as_object)
254                {
255                    merge_element_content(&mut merged, content);
256                }
257            }
258            let mut result = Map::new();
259            let decl = declaration
260                .cloned()
261                .unwrap_or_else(|| serde_json::json!({"@version": "1.0", "@encoding": "UTF-8"}));
262            result.insert("?xml".to_string(), decl);
263            result.insert(root_key, Value::Object(merged));
264            Some(Value::Object(result))
265        }
266
267        pub fn reorder(element: &Value, key_order: &[String]) -> Option<Value> {
268            let obj = element.as_object()?;
269            let root_key = obj.keys().find(|k| *k != "?xml")?.clone();
270            let root_content = obj.get(&root_key)?.as_object()?;
271            let mut reordered = Map::new();
272            for key in key_order {
273                if let Some(v) = root_content.get(key) {
274                    reordered.insert(key.clone(), v.clone());
275                }
276            }
277            for (key, value) in root_content {
278                if !reordered.contains_key(key) {
279                    reordered.insert(key.clone(), value.clone());
280                }
281            }
282            let mut result = Map::new();
283            if let Some(decl) = obj.get("?xml") {
284                result.insert("?xml".to_string(), decl.clone());
285            }
286            result.insert(root_key, Value::Object(reordered));
287            Some(Value::Object(result))
288        }
289    }
290
291    /// Serialised form: `Value`'s `==` ignores map key order, so compare strings.
292    fn ordered(value: &Option<Value>) -> String {
293        serde_json::to_string(value).unwrap()
294    }
295
296    #[test]
297    fn owned_merge_matches_reference_including_key_order() {
298        let cases: Vec<Vec<Value>> = vec![
299            // Scalar then array for the same key: the swap-remove branch, which
300            // moves keys -- the order the owned version must reproduce.
301            vec![
302                json!({"R": {"a": "1", "b": "2", "c": "3"}}),
303                json!({"R": {"a": ["x", "y"], "d": "4"}}),
304            ],
305            // Object then object: existing key keeps its position.
306            vec![
307                json!({"?xml": {"@version": "1.0"}, "R": {"o": {"k": 1}, "z": "z"}}),
308                json!({"R": {"o": {"k": 2}, "p": "p"}}),
309                json!({"R": {"o": {"k": 3}}}),
310            ],
311            // Array accumulation, primitive first-wins, mixed shapes.
312            vec![
313                json!({"R": {"items": [{"n": 1}], "label": "first"}}),
314                json!({"R": {"items": [{"n": 2}, {"n": 3}], "label": "second"}}),
315                json!({"R": {"items": {"n": 4}}}),
316            ],
317            // Leading declaration-only / empty / foreign-root / non-object entries.
318            vec![
319                json!({"?xml": {"@version": "1.0", "@encoding": "UTF-8"}}),
320                json!({}),
321                json!("not an object"),
322                json!({"Other": {"x": 1}}),
323                json!({"R": {"x": "kept"}}),
324                json!({"?xml": {"@version": "ignored"}, "R": {"y": "also"}}),
325            ],
326            // Root content that isn't an object is skipped.
327            vec![json!({"R": "text"}), json!({"R": {"a": 1}})],
328            // Nothing to merge: both return None.
329            vec![],
330        ];
331        for (i, elements) in cases.iter().enumerate() {
332            let expected = ordered(&reference::merge(elements));
333            assert_eq!(
334                ordered(&merge_xml_elements_owned(elements.clone())),
335                expected,
336                "case {i}"
337            );
338            assert_eq!(
339                ordered(&merge_xml_elements(elements)),
340                expected,
341                "borrowing wrapper, case {i}"
342            );
343        }
344    }
345
346    #[test]
347    fn owned_reorder_matches_reference_including_key_order() {
348        let order = |keys: &[&str]| keys.iter().map(|k| k.to_string()).collect::<Vec<_>>();
349        let doc = json!({
350            "?xml": {"@version": "1.0"},
351            "R": {"c": 3, "a": [1, 2], "b": {"x": 1}, "d": "4"}
352        });
353        let cases = [
354            order(&["a", "b", "c", "d"]),
355            order(&["d", "missing", "a"]),
356            order(&["b", "b", "a"]), // duplicate key keeps its first position
357            order(&[]),
358        ];
359        for key_order in &cases {
360            let expected = ordered(&reference::reorder(&doc, key_order));
361            assert_eq!(
362                ordered(&reorder_root_keys(&doc, key_order)),
363                expected,
364                "{key_order:?}"
365            );
366            assert_eq!(
367                ordered(&Some(reorder_root_keys_owned(doc.clone(), key_order))),
368                expected,
369                "owned {key_order:?}"
370            );
371        }
372    }
373
374    #[test]
375    fn reorder_owned_returns_unexpected_shapes_unchanged() {
376        let key_order = vec!["a".to_string()];
377        for value in [
378            json!("text"),
379            json!({}),
380            json!({"?xml": {"@version": "1.0"}}),
381            json!({"R": "not an object"}),
382        ] {
383            assert!(reorder_root_keys(&value, &key_order).is_none(), "{value}");
384            assert_eq!(reorder_root_keys_owned(value.clone(), &key_order), value);
385        }
386    }
387
388    #[test]
389    fn merge_empty_returns_none() {
390        assert!(merge_xml_elements(&[]).is_none());
391    }
392
393    #[test]
394    fn merge_single_element_preserves_structure() {
395        let el = json!({
396            "?xml": { "@version": "1.0", "@encoding": "UTF-8" },
397            "Root": { "@xmlns": "http://example.com", "child": "a" }
398        });
399        let merged = merge_xml_elements(std::slice::from_ref(&el)).unwrap();
400        assert!(merged.get("?xml").is_some());
401        let root = merged.get("Root").and_then(|v| v.as_object()).unwrap();
402        assert_eq!(root.get("child").and_then(|v| v.as_str()), Some("a"));
403        assert_eq!(
404            root.get("@xmlns").and_then(|v| v.as_str()),
405            Some("http://example.com")
406        );
407    }
408
409    #[test]
410    fn merge_two_elements_combines_nested_objects_into_array() {
411        let a = json!({ "Root": { "section": { "name": "first" } } });
412        let b = json!({ "Root": { "section": { "name": "second" } } });
413        let merged = merge_xml_elements(&[a, b]).unwrap();
414        let root = merged.get("Root").and_then(|v| v.as_object()).unwrap();
415        let sections = root.get("section").and_then(|v| v.as_array()).unwrap();
416        assert_eq!(sections.len(), 2);
417        assert_eq!(
418            sections[0].get("name").and_then(|v| v.as_str()),
419            Some("first")
420        );
421        assert_eq!(
422            sections[1].get("name").and_then(|v| v.as_str()),
423            Some("second")
424        );
425    }
426
427    #[test]
428    fn reorder_root_keys_reorders_and_appends_extra() {
429        let el = json!({
430            "?xml": { "@version": "1.0" },
431            "Root": { "z": "last", "a": "first", "m": "mid" }
432        });
433        let reordered = reorder_root_keys(&el, &["a".into(), "m".into()]).unwrap();
434        let root = reordered.get("Root").and_then(|v| v.as_object()).unwrap();
435        let keys: Vec<_> = root
436            .keys()
437            .filter(|k| !k.starts_with('@'))
438            .cloned()
439            .collect();
440        assert_eq!(keys, ["a", "m", "z"]);
441    }
442
443    #[test]
444    fn merge_elements_without_declaration_uses_default() {
445        let a = json!({ "Root": { "a": "1" } });
446        let b = json!({ "Root": { "b": "2" } });
447        let merged = merge_xml_elements(&[a, b]).unwrap();
448        // Pin the exact shape of `default_xml_declaration`: a `Default::default()`
449        // substitution would replace it with `Value::Null` and the assertions on
450        // `is_some()` alone would still pass.
451        let decl = merged
452            .get("?xml")
453            .and_then(|v| v.as_object())
454            .expect("default ?xml declaration must be an object, not Null");
455        assert_eq!(decl.get("@version").and_then(|v| v.as_str()), Some("1.0"));
456        assert_eq!(
457            decl.get("@encoding").and_then(|v| v.as_str()),
458            Some("UTF-8")
459        );
460        let root = merged.get("Root").and_then(|v| v.as_object()).unwrap();
461        assert_eq!(root.get("a").and_then(|v| v.as_str()), Some("1"));
462        assert_eq!(root.get("b").and_then(|v| v.as_str()), Some("2"));
463    }
464
465    #[test]
466    fn merge_array_value_coalesces_into_array() {
467        let a = json!({ "Root": { "item": { "x": "1" } } });
468        let b = json!({ "Root": { "item": { "x": "2" } } });
469        let merged = merge_xml_elements(&[a, b]).unwrap();
470        let items = merged
471            .get("Root")
472            .and_then(|r| r.get("item"))
473            .and_then(|i| i.as_array())
474            .unwrap();
475        assert_eq!(items.len(), 2);
476    }
477
478    #[test]
479    fn merge_elements_with_non_object_or_missing_root_skipped() {
480        // First element supplies the root key. Second element is a primitive (skipped).
481        // Third element is an object but lacks the root key (skipped). Fourth element
482        // has the root key but the root value is a primitive (skipped).
483        let a = json!({ "Root": { "item": "a" } });
484        let b = json!("not-an-object");
485        let c = json!({ "Other": { "item": "c" } });
486        let d = json!({ "Root": "primitive" });
487        let merged = merge_xml_elements(&[a, b, c, d]).unwrap();
488        let root = merged.get("Root").and_then(|v| v.as_object()).unwrap();
489        assert_eq!(root.get("item").and_then(|v| v.as_str()), Some("a"));
490    }
491
492    #[test]
493    fn merge_primitive_value_does_not_overwrite_existing() {
494        // First has primitive "item"; second also has primitive "item" - kept first
495        let a = json!({ "Root": { "item": "first" } });
496        let b = json!({ "Root": { "item": "second" } });
497        let merged = merge_xml_elements(&[a, b]).unwrap();
498        let item = merged
499            .get("Root")
500            .and_then(|r| r.get("item"))
501            .and_then(|v| v.as_str())
502            .unwrap();
503        assert_eq!(item, "first");
504    }
505
506    #[test]
507    fn merge_array_value_when_target_key_absent_sets_array() {
508        // Target lacks "item"; second element provides item as array
509        let a = json!({ "Root": { "other": "x" } });
510        let b = json!({ "Root": { "item": [ { "x": "1" } ] } });
511        let merged = merge_xml_elements(&[a, b]).unwrap();
512        let items = merged
513            .get("Root")
514            .and_then(|r| r.get("item"))
515            .and_then(|v| v.as_array())
516            .unwrap();
517        assert_eq!(items.len(), 1);
518    }
519
520    #[test]
521    fn merge_array_value_extends_existing_array() {
522        // Both elements have item as array → extend
523        let a = json!({ "Root": { "item": [ { "x": "1" } ] } });
524        let b = json!({ "Root": { "item": [ { "x": "2" }, { "x": "3" } ] } });
525        let merged = merge_xml_elements(&[a, b]).unwrap();
526        let items = merged
527            .get("Root")
528            .and_then(|r| r.get("item"))
529            .and_then(|v| v.as_array())
530            .unwrap();
531        assert_eq!(items.len(), 3);
532    }
533
534    #[test]
535    fn reorder_root_keys_returns_none_for_invalid_inputs() {
536        // Not an object
537        assert!(reorder_root_keys(&json!("string"), &["a".into()]).is_none());
538        // No root key (only declaration)
539        assert!(reorder_root_keys(&json!({ "?xml": {} }), &["a".into()]).is_none());
540        // Root value not object
541        let el = json!({ "Root": "primitive" });
542        assert!(reorder_root_keys(&el, &["a".into()]).is_none());
543    }
544
545    #[test]
546    fn reorder_root_keys_without_declaration_omits_it() {
547        let el = json!({ "Root": { "b": "2", "a": "1" } });
548        let out = reorder_root_keys(&el, &["a".into(), "b".into()]).unwrap();
549        assert!(out.get("?xml").is_none());
550        let root = out.get("Root").and_then(|v| v.as_object()).unwrap();
551        let keys: Vec<_> = root.keys().cloned().collect();
552        assert_eq!(keys, ["a", "b"]);
553    }
554
555    #[test]
556    fn merge_skips_leading_declaration_only_element() {
557        // Simulates a partially-written disassembled file that only contains the XML
558        // declaration: the merge should still produce a valid root from later elements.
559        let a = json!({ "?xml": { "@version": "1.0" } });
560        let b = json!({ "?xml": { "@version": "1.0" }, "Root": { "item": "x" } });
561        let merged = merge_xml_elements(&[a, b]).unwrap();
562        let root = merged.get("Root").and_then(|v| v.as_object()).unwrap();
563        assert_eq!(root.get("item").and_then(|v| v.as_str()), Some("x"));
564    }
565
566    #[test]
567    fn merge_skips_leading_empty_object_element() {
568        // A fully empty object leading the slice (e.g. an empty file) used to poison
569        // the merge and emit `<root></root>`. Now it's skipped and the next element wins.
570        let a = json!({});
571        let b = json!({ "Root": { "item": "ok" } });
572        let merged = merge_xml_elements(&[a, b]).unwrap();
573        let root = merged.get("Root").and_then(|v| v.as_object()).unwrap();
574        assert_eq!(root.get("item").and_then(|v| v.as_str()), Some("ok"));
575    }
576
577    #[test]
578    fn merge_returns_none_when_every_element_has_no_root() {
579        // All elements are empty or declaration-only: merge must return None so callers
580        // can surface an error rather than writing a stub document.
581        let a = json!({});
582        let b = json!({ "?xml": { "@version": "1.0" } });
583        assert!(merge_xml_elements(&[a, b]).is_none());
584    }
585
586    #[test]
587    fn merge_array_value_when_target_has_key_non_array_coalesces_into_array() {
588        // First element has "item" as object; second has "item" as array → coalesce to [existing, ...new]
589        let a = json!({ "Root": { "item": { "x": "1" } } });
590        let b = json!({ "Root": { "item": [ { "x": "2" } ] } });
591        let merged = merge_xml_elements(&[a, b]).unwrap();
592        let items = merged
593            .get("Root")
594            .and_then(|r| r.get("item"))
595            .and_then(|i| i.as_array())
596            .unwrap();
597        assert_eq!(items.len(), 2);
598        assert_eq!(items[0].get("x").and_then(|v| v.as_str()), Some("1"));
599        assert_eq!(items[1].get("x").and_then(|v| v.as_str()), Some("2"));
600    }
601}