Skip to main content

ios_core/services/companion/
mod.rs

1//! Companion proxy service client.
2//!
3//! Service: `com.apple.companion_proxy`
4
5use 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}