ios_core/services/preboard/
mod.rs1use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
6
7pub const SERVICE_NAME: &str = "com.apple.preboardservice_v2";
8const MAX_PLIST_SIZE: usize = 4 * 1024 * 1024;
9
10#[derive(Debug, thiserror::Error)]
11pub enum PreboardError {
12 #[error("IO error: {0}")]
13 Io(#[from] std::io::Error),
14 #[error("plist error: {0}")]
15 Plist(String),
16}
17
18pub struct PreboardClient<S> {
19 stream: S,
20}
21
22impl<S: AsyncRead + AsyncWrite + Unpin> PreboardClient<S> {
23 pub fn new(stream: S) -> Self {
24 Self { stream }
25 }
26
27 pub async fn create_stashbag(
28 &mut self,
29 manifest: plist::Dictionary,
30 ) -> Result<plist::Dictionary, PreboardError> {
31 self.send_command("CreateStashbag", manifest).await
32 }
33
34 pub async fn commit_stashbag(
35 &mut self,
36 manifest: plist::Dictionary,
37 ) -> Result<plist::Dictionary, PreboardError> {
38 self.send_command("CommitStashbag", manifest).await
39 }
40
41 async fn send_command(
42 &mut self,
43 command: &str,
44 manifest: plist::Dictionary,
45 ) -> Result<plist::Dictionary, PreboardError> {
46 let request = plist::Dictionary::from_iter([
47 (
48 "Command".to_string(),
49 plist::Value::String(command.to_string()),
50 ),
51 ("Manifest".to_string(), plist::Value::Dictionary(manifest)),
52 ]);
53 send_plist(&mut self.stream, &plist::Value::Dictionary(request)).await?;
54 recv_plist(&mut self.stream).await
55 }
56}
57
58async fn send_plist<S: AsyncWrite + Unpin>(
59 stream: &mut S,
60 value: &plist::Value,
61) -> Result<(), PreboardError> {
62 let mut buf = Vec::new();
63 plist::to_writer_xml(&mut buf, value).map_err(|e| PreboardError::Plist(e.to_string()))?;
64 stream.write_all(&(buf.len() as u32).to_be_bytes()).await?;
65 stream.write_all(&buf).await?;
66 stream.flush().await?;
67 Ok(())
68}
69
70async fn recv_plist<S: AsyncRead + Unpin>(
71 stream: &mut S,
72) -> Result<plist::Dictionary, PreboardError> {
73 let mut len_buf = [0u8; 4];
74 stream.read_exact(&mut len_buf).await?;
75 let len = u32::from_be_bytes(len_buf) as usize;
76 if len > MAX_PLIST_SIZE {
77 return Err(PreboardError::Plist(format!(
78 "plist length {len} exceeds max {MAX_PLIST_SIZE}"
79 )));
80 }
81 let mut buf = vec![0u8; len];
82 stream.read_exact(&mut buf).await?;
83 plist::from_bytes(&buf).map_err(|e| PreboardError::Plist(e.to_string()))
84}
85
86#[cfg(test)]
87mod tests {
88 use std::pin::Pin;
89 use std::task::{Context, Poll};
90
91 use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
92
93 use super::*;
94
95 struct MockStream {
96 read_buf: Vec<u8>,
97 written: Vec<u8>,
98 read_pos: usize,
99 }
100
101 impl MockStream {
102 fn with_response(value: plist::Value) -> Self {
103 let mut payload = Vec::new();
104 plist::to_writer_xml(&mut payload, &value).unwrap();
105 let mut read_buf = Vec::new();
106 read_buf.extend_from_slice(&(payload.len() as u32).to_be_bytes());
107 read_buf.extend_from_slice(&payload);
108 Self {
109 read_buf,
110 written: Vec::new(),
111 read_pos: 0,
112 }
113 }
114 }
115
116 impl AsyncRead for MockStream {
117 fn poll_read(
118 mut self: Pin<&mut Self>,
119 _cx: &mut Context<'_>,
120 buf: &mut ReadBuf<'_>,
121 ) -> Poll<std::io::Result<()>> {
122 let remaining = self.read_buf.len().saturating_sub(self.read_pos);
123 if remaining == 0 {
124 return Poll::Ready(Err(std::io::Error::new(
125 std::io::ErrorKind::UnexpectedEof,
126 "no more test data",
127 )));
128 }
129 let to_copy = remaining.min(buf.remaining());
130 let start = self.read_pos;
131 let end = start + to_copy;
132 buf.put_slice(&self.read_buf[start..end]);
133 self.read_pos = end;
134 Poll::Ready(Ok(()))
135 }
136 }
137
138 impl AsyncWrite for MockStream {
139 fn poll_write(
140 mut self: Pin<&mut Self>,
141 _cx: &mut Context<'_>,
142 buf: &[u8],
143 ) -> Poll<std::io::Result<usize>> {
144 self.written.extend_from_slice(buf);
145 Poll::Ready(Ok(buf.len()))
146 }
147
148 fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
149 Poll::Ready(Ok(()))
150 }
151
152 fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
153 Poll::Ready(Ok(()))
154 }
155 }
156
157 #[tokio::test]
158 async fn create_stashbag_sends_manifest() {
159 let response = plist::Value::Dictionary(plist::Dictionary::new());
160 let mut stream = MockStream::with_response(response);
161 let mut client = PreboardClient::new(&mut stream);
162
163 client
164 .create_stashbag(plist::Dictionary::from_iter([(
165 "Example".to_string(),
166 plist::Value::Boolean(true),
167 )]))
168 .await
169 .unwrap();
170
171 let len = u32::from_be_bytes(stream.written[..4].try_into().unwrap()) as usize;
172 let payload = &stream.written[4..4 + len];
173 let dict: plist::Dictionary = plist::from_bytes(payload).unwrap();
174 assert_eq!(dict["Command"].as_string(), Some("CreateStashbag"));
175 assert_eq!(
176 dict["Manifest"]
177 .as_dictionary()
178 .and_then(|m| m.get("Example"))
179 .and_then(plist::Value::as_boolean),
180 Some(true)
181 );
182 }
183
184 #[tokio::test]
185 async fn recv_plist_rejects_oversized_frame() {
186 let mut read_buf = ((MAX_PLIST_SIZE as u32) + 1).to_be_bytes().to_vec();
187 read_buf.extend_from_slice(b"ignored");
188 let mut stream = MockStream {
189 read_buf,
190 written: Vec::new(),
191 read_pos: 0,
192 };
193
194 let err = recv_plist(&mut stream).await.unwrap_err();
195 assert!(matches!(err, PreboardError::Plist(message) if message.contains("exceeds max")));
196 }
197}