1use std::{cell::SyncUnsafeCell, sync::Mutex};
2
3use tracing::{debug, error};
4use widestring::U16CStr;
5use windows_sys::{
6 Win32::UI::Shell::{SHELLEXECUTEINFOW, ShellExecuteExW},
7 core::BOOL,
8};
9
10use crate::OpenVerb;
11
12type ShellExecuteExWFn = unsafe extern "system" fn(pexecinfo: *mut SHELLEXECUTEINFOW) -> BOOL;
13
14static TRUE_SHELL_EXECUTE_EX_W: SyncUnsafeCell<ShellExecuteExWFn> =
15 SyncUnsafeCell::new(ShellExecuteExW);
16
17#[derive(Default)]
19pub struct HookConfig {
20 pub verbs: Vec<Box<dyn OpenVerb>>,
21}
22
23static HOOK_CONFIG: Mutex<HookConfig> = Mutex::new(HookConfig { verbs: vec![] });
24
25unsafe extern "system" fn shell_execute_ex_w(pexecinfo: *mut SHELLEXECUTEINFOW) -> BOOL {
26 let real = || unsafe { (*TRUE_SHELL_EXECUTE_EX_W.get())(pexecinfo) };
27 let info = unsafe { &*pexecinfo };
28
29 let Some(path) =
32 ib_shell_item::path::ShellPath::from_path_or_id_list(info.lpFile, info.lpIDList as _)
33 else {
34 return real();
35 };
36
37 let verb = (!info.lpVerb.is_null()).then(|| unsafe { U16CStr::from_ptr_str(info.lpVerb) });
39 #[cfg(test)]
40 eprintln!("verb: {verb:?}");
41
42 debug!(?verb, ?path);
43
44 if verb.is_none_or(|verb| verb == widestring::u16str!("open")) {
45 let config = HOOK_CONFIG.lock().unwrap();
46
47 if let Some(r) = crate::open_verbs(&path, config.verbs.as_slice()) {
48 return r.is_ok() as _;
49 }
50 }
51
52 real()
53}
54
55fn hook(enable: bool) -> windows::core::Result<()> {
56 let res = unsafe {
57 slim_detours_sys::SlimDetoursInlineHook(
58 enable as _,
59 TRUE_SHELL_EXECUTE_EX_W.get().cast(),
60 shell_execute_ex_w as _,
61 )
62 };
63 windows::core::HRESULT(res).ok()
64}
65
66pub fn set_hook(config: Option<HookConfig>) {
69 if let Some(config) = config {
70 let mut hook_config = HOOK_CONFIG.lock().unwrap();
71 *hook_config = config;
72 if let Err(e) = hook(true) {
73 error!(%e, "Failed to hook ShellExecuteExW");
74 }
75 } else {
76 if let Err(e) = hook(false) {
77 error!(%e, "Failed to detach hook");
78 }
79 }
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85 use std::{
86 assert_matches::assert_matches,
87 path::{Path, PathBuf},
88 sync::{Arc, Mutex},
89 };
90
91 struct TestVerb {
92 path: Arc<Mutex<Option<PathBuf>>>,
93 }
94
95 impl OpenVerb for TestVerb {
96 fn handle(&self, path: &Path) -> Option<anyhow::Result<()>> {
97 let mut p = self.path.lock().unwrap();
98 *p = Some(path.to_path_buf());
99 Some(Ok(()))
100 }
101 }
102
103 #[test]
104 fn test_hook_intercepts_open() {
105 let path = Arc::new(Mutex::new(None::<PathBuf>));
107 let test_verb = TestVerb { path: path.clone() };
108
109 set_hook(Some(HookConfig {
111 verbs: vec![Box::new(test_verb)],
112 }));
113
114 assert_matches!(open::that_detached("test a"), Ok(_));
117
118 let captured_path = path.lock().unwrap();
120 assert_eq!(*captured_path, Some(PathBuf::from("test a")));
121 }
122}