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