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 child_elements: Vec<(&String, &Value)> = children
42 .iter()
43 .filter_map(|(k, v)| {
44 if *k == "#text" {
45 text_content = value_to_string(v);
46 None
47 } else if *k == "#raw-text" {
48 raw_text_content = value_to_string(v);
49 None
50 } else if *k == "#comment" {
51 comment_content = value_to_string(v);
52 None
53 } else if *k == "#text-tail" {
54 text_tail_content = value_to_string(v);
55 None
56 } else if *k == "#cdata" {
57 cdata_content = value_to_string(v);
58 None
59 } else {
60 Some((*k, *v))
61 }
62 })
63 .collect();
64
65 let attrs: Vec<(String, String)> = attrs
66 .iter()
67 .map(|(k, v)| (attr_name(k), value_to_string(v)))
68 .collect();
69
70 let mut start = BytesStart::new(name);
71 for (k, v) in &attrs {
72 start.push_attribute((k.as_str(), v.as_str()));
73 }
74 writer.write_event(Event::Start(start))?;
75
76 if !child_elements.is_empty() {
77 writer.write_event(Event::Text(BytesText::new(
78 format!("\n{}", child_indent).as_str(),
79 )))?;
80
81 let child_count = child_elements.len();
82 for (idx, (child_name, child_value)) in child_elements.iter().enumerate() {
83 let is_last = idx == child_count - 1;
84 match child_value {
85 Value::Array(arr) => {
86 let arr_len = arr.len();
87 for (i, item) in arr.iter().enumerate() {
88 let arr_last = i == arr_len - 1;
89 write_element(writer, child_name, item, indent_level + 1)?;
90 if !arr_last {
91 writer.write_event(Event::Text(BytesText::new(
92 format!("\n{}", child_indent).as_str(),
93 )))?;
94 }
95 }
96 if !is_last {
97 writer.write_event(Event::Text(BytesText::new(
98 format!("\n{}", child_indent).as_str(),
99 )))?;
100 }
101 }
102 Value::Object(_) => {
103 write_element(writer, child_name, child_value, indent_level + 1)?;
104 if !is_last {
105 writer.write_event(Event::Text(BytesText::new(
106 format!("\n{}", child_indent).as_str(),
107 )))?;
108 }
109 }
110 _ => {
111 writer
112 .write_event(Event::Start(BytesStart::new(child_name.as_str())))?;
113 writer.write_event(Event::Text(BytesText::new(
114 value_to_string(child_value).as_str(),
115 )))?;
116 writer.write_event(Event::End(BytesEnd::new(child_name.as_str())))?;
117 if !is_last {
118 writer.write_event(Event::Text(BytesText::new(
119 format!("\n{}", child_indent).as_str(),
120 )))?;
121 }
122 }
123 }
124 }
125
126 writer.write_event(Event::Text(BytesText::new(
127 format!("\n{}", indent).as_str(),
128 )))?;
129 } else if !cdata_content.is_empty()
130 || !text_content.is_empty()
131 || !raw_text_content.is_empty()
132 || !comment_content.is_empty()
133 || !text_tail_content.is_empty()
134 {
135 if text_content.is_empty()
137 && raw_text_content.is_empty()
138 && comment_content.is_empty()
139 {
140 writer.write_event(Event::Text(BytesText::new(
141 format!("\n{}", child_indent).as_str(),
142 )))?;
143 }
144 if !text_content.is_empty() {
146 writer.write_event(Event::Text(BytesText::new(text_content.as_str())))?;
147 }
148 if !raw_text_content.is_empty() {
151 writer.write_event(Event::Text(BytesText::from_escaped(partial_escape(
152 raw_text_content.as_str(),
153 ))))?;
154 }
155 if !comment_content.is_empty() {
156 writer.write_event(Event::Comment(BytesText::new(comment_content.as_str())))?;
157 }
158 if !text_tail_content.is_empty() {
159 writer.write_event(Event::Text(BytesText::new(text_tail_content.as_str())))?;
160 }
161 if !cdata_content.is_empty() {
162 writer.write_event(Event::CData(BytesCData::new(cdata_content.as_str())))?;
163 }
164 if !cdata_content.is_empty() {
166 writer.write_event(Event::Text(BytesText::new(
167 format!("\n{}", indent).as_str(),
168 )))?;
169 }
170 }
171
172 writer.write_event(Event::End(BytesEnd::new(name)))?;
173 }
174 Value::Array(arr) => {
175 for item in arr {
176 write_element(writer, name, item, indent_level)?;
177 }
178 }
179 _ => {
180 writer.write_event(Event::Start(BytesStart::new(name)))?;
181 writer.write_event(Event::Text(BytesText::new(
182 value_to_string(content).as_str(),
183 )))?;
184 writer.write_event(Event::End(BytesEnd::new(name)))?;
185 }
186 }
187
188 Ok(())
189}
190
191fn build_xml_from_object(
192 element: &Map<String, Value>,
193) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
194 let mut writer = Writer::new(Vec::new());
196
197 let (declaration, root_key, root_value) = if let Some(decl) = element.get("?xml") {
198 let root_key = element
199 .keys()
200 .find(|k| *k != "?xml")
201 .cloned()
202 .unwrap_or_else(|| "root".to_string());
203 let root_value = element
204 .get(&root_key)
205 .cloned()
206 .unwrap_or_else(|| Value::Object(Map::new()));
207 (Some(decl), root_key, root_value)
208 } else {
209 let root_key = element
210 .keys()
211 .next()
212 .cloned()
213 .unwrap_or_else(|| "root".to_string());
214 let root_value = element
215 .get(&root_key)
216 .cloned()
217 .unwrap_or_else(|| Value::Object(Map::new()));
218 (None, root_key, root_value)
219 };
220
221 if let Some(obj) = declaration.and_then(|d| d.as_object()) {
222 let version = obj
223 .get("@version")
224 .and_then(|v| v.as_str())
225 .unwrap_or("1.0");
226 let encoding = obj.get("@encoding").and_then(|v| v.as_str());
227 let standalone = obj.get("@standalone").and_then(|v| v.as_str());
228 writer.write_event(Event::Decl(BytesDecl::new(version, encoding, standalone)))?;
229 writer.write_event(Event::Text(BytesText::new("\n")))?;
230 }
231
232 write_element(&mut writer, &root_key, &root_value, 0)?;
233
234 let result = String::from_utf8(writer.into_inner())?;
235 Ok(result.trim_end().to_string())
236}
237
238pub fn build_xml_string(element: &XmlElement) -> String {
240 match element {
241 Value::Object(obj) => build_xml_from_object(obj).unwrap_or_default(),
242 _ => String::new(),
243 }
244}
245
246#[cfg(test)]
247mod tests {
248 use super::*;
249 use serde_json::json;
250
251 #[test]
252 fn build_xml_string_non_object_returns_empty() {
253 assert!(build_xml_string(&Value::Array(vec![])).is_empty());
254 assert!(build_xml_string(&Value::Null).is_empty());
255 }
256
257 #[test]
258 fn build_xml_string_simple_root() {
259 let el = json!({
260 "?xml": { "@version": "1.0", "@encoding": "UTF-8" },
261 "root": { "child": "value" }
262 });
263 let out = build_xml_string(&el);
264 assert!(out.contains("<?xml"));
265 assert!(out.contains("<root>"));
266 assert!(out.contains("<child>value</child>"));
267 assert!(out.contains("</root>"));
268 }
269
270 #[test]
271 fn build_xml_string_with_attributes() {
272 let el = json!({
273 "root": { "@xmlns": "http://example.com", "a": "b" }
274 });
275 let out = build_xml_string(&el);
276 assert!(out.contains("xmlns"));
277 assert!(out.contains("http://example.com"));
278 assert!(out.contains("<a>b</a>"));
279 }
280
281 #[test]
282 fn build_xml_string_with_array() {
283 let el = json!({
284 "root": { "item": [ { "x": "1" }, { "x": "2" } ] }
285 });
286 let out = build_xml_string(&el);
287 assert!(out.contains("<item>"));
288 assert!(out.contains("<x>1</x>"));
289 assert!(out.contains("<x>2</x>"));
290 }
291
292 #[test]
293 fn build_xml_string_without_declaration() {
294 let el = json!({ "root": { "a": "b" } });
295 let out = build_xml_string(&el);
296 assert!(!out.contains("<?xml"));
297 assert!(out.contains("<root>"));
298 }
299
300 #[test]
301 fn build_xml_string_with_text_comment_cdata() {
302 let root = json!({
303 "#text": "text",
304 "#comment": " a comment ",
305 "#cdata": "<cdata>"
306 });
307 let el = json!({
308 "?xml": { "@version": "1.0" },
309 "root": root
310 });
311 let out = build_xml_string(&el);
312 assert!(out.contains("text"));
313 assert!(out.contains("<!--"));
314 assert!(out.contains(" a comment "));
315 assert!(out.contains("<![CDATA["));
316 assert!(out.contains("<cdata>"));
317 }
318
319 #[test]
320 fn build_xml_string_with_declaration_encoding_standalone() {
321 let el = json!({
322 "?xml": { "@version": "1.0", "@encoding": "UTF-8", "@standalone": "yes" },
323 "root": { "a": "b" }
324 });
325 let out = build_xml_string(&el);
326 assert!(out.contains("<?xml"));
327 assert!(out.contains("UTF-8"));
328 assert!(out.contains("standalone"));
329 assert!(out.contains("<root>"));
330 }
331
332 #[test]
333 fn build_xml_string_primitive_sibling_children() {
334 let el = json!({
336 "root": { "obj": { "x": "1" }, "num": 42, "flag": true }
337 });
338 let out = build_xml_string(&el);
339 assert!(out.contains("<obj>"));
340 assert!(out.contains("<num>42</num>"));
341 assert!(out.contains("<flag>true</flag>"));
342 }
343
344 #[test]
345 fn build_xml_string_null_child_value() {
346 let el = json!({
347 "root": { "empty": null }
348 });
349 let out = build_xml_string(&el);
350 assert!(out.contains("<empty>"));
351 assert!(out.contains("</empty>"));
352 assert!(
356 !out.contains("null"),
357 "Value::Null child should render as empty content, not the string \"null\": {out}"
358 );
359 assert!(out.contains("<empty></empty>"));
360 }
361
362 #[test]
363 fn build_xml_string_primitive_siblings_have_inter_element_indent() {
364 let el = json!({ "root": { "a": 1, "b": 2 } });
369 let out = build_xml_string(&el);
370 assert!(
371 out.contains("<a>1</a>\n <b>2</b>"),
372 "expected `<a>1</a>` to be followed by newline + 4-space indent then `<b>2</b>`, got:\n{out}"
373 );
374 assert!(
377 out.contains("<b>2</b>\n</root>"),
378 "expected `<b>2</b>` to be followed directly by the root close tag, got:\n{out}"
379 );
380 }
381
382 #[test]
383 fn build_xml_string_comment_only_leaf() {
384 let el = json!({
388 "?xml": { "@version": "1.0" },
389 "root": { "#comment": " just a comment " }
390 });
391 let out = build_xml_string(&el);
392 assert!(out.contains("<!--"), "expected comment open in: {out}");
393 assert!(
394 out.contains(" just a comment "),
395 "expected comment text preserved verbatim in: {out}"
396 );
397 assert!(out.contains("-->"));
398 }
399
400 #[test]
401 fn build_xml_string_text_tail_only_leaf() {
402 let el = json!({
406 "?xml": { "@version": "1.0" },
407 "root": { "#text-tail": "tail-only-content" }
408 });
409 let out = build_xml_string(&el);
410 assert!(
411 out.contains("tail-only-content"),
412 "expected text-tail content rendered between root tags, got:\n{out}"
413 );
414 assert!(out.contains("<root>"));
415 assert!(out.contains("</root>"));
416 }
417
418 #[test]
419 fn build_xml_string_cdata_only_no_text_or_comment() {
420 let root = json!({ "#cdata": "only cdata content" });
421 let el = json!({ "?xml": { "@version": "1.0" }, "root": root });
422 let out = build_xml_string(&el);
423 assert!(out.contains("<![CDATA["));
424 assert!(out.contains("only cdata content"));
425 }
426
427 #[test]
428 fn build_xml_string_declaration_only_defaults_root_key() {
429 let el = json!({ "?xml": { "@version": "1.0", "@encoding": "UTF-8" } });
430 let out = build_xml_string(&el);
431 assert!(out.contains("<?xml"));
432 assert!(out.contains("<root>"));
433 }
434
435 #[test]
436 fn build_xml_string_declaration_non_object_skips_decl_write() {
437 let el = json!({ "?xml": "not-an-object", "root": { "a": "b" } });
438 let out = build_xml_string(&el);
439 assert!(!out.contains("<?xml"));
440 assert!(out.contains("<root>"));
441 }
442
443 #[test]
444 fn build_xml_string_root_value_array_sibling_elements() {
445 let el = json!({
447 "root": [ { "a": "1" }, { "b": "2" } ]
448 });
449 let out = build_xml_string(&el);
450 assert!(out.contains("<root>"));
451 assert!(out.contains("<a>1</a>"));
452 assert!(out.contains("<b>2</b>"));
453 assert!(out.contains("</root>"));
454 }
455
456 #[test]
457 fn build_xml_string_root_value_primitive() {
458 let el = json!({ "root": 42 });
460 let out = build_xml_string(&el);
461 assert!(out.contains("<root>42</root>"));
462 }
463
464 #[test]
465 fn build_xml_string_array_child_not_last_sibling_writes_inter_element_indent() {
466 let el = json!({
471 "root": {
472 "items": [{ "x": "1" }],
473 "sibling": "y"
474 }
475 });
476 let out = build_xml_string(&el);
477 assert!(
478 out.contains("<items>"),
479 "items element must be present: {out}"
480 );
481 assert!(
482 out.contains("<sibling>y</sibling>"),
483 "sibling must be present: {out}"
484 );
485 }
486
487 #[test]
488 fn build_xml_string_empty_array_child_produces_no_elements() {
489 let el = json!({ "root": { "items": [] } });
492 let out = build_xml_string(&el);
493 assert!(
495 !out.contains("<items>"),
496 "no elements for empty array: {out}"
497 );
498 assert!(
499 out.contains("<root>"),
500 "root element must be present: {out}"
501 );
502 }
503
504 #[test]
505 fn build_xml_string_attribute_value_array_uses_serde_fallback() {
506 let el = json!({
508 "root": { "@tags": ["a", "b"], "child": "v" }
509 });
510 let out = build_xml_string(&el);
511 assert!(
512 out.contains("child"),
513 "child element must be present: {out}"
514 );
515 }
516
517 #[test]
518 fn build_xml_string_attribute_value_object_uses_serde_fallback() {
519 let el = json!({
521 "root": { "@complex": { "nested": true }, "child": "v" }
522 });
523 let out = build_xml_string(&el);
524 assert!(out.contains("child"));
525 assert!(out.contains("v"));
526 }
527}