From 2a6631f3da6f45bb11760b46c7caedde139723e3 Mon Sep 17 00:00:00 2001 From: uncor3 Date: Fri, 19 Dec 2025 20:10:42 -0800 Subject: [PATCH 01/34] fix offline build (#47) --- ffi/build.rs | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/ffi/build.rs b/ffi/build.rs index a6d1b5d..fa36fdf 100644 --- a/ffi/build.rs +++ b/ffi/build.rs @@ -35,14 +35,22 @@ fn main() { .expect("Unable to generate bindings") .write_to_file("idevice.h"); - // download plist.h - let h = ureq::get("https://raw.githubusercontent.com/libimobiledevice/libplist/refs/heads/master/include/plist/plist.h") - .call() - .expect("failed to download plist.h"); - let h = h - .into_body() - .read_to_string() - .expect("failed to get string content"); + // Check if plist.h exists locally first, otherwise download + let plist_h_path = "plist.h"; + let h = if std::path::Path::new(plist_h_path).exists() { + std::fs::read_to_string(plist_h_path) + .expect("failed to read plist.h") + } else { + // download plist.h + let h = ureq::get("https://raw.githubusercontent.com/libimobiledevice/libplist/refs/heads/master/include/plist/plist.h") + .call() + .expect("failed to download plist.h"); + h + .into_body() + .read_to_string() + .expect("failed to get string content") + }; + let mut f = OpenOptions::new().append(true).open("idevice.h").unwrap(); f.write_all(b"\n\n\n").unwrap(); f.write_all(&h.into_bytes()) From 83e43aa3d66014dc727c43ede1dcd29e526fef58 Mon Sep 17 00:00:00 2001 From: Abdullah Al-Banna Date: Sat, 20 Dec 2025 23:30:26 +0300 Subject: [PATCH 02/34] add the `futures` dep for the afc feature (#48) --- idevice/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/idevice/Cargo.toml b/idevice/Cargo.toml index d6104ae..bdcb6e8 100644 --- a/idevice/Cargo.toml +++ b/idevice/Cargo.toml @@ -71,7 +71,7 @@ ring = ["rustls", "rustls/ring", "tokio-rustls/ring"] rustls = ["dep:rustls", "dep:tokio-rustls"] openssl = ["dep:openssl", "dep:tokio-openssl"] -afc = ["dep:chrono"] +afc = ["dep:chrono", "dep:futures"] amfi = [] bt_packet_logger = [] companion_proxy = [] From ae5071a309617135124c77b6a530a56e943ef3b5 Mon Sep 17 00:00:00 2001 From: se2crid <151872490+se2crid@users.noreply.github.com> Date: Sat, 20 Dec 2025 21:31:30 +0100 Subject: [PATCH 03/34] Include ErrorDescription in unknown device errors (#46) * Include ErrorDescription in unknown device errors * Remove unused error import from tracing --- idevice/src/lib.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/idevice/src/lib.rs b/idevice/src/lib.rs index cacd0de..cb62deb 100644 --- a/idevice/src/lib.rs +++ b/idevice/src/lib.rs @@ -38,7 +38,7 @@ use std::{ }; use thiserror::Error; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; -use tracing::{debug, error, trace}; +use tracing::{debug, trace}; pub use util::{pretty_print_dictionary, pretty_print_plist}; @@ -482,7 +482,13 @@ impl Idevice { if let Some(e) = IdeviceError::from_device_error_type(e.as_str(), &res) { return Err(e); } else { - return Err(IdeviceError::UnknownErrorType(e)); + let msg = + if let Some(desc) = res.get("ErrorDescription").and_then(|x| x.as_string()) { + format!("{} ({})", e, desc) + } else { + e + }; + return Err(IdeviceError::UnknownErrorType(msg)); } } Ok(res) From d8bff83753737d8f6c80c9b1905f6d894c08fd41 Mon Sep 17 00:00:00 2001 From: alexytomi <60690056+alexytomi@users.noreply.github.com> Date: Mon, 22 Dec 2025 00:20:24 +0800 Subject: [PATCH 04/34] Fix failure to compile to 32-bit (#49) --- idevice/src/services/afc/inner_file.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/idevice/src/services/afc/inner_file.rs b/idevice/src/services/afc/inner_file.rs index a245fed..757e1ea 100644 --- a/idevice/src/services/afc/inner_file.rs +++ b/idevice/src/services/afc/inner_file.rs @@ -140,7 +140,7 @@ crate::impl_to_structs!(InnerFileDescriptor<'_>, OwnedInnerFileDescriptor; { let mut collected_bytes = Vec::with_capacity(n); for chunk in chunk_number(n, MAX_TRANSFER as usize) { - let header_payload = [self.fd.to_le_bytes(), chunk.to_le_bytes()].concat(); + let header_payload = [self.fd.to_le_bytes(), (chunk as u64).to_le_bytes()].concat(); let res = self .as_mut() .send_packet(AfcOpcode::Read, header_payload, Vec::new()) From 328224d46cd3ce021b18627d6e3820e3a8e0dd4f Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Mon, 22 Dec 2025 12:26:43 -0700 Subject: [PATCH 05/34] cargo fmt --- ffi/build.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/ffi/build.rs b/ffi/build.rs index fa36fdf..f24a1ab 100644 --- a/ffi/build.rs +++ b/ffi/build.rs @@ -38,19 +38,17 @@ fn main() { // Check if plist.h exists locally first, otherwise download let plist_h_path = "plist.h"; let h = if std::path::Path::new(plist_h_path).exists() { - std::fs::read_to_string(plist_h_path) - .expect("failed to read plist.h") + std::fs::read_to_string(plist_h_path).expect("failed to read plist.h") } else { // download plist.h let h = ureq::get("https://raw.githubusercontent.com/libimobiledevice/libplist/refs/heads/master/include/plist/plist.h") .call() .expect("failed to download plist.h"); - h - .into_body() + h.into_body() .read_to_string() .expect("failed to get string content") }; - + let mut f = OpenOptions::new().append(true).open("idevice.h").unwrap(); f.write_all(b"\n\n\n").unwrap(); f.write_all(&h.into_bytes()) From 35c3d613556cf794a221ea3c68732d6fd1c0ef74 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Tue, 23 Dec 2025 08:18:41 -0700 Subject: [PATCH 06/34] Make escrow bag an optional pairing file field --- idevice/src/pairing_file.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/idevice/src/pairing_file.rs b/idevice/src/pairing_file.rs index 94299d3..9c7ad92 100644 --- a/idevice/src/pairing_file.rs +++ b/idevice/src/pairing_file.rs @@ -38,7 +38,7 @@ pub struct PairingFile { /// Host identifier pub host_id: String, /// Escrow bag allowing for access while locked - pub escrow_bag: Vec, + pub escrow_bag: Option>, /// Device's WiFi MAC address pub wifi_mac_address: String, /// Device's Unique Device Identifier (optional) @@ -73,7 +73,7 @@ struct RawPairingFile { system_buid: String, #[serde(rename = "HostID")] host_id: String, - escrow_bag: Data, + escrow_bag: Option, // None on Apple Watch #[serde(rename = "WiFiMACAddress")] wifi_mac_address: String, #[serde(rename = "UDID")] @@ -206,7 +206,7 @@ impl TryFrom for PairingFile { root_certificate: CertificateDer::from_pem_slice(&root_certificate_pem)?, system_buid: value.system_buid, host_id: value.host_id, - escrow_bag: value.escrow_bag.into(), + escrow_bag: value.escrow_bag.map(|x| x.into()), wifi_mac_address: value.wifi_mac_address, udid: value.udid, }) @@ -230,7 +230,7 @@ impl TryFrom for PairingFile { root_certificate: X509::from_pem(&Into::>::into(value.root_certificate))?, system_buid: value.system_buid, host_id: value.host_id, - escrow_bag: value.escrow_bag.into(), + escrow_bag: value.escrow_bag.map(|x| x.into()), wifi_mac_address: value.wifi_mac_address, udid: value.udid, }) @@ -258,7 +258,7 @@ impl From for RawPairingFile { root_certificate: Data::new(root_cert_data), system_buid: value.system_buid, host_id: value.host_id.clone(), - escrow_bag: Data::new(value.escrow_bag), + escrow_bag: value.escrow_bag.map(Data::new), wifi_mac_address: value.wifi_mac_address, udid: value.udid, } @@ -278,7 +278,7 @@ impl TryFrom for RawPairingFile { root_certificate: Data::new(value.root_certificate.to_pem()?), system_buid: value.system_buid, host_id: value.host_id.clone(), - escrow_bag: Data::new(value.escrow_bag), + escrow_bag: value.escrow_bag.map(Data::new), wifi_mac_address: value.wifi_mac_address, udid: value.udid, }) From 081cb2f8d8e6f0d7a76966e35bfef2db68ae2a11 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Sun, 28 Dec 2025 19:33:59 -0700 Subject: [PATCH 07/34] Bump version --- Cargo.lock | 2 +- idevice/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6b5227b..543979d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1060,7 +1060,7 @@ dependencies = [ [[package]] name = "idevice" -version = "0.1.50" +version = "0.1.51" dependencies = [ "async-stream", "async_zip", diff --git a/idevice/Cargo.toml b/idevice/Cargo.toml index bdcb6e8..ab47bdf 100644 --- a/idevice/Cargo.toml +++ b/idevice/Cargo.toml @@ -2,7 +2,7 @@ name = "idevice" description = "A Rust library to interact with services on iOS devices." authors = ["Jackson Coxson"] -version = "0.1.50" +version = "0.1.51" edition = "2024" license = "MIT" documentation = "https://docs.rs/idevice" From 6d9f0987c1581d6df568297f67fb252e80febb96 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Wed, 31 Dec 2025 21:21:37 -0700 Subject: [PATCH 08/34] Migrate to plist_macro crate for utils --- Cargo.lock | 11 + idevice/Cargo.toml | 1 + idevice/src/cursor.rs | 334 ++++++++ idevice/src/lib.rs | 11 +- idevice/src/obfuscation.rs | 15 + idevice/src/plist_macro.rs | 714 ------------------ .../src/services/core_device/app_service.rs | 3 +- idevice/src/tss.rs | 5 +- idevice/src/usbmuxd/raw_packet.rs | 2 +- idevice/src/util.rs | 142 ---- idevice/src/utils/installation/helpers.rs | 5 +- idevice/src/xpc/format.rs | 3 +- tools/Cargo.toml | 1 + tools/src/companion_proxy.rs | 4 +- tools/src/lockdown.rs | 3 +- tools/src/mounter.rs | 6 +- tools/src/restore_service.rs | 3 +- 17 files changed, 385 insertions(+), 878 deletions(-) create mode 100644 idevice/src/cursor.rs create mode 100644 idevice/src/obfuscation.rs delete mode 100644 idevice/src/plist_macro.rs delete mode 100644 idevice/src/util.rs diff --git a/Cargo.lock b/Cargo.lock index 543979d..295cf8e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1076,6 +1076,7 @@ dependencies = [ "obfstr", "openssl", "plist", + "plist-macro", "rand 0.9.2", "reqwest", "rsa", @@ -1122,6 +1123,7 @@ dependencies = [ "idevice", "ns-keyed-archive", "plist", + "plist-macro", "sha2", "tokio", "tracing", @@ -1730,6 +1732,15 @@ dependencies = [ "time", ] +[[package]] +name = "plist-macro" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb72007326fe20721ef27304fcf2d1bd5877b92d13dbd8df735fd33407e31c2a" +dependencies = [ + "plist", +] + [[package]] name = "plist_ffi" version = "0.1.6" diff --git a/idevice/Cargo.toml b/idevice/Cargo.toml index ab47bdf..20605c2 100644 --- a/idevice/Cargo.toml +++ b/idevice/Cargo.toml @@ -21,6 +21,7 @@ tokio-openssl = { version = "0.6", optional = true } openssl = { version = "0.10", optional = true } plist = { version = "1.8" } +plist-macro = { version = "0.1" } serde = { version = "1", features = ["derive"] } ns-keyed-archive = { version = "0.1.4", optional = true } crossfire = { version = "2.1", optional = true } diff --git a/idevice/src/cursor.rs b/idevice/src/cursor.rs new file mode 100644 index 0000000..20d0df4 --- /dev/null +++ b/idevice/src/cursor.rs @@ -0,0 +1,334 @@ +// Jackson Coxson + +#[derive(Clone, Debug)] +pub struct Cursor<'a> { + inner: &'a [u8], + pos: usize, +} + +impl<'a> Cursor<'a> { + /// Creates a new cursor + pub fn new(inner: &'a [u8]) -> Self { + Self { inner, pos: 0 } + } + + pub fn len(&self) -> usize { + self.inner.len() + } + + pub fn is_empty(&self) -> bool { + self.inner.is_empty() + } + + pub fn at_end(&self) -> bool { + self.pos == self.inner.len() + } + + pub fn read(&mut self, to_read: usize) -> Option<&'a [u8]> { + // Check if the end of the slice (self.pos + to_read) is beyond the buffer length + if self + .pos + .checked_add(to_read) + .is_none_or(|end_pos| end_pos > self.inner.len()) + { + return None; + } + + // The end of the slice is self.pos + to_read + let end_pos = self.pos + to_read; + let res = Some(&self.inner[self.pos..end_pos]); + self.pos = end_pos; + res + } + + pub fn back(&mut self, to_back: usize) { + let to_back = if to_back > self.pos { + self.pos + } else { + to_back + }; + + self.pos -= to_back; + } + + /// True if actually all zeroes + pub fn read_assert_zero(&mut self, to_read: usize) -> Option<()> { + let bytes = self.read(to_read)?; + + #[cfg(debug_assertions)] + for b in bytes.iter() { + if *b > 0 { + eprintln!("Zero read contained non-zero values!"); + eprintln!("{bytes:02X?}"); + return None; + } + } + + Some(()) + } + + pub fn read_to(&mut self, end: usize) -> Option<&'a [u8]> { + if end > self.inner.len() { + return None; + } + let res = Some(&self.inner[self.pos..end]); + self.pos = end; + res + } + + pub fn peek_to(&mut self, end: usize) -> Option<&'a [u8]> { + if end > self.inner.len() { + return None; + } + Some(&self.inner[self.pos..end]) + } + + pub fn peek(&self, to_read: usize) -> Option<&'a [u8]> { + if self + .pos + .checked_add(to_read) + .is_none_or(|end_pos| end_pos > self.inner.len()) + { + return None; + } + + let end_pos = self.pos + to_read; + Some(&self.inner[self.pos..end_pos]) + } + + pub fn reveal(&self, surrounding: usize) { + let len = self.inner.len(); + + if self.pos > len { + println!("Cursor is past end of buffer"); + return; + } + + let start = self.pos.saturating_sub(surrounding); + let end = (self.pos + surrounding + 1).min(len); + + // HEADER + println!("Reveal around pos {} ({} bytes):", self.pos, surrounding); + + // --- HEX LINE --- + print!("Hex: "); + for i in start..end { + if i == self.pos { + print!("[{:02X}] ", self.inner[i]); + } else { + print!("{:02X} ", self.inner[i]); + } + } + println!(); + + // --- ASCII LINE --- + print!("Ascii: "); + for i in start..end { + let b = self.inner[i]; + let c = if b.is_ascii_graphic() || b == b' ' { + b as char + } else { + '.' + }; + + if i == self.pos { + print!("[{}] ", c); + } else { + print!("{} ", c); + } + } + println!(); + + // --- OFFSET LINE --- + print!("Offset: "); + for i in start..end { + let off = i as isize - self.pos as isize; + if i == self.pos { + print!("[{}] ", off); + } else { + print!("{:<3} ", off); + } + } + println!(); + } + + pub fn remaining(&mut self) -> &'a [u8] { + let res = &self.inner[self.pos..]; + self.pos = self.inner.len(); + res + } + + pub fn read_u8(&mut self) -> Option { + if self.pos == self.inner.len() { + return None; + } + let res = Some(self.inner[self.pos]); + self.pos += 1; + res + } + + pub fn read_le_u16(&mut self) -> Option { + const SIZE: usize = 2; + let bytes = self.read(SIZE)?; + let bytes: [u8; SIZE] = bytes.try_into().unwrap(); + Some(u16::from_le_bytes(bytes)) + } + + pub fn read_be_u16(&mut self) -> Option { + const SIZE: usize = 2; + let bytes = self.read(SIZE)?; + let bytes: [u8; SIZE] = bytes.try_into().unwrap(); + Some(u16::from_be_bytes(bytes)) + } + + pub fn read_le_u32(&mut self) -> Option { + const SIZE: usize = 4; + let bytes = self.read(SIZE)?; + let bytes: [u8; SIZE] = bytes.try_into().unwrap(); + Some(u32::from_le_bytes(bytes)) + } + + pub fn read_be_u32(&mut self) -> Option { + const SIZE: usize = 4; + let bytes = self.read(SIZE)?; + let bytes: [u8; SIZE] = bytes.try_into().unwrap(); + Some(u32::from_be_bytes(bytes)) + } + + pub fn read_le_u64(&mut self) -> Option { + const SIZE: usize = 8; + let bytes = self.read(SIZE)?; + let bytes: [u8; SIZE] = bytes.try_into().unwrap(); + Some(u64::from_le_bytes(bytes)) + } + + pub fn read_be_u64(&mut self) -> Option { + const SIZE: usize = 8; + let bytes = self.read(SIZE)?; + let bytes: [u8; SIZE] = bytes.try_into().unwrap(); + Some(u64::from_be_bytes(bytes)) + } + + pub fn read_le_u128(&mut self) -> Option { + const SIZE: usize = 16; + let bytes = self.read(SIZE)?; + let bytes: [u8; SIZE] = bytes.try_into().unwrap(); + Some(u128::from_le_bytes(bytes)) + } + + pub fn read_be_u128(&mut self) -> Option { + const SIZE: usize = 16; + let bytes = self.read(SIZE)?; + let bytes: [u8; SIZE] = bytes.try_into().unwrap(); + Some(u128::from_be_bytes(bytes)) + } + + pub fn read_le_f32(&mut self) -> Option { + const SIZE: usize = 4; + let bytes = self.read(SIZE)?; + let bytes: [u8; SIZE] = bytes.try_into().unwrap(); + Some(f32::from_le_bytes(bytes)) + } + + pub fn read_be_f32(&mut self) -> Option { + const SIZE: usize = 4; + let bytes = self.read(SIZE)?; + let bytes: [u8; SIZE] = bytes.try_into().unwrap(); + Some(f32::from_be_bytes(bytes)) + } + + pub fn read_i8(&mut self) -> Option { + if self.pos == self.inner.len() { + return None; + } + let res = Some(self.inner[self.pos]).map(|x| x as i8); + self.pos += 1; + res + } + + pub fn read_le_i16(&mut self) -> Option { + const SIZE: usize = 2; + let bytes = self.read(SIZE)?; + let bytes: [u8; SIZE] = bytes.try_into().unwrap(); + Some(i16::from_le_bytes(bytes)) + } + + pub fn read_be_i16(&mut self) -> Option { + const SIZE: usize = 2; + let bytes = self.read(SIZE)?; + let bytes: [u8; SIZE] = bytes.try_into().unwrap(); + Some(i16::from_be_bytes(bytes)) + } + + pub fn read_le_i32(&mut self) -> Option { + const SIZE: usize = 4; + let bytes = self.read(SIZE)?; + let bytes: [u8; SIZE] = bytes.try_into().unwrap(); + Some(i32::from_le_bytes(bytes)) + } + + pub fn read_be_i32(&mut self) -> Option { + const SIZE: usize = 4; + let bytes = self.read(SIZE)?; + let bytes: [u8; SIZE] = bytes.try_into().unwrap(); + Some(i32::from_be_bytes(bytes)) + } + + pub fn read_le_i64(&mut self) -> Option { + const SIZE: usize = 8; + let bytes = self.read(SIZE)?; + let bytes: [u8; SIZE] = bytes.try_into().unwrap(); + Some(i64::from_le_bytes(bytes)) + } + + pub fn read_be_i64(&mut self) -> Option { + const SIZE: usize = 8; + let bytes = self.read(SIZE)?; + let bytes: [u8; SIZE] = bytes.try_into().unwrap(); + Some(i64::from_be_bytes(bytes)) + } + + pub fn read_le_i128(&mut self) -> Option { + const SIZE: usize = 16; + let bytes = self.read(SIZE)?; + let bytes: [u8; SIZE] = bytes.try_into().unwrap(); + Some(i128::from_le_bytes(bytes)) + } + + pub fn read_be_i128(&mut self) -> Option { + const SIZE: usize = 16; + let bytes = self.read(SIZE)?; + let bytes: [u8; SIZE] = bytes.try_into().unwrap(); + Some(i128::from_be_bytes(bytes)) + } + + pub fn take_2(&mut self) -> Option<[u8; 2]> { + let bytes = self.read(2)?; + Some(bytes.to_owned().try_into().unwrap()) + } + + pub fn take_3(&mut self) -> Option<[u8; 3]> { + let bytes = self.read(3)?; + Some(bytes.to_owned().try_into().unwrap()) + } + + pub fn take_4(&mut self) -> Option<[u8; 4]> { + let bytes = self.read(4)?; + Some(bytes.to_owned().try_into().unwrap()) + } + + pub fn take_8(&mut self) -> Option<[u8; 8]> { + let bytes = self.read(8)?; + Some(bytes.to_owned().try_into().unwrap()) + } + + pub fn take_20(&mut self) -> Option<[u8; 20]> { + let bytes = self.read(20)?; + Some(bytes.to_owned().try_into().unwrap()) + } + + pub fn take_32(&mut self) -> Option<[u8; 32]> { + let bytes = self.read(32)?; + Some(bytes.to_owned().try_into().unwrap()) + } +} diff --git a/idevice/src/lib.rs b/idevice/src/lib.rs index cb62deb..5aa23d6 100644 --- a/idevice/src/lib.rs +++ b/idevice/src/lib.rs @@ -5,8 +5,9 @@ #[cfg(all(feature = "pair", feature = "rustls"))] mod ca; +pub mod cursor; +mod obfuscation; pub mod pairing_file; -pub mod plist_macro; pub mod provider; #[cfg(feature = "rustls")] mod sni; @@ -18,7 +19,6 @@ pub mod tss; pub mod tunneld; #[cfg(feature = "usbmuxd")] pub mod usbmuxd; -mod util; pub mod utils; #[cfg(feature = "xpc")] pub mod xpc; @@ -29,6 +29,7 @@ pub use services::*; #[cfg(feature = "xpc")] pub use xpc::RemoteXpcClient; +use plist_macro::{plist, pretty_print_dictionary, pretty_print_plist}; use provider::{IdeviceProvider, RsdProvider}; #[cfg(feature = "rustls")] use rustls::{crypto::CryptoProvider, pki_types::ServerName}; @@ -40,8 +41,6 @@ use thiserror::Error; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; use tracing::{debug, trace}; -pub use util::{pretty_print_dictionary, pretty_print_plist}; - use crate::services::lockdown::LockdownClient; /// A trait combining all required characteristics for a device communication socket @@ -192,7 +191,7 @@ impl Idevice { /// # Errors /// Returns `IdeviceError` if communication fails or response is invalid pub async fn get_type(&mut self) -> Result { - let req = crate::plist!({ + let req = plist!({ "Label": self.label.clone(), "Request": "QueryType", }); @@ -212,7 +211,7 @@ impl Idevice { /// # Errors /// Returns `IdeviceError` if the protocol sequence isn't followed correctly pub async fn rsd_checkin(&mut self) -> Result<(), IdeviceError> { - let req = crate::plist!({ + let req = plist!({ "Label": self.label.clone(), "ProtocolVersion": "2", "Request": "RSDCheckin", diff --git a/idevice/src/obfuscation.rs b/idevice/src/obfuscation.rs new file mode 100644 index 0000000..d101c09 --- /dev/null +++ b/idevice/src/obfuscation.rs @@ -0,0 +1,15 @@ +// Jackson Coxson + +#[macro_export] +macro_rules! obf { + ($lit:literal) => {{ + #[cfg(feature = "obfuscate")] + { + std::borrow::Cow::Owned(obfstr::obfstr!($lit).to_string()) + } + #[cfg(not(feature = "obfuscate"))] + { + std::borrow::Cow::Borrowed($lit) + } + }}; +} diff --git a/idevice/src/plist_macro.rs b/idevice/src/plist_macro.rs deleted file mode 100644 index 3453b43..0000000 --- a/idevice/src/plist_macro.rs +++ /dev/null @@ -1,714 +0,0 @@ -// Jackson Coxson -// Ported from serde's json! - -/// Construct a `plist::Value` from a JSON-like literal. -/// -/// ``` -/// # use idevice::plist; -/// # -/// let value = plist!({ -/// "code": 200, -/// "success": true, -/// "payload": { -/// "features": [ -/// "serde", -/// "plist" -/// ], -/// "homepage": null -/// } -/// }); -/// ``` -/// -/// Variables or expressions can be interpolated into the plist literal. Any type -/// interpolated into an array element or object value must implement `Into`. -/// If the conversion fails, the `plist!` macro will panic. -/// -/// ``` -/// # use idevice::plist; -/// # -/// let code = 200; -/// let features = vec!["serde", "plist"]; -/// -/// let value = plist!({ -/// "code": code, -/// "success": code == 200, -/// "payload": { -/// features[0]: features[1] -/// } -/// }); -/// ``` -/// -/// Trailing commas are allowed inside both arrays and objects. -/// -/// ``` -/// # use idevice::plist; -/// # -/// let value = plist!([ -/// "notice", -/// "the", -/// "trailing", -/// "comma -->", -/// ]); -/// ``` -#[macro_export] -macro_rules! plist { - // Force: dictionary out - (dict { $($tt:tt)+ }) => {{ - let mut object = plist::Dictionary::new(); - $crate::plist_internal!(@object object () ($($tt)+) ($($tt)+)); - object - }}; - - // Force: value out (explicit, though default already does this) - (value { $($tt:tt)+ }) => { - $crate::plist_internal!({ $($tt)+ }) - }; - - // Force: raw vec of plist::Value out - (array [ $($tt:tt)+ ]) => { - $crate::plist_internal!(@array [] $($tt)+) - }; - - // Hide distracting implementation details from the generated rustdoc. - ($($plist:tt)+) => { - $crate::plist_internal!($($plist)+) - }; -} - -#[macro_export] -#[doc(hidden)] -macro_rules! plist_internal { - ////////////////////////////////////////////////////////////////////////// - // TT muncher for parsing the inside of an array [...]. Produces a vec![...] - // of the elements. - // - // Must be invoked as: plist_internal!(@array [] $($tt)*) - ////////////////////////////////////////////////////////////////////////// - - // Done with trailing comma. - (@array [$($elems:expr,)*]) => { - vec![$($elems,)*] - }; - - // Done without trailing comma. - (@array [$($elems:expr),*]) => { - vec![$($elems),*] - }; - - // Next element is `null`. - (@array [$($elems:expr,)*] null $($rest:tt)*) => { - $crate::plist_internal!(@array [$($elems,)* $crate::plist_internal!(null)] $($rest)*) - }; - - // Next element is `true`. - (@array [$($elems:expr,)*] true $($rest:tt)*) => { - $crate::plist_internal!(@array [$($elems,)* $crate::plist_internal!(true)] $($rest)*) - }; - - // Next element is `false`. - (@array [$($elems:expr,)*] false $($rest:tt)*) => { - $crate::plist_internal!(@array [$($elems,)* $crate::plist_internal!(false)] $($rest)*) - }; - - // Next element is an array. - (@array [$($elems:expr,)*] [$($array:tt)*] $($rest:tt)*) => { - $crate::plist_internal!(@array [$($elems,)* $crate::plist_internal!([$($array)*])] $($rest)*) - }; - - // Next element is a map. - (@array [$($elems:expr,)*] {$($map:tt)*} $($rest:tt)*) => { - $crate::plist_internal!(@array [$($elems,)* $crate::plist_internal!({$($map)*})] $($rest)*) - }; - - // Next element is an expression followed by comma. - (@array [$($elems:expr,)*] $next:expr, $($rest:tt)*) => { - $crate::plist_internal!(@array [$($elems,)* $crate::plist_internal!($next),] $($rest)*) - }; - - // Last element is an expression with no trailing comma. - (@array [$($elems:expr,)*] $last:expr) => { - $crate::plist_internal!(@array [$($elems,)* $crate::plist_internal!($last)]) - }; - - // Comma after the most recent element. - (@array [$($elems:expr),*] , $($rest:tt)*) => { - $crate::plist_internal!(@array [$($elems,)*] $($rest)*) - }; - - // Unexpected token after most recent element. - (@array [$($elems:expr),*] $unexpected:tt $($rest:tt)*) => { - $crate::plist_unexpected!($unexpected) - }; - - (@array [$($elems:expr,)*] ? $maybe:expr , $($rest:tt)*) => { - if let Some(__v) = $crate::plist_macro::plist_maybe($maybe) { - $crate::plist_internal!(@array [$($elems,)* __v,] $($rest)*) - } else { - $crate::plist_internal!(@array [$($elems,)*] $($rest)*) - } - }; - (@array [$($elems:expr,)*] ? $maybe:expr) => { - if let Some(__v) = $crate::plist_macro::plist_maybe($maybe) { - $crate::plist_internal!(@array [$($elems,)* __v]) - } else { - $crate::plist_internal!(@array [$($elems,)*]) - } - }; - - ////////////////////////////////////////////////////////////////////////// - // TT muncher for parsing the inside of an object {...}. Each entry is - // inserted into the given map variable. - // - // Must be invoked as: plist_internal!(@object $map () ($($tt)*) ($($tt)*)) - // - // We require two copies of the input tokens so that we can match on one - // copy and trigger errors on the other copy. - ////////////////////////////////////////////////////////////////////////// - - // Done. - (@object $object:ident () () ()) => {}; - - // Insert the current entry followed by trailing comma. - (@object $object:ident [$($key:tt)+] ($value:expr) , $($rest:tt)*) => { - let _ = $object.insert(($($key)+).into(), $value); - $crate::plist_internal!(@object $object () ($($rest)*) ($($rest)*)); - }; - - // Current entry followed by unexpected token. - (@object $object:ident [$($key:tt)+] ($value:expr) $unexpected:tt $($rest:tt)*) => { - $crate::plist_unexpected!($unexpected); - }; - - // Insert the last entry without trailing comma. - (@object $object:ident [$($key:tt)+] ($value:expr)) => { - let _ = $object.insert(($($key)+).into(), $value); - }; - - // Next value is `null`. - (@object $object:ident ($($key:tt)+) (: null $($rest:tt)*) $copy:tt) => { - $crate::plist_internal!(@object $object [$($key)+] ($crate::plist_internal!(null)) $($rest)*); - }; - - // Next value is `true`. - (@object $object:ident ($($key:tt)+) (: true $($rest:tt)*) $copy:tt) => { - $crate::plist_internal!(@object $object [$($key)+] ($crate::plist_internal!(true)) $($rest)*); - }; - - // Next value is `false`. - (@object $object:ident ($($key:tt)+) (: false $($rest:tt)*) $copy:tt) => { - $crate::plist_internal!(@object $object [$($key)+] ($crate::plist_internal!(false)) $($rest)*); - }; - - // Next value is an array. - (@object $object:ident ($($key:tt)+) (: [$($array:tt)*] $($rest:tt)*) $copy:tt) => { - $crate::plist_internal!(@object $object [$($key)+] ($crate::plist_internal!([$($array)*])) $($rest)*); - }; - - // Next value is a map. - (@object $object:ident ($($key:tt)+) (: {$($map:tt)*} $($rest:tt)*) $copy:tt) => { - $crate::plist_internal!(@object $object [$($key)+] ($crate::plist_internal!({$($map)*})) $($rest)*); - }; - - // Optional insert with trailing comma: key?: expr, - (@object $object:ident ($($key:tt)+) (:? $value:expr , $($rest:tt)*) $copy:tt) => { - if let Some(__v) = $crate::plist_macro::plist_maybe($value) { - let _ = $object.insert(($($key)+).into(), __v); - } - $crate::plist_internal!(@object $object () ($($rest)*) ($($rest)*)); - }; - - // Optional insert, last entry: key?: expr - (@object $object:ident ($($key:tt)+) (:? $value:expr) $copy:tt) => { - if let Some(__v) = $crate::plist_macro::plist_maybe($value) { - let _ = $object.insert(($($key)+).into(), __v); - } - }; - - (@object $object:ident () ( :< $value:expr , $($rest:tt)*) $copy:tt) => { - { - let __v = $crate::plist_internal!($value); - let __dict = $crate::plist_macro::IntoPlistDict::into_plist_dict(__v); - for (__k, __val) in __dict { - let _ = $object.insert(__k, __val); - } - } - $crate::plist_internal!(@object $object () ($($rest)*) ($($rest)*)); - }; - - // Merge: last entry `:< expr` - (@object $object:ident () ( :< $value:expr ) $copy:tt) => { - { - let __v = $crate::plist_internal!($value); - let __dict = $crate::plist_macro::IntoPlistDict::into_plist_dict(__v); - for (__k, __val) in __dict { - let _ = $object.insert(__k, __val); - } - } - }; - - // Optional merge: `:< ? expr,` — only merge if Some(...) - (@object $object:ident () ( :< ? $value:expr , $($rest:tt)*) $copy:tt) => { - if let Some(__dict) = $crate::plist_macro::maybe_into_dict($value) { - for (__k, __val) in __dict { - let _ = $object.insert(__k, __val); - } - } - $crate::plist_internal!(@object $object () ($($rest)*) ($($rest)*)); - }; - - // Optional merge: last entry `:< ? expr` - (@object $object:ident () ( :< ? $value:expr ) $copy:tt) => { - if let Some(__dict) = $crate::plist_macro::maybe_into_dict($value) { - for (__k, __val) in __dict { - let _ = $object.insert(__k, __val); - } - } - }; - - // Next value is an expression followed by comma. - (@object $object:ident ($($key:tt)+) (: $value:expr , $($rest:tt)*) $copy:tt) => { - $crate::plist_internal!(@object $object [$($key)+] ($crate::plist_internal!($value)) , $($rest)*); - }; - - // Last value is an expression with no trailing comma. - (@object $object:ident ($($key:tt)+) (: $value:expr) $copy:tt) => { - $crate::plist_internal!(@object $object [$($key)+] ($crate::plist_internal!($value))); - }; - - // Missing value for last entry. Trigger a reasonable error message. - (@object $object:ident ($($key:tt)+) (:) $copy:tt) => { - // "unexpected end of macro invocation" - $crate::plist_internal!(); - }; - - // Missing colon and value for last entry. Trigger a reasonable error - // message. - (@object $object:ident ($($key:tt)+) () $copy:tt) => { - // "unexpected end of macro invocation" - $crate::plist_internal!(); - }; - - // Misplaced colon. Trigger a reasonable error message. - (@object $object:ident () (: $($rest:tt)*) ($colon:tt $($copy:tt)*)) => { - // Takes no arguments so "no rules expected the token `:`". - $crate::plist_unexpected!($colon); - }; - - // Found a comma inside a key. Trigger a reasonable error message. - (@object $object:ident ($($key:tt)*) (, $($rest:tt)*) ($comma:tt $($copy:tt)*)) => { - // Takes no arguments so "no rules expected the token `,`". - $crate::plist_unexpected!($comma); - }; - - // Key is fully parenthesized. This avoids clippy double_parens false - // positives because the parenthesization may be necessary here. - (@object $object:ident () (($key:expr) : $($rest:tt)*) $copy:tt) => { - $crate::plist_internal!(@object $object ($key) (: $($rest)*) (: $($rest)*)); - }; - - // Refuse to absorb colon token into key expression. - (@object $object:ident ($($key:tt)*) (: $($unexpected:tt)+) $copy:tt) => { - $crate::plist_expect_expr_comma!($($unexpected)+); - }; - - // Munch a token into the current key. - (@object $object:ident ($($key:tt)*) ($tt:tt $($rest:tt)*) $copy:tt) => { - $crate::plist_internal!(@object $object ($($key)* $tt) ($($rest)*) ($($rest)*)); - }; - - ////////////////////////////////////////////////////////////////////////// - // The main implementation. - // - // Must be invoked as: plist_internal!($($plist)+) - ////////////////////////////////////////////////////////////////////////// - - (null) => { - plist::Value::String("".to_string()) // plist doesn't have null, use empty string or consider other representation - }; - - (true) => { - plist::Value::Boolean(true) - }; - - (false) => { - plist::Value::Boolean(false) - }; - - ([]) => { - plist::Value::Array(vec![]) - }; - - ([ $($tt:tt)+ ]) => { - plist::Value::Array($crate::plist_internal!(@array [] $($tt)+)) - }; - - ({}) => { - plist::Value::Dictionary(plist::Dictionary::new()) - }; - - ({ $($tt:tt)+ }) => { - plist::Value::Dictionary({ - let mut object = plist::Dictionary::new(); - $crate::plist_internal!(@object object () ($($tt)+) ($($tt)+)); - object - }) - }; - - // Any type that can be converted to plist::Value: numbers, strings, variables etc. - // Must be below every other rule. - ($other:expr) => { - $crate::plist_macro::plist_to_value($other) - }; -} - -#[macro_export] -#[doc(hidden)] -macro_rules! plist_unexpected { - () => {}; -} - -#[macro_export] -#[doc(hidden)] -macro_rules! plist_expect_expr_comma { - ($e:expr , $($tt:tt)*) => {}; -} - -// Helper function to convert various types to plist::Value -#[doc(hidden)] -pub fn plist_to_value(value: T) -> plist::Value { - value.to_plist_value() -} - -// Trait for types that can be converted to plist::Value -pub trait PlistConvertible { - fn to_plist_value(self) -> plist::Value; -} - -// Implementations for common types -impl PlistConvertible for plist::Value { - fn to_plist_value(self) -> plist::Value { - self - } -} - -impl PlistConvertible for String { - fn to_plist_value(self) -> plist::Value { - plist::Value::String(self) - } -} - -impl PlistConvertible for &str { - fn to_plist_value(self) -> plist::Value { - plist::Value::String(self.to_string()) - } -} - -impl PlistConvertible for i16 { - fn to_plist_value(self) -> plist::Value { - plist::Value::Integer(self.into()) - } -} - -impl PlistConvertible for i32 { - fn to_plist_value(self) -> plist::Value { - plist::Value::Integer(self.into()) - } -} - -impl PlistConvertible for i64 { - fn to_plist_value(self) -> plist::Value { - plist::Value::Integer(self.into()) - } -} - -impl PlistConvertible for u16 { - fn to_plist_value(self) -> plist::Value { - plist::Value::Integer((self as i64).into()) - } -} - -impl PlistConvertible for u32 { - fn to_plist_value(self) -> plist::Value { - plist::Value::Integer((self as i64).into()) - } -} - -impl PlistConvertible for u64 { - fn to_plist_value(self) -> plist::Value { - plist::Value::Integer((self as i64).into()) - } -} - -impl PlistConvertible for f32 { - fn to_plist_value(self) -> plist::Value { - plist::Value::Real(self as f64) - } -} - -impl PlistConvertible for f64 { - fn to_plist_value(self) -> plist::Value { - plist::Value::Real(self) - } -} - -impl PlistConvertible for bool { - fn to_plist_value(self) -> plist::Value { - plist::Value::Boolean(self) - } -} - -impl<'a> PlistConvertible for std::borrow::Cow<'a, str> { - fn to_plist_value(self) -> plist::Value { - plist::Value::String(self.into_owned()) - } -} -impl PlistConvertible for Vec { - fn to_plist_value(self) -> plist::Value { - plist::Value::Data(self) - } -} -impl PlistConvertible for &[u8] { - fn to_plist_value(self) -> plist::Value { - plist::Value::Data(self.to_vec()) - } -} -impl PlistConvertible for std::time::SystemTime { - fn to_plist_value(self) -> plist::Value { - plist::Value::Date(self.into()) - } -} - -impl PlistConvertible for Vec { - fn to_plist_value(self) -> plist::Value { - plist::Value::Array(self.into_iter().map(|item| item.to_plist_value()).collect()) - } -} - -impl PlistConvertible for &[T] { - fn to_plist_value(self) -> plist::Value { - plist::Value::Array( - self.iter() - .map(|item| item.clone().to_plist_value()) - .collect(), - ) - } -} - -impl PlistConvertible for [T; N] { - fn to_plist_value(self) -> plist::Value { - plist::Value::Array(self.into_iter().map(|item| item.to_plist_value()).collect()) - } -} - -impl PlistConvertible for &[T; N] { - fn to_plist_value(self) -> plist::Value { - plist::Value::Array( - self.iter() - .map(|item| item.clone().to_plist_value()) - .collect(), - ) - } -} - -impl PlistConvertible for plist::Dictionary { - fn to_plist_value(self) -> plist::Value { - plist::Value::Dictionary(self) - } -} - -impl PlistConvertible for std::collections::HashMap -where - K: Into, - V: PlistConvertible, -{ - fn to_plist_value(self) -> plist::Value { - let mut dict = plist::Dictionary::new(); - for (key, value) in self { - dict.insert(key.into(), value.to_plist_value()); - } - plist::Value::Dictionary(dict) - } -} - -impl PlistConvertible for std::collections::BTreeMap -where - K: Into, - V: PlistConvertible, -{ - fn to_plist_value(self) -> plist::Value { - let mut dict = plist::Dictionary::new(); - for (key, value) in self { - dict.insert(key.into(), value.to_plist_value()); - } - plist::Value::Dictionary(dict) - } -} - -// Treat plain T as Some(T) and Option as-is. -pub trait MaybePlist { - fn into_option_value(self) -> Option; -} - -impl MaybePlist for T { - fn into_option_value(self) -> Option { - Some(self.to_plist_value()) - } -} - -impl MaybePlist for Option { - fn into_option_value(self) -> Option { - self.map(|v| v.to_plist_value()) - } -} - -#[doc(hidden)] -pub fn plist_maybe(v: T) -> Option { - v.into_option_value() -} - -// Convert things into a Dictionary we can merge. -pub trait IntoPlistDict { - fn into_plist_dict(self) -> plist::Dictionary; -} - -impl IntoPlistDict for plist::Dictionary { - fn into_plist_dict(self) -> plist::Dictionary { - self - } -} - -impl IntoPlistDict for plist::Value { - fn into_plist_dict(self) -> plist::Dictionary { - match self { - plist::Value::Dictionary(d) => d, - other => panic!("plist :< expects a dictionary, got {other:?}"), - } - } -} - -impl IntoPlistDict for std::collections::HashMap -where - K: Into, - V: PlistConvertible, -{ - fn into_plist_dict(self) -> plist::Dictionary { - let mut d = plist::Dictionary::new(); - for (k, v) in self { - d.insert(k.into(), v.to_plist_value()); - } - d - } -} - -impl IntoPlistDict for std::collections::BTreeMap -where - K: Into, - V: PlistConvertible, -{ - fn into_plist_dict(self) -> plist::Dictionary { - let mut d = plist::Dictionary::new(); - for (k, v) in self { - d.insert(k.into(), v.to_plist_value()); - } - d - } -} - -// Optional version: T or Option. -pub trait MaybeIntoPlistDict { - fn into_option_plist_dict(self) -> Option; -} -impl MaybeIntoPlistDict for T { - fn into_option_plist_dict(self) -> Option { - Some(self.into_plist_dict()) - } -} -impl MaybeIntoPlistDict for Option { - fn into_option_plist_dict(self) -> Option { - self.map(|t| t.into_plist_dict()) - } -} - -#[doc(hidden)] -pub fn maybe_into_dict(v: T) -> Option { - v.into_option_plist_dict() -} - -#[cfg(test)] -mod tests { - #[test] - fn test_plist_macro_basic() { - let value = plist!({ - "name": "test", - "count": 42, - "active": true, - "items": ["a", ?"b", "c"] - }); - - if let plist::Value::Dictionary(dict) = value { - assert_eq!( - dict.get("name"), - Some(&plist::Value::String("test".to_string())) - ); - assert_eq!(dict.get("count"), Some(&plist::Value::Integer(42.into()))); - assert_eq!(dict.get("active"), Some(&plist::Value::Boolean(true))); - } else { - panic!("Expected dictionary"); - } - } - - #[test] - fn test_plist_macro_with_variables() { - let name = "dynamic"; - let count = 100; - let items = vec!["x", "y"]; - let none: Option = None; - - let to_merge = plist!({ - "reee": "cool beans" - }); - let maybe_merge = Some(plist!({ - "yeppers": "what did I say about yeppers", - "replace me": 2, - })); - let value = plist!({ - "name": name, - "count": count, - "items": items, - "omit me":? none, - "keep me":? Some(123), - "replace me": 1, - :< to_merge, - : AppServiceClient { "user": { "active": true, }, - "platformSpecificOptions": plist::Value::Data(crate::util::plist_to_xml_bytes(&platform_options.unwrap_or_default())), + "platformSpecificOptions": plist::Value::Data(plist_to_xml_bytes(&platform_options.unwrap_or_default())), }, }); diff --git a/idevice/src/tss.rs b/idevice/src/tss.rs index 1e02e35..89de161 100644 --- a/idevice/src/tss.rs +++ b/idevice/src/tss.rs @@ -6,9 +6,10 @@ //! - Handle cryptographic signing operations use plist::Value; +use plist_macro::plist_to_xml_bytes; use tracing::{debug, warn}; -use crate::{IdeviceError, util::plist_to_xml_bytes}; +use crate::IdeviceError; /// TSS client version string sent in requests const TSS_CLIENT_VERSION_STRING: &str = "libauthinstall-1033.0.2"; @@ -30,7 +31,7 @@ impl TSSRequest { /// - Client version string /// - Random UUID for request identification pub fn new() -> Self { - let inner = crate::plist!(dict { + let inner = plist_macro::plist!(dict { "@HostPlatformInfo": "mac", "@VersionInfo": TSS_CLIENT_VERSION_STRING, "@UUID": uuid::Uuid::new_v4().to_string().to_uppercase() diff --git a/idevice/src/usbmuxd/raw_packet.rs b/idevice/src/usbmuxd/raw_packet.rs index 0a9aaba..7bc85a4 100644 --- a/idevice/src/usbmuxd/raw_packet.rs +++ b/idevice/src/usbmuxd/raw_packet.rs @@ -1,6 +1,6 @@ // Jackson Coxson -use crate::util::plist_to_xml_bytes; +use plist_macro::plist_to_xml_bytes; use tracing::warn; #[derive(Debug)] diff --git a/idevice/src/util.rs b/idevice/src/util.rs deleted file mode 100644 index 4e69565..0000000 --- a/idevice/src/util.rs +++ /dev/null @@ -1,142 +0,0 @@ -//! Utility Functions -//! -//! Provides helper functions for working with Apple's Property List (PLIST) format, -//! including serialization and pretty-printing utilities. -#![allow(dead_code)] // functions might not be used by all features - -use plist::Value; - -/// Converts a PLIST dictionary to XML-formatted bytes -/// -/// # Arguments -/// * `p` - The PLIST dictionary to serialize -/// -/// # Returns -/// A byte vector containing the XML representation -/// -/// # Panics -/// Will panic if serialization fails (should only happen with invalid data) -/// -/// # Example -/// ```rust -/// let mut dict = plist::Dictionary::new(); -/// dict.insert("key".into(), "value".into()); -/// let xml_bytes = plist_to_xml_bytes(&dict); -/// ``` -pub fn plist_to_xml_bytes(p: &plist::Dictionary) -> Vec { - let buf = Vec::new(); - let mut writer = std::io::BufWriter::new(buf); - plist::to_writer_xml(&mut writer, &p).unwrap(); - - writer.into_inner().unwrap() -} - -/// Pretty-prints a PLIST value with indentation -/// -/// # Arguments -/// * `p` - The PLIST value to format -/// -/// # Returns -/// A formatted string representation -pub fn pretty_print_plist(p: &Value) -> String { - print_plist(p, 0) -} - -/// Pretty-prints a PLIST dictionary with key-value pairs -/// -/// # Arguments -/// * `dict` - The dictionary to format -/// -/// # Returns -/// A formatted string representation with newlines and indentation -/// -/// # Example -/// ```rust -/// let mut dict = plist::Dictionary::new(); -/// dict.insert("name".into(), "John".into()); -/// dict.insert("age".into(), 30.into()); -/// println!("{}", pretty_print_dictionary(&dict)); -/// ``` -pub fn pretty_print_dictionary(dict: &plist::Dictionary) -> String { - let items: Vec = dict - .iter() - .map(|(k, v)| format!("{}: {}", k, print_plist(v, 2))) - .collect(); - format!("{{\n{}\n}}", items.join(",\n")) -} - -/// Internal recursive function for printing PLIST values with indentation -/// -/// # Arguments -/// * `p` - The PLIST value to format -/// * `indentation` - Current indentation level -/// -/// # Returns -/// Formatted string representation -fn print_plist(p: &Value, indentation: usize) -> String { - let indent = " ".repeat(indentation); - match p { - Value::Array(vec) => { - let items: Vec = vec - .iter() - .map(|v| { - format!( - "{}{}", - " ".repeat(indentation + 2), - print_plist(v, indentation + 2) - ) - }) - .collect(); - format!("[\n{}\n{}]", items.join(",\n"), indent) - } - Value::Dictionary(dict) => { - let items: Vec = dict - .iter() - .map(|(k, v)| { - format!( - "{}{}: {}", - " ".repeat(indentation + 2), - k, - print_plist(v, indentation + 2) - ) - }) - .collect(); - format!("{{\n{}\n{}}}", items.join(",\n"), indent) - } - Value::Boolean(b) => format!("{b}"), - Value::Data(vec) => { - let len = vec.len(); - let preview: String = vec - .iter() - .take(20) - .map(|b| format!("{b:02X}")) - .collect::>() - .join(" "); - if len > 20 { - format!("Data({preview}... Len: {len})") - } else { - format!("Data({preview} Len: {len})") - } - } - Value::Date(date) => format!("Date({})", date.to_xml_format()), - Value::Real(f) => format!("{f}"), - Value::Integer(i) => format!("{i}"), - Value::String(s) => format!("\"{s}\""), - Value::Uid(_uid) => "Uid(?)".to_string(), - _ => "Unknown".to_string(), - } -} - -#[macro_export] -macro_rules! obf { - ($lit:literal) => {{ - #[cfg(feature = "obfuscate")] - { - std::borrow::Cow::Owned(obfstr::obfstr!($lit).to_string()) - } - #[cfg(not(feature = "obfuscate"))] - { - std::borrow::Cow::Borrowed($lit) - } - }}; -} diff --git a/idevice/src/utils/installation/helpers.rs b/idevice/src/utils/installation/helpers.rs index f88ce0d..8e6057f 100644 --- a/idevice/src/utils/installation/helpers.rs +++ b/idevice/src/utils/installation/helpers.rs @@ -1,13 +1,12 @@ -use std::{io::Cursor, path::Path}; - use async_zip::base::read::seek::ZipFileReader; use futures::AsyncReadExt as _; +use plist_macro::plist; +use std::{io::Cursor, path::Path}; use tokio::io::{AsyncBufRead, AsyncSeek, BufReader}; use crate::{ IdeviceError, IdeviceService, afc::{AfcClient, opcode::AfcFopenMode}, - plist, provider::IdeviceProvider, }; diff --git a/idevice/src/xpc/format.rs b/idevice/src/xpc/format.rs index f280656..5803be5 100644 --- a/idevice/src/xpc/format.rs +++ b/idevice/src/xpc/format.rs @@ -1,3 +1,4 @@ +use plist_macro::plist; use std::{ ffi::CString, io::{BufRead, Cursor, Read}, @@ -169,7 +170,7 @@ impl XPCObject { plist::Value::Dictionary(dict) } Self::FileTransfer { msg_id, data } => { - crate::plist!({ + plist!({ "msg_id": *msg_id, "data": data.to_plist(), }) diff --git a/tools/Cargo.toml b/tools/Cargo.toml index 9beaa5d..106977b 100644 --- a/tools/Cargo.toml +++ b/tools/Cargo.toml @@ -157,6 +157,7 @@ sha2 = { version = "0.10" } ureq = { version = "3" } clap = { version = "4.5" } plist = { version = "1.7" } +plist-macro = { version = "0.1" } ns-keyed-archive = "0.1.2" uuid = "1.16" futures-util = { version = "0.3" } diff --git a/tools/src/companion_proxy.rs b/tools/src/companion_proxy.rs index f37f345..8e02c99 100644 --- a/tools/src/companion_proxy.rs +++ b/tools/src/companion_proxy.rs @@ -3,9 +3,9 @@ use clap::{Arg, Command, arg}; use idevice::{ IdeviceService, RsdService, companion_proxy::CompanionProxy, - core_device_proxy::CoreDeviceProxy, pretty_print_dictionary, pretty_print_plist, - rsd::RsdHandshake, + core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake, }; +use plist_macro::{pretty_print_dictionary, pretty_print_plist}; mod common; diff --git a/tools/src/lockdown.rs b/tools/src/lockdown.rs index 2fcf138..8f654f1 100644 --- a/tools/src/lockdown.rs +++ b/tools/src/lockdown.rs @@ -1,8 +1,9 @@ // Jackson Coxson use clap::{Arg, Command, arg}; -use idevice::{IdeviceService, lockdown::LockdownClient, pretty_print_plist}; +use idevice::{IdeviceService, lockdown::LockdownClient}; use plist::Value; +use plist_macro::pretty_print_plist; mod common; diff --git a/tools/src/mounter.rs b/tools/src/mounter.rs index 8b67056..baeb4ee 100644 --- a/tools/src/mounter.rs +++ b/tools/src/mounter.rs @@ -4,10 +4,8 @@ use std::{io::Write, path::PathBuf}; use clap::{Arg, Command, arg, value_parser}; -use idevice::{ - IdeviceService, lockdown::LockdownClient, mobile_image_mounter::ImageMounter, - pretty_print_plist, -}; +use idevice::{IdeviceService, lockdown::LockdownClient, mobile_image_mounter::ImageMounter}; +use plist_macro::pretty_print_plist; mod common; diff --git a/tools/src/restore_service.rs b/tools/src/restore_service.rs index e2a2efd..37b5a93 100644 --- a/tools/src/restore_service.rs +++ b/tools/src/restore_service.rs @@ -2,9 +2,10 @@ use clap::{Arg, Command}; use idevice::{ - IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, pretty_print_dictionary, + IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, restore_service::RestoreServiceClient, rsd::RsdHandshake, }; +use plist_macro::pretty_print_dictionary; mod common; From 166c4978780bffa1c7e18d29af79cf7d71a06859 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Sat, 3 Jan 2026 16:37:55 -0700 Subject: [PATCH 09/34] Implement seek and tell for AFC (#42) --- ffi/src/afc.rs | 122 +++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 113 insertions(+), 9 deletions(-) diff --git a/ffi/src/afc.rs b/ffi/src/afc.rs index 65664af..549ab7a 100644 --- a/ffi/src/afc.rs +++ b/ffi/src/afc.rs @@ -1,12 +1,13 @@ // Jackson Coxson -use std::ptr::null_mut; +use std::{io::SeekFrom, ptr::null_mut}; use idevice::{ IdeviceError, IdeviceService, - afc::{AfcClient, DeviceInfo, FileInfo}, + afc::{AfcClient, DeviceInfo, FileInfo, file::FileDescriptor}, provider::IdeviceProvider, }; +use tokio::io::{AsyncReadExt, AsyncSeekExt}; use crate::{ IdeviceFfiError, IdeviceHandle, LOCAL_RUNTIME, ffi_err, provider::IdeviceProviderHandle, @@ -555,12 +556,13 @@ pub unsafe extern "C" fn afc_file_close(handle: *mut AfcFileHandle) -> *mut Idev } } -/// Reads data from an open file +/// Reads data from an open file. This advances the cursor of the file. /// /// # Arguments /// * [`handle`] - File handle to read from /// * [`data`] - Will be set to point to the read data -/// * [`length`] - Will be set to the length of the read data +/// * [`len`] - Number of bytes to read from the file +/// * [`bytes_read`] - The number of bytes read from the file /// /// # Returns /// An IdeviceFfiError on error, null on success @@ -571,21 +573,29 @@ pub unsafe extern "C" fn afc_file_close(handle: *mut AfcFileHandle) -> *mut Idev pub unsafe extern "C" fn afc_file_read( handle: *mut AfcFileHandle, data: *mut *mut u8, - length: *mut libc::size_t, + len: usize, + bytes_read: *mut libc::size_t, ) -> *mut IdeviceFfiError { - if handle.is_null() || data.is_null() || length.is_null() { + if handle.is_null() || data.is_null() || bytes_read.is_null() { return ffi_err!(IdeviceError::FfiInvalidArg); } - let fd = unsafe { &mut *(handle as *mut idevice::afc::file::FileDescriptor) }; - let res: Result, IdeviceError> = run_sync(async move { fd.read_entire().await }); + let fd = unsafe { &mut *(handle as *mut FileDescriptor) }; + let res: Result, IdeviceError> = run_sync({ + let mut buf = Vec::with_capacity(len); + async move { + let r = fd.read(&mut buf).await?; + buf.resize(r, 0); + Ok(buf) + } + }); match res { Ok(bytes) => { let mut boxed = bytes.into_boxed_slice(); unsafe { *data = boxed.as_mut_ptr(); - *length = boxed.len(); + *bytes_read = boxed.len(); } std::mem::forget(boxed); null_mut() @@ -594,6 +604,100 @@ pub unsafe extern "C" fn afc_file_read( } } +/// Moves the read/write cursor in an open file. +/// +/// # Arguments +/// * [`handle`] - File handle whose cursor should be moved +/// * [`offset`] - Distance to move the cursor, interpreted based on `whence` +/// * [`whence`] - Origin used for the seek operation: +/// * `0` — Seek from the start of the file (`SeekFrom::Start`) +/// * `1` — Seek from the current cursor position (`SeekFrom::Current`) +/// * `2` — Seek from the end of the file (`SeekFrom::End`) +/// * [`new_pos`] - Output parameter; will be set to the new absolute cursor position +/// +/// # Returns +/// An [`IdeviceFfiError`] on error, or null on success. +/// +/// # Safety +/// All pointers must be valid and non-null. +/// +/// # Notes +/// * If `whence` is invalid, this function returns `FfiInvalidArg`. +/// * The AFC protocol may restrict seeking beyond certain bounds; such errors +/// are reported through the returned [`IdeviceFfiError`]. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn afc_file_seek( + handle: *mut AfcFileHandle, + offset: i64, + whence: libc::c_int, + new_pos: *mut libc::off_t, +) -> *mut IdeviceFfiError { + if handle.is_null() || new_pos.is_null() { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + + let fd = unsafe { &mut *(handle as *mut FileDescriptor) }; + + let seek_from = match whence { + 0 => SeekFrom::Start(offset as u64), + 1 => SeekFrom::Current(offset), + 2 => SeekFrom::End(offset), + _ => return ffi_err!(IdeviceError::FfiInvalidArg), + }; + + let res: Result = run_sync(async move { Ok(fd.seek(seek_from).await?) }); + + match res { + Ok(pos) => { + unsafe { + *new_pos = pos as libc::off_t; + } + null_mut() + } + Err(e) => ffi_err!(e), + } +} + +/// Returns the current read/write cursor position of an open file. +/// +/// # Arguments +/// * [`handle`] - File handle whose cursor should be queried +/// * [`pos`] - Output parameter; will be set to the current absolute cursor position +/// +/// # Returns +/// An [`IdeviceFfiError`] on error, or null on success. +/// +/// # Safety +/// All pointers must be valid and non-null. +/// +/// # Notes +/// This function is equivalent to performing a seek operation with +/// `SeekFrom::Current(0)` internally. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn afc_file_tell( + handle: *mut AfcFileHandle, + pos: *mut libc::off_t, +) -> *mut IdeviceFfiError { + if handle.is_null() || pos.is_null() { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + + let fd = unsafe { &mut *(handle as *mut FileDescriptor) }; + + let res: Result = + run_sync(async { Ok(fd.seek(SeekFrom::Current(0)).await?) }); + + match res { + Ok(cur) => { + unsafe { + *pos = cur as libc::off_t; + } + null_mut() + } + Err(e) => ffi_err!(e), + } +} + /// Writes data to an open file /// /// # Arguments From 2eebbff1728c2dabe7a772a6c2f0ad65452a0747 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Wed, 31 Dec 2025 21:24:37 -0700 Subject: [PATCH 10/34] Bump reqwest --- Cargo.lock | 21 ++++----------------- idevice/Cargo.toml | 2 +- 2 files changed, 5 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 295cf8e..4d56cb9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1919,9 +1919,9 @@ checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "reqwest" -version = "0.12.24" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" +checksum = "04e9018c9d814e5f30cc16a0f03271aeab3571e609612d9fe78c1aa8d11c2f62" dependencies = [ "base64", "bytes", @@ -1937,7 +1937,6 @@ dependencies = [ "pin-project-lite", "serde", "serde_json", - "serde_urlencoded", "sync_wrapper", "tokio", "tower", @@ -2124,18 +2123,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - [[package]] name = "sha1" version = "0.10.6" @@ -2541,9 +2528,9 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.6" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ "bitflags", "bytes", diff --git a/idevice/Cargo.toml b/idevice/Cargo.toml index 20605c2..606d3b9 100644 --- a/idevice/Cargo.toml +++ b/idevice/Cargo.toml @@ -41,7 +41,7 @@ json = { version = "0.12", optional = true } byteorder = { version = "1.5", optional = true } bytes = { version = "1.10", optional = true } -reqwest = { version = "0.12", features = [ +reqwest = { version = "0.13", features = [ "json", ], optional = true, default-features = false } rand = { version = "0.9", optional = true } From 189dd5caf2118bb720e0f349ce59dba2a6322f4e Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Sat, 3 Jan 2026 16:58:33 -0700 Subject: [PATCH 11/34] Refactor idevice tools into single binary --- Cargo.lock | 60 ++++ tools/Cargo.toml | 128 +------- tools/src/activation.rs | 115 ++------ tools/src/afc.rs | 304 +++++++++---------- tools/src/amfi.rs | 160 ++++------ tools/src/app_service.rs | 358 ++++++++++------------- tools/src/bt_packet_logger.rs | 69 +---- tools/src/companion_proxy.rs | 222 +++++++------- tools/src/crash_logs.rs | 145 ++++------ tools/src/debug_proxy.rs | 65 +---- tools/src/diagnostics.rs | 206 ++++++------- tools/src/diagnosticsservice.rs | 65 +---- tools/src/dvt_packet_parser.rs | 20 +- tools/src/heartbeat_client.rs | 58 +--- tools/src/ideviceinfo.rs | 62 +--- tools/src/ideviceinstaller.rs | 158 ++++------ tools/src/installcoordination_proxy.rs | 148 ++++------ tools/src/instproxy.rs | 162 +++++------ tools/src/location_simulation.rs | 198 ++++++------- tools/src/lockdown.rs | 144 ++++----- tools/src/main.rs | 328 +++++++++++++++++++++ tools/src/misagent.rs | 132 ++++----- tools/src/mobilebackup2.rs | 386 ++++++++++++------------- tools/src/mounter.rs | 330 ++++++++++----------- tools/src/notifications.rs | 63 +--- tools/src/os_trace_relay.rs | 57 +--- tools/src/pair.rs | 39 +-- tools/src/pcapd.rs | 53 +--- tools/src/preboard.rs | 90 ++---- tools/src/process_control.rs | 78 +---- tools/src/remotexpc.rs | 62 +--- tools/src/restore_service.rs | 163 +++++------ tools/src/screenshot.rs | 75 +---- tools/src/syslog_relay.rs | 57 +--- 34 files changed, 1983 insertions(+), 2777 deletions(-) create mode 100644 tools/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 4d56cb9..4944606 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -427,6 +427,19 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "console" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03e45a4a8926227e4197636ba97a9fc9b00477e9f4bd711395687c5f0734bec4" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "unicode-width", + "windows-sys 0.61.2", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -546,6 +559,18 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "dialoguer" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25f104b501bf2364e78d0d3974cbc774f738f5865306ed128e1e0d7499c0ad96" +dependencies = [ + "console", + "shell-words", + "tempfile", + "zeroize", +] + [[package]] name = "digest" version = "0.10.7" @@ -580,6 +605,12 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + [[package]] name = "encoding_rs" version = "0.8.35" @@ -1121,6 +1152,7 @@ dependencies = [ "clap", "futures-util", "idevice", + "jkcli", "ns-keyed-archive", "plist", "plist-macro", @@ -1202,6 +1234,16 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "jkcli" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80352668d7fb7afdf101bcedf2ec2ae77887f550114dd38502a3c9365189c06f" +dependencies = [ + "dialoguer", + "owo-colors", +] + [[package]] name = "jobserver" version = "0.1.34" @@ -1599,6 +1641,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "owo-colors" +version = "4.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52" + [[package]] name = "parking" version = "2.2.1" @@ -2154,6 +2202,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shell-words" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77" + [[package]] name = "shlex" version = "1.3.0" @@ -2674,6 +2728,12 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + [[package]] name = "untrusted" version = "0.9.0" diff --git a/tools/Cargo.toml b/tools/Cargo.toml index 106977b..28cc33c 100644 --- a/tools/Cargo.toml +++ b/tools/Cargo.toml @@ -8,26 +8,7 @@ license = "MIT" documentation = "https://docs.rs/idevice" repository = "https://github.com/jkcoxson/idevice" keywords = ["lockdownd", "ios"] - -[[bin]] -name = "ideviceinfo" -path = "src/ideviceinfo.rs" - -[[bin]] -name = "heartbeat_client" -path = "src/heartbeat_client.rs" - -[[bin]] -name = "instproxy" -path = "src/instproxy.rs" - -[[bin]] -name = "ideviceinstaller" -path = "src/ideviceinstaller.rs" - -[[bin]] -name = "mounter" -path = "src/mounter.rs" +default-run = "idevice-tools" # [[bin]] # name = "core_device_proxy_tun" @@ -37,112 +18,6 @@ path = "src/mounter.rs" name = "idevice_id" path = "src/idevice_id.rs" -[[bin]] -name = "process_control" -path = "src/process_control.rs" - -[[bin]] -name = "dvt_packet_parser" -path = "src/dvt_packet_parser.rs" - -[[bin]] -name = "remotexpc" -path = "src/remotexpc.rs" - -[[bin]] -name = "debug_proxy" -path = "src/debug_proxy.rs" - -[[bin]] -name = "misagent" -path = "src/misagent.rs" - -[[bin]] -name = "location_simulation" -path = "src/location_simulation.rs" - -[[bin]] -name = "afc" -path = "src/afc.rs" - -[[bin]] -name = "crash_logs" -path = "src/crash_logs.rs" - -[[bin]] -name = "amfi" -path = "src/amfi.rs" - -[[bin]] -name = "pair" -path = "src/pair.rs" - -[[bin]] -name = "syslog_relay" -path = "src/syslog_relay.rs" - -[[bin]] -name = "os_trace_relay" -path = "src/os_trace_relay.rs" - -[[bin]] -name = "app_service" -path = "src/app_service.rs" - -[[bin]] -name = "lockdown" -path = "src/lockdown.rs" - -[[bin]] -name = "restore_service" -path = "src/restore_service.rs" - -[[bin]] -name = "companion_proxy" -path = "src/companion_proxy.rs" - -[[bin]] -name = "diagnostics" -path = "src/diagnostics.rs" - -[[bin]] -name = "mobilebackup2" -path = "src/mobilebackup2.rs" - -[[bin]] -name = "diagnosticsservice" -path = "src/diagnosticsservice.rs" - -[[bin]] -name = "bt_packet_logger" -path = "src/bt_packet_logger.rs" - -[[bin]] -name = "pcapd" -path = "src/pcapd.rs" - -[[bin]] -name = "preboard" -path = "src/preboard.rs" - - -[[bin]] -name = "screenshot" -path = "src/screenshot.rs" - -[[bin]] -name = "activation" -path = "src/activation.rs" - -[[bin]] -name = "notifications" -path = "src/notifications.rs" - - -[[bin]] -name = "installcoordination_proxy" -path = "src/installcoordination_proxy.rs" - [[bin]] name = "iproxy" path = "src/iproxy.rs" @@ -156,6 +31,7 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] } sha2 = { version = "0.10" } ureq = { version = "3" } clap = { version = "4.5" } +jkcli = { version = "0.1" } plist = { version = "1.7" } plist-macro = { version = "0.1" } ns-keyed-archive = "0.1.2" diff --git a/tools/src/activation.rs b/tools/src/activation.rs index 86f65e5..f9dee47 100644 --- a/tools/src/activation.rs +++ b/tools/src/activation.rs @@ -1,65 +1,23 @@ // Jackson Coxson -use clap::{Arg, Command}; use idevice::{ IdeviceService, lockdown::LockdownClient, mobileactivationd::MobileActivationdClient, + provider::IdeviceProvider, }; +use jkcli::{CollectedArguments, JkCommand}; -mod common; - -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - - let matches = Command::new("activation") - .about("mobileactivationd") - .arg( - Arg::new("host") - .long("host") - .value_name("HOST") - .help("IP address of the device"), +pub fn register() -> JkCommand { + JkCommand::new() + .help("Manage activation status on an iOS device") + .with_subcommand("state", JkCommand::new().help("Gets the activation state")) + .with_subcommand( + "deactivate", + JkCommand::new().help("Deactivates the device"), ) - .arg( - Arg::new("pairing_file") - .long("pairing-file") - .value_name("PATH") - .help("Path to the pairing file"), - ) - .arg( - Arg::new("udid") - .value_name("UDID") - .help("UDID of the device (overrides host/pairing file)") - .index(1), - ) - .arg( - Arg::new("about") - .long("about") - .help("Show about information") - .action(clap::ArgAction::SetTrue), - ) - .subcommand(Command::new("state").about("Gets the activation state")) - .subcommand(Command::new("deactivate").about("Deactivates the device")) - .get_matches(); - - if matches.get_flag("about") { - println!("activation - activate the device"); - println!("Copyright (c) 2025 Jackson Coxson"); - return; - } - - let udid = matches.get_one::("udid"); - let host = matches.get_one::("host"); - let pairing_file = matches.get_one::("pairing_file"); - - let provider = match common::get_provider(udid, host, pairing_file, "activation-jkcoxson").await - { - Ok(p) => p, - Err(e) => { - eprintln!("{e}"); - return; - } - }; + .subcommand_required(true) +} +pub async fn main(arguments: &CollectedArguments, provider: Box) { let activation_client = MobileActivationdClient::new(&*provider); let mut lc = LockdownClient::connect(&*provider) .await @@ -74,40 +32,19 @@ async fn main() { .into_string() .unwrap(); - if matches.subcommand_matches("state").is_some() { - let s = activation_client.state().await.expect("no state"); - println!("Activation State: {s}"); - } else if matches.subcommand_matches("deactivate").is_some() { - println!("CAUTION: You are deactivating {udid}, press enter to continue."); - let mut input = String::new(); - std::io::stdin().read_line(&mut input).ok(); - activation_client.deactivate().await.expect("no deactivate"); - // } else if matches.subcommand_matches("accept").is_some() { - // amfi_client - // .accept_developer_mode() - // .await - // .expect("Failed to show"); - // } else if matches.subcommand_matches("status").is_some() { - // let status = amfi_client - // .get_developer_mode_status() - // .await - // .expect("Failed to get status"); - // println!("Enabled: {status}"); - // } else if let Some(matches) = matches.subcommand_matches("state") { - // let uuid: &String = match matches.get_one("uuid") { - // Some(u) => u, - // None => { - // eprintln!("No UUID passed. Invalid usage, pass -h for help"); - // return; - // } - // }; - // let status = amfi_client - // .trust_app_signer(uuid) - // .await - // .expect("Failed to get state"); - // println!("Enabled: {status}"); - } else { - eprintln!("Invalid usage, pass -h for help"); + let (sub_name, _sub_args) = arguments.first_subcommand().expect("no subarg passed"); + + match sub_name.as_str() { + "state" => { + let s = activation_client.state().await.expect("no state"); + println!("Activation State: {s}"); + } + "deactivate" => { + println!("CAUTION: You are deactivating {udid}, press enter to continue."); + let mut input = String::new(); + std::io::stdin().read_line(&mut input).ok(); + activation_client.deactivate().await.expect("no deactivate"); + } + _ => unreachable!(), } - return; } diff --git a/tools/src/afc.rs b/tools/src/afc.rs index e1fbb5f..f08719b 100644 --- a/tools/src/afc.rs +++ b/tools/src/afc.rs @@ -2,130 +2,119 @@ use std::path::PathBuf; -use clap::{Arg, Command, value_parser}; use idevice::{ IdeviceService, afc::{AfcClient, opcode::AfcFopenMode}, house_arrest::HouseArrestClient, + provider::IdeviceProvider, }; +use jkcli::{CollectedArguments, JkArgument, JkCommand, JkFlag}; -mod common; +const DOCS_HELP: &str = "Read the documents from a bundle. Note that when vending documents, you can only access files in /Documents"; -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - - let matches = Command::new("afc") - .about("Manage files on the device") - .arg( - Arg::new("host") - .long("host") - .value_name("HOST") - .help("IP address of the device"), +pub fn register() -> JkCommand { + JkCommand::new() + .help("Manage files in the AFC jail of a device") + .with_flag( + JkFlag::new("documents") + .with_help(DOCS_HELP) + .with_argument(JkArgument::new().required(true)), ) - .arg( - Arg::new("pairing_file") - .long("pairing-file") - .value_name("PATH") - .help("Path to the pairing file"), + .with_flag( + JkFlag::new("container") + .with_help("Read the container contents of a bundle") + .with_argument(JkArgument::new().required(true)), ) - .arg( - Arg::new("udid") - .long("udid") - .value_name("UDID") - .help("UDID of the device (overrides host/pairing file)"), - ) - .arg( - Arg::new("documents") - .long("documents") - .value_name("BUNDLE_ID") - .help("Read the documents from a bundle. Note that when vending documents, you can only access files in /Documents") - .global(true), - ) - .arg( - Arg::new("container") - .long("container") - .value_name("BUNDLE_ID") - .help("Read the container contents of a bundle") - .global(true), - ) - .arg( - Arg::new("about") - .long("about") - .help("Show about information") - .action(clap::ArgAction::SetTrue), - ) - .subcommand( - Command::new("list") - .about("Lists the items in the directory") - .arg(Arg::new("path").required(true).index(1)), - ) - .subcommand( - Command::new("download") - .about("Downloads a file") - .arg(Arg::new("path").required(true).index(1)) - .arg(Arg::new("save").required(true).index(2)), - ) - .subcommand( - Command::new("upload") - .about("Creates a directory") - .arg( - Arg::new("file") + .with_subcommand( + "list", + JkCommand::new() + .help("Lists the items in the directory") + .with_argument( + JkArgument::new() .required(true) - .index(1) - .value_parser(value_parser!(PathBuf)), + .with_help("The directory to list in"), + ), + ) + .with_subcommand( + "download", + JkCommand::new() + .help("Download a file") + .with_argument( + JkArgument::new() + .required(true) + .with_help("Path in the AFC jail"), ) - .arg(Arg::new("path").required(true).index(2)), + .with_argument( + JkArgument::new() + .required(true) + .with_help("Path to save file to"), + ), ) - .subcommand( - Command::new("mkdir") - .about("Creates a directory") - .arg(Arg::new("path").required(true).index(1)), + .with_subcommand( + "upload", + JkCommand::new() + .help("Upload a file") + .with_argument( + JkArgument::new() + .required(true) + .with_help("Path to the file to upload"), + ) + .with_argument( + JkArgument::new() + .required(true) + .with_help("Path to save file to in the AFC jail"), + ), ) - .subcommand( - Command::new("remove") - .about("Remove a provisioning profile") - .arg(Arg::new("path").required(true).index(1)), + .with_subcommand( + "mkdir", + JkCommand::new().help("Create a folder").with_argument( + JkArgument::new() + .required(true) + .with_help("Path to the folder to create in the AFC jail"), + ), ) - .subcommand( - Command::new("remove_all") - .about("Remove a provisioning profile") - .arg(Arg::new("path").required(true).index(1)), + .with_subcommand( + "remove", + JkCommand::new().help("Remove a file").with_argument( + JkArgument::new() + .required(true) + .with_help("Path to the file to remove"), + ), ) - .subcommand( - Command::new("info") - .about("Get info about a file") - .arg(Arg::new("path").required(true).index(1)), + .with_subcommand( + "remove_all", + JkCommand::new().help("Remove a folder").with_argument( + JkArgument::new() + .required(true) + .with_help("Path to the folder to remove"), + ), ) - .subcommand(Command::new("device_info").about("Get info about the device")) - .get_matches(); + .with_subcommand( + "info", + JkCommand::new() + .help("Get info about a file") + .with_argument( + JkArgument::new() + .required(true) + .with_help("Path to the file to get info for"), + ), + ) + .with_subcommand( + "device_info", + JkCommand::new().help("Get info about the device"), + ) + .subcommand_required(true) +} - if matches.get_flag("about") { - println!("afc"); - println!("Copyright (c) 2025 Jackson Coxson"); - return; - } - - let udid = matches.get_one::("udid"); - let host = matches.get_one::("host"); - let pairing_file = matches.get_one::("pairing_file"); - - let provider = match common::get_provider(udid, host, pairing_file, "afc-jkcoxson").await { - Ok(p) => p, - Err(e) => { - eprintln!("{e}"); - return; - } - }; - - let mut afc_client = if let Some(bundle_id) = matches.get_one::("container") { +pub async fn main(arguments: &CollectedArguments, provider: Box) { + let mut afc_client = if let Some(bundle_id) = arguments.get_flag::("container") { let h = HouseArrestClient::connect(&*provider) .await .expect("Failed to connect to house arrest"); h.vend_container(bundle_id) .await .expect("Failed to vend container") - } else if let Some(bundle_id) = matches.get_one::("documents") { + } else if let Some(bundle_id) = arguments.get_flag::("documents") { let h = HouseArrestClient::connect(&*provider) .await .expect("Failed to connect to house arrest"); @@ -138,59 +127,72 @@ async fn main() { .expect("Unable to connect to misagent") }; - if let Some(matches) = matches.subcommand_matches("list") { - let path = matches.get_one::("path").expect("No path passed"); - let res = afc_client.list_dir(path).await.expect("Failed to read dir"); - println!("{path}\n{res:#?}"); - } else if let Some(matches) = matches.subcommand_matches("mkdir") { - let path = matches.get_one::("path").expect("No path passed"); - afc_client.mk_dir(path).await.expect("Failed to mkdir"); - } else if let Some(matches) = matches.subcommand_matches("download") { - let path = matches.get_one::("path").expect("No path passed"); - let save = matches.get_one::("save").expect("No path passed"); + let (sub_name, sub_args) = arguments.first_subcommand().unwrap(); + let mut sub_args = sub_args.clone(); + match sub_name.as_str() { + "list" => { + let path = sub_args.next_argument::().expect("No path passed"); + let res = afc_client + .list_dir(&path) + .await + .expect("Failed to read dir"); + println!("{path}\n{res:#?}"); + } + "mkdir" => { + let path = sub_args.next_argument::().expect("No path passed"); + afc_client.mk_dir(path).await.expect("Failed to mkdir"); + } + "download" => { + let path = sub_args.next_argument::().expect("No path passed"); + let save = sub_args.next_argument::().expect("No path passed"); - let mut file = afc_client - .open(path, AfcFopenMode::RdOnly) - .await - .expect("Failed to open"); + let mut file = afc_client + .open(path, AfcFopenMode::RdOnly) + .await + .expect("Failed to open"); - let res = file.read_entire().await.expect("Failed to read"); - tokio::fs::write(save, res) - .await - .expect("Failed to write to file"); - } else if let Some(matches) = matches.subcommand_matches("upload") { - let file = matches.get_one::("file").expect("No path passed"); - let path = matches.get_one::("path").expect("No path passed"); + let res = file.read_entire().await.expect("Failed to read"); + tokio::fs::write(save, res) + .await + .expect("Failed to write to file"); + } + "upload" => { + let file = sub_args.next_argument::().expect("No path passed"); + let path = sub_args.next_argument::().expect("No path passed"); - let bytes = tokio::fs::read(file).await.expect("Failed to read file"); - let mut file = afc_client - .open(path, AfcFopenMode::WrOnly) - .await - .expect("Failed to open"); + let bytes = tokio::fs::read(file).await.expect("Failed to read file"); + let mut file = afc_client + .open(path, AfcFopenMode::WrOnly) + .await + .expect("Failed to open"); - file.write_entire(&bytes) - .await - .expect("Failed to upload bytes"); - } else if let Some(matches) = matches.subcommand_matches("remove") { - let path = matches.get_one::("path").expect("No path passed"); - afc_client.remove(path).await.expect("Failed to remove"); - } else if let Some(matches) = matches.subcommand_matches("remove_all") { - let path = matches.get_one::("path").expect("No path passed"); - afc_client.remove_all(path).await.expect("Failed to remove"); - } else if let Some(matches) = matches.subcommand_matches("info") { - let path = matches.get_one::("path").expect("No path passed"); - let res = afc_client - .get_file_info(path) - .await - .expect("Failed to get file info"); - println!("{res:#?}"); - } else if matches.subcommand_matches("device_info").is_some() { - let res = afc_client - .get_device_info() - .await - .expect("Failed to get file info"); - println!("{res:#?}"); - } else { - eprintln!("Invalid usage, pass -h for help"); + file.write_entire(&bytes) + .await + .expect("Failed to upload bytes"); + } + "remove" => { + let path = sub_args.next_argument::().expect("No path passed"); + afc_client.remove(path).await.expect("Failed to remove"); + } + "remove_all" => { + let path = sub_args.next_argument::().expect("No path passed"); + afc_client.remove_all(path).await.expect("Failed to remove"); + } + "info" => { + let path = sub_args.next_argument::().expect("No path passed"); + let res = afc_client + .get_file_info(path) + .await + .expect("Failed to get file info"); + println!("{res:#?}"); + } + "device_info" => { + let res = afc_client + .get_device_info() + .await + .expect("Failed to get file info"); + println!("{res:#?}"); + } + _ => unreachable!(), } } diff --git a/tools/src/amfi.rs b/tools/src/amfi.rs index bff4847..0298da1 100644 --- a/tools/src/amfi.rs +++ b/tools/src/amfi.rs @@ -1,109 +1,77 @@ // Jackson Coxson -use clap::{Arg, Command}; -use idevice::{IdeviceService, amfi::AmfiClient}; +use idevice::{IdeviceService, amfi::AmfiClient, provider::IdeviceProvider}; +use jkcli::{CollectedArguments, JkArgument, JkCommand}; -mod common; - -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - - let matches = Command::new("amfi") - .about("Mess with developer mode") - .arg( - Arg::new("host") - .long("host") - .value_name("HOST") - .help("IP address of the device"), +pub fn register() -> JkCommand { + JkCommand::new() + .help("Mess with devleoper mode") + .with_subcommand( + "show", + JkCommand::new().help("Shows the developer mode option in settings"), ) - .arg( - Arg::new("pairing_file") - .long("pairing-file") - .value_name("PATH") - .help("Path to the pairing file"), + .with_subcommand("enable", JkCommand::new().help("Enables developer mode")) + .with_subcommand( + "accept", + JkCommand::new().help("Shows the accept dialogue for developer mode"), ) - .arg( - Arg::new("udid") - .value_name("UDID") - .help("UDID of the device (overrides host/pairing file)") - .index(1), + .with_subcommand( + "status", + JkCommand::new().help("Gets the developer mode status"), ) - .arg( - Arg::new("about") - .long("about") - .help("Show about information") - .action(clap::ArgAction::SetTrue), - ) - .subcommand(Command::new("show").about("Shows the developer mode option in settings")) - .subcommand(Command::new("enable").about("Enables developer mode")) - .subcommand(Command::new("accept").about("Shows the accept dialogue for developer mode")) - .subcommand(Command::new("status").about("Gets the developer mode status")) - .subcommand( - Command::new("trust") - .about("Trusts an app signer") - .arg(Arg::new("uuid").required(true)), - ) - .get_matches(); - - if matches.get_flag("about") { - println!("amfi - manage developer mode"); - println!("Copyright (c) 2025 Jackson Coxson"); - return; - } - - let udid = matches.get_one::("udid"); - let host = matches.get_one::("host"); - let pairing_file = matches.get_one::("pairing_file"); - - let provider = match common::get_provider(udid, host, pairing_file, "amfi-jkcoxson").await { - Ok(p) => p, - Err(e) => { - eprintln!("{e}"); - return; - } - }; + .with_subcommand("trust", JkCommand::new().help("Trusts an app signer")) + .with_argument(JkArgument::new().with_help("UUID").required(true)) + .subcommand_required(true) +} +pub async fn main(arguments: &CollectedArguments, provider: Box) { let mut amfi_client = AmfiClient::connect(&*provider) .await .expect("Failed to connect to amfi"); - if matches.subcommand_matches("show").is_some() { - amfi_client - .reveal_developer_mode_option_in_ui() - .await - .expect("Failed to show"); - } else if matches.subcommand_matches("enable").is_some() { - amfi_client - .enable_developer_mode() - .await - .expect("Failed to show"); - } else if matches.subcommand_matches("accept").is_some() { - amfi_client - .accept_developer_mode() - .await - .expect("Failed to show"); - } else if matches.subcommand_matches("status").is_some() { - let status = amfi_client - .get_developer_mode_status() - .await - .expect("Failed to get status"); - println!("Enabled: {status}"); - } else if let Some(matches) = matches.subcommand_matches("state") { - let uuid: &String = match matches.get_one("uuid") { - Some(u) => u, - None => { - eprintln!("No UUID passed. Invalid usage, pass -h for help"); - return; - } - }; - let status = amfi_client - .trust_app_signer(uuid) - .await - .expect("Failed to get state"); - println!("Enabled: {status}"); - } else { - eprintln!("Invalid usage, pass -h for help"); + let (sub_name, sub_args) = arguments.first_subcommand().expect("No subcommand passed"); + let mut sub_args = sub_args.clone(); + + match sub_name.as_str() { + "show" => { + amfi_client + .reveal_developer_mode_option_in_ui() + .await + .expect("Failed to show"); + } + "enable" => { + amfi_client + .enable_developer_mode() + .await + .expect("Failed to show"); + } + "accept" => { + amfi_client + .accept_developer_mode() + .await + .expect("Failed to show"); + } + "status" => { + let status = amfi_client + .get_developer_mode_status() + .await + .expect("Failed to get status"); + println!("Enabled: {status}"); + } + "trust" => { + let uuid: String = match sub_args.next_argument() { + Some(u) => u, + None => { + eprintln!("No UUID passed. Invalid usage, pass -h for help"); + return; + } + }; + let status = amfi_client + .trust_app_signer(uuid) + .await + .expect("Failed to get state"); + println!("Enabled: {status}"); + } + _ => unreachable!(), } - return; } diff --git a/tools/src/app_service.rs b/tools/src/app_service.rs index c286e4e..5cc5032 100644 --- a/tools/src/app_service.rs +++ b/tools/src/app_service.rs @@ -1,111 +1,72 @@ // Jackson Coxson -use clap::{Arg, Command}; use idevice::{ IdeviceService, RsdService, core_device::{AppServiceClient, OpenStdioSocketClient}, core_device_proxy::CoreDeviceProxy, + provider::IdeviceProvider, rsd::RsdHandshake, }; +use jkcli::{CollectedArguments, JkArgument, JkCommand}; -mod common; - -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - - let matches = Command::new("remotexpc") - .about("Get services from RemoteXPC") - .arg( - Arg::new("host") - .long("host") - .value_name("HOST") - .help("IP address of the device"), - ) - .arg( - Arg::new("pairing_file") - .long("pairing-file") - .value_name("PATH") - .help("Path to the pairing file"), - ) - .arg( - Arg::new("udid") - .value_name("UDID") - .help("UDID of the device (overrides host/pairing file)") - .index(1), - ) - .arg( - Arg::new("tunneld") - .long("tunneld") - .help("Use tunneld") - .action(clap::ArgAction::SetTrue), - ) - .arg( - Arg::new("about") - .long("about") - .help("Show about information") - .action(clap::ArgAction::SetTrue), - ) - .subcommand(Command::new("list").about("Lists the images mounted on the device")) - .subcommand( - Command::new("launch") - .about("Launch the app on the device") - .arg( - Arg::new("bundle_id") - .required(true) - .help("The bundle ID to launch"), +pub fn register() -> JkCommand { + JkCommand::new() + .help("Interact with the RemoteXPC app service on the device") + .with_subcommand("list", JkCommand::new().help("List apps on the device")) + .with_subcommand( + "launch", + JkCommand::new() + .help("Launch an app on the device") + .with_argument( + JkArgument::new() + .with_help("Bundle ID to launch") + .required(true), ), ) - .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"), + .with_subcommand( + "processes", + JkCommand::new().help("List the processes running"), + ) + .with_subcommand( + "uninstall", + JkCommand::new().help("Uninstall an app").with_argument( + JkArgument::new() + .with_help("Bundle ID to uninstall") + .required(true), ), ) - .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")), + .with_subcommand( + "signal", + JkCommand::new() + .help("Uninstall an app") + .with_argument(JkArgument::new().with_help("PID to signal").required(true)) + .with_argument(JkArgument::new().with_help("Signal to send").required(true)), ) - .subcommand( - Command::new("icon") - .about("Send a signal to an app") - .arg( - Arg::new("bundle_id") - .required(true) - .help("The bundle ID to fetch"), + .with_subcommand( + "icon", + JkCommand::new() + .help("Fetch an icon for an app") + .with_argument( + JkArgument::new() + .with_help("Bundle ID for the app") + .required(true), ) - .arg( - Arg::new("path") - .required(true) - .help("The path to save the icon to"), + .with_argument( + JkArgument::new() + .with_help("Path to save it to") + .required(true), ) - .arg(Arg::new("hw").required(false).help("The height and width")) - .arg(Arg::new("scale").required(false).help("The scale")), + .with_argument( + JkArgument::new() + .with_help("Height and width") + .required(true), + ) + .with_argument(JkArgument::new().with_help("Scale").required(true)), ) - .get_matches(); + .subcommand_required(true) +} - if matches.get_flag("about") { - println!("debug_proxy - connect to the debug proxy and run commands"); - println!("Copyright (c) 2025 Jackson Coxson"); - return; - } - - let udid = matches.get_one::("udid"); - let pairing_file = matches.get_one::("pairing_file"); - let host = matches.get_one::("host"); - - let provider = - match common::get_provider(udid, host, pairing_file, "app_service-jkcoxson").await { - Ok(p) => p, - Err(e) => { - eprintln!("{e}"); - return; - } - }; +pub async fn main(arguments: &CollectedArguments, provider: Box) { let proxy = CoreDeviceProxy::connect(&*provider) .await .expect("no core proxy"); @@ -123,121 +84,122 @@ async fn main() { .await .expect("no connect"); - if matches.subcommand_matches("list").is_some() { - let apps = asc - .list_apps(true, true, true, true, true) - .await - .expect("Failed to get apps"); - println!("{apps:#?}"); - } else if let Some(matches) = matches.subcommand_matches("launch") { - let bundle_id: &String = match matches.get_one("bundle_id") { - Some(b) => b, - None => { - eprintln!("No bundle ID passed"); - return; - } - }; + let (sub_name, sub_args) = arguments.first_subcommand().expect("No subcommand"); + let mut sub_args = sub_args.clone(); - let mut stdio_conn = OpenStdioSocketClient::connect_rsd(&mut adapter, &mut handshake) - .await - .expect("no stdio"); - - let stdio_uuid = stdio_conn.read_uuid().await.expect("no uuid"); - println!("stdio uuid: {stdio_uuid:?}"); - - let res = asc - .launch_application(bundle_id, &[], true, false, None, None, Some(stdio_uuid)) - .await - .expect("no launch"); - - println!("Launch response {res:#?}"); - - let (mut remote_reader, mut remote_writer) = tokio::io::split(stdio_conn.inner); - let mut local_stdin = tokio::io::stdin(); - let mut local_stdout = tokio::io::stdout(); - - tokio::select! { - // Task 1: Copy data from the remote process to local stdout - res = tokio::io::copy(&mut remote_reader, &mut local_stdout) => { - if let Err(e) = res { - eprintln!("Error copying from remote to local: {}", e); + match sub_name.as_str() { + "list" => { + let apps = asc + .list_apps(true, true, true, true, true) + .await + .expect("Failed to get apps"); + println!("{apps:#?}"); + } + "launch" => { + let bundle_id: String = match sub_args.next_argument() { + Some(b) => b, + None => { + eprintln!("No bundle ID passed"); + return; } - println!("\nRemote connection closed."); - return; - } - // Task 2: Copy data from local stdin to the remote process - res = tokio::io::copy(&mut local_stdin, &mut remote_writer) => { - if let Err(e) = res { - eprintln!("Error copying from local to remote: {}", e); + }; + + let mut stdio_conn = OpenStdioSocketClient::connect_rsd(&mut adapter, &mut handshake) + .await + .expect("no stdio"); + + let stdio_uuid = stdio_conn.read_uuid().await.expect("no uuid"); + println!("stdio uuid: {stdio_uuid:?}"); + + let res = asc + .launch_application(bundle_id, &[], true, false, None, None, Some(stdio_uuid)) + .await + .expect("no launch"); + + println!("Launch response {res:#?}"); + + let (mut remote_reader, mut remote_writer) = tokio::io::split(stdio_conn.inner); + let mut local_stdin = tokio::io::stdin(); + let mut local_stdout = tokio::io::stdout(); + + tokio::select! { + // Task 1: Copy data from the remote process to local stdout + res = tokio::io::copy(&mut remote_reader, &mut local_stdout) => { + if let Err(e) = res { + eprintln!("Error copying from remote to local: {}", e); + } + println!("\nRemote connection closed."); + } + // Task 2: Copy data from local stdin to the remote process + res = tokio::io::copy(&mut local_stdin, &mut remote_writer) => { + if let Err(e) = res { + eprintln!("Error copying from local to remote: {}", e); + } + println!("\nLocal stdin closed."); } - println!("\nLocal stdin closed."); - return; } } - } 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; - } - }; + "processes" => { + let p = asc.list_processes().await.expect("no processes?"); + println!("{p:#?}"); + } + "uninstall" => { + let bundle_id: String = match sub_args.next_argument() { + 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; - } - }; + asc.uninstall_app(bundle_id).await.expect("no launch") + } + "signal" => { + let pid: u32 = match sub_args.next_argument() { + Some(b) => b, + None => { + eprintln!("No bundle PID passed"); + return; + } + }; + let signal: u32 = match sub_args.next_argument() { + Some(b) => b, + 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.send_signal(pid, signal).await.expect("no signal"); + println!("{res:#?}"); + } + "icon" => { + let bundle_id: String = match sub_args.next_argument() { + Some(b) => b, + None => { + eprintln!("No bundle ID passed"); + return; + } + }; + let save_path: String = match sub_args.next_argument() { + Some(b) => b, + None => { + eprintln!("No bundle ID passed"); + return; + } + }; + let hw: f32 = sub_args.next_argument().unwrap_or(1.0); + let scale: f32 = sub_args.next_argument().unwrap_or(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"); + 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"); + } + _ => unreachable!(), } } diff --git a/tools/src/bt_packet_logger.rs b/tools/src/bt_packet_logger.rs index 7529200..346f115 100644 --- a/tools/src/bt_packet_logger.rs +++ b/tools/src/bt_packet_logger.rs @@ -1,71 +1,20 @@ // Jackson Coxson -use clap::{Arg, Command}; use futures_util::StreamExt; -use idevice::{IdeviceService, bt_packet_logger::BtPacketLoggerClient}; +use idevice::{IdeviceService, bt_packet_logger::BtPacketLoggerClient, provider::IdeviceProvider}; +use jkcli::{CollectedArguments, JkArgument, JkCommand}; use tokio::io::AsyncWrite; use crate::pcap::{write_pcap_header, write_pcap_record}; -mod common; -mod pcap; +pub fn register() -> JkCommand { + JkCommand::new() + .help("Writes Bluetooth pcap data") + .with_argument(JkArgument::new().with_help("Write PCAP to this file (use '-' for stdout)")) +} -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - - let matches = Command::new("amfi") - .about("Capture Bluetooth packets") - .arg( - Arg::new("host") - .long("host") - .value_name("HOST") - .help("IP address of the device"), - ) - .arg( - Arg::new("pairing_file") - .long("pairing-file") - .value_name("PATH") - .help("Path to the pairing file"), - ) - .arg( - Arg::new("udid") - .value_name("UDID") - .help("UDID of the device (overrides host/pairing file)") - .index(1), - ) - .arg( - Arg::new("about") - .long("about") - .help("Show about information") - .action(clap::ArgAction::SetTrue), - ) - .arg( - Arg::new("out") - .long("out") - .value_name("PCAP") - .help("Write PCAP to this file (use '-' for stdout)"), - ) - .get_matches(); - - if matches.get_flag("about") { - println!("bt_packet_logger - capture bluetooth packets"); - println!("Copyright (c) 2025 Jackson Coxson"); - return; - } - - let udid = matches.get_one::("udid"); - let host = matches.get_one::("host"); - let pairing_file = matches.get_one::("pairing_file"); - let out = matches.get_one::("out").map(String::to_owned); - - let provider = match common::get_provider(udid, host, pairing_file, "amfi-jkcoxson").await { - Ok(p) => p, - Err(e) => { - eprintln!("{e}"); - return; - } - }; +pub async fn main(arguments: &CollectedArguments, provider: Box) { + let out: Option = arguments.clone().next_argument(); let logger_client = BtPacketLoggerClient::connect(&*provider) .await diff --git a/tools/src/companion_proxy.rs b/tools/src/companion_proxy.rs index 8e02c99..eec9699 100644 --- a/tools/src/companion_proxy.rs +++ b/tools/src/companion_proxy.rs @@ -1,83 +1,60 @@ // Jackson Coxson -use clap::{Arg, Command, arg}; use idevice::{ IdeviceService, RsdService, companion_proxy::CompanionProxy, - core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake, + core_device_proxy::CoreDeviceProxy, provider::IdeviceProvider, rsd::RsdHandshake, }; +use jkcli::{CollectedArguments, JkArgument, JkCommand}; use plist_macro::{pretty_print_dictionary, pretty_print_plist}; -mod common; - -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - - let matches = Command::new("companion_proxy") - .about("Apple Watch things") - .arg( - Arg::new("host") - .long("host") - .value_name("HOST") - .help("IP address of the device"), - ) - .arg( - Arg::new("pairing_file") - .long("pairing-file") - .value_name("PATH") - .help("Path to the pairing file"), - ) - .arg( - Arg::new("udid") - .value_name("UDID") - .help("UDID of the device (overrides host/pairing file)") - .index(1), - ) - .arg( - Arg::new("about") - .long("about") - .help("Show about information") - .action(clap::ArgAction::SetTrue), - ) - .subcommand(Command::new("list").about("List the companions on the device")) - .subcommand(Command::new("listen").about("Listen for devices")) - .subcommand( - Command::new("get") - .about("Gets a value") - .arg(arg!(-d --device_udid "the device udid to get from").required(true)) - .arg(arg!(-v --value "the value to get").required(true)), - ) - .subcommand( - Command::new("start") - .about("Starts a service") - .arg(arg!(-p --port "the port").required(true)) - .arg(arg!(-n --name "the optional service name").required(false)), - ) - .subcommand( - Command::new("stop") - .about("Starts a service") - .arg(arg!(-p --port "the port").required(true)), - ) - .get_matches(); - - if matches.get_flag("about") { - println!("companion_proxy"); - println!("Copyright (c) 2025 Jackson Coxson"); - return; - } - - let udid = matches.get_one::("udid"); - let host = matches.get_one::("host"); - let pairing_file = matches.get_one::("pairing_file"); - - let provider = match common::get_provider(udid, host, pairing_file, "amfi-jkcoxson").await { - Ok(p) => p, - Err(e) => { - eprintln!("{e}"); - return; - } - }; +pub fn register() -> JkCommand { + JkCommand::new() + .help("Apple Watch proxy") + .with_subcommand( + "list", + JkCommand::new().help("List the companions on the device"), + ) + .with_subcommand("listen", JkCommand::new().help("Listen for devices")) + .with_subcommand( + "get", + JkCommand::new() + .help("Gets a value from an AW") + .with_argument( + JkArgument::new() + .with_help("The AW UDID to get from") + .required(true), + ) + .with_argument( + JkArgument::new() + .with_help("The value to get") + .required(true), + ), + ) + .with_subcommand( + "start", + JkCommand::new() + .help("Starts a service on the Apple Watch") + .with_argument( + JkArgument::new() + .with_help("The port to listen on") + .required(true), + ) + .with_argument(JkArgument::new().with_help("The service name")), + ) + .with_subcommand( + "stop", + JkCommand::new() + .help("Stops a service on the Apple Watch") + .with_argument( + JkArgument::new() + .with_help("The port to stop") + .required(true), + ), + ) + .subcommand_required(true) +} +pub async fn main(arguments: &CollectedArguments, provider: Box) { let proxy = CoreDeviceProxy::connect(&*provider) .await .expect("no core_device_proxy"); @@ -97,55 +74,72 @@ async fn main() { // .await // .expect("Failed to connect to companion proxy"); - if matches.subcommand_matches("list").is_some() { - proxy.get_device_registry().await.expect("Failed to show"); - } else if matches.subcommand_matches("listen").is_some() { - let mut stream = proxy.listen_for_devices().await.expect("Failed to show"); - while let Ok(v) = stream.next().await { - println!("{}", pretty_print_dictionary(&v)); - } - } else if let Some(matches) = matches.subcommand_matches("get") { - let key = matches.get_one::("value").expect("no value passed"); - let udid = matches - .get_one::("device_udid") - .expect("no AW udid passed"); + let (sub_name, sub_args) = arguments.first_subcommand().unwrap(); + let mut sub_args = sub_args.clone(); - match proxy.get_value(udid, key).await { - Ok(value) => { - println!("{}", pretty_print_plist(&value)); - } - Err(e) => { - eprintln!("Error getting value: {e}"); + match sub_name.as_str() { + "list" => { + proxy.get_device_registry().await.expect("Failed to show"); + } + "listen" => { + let mut stream = proxy.listen_for_devices().await.expect("Failed to show"); + while let Ok(v) = stream.next().await { + println!("{}", pretty_print_dictionary(&v)); } } - } else if let Some(matches) = matches.subcommand_matches("start") { - let port: u16 = matches - .get_one::("port") - .expect("no port passed") - .parse() - .expect("not a number"); - let name = matches.get_one::("name").map(|x| x.as_str()); + "get" => { + let key: String = sub_args.next_argument::().expect("no value passed"); + let udid = sub_args + .next_argument::() + .expect("no AW udid passed"); - match proxy.start_forwarding_service_port(port, name, None).await { - Ok(value) => { - println!("started on port {value}"); + match proxy.get_value(udid, key).await { + Ok(value) => { + println!("{}", pretty_print_plist(&value)); + } + Err(e) => { + eprintln!("Error getting value: {e}"); + } } - Err(e) => { + } + "start" => { + let port: u16 = sub_args + .next_argument::() + .expect("no port passed") + .parse() + .expect("not a number"); + let name = sub_args.next_argument::(); + + match proxy + .start_forwarding_service_port( + port, + match &name { + Some(n) => Some(n.as_str()), + None => None, + }, + None, + ) + .await + { + Ok(value) => { + println!("started on port {value}"); + } + Err(e) => { + eprintln!("Error starting: {e}"); + } + } + } + "stop" => { + let port: u16 = sub_args + .next_argument::() + .expect("no port passed") + .parse() + .expect("not a number"); + + if let Err(e) = proxy.stop_forwarding_service_port(port).await { eprintln!("Error starting: {e}"); } } - } else if let Some(matches) = matches.subcommand_matches("stop") { - let port: u16 = matches - .get_one::("port") - .expect("no port passed") - .parse() - .expect("not a number"); - - if let Err(e) = proxy.stop_forwarding_service_port(port).await { - eprintln!("Error starting: {e}"); - } - } else { - eprintln!("Invalid usage, pass -h for help"); + _ => unreachable!(), } - return; } diff --git a/tools/src/crash_logs.rs b/tools/src/crash_logs.rs index b17ef13..7265647 100644 --- a/tools/src/crash_logs.rs +++ b/tools/src/crash_logs.rs @@ -1,96 +1,79 @@ // Jackson Coxson -use clap::{Arg, Command}; use idevice::{ IdeviceService, crashreportcopymobile::{CrashReportCopyMobileClient, flush_reports}, + provider::IdeviceProvider, }; +use jkcli::{CollectedArguments, JkArgument, JkCommand}; -mod common; +pub fn register() -> JkCommand { + JkCommand::new() + .help("Manage crash logs") + .with_subcommand( + "list", + JkCommand::new() + .help("List crash logs in the directory") + .with_argument( + JkArgument::new() + .with_help("Path to list in") + .required(true), + ), + ) + .with_subcommand( + "flush", + JkCommand::new().help("Flushes reports to the directory"), + ) + .with_subcommand( + "pull", + JkCommand::new() + .help("Check the capabilities") + .with_argument( + JkArgument::new() + .with_help("Path to the log to pull") + .required(true), + ) + .with_argument( + JkArgument::new() + .with_help("Path to save the log to") + .required(true), + ), + ) + .subcommand_required(true) +} -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - - let matches = Command::new("crash_logs") - .about("Manage crash logs") - .arg( - Arg::new("host") - .long("host") - .value_name("HOST") - .help("IP address of the device"), - ) - .arg( - Arg::new("pairing_file") - .long("pairing-file") - .value_name("PATH") - .help("Path to the pairing file"), - ) - .arg( - Arg::new("udid") - .value_name("UDID") - .help("UDID of the device (overrides host/pairing file)"), - ) - .arg( - Arg::new("about") - .long("about") - .help("Show about information") - .action(clap::ArgAction::SetTrue), - ) - .subcommand( - Command::new("list") - .about("Lists the items in the directory") - .arg(Arg::new("dir").required(false).index(1)), - ) - .subcommand(Command::new("flush").about("Flushes reports to the directory")) - .subcommand( - Command::new("pull") - .about("Pulls a log") - .arg(Arg::new("path").required(true).index(1)) - .arg(Arg::new("save").required(true).index(2)) - .arg(Arg::new("dir").required(false).index(3)), - ) - .get_matches(); - - if matches.get_flag("about") { - println!("crash_logs - manage crash logs on the device"); - println!("Copyright (c) 2025 Jackson Coxson"); - return; - } - - let udid = matches.get_one::("udid"); - let host = matches.get_one::("host"); - let pairing_file = matches.get_one::("pairing_file"); - - let provider = match common::get_provider(udid, host, pairing_file, "afc-jkcoxson").await { - Ok(p) => p, - Err(e) => { - eprintln!("{e}"); - return; - } - }; +pub async fn main(arguments: &CollectedArguments, provider: Box) { let mut crash_client = CrashReportCopyMobileClient::connect(&*provider) .await .expect("Unable to connect to misagent"); - if let Some(matches) = matches.subcommand_matches("list") { - let dir_path: Option<&String> = matches.get_one("dir"); - let res = crash_client - .ls(dir_path.map(|x| x.as_str())) - .await - .expect("Failed to read dir"); - println!("{res:#?}"); - } else if matches.subcommand_matches("flush").is_some() { - flush_reports(&*provider).await.expect("Failed to flush"); - } else if let Some(matches) = matches.subcommand_matches("pull") { - let path = matches.get_one::("path").expect("No path passed"); - let save = matches.get_one::("save").expect("No path passed"); + let (sub_name, sub_args) = arguments.first_subcommand().expect("No sub command passed"); + let mut sub_args = sub_args.clone(); - let res = crash_client.pull(path).await.expect("Failed to pull log"); - tokio::fs::write(save, res) - .await - .expect("Failed to write to file"); - } else { - eprintln!("Invalid usage, pass -h for help"); + match sub_name.as_str() { + "list" => { + let dir_path: Option = sub_args.next_argument(); + let res = crash_client + .ls(match &dir_path { + Some(d) => Some(d.as_str()), + None => None, + }) + .await + .expect("Failed to read dir"); + println!("{res:#?}"); + } + "flush" => { + flush_reports(&*provider).await.expect("Failed to flush"); + } + "pull" => { + let path = sub_args.next_argument::().expect("No path passed"); + let save = sub_args.next_argument::().expect("No path passed"); + + let res = crash_client.pull(path).await.expect("Failed to pull log"); + tokio::fs::write(save, res) + .await + .expect("Failed to write to file"); + } + _ => unreachable!(), } } diff --git a/tools/src/debug_proxy.rs b/tools/src/debug_proxy.rs index f965bd8..4d09d4b 100644 --- a/tools/src/debug_proxy.rs +++ b/tools/src/debug_proxy.rs @@ -2,70 +2,17 @@ use std::io::Write; -use clap::{Arg, Command}; use idevice::{ IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, debug_proxy::DebugProxyClient, - rsd::RsdHandshake, + provider::IdeviceProvider, rsd::RsdHandshake, }; +use jkcli::{CollectedArguments, JkCommand}; -mod common; +pub fn register() -> JkCommand { + JkCommand::new().help("Start a debug proxy shell") +} -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - - let matches = Command::new("remotexpc") - .about("Get services from RemoteXPC") - .arg( - Arg::new("host") - .long("host") - .value_name("HOST") - .help("IP address of the device"), - ) - .arg( - Arg::new("pairing_file") - .long("pairing-file") - .value_name("PATH") - .help("Path to the pairing file"), - ) - .arg( - Arg::new("udid") - .value_name("UDID") - .help("UDID of the device (overrides host/pairing file)") - .index(1), - ) - .arg( - Arg::new("tunneld") - .long("tunneld") - .help("Use tunneld") - .action(clap::ArgAction::SetTrue), - ) - .arg( - Arg::new("about") - .long("about") - .help("Show about information") - .action(clap::ArgAction::SetTrue), - ) - .get_matches(); - - if matches.get_flag("about") { - println!("debug_proxy - connect to the debug proxy and run commands"); - println!("Copyright (c) 2025 Jackson Coxson"); - return; - } - - let udid = matches.get_one::("udid"); - let pairing_file = matches.get_one::("pairing_file"); - let host = matches.get_one::("host"); - - let provider = - match common::get_provider(udid, host, pairing_file, "debug-proxy-jkcoxson").await { - Ok(p) => p, - Err(e) => { - eprintln!("{e}"); - return; - } - }; +pub async fn main(_arguments: &CollectedArguments, provider: Box) { let proxy = CoreDeviceProxy::connect(&*provider) .await .expect("no core proxy"); diff --git a/tools/src/diagnostics.rs b/tools/src/diagnostics.rs index 674f757..bb7d1c5 100644 --- a/tools/src/diagnostics.rs +++ b/tools/src/diagnostics.rs @@ -1,106 +1,71 @@ // Jackson Coxson // idevice Rust implementation of libimobiledevice's idevicediagnostics -use clap::{Arg, ArgMatches, Command}; -use idevice::{IdeviceService, services::diagnostics_relay::DiagnosticsRelayClient}; +use idevice::{ + IdeviceService, provider::IdeviceProvider, services::diagnostics_relay::DiagnosticsRelayClient, +}; +use jkcli::{CollectedArguments, JkArgument, JkCommand, JkFlag}; -mod common; - -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - - let matches = Command::new("idevicediagnostics") - .about("Interact with the diagnostics interface of a device") - .arg( - Arg::new("host") - .long("host") - .value_name("HOST") - .help("IP address of the device"), - ) - .arg( - Arg::new("pairing_file") - .long("pairing-file") - .value_name("PATH") - .help("Path to the pairing file"), - ) - .arg( - Arg::new("udid") - .value_name("UDID") - .help("UDID of the device (overrides host/pairing file)") - .index(1), - ) - .arg( - Arg::new("about") - .long("about") - .help("Show about information") - .action(clap::ArgAction::SetTrue), - ) - .subcommand( - Command::new("ioregistry") - .about("Print IORegistry information") - .arg( - Arg::new("plane") - .long("plane") - .value_name("PLANE") - .help("IORegistry plane to query (e.g., IODeviceTree, IOService)"), +pub fn register() -> JkCommand { + JkCommand::new() + .help("Interact with the diagnostics interface of a device") + .with_subcommand( + "ioregistry", + JkCommand::new() + .help("Print IORegistry information") + .with_flag( + JkFlag::new("plane") + .with_help("IORegistry plane to query (e.g., IODeviceTree, IOService)") + .with_argument(JkArgument::new().required(true)), ) - .arg( - Arg::new("name") - .long("name") - .value_name("NAME") - .help("Entry name to filter by"), + .with_flag( + JkFlag::new("name") + .with_help("Entry name to filter by") + .with_argument(JkArgument::new().required(true)), ) - .arg( - Arg::new("class") - .long("class") - .value_name("CLASS") - .help("Entry class to filter by"), + .with_flag( + JkFlag::new("class") + .with_help("Entry class to filter by") + .with_argument(JkArgument::new().required(true)), ), ) - .subcommand( - Command::new("mobilegestalt") - .about("Print MobileGestalt information") - .arg( - Arg::new("keys") - .long("keys") - .value_name("KEYS") - .help("Comma-separated list of keys to query") - .value_delimiter(',') - .num_args(1..), + .with_subcommand( + "mobilegestalt", + JkCommand::new() + .help("Print MobileGestalt information") + .with_argument( + JkArgument::new() + .with_help("Comma-separated list of keys to query") + .required(true), ), ) - .subcommand(Command::new("gasguage").about("Print gas gauge (battery) information")) - .subcommand(Command::new("nand").about("Print NAND flash information")) - .subcommand(Command::new("all").about("Print all available diagnostics information")) - .subcommand(Command::new("wifi").about("Print WiFi diagnostics information")) - .subcommand(Command::new("goodbye").about("Send Goodbye to diagnostics relay")) - .subcommand(Command::new("restart").about("Restart the device")) - .subcommand(Command::new("shutdown").about("Shutdown the device")) - .subcommand(Command::new("sleep").about("Put the device to sleep")) - .get_matches(); - - if matches.get_flag("about") { - println!( - "idevicediagnostics - interact with the diagnostics interface of a device. Reimplementation of libimobiledevice's binary." - ); - println!("Copyright (c) 2025 Jackson Coxson"); - return; - } - - let udid = matches.get_one::("udid"); - let host = matches.get_one::("host"); - let pairing_file = matches.get_one::("pairing_file"); - - let provider = - match common::get_provider(udid, host, pairing_file, "idevicediagnostics-jkcoxson").await { - Ok(p) => p, - Err(e) => { - eprintln!("{e}"); - return; - } - }; + .with_subcommand( + "gasguage", + JkCommand::new().help("Print gas gauge (battery) information"), + ) + .with_subcommand( + "nand", + JkCommand::new().help("Print NAND flash information"), + ) + .with_subcommand( + "all", + JkCommand::new().help("Print all available diagnostics information"), + ) + .with_subcommand( + "wifi", + JkCommand::new().help("Print WiFi diagnostics information"), + ) + .with_subcommand( + "goodbye", + JkCommand::new().help("Send Goodbye to diagnostics relay"), + ) + .with_subcommand("restart", JkCommand::new().help("Restart the device")) + .with_subcommand("shutdown", JkCommand::new().help("Shutdown the device")) + .with_subcommand("sleep", JkCommand::new().help("Put the device to sleep")) + .subcommand_required(true) +} +pub async fn main(arguments: &CollectedArguments, provider: Box) { let mut diagnostics_client = match DiagnosticsRelayClient::connect(&*provider).await { Ok(client) => client, Err(e) => { @@ -109,47 +74,52 @@ async fn main() { } }; - match matches.subcommand() { - Some(("ioregistry", sub_matches)) => { - handle_ioregistry(&mut diagnostics_client, sub_matches).await; + let (sub_name, sub_args) = arguments.first_subcommand().unwrap(); + let mut sub_matches = sub_args.clone(); + + match sub_name.as_str() { + "ioregistry" => { + handle_ioregistry(&mut diagnostics_client, &sub_matches).await; } - Some(("mobilegestalt", sub_matches)) => { - handle_mobilegestalt(&mut diagnostics_client, sub_matches).await; + "mobilegestalt" => { + handle_mobilegestalt(&mut diagnostics_client, &mut sub_matches).await; } - Some(("gasguage", _)) => { + "gasguage" => { handle_gasguage(&mut diagnostics_client).await; } - Some(("nand", _)) => { + "nand" => { handle_nand(&mut diagnostics_client).await; } - Some(("all", _)) => { + "all" => { handle_all(&mut diagnostics_client).await; } - Some(("wifi", _)) => { + "wifi" => { handle_wifi(&mut diagnostics_client).await; } - Some(("restart", _)) => { + "restart" => { handle_restart(&mut diagnostics_client).await; } - Some(("shutdown", _)) => { + "shutdown" => { handle_shutdown(&mut diagnostics_client).await; } - Some(("sleep", _)) => { + "sleep" => { handle_sleep(&mut diagnostics_client).await; } - Some(("goodbye", _)) => { + "goodbye" => { handle_goodbye(&mut diagnostics_client).await; } - _ => { - eprintln!("No subcommand specified. Use --help for usage information."); - } + _ => unreachable!(), } } -async fn handle_ioregistry(client: &mut DiagnosticsRelayClient, matches: &ArgMatches) { - let plane = matches.get_one::("plane").map(|s| s.as_str()); - let name = matches.get_one::("name").map(|s| s.as_str()); - let class = matches.get_one::("class").map(|s| s.as_str()); +async fn handle_ioregistry(client: &mut DiagnosticsRelayClient, matches: &CollectedArguments) { + let plane = matches.get_flag::("plane"); + let name = matches.get_flag::("name"); + let class = matches.get_flag::("class"); + + let plane = plane.as_deref(); + let name = name.as_deref(); + let class = class.as_deref(); match client.ioregistry(plane, name, class).await { Ok(Some(data)) => { @@ -164,12 +134,14 @@ async fn handle_ioregistry(client: &mut DiagnosticsRelayClient, matches: &ArgMat } } -async fn handle_mobilegestalt(client: &mut DiagnosticsRelayClient, matches: &ArgMatches) { - let keys = matches - .get_many::("keys") - .map(|values| values.map(|s| s.to_string()).collect::>()); +async fn handle_mobilegestalt( + client: &mut DiagnosticsRelayClient, + matches: &mut CollectedArguments, +) { + let keys = matches.next_argument::().unwrap(); + let keys = keys.split(',').map(|x| x.to_string()).collect(); - match client.mobilegestalt(keys).await { + match client.mobilegestalt(Some(keys)).await { Ok(Some(data)) => { println!("{data:#?}"); } diff --git a/tools/src/diagnosticsservice.rs b/tools/src/diagnosticsservice.rs index 74e6ca4..337e232 100644 --- a/tools/src/diagnosticsservice.rs +++ b/tools/src/diagnosticsservice.rs @@ -1,71 +1,18 @@ // Jackson Coxson -use clap::{Arg, Command}; use futures_util::StreamExt; use idevice::{ IdeviceService, RsdService, core_device::DiagnostisServiceClient, - core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake, + core_device_proxy::CoreDeviceProxy, provider::IdeviceProvider, rsd::RsdHandshake, }; +use jkcli::{CollectedArguments, JkCommand}; use tokio::io::AsyncWriteExt; -mod common; +pub fn register() -> JkCommand { + JkCommand::new().help("Retrieve a sysdiagnose") +} -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - - let matches = Command::new("remotexpc") - .about("Gets a sysdiagnose") - .arg( - Arg::new("host") - .long("host") - .value_name("HOST") - .help("IP address of the device"), - ) - .arg( - Arg::new("pairing_file") - .long("pairing-file") - .value_name("PATH") - .help("Path to the pairing file"), - ) - .arg( - Arg::new("udid") - .value_name("UDID") - .help("UDID of the device (overrides host/pairing file)") - .index(1), - ) - .arg( - Arg::new("tunneld") - .long("tunneld") - .help("Use tunneld") - .action(clap::ArgAction::SetTrue), - ) - .arg( - Arg::new("about") - .long("about") - .help("Show about information") - .action(clap::ArgAction::SetTrue), - ) - .get_matches(); - - if matches.get_flag("about") { - println!("debug_proxy - connect to the debug proxy and run commands"); - println!("Copyright (c) 2025 Jackson Coxson"); - return; - } - - let udid = matches.get_one::("udid"); - let pairing_file = matches.get_one::("pairing_file"); - let host = matches.get_one::("host"); - - let provider = - match common::get_provider(udid, host, pairing_file, "diagnosticsservice-jkcoxson").await { - Ok(p) => p, - Err(e) => { - eprintln!("{e}"); - return; - } - }; +pub async fn main(_arguments: &CollectedArguments, provider: Box) { let proxy = CoreDeviceProxy::connect(&*provider) .await .expect("no core proxy"); diff --git a/tools/src/dvt_packet_parser.rs b/tools/src/dvt_packet_parser.rs index 7249520..17d1e35 100644 --- a/tools/src/dvt_packet_parser.rs +++ b/tools/src/dvt_packet_parser.rs @@ -1,10 +1,22 @@ // Jackson Coxson -use idevice::dvt::message::Message; +use idevice::{dvt::message::Message, provider::IdeviceProvider}; +use jkcli::{CollectedArguments, JkArgument, JkCommand}; -#[tokio::main] -async fn main() { - let file = std::env::args().nth(1).expect("No file passed"); +pub fn register() -> JkCommand { + JkCommand::new() + .help("Parse a DVT packet from a file") + .with_argument( + JkArgument::new() + .required(true) + .with_help("Path the the packet file"), + ) +} + +pub async fn main(arguments: &CollectedArguments, _provider: Box) { + let mut arguments = arguments.clone(); + + let file: String = arguments.next_argument().expect("No file passed"); let mut bytes = tokio::fs::File::open(file).await.unwrap(); let message = Message::from_reader(&mut bytes).await.unwrap(); diff --git a/tools/src/heartbeat_client.rs b/tools/src/heartbeat_client.rs index b40ba08..b1e709d 100644 --- a/tools/src/heartbeat_client.rs +++ b/tools/src/heartbeat_client.rs @@ -1,60 +1,14 @@ // Jackson Coxson // Heartbeat client -use clap::{Arg, Command}; -use idevice::{IdeviceService, heartbeat::HeartbeatClient}; +use idevice::{IdeviceService, heartbeat::HeartbeatClient, provider::IdeviceProvider}; +use jkcli::{CollectedArguments, JkCommand}; -mod common; +pub fn register() -> JkCommand { + JkCommand::new().help("heartbeat a device") +} -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - let matches = Command::new("core_device_proxy_tun") - .about("Start a tunnel") - .arg( - Arg::new("host") - .long("host") - .value_name("HOST") - .help("IP address of the device"), - ) - .arg( - Arg::new("pairing_file") - .long("pairing-file") - .value_name("PATH") - .help("Path to the pairing file"), - ) - .arg( - Arg::new("udid") - .value_name("UDID") - .help("UDID of the device (overrides host/pairing file)") - .index(1), - ) - .arg( - Arg::new("about") - .long("about") - .help("Show about information") - .action(clap::ArgAction::SetTrue), - ) - .get_matches(); - - if matches.get_flag("about") { - println!("heartbeat_client - heartbeat a device"); - println!("Copyright (c) 2025 Jackson Coxson"); - return; - } - - let udid = matches.get_one::("udid"); - let host = matches.get_one::("host"); - let pairing_file = matches.get_one::("pairing_file"); - - let provider = - match common::get_provider(udid, host, pairing_file, "heartbeat_client-jkcoxson").await { - Ok(p) => p, - Err(e) => { - eprintln!("{e}"); - return; - } - }; +pub async fn main(_arguments: &CollectedArguments, provider: Box) { let mut heartbeat_client = HeartbeatClient::connect(&*provider) .await .expect("Unable to connect to heartbeat"); diff --git a/tools/src/ideviceinfo.rs b/tools/src/ideviceinfo.rs index d9f0d85..3ce7086 100644 --- a/tools/src/ideviceinfo.rs +++ b/tools/src/ideviceinfo.rs @@ -1,64 +1,14 @@ // Jackson Coxson // idevice Rust implementation of libimobiledevice's ideviceinfo -use clap::{Arg, Command}; -use idevice::{IdeviceService, lockdown::LockdownClient}; +use idevice::{IdeviceService, lockdown::LockdownClient, provider::IdeviceProvider}; +use jkcli::{CollectedArguments, JkCommand}; -mod common; - -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - - let matches = Command::new("core_device_proxy_tun") - .about("Start a tunnel") - .arg( - Arg::new("host") - .long("host") - .value_name("HOST") - .help("IP address of the device"), - ) - .arg( - Arg::new("pairing_file") - .long("pairing-file") - .value_name("PATH") - .help("Path to the pairing file"), - ) - .arg( - Arg::new("udid") - .value_name("UDID") - .help("UDID of the device (overrides host/pairing file)") - .index(1), - ) - .arg( - Arg::new("about") - .long("about") - .help("Show about information") - .action(clap::ArgAction::SetTrue), - ) - .get_matches(); - - if matches.get_flag("about") { - println!( - "ideviceinfo - get information from the idevice. Reimplementation of libimobiledevice's binary." - ); - println!("Copyright (c) 2025 Jackson Coxson"); - return; - } - - let udid = matches.get_one::("udid"); - let host = matches.get_one::("host"); - let pairing_file = matches.get_one::("pairing_file"); - - let provider = - match common::get_provider(udid, host, pairing_file, "ideviceinfo-jkcoxson").await { - Ok(p) => p, - Err(e) => { - eprintln!("{e}"); - return; - } - }; +pub fn register() -> JkCommand { + JkCommand::new().help("ideviceinfo - get information from the idevice. Reimplementation of libimobiledevice's binary.") +} +pub async fn main(_arguments: &CollectedArguments, provider: Box) { let mut lockdown_client = match LockdownClient::connect(&*provider).await { Ok(l) => l, Err(e) => { diff --git a/tools/src/ideviceinstaller.rs b/tools/src/ideviceinstaller.rs index a1c94cf..d7a8290 100644 --- a/tools/src/ideviceinstaller.rs +++ b/tools/src/ideviceinstaller.rs @@ -1,103 +1,73 @@ // A minimal ideviceinstaller-like CLI to install/upgrade apps -use clap::{Arg, ArgAction, Command}; -use idevice::utils::installation; +use idevice::{provider::IdeviceProvider, utils::installation}; +use jkcli::{CollectedArguments, JkArgument, JkCommand}; -mod common; +pub fn register() -> JkCommand { + JkCommand::new() + .help("Manage files in the AFC jail of a device") + .with_subcommand( + "install", + JkCommand::new() + .help("Install a local .ipa or directory") + .with_argument( + JkArgument::new() + .required(true) + .with_help("Path to the .ipa or directory containing the app"), + ), + ) + .with_subcommand( + "upgrade", + JkCommand::new() + .help("Install a local .ipa or directory") + .with_argument( + JkArgument::new() + .required(true) + .with_help("Path to the .ipa or directory containing the app"), + ), + ) + .subcommand_required(true) +} -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); +pub async fn main(arguments: &CollectedArguments, provider: Box) { + let (sub_name, sub_args) = arguments.first_subcommand().expect("no sub arg"); + let mut sub_args = sub_args.clone(); - let matches = Command::new("ideviceinstaller") - .about("Install/upgrade apps on an iOS device (AFC + InstallationProxy)") - .arg( - Arg::new("host") - .long("host") - .value_name("HOST") - .help("IP address of the device"), - ) - .arg( - Arg::new("pairing_file") - .long("pairing-file") - .value_name("PATH") - .help("Path to the pairing file"), - ) - .arg( - Arg::new("udid") - .value_name("UDID") - .help("UDID of the device (overrides host/pairing file)") - .index(1), - ) - .arg( - Arg::new("about") - .long("about") - .help("Show about information") - .action(ArgAction::SetTrue), - ) - .subcommand( - Command::new("install") - .about("Install a local .ipa or directory") - .arg(Arg::new("path").required(true).value_name("PATH")), - ) - .subcommand( - Command::new("upgrade") - .about("Upgrade from a local .ipa or directory") - .arg(Arg::new("path").required(true).value_name("PATH")), - ) - .get_matches(); - - if matches.get_flag("about") { - println!("ideviceinstaller - install/upgrade apps using AFC + InstallationProxy (Rust)"); - println!("Copyright (c) 2025"); - return; - } - - let udid = matches.get_one::("udid"); - let host = matches.get_one::("host"); - let pairing_file = matches.get_one::("pairing_file"); - - let provider = match common::get_provider(udid, host, pairing_file, "ideviceinstaller").await { - Ok(p) => p, - Err(e) => { - eprintln!("{e}"); - return; + match sub_name.as_str() { + "install" => { + let path: String = sub_args.next_argument().expect("required"); + match installation::install_package_with_callback( + &*provider, + path, + None, + |(percentage, _)| async move { + println!("Installing: {percentage}%"); + }, + (), + ) + .await + { + Ok(()) => println!("install success"), + Err(e) => eprintln!("Install failed: {e}"), + } } - }; - - if let Some(matches) = matches.subcommand_matches("install") { - let path: &String = matches.get_one("path").expect("required"); - match installation::install_package_with_callback( - &*provider, - path, - None, - |(percentage, _)| async move { - println!("Installing: {percentage}%"); - }, - (), - ) - .await - { - Ok(()) => println!("install success"), - Err(e) => eprintln!("Install failed: {e}"), + "upgrade" => { + let path: String = sub_args.next_argument().expect("required"); + match installation::upgrade_package_with_callback( + &*provider, + path, + None, + |(percentage, _)| async move { + println!("Upgrading: {percentage}%"); + }, + (), + ) + .await + { + Ok(()) => println!("upgrade success"), + Err(e) => eprintln!("Upgrade failed: {e}"), + } } - } else if let Some(matches) = matches.subcommand_matches("upgrade") { - let path: &String = matches.get_one("path").expect("required"); - match installation::upgrade_package_with_callback( - &*provider, - path, - None, - |(percentage, _)| async move { - println!("Upgrading: {percentage}%"); - }, - (), - ) - .await - { - Ok(()) => println!("upgrade success"), - Err(e) => eprintln!("Upgrade failed: {e}"), - } - } else { - eprintln!("Invalid usage, pass -h for help"); + _ => unreachable!(), } } diff --git a/tools/src/installcoordination_proxy.rs b/tools/src/installcoordination_proxy.rs index d00e867..08fb2f0 100644 --- a/tools/src/installcoordination_proxy.rs +++ b/tools/src/installcoordination_proxy.rs @@ -1,87 +1,39 @@ // Jackson Coxson -use clap::{Arg, Command}; use idevice::{ IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, - installcoordination_proxy::InstallcoordinationProxy, rsd::RsdHandshake, + installcoordination_proxy::InstallcoordinationProxy, provider::IdeviceProvider, + rsd::RsdHandshake, }; +use jkcli::{CollectedArguments, JkArgument, JkCommand}; -mod common; - -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - - let matches = Command::new("installationcoordination_proxy") - .about("") - .arg( - Arg::new("host") - .long("host") - .value_name("HOST") - .help("IP address of the device"), - ) - .arg( - Arg::new("pairing_file") - .long("pairing-file") - .value_name("PATH") - .help("Path to the pairing file"), - ) - .arg( - Arg::new("udid") - .value_name("UDID") - .help("UDID of the device (overrides host/pairing file)") - .index(1), - ) - .arg( - Arg::new("tunneld") - .long("tunneld") - .help("Use tunneld") - .action(clap::ArgAction::SetTrue), - ) - .arg( - Arg::new("about") - .long("about") - .help("Show about information") - .action(clap::ArgAction::SetTrue), - ) - .subcommand( - Command::new("info") - .about("Get info about an app on the device") - .arg( - Arg::new("bundle_id") +pub fn register() -> JkCommand { + JkCommand::new() + .help("Interact with the RemoteXPC installation coordination proxy") + .with_subcommand( + "info", + JkCommand::new() + .help("Get info about an app on the device") + .with_argument( + JkArgument::new() .required(true) - .help("The bundle ID to query"), + .with_help("The bundle ID to query"), ), ) - .subcommand( - Command::new("uninstall") - .about("Get info about an app on the device") - .arg( - Arg::new("bundle_id") + .with_subcommand( + "uninstall", + JkCommand::new() + .help("Uninstalls an app on the device") + .with_argument( + JkArgument::new() .required(true) - .help("The bundle ID to query"), + .with_help("The bundle ID to delete"), ), ) - .get_matches(); + .subcommand_required(true) +} - if matches.get_flag("about") { - println!("debug_proxy - connect to the debug proxy and run commands"); - println!("Copyright (c) 2025 Jackson Coxson"); - return; - } - - let udid = matches.get_one::("udid"); - let pairing_file = matches.get_one::("pairing_file"); - let host = matches.get_one::("host"); - - let provider = - match common::get_provider(udid, host, pairing_file, "app_service-jkcoxson").await { - Ok(p) => p, - Err(e) => { - eprintln!("{e}"); - return; - } - }; +pub async fn main(arguments: &CollectedArguments, provider: Box) { let proxy = CoreDeviceProxy::connect(&*provider) .await .expect("no core proxy"); @@ -103,30 +55,38 @@ async fn main() { .await .expect("no connect"); - if let Some(matches) = matches.subcommand_matches("info") { - let bundle_id: &String = match matches.get_one("bundle_id") { - Some(b) => b, - None => { - eprintln!("No bundle ID passed"); - return; - } - }; + let (sub_name, sub_args) = arguments.first_subcommand().unwrap(); + let mut sub_args = sub_args.clone(); - let res = icp.query_app_path(bundle_id).await.expect("no info"); - println!("Path: {res}"); - } 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; - } - }; + match sub_name.as_str() { + "info" => { + let bundle_id: String = match sub_args.next_argument() { + Some(b) => b, + None => { + eprintln!("No bundle ID passed"); + return; + } + }; - icp.uninstall_app(bundle_id) - .await - .expect("uninstall failed"); - } else { - eprintln!("Invalid usage, pass -h for help"); + let res = icp + .query_app_path(bundle_id.as_str()) + .await + .expect("no info"); + println!("Path: {res}"); + } + "uninstall" => { + let bundle_id: String = match sub_args.next_argument() { + Some(b) => b, + None => { + eprintln!("No bundle ID passed"); + return; + } + }; + + icp.uninstall_app(bundle_id.as_str()) + .await + .expect("uninstall failed"); + } + _ => unreachable!(), } } diff --git a/tools/src/instproxy.rs b/tools/src/instproxy.rs index 54eb64e..f126519 100644 --- a/tools/src/instproxy.rs +++ b/tools/src/instproxy.rs @@ -1,108 +1,84 @@ // Jackson Coxson // Just lists apps for now -use clap::{Arg, Command}; -use idevice::{IdeviceService, installation_proxy::InstallationProxyClient}; +use idevice::{ + IdeviceService, installation_proxy::InstallationProxyClient, provider::IdeviceProvider, +}; +use jkcli::{CollectedArguments, JkArgument, JkCommand}; -mod common; - -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - - let matches = Command::new("core_device_proxy_tun") - .about("Start a tunnel") - .arg( - Arg::new("host") - .long("host") - .value_name("HOST") - .help("IP address of the device"), +pub fn register() -> JkCommand { + JkCommand::new() + .help("Manage files in the AFC jail of a device") + .with_subcommand( + "lookup", + JkCommand::new().help("Gets the apps on the device"), ) - .arg( - Arg::new("pairing_file") - .long("pairing-file") - .value_name("PATH") - .help("Path to the pairing file"), + .with_subcommand( + "browse", + JkCommand::new().help("Browses the apps on the device"), ) - .arg( - Arg::new("udid") - .value_name("UDID") - .help("UDID of the device (overrides host/pairing file)") - .index(1), + .with_subcommand( + "check_capabilities", + JkCommand::new().help("Check the capabilities"), ) - .arg( - Arg::new("about") - .long("about") - .help("Show about information") - .action(clap::ArgAction::SetTrue), + .with_subcommand( + "install", + JkCommand::new() + .help("Install an app in the AFC jail") + .with_argument( + JkArgument::new() + .required(true) + .with_help("Path in the AFC jail"), + ), ) - .subcommand(Command::new("lookup").about("Gets the apps on the device")) - .subcommand(Command::new("browse").about("Browses the apps on the device")) - .subcommand(Command::new("check_capabilities").about("Check the capabilities")) - .subcommand( - Command::new("install") - .about("Install an app in the AFC jail") - .arg(Arg::new("path")), - ) - .get_matches(); - - if matches.get_flag("about") { - println!( - "instproxy - query and manage apps installed on a device. Reimplementation of libimobiledevice's binary." - ); - println!("Copyright (c) 2025 Jackson Coxson"); - return; - } - - let udid = matches.get_one::("udid"); - let host = matches.get_one::("host"); - let pairing_file = matches.get_one::("pairing_file"); - - let provider = match common::get_provider(udid, host, pairing_file, "instproxy-jkcoxson").await - { - Ok(p) => p, - Err(e) => { - eprintln!("{e}"); - return; - } - }; + .subcommand_required(true) +} +pub async fn main(arguments: &CollectedArguments, provider: Box) { let mut instproxy_client = InstallationProxyClient::connect(&*provider) .await .expect("Unable to connect to instproxy"); - if matches.subcommand_matches("lookup").is_some() { - let apps = instproxy_client.get_apps(Some("User"), None).await.unwrap(); - for app in apps.keys() { - println!("{app}"); - } - } else if matches.subcommand_matches("browse").is_some() { - instproxy_client.browse(None).await.expect("browse failed"); - } else if matches.subcommand_matches("check_capabilities").is_some() { - instproxy_client - .check_capabilities_match(Vec::new(), None) - .await - .expect("check failed"); - } else if let Some(matches) = matches.subcommand_matches("install") { - let path: &String = match matches.get_one("path") { - Some(p) => p, - None => { - eprintln!("No path passed, pass -h for help"); - return; - } - }; - instproxy_client - .install_with_callback( - path, - None, - async |(percentage, _)| { - println!("Installing: {percentage}"); - }, - (), - ) - .await - .expect("Failed to install") - } else { - eprintln!("Invalid usage, pass -h for help"); + let (sub_name, sub_args) = arguments.first_subcommand().expect("no sub arg"); + let mut sub_args = sub_args.clone(); + + match sub_name.as_str() { + "lookup" => { + let apps = instproxy_client.get_apps(Some("User"), None).await.unwrap(); + for app in apps.keys() { + println!("{app}"); + } + } + "browse" => { + instproxy_client.browse(None).await.expect("browse failed"); + } + "check_capabilities" => { + instproxy_client + .check_capabilities_match(Vec::new(), None) + .await + .expect("check failed"); + } + "install" => { + let path: String = match sub_args.next_argument() { + Some(p) => p, + None => { + eprintln!("No path passed, pass -h for help"); + return; + } + }; + + instproxy_client + .install_with_callback( + path, + None, + async |(percentage, _)| { + println!("Installing: {percentage}"); + }, + (), + ) + .await + .expect("Failed to install") + } + _ => unreachable!(), } } diff --git a/tools/src/location_simulation.rs b/tools/src/location_simulation.rs index a38fda9..ca364e1 100644 --- a/tools/src/location_simulation.rs +++ b/tools/src/location_simulation.rs @@ -1,70 +1,33 @@ // Jackson Coxson // Just lists apps for now -use clap::{Arg, Command}; +use idevice::provider::IdeviceProvider; use idevice::{IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake}; use idevice::dvt::location_simulation::LocationSimulationClient; use idevice::services::simulate_location::LocationSimulationService; -mod common; +use jkcli::{CollectedArguments, JkArgument, JkCommand}; -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); +pub fn register() -> JkCommand { + JkCommand::new() + .help("Simulate device location") + .with_subcommand( + "clear", + JkCommand::new().help("Clears the location set on the device"), + ) + .with_subcommand( + "set", + JkCommand::new() + .help("Set the location on the device") + .with_argument(JkArgument::new().with_help("latitude").required(true)) + .with_argument(JkArgument::new().with_help("longitutde").required(true)), + ) + .subcommand_required(true) +} - let matches = Command::new("simulate_location") - .about("Simulate device location") - .arg( - Arg::new("host") - .long("host") - .value_name("HOST") - .help("IP address of the device"), - ) - .arg( - Arg::new("pairing_file") - .long("pairing-file") - .value_name("PATH") - .help("Path to the pairing file"), - ) - .arg( - Arg::new("udid") - .value_name("UDID") - .help("UDID of the device (overrides host/pairing file)") - .index(1), - ) - .arg( - Arg::new("about") - .long("about") - .help("Show about information") - .action(clap::ArgAction::SetTrue), - ) - .subcommand(Command::new("clear").about("Clears the location set on the device")) - .subcommand( - Command::new("set") - .about("Set the location on the device") - .arg(Arg::new("latitude").required(true)) - .arg(Arg::new("longitude").required(true)), - ) - .get_matches(); - - if matches.get_flag("about") { - println!("simulate_location - Sets the simulated location on an iOS device"); - println!("Copyright (c) 2025 Jackson Coxson"); - return; - } - - let udid = matches.get_one::("udid"); - let host = matches.get_one::("host"); - let pairing_file = matches.get_one::("pairing_file"); - - let provider = - match common::get_provider(udid, host, pairing_file, "simulate_location-jkcoxson").await { - Ok(p) => p, - Err(e) => { - eprintln!("{e}"); - return; - } - }; +pub async fn main(arguments: &CollectedArguments, provider: Box) { + let (sub_name, sub_args) = arguments.first_subcommand().expect("No sub arg passed"); + let mut sub_args = sub_args.clone(); if let Ok(proxy) = CoreDeviceProxy::connect(&*provider).await { let rsd_port = proxy.handshake.server_rsd_port; @@ -86,42 +49,44 @@ async fn main() { let mut ls_client = LocationSimulationClient::new(&mut ls_client) .await .expect("Unable to get channel for location simulation"); - if matches.subcommand_matches("clear").is_some() { - ls_client.clear().await.expect("Unable to clear"); - println!("Location cleared!"); - } else if let Some(matches) = matches.subcommand_matches("set") { - let latitude: &String = match matches.get_one("latitude") { - Some(l) => l, - None => { - eprintln!("No latitude passed! Pass -h for help"); - return; - } - }; - let latitude: f64 = latitude.parse().expect("Failed to parse as float"); - let longitude: &String = match matches.get_one("longitude") { - Some(l) => l, - None => { - eprintln!("No longitude passed! Pass -h for help"); - return; - } - }; - let longitude: f64 = longitude.parse().expect("Failed to parse as float"); - ls_client - .set(latitude, longitude) - .await - .expect("Failed to set location"); - - println!("Location set!"); - println!("Press ctrl-c to stop"); - loop { + match sub_name.as_str() { + "clear" => { + ls_client.clear().await.expect("Unable to clear"); + println!("Location cleared!"); + } + "set" => { + let latitude: String = match sub_args.next_argument() { + Some(l) => l, + None => { + eprintln!("No latitude passed! Pass -h for help"); + return; + } + }; + let latitude: f64 = latitude.parse().expect("Failed to parse as float"); + let longitude: String = match sub_args.next_argument() { + Some(l) => l, + None => { + eprintln!("No longitude passed! Pass -h for help"); + return; + } + }; + let longitude: f64 = longitude.parse().expect("Failed to parse as float"); ls_client .set(latitude, longitude) .await .expect("Failed to set location"); - tokio::time::sleep(std::time::Duration::from_secs(5)).await; + + println!("Location set!"); + println!("Press ctrl-c to stop"); + loop { + ls_client + .set(latitude, longitude) + .await + .expect("Failed to set location"); + tokio::time::sleep(std::time::Duration::from_secs(5)).await; + } } - } else { - eprintln!("Invalid usage, pass -h for help"); + _ => unreachable!(), } } else { let mut location_client = match LocationSimulationService::connect(&*provider).await { @@ -133,35 +98,36 @@ async fn main() { return; } }; - if matches.subcommand_matches("clear").is_some() { - location_client.clear().await.expect("Unable to clear"); - println!("Location cleared!"); - } else if let Some(matches) = matches.subcommand_matches("set") { - let latitude: &String = match matches.get_one("latitude") { - Some(l) => l, - None => { - eprintln!("No latitude passed! Pass -h for help"); - return; - } - }; - let longitude: &String = match matches.get_one("longitude") { - Some(l) => l, - None => { - eprintln!("No longitude passed! Pass -h for help"); - return; - } - }; - location_client - .set(latitude, longitude) - .await - .expect("Failed to set location"); + match sub_name.as_str() { + "clear" => { + location_client.clear().await.expect("Unable to clear"); + println!("Location cleared!"); + } + "set" => { + let latitude: String = match sub_args.next_argument() { + Some(l) => l, + None => { + eprintln!("No latitude passed! Pass -h for help"); + return; + } + }; - println!("Location set!"); - } else { - eprintln!("Invalid usage, pass -h for help"); + let longitude: String = match sub_args.next_argument() { + Some(l) => l, + None => { + eprintln!("No longitude passed! Pass -h for help"); + return; + } + }; + location_client + .set(latitude.as_str(), longitude.as_str()) + .await + .expect("Failed to set location"); + + println!("Location set!"); + } + _ => unreachable!(), } }; - - return; } diff --git a/tools/src/lockdown.rs b/tools/src/lockdown.rs index 8f654f1..7e60224 100644 --- a/tools/src/lockdown.rs +++ b/tools/src/lockdown.rs @@ -1,78 +1,40 @@ // Jackson Coxson -use clap::{Arg, Command, arg}; -use idevice::{IdeviceService, lockdown::LockdownClient}; +use idevice::{IdeviceService, lockdown::LockdownClient, provider::IdeviceProvider}; +use jkcli::{CollectedArguments, JkArgument, JkCommand}; use plist::Value; use plist_macro::pretty_print_plist; -mod common; - -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - - let matches = Command::new("lockdown") - .about("Start a tunnel") - .arg( - Arg::new("host") - .long("host") - .value_name("HOST") - .help("IP address of the device"), +pub fn register() -> JkCommand { + JkCommand::new() + .help("Interact with lockdown") + .with_subcommand( + "get", + JkCommand::new() + .help("Gets a value from lockdown") + .with_argument(JkArgument::new().with_help("The value to get")) + .with_argument(JkArgument::new().with_help("The domain to get in")), ) - .arg( - Arg::new("pairing_file") - .long("pairing-file") - .value_name("PATH") - .help("Path to the pairing file"), + .with_subcommand( + "set", + JkCommand::new() + .help("Gets a value from lockdown") + .with_argument( + JkArgument::new() + .with_help("The value to set") + .required(true), + ) + .with_argument( + JkArgument::new() + .with_help("The value key to set") + .required(true), + ) + .with_argument(JkArgument::new().with_help("The domain to set in")), ) - .arg( - Arg::new("udid") - .value_name("UDID") - .help("UDID of the device (overrides host/pairing file)") - .index(1), - ) - .arg( - Arg::new("about") - .long("about") - .help("Show about information") - .action(clap::ArgAction::SetTrue), - ) - .subcommand( - Command::new("get") - .about("Gets a value") - .arg(arg!(-v --value "the value to get").required(false)) - .arg(arg!(-d --domain "the domain to get in").required(false)), - ) - .subcommand( - Command::new("set") - .about("Sets a lockdown value") - .arg(arg!(-k --key "the key to set").required(true)) - .arg(arg!(-v --value "the value to set the key to").required(true)) - .arg(arg!(-d --domain "the domain to get in").required(false)), - ) - .get_matches(); - - if matches.get_flag("about") { - println!( - "lockdown - query and manage values on a device. Reimplementation of libimobiledevice's binary." - ); - println!("Copyright (c) 2025 Jackson Coxson"); - return; - } - - let udid = matches.get_one::("udid"); - let host = matches.get_one::("host"); - let pairing_file = matches.get_one::("pairing_file"); - - let provider = - match common::get_provider(udid, host, pairing_file, "ideviceinfo-jkcoxson").await { - Ok(p) => p, - Err(e) => { - eprintln!("{e}"); - return; - } - }; + .subcommand_required(true) +} +pub async fn main(arguments: &CollectedArguments, provider: Box) { let mut lockdown_client = LockdownClient::connect(&*provider) .await .expect("Unable to connect to lockdown"); @@ -82,12 +44,27 @@ async fn main() { .await .expect("no session"); - match matches.subcommand() { - Some(("get", sub_m)) => { - let key = sub_m.get_one::("value").map(|x| x.as_str()); - let domain = sub_m.get_one::("domain").map(|x| x.as_str()); + let (sub_name, sub_args) = arguments.first_subcommand().expect("No subcommand"); + let mut sub_args = sub_args.clone(); - match lockdown_client.get_value(key, domain).await { + match sub_name.as_str() { + "get" => { + let key: Option = sub_args.next_argument(); + let domain: Option = sub_args.next_argument(); + + match lockdown_client + .get_value( + match &key { + Some(k) => Some(k.as_str()), + None => None, + }, + match &domain { + Some(d) => Some(d.as_str()), + None => None, + }, + ) + .await + { Ok(value) => { println!("{}", pretty_print_plist(&value)); } @@ -96,25 +73,28 @@ async fn main() { } } } - - Some(("set", sub_m)) => { - let key = sub_m.get_one::("key").unwrap(); - let value_str = sub_m.get_one::("value").unwrap(); - let domain = sub_m.get_one::("domain"); + "set" => { + let value_str: String = sub_args.next_argument().unwrap(); + let key: String = sub_args.next_argument().unwrap(); + let domain: Option = sub_args.next_argument(); let value = Value::String(value_str.clone()); match lockdown_client - .set_value(key, value, domain.map(|x| x.as_str())) + .set_value( + key, + value, + match &domain { + Some(d) => Some(d.as_str()), + None => None, + }, + ) .await { Ok(()) => println!("Successfully set"), Err(e) => eprintln!("Error setting value: {e}"), } } - - _ => { - eprintln!("No subcommand provided. Try `--help` for usage."); - } + _ => unreachable!(), } } diff --git a/tools/src/main.rs b/tools/src/main.rs new file mode 100644 index 0000000..5e215b4 --- /dev/null +++ b/tools/src/main.rs @@ -0,0 +1,328 @@ +// Jackson Coxson + +use std::{ + net::{IpAddr, SocketAddr}, + str::FromStr, +}; + +use idevice::{ + pairing_file::PairingFile, + provider::{IdeviceProvider, TcpProvider}, + usbmuxd::{Connection, UsbmuxdAddr, UsbmuxdConnection, UsbmuxdDevice}, +}; +use jkcli::{JkArgument, JkCommand, JkFlag}; + +mod activation; +mod afc; +mod amfi; +mod app_service; +mod bt_packet_logger; +mod companion_proxy; +mod crash_logs; +mod debug_proxy; +mod diagnostics; +mod diagnosticsservice; +mod dvt_packet_parser; +mod heartbeat_client; +mod ideviceinfo; +mod ideviceinstaller; +mod installcoordination_proxy; +mod instproxy; +mod location_simulation; +mod lockdown; +mod misagent; +mod mobilebackup2; +mod mounter; +mod notifications; +mod os_trace_relay; +mod pair; +mod pcapd; +mod preboard; +mod process_control; +mod remotexpc; +mod restore_service; +mod screenshot; +mod syslog_relay; + +mod pcap; + +#[tokio::main] +async fn main() { + tracing_subscriber::fmt::init(); + + // Set the base CLI + let arguments = JkCommand::new() + .with_flag( + JkFlag::new("about") + .with_help("Prints the about message") + .with_short_curcuit(|| { + eprintln!("idevice-rs-tools - Jackson Coxson\n"); + eprintln!("Tools to manage and manipulate iOS devices"); + eprintln!("Version {}", env!("CARGO_PKG_VERSION")); + eprintln!("https://github.com/jkcoxson/idevice"); + eprintln!("\nOn to eternal perfection!"); + std::process::exit(0); + }), + ) + .with_flag( + JkFlag::new("version") + .with_help("Prints the version") + .with_short_curcuit(|| { + println!("{}", env!("CARGO_PKG_VERSION")); + std::process::exit(0); + }), + ) + .with_flag( + JkFlag::new("pairing-file") + .with_argument(JkArgument::new().required(true)) + .with_help("The path to the pairing file to use"), + ) + .with_flag( + JkFlag::new("host") + .with_argument(JkArgument::new().required(true)) + .with_help("The host to connect to"), + ) + .with_flag( + JkFlag::new("udid") + .with_argument(JkArgument::new().required(true)) + .with_help("The UDID to use"), + ) + .with_subcommand("activation", activation::register()) + .with_subcommand("afc", afc::register()) + .with_subcommand("amfi", amfi::register()) + .with_subcommand("app_service", app_service::register()) + .with_subcommand("bt_packet_logger", bt_packet_logger::register()) + .with_subcommand("companion_proxy", companion_proxy::register()) + .with_subcommand("crash_logs", crash_logs::register()) + .with_subcommand("debug_proxy", debug_proxy::register()) + .with_subcommand("diagnostics", diagnostics::register()) + .with_subcommand("diagnosticsservice", diagnosticsservice::register()) + .with_subcommand("dvt_packet_parser", dvt_packet_parser::register()) + .with_subcommand("heartbeat_client", heartbeat_client::register()) + .with_subcommand("ideviceinfo", ideviceinfo::register()) + .with_subcommand("ideviceinstaller", ideviceinstaller::register()) + .with_subcommand( + "installcoordination_proxy", + installcoordination_proxy::register(), + ) + .with_subcommand("instproxy", instproxy::register()) + .with_subcommand("location_simulation", location_simulation::register()) + .with_subcommand("lockdown", lockdown::register()) + .with_subcommand("misagent", misagent::register()) + .with_subcommand("mobilebackup2", mobilebackup2::register()) + .with_subcommand("mounter", mounter::register()) + .with_subcommand("notifications", notifications::register()) + .with_subcommand("os_trace_relay", os_trace_relay::register()) + .with_subcommand("pair", pair::register()) + .with_subcommand("pcapd", pcapd::register()) + .with_subcommand("preboard", preboard::register()) + .with_subcommand("process_control", process_control::register()) + .with_subcommand("remotexpc", remotexpc::register()) + .with_subcommand("restore_service", restore_service::register()) + .with_subcommand("screenshot", screenshot::register()) + .with_subcommand("syslog_relay", syslog_relay::register()) + .subcommand_required(true) + .collect() + .expect("Failed to collect CLI args"); + + let udid = arguments.get_flag::("udid"); + let host = arguments.get_flag::("host"); + let pairing_file = arguments.get_flag::("pairing-file"); + + let provider = match get_provider(udid, host, pairing_file, "idevice-rs-tools").await { + Ok(p) => p, + Err(e) => { + eprintln!("{e}"); + return; + } + }; + + let (subcommand, sub_args) = match arguments.first_subcommand() { + Some(s) => s, + None => { + eprintln!("No subcommand passed, pass -h for help"); + return; + } + }; + + match subcommand.as_str() { + "activation" => { + activation::main(sub_args, provider).await; + } + "afc" => { + afc::main(sub_args, provider).await; + } + "amfi" => { + amfi::main(sub_args, provider).await; + } + "app_service" => { + app_service::main(sub_args, provider).await; + } + "bt_packet_logger" => { + bt_packet_logger::main(sub_args, provider).await; + } + "companion_proxy" => { + companion_proxy::main(sub_args, provider).await; + } + "crash_logs" => { + crash_logs::main(sub_args, provider).await; + } + "debug_proxy" => { + debug_proxy::main(sub_args, provider).await; + } + "diagnostics" => { + diagnostics::main(sub_args, provider).await; + } + "diagnosticsservice" => { + diagnosticsservice::main(sub_args, provider).await; + } + "dvt_packet_parser" => { + dvt_packet_parser::main(sub_args, provider).await; + } + "heartbeat_client" => { + heartbeat_client::main(sub_args, provider).await; + } + "ideviceinfo" => { + ideviceinfo::main(sub_args, provider).await; + } + "ideviceinstaller" => { + ideviceinstaller::main(sub_args, provider).await; + } + "installcoordination_proxy" => { + installcoordination_proxy::main(sub_args, provider).await; + } + "instproxy" => { + instproxy::main(sub_args, provider).await; + } + "location_simulation" => { + location_simulation::main(sub_args, provider).await; + } + "lockdown" => { + lockdown::main(sub_args, provider).await; + } + "misagent" => { + misagent::main(sub_args, provider).await; + } + "mobilebackup2" => { + mobilebackup2::main(sub_args, provider).await; + } + "mounter" => { + mounter::main(sub_args, provider).await; + } + "notifications" => { + notifications::main(sub_args, provider).await; + } + "os_trace_relay" => { + os_trace_relay::main(sub_args, provider).await; + } + "pair" => { + pair::main(sub_args, provider).await; + } + "pcapd" => { + pcapd::main(sub_args, provider).await; + } + "preboard" => { + preboard::main(sub_args, provider).await; + } + "process_control" => { + process_control::main(sub_args, provider).await; + } + "remotexpc" => { + remotexpc::main(sub_args, provider).await; + } + "restore_service" => { + restore_service::main(sub_args, provider).await; + } + "screenshot" => { + screenshot::main(sub_args, provider).await; + } + "syslog_relay" => { + syslog_relay::main(sub_args, provider).await; + } + _ => unreachable!(), + } +} + +async fn get_provider( + udid: Option, + host: Option, + pairing_file: Option, + label: &str, +) -> Result, String> { + let provider: Box = if let Some(udid) = udid { + let mut usbmuxd = if let Ok(var) = std::env::var("USBMUXD_SOCKET_ADDRESS") { + let socket = SocketAddr::from_str(&var).expect("Bad USBMUXD_SOCKET_ADDRESS"); + let socket = tokio::net::TcpStream::connect(socket) + .await + .expect("unable to connect to socket address"); + UsbmuxdConnection::new(Box::new(socket), 1) + } else { + UsbmuxdConnection::default() + .await + .expect("Unable to connect to usbmxud") + }; + + let dev = match usbmuxd.get_device(udid.as_str()).await { + Ok(d) => d, + Err(e) => { + return Err(format!("Device not found: {e:?}")); + } + }; + Box::new(dev.to_provider(UsbmuxdAddr::from_env_var().unwrap(), label)) + } else if let Some(host) = host + && let Some(pairing_file) = pairing_file + { + let host = match IpAddr::from_str(host.as_str()) { + Ok(h) => h, + Err(e) => { + return Err(format!("Invalid host: {e:?}")); + } + }; + let pairing_file = match PairingFile::read_from_file(pairing_file) { + Ok(p) => p, + Err(e) => { + return Err(format!("Unable to read pairing file: {e:?}")); + } + }; + + Box::new(TcpProvider { + addr: host, + pairing_file, + label: label.to_string(), + }) + } else { + let mut usbmuxd = if let Ok(var) = std::env::var("USBMUXD_SOCKET_ADDRESS") { + let socket = SocketAddr::from_str(&var).expect("Bad USBMUXD_SOCKET_ADDRESS"); + let socket = tokio::net::TcpStream::connect(socket) + .await + .expect("unable to connect to socket address"); + UsbmuxdConnection::new(Box::new(socket), 1) + } else { + UsbmuxdConnection::default() + .await + .expect("Unable to connect to usbmxud") + }; + let devs = match usbmuxd.get_devices().await { + Ok(d) => d, + Err(e) => { + return Err(format!("Unable to get devices from usbmuxd: {e:?}")); + } + }; + let usb_devs: Vec<&UsbmuxdDevice> = devs + .iter() + .filter(|x| x.connection_type == Connection::Usb) + .collect(); + + if devs.is_empty() { + return Err("No devices connected!".to_string()); + } + + let chosen_dev = if !usb_devs.is_empty() { + usb_devs[0] + } else { + &devs[0] + }; + Box::new(chosen_dev.to_provider(UsbmuxdAddr::from_env_var().unwrap(), label)) + }; + Ok(provider) +} diff --git a/tools/src/misagent.rs b/tools/src/misagent.rs index 443607f..39c1971 100644 --- a/tools/src/misagent.rs +++ b/tools/src/misagent.rs @@ -2,99 +2,71 @@ use std::path::PathBuf; -use clap::{Arg, Command, arg, value_parser}; -use idevice::{IdeviceService, misagent::MisagentClient}; +use idevice::{IdeviceService, misagent::MisagentClient, provider::IdeviceProvider}; +use jkcli::{CollectedArguments, JkArgument, JkCommand}; -mod common; - -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - - let matches = Command::new("core_device_proxy_tun") - .about("Start a tunnel") - .arg( - Arg::new("host") - .long("host") - .value_name("HOST") - .help("IP address of the device"), - ) - .arg( - Arg::new("pairing_file") - .long("pairing-file") - .value_name("PATH") - .help("Path to the pairing file"), - ) - .arg( - Arg::new("udid") - .value_name("UDID") - .help("UDID of the device (overrides host/pairing file)"), - ) - .arg( - Arg::new("about") - .long("about") - .help("Show about information") - .action(clap::ArgAction::SetTrue), - ) - .subcommand( - Command::new("list") - .about("Lists the images mounted on the device") - .arg( - arg!(-s --save "the folder to save the profiles to") - .value_parser(value_parser!(PathBuf)), +pub fn register() -> JkCommand { + JkCommand::new() + .help("Manage provisioning profiles on the device") + .with_subcommand( + "list", + JkCommand::new() + .help("List profiles installed on the device") + .with_argument( + JkArgument::new() + .with_help("Path to save profiles from the device") + .required(false), ), ) - .subcommand( - Command::new("remove") - .about("Remove a provisioning profile") - .arg(Arg::new("id").required(true).index(1)), + .with_subcommand( + "remove", + JkCommand::new() + .help("Remove a profile installed on the device") + .with_argument( + JkArgument::new() + .with_help("ID of the profile to remove") + .required(true), + ), ) - .get_matches(); + .subcommand_required(true) +} - if matches.get_flag("about") { - println!( - "mounter - query and manage images mounted on a device. Reimplementation of libimobiledevice's binary." - ); - println!("Copyright (c) 2025 Jackson Coxson"); - return; - } +pub async fn main(arguments: &CollectedArguments, provider: Box) { + tracing_subscriber::fmt::init(); - let udid = matches.get_one::("udid"); - let host = matches.get_one::("host"); - let pairing_file = matches.get_one::("pairing_file"); - - let provider = match common::get_provider(udid, host, pairing_file, "misagent-jkcoxson").await { - Ok(p) => p, - Err(e) => { - eprintln!("{e}"); - return; - } - }; let mut misagent_client = MisagentClient::connect(&*provider) .await .expect("Unable to connect to misagent"); - if let Some(matches) = matches.subcommand_matches("list") { - let images = misagent_client - .copy_all() - .await - .expect("Unable to get images"); - if let Some(path) = matches.get_one::("save") { - tokio::fs::create_dir_all(path) - .await - .expect("Unable to create save DIR"); + let (sub_name, sub_args) = arguments.first_subcommand().expect("No subcommand passed"); + let mut sub_args = sub_args.clone(); - for (index, image) in images.iter().enumerate() { - let f = path.join(format!("{index}.pem")); - tokio::fs::write(f, image) + match sub_name.as_str() { + "list" => { + let images = misagent_client + .copy_all() + .await + .expect("Unable to get images"); + if let Some(path) = sub_args.next_argument::() { + tokio::fs::create_dir_all(&path) .await - .expect("Failed to write image"); + .expect("Unable to create save DIR"); + + for (index, image) in images.iter().enumerate() { + let f = path.join(format!("{index}.pem")); + tokio::fs::write(f, image) + .await + .expect("Failed to write image"); + } } } - } else if let Some(matches) = matches.subcommand_matches("remove") { - let id = matches.get_one::("id").expect("No ID passed"); - misagent_client.remove(id).await.expect("Failed to remove"); - } else { - eprintln!("Invalid usage, pass -h for help"); + "remove" => { + let id = sub_args.next_argument::().expect("No ID passed"); + misagent_client + .remove(id.as_str()) + .await + .expect("Failed to remove"); + } + _ => unreachable!(), } } diff --git a/tools/src/mobilebackup2.rs b/tools/src/mobilebackup2.rs index 18749e8..15c827b 100644 --- a/tools/src/mobilebackup2.rs +++ b/tools/src/mobilebackup2.rs @@ -1,191 +1,135 @@ // Jackson Coxson // Mobile Backup 2 tool for iOS devices -use clap::{Arg, Command}; use idevice::{ IdeviceService, mobilebackup2::{MobileBackup2Client, RestoreOptions}, + provider::IdeviceProvider, }; +use jkcli::{CollectedArguments, JkArgument, JkCommand, JkFlag}; use plist::Dictionary; use std::fs; use std::io::{Read, Write}; use std::path::Path; -mod common; - -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - - let matches = Command::new("mobilebackup2") - .about("Mobile Backup 2 tool for iOS devices") - .arg( - Arg::new("host") - .long("host") - .value_name("HOST") - .help("IP address of the device"), - ) - .arg( - Arg::new("pairing_file") - .long("pairing-file") - .value_name("PATH") - .help("Path to the pairing file"), - ) - .arg( - Arg::new("udid") - .value_name("UDID") - .help("UDID of the device (overrides host/pairing file)") - .index(1), - ) - .arg( - Arg::new("about") - .long("about") - .help("Show about information") - .action(clap::ArgAction::SetTrue), - ) - .subcommand( - Command::new("info") - .about("Get backup information from a local backup directory") - .arg(Arg::new("dir").long("dir").value_name("DIR").required(true)) - .arg( - Arg::new("source") - .long("source") - .value_name("SOURCE") - .help("Source identifier (defaults to current UDID)"), - ), - ) - .subcommand( - Command::new("list") - .about("List files of the last backup from a local backup directory") - .arg(Arg::new("dir").long("dir").value_name("DIR").required(true)) - .arg(Arg::new("source").long("source").value_name("SOURCE")), - ) - .subcommand( - Command::new("backup") - .about("Start a backup operation") - .arg( - Arg::new("dir") - .long("dir") - .value_name("DIR") - .help("Backup directory on host") +pub fn register() -> JkCommand { + JkCommand::new() + .help("Mobile Backup 2 tool for iOS devices") + .with_subcommand( + "info", + JkCommand::new() + .help("Get backup information from a local backup directory") + .with_argument( + JkArgument::new() + .with_help("Backup DIR to read from") .required(true), ) - .arg( - Arg::new("target") - .long("target") - .value_name("TARGET") - .help("Target identifier for the backup"), - ) - .arg( - Arg::new("source") - .long("source") - .value_name("SOURCE") - .help("Source identifier for the backup"), + .with_argument( + JkArgument::new() + .with_help("Source identifier (defaults to current UDID)") + .required(true), ), ) - .subcommand( - Command::new("restore") - .about("Restore from a local backup directory (DeviceLink)") - .arg(Arg::new("dir").long("dir").value_name("DIR").required(true)) - .arg( - Arg::new("source") - .long("source") - .value_name("SOURCE") - .help("Source UDID; defaults to current device UDID"), + .with_subcommand( + "list", + JkCommand::new() + .help("List files of the last backup from a local backup directory") + .with_argument( + JkArgument::new() + .with_help("Backup DIR to read from") + .required(true), ) - .arg( - Arg::new("password") - .long("password") - .value_name("PWD") - .help("Backup password if encrypted"), - ) - .arg( - Arg::new("no-reboot") - .long("no-reboot") - .action(clap::ArgAction::SetTrue), - ) - .arg( - Arg::new("no-copy") - .long("no-copy") - .action(clap::ArgAction::SetTrue), - ) - .arg( - Arg::new("no-settings") - .long("no-settings") - .action(clap::ArgAction::SetTrue), - ) - .arg( - Arg::new("system") - .long("system") - .action(clap::ArgAction::SetTrue), - ) - .arg( - Arg::new("remove") - .long("remove") - .action(clap::ArgAction::SetTrue), + .with_argument( + JkArgument::new() + .with_help("Source identifier (defaults to current UDID)") + .required(true), ), ) - .subcommand( - Command::new("unback") - .about("Unpack a complete backup to device hierarchy") - .arg(Arg::new("dir").long("dir").value_name("DIR").required(true)) - .arg(Arg::new("source").long("source").value_name("SOURCE")) - .arg(Arg::new("password").long("password").value_name("PWD")), - ) - .subcommand( - Command::new("extract") - .about("Extract a file from a previous backup") - .arg(Arg::new("dir").long("dir").value_name("DIR").required(true)) - .arg(Arg::new("source").long("source").value_name("SOURCE")) - .arg( - Arg::new("domain") - .long("domain") - .value_name("DOMAIN") + .with_subcommand( + "backup", + JkCommand::new() + .help("Start a backup operation") + .with_argument( + JkArgument::new() + .with_help("Backup directory on host") .required(true), ) - .arg( - Arg::new("path") - .long("path") - .value_name("REL_PATH") + .with_argument( + JkArgument::new() + .with_help("Target identifier for the backup") .required(true), ) - .arg(Arg::new("password").long("password").value_name("PWD")), + .with_argument( + JkArgument::new() + .with_help("Source identifier for the backup") + .required(true), + ), ) - .subcommand( - Command::new("change-password") - .about("Change backup password") - .arg(Arg::new("dir").long("dir").value_name("DIR").required(true)) - .arg(Arg::new("old").long("old").value_name("OLD")) - .arg(Arg::new("new").long("new").value_name("NEW")), + .with_subcommand( + "restore", + JkCommand::new() + .help("Restore from a local backup directory (DeviceLink)") + .with_argument(JkArgument::new().with_help("DIR").required(true)) + .with_argument( + JkArgument::new() + .with_help("Source UDID; defaults to current device UDID") + .required(true), + ) + .with_argument( + JkArgument::new() + .with_help("Backup password if encrypted") + .required(true), + ) + .with_flag(JkFlag::new("no-reboot")) + .with_flag(JkFlag::new("no-copy")) + .with_flag(JkFlag::new("no-settings")) + .with_flag(JkFlag::new("system")) + .with_flag(JkFlag::new("remove")), ) - .subcommand( - Command::new("erase-device") - .about("Erase the device via mobilebackup2") - .arg(Arg::new("dir").long("dir").value_name("DIR").required(true)), + .with_subcommand( + "unback", + JkCommand::new() + .help("Unpack a complete backup to device hierarchy") + .with_argument(JkArgument::new().with_help("DIR").required(true)) + .with_argument(JkArgument::new().with_help("Source")) + .with_argument(JkArgument::new().with_help("Password")), ) - .subcommand(Command::new("freespace").about("Get free space information")) - .subcommand(Command::new("encryption").about("Check backup encryption status")) - .get_matches(); - - if matches.get_flag("about") { - println!("mobilebackup2 - manage device backups using Mobile Backup 2 service"); - println!("Copyright (c) 2025 Jackson Coxson"); - return; - } - - let udid = matches.get_one::("udid"); - let host = matches.get_one::("host"); - let pairing_file = matches.get_one::("pairing_file"); - - let provider = - match common::get_provider(udid, host, pairing_file, "mobilebackup2-jkcoxson").await { - Ok(p) => p, - Err(e) => { - eprintln!("Error creating provider: {e}"); - return; - } - }; + .with_subcommand( + "extract", + JkCommand::new() + .help("Extract a file from a previous backup") + .with_argument(JkArgument::new().with_help("DIR").required(true)) + .with_argument(JkArgument::new().with_help("Source").required(true)) + .with_argument(JkArgument::new().with_help("Domain").required(true)) + .with_argument(JkArgument::new().with_help("Path").required(true)) + .with_argument(JkArgument::new().with_help("Password").required(true)), + ) + .with_subcommand( + "change-password", + JkCommand::new() + .help("Change backup password") + .with_argument(JkArgument::new().with_help("DIR").required(true)) + .with_argument(JkArgument::new().with_help("Old password").required(true)) + .with_argument(JkArgument::new().with_help("New password").required(true)), + ) + .with_subcommand( + "erase-device", + JkCommand::new() + .help("Erase the device via mobilebackup2") + .with_argument(JkArgument::new().with_help("DIR").required(true)), + ) + .with_subcommand( + "freespace", + JkCommand::new().help("Get free space information"), + ) + .with_subcommand( + "encryption", + JkCommand::new().help("Check backup encryption status"), + ) + .subcommand_required(true) +} +pub async fn main(arguments: &CollectedArguments, provider: Box) { let mut backup_client = match MobileBackup2Client::connect(&*provider).await { Ok(client) => client, Err(e) => { @@ -194,11 +138,16 @@ async fn main() { } }; - match matches.subcommand() { - Some(("info", sub)) => { - let dir = sub.get_one::("dir").unwrap(); - let source = sub.get_one::("source").map(|s| s.as_str()); - match backup_client.info_from_path(Path::new(dir), source).await { + let (sub_name, sub_args) = arguments.first_subcommand().unwrap(); + let mut sub_args = sub_args.clone(); + + match sub_name.as_str() { + "info" => { + let dir = sub_args.next_argument::().unwrap(); + let source = sub_args.next_argument::(); + let source = source.as_deref(); + + match backup_client.info_from_path(Path::new(&dir), source).await { Ok(dict) => { println!("Backup Information:"); for (k, v) in dict { @@ -208,10 +157,12 @@ async fn main() { Err(e) => eprintln!("Failed to get info: {e}"), } } - Some(("list", sub)) => { - let dir = sub.get_one::("dir").unwrap(); - let source = sub.get_one::("source").map(|s| s.as_str()); - match backup_client.list_from_path(Path::new(dir), source).await { + "list" => { + let dir = sub_args.next_argument::().unwrap(); + let source = sub_args.next_argument::(); + let source = source.as_deref(); + + match backup_client.list_from_path(Path::new(&dir), source).await { Ok(dict) => { println!("List Response:"); for (k, v) in dict { @@ -221,12 +172,12 @@ async fn main() { Err(e) => eprintln!("Failed to list: {e}"), } } - Some(("backup", sub_matches)) => { - let target = sub_matches.get_one::("target").map(|s| s.as_str()); - let source = sub_matches.get_one::("source").map(|s| s.as_str()); - let dir = sub_matches - .get_one::("dir") - .expect("dir is required"); + "backup" => { + let target = sub_args.next_argument::(); + let target = target.as_deref(); + let source = sub_args.next_argument::(); + let source = source.as_deref(); + let dir = sub_args.next_argument::().expect("dir is required"); println!("Starting backup operation..."); let res = backup_client @@ -234,95 +185,112 @@ async fn main() { .await; if let Err(e) = res { eprintln!("Failed to send backup request: {e}"); - } else if let Err(e) = process_dl_loop(&mut backup_client, Path::new(dir)).await { + } else if let Err(e) = process_dl_loop(&mut backup_client, Path::new(&dir)).await { eprintln!("Backup failed during DL loop: {e}"); } else { println!("Backup flow finished"); } } - Some(("restore", sub)) => { - let dir = sub.get_one::("dir").unwrap(); - let source = sub.get_one::("source").map(|s| s.as_str()); + "restore" => { + let dir = sub_args.next_argument::().unwrap(); + let source = sub_args.next_argument::(); + let source = source.as_deref(); + let mut ropts = RestoreOptions::new(); - if sub.get_flag("no-reboot") { + if sub_args.has_flag("no-reboot") { ropts = ropts.with_reboot(false); } - if sub.get_flag("no-copy") { + if sub_args.has_flag("no-copy") { ropts = ropts.with_copy(false); } - if sub.get_flag("no-settings") { + if sub_args.has_flag("no-settings") { ropts = ropts.with_preserve_settings(false); } - if sub.get_flag("system") { + if sub_args.has_flag("system") { ropts = ropts.with_system_files(true); } - if sub.get_flag("remove") { + if sub_args.has_flag("remove") { ropts = ropts.with_remove_items_not_restored(true); } - if let Some(pw) = sub.get_one::("password") { + if let Some(pw) = sub_args.next_argument::() { ropts = ropts.with_password(pw); } match backup_client - .restore_from_path(Path::new(dir), source, Some(ropts)) + .restore_from_path(Path::new(&dir), source, Some(ropts)) .await { Ok(_) => println!("Restore flow finished"), Err(e) => eprintln!("Restore failed: {e}"), } } - Some(("unback", sub)) => { - let dir = sub.get_one::("dir").unwrap(); - let source = sub.get_one::("source").map(|s| s.as_str()); - let password = sub.get_one::("password").map(|s| s.as_str()); + "unback" => { + let dir = sub_args.next_argument::().unwrap(); + let source = sub_args.next_argument::(); + let source = source.as_deref(); + let password = sub_args.next_argument::(); + let password = password.as_deref(); + match backup_client - .unback_from_path(Path::new(dir), password, source) + .unback_from_path(Path::new(&dir), password, source) .await { Ok(_) => println!("Unback finished"), Err(e) => eprintln!("Unback failed: {e}"), } } - Some(("extract", sub)) => { - let dir = sub.get_one::("dir").unwrap(); - let source = sub.get_one::("source").map(|s| s.as_str()); - let domain = sub.get_one::("domain").unwrap(); - let rel = sub.get_one::("path").unwrap(); - let password = sub.get_one::("password").map(|s| s.as_str()); + "extract" => { + let dir = sub_args.next_argument::().unwrap(); + let source = sub_args.next_argument::(); + let source = source.as_deref(); + let domain = sub_args.next_argument::().unwrap(); + let rel = sub_args.next_argument::().unwrap(); + let password = sub_args.next_argument::(); + let password = password.as_deref(); + match backup_client - .extract_from_path(domain, rel, Path::new(dir), password, source) + .extract_from_path( + domain.as_str(), + rel.as_str(), + Path::new(&dir), + password, + source, + ) .await { Ok(_) => println!("Extract finished"), Err(e) => eprintln!("Extract failed: {e}"), } } - Some(("change-password", sub)) => { - let dir = sub.get_one::("dir").unwrap(); - let old = sub.get_one::("old").map(|s| s.as_str()); - let newv = sub.get_one::("new").map(|s| s.as_str()); + "change-password" => { + let dir = sub_args.next_argument::().unwrap(); + let old = sub_args.next_argument::(); + let old = old.as_deref(); + let newv = sub_args.next_argument::(); + let newv = newv.as_deref(); + match backup_client - .change_password_from_path(Path::new(dir), old, newv) + .change_password_from_path(Path::new(&dir), old, newv) .await { Ok(_) => println!("Change password finished"), Err(e) => eprintln!("Change password failed: {e}"), } } - Some(("erase-device", sub)) => { - let dir = sub.get_one::("dir").unwrap(); - match backup_client.erase_device_from_path(Path::new(dir)).await { + "erase-device" => { + let dir = sub_args.next_argument::().unwrap(); + match backup_client.erase_device_from_path(Path::new(&dir)).await { Ok(_) => println!("Erase device command sent"), Err(e) => eprintln!("Erase device failed: {e}"), } } - Some(("freespace", _)) => match backup_client.get_freespace().await { + "freespace" => match backup_client.get_freespace().await { Ok(freespace) => { let freespace_gb = freespace as f64 / (1024.0 * 1024.0 * 1024.0); println!("Free space: {freespace} bytes ({freespace_gb:.2} GB)"); } Err(e) => eprintln!("Failed to get free space: {e}"), }, - Some(("encryption", _)) => match backup_client.check_backup_encryption().await { + "encryption" => match backup_client.check_backup_encryption().await { Ok(is_encrypted) => { println!( "Backup encryption: {}", diff --git a/tools/src/mounter.rs b/tools/src/mounter.rs index baeb4ee..4674294 100644 --- a/tools/src/mounter.rs +++ b/tools/src/mounter.rs @@ -3,88 +3,58 @@ use std::{io::Write, path::PathBuf}; -use clap::{Arg, Command, arg, value_parser}; -use idevice::{IdeviceService, lockdown::LockdownClient, mobile_image_mounter::ImageMounter}; +use idevice::{ + IdeviceService, lockdown::LockdownClient, mobile_image_mounter::ImageMounter, + provider::IdeviceProvider, +}; +use jkcli::{CollectedArguments, JkArgument, JkCommand, JkFlag}; use plist_macro::pretty_print_plist; -mod common; - -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - - let matches = Command::new("core_device_proxy_tun") - .about("Start a tunnel") - .arg( - Arg::new("host") - .long("host") - .value_name("HOST") - .help("IP address of the device"), +pub fn register() -> JkCommand { + JkCommand::new() + .help("Manage mounts on an iOS device") + .with_subcommand( + "list", + JkCommand::new().help("Lists the images mounted on the device"), ) - .arg( - Arg::new("pairing_file") - .long("pairing-file") - .value_name("PATH") - .help("Path to the pairing file"), + .with_subcommand( + "unmount", + JkCommand::new().help("Unmounts the developer disk image"), ) - .arg( - Arg::new("udid") - .value_name("UDID") - .help("UDID of the device (overrides host/pairing file)") - .index(1), - ) - .arg( - Arg::new("about") - .long("about") - .help("Show about information") - .action(clap::ArgAction::SetTrue), - ) - .subcommand(Command::new("list").about("Lists the images mounted on the device")) - .subcommand(Command::new("unmount").about("Unmounts the developer disk image")) - .subcommand( - Command::new("mount") - .about("Mounts the developer disk image") - .arg( - arg!(-i --image "the developer disk image to mount") - .value_parser(value_parser!(PathBuf)) + .with_subcommand( + "mount", + JkCommand::new() + .help("Mounts the developer disk image") + .with_flag( + JkFlag::new("image") + .with_short("i") + .with_argument(JkArgument::new().required(true)) + .with_help("A path to the image to mount") .required(true), ) - .arg( - arg!(-b --manifest "the build manifest (iOS 17+)") - .value_parser(value_parser!(PathBuf)), + .with_flag( + JkFlag::new("manifest") + .with_short("b") + .with_argument(JkArgument::new()) + .with_help("the build manifest (iOS 17+)"), ) - .arg( - arg!(-t --trustcache "the trust cache (iOS 17+)") - .value_parser(value_parser!(PathBuf)), + .with_flag( + JkFlag::new("trustcache") + .with_short("t") + .with_argument(JkArgument::new()) + .with_help("the trust cache (iOS 17+)"), ) - .arg( - arg!(-s --signature "the image signature (iOS < 17.0") - .value_parser(value_parser!(PathBuf)), + .with_flag( + JkFlag::new("signature") + .with_short("s") + .with_argument(JkArgument::new()) + .with_help("the image signature (iOS < 17.0"), ), ) - .get_matches(); - - if matches.get_flag("about") { - println!( - "mounter - query and manage images mounted on a device. Reimplementation of libimobiledevice's binary." - ); - println!("Copyright (c) 2025 Jackson Coxson"); - return; - } - - let udid = matches.get_one::("udid"); - let host = matches.get_one::("host"); - let pairing_file = matches.get_one::("pairing_file"); - - let provider = - match common::get_provider(udid, host, pairing_file, "ideviceinfo-jkcoxson").await { - Ok(p) => p, - Err(e) => { - eprintln!("{e}"); - return; - } - }; + .subcommand_required(true) +} +pub async fn main(arguments: &CollectedArguments, provider: Box) { let mut lockdown_client = LockdownClient::connect(&*provider) .await .expect("Unable to connect to lockdown"); @@ -117,114 +87,120 @@ async fn main() { .await .expect("Unable to connect to image mounter"); - if matches.subcommand_matches("list").is_some() { - let images = mounter_client - .copy_devices() - .await - .expect("Unable to get images"); - for i in images { - println!("{}", pretty_print_plist(&i)); - } - } else if matches.subcommand_matches("unmount").is_some() { - if product_version < 17 { - mounter_client - .unmount_image("/Developer") + let (subcommand, sub_args) = arguments + .first_subcommand() + .expect("No subcommand passed! Pass -h for help"); + + match subcommand.as_str() { + "list" => { + let images = mounter_client + .copy_devices() .await - .expect("Failed to unmount"); - } else { - mounter_client - .unmount_image("/System/Developer") - .await - .expect("Failed to unmount"); - } - } else if let Some(matches) = matches.subcommand_matches("mount") { - let image: &PathBuf = match matches.get_one("image") { - Some(i) => i, - None => { - eprintln!("No image was passed! Pass -h for help"); - return; + .expect("Unable to get images"); + for i in images { + println!("{}", pretty_print_plist(&i)); } - }; - let image = tokio::fs::read(image).await.expect("Unable to read image"); - if product_version < 17 { - let signature: &PathBuf = match matches.get_one("signature") { - Some(s) => s, - None => { - eprintln!("No signature was passed! Pass -h for help"); - return; - } - }; - let signature = tokio::fs::read(signature) - .await - .expect("Unable to read signature"); - - mounter_client - .mount_developer(&image, signature) - .await - .expect("Unable to mount"); - } else { - let manifest: &PathBuf = match matches.get_one("manifest") { - Some(s) => s, - None => { - eprintln!("No build manifest was passed! Pass -h for help"); - return; - } - }; - let build_manifest = &tokio::fs::read(manifest) - .await - .expect("Unable to read signature"); - - let trust_cache: &PathBuf = match matches.get_one("trustcache") { - Some(s) => s, - None => { - eprintln!("No trust cache was passed! Pass -h for help"); - return; - } - }; - let trust_cache = tokio::fs::read(trust_cache) - .await - .expect("Unable to read signature"); - - let unique_chip_id = - match lockdown_client.get_value(Some("UniqueChipID"), None).await { - Ok(u) => u, - Err(_) => { - lockdown_client - .start_session(&provider.get_pairing_file().await.unwrap()) - .await - .expect("Unable to start session"); - lockdown_client - .get_value(Some("UniqueChipID"), None) - .await - .expect("Unable to get UniqueChipID") - } - } - .as_unsigned_integer() - .expect("Unexpected value for chip IP"); - - mounter_client - .mount_personalized_with_callback( - &*provider, - image, - trust_cache, - build_manifest, - None, - unique_chip_id, - async |((n, d), _)| { - let percent = (n as f64 / d as f64) * 100.0; - print!("\rProgress: {percent:.2}%"); - std::io::stdout().flush().unwrap(); // Make sure it prints immediately - if n == d { - println!(); - } - }, - (), - ) - .await - .expect("Unable to mount"); } - } else { - eprintln!("Invalid usage, pass -h for help"); + "unmount" => { + if product_version < 17 { + mounter_client + .unmount_image("/Developer") + .await + .expect("Failed to unmount"); + } else { + mounter_client + .unmount_image("/System/Developer") + .await + .expect("Failed to unmount"); + } + } + "mount" => { + let image: PathBuf = match sub_args.get_flag("image") { + Some(i) => i, + None => { + eprintln!("No image was passed! Pass -h for help"); + return; + } + }; + let image = tokio::fs::read(image).await.expect("Unable to read image"); + if product_version < 17 { + let signature: PathBuf = match sub_args.get_flag("signature") { + Some(s) => s, + None => { + eprintln!("No signature was passed! Pass -h for help"); + return; + } + }; + let signature = tokio::fs::read(signature) + .await + .expect("Unable to read signature"); + + mounter_client + .mount_developer(&image, signature) + .await + .expect("Unable to mount"); + } else { + let manifest: PathBuf = match sub_args.get_flag("manifest") { + Some(s) => s, + None => { + eprintln!("No build manifest was passed! Pass -h for help"); + return; + } + }; + let build_manifest = &tokio::fs::read(manifest) + .await + .expect("Unable to read signature"); + + let trust_cache: PathBuf = match sub_args.get_flag("trustcache") { + Some(s) => s, + None => { + eprintln!("No trust cache was passed! Pass -h for help"); + return; + } + }; + let trust_cache = tokio::fs::read(trust_cache) + .await + .expect("Unable to read signature"); + + let unique_chip_id = + match lockdown_client.get_value(Some("UniqueChipID"), None).await { + Ok(u) => u, + Err(_) => { + lockdown_client + .start_session(&provider.get_pairing_file().await.unwrap()) + .await + .expect("Unable to start session"); + lockdown_client + .get_value(Some("UniqueChipID"), None) + .await + .expect("Unable to get UniqueChipID") + } + } + .as_unsigned_integer() + .expect("Unexpected value for chip IP"); + + mounter_client + .mount_personalized_with_callback( + &*provider, + image, + trust_cache, + build_manifest, + None, + unique_chip_id, + async |((n, d), _)| { + let percent = (n as f64 / d as f64) * 100.0; + print!("\rProgress: {percent:.2}%"); + std::io::stdout().flush().unwrap(); // Make sure it prints immediately + if n == d { + println!(); + } + }, + (), + ) + .await + .expect("Unable to mount"); + } + } + _ => unreachable!(), } - return; } diff --git a/tools/src/notifications.rs b/tools/src/notifications.rs index 703eb9d..7026489 100644 --- a/tools/src/notifications.rs +++ b/tools/src/notifications.rs @@ -1,59 +1,16 @@ // Monitor memory and app notifications -use clap::{Arg, Command}; -use idevice::{IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake}; -mod common; +use idevice::{ + IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, provider::IdeviceProvider, + rsd::RsdHandshake, +}; +use jkcli::{CollectedArguments, JkCommand}; -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - let matches = Command::new("notifications") - .about("start notifications") - .arg( - Arg::new("host") - .long("host") - .value_name("HOST") - .help("IP address of the device"), - ) - .arg( - Arg::new("pairing_file") - .long("pairing-file") - .value_name("PATH") - .help("Path to the pairing file"), - ) - .arg( - Arg::new("udid") - .value_name("UDID") - .help("UDID of the device (overrides host/pairing file)") - .index(1), - ) - .arg( - Arg::new("about") - .long("about") - .help("Show about information") - .action(clap::ArgAction::SetTrue), - ) - .get_matches(); - - if matches.get_flag("about") { - print!("notifications - start notifications to ios device"); - println!("Copyright (c) 2025 Jackson Coxson"); - return; - } - - let udid = matches.get_one::("udid"); - let host = matches.get_one::("host"); - let pairing_file = matches.get_one::("pairing_file"); - - let provider = - match common::get_provider(udid, host, pairing_file, "notifications-jkcoxson").await { - Ok(p) => p, - Err(e) => { - eprintln!("{e}"); - return; - } - }; +pub fn register() -> JkCommand { + JkCommand::new().help("Notification proxy") +} +pub async fn main(_arguments: &CollectedArguments, provider: Box) { let proxy = CoreDeviceProxy::connect(&*provider) .await .expect("no core proxy"); @@ -80,7 +37,6 @@ async fn main() { .await .expect("Failed to start notifications"); - // Handle Ctrl+C gracefully loop { tokio::select! { _ = tokio::signal::ctrl_c() => { @@ -88,7 +44,6 @@ async fn main() { break; } - // Branch 2: Wait for the next batch of notifications. result = notification_client.get_notification() => { if let Err(e) = result { eprintln!("Failed to get notifications: {}", e); diff --git a/tools/src/os_trace_relay.rs b/tools/src/os_trace_relay.rs index f85f396..f3daf44 100644 --- a/tools/src/os_trace_relay.rs +++ b/tools/src/os_trace_relay.rs @@ -1,58 +1,13 @@ // Jackson Coxson -use clap::{Arg, Command}; -use idevice::{IdeviceService, os_trace_relay::OsTraceRelayClient}; +use idevice::{IdeviceService, os_trace_relay::OsTraceRelayClient, provider::IdeviceProvider}; +use jkcli::{CollectedArguments, JkCommand}; -mod common; +pub fn register() -> JkCommand { + JkCommand::new().help("Relay OS logs") +} -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - - let matches = Command::new("os_trace_relay") - .about("Relay system logs") - .arg( - Arg::new("host") - .long("host") - .value_name("HOST") - .help("IP address of the device"), - ) - .arg( - Arg::new("pairing_file") - .long("pairing-file") - .value_name("PATH") - .help("Path to the pairing file"), - ) - .arg( - Arg::new("udid") - .value_name("UDID") - .help("UDID of the device (overrides host/pairing file)"), - ) - .arg( - Arg::new("about") - .long("about") - .help("Show about information") - .action(clap::ArgAction::SetTrue), - ) - .get_matches(); - - if matches.get_flag("about") { - println!("Relay logs on the device"); - println!("Copyright (c) 2025 Jackson Coxson"); - return; - } - - let udid = matches.get_one::("udid"); - let host = matches.get_one::("host"); - let pairing_file = matches.get_one::("pairing_file"); - - let provider = match common::get_provider(udid, host, pairing_file, "misagent-jkcoxson").await { - Ok(p) => p, - Err(e) => { - eprintln!("{e}"); - return; - } - }; +pub async fn main(_arguments: &CollectedArguments, provider: Box) { let log_client = OsTraceRelayClient::connect(&*provider) .await .expect("Unable to connect to misagent"); diff --git a/tools/src/pair.rs b/tools/src/pair.rs index fae58e8..1cfc23d 100644 --- a/tools/src/pair.rs +++ b/tools/src/pair.rs @@ -1,46 +1,29 @@ // Jackson Coxson -use clap::{Arg, Command}; use idevice::{ IdeviceService, lockdown::LockdownClient, + provider::IdeviceProvider, usbmuxd::{Connection, UsbmuxdAddr, UsbmuxdConnection}, }; +use jkcli::{CollectedArguments, JkArgument, JkCommand}; -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); +pub fn register() -> JkCommand { + JkCommand::new() + .help("Manage files in the AFC jail of a device") + .with_argument(JkArgument::new().with_help("A UDID to override and pair with")) +} - let matches = Command::new("pair") - .about("Pair with the device") - .arg( - Arg::new("udid") - .value_name("UDID") - .help("UDID of the device (overrides host/pairing file)") - .index(1), - ) - .arg( - Arg::new("about") - .long("about") - .help("Show about information") - .action(clap::ArgAction::SetTrue), - ) - .get_matches(); - - if matches.get_flag("about") { - println!("pair - pair with the device"); - println!("Copyright (c) 2025 Jackson Coxson"); - return; - } - - let udid = matches.get_one::("udid"); +pub async fn main(arguments: &CollectedArguments, _provider: Box) { + let mut arguments = arguments.clone(); + let udid: Option = arguments.next_argument(); let mut u = UsbmuxdConnection::default() .await .expect("Failed to connect to usbmuxd"); let dev = match udid { Some(udid) => u - .get_device(udid) + .get_device(udid.as_str()) .await .expect("Failed to get device with specific udid"), None => u diff --git a/tools/src/pcapd.rs b/tools/src/pcapd.rs index d489f45..2fca403 100644 --- a/tools/src/pcapd.rs +++ b/tools/src/pcapd.rs @@ -1,55 +1,20 @@ // Jackson Coxson -use clap::{Arg, Command}; use idevice::{ IdeviceService, pcapd::{PcapFileWriter, PcapdClient}, + provider::IdeviceProvider, }; +use jkcli::{CollectedArguments, JkArgument, JkCommand}; -mod common; +pub fn register() -> JkCommand { + JkCommand::new() + .help("Writes pcap network data") + .with_argument(JkArgument::new().with_help("Write PCAP to this file (use '-' for stdout)")) +} -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - - let matches = Command::new("pcapd") - .about("Capture IP packets") - .arg( - Arg::new("udid") - .value_name("UDID") - .help("UDID of the device (overrides host/pairing file)") - .index(1), - ) - .arg( - Arg::new("about") - .long("about") - .help("Show about information") - .action(clap::ArgAction::SetTrue), - ) - .arg( - Arg::new("out") - .long("out") - .value_name("PCAP") - .help("Write PCAP to this file (use '-' for stdout)"), - ) - .get_matches(); - - if matches.get_flag("about") { - println!("bt_packet_logger - capture bluetooth packets"); - println!("Copyright (c) 2025 Jackson Coxson"); - return; - } - - let udid = matches.get_one::("udid"); - let out = matches.get_one::("out").map(String::to_owned); - - let provider = match common::get_provider(udid, None, None, "pcapd-jkcoxson").await { - Ok(p) => p, - Err(e) => { - eprintln!("{e}"); - return; - } - }; +pub async fn main(arguments: &CollectedArguments, provider: Box) { + let out = arguments.clone().next_argument::(); let mut logger_client = PcapdClient::connect(&*provider) .await diff --git a/tools/src/preboard.rs b/tools/src/preboard.rs index 5655db3..a6bfdb3 100644 --- a/tools/src/preboard.rs +++ b/tools/src/preboard.rs @@ -1,76 +1,34 @@ // Jackson Coxson -use clap::{Arg, Command}; -use idevice::{IdeviceService, preboard_service::PreboardServiceClient}; +use idevice::{IdeviceService, preboard_service::PreboardServiceClient, provider::IdeviceProvider}; +use jkcli::{CollectedArguments, JkCommand}; -mod common; - -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - - let matches = Command::new("preboard") - .about("Mess with developer mode") - .arg( - Arg::new("host") - .long("host") - .value_name("HOST") - .help("IP address of the device"), - ) - .arg( - Arg::new("pairing_file") - .long("pairing-file") - .value_name("PATH") - .help("Path to the pairing file"), - ) - .arg( - Arg::new("udid") - .value_name("UDID") - .help("UDID of the device (overrides host/pairing file)") - .index(1), - ) - .arg( - Arg::new("about") - .long("about") - .help("Show about information") - .action(clap::ArgAction::SetTrue), - ) - .subcommand(Command::new("create").about("Create a stashbag??")) - .subcommand(Command::new("commit").about("Commit a stashbag??")) - .get_matches(); - - if matches.get_flag("about") { - println!("preboard - no idea what this does"); - println!("Copyright (c) 2025 Jackson Coxson"); - return; - } - - let udid = matches.get_one::("udid"); - let host = matches.get_one::("host"); - let pairing_file = matches.get_one::("pairing_file"); - - let provider = match common::get_provider(udid, host, pairing_file, "amfi-jkcoxson").await { - Ok(p) => p, - Err(e) => { - eprintln!("{e}"); - return; - } - }; +pub fn register() -> JkCommand { + JkCommand::new() + .help("Interact with the preboard service") + .with_subcommand("create", JkCommand::new().help("Create a stashbag??")) + .with_subcommand("commit", JkCommand::new().help("Commit a stashbag??")) + .subcommand_required(true) +} +pub async fn main(arguments: &CollectedArguments, provider: Box) { let mut pc = PreboardServiceClient::connect(&*provider) .await .expect("Failed to connect to Preboard"); - if matches.subcommand_matches("create").is_some() { - pc.create_stashbag(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 0]) - .await - .expect("Failed to create"); - } else if matches.subcommand_matches("commit").is_some() { - pc.commit_stashbag(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 0]) - .await - .expect("Failed to create"); - } else { - eprintln!("Invalid usage, pass -h for help"); + let (sub_name, _) = arguments.first_subcommand().unwrap(); + + match sub_name.as_str() { + "create" => { + pc.create_stashbag(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 0]) + .await + .expect("Failed to create"); + } + "commit" => { + pc.commit_stashbag(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 0]) + .await + .expect("Failed to create"); + } + _ => unreachable!(), } - return; } diff --git a/tools/src/process_control.rs b/tools/src/process_control.rs index d0939b4..743d498 100644 --- a/tools/src/process_control.rs +++ b/tools/src/process_control.rs @@ -1,76 +1,26 @@ // Jackson Coxson -use clap::{Arg, Command}; +use idevice::provider::IdeviceProvider; use idevice::services::lockdown::LockdownClient; use idevice::{IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake}; +use jkcli::{CollectedArguments, JkArgument, JkCommand}; -mod common; +pub fn register() -> JkCommand { + JkCommand::new() + .help("Launch an app with process control") + .with_argument( + JkArgument::new() + .required(true) + .with_help("The bundle ID to launch"), + ) +} -#[tokio::main] -async fn main() { +pub async fn main(arguments: &CollectedArguments, provider: Box) { tracing_subscriber::fmt::init(); - let matches = Command::new("process_control") - .about("Query process control") - .arg( - Arg::new("host") - .long("host") - .value_name("HOST") - .help("IP address of the device"), - ) - .arg( - Arg::new("pairing_file") - .long("pairing-file") - .value_name("PATH") - .help("Path to the pairing file"), - ) - .arg( - Arg::new("udid") - .value_name("UDID") - .help("UDID of the device (overrides host/pairing file)") - .index(2), - ) - .arg( - Arg::new("about") - .long("about") - .help("Show about information") - .action(clap::ArgAction::SetTrue), - ) - .arg( - Arg::new("tunneld") - .long("tunneld") - .help("Use tunneld for connection") - .action(clap::ArgAction::SetTrue), - ) - .arg( - Arg::new("bundle_id") - .value_name("Bundle ID") - .help("Bundle ID of the app to launch") - .index(1), - ) - .get_matches(); + let mut arguments = arguments.clone(); - if matches.get_flag("about") { - println!("process_control - launch and manage processes on the device"); - println!("Copyright (c) 2025 Jackson Coxson"); - return; - } - - let udid = matches.get_one::("udid"); - let pairing_file = matches.get_one::("pairing_file"); - let host = matches.get_one::("host"); - let bundle_id = matches - .get_one::("bundle_id") - .expect("No bundle ID specified"); - - let provider = - match common::get_provider(udid, host, pairing_file, "process_control-jkcoxson").await { - Ok(p) => p, - Err(e) => { - eprintln!("{e}"); - return; - } - }; + let bundle_id: String = arguments.next_argument().expect("No bundle ID specified"); let mut rs_client_opt: Option< idevice::dvt::remote_server::RemoteServerClient>, diff --git a/tools/src/remotexpc.rs b/tools/src/remotexpc.rs index 31df4b6..5b25573 100644 --- a/tools/src/remotexpc.rs +++ b/tools/src/remotexpc.rs @@ -1,65 +1,17 @@ // Jackson Coxson // Print out all the RemoteXPC services -use clap::{Arg, Command}; use idevice::{ - IdeviceService, core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake, - tcp::stream::AdapterStream, + IdeviceService, core_device_proxy::CoreDeviceProxy, provider::IdeviceProvider, + rsd::RsdHandshake, tcp::stream::AdapterStream, }; +use jkcli::{CollectedArguments, JkCommand}; -mod common; - -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - - let matches = Command::new("remotexpc") - .about("Get services from RemoteXPC") - .arg( - Arg::new("host") - .long("host") - .value_name("HOST") - .help("IP address of the device"), - ) - .arg( - Arg::new("pairing_file") - .long("pairing-file") - .value_name("PATH") - .help("Path to the pairing file"), - ) - .arg( - Arg::new("udid") - .value_name("UDID") - .help("UDID of the device (overrides host/pairing file)") - .index(1), - ) - .arg( - Arg::new("about") - .long("about") - .help("Show about information") - .action(clap::ArgAction::SetTrue), - ) - .get_matches(); - - if matches.get_flag("about") { - println!("remotexpc - get info from RemoteXPC"); - println!("Copyright (c) 2025 Jackson Coxson"); - return; - } - - let udid = matches.get_one::("udid"); - let pairing_file = matches.get_one::("pairing_file"); - let host = matches.get_one::("host"); - - let provider = match common::get_provider(udid, host, pairing_file, "remotexpc-jkcoxson").await - { - Ok(p) => p, - Err(e) => { - eprintln!("{e}"); - return; - } - }; +pub fn register() -> JkCommand { + JkCommand::new().help("Get services from RemoteXPC") +} +pub async fn main(_arguments: &CollectedArguments, provider: Box) { let proxy = CoreDeviceProxy::connect(&*provider) .await .expect("no core proxy"); diff --git a/tools/src/restore_service.rs b/tools/src/restore_service.rs index 37b5a93..3daecf6 100644 --- a/tools/src/restore_service.rs +++ b/tools/src/restore_service.rs @@ -1,77 +1,41 @@ // Jackson Coxson -use clap::{Arg, Command}; use idevice::{ - IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, + IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, provider::IdeviceProvider, restore_service::RestoreServiceClient, rsd::RsdHandshake, }; +use jkcli::{CollectedArguments, JkArgument, JkCommand}; use plist_macro::pretty_print_dictionary; -mod common; - -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - - let matches = Command::new("restore_service") - .about("Interact with the Restore Service service") - .arg( - Arg::new("host") - .long("host") - .value_name("HOST") - .help("IP address of the device"), +pub fn register() -> JkCommand { + JkCommand::new() + .help("Interact with the Restore Service service") + .with_subcommand("delay", JkCommand::new().help("Delay recovery image")) + .with_subcommand("recovery", JkCommand::new().help("Enter recovery mode")) + .with_subcommand("reboot", JkCommand::new().help("Reboots the device")) + .with_subcommand( + "preflightinfo", + JkCommand::new().help("Gets the preflight info"), ) - .arg( - Arg::new("pairing_file") - .long("pairing-file") - .value_name("PATH") - .help("Path to the pairing file"), + .with_subcommand("nonces", JkCommand::new().help("Gets the nonces")) + .with_subcommand( + "app_parameters", + JkCommand::new().help("Gets the app parameters"), ) - .arg( - Arg::new("udid") - .value_name("UDID") - .help("UDID of the device (overrides host/pairing file)"), + .with_subcommand( + "restore_lang", + JkCommand::new() + .help("Restores the language") + .with_argument( + JkArgument::new() + .required(true) + .with_help("Language to restore"), + ), ) - .arg( - Arg::new("about") - .long("about") - .help("Show about information") - .action(clap::ArgAction::SetTrue), - ) - .subcommand(Command::new("delay").about("Delay recovery image")) - .subcommand(Command::new("recovery").about("Enter recovery mode")) - .subcommand(Command::new("reboot").about("Reboots the device")) - .subcommand(Command::new("preflightinfo").about("Gets the preflight info")) - .subcommand(Command::new("nonces").about("Gets the nonces")) - .subcommand(Command::new("app_parameters").about("Gets the app parameters")) - .subcommand( - Command::new("restore_lang") - .about("Restores the language") - .arg(Arg::new("language").required(true).index(1)), - ) - .get_matches(); - - if matches.get_flag("about") { - println!( - "mounter - query and manage images mounted on a device. Reimplementation of libimobiledevice's binary." - ); - println!("Copyright (c) 2025 Jackson Coxson"); - return; - } - - let udid = matches.get_one::("udid"); - let host = matches.get_one::("host"); - let pairing_file = matches.get_one::("pairing_file"); - - let provider = - match common::get_provider(udid, host, pairing_file, "restore_service-jkcoxson").await { - Ok(p) => p, - Err(e) => { - eprintln!("{e}"); - return; - } - }; + .subcommand_required(true) +} +pub async fn main(arguments: &CollectedArguments, provider: Box) { let proxy = CoreDeviceProxy::connect(&*provider) .await .expect("no core proxy"); @@ -89,37 +53,46 @@ async fn main() { .await .expect("Unable to connect to service"); - if matches.subcommand_matches("recovery").is_some() { - restore_client - .enter_recovery() - .await - .expect("command failed"); - } else if matches.subcommand_matches("reboot").is_some() { - restore_client.reboot().await.expect("command failed"); - } else if matches.subcommand_matches("preflightinfo").is_some() { - let info = restore_client - .get_preflightinfo() - .await - .expect("command failed"); - pretty_print_dictionary(&info); - } else if matches.subcommand_matches("nonces").is_some() { - let nonces = restore_client.get_nonces().await.expect("command failed"); - pretty_print_dictionary(&nonces); - } else if matches.subcommand_matches("app_parameters").is_some() { - let params = restore_client - .get_app_parameters() - .await - .expect("command failed"); - pretty_print_dictionary(¶ms); - } else if let Some(matches) = matches.subcommand_matches("restore_lang") { - let lang = matches - .get_one::("language") - .expect("No language passed"); - restore_client - .restore_lang(lang) - .await - .expect("failed to restore lang"); - } else { - eprintln!("Invalid usage, pass -h for help"); + let (sub_name, sub_args) = arguments.first_subcommand().unwrap(); + let mut sub_args = sub_args.clone(); + + match sub_name.as_str() { + "recovery" => { + restore_client + .enter_recovery() + .await + .expect("command failed"); + } + "reboot" => { + restore_client.reboot().await.expect("command failed"); + } + "preflightinfo" => { + let info = restore_client + .get_preflightinfo() + .await + .expect("command failed"); + println!("{}", pretty_print_dictionary(&info)); + } + "nonces" => { + let nonces = restore_client.get_nonces().await.expect("command failed"); + println!("{}", pretty_print_dictionary(&nonces)); + } + "app_parameters" => { + let params = restore_client + .get_app_parameters() + .await + .expect("command failed"); + println!("{}", pretty_print_dictionary(¶ms)); + } + "restore_lang" => { + let lang: String = sub_args + .next_argument::() + .expect("No language passed"); + restore_client + .restore_lang(lang) + .await + .expect("failed to restore lang"); + } + _ => unreachable!(), } } diff --git a/tools/src/screenshot.rs b/tools/src/screenshot.rs index db55613..82d1a17 100644 --- a/tools/src/screenshot.rs +++ b/tools/src/screenshot.rs @@ -1,69 +1,20 @@ -use clap::{Arg, Command}; -use idevice::{IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake}; +use idevice::{ + IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, provider::IdeviceProvider, + rsd::RsdHandshake, +}; +use jkcli::{CollectedArguments, JkArgument, JkCommand}; use std::fs; use idevice::screenshotr::ScreenshotService; -mod common; +pub fn register() -> JkCommand { + JkCommand::new() + .help("Take a screenshot") + .with_argument(JkArgument::new().with_help("Output path").required(true)) +} -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - let matches = Command::new("screen_shot") - .about("take screenshot") - .arg( - Arg::new("host") - .long("host") - .value_name("HOST") - .help("IP address of the device"), - ) - .arg( - Arg::new("pairing_file") - .long("pairing-file") - .value_name("PATH") - .help("Path to the pairing file"), - ) - .arg( - Arg::new("udid") - .value_name("UDID") - .help("UDID of the device (overrides host/pairing file)") - .index(1), - ) - .arg( - Arg::new("output") - .short('o') - .long("output") - .value_name("FILE") - .help("Output file path for the screenshot (default: ./screenshot.png)") - .default_value("screenshot.png"), - ) - .arg( - Arg::new("about") - .long("about") - .help("Show about information") - .action(clap::ArgAction::SetTrue), - ) - .get_matches(); - - if matches.get_flag("about") { - print!("screen_shot - take screenshot from ios device"); - println!("Copyright (c) 2025 Jackson Coxson"); - return; - } - - let udid = matches.get_one::("udid"); - let host = matches.get_one::("host"); - let pairing_file = matches.get_one::("pairing_file"); - let output_path = matches.get_one::("output").unwrap(); - - let provider = - match common::get_provider(udid, host, pairing_file, "take_screenshot-jkcoxson").await { - Ok(p) => p, - Err(e) => { - eprintln!("{e}"); - return; - } - }; +pub async fn main(arguments: &CollectedArguments, provider: Box) { + let output_path = arguments.clone().next_argument::().unwrap(); let res = if let Ok(proxy) = CoreDeviceProxy::connect(&*provider).await { println!("Using DVT over CoreDeviceProxy"); @@ -104,7 +55,7 @@ async fn main() { screenshot_client.take_screenshot().await.unwrap() }; - match fs::write(output_path, res) { + match fs::write(&output_path, res) { Ok(_) => println!("Screenshot saved to: {}", output_path), Err(e) => eprintln!("Failed to write screenshot to file: {}", e), } diff --git a/tools/src/syslog_relay.rs b/tools/src/syslog_relay.rs index 0552579..11b116c 100644 --- a/tools/src/syslog_relay.rs +++ b/tools/src/syslog_relay.rs @@ -1,58 +1,13 @@ // Jackson Coxson -use clap::{Arg, Command}; -use idevice::{IdeviceService, syslog_relay::SyslogRelayClient}; +use idevice::{IdeviceService, provider::IdeviceProvider, syslog_relay::SyslogRelayClient}; +use jkcli::{CollectedArguments, JkCommand}; -mod common; +pub fn register() -> JkCommand { + JkCommand::new().help("Relay system logs") +} -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - - let matches = Command::new("syslog_relay") - .about("Relay system logs") - .arg( - Arg::new("host") - .long("host") - .value_name("HOST") - .help("IP address of the device"), - ) - .arg( - Arg::new("pairing_file") - .long("pairing-file") - .value_name("PATH") - .help("Path to the pairing file"), - ) - .arg( - Arg::new("udid") - .value_name("UDID") - .help("UDID of the device (overrides host/pairing file)"), - ) - .arg( - Arg::new("about") - .long("about") - .help("Show about information") - .action(clap::ArgAction::SetTrue), - ) - .get_matches(); - - if matches.get_flag("about") { - println!("Relay logs on the device"); - println!("Copyright (c) 2025 Jackson Coxson"); - return; - } - - let udid = matches.get_one::("udid"); - let host = matches.get_one::("host"); - let pairing_file = matches.get_one::("pairing_file"); - - let provider = match common::get_provider(udid, host, pairing_file, "misagent-jkcoxson").await { - Ok(p) => p, - Err(e) => { - eprintln!("{e}"); - return; - } - }; +pub async fn main(_arguments: &CollectedArguments, provider: Box) { let mut log_client = SyslogRelayClient::connect(&*provider) .await .expect("Unable to connect to misagent"); From 96b380ebc900845461c3dff60dd64423b9b843fc Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Mon, 5 Jan 2026 06:42:56 -0700 Subject: [PATCH 12/34] Correctly parse DDI image lookup result --- idevice/src/services/mobile_image_mounter.rs | 6 +++++- tools/src/mounter.rs | 15 +++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/idevice/src/services/mobile_image_mounter.rs b/idevice/src/services/mobile_image_mounter.rs index bb4c18b..1c2f1d1 100644 --- a/idevice/src/services/mobile_image_mounter.rs +++ b/idevice/src/services/mobile_image_mounter.rs @@ -90,7 +90,11 @@ impl ImageMounter { self.idevice.send_plist(req).await?; let res = self.idevice.read_plist().await?; - match res.get("ImageSignature") { + match res + .get("ImageSignature") + .and_then(|x| x.as_array()) + .and_then(|x| x.first()) + { Some(plist::Value::Data(signature)) => Ok(signature.clone()), _ => Err(IdeviceError::NotFound), } diff --git a/tools/src/mounter.rs b/tools/src/mounter.rs index 4674294..90e9ebc 100644 --- a/tools/src/mounter.rs +++ b/tools/src/mounter.rs @@ -17,6 +17,10 @@ pub fn register() -> JkCommand { "list", JkCommand::new().help("Lists the images mounted on the device"), ) + .with_subcommand( + "lookup", + JkCommand::new().help("Lookup the image signature on the device"), + ) .with_subcommand( "unmount", JkCommand::new().help("Unmounts the developer disk image"), @@ -101,6 +105,17 @@ pub async fn main(arguments: &CollectedArguments, provider: Box { + let sig = mounter_client + .lookup_image(if product_version < 17 { + "Developer" + } else { + "Personalized" + }) + .await + .expect("Failed to lookup images"); + println!("Image signature: {sig:02X?}"); + } "unmount" => { if product_version < 17 { mounter_client From 13be1ae377f56cf489340544ff10b654032247e9 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Mon, 5 Jan 2026 06:55:11 -0700 Subject: [PATCH 13/34] Add read_entire to FFI a --- ffi/examples/afc.c | 2 +- ffi/src/afc.rs | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/ffi/examples/afc.c b/ffi/examples/afc.c index c2ac39e..a6edfb4 100644 --- a/ffi/examples/afc.c +++ b/ffi/examples/afc.c @@ -254,7 +254,7 @@ int main(int argc, char **argv) { } else { uint8_t *data = NULL; size_t length = 0; - err = afc_file_read(file, &data, &length); + err = afc_file_read_entire(file, &data, &length); if (err == NULL) { if (write_file(dest_path, data, length)) { printf("File downloaded successfully\n"); diff --git a/ffi/src/afc.rs b/ffi/src/afc.rs index 549ab7a..8a3d692 100644 --- a/ffi/src/afc.rs +++ b/ffi/src/afc.rs @@ -604,6 +604,45 @@ pub unsafe extern "C" fn afc_file_read( } } +/// Reads all data from an open file. +/// +/// # Arguments +/// * [`handle`] - File handle to read from +/// * [`data`] - Will be set to point to the read data +/// * [`length`] - The number of bytes read from the file +/// +/// # Returns +/// An IdeviceFfiError on error, null on success +/// +/// # Safety +/// All pointers must be valid and non-null +#[unsafe(no_mangle)] +pub unsafe extern "C" fn afc_file_read_entire( + handle: *mut AfcFileHandle, + data: *mut *mut u8, + length: *mut libc::size_t, +) -> *mut IdeviceFfiError { + if handle.is_null() || data.is_null() || length.is_null() { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + + let fd = unsafe { &mut *(handle as *mut idevice::afc::file::FileDescriptor) }; + let res: Result, IdeviceError> = run_sync(async move { fd.read_entire().await }); + + match res { + Ok(bytes) => { + let mut boxed = bytes.into_boxed_slice(); + unsafe { + *data = boxed.as_mut_ptr(); + *length = boxed.len(); + } + std::mem::forget(boxed); + null_mut() + } + Err(e) => ffi_err!(e), + } +} + /// Moves the read/write cursor in an open file. /// /// # Arguments From ae39fcb7dfe6538715160663edbf1ee9c17268b8 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Mon, 5 Jan 2026 07:08:40 -0700 Subject: [PATCH 14/34] Add afc2 abstractions (#55) --- ffi/src/afc.rs | 38 +++++++++++++++++++++++++++++++++ idevice/src/lib.rs | 4 ++++ idevice/src/services/afc/mod.rs | 38 +++++++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+) diff --git a/ffi/src/afc.rs b/ffi/src/afc.rs index 8a3d692..71ae9af 100644 --- a/ffi/src/afc.rs +++ b/ffi/src/afc.rs @@ -54,6 +54,44 @@ pub unsafe extern "C" fn afc_client_connect( } } +/// Connects to the AFC2 service using a TCP provider +/// +/// # Arguments +/// * [`provider`] - An IdeviceProvider +/// * [`client`] - On success, will be set to point to a newly allocated AfcClient handle +/// +/// # Returns +/// An IdeviceFfiError on error, null on success +/// +/// # Safety +/// `provider` must be a valid pointer to a handle allocated by this library +/// `client` must be a valid, non-null pointer to a location where the handle will be stored +#[unsafe(no_mangle)] +pub unsafe extern "C" fn afc2_client_connect( + provider: *mut IdeviceProviderHandle, + client: *mut *mut AfcClientHandle, +) -> *mut IdeviceFfiError { + if provider.is_null() || client.is_null() { + tracing::error!("Null pointer provided"); + return ffi_err!(IdeviceError::FfiInvalidArg); + } + + let res = run_sync_local(async { + let provider_ref: &dyn IdeviceProvider = unsafe { &*(*provider).0 }; + + AfcClient::new_afc2(provider_ref).await + }); + + match res { + Ok(r) => { + let boxed = Box::new(AfcClientHandle(r)); + unsafe { *client = Box::into_raw(boxed) }; + null_mut() + } + Err(e) => ffi_err!(e), + } +} + /// Creates a new AfcClient from an existing Idevice connection /// /// # Arguments diff --git a/idevice/src/lib.rs b/idevice/src/lib.rs index 5aa23d6..5081f33 100644 --- a/idevice/src/lib.rs +++ b/idevice/src/lib.rs @@ -76,6 +76,7 @@ pub trait IdeviceService: Sized { async fn connect(provider: &dyn IdeviceProvider) -> Result { let mut lockdown = LockdownClient::connect(provider).await?; + #[cfg(feature = "openssl")] let legacy = lockdown .get_value(Some("ProductVersion"), None) .await @@ -87,6 +88,9 @@ pub trait IdeviceService: Sized { .map(|x| x < 5) .unwrap_or(false); + #[cfg(not(feature = "openssl"))] + let legacy = false; + lockdown .start_session(&provider.get_pairing_file().await?) .await?; diff --git a/idevice/src/services/afc/mod.rs b/idevice/src/services/afc/mod.rs index f868585..8f8d71d 100644 --- a/idevice/src/services/afc/mod.rs +++ b/idevice/src/services/afc/mod.rs @@ -13,6 +13,7 @@ use tracing::warn; use crate::{ Idevice, IdeviceError, IdeviceService, afc::file::{FileDescriptor, OwnedFileDescriptor}, + lockdown::LockdownClient, obf, }; @@ -91,6 +92,43 @@ impl AfcClient { } } + /// Connects to afc2 from a provider + pub async fn new_afc2( + provider: &dyn crate::provider::IdeviceProvider, + ) -> Result { + let mut lockdown = LockdownClient::connect(provider).await?; + + #[cfg(feature = "openssl")] + let legacy = lockdown + .get_value(Some("ProductVersion"), None) + .await + .ok() + .as_ref() + .and_then(|x| x.as_string()) + .and_then(|x| x.split(".").next()) + .and_then(|x| x.parse::().ok()) + .map(|x| x < 5) + .unwrap_or(false); + + #[cfg(not(feature = "openssl"))] + let legacy = false; + + lockdown + .start_session(&provider.get_pairing_file().await?) + .await?; + + let (port, ssl) = lockdown.start_service(obf!("com.apple.afc2")).await?; + + let mut idevice = provider.connect(port).await?; + if ssl { + idevice + .start_session(&provider.get_pairing_file().await?, legacy) + .await?; + } + + Self::from_stream(idevice).await + } + /// Lists the contents of a directory on the device /// /// # Arguments From 602e1ba855eba0544ab584851cb10f488330282f Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Mon, 5 Jan 2026 07:23:55 -0700 Subject: [PATCH 15/34] Replace off_t with Windows-allowed value in AFC FFI --- ffi/src/afc.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ffi/src/afc.rs b/ffi/src/afc.rs index 71ae9af..6c70436 100644 --- a/ffi/src/afc.rs +++ b/ffi/src/afc.rs @@ -707,7 +707,7 @@ pub unsafe extern "C" fn afc_file_seek( handle: *mut AfcFileHandle, offset: i64, whence: libc::c_int, - new_pos: *mut libc::off_t, + new_pos: *mut i64, ) -> *mut IdeviceFfiError { if handle.is_null() || new_pos.is_null() { return ffi_err!(IdeviceError::FfiInvalidArg); @@ -727,7 +727,7 @@ pub unsafe extern "C" fn afc_file_seek( match res { Ok(pos) => { unsafe { - *new_pos = pos as libc::off_t; + *new_pos = pos as i64; } null_mut() } @@ -753,7 +753,7 @@ pub unsafe extern "C" fn afc_file_seek( #[unsafe(no_mangle)] pub unsafe extern "C" fn afc_file_tell( handle: *mut AfcFileHandle, - pos: *mut libc::off_t, + pos: *mut i64, ) -> *mut IdeviceFfiError { if handle.is_null() || pos.is_null() { return ffi_err!(IdeviceError::FfiInvalidArg); @@ -767,7 +767,7 @@ pub unsafe extern "C" fn afc_file_tell( match res { Ok(cur) => { unsafe { - *pos = cur as libc::off_t; + *pos = cur as i64; } null_mut() } From 6dcfd4bc4c241bce34a04b9c3d4a659a11c2b144 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Mon, 5 Jan 2026 07:57:46 -0700 Subject: [PATCH 16/34] Move domain to flag of lockdown CLI --- tools/src/lockdown.rs | 35 +++++++++++++---------------------- 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/tools/src/lockdown.rs b/tools/src/lockdown.rs index 7e60224..3ad0e8b 100644 --- a/tools/src/lockdown.rs +++ b/tools/src/lockdown.rs @@ -1,7 +1,7 @@ // Jackson Coxson use idevice::{IdeviceService, lockdown::LockdownClient, provider::IdeviceProvider}; -use jkcli::{CollectedArguments, JkArgument, JkCommand}; +use jkcli::{CollectedArguments, JkArgument, JkCommand, JkFlag}; use plist::Value; use plist_macro::pretty_print_plist; @@ -12,8 +12,7 @@ pub fn register() -> JkCommand { "get", JkCommand::new() .help("Gets a value from lockdown") - .with_argument(JkArgument::new().with_help("The value to get")) - .with_argument(JkArgument::new().with_help("The domain to get in")), + .with_argument(JkArgument::new().with_help("The value to get")), ) .with_subcommand( "set", @@ -28,8 +27,12 @@ pub fn register() -> JkCommand { JkArgument::new() .with_help("The value key to set") .required(true), - ) - .with_argument(JkArgument::new().with_help("The domain to set in")), + ), + ) + .with_flag( + JkFlag::new("domain") + .with_help("The domain to set/get in") + .with_argument(JkArgument::new().required(true)), ) .subcommand_required(true) } @@ -47,10 +50,12 @@ pub async fn main(arguments: &CollectedArguments, provider: Box = sub_args.get_flag("domain"); + let domain = domain.as_deref(); + match sub_name.as_str() { "get" => { let key: Option = sub_args.next_argument(); - let domain: Option = sub_args.next_argument(); match lockdown_client .get_value( @@ -58,10 +63,7 @@ pub async fn main(arguments: &CollectedArguments, provider: Box Some(k.as_str()), None => None, }, - match &domain { - Some(d) => Some(d.as_str()), - None => None, - }, + domain, ) .await { @@ -76,21 +78,10 @@ pub async fn main(arguments: &CollectedArguments, provider: Box { let value_str: String = sub_args.next_argument().unwrap(); let key: String = sub_args.next_argument().unwrap(); - let domain: Option = sub_args.next_argument(); let value = Value::String(value_str.clone()); - match lockdown_client - .set_value( - key, - value, - match &domain { - Some(d) => Some(d.as_str()), - None => None, - }, - ) - .await - { + match lockdown_client.set_value(key, value, domain).await { Ok(()) => println!("Successfully set"), Err(e) => eprintln!("Error setting value: {e}"), } From a4e17ea076b0a02e570876a4af4773d3d9463695 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Mon, 5 Jan 2026 08:08:11 -0700 Subject: [PATCH 17/34] Poll correct sub argument tree for domain flag in lockdown CLI --- tools/src/lockdown.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/src/lockdown.rs b/tools/src/lockdown.rs index 3ad0e8b..3d0af6d 100644 --- a/tools/src/lockdown.rs +++ b/tools/src/lockdown.rs @@ -47,12 +47,12 @@ pub async fn main(arguments: &CollectedArguments, provider: Box = arguments.get_flag("domain"); + let domain = domain.as_deref(); + let (sub_name, sub_args) = arguments.first_subcommand().expect("No subcommand"); let mut sub_args = sub_args.clone(); - let domain: Option = sub_args.get_flag("domain"); - let domain = domain.as_deref(); - match sub_name.as_str() { "get" => { let key: Option = sub_args.next_argument(); From bb64dc0b1c52be6ffa9985166a3135887d8a5819 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Mon, 5 Jan 2026 12:00:11 -0700 Subject: [PATCH 18/34] Implement lockdown enter recovery --- ffi/src/lockdown.rs | 31 ++++++++++++++++++++++++++++++- idevice/src/services/lockdown.rs | 17 +++++++++++++++++ tools/src/lockdown.rs | 8 ++++++++ 3 files changed, 55 insertions(+), 1 deletion(-) diff --git a/ffi/src/lockdown.rs b/ffi/src/lockdown.rs index 0912797..046dfa2 100644 --- a/ffi/src/lockdown.rs +++ b/ffi/src/lockdown.rs @@ -179,7 +179,7 @@ pub unsafe extern "C" fn lockdownd_get_value( domain: *const libc::c_char, out_plist: *mut plist_t, ) -> *mut IdeviceFfiError { - if out_plist.is_null() { + if client.is_null() || out_plist.is_null() { return ffi_err!(IdeviceError::FfiInvalidArg); } @@ -221,6 +221,35 @@ pub unsafe extern "C" fn lockdownd_get_value( } } +/// Tells the device to enter recovery mode +/// +/// # Arguments +/// * `client` - A valid LockdowndClient handle +/// +/// # Returns +/// An IdeviceFfiError on error, null on success +/// +/// # Safety +/// `client` must be a valid pointer to a handle allocated by this library +#[unsafe(no_mangle)] +pub unsafe extern "C" fn lockdownd_enter_recovery( + client: *mut LockdowndClientHandle, +) -> *mut IdeviceFfiError { + if client.is_null() { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + + let res: Result<(), IdeviceError> = run_sync_local(async move { + let client_ref = unsafe { &mut (*client).0 }; + client_ref.enter_recovery().await + }); + + match res { + Ok(_) => null_mut(), + Err(e) => ffi_err!(e), + } +} + /// Frees a LockdowndClient handle /// /// # Arguments diff --git a/idevice/src/services/lockdown.rs b/idevice/src/services/lockdown.rs index a6a4ea5..34d88b9 100644 --- a/idevice/src/services/lockdown.rs +++ b/idevice/src/services/lockdown.rs @@ -326,6 +326,23 @@ impl LockdownClient { } } } + + /// Tell the device to enter recovery mode + pub async fn enter_recovery(&mut self) -> Result<(), IdeviceError> { + self.idevice + .send_plist(crate::plist!({ + "Request": "EnterRecovery" + })) + .await?; + + let res = self.idevice.read_plist().await?; + + if res.get("Request").and_then(|x| x.as_string()) == Some("EnterRecovery") { + Ok(()) + } else { + Err(IdeviceError::UnexpectedResponse) + } + } } impl From for LockdownClient { diff --git a/tools/src/lockdown.rs b/tools/src/lockdown.rs index 3d0af6d..42eb87c 100644 --- a/tools/src/lockdown.rs +++ b/tools/src/lockdown.rs @@ -29,6 +29,10 @@ pub fn register() -> JkCommand { .required(true), ), ) + .with_subcommand( + "recovery", + JkCommand::new().help("Tell the device to enter recovery mode"), + ) .with_flag( JkFlag::new("domain") .with_help("The domain to set/get in") @@ -86,6 +90,10 @@ pub async fn main(arguments: &CollectedArguments, provider: Box eprintln!("Error setting value: {e}"), } } + "recovery" => lockdown_client + .enter_recovery() + .await + .expect("Failed to enter recovery"), _ => unreachable!(), } } From 1db78e6a8d3b247183da91ae68b4e43379957339 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Wed, 14 Jan 2026 08:14:09 -0700 Subject: [PATCH 19/34] Re-add iOS checks to CI --- .github/workflows/ci.yml | 12 +++++++++++- justfile | 3 ++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e4dc6bc..dcee207 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,8 +30,12 @@ jobs: - name: Install rustup targets run: | + rustup target add aarch64-apple-ios && \ + rustup target add x86_64-apple-ios && \ + rustup target add aarch64-apple-ios-sim && \ rustup target add aarch64-apple-darwin && \ - rustup target add x86_64-apple-darwin && cargo install --force --locked bindgen-cli + rustup target add x86_64-apple-darwin && \ + cargo install --force --locked bindgen-cli - name: Build all Apple targets and examples/tools run: | @@ -44,6 +48,12 @@ jobs: path: | target/*apple*/release/libidevice_ffi.a + - name: Upload macOS+iOS XCFramework + uses: actions/upload-artifact@v4 + with: + name: idevice-xcframework + path: swift/bundle.zip + - name: Upload C examples/tools uses: actions/upload-artifact@v4 with: diff --git a/justfile b/justfile index 9e5d593..1166802 100644 --- a/justfile +++ b/justfile @@ -6,7 +6,7 @@ check-features: ci-check: build-ffi-native build-tools-native build-cpp build-c cargo clippy --all-targets --all-features -- -D warnings cargo fmt -- --check -macos-ci-check: ci-check +macos-ci-check: ci-check xcframework cd tools && cargo build --release --target x86_64-apple-darwin windows-ci-check: build-ffi-native build-tools-native build-cpp @@ -57,6 +57,7 @@ xcframework: apple-build apple-build: # requires a Mac # iOS device build BINDGEN_EXTRA_CLANG_ARGS="--sysroot=$(xcrun --sdk iphoneos --show-sdk-path)" \ + IPHONEOS_DEPLOYMENT_TARGET=17.0 \ cargo build --release --target aarch64-apple-ios --features obfuscate # iOS Simulator (arm64) From f44a5c07799e16a36cf3b4ea36d0d8aa7c02e29a Mon Sep 17 00:00:00 2001 From: SAMSAM Date: Wed, 14 Jan 2026 12:01:04 -0800 Subject: [PATCH 20/34] fix: fixes misagent cmd + adds new install subcmd (#59) Calling init twice is a bad idea and causes the program to crash, also adds an install command for provisioning profile paths --- tools/src/misagent.rs | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/tools/src/misagent.rs b/tools/src/misagent.rs index 39c1971..f535908 100644 --- a/tools/src/misagent.rs +++ b/tools/src/misagent.rs @@ -28,12 +28,20 @@ pub fn register() -> JkCommand { .required(true), ), ) + .with_subcommand( + "install", + JkCommand::new() + .help("Install a provisioning profile on the device") + .with_argument( + JkArgument::new() + .with_help("Path to the provisioning profile to install") + .required(true), + ), + ) .subcommand_required(true) } pub async fn main(arguments: &CollectedArguments, provider: Box) { - tracing_subscriber::fmt::init(); - let mut misagent_client = MisagentClient::connect(&*provider) .await .expect("Unable to connect to misagent"); @@ -67,6 +75,16 @@ pub async fn main(arguments: &CollectedArguments, provider: Box { + let path = sub_args + .next_argument::() + .expect("No profile path passed"); + let profile = tokio::fs::read(path).await.expect("Unable to read profile"); + misagent_client + .install(profile) + .await + .expect("Failed to install profile"); + } _ => unreachable!(), } } From ead5fbf3d3d73a932b04545163793708d5596448 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Wed, 14 Jan 2026 13:39:07 -0700 Subject: [PATCH 21/34] Add house arrest bindings --- ffi/src/house_arrest.rs | 183 ++++++++++++++++++++++++++++++++++++++++ ffi/src/lib.rs | 2 + 2 files changed, 185 insertions(+) create mode 100644 ffi/src/house_arrest.rs diff --git a/ffi/src/house_arrest.rs b/ffi/src/house_arrest.rs new file mode 100644 index 0000000..d4ab0b3 --- /dev/null +++ b/ffi/src/house_arrest.rs @@ -0,0 +1,183 @@ +// Jackson Coxson + +use std::{ + ffi::{CStr, c_char}, + ptr::null_mut, +}; + +use idevice::{ + IdeviceError, IdeviceService, afc::AfcClient, house_arrest::HouseArrestClient, + provider::IdeviceProvider, +}; + +use crate::{ + IdeviceFfiError, IdeviceHandle, afc::AfcClientHandle, ffi_err, provider::IdeviceProviderHandle, + run_sync_local, +}; + +pub struct HouseArrestClientHandle(pub HouseArrestClient); + +/// Connects to the House Arrest service using a TCP provider +/// +/// # Arguments +/// * [`provider`] - An IdeviceProvider +/// * [`client`] - On success, will be set to point to a newly allocated HouseArrestClient handle +/// +/// # Returns +/// An IdeviceFfiError on error, null on success +/// +/// # Safety +/// `provider` must be a valid pointer to a handle allocated by this library +/// `client` must be a valid, non-null pointer to a location where the handle will be stored +#[unsafe(no_mangle)] +pub unsafe extern "C" fn house_arrest_client_connect( + provider: *mut IdeviceProviderHandle, + client: *mut *mut HouseArrestClientHandle, +) -> *mut IdeviceFfiError { + if provider.is_null() || client.is_null() { + tracing::error!("Null pointer provided"); + return ffi_err!(IdeviceError::FfiInvalidArg); + } + + let res = run_sync_local(async { + let provider_ref: &dyn IdeviceProvider = unsafe { &*(*provider).0 }; + + HouseArrestClient::connect(provider_ref).await + }); + + match res { + Ok(r) => { + let boxed = Box::new(HouseArrestClientHandle(r)); + unsafe { *client = Box::into_raw(boxed) }; + null_mut() + } + Err(e) => ffi_err!(e), + } +} + +/// Creates a new HouseArrestClient from an existing Idevice connection +/// +/// # Arguments +/// * [`socket`] - An IdeviceSocket handle +/// * [`client`] - On success, will be set to point to a newly allocated HouseArrestClient handle +/// +/// # Returns +/// An IdeviceFfiError on error, null on success +/// +/// # Safety +/// `socket` must be a valid pointer to a handle allocated by this library +/// `client` must be a valid, non-null pointer to a location where the handle will be stored +#[unsafe(no_mangle)] +pub unsafe extern "C" fn house_arrest_client_new( + socket: *mut IdeviceHandle, + client: *mut *mut HouseArrestClientHandle, +) -> *mut IdeviceFfiError { + if socket.is_null() || client.is_null() { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + let socket = unsafe { Box::from_raw(socket) }.0; + let r = HouseArrestClient::new(socket); + let boxed = Box::new(HouseArrestClientHandle(r)); + unsafe { *client = Box::into_raw(boxed) }; + null_mut() +} + +/// Vends a container for an app +/// +/// # Arguments +/// * [`client`] - The House Arrest client +/// * [`bundle_id`] - The bundle ID to vend for +/// * [`afc_client`] - The new AFC client for the underlying connection +/// +/// # Returns +/// An IdeviceFfiError on error, null on success +/// +/// # Safety +/// `client` must be a allocated by this library +/// `bundle_id` must be a NULL-terminated string +/// `afc_client` must be a valid, non-null pointer where the new AFC client will be stored +#[unsafe(no_mangle)] +pub unsafe extern "C" fn house_arrest_vend_container( + client: *mut HouseArrestClientHandle, + bundle_id: *const c_char, + afc_client: *mut *mut AfcClientHandle, +) -> *mut IdeviceFfiError { + if client.is_null() || bundle_id.is_null() || afc_client.is_null() { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + + let bundle_id = unsafe { CStr::from_ptr(bundle_id) }.to_string_lossy(); + let client_ref = unsafe { Box::from_raw(client) }.0; // take ownership and drop + + let res: Result = + run_sync_local(async move { client_ref.vend_container(bundle_id).await }); + + match res { + Ok(a) => { + let a = Box::into_raw(Box::new(AfcClientHandle(a))); + unsafe { *afc_client = a }; + null_mut() + } + Err(e) => { + ffi_err!(e) + } + } +} + +/// Vends documents for an app +/// +/// # Arguments +/// * [`client`] - The House Arrest client +/// * [`bundle_id`] - The bundle ID to vend for +/// * [`afc_client`] - The new AFC client for the underlying connection +/// +/// # Returns +/// An IdeviceFfiError on error, null on success +/// +/// # Safety +/// `client` must be a allocated by this library +/// `bundle_id` must be a NULL-terminated string +/// `afc_client` must be a valid, non-null pointer where the new AFC client will be stored +#[unsafe(no_mangle)] +pub unsafe extern "C" fn house_arrest_vend_documents( + client: *mut HouseArrestClientHandle, + bundle_id: *const c_char, + afc_client: *mut *mut AfcClientHandle, +) -> *mut IdeviceFfiError { + if client.is_null() || bundle_id.is_null() || afc_client.is_null() { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + + let bundle_id = unsafe { CStr::from_ptr(bundle_id) }.to_string_lossy(); + let client_ref = unsafe { Box::from_raw(client) }.0; // take ownership and drop + + let res: Result = + run_sync_local(async move { client_ref.vend_documents(bundle_id).await }); + + match res { + Ok(a) => { + let a = Box::into_raw(Box::new(AfcClientHandle(a))); + unsafe { *afc_client = a }; + null_mut() + } + Err(e) => { + ffi_err!(e) + } + } +} + +/// Frees an HouseArrestClient handle +/// +/// # Arguments +/// * [`handle`] - The handle to free +/// +/// # Safety +/// `handle` must be a valid pointer to the handle that was allocated by this library, +/// or NULL (in which case this function does nothing) +#[unsafe(no_mangle)] +pub unsafe extern "C" fn house_arrest_client_free(handle: *mut HouseArrestClientHandle) { + if !handle.is_null() { + tracing::debug!("Freeing house_arrest_client"); + let _ = unsafe { Box::from_raw(handle) }; + } +} diff --git a/ffi/src/lib.rs b/ffi/src/lib.rs index d237ed5..b65c8b1 100644 --- a/ffi/src/lib.rs +++ b/ffi/src/lib.rs @@ -19,6 +19,8 @@ pub mod dvt; mod errors; #[cfg(feature = "heartbeat")] pub mod heartbeat; +#[cfg(feature = "house_arrest")] +pub mod house_arrest; #[cfg(feature = "installation_proxy")] pub mod installation_proxy; pub mod lockdown; From 5bb9330cf6581bfbc986c611c0ac2fa6a43a84dc Mon Sep 17 00:00:00 2001 From: uncor3 Date: Wed, 14 Jan 2026 13:46:47 -0800 Subject: [PATCH 22/34] add diag relay c++ bindings, screenshotr ffi (#58) --- cpp/include/idevice++/diagnostics_relay.hpp | 60 ++++++++ cpp/src/diagnostics_relay.cpp | 159 ++++++++++++++++++++ ffi/Cargo.toml | 2 + ffi/src/afc.rs | 4 +- ffi/src/diagnostics_relay.rs | 2 +- ffi/src/lib.rs | 2 + ffi/src/lockdown.rs | 54 +++++++ ffi/src/screenshotr.rs | 132 ++++++++++++++++ 8 files changed, 412 insertions(+), 3 deletions(-) create mode 100644 cpp/include/idevice++/diagnostics_relay.hpp create mode 100644 cpp/src/diagnostics_relay.cpp create mode 100644 ffi/src/screenshotr.rs diff --git a/cpp/include/idevice++/diagnostics_relay.hpp b/cpp/include/idevice++/diagnostics_relay.hpp new file mode 100644 index 0000000..77a1bda --- /dev/null +++ b/cpp/include/idevice++/diagnostics_relay.hpp @@ -0,0 +1,60 @@ +// Jackson Coxson + +#pragma once +#include +#include +#include +#include +#include +#include + +namespace IdeviceFFI { + +using DiagnosticsRelayPtr = + std::unique_ptr>; + +class DiagnosticsRelay { + public: + // Factory: connect via Provider + static Result connect(Provider& provider); + + // Factory: wrap an existing Idevice socket (consumes it on success) + static Result from_socket(Idevice&& socket); + + // API Methods - queries returning optional plist + Result, FfiError> ioregistry(Option current_plane, + Option entry_name, + Option entry_class) const; + + Result, FfiError> mobilegestalt(Option> keys) const; + + Result, FfiError> gasguage() const; + Result, FfiError> nand() const; + Result, FfiError> all() const; + Result, FfiError> wifi() const; + + // API Methods - actions + Result restart(); + Result shutdown(); + Result sleep(); + Result goodbye(); + + // RAII / moves + ~DiagnosticsRelay() noexcept = default; + DiagnosticsRelay(DiagnosticsRelay&&) noexcept = default; + DiagnosticsRelay& operator=(DiagnosticsRelay&&) noexcept = default; + DiagnosticsRelay(const DiagnosticsRelay&) = delete; + DiagnosticsRelay& operator=(const DiagnosticsRelay&) = delete; + + DiagnosticsRelayClientHandle* raw() const noexcept { return handle_.get(); } + static DiagnosticsRelay adopt(DiagnosticsRelayClientHandle* h) noexcept { + return DiagnosticsRelay(h); + } + + private: + explicit DiagnosticsRelay(DiagnosticsRelayClientHandle* h) noexcept : handle_(h) {} + DiagnosticsRelayPtr handle_{}; +}; + +} // namespace IdeviceFFI \ No newline at end of file diff --git a/cpp/src/diagnostics_relay.cpp b/cpp/src/diagnostics_relay.cpp new file mode 100644 index 0000000..398c923 --- /dev/null +++ b/cpp/src/diagnostics_relay.cpp @@ -0,0 +1,159 @@ +// Jackson Coxson + +#include +#include +#include +#include + +namespace IdeviceFFI { + +// -------- Factory Methods -------- + +Result DiagnosticsRelay::connect(Provider& provider) { + DiagnosticsRelayClientHandle* out = nullptr; + FfiError e(::diagnostics_relay_client_connect(provider.raw(), &out)); + if (e) { + return Err(e); + } + return Ok(DiagnosticsRelay::adopt(out)); +} + +Result DiagnosticsRelay::from_socket(Idevice&& socket) { + DiagnosticsRelayClientHandle* out = nullptr; + FfiError e(::diagnostics_relay_client_new(socket.raw(), &out)); + if (e) { + return Err(e); + } + socket.release(); + return Ok(DiagnosticsRelay::adopt(out)); +} + +// -------- API Methods -------- + +Result, FfiError> +DiagnosticsRelay::ioregistry(Option current_plane, + Option entry_name, + Option entry_class) const { + plist_t res = nullptr; + + const char* plane_ptr = current_plane.is_some() ? current_plane.unwrap().c_str() : nullptr; + const char* name_ptr = entry_name.is_some() ? entry_name.unwrap().c_str() : nullptr; + const char* class_ptr = entry_class.is_some() ? entry_class.unwrap().c_str() : nullptr; + + FfiError e( + ::diagnostics_relay_client_ioregistry(handle_.get(), plane_ptr, name_ptr, class_ptr, &res)); + if (e) { + return Err(e); + } + + if (res == nullptr) { + return Ok(Option(None)); + } + return Ok(Some(res)); +} + +Result, FfiError> +DiagnosticsRelay::mobilegestalt(Option> keys) const { + plist_t res = nullptr; + + if (!keys.is_some() || keys.unwrap().empty()) { + return Err(FfiError::InvalidArgument()); + } + + FfiError e(::diagnostics_relay_client_mobilegestalt( + handle_.get(), keys.unwrap().data(), keys.unwrap().size(), &res)); + if (e) { + return Err(e); + } + + if (res == nullptr) { + return Ok(Option(None)); + } + return Ok(Some(res)); +} + +Result, FfiError> DiagnosticsRelay::gasguage() const { + plist_t res = nullptr; + FfiError e(::diagnostics_relay_client_gasguage(handle_.get(), &res)); + if (e) { + return Err(e); + } + + if (res == nullptr) { + return Ok(Option(None)); + } + return Ok(Some(res)); +} + +Result, FfiError> DiagnosticsRelay::nand() const { + plist_t res = nullptr; + FfiError e(::diagnostics_relay_client_nand(handle_.get(), &res)); + if (e) { + return Err(e); + } + + if (res == nullptr) { + return Ok(Option(None)); + } + return Ok(Some(res)); +} + +Result, FfiError> DiagnosticsRelay::all() const { + plist_t res = nullptr; + FfiError e(::diagnostics_relay_client_all(handle_.get(), &res)); + if (e) { + return Err(e); + } + + if (res == nullptr) { + return Ok(Option(None)); + } + return Ok(Some(res)); +} + +Result, FfiError> DiagnosticsRelay::wifi() const { + plist_t res = nullptr; + FfiError e(::diagnostics_relay_client_wifi(handle_.get(), &res)); + if (e) { + return Err(e); + } + + if (res == nullptr) { + return Ok(Option(None)); + } + return Ok(Some(res)); +} + +Result DiagnosticsRelay::restart() { + FfiError e(::diagnostics_relay_client_restart(handle_.get())); + if (e) { + return Err(e); + } + return Ok(); +} + +Result DiagnosticsRelay::shutdown() { + FfiError e(::diagnostics_relay_client_shutdown(handle_.get())); + if (e) { + return Err(e); + } + return Ok(); +} + +Result DiagnosticsRelay::sleep() { + FfiError e(::diagnostics_relay_client_sleep(handle_.get())); + if (e) { + return Err(e); + } + return Ok(); +} + +Result DiagnosticsRelay::goodbye() { + FfiError e(::diagnostics_relay_client_goodbye(handle_.get())); + if (e) { + return Err(e); + } + return Ok(); +} + +} // namespace IdeviceFFI \ No newline at end of file diff --git a/ffi/Cargo.toml b/ffi/Cargo.toml index 13c2b36..4e3d8f7 100644 --- a/ffi/Cargo.toml +++ b/ffi/Cargo.toml @@ -50,6 +50,7 @@ tss = ["idevice/tss"] tunneld = ["idevice/tunneld"] usbmuxd = ["idevice/usbmuxd"] xpc = ["idevice/xpc"] +screenshotr = ["idevice/screenshotr"] full = [ "afc", "amfi", @@ -75,6 +76,7 @@ full = [ "tunneld", "springboardservices", "syslog_relay", + "screenshotr", ] default = ["full", "aws-lc"] diff --git a/ffi/src/afc.rs b/ffi/src/afc.rs index 6c70436..19988da 100644 --- a/ffi/src/afc.rs +++ b/ffi/src/afc.rs @@ -620,10 +620,10 @@ pub unsafe extern "C" fn afc_file_read( let fd = unsafe { &mut *(handle as *mut FileDescriptor) }; let res: Result, IdeviceError> = run_sync({ - let mut buf = Vec::with_capacity(len); + let mut buf = vec![0u8; len]; async move { let r = fd.read(&mut buf).await?; - buf.resize(r, 0); + buf.truncate(r); Ok(buf) } }); diff --git a/ffi/src/diagnostics_relay.rs b/ffi/src/diagnostics_relay.rs index fb65a11..66e2c83 100644 --- a/ffi/src/diagnostics_relay.rs +++ b/ffi/src/diagnostics_relay.rs @@ -190,7 +190,7 @@ pub unsafe extern "C" fn diagnostics_relay_client_mobilegestalt( return ffi_err!(IdeviceError::FfiInvalidArg); } - let keys = if keys.is_null() { + let keys = if !keys.is_null() { let keys = unsafe { std::slice::from_raw_parts(keys, keys_len) }; Some( keys.iter() diff --git a/ffi/src/lib.rs b/ffi/src/lib.rs index b65c8b1..31e0147 100644 --- a/ffi/src/lib.rs +++ b/ffi/src/lib.rs @@ -35,6 +35,8 @@ mod pairing_file; pub mod provider; #[cfg(feature = "xpc")] pub mod rsd; +#[cfg(feature = "screenshotr")] +pub mod screenshotr; #[cfg(feature = "springboardservices")] pub mod springboardservices; #[cfg(feature = "syslog_relay")] diff --git a/ffi/src/lockdown.rs b/ffi/src/lockdown.rs index 046dfa2..77b7717 100644 --- a/ffi/src/lockdown.rs +++ b/ffi/src/lockdown.rs @@ -250,6 +250,60 @@ pub unsafe extern "C" fn lockdownd_enter_recovery( } } +/// Sets a value in lockdownd +/// +/// # Arguments +/// * `client` - A valid LockdowndClient handle +/// * `key` - The key to set (null-terminated string) +/// * `value` - The value to set as a plist +/// * `domain` - The domain to set in (null-terminated string, optional) +/// +/// # Returns +/// An IdeviceFfiError on error, null on success +/// +/// # Safety +/// `client` must be a valid pointer to a handle allocated by this library +/// `key` must be a valid null-terminated string +/// `value` must be a valid plist +/// `domain` must be a valid null-terminated string or NULL +#[unsafe(no_mangle)] +pub unsafe extern "C" fn lockdownd_set_value( + client: *mut LockdowndClientHandle, + key: *const libc::c_char, + value: plist_t, + domain: *const libc::c_char, +) -> *mut IdeviceFfiError { + if client.is_null() || key.is_null() || value.is_null() { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + + let key = match unsafe { std::ffi::CStr::from_ptr(key) }.to_str() { + Ok(k) => k, + Err(_) => return ffi_err!(IdeviceError::InvalidCString), + }; + + let domain = if domain.is_null() { + None + } else { + Some(match unsafe { std::ffi::CStr::from_ptr(domain) }.to_str() { + Ok(d) => d, + Err(_) => return ffi_err!(IdeviceError::InvalidCString), + }) + }; + + let value = unsafe { &mut *value }.borrow_self().clone(); + + let res: Result<(), IdeviceError> = run_sync_local(async move { + let client_ref = unsafe { &mut (*client).0 }; + client_ref.set_value(key, value, domain).await + }); + + match res { + Ok(_) => null_mut(), + Err(e) => ffi_err!(e), + } +} + /// Frees a LockdowndClient handle /// /// # Arguments diff --git a/ffi/src/screenshotr.rs b/ffi/src/screenshotr.rs new file mode 100644 index 0000000..f6b52aa --- /dev/null +++ b/ffi/src/screenshotr.rs @@ -0,0 +1,132 @@ +// Jackson Coxson + +use std::ptr::null_mut; + +use idevice::{ + IdeviceError, IdeviceService, provider::IdeviceProvider, + services::screenshotr::ScreenshotService, +}; + +use crate::{IdeviceFfiError, ffi_err, provider::IdeviceProviderHandle, run_sync_local}; + +pub struct ScreenshotrClientHandle(pub ScreenshotService); + +/// Represents a screenshot data buffer +#[repr(C)] +pub struct ScreenshotData { + pub data: *mut u8, + pub length: usize, +} + +/// Connects to screenshotr service using provider +/// +/// # Arguments +/// * [`provider`] - An IdeviceProvider +/// * [`client`] - On success, will be set to point to a newly allocated ScreenshotrClient handle +/// +/// # Returns +/// An IdeviceFfiError on error, null on success +/// +/// # Safety +/// `provider` must be a valid pointer to a handle allocated by this library +/// `client` must be a valid, non-null pointer to a location where the handle will be stored +#[unsafe(no_mangle)] +pub unsafe extern "C" fn screenshotr_connect( + provider: *mut IdeviceProviderHandle, + client: *mut *mut ScreenshotrClientHandle, +) -> *mut IdeviceFfiError { + if provider.is_null() || client.is_null() { + tracing::error!("Null pointer provided"); + return ffi_err!(IdeviceError::FfiInvalidArg); + } + + let res: Result = run_sync_local(async move { + let provider_ref: &dyn IdeviceProvider = unsafe { &*(*provider).0 }; + ScreenshotService::connect(provider_ref).await + }); + + match res { + Ok(r) => { + let boxed = Box::new(ScreenshotrClientHandle(r)); + unsafe { *client = Box::into_raw(boxed) }; + null_mut() + } + Err(e) => ffi_err!(e), + } +} + +/// Takes a screenshot from the device +/// +/// # Arguments +/// * `client` - A valid ScreenshotrClient handle +/// * `screenshot` - Pointer to store the screenshot data +/// +/// # Returns +/// An IdeviceFfiError on error, null on success +/// +/// # Safety +/// `client` must be a valid pointer to a handle allocated by this library +/// `screenshot` must be a valid pointer to store the screenshot data +/// The caller is responsible for freeing the screenshot data using screenshotr_screenshot_free +#[unsafe(no_mangle)] +pub unsafe extern "C" fn screenshotr_take_screenshot( + client: *mut ScreenshotrClientHandle, + screenshot: *mut ScreenshotData, +) -> *mut IdeviceFfiError { + if client.is_null() || screenshot.is_null() { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + + let res: Result, IdeviceError> = run_sync_local(async move { + let client_ref = unsafe { &mut (*client).0 }; + client_ref.take_screenshot().await + }); + + match res { + Ok(data) => { + let len = data.len(); + let boxed = data.into_boxed_slice(); + let ptr = Box::into_raw(boxed) as *mut u8; + + unsafe { + (*screenshot).data = ptr; + (*screenshot).length = len; + } + null_mut() + } + Err(e) => ffi_err!(e), + } +} + +/// Frees screenshot data +/// +/// # Arguments +/// * `screenshot` - The screenshot data to free +/// +/// # Safety +/// `screenshot` must be a valid ScreenshotData that was allocated by screenshotr_take_screenshot +/// or NULL (in which case this function does nothing) +#[unsafe(no_mangle)] +pub unsafe extern "C" fn screenshotr_screenshot_free(screenshot: ScreenshotData) { + if !screenshot.data.is_null() && screenshot.length > 0 { + tracing::debug!("Freeing screenshot data"); + let _ = + unsafe { Vec::from_raw_parts(screenshot.data, screenshot.length, screenshot.length) }; + } +} + +/// Frees a ScreenshotrClient handle +/// +/// # Arguments +/// * [`handle`] - The handle to free +/// +/// # Safety +/// `handle` must be a valid pointer to the handle that was allocated by this library, +/// or NULL (in which case this function does nothing) +#[unsafe(no_mangle)] +pub unsafe extern "C" fn screenshotr_client_free(handle: *mut ScreenshotrClientHandle) { + if !handle.is_null() { + tracing::debug!("Freeing screenshotr_client"); + let _ = unsafe { Box::from_raw(handle) }; + } +} From 142708c2891e7881a895c2e88538152d0c2bb976 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Mon, 19 Jan 2026 16:02:13 -0700 Subject: [PATCH 23/34] Add a no-session flag to lockdown CLI --- tools/src/lockdown.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tools/src/lockdown.rs b/tools/src/lockdown.rs index 42eb87c..ea38c1c 100644 --- a/tools/src/lockdown.rs +++ b/tools/src/lockdown.rs @@ -38,6 +38,7 @@ pub fn register() -> JkCommand { .with_help("The domain to set/get in") .with_argument(JkArgument::new().required(true)), ) + .with_flag(JkFlag::new("no-session").with_help("Don't start a TLS session")) .subcommand_required(true) } @@ -46,10 +47,12 @@ pub async fn main(arguments: &CollectedArguments, provider: Box = arguments.get_flag("domain"); let domain = domain.as_deref(); From 9a71279fe958626e028a791cb2d2099a765a12fe Mon Sep 17 00:00:00 2001 From: fulln Date: Fri, 23 Jan 2026 06:32:01 +0800 Subject: [PATCH 24/34] feat(springboard): add get_icon_state and set_icon_state methods (#63) * feat(springboard): add get_icon_state method for reading home screen layout Add get_icon_state() method to SpringBoardServicesClient that retrieves the current home screen icon layout from iOS devices. Features: - Read complete home screen layout including icon positions and folders - Support for optional formatVersion parameter - Works on all iOS versions (tested on iOS 18.7.3) - Comprehensive documentation with usage examples Note: This PR intentionally does NOT include set_icon_state() as that functionality is non-operational on iOS 18+ (see issue #62 for details). Tested on: - Device: iPhone 16,2 (iPhone 15 Pro) - iOS: 18.7.3 (Build 22H217) * feat(springboard): add set_icon_state method with date precision fix - Implement set_icon_state() to modify home screen layout - Implement set_icon_state_with_version() with format_version parameter - Add truncate_dates_to_seconds() to convert nanosecond precision dates to second precision - Fix iOS compatibility issue where high-precision dates were rejected - Successfully tested on iOS 18.7.3 (previously believed to be restricted) - Follows pymobiledevice3 implementation pattern * refactor(utils): extract truncate_dates_to_seconds to utils::plist module - Move date truncation logic from springboardservices to reusable utils::plist module - Add comprehensive unit tests for date truncation functionality - Add public API documentation for the utility function - This makes the date normalization logic available for other services that may need it * perf(springboard): normalize dates on read instead of write - Move date truncation from set_icon_state to get_icon_state - Eliminates unnecessary clone() operation in set_icon_state - Better performance when setting icon state multiple times - Cleaner API: data from get_icon_state is directly usable in set_icon_state - Users don't need to worry about date precision issues * refactor(springboard): address PR feedback - use Option<&str> and add error validation - Change format_version parameter from Option to Option<&str> for consistency - Remove outdated iOS 18+ restriction comments since setIconState works on iOS 18+ - Add error validation to get_icon_state method similar to get_icon_pngdata - Update documentation to reflect accurate iOS compatibility * Fix cargo clippy warnings * Fix clippy warnings in plist.rs * Add springboard CLI commands --------- Co-authored-by: Jackson Coxson --- Cargo.lock | 4 +- idevice/Cargo.toml | 2 +- idevice/src/services/springboardservices.rs | 130 +++++++++++++++- idevice/src/utils/mod.rs | 2 + idevice/src/utils/plist.rs | 155 ++++++++++++++++++++ tools/Cargo.toml | 2 +- tools/src/main.rs | 5 + tools/src/springboardservices.rs | 76 ++++++++++ 8 files changed, 371 insertions(+), 5 deletions(-) create mode 100644 idevice/src/utils/plist.rs create mode 100644 tools/src/springboardservices.rs diff --git a/Cargo.lock b/Cargo.lock index 4944606..4bee460 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1782,9 +1782,9 @@ dependencies = [ [[package]] name = "plist-macro" -version = "0.1.0" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb72007326fe20721ef27304fcf2d1bd5877b92d13dbd8df735fd33407e31c2a" +checksum = "8888e02e251eba3258cc58fb79f0d8675c34b3428749e738562d58a0271bf035" dependencies = [ "plist", ] diff --git a/idevice/Cargo.toml b/idevice/Cargo.toml index 606d3b9..5a474bf 100644 --- a/idevice/Cargo.toml +++ b/idevice/Cargo.toml @@ -21,7 +21,7 @@ tokio-openssl = { version = "0.6", optional = true } openssl = { version = "0.10", optional = true } plist = { version = "1.8" } -plist-macro = { version = "0.1" } +plist-macro = { version = "0.1.3" } serde = { version = "1", features = ["derive"] } ns-keyed-archive = { version = "0.1.4", optional = true } crossfire = { version = "2.1", optional = true } diff --git a/idevice/src/services/springboardservices.rs b/idevice/src/services/springboardservices.rs index 81a28b4..15177d1 100644 --- a/idevice/src/services/springboardservices.rs +++ b/idevice/src/services/springboardservices.rs @@ -3,7 +3,7 @@ //! Provides functionality for interacting with the SpringBoard services on iOS devices, //! which manages home screen and app icon related operations. -use crate::{Idevice, IdeviceError, IdeviceService, obf}; +use crate::{Idevice, IdeviceError, IdeviceService, obf, utils::plist::truncate_dates_to_seconds}; /// Client for interacting with the iOS SpringBoard services /// @@ -70,4 +70,132 @@ impl SpringBoardServicesClient { _ => Err(IdeviceError::UnexpectedResponse), } } + + /// Retrieves the current icon state from the device + /// + /// The icon state contains the layout and organization of all apps on the home screen, + /// including folder structures and icon positions. This is a read-only operation. + /// + /// # Arguments + /// * `format_version` - Optional format version string for the icon state format + /// + /// # Returns + /// A plist Value containing the complete icon state structure + /// + /// # Errors + /// Returns `IdeviceError` if: + /// - Communication fails + /// - The response is malformed + /// + /// # Example + /// ```rust + /// use idevice::services::springboardservices::SpringBoardServicesClient; + /// + /// let mut client = SpringBoardServicesClient::connect(&provider).await?; + /// let icon_state = client.get_icon_state(None).await?; + /// println!("Icon state: {:?}", icon_state); + /// ``` + /// + /// # Notes + /// This method successfully reads the home screen layout on all iOS versions. + pub async fn get_icon_state( + &mut self, + format_version: Option<&str>, + ) -> Result { + let req = crate::plist!({ + "command": "getIconState", + "formatVersion":? format_version, + }); + + self.idevice.send_plist(req).await?; + let mut res = self.idevice.read_plist_value().await?; + + // Some devices may return an error dictionary instead of icon state. + // Detect this and surface it as an UnexpectedResponse, similar to get_icon_pngdata. + if let plist::Value::Dictionary(ref dict) = res + && (dict.contains_key("error") || dict.contains_key("Error")) + { + return Err(IdeviceError::UnexpectedResponse); + } + + truncate_dates_to_seconds(&mut res); + + Ok(res) + } + + /// Sets the icon state on the device + /// + /// This method allows you to modify the home screen layout by providing a new icon state. + /// The icon state structure should match the format returned by `get_icon_state`. + /// + /// # Arguments + /// * `icon_state` - A plist Value containing the complete icon state structure + /// + /// # Returns + /// Ok(()) if the icon state was successfully set + /// + /// # Errors + /// Returns `IdeviceError` if: + /// - Communication fails + /// - The icon state format is invalid + /// - The device rejects the new layout + /// + /// # Example + /// ```rust + /// use idevice::services::springboardservices::SpringBoardServicesClient; + /// + /// let mut client = SpringBoardServicesClient::connect(&provider).await?; + /// let mut icon_state = client.get_icon_state(None).await?; + /// + /// // Modify the icon state (e.g., swap two icons) + /// // ... modify icon_state ... + /// + /// client.set_icon_state(icon_state).await?; + /// println!("Icon state updated successfully"); + /// ``` + /// + /// # Notes + /// - Changes take effect immediately + /// - The device may validate the icon state structure before applying + /// - Invalid icon states will be rejected by the device + pub async fn set_icon_state(&mut self, icon_state: plist::Value) -> Result<(), IdeviceError> { + let req = crate::plist!({ + "command": "setIconState", + "iconState": icon_state, + }); + + self.idevice.send_plist(req).await?; + Ok(()) + } + + /// Sets the icon state with a specific format version + /// + /// This is similar to `set_icon_state` but allows specifying a format version. + /// + /// # Arguments + /// * `icon_state` - A plist Value containing the complete icon state structure + /// * `format_version` - Optional format version string + /// + /// # Returns + /// Ok(()) if the icon state was successfully set + /// + /// # Errors + /// Returns `IdeviceError` if: + /// - Communication fails + /// - The icon state format is invalid + /// - The device rejects the new layout + pub async fn set_icon_state_with_version( + &mut self, + icon_state: plist::Value, + format_version: Option<&str>, + ) -> Result<(), IdeviceError> { + let req = crate::plist!({ + "command": "setIconState", + "iconState": icon_state, + "formatVersion":? format_version, + }); + + self.idevice.send_plist(req).await?; + Ok(()) + } } diff --git a/idevice/src/utils/mod.rs b/idevice/src/utils/mod.rs index d30df75..ded61d4 100644 --- a/idevice/src/utils/mod.rs +++ b/idevice/src/utils/mod.rs @@ -2,3 +2,5 @@ #[cfg(all(feature = "afc", feature = "installation_proxy"))] pub mod installation; + +pub mod plist; diff --git a/idevice/src/utils/plist.rs b/idevice/src/utils/plist.rs new file mode 100644 index 0000000..fb763ac --- /dev/null +++ b/idevice/src/utils/plist.rs @@ -0,0 +1,155 @@ +/// Utilities for working with plist values +/// +/// Truncates all Date values in a plist structure to second precision. +/// +/// This function recursively walks through a plist Value and truncates any Date values +/// from nanosecond precision to second precision. This is necessary for compatibility +/// with iOS devices that reject high-precision date formats. +/// +/// # Arguments +/// * `value` - The plist Value to normalize (modified in place) +/// +/// # Example +/// ```rust,no_run +/// use idevice::utils::plist::truncate_dates_to_seconds; +/// use plist::Value; +/// +/// let mut icon_state = Value::Array(vec![]); +/// truncate_dates_to_seconds(&mut icon_state); +/// ``` +/// +/// # Details +/// - Converts dates from format: `2026-01-17T03:09:58.332738876Z` (nanosecond precision) +/// - To format: `2026-01-17T03:09:58Z` (second precision) +/// - Recursively processes Arrays and Dictionaries +/// - Other value types are left unchanged +pub fn truncate_dates_to_seconds(value: &mut plist::Value) { + match value { + plist::Value::Date(date) => { + let xml_string = date.to_xml_format(); + if let Some(dot_pos) = xml_string.find('.') + && xml_string[dot_pos..].contains('Z') + { + let truncated_string = format!("{}Z", &xml_string[..dot_pos]); + if let Ok(new_date) = plist::Date::from_xml_format(&truncated_string) { + *date = new_date; + } + } + } + plist::Value::Array(arr) => { + for item in arr.iter_mut() { + truncate_dates_to_seconds(item); + } + } + plist::Value::Dictionary(dict) => { + for (_, v) in dict.iter_mut() { + truncate_dates_to_seconds(v); + } + } + _ => {} + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_truncate_date_with_nanoseconds() { + let date_str = "2026-01-17T03:09:58.332738876Z"; + let date = plist::Date::from_xml_format(date_str).unwrap(); + let mut value = plist::Value::Date(date); + + truncate_dates_to_seconds(&mut value); + + if let plist::Value::Date(truncated_date) = value { + let result = truncated_date.to_xml_format(); + assert!( + !result.contains('.'), + "Date should not contain fractional seconds" + ); + assert!(result.ends_with('Z'), "Date should end with Z"); + assert!( + result.starts_with("2026-01-17T03:09:58"), + "Date should preserve main timestamp" + ); + } else { + panic!("Value should still be a Date"); + } + } + + #[test] + fn test_truncate_date_already_truncated() { + let date_str = "2026-01-17T03:09:58Z"; + let date = plist::Date::from_xml_format(date_str).unwrap(); + let original_format = date.to_xml_format(); + let mut value = plist::Value::Date(date); + + truncate_dates_to_seconds(&mut value); + + if let plist::Value::Date(truncated_date) = value { + let result = truncated_date.to_xml_format(); + assert_eq!( + result, original_format, + "Already truncated date should remain unchanged" + ); + } + } + + #[test] + fn test_truncate_dates_in_array() { + let date1 = plist::Date::from_xml_format("2026-01-17T03:09:58.123456Z").unwrap(); + let date2 = plist::Date::from_xml_format("2026-01-18T04:10:59.987654Z").unwrap(); + let mut value = + plist::Value::Array(vec![plist::Value::Date(date1), plist::Value::Date(date2)]); + + truncate_dates_to_seconds(&mut value); + + if let plist::Value::Array(arr) = value { + for item in arr { + if let plist::Value::Date(date) = item { + let formatted = date.to_xml_format(); + assert!( + !formatted.contains('.'), + "Dates in array should be truncated" + ); + } + } + } + } + + #[test] + fn test_truncate_dates_in_dictionary() { + let date = plist::Date::from_xml_format("2026-01-17T03:09:58.999999Z").unwrap(); + let mut dict = plist::Dictionary::new(); + dict.insert("timestamp".to_string(), plist::Value::Date(date)); + let mut value = plist::Value::Dictionary(dict); + + truncate_dates_to_seconds(&mut value); + + if let plist::Value::Dictionary(dict) = value + && let Some(plist::Value::Date(date)) = dict.get("timestamp") + { + let formatted = date.to_xml_format(); + assert!( + !formatted.contains('.'), + "Date in dictionary should be truncated" + ); + } + } + + #[test] + fn test_other_value_types_unchanged() { + let mut string_val = plist::Value::String("test".to_string()); + let mut int_val = plist::Value::Integer(42.into()); + let mut bool_val = plist::Value::Boolean(true); + + truncate_dates_to_seconds(&mut string_val); + truncate_dates_to_seconds(&mut int_val); + truncate_dates_to_seconds(&mut bool_val); + + assert!(matches!(string_val, plist::Value::String(_))); + assert!(matches!(int_val, plist::Value::Integer(_))); + assert!(matches!(bool_val, plist::Value::Boolean(_))); + } +} diff --git a/tools/Cargo.toml b/tools/Cargo.toml index 28cc33c..1eaea88 100644 --- a/tools/Cargo.toml +++ b/tools/Cargo.toml @@ -33,7 +33,7 @@ ureq = { version = "3" } clap = { version = "4.5" } jkcli = { version = "0.1" } plist = { version = "1.7" } -plist-macro = { version = "0.1" } +plist-macro = { version = "0.1.3" } ns-keyed-archive = "0.1.2" uuid = "1.16" futures-util = { version = "0.3" } diff --git a/tools/src/main.rs b/tools/src/main.rs index 5e215b4..4bd3d77 100644 --- a/tools/src/main.rs +++ b/tools/src/main.rs @@ -42,6 +42,7 @@ mod process_control; mod remotexpc; mod restore_service; mod screenshot; +mod springboardservices; mod syslog_relay; mod pcap; @@ -120,6 +121,7 @@ async fn main() { .with_subcommand("remotexpc", remotexpc::register()) .with_subcommand("restore_service", restore_service::register()) .with_subcommand("screenshot", screenshot::register()) + .with_subcommand("springboard", springboardservices::register()) .with_subcommand("syslog_relay", syslog_relay::register()) .subcommand_required(true) .collect() @@ -236,6 +238,9 @@ async fn main() { "screenshot" => { screenshot::main(sub_args, provider).await; } + "springboard" => { + springboardservices::main(sub_args, provider).await; + } "syslog_relay" => { syslog_relay::main(sub_args, provider).await; } diff --git a/tools/src/springboardservices.rs b/tools/src/springboardservices.rs new file mode 100644 index 0000000..5f0fb56 --- /dev/null +++ b/tools/src/springboardservices.rs @@ -0,0 +1,76 @@ +// Jackson Coxson + +use idevice::{ + IdeviceService, provider::IdeviceProvider, springboardservices::SpringBoardServicesClient, +}; +use jkcli::{CollectedArguments, JkArgument, JkCommand, JkFlag}; +use plist_macro::{plist_value_to_xml_bytes, pretty_print_plist}; + +pub fn register() -> JkCommand { + JkCommand::new() + .help("Manage the springboard service") + .with_subcommand( + "get_icon_state", + JkCommand::new() + .help("Gets the icon state from the device") + .with_argument( + JkArgument::new() + .with_help("Version to query by") + .required(false), + ) + .with_flag( + JkFlag::new("save") + .with_help("Path to save to") + .with_argument(JkArgument::new().required(true)), + ), + ) + .with_subcommand( + "set_icon_state", + JkCommand::new().help("Sets the icon state").with_argument( + JkArgument::new() + .with_help("plist to set based on") + .required(true), + ), + ) + .subcommand_required(true) +} + +pub async fn main(arguments: &CollectedArguments, provider: Box) { + let mut sbc = SpringBoardServicesClient::connect(&*provider) + .await + .expect("Failed to connect to springboardservices"); + + let (sub_name, sub_args) = arguments.first_subcommand().expect("No subcommand passed"); + let mut sub_args = sub_args.clone(); + + match sub_name.as_str() { + "get_icon_state" => { + let version: Option = sub_args.next_argument(); + let version = version.as_deref(); + let state = sbc + .get_icon_state(version) + .await + .expect("Failed to get icon state"); + println!("{}", pretty_print_plist(&state)); + + if let Some(path) = sub_args.get_flag::("save") { + tokio::fs::write(path, plist_value_to_xml_bytes(&state)) + .await + .expect("Failed to save to path"); + } + } + "set_icon_state" => { + let load_path = sub_args.next_argument::().unwrap(); + let load = tokio::fs::read(load_path) + .await + .expect("Failed to read plist"); + let load: plist::Value = + plist::from_bytes(&load).expect("Failed to parse bytes as plist"); + + sbc.set_icon_state(load) + .await + .expect("Failed to set icon state"); + } + _ => unreachable!(), + } +} From 77ea34f820fd51d1bd5fc93d61eceeb7a2fe3264 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Thu, 22 Jan 2026 15:34:11 -0700 Subject: [PATCH 25/34] Bump version --- Cargo.lock | 4 ++-- idevice/Cargo.toml | 2 +- tools/Cargo.toml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4bee460..ca23049 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1091,7 +1091,7 @@ dependencies = [ [[package]] name = "idevice" -version = "0.1.51" +version = "0.1.52" dependencies = [ "async-stream", "async_zip", @@ -1147,7 +1147,7 @@ dependencies = [ [[package]] name = "idevice-tools" -version = "0.1.0" +version = "0.1.52" dependencies = [ "clap", "futures-util", diff --git a/idevice/Cargo.toml b/idevice/Cargo.toml index 5a474bf..fd5cd7d 100644 --- a/idevice/Cargo.toml +++ b/idevice/Cargo.toml @@ -2,7 +2,7 @@ name = "idevice" description = "A Rust library to interact with services on iOS devices." authors = ["Jackson Coxson"] -version = "0.1.51" +version = "0.1.52" edition = "2024" license = "MIT" documentation = "https://docs.rs/idevice" diff --git a/tools/Cargo.toml b/tools/Cargo.toml index 1eaea88..d18bf99 100644 --- a/tools/Cargo.toml +++ b/tools/Cargo.toml @@ -2,7 +2,7 @@ name = "idevice-tools" description = "Rust binary tools to interact with services on iOS devices." authors = ["Jackson Coxson"] -version = "0.1.0" +version = "0.1.52" edition = "2024" license = "MIT" documentation = "https://docs.rs/idevice" From 496e0991879434f4800b0ad7f2e896649ddb7685 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Fri, 30 Jan 2026 08:56:07 -0700 Subject: [PATCH 26/34] Make UUID an argument of trust subcommand in amfi --- tools/src/amfi.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tools/src/amfi.rs b/tools/src/amfi.rs index 0298da1..432449e 100644 --- a/tools/src/amfi.rs +++ b/tools/src/amfi.rs @@ -19,8 +19,12 @@ pub fn register() -> JkCommand { "status", JkCommand::new().help("Gets the developer mode status"), ) - .with_subcommand("trust", JkCommand::new().help("Trusts an app signer")) - .with_argument(JkArgument::new().with_help("UUID").required(true)) + .with_subcommand( + "trust", + JkCommand::new() + .help("Trusts an app signer") + .with_argument(JkArgument::new().with_help("UUID").required(true)), + ) .subcommand_required(true) } From 38a3a558b5f7420475e55ba077ef0977a2de6c3b Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Fri, 6 Feb 2026 16:45:27 -0700 Subject: [PATCH 27/34] (BREAKING) implement host name parameter for lockdown pair --- idevice/src/services/lockdown.rs | 2 ++ tools/src/pair.rs | 13 +++++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/idevice/src/services/lockdown.rs b/idevice/src/services/lockdown.rs index 34d88b9..df147e4 100644 --- a/idevice/src/services/lockdown.rs +++ b/idevice/src/services/lockdown.rs @@ -260,6 +260,7 @@ impl LockdownClient { &mut self, host_id: impl Into, system_buid: impl Into, + host_name: Option<&str>, ) -> Result { let host_id = host_id.into(); let system_buid = system_buid.into(); @@ -297,6 +298,7 @@ impl LockdownClient { let req = crate::plist!({ "Label": self.idevice.label.clone(), "Request": "Pair", + "HostName":? host_name, "PairRecord": pair_record.clone(), "ProtocolVersion": "2", "PairingOptions": { diff --git a/tools/src/pair.rs b/tools/src/pair.rs index 1cfc23d..fd5ec29 100644 --- a/tools/src/pair.rs +++ b/tools/src/pair.rs @@ -6,12 +6,18 @@ use idevice::{ provider::IdeviceProvider, usbmuxd::{Connection, UsbmuxdAddr, UsbmuxdConnection}, }; -use jkcli::{CollectedArguments, JkArgument, JkCommand}; +use jkcli::{CollectedArguments, JkArgument, JkCommand, JkFlag}; pub fn register() -> JkCommand { JkCommand::new() .help("Manage files in the AFC jail of a device") .with_argument(JkArgument::new().with_help("A UDID to override and pair with")) + .with_flag( + JkFlag::new("name") + .with_help("The host name to report to the device") + .with_argument(JkArgument::new().required(true)) + .with_short("n"), + ) } pub async fn main(arguments: &CollectedArguments, _provider: Box) { @@ -45,8 +51,11 @@ pub async fn main(arguments: &CollectedArguments, _provider: Box("name"); + let name = name.as_deref(); + let mut pairing_file = lockdown_client - .pair(id, u.get_buid().await.unwrap()) + .pair(id, u.get_buid().await.unwrap(), name) .await .expect("Failed to pair"); From c5aa731ee54f5ec69259434746af0e1b3151a6a2 Mon Sep 17 00:00:00 2001 From: neo Date: Mon, 9 Feb 2026 21:14:16 -0500 Subject: [PATCH 28/34] feat(springboard): add wallpaper preview command support (#64) * feat(springboard): add wallpaper preview command support * Use subargs to switch between preview type in sb cli --------- Co-authored-by: Jackson Coxson --- ffi/src/springboardservices.rs | 90 +++++++++++++++++++++ idevice/src/services/springboardservices.rs | 67 +++++++++++++++ tools/src/springboardservices.rs | 31 +++++++ 3 files changed, 188 insertions(+) diff --git a/ffi/src/springboardservices.rs b/ffi/src/springboardservices.rs index 50d56d5..4145ebd 100644 --- a/ffi/src/springboardservices.rs +++ b/ffi/src/springboardservices.rs @@ -137,6 +137,96 @@ pub unsafe extern "C" fn springboard_services_get_icon( } } +/// Gets the home screen wallpaper preview as PNG image +/// +/// # Arguments +/// * `client` - A valid SpringBoardServicesClient handle +/// * `out_result` - On success, will be set to point to newly allocated png image +/// * `out_result_len` - On success, will contain the size of the data in bytes +/// +/// # Returns +/// An IdeviceFfiError on error, null on success +/// +/// # Safety +/// `client` must be a valid pointer to a handle allocated by this library +/// `out_result` and `out_result_len` must be valid, non-null pointers +#[unsafe(no_mangle)] +pub unsafe extern "C" fn springboard_services_get_home_screen_wallpaper_preview( + client: *mut SpringBoardServicesClientHandle, + out_result: *mut *mut c_void, + out_result_len: *mut libc::size_t, +) -> *mut IdeviceFfiError { + if client.is_null() || out_result.is_null() || out_result_len.is_null() { + tracing::error!("Invalid arguments: {client:?}, {out_result:?}"); + return ffi_err!(IdeviceError::FfiInvalidArg); + } + let client = unsafe { &mut *client }; + + let res: Result, IdeviceError> = + run_sync(async { client.0.get_home_screen_wallpaper_preview_pngdata().await }); + + match res { + Ok(r) => { + let len = r.len(); + let boxed_slice = r.into_boxed_slice(); + let ptr = boxed_slice.as_ptr(); + std::mem::forget(boxed_slice); + + unsafe { + *out_result = ptr as *mut c_void; + *out_result_len = len; + } + null_mut() + } + Err(e) => ffi_err!(e), + } +} + +/// Gets the lock screen wallpaper preview as PNG image +/// +/// # Arguments +/// * `client` - A valid SpringBoardServicesClient handle +/// * `out_result` - On success, will be set to point to newly allocated png image +/// * `out_result_len` - On success, will contain the size of the data in bytes +/// +/// # Returns +/// An IdeviceFfiError on error, null on success +/// +/// # Safety +/// `client` must be a valid pointer to a handle allocated by this library +/// `out_result` and `out_result_len` must be valid, non-null pointers +#[unsafe(no_mangle)] +pub unsafe extern "C" fn springboard_services_get_lock_screen_wallpaper_preview( + client: *mut SpringBoardServicesClientHandle, + out_result: *mut *mut c_void, + out_result_len: *mut libc::size_t, +) -> *mut IdeviceFfiError { + if client.is_null() || out_result.is_null() || out_result_len.is_null() { + tracing::error!("Invalid arguments: {client:?}, {out_result:?}"); + return ffi_err!(IdeviceError::FfiInvalidArg); + } + let client = unsafe { &mut *client }; + + let res: Result, IdeviceError> = + run_sync(async { client.0.get_lock_screen_wallpaper_preview_pngdata().await }); + + match res { + Ok(r) => { + let len = r.len(); + let boxed_slice = r.into_boxed_slice(); + let ptr = boxed_slice.as_ptr(); + std::mem::forget(boxed_slice); + + unsafe { + *out_result = ptr as *mut c_void; + *out_result_len = len; + } + null_mut() + } + Err(e) => ffi_err!(e), + } +} + /// Frees an SpringBoardServicesClient handle /// /// # Arguments diff --git a/idevice/src/services/springboardservices.rs b/idevice/src/services/springboardservices.rs index 15177d1..11b4b9d 100644 --- a/idevice/src/services/springboardservices.rs +++ b/idevice/src/services/springboardservices.rs @@ -198,4 +198,71 @@ impl SpringBoardServicesClient { self.idevice.send_plist(req).await?; Ok(()) } + /// Gets the home screen wallpaper preview as PNG data + /// + /// This gets a rendered preview of the home screen wallpaper. + /// + /// # Returns + /// The raw PNG data of the home screen wallpaper preview + /// + /// # Errors + /// Returns `IdeviceError` if: + /// - Communication fails + /// - The device rejects the request + /// - The image is malformed/corupted + /// + /// # Example + /// ```rust + /// let wallpaper = client.get_home_screen_wallpaper_preview_pngdata().await?; + /// std::fs::write("home.png", wallpaper)?; + /// ``` + pub async fn get_home_screen_wallpaper_preview_pngdata( + &mut self, + ) -> Result, IdeviceError> { + let req = crate::plist!({ + "command": "getWallpaperPreviewImage", + "wallpaperName": "homescreen", + }); + self.idevice.send_plist(req).await?; + + let mut res = self.idevice.read_plist().await?; + match res.remove("pngData") { + Some(plist::Value::Data(res)) => Ok(res), + _ => Err(IdeviceError::UnexpectedResponse), + } + } + + /// Gets the lock screen wallpaper preview as PNG data + /// + /// This gets a rendered preview of the lock screen wallpaper. + /// + /// # Returns + /// The raw PNG data of the lock screen wallpaper preview + /// + /// # Errors + /// Returns `IdeviceError` if: + /// - Communication fails + /// - The device rejects the request + /// - The image is malformed/corupted + /// + /// # Example + /// ```rust + /// let wallpaper = client.get_lock_screen_wallpaper_preview_pngdata().await?; + /// std::fs::write("lock.png", wallpaper)?; + /// ``` + pub async fn get_lock_screen_wallpaper_preview_pngdata( + &mut self, + ) -> Result, IdeviceError> { + let req = crate::plist!({ + "command": "getWallpaperPreviewImage", + "wallpaperName": "lockscreen", + }); + self.idevice.send_plist(req).await?; + + let mut res = self.idevice.read_plist().await?; + match res.remove("pngData") { + Some(plist::Value::Data(res)) => Ok(res), + _ => Err(IdeviceError::UnexpectedResponse), + } + } } diff --git a/tools/src/springboardservices.rs b/tools/src/springboardservices.rs index 5f0fb56..f997c2f 100644 --- a/tools/src/springboardservices.rs +++ b/tools/src/springboardservices.rs @@ -32,6 +32,19 @@ pub fn register() -> JkCommand { .required(true), ), ) + .with_subcommand( + "get_wallpaper_preview", + JkCommand::new() + .help("Gets wallpaper preview") + .with_subcommand("homescreen", JkCommand::new()) + .with_subcommand("lockscreen", JkCommand::new()) + .subcommand_required(true) + .with_flag( + JkFlag::new("save") + .with_help("Path to save the wallpaper PNG file, or preview.png by default") + .with_argument(JkArgument::new().required(true)), + ), + ) .subcommand_required(true) } @@ -71,6 +84,24 @@ pub async fn main(arguments: &CollectedArguments, provider: Box { + let (wallpaper_type, _) = sub_args.first_subcommand().unwrap(); + + let wallpaper = match wallpaper_type.as_str() { + "homescreen" => sbc.get_home_screen_wallpaper_preview_pngdata().await, + "lockscreen" => sbc.get_lock_screen_wallpaper_preview_pngdata().await, + _ => panic!("Invalid wallpaper type. Use 'homescreen' or 'lockscreen'"), + } + .expect("Failed to get wallpaper preview"); + + let save_path = sub_args + .get_flag::("save") + .unwrap_or("preview.png".to_string()); + + tokio::fs::write(&save_path, wallpaper) + .await + .expect("Failed to save wallpaper"); + } _ => unreachable!(), } } From cb375f88a1c253acbd837e0fa8c5829bdb15558c Mon Sep 17 00:00:00 2001 From: neo Date: Fri, 13 Feb 2026 09:54:00 -0500 Subject: [PATCH 29/34] feat(springboard): get device orientation (#65) --- ffi/src/springboardservices.rs | 36 +++++++++++++ idevice/src/services/springboardservices.rs | 60 +++++++++++++++++++++ tools/src/springboardservices.rs | 11 ++++ 3 files changed, 107 insertions(+) diff --git a/ffi/src/springboardservices.rs b/ffi/src/springboardservices.rs index 4145ebd..a56709f 100644 --- a/ffi/src/springboardservices.rs +++ b/ffi/src/springboardservices.rs @@ -227,6 +227,42 @@ pub unsafe extern "C" fn springboard_services_get_lock_screen_wallpaper_preview( } } +/// Gets the current interface orientation of the device +/// +/// # Arguments +/// * `client` - A valid SpringBoardServicesClient handle +/// * `out_orientation` - On success, will contain the orientation value (0-4) +/// +/// # Returns +/// An IdeviceFfiError on error, null on success +/// +/// # Safety +/// `client` must be a valid pointer to a handle allocated by this library +/// `out_orientation` must be a valid, non-null pointer +#[unsafe(no_mangle)] +pub unsafe extern "C" fn springboard_services_get_interface_orientation( + client: *mut SpringBoardServicesClientHandle, + out_orientation: *mut u8, +) -> *mut IdeviceFfiError { + if client.is_null() || out_orientation.is_null() { + tracing::error!("Invalid arguments: {client:?}, {out_orientation:?}"); + return ffi_err!(IdeviceError::FfiInvalidArg); + } + let client = unsafe { &mut *client }; + + let res = run_sync(async { client.0.get_interface_orientation().await }); + + match res { + Ok(orientation) => { + unsafe { + *out_orientation = orientation as u8; + } + null_mut() + } + Err(e) => ffi_err!(e), + } +} + /// Frees an SpringBoardServicesClient handle /// /// # Arguments diff --git a/idevice/src/services/springboardservices.rs b/idevice/src/services/springboardservices.rs index 11b4b9d..3e89004 100644 --- a/idevice/src/services/springboardservices.rs +++ b/idevice/src/services/springboardservices.rs @@ -5,6 +5,22 @@ use crate::{Idevice, IdeviceError, IdeviceService, obf, utils::plist::truncate_dates_to_seconds}; +/// Orientation of the device +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum InterfaceOrientation { + /// Orientation is unknown or cannot be determined + Unknown = 0, + /// Portrait mode (normal vertical) + Portrait = 1, + /// Portrait mode upside down + PortraitUpsideDown = 2, + /// Landscape with home button on the right (notch to the left) + LandscapeRight = 3, + /// Landscape with home button on the left (notch to the right) + LandscapeLeft = 4, +} + /// Client for interacting with the iOS SpringBoard services /// /// This service provides access to home screen and app icon functionality, @@ -198,6 +214,7 @@ impl SpringBoardServicesClient { self.idevice.send_plist(req).await?; Ok(()) } + /// Gets the home screen wallpaper preview as PNG data /// /// This gets a rendered preview of the home screen wallpaper. @@ -265,4 +282,47 @@ impl SpringBoardServicesClient { _ => Err(IdeviceError::UnexpectedResponse), } } + + /// Gets the current interface orientation of the device + /// + /// This gets which way the device is currently facing + /// + /// # Returns + /// The current `InterfaceOrientation` of the device + /// + /// # Errors + /// Returns `IdeviceError` if: + /// - Communication fails + /// - The device doesn't support this command + /// - The response format is unexpected + /// + /// # Example + /// ```rust + /// let orientation = client.get_interface_orientation().await?; + /// println!("Device orientation: {:?}", orientation); + /// ``` + pub async fn get_interface_orientation( + &mut self, + ) -> Result { + let req = crate::plist!({ + "command": "getInterfaceOrientation", + }); + self.idevice.send_plist(req).await?; + + let res = self.idevice.read_plist().await?; + let orientation_value = res + .get("interfaceOrientation") + .and_then(|v| v.as_unsigned_integer()) + .ok_or(IdeviceError::UnexpectedResponse)?; + + let orientation = match orientation_value { + 1 => InterfaceOrientation::Portrait, + 2 => InterfaceOrientation::PortraitUpsideDown, + 3 => InterfaceOrientation::LandscapeRight, + 4 => InterfaceOrientation::LandscapeLeft, + _ => InterfaceOrientation::Unknown, + }; + + Ok(orientation) + } } diff --git a/tools/src/springboardservices.rs b/tools/src/springboardservices.rs index f997c2f..1165b6a 100644 --- a/tools/src/springboardservices.rs +++ b/tools/src/springboardservices.rs @@ -45,6 +45,10 @@ pub fn register() -> JkCommand { .with_argument(JkArgument::new().required(true)), ), ) + .with_subcommand( + "get_interface_orientation", + JkCommand::new().help("Gets the device's current screen orientation"), + ) .subcommand_required(true) } @@ -102,6 +106,13 @@ pub async fn main(arguments: &CollectedArguments, provider: Box { + let orientation = sbc + .get_interface_orientation() + .await + .expect("Failed to get interface orientation"); + println!("{:?}", orientation); + } _ => unreachable!(), } } From a523f0cb9ceae74bd5219fbbff9ff5352e645db1 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Fri, 13 Feb 2026 12:10:20 -0700 Subject: [PATCH 30/34] Bump version --- Cargo.lock | 4 ++-- idevice/Cargo.toml | 2 +- tools/Cargo.toml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ca23049..4c6c614 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1091,7 +1091,7 @@ dependencies = [ [[package]] name = "idevice" -version = "0.1.52" +version = "0.1.53" dependencies = [ "async-stream", "async_zip", @@ -1147,7 +1147,7 @@ dependencies = [ [[package]] name = "idevice-tools" -version = "0.1.52" +version = "0.1.53" dependencies = [ "clap", "futures-util", diff --git a/idevice/Cargo.toml b/idevice/Cargo.toml index fd5cd7d..c929127 100644 --- a/idevice/Cargo.toml +++ b/idevice/Cargo.toml @@ -2,7 +2,7 @@ name = "idevice" description = "A Rust library to interact with services on iOS devices." authors = ["Jackson Coxson"] -version = "0.1.52" +version = "0.1.53" edition = "2024" license = "MIT" documentation = "https://docs.rs/idevice" diff --git a/tools/Cargo.toml b/tools/Cargo.toml index d18bf99..8e7e710 100644 --- a/tools/Cargo.toml +++ b/tools/Cargo.toml @@ -2,7 +2,7 @@ name = "idevice-tools" description = "Rust binary tools to interact with services on iOS devices." authors = ["Jackson Coxson"] -version = "0.1.52" +version = "0.1.53" edition = "2024" license = "MIT" documentation = "https://docs.rs/idevice" From 54439b85dd48663a4562ad01f63fbc57351e1f3d Mon Sep 17 00:00:00 2001 From: neo Date: Fri, 13 Feb 2026 15:00:47 -0500 Subject: [PATCH 31/34] feat(springboard): get homescreen icon metrics (#67) * feat(springboard): get homescreen icon metrics * chore: clippy and fmt --- ffi/src/springboardservices.rs | 38 +++++++++++++++++++++ idevice/src/services/springboardservices.rs | 27 +++++++++++++++ tools/src/springboardservices.rs | 12 +++++++ 3 files changed, 77 insertions(+) diff --git a/ffi/src/springboardservices.rs b/ffi/src/springboardservices.rs index a56709f..7f1cade 100644 --- a/ffi/src/springboardservices.rs +++ b/ffi/src/springboardservices.rs @@ -7,6 +7,7 @@ use idevice::{ IdeviceError, IdeviceService, provider::IdeviceProvider, springboardservices::SpringBoardServicesClient, }; +use plist_ffi::plist_t; use crate::{ IdeviceFfiError, IdeviceHandle, ffi_err, provider::IdeviceProviderHandle, run_sync, @@ -263,6 +264,43 @@ pub unsafe extern "C" fn springboard_services_get_interface_orientation( } } +/// Gets the home screen icon layout metrics +/// +/// # Arguments +/// * `client` - A valid SpringBoardServicesClient handle +/// * `res` - On success, will point to a plist dictionary node containing the metrics +/// +/// # Returns +/// An IdeviceFfiError on error, null on success +/// +/// # Safety +/// `client` must be a valid pointer to a handle allocated by this library +/// `res` must be a valid, non-null pointer +#[unsafe(no_mangle)] +pub unsafe extern "C" fn springboard_services_get_homescreen_icon_metrics( + client: *mut SpringBoardServicesClientHandle, + res: *mut plist_t, +) -> *mut IdeviceFfiError { + if client.is_null() || res.is_null() { + tracing::error!("Invalid arguments: {client:?}, {res:?}"); + return ffi_err!(IdeviceError::FfiInvalidArg); + } + let client = unsafe { &mut *client }; + + let output = run_sync(async { client.0.get_homescreen_icon_metrics().await }); + + match output { + Ok(metrics) => { + unsafe { + *res = + plist_ffi::PlistWrapper::new_node(plist::Value::Dictionary(metrics)).into_ptr(); + } + null_mut() + } + Err(e) => ffi_err!(e), + } +} + /// Frees an SpringBoardServicesClient handle /// /// # Arguments diff --git a/idevice/src/services/springboardservices.rs b/idevice/src/services/springboardservices.rs index 3e89004..1fb98d7 100644 --- a/idevice/src/services/springboardservices.rs +++ b/idevice/src/services/springboardservices.rs @@ -325,4 +325,31 @@ impl SpringBoardServicesClient { Ok(orientation) } + + /// Gets the home screen icon layout metrics + /// + /// Returns icon spacing, size, and positioning information + /// + /// # Returns + /// A `plist::Dictionary` containing the icon layout metrics + /// + /// # Errors + /// Returns `IdeviceError` if: + /// - Communication fails + /// - The response is malformed + /// + /// # Example + /// ```rust + /// let metrics = client.get_homescreen_icon_metrics().await?; + /// println!("{:?}", metrics); + /// ``` + pub async fn get_homescreen_icon_metrics(&mut self) -> Result { + let req = crate::plist!({ + "command": "getHomeScreenIconMetrics", + }); + self.idevice.send_plist(req).await?; + + let res = self.idevice.read_plist().await?; + Ok(res) + } } diff --git a/tools/src/springboardservices.rs b/tools/src/springboardservices.rs index 1165b6a..d4efbf4 100644 --- a/tools/src/springboardservices.rs +++ b/tools/src/springboardservices.rs @@ -49,6 +49,10 @@ pub fn register() -> JkCommand { "get_interface_orientation", JkCommand::new().help("Gets the device's current screen orientation"), ) + .with_subcommand( + "get_homescreen_icon_metrics", + JkCommand::new().help("Gets home screen icon layout metrics"), + ) .subcommand_required(true) } @@ -113,6 +117,14 @@ pub async fn main(arguments: &CollectedArguments, provider: Box { + let metrics = sbc + .get_homescreen_icon_metrics() + .await + .expect("Failed to get homescreen icon metrics"); + let metrics_value = plist::Value::Dictionary(metrics); + println!("{}", pretty_print_plist(&metrics_value)); + } _ => unreachable!(), } } From bfe44e16e4de59c5728078ad50199fda248951d1 Mon Sep 17 00:00:00 2001 From: neo Date: Sat, 14 Feb 2026 15:16:26 -0500 Subject: [PATCH 32/34] feat: notification proxy (#70) * init * chore: clippy and fmt * feat: ffi wrapper * feat: multi-observe and timeout to notification proxy * fix: nitpicks 1. proxy death its onw error in emun #69 2. make returned stream actual stream, copied from https://github.com/jkcoxson/idevice/blob/54439b85dd48663a4562ad01f63fbc57351e1f3d/idevice/src/services/bt_packet_logger.rs#L126-L138 --- cpp/include/idevice++/notification_proxy.hpp | 46 +++ cpp/src/notification_proxy.cpp | 82 +++++ ffi/Cargo.toml | 2 + ffi/src/lib.rs | 2 + ffi/src/notification_proxy.rs | 311 +++++++++++++++++++ idevice/Cargo.toml | 2 + idevice/src/lib.rs | 7 + idevice/src/services/mod.rs | 2 + idevice/src/services/notification_proxy.rs | 212 +++++++++++++ tools/src/main.rs | 5 + tools/src/notification_proxy_client.rs | 85 +++++ 11 files changed, 756 insertions(+) create mode 100644 cpp/include/idevice++/notification_proxy.hpp create mode 100644 cpp/src/notification_proxy.cpp create mode 100644 ffi/src/notification_proxy.rs create mode 100644 idevice/src/services/notification_proxy.rs create mode 100644 tools/src/notification_proxy_client.rs diff --git a/cpp/include/idevice++/notification_proxy.hpp b/cpp/include/idevice++/notification_proxy.hpp new file mode 100644 index 0000000..f4bdcf8 --- /dev/null +++ b/cpp/include/idevice++/notification_proxy.hpp @@ -0,0 +1,46 @@ +#pragma once +#include +#include +#include +#include +#include +#include + +namespace IdeviceFFI { + +using NotificationProxyPtr = std::unique_ptr>; + +class NotificationProxy { + public: + // Factory: connect via Provider + static Result connect(Provider& provider); + + // Factory: wrap an existing Idevice socket (consumes it on success) + static Result from_socket(Idevice&& socket); + + // Ops + Result post_notification(const std::string& name); + Result observe_notification(const std::string& name); + Result observe_notifications(const std::vector& names); + Result receive_notification(); + Result receive_notification_with_timeout(u_int64_t interval); + + // RAII / moves + ~NotificationProxy() noexcept = default; + NotificationProxy(NotificationProxy&&) noexcept = default; + NotificationProxy& operator=(NotificationProxy&&) noexcept = default; + NotificationProxy(const NotificationProxy&) = delete; + NotificationProxy& operator=(const NotificationProxy&) = delete; + + NotificationProxyClientHandle* raw() const noexcept { return handle_.get(); } + static NotificationProxy adopt(NotificationProxyClientHandle* h) noexcept { + return NotificationProxy(h); + } + + private: + explicit NotificationProxy(NotificationProxyClientHandle* h) noexcept : handle_(h) {} + NotificationProxyPtr handle_{}; +}; + +} // namespace IdeviceFFI diff --git a/cpp/src/notification_proxy.cpp b/cpp/src/notification_proxy.cpp new file mode 100644 index 0000000..dea5664 --- /dev/null +++ b/cpp/src/notification_proxy.cpp @@ -0,0 +1,82 @@ +// Jackson Coxson + +#include +#include +#include +#include + +namespace IdeviceFFI { + +Result NotificationProxy::connect(Provider& provider) { + NotificationProxyClientHandle* out = nullptr; + FfiError e(::notification_proxy_connect(provider.raw(), &out)); + if (e) { + provider.release(); + return Err(e); + } + return Ok(NotificationProxy::adopt(out)); +} + +Result NotificationProxy::from_socket(Idevice&& socket) { + NotificationProxyClientHandle* out = nullptr; + FfiError e(::notification_proxy_new(socket.raw(), &out)); + if (e) { + return Err(e); + } + socket.release(); + return Ok(NotificationProxy::adopt(out)); +} + +Result NotificationProxy::post_notification(const std::string& name) { + FfiError e(::notification_proxy_post(handle_.get(), name.c_str())); + if (e) { + return Err(e); + } + return Ok(); +} + +Result NotificationProxy::observe_notification(const std::string& name) { + FfiError e(::notification_proxy_observe(handle_.get(), name.c_str())); + if (e) { + return Err(e); + } + return Ok(); +} + +Result NotificationProxy::observe_notifications(const std::vector& names) { + std::vector ptrs; + ptrs.reserve(names.size() + 1); + for (const auto& n : names) { + ptrs.push_back(n.c_str()); + } + ptrs.push_back(nullptr); + FfiError e(::notification_proxy_observe_multiple(handle_.get(), ptrs.data())); + if (e) { + return Err(e); + } + return Ok(); +} + +Result NotificationProxy::receive_notification() { + char* name_ptr = nullptr; + FfiError e(::notification_proxy_receive(handle_.get(), &name_ptr)); + if (e) { + return Err(e); + } + std::string name(name_ptr); + ::notification_proxy_free_string(name_ptr); + return Ok(std::move(name)); +} + +Result NotificationProxy::receive_notification_with_timeout(u_int64_t interval) { + char* name_ptr = nullptr; + FfiError e(::notification_proxy_receive_with_timeout(handle_.get(), interval, &name_ptr)); + if (e) { + return Err(e); + } + std::string name(name_ptr); + ::notification_proxy_free_string(name_ptr); + return Ok(std::move(name)); +} + +} // namespace IdeviceFFI diff --git a/ffi/Cargo.toml b/ffi/Cargo.toml index 4e3d8f7..6b8e70e 100644 --- a/ffi/Cargo.toml +++ b/ffi/Cargo.toml @@ -34,6 +34,7 @@ debug_proxy = ["idevice/debug_proxy"] diagnostics_relay = ["idevice/diagnostics_relay"] dvt = ["idevice/dvt"] heartbeat = ["idevice/heartbeat"] +notification_proxy = ["idevice/notification_proxy"] house_arrest = ["idevice/house_arrest"] installation_proxy = ["idevice/installation_proxy"] springboardservices = ["idevice/springboardservices"] @@ -61,6 +62,7 @@ full = [ "diagnostics_relay", "dvt", "heartbeat", + "notification_proxy", "house_arrest", "installation_proxy", "misagent", diff --git a/ffi/src/lib.rs b/ffi/src/lib.rs index 31e0147..6b8d004 100644 --- a/ffi/src/lib.rs +++ b/ffi/src/lib.rs @@ -29,6 +29,8 @@ pub mod logging; pub mod misagent; #[cfg(feature = "mobile_image_mounter")] pub mod mobile_image_mounter; +#[cfg(feature = "notification_proxy")] +pub mod notification_proxy; #[cfg(feature = "syslog_relay")] pub mod os_trace_relay; mod pairing_file; diff --git a/ffi/src/notification_proxy.rs b/ffi/src/notification_proxy.rs new file mode 100644 index 0000000..d883f88 --- /dev/null +++ b/ffi/src/notification_proxy.rs @@ -0,0 +1,311 @@ +// Jackson Coxson + +use std::ffi::{CStr, CString, c_char}; +use std::ptr::null_mut; + +use idevice::{ + IdeviceError, IdeviceService, notification_proxy::NotificationProxyClient, + provider::IdeviceProvider, +}; + +use crate::{ + IdeviceFfiError, IdeviceHandle, ffi_err, provider::IdeviceProviderHandle, run_sync_local, +}; + +pub struct NotificationProxyClientHandle(pub NotificationProxyClient); + +/// Automatically creates and connects to Notification Proxy, returning a client handle +/// +/// # Arguments +/// * [`provider`] - An IdeviceProvider +/// * [`client`] - On success, will be set to point to a newly allocated NotificationProxyClient handle +/// +/// # Returns +/// An IdeviceFfiError on error, null on success +/// +/// # Safety +/// `provider` must be a valid pointer to a handle allocated by this library +/// `client` must be a valid, non-null pointer to a location where the handle will be stored +#[unsafe(no_mangle)] +pub unsafe extern "C" fn notification_proxy_connect( + provider: *mut IdeviceProviderHandle, + client: *mut *mut NotificationProxyClientHandle, +) -> *mut IdeviceFfiError { + if provider.is_null() || client.is_null() { + tracing::error!("Null pointer provided"); + return ffi_err!(IdeviceError::FfiInvalidArg); + } + + let res: Result = run_sync_local(async move { + let provider_ref: &dyn IdeviceProvider = unsafe { &*(*provider).0 }; + NotificationProxyClient::connect(provider_ref).await + }); + + match res { + Ok(r) => { + let boxed = Box::new(NotificationProxyClientHandle(r)); + unsafe { *client = Box::into_raw(boxed) }; + null_mut() + } + Err(e) => { + ffi_err!(e) + } + } +} + +/// Creates a new NotificationProxyClient from an existing Idevice connection +/// +/// # Arguments +/// * [`socket`] - An IdeviceSocket handle +/// * [`client`] - On success, will be set to point to a newly allocated NotificationProxyClient handle +/// +/// # Returns +/// An IdeviceFfiError on error, null on success +/// +/// # Safety +/// `socket` must be a valid pointer to a handle allocated by this library. The socket is consumed, +/// and may not be used again. +/// `client` must be a valid, non-null pointer to a location where the handle will be stored +#[unsafe(no_mangle)] +pub unsafe extern "C" fn notification_proxy_new( + socket: *mut IdeviceHandle, + client: *mut *mut NotificationProxyClientHandle, +) -> *mut IdeviceFfiError { + if socket.is_null() || client.is_null() { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + let socket = unsafe { Box::from_raw(socket) }.0; + let r = NotificationProxyClient::new(socket); + let boxed = Box::new(NotificationProxyClientHandle(r)); + unsafe { *client = Box::into_raw(boxed) }; + null_mut() +} + +/// Posts a notification to the device +/// +/// # Arguments +/// * `client` - A valid NotificationProxyClient handle +/// * `name` - C string containing the notification name +/// +/// # Returns +/// An IdeviceFfiError on error, null on success +/// +/// # Safety +/// `client` must be a valid pointer to a handle allocated by this library +/// `name` must be a valid null-terminated C string +#[unsafe(no_mangle)] +pub unsafe extern "C" fn notification_proxy_post( + client: *mut NotificationProxyClientHandle, + name: *const c_char, +) -> *mut IdeviceFfiError { + if client.is_null() || name.is_null() { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + + let name_str = match unsafe { CStr::from_ptr(name) }.to_str() { + Ok(s) => s.to_string(), + Err(_) => return ffi_err!(IdeviceError::FfiInvalidString), + }; + + let res: Result<(), IdeviceError> = run_sync_local(async move { + let client_ref = unsafe { &mut (*client).0 }; + client_ref.post_notification(name_str).await + }); + + match res { + Ok(_) => null_mut(), + Err(e) => ffi_err!(e), + } +} + +/// Observes a specific notification +/// +/// # Arguments +/// * `client` - A valid NotificationProxyClient handle +/// * `name` - C string containing the notification name to observe +/// +/// # Returns +/// An IdeviceFfiError on error, null on success +/// +/// # Safety +/// `client` must be a valid pointer to a handle allocated by this library +/// `name` must be a valid null-terminated C string +#[unsafe(no_mangle)] +pub unsafe extern "C" fn notification_proxy_observe( + client: *mut NotificationProxyClientHandle, + name: *const c_char, +) -> *mut IdeviceFfiError { + if client.is_null() || name.is_null() { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + + let name_str = match unsafe { CStr::from_ptr(name) }.to_str() { + Ok(s) => s.to_string(), + Err(_) => return ffi_err!(IdeviceError::FfiInvalidString), + }; + + let res: Result<(), IdeviceError> = run_sync_local(async move { + let client_ref = unsafe { &mut (*client).0 }; + client_ref.observe_notification(name_str).await + }); + + match res { + Ok(_) => null_mut(), + Err(e) => ffi_err!(e), + } +} + +/// Observes multiple notifications at once +/// +/// # Arguments +/// * `client` - A valid NotificationProxyClient handle +/// * `names` - A null-terminated array of C strings containing notification names +/// +/// # Returns +/// An IdeviceFfiError on error, null on success +/// +/// # Safety +/// `client` must be a valid pointer to a handle allocated by this library +/// `names` must be a valid pointer to a null-terminated array of null-terminated C strings +#[unsafe(no_mangle)] +pub unsafe extern "C" fn notification_proxy_observe_multiple( + client: *mut NotificationProxyClientHandle, + names: *const *const c_char, +) -> *mut IdeviceFfiError { + if client.is_null() || names.is_null() { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + + let mut notification_names: Vec = Vec::new(); + let mut i = 0; + loop { + let ptr = unsafe { *names.add(i) }; + if ptr.is_null() { + break; + } + match unsafe { CStr::from_ptr(ptr) }.to_str() { + Ok(s) => notification_names.push(s.to_string()), + Err(_) => return ffi_err!(IdeviceError::FfiInvalidString), + } + i += 1; + } + + let refs: Vec<&str> = notification_names.iter().map(|s| s.as_str()).collect(); + + let res: Result<(), IdeviceError> = run_sync_local(async move { + let client_ref = unsafe { &mut (*client).0 }; + client_ref.observe_notifications(&refs).await + }); + + match res { + Ok(_) => null_mut(), + Err(e) => ffi_err!(e), + } +} + +/// Receives the next notification from the device +/// +/// # Arguments +/// * `client` - A valid NotificationProxyClient handle +/// * `name_out` - On success, will be set to a newly allocated C string containing the notification name +/// +/// # Returns +/// An IdeviceFfiError on error, null on success +/// +/// # Safety +/// `client` must be a valid pointer to a handle allocated by this library +/// `name_out` must be a valid pointer. The returned string must be freed with `notification_proxy_free_string` +#[unsafe(no_mangle)] +pub unsafe extern "C" fn notification_proxy_receive( + client: *mut NotificationProxyClientHandle, + name_out: *mut *mut c_char, +) -> *mut IdeviceFfiError { + if client.is_null() || name_out.is_null() { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + + let res: Result = run_sync_local(async move { + let client_ref = unsafe { &mut (*client).0 }; + client_ref.receive_notification().await + }); + + match res { + Ok(name) => match CString::new(name) { + Ok(c_string) => { + unsafe { *name_out = c_string.into_raw() }; + null_mut() + } + Err(_) => ffi_err!(IdeviceError::FfiInvalidString), + }, + Err(e) => ffi_err!(e), + } +} + +/// Receives the next notification with a timeout +/// +/// # Arguments +/// * `client` - A valid NotificationProxyClient handle +/// * `interval` - Timeout in seconds to wait for a notification +/// * `name_out` - On success, will be set to a newly allocated C string containing the notification name +/// +/// # Returns +/// An IdeviceFfiError on error, null on success +/// +/// # Safety +/// `client` must be a valid pointer to a handle allocated by this library +/// `name_out` must be a valid pointer. The returned string must be freed with `notification_proxy_free_string` +#[unsafe(no_mangle)] +pub unsafe extern "C" fn notification_proxy_receive_with_timeout( + client: *mut NotificationProxyClientHandle, + interval: u64, + name_out: *mut *mut c_char, +) -> *mut IdeviceFfiError { + if client.is_null() || name_out.is_null() { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + + let res: Result = run_sync_local(async move { + let client_ref = unsafe { &mut (*client).0 }; + client_ref.receive_notification_with_timeout(interval).await + }); + + match res { + Ok(name) => match CString::new(name) { + Ok(c_string) => { + unsafe { *name_out = c_string.into_raw() }; + null_mut() + } + Err(_) => ffi_err!(IdeviceError::FfiInvalidString), + }, + Err(e) => ffi_err!(e), + } +} + +/// Frees a string returned by notification_proxy_receive +/// +/// # Safety +/// `s` must be a valid pointer returned from `notification_proxy_receive` +#[unsafe(no_mangle)] +pub unsafe extern "C" fn notification_proxy_free_string(s: *mut c_char) { + if !s.is_null() { + let _ = unsafe { CString::from_raw(s) }; + } +} + +/// Frees a handle +/// +/// # Arguments +/// * [`handle`] - The handle to free +/// +/// # Safety +/// `handle` must be a valid pointer to the handle that was allocated by this library, +/// or NULL (in which case this function does nothing) +#[unsafe(no_mangle)] +pub unsafe extern "C" fn notification_proxy_client_free( + handle: *mut NotificationProxyClientHandle, +) { + if !handle.is_null() { + tracing::debug!("Freeing notification_proxy_client"); + let _ = unsafe { Box::from_raw(handle) }; + } +} diff --git a/idevice/Cargo.toml b/idevice/Cargo.toml index c929127..3a448f8 100644 --- a/idevice/Cargo.toml +++ b/idevice/Cargo.toml @@ -97,6 +97,7 @@ misagent = [] mobile_image_mounter = ["dep:sha2"] mobileactivationd = ["dep:reqwest"] mobilebackup2 = [] +notification_proxy = ["tokio/macros", "tokio/time", "dep:async-stream", "dep:futures"] location_simulation = [] pair = ["chrono/default", "tokio/time", "dep:sha2", "dep:rsa", "dep:x509-cert"] pcapd = [] @@ -138,6 +139,7 @@ full = [ "mobile_image_mounter", "mobileactivationd", "mobilebackup2", + "notification_proxy", "pair", "pcapd", "preboard_service", diff --git a/idevice/src/lib.rs b/idevice/src/lib.rs index 5081f33..ba62ed0 100644 --- a/idevice/src/lib.rs +++ b/idevice/src/lib.rs @@ -865,6 +865,10 @@ pub enum IdeviceError { #[error("Developer mode is not enabled")] DeveloperModeNotEnabled = -68, + + #[cfg(feature = "notification_proxy")] + #[error("notification proxy died")] + NotificationProxyDeath = -69, } impl IdeviceError { @@ -1030,6 +1034,9 @@ impl IdeviceError { #[cfg(feature = "installation_proxy")] IdeviceError::MalformedPackageArchive(_) => -67, IdeviceError::DeveloperModeNotEnabled => -68, + + #[cfg(feature = "notification_proxy")] + IdeviceError::NotificationProxyDeath => -69, } } } diff --git a/idevice/src/services/mod.rs b/idevice/src/services/mod.rs index e795578..802b837 100644 --- a/idevice/src/services/mod.rs +++ b/idevice/src/services/mod.rs @@ -35,6 +35,8 @@ pub mod mobile_image_mounter; pub mod mobileactivationd; #[cfg(feature = "mobilebackup2")] pub mod mobilebackup2; +#[cfg(feature = "notification_proxy")] +pub mod notification_proxy; #[cfg(feature = "syslog_relay")] pub mod os_trace_relay; #[cfg(feature = "pcapd")] diff --git a/idevice/src/services/notification_proxy.rs b/idevice/src/services/notification_proxy.rs new file mode 100644 index 0000000..e389a43 --- /dev/null +++ b/idevice/src/services/notification_proxy.rs @@ -0,0 +1,212 @@ +//! iOS Device Notification Proxy Service +//! +//! Based on libimobiledevice's notification_proxy implementation +//! +//! Common notification identifiers: +//! Full list: include/libimobiledevice/notification_proxy.h +//! +//! - Notifications that can be sent (PostNotification): +//! - `com.apple.itunes-mobdev.syncWillStart` - Sync will start +//! - `com.apple.itunes-mobdev.syncDidStart` - Sync started +//! - `com.apple.itunes-mobdev.syncDidFinish` - Sync finished +//! - `com.apple.itunes-mobdev.syncLockRequest` - Request sync lock +//! +//! - Notifications that can be observed (ObserveNotification): +//! - `com.apple.itunes-client.syncCancelRequest` - Cancel sync request +//! - `com.apple.itunes-client.syncSuspendRequest` - Suspend sync +//! - `com.apple.itunes-client.syncResumeRequest` - Resume sync +//! - `com.apple.mobile.lockdown.phone_number_changed` - Phone number changed +//! - `com.apple.mobile.lockdown.device_name_changed` - Device name changed +//! - `com.apple.mobile.lockdown.timezone_changed` - Timezone changed +//! - `com.apple.mobile.lockdown.trusted_host_attached` - Trusted host attached +//! - `com.apple.mobile.lockdown.host_detached` - Host detached +//! - `com.apple.mobile.lockdown.host_attached` - Host attached +//! - `com.apple.mobile.lockdown.registration_failed` - Registration failed +//! - `com.apple.mobile.lockdown.activation_state` - Activation state +//! - `com.apple.mobile.lockdown.brick_state` - Brick state +//! - `com.apple.mobile.lockdown.disk_usage_changed` - Disk usage (iOS 4.0+) +//! - `com.apple.mobile.data_sync.domain_changed` - Data sync domain changed +//! - `com.apple.mobile.application_installed` - App installed +//! - `com.apple.mobile.application_uninstalled` - App uninstalled + +use std::pin::Pin; + +use futures::Stream; +use tracing::warn; + +use crate::{Idevice, IdeviceError, IdeviceService, obf}; + +/// Client for interacting with the iOS notification proxy service +/// +/// The notification proxy service provides a mechanism to observe and post +/// system notifications. +/// +/// Use `observe_notification` to register for events, then `receive_notification` +/// to wait for them. +#[derive(Debug)] +pub struct NotificationProxyClient { + /// The underlying device connection with established notification_proxy service + pub idevice: Idevice, +} + +impl IdeviceService for NotificationProxyClient { + /// Returns the notification proxy service name as registered with lockdownd + fn service_name() -> std::borrow::Cow<'static, str> { + obf!("com.apple.mobile.notification_proxy") + } + async fn from_stream(idevice: Idevice) -> Result { + Ok(Self::new(idevice)) + } +} + +impl NotificationProxyClient { + /// Creates a new notification proxy client from an existing device connection + /// + /// # Arguments + /// * `idevice` - Pre-established device connection + pub fn new(idevice: Idevice) -> Self { + Self { idevice } + } + + /// Posts a notification to the device + /// + /// # Arguments + /// * `notification_name` - Name of the notification to post + /// + /// # Errors + /// Returns `IdeviceError` if the notification fails to send + pub async fn post_notification( + &mut self, + notification_name: impl Into, + ) -> Result<(), IdeviceError> { + let request = crate::plist!({ + "Command": "PostNotification", + "Name": notification_name.into() + }); + self.idevice.send_plist(request).await + } + + /// Registers to observe a specific notification + /// + /// After calling this, use `receive_notification` to wait for events. + /// + /// # Arguments + /// * `notification_name` - Name of the notification to observe + /// + /// # Errors + /// Returns `IdeviceError` if the registration fails + pub async fn observe_notification( + &mut self, + notification_name: impl Into, + ) -> Result<(), IdeviceError> { + let request = crate::plist!({ + "Command": "ObserveNotification", + "Name": notification_name.into() + }); + self.idevice.send_plist(request).await + } + + /// Registers to observe multiple notifications at once + /// + /// # Arguments + /// * `notification_names` - Slice of notification names to observe + /// + /// # Errors + /// Returns `IdeviceError` if any registration fails + pub async fn observe_notifications( + &mut self, + notification_names: &[&str], + ) -> Result<(), IdeviceError> { + for name in notification_names { + self.observe_notification(*name).await?; + } + Ok(()) + } + + /// Waits for and receives the next notification from the device + /// + /// # Returns + /// The name of the received notification + /// + /// # Errors + /// - `NotificationProxyDeath` if the proxy connection died + /// - `UnexpectedResponse` if the response format is invalid + pub async fn receive_notification(&mut self) -> Result { + let response = self.idevice.read_plist().await?; + + match response.get("Command").and_then(|c| c.as_string()) { + Some("RelayNotification") => match response.get("Name").and_then(|n| n.as_string()) { + Some(name) => Ok(name.to_string()), + None => Err(IdeviceError::UnexpectedResponse), + }, + Some("ProxyDeath") => { + warn!("NotificationProxy died!"); + Err(IdeviceError::NotificationProxyDeath) + } + _ => Err(IdeviceError::UnexpectedResponse), + } + } + + /// Waits for a notification with a timeout + /// + /// # Arguments + /// * `interval` - Timeout in seconds to wait for a notification + /// + /// # Returns + /// The name of the received notification + /// + /// # Errors + /// - `NotificationProxyDeath` if the proxy connection died + /// - `UnexpectedResponse` if the response format is invalid + /// - `HeartbeatTimeout` if no notification received before interval + pub async fn receive_notification_with_timeout( + &mut self, + interval: u64, + ) -> Result { + tokio::select! { + result = self.receive_notification() => result, + _ = tokio::time::sleep(tokio::time::Duration::from_secs(interval)) => { + Err(IdeviceError::HeartbeatTimeout) + } + } + } + + /// Continuous stream of notifications. + pub fn into_stream( + mut self, + ) -> Pin> + Send>> { + Box::pin(async_stream::try_stream! { + loop { + let response = self.idevice.read_plist().await?; + + match response.get("Command").and_then(|c| c.as_string()) { + Some("RelayNotification") => { + match response.get("Name").and_then(|n| n.as_string()) { + Some(name) => yield name.to_string(), + None => Err(IdeviceError::UnexpectedResponse)?, + } + } + Some("ProxyDeath") => { + warn!("NotificationProxy died!"); + Err(IdeviceError::NotificationProxyDeath)?; + } + _ => Err(IdeviceError::UnexpectedResponse)?, + } + } + }) + } + + /// Shuts down the notification proxy connection + /// + /// # Errors + /// Returns `IdeviceError` if the shutdown command fails to send + pub async fn shutdown(&mut self) -> Result<(), IdeviceError> { + let request = crate::plist!({ + "Command": "Shutdown" + }); + self.idevice.send_plist(request).await?; + // Best-effort: wait for ProxyDeath ack + let _ = self.idevice.read_plist().await; + Ok(()) + } +} diff --git a/tools/src/main.rs b/tools/src/main.rs index 4bd3d77..db70ea4 100644 --- a/tools/src/main.rs +++ b/tools/src/main.rs @@ -33,6 +33,7 @@ mod lockdown; mod misagent; mod mobilebackup2; mod mounter; +mod notification_proxy_client; mod notifications; mod os_trace_relay; mod pair; @@ -113,6 +114,7 @@ async fn main() { .with_subcommand("mobilebackup2", mobilebackup2::register()) .with_subcommand("mounter", mounter::register()) .with_subcommand("notifications", notifications::register()) + .with_subcommand("notification_proxy", notification_proxy_client::register()) .with_subcommand("os_trace_relay", os_trace_relay::register()) .with_subcommand("pair", pair::register()) .with_subcommand("pcapd", pcapd::register()) @@ -214,6 +216,9 @@ async fn main() { "notifications" => { notifications::main(sub_args, provider).await; } + "notification_proxy" => { + notification_proxy_client::main(sub_args, provider).await; + } "os_trace_relay" => { os_trace_relay::main(sub_args, provider).await; } diff --git a/tools/src/notification_proxy_client.rs b/tools/src/notification_proxy_client.rs new file mode 100644 index 0000000..9559d62 --- /dev/null +++ b/tools/src/notification_proxy_client.rs @@ -0,0 +1,85 @@ +// Jackson Coxson + +use idevice::{ + IdeviceService, notification_proxy::NotificationProxyClient, provider::IdeviceProvider, +}; +use jkcli::{CollectedArguments, JkArgument, JkCommand}; + +pub fn register() -> JkCommand { + JkCommand::new() + .help("Notification proxy") + .with_subcommand( + "observe", + JkCommand::new() + .help("Observe notifications from the device") + .with_argument( + JkArgument::new() + .with_help("The notification ID to observe") + .required(true), + ), + ) + .with_subcommand( + "post", + JkCommand::new() + .help("Post a notification to the device") + .with_argument( + JkArgument::new() + .with_help("The notification ID to post") + .required(true), + ), + ) + .subcommand_required(true) +} + +pub async fn main(arguments: &CollectedArguments, provider: Box) { + let mut client = NotificationProxyClient::connect(&*provider) + .await + .expect("Unable to connect to notification proxy"); + + let (subcommand, sub_args) = arguments.first_subcommand().unwrap(); + let mut sub_args = sub_args.clone(); + + match subcommand.as_str() { + "observe" => { + let input: String = sub_args + .next_argument::() + .expect("No notification ID passed"); + + let notifications: Vec<&str> = input.split_whitespace().collect(); + client + .observe_notifications(¬ifications) + .await + .expect("Failed to observe notifications"); + + loop { + tokio::select! { + _ = tokio::signal::ctrl_c() => { + println!("\nShutdown signal received, exiting."); + break; + } + + result = client.receive_notification() => { + match result { + Ok(notif) => println!("Received notification: {}", notif), + Err(e) => { + eprintln!("Failed to receive notification: {}", e); + break; + } + } + } + } + } + } + "post" => { + let notification: String = sub_args + .next_argument::() + .expect("No notification ID passed"); + + client + .post_notification(¬ification) + .await + .expect("Failed to post notification"); + } + _ => unreachable!(), + } +} From c246362f5442ae7320d4a24251e2ff05c6aece00 Mon Sep 17 00:00:00 2001 From: neo Date: Sat, 14 Feb 2026 15:18:37 -0500 Subject: [PATCH 33/34] chore(readme): update (#68) * feat(springboard): add get_icon subcommand * chore: update readme --- README.md | 25 ++++++++++++++----------- tools/src/springboardservices.rs | 31 +++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 0cbd84b..16d73e0 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,8 @@ To keep dependency bloat and compile time down, everything is contained in featu |------------------------|-----------------------------------------------------------------------------| | `afc` | Apple File Conduit for file system access.| | `amfi` | Apple mobile file integrity service | +| `bt_packet_logger` | Capture Bluetooth packets. | +| `companion_proxy` | Manage paired Apple Watches. | | `core_device_proxy` | Start a secure tunnel to access protected services. | | `crashreportcopymobile`| Copy crash reports.| | `debug_proxy` | Send GDB commands to the device.| @@ -55,13 +57,19 @@ To keep dependency bloat and compile time down, everything is contained in featu | `heartbeat` | Maintain a heartbeat connection.| | `house_arrest` | Manage files in app containers | | `installation_proxy` | Manage app installation and uninstallation.| -| `springboardservices` | Control SpringBoard (e.g. UI interactions). Partial support.| -| `misagent` | Manage provisioning profiles on the device.| -| `mobilebackup2` | Manage backups.| -| `mobile_image_mounter` | Manage DDI images.| +| `installcoordination_proxy` | Manage app installation coordination.| | `location_simulation` | Simulate GPS locations on the device.| +| `misagent` | Manage provisioning profiles on the device.| +| `mobile_image_mounter` | Manage DDI images.| +| `mobileactivationd` | Activate/Deactivate device.| +| `mobilebackup2` | Manage backups.| | `pair` | Pair the device.| -| `syslog_relay` | Relay system logs from the device | +| `pcapd` | Capture network packets.| +| `preboard_service` | Interface with Preboard.| +| `restore_service` | Restore service (recovery/reboot).| +| `screenshotr` | Take screenshots.| +| `springboardservices` | Control SpringBoard (icons, wallpaper, orientation, etc.).| +| `syslog_relay` | Relay system logs and OS trace logs from the device. | | `tcp` | Connect to devices over TCP.| | `tunnel_tcp_stack` | Naive in-process TCP stack for `core_device_proxy`.| | `tss` | Make requests to Apple's TSS servers. Partial support.| @@ -73,16 +81,11 @@ To keep dependency bloat and compile time down, everything is contained in featu Finish the following: -- springboard +- webinspector Implement the following: -- companion_proxy -- diagnostics -- mobilebackup2 - notification_proxy -- screenshot -- webinspector As this project is done in my free time within my busy schedule, there is no ETA for any of these. Feel free to contribute or donate! diff --git a/tools/src/springboardservices.rs b/tools/src/springboardservices.rs index d4efbf4..94d24cb 100644 --- a/tools/src/springboardservices.rs +++ b/tools/src/springboardservices.rs @@ -53,6 +53,21 @@ pub fn register() -> JkCommand { "get_homescreen_icon_metrics", JkCommand::new().help("Gets home screen icon layout metrics"), ) + .with_subcommand( + "get_icon", + JkCommand::new() + .help("Gets an app's icon as PNG") + .with_argument( + JkArgument::new() + .with_help("Bundle identifier (e.g. com.apple.Maps)") + .required(true), + ) + .with_flag( + JkFlag::new("save") + .with_help("Path to save the icon PNG file, or icon.png by default") + .with_argument(JkArgument::new().required(true)), + ), + ) .subcommand_required(true) } @@ -125,6 +140,22 @@ pub async fn main(arguments: &CollectedArguments, provider: Box { + let bundle_id = sub_args.next_argument::().unwrap(); + + let icon_data = sbc + .get_icon_pngdata(bundle_id) + .await + .expect("Failed to get icon"); + + let save_path = sub_args + .get_flag::("save") + .unwrap_or("icon.png".to_string()); + + tokio::fs::write(&save_path, icon_data) + .await + .expect("Failed to save icon"); + } _ => unreachable!(), } } From b459eebe9dc882ad88867795f373a147514bb37b Mon Sep 17 00:00:00 2001 From: khcrysalis <97859147+khcrysalis@users.noreply.github.com> Date: Sat, 14 Feb 2026 12:19:32 -0800 Subject: [PATCH 34/34] feat: support `ios-arm64_x86_64-maccatalyst` in xcframework (#69) * feat: support maccatalyst in xcframework * fix: missing ios-macabi targets in ci --- .github/workflows/ci.yml | 2 ++ justfile | 17 +++++++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dcee207..b36dc43 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,6 +35,8 @@ jobs: rustup target add aarch64-apple-ios-sim && \ rustup target add aarch64-apple-darwin && \ rustup target add x86_64-apple-darwin && \ + rustup target add aarch64-apple-ios-macabi && \ + rustup target add x86_64-apple-ios-macabi && \ cargo install --force --locked bindgen-cli - name: Build all Apple targets and examples/tools diff --git a/justfile b/justfile index 1166802..76a6cad 100644 --- a/justfile +++ b/justfile @@ -40,6 +40,9 @@ xcframework: apple-build lipo -create -output swift/libs/idevice-ios-sim.a \ target/aarch64-apple-ios-sim/release/libidevice_ffi.a \ target/x86_64-apple-ios/release/libidevice_ffi.a + lipo -create -output swift/libs/idevice-maccatalyst.a \ + target/aarch64-apple-ios-macabi/release/libidevice_ffi.a \ + target/x86_64-apple-ios-macabi/release/libidevice_ffi.a lipo -create -output swift/libs/idevice-macos.a \ target/aarch64-apple-darwin/release/libidevice_ffi.a \ target/x86_64-apple-darwin/release/libidevice_ffi.a @@ -48,8 +51,9 @@ xcframework: apple-build -library target/aarch64-apple-ios/release/libidevice_ffi.a -headers swift/include \ -library swift/libs/idevice-ios-sim.a -headers swift/include \ -library swift/libs/idevice-macos.a -headers swift/include \ + -library swift/libs/idevice-maccatalyst.a -headers swift/include \ -output swift/IDevice.xcframework - + zip -r swift/bundle.zip swift/IDevice.xcframework openssl dgst -sha256 swift/bundle.zip @@ -68,7 +72,16 @@ apple-build: # requires a Mac BINDGEN_EXTRA_CLANG_ARGS="--sysroot=$(xcrun --sdk iphonesimulator --show-sdk-path)" \ cargo build --release --target x86_64-apple-ios + # Mac Catalyst (arm64) + # AWS-LC has an a hard time compiling for an iOS with macabi target, so we switch to ring. + BINDGEN_EXTRA_CLANG_ARGS="--sysroot=$(xcrun --sdk macosx --show-sdk-path)" \ + cargo build --release --target aarch64-apple-ios-macabi --no-default-features --features "ring full" + + # Mac Catalyst (x86_64) + # AWS-LC has an a hard time compiling for an iOS with macabi target, so we switch to ring. + BINDGEN_EXTRA_CLANG_ARGS="--sysroot=$(xcrun --sdk macosx --show-sdk-path)" \ + cargo build --release --target x86_64-apple-ios-macabi --no-default-features --features "ring full" + # macOS (native) – no special env needed cargo build --release --target aarch64-apple-darwin cargo build --release --target x86_64-apple-darwin -