use std::{
future::Future,
pin::Pin,
sync::{
Arc,
atomic::{AtomicBool, Ordering},
},
};
use futures::{
StreamExt,
channel::mpsc::{UnboundedReceiver, UnboundedSender},
};
use plist::Value;
use tracing::{debug, error, info, warn};
use super::restored::RestoredClient;
use crate::{Idevice, IdeviceError, services::restore::RestoreError};
pub trait ComponentSource: Send {
fn read_component<'a>(
&'a mut self,
path: &'a str,
) -> Pin<Box<dyn Future<Output = Result<Vec<u8>, IdeviceError>> + Send + 'a>>;
#[allow(clippy::type_complexity)]
fn open_component<'a>(
&'a mut self,
path: &'a str,
) -> Pin<
Box<
dyn Future<Output = Result<Box<dyn ComponentReader + Send + 'a>, IdeviceError>>
+ Send
+ 'a,
>,
> {
Box::pin(async move {
let data = self.read_component(path).await?;
Ok(Box::new(BufferedComponentReader { data, pos: 0 })
as Box<dyn ComponentReader + Send>)
})
}
}
pub trait ComponentReader: Send {
fn read<'a>(
&'a mut self,
buf: &'a mut [u8],
) -> Pin<Box<dyn Future<Output = Result<usize, IdeviceError>> + Send + 'a>>;
}
struct BufferedComponentReader {
data: Vec<u8>,
pos: usize,
}
impl ComponentReader for BufferedComponentReader {
fn read<'a>(
&'a mut self,
buf: &'a mut [u8],
) -> Pin<Box<dyn Future<Output = Result<usize, IdeviceError>> + Send + 'a>> {
Box::pin(async move {
let n = (self.data.len() - self.pos).min(buf.len());
buf[..n].copy_from_slice(&self.data[self.pos..self.pos + n]);
self.pos += n;
Ok(n)
})
}
}
impl<R> ComponentSource for super::ipsw::Ipsw<R>
where
R: tokio::io::AsyncBufRead + tokio::io::AsyncSeek + Unpin + Send,
{
fn read_component<'a>(
&'a mut self,
path: &'a str,
) -> Pin<Box<dyn Future<Output = Result<Vec<u8>, IdeviceError>> + Send + 'a>> {
Box::pin(async move { self.read_file(path).await })
}
#[allow(clippy::type_complexity)]
fn open_component<'a>(
&'a mut self,
path: &'a str,
) -> Pin<
Box<
dyn Future<Output = Result<Box<dyn ComponentReader + Send + 'a>, IdeviceError>>
+ Send
+ 'a,
>,
> {
Box::pin(async move { self.open_entry_reader(path).await })
}
}
pub trait DataPortConnector: Send + Sync {
fn connect(
&self,
port: u16,
) -> Pin<Box<dyn Future<Output = Result<Idevice, IdeviceError>> + Send>>;
}
#[derive(Debug)]
pub struct ProviderDataPorts<'p> {
pub provider: &'p dyn crate::provider::IdeviceProvider,
}
impl DataPortConnector for ProviderDataPorts<'_> {
fn connect(
&self,
port: u16,
) -> Pin<Box<dyn Future<Output = Result<Idevice, IdeviceError>> + Send>> {
self.provider.connect(port)
}
}
#[derive(Debug, Clone, serde::Serialize)]
pub enum RestoreProgressEvent {
Operation {
operation: u64,
progress: u64,
},
Step(String),
Transfer {
component: String,
sent: u64,
total: Option<u64>,
},
}
#[derive(Debug, Clone, Default)]
pub struct RestoreCancel {
flag: Arc<AtomicBool>,
}
impl RestoreCancel {
pub fn new() -> Self {
Self::default()
}
pub fn cancel(&self) {
self.flag.store(true, Ordering::Relaxed);
}
pub fn is_cancelled(&self) -> bool {
self.flag.load(Ordering::Relaxed)
}
}
#[derive(Debug, Clone)]
pub struct RestoreProgressSender {
tx: UnboundedSender<RestoreProgressEvent>,
}
impl RestoreProgressSender {
pub(super) fn send(&self, event: RestoreProgressEvent) {
let _ = self.tx.unbounded_send(event);
}
}
#[derive(Debug)]
pub struct RestoreProgressReceiver {
rx: UnboundedReceiver<RestoreProgressEvent>,
}
impl RestoreProgressReceiver {
pub async fn recv(&mut self) -> Option<RestoreProgressEvent> {
self.rx.next().await
}
}
pub fn progress_channel() -> (RestoreProgressSender, RestoreProgressReceiver) {
let (tx, rx) = futures::channel::mpsc::unbounded();
(RestoreProgressSender { tx }, RestoreProgressReceiver { rx })
}
pub struct RestoreContext<'a> {
pub restored: &'a mut RestoredClient,
pub build_identity: &'a plist::Dictionary,
pub board_id: u64,
pub chip_id: u64,
pub ecid: u64,
pub tss_ticket: &'a [u8],
pub components: &'a mut dyn ComponentSource,
pub filesystem: Option<&'a mut dyn super::asr::FilesystemImage>,
pub data_ports: &'a mut dyn DataPortConnector,
pub progress: Option<RestoreProgressSender>,
pub cancel: Option<RestoreCancel>,
}
impl std::fmt::Debug for RestoreContext<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RestoreContext")
.field("tss_ticket_len", &self.tss_ticket.len())
.finish_non_exhaustive()
}
}
impl RestoreContext<'_> {
pub(super) fn without_filesystem(&mut self) -> RestoreContext<'_> {
RestoreContext {
restored: &mut *self.restored,
build_identity: self.build_identity,
board_id: self.board_id,
chip_id: self.chip_id,
ecid: self.ecid,
tss_ticket: self.tss_ticket,
components: &mut *self.components,
filesystem: None,
data_ports: &mut *self.data_ports,
progress: self.progress.clone(),
cancel: self.cancel.clone(),
}
}
pub(super) fn emit(&self, event: RestoreProgressEvent) {
if let Some(tx) = &self.progress {
tx.send(event);
}
}
pub(super) fn is_cancelled(&self) -> bool {
self.cancel
.as_ref()
.is_some_and(RestoreCancel::is_cancelled)
}
pub(super) fn check_cancel(&self) -> Result<(), IdeviceError> {
if self.is_cancelled() {
Err(IdeviceError::Restore(RestoreError::Cancelled))
} else {
Ok(())
}
}
}
pub async fn run_restore(
mut ctx: RestoreContext<'_>,
options: plist::Dictionary,
) -> Result<(), IdeviceError> {
ctx.restored.start_restore(options).await?;
let result = drive_restore(&mut ctx).await;
if matches!(&result, Err(IdeviceError::Restore(RestoreError::Cancelled))) {
info!("restore cancelled; rebooting device toward recovery");
if let Err(e) = ctx.restored.reboot().await {
debug!("reboot request during cancel failed (expected if link dropped): {e}");
}
}
if let Err(e) = ctx.restored.goodbye().await {
debug!("goodbye during teardown failed (expected if link dropped): {e}");
}
result
}
async fn drive_restore(ctx: &mut RestoreContext<'_>) -> Result<(), IdeviceError> {
loop {
ctx.check_cancel()?;
let message = ctx.restored.recv().await?;
let msg_type = message
.get("MsgType")
.and_then(Value::as_string)
.unwrap_or_default();
match msg_type {
"DataRequestMsg" | "AsyncDataRequestMsg" => {
super::data_request::dispatch(ctx, &message)
.await
.inspect_err(|e| {
error!("data request handler failed, aborting restore: {e}")
})?;
}
"ProgressMsg" => handle_progress(ctx, &message),
"StatusMsg" => {
if handle_status(ctx, &message).await? {
info!("restore finished successfully");
return Ok(());
}
}
"CheckpointMsg" => debug!("checkpoint: {message:?}"),
"PreviousRestoreLogMsg" => debug!("previous restore log received"),
"BBUpdateStatusMsg" => handle_bb_update_status(&message)?,
"BasebandUpdaterOutputData" => {
debug!("baseband updater output: {message:?}")
}
"RestoredCrash" => {
error!("restored crashed: {message:?}");
return Err(IdeviceError::Restore(RestoreError::RestoredCrashed));
}
"AsyncWait" => debug!("async wait: {message:?}"),
"RestoreAttestation" => {
debug!("restore attestation request; declining");
ctx.restored
.send(crate::plist!({ "RestoreShouldAttest": false }))
.await?;
}
other => warn!("unhandled MsgType `{other}`: {message:?}"),
}
}
}
fn handle_progress(ctx: &RestoreContext<'_>, message: &plist::Dictionary) {
let op = message
.get("Operation")
.and_then(Value::as_unsigned_integer);
let progress = message.get("Progress").and_then(Value::as_unsigned_integer);
debug!("progress: operation={op:?} progress={progress:?}");
if let (Some(op), Some(progress)) = (op, progress) {
ctx.emit(RestoreProgressEvent::Operation {
operation: op,
progress,
});
}
}
fn handle_bb_update_status(message: &plist::Dictionary) -> Result<(), IdeviceError> {
let accepted = message
.get("Accepted")
.and_then(Value::as_boolean)
.unwrap_or(false);
if !accepted {
let detail = message
.get("Error")
.and_then(Value::as_dictionary)
.and_then(|e| {
e.get("NSLocalizedDescription")
.or_else(|| e.get("NSDescription"))
})
.and_then(Value::as_string)
.unwrap_or("device did not accept BasebandData");
error!("baseband update rejected: {detail}");
return Err(IdeviceError::Restore(RestoreError::BasebandRejected(
detail.to_string(),
)));
}
let done = message
.get("Output")
.and_then(Value::as_dictionary)
.and_then(|o| o.get("done"))
.and_then(Value::as_boolean)
.unwrap_or(false);
if done {
info!("baseband update completed");
} else {
debug!("baseband update in progress");
}
Ok(())
}
async fn handle_status(
ctx: &mut RestoreContext<'_>,
message: &plist::Dictionary,
) -> Result<bool, IdeviceError> {
let status = message.get("Status").and_then(Value::as_signed_integer);
let amr_error = message.get("AMRError").and_then(Value::as_signed_integer);
if let Some(log) = message.get("Log").and_then(Value::as_string) {
debug!("device log:\n{log}");
}
if status == Some(0) && amr_error.unwrap_or(0) == 0 {
ctx.restored
.send(crate::plist!({ "MsgType": "ReceivedFinalStatusMsg" }))
.await?;
return Ok(true);
}
if message.contains_key("Error") || amr_error.unwrap_or(0) != 0 {
let detail = message
.get("Error")
.and_then(collect_error_descriptions)
.unwrap_or_else(|| "no description".into());
error!("device reported a fatal restore error (AMRError={amr_error:?}): {detail}");
return Err(IdeviceError::Restore(RestoreError::DeviceReported {
amr_error: amr_error.unwrap_or(-1),
detail,
}));
}
if let Some(s) = status {
error!("device reported restore status {s}");
}
Ok(false)
}
fn collect_error_descriptions(value: &Value) -> Option<String> {
fn walk(value: &Value, out: &mut Vec<String>) {
match value {
Value::Dictionary(d) => {
for (k, v) in d {
if k == "NSDescription" {
if let Some(s) = v
.as_dictionary()
.and_then(|c| c.get("Content"))
.and_then(Value::as_string)
.or_else(|| v.as_string())
{
out.push(s.trim().to_string());
}
}
walk(v, out);
}
}
Value::Array(a) => a.iter().for_each(|v| walk(v, out)),
_ => {}
}
}
let mut out = Vec::new();
walk(value, &mut out);
if out.is_empty() {
None
} else {
Some(out.join(" -> "))
}
}