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