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