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
10service_error!(IdamError);
11
12pub struct IdamClient<S> {
13    stream: S,
14}
15
16impl<S: AsyncRead + AsyncWrite + Unpin> IdamClient<S> {
17    pub fn new(stream: S) -> Self {
18        Self { stream }
19    }
20
21    pub async fn configuration_inquiry(&mut self) -> Result<plist::Value, IdamError> {
22        let request = plist::Dictionary::from_iter([(
23            "Configuration Inquiry".to_string(),
24            plist::Value::Boolean(true),
25        )]);
26        send_plist(&mut self.stream, &plist::Value::Dictionary(request)).await?;
27        Ok(plist::Value::Dictionary(
28            recv_plist(&mut self.stream).await?,
29        ))
30    }
31
32    pub async fn set_configuration(&mut self, enabled: bool) -> Result<plist::Value, IdamError> {
33        let request = plist::Dictionary::from_iter([(
34            "Set IDAM Configuration".to_string(),
35            plist::Value::Boolean(enabled),
36        )]);
37        send_plist(&mut self.stream, &plist::Value::Dictionary(request)).await?;
38        match recv_plist(&mut self.stream).await {
39            Ok(response) => Ok(plist::Value::Dictionary(response)),
40            Err(IdamError::Io(err)) if err.kind() == std::io::ErrorKind::UnexpectedEof => {
41                Ok(plist::Value::Dictionary(plist::Dictionary::new()))
42            }
43            Err(err) => Err(err),
44        }
45    }
46}
47
48async fn send_plist<S: AsyncWrite + Unpin>(
49    stream: &mut S,
50    value: &plist::Value,
51) -> Result<(), IdamError> {
52    let mut buf = Vec::new();
53    plist::to_writer_xml(&mut buf, value)?;
54    stream.write_all(&(buf.len() as u32).to_be_bytes()).await?;
55    stream.write_all(&buf).await?;
56    stream.flush().await?;
57    Ok(())
58}
59
60async fn recv_plist<S: AsyncRead + Unpin>(stream: &mut S) -> Result<plist::Dictionary, IdamError> {
61    let mut len_buf = [0u8; 4];
62    stream.read_exact(&mut len_buf).await?;
63    let len = u32::from_be_bytes(len_buf) as usize;
64    const MAX_PLIST_SIZE: usize = 1024 * 1024;
65    if len > MAX_PLIST_SIZE {
66        return Err(IdamError::Protocol(format!(
67            "plist length {len} exceeds max {MAX_PLIST_SIZE}"
68        )));
69    }
70    let mut buf = vec![0u8; len];
71    stream.read_exact(&mut buf).await?;
72    Ok(plist::from_bytes(&buf)?)
73}
74
75#[cfg(test)]
76mod tests {
77    use crate::test_util::MockStream;
78
79    use super::*;
80
81    #[tokio::test]
82    async fn configuration_inquiry_sends_expected_request() {
83        let response = plist::Value::Dictionary(plist::Dictionary::from_iter([(
84            "SupportsIDAM".to_string(),
85            plist::Value::Boolean(true),
86        )]));
87        let mut stream = MockStream::with_response(response);
88        let mut client = IdamClient::new(&mut stream);
89
90        let value = client.configuration_inquiry().await.unwrap();
91        let dict = value
92            .as_dictionary()
93            .expect("configuration inquiry should return a plist dictionary");
94        assert_eq!(
95            dict.get("SupportsIDAM").and_then(plist::Value::as_boolean),
96            Some(true),
97            "configuration inquiry should return plist payload"
98        );
99
100        let len = u32::from_be_bytes(stream.written[..4].try_into().unwrap()) as usize;
101        let payload = &stream.written[4..4 + len];
102        let dict: plist::Dictionary = plist::from_bytes(payload).unwrap();
103        assert_eq!(dict["Configuration Inquiry"].as_boolean(), Some(true));
104    }
105
106    #[tokio::test]
107    async fn set_configuration_sends_expected_boolean() {
108        let response = plist::Value::Dictionary(plist::Dictionary::from_iter([(
109            "Status".to_string(),
110            plist::Value::String("Acknowledged".into()),
111        )]));
112        let mut stream = MockStream::with_response(response);
113        let mut client = IdamClient::new(&mut stream);
114
115        let value = client.set_configuration(false).await.unwrap();
116        let dict = value
117            .as_dictionary()
118            .expect("set_configuration should return a plist dictionary");
119        assert_eq!(
120            dict.get("Status").and_then(plist::Value::as_string),
121            Some("Acknowledged")
122        );
123
124        let len = u32::from_be_bytes(stream.written[..4].try_into().unwrap()) as usize;
125        let payload = &stream.written[4..4 + len];
126        let dict: plist::Dictionary = plist::from_bytes(payload).unwrap();
127        assert_eq!(dict["Set IDAM Configuration"].as_boolean(), Some(false));
128    }
129
130    #[tokio::test]
131    async fn set_configuration_treats_eof_as_success() {
132        let mut stream = MockStream::eof();
133        let mut client = IdamClient::new(&mut stream);
134
135        let value = client.set_configuration(true).await.unwrap();
136        assert_eq!(
137            value.as_dictionary().map(plist::Dictionary::len),
138            Some(0),
139            "EOF after set should be treated as a successful empty response"
140        );
141
142        let len = u32::from_be_bytes(stream.written[..4].try_into().unwrap()) as usize;
143        let payload = &stream.written[4..4 + len];
144        let dict: plist::Dictionary = plist::from_bytes(payload).unwrap();
145        assert_eq!(dict["Set IDAM Configuration"].as_boolean(), Some(true));
146    }
147}