Skip to main content

ios_core/services/idam/
mod.rs

1//! IDAM (Inter-Device Audio and MIDI) service client.
2//!
3//! Service: `com.apple.idamd`
4
5use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
6
7pub const SERVICE_NAME: &str = "com.apple.idamd";
8pub const RSD_SERVICE_NAME: &str = "com.apple.idamd.shim.remote";
9
10#[derive(Debug, thiserror::Error)]
11pub enum IdamError {
12    #[error("IO error: {0}")]
13    Io(#[from] std::io::Error),
14    #[error("plist error: {0}")]
15    Plist(String),
16    #[error("protocol error: {0}")]
17    Protocol(String),
18}
19
20pub struct IdamClient<S> {
21    stream: S,
22}
23
24impl<S: AsyncRead + AsyncWrite + Unpin> IdamClient<S> {
25    pub fn new(stream: S) -> Self {
26        Self { stream }
27    }
28
29    pub async fn configuration_inquiry(&mut self) -> Result<plist::Value, IdamError> {
30        let request = plist::Dictionary::from_iter([(
31            "Configuration Inquiry".to_string(),
32            plist::Value::Boolean(true),
33        )]);
34        send_plist(&mut self.stream, &plist::Value::Dictionary(request)).await?;
35        Ok(plist::Value::Dictionary(
36            recv_plist(&mut self.stream).await?,
37        ))
38    }
39
40    pub async fn set_configuration(&mut self, enabled: bool) -> Result<plist::Value, IdamError> {
41        let request = plist::Dictionary::from_iter([(
42            "Set IDAM Configuration".to_string(),
43            plist::Value::Boolean(enabled),
44        )]);
45        send_plist(&mut self.stream, &plist::Value::Dictionary(request)).await?;
46        match recv_plist(&mut self.stream).await {
47            Ok(response) => Ok(plist::Value::Dictionary(response)),
48            Err(IdamError::Io(err)) if err.kind() == std::io::ErrorKind::UnexpectedEof => {
49                Ok(plist::Value::Dictionary(plist::Dictionary::new()))
50            }
51            Err(err) => Err(err),
52        }
53    }
54}
55
56async fn send_plist<S: AsyncWrite + Unpin>(
57    stream: &mut S,
58    value: &plist::Value,
59) -> Result<(), IdamError> {
60    let mut buf = Vec::new();
61    plist::to_writer_xml(&mut buf, value).map_err(|e| IdamError::Plist(e.to_string()))?;
62    stream.write_all(&(buf.len() as u32).to_be_bytes()).await?;
63    stream.write_all(&buf).await?;
64    stream.flush().await?;
65    Ok(())
66}
67
68async fn recv_plist<S: AsyncRead + Unpin>(stream: &mut S) -> Result<plist::Dictionary, IdamError> {
69    let mut len_buf = [0u8; 4];
70    stream.read_exact(&mut len_buf).await?;
71    let len = u32::from_be_bytes(len_buf) as usize;
72    const MAX_PLIST_SIZE: usize = 1024 * 1024;
73    if len > MAX_PLIST_SIZE {
74        return Err(IdamError::Protocol(format!(
75            "plist length {len} exceeds max {MAX_PLIST_SIZE}"
76        )));
77    }
78    let mut buf = vec![0u8; len];
79    stream.read_exact(&mut buf).await?;
80    plist::from_bytes(&buf).map_err(|e| IdamError::Plist(e.to_string()))
81}
82
83#[cfg(test)]
84mod tests {
85    use std::pin::Pin;
86    use std::task::{Context, Poll};
87
88    use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
89
90    use super::*;
91
92    struct MockStream {
93        read_buf: Vec<u8>,
94        written: Vec<u8>,
95        read_pos: usize,
96    }
97
98    impl MockStream {
99        fn with_response(value: plist::Value) -> Self {
100            let mut payload = Vec::new();
101            plist::to_writer_xml(&mut payload, &value).unwrap();
102            let mut read_buf = Vec::new();
103            read_buf.extend_from_slice(&(payload.len() as u32).to_be_bytes());
104            read_buf.extend_from_slice(&payload);
105            Self {
106                read_buf,
107                written: Vec::new(),
108                read_pos: 0,
109            }
110        }
111
112        fn eof() -> Self {
113            Self {
114                read_buf: Vec::new(),
115                written: Vec::new(),
116                read_pos: 0,
117            }
118        }
119    }
120
121    impl AsyncRead for MockStream {
122        fn poll_read(
123            mut self: Pin<&mut Self>,
124            _cx: &mut Context<'_>,
125            buf: &mut ReadBuf<'_>,
126        ) -> Poll<std::io::Result<()>> {
127            let remaining = self.read_buf.len().saturating_sub(self.read_pos);
128            if remaining == 0 {
129                return Poll::Ready(Err(std::io::Error::new(
130                    std::io::ErrorKind::UnexpectedEof,
131                    "no more test data",
132                )));
133            }
134            let to_copy = remaining.min(buf.remaining());
135            let start = self.read_pos;
136            let end = start + to_copy;
137            buf.put_slice(&self.read_buf[start..end]);
138            self.read_pos = end;
139            Poll::Ready(Ok(()))
140        }
141    }
142
143    impl AsyncWrite for MockStream {
144        fn poll_write(
145            mut self: Pin<&mut Self>,
146            _cx: &mut Context<'_>,
147            buf: &[u8],
148        ) -> Poll<std::io::Result<usize>> {
149            self.written.extend_from_slice(buf);
150            Poll::Ready(Ok(buf.len()))
151        }
152
153        fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
154            Poll::Ready(Ok(()))
155        }
156
157        fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
158            Poll::Ready(Ok(()))
159        }
160    }
161
162    #[tokio::test]
163    async fn configuration_inquiry_sends_expected_request() {
164        let response = plist::Value::Dictionary(plist::Dictionary::from_iter([(
165            "SupportsIDAM".to_string(),
166            plist::Value::Boolean(true),
167        )]));
168        let mut stream = MockStream::with_response(response);
169        let mut client = IdamClient::new(&mut stream);
170
171        let value = client.configuration_inquiry().await.unwrap();
172        let dict = value
173            .as_dictionary()
174            .expect("configuration inquiry should return a plist dictionary");
175        assert_eq!(
176            dict.get("SupportsIDAM").and_then(plist::Value::as_boolean),
177            Some(true),
178            "configuration inquiry should return plist payload"
179        );
180
181        let len = u32::from_be_bytes(stream.written[..4].try_into().unwrap()) as usize;
182        let payload = &stream.written[4..4 + len];
183        let dict: plist::Dictionary = plist::from_bytes(payload).unwrap();
184        assert_eq!(dict["Configuration Inquiry"].as_boolean(), Some(true));
185    }
186
187    #[tokio::test]
188    async fn set_configuration_sends_expected_boolean() {
189        let response = plist::Value::Dictionary(plist::Dictionary::from_iter([(
190            "Status".to_string(),
191            plist::Value::String("Acknowledged".into()),
192        )]));
193        let mut stream = MockStream::with_response(response);
194        let mut client = IdamClient::new(&mut stream);
195
196        let value = client.set_configuration(false).await.unwrap();
197        let dict = value
198            .as_dictionary()
199            .expect("set_configuration should return a plist dictionary");
200        assert_eq!(
201            dict.get("Status").and_then(plist::Value::as_string),
202            Some("Acknowledged")
203        );
204
205        let len = u32::from_be_bytes(stream.written[..4].try_into().unwrap()) as usize;
206        let payload = &stream.written[4..4 + len];
207        let dict: plist::Dictionary = plist::from_bytes(payload).unwrap();
208        assert_eq!(dict["Set IDAM Configuration"].as_boolean(), Some(false));
209    }
210
211    #[tokio::test]
212    async fn set_configuration_treats_eof_as_success() {
213        let mut stream = MockStream::eof();
214        let mut client = IdamClient::new(&mut stream);
215
216        let value = client.set_configuration(true).await.unwrap();
217        assert_eq!(
218            value.as_dictionary().map(plist::Dictionary::len),
219            Some(0),
220            "EOF after set should be treated as a successful empty response"
221        );
222
223        let len = u32::from_be_bytes(stream.written[..4].try_into().unwrap()) as usize;
224        let payload = &stream.written[4..4 + len];
225        let dict: plist::Dictionary = plist::from_bytes(payload).unwrap();
226        assert_eq!(dict["Set IDAM Configuration"].as_boolean(), Some(true));
227    }
228}