use std::{cmp, mem};
use bon::Builder;
use windows::{
Win32::{
Foundation::{HWND, LPARAM},
UI::Shell::{
Common::{ITEMIDLIST, STRRET},
IShellFolder, SHCIDS_ALLFIELDS, SHCIDS_CANONICALONLY, SHGDN_FORPARSING, SHGDNF,
SHGetDesktopFolder, StrRetToBSTR,
},
},
core::{BSTR, PCWSTR, Result, w},
};
use crate::{
id_list::{ChildIDRef, RelativeIDList},
prop::attribute::ItemAttributes,
};
mod compare;
#[derive(Debug, Clone, Copy, Default, Builder)]
pub struct CompareIDs {
#[builder(default, into)]
pub column: u16,
#[builder(default)]
pub flags: u16,
}
impl CompareIDs {
pub const ALL_FIELDS: CompareIDs = CompareIDs {
column: 0,
flags: (SHCIDS_ALLFIELDS >> 16) as u16,
};
pub const CANONICAL_ONLY: CompareIDs = CompareIDs {
column: 0,
flags: (SHCIDS_CANONICALONLY >> 16) as u16,
};
}
impl Into<LPARAM> for CompareIDs {
fn into(self) -> LPARAM {
LPARAM((self.column as u32 | (self.flags as u32) << 16) as isize)
}
}
pub trait ShellFolder {
fn from_desktop() -> Result<IShellFolder> {
unsafe { SHGetDesktopFolder() }
}
fn from_id_list(pidl: &RelativeIDList) -> Result<IShellFolder> {
let desktop = Self::from_desktop()?;
unsafe { desktop.BindToObject(pidl.0, None) }
}
fn from_path_w(hwnd: HWND, path: PCWSTR) -> Result<IShellFolder> {
let desktop = Self::from_desktop()?;
let pidl = desktop.parse_display_name_to_id_list(hwnd, path)?;
unsafe { desktop.BindToObject(pidl.0, None) }
}
fn from_fs_any(hwnd: HWND) -> Result<IShellFolder> {
Self::from_path_w(hwnd, w!(r"C:\Windows"))
}
fn parse_display_name(
&self,
hwnd: HWND,
display_name: PCWSTR,
) -> Result<(usize, RelativeIDList)>;
fn parse_display_name_to_id_list(
&self,
hwnd: HWND,
display_name: PCWSTR,
) -> Result<RelativeIDList> {
self.parse_display_name(hwnd, display_name).map(|r| r.1)
}
fn compare_ids(
&self,
param: CompareIDs,
pidl1: &RelativeIDList,
pidl2: &RelativeIDList,
) -> Result<cmp::Ordering>;
fn get_attributes_of(
&self,
children: &[ChildIDRef],
mask: ItemAttributes,
) -> Result<ItemAttributes>;
fn is_child_folder(&self, child: ChildIDRef) -> bool {
self.get_attributes_of(&[child], ItemAttributes::Folder)
.is_ok_and(|attrs| attrs.contains(ItemAttributes::Folder))
}
fn is_child_fs_folder(&self, child: ChildIDRef) -> bool {
const MASK: ItemAttributes = ItemAttributes::Folder.union(ItemAttributes::FileSystem);
self.get_attributes_of(&[child], MASK)
.is_ok_and(|attrs| attrs.contains(MASK))
}
#[deprecated = "This is usually not supported, including CFSFolder from Windows XP to 11"]
fn are_children_folders(&self, children: &[ChildIDRef]) -> bool {
self.get_attributes_of(children, ItemAttributes::Folder)
.is_ok_and(|attrs| attrs.contains(ItemAttributes::Folder))
}
fn get_display_name_of(&self, pidl: ChildIDRef, uflags: SHGDNF) -> Result<BSTR>;
fn get_path_of(&self, pidl: ChildIDRef) -> Result<BSTR> {
self.get_display_name_of(pidl, SHGDN_FORPARSING)
}
}
impl ShellFolder for IShellFolder {
fn parse_display_name(
&self,
hwnd: HWND,
display_name: PCWSTR,
) -> Result<(usize, RelativeIDList)> {
let mut ch_eaten: u32 = 0;
let mut pidl: *mut ITEMIDLIST = std::ptr::null_mut();
unsafe {
self.ParseDisplayName(
hwnd,
None,
display_name,
Some(&mut ch_eaten),
&mut pidl,
std::ptr::null_mut(),
)
}?;
Ok((ch_eaten as usize, RelativeIDList(pidl)))
}
fn compare_ids(
&self,
param: CompareIDs,
pidl1: &RelativeIDList,
pidl2: &RelativeIDList,
) -> Result<cmp::Ordering> {
let hres = unsafe { self.CompareIDs(param.into(), pidl1.0, pidl2.0) };
hres.ok()?;
let code = (hres.0 & 0xFFFF) as i16;
Ok(code.cmp(&0))
}
fn get_attributes_of(
&self,
children: &[ChildIDRef],
mask: ItemAttributes,
) -> Result<ItemAttributes> {
let children: &[*const ITEMIDLIST] = unsafe { mem::transmute(children) };
let mut mask = mask.bits();
unsafe { self.GetAttributesOf(children, &mut mask) }?;
Ok(ItemAttributes::from_bits_retain(mask))
}
fn get_display_name_of(&self, pidl: ChildIDRef, uflags: SHGDNF) -> Result<BSTR> {
let mut name = STRRET::default();
unsafe { self.GetDisplayNameOf(pidl.0, uflags, &mut name) }?;
let mut str = BSTR::new();
(unsafe { StrRetToBSTR(&mut name, Some(pidl.0), &mut str) })?;
Ok(str)
}
}
#[cfg(test)]
mod tests {
use super::*;
use windows::core::w;
use crate::prop::column::FSColumn;
#[test]
fn from_desktop() {
let _desktop = IShellFolder::from_desktop().unwrap();
}
#[test]
fn from_path() {
let _folder = IShellFolder::from_path_w(HWND::default(), w!(r"C:\Windows")).unwrap();
}
#[test]
fn parse_display_name() {
let desktop = IShellFolder::from_desktop().unwrap();
let display_name = w!(r"C:\Windows");
let result = desktop.parse_display_name(HWND::default(), display_name);
dbg!(&result);
let (_ch_eaten, pidl) = result.unwrap();
assert!(!pidl.0.is_null());
}
#[test]
fn compare_ids() {
let c = IShellFolder::from_path_w(HWND::default(), w!(r"C:\")).unwrap();
let windows_pidl = c
.parse_display_name_to_id_list(HWND::default(), w!(r"Windows"))
.unwrap();
let users_pidl = c
.parse_display_name_to_id_list(HWND::default(), w!(r"Users"))
.unwrap();
let (windows_pidl, users_pidl) = (&windows_pidl, &users_pidl);
dbg!(windows_pidl, users_pidl);
let result = c
.compare_ids(Default::default(), windows_pidl, users_pidl)
.unwrap();
assert_eq!(result, cmp::Ordering::Greater);
let result = c
.compare_ids(CompareIDs::CANONICAL_ONLY, windows_pidl, users_pidl)
.unwrap();
assert_eq!(result, cmp::Ordering::Greater);
let result = c
.compare_ids(
CompareIDs::builder().column(FSColumn::Size).build(),
windows_pidl,
users_pidl,
)
.unwrap();
assert_eq!(result, cmp::Ordering::Equal);
}
#[test]
fn compare_ids_size() {
let windows = IShellFolder::from_path_w(HWND::default(), w!(r"C:\Windows")).unwrap();
let explorer_pidl = windows
.parse_display_name_to_id_list(HWND::default(), w!(r"explorer.exe"))
.unwrap();
let notepad_pidl = windows
.parse_display_name_to_id_list(HWND::default(), w!(r"notepad.exe"))
.unwrap();
let (explorer_pidl, notepad_pidl) = (&explorer_pidl, ¬epad_pidl);
let result = windows
.compare_ids(
CompareIDs::builder().column(FSColumn::Size).build(),
explorer_pidl,
notepad_pidl,
)
.unwrap();
assert_eq!(result, cmp::Ordering::Greater);
let result = windows
.compare_ids(
CompareIDs::builder().column(FSColumn::Size).build(),
notepad_pidl,
explorer_pidl,
)
.unwrap();
assert_eq!(result, cmp::Ordering::Less);
}
#[test]
fn compare_ids_nest() {
let desktop = IShellFolder::from_desktop().unwrap();
let windows_pidl = desktop
.parse_display_name_to_id_list(HWND::default(), w!(r"C:\Windows"))
.unwrap();
let users_pidl = desktop
.parse_display_name_to_id_list(HWND::default(), w!(r"C:\Users"))
.unwrap();
let (windows_pidl, users_pidl) = (&windows_pidl, &users_pidl);
let result = desktop
.compare_ids(Default::default(), windows_pidl, users_pidl)
.unwrap();
assert_eq!(result, cmp::Ordering::Greater);
let result = desktop
.compare_ids(CompareIDs::CANONICAL_ONLY, windows_pidl, users_pidl)
.unwrap();
assert_eq!(result, cmp::Ordering::Greater);
}
#[test]
fn compare_ids_equal() {
let desktop = IShellFolder::from_desktop().unwrap();
let pidl1 = desktop
.parse_display_name_to_id_list(HWND::default(), w!(r"C:\Windows"))
.unwrap();
let pidl2 = desktop
.parse_display_name_to_id_list(HWND::default(), w!(r"C:\Windows"))
.unwrap();
let (pidl1, pidl2) = (&pidl1, &pidl2);
let result = desktop
.compare_ids(Default::default(), pidl1, pidl2)
.unwrap();
assert_eq!(result, cmp::Ordering::Equal);
}
#[test]
fn compare_ids_err() {
let desktop = IShellFolder::from_desktop().unwrap();
let pidl1 = RelativeIDList(Default::default());
let pidl2 = RelativeIDList(Default::default());
let (pidl1, pidl2) = (&pidl1, &pidl2);
let result = desktop.compare_ids(Default::default(), pidl1, pidl2);
assert!(result.is_err());
}
#[test]
fn get_display_name_of() {
let c = IShellFolder::from_path_w(HWND::default(), w!(r"C:\")).unwrap();
let windows_pidl = c
.parse_display_name_to_id_list(HWND::default(), w!(r"Windows"))
.unwrap();
let result = c.get_display_name_of(windows_pidl.to_child_ref(), SHGDNF(0));
assert_eq!(result.unwrap().to_string(), "Windows");
let result = c.get_path_of(windows_pidl.to_child_ref());
assert_eq!(result.unwrap().to_string(), r"C:\Windows");
}
}