Skip to main content

apple_cf/cf/
primitives.rs

1//! Core Foundation primitive value wrappers.
2//!
3#![allow(clippy::missing_panics_doc)]
4
5//! ```rust
6//! use apple_cf::cf::{CFData, CFDate, CFNumber, CFString, CFUUID};
7//!
8//! let string = CFString::new("hello");
9//! let number = CFNumber::from_i64(42);
10//! let data = CFData::from_bytes([1, 2, 3, 4]);
11//! let uuid = CFUUID::new();
12//!
13//! assert_eq!(string.to_string(), "hello");
14//! assert_eq!(number.to_i64(), Some(42));
15//! assert_eq!(data.to_vec(), vec![1, 2, 3, 4]);
16//! assert_eq!(uuid.bytes().len(), 16);
17//!
18//! let now = CFDate::now();
19//! assert!(now.to_system_time().is_some());
20//! ```
21
22use super::base::{impl_cf_type_wrapper, CFType};
23use crate::ffi;
24use std::ffi::CString;
25use std::fmt;
26use std::time::{Duration, SystemTime, UNIX_EPOCH};
27
28const CF_ABSOLUTE_TIME_INTERVAL_SINCE_1970: f64 = 978_307_200.0;
29
30impl_cf_type_wrapper!(CFString, cf_string_get_type_id);
31impl_cf_type_wrapper!(CFNumber, cf_number_get_type_id);
32impl_cf_type_wrapper!(CFData, cf_data_get_type_id);
33impl_cf_type_wrapper!(CFDate, cf_date_get_type_id);
34impl_cf_type_wrapper!(CFUUID, cf_uuid_get_type_id);
35impl_cf_type_wrapper!(CFError, cf_error_get_type_id);
36
37impl CFString {
38    /// Create a UTF-8 `CFString`.
39    #[must_use]
40    pub fn new(value: &str) -> Self {
41        let ptr = unsafe { ffi::acf_cf_string_create_with_bytes(value.as_ptr(), value.len()) };
42        unsafe { Self::from_raw(ptr) }.expect("CFStringCreateWithBytes returned NULL")
43    }
44
45    /// Number of UTF-16 code units in the string.
46    #[must_use]
47    pub fn len(&self) -> usize {
48        unsafe { ffi::cf_string_get_length(self.as_ptr()) }
49    }
50
51    /// Whether the string is empty.
52    #[must_use]
53    pub fn is_empty(&self) -> bool {
54        self.len() == 0
55    }
56
57    /// Copy the string into a Rust `String`.
58    #[must_use]
59    pub fn to_string_lossy(&self) -> String {
60        let mut units: Vec<u16> = Vec::new();
61        loop {
62            let capacity = units.len();
63            let buffer = if capacity == 0 {
64                std::ptr::null_mut()
65            } else {
66                units.as_mut_ptr()
67            };
68            let len = unsafe { ffi::acf_cf_string_copy_utf16(self.as_ptr(), buffer, capacity) };
69            if len <= capacity {
70                units.truncate(len);
71                return String::from_utf16_lossy(&units);
72            }
73            units.resize(len, 0);
74        }
75    }
76}
77
78impl Default for CFString {
79    fn default() -> Self {
80        Self::new("")
81    }
82}
83
84impl fmt::Display for CFString {
85    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86        f.write_str(&self.to_string_lossy())
87    }
88}
89
90impl CFNumber {
91    /// Create an integer number.
92    #[must_use]
93    pub fn from_i64(value: i64) -> Self {
94        let ptr = unsafe { ffi::cf_number_create_i64(value) };
95        unsafe { Self::from_raw(ptr) }.expect("CFNumberCreate returned NULL")
96    }
97
98    /// Create an unsigned integer number.
99    #[must_use]
100    pub fn from_u64(value: u64) -> Self {
101        let ptr = unsafe { ffi::cf_number_create_u64(value) };
102        unsafe { Self::from_raw(ptr) }.expect("CFNumberCreate returned NULL")
103    }
104
105    /// Create a floating-point number.
106    #[must_use]
107    pub fn from_f64(value: f64) -> Self {
108        let ptr = unsafe { ffi::cf_number_create_f64(value) };
109        unsafe { Self::from_raw(ptr) }.expect("CFNumberCreate returned NULL")
110    }
111
112    /// Convert to `i64` if representable.
113    #[must_use]
114    pub fn to_i64(&self) -> Option<i64> {
115        let mut out = 0_i64;
116        let ok = unsafe { ffi::cf_number_get_i64(self.as_ptr(), &raw mut out) };
117        ok.then_some(out)
118    }
119
120    /// Convert to `u64` if representable.
121    #[must_use]
122    pub fn to_u64(&self) -> Option<u64> {
123        let mut out = 0_u64;
124        let ok = unsafe { ffi::cf_number_get_u64(self.as_ptr(), &raw mut out) };
125        ok.then_some(out)
126    }
127
128    /// Convert to `f64` if representable.
129    #[must_use]
130    pub fn to_f64(&self) -> Option<f64> {
131        let mut out = 0.0_f64;
132        let ok = unsafe { ffi::cf_number_get_f64(self.as_ptr(), &raw mut out) };
133        ok.then_some(out)
134    }
135
136    /// Whether the number was created from a floating-point representation.
137    #[must_use]
138    pub fn is_float_type(&self) -> bool {
139        unsafe { ffi::cf_number_is_float_type(self.as_ptr()) }
140    }
141}
142
143impl From<i64> for CFNumber {
144    fn from(value: i64) -> Self {
145        Self::from_i64(value)
146    }
147}
148
149impl From<u64> for CFNumber {
150    fn from(value: u64) -> Self {
151        Self::from_u64(value)
152    }
153}
154
155impl From<f64> for CFNumber {
156    fn from(value: f64) -> Self {
157        Self::from_f64(value)
158    }
159}
160
161impl CFData {
162    /// Copy bytes into a new `CFData`.
163    #[must_use]
164    pub fn from_bytes<B: AsRef<[u8]>>(bytes: B) -> Self {
165        let bytes = bytes.as_ref();
166        let ptr = unsafe { ffi::cf_data_create(bytes.as_ptr(), bytes.len()) };
167        unsafe { Self::from_raw(ptr) }.expect("CFDataCreate returned NULL")
168    }
169
170    /// Number of bytes stored in the data blob.
171    #[must_use]
172    pub fn len(&self) -> usize {
173        unsafe { ffi::cf_data_get_length(self.as_ptr()) }
174    }
175
176    /// Whether the data blob is empty.
177    #[must_use]
178    pub fn is_empty(&self) -> bool {
179        self.len() == 0
180    }
181
182    /// Copy the data into a Rust-owned vector.
183    #[must_use]
184    pub fn to_vec(&self) -> Vec<u8> {
185        let mut bytes = vec![0_u8; self.len()];
186        loop {
187            let capacity = bytes.len();
188            let buffer = if capacity == 0 {
189                std::ptr::null_mut()
190            } else {
191                bytes.as_mut_ptr()
192            };
193            let len = unsafe { ffi::acf_cf_data_copy_bytes(self.as_ptr(), buffer, capacity) };
194            if len <= capacity {
195                bytes.truncate(len);
196                return bytes;
197            }
198            bytes.resize(len, 0);
199        }
200    }
201}
202
203impl CFDate {
204    /// Create a date from Core Foundation absolute time (seconds since 2001-01-01 00:00:00 UTC).
205    #[must_use]
206    pub fn from_absolute_time(absolute_time: f64) -> Self {
207        let ptr = unsafe { ffi::cf_date_create(absolute_time) };
208        unsafe { Self::from_raw(ptr) }.expect("CFDateCreate returned NULL")
209    }
210
211    /// Current wall-clock time.
212    #[must_use]
213    pub fn now() -> Self {
214        Self::from_system_time(SystemTime::now())
215    }
216
217    /// Convert from a Rust `SystemTime`.
218    #[must_use]
219    pub fn from_system_time(time: SystemTime) -> Self {
220        let unix_seconds = match time.duration_since(UNIX_EPOCH) {
221            Ok(duration) => duration.as_secs_f64(),
222            Err(err) => -err.duration().as_secs_f64(),
223        };
224        let absolute = unix_seconds - CF_ABSOLUTE_TIME_INTERVAL_SINCE_1970;
225        Self::from_absolute_time(absolute)
226    }
227
228    /// Core Foundation absolute time.
229    #[must_use]
230    pub fn absolute_time(&self) -> f64 {
231        unsafe { ffi::cf_date_get_absolute_time(self.as_ptr()) }
232    }
233
234    /// Convert to `SystemTime` when representable.
235    #[must_use]
236    pub fn to_system_time(&self) -> Option<SystemTime> {
237        let unix_seconds = self.absolute_time() + CF_ABSOLUTE_TIME_INTERVAL_SINCE_1970;
238        if unix_seconds >= 0.0 {
239            UNIX_EPOCH.checked_add(Duration::try_from_secs_f64(unix_seconds).ok()?)
240        } else {
241            UNIX_EPOCH.checked_sub(Duration::try_from_secs_f64(-unix_seconds).ok()?)
242        }
243    }
244}
245
246impl CFUUID {
247    /// Generate a new random UUID.
248    #[must_use]
249    pub fn new() -> Self {
250        let ptr = unsafe { ffi::cf_uuid_create() };
251        unsafe { Self::from_raw(ptr) }.expect("CFUUIDCreate returned NULL")
252    }
253
254    /// Parse a UUID string.
255    #[must_use]
256    pub fn parse_str(value: &str) -> Option<Self> {
257        let canonical = value
258            .strip_prefix('{')
259            .and_then(|inner| inner.strip_suffix('}'))
260            .unwrap_or(value);
261        let well_formed = canonical.len() == 36
262            && canonical.bytes().enumerate().all(|(index, byte)| {
263                if matches!(index, 8 | 13 | 18 | 23) {
264                    byte == b'-'
265                } else {
266                    byte.is_ascii_hexdigit()
267                }
268            });
269        if !well_formed {
270            return None;
271        }
272        let value = CString::new(canonical).ok()?;
273        let ptr = unsafe { ffi::cf_uuid_create_from_string(value.as_ptr()) };
274        unsafe { Self::from_raw(ptr) }
275    }
276
277    /// Canonical textual representation.
278    #[must_use]
279    pub fn string(&self) -> CFString {
280        let ptr = unsafe { ffi::cf_uuid_copy_string(self.as_ptr()) };
281        unsafe { CFString::from_raw(ptr) }.expect("CFUUIDCreateString returned NULL")
282    }
283
284    /// UUID bytes in RFC-4122 order.
285    #[must_use]
286    pub fn bytes(&self) -> [u8; 16] {
287        let mut bytes = [0_u8; 16];
288        unsafe { ffi::cf_uuid_get_bytes(self.as_ptr(), bytes.as_mut_ptr()) };
289        bytes
290    }
291}
292
293impl Default for CFUUID {
294    fn default() -> Self {
295        Self::new()
296    }
297}
298
299impl fmt::Display for CFUUID {
300    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
301        fmt::Display::fmt(&self.string(), f)
302    }
303}
304
305impl CFError {
306    /// Create a Core Foundation error object.
307    #[must_use]
308    pub fn new(domain: &CFString, code: i64, description: Option<&str>) -> Self {
309        let description = description.map(CFString::new);
310        let ptr = unsafe {
311            ffi::acf_cf_error_create(
312                domain.as_ptr(),
313                code,
314                description
315                    .as_ref()
316                    .map_or(std::ptr::null_mut(), CFString::as_ptr),
317            )
318        };
319        unsafe { Self::from_raw(ptr) }.expect("CFErrorCreate returned NULL")
320    }
321
322    /// Error domain.
323    #[must_use]
324    pub fn domain(&self) -> CFString {
325        let ptr = unsafe { ffi::cf_error_get_domain(self.as_ptr()) };
326        unsafe { CFString::from_raw(ptr) }.expect("CFErrorGetDomain returned NULL")
327    }
328
329    /// Numeric error code.
330    #[must_use]
331    pub fn code(&self) -> i64 {
332        unsafe { ffi::cf_error_get_code(self.as_ptr()) }
333    }
334
335    /// Localized description if present.
336    #[must_use]
337    pub fn description_string(&self) -> Option<CFString> {
338        let ptr = unsafe { ffi::cf_error_copy_description(self.as_ptr()) };
339        unsafe { CFString::from_raw(ptr) }
340    }
341
342    /// Failure reason if present.
343    #[must_use]
344    pub fn failure_reason(&self) -> Option<CFString> {
345        let ptr = unsafe { ffi::cf_error_copy_failure_reason(self.as_ptr()) };
346        unsafe { CFString::from_raw(ptr) }
347    }
348}
349
350impl fmt::Display for CFError {
351    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
352        if let Some(description) = self.description_string() {
353            write!(f, "{} ({})", description, self.code())
354        } else {
355            write!(f, "{} ({})", self.domain(), self.code())
356        }
357    }
358}
359
360impl std::error::Error for CFError {}
361
362impl From<CFString> for CFType {
363    fn from(value: CFString) -> Self {
364        value.into_cf_type()
365    }
366}
367
368impl From<CFNumber> for CFType {
369    fn from(value: CFNumber) -> Self {
370        value.into_cf_type()
371    }
372}
373
374impl From<CFData> for CFType {
375    fn from(value: CFData) -> Self {
376        value.into_cf_type()
377    }
378}
379
380impl From<CFDate> for CFType {
381    fn from(value: CFDate) -> Self {
382        value.into_cf_type()
383    }
384}
385
386impl From<CFUUID> for CFType {
387    fn from(value: CFUUID) -> Self {
388        value.into_cf_type()
389    }
390}