ios_core/services/screenshot/
mod.rs1use bytes::Bytes;
14use serde::Serialize;
15use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
16
17pub const SERVICE_NAME: &str = "com.apple.mobile.screenshotr";
18
19#[derive(Debug, thiserror::Error)]
20pub enum ScreenshotError {
21 #[error("IO error: {0}")]
22 Io(#[from] std::io::Error),
23 #[error("plist error: {0}")]
24 Plist(String),
25 #[error("protocol error: {0}")]
26 Protocol(String),
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
30#[serde(rename_all = "lowercase")]
31pub enum ScreenshotFormat {
32 Png,
33 Jpeg,
34 Tiff,
35 Unknown,
36}
37
38impl ScreenshotFormat {
39 pub fn mime_type(self) -> &'static str {
40 match self {
41 Self::Png => "image/png",
42 Self::Jpeg => "image/jpeg",
43 Self::Tiff => "image/tiff",
44 Self::Unknown => "application/octet-stream",
45 }
46 }
47
48 pub fn detect(bytes: &[u8]) -> Self {
49 if bytes.starts_with(&[0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A]) {
50 Self::Png
51 } else if bytes.starts_with(&[0xFF, 0xD8, 0xFF]) {
52 Self::Jpeg
53 } else if bytes.starts_with(b"II*\0") || bytes.starts_with(b"MM\0*") {
54 Self::Tiff
55 } else {
56 Self::Unknown
57 }
58 }
59}
60
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct ScreenshotImage {
63 pub data: Bytes,
64 pub format: ScreenshotFormat,
65}
66
67impl ScreenshotImage {
68 pub fn from_bytes(data: Bytes) -> Self {
69 let format = ScreenshotFormat::detect(&data);
70 Self { data, format }
71 }
72
73 pub fn mime_type(&self) -> &'static str {
74 self.format.mime_type()
75 }
76
77 pub fn byte_len(&self) -> usize {
78 self.data.len()
79 }
80}
81
82#[derive(Serialize)]
83#[serde(rename_all = "PascalCase")]
84struct VersionExchangeRequest {
85 message_type: &'static str,
86 supported_versions: Vec<u64>,
87}
88
89#[derive(Serialize)]
90#[serde(rename_all = "PascalCase")]
91struct DeviceReadyRequest {
92 message_type: &'static str,
93}
94
95pub async fn take_screenshot<S>(stream: &mut S) -> Result<ScreenshotImage, ScreenshotError>
99where
100 S: AsyncRead + AsyncWrite + Unpin,
101{
102 send_plist(
104 stream,
105 &VersionExchangeRequest {
106 message_type: "DLMessageVersionExchange",
107 supported_versions: vec![1],
108 },
109 )
110 .await?;
111
112 recv_plist_raw(stream).await?;
114
115 send_plist(
117 stream,
118 &DeviceReadyRequest {
119 message_type: "DLMessageDeviceReady",
120 },
121 )
122 .await?;
123
124 let data = recv_plist_raw(stream).await?;
126
127 let val: plist::Value =
129 plist::from_bytes(&data).map_err(|e| ScreenshotError::Plist(e.to_string()))?;
130
131 if let Some(arr) = val.as_array() {
133 for item in arr {
134 if let Some(dict) = item.as_dictionary() {
135 if let Some(img) = dict.get("ScreenShotData") {
136 if let Some(bytes) = img.as_data() {
137 return Ok(ScreenshotImage::from_bytes(Bytes::copy_from_slice(bytes)));
138 }
139 }
140 }
141 }
142 }
143
144 Err(ScreenshotError::Protocol(
145 "ScreenShotData not found in response".into(),
146 ))
147}
148
149async fn send_plist<S, T>(stream: &mut S, value: &T) -> Result<(), ScreenshotError>
152where
153 S: AsyncWrite + Unpin,
154 T: Serialize,
155{
156 let mut buf = Vec::new();
157 plist::to_writer_xml(&mut buf, value).map_err(|e| ScreenshotError::Plist(e.to_string()))?;
158 stream.write_all(&(buf.len() as u32).to_be_bytes()).await?;
159 stream.write_all(&buf).await?;
160 stream.flush().await?;
161 Ok(())
162}
163
164async fn recv_plist_raw<S>(stream: &mut S) -> Result<Vec<u8>, ScreenshotError>
165where
166 S: AsyncRead + Unpin,
167{
168 let mut len_buf = [0u8; 4];
169 stream.read_exact(&mut len_buf).await?;
170 let len = u32::from_be_bytes(len_buf) as usize;
171 const MAX_PLIST_SIZE: usize = 4 * 1024 * 1024;
172 if len > MAX_PLIST_SIZE {
173 return Err(ScreenshotError::Protocol(format!(
174 "plist length {len} exceeds maximum of {MAX_PLIST_SIZE}"
175 )));
176 }
177 let mut buf = vec![0u8; len];
178 stream.read_exact(&mut buf).await?;
179 Ok(buf)
180}
181
182#[cfg(test)]
183mod tests {
184 use bytes::Bytes;
185
186 use super::{ScreenshotFormat, ScreenshotImage};
187
188 #[test]
189 fn detects_png_signature() {
190 let format = ScreenshotFormat::detect(&[0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A]);
191 assert_eq!(format, ScreenshotFormat::Png);
192 assert_eq!(format.mime_type(), "image/png");
193 }
194
195 #[test]
196 fn detects_jpeg_signature() {
197 let format = ScreenshotFormat::detect(&[0xFF, 0xD8, 0xFF, 0xE0]);
198 assert_eq!(format, ScreenshotFormat::Jpeg);
199 assert_eq!(format.mime_type(), "image/jpeg");
200 }
201
202 #[test]
203 fn detects_tiff_signatures() {
204 assert_eq!(
205 ScreenshotFormat::detect(b"II*\0rest"),
206 ScreenshotFormat::Tiff
207 );
208 assert_eq!(
209 ScreenshotFormat::detect(b"MM\0*rest"),
210 ScreenshotFormat::Tiff
211 );
212 }
213
214 #[test]
215 fn unknown_signature_falls_back_to_octet_stream() {
216 let image = ScreenshotImage::from_bytes(Bytes::from_static(b"not-an-image"));
217 assert_eq!(image.format, ScreenshotFormat::Unknown);
218 assert_eq!(image.mime_type(), "application/octet-stream");
219 assert_eq!(image.byte_len(), 12);
220 }
221}