Skip to content

Commit 339bbcf

Browse files
committed
build_helper: add recursive_remove helper
`recursive_remove` is intended to be a wrapper around `std::fs::remove_dir_all`, but which also allows the removal target to be a non-directory entry, i.e. a file or a symlink. It also tries to remove read-only attributes from filesystem entities on Windows.
1 parent 582e910 commit 339bbcf

File tree

3 files changed

+314
-0
lines changed

3 files changed

+314
-0
lines changed

src/build_helper/src/fs/mod.rs

+107
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
//! Misc filesystem related helpers for use by bootstrap and tools.
2+
3+
use std::path::Path;
4+
use std::{fs, io};
5+
6+
#[cfg(test)]
7+
mod tests;
8+
9+
/// Helper to ignore [`std::io::ErrorKind::NotFound`], but still propagate other
10+
/// [`std::io::ErrorKind`]s.
11+
pub fn ignore_not_found<Op>(mut op: Op) -> io::Result<()>
12+
where
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 [`std::fs::remove_dir_all`]-like helper recursively remotes filesystem entities starting at
23+
/// the provided root path. This helper supports being used on a single non-directory entity, e.g. a
24+
/// symlink or file.
25+
///
26+
/// - This will produce an error if the target path is not found.
27+
/// - Like [`std::fs::remove_dir_all`], this helper does not traverse symbolic links, will remove
28+
/// symbolic link itself.
29+
/// - This helper will try to remove even read-only files and directories on Windows.
30+
/// - This helper is **not** robust against races on the underlying filesystem, behavior is
31+
/// unspecified if this helper is called concurrently.
32+
/// - This helper is not robust against TOCTOU problems.
33+
///
34+
/// FIXME: this implementation is insufficiently robust to replace bootstrap's clean `rm_rf`
35+
/// implementation as this implementation currently does not perform retries.
36+
#[track_caller]
37+
pub fn recursive_remove<P: AsRef<Path>>(path: P) -> io::Result<()> {
38+
let path = path.as_ref();
39+
let metadata = fs::symlink_metadata(path)?;
40+
if metadata.is_dir() {
41+
for e in fs::read_dir(path)? {
42+
let entry = e?;
43+
let entry_metadata = fs::symlink_metadata(&entry.path())?;
44+
if entry_metadata.is_dir() {
45+
recursive_remove(entry.path())?;
46+
} else {
47+
recursive_remove_single(&entry.path())?;
48+
}
49+
}
50+
fs::remove_dir(&path)?;
51+
} else {
52+
recursive_remove_single(path)?;
53+
}
54+
55+
Ok(())
56+
}
57+
58+
fn recursive_remove_single(path: &Path) -> io::Result<()> {
59+
let metadata = fs::symlink_metadata(path)?;
60+
61+
if metadata.is_file() {
62+
try_remove_op_override_perm(fs::remove_file, path, metadata)
63+
} else if metadata.is_symlink() {
64+
#[cfg(windows)]
65+
{
66+
use std::os::windows::fs::FileTypeExt;
67+
if metadata.file_type().is_symlink_dir() {
68+
try_remove_op_override_perm(fs::remove_dir, path, metadata)
69+
} else {
70+
try_remove_op_override_perm(fs::remove_file, path, metadata)
71+
}
72+
}
73+
#[cfg(not(windows))]
74+
{
75+
try_remove_op_override_perm(fs::remove_file, path, metadata)
76+
}
77+
} else if metadata.is_dir() {
78+
unreachable!("`recursive_remove_single` does not expect dir: {}", path.display())
79+
} else {
80+
// Unrecognized file type, let it go.
81+
Ok(())
82+
}
83+
}
84+
85+
fn try_remove_op_override_perm<'p, Op>(
86+
mut remove_op: Op,
87+
path: &'p Path,
88+
metadata: fs::Metadata,
89+
) -> io::Result<()>
90+
where
91+
Op: FnMut(&'p Path) -> io::Result<()>,
92+
{
93+
match remove_op(path) {
94+
Ok(()) => Ok(()),
95+
// On Windows, we need to remove read-only attribute for a read-only file.
96+
#[cfg(windows)]
97+
Err(e) if e.kind() == io::ErrorKind::PermissionDenied => {
98+
// Try to remove the read-only attribute.
99+
let mut perms = metadata.permissions();
100+
perms.set_readonly(false);
101+
fs::set_permissions(path, perms)?;
102+
// Then try again.
103+
remove_op(path)
104+
}
105+
Err(e) => Err(e),
106+
}
107+
}

src/build_helper/src/fs/tests.rs

+206
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
#![deny(unused_must_use)]
2+
3+
use std::{env, fs, io};
4+
5+
use super::recursive_remove;
6+
7+
mod recursive_remove_tests {
8+
use super::*;
9+
10+
// Basic cases
11+
12+
#[test]
13+
fn nonexistent_path() {
14+
let tmpdir = env::temp_dir();
15+
let path = tmpdir.join("__INTERNAL_BOOTSTRAP_nonexistent_path");
16+
assert!(fs::symlink_metadata(&path).is_err_and(|e| e.kind() == io::ErrorKind::NotFound));
17+
assert!(recursive_remove(&path).is_err_and(|e| e.kind() == io::ErrorKind::NotFound));
18+
}
19+
20+
#[test]
21+
fn file() {
22+
let tmpdir = env::temp_dir();
23+
let path = tmpdir.join("__INTERNAL_BOOTSTRAP_file");
24+
fs::write(&path, b"").unwrap();
25+
assert!(fs::symlink_metadata(&path).is_ok());
26+
assert!(recursive_remove(&path).is_ok());
27+
assert!(fs::symlink_metadata(&path).is_err_and(|e| e.kind() == io::ErrorKind::NotFound));
28+
}
29+
30+
mod dir_tests {
31+
use super::*;
32+
33+
#[test]
34+
fn dir_empty() {
35+
let tmpdir = env::temp_dir();
36+
let path = tmpdir.join("__INTERNAL_BOOTSTRAP_dir_tests_dir_empty");
37+
fs::create_dir_all(&path).unwrap();
38+
assert!(fs::symlink_metadata(&path).is_ok());
39+
assert!(recursive_remove(&path).is_ok());
40+
assert!(
41+
fs::symlink_metadata(&path).is_err_and(|e| e.kind() == io::ErrorKind::NotFound)
42+
);
43+
}
44+
45+
#[test]
46+
fn dir_recursive() {
47+
let tmpdir = env::temp_dir();
48+
let path = tmpdir.join("__INTERNAL_BOOTSTRAP_dir_tests_dir_recursive");
49+
fs::create_dir_all(&path).unwrap();
50+
assert!(fs::symlink_metadata(&path).is_ok());
51+
52+
let file_a = path.join("a.txt");
53+
fs::write(&file_a, b"").unwrap();
54+
assert!(fs::symlink_metadata(&file_a).is_ok());
55+
56+
let dir_b = path.join("b");
57+
fs::create_dir_all(&dir_b).unwrap();
58+
assert!(fs::symlink_metadata(&dir_b).is_ok());
59+
60+
let file_c = dir_b.join("c.rs");
61+
fs::write(&file_c, b"").unwrap();
62+
assert!(fs::symlink_metadata(&file_c).is_ok());
63+
64+
assert!(recursive_remove(&path).is_ok());
65+
66+
assert!(
67+
fs::symlink_metadata(&file_a).is_err_and(|e| e.kind() == io::ErrorKind::NotFound)
68+
);
69+
assert!(
70+
fs::symlink_metadata(&dir_b).is_err_and(|e| e.kind() == io::ErrorKind::NotFound)
71+
);
72+
assert!(
73+
fs::symlink_metadata(&file_c).is_err_and(|e| e.kind() == io::ErrorKind::NotFound)
74+
);
75+
}
76+
}
77+
78+
/// Check that [`recursive_remove`] does not traverse symlinks and only removes symlinks
79+
/// themselves.
80+
///
81+
/// Symlink-to-file versus symlink-to-dir is a distinction that's important on Windows, but not
82+
/// on Unix.
83+
mod symlink_tests {
84+
use super::*;
85+
86+
#[cfg(unix)]
87+
#[test]
88+
fn unix_symlink() {
89+
let tmpdir = env::temp_dir();
90+
let path = tmpdir.join("__INTERNAL_BOOTSTRAP_symlink_tests_unix_symlink");
91+
let symlink_path =
92+
tmpdir.join("__INTERNAL_BOOTSTRAP__symlink_tests_unix_symlink_symlink");
93+
fs::write(&path, b"").unwrap();
94+
95+
assert!(fs::symlink_metadata(&path).is_ok());
96+
assert!(
97+
fs::symlink_metadata(&symlink_path)
98+
.is_err_and(|e| e.kind() == io::ErrorKind::NotFound)
99+
);
100+
101+
std::os::unix::fs::symlink(&path, &symlink_path).unwrap();
102+
103+
assert!(recursive_remove(&symlink_path).is_ok());
104+
105+
// Check that the symlink got removed...
106+
assert!(
107+
fs::symlink_metadata(&symlink_path)
108+
.is_err_and(|e| e.kind() == io::ErrorKind::NotFound)
109+
);
110+
// ... but pointed-to file still exists.
111+
assert!(fs::symlink_metadata(&path).is_ok());
112+
113+
fs::remove_file(&path).unwrap();
114+
}
115+
116+
#[cfg(windows)]
117+
#[test]
118+
fn windows_symlink_to_file() {
119+
let tmpdir = env::temp_dir();
120+
let path = tmpdir.join("__INTERNAL_BOOTSTRAP_symlink_tests_windows_symlink_to_file");
121+
let symlink_path = tmpdir
122+
.join("__INTERNAL_BOOTSTRAP_SYMLINK_symlink_tests_windows_symlink_to_file_symlink");
123+
fs::write(&path, b"").unwrap();
124+
125+
assert!(fs::symlink_metadata(&path).is_ok());
126+
assert!(
127+
fs::symlink_metadata(&symlink_path)
128+
.is_err_and(|e| e.kind() == io::ErrorKind::NotFound)
129+
);
130+
131+
std::os::windows::fs::symlink_file(&path, &symlink_path).unwrap();
132+
133+
assert!(recursive_remove(&symlink_path).is_ok());
134+
135+
// Check that the symlink-to-file got removed...
136+
assert!(
137+
fs::symlink_metadata(&symlink_path)
138+
.is_err_and(|e| e.kind() == io::ErrorKind::NotFound)
139+
);
140+
// ... but pointed-to file still exists.
141+
assert!(fs::symlink_metadata(&path).is_ok());
142+
143+
fs::remove_file(&path).unwrap();
144+
}
145+
146+
#[cfg(windows)]
147+
#[test]
148+
fn windows_symlink_to_dir() {
149+
let tmpdir = env::temp_dir();
150+
let path = tmpdir.join("__INTERNAL_BOOTSTRAP_symlink_tests_windows_symlink_to_dir");
151+
let symlink_path =
152+
tmpdir.join("__INTERNAL_BOOTSTRAP_symlink_tests_windows_symlink_to_dir_symlink");
153+
fs::create_dir_all(&path).unwrap();
154+
155+
assert!(fs::symlink_metadata(&path).is_ok());
156+
assert!(
157+
fs::symlink_metadata(&symlink_path)
158+
.is_err_and(|e| e.kind() == io::ErrorKind::NotFound)
159+
);
160+
161+
std::os::windows::fs::symlink_dir(&path, &symlink_path).unwrap();
162+
163+
assert!(recursive_remove(&symlink_path).is_ok());
164+
165+
// Check that the symlink-to-dir got removed...
166+
assert!(
167+
fs::symlink_metadata(&symlink_path)
168+
.is_err_and(|e| e.kind() == io::ErrorKind::NotFound)
169+
);
170+
// ... but pointed-to dir still exists.
171+
assert!(fs::symlink_metadata(&path).is_ok());
172+
173+
fs::remove_dir_all(&path).unwrap();
174+
}
175+
}
176+
177+
/// Read-only file and directories only need special handling on Windows.
178+
#[cfg(windows)]
179+
mod readonly_tests {
180+
use super::*;
181+
182+
#[test]
183+
fn overrides_readonly() {
184+
let tmpdir = env::temp_dir();
185+
let path = tmpdir.join("__INTERNAL_BOOTSTRAP_readonly_tests_overrides_readonly");
186+
fs::write(&path, b"").unwrap();
187+
188+
let mut perms = fs::symlink_metadata(&path).unwrap().permissions();
189+
perms.set_readonly(true);
190+
fs::set_permissions(&path, perms).unwrap();
191+
192+
// Check that file exists but is read-only, and that normal `std::fs::remove_file` fails
193+
// to delete the file.
194+
assert!(fs::symlink_metadata(&path).is_ok_and(|m| m.permissions().readonly()));
195+
assert!(
196+
fs::remove_file(&path).is_err_and(|e| e.kind() == io::ErrorKind::PermissionDenied)
197+
);
198+
199+
assert!(recursive_remove(&path).is_ok());
200+
201+
assert!(
202+
fs::symlink_metadata(&path).is_err_and(|e| e.kind() == io::ErrorKind::NotFound)
203+
);
204+
}
205+
}
206+
}

src/build_helper/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
33
pub mod ci;
44
pub mod drop_bomb;
5+
pub mod fs;
56
pub mod git;
67
pub mod metrics;
78
pub mod stage0_parser;

0 commit comments

Comments
 (0)