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::StrykeValue;
13use parking_lot::Mutex;
14
15static ZSH_GLOB_GLOBAL_MUTEX: Mutex<()> = Mutex::new(());
22
23const STRYKE_GLOB_OPT_KEYS: &[&str] = &[
25 "nullglob",
26 "markdirs",
27 "dotglob",
28 "globdots",
29 "listtypes",
30 "numericglobsort",
31 "chaselinks",
32 "extendedglob",
33 "caseglob",
34 "nocaseglob",
35 "globstarshort",
36 "bareglobqual",
37 "braceccl",
38];
39
40pub(crate) struct StrykeGlobOptsGuard {
43 snap: std::collections::HashMap<String, bool>,
44}
45
46impl StrykeGlobOptsGuard {
47 pub(crate) fn new() -> Self {
48 let snap = zsh::options::opt_state_snapshot();
49 zsh::options::opt_state_set("nullglob", true);
50 zsh::options::opt_state_set("markdirs", false);
51 zsh::options::opt_state_set("dotglob", false);
52 zsh::options::opt_state_set("globdots", false);
53 zsh::options::opt_state_set("listtypes", false);
54 zsh::options::opt_state_set("numericglobsort", false);
55 zsh::options::opt_state_set("chaselinks", false);
56 zsh::options::opt_state_set("extendedglob", true);
57 zsh::options::opt_state_set("caseglob", true);
58 zsh::options::opt_state_set("nocaseglob", false);
59 zsh::options::opt_state_set("globstarshort", true);
60 zsh::options::opt_state_set("bareglobqual", true);
61 zsh::options::opt_state_set("braceccl", true);
62 Self { snap }
63 }
64}
65
66impl Drop for StrykeGlobOptsGuard {
67 fn drop(&mut self) {
68 for &key in STRYKE_GLOB_OPT_KEYS {
69 if let Some(v) = self.snap.get(key) {
70 zsh::options::opt_state_set(key, *v);
71 } else {
72 zsh::options::opt_state_unset(key);
73 }
74 }
75 }
76}
77
78pub use crate::perl_decode::{
79 decode_utf8_or_latin1, decode_utf8_or_latin1_line, decode_utf8_or_latin1_read_until,
80};
81
82pub fn read_file_text_perl_compat(path: impl AsRef<Path>) -> io::Result<String> {
85 let bytes = std::fs::read(path.as_ref())?;
86 Ok(decode_utf8_or_latin1(&bytes))
87}
88
89pub(crate) fn stryke_glob(pattern: &str) -> Vec<String> {
96 let (stripped_leading, had_dot_slash) = if let Some(rest) = pattern.strip_prefix("./") {
106 (rest.to_string(), true)
107 } else {
108 (pattern.to_string(), false)
109 };
110 let normalized = stripped_leading.replace("/./", "/");
111 let is_relative_pattern = !std::path::Path::new(&normalized).is_absolute();
112 let results = {
113 let _lock = ZSH_GLOB_GLOBAL_MUTEX.lock();
114 let _opts = StrykeGlobOptsGuard::new();
115 zsh::glob::glob(&normalized)
116 };
117 let stripped: Vec<String> = if is_relative_pattern {
124 let cwd = std::env::current_dir().ok();
125 let cwd_canonical = cwd
126 .as_ref()
127 .and_then(|p| std::fs::canonicalize(p).ok())
128 .map(|p| p.to_string_lossy().into_owned());
129 let cwd_plain = cwd.as_ref().map(|p| p.to_string_lossy().into_owned());
130 results
131 .into_iter()
132 .map(|p| {
133 for pref in [cwd_canonical.as_deref(), cwd_plain.as_deref()]
134 .into_iter()
135 .flatten()
136 {
137 if let Some(rest) = p.strip_prefix(pref) {
138 let r = rest.trim_start_matches('/');
139 return if r.is_empty() {
140 ".".to_string()
141 } else {
142 r.to_string()
143 };
144 }
145 }
146 p
147 })
148 .collect()
149 } else {
150 results
151 };
152 if had_dot_slash {
153 stripped
154 .into_iter()
155 .map(|p| {
156 if p.starts_with("./") {
157 p
158 } else {
159 format!("./{}", p)
160 }
161 })
162 .collect()
163 } else {
164 stripped
165 }
166}
167
168pub fn read_file_text_or_glob(path: &str) -> io::Result<String> {
182 let (stripped, qual) = zsh::glob::split_qualifier(path);
183 let is_glob = qual.is_some() || zsh::glob::haswilds(stripped);
184 if !is_glob {
185 return read_file_text_perl_compat(path);
186 }
187 let paths = stryke_glob(path);
188 if paths.is_empty() {
189 return Err(io::Error::new(
190 io::ErrorKind::NotFound,
191 format!("no files matched glob: {}", path),
192 ));
193 }
194 let mut out = String::new();
195 for p in &paths {
196 let meta = std::fs::metadata(p)?;
197 if !meta.is_file() {
198 return Err(io::Error::new(
199 io::ErrorKind::InvalidInput,
200 format!("slurp: not a regular file: {}", p),
201 ));
202 }
203 out.push_str(&read_file_text_perl_compat(p)?);
204 }
205 Ok(out)
206}
207
208fn pattern_is_glob(path: &str) -> bool {
213 let (stripped, qual) = zsh::glob::split_qualifier(path);
214 qual.is_some() || zsh::glob::haswilds(stripped) || zsh::glob::hasbraces(stripped, true)
215}
216
217pub fn read_line_perl_compat(reader: &mut impl BufRead, buf: &mut String) -> io::Result<usize> {
219 buf.clear();
220 let mut raw = Vec::new();
221 let n = reader.read_until(b'\n', &mut raw)?;
222 if n == 0 {
223 return Ok(0);
224 }
225 buf.push_str(&decode_utf8_or_latin1_read_until(&raw));
226 Ok(n)
227}
228
229pub fn read_logical_line_perl_compat(reader: &mut impl BufRead) -> io::Result<Option<String>> {
232 let mut buf = Vec::new();
233 let n = reader.read_until(b'\n', &mut buf)?;
234 if n == 0 {
235 return Ok(None);
236 }
237 if buf.ends_with(b"\n") {
238 buf.pop();
239 if buf.ends_with(b"\r") {
240 buf.pop();
241 }
242 }
243 Ok(Some(decode_utf8_or_latin1_line(&buf)))
244}
245
246pub fn filetest_is_tty(path: &str) -> bool {
249 #[cfg(unix)]
250 {
251 use std::os::unix::io::AsRawFd;
252 if let Some(fd) = tty_fd_literal(path) {
253 return unsafe { libc::isatty(fd) != 0 };
254 }
255 if let Ok(f) = std::fs::File::open(path) {
256 return unsafe { libc::isatty(f.as_raw_fd()) != 0 };
257 }
258 }
259 #[cfg(not(unix))]
260 {
261 let _ = path;
262 }
263 false
264}
265
266#[cfg(unix)]
267fn tty_fd_literal(path: &str) -> Option<i32> {
268 match path {
269 "" | "STDIN" | "-" | "/dev/stdin" => Some(0),
270 "STDOUT" | "/dev/stdout" => Some(1),
271 "STDERR" | "/dev/stderr" => Some(2),
272 p if p.starts_with("/dev/fd/") => p.strip_prefix("/dev/fd/").and_then(|s| s.parse().ok()),
273 _ => path.parse::<i32>().ok().filter(|&n| (0..128).contains(&n)),
274 }
275}
276
277#[cfg(unix)]
280pub fn filetest_effective_access(path: &str, check: u32) -> bool {
281 use std::os::unix::fs::MetadataExt;
282 let meta = match std::fs::metadata(path) {
283 Ok(m) => m,
284 Err(_) => return false,
285 };
286 let mode = meta.mode();
287 let euid = unsafe { libc::geteuid() };
288 let egid = unsafe { libc::getegid() };
289 if euid == 0 {
291 return if check == 1 { mode & 0o111 != 0 } else { true };
292 }
293 if meta.uid() == euid {
294 return mode & (check << 6) != 0;
295 }
296 if meta.gid() == egid {
297 return mode & (check << 3) != 0;
298 }
299 mode & check != 0
300}
301
302#[cfg(unix)]
304pub fn filetest_real_access(path: &str, amode: libc::c_int) -> bool {
305 match std::ffi::CString::new(path) {
306 Ok(c) => unsafe { libc::access(c.as_ptr(), amode) == 0 },
307 Err(_) => false,
308 }
309}
310
311#[cfg(unix)]
313pub fn filetest_owned_effective(path: &str) -> bool {
314 use std::os::unix::fs::MetadataExt;
315 std::fs::metadata(path)
316 .map(|m| m.uid() == unsafe { libc::geteuid() })
317 .unwrap_or(false)
318}
319
320#[cfg(unix)]
322pub fn filetest_owned_real(path: &str) -> bool {
323 use std::os::unix::fs::MetadataExt;
324 std::fs::metadata(path)
325 .map(|m| m.uid() == unsafe { libc::getuid() })
326 .unwrap_or(false)
327}
328
329#[cfg(unix)]
331pub fn filetest_is_pipe(path: &str) -> bool {
332 use std::os::unix::fs::FileTypeExt;
333 std::fs::metadata(path)
334 .map(|m| m.file_type().is_fifo())
335 .unwrap_or(false)
336}
337
338#[cfg(unix)]
340pub fn filetest_is_socket(path: &str) -> bool {
341 use std::os::unix::fs::FileTypeExt;
342 std::fs::metadata(path)
343 .map(|m| m.file_type().is_socket())
344 .unwrap_or(false)
345}
346
347#[cfg(unix)]
349pub fn filetest_is_block_device(path: &str) -> bool {
350 use std::os::unix::fs::FileTypeExt;
351 std::fs::metadata(path)
352 .map(|m| m.file_type().is_block_device())
353 .unwrap_or(false)
354}
355
356#[cfg(unix)]
358pub fn filetest_is_char_device(path: &str) -> bool {
359 use std::os::unix::fs::FileTypeExt;
360 std::fs::metadata(path)
361 .map(|m| m.file_type().is_char_device())
362 .unwrap_or(false)
363}
364
365#[cfg(unix)]
367pub fn filetest_is_setuid(path: &str) -> bool {
368 use std::os::unix::fs::MetadataExt;
369 std::fs::metadata(path)
370 .map(|m| m.mode() & 0o4000 != 0)
371 .unwrap_or(false)
372}
373
374#[cfg(unix)]
376pub fn filetest_is_setgid(path: &str) -> bool {
377 use std::os::unix::fs::MetadataExt;
378 std::fs::metadata(path)
379 .map(|m| m.mode() & 0o2000 != 0)
380 .unwrap_or(false)
381}
382
383#[cfg(unix)]
385pub fn filetest_is_sticky(path: &str) -> bool {
386 use std::os::unix::fs::MetadataExt;
387 std::fs::metadata(path)
388 .map(|m| m.mode() & 0o1000 != 0)
389 .unwrap_or(false)
390}
391
392pub fn filetest_is_text(path: &str) -> bool {
394 filetest_text_binary(path, true)
395}
396
397pub fn filetest_is_binary(path: &str) -> bool {
399 filetest_text_binary(path, false)
400}
401
402fn filetest_text_binary(path: &str, want_text: bool) -> bool {
403 use std::io::Read;
404 let mut f = match std::fs::File::open(path) {
405 Ok(f) => f,
406 Err(_) => return false,
407 };
408 let mut buf = [0u8; 512];
409 let n = match f.read(&mut buf) {
410 Ok(n) => n,
411 Err(_) => return false,
412 };
413 if n == 0 {
414 return want_text;
416 }
417 let slice = &buf[..n];
418 let non_text = slice
420 .iter()
421 .filter(|&&b| b == 0 || (b < 0x20 && b != b'\t' && b != b'\n' && b != b'\r' && b != 0x1b))
422 .count();
423 let is_text = (non_text as f64 / n as f64) < 0.30;
424 if want_text {
425 is_text
426 } else {
427 !is_text
428 }
429}
430
431#[cfg(unix)]
433pub fn filetest_age_days(path: &str, which: char) -> Option<f64> {
434 use std::os::unix::fs::MetadataExt;
435 let meta = std::fs::metadata(path).ok()?;
436 let t = match which {
437 'M' => meta.mtime() as f64,
438 'A' => meta.atime() as f64,
439 _ => meta.ctime() as f64,
440 };
441 let now = std::time::SystemTime::now()
442 .duration_since(std::time::UNIX_EPOCH)
443 .unwrap_or_default()
444 .as_secs_f64();
445 Some((now - t) / 86400.0)
446}
447
448pub fn stat_path(path: &str, symlink: bool) -> StrykeValue {
450 let res = if symlink {
451 std::fs::symlink_metadata(path)
452 } else {
453 std::fs::metadata(path)
454 };
455 match res {
456 Ok(meta) => StrykeValue::array(perl_stat_from_metadata(&meta)),
457 Err(_) => StrykeValue::array(vec![]),
458 }
459}
460
461pub fn perl_stat_from_metadata(meta: &std::fs::Metadata) -> Vec<StrykeValue> {
462 #[cfg(unix)]
463 {
464 use std::os::unix::fs::MetadataExt;
465 vec![
466 StrykeValue::integer(meta.dev() as i64),
467 StrykeValue::integer(meta.ino() as i64),
468 StrykeValue::integer(meta.mode() as i64),
469 StrykeValue::integer(meta.nlink() as i64),
470 StrykeValue::integer(meta.uid() as i64),
471 StrykeValue::integer(meta.gid() as i64),
472 StrykeValue::integer(meta.rdev() as i64),
473 StrykeValue::integer(meta.len() as i64),
474 StrykeValue::integer(meta.atime()),
475 StrykeValue::integer(meta.mtime()),
476 StrykeValue::integer(meta.ctime()),
477 StrykeValue::integer(meta.blksize() as i64),
478 StrykeValue::integer(meta.blocks() as i64),
479 ]
480 }
481 #[cfg(not(unix))]
482 {
483 let len = meta.len() as i64;
484 vec![
485 StrykeValue::integer(0),
486 StrykeValue::integer(0),
487 StrykeValue::integer(0),
488 StrykeValue::integer(0),
489 StrykeValue::integer(0),
490 StrykeValue::integer(0),
491 StrykeValue::integer(0),
492 StrykeValue::integer(len),
493 StrykeValue::integer(0),
494 StrykeValue::integer(0),
495 StrykeValue::integer(0),
496 StrykeValue::integer(0),
497 StrykeValue::integer(0),
498 ]
499 }
500}
501
502pub fn link_hard(old: &str, new: &str) -> StrykeValue {
503 StrykeValue::integer(if std::fs::hard_link(old, new).is_ok() {
504 1
505 } else {
506 0
507 })
508}
509
510pub fn link_sym(old: &str, new: &str) -> StrykeValue {
511 #[cfg(unix)]
512 {
513 use std::os::unix::fs::symlink;
514 StrykeValue::integer(if symlink(old, new).is_ok() { 1 } else { 0 })
515 }
516 #[cfg(not(unix))]
517 {
518 let _ = (old, new);
519 StrykeValue::integer(0)
520 }
521}
522
523pub fn read_link(path: &str) -> StrykeValue {
524 match std::fs::read_link(path) {
525 Ok(p) => StrykeValue::string(p.to_string_lossy().into_owned()),
526 Err(_) => StrykeValue::UNDEF,
527 }
528}
529
530pub fn realpath_resolved(path: &str) -> io::Result<String> {
532 std::fs::canonicalize(path).map(|p| p.to_string_lossy().into_owned())
533}
534
535pub fn canonpath_logical(path: &str) -> String {
539 use std::path::Component;
540 if path.is_empty() {
541 return String::new();
542 }
543 let mut stack: Vec<String> = Vec::new();
544 let mut anchored = false;
545 for c in Path::new(path).components() {
546 match c {
547 Component::Prefix(p) => {
548 stack.push(p.as_os_str().to_string_lossy().into_owned());
549 }
550 Component::RootDir => {
551 anchored = true;
552 stack.clear();
553 }
554 Component::CurDir => {}
555 Component::Normal(s) => {
556 stack.push(s.to_string_lossy().into_owned());
557 }
558 Component::ParentDir => {
559 if anchored {
560 if !stack.is_empty() {
561 stack.pop();
562 }
563 } else if stack.is_empty() || stack.last().is_some_and(|t| t == "..") {
564 stack.push("..".to_string());
565 } else {
566 stack.pop();
567 }
568 }
569 }
570 }
571 let body = stack.join("/");
572 if anchored {
573 if body.is_empty() {
574 "/".to_string()
575 } else {
576 format!("/{body}")
577 }
578 } else if body.is_empty() {
579 ".".to_string()
580 } else {
581 body
582 }
583}
584
585pub fn list_files(dir: &str) -> StrykeValue {
588 let mut names: Vec<String> = Vec::new();
589 if let Ok(entries) = std::fs::read_dir(dir) {
590 for entry in entries.flatten() {
591 if let Some(name) = entry.file_name().to_str() {
592 names.push(name.to_string());
593 }
594 }
595 }
596 names.sort();
597 StrykeValue::array(names.into_iter().map(StrykeValue::string).collect())
598}
599
600pub fn list_filesf(dir: &str) -> StrykeValue {
604 let mut names: Vec<String> = Vec::new();
605 if let Ok(entries) = std::fs::read_dir(dir) {
606 for entry in entries.flatten() {
607 if entry.file_type().map(|ft| ft.is_file()).unwrap_or(false) {
608 if let Some(name) = entry.file_name().to_str() {
609 names.push(name.to_string());
610 }
611 }
612 }
613 }
614 names.sort();
615 StrykeValue::array(names.into_iter().map(StrykeValue::string).collect())
616}
617
618pub fn list_filesf_recursive(dir: &str) -> StrykeValue {
622 let root = std::path::Path::new(dir);
623 let mut paths: Vec<String> = Vec::new();
624 fn walk(base: &std::path::Path, rel: &str, out: &mut Vec<String>) {
625 let Ok(entries) = std::fs::read_dir(base) else {
626 return;
627 };
628 for entry in entries.flatten() {
629 let ft = match entry.file_type() {
630 Ok(ft) => ft,
631 Err(_) => continue,
632 };
633 let name = match entry.file_name().into_string() {
634 Ok(n) => n,
635 Err(_) => continue,
636 };
637 let child_rel = if rel.is_empty() {
638 name.clone()
639 } else {
640 format!("{rel}/{name}")
641 };
642 if ft.is_file() {
643 out.push(child_rel);
644 } else if ft.is_dir() {
645 walk(&base.join(&name), &child_rel, out);
646 }
647 }
648 }
649 walk(root, "", &mut paths);
650 paths.sort();
651 StrykeValue::array(paths.into_iter().map(StrykeValue::string).collect())
652}
653
654pub fn list_dirs(dir: &str) -> StrykeValue {
657 let mut names: Vec<String> = Vec::new();
658 if let Ok(entries) = std::fs::read_dir(dir) {
659 for entry in entries.flatten() {
660 if entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false) {
661 if let Some(name) = entry.file_name().to_str() {
662 names.push(name.to_string());
663 }
664 }
665 }
666 }
667 names.sort();
668 StrykeValue::array(names.into_iter().map(StrykeValue::string).collect())
669}
670
671pub fn list_dirs_recursive(dir: &str) -> StrykeValue {
675 let root = std::path::Path::new(dir);
676 let mut paths: Vec<String> = Vec::new();
677 fn walk(base: &std::path::Path, rel: &str, out: &mut Vec<String>) {
678 let Ok(entries) = std::fs::read_dir(base) else {
679 return;
680 };
681 for entry in entries.flatten() {
682 let ft = match entry.file_type() {
683 Ok(ft) => ft,
684 Err(_) => continue,
685 };
686 if !ft.is_dir() {
687 continue;
688 }
689 let name = match entry.file_name().into_string() {
690 Ok(n) => n,
691 Err(_) => continue,
692 };
693 let child_rel = if rel.is_empty() {
694 name.clone()
695 } else {
696 format!("{rel}/{name}")
697 };
698 out.push(child_rel.clone());
699 walk(&base.join(&name), &child_rel, out);
700 }
701 }
702 walk(root, "", &mut paths);
703 paths.sort();
704 StrykeValue::array(paths.into_iter().map(StrykeValue::string).collect())
705}
706
707pub fn list_sym_links(dir: &str) -> StrykeValue {
710 let mut names: Vec<String> = Vec::new();
711 if let Ok(entries) = std::fs::read_dir(dir) {
712 for entry in entries.flatten() {
713 if entry.file_type().map(|ft| ft.is_symlink()).unwrap_or(false) {
714 if let Some(name) = entry.file_name().to_str() {
715 names.push(name.to_string());
716 }
717 }
718 }
719 }
720 names.sort();
721 StrykeValue::array(names.into_iter().map(StrykeValue::string).collect())
722}
723
724pub fn list_sockets(dir: &str) -> StrykeValue {
727 let mut names: Vec<String> = Vec::new();
728 #[cfg(unix)]
729 {
730 use std::os::unix::fs::FileTypeExt;
731 if let Ok(entries) = std::fs::read_dir(dir) {
732 for entry in entries.flatten() {
733 if entry.file_type().map(|ft| ft.is_socket()).unwrap_or(false) {
734 if let Some(name) = entry.file_name().to_str() {
735 names.push(name.to_string());
736 }
737 }
738 }
739 }
740 }
741 let _ = dir;
742 names.sort();
743 StrykeValue::array(names.into_iter().map(StrykeValue::string).collect())
744}
745
746pub fn list_pipes(dir: &str) -> StrykeValue {
749 let mut names: Vec<String> = Vec::new();
750 #[cfg(unix)]
751 {
752 use std::os::unix::fs::FileTypeExt;
753 if let Ok(entries) = std::fs::read_dir(dir) {
754 for entry in entries.flatten() {
755 if entry.file_type().map(|ft| ft.is_fifo()).unwrap_or(false) {
756 if let Some(name) = entry.file_name().to_str() {
757 names.push(name.to_string());
758 }
759 }
760 }
761 }
762 }
763 let _ = dir;
764 names.sort();
765 StrykeValue::array(names.into_iter().map(StrykeValue::string).collect())
766}
767
768pub fn list_block_devices(dir: &str) -> StrykeValue {
771 let mut names: Vec<String> = Vec::new();
772 #[cfg(unix)]
773 {
774 use std::os::unix::fs::FileTypeExt;
775 if let Ok(entries) = std::fs::read_dir(dir) {
776 for entry in entries.flatten() {
777 if entry
778 .file_type()
779 .map(|ft| ft.is_block_device())
780 .unwrap_or(false)
781 {
782 if let Some(name) = entry.file_name().to_str() {
783 names.push(name.to_string());
784 }
785 }
786 }
787 }
788 }
789 let _ = dir;
790 names.sort();
791 StrykeValue::array(names.into_iter().map(StrykeValue::string).collect())
792}
793
794pub fn list_executables(dir: &str) -> StrykeValue {
797 let mut names: Vec<String> = Vec::new();
798 #[cfg(unix)]
799 {
800 if let Ok(entries) = std::fs::read_dir(dir) {
801 for entry in entries.flatten() {
802 if entry.file_type().map(|ft| ft.is_file()).unwrap_or(false)
803 && unix_path_executable(&entry.path())
804 {
805 if let Some(name) = entry.file_name().to_str() {
806 names.push(name.to_string());
807 }
808 }
809 }
810 }
811 }
812 #[cfg(not(unix))]
813 {
814 if let Ok(entries) = std::fs::read_dir(dir) {
815 for entry in entries.flatten() {
816 if entry.file_type().map(|ft| ft.is_file()).unwrap_or(false) {
817 let p = entry.path();
818 if let Some(ext) = p.extension() {
819 if ext == "exe" || ext == "bat" || ext == "cmd" {
820 if let Some(name) = entry.file_name().to_str() {
821 names.push(name.to_string());
822 }
823 }
824 }
825 }
826 }
827 }
828 }
829 let _ = dir;
830 names.sort();
831 StrykeValue::array(names.into_iter().map(StrykeValue::string).collect())
832}
833
834pub fn list_char_devices(dir: &str) -> StrykeValue {
837 let mut names: Vec<String> = Vec::new();
838 #[cfg(unix)]
839 {
840 use std::os::unix::fs::FileTypeExt;
841 if let Ok(entries) = std::fs::read_dir(dir) {
842 for entry in entries.flatten() {
843 if entry
844 .file_type()
845 .map(|ft| ft.is_char_device())
846 .unwrap_or(false)
847 {
848 if let Some(name) = entry.file_name().to_str() {
849 names.push(name.to_string());
850 }
851 }
852 }
853 }
854 }
855 let _ = dir;
856 names.sort();
857 StrykeValue::array(names.into_iter().map(StrykeValue::string).collect())
858}
859
860pub fn glob_patterns(patterns: &[String]) -> StrykeValue {
861 let mut paths: Vec<String> = Vec::new();
862 for pat in patterns {
863 if !pattern_is_glob(pat) {
864 paths.push(normalize_glob_path_display(pat.clone()));
865 continue;
866 }
867 for s in stryke_glob(pat) {
868 paths.push(normalize_glob_path_display(s));
869 }
870 }
871 paths.sort();
872 paths.dedup();
873 StrykeValue::array(paths.into_iter().map(StrykeValue::string).collect())
874}
875
876pub fn glob_par_patterns(patterns: &[String]) -> StrykeValue {
879 glob_par_patterns_inner(patterns, None)
880}
881
882pub fn glob_par_patterns_with_progress(patterns: &[String], progress: bool) -> StrykeValue {
885 if patterns.is_empty() {
886 return StrykeValue::array(Vec::new());
887 }
888 let pmap = PmapProgress::new(progress, patterns.len());
889 let v = glob_par_patterns_inner(patterns, Some(&pmap));
890 pmap.finish();
891 v
892}
893
894fn glob_par_patterns_inner(patterns: &[String], progress: Option<&PmapProgress>) -> StrykeValue {
895 let out: Vec<String> = patterns
900 .par_iter()
901 .flat_map_iter(|pat| {
902 let rows: Vec<String> = if !pattern_is_glob(pat) {
903 vec![pat.clone()]
904 } else {
905 stryke_glob(pat)
906 };
907 if let Some(p) = progress {
908 p.tick();
909 }
910 rows
911 })
912 .collect();
913 let mut paths: Vec<String> = out.into_iter().map(normalize_glob_path_display).collect();
914 paths.sort();
915 paths.dedup();
916 StrykeValue::array(paths.into_iter().map(StrykeValue::string).collect())
917}
918
919fn normalize_glob_path_display(s: String) -> String {
924 s
925}
926
927pub fn rename_paths(old: &str, new: &str) -> StrykeValue {
929 StrykeValue::integer(if std::fs::rename(old, new).is_ok() {
930 1
931 } else {
932 0
933 })
934}
935
936#[inline]
937fn is_cross_device_rename(e: &io::Error) -> bool {
938 if e.kind() == io::ErrorKind::CrossesDevices {
939 return true;
940 }
941 #[cfg(unix)]
942 {
943 if e.raw_os_error() == Some(libc::EXDEV) {
944 return true;
945 }
946 }
947 false
948}
949
950fn try_move_path(from: &str, to: &str) -> io::Result<()> {
951 match std::fs::rename(from, to) {
952 Ok(()) => Ok(()),
953 Err(e) => {
954 if !is_cross_device_rename(&e) {
955 return Err(e);
956 }
957 let meta = std::fs::symlink_metadata(from)?;
958 if meta.is_dir() {
959 return Err(io::Error::new(
960 io::ErrorKind::Unsupported,
961 "move: cross-device directory move is not supported",
962 ));
963 }
964 if !meta.is_file() && !meta.is_symlink() {
965 return Err(io::Error::new(
966 io::ErrorKind::Unsupported,
967 "move: cross-device move supports files and symlinks only",
968 ));
969 }
970 std::fs::copy(from, to)?;
971 std::fs::remove_file(from)?;
972 Ok(())
973 }
974 }
975}
976
977pub fn move_path(from: &str, to: &str) -> StrykeValue {
980 StrykeValue::integer(if try_move_path(from, to).is_ok() {
981 1
982 } else {
983 0
984 })
985}
986
987#[cfg(unix)]
988fn unix_path_executable(path: &Path) -> bool {
989 use std::os::unix::fs::PermissionsExt;
990 std::fs::metadata(path)
991 .ok()
992 .filter(|m| m.is_file())
993 .is_some_and(|m| m.permissions().mode() & 0o111 != 0)
994}
995
996#[cfg(not(unix))]
997fn unix_path_executable(path: &Path) -> bool {
998 path.is_file()
999}
1000
1001fn display_executable_path(path: &Path) -> Option<String> {
1002 if !unix_path_executable(path) {
1003 return None;
1004 }
1005 path.canonicalize()
1006 .ok()
1007 .map(|p| p.to_string_lossy().into_owned())
1008 .or_else(|| Some(path.to_string_lossy().into_owned()))
1009}
1010
1011#[cfg(windows)]
1012fn pathext_suffixes() -> Vec<String> {
1013 env::var_os("PATHEXT")
1014 .map(|s| {
1015 env::split_paths(&s)
1016 .filter_map(|p| p.to_str().map(str::to_ascii_lowercase))
1017 .collect()
1018 })
1019 .unwrap_or_else(|| vec![".exe".into(), ".cmd".into(), ".bat".into(), ".com".into()])
1020}
1021
1022#[cfg(windows)]
1023fn which_in_dir(dir: &Path, program: &str) -> Option<String> {
1024 let plain = dir.join(program);
1025 if let Some(s) = display_executable_path(&plain) {
1026 return Some(s);
1027 }
1028 if !program.contains('.') {
1029 for ext in pathext_suffixes() {
1030 let cand = dir.join(format!("{program}{ext}"));
1031 if let Some(s) = display_executable_path(&cand) {
1032 return Some(s);
1033 }
1034 }
1035 }
1036 None
1037}
1038
1039#[cfg(not(windows))]
1040fn which_in_dir(dir: &Path, program: &str) -> Option<String> {
1041 display_executable_path(&dir.join(program))
1042}
1043
1044pub fn which_executable(program: &str, include_dot: bool) -> Option<String> {
1047 if program.is_empty() {
1048 return None;
1049 }
1050 if program.contains('/') || (cfg!(windows) && program.contains('\\')) {
1051 return display_executable_path(Path::new(program));
1052 }
1053 let path_os = env::var_os("PATH")?;
1054 for dir in env::split_paths(&path_os) {
1055 if let Some(s) = which_in_dir(&dir, program) {
1056 return Some(s);
1057 }
1058 }
1059 if include_dot {
1060 return which_in_dir(Path::new("."), program);
1061 }
1062 None
1063}
1064
1065pub fn read_file_bytes(path: &str) -> io::Result<Arc<Vec<u8>>> {
1067 Ok(Arc::new(std::fs::read(path)?))
1068}
1069
1070fn adjacent_temp_path(target: &Path) -> PathBuf {
1072 let dir = target.parent().unwrap_or_else(|| Path::new("."));
1073 let name = target
1074 .file_name()
1075 .map(|s| s.to_string_lossy().into_owned())
1076 .unwrap_or_else(|| "file".to_string());
1077 let rnd: u32 = rand::thread_rng().gen();
1078 dir.join(format!("{name}.spurt-tmp-{rnd}"))
1079}
1080
1081pub fn spurt_path(path: &str, data: &[u8], mkdir_parents: bool, atomic: bool) -> io::Result<()> {
1084 let path = Path::new(path);
1085 if mkdir_parents {
1086 if let Some(parent) = path.parent() {
1087 if !parent.as_os_str().is_empty() {
1088 std::fs::create_dir_all(parent)?;
1089 }
1090 }
1091 }
1092 if !atomic {
1093 return std::fs::write(path, data);
1094 }
1095 let tmp = adjacent_temp_path(path);
1096 {
1097 let mut f = std::fs::File::create(&tmp)?;
1098 f.write_all(data)?;
1099 f.sync_all().ok();
1100 }
1101 std::fs::rename(&tmp, path)?;
1102 Ok(())
1103}
1104
1105pub fn copy_file(from: &str, to: &str, preserve_metadata: bool) -> StrykeValue {
1108 let times = if preserve_metadata {
1109 std::fs::metadata(from).ok().map(|src_meta| {
1110 let at = src_meta
1111 .accessed()
1112 .ok()
1113 .and_then(|t| t.duration_since(UNIX_EPOCH).ok())
1114 .map(|d| d.as_secs() as i64)
1115 .unwrap_or(0);
1116 let mt = src_meta
1117 .modified()
1118 .ok()
1119 .and_then(|t| t.duration_since(UNIX_EPOCH).ok())
1120 .map(|d| d.as_secs() as i64)
1121 .unwrap_or(0);
1122 (at, mt)
1123 })
1124 } else {
1125 None
1126 };
1127 if std::fs::copy(from, to).is_err() {
1128 return StrykeValue::integer(0);
1129 }
1130 if let Some((at, mt)) = times {
1131 let _ = utime_paths(at, mt, &[to.to_string()]);
1132 }
1133 StrykeValue::integer(1)
1134}
1135
1136pub fn path_basename(path: &str) -> String {
1138 Path::new(path)
1139 .file_name()
1140 .map(|s| s.to_string_lossy().into_owned())
1141 .unwrap_or_default()
1142}
1143
1144pub fn path_dirname(path: &str) -> String {
1146 if path.is_empty() {
1147 return String::new();
1148 }
1149 let p = Path::new(path);
1150 if path == "/" {
1151 return "/".to_string();
1152 }
1153 match p.parent() {
1154 None => ".".to_string(),
1155 Some(parent) => {
1156 let s = parent.to_string_lossy();
1157 if s.is_empty() {
1158 ".".to_string()
1159 } else {
1160 s.into_owned()
1161 }
1162 }
1163 }
1164}
1165
1166pub fn fileparse_path(path: &str, suffix: Option<&str>) -> (String, String, String) {
1170 let dir = path_dirname(path);
1171 let full_base = path_basename(path);
1172 let (base, sfx) = if let Some(suf) = suffix.filter(|s| !s.is_empty()) {
1173 if full_base.ends_with(suf) && full_base.len() > suf.len() {
1174 (
1175 full_base[..full_base.len() - suf.len()].to_string(),
1176 suf.to_string(),
1177 )
1178 } else {
1179 (full_base.clone(), String::new())
1180 }
1181 } else {
1182 (full_base.clone(), String::new())
1183 };
1184 (base, dir, sfx)
1185}
1186
1187pub fn chmod_paths(paths: &[String], mode: i64) -> i64 {
1189 #[cfg(unix)]
1190 {
1191 use std::os::unix::fs::PermissionsExt;
1192 let mut count = 0i64;
1193 for path in paths {
1194 if let Ok(meta) = std::fs::metadata(path) {
1195 let mut perms = meta.permissions();
1196 let old = perms.mode();
1197 perms.set_mode((old & !0o777) | (mode as u32 & 0o777));
1199 if std::fs::set_permissions(path, perms).is_ok() {
1200 count += 1;
1201 }
1202 }
1203 }
1204 count
1205 }
1206 #[cfg(not(unix))]
1207 {
1208 let _ = (paths, mode);
1209 0
1210 }
1211}
1212
1213pub fn utime_paths(atime_sec: i64, mtime_sec: i64, paths: &[String]) -> i64 {
1215 #[cfg(unix)]
1216 {
1217 use std::ffi::CString;
1218 let mut count = 0i64;
1219 let tv = [
1220 libc::timeval {
1221 tv_sec: atime_sec as libc::time_t,
1222 tv_usec: 0,
1223 },
1224 libc::timeval {
1225 tv_sec: mtime_sec as libc::time_t,
1226 tv_usec: 0,
1227 },
1228 ];
1229 for path in paths {
1230 let Ok(cs) = CString::new(path.as_str()) else {
1231 continue;
1232 };
1233 if unsafe { libc::utimes(cs.as_ptr(), tv.as_ptr()) } == 0 {
1234 count += 1;
1235 }
1236 }
1237 count
1238 }
1239 #[cfg(not(unix))]
1240 {
1241 let _ = (atime_sec, mtime_sec, paths);
1242 0
1243 }
1244}
1245
1246pub fn chown_paths(paths: &[String], uid: i64, gid: i64) -> i64 {
1248 #[cfg(unix)]
1249 {
1250 use std::ffi::CString;
1251 let u = if uid < 0 {
1252 (!0u32) as libc::uid_t
1253 } else {
1254 uid as libc::uid_t
1255 };
1256 let g = if gid < 0 {
1257 (!0u32) as libc::gid_t
1258 } else {
1259 gid as libc::gid_t
1260 };
1261 let mut count = 0i64;
1262 for path in paths {
1263 let Ok(c) = CString::new(path.as_str()) else {
1264 continue;
1265 };
1266 let r = unsafe { libc::chown(c.as_ptr(), u, g) };
1267 if r == 0 {
1268 count += 1;
1269 }
1270 }
1271 count
1272 }
1273 #[cfg(not(unix))]
1274 {
1275 let _ = (paths, uid, gid);
1276 0
1277 }
1278}
1279
1280pub fn touch_paths(paths: &[String]) -> i64 {
1283 use std::fs::OpenOptions;
1284 let mut count = 0i64;
1285 for path in paths {
1286 if path.is_empty() {
1287 continue;
1288 }
1289 let created = OpenOptions::new()
1291 .create(true)
1292 .append(true)
1293 .open(path)
1294 .is_ok();
1295 if !created {
1296 continue;
1297 }
1298 #[cfg(unix)]
1300 {
1301 use std::ffi::CString;
1302 if let Ok(cs) = CString::new(path.as_str()) {
1303 unsafe { libc::utimes(cs.as_ptr(), std::ptr::null()) };
1305 }
1306 }
1307 count += 1;
1308 }
1309 count
1310}
1311
1312#[cfg(test)]
1313mod tests {
1314 use super::*;
1315 use std::collections::HashSet;
1316
1317 #[test]
1318 fn glob_par_matches_sequential_glob_set() {
1319 let base = std::env::temp_dir().join(format!("stryke_glob_par_{}", std::process::id()));
1320 let _ = std::fs::remove_dir_all(&base);
1321 std::fs::create_dir_all(base.join("a")).unwrap();
1322 std::fs::create_dir_all(base.join("b")).unwrap();
1323 std::fs::create_dir_all(base.join("b/nested")).unwrap();
1324 std::fs::File::create(base.join("a/x.log")).unwrap();
1325 std::fs::File::create(base.join("b/y.log")).unwrap();
1326 std::fs::File::create(base.join("b/nested/z.log")).unwrap();
1327 std::fs::File::create(base.join("root.txt")).unwrap();
1328
1329 let pat = format!("{}/**/*.log", base.display());
1331 let a = glob_patterns(std::slice::from_ref(&pat));
1332 let b = glob_par_patterns(std::slice::from_ref(&pat));
1333 let _ = std::fs::remove_dir_all(&base);
1334
1335 let set_a: HashSet<String> = a
1336 .as_array_vec()
1337 .expect("expected array")
1338 .into_iter()
1339 .map(|x| x.to_string())
1340 .collect();
1341 let set_b: HashSet<String> = b
1342 .as_array_vec()
1343 .expect("expected array")
1344 .into_iter()
1345 .map(|x| x.to_string())
1346 .collect();
1347 assert_eq!(set_a, set_b);
1348 }
1349
1350 #[test]
1351 fn glob_par_src_rs_matches_when_src_tree_present() {
1352 let root = Path::new(env!("CARGO_MANIFEST_DIR"));
1353 let src = root.join("src");
1354 if !src.is_dir() {
1355 return;
1356 }
1357 let pat = src.join("*.rs").to_string_lossy().into_owned();
1358 let v = glob_par_patterns(&[pat])
1359 .as_array_vec()
1360 .expect("expected array");
1361 assert!(
1362 !v.is_empty(),
1363 "glob_par src/*.rs should find at least one .rs under src/"
1364 );
1365 }
1366
1367 #[test]
1368 fn glob_par_progress_false_same_as_plain() {
1369 let tmp = Path::new(env!("CARGO_MANIFEST_DIR"))
1370 .join("target")
1371 .join(format!("glob_par_prog_false_{}", std::process::id()));
1372 let _ = std::fs::remove_dir_all(&tmp);
1373 std::fs::create_dir_all(&tmp).unwrap();
1374 std::fs::write(tmp.join("probe.rs"), b"// x\n").unwrap();
1375 let pat = tmp.join("*.rs").to_string_lossy().replace('\\', "/");
1376 let a = glob_par_patterns(std::slice::from_ref(&pat));
1377 let b = glob_par_patterns_with_progress(std::slice::from_ref(&pat), false);
1378 let _ = std::fs::remove_dir_all(&tmp);
1379 let va = a.as_array_vec().expect("a");
1380 let vb = b.as_array_vec().expect("b");
1381 assert_eq!(va.len(), vb.len(), "glob_par vs glob_par(..., progress=>0)");
1382 for (x, y) in va.iter().zip(vb.iter()) {
1383 assert_eq!(x.to_string(), y.to_string());
1384 }
1385 }
1386
1387 #[test]
1388 fn read_file_text_perl_compat_maps_invalid_utf8_to_latin1_octets() {
1389 let path = std::env::temp_dir().join(format!("stryke_bad_utf8_{}.txt", std::process::id()));
1390 std::fs::write(&path, b"ok\xff\xfe\x80\n").unwrap();
1392 let s = read_file_text_perl_compat(&path).expect("read");
1393 assert!(s.starts_with("ok"));
1394 assert_eq!(&s[2..], "\u{00ff}\u{00fe}\u{0080}\n");
1395 let _ = std::fs::remove_file(&path);
1396 }
1397
1398 #[test]
1399 fn read_logical_line_perl_compat_splits_and_decodes_per_line() {
1400 use std::io::Cursor;
1401 let mut r = Cursor::new(b"a\xff\nb\n");
1402 assert_eq!(
1403 read_logical_line_perl_compat(&mut r).unwrap(),
1404 Some("a\u{00ff}".to_string())
1405 );
1406 assert_eq!(
1407 read_logical_line_perl_compat(&mut r).unwrap(),
1408 Some("b".to_string())
1409 );
1410 assert_eq!(read_logical_line_perl_compat(&mut r).unwrap(), None);
1411 }
1412}