ios_core/services/idam/
mod.rs1use 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}