Skip to main content

xml_disassembler/builders/
merge_xml_elements.rs

1//! Merge multiple XML elements into one.
2
3use serde_json::{Map, Value};
4
5use crate::types::XmlElement;
6
7fn is_mergeable_object(value: &Value) -> bool {
8    value.is_object() && !value.is_array()
9}
10
11fn merge_element_content(target: &mut Map<String, Value>, source: &Map<String, Value>) {
12    for (key, value) in source {
13        if value.is_array() {
14            merge_array_value(target, key, value.as_array().unwrap());
15        } else if is_mergeable_object(value) {
16            merge_object_value(target, key, value.as_object().unwrap());
17        } else {
18            merge_primitive_value(target, key, value);
19        }
20    }
21}
22
23fn merge_array_value(target: &mut Map<String, Value>, key: &str, value: &[Value]) {
24    if !target.contains_key(key) {
25        target.insert(key.to_string(), Value::Array(value.to_vec()));
26    } else if let Some(Value::Array(arr)) = target.get_mut(key) {
27        arr.extend(value.iter().cloned());
28    } else {
29        let existing = target.remove(key).unwrap();
30        target.insert(
31            key.to_string(),
32            Value::Array(
33                [vec![existing], value.to_vec()]
34                    .into_iter()
35                    .flatten()
36                    .collect(),
37            ),
38        );
39    }
40}
41
42fn merge_object_value(target: &mut Map<String, Value>, key: &str, value: &Map<String, Value>) {
43    if let Some(Value::Array(arr)) = target.get_mut(key) {
44        arr.push(Value::Object(value.clone()));
45    } else if let Some(existing) = target.get(key) {
46        let existing = existing.clone();
47        target.insert(
48            key.to_string(),
49            Value::Array(vec![existing, Value::Object(value.clone())]),
50        );
51    } else {
52        target.insert(key.to_string(), Value::Object(value.clone()));
53    }
54}
55
56fn merge_primitive_value(target: &mut Map<String, Value>, key: &str, value: &Value) {
57    if !target.contains_key(key) {
58        target.insert(key.to_string(), value.clone());
59    }
60}
61
62fn default_xml_declaration() -> Value {
63    let mut decl = Map::new();
64    decl.insert("@version".to_string(), Value::String("1.0".to_string()));
65    decl.insert("@encoding".to_string(), Value::String("UTF-8".to_string()));
66    Value::Object(decl)
67}
68
69fn build_final_xml_element(
70    declaration: Option<&Value>,
71    root_key: &str,
72    content: Map<String, Value>,
73) -> XmlElement {
74    let mut result = Map::new();
75    let decl = declaration.cloned().unwrap_or_else(default_xml_declaration);
76    result.insert("?xml".to_string(), decl);
77    result.insert(root_key.to_string(), Value::Object(content));
78    Value::Object(result)
79}
80
81/// Reorder the root element's child keys to match the given order.
82/// Keys not in `key_order` are appended at the end.
83pub fn reorder_root_keys(element: &XmlElement, key_order: &[String]) -> Option<XmlElement> {
84    let obj = element.as_object()?;
85    let root_key = obj.keys().find(|k| *k != "?xml")?.clone();
86    let root_content = obj.get(&root_key)?.as_object()?;
87    let mut reordered = Map::new();
88    for key in key_order {
89        if let Some(v) = root_content.get(key) {
90            reordered.insert(key.clone(), v.clone());
91        }
92    }
93    for (key, value) in root_content {
94        if !reordered.contains_key(key) {
95            reordered.insert(key.clone(), value.clone());
96        }
97    }
98    let mut result = Map::new();
99    if let Some(decl) = obj.get("?xml") {
100        result.insert("?xml".to_string(), decl.clone());
101    }
102    result.insert(root_key, Value::Object(reordered));
103    Some(Value::Object(result))
104}
105
106/// Merge multiple XML elements into one.
107///
108/// Scans elements for the first non-`?xml` root key; tolerates leading entries
109/// that are empty or declaration-only (e.g. a partially-written disassembled file),
110/// which previously caused the whole merge to return `None` and the caller to
111/// emit an empty `<root>` document.
112pub fn merge_xml_elements(elements: &[XmlElement]) -> Option<XmlElement> {
113    if elements.is_empty() {
114        log::error!("No elements to merge.");
115        return None;
116    }
117
118    let root_key = elements
119        .iter()
120        .find_map(|el| el.as_object()?.keys().find(|k| *k != "?xml").cloned())?;
121
122    let declaration = elements.iter().find_map(|el| el.as_object()?.get("?xml"));
123
124    let mut merged_content = Map::new();
125    for element in elements {
126        if let Some(obj) = element.as_object() {
127            if let Some(root_content) = obj.get(&root_key) {
128                if let Some(content_obj) = root_content.as_object() {
129                    merge_element_content(&mut merged_content, content_obj);
130                }
131            }
132        }
133    }
134
135    Some(build_final_xml_element(
136        declaration,
137        &root_key,
138        merged_content,
139    ))
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145    use serde_json::json;
146
147    #[test]
148    fn merge_empty_returns_none() {
149        assert!(merge_xml_elements(&[]).is_none());
150    }
151
152    #[test]
153    fn merge_single_element_preserves_structure() {
154        let el = json!({
155            "?xml": { "@version": "1.0", "@encoding": "UTF-8" },
156            "Root": { "@xmlns": "http://example.com", "child": "a" }
157        });
158        let merged = merge_xml_elements(std::slice::from_ref(&el)).unwrap();
159        assert!(merged.get("?xml").is_some());
160        let root = merged.get("Root").and_then(|v| v.as_object()).unwrap();
161        assert_eq!(root.get("child").and_then(|v| v.as_str()), Some("a"));
162        assert_eq!(
163            root.get("@xmlns").and_then(|v| v.as_str()),
164            Some("http://example.com")
165        );
166    }
167
168    #[test]
169    fn merge_two_elements_combines_nested_objects_into_array() {
170        let a = json!({ "Root": { "section": { "name": "first" } } });
171        let b = json!({ "Root": { "section": { "name": "second" } } });
172        let merged = merge_xml_elements(&[a, b]).unwrap();
173        let root = merged.get("Root").and_then(|v| v.as_object()).unwrap();
174        let sections = root.get("section").and_then(|v| v.as_array()).unwrap();
175        assert_eq!(sections.len(), 2);
176        assert_eq!(
177            sections[0].get("name").and_then(|v| v.as_str()),
178            Some("first")
179        );
180        assert_eq!(
181            sections[1].get("name").and_then(|v| v.as_str()),
182            Some("second")
183        );
184    }
185
186    #[test]
187    fn reorder_root_keys_reorders_and_appends_extra() {
188        let el = json!({
189            "?xml": { "@version": "1.0" },
190            "Root": { "z": "last", "a": "first", "m": "mid" }
191        });
192        let reordered = reorder_root_keys(&el, &["a".into(), "m".into()]).unwrap();
193        let root = reordered.get("Root").and_then(|v| v.as_object()).unwrap();
194        let keys: Vec<_> = root
195            .keys()
196            .filter(|k| !k.starts_with('@'))
197            .cloned()
198            .collect();
199        assert_eq!(keys, ["a", "m", "z"]);
200    }
201
202    #[test]
203    fn merge_elements_without_declaration_uses_default() {
204        let a = json!({ "Root": { "a": "1" } });
205        let b = json!({ "Root": { "b": "2" } });
206        let merged = merge_xml_elements(&[a, b]).unwrap();
207        assert!(merged.get("?xml").is_some());
208        let root = merged.get("Root").and_then(|v| v.as_object()).unwrap();
209        assert_eq!(root.get("a").and_then(|v| v.as_str()), Some("1"));
210        assert_eq!(root.get("b").and_then(|v| v.as_str()), Some("2"));
211    }
212
213    #[test]
214    fn merge_array_value_coalesces_into_array() {
215        let a = json!({ "Root": { "item": { "x": "1" } } });
216        let b = json!({ "Root": { "item": { "x": "2" } } });
217        let merged = merge_xml_elements(&[a, b]).unwrap();
218        let items = merged
219            .get("Root")
220            .and_then(|r| r.get("item"))
221            .and_then(|i| i.as_array())
222            .unwrap();
223        assert_eq!(items.len(), 2);
224    }
225
226    #[test]
227    fn merge_elements_with_non_object_or_missing_root_skipped() {
228        // First element supplies the root key. Second element is a primitive (skipped).
229        // Third element is an object but lacks the root key (skipped). Fourth element
230        // has the root key but the root value is a primitive (skipped).
231        let a = json!({ "Root": { "item": "a" } });
232        let b = json!("not-an-object");
233        let c = json!({ "Other": { "item": "c" } });
234        let d = json!({ "Root": "primitive" });
235        let merged = merge_xml_elements(&[a, b, c, d]).unwrap();
236        let root = merged.get("Root").and_then(|v| v.as_object()).unwrap();
237        assert_eq!(root.get("item").and_then(|v| v.as_str()), Some("a"));
238    }
239
240    #[test]
241    fn merge_primitive_value_does_not_overwrite_existing() {
242        // First has primitive "item"; second also has primitive "item" - kept first
243        let a = json!({ "Root": { "item": "first" } });
244        let b = json!({ "Root": { "item": "second" } });
245        let merged = merge_xml_elements(&[a, b]).unwrap();
246        let item = merged
247            .get("Root")
248            .and_then(|r| r.get("item"))
249            .and_then(|v| v.as_str())
250            .unwrap();
251        assert_eq!(item, "first");
252    }
253
254    #[test]
255    fn merge_array_value_when_target_key_absent_sets_array() {
256        // Target lacks "item"; second element provides item as array
257        let a = json!({ "Root": { "other": "x" } });
258        let b = json!({ "Root": { "item": [ { "x": "1" } ] } });
259        let merged = merge_xml_elements(&[a, b]).unwrap();
260        let items = merged
261            .get("Root")
262            .and_then(|r| r.get("item"))
263            .and_then(|v| v.as_array())
264            .unwrap();
265        assert_eq!(items.len(), 1);
266    }
267
268    #[test]
269    fn merge_array_value_extends_existing_array() {
270        // Both elements have item as array → extend
271        let a = json!({ "Root": { "item": [ { "x": "1" } ] } });
272        let b = json!({ "Root": { "item": [ { "x": "2" }, { "x": "3" } ] } });
273        let merged = merge_xml_elements(&[a, b]).unwrap();
274        let items = merged
275            .get("Root")
276            .and_then(|r| r.get("item"))
277            .and_then(|v| v.as_array())
278            .unwrap();
279        assert_eq!(items.len(), 3);
280    }
281
282    #[test]
283    fn reorder_root_keys_returns_none_for_invalid_inputs() {
284        // Not an object
285        assert!(reorder_root_keys(&json!("string"), &["a".into()]).is_none());
286        // No root key (only declaration)
287        assert!(reorder_root_keys(&json!({ "?xml": {} }), &["a".into()]).is_none());
288        // Root value not object
289        let el = json!({ "Root": "primitive" });
290        assert!(reorder_root_keys(&el, &["a".into()]).is_none());
291    }
292
293    #[test]
294    fn reorder_root_keys_without_declaration_omits_it() {
295        let el = json!({ "Root": { "b": "2", "a": "1" } });
296        let out = reorder_root_keys(&el, &["a".into(), "b".into()]).unwrap();
297        assert!(out.get("?xml").is_none());
298        let root = out.get("Root").and_then(|v| v.as_object()).unwrap();
299        let keys: Vec<_> = root.keys().cloned().collect();
300        assert_eq!(keys, ["a", "b"]);
301    }
302
303    #[test]
304    fn merge_skips_leading_declaration_only_element() {
305        // Simulates a partially-written disassembled file that only contains the XML
306        // declaration: the merge should still produce a valid root from later elements.
307        let a = json!({ "?xml": { "@version": "1.0" } });
308        let b = json!({ "?xml": { "@version": "1.0" }, "Root": { "item": "x" } });
309        let merged = merge_xml_elements(&[a, b]).unwrap();
310        let root = merged.get("Root").and_then(|v| v.as_object()).unwrap();
311        assert_eq!(root.get("item").and_then(|v| v.as_str()), Some("x"));
312    }
313
314    #[test]
315    fn merge_skips_leading_empty_object_element() {
316        // A fully empty object leading the slice (e.g. an empty file) used to poison
317        // the merge and emit `<root></root>`. Now it's skipped and the next element wins.
318        let a = json!({});
319        let b = json!({ "Root": { "item": "ok" } });
320        let merged = merge_xml_elements(&[a, b]).unwrap();
321        let root = merged.get("Root").and_then(|v| v.as_object()).unwrap();
322        assert_eq!(root.get("item").and_then(|v| v.as_str()), Some("ok"));
323    }
324
325    #[test]
326    fn merge_returns_none_when_every_element_has_no_root() {
327        // All elements are empty or declaration-only: merge must return None so callers
328        // can surface an error rather than writing a stub document.
329        let a = json!({});
330        let b = json!({ "?xml": { "@version": "1.0" } });
331        assert!(merge_xml_elements(&[a, b]).is_none());
332    }
333
334    #[test]
335    fn merge_array_value_when_target_has_key_non_array_coalesces_into_array() {
336        // First element has "item" as object; second has "item" as array → coalesce to [existing, ...new]
337        let a = json!({ "Root": { "item": { "x": "1" } } });
338        let b = json!({ "Root": { "item": [ { "x": "2" } ] } });
339        let merged = merge_xml_elements(&[a, b]).unwrap();
340        let items = merged
341            .get("Root")
342            .and_then(|r| r.get("item"))
343            .and_then(|i| i.as_array())
344            .unwrap();
345        assert_eq!(items.len(), 2);
346        assert_eq!(items[0].get("x").and_then(|v| v.as_str()), Some("1"));
347        assert_eq!(items[1].get("x").and_then(|v| v.as_str()), Some("2"));
348    }
349}