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
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
95/// Capture a screenshot from the device.
96///
97/// Returns raw image bytes plus detected format metadata.
98pub async fn take_screenshot<S>(stream: &mut S) -> Result<ScreenshotImage, ScreenshotError>
99where
100    S: AsyncRead + AsyncWrite + Unpin,
101{
102    // 1. Send version exchange
103    send_plist(
104        stream,
105        &VersionExchangeRequest {
106            message_type: "DLMessageVersionExchange",
107            supported_versions: vec![1],
108        },
109    )
110    .await?;
111
112    // 2. Recv version exchange response (ignore content)
113    recv_plist_raw(stream).await?;
114
115    // 3. Send device ready
116    send_plist(
117        stream,
118        &DeviceReadyRequest {
119            message_type: "DLMessageDeviceReady",
120        },
121    )
122    .await?;
123
124    // 4. Recv screenshot plist
125    let data = recv_plist_raw(stream).await?;
126
127    // Parse plist to find ScreenShotData
128    let val: plist::Value =
129        plist::from_bytes(&data).map_err(|e| ScreenshotError::Plist(e.to_string()))?;
130
131    // The plist is an array: [MessageType, {ScreenShotData: <data>}]
132    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
149// ── plist framing (same as lockdown: 4-byte BE length prefix) ─────────────────
150
151async 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}