config_disassembler/xml/builders/
build_xml_string.rs1use quick_xml::escape::partial_escape;
4use quick_xml::events::{BytesCData, BytesDecl, BytesEnd, BytesStart, BytesText, Event};
5use quick_xml::Writer;
6use serde_json::{Map, Value};
7
8use crate::xml::types::XmlElement;
9
10fn value_to_string(v: &Value) -> String {
11 match v {
12 Value::String(s) => s.clone(),
13 Value::Number(n) => n.to_string(),
14 Value::Bool(b) => b.to_string(),
15 Value::Null => String::new(),
16 _ => serde_json::to_string(v).unwrap_or_default(),
17 }
18}
19
20fn write_element<W: std::io::Write>(
21 writer: &mut Writer<W>,
22 name: &str,
23 content: &Value,
24 indent_level: usize,
25) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
26 let indent = " ".repeat(indent_level);
27 let child_indent = " ".repeat(indent_level + 1);
28
29 match content {
30 Value::Object(obj) => {
31 let (attrs, children): (Vec<_>, Vec<_>) =
32 obj.iter().partition(|(k, _)| k.starts_with('@'));
33
34 let attr_name = |k: &str| k.trim_start_matches('@').to_string();
35
36 let mut text_content = String::new();
37 let mut raw_text_content = String::new();
38 let mut comment_content = String::new();
39 let mut text_tail_content = String::new();
40 let mut cdata_content = String::new();
41 let mut is_compact = false;
42 let child_elements: Vec<(&String, &Value)> = children
43 .iter()
44 .filter_map(|(k, v)| {
45 if *k == "#text" {
46 text_content = value_to_string(v);
47 None
48 } else if *k == "#raw-text" {
49 raw_text_content = value_to_string(v);
50 None
51 } else if *k == "#comment" {
52 comment_content = value_to_string(v);
53 None
54 } else if *k == "#text-tail" {
55 text_tail_content = value_to_string(v);
56 None
57 } else if *k == "#cdata" {
58 cdata_content = value_to_string(v);
59 None
60 } else if *k == "#compact" {
61 is_compact = v.as_bool().unwrap_or(false);
62 None
63 } else {
64 Some((*k, *v))
65 }
66 })
67 .collect();
68
69 let attrs: Vec<(String, String)> = attrs
70 .iter()
71 .map(|(k, v)| (attr_name(k), value_to_string(v)))
72 .collect();
73
74 let mut start = BytesStart::new(name);
75 for (k, v) in &attrs {
76 start.push_attribute((k.as_str(), v.as_str()));
77 }
78 writer.write_event(Event::Start(start))?;
79
80 let has_mixed_content = !cdata_content.is_empty()
81 || !text_content.is_empty()
82 || !raw_text_content.is_empty()
83 || !comment_content.is_empty()
84 || !text_tail_content.is_empty();
85
86 if is_compact
87 && child_elements.len() == 1
88 && matches!(child_elements[0].1, Value::Object(_))
89 {
90 let (child_name, child_value) = child_elements[0];
95 write_element(writer, child_name, child_value, indent_level)?;
96 } else {
97 if !child_elements.is_empty() {
98 writer.write_event(Event::Text(BytesText::new(
99 format!("\n{}", child_indent).as_str(),
100 )))?;
101
102 let child_count = child_elements.len();
103 for (idx, (child_name, child_value)) in child_elements.iter().enumerate() {
104 let is_last = idx == child_count - 1;
105 match child_value {
106 Value::Array(arr) => {
107 let arr_len = arr.len();
108 for (i, item) in arr.iter().enumerate() {
109 let arr_last = i == arr_len - 1;
110 write_element(writer, child_name, item, indent_level + 1)?;
111 if !arr_last {
112 writer.write_event(Event::Text(BytesText::new(
113 format!("\n{}", child_indent).as_str(),
114 )))?;
115 }
116 }
117 if !is_last {
118 writer.write_event(Event::Text(BytesText::new(
119 format!("\n{}", child_indent).as_str(),
120 )))?;
121 }
122 }
123 Value::Object(_) => {
124 write_element(writer, child_name, child_value, indent_level + 1)?;
125 if !is_last {
126 writer.write_event(Event::Text(BytesText::new(
127 format!("\n{}", child_indent).as_str(),
128 )))?;
129 }
130 }
131 _ => {
132 writer.write_event(Event::Start(BytesStart::new(
133 child_name.as_str(),
134 )))?;
135 writer.write_event(Event::Text(BytesText::new(
136 value_to_string(child_value).as_str(),
137 )))?;
138 writer
139 .write_event(Event::End(BytesEnd::new(child_name.as_str())))?;
140 if !is_last {
141 writer.write_event(Event::Text(BytesText::new(
142 format!("\n{}", child_indent).as_str(),
143 )))?;
144 }
145 }
146 }
147 }
148
149 writer.write_event(Event::Text(BytesText::new(
150 format!("\n{}", indent).as_str(),
151 )))?;
152 }
153
154 if has_mixed_content {
162 if child_elements.is_empty()
167 && text_content.is_empty()
168 && raw_text_content.is_empty()
169 && comment_content.is_empty()
170 {
171 writer.write_event(Event::Text(BytesText::new(
172 format!("\n{}", child_indent).as_str(),
173 )))?;
174 }
175 if !text_content.is_empty() {
177 writer.write_event(Event::Text(BytesText::new(text_content.as_str())))?;
178 }
179 if !raw_text_content.is_empty() {
182 writer.write_event(Event::Text(BytesText::from_escaped(
183 partial_escape(raw_text_content.as_str()),
184 )))?;
185 }
186 if !comment_content.is_empty() {
187 writer.write_event(Event::Comment(BytesText::from_escaped(
195 comment_content.as_str(),
196 )))?;
197 }
198 if !text_tail_content.is_empty() {
199 writer
200 .write_event(Event::Text(BytesText::new(text_tail_content.as_str())))?;
201 }
202 if !cdata_content.is_empty() {
203 writer
204 .write_event(Event::CData(BytesCData::new(cdata_content.as_str())))?;
205 }
206 if !cdata_content.is_empty() && child_elements.is_empty() {
210 writer.write_event(Event::Text(BytesText::new(
211 format!("\n{}", indent).as_str(),
212 )))?;
213 }
214 }
215 }
216
217 writer.write_event(Event::End(BytesEnd::new(name)))?;
218 }
219 Value::Array(arr) => {
220 for item in arr {
221 write_element(writer, name, item, indent_level)?;
222 }
223 }
224 _ => {
225 writer.write_event(Event::Start(BytesStart::new(name)))?;
226 writer.write_event(Event::Text(BytesText::new(
227 value_to_string(content).as_str(),
228 )))?;
229 writer.write_event(Event::End(BytesEnd::new(name)))?;
230 }
231 }
232
233 Ok(())
234}
235
236fn build_xml_from_object(
237 element: &Map<String, Value>,
238) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
239 let mut writer = Writer::new(Vec::new());
241
242 let (declaration, root_key, root_value) = if let Some(decl) = element.get("?xml") {
243 let root_key = element
244 .keys()
245 .find(|k| *k != "?xml")
246 .cloned()
247 .unwrap_or_else(|| "root".to_string());
248 let root_value = element
249 .get(&root_key)
250 .cloned()
251 .unwrap_or_else(|| Value::Object(Map::new()));
252 (Some(decl), root_key, root_value)
253 } else {
254 let root_key = element
255 .keys()
256 .next()
257 .cloned()
258 .unwrap_or_else(|| "root".to_string());
259 let root_value = element
260 .get(&root_key)
261 .cloned()
262 .unwrap_or_else(|| Value::Object(Map::new()));
263 (None, root_key, root_value)
264 };
265
266 if let Some(obj) = declaration.and_then(|d| d.as_object()) {
267 let version = obj
268 .get("@version")
269 .and_then(|v| v.as_str())
270 .unwrap_or("1.0");
271 let encoding = obj.get("@encoding").and_then(|v| v.as_str());
272 let standalone = obj.get("@standalone").and_then(|v| v.as_str());
273 writer.write_event(Event::Decl(BytesDecl::new(version, encoding, standalone)))?;
274 writer.write_event(Event::Text(BytesText::new("\n")))?;
275 }
276
277 write_element(&mut writer, &root_key, &root_value, 0)?;
278
279 let result = String::from_utf8(writer.into_inner())?;
280 Ok(result.trim_end().to_string())
281}
282
283pub fn build_xml_string(element: &XmlElement) -> String {
285 match element {
286 Value::Object(obj) => build_xml_from_object(obj).unwrap_or_default(),
287 _ => String::new(),
288 }
289}
290
291#[cfg(test)]
292mod tests {
293 use super::*;
294 use serde_json::json;
295
296 #[test]
297 fn build_xml_string_non_object_returns_empty() {
298 assert!(build_xml_string(&Value::Array(vec![])).is_empty());
299 assert!(build_xml_string(&Value::Null).is_empty());
300 }
301
302 #[test]
303 fn build_xml_string_simple_root() {
304 let el = json!({
305 "?xml": { "@version": "1.0", "@encoding": "UTF-8" },
306 "root": { "child": "value" }
307 });
308 let out = build_xml_string(&el);
309 assert!(out.contains("<?xml"));
310 assert!(out.contains("<root>"));
311 assert!(out.contains("<child>value</child>"));
312 assert!(out.contains("</root>"));
313 }
314
315 #[test]
316 fn build_xml_string_with_attributes() {
317 let el = json!({
318 "root": { "@xmlns": "http://example.com", "a": "b" }
319 });
320 let out = build_xml_string(&el);
321 assert!(out.contains("xmlns"));
322 assert!(out.contains("http://example.com"));
323 assert!(out.contains("<a>b</a>"));
324 }
325
326 #[test]
327 fn build_xml_string_with_array() {
328 let el = json!({
329 "root": { "item": [ { "x": "1" }, { "x": "2" } ] }
330 });
331 let out = build_xml_string(&el);
332 assert!(out.contains("<item>"));
333 assert!(out.contains("<x>1</x>"));
334 assert!(out.contains("<x>2</x>"));
335 }
336
337 #[test]
338 fn build_xml_string_without_declaration() {
339 let el = json!({ "root": { "a": "b" } });
340 let out = build_xml_string(&el);
341 assert!(!out.contains("<?xml"));
342 assert!(out.contains("<root>"));
343 }
344
345 #[test]
346 fn build_xml_string_with_text_comment_cdata() {
347 let root = json!({
348 "#text": "text",
349 "#comment": " a comment ",
350 "#cdata": "<cdata>"
351 });
352 let el = json!({
353 "?xml": { "@version": "1.0" },
354 "root": root
355 });
356 let out = build_xml_string(&el);
357 assert!(out.contains("text"));
358 assert!(out.contains("<!--"));
359 assert!(out.contains(" a comment "));
360 assert!(out.contains("<![CDATA["));
361 assert!(out.contains("<cdata>"));
362 }
363
364 #[test]
365 fn build_xml_string_with_declaration_encoding_standalone() {
366 let el = json!({
367 "?xml": { "@version": "1.0", "@encoding": "UTF-8", "@standalone": "yes" },
368 "root": { "a": "b" }
369 });
370 let out = build_xml_string(&el);
371 assert!(out.contains("<?xml"));
372 assert!(out.contains("UTF-8"));
373 assert!(out.contains("standalone"));
374 assert!(out.contains("<root>"));
375 }
376
377 #[test]
378 fn build_xml_string_primitive_sibling_children() {
379 let el = json!({
381 "root": { "obj": { "x": "1" }, "num": 42, "flag": true }
382 });
383 let out = build_xml_string(&el);
384 assert!(out.contains("<obj>"));
385 assert!(out.contains("<num>42</num>"));
386 assert!(out.contains("<flag>true</flag>"));
387 }
388
389 #[test]
390 fn build_xml_string_null_child_value() {
391 let el = json!({
392 "root": { "empty": null }
393 });
394 let out = build_xml_string(&el);
395 assert!(out.contains("<empty>"));
396 assert!(out.contains("</empty>"));
397 assert!(
401 !out.contains("null"),
402 "Value::Null child should render as empty content, not the string \"null\": {out}"
403 );
404 assert!(out.contains("<empty></empty>"));
405 }
406
407 #[test]
408 fn build_xml_string_primitive_siblings_have_inter_element_indent() {
409 let el = json!({ "root": { "a": 1, "b": 2 } });
414 let out = build_xml_string(&el);
415 assert!(
416 out.contains("<a>1</a>\n <b>2</b>"),
417 "expected `<a>1</a>` to be followed by newline + 4-space indent then `<b>2</b>`, got:\n{out}"
418 );
419 assert!(
422 out.contains("<b>2</b>\n</root>"),
423 "expected `<b>2</b>` to be followed directly by the root close tag, got:\n{out}"
424 );
425 }
426
427 #[test]
428 fn build_xml_string_comment_only_leaf() {
429 let el = json!({
433 "?xml": { "@version": "1.0" },
434 "root": { "#comment": " just a comment " }
435 });
436 let out = build_xml_string(&el);
437 assert!(out.contains("<!--"), "expected comment open in: {out}");
438 assert!(
439 out.contains(" just a comment "),
440 "expected comment text preserved verbatim in: {out}"
441 );
442 assert!(out.contains("-->"));
443 }
444
445 #[test]
446 fn build_xml_string_text_tail_only_leaf() {
447 let el = json!({
451 "?xml": { "@version": "1.0" },
452 "root": { "#text-tail": "tail-only-content" }
453 });
454 let out = build_xml_string(&el);
455 assert!(
456 out.contains("tail-only-content"),
457 "expected text-tail content rendered between root tags, got:\n{out}"
458 );
459 assert!(out.contains("<root>"));
460 assert!(out.contains("</root>"));
461 }
462
463 #[test]
464 fn build_xml_string_cdata_only_no_text_or_comment() {
465 let root = json!({ "#cdata": "only cdata content" });
466 let el = json!({ "?xml": { "@version": "1.0" }, "root": root });
467 let out = build_xml_string(&el);
468 assert!(out.contains("<![CDATA["));
469 assert!(out.contains("only cdata content"));
470 }
471
472 #[test]
473 fn build_xml_string_declaration_only_defaults_root_key() {
474 let el = json!({ "?xml": { "@version": "1.0", "@encoding": "UTF-8" } });
475 let out = build_xml_string(&el);
476 assert!(out.contains("<?xml"));
477 assert!(out.contains("<root>"));
478 }
479
480 #[test]
481 fn build_xml_string_declaration_non_object_skips_decl_write() {
482 let el = json!({ "?xml": "not-an-object", "root": { "a": "b" } });
483 let out = build_xml_string(&el);
484 assert!(!out.contains("<?xml"));
485 assert!(out.contains("<root>"));
486 }
487
488 #[test]
489 fn build_xml_string_root_value_array_sibling_elements() {
490 let el = json!({
492 "root": [ { "a": "1" }, { "b": "2" } ]
493 });
494 let out = build_xml_string(&el);
495 assert!(out.contains("<root>"));
496 assert!(out.contains("<a>1</a>"));
497 assert!(out.contains("<b>2</b>"));
498 assert!(out.contains("</root>"));
499 }
500
501 #[test]
502 fn build_xml_string_root_value_primitive() {
503 let el = json!({ "root": 42 });
505 let out = build_xml_string(&el);
506 assert!(out.contains("<root>42</root>"));
507 }
508
509 #[test]
510 fn build_xml_string_array_child_not_last_sibling_writes_inter_element_indent() {
511 let el = json!({
516 "root": {
517 "items": [{ "x": "1" }],
518 "sibling": "y"
519 }
520 });
521 let out = build_xml_string(&el);
522 assert!(
523 out.contains("<items>"),
524 "items element must be present: {out}"
525 );
526 assert!(
527 out.contains("<sibling>y</sibling>"),
528 "sibling must be present: {out}"
529 );
530 }
531
532 #[test]
533 fn build_xml_string_empty_array_child_produces_no_elements() {
534 let el = json!({ "root": { "items": [] } });
537 let out = build_xml_string(&el);
538 assert!(
540 !out.contains("<items>"),
541 "no elements for empty array: {out}"
542 );
543 assert!(
544 out.contains("<root>"),
545 "root element must be present: {out}"
546 );
547 }
548
549 #[test]
550 fn build_xml_string_attribute_value_array_uses_serde_fallback() {
551 let el = json!({
553 "root": { "@tags": ["a", "b"], "child": "v" }
554 });
555 let out = build_xml_string(&el);
556 assert!(
557 out.contains("child"),
558 "child element must be present: {out}"
559 );
560 }
561
562 #[test]
563 fn build_xml_string_attribute_value_object_uses_serde_fallback() {
564 let el = json!({
566 "root": { "@complex": { "nested": true }, "child": "v" }
567 });
568 let out = build_xml_string(&el);
569 assert!(out.contains("child"));
570 assert!(out.contains("v"));
571 }
572}