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 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}