1#![allow(
11 clippy::cast_possible_wrap,
12 clippy::cast_sign_loss,
13 clippy::cast_possible_truncation
14)]
15
16use crate::cg::CGRect;
17use crate::ffi::{FFIApplicationData, FFIDisplayData, FFIWindowData};
18use std::collections::HashMap;
19use std::ffi::c_void;
20use std::mem::MaybeUninit;
21
22const MAX_DISPLAYS: usize = 64;
26const MAX_WINDOWS: usize = 4096;
27const MAX_APPS: usize = 1024;
28const STRING_POOL_BYTES: usize = 256 * 1024;
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub struct DisplaySnapshot {
33 pub display_id: u32,
34 pub width: i32,
35 pub height: i32,
36 pub frame: CGRect,
37}
38
39#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct ApplicationSnapshot {
42 pub process_id: i32,
43 pub bundle_identifier: String,
44 pub application_name: String,
45}
46
47#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct WindowSnapshot {
50 pub window_id: u32,
51 pub window_layer: i32,
52 pub is_on_screen: bool,
53 pub is_active: bool,
54 pub frame: CGRect,
55 pub title: Option<String>,
59 pub owning_app_index: Option<usize>,
63}
64
65#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
73pub struct SnapshotTruncation {
74 pub displays: bool,
76 pub windows: bool,
78 pub applications: bool,
80}
81
82impl SnapshotTruncation {
83 #[must_use]
85 pub const fn any(self) -> bool {
86 self.displays || self.windows || self.applications
87 }
88}
89
90#[derive(Debug, Default, Clone, PartialEq, Eq)]
92pub struct ContentSnapshot {
93 pub displays: Vec<DisplaySnapshot>,
94 pub applications: Vec<ApplicationSnapshot>,
95 pub windows: Vec<WindowSnapshot>,
96 pub truncation: SnapshotTruncation,
98 pub string_pool_used: usize,
107}
108
109impl ContentSnapshot {
110 #[must_use]
112 pub const fn string_pool_capacity() -> usize {
113 STRING_POOL_BYTES
114 }
115
116 pub(crate) fn collect(content: *const c_void) -> Option<Self> {
122 if content.is_null() {
123 return None;
124 }
125
126 let mut pool = StringPool::new();
130
131 let (displays, displays_truncated) = unsafe { collect_displays(content) };
135 let (applications, apps_truncated, apps_pool_used) =
136 unsafe { collect_applications(content, &mut pool) };
137 let (windows, windows_truncated, windows_pool_used) =
138 unsafe { collect_windows(content, &applications, &mut pool) };
139
140 Some(Self {
141 displays,
142 applications,
143 windows,
144 truncation: SnapshotTruncation {
145 displays: displays_truncated,
146 windows: windows_truncated,
147 applications: apps_truncated,
148 },
149 string_pool_used: apps_pool_used.max(windows_pool_used),
150 })
151 }
152}
153
154struct StringPool {
160 buf: Vec<MaybeUninit<u8>>,
161}
162
163impl StringPool {
164 fn new() -> Self {
165 Self {
166 buf: Vec::with_capacity(STRING_POOL_BYTES),
167 }
168 }
169
170 fn as_mut_ptr(&mut self) -> *mut i8 {
171 self.buf.as_mut_ptr().cast::<i8>()
172 }
173
174 unsafe fn initialised(&self, used: usize) -> &[u8] {
181 let used = used.min(STRING_POOL_BYTES);
182 unsafe { std::slice::from_raw_parts(self.buf.as_ptr().cast::<u8>(), used) }
185 }
186}
187
188unsafe fn packed_at<T: Copy>(base: *const MaybeUninit<T>, i: usize) -> T {
201 unsafe { base.add(i).read().assume_init() }
204}
205
206unsafe fn collect_displays(content: *const c_void) -> (Vec<DisplaySnapshot>, bool) {
208 unsafe {
209 let mut buffer: Vec<MaybeUninit<FFIDisplayData>> = Vec::with_capacity(MAX_DISPLAYS);
214 let written = crate::ffi::sc_shareable_content_get_displays_batch(
215 content,
216 buffer.as_mut_ptr().cast::<c_void>(),
217 MAX_DISPLAYS as isize,
218 );
219 if written <= 0 {
220 return (Vec::new(), false);
221 }
222 let count = (written as usize).min(MAX_DISPLAYS);
223
224 let displays = (0..count)
225 .map(|i| {
226 let d = packed_at(buffer.as_ptr(), i);
227 DisplaySnapshot {
228 display_id: d.display_id,
229 width: d.width,
230 height: d.height,
231 frame: CGRect::new(d.frame.x, d.frame.y, d.frame.width, d.frame.height),
232 }
233 })
234 .collect();
235
236 (displays, count == MAX_DISPLAYS)
237 }
238}
239
240unsafe fn collect_applications(
242 content: *const c_void,
243 pool: &mut StringPool,
244) -> (Vec<ApplicationSnapshot>, bool, usize) {
245 unsafe {
246 let mut packed: Vec<MaybeUninit<FFIApplicationData>> = Vec::with_capacity(MAX_APPS);
247 let mut strings_used: isize = 0;
248
249 let written = crate::ffi::sc_shareable_content_get_applications_batch(
250 content,
251 packed.as_mut_ptr().cast::<c_void>(),
252 MAX_APPS as isize,
253 pool.as_mut_ptr(),
254 STRING_POOL_BYTES as isize,
255 &raw mut strings_used,
256 );
257 if written <= 0 {
258 return (Vec::new(), false, 0);
259 }
260 let count = (written as usize).min(MAX_APPS);
261 let used = (strings_used.max(0) as usize).min(STRING_POOL_BYTES);
262 let bytes = pool.initialised(used);
263
264 let apps = (0..count)
265 .map(|i| {
266 let app = packed_at(packed.as_ptr(), i);
267 ApplicationSnapshot {
268 process_id: app.process_id,
269 bundle_identifier: read_string(
270 bytes,
271 app.bundle_id_offset,
272 app.bundle_id_length,
273 ),
274 application_name: read_string(bytes, app.app_name_offset, app.app_name_length),
275 }
276 })
277 .collect();
278
279 (apps, count == MAX_APPS, used)
280 }
281}
282
283unsafe fn collect_windows(
285 content: *const c_void,
286 applications: &[ApplicationSnapshot],
287 pool: &mut StringPool,
288) -> (Vec<WindowSnapshot>, bool, usize) {
289 unsafe {
290 let mut packed: Vec<MaybeUninit<FFIWindowData>> = Vec::with_capacity(MAX_WINDOWS);
291 let mut strings_used: isize = 0;
292
293 let written = crate::ffi::sc_shareable_content_get_windows_batch(
294 content,
295 packed.as_mut_ptr().cast::<c_void>(),
296 MAX_WINDOWS as isize,
297 pool.as_mut_ptr(),
298 STRING_POOL_BYTES as isize,
299 &raw mut strings_used,
300 );
301
302 if written <= 0 {
303 return (Vec::new(), false, 0);
304 }
305 let count = (written as usize).min(MAX_WINDOWS);
306 let used = (strings_used.max(0) as usize).min(STRING_POOL_BYTES);
307 let bytes = pool.initialised(used);
308 let app_indices: HashMap<i32, usize> = applications
309 .iter()
310 .enumerate()
311 .map(|(index, app)| (app.process_id, index))
312 .collect();
313
314 let windows = (0..count)
315 .map(|i| {
316 let w = packed_at(packed.as_ptr(), i);
317 let title = if w.title_length == 0 {
318 None
319 } else {
320 let s = read_string(bytes, w.title_offset, w.title_length);
321 if s.is_empty() {
322 None
323 } else {
324 Some(s)
325 }
326 };
327 let owning_app_index = app_indices.get(&w.owning_app_process_id).copied();
328 WindowSnapshot {
329 window_id: w.window_id,
330 window_layer: w.window_layer,
331 is_on_screen: w.is_on_screen,
332 is_active: w.is_active,
333 frame: CGRect::new(w.frame.x, w.frame.y, w.frame.width, w.frame.height),
334 title,
335 owning_app_index,
336 }
337 })
338 .collect();
339
340 (windows, count == MAX_WINDOWS, used)
341 }
342}
343
344fn read_string(pool: &[u8], offset: u32, length: u32) -> String {
345 read_str(pool, offset, length).map_or_else(String::new, str::to_owned)
346}
347
348fn read_str(pool: &[u8], offset: u32, length: u32) -> Option<&str> {
354 let start = offset as usize;
355 let end = start.checked_add(length as usize)?;
356 let bytes = pool.get(start..end)?;
357 std::str::from_utf8(bytes).ok()
358}
359
360#[cfg(test)]
361mod tests {
362 use super::*;
363 use crate::ffi::FFIRect;
364
365 #[test]
369 fn packed_at_reads_bridge_written_entries_from_a_len_zero_vec() {
370 let mut buffer: Vec<MaybeUninit<FFIDisplayData>> = Vec::with_capacity(MAX_DISPLAYS);
371 assert_eq!(buffer.len(), 0, "with_capacity must not set len");
372
373 let written = [
374 FFIDisplayData {
375 display_id: 7,
376 width: 1920,
377 height: 1080,
378 frame: FFIRect {
379 x: 0.0,
380 y: 0.0,
381 width: 1920.0,
382 height: 1080.0,
383 },
384 },
385 FFIDisplayData {
386 display_id: 9,
387 width: 800,
388 height: 600,
389 frame: FFIRect {
390 x: 1.0,
391 y: 2.0,
392 width: 800.0,
393 height: 600.0,
394 },
395 },
396 ];
397 unsafe {
398 std::ptr::copy_nonoverlapping(
399 written.as_ptr(),
400 buffer.as_mut_ptr().cast::<FFIDisplayData>(),
401 written.len(),
402 );
403 }
404
405 let read: Vec<u32> = (0..written.len())
406 .map(|i| unsafe { packed_at(buffer.as_ptr(), i) }.display_id)
407 .collect();
408 assert_eq!(read, vec![7, 9]);
409 }
410
411 #[test]
412 fn string_pool_reads_only_the_written_prefix() {
413 let mut pool = StringPool::new();
414 let src = b"hello world";
415 unsafe {
416 std::ptr::copy_nonoverlapping(src.as_ptr(), pool.as_mut_ptr().cast::<u8>(), src.len());
417 }
418 let bytes = unsafe { pool.initialised(src.len()) };
419 assert_eq!(bytes, src);
420 assert_eq!(read_str(bytes, 0, 5), Some("hello"));
421 assert_eq!(read_str(bytes, 6, 5), Some("world"));
422 assert_eq!(read_str(bytes, 6, 99), None);
424 assert_eq!(read_str(bytes, u32::MAX, 1), None);
425 }
426
427 #[test]
428 fn truncation_any_reflects_individual_flags() {
429 assert!(!SnapshotTruncation::default().any());
430 assert!(SnapshotTruncation {
431 windows: true,
432 ..Default::default()
433 }
434 .any());
435 assert!(SnapshotTruncation {
436 displays: true,
437 ..Default::default()
438 }
439 .any());
440 assert!(SnapshotTruncation {
441 applications: true,
442 ..Default::default()
443 }
444 .any());
445 }
446
447 #[test]
448 fn collect_rejects_null_content() {
449 assert!(ContentSnapshot::collect(std::ptr::null()).is_none());
450 }
451}