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 = plist::from_bytes(&data)?;
121
122 if let Some(arr) = val.as_array() {
124 for item in arr {
125 if let Some(dict) = item.as_dictionary() {
126 if let Some(img) = dict.get("ScreenShotData") {
127 if let Some(bytes) = img.as_data() {
128 return Ok(ScreenshotImage::from_bytes(Bytes::copy_from_slice(bytes)));
129 }
130 }
131 }
132 }
133 }
134
135 Err(ScreenshotError::Protocol(
136 "ScreenShotData not found in response".into(),
137 ))
138}
139
140async fn send_plist<S, T>(stream: &mut S, value: &T) -> Result<(), ScreenshotError>
143where
144 S: AsyncWrite + Unpin,
145 T: Serialize,
146{
147 let mut buf = Vec::new();
148 plist::to_writer_xml(&mut buf, value)?;
149 stream.write_all(&(buf.len() as u32).to_be_bytes()).await?;
150 stream.write_all(&buf).await?;
151 stream.flush().await?;
152 Ok(())
153}
154
155async fn recv_plist_raw<S>(stream: &mut S) -> Result<Vec<u8>, ScreenshotError>
156where
157 S: AsyncRead + Unpin,
158{
159 let mut len_buf = [0u8; 4];
160 stream.read_exact(&mut len_buf).await?;
161 let len = u32::from_be_bytes(len_buf) as usize;
162 const MAX_PLIST_SIZE: usize = 4 * 1024 * 1024;
163 if len > MAX_PLIST_SIZE {
164 return Err(ScreenshotError::Protocol(format!(
165 "plist length {len} exceeds maximum of {MAX_PLIST_SIZE}"
166 )));
167 }
168 let mut buf = vec![0u8; len];
169 stream.read_exact(&mut buf).await?;
170 Ok(buf)
171}
172
173#[cfg(test)]
174mod tests {
175 use bytes::Bytes;
176
177 use super::{ScreenshotFormat, ScreenshotImage};
178
179 #[test]
180 fn detects_png_signature() {
181 let format = ScreenshotFormat::detect(&[0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A]);
182 assert_eq!(format, ScreenshotFormat::Png);
183 assert_eq!(format.mime_type(), "image/png");
184 }
185
186 #[test]
187 fn detects_jpeg_signature() {
188 let format = ScreenshotFormat::detect(&[0xFF, 0xD8, 0xFF, 0xE0]);
189 assert_eq!(format, ScreenshotFormat::Jpeg);
190 assert_eq!(format.mime_type(), "image/jpeg");
191 }
192
193 #[test]
194 fn detects_tiff_signatures() {
195 assert_eq!(
196 ScreenshotFormat::detect(b"II*\0rest"),
197 ScreenshotFormat::Tiff
198 );
199 assert_eq!(
200 ScreenshotFormat::detect(b"MM\0*rest"),
201 ScreenshotFormat::Tiff
202 );
203 }
204
205 #[test]
206 fn unknown_signature_falls_back_to_octet_stream() {
207 let image = ScreenshotImage::from_bytes(Bytes::from_static(b"not-an-image"));
208 assert_eq!(image.format, ScreenshotFormat::Unknown);
209 assert_eq!(image.mime_type(), "application/octet-stream");
210 assert_eq!(image.byte_len(), 12);
211 }
212}