Skip to main content

ios_core/services/
device_link.rs

1use serde::Serialize;
2use tokio::io::{AsyncRead, AsyncWrite};
3
4service_error!(DeviceLinkError);
5
6const MAX_PLIST_SIZE: usize = 4 * 1024 * 1024;
7
8impl From<super::plist_frame::PlistFrameError> for DeviceLinkError {
9    fn from(error: super::plist_frame::PlistFrameError) -> Self {
10        match error {
11            super::plist_frame::PlistFrameError::Io(error) => Self::Io(error),
12            super::plist_frame::PlistFrameError::Plist(error) => Self::Plist(error),
13            super::plist_frame::PlistFrameError::Protocol(message) => Self::Protocol(message),
14        }
15    }
16}
17
18pub struct DeviceLinkClient<S> {
19    stream: S,
20}
21
22impl<S> DeviceLinkClient<S> {
23    pub fn new(stream: S) -> Self {
24        Self { stream }
25    }
26
27    pub fn into_inner(self) -> S {
28        self.stream
29    }
30
31    pub fn stream_mut(&mut self) -> &mut S {
32        &mut self.stream
33    }
34}
35
36impl<S> DeviceLinkClient<S>
37where
38    S: AsyncRead + AsyncWrite + Unpin,
39{
40    pub async fn version_exchange(&mut self) -> Result<u64, DeviceLinkError> {
41        let response = self.recv_message().await?;
42        let message = response.as_array().ok_or_else(|| {
43            DeviceLinkError::Protocol(format!(
44                "device link version exchange expected array, got {response:?}"
45            ))
46        })?;
47
48        let message_type = message
49            .first()
50            .and_then(plist::Value::as_string)
51            .ok_or_else(|| {
52                DeviceLinkError::Protocol(format!(
53                    "device link version exchange missing message type: {response:?}"
54                ))
55            })?;
56        if message_type != "DLMessageVersionExchange" {
57            return Err(DeviceLinkError::Protocol(format!(
58                "expected DLMessageVersionExchange, got {message_type}"
59            )));
60        }
61
62        let version = message
63            .get(1)
64            .and_then(|value| match value {
65                plist::Value::Integer(value) => value.as_unsigned(),
66                _ => None,
67            })
68            .ok_or_else(|| {
69                DeviceLinkError::Protocol(format!(
70                    "device link version exchange missing major version: {response:?}"
71                ))
72            })?;
73
74        self.send_message(&vec![
75            plist::Value::String("DLMessageVersionExchange".into()),
76            plist::Value::String("DLVersionsOk".into()),
77            plist::Value::Integer(version.into()),
78        ])
79        .await?;
80
81        let ready = self.recv_message().await?;
82        let ready_message = ready.as_array().ok_or_else(|| {
83            DeviceLinkError::Protocol(format!("device ready expected array, got {ready:?}"))
84        })?;
85        let ready_type = ready_message
86            .first()
87            .and_then(plist::Value::as_string)
88            .ok_or_else(|| {
89                DeviceLinkError::Protocol(format!("device ready missing message type: {ready:?}"))
90            })?;
91        if ready_type != "DLMessageDeviceReady" {
92            return Err(DeviceLinkError::Protocol(format!(
93                "expected DLMessageDeviceReady, got {ready_type}"
94            )));
95        }
96
97        Ok(version)
98    }
99
100    pub async fn send_process_message<T>(&mut self, message: &T) -> Result<(), DeviceLinkError>
101    where
102        T: Serialize,
103    {
104        self.send_message(&("DLMessageProcessMessage", message))
105            .await
106    }
107
108    pub async fn recv_process_message(&mut self) -> Result<plist::Dictionary, DeviceLinkError> {
109        let response = self.recv_message().await?;
110        let message = response.as_array().ok_or_else(|| {
111            DeviceLinkError::Protocol(format!("process message expected array, got {response:?}"))
112        })?;
113
114        let message_type = message
115            .first()
116            .and_then(plist::Value::as_string)
117            .ok_or_else(|| {
118                DeviceLinkError::Protocol(format!(
119                    "process message missing message type: {response:?}"
120                ))
121            })?;
122        if message_type != "DLMessageProcessMessage" {
123            return Err(DeviceLinkError::Protocol(format!(
124                "expected DLMessageProcessMessage, got {message_type}"
125            )));
126        }
127
128        message
129            .get(1)
130            .and_then(plist::Value::as_dictionary)
131            .cloned()
132            .ok_or_else(|| {
133                DeviceLinkError::Protocol(format!(
134                    "process message missing dictionary payload: {response:?}"
135                ))
136            })
137    }
138
139    pub async fn send_message<T>(&mut self, message: &T) -> Result<(), DeviceLinkError>
140    where
141        T: Serialize,
142    {
143        super::plist_frame::write_xml_plist_frame(&mut self.stream, message, MAX_PLIST_SIZE)
144            .await
145            .map_err(DeviceLinkError::from)
146    }
147
148    pub async fn recv_message(&mut self) -> Result<plist::Value, DeviceLinkError> {
149        super::plist_frame::read_plist_frame(&mut self.stream, MAX_PLIST_SIZE)
150            .await
151            .map_err(DeviceLinkError::from)
152    }
153
154    pub async fn disconnect(&mut self) -> Result<(), DeviceLinkError> {
155        self.send_message(&vec![
156            plist::Value::String("DLMessageDisconnect".into()),
157            plist::Value::String("___EmptyParameterString___".into()),
158        ])
159        .await
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use tokio::io::{duplex, AsyncReadExt, AsyncWriteExt};
166
167    use super::*;
168
169    fn encode_frame(value: &plist::Value) -> Vec<u8> {
170        let mut payload = Vec::new();
171        plist::to_writer_xml(&mut payload, value).expect("plist serialization");
172        let mut frame = Vec::with_capacity(payload.len() + 4);
173        frame.extend_from_slice(&(payload.len() as u32).to_be_bytes());
174        frame.extend_from_slice(&payload);
175        frame
176    }
177
178    async fn read_frame(stream: &mut tokio::io::DuplexStream) -> plist::Value {
179        let mut len_buf = [0u8; 4];
180        stream.read_exact(&mut len_buf).await.expect("frame length");
181        let len = u32::from_be_bytes(len_buf) as usize;
182        let mut payload = vec![0u8; len];
183        stream
184            .read_exact(&mut payload)
185            .await
186            .expect("frame payload");
187        plist::from_bytes(&payload).expect("plist decode")
188    }
189
190    #[tokio::test]
191    async fn version_exchange_sends_versions_ok_and_returns_major_version() {
192        let (client_stream, mut server_stream) = duplex(4096);
193        let task = tokio::spawn(async move {
194            let mut client = DeviceLinkClient::new(client_stream);
195            client.version_exchange().await.unwrap()
196        });
197
198        server_stream
199            .write_all(&encode_frame(&plist::Value::Array(vec![
200                plist::Value::String("DLMessageVersionExchange".into()),
201                plist::Value::Integer(300u64.into()),
202            ])))
203            .await
204            .unwrap();
205
206        let versions_ok = read_frame(&mut server_stream).await;
207        assert_eq!(
208            versions_ok.as_array(),
209            Some(&vec![
210                plist::Value::String("DLMessageVersionExchange".into()),
211                plist::Value::String("DLVersionsOk".into()),
212                plist::Value::Integer(300u64.into()),
213            ])
214        );
215
216        server_stream
217            .write_all(&encode_frame(&plist::Value::Array(vec![
218                plist::Value::String("DLMessageDeviceReady".into()),
219            ])))
220            .await
221            .unwrap();
222
223        assert_eq!(task.await.unwrap(), 300);
224    }
225
226    #[tokio::test]
227    async fn recv_process_message_requires_dictionary_payload() {
228        let (client_stream, mut server_stream) = duplex(4096);
229        let task = tokio::spawn(async move {
230            let mut client = DeviceLinkClient::new(client_stream);
231            client.recv_process_message().await
232        });
233
234        server_stream
235            .write_all(&encode_frame(&plist::Value::Array(vec![
236                plist::Value::String("DLMessageProcessMessage".into()),
237                plist::Value::String("not-a-dict".into()),
238            ])))
239            .await
240            .unwrap();
241
242        let err = task
243            .await
244            .unwrap()
245            .expect_err("non-dictionary payload must fail");
246        assert!(err
247            .to_string()
248            .contains("process message missing dictionary payload"));
249    }
250
251    #[tokio::test]
252    async fn disconnect_sends_expected_message() {
253        let (client_stream, mut server_stream) = duplex(4096);
254        let task = tokio::spawn(async move {
255            let mut client = DeviceLinkClient::new(client_stream);
256            client.disconnect().await.unwrap();
257        });
258
259        let disconnect = read_frame(&mut server_stream).await;
260        assert_eq!(
261            disconnect.as_array(),
262            Some(&vec![
263                plist::Value::String("DLMessageDisconnect".into()),
264                plist::Value::String("___EmptyParameterString___".into()),
265            ])
266        );
267
268        task.await.unwrap();
269    }
270}