mirror of
https://github.com/jkcoxson/idevice.git
synced 2026-03-02 06:26:15 +01:00
feat: impl parts of diagnostics and mobilebackup2 (#20)
* feat: add udid cache to idevice * feat: impl diagnostics * feat: impl mobilebackup2 * docs: update README.md * fix: make clippy happy * fix: make linux clippy happy * fix: make linux clippy happy again * fix: make clippy happy again * fix: small updates
This commit is contained in:
@@ -28,20 +28,22 @@ To keep dependency bloat and compile time down, everything is contained in featu
|
||||
| `core_device_proxy` | Start a secure tunnel to access protected services. |
|
||||
| `crashreportcopymobile`| Copy crash reports.|
|
||||
| `debug_proxy` | Send GDB commands to the device.|
|
||||
| `diagnostics_relay` | Access device diagnostics information (IORegistry, MobileGestalt, battery, NAND, device control).|
|
||||
| `dvt` | Access Apple developer tools (e.g. Instruments).|
|
||||
| `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.|
|
||||
| `location_simulation` | Simulate GPS locations on the device.|
|
||||
| `pair` | Pair the device.|
|
||||
| `syslog_relay` | Relay system 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.|
|
||||
| `tunneld` | Interface with [pymobiledevice3](https://github.com/doronz88/pymobiledevice3)’s tunneld. |
|
||||
| `tss` | Make requests to Apple's TSS servers. Partial support.|
|
||||
| `tunneld` | Interface with [pymobiledevice3](https://github.com/doronz88/pymobiledevice3)'s tunneld. |
|
||||
| `usbmuxd` | Connect using the usbmuxd daemon.|
|
||||
| `xpc` | Access protected services via XPC over RSD. |
|
||||
|
||||
|
||||
@@ -75,6 +75,7 @@ installation_proxy = []
|
||||
springboardservices = []
|
||||
misagent = []
|
||||
mobile_image_mounter = ["dep:sha2"]
|
||||
mobilebackup2 = []
|
||||
location_simulation = []
|
||||
pair = ["chrono/default", "tokio/time", "dep:sha2", "dep:rsa", "dep:x509-cert"]
|
||||
obfuscate = ["dep:obfstr"]
|
||||
@@ -109,6 +110,7 @@ full = [
|
||||
"location_simulation",
|
||||
"misagent",
|
||||
"mobile_image_mounter",
|
||||
"mobilebackup2",
|
||||
"pair",
|
||||
"restore_service",
|
||||
"rsd",
|
||||
@@ -123,4 +125,4 @@ full = [
|
||||
]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
all-features = true
|
||||
@@ -74,6 +74,11 @@ pub trait IdeviceService: Sized {
|
||||
lockdown
|
||||
.start_session(&provider.get_pairing_file().await?)
|
||||
.await?;
|
||||
// Best-effort fetch UDID for downstream defaults (e.g., MobileBackup2 Target/Source identifiers)
|
||||
let udid_value = match lockdown.get_value(Some("UniqueDeviceID"), None).await {
|
||||
Ok(v) => v.as_string().map(|s| s.to_string()),
|
||||
Err(_) => None,
|
||||
};
|
||||
|
||||
let (port, ssl) = lockdown.start_service(Self::service_name()).await?;
|
||||
|
||||
@@ -84,6 +89,10 @@ pub trait IdeviceService: Sized {
|
||||
.await?;
|
||||
}
|
||||
|
||||
if let Some(udid) = udid_value {
|
||||
idevice.set_udid(udid);
|
||||
}
|
||||
|
||||
Self::from_stream(idevice).await
|
||||
}
|
||||
|
||||
@@ -123,6 +132,8 @@ pub struct Idevice {
|
||||
socket: Option<Box<dyn ReadWrite>>,
|
||||
/// Unique label identifying this connection
|
||||
label: String,
|
||||
/// Cached device UDID for convenience in higher-level protocols
|
||||
udid: Option<String>,
|
||||
}
|
||||
|
||||
impl Idevice {
|
||||
@@ -135,6 +146,7 @@ impl Idevice {
|
||||
Self {
|
||||
socket: Some(socket),
|
||||
label: label.into(),
|
||||
udid: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,6 +154,16 @@ impl Idevice {
|
||||
self.socket
|
||||
}
|
||||
|
||||
/// Sets cached UDID
|
||||
pub fn set_udid(&mut self, udid: impl Into<String>) {
|
||||
self.udid = Some(udid.into());
|
||||
}
|
||||
|
||||
/// Returns cached UDID if available
|
||||
pub fn udid(&self) -> Option<&str> {
|
||||
self.udid.as_deref()
|
||||
}
|
||||
|
||||
/// Queries the device type
|
||||
///
|
||||
/// Sends a QueryType request and parses the response
|
||||
|
||||
@@ -20,7 +20,7 @@ impl IdeviceService for DiagnosticsRelayClient {
|
||||
}
|
||||
|
||||
impl DiagnosticsRelayClient {
|
||||
/// Creates a new client from an existing device connection
|
||||
/// Creates a new client from an existing device connection
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `idevice` - Pre-established device connection
|
||||
@@ -74,7 +74,223 @@ impl DiagnosticsRelayClient {
|
||||
.and_then(|x| x.into_dictionary())
|
||||
.and_then(|mut x| x.remove("IORegistry"))
|
||||
.and_then(|x| x.into_dictionary());
|
||||
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
/// Requests MobileGestalt information from the device
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `keys` - Optional list of specific keys to request. If None, requests all available keys
|
||||
///
|
||||
/// # Returns
|
||||
/// A dictionary containing the requested MobileGestalt information
|
||||
pub async fn mobilegestalt(
|
||||
&mut self,
|
||||
keys: Option<Vec<String>>,
|
||||
) -> Result<Option<plist::Dictionary>, IdeviceError> {
|
||||
let mut req = plist::Dictionary::new();
|
||||
req.insert("Request".into(), "MobileGestalt".into());
|
||||
|
||||
if let Some(keys) = keys {
|
||||
let keys_array: Vec<plist::Value> = keys.into_iter().map(|k| k.into()).collect();
|
||||
req.insert("MobileGestaltKeys".into(), plist::Value::Array(keys_array));
|
||||
}
|
||||
|
||||
self.idevice
|
||||
.send_plist(plist::Value::Dictionary(req))
|
||||
.await?;
|
||||
let mut res = self.idevice.read_plist().await?;
|
||||
|
||||
match res.get("Status").and_then(|x| x.as_string()) {
|
||||
Some("Success") => {}
|
||||
_ => {
|
||||
return Err(IdeviceError::UnexpectedResponse);
|
||||
}
|
||||
}
|
||||
|
||||
let res = res
|
||||
.remove("Diagnostics")
|
||||
.and_then(|x| x.into_dictionary());
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// Requests gas gauge information from the device
|
||||
///
|
||||
/// # Returns
|
||||
/// A dictionary containing gas gauge (battery) information
|
||||
pub async fn gasguage(&mut self) -> Result<Option<plist::Dictionary>, IdeviceError> {
|
||||
let mut req = plist::Dictionary::new();
|
||||
req.insert("Request".into(), "GasGauge".into());
|
||||
|
||||
self.idevice
|
||||
.send_plist(plist::Value::Dictionary(req))
|
||||
.await?;
|
||||
let mut res = self.idevice.read_plist().await?;
|
||||
|
||||
match res.get("Status").and_then(|x| x.as_string()) {
|
||||
Some("Success") => {}
|
||||
_ => {
|
||||
return Err(IdeviceError::UnexpectedResponse);
|
||||
}
|
||||
}
|
||||
|
||||
let res = res
|
||||
.remove("Diagnostics")
|
||||
.and_then(|x| x.into_dictionary());
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// Requests NAND information from the device
|
||||
///
|
||||
/// # Returns
|
||||
/// A dictionary containing NAND flash information
|
||||
pub async fn nand(&mut self) -> Result<Option<plist::Dictionary>, IdeviceError> {
|
||||
let mut req = plist::Dictionary::new();
|
||||
req.insert("Request".into(), "NAND".into());
|
||||
|
||||
self.idevice
|
||||
.send_plist(plist::Value::Dictionary(req))
|
||||
.await?;
|
||||
let mut res = self.idevice.read_plist().await?;
|
||||
|
||||
match res.get("Status").and_then(|x| x.as_string()) {
|
||||
Some("Success") => {}
|
||||
_ => {
|
||||
return Err(IdeviceError::UnexpectedResponse);
|
||||
}
|
||||
}
|
||||
|
||||
let res = res
|
||||
.remove("Diagnostics")
|
||||
.and_then(|x| x.into_dictionary());
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// Requests all available diagnostics information
|
||||
///
|
||||
/// # Returns
|
||||
/// A dictionary containing all diagnostics information
|
||||
pub async fn all(&mut self) -> Result<Option<plist::Dictionary>, IdeviceError> {
|
||||
let mut req = plist::Dictionary::new();
|
||||
req.insert("Request".into(), "All".into());
|
||||
|
||||
self.idevice
|
||||
.send_plist(plist::Value::Dictionary(req))
|
||||
.await?;
|
||||
let mut res = self.idevice.read_plist().await?;
|
||||
|
||||
match res.get("Status").and_then(|x| x.as_string()) {
|
||||
Some("Success") => {}
|
||||
_ => {
|
||||
return Err(IdeviceError::UnexpectedResponse);
|
||||
}
|
||||
}
|
||||
|
||||
let res = res
|
||||
.remove("Diagnostics")
|
||||
.and_then(|x| x.into_dictionary());
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// Restarts the device
|
||||
///
|
||||
/// # Returns
|
||||
/// Result indicating success or failure
|
||||
pub async fn restart(&mut self) -> Result<(), IdeviceError> {
|
||||
let mut req = plist::Dictionary::new();
|
||||
req.insert("Request".into(), "Restart".into());
|
||||
|
||||
self.idevice
|
||||
.send_plist(plist::Value::Dictionary(req))
|
||||
.await?;
|
||||
let res = self.idevice.read_plist().await?;
|
||||
|
||||
match res.get("Status").and_then(|x| x.as_string()) {
|
||||
Some("Success") => Ok(()),
|
||||
_ => Err(IdeviceError::UnexpectedResponse),
|
||||
}
|
||||
}
|
||||
|
||||
/// Shuts down the device
|
||||
///
|
||||
/// # Returns
|
||||
/// Result indicating success or failure
|
||||
pub async fn shutdown(&mut self) -> Result<(), IdeviceError> {
|
||||
let mut req = plist::Dictionary::new();
|
||||
req.insert("Request".into(), "Shutdown".into());
|
||||
|
||||
self.idevice
|
||||
.send_plist(plist::Value::Dictionary(req))
|
||||
.await?;
|
||||
let res = self.idevice.read_plist().await?;
|
||||
|
||||
match res.get("Status").and_then(|x| x.as_string()) {
|
||||
Some("Success") => Ok(()),
|
||||
_ => Err(IdeviceError::UnexpectedResponse),
|
||||
}
|
||||
}
|
||||
|
||||
/// Puts the device to sleep
|
||||
///
|
||||
/// # Returns
|
||||
/// Result indicating success or failure
|
||||
pub async fn sleep(&mut self) -> Result<(), IdeviceError> {
|
||||
let mut req = plist::Dictionary::new();
|
||||
req.insert("Request".into(), "Sleep".into());
|
||||
|
||||
self.idevice
|
||||
.send_plist(plist::Value::Dictionary(req))
|
||||
.await?;
|
||||
let res = self.idevice.read_plist().await?;
|
||||
|
||||
match res.get("Status").and_then(|x| x.as_string()) {
|
||||
Some("Success") => Ok(()),
|
||||
_ => Err(IdeviceError::UnexpectedResponse),
|
||||
}
|
||||
}
|
||||
|
||||
/// Requests WiFi diagnostics from the device
|
||||
pub async fn wifi(&mut self) -> Result<Option<plist::Dictionary>, IdeviceError> {
|
||||
let mut req = plist::Dictionary::new();
|
||||
req.insert("Request".into(), "WiFi".into());
|
||||
|
||||
self.idevice
|
||||
.send_plist(plist::Value::Dictionary(req))
|
||||
.await?;
|
||||
let mut res = self.idevice.read_plist().await?;
|
||||
|
||||
match res.get("Status").and_then(|x| x.as_string()) {
|
||||
Some("Success") => {}
|
||||
_ => {
|
||||
return Err(IdeviceError::UnexpectedResponse);
|
||||
}
|
||||
}
|
||||
|
||||
let res = res
|
||||
.remove("Diagnostics")
|
||||
.and_then(|x| x.into_dictionary());
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// Sends Goodbye request signaling end of communication
|
||||
pub async fn goodbye(&mut self) -> Result<(), IdeviceError> {
|
||||
let mut req = plist::Dictionary::new();
|
||||
req.insert("Request".into(), "Goodbye".into());
|
||||
self.idevice
|
||||
.send_plist(plist::Value::Dictionary(req))
|
||||
.await?;
|
||||
let res = self.idevice.read_plist().await?;
|
||||
match res.get("Status").and_then(|x| x.as_string()) {
|
||||
Some("Success") => Ok(()),
|
||||
Some("UnknownRequest") => Err(IdeviceError::UnexpectedResponse),
|
||||
_ => Err(IdeviceError::UnexpectedResponse),
|
||||
}
|
||||
}
|
||||
}
|
||||
1115
idevice/src/services/mobilebackup2.rs
Normal file
1115
idevice/src/services/mobilebackup2.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -27,6 +27,8 @@ pub mod lockdown;
|
||||
pub mod misagent;
|
||||
#[cfg(feature = "mobile_image_mounter")]
|
||||
pub mod mobile_image_mounter;
|
||||
#[cfg(feature = "mobilebackup2")]
|
||||
pub mod mobilebackup2;
|
||||
#[cfg(feature = "syslog_relay")]
|
||||
pub mod os_trace_relay;
|
||||
#[cfg(feature = "restore_service")]
|
||||
|
||||
@@ -97,6 +97,14 @@ path = "src/restore_service.rs"
|
||||
name = "companion_proxy"
|
||||
path = "src/companion_proxy.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "diagnostics"
|
||||
path = "src/diagnostics.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "mobilebackup2"
|
||||
path = "src/mobilebackup2.rs"
|
||||
|
||||
[dependencies]
|
||||
idevice = { path = "../idevice", features = ["full"], default-features = false }
|
||||
tokio = { version = "1.43", features = ["full"] }
|
||||
|
||||
300
tools/src/diagnostics.rs
Normal file
300
tools/src/diagnostics.rs
Normal file
@@ -0,0 +1,300 @@
|
||||
// Jackson Coxson
|
||||
// idevice Rust implementation of libimobiledevice's idevicediagnostics
|
||||
|
||||
use clap::{Arg, Command, ArgMatches};
|
||||
use idevice::{services::diagnostics_relay::DiagnosticsRelayClient, IdeviceService};
|
||||
|
||||
mod common;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
env_logger::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)")
|
||||
)
|
||||
.arg(
|
||||
Arg::new("name")
|
||||
.long("name")
|
||||
.value_name("NAME")
|
||||
.help("Entry name to filter by")
|
||||
)
|
||||
.arg(
|
||||
Arg::new("class")
|
||||
.long("class")
|
||||
.value_name("CLASS")
|
||||
.help("Entry class to filter by")
|
||||
)
|
||||
)
|
||||
.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..)
|
||||
)
|
||||
)
|
||||
.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::<String>("udid");
|
||||
let host = matches.get_one::<String>("host");
|
||||
let pairing_file = matches.get_one::<String>("pairing_file");
|
||||
|
||||
let provider =
|
||||
match common::get_provider(udid, host, pairing_file, "idevicediagnostics-jkcoxson").await {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
eprintln!("{e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let mut diagnostics_client = match DiagnosticsRelayClient::connect(&*provider).await {
|
||||
Ok(client) => client,
|
||||
Err(e) => {
|
||||
eprintln!("Unable to connect to diagnostics relay: {e:?}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
match matches.subcommand() {
|
||||
Some(("ioregistry", sub_matches)) => {
|
||||
handle_ioregistry(&mut diagnostics_client, sub_matches).await;
|
||||
}
|
||||
Some(("mobilegestalt", sub_matches)) => {
|
||||
handle_mobilegestalt(&mut diagnostics_client, sub_matches).await;
|
||||
}
|
||||
Some(("gasguage", _)) => {
|
||||
handle_gasguage(&mut diagnostics_client).await;
|
||||
}
|
||||
Some(("nand", _)) => {
|
||||
handle_nand(&mut diagnostics_client).await;
|
||||
}
|
||||
Some(("all", _)) => {
|
||||
handle_all(&mut diagnostics_client).await;
|
||||
}
|
||||
Some(("wifi", _)) => {
|
||||
handle_wifi(&mut diagnostics_client).await;
|
||||
}
|
||||
Some(("restart", _)) => {
|
||||
handle_restart(&mut diagnostics_client).await;
|
||||
}
|
||||
Some(("shutdown", _)) => {
|
||||
handle_shutdown(&mut diagnostics_client).await;
|
||||
}
|
||||
Some(("sleep", _)) => {
|
||||
handle_sleep(&mut diagnostics_client).await;
|
||||
}
|
||||
Some(("goodbye", _)) => {
|
||||
handle_goodbye(&mut diagnostics_client).await;
|
||||
}
|
||||
_ => {
|
||||
eprintln!("No subcommand specified. Use --help for usage information.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_ioregistry(client: &mut DiagnosticsRelayClient, matches: &ArgMatches) {
|
||||
let plane = matches.get_one::<String>("plane").map(|s| s.as_str());
|
||||
let name = matches.get_one::<String>("name").map(|s| s.as_str());
|
||||
let class = matches.get_one::<String>("class").map(|s| s.as_str());
|
||||
|
||||
match client.ioregistry(plane, name, class).await {
|
||||
Ok(Some(data)) => {
|
||||
println!("{data:#?}");
|
||||
}
|
||||
Ok(None) => {
|
||||
println!("No IORegistry data returned");
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to get IORegistry data: {e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_mobilegestalt(client: &mut DiagnosticsRelayClient, matches: &ArgMatches) {
|
||||
let keys = matches.get_many::<String>("keys")
|
||||
.map(|values| values.map(|s| s.to_string()).collect::<Vec<_>>());
|
||||
|
||||
match client.mobilegestalt(keys).await {
|
||||
Ok(Some(data)) => {
|
||||
println!("{data:#?}");
|
||||
}
|
||||
Ok(None) => {
|
||||
println!("No MobileGestalt data returned");
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to get MobileGestalt data: {e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_gasguage(client: &mut DiagnosticsRelayClient) {
|
||||
match client.gasguage().await {
|
||||
Ok(Some(data)) => {
|
||||
println!("{data:#?}");
|
||||
}
|
||||
Ok(None) => {
|
||||
println!("No gas gauge data returned");
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to get gas gauge data: {e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_nand(client: &mut DiagnosticsRelayClient) {
|
||||
match client.nand().await {
|
||||
Ok(Some(data)) => {
|
||||
println!("{data:#?}");
|
||||
}
|
||||
Ok(None) => {
|
||||
println!("No NAND data returned");
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to get NAND data: {e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_all(client: &mut DiagnosticsRelayClient) {
|
||||
match client.all().await {
|
||||
Ok(Some(data)) => {
|
||||
println!("{data:#?}");
|
||||
}
|
||||
Ok(None) => {
|
||||
println!("No diagnostics data returned");
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to get all diagnostics data: {e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_wifi(client: &mut DiagnosticsRelayClient) {
|
||||
match client.wifi().await {
|
||||
Ok(Some(data)) => {
|
||||
println!("{data:#?}");
|
||||
}
|
||||
Ok(None) => {
|
||||
println!("No WiFi diagnostics returned");
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to get WiFi diagnostics: {e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_restart(client: &mut DiagnosticsRelayClient) {
|
||||
match client.restart().await {
|
||||
Ok(()) => {
|
||||
println!("Device restart command sent successfully");
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to restart device: {e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_shutdown(client: &mut DiagnosticsRelayClient) {
|
||||
match client.shutdown().await {
|
||||
Ok(()) => {
|
||||
println!("Device shutdown command sent successfully");
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to shutdown device: {e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_sleep(client: &mut DiagnosticsRelayClient) {
|
||||
match client.sleep().await {
|
||||
Ok(()) => {
|
||||
println!("Device sleep command sent successfully");
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to put device to sleep: {e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_goodbye(client: &mut DiagnosticsRelayClient) {
|
||||
match client.goodbye().await {
|
||||
Ok(()) => println!("Goodbye acknowledged by device"),
|
||||
Err(e) => eprintln!("Goodbye failed: {e:?}"),
|
||||
}
|
||||
}
|
||||
562
tools/src/mobilebackup2.rs
Normal file
562
tools/src/mobilebackup2.rs
Normal file
@@ -0,0 +1,562 @@
|
||||
// Jackson Coxson
|
||||
// Mobile Backup 2 tool for iOS devices
|
||||
|
||||
use clap::{Arg, Command};
|
||||
use idevice::{mobilebackup2::{MobileBackup2Client, RestoreOptions}, IdeviceService};
|
||||
use plist::Dictionary;
|
||||
use std::fs;
|
||||
use std::io::{Read, Write};
|
||||
use std::path::Path;
|
||||
|
||||
mod common;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
env_logger::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")
|
||||
.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"),
|
||||
),
|
||||
)
|
||||
.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"))
|
||||
.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))
|
||||
)
|
||||
.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").required(true))
|
||||
.arg(Arg::new("path").long("path").value_name("REL_PATH").required(true))
|
||||
.arg(Arg::new("password").long("password").value_name("PWD"))
|
||||
)
|
||||
.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"))
|
||||
)
|
||||
.subcommand(
|
||||
Command::new("erase-device")
|
||||
.about("Erase the device via mobilebackup2")
|
||||
.arg(Arg::new("dir").long("dir").value_name("DIR").required(true))
|
||||
)
|
||||
.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::<String>("udid");
|
||||
let host = matches.get_one::<String>("host");
|
||||
let pairing_file = matches.get_one::<String>("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;
|
||||
}
|
||||
};
|
||||
|
||||
let mut backup_client = match MobileBackup2Client::connect(&*provider).await {
|
||||
Ok(client) => client,
|
||||
Err(e) => {
|
||||
eprintln!("Unable to connect to mobilebackup2 service: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
match matches.subcommand() {
|
||||
Some(("info", sub)) => {
|
||||
let dir = sub.get_one::<String>("dir").unwrap();
|
||||
let source = sub.get_one::<String>("source").map(|s| s.as_str());
|
||||
match backup_client.info_from_path(Path::new(dir), source).await {
|
||||
Ok(dict) => {
|
||||
println!("Backup Information:");
|
||||
for (k, v) in dict { println!(" {k}: {v:?}"); }
|
||||
}
|
||||
Err(e) => eprintln!("Failed to get info: {e}"),
|
||||
}
|
||||
}
|
||||
Some(("list", sub)) => {
|
||||
let dir = sub.get_one::<String>("dir").unwrap();
|
||||
let source = sub.get_one::<String>("source").map(|s| s.as_str());
|
||||
match backup_client.list_from_path(Path::new(dir), source).await {
|
||||
Ok(dict) => {
|
||||
println!("List Response:");
|
||||
for (k, v) in dict { println!(" {k}: {v:?}"); }
|
||||
}
|
||||
Err(e) => eprintln!("Failed to list: {e}"),
|
||||
}
|
||||
}
|
||||
Some(("backup", sub_matches)) => {
|
||||
let target = sub_matches.get_one::<String>("target").map(|s| s.as_str());
|
||||
let source = sub_matches.get_one::<String>("source").map(|s| s.as_str());
|
||||
let dir = sub_matches.get_one::<String>("dir").expect("dir is required");
|
||||
|
||||
println!("Starting backup operation...");
|
||||
let res = backup_client
|
||||
.send_request("Backup", target, source, None::<Dictionary>)
|
||||
.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 {
|
||||
eprintln!("Backup failed during DL loop: {e}");
|
||||
} else {
|
||||
println!("Backup flow finished");
|
||||
}
|
||||
}
|
||||
Some(("restore", sub)) => {
|
||||
let dir = sub.get_one::<String>("dir").unwrap();
|
||||
let source = sub.get_one::<String>("source").map(|s| s.as_str());
|
||||
let mut ropts = RestoreOptions::new();
|
||||
if sub.get_flag("no-reboot") { ropts = ropts.with_reboot(false); }
|
||||
if sub.get_flag("no-copy") { ropts = ropts.with_copy(false); }
|
||||
if sub.get_flag("no-settings") { ropts = ropts.with_preserve_settings(false); }
|
||||
if sub.get_flag("system") { ropts = ropts.with_system_files(true); }
|
||||
if sub.get_flag("remove") { ropts = ropts.with_remove_items_not_restored(true); }
|
||||
if let Some(pw) = sub.get_one::<String>("password") { ropts = ropts.with_password(pw); }
|
||||
match backup_client.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::<String>("dir").unwrap();
|
||||
let source = sub.get_one::<String>("source").map(|s| s.as_str());
|
||||
let password = sub.get_one::<String>("password").map(|s| s.as_str());
|
||||
match backup_client.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::<String>("dir").unwrap();
|
||||
let source = sub.get_one::<String>("source").map(|s| s.as_str());
|
||||
let domain = sub.get_one::<String>("domain").unwrap();
|
||||
let rel = sub.get_one::<String>("path").unwrap();
|
||||
let password = sub.get_one::<String>("password").map(|s| s.as_str());
|
||||
match backup_client.extract_from_path(domain, rel, 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::<String>("dir").unwrap();
|
||||
let old = sub.get_one::<String>("old").map(|s| s.as_str());
|
||||
let newv = sub.get_one::<String>("new").map(|s| s.as_str());
|
||||
match backup_client.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::<String>("dir").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 {
|
||||
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 {
|
||||
Ok(is_encrypted) => {
|
||||
println!("Backup encryption: {}", if is_encrypted { "Enabled" } else { "Disabled" });
|
||||
}
|
||||
Err(e) => eprintln!("Failed to check backup encryption: {e}"),
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
println!("No subcommand provided. Use --help for available commands.");
|
||||
}
|
||||
}
|
||||
|
||||
// Disconnect from the service
|
||||
if let Err(e) = backup_client.disconnect().await {
|
||||
eprintln!("Warning: Failed to disconnect cleanly: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
use idevice::services::mobilebackup2::{
|
||||
DL_CODE_ERROR_LOCAL as CODE_ERROR_LOCAL,
|
||||
DL_CODE_FILE_DATA as CODE_FILE_DATA,
|
||||
DL_CODE_SUCCESS as CODE_SUCCESS,
|
||||
};
|
||||
|
||||
async fn process_dl_loop(
|
||||
client: &mut MobileBackup2Client,
|
||||
host_dir: &Path,
|
||||
) -> Result<Option<Dictionary>, idevice::IdeviceError> {
|
||||
loop {
|
||||
let (tag, value) = client.receive_dl_message().await?;
|
||||
match tag.as_str() {
|
||||
"DLMessageDownloadFiles" => {
|
||||
handle_download_files(client, &value, host_dir).await?;
|
||||
}
|
||||
"DLMessageUploadFiles" => {
|
||||
handle_upload_files(client, &value, host_dir).await?;
|
||||
}
|
||||
"DLMessageGetFreeDiskSpace" => {
|
||||
// Minimal implementation: report unknown/zero with success
|
||||
client
|
||||
.send_status_response(0, None, Some(plist::Value::Integer(0u64.into())))
|
||||
.await?;
|
||||
}
|
||||
"DLContentsOfDirectory" => {
|
||||
// Minimal: return empty listing
|
||||
let empty = plist::Value::Dictionary(Dictionary::new());
|
||||
client.send_status_response(0, None, Some(empty)).await?;
|
||||
}
|
||||
"DLMessageCreateDirectory" => {
|
||||
let status = create_directory_from_message(&value, host_dir);
|
||||
client
|
||||
.send_status_response(status, None, None)
|
||||
.await?;
|
||||
}
|
||||
"DLMessageMoveFiles" | "DLMessageMoveItems" => {
|
||||
let status = move_files_from_message(&value, host_dir);
|
||||
client
|
||||
.send_status_response(status, None, Some(plist::Value::Dictionary(Dictionary::new())))
|
||||
.await?;
|
||||
}
|
||||
"DLMessageRemoveFiles" | "DLMessageRemoveItems" => {
|
||||
let status = remove_files_from_message(&value, host_dir);
|
||||
client
|
||||
.send_status_response(status, None, Some(plist::Value::Dictionary(Dictionary::new())))
|
||||
.await?;
|
||||
}
|
||||
"DLMessageCopyItem" => {
|
||||
let status = copy_item_from_message(&value, host_dir);
|
||||
client
|
||||
.send_status_response(status, None, Some(plist::Value::Dictionary(Dictionary::new())))
|
||||
.await?;
|
||||
}
|
||||
"DLMessageProcessMessage" => {
|
||||
// Final status/content: return inner dict
|
||||
if let plist::Value::Array(arr) = value
|
||||
&& let Some(plist::Value::Dictionary(dict)) = arr.get(1)
|
||||
{
|
||||
return Ok(Some(dict.clone()));
|
||||
}
|
||||
return Ok(None);
|
||||
}
|
||||
"DLMessageDisconnect" => {
|
||||
return Ok(None);
|
||||
}
|
||||
other => {
|
||||
eprintln!("Unsupported DL message: {other}");
|
||||
client
|
||||
.send_status_response(-1, Some("Operation not supported"), None)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_download_files(
|
||||
client: &mut MobileBackup2Client,
|
||||
dl_value: &plist::Value,
|
||||
host_dir: &Path,
|
||||
) -> Result<(), idevice::IdeviceError> {
|
||||
// dl_value is an array: ["DLMessageDownloadFiles", [paths...], progress?]
|
||||
let mut err_any = false;
|
||||
if let plist::Value::Array(arr) = dl_value
|
||||
&& arr.len() >= 2
|
||||
&& let Some(plist::Value::Array(files)) = arr.get(1)
|
||||
{
|
||||
for pv in files {
|
||||
if let Some(path) = pv.as_string()
|
||||
&& let Err(e) = send_single_file(client, host_dir, path).await
|
||||
{
|
||||
eprintln!("Failed to send file {path}: {e}");
|
||||
err_any = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// terminating zero dword
|
||||
client
|
||||
.idevice
|
||||
.send_raw(&0u32.to_be_bytes())
|
||||
.await?;
|
||||
// status response
|
||||
if err_any {
|
||||
client
|
||||
.send_status_response(-13, Some("Multi status"), Some(plist::Value::Dictionary(Dictionary::new())))
|
||||
.await
|
||||
} else {
|
||||
client
|
||||
.send_status_response(0, None, Some(plist::Value::Dictionary(Dictionary::new())))
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_single_file(
|
||||
client: &mut MobileBackup2Client,
|
||||
host_dir: &Path,
|
||||
rel_path: &str,
|
||||
) -> Result<(), idevice::IdeviceError> {
|
||||
let full = host_dir.join(rel_path);
|
||||
let path_bytes = rel_path.as_bytes().to_vec();
|
||||
let nlen = (path_bytes.len() as u32).to_be_bytes();
|
||||
client.idevice.send_raw(&nlen).await?;
|
||||
client.idevice.send_raw(&path_bytes).await?;
|
||||
|
||||
let mut f = match std::fs::File::open(&full) {
|
||||
Ok(f) => f,
|
||||
Err(e) => {
|
||||
// send error
|
||||
let desc = e.to_string();
|
||||
let size = (desc.len() as u32 + 1).to_be_bytes();
|
||||
let mut hdr = Vec::with_capacity(5);
|
||||
hdr.extend_from_slice(&size);
|
||||
hdr.push(CODE_ERROR_LOCAL);
|
||||
client.idevice.send_raw(&hdr).await?;
|
||||
client.idevice.send_raw(desc.as_bytes()).await?;
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
let mut buf = [0u8; 32768];
|
||||
loop {
|
||||
let read = f.read(&mut buf).unwrap_or(0);
|
||||
if read == 0 {
|
||||
break;
|
||||
}
|
||||
let size = ((read as u32) + 1).to_be_bytes();
|
||||
let mut hdr = Vec::with_capacity(5);
|
||||
hdr.extend_from_slice(&size);
|
||||
hdr.push(CODE_FILE_DATA);
|
||||
client.idevice.send_raw(&hdr).await?;
|
||||
client.idevice.send_raw(&buf[..read]).await?;
|
||||
}
|
||||
// success trailer
|
||||
let mut ok = [0u8; 5];
|
||||
ok[..4].copy_from_slice(&1u32.to_be_bytes());
|
||||
ok[4] = CODE_SUCCESS;
|
||||
client.idevice.send_raw(&ok).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_upload_files(
|
||||
client: &mut MobileBackup2Client,
|
||||
_dl_value: &plist::Value,
|
||||
host_dir: &Path,
|
||||
) -> Result<(), idevice::IdeviceError> {
|
||||
// Minimal receiver: read pairs of (dir, filename) and block stream
|
||||
// Receive dir name
|
||||
loop {
|
||||
let dlen = read_be_u32(client).await?;
|
||||
if dlen == 0 {
|
||||
break;
|
||||
}
|
||||
let dname = read_exact_string(client, dlen as usize).await?;
|
||||
let flen = read_be_u32(client).await?;
|
||||
if flen == 0 {
|
||||
break;
|
||||
}
|
||||
let fname = read_exact_string(client, flen as usize).await?;
|
||||
let dst = host_dir.join(&fname);
|
||||
if let Some(parent) = dst.parent() {
|
||||
let _ = fs::create_dir_all(parent);
|
||||
}
|
||||
let mut file = std::fs::File::create(&dst).map_err(|e| idevice::IdeviceError::InternalError(e.to_string()))?;
|
||||
loop {
|
||||
let nlen = read_be_u32(client).await?;
|
||||
if nlen == 0 {
|
||||
break;
|
||||
}
|
||||
let code = read_one(client).await?;
|
||||
if code == CODE_FILE_DATA {
|
||||
let size = (nlen - 1) as usize;
|
||||
let data = read_exact(client, size).await?;
|
||||
file.write_all(&data).map_err(|e| idevice::IdeviceError::InternalError(e.to_string()))?;
|
||||
} else {
|
||||
let _ = read_exact(client, (nlen - 1) as usize).await?;
|
||||
}
|
||||
}
|
||||
let _ = dname; // not used
|
||||
}
|
||||
client
|
||||
.send_status_response(0, None, Some(plist::Value::Dictionary(Dictionary::new())))
|
||||
.await
|
||||
}
|
||||
|
||||
async fn read_be_u32(client: &mut MobileBackup2Client) -> Result<u32, idevice::IdeviceError> {
|
||||
let buf = client.idevice.read_raw(4).await?;
|
||||
Ok(u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]))
|
||||
}
|
||||
|
||||
async fn read_one(client: &mut MobileBackup2Client) -> Result<u8, idevice::IdeviceError> {
|
||||
let buf = client.idevice.read_raw(1).await?;
|
||||
Ok(buf[0])
|
||||
}
|
||||
|
||||
async fn read_exact(client: &mut MobileBackup2Client, size: usize) -> Result<Vec<u8>, idevice::IdeviceError> {
|
||||
client.idevice.read_raw(size).await
|
||||
}
|
||||
|
||||
async fn read_exact_string(client: &mut MobileBackup2Client, size: usize) -> Result<String, idevice::IdeviceError> {
|
||||
let buf = client.idevice.read_raw(size).await?;
|
||||
Ok(String::from_utf8_lossy(&buf).to_string())
|
||||
}
|
||||
|
||||
fn create_directory_from_message(dl_value: &plist::Value, host_dir: &Path) -> i64 {
|
||||
if let plist::Value::Array(arr) = dl_value
|
||||
&& arr.len() >= 2
|
||||
&& let Some(plist::Value::String(dir)) = arr.get(1)
|
||||
{
|
||||
let path = host_dir.join(dir);
|
||||
return match fs::create_dir_all(&path) {
|
||||
Ok(_) => 0,
|
||||
Err(_) => -1,
|
||||
};
|
||||
}
|
||||
-1
|
||||
}
|
||||
|
||||
fn move_files_from_message(dl_value: &plist::Value, host_dir: &Path) -> i64 {
|
||||
if let plist::Value::Array(arr) = dl_value
|
||||
&& arr.len() >= 2
|
||||
&& let Some(plist::Value::Dictionary(map)) = arr.get(1)
|
||||
{
|
||||
for (from, to_v) in map.iter() {
|
||||
if let Some(to) = to_v.as_string() {
|
||||
let old = host_dir.join(from);
|
||||
let newp = host_dir.join(to);
|
||||
if let Some(parent) = newp.parent() {
|
||||
let _ = fs::create_dir_all(parent);
|
||||
}
|
||||
if fs::rename(&old, &newp).is_err() {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
-1
|
||||
}
|
||||
|
||||
fn remove_files_from_message(dl_value: &plist::Value, host_dir: &Path) -> i64 {
|
||||
if let plist::Value::Array(arr) = dl_value
|
||||
&& arr.len() >= 2
|
||||
&& let Some(plist::Value::Array(items)) = arr.get(1)
|
||||
{
|
||||
for it in items {
|
||||
if let Some(p) = it.as_string() {
|
||||
let path = host_dir.join(p);
|
||||
if path.is_dir() {
|
||||
if fs::remove_dir_all(&path).is_err() { return -1; }
|
||||
} else if path.exists() && fs::remove_file(&path).is_err() {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
-1
|
||||
}
|
||||
|
||||
fn copy_item_from_message(dl_value: &plist::Value, host_dir: &Path) -> i64 {
|
||||
if let plist::Value::Array(arr) = dl_value
|
||||
&& arr.len() >= 3
|
||||
&& let (Some(plist::Value::String(src)), Some(plist::Value::String(dst))) = (arr.get(1), arr.get(2))
|
||||
{
|
||||
let from = host_dir.join(src);
|
||||
let to = host_dir.join(dst);
|
||||
if let Some(parent) = to.parent() { let _ = fs::create_dir_all(parent); }
|
||||
if from.is_dir() {
|
||||
// shallow copy: create dir
|
||||
return match fs::create_dir_all(&to) { Ok(_) => 0, Err(_) => -1 };
|
||||
} else {
|
||||
return match fs::copy(&from, &to) { Ok(_) => 0, Err(_) => -1 };
|
||||
}
|
||||
}
|
||||
-1
|
||||
}
|
||||
Reference in New Issue
Block a user