From eb37facd04815d3eed8f51bd9b1f5d1158bcd243 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Thu, 16 Oct 2025 11:00:12 -0600 Subject: [PATCH] Implement XPC macro --- idevice/src/xpc/format.rs | 2 +- idevice/src/xpc/mod.rs | 1 + idevice/src/xpc/xpc_macro.rs | 609 +++++++++++++++++++++++++++++++++++ 3 files changed, 611 insertions(+), 1 deletion(-) create mode 100644 idevice/src/xpc/xpc_macro.rs diff --git a/idevice/src/xpc/format.rs b/idevice/src/xpc/format.rs index bc67f63..2d84000 100644 --- a/idevice/src/xpc/format.rs +++ b/idevice/src/xpc/format.rs @@ -99,7 +99,7 @@ impl TryFrom for XPCType { pub type Dictionary = IndexMap; -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub enum XPCObject { Bool(bool), Dictionary(Dictionary), diff --git a/idevice/src/xpc/mod.rs b/idevice/src/xpc/mod.rs index 3b580dc..804e246 100644 --- a/idevice/src/xpc/mod.rs +++ b/idevice/src/xpc/mod.rs @@ -9,6 +9,7 @@ use crate::{IdeviceError, ReadWrite}; mod format; mod http2; +pub mod xpc_macro; use format::XPCFlag; pub use format::{Dictionary, XPCMessage, XPCObject}; diff --git a/idevice/src/xpc/xpc_macro.rs b/idevice/src/xpc/xpc_macro.rs new file mode 100644 index 0000000..56a7ed0 --- /dev/null +++ b/idevice/src/xpc/xpc_macro.rs @@ -0,0 +1,609 @@ +// Jackson Coxson +// Ported from serde's json! and the plist! macro + +/// Construct an `XPCObject` from a JSON-like literal. +/// +/// The `xpc!` macro allows you to construct `XPCObject` values with a syntax +/// similar to JSON. It supports signed and unsigned integers, which is a key +/// feature of XPC. +/// +/// ``` +/// # use my_crate::xpc; // Replace with your crate name +/// # use my_crate::xpc_macro::*; +/// # +/// let value = xpc!({ +/// "message": "hello", +/// "is_reply": true, +/// "signed_value": -42, +/// "unsigned_value": 42u64, +/// "payload": { +/// "items": [1, 2, 3], +/// "metadata": null // Becomes an empty string +/// } +/// }); +/// ``` +/// +/// ### Interpolation +/// You can interpolate variables and expressions directly into the macro. +/// Any interpolated value must implement the `XpcConvertible` trait. +/// +/// ``` +/// # use my_crate::xpc; +/// # use my_crate::xpc_macro::*; +/// # +/// let user_id = 1001u64; +/// let is_admin = false; +/// +/// let request = xpc!({ +/// "user_id": user_id, +/// "is_admin": is_admin, +/// "permissions": ["read", "write"], +/// }); +/// ``` +/// +/// ### Optional Fields and Merging +/// The macro supports optional fields using `?` and dictionary merging +/// using `:<` for cleaner construction of complex objects. +/// +/// ``` +/// # use my_crate::xpc; +/// # use my_crate::xpc_macro::*; +/// # +/// let maybe_tag: Option<&str> = Some("important"); +/// let base_config = xpc!({ "timeout": 5000u64 }); +/// +/// let message = xpc!({ +/// "message_id": "msg-123", +/// "tag":? maybe_tag, +/// :< base_config, +/// }); +/// ``` +#[macro_export] +macro_rules! xpc { + // Hide distracting implementation details from the generated rustdoc. + ($($xpc:tt)+) => { + $crate::xpc_internal!($($xpc)+) + }; +} + +#[macro_export] +#[doc(hidden)] +macro_rules! xpc_internal { + ////////////////////////////////////////////////////////////////////////// + // TT muncher for parsing the inside of an array [...]. + ////////////////////////////////////////////////////////////////////////// + + // Done with trailing comma. + (@array [$($elems:expr,)*]) => { + vec![$($elems,)*] + }; + + // Done without trailing comma. + (@array [$($elems:expr),*]) => { + vec![$($elems),*] + }; + + // Next element is `null`. + (@array [$($elems:expr,)*] null $($rest:tt)*) => { + $crate::xpc_internal!(@array [$($elems,)* $crate::xpc_internal!(null)] $($rest)*) + }; + + // Next element is `true`. + (@array [$($elems:expr,)*] true $($rest:tt)*) => { + $crate::xpc_internal!(@array [$($elems,)* $crate::xpc_internal!(true)] $($rest)*) + }; + + // Next element is `false`. + (@array [$($elems:expr,)*] false $($rest:tt)*) => { + $crate::xpc_internal!(@array [$($elems,)* $crate::xpc_internal!(false)] $($rest)*) + }; + + // Next element is an array. + (@array [$($elems:expr,)*] [$($array:tt)*] $($rest:tt)*) => { + $crate::xpc_internal!(@array [$($elems,)* $crate::xpc_internal!([$($array)*])] $($rest)*) + }; + + // Next element is a map. + (@array [$($elems:expr,)*] {$($map:tt)*} $($rest:tt)*) => { + $crate::xpc_internal!(@array [$($elems,)* $crate::xpc_internal!({$($map)*})] $($rest)*) + }; + + // Optional element + (@array [$($elems:expr,)*] ? $maybe:expr, $($rest:tt)*) => { + if let Some(__v) = $crate::xpc::xpc_macro::xpc_maybe($maybe) { + $crate::xpc_internal!(@array [$($elems,)* __v,] $($rest)*) + } else { + $crate::xpc_internal!(@array [$($elems,)*] $($rest)*) + } + }; + (@array [$($elems:expr,)*] ? $maybe:expr) => { + if let Some(__v) = $crate::xpc_macro::xpc_maybe($maybe) { + $crate::xpc_internal!(@array [$($elems,)* __v]) + } else { + $crate::xpc_internal!(@array [$($elems,)*]) + } + }; + + // Next element is an expression followed by comma. + (@array [$($elems:expr,)*] $next:expr, $($rest:tt)*) => { + $crate::xpc_internal!(@array [$($elems,)* $crate::xpc_internal!($next),] $($rest)*) + }; + + // Last element is an expression with no trailing comma. + (@array [$($elems:expr,)*] $last:expr) => { + $crate::xpc_internal!(@array [$($elems,)* $crate::xpc_internal!($last)]) + }; + + // Comma after the most recent element. + (@array [$($elems:expr),*] , $($rest:tt)*) => { + $crate::xpc_internal!(@array [$($elems,)*] $($rest)*) + }; + + // Unexpected token after most recent element. + (@array [$($elems:expr),*] $unexpected:tt $($rest:tt)*) => { + $crate::xpc_unexpected!($unexpected) + }; + + ////////////////////////////////////////////////////////////////////////// + // TT muncher for parsing the inside of an object {...}. + ////////////////////////////////////////////////////////////////////////// + + // Done. + (@object $object:ident () () ()) => {}; + + // Insert the current entry followed by trailing comma. + (@object $object:ident [$($key:tt)+] ($value:expr) , $($rest:tt)*) => { + let _ = $object.insert(($($key)+).into(), $value); + $crate::xpc_internal!(@object $object () ($($rest)*) ($($rest)*)); + }; + + // Current entry followed by unexpected token. + (@object $object:ident [$($key:tt)+] ($value:expr) $unexpected:tt $($rest:tt)*) => { + $crate::xpc_unexpected!($unexpected); + }; + + // Insert the last entry without trailing comma. + (@object $object:ident [$($key:tt)+] ($value:expr)) => { + let _ = $object.insert(($($key)+).into(), $value); + }; + + // Next value is `null`. + (@object $object:ident ($($key:tt)+) (: null $($rest:tt)*) $copy:tt) => { + $crate::xpc_internal!(@object $object [$($key)+] ($crate::xpc_internal!(null)) $($rest)*); + }; + + // Next value is `true`. + (@object $object:ident ($($key:tt)+) (: true $($rest:tt)*) $copy:tt) => { + $crate::xpc_internal!(@object $object [$($key)+] ($crate::xpc_internal!(true)) $($rest)*); + }; + + // Next value is `false`. + (@object $object:ident ($($key:tt)+) (: false $($rest:tt)*) $copy:tt) => { + $crate::xpc_internal!(@object $object [$($key)+] ($crate::xpc_internal!(false)) $($rest)*); + }; + + // Next value is an array. + (@object $object:ident ($($key:tt)+) (: [$($array:tt)*] $($rest:tt)*) $copy:tt) => { + $crate::xpc_internal!(@object $object [$($key)+] ($crate::xpc_internal!([$($array)*])) $($rest)*); + }; + + // Next value is a map. + (@object $object:ident ($($key:tt)+) (: {$($map:tt)*} $($rest:tt)*) $copy:tt) => { + $crate::xpc_internal!(@object $object [$($key)+] ($crate::xpc_internal!({$($map)*})) $($rest)*); + }; + + // Optional insert: `key:? value` + (@object $object:ident ($($key:tt)+) (:? $value:expr, $($rest:tt)*) $copy:tt) => { + if let Some(__v) = $crate::xpc::xpc_macro::xpc_maybe($value) { + let _ = $object.insert(($($key)+).into(), __v); + } + $crate::xpc_internal!(@object $object () ($($rest)*) ($($rest)*)); + }; + (@object $object:ident ($($key:tt)+) (:? $value:expr) $copy:tt) => { + if let Some(__v) = $crate::xpc_macro::xpc_maybe($value) { + let _ = $object.insert(($($key)+).into(), __v); + } + }; + + // Merge: `:< value` + (@object $object:ident () (:< $value:expr, $($rest:tt)*) $copy:tt) => { + { + let __v = $crate::xpc_internal!($value); + let __dict = $crate::xpc::xpc_macro::IntoXpcDict::into_xpc_dict(__v); + for (__k, __val) in __dict { + let _ = $object.insert(__k, __val); + } + } + $crate::xpc_internal!(@object $object () ($($rest)*) ($($rest)*)); + }; + (@object $object:ident () (:< $value:expr) $copy:tt) => { + { + let __v = $crate::xpc_internal!($value); + let __dict = $crate::xpc_macro::IntoXpcDict::into_xpc_dict(__v); + for (__k, __val) in __dict { + let _ = $object.insert(__k, __val); + } + } + }; + + // Optional merge: `: { + if let Some(__dict) = $crate::xpc::xpc_macro::maybe_into_xpc_dict($value) { + for (__k, __val) in __dict { + let _ = $object.insert(__k, __val); + } + } + $crate::xpc_internal!(@object $object () ($($rest)*) ($($rest)*)); + }; + (@object $object:ident () (:< ? $value:expr) $copy:tt) => { + if let Some(__dict) = $crate::xpc_macro::maybe_into_xpc_dict($value) { + for (__k, __val) in __dict { + let _ = $object.insert(__k, __val); + } + } + }; + + // Next value is an expression followed by comma. + (@object $object:ident ($($key:tt)+) (: $value:expr, $($rest:tt)*) $copy:tt) => { + $crate::xpc_internal!(@object $object [$($key)+] ($crate::xpc_internal!($value)), $($rest)*); + }; + + // Last value is an expression with no trailing comma. + (@object $object:ident ($($key:tt)+) (: $value:expr) $copy:tt) => { + $crate::xpc_internal!(@object $object [$($key)+] ($crate::xpc_internal!($value))); + }; + + // Missing value for last entry. + (@object $object:ident ($($key:tt)+) (:) $copy:tt) => { + $crate::xpc_internal!(); + }; + + // Missing colon and value for last entry. + (@object $object:ident ($($key:tt)+) () $copy:tt) => { + $crate::xpc_internal!(); + }; + + // Misplaced colon. + (@object $object:ident () (: $($rest:tt)*) ($colon:tt $($copy:tt)*)) => { + $crate::xpc_unexpected!($colon); + }; + + // Munch a token into the current key. + (@object $object:ident ($($key:tt)*) ($tt:tt $($rest:tt)*) $copy:tt) => { + $crate::xpc_internal!(@object $object ($($key)* $tt) ($($rest)*) ($($rest)*)); + }; + + ////////////////////////////////////////////////////////////////////////// + // The main implementation. + ////////////////////////////////////////////////////////////////////////// + + (null) => { + // XPC does not have a native null type. We'll use an empty string as a convention. + $crate::xpc::XPCObject::String("".to_string()) + }; + + (true) => { + $crate::xpc::XPCObject::Bool(true) + }; + + (false) => { + $crate::xpc::XPCObject::Bool(false) + }; + + ([ $($tt:tt)+ ]) => { + $crate::xpc::XPCObject::Array($crate::xpc_internal!(@array [] $($tt)+)) + }; + + ({ $($tt:tt)+ }) => { + $crate::xpc::XPCObject::Dictionary({ + let mut object = $crate::xpc::Dictionary::new(); + $crate::xpc_internal!(@object object () ($($tt)+) ($($tt)+)); + object + }) + }; + + // Any other expression that implements XpcConvertible. + // Must be below every other rule. + ($other:expr) => { + $crate::xpc::xpc_macro::xpc_to_value($other) + }; +} + +#[macro_export] +#[doc(hidden)] +macro_rules! xpc_unexpected { + () => {}; +} + +/// Conversion traits and helper functions for the `xpc!` macro. +use crate::xpc::{Dictionary, XPCObject}; +use std::collections::{BTreeMap, HashMap}; + +// Convert various types to XPCObject +pub trait XpcConvertible { + fn to_xpc_value(self) -> XPCObject; +} + +#[doc(hidden)] +pub fn xpc_to_value(value: T) -> XPCObject { + value.to_xpc_value() +} + +// Base implementation +impl XpcConvertible for XPCObject { + fn to_xpc_value(self) -> XPCObject { + self + } +} + +// Primitive implementations +impl XpcConvertible for String { + fn to_xpc_value(self) -> XPCObject { + XPCObject::String(self) + } +} +impl XpcConvertible for &str { + fn to_xpc_value(self) -> XPCObject { + XPCObject::String(self.to_string()) + } +} +impl XpcConvertible for bool { + fn to_xpc_value(self) -> XPCObject { + XPCObject::Bool(self) + } +} + +// Signed integer implementations +macro_rules! impl_xpc_convertible_for_signed { + ($($t:ty),*) => { + $(impl XpcConvertible for $t { + fn to_xpc_value(self) -> XPCObject { XPCObject::Int64(self as i64) } + })* + }; + } +impl_xpc_convertible_for_signed!(i8, i16, i32, i64, isize); + +// Unsigned integer implementations +macro_rules! impl_xpc_convertible_for_unsigned { + ($($t:ty),*) => { + $(impl XpcConvertible for $t { + fn to_xpc_value(self) -> XPCObject { XPCObject::UInt64(self as u64) } + })* + }; + } +impl_xpc_convertible_for_unsigned!(u16, u32, u64, usize); + +// Floating point implementations +impl XpcConvertible for f32 { + fn to_xpc_value(self) -> XPCObject { + XPCObject::Double(self as f64) + } +} +impl XpcConvertible for f64 { + fn to_xpc_value(self) -> XPCObject { + XPCObject::Double(self) + } +} + +// Other XPC-specific types +impl XpcConvertible for Vec { + fn to_xpc_value(self) -> XPCObject { + XPCObject::Data(self) + } +} +impl XpcConvertible for &[u8] { + fn to_xpc_value(self) -> XPCObject { + XPCObject::Data(self.to_vec()) + } +} +impl XpcConvertible for uuid::Uuid { + fn to_xpc_value(self) -> XPCObject { + XPCObject::Uuid(self) + } +} +impl XpcConvertible for std::time::SystemTime { + fn to_xpc_value(self) -> XPCObject { + XPCObject::Date(self) + } +} + +// Collection implementations +impl XpcConvertible for Vec { + fn to_xpc_value(self) -> XPCObject { + XPCObject::Array(self.into_iter().map(XpcConvertible::to_xpc_value).collect()) + } +} +impl XpcConvertible for &[T] { + fn to_xpc_value(self) -> XPCObject { + XPCObject::Array( + self.iter() + .cloned() + .map(XpcConvertible::to_xpc_value) + .collect(), + ) + } +} +impl XpcConvertible for HashMap +where + K: Into, + V: XpcConvertible, +{ + fn to_xpc_value(self) -> XPCObject { + XPCObject::Dictionary( + self.into_iter() + .map(|(k, v)| (k.into(), v.to_xpc_value())) + .collect(), + ) + } +} +impl XpcConvertible for BTreeMap +where + K: Into, + V: XpcConvertible, +{ + fn to_xpc_value(self) -> XPCObject { + XPCObject::Dictionary( + self.into_iter() + .map(|(k, v)| (k.into(), v.to_xpc_value())) + .collect(), + ) + } +} + +// Optional value handling (for `key:? value`) +pub trait MaybeXpc { + fn into_option_xpc(self) -> Option; +} +impl MaybeXpc for T { + fn into_option_xpc(self) -> Option { + Some(self.to_xpc_value()) + } +} +impl MaybeXpc for Option { + fn into_option_xpc(self) -> Option { + self.map(XpcConvertible::to_xpc_value) + } +} + +#[doc(hidden)] +pub fn xpc_maybe(v: T) -> Option { + v.into_option_xpc() +} + +// Dictionary merging (for `:< dict`) +pub trait IntoXpcDict { + fn into_xpc_dict(self) -> Dictionary; +} +impl IntoXpcDict for Dictionary { + fn into_xpc_dict(self) -> Dictionary { + self + } +} +impl IntoXpcDict for XPCObject { + fn into_xpc_dict(self) -> Dictionary { + match self { + XPCObject::Dictionary(d) => d, + other => panic!( + "xpc! macro merge `:<` expects a dictionary, found {:?}", + other + ), + } + } +} +impl IntoXpcDict for HashMap +where + K: Into, + V: XpcConvertible, +{ + fn into_xpc_dict(self) -> Dictionary { + self.into_iter() + .map(|(k, v)| (k.into(), v.to_xpc_value())) + .collect() + } +} + +// Optional dictionary merging (for `: Option; +} +impl MaybeIntoXpcDict for T { + fn into_option_xpc_dict(self) -> Option { + Some(self.into_xpc_dict()) + } +} +impl MaybeIntoXpcDict for Option { + fn into_option_xpc_dict(self) -> Option { + self.map(IntoXpcDict::into_xpc_dict) + } +} + +#[doc(hidden)] +pub fn maybe_into_xpc_dict(v: T) -> Option { + v.into_option_xpc_dict() +} + +#[cfg(test)] +mod tests { + use crate::xpc::{Dictionary, XPCObject}; + use uuid::Uuid; + + #[test] + fn test_xpc_macro_primitives() { + assert_eq!(xpc!(null), XPCObject::String("".to_string())); + assert_eq!(xpc!(true), XPCObject::Bool(true)); + assert_eq!(xpc!(-123), XPCObject::Int64(-123)); + assert_eq!(xpc!(123), XPCObject::Int64(123)); + assert_eq!(xpc!(123u32), XPCObject::UInt64(123)); + assert_eq!(xpc!(123u64), XPCObject::UInt64(123)); + assert_eq!(xpc!(123.45), XPCObject::Double(123.45)); + assert_eq!(xpc!("hello"), XPCObject::String("hello".to_string())); + } + + #[test] + fn test_xpc_macro_collections() { + let arr = xpc!([1, "two", true]); + match arr { + XPCObject::Array(vec) => { + assert_eq!(vec.len(), 3); + assert_eq!(vec[0], XPCObject::Int64(1)); + assert_eq!(vec[1], XPCObject::String("two".to_string())); + assert_eq!(vec[2], XPCObject::Bool(true)); + } + _ => panic!("Expected array"), + } + + let dict = xpc!({ + "key1": 1u64, + "key2": "value2" + }); + match dict { + XPCObject::Dictionary(map) => { + assert_eq!(map.len(), 2); + assert_eq!(map.get("key1"), Some(&XPCObject::UInt64(1))); + assert_eq!( + map.get("key2"), + Some(&XPCObject::String("value2".to_string())) + ); + } + _ => panic!("Expected dictionary"), + } + } + + #[test] + fn test_xpc_macro_interpolation_and_optional() { + let my_uuid = Uuid::new_v4(); + let optional_field: Option = None; + let present_field = Some("I'm here"); + let base = xpc!({ "base_field": true }); + let optional_base: Option = None; + + let obj = xpc!({ + "id": my_uuid, + "optional_field":? optional_field, + "present_field":? present_field, + :< base, + :, 3], + }); + + if let XPCObject::Dictionary(dict) = obj { + assert_eq!(dict.get("id"), Some(&XPCObject::Uuid(my_uuid))); + assert!(dict.get("optional_field").is_none()); + assert_eq!( + dict.get("present_field"), + Some(&XPCObject::String("I'm here".to_string())) + ); + assert_eq!(dict.get("base_field"), Some(&XPCObject::Bool(true))); + + if let Some(XPCObject::Array(arr)) = dict.get("arr") { + assert_eq!(arr.len(), 2); + assert_eq!(arr[0], XPCObject::Int64(1)); + assert_eq!(arr[1], XPCObject::Int64(3)); + } else { + panic!("Expected array for 'arr' key"); + } + } else { + panic!("Expected dictionary"); + } + } +}