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}