screencapturekit/utils/ffi_string.rs
1//! FFI string utilities
2//!
3//! Helper functions for retrieving strings from C/Objective-C APIs
4//! that use buffer-based string retrieval patterns.
5
6use std::ffi::CStr;
7
8/// Default buffer size for FFI string retrieval
9pub const DEFAULT_BUFFER_SIZE: usize = 1024;
10
11/// Smaller buffer size for short strings (e.g., device IDs, stream names)
12pub const SMALL_BUFFER_SIZE: usize = 256;
13
14/// Retrieves a string from an FFI function that writes to a buffer.
15///
16/// This is a common pattern in Objective-C FFI where a function:
17/// 1. Takes a buffer pointer and length
18/// 2. Writes a null-terminated string to the buffer
19/// 3. Returns a boolean indicating success
20///
21/// # Arguments
22/// * `buffer_size` - Size of the buffer to allocate
23/// * `ffi_call` - A closure that takes (`buffer_ptr`, `buffer_len`) and returns success bool
24///
25/// # Returns
26/// * `Some(String)` if the FFI call succeeded and the string was valid UTF-8
27/// * `None` if the FFI call failed or returned an empty string
28///
29/// # Safety
30/// The caller must ensure the `ffi_call` closure properly writes a null-terminated
31/// string to the provided buffer and does not write beyond the buffer length.
32///
33/// # Example
34/// ```
35/// use screencapturekit::utils::ffi_string::ffi_string_from_buffer;
36///
37/// let result = unsafe {
38/// ffi_string_from_buffer(64, |buf, len| {
39/// // Simulate FFI call that writes "hello" to buffer
40/// let src = b"hello\0";
41/// if len >= src.len() as isize {
42/// std::ptr::copy_nonoverlapping(src.as_ptr(), buf as *mut u8, src.len());
43/// true
44/// } else {
45/// false
46/// }
47/// })
48/// };
49/// assert_eq!(result, Some("hello".to_string()));
50/// ```
51#[allow(clippy::cast_possible_wrap)]
52pub unsafe fn ffi_string_from_buffer<F>(buffer_size: usize, ffi_call: F) -> Option<String>
53where
54 F: FnOnce(*mut i8, isize) -> bool,
55{
56 let mut buffer = vec![0i8; buffer_size];
57 let success = ffi_call(buffer.as_mut_ptr(), buffer.len() as isize);
58 if success {
59 let c_str = CStr::from_ptr(buffer.as_ptr());
60 let s = c_str.to_string_lossy().to_string();
61 if s.is_empty() {
62 None
63 } else {
64 Some(s)
65 }
66 } else {
67 None
68 }
69}
70
71/// Same as [`ffi_string_from_buffer`] but returns an empty string on failure
72/// instead of `None`.
73///
74/// Useful when the API should always return a string, even if empty.
75///
76/// # Safety
77/// The caller must ensure that the FFI call writes valid UTF-8 data to the buffer.
78#[allow(clippy::cast_possible_wrap)]
79pub unsafe fn ffi_string_from_buffer_or_empty<F>(buffer_size: usize, ffi_call: F) -> String
80where
81 F: FnOnce(*mut i8, isize) -> bool,
82{
83 ffi_string_from_buffer(buffer_size, ffi_call).unwrap_or_default()
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89
90 #[test]
91 fn test_ffi_string_from_buffer_success() {
92 let result = unsafe {
93 ffi_string_from_buffer(64, |buf, _len| {
94 let test_str = b"hello\0";
95 std::ptr::copy_nonoverlapping(test_str.as_ptr(), buf.cast::<u8>(), test_str.len());
96 true
97 })
98 };
99 assert_eq!(result, Some("hello".to_string()));
100 }
101
102 #[test]
103 fn test_ffi_string_from_buffer_failure() {
104 let result = unsafe { ffi_string_from_buffer(64, |_buf, _len| false) };
105 assert_eq!(result, None);
106 }
107
108 #[test]
109 fn test_ffi_string_from_buffer_empty() {
110 let result = unsafe {
111 ffi_string_from_buffer(64, |buf, _len| {
112 *buf = 0; // empty string
113 true
114 })
115 };
116 assert_eq!(result, None);
117 }
118
119 #[test]
120 fn test_ffi_string_or_empty() {
121 let result = unsafe { ffi_string_from_buffer_or_empty(64, |_buf, _len| false) };
122 assert_eq!(result, String::new());
123 }
124}