msg_tool\utils/
files.rs

1//! Utilities for File Operations
2use crate::scripts::{ALL_EXTS, ARCHIVE_EXTS};
3use std::fs;
4use std::io;
5use std::io::{Read, Write};
6use std::path::{Path, PathBuf};
7
8/// Returns the relative path from `root` to `target`.
9pub fn relative_path<P: AsRef<Path>, T: AsRef<Path>>(root: P, target: T) -> PathBuf {
10    let root = root
11        .as_ref()
12        .canonicalize()
13        .unwrap_or_else(|_| root.as_ref().to_path_buf());
14    let target = target
15        .as_ref()
16        .canonicalize()
17        .unwrap_or_else(|_| target.as_ref().to_path_buf());
18
19    let mut root_components: Vec<_> = root.components().collect();
20    let mut target_components: Vec<_> = target.components().collect();
21
22    // Remove common prefix
23    while !root_components.is_empty()
24        && !target_components.is_empty()
25        && root_components[0] == target_components[0]
26    {
27        root_components.remove(0);
28        target_components.remove(0);
29    }
30
31    // Add ".." for each remaining root component
32    let mut result = PathBuf::new();
33    for _ in root_components {
34        result.push("..");
35    }
36
37    // Add remaining target components
38    for component in target_components {
39        result.push(component);
40    }
41
42    result
43}
44
45/// Finds all files in the specified directory and its subdirectories.
46pub fn find_files(path: &str, recursive: bool, no_ext_filter: bool) -> io::Result<Vec<String>> {
47    let mut result = Vec::new();
48    let dir_path = Path::new(&path);
49
50    if dir_path.is_dir() {
51        for entry in fs::read_dir(dir_path)? {
52            let entry = entry?;
53            let path = entry.path();
54
55            if path.is_file()
56                && (no_ext_filter
57                    || path.file_name().map_or(false, |file| {
58                        path.extension().map_or(true, |_| {
59                            let file = file.to_string_lossy().to_lowercase();
60                            for ext in ALL_EXTS.iter() {
61                                if file.ends_with(&format!(".{}", ext)) {
62                                    return true;
63                                }
64                            }
65                            false
66                        })
67                    }))
68            {
69                if let Some(path_str) = path.to_str() {
70                    result.push(path_str.to_string());
71                }
72            } else if recursive && path.is_dir() {
73                if let Some(path_str) = path.to_str() {
74                    let mut sub_files =
75                        find_files(&path_str.to_string(), recursive, no_ext_filter)?;
76                    result.append(&mut sub_files);
77                }
78            }
79        }
80    }
81
82    Ok(result)
83}
84
85/// Finds all archive files in the specified directory and its subdirectories.
86pub fn find_arc_files(path: &str, recursive: bool) -> io::Result<Vec<String>> {
87    let mut result = Vec::new();
88    let dir_path = Path::new(&path);
89
90    if dir_path.is_dir() {
91        for entry in fs::read_dir(dir_path)? {
92            let entry = entry?;
93            let path = entry.path();
94
95            if path.is_file()
96                && path.file_name().map_or(false, |file| {
97                    path.extension().map_or(true, |_| {
98                        let file = file.to_string_lossy().to_lowercase();
99                        for ext in ARCHIVE_EXTS.iter() {
100                            if file.ends_with(&format!(".{}", ext)) {
101                                return true;
102                            }
103                        }
104                        false
105                    })
106                })
107            {
108                if let Some(path_str) = path.to_str() {
109                    result.push(path_str.to_string());
110                }
111            } else if recursive && path.is_dir() {
112                if let Some(path_str) = path.to_str() {
113                    let mut sub_files = find_arc_files(&path_str.to_string(), recursive)?;
114                    result.append(&mut sub_files);
115                }
116            }
117        }
118    }
119
120    Ok(result)
121}
122
123/// Collects files from the specified path, either as a directory or a single file.
124pub fn collect_files(
125    path: &str,
126    recursive: bool,
127    no_ext_filter: bool,
128) -> io::Result<(Vec<String>, bool)> {
129    let pa = Path::new(path);
130    if pa.is_dir() {
131        return Ok((find_files(path, recursive, no_ext_filter)?, true));
132    }
133    if pa.is_file() {
134        return Ok((vec![path.to_string()], false));
135    }
136    Err(io::Error::new(
137        io::ErrorKind::NotFound,
138        format!("Path {} is neither a file nor a directory", pa.display()),
139    ))
140}
141
142/// Finds all files with specific extensions in the specified directory and its subdirectories.
143pub fn find_ext_files(path: &str, recursive: bool, exts: &[&str]) -> io::Result<Vec<String>> {
144    let mut result = Vec::new();
145    let dir_path = Path::new(&path);
146
147    if dir_path.is_dir() {
148        for entry in fs::read_dir(dir_path)? {
149            let entry = entry?;
150            let path = entry.path();
151
152            if path.is_file()
153                && path.file_name().map_or(false, |file| {
154                    path.extension().map_or(true, |_| {
155                        let file = file.to_string_lossy().to_lowercase();
156                        for ext in exts {
157                            if file.ends_with(&format!(".{}", ext)) {
158                                return true;
159                            }
160                        }
161                        false
162                    })
163                })
164            {
165                if let Some(path_str) = path.to_str() {
166                    result.push(path_str.to_string());
167                }
168            } else if recursive && path.is_dir() {
169                if let Some(path_str) = path.to_str() {
170                    let mut sub_files = find_ext_files(path_str, recursive, exts)?;
171                    result.append(&mut sub_files);
172                }
173            }
174        }
175    }
176
177    Ok(result)
178}
179
180/// Collects files with specific extensions from the specified path, either as a directory or a single file.
181pub fn collect_ext_files(
182    path: &str,
183    recursive: bool,
184    exts: &[&str],
185) -> io::Result<(Vec<String>, bool)> {
186    let pa = Path::new(path);
187    if pa.is_dir() {
188        return Ok((find_ext_files(path, recursive, exts)?, true));
189    }
190    if pa.is_file() {
191        return Ok((vec![path.to_string()], false));
192    }
193    Err(io::Error::new(
194        io::ErrorKind::NotFound,
195        format!("Path {} is neither a file nor a directory", pa.display()),
196    ))
197}
198
199/// Collects archive files from the specified path, either as a directory or a single file.
200pub fn collect_arc_files(path: &str, recursive: bool) -> io::Result<(Vec<String>, bool)> {
201    let pa = Path::new(path);
202    if pa.is_dir() {
203        return Ok((find_arc_files(path, recursive)?, true));
204    }
205    if pa.is_file() {
206        return Ok((vec![path.to_string()], false));
207    }
208    Err(io::Error::new(
209        io::ErrorKind::NotFound,
210        format!("Path {} is neither a file nor a directory", pa.display()),
211    ))
212}
213
214/// Reads the content of a file or standard input if the path is "-".
215pub fn read_file<F: AsRef<Path> + ?Sized>(f: &F) -> io::Result<Vec<u8>> {
216    let mut content = Vec::new();
217    if f.as_ref() == Path::new("-") {
218        io::stdin().read_to_end(&mut content)?;
219    } else {
220        content = fs::read(f)?;
221    }
222    Ok(content)
223}
224
225/// Writes content to a file or standard output if the path is "-".
226pub fn write_file<F: AsRef<Path> + ?Sized>(f: &F) -> io::Result<Box<dyn Write>> {
227    Ok(if f.as_ref() == Path::new("-") {
228        Box::new(io::stdout())
229    } else {
230        Box::new(fs::File::create(f)?)
231    })
232}
233
234/// Ensures that the parent directory for the specified path exists, creating it if necessary.
235pub fn make_sure_dir_exists<F: AsRef<Path> + ?Sized>(f: &F) -> io::Result<()> {
236    let path = f.as_ref();
237    if let Some(parent) = path.parent() {
238        if !parent.exists() {
239            fs::create_dir_all(parent)?;
240        }
241    }
242    Ok(())
243}
244
245/// Replace symbols not allowed in Windows path with underscores.
246pub fn sanitize_path(path: &str) -> String {
247    // Split path into components, preserving separators
248    if path.is_empty() {
249        return String::new();
250    }
251
252    let invalid_chars: &[char] = &['<', '>', '"', '|', '?', '*'];
253    let mut result = String::with_capacity(path.len());
254
255    let reserved_names: Vec<String> = {
256        let mut v = vec!["CON", "PRN", "AUX", "NUL"]
257            .into_iter()
258            .map(|s| s.to_string())
259            .collect::<Vec<_>>();
260        for i in 1..=9 {
261            v.push(format!("COM{}", i));
262            v.push(format!("LPT{}", i));
263        }
264        v
265    };
266
267    let bytes = path.as_bytes();
268    let len = bytes.len();
269    let mut start = 0usize;
270
271    while start < len {
272        // find next separator index
273        let mut end = start;
274        while end < len && bytes[end] != b'\\' && bytes[end] != b'/' {
275            end += 1;
276        }
277        // segment is path[start..end]
278        let seg = &path[start..end];
279
280        // sanitize segment
281        let mut s = String::with_capacity(seg.len());
282        for (i, ch) in seg.chars().enumerate() {
283            // allow drive letter colon like "C:" (i == 1, first char is ASCII letter)
284            if ch == ':' {
285                if i == 1 {
286                    // check first char is ASCII letter
287                    if seg
288                        .chars()
289                        .next()
290                        .map(|c| c.is_ascii_alphabetic())
291                        .unwrap_or(false)
292                    {
293                        s.push(':');
294                        continue;
295                    }
296                }
297                // otherwise treat as invalid
298                s.push('_');
299                continue;
300            }
301            // keep separators out of segment (shouldn't appear here)
302            // replace control chars and other invalids
303            if (ch as u32) < 32 || invalid_chars.contains(&ch) {
304                s.push('_');
305            } else {
306                s.push(ch);
307            }
308        }
309
310        // trim trailing spaces and dots (Windows disallows filenames ending with space or dot)
311        while s.ends_with(' ') || s.ends_with('.') {
312            s.pop();
313        }
314
315        if s.is_empty() {
316            s.push('_');
317        } else {
318            // check reserved names (base name before first '.')
319            let base = s.split('.').next().unwrap_or("").to_ascii_uppercase();
320            if reserved_names.iter().any(|r| r == &base) {
321                s = format!("_{}", s);
322            }
323        }
324
325        result.push_str(&s);
326
327        // append separator if present
328        if end < len {
329            // keep original separator (preserve '\' or '/')
330            result.push(path.as_bytes()[end] as char);
331            start = end + 1;
332        } else {
333            start = end;
334        }
335    }
336
337    result
338}