1 use std::{ 2 ffi::{c_void, OsStr, OsString}, 3 mem::MaybeUninit, 4 ops::DerefMut, 5 os::{ 6 raw::c_int, 7 windows::{ffi::OsStrExt, prelude::*}, 8 }, 9 path::PathBuf, 10 }; 11 12 use scopeguard::defer; 13 use windows::{self, Guid, Interface, HRESULT}; 14 15 use crate::{into_unknown, Error, TrashContext, TrashItem}; 16 17 mod bindings { 18 ::windows::include_bindings!(); 19 } 20 use bindings::Windows::Win32::{ 21 Automation::*, Com::*, Shell::*, SystemServices::*, WindowsAndMessaging::*, 22 WindowsProgramming::*, WindowsPropertiesSystem::*, 23 }; 24 25 /////////////////////////////////////////////////////////////////////////// 26 // These don't have bindings in windows-rs for some reason 27 /////////////////////////////////////////////////////////////////////////// 28 const PSGUID_DISPLACED: Guid = 29 Guid::from_values(0x9b174b33, 0x40ff, 0x11d2, [0xa2, 0x7e, 0x00, 0xc0, 0x4f, 0xc3, 0x8, 0x71]); 30 const PID_DISPLACED_FROM: u32 = 2; 31 const PID_DISPLACED_DATE: u32 = 3; 32 const SCID_ORIGINAL_LOCATION: PROPERTYKEY = 33 PROPERTYKEY { fmtid: PSGUID_DISPLACED, pid: PID_DISPLACED_FROM }; 34 const SCID_DATE_DELETED: PROPERTYKEY = 35 PROPERTYKEY { fmtid: PSGUID_DISPLACED, pid: PID_DISPLACED_DATE }; 36 37 const FOF_SILENT: u32 = 0x0004; 38 const FOF_NOCONFIRMATION: u32 = 0x0010; 39 const FOF_ALLOWUNDO: u32 = 0x0040; 40 const FOF_NOCONFIRMMKDIR: u32 = 0x0200; 41 const FOF_NOERRORUI: u32 = 0x0400; 42 const FOF_WANTNUKEWARNING: u32 = 0x4000; 43 const FOF_NO_UI: u32 = FOF_SILENT | FOF_NOCONFIRMATION | FOF_NOERRORUI | FOF_NOCONFIRMMKDIR; 44 const FOFX_EARLYFAILURE: u32 = 0x00100000; 45 /////////////////////////////////////////////////////////////////////////// 46 47 macro_rules! check_res_and_get_ok { 48 {$f_name:ident($($args:tt)*)} => ({ 49 let res = $f_name($($args)*); 50 match res { 51 Err(e) => { 52 return Err(Error::Unknown { 53 description: format!("`{}` failed with the result: {:?}", stringify!($f_name), e) 54 }); 55 } 56 Ok(value) => value 57 } 58 }); 59 {$obj:ident.$f_name:ident($($args:tt)*)} => ({ 60 let res = $obj.$f_name($($args)*); 61 match res { 62 Err(e) => { 63 return Err(Error::Unknown { 64 description: format!("`{}` failed with the result: {:?}", stringify!($f_name), e) 65 }); 66 } 67 Ok(value) => value 68 } 69 }); 70 } 71 72 macro_rules! check_hresult { 73 {$f_name:ident($($args:tt)*)} => ({ 74 let hr = $f_name($($args)*); 75 if hr.is_err() { 76 return Err(Error::Unknown { 77 description: format!("`{}` failed with the result: {:?}", stringify!($f_name), hr) 78 }); 79 } 80 }); 81 {$obj:ident.$f_name:ident($($args:tt)*)} => ({ 82 let _ = check_and_get_hresult!{$obj.$f_name($($args)*)}; 83 }); 84 } 85 macro_rules! check_and_get_hresult { 86 {$obj:ident.$f_name:ident($($args:tt)*)} => ({ 87 let hr = ($obj).$f_name($($args)*); 88 if hr.is_err() { 89 return Err(Error::Unknown { 90 description: format!("`{}` failed with the result: {:?}", stringify!($f_name), hr) 91 }); 92 } 93 hr 94 }); 95 } 96 97 #[derive(Clone, Default, Debug)] 98 pub struct PlatformTrashContext; 99 impl PlatformTrashContext { 100 pub const fn new() -> Self { 101 PlatformTrashContext 102 } 103 } 104 impl TrashContext { 105 /// See https://docs.microsoft.com/en-us/windows/win32/api/shellapi/ns-shellapi-_shfileopstructa 106 pub(crate) fn delete_all_canonicalized(&self, full_paths: Vec<PathBuf>) -> Result<(), Error> { 107 ensure_com_initialized(); 108 unsafe { 109 let pfo: IFileOperation = check_res_and_get_ok! { 110 CoCreateInstance( 111 &FileOperation as *const _, 112 None, 113 CLSCTX::CLSCTX_ALL 114 ) 115 }; 116 check_hresult! { pfo.SetOperationFlags(FOF_NO_UI | FOF_ALLOWUNDO | FOF_WANTNUKEWARNING) }; 117 for full_path in full_paths.iter() { 118 let path_prefix = ['\\' as u16, '\\' as u16, '?' as u16, '\\' as u16]; 119 let mut wide_path_container: Vec<_> = 120 full_path.as_os_str().encode_wide().chain(std::iter::once(0)).collect(); 121 let wide_path_slice = if wide_path_container.starts_with(&path_prefix) { 122 &mut wide_path_container[path_prefix.len()..] 123 } else { 124 &mut wide_path_container[0..] 125 }; 126 let shi: IShellItem = check_res_and_get_ok! { 127 SHCreateItemFromParsingName( 128 PWSTR(wide_path_slice.as_mut_ptr()), 129 None, 130 ) 131 }; 132 check_hresult! { pfo.DeleteItem(shi, None) }; 133 } 134 check_hresult! { pfo.PerformOperations() }; 135 Ok(()) 136 } 137 } 138 } 139 140 pub fn list() -> Result<Vec<TrashItem>, Error> { 141 ensure_com_initialized(); 142 unsafe { 143 let recycle_bin: IShellFolder2 = bind_to_csidl(CSIDL_BITBUCKET as c_int)?; 144 let mut peidl = MaybeUninit::<Option<IEnumIDList>>::uninit(); 145 let flags = _SHCONTF::SHCONTF_FOLDERS.0 | _SHCONTF::SHCONTF_NONFOLDERS.0; 146 let hr = check_and_get_hresult! { 147 recycle_bin.EnumObjects( 148 HWND::NULL, 149 flags as u32, 150 peidl.as_mut_ptr(), 151 ) 152 }; 153 // WARNING `hr.is_ok()` is DIFFERENT from `hr == S_OK`, because 154 // `is_ok` returns true if the HRESULT as any of the several success codes 155 // but here we want to be more strict and only accept S_OK. 156 if hr != S_OK { 157 return Err(Error::Unknown { 158 description: format!( 159 "`EnumObjects` returned with HRESULT {:X}, but 0x0 was expected.", 160 hr.0 161 ), 162 }); 163 } 164 let peidl = peidl.assume_init().ok_or_else(|| Error::Unknown { 165 description: "`EnumObjects` set its output to None.".into(), 166 })?; 167 let mut item_vec = Vec::new(); 168 let mut item_uninit = MaybeUninit::<*mut ITEMIDLIST>::uninit(); 169 while peidl.Next(1, item_uninit.as_mut_ptr(), std::ptr::null_mut()) == S_OK { 170 let item = item_uninit.assume_init(); 171 defer! {{ CoTaskMemFree(item as *mut c_void); }} 172 let id = get_display_name((&recycle_bin).into(), item, _SHGDNF::SHGDN_FORPARSING)?; 173 let name = get_display_name((&recycle_bin).into(), item, _SHGDNF::SHGDN_INFOLDER)?; 174 175 let orig_loc = get_detail(&recycle_bin, item, &SCID_ORIGINAL_LOCATION as *const _)?; 176 let date_deleted = get_date_unix(&recycle_bin, item, &SCID_DATE_DELETED as *const _)?; 177 178 item_vec.push(TrashItem { 179 id, 180 name: name.into_string().map_err(|original| Error::ConvertOsString { original })?, 181 original_parent: PathBuf::from(orig_loc), 182 time_deleted: date_deleted, 183 }); 184 } 185 Ok(item_vec) 186 } 187 } 188 189 pub fn purge_all<I>(items: I) -> Result<(), Error> 190 where 191 I: IntoIterator<Item = TrashItem>, 192 { 193 ensure_com_initialized(); 194 unsafe { 195 let recycle_bin: IShellFolder2 = bind_to_csidl(CSIDL_BITBUCKET as i32)?; 196 let pfo: IFileOperation = check_res_and_get_ok! { 197 CoCreateInstance( 198 &FileOperation as *const _, 199 None, 200 CLSCTX::CLSCTX_ALL, 201 ) 202 }; 203 check_hresult! { pfo.SetOperationFlags(FOF_NO_UI) }; 204 let mut at_least_one = false; 205 for item in items { 206 at_least_one = true; 207 let mut id_wstr: Vec<_> = item.id.encode_wide().chain(std::iter::once(0)).collect(); 208 let mut pidl = MaybeUninit::<*mut ITEMIDLIST>::uninit(); 209 check_hresult! { 210 recycle_bin.ParseDisplayName( 211 HWND::NULL, 212 None, 213 PWSTR(id_wstr.as_mut_ptr()), 214 std::ptr::null_mut(), 215 pidl.as_mut_ptr(), 216 std::ptr::null_mut(), 217 ) 218 }; 219 let pidl = pidl.assume_init(); 220 defer! {{ CoTaskMemFree(pidl as *mut c_void); }} 221 let shi: IShellItem = check_res_and_get_ok! { 222 SHCreateItemWithParent( 223 std::ptr::null_mut(), 224 &recycle_bin, 225 pidl, 226 ) 227 }; 228 check_hresult! { pfo.DeleteItem(shi, None) }; 229 } 230 if at_least_one { 231 check_hresult! { pfo.PerformOperations() }; 232 } 233 Ok(()) 234 } 235 } 236 237 pub fn restore_all<I>(items: I) -> Result<(), Error> 238 where 239 I: IntoIterator<Item = TrashItem>, 240 { 241 let items: Vec<_> = items.into_iter().collect(); 242 243 // Do a quick and dirty check if the target items already exist at the location 244 // and if they do, return all of them, if they don't just go ahead with the processing 245 // without giving a damn. 246 // Note that this is not 'thread safe' meaning that if a paralell thread (or process) 247 // does this operation the exact same time or creates files or folders right after this check, 248 // then the files that would collide will not be detected and returned as part of an error. 249 // Instead Windows will display a prompt to the user whether they want to replace or skip. 250 for item in items.iter() { 251 let path = item.original_path(); 252 if path.exists() { 253 return Err(Error::RestoreCollision { path, remaining_items: items }); 254 } 255 } 256 ensure_com_initialized(); 257 unsafe { 258 let recycle_bin: IShellFolder2 = bind_to_csidl(CSIDL_BITBUCKET as i32)?; 259 let pfo: IFileOperation = check_res_and_get_ok! { 260 CoCreateInstance( 261 &FileOperation as *const _, 262 None, 263 CLSCTX::CLSCTX_ALL, 264 ) 265 }; 266 check_hresult! { pfo.SetOperationFlags(FOF_NO_UI | FOFX_EARLYFAILURE) }; 267 for item in items.iter() { 268 let mut id_wstr: Vec<_> = item.id.encode_wide().chain(std::iter::once(0)).collect(); 269 let mut pidl = MaybeUninit::<*mut ITEMIDLIST>::uninit(); 270 check_hresult! { 271 recycle_bin.ParseDisplayName( 272 HWND::NULL, 273 None, 274 PWSTR(id_wstr.as_mut_ptr()), 275 std::ptr::null_mut(), 276 pidl.as_mut_ptr(), 277 std::ptr::null_mut(), 278 ) 279 }; 280 let pidl = pidl.assume_init(); 281 defer! {{ CoTaskMemFree(pidl as *mut c_void); }} 282 let trash_item_shi: IShellItem = check_res_and_get_ok! { 283 SHCreateItemWithParent( 284 std::ptr::null_mut(), 285 &recycle_bin, 286 pidl, 287 ) 288 }; 289 let mut parent_path_wide: Vec<_> = 290 item.original_parent.as_os_str().encode_wide().chain(std::iter::once(0)).collect(); 291 let orig_folder_shi: IShellItem = check_res_and_get_ok! { 292 SHCreateItemFromParsingName( 293 PWSTR(parent_path_wide.as_mut_ptr()), 294 None, 295 ) 296 }; 297 let mut name_wstr: Vec<_> = AsRef::<OsStr>::as_ref(&item.name) 298 .encode_wide() 299 .chain(std::iter::once(0)) 300 .collect(); 301 check_hresult! { pfo.MoveItem(trash_item_shi, orig_folder_shi, PWSTR(name_wstr.as_mut_ptr()), None) }; 302 } 303 if !items.is_empty() { 304 check_hresult! { pfo.PerformOperations() }; 305 } 306 Ok(()) 307 } 308 } 309 310 unsafe fn get_display_name( 311 psf: IShellFolder, 312 pidl: *mut ITEMIDLIST, 313 flags: _SHGDNF, 314 ) -> Result<OsString, Error> { 315 let mut sr = MaybeUninit::<STRRET>::uninit(); 316 check_hresult! { psf.GetDisplayNameOf(pidl, flags.0 as u32, sr.as_mut_ptr()) }; 317 let mut sr = sr.assume_init(); 318 let mut name = MaybeUninit::<PWSTR>::uninit(); 319 check_hresult! { StrRetToStrW(&mut sr as *mut _, pidl, name.as_mut_ptr()) }; 320 let name = name.assume_init(); 321 let result = wstr_to_os_string(name); 322 CoTaskMemFree(name.0 as *mut c_void); 323 Ok(result) 324 } 325 326 unsafe fn wstr_to_os_string(wstr: PWSTR) -> OsString { 327 let mut len = 0; 328 while *(wstr.0.offset(len)) != 0 { 329 len += 1; 330 } 331 let wstr_slice = std::slice::from_raw_parts(wstr.0, len as usize); 332 OsString::from_wide(wstr_slice) 333 } 334 335 unsafe fn get_detail( 336 psf: &IShellFolder2, 337 pidl: *mut ITEMIDLIST, 338 pscid: *const PROPERTYKEY, 339 ) -> Result<OsString, Error> { 340 let mut vt = MaybeUninit::<VARIANT>::uninit(); 341 check_hresult! { psf.GetDetailsEx(pidl, pscid, vt.as_mut_ptr()) }; 342 let vt = vt.assume_init(); 343 let mut vt = scopeguard::guard(vt, |mut vt| { 344 // Ignoring the return value 345 let _ = VariantClear(&mut vt as *mut _); 346 }); 347 check_hresult! { 348 VariantChangeType(vt.deref_mut() as *mut _, vt.deref_mut() as *mut _, 0, VARENUM::VT_BSTR.0 as u16) 349 }; 350 let pstr = vt.Anonymous.Anonymous.Anonymous.bstrVal; 351 Ok(wstr_to_os_string(PWSTR(pstr))) 352 } 353 354 unsafe fn get_date_unix( 355 psf: &IShellFolder2, 356 pidl: *mut ITEMIDLIST, 357 pscid: *const PROPERTYKEY, 358 ) -> Result<i64, Error> { 359 let mut vt = MaybeUninit::<VARIANT>::uninit(); 360 check_hresult! { psf.GetDetailsEx(pidl, pscid, vt.as_mut_ptr()) }; 361 let vt = vt.assume_init(); 362 let mut vt = scopeguard::guard(vt, |mut vt| { 363 // Ignoring the return value 364 let _ = VariantClear(&mut vt as *mut _); 365 }); 366 check_hresult! { 367 VariantChangeType(vt.deref_mut() as *mut _, vt.deref_mut() as *mut _, 0, VARENUM::VT_DATE.0 as u16) 368 }; 369 let date = vt.Anonymous.Anonymous.Anonymous.date; 370 let unix_time = variant_time_to_unix_time(date)?; 371 Ok(unix_time) 372 } 373 374 unsafe fn variant_time_to_unix_time(from: f64) -> Result<i64, Error> { 375 #[repr(C)] 376 #[derive(Clone, Copy)] 377 struct LargeIntegerParts { 378 low_part: u32, 379 high_part: u32, 380 } 381 #[repr(C)] 382 union LargeInteger { 383 parts: LargeIntegerParts, 384 whole: u64, 385 } 386 let mut st = MaybeUninit::<SYSTEMTIME>::uninit(); 387 if 0 == VariantTimeToSystemTime(from, st.as_mut_ptr()) { 388 return Err(Error::Unknown { 389 description: format!( 390 "`VariantTimeToSystemTime` indicated failure for the parameter {:?}", 391 from 392 ), 393 }); 394 } 395 let st = st.assume_init(); 396 let mut ft = MaybeUninit::<FILETIME>::uninit(); 397 if SystemTimeToFileTime(&st, ft.as_mut_ptr()) == false { 398 return Err(Error::Unknown { 399 description: format!( 400 "`SystemTimeToFileTime` failed with: {:?}", 401 HRESULT::from_thread() 402 ), 403 }); 404 } 405 let ft = ft.assume_init(); 406 407 let large_int = LargeInteger { 408 parts: LargeIntegerParts { low_part: ft.dwLowDateTime, high_part: ft.dwHighDateTime }, 409 }; 410 411 // Applying assume init straight away because there's no explicit support to initialize struct 412 // fields one-by-one in an `MaybeUninit` as of Rust 1.39.0 413 // See: https://github.com/rust-lang/rust/blob/1.39.0/src/libcore/mem/maybe_uninit.rs#L170 414 // let mut uli = MaybeUninit::<ULARGE_INTEGER>::zeroed().assume_init(); 415 // { 416 // let u_mut = uli.u_mut(); 417 // u_mut.LowPart = ft.dwLowDateTime; 418 // u_mut.HighPart = std::mem::transmute(ft.dwHighDateTime); 419 // } 420 let windows_ticks: u64 = large_int.whole; 421 Ok(windows_ticks_to_unix_seconds(windows_ticks)) 422 } 423 424 fn windows_ticks_to_unix_seconds(windows_ticks: u64) -> i64 { 425 // Fun fact: if my calculations are correct, then storing sucn ticks in an 426 // i64 can remain valid until about 6000 years from the very first tick 427 const WINDOWS_TICK: u64 = 10000000; 428 const SEC_TO_UNIX_EPOCH: i64 = 11644473600; 429 (windows_ticks / WINDOWS_TICK) as i64 - SEC_TO_UNIX_EPOCH 430 } 431 432 unsafe fn bind_to_csidl<T: Interface>(csidl: c_int) -> Result<T, Error> { 433 let mut pidl = MaybeUninit::<*mut ITEMIDLIST>::uninit(); 434 check_hresult! { 435 SHGetSpecialFolderLocation(HWND::NULL, csidl, pidl.as_mut_ptr()) 436 }; 437 let pidl = pidl.assume_init(); 438 defer! {{ CoTaskMemFree(pidl as _); }}; 439 440 let mut desktop = MaybeUninit::<Option<IShellFolder>>::uninit(); 441 check_hresult! { SHGetDesktopFolder(desktop.as_mut_ptr()) }; 442 let desktop = desktop.assume_init(); 443 let desktop = desktop.ok_or_else(|| Error::Unknown { 444 description: "`SHGetDesktopFolder` set its output to `None`.".into(), 445 })?; 446 if (*pidl).mkid.cb != 0 { 447 let target: T = check_res_and_get_ok! { desktop.BindToObject(pidl, None) }; 448 Ok(target) 449 } else { 450 Ok(desktop.cast().map_err(into_unknown)?) 451 } 452 } 453 454 struct CoInitializer {} 455 impl CoInitializer { 456 fn new() -> CoInitializer { 457 //let first = INITIALIZER_THREAD_COUNT.fetch_add(1, Ordering::SeqCst) == 0; 458 #[cfg(all( 459 not(feature = "coinit_multithreaded"), 460 not(feature = "coinit_apartmentthreaded") 461 ))] 462 { 463 0 = "THIS IS AN ERROR ON PURPOSE. Either the `coinit_multithreaded` or the `coinit_apartmentthreaded` feature must be specified"; 464 } 465 let mut init_mode; 466 #[cfg(feature = "coinit_multithreaded")] 467 { 468 init_mode = COINIT::COINIT_MULTITHREADED; 469 } 470 #[cfg(feature = "coinit_apartmentthreaded")] 471 { 472 init_mode = COINIT::COINIT_APARTMENTTHREADED; 473 } 474 475 // These flags can be combined with either of coinit_multithreaded or coinit_apartmentthreaded. 476 if cfg!(feature = "coinit_disable_ole1dde") { 477 init_mode |= COINIT::COINIT_DISABLE_OLE1DDE; 478 } 479 if cfg!(feature = "coinit_speed_over_memory") { 480 init_mode |= COINIT::COINIT_SPEED_OVER_MEMORY; 481 } 482 let hr = unsafe { CoInitializeEx(std::ptr::null_mut(), init_mode) }; 483 if hr.is_err() { 484 panic!("Call to CoInitializeEx failed. HRESULT: {:?}. Consider using `trash` with the feature `coinit_multithreaded`", hr); 485 } 486 CoInitializer {} 487 } 488 } 489 impl Drop for CoInitializer { 490 fn drop(&mut self) { 491 // TODO: This does not get called because it's a global static. 492 // Is there an atexit in Win32? 493 unsafe { 494 CoUninitialize(); 495 } 496 } 497 } 498 thread_local! { 499 static CO_INITIALIZER: CoInitializer = CoInitializer::new(); 500 } 501 fn ensure_com_initialized() { 502 CO_INITIALIZER.with(|_| {}); 503 } 504