use std::io::Write;
use std::path::PathBuf;
use std::time::{Duration, Instant};
use indicatif::{ProgressBar, ProgressStyle};
use restorekit::progress::Event;
use restorekit::restore::Mode;
use restorekit::{firmware, restore, Device, Error, Result};
use super::render;
struct PhaseTimer {
start: Instant,
current: Option<(String, Instant, Option<Instant>)>,
phases: Vec<(String, Duration, Option<Duration>)>,
}
impl PhaseTimer {
fn new() -> Self {
Self {
start: Instant::now(),
current: None,
phases: Vec::new(),
}
}
fn step(&mut self, name: &str, progress: f32) {
let now = Instant::now();
let full = if progress >= 0.98 { Some(now) } else { None };
match &mut self.current {
Some((cur, _, full_at)) if cur == name => {
if full_at.is_none() && progress >= 0.98 {
*full_at = Some(now);
}
}
Some((cur, started, full_at)) => {
let active = full_at.map(|f| f - *started);
self.phases.push((cur.clone(), now - *started, active));
self.current = Some((name.to_string(), now, full));
}
None => self.current = Some((name.to_string(), now, full)),
}
}
fn finish(&mut self) {
if let Some((cur, started, full_at)) = self.current.take() {
let active = full_at.map(|f| f - started);
self.phases.push((cur, started.elapsed(), active));
}
}
fn report(&self, json: bool) {
if self.phases.is_empty() {
return;
}
let total = self.start.elapsed();
if json {
let phases: Vec<_> = self
.phases
.iter()
.map(|(n, d, a)| {
serde_json::json!({
"phase": n,
"seconds": d.as_secs_f64(),
"active_seconds": a.map(|a| a.as_secs_f64()),
})
})
.collect();
println!(
"{}",
serde_json::json!({ "event": "phase_timing", "total_seconds": total.as_secs_f64(), "phases": phases })
);
return;
}
eprintln!("\nRestore phase timing (active = data moving, wait = device working):");
for (name, dur, active) in &self.phases {
match active {
Some(a) if *dur > *a + Duration::from_secs(2) => eprintln!(
" {:<20} {:>7} ({} active, {} device-side)",
name,
fmt_dur(*dur),
fmt_dur(*a),
fmt_dur(*dur - *a)
),
_ => eprintln!(" {:<20} {:>7}", name, fmt_dur(*dur)),
}
}
eprintln!(" {:<20} {:>7}", "total", fmt_dur(total));
}
}
fn fmt_dur(d: Duration) -> String {
let s = d.as_secs();
if s >= 60 {
format!("{}m{:02}s", s / 60, s % 60)
} else {
format!("{s}s")
}
}
pub struct Opts {
pub mode: Mode,
pub ipsw: Option<PathBuf>,
pub os_version: Option<String>,
pub identifier: Option<String>,
pub ecid: Option<u64>,
pub dongle: Option<String>,
pub yes: bool,
pub cache_dir: Option<PathBuf>,
pub json: bool,
pub verbose: bool,
}
pub fn run(opts: Opts) -> Result<()> {
let device = super::dfu::ensure_present(
opts.json,
Duration::from_secs(120),
opts.dongle.clone(),
opts.ecid,
)?;
restore_device(&device, opts)
}
fn restore_device(device: &Device, opts: Opts) -> Result<()> {
let json = opts.json;
let identity = device
.identity
.as_ref()
.expect("restore target carries an identity");
let ecid = device.ecid.expect("restore target carries an ECID");
let cache = match &opts.cache_dir {
Some(d) => d.clone(),
None => firmware::default_cache_dir()?,
};
let ipsw_path = if let Some(path) = &opts.ipsw {
path.clone()
} else {
let identifier = opts
.identifier
.clone()
.or_else(|| device.identifier().map(str::to_string))
.ok_or(Error::UnknownModel {
cpid: identity.cpid,
bdid: identity.bdid,
})?;
say(json, &format!("Resolving firmware for {identifier}..."));
let fw = firmware::resolve(&identifier, opts.os_version.as_deref())?;
if json {
render::emit_json(&Event::FirmwareResolved {
identifier: fw.identifier.clone(),
version: fw.version.clone(),
build: fw.build.clone(),
size: fw.size,
url: fw.url.clone(),
});
} else {
let os = if device.is_t2() { "bridgeOS" } else { "macOS" };
println!(" {os} {} (build {})", fw.version, fw.build);
}
let bar = ProgressBar::hidden();
let path = firmware::download(&cache, &fw, &mut |e| render::download(&bar, e, json))?;
bar.finish_and_clear();
path
};
let mode = opts.mode;
if !confirm(device, mode, opts.yes, json)? {
say(json, "Aborted.");
return Ok(());
}
say(json, "Starting restore. Do not disconnect the target.");
#[cfg(target_os = "windows")]
say(
json,
" A Windows (UAC) prompt will appear to set up restore-mode USB access — approve it.",
);
let bar = if json || opts.verbose {
ProgressBar::hidden()
} else {
let b = ProgressBar::new(100);
b.set_style(
ProgressStyle::with_template("{msg:24} {bar:32.green/black} {percent:>3}%").unwrap(),
);
b
};
let mut wipe: Option<(String, String)> = None;
let mut checkpoints: Option<(Vec<String>, Vec<String>)> = None;
let mut timer = PhaseTimer::new();
let result = restore::restore(
&ipsw_path,
ecid,
Some(&cache),
mode,
opts.verbose,
&mut |event| {
match &event {
Event::RestoreStep { name, progress, .. } => timer.step(name, *progress),
Event::Obliteration { status, detail } => {
let strong = status == "confirmed" || status == "failed";
if wipe.is_none() || strong {
wipe = Some((status.clone(), detail.clone()));
}
}
Event::Checkpoints { json, raw } => {
checkpoints = Some((json.clone(), raw.clone()));
}
_ => {}
}
restore_render(&bar, event, json);
},
);
timer.finish();
bar.finish_and_clear();
timer.report(json);
report_wipe(json, wipe.as_ref());
let wipe_status = wipe.as_ref().map(|(s, _)| s.as_str());
if mode == Mode::Obliterate {
return match wipe_status {
Some("confirmed") => {
record_restore_history(device, mode, wipe.as_ref(), checkpoints.as_ref(), true);
say(json, completion_message(device, mode));
Ok(())
}
_ => {
record_restore_history(device, mode, wipe.as_ref(), checkpoints.as_ref(), false);
Err(result.err().unwrap_or(Error::RestoreFailed {
status: -1,
log_tail: "obliterate did not confirm the encryption-key wipe".into(),
}))
}
};
}
match result {
Ok(_) => {
record_restore_history(device, mode, wipe.as_ref(), checkpoints.as_ref(), true);
if wipe.as_ref().map(|(s, _)| s.as_str()) == Some("failed") {
return Err(Error::RestoreFailed {
status: -1,
log_tail: "device reported the encryption-key wipe FAILED".into(),
});
}
say(json, completion_message(device, mode));
Ok(())
}
Err(e) => {
record_restore_history(device, mode, wipe.as_ref(), checkpoints.as_ref(), false);
if wipe.as_ref().map(|(s, _)| s.as_str()) == Some("confirmed") {
say(
json,
"The encryption key was destroyed before the restore failed, so the \
old data is unrecoverable. Re-run the restore to reinstall the OS.",
);
}
Err(e)
}
}
}
#[cfg(feature = "history")]
fn record_restore_history(
device: &Device,
mode: Mode,
wipe: Option<&(String, String)>,
checkpoints: Option<&(Vec<String>, Vec<String>)>,
ok: bool,
) {
use restorekit::history::{self, HistoryEntry};
let obliteration = wipe.map(|(status, _)| status.clone());
let wiped = matches!(obliteration.as_deref(), Some("confirmed") | Some("failed"));
if !ok && !wiped {
return;
}
let (checkpoints_json, checkpoints_raw) = match checkpoints {
Some((j, r)) => (
(!j.is_empty()).then(|| serde_json::to_string(j).unwrap_or_default()),
(!r.is_empty()).then(|| serde_json::to_string(r).unwrap_or_default()),
),
None => (None, None),
};
let entry = HistoryEntry {
serial_number: device.srnm.clone(),
ecid: device.ecid_hex().unwrap_or_default(),
model_identifier: device.identifier().map(str::to_string),
name: device.display_name(),
mode: match mode {
Mode::Erase => "restore",
Mode::Revive => "revive",
Mode::Obliterate => "obliterate",
}
.to_string(),
status: match (ok, mode) {
(true, Mode::Obliterate) => "obliterated",
(true, _) => "restored",
(false, _) => "restore_failed",
}
.to_string(),
timestamp_rfc3339: history::now_rfc3339(),
obliteration,
checkpoints_json,
checkpoints_raw,
};
if let Err(e) = history::record(&entry) {
eprintln!("warning: could not record restore history: {e}");
}
}
#[cfg(not(feature = "history"))]
fn record_restore_history(
_: &Device,
_: Mode,
_: Option<&(String, String)>,
_: Option<&(Vec<String>, Vec<String>)>,
_: bool,
) {
}
fn report_wipe(json: bool, wipe: Option<&(String, String)>) {
let Some((status, detail)) = wipe else { return };
match status.as_str() {
"confirmed" => say(
json,
&format!("Encryption key obliterated (verified): {detail}"),
),
"failed" => say(
json,
&format!("WARNING: the device reported the encryption-key wipe FAILED: {detail}"),
),
"unconfirmed" => say(
json,
"Note: no encryption-key obliteration signal appeared in the device log. The \
erase still destroys the key; it just could not be verified. Re-run with \
--verbose to inspect the device log.",
),
_ => {}
}
}
fn completion_message(device: &Device, mode: Mode) -> &'static str {
match (device.is_t2(), mode) {
(_, Mode::Obliterate) => {
"Key obliterated. The Mac is wiped with no OS — run `restorekit restore` to reinstall."
}
(true, Mode::Erase) => {
"bridgeOS restore complete. Reinstall macOS via internet recovery (hold Cmd-R at boot)."
}
(true, Mode::Revive) => {
"bridgeOS revive complete. macOS and your data are preserved; the Mac should boot normally."
}
_ => "Restore complete. The target should boot to Setup Assistant.",
}
}
fn say(json: bool, msg: &str) {
if !json {
println!("{msg}");
}
}
fn confirm(device: &Device, mode: Mode, yes: bool, json: bool) -> Result<bool> {
if mode == Mode::Revive || yes {
return Ok(true);
}
if json {
return Err(Error::RestoreFailed {
status: -1,
log_tail: "refusing to erase without --yes in --json mode".into(),
});
}
println!();
println!(
" WARNING: this will ERASE ALL DATA on {} (ECID {}).",
device.display_name(),
device.ecid_hex().unwrap_or_default()
);
if mode == Mode::Obliterate {
println!(
" This destroys the encryption key and STOPS — no OS is reinstalled. The Mac will"
);
println!(
" be left wiped and unbootable; run `restorekit restore` afterward to make it usable."
);
} else if device.is_t2() {
println!(
" This T2 Mac will be wiped and restored to bridgeOS only — macOS is NOT reinstalled."
);
println!(
" Afterward you must reinstall macOS via internet recovery (hold Cmd-R at boot)."
);
println!(" To update bridgeOS without erasing, run `restorekit revive` instead.");
}
print!(" Type ERASE to continue: ");
std::io::stdout().flush()?;
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
Ok(input.trim() == "ERASE")
}
fn restore_render(bar: &ProgressBar, event: Event, json: bool) {
if json {
render::emit_json(&event);
return;
}
match event {
Event::RestoreStep { name, progress, .. } => {
bar.set_message(name);
bar.set_position((progress * 100.0) as u64);
}
Event::RestoreRetrying {
attempt,
max_attempts,
..
} => {
bar.println(format!(
"Connection to the target dropped; retrying restore ({}/{max_attempts})...",
attempt + 1
));
}
Event::Done => bar.set_position(100),
_ => {}
}
}