// Copyright 2020-2022 Yang Hongbo, Qingdao IotPi Information Technology, Ltd.
// This file is part of libimobiledeivce.
// libimobiledevice-rs is free software: you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
// libimobiledevice-rs is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with libimobiledevice-rs. If not, see .
use clap::{
crate_authors, crate_name, crate_version, App, AppSettings, Arg, ArgMatches, SubCommand,
};
use std::borrow::Cow;
use std::collections::HashMap;
use std::error::Error;
use std::ffi::{CStr, CString};
use std::fs::{self, File};
use std::io::{self, prelude::*};
use std::mem;
use std::os::unix::ffi::OsStrExt;
use std::path::{Path, PathBuf};
use std::rc::Rc;
use std::{thread, time};
use anyhow::{anyhow, Result};
use chrono::prelude::*;
use libc;
use uuid::Uuid;
use libimobiledevice::{
afc::{AfcClient, AfcError, FileHandle as AfcFileHandle},
ffi::{self, idevice_options},
idevice::Device,
installation_proxy::{ClientOptions, InstproxyClient, OptionsType},
lockdownd::LockdowndClient,
mobilebackup2::{
Mobilebackup2Client, Mobilebackup2Error, Mobilebackup2Result, ReceivedMessage,
},
notification_proxy::{NpClient, NpClientTrait},
};
use libplist::plist::{IterDict, Plist};
const CODE_SUCCESS: u8 = 0x00;
const CODE_ERROR_LOCAL: u8 = 0x06;
const CODE_ERROR_REMOTE: u8 = 0x0b;
const CODE_FILE_DATA: u8 = 0x0c;
const ITUNES_RESTORE_DIR: &'static str = "/iTunesRestore";
const ITUNES_RESTORE_RESTORE_APPLICATION_PLIST_FILE: &'static str =
"/iTunesRestore/RestoreApplications.plist";
fn main() -> Result<()> {
// let dt_20010101 = Utc.ymd(2001, 1, 1).and_hms(0, 0, 0);
// println!("dt: {:?}", dt_20010101);
// println!("timestamp: {:?}", dt_20010101.timestamp());
let matches = App::new(crate_name!())
.version(crate_version!())
.author(crate_authors!())
.about("idevice backup status")
.arg(
Arg::with_name("domain")
.short("q")
.value_name("domain")
.help("set domain of query to NAME. Default: None")
.takes_value(true),
)
.arg(
Arg::with_name("key")
.short("k")
.value_name("key")
.help("only query key specified by NAME. Default: All keys.")
.takes_value(true),
)
.arg(
Arg::with_name("debug")
.short("d")
.long("debug")
.help("enable communication debug (inside libimobiledevice)"),
)
.setting(AppSettings::SubcommandRequiredElseHelp)
.subcommand(
SubCommand::with_name("backup")
.about("start backup service")
.arg(
Arg::with_name("full")
.long("full")
.help("force full backup from device."),
)
.arg(Arg::with_name("path").takes_value(true))
.arg(
Arg::with_name("udid")
.short("u")
.value_name("udid")
.help("target specific device by UDID")
.takes_value(true),
)
.arg(
Arg::with_name("source_udid")
.short("s")
.long("source")
.value_name("source_udid")
.help("source identifier of backup data?")
.takes_value(true),
),
)
.subcommand(
SubCommand::with_name("restore")
.about("start restore service")
.arg(Arg::with_name("path").takes_value(true))
.arg(
Arg::with_name("udid")
.short("u")
.value_name("udid")
.help("target specific device by UDID")
.takes_value(true),
)
.arg(
Arg::with_name("source_udid")
.short("s")
.long("source")
.value_name("source_udid")
.help("source identifier of backup data?")
.required(true)
.takes_value(true),
)
.arg(
Arg::with_name("system")
.long("system")
.help("restore system files, too")
.takes_value(false),
)
.arg(
Arg::with_name("settings")
.long("settings")
.help("restore device settings from the backup")
.takes_value(false),
)
.arg(
Arg::with_name("no-reboot")
.long("no-reboot")
.help("do NOT reboot the device when done (default: yes)")
.takes_value(false),
)
.arg(
Arg::with_name("copy")
.long("copy")
.help("create a copy of backup folder before restoring")
.takes_value(false),
)
.arg(
Arg::with_name("remove")
.long("remove")
.help("remove items which are not being restored")
.takes_value(false),
)
.arg(
Arg::with_name("skip-apps")
.long("skip-apps")
.help("do not trigger re-installation of apps after restore")
.takes_value(false),
),
)
.get_matches();
// let udid = matches.value_of("udid");
// let domain = matches.value_of("domain");
// let key = matches.value_of("key");
let debug: bool = matches.is_present("debug");
if debug {
println!("debug");
unsafe { ffi::idevice_set_debug_level(1) };
}
if let Some(matches) = matches.subcommand_matches("backup") {
let result = start_backup(&matches, debug);
println!("start backup result: {:?}", result);
} else if let Some(matches) = matches.subcommand_matches("restore") {
start_restore(&matches, debug)?;
}
Ok(())
}
fn start_restore(matches: &ArgMatches, debug: bool) -> Result<()> {
println!("args: {:?}", matches);
let udid = matches.value_of("udid");
// source_udid is required
let source_udid = matches.value_of("source_udid").unwrap();
let path = matches.value_of("path").ok_or(anyhow!("No Path"))?;
let backup_root_directory_path = PathBuf::from(path);
println!(
"backup root directory path: {:?}",
backup_root_directory_path
);
// 1. check for existence of Info.plist
let mut info_plist_path = backup_root_directory_path.clone();
info_plist_path.push(&source_udid);
info_plist_path.push("Info.plist");
let info_plist_path = info_plist_path;
if !info_plist_path.exists() {
panic!("{:?} does not exist", info_plist_path);
}
let mut file = File::open(info_plist_path)?;
let info_plist = Plist::from(&mut file)?;
// 2. check for Manifest.plist
let mut manifest_plist_path = backup_root_directory_path.clone();
manifest_plist_path.push(&source_udid);
manifest_plist_path.push("Manifest.plist");
let manifest_plist_path = manifest_plist_path;
if !manifest_plist_path.exists() {
panic!("{:?} does not exist", manifest_plist_path);
}
// 3. read Manifest.plist
let mut file = File::open(manifest_plist_path)?;
let manifest_plist = Plist::from(&mut file)?;
// 4. check IsEncrypted in Manifest.plist
let _is_encrypted = manifest_plist
.dict_get_item("IsEncrypted")
.ok_or(anyhow!("failed to get IsEncrypted from Manifest.plist",))?;
let _is_encrypted = _is_encrypted
.bool_value()
.ok_or(anyhow!("failed to get bool value of IsEncrypted"))?;
// 5. connect to lockdownd (same to backup)
let device = Rc::new(Device::new_with_options(
udid,
idevice_options::IDEVICE_LOOKUP_USBMUX,
)?);
let lockdownd_client = LockdowndClient::new_with_handshake(device.clone(), crate_name!())?;
let udid_str: String;
if let Some(udid) = udid {
udid_str = String::from(udid);
} else {
udid_str = device.get_udid()?
}
let info_target_udid = info_plist
.dict_get_item("Target Identifier")
.ok_or(anyhow!("failed to retrieve 'Target Identifier'"))?;
let info_target_udid = info_target_udid.string_value().ok_or(anyhow!(
"failed to extract 'Target Identifier' into string value"
))?;
if info_target_udid != source_udid {
return Err(anyhow!(
"source({}) is not same as Target Indentifier({}) inside Info.plist",
source_udid,
info_target_udid
));
}
// 6. check for "WillEncrypt" (same to backup)
let will_encrypt_plist =
lockdownd_client.get_value(Some("com.apple.mobile.backup"), Some("WillEncrypt"));
println!("{:?}", will_encrypt_plist);
// 7. check for ProductVersion (same to backup)
let product_version = lockdownd_client.get_value(None, Some("ProductVersion"));
println!("ProductVersion: {:?}", product_version);
let product_version: Option =
product_version.map_or(None, |v| v.map_or(None, |v| v.string_value()));
println!("ProductVersion: {:?}", product_version);
// 8. start lockdownd service (same to backup)
// 9. start AFC service (same to backup)
// 10. start mobilebackup2 service (same to backup)
// 11. create mobilebackup2 client (same to backup)
// 12. exchange mobilebackup2 version (same to backup)
// 13. read Info.plist (same to backup)
// 14. post notification NP_SYNC_WILL_START and afc_file_open (same to backup)
// 15. lock file (same to backup)
let (mobilebackup2_client, afc_client, np_client, afc_lock_handle) =
pre_start_service(&lockdownd_client, device.clone())?;
// 16. check Status.plist / snapshot status
let mut status_plist_path = backup_root_directory_path.clone();
status_plist_path.push(source_udid);
status_plist_path.push("Status.plist");
let status_plist_path = status_plist_path;
if !status_plist_path.exists() {
panic!("{:?} does not exist", status_plist_path);
}
let mut file = File::open(status_plist_path)?;
let plist = Plist::from(&mut file)?;
let snapshot_state = plist
.dict_get_item("SnapshotState")
.ok_or(anyhow!("failed to get SnapshotState in Status.plist"))?;
let snapshot_state = snapshot_state
.string_value()
.ok_or(anyhow!("cannot read SnapshotState from Status.plist"))?;
if snapshot_state != "finished" {
return Err(anyhow!("This snapshot is not finished"));
}
// 17. setup restore options
if !matches.is_present("skip-apps") {
write_restore_applications(&info_plist, &afc_client)?;
}
let mut opts = Plist::new_dict().ok_or(anyhow!("failed to new plist dict"))?;
let key = "RestoreSystemFiles"; // include Photos, etc
let system = matches.is_present("system");
let value = Plist::new_bool(system).ok_or(anyhow!("failed to new plist bool for {}", key))?;
opts.dict_set_item(key, value);
let key = "RestoreShouldReboot";
let no_reboot = matches.is_present("no-reboot");
let value =
Plist::new_bool(!no_reboot).ok_or(anyhow!("failed to new plist bool for {}", key))?;
opts.dict_set_item(key, value);
let key = "RestoreDontCopyBackup";
let copy = matches.is_present("copy");
let value = Plist::new_bool(!copy).ok_or(anyhow!("failed to new plist bool for {}", key))?;
opts.dict_set_item(key, value);
let key = "RestorePreserveSettings";
let settings = matches.is_present("settings");
let value = Plist::new_bool(settings).ok_or(anyhow!("failed to new plist bool for {}", key))?;
opts.dict_set_item(key, value);
let key = "RemoveItemsNotRestored";
let remove = matches.is_present("remove");
let value = Plist::new_bool(remove).ok_or(anyhow!("failed to new plist bool for {}", key))?;
opts.dict_set_item(key, value);
// 18. start restore
mobilebackup2_client.send_request("Restore", &udid_str, Some(&source_udid), Some(&opts))?;
// 19. restore / message handling loop (share loop code to backup)
let result = handle_process_loop(
&mobilebackup2_client,
np_client.clone(),
&backup_root_directory_path,
Some(afc_lock_handle),
debug,
);
if result.is_err() {
// failed to restore, then remove /iTunesRestore/
let _ = afc_client.remove_path(ITUNES_RESTORE_RESTORE_APPLICATION_PLIST_FILE);
let _ = afc_client.remove_path(ITUNES_RESTORE_DIR);
}
// force drop sequence
// checked dop sequence by adding println! to related fn drop
// but still has some communication error before sending
// DLMessageDisconnect message
drop(lockdownd_client);
drop(mobilebackup2_client);
drop(afc_client);
drop(np_client);
drop(device);
result
}
pub fn start_backup(matches: &ArgMatches, debug: bool) -> Result<()> {
println!("args: {:?}", matches);
let path = matches.value_of("path").ok_or(anyhow!("No Path"))?;
let backup_root_directory_path = Path::new(path);
println!(
"backup root directory path: {:?}",
backup_root_directory_path
);
let udid = matches.value_of("udid");
let domain = matches.value_of("domain");
let key = matches.value_of("key");
let source_udid = matches.value_of("source_udid");
let device = Rc::new(Device::new_with_options(
udid,
idevice_options::IDEVICE_LOOKUP_USBMUX | idevice_options::IDEVICE_LOOKUP_NETWORK,
)?);
let lockdownd_client = LockdowndClient::new_with_handshake(device.clone(), crate_name!())?;
let _plist = lockdownd_client.get_value(domain, key)?;
let udid_str: String;
if let Some(udid) = udid {
udid_str = String::from(udid);
} else {
udid_str = device.get_udid()?
}
let source_udid_str: String;
if let Some(udid) = source_udid {
source_udid_str = String::from(udid);
} else {
source_udid_str = udid_str.clone();
}
println!(
"udid: {}, source_udid: {}",
udid_str.as_str(),
source_udid_str.as_str()
);
let mut target_backup_directory_path = PathBuf::from(backup_root_directory_path);
target_backup_directory_path.push(source_udid_str.as_str());
let target_backup_directory_path = target_backup_directory_path;
let mut info_plist_path = target_backup_directory_path.clone();
info_plist_path.push("Info.plist");
let info_plist_path = info_plist_path;
let will_encrypt_plist =
lockdownd_client.get_value(Some("com.apple.mobile.backup"), Some("WillEncrypt"));
println!("{:?}", will_encrypt_plist);
let will_encrypt_plist = will_encrypt_plist.map_or(false, |v| {
v.map_or(false, |v| v.bool_value().unwrap_or(false))
});
println!("will_encrypt_plist: {:?}", will_encrypt_plist);
let product_version = lockdownd_client.get_value(None, Some("ProductVersion"));
println!("ProductVersion: {:?}", product_version);
let product_version: Option =
product_version.map_or(None, |v| v.map_or(None, |v| v.string_value()));
println!("ProductVersion: {:?}", product_version);
let (mobilebackup2_client, afc_client, np_client, afc_lock_handle) =
pre_start_service(&lockdownd_client, device.clone())?;
println!("Starting backup...");
// fs::DirBuilder::new()
// .recursive(true)
// .create(&target_backup_directory_path)?;
let info_plist =
mobilebackup_factory_info_plist_new(udid_str.as_str(), device.clone(), &afc_client)?;
if debug {
println!("info_plist: {:#?}", info_plist);
}
let data = info_plist.to_xml().ok_or(anyhow!(format!(
"failed to generate xml format({} @ {}",
std::file!(),
std::line!()
)))?;
let mut file = File::create(info_plist_path)?;
file.write(&data[..])?;
println!("Requesting backup from device...");
mobilebackup2_client.send_request("Backup", &udid_str, Some(&source_udid_str), None)?;
let result = handle_process_loop(
&mobilebackup2_client,
np_client.clone(),
&backup_root_directory_path,
Some(afc_lock_handle),
debug,
);
// force drop sequence
drop(lockdownd_client);
drop(mobilebackup2_client);
drop(afc_client);
drop(np_client);
drop(device);
result
}
fn pre_start_service(
lockdownd_client: &LockdowndClient,
device: Rc,
) -> Result<(Mobilebackup2Client, AfcClient, Rc, AfcFileHandle)> {
let service = lockdownd_client.start_service(ffi::NP_SERVICE_NAME)?;
let np_client = Rc::new(NpClient::new(device.clone(), &service)?);
let error = np_client.set_notify_callback(|notification| {
println!("notification inside closure: {}", notification);
});
println!("set_notify_callback: {:?}", error);
let notification_spec: &[&[u8]] = &[
ffi::NP_SYNC_CANCEL_REQUEST,
ffi::NP_SYNC_SUSPEND_REQUEST,
ffi::NP_SYNC_RESUME_REQUEST,
ffi::NP_BACKUP_DOMAIN_CHANGED,
];
let error = np_client.observe_notifications(notification_spec);
println!("observe_notification: {:?}", error);
let service = lockdownd_client.start_service(ffi::AFC_SERVICE_NAME)?;
let afc_client = AfcClient::new(&device, &service)?;
let service =
lockdownd_client.start_service_with_escrow_bag(ffi::MOBILEBACKUP2_SERVICE_NAME)?;
let mobilebackup2_client = Mobilebackup2Client::new(device.clone(), &service)?;
let local_versions: &[f64] = &[2.0, 2.1];
let remote_version = mobilebackup2_client.version_exchange(local_versions)?;
println!("mobile2backup remote version: {}", remote_version);
do_post_notification(np_client.clone(), ffi::NP_SYNC_WILL_START)?;
let lock_path = "/com.apple.itunes.lock_sync";
let afc_lock_handle = afc_client.file_open(&lock_path, ffi::afc_file_mode_t::AFC_FOPEN_RW)?;
do_post_notification(np_client.clone(), ffi::NP_SYNC_LOCK_REQUEST)?;
const LOCK_ATTEMPTS: u32 = 50;
const LOCK_WAIT: u64 = 200;
let mut locked = false;
// TODO: not a good logic. can I wrap/hide handle in a struct, and hide loop detail in some structure?
for _ in 0..LOCK_ATTEMPTS {
let rst = afc_lock_handle.lock(ffi::afc_lock_op_t::AFC_LOCK_EX);
match rst {
Ok(_) => {
locked = true;
break;
}
Err(AfcError(ffi::afc_error_t::AFC_E_OP_WOULD_BLOCK)) => {
thread::sleep(time::Duration::from_millis(LOCK_WAIT));
}
Err(AfcError(_other)) => {
break;
}
}
}
if !locked {
return Err(anyhow!(format!(
"ERROR: could not lock file! error code: {:?}",
ffi::afc_error_t::AFC_E_INTERNAL_ERROR
)));
}
do_post_notification(np_client.clone(), ffi::NP_SYNC_DID_START)?;
Ok((mobilebackup2_client, afc_client, np_client, afc_lock_handle))
}
fn handle_process_loop(
mobilebackup2_client: &Mobilebackup2Client,
np_client: Rc,
backup_root_directory_path: &Path,
lock_file_handle: Option,
debug: bool,
) -> Result<()> {
let result = process_loop(mobilebackup2_client, backup_root_directory_path, debug);
if let Some(handle) = lock_file_handle {
handle.lock(ffi::afc_lock_op_t::AFC_LOCK_UN)?;
do_post_notification(np_client.clone(), ffi::NP_SYNC_DID_FINISH)?;
}
result
}
fn process_loop(
mobilebackup2_client: &Mobilebackup2Client,
backup_root_directory_path: &Path,
debug: bool,
) -> Result<()> {
loop {
println!("receiving message ...");
let result = mobilebackup2_client.receive_message();
match result {
Ok(message) => {
if debug {
println!("message: {:#?}", message);
}
match message {
ReceivedMessage::UploadFiles(ref plist) => {
mb2_handle_receive_files_message(
&mobilebackup2_client,
plist,
&backup_root_directory_path,
)?;
}
ReceivedMessage::DownloadFiles(ref plist) => {
mb2_handle_send_files(
&mobilebackup2_client,
plist,
&backup_root_directory_path,
)?;
}
ReceivedMessage::ContentsOfDirectory(ref plist) => {
mb2_handle_contents_of_directory_message(
&mobilebackup2_client,
plist,
&backup_root_directory_path,
)?;
}
ReceivedMessage::GetFreeDiskSpace => {
mb2_handle_get_free_space(
&mobilebackup2_client,
&backup_root_directory_path,
)?;
}
ReceivedMessage::CreateDirectory(ref plist) => {
mb2_handle_create_directory_message(
&mobilebackup2_client,
plist,
&backup_root_directory_path,
)?;
}
ReceivedMessage::MoveFiles(ref plist)
| ReceivedMessage::MoveItems(ref plist) => {
mb2_handle_move_files_message(
&mobilebackup2_client,
plist,
&backup_root_directory_path,
)?;
}
ReceivedMessage::RemoveFiles(ref plist)
| ReceivedMessage::RemoveItems(ref plist) => {
mb2_handle_remove_item_message(
&mobilebackup2_client,
plist,
&backup_root_directory_path,
)?;
}
ReceivedMessage::CopyItem(ref plist) => {
mb2_handle_copy_item_message(
&mobilebackup2_client,
plist,
&backup_root_directory_path,
)?;
}
ReceivedMessage::ProcessMessage(ref plist) => {
mb2_handle_process_message(&mobilebackup2_client, plist)?;
// need to quit cleanly
return Ok(());
}
ReceivedMessage::PurgeDiskSpace => {
mb2_handle_purge_disk_space_message(&mobilebackup2_client)?;
}
ReceivedMessage::Disconnect => return Ok(()),
// TODO: add missing arms
_ => (),
}
}
Err(Mobilebackup2Error(err)) => match err {
ffi::mobilebackup2_error_t::MOBILEBACKUP2_E_RECEIVE_TIMEOUT => {
println!("Receive time out, retry ...");
}
_ => {
println!("Receive message error: {:?}", err);
return Err(From::from(Mobilebackup2Error(err)));
}
},
}
}
}
fn write_restore_applications(info_plist: &Plist, afc_client: &AfcClient) -> Result<()> {
match info_plist.dict_get_item("Applications") {
Some(applications_plist) => {
let applications_xml = applications_plist
.to_xml()
.ok_or(anyhow!("failed to convert plist into xml"))?;
afc_client.make_directory(ITUNES_RESTORE_DIR)?;
let file = afc_client.file_open(
ITUNES_RESTORE_RESTORE_APPLICATION_PLIST_FILE,
ffi::afc_file_mode_t::AFC_FOPEN_WR,
)?;
let written = file.write(applications_xml.as_slice())?;
if written as usize != applications_xml.len() {
return Err(anyhow!(
"failed to write {} {} of {} bytes",
ITUNES_RESTORE_RESTORE_APPLICATION_PLIST_FILE,
written,
applications_xml.len()
));
}
}
None => eprintln!("failed to get 'Applications' entry, skipping"),
}
Ok(())
}
fn mb2_finishing_handling_message(client: &Mobilebackup2Client, result: Result<()>) -> Result<()> {
match result {
Ok(_) => {
let empty = Plist::new_dict();
client.send_status_response(0, None, empty)?;
Ok(())
}
Err(error) => {
println!("finishing handling message: {:?}", error);
match error.downcast_ref::() {
Some(io_error) => {
if let Some(errno) = io_error.raw_os_error() {
let msg = io_error.description();
client.send_os_status_response(errno, Some(msg))?;
} else {
client.send_os_status_response(22, None)?;
}
}
None => {
client.send_os_status_response(22, None)?;
}
}
Err(From::from(error))
}
}
}
fn mb2_handle_receive_files_message(
client: &Mobilebackup2Client,
message: &Plist,
backup_path: &Path,
) -> Result<()> {
let plist = message;
let backup_total_size = plist.array_get_item(3).ok_or(anyhow!(format!(
"total backup size in plist is not valud({} @ {}",
std::file!(),
std::line!()
)))?;
let backup_total_size = backup_total_size.uint_value().ok_or(anyhow!(format!(
"failed to get total backup size ({} @ {}",
std::file!(),
std::line!()
)))?;
if backup_total_size > 0 {
println!("Receiving files ...\n");
}
let result = mb2_receive_files(client, backup_path);
mb2_finishing_handling_message(client, result)?;
Ok(())
}
fn mb2_receive_files(client: &Mobilebackup2Client, backup_path: &Path) -> Result<()> {
// during test, it seems to be 256 * 1024 at most
const BUF_LEN: usize = 1024 * 1024; //32768;
let mut receiving_buffer = Vec::::new();
receiving_buffer.resize(BUF_LEN, 0);
let mut last_code = 0 as u8;
loop {
let _dname = match mb2_receive_filename(client) {
Ok(filename) => filename,
Err(other) => {
match other.downcast_ref::() {
Some(err) => {
let Mobilebackup2Error(errcode) = err;
match errcode {
ffi::mobilebackup2_error_t::MOBILEBACKUP2_E_SUCCESS => {
break;
}
_ => (),
}
}
_ => (),
}
return Err(From::from(other));
}
};
let local_name = mb2_receive_filename(client)?;
// println!("dname: {}", dname);
println!("local_name: {}", local_name);
let local_path = backup_path.join(local_name);
let mut file_length = client.receive_raw_u32()?;
let mut code: u8;
if file_length > 0 {
code = client.receive_raw_u8()?;
} else {
break;
}
if code != CODE_SUCCESS && code != CODE_FILE_DATA && code != CODE_ERROR_REMOTE {
println!("Found new flag: {:#x}", code);
}
let result = fs::remove_file(&local_path);
// Only I ignore not found error
if let Err(error) = result {
if io::ErrorKind::NotFound != error.kind() {
return Err(From::from(error));
}
}
let mut file = File::create(&local_path)?;
while file_length > 0 && code == CODE_FILE_DATA {
// minus code size (one byte)
let block_length = file_length - 1;
let _received_len = mb2_receive_block(
client,
&mut receiving_buffer,
block_length,
|buffer: &[u8]| {
file.write(&buffer[..])?;
Ok(())
},
)?;
file_length = client.receive_raw_u32()?;
if file_length > 0 {
last_code = code;
code = client.receive_raw_u8()?;
if code != CODE_SUCCESS && code != CODE_FILE_DATA && code != CODE_ERROR_REMOTE {
println!("Found new flag: {:#x}", code);
break;
}
} else {
break;
}
}
if file_length == 0 {
break;
}
// If sent using CODE_FILE_DATA, end marker will be CODE_ERROR_REMOTE which is not an error!
if code == CODE_ERROR_REMOTE {
let length = file_length - 1;
let mut message = String::with_capacity(length as usize);
mb2_receive_block(client, &mut receiving_buffer, length, |buffer: &[u8]| {
let msg = String::from_utf8_lossy(buffer);
if let Cow::Borrowed(msg) = msg {
message.push_str(msg);
} else if let Cow::Owned(msg) = msg {
message.push_str(&msg);
}
Ok(())
})?;
if last_code != CODE_FILE_DATA {
println!("Received an error message from device: {}", &message);
}
}
}
Ok(())
}
fn mb2_receive_block(
client: &Mobilebackup2Client,
buffer: &mut dyn AsMut>,
total_length: u32,
mut func: F,
) -> Mobilebackup2Result
where
F: FnMut(&[u8]) -> io::Result<()>,
{
let mut buffer = buffer.as_mut();
// receiving one block of CODE_FILE_DATA
let mut received_block_length = 0 as u32;
while received_block_length < total_length {
let left_length = total_length - received_block_length;
let buf_len = buffer.len();
let receiving_len = if left_length > buf_len as u32 {
buf_len as u32
} else {
left_length
};
let received_len = client.receive_raw(&mut buffer, receiving_len)?;
let _ = func(&buffer[..(received_len as usize)]);
if received_len == 0 {
break;
}
received_block_length += receiving_len;
}
Ok(received_block_length)
}
fn mb2_receive_filename(client: &Mobilebackup2Client) -> Result {
let length = mb2_receive_filename_length(client)?;
let full_length = length + 1;
let mut data = Vec::::with_capacity(full_length as usize);
data.resize(full_length as usize, 0);
let _read_length = client.receive_raw(&mut data, length)?;
let filename = CStr::from_bytes_with_nul(&data)?;
let filename = filename.to_string_lossy().into_owned();
Ok(filename)
}
fn mb2_receive_filename_length(client: &Mobilebackup2Client) -> Result {
loop {
let result = client.receive_raw_u32();
match result {
Ok(length) => {
if length > 0 {
return Ok(length);
} else {
// if length is 0, just return / break loop
return Err(From::from(Mobilebackup2Error(
ffi::mobilebackup2_error_t::MOBILEBACKUP2_E_SUCCESS,
)));
}
}
Err(Mobilebackup2Error(err)) => match err {
ffi::mobilebackup2_error_t::MOBILEBACKUP2_E_RECEIVE_TIMEOUT => {
continue;
}
_ => {
return Err(From::from(Mobilebackup2Error(err)));
}
},
}
}
}
fn mb2_handle_send_files(
client: &Mobilebackup2Client,
message: &Plist,
backup_path: &Path,
) -> Result<()> {
let plist = message;
let files = plist.array_get_item(1).ok_or(anyhow!(format!(
"failed to get files({} @ {}",
std::file!(),
std::line!()
)))?;
let count = files.array_get_size();
let mut errors = Vec::<(PathBuf, Plist)>::new();
for i in 0..count {
let file = files.array_get_item(i).ok_or(anyhow!(format!(
"failed to get file @ {}({} @ {}",
i,
std::file!(),
std::line!()
)))?;
// as noted in implementation in libimobiledevice/tools/idevicebackup2.c,
// here may just ignore error status and continue
let file = file.string_value().ok_or(anyhow!(format!(
"failed to get file name @ {}({} @ {}",
i,
std::file!(),
std::line!()
)))?;
let file = PathBuf::from(file);
let ret = mb2_handle_send_file(client, backup_path, &file, &mut errors);
if let Err(_err) = ret {
let path = backup_path.join(&file);
println!("failed to send file at {:?}", path);
}
}
println!("send terminating 0 dword");
// send terminating 0 dword
client.send_raw_u32(0)?;
let mut errplist = Plist::new_dict().unwrap();
if errors.len() > 0 {
for (path, plist) in errors {
let path = path.to_str().ok_or(anyhow!(format!(
"failed to convert path({:?}) to string({} @ {}",
path,
std::file!(),
std::line!()
)))?;
errplist.dict_set_item(path, plist);
}
println!("send_status_response: {:?}", errplist);
client.send_status_response(-13, Some("Multi status"), Some(errplist))?;
} else {
println!("send_status_response(0, null, empty)");
client.send_status_response(0, None, Some(errplist))?;
}
Ok(())
}
fn mb2_handle_send_file(
client: &Mobilebackup2Client,
backup_path: &Path,
path: &Path,
errors: &mut Vec<(PathBuf, Plist)>,
) -> Result<()> {
let local_full_path = backup_path.join(path);
let remote_path_str = path.to_str().ok_or(anyhow!(format!(
"failed to convert path({:?}) to string({} @ {}",
local_full_path,
std::file!(),
std::line!()
)))?;
let remote_path_cstr = CString::new(remote_path_str).map_err(|_| {
anyhow!(format!(
"failed to convert string({}) to cstring({} @ {}",
remote_path_str,
std::file!(),
std::line!()
))
})?;
println!("remote_path_cstr: {:?}", remote_path_cstr);
let data = remote_path_cstr.as_bytes();
// send path length and path
client.send_raw_u32(data.len() as u32)?;
client.send_raw(data)?;
if local_full_path.exists() {
let file_metadata = fs::metadata(&local_full_path)?;
let file_total_size = file_metadata.len() as usize;
println!("sending '{}' ({} bytes)", remote_path_str, file_total_size);
let mut f = File::open(&local_full_path)?;
const BUF_LEN: usize = 32768;
let mut buf = [0 as u8; BUF_LEN];
let mut sent: usize = 0;
while sent < file_total_size {
let diff = file_total_size - sent;
let length = if diff >= BUF_LEN { BUF_LEN } else { diff };
let mut real_buf = &mut buf[0..length];
f.read_exact(&mut real_buf)?;
mb2_send_block_header(client, length as u32, CODE_FILE_DATA)?;
client.send_raw(&real_buf)?;
sent += length as usize;
}
mb2_send_block_header(client, 0, CODE_SUCCESS)?;
println!("sending done");
Ok(())
} else {
let error_desc = "No such file or directory";
let error_desc_cstr = CString::new(error_desc).unwrap();
let data = error_desc_cstr.as_bytes();
let length = data.len();
println!("Sending block({}: {})", std::file!(), std::line!());
mb2_send_block_header(client, length as u32, CODE_ERROR_LOCAL)?;
client.send_raw(&data)?;
let plist = mb2_multi_status_add_file_error(&path, -6, error_desc);
errors.push((path.to_path_buf(), plist));
Err(From::from(io::Error::new(
io::ErrorKind::NotFound,
anyhow!(format!(
"path({:?}) is not found({} @ {}",
local_full_path,
std::file!(),
std::line!()
)),
)))
}
}
#[inline]
fn mb2_send_block_header(client: &Mobilebackup2Client, length: u32, code: u8) -> Result<()> {
let mut header = [0 as u8; 5];
let length = length + 1;
let bytes = length.to_be_bytes();
header[0..4].copy_from_slice(&bytes);
header[4] = code;
client.send_raw(&header)?;
Ok(())
}
fn mb2_multi_status_add_file_error(_path: &Path, error_code: i64, error_message: &str) -> Plist {
let mut filedict = Plist::new_dict().unwrap();
if let Some(error_message) = Plist::new_string(error_message) {
filedict.dict_set_item("DLFileErrorString", error_message);
}
if let Some(error_code) = Plist::new_int(error_code) {
filedict.dict_set_item("DLFileErrorCode", error_code);
}
filedict
}
fn mb2_handle_process_message(_client: &Mobilebackup2Client, message: &Plist) -> Result<()> {
println!("process message");
if let Some(root) = message.array_get_item(1) {
if let Plist::Dict(_) = &root {
if let Some(error_code) = root.dict_get_item("ErrorCode") {
if let Some(error_code) = error_code.uint_value() {
println!("error_code: {}", error_code);
if error_code == 0 {
} else {
}
}
if let Some(error_desc) = root.dict_get_item("ErrorDescription") {
if let Some(error_desc) = error_desc.string_value() {
println!("error_desc: {}", error_desc);
}
}
if let Some(content) = root.dict_get_item("Content") {
if let Some(content) = content.string_value() {
println!("content: {}", content);
}
}
}
}
}
Ok(())
}
fn mb2_handle_get_free_space(client: &Mobilebackup2Client, backup_path: &Path) -> Result<()> {
let path_cstr = CString::new(backup_path.as_os_str().as_bytes())?;
unsafe {
let mut stat: libc::statvfs = mem::zeroed();
let err = libc::statvfs(path_cstr.as_ptr(), &mut stat);
if err == 0 {
let freespace = stat.f_frsize * (stat.f_bavail as u64);
let plist = Plist::new_uint(freespace).expect(
format!(
"failed to create Plist::uint({} @ {}",
std::file!(),
std::line!()
)
.as_str(),
);
client.send_status_response(err, None, Some(plist))?;
Ok(())
} else {
Err(From::from(io::Error::last_os_error()))
}
}
}
fn mb2_handle_purge_disk_space_message(client: &Mobilebackup2Client) -> Result<()> {
let empty = Plist::new_dict().ok_or(anyhow!("failed to create emtpy plist dict"))?;
client.send_status_response(-1, Some("Operation not supported"), Some(empty))?;
Ok(())
}
fn mb2_handle_contents_of_directory_message(
client: &Mobilebackup2Client,
plist: &Plist,
backup_path: &Path,
) -> Result<()> {
let entry = plist.array_get_item(1).ok_or(anyhow!(format!(
"failed to get directory entry plist({} @ {}",
std::file!(),
std::line!()
)))?;
let entry = entry.string_value().ok_or(anyhow!(format!(
"failed to get directory entry string value({} @ {}",
std::file!(),
std::line!()
)))?;
let target_path = backup_path.join(entry);
let mut contents = Plist::new_dict().ok_or(anyhow!(format!(
"failed to create new dict({} @ {}",
std::file!(),
std::line!()
)))?;
let entries = fs::read_dir(target_path)?;
for entry in entries {
if let Ok(entry) = entry {
if let Ok(metadata) = entry.metadata() {
if let Some(mut filedict) = Plist::new_dict() {
const DL_FILE_TYPE_UNKNOWN: &'static str = "DLFileTypeUnknown";
const DL_FILE_TYPE_REGULAR: &'static str = "DLFileTypeRegular";
const DL_FILE_TYPE_DIRECTORY: &'static str = "DLFileTypeDirectory";
let filename_osstr = entry.file_name();
let filename = filename_osstr.to_str().ok_or(anyhow!(format!(
"failed to retreive filename({} @ {}",
std::file!(),
std::line!()
)))?;
let file_type: &str;
if metadata.is_dir() {
file_type = DL_FILE_TYPE_DIRECTORY;
} else if metadata.is_file() {
file_type = DL_FILE_TYPE_REGULAR;
} else {
file_type = DL_FILE_TYPE_UNKNOWN;
}
let file_type = Plist::new_string(file_type).ok_or(anyhow!(format!(
"failed to create plist string filename({} @ {}",
std::file!(),
std::line!()
)))?;
filedict.dict_set_item("DLFileType", file_type);
let file_size = metadata.len();
let file_size = Plist::new_uint(file_size).ok_or(anyhow!(format!(
"failed to create plist uint file size({} @ {}",
std::file!(),
std::line!()
)))?;
filedict.dict_set_item("DLFileSize", file_size);
if let Ok(modified) = metadata.modified() {
let modified =
Plist::new_date_from_systime(modified).ok_or(anyhow!(format!(
"failed to create plist date({} @ {}",
std::file!(),
std::line!()
)))?;
filedict.dict_set_item("DLFileModificationDate", modified);
}
contents.dict_set_item(filename, filedict);
}
}
}
}
client.send_status_response(0, None, Some(contents))?;
Ok(())
}
fn mb2_handle_create_directory_message(
client: &Mobilebackup2Client,
plist: &Plist,
backup_path: &Path,
) -> Result<()> {
let dir = plist.array_get_item(1).ok_or(anyhow!(format!(
"failed to get directory entry plist({} @ {}",
std::file!(),
std::line!()
)))?;
let dir = dir.string_value().ok_or(anyhow!(format!(
"failed to get directory entry string value({} @ {}",
std::file!(),
std::line!()
)))?;
let target_path = backup_path.join(dir);
let result = fs::DirBuilder::new().recursive(true).create(target_path);
if result.is_ok() {
client.send_status_response(0, None, None)?;
Ok(())
} else {
let oserror = client.send_last_os_status_response()?;
Err(From::from(oserror))
}
}
fn mb2_handle_move_files_message(
client: &Mobilebackup2Client,
plist: &Plist,
backup_path: &Path,
) -> Result<()> {
let files = plist.array_get_item(1).ok_or(anyhow!(format!(
"failed to get moves files({} @ {}",
std::file!(),
std::line!()
)))?;
let _size = files.dict_get_size();
let files_iter = files.dict_iter();
let iter = files_iter.ok_or(anyhow!(format!(
"failed to get files iterator({} @ {}",
std::file!(),
std::line!()
)))?;
if let Err(_error) = mb2_move_files(iter, backup_path) {
client.send_last_os_status_response()?;
Ok(())
} else {
let empty = Plist::new_dict();
client.send_status_response(0, None, empty)?;
Ok(())
}
}
fn mb2_move_files(iter: IterDict, backup_path: &Path) -> io::Result<()> {
for (oldpath, value) in iter {
if let Some(newpath) = value.string_value() {
let newpath = backup_path.join(newpath);
let oldpath = backup_path.join(oldpath);
if let Ok(metadata) = fs::metadata(&newpath) {
if metadata.is_dir() {
fs::remove_dir_all(&newpath)?;
} else {
fs::remove_file(&newpath)?;
}
}
fs::rename(oldpath, &newpath)?;
}
}
Ok(())
}
fn mb2_handle_remove_item_message(
client: &Mobilebackup2Client,
plist: &Plist,
backup_path: &Path,
) -> Result<()> {
let result = mb2_handle_remove_item(client, plist, backup_path);
mb2_finishing_handling_message(client, result)?;
Ok(())
}
fn mb2_handle_remove_item(
_client: &Mobilebackup2Client,
plist: &Plist,
backup_path: &Path,
) -> Result<()> {
let files = plist.array_get_item(1).ok_or(anyhow!(format!(
"failed to get removing files({} @ {}",
std::file!(),
std::line!()
)))?;
let size = files.array_get_size();
for i in 0..size {
if let Some(item) = files.array_get_item(i) {
if let Some(filename) = item.string_value() {
let filepath = backup_path.join(&filename);
mb2_remove_item(&filepath)?;
}
}
}
Ok(())
}
fn mb2_remove_item(target_path: &Path) -> Result<()> {
if let Ok(metadata) = fs::metadata(target_path) {
if metadata.is_dir() {
fs::remove_dir_all(target_path)?;
} else if metadata.is_file() {
fs::remove_file(target_path)?;
}
}
Ok(())
}
fn mb2_handle_copy_item_message(
client: &Mobilebackup2Client,
plist: &Plist,
backup_path: &Path,
) -> Result<()> {
let result = mb2_handle_copy_item(client, plist, backup_path);
mb2_finishing_handling_message(client, result)?;
Ok(())
}
fn mb2_handle_copy_item(
client: &Mobilebackup2Client,
plist: &Plist,
backup_path: &Path,
) -> Result<()> {
let srcpath = plist.array_get_item(1).ok_or(anyhow!(format!(
"failed to get copy src file({} @ {}",
std::file!(),
std::line!()
)))?;
let dstpath = plist.array_get_item(2).ok_or(anyhow!(format!(
"failed to get copy dest file({} @ {}",
std::file!(),
std::line!()
)))?;
let srcpath = srcpath.string_value().ok_or(anyhow!(format!(
"failed to get src file name string value({} @ {}",
std::file!(),
std::line!()
)))?;
let dstpath = dstpath.string_value().ok_or(anyhow!(format!(
"failed to get dest file name string value ({} @ {}",
std::file!(),
std::line!()
)))?;
let srcpath = backup_path.join(&srcpath);
let dstpath = backup_path.join(&dstpath);
let src_meta = fs::metadata(&srcpath)?;
if src_meta.is_dir() {
mb2_copy_dir(&srcpath, &dstpath)?;
} else if src_meta.is_file() {
mb2_copy_file(&srcpath, &dstpath)?;
}
let empty = Plist::new_dict();
client.send_status_response(0, None, empty)?;
Ok(())
}
fn mb2_copy_dir(srcpath: &Path, dstpath: &Path) -> Result<()> {
let entries = fs::read_dir(srcpath)?;
if !dstpath.exists() {
fs::create_dir(dstpath)?;
}
for entry in entries {
if let Ok(entry) = entry {
let filename = entry.file_name();
if let Ok(metadata) = entry.metadata() {
let srcpath = srcpath.join(&filename);
let dstpath = dstpath.join(&filename);
if metadata.is_file() {
mb2_copy_file(&srcpath, &dstpath)?;
} else if metadata.is_dir() {
mb2_copy_dir(&srcpath, &dstpath)?;
}
}
}
}
Ok(())
}
#[inline]
fn mb2_copy_file(srcpath: &Path, dstpath: &Path) -> Result<()> {
fs::copy(srcpath, dstpath)?;
Ok(())
}
fn mobilebackup_factory_info_plist_new(
_udid: &str,
device: Rc,
_afc: &AfcClient,
) -> Result {
let lockdownd_client = LockdowndClient::new_with_handshake(device.clone(), crate_name!())?;
let mut out_plist = Plist::new_dict().ok_or(anyhow!("new_dict returned None"))?;
let root_node = lockdownd_client
.get_value(None, None)?
.ok_or(anyhow!(format!(
"failed to get root node({} @ {}",
std::file!(),
std::line!()
)))?;
let _itunes_settings = lockdownd_client.get_value(Some("com.apple.iTunes"), None)?;
let _min_itunes_version =
lockdownd_client.get_value(Some("com.apple.mobile.iTunes"), Some("MinITunesVersion"))?;
let instproxy_client = InstproxyClient::start_service(device.clone(), crate_name!())?;
let mut options =
ClientOptions::new().ok_or(anyhow!("failed to create instproxy client new options"))?;
let mut key_values = HashMap::with_capacity(1);
key_values.insert(
"ApplicationType".to_string(),
OptionsType::String("User".to_string()),
);
options.add(key_values);
let attributes = vec![
"CFBundleIdentifier".to_string(),
"ApplicationSINF".to_string(),
"iTunesMetadata".to_string(),
];
options.set_return_attributes(attributes);
println!("options: {:#?}", options);
let apps = instproxy_client
.browse(&options)?
.ok_or(anyhow!("failed to browse apps"))?;
// println!("apps: {:#?}", apps);
let mut app_dict = Plist::new_dict().ok_or(anyhow!(format!(
"failed to create new dict({} @ {}",
std::file!(),
std::line!()
)))?;
let mut installed_apps = Plist::new_array().ok_or(anyhow!(format!(
"failed to create new array({} @ {}",
std::file!(),
std::line!()
)))?;
let size = apps.array_get_size();
for i in 0..size {
if let Some(entry) = apps.array_get_item(i) {
if let Some(bundle_id_plist) = entry.dict_get_item("CFBundleIdentifier") {
if let Some(bundle_id) = bundle_id_plist.string_value() {
installed_apps.array_append_item(bundle_id_plist);
if let Some(sinf) = entry.dict_get_item("ApplicationSINF") {
if let Some(meta) = entry.dict_get_item("iTunesMetadata") {
let mut adict = Plist::new_dict().ok_or(anyhow!(format!(
"failed to create new array({} @ {}",
std::file!(),
std::line!()
)))?;
adict.dict_set_item("ApplicationSINF", sinf);
adict.dict_set_item("iTunesMetadata", meta);
app_dict.dict_set_item(&bundle_id, adict);
}
}
};
}
}
}
out_plist.dict_set_item("Installed Applications", installed_apps);
out_plist.dict_set_item("Applications", app_dict);
let out_value = root_node
.dict_get_item("BuildVersion")
.ok_or(anyhow!(format!(
"failed to get BuildVersion({} @ {}",
std::file!(),
std::line!()
)))?;
out_plist.dict_set_item("Build Version", out_value);
let out_value = root_node
.dict_get_item("DeviceName")
.ok_or(anyhow!(format!(
"failed to get DeviceName({} @ {}",
std::file!(),
std::line!()
)))?;
out_plist.dict_set_item("Device Name", out_value.clone());
out_plist.dict_set_item("Display Name", out_value);
if let Some(out_value) = root_node.dict_get_item("IntegratedCircuitCardIdentity") {
match out_value {
Plist::None => (),
other => out_plist.dict_set_item("ICCID", other),
}
}
if let Some(out_value) = root_node.dict_get_item("InternationalMobileEquipmentIdentity") {
match out_value {
Plist::None => (),
other => out_plist.dict_set_item("IMEI", other),
}
}
let guid = Uuid::new_v4().simple().to_string();
let guid = Plist::new_string(guid.as_str()).ok_or(anyhow!(format!(
"failed to create GUID string({} @ {}",
std::file!(),
std::line!()
)))?;
out_plist.dict_set_item("GUID", guid);
let now = Utc::now();
let backup_date = Plist::new_date(now).ok_or(anyhow!(format!(
"failed to create backup date({} @ {}",
std::file!(),
std::line!()
)))?;
out_plist.dict_set_item("Last Backup Date", backup_date);
if let Some(out_value) = root_node.dict_get_item("MobileEquipmentIdentifier") {
match out_value {
Plist::None => (),
other => out_plist.dict_set_item("MEID", other),
}
}
if let Some(out_value) = root_node.dict_get_item("PhoneNumber") {
match out_value {
Plist::None => (),
other => out_plist.dict_set_item("Phone Number", other),
}
}
let out_value = root_node
.dict_get_item("ProductType")
.ok_or(anyhow!(format!(
"failed to get ProductType({} @ {}",
std::file!(),
std::line!()
)))?;
out_plist.dict_set_item("Product Type", out_value);
let out_value = root_node
.dict_get_item("ProductVersion")
.ok_or(anyhow!(format!(
"failed to get ProductVersion({} @ {}",
std::file!(),
std::line!()
)))?;
out_plist.dict_set_item("Product Version", out_value);
let out_value = root_node
.dict_get_item("SerialNumber")
.ok_or(anyhow!(format!(
"failed to get SerialNumber({} @ {}",
std::file!(),
std::line!()
)))?;
out_plist.dict_set_item("Serial Number", out_value);
let out_value = root_node
.dict_get_item("UniqueDeviceID")
.ok_or(anyhow!(format!(
"failed to get UniqueDeviceID({} @ {}",
std::file!(),
std::line!()
)))?;
out_plist.dict_set_item("Target Identifier", out_value);
let out_value = Plist::new_string("Device").ok_or(anyhow!(format!(
"failed to create \"Devive\" string ({} @ {}",
std::file!(),
std::line!()
)))?;
out_plist.dict_set_item("Target Type", out_value);
let udid_str = device.get_udid()?;
let udid_uppercase = udid_str.to_ascii_uppercase();
let out_value = Plist::new_string(udid_uppercase.as_str()).ok_or(anyhow!(format!(
"failed to create \"Udid\" string ({} @ {}",
std::file!(),
std::line!()
)))?;
out_plist.dict_set_item("Unique Identifier", out_value);
Ok(out_plist)
}
// fn mobilebackup_afc_get_file_contents(_afc: &AfcClient, _filename: &str) {
// }
fn do_post_notification(client: Rc, notification: &[u8]) -> Result<()> {
client.post_notification(notification)?;
Ok(())
}