Skip to main content

xml_disassembler/handlers/
disassemble.rs

1//! Disassemble XML file handler.
2
3use crate::builders::build_disassembled_files_unified;
4use crate::types::BuildDisassembledFilesOptions;
5use ignore::gitignore::GitignoreBuilder;
6use std::path::Path;
7use tokio::fs;
8
9pub struct DisassembleXmlFileHandler {
10    ign: Option<ignore::gitignore::Gitignore>,
11}
12
13impl DisassembleXmlFileHandler {
14    pub fn new() -> Self {
15        Self { ign: None }
16    }
17
18    async fn load_ignore_rules(&mut self, ignore_path: &str) {
19        let path = Path::new(ignore_path);
20        if path.exists() {
21            if let Ok(content) = fs::read_to_string(path).await {
22                let root = path.parent().unwrap_or(Path::new("."));
23                let mut builder = GitignoreBuilder::new(root);
24                for line in content.lines() {
25                    let _ = builder.add_line(None, line);
26                }
27                if let Ok(gi) = builder.build() {
28                    self.ign = Some(gi);
29                }
30            }
31        }
32    }
33
34    fn posix_path(path: &str) -> String {
35        path.replace('\\', "/")
36    }
37
38    fn is_xml_file(file_path: &str) -> bool {
39        file_path.to_lowercase().ends_with(".xml")
40    }
41
42    fn is_ignored(&self, path: &str) -> bool {
43        self.ign
44            .as_ref()
45            .map(|ign| ign.matched(path, false).is_ignore())
46            .unwrap_or(false)
47    }
48
49    #[allow(clippy::too_many_arguments)]
50    pub async fn disassemble(
51        &mut self,
52        file_path: &str,
53        unique_id_elements: Option<&str>,
54        strategy: Option<&str>,
55        pre_purge: bool,
56        post_purge: bool,
57        ignore_path: &str,
58        format: &str,
59    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
60        let strategy = strategy.unwrap_or("unique-id");
61        let strategy = if ["unique-id", "grouped-by-tag"].contains(&strategy) {
62            strategy
63        } else {
64            log::warn!(
65                "Unsupported strategy \"{}\", defaulting to \"unique-id\".",
66                strategy
67            );
68            "unique-id"
69        };
70
71        self.load_ignore_rules(ignore_path).await;
72
73        let path = Path::new(file_path);
74        let meta = fs::metadata(path).await?;
75        let cwd = std::env::current_dir().unwrap_or_else(|_| Path::new(".").to_path_buf());
76        let relative_path = path.strip_prefix(&cwd).unwrap_or(path).to_string_lossy();
77        let relative_path = Self::posix_path(&relative_path);
78
79        if meta.is_file() {
80            self.handle_file(
81                file_path,
82                &relative_path,
83                unique_id_elements,
84                strategy,
85                pre_purge,
86                post_purge,
87                format,
88            )
89            .await?;
90        } else if meta.is_dir() {
91            self.handle_directory(
92                file_path,
93                unique_id_elements,
94                strategy,
95                pre_purge,
96                post_purge,
97                format,
98            )
99            .await?;
100        }
101
102        Ok(())
103    }
104
105    #[allow(clippy::too_many_arguments)]
106    async fn handle_file(
107        &self,
108        file_path: &str,
109        relative_path: &str,
110        unique_id_elements: Option<&str>,
111        strategy: &str,
112        pre_purge: bool,
113        post_purge: bool,
114        format: &str,
115    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
116        let resolved = Path::new(file_path)
117            .canonicalize()
118            .unwrap_or_else(|_| Path::new(file_path).to_path_buf());
119        let resolved_str = resolved.to_string_lossy();
120
121        if !Self::is_xml_file(&resolved_str) {
122            log::error!(
123                "The file path provided is not an XML file: {}",
124                resolved_str
125            );
126            return Ok(());
127        }
128
129        if self.is_ignored(relative_path) {
130            log::warn!("File ignored by ignore rules: {}", resolved_str);
131            return Ok(());
132        }
133
134        let dir_path = resolved.parent().unwrap_or(Path::new("."));
135        self.process_file(
136            dir_path.to_str().unwrap_or("."),
137            strategy,
138            &resolved_str,
139            unique_id_elements,
140            pre_purge,
141            post_purge,
142            format,
143        )
144        .await
145    }
146
147    async fn handle_directory(
148        &self,
149        dir_path: &str,
150        unique_id_elements: Option<&str>,
151        strategy: &str,
152        pre_purge: bool,
153        post_purge: bool,
154        format: &str,
155    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
156        let mut entries = fs::read_dir(dir_path).await?;
157        let cwd = std::env::current_dir().unwrap_or_else(|_| Path::new(".").to_path_buf());
158
159        while let Some(entry) = entries.next_entry().await? {
160            let sub_path = entry.path();
161            let sub_file_path = sub_path.to_string_lossy();
162            let relative_sub = sub_path
163                .strip_prefix(&cwd)
164                .unwrap_or(&sub_path)
165                .to_string_lossy();
166            let relative_sub = Self::posix_path(&relative_sub);
167
168            if sub_path.is_file() && Self::is_xml_file(&sub_file_path) {
169                if self.is_ignored(&relative_sub) {
170                    log::warn!("File ignored by ignore rules: {}", sub_file_path);
171                } else {
172                    self.process_file(
173                        dir_path,
174                        strategy,
175                        &sub_file_path,
176                        unique_id_elements,
177                        pre_purge,
178                        post_purge,
179                        format,
180                    )
181                    .await?;
182                }
183            }
184        }
185        Ok(())
186    }
187
188    #[allow(clippy::too_many_arguments)]
189    async fn process_file(
190        &self,
191        dir_path: &str,
192        strategy: &str,
193        file_path: &str,
194        unique_id_elements: Option<&str>,
195        pre_purge: bool,
196        post_purge: bool,
197        format: &str,
198    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
199        log::debug!("Parsing file to disassemble: {}", file_path);
200
201        let file_name = Path::new(file_path)
202            .file_stem()
203            .and_then(|s| s.to_str())
204            .unwrap_or("output");
205        let base_name = file_name.split('.').next().unwrap_or(file_name);
206        let output_path = Path::new(dir_path).join(base_name);
207
208        if pre_purge && output_path.exists() {
209            fs::remove_dir_all(&output_path).await.ok();
210        }
211
212        build_disassembled_files_unified(BuildDisassembledFilesOptions {
213            file_path,
214            disassembled_path: output_path.to_str().unwrap_or("."),
215            base_name: file_name,
216            post_purge,
217            format,
218            unique_id_elements,
219            strategy,
220        })
221        .await
222    }
223}
224
225impl Default for DisassembleXmlFileHandler {
226    fn default() -> Self {
227        Self::new()
228    }
229}