use crate::SPath;
use crate::error::{Cause, PathAndCause};
use crate::{Error, Result};
#[cfg(target_os = "macos")]
pub(crate) fn try_silent_trash_move(path: &SPath) -> Result<bool> {
use std::{
fs,
time::{SystemTime, UNIX_EPOCH},
};
let home = std::env::var("HOME").ok();
let trash_dir = match home {
Some(home) => {
let tp = SPath::from(format!("{home}/.Trash"));
if tp.is_dir() {
tp
} else {
return Ok(false); }
}
None => return Ok(false),
};
let stem = path.file_stem().unwrap_or("unnamed");
let ext = path.extension().unwrap_or("");
let now_ms = SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_millis() as u64;
let ts = format_trash_timestamp(now_ms);
let dest_name = if ext.is_empty() {
format!("{stem}--{ts}")
} else {
format!("{stem}--{ts}.{ext}")
};
let dest = trash_dir.join(dest_name);
match fs::rename(path.as_std_path(), dest.as_std_path()) {
Ok(()) => {
let _ = set_putback_metadata(path, &dest);
Ok(true)
}
Err(e) if e.kind() == std::io::ErrorKind::CrossesDevices => {
Ok(false) }
Err(e) => Err(Error::CantTrash(PathAndCause {
path: path.to_string(),
cause: Cause::Custom(format!("Silent trash move failed: {e}")),
})),
}
}
#[cfg(target_os = "macos")]
fn set_putback_metadata(original_path: &SPath, dest: &SPath) -> Result<()> {
let original_abs = resolve_original_abs_path(original_path);
let plist = encode_binary_plist_string(&original_abs);
let name = std::ffi::CString::new("com.apple.metadata:com.apple.trash.putback").map_err(|e| {
Error::CantTrash(PathAndCause {
path: dest.to_string(),
cause: Cause::Custom(format!("Cannot build xattr name: {e}")),
})
})?;
let dest_c = path_to_cstring(dest.as_std_path()).map_err(|e| {
Error::CantTrash(PathAndCause {
path: dest.to_string(),
cause: Cause::Custom(format!("Cannot build dest path for xattr: {e}")),
})
})?;
#[allow(unsafe_code)]
let ret = unsafe {
setxattr(
dest_c.as_ptr(),
name.as_ptr(),
plist.as_ptr() as *const std::ffi::c_void,
plist.len(),
0,
0,
)
};
if ret != 0 {
let err = std::io::Error::last_os_error();
return Err(Error::CantTrash(PathAndCause {
path: dest.to_string(),
cause: Cause::Custom(format!("Cannot set put-back xattr: {err}")),
}));
}
Ok(())
}
#[cfg(target_os = "macos")]
fn resolve_original_abs_path(original_path: &SPath) -> String {
if let (Some(parent), Some(file_name)) = (original_path.parent(), original_path.file_name())
&& let Ok(parent_canon) = parent.canonicalize()
{
return parent_canon.join(file_name).to_string();
}
if original_path.as_str().starts_with('/') {
original_path.to_string()
} else if let Ok(cwd) = std::env::current_dir()
&& let Ok(cwd_spath) = SPath::from_std_path_buf(cwd)
{
cwd_spath.join(original_path.as_str()).to_string()
} else {
original_path.to_string()
}
}
#[cfg(target_os = "macos")]
fn encode_binary_plist_string(value: &str) -> Vec<u8> {
let mut buf: Vec<u8> = Vec::new();
buf.extend_from_slice(b"bplist00");
let object_offset = buf.len() as u64;
if value.is_ascii() {
write_plist_length_header(&mut buf, 0x50, value.len());
buf.extend_from_slice(value.as_bytes());
} else {
let utf16: Vec<u16> = value.encode_utf16().collect();
write_plist_length_header(&mut buf, 0x60, utf16.len());
for unit in utf16 {
buf.extend_from_slice(&unit.to_be_bytes());
}
}
let offset_table_start = buf.len() as u64;
let offset_size: u8 = 1;
buf.push(object_offset as u8);
let num_objects: u64 = 1;
let top_object: u64 = 0;
buf.extend_from_slice(&[0u8; 5]); buf.push(0); buf.push(offset_size); buf.push(8); buf.extend_from_slice(&num_objects.to_be_bytes());
buf.extend_from_slice(&top_object.to_be_bytes());
buf.extend_from_slice(&offset_table_start.to_be_bytes());
buf
}
#[cfg(target_os = "macos")]
fn write_plist_length_header(buf: &mut Vec<u8>, marker_base: u8, len: usize) {
if len < 15 {
buf.push(marker_base | (len as u8));
} else {
buf.push(marker_base | 0x0F);
if len <= u8::MAX as usize {
buf.push(0x10);
buf.push(len as u8);
} else if len <= u16::MAX as usize {
buf.push(0x11);
buf.extend_from_slice(&(len as u16).to_be_bytes());
} else {
buf.push(0x12);
buf.extend_from_slice(&(len as u32).to_be_bytes());
}
}
}
#[cfg(target_os = "macos")]
fn path_to_cstring(path: &std::path::Path) -> std::io::Result<std::ffi::CString> {
use std::os::unix::ffi::OsStrExt;
std::ffi::CString::new(path.as_os_str().as_bytes())
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))
}
#[cfg(target_os = "macos")]
#[allow(unsafe_code)]
unsafe extern "C" {
fn setxattr(
path: *const std::ffi::c_char,
name: *const std::ffi::c_char,
value: *const std::ffi::c_void,
size: usize,
position: u32,
options: std::ffi::c_int,
) -> std::ffi::c_int;
}
#[cfg(target_os = "macos")]
fn format_trash_timestamp(epoch_ms: u64) -> String {
let total_secs = epoch_ms / 1_000;
let millis = epoch_ms % 1_000;
let secs_of_day = total_secs % 86_400;
let days = (total_secs / 86_400) as i64;
let hour = secs_of_day / 3_600;
let minute = (secs_of_day % 3_600) / 60;
let second = secs_of_day % 60;
let z = days + 719_468;
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
let doe = z - era * 146_097; let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365; let year_base = yoe + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); let mp = (5 * doy + 2) / 153; let day = (doy - (153 * mp + 2) / 5 + 1) as u64; let month = if mp < 10 { mp + 3 } else { mp - 9 } as u64; let year = (year_base + if month <= 2 { 1 } else { 0 }) as u64;
format!("{year:04}-{month:02}-{day:02}-{hour:02}-{minute:02}-{second:02}-{millis:03}")
}