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_executables(dir: &str) -> PerlValue {
606 let mut names: Vec<String> = Vec::new();
607 #[cfg(unix)]
608 {
609 if let Ok(entries) = std::fs::read_dir(dir) {
610 for entry in entries.flatten() {
611 if entry.file_type().map(|ft| ft.is_file()).unwrap_or(false)
612 && unix_path_executable(&entry.path())
613 {
614 if let Some(name) = entry.file_name().to_str() {
615 names.push(name.to_string());
616 }
617 }
618 }
619 }
620 }
621 #[cfg(not(unix))]
622 {
623 if let Ok(entries) = std::fs::read_dir(dir) {
624 for entry in entries.flatten() {
625 if entry.file_type().map(|ft| ft.is_file()).unwrap_or(false) {
626 let p = entry.path();
627 if let Some(ext) = p.extension() {
628 if ext == "exe" || ext == "bat" || ext == "cmd" {
629 if let Some(name) = entry.file_name().to_str() {
630 names.push(name.to_string());
631 }
632 }
633 }
634 }
635 }
636 }
637 }
638 let _ = dir;
639 names.sort();
640 PerlValue::array(names.into_iter().map(PerlValue::string).collect())
641}
642
643pub fn list_char_devices(dir: &str) -> PerlValue {
646 let mut names: Vec<String> = Vec::new();
647 #[cfg(unix)]
648 {
649 use std::os::unix::fs::FileTypeExt;
650 if let Ok(entries) = std::fs::read_dir(dir) {
651 for entry in entries.flatten() {
652 if entry
653 .file_type()
654 .map(|ft| ft.is_char_device())
655 .unwrap_or(false)
656 {
657 if let Some(name) = entry.file_name().to_str() {
658 names.push(name.to_string());
659 }
660 }
661 }
662 }
663 }
664 let _ = dir;
665 names.sort();
666 PerlValue::array(names.into_iter().map(PerlValue::string).collect())
667}
668
669pub fn glob_patterns(patterns: &[String]) -> PerlValue {
670 let mut paths: Vec<String> = Vec::new();
671 for pat in patterns {
672 if let Ok(g) = glob::glob(pat) {
673 for e in g.flatten() {
674 paths.push(normalize_glob_path_display(
675 e.to_string_lossy().into_owned(),
676 ));
677 }
678 }
679 }
680 paths.sort();
681 paths.dedup();
682 PerlValue::array(paths.into_iter().map(PerlValue::string).collect())
683}
684
685fn glob_base_path(pat: &str) -> PathBuf {
687 let p = Path::new(pat);
688 let mut acc = PathBuf::new();
689 for c in p.components() {
690 let s = c.as_os_str().to_string_lossy();
691 if s.contains('*') || s.contains('?') || s.contains('[') {
692 break;
693 }
694 acc.push(c.as_os_str());
695 }
696 if acc.as_os_str().is_empty() {
697 PathBuf::from(".")
698 } else {
699 acc
700 }
701}
702
703fn glob_par_walk(dir: &Path, pattern: &Pattern, options: &MatchOptions) -> Vec<String> {
704 let read = match std::fs::read_dir(dir) {
705 Ok(r) => r,
706 Err(_) => return Vec::new(),
707 };
708 let entries: Vec<_> = read.filter_map(|e| e.ok()).collect();
709 entries
710 .par_iter()
711 .flat_map_iter(|e| {
712 let path = e.path();
713 let mut out = Vec::new();
714 let s = path.to_string_lossy();
715 if pattern.matches_with(s.as_ref(), *options) {
716 out.push(s.into_owned());
717 }
718 if path.is_dir() {
719 out.extend(glob_par_walk(&path, pattern, options));
720 }
721 out.into_iter()
722 })
723 .collect()
724}
725
726pub fn glob_par_patterns(patterns: &[String]) -> PerlValue {
729 glob_par_patterns_inner(patterns, None)
730}
731
732pub fn glob_par_patterns_with_progress(patterns: &[String], progress: bool) -> PerlValue {
735 if patterns.is_empty() {
736 return PerlValue::array(Vec::new());
737 }
738 let pmap = PmapProgress::new(progress, patterns.len());
739 let v = glob_par_patterns_inner(patterns, Some(&pmap));
740 pmap.finish();
741 v
742}
743
744fn glob_par_patterns_inner(patterns: &[String], progress: Option<&PmapProgress>) -> PerlValue {
745 let options = MatchOptions::new();
746 let out: Vec<String> = patterns
747 .par_iter()
748 .flat_map_iter(|pat| {
749 let rows = (|| {
750 let Ok(pattern) = Pattern::new(pat) else {
751 return Vec::new();
752 };
753 let base = glob_base_path(pat);
754 if !base.exists() {
755 return Vec::new();
756 }
757 glob_par_walk(&base, &pattern, &options)
758 })();
759 if let Some(p) = progress {
760 p.tick();
761 }
762 rows
763 })
764 .collect();
765 let mut paths: Vec<String> = out.into_iter().map(normalize_glob_path_display).collect();
766 paths.sort();
767 paths.dedup();
768 PerlValue::array(paths.into_iter().map(PerlValue::string).collect())
769}
770
771fn normalize_glob_path_display(s: String) -> String {
773 let p = Path::new(&s);
774 if p.is_absolute() || s.starts_with("./") || s.starts_with("../") {
775 s
776 } else {
777 format!("./{s}")
778 }
779}
780
781pub fn rename_paths(old: &str, new: &str) -> PerlValue {
783 PerlValue::integer(if std::fs::rename(old, new).is_ok() {
784 1
785 } else {
786 0
787 })
788}
789
790#[inline]
791fn is_cross_device_rename(e: &io::Error) -> bool {
792 if e.kind() == io::ErrorKind::CrossesDevices {
793 return true;
794 }
795 #[cfg(unix)]
796 {
797 if e.raw_os_error() == Some(libc::EXDEV) {
798 return true;
799 }
800 }
801 false
802}
803
804fn try_move_path(from: &str, to: &str) -> io::Result<()> {
805 match std::fs::rename(from, to) {
806 Ok(()) => Ok(()),
807 Err(e) => {
808 if !is_cross_device_rename(&e) {
809 return Err(e);
810 }
811 let meta = std::fs::symlink_metadata(from)?;
812 if meta.is_dir() {
813 return Err(io::Error::new(
814 io::ErrorKind::Unsupported,
815 "move: cross-device directory move is not supported",
816 ));
817 }
818 if !meta.is_file() && !meta.is_symlink() {
819 return Err(io::Error::new(
820 io::ErrorKind::Unsupported,
821 "move: cross-device move supports files and symlinks only",
822 ));
823 }
824 std::fs::copy(from, to)?;
825 std::fs::remove_file(from)?;
826 Ok(())
827 }
828 }
829}
830
831pub fn move_path(from: &str, to: &str) -> PerlValue {
834 PerlValue::integer(if try_move_path(from, to).is_ok() {
835 1
836 } else {
837 0
838 })
839}
840
841#[cfg(unix)]
842fn unix_path_executable(path: &Path) -> bool {
843 use std::os::unix::fs::PermissionsExt;
844 std::fs::metadata(path)
845 .ok()
846 .filter(|m| m.is_file())
847 .is_some_and(|m| m.permissions().mode() & 0o111 != 0)
848}
849
850#[cfg(not(unix))]
851fn unix_path_executable(path: &Path) -> bool {
852 path.is_file()
853}
854
855fn display_executable_path(path: &Path) -> Option<String> {
856 if !unix_path_executable(path) {
857 return None;
858 }
859 path.canonicalize()
860 .ok()
861 .map(|p| p.to_string_lossy().into_owned())
862 .or_else(|| Some(path.to_string_lossy().into_owned()))
863}
864
865#[cfg(windows)]
866fn pathext_suffixes() -> Vec<String> {
867 env::var_os("PATHEXT")
868 .map(|s| {
869 env::split_paths(&s)
870 .filter_map(|p| p.to_str().map(str::to_ascii_lowercase))
871 .collect()
872 })
873 .unwrap_or_else(|| vec![".exe".into(), ".cmd".into(), ".bat".into(), ".com".into()])
874}
875
876#[cfg(windows)]
877fn which_in_dir(dir: &Path, program: &str) -> Option<String> {
878 let plain = dir.join(program);
879 if let Some(s) = display_executable_path(&plain) {
880 return Some(s);
881 }
882 if !program.contains('.') {
883 for ext in pathext_suffixes() {
884 let cand = dir.join(format!("{program}{ext}"));
885 if let Some(s) = display_executable_path(&cand) {
886 return Some(s);
887 }
888 }
889 }
890 None
891}
892
893#[cfg(not(windows))]
894fn which_in_dir(dir: &Path, program: &str) -> Option<String> {
895 display_executable_path(&dir.join(program))
896}
897
898pub fn which_executable(program: &str, include_dot: bool) -> Option<String> {
901 if program.is_empty() {
902 return None;
903 }
904 if program.contains('/') || (cfg!(windows) && program.contains('\\')) {
905 return display_executable_path(Path::new(program));
906 }
907 let path_os = env::var_os("PATH")?;
908 for dir in env::split_paths(&path_os) {
909 if let Some(s) = which_in_dir(&dir, program) {
910 return Some(s);
911 }
912 }
913 if include_dot {
914 return which_in_dir(Path::new("."), program);
915 }
916 None
917}
918
919pub fn read_file_bytes(path: &str) -> io::Result<Arc<Vec<u8>>> {
921 Ok(Arc::new(std::fs::read(path)?))
922}
923
924fn adjacent_temp_path(target: &Path) -> PathBuf {
926 let dir = target.parent().unwrap_or_else(|| Path::new("."));
927 let name = target
928 .file_name()
929 .map(|s| s.to_string_lossy().into_owned())
930 .unwrap_or_else(|| "file".to_string());
931 let rnd: u32 = rand::thread_rng().gen();
932 dir.join(format!("{name}.spurt-tmp-{rnd}"))
933}
934
935pub fn spurt_path(path: &str, data: &[u8], mkdir_parents: bool, atomic: bool) -> io::Result<()> {
938 let path = Path::new(path);
939 if mkdir_parents {
940 if let Some(parent) = path.parent() {
941 if !parent.as_os_str().is_empty() {
942 std::fs::create_dir_all(parent)?;
943 }
944 }
945 }
946 if !atomic {
947 return std::fs::write(path, data);
948 }
949 let tmp = adjacent_temp_path(path);
950 {
951 let mut f = std::fs::File::create(&tmp)?;
952 f.write_all(data)?;
953 f.sync_all().ok();
954 }
955 std::fs::rename(&tmp, path)?;
956 Ok(())
957}
958
959pub fn copy_file(from: &str, to: &str, preserve_metadata: bool) -> PerlValue {
962 let times = if preserve_metadata {
963 std::fs::metadata(from).ok().map(|src_meta| {
964 let at = src_meta
965 .accessed()
966 .ok()
967 .and_then(|t| t.duration_since(UNIX_EPOCH).ok())
968 .map(|d| d.as_secs() as i64)
969 .unwrap_or(0);
970 let mt = src_meta
971 .modified()
972 .ok()
973 .and_then(|t| t.duration_since(UNIX_EPOCH).ok())
974 .map(|d| d.as_secs() as i64)
975 .unwrap_or(0);
976 (at, mt)
977 })
978 } else {
979 None
980 };
981 if std::fs::copy(from, to).is_err() {
982 return PerlValue::integer(0);
983 }
984 if let Some((at, mt)) = times {
985 let _ = utime_paths(at, mt, &[to.to_string()]);
986 }
987 PerlValue::integer(1)
988}
989
990pub fn path_basename(path: &str) -> String {
992 Path::new(path)
993 .file_name()
994 .map(|s| s.to_string_lossy().into_owned())
995 .unwrap_or_default()
996}
997
998pub fn path_dirname(path: &str) -> String {
1000 if path.is_empty() {
1001 return String::new();
1002 }
1003 let p = Path::new(path);
1004 if path == "/" {
1005 return "/".to_string();
1006 }
1007 match p.parent() {
1008 None => ".".to_string(),
1009 Some(parent) => {
1010 let s = parent.to_string_lossy();
1011 if s.is_empty() {
1012 ".".to_string()
1013 } else {
1014 s.into_owned()
1015 }
1016 }
1017 }
1018}
1019
1020pub fn fileparse_path(path: &str, suffix: Option<&str>) -> (String, String, String) {
1024 let dir = path_dirname(path);
1025 let full_base = path_basename(path);
1026 let (base, sfx) = if let Some(suf) = suffix.filter(|s| !s.is_empty()) {
1027 if full_base.ends_with(suf) && full_base.len() > suf.len() {
1028 (
1029 full_base[..full_base.len() - suf.len()].to_string(),
1030 suf.to_string(),
1031 )
1032 } else {
1033 (full_base.clone(), String::new())
1034 }
1035 } else {
1036 (full_base.clone(), String::new())
1037 };
1038 (base, dir, sfx)
1039}
1040
1041pub fn chmod_paths(paths: &[String], mode: i64) -> i64 {
1043 #[cfg(unix)]
1044 {
1045 use std::os::unix::fs::PermissionsExt;
1046 let mut count = 0i64;
1047 for path in paths {
1048 if let Ok(meta) = std::fs::metadata(path) {
1049 let mut perms = meta.permissions();
1050 let old = perms.mode();
1051 perms.set_mode((old & !0o777) | (mode as u32 & 0o777));
1053 if std::fs::set_permissions(path, perms).is_ok() {
1054 count += 1;
1055 }
1056 }
1057 }
1058 count
1059 }
1060 #[cfg(not(unix))]
1061 {
1062 let _ = (paths, mode);
1063 0
1064 }
1065}
1066
1067pub fn utime_paths(atime_sec: i64, mtime_sec: i64, paths: &[String]) -> i64 {
1069 #[cfg(unix)]
1070 {
1071 use std::ffi::CString;
1072 let mut count = 0i64;
1073 let tv = [
1074 libc::timeval {
1075 tv_sec: atime_sec as libc::time_t,
1076 tv_usec: 0,
1077 },
1078 libc::timeval {
1079 tv_sec: mtime_sec as libc::time_t,
1080 tv_usec: 0,
1081 },
1082 ];
1083 for path in paths {
1084 let Ok(cs) = CString::new(path.as_str()) else {
1085 continue;
1086 };
1087 if unsafe { libc::utimes(cs.as_ptr(), tv.as_ptr()) } == 0 {
1088 count += 1;
1089 }
1090 }
1091 count
1092 }
1093 #[cfg(not(unix))]
1094 {
1095 let _ = (atime_sec, mtime_sec, paths);
1096 0
1097 }
1098}
1099
1100pub fn chown_paths(paths: &[String], uid: i64, gid: i64) -> i64 {
1102 #[cfg(unix)]
1103 {
1104 use std::ffi::CString;
1105 let u = if uid < 0 {
1106 (!0u32) as libc::uid_t
1107 } else {
1108 uid as libc::uid_t
1109 };
1110 let g = if gid < 0 {
1111 (!0u32) as libc::gid_t
1112 } else {
1113 gid as libc::gid_t
1114 };
1115 let mut count = 0i64;
1116 for path in paths {
1117 let Ok(c) = CString::new(path.as_str()) else {
1118 continue;
1119 };
1120 let r = unsafe { libc::chown(c.as_ptr(), u, g) };
1121 if r == 0 {
1122 count += 1;
1123 }
1124 }
1125 count
1126 }
1127 #[cfg(not(unix))]
1128 {
1129 let _ = (paths, uid, gid);
1130 0
1131 }
1132}
1133
1134pub fn touch_paths(paths: &[String]) -> i64 {
1137 use std::fs::OpenOptions;
1138 let mut count = 0i64;
1139 for path in paths {
1140 if path.is_empty() {
1141 continue;
1142 }
1143 let created = OpenOptions::new()
1145 .create(true)
1146 .append(true)
1147 .open(path)
1148 .is_ok();
1149 if !created {
1150 continue;
1151 }
1152 #[cfg(unix)]
1154 {
1155 use std::ffi::CString;
1156 if let Ok(cs) = CString::new(path.as_str()) {
1157 unsafe { libc::utimes(cs.as_ptr(), std::ptr::null()) };
1159 }
1160 }
1161 count += 1;
1162 }
1163 count
1164}
1165
1166#[cfg(test)]
1167mod tests {
1168 use super::*;
1169 use std::collections::HashSet;
1170
1171 #[test]
1172 fn glob_par_matches_sequential_glob_set() {
1173 let base = std::env::temp_dir().join(format!("stryke_glob_par_{}", std::process::id()));
1174 let _ = std::fs::remove_dir_all(&base);
1175 std::fs::create_dir_all(base.join("a")).unwrap();
1176 std::fs::create_dir_all(base.join("b")).unwrap();
1177 std::fs::create_dir_all(base.join("b/nested")).unwrap();
1178 std::fs::File::create(base.join("a/x.log")).unwrap();
1179 std::fs::File::create(base.join("b/y.log")).unwrap();
1180 std::fs::File::create(base.join("b/nested/z.log")).unwrap();
1181 std::fs::File::create(base.join("root.txt")).unwrap();
1182
1183 let pat = format!("{}/**/*.log", base.display());
1185 let a = glob_patterns(std::slice::from_ref(&pat));
1186 let b = glob_par_patterns(std::slice::from_ref(&pat));
1187 let _ = std::fs::remove_dir_all(&base);
1188
1189 let set_a: HashSet<String> = a
1190 .as_array_vec()
1191 .expect("expected array")
1192 .into_iter()
1193 .map(|x| x.to_string())
1194 .collect();
1195 let set_b: HashSet<String> = b
1196 .as_array_vec()
1197 .expect("expected array")
1198 .into_iter()
1199 .map(|x| x.to_string())
1200 .collect();
1201 assert_eq!(set_a, set_b);
1202 }
1203
1204 #[test]
1205 fn glob_par_src_rs_matches_when_src_tree_present() {
1206 let root = Path::new(env!("CARGO_MANIFEST_DIR"));
1207 let src = root.join("src");
1208 if !src.is_dir() {
1209 return;
1210 }
1211 let pat = src.join("*.rs").to_string_lossy().into_owned();
1212 let v = glob_par_patterns(&[pat])
1213 .as_array_vec()
1214 .expect("expected array");
1215 assert!(
1216 !v.is_empty(),
1217 "glob_par src/*.rs should find at least one .rs under src/"
1218 );
1219 }
1220
1221 #[test]
1222 fn glob_par_progress_false_same_as_plain() {
1223 let tmp = Path::new(env!("CARGO_MANIFEST_DIR"))
1224 .join("target")
1225 .join(format!("glob_par_prog_false_{}", std::process::id()));
1226 let _ = std::fs::remove_dir_all(&tmp);
1227 std::fs::create_dir_all(&tmp).unwrap();
1228 std::fs::write(tmp.join("probe.rs"), b"// x\n").unwrap();
1229 let pat = tmp.join("*.rs").to_string_lossy().replace('\\', "/");
1230 let a = glob_par_patterns(std::slice::from_ref(&pat));
1231 let b = glob_par_patterns_with_progress(std::slice::from_ref(&pat), false);
1232 let _ = std::fs::remove_dir_all(&tmp);
1233 let va = a.as_array_vec().expect("a");
1234 let vb = b.as_array_vec().expect("b");
1235 assert_eq!(va.len(), vb.len(), "glob_par vs glob_par(..., progress=>0)");
1236 for (x, y) in va.iter().zip(vb.iter()) {
1237 assert_eq!(x.to_string(), y.to_string());
1238 }
1239 }
1240
1241 #[test]
1242 fn read_file_text_perl_compat_maps_invalid_utf8_to_latin1_octets() {
1243 let path = std::env::temp_dir().join(format!("stryke_bad_utf8_{}.txt", std::process::id()));
1244 std::fs::write(&path, b"ok\xff\xfe\x80\n").unwrap();
1246 let s = read_file_text_perl_compat(&path).expect("read");
1247 assert!(s.starts_with("ok"));
1248 assert_eq!(&s[2..], "\u{00ff}\u{00fe}\u{0080}\n");
1249 let _ = std::fs::remove_file(&path);
1250 }
1251
1252 #[test]
1253 fn read_logical_line_perl_compat_splits_and_decodes_per_line() {
1254 use std::io::Cursor;
1255 let mut r = Cursor::new(b"a\xff\nb\n");
1256 assert_eq!(
1257 read_logical_line_perl_compat(&mut r).unwrap(),
1258 Some("a\u{00ff}".to_string())
1259 );
1260 assert_eq!(
1261 read_logical_line_perl_compat(&mut r).unwrap(),
1262 Some("b".to_string())
1263 );
1264 assert_eq!(read_logical_line_perl_compat(&mut r).unwrap(), None);
1265 }
1266}