1use std::env;
5use std::ffi::{OsStr, OsString};
6use std::fs::{self, File, Metadata, OpenOptions};
7use std::io;
8use std::io::prelude::*;
9use std::iter;
10use std::path::{Component, Path, PathBuf};
11
12use anyhow::{Context, Result};
13use tempfile::Builder as TempFileBuilder;
14
15pub fn join_paths<T: AsRef<OsStr>>(paths: &[T], env: &str) -> Result<OsString> {
22    env::join_paths(paths.iter()).with_context(|| {
23        let mut message = format!(
24            "failed to join paths from `${env}` together\n\n\
25             Check if any of path segments listed below contain an \
26             unterminated quote character or path separator:"
27        );
28        for path in paths {
29            use std::fmt::Write;
30            write!(&mut message, "\n    {:?}", Path::new(path)).unwrap();
31        }
32
33        message
34    })
35}
36
37pub fn dylib_path_envvar() -> &'static str {
40    if cfg!(windows) {
41        "PATH"
42    } else if cfg!(target_os = "macos") {
43        "DYLD_FALLBACK_LIBRARY_PATH"
59    } else if cfg!(target_os = "aix") {
60        "LIBPATH"
61    } else {
62        "LD_LIBRARY_PATH"
63    }
64}
65
66pub fn dylib_path() -> Vec<PathBuf> {
71    match env::var_os(dylib_path_envvar()) {
72        Some(var) => env::split_paths(&var).collect(),
73        None => Vec::new(),
74    }
75}
76
77pub fn normalize_path(path: &Path) -> PathBuf {
86    let mut components = path.components().peekable();
87    let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
88        components.next();
89        PathBuf::from(c.as_os_str())
90    } else {
91        PathBuf::new()
92    };
93
94    for component in components {
95        match component {
96            Component::Prefix(..) => unreachable!(),
97            Component::RootDir => {
98                ret.push(component.as_os_str());
99            }
100            Component::CurDir => {}
101            Component::ParentDir => {
102                ret.pop();
103            }
104            Component::Normal(c) => {
105                ret.push(c);
106            }
107        }
108    }
109    ret
110}
111
112pub fn resolve_executable(exec: &Path) -> Result<PathBuf> {
117    if exec.components().count() == 1 {
118        let paths = env::var_os("PATH").ok_or_else(|| anyhow::format_err!("no PATH"))?;
119        let candidates = env::split_paths(&paths).flat_map(|path| {
120            let candidate = path.join(exec);
121            let with_exe = if env::consts::EXE_EXTENSION.is_empty() {
122                None
123            } else {
124                Some(candidate.with_extension(env::consts::EXE_EXTENSION))
125            };
126            iter::once(candidate).chain(with_exe)
127        });
128        for candidate in candidates {
129            if candidate.is_file() {
130                return Ok(candidate);
131            }
132        }
133
134        anyhow::bail!("no executable for `{}` found in PATH", exec.display())
135    } else {
136        Ok(exec.into())
137    }
138}
139
140pub fn metadata<P: AsRef<Path>>(path: P) -> Result<Metadata> {
144    let path = path.as_ref();
145    std::fs::metadata(path)
146        .with_context(|| format!("failed to load metadata for path `{}`", path.display()))
147}
148
149pub fn symlink_metadata<P: AsRef<Path>>(path: P) -> Result<Metadata> {
153    let path = path.as_ref();
154    std::fs::symlink_metadata(path)
155        .with_context(|| format!("failed to load metadata for path `{}`", path.display()))
156}
157
158pub fn read(path: &Path) -> Result<String> {
162    match String::from_utf8(read_bytes(path)?) {
163        Ok(s) => Ok(s),
164        Err(_) => anyhow::bail!("path at `{}` was not valid utf-8", path.display()),
165    }
166}
167
168pub fn read_bytes(path: &Path) -> Result<Vec<u8>> {
172    fs::read(path).with_context(|| format!("failed to read `{}`", path.display()))
173}
174
175pub fn write<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> Result<()> {
179    let path = path.as_ref();
180    fs::write(path, contents.as_ref())
181        .with_context(|| format!("failed to write `{}`", path.display()))
182}
183
184pub fn write_atomic<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> Result<()> {
188    let path = path.as_ref();
189
190    #[cfg(unix)]
195    let perms = path.metadata().ok().map(|meta| {
196        use std::os::unix::fs::PermissionsExt;
197
198        #[allow(clippy::useless_conversion)]
200        let mask = u32::from(libc::S_IRWXU | libc::S_IRWXG | libc::S_IRWXO);
201        let mode = meta.permissions().mode() & mask;
202
203        std::fs::Permissions::from_mode(mode)
204    });
205
206    let mut tmp = TempFileBuilder::new()
207        .prefix(path.file_name().unwrap())
208        .tempfile_in(path.parent().unwrap())?;
209    tmp.write_all(contents.as_ref())?;
210
211    #[cfg(unix)]
215    if let Some(perms) = perms {
216        tmp.as_file().set_permissions(perms)?;
217    }
218
219    tmp.persist(path)?;
220    Ok(())
221}
222
223pub fn temp_dir_in<P: AsRef<Path>, T>(path: P, f: impl FnOnce(&Path) -> Result<T>) -> Result<T> {
231    let path = path.as_ref();
232
233    std::fs::create_dir_all(path)
234        .with_context(|| format!("failed to create directory for tmpdir `{}`", path.display()))?;
235
236    let tmp = TempFileBuilder::new().tempdir_in(path)?;
237    f(tmp.path())
238}
239
240pub fn write_if_changed<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> Result<()> {
243    (|| -> Result<()> {
244        let contents = contents.as_ref();
245        let mut f = OpenOptions::new()
246            .read(true)
247            .write(true)
248            .create(true)
249            .truncate(false)
252            .open(&path)?;
253        let mut orig = Vec::new();
254        f.read_to_end(&mut orig)?;
255        if orig != contents {
256            f.set_len(0)?;
257            f.seek(io::SeekFrom::Start(0))?;
258            f.write_all(contents)?;
259        }
260        Ok(())
261    })()
262    .with_context(|| format!("failed to write `{}`", path.as_ref().display()))?;
263    Ok(())
264}
265
266pub fn append(path: &Path, contents: &[u8]) -> Result<()> {
269    (|| -> Result<()> {
270        let mut f = OpenOptions::new().append(true).create(true).open(path)?;
271
272        f.write_all(contents)?;
273        Ok(())
274    })()
275    .with_context(|| format!("failed to write `{}`", path.display()))?;
276    Ok(())
277}
278
279pub fn create<P: AsRef<Path>>(path: P) -> Result<File> {
281    let path = path.as_ref();
282    File::create(path).with_context(|| format!("failed to create file `{}`", path.display()))
283}
284
285pub fn open<P: AsRef<Path>>(path: P) -> Result<File> {
287    let path = path.as_ref();
288    File::open(path).with_context(|| format!("failed to open file `{}`", path.display()))
289}
290
291pub fn path2bytes(path: &Path) -> Result<&[u8]> {
293    #[cfg(unix)]
294    {
295        use std::os::unix::prelude::*;
296        Ok(path.as_os_str().as_bytes())
297    }
298    #[cfg(windows)]
299    {
300        match path.as_os_str().to_str() {
301            Some(s) => Ok(s.as_bytes()),
302            None => Err(anyhow::format_err!(
303                "invalid non-unicode path: {}",
304                path.display()
305            )),
306        }
307    }
308}
309
310pub fn bytes2path(bytes: &[u8]) -> Result<PathBuf> {
312    #[cfg(unix)]
313    {
314        use std::os::unix::prelude::*;
315        Ok(PathBuf::from(OsStr::from_bytes(bytes)))
316    }
317    #[cfg(windows)]
318    {
319        use std::str;
320        match str::from_utf8(bytes) {
321            Ok(s) => Ok(PathBuf::from(s)),
322            Err(..) => Err(anyhow::format_err!("invalid non-unicode path")),
323        }
324    }
325}
326
327pub fn ancestors<'a>(path: &'a Path, stop_root_at: Option<&Path>) -> PathAncestors<'a> {
333    PathAncestors::new(path, stop_root_at)
334}
335
336pub struct PathAncestors<'a> {
338    current: Option<&'a Path>,
339    stop_at: Option<PathBuf>,
340}
341
342impl<'a> PathAncestors<'a> {
343    fn new(path: &'a Path, stop_root_at: Option<&Path>) -> PathAncestors<'a> {
344        let stop_at = env::var("__CARGO_TEST_ROOT")
345            .ok()
346            .map(PathBuf::from)
347            .or_else(|| stop_root_at.map(|p| p.to_path_buf()));
348        PathAncestors {
349            current: Some(path),
350            stop_at,
352        }
353    }
354}
355
356impl<'a> Iterator for PathAncestors<'a> {
357    type Item = &'a Path;
358
359    fn next(&mut self) -> Option<&'a Path> {
360        if let Some(path) = self.current {
361            self.current = path.parent();
362
363            if let Some(ref stop_at) = self.stop_at
364                && path == stop_at
365            {
366                self.current = None;
367            }
368
369            Some(path)
370        } else {
371            None
372        }
373    }
374}
375
376pub fn create_dir_all(p: impl AsRef<Path>) -> Result<()> {
378    _create_dir_all(p.as_ref())
379}
380
381fn _create_dir_all(p: &Path) -> Result<()> {
382    fs::create_dir_all(p)
383        .with_context(|| format!("failed to create directory `{}`", p.display()))?;
384    Ok(())
385}
386
387pub fn remove_dir_all<P: AsRef<Path>>(p: P) -> Result<()> {
391    _remove_dir_all(p.as_ref()).or_else(|prev_err| {
392        fs::remove_dir_all(p.as_ref()).with_context(|| {
396            format!(
397                "{:?}\n\nError: failed to remove directory `{}`",
398                prev_err,
399                p.as_ref().display(),
400            )
401        })
402    })
403}
404
405fn _remove_dir_all(p: &Path) -> Result<()> {
406    if symlink_metadata(p)?.is_symlink() {
407        return remove_file(p);
408    }
409    let entries = p
410        .read_dir()
411        .with_context(|| format!("failed to read directory `{}`", p.display()))?;
412    for entry in entries {
413        let entry = entry?;
414        let path = entry.path();
415        if entry.file_type()?.is_dir() {
416            remove_dir_all(&path)?;
417        } else {
418            remove_file(&path)?;
419        }
420    }
421    remove_dir(p)
422}
423
424pub fn remove_dir<P: AsRef<Path>>(p: P) -> Result<()> {
426    _remove_dir(p.as_ref())
427}
428
429fn _remove_dir(p: &Path) -> Result<()> {
430    fs::remove_dir(p).with_context(|| format!("failed to remove directory `{}`", p.display()))?;
431    Ok(())
432}
433
434pub fn remove_file<P: AsRef<Path>>(p: P) -> Result<()> {
441    _remove_file(p.as_ref())
442}
443
444fn _remove_file(p: &Path) -> Result<()> {
445    #[cfg(target_os = "windows")]
449    {
450        use std::os::windows::fs::FileTypeExt;
451        let metadata = symlink_metadata(p)?;
452        let file_type = metadata.file_type();
453        if file_type.is_symlink_dir() {
454            return remove_symlink_dir_with_permission_check(p);
455        }
456    }
457
458    remove_file_with_permission_check(p)
459}
460
461#[cfg(target_os = "windows")]
462fn remove_symlink_dir_with_permission_check(p: &Path) -> Result<()> {
463    remove_with_permission_check(fs::remove_dir, p)
464        .with_context(|| format!("failed to remove symlink dir `{}`", p.display()))
465}
466
467fn remove_file_with_permission_check(p: &Path) -> Result<()> {
468    remove_with_permission_check(fs::remove_file, p)
469        .with_context(|| format!("failed to remove file `{}`", p.display()))
470}
471
472fn remove_with_permission_check<F, P>(remove_func: F, p: P) -> io::Result<()>
473where
474    F: Fn(P) -> io::Result<()>,
475    P: AsRef<Path> + Clone,
476{
477    match remove_func(p.clone()) {
478        Ok(()) => Ok(()),
479        Err(e) => {
480            if e.kind() == io::ErrorKind::PermissionDenied
481                && set_not_readonly(p.as_ref()).unwrap_or(false)
482            {
483                remove_func(p)
484            } else {
485                Err(e)
486            }
487        }
488    }
489}
490
491fn set_not_readonly(p: &Path) -> io::Result<bool> {
492    let mut perms = p.metadata()?.permissions();
493    if !perms.readonly() {
494        return Ok(false);
495    }
496
497    #[cfg(unix)]
498    {
499        use std::os::unix::fs::PermissionsExt;
500        perms.set_mode(0o640);
501    }
502    #[cfg(not(unix))]
503    #[allow(clippy::permissions_set_readonly_false)]
504    {
505        perms.set_readonly(false);
506    }
507
508    fs::set_permissions(p, perms)?;
509    Ok(true)
510}
511
512pub fn link_or_copy(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> Result<()> {
516    let src = src.as_ref();
517    let dst = dst.as_ref();
518    _link_or_copy(src, dst)
519}
520
521fn _link_or_copy(src: &Path, dst: &Path) -> Result<()> {
522    log::debug!("linking {} to {}", src.display(), dst.display());
523    if same_file::is_same_file(src, dst).unwrap_or(false) {
524        return Ok(());
525    }
526
527    if fs::symlink_metadata(dst).is_ok() {
532        remove_file(dst)?;
533    }
534
535    let link_result = if src.is_dir() {
536        #[cfg(target_os = "redox")]
537        use std::os::redox::fs::symlink;
538        #[cfg(unix)]
539        use std::os::unix::fs::symlink;
540        #[cfg(windows)]
541        use std::os::windows::fs::symlink_dir as symlink;
546
547        let dst_dir = dst.parent().unwrap();
548        let src = if src.starts_with(dst_dir) {
549            src.strip_prefix(dst_dir).unwrap()
550        } else {
551            src
552        };
553        symlink(src, dst)
554    } else if cfg!(target_os = "macos") {
555        fs::copy(src, dst).map_or_else(
566            |e| {
567                if e.raw_os_error()
568                    .map_or(false, |os_err| os_err == 35 )
569                {
570                    log::info!("copy failed {e:?}. falling back to fs::hard_link");
571
572                    fs::hard_link(src, dst)
576                } else {
577                    Err(e)
578                }
579            },
580            |_| Ok(()),
581        )
582    } else {
583        fs::hard_link(src, dst)
584    };
585    link_result
586        .or_else(|err| {
587            log::debug!("link failed {err}. falling back to fs::copy");
588            fs::copy(src, dst).map(|_| ())
589        })
590        .with_context(|| {
591            format!(
592                "failed to link or copy `{}` to `{}`",
593                src.display(),
594                dst.display()
595            )
596        })?;
597    Ok(())
598}
599
600pub fn copy<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> Result<u64> {
604    let from = from.as_ref();
605    let to = to.as_ref();
606    fs::copy(from, to)
607        .with_context(|| format!("failed to copy `{}` to `{}`", from.display(), to.display()))
608}
609
610pub fn strip_prefix_canonical<P: AsRef<Path>>(
616    path: P,
617    base: P,
618) -> Result<PathBuf, std::path::StripPrefixError> {
619    let safe_canonicalize = |path: &Path| match path.canonicalize() {
621        Ok(p) => p,
622        Err(e) => {
623            log::warn!("cannot canonicalize {path:?}: {e:?}");
624            path.to_path_buf()
625        }
626    };
627    let canon_path = safe_canonicalize(path.as_ref());
628    let canon_base = safe_canonicalize(base.as_ref());
629    canon_path.strip_prefix(canon_base).map(|p| p.to_path_buf())
630}
631
632pub fn create_dir_all_excluded_from_backups_atomic(p: impl AsRef<Path>) -> Result<()> {
642    let path = p.as_ref();
643    if path.is_dir() {
644        return Ok(());
645    }
646
647    let parent = path.parent().unwrap();
648    let base = path.file_name().unwrap();
649    create_dir_all(parent)?;
650    let tempdir = TempFileBuilder::new().prefix(base).tempdir_in(parent)?;
662    exclude_from_backups(tempdir.path());
663    exclude_from_content_indexing(tempdir.path());
664    if let Err(e) = fs::rename(tempdir.path(), path)
672        && !path.exists()
673    {
674        return Err(anyhow::Error::from(e))
675            .with_context(|| format!("failed to create directory `{}`", path.display()));
676    }
677    Ok(())
678}
679
680pub fn exclude_from_backups_and_indexing(p: impl AsRef<Path>) {
684    let path = p.as_ref();
685    exclude_from_backups(path);
686    exclude_from_content_indexing(path);
687}
688
689fn exclude_from_backups(path: &Path) {
698    exclude_from_time_machine(path);
699    let file = path.join("CACHEDIR.TAG");
700    if !file.exists() {
701        let _ = std::fs::write(
702            file,
703            "Signature: 8a477f597d28d172789f06886806bc55
704# This file is a cache directory tag created by cargo.
705# For information about cache directory tags see https://bford.info/cachedir/
706",
707        );
708        }
711}
712
713fn exclude_from_content_indexing(path: &Path) {
721    #[cfg(windows)]
722    {
723        use std::iter::once;
724        use std::os::windows::prelude::OsStrExt;
725        use windows_sys::Win32::Storage::FileSystem::{
726            FILE_ATTRIBUTE_NOT_CONTENT_INDEXED, GetFileAttributesW, SetFileAttributesW,
727        };
728
729        let path: Vec<u16> = path.as_os_str().encode_wide().chain(once(0)).collect();
730        unsafe {
732            SetFileAttributesW(
733                path.as_ptr(),
734                GetFileAttributesW(path.as_ptr()) | FILE_ATTRIBUTE_NOT_CONTENT_INDEXED,
735            );
736        }
737    }
738    #[cfg(not(windows))]
739    {
740        let _ = path;
741    }
742}
743
744#[cfg(not(target_os = "macos"))]
745fn exclude_from_time_machine(_: &Path) {}
746
747#[cfg(target_os = "macos")]
748fn exclude_from_time_machine(path: &Path) {
750    use core_foundation::base::TCFType;
751    use core_foundation::{number, string, url};
752    use std::ptr;
753
754    let is_excluded_key: Result<string::CFString, _> = "NSURLIsExcludedFromBackupKey".parse();
757    let path = url::CFURL::from_path(path, false);
758    if let (Some(path), Ok(is_excluded_key)) = (path, is_excluded_key) {
759        unsafe {
760            url::CFURLSetResourcePropertyForKey(
761                path.as_concrete_TypeRef(),
762                is_excluded_key.as_concrete_TypeRef(),
763                number::kCFBooleanTrue as *const _,
764                ptr::null_mut(),
765            );
766        }
767    }
768    }
771
772#[cfg(test)]
773mod tests {
774    use super::join_paths;
775    use super::write;
776    use super::write_atomic;
777
778    #[test]
779    fn write_works() {
780        let original_contents = "[dependencies]\nfoo = 0.1.0";
781
782        let tmpdir = tempfile::tempdir().unwrap();
783        let path = tmpdir.path().join("Cargo.toml");
784        write(&path, original_contents).unwrap();
785        let contents = std::fs::read_to_string(&path).unwrap();
786        assert_eq!(contents, original_contents);
787    }
788    #[test]
789    fn write_atomic_works() {
790        let original_contents = "[dependencies]\nfoo = 0.1.0";
791
792        let tmpdir = tempfile::tempdir().unwrap();
793        let path = tmpdir.path().join("Cargo.toml");
794        write_atomic(&path, original_contents).unwrap();
795        let contents = std::fs::read_to_string(&path).unwrap();
796        assert_eq!(contents, original_contents);
797    }
798
799    #[test]
800    #[cfg(unix)]
801    fn write_atomic_permissions() {
802        use std::os::unix::fs::PermissionsExt;
803
804        #[allow(clippy::useless_conversion)]
805        let perms = u32::from(libc::S_IRWXU | libc::S_IRGRP | libc::S_IWGRP | libc::S_IROTH);
806        let original_perms = std::fs::Permissions::from_mode(perms);
807
808        let tmp = tempfile::Builder::new().tempfile().unwrap();
809
810        tmp.as_file()
812            .set_permissions(original_perms.clone())
813            .unwrap();
814
815        write_atomic(tmp.path(), "new").unwrap();
818        assert_eq!(std::fs::read_to_string(tmp.path()).unwrap(), "new");
819
820        let new_perms = std::fs::metadata(tmp.path()).unwrap().permissions();
821
822        #[allow(clippy::useless_conversion)]
823        let mask = u32::from(libc::S_IRWXU | libc::S_IRWXG | libc::S_IRWXO);
824        assert_eq!(original_perms.mode(), new_perms.mode() & mask);
825    }
826
827    #[test]
828    fn join_paths_lists_paths_on_error() {
829        let valid_paths = vec!["/testing/one", "/testing/two"];
830        let _joined = join_paths(&valid_paths, "TESTING1").unwrap();
832
833        #[cfg(unix)]
834        {
835            let invalid_paths = vec!["/testing/one", "/testing/t:wo/three"];
836            let err = join_paths(&invalid_paths, "TESTING2").unwrap_err();
837            assert_eq!(
838                err.to_string(),
839                "failed to join paths from `$TESTING2` together\n\n\
840             Check if any of path segments listed below contain an \
841             unterminated quote character or path separator:\
842             \n    \"/testing/one\"\
843             \n    \"/testing/t:wo/three\"\
844             "
845            );
846        }
847        #[cfg(windows)]
848        {
849            let invalid_paths = vec!["/testing/one", "/testing/t\"wo/three"];
850            let err = join_paths(&invalid_paths, "TESTING2").unwrap_err();
851            assert_eq!(
852                err.to_string(),
853                "failed to join paths from `$TESTING2` together\n\n\
854             Check if any of path segments listed below contain an \
855             unterminated quote character or path separator:\
856             \n    \"/testing/one\"\
857             \n    \"/testing/t\\\"wo/three\"\
858             "
859            );
860        }
861    }
862
863    #[test]
864    #[cfg(windows)]
865    fn test_remove_symlink_dir() {
866        use super::*;
867        use std::fs;
868        use std::os::windows::fs::symlink_dir;
869
870        let tmpdir = tempfile::tempdir().unwrap();
871        let dir_path = tmpdir.path().join("testdir");
872        let symlink_path = tmpdir.path().join("symlink");
873
874        fs::create_dir(&dir_path).unwrap();
875
876        symlink_dir(&dir_path, &symlink_path).expect("failed to create symlink");
877
878        assert!(symlink_path.exists());
879
880        assert!(remove_file(symlink_path.clone()).is_ok());
881
882        assert!(!symlink_path.exists());
883        assert!(dir_path.exists());
884    }
885
886    #[test]
887    #[cfg(windows)]
888    fn test_remove_symlink_file() {
889        use super::*;
890        use std::fs;
891        use std::os::windows::fs::symlink_file;
892
893        let tmpdir = tempfile::tempdir().unwrap();
894        let file_path = tmpdir.path().join("testfile");
895        let symlink_path = tmpdir.path().join("symlink");
896
897        fs::write(&file_path, b"test").unwrap();
898
899        symlink_file(&file_path, &symlink_path).expect("failed to create symlink");
900
901        assert!(symlink_path.exists());
902
903        assert!(remove_file(symlink_path.clone()).is_ok());
904
905        assert!(!symlink_path.exists());
906        assert!(file_path.exists());
907    }
908}