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";
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(#[from] plist::Error),
16    #[error("protocol error: {0}")]
17    Protocol(String),
18}
19
20pub struct PreboardClient<S> {
21    stream: S,
22}
23
24impl<S: AsyncRead + AsyncWrite + Unpin> PreboardClient<S> {
25    pub fn new(stream: S) -> Self {
26        Self { stream }
27    }
28
29    pub async fn create_stashbag(
30        &mut self,
31        manifest: plist::Dictionary,
32    ) -> Result<plist::Dictionary, PreboardError> {
33        self.send_command("CreateStashbag", manifest).await
34    }
35
36    pub async fn commit_stashbag(
37        &mut self,
38        manifest: plist::Dictionary,
39    ) -> Result<plist::Dictionary, PreboardError> {
40        self.send_command("CommitStashbag", manifest).await
41    }
42
43    async fn send_command(
44        &mut self,
45        command: &str,
46        manifest: plist::Dictionary,
47    ) -> Result<plist::Dictionary, PreboardError> {
48        let request = plist::Dictionary::from_iter([
49            (
50                "Command".to_string(),
51                plist::Value::String(command.to_string()),
52            ),
53            ("Manifest".to_string(), plist::Value::Dictionary(manifest)),
54        ]);
55        send_plist(&mut self.stream, &plist::Value::Dictionary(request)).await?;
56        recv_plist(&mut self.stream).await
57    }
58}
59
60async fn send_plist<S: AsyncWrite + Unpin>(
61    stream: &mut S,
62    value: &plist::Value,
63) -> Result<(), PreboardError> {
64    let mut buf = Vec::new();
65    plist::to_writer_xml(&mut buf, value)?;
66    stream.write_all(&(buf.len() as u32).to_be_bytes()).await?;
67    stream.write_all(&buf).await?;
68    stream.flush().await?;
69    Ok(())
70}
71
72async fn recv_plist<S: AsyncRead + Unpin>(
73    stream: &mut S,
74) -> Result<plist::Dictionary, PreboardError> {
75    let mut len_buf = [0u8; 4];
76    stream.read_exact(&mut len_buf).await?;
77    let len = u32::from_be_bytes(len_buf) as usize;
78    if len > MAX_PLIST_SIZE {
79        return Err(PreboardError::Protocol(format!(
80            "plist length {len} exceeds max {MAX_PLIST_SIZE}"
81        )));
82    }
83    let mut buf = vec![0u8; len];
84    stream.read_exact(&mut buf).await?;
85    Ok(plist::from_bytes(&buf)?)
86}
87
88#[cfg(test)]
89mod tests {
90    use crate::test_util::MockStream;
91
92    use super::*;
93
94    #[tokio::test]
95    async fn create_stashbag_sends_manifest() {
96        let response = plist::Value::Dictionary(plist::Dictionary::new());
97        let mut stream = MockStream::with_response(response);
98        let mut client = PreboardClient::new(&mut stream);
99
100        client
101            .create_stashbag(plist::Dictionary::from_iter([(
102                "Example".to_string(),
103                plist::Value::Boolean(true),
104            )]))
105            .await
106            .unwrap();
107
108        let len = u32::from_be_bytes(stream.written[..4].try_into().unwrap()) as usize;
109        let payload = &stream.written[4..4 + len];
110        let dict: plist::Dictionary = plist::from_bytes(payload).unwrap();
111        assert_eq!(dict["Command"].as_string(), Some("CreateStashbag"));
112        assert_eq!(
113            dict["Manifest"]
114                .as_dictionary()
115                .and_then(|m| m.get("Example"))
116                .and_then(plist::Value::as_boolean),
117            Some(true)
118        );
119    }
120
121    #[tokio::test]
122    async fn recv_plist_rejects_oversized_frame() {
123        let mut read_buf = ((MAX_PLIST_SIZE as u32) + 1).to_be_bytes().to_vec();
124        read_buf.extend_from_slice(b"ignored");
125        let mut stream = MockStream::new(read_buf);
126
127        let err = recv_plist(&mut stream).await.unwrap_err();
128        assert!(matches!(err, PreboardError::Protocol(message) if message.contains("exceeds max")));
129    }
130}