ib_shell_item/folder/mod.rs
1/*!
2## `CompareIDs()`
3[IShellFolder::CompareIDs (shobjidl_core.h)](https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-ishellfolder-compareids)
4
5In Explorer of Windows 11 24H2, `CompareIDs()` is called through:
6```cpp
7windows.storage.dll!DVCompareColumns+0x1d7
8windows.storage.dll!_DSA_MergeSort2+0x15e
9windows.storage.dll!_DSA_MergeSort2+0x54
10windows.storage.dll!_DSA_MergeSort2+0x54
11windows.storage.dll!_DSA_MergeSort2+0x66
12windows.storage.dll!_DSA_MergeSort+0x69
13windows.storage.dll!DSA_Sort+0x4f
14windows.storage.dll!CSortTask::InternalResumeRT+0xc0
15windows.storage.dll!CRunnableTask::Run+0xb6
16windows.storage.dll!CShellTaskThread::ThreadProc+0x2ca
17windows.storage.dll!CShellTaskThread::s_ThreadProc+0x15e
18SHCore.dll!ExecuteWorkItemThreadProc+0x15
19```
20On a different thread than `IPropertyStore::GetValue()` is called,
21and may be different for each sort for the same Explorer window.
22
23Different PIDLs of the same file system path may be passed in rare cases.
24
25If `CompareIDs()` returns error, Explorer will fall back to compare `ItemNameDisplay`
26instead when comparing other properties, including folder size.
27
28Inconsistent ordering may cause buggy results, like the column order is sometimes correct but sometimes wrong.
29
30## Implementations
31### `CFSFolder`
32Interfaces (on Windows 11 24H2):
33- `000214e6_0000_0000_c000_000000000046` `ShellFolder`
34- `b3a4b685_b685_4805_99d9_5dead2873236` `ParentAndItem`
35- `93f2f68c_1d1b_11d3_a30e_00c04f79abd1` `ShellFolder2`
36- `cef04fdf_fe72_11d2_87a5_00c04f6837cf` `PersistFolder3`
37- `0000010c_0000_0000_c000_000000000046` `Persist`
38- `000214ea_0000_0000_c000_000000000046` `PersistFolder`
39- `1ac3d9f0_175c_11d1_95be_00609797ea4f` `PersistFolder2`
40- `000214e5_0000_0000_c000_000000000046` `ShellIcon`
41- `add8ba80_002b_11d0_8f0f_00c04fd7d062` `DelegateFolder`
42- `321a6a6a_d61f_4bf3_97ae_14be2986bb36` `ObjectWithBackReferences`
43- `7d688a70_c613_11d0_999b_00c04fd655e1` `ShellIconOverlay`
44- `37d84f60_42cb_11ce_8135_00aa004bb851` `PersistPropertyBag`
45- `0000000b_0000_0000_c000_000000000046` `Storage`
46- `1df0d7f1_b267_4d28_8b10_12e23202a5c4` `ItemNameLimits`
47- `3409e930_5a39_11d1_83fa_00a0c90dc849` `ContextMenuCB`
48- `b722bccb_4e68_101b_a2bc_00aa00404770` `OleCommandTarget`
49- `a6087428_3be3_4d73_b308_7c04a540bf1a` `ObjectProvider`
50- `fc4801a3_2ba9_11cf_a229_00aa003d7352` `ObjectWithSite`
51- `000214fe_0000_0000_c000_000000000046` `RemoteComputer`
52- `e35b4b2e_00da_4bc1_9f13_38bc11f5d417` `ThumbnailHandlerFactory`
53- `e07010ec_bc17_44c0_97b0_46c7c95b9edc` `ExplorerPaneVisibility`
54- `e9701183_e6b3_4ff2_8568_813615fec7be` `NameSpaceTreeControlFolderCapabilities`
55- `c938b119_d3ad_4d02_b5ee_164c2ec8160e`
56- `fdbee76e_f12b_408e_93ab_9be8521000d9`
57- `2536f9ac_2876_408a_9adf_1fe1c14c0e7f`
58- `089f3011_bb5c_4f9c_9b8f_9a67ed446e91`
59- `08727c66_4a04_456d_8c9a_cc1f65490753`
60- `76347b91_9846_4ce7_9a57_69b910d16123`
61- `0681c275_472b_4097_97b3_f19e4875fdc9`
62- `124bae2c_cb94_42cd_b5b8_4358789684ef`
63- `ff314a1e_06fa_4f3a_84be_7aa1c6be2470`
64- `47d9e2b2_cbb3_4fe3_a925_f49978685982`
65- `053b4a86_0dc9_40a3_b7ed_bc6a2e951f48`
66- `3f943012_447b_4109_8b74_720106853c96`
67- `c51e78b5_566b_4cb0_b6ed_784e18797e23`
68- `dc0ac42a_141e_4876_9c43_824829440de0`
69- `be9da82b_cc54_4b19_8c22_ad7762ff29eb`
70- `013c437f_d523_41fa_8beb_f5100e1ca41c`
71- `127f6acb_7e78_4368_83a4_ed1de72baca6`
72- `d960050c_f4e1_4294_ac4b_598913605923`
73*/
74use std::{cmp, mem};
75
76use bon::Builder;
77use windows::{
78 Win32::{
79 Foundation::{HWND, LPARAM},
80 UI::Shell::{
81 Common::{ITEMIDLIST, STRRET},
82 IShellFolder, SHCIDS_ALLFIELDS, SHCIDS_CANONICALONLY, SHGDN_FORPARSING, SHGDNF,
83 SHGetDesktopFolder, StrRetToBSTR,
84 },
85 },
86 core::{BSTR, PCWSTR, Result, w},
87};
88
89use crate::{
90 id_list::{ChildIDRef, RelativeIDList},
91 prop::attribute::ItemAttributes,
92};
93
94mod compare;
95
96/// [IShellFolder::CompareIDs (shobjidl_core.h)](https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-ishellfolder-compareids#parameters)
97#[derive(Debug, Clone, Copy, Default, Builder)]
98pub struct CompareIDs {
99 /// - [`prop::column::FSColumn`](crate::prop::column::FSColumn)
100 #[builder(default, into)]
101 pub column: u16,
102 #[builder(default)]
103 pub flags: u16,
104}
105
106impl CompareIDs {
107 /// Version 5.0.
108 /// Compare all the information contained in the [`ITEMIDLIST`] structure, not just the display names.
109 /// This flag is valid only for folder objects that support the [`IShellFolder2`] interface.
110 /// For instance, if the two items are files, the folder should compare their names, sizes, file times, attributes, and any other information in the structures.
111 /// If this flag is set, `column` must be zero.
112 pub const ALL_FIELDS: CompareIDs = CompareIDs {
113 column: 0,
114 flags: (SHCIDS_ALLFIELDS >> 16) as u16,
115 };
116
117 /// Version 5.0.
118 /// When comparing by name, compare the system names but not the display names.
119 /// When this flag is passed, the two items are compared by whatever criteria the Shell folder determines are most efficient, as long as it implements a consistent sort function.
120 /// This flag is useful when comparing for equality or when the results of the sort are not displayed to the user.
121 /// This flag cannot be combined with other flags.
122 pub const CANONICAL_ONLY: CompareIDs = CompareIDs {
123 column: 0,
124 flags: (SHCIDS_CANONICALONLY >> 16) as u16,
125 };
126}
127
128impl Into<LPARAM> for CompareIDs {
129 fn into(self) -> LPARAM {
130 LPARAM((self.column as u32 | (self.flags as u32) << 16) as isize)
131 }
132}
133
134/// [IShellFolder (shobjidl_core.h)](https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/nn-shobjidl_core-ishellfolder)
135pub trait ShellFolder {
136 /// Retrieves the [`IShellFolder`] interface for the desktop folder, which is the root of the Shell's namespace.
137 ///
138 /// [SHGetDesktopFolder (shlobj_core.h)](hhttps://learn.microsoft.com/en-us/windows/win32/api/shlobj_core/nf-shlobj_core-shgetdesktopfolder)
139 fn from_desktop() -> Result<IShellFolder> {
140 unsafe { SHGetDesktopFolder() }
141 }
142
143 /// Creates a child folder that represents the folder containing the given item.
144 ///
145 /// [IShellFolder::BindToObject (shobjidl_core.h)](https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-ishellfolder-bindtoobject)
146 fn from_id_list(pidl: &RelativeIDList) -> Result<IShellFolder> {
147 let desktop = Self::from_desktop()?;
148 unsafe { desktop.BindToObject(pidl.0, None) }
149 }
150
151 /// Creates a shell folder from a display name path.
152 ///
153 /// This combines [`ShellFolder::parse_display_name()`] and [`ShellFolder::from_id_list`] to parse a path and return its folder.
154 ///
155 /// Ref:
156 /// - [IShellFolder from Path String](https://forums.codeguru.com/showthread.php?105564-IShellFolder-from-Path-String)
157 /// - [php - How can I convert an absolute system path to an IShellFolder? - Stack Overflow](https://stackoverflow.com/questions/22548071/how-can-i-convert-an-absolute-system-path-to-an-ishellfolder)
158 fn from_path_w(hwnd: HWND, path: PCWSTR) -> Result<IShellFolder> {
159 let desktop = Self::from_desktop()?;
160 let pidl = desktop.parse_display_name_to_id_list(hwnd, path)?;
161 unsafe { desktop.BindToObject(pidl.0, None) }
162 }
163
164 /// Returns an arbitrary object of [`CFSFolder`](super::folder#cfsfolder).
165 fn from_fs_any(hwnd: HWND) -> Result<IShellFolder> {
166 Self::from_path_w(hwnd, w!(r"C:\Windows"))
167 }
168
169 /// Translates the display name of a file object or a folder into an item identifier list.
170 ///
171 /// - Doesn't handle relative path or parent folder indicators ("." or "..").
172 /// - Case-insensitive.
173 ///
174 /// [IShellFolder::ParseDisplayName (shobjidl_core.h)](https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-ishellfolder-parsedisplayname)
175 ///
176 /// ## Returns
177 /// When it is no longer needed, it is the responsibility of the caller to free this resource by calling [`CoTaskMemFree`].
178 fn parse_display_name(
179 &self,
180 hwnd: HWND,
181 display_name: PCWSTR,
182 ) -> Result<(usize, RelativeIDList)>;
183
184 fn parse_display_name_to_id_list(
185 &self,
186 hwnd: HWND,
187 display_name: PCWSTR,
188 ) -> Result<RelativeIDList> {
189 self.parse_display_name(hwnd, display_name).map(|r| r.1)
190 }
191
192 /// [IShellFolder::CompareIDs (shobjidl_core.h)](https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-ishellfolder-compareids)
193 ///
194 /// See [`CompareIDs()`](super::folder#compareids) for details.
195 fn compare_ids(
196 &self,
197 param: CompareIDs,
198 pidl1: &RelativeIDList,
199 pidl2: &RelativeIDList,
200 ) -> Result<cmp::Ordering>;
201
202 /// [IShellFolder::GetAttributesOf (shobjidl_core.h)](https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-ishellfolder-getattributesof)
203 ///
204 /// ## Returns
205 /// Requested attributes in `mask` that are common to all of the specified items.
206 fn get_attributes_of(
207 &self,
208 children: &[ChildIDRef],
209 mask: ItemAttributes,
210 ) -> Result<ItemAttributes>;
211
212 /// Tests if the child is a Shell folder (not necessarily a file system directory).
213 ///
214 /// This can also be implemented via [`ShellFolder::get_display_name_of()`].
215 /// But it's probably slower as attributes are already stored in the ID.
216 fn is_child_folder(&self, child: ChildIDRef) -> bool {
217 self.get_attributes_of(&[child], ItemAttributes::Folder)
218 .is_ok_and(|attrs| attrs.contains(ItemAttributes::Folder))
219 }
220
221 /// Tests if the child is a file system directory.
222 ///
223 /// This can also be implemented via [`ShellFolder::get_path_of()`].
224 /// But it's probably slower as attributes are already stored in the ID.
225 fn is_child_fs_folder(&self, child: ChildIDRef) -> bool {
226 const MASK: ItemAttributes = ItemAttributes::Folder.union(ItemAttributes::FileSystem);
227 self.get_attributes_of(&[child], MASK)
228 .is_ok_and(|attrs| attrs.contains(MASK))
229 }
230
231 /// Tests if the children are Shell folders (not necessarily file system directories).
232 ///
233 /// This can also be implemented via [`ShellFolder::get_display_name_of()`].
234 /// But it's probably slower as attributes are already stored in the ID.
235 #[deprecated = "This is usually not supported, including CFSFolder from Windows XP to 11"]
236 fn are_children_folders(&self, children: &[ChildIDRef]) -> bool {
237 self.get_attributes_of(children, ItemAttributes::Folder)
238 .is_ok_and(|attrs| attrs.contains(ItemAttributes::Folder))
239 }
240
241 /// Retrieves the display name for a specified item in the namespace.
242 ///
243 /// [IShellFolder::GetDisplayNameOf (shobjidl_core.h)](https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-ishellfolder-getdisplaynameof)
244 ///
245 /// ## Parameters
246 /// - `pidl`: Pointer to an item identifier list that identifies the child item.
247 /// - `uflags`: Flags that specify the type of display name to return.
248 ///
249 /// See [_SHGDNF (shobjidl_core.h)](https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/ne-shobjidl_core-_shgdnf)
250 ///
251 /// ## Returns
252 /// The display name of the item specified by `pidl`, in the format specified by `uflags`.
253 ///
254 /// Because [`STRRET`] is hard to work with, this method converts it to [`BSTR`]
255 /// before returning. However, this introduces a mem alloc. If you want to avoid it
256 /// you can directly call [`IShellFolder::GetDisplayNameOf()`].
257 fn get_display_name_of(&self, pidl: ChildIDRef, uflags: SHGDNF) -> Result<BSTR>;
258
259 /// Get the display name for parsing relative to the desktop.
260 ///
261 /// i.e. `get_display_name_of(SHGDN_FORPARSING)`
262 ///
263 /// ## Returns
264 /// e.g. `C:\Windows`
265 fn get_path_of(&self, pidl: ChildIDRef) -> Result<BSTR> {
266 self.get_display_name_of(pidl, SHGDN_FORPARSING)
267 }
268}
269
270impl ShellFolder for IShellFolder {
271 fn parse_display_name(
272 &self,
273 hwnd: HWND,
274 display_name: PCWSTR,
275 ) -> Result<(usize, RelativeIDList)> {
276 let mut ch_eaten: u32 = 0;
277 let mut pidl: *mut ITEMIDLIST = std::ptr::null_mut();
278 unsafe {
279 self.ParseDisplayName(
280 hwnd,
281 None,
282 display_name,
283 Some(&mut ch_eaten),
284 &mut pidl,
285 std::ptr::null_mut(),
286 )
287 }?;
288 Ok((ch_eaten as usize, RelativeIDList(pidl)))
289 }
290
291 fn compare_ids(
292 &self,
293 param: CompareIDs,
294 pidl1: &RelativeIDList,
295 pidl2: &RelativeIDList,
296 ) -> Result<cmp::Ordering> {
297 let hres = unsafe { self.CompareIDs(param.into(), pidl1.0, pidl2.0) };
298 hres.ok()?;
299 let code = (hres.0 & 0xFFFF) as i16;
300 // dbg!(code, code.cmp(&0));
301 Ok(code.cmp(&0))
302 }
303
304 fn get_attributes_of(
305 &self,
306 children: &[ChildIDRef],
307 mask: ItemAttributes,
308 ) -> Result<ItemAttributes> {
309 let children: &[*const ITEMIDLIST] = unsafe { mem::transmute(children) };
310 let mut mask = mask.bits();
311 unsafe { self.GetAttributesOf(children, &mut mask) }?;
312 Ok(ItemAttributes::from_bits_retain(mask))
313 }
314
315 fn get_display_name_of(&self, pidl: ChildIDRef, uflags: SHGDNF) -> Result<BSTR> {
316 let mut name = STRRET::default();
317 unsafe { self.GetDisplayNameOf(pidl.0, uflags, &mut name) }?;
318 let mut str = BSTR::new();
319 (unsafe { StrRetToBSTR(&mut name, Some(pidl.0), &mut str) })?;
320 Ok(str)
321 }
322}
323
324#[cfg(test)]
325mod tests {
326 use super::*;
327 use windows::core::w;
328
329 use crate::prop::column::FSColumn;
330
331 #[test]
332 fn from_desktop() {
333 let _desktop = IShellFolder::from_desktop().unwrap();
334 }
335
336 #[test]
337 fn from_path() {
338 let _folder = IShellFolder::from_path_w(HWND::default(), w!(r"C:\Windows")).unwrap();
339 }
340
341 #[test]
342 fn parse_display_name() {
343 let desktop = IShellFolder::from_desktop().unwrap();
344 let display_name = w!(r"C:\Windows");
345 let result = desktop.parse_display_name(HWND::default(), display_name);
346 dbg!(&result);
347 let (_ch_eaten, pidl) = result.unwrap();
348 // Broken?
349 // assert!(ch_eaten > 0);
350 assert!(!pidl.0.is_null());
351 }
352
353 #[test]
354 fn compare_ids() {
355 let c = IShellFolder::from_path_w(HWND::default(), w!(r"C:\")).unwrap();
356
357 let windows_pidl = c
358 .parse_display_name_to_id_list(HWND::default(), w!(r"Windows"))
359 .unwrap();
360 let users_pidl = c
361 .parse_display_name_to_id_list(HWND::default(), w!(r"Users"))
362 .unwrap();
363 let (windows_pidl, users_pidl) = (&windows_pidl, &users_pidl);
364 dbg!(windows_pidl, users_pidl);
365
366 let result = c
367 .compare_ids(Default::default(), windows_pidl, users_pidl)
368 .unwrap();
369 assert_eq!(result, cmp::Ordering::Greater);
370
371 let result = c
372 .compare_ids(CompareIDs::CANONICAL_ONLY, windows_pidl, users_pidl)
373 .unwrap();
374 assert_eq!(result, cmp::Ordering::Greater);
375
376 let result = c
377 .compare_ids(
378 CompareIDs::builder().column(FSColumn::Size).build(),
379 windows_pidl,
380 users_pidl,
381 )
382 .unwrap();
383 assert_eq!(result, cmp::Ordering::Equal);
384 }
385
386 #[test]
387 fn compare_ids_size() {
388 let windows = IShellFolder::from_path_w(HWND::default(), w!(r"C:\Windows")).unwrap();
389
390 let explorer_pidl = windows
391 .parse_display_name_to_id_list(HWND::default(), w!(r"explorer.exe"))
392 .unwrap();
393 let notepad_pidl = windows
394 .parse_display_name_to_id_list(HWND::default(), w!(r"notepad.exe"))
395 .unwrap();
396 let (explorer_pidl, notepad_pidl) = (&explorer_pidl, ¬epad_pidl);
397
398 let result = windows
399 .compare_ids(
400 CompareIDs::builder().column(FSColumn::Size).build(),
401 explorer_pidl,
402 notepad_pidl,
403 )
404 .unwrap();
405 assert_eq!(result, cmp::Ordering::Greater);
406 let result = windows
407 .compare_ids(
408 CompareIDs::builder().column(FSColumn::Size).build(),
409 notepad_pidl,
410 explorer_pidl,
411 )
412 .unwrap();
413 assert_eq!(result, cmp::Ordering::Less);
414 }
415
416 #[test]
417 fn compare_ids_nest() {
418 let desktop = IShellFolder::from_desktop().unwrap();
419
420 let windows_pidl = desktop
421 .parse_display_name_to_id_list(HWND::default(), w!(r"C:\Windows"))
422 .unwrap();
423 let users_pidl = desktop
424 .parse_display_name_to_id_list(HWND::default(), w!(r"C:\Users"))
425 .unwrap();
426 let (windows_pidl, users_pidl) = (&windows_pidl, &users_pidl);
427
428 let result = desktop
429 .compare_ids(Default::default(), windows_pidl, users_pidl)
430 .unwrap();
431 assert_eq!(result, cmp::Ordering::Greater);
432
433 let result = desktop
434 .compare_ids(CompareIDs::CANONICAL_ONLY, windows_pidl, users_pidl)
435 .unwrap();
436 assert_eq!(result, cmp::Ordering::Greater);
437 }
438
439 #[test]
440 fn compare_ids_equal() {
441 let desktop = IShellFolder::from_desktop().unwrap();
442
443 let pidl1 = desktop
444 .parse_display_name_to_id_list(HWND::default(), w!(r"C:\Windows"))
445 .unwrap();
446 let pidl2 = desktop
447 .parse_display_name_to_id_list(HWND::default(), w!(r"C:\Windows"))
448 .unwrap();
449 let (pidl1, pidl2) = (&pidl1, &pidl2);
450
451 let result = desktop
452 .compare_ids(Default::default(), pidl1, pidl2)
453 .unwrap();
454 assert_eq!(result, cmp::Ordering::Equal);
455 }
456
457 #[test]
458 fn compare_ids_err() {
459 // CompareIDs with invalid PIDs should fail - just verify it compiles and returns Err
460 let desktop = IShellFolder::from_desktop().unwrap();
461 let pidl1 = RelativeIDList(Default::default());
462 let pidl2 = RelativeIDList(Default::default());
463 let (pidl1, pidl2) = (&pidl1, &pidl2);
464 let result = desktop.compare_ids(Default::default(), pidl1, pidl2);
465 assert!(result.is_err());
466 }
467
468 #[test]
469 fn get_display_name_of() {
470 let c = IShellFolder::from_path_w(HWND::default(), w!(r"C:\")).unwrap();
471
472 let windows_pidl = c
473 .parse_display_name_to_id_list(HWND::default(), w!(r"Windows"))
474 .unwrap();
475
476 // Test with SHGDNF::SHGDN_NORMAL (0)
477 let result = c.get_display_name_of(windows_pidl.to_child_ref(), SHGDNF(0));
478 assert_eq!(result.unwrap().to_string(), "Windows");
479
480 let result = c.get_path_of(windows_pidl.to_child_ref());
481 assert_eq!(result.unwrap().to_string(), r"C:\Windows");
482 }
483}