1use glob::{MatchOptions, Pattern};
4use rand::Rng;
5use rayon::prelude::*;
6use std::env;
7use std::io::{self, BufRead, Write};
8use std::path::{Path, PathBuf};
9use std::sync::Arc;
10use std::time::UNIX_EPOCH;
11
12use crate::pmap_progress::PmapProgress;
13use crate::value::PerlValue;
14
15pub use crate::perl_decode::{
16 decode_utf8_or_latin1, decode_utf8_or_latin1_line, decode_utf8_or_latin1_read_until,
17};
18
19pub fn read_file_text_perl_compat(path: impl AsRef<Path>) -> io::Result<String> {
22 let bytes = std::fs::read(path.as_ref())?;
23 Ok(decode_utf8_or_latin1(&bytes))
24}
25
26pub fn read_line_perl_compat(reader: &mut impl BufRead, buf: &mut String) -> io::Result<usize> {
28 buf.clear();
29 let mut raw = Vec::new();
30 let n = reader.read_until(b'\n', &mut raw)?;
31 if n == 0 {
32 return Ok(0);
33 }
34 buf.push_str(&decode_utf8_or_latin1_read_until(&raw));
35 Ok(n)
36}
37
38pub fn read_logical_line_perl_compat(reader: &mut impl BufRead) -> io::Result<Option<String>> {
41 let mut buf = Vec::new();
42 let n = reader.read_until(b'\n', &mut buf)?;
43 if n == 0 {
44 return Ok(None);
45 }
46 if buf.ends_with(b"\n") {
47 buf.pop();
48 if buf.ends_with(b"\r") {
49 buf.pop();
50 }
51 }
52 Ok(Some(decode_utf8_or_latin1_line(&buf)))
53}
54
55pub fn filetest_is_tty(path: &str) -> bool {
58 #[cfg(unix)]
59 {
60 use std::os::unix::io::AsRawFd;
61 if let Some(fd) = tty_fd_literal(path) {
62 return unsafe { libc::isatty(fd) != 0 };
63 }
64 if let Ok(f) = std::fs::File::open(path) {
65 return unsafe { libc::isatty(f.as_raw_fd()) != 0 };
66 }
67 }
68 #[cfg(not(unix))]
69 {
70 let _ = path;
71 }
72 false
73}
74
75#[cfg(unix)]
76fn tty_fd_literal(path: &str) -> Option<i32> {
77 match path {
78 "" | "STDIN" | "-" | "/dev/stdin" => Some(0),
79 "STDOUT" | "/dev/stdout" => Some(1),
80 "STDERR" | "/dev/stderr" => Some(2),
81 p if p.starts_with("/dev/fd/") => p.strip_prefix("/dev/fd/").and_then(|s| s.parse().ok()),
82 _ => path.parse::<i32>().ok().filter(|&n| (0..128).contains(&n)),
83 }
84}
85
86#[cfg(unix)]
89pub fn filetest_effective_access(path: &str, check: u32) -> bool {
90 use std::os::unix::fs::MetadataExt;
91 let meta = match std::fs::metadata(path) {
92 Ok(m) => m,
93 Err(_) => return false,
94 };
95 let mode = meta.mode();
96 let euid = unsafe { libc::geteuid() };
97 let egid = unsafe { libc::getegid() };
98 if euid == 0 {
100 return if check == 1 { mode & 0o111 != 0 } else { true };
101 }
102 if meta.uid() == euid {
103 return mode & (check << 6) != 0;
104 }
105 if meta.gid() == egid {
106 return mode & (check << 3) != 0;
107 }
108 mode & check != 0
109}
110
111#[cfg(unix)]
113pub fn filetest_real_access(path: &str, amode: libc::c_int) -> bool {
114 match std::ffi::CString::new(path) {
115 Ok(c) => unsafe { libc::access(c.as_ptr(), amode) == 0 },
116 Err(_) => false,
117 }
118}
119
120#[cfg(unix)]
122pub fn filetest_owned_effective(path: &str) -> bool {
123 use std::os::unix::fs::MetadataExt;
124 std::fs::metadata(path)
125 .map(|m| m.uid() == unsafe { libc::geteuid() })
126 .unwrap_or(false)
127}
128
129#[cfg(unix)]
131pub fn filetest_owned_real(path: &str) -> bool {
132 use std::os::unix::fs::MetadataExt;
133 std::fs::metadata(path)
134 .map(|m| m.uid() == unsafe { libc::getuid() })
135 .unwrap_or(false)
136}
137
138#[cfg(unix)]
140pub fn filetest_is_pipe(path: &str) -> bool {
141 use std::os::unix::fs::FileTypeExt;
142 std::fs::metadata(path)
143 .map(|m| m.file_type().is_fifo())
144 .unwrap_or(false)
145}
146
147#[cfg(unix)]
149pub fn filetest_is_socket(path: &str) -> bool {
150 use std::os::unix::fs::FileTypeExt;
151 std::fs::metadata(path)
152 .map(|m| m.file_type().is_socket())
153 .unwrap_or(false)
154}
155
156#[cfg(unix)]
158pub fn filetest_is_block_device(path: &str) -> bool {
159 use std::os::unix::fs::FileTypeExt;
160 std::fs::metadata(path)
161 .map(|m| m.file_type().is_block_device())
162 .unwrap_or(false)
163}
164
165#[cfg(unix)]
167pub fn filetest_is_char_device(path: &str) -> bool {
168 use std::os::unix::fs::FileTypeExt;
169 std::fs::metadata(path)
170 .map(|m| m.file_type().is_char_device())
171 .unwrap_or(false)
172}
173
174#[cfg(unix)]
176pub fn filetest_is_setuid(path: &str) -> bool {
177 use std::os::unix::fs::MetadataExt;
178 std::fs::metadata(path)
179 .map(|m| m.mode() & 0o4000 != 0)
180 .unwrap_or(false)
181}
182
183#[cfg(unix)]
185pub fn filetest_is_setgid(path: &str) -> bool {
186 use std::os::unix::fs::MetadataExt;
187 std::fs::metadata(path)
188 .map(|m| m.mode() & 0o2000 != 0)
189 .unwrap_or(false)
190}
191
192#[cfg(unix)]
194pub fn filetest_is_sticky(path: &str) -> bool {
195 use std::os::unix::fs::MetadataExt;
196 std::fs::metadata(path)
197 .map(|m| m.mode() & 0o1000 != 0)
198 .unwrap_or(false)
199}
200
201pub fn filetest_is_text(path: &str) -> bool {
203 filetest_text_binary(path, true)
204}
205
206pub fn filetest_is_binary(path: &str) -> bool {
208 filetest_text_binary(path, false)
209}
210
211fn filetest_text_binary(path: &str, want_text: bool) -> bool {
212 use std::io::Read;
213 let mut f = match std::fs::File::open(path) {
214 Ok(f) => f,
215 Err(_) => return false,
216 };
217 let mut buf = [0u8; 512];
218 let n = match f.read(&mut buf) {
219 Ok(n) => n,
220 Err(_) => return false,
221 };
222 if n == 0 {
223 return want_text;
225 }
226 let slice = &buf[..n];
227 let non_text = slice
229 .iter()
230 .filter(|&&b| b == 0 || (b < 0x20 && b != b'\t' && b != b'\n' && b != b'\r' && b != 0x1b))
231 .count();
232 let is_text = (non_text as f64 / n as f64) < 0.30;
233 if want_text {
234 is_text
235 } else {
236 !is_text
237 }
238}
239
240#[cfg(unix)]
242pub fn filetest_age_days(path: &str, which: char) -> Option<f64> {
243 use std::os::unix::fs::MetadataExt;
244 let meta = std::fs::metadata(path).ok()?;
245 let t = match which {
246 'M' => meta.mtime() as f64,
247 'A' => meta.atime() as f64,
248 _ => meta.ctime() as f64,
249 };
250 let now = std::time::SystemTime::now()
251 .duration_since(std::time::UNIX_EPOCH)
252 .unwrap_or_default()
253 .as_secs_f64();
254 Some((now - t) / 86400.0)
255}
256
257pub fn stat_path(path: &str, symlink: bool) -> PerlValue {
259 let res = if symlink {
260 std::fs::symlink_metadata(path)
261 } else {
262 std::fs::metadata(path)
263 };
264 match res {
265 Ok(meta) => PerlValue::array(perl_stat_from_metadata(&meta)),
266 Err(_) => PerlValue::array(vec![]),
267 }
268}
269
270pub fn perl_stat_from_metadata(meta: &std::fs::Metadata) -> Vec<PerlValue> {
271 #[cfg(unix)]
272 {
273 use std::os::unix::fs::MetadataExt;
274 vec![
275 PerlValue::integer(meta.dev() as i64),
276 PerlValue::integer(meta.ino() as i64),
277 PerlValue::integer(meta.mode() as i64),
278 PerlValue::integer(meta.nlink() as i64),
279 PerlValue::integer(meta.uid() as i64),
280 PerlValue::integer(meta.gid() as i64),
281 PerlValue::integer(meta.rdev() as i64),
282 PerlValue::integer(meta.len() as i64),
283 PerlValue::integer(meta.atime()),
284 PerlValue::integer(meta.mtime()),
285 PerlValue::integer(meta.ctime()),
286 PerlValue::integer(meta.blksize() as i64),
287 PerlValue::integer(meta.blocks() as i64),
288 ]
289 }
290 #[cfg(not(unix))]
291 {
292 let len = meta.len() as i64;
293 vec![
294 PerlValue::integer(0),
295 PerlValue::integer(0),
296 PerlValue::integer(0),
297 PerlValue::integer(0),
298 PerlValue::integer(0),
299 PerlValue::integer(0),
300 PerlValue::integer(0),
301 PerlValue::integer(len),
302 PerlValue::integer(0),
303 PerlValue::integer(0),
304 PerlValue::integer(0),
305 PerlValue::integer(0),
306 PerlValue::integer(0),
307 ]
308 }
309}
310
311pub fn link_hard(old: &str, new: &str) -> PerlValue {
312 PerlValue::integer(if std::fs::hard_link(old, new).is_ok() {
313 1
314 } else {
315 0
316 })
317}
318
319pub fn link_sym(old: &str, new: &str) -> PerlValue {
320 #[cfg(unix)]
321 {
322 use std::os::unix::fs::symlink;
323 PerlValue::integer(if symlink(old, new).is_ok() { 1 } else { 0 })
324 }
325 #[cfg(not(unix))]
326 {
327 let _ = (old, new);
328 PerlValue::integer(0)
329 }
330}
331
332pub fn read_link(path: &str) -> PerlValue {
333 match std::fs::read_link(path) {
334 Ok(p) => PerlValue::string(p.to_string_lossy().into_owned()),
335 Err(_) => PerlValue::UNDEF,
336 }
337}
338
339pub fn realpath_resolved(path: &str) -> io::Result<String> {
341 std::fs::canonicalize(path).map(|p| p.to_string_lossy().into_owned())
342}
343
344pub fn canonpath_logical(path: &str) -> String {
348 use std::path::Component;
349 if path.is_empty() {
350 return String::new();
351 }
352 let mut stack: Vec<String> = Vec::new();
353 let mut anchored = false;
354 for c in Path::new(path).components() {
355 match c {
356 Component::Prefix(p) => {
357 stack.push(p.as_os_str().to_string_lossy().into_owned());
358 }
359 Component::RootDir => {
360 anchored = true;
361 stack.clear();
362 }
363 Component::CurDir => {}
364 Component::Normal(s) => {
365 stack.push(s.to_string_lossy().into_owned());
366 }
367 Component::ParentDir => {
368 if anchored {
369 if !stack.is_empty() {
370 stack.pop();
371 }
372 } else if stack.is_empty() || stack.last().is_some_and(|t| t == "..") {
373 stack.push("..".to_string());
374 } else {
375 stack.pop();
376 }
377 }
378 }
379 }
380 let body = stack.join("/");
381 if anchored {
382 if body.is_empty() {
383 "/".to_string()
384 } else {
385 format!("/{body}")
386 }
387 } else if body.is_empty() {
388 ".".to_string()
389 } else {
390 body
391 }
392}
393
394pub fn list_files(dir: &str) -> PerlValue {
397 let mut names: Vec<String> = Vec::new();
398 if let Ok(entries) = std::fs::read_dir(dir) {
399 for entry in entries.flatten() {
400 if let Some(name) = entry.file_name().to_str() {
401 names.push(name.to_string());
402 }
403 }
404 }
405 names.sort();
406 PerlValue::array(names.into_iter().map(PerlValue::string).collect())
407}
408
409pub fn list_filesf(dir: &str) -> PerlValue {
413 let mut names: Vec<String> = Vec::new();
414 if let Ok(entries) = std::fs::read_dir(dir) {
415 for entry in entries.flatten() {
416 if entry.file_type().map(|ft| ft.is_file()).unwrap_or(false) {
417 if let Some(name) = entry.file_name().to_str() {
418 names.push(name.to_string());
419 }
420 }
421 }
422 }
423 names.sort();
424 PerlValue::array(names.into_iter().map(PerlValue::string).collect())
425}
426
427pub fn list_filesf_recursive(dir: &str) -> PerlValue {
431 let root = std::path::Path::new(dir);
432 let mut paths: Vec<String> = Vec::new();
433 fn walk(base: &std::path::Path, rel: &str, out: &mut Vec<String>) {
434 let Ok(entries) = std::fs::read_dir(base) else {
435 return;
436 };
437 for entry in entries.flatten() {
438 let ft = match entry.file_type() {
439 Ok(ft) => ft,
440 Err(_) => continue,
441 };
442 let name = match entry.file_name().into_string() {
443 Ok(n) => n,
444 Err(_) => continue,
445 };
446 let child_rel = if rel.is_empty() {
447 name.clone()
448 } else {
449 format!("{rel}/{name}")
450 };
451 if ft.is_file() {
452 out.push(child_rel);
453 } else if ft.is_dir() {
454 walk(&base.join(&name), &child_rel, out);
455 }
456 }
457 }
458 walk(root, "", &mut paths);
459 paths.sort();
460 PerlValue::array(paths.into_iter().map(PerlValue::string).collect())
461}
462
463pub fn list_dirs(dir: &str) -> PerlValue {
466 let mut names: Vec<String> = Vec::new();
467 if let Ok(entries) = std::fs::read_dir(dir) {
468 for entry in entries.flatten() {
469 if entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false) {
470 if let Some(name) = entry.file_name().to_str() {
471 names.push(name.to_string());
472 }
473 }
474 }
475 }
476 names.sort();
477 PerlValue::array(names.into_iter().map(PerlValue::string).collect())
478}
479
480pub fn list_dirs_recursive(dir: &str) -> PerlValue {
484 let root = std::path::Path::new(dir);
485 let mut paths: Vec<String> = Vec::new();
486 fn walk(base: &std::path::Path, rel: &str, out: &mut Vec<String>) {
487 let Ok(entries) = std::fs::read_dir(base) else {
488 return;
489 };
490 for entry in entries.flatten() {
491 let ft = match entry.file_type() {
492 Ok(ft) => ft,
493 Err(_) => continue,
494 };
495 if !ft.is_dir() {
496 continue;
497 }
498 let name = match entry.file_name().into_string() {
499 Ok(n) => n,
500 Err(_) => continue,
501 };
502 let child_rel = if rel.is_empty() {
503 name.clone()
504 } else {
505 format!("{rel}/{name}")
506 };
507 out.push(child_rel.clone());
508 walk(&base.join(&name), &child_rel, out);
509 }
510 }
511 walk(root, "", &mut paths);
512 paths.sort();
513 PerlValue::array(paths.into_iter().map(PerlValue::string).collect())
514}
515
516pub fn list_sym_links(dir: &str) -> PerlValue {
519 let mut names: Vec<String> = Vec::new();
520 if let Ok(entries) = std::fs::read_dir(dir) {
521 for entry in entries.flatten() {
522 if entry.file_type().map(|ft| ft.is_symlink()).unwrap_or(false) {
523 if let Some(name) = entry.file_name().to_str() {
524 names.push(name.to_string());
525 }
526 }
527 }
528 }
529 names.sort();
530 PerlValue::array(names.into_iter().map(PerlValue::string).collect())
531}
532
533pub fn list_sockets(dir: &str) -> PerlValue {
536 let mut names: Vec<String> = Vec::new();
537 #[cfg(unix)]
538 {
539 use std::os::unix::fs::FileTypeExt;
540 if let Ok(entries) = std::fs::read_dir(dir) {
541 for entry in entries.flatten() {
542 if entry.file_type().map(|ft| ft.is_socket()).unwrap_or(false) {
543 if let Some(name) = entry.file_name().to_str() {
544 names.push(name.to_string());
545 }
546 }
547 }
548 }
549 }
550 let _ = dir;
551 names.sort();
552 PerlValue::array(names.into_iter().map(PerlValue::string).collect())
553}
554
555pub fn list_pipes(dir: &str) -> PerlValue {
558 let mut names: Vec<String> = Vec::new();
559 #[cfg(unix)]
560 {
561 use std::os::unix::fs::FileTypeExt;
562 if let Ok(entries) = std::fs::read_dir(dir) {
563 for entry in entries.flatten() {
564 if entry.file_type().map(|ft| ft.is_fifo()).unwrap_or(false) {
565 if let Some(name) = entry.file_name().to_str() {
566 names.push(name.to_string());
567 }
568 }
569 }
570 }
571 }
572 let _ = dir;
573 names.sort();
574 PerlValue::array(names.into_iter().map(PerlValue::string).collect())
575}
576
577pub fn list_block_devices(dir: &str) -> PerlValue {
580 let mut names: Vec<String> = Vec::new();
581 #[cfg(unix)]
582 {
583 use std::os::unix::fs::FileTypeExt;
584 if let Ok(entries) = std::fs::read_dir(dir) {
585 for entry in entries.flatten() {
586 if entry
587 .file_type()
588 .map(|ft| ft.is_block_device())
589 .unwrap_or(false)
590 {
591 if let Some(name) = entry.file_name().to_str() {
592 names.push(name.to_string());
593 }
594 }
595 }
596 }
597 }
598 let _ = dir;
599 names.sort();
600 PerlValue::array(names.into_iter().map(PerlValue::string).collect())
601}
602
603pub fn list_char_devices(dir: &str) -> PerlValue {
606 let mut names: Vec<String> = Vec::new();
607 #[cfg(unix)]
608 {
609 use std::os::unix::fs::FileTypeExt;
610 if let Ok(entries) = std::fs::read_dir(dir) {
611 for entry in entries.flatten() {
612 if entry
613 .file_type()
614 .map(|ft| ft.is_char_device())
615 .unwrap_or(false)
616 {
617 if let Some(name) = entry.file_name().to_str() {
618 names.push(name.to_string());
619 }
620 }
621 }
622 }
623 }
624 let _ = dir;
625 names.sort();
626 PerlValue::array(names.into_iter().map(PerlValue::string).collect())
627}
628
629pub fn glob_patterns(patterns: &[String]) -> PerlValue {
630 let mut paths: Vec<String> = Vec::new();
631 for pat in patterns {
632 if let Ok(g) = glob::glob(pat) {
633 for e in g.flatten() {
634 paths.push(normalize_glob_path_display(
635 e.to_string_lossy().into_owned(),
636 ));
637 }
638 }
639 }
640 paths.sort();
641 paths.dedup();
642 PerlValue::array(paths.into_iter().map(PerlValue::string).collect())
643}
644
645fn glob_base_path(pat: &str) -> PathBuf {
647 let p = Path::new(pat);
648 let mut acc = PathBuf::new();
649 for c in p.components() {
650 let s = c.as_os_str().to_string_lossy();
651 if s.contains('*') || s.contains('?') || s.contains('[') {
652 break;
653 }
654 acc.push(c.as_os_str());
655 }
656 if acc.as_os_str().is_empty() {
657 PathBuf::from(".")
658 } else {
659 acc
660 }
661}
662
663fn glob_par_walk(dir: &Path, pattern: &Pattern, options: &MatchOptions) -> Vec<String> {
664 let read = match std::fs::read_dir(dir) {
665 Ok(r) => r,
666 Err(_) => return Vec::new(),
667 };
668 let entries: Vec<_> = read.filter_map(|e| e.ok()).collect();
669 entries
670 .par_iter()
671 .flat_map_iter(|e| {
672 let path = e.path();
673 let mut out = Vec::new();
674 let s = path.to_string_lossy();
675 if pattern.matches_with(s.as_ref(), *options) {
676 out.push(s.into_owned());
677 }
678 if path.is_dir() {
679 out.extend(glob_par_walk(&path, pattern, options));
680 }
681 out.into_iter()
682 })
683 .collect()
684}
685
686pub fn glob_par_patterns(patterns: &[String]) -> PerlValue {
689 glob_par_patterns_inner(patterns, None)
690}
691
692pub fn glob_par_patterns_with_progress(patterns: &[String], progress: bool) -> PerlValue {
695 if patterns.is_empty() {
696 return PerlValue::array(Vec::new());
697 }
698 let pmap = PmapProgress::new(progress, patterns.len());
699 let v = glob_par_patterns_inner(patterns, Some(&pmap));
700 pmap.finish();
701 v
702}
703
704fn glob_par_patterns_inner(patterns: &[String], progress: Option<&PmapProgress>) -> PerlValue {
705 let options = MatchOptions::new();
706 let out: Vec<String> = patterns
707 .par_iter()
708 .flat_map_iter(|pat| {
709 let rows = (|| {
710 let Ok(pattern) = Pattern::new(pat) else {
711 return Vec::new();
712 };
713 let base = glob_base_path(pat);
714 if !base.exists() {
715 return Vec::new();
716 }
717 glob_par_walk(&base, &pattern, &options)
718 })();
719 if let Some(p) = progress {
720 p.tick();
721 }
722 rows
723 })
724 .collect();
725 let mut paths: Vec<String> = out.into_iter().map(normalize_glob_path_display).collect();
726 paths.sort();
727 paths.dedup();
728 PerlValue::array(paths.into_iter().map(PerlValue::string).collect())
729}
730
731fn normalize_glob_path_display(s: String) -> String {
733 let p = Path::new(&s);
734 if p.is_absolute() || s.starts_with("./") || s.starts_with("../") {
735 s
736 } else {
737 format!("./{s}")
738 }
739}
740
741pub fn rename_paths(old: &str, new: &str) -> PerlValue {
743 PerlValue::integer(if std::fs::rename(old, new).is_ok() {
744 1
745 } else {
746 0
747 })
748}
749
750#[inline]
751fn is_cross_device_rename(e: &io::Error) -> bool {
752 if e.kind() == io::ErrorKind::CrossesDevices {
753 return true;
754 }
755 #[cfg(unix)]
756 {
757 if e.raw_os_error() == Some(libc::EXDEV) {
758 return true;
759 }
760 }
761 false
762}
763
764fn try_move_path(from: &str, to: &str) -> io::Result<()> {
765 match std::fs::rename(from, to) {
766 Ok(()) => Ok(()),
767 Err(e) => {
768 if !is_cross_device_rename(&e) {
769 return Err(e);
770 }
771 let meta = std::fs::symlink_metadata(from)?;
772 if meta.is_dir() {
773 return Err(io::Error::new(
774 io::ErrorKind::Unsupported,
775 "move: cross-device directory move is not supported",
776 ));
777 }
778 if !meta.is_file() && !meta.is_symlink() {
779 return Err(io::Error::new(
780 io::ErrorKind::Unsupported,
781 "move: cross-device move supports files and symlinks only",
782 ));
783 }
784 std::fs::copy(from, to)?;
785 std::fs::remove_file(from)?;
786 Ok(())
787 }
788 }
789}
790
791pub fn move_path(from: &str, to: &str) -> PerlValue {
794 PerlValue::integer(if try_move_path(from, to).is_ok() {
795 1
796 } else {
797 0
798 })
799}
800
801#[cfg(unix)]
802fn unix_path_executable(path: &Path) -> bool {
803 use std::os::unix::fs::PermissionsExt;
804 std::fs::metadata(path)
805 .ok()
806 .filter(|m| m.is_file())
807 .is_some_and(|m| m.permissions().mode() & 0o111 != 0)
808}
809
810#[cfg(not(unix))]
811fn unix_path_executable(path: &Path) -> bool {
812 path.is_file()
813}
814
815fn display_executable_path(path: &Path) -> Option<String> {
816 if !unix_path_executable(path) {
817 return None;
818 }
819 path.canonicalize()
820 .ok()
821 .map(|p| p.to_string_lossy().into_owned())
822 .or_else(|| Some(path.to_string_lossy().into_owned()))
823}
824
825#[cfg(windows)]
826fn pathext_suffixes() -> Vec<String> {
827 env::var_os("PATHEXT")
828 .map(|s| {
829 env::split_paths(&s)
830 .filter_map(|p| p.to_str().map(str::to_ascii_lowercase))
831 .collect()
832 })
833 .unwrap_or_else(|| vec![".exe".into(), ".cmd".into(), ".bat".into(), ".com".into()])
834}
835
836#[cfg(windows)]
837fn which_in_dir(dir: &Path, program: &str) -> Option<String> {
838 let plain = dir.join(program);
839 if let Some(s) = display_executable_path(&plain) {
840 return Some(s);
841 }
842 if !program.contains('.') {
843 for ext in pathext_suffixes() {
844 let cand = dir.join(format!("{program}{ext}"));
845 if let Some(s) = display_executable_path(&cand) {
846 return Some(s);
847 }
848 }
849 }
850 None
851}
852
853#[cfg(not(windows))]
854fn which_in_dir(dir: &Path, program: &str) -> Option<String> {
855 display_executable_path(&dir.join(program))
856}
857
858pub fn which_executable(program: &str, include_dot: bool) -> Option<String> {
861 if program.is_empty() {
862 return None;
863 }
864 if program.contains('/') || (cfg!(windows) && program.contains('\\')) {
865 return display_executable_path(Path::new(program));
866 }
867 let path_os = env::var_os("PATH")?;
868 for dir in env::split_paths(&path_os) {
869 if let Some(s) = which_in_dir(&dir, program) {
870 return Some(s);
871 }
872 }
873 if include_dot {
874 return which_in_dir(Path::new("."), program);
875 }
876 None
877}
878
879pub fn read_file_bytes(path: &str) -> io::Result<Arc<Vec<u8>>> {
881 Ok(Arc::new(std::fs::read(path)?))
882}
883
884fn adjacent_temp_path(target: &Path) -> PathBuf {
886 let dir = target.parent().unwrap_or_else(|| Path::new("."));
887 let name = target
888 .file_name()
889 .map(|s| s.to_string_lossy().into_owned())
890 .unwrap_or_else(|| "file".to_string());
891 let rnd: u32 = rand::thread_rng().gen();
892 dir.join(format!("{name}.spurt-tmp-{rnd}"))
893}
894
895pub fn spurt_path(path: &str, data: &[u8], mkdir_parents: bool, atomic: bool) -> io::Result<()> {
898 let path = Path::new(path);
899 if mkdir_parents {
900 if let Some(parent) = path.parent() {
901 if !parent.as_os_str().is_empty() {
902 std::fs::create_dir_all(parent)?;
903 }
904 }
905 }
906 if !atomic {
907 return std::fs::write(path, data);
908 }
909 let tmp = adjacent_temp_path(path);
910 {
911 let mut f = std::fs::File::create(&tmp)?;
912 f.write_all(data)?;
913 f.sync_all().ok();
914 }
915 std::fs::rename(&tmp, path)?;
916 Ok(())
917}
918
919pub fn copy_file(from: &str, to: &str, preserve_metadata: bool) -> PerlValue {
922 let times = if preserve_metadata {
923 std::fs::metadata(from).ok().map(|src_meta| {
924 let at = src_meta
925 .accessed()
926 .ok()
927 .and_then(|t| t.duration_since(UNIX_EPOCH).ok())
928 .map(|d| d.as_secs() as i64)
929 .unwrap_or(0);
930 let mt = src_meta
931 .modified()
932 .ok()
933 .and_then(|t| t.duration_since(UNIX_EPOCH).ok())
934 .map(|d| d.as_secs() as i64)
935 .unwrap_or(0);
936 (at, mt)
937 })
938 } else {
939 None
940 };
941 if std::fs::copy(from, to).is_err() {
942 return PerlValue::integer(0);
943 }
944 if let Some((at, mt)) = times {
945 let _ = utime_paths(at, mt, &[to.to_string()]);
946 }
947 PerlValue::integer(1)
948}
949
950pub fn path_basename(path: &str) -> String {
952 Path::new(path)
953 .file_name()
954 .map(|s| s.to_string_lossy().into_owned())
955 .unwrap_or_default()
956}
957
958pub fn path_dirname(path: &str) -> String {
960 if path.is_empty() {
961 return String::new();
962 }
963 let p = Path::new(path);
964 if path == "/" {
965 return "/".to_string();
966 }
967 match p.parent() {
968 None => ".".to_string(),
969 Some(parent) => {
970 let s = parent.to_string_lossy();
971 if s.is_empty() {
972 ".".to_string()
973 } else {
974 s.into_owned()
975 }
976 }
977 }
978}
979
980pub fn fileparse_path(path: &str, suffix: Option<&str>) -> (String, String, String) {
984 let dir = path_dirname(path);
985 let full_base = path_basename(path);
986 let (base, sfx) = if let Some(suf) = suffix.filter(|s| !s.is_empty()) {
987 if full_base.ends_with(suf) && full_base.len() > suf.len() {
988 (
989 full_base[..full_base.len() - suf.len()].to_string(),
990 suf.to_string(),
991 )
992 } else {
993 (full_base.clone(), String::new())
994 }
995 } else {
996 (full_base.clone(), String::new())
997 };
998 (base, dir, sfx)
999}
1000
1001pub fn chmod_paths(paths: &[String], mode: i64) -> i64 {
1003 #[cfg(unix)]
1004 {
1005 use std::os::unix::fs::PermissionsExt;
1006 let mut count = 0i64;
1007 for path in paths {
1008 if let Ok(meta) = std::fs::metadata(path) {
1009 let mut perms = meta.permissions();
1010 let old = perms.mode();
1011 perms.set_mode((old & !0o777) | (mode as u32 & 0o777));
1013 if std::fs::set_permissions(path, perms).is_ok() {
1014 count += 1;
1015 }
1016 }
1017 }
1018 count
1019 }
1020 #[cfg(not(unix))]
1021 {
1022 let _ = (paths, mode);
1023 0
1024 }
1025}
1026
1027pub fn utime_paths(atime_sec: i64, mtime_sec: i64, paths: &[String]) -> i64 {
1029 #[cfg(unix)]
1030 {
1031 use std::ffi::CString;
1032 let mut count = 0i64;
1033 let tv = [
1034 libc::timeval {
1035 tv_sec: atime_sec as libc::time_t,
1036 tv_usec: 0,
1037 },
1038 libc::timeval {
1039 tv_sec: mtime_sec as libc::time_t,
1040 tv_usec: 0,
1041 },
1042 ];
1043 for path in paths {
1044 let Ok(cs) = CString::new(path.as_str()) else {
1045 continue;
1046 };
1047 if unsafe { libc::utimes(cs.as_ptr(), tv.as_ptr()) } == 0 {
1048 count += 1;
1049 }
1050 }
1051 count
1052 }
1053 #[cfg(not(unix))]
1054 {
1055 let _ = (atime_sec, mtime_sec, paths);
1056 0
1057 }
1058}
1059
1060pub fn chown_paths(paths: &[String], uid: i64, gid: i64) -> i64 {
1062 #[cfg(unix)]
1063 {
1064 use std::ffi::CString;
1065 let u = if uid < 0 {
1066 (!0u32) as libc::uid_t
1067 } else {
1068 uid as libc::uid_t
1069 };
1070 let g = if gid < 0 {
1071 (!0u32) as libc::gid_t
1072 } else {
1073 gid as libc::gid_t
1074 };
1075 let mut count = 0i64;
1076 for path in paths {
1077 let Ok(c) = CString::new(path.as_str()) else {
1078 continue;
1079 };
1080 let r = unsafe { libc::chown(c.as_ptr(), u, g) };
1081 if r == 0 {
1082 count += 1;
1083 }
1084 }
1085 count
1086 }
1087 #[cfg(not(unix))]
1088 {
1089 let _ = (paths, uid, gid);
1090 0
1091 }
1092}
1093
1094pub fn touch_paths(paths: &[String]) -> i64 {
1097 use std::fs::OpenOptions;
1098 let mut count = 0i64;
1099 for path in paths {
1100 if path.is_empty() {
1101 continue;
1102 }
1103 let created = OpenOptions::new()
1105 .create(true)
1106 .append(true)
1107 .open(path)
1108 .is_ok();
1109 if !created {
1110 continue;
1111 }
1112 #[cfg(unix)]
1114 {
1115 use std::ffi::CString;
1116 if let Ok(cs) = CString::new(path.as_str()) {
1117 unsafe { libc::utimes(cs.as_ptr(), std::ptr::null()) };
1119 }
1120 }
1121 count += 1;
1122 }
1123 count
1124}
1125
1126#[cfg(test)]
1127mod tests {
1128 use super::*;
1129 use std::collections::HashSet;
1130
1131 #[test]
1132 fn glob_par_matches_sequential_glob_set() {
1133 let base = std::env::temp_dir().join(format!("stryke_glob_par_{}", std::process::id()));
1134 let _ = std::fs::remove_dir_all(&base);
1135 std::fs::create_dir_all(base.join("a")).unwrap();
1136 std::fs::create_dir_all(base.join("b")).unwrap();
1137 std::fs::create_dir_all(base.join("b/nested")).unwrap();
1138 std::fs::File::create(base.join("a/x.log")).unwrap();
1139 std::fs::File::create(base.join("b/y.log")).unwrap();
1140 std::fs::File::create(base.join("b/nested/z.log")).unwrap();
1141 std::fs::File::create(base.join("root.txt")).unwrap();
1142
1143 let pat = format!("{}/**/*.log", base.display());
1145 let a = glob_patterns(std::slice::from_ref(&pat));
1146 let b = glob_par_patterns(std::slice::from_ref(&pat));
1147 let _ = std::fs::remove_dir_all(&base);
1148
1149 let set_a: HashSet<String> = a
1150 .as_array_vec()
1151 .expect("expected array")
1152 .into_iter()
1153 .map(|x| x.to_string())
1154 .collect();
1155 let set_b: HashSet<String> = b
1156 .as_array_vec()
1157 .expect("expected array")
1158 .into_iter()
1159 .map(|x| x.to_string())
1160 .collect();
1161 assert_eq!(set_a, set_b);
1162 }
1163
1164 #[test]
1165 fn glob_par_src_rs_matches_when_src_tree_present() {
1166 let root = Path::new(env!("CARGO_MANIFEST_DIR"));
1167 let src = root.join("src");
1168 if !src.is_dir() {
1169 return;
1170 }
1171 let pat = src.join("*.rs").to_string_lossy().into_owned();
1172 let v = glob_par_patterns(&[pat])
1173 .as_array_vec()
1174 .expect("expected array");
1175 assert!(
1176 !v.is_empty(),
1177 "glob_par src/*.rs should find at least one .rs under src/"
1178 );
1179 }
1180
1181 #[test]
1182 fn glob_par_progress_false_same_as_plain() {
1183 let tmp = Path::new(env!("CARGO_MANIFEST_DIR"))
1184 .join("target")
1185 .join(format!("glob_par_prog_false_{}", std::process::id()));
1186 let _ = std::fs::remove_dir_all(&tmp);
1187 std::fs::create_dir_all(&tmp).unwrap();
1188 std::fs::write(tmp.join("probe.rs"), b"// x\n").unwrap();
1189 let pat = tmp.join("*.rs").to_string_lossy().replace('\\', "/");
1190 let a = glob_par_patterns(std::slice::from_ref(&pat));
1191 let b = glob_par_patterns_with_progress(std::slice::from_ref(&pat), false);
1192 let _ = std::fs::remove_dir_all(&tmp);
1193 let va = a.as_array_vec().expect("a");
1194 let vb = b.as_array_vec().expect("b");
1195 assert_eq!(va.len(), vb.len(), "glob_par vs glob_par(..., progress=>0)");
1196 for (x, y) in va.iter().zip(vb.iter()) {
1197 assert_eq!(x.to_string(), y.to_string());
1198 }
1199 }
1200
1201 #[test]
1202 fn read_file_text_perl_compat_maps_invalid_utf8_to_latin1_octets() {
1203 let path = std::env::temp_dir().join(format!("stryke_bad_utf8_{}.txt", std::process::id()));
1204 std::fs::write(&path, b"ok\xff\xfe\x80\n").unwrap();
1206 let s = read_file_text_perl_compat(&path).expect("read");
1207 assert!(s.starts_with("ok"));
1208 assert_eq!(&s[2..], "\u{00ff}\u{00fe}\u{0080}\n");
1209 let _ = std::fs::remove_file(&path);
1210 }
1211
1212 #[test]
1213 fn read_logical_line_perl_compat_splits_and_decodes_per_line() {
1214 use std::io::Cursor;
1215 let mut r = Cursor::new(b"a\xff\nb\n");
1216 assert_eq!(
1217 read_logical_line_perl_compat(&mut r).unwrap(),
1218 Some("a\u{00ff}".to_string())
1219 );
1220 assert_eq!(
1221 read_logical_line_perl_compat(&mut r).unwrap(),
1222 Some("b".to_string())
1223 );
1224 assert_eq!(read_logical_line_perl_compat(&mut r).unwrap(), None);
1225 }
1226}