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(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 crate::test_util::MockStream;
89
90    use super::*;
91
92    #[tokio::test]
93    async fn create_stashbag_sends_manifest() {
94        let response = plist::Value::Dictionary(plist::Dictionary::new());
95        let mut stream = MockStream::with_response(response);
96        let mut client = PreboardClient::new(&mut stream);
97
98        client
99            .create_stashbag(plist::Dictionary::from_iter([(
100                "Example".to_string(),
101                plist::Value::Boolean(true),
102            )]))
103            .await
104            .unwrap();
105
106        let len = u32::from_be_bytes(stream.written[..4].try_into().unwrap()) as usize;
107        let payload = &stream.written[4..4 + len];
108        let dict: plist::Dictionary = plist::from_bytes(payload).unwrap();
109        assert_eq!(dict["Command"].as_string(), Some("CreateStashbag"));
110        assert_eq!(
111            dict["Manifest"]
112                .as_dictionary()
113                .and_then(|m| m.get("Example"))
114                .and_then(plist::Value::as_boolean),
115            Some(true)
116        );
117    }
118
119    #[tokio::test]
120    async fn recv_plist_rejects_oversized_frame() {
121        let mut read_buf = ((MAX_PLIST_SIZE as u32) + 1).to_be_bytes().to_vec();
122        read_buf.extend_from_slice(b"ignored");
123        let mut stream = MockStream::new(read_buf);
124
125        let err = recv_plist(&mut stream).await.unwrap_err();
126        assert!(matches!(err, PreboardError::Plist(message) if message.contains("exceeds max")));
127    }
128}