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