win_context_menu/shell_item.rs
1//! Resolve filesystem paths into shell objects (`IShellFolder` + child PIDLs).
2//!
3//! The Windows Shell identifies items not by path strings but by *PIDLs*
4//! (Pointer to an Item ID List). This module converts user-supplied paths into
5//! the parent `IShellFolder` plus one or more relative child PIDLs that the
6//! context-menu APIs require.
7
8use std::path::{Path, PathBuf};
9
10use windows::Win32::System::Com::CoTaskMemAlloc;
11use windows::Win32::UI::Shell::Common::ITEMIDLIST;
12use windows::Win32::UI::Shell::{IShellFolder, SHBindToObject, SHBindToParent, SHParseDisplayName};
13
14use crate::com::Pidl;
15use crate::error::{Error, Result};
16use crate::util::{path_to_wide, strip_extended_prefix, wide_to_pcwstr};
17
18/// Resolved shell items ready to create a context menu from.
19///
20/// Holds the parent `IShellFolder` and one or more child PIDLs. For
21/// *background* menus (right-clicking empty space inside a folder), `child_pidls`
22/// is empty and the parent itself is the folder.
23///
24/// Create instances via [`ShellItems::from_path`], [`ShellItems::from_paths`],
25/// or [`ShellItems::folder_background`].
26pub struct ShellItems {
27 pub(crate) parent: IShellFolder,
28 pub(crate) child_pidls: Vec<Pidl>,
29 /// Keep absolute PIDLs alive so child pointers derived from them stay valid.
30 pub(crate) _absolute_pidls: Vec<Pidl>,
31 pub(crate) is_background: bool,
32}
33
34impl ShellItems {
35 /// Resolve a single file or folder path to shell items.
36 ///
37 /// # Errors
38 ///
39 /// Returns [`Error::ParsePath`] if the path does not exist or cannot be
40 /// resolved by the shell.
41 pub fn from_path(path: impl AsRef<Path>) -> Result<Self> {
42 Self::from_paths(&[path.as_ref().to_path_buf()])
43 }
44
45 /// Resolve multiple paths. **All paths must share the same parent folder.**
46 ///
47 /// This is the multi-select equivalent — the resulting context menu will
48 /// act on all specified items at once (like selecting several files in
49 /// Explorer, then right-clicking).
50 ///
51 /// # Errors
52 ///
53 /// - [`Error::NoCommonParent`] if `paths` is empty or the paths do not
54 /// share a common parent folder.
55 /// - [`Error::ParsePath`] if any path cannot be resolved.
56 pub fn from_paths(paths: &[impl AsRef<Path>]) -> Result<Self> {
57 if paths.is_empty() {
58 return Err(Error::NoCommonParent);
59 }
60
61 let mut absolute_pidls = Vec::with_capacity(paths.len());
62 let mut child_pidls = Vec::with_capacity(paths.len());
63 let mut parent_folder: Option<IShellFolder> = None;
64 let mut parent_path: Option<PathBuf> = None;
65
66 for path in paths {
67 let path = path.as_ref();
68 let canonical = std::fs::canonicalize(path)
69 .map(|p| strip_extended_prefix(&p))
70 .unwrap_or_else(|_| path.to_path_buf());
71
72 // Verify all paths share the same parent
73 let this_parent = canonical.parent().map(|p| p.to_path_buf());
74 match (&parent_path, &this_parent) {
75 (Some(existing), Some(new)) if existing != new => {
76 return Err(Error::NoCommonParent);
77 }
78 (None, Some(_)) => {
79 parent_path = this_parent;
80 }
81 _ => {}
82 }
83
84 let wide = path_to_wide(&canonical);
85 let pcwstr = wide_to_pcwstr(&wide);
86
87 // Parse the display name to an absolute PIDL.
88 let mut abs_pidl: *mut ITEMIDLIST = std::ptr::null_mut();
89 // SAFETY: `SHParseDisplayName` is a standard Shell API. We pass a
90 // valid null-terminated wide string (`pcwstr` derived from `wide`)
91 // and receive an output PIDL allocated via `CoTaskMemAlloc`. On
92 // success the PIDL is wrapped in `Pidl` for automatic cleanup.
93 unsafe {
94 SHParseDisplayName(pcwstr, None, &mut abs_pidl, 0, None).map_err(|e| {
95 Error::ParsePath {
96 path: canonical.clone(),
97 source: e,
98 }
99 })?;
100 }
101 // SAFETY: `abs_pidl` was just allocated by `SHParseDisplayName`
102 // via `CoTaskMemAlloc`. Ownership transfers to `Pidl`.
103 let abs_pidl = unsafe { Pidl::from_raw(abs_pidl) };
104
105 // Bind to parent to get the IShellFolder and a relative child PIDL.
106 let mut child_pidl_ptr: *mut ITEMIDLIST = std::ptr::null_mut();
107 // SAFETY: `SHBindToParent` takes an absolute PIDL and returns the
108 // parent `IShellFolder` plus a pointer to the last (relative) PIDL
109 // component. The relative PIDL points *into* `abs_pidl`'s memory,
110 // so we copy it into a fresh CoTaskMemAlloc buffer below.
111 // `CoTaskMemAlloc` + `copy_nonoverlapping` + null-terminator write
112 // are safe because we allocate `child_size + 2` bytes and copy
113 // exactly `child_size` bytes.
114 let shell_folder: IShellFolder = unsafe {
115 let mut child_pidl_raw: *mut ITEMIDLIST = std::ptr::null_mut();
116 let folder: IShellFolder =
117 SHBindToParent(abs_pidl.as_ptr(), Some(&mut child_pidl_raw))
118 .map_err(Error::BindToParent)?;
119
120 // Copy the relative child PIDL into its own CoTaskMem buffer
121 // because `child_pidl_raw` borrows from `abs_pidl`.
122 let child_size = pidl_size(child_pidl_raw as *const _);
123 if child_size > 0 {
124 // SAFETY: `CoTaskMemAlloc` returns a block of at least
125 // `child_size + 2` bytes. We copy exactly `child_size`
126 // bytes of PIDL data and write a 2-byte null terminator
127 // (the standard PIDL terminator: a SHITEMID with cb == 0).
128 let alloc = CoTaskMemAlloc(child_size + 2);
129 if !alloc.is_null() {
130 std::ptr::copy_nonoverlapping(
131 child_pidl_raw as *const u8,
132 alloc as *mut u8,
133 child_size,
134 );
135 // Write null terminator (cb = 0)
136 std::ptr::write_bytes((alloc as *mut u8).add(child_size), 0, 2);
137 child_pidl_ptr = alloc as *mut ITEMIDLIST;
138 }
139 }
140
141 folder
142 };
143
144 if parent_folder.is_none() {
145 parent_folder = Some(shell_folder);
146 }
147
148 // SAFETY: `child_pidl_ptr` was allocated by `CoTaskMemAlloc` above.
149 child_pidls.push(unsafe { Pidl::from_raw(child_pidl_ptr) });
150 absolute_pidls.push(abs_pidl);
151 }
152
153 Ok(Self {
154 parent: parent_folder.unwrap(),
155 child_pidls,
156 _absolute_pidls: absolute_pidls,
157 is_background: false,
158 })
159 }
160
161 /// Create shell items for the **background** of a folder.
162 ///
163 /// This is equivalent to right-clicking the empty space inside a folder
164 /// in Explorer (rather than right-clicking a specific file). The resulting
165 /// context menu typically offers "New", "Paste", "View", etc.
166 ///
167 /// # Errors
168 ///
169 /// Returns [`Error::ParsePath`] if the folder path cannot be resolved.
170 pub fn folder_background(folder: impl AsRef<Path>) -> Result<Self> {
171 let folder = folder.as_ref();
172 let canonical = std::fs::canonicalize(folder)
173 .map(|p| strip_extended_prefix(&p))
174 .unwrap_or_else(|_| folder.to_path_buf());
175
176 let wide = path_to_wide(&canonical);
177 let pcwstr = wide_to_pcwstr(&wide);
178
179 let mut abs_pidl: *mut ITEMIDLIST = std::ptr::null_mut();
180 // SAFETY: Same as the `SHParseDisplayName` call in `from_paths`.
181 unsafe {
182 SHParseDisplayName(pcwstr, None, &mut abs_pidl, 0, None).map_err(|e| {
183 Error::ParsePath {
184 path: canonical.clone(),
185 source: e,
186 }
187 })?;
188 }
189 // SAFETY: `abs_pidl` was just allocated by `SHParseDisplayName`.
190 let abs_pidl = unsafe { Pidl::from_raw(abs_pidl) };
191
192 // Bind directly to the folder as an IShellFolder so we can ask for its
193 // background context menu via `CreateViewObject`.
194 // SAFETY: `SHBindToObject` with `None` parent and a valid absolute
195 // PIDL returns the corresponding `IShellFolder`.
196 let folder_shell: IShellFolder = unsafe {
197 SHBindToObject(None, abs_pidl.as_ptr(), None).map_err(Error::BindToParent)?
198 };
199
200 Ok(Self {
201 parent: folder_shell,
202 child_pidls: Vec::new(),
203 _absolute_pidls: vec![abs_pidl],
204 is_background: true,
205 })
206 }
207}
208
209/// Calculate the total byte size of a PIDL chain (excluding the 2-byte null
210/// terminator).
211///
212/// A PIDL is a contiguous sequence of `SHITEMID` structures. Each starts with
213/// a `u16 cb` field giving its total size in bytes (including the `cb` field
214/// itself). A `cb` value of 0 marks the end.
215///
216/// # Safety
217///
218/// `pidl` must point to a valid, null-terminated PIDL or be null.
219unsafe fn pidl_size(pidl: *const ITEMIDLIST) -> usize {
220 if pidl.is_null() {
221 return 0;
222 }
223 let mut size = 0usize;
224 let mut ptr = pidl as *const u8;
225 loop {
226 // SAFETY: The PIDL layout guarantees a `u16` at every SHITEMID
227 // boundary. We use `read_unaligned` because the shell may hand us
228 // PIDLs at odd alignments after CoTaskMemAlloc.
229 let cb = unsafe { (ptr as *const u16).read_unaligned() };
230 if cb == 0 {
231 break;
232 }
233 size += cb as usize;
234 // SAFETY: Advancing by `cb` bytes moves to the next SHITEMID in
235 // the chain. The chain is terminated by cb == 0.
236 ptr = unsafe { ptr.add(cb as usize) };
237 }
238 size
239}