win_fast_rm/
lib.rs

1use std::ffi::OsStr;
2use std::path::PathBuf;
3use windows::core::HSTRING;
4use windows::Win32::{
5    Foundation::ERROR_FILE_NOT_FOUND,
6    Storage::FileSystem::GetFullPathNameW,
7    System::Com::{CoCreateInstance, CoInitializeEx, CLSCTX_ALL, COINIT_APARTMENTTHREADED},
8    UI::Shell::{
9        FileOperation, IFileOperation, IShellItem, SHCreateItemFromParsingName, FOF_NO_UI,
10    },
11};
12
13pub fn resolve_path(path: &OsStr) -> PathBuf {
14    // Canonicalize is fine on Linux and MacOS. Windows uses UNC and that messes up IFileOperation.
15    // TODO: Check if canonicalize is actually fine, it might error on non-existent paths.
16    #[cfg(not(windows))]
17    {
18        return PathBuf::from(path).canonicalize().unwrap();
19    }
20
21    #[cfg(windows)]
22    unsafe {
23        let size = GetFullPathNameW::<&HSTRING>(&HSTRING::from(path.to_str().unwrap()), None, None);
24        let mut buffer = vec![0u16; size as usize];
25        GetFullPathNameW::<&HSTRING>(
26            &HSTRING::from(path.to_str().unwrap()),
27            Some(buffer.as_mut_slice()),
28            None,
29        );
30
31        let expanded_path = String::from_utf16_lossy(&buffer);
32        // Strip nulls from utf16 string because PathBuf hates them apparently
33        PathBuf::from(expanded_path.trim_end_matches('\u{0}'))
34    }
35}
36
37pub fn delete_path(path: &PathBuf) -> windows::core::Result<()> {
38    // No-op on non-Windows platforms. Might replace with normal file delete later on as a fallback.
39    #[cfg(not(windows))]
40    {
41        eprintln!("This program is only intended for Windows.");
42        std::process::exit(1);
43    }
44    /*
45    Initialize COM using multi-threaded apartment model.
46    It's *technically* meant for GUI threads, according to https://learn.microsoft.com/en-us/windows/win32/learnwin32/initializing-the-com-library,
47    however it seems to just shave off ~2-3ms compared to normal multi-threaded, at least on Windows 11.
48    */
49    unsafe {
50        CoInitializeEx(None, COINIT_APARTMENTTHREADED)?;
51    }
52
53    if !path.try_exists().unwrap_or(false) {
54        return Err(ERROR_FILE_NOT_FOUND.into());
55    }
56
57    // Silly windows conversions...
58    let string_path = HSTRING::from(path.to_str().unwrap());
59
60    // Create ShellItem from path, IFileOperation operates on ShellItems
61    unsafe {
62        let shell_item =
63            SHCreateItemFromParsingName::<&HSTRING, Option<_>, IShellItem>(&string_path, None)?;
64
65        // File operation instance, with ugly CoCreateInstance generics because Windows API be like that
66        let file_operation =
67            CoCreateInstance::<Option<_>, IFileOperation>(&FileOperation, None, CLSCTX_ALL)?;
68
69        // Recursive by default! Can be disabled with https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-ifileoperation-setoperationflags#fof_norecursion-0x1000
70        // We just need to prevent the confirmation dialog from showing. FOF_NO_UI is actually from an older Win32 API and not documented by the IFileOperation interface, but still works as expected.
71        file_operation.SetOperationFlags(FOF_NO_UI)?;
72        file_operation.DeleteItem(&shell_item, None)?;
73        file_operation.PerformOperations()?;
74    }
75
76    Ok(())
77}