use std::ffi::{c_void, CString};
use std::path::Path;
use std::sync::Mutex;
use restorekit_sys as sys;
use crate::error::{Error, Result};
use crate::progress::{Event, ProgressFn};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Mode {
Erase,
Revive,
}
fn step_name(step: i32) -> &'static str {
match step {
sys::RESTORE_STEP_DETECT => "detecting device",
sys::RESTORE_STEP_PREPARE => "preparing",
sys::RESTORE_STEP_UPLOAD_FS => "uploading filesystem",
sys::RESTORE_STEP_VERIFY_FS => "verifying filesystem",
sys::RESTORE_STEP_FLASH_FW => "flashing firmware",
sys::RESTORE_STEP_FLASH_BB => "flashing baseband",
sys::RESTORE_STEP_FUD => "flashing firmware updater",
sys::RESTORE_STEP_UPLOAD_IMG => "uploading image",
_ => "restoring",
}
}
static RESTORE_LOCK: Mutex<()> = Mutex::new(());
const RESTORE_STACK: usize = 64 * 1024 * 1024;
unsafe extern "C" fn progress_trampoline(step: i32, step_progress: f64, userdata: *mut c_void) {
if userdata.is_null() {
return;
}
let tx = &*(userdata as *const std::sync::mpsc::Sender<Event>);
let _ = tx.send(Event::RestoreStep {
step: step.max(0) as u32,
name: step_name(step).to_string(),
progress: step_progress as f32,
});
}
pub fn restore(
ipsw: &Path,
ecid: u64,
cache_dir: Option<&Path>,
mode: Mode,
verbose: bool,
progress: ProgressFn,
) -> Result<()> {
let _guard = RESTORE_LOCK.lock().unwrap_or_else(|e| e.into_inner());
#[cfg(any(target_os = "linux", target_os = "windows"))]
let _usbmuxd = crate::usbmuxd::UsbmuxdGuard::start(progress)?;
#[cfg(target_os = "windows")]
let _restore_watcher = crate::driver::spawn_restore_mode_watcher();
sys::install_log_capture(verbose);
sys::set_log_level(if verbose {
sys::LL_DEBUG
} else {
sys::LL_WARNING
});
sys::init_progress();
let ipsw = ipsw.to_path_buf();
let cache_dir = cache_dir.map(Path::to_path_buf);
let (tx, rx) = std::sync::mpsc::channel::<Event>();
let worker = std::thread::Builder::new()
.name("restore".into())
.stack_size(RESTORE_STACK)
.spawn(move || -> Result<()> {
let ipsw_c = CString::new(ipsw.as_os_str().to_string_lossy().as_bytes())
.map_err(|_| Error::Download("ipsw path contains a NUL byte".into()))?;
let cache_c = cache_dir
.map(|d| CString::new(d.as_os_str().to_string_lossy().as_bytes()))
.transpose()
.map_err(|_| Error::Download("cache path contains a NUL byte".into()))?;
unsafe {
let client = sys::idevicerestore_client_new();
if client.is_null() {
return Err(Error::RestoreFailed {
status: -1,
log_tail: "idevicerestore_client_new returned null".into(),
});
}
#[allow(clippy::redundant_closure_call)]
let result = (|| {
sys::idevicerestore_set_ecid(client, ecid);
let flags = match mode {
Mode::Erase => sys::FLAG_ERASE,
Mode::Revive => 0,
};
sys::idevicerestore_set_flags(client, flags);
sys::idevicerestore_set_ipsw(client, ipsw_c.as_ptr());
if let Some(cache) = &cache_c {
sys::idevicerestore_set_cache_path(client, cache.as_ptr());
}
sys::idevicerestore_set_progress_callback(
client,
Some(progress_trampoline),
&tx as *const std::sync::mpsc::Sender<Event> as *mut c_void,
);
let rc = sys::idevicerestore_start(client);
if rc == 0 {
Ok(())
} else {
let tail = sys::error_tail(20);
let log_tail = if tail.is_empty() {
format!("idevicerestore_start returned {rc}")
} else {
tail
};
Err(Error::RestoreFailed {
status: rc,
log_tail,
})
}
})();
sys::idevicerestore_client_free(client);
result
}
})
.map_err(|e| Error::RestoreFailed {
status: -1,
log_tail: format!("failed to spawn restore thread: {e}"),
})?;
for event in rx {
progress(event);
}
worker.join().unwrap_or_else(|_| {
Err(Error::RestoreFailed {
status: -1,
log_tail: "restore thread panicked".into(),
})
})?;
progress(Event::Done);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn step_names_have_fallback() {
assert_eq!(
step_name(sys::RESTORE_STEP_UPLOAD_FS),
"uploading filesystem"
);
assert_eq!(step_name(999), "restoring");
}
}