tinymist_std/fs/
paths.rs

1//! Upstream: <https://github.com/rust-lang/cargo/blob/rust-1.83.0/crates/cargo-util/src/paths.rs>
2//! Various utilities for working with files and paths.
3
4use 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
15/// Joins paths into a string suitable for the `PATH` environment variable.
16///
17/// This is equivalent to [`std::env::join_paths`], but includes a more
18/// detailed error message. The given `env` argument is the name of the
19/// environment variable this is will be used for, which is included in the
20/// error message.
21pub 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
37/// Returns the name of the environment variable used for searching for
38/// dynamic libraries.
39pub fn dylib_path_envvar() -> &'static str {
40    if cfg!(windows) {
41        "PATH"
42    } else if cfg!(target_os = "macos") {
43        // When loading and linking a dynamic library or bundle, dlopen
44        // searches in LD_LIBRARY_PATH, DYLD_LIBRARY_PATH, PWD, and
45        // DYLD_FALLBACK_LIBRARY_PATH.
46        // In the Mach-O format, a dynamic library has an "install path."
47        // Clients linking against the library record this path, and the
48        // dynamic linker, dyld, uses it to locate the library.
49        // dyld searches DYLD_LIBRARY_PATH *before* the install path.
50        // dyld searches DYLD_FALLBACK_LIBRARY_PATH only if it cannot
51        // find the library in the install path.
52        // Setting DYLD_LIBRARY_PATH can easily have unintended
53        // consequences.
54        //
55        // Also, DYLD_LIBRARY_PATH appears to have significant performance
56        // penalty starting in 10.13. Cargo's testsuite ran more than twice as
57        // slow with it on CI.
58        "DYLD_FALLBACK_LIBRARY_PATH"
59    } else if cfg!(target_os = "aix") {
60        "LIBPATH"
61    } else {
62        "LD_LIBRARY_PATH"
63    }
64}
65
66/// Returns a list of directories that are searched for dynamic libraries.
67///
68/// Note that some operating systems will have defaults if this is empty that
69/// will need to be dealt with.
70pub 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
77/// Normalize a path, removing things like `.` and `..`.
78///
79/// CAUTION: This does not resolve symlinks (unlike
80/// [`std::fs::canonicalize`]). This may cause incorrect or surprising
81/// behavior at times. This should be used carefully. Unfortunately,
82/// [`std::fs::canonicalize`] can be hard to use correctly, since it can often
83/// fail, or on Windows returns annoying device paths. This is a problem Cargo
84/// needs to improve on.
85pub 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
112/// Returns the absolute path of where the given executable is located based
113/// on searching the `PATH` environment variable.
114///
115/// Returns an error if it cannot be found.
116pub 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
140/// Returns metadata for a file (follows symlinks).
141///
142/// Equivalent to [`std::fs::metadata`] with better error messages.
143pub 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
149/// Returns metadata for a file without following symlinks.
150///
151/// Equivalent to [`std::fs::metadata`] with better error messages.
152pub 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
158/// Reads a file to a string.
159///
160/// Equivalent to [`std::fs::read_to_string`] with better error messages.
161pub 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
168/// Reads a file into a bytes vector.
169///
170/// Equivalent to [`std::fs::read`] with better error messages.
171pub fn read_bytes(path: &Path) -> Result<Vec<u8>> {
172    fs::read(path).with_context(|| format!("failed to read `{}`", path.display()))
173}
174
175/// Writes a file to disk.
176///
177/// Equivalent to [`std::fs::write`] with better error messages.
178pub 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
184/// Writes a file to disk atomically.
185///
186/// write_atomic uses tempfile::persist to accomplish atomic writes.
187pub fn write_atomic<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> Result<()> {
188    let path = path.as_ref();
189
190    // On unix platforms, get the permissions of the original file. Copy only the
191    // user/group/other read/write/execute permission bits. The tempfile lib
192    // defaults to an initial mode of 0o600, and we'll set the proper
193    // permissions after creating the file.
194    #[cfg(unix)]
195    let perms = path.metadata().ok().map(|meta| {
196        use std::os::unix::fs::PermissionsExt;
197
198        // these constants are u16 on macOS
199        #[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    // On unix platforms, set the permissions on the newly created file. We can use
212    // fchmod (called by the std lib; subject to change) which ignores the umask
213    // so that the new file has the same permissions as the old file.
214    #[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
223/// Creates a temporary directory in the given path and calls the given
224/// function.
225///
226/// The temporary directory is automatically destroyed after the function
227/// returns, even if it panics.
228///
229/// Note: it may leak if the process is killed.
230pub 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
240/// Equivalent to [`write()`], but does not write anything if the file contents
241/// are identical to the given contents.
242pub 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            // todo: cargo doesn't have `truncate` in the options, but it is
250            // suggested by clippy
251            .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
266/// Equivalent to [`write()`], but appends to the end instead of replacing the
267/// contents.
268pub 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
279/// Creates a new file.
280pub 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
285/// Opens an existing file.
286pub 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
291/// Converts a path to UTF-8 bytes.
292pub 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    #[cfg(not(any(unix, windows)))]
309    {
310        match path.as_os_str().to_str() {
311            Some(s) => Ok(s.as_bytes()),
312            None => Err(anyhow::format_err!(
313                "invalid non-unicode path: {}",
314                path.display()
315            )),
316        }
317    }
318}
319
320/// Converts UTF-8 bytes to a path.
321pub fn bytes2path(bytes: &[u8]) -> Result<PathBuf> {
322    #[cfg(unix)]
323    {
324        use std::os::unix::prelude::*;
325        Ok(PathBuf::from(OsStr::from_bytes(bytes)))
326    }
327    #[cfg(windows)]
328    {
329        use std::str;
330        match str::from_utf8(bytes) {
331            Ok(s) => Ok(PathBuf::from(s)),
332            Err(..) => Err(anyhow::format_err!("invalid non-unicode path")),
333        }
334    }
335    #[cfg(not(any(unix, windows)))]
336    {
337        use std::str;
338        match str::from_utf8(bytes) {
339            Ok(s) => Ok(PathBuf::from(s)),
340            Err(..) => Err(anyhow::format_err!("invalid non-unicode path")),
341        }
342    }
343}
344
345/// Returns an iterator that walks up the directory hierarchy towards the root.
346///
347/// Each item is a [`Path`]. It will start with the given path, finishing at
348/// the root. If the `stop_root_at` parameter is given, it will stop at the
349/// given path (which will be the last item).
350pub fn ancestors<'a>(path: &'a Path, stop_root_at: Option<&Path>) -> PathAncestors<'a> {
351    PathAncestors::new(path, stop_root_at)
352}
353
354/// An iterator that walks up the directory hierarchy towards the root.
355pub struct PathAncestors<'a> {
356    current: Option<&'a Path>,
357    stop_at: Option<PathBuf>,
358}
359
360impl<'a> PathAncestors<'a> {
361    fn new(path: &'a Path, stop_root_at: Option<&Path>) -> PathAncestors<'a> {
362        let stop_at = env::var("__CARGO_TEST_ROOT")
363            .ok()
364            .map(PathBuf::from)
365            .or_else(|| stop_root_at.map(|p| p.to_path_buf()));
366        PathAncestors {
367            current: Some(path),
368            //HACK: avoid reading `~/.cargo/config` when testing Cargo itself.
369            stop_at,
370        }
371    }
372}
373
374impl<'a> Iterator for PathAncestors<'a> {
375    type Item = &'a Path;
376
377    fn next(&mut self) -> Option<&'a Path> {
378        if let Some(path) = self.current {
379            self.current = path.parent();
380
381            if let Some(ref stop_at) = self.stop_at
382                && path == stop_at
383            {
384                self.current = None;
385            }
386
387            Some(path)
388        } else {
389            None
390        }
391    }
392}
393
394/// Equivalent to [`std::fs::create_dir_all`] with better error messages.
395pub fn create_dir_all(p: impl AsRef<Path>) -> Result<()> {
396    _create_dir_all(p.as_ref())
397}
398
399fn _create_dir_all(p: &Path) -> Result<()> {
400    fs::create_dir_all(p)
401        .with_context(|| format!("failed to create directory `{}`", p.display()))?;
402    Ok(())
403}
404
405/// Equivalent to [`std::fs::remove_dir_all`] with better error messages.
406///
407/// This does *not* follow symlinks.
408pub fn remove_dir_all<P: AsRef<Path>>(p: P) -> Result<()> {
409    _remove_dir_all(p.as_ref()).or_else(|prev_err| {
410        // `std::fs::remove_dir_all` is highly specialized for different platforms
411        // and may be more reliable than a simple walk. We try the walk first in
412        // order to report more detailed errors.
413        fs::remove_dir_all(p.as_ref()).with_context(|| {
414            format!(
415                "{:?}\n\nError: failed to remove directory `{}`",
416                prev_err,
417                p.as_ref().display(),
418            )
419        })
420    })
421}
422
423fn _remove_dir_all(p: &Path) -> Result<()> {
424    if symlink_metadata(p)?.is_symlink() {
425        return remove_file(p);
426    }
427    let entries = p
428        .read_dir()
429        .with_context(|| format!("failed to read directory `{}`", p.display()))?;
430    for entry in entries {
431        let entry = entry?;
432        let path = entry.path();
433        if entry.file_type()?.is_dir() {
434            remove_dir_all(&path)?;
435        } else {
436            remove_file(&path)?;
437        }
438    }
439    remove_dir(p)
440}
441
442/// Equivalent to [`std::fs::remove_dir`] with better error messages.
443pub fn remove_dir<P: AsRef<Path>>(p: P) -> Result<()> {
444    _remove_dir(p.as_ref())
445}
446
447fn _remove_dir(p: &Path) -> Result<()> {
448    fs::remove_dir(p).with_context(|| format!("failed to remove directory `{}`", p.display()))?;
449    Ok(())
450}
451
452/// Equivalent to [`std::fs::remove_file`] with better error messages.
453///
454/// If the file is readonly, this will attempt to change the permissions to
455/// force the file to be deleted.
456/// On Windows, if the file is a symlink to a directory, this will attempt to
457/// remove the symlink itself.
458pub fn remove_file<P: AsRef<Path>>(p: P) -> Result<()> {
459    _remove_file(p.as_ref())
460}
461
462fn _remove_file(p: &Path) -> Result<()> {
463    // For Windows, we need to check if the file is a symlink to a directory
464    // and remove the symlink itself by calling `remove_dir` instead of
465    // `remove_file`.
466    #[cfg(target_os = "windows")]
467    {
468        use std::os::windows::fs::FileTypeExt;
469        let metadata = symlink_metadata(p)?;
470        let file_type = metadata.file_type();
471        if file_type.is_symlink_dir() {
472            return remove_symlink_dir_with_permission_check(p);
473        }
474    }
475
476    remove_file_with_permission_check(p)
477}
478
479#[cfg(target_os = "windows")]
480fn remove_symlink_dir_with_permission_check(p: &Path) -> Result<()> {
481    remove_with_permission_check(fs::remove_dir, p)
482        .with_context(|| format!("failed to remove symlink dir `{}`", p.display()))
483}
484
485fn remove_file_with_permission_check(p: &Path) -> Result<()> {
486    remove_with_permission_check(fs::remove_file, p)
487        .with_context(|| format!("failed to remove file `{}`", p.display()))
488}
489
490fn remove_with_permission_check<F, P>(remove_func: F, p: P) -> io::Result<()>
491where
492    F: Fn(P) -> io::Result<()>,
493    P: AsRef<Path> + Clone,
494{
495    match remove_func(p.clone()) {
496        Ok(()) => Ok(()),
497        Err(e) => {
498            if e.kind() == io::ErrorKind::PermissionDenied
499                && set_not_readonly(p.as_ref()).unwrap_or(false)
500            {
501                remove_func(p)
502            } else {
503                Err(e)
504            }
505        }
506    }
507}
508
509fn set_not_readonly(p: &Path) -> io::Result<bool> {
510    let mut perms = p.metadata()?.permissions();
511    if !perms.readonly() {
512        return Ok(false);
513    }
514
515    #[cfg(unix)]
516    {
517        use std::os::unix::fs::PermissionsExt;
518        perms.set_mode(0o640);
519    }
520    #[cfg(not(unix))]
521    #[allow(clippy::permissions_set_readonly_false)]
522    {
523        perms.set_readonly(false);
524    }
525
526    fs::set_permissions(p, perms)?;
527    Ok(true)
528}
529
530/// Hardlink (file) or symlink (dir) src to dst if possible, otherwise copy it.
531///
532/// If the destination already exists, it is removed before linking.
533pub fn link_or_copy(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> Result<()> {
534    let src = src.as_ref();
535    let dst = dst.as_ref();
536    _link_or_copy(src, dst)
537}
538
539fn _link_or_copy(src: &Path, dst: &Path) -> Result<()> {
540    log::debug!("linking {} to {}", src.display(), dst.display());
541    if same_file::is_same_file(src, dst).unwrap_or(false) {
542        return Ok(());
543    }
544
545    // NB: we can't use dst.exists(), as if dst is a broken symlink,
546    // dst.exists() will return false. This is problematic, as we still need to
547    // unlink dst in this case. symlink_metadata(dst).is_ok() will tell us
548    // whether dst exists *without* following symlinks, which is what we want.
549    if fs::symlink_metadata(dst).is_ok() {
550        remove_file(dst)?;
551    }
552
553    let link_result = if src.is_dir() {
554        #[cfg(target_os = "redox")]
555        use std::os::redox::fs::symlink;
556        #[cfg(unix)]
557        use std::os::unix::fs::symlink;
558        #[cfg(windows)]
559        // FIXME: This should probably panic or have a copy fallback. Symlinks
560        // are not supported in all windows environments. Currently symlinking
561        // is only used for .dSYM directories on macos, but this shouldn't be
562        // accidentally relied upon.
563        use std::os::windows::fs::symlink_dir as symlink;
564
565        let dst_dir = dst.parent().unwrap();
566        let src = if src.starts_with(dst_dir) {
567            src.strip_prefix(dst_dir).unwrap()
568        } else {
569            src
570        };
571        #[cfg(any(unix, windows, target_os = "redox"))]
572        {
573            symlink(src, dst)
574        }
575        #[cfg(not(any(unix, windows, target_os = "redox")))]
576        {
577            let _ = src;
578            Err(io::Error::new(
579                io::ErrorKind::Unsupported,
580                "symlinks are not supported on this target",
581            ))
582        }
583    } else if cfg!(target_os = "macos") {
584        // There seems to be a race condition with APFS when hard-linking
585        // binaries. Gatekeeper does not have signing or hash information
586        // stored in kernel when running the process. Therefore killing it.
587        // This problem does not appear when copying files as kernel has
588        // time to process it. Note that: fs::copy on macos is using
589        // CopyOnWrite (syscall fclonefileat) which should be as fast as
590        // hardlinking. See these issues for the details:
591        //
592        // * https://github.com/rust-lang/cargo/issues/7821
593        // * https://github.com/rust-lang/cargo/issues/10060
594        fs::copy(src, dst).map_or_else(
595            |e| {
596                if e.raw_os_error()
597                    .map_or(false, |os_err| os_err == 35 /* libc::EAGAIN */)
598                {
599                    log::info!("copy failed {e:?}. falling back to fs::hard_link");
600
601                    // Working around an issue copying too fast with zfs (probably related to
602                    // https://github.com/openzfsonosx/zfs/issues/809)
603                    // See https://github.com/rust-lang/cargo/issues/13838
604                    fs::hard_link(src, dst)
605                } else {
606                    Err(e)
607                }
608            },
609            |_| Ok(()),
610        )
611    } else {
612        fs::hard_link(src, dst)
613    };
614    link_result
615        .or_else(|err| {
616            log::debug!("link failed {err}. falling back to fs::copy");
617            fs::copy(src, dst).map(|_| ())
618        })
619        .with_context(|| {
620            format!(
621                "failed to link or copy `{}` to `{}`",
622                src.display(),
623                dst.display()
624            )
625        })?;
626    Ok(())
627}
628
629/// Copies a file from one location to another.
630///
631/// Equivalent to [`std::fs::copy`] with better error messages.
632pub fn copy<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> Result<u64> {
633    let from = from.as_ref();
634    let to = to.as_ref();
635    fs::copy(from, to)
636        .with_context(|| format!("failed to copy `{}` to `{}`", from.display(), to.display()))
637}
638
639/// Recursively copies a directory.
640///
641/// Symlinks are followed and copied as their target contents.
642pub fn copy_dir_all<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> Result<()> {
643    let from = from.as_ref();
644    let to = to.as_ref();
645    _copy_dir_all(from, to)
646}
647
648fn _copy_dir_all(from: &Path, to: &Path) -> Result<()> {
649    create_dir_all(to)?;
650    let entries = from
651        .read_dir()
652        .with_context(|| format!("failed to read directory `{}`", from.display()))?;
653
654    for entry in entries {
655        let entry = entry
656            .with_context(|| format!("failed to read directory entry in `{}`", from.display()))?;
657        let source = entry.path();
658        let target = to.join(entry.file_name());
659        let file_type = entry
660            .file_type()
661            .with_context(|| format!("failed to read file type for `{}`", source.display()))?;
662
663        if file_type.is_dir() {
664            copy_dir_all(&source, &target)?;
665        } else if file_type.is_file() {
666            copy(&source, &target)?;
667        } else if file_type.is_symlink() {
668            let metadata = metadata(&source)?;
669            if metadata.is_dir() {
670                copy_dir_all(&source, &target)?;
671            } else {
672                copy(&source, &target)?;
673            }
674        }
675    }
676
677    Ok(())
678}
679
680/// Strips `base` from `path`.
681///
682/// This canonicalizes both paths before stripping. This is useful if the
683/// paths are obtained in different ways, and one or the other may or may not
684/// have been normalized in some way.
685pub fn strip_prefix_canonical<P: AsRef<Path>>(
686    path: P,
687    base: P,
688) -> Result<PathBuf, std::path::StripPrefixError> {
689    // Not all filesystems support canonicalize. Just ignore if it doesn't work.
690    let safe_canonicalize = |path: &Path| match path.canonicalize() {
691        Ok(p) => p,
692        Err(e) => {
693            log::warn!("cannot canonicalize {path:?}: {e:?}");
694            path.to_path_buf()
695        }
696    };
697    let canon_path = safe_canonicalize(path.as_ref());
698    let canon_base = safe_canonicalize(base.as_ref());
699    canon_path.strip_prefix(canon_base).map(|p| p.to_path_buf())
700}
701
702/// Creates an excluded from cache directory atomically with its parents as
703/// needed.
704///
705/// The atomicity only covers creating the leaf directory and exclusion from
706/// cache. Any missing parent directories will not be created in an atomic
707/// manner.
708///
709/// This function is idempotent and in addition to that it won't exclude ``p``
710/// from cache if it already exists.
711pub fn create_dir_all_excluded_from_backups_atomic(p: impl AsRef<Path>) -> Result<()> {
712    let path = p.as_ref();
713    if path.is_dir() {
714        return Ok(());
715    }
716
717    let parent = path.parent().unwrap();
718    let base = path.file_name().unwrap();
719    create_dir_all(parent)?;
720    // We do this in two steps (first create a temporary directory and exclude
721    // it from backups, then rename it to the desired name. If we created the
722    // directory directly where it should be and then excluded it from backups
723    // we would risk a situation where cargo is interrupted right after the
724    // directory creation but before the exclusion the directory would remain
725    // non-excluded from backups because we only perform exclusion right after
726    // we created the directory ourselves.
727    //
728    // We need the tempdir created in parent instead of $TMP, because only then we
729    // can be easily sure that rename() will succeed (the new name needs to be
730    // on the same mount point as the old one).
731    let tempdir = TempFileBuilder::new().prefix(base).tempdir_in(parent)?;
732    exclude_from_backups(tempdir.path());
733    exclude_from_content_indexing(tempdir.path());
734    // Previously std::fs::create_dir_all() (through paths::create_dir_all()) was
735    // used here to create the directory directly and fs::create_dir_all()
736    // explicitly treats the directory being created concurrently by another
737    // thread or process as success, hence the check below to follow the
738    // existing behavior. If we get an error at rename() and suddenly the
739    // directory (which didn't exist a moment earlier) exists we can infer from
740    // it's another cargo process doing work.
741    if let Err(e) = fs::rename(tempdir.path(), path)
742        && !path.exists()
743    {
744        return Err(anyhow::Error::from(e))
745            .with_context(|| format!("failed to create directory `{}`", path.display()));
746    }
747    Ok(())
748}
749
750/// Mark an existing directory as excluded from backups and indexing.
751///
752/// Errors in marking it are ignored.
753pub fn exclude_from_backups_and_indexing(p: impl AsRef<Path>) {
754    let path = p.as_ref();
755    exclude_from_backups(path);
756    exclude_from_content_indexing(path);
757}
758
759/// Marks the directory as excluded from archives/backups.
760///
761/// This is recommended to prevent derived/temporary files from bloating
762/// backups. There are two mechanisms used to achieve this right now:
763///
764/// * A dedicated resource property excluding from Time Machine backups on macOS
765/// * CACHEDIR.TAG files supported by various tools in a platform-independent
766///   way
767fn exclude_from_backups(path: &Path) {
768    exclude_from_time_machine(path);
769    let file = path.join("CACHEDIR.TAG");
770    if !file.exists() {
771        let _ = std::fs::write(
772            file,
773            "Signature: 8a477f597d28d172789f06886806bc55
774# This file is a cache directory tag created by cargo.
775# For information about cache directory tags see https://bford.info/cachedir/
776",
777        );
778        // Similarly to exclude_from_time_machine() we ignore errors here as
779        // it's an optional feature.
780    }
781}
782
783/// Marks the directory as excluded from content indexing.
784///
785/// This is recommended to prevent the content of derived/temporary files from
786/// being indexed. This is very important for Windows users, as the live content
787/// indexing significantly slows cargo's I/O operations.
788///
789/// This is currently a no-op on non-Windows platforms.
790fn exclude_from_content_indexing(path: &Path) {
791    #[cfg(windows)]
792    {
793        use std::iter::once;
794        use std::os::windows::prelude::OsStrExt;
795        use windows_sys::Win32::Storage::FileSystem::{
796            FILE_ATTRIBUTE_NOT_CONTENT_INDEXED, GetFileAttributesW, SetFileAttributesW,
797        };
798
799        let path: Vec<u16> = path.as_os_str().encode_wide().chain(once(0)).collect();
800        // SAFETY: this is implemented by the cargo
801        unsafe {
802            SetFileAttributesW(
803                path.as_ptr(),
804                GetFileAttributesW(path.as_ptr()) | FILE_ATTRIBUTE_NOT_CONTENT_INDEXED,
805            );
806        }
807    }
808    #[cfg(not(windows))]
809    {
810        let _ = path;
811    }
812}
813
814#[cfg(not(target_os = "macos"))]
815fn exclude_from_time_machine(_: &Path) {}
816
817#[cfg(target_os = "macos")]
818/// Marks files or directories as excluded from Time Machine on macOS
819fn exclude_from_time_machine(path: &Path) {
820    use core_foundation::base::TCFType;
821    use core_foundation::{number, string, url};
822    use std::ptr;
823
824    // For compatibility with 10.7 a string is used instead of global
825    // kCFURLIsExcludedFromBackupKey
826    let is_excluded_key: Result<string::CFString, _> = "NSURLIsExcludedFromBackupKey".parse();
827    let path = url::CFURL::from_path(path, false);
828    if let (Some(path), Ok(is_excluded_key)) = (path, is_excluded_key) {
829        unsafe {
830            url::CFURLSetResourcePropertyForKey(
831                path.as_concrete_TypeRef(),
832                is_excluded_key.as_concrete_TypeRef(),
833                number::kCFBooleanTrue as *const _,
834                ptr::null_mut(),
835            );
836        }
837    }
838    // Errors are ignored, since it's an optional feature and failure
839    // doesn't prevent Cargo from working
840}
841
842#[cfg(test)]
843mod tests {
844    use super::join_paths;
845    use super::write;
846    use super::write_atomic;
847
848    #[test]
849    fn write_works() {
850        let original_contents = "[dependencies]\nfoo = 0.1.0";
851
852        let tmpdir = tempfile::tempdir().unwrap();
853        let path = tmpdir.path().join("Cargo.toml");
854        write(&path, original_contents).unwrap();
855        let contents = std::fs::read_to_string(&path).unwrap();
856        assert_eq!(contents, original_contents);
857    }
858    #[test]
859    fn write_atomic_works() {
860        let original_contents = "[dependencies]\nfoo = 0.1.0";
861
862        let tmpdir = tempfile::tempdir().unwrap();
863        let path = tmpdir.path().join("Cargo.toml");
864        write_atomic(&path, original_contents).unwrap();
865        let contents = std::fs::read_to_string(&path).unwrap();
866        assert_eq!(contents, original_contents);
867    }
868
869    #[test]
870    #[cfg(unix)]
871    fn write_atomic_permissions() {
872        use std::os::unix::fs::PermissionsExt;
873
874        #[allow(clippy::useless_conversion)]
875        let perms = u32::from(libc::S_IRWXU | libc::S_IRGRP | libc::S_IWGRP | libc::S_IROTH);
876        let original_perms = std::fs::Permissions::from_mode(perms);
877
878        let tmp = tempfile::Builder::new().tempfile().unwrap();
879
880        // need to set the permissions after creating the file to avoid umask
881        tmp.as_file()
882            .set_permissions(original_perms.clone())
883            .unwrap();
884
885        // after this call, the file at `tmp.path()` will not be the same as the file
886        // held by `tmp`
887        write_atomic(tmp.path(), "new").unwrap();
888        assert_eq!(std::fs::read_to_string(tmp.path()).unwrap(), "new");
889
890        let new_perms = std::fs::metadata(tmp.path()).unwrap().permissions();
891
892        #[allow(clippy::useless_conversion)]
893        let mask = u32::from(libc::S_IRWXU | libc::S_IRWXG | libc::S_IRWXO);
894        assert_eq!(original_perms.mode(), new_perms.mode() & mask);
895    }
896
897    #[test]
898    fn join_paths_lists_paths_on_error() {
899        let valid_paths = vec!["/testing/one", "/testing/two"];
900        // does not fail on valid input
901        let _joined = join_paths(&valid_paths, "TESTING1").unwrap();
902
903        #[cfg(unix)]
904        {
905            let invalid_paths = vec!["/testing/one", "/testing/t:wo/three"];
906            let err = join_paths(&invalid_paths, "TESTING2").unwrap_err();
907            assert_eq!(
908                err.to_string(),
909                "failed to join paths from `$TESTING2` together\n\n\
910             Check if any of path segments listed below contain an \
911             unterminated quote character or path separator:\
912             \n    \"/testing/one\"\
913             \n    \"/testing/t:wo/three\"\
914             "
915            );
916        }
917        #[cfg(windows)]
918        {
919            let invalid_paths = vec!["/testing/one", "/testing/t\"wo/three"];
920            let err = join_paths(&invalid_paths, "TESTING2").unwrap_err();
921            assert_eq!(
922                err.to_string(),
923                "failed to join paths from `$TESTING2` together\n\n\
924             Check if any of path segments listed below contain an \
925             unterminated quote character or path separator:\
926             \n    \"/testing/one\"\
927             \n    \"/testing/t\\\"wo/three\"\
928             "
929            );
930        }
931    }
932
933    #[test]
934    #[cfg(windows)]
935    fn test_remove_symlink_dir() {
936        use super::*;
937        use std::fs;
938        use std::os::windows::fs::symlink_dir;
939
940        let tmpdir = tempfile::tempdir().unwrap();
941        let dir_path = tmpdir.path().join("testdir");
942        let symlink_path = tmpdir.path().join("symlink");
943
944        fs::create_dir(&dir_path).unwrap();
945
946        symlink_dir(&dir_path, &symlink_path).expect("failed to create symlink");
947
948        assert!(symlink_path.exists());
949
950        assert!(remove_file(symlink_path.clone()).is_ok());
951
952        assert!(!symlink_path.exists());
953        assert!(dir_path.exists());
954    }
955
956    #[test]
957    #[cfg(windows)]
958    fn test_remove_symlink_file() {
959        use super::*;
960        use std::fs;
961        use std::os::windows::fs::symlink_file;
962
963        let tmpdir = tempfile::tempdir().unwrap();
964        let file_path = tmpdir.path().join("testfile");
965        let symlink_path = tmpdir.path().join("symlink");
966
967        fs::write(&file_path, b"test").unwrap();
968
969        symlink_file(&file_path, &symlink_path).expect("failed to create symlink");
970
971        assert!(symlink_path.exists());
972
973        assert!(remove_file(symlink_path.clone()).is_ok());
974
975        assert!(!symlink_path.exists());
976        assert!(file_path.exists());
977    }
978}