Skip to main content

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}