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        unsafe { 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        unsafe { 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        unsafe { 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        unsafe { 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        unsafe { 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        unsafe { 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        unsafe { 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        unsafe { 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        unsafe { 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        unsafe { 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        unsafe { 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        unsafe { 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        unsafe { 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        unsafe { 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        unsafe { 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        unsafe { 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        unsafe { 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        unsafe { 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        unsafe { 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        unsafe { Self::from_raw(ptr) }
273            .expect("CFCharacterSetCreateWithCharactersInString returned NULL")
274    }
275
276    /// Invert the character set.
277    #[must_use]
278    pub fn inverted(&self) -> Self {
279        let ptr = unsafe { ffi::cf_character_set_create_inverted_set(self.as_ptr()) };
280        unsafe { Self::from_raw(ptr) }.expect("CFCharacterSetCreateInvertedSet returned NULL")
281    }
282
283    /// Whether `character` is a member of the set.
284    #[must_use]
285    pub fn contains(&self, character: char) -> bool {
286        unsafe { ffi::cf_character_set_is_character_member(self.as_ptr(), u32::from(character)) }
287    }
288}
289
290impl CFNumberFormatter {
291    /// Create a number formatter for the given locale and style.
292    #[must_use]
293    pub fn new(locale: Option<&CFLocale>, style: CFNumberFormatterStyle) -> Self {
294        let ptr = unsafe {
295            ffi::cf_number_formatter_create(
296                locale.map_or(std::ptr::null_mut(), CFLocale::as_ptr),
297                style as i32,
298            )
299        };
300        unsafe { Self::from_raw(ptr) }.expect("CFNumberFormatterCreate returned NULL")
301    }
302
303    /// Format a number into a string.
304    #[must_use]
305    pub fn format_number(&self, number: &CFNumber) -> CFString {
306        let ptr = unsafe {
307            ffi::cf_number_formatter_create_string_with_number(self.as_ptr(), number.as_ptr())
308        };
309        unsafe { CFString::from_raw(ptr) }
310            .expect("CFNumberFormatterCreateStringWithNumber returned NULL")
311    }
312
313    /// Parse a string into a Core Foundation number.
314    #[must_use]
315    pub fn parse_number(&self, string: &CFString) -> Option<CFNumber> {
316        let ptr = unsafe {
317            ffi::cf_number_formatter_create_number_from_string(self.as_ptr(), string.as_ptr())
318        };
319        unsafe { CFNumber::from_raw(ptr) }
320    }
321}
322
323impl CFDateFormatter {
324    /// Create a date formatter for the given locale and styles.
325    #[must_use]
326    pub fn new(
327        locale: Option<&CFLocale>,
328        date_style: CFDateFormatterStyle,
329        time_style: CFDateFormatterStyle,
330    ) -> Self {
331        let ptr = unsafe {
332            ffi::cf_date_formatter_create(
333                locale.map_or(std::ptr::null_mut(), CFLocale::as_ptr),
334                date_style as i32,
335                time_style as i32,
336            )
337        };
338        unsafe { Self::from_raw(ptr) }.expect("CFDateFormatterCreate returned NULL")
339    }
340
341    /// Format a date into a localized string.
342    #[must_use]
343    pub fn format_date(&self, date: &CFDate) -> CFString {
344        let ptr =
345            unsafe { ffi::cf_date_formatter_create_string_with_date(self.as_ptr(), date.as_ptr()) };
346        unsafe { CFString::from_raw(ptr) }
347            .expect("CFDateFormatterCreateStringWithDate returned NULL")
348    }
349}
350
351impl CFFileSecurity {
352    /// Create a mutable file-security object.
353    #[must_use]
354    pub fn new() -> Self {
355        let ptr = unsafe { ffi::cf_file_security_create() };
356        unsafe { Self::from_raw(ptr) }.expect("CFFileSecurityCreate returned NULL")
357    }
358
359    /// Owner UUID, if present.
360    #[must_use]
361    pub fn owner_uuid(&self) -> Option<CFUUID> {
362        let ptr = unsafe { ffi::cf_file_security_copy_owner_uuid(self.as_ptr()) };
363        unsafe { CFUUID::from_raw(ptr) }
364    }
365
366    /// Set the owner UUID.
367    #[must_use]
368    pub fn set_owner_uuid(&self, uuid: &CFUUID) -> bool {
369        unsafe { ffi::cf_file_security_set_owner_uuid(self.as_ptr(), uuid.as_ptr()) }
370    }
371
372    /// File mode, if present.
373    #[must_use]
374    pub fn mode(&self) -> Option<u32> {
375        let mut mode = 0_u32;
376        let ok = unsafe { ffi::cf_file_security_get_mode(self.as_ptr(), &mut mode) };
377        ok.then_some(mode)
378    }
379
380    /// Set the file mode bits.
381    #[must_use]
382    pub fn set_mode(&self, mode: u32) -> bool {
383        unsafe { ffi::cf_file_security_set_mode(self.as_ptr(), mode) }
384    }
385}
386
387impl Default for CFFileSecurity {
388    fn default() -> Self {
389        Self::new()
390    }
391}
392
393/// Core Foundation preferences helpers.
394#[derive(Debug)]
395pub struct CFPreferences;
396
397impl CFPreferences {
398    /// Set or clear an application-scoped preference value.
399    pub fn set_app_value(key: &CFString, value: Option<&dyn AsCFType>, app_id: &CFString) {
400        unsafe {
401            ffi::cf_preferences_set_app_value(
402                key.as_ptr(),
403                value.map_or(std::ptr::null_mut(), AsCFType::as_ptr),
404                app_id.as_ptr(),
405            );
406        }
407    }
408
409    /// Copy an application-scoped preference value.
410    #[must_use]
411    pub fn app_value(key: &CFString, app_id: &CFString) -> Option<CFType> {
412        let ptr = unsafe { ffi::cf_preferences_copy_app_value(key.as_ptr(), app_id.as_ptr()) };
413        unsafe { CFType::from_raw(ptr) }
414    }
415
416    /// Flush pending preference changes.
417    #[must_use]
418    pub fn synchronize(app_id: &CFString) -> bool {
419        unsafe { ffi::cf_preferences_app_synchronize(app_id.as_ptr()) }
420    }
421}
422
423/// Tiny wrapper around the remaining useful `CFXML` helpers.
424#[derive(Debug)]
425pub struct CFXML;
426
427impl CFXML {
428    /// Escape XML entities in `value`.
429    #[must_use]
430    pub fn escape_entities(value: &CFString) -> CFString {
431        let ptr = unsafe { ffi::cf_xml_create_string_by_escaping_entities(value.as_ptr()) };
432        unsafe { CFString::from_raw(ptr) }
433            .expect("CFXMLCreateStringByEscapingEntities returned NULL")
434    }
435
436    /// Unescape XML entities in `value`.
437    #[must_use]
438    pub fn unescape_entities(value: &CFString) -> CFString {
439        let ptr = unsafe { ffi::cf_xml_create_string_by_unescaping_entities(value.as_ptr()) };
440        unsafe { CFString::from_raw(ptr) }
441            .expect("CFXMLCreateStringByUnescapingEntities returned NULL")
442    }
443}