Skip to main content

ib_shell_verb/
hook.rs

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/// Hook configuration containing the verbs to try before falling back to default behavior.
18#[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    // Some programs use PIDL to open normal files
30    // https://github.com/Chaoses-Ib/IbEverythingExt/issues/104
31    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    // Check if verb is "open"
38    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
66/// Set the hook with optional config.
67/// If config is None, the hook is disabled.
68pub 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        // Create a mock verb that tracks the path
106        let path = Arc::new(Mutex::new(None::<PathBuf>));
107        let test_verb = TestVerb { path: path.clone() };
108
109        // Set the hook with config
110        set_hook(Some(HookConfig {
111            verbs: vec![Box::new(test_verb)],
112        }));
113
114        // Use open::that_detached which will call ShellExecuteExW internally
115        // The hook should intercept this and call our mock verb
116        assert_matches!(open::that_detached("test a"), Ok(_));
117
118        // Verify the mock verb was called with the correct path
119        let captured_path = path.lock().unwrap();
120        assert_eq!(*captured_path, Some(PathBuf::from("test a")));
121    }
122}