use anyhow::{anyhow, Context, Result};
use std::ffi::{CStr, CString};
use std::os::unix::ffi::OsStrExt;
use std::path::Path;
use std::process::Command;
pub fn delete_to_trash(path: &Path) -> Result<()> {
let path = CString::new(path.as_os_str().as_bytes())
.map_err(|_| anyhow!("path contains a null byte"))?;
unsafe {
let _pool = AutoreleasePool::new()?;
let path_string = msg_send_id_with_cstr(
class(c"NSString")?,
selector(c"stringWithUTF8String:"),
path.as_ptr(),
);
if path_string.is_null() {
return Err(anyhow!("could not create NSString for path"));
}
let url = msg_send_id_with_id(class(c"NSURL")?, selector(c"fileURLWithPath:"), path_string);
if url.is_null() {
return Err(anyhow!("could not create file URL for path"));
}
let manager = msg_send_id(class(c"NSFileManager")?, selector(c"defaultManager"));
if manager.is_null() {
return Err(anyhow!("could not access NSFileManager"));
}
let mut resulting_url: Id = std::ptr::null_mut();
let mut error: Id = std::ptr::null_mut();
let ok = msg_send_bool_with_trash_args(
manager,
selector(c"trashItemAtURL:resultingItemURL:error:"),
url,
&mut resulting_url,
&mut error,
);
if ok {
Ok(())
} else {
Err(anyhow!("move to Trash failed: {}", error_message(error)))
}
}
}
type Id = *mut libc::c_void;
type Sel = *mut libc::c_void;
#[link(name = "Foundation", kind = "framework")]
extern "C" {}
#[link(name = "objc")]
extern "C" {
fn objc_getClass(name: *const libc::c_char) -> Id;
fn sel_registerName(name: *const libc::c_char) -> Sel;
fn objc_msgSend();
}
struct AutoreleasePool {
id: Id,
}
impl AutoreleasePool {
unsafe fn new() -> Result<Self> {
let allocated = msg_send_id(class(c"NSAutoreleasePool")?, selector(c"alloc"));
let id = msg_send_id(allocated, selector(c"init"));
if id.is_null() {
Err(anyhow!("could not create autorelease pool"))
} else {
Ok(Self { id })
}
}
}
impl Drop for AutoreleasePool {
fn drop(&mut self) {
unsafe {
msg_send_void(self.id, selector(c"drain"));
}
}
}
fn class(name: &CStr) -> Result<Id> {
let class = unsafe { objc_getClass(name.as_ptr()) };
if class.is_null() {
Err(anyhow!(
"Objective-C class not found: {}",
name.to_string_lossy()
))
} else {
Ok(class)
}
}
fn selector(name: &CStr) -> Sel {
unsafe { sel_registerName(name.as_ptr()) }
}
unsafe fn msg_send_id(receiver: Id, sel: Sel) -> Id {
let f: unsafe extern "C" fn(Id, Sel) -> Id = std::mem::transmute(objc_msgSend as *const ());
f(receiver, sel)
}
unsafe fn msg_send_id_with_cstr(receiver: Id, sel: Sel, arg: *const libc::c_char) -> Id {
let f: unsafe extern "C" fn(Id, Sel, *const libc::c_char) -> Id =
std::mem::transmute(objc_msgSend as *const ());
f(receiver, sel, arg)
}
unsafe fn msg_send_id_with_id(receiver: Id, sel: Sel, arg: Id) -> Id {
let f: unsafe extern "C" fn(Id, Sel, Id) -> Id = std::mem::transmute(objc_msgSend as *const ());
f(receiver, sel, arg)
}
unsafe fn msg_send_bool_with_trash_args(
receiver: Id,
sel: Sel,
url: Id,
resulting_url: *mut Id,
error: *mut Id,
) -> bool {
let f: unsafe extern "C" fn(Id, Sel, Id, *mut Id, *mut Id) -> libc::c_schar =
std::mem::transmute(objc_msgSend as *const ());
f(receiver, sel, url, resulting_url, error) != 0
}
unsafe fn msg_send_const_char(receiver: Id, sel: Sel) -> *const libc::c_char {
let f: unsafe extern "C" fn(Id, Sel) -> *const libc::c_char =
std::mem::transmute(objc_msgSend as *const ());
f(receiver, sel)
}
unsafe fn msg_send_void(receiver: Id, sel: Sel) {
let f: unsafe extern "C" fn(Id, Sel) = std::mem::transmute(objc_msgSend as *const ());
f(receiver, sel);
}
unsafe fn error_message(error: Id) -> String {
if error.is_null() {
return String::from("unknown error");
}
let description = msg_send_id(error, selector(c"localizedDescription"));
if description.is_null() {
return String::from("unknown error");
}
let bytes = msg_send_const_char(description, selector(c"UTF8String"));
if bytes.is_null() {
String::from("unknown error")
} else {
CStr::from_ptr(bytes).to_string_lossy().into_owned()
}
}
pub fn empty_trash() -> Result<()> {
empty_trash_with(run_osascript)
}
pub fn empty_trash_with(runner: fn(&str, &[&str]) -> Result<()>) -> Result<()> {
runner(
"osascript",
&["-e", "tell application \"Finder\" to empty trash"],
)
}
pub fn run_osascript(program: &str, args: &[&str]) -> Result<()> {
let output = Command::new(program)
.args(args)
.output()
.with_context(|| format!("failed to execute {program}"))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(anyhow!("{program} failed: {}", stderr.trim()));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::ffi::OsString;
use std::os::unix::ffi::OsStringExt;
#[test]
fn rejects_paths_with_null_bytes() {
let os_string = OsString::from_vec(b"/tmp/diskr\0bad".to_vec());
let path = Path::new(&os_string);
assert!(delete_to_trash(path).is_err());
}
#[test]
fn empty_trash_invokes_runner_with_osascript_args() {
use std::cell::RefCell;
thread_local! {
static CAPTURED: RefCell<Vec<(String, Vec<String>)>> = const { RefCell::new(Vec::new()) };
}
fn fake_runner(program: &str, args: &[&str]) -> Result<()> {
CAPTURED.with(|c| {
c.borrow_mut().push((
program.to_string(),
args.iter().map(|s| s.to_string()).collect(),
));
});
Ok(())
}
let result = empty_trash_with(fake_runner);
assert!(result.is_ok());
CAPTURED.with(|c| {
let captured = c.borrow();
assert_eq!(captured.len(), 1);
let (prog, args) = &captured[0];
assert_eq!(prog, "osascript");
assert_eq!(args, &["-e", "tell application \"Finder\" to empty trash"]);
});
}
#[test]
fn empty_trash_propagates_runner_error() {
fn failing_runner(_program: &str, _args: &[&str]) -> Result<()> {
Err(anyhow!("simulated failure"))
}
let result = empty_trash_with(failing_runner);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("simulated failure"));
}
}