idevice/services/core_device/
app_service.rs

1// Jackson Coxson
2
3use log::warn;
4use serde::Deserialize;
5
6use crate::{IdeviceError, ReadWrite, RsdService, obf, xpc::XPCObject};
7
8use super::CoreDeviceServiceClient;
9
10impl RsdService for AppServiceClient<Box<dyn ReadWrite>> {
11    fn rsd_service_name() -> std::borrow::Cow<'static, str> {
12        obf!("com.apple.coredevice.appservice")
13    }
14
15    async fn from_stream(stream: Box<dyn ReadWrite>) -> Result<Self, IdeviceError> {
16        Ok(Self {
17            inner: CoreDeviceServiceClient::new(stream).await?,
18        })
19    }
20}
21
22pub struct AppServiceClient<R: ReadWrite> {
23    inner: CoreDeviceServiceClient<R>,
24}
25
26#[derive(Deserialize, Clone, Debug)]
27pub struct AppListEntry {
28    #[serde(rename = "isRemovable")]
29    pub is_removable: bool,
30    pub name: String,
31    #[serde(rename = "isFirstParty")]
32    pub is_first_party: bool,
33    pub path: String,
34    #[serde(rename = "bundleIdentifier")]
35    pub bundle_identifier: String,
36    #[serde(rename = "isDeveloperApp")]
37    pub is_developer_app: bool,
38    #[serde(rename = "bundleVersion")]
39    pub bundle_version: Option<String>,
40    #[serde(rename = "isInternal")]
41    pub is_internal: bool,
42    #[serde(rename = "isHidden")]
43    pub is_hidden: bool,
44    #[serde(rename = "isAppClip")]
45    pub is_app_clip: bool,
46    pub version: Option<String>,
47}
48
49#[derive(Deserialize, Clone, Debug)]
50pub struct LaunchResponse {
51    #[serde(rename = "processIdentifierVersion")]
52    pub process_identifier_version: u32,
53    #[serde(rename = "processIdentifier")]
54    pub pid: u32,
55    #[serde(rename = "executableURL")]
56    pub executable_url: ExecutableUrl,
57    #[serde(rename = "auditToken")]
58    pub audit_token: Vec<u32>,
59}
60
61#[derive(Deserialize, Clone, Debug)]
62pub struct ExecutableUrl {
63    pub relative: String,
64}
65
66#[derive(Deserialize, Clone, Debug)]
67pub struct ProcessToken {
68    #[serde(rename = "processIdentifier")]
69    pub pid: u32,
70    #[serde(rename = "executableURL")]
71    pub executable_url: Option<ExecutableUrl>,
72}
73
74#[derive(Deserialize, Clone, Debug)]
75pub struct SignalResponse {
76    pub process: ProcessToken,
77    #[serde(rename = "deviceTimestamp")]
78    pub device_timestamp: plist::Date,
79    pub signal: u32,
80}
81
82/// Icon data is in a proprietary format.
83///
84/// ```
85/// 0000: 06 00 00 00 40 06 00 00 00 00 00 00 01 00 00 00 - header
86/// 0010: 00 00 a0 41 00 00 a0 41 00 00 00 00 00 00 00 00 - width x height as float
87/// 0020: 00 00 a0 41 00 00 a0 41 00 00 00 00 00 00 00 00 - wdith x height (again?)
88/// 0030: 00 00 00 00 03 08 08 09 2a 68 6f 7d 44 a9 b7 d0 - start of image data
89/// <snip>
90/// ```
91///
92/// The data can be parsed like so in Python
93///
94/// ```python
95/// from PIL import Image
96///
97/// width, height = 20, 20 (from the float sizes)
98/// with open("icon.raw", "rb") as f:
99///     f.seek(0x30)
100///     raw = f.read(width * height * 4)
101///
102/// img = Image.frombytes("RGBA", (width, height), raw)
103/// img.save("icon.png")
104/// ```
105#[derive(Deserialize, Clone, Debug)]
106pub struct IconData {
107    pub data: plist::Data,
108    #[serde(rename = "iconSize.height")]
109    pub icon_height: f64,
110    #[serde(rename = "iconSize.width")]
111    pub icon_width: f64,
112    #[serde(rename = "minimumSize.height")]
113    pub minimum_height: f64,
114    #[serde(rename = "minimumSize.width")]
115    pub minimum_width: f64,
116    #[serde(rename = "$classes")]
117    pub classes: Vec<String>,
118    #[serde(rename = "validationToken")]
119    pub validation_token: plist::Data,
120    pub uuid: IconUuid,
121}
122
123#[derive(Deserialize, Clone, Debug)]
124pub struct IconUuid {
125    #[serde(rename = "NS.uuidbytes")]
126    pub bytes: plist::Data,
127    #[serde(rename = "$classes")]
128    pub classes: Vec<String>,
129}
130
131impl<R: ReadWrite> AppServiceClient<R> {
132    pub async fn new(stream: R) -> Result<Self, IdeviceError> {
133        Ok(Self {
134            inner: CoreDeviceServiceClient::new(stream).await?,
135        })
136    }
137
138    pub async fn list_apps(
139        &mut self,
140        app_clips: bool,
141        removable_apps: bool,
142        hidden_apps: bool,
143        internal_apps: bool,
144        default_apps: bool,
145    ) -> Result<Vec<AppListEntry>, IdeviceError> {
146        let options = crate::plist!(dict {
147            "includeAppClips": app_clips,
148            "includeRemovableApps": removable_apps,
149            "includeHiddenApps": hidden_apps,
150            "includeInternalApps": internal_apps,
151            "includeDefaultApps": default_apps,
152        });
153        let res = self
154            .inner
155            .invoke_with_plist("com.apple.coredevice.feature.listapps", options)
156            .await?;
157
158        let res = match res.as_array() {
159            Some(a) => a,
160            None => {
161                warn!("CoreDevice result was not an array");
162                return Err(IdeviceError::UnexpectedResponse);
163            }
164        };
165
166        let mut desd = Vec::new();
167        for r in res {
168            let r: AppListEntry = match plist::from_value(r) {
169                Ok(r) => r,
170                Err(e) => {
171                    warn!("Failed to parse app entry: {e:?}");
172                    return Err(IdeviceError::UnexpectedResponse);
173                }
174            };
175            desd.push(r);
176        }
177
178        Ok(desd)
179    }
180
181    /// Launches an application by a bundle ID.
182    ///
183    /// # Notes
184    /// * `start_suspended` - If set to true, you will need to attach a debugger using
185    ///   `DebugServer` to continue.
186    ///
187    /// * `stdio_uuid` - Create a new ``OpenStdioSocketClient``, read the UUID, and pass it to this
188    ///   function. Note that if the process already has another stdio UUID, this parameter is ignored by
189    ///   iOS. Either make sure the proccess isn't running, or pass ``kill_existing: true``
190    #[allow(clippy::too_many_arguments)] // still didn't ask
191    pub async fn launch_application(
192        &mut self,
193        bundle_id: impl Into<String>,
194        arguments: &[&str],
195        kill_existing: bool,
196        start_suspended: bool,
197        environment: Option<plist::Dictionary>,
198        platform_options: Option<plist::Dictionary>,
199        stdio_uuid: Option<uuid::Uuid>,
200    ) -> Result<LaunchResponse, IdeviceError> {
201        let bundle_id = bundle_id.into();
202
203        let req = crate::plist!({
204            "applicationSpecifier": {
205                "bundleIdentifier": {
206                    "_0": bundle_id
207                }
208            },
209            "options": {
210                "arguments": arguments,
211                "environmentVariables": environment.unwrap_or_default(),
212                "standardIOUsesPseudoterminals": true,
213                "startStopped": start_suspended,
214                "terminateExisting": kill_existing,
215                "user": {
216                    "active": true,
217                },
218                "platformSpecificOptions": plist::Value::Data(crate::util::plist_to_xml_bytes(&platform_options.unwrap_or_default())),
219            },
220        });
221
222        let req: XPCObject = req.into();
223        let mut req = req.to_dictionary().unwrap();
224        req.insert(
225            "standardIOIdentifiers".into(),
226            match stdio_uuid {
227                Some(u) => {
228                    let u = XPCObject::Uuid(u);
229                    let mut d = crate::xpc::Dictionary::new();
230                    d.insert("standardInput".into(), u.clone());
231                    d.insert("standardOutput".into(), u.clone());
232                    d.insert("standardError".into(), u.clone());
233                    d.into()
234                }
235                None => crate::xpc::Dictionary::new().into(),
236            },
237        );
238
239        let res = self
240            .inner
241            .invoke("com.apple.coredevice.feature.launchapplication", Some(req))
242            .await?;
243
244        let res = match res
245            .as_dictionary()
246            .and_then(|r| r.get("processToken"))
247            .and_then(|x| plist::from_value(x).ok())
248        {
249            Some(r) => r,
250            None => {
251                warn!("CoreDevice res did not contain parsable processToken");
252                return Err(IdeviceError::UnexpectedResponse);
253            }
254        };
255
256        Ok(res)
257    }
258
259    pub async fn list_processes(&mut self) -> Result<Vec<ProcessToken>, IdeviceError> {
260        let res = self
261            .inner
262            .invoke("com.apple.coredevice.feature.listprocesses", None)
263            .await?;
264
265        let res = match res
266            .as_dictionary()
267            .and_then(|x| x.get("processTokens"))
268            .and_then(|x| plist::from_value(x).ok())
269        {
270            Some(r) => r,
271            None => {
272                warn!("CoreDevice res did not contain parsable processToken");
273                return Err(IdeviceError::UnexpectedResponse);
274            }
275        };
276
277        Ok(res)
278    }
279
280    /// Gives no response on failure or success
281    pub async fn uninstall_app(
282        &mut self,
283        bundle_id: impl Into<String>,
284    ) -> Result<(), IdeviceError> {
285        let bundle_id = bundle_id.into();
286        self.inner
287            .invoke_with_plist(
288                "com.apple.coredevice.feature.uninstallapp",
289                crate::plist!({"bundleIdentifier": bundle_id})
290                    .into_dictionary()
291                    .unwrap(),
292            )
293            .await?;
294
295        Ok(())
296    }
297
298    pub async fn send_signal(
299        &mut self,
300        pid: u32,
301        signal: u32,
302    ) -> Result<SignalResponse, IdeviceError> {
303        let res = self
304            .inner
305            .invoke_with_plist(
306                "com.apple.coredevice.feature.sendsignaltoprocess",
307                crate::plist!({
308                    "process": { "processIdentifier": pid as i64},
309                    "signal": signal as i64,
310                })
311                .into_dictionary()
312                .unwrap(),
313            )
314            .await?;
315
316        let res = match plist::from_value(&res) {
317            Ok(r) => r,
318            Err(e) => {
319                warn!("Could not parse signal response: {e:?}");
320                return Err(IdeviceError::UnexpectedResponse);
321            }
322        };
323
324        Ok(res)
325    }
326
327    pub async fn fetch_app_icon(
328        &mut self,
329        bundle_id: impl Into<String>,
330        width: f32,
331        height: f32,
332        scale: f32,
333        allow_placeholder: bool,
334    ) -> Result<IconData, IdeviceError> {
335        let bundle_id = bundle_id.into();
336        let res = self
337            .inner
338            .invoke_with_plist(
339                "com.apple.coredevice.feature.fetchappicons",
340                crate::plist!({
341                    "width": width,
342                    "height": height,
343                    "scale": scale,
344                    "allowPlaceholder": allow_placeholder,
345                    "bundleIdentifier": bundle_id
346                })
347                .into_dictionary()
348                .unwrap(),
349            )
350            .await?;
351
352        let res = match res
353            .as_dictionary()
354            .and_then(|x| x.get("appIconContainer"))
355            .and_then(|x| x.as_dictionary())
356            .and_then(|x| x.get("iconImage"))
357            .and_then(|x| x.as_data())
358        {
359            Some(r) => r.to_vec(),
360            None => {
361                warn!("Did not receive appIconContainer/iconImage data");
362                return Err(IdeviceError::UnexpectedResponse);
363            }
364        };
365
366        let res = ns_keyed_archive::decode::from_bytes(&res)?;
367        match plist::from_value(&res) {
368            Ok(r) => Ok(r),
369            Err(e) => {
370                warn!("Failed to deserialize ns keyed archive: {e:?}");
371                Err(IdeviceError::UnexpectedResponse)
372            }
373        }
374    }
375}