ib_shell_item/hook/
mod.rs1use std::{cell::SyncUnsafeCell, path::PathBuf, sync::RwLock};
22
23use bon::Builder;
24use serde::{Deserialize, Serialize};
25use tracing::{debug, error, info, trace, warn};
26use windows::{
27 Win32::UI::Shell::{IShellItem, IShellItem2},
28 core::{GUID, Interface},
29};
30use windows_sys::{Win32::UI::Shell::Common::ITEMIDLIST, core::HRESULT};
31
32use crate::{ShellItem, ShellItemDisplayName};
33
34pub mod display_name;
35#[cfg(feature = "hook-dll")]
36pub mod dll;
37pub mod folder;
38#[cfg(feature = "hook-dll")]
39pub mod inject;
40#[cfg(feature = "prop")]
41pub mod prop;
42
43type SHCreateItemFromIDListFn = unsafe extern "system" fn(
44 pidl: *const ITEMIDLIST,
45 riid: *const GUID,
46 ppv: *mut *mut core::ffi::c_void,
47) -> HRESULT;
48
49windows_link::link!("windows.storage.dll" "system" "SHCreateItemFromIDList" fn SHCreateItemFromIDList_windows_storage(pidl : *const ITEMIDLIST, riid : *const GUID, ppv : *mut *mut core::ffi::c_void) -> HRESULT);
51
52static TRUE_SH_CREATE_ITEM_FROM_ID_LIST: SyncUnsafeCell<SHCreateItemFromIDListFn> =
53 SyncUnsafeCell::new(SHCreateItemFromIDList_windows_storage);
54
55#[derive(Default, Serialize, Deserialize, Clone, Builder, Debug)]
58pub struct HookConfig {
59 pub enabled: bool,
61
62 pub display_name: Option<display_name::DisplayNameHookConfig>,
64
65 pub folder: Option<folder::FolderHookConfig>,
66
67 #[cfg(feature = "prop")]
68 pub property: Option<prop::PropertyHookConfig>,
69
70 pub log: Option<PathBuf>,
76}
77
78static HOOK_CONFIG: RwLock<HookConfig> = RwLock::new(HookConfig {
81 enabled: false,
82 display_name: None,
83 folder: None,
84 #[cfg(feature = "prop")]
85 property: None,
86 log: None,
87});
88
89unsafe extern "system" fn sh_create_item_from_id_list(
91 pidl: *const ITEMIDLIST,
92 riid: *const GUID,
93 ppv: *mut *mut core::ffi::c_void,
94) -> HRESULT {
95 let real = || unsafe { (*TRUE_SH_CREATE_ITEM_FROM_ID_LIST.get())(pidl, riid, ppv) };
96
97 let result = real();
98
99 let config = HOOK_CONFIG.read().unwrap();
100 if !config.enabled {
101 return result;
102 }
103
104 trace!(?pidl, ?riid, ?ppv, ?result, "SHCreateItemFromIDList called");
106 if result >= 0 {
107 let iid = unsafe { *riid };
108 let item = unsafe {
109 match iid {
110 IShellItem::IID => {
111 IShellItem::from_raw_borrowed(&*ppv).unwrap()
114 }
115 IShellItem2::IID => {
116 return result;
125 }
126 _ => {
127 warn!(?iid, "unknown");
128 return result;
129 }
130 }
131 };
132 let name = item.get_display_name(ShellItemDisplayName::FileSystemPath);
133 debug!(?name, "SHCreateItemFromIDList called");
134
135 if config.display_name.is_some() {
137 let get_display_name = item.vtable().GetDisplayName;
138 if let Err(e) = display_name::enable_hook(get_display_name) {
139 error!(%e, "Failed to hook GetDisplayName");
140 }
141 }
142
143 #[cfg(feature = "prop")]
144 if config.property.is_some() {
145 if let Ok(item2) = item.cast::<IShellItem2>() {
146 if let Err(e) = prop::enable_hook(&item2) {
147 error!(%e, "Failed to hook prop");
148 }
149 }
150 }
151 } else {
152 debug!(?result, "SHCreateItemFromIDList called");
153 }
154
155 result
156}
157
158fn hook(enable: bool) -> windows::core::Result<()> {
159 let res = unsafe {
160 slim_detours_sys::SlimDetoursInlineHook(
161 enable as _,
162 TRUE_SH_CREATE_ITEM_FROM_ID_LIST.get().cast(),
163 sh_create_item_from_id_list as _,
164 )
165 };
166 windows::core::HRESULT(res).ok()
167}
168
169#[cfg(feature = "hook-log")]
171fn log_init(log_path: &PathBuf) {
172 #[cfg(debug_assertions)]
174 let writer = {
175 let log_dir = log_path.parent().unwrap();
176 let log_filename = log_path.file_name().unwrap();
177 tracing_appender::rolling::never(log_dir, log_filename)
178 };
179 #[cfg(not(debug_assertions))]
180 let (writer, _guard) = tracing_appender::non_blocking(
181 std::fs::OpenOptions::new()
182 .append(true)
183 .create(true)
184 .open(log_path)
185 .ok()
186 .unwrap(),
187 );
188
189 let _ = tracing_subscriber::fmt()
190 .with_writer(writer)
191 .with_max_level(tracing::Level::DEBUG)
192 .with_ansi(false)
193 .try_init();
194 info!("log_init");
195}
196
197pub fn set_hook(config: Option<HookConfig>) {
200 if let Some(config) = config {
201 let mut hook_config = HOOK_CONFIG.write().unwrap();
202 *hook_config = config;
203 if hook_config.enabled {
204 #[cfg(feature = "hook-log")]
205 if let Some(ref log_path) = hook_config.log {
206 log_init(log_path);
207 }
208 info!("attach");
209 if let Err(e) = hook(true) {
210 error!(%e, "Failed to hook SHCreateItemFromIDList");
211 }
212
213 if let Err(e) = folder::apply(hook_config.folder.clone()) {
214 error!(?e, "folder");
215 }
216 }
217 } else {
218 info!("detach");
219 if let Err(e) = hook(false) {
220 error!(%e, "Failed to detach hook");
221 }
222 if let Err(e) = display_name::disable_hook() {
224 error!(%e, "Failed to detach GetDisplayName");
225 }
226 #[cfg(feature = "prop")]
227 if let Err(e) = prop::disable_hook() {
228 error!(%e, "Failed to detach prop");
229 }
230
231 if let Err(e) = folder::apply(None) {
232 error!(?e, "folder");
233 }
234
235 #[cfg(feature = "everything")]
236 unsafe {
237 everything_ipc::wm::EverythingClient::shared_quit_join_thread()
238 };
239 }
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245
246 #[test]
247 fn hook_config_default() {
248 let config = HookConfig::default();
249 assert!(!config.enabled);
250 }
251
252 #[test]
253 fn set_hook_none() {
254 set_hook(None);
256 }
257
258 #[test]
259 fn set_hook_disabled() {
260 set_hook(Some(HookConfig {
261 enabled: false,
262 ..Default::default()
263 }));
264 }
265}