use plist::Dictionary;
use std::future::Future;
use std::io::{Read, Write};
use std::path::Path;
use std::pin::Pin;
use std::time::SystemTime;
use tokio::io::AsyncReadExt;
use tracing::{debug, warn};
use crate::{Idevice, IdeviceError, IdeviceService, obf};
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;
#[derive(Debug)]
pub struct DirEntryInfo {
pub name: String,
pub is_dir: bool,
pub is_file: bool,
pub size: u64,
pub modified: Option<SystemTime>,
}
pub trait BackupDelegate: Send + Sync {
fn get_free_disk_space(&self, path: &Path) -> u64;
#[allow(clippy::type_complexity)]
fn open_file_read<'a>(
&'a self,
path: &'a Path,
) -> Pin<Box<dyn Future<Output = Result<Box<dyn Read + Send>, IdeviceError>> + Send + 'a>>;
#[allow(clippy::type_complexity)]
fn create_file_write<'a>(
&'a self,
path: &'a Path,
) -> Pin<Box<dyn Future<Output = Result<Box<dyn Write + Send>, IdeviceError>> + Send + 'a>>;
fn create_dir_all<'a>(
&'a self,
path: &'a Path,
) -> Pin<Box<dyn Future<Output = Result<(), IdeviceError>> + Send + 'a>>;
fn remove<'a>(
&'a self,
path: &'a Path,
) -> Pin<Box<dyn Future<Output = Result<(), IdeviceError>> + Send + 'a>>;
fn rename<'a>(
&'a self,
from: &'a Path,
to: &'a Path,
) -> Pin<Box<dyn Future<Output = Result<(), IdeviceError>> + Send + 'a>>;
fn copy<'a>(
&'a self,
src: &'a Path,
dst: &'a Path,
) -> Pin<Box<dyn Future<Output = Result<(), IdeviceError>> + Send + 'a>>;
fn exists<'a>(&'a self, path: &'a Path) -> Pin<Box<dyn Future<Output = bool> + Send + 'a>>;
fn is_dir<'a>(&'a self, path: &'a Path) -> Pin<Box<dyn Future<Output = bool> + Send + 'a>>;
fn list_dir<'a>(
&'a self,
path: &'a Path,
) -> Pin<Box<dyn Future<Output = Result<Vec<DirEntryInfo>, IdeviceError>> + Send + 'a>>;
fn on_file_received(&self, _path: &str, _file_count: u32) {}
fn on_progress(&self, _bytes_done: u64, _bytes_total: u64, _overall_progress: f64) {}
}
#[derive(Debug, Clone, Copy)]
pub struct FsBackupDelegate;
impl BackupDelegate for FsBackupDelegate {
fn get_free_disk_space(&self, _path: &Path) -> u64 {
0
}
fn open_file_read<'a>(
&'a self,
path: &'a Path,
) -> Pin<Box<dyn Future<Output = Result<Box<dyn Read + Send>, IdeviceError>> + Send + 'a>> {
Box::pin(async move {
let file = tokio::fs::File::open(path)
.await
.map_err(|e| IdeviceError::InternalError(e.to_string()))?;
let std_file = file.into_std().await;
Ok(Box::new(std_file) as Box<dyn Read + Send>)
})
}
fn create_file_write<'a>(
&'a self,
path: &'a Path,
) -> Pin<Box<dyn Future<Output = Result<Box<dyn Write + Send>, IdeviceError>> + Send + 'a>>
{
Box::pin(async move {
let file = tokio::fs::File::create(path)
.await
.map_err(|e| IdeviceError::InternalError(e.to_string()))?;
let std_file = file.into_std().await;
Ok(Box::new(std_file) as Box<dyn Write + Send>)
})
}
fn create_dir_all<'a>(
&'a self,
path: &'a Path,
) -> Pin<Box<dyn Future<Output = Result<(), IdeviceError>> + Send + 'a>> {
Box::pin(async move {
tokio::fs::create_dir_all(path)
.await
.map_err(|e| IdeviceError::InternalError(e.to_string()))
})
}
fn remove<'a>(
&'a self,
path: &'a Path,
) -> Pin<Box<dyn Future<Output = Result<(), IdeviceError>> + Send + 'a>> {
Box::pin(async move {
let meta = tokio::fs::metadata(path).await;
match meta {
Ok(m) if m.is_dir() => tokio::fs::remove_dir_all(path).await,
_ => tokio::fs::remove_file(path).await,
}
.map_err(|e| IdeviceError::InternalError(e.to_string()))
})
}
fn rename<'a>(
&'a self,
from: &'a Path,
to: &'a Path,
) -> Pin<Box<dyn Future<Output = Result<(), IdeviceError>> + Send + 'a>> {
Box::pin(async move {
tokio::fs::rename(from, to)
.await
.map_err(|e| IdeviceError::InternalError(e.to_string()))
})
}
fn copy<'a>(
&'a self,
src: &'a Path,
dst: &'a Path,
) -> Pin<Box<dyn Future<Output = Result<(), IdeviceError>> + Send + 'a>> {
Box::pin(async move {
let meta = tokio::fs::metadata(src).await;
if meta.is_ok_and(|m| m.is_dir()) {
tokio::fs::create_dir_all(dst).await
} else {
tokio::fs::copy(src, dst).await.map(|_| ())
}
.map_err(|e| IdeviceError::InternalError(e.to_string()))
})
}
fn exists<'a>(&'a self, path: &'a Path) -> Pin<Box<dyn Future<Output = bool> + Send + 'a>> {
Box::pin(async move { tokio::fs::try_exists(path).await.unwrap_or(false) })
}
fn is_dir<'a>(&'a self, path: &'a Path) -> Pin<Box<dyn Future<Output = bool> + Send + 'a>> {
Box::pin(async move {
tokio::fs::metadata(path)
.await
.map(|m| m.is_dir())
.unwrap_or(false)
})
}
fn list_dir<'a>(
&'a self,
path: &'a Path,
) -> Pin<Box<dyn Future<Output = Result<Vec<DirEntryInfo>, IdeviceError>> + Send + 'a>> {
Box::pin(async move {
let mut entries = tokio::fs::read_dir(path)
.await
.map_err(|e| IdeviceError::InternalError(e.to_string()))?;
let mut result = Vec::new();
while let Ok(Some(entry)) = entries.next_entry().await {
let name = entry.file_name().to_string_lossy().to_string();
let meta = entry.metadata().await.ok();
result.push(DirEntryInfo {
name,
is_dir: meta.as_ref().is_some_and(|m| m.is_dir()),
is_file: meta.as_ref().is_some_and(|m| m.is_file()),
size: meta.as_ref().map_or(0, |m| m.len()),
modified: meta.and_then(|m| m.modified().ok()),
});
}
Ok(result)
})
}
}
#[derive(Debug)]
pub struct MobileBackup2Client {
pub idevice: Idevice,
pub protocol_version: f64,
}
impl IdeviceService for MobileBackup2Client {
fn service_name() -> std::borrow::Cow<'static, str> {
obf!("com.apple.mobilebackup2")
}
async fn from_stream(idevice: Idevice) -> Result<Self, crate::IdeviceError> {
let mut client = Self::new(idevice);
client.dl_version_exchange().await?;
client.version_exchange().await?;
Ok(client)
}
}
#[derive(Debug, Clone, Copy)]
pub enum BackupMessageType {
BackupMessageTypeBackup,
BackupMessageTypeRestore,
BackupMessageTypeInfo,
BackupMessageTypeList,
BackupMessageTypeUploadFiles,
BackupMessageTypeDownloadFiles,
BackupMessageTypeClearBackupData,
BackupMessageTypeMoveFiles,
BackupMessageTypeRemoveFiles,
BackupMessageTypeCreateDirectory,
BackupMessageTypeAcquireLock,
BackupMessageTypeReleaseLock,
BackupMessageTypeCopyItem,
BackupMessageTypeDisconnect,
BackupMessageTypeProcessMessage,
BackupMessageTypeGetFreespace,
BackupMessageTypeFactoryInfo,
BackupMessageTypeCheckBackupEncryption,
}
impl BackupMessageType {
pub fn as_str(&self) -> &'static str {
match self {
BackupMessageType::BackupMessageTypeBackup => "Backup",
BackupMessageType::BackupMessageTypeRestore => "Restore",
BackupMessageType::BackupMessageTypeInfo => "Info",
BackupMessageType::BackupMessageTypeList => "List",
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",
}
}
}
#[derive(Debug, Clone)]
pub struct BackupInfo {
pub uuid: String,
pub device_name: String,
pub display_name: String,
pub last_backup_date: Option<String>,
pub version: String,
pub is_encrypted: bool,
}
#[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<String>,
}
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<String>) -> Self {
self.password = Some(password.into());
self
}
pub fn to_plist(&self) -> Dictionary {
crate::plist!(dict {
"RestoreShouldReboot": self.reboot,
"RestoreDontCopyBackup": !self.copy,
"RestorePreserveSettings": self.preserve_settings,
"RestoreSystemFiles": self.system_files,
"RemoveItemsNotRestored": self.remove_items_not_restored,
"Password":? self.password.clone()
})
}
}
impl MobileBackup2Client {
pub fn new(idevice: Idevice) -> Self {
Self {
idevice,
protocol_version: 0.0,
}
}
async fn dl_version_exchange(&mut self) -> Result<(), IdeviceError> {
debug!("Starting DeviceLink version exchange");
let (msg, _arr) = self.receive_dl_message().await?;
if msg != "DLMessageVersionExchange" {
warn!("Expected DLMessageVersionExchange, got {msg}");
return Err(IdeviceError::UnexpectedResponse(
"expected DLMessageVersionExchange during handshake".into(),
));
}
let out = vec![
plist::Value::String("DLMessageVersionExchange".into()),
plist::Value::String("DLVersionsOk".into()),
plist::Value::Integer(400u64.into()),
];
self.send_dl_array(out).await?;
let (msg2, _arr2) = self.receive_dl_message().await?;
if msg2 != "DLMessageDeviceReady" {
warn!("Expected DLMessageDeviceReady, got {msg2}");
return Err(IdeviceError::UnexpectedResponse(
"expected DLMessageDeviceReady after version exchange".into(),
));
}
Ok(())
}
async fn send_dl_array(&mut self, array: Vec<plist::Value>) -> Result<(), IdeviceError> {
self.idevice.send_bplist(plist::Value::Array(array)).await
}
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];
if let Err(e) = socket.read_exact(&mut buf).await {
debug!("Failed to read DL message length: {e}");
return Err(e.into());
}
let len = u32::from_be_bytes(buf);
debug!("Reading DL message body: {len} bytes");
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()
{
debug!("Received DL message: {tag}");
return Ok((tag.clone(), value));
}
warn!("Invalid DL message format");
Err(IdeviceError::UnexpectedResponse(
"invalid DL message format: expected array with string tag".into(),
))
} else {
Err(IdeviceError::NoEstablishedConnection)
}
}
async fn version_exchange(&mut self) -> Result<(), IdeviceError> {
debug!("Starting mobilebackup2 version exchange");
let hello_dict = crate::plist!(dict {
"SupportedProtocolVersions": [
2.0, 2.1
]
});
self.send_device_link_message("Hello", Some(hello_dict))
.await?;
let response = self.receive_device_link_message("Response").await?;
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(
"version exchange failed with non-zero ErrorCode".into(),
));
}
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(
"missing ProtocolVersion in version exchange response".into(),
));
}
Ok(())
}
async fn send_device_link_message(
&mut self,
message_name: &str,
options: Option<Dictionary>,
) -> Result<(), IdeviceError> {
let message_dict = crate::plist!(dict {
"MessageName": message_name,
:<? options,
});
debug!("Sending device link message: {message_name}");
self.idevice
.send_bplist(crate::plist!(["DLMessageProcessMessage", message_dict]))
.await
}
async fn receive_device_link_message(
&mut self,
expected_message: &str,
) -> Result<Dictionary, IdeviceError> {
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)?;
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"
{
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(
"device link MessageName does not match expected".into(),
));
}
} else {
warn!("No MessageName in response");
return Err(IdeviceError::UnexpectedResponse(
"missing MessageName in DLMessageProcessMessage".into(),
));
}
}
return Ok(dict.clone());
}
warn!("Invalid device link message format");
Err(IdeviceError::UnexpectedResponse(
"invalid device link message format".into(),
))
} else {
Err(IdeviceError::NoEstablishedConnection)
}
}
async fn send_backup_message(
&mut self,
message_type: BackupMessageType,
options: Option<Dictionary>,
) -> Result<(), IdeviceError> {
self.send_device_link_message(message_type.as_str(), options)
.await
}
pub async fn send_request(
&mut self,
request: &str,
target_identifier: Option<&str>,
source_identifier: Option<&str>,
options: Option<Dictionary>,
) -> Result<(), IdeviceError> {
let dict = crate::plist!(dict {
"TargetIdentifier":? target_identifier,
"SourceIdentifier":? source_identifier,
"Options":? options,
});
self.send_device_link_message(request, Some(dict)).await
}
pub async fn send_status_response(
&mut self,
status_code: i64,
status1: Option<&str>,
status2: Option<plist::Value>,
) -> 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
}
async fn receive_backup_response(&mut self) -> Result<Dictionary, IdeviceError> {
self.receive_device_link_message("").await
}
pub async fn request_backup_info(&mut self) -> Result<Dictionary, IdeviceError> {
self.send_backup_message(BackupMessageType::BackupMessageTypeInfo, None)
.await?;
let response = self.receive_backup_response().await?;
if let Some(error) = response.get("ErrorCode") {
warn!("Backup info request failed with error: {error:?}");
return Err(IdeviceError::UnexpectedResponse(
"backup info request returned ErrorCode".into(),
));
}
Ok(response)
}
pub async fn list_backups(&mut self) -> Result<Vec<BackupInfo>, IdeviceError> {
self.send_backup_message(BackupMessageType::BackupMessageTypeList, None)
.await?;
let response = self.receive_backup_response().await?;
if let Some(error) = response.get("ErrorCode") {
warn!("List backups request failed with error: {error:?}");
return Err(IdeviceError::UnexpectedResponse(
"list backups request returned ErrorCode".into(),
));
}
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)
}
pub async fn start_backup(
&mut self,
target_identifier: Option<&str>,
source_identifier: Option<&str>,
options: Option<Dictionary>,
) -> Result<(), IdeviceError> {
self.send_request(
BackupMessageType::BackupMessageTypeBackup.as_str(),
target_identifier,
source_identifier,
options,
)
.await?;
let response = self.receive_backup_response().await?;
if let Some(error) = response.get("ErrorCode") {
warn!("Backup start failed with error: {error:?}");
return Err(IdeviceError::UnexpectedResponse(
"backup start returned ErrorCode".into(),
));
}
debug!("Backup started successfully");
Ok(())
}
#[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<Dictionary>,
) -> Result<(), IdeviceError> {
let mut opts = options.unwrap_or_default();
if !opts.contains_key("RestoreShouldReboot") {
opts.insert("RestoreShouldReboot".into(), plist::Value::Boolean(true));
}
if !opts.contains_key("RestoreDontCopyBackup") {
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),
);
}
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(opts),
)
.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(
"restore start returned ErrorCode".into(),
));
}
debug!("Restore started successfully");
Ok(())
}
pub async fn backup_from_path(
&mut self,
backup_root: &Path,
source_identifier: Option<&str>,
options: Option<Dictionary>,
delegate: &dyn BackupDelegate,
) -> Result<Option<Dictionary>, IdeviceError> {
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)?,
};
let backup_dir = backup_root.join(source);
let _ = delegate.create_dir_all(&backup_dir).await;
self.send_request(
BackupMessageType::BackupMessageTypeBackup.as_str(),
target_udid,
Some(source),
options,
)
.await?;
self.process_dl_loop(backup_root, delegate).await
}
pub async fn restore_from_path(
&mut self,
backup_root: &Path,
source_identifier: Option<&str>,
options: Option<RestoreOptions>,
delegate: &dyn BackupDelegate,
) -> Result<Option<Dictionary>, IdeviceError> {
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)?,
};
let backup_dir = backup_root.join(source);
if !delegate.exists(&backup_dir).await {
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?;
self.process_dl_loop(backup_root, delegate).await
}
async fn process_dl_loop(
&mut self,
host_dir: &Path,
delegate: &dyn BackupDelegate,
) -> Result<Option<Dictionary>, IdeviceError> {
let mut overall_progress: f64 = -1.0;
loop {
let (tag, value) = self.receive_dl_message().await?;
if let plist::Value::Array(arr) = &value {
let progress_idx = match tag.as_str() {
"DLMessageUploadFiles" => Some(2),
"DLMessageDownloadFiles"
| "DLMessageMoveFiles"
| "DLMessageMoveItems"
| "DLMessageRemoveFiles"
| "DLMessageRemoveItems" => Some(3),
_ => None,
};
if let Some(idx) = progress_idx
&& let Some(plist::Value::Real(p)) = arr.get(idx)
&& *p > 0.0
{
overall_progress = *p;
}
}
match tag.as_str() {
"DLMessageDownloadFiles" => {
self.handle_download_files(&value, host_dir, delegate)
.await?;
}
"DLMessageUploadFiles" => {
self.handle_upload_files(&value, host_dir, delegate, overall_progress)
.await?;
}
"DLMessageGetFreeDiskSpace" => {
let freespace = delegate.get_free_disk_space(host_dir);
self.send_status_response(
0,
None,
Some(plist::Value::Integer(freespace.into())),
)
.await?;
}
"DLContentsOfDirectory" => {
let listing = Self::list_directory_contents(&value, host_dir, delegate).await;
self.send_status_response(0, None, Some(listing)).await?;
}
"DLMessageCreateDirectory" => {
if let plist::Value::Array(arr) = &value
&& let Some(plist::Value::String(dir)) = arr.get(1)
{
debug!("Creating directory: {dir}");
}
let status =
Self::create_directory_from_message(&value, host_dir, delegate).await;
self.send_status_response(status, None, None).await?;
}
"DLMessageMoveFiles" | "DLMessageMoveItems" => {
let status = Self::move_files_from_message(&value, host_dir, delegate).await;
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, delegate).await;
self.send_status_response(
status,
None,
Some(plist::Value::Dictionary(Dictionary::new())),
)
.await?;
}
"DLMessageCopyItem" => {
let status = Self::copy_item_from_message(&value, host_dir, delegate).await;
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,
delegate: &dyn BackupDelegate,
) -> 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() {
debug!("Device requested file: {path}");
if let Err(e) = self.send_single_file(host_dir, path, delegate).await {
warn!("Failed to send file {path}: {e}");
err_any = true;
}
}
}
}
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,
delegate: &dyn BackupDelegate,
) -> 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 delegate.open_file_read(&full).await {
Ok(f) => f,
Err(e) => {
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?;
}
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,
delegate: &dyn BackupDelegate,
overall_progress: f64,
) -> Result<(), IdeviceError> {
let mut file_count: u32 = 0;
let mut bytes_done: u64 = 0;
let bytes_total = if let plist::Value::Array(arr) = dl_value {
arr.get(3)
.and_then(|v| v.as_unsigned_integer())
.unwrap_or(0)
} else {
0
};
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 _ = delegate.create_dir_all(parent).await;
}
let mut nlen = self.read_be_u32().await?;
if nlen == 0 {
continue;
}
let mut code = self.read_one().await?;
let _ = delegate.remove(&dst).await;
let mut file = delegate.create_file_write(&dst).await?;
while code == DL_CODE_FILE_DATA {
let block_size = (nlen - 1) as usize;
let data = self.read_exact(block_size).await?;
file.write_all(&data)
.map_err(|e| IdeviceError::InternalError(e.to_string()))?;
bytes_done += block_size as u64;
nlen = self.read_be_u32().await?;
if nlen > 0 {
code = self.read_one().await?;
} else {
break;
}
}
file_count += 1;
delegate.on_file_received(&fname, file_count);
delegate.on_progress(bytes_done, bytes_total, overall_progress);
if nlen > 0 && code != DL_CODE_FILE_DATA && code != DL_CODE_SUCCESS {
let _ = self.read_exact((nlen - 1) as usize).await?;
}
}
debug!("Received {file_count} files from device");
self.send_status_response(0, None, Some(plist::Value::Dictionary(Dictionary::new())))
.await
}
async fn read_be_u32(&mut self) -> Result<u32, IdeviceError> {
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<u8, IdeviceError> {
let buf = self.idevice.read_raw(1).await?;
Ok(buf[0])
}
async fn read_exact(&mut self, size: usize) -> Result<Vec<u8>, IdeviceError> {
self.idevice.read_raw(size).await
}
async fn read_exact_string(&mut self, size: usize) -> Result<String, IdeviceError> {
let buf = self.idevice.read_raw(size).await?;
Ok(String::from_utf8_lossy(&buf).to_string())
}
async fn create_directory_from_message(
dl_value: &plist::Value,
host_dir: &Path,
delegate: &dyn BackupDelegate,
) -> 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 delegate.create_dir_all(&path).await {
Ok(_) => 0,
Err(_) => -1,
};
}
-1
}
async fn move_files_from_message(
dl_value: &plist::Value,
host_dir: &Path,
delegate: &dyn BackupDelegate,
) -> 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 _ = delegate.create_dir_all(parent).await;
}
if delegate.rename(&old, &newp).await.is_err() {
return -1;
}
}
}
return 0;
}
-1
}
async fn remove_files_from_message(
dl_value: &plist::Value,
host_dir: &Path,
delegate: &dyn BackupDelegate,
) -> 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 delegate.exists(&path).await && delegate.remove(&path).await.is_err() {
return -1;
}
}
}
return 0;
}
-1
}
async fn copy_item_from_message(
dl_value: &plist::Value,
host_dir: &Path,
delegate: &dyn BackupDelegate,
) -> 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 _ = delegate.create_dir_all(parent).await;
}
return match delegate.copy(&from, &to).await {
Ok(_) => 0,
Err(_) => -1,
};
}
-1
}
#[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();
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(
"restore start returned ErrorCode".into(),
));
}
debug!("Restore started successfully");
Ok(())
}
async fn assert_backup_exists(
backup_root: &Path,
source: &str,
delegate: &dyn BackupDelegate,
) -> Result<(), IdeviceError> {
let device_dir = backup_root.join(source);
if delegate.exists(&device_dir.join("Info.plist")).await
&& delegate.exists(&device_dir.join("Manifest.plist")).await
&& delegate.exists(&device_dir.join("Status.plist")).await
{
Ok(())
} else {
Err(IdeviceError::NotFound)
}
}
async fn assert_backup_has_manifest(
backup_root: &Path,
source: &str,
delegate: &dyn BackupDelegate,
) -> Result<(), IdeviceError> {
let device_dir = backup_root.join(source);
if delegate.exists(&device_dir.join("Manifest.plist")).await {
Ok(())
} else {
Err(IdeviceError::NotFound)
}
}
pub async fn info_from_path(
&mut self,
backup_root: &Path,
source_identifier: Option<&str>,
delegate: &dyn BackupDelegate,
) -> Result<Dictionary, 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, delegate).await?;
let dict = crate::plist!(dict {
"TargetIdentifier": target_udid.unwrap(),
"SourceIdentifier":? source_identifier,
});
self.send_device_link_message("Info", Some(dict)).await?;
match self.process_dl_loop(backup_root, delegate).await? {
Some(res) => Ok(res),
None => Err(IdeviceError::UnexpectedResponse(
"info_from_path DL loop returned no response".into(),
)),
}
}
pub async fn list_from_path(
&mut self,
backup_root: &Path,
source_identifier: Option<&str>,
delegate: &dyn BackupDelegate,
) -> Result<Dictionary, 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, delegate).await?;
let dict = crate::plist!(dict {
"MessageName": "List",
"TargetIdentifier": target_udid.unwrap(),
"SourceIdentifier": source,
});
self.send_device_link_message("List", Some(dict)).await?;
match self.process_dl_loop(backup_root, delegate).await? {
Some(res) => Ok(res),
None => Err(IdeviceError::UnexpectedResponse(
"list_from_path DL loop returned no response".into(),
)),
}
}
pub async fn unback_from_path(
&mut self,
backup_root: &Path,
password: Option<&str>,
source_identifier: Option<&str>,
delegate: &dyn BackupDelegate,
) -> Result<(), IdeviceError> {
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)?,
};
Self::assert_backup_has_manifest(backup_root, source, delegate).await?;
let opts = password.map(|pw| crate::plist!(dict { "Password": pw }));
self.send_request("Unback", target_udid, Some(source), opts)
.await?;
let _ = self.process_dl_loop(backup_root, delegate).await?;
Ok(())
}
pub async fn extract_from_path(
&mut self,
domain_name: &str,
relative_path: &str,
backup_root: &Path,
password: Option<&str>,
source_identifier: Option<&str>,
delegate: &dyn BackupDelegate,
) -> Result<(), IdeviceError> {
let target_udid = self.idevice.udid();
let source = source_identifier
.or(target_udid)
.ok_or(IdeviceError::InvalidHostID)?;
Self::assert_backup_has_manifest(backup_root, source, delegate).await?;
let dict = crate::plist!(dict {
"MessageName": "Extract",
"TargetIdentifier": target_udid.unwrap(),
"DomainName": domain_name,
"RelativePath": relative_path,
"SourceIdentifier": source,
"Password":? password,
});
self.send_device_link_message("Extract", Some(dict)).await?;
let _ = self.process_dl_loop(backup_root, delegate).await?;
Ok(())
}
pub async fn change_password_from_path(
&mut self,
backup_root: &Path,
old: Option<&str>,
new: Option<&str>,
delegate: &dyn BackupDelegate,
) -> Result<(), IdeviceError> {
let target_udid = self.idevice.udid();
let dict = crate::plist!(dict {
"MessageName": "ChangePassword",
"TargetIdentifier": target_udid.ok_or(IdeviceError::InvalidHostID)?,
"OldPassword":? old,
"NewPassword":? new
});
self.send_device_link_message("ChangePassword", Some(dict))
.await?;
let _ = self.process_dl_loop(backup_root, delegate).await?;
Ok(())
}
pub async fn erase_device_from_path(
&mut self,
backup_root: &Path,
delegate: &dyn BackupDelegate,
) -> Result<(), IdeviceError> {
let target_udid = self.idevice.udid();
let dict = crate::plist!(dict {
"MessageName": "EraseDevice",
"TargetIdentifier": target_udid.ok_or(IdeviceError::InvalidHostID)?
});
self.send_device_link_message("EraseDevice", Some(dict))
.await?;
let _ = self.process_dl_loop(backup_root, delegate).await?;
Ok(())
}
pub async fn get_freespace(&mut self) -> Result<u64, IdeviceError> {
Err(IdeviceError::UnexpectedResponse(
"get_freespace is not a valid host-initiated request".into(),
))
}
pub async fn check_backup_encryption(&mut self) -> Result<bool, IdeviceError> {
Err(IdeviceError::UnexpectedResponse(
"check_backup_encryption is not a valid host-initiated request".into(),
))
}
async fn list_directory_contents(
dl_value: &plist::Value,
host_dir: &Path,
delegate: &dyn BackupDelegate,
) -> plist::Value {
let mut dirlist = Dictionary::new();
let rel_path = if let plist::Value::Array(arr) = dl_value
&& arr.len() >= 2
&& let Some(plist::Value::String(dir)) = arr.get(1)
{
dir.clone()
} else {
return plist::Value::Dictionary(dirlist);
};
let full_path = host_dir.join(&rel_path);
if let Ok(entries) = delegate.list_dir(&full_path).await {
for entry in entries {
let mut fdict = Dictionary::new();
let ftype = if entry.is_dir {
"DLFileTypeDirectory"
} else if entry.is_file {
"DLFileTypeRegular"
} else {
"DLFileTypeUnknown"
};
fdict.insert("DLFileType".into(), plist::Value::String(ftype.into()));
fdict.insert(
"DLFileSize".into(),
plist::Value::Integer(entry.size.into()),
);
if let Some(mtime) = entry.modified {
fdict.insert(
"DLFileModificationDate".into(),
plist::Value::Date(mtime.into()),
);
}
dirlist.insert(entry.name, plist::Value::Dictionary(fdict));
}
}
plist::Value::Dictionary(dirlist)
}
pub async fn disconnect(&mut self) -> Result<(), IdeviceError> {
let arr = crate::plist!(array [
"DLMessageDisconnect",
"___EmptyParameterString___"
]);
self.send_dl_array(arr).await?;
debug!("Disconnected from backup service");
Ok(())
}
}
#[cfg(feature = "rsd")]
impl crate::RsdService for MobileBackup2Client {
fn rsd_service_name() -> std::borrow::Cow<'static, str> {
crate::obf!("com.apple.mobilebackup2.shim.remote")
}
async fn from_stream(stream: Box<dyn crate::ReadWrite>) -> Result<Self, crate::IdeviceError> {
let mut idevice = crate::Idevice::new(stream, "");
idevice.rsd_checkin().await?;
let mut client = Self::new(idevice);
client.dl_version_exchange().await?;
client.version_exchange().await?;
Ok(client)
}
}