1use 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
24pub 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#[derive(Debug, Clone)]
41pub struct XpcMessage {
42 pub flags: u32,
43 pub msg_id: u64,
44 pub body: Option<XpcValue>,
46}
47
48#[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
97const 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
112pub 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); }
187 XpcValue::Array(arr) => {
188 out.put_u32_le(TYPE_ARRAY);
189 let len_pos = out.len();
190 out.put_u32_le(0); 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); 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); 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
287pub 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 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#[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}