config_disassembler/xml/builders/
build_xml_string.rs1use quick_xml::events::{BytesCData, BytesDecl, BytesEnd, BytesStart, BytesText, Event};
4use quick_xml::Writer;
5use serde_json::{Map, Value};
6
7use crate::xml::types::XmlElement;
8
9fn value_to_string(v: &Value) -> String {
10 match v {
11 Value::String(s) => s.clone(),
12 Value::Number(n) => n.to_string(),
13 Value::Bool(b) => b.to_string(),
14 Value::Null => String::new(),
15 _ => serde_json::to_string(v).unwrap_or_default(),
16 }
17}
18
19fn write_element<W: std::io::Write>(
20 writer: &mut Writer<W>,
21 name: &str,
22 content: &Value,
23 indent_level: usize,
24) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
25 let indent = " ".repeat(indent_level);
26 let child_indent = " ".repeat(indent_level + 1);
27
28 match content {
29 Value::Object(obj) => {
30 let (attrs, children): (Vec<_>, Vec<_>) =
31 obj.iter().partition(|(k, _)| k.starts_with('@'));
32
33 let attr_name = |k: &str| k.trim_start_matches('@').to_string();
34
35 let mut text_content = String::new();
36 let mut comment_content = String::new();
37 let mut text_tail_content = String::new();
38 let mut cdata_content = String::new();
39 let child_elements: Vec<(&String, &Value)> = children
40 .iter()
41 .filter_map(|(k, v)| {
42 if *k == "#text" {
43 text_content = value_to_string(v);
44 None
45 } else if *k == "#comment" {
46 comment_content = value_to_string(v);
47 None
48 } else if *k == "#text-tail" {
49 text_tail_content = value_to_string(v);
50 None
51 } else if *k == "#cdata" {
52 cdata_content = value_to_string(v);
53 None
54 } else {
55 Some((*k, *v))
56 }
57 })
58 .collect();
59
60 let attrs: Vec<(String, String)> = attrs
61 .iter()
62 .map(|(k, v)| (attr_name(k), value_to_string(v)))
63 .collect();
64
65 let mut start = BytesStart::new(name);
66 for (k, v) in &attrs {
67 start.push_attribute((k.as_str(), v.as_str()));
68 }
69 writer.write_event(Event::Start(start))?;
70
71 if !child_elements.is_empty() {
72 writer.write_event(Event::Text(BytesText::new(
73 format!("\n{}", child_indent).as_str(),
74 )))?;
75
76 let child_count = child_elements.len();
77 for (idx, (child_name, child_value)) in child_elements.iter().enumerate() {
78 let is_last = idx == child_count - 1;
79 match child_value {
80 Value::Array(arr) => {
81 let arr_len = arr.len();
82 for (i, item) in arr.iter().enumerate() {
83 let arr_last = i == arr_len - 1;
84 write_element(writer, child_name, item, indent_level + 1)?;
85 if !arr_last {
86 writer.write_event(Event::Text(BytesText::new(
87 format!("\n{}", child_indent).as_str(),
88 )))?;
89 }
90 }
91 if !is_last {
92 writer.write_event(Event::Text(BytesText::new(
93 format!("\n{}", child_indent).as_str(),
94 )))?;
95 }
96 }
97 Value::Object(_) => {
98 write_element(writer, child_name, child_value, indent_level + 1)?;
99 if !is_last {
100 writer.write_event(Event::Text(BytesText::new(
101 format!("\n{}", child_indent).as_str(),
102 )))?;
103 }
104 }
105 _ => {
106 writer
107 .write_event(Event::Start(BytesStart::new(child_name.as_str())))?;
108 writer.write_event(Event::Text(BytesText::new(
110 value_to_string(child_value).as_str(),
111 )))?;
112 writer.write_event(Event::End(BytesEnd::new(child_name.as_str())))?;
113 if !is_last {
114 writer.write_event(Event::Text(BytesText::new(
115 format!("\n{}", child_indent).as_str(),
116 )))?;
117 }
118 }
119 }
120 }
121
122 writer.write_event(Event::Text(BytesText::new(
123 format!("\n{}", indent).as_str(),
124 )))?;
125 } else if !cdata_content.is_empty()
126 || !text_content.is_empty()
127 || !comment_content.is_empty()
128 || !text_tail_content.is_empty()
129 {
130 if text_content.is_empty() && comment_content.is_empty() {
132 writer.write_event(Event::Text(BytesText::new(
133 format!("\n{}", child_indent).as_str(),
134 )))?;
135 }
136 if !text_content.is_empty() {
138 writer.write_event(Event::Text(BytesText::new(text_content.as_str())))?;
139 }
140 if !comment_content.is_empty() {
141 writer.write_event(Event::Comment(BytesText::new(comment_content.as_str())))?;
142 }
143 if !text_tail_content.is_empty() {
144 writer.write_event(Event::Text(BytesText::new(text_tail_content.as_str())))?;
145 }
146 if !cdata_content.is_empty() {
147 writer.write_event(Event::CData(BytesCData::new(cdata_content.as_str())))?;
148 }
149 if !cdata_content.is_empty() {
151 writer.write_event(Event::Text(BytesText::new(
152 format!("\n{}", indent).as_str(),
153 )))?;
154 }
155 }
156
157 writer.write_event(Event::End(BytesEnd::new(name)))?;
158 }
159 Value::Array(arr) => {
160 for item in arr {
161 write_element(writer, name, item, indent_level)?;
162 }
163 }
164 _ => {
165 writer.write_event(Event::Start(BytesStart::new(name)))?;
166 writer.write_event(Event::Text(BytesText::new(
168 value_to_string(content).as_str(),
169 )))?;
170 writer.write_event(Event::End(BytesEnd::new(name)))?;
171 }
172 }
173
174 Ok(())
175}
176
177fn build_xml_from_object(
178 element: &Map<String, Value>,
179) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
180 let mut writer = Writer::new(Vec::new());
182
183 let (declaration, root_key, root_value) = if let Some(decl) = element.get("?xml") {
184 let root_key = element
185 .keys()
186 .find(|k| *k != "?xml")
187 .cloned()
188 .unwrap_or_else(|| "root".to_string());
189 let root_value = element
190 .get(&root_key)
191 .cloned()
192 .unwrap_or_else(|| Value::Object(Map::new()));
193 (Some(decl), root_key, root_value)
194 } else {
195 let root_key = element
196 .keys()
197 .next()
198 .cloned()
199 .unwrap_or_else(|| "root".to_string());
200 let root_value = element
201 .get(&root_key)
202 .cloned()
203 .unwrap_or_else(|| Value::Object(Map::new()));
204 (None, root_key, root_value)
205 };
206
207 if let Some(obj) = declaration.and_then(|d| d.as_object()) {
208 let version = obj
209 .get("@version")
210 .and_then(|v| v.as_str())
211 .unwrap_or("1.0");
212 let encoding = obj.get("@encoding").and_then(|v| v.as_str());
213 let standalone = obj.get("@standalone").and_then(|v| v.as_str());
214 writer.write_event(Event::Decl(BytesDecl::new(version, encoding, standalone)))?;
215 writer.write_event(Event::Text(BytesText::new("\n")))?;
216 }
217
218 write_element(&mut writer, &root_key, &root_value, 0)?;
219
220 let result = String::from_utf8(writer.into_inner())?;
221 Ok(result.trim_end().to_string())
222}
223
224pub fn build_xml_string(element: &XmlElement) -> String {
226 match element {
227 Value::Object(obj) => build_xml_from_object(obj).unwrap_or_default(),
228 _ => String::new(),
229 }
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235 use serde_json::json;
236
237 #[test]
238 fn build_xml_string_non_object_returns_empty() {
239 assert!(build_xml_string(&Value::Array(vec![])).is_empty());
240 assert!(build_xml_string(&Value::Null).is_empty());
241 }
242
243 #[test]
244 fn build_xml_string_simple_root() {
245 let el = json!({
246 "?xml": { "@version": "1.0", "@encoding": "UTF-8" },
247 "root": { "child": "value" }
248 });
249 let out = build_xml_string(&el);
250 assert!(out.contains("<?xml"));
251 assert!(out.contains("<root>"));
252 assert!(out.contains("<child>value</child>"));
253 assert!(out.contains("</root>"));
254 }
255
256 #[test]
257 fn build_xml_string_with_attributes() {
258 let el = json!({
259 "root": { "@xmlns": "http://example.com", "a": "b" }
260 });
261 let out = build_xml_string(&el);
262 assert!(out.contains("xmlns"));
263 assert!(out.contains("http://example.com"));
264 assert!(out.contains("<a>b</a>"));
265 }
266
267 #[test]
268 fn build_xml_string_with_array() {
269 let el = json!({
270 "root": { "item": [ { "x": "1" }, { "x": "2" } ] }
271 });
272 let out = build_xml_string(&el);
273 assert!(out.contains("<item>"));
274 assert!(out.contains("<x>1</x>"));
275 assert!(out.contains("<x>2</x>"));
276 }
277
278 #[test]
279 fn build_xml_string_without_declaration() {
280 let el = json!({ "root": { "a": "b" } });
281 let out = build_xml_string(&el);
282 assert!(!out.contains("<?xml"));
283 assert!(out.contains("<root>"));
284 }
285
286 #[test]
287 fn build_xml_string_with_text_comment_cdata() {
288 let root = json!({
289 "#text": "text",
290 "#comment": " a comment ",
291 "#cdata": "<cdata>"
292 });
293 let el = json!({
294 "?xml": { "@version": "1.0" },
295 "root": root
296 });
297 let out = build_xml_string(&el);
298 assert!(out.contains("text"));
299 assert!(out.contains("<!--"));
300 assert!(out.contains(" a comment "));
301 assert!(out.contains("<![CDATA["));
302 assert!(out.contains("<cdata>"));
303 }
304
305 #[test]
306 fn build_xml_string_with_declaration_encoding_standalone() {
307 let el = json!({
308 "?xml": { "@version": "1.0", "@encoding": "UTF-8", "@standalone": "yes" },
309 "root": { "a": "b" }
310 });
311 let out = build_xml_string(&el);
312 assert!(out.contains("<?xml"));
313 assert!(out.contains("UTF-8"));
314 assert!(out.contains("standalone"));
315 assert!(out.contains("<root>"));
316 }
317
318 #[test]
319 fn build_xml_string_primitive_sibling_children() {
320 let el = json!({
322 "root": { "obj": { "x": "1" }, "num": 42, "flag": true }
323 });
324 let out = build_xml_string(&el);
325 assert!(out.contains("<obj>"));
326 assert!(out.contains("<num>42</num>"));
327 assert!(out.contains("<flag>true</flag>"));
328 }
329
330 #[test]
331 fn build_xml_string_null_child_value() {
332 let el = json!({
333 "root": { "empty": null }
334 });
335 let out = build_xml_string(&el);
336 assert!(out.contains("<empty>"));
337 assert!(out.contains("</empty>"));
338 assert!(
342 !out.contains("null"),
343 "Value::Null child should render as empty content, not the string \"null\": {out}"
344 );
345 assert!(out.contains("<empty></empty>"));
346 }
347
348 #[test]
349 fn build_xml_string_primitive_siblings_have_inter_element_indent() {
350 let el = json!({ "root": { "a": 1, "b": 2 } });
355 let out = build_xml_string(&el);
356 assert!(
357 out.contains("<a>1</a>\n <b>2</b>"),
358 "expected `<a>1</a>` to be followed by newline + 4-space indent then `<b>2</b>`, got:\n{out}"
359 );
360 assert!(
363 out.contains("<b>2</b>\n</root>"),
364 "expected `<b>2</b>` to be followed directly by the root close tag, got:\n{out}"
365 );
366 }
367
368 #[test]
369 fn build_xml_string_comment_only_leaf() {
370 let el = json!({
374 "?xml": { "@version": "1.0" },
375 "root": { "#comment": " just a comment " }
376 });
377 let out = build_xml_string(&el);
378 assert!(out.contains("<!--"), "expected comment open in: {out}");
379 assert!(
380 out.contains(" just a comment "),
381 "expected comment text preserved verbatim in: {out}"
382 );
383 assert!(out.contains("-->"));
384 }
385
386 #[test]
387 fn build_xml_string_text_tail_only_leaf() {
388 let el = json!({
392 "?xml": { "@version": "1.0" },
393 "root": { "#text-tail": "tail-only-content" }
394 });
395 let out = build_xml_string(&el);
396 assert!(
397 out.contains("tail-only-content"),
398 "expected text-tail content rendered between root tags, got:\n{out}"
399 );
400 assert!(out.contains("<root>"));
401 assert!(out.contains("</root>"));
402 }
403
404 #[test]
405 fn build_xml_string_cdata_only_no_text_or_comment() {
406 let root = json!({ "#cdata": "only cdata content" });
407 let el = json!({ "?xml": { "@version": "1.0" }, "root": root });
408 let out = build_xml_string(&el);
409 assert!(out.contains("<![CDATA["));
410 assert!(out.contains("only cdata content"));
411 }
412
413 #[test]
414 fn build_xml_string_declaration_only_defaults_root_key() {
415 let el = json!({ "?xml": { "@version": "1.0", "@encoding": "UTF-8" } });
416 let out = build_xml_string(&el);
417 assert!(out.contains("<?xml"));
418 assert!(out.contains("<root>"));
419 }
420
421 #[test]
422 fn build_xml_string_declaration_non_object_skips_decl_write() {
423 let el = json!({ "?xml": "not-an-object", "root": { "a": "b" } });
424 let out = build_xml_string(&el);
425 assert!(!out.contains("<?xml"));
426 assert!(out.contains("<root>"));
427 }
428
429 #[test]
430 fn build_xml_string_root_value_array_sibling_elements() {
431 let el = json!({
433 "root": [ { "a": "1" }, { "b": "2" } ]
434 });
435 let out = build_xml_string(&el);
436 assert!(out.contains("<root>"));
437 assert!(out.contains("<a>1</a>"));
438 assert!(out.contains("<b>2</b>"));
439 assert!(out.contains("</root>"));
440 }
441
442 #[test]
443 fn build_xml_string_root_value_primitive() {
444 let el = json!({ "root": 42 });
446 let out = build_xml_string(&el);
447 assert!(out.contains("<root>42</root>"));
448 }
449
450 #[test]
451 fn build_xml_string_attribute_value_object_uses_serde_fallback() {
452 let el = json!({
454 "root": { "@complex": { "nested": true }, "child": "v" }
455 });
456 let out = build_xml_string(&el);
457 assert!(out.contains("child"));
458 assert!(out.contains("v"));
459 }
460}