git_policy_cli/subvolume_remove.rs
1//! What:
2//! Remove a directory tree, making each directory the user owns writable on the way down.
3//! Why:
4//! The migration preserves permission bits, so a checkout can hold a mode-555 directory with files in it. Unlinking a
5//! file needs write permission on its directory, so `std::fs::remove_dir_all` stops at the first such directory and
6//! both the deletion of the old tree and the emptying of a subvolume fail partway. Every directory is opened without
7//! following links and given the owner's read, write and search bits through its descriptor before the tree is
8//! removed; a directory the user does not own keeps its mode and the removal reports the operating system's refusal.
9//!
10//! In TS you'd write (pseudocode):
11//! ```ts
12//! await rm(path, { recursive: true, force: true }); // after chmod u+rwx on every directory in it
13//! ```
14
15use super::subvolume_anchor::Anchor;
16use std::os::unix::fs::{MetadataExt, PermissionsExt};
17use std::path::{Path, PathBuf};
18
19/// What:
20/// Give the owner read, write and search permission on every directory below `top` (and on `top`), never following a
21/// link.
22/// The walk keeps a list of directories still to visit, not recursion, so depth costs no stack.
23/// Why:
24/// See the module description. A directory that cannot be opened is skipped here; the removal that follows names it.
25///
26/// In TS you'd write (pseudocode):
27/// ```ts
28/// await makeDirectoriesWritable(top);
29/// ```
30fn make_directories_writable(top: &Path) {
31 let mut pending: Vec<PathBuf> = vec![PathBuf::new()];
32 while let Some(relative) = pending.pop() {
33 let Ok(anchor) = Anchor::open_below(top, relative.as_path()) else { continue };
34 if let Ok(metadata) = anchor.file().metadata() && metadata.mode() & 0o700 != 0o700 {
35 let _widened = anchor.file().set_permissions(std::fs::Permissions::from_mode(metadata.mode() | 0o700));
36 }
37 let Ok(listing) = std::fs::read_dir(anchor.path()) else { continue };
38 for item in listing.flatten() {
39 if item.file_type().is_ok_and(|kind: std::fs::FileType| { return kind.is_dir(); }) {
40 pending.push(relative.join(item.file_name()));
41 }
42 }
43 }
44}
45
46/// What:
47/// Remove `path`, whatever it is: a file or link is unlinked, a directory is emptied and removed.
48/// Why:
49/// Links are never followed, as `remove_dir_all` guarantees; see the module description for the permissions.
50///
51/// In TS you'd write (pseudocode):
52/// ```ts
53/// await removeTree(path);
54/// ```
55pub fn remove_tree(path: &Path) -> std::io::Result<()> {
56 let metadata: std::fs::Metadata = std::fs::symlink_metadata(path)?;
57 if !metadata.is_dir() {
58 return std::fs::remove_file(path);
59 }
60 make_directories_writable(path);
61 return std::fs::remove_dir_all(path);
62}
63
64/// Removal controls stay out of the release executable.
65#[cfg(test)]
66#[path = "subvolume_remove_tests.rs"]
67mod tests;