build_helper/fs/
mod.rs

1//! Misc filesystem related helpers for use by bootstrap and tools.
2use std::fs::Metadata;
3use std::path::Path;
4use std::{fs, io};
5
6#[cfg(test)]
7mod tests;
8
9/// Helper to ignore [`std::io::ErrorKind::NotFound`], but still propagate other
10/// [`std::io::ErrorKind`]s.
11pub fn ignore_not_found<Op>(mut op: Op) -> io::Result<()>
12where
13    Op: FnMut() -> io::Result<()>,
14{
15    match op() {
16        Ok(()) => Ok(()),
17        Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()),
18        Err(e) => Err(e),
19    }
20}
21
22/// A wrapper around [`std::fs::remove_dir_all`] that can also be used on *non-directory entries*,
23/// including files and symbolic links.
24///
25/// - This will not produce an error if the target path is not found.
26/// - Like [`std::fs::remove_dir_all`], this helper does not traverse symbolic links, will remove
27///   symbolic link itself.
28/// - This helper is **not** robust against races on the underlying filesystem, behavior is
29///   unspecified if this helper is called concurrently.
30/// - This helper is not robust against TOCTOU problems.
31///
32/// FIXME: Audit whether this implementation is robust enough to replace bootstrap's clean `rm_rf`.
33#[track_caller]
34pub fn recursive_remove<P: AsRef<Path>>(path: P) -> io::Result<()> {
35    let path = path.as_ref();
36
37    // If the path doesn't exist, we treat it as a successful no-op.
38    // From the caller's perspective, the goal is simply "ensure this file/dir is gone" —
39    // if it's already not there, that's a success, not an error.
40    let metadata = match fs::symlink_metadata(path) {
41        Ok(m) => m,
42        Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(()),
43        Err(e) => return Err(e),
44    };
45
46    #[cfg(windows)]
47    let is_dir_like = |meta: &fs::Metadata| {
48        use std::os::windows::fs::FileTypeExt;
49        meta.is_dir() || meta.file_type().is_symlink_dir()
50    };
51    #[cfg(not(windows))]
52    let is_dir_like = fs::Metadata::is_dir;
53
54    const MAX_RETRIES: usize = 5;
55    const RETRY_DELAY_MS: u64 = 100;
56
57    let try_remove = || {
58        if is_dir_like(&metadata) {
59            fs::remove_dir_all(path)
60        } else {
61            try_remove_op_set_perms(fs::remove_file, path, metadata.clone())
62        }
63    };
64
65    // Retry deletion a few times to handle transient filesystem errors.
66    // This is unusual for local file operations, but it's a mitigation
67    // against unlikely events where malware scanners may be holding a
68    // file beyond our control, to give the malware scanners some opportunity
69    // to release their hold.
70    for attempt in 0..MAX_RETRIES {
71        match try_remove() {
72            Ok(()) => return Ok(()),
73            Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(()),
74            Err(_) if attempt < MAX_RETRIES - 1 => {
75                std::thread::sleep(std::time::Duration::from_millis(RETRY_DELAY_MS));
76                continue;
77            }
78            Err(e) => return Err(e),
79        }
80    }
81
82    Ok(())
83}
84
85fn try_remove_op_set_perms<'p, Op>(mut op: Op, path: &'p Path, metadata: Metadata) -> io::Result<()>
86where
87    Op: FnMut(&'p Path) -> io::Result<()>,
88{
89    match op(path) {
90        Ok(()) => Ok(()),
91        Err(e) if e.kind() == io::ErrorKind::PermissionDenied => {
92            let mut perms = metadata.permissions();
93            perms.set_readonly(false);
94            fs::set_permissions(path, perms)?;
95            op(path)
96        }
97        Err(e) => Err(e),
98    }
99}
100
101pub fn remove_and_create_dir_all<P: AsRef<Path>>(path: P) -> io::Result<()> {
102    let path = path.as_ref();
103    recursive_remove(path)?;
104    fs::create_dir_all(path)
105}