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 empty_root = Value::Object(Map::new());
247 let (declaration, root_key, root_value) = if let Some(decl) = element.get("?xml") {
248 let root_key = element
249 .keys()
250 .find(|k| *k != "?xml")
251 .cloned()
252 .unwrap_or_else(|| "root".to_string());
253 let root_value = element.get(&root_key).unwrap_or(&empty_root);
254 (Some(decl), root_key, root_value)
255 } else {
256 let root_key = element
257 .keys()
258 .next()
259 .cloned()
260 .unwrap_or_else(|| "root".to_string());
261 let root_value = element.get(&root_key).unwrap_or(&empty_root);
262 (None, root_key, root_value)
263 };
264
265 if let Some(obj) = declaration.and_then(|d| d.as_object()) {
266 let version = obj
267 .get("@version")
268 .and_then(|v| v.as_str())
269 .unwrap_or("1.0");
270 let encoding = obj.get("@encoding").and_then(|v| v.as_str());
271 let standalone = obj.get("@standalone").and_then(|v| v.as_str());
272 writer.write_event(Event::Decl(BytesDecl::new(version, encoding, standalone)))?;
273 writer.write_event(Event::Text(BytesText::new("\n")))?;
274 }
275
276 write_element(&mut writer, &root_key, root_value, 0)?;
277
278 let mut result = String::from_utf8(writer.into_inner())?;
281 let trimmed_len = result.trim_end().len();
282 result.truncate(trimmed_len);
283 Ok(result)
284}
285
286pub fn build_xml_string(element: &XmlElement) -> String {
288 match element {
289 Value::Object(obj) => build_xml_from_object(obj).unwrap_or_default(),
290 _ => String::new(),
291 }
292}
293
294#[cfg(test)]
295mod tests {
296 use super::*;
297 use serde_json::json;
298
299 #[test]
300 fn build_xml_string_non_object_returns_empty() {
301 assert!(build_xml_string(&Value::Array(vec![])).is_empty());
302 assert!(build_xml_string(&Value::Null).is_empty());
303 }
304
305 #[test]
306 fn build_xml_string_simple_root() {
307 let el = json!({
308 "?xml": { "@version": "1.0", "@encoding": "UTF-8" },
309 "root": { "child": "value" }
310 });
311 let out = build_xml_string(&el);
312 assert!(out.contains("<?xml"));
313 assert!(out.contains("<root>"));
314 assert!(out.contains("<child>value</child>"));
315 assert!(out.contains("</root>"));
316 }
317
318 #[test]
319 fn build_xml_string_with_attributes() {
320 let el = json!({
321 "root": { "@xmlns": "http://example.com", "a": "b" }
322 });
323 let out = build_xml_string(&el);
324 assert!(out.contains("xmlns"));
325 assert!(out.contains("http://example.com"));
326 assert!(out.contains("<a>b</a>"));
327 }
328
329 #[test]
330 fn build_xml_string_with_array() {
331 let el = json!({
332 "root": { "item": [ { "x": "1" }, { "x": "2" } ] }
333 });
334 let out = build_xml_string(&el);
335 assert!(out.contains("<item>"));
336 assert!(out.contains("<x>1</x>"));
337 assert!(out.contains("<x>2</x>"));
338 }
339
340 #[test]
341 fn build_xml_string_without_declaration() {
342 let el = json!({ "root": { "a": "b" } });
343 let out = build_xml_string(&el);
344 assert!(!out.contains("<?xml"));
345 assert!(out.contains("<root>"));
346 }
347
348 #[test]
349 fn build_xml_string_with_text_comment_cdata() {
350 let root = json!({
351 "#text": "text",
352 "#comment": " a comment ",
353 "#cdata": "<cdata>"
354 });
355 let el = json!({
356 "?xml": { "@version": "1.0" },
357 "root": root
358 });
359 let out = build_xml_string(&el);
360 assert!(out.contains("text"));
361 assert!(out.contains("<!--"));
362 assert!(out.contains(" a comment "));
363 assert!(out.contains("<![CDATA["));
364 assert!(out.contains("<cdata>"));
365 }
366
367 #[test]
368 fn build_xml_string_with_declaration_encoding_standalone() {
369 let el = json!({
370 "?xml": { "@version": "1.0", "@encoding": "UTF-8", "@standalone": "yes" },
371 "root": { "a": "b" }
372 });
373 let out = build_xml_string(&el);
374 assert!(out.contains("<?xml"));
375 assert!(out.contains("UTF-8"));
376 assert!(out.contains("standalone"));
377 assert!(out.contains("<root>"));
378 }
379
380 #[test]
381 fn build_xml_string_primitive_sibling_children() {
382 let el = json!({
384 "root": { "obj": { "x": "1" }, "num": 42, "flag": true }
385 });
386 let out = build_xml_string(&el);
387 assert!(out.contains("<obj>"));
388 assert!(out.contains("<num>42</num>"));
389 assert!(out.contains("<flag>true</flag>"));
390 }
391
392 #[test]
393 fn build_xml_string_null_child_value() {
394 let el = json!({
395 "root": { "empty": null }
396 });
397 let out = build_xml_string(&el);
398 assert!(out.contains("<empty>"));
399 assert!(out.contains("</empty>"));
400 assert!(
404 !out.contains("null"),
405 "Value::Null child should render as empty content, not the string \"null\": {out}"
406 );
407 assert!(out.contains("<empty></empty>"));
408 }
409
410 #[test]
411 fn build_xml_string_primitive_siblings_have_inter_element_indent() {
412 let el = json!({ "root": { "a": 1, "b": 2 } });
417 let out = build_xml_string(&el);
418 assert!(
419 out.contains("<a>1</a>\n <b>2</b>"),
420 "expected `<a>1</a>` to be followed by newline + 4-space indent then `<b>2</b>`, got:\n{out}"
421 );
422 assert!(
425 out.contains("<b>2</b>\n</root>"),
426 "expected `<b>2</b>` to be followed directly by the root close tag, got:\n{out}"
427 );
428 }
429
430 #[test]
431 fn build_xml_string_comment_only_leaf() {
432 let el = json!({
436 "?xml": { "@version": "1.0" },
437 "root": { "#comment": " just a comment " }
438 });
439 let out = build_xml_string(&el);
440 assert!(out.contains("<!--"), "expected comment open in: {out}");
441 assert!(
442 out.contains(" just a comment "),
443 "expected comment text preserved verbatim in: {out}"
444 );
445 assert!(out.contains("-->"));
446 }
447
448 #[test]
449 fn build_xml_string_text_tail_only_leaf() {
450 let el = json!({
454 "?xml": { "@version": "1.0" },
455 "root": { "#text-tail": "tail-only-content" }
456 });
457 let out = build_xml_string(&el);
458 assert!(
459 out.contains("tail-only-content"),
460 "expected text-tail content rendered between root tags, got:\n{out}"
461 );
462 assert!(out.contains("<root>"));
463 assert!(out.contains("</root>"));
464 }
465
466 #[test]
467 fn build_xml_string_cdata_only_no_text_or_comment() {
468 let root = json!({ "#cdata": "only cdata content" });
469 let el = json!({ "?xml": { "@version": "1.0" }, "root": root });
470 let out = build_xml_string(&el);
471 assert!(out.contains("<![CDATA["));
472 assert!(out.contains("only cdata content"));
473 }
474
475 #[test]
476 fn build_xml_string_declaration_only_defaults_root_key() {
477 let el = json!({ "?xml": { "@version": "1.0", "@encoding": "UTF-8" } });
478 let out = build_xml_string(&el);
479 assert!(out.contains("<?xml"));
480 assert!(out.contains("<root>"));
481 }
482
483 #[test]
484 fn build_xml_string_declaration_non_object_skips_decl_write() {
485 let el = json!({ "?xml": "not-an-object", "root": { "a": "b" } });
486 let out = build_xml_string(&el);
487 assert!(!out.contains("<?xml"));
488 assert!(out.contains("<root>"));
489 }
490
491 #[test]
492 fn build_xml_string_root_value_array_sibling_elements() {
493 let el = json!({
495 "root": [ { "a": "1" }, { "b": "2" } ]
496 });
497 let out = build_xml_string(&el);
498 assert!(out.contains("<root>"));
499 assert!(out.contains("<a>1</a>"));
500 assert!(out.contains("<b>2</b>"));
501 assert!(out.contains("</root>"));
502 }
503
504 #[test]
505 fn build_xml_string_root_value_primitive() {
506 let el = json!({ "root": 42 });
508 let out = build_xml_string(&el);
509 assert!(out.contains("<root>42</root>"));
510 }
511
512 #[test]
513 fn build_xml_string_array_child_not_last_sibling_writes_inter_element_indent() {
514 let el = json!({
519 "root": {
520 "items": [{ "x": "1" }],
521 "sibling": "y"
522 }
523 });
524 let out = build_xml_string(&el);
525 assert!(
526 out.contains("<items>"),
527 "items element must be present: {out}"
528 );
529 assert!(
530 out.contains("<sibling>y</sibling>"),
531 "sibling must be present: {out}"
532 );
533 }
534
535 #[test]
536 fn build_xml_string_empty_array_child_produces_no_elements() {
537 let el = json!({ "root": { "items": [] } });
540 let out = build_xml_string(&el);
541 assert!(
543 !out.contains("<items>"),
544 "no elements for empty array: {out}"
545 );
546 assert!(
547 out.contains("<root>"),
548 "root element must be present: {out}"
549 );
550 }
551
552 #[test]
553 fn build_xml_string_attribute_value_array_uses_serde_fallback() {
554 let el = json!({
556 "root": { "@tags": ["a", "b"], "child": "v" }
557 });
558 let out = build_xml_string(&el);
559 assert!(
560 out.contains("child"),
561 "child element must be present: {out}"
562 );
563 }
564
565 #[test]
566 fn build_xml_string_attribute_value_object_uses_serde_fallback() {
567 let el = json!({
569 "root": { "@complex": { "nested": true }, "child": "v" }
570 });
571 let out = build_xml_string(&el);
572 assert!(out.contains("child"));
573 assert!(out.contains("v"));
574 }
575}