prefs 0.1.2

Type-safe macOS preferences library
use std::ffi::{CStr, CString, c_char};

use crate::error::{Error, Result};
use crate::raw;

/// Types that can be stored in and read from macOS preferences.
///
/// Implemented for `bool`, `i64`, `f64`, `String`, and `Vec<String>`.
pub trait PreferenceValue: Sized {
    /// Read an optional value from the given domain and key.
    fn get(domain: &CStr, key: &CStr) -> Result<Option<Self>>;
    /// Write a value to the given domain and key.
    fn set(domain: &CStr, key: &CStr, value: &Self) -> Result<()>;
    /// Remove the key from the given domain.
    fn remove(domain: &CStr, key: &CStr) -> Result<()>;
    /// Check whether the key exists in the given domain.
    fn contains(domain: &CStr, key: &CStr) -> Result<bool>;
}

impl PreferenceValue for bool {
    fn get(domain: &CStr, key: &CStr) -> Result<Option<Self>> {
        let result = raw::get_bool_optional(domain, key);
        if result.found {
            Ok(Some(result.value))
        } else {
            Ok(None)
        }
    }

    fn set(domain: &CStr, key: &CStr, value: &Self) -> Result<()> {
        if raw::set_bool(domain, key, *value) {
            Ok(())
        } else {
            Err(Error::WriteFailed)
        }
    }

    fn remove(domain: &CStr, key: &CStr) -> Result<()> {
        if raw::remove(domain, key) {
            Ok(())
        } else {
            Err(Error::WriteFailed)
        }
    }

    fn contains(domain: &CStr, key: &CStr) -> Result<bool> {
        Ok(raw::contains(domain, key))
    }
}

impl PreferenceValue for i64 {
    fn get(domain: &CStr, key: &CStr) -> Result<Option<Self>> {
        let result = raw::get_i64_optional(domain, key);
        if result.found {
            Ok(Some(result.value))
        } else {
            Ok(None)
        }
    }

    fn set(domain: &CStr, key: &CStr, value: &Self) -> Result<()> {
        if raw::set_i64(domain, key, *value) {
            Ok(())
        } else {
            Err(Error::WriteFailed)
        }
    }

    fn remove(domain: &CStr, key: &CStr) -> Result<()> {
        if raw::remove(domain, key) {
            Ok(())
        } else {
            Err(Error::WriteFailed)
        }
    }

    fn contains(domain: &CStr, key: &CStr) -> Result<bool> {
        Ok(raw::contains(domain, key))
    }
}

impl PreferenceValue for f64 {
    fn get(domain: &CStr, key: &CStr) -> Result<Option<Self>> {
        let result = raw::get_f64_optional(domain, key);
        if result.found {
            Ok(Some(result.value))
        } else {
            Ok(None)
        }
    }

    fn set(domain: &CStr, key: &CStr, value: &Self) -> Result<()> {
        if raw::set_f64(domain, key, *value) {
            Ok(())
        } else {
            Err(Error::WriteFailed)
        }
    }

    fn remove(domain: &CStr, key: &CStr) -> Result<()> {
        if raw::remove(domain, key) {
            Ok(())
        } else {
            Err(Error::WriteFailed)
        }
    }

    fn contains(domain: &CStr, key: &CStr) -> Result<bool> {
        Ok(raw::contains(domain, key))
    }
}

impl PreferenceValue for String {
    fn get(domain: &CStr, key: &CStr) -> Result<Option<Self>> {
        let ptr = raw::copy_string(domain, key);
        if ptr.is_null() {
            return Ok(None);
        }

        let _guard = RawStringGuard(ptr);
        Ok(Some(
            unsafe { CStr::from_ptr(ptr) }
                .to_string_lossy()
                .into_owned(),
        ))
    }

    fn set(domain: &CStr, key: &CStr, value: &Self) -> Result<()> {
        let cstr = CString::new(value.as_str())?;
        if raw::set_string(domain, key, &cstr) {
            Ok(())
        } else {
            Err(Error::WriteFailed)
        }
    }

    fn remove(domain: &CStr, key: &CStr) -> Result<()> {
        if raw::remove(domain, key) {
            Ok(())
        } else {
            Err(Error::WriteFailed)
        }
    }

    fn contains(domain: &CStr, key: &CStr) -> Result<bool> {
        Ok(raw::contains(domain, key))
    }
}

impl PreferenceValue for Vec<String> {
    fn get(domain: &CStr, key: &CStr) -> Result<Option<Self>> {
        let mut raw_array = raw::copy_string_array(domain, key);
        let _guard = RawStringArrayGuard(std::ptr::addr_of_mut!(raw_array));
        if !raw_array.found {
            return Ok(None);
        }

        let mut values = Vec::with_capacity(raw_array.len);
        if raw_array.values.is_null() || raw_array.len == 0 {
            return Ok(Some(values));
        }

        let raw_values = unsafe { std::slice::from_raw_parts(raw_array.values, raw_array.len) };
        for value in raw_values {
            if value.is_null() {
                continue;
            }

            values.push(
                unsafe { CStr::from_ptr(*value) }
                    .to_string_lossy()
                    .into_owned(),
            );
        }

        Ok(Some(values))
    }

    fn set(domain: &CStr, key: &CStr, value: &Self) -> Result<()> {
        let cstrings: Vec<CString> = value
            .iter()
            .map(|s| CString::new(s.as_str()))
            .collect::<std::result::Result<Vec<_>, _>>()?;
        let refs: Vec<&CStr> = cstrings.iter().map(CString::as_c_str).collect();

        if raw::set_string_array(domain, key, &refs) {
            Ok(())
        } else {
            Err(Error::WriteFailed)
        }
    }

    fn remove(domain: &CStr, key: &CStr) -> Result<()> {
        if raw::remove(domain, key) {
            Ok(())
        } else {
            Err(Error::WriteFailed)
        }
    }

    fn contains(domain: &CStr, key: &CStr) -> Result<bool> {
        Ok(raw::contains(domain, key))
    }
}

struct RawStringGuard(*mut c_char);

impl Drop for RawStringGuard {
    fn drop(&mut self) {
        raw::free_string(self.0);
    }
}

struct RawStringArrayGuard(*mut raw::RawStringArray);

impl Drop for RawStringArrayGuard {
    fn drop(&mut self) {
        raw::free_string_array(self.0);
    }
}