1//! Real Wayland socket/FD transport against a private compositor fixture, no desktop access.
2use crate::{
3 capture::capture_on,
4 protocols::{
5 export::server::{
6 hyprland_toplevel_export_frame_v1::{
7 self as frame, HyprlandToplevelExportFrameV1 as Frame,
8 },
9 hyprland_toplevel_export_manager_v1::{
10 self as export, HyprlandToplevelExportManagerV1 as Export,
11 },
12 },
13 mapping::server::{
14 hyprland_toplevel_mapping_manager_v1::{
15 self as mapping, HyprlandToplevelMappingManagerV1 as Mapping,
16 },
17 hyprland_toplevel_window_mapping_handle_v1::{
18 self as handle, HyprlandToplevelWindowMappingHandleV1 as Handle,
19 },
20 },
21 },
22};
23use std::{
24 fs::File,
25 io::{BufReader, Write},
26 os::unix::{fs::FileExt, net::UnixStream},
27 sync::{Arc, Mutex},
28 thread,
29};
30use wayland_protocols_wlr::foreign_toplevel::v1::server::{
31 zwlr_foreign_toplevel_handle_v1::{self as toplevel, ZwlrForeignToplevelHandleV1 as Toplevel},
32 zwlr_foreign_toplevel_manager_v1::{self as manager, ZwlrForeignToplevelManagerV1 as Manager},
33};
34use wayland_server::{
35 Client, DataInit, Dispatch, Display, DisplayHandle, GlobalDispatch, New, Resource,
36 backend::{ClientData, ClientId, DisconnectReason},
37 protocol::{
38 wl_buffer::{self, WlBuffer},
39 wl_shm::{self, WlShm},
40 wl_shm_pool::{self, WlShmPool},
41 },
42};
43
44const TARGET: u64 = 0x12345678abcdef01;
45// Same low 32 bits: a truncated-address capture would pick the wrong window.
46const OTHER: u64 = 0xfedcba98abcdef01;
47#[derive(Clone, Copy)]
48enum Mode {
49 Success,
50 Denied,
51 BadBuffer,
52}
53struct State {
54 mode: Mode,
55 captures: Arc<Mutex<Vec<u64>>>,
56}
57#[derive(Debug)]
58struct ClientState;
59impl ClientData for ClientState {
60 fn initialized(&self, _: ClientId) {}
61 fn disconnected(&self, _: ClientId, _: DisconnectReason) {}
62}
63struct Fixture {
64 stop: UnixStream,
65 thread: Option<thread::JoinHandle<()>>,
66 captures: Arc<Mutex<Vec<u64>>>,
67}
68impl Fixture {
69 fn start(mode: Mode) -> (Self, wayland_client::Connection) {
70 let mut display = Display::<State>::new().unwrap();
71 let handle = display.handle();
72 handle.create_global::<State, WlShm, _>(1, ());
73 handle.create_global::<State, Mapping, _>(1, ());
74 handle.create_global::<State, Export, _>(2, ());
75 handle.create_global::<State, Manager, _>(3, ());
76 let (client, server) = UnixStream::pair().unwrap();
77 display
78 .handle()
79 .insert_client(server, Arc::new(ClientState))
80 .unwrap();
81 let (stop, wake) = UnixStream::pair().unwrap();
82 let captures = Arc::new(Mutex::new(Vec::new()));
83 let mut state = State {
84 mode,
85 captures: captures.clone(),
86 };
87 let thread = thread::spawn(move || {
88 loop {
89 display.dispatch_clients(&mut state).unwrap();
90 display.flush_clients().unwrap();
91 let mut fds = [
92 rustix::event::PollFd::new(&display, rustix::event::PollFlags::IN),
93 rustix::event::PollFd::new(&wake, rustix::event::PollFlags::IN),
94 ];
95 rustix::event::poll(&mut fds, None).unwrap();
96 if !fds[1].revents().is_empty() {
97 break;
98 }
99 }
100 });
101 (
102 Self {
103 stop,
104 thread: Some(thread),
105 captures,
106 },
107 wayland_client::Connection::from_socket(client).unwrap(),
108 )
109 }
110}
111impl Drop for Fixture {
112 fn drop(&mut self) {
113 let _ = self.stop.write_all(&[1]);
114 if let Some(thread) = self.thread.take() {
115 thread.join().unwrap();
116 }
117 }
118}
119
120macro_rules! global {
121 ($ty:ty) => {
122 impl GlobalDispatch<$ty, ()> for State {
123 fn bind(
124 _: &mut Self,
125 _: &DisplayHandle,
126 _: &Client,
127 resource: New<$ty>,
128 _: &(),
129 init: &mut DataInit<'_, Self>,
130 ) {
131 init.init(resource, ());
132 }
133 }
134 };
135}
136global!(Export);
137global!(Mapping);
138impl GlobalDispatch<WlShm, ()> for State {
139 fn bind(
140 _: &mut Self,
141 _: &DisplayHandle,
142 _: &Client,
143 resource: New<WlShm>,
144 _: &(),
145 init: &mut DataInit<'_, Self>,
146 ) {
147 init.init(resource, ()).format(wl_shm::Format::Argb8888);
148 }
149}
150impl GlobalDispatch<Manager, ()> for State {
151 fn bind(
152 _: &mut Self,
153 dh: &DisplayHandle,
154 client: &Client,
155 resource: New<Manager>,
156 _: &(),
157 init: &mut DataInit<'_, Self>,
158 ) {
159 let manager = init.init(resource, ());
160 for address in [OTHER, TARGET] {
161 let window = client
162 .create_resource::<Toplevel, _, Self>(dh, 3, address)
163 .unwrap();
164 manager.toplevel(&window);
165 window.title("same title".into());
166 window.app_id("same-app".into());
167 window.done();
168 }
169 }
170}
171impl Dispatch<Manager, ()> for State {
172 fn request(
173 _: &mut Self,
174 _: &Client,
175 _: &Manager,
176 _: manager::Request,
177 _: &(),
178 _: &DisplayHandle,
179 _: &mut DataInit<'_, Self>,
180 ) {
181 }
182}
183impl Dispatch<Toplevel, u64> for State {
184 fn request(
185 _: &mut Self,
186 _: &Client,
187 _: &Toplevel,
188 _: toplevel::Request,
189 _: &u64,
190 _: &DisplayHandle,
191 _: &mut DataInit<'_, Self>,
192 ) {
193 }
194}
195impl Dispatch<Handle, ()> for State {
196 fn request(
197 _: &mut Self,
198 _: &Client,
199 _: &Handle,
200 _: handle::Request,
201 _: &(),
202 _: &DisplayHandle,
203 _: &mut DataInit<'_, Self>,
204 ) {
205 }
206}
207impl Dispatch<Mapping, ()> for State {
208 fn request(
209 _: &mut Self,
210 _: &Client,
211 _: &Mapping,
212 request: mapping::Request,
213 _: &(),
214 _: &DisplayHandle,
215 init: &mut DataInit<'_, Self>,
216 ) {
217 if let mapping::Request::GetWindowForToplevelWlr { handle, toplevel } = request {
218 let address = *toplevel.data::<u64>().unwrap();
219 init.init(handle, ())
220 .window_address((address >> 32) as u32, address as u32);
221 }
222 }
223}
224impl Dispatch<Export, ()> for State {
225 fn request(
226 state: &mut Self,
227 _: &Client,
228 _: &Export,
229 request: export::Request,
230 _: &(),
231 _: &DisplayHandle,
232 init: &mut DataInit<'_, Self>,
233 ) {
234 match request {
235 export::Request::CaptureToplevelWithWlrToplevelHandle {
236 frame,
237 overlay_cursor,
238 handle,
239 } => {
240 assert_eq!(overlay_cursor, 0);
241 state
242 .captures
243 .lock()
244 .unwrap()
245 .push(*handle.data::<u64>().unwrap());
246 let frame = init.init(frame, ());
247 match state.mode {
248 Mode::Denied => frame.failed(),
249 Mode::BadBuffer => {
250 frame.buffer(wl_shm::Format::Argb8888, 16384, 16384, 65536);
251 frame.buffer_done();
252 }
253 Mode::Success => {
254 frame.buffer(wl_shm::Format::Argb8888, 2, 1, 12);
255 frame.buffer_done();
256 }
257 }
258 }
259 export::Request::CaptureToplevel { .. } => {
260 panic!("Do not use truncated window addresses")
261 }
262 _ => {}
263 }
264 }
265}
266impl Dispatch<WlShm, ()> for State {
267 fn request(
268 _: &mut Self,
269 _: &Client,
270 _: &WlShm,
271 request: wl_shm::Request,
272 _: &(),
273 _: &DisplayHandle,
274 init: &mut DataInit<'_, Self>,
275 ) {
276 if let wl_shm::Request::CreatePool { id, fd, .. } = request {
277 init.init(id, Arc::new(File::from(fd)));
278 }
279 }
280}
281struct BufferData {
282 file: Arc<File>,
283 offset: u64,
284}
285impl Dispatch<WlShmPool, Arc<File>> for State {
286 fn request(
287 _: &mut Self,
288 _: &Client,
289 _: &WlShmPool,
290 request: wl_shm_pool::Request,
291 file: &Arc<File>,
292 _: &DisplayHandle,
293 init: &mut DataInit<'_, Self>,
294 ) {
295 if let wl_shm_pool::Request::CreateBuffer {
296 id,
297 offset,
298 width,
299 height,
300 stride,
301 ..
302 } = request
303 {
304 assert_eq!((width, height, stride), (2, 1, 12));
305 init.init(
306 id,
307 BufferData {
308 file: file.clone(),
309 offset: offset as u64,
310 },
311 );
312 }
313 }
314}
315impl Dispatch<WlBuffer, BufferData> for State {
316 fn request(
317 _: &mut Self,
318 _: &Client,
319 _: &WlBuffer,
320 _: wl_buffer::Request,
321 _: &BufferData,
322 _: &DisplayHandle,
323 _: &mut DataInit<'_, Self>,
324 ) {
325 }
326}
327impl Dispatch<Frame, ()> for State {
328 fn request(
329 _: &mut Self,
330 _: &Client,
331 resource: &Frame,
332 request: frame::Request,
333 _: &(),
334 _: &DisplayHandle,
335 _: &mut DataInit<'_, Self>,
336 ) {
337 if let frame::Request::Copy {
338 buffer,
339 ignore_damage,
340 } = request
341 {
342 assert_eq!(ignore_damage, 1, "Static windows must not wait for damage");
343 let data = buffer.data::<BufferData>().unwrap();
344 data.file
345 .write_all_at(&[30, 20, 10, 255, 60, 50, 40, 255, 0, 0, 0, 0], data.offset)
346 .unwrap();
347 resource.flags(frame::Flags::empty());
348 resource.ready(0, 1, 0);
349 }
350 }
351}
352
353#[test]
354fn exports_exact_window_over_real_fd_transport() {
355 let (fixture, connection) = Fixture::start(Mode::Success);
356 let directory = tempfile::tempdir().unwrap();
357 capture_on(connection, TARGET, directory.path()).unwrap();
358 assert_eq!(*fixture.captures.lock().unwrap(), [TARGET]);
359 let mut reader = png::Decoder::new(BufReader::new(
360 File::open(directory.path().join("capture.png")).unwrap(),
361 ))
362 .read_info()
363 .unwrap();
364 let mut pixels = vec![0; reader.output_buffer_size().unwrap()];
365 reader.next_frame(&mut pixels).unwrap();
366 assert_eq!(pixels, [10, 20, 30, 255, 40, 50, 60, 255]);
367}
368#[test]
369fn failed_export_produces_no_image_and_no_second_capture() {
370 let (fixture, connection) = Fixture::start(Mode::Denied);
371 let directory = tempfile::tempdir().unwrap();
372 assert!(
373 capture_on(connection, TARGET, directory.path())
374 .unwrap_err()
375 .to_string()
376 .contains("permission")
377 );
378 assert_eq!(*fixture.captures.lock().unwrap(), [TARGET]);
379 assert!(!directory.path().join("capture.png").exists());
380}
381#[test]
382fn rejects_oversized_buffers_before_allocating_shared_memory() {
383 let (_fixture, connection) = Fixture::start(Mode::BadBuffer);
384 let directory = tempfile::tempdir().unwrap();
385 assert!(
386 capture_on(connection, TARGET, directory.path())
387 .unwrap_err()
388 .to_string()
389 .contains("too large")
390 );
391 assert!(!directory.path().join("capture.png").exists());
392}