Skip to main content

win_text_inject/
delayed.rs

1//! Delayed clipboard rendering: know exactly when the target read the clipboard.
2//!
3//! The "it pasted my previous clipboard" bug exists because the injector restores on a timer while
4//! the target reads the clipboard whenever its message pump gets round to it. Any fixed delay is a
5//! guess, and under load the guess is wrong. Tuning the delay upward — which is the shipped
6//! mitigation in every tool surveyed — only moves the threshold.
7//!
8//! Delayed rendering removes the guess. Instead of publishing the text, publish a promise:
9//! `SetClipboardData(CF_UNICODETEXT, NULL)` with this process as clipboard owner. Windows then
10//! sends `WM_RENDERFORMAT` to the owner at the instant a consumer actually asks for the data, and
11//! the owner supplies it then. That message *is* the "the target has read it" signal, so the
12//! restore can be sequenced strictly after the read instead of racing it.
13//!
14//! Requires a window with a running message pump, so this owns a hidden window on its own thread.
15
16use std::sync::atomic::{AtomicBool, AtomicIsize, Ordering};
17use std::sync::{Condvar, Mutex, OnceLock};
18use std::time::Duration;
19
20use windows::core::w;
21use windows::Win32::Foundation::{HANDLE, HWND, LPARAM, LRESULT, WPARAM};
22use windows::Win32::System::DataExchange::{
23    CloseClipboard, EmptyClipboard, GetClipboardOwner, IsClipboardFormatAvailable, OpenClipboard,
24    SetClipboardData,
25};
26use windows::Win32::System::LibraryLoader::GetModuleHandleW;
27use windows::Win32::UI::WindowsAndMessaging::{
28    CreateWindowExW, DefWindowProcW, DispatchMessageW, GetMessageW, RegisterClassW,
29    TranslateMessage, CW_USEDEFAULT, MSG, WM_DESTROYCLIPBOARD, WM_RENDERALLFORMATS,
30    WM_RENDERFORMAT, WNDCLASSW, WS_OVERLAPPED,
31};
32
33use crate::clipboard::{alloc_global_public, utf16_bytes_public, CF_UNICODETEXT_PUBLIC};
34use crate::Error;
35
36/// Text formats Windows may ask us to render. `CF_UNICODETEXT` is what we advertise; the others are
37/// synthesized from it, and a consumer asking for one of those still routes back to us.
38const CF_TEXT: u32 = 1;
39const CF_OEMTEXT: u32 = 7;
40
41struct State {
42    /// Text to hand over when a consumer asks. Cleared once ownership is lost.
43    pending: Option<String>,
44    /// Set when a consumer actually requested the data.
45    rendered: bool,
46    /// How many times the data has been requested since publishing.
47    ///
48    /// Consumers are not guaranteed to read exactly once. Chromium in particular touches the
49    /// clipboard more than once per paste, so the first render is not proof the paste completed.
50    render_count: u32,
51    /// Tick of the most recent render, for debouncing the restore.
52    last_render: Option<std::time::Instant>,
53    /// Render count observed when the paste was triggered. Reads at or below this were caused by
54    /// something other than the paste (a clipboard manager, history service) and prove nothing.
55    baseline: u32,
56}
57
58fn state() -> &'static (Mutex<State>, Condvar) {
59    static S: OnceLock<(Mutex<State>, Condvar)> = OnceLock::new();
60    S.get_or_init(|| {
61        (
62            Mutex::new(State {
63                pending: None,
64                rendered: false,
65                render_count: 0,
66                last_render: None,
67                baseline: 0,
68            }),
69            Condvar::new(),
70        )
71    })
72}
73
74static OWNER_HWND: AtomicIsize = AtomicIsize::new(0);
75static THREAD_STARTED: AtomicBool = AtomicBool::new(false);
76
77unsafe extern "system" fn wndproc(hwnd: HWND, msg: u32, wp: WPARAM, lp: LPARAM) -> LRESULT {
78    match msg {
79        // A consumer asked for one specific format. The clipboard is already open by the requester,
80        // so this must call SetClipboardData without opening it.
81        WM_RENDERFORMAT => {
82            render(wp.0 as u32);
83            LRESULT(0)
84        }
85        // We are losing ownership or shutting down; supply everything we promised.
86        WM_RENDERALLFORMATS => {
87            if unsafe { OpenClipboard(Some(hwnd)) }.is_ok() {
88                render(CF_UNICODETEXT_PUBLIC);
89                let _ = unsafe { CloseClipboard() };
90            }
91            LRESULT(0)
92        }
93        // Another process took the clipboard. Our promise is void.
94        WM_DESTROYCLIPBOARD => {
95            let (lock, _) = state();
96            if let Ok(mut s) = lock.lock() {
97                s.pending = None;
98            }
99            LRESULT(0)
100        }
101        _ => unsafe { DefWindowProcW(hwnd, msg, wp, lp) },
102    }
103}
104
105/// Supply the promised text for `format`, and record that a real read occurred.
106fn render(format: u32) {
107    if !matches!(format, CF_UNICODETEXT_PUBLIC | CF_TEXT | CF_OEMTEXT) {
108        return;
109    }
110    let (lock, cvar) = state();
111    let Ok(mut s) = lock.lock() else { return };
112    let Some(text) = s.pending.clone() else {
113        return;
114    };
115
116    if let Ok(handle) = alloc_global_public(&utf16_bytes_public(&text)) {
117        // Deliberately always CF_UNICODETEXT: Windows synthesizes the narrow formats from it, so
118        // one render satisfies a consumer that asked for any of them.
119        let _ = unsafe { SetClipboardData(CF_UNICODETEXT_PUBLIC, Some(HANDLE(handle.0))) };
120    }
121
122    s.rendered = true;
123    s.render_count += 1;
124    s.last_render = Some(std::time::Instant::now());
125    cvar.notify_all();
126}
127
128/// Start the owner window and its message pump. Idempotent.
129fn ensure_owner() -> Result<HWND, Error> {
130    if let 0 = OWNER_HWND.load(Ordering::SeqCst) {
131    } else {
132        return Ok(HWND(OWNER_HWND.load(Ordering::SeqCst) as *mut _));
133    }
134
135    if THREAD_STARTED.swap(true, Ordering::SeqCst) {
136        // Another caller is mid-startup; wait for the handle to appear.
137        for _ in 0..200 {
138            let h = OWNER_HWND.load(Ordering::SeqCst);
139            if h != 0 {
140                return Ok(HWND(h as *mut _));
141            }
142            std::thread::sleep(Duration::from_millis(5));
143        }
144        return Err(Error::OwnerWindowFailed);
145    }
146
147    std::thread::Builder::new()
148        .name("win-text-inject-clipboard-owner".into())
149        .spawn(|| unsafe {
150            let instance = match GetModuleHandleW(None) {
151                Ok(i) => i,
152                Err(_) => return,
153            };
154            let class = w!("WinTextInjectClipboardOwner");
155            let wc = WNDCLASSW {
156                lpfnWndProc: Some(wndproc),
157                hInstance: instance.into(),
158                lpszClassName: class,
159                ..Default::default()
160            };
161            RegisterClassW(&wc);
162
163            // Never shown. Not HWND_MESSAGE: message-only windows are not reliable clipboard
164            // owners, and clipboard owner messages are sent directly to the window anyway.
165            let hwnd = match CreateWindowExW(
166                Default::default(),
167                class,
168                w!("win-text-inject clipboard owner"),
169                WS_OVERLAPPED,
170                CW_USEDEFAULT,
171                CW_USEDEFAULT,
172                0,
173                0,
174                None,
175                None,
176                Some(instance.into()),
177                None,
178            ) {
179                Ok(h) => h,
180                Err(_) => return,
181            };
182
183            OWNER_HWND.store(hwnd.0 as isize, Ordering::SeqCst);
184
185            let mut msg = MSG::default();
186            while GetMessageW(&mut msg, None, 0, 0).as_bool() {
187                let _ = TranslateMessage(&msg);
188                DispatchMessageW(&msg);
189            }
190        })
191        .map_err(|_| Error::OwnerWindowFailed)?;
192
193    for _ in 0..200 {
194        let h = OWNER_HWND.load(Ordering::SeqCst);
195        if h != 0 {
196            return Ok(HWND(h as *mut _));
197        }
198        std::thread::sleep(Duration::from_millis(5));
199    }
200    Err(Error::OwnerWindowFailed)
201}
202
203/// A promise of text placed on the clipboard, not yet materialized.
204pub struct Offer;
205
206impl Offer {
207    /// Advertise `text` on the clipboard without publishing it.
208    ///
209    /// Nothing is copied until a consumer asks, at which point [`Offer::wait_for_read`] returns.
210    pub fn publish(text: &str) -> Result<Self, Error> {
211        let hwnd = ensure_owner()?;
212
213        {
214            let (lock, _) = state();
215            let mut s = lock.lock().map_err(|_| Error::OwnerWindowFailed)?;
216            s.pending = Some(text.to_owned());
217            s.rendered = false;
218            s.render_count = 0;
219            s.last_render = None;
220            s.baseline = 0;
221        }
222
223        {
224            // Retrying open is essential here: a contended clipboard otherwise fails outright with
225            // ERROR_ACCESS_DENIED, which in a dictation app means a dropped transcript.
226            let _guard = crate::clipboard::ClipboardGuard::open_owned_by(hwnd)?;
227            let result = unsafe {
228                (|| {
229                    EmptyClipboard().map_err(Error::Clipboard)?;
230                    // NULL data is the promise; Windows comes back with WM_RENDERFORMAT.
231                    //
232                    // The return value cannot detect failure here. For delayed rendering
233                    // SetClipboardData returns NULL on *success*, which the bindings surface as an
234                    // Err, and Win32 does not reset the thread's last-error on success -- so a
235                    // stale code from an unrelated earlier call reads as a failure that never
236                    // happened. Observed in the wild as ERROR_NOT_FOUND (0x80070490) on roughly one
237                    // paste in four. Clipboard ownership is verified below instead, which is the
238                    // actual post-condition.
239                    let _ = SetClipboardData(CF_UNICODETEXT_PUBLIC, None);
240                    crate::clipboard::attach_privacy_formats();
241                    Ok::<(), Error>(())
242                })()
243            };
244            result?;
245
246            // Ownership plus format availability is the real confirmation the promise was
247            // accepted. Both are observable facts about the clipboard rather than an error code
248            // that may be stale.
249            let owned = unsafe { GetClipboardOwner() }.unwrap_or_default() == hwnd;
250            let advertised = unsafe { IsClipboardFormatAvailable(CF_UNICODETEXT_PUBLIC) }.is_ok();
251            if !owned || !advertised {
252                return Err(Error::OwnerWindowFailed);
253            }
254        }
255
256        Ok(Self)
257    }
258
259    /// Block until a consumer actually read the clipboard, or `timeout` elapses.
260    ///
261    /// Returns `true` if the data was read. A `false` return means the paste never reached the
262    /// target, which is itself useful: the caller can report that instead of silently assuming
263    /// success.
264    pub fn wait_for_read(&self, timeout: Duration) -> bool {
265        let (lock, cvar) = state();
266        let Ok(guard) = lock.lock() else { return false };
267        let Ok((guard, _)) = cvar.wait_timeout_while(guard, timeout, |s| !s.rendered) else {
268            return false;
269        };
270        guard.rendered
271    }
272
273    /// Wait for the first read, then until reads have been quiet for `quiet`.
274    ///
275    /// A single `WM_RENDERFORMAT` is *not* proof the paste completed. Chromium touches the
276    /// clipboard more than once per paste — an early probe, then the real read — so restoring after
277    /// the first render puts the old text back before the read that matters, which is precisely the
278    /// bug this was meant to fix. Waiting for renders to go quiet covers multi-read consumers.
279    ///
280    /// Returns the number of reads observed, or `None` if none arrived within `timeout`.
281    pub fn wait_for_reads_to_settle(&self, timeout: Duration, quiet: Duration) -> Option<u32> {
282        if !self.wait_for_read(timeout) {
283            return None;
284        }
285        loop {
286            let last = {
287                let (lock, _) = state();
288                let s = lock.lock().ok()?;
289                s.last_render?
290            };
291            let elapsed = last.elapsed();
292            if elapsed >= quiet {
293                break;
294            }
295            std::thread::sleep(quiet - elapsed);
296        }
297        let (lock, _) = state();
298        let s = lock.lock().ok()?;
299        Some(s.render_count)
300    }
301
302    /// Number of times a consumer has asked for the data since publishing.
303    pub fn read_count(&self) -> u32 {
304        state().0.lock().map(|s| s.render_count).unwrap_or(0)
305    }
306
307    /// Record that the paste has now been triggered.
308    ///
309    /// Anything that reads the clipboard — a clipboard manager, a history service — satisfies the
310    /// render, so a read observed *before* the paste says nothing about the target. Marking here
311    /// lets [`Offer::wait_for_target_read`] ignore those and wait for a read caused by the paste.
312    pub fn mark_paste_sent(&self) {
313        if let Ok(mut s) = state().0.lock() {
314            s.baseline = s.render_count;
315        }
316    }
317
318    /// Whether the promise was already materialized before the paste was sent.
319    ///
320    /// Once *any* consumer forces the render, the clipboard holds real data and Windows sends no
321    /// further `WM_RENDERFORMAT`. The target's read is then unobservable — not delayed, gone. A
322    /// clipboard manager that archives every change causes exactly this, so callers must have a
323    /// fallback rather than waiting for a signal that can never arrive.
324    pub fn consumed_before_paste(&self) -> bool {
325        state().0.lock().map(|s| s.baseline > 0).unwrap_or(false)
326    }
327
328    /// Wait for a read that happened *after* [`Offer::mark_paste_sent`], then for reads to settle.
329    ///
330    /// Cannot be satisfied by a clipboard manager that read the promise the moment it was
331    /// published. Returns `None` if no such read arrives, which includes the case where the promise
332    /// was already consumed — check [`Offer::consumed_before_paste`] to tell those apart.
333    pub fn wait_for_target_read(&self, timeout: Duration, quiet: Duration) -> Option<u32> {
334        let (lock, cvar) = state();
335        {
336            let guard = lock.lock().ok()?;
337            let (guard, timed_out) = cvar
338                .wait_timeout_while(guard, timeout, |s| s.render_count <= s.baseline)
339                .ok()?;
340            if timed_out.timed_out() && guard.render_count <= guard.baseline {
341                return None;
342            }
343        }
344        loop {
345            let last = { lock.lock().ok()?.last_render? };
346            let elapsed = last.elapsed();
347            if elapsed >= quiet {
348                break;
349            }
350            std::thread::sleep(quiet - elapsed);
351        }
352        let s = lock.lock().ok()?;
353        Some(s.render_count - s.baseline)
354    }
355}
356
357#[cfg(test)]
358mod tests {
359    use super::*;
360
361    #[test]
362    fn owner_window_starts_and_is_reused() {
363        let a = ensure_owner().expect("owner window");
364        let b = ensure_owner().expect("owner window again");
365        assert_eq!(a.0, b.0);
366        assert!(!a.0.is_null());
367    }
368
369    #[test]
370    fn non_text_formats_are_not_rendered() {
371        // CF_BITMAP (2) is not something we promise; asking for it must not mark a read.
372        let (lock, _) = state();
373        {
374            let mut s = lock.lock().unwrap();
375            s.pending = Some("x".into());
376            s.rendered = false;
377        }
378        render(2);
379        assert!(!lock.lock().unwrap().rendered);
380    }
381}