Skip to main content

ios_core/services/screenshot/
mod.rs

1//! Screenshot service.
2//!
3//! Connects to `com.apple.mobile.screenshotr` and captures a screenshot.
4//!
5//! Protocol: plist-framed (same 4-byte BE length prefix as lockdown).
6//! 1. Send DL message: {"MessageType":"DLMessageVersionExchange", "SupportedVersions":[1]}
7//! 2. Recv version exchange response
8//! 3. Send DL ready: {"MessageType":"DLMessageDeviceReady"}
9//! 4. Recv: screenshot plist with "ScreenShotData" key (TIFF/PNG/JPEG bytes)
10//!
11//! Reference: libimobiledevice screenshotr protocol
12
13use 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
87/// Capture a screenshot from the device.
88///
89/// Returns raw image bytes plus detected format metadata.
90pub async fn take_screenshot<S>(stream: &mut S) -> Result<ScreenshotImage, ScreenshotError>
91where
92    S: AsyncRead + AsyncWrite + Unpin,
93{
94    // 1. Send version exchange
95    send_plist(
96        stream,
97        &VersionExchangeRequest {
98            message_type: "DLMessageVersionExchange",
99            supported_versions: vec![1],
100        },
101    )
102    .await?;
103
104    // 2. Recv version exchange response (ignore content)
105    recv_plist_raw(stream).await?;
106
107    // 3. Send device ready
108    send_plist(
109        stream,
110        &DeviceReadyRequest {
111            message_type: "DLMessageDeviceReady",
112        },
113    )
114    .await?;
115
116    // 4. Recv screenshot plist
117    let data = recv_plist_raw(stream).await?;
118
119    // Parse plist to find ScreenShotData
120    let val: plist::Value =
121        plist::from_bytes(&data).map_err(|e| ScreenshotError::Plist(e.to_string()))?;
122
123    // The plist is an array: [MessageType, {ScreenShotData: <data>}]
124    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
141// ── plist framing (same as lockdown: 4-byte BE length prefix) ─────────────────
142
143async 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}