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(checked_u64_len("body", body_buf.len())?);
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(checked_u32_len("data", d.len())?);
163            out.put_slice(d);
164            let padded = checked_align4("data", 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
173                .len()
174                .checked_add(1)
175                .ok_or_else(|| XpcError::Tls("XPC string length overflow".to_string()))?;
176            out.put_u32_le(checked_u32_len("string", total)?);
177            out.put_slice(raw);
178            let padded = checked_align4("string", total)?;
179            for _ in raw.len()..padded {
180                out.put_u8(0);
181            }
182        }
183        XpcValue::Uuid(u) => {
184            out.put_u32_le(TYPE_UUID);
185            out.put_slice(u); // no length field — matches Go wire format
186        }
187        XpcValue::Array(arr) => {
188            out.put_u32_le(TYPE_ARRAY);
189            let len_pos = out.len();
190            out.put_u32_le(0); // placeholder
191            let start = out.len();
192            out.put_u32_le(checked_u32_len("array count", arr.len())?);
193            for v in arr {
194                encode_value(v, out)?;
195            }
196            let len_usize = out.len() - start;
197            let len = checked_collection_len("array", len_usize)?;
198            out[len_pos..len_pos + 4].copy_from_slice(&len.to_le_bytes());
199        }
200        XpcValue::Dictionary(map) => {
201            out.put_u32_le(TYPE_DICTIONARY);
202            let len_pos = out.len();
203            out.put_u32_le(0); // placeholder
204            let start = out.len();
205            out.put_u32_le(checked_u32_len("dict count", map.len())?);
206            for (k, v) in map {
207                encode_dict_key(k, out)?;
208                encode_value(v, out)?;
209            }
210            let len_usize = out.len() - start;
211            let len = checked_collection_len("dict", len_usize)?;
212            out[len_pos..len_pos + 4].copy_from_slice(&len.to_le_bytes());
213        }
214        XpcValue::FileTransfer { msg_id, data } => {
215            out.put_u32_le(TYPE_FILE_TRANSFER);
216            out.put_u64_le(*msg_id);
217            encode_value(data, out)?;
218        }
219    }
220    Ok(())
221}
222
223fn align4(n: usize) -> usize {
224    (n + 3) & !3
225}
226
227fn checked_collection_len(kind: &str, len: usize) -> Result<u32, XpcError> {
228    u32::try_from(len)
229        .map_err(|_| XpcError::Tls(format!("XPC {kind} encoded size exceeds u32::MAX: {len}")))
230}
231
232fn checked_u32_len(kind: &str, len: usize) -> Result<u32, XpcError> {
233    u32::try_from(len)
234        .map_err(|_| XpcError::Tls(format!("XPC {kind} length exceeds u32::MAX: {len}")))
235}
236
237fn checked_u64_len(kind: &str, len: usize) -> Result<u64, XpcError> {
238    u64::try_from(len)
239        .map_err(|_| XpcError::Tls(format!("XPC {kind} length exceeds u64::MAX: {len}")))
240}
241
242fn checked_align4(kind: &str, len: usize) -> Result<usize, XpcError> {
243    len.checked_add(3)
244        .map(|value| value & !3)
245        .ok_or_else(|| XpcError::Tls(format!("XPC {kind} padded length overflow: {len}")))
246}
247
248fn encode_dict_key(key: &str, out: &mut BytesMut) -> Result<(), XpcError> {
249    let raw = key.as_bytes();
250    out.put_slice(raw);
251    out.put_u8(0);
252    let total = raw
253        .len()
254        .checked_add(1)
255        .ok_or_else(|| XpcError::Tls("XPC dict key length overflow".to_string()))?;
256    let padded = checked_align4("dict key", total)?;
257    for _ in total..padded {
258        out.put_u8(0);
259    }
260    Ok(())
261}
262
263fn decode_dict_key(buf: &mut Bytes) -> Result<String, XpcError> {
264    let nul_pos = buf
265        .iter()
266        .position(|&b| b == 0)
267        .ok_or_else(|| XpcError::Tls("XPC: unterminated dictionary key".into()))?;
268    let raw = buf.copy_to_bytes(nul_pos);
269    if buf.remaining() < 1 {
270        return Err(XpcError::Tls("XPC: dict key terminator truncated".into()));
271    }
272    buf.advance(1); // NUL terminator
273    let total = nul_pos + 1;
274    let padded = align4(total);
275    let pad = padded - total;
276    if buf.remaining() < pad {
277        return Err(XpcError::Tls("XPC: dict key padding truncated".into()));
278    }
279    if pad > 0 {
280        buf.advance(pad);
281    }
282    let s = std::str::from_utf8(&raw)
283        .map_err(|_| XpcError::Tls("XPC: invalid UTF-8 in dict key".into()))?;
284    Ok(s.to_string())
285}
286
287// ── Decode ────────────────────────────────────────────────────────────────────
288
289/// Decode an XPC message from a byte buffer.
290pub fn decode_message(mut buf: Bytes) -> Result<XpcMessage, XpcError> {
291    if buf.remaining() < 4 {
292        return Err(XpcError::Tls("XPC: buffer too short for magic".into()));
293    }
294    let magic = buf.get_u32_le();
295    if magic != WRAPPER_MAGIC {
296        return Err(XpcError::Tls(format!("XPC: bad magic 0x{magic:08X}")));
297    }
298    if buf.remaining() < 20 {
299        return Err(XpcError::Tls("XPC: buffer too short for header".into()));
300    }
301    let flags = buf.get_u32_le();
302    let body_len = buf.get_u64_le() as usize;
303    let msg_id = buf.get_u64_le();
304
305    let body = if body_len > 0 {
306        if buf.remaining() < body_len {
307            return Err(XpcError::Tls("XPC: body truncated".into()));
308        }
309        let mut body_buf = buf.copy_to_bytes(body_len);
310        // Body header is object magic followed by protocol version.
311        if body_buf.remaining() >= 8 {
312            let obj_magic = body_buf.get_u32_le();
313            if obj_magic != OBJECT_MAGIC {
314                return Err(XpcError::Tls(format!(
315                    "XPC: bad object magic 0x{obj_magic:08X}"
316                )));
317            }
318            let version = body_buf.get_u32_le();
319            if version != BODY_VERSION {
320                return Err(XpcError::Tls(format!(
321                    "XPC: bad body version 0x{version:08X}"
322                )));
323            }
324            Some(decode_value(&mut body_buf)?)
325        } else {
326            None
327        }
328    } else {
329        None
330    };
331
332    Ok(XpcMessage {
333        flags,
334        msg_id,
335        body,
336    })
337}
338
339/// Incrementally reassembles complete XPC messages from DATA frame payloads.
340#[derive(Debug, Default)]
341pub(crate) struct XpcMessageBuffer {
342    pending: BytesMut,
343}
344
345impl XpcMessageBuffer {
346    pub(crate) fn new() -> Self {
347        Self {
348            pending: BytesMut::new(),
349        }
350    }
351
352    pub(crate) fn push(&mut self, bytes: &[u8]) {
353        self.pending.extend_from_slice(bytes);
354    }
355
356    pub(crate) fn try_next(&mut self) -> Result<Option<XpcMessage>, XpcError> {
357        if self.pending.len() < 24 {
358            return Ok(None);
359        }
360
361        let body_len = u64::from_le_bytes(
362            self.pending[8..16]
363                .try_into()
364                .map_err(|_| XpcError::Tls("XPC: invalid wrapper header".into()))?,
365        ) as usize;
366        let total_len = 24usize
367            .checked_add(body_len)
368            .ok_or_else(|| XpcError::Tls("XPC: message length overflow".into()))?;
369        if self.pending.len() < total_len {
370            return Ok(None);
371        }
372
373        let payload = self.pending.split_to(total_len).freeze();
374        decode_message(payload).map(Some)
375    }
376}
377
378fn decode_value(buf: &mut Bytes) -> Result<XpcValue, XpcError> {
379    if buf.remaining() < 4 {
380        return Err(XpcError::Tls("XPC: value too short".into()));
381    }
382    let type_tag = buf.get_u32_le();
383
384    match type_tag {
385        TYPE_NULL => Ok(XpcValue::Null),
386        TYPE_BOOL => {
387            if buf.remaining() < 4 {
388                return Err(XpcError::Tls("XPC: bool truncated".into()));
389            }
390            let value = buf.get_u8() != 0;
391            buf.advance(3);
392            Ok(XpcValue::Bool(value))
393        }
394        TYPE_INT64 => {
395            if buf.remaining() < 8 {
396                return Err(XpcError::Tls("XPC: i64 truncated".into()));
397            }
398            Ok(XpcValue::Int64(buf.get_i64_le()))
399        }
400        TYPE_UINT64 => {
401            if buf.remaining() < 8 {
402                return Err(XpcError::Tls("XPC: u64 truncated".into()));
403            }
404            Ok(XpcValue::Uint64(buf.get_u64_le()))
405        }
406        TYPE_DOUBLE => {
407            if buf.remaining() < 8 {
408                return Err(XpcError::Tls("XPC: f64 truncated".into()));
409            }
410            Ok(XpcValue::Double(buf.get_f64_le()))
411        }
412        TYPE_DATE => {
413            if buf.remaining() < 8 {
414                return Err(XpcError::Tls("XPC: date truncated".into()));
415            }
416            Ok(XpcValue::Date(buf.get_i64_le()))
417        }
418        TYPE_DATA => {
419            if buf.remaining() < 4 {
420                return Err(XpcError::Tls("XPC: data length truncated".into()));
421            }
422            let data_len = buf.get_u32_le() as usize;
423            let padded = align4(data_len);
424            if buf.remaining() < padded {
425                return Err(XpcError::Tls("XPC: data truncated".into()));
426            }
427            let data = buf.copy_to_bytes(data_len);
428            let pad = padded - data_len;
429            if pad > 0 {
430                buf.advance(pad);
431            }
432            Ok(XpcValue::Data(data))
433        }
434        TYPE_STRING => {
435            if buf.remaining() < 4 {
436                return Err(XpcError::Tls("XPC: string length truncated".into()));
437            }
438            let data_len = buf.get_u32_le() as usize;
439            let padded = align4(data_len);
440            if buf.remaining() < padded {
441                return Err(XpcError::Tls("XPC: string truncated".into()));
442            }
443            let raw = buf.copy_to_bytes(data_len);
444            let pad = padded - data_len;
445            if pad > 0 {
446                buf.advance(pad);
447            }
448            let end = raw.iter().position(|&b| b == 0).unwrap_or(raw.len());
449            let s = std::str::from_utf8(&raw[..end])
450                .map_err(|_| XpcError::Tls("XPC: invalid UTF-8 in string".into()))?;
451            Ok(XpcValue::String(s.to_string()))
452        }
453        TYPE_UUID => {
454            if buf.remaining() < 16 {
455                return Err(XpcError::Tls("XPC: uuid truncated".into()));
456            }
457            let mut u = [0u8; 16];
458            buf.copy_to_slice(&mut u);
459            Ok(XpcValue::Uuid(u))
460        }
461        TYPE_ARRAY => {
462            if buf.remaining() < 8 {
463                return Err(XpcError::Tls("XPC: array header truncated".into()));
464            }
465            let _data_len = buf.get_u32_le() as usize;
466            if buf.remaining() < 4 {
467                return Err(XpcError::Tls("XPC: array count truncated".into()));
468            }
469            let count = buf.get_u32_le() as usize;
470            const MAX_XPC_COLLECTION_SIZE: usize = 65536;
471            if count > MAX_XPC_COLLECTION_SIZE {
472                return Err(XpcError::Tls(format!("XPC collection too large: {count}")));
473            }
474            let mut arr = Vec::with_capacity(count.min(256));
475            for _ in 0..count {
476                arr.push(decode_value(buf)?);
477            }
478            Ok(XpcValue::Array(arr))
479        }
480        TYPE_DICTIONARY => {
481            if buf.remaining() < 8 {
482                return Err(XpcError::Tls("XPC: dict header truncated".into()));
483            }
484            let _data_len = buf.get_u32_le() as usize;
485            if buf.remaining() < 4 {
486                return Err(XpcError::Tls("XPC: dict count truncated".into()));
487            }
488            let count = buf.get_u32_le() as usize;
489            const MAX_XPC_COLLECTION_SIZE: usize = 65536;
490            if count > MAX_XPC_COLLECTION_SIZE {
491                return Err(XpcError::Tls(format!("XPC collection too large: {count}")));
492            }
493            let mut map = IndexMap::with_capacity(count.min(256));
494            for _ in 0..count {
495                let key = decode_dict_key(buf)?;
496                let val = decode_value(buf)?;
497                map.insert(key, val);
498            }
499            Ok(XpcValue::Dictionary(map))
500        }
501        TYPE_FILE_TRANSFER => {
502            if buf.remaining() < 8 {
503                return Err(XpcError::Tls("XPC: file transfer truncated".into()));
504            }
505            let msg_id = buf.get_u64_le();
506            let data = decode_value(buf)?;
507            Ok(XpcValue::FileTransfer {
508                msg_id,
509                data: Box::new(data),
510            })
511        }
512        other => Err(XpcError::Tls(format!("XPC: unknown type 0x{other:08X}"))),
513    }
514}
515
516#[cfg(test)]
517mod tests {
518    use super::*;
519
520    fn roundtrip(val: XpcValue) -> XpcValue {
521        let msg = XpcMessage {
522            flags: flags::ALWAYS_SET | flags::DATA,
523            msg_id: 1,
524            body: Some(val),
525        };
526        let bytes = encode_message(&msg).unwrap();
527        decode_message(bytes).unwrap().body.unwrap()
528    }
529
530    #[test]
531    fn test_xpc_string_roundtrip() {
532        let v = roundtrip(XpcValue::String("hello".into()));
533        assert_eq!(v.as_str(), Some("hello"));
534    }
535
536    #[test]
537    fn test_xpc_uint64_roundtrip() {
538        let v = roundtrip(XpcValue::Uint64(12345678));
539        assert_eq!(v.as_uint64(), Some(12345678));
540    }
541
542    #[test]
543    fn test_xpc_dict_roundtrip() {
544        let mut map = IndexMap::new();
545        map.insert("key1".to_string(), XpcValue::String("val1".into()));
546        map.insert("key2".to_string(), XpcValue::Uint64(99));
547        let v = roundtrip(XpcValue::Dictionary(map));
548        let d = v.as_dict().unwrap();
549        assert_eq!(d["key1"].as_str(), Some("val1"));
550        assert_eq!(d["key2"].as_uint64(), Some(99));
551    }
552
553    #[test]
554    fn test_xpc_no_body() {
555        let msg = XpcMessage {
556            flags: flags::ALWAYS_SET,
557            msg_id: 7,
558            body: None,
559        };
560        let bytes = encode_message(&msg).unwrap();
561        let decoded = decode_message(bytes).unwrap();
562        assert_eq!(decoded.msg_id, 7);
563        assert!(decoded.body.is_none());
564    }
565
566    #[test]
567    fn test_xpc_file_transfer_roundtrip() {
568        let v = roundtrip(XpcValue::FileTransfer {
569            msg_id: 9,
570            data: Box::new(XpcValue::Dictionary(IndexMap::from([(
571                "s".to_string(),
572                XpcValue::Uint64(4096),
573            )]))),
574        });
575
576        let (msg_id, data) = v.as_file_transfer().unwrap();
577        assert_eq!(msg_id, 9);
578        assert_eq!(
579            data.as_dict()
580                .and_then(|dict| dict.get("s"))
581                .and_then(XpcValue::as_uint64),
582            Some(4096)
583        );
584    }
585
586    #[test]
587    fn collection_length_rejects_values_above_u32_max() {
588        let err = checked_collection_len("array", u32::MAX as usize + 1).unwrap_err();
589        assert!(err
590            .to_string()
591            .contains("array encoded size exceeds u32::MAX"));
592    }
593
594    #[test]
595    fn checked_xpc_u32_len_rejects_values_above_u32_max() {
596        let err = checked_u32_len("data", u32::MAX as usize + 1).unwrap_err();
597        assert!(err.to_string().contains("data length exceeds u32::MAX"));
598    }
599
600    #[test]
601    fn message_buffer_reassembles_fragmented_messages() {
602        let msg1 = XpcMessage {
603            flags: flags::ALWAYS_SET | flags::DATA,
604            msg_id: 1,
605            body: Some(XpcValue::String("one".into())),
606        };
607        let msg2 = XpcMessage {
608            flags: flags::ALWAYS_SET | flags::DATA,
609            msg_id: 2,
610            body: Some(XpcValue::String("two".into())),
611        };
612        let bytes1 = encode_message(&msg1).unwrap();
613        let bytes2 = encode_message(&msg2).unwrap();
614        let mut buffer = XpcMessageBuffer::new();
615
616        buffer.push(&bytes1[..10]);
617        assert!(buffer.try_next().unwrap().is_none());
618
619        buffer.push(&bytes1[10..]);
620        buffer.push(&bytes2);
621
622        assert_eq!(buffer.try_next().unwrap().unwrap().msg_id, 1);
623        assert_eq!(buffer.try_next().unwrap().unwrap().msg_id, 2);
624        assert!(buffer.try_next().unwrap().is_none());
625    }
626}