use std::{ffi::OsString, path::PathBuf, process::Command};
use log::{trace, warn};
use objc::{
class, msg_send,
runtime::{Object, BOOL, NO},
sel, sel_impl,
};
use crate::{into_unknown, Error, TrashContext};
#[link(name = "Foundation", kind = "framework")]
extern "C" {
}
#[allow(non_camel_case_types)]
type id = *mut Object;
#[allow(non_upper_case_globals)]
const nil: id = std::ptr::null_mut();
#[allow(non_upper_case_globals)]
const NSUTF8StringEncoding: usize = 4;
#[derive(Copy, Clone, Debug)]
pub enum DeleteMethod {
Finder,
NsFileManager,
}
impl DeleteMethod {
pub const fn new() -> Self {
DeleteMethod::Finder
}
}
impl Default for DeleteMethod {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone, Default, Debug)]
pub struct PlatformTrashContext {
delete_method: DeleteMethod,
}
impl PlatformTrashContext {
pub const fn new() -> Self {
Self { delete_method: DeleteMethod::new() }
}
}
pub trait TrashContextExtMacos {
fn set_delete_method(&mut self, method: DeleteMethod);
fn delete_method(&self) -> DeleteMethod;
}
impl TrashContextExtMacos for TrashContext {
fn set_delete_method(&mut self, method: DeleteMethod) {
self.platform_specific.delete_method = method;
}
fn delete_method(&self) -> DeleteMethod {
self.platform_specific.delete_method
}
}
impl TrashContext {
pub(crate) fn delete_all_canonicalized(&self, full_paths: Vec<PathBuf>) -> Result<(), Error> {
let full_paths = full_paths.into_iter().map(to_string).collect::<Result<Vec<_>, _>>()?;
match self.platform_specific.delete_method {
DeleteMethod::Finder => delete_using_finder(full_paths),
DeleteMethod::NsFileManager => delete_using_file_mgr(full_paths),
}
}
}
fn delete_using_file_mgr(full_paths: Vec<String>) -> Result<(), Error> {
trace!("Starting delete_using_file_mgr");
let url_cls = class!(NSURL);
let file_mgr_cls = class!(NSFileManager);
let file_mgr: id = unsafe { msg_send![file_mgr_cls, defaultManager] };
for path in full_paths {
let string = to_ns_string(&path);
trace!("Starting fileURLWithPath");
let url: id = unsafe { msg_send![url_cls, fileURLWithPath:string.ptr] };
if url == nil {
return Err(Error::Unknown {
description: format!("Failed to convert a path to an NSURL. Path: '{path}'"),
});
}
trace!("Finished fileURLWithPath");
let mut error: id = nil;
trace!("Calling trashItemAtURL");
let success: BOOL = unsafe {
msg_send![
file_mgr,
trashItemAtURL:url
resultingItemURL:nil
error:(&mut error as *mut id)
]
};
trace!("Finished trashItemAtURL");
if success == NO {
trace!("success was NO");
if error == nil {
return Err(Error::Unknown {
description: format!(
"While deleting '{path}', `trashItemAtURL` returned with failure but no error was specified.",
),
});
}
let code: isize = unsafe { msg_send![error, code] };
let domain: id = unsafe { msg_send![error, domain] };
let domain = unsafe { ns_string_to_rust(domain)? };
return Err(Error::Unknown {
description: format!(
"While deleting '{path}', `trashItemAtURL` failed, code: {code}, domain: {domain}",
),
});
}
}
Ok(())
}
fn delete_using_finder(full_paths: Vec<String>) -> Result<(), Error> {
let mut command = Command::new("osascript");
let posix_files = full_paths.into_iter().map(|p| format!("POSIX file \"{p}\"")).collect::<Vec<String>>().join(", ");
let script = format!("tell application \"Finder\" to delete {{ {posix_files} }}");
let argv: Vec<OsString> = vec!["-e".into(), script.into()];
command.args(argv);
let result = command.output().map_err(into_unknown)?;
if !result.status.success() {
let stderr = String::from_utf8_lossy(&result.stderr);
return Err(Error::Unknown {
description: format!(
"The AppleScript exited with error. Error code: {:?}, stderr: {}",
result.status.code(),
stderr
),
});
}
Ok(())
}
fn to_string<T: Into<OsString>>(str_in: T) -> Result<String, Error> {
let os_string = str_in.into();
let s = os_string.to_str();
match s {
Some(s) => Ok(s.to_owned()),
None => Err(Error::ConvertOsString { original: os_string }),
}
}
#[repr(transparent)]
struct OwnedObject {
pub ptr: id,
}
impl Drop for OwnedObject {
fn drop(&mut self) {
#[allow(clippy::let_unit_value)]
{
let () = unsafe { msg_send![self.ptr, release] };
}
}
}
fn to_ns_string(s: &str) -> OwnedObject {
trace!("Called to_ns_string on '{}'", s);
let utf8 = s.as_bytes();
let string_cls = class!(NSString);
let alloced_string: id = unsafe { msg_send![string_cls, alloc] };
let mut string: id = unsafe {
msg_send![
alloced_string,
initWithBytes:utf8.as_ptr()
length:utf8.len()
encoding:NSUTF8StringEncoding
]
};
if string == nil {
warn!("initWithBytes returned nil when trying to convert a rust string to an NSString");
string = unsafe { msg_send![alloced_string, init] };
}
OwnedObject { ptr: string }
}
unsafe fn ns_string_to_rust(string: id) -> Result<String, Error> {
if string == nil {
return Ok(String::new());
}
let utf8_bytes: *const u8 = msg_send![string, UTF8String];
let utf8_len: usize = msg_send![string, lengthOfBytesUsingEncoding: NSUTF8StringEncoding];
let str_slice = std::slice::from_raw_parts(utf8_bytes, utf8_len);
let rust_str = std::str::from_utf8(str_slice).map_err(into_unknown)?;
Ok(rust_str.to_owned())
}
#[cfg(test)]
mod tests {
use crate::{
macos::{DeleteMethod, TrashContextExtMacos},
tests::{get_unique_name, init_logging},
TrashContext,
};
use serial_test::serial;
use std::fs::File;
#[test]
#[serial]
fn test_delete_with_ns_file_manager() {
init_logging();
let mut trash_ctx = TrashContext::default();
trash_ctx.set_delete_method(DeleteMethod::NsFileManager);
let path = get_unique_name();
File::create(&path).unwrap();
trash_ctx.delete(&path).unwrap();
assert!(File::open(&path).is_err());
}
}