diff --git a/idevice/src/services/core_device/app_service.rs b/idevice/src/services/core_device/app_service.rs index baaaa08..6a54d61 100644 --- a/idevice/src/services/core_device/app_service.rs +++ b/idevice/src/services/core_device/app_service.rs @@ -73,6 +73,63 @@ pub struct ProcessToken { pub executable_url: Option, } +#[derive(Deserialize, Clone, Debug)] +pub struct SignalResponse { + pub process: ProcessToken, + #[serde(rename = "deviceTimestamp")] + pub device_timestamp: plist::Date, + pub signal: u32, +} + +/// Icon data is in a proprietary format. +/// +/// ``` +/// 0000: 06 00 00 00 40 06 00 00 00 00 00 00 01 00 00 00 - header +/// 0010: 00 00 a0 41 00 00 a0 41 00 00 00 00 00 00 00 00 - width x height as float +/// 0020: 00 00 a0 41 00 00 a0 41 00 00 00 00 00 00 00 00 - wdith x height (again?) +/// 0030: 00 00 00 00 03 08 08 09 2a 68 6f 7d 44 a9 b7 d0 - start of image data +/// +/// ``` +/// +/// The data can be parsed like so in Python +/// +/// ```python +/// from PIL import Image +/// +/// width, height = 20, 20 (from the float sizes) +/// with open("icon.raw", "rb") as f: +/// f.seek(0x30) +/// raw = f.read(width * height * 4) +/// +/// img = Image.frombytes("RGBA", (width, height), raw) +/// img.save("icon.png") +/// ``` +#[derive(Deserialize, Clone, Debug)] +pub struct IconData { + pub data: plist::Data, + #[serde(rename = "iconSize.height")] + pub icon_height: f64, + #[serde(rename = "iconSize.width")] + pub icon_width: f64, + #[serde(rename = "minimumSize.height")] + pub minimum_height: f64, + #[serde(rename = "minimumSize.width")] + pub minimum_width: f64, + #[serde(rename = "$classes")] + pub classes: Vec, + #[serde(rename = "validationToken")] + pub validation_token: plist::Data, + pub uuid: IconUuid, +} + +#[derive(Deserialize, Clone, Debug)] +pub struct IconUuid { + #[serde(rename = "NS.uuidbytes")] + pub bytes: plist::Data, + #[serde(rename = "$classes")] + pub classes: Vec, +} + impl AppServiceClient { pub async fn new(stream: R) -> Result { Ok(Self { @@ -181,12 +238,10 @@ impl AppServiceClient { .invoke("com.apple.coredevice.feature.listprocesses", None) .await?; - println!("{}", pretty_print_plist(&res)); - let res = match res .as_dictionary() .and_then(|x| x.get("processTokens")) - .and_then(|x| plist::from_value(x).unwrap()) + .and_then(|x| plist::from_value(x).ok()) { Some(r) => r, None => { @@ -197,4 +252,107 @@ impl AppServiceClient { Ok(res) } + + /// Gives no response on failure or success + pub async fn uninstall_app( + &mut self, + bundle_id: impl Into, + ) -> Result<(), IdeviceError> { + let bundle_id = bundle_id.into(); + self.inner + .invoke( + "com.apple.coredevice.feature.uninstallapp", + Some( + crate::plist!({"bundleIdentifier": bundle_id}) + .into_dictionary() + .unwrap(), + ), + ) + .await?; + + Ok(()) + } + + pub async fn send_signal( + &mut self, + pid: u32, + signal: u32, + ) -> Result { + let res = self + .inner + .invoke( + "com.apple.coredevice.feature.sendsignaltoprocess", + Some( + crate::plist!({ + "process": { "processIdentifier": pid as i64}, + "signal": signal as i64, + }) + .into_dictionary() + .unwrap(), + ), + ) + .await?; + + let res = match plist::from_value(&res) { + Ok(r) => r, + Err(e) => { + warn!("Could not parse signal response: {e:?}"); + return Err(IdeviceError::UnexpectedResponse); + } + }; + + Ok(res) + } + + pub async fn fetch_app_icon( + &mut self, + bundle_id: impl Into, + width: f32, + height: f32, + scale: f32, + allow_placeholder: bool, + ) -> Result { + let bundle_id = bundle_id.into(); + let res = self + .inner + .invoke( + "com.apple.coredevice.feature.fetchappicons", + Some( + crate::plist!({ + "width": width, + "height": height, + "scale": scale, + "allowPlaceholder": allow_placeholder, + "bundleIdentifier": bundle_id + }) + .into_dictionary() + .unwrap(), + ), + ) + .await?; + + let res = match res + .as_dictionary() + .and_then(|x| x.get("appIconContainer")) + .and_then(|x| x.as_dictionary()) + .and_then(|x| x.get("iconImage")) + .and_then(|x| x.as_data()) + { + Some(r) => r.to_vec(), + None => { + warn!("Did not receive appIconContainer/iconImage data"); + return Err(IdeviceError::UnexpectedResponse); + } + }; + + let res = ns_keyed_archive::decode::from_bytes(&res)?; + println!("{}", pretty_print_plist(&res)); + match plist::from_value(&res) { + Ok(r) => Ok(r), + Err(e) => { + warn!("Failed to deserialize ns keyed archive: {e:?}"); + Err(IdeviceError::UnexpectedResponse) + } + } + } } diff --git a/tools/src/app_service.rs b/tools/src/app_service.rs index 45d0955..51e5279 100644 --- a/tools/src/app_service.rs +++ b/tools/src/app_service.rs @@ -58,6 +58,35 @@ async fn main() { ), ) .subcommand(Command::new("processes").about("List the processes running")) + .subcommand( + Command::new("uninstall").about("Uninstall an app").arg( + Arg::new("bundle_id") + .required(true) + .help("The bundle ID to uninstall"), + ), + ) + .subcommand( + Command::new("signal") + .about("Send a signal to an app") + .arg(Arg::new("pid").required(true).help("PID to send to")) + .arg(Arg::new("signal").required(true).help("Signal to send")), + ) + .subcommand( + Command::new("icon") + .about("Send a signal to an app") + .arg( + Arg::new("bundle_id") + .required(true) + .help("The bundle ID to fetch"), + ) + .arg( + Arg::new("path") + .required(true) + .help("The path to save the icon to"), + ) + .arg(Arg::new("hw").required(false).help("The height and width")) + .arg(Arg::new("scale").required(false).help("The scale")), + ) .get_matches(); if matches.get_flag("about") { @@ -125,6 +154,66 @@ async fn main() { } else if matches.subcommand_matches("processes").is_some() { let p = asc.list_processes().await.expect("no processes?"); println!("{p:#?}"); + } else if let Some(matches) = matches.subcommand_matches("uninstall") { + let bundle_id: &String = match matches.get_one("bundle_id") { + Some(b) => b, + None => { + eprintln!("No bundle ID passed"); + return; + } + }; + + asc.uninstall_app(bundle_id).await.expect("no launch") + } else if let Some(matches) = matches.subcommand_matches("signal") { + let pid: u32 = match matches.get_one::("pid") { + Some(b) => b.parse().expect("failed to parse PID as u32"), + None => { + eprintln!("No bundle PID passed"); + return; + } + }; + let signal: u32 = match matches.get_one::("signal") { + Some(b) => b.parse().expect("failed to parse signal as u32"), + None => { + eprintln!("No bundle signal passed"); + return; + } + }; + + let res = asc.send_signal(pid, signal).await.expect("no signal"); + println!("{res:#?}"); + } else if let Some(matches) = matches.subcommand_matches("icon") { + let bundle_id: &String = match matches.get_one("bundle_id") { + Some(b) => b, + None => { + eprintln!("No bundle ID passed"); + return; + } + }; + let save_path: &String = match matches.get_one("path") { + Some(b) => b, + None => { + eprintln!("No bundle ID passed"); + return; + } + }; + let hw: f32 = match matches.get_one::("hw") { + Some(b) => b.parse().expect("failed to parse PID as f32"), + None => 1.0, + }; + let scale: f32 = match matches.get_one::("scale") { + Some(b) => b.parse().expect("failed to parse signal as f32"), + None => 1.0, + }; + + let res = asc + .fetch_app_icon(bundle_id, hw, hw, scale, true) + .await + .expect("no signal"); + println!("{res:?}"); + tokio::fs::write(save_path, res.data) + .await + .expect("failed to save"); } else { eprintln!("Invalid usage, pass -h for help"); }