config_disassembler/xml/parsers/
parse_xml.rs1use std::sync::LazyLock;
4
5use regex::Regex;
6use serde_json::Value;
7use tokio::fs;
8
9use crate::xml::parsers::parse_xml_cdata;
10use crate::xml::parsers::{mark_compact_elements, strip_whitespace_text_nodes_in_place};
11use crate::xml::types::XmlElement;
12
13pub async fn parse_xml(file_path: &str) -> Option<XmlElement> {
15 let content = match fs::read_to_string(file_path).await {
16 Ok(c) => c,
17 Err(e) => {
18 log::error!(
19 "{} was unable to be parsed and will not be processed. Confirm formatting and try again.",
20 file_path
21 );
22 log::debug!("Parse error: {}", e);
23 return None;
24 }
25 };
26 parse_xml_from_str(&content, file_path)
27}
28
29pub fn parse_xml_from_str(content: &str, file_path: &str) -> Option<XmlElement> {
32 let mut parsed: Value = match parse_xml_cdata::parse_xml_with_cdata(content) {
33 Ok(v) => v,
34 Err(e) => {
35 log::error!(
36 "{} was unable to be parsed and will not be processed. Confirm formatting and try again.",
37 file_path
38 );
39 log::debug!("Parse error: {}", e);
40 return None;
41 }
42 };
43
44 let root_children = parsed
54 .as_object_mut()
55 .expect("parse_xml_with_cdata always returns an object");
56 for value in root_children.values_mut() {
57 mark_compact_elements(value);
58 }
59
60 strip_whitespace_text_nodes_in_place(&mut parsed);
61 Some(parsed)
62}
63
64static XMLNS_RE: LazyLock<Regex> =
70 LazyLock::new(|| Regex::new(r#"xmlns="([^"]*)""#).expect("valid regex"));
71static DECLARATION_RE: LazyLock<Regex> =
72 LazyLock::new(|| Regex::new(r#"<\?xml\s+([^?]+)\?>"#).expect("valid regex"));
73static VERSION_RE: LazyLock<Regex> =
74 LazyLock::new(|| Regex::new(r#"version="([^"]*)""#).expect("valid regex"));
75static ENCODING_RE: LazyLock<Regex> =
76 LazyLock::new(|| Regex::new(r#"encoding="([^"]*)""#).expect("valid regex"));
77static STANDALONE_RE: LazyLock<Regex> =
78 LazyLock::new(|| Regex::new(r#"standalone="([^"]*)""#).expect("valid regex"));
79
80pub fn extract_xmlns_from_raw(xml_content: &str) -> Option<String> {
83 XMLNS_RE.captures(xml_content).map(|c| c[1].to_string())
84}
85
86pub fn extract_xml_declaration_from_raw(xml_content: &str) -> Option<XmlElement> {
90 let decl_content = DECLARATION_RE.captures(xml_content)?.get(1)?.as_str();
91 let mut decl = serde_json::Map::new();
92 {
93 let cap = VERSION_RE.captures(decl_content)?;
94 decl.insert("@version".to_string(), Value::String(cap[1].to_string()));
95 }
96 if let Some(cap) = ENCODING_RE.captures(decl_content) {
97 decl.insert("@encoding".to_string(), Value::String(cap[1].to_string()));
98 }
99 if let Some(cap) = STANDALONE_RE.captures(decl_content) {
100 decl.insert("@standalone".to_string(), Value::String(cap[1].to_string()));
101 }
102 Some(Value::Object(decl))
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108
109 #[test]
110 fn extract_xmlns_from_raw_finds_namespace() {
111 let xml = r#"<root xmlns="http://soap.sforce.com/2006/04/metadata"><a/></root>"#;
112 assert_eq!(
113 extract_xmlns_from_raw(xml),
114 Some("http://soap.sforce.com/2006/04/metadata".to_string())
115 );
116 }
117
118 #[test]
119 fn extract_xmlns_from_raw_returns_none_when_absent() {
120 let xml = r#"<root><a/></root>"#;
121 assert_eq!(extract_xmlns_from_raw(xml), None);
122 }
123
124 #[test]
125 fn extract_xml_declaration_from_raw_parses_version_and_encoding() {
126 let xml = r#"<?xml version="1.0" encoding="UTF-8"?><root/>"#;
127 let decl = extract_xml_declaration_from_raw(xml).unwrap();
128 let obj = decl.as_object().unwrap();
129 assert_eq!(obj.get("@version").and_then(|v| v.as_str()), Some("1.0"));
130 assert_eq!(obj.get("@encoding").and_then(|v| v.as_str()), Some("UTF-8"));
131 }
132
133 #[test]
134 fn extract_xml_declaration_from_raw_parses_standalone() {
135 let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?><root/>"#;
136 let decl = extract_xml_declaration_from_raw(xml).unwrap();
137 let obj = decl.as_object().unwrap();
138 assert_eq!(obj.get("@standalone").and_then(|v| v.as_str()), Some("yes"));
139 }
140
141 #[test]
142 fn extract_xml_declaration_from_raw_returns_none_without_declaration() {
143 let xml = r#"<root/>"#;
144 assert!(extract_xml_declaration_from_raw(xml).is_none());
145 }
146
147 #[test]
148 fn extract_xml_declaration_from_raw_returns_none_when_version_missing() {
149 let xml = r#"<?xml encoding="UTF-8"?><root/>"#;
150 assert!(extract_xml_declaration_from_raw(xml).is_none());
151 }
152
153 #[test]
154 fn extract_xml_declaration_from_raw_version_only_no_encoding_no_standalone() {
155 let xml = r#"<?xml version="1.0"?><root/>"#;
158 let decl = extract_xml_declaration_from_raw(xml).unwrap();
159 let obj = decl.as_object().unwrap();
160 assert_eq!(obj.get("@version").and_then(|v| v.as_str()), Some("1.0"));
161 assert!(
162 obj.get("@encoding").is_none(),
163 "encoding must be absent when not in source"
164 );
165 assert!(
166 obj.get("@standalone").is_none(),
167 "standalone must be absent when not in source"
168 );
169 }
170
171 #[test]
172 fn parse_xml_from_str_invalid_xml_returns_none() {
173 let result = parse_xml_from_str("<<", "test.xml");
174 assert!(result.is_none());
175 }
176
177 #[tokio::test]
178 async fn parse_xml_missing_file_returns_none() {
179 let result = parse_xml("/nonexistent/path/file.xml").await;
180 assert!(result.is_none());
181 }
182}