Skip to content

Commit 4c0632e

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.
1 parent a64d822 commit 4c0632e

File tree

3 files changed

+241
-0
lines changed

3 files changed

+241
-0
lines changed

src/build_helper/src/fs/mod.rs

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
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>(op: Op) -> io::Result<()>
12+
where
13+
Op: FnOnce() -> 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 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: this implementation is insufficiently robust to replace bootstrap's clean `rm_rf`
33+
/// implementation:
34+
///
35+
/// - This implementation currently does not perform retries.
36+
/// - This implementation does not try to override fs permissions.
37+
#[track_caller]
38+
pub fn recursive_remove<P: AsRef<Path>>(path: P) -> io::Result<()> {
39+
let path = path.as_ref();
40+
let metadata = fs::symlink_metadata(path)?;
41+
42+
if metadata.is_dir() {
43+
fs::remove_dir_all(path)
44+
} else if metadata.is_file() {
45+
fs::remove_file(path)
46+
} else if metadata.is_symlink() {
47+
#[cfg(windows)]
48+
{
49+
use std::os::windows::fs::FileTypeExt;
50+
if metadata.file_type().is_symlink_dir() {
51+
fs::remove_dir(path)
52+
} else {
53+
fs::remove_file(path)
54+
}
55+
}
56+
#[cfg(not(windows))]
57+
{
58+
fs::remove_file(path)
59+
}
60+
} else {
61+
// Unrecognized file type, let it go.
62+
Ok(())
63+
}
64+
}

src/build_helper/src/fs/tests.rs

+176
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
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+
// Base case
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+
}

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)