ios_core/services/companion/
mod.rs1use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
6
7pub const SERVICE_NAME: &str = "com.apple.companion_proxy";
8
9service_error!(CompanionError);
10
11pub struct CompanionProxyClient<S> {
12 stream: S,
13}
14
15impl<S: AsyncRead + AsyncWrite + Unpin> CompanionProxyClient<S> {
16 pub fn new(stream: S) -> Self {
17 Self { stream }
18 }
19
20 pub async fn list(&mut self) -> Result<Vec<plist::Value>, CompanionError> {
21 let request = plist::Dictionary::from_iter([(
22 "Command".to_string(),
23 plist::Value::String("GetDeviceRegistry".into()),
24 )]);
25 send_plist(&mut self.stream, &plist::Value::Dictionary(request)).await?;
26 let response = recv_plist(&mut self.stream).await?;
27 Ok(response
28 .get("PairedDevicesArray")
29 .and_then(plist::Value::as_array)
30 .cloned()
31 .unwrap_or_default())
32 }
33
34 pub async fn get_value(
35 &mut self,
36 udid: &str,
37 key: &str,
38 ) -> Result<plist::Value, CompanionError> {
39 let request = plist::Dictionary::from_iter([
40 (
41 "Command".to_string(),
42 plist::Value::String("GetValueFromRegistry".into()),
43 ),
44 (
45 "GetValueGizmoUDIDKey".to_string(),
46 plist::Value::String(udid.to_string()),
47 ),
48 (
49 "GetValueKeyKey".to_string(),
50 plist::Value::String(key.to_string()),
51 ),
52 ]);
53 send_plist(&mut self.stream, &plist::Value::Dictionary(request)).await?;
54 let response = recv_plist(&mut self.stream).await?;
55 if let Some(value) = response.get("RetrievedValueDictionary") {
56 return Ok(value.clone());
57 }
58 let error = response
59 .get("Error")
60 .and_then(plist::Value::as_string)
61 .unwrap_or("missing RetrievedValueDictionary");
62 Err(CompanionError::Protocol(error.to_string()))
63 }
64}
65
66async fn send_plist<S: AsyncWrite + Unpin>(
67 stream: &mut S,
68 value: &plist::Value,
69) -> Result<(), CompanionError> {
70 let mut buf = Vec::new();
71 plist::to_writer_xml(&mut buf, value)?;
72 stream.write_all(&(buf.len() as u32).to_be_bytes()).await?;
73 stream.write_all(&buf).await?;
74 stream.flush().await?;
75 Ok(())
76}
77
78async fn recv_plist<S: AsyncRead + Unpin>(
79 stream: &mut S,
80) -> Result<plist::Dictionary, CompanionError> {
81 let mut len_buf = [0u8; 4];
82 stream.read_exact(&mut len_buf).await?;
83 let len = u32::from_be_bytes(len_buf) as usize;
84 const MAX_PLIST_SIZE: usize = 1024 * 1024;
85 if len > MAX_PLIST_SIZE {
86 return Err(CompanionError::Protocol(format!(
87 "plist length {len} exceeds max {MAX_PLIST_SIZE}"
88 )));
89 }
90 let mut buf = vec![0u8; len];
91 stream.read_exact(&mut buf).await?;
92 Ok(plist::from_bytes(&buf)?)
93}
94
95#[cfg(test)]
96mod tests {
97 use crate::test_util::MockStream;
98
99 use super::*;
100
101 #[tokio::test]
102 async fn list_sends_get_device_registry_command() {
103 let response = plist::Value::Dictionary(plist::Dictionary::from_iter([(
104 "PairedDevicesArray".to_string(),
105 plist::Value::Array(vec![plist::Value::String("watch".into())]),
106 )]));
107 let mut stream = MockStream::with_response(response);
108 let mut client = CompanionProxyClient::new(&mut stream);
109
110 let devices = client.list().await.unwrap();
111 assert_eq!(devices.len(), 1);
112
113 let len = u32::from_be_bytes(stream.written[..4].try_into().unwrap()) as usize;
114 let payload = &stream.written[4..4 + len];
115 let dict: plist::Dictionary = plist::from_bytes(payload).unwrap();
116 assert_eq!(dict["Command"].as_string(), Some("GetDeviceRegistry"));
117 }
118
119 #[tokio::test]
120 async fn get_value_sends_registry_lookup_request() {
121 let response = plist::Value::Dictionary(plist::Dictionary::from_iter([(
122 "RetrievedValueDictionary".to_string(),
123 plist::Value::String("AppleWatch".into()),
124 )]));
125 let mut stream = MockStream::with_response(response);
126 let mut client = CompanionProxyClient::new(&mut stream);
127
128 let value = client.get_value("watch-udid", "name").await.unwrap();
129 assert_eq!(value.as_string(), Some("AppleWatch"));
130
131 let len = u32::from_be_bytes(stream.written[..4].try_into().unwrap()) as usize;
132 let payload = &stream.written[4..4 + len];
133 let dict: plist::Dictionary = plist::from_bytes(payload).unwrap();
134 assert_eq!(dict["Command"].as_string(), Some("GetValueFromRegistry"));
135 assert_eq!(dict["GetValueGizmoUDIDKey"].as_string(), Some("watch-udid"));
136 assert_eq!(dict["GetValueKeyKey"].as_string(), Some("name"));
137 }
138}