Skip to main content

max / audiofiles

11.1 KB · 335 lines History Blame Raw
1 //! Windows drag backend: OLE `DoDragDrop` with `CF_HDROP`.
2 //!
3 //! Implements the three COM interfaces that Windows requires to act as a
4 //! drag source for files:
5 //!
6 //! - **`IDropSource`** — controls drag continuation (escape cancels, mouse-up drops).
7 //! - **`IDataObject`** — serves clipboard data; we provide a single `CF_HDROP`
8 //! `HGLOBAL` containing a `DROPFILES` header followed by null-terminated
9 //! UTF-16 file paths.
10 //! - **`IEnumFORMATETC`** — enumerates available clipboard formats (just CF_HDROP).
11 //!
12 //! `DoDragDrop` is a blocking/modal call — it runs a nested message loop
13 //! and returns only when the user drops or cancels. The `DRAG_ACTIVE` guard
14 //! in the parent module is cleared synchronously after it returns.
15
16 use std::os::windows::ffi::OsStrExt;
17 use std::path::PathBuf;
18 use std::sync::atomic::{AtomicU32, Ordering};
19
20 use tracing::{debug, warn};
21 use windows::core::*;
22 use windows::Win32::Foundation::*;
23 use windows::Win32::System::Com::*;
24 // STGMEDIUM and STGMEDIUM_0 are in Win32::System::Com (re-exported via Com::*)
25 use windows::Win32::System::Memory::*;
26 use windows::Win32::System::Ole::*;
27 use windows::Win32::System::SystemServices::MODIFIERKEYS_FLAGS;
28
29 const CF_HDROP_VALUE: u16 = 15;
30 const MK_LBUTTON: u32 = 0x0001;
31
32 // ---------- DROPFILES header ----------
33
34 /// Win32 DROPFILES structure. Defined here to avoid pulling in the Shell feature.
35 /// Layout is ABI-stable: 20-byte header followed by null-terminated UTF-16 paths.
36 #[repr(C)]
37 struct DropFilesHeader {
38 /// Byte offset from the start of this struct to the first file path.
39 p_files: u32,
40 pt_x: i32,
41 pt_y: i32,
42 /// Non-client area flag (unused, zero).
43 f_nc: i32,
44 /// 1 = paths are UTF-16. Must be 1 for Unicode paths.
45 f_wide: i32,
46 }
47
48 fn hdrop_formatetc() -> FORMATETC {
49 FORMATETC {
50 cfFormat: CF_HDROP_VALUE,
51 ptd: std::ptr::null_mut(),
52 dwAspect: DVASPECT_CONTENT.0 as u32,
53 lindex: -1,
54 tymed: TYMED_HGLOBAL.0 as u32,
55 }
56 }
57
58 // ---------- Build CF_HDROP HGLOBAL ----------
59
60 /// Allocate a moveable HGLOBAL containing a DROPFILES header + UTF-16 paths.
61 /// The buffer ends with a double-null terminator (first null ends the last
62 /// path, second null ends the list). GMEM_ZEROINIT handles the trailing null.
63 ///
64 /// # Safety
65 /// Caller must free the returned HGLOBAL via `GlobalFree` when done.
66 /// All pointer arithmetic stays within the `total_bytes` allocation.
67 unsafe fn build_hdrop(paths: &[PathBuf]) -> Result<(HGLOBAL, usize)> {
68 let header_size = std::mem::size_of::<DropFilesHeader>();
69 let wide_paths: Vec<Vec<u16>> = paths
70 .iter()
71 .map(|p| p.as_os_str().encode_wide().chain(std::iter::once(0)).collect())
72 .collect();
73 let total_wide: usize = wide_paths.iter().map(|w| w.len()).sum::<usize>() + 1;
74 let total_bytes = header_size + total_wide * 2;
75
76 let h = GlobalAlloc(GMEM_MOVEABLE | GMEM_ZEROINIT, total_bytes)?;
77 let ptr = GlobalLock(h);
78 if ptr.is_null() {
79 let _ = GlobalFree(Some(h));
80 return Err(Error::from(E_OUTOFMEMORY));
81 }
82 let ptr = ptr as *mut u8;
83
84 let header = ptr as *mut DropFilesHeader;
85 (*header).p_files = header_size as u32;
86 (*header).f_wide = 1;
87
88 let mut offset = header_size;
89 for wide in &wide_paths {
90 let byte_len = wide.len() * 2;
91 std::ptr::copy_nonoverlapping(wide.as_ptr() as *const u8, ptr.add(offset), byte_len);
92 offset += byte_len;
93 }
94
95 let _ = GlobalUnlock(h);
96 Ok((h, total_bytes))
97 }
98
99 // ---------- IDropSource ----------
100
101 /// Minimal drag source: cancel on Escape, drop on mouse-up, default cursors.
102 #[implement(IDropSource)]
103 struct DropSource;
104
105 impl IDropSource_Impl for DropSource_Impl {
106 fn QueryContinueDrag(&self, fescapepressed: BOOL, grfkeystate: MODIFIERKEYS_FLAGS) -> HRESULT {
107 if fescapepressed.as_bool() {
108 DRAGDROP_S_CANCEL
109 } else if grfkeystate.0 & MK_LBUTTON == 0 {
110 DRAGDROP_S_DROP
111 } else {
112 S_OK
113 }
114 }
115
116 fn GiveFeedback(&self, _dweffect: DROPEFFECT) -> HRESULT {
117 DRAGDROP_S_USEDEFAULTCURSORS
118 }
119 }
120
121 // ---------- IEnumFORMATETC ----------
122
123 /// Enumerates a single clipboard format (CF_HDROP).
124 #[implement(IEnumFORMATETC)]
125 struct HDropFormatEnum {
126 pos: AtomicU32,
127 }
128
129 impl IEnumFORMATETC_Impl for HDropFormatEnum_Impl {
130 fn Next(
131 &self,
132 celt: u32,
133 rgelt: *mut FORMATETC,
134 pceltfetched: *mut u32,
135 ) -> HRESULT {
136 let pos = self.pos.load(Ordering::Relaxed);
137 if pos >= 1 || celt == 0 {
138 if !pceltfetched.is_null() {
139 // SAFETY: COM contract — pceltfetched is a valid caller-allocated
140 // out-parameter when non-null (null-checked above).
141 unsafe { *pceltfetched = 0 };
142 }
143 return S_FALSE;
144 }
145 self.pos.store(1, Ordering::Relaxed);
146 // SAFETY: COM contract — rgelt points to a caller-allocated array of at
147 // least `celt` FORMATETC elements. pceltfetched is valid when non-null.
148 unsafe {
149 *rgelt = hdrop_formatetc();
150 if !pceltfetched.is_null() {
151 *pceltfetched = 1;
152 }
153 }
154 S_OK
155 }
156
157 fn Skip(&self, celt: u32) -> windows_core::Result<()> {
158 self.pos.fetch_add(celt, Ordering::Relaxed);
159 Ok(())
160 }
161
162 fn Reset(&self) -> windows_core::Result<()> {
163 self.pos.store(0, Ordering::Relaxed);
164 Ok(())
165 }
166
167 fn Clone(&self) -> Result<IEnumFORMATETC> {
168 let e: IEnumFORMATETC = HDropFormatEnum {
169 pos: AtomicU32::new(self.pos.load(Ordering::Relaxed)),
170 }
171 .into();
172 Ok(e)
173 }
174 }
175
176 // ---------- IDataObject ----------
177
178 /// Serves a single CF_HDROP payload. Clones the HGLOBAL on each `GetData`
179 /// call so the caller owns its copy. Frees the original on drop.
180 #[implement(IDataObject)]
181 struct FileDataObject {
182 hdrop: HGLOBAL,
183 size: usize,
184 }
185
186 impl Drop for FileDataObject {
187 fn drop(&mut self) {
188 // SAFETY: self.hdrop was allocated by build_hdrop via GlobalAlloc and
189 // has not been freed yet (Drop runs exactly once).
190 unsafe {
191 let _ = GlobalFree(Some(self.hdrop));
192 }
193 }
194 }
195
196 impl IDataObject_Impl for FileDataObject_Impl {
197 fn GetData(&self, pformatetcin: *const FORMATETC) -> Result<STGMEDIUM> {
198 // SAFETY: COM contract — pformatetcin is a valid non-null pointer to a
199 // caller-owned FORMATETC.
200 let fmt = unsafe { &*pformatetcin };
201 if fmt.cfFormat != CF_HDROP_VALUE {
202 return Err(Error::from(DV_E_FORMATETC));
203 }
204
205 // SAFETY: self.hdrop is valid (not freed until Drop). New HGLOBAL is
206 // allocated at the same size. Copy stays within bounds (self.size bytes).
207 // The returned STGMEDIUM transfers ownership of `h` to the caller.
208 unsafe {
209 let h = GlobalAlloc(GMEM_MOVEABLE, self.size)?;
210 let src = GlobalLock(self.hdrop);
211 if src.is_null() {
212 let _ = GlobalFree(Some(h));
213 return Err(Error::from(E_OUTOFMEMORY));
214 }
215 let dst = GlobalLock(h);
216 if dst.is_null() {
217 let _ = GlobalUnlock(self.hdrop);
218 let _ = GlobalFree(Some(h));
219 return Err(Error::from(E_OUTOFMEMORY));
220 }
221 std::ptr::copy_nonoverlapping(src as *const u8, dst as *mut u8, self.size);
222 let _ = GlobalUnlock(self.hdrop);
223 let _ = GlobalUnlock(h);
224
225 Ok(STGMEDIUM {
226 tymed: TYMED_HGLOBAL.0 as u32,
227 u: STGMEDIUM_0 { hGlobal: h },
228 pUnkForRelease: std::mem::ManuallyDrop::new(None),
229 })
230 }
231 }
232
233 fn GetDataHere(&self, _: *const FORMATETC, _: *mut STGMEDIUM) -> Result<()> {
234 Err(Error::from(E_NOTIMPL))
235 }
236
237 fn QueryGetData(&self, pformatetc: *const FORMATETC) -> HRESULT {
238 // SAFETY: COM contract — pformatetc is a valid non-null pointer.
239 let fmt = unsafe { &*pformatetc };
240 if fmt.cfFormat == CF_HDROP_VALUE {
241 S_OK
242 } else {
243 DV_E_FORMATETC
244 }
245 }
246
247 fn GetCanonicalFormatEtc(&self, _: *const FORMATETC, pformatetcout: *mut FORMATETC) -> HRESULT {
248 // SAFETY: COM contract guarantees `pformatetcout` is a valid non-null pointer
249 // allocated by the caller. We only write `ptd` to indicate no device target.
250 unsafe {
251 (*pformatetcout).ptd = std::ptr::null_mut();
252 }
253 DATA_S_SAMEFORMATETC
254 }
255
256 fn SetData(&self, _: *const FORMATETC, _: *const STGMEDIUM, _: BOOL) -> Result<()> {
257 Err(Error::from(E_NOTIMPL))
258 }
259
260 fn EnumFormatEtc(&self, dwdirection: u32) -> Result<IEnumFORMATETC> {
261 if dwdirection != DATADIR_GET.0 as u32 {
262 return Err(Error::from(E_NOTIMPL));
263 }
264 let e: IEnumFORMATETC = HDropFormatEnum {
265 pos: AtomicU32::new(0),
266 }
267 .into();
268 Ok(e)
269 }
270
271 fn DAdvise(&self, _: *const FORMATETC, _: u32, _: Ref<IAdviseSink>) -> Result<u32> {
272 Err(Error::from(OLE_E_ADVISENOTSUPPORTED))
273 }
274
275 fn DUnadvise(&self, _: u32) -> Result<()> {
276 Err(Error::from(OLE_E_ADVISENOTSUPPORTED))
277 }
278
279 fn EnumDAdvise(&self) -> Result<IEnumSTATDATA> {
280 Err(Error::from(OLE_E_ADVISENOTSUPPORTED))
281 }
282 }
283
284 // ---------- Entry point ----------
285
286 /// Start a blocking OLE drag-drop session. Returns `true` if the user
287 /// completed the drop (as opposed to cancelling or dragging to a
288 /// non-accepting target).
289 pub(super) fn begin_drag_session(paths: &[PathBuf]) -> bool {
290 // SAFETY: OleInitialize/OleUninitialize bracket the session. build_hdrop is
291 // called with valid paths and its HGLOBAL is owned by FileDataObject (freed
292 // on drop). COM objects are constructed via safe `into()`. DoDragDrop is the
293 // standard OLE drag entry point — it blocks until the user drops or cancels.
294 unsafe {
295 // OleInitialize is the superset of CoInitializeEx — required for DoDragDrop.
296 // Ok(()) means we initialized, Err with S_FALSE means already initialized (fine).
297 let ole_result = OleInitialize(None);
298 let we_initialized = ole_result.is_ok();
299 if !we_initialized {
300 // Check if it's the benign "already initialized" case
301 if let Err(ref e) = ole_result {
302 if e.code() != S_FALSE {
303 warn!(?ole_result, "OleInitialize failed");
304 return false;
305 }
306 }
307 }
308
309 let result = (|| -> Result<bool> {
310 let (hdrop, size) = build_hdrop(paths)?;
311
312 let data_object: IDataObject = FileDataObject { hdrop, size }.into();
313 let drop_source: IDropSource = DropSource.into();
314
315 let mut effect = DROPEFFECT(0);
316 let hr = DoDragDrop(&data_object, &drop_source, DROPEFFECT_COPY, &mut effect);
317
318 debug!(?hr, ?effect, "DoDragDrop completed");
319 Ok(hr == DRAGDROP_S_DROP)
320 })();
321
322 if we_initialized {
323 OleUninitialize();
324 }
325
326 match result {
327 Ok(dropped) => dropped,
328 Err(e) => {
329 warn!(%e, "Drag failed");
330 false
331 }
332 }
333 }
334 }
335