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