Skip to main content

ios_core/xpc/
message.rs

1//! XPC message encoder/decoder for the Apple XPC binary protocol.
2//!
3//! Reference: go-ios/ios/xpc/encoding.go
4//!
5//! Message layout (all little-endian):
6//!   magic    [4] = 0x29B00B92
7//!   flags    [4]
8//!   body_len [8]  (0 if no body)
9//!   msg_id   [8]
10//!   [if body_len > 0]:
11//!     body_magic   [4] = 0x42133742
12//!     body_version [4] = 0x00000005
13//!     <XPC value encoding>
14
15use bytes::{Buf, BufMut, Bytes, BytesMut};
16use indexmap::IndexMap;
17
18use crate::xpc::XpcError;
19
20pub const WRAPPER_MAGIC: u32 = 0x29B00B92;
21pub const OBJECT_MAGIC: u32 = 0x42133742;
22pub const BODY_VERSION: u32 = 0x00000005;
23
24/// XPC message flags.
25pub mod flags {
26    pub const ALWAYS_SET: u32 = 0x00000001;
27    pub const DATA: u32 = 0x00000100;
28    pub const DATA_PRESENT: u32 = DATA;
29    pub const HEARTBEAT_REQUEST: u32 = 0x00010000;
30    pub const WANTING_REPLY: u32 = HEARTBEAT_REQUEST;
31    pub const HEARTBEAT_REPLY: u32 = 0x00020000;
32    pub const REPLY: u32 = HEARTBEAT_REPLY;
33    pub const FILE_OPEN: u32 = 0x00100000;
34    pub const FILE_TX_STREAM_REQUEST: u32 = FILE_OPEN;
35    pub const FILE_TX_STREAM_RESPONSE: u32 = 0x00200000;
36    pub const INIT_HANDSHAKE: u32 = 0x00400000;
37}
38
39/// An XPC message.
40#[derive(Debug, Clone)]
41pub struct XpcMessage {
42    pub flags: u32,
43    pub msg_id: u64,
44    /// None when body_len == 0
45    pub body: Option<XpcValue>,
46}
47
48/// XPC value variants (matches go-ios encoding.go type constants).
49#[derive(Debug, Clone, PartialEq)]
50pub enum XpcValue {
51    Null,
52    Bool(bool),
53    Int64(i64),
54    Uint64(u64),
55    Double(f64),
56    Date(i64),
57    Data(Bytes),
58    String(String),
59    Uuid([u8; 16]),
60    Array(Vec<XpcValue>),
61    Dictionary(IndexMap<String, XpcValue>),
62    FileTransfer { msg_id: u64, data: Box<XpcValue> },
63}
64
65impl XpcValue {
66    pub fn as_str(&self) -> Option<&str> {
67        if let XpcValue::String(s) = self {
68            Some(s)
69        } else {
70            None
71        }
72    }
73    pub fn as_dict(&self) -> Option<&IndexMap<String, XpcValue>> {
74        if let XpcValue::Dictionary(d) = self {
75            Some(d)
76        } else {
77            None
78        }
79    }
80    pub fn as_uint64(&self) -> Option<u64> {
81        if let XpcValue::Uint64(n) = self {
82            Some(*n)
83        } else {
84            None
85        }
86    }
87
88    pub fn as_file_transfer(&self) -> Option<(u64, &XpcValue)> {
89        if let XpcValue::FileTransfer { msg_id, data } = self {
90            Some((*msg_id, data.as_ref()))
91        } else {
92            None
93        }
94    }
95}
96
97// ── Type codes ─────────────────────────────────────────────────────────────────
98
99const TYPE_NULL: u32 = 0x00001000;
100const TYPE_BOOL: u32 = 0x00002000;
101const TYPE_INT64: u32 = 0x00003000;
102const TYPE_UINT64: u32 = 0x00004000;
103const TYPE_DOUBLE: u32 = 0x00005000;
104const TYPE_DATE: u32 = 0x00007000;
105const TYPE_DATA: u32 = 0x00008000;
106const TYPE_STRING: u32 = 0x00009000;
107const TYPE_UUID: u32 = 0x0000A000;
108const TYPE_ARRAY: u32 = 0x0000E000;
109const TYPE_DICTIONARY: u32 = 0x0000F000;
110const TYPE_FILE_TRANSFER: u32 = 0x0001A000;
111
112// ── Encode ────────────────────────────────────────────────────────────────────
113
114/// Encode an XPC message to bytes.
115pub fn encode_message(msg: &XpcMessage) -> Result<Bytes, XpcError> {
116    let mut body_buf = BytesMut::new();
117    if let Some(body) = &msg.body {
118        body_buf.put_u32_le(OBJECT_MAGIC);
119        body_buf.put_u32_le(BODY_VERSION);
120        encode_value(body, &mut body_buf)?;
121    }
122
123    let mut out = BytesMut::new();
124    out.put_u32_le(WRAPPER_MAGIC);
125    out.put_u32_le(msg.flags);
126    out.put_u64_le(body_buf.len() as u64);
127    out.put_u64_le(msg.msg_id);
128    out.extend_from_slice(&body_buf);
129    Ok(out.freeze())
130}
131
132fn encode_value(val: &XpcValue, out: &mut BytesMut) -> Result<(), XpcError> {
133    match val {
134        XpcValue::Null => {
135            out.put_u32_le(TYPE_NULL);
136        }
137        XpcValue::Bool(b) => {
138            out.put_u32_le(TYPE_BOOL);
139            out.put_u8(if *b { 1 } else { 0 });
140            out.put_u8(0);
141            out.put_u8(0);
142            out.put_u8(0);
143        }
144        XpcValue::Int64(n) => {
145            out.put_u32_le(TYPE_INT64);
146            out.put_i64_le(*n);
147        }
148        XpcValue::Uint64(n) => {
149            out.put_u32_le(TYPE_UINT64);
150            out.put_u64_le(*n);
151        }
152        XpcValue::Double(f) => {
153            out.put_u32_le(TYPE_DOUBLE);
154            out.put_f64_le(*f);
155        }
156        XpcValue::Date(n) => {
157            out.put_u32_le(TYPE_DATE);
158            out.put_i64_le(*n);
159        }
160        XpcValue::Data(d) => {
161            out.put_u32_le(TYPE_DATA);
162            out.put_u32_le(d.len() as u32);
163            out.put_slice(d);
164            let padded = align4(d.len());
165            for _ in d.len()..padded {
166                out.put_u8(0);
167            }
168        }
169        XpcValue::String(s) => {
170            out.put_u32_le(TYPE_STRING);
171            let raw = s.as_bytes();
172            let total = raw.len() + 1; // includes null terminator
173            out.put_u32_le(total as u32);
174            out.put_slice(raw);
175            let padded = align4(total);
176            for _ in raw.len()..padded {
177                out.put_u8(0);
178            }
179        }
180        XpcValue::Uuid(u) => {
181            out.put_u32_le(TYPE_UUID);
182            out.put_slice(u); // no length field — matches Go wire format
183        }
184        XpcValue::Array(arr) => {
185            out.put_u32_le(TYPE_ARRAY);
186            let len_pos = out.len();
187            out.put_u32_le(0); // placeholder
188            let start = out.len();
189            out.put_u32_le(arr.len() as u32);
190            for v in arr {
191                encode_value(v, out)?;
192            }
193            let len_usize = out.len() - start;
194            let len = checked_collection_len("array", len_usize)?;
195            out[len_pos..len_pos + 4].copy_from_slice(&len.to_le_bytes());
196        }
197        XpcValue::Dictionary(map) => {
198            out.put_u32_le(TYPE_DICTIONARY);
199            let len_pos = out.len();
200            out.put_u32_le(0); // placeholder
201            let start = out.len();
202            out.put_u32_le(map.len() as u32);
203            for (k, v) in map {
204                encode_dict_key(k, out);
205                encode_value(v, out)?;
206            }
207            let len_usize = out.len() - start;
208            let len = checked_collection_len("dict", len_usize)?;
209            out[len_pos..len_pos + 4].copy_from_slice(&len.to_le_bytes());
210        }
211        XpcValue::FileTransfer { msg_id, data } => {
212            out.put_u32_le(TYPE_FILE_TRANSFER);
213            out.put_u64_le(*msg_id);
214            encode_value(data, out)?;
215        }
216    }
217    Ok(())
218}
219
220fn align4(n: usize) -> usize {
221    (n + 3) & !3
222}
223
224fn checked_collection_len(kind: &str, len: usize) -> Result<u32, XpcError> {
225    u32::try_from(len)
226        .map_err(|_| XpcError::Tls(format!("XPC {kind} encoded size exceeds u32::MAX: {len}")))
227}
228
229fn encode_dict_key(key: &str, out: &mut BytesMut) {
230    let raw = key.as_bytes();
231    out.put_slice(raw);
232    out.put_u8(0);
233    let total = raw.len() + 1;
234    let padded = align4(total);
235    for _ in total..padded {
236        out.put_u8(0);
237    }
238}
239
240fn decode_dict_key(buf: &mut Bytes) -> Result<String, XpcError> {
241    let nul_pos = buf
242        .iter()
243        .position(|&b| b == 0)
244        .ok_or_else(|| XpcError::Tls("XPC: unterminated dictionary key".into()))?;
245    let raw = buf.copy_to_bytes(nul_pos);
246    if buf.remaining() < 1 {
247        return Err(XpcError::Tls("XPC: dict key terminator truncated".into()));
248    }
249    buf.advance(1); // NUL terminator
250    let total = nul_pos + 1;
251    let padded = align4(total);
252    let pad = padded - total;
253    if buf.remaining() < pad {
254        return Err(XpcError::Tls("XPC: dict key padding truncated".into()));
255    }
256    if pad > 0 {
257        buf.advance(pad);
258    }
259    let s = std::str::from_utf8(&raw)
260        .map_err(|_| XpcError::Tls("XPC: invalid UTF-8 in dict key".into()))?;
261    Ok(s.to_string())
262}
263
264// ── Decode ────────────────────────────────────────────────────────────────────
265
266/// Decode an XPC message from a byte buffer.
267pub fn decode_message(mut buf: Bytes) -> Result<XpcMessage, XpcError> {
268    if buf.remaining() < 4 {
269        return Err(XpcError::Tls("XPC: buffer too short for magic".into()));
270    }
271    let magic = buf.get_u32_le();
272    if magic != WRAPPER_MAGIC {
273        return Err(XpcError::Tls(format!("XPC: bad magic 0x{magic:08X}")));
274    }
275    if buf.remaining() < 20 {
276        return Err(XpcError::Tls("XPC: buffer too short for header".into()));
277    }
278    let flags = buf.get_u32_le();
279    let body_len = buf.get_u64_le() as usize;
280    let msg_id = buf.get_u64_le();
281
282    let body = if body_len > 0 {
283        if buf.remaining() < body_len {
284            return Err(XpcError::Tls("XPC: body truncated".into()));
285        }
286        let mut body_buf = buf.copy_to_bytes(body_len);
287        // Body header is object magic followed by protocol version.
288        if body_buf.remaining() >= 8 {
289            let obj_magic = body_buf.get_u32_le();
290            if obj_magic != OBJECT_MAGIC {
291                return Err(XpcError::Tls(format!(
292                    "XPC: bad object magic 0x{obj_magic:08X}"
293                )));
294            }
295            let version = body_buf.get_u32_le();
296            if version != BODY_VERSION {
297                return Err(XpcError::Tls(format!(
298                    "XPC: bad body version 0x{version:08X}"
299                )));
300            }
301            Some(decode_value(&mut body_buf)?)
302        } else {
303            None
304        }
305    } else {
306        None
307    };
308
309    Ok(XpcMessage {
310        flags,
311        msg_id,
312        body,
313    })
314}
315
316fn decode_value(buf: &mut Bytes) -> Result<XpcValue, XpcError> {
317    if buf.remaining() < 4 {
318        return Err(XpcError::Tls("XPC: value too short".into()));
319    }
320    let type_tag = buf.get_u32_le();
321
322    match type_tag {
323        TYPE_NULL => Ok(XpcValue::Null),
324        TYPE_BOOL => {
325            if buf.remaining() < 4 {
326                return Err(XpcError::Tls("XPC: bool truncated".into()));
327            }
328            let value = buf.get_u8() != 0;
329            buf.advance(3);
330            Ok(XpcValue::Bool(value))
331        }
332        TYPE_INT64 => {
333            if buf.remaining() < 8 {
334                return Err(XpcError::Tls("XPC: i64 truncated".into()));
335            }
336            Ok(XpcValue::Int64(buf.get_i64_le()))
337        }
338        TYPE_UINT64 => {
339            if buf.remaining() < 8 {
340                return Err(XpcError::Tls("XPC: u64 truncated".into()));
341            }
342            Ok(XpcValue::Uint64(buf.get_u64_le()))
343        }
344        TYPE_DOUBLE => {
345            if buf.remaining() < 8 {
346                return Err(XpcError::Tls("XPC: f64 truncated".into()));
347            }
348            Ok(XpcValue::Double(buf.get_f64_le()))
349        }
350        TYPE_DATE => {
351            if buf.remaining() < 8 {
352                return Err(XpcError::Tls("XPC: date truncated".into()));
353            }
354            Ok(XpcValue::Date(buf.get_i64_le()))
355        }
356        TYPE_DATA => {
357            if buf.remaining() < 4 {
358                return Err(XpcError::Tls("XPC: data length truncated".into()));
359            }
360            let data_len = buf.get_u32_le() as usize;
361            let padded = align4(data_len);
362            if buf.remaining() < padded {
363                return Err(XpcError::Tls("XPC: data truncated".into()));
364            }
365            let data = buf.copy_to_bytes(data_len);
366            let pad = padded - data_len;
367            if pad > 0 {
368                buf.advance(pad);
369            }
370            Ok(XpcValue::Data(data))
371        }
372        TYPE_STRING => {
373            if buf.remaining() < 4 {
374                return Err(XpcError::Tls("XPC: string length truncated".into()));
375            }
376            let data_len = buf.get_u32_le() as usize;
377            let padded = align4(data_len);
378            if buf.remaining() < padded {
379                return Err(XpcError::Tls("XPC: string truncated".into()));
380            }
381            let raw = buf.copy_to_bytes(data_len);
382            let pad = padded - data_len;
383            if pad > 0 {
384                buf.advance(pad);
385            }
386            let end = raw.iter().position(|&b| b == 0).unwrap_or(raw.len());
387            let s = std::str::from_utf8(&raw[..end])
388                .map_err(|_| XpcError::Tls("XPC: invalid UTF-8 in string".into()))?;
389            Ok(XpcValue::String(s.to_string()))
390        }
391        TYPE_UUID => {
392            if buf.remaining() < 16 {
393                return Err(XpcError::Tls("XPC: uuid truncated".into()));
394            }
395            let mut u = [0u8; 16];
396            buf.copy_to_slice(&mut u);
397            Ok(XpcValue::Uuid(u))
398        }
399        TYPE_ARRAY => {
400            if buf.remaining() < 8 {
401                return Err(XpcError::Tls("XPC: array header truncated".into()));
402            }
403            let _data_len = buf.get_u32_le() as usize;
404            if buf.remaining() < 4 {
405                return Err(XpcError::Tls("XPC: array count truncated".into()));
406            }
407            let count = buf.get_u32_le() as usize;
408            const MAX_XPC_COLLECTION_SIZE: usize = 65536;
409            if count > MAX_XPC_COLLECTION_SIZE {
410                return Err(XpcError::Tls(format!("XPC collection too large: {count}")));
411            }
412            let mut arr = Vec::with_capacity(count.min(256));
413            for _ in 0..count {
414                arr.push(decode_value(buf)?);
415            }
416            Ok(XpcValue::Array(arr))
417        }
418        TYPE_DICTIONARY => {
419            if buf.remaining() < 8 {
420                return Err(XpcError::Tls("XPC: dict header truncated".into()));
421            }
422            let _data_len = buf.get_u32_le() as usize;
423            if buf.remaining() < 4 {
424                return Err(XpcError::Tls("XPC: dict count truncated".into()));
425            }
426            let count = buf.get_u32_le() as usize;
427            const MAX_XPC_COLLECTION_SIZE: usize = 65536;
428            if count > MAX_XPC_COLLECTION_SIZE {
429                return Err(XpcError::Tls(format!("XPC collection too large: {count}")));
430            }
431            let mut map = IndexMap::with_capacity(count.min(256));
432            for _ in 0..count {
433                let key = decode_dict_key(buf)?;
434                let val = decode_value(buf)?;
435                map.insert(key, val);
436            }
437            Ok(XpcValue::Dictionary(map))
438        }
439        TYPE_FILE_TRANSFER => {
440            if buf.remaining() < 8 {
441                return Err(XpcError::Tls("XPC: file transfer truncated".into()));
442            }
443            let msg_id = buf.get_u64_le();
444            let data = decode_value(buf)?;
445            Ok(XpcValue::FileTransfer {
446                msg_id,
447                data: Box::new(data),
448            })
449        }
450        other => Err(XpcError::Tls(format!("XPC: unknown type 0x{other:08X}"))),
451    }
452}
453
454#[cfg(test)]
455mod tests {
456    use super::*;
457
458    fn roundtrip(val: XpcValue) -> XpcValue {
459        let msg = XpcMessage {
460            flags: flags::ALWAYS_SET | flags::DATA,
461            msg_id: 1,
462            body: Some(val),
463        };
464        let bytes = encode_message(&msg).unwrap();
465        decode_message(bytes).unwrap().body.unwrap()
466    }
467
468    #[test]
469    fn test_xpc_string_roundtrip() {
470        let v = roundtrip(XpcValue::String("hello".into()));
471        assert_eq!(v.as_str(), Some("hello"));
472    }
473
474    #[test]
475    fn test_xpc_uint64_roundtrip() {
476        let v = roundtrip(XpcValue::Uint64(12345678));
477        assert_eq!(v.as_uint64(), Some(12345678));
478    }
479
480    #[test]
481    fn test_xpc_dict_roundtrip() {
482        let mut map = IndexMap::new();
483        map.insert("key1".to_string(), XpcValue::String("val1".into()));
484        map.insert("key2".to_string(), XpcValue::Uint64(99));
485        let v = roundtrip(XpcValue::Dictionary(map));
486        let d = v.as_dict().unwrap();
487        assert_eq!(d["key1"].as_str(), Some("val1"));
488        assert_eq!(d["key2"].as_uint64(), Some(99));
489    }
490
491    #[test]
492    fn test_xpc_no_body() {
493        let msg = XpcMessage {
494            flags: flags::ALWAYS_SET,
495            msg_id: 7,
496            body: None,
497        };
498        let bytes = encode_message(&msg).unwrap();
499        let decoded = decode_message(bytes).unwrap();
500        assert_eq!(decoded.msg_id, 7);
501        assert!(decoded.body.is_none());
502    }
503
504    #[test]
505    fn test_xpc_file_transfer_roundtrip() {
506        let v = roundtrip(XpcValue::FileTransfer {
507            msg_id: 9,
508            data: Box::new(XpcValue::Dictionary(IndexMap::from([(
509                "s".to_string(),
510                XpcValue::Uint64(4096),
511            )]))),
512        });
513
514        let (msg_id, data) = v.as_file_transfer().unwrap();
515        assert_eq!(msg_id, 9);
516        assert_eq!(
517            data.as_dict()
518                .and_then(|dict| dict.get("s"))
519                .and_then(XpcValue::as_uint64),
520            Some(4096)
521        );
522    }
523
524    #[test]
525    fn collection_length_rejects_values_above_u32_max() {
526        let err = checked_collection_len("array", u32::MAX as usize + 1).unwrap_err();
527        assert!(err
528            .to_string()
529            .contains("array encoded size exceeds u32::MAX"));
530    }
531}