use std::{
ffi::{CStr, CString},
os::raw::{c_char, c_void},
ptr, slice,
};
#[link(name = "System", kind = "framework")]
unsafe extern "C" {
fn xpc_dictionary_create(
keys: *const *const c_char,
values: *const *const c_void,
count: usize,
) -> *mut c_void;
fn xpc_dictionary_set_string(xdict: *mut c_void, key: *const c_char, value: *const c_char);
fn xpc_dictionary_set_uint64(xdict: *mut c_void, key: *const c_char, value: u64);
fn xpc_dictionary_set_bool(xdict: *mut c_void, key: *const c_char, value: bool);
fn xpc_dictionary_set_uuid(xdict: *mut c_void, key: *const c_char, uuid: *const u8);
fn xpc_dictionary_get_string(xdict: *mut c_void, key: *const c_char) -> *const c_char;
fn xpc_dictionary_get_uint64(xdict: *mut c_void, key: *const c_char) -> u64;
fn xpc_dictionary_get_data(
xdict: *mut c_void,
key: *const c_char,
length: *mut usize,
) -> *const c_void;
fn xpc_release(xpc_object: *mut c_void);
fn xpc_retain(xpc_object: *mut c_void) -> *mut c_void;
}
#[derive(Debug)]
pub struct XpcObject {
raw: *mut c_void,
}
unsafe impl Send for XpcObject {}
unsafe impl Sync for XpcObject {}
impl XpcObject {
#[must_use]
pub fn new_dictionary() -> Self {
let raw = unsafe { xpc_dictionary_create(ptr::null(), ptr::null(), 0) };
Self { raw }
}
pub unsafe fn from_raw_retained_or_null(raw: *mut c_void) -> Option<Self> {
if raw.is_null() {
None
} else {
Some(Self { raw })
}
}
#[must_use]
pub fn as_ptr(&self) -> *mut c_void {
self.raw
}
#[must_use]
pub fn retain(&self) -> Self {
let raw = unsafe { xpc_retain(self.raw) };
Self { raw }
}
pub fn set_string(&self, key: &CStr, value: &str) {
let Ok(val_c) = CString::new(value) else {
tracing::warn!(?key, "xpc set_string: value contained NUL; skipping");
return;
};
unsafe { xpc_dictionary_set_string(self.raw, key.as_ptr(), val_c.as_ptr()) };
}
pub fn set_uint64(&self, key: &CStr, value: u64) {
unsafe { xpc_dictionary_set_uint64(self.raw, key.as_ptr(), value) };
}
pub fn set_bool(&self, key: &CStr, value: bool) {
unsafe { xpc_dictionary_set_bool(self.raw, key.as_ptr(), value) };
}
pub fn set_uuid(&self, key: &CStr, uuid: &[u8; 16]) {
unsafe { xpc_dictionary_set_uuid(self.raw, key.as_ptr(), uuid.as_ptr()) };
}
#[must_use]
pub fn get_uint64(&self, key: &CStr) -> u64 {
unsafe { xpc_dictionary_get_uint64(self.raw, key.as_ptr()) }
}
#[must_use]
pub fn get_string(&self, key: &CStr) -> Option<String> {
let raw = unsafe { xpc_dictionary_get_string(self.raw, key.as_ptr()) };
if raw.is_null() {
return None;
}
let bytes = unsafe { CStr::from_ptr(raw) }.to_bytes();
Some(String::from_utf8_lossy(bytes).into_owned())
}
#[must_use]
pub fn get_data(&self, key: &CStr) -> Option<Vec<u8>> {
let mut len: usize = 0;
let raw = unsafe { xpc_dictionary_get_data(self.raw, key.as_ptr(), &raw mut len) };
if raw.is_null() || len == 0 {
return None;
}
let slice = unsafe { slice::from_raw_parts(raw.cast::<u8>(), len) };
Some(slice.to_vec())
}
}
impl Drop for XpcObject {
fn drop(&mut self) {
if !self.raw.is_null() {
unsafe { xpc_release(self.raw) };
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_should_round_trip_uint64_through_xpc_dictionary() {
let dict = XpcObject::new_dictionary();
dict.set_uint64(c"vmnet_operation_mode_key", 1000);
assert_eq!(dict.get_uint64(c"vmnet_operation_mode_key"), 1000);
}
#[test]
fn test_should_round_trip_string_through_xpc_dictionary() {
let dict = XpcObject::new_dictionary();
dict.set_string(
c"vmnet_interface_id_key",
"00112233-4455-6677-8899-AABBCCDDEEFF",
);
assert_eq!(
dict.get_string(c"vmnet_interface_id_key").as_deref(),
Some("00112233-4455-6677-8899-AABBCCDDEEFF")
);
}
#[test]
fn test_should_return_none_for_missing_string_key() {
let dict = XpcObject::new_dictionary();
assert!(dict.get_string(c"missing").is_none());
}
#[test]
fn test_should_skip_set_string_when_value_contains_nul() {
let dict = XpcObject::new_dictionary();
dict.set_string(c"k", "with\0nul");
assert!(dict.get_string(c"k").is_none());
}
}