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(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; 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); }
184 XpcValue::Array(arr) => {
185 out.put_u32_le(TYPE_ARRAY);
186 let len_pos = out.len();
187 out.put_u32_le(0); 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); 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); 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
264pub 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 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}