use std::path::PathBuf;
use clap::{Parser, Subcommand};
mod commands;
#[derive(Parser)]
#[command(name = "restorekit", version, about = "DFU-restore Apple Silicon Macs")]
struct Cli {
#[arg(long, global = true)]
json: bool,
#[arg(long, global = true)]
cache_dir: Option<PathBuf>,
#[arg(short, long, global = true)]
verbose: bool,
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
List,
Dfu(TargetArgs),
Reboot(TargetArgs),
Download {
#[arg(long)]
identifier: Option<String>,
#[arg(long)]
os_version: Option<String>,
#[arg(long, value_parser = parse_ecid)]
ecid: Option<u64>,
},
Restore(RestoreArgs),
Revive(ReviveArgs),
Cache {
#[arg(long)]
clear: bool,
#[arg(long)]
path: bool,
},
#[cfg(feature = "history")]
History {
#[command(subcommand)]
action: HistoryAction,
},
#[cfg(target_os = "windows")]
SetupDriver {
#[arg(long, hide = true)]
elevated: bool,
#[arg(long, hide = true)]
result_file: Option<PathBuf>,
},
}
#[derive(clap::Args)]
struct TargetArgs {
#[arg(long, value_parser = parse_ecid, conflicts_with = "port")]
ecid: Option<u64>,
#[arg(long)]
port: Option<i32>,
}
impl TargetArgs {
fn into_target(self) -> restorekit::DfuTarget {
match (self.ecid, self.port) {
(Some(e), _) => restorekit::DfuTarget::Ecid(e),
(_, Some(rid)) => restorekit::DfuTarget::Port(rid),
_ => restorekit::DfuTarget::Auto,
}
}
}
#[cfg(feature = "history")]
#[derive(Subcommand)]
enum HistoryAction {
List,
Export {
path: PathBuf,
},
Clear,
}
#[derive(clap::Args)]
struct FirmwareArgs {
#[arg(long)]
ipsw: Option<PathBuf>,
#[arg(long)]
os_version: Option<String>,
#[arg(long)]
identifier: Option<String>,
#[arg(long, value_parser = parse_ecid)]
ecid: Option<u64>,
}
#[derive(clap::Args)]
struct RestoreArgs {
#[command(flatten)]
firmware: FirmwareArgs,
#[arg(long)]
yes: bool,
}
#[derive(clap::Args)]
struct ReviveArgs {
#[command(flatten)]
firmware: FirmwareArgs,
}
fn parse_ecid(s: &str) -> Result<u64, String> {
let s = s.trim();
let parsed = if let Some(hex) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
u64::from_str_radix(hex, 16)
} else if s.bytes().all(|b| b.is_ascii_digit()) {
s.parse()
} else {
u64::from_str_radix(s, 16)
};
parsed.map_err(|_| format!("invalid ECID '{s}': expected hex (0x…) or decimal"))
}
impl FirmwareArgs {
fn into_opts(
self,
revive: bool,
yes: bool,
cache_dir: Option<PathBuf>,
json: bool,
verbose: bool,
) -> commands::restore::Opts {
commands::restore::Opts {
revive,
ipsw: self.ipsw,
os_version: self.os_version,
identifier: self.identifier,
ecid: self.ecid,
yes,
cache_dir,
json,
verbose,
}
}
}
fn main() {
#[cfg(target_os = "windows")]
{
let args: Vec<String> = std::env::args().collect();
if let Some(i) = args
.iter()
.position(|a| a == restorekit::driver::RESTORE_WATCH_ARG)
{
if let Some(liveness) = args.get(i + 1) {
restorekit::driver::run_restore_mode_watch_worker(std::path::Path::new(liveness));
}
return;
}
}
let cli = Cli::parse();
let result = match cli.command {
Command::List => commands::list::run(cli.json),
Command::Dfu(t) => commands::dfu::enter(cli.json, t.into_target()),
Command::Reboot(t) => commands::dfu::reboot(cli.json, t.into_target()),
Command::Download {
identifier,
os_version,
ecid,
} => commands::download::run(identifier, os_version, ecid, cli.cache_dir, cli.json),
Command::Restore(args) => commands::restore::run(args.firmware.into_opts(
false,
args.yes,
cli.cache_dir,
cli.json,
cli.verbose,
)),
Command::Revive(args) => commands::restore::run(args.firmware.into_opts(
true,
false,
cli.cache_dir,
cli.json,
cli.verbose,
)),
Command::Cache { clear, path } => commands::cache::run(cli.cache_dir, clear, path),
#[cfg(feature = "history")]
Command::History { action } => match action {
HistoryAction::List => commands::history::list(cli.json),
HistoryAction::Export { path } => commands::history::export(path),
HistoryAction::Clear => commands::history::clear(),
},
#[cfg(target_os = "windows")]
Command::SetupDriver {
elevated,
result_file,
} => commands::setup_driver::run(cli.json, elevated, result_file),
};
if let Err(e) = result {
if cli.json {
println!(
"{}",
serde_json::json!({ "event": "error", "message": e.to_string() })
);
} else {
eprintln!("error: {e}");
}
std::process::exit(1);
}
}
#[cfg(test)]
mod tests {
use super::parse_ecid;
#[test]
fn parses_ecid_forms() {
assert_eq!(parse_ecid("0xC60A812345678").unwrap(), 0xc60a812345678);
assert_eq!(parse_ecid("0Xc60a812345678").unwrap(), 0xc60a812345678);
assert_eq!(parse_ecid("12345").unwrap(), 12345);
assert_eq!(parse_ecid("c60a812345678").unwrap(), 0xc60a812345678);
assert!(parse_ecid("nope!").is_err());
assert!(parse_ecid("").is_err());
}
}