From 0bb5deada84298e50381351fce741d0aba9d72bf Mon Sep 17 00:00:00 2001 From: Ylarod Date: Wed, 13 Aug 2025 21:41:48 +0800 Subject: [PATCH] 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 --- README.md | 6 +- idevice/Cargo.toml | 4 +- idevice/src/lib.rs | 22 + idevice/src/services/diagnostics_relay.rs | 220 +++- idevice/src/services/mobilebackup2.rs | 1115 +++++++++++++++++++++ idevice/src/services/mod.rs | 2 + tools/Cargo.toml | 8 + tools/src/diagnostics.rs | 300 ++++++ tools/src/mobilebackup2.rs | 562 +++++++++++ 9 files changed, 2234 insertions(+), 5 deletions(-) create mode 100644 idevice/src/services/mobilebackup2.rs create mode 100644 tools/src/diagnostics.rs create mode 100644 tools/src/mobilebackup2.rs diff --git a/README.md b/README.md index 2bed136..50a311a 100644 --- a/README.md +++ b/README.md @@ -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. | diff --git a/idevice/Cargo.toml b/idevice/Cargo.toml index 113cc75..6c84b41 100644 --- a/idevice/Cargo.toml +++ b/idevice/Cargo.toml @@ -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 \ No newline at end of file diff --git a/idevice/src/lib.rs b/idevice/src/lib.rs index 3313de8..48003ee 100644 --- a/idevice/src/lib.rs +++ b/idevice/src/lib.rs @@ -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>, /// Unique label identifying this connection label: String, + /// Cached device UDID for convenience in higher-level protocols + udid: Option, } 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) { + 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 diff --git a/idevice/src/services/diagnostics_relay.rs b/idevice/src/services/diagnostics_relay.rs index 7811b85..7b525c5 100644 --- a/idevice/src/services/diagnostics_relay.rs +++ b/idevice/src/services/diagnostics_relay.rs @@ -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>, + ) -> Result, IdeviceError> { + let mut req = plist::Dictionary::new(); + req.insert("Request".into(), "MobileGestalt".into()); + + if let Some(keys) = keys { + let keys_array: Vec = 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, 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, 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, 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, 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), + } + } +} \ No newline at end of file diff --git a/idevice/src/services/mobilebackup2.rs b/idevice/src/services/mobilebackup2.rs new file mode 100644 index 0000000..2276e8a --- /dev/null +++ b/idevice/src/services/mobilebackup2.rs @@ -0,0 +1,1115 @@ +//! iOS Mobile Backup 2 Service Client +//! +//! Provides functionality for interacting with the mobilebackup2 service on iOS devices, +//! which allows creating, restoring, and managing device backups. + +use log::{debug, warn}; +use plist::Dictionary; +use tokio::io::AsyncReadExt; +use std::fs; +use std::io::{Read, Write}; +use std::path::Path; + +use crate::{Idevice, IdeviceError, IdeviceService, obf}; + +/// DeviceLink message codes used in MobileBackup2 binary streams +pub const DL_CODE_SUCCESS: u8 = 0x00; +pub const DL_CODE_ERROR_LOCAL: u8 = 0x06; +pub const DL_CODE_ERROR_REMOTE: u8 = 0x0b; +pub const DL_CODE_FILE_DATA: u8 = 0x0c; + +/// Client for interacting with the iOS mobile backup 2 service +/// +/// This service provides access to device backup functionality including +/// creating backups, restoring from backups, and managing backup data. +pub struct MobileBackup2Client { + /// The underlying device connection with established mobilebackup2 service + pub idevice: Idevice, + /// Protocol version negotiated with the device + pub protocol_version: f64, +} + +impl IdeviceService for MobileBackup2Client { + /// Returns the mobile backup 2 service name as registered with lockdownd + fn service_name() -> std::borrow::Cow<'static, str> { + obf!("com.apple.mobilebackup2") + } + + async fn from_stream(idevice: Idevice) -> Result { + let mut client = Self::new(idevice); + // Perform DeviceLink handshake first + client.dl_version_exchange().await?; + // Perform version exchange after connection + client.version_exchange().await?; + Ok(client) + } +} + +/// Backup message types used in the mobilebackup2 protocol +#[derive(Debug, Clone)] +pub enum BackupMessageType { + /// Request to start a backup operation + BackupMessageTypeBackup, + /// Request to restore from a backup + BackupMessageTypeRestore, + /// Information message + BackupMessageTypeInfo, + /// List available backups + BackupMessageTypeList, + /// Upload files to backup + BackupMessageTypeUploadFiles, + /// Download files from backup + BackupMessageTypeDownloadFiles, + /// Clear backup data + BackupMessageTypeClearBackupData, + /// Move files in backup + BackupMessageTypeMoveFiles, + /// Remove files from backup + BackupMessageTypeRemoveFiles, + /// Create directory in backup + BackupMessageTypeCreateDirectory, + /// Acquire lock for backup operation + BackupMessageTypeAcquireLock, + /// Release lock after backup operation + BackupMessageTypeReleaseLock, + /// Copy item in backup + BackupMessageTypeCopyItem, + /// Disconnect from service + BackupMessageTypeDisconnect, + /// Process message + BackupMessageTypeProcessMessage, + /// Get freespace information + BackupMessageTypeGetFreespace, + /// Factory info + BackupMessageTypeFactoryInfo, + /// Check if backup is encrypted + BackupMessageTypeCheckBackupEncryption, +} + +impl BackupMessageType { + /// Convert message type to string representation + pub fn as_str(&self) -> &'static str { + match self { + // These map to MobileBackup2 request names per libimobiledevice + BackupMessageType::BackupMessageTypeBackup => "Backup", + BackupMessageType::BackupMessageTypeRestore => "Restore", + BackupMessageType::BackupMessageTypeInfo => "Info", + BackupMessageType::BackupMessageTypeList => "List", + // The following are DL control messages and not sent via MessageName + BackupMessageType::BackupMessageTypeUploadFiles => "DLMessageUploadFiles", + BackupMessageType::BackupMessageTypeDownloadFiles => "DLMessageDownloadFiles", + BackupMessageType::BackupMessageTypeClearBackupData => "DLMessageClearBackupData", + BackupMessageType::BackupMessageTypeMoveFiles => "DLMessageMoveFiles", + BackupMessageType::BackupMessageTypeRemoveFiles => "DLMessageRemoveFiles", + BackupMessageType::BackupMessageTypeCreateDirectory => "DLMessageCreateDirectory", + BackupMessageType::BackupMessageTypeAcquireLock => "DLMessageAcquireLock", + BackupMessageType::BackupMessageTypeReleaseLock => "DLMessageReleaseLock", + BackupMessageType::BackupMessageTypeCopyItem => "DLMessageCopyItem", + BackupMessageType::BackupMessageTypeDisconnect => "DLMessageDisconnect", + BackupMessageType::BackupMessageTypeProcessMessage => "DLMessageProcessMessage", + BackupMessageType::BackupMessageTypeGetFreespace => "DLMessageGetFreeDiskSpace", + BackupMessageType::BackupMessageTypeFactoryInfo => "FactoryInfo", + BackupMessageType::BackupMessageTypeCheckBackupEncryption => "CheckBackupEncryption", + } + } +} + +/// Backup information structure +#[derive(Debug, Clone)] +pub struct BackupInfo { + /// Backup UUID + pub uuid: String, + /// Device name + pub device_name: String, + /// Display name + pub display_name: String, + /// Last backup date + pub last_backup_date: Option, + /// Backup version + pub version: String, + /// Whether backup is encrypted + pub is_encrypted: bool, +} + +/// High-level builder for restore options so callers don't need to remember raw keys +#[derive(Debug, Clone)] +pub struct RestoreOptions { + pub reboot: bool, + pub copy: bool, + pub preserve_settings: bool, + pub system_files: bool, + pub remove_items_not_restored: bool, + pub password: Option, +} + +impl Default for RestoreOptions { + fn default() -> Self { + Self { + reboot: true, + copy: true, + preserve_settings: true, + system_files: false, + remove_items_not_restored: false, + password: None, + } + } +} + +impl RestoreOptions { + pub fn new() -> Self { Self::default() } + pub fn with_reboot(mut self, reboot: bool) -> Self { self.reboot = reboot; self } + pub fn with_copy(mut self, copy: bool) -> Self { self.copy = copy; self } + pub fn with_preserve_settings(mut self, preserve: bool) -> Self { self.preserve_settings = preserve; self } + pub fn with_system_files(mut self, system: bool) -> Self { self.system_files = system; self } + pub fn with_remove_items_not_restored(mut self, remove: bool) -> Self { self.remove_items_not_restored = remove; self } + pub fn with_password(mut self, password: impl Into) -> Self { self.password = Some(password.into()); self } + + pub fn to_plist(&self) -> Dictionary { + let mut opts = Dictionary::new(); + opts.insert("RestoreShouldReboot".into(), plist::Value::Boolean(self.reboot)); + opts.insert("RestoreDontCopyBackup".into(), plist::Value::Boolean(!self.copy)); + opts.insert("RestorePreserveSettings".into(), plist::Value::Boolean(self.preserve_settings)); + opts.insert("RestoreSystemFiles".into(), plist::Value::Boolean(self.system_files)); + opts.insert("RemoveItemsNotRestored".into(), plist::Value::Boolean(self.remove_items_not_restored)); + if let Some(pw) = &self.password { + opts.insert("Password".into(), plist::Value::String(pw.clone())); + } + opts + } +} + +impl MobileBackup2Client { + /// Creates a new mobile backup 2 client from an existing device connection + /// + /// # Arguments + /// * `idevice` - Pre-established device connection + pub fn new(idevice: Idevice) -> Self { + Self { + idevice, + protocol_version: 0.0, + } + } + + /// Performs DeviceLink version exchange handshake + /// + /// Sequence: + /// 1) Receive ["DLMessageVersionExchange", major, minor] + /// 2) Send ["DLMessageVersionExchange", "DLVersionsOk", 400] + /// 3) Receive ["DLMessageDeviceReady"] + async fn dl_version_exchange(&mut self) -> Result<(), IdeviceError> { + debug!("Starting DeviceLink version exchange"); + // 1) Receive DLMessageVersionExchange + let (msg, _arr) = self.receive_dl_message().await?; + if msg != "DLMessageVersionExchange" { + warn!("Expected DLMessageVersionExchange, got {msg}"); + return Err(IdeviceError::UnexpectedResponse); + } + + // 2) Send DLVersionsOk with version 400 + let out = vec![ + plist::Value::String("DLMessageVersionExchange".into()), + plist::Value::String("DLVersionsOk".into()), + plist::Value::Integer(400u64.into()), + ]; + self.send_dl_array(out).await?; + + // 3) Receive DLMessageDeviceReady + let (msg2, _arr2) = self.receive_dl_message().await?; + if msg2 != "DLMessageDeviceReady" { + warn!("Expected DLMessageDeviceReady, got {msg2}"); + return Err(IdeviceError::UnexpectedResponse); + } + Ok(()) + } + + /// Sends a raw DL array as binary plist + async fn send_dl_array(&mut self, array: Vec) -> Result<(), IdeviceError> { + self.idevice + .send_bplist(plist::Value::Array(array)) + .await + } + + /// Receives any DL* message and returns (message_tag, full_array_value) + pub async fn receive_dl_message(&mut self) -> Result<(String, plist::Value), IdeviceError> { + if let Some(socket) = &mut self.idevice.socket { + let mut buf = [0u8; 4]; + socket.read_exact(&mut buf).await?; + let len = u32::from_be_bytes(buf); + let mut body = vec![0; len as usize]; + socket.read_exact(&mut body).await?; + let value: plist::Value = plist::from_bytes(&body)?; + if let plist::Value::Array(arr) = &value + && let Some(plist::Value::String(tag)) = arr.first() + { + return Ok((tag.clone(), value)); + } + warn!("Invalid DL message format"); + Err(IdeviceError::UnexpectedResponse) + } else { + Err(IdeviceError::NoEstablishedConnection) + } + } + + /// Performs version exchange with the device + /// + /// This is required by the mobilebackup2 protocol and must be called + /// before any other operations. + /// + /// # Returns + /// `Ok(())` on successful version negotiation + /// + /// # Errors + /// Returns `IdeviceError` if version exchange fails + async fn version_exchange(&mut self) -> Result<(), IdeviceError> { + debug!("Starting mobilebackup2 version exchange"); + + // Send supported protocol versions (matching libimobiledevice) + let mut hello_dict = Dictionary::new(); + let versions = vec![plist::Value::Real(2.0), plist::Value::Real(2.1)]; + hello_dict.insert("SupportedProtocolVersions".into(), plist::Value::Array(versions)); + + self.send_device_link_message("Hello", Some(hello_dict)).await?; + + // Receive response + let response = self.receive_device_link_message("Response").await?; + + // Check for error + if let Some(error_code) = response.get("ErrorCode") + && let Some(code) = error_code.as_unsigned_integer() + && code != 0 + { + warn!("Version exchange failed with error code: {code}"); + return Err(IdeviceError::UnexpectedResponse); + } + + // Get negotiated protocol version + if let Some(version) = response.get("ProtocolVersion").and_then(|v| v.as_real()) { + self.protocol_version = version; + debug!("Negotiated protocol version: {version}"); + } else { + warn!("No protocol version in response"); + return Err(IdeviceError::UnexpectedResponse); + } + + Ok(()) + } + + /// Sends a device link message (DLMessageProcessMessage format) + /// + /// This follows the device_link_service protocol used by mobilebackup2 + /// + /// # Arguments + /// * `message_name` - The message name (e.g., "Hello", "kBackupMessageTypeInfo") + /// * `options` - Optional dictionary of options for the message + /// + /// # Returns + /// `Ok(())` on successful message send + /// + /// # Errors + /// Returns `IdeviceError` if communication fails + async fn send_device_link_message( + &mut self, + message_name: &str, + options: Option, + ) -> Result<(), IdeviceError> { + // Create DLMessageProcessMessage array format + let mut message_array = Vec::new(); + message_array.push(plist::Value::String("DLMessageProcessMessage".into())); + + // Create the actual message dictionary + let mut message_dict = Dictionary::new(); + message_dict.insert("MessageName".into(), message_name.into()); + + if let Some(opts) = options { + for (key, value) in opts { + message_dict.insert(key, value); + } + } + + message_array.push(plist::Value::Dictionary(message_dict)); + + debug!("Sending device link message: {message_name}"); + self.idevice + .send_bplist(plist::Value::Array(message_array)) + .await + } + + /// Receives a device link message and validates the message name + /// + /// Arguments + /// * `expected_message` - The expected message name to validate + /// + /// # Returns + /// The message dictionary on success + /// + /// # Errors + /// Returns `IdeviceError` if communication fails or message name doesn't match + async fn receive_device_link_message(&mut self, expected_message: &str) -> Result { + // Read raw bytes and parse as plist::Value to handle array format + if let Some(socket) = &mut self.idevice.socket { + debug!("Reading response size"); + let mut buf = [0u8; 4]; + socket.read_exact(&mut buf).await?; + let len = u32::from_be_bytes(buf); + let mut buf = vec![0; len as usize]; + socket.read_exact(&mut buf).await?; + let response_value: plist::Value = plist::from_bytes(&buf)?; + + // Parse DLMessageProcessMessage format + if let plist::Value::Array(array) = response_value + && array.len() >= 2 + && let Some(plist::Value::String(dl_message)) = array.first() + && let Some(plist::Value::Dictionary(dict)) = array.get(1) + && dl_message == "DLMessageProcessMessage" + { + // Check MessageName if expected + if !expected_message.is_empty() { + if let Some(message_name) = dict.get("MessageName").and_then(|v| v.as_string()) { + if message_name != expected_message { + warn!("Expected message '{expected_message}', got '{message_name}'"); + return Err(IdeviceError::UnexpectedResponse); + } + } else { + warn!("No MessageName in response"); + return Err(IdeviceError::UnexpectedResponse); + } + } + return Ok(dict.clone()); + } + + warn!("Invalid device link message format"); + Err(IdeviceError::UnexpectedResponse) + } else { + Err(IdeviceError::NoEstablishedConnection) + } + } + + /// Sends a backup message to the device + /// + /// # Arguments + /// * `message_type` - The type of backup message to send + /// * `options` - Optional dictionary of options for the message + /// + /// # Returns + /// `Ok(())` on successful message send + /// + /// # Errors + /// Returns `IdeviceError` if communication fails + async fn send_backup_message( + &mut self, + message_type: BackupMessageType, + options: Option, + ) -> Result<(), IdeviceError> { + self.send_device_link_message(message_type.as_str(), options).await + } + + /// Sends a MobileBackup2 request with proper envelope and identifiers + pub async fn send_request( + &mut self, + request: &str, + target_identifier: Option<&str>, + source_identifier: Option<&str>, + options: Option, + ) -> Result<(), IdeviceError> { + let mut dict = Dictionary::new(); + if let Some(t) = target_identifier { + dict.insert("TargetIdentifier".into(), t.into()); + } + if let Some(s) = source_identifier { + dict.insert("SourceIdentifier".into(), s.into()); + } + if let Some(opts) = options { + dict.insert("Options".into(), plist::Value::Dictionary(opts)); + // Special cases like Unback/EnableCloudBackup are handled by caller if needed + } + self.send_device_link_message(request, Some(dict)).await + } + + /// Sends a DLMessageStatusResponse array + pub async fn send_status_response( + &mut self, + status_code: i64, + status1: Option<&str>, + status2: Option, + ) -> Result<(), IdeviceError> { + let arr = vec![ + plist::Value::String("DLMessageStatusResponse".into()), + plist::Value::Integer(status_code.into()), + plist::Value::String(status1.unwrap_or("___EmptyParameterString___").into()), + status2.unwrap_or_else(|| plist::Value::String("___EmptyParameterString___".into())), + ]; + self.send_dl_array(arr).await + } + + /// Receives a response from the backup service + /// + /// # Returns + /// The response as a plist Dictionary + /// + /// # Errors + /// Returns `IdeviceError` if communication fails or response is malformed + async fn receive_backup_response(&mut self) -> Result { + self.receive_device_link_message("").await + } + + /// Requests device information for backup + /// + /// # Returns + /// A dictionary containing device information + /// + /// # Errors + /// Returns `IdeviceError` if the request fails + pub async fn request_backup_info(&mut self) -> Result { + // Per protocol use MessageName "Info" + self.send_backup_message(BackupMessageType::BackupMessageTypeInfo, None) + .await?; + + let response = self.receive_backup_response().await?; + + // Check for error in response + if let Some(error) = response.get("ErrorCode") { + warn!("Backup info request failed with error: {error:?}"); + return Err(IdeviceError::UnexpectedResponse); + } + + Ok(response) + } + + /// Lists available backups on the device + /// + /// # Returns + /// A vector of backup information + /// + /// # Errors + /// Returns `IdeviceError` if the request fails + pub async fn list_backups(&mut self) -> Result, IdeviceError> { + self.send_backup_message(BackupMessageType::BackupMessageTypeList, None) + .await?; + + let response = self.receive_backup_response().await?; + + // Check for error in response + if let Some(error) = response.get("ErrorCode") { + warn!("List backups request failed with error: {error:?}"); + return Err(IdeviceError::UnexpectedResponse); + } + + let mut backups = Vec::new(); + + if let Some(plist::Value::Array(backup_list)) = response.get("BackupList") { + for backup_item in backup_list { + if let plist::Value::Dictionary(backup_dict) = backup_item { + let uuid = backup_dict + .get("BackupUUID") + .and_then(|v| v.as_string()) + .unwrap_or_default() + .to_string(); + + let device_name = backup_dict + .get("DeviceName") + .and_then(|v| v.as_string()) + .unwrap_or_default() + .to_string(); + + let display_name = backup_dict + .get("DisplayName") + .and_then(|v| v.as_string()) + .unwrap_or_default() + .to_string(); + + let last_backup_date = backup_dict + .get("LastBackupDate") + .and_then(|v| v.as_string()) + .map(|s| s.to_string()); + + let version = backup_dict + .get("Version") + .and_then(|v| v.as_string()) + .unwrap_or("Unknown") + .to_string(); + + let is_encrypted = backup_dict + .get("IsEncrypted") + .and_then(|v| v.as_boolean()) + .unwrap_or(false); + + backups.push(BackupInfo { + uuid, + device_name, + display_name, + last_backup_date, + version, + is_encrypted, + }); + } + } + } + + Ok(backups) + } + + /// Starts a backup operation + /// + /// # Arguments + /// * `target_identifier` - Optional target identifier for the backup + /// * `source_identifier` - Optional source identifier for the backup + /// * `options` - Optional backup options + /// + /// # Returns + /// `Ok(())` on successful backup start + /// + /// # Errors + /// Returns `IdeviceError` if the backup fails to start + pub async fn start_backup( + &mut self, + target_identifier: Option<&str>, + source_identifier: Option<&str>, + options: Option, + ) -> Result<(), IdeviceError> { + self.send_request( + BackupMessageType::BackupMessageTypeBackup.as_str(), + target_identifier, + source_identifier, + options, + ) + .await?; + + let response = self.receive_backup_response().await?; + + // Check for error in response + if let Some(error) = response.get("ErrorCode") { + warn!("Backup start failed with error: {error:?}"); + return Err(IdeviceError::UnexpectedResponse); + } + + debug!("Backup started successfully"); + Ok(()) + } + + /// Starts a restore operation + /// + /// # Arguments + /// * `backup_uuid` - UUID of the backup to restore from + /// * `options` - Optional restore options + /// + /// # Returns + /// `Ok(())` on successful restore start + /// + /// # Errors + /// Returns `IdeviceError` if the restore fails to start + #[deprecated(note = "Use restore_from_path; restore via BackupUUID is not supported by device/mobilebackup2")] + pub async fn start_restore( + &mut self, + _backup_uuid: &str, + options: Option, + ) -> Result<(), IdeviceError> { + let mut opts = options.unwrap_or_default(); + // Align default restore options with pymobiledevice semantics + // Caller-specified values (if any) take precedence. + if !opts.contains_key("RestoreShouldReboot") { + opts.insert("RestoreShouldReboot".into(), plist::Value::Boolean(true)); + } + if !opts.contains_key("RestoreDontCopyBackup") { + // pymobiledevice: copy=True -> RestoreDontCopyBackup=False + opts.insert("RestoreDontCopyBackup".into(), plist::Value::Boolean(false)); + } + if !opts.contains_key("RestorePreserveSettings") { + opts.insert("RestorePreserveSettings".into(), plist::Value::Boolean(true)); + } + if !opts.contains_key("RestoreSystemFiles") { + opts.insert("RestoreSystemFiles".into(), plist::Value::Boolean(false)); + } + if !opts.contains_key("RemoveItemsNotRestored") { + opts.insert("RemoveItemsNotRestored".into(), plist::Value::Boolean(false)); + } + // Avoid borrowing self while sending request + let target_udid_owned = self.idevice.udid().map(|s| s.to_string()); + let target_udid = target_udid_owned.as_deref(); + self.send_request( + BackupMessageType::BackupMessageTypeRestore.as_str(), + // default identifiers to current UDID if available + target_udid, + target_udid, + Some(opts), + ) + .await?; + + let response = self.receive_backup_response().await?; + + // Check for error in response + if let Some(error) = response.get("ErrorCode") { + warn!("Restore start failed with error: {error:?}"); + return Err(IdeviceError::UnexpectedResponse); + } + + debug!("Restore started successfully"); + Ok(()) + } + + /// High-level API: Restore from a local backup directory using DeviceLink file exchange + /// + /// - `backup_root` should point to the backup root directory (which contains the `` subdirectory) + /// - If `source_identifier` is None, the current connected device's UDID will be used by default + /// - `options` should be constructed using the `RestoreOptions` builder; if not provided, defaults will be used + pub async fn restore_from_path( + &mut self, + backup_root: &Path, + source_identifier: Option<&str>, + options: Option, + ) -> Result<(), IdeviceError> { + // Take owned UDID to avoid aliasing borrows + let target_udid_owned = self.idevice.udid().map(|s| s.to_string()); + let target_udid = target_udid_owned.as_deref(); + let source: &str = match source_identifier { + Some(s) => s, + None => target_udid.ok_or(IdeviceError::InvalidHostID)?, + }; + + // 简单存在性校验:backup_root/source 必须存在 + let backup_dir = backup_root.join(source); + if !backup_dir.exists() { + return Err(IdeviceError::NotFound); + } + + let opts = options.unwrap_or_default().to_plist(); + self.send_request( + BackupMessageType::BackupMessageTypeRestore.as_str(), + target_udid, + Some(source), + Some(opts), + ).await?; + + // 进入 DeviceLink 文件交换循环,根目录传入 backup_root(协议请求包含 source 前缀) + let _ = self.process_restore_dl_loop(backup_root).await?; + Ok(()) + } + + async fn process_restore_dl_loop(&mut self, host_dir: &Path) -> Result, IdeviceError> { + loop { + let (tag, value) = self.receive_dl_message().await?; + match tag.as_str() { + "DLMessageDownloadFiles" => { + self.handle_download_files(&value, host_dir).await?; + } + "DLMessageUploadFiles" => { + self.handle_upload_files(&value, host_dir).await?; + } + "DLMessageGetFreeDiskSpace" => { + // Minimal implementation: report 0 with success + self.send_status_response(0, None, Some(plist::Value::Integer(0u64.into()))).await?; + } + "DLContentsOfDirectory" => { + let empty = plist::Value::Dictionary(Dictionary::new()); + self.send_status_response(0, None, Some(empty)).await?; + } + "DLMessageCreateDirectory" => { + let status = Self::create_directory_from_message(&value, host_dir); + self.send_status_response(status, None, None).await?; + } + "DLMessageMoveFiles" | "DLMessageMoveItems" => { + let status = Self::move_files_from_message(&value, host_dir); + self.send_status_response(status, None, Some(plist::Value::Dictionary(Dictionary::new()))).await?; + } + "DLMessageRemoveFiles" | "DLMessageRemoveItems" => { + let status = Self::remove_files_from_message(&value, host_dir); + self.send_status_response(status, None, Some(plist::Value::Dictionary(Dictionary::new()))).await?; + } + "DLMessageCopyItem" => { + let status = Self::copy_item_from_message(&value, host_dir); + self.send_status_response(status, None, Some(plist::Value::Dictionary(Dictionary::new()))).await?; + } + "DLMessageProcessMessage" => { + 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 => { + warn!("Unsupported DL message: {other}"); + self.send_status_response(-1, Some("Operation not supported"), None).await?; + } + } + } + } + + async fn handle_download_files(&mut self, dl_value: &plist::Value, host_dir: &Path) -> Result<(), IdeviceError> { + 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) = self.send_single_file(host_dir, path).await + { + warn!("Failed to send file {path}: {e}"); + err_any = true; + } + } + } + // terminating zero dword + self.idevice.send_raw(&0u32.to_be_bytes()).await?; + if err_any { + self.send_status_response(-13, Some("Multi status"), Some(plist::Value::Dictionary(Dictionary::new()))).await + } else { + self.send_status_response(0, None, Some(plist::Value::Dictionary(Dictionary::new()))).await + } + } + + async fn send_single_file(&mut self, host_dir: &Path, rel_path: &str) -> Result<(), 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(); + self.idevice.send_raw(&nlen).await?; + self.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(DL_CODE_ERROR_LOCAL); + self.idevice.send_raw(&hdr).await?; + self.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(DL_CODE_FILE_DATA); + self.idevice.send_raw(&hdr).await?; + self.idevice.send_raw(&buf[..read]).await?; + } + // success trailer + let mut ok = [0u8; 5]; + ok[..4].copy_from_slice(&1u32.to_be_bytes()); + ok[4] = DL_CODE_SUCCESS; + self.idevice.send_raw(&ok).await?; + Ok(()) + } + + async fn handle_upload_files(&mut self, _dl_value: &plist::Value, host_dir: &Path) -> Result<(), IdeviceError> { + loop { + let dlen = self.read_be_u32().await?; + if dlen == 0 { break; } + let dname = self.read_exact_string(dlen as usize).await?; + let flen = self.read_be_u32().await?; + if flen == 0 { break; } + let fname = self.read_exact_string(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| IdeviceError::InternalError(e.to_string()))?; + loop { + let nlen = self.read_be_u32().await?; + if nlen == 0 { break; } + let code = self.read_one().await?; + if code == DL_CODE_FILE_DATA { + let size = (nlen - 1) as usize; + let data = self.read_exact(size).await?; + file.write_all(&data).map_err(|e| IdeviceError::InternalError(e.to_string()))?; + } else { + let _ = self.read_exact((nlen - 1) as usize).await?; + } + } + let _ = dname; // unused + } + self.send_status_response(0, None, Some(plist::Value::Dictionary(Dictionary::new()))).await + } + + async fn read_be_u32(&mut self) -> Result { + let buf = self.idevice.read_raw(4).await?; + Ok(u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]])) + } + + async fn read_one(&mut self) -> Result { + let buf = self.idevice.read_raw(1).await?; + Ok(buf[0]) + } + + async fn read_exact(&mut self, size: usize) -> Result, IdeviceError> { + self.idevice.read_raw(size).await + } + + async fn read_exact_string(&mut self, size: usize) -> Result { + let buf = self.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() { + return match fs::create_dir_all(&to) { Ok(_) => 0, Err(_) => -1 }; + } else { + return match fs::copy(&from, &to) { Ok(_) => 0, Err(_) => -1 }; + } + } + -1 + } + + /// Starts a restore using the typed RestoreOptions builder + #[deprecated(note = "Use restore_from_path; restore via BackupUUID is not supported by device/mobilebackup2")] + pub async fn start_restore_with( + &mut self, + _backup_uuid: &str, + opts: RestoreOptions, + ) -> Result<(), IdeviceError> { + let dict = opts.to_plist(); + // Avoid borrowing self during request + let target_udid_owned = self.idevice.udid().map(|s| s.to_string()); + let target_udid = target_udid_owned.as_deref(); + self.send_request( + BackupMessageType::BackupMessageTypeRestore.as_str(), + target_udid, + target_udid, + Some(dict), + ) + .await?; + + let response = self.receive_backup_response().await?; + if let Some(error) = response.get("ErrorCode") { + warn!("Restore start failed with error: {error:?}"); + return Err(IdeviceError::UnexpectedResponse); + } + debug!("Restore started successfully"); + Ok(()) + } + + /// Assert backup dir structure exists for a given source identifier (UDID) + fn assert_backup_exists(&self, backup_root: &Path, source: &str) -> Result<(), IdeviceError> { + let device_dir = backup_root.join(source); + if device_dir.join("Info.plist").exists() + && device_dir.join("Manifest.plist").exists() + && device_dir.join("Status.plist").exists() + { + Ok(()) + } else { + Err(IdeviceError::NotFound) + } + } + + /// Get backup information using DeviceLink against a given backup root/source + pub async fn info_from_path( + &mut self, + backup_root: &Path, + source_identifier: Option<&str>, + ) -> Result { + let target_udid = self.idevice.udid(); + let source = source_identifier.or(target_udid).ok_or(IdeviceError::InvalidHostID)?; + self.assert_backup_exists(backup_root, source)?; + + let mut dict = Dictionary::new(); + dict.insert("TargetIdentifier".into(), plist::Value::String(target_udid.unwrap().to_string())); + if let Some(src) = source_identifier { dict.insert("SourceIdentifier".into(), plist::Value::String(src.to_string())); } + self.send_device_link_message("Info", Some(dict)).await?; + + match self.process_restore_dl_loop(backup_root).await? { + Some(res) => Ok(res), + None => Err(IdeviceError::UnexpectedResponse), + } + } + + /// List last backup contents (returns raw response dictionary) + pub async fn list_from_path( + &mut self, + backup_root: &Path, + source_identifier: Option<&str>, + ) -> Result { + let target_udid = self.idevice.udid(); + let source = source_identifier.or(target_udid).ok_or(IdeviceError::InvalidHostID)?; + self.assert_backup_exists(backup_root, source)?; + + let mut dict = Dictionary::new(); + dict.insert("MessageName".into(), plist::Value::String("List".into())); + dict.insert("TargetIdentifier".into(), plist::Value::String(target_udid.unwrap().to_string())); + dict.insert("SourceIdentifier".into(), plist::Value::String(source.to_string())); + self.send_device_link_message("List", Some(dict)).await?; + + match self.process_restore_dl_loop(backup_root).await? { + Some(res) => Ok(res), + None => Err(IdeviceError::UnexpectedResponse), + } + } + + /// Unpack a complete backup to device hierarchy + pub async fn unback_from_path( + &mut self, + backup_root: &Path, + password: Option<&str>, + source_identifier: Option<&str>, + ) -> Result<(), IdeviceError> { + let target_udid = self.idevice.udid(); + let source = source_identifier.or(target_udid).ok_or(IdeviceError::InvalidHostID)?; + self.assert_backup_exists(backup_root, source)?; + + let mut dict = Dictionary::new(); + dict.insert("TargetIdentifier".into(), plist::Value::String(target_udid.unwrap().to_string())); + dict.insert("MessageName".into(), plist::Value::String("Unback".into())); + dict.insert("SourceIdentifier".into(), plist::Value::String(source.to_string())); + if let Some(pw) = password { dict.insert("Password".into(), plist::Value::String(pw.to_string())); } + self.send_device_link_message("Unback", Some(dict)).await?; + let _ = self.process_restore_dl_loop(backup_root).await?; + Ok(()) + } + + /// Extract a single file from a previous backup + pub async fn extract_from_path( + &mut self, + domain_name: &str, + relative_path: &str, + backup_root: &Path, + password: Option<&str>, + source_identifier: Option<&str>, + ) -> Result<(), IdeviceError> { + let target_udid = self.idevice.udid(); + let source = source_identifier.or(target_udid).ok_or(IdeviceError::InvalidHostID)?; + self.assert_backup_exists(backup_root, source)?; + + let mut dict = Dictionary::new(); + dict.insert("MessageName".into(), plist::Value::String("Extract".into())); + dict.insert("TargetIdentifier".into(), plist::Value::String(target_udid.unwrap().to_string())); + dict.insert("DomainName".into(), plist::Value::String(domain_name.to_string())); + dict.insert("RelativePath".into(), plist::Value::String(relative_path.to_string())); + dict.insert("SourceIdentifier".into(), plist::Value::String(source.to_string())); + if let Some(pw) = password { dict.insert("Password".into(), plist::Value::String(pw.to_string())); } + self.send_device_link_message("Extract", Some(dict)).await?; + let _ = self.process_restore_dl_loop(backup_root).await?; + Ok(()) + } + + /// Change backup password (enable/disable if new/old missing) + pub async fn change_password_from_path( + &mut self, + backup_root: &Path, + old: Option<&str>, + new: Option<&str>, + ) -> Result<(), IdeviceError> { + let target_udid = self.idevice.udid(); + let mut dict = Dictionary::new(); + dict.insert("MessageName".into(), plist::Value::String("ChangePassword".into())); + dict.insert("TargetIdentifier".into(), plist::Value::String(target_udid.ok_or(IdeviceError::InvalidHostID)?.to_string())); + if let Some(o) = old { dict.insert("OldPassword".into(), plist::Value::String(o.to_string())); } + if let Some(n) = new { dict.insert("NewPassword".into(), plist::Value::String(n.to_string())); } + self.send_device_link_message("ChangePassword", Some(dict)).await?; + let _ = self.process_restore_dl_loop(backup_root).await?; + Ok(()) + } + + /// Erase device via mobilebackup2 + pub async fn erase_device_from_path(&mut self, backup_root: &Path) -> Result<(), IdeviceError> { + let target_udid = self.idevice.udid(); + let mut dict = Dictionary::new(); + dict.insert("MessageName".into(), plist::Value::String("EraseDevice".into())); + dict.insert("TargetIdentifier".into(), plist::Value::String(target_udid.ok_or(IdeviceError::InvalidHostID)?.to_string())); + self.send_device_link_message("EraseDevice", Some(dict)).await?; + let _ = self.process_restore_dl_loop(backup_root).await?; + Ok(()) + } + + /// Gets free space information from the device + /// + /// # Returns + /// Free space in bytes + /// + /// # Errors + /// Returns `IdeviceError` if the request fails + pub async fn get_freespace(&mut self) -> Result { + // Not a valid host-initiated request in protocol; device asks via DLMessageGetFreeDiskSpace + Err(IdeviceError::UnexpectedResponse) + } + + /// Checks if backup encryption is enabled + /// + /// # Returns + /// `true` if backup encryption is enabled, `false` otherwise + /// + /// # Errors + /// Returns `IdeviceError` if the request fails + pub async fn check_backup_encryption(&mut self) -> Result { + // Not part of host-initiated MB2 protocol; caller should inspect Manifest/lockdown + Err(IdeviceError::UnexpectedResponse) + } + + /// Disconnects from the backup service + /// + /// # Returns + /// `Ok(())` on successful disconnection + /// + /// # Errors + /// Returns `IdeviceError` if disconnection fails + pub async fn disconnect(&mut self) -> Result<(), IdeviceError> { + // Send DLMessageDisconnect array per DeviceLink protocol + let arr = vec![ + plist::Value::String("DLMessageDisconnect".into()), + plist::Value::String("___EmptyParameterString___".into()), + ]; + self.send_dl_array(arr).await?; + debug!("Disconnected from backup service"); + Ok(()) + } +} \ No newline at end of file diff --git a/idevice/src/services/mod.rs b/idevice/src/services/mod.rs index f0fb2c6..fd341ec 100644 --- a/idevice/src/services/mod.rs +++ b/idevice/src/services/mod.rs @@ -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")] diff --git a/tools/Cargo.toml b/tools/Cargo.toml index 17d5510..2cc126e 100644 --- a/tools/Cargo.toml +++ b/tools/Cargo.toml @@ -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"] } diff --git a/tools/src/diagnostics.rs b/tools/src/diagnostics.rs new file mode 100644 index 0000000..c6a007d --- /dev/null +++ b/tools/src/diagnostics.rs @@ -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::("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; + } + }; + + 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::("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()); + + 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::("keys") + .map(|values| values.map(|s| s.to_string()).collect::>()); + + 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:?}"), + } +} \ No newline at end of file diff --git a/tools/src/mobilebackup2.rs b/tools/src/mobilebackup2.rs new file mode 100644 index 0000000..4ec7259 --- /dev/null +++ b/tools/src/mobilebackup2.rs @@ -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::("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; + } + }; + + 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::("dir").unwrap(); + let source = sub.get_one::("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::("dir").unwrap(); + let source = sub.get_one::("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::("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"); + + println!("Starting backup operation..."); + let res = backup_client + .send_request("Backup", target, source, None::) + .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::("dir").unwrap(); + let source = sub.get_one::("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::("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::("dir").unwrap(); + let source = sub.get_one::("source").map(|s| s.as_str()); + let password = sub.get_one::("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::("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()); + 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::("dir").unwrap(); + let old = sub.get_one::("old").map(|s| s.as_str()); + let newv = sub.get_one::("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::("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, 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 { + 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 { + let buf = client.idevice.read_raw(1).await?; + Ok(buf[0]) +} + +async fn read_exact(client: &mut MobileBackup2Client, size: usize) -> Result, idevice::IdeviceError> { + client.idevice.read_raw(size).await +} + +async fn read_exact_string(client: &mut MobileBackup2Client, size: usize) -> Result { + 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 +} \ No newline at end of file