Skip to main content

ios_core/services/preboard/
mod.rs

1//! Preboard service client.
2//!
3//! Service: `com.apple.preboardservice_v2`
4
5use 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}