use super::fs_beneath::{ObjectId, device_of, handle_path, identity_at, identity_of, open_directory_at, remove_at};
use std::ffi::{OsStr, OsString};
use std::fs::File;
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
const TAG: &str = "worktreeTrashRemove";
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Removal {
Removed,
Absent,
}
type Describe<'a> = &'a dyn Fn() -> PathBuf;
struct Frame {
dir: File,
name: OsString,
entries: Vec<(OsString, bool)>,
}
fn is_gone(error: &std::io::Error) -> bool {
return error.kind() == std::io::ErrorKind::NotFound;
}
fn list_entries(dir: &File, shown: Describe<'_>) -> Result<Vec<(OsString, bool)>, String> {
let listing = std::fs::read_dir(handle_path(dir)).map_err(|error: std::io::Error| { return format!("{} cannot be listed ({error})", shown().display()); })?;
let mut entries: Vec<(OsString, bool)> = Vec::new();
for listed in listing {
let entry: std::fs::DirEntry = listed.map_err(|error: std::io::Error| { return format!("{} cannot be listed ({error})", shown().display()); })?;
let is_directory: bool = entry.file_type().map_err(|error: std::io::Error| { return format!("{} cannot be examined ({error})", shown().join(entry.file_name()).display()); })?.is_dir();
entries.push((entry.file_name(), is_directory));
}
return Ok(entries);
}
fn make_writable(dir: &File, shown: Describe<'_>) {
let Ok(metadata) = dir.metadata() else { return };
let mode: u32 = metadata.permissions().mode();
if mode & 0o700 == 0o700 {
return;
}
if let Err(error) = dir.set_permissions(std::fs::Permissions::from_mode(mode | 0o700)) {
super::diagnostic_log::debug(TAG, format!("{} keeps its mode ({error})", shown().display()).as_str());
}
}
fn enter(parent: &File, name: &OsStr, device: u64, shown: Describe<'_>) -> Result<Frame, String> {
let dir: File = open_directory_at(parent, name).map_err(|error: std::io::Error| { return format!("{} is not a plain directory that can be entered, so it is left alone ({error})", shown().display()); })?;
let found: u64 = device_of(&dir).map_err(|error: std::io::Error| { return format!("{} cannot be examined ({error})", shown().display()); })?;
if found != device {
return Err(format!("{} is on another device, so it is left alone", shown().display()));
}
make_writable(&dir, shown);
let entries: Vec<(OsString, bool)> = list_entries(&dir, shown)?;
return Ok(Frame { dir, name: name.to_os_string(), entries });
}
pub fn remove_tree_beneath(label: &Path, parent: &File, name: &OsStr, expected: Option<ObjectId>) -> Result<Removal, String> {
let top: PathBuf = label.join(name);
let device: u64 = device_of(parent).map_err(|error: std::io::Error| { return format!("{} cannot be examined ({error})", label.display()); })?;
match identity_at(parent, name) {
Ok(found) => {
if let Some(wanted) = expected && found != wanted {
return Err(format!("{} is not the object that was set aside ({} instead of {}), so it is left alone", top.display(), found.encode(), wanted.encode()));
}
}
Err(error) if is_gone(&error) => return Ok(Removal::Absent),
Err(error) => return Err(format!("{} cannot be examined ({error})", top.display())),
}
let first: Frame = enter(parent, name, device, &|| { return top.clone(); })?;
if let Some(wanted) = expected {
let opened: ObjectId = identity_of(&first.dir).map_err(|error: std::io::Error| { return format!("{} cannot be examined ({error})", top.display()); })?;
if opened != wanted {
return Err(format!("{} is not the object that was set aside ({} instead of {}), so it is left alone", top.display(), opened.encode(), wanted.encode()));
}
}
let mut stack: Vec<Frame> = vec![first];
let mut shown: Vec<OsString> = Vec::new();
let here = |names: &[OsString]| -> PathBuf { return names.iter().fold(top.clone(), |path: PathBuf, part: &OsString| { return path.join(part); }); };
loop {
let Some(frame) = stack.last_mut() else { break };
match frame.entries.pop() {
Some((entry, true)) => {
let next: Frame = enter(&frame.dir, entry.as_os_str(), device, &|| { return here(shown.as_slice()).join(&entry); })?;
shown.push(entry);
stack.push(next);
}
Some((entry, false)) => match remove_at(&frame.dir, entry.as_os_str(), false) {
Ok(()) => {}
Err(error) if is_gone(&error) => {}
Err(error) => return Err(format!("{} cannot be removed ({error})", here(shown.as_slice()).join(&entry).display())),
},
None => {
let Some(done) = stack.pop() else { break };
let holder: &File = stack.last().map_or(parent, |above: &Frame| { return &above.dir; });
match remove_at(holder, done.name.as_os_str(), true) {
Ok(()) => {}
Err(error) if is_gone(&error) => {}
Err(error) => return Err(format!("{} cannot be removed ({error})", here(shown.as_slice()).display())),
}
shown.pop();
}
}
}
return Ok(Removal::Removed);
}
#[cfg(test)]
#[path = "worktree_trash_remove_tests.rs"]
mod tests;