1use crate::scripts::{ALL_EXTS, ARCHIVE_EXTS};
3#[cfg(not(windows))]
4use std::ffi::OsString;
5use std::fs;
6use std::io;
7use std::io::{Read, Write};
8#[cfg(not(windows))]
9use std::path::Component;
10use std::path::{Path, PathBuf};
11
12pub fn relative_path<P: AsRef<Path>, T: AsRef<Path>>(root: P, target: T) -> PathBuf {
14 let root = root
15 .as_ref()
16 .canonicalize()
17 .unwrap_or_else(|_| root.as_ref().to_path_buf());
18 let target = target
19 .as_ref()
20 .canonicalize()
21 .unwrap_or_else(|_| target.as_ref().to_path_buf());
22
23 let mut root_components: Vec<_> = root.components().collect();
24 let mut target_components: Vec<_> = target.components().collect();
25
26 while !root_components.is_empty()
28 && !target_components.is_empty()
29 && root_components[0] == target_components[0]
30 {
31 root_components.remove(0);
32 target_components.remove(0);
33 }
34
35 let mut result = PathBuf::new();
37 for _ in root_components {
38 result.push("..");
39 }
40
41 for component in target_components {
43 result.push(component);
44 }
45
46 result
47}
48
49pub fn find_files(path: &str, recursive: bool, no_ext_filter: bool) -> io::Result<Vec<String>> {
51 let mut result = Vec::new();
52 let dir_path = Path::new(&path);
53
54 if dir_path.is_dir() {
55 for entry in fs::read_dir(dir_path)? {
56 let entry = entry?;
57 let path = entry.path();
58
59 if path.is_file()
60 && (no_ext_filter
61 || path.file_name().map_or(false, |file| {
62 path.extension().map_or(true, |_| {
63 let file = file.to_string_lossy().to_lowercase();
64 for ext in ALL_EXTS.iter() {
65 if file.ends_with(&format!(".{}", ext)) {
66 return true;
67 }
68 }
69 false
70 })
71 }))
72 {
73 if let Some(path_str) = path.to_str() {
74 result.push(path_str.to_string());
75 }
76 } else if recursive && path.is_dir() {
77 if let Some(path_str) = path.to_str() {
78 let mut sub_files =
79 find_files(&path_str.to_string(), recursive, no_ext_filter)?;
80 result.append(&mut sub_files);
81 }
82 }
83 }
84 }
85
86 Ok(result)
87}
88
89pub fn find_arc_files(path: &str, recursive: bool) -> io::Result<Vec<String>> {
91 let mut result = Vec::new();
92 let dir_path = Path::new(&path);
93
94 if dir_path.is_dir() {
95 for entry in fs::read_dir(dir_path)? {
96 let entry = entry?;
97 let path = entry.path();
98
99 if path.is_file()
100 && path.file_name().map_or(false, |file| {
101 path.extension().map_or(true, |_| {
102 let file = file.to_string_lossy().to_lowercase();
103 for ext in ARCHIVE_EXTS.iter() {
104 if file.ends_with(&format!(".{}", ext)) {
105 return true;
106 }
107 }
108 false
109 })
110 })
111 {
112 if let Some(path_str) = path.to_str() {
113 result.push(path_str.to_string());
114 }
115 } else if recursive && path.is_dir() {
116 if let Some(path_str) = path.to_str() {
117 let mut sub_files = find_arc_files(&path_str.to_string(), recursive)?;
118 result.append(&mut sub_files);
119 }
120 }
121 }
122 }
123
124 Ok(result)
125}
126
127pub fn collect_files(
129 path: &str,
130 recursive: bool,
131 no_ext_filter: bool,
132) -> io::Result<(Vec<String>, bool)> {
133 let pa = Path::new(path);
134 if pa.is_dir() {
135 return Ok((find_files(path, recursive, no_ext_filter)?, true));
136 }
137 if pa.is_file() {
138 return Ok((vec![path.to_string()], false));
139 }
140 Err(io::Error::new(
141 io::ErrorKind::NotFound,
142 format!("Path {} is neither a file nor a directory", pa.display()),
143 ))
144}
145
146pub fn find_ext_files(path: &str, recursive: bool, exts: &[&str]) -> io::Result<Vec<String>> {
148 let mut result = Vec::new();
149 let dir_path = Path::new(&path);
150
151 if dir_path.is_dir() {
152 for entry in fs::read_dir(dir_path)? {
153 let entry = entry?;
154 let path = entry.path();
155
156 if path.is_file()
157 && path.file_name().map_or(false, |file| {
158 path.extension().map_or(true, |_| {
159 let file = file.to_string_lossy().to_lowercase();
160 for ext in exts {
161 if file.ends_with(&format!(".{}", ext)) {
162 return true;
163 }
164 }
165 false
166 })
167 })
168 {
169 if let Some(path_str) = path.to_str() {
170 result.push(path_str.to_string());
171 }
172 } else if recursive && path.is_dir() {
173 if let Some(path_str) = path.to_str() {
174 let mut sub_files = find_ext_files(path_str, recursive, exts)?;
175 result.append(&mut sub_files);
176 }
177 }
178 }
179 }
180
181 Ok(result)
182}
183
184pub fn collect_ext_files(
186 path: &str,
187 recursive: bool,
188 exts: &[&str],
189) -> io::Result<(Vec<String>, bool)> {
190 let pa = Path::new(path);
191 if pa.is_dir() {
192 return Ok((find_ext_files(path, recursive, exts)?, true));
193 }
194 if pa.is_file() {
195 return Ok((vec![path.to_string()], false));
196 }
197 Err(io::Error::new(
198 io::ErrorKind::NotFound,
199 format!("Path {} is neither a file nor a directory", pa.display()),
200 ))
201}
202
203pub fn collect_arc_files(path: &str, recursive: bool) -> io::Result<(Vec<String>, bool)> {
205 let pa = Path::new(path);
206 if pa.is_dir() {
207 return Ok((find_arc_files(path, recursive)?, true));
208 }
209 if pa.is_file() {
210 return Ok((vec![path.to_string()], false));
211 }
212 Err(io::Error::new(
213 io::ErrorKind::NotFound,
214 format!("Path {} is neither a file nor a directory", pa.display()),
215 ))
216}
217
218pub fn read_file<F: AsRef<Path> + ?Sized>(f: &F) -> io::Result<Vec<u8>> {
220 let mut content = Vec::new();
221 if f.as_ref() == Path::new("-") {
222 io::stdin().read_to_end(&mut content)?;
223 } else {
224 content = fs::read(f)?;
225 }
226 Ok(content)
227}
228
229pub fn write_file<F: AsRef<Path> + ?Sized>(f: &F) -> io::Result<Box<dyn Write>> {
231 Ok(if f.as_ref() == Path::new("-") {
232 Box::new(io::stdout())
233 } else {
234 Box::new(fs::File::create(f)?)
235 })
236}
237
238pub fn make_sure_dir_exists<F: AsRef<Path> + ?Sized>(f: &F) -> io::Result<()> {
240 let path = f.as_ref();
241 if let Some(parent) = path.parent() {
242 if !parent.exists() {
243 fs::create_dir_all(parent)?;
244 }
245 }
246 Ok(())
247}
248
249pub fn sanitize_path(path: &str) -> String {
251 if path.is_empty() {
253 return String::new();
254 }
255
256 let invalid_chars: &[char] = &['<', '>', '"', '|', '?', '*'];
257 let mut result = String::with_capacity(path.len());
258
259 let reserved_names: Vec<String> = {
260 let mut v = vec!["CON", "PRN", "AUX", "NUL"]
261 .into_iter()
262 .map(|s| s.to_string())
263 .collect::<Vec<_>>();
264 for i in 1..=9 {
265 v.push(format!("COM{}", i));
266 v.push(format!("LPT{}", i));
267 }
268 v
269 };
270
271 let bytes = path.as_bytes();
272 let len = bytes.len();
273 let mut start = 0usize;
274
275 while start < len {
276 let mut end = start;
278 while end < len && bytes[end] != b'\\' && bytes[end] != b'/' {
279 end += 1;
280 }
281 let seg = &path[start..end];
283
284 let mut s = String::with_capacity(seg.len());
286 for (i, ch) in seg.chars().enumerate() {
287 if ch == ':' {
289 if i == 1 {
290 if seg
292 .chars()
293 .next()
294 .map(|c| c.is_ascii_alphabetic())
295 .unwrap_or(false)
296 {
297 s.push(':');
298 continue;
299 }
300 }
301 s.push('_');
303 continue;
304 }
305 if (ch as u32) < 32 || invalid_chars.contains(&ch) {
308 s.push('_');
309 } else {
310 s.push(ch);
311 }
312 }
313
314 while s.ends_with(' ') || s.ends_with('.') {
316 s.pop();
317 }
318
319 if s.is_empty() {
320 s.push('_');
321 } else {
322 let base = s.split('.').next().unwrap_or("").to_ascii_uppercase();
324 if reserved_names.iter().any(|r| r == &base) {
325 s = format!("_{}", s);
326 }
327 }
328
329 result.push_str(&s);
330
331 if end < len {
333 result.push(path.as_bytes()[end] as char);
335 start = end + 1;
336 } else {
337 start = end;
338 }
339 }
340
341 result
342}
343
344pub fn get_ignorecase_path<P: AsRef<Path>>(path: P) -> std::io::Result<PathBuf> {
345 #[cfg(windows)]
346 return Ok(path.as_ref().to_path_buf());
347 #[cfg(not(windows))]
348 {
349 let path = path.as_ref();
350 if path.exists() {
352 return Ok(path.to_path_buf());
353 }
354 {
355 fn resolve_from_base(base: PathBuf, tail: &[OsString]) -> io::Result<Option<PathBuf>> {
358 let mut cur = base;
359 for comp in tail {
360 let direct = cur.join(comp);
361 if direct.exists() {
362 cur = direct;
363 continue;
364 }
365
366 if !cur.is_dir() {
367 return Ok(None);
368 }
369
370 let mut found = None;
371 for entry in fs::read_dir(&cur)? {
372 let entry = entry?;
373 let name = entry.file_name();
374 if name
375 .to_string_lossy()
376 .eq_ignore_ascii_case(&comp.to_string_lossy())
377 {
378 found = Some(cur.join(name));
379 break;
380 }
381 }
382
383 match found {
384 Some(p) => cur = p,
385 None => return Ok(None),
386 }
387 }
388 Ok(Some(cur))
389 }
390
391 let orig = path;
392 if orig.exists() {
394 return Ok(orig.to_path_buf());
395 }
396
397 let comps: Vec<OsString> = orig
399 .components()
400 .map(|c| match c {
401 Component::Prefix(p) => p.as_os_str().to_os_string(),
402 Component::RootDir => OsString::from(std::path::MAIN_SEPARATOR.to_string()),
403 other => other.as_os_str().to_os_string(),
404 })
405 .collect();
406
407 let len = comps.len();
409 for idx in (0..len).rev() {
410 let mut parent = PathBuf::new();
412 for j in 0..idx {
413 parent.push(&comps[j]);
414 }
415 if parent.as_os_str().is_empty() {
416 parent = PathBuf::from(".");
417 }
418
419 if !parent.exists() || !parent.is_dir() {
421 continue;
422 }
423
424 let target = &comps[idx];
426 let mut matched_name: Option<OsString> = None;
427 for entry in fs::read_dir(&parent)? {
428 let entry = entry?;
429 let name = entry.file_name();
430 if name
431 .to_string_lossy()
432 .eq_ignore_ascii_case(&target.to_string_lossy())
433 {
434 matched_name = Some(name);
435 break;
436 }
437 }
438
439 if let Some(name) = matched_name {
440 let candidate_base = parent.join(name);
442 let tail: Vec<OsString> = comps.iter().skip(idx + 1).cloned().collect();
443 if tail.is_empty() {
444 if candidate_base.exists() {
445 return Ok(candidate_base);
446 } else {
447 continue;
450 }
451 }
452
453 if let Some(resolved) = resolve_from_base(candidate_base, &tail)? {
454 return Ok(resolved);
455 }
456 }
457 }
458
459 Err(io::Error::new(
460 io::ErrorKind::NotFound,
461 format!(
462 "Path {} not found (case-insensitive search failed)",
463 path.display()
464 ),
465 ))
466 }
467 }
468}