diff options
| author | Jieyou Xu <jieyouxu@outlook.com> | 2024-12-22 23:38:28 +0800 |
|---|---|---|
| committer | 许杰友 Jieyou Xu (Joe) <39484203+jieyouxu@users.noreply.github.com> | 2024-12-23 03:25:36 +0800 |
| commit | 98fdcaed50dfc5346263ed40e9eacb1fc5c124ec (patch) | |
| tree | 76105213227399eeb6b451d6196f663bd32dda55 | |
| parent | 3b388eb9b692d27f9c2054ba2e3f350d0fa19afe (diff) | |
| download | rust-98fdcaed50dfc5346263ed40e9eacb1fc5c124ec.tar.gz rust-98fdcaed50dfc5346263ed40e9eacb1fc5c124ec.zip | |
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 for non-dir entries, as `std::fs::remove_dir_all` handles the dir (and its children) read-only cases. Co-authored-by: Chris Denton <chris@chrisdenton.dev>
| -rw-r--r-- | src/build_helper/src/fs/mod.rs | 69 | ||||
| -rw-r--r-- | src/build_helper/src/fs/tests.rs | 214 | ||||
| -rw-r--r-- | src/build_helper/src/lib.rs | 1 |
3 files changed, 284 insertions, 0 deletions
diff --git a/src/build_helper/src/fs/mod.rs b/src/build_helper/src/fs/mod.rs new file mode 100644 index 00000000000..02029846fd1 --- /dev/null +++ b/src/build_helper/src/fs/mod.rs @@ -0,0 +1,69 @@ +//! Misc filesystem related helpers for use by bootstrap and tools. +use std::fs::Metadata; +use std::path::Path; +use std::{fs, io}; + +#[cfg(test)] +mod tests; + +/// Helper to ignore [`std::io::ErrorKind::NotFound`], but still propagate other +/// [`std::io::ErrorKind`]s. +pub fn ignore_not_found<Op>(mut op: Op) -> io::Result<()> +where + Op: FnMut() -> io::Result<()>, +{ + match op() { + Ok(()) => Ok(()), + Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()), + Err(e) => Err(e), + } +} + +/// A wrapper around [`std::fs::remove_dir_all`] that can also be used on *non-directory entries*, +/// including files and symbolic links. +/// +/// - This will produce an error if the target path is not found. +/// - Like [`std::fs::remove_dir_all`], this helper does not traverse symbolic links, will remove +/// symbolic link itself. +/// - This helper is **not** robust against races on the underlying filesystem, behavior is +/// unspecified if this helper is called concurrently. +/// - This helper is not robust against TOCTOU problems. +/// +/// FIXME: this implementation is insufficiently robust to replace bootstrap's clean `rm_rf` +/// implementation: +/// +/// - This implementation currently does not perform retries. +#[track_caller] +pub fn recursive_remove<P: AsRef<Path>>(path: P) -> io::Result<()> { + let path = path.as_ref(); + let metadata = fs::symlink_metadata(path)?; + #[cfg(windows)] + let is_dir_like = |meta: &fs::Metadata| { + use std::os::windows::fs::FileTypeExt; + meta.is_dir() || meta.file_type().is_symlink_dir() + }; + #[cfg(not(windows))] + let is_dir_like = fs::Metadata::is_dir; + + if is_dir_like(&metadata) { + fs::remove_dir_all(path) + } else { + try_remove_op_set_perms(fs::remove_file, path, metadata) + } +} + +fn try_remove_op_set_perms<'p, Op>(mut op: Op, path: &'p Path, metadata: Metadata) -> io::Result<()> +where + Op: FnMut(&'p Path) -> io::Result<()>, +{ + match op(path) { + Ok(()) => Ok(()), + Err(e) if e.kind() == io::ErrorKind::PermissionDenied => { + let mut perms = metadata.permissions(); + perms.set_readonly(false); + fs::set_permissions(path, perms)?; + op(path) + } + Err(e) => Err(e), + } +} diff --git a/src/build_helper/src/fs/tests.rs b/src/build_helper/src/fs/tests.rs new file mode 100644 index 00000000000..1e694393127 --- /dev/null +++ b/src/build_helper/src/fs/tests.rs @@ -0,0 +1,214 @@ +#![deny(unused_must_use)] + +use std::{env, fs, io}; + +use super::recursive_remove; + +mod recursive_remove_tests { + use super::*; + + // Basic cases + + #[test] + fn nonexistent_path() { + let tmpdir = env::temp_dir(); + let path = tmpdir.join("__INTERNAL_BOOTSTRAP_nonexistent_path"); + assert!(fs::symlink_metadata(&path).is_err_and(|e| e.kind() == io::ErrorKind::NotFound)); + assert!(recursive_remove(&path).is_err_and(|e| e.kind() == io::ErrorKind::NotFound)); + } + + #[test] + fn file() { + let tmpdir = env::temp_dir(); + let path = tmpdir.join("__INTERNAL_BOOTSTRAP_file"); + fs::write(&path, b"").unwrap(); + assert!(fs::symlink_metadata(&path).is_ok()); + assert!(recursive_remove(&path).is_ok()); + assert!(fs::symlink_metadata(&path).is_err_and(|e| e.kind() == io::ErrorKind::NotFound)); + } + + mod dir_tests { + use super::*; + + #[test] + fn dir_empty() { + let tmpdir = env::temp_dir(); + let path = tmpdir.join("__INTERNAL_BOOTSTRAP_dir_tests_dir_empty"); + fs::create_dir_all(&path).unwrap(); + assert!(fs::symlink_metadata(&path).is_ok()); + assert!(recursive_remove(&path).is_ok()); + assert!( + fs::symlink_metadata(&path).is_err_and(|e| e.kind() == io::ErrorKind::NotFound) + ); + } + + #[test] + fn dir_recursive() { + let tmpdir = env::temp_dir(); + let path = tmpdir.join("__INTERNAL_BOOTSTRAP_dir_tests_dir_recursive"); + fs::create_dir_all(&path).unwrap(); + assert!(fs::symlink_metadata(&path).is_ok()); + + let file_a = path.join("a.txt"); + fs::write(&file_a, b"").unwrap(); + assert!(fs::symlink_metadata(&file_a).is_ok()); + + let dir_b = path.join("b"); + fs::create_dir_all(&dir_b).unwrap(); + assert!(fs::symlink_metadata(&dir_b).is_ok()); + + let file_c = dir_b.join("c.rs"); + fs::write(&file_c, b"").unwrap(); + assert!(fs::symlink_metadata(&file_c).is_ok()); + + assert!(recursive_remove(&path).is_ok()); + + assert!( + fs::symlink_metadata(&file_a).is_err_and(|e| e.kind() == io::ErrorKind::NotFound) + ); + assert!( + fs::symlink_metadata(&dir_b).is_err_and(|e| e.kind() == io::ErrorKind::NotFound) + ); + assert!( + fs::symlink_metadata(&file_c).is_err_and(|e| e.kind() == io::ErrorKind::NotFound) + ); + } + } + + /// Check that [`recursive_remove`] does not traverse symlinks and only removes symlinks + /// themselves. + /// + /// Symlink-to-file versus symlink-to-dir is a distinction that's important on Windows, but not + /// on Unix. + mod symlink_tests { + use super::*; + + #[cfg(unix)] + #[test] + fn unix_symlink() { + let tmpdir = env::temp_dir(); + let path = tmpdir.join("__INTERNAL_BOOTSTRAP_symlink_tests_unix_symlink"); + let symlink_path = + tmpdir.join("__INTERNAL_BOOTSTRAP__symlink_tests_unix_symlink_symlink"); + fs::write(&path, b"").unwrap(); + + assert!(fs::symlink_metadata(&path).is_ok()); + assert!( + fs::symlink_metadata(&symlink_path) + .is_err_and(|e| e.kind() == io::ErrorKind::NotFound) + ); + + std::os::unix::fs::symlink(&path, &symlink_path).unwrap(); + + assert!(recursive_remove(&symlink_path).is_ok()); + + // Check that the symlink got removed... + assert!( + fs::symlink_metadata(&symlink_path) + .is_err_and(|e| e.kind() == io::ErrorKind::NotFound) + ); + // ... but pointed-to file still exists. + assert!(fs::symlink_metadata(&path).is_ok()); + + fs::remove_file(&path).unwrap(); + } + + #[cfg(windows)] + #[test] + fn windows_symlink_to_file() { + let tmpdir = env::temp_dir(); + let path = tmpdir.join("__INTERNAL_BOOTSTRAP_symlink_tests_windows_symlink_to_file"); + let symlink_path = tmpdir + .join("__INTERNAL_BOOTSTRAP_SYMLINK_symlink_tests_windows_symlink_to_file_symlink"); + fs::write(&path, b"").unwrap(); + + assert!(fs::symlink_metadata(&path).is_ok()); + assert!( + fs::symlink_metadata(&symlink_path) + .is_err_and(|e| e.kind() == io::ErrorKind::NotFound) + ); + + std::os::windows::fs::symlink_file(&path, &symlink_path).unwrap(); + + assert!(recursive_remove(&symlink_path).is_ok()); + + // Check that the symlink-to-file got removed... + assert!( + fs::symlink_metadata(&symlink_path) + .is_err_and(|e| e.kind() == io::ErrorKind::NotFound) + ); + // ... but pointed-to file still exists. + assert!(fs::symlink_metadata(&path).is_ok()); + + fs::remove_file(&path).unwrap(); + } + + #[cfg(windows)] + #[test] + fn windows_symlink_to_dir() { + let tmpdir = env::temp_dir(); + let path = tmpdir.join("__INTERNAL_BOOTSTRAP_symlink_tests_windows_symlink_to_dir"); + let symlink_path = + tmpdir.join("__INTERNAL_BOOTSTRAP_symlink_tests_windows_symlink_to_dir_symlink"); + fs::create_dir_all(&path).unwrap(); + + assert!(fs::symlink_metadata(&path).is_ok()); + assert!( + fs::symlink_metadata(&symlink_path) + .is_err_and(|e| e.kind() == io::ErrorKind::NotFound) + ); + + std::os::windows::fs::symlink_dir(&path, &symlink_path).unwrap(); + + assert!(recursive_remove(&symlink_path).is_ok()); + + // Check that the symlink-to-dir got removed... + assert!( + fs::symlink_metadata(&symlink_path) + .is_err_and(|e| e.kind() == io::ErrorKind::NotFound) + ); + // ... but pointed-to dir still exists. + assert!(fs::symlink_metadata(&path).is_ok()); + + fs::remove_dir_all(&path).unwrap(); + } + } + + /// Read-only file and directories only need special handling on Windows. + #[cfg(windows)] + mod readonly_tests { + use super::*; + + #[test] + fn overrides_readonly() { + let tmpdir = env::temp_dir(); + let path = tmpdir.join("__INTERNAL_BOOTSTRAP_readonly_tests_overrides_readonly"); + + // In case of a previous failed test: + if let Ok(mut perms) = fs::symlink_metadata(&path).map(|m| m.permissions()) { + perms.set_readonly(false); + fs::set_permissions(&path, perms).unwrap(); + fs::remove_file(&path).unwrap(); + } + + fs::write(&path, b"").unwrap(); + + let mut perms = fs::symlink_metadata(&path).unwrap().permissions(); + perms.set_readonly(true); + fs::set_permissions(&path, perms).unwrap(); + + // Check that file exists but is read-only, and that normal `std::fs::remove_file` fails + // to delete the file. + assert!(fs::symlink_metadata(&path).is_ok_and(|m| m.permissions().readonly())); + assert!( + fs::remove_file(&path).is_err_and(|e| e.kind() == io::ErrorKind::PermissionDenied) + ); + + assert!(recursive_remove(&path).is_ok()); + + assert!( + fs::symlink_metadata(&path).is_err_and(|e| e.kind() == io::ErrorKind::NotFound) + ); + } + } +} diff --git a/src/build_helper/src/lib.rs b/src/build_helper/src/lib.rs index 4a4f0ca2a9d..dceb5fdeeea 100644 --- a/src/build_helper/src/lib.rs +++ b/src/build_helper/src/lib.rs @@ -2,6 +2,7 @@ pub mod ci; pub mod drop_bomb; +pub mod fs; pub mod git; pub mod metrics; pub mod stage0_parser; |
