#![allow(
clippy::cast_possible_wrap,
clippy::cast_sign_loss,
clippy::cast_possible_truncation
)]
use crate::cg::CGRect;
use crate::ffi::{FFIApplicationData, FFIDisplayData, FFIWindowData};
use std::collections::HashMap;
use std::ffi::c_void;
use std::mem::MaybeUninit;
const MAX_DISPLAYS: usize = 64;
const MAX_WINDOWS: usize = 4096;
const MAX_APPS: usize = 1024;
const STRING_POOL_BYTES: usize = 256 * 1024;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DisplaySnapshot {
pub display_id: u32,
pub width: i32,
pub height: i32,
pub frame: CGRect,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ApplicationSnapshot {
pub process_id: i32,
pub bundle_identifier: String,
pub application_name: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WindowSnapshot {
pub window_id: u32,
pub window_layer: i32,
pub is_on_screen: bool,
pub is_active: bool,
pub frame: CGRect,
pub title: Option<String>,
pub owning_app_index: Option<usize>,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct SnapshotTruncation {
pub displays: bool,
pub windows: bool,
pub applications: bool,
}
impl SnapshotTruncation {
#[must_use]
pub const fn any(self) -> bool {
self.displays || self.windows || self.applications
}
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct ContentSnapshot {
pub displays: Vec<DisplaySnapshot>,
pub applications: Vec<ApplicationSnapshot>,
pub windows: Vec<WindowSnapshot>,
pub truncation: SnapshotTruncation,
pub string_pool_used: usize,
}
impl ContentSnapshot {
#[must_use]
pub const fn string_pool_capacity() -> usize {
STRING_POOL_BYTES
}
pub(crate) fn collect(content: *const c_void) -> Option<Self> {
if content.is_null() {
return None;
}
let mut pool = StringPool::new();
let (displays, displays_truncated) = unsafe { collect_displays(content) };
let (applications, apps_truncated, apps_pool_used) =
unsafe { collect_applications(content, &mut pool) };
let (windows, windows_truncated, windows_pool_used) =
unsafe { collect_windows(content, &applications, &mut pool) };
Some(Self {
displays,
applications,
windows,
truncation: SnapshotTruncation {
displays: displays_truncated,
windows: windows_truncated,
applications: apps_truncated,
},
string_pool_used: apps_pool_used.max(windows_pool_used),
})
}
}
struct StringPool {
buf: Vec<MaybeUninit<u8>>,
}
impl StringPool {
fn new() -> Self {
Self {
buf: Vec::with_capacity(STRING_POOL_BYTES),
}
}
fn as_mut_ptr(&mut self) -> *mut i8 {
self.buf.as_mut_ptr().cast::<i8>()
}
unsafe fn initialised(&self, used: usize) -> &[u8] {
let used = used.min(STRING_POOL_BYTES);
unsafe { std::slice::from_raw_parts(self.buf.as_ptr().cast::<u8>(), used) }
}
}
unsafe fn packed_at<T: Copy>(base: *const MaybeUninit<T>, i: usize) -> T {
unsafe { base.add(i).read().assume_init() }
}
unsafe fn collect_displays(content: *const c_void) -> (Vec<DisplaySnapshot>, bool) {
unsafe {
let mut buffer: Vec<MaybeUninit<FFIDisplayData>> = Vec::with_capacity(MAX_DISPLAYS);
let written = crate::ffi::sc_shareable_content_get_displays_batch(
content,
buffer.as_mut_ptr().cast::<c_void>(),
MAX_DISPLAYS as isize,
);
if written <= 0 {
return (Vec::new(), false);
}
let count = (written as usize).min(MAX_DISPLAYS);
let displays = (0..count)
.map(|i| {
let d = packed_at(buffer.as_ptr(), i);
DisplaySnapshot {
display_id: d.display_id,
width: d.width,
height: d.height,
frame: CGRect::new(d.frame.x, d.frame.y, d.frame.width, d.frame.height),
}
})
.collect();
(displays, count == MAX_DISPLAYS)
}
}
unsafe fn collect_applications(
content: *const c_void,
pool: &mut StringPool,
) -> (Vec<ApplicationSnapshot>, bool, usize) {
unsafe {
let mut packed: Vec<MaybeUninit<FFIApplicationData>> = Vec::with_capacity(MAX_APPS);
let mut strings_used: isize = 0;
let written = crate::ffi::sc_shareable_content_get_applications_batch(
content,
packed.as_mut_ptr().cast::<c_void>(),
MAX_APPS as isize,
pool.as_mut_ptr(),
STRING_POOL_BYTES as isize,
&mut strings_used,
);
if written <= 0 {
return (Vec::new(), false, 0);
}
let count = (written as usize).min(MAX_APPS);
let used = (strings_used.max(0) as usize).min(STRING_POOL_BYTES);
let bytes = pool.initialised(used);
let apps = (0..count)
.map(|i| {
let app = packed_at(packed.as_ptr(), i);
ApplicationSnapshot {
process_id: app.process_id,
bundle_identifier: read_string(
bytes,
app.bundle_id_offset,
app.bundle_id_length,
),
application_name: read_string(bytes, app.app_name_offset, app.app_name_length),
}
})
.collect();
(apps, count == MAX_APPS, used)
}
}
unsafe fn collect_windows(
content: *const c_void,
applications: &[ApplicationSnapshot],
pool: &mut StringPool,
) -> (Vec<WindowSnapshot>, bool, usize) {
unsafe {
let mut packed: Vec<MaybeUninit<FFIWindowData>> = Vec::with_capacity(MAX_WINDOWS);
let mut strings_used: isize = 0;
let written = crate::ffi::sc_shareable_content_get_windows_batch(
content,
packed.as_mut_ptr().cast::<c_void>(),
MAX_WINDOWS as isize,
pool.as_mut_ptr(),
STRING_POOL_BYTES as isize,
&mut strings_used,
);
if written <= 0 {
return (Vec::new(), false, 0);
}
let count = (written as usize).min(MAX_WINDOWS);
let used = (strings_used.max(0) as usize).min(STRING_POOL_BYTES);
let bytes = pool.initialised(used);
let app_indices: HashMap<i32, usize> = applications
.iter()
.enumerate()
.map(|(index, app)| (app.process_id, index))
.collect();
let windows = (0..count)
.map(|i| {
let w = packed_at(packed.as_ptr(), i);
let title = if w.title_length == 0 {
None
} else {
let s = read_string(bytes, w.title_offset, w.title_length);
if s.is_empty() {
None
} else {
Some(s)
}
};
let owning_app_index = app_indices.get(&w.owning_app_process_id).copied();
WindowSnapshot {
window_id: w.window_id,
window_layer: w.window_layer,
is_on_screen: w.is_on_screen,
is_active: w.is_active,
frame: CGRect::new(w.frame.x, w.frame.y, w.frame.width, w.frame.height),
title,
owning_app_index,
}
})
.collect();
(windows, count == MAX_WINDOWS, used)
}
}
fn read_string(pool: &[u8], offset: u32, length: u32) -> String {
read_str(pool, offset, length).map_or_else(String::new, str::to_owned)
}
fn read_str(pool: &[u8], offset: u32, length: u32) -> Option<&str> {
let start = offset as usize;
let end = start.checked_add(length as usize)?;
let bytes = pool.get(start..end)?;
std::str::from_utf8(bytes).ok()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ffi::FFIRect;
#[test]
fn packed_at_reads_bridge_written_entries_from_a_len_zero_vec() {
let mut buffer: Vec<MaybeUninit<FFIDisplayData>> = Vec::with_capacity(MAX_DISPLAYS);
assert_eq!(buffer.len(), 0, "with_capacity must not set len");
let written = [
FFIDisplayData {
display_id: 7,
width: 1920,
height: 1080,
frame: FFIRect {
x: 0.0,
y: 0.0,
width: 1920.0,
height: 1080.0,
},
},
FFIDisplayData {
display_id: 9,
width: 800,
height: 600,
frame: FFIRect {
x: 1.0,
y: 2.0,
width: 800.0,
height: 600.0,
},
},
];
unsafe {
std::ptr::copy_nonoverlapping(
written.as_ptr(),
buffer.as_mut_ptr().cast::<FFIDisplayData>(),
written.len(),
);
}
let read: Vec<u32> = (0..written.len())
.map(|i| unsafe { packed_at(buffer.as_ptr(), i) }.display_id)
.collect();
assert_eq!(read, vec![7, 9]);
}
#[test]
fn string_pool_reads_only_the_written_prefix() {
let mut pool = StringPool::new();
let src = b"hello world";
unsafe {
std::ptr::copy_nonoverlapping(src.as_ptr(), pool.as_mut_ptr().cast::<u8>(), src.len());
}
let bytes = unsafe { pool.initialised(src.len()) };
assert_eq!(bytes, src);
assert_eq!(read_str(bytes, 0, 5), Some("hello"));
assert_eq!(read_str(bytes, 6, 5), Some("world"));
assert_eq!(read_str(bytes, 6, 99), None);
assert_eq!(read_str(bytes, u32::MAX, 1), None);
}
#[test]
fn truncation_any_reflects_individual_flags() {
assert!(!SnapshotTruncation::default().any());
assert!(SnapshotTruncation {
windows: true,
..Default::default()
}
.any());
assert!(SnapshotTruncation {
displays: true,
..Default::default()
}
.any());
assert!(SnapshotTruncation {
applications: true,
..Default::default()
}
.any());
}
#[test]
fn collect_rejects_null_content() {
assert!(ContentSnapshot::collect(std::ptr::null()).is_none());
}
}