Skip to main content

restorekit_sys/
lib.rs

1//! Raw FFI to the statically-linked idevicerestore client API
2//! (see `vendor/idevicerestore/src/idevicerestore.h`).
3#![allow(non_camel_case_types)]
4
5// Force the vendored static C deps into the final link. The idevicerestore
6// stack references their symbols (OpenSSL, zlib, libcurl) but our Rust code
7// doesn't, so we must keep the crates from being pruned.
8extern crate curl_sys as _;
9extern crate libz_sys as _;
10extern crate openssl_sys as _;
11
12use std::os::raw::{c_char, c_int, c_void};
13
14// Restore flags from idevicerestore.h.
15pub const FLAG_DEBUG: c_int = 1 << 1;
16pub const FLAG_ERASE: c_int = 1 << 2;
17
18// idevicerestore log levels (enum loglevel in log.h).
19pub const LL_ERROR: c_int = 0;
20pub const LL_WARNING: c_int = 1;
21pub const LL_NOTICE: c_int = 2;
22pub const LL_INFO: c_int = 3;
23pub const LL_VERBOSE: c_int = 4;
24pub const LL_DEBUG: c_int = 5;
25
26extern "C" {
27    /// idevicerestore's global logging threshold; messages above it are dropped.
28    static mut log_level: c_int;
29}
30
31/// Set idevicerestore's global log verbosity. Messages more verbose than
32/// `level` are never emitted (by default it dumps everything to stdout).
33pub fn set_log_level(level: c_int) {
34    unsafe {
35        log_level = level;
36    }
37}
38
39use std::sync::atomic::{AtomicBool, Ordering};
40use std::sync::Mutex;
41
42extern "C" {
43    /// Installs our log trampoline (see csrc/log_capture.c).
44    fn restorekit_install_log_capture();
45}
46
47static LOG_LINES: Mutex<Vec<(c_int, String)>> = Mutex::new(Vec::new());
48static LOG_ECHO: AtomicBool = AtomicBool::new(false);
49const LOG_CAPACITY: usize = 512;
50
51/// Optional sink that receives every captured log line as it arrives, for
52/// live-streaming the restore log (see [`set_log_sink`]).
53type LogSink = Box<dyn Fn(c_int, &str) + Send>;
54static LOG_SINK: Mutex<Option<LogSink>> = Mutex::new(None);
55
56/// Install (or clear, with `None`) a callback invoked for each idevicerestore
57/// log line as it is captured. Used to stream the restore log to a frontend.
58pub fn set_log_sink(sink: Option<LogSink>) {
59    *LOG_SINK.lock().unwrap_or_else(|e| e.into_inner()) = sink;
60}
61
62/// Called from C (log_capture.c) for each idevicerestore log line.
63///
64/// # Safety
65/// `msg`, if non-null, must be a valid NUL-terminated C string.
66#[no_mangle]
67pub unsafe extern "C" fn restorekit_log_capture(level: c_int, msg: *const c_char) {
68    if msg.is_null() {
69        return;
70    }
71    let text = std::ffi::CStr::from_ptr(msg).to_string_lossy();
72    let line = text.trim_end_matches(['\n', '\r']).to_string();
73    if line.is_empty() {
74        return;
75    }
76    if LOG_ECHO.load(Ordering::Relaxed) {
77        eprintln!("{line}");
78    }
79    if let Ok(guard) = LOG_SINK.lock() {
80        if let Some(sink) = guard.as_ref() {
81            sink(level, &line);
82        }
83    }
84    if let Ok(mut buf) = LOG_LINES.lock() {
85        if buf.len() >= LOG_CAPACITY {
86            buf.remove(0);
87        }
88        buf.push((level, line));
89    }
90}
91
92/// Route idevicerestore's logging into the capture sink. When `echo` is set,
93/// lines are also printed to stderr (for verbose mode). Clears prior lines.
94pub fn install_log_capture(echo: bool) {
95    LOG_ECHO.store(echo, Ordering::Relaxed);
96    if let Ok(mut buf) = LOG_LINES.lock() {
97        buf.clear();
98    }
99    unsafe { restorekit_install_log_capture() };
100}
101
102/// The last `max_lines` captured error/warning lines, newest-relevant last.
103pub fn error_tail(max_lines: usize) -> String {
104    let buf = match LOG_LINES.lock() {
105        Ok(b) => b,
106        Err(e) => e.into_inner(),
107    };
108    let errors: Vec<&str> = buf
109        .iter()
110        .filter(|(level, _)| *level <= LL_WARNING)
111        .map(|(_, line)| line.as_str())
112        .collect();
113    let start = errors.len().saturating_sub(max_lines);
114    errors[start..].join("\n")
115}
116
117// Progress step numbers (enum in idevicerestore.h).
118pub const RESTORE_STEP_DETECT: c_int = 0;
119pub const RESTORE_STEP_PREPARE: c_int = 1;
120pub const RESTORE_STEP_UPLOAD_FS: c_int = 2;
121pub const RESTORE_STEP_VERIFY_FS: c_int = 3;
122pub const RESTORE_STEP_FLASH_FW: c_int = 4;
123pub const RESTORE_STEP_FLASH_BB: c_int = 5;
124pub const RESTORE_STEP_FUD: c_int = 6;
125pub const RESTORE_STEP_UPLOAD_IMG: c_int = 7;
126
127/// Opaque idevicerestore client handle.
128#[repr(C)]
129pub struct idevicerestore_client_t {
130    _private: [u8; 0],
131}
132
133pub type idevicerestore_progress_cb_t =
134    Option<unsafe extern "C" fn(step: c_int, step_progress: f64, userdata: *mut c_void)>;
135
136extern "C" {
137    pub fn idevicerestore_client_new() -> *mut idevicerestore_client_t;
138    pub fn idevicerestore_client_free(client: *mut idevicerestore_client_t);
139    pub fn idevicerestore_set_ecid(client: *mut idevicerestore_client_t, ecid: u64);
140    pub fn idevicerestore_set_udid(client: *mut idevicerestore_client_t, udid: *const c_char);
141    pub fn idevicerestore_set_flags(client: *mut idevicerestore_client_t, flags: c_int);
142    pub fn idevicerestore_set_ipsw(client: *mut idevicerestore_client_t, path: *const c_char);
143    pub fn idevicerestore_set_cache_path(client: *mut idevicerestore_client_t, path: *const c_char);
144    pub fn idevicerestore_set_progress_callback(
145        client: *mut idevicerestore_client_t,
146        cbfunc: idevicerestore_progress_cb_t,
147        userdata: *mut c_void,
148    );
149    pub fn idevicerestore_start(client: *mut idevicerestore_client_t) -> c_int;
150    /// Register a progress bar (or, with a NULL label, just run the one-time
151    /// init of idevicerestore's global progress mutex).
152    pub fn register_progress(tag: u32, label: *const c_char);
153    /// Override the sink for idevicerestore's tagged progress bars. With a NULL
154    /// handler it prints them straight to stdout via `print_progress_bar`.
155    fn set_update_progress_func(
156        func: Option<unsafe extern "C" fn(list: *mut *mut c_void, count: c_int)>,
157    );
158}
159
160/// No-op sink for the tagged progress-bar system: swallow the update.
161unsafe extern "C" fn discard_progress(_list: *mut *mut c_void, _count: c_int) {}
162
163/// Stop idevicerestore's tagged progress bars (the `dfu.c`/`recovery.c`
164/// "Uploading" bars) from being dumped straight to stdout, where they corrupt
165/// `--json` output and interleave with our own progress UI. Step-level progress
166/// still flows through the progress callback. Call before starting a restore.
167pub fn suppress_tagged_progress() {
168    unsafe { set_update_progress_func(Some(discard_progress)) };
169}
170
171/// Initialize idevicerestore's progress subsystem before a restore.
172///
173/// Several of its progress functions (e.g. `finalize_progress`, called from
174/// `dfu_client_free`) lock a global mutex without the lazy-init guard that
175/// `set_progress`/`register_progress` run. On Windows that mutex is a
176/// `CRITICAL_SECTION`, so locking it uninitialized is an access violation.
177/// `register_progress` with a NULL label runs the init and returns without
178/// adding an entry.
179pub fn init_progress() {
180    unsafe { register_progress(0, std::ptr::null()) };
181}
182
183// ── Embedded usbmuxd server (Linux + Windows) ────────────────────────────────
184
185#[cfg(any(target_os = "linux", target_os = "windows"))]
186extern "C" {
187    fn restorekit_usbmuxd_start(socket_path: *const c_char) -> c_int;
188    fn restorekit_usbmuxd_run();
189    fn restorekit_usbmuxd_stop();
190    fn restorekit_usbmuxd_cleanup();
191}
192
193/// Initialize the embedded usbmuxd server, binding a Unix socket at `path`.
194/// Returns `Ok(())` on success.
195#[cfg(any(target_os = "linux", target_os = "windows"))]
196pub fn usbmuxd_start(path: &std::ffi::CStr) -> std::result::Result<(), c_int> {
197    let rc = unsafe { restorekit_usbmuxd_start(path.as_ptr()) };
198    if rc == 0 {
199        Ok(())
200    } else {
201        Err(rc)
202    }
203}
204
205/// Run the usbmuxd event loop (blocks until [`usbmuxd_stop`] is called).
206#[cfg(any(target_os = "linux", target_os = "windows"))]
207pub fn usbmuxd_run() {
208    unsafe { restorekit_usbmuxd_run() }
209}
210
211/// Signal the event loop to exit.
212#[cfg(any(target_os = "linux", target_os = "windows"))]
213pub fn usbmuxd_stop() {
214    unsafe { restorekit_usbmuxd_stop() }
215}
216
217/// Tear down USB devices, close the listen socket, and unlink the socket file.
218#[cfg(any(target_os = "linux", target_os = "windows"))]
219pub fn usbmuxd_cleanup() {
220    unsafe { restorekit_usbmuxd_cleanup() }
221}
222
223#[cfg(test)]
224mod tests {
225    use super::*;
226    use std::ffi::CString;
227
228    #[test]
229    fn error_tail_filters_by_level() {
230        if let Ok(mut b) = LOG_LINES.lock() {
231            b.clear();
232        }
233        LOG_ECHO.store(false, Ordering::Relaxed);
234
235        let cap = |level, s: &str| {
236            let c = CString::new(s).unwrap();
237            unsafe { restorekit_log_capture(level, c.as_ptr()) };
238        };
239        cap(LL_INFO, "info line");
240        cap(LL_ERROR, "boom -3");
241        cap(LL_VERBOSE, "verbose noise");
242        cap(LL_WARNING, "careful");
243
244        let tail = error_tail(10);
245        assert!(tail.contains("boom -3"));
246        assert!(tail.contains("careful"));
247        assert!(!tail.contains("info line"));
248        assert!(!tail.contains("verbose noise"));
249    }
250}