Skip to main content

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, &notepad_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}