apple-cf 0.11.0

Safe Rust bindings for Apple's shared Core* frameworks (CoreFoundation, CoreMedia, CoreVideo, CoreGraphics, IOSurface, Dispatch).
Documentation
//! Core Foundation primitive value wrappers.
//!
#![allow(clippy::missing_panics_doc)]

//! ```rust
//! use apple_cf::cf::{CFData, CFDate, CFNumber, CFString, CFUUID};
//!
//! let string = CFString::new("hello");
//! let number = CFNumber::from_i64(42);
//! let data = CFData::from_bytes([1, 2, 3, 4]);
//! let uuid = CFUUID::new();
//!
//! assert_eq!(string.to_string(), "hello");
//! assert_eq!(number.to_i64(), Some(42));
//! assert_eq!(data.to_vec(), vec![1, 2, 3, 4]);
//! assert_eq!(uuid.bytes().len(), 16);
//!
//! let now = CFDate::now();
//! assert!(now.to_system_time().is_some());
//! ```

use super::base::{impl_cf_type_wrapper, CFType};
use crate::ffi;
use std::ffi::CString;
use std::fmt;
use std::time::{Duration, SystemTime, UNIX_EPOCH};

const CF_ABSOLUTE_TIME_INTERVAL_SINCE_1970: f64 = 978_307_200.0;

impl_cf_type_wrapper!(CFString, cf_string_get_type_id);
impl_cf_type_wrapper!(CFNumber, cf_number_get_type_id);
impl_cf_type_wrapper!(CFData, cf_data_get_type_id);
impl_cf_type_wrapper!(CFDate, cf_date_get_type_id);
impl_cf_type_wrapper!(CFUUID, cf_uuid_get_type_id);
impl_cf_type_wrapper!(CFError, cf_error_get_type_id);

impl CFString {
    /// Create a UTF-8 `CFString`.
    #[must_use]
    pub fn new(value: &str) -> Self {
        let ptr = unsafe { ffi::acf_cf_string_create_with_bytes(value.as_ptr(), value.len()) };
        unsafe { Self::from_raw(ptr) }.expect("CFStringCreateWithBytes returned NULL")
    }

    /// Number of UTF-16 code units in the string.
    #[must_use]
    pub fn len(&self) -> usize {
        unsafe { ffi::cf_string_get_length(self.as_ptr()) }
    }

    /// Whether the string is empty.
    #[must_use]
    pub fn is_empty(&self) -> bool {
        self.len() == 0
    }

    /// Copy the string into a Rust `String`.
    #[must_use]
    pub fn to_string_lossy(&self) -> String {
        let mut units: Vec<u16> = Vec::new();
        loop {
            let capacity = units.len();
            let buffer = if capacity == 0 {
                std::ptr::null_mut()
            } else {
                units.as_mut_ptr()
            };
            let len = unsafe { ffi::acf_cf_string_copy_utf16(self.as_ptr(), buffer, capacity) };
            if len <= capacity {
                units.truncate(len);
                return String::from_utf16_lossy(&units);
            }
            units.resize(len, 0);
        }
    }
}

impl Default for CFString {
    fn default() -> Self {
        Self::new("")
    }
}

impl fmt::Display for CFString {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(&self.to_string_lossy())
    }
}

impl CFNumber {
    /// Create an integer number.
    #[must_use]
    pub fn from_i64(value: i64) -> Self {
        let ptr = unsafe { ffi::cf_number_create_i64(value) };
        unsafe { Self::from_raw(ptr) }.expect("CFNumberCreate returned NULL")
    }

    /// Create an unsigned integer number.
    #[must_use]
    pub fn from_u64(value: u64) -> Self {
        let ptr = unsafe { ffi::cf_number_create_u64(value) };
        unsafe { Self::from_raw(ptr) }.expect("CFNumberCreate returned NULL")
    }

    /// Create a floating-point number.
    #[must_use]
    pub fn from_f64(value: f64) -> Self {
        let ptr = unsafe { ffi::cf_number_create_f64(value) };
        unsafe { Self::from_raw(ptr) }.expect("CFNumberCreate returned NULL")
    }

    /// Convert to `i64` if representable.
    #[must_use]
    pub fn to_i64(&self) -> Option<i64> {
        let mut out = 0_i64;
        let ok = unsafe { ffi::cf_number_get_i64(self.as_ptr(), &raw mut out) };
        ok.then_some(out)
    }

    /// Convert to `u64` if representable.
    #[must_use]
    pub fn to_u64(&self) -> Option<u64> {
        let mut out = 0_u64;
        let ok = unsafe { ffi::cf_number_get_u64(self.as_ptr(), &raw mut out) };
        ok.then_some(out)
    }

    /// Convert to `f64` if representable.
    #[must_use]
    pub fn to_f64(&self) -> Option<f64> {
        let mut out = 0.0_f64;
        let ok = unsafe { ffi::cf_number_get_f64(self.as_ptr(), &raw mut out) };
        ok.then_some(out)
    }

    /// Whether the number was created from a floating-point representation.
    #[must_use]
    pub fn is_float_type(&self) -> bool {
        unsafe { ffi::cf_number_is_float_type(self.as_ptr()) }
    }
}

impl From<i64> for CFNumber {
    fn from(value: i64) -> Self {
        Self::from_i64(value)
    }
}

impl From<u64> for CFNumber {
    fn from(value: u64) -> Self {
        Self::from_u64(value)
    }
}

impl From<f64> for CFNumber {
    fn from(value: f64) -> Self {
        Self::from_f64(value)
    }
}

impl CFData {
    /// Copy bytes into a new `CFData`.
    #[must_use]
    pub fn from_bytes<B: AsRef<[u8]>>(bytes: B) -> Self {
        let bytes = bytes.as_ref();
        let ptr = unsafe { ffi::cf_data_create(bytes.as_ptr(), bytes.len()) };
        unsafe { Self::from_raw(ptr) }.expect("CFDataCreate returned NULL")
    }

    /// Number of bytes stored in the data blob.
    #[must_use]
    pub fn len(&self) -> usize {
        unsafe { ffi::cf_data_get_length(self.as_ptr()) }
    }

    /// Whether the data blob is empty.
    #[must_use]
    pub fn is_empty(&self) -> bool {
        self.len() == 0
    }

    /// Copy the data into a Rust-owned vector.
    #[must_use]
    pub fn to_vec(&self) -> Vec<u8> {
        let mut bytes = vec![0_u8; self.len()];
        loop {
            let capacity = bytes.len();
            let buffer = if capacity == 0 {
                std::ptr::null_mut()
            } else {
                bytes.as_mut_ptr()
            };
            let len = unsafe { ffi::acf_cf_data_copy_bytes(self.as_ptr(), buffer, capacity) };
            if len <= capacity {
                bytes.truncate(len);
                return bytes;
            }
            bytes.resize(len, 0);
        }
    }
}

impl CFDate {
    /// Create a date from Core Foundation absolute time (seconds since 2001-01-01 00:00:00 UTC).
    #[must_use]
    pub fn from_absolute_time(absolute_time: f64) -> Self {
        let ptr = unsafe { ffi::cf_date_create(absolute_time) };
        unsafe { Self::from_raw(ptr) }.expect("CFDateCreate returned NULL")
    }

    /// Current wall-clock time.
    #[must_use]
    pub fn now() -> Self {
        Self::from_system_time(SystemTime::now())
    }

    /// Convert from a Rust `SystemTime`.
    #[must_use]
    pub fn from_system_time(time: SystemTime) -> Self {
        let unix_seconds = match time.duration_since(UNIX_EPOCH) {
            Ok(duration) => duration.as_secs_f64(),
            Err(err) => -err.duration().as_secs_f64(),
        };
        let absolute = unix_seconds - CF_ABSOLUTE_TIME_INTERVAL_SINCE_1970;
        Self::from_absolute_time(absolute)
    }

    /// Core Foundation absolute time.
    #[must_use]
    pub fn absolute_time(&self) -> f64 {
        unsafe { ffi::cf_date_get_absolute_time(self.as_ptr()) }
    }

    /// Convert to `SystemTime` when representable.
    #[must_use]
    pub fn to_system_time(&self) -> Option<SystemTime> {
        let unix_seconds = self.absolute_time() + CF_ABSOLUTE_TIME_INTERVAL_SINCE_1970;
        if unix_seconds >= 0.0 {
            UNIX_EPOCH.checked_add(Duration::try_from_secs_f64(unix_seconds).ok()?)
        } else {
            UNIX_EPOCH.checked_sub(Duration::try_from_secs_f64(-unix_seconds).ok()?)
        }
    }
}

impl CFUUID {
    /// Generate a new random UUID.
    #[must_use]
    pub fn new() -> Self {
        let ptr = unsafe { ffi::cf_uuid_create() };
        unsafe { Self::from_raw(ptr) }.expect("CFUUIDCreate returned NULL")
    }

    /// Parse a UUID string.
    #[must_use]
    pub fn parse_str(value: &str) -> Option<Self> {
        let canonical = value
            .strip_prefix('{')
            .and_then(|inner| inner.strip_suffix('}'))
            .unwrap_or(value);
        let well_formed = canonical.len() == 36
            && canonical.bytes().enumerate().all(|(index, byte)| {
                if matches!(index, 8 | 13 | 18 | 23) {
                    byte == b'-'
                } else {
                    byte.is_ascii_hexdigit()
                }
            });
        if !well_formed {
            return None;
        }
        let value = CString::new(canonical).ok()?;
        let ptr = unsafe { ffi::cf_uuid_create_from_string(value.as_ptr()) };
        unsafe { Self::from_raw(ptr) }
    }

    /// Canonical textual representation.
    #[must_use]
    pub fn string(&self) -> CFString {
        let ptr = unsafe { ffi::cf_uuid_copy_string(self.as_ptr()) };
        unsafe { CFString::from_raw(ptr) }.expect("CFUUIDCreateString returned NULL")
    }

    /// UUID bytes in RFC-4122 order.
    #[must_use]
    pub fn bytes(&self) -> [u8; 16] {
        let mut bytes = [0_u8; 16];
        unsafe { ffi::cf_uuid_get_bytes(self.as_ptr(), bytes.as_mut_ptr()) };
        bytes
    }
}

impl Default for CFUUID {
    fn default() -> Self {
        Self::new()
    }
}

impl fmt::Display for CFUUID {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        fmt::Display::fmt(&self.string(), f)
    }
}

impl CFError {
    /// Create a Core Foundation error object.
    #[must_use]
    pub fn new(domain: &CFString, code: i64, description: Option<&str>) -> Self {
        let description = description.map(CFString::new);
        let ptr = unsafe {
            ffi::acf_cf_error_create(
                domain.as_ptr(),
                code,
                description
                    .as_ref()
                    .map_or(std::ptr::null_mut(), CFString::as_ptr),
            )
        };
        unsafe { Self::from_raw(ptr) }.expect("CFErrorCreate returned NULL")
    }

    /// Error domain.
    #[must_use]
    pub fn domain(&self) -> CFString {
        let ptr = unsafe { ffi::cf_error_get_domain(self.as_ptr()) };
        unsafe { CFString::from_raw(ptr) }.expect("CFErrorGetDomain returned NULL")
    }

    /// Numeric error code.
    #[must_use]
    pub fn code(&self) -> i64 {
        unsafe { ffi::cf_error_get_code(self.as_ptr()) }
    }

    /// Localized description if present.
    #[must_use]
    pub fn description_string(&self) -> Option<CFString> {
        let ptr = unsafe { ffi::cf_error_copy_description(self.as_ptr()) };
        unsafe { CFString::from_raw(ptr) }
    }

    /// Failure reason if present.
    #[must_use]
    pub fn failure_reason(&self) -> Option<CFString> {
        let ptr = unsafe { ffi::cf_error_copy_failure_reason(self.as_ptr()) };
        unsafe { CFString::from_raw(ptr) }
    }
}

impl fmt::Display for CFError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        if let Some(description) = self.description_string() {
            write!(f, "{} ({})", description, self.code())
        } else {
            write!(f, "{} ({})", self.domain(), self.code())
        }
    }
}

impl std::error::Error for CFError {}

impl From<CFString> for CFType {
    fn from(value: CFString) -> Self {
        value.into_cf_type()
    }
}

impl From<CFNumber> for CFType {
    fn from(value: CFNumber) -> Self {
        value.into_cf_type()
    }
}

impl From<CFData> for CFType {
    fn from(value: CFData) -> Self {
        value.into_cf_type()
    }
}

impl From<CFDate> for CFType {
    fn from(value: CFDate) -> Self {
        value.into_cf_type()
    }
}

impl From<CFUUID> for CFType {
    fn from(value: CFUUID) -> Self {
        value.into_cf_type()
    }
}