use std::collections::HashMap;
use std::ffi::{CStr, CString, c_char, c_void};
use std::time::Duration;
use nojson::DisplayJson;
use crate::core::error::{ClientError, Result};
#[expect(non_camel_case_types)]
type xpc_object_t = *mut c_void;
#[expect(non_camel_case_types)]
type xpc_connection_t = *mut c_void;
unsafe extern "C" {
fn xpc_bridge_create_connection(name: *const c_char) -> xpc_connection_t;
fn xpc_bridge_connection_cancel(conn: xpc_connection_t);
fn xpc_bridge_create_dictionary() -> xpc_object_t;
fn xpc_bridge_dictionary_set_string(d: xpc_object_t, k: *const c_char, v: *const c_char);
fn xpc_bridge_dictionary_set_data(
d: xpc_object_t,
k: *const c_char,
v: *const c_void,
l: usize,
);
fn xpc_bridge_dictionary_set_int64(d: xpc_object_t, k: *const c_char, v: i64);
fn xpc_bridge_dictionary_set_uint64(d: xpc_object_t, k: *const c_char, v: u64);
fn xpc_bridge_dictionary_set_bool(d: xpc_object_t, k: *const c_char, v: bool);
fn xpc_bridge_dictionary_set_fd(d: xpc_object_t, k: *const c_char, v: i32) -> bool;
fn xpc_bridge_release(obj: xpc_object_t);
fn xpc_bridge_send_message_with_reply_sync(
c: xpc_connection_t,
m: xpc_object_t,
timeout_ns: u64,
) -> xpc_object_t;
fn xpc_bridge_copy_data(d: xpc_object_t, k: *const c_char, l: *mut usize) -> *const c_void;
fn xpc_bridge_get_string(d: xpc_object_t, k: *const c_char) -> *const c_char;
fn xpc_bridge_get_int64_checked(d: xpc_object_t, k: *const c_char, out: *mut i64) -> bool;
fn xpc_bridge_is_error(obj: xpc_object_t) -> bool;
fn xpc_bridge_get_error_description(obj: xpc_object_t, b: *mut c_char, bl: usize) -> bool;
fn xpc_bridge_get_log_fds(reply: xpc_object_t, fds: *mut i32, max_fds: usize) -> i32;
}
const ROUTE_KEY: &CStr = c"com.apple.container.xpc.route";
const ERROR_KEY: &CStr = c"com.apple.container.xpc.error";
pub(crate) const SERVICE_NAME: &CStr = c"com.apple.container.apiserver";
pub(crate) const IMAGE_SERVICE: &CStr = c"com.apple.container.core.container-core-images";
pub(crate) const DEFAULT_TIMEOUT: Duration = Duration::from_secs(60);
pub(crate) const LONG_TIMEOUT: Duration = Duration::from_secs(86400);
pub(crate) struct RawReply {
obj: xpc_object_t,
}
unsafe impl Send for RawReply {}
impl RawReply {
pub(crate) fn data(&self, key: &CStr) -> Option<Vec<u8>> {
unsafe {
let mut len = 0;
let p = xpc_bridge_copy_data(self.obj, key.as_ptr(), &mut len);
if p.is_null() || len == 0 {
return None;
}
let v = std::slice::from_raw_parts(p as *const u8, len).to_vec();
libc::free(p as *mut c_void);
Some(v)
}
}
pub(crate) fn string(&self, key: &CStr) -> Option<String> {
unsafe {
let p = xpc_bridge_get_string(self.obj, key.as_ptr());
if p.is_null() {
return None;
}
CStr::from_ptr(p).to_str().ok().map(|s| s.to_string())
}
}
pub(crate) fn try_int64(&self, key: &CStr) -> Result<i64> {
let mut out = 0i64;
let ok = unsafe { xpc_bridge_get_int64_checked(self.obj, key.as_ptr(), &mut out) };
if ok {
Ok(out)
} else {
Err(ClientError::Xpc(format!(
"missing or non-int64 key: {}",
key.to_string_lossy()
))
.into())
}
}
pub(crate) fn log_fds(&self) -> Vec<std::os::fd::RawFd> {
let mut fds = [0i32; 2];
let n = unsafe { xpc_bridge_get_log_fds(self.obj, fds.as_mut_ptr(), fds.len()) };
if n <= 0 {
return Vec::new();
}
fds[..n as usize].to_vec()
}
pub(crate) fn is_error(&self) -> bool {
unsafe { xpc_bridge_is_error(self.obj) }
}
pub(crate) fn error_desc(&self) -> Option<String> {
let mut buf = vec![0u8; 1024];
if unsafe {
xpc_bridge_get_error_description(self.obj, buf.as_mut_ptr() as *mut c_char, buf.len())
} {
let end = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
Some(String::from_utf8_lossy(&buf[..end]).into_owned())
} else {
None
}
}
pub(crate) fn json_error(&self) -> Option<ClientError> {
let ek = ERROR_KEY;
self.data(ek).map(|d| {
let text = std::str::from_utf8(&d).unwrap_or("");
if let Ok(j) = nojson::RawJson::parse(text) {
let code = j
.value()
.to_member("code")
.and_then(|m| m.required())
.and_then(|v| {
let s: String = v.try_into()?;
Ok(s)
})
.unwrap_or_default();
let msg = j
.value()
.to_member("message")
.and_then(|m| m.required())
.and_then(|v| {
let s: String = v.try_into()?;
Ok(s)
})
.unwrap_or_default();
ClientError::Xpc(format!("XPC error {code}: {msg}"))
} else {
ClientError::Xpc("XPC error unparseable".into())
}
})
}
}
impl Drop for RawReply {
fn drop(&mut self) {
unsafe {
xpc_bridge_release(self.obj);
}
}
}
pub(crate) struct XpcConn {
conn: xpc_connection_t,
}
unsafe impl Send for XpcConn {}
impl XpcConn {
pub(crate) fn connect(service: &CStr) -> Result<XpcConn> {
let conn = unsafe { xpc_bridge_create_connection(service.as_ptr()) };
if conn.is_null() {
return Err(ClientError::XpcConnect.into());
}
Ok(XpcConn { conn })
}
pub(crate) fn send(&self, route: &str, entries: &[(CString, KeyValue)]) -> Result<RawReply> {
self.send_with_timeout(route, entries, DEFAULT_TIMEOUT)
}
pub(crate) fn send_with_timeout(
&self,
route: &str,
entries: &[(CString, KeyValue)],
timeout: Duration,
) -> Result<RawReply> {
let rv = CString::new(route).map_err(|_| ClientError::Xpc("route contains NUL".into()))?;
let mut cstrings = Vec::new();
for (_, val) in entries {
if let KeyValue::String(s) = val {
cstrings.push(
CString::new(s.as_str())
.map_err(|_| ClientError::Xpc("string value contains NUL".into()))?,
);
}
}
let mut cstrings = cstrings.into_iter();
let msg = unsafe { xpc_bridge_create_dictionary() };
let rk = ROUTE_KEY;
unsafe {
xpc_bridge_dictionary_set_string(msg, rk.as_ptr(), rv.as_ptr());
}
for (key, val) in entries {
match val {
KeyValue::String(_) => {
let c = cstrings.next().expect("cstring must exist");
unsafe {
xpc_bridge_dictionary_set_string(msg, key.as_ptr(), c.as_ptr());
}
}
KeyValue::Data(d) => unsafe {
xpc_bridge_dictionary_set_data(
msg,
key.as_ptr(),
d.as_ptr() as *const c_void,
d.len(),
);
},
KeyValue::Bool(b) => unsafe {
xpc_bridge_dictionary_set_bool(msg, key.as_ptr(), *b);
},
KeyValue::Int64(v) => unsafe {
xpc_bridge_dictionary_set_int64(msg, key.as_ptr(), *v);
},
KeyValue::UInt64(v) => unsafe {
xpc_bridge_dictionary_set_uint64(msg, key.as_ptr(), *v);
},
KeyValue::Fd(v) => {
let ok = unsafe { xpc_bridge_dictionary_set_fd(msg, key.as_ptr(), *v) };
if !ok {
unsafe {
xpc_bridge_release(msg);
}
return Err(ClientError::Xpc(
"failed to set file descriptor in XPC dictionary".into(),
)
.into());
}
}
}
}
let timeout_ns = u64::try_from(timeout.as_nanos()).unwrap_or(u64::MAX);
let raw = unsafe { xpc_bridge_send_message_with_reply_sync(self.conn, msg, timeout_ns) };
unsafe {
xpc_bridge_release(msg);
}
if raw.is_null() {
return Err(ClientError::XpcTimeout.into());
}
let reply = RawReply { obj: raw };
if reply.is_error() {
return Err(ClientError::Xpc(reply.error_desc().unwrap_or_default()).into());
}
if let Some(e) = reply.json_error() {
return Err(e.into());
}
Ok(reply)
}
}
impl Drop for XpcConn {
fn drop(&mut self) {
unsafe {
xpc_bridge_connection_cancel(self.conn);
xpc_bridge_release(self.conn);
}
}
}
pub(crate) enum KeyValue {
String(String),
Data(Vec<u8>),
Bool(bool),
Int64(i64),
UInt64(u64),
Fd(std::os::fd::RawFd),
}
pub(crate) fn k(name: &'static str) -> CString {
CString::new(name).expect("key must not contain NUL")
}
pub(crate) fn id_key() -> CString {
k("id")
}
pub(crate) fn s(v: &str) -> KeyValue {
KeyValue::String(v.to_string())
}
pub(crate) fn j(v: &impl DisplayJson) -> Vec<u8> {
nojson::Json(v).to_string().into_bytes()
}
pub(crate) fn member_opt_string(item: &nojson::RawJsonValue, key: &str) -> Option<String> {
item.to_member(key)
.ok()
.and_then(|m| m.required().ok())
.and_then(|v| TryInto::<String>::try_into(v).ok())
}
pub(crate) struct Filters {
pub(crate) ids: Vec<String>,
pub(crate) labels: HashMap<String, String>,
}
impl DisplayJson for Filters {
fn fmt(&self, f: &mut nojson::JsonFormatter) -> std::fmt::Result {
f.object(|f| {
f.member("ids", &self.ids)?;
f.member("labels", &self.labels)
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn empty_reply() -> RawReply {
let obj = unsafe { xpc_bridge_create_dictionary() };
assert!(!obj.is_null(), "辞書の作成に失敗しないこと");
RawReply { obj }
}
#[test]
fn try_int64_returns_value_when_key_is_int64() {
let reply = empty_reply();
let key = CString::new("exitCode").expect("キーの作成に失敗しないこと");
unsafe {
xpc_bridge_dictionary_set_int64(reply.obj, key.as_ptr(), 42);
}
let v = reply.try_int64(&key).expect("int64 を取得できること");
assert_eq!(v, 42, "設定した int64 が返ること");
}
#[test]
fn try_int64_errors_when_key_missing() {
let reply = empty_reply();
let key = CString::new("exitCode").expect("キーの作成に失敗しないこと");
let err = reply
.try_int64(&key)
.expect_err("キー欠落はエラーであること");
let msg = err.to_string();
assert!(
msg.contains("missing or non-int64"),
"欠落を示すメッセージであること: {msg}"
);
}
#[test]
fn try_int64_errors_when_key_is_string() {
let reply = empty_reply();
let key = CString::new("exitCode").expect("キーの作成に失敗しないこと");
let val = CString::new("整数でない値").expect("値の作成に失敗しないこと");
unsafe {
xpc_bridge_dictionary_set_string(reply.obj, key.as_ptr(), val.as_ptr());
}
let err = reply
.try_int64(&key)
.expect_err("型不一致はエラーであること");
let msg = err.to_string();
assert!(
msg.contains("missing or non-int64"),
"型不一致を示すメッセージであること: {msg}"
);
}
}