Skip to main content

apple_cf/cf/
resources.rs

1//! Core Foundation resource, locale, formatter, and preferences wrappers.
2//!
3#![allow(clippy::missing_panics_doc)]
4
5//! ```rust
6//! use apple_cf::cf::{
7//!     CFCalendar, CFCharacterSet, CFDate, CFDateFormatter, CFDateFormatterStyle, CFFileSecurity,
8//!     CFLocale, CFNumber, CFNumberFormatter, CFNumberFormatterStyle, CFPreferences, CFString,
9//!     CFTimeZone, CFURL, CFUUID, CFXML,
10//! };
11//!
12//! let url = CFURL::from_file_system_path("/System/Library", true);
13//! assert!(url.has_directory_path());
14//!
15//! let locale = CFLocale::current();
16//! let tz = CFTimeZone::current();
17//! let calendar = CFCalendar::current();
18//! assert!(!locale.identifier().is_empty());
19//! assert!(!tz.name().is_empty());
20//! assert!(!calendar.identifier().is_empty());
21//!
22//! let charset = CFCharacterSet::from_characters_in_string(&CFString::new("abc"));
23//! assert!(charset.contains('a'));
24//!
25//! let formatter = CFNumberFormatter::new(None, CFNumberFormatterStyle::Decimal);
26//! let rendered = formatter.format_number(&CFNumber::from_i64(1234));
27//! assert!(!rendered.is_empty());
28//!
29//! let date_formatter = CFDateFormatter::new(None, CFDateFormatterStyle::Short, CFDateFormatterStyle::NoStyle);
30//! assert!(!date_formatter.format_date(&CFDate::now()).is_empty());
31//!
32//! let app_id = CFString::new("com.doomfish.apple-cf.tests");
33//! CFPreferences::set_app_value(&CFString::new("example"), Some(&CFString::new("value")), &app_id);
34//! let _ = CFPreferences::synchronize(&app_id);
35//!
36//! let file_security = CFFileSecurity::new();
37//! let owner = CFUUID::new();
38//! assert!(file_security.set_owner_uuid(&owner));
39//!
40//! let escaped = CFXML::escape_entities(&CFString::new("<tag>"));
41//! assert!(escaped.to_string().contains("&lt;"));
42//! ```
43
44use super::base::{impl_cf_type_wrapper, AsCFType, CFType};
45use super::{CFDate, CFNumber, CFString, CFUUID};
46use crate::ffi;
47use std::ffi::CString;
48
49fn to_cstring(value: &str) -> CString {
50    CString::new(value).expect("Core Foundation strings may not contain interior NUL bytes")
51}
52
53impl_cf_type_wrapper!(CFURL, cf_url_get_type_id);
54impl_cf_type_wrapper!(CFBundle, cf_bundle_get_type_id);
55impl_cf_type_wrapper!(CFLocale, cf_locale_get_type_id);
56impl_cf_type_wrapper!(CFCalendar, cf_calendar_get_type_id);
57impl_cf_type_wrapper!(CFTimeZone, cf_time_zone_get_type_id);
58impl_cf_type_wrapper!(CFCharacterSet, cf_character_set_get_type_id);
59impl_cf_type_wrapper!(CFNumberFormatter, cf_number_formatter_get_type_id);
60impl_cf_type_wrapper!(CFDateFormatter, cf_date_formatter_get_type_id);
61impl_cf_type_wrapper!(CFFileSecurity, cf_file_security_get_type_id);
62
63/// `CFNumberFormatterStyle` values mirrored from Core Foundation.
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
65#[repr(i32)]
66pub enum CFNumberFormatterStyle {
67    NoStyle = 0,
68    Decimal = 1,
69    Currency = 2,
70    Percent = 3,
71    Scientific = 4,
72    SpellOut = 5,
73}
74
75/// `CFDateFormatterStyle` values mirrored from Core Foundation.
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
77#[repr(i32)]
78pub enum CFDateFormatterStyle {
79    NoStyle = 0,
80    Short = 1,
81    Medium = 2,
82    Long = 3,
83    Full = 4,
84}
85
86impl CFURL {
87    /// Create a URL from an absolute string.
88    #[must_use]
89    pub fn from_string(value: &str) -> Self {
90        let value = to_cstring(value);
91        let ptr = unsafe { ffi::cf_url_create_with_string(value.as_ptr()) };
92        Self::from_raw(ptr).expect("CFURLCreateWithString returned NULL")
93    }
94
95    /// Create a file URL from a POSIX path.
96    #[must_use]
97    pub fn from_file_system_path(path: &str, is_directory: bool) -> Self {
98        let path = to_cstring(path);
99        let ptr = unsafe { ffi::cf_url_create_file_path(path.as_ptr(), is_directory) };
100        Self::from_raw(ptr).expect("CFURLCreateWithFileSystemPath returned NULL")
101    }
102
103    /// Absolute string form of the URL.
104    #[must_use]
105    pub fn absolute_string(&self) -> CFString {
106        let ptr = unsafe { ffi::cf_url_copy_absolute_string(self.as_ptr()) };
107        CFString::from_raw(ptr).expect("CFURLCopyAbsoluteString returned NULL")
108    }
109
110    /// File-system path (POSIX style) for file URLs.
111    #[must_use]
112    pub fn file_system_path(&self) -> CFString {
113        let ptr = unsafe { ffi::cf_url_copy_file_system_path(self.as_ptr()) };
114        CFString::from_raw(ptr).expect("CFURLCopyFileSystemPath returned NULL")
115    }
116
117    /// Whether the URL ends with a directory path separator.
118    #[must_use]
119    pub fn has_directory_path(&self) -> bool {
120        unsafe { ffi::cf_url_has_directory_path(self.as_ptr()) }
121    }
122}
123
124impl CFBundle {
125    /// Main bundle for the current process, if any.
126    #[must_use]
127    pub fn main() -> Option<Self> {
128        let ptr = unsafe { ffi::cf_bundle_get_main() };
129        Self::from_raw(ptr)
130    }
131
132    /// Create a bundle wrapper from a bundle URL.
133    #[must_use]
134    pub fn from_url(url: &CFURL) -> Option<Self> {
135        let ptr = unsafe { ffi::cf_bundle_create(url.as_ptr()) };
136        Self::from_raw(ptr)
137    }
138
139    /// Bundle identifier, if present.
140    #[must_use]
141    pub fn identifier(&self) -> Option<CFString> {
142        let ptr = unsafe { ffi::cf_bundle_copy_identifier(self.as_ptr()) };
143        CFString::from_raw(ptr)
144    }
145
146    /// Bundle URL.
147    #[must_use]
148    pub fn bundle_url(&self) -> CFURL {
149        let ptr = unsafe { ffi::cf_bundle_copy_bundle_url(self.as_ptr()) };
150        CFURL::from_raw(ptr).expect("CFBundleCopyBundleURL returned NULL")
151    }
152
153    /// Locate a resource by name and optional extension/subdirectory.
154    #[must_use]
155    pub fn resource_url(
156        &self,
157        name: &str,
158        extension: Option<&str>,
159        subdir: Option<&str>,
160    ) -> Option<CFURL> {
161        let name = to_cstring(name);
162        let extension = extension.map(to_cstring);
163        let subdir = subdir.map(to_cstring);
164        let ptr = unsafe {
165            ffi::cf_bundle_copy_resource_url(
166                self.as_ptr(),
167                name.as_ptr(),
168                extension.as_ref().map_or(std::ptr::null(), |s| s.as_ptr()),
169                subdir.as_ref().map_or(std::ptr::null(), |s| s.as_ptr()),
170            )
171        };
172        CFURL::from_raw(ptr)
173    }
174}
175
176impl CFLocale {
177    /// Current user locale.
178    #[must_use]
179    pub fn current() -> Self {
180        let ptr = unsafe { ffi::cf_locale_copy_current() };
181        Self::from_raw(ptr).expect("CFLocaleCopyCurrent returned NULL")
182    }
183
184    /// Create a locale from an identifier such as `en_US`.
185    #[must_use]
186    pub fn new(identifier: &str) -> Self {
187        let identifier = to_cstring(identifier);
188        let ptr = unsafe { ffi::cf_locale_create(identifier.as_ptr()) };
189        Self::from_raw(ptr).expect("CFLocaleCreate returned NULL")
190    }
191
192    /// Locale identifier.
193    #[must_use]
194    pub fn identifier(&self) -> CFString {
195        let ptr = unsafe { ffi::cf_locale_copy_identifier(self.as_ptr()) };
196        CFString::from_raw(ptr).expect("CFLocale identifier should be non-null")
197    }
198}
199
200impl CFCalendar {
201    /// Current user calendar.
202    #[must_use]
203    pub fn current() -> Self {
204        let ptr = unsafe { ffi::cf_calendar_copy_current() };
205        Self::from_raw(ptr).expect("CFCalendarCopyCurrent returned NULL")
206    }
207
208    /// Create a calendar by identifier (for example `gregorian`).
209    #[must_use]
210    pub fn new(identifier: &str) -> Self {
211        let identifier = to_cstring(identifier);
212        let ptr = unsafe { ffi::cf_calendar_create(identifier.as_ptr()) };
213        Self::from_raw(ptr).expect("CFCalendarCreateWithIdentifier returned NULL")
214    }
215
216    /// Calendar identifier.
217    #[must_use]
218    pub fn identifier(&self) -> CFString {
219        let ptr = unsafe { ffi::cf_calendar_copy_identifier(self.as_ptr()) };
220        CFString::from_raw(ptr).expect("CFCalendar identifier should be non-null")
221    }
222
223    /// Time zone attached to the calendar.
224    #[must_use]
225    pub fn time_zone(&self) -> CFTimeZone {
226        let ptr = unsafe { ffi::cf_calendar_copy_time_zone(self.as_ptr()) };
227        CFTimeZone::from_raw(ptr).expect("CFCalendarCopyTimeZone returned NULL")
228    }
229
230    /// Update the calendar's time zone.
231    pub fn set_time_zone(&self, time_zone: &CFTimeZone) {
232        unsafe { ffi::cf_calendar_set_time_zone(self.as_ptr(), time_zone.as_ptr()) };
233    }
234}
235
236impl CFTimeZone {
237    /// Current system time zone.
238    #[must_use]
239    pub fn current() -> Self {
240        let ptr = unsafe { ffi::cf_time_zone_copy_current() };
241        Self::from_raw(ptr).expect("CFTimeZoneCopyCurrent returned NULL")
242    }
243
244    /// Create a time zone by name, for example `UTC`.
245    #[must_use]
246    pub fn new(name: &str) -> Self {
247        let name = to_cstring(name);
248        let ptr = unsafe { ffi::cf_time_zone_create(name.as_ptr()) };
249        Self::from_raw(ptr).expect("CFTimeZoneCreateWithName returned NULL")
250    }
251
252    /// Time zone name.
253    #[must_use]
254    pub fn name(&self) -> CFString {
255        let ptr = unsafe { ffi::cf_time_zone_copy_name(self.as_ptr()) };
256        CFString::from_raw(ptr).expect("CFTimeZoneGetName returned NULL")
257    }
258
259    /// Offset from GMT in seconds for the supplied date.
260    #[must_use]
261    pub fn seconds_from_gmt(&self, date: &CFDate) -> i32 {
262        unsafe { ffi::cf_time_zone_get_seconds_from_gmt(self.as_ptr(), date.as_ptr()) }
263    }
264}
265
266impl CFCharacterSet {
267    /// Create a character set from the characters contained in `string`.
268    #[must_use]
269    pub fn from_characters_in_string(string: &CFString) -> Self {
270        let ptr =
271            unsafe { ffi::cf_character_set_create_with_characters_in_string(string.as_ptr()) };
272        Self::from_raw(ptr).expect("CFCharacterSetCreateWithCharactersInString returned NULL")
273    }
274
275    /// Invert the character set.
276    #[must_use]
277    pub fn inverted(&self) -> Self {
278        let ptr = unsafe { ffi::cf_character_set_create_inverted_set(self.as_ptr()) };
279        Self::from_raw(ptr).expect("CFCharacterSetCreateInvertedSet returned NULL")
280    }
281
282    /// Whether `character` is a member of the set.
283    #[must_use]
284    pub fn contains(&self, character: char) -> bool {
285        unsafe { ffi::cf_character_set_is_character_member(self.as_ptr(), u32::from(character)) }
286    }
287}
288
289impl CFNumberFormatter {
290    /// Create a number formatter for the given locale and style.
291    #[must_use]
292    pub fn new(locale: Option<&CFLocale>, style: CFNumberFormatterStyle) -> Self {
293        let ptr = unsafe {
294            ffi::cf_number_formatter_create(
295                locale.map_or(std::ptr::null_mut(), CFLocale::as_ptr),
296                style as i32,
297            )
298        };
299        Self::from_raw(ptr).expect("CFNumberFormatterCreate returned NULL")
300    }
301
302    /// Format a number into a string.
303    #[must_use]
304    pub fn format_number(&self, number: &CFNumber) -> CFString {
305        let ptr = unsafe {
306            ffi::cf_number_formatter_create_string_with_number(self.as_ptr(), number.as_ptr())
307        };
308        CFString::from_raw(ptr).expect("CFNumberFormatterCreateStringWithNumber returned NULL")
309    }
310
311    /// Parse a string into a Core Foundation number.
312    #[must_use]
313    pub fn parse_number(&self, string: &CFString) -> Option<CFNumber> {
314        let ptr = unsafe {
315            ffi::cf_number_formatter_create_number_from_string(self.as_ptr(), string.as_ptr())
316        };
317        CFNumber::from_raw(ptr)
318    }
319}
320
321impl CFDateFormatter {
322    /// Create a date formatter for the given locale and styles.
323    #[must_use]
324    pub fn new(
325        locale: Option<&CFLocale>,
326        date_style: CFDateFormatterStyle,
327        time_style: CFDateFormatterStyle,
328    ) -> Self {
329        let ptr = unsafe {
330            ffi::cf_date_formatter_create(
331                locale.map_or(std::ptr::null_mut(), CFLocale::as_ptr),
332                date_style as i32,
333                time_style as i32,
334            )
335        };
336        Self::from_raw(ptr).expect("CFDateFormatterCreate returned NULL")
337    }
338
339    /// Format a date into a localized string.
340    #[must_use]
341    pub fn format_date(&self, date: &CFDate) -> CFString {
342        let ptr =
343            unsafe { ffi::cf_date_formatter_create_string_with_date(self.as_ptr(), date.as_ptr()) };
344        CFString::from_raw(ptr).expect("CFDateFormatterCreateStringWithDate returned NULL")
345    }
346}
347
348impl CFFileSecurity {
349    /// Create a mutable file-security object.
350    #[must_use]
351    pub fn new() -> Self {
352        let ptr = unsafe { ffi::cf_file_security_create() };
353        Self::from_raw(ptr).expect("CFFileSecurityCreate returned NULL")
354    }
355
356    /// Owner UUID, if present.
357    #[must_use]
358    pub fn owner_uuid(&self) -> Option<CFUUID> {
359        let ptr = unsafe { ffi::cf_file_security_copy_owner_uuid(self.as_ptr()) };
360        CFUUID::from_raw(ptr)
361    }
362
363    /// Set the owner UUID.
364    #[must_use]
365    pub fn set_owner_uuid(&self, uuid: &CFUUID) -> bool {
366        unsafe { ffi::cf_file_security_set_owner_uuid(self.as_ptr(), uuid.as_ptr()) }
367    }
368
369    /// File mode, if present.
370    #[must_use]
371    pub fn mode(&self) -> Option<u32> {
372        let mut mode = 0_u32;
373        let ok = unsafe { ffi::cf_file_security_get_mode(self.as_ptr(), &mut mode) };
374        ok.then_some(mode)
375    }
376
377    /// Set the file mode bits.
378    #[must_use]
379    pub fn set_mode(&self, mode: u32) -> bool {
380        unsafe { ffi::cf_file_security_set_mode(self.as_ptr(), mode) }
381    }
382}
383
384impl Default for CFFileSecurity {
385    fn default() -> Self {
386        Self::new()
387    }
388}
389
390/// Core Foundation preferences helpers.
391#[derive(Debug)]
392pub struct CFPreferences;
393
394impl CFPreferences {
395    /// Set or clear an application-scoped preference value.
396    pub fn set_app_value(key: &CFString, value: Option<&dyn AsCFType>, app_id: &CFString) {
397        unsafe {
398            ffi::cf_preferences_set_app_value(
399                key.as_ptr(),
400                value.map_or(std::ptr::null_mut(), AsCFType::as_ptr),
401                app_id.as_ptr(),
402            );
403        }
404    }
405
406    /// Copy an application-scoped preference value.
407    #[must_use]
408    pub fn app_value(key: &CFString, app_id: &CFString) -> Option<CFType> {
409        let ptr = unsafe { ffi::cf_preferences_copy_app_value(key.as_ptr(), app_id.as_ptr()) };
410        CFType::from_raw(ptr)
411    }
412
413    /// Flush pending preference changes.
414    #[must_use]
415    pub fn synchronize(app_id: &CFString) -> bool {
416        unsafe { ffi::cf_preferences_app_synchronize(app_id.as_ptr()) }
417    }
418}
419
420/// Tiny wrapper around the remaining useful `CFXML` helpers.
421#[derive(Debug)]
422pub struct CFXML;
423
424impl CFXML {
425    /// Escape XML entities in `value`.
426    #[must_use]
427    pub fn escape_entities(value: &CFString) -> CFString {
428        let ptr = unsafe { ffi::cf_xml_create_string_by_escaping_entities(value.as_ptr()) };
429        CFString::from_raw(ptr).expect("CFXMLCreateStringByEscapingEntities returned NULL")
430    }
431
432    /// Unescape XML entities in `value`.
433    #[must_use]
434    pub fn unescape_entities(value: &CFString) -> CFString {
435        let ptr = unsafe { ffi::cf_xml_create_string_by_unescaping_entities(value.as_ptr()) };
436        CFString::from_raw(ptr).expect("CFXMLCreateStringByUnescapingEntities returned NULL")
437    }
438}