mirror of
https://github.com/jkcoxson/idevice.git
synced 2026-03-02 14:36:16 +01:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c752ee92c5 | ||
|
|
76d847664b | ||
|
|
1f7924b773 | ||
|
|
b459eebe9d | ||
|
|
c246362f54 | ||
|
|
bfe44e16e4 | ||
|
|
54439b85dd |
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -35,6 +35,8 @@ jobs:
|
||||
rustup target add aarch64-apple-ios-sim && \
|
||||
rustup target add aarch64-apple-darwin && \
|
||||
rustup target add x86_64-apple-darwin && \
|
||||
rustup target add aarch64-apple-ios-macabi && \
|
||||
rustup target add x86_64-apple-ios-macabi && \
|
||||
cargo install --force --locked bindgen-cli
|
||||
|
||||
- name: Build all Apple targets and examples/tools
|
||||
|
||||
28
README.md
28
README.md
@@ -47,6 +47,8 @@ To keep dependency bloat and compile time down, everything is contained in featu
|
||||
|------------------------|-----------------------------------------------------------------------------|
|
||||
| `afc` | Apple File Conduit for file system access.|
|
||||
| `amfi` | Apple mobile file integrity service |
|
||||
| `bt_packet_logger` | Capture Bluetooth packets. |
|
||||
| `companion_proxy` | Manage paired Apple Watches. |
|
||||
| `core_device_proxy` | Start a secure tunnel to access protected services. |
|
||||
| `crashreportcopymobile`| Copy crash reports.|
|
||||
| `debug_proxy` | Send GDB commands to the device.|
|
||||
@@ -55,34 +57,36 @@ To keep dependency bloat and compile time down, everything is contained in featu
|
||||
| `heartbeat` | Maintain a heartbeat connection.|
|
||||
| `house_arrest` | Manage files in app containers |
|
||||
| `installation_proxy` | Manage app installation and uninstallation.|
|
||||
| `springboardservices` | Control SpringBoard (e.g. UI interactions). Partial support.|
|
||||
| `misagent` | Manage provisioning profiles on the device.|
|
||||
| `mobilebackup2` | Manage backups.|
|
||||
| `mobile_image_mounter` | Manage DDI images.|
|
||||
| `installcoordination_proxy` | Manage app installation coordination.|
|
||||
| `location_simulation` | Simulate GPS locations on the device.|
|
||||
| `misagent` | Manage provisioning profiles on the device.|
|
||||
| `mobile_image_mounter` | Manage DDI images.|
|
||||
| `mobileactivationd` | Activate/Deactivate device.|
|
||||
| `mobilebackup2` | Manage backups.|
|
||||
| `pair` | Pair the device.|
|
||||
| `syslog_relay` | Relay system logs from the device |
|
||||
| `pcapd` | Capture network packets.|
|
||||
| `preboard_service` | Interface with Preboard.|
|
||||
| `restore_service` | Restore service (recovery/reboot).|
|
||||
| `screenshotr` | Take screenshots.|
|
||||
| `springboardservices` | Control SpringBoard (icons, wallpaper, orientation, etc.).|
|
||||
| `syslog_relay` | Relay system logs and OS trace logs from the device. |
|
||||
| `tcp` | Connect to devices over TCP.|
|
||||
| `tunnel_tcp_stack` | Naive in-process TCP stack for `core_device_proxy`.|
|
||||
| `tss` | Make requests to Apple's TSS servers. Partial support.|
|
||||
| `tunneld` | Interface with [pymobiledevice3](https://github.com/doronz88/pymobiledevice3)'s tunneld. |
|
||||
| `usbmuxd` | Connect using the usbmuxd daemon.|
|
||||
| `xpc` | Access protected services via XPC over RSD. |
|
||||
| `notification_proxy` | Post and observe iOS notifications. |
|
||||
|
||||
### Planned/TODO
|
||||
|
||||
Finish the following:
|
||||
|
||||
- springboard
|
||||
- webinspector
|
||||
|
||||
Implement the following:
|
||||
|
||||
- companion_proxy
|
||||
- diagnostics
|
||||
- mobilebackup2
|
||||
- notification_proxy
|
||||
- screenshot
|
||||
- webinspector
|
||||
- file_relay
|
||||
|
||||
As this project is done in my free time within my busy schedule, there
|
||||
is no ETA for any of these. Feel free to contribute or donate!
|
||||
|
||||
50
cpp/include/idevice++/crashreportcopymobile.hpp
Normal file
50
cpp/include/idevice++/crashreportcopymobile.hpp
Normal file
@@ -0,0 +1,50 @@
|
||||
// Jackson Coxson
|
||||
|
||||
#pragma once
|
||||
#include <idevice++/bindings.hpp>
|
||||
#include <idevice++/ffi.hpp>
|
||||
#include <idevice++/provider.hpp>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace IdeviceFFI {
|
||||
|
||||
using CrashReportCopyMobilePtr =
|
||||
std::unique_ptr<CrashReportCopyMobileHandle,
|
||||
FnDeleter<CrashReportCopyMobileHandle, crash_report_client_free>>;
|
||||
|
||||
class CrashReportCopyMobile {
|
||||
public:
|
||||
// Factory: connect via Provider
|
||||
static Result<CrashReportCopyMobile, FfiError> connect(Provider& provider);
|
||||
|
||||
// Factory: wrap an existing Idevice socket (consumes it on success)
|
||||
static Result<CrashReportCopyMobile, FfiError> from_socket(Idevice&& socket);
|
||||
|
||||
// Static: flush crash reports from system storage
|
||||
static Result<void, FfiError> flush(Provider& provider);
|
||||
|
||||
// Ops
|
||||
Result<std::vector<std::string>, FfiError> ls(const char* dir_path = nullptr);
|
||||
Result<std::vector<char>, FfiError> pull(const std::string& log_name);
|
||||
Result<void, FfiError> remove(const std::string& log_name);
|
||||
|
||||
// RAII / moves
|
||||
~CrashReportCopyMobile() noexcept = default;
|
||||
CrashReportCopyMobile(CrashReportCopyMobile&&) noexcept = default;
|
||||
CrashReportCopyMobile& operator=(CrashReportCopyMobile&&) noexcept = default;
|
||||
CrashReportCopyMobile(const CrashReportCopyMobile&) = delete;
|
||||
CrashReportCopyMobile& operator=(const CrashReportCopyMobile&) = delete;
|
||||
|
||||
CrashReportCopyMobileHandle* raw() const noexcept { return handle_.get(); }
|
||||
static CrashReportCopyMobile adopt(CrashReportCopyMobileHandle* h) noexcept {
|
||||
return CrashReportCopyMobile(h);
|
||||
}
|
||||
|
||||
private:
|
||||
explicit CrashReportCopyMobile(CrashReportCopyMobileHandle* h) noexcept : handle_(h) {}
|
||||
CrashReportCopyMobilePtr handle_{};
|
||||
};
|
||||
|
||||
} // namespace IdeviceFFI
|
||||
46
cpp/include/idevice++/notification_proxy.hpp
Normal file
46
cpp/include/idevice++/notification_proxy.hpp
Normal file
@@ -0,0 +1,46 @@
|
||||
#pragma once
|
||||
#include <idevice++/bindings.hpp>
|
||||
#include <idevice++/ffi.hpp>
|
||||
#include <idevice++/provider.hpp>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace IdeviceFFI {
|
||||
|
||||
using NotificationProxyPtr = std::unique_ptr<NotificationProxyClientHandle,
|
||||
FnDeleter<NotificationProxyClientHandle, notification_proxy_client_free>>;
|
||||
|
||||
class NotificationProxy {
|
||||
public:
|
||||
// Factory: connect via Provider
|
||||
static Result<NotificationProxy, FfiError> connect(Provider& provider);
|
||||
|
||||
// Factory: wrap an existing Idevice socket (consumes it on success)
|
||||
static Result<NotificationProxy, FfiError> from_socket(Idevice&& socket);
|
||||
|
||||
// Ops
|
||||
Result<void, FfiError> post_notification(const std::string& name);
|
||||
Result<void, FfiError> observe_notification(const std::string& name);
|
||||
Result<void, FfiError> observe_notifications(const std::vector<std::string>& names);
|
||||
Result<std::string, FfiError> receive_notification();
|
||||
Result<std::string, FfiError> receive_notification_with_timeout(u_int64_t interval);
|
||||
|
||||
// RAII / moves
|
||||
~NotificationProxy() noexcept = default;
|
||||
NotificationProxy(NotificationProxy&&) noexcept = default;
|
||||
NotificationProxy& operator=(NotificationProxy&&) noexcept = default;
|
||||
NotificationProxy(const NotificationProxy&) = delete;
|
||||
NotificationProxy& operator=(const NotificationProxy&) = delete;
|
||||
|
||||
NotificationProxyClientHandle* raw() const noexcept { return handle_.get(); }
|
||||
static NotificationProxy adopt(NotificationProxyClientHandle* h) noexcept {
|
||||
return NotificationProxy(h);
|
||||
}
|
||||
|
||||
private:
|
||||
explicit NotificationProxy(NotificationProxyClientHandle* h) noexcept : handle_(h) {}
|
||||
NotificationProxyPtr handle_{};
|
||||
};
|
||||
|
||||
} // namespace IdeviceFFI
|
||||
94
cpp/src/crashreportcopymobile.cpp
Normal file
94
cpp/src/crashreportcopymobile.cpp
Normal file
@@ -0,0 +1,94 @@
|
||||
// Jackson Coxson
|
||||
|
||||
#include <cstring>
|
||||
#include <idevice++/bindings.hpp>
|
||||
#include <idevice++/crashreportcopymobile.hpp>
|
||||
#include <idevice++/ffi.hpp>
|
||||
#include <idevice++/provider.hpp>
|
||||
|
||||
namespace IdeviceFFI {
|
||||
|
||||
// -------- Factory Methods --------
|
||||
|
||||
Result<CrashReportCopyMobile, FfiError> CrashReportCopyMobile::connect(Provider& provider) {
|
||||
CrashReportCopyMobileHandle* out = nullptr;
|
||||
FfiError e(::crash_report_client_connect(provider.raw(), &out));
|
||||
if (e) {
|
||||
return Err(e);
|
||||
}
|
||||
return Ok(CrashReportCopyMobile::adopt(out));
|
||||
}
|
||||
|
||||
Result<CrashReportCopyMobile, FfiError> CrashReportCopyMobile::from_socket(Idevice&& socket) {
|
||||
CrashReportCopyMobileHandle* out = nullptr;
|
||||
FfiError e(::crash_report_client_new(socket.raw(), &out));
|
||||
if (e) {
|
||||
return Err(e);
|
||||
}
|
||||
socket.release();
|
||||
return Ok(CrashReportCopyMobile::adopt(out));
|
||||
}
|
||||
|
||||
Result<void, FfiError> CrashReportCopyMobile::flush(Provider& provider) {
|
||||
FfiError e(::crash_report_flush(provider.raw()));
|
||||
if (e) {
|
||||
return Err(e);
|
||||
}
|
||||
return Ok();
|
||||
}
|
||||
|
||||
// -------- Ops --------
|
||||
|
||||
Result<std::vector<std::string>, FfiError>
|
||||
CrashReportCopyMobile::ls(const char* dir_path) {
|
||||
char** entries_raw = nullptr;
|
||||
size_t count = 0;
|
||||
|
||||
FfiError e(::crash_report_client_ls(handle_.get(), dir_path, &entries_raw, &count));
|
||||
if (e) {
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
std::vector<std::string> result;
|
||||
if (entries_raw) {
|
||||
result.reserve(count);
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
if (entries_raw[i]) {
|
||||
result.emplace_back(entries_raw[i]);
|
||||
::idevice_string_free(entries_raw[i]);
|
||||
}
|
||||
}
|
||||
std::free(entries_raw);
|
||||
}
|
||||
|
||||
return Ok(std::move(result));
|
||||
}
|
||||
|
||||
Result<std::vector<char>, FfiError>
|
||||
CrashReportCopyMobile::pull(const std::string& log_name) {
|
||||
uint8_t* data = nullptr;
|
||||
size_t length = 0;
|
||||
|
||||
FfiError e(::crash_report_client_pull(handle_.get(), log_name.c_str(), &data, &length));
|
||||
if (e) {
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
std::vector<char> result;
|
||||
if (data && length > 0) {
|
||||
result.assign(reinterpret_cast<char*>(data), reinterpret_cast<char*>(data) + length);
|
||||
::idevice_data_free(data, length);
|
||||
}
|
||||
|
||||
return Ok(std::move(result));
|
||||
}
|
||||
|
||||
Result<void, FfiError> CrashReportCopyMobile::remove(const std::string& log_name) {
|
||||
FfiError e(::crash_report_client_remove(handle_.get(), log_name.c_str()));
|
||||
if (e) {
|
||||
return Err(e);
|
||||
}
|
||||
return Ok();
|
||||
}
|
||||
|
||||
} // namespace IdeviceFFI
|
||||
82
cpp/src/notification_proxy.cpp
Normal file
82
cpp/src/notification_proxy.cpp
Normal file
@@ -0,0 +1,82 @@
|
||||
// Jackson Coxson
|
||||
|
||||
#include <idevice++/bindings.hpp>
|
||||
#include <idevice++/ffi.hpp>
|
||||
#include <idevice++/notification_proxy.hpp>
|
||||
#include <idevice++/provider.hpp>
|
||||
|
||||
namespace IdeviceFFI {
|
||||
|
||||
Result<NotificationProxy, FfiError> NotificationProxy::connect(Provider& provider) {
|
||||
NotificationProxyClientHandle* out = nullptr;
|
||||
FfiError e(::notification_proxy_connect(provider.raw(), &out));
|
||||
if (e) {
|
||||
provider.release();
|
||||
return Err(e);
|
||||
}
|
||||
return Ok(NotificationProxy::adopt(out));
|
||||
}
|
||||
|
||||
Result<NotificationProxy, FfiError> NotificationProxy::from_socket(Idevice&& socket) {
|
||||
NotificationProxyClientHandle* out = nullptr;
|
||||
FfiError e(::notification_proxy_new(socket.raw(), &out));
|
||||
if (e) {
|
||||
return Err(e);
|
||||
}
|
||||
socket.release();
|
||||
return Ok(NotificationProxy::adopt(out));
|
||||
}
|
||||
|
||||
Result<void, FfiError> NotificationProxy::post_notification(const std::string& name) {
|
||||
FfiError e(::notification_proxy_post(handle_.get(), name.c_str()));
|
||||
if (e) {
|
||||
return Err(e);
|
||||
}
|
||||
return Ok();
|
||||
}
|
||||
|
||||
Result<void, FfiError> NotificationProxy::observe_notification(const std::string& name) {
|
||||
FfiError e(::notification_proxy_observe(handle_.get(), name.c_str()));
|
||||
if (e) {
|
||||
return Err(e);
|
||||
}
|
||||
return Ok();
|
||||
}
|
||||
|
||||
Result<void, FfiError> NotificationProxy::observe_notifications(const std::vector<std::string>& names) {
|
||||
std::vector<const char*> ptrs;
|
||||
ptrs.reserve(names.size() + 1);
|
||||
for (const auto& n : names) {
|
||||
ptrs.push_back(n.c_str());
|
||||
}
|
||||
ptrs.push_back(nullptr);
|
||||
FfiError e(::notification_proxy_observe_multiple(handle_.get(), ptrs.data()));
|
||||
if (e) {
|
||||
return Err(e);
|
||||
}
|
||||
return Ok();
|
||||
}
|
||||
|
||||
Result<std::string, FfiError> NotificationProxy::receive_notification() {
|
||||
char* name_ptr = nullptr;
|
||||
FfiError e(::notification_proxy_receive(handle_.get(), &name_ptr));
|
||||
if (e) {
|
||||
return Err(e);
|
||||
}
|
||||
std::string name(name_ptr);
|
||||
::notification_proxy_free_string(name_ptr);
|
||||
return Ok(std::move(name));
|
||||
}
|
||||
|
||||
Result<std::string, FfiError> NotificationProxy::receive_notification_with_timeout(u_int64_t interval) {
|
||||
char* name_ptr = nullptr;
|
||||
FfiError e(::notification_proxy_receive_with_timeout(handle_.get(), interval, &name_ptr));
|
||||
if (e) {
|
||||
return Err(e);
|
||||
}
|
||||
std::string name(name_ptr);
|
||||
::notification_proxy_free_string(name_ptr);
|
||||
return Ok(std::move(name));
|
||||
}
|
||||
|
||||
} // namespace IdeviceFFI
|
||||
@@ -34,6 +34,7 @@ debug_proxy = ["idevice/debug_proxy"]
|
||||
diagnostics_relay = ["idevice/diagnostics_relay"]
|
||||
dvt = ["idevice/dvt"]
|
||||
heartbeat = ["idevice/heartbeat"]
|
||||
notification_proxy = ["idevice/notification_proxy"]
|
||||
house_arrest = ["idevice/house_arrest"]
|
||||
installation_proxy = ["idevice/installation_proxy"]
|
||||
springboardservices = ["idevice/springboardservices"]
|
||||
@@ -61,6 +62,7 @@ full = [
|
||||
"diagnostics_relay",
|
||||
"dvt",
|
||||
"heartbeat",
|
||||
"notification_proxy",
|
||||
"house_arrest",
|
||||
"installation_proxy",
|
||||
"misagent",
|
||||
|
||||
325
ffi/src/crashreportcopymobile.rs
Normal file
325
ffi/src/crashreportcopymobile.rs
Normal file
@@ -0,0 +1,325 @@
|
||||
// Jackson Coxson
|
||||
|
||||
use std::{
|
||||
ffi::{CStr, c_char},
|
||||
ptr::null_mut,
|
||||
};
|
||||
|
||||
use idevice::{
|
||||
IdeviceError, IdeviceService,
|
||||
provider::IdeviceProvider,
|
||||
services::crashreportcopymobile::{CrashReportCopyMobileClient, flush_reports},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
IdeviceFfiError, IdeviceHandle, afc::AfcClientHandle, ffi_err, provider::IdeviceProviderHandle,
|
||||
run_sync_local,
|
||||
};
|
||||
|
||||
pub struct CrashReportCopyMobileHandle(pub CrashReportCopyMobileClient);
|
||||
|
||||
/// Automatically creates and connects to the crash report copy mobile service,
|
||||
/// returning a client handle
|
||||
///
|
||||
/// # Arguments
|
||||
/// * [`provider`] - An IdeviceProvider
|
||||
/// * [`client`] - On success, will be set to point to a newly allocated handle
|
||||
///
|
||||
/// # Returns
|
||||
/// An IdeviceFfiError on error, null on success
|
||||
///
|
||||
/// # Safety
|
||||
/// `provider` must be a valid pointer to a handle allocated by this library
|
||||
/// `client` must be a valid, non-null pointer to a location where the handle will be stored
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn crash_report_client_connect(
|
||||
provider: *mut IdeviceProviderHandle,
|
||||
client: *mut *mut CrashReportCopyMobileHandle,
|
||||
) -> *mut IdeviceFfiError {
|
||||
if provider.is_null() || client.is_null() {
|
||||
tracing::error!("Null pointer provided");
|
||||
return ffi_err!(IdeviceError::FfiInvalidArg);
|
||||
}
|
||||
|
||||
let res: Result<CrashReportCopyMobileClient, IdeviceError> = run_sync_local(async move {
|
||||
let provider_ref: &dyn IdeviceProvider = unsafe { &*(*provider).0 };
|
||||
CrashReportCopyMobileClient::connect(provider_ref).await
|
||||
});
|
||||
|
||||
match res {
|
||||
Ok(r) => {
|
||||
let boxed = Box::new(CrashReportCopyMobileHandle(r));
|
||||
unsafe { *client = Box::into_raw(boxed) };
|
||||
null_mut()
|
||||
}
|
||||
Err(e) => ffi_err!(e),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new CrashReportCopyMobile client from an existing Idevice connection
|
||||
///
|
||||
/// # Arguments
|
||||
/// * [`socket`] - An IdeviceSocket handle
|
||||
/// * [`client`] - On success, will be set to point to a newly allocated handle
|
||||
///
|
||||
/// # Returns
|
||||
/// An IdeviceFfiError on error, null on success
|
||||
///
|
||||
/// # Safety
|
||||
/// `socket` must be a valid pointer to a handle allocated by this library
|
||||
/// `client` must be a valid, non-null pointer to a location where the handle will be stored
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn crash_report_client_new(
|
||||
socket: *mut IdeviceHandle,
|
||||
client: *mut *mut CrashReportCopyMobileHandle,
|
||||
) -> *mut IdeviceFfiError {
|
||||
if socket.is_null() || client.is_null() {
|
||||
return ffi_err!(IdeviceError::FfiInvalidArg);
|
||||
}
|
||||
let socket = unsafe { Box::from_raw(socket) }.0;
|
||||
let r = CrashReportCopyMobileClient::new(socket);
|
||||
let boxed = Box::new(CrashReportCopyMobileHandle(r));
|
||||
unsafe { *client = Box::into_raw(boxed) };
|
||||
null_mut()
|
||||
}
|
||||
|
||||
/// Lists crash report files in the specified directory
|
||||
///
|
||||
/// # Arguments
|
||||
/// * [`client`] - A valid CrashReportCopyMobile handle
|
||||
/// * [`dir_path`] - Optional directory path (NULL for root "/")
|
||||
/// * [`entries`] - Will be set to point to an array of C strings
|
||||
/// * [`count`] - Will be set to the number of entries
|
||||
///
|
||||
/// # Returns
|
||||
/// An IdeviceFfiError on error, null on success
|
||||
///
|
||||
/// # Safety
|
||||
/// All pointers must be valid and non-null
|
||||
/// `dir_path` may be NULL (defaults to root)
|
||||
/// Caller must free the returned array with `afc_free_directory_entries`
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn crash_report_client_ls(
|
||||
client: *mut CrashReportCopyMobileHandle,
|
||||
dir_path: *const c_char,
|
||||
entries: *mut *mut *mut c_char,
|
||||
count: *mut libc::size_t,
|
||||
) -> *mut IdeviceFfiError {
|
||||
if client.is_null() || entries.is_null() || count.is_null() {
|
||||
return ffi_err!(IdeviceError::FfiInvalidArg);
|
||||
}
|
||||
|
||||
let path = if dir_path.is_null() {
|
||||
None
|
||||
} else {
|
||||
match unsafe { CStr::from_ptr(dir_path) }.to_str() {
|
||||
Ok(s) => Some(s),
|
||||
Err(_) => return ffi_err!(IdeviceError::FfiInvalidString),
|
||||
}
|
||||
};
|
||||
|
||||
let res: Result<Vec<String>, IdeviceError> = run_sync_local(async {
|
||||
let client_ref = unsafe { &mut (*client).0 };
|
||||
client_ref.ls(path).await
|
||||
});
|
||||
|
||||
match res {
|
||||
Ok(items) => {
|
||||
let c_strings = items
|
||||
.into_iter()
|
||||
.filter_map(|s| std::ffi::CString::new(s).ok())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let string_count = c_strings.len();
|
||||
|
||||
// Allocate array for char pointers (with NULL terminator)
|
||||
let layout = std::alloc::Layout::array::<*mut c_char>(string_count + 1).unwrap();
|
||||
let ptr = unsafe { std::alloc::alloc(layout) as *mut *mut c_char };
|
||||
if ptr.is_null() {
|
||||
return ffi_err!(IdeviceError::FfiInvalidArg);
|
||||
}
|
||||
|
||||
for (i, cstring) in c_strings.into_iter().enumerate() {
|
||||
let string_ptr = cstring.into_raw();
|
||||
unsafe { *ptr.add(i) = string_ptr };
|
||||
}
|
||||
|
||||
// NULL terminator
|
||||
unsafe { *ptr.add(string_count) = std::ptr::null_mut() };
|
||||
|
||||
unsafe {
|
||||
*entries = ptr;
|
||||
*count = string_count;
|
||||
}
|
||||
|
||||
null_mut()
|
||||
}
|
||||
Err(e) => ffi_err!(e),
|
||||
}
|
||||
}
|
||||
|
||||
/// Downloads a crash report file from the device
|
||||
///
|
||||
/// # Arguments
|
||||
/// * [`client`] - A valid CrashReportCopyMobile handle
|
||||
/// * [`log_name`] - Name of the log file to download (C string)
|
||||
/// * [`data`] - Will be set to point to the file contents
|
||||
/// * [`length`] - Will be set to the size of the data
|
||||
///
|
||||
/// # Returns
|
||||
/// An IdeviceFfiError on error, null on success
|
||||
///
|
||||
/// # Safety
|
||||
/// All pointers must be valid and non-null
|
||||
/// `log_name` must be a valid C string
|
||||
/// Caller must free the returned data with `idevice_data_free`
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn crash_report_client_pull(
|
||||
client: *mut CrashReportCopyMobileHandle,
|
||||
log_name: *const c_char,
|
||||
data: *mut *mut u8,
|
||||
length: *mut libc::size_t,
|
||||
) -> *mut IdeviceFfiError {
|
||||
if client.is_null() || log_name.is_null() || data.is_null() || length.is_null() {
|
||||
return ffi_err!(IdeviceError::FfiInvalidArg);
|
||||
}
|
||||
|
||||
let name = match unsafe { CStr::from_ptr(log_name) }.to_str() {
|
||||
Ok(s) => s.to_string(),
|
||||
Err(_) => return ffi_err!(IdeviceError::FfiInvalidString),
|
||||
};
|
||||
|
||||
let res: Result<Vec<u8>, IdeviceError> = run_sync_local(async {
|
||||
let client_ref = unsafe { &mut (*client).0 };
|
||||
client_ref.pull(name).await
|
||||
});
|
||||
|
||||
match res {
|
||||
Ok(file_data) => {
|
||||
let len = file_data.len();
|
||||
let mut boxed = file_data.into_boxed_slice();
|
||||
unsafe {
|
||||
*data = boxed.as_mut_ptr();
|
||||
*length = len;
|
||||
}
|
||||
std::mem::forget(boxed);
|
||||
null_mut()
|
||||
}
|
||||
Err(e) => ffi_err!(e),
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes a crash report file from the device
|
||||
///
|
||||
/// # Arguments
|
||||
/// * [`client`] - A valid CrashReportCopyMobile handle
|
||||
/// * [`log_name`] - Name of the log file to remove (C string)
|
||||
///
|
||||
/// # Returns
|
||||
/// An IdeviceFfiError on error, null on success
|
||||
///
|
||||
/// # Safety
|
||||
/// `client` must be a valid pointer to a handle allocated by this library
|
||||
/// `log_name` must be a valid C string
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn crash_report_client_remove(
|
||||
client: *mut CrashReportCopyMobileHandle,
|
||||
log_name: *const c_char,
|
||||
) -> *mut IdeviceFfiError {
|
||||
if client.is_null() || log_name.is_null() {
|
||||
return ffi_err!(IdeviceError::FfiInvalidArg);
|
||||
}
|
||||
|
||||
let name = match unsafe { CStr::from_ptr(log_name) }.to_str() {
|
||||
Ok(s) => s.to_string(),
|
||||
Err(_) => return ffi_err!(IdeviceError::FfiInvalidString),
|
||||
};
|
||||
|
||||
let res = run_sync_local(async {
|
||||
let client_ref = unsafe { &mut (*client).0 };
|
||||
client_ref.remove(name).await
|
||||
});
|
||||
|
||||
match res {
|
||||
Ok(_) => null_mut(),
|
||||
Err(e) => ffi_err!(e),
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts this client to an AFC client for advanced file operations
|
||||
///
|
||||
/// # Arguments
|
||||
/// * [`client`] - A valid CrashReportCopyMobile handle (will be consumed)
|
||||
/// * [`afc_client`] - On success, will be set to an AFC client handle
|
||||
///
|
||||
/// # Returns
|
||||
/// An IdeviceFfiError on error, null on success
|
||||
///
|
||||
/// # Safety
|
||||
/// `client` must be a valid pointer (will be freed after this call)
|
||||
/// `afc_client` must be a valid, non-null pointer where the new AFC client will be stored
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn crash_report_client_to_afc(
|
||||
client: *mut CrashReportCopyMobileHandle,
|
||||
afc_client: *mut *mut AfcClientHandle,
|
||||
) -> *mut IdeviceFfiError {
|
||||
if client.is_null() || afc_client.is_null() {
|
||||
return ffi_err!(IdeviceError::FfiInvalidArg);
|
||||
}
|
||||
|
||||
let crash_client = unsafe { Box::from_raw(client) }.0;
|
||||
let afc = crash_client.to_afc_client();
|
||||
|
||||
let a = Box::into_raw(Box::new(AfcClientHandle(afc)));
|
||||
unsafe { *afc_client = a };
|
||||
|
||||
null_mut()
|
||||
}
|
||||
|
||||
/// Triggers a flush of crash logs from system storage
|
||||
///
|
||||
/// This connects to the crashreportmover service to move crash logs
|
||||
/// into the AFC-accessible directory. Should be called before listing logs.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * [`provider`] - An IdeviceProvider
|
||||
///
|
||||
/// # Returns
|
||||
/// An IdeviceFfiError on error, null on success
|
||||
///
|
||||
/// # Safety
|
||||
/// `provider` must be a valid pointer to a handle allocated by this library
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn crash_report_flush(
|
||||
provider: *mut IdeviceProviderHandle,
|
||||
) -> *mut IdeviceFfiError {
|
||||
if provider.is_null() {
|
||||
return ffi_err!(IdeviceError::FfiInvalidArg);
|
||||
}
|
||||
|
||||
let res = run_sync_local(async {
|
||||
let provider_ref: &dyn IdeviceProvider = unsafe { &*(*provider).0 };
|
||||
flush_reports(provider_ref).await
|
||||
});
|
||||
|
||||
match res {
|
||||
Ok(_) => null_mut(),
|
||||
Err(e) => ffi_err!(e),
|
||||
}
|
||||
}
|
||||
|
||||
/// Frees a CrashReportCopyMobile client handle
|
||||
///
|
||||
/// # Arguments
|
||||
/// * [`handle`] - The handle to free
|
||||
///
|
||||
/// # Safety
|
||||
/// `handle` must be a valid pointer to the handle that was allocated by this library,
|
||||
/// or NULL (in which case this function does nothing)
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn crash_report_client_free(handle: *mut CrashReportCopyMobileHandle) {
|
||||
if !handle.is_null() {
|
||||
tracing::debug!("Freeing crash_report_client");
|
||||
let _ = unsafe { Box::from_raw(handle) };
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,8 @@ pub mod amfi;
|
||||
pub mod core_device;
|
||||
#[cfg(feature = "core_device_proxy")]
|
||||
pub mod core_device_proxy;
|
||||
#[cfg(feature = "crashreportcopymobile")]
|
||||
pub mod crashreportcopymobile;
|
||||
#[cfg(feature = "debug_proxy")]
|
||||
pub mod debug_proxy;
|
||||
#[cfg(feature = "diagnostics_relay")]
|
||||
@@ -29,6 +31,8 @@ pub mod logging;
|
||||
pub mod misagent;
|
||||
#[cfg(feature = "mobile_image_mounter")]
|
||||
pub mod mobile_image_mounter;
|
||||
#[cfg(feature = "notification_proxy")]
|
||||
pub mod notification_proxy;
|
||||
#[cfg(feature = "syslog_relay")]
|
||||
pub mod os_trace_relay;
|
||||
mod pairing_file;
|
||||
|
||||
@@ -157,6 +157,74 @@ pub unsafe extern "C" fn lockdownd_start_service(
|
||||
}
|
||||
}
|
||||
|
||||
/// Pairs with the device using lockdownd
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `client` - A valid LockdowndClient handle
|
||||
/// * `host_id` - The host ID (null-terminated string)
|
||||
/// * `system_buid` - The system BUID (null-terminated string)
|
||||
/// * `pairing_file` - On success, will be set to point to a newly allocated IdevicePairingFile handle
|
||||
///
|
||||
/// # Returns
|
||||
/// An IdeviceFfiError on error, null on success
|
||||
///
|
||||
/// # Safety
|
||||
/// `client` must be a valid pointer to a handle allocated by this library
|
||||
/// `host_id` must be a valid null-terminated string
|
||||
/// `system_buid` must be a valid null-terminated string
|
||||
/// `pairing_file` must be a valid, non-null pointer to a location where the handle will be stored
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn lockdownd_pair(
|
||||
client: *mut LockdowndClientHandle,
|
||||
host_id: *const libc::c_char,
|
||||
system_buid: *const libc::c_char,
|
||||
host_name: *const libc::c_char,
|
||||
pairing_file: *mut *mut IdevicePairingFile,
|
||||
) -> *mut IdeviceFfiError {
|
||||
if client.is_null() || host_id.is_null() || system_buid.is_null() {
|
||||
return ffi_err!(IdeviceError::FfiInvalidArg);
|
||||
}
|
||||
|
||||
let host_id = unsafe {
|
||||
std::ffi::CStr::from_ptr(host_id)
|
||||
.to_string_lossy()
|
||||
.into_owned()
|
||||
};
|
||||
let system_buid = unsafe {
|
||||
std::ffi::CStr::from_ptr(system_buid)
|
||||
.to_string_lossy()
|
||||
.into_owned()
|
||||
};
|
||||
|
||||
let host_name = if host_name.is_null() {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
match unsafe { std::ffi::CStr::from_ptr(host_name) }.to_str() {
|
||||
Ok(v) => v,
|
||||
Err(_) => {
|
||||
return ffi_err!(IdeviceError::InvalidCString);
|
||||
}
|
||||
},
|
||||
)
|
||||
};
|
||||
|
||||
let res = run_sync_local(async move {
|
||||
let client_ref = unsafe { &mut (*client).0 };
|
||||
|
||||
client_ref.pair(host_id, system_buid, host_name).await
|
||||
});
|
||||
|
||||
match res {
|
||||
Ok(pairing_file_res) => {
|
||||
let boxed_pairing_file = Box::new(IdevicePairingFile(pairing_file_res));
|
||||
unsafe { *pairing_file = Box::into_raw(boxed_pairing_file) };
|
||||
null_mut()
|
||||
}
|
||||
Err(e) => ffi_err!(e),
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets a value from lockdownd
|
||||
///
|
||||
/// # Arguments
|
||||
|
||||
311
ffi/src/notification_proxy.rs
Normal file
311
ffi/src/notification_proxy.rs
Normal file
@@ -0,0 +1,311 @@
|
||||
// Jackson Coxson
|
||||
|
||||
use std::ffi::{CStr, CString, c_char};
|
||||
use std::ptr::null_mut;
|
||||
|
||||
use idevice::{
|
||||
IdeviceError, IdeviceService, notification_proxy::NotificationProxyClient,
|
||||
provider::IdeviceProvider,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
IdeviceFfiError, IdeviceHandle, ffi_err, provider::IdeviceProviderHandle, run_sync_local,
|
||||
};
|
||||
|
||||
pub struct NotificationProxyClientHandle(pub NotificationProxyClient);
|
||||
|
||||
/// Automatically creates and connects to Notification Proxy, returning a client handle
|
||||
///
|
||||
/// # Arguments
|
||||
/// * [`provider`] - An IdeviceProvider
|
||||
/// * [`client`] - On success, will be set to point to a newly allocated NotificationProxyClient handle
|
||||
///
|
||||
/// # Returns
|
||||
/// An IdeviceFfiError on error, null on success
|
||||
///
|
||||
/// # Safety
|
||||
/// `provider` must be a valid pointer to a handle allocated by this library
|
||||
/// `client` must be a valid, non-null pointer to a location where the handle will be stored
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn notification_proxy_connect(
|
||||
provider: *mut IdeviceProviderHandle,
|
||||
client: *mut *mut NotificationProxyClientHandle,
|
||||
) -> *mut IdeviceFfiError {
|
||||
if provider.is_null() || client.is_null() {
|
||||
tracing::error!("Null pointer provided");
|
||||
return ffi_err!(IdeviceError::FfiInvalidArg);
|
||||
}
|
||||
|
||||
let res: Result<NotificationProxyClient, IdeviceError> = run_sync_local(async move {
|
||||
let provider_ref: &dyn IdeviceProvider = unsafe { &*(*provider).0 };
|
||||
NotificationProxyClient::connect(provider_ref).await
|
||||
});
|
||||
|
||||
match res {
|
||||
Ok(r) => {
|
||||
let boxed = Box::new(NotificationProxyClientHandle(r));
|
||||
unsafe { *client = Box::into_raw(boxed) };
|
||||
null_mut()
|
||||
}
|
||||
Err(e) => {
|
||||
ffi_err!(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new NotificationProxyClient from an existing Idevice connection
|
||||
///
|
||||
/// # Arguments
|
||||
/// * [`socket`] - An IdeviceSocket handle
|
||||
/// * [`client`] - On success, will be set to point to a newly allocated NotificationProxyClient handle
|
||||
///
|
||||
/// # Returns
|
||||
/// An IdeviceFfiError on error, null on success
|
||||
///
|
||||
/// # Safety
|
||||
/// `socket` must be a valid pointer to a handle allocated by this library. The socket is consumed,
|
||||
/// and may not be used again.
|
||||
/// `client` must be a valid, non-null pointer to a location where the handle will be stored
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn notification_proxy_new(
|
||||
socket: *mut IdeviceHandle,
|
||||
client: *mut *mut NotificationProxyClientHandle,
|
||||
) -> *mut IdeviceFfiError {
|
||||
if socket.is_null() || client.is_null() {
|
||||
return ffi_err!(IdeviceError::FfiInvalidArg);
|
||||
}
|
||||
let socket = unsafe { Box::from_raw(socket) }.0;
|
||||
let r = NotificationProxyClient::new(socket);
|
||||
let boxed = Box::new(NotificationProxyClientHandle(r));
|
||||
unsafe { *client = Box::into_raw(boxed) };
|
||||
null_mut()
|
||||
}
|
||||
|
||||
/// Posts a notification to the device
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `client` - A valid NotificationProxyClient handle
|
||||
/// * `name` - C string containing the notification name
|
||||
///
|
||||
/// # Returns
|
||||
/// An IdeviceFfiError on error, null on success
|
||||
///
|
||||
/// # Safety
|
||||
/// `client` must be a valid pointer to a handle allocated by this library
|
||||
/// `name` must be a valid null-terminated C string
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn notification_proxy_post(
|
||||
client: *mut NotificationProxyClientHandle,
|
||||
name: *const c_char,
|
||||
) -> *mut IdeviceFfiError {
|
||||
if client.is_null() || name.is_null() {
|
||||
return ffi_err!(IdeviceError::FfiInvalidArg);
|
||||
}
|
||||
|
||||
let name_str = match unsafe { CStr::from_ptr(name) }.to_str() {
|
||||
Ok(s) => s.to_string(),
|
||||
Err(_) => return ffi_err!(IdeviceError::FfiInvalidString),
|
||||
};
|
||||
|
||||
let res: Result<(), IdeviceError> = run_sync_local(async move {
|
||||
let client_ref = unsafe { &mut (*client).0 };
|
||||
client_ref.post_notification(name_str).await
|
||||
});
|
||||
|
||||
match res {
|
||||
Ok(_) => null_mut(),
|
||||
Err(e) => ffi_err!(e),
|
||||
}
|
||||
}
|
||||
|
||||
/// Observes a specific notification
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `client` - A valid NotificationProxyClient handle
|
||||
/// * `name` - C string containing the notification name to observe
|
||||
///
|
||||
/// # Returns
|
||||
/// An IdeviceFfiError on error, null on success
|
||||
///
|
||||
/// # Safety
|
||||
/// `client` must be a valid pointer to a handle allocated by this library
|
||||
/// `name` must be a valid null-terminated C string
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn notification_proxy_observe(
|
||||
client: *mut NotificationProxyClientHandle,
|
||||
name: *const c_char,
|
||||
) -> *mut IdeviceFfiError {
|
||||
if client.is_null() || name.is_null() {
|
||||
return ffi_err!(IdeviceError::FfiInvalidArg);
|
||||
}
|
||||
|
||||
let name_str = match unsafe { CStr::from_ptr(name) }.to_str() {
|
||||
Ok(s) => s.to_string(),
|
||||
Err(_) => return ffi_err!(IdeviceError::FfiInvalidString),
|
||||
};
|
||||
|
||||
let res: Result<(), IdeviceError> = run_sync_local(async move {
|
||||
let client_ref = unsafe { &mut (*client).0 };
|
||||
client_ref.observe_notification(name_str).await
|
||||
});
|
||||
|
||||
match res {
|
||||
Ok(_) => null_mut(),
|
||||
Err(e) => ffi_err!(e),
|
||||
}
|
||||
}
|
||||
|
||||
/// Observes multiple notifications at once
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `client` - A valid NotificationProxyClient handle
|
||||
/// * `names` - A null-terminated array of C strings containing notification names
|
||||
///
|
||||
/// # Returns
|
||||
/// An IdeviceFfiError on error, null on success
|
||||
///
|
||||
/// # Safety
|
||||
/// `client` must be a valid pointer to a handle allocated by this library
|
||||
/// `names` must be a valid pointer to a null-terminated array of null-terminated C strings
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn notification_proxy_observe_multiple(
|
||||
client: *mut NotificationProxyClientHandle,
|
||||
names: *const *const c_char,
|
||||
) -> *mut IdeviceFfiError {
|
||||
if client.is_null() || names.is_null() {
|
||||
return ffi_err!(IdeviceError::FfiInvalidArg);
|
||||
}
|
||||
|
||||
let mut notification_names: Vec<String> = Vec::new();
|
||||
let mut i = 0;
|
||||
loop {
|
||||
let ptr = unsafe { *names.add(i) };
|
||||
if ptr.is_null() {
|
||||
break;
|
||||
}
|
||||
match unsafe { CStr::from_ptr(ptr) }.to_str() {
|
||||
Ok(s) => notification_names.push(s.to_string()),
|
||||
Err(_) => return ffi_err!(IdeviceError::FfiInvalidString),
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
|
||||
let refs: Vec<&str> = notification_names.iter().map(|s| s.as_str()).collect();
|
||||
|
||||
let res: Result<(), IdeviceError> = run_sync_local(async move {
|
||||
let client_ref = unsafe { &mut (*client).0 };
|
||||
client_ref.observe_notifications(&refs).await
|
||||
});
|
||||
|
||||
match res {
|
||||
Ok(_) => null_mut(),
|
||||
Err(e) => ffi_err!(e),
|
||||
}
|
||||
}
|
||||
|
||||
/// Receives the next notification from the device
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `client` - A valid NotificationProxyClient handle
|
||||
/// * `name_out` - On success, will be set to a newly allocated C string containing the notification name
|
||||
///
|
||||
/// # Returns
|
||||
/// An IdeviceFfiError on error, null on success
|
||||
///
|
||||
/// # Safety
|
||||
/// `client` must be a valid pointer to a handle allocated by this library
|
||||
/// `name_out` must be a valid pointer. The returned string must be freed with `notification_proxy_free_string`
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn notification_proxy_receive(
|
||||
client: *mut NotificationProxyClientHandle,
|
||||
name_out: *mut *mut c_char,
|
||||
) -> *mut IdeviceFfiError {
|
||||
if client.is_null() || name_out.is_null() {
|
||||
return ffi_err!(IdeviceError::FfiInvalidArg);
|
||||
}
|
||||
|
||||
let res: Result<String, IdeviceError> = run_sync_local(async move {
|
||||
let client_ref = unsafe { &mut (*client).0 };
|
||||
client_ref.receive_notification().await
|
||||
});
|
||||
|
||||
match res {
|
||||
Ok(name) => match CString::new(name) {
|
||||
Ok(c_string) => {
|
||||
unsafe { *name_out = c_string.into_raw() };
|
||||
null_mut()
|
||||
}
|
||||
Err(_) => ffi_err!(IdeviceError::FfiInvalidString),
|
||||
},
|
||||
Err(e) => ffi_err!(e),
|
||||
}
|
||||
}
|
||||
|
||||
/// Receives the next notification with a timeout
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `client` - A valid NotificationProxyClient handle
|
||||
/// * `interval` - Timeout in seconds to wait for a notification
|
||||
/// * `name_out` - On success, will be set to a newly allocated C string containing the notification name
|
||||
///
|
||||
/// # Returns
|
||||
/// An IdeviceFfiError on error, null on success
|
||||
///
|
||||
/// # Safety
|
||||
/// `client` must be a valid pointer to a handle allocated by this library
|
||||
/// `name_out` must be a valid pointer. The returned string must be freed with `notification_proxy_free_string`
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn notification_proxy_receive_with_timeout(
|
||||
client: *mut NotificationProxyClientHandle,
|
||||
interval: u64,
|
||||
name_out: *mut *mut c_char,
|
||||
) -> *mut IdeviceFfiError {
|
||||
if client.is_null() || name_out.is_null() {
|
||||
return ffi_err!(IdeviceError::FfiInvalidArg);
|
||||
}
|
||||
|
||||
let res: Result<String, IdeviceError> = run_sync_local(async move {
|
||||
let client_ref = unsafe { &mut (*client).0 };
|
||||
client_ref.receive_notification_with_timeout(interval).await
|
||||
});
|
||||
|
||||
match res {
|
||||
Ok(name) => match CString::new(name) {
|
||||
Ok(c_string) => {
|
||||
unsafe { *name_out = c_string.into_raw() };
|
||||
null_mut()
|
||||
}
|
||||
Err(_) => ffi_err!(IdeviceError::FfiInvalidString),
|
||||
},
|
||||
Err(e) => ffi_err!(e),
|
||||
}
|
||||
}
|
||||
|
||||
/// Frees a string returned by notification_proxy_receive
|
||||
///
|
||||
/// # Safety
|
||||
/// `s` must be a valid pointer returned from `notification_proxy_receive`
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn notification_proxy_free_string(s: *mut c_char) {
|
||||
if !s.is_null() {
|
||||
let _ = unsafe { CString::from_raw(s) };
|
||||
}
|
||||
}
|
||||
|
||||
/// Frees a handle
|
||||
///
|
||||
/// # Arguments
|
||||
/// * [`handle`] - The handle to free
|
||||
///
|
||||
/// # Safety
|
||||
/// `handle` must be a valid pointer to the handle that was allocated by this library,
|
||||
/// or NULL (in which case this function does nothing)
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn notification_proxy_client_free(
|
||||
handle: *mut NotificationProxyClientHandle,
|
||||
) {
|
||||
if !handle.is_null() {
|
||||
tracing::debug!("Freeing notification_proxy_client");
|
||||
let _ = unsafe { Box::from_raw(handle) };
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ use idevice::{
|
||||
IdeviceError, IdeviceService, provider::IdeviceProvider,
|
||||
springboardservices::SpringBoardServicesClient,
|
||||
};
|
||||
use plist_ffi::plist_t;
|
||||
|
||||
use crate::{
|
||||
IdeviceFfiError, IdeviceHandle, ffi_err, provider::IdeviceProviderHandle, run_sync,
|
||||
@@ -263,6 +264,43 @@ pub unsafe extern "C" fn springboard_services_get_interface_orientation(
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the home screen icon layout metrics
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `client` - A valid SpringBoardServicesClient handle
|
||||
/// * `res` - On success, will point to a plist dictionary node containing the metrics
|
||||
///
|
||||
/// # Returns
|
||||
/// An IdeviceFfiError on error, null on success
|
||||
///
|
||||
/// # Safety
|
||||
/// `client` must be a valid pointer to a handle allocated by this library
|
||||
/// `res` must be a valid, non-null pointer
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn springboard_services_get_homescreen_icon_metrics(
|
||||
client: *mut SpringBoardServicesClientHandle,
|
||||
res: *mut plist_t,
|
||||
) -> *mut IdeviceFfiError {
|
||||
if client.is_null() || res.is_null() {
|
||||
tracing::error!("Invalid arguments: {client:?}, {res:?}");
|
||||
return ffi_err!(IdeviceError::FfiInvalidArg);
|
||||
}
|
||||
let client = unsafe { &mut *client };
|
||||
|
||||
let output = run_sync(async { client.0.get_homescreen_icon_metrics().await });
|
||||
|
||||
match output {
|
||||
Ok(metrics) => {
|
||||
unsafe {
|
||||
*res =
|
||||
plist_ffi::PlistWrapper::new_node(plist::Value::Dictionary(metrics)).into_ptr();
|
||||
}
|
||||
null_mut()
|
||||
}
|
||||
Err(e) => ffi_err!(e),
|
||||
}
|
||||
}
|
||||
|
||||
/// Frees an SpringBoardServicesClient handle
|
||||
///
|
||||
/// # Arguments
|
||||
|
||||
@@ -97,6 +97,7 @@ misagent = []
|
||||
mobile_image_mounter = ["dep:sha2"]
|
||||
mobileactivationd = ["dep:reqwest"]
|
||||
mobilebackup2 = []
|
||||
notification_proxy = ["tokio/macros", "tokio/time", "dep:async-stream", "dep:futures"]
|
||||
location_simulation = []
|
||||
pair = ["chrono/default", "tokio/time", "dep:sha2", "dep:rsa", "dep:x509-cert"]
|
||||
pcapd = []
|
||||
@@ -138,6 +139,7 @@ full = [
|
||||
"mobile_image_mounter",
|
||||
"mobileactivationd",
|
||||
"mobilebackup2",
|
||||
"notification_proxy",
|
||||
"pair",
|
||||
"pcapd",
|
||||
"preboard_service",
|
||||
|
||||
@@ -865,6 +865,14 @@ pub enum IdeviceError {
|
||||
|
||||
#[error("Developer mode is not enabled")]
|
||||
DeveloperModeNotEnabled = -68,
|
||||
|
||||
#[cfg(feature = "notification_proxy")]
|
||||
#[error("notification proxy died")]
|
||||
NotificationProxyDeath = -69,
|
||||
|
||||
#[cfg(feature = "installation_proxy")]
|
||||
#[error("Application verification failed: {0}")]
|
||||
ApplicationVerificationFailed(String) = -70,
|
||||
}
|
||||
|
||||
impl IdeviceError {
|
||||
@@ -908,6 +916,15 @@ impl IdeviceError {
|
||||
Some(Self::InternalError(detailed_error))
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "installation_proxy")]
|
||||
"ApplicationVerificationFailed" => {
|
||||
let msg = context
|
||||
.get("ErrorDescription")
|
||||
.and_then(|x| x.as_string())
|
||||
.unwrap_or("No context")
|
||||
.to_string();
|
||||
Some(Self::ApplicationVerificationFailed(msg))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -1030,6 +1047,12 @@ impl IdeviceError {
|
||||
#[cfg(feature = "installation_proxy")]
|
||||
IdeviceError::MalformedPackageArchive(_) => -67,
|
||||
IdeviceError::DeveloperModeNotEnabled => -68,
|
||||
|
||||
#[cfg(feature = "notification_proxy")]
|
||||
IdeviceError::NotificationProxyDeath => -69,
|
||||
|
||||
#[cfg(feature = "installation_proxy")]
|
||||
IdeviceError::ApplicationVerificationFailed(_) => -70,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,8 @@ pub mod mobile_image_mounter;
|
||||
pub mod mobileactivationd;
|
||||
#[cfg(feature = "mobilebackup2")]
|
||||
pub mod mobilebackup2;
|
||||
#[cfg(feature = "notification_proxy")]
|
||||
pub mod notification_proxy;
|
||||
#[cfg(feature = "syslog_relay")]
|
||||
pub mod os_trace_relay;
|
||||
#[cfg(feature = "pcapd")]
|
||||
|
||||
212
idevice/src/services/notification_proxy.rs
Normal file
212
idevice/src/services/notification_proxy.rs
Normal file
@@ -0,0 +1,212 @@
|
||||
//! iOS Device Notification Proxy Service
|
||||
//!
|
||||
//! Based on libimobiledevice's notification_proxy implementation
|
||||
//!
|
||||
//! Common notification identifiers:
|
||||
//! Full list: include/libimobiledevice/notification_proxy.h
|
||||
//!
|
||||
//! - Notifications that can be sent (PostNotification):
|
||||
//! - `com.apple.itunes-mobdev.syncWillStart` - Sync will start
|
||||
//! - `com.apple.itunes-mobdev.syncDidStart` - Sync started
|
||||
//! - `com.apple.itunes-mobdev.syncDidFinish` - Sync finished
|
||||
//! - `com.apple.itunes-mobdev.syncLockRequest` - Request sync lock
|
||||
//!
|
||||
//! - Notifications that can be observed (ObserveNotification):
|
||||
//! - `com.apple.itunes-client.syncCancelRequest` - Cancel sync request
|
||||
//! - `com.apple.itunes-client.syncSuspendRequest` - Suspend sync
|
||||
//! - `com.apple.itunes-client.syncResumeRequest` - Resume sync
|
||||
//! - `com.apple.mobile.lockdown.phone_number_changed` - Phone number changed
|
||||
//! - `com.apple.mobile.lockdown.device_name_changed` - Device name changed
|
||||
//! - `com.apple.mobile.lockdown.timezone_changed` - Timezone changed
|
||||
//! - `com.apple.mobile.lockdown.trusted_host_attached` - Trusted host attached
|
||||
//! - `com.apple.mobile.lockdown.host_detached` - Host detached
|
||||
//! - `com.apple.mobile.lockdown.host_attached` - Host attached
|
||||
//! - `com.apple.mobile.lockdown.registration_failed` - Registration failed
|
||||
//! - `com.apple.mobile.lockdown.activation_state` - Activation state
|
||||
//! - `com.apple.mobile.lockdown.brick_state` - Brick state
|
||||
//! - `com.apple.mobile.lockdown.disk_usage_changed` - Disk usage (iOS 4.0+)
|
||||
//! - `com.apple.mobile.data_sync.domain_changed` - Data sync domain changed
|
||||
//! - `com.apple.mobile.application_installed` - App installed
|
||||
//! - `com.apple.mobile.application_uninstalled` - App uninstalled
|
||||
|
||||
use std::pin::Pin;
|
||||
|
||||
use futures::Stream;
|
||||
use tracing::warn;
|
||||
|
||||
use crate::{Idevice, IdeviceError, IdeviceService, obf};
|
||||
|
||||
/// Client for interacting with the iOS notification proxy service
|
||||
///
|
||||
/// The notification proxy service provides a mechanism to observe and post
|
||||
/// system notifications.
|
||||
///
|
||||
/// Use `observe_notification` to register for events, then `receive_notification`
|
||||
/// to wait for them.
|
||||
#[derive(Debug)]
|
||||
pub struct NotificationProxyClient {
|
||||
/// The underlying device connection with established notification_proxy service
|
||||
pub idevice: Idevice,
|
||||
}
|
||||
|
||||
impl IdeviceService for NotificationProxyClient {
|
||||
/// Returns the notification proxy service name as registered with lockdownd
|
||||
fn service_name() -> std::borrow::Cow<'static, str> {
|
||||
obf!("com.apple.mobile.notification_proxy")
|
||||
}
|
||||
async fn from_stream(idevice: Idevice) -> Result<Self, crate::IdeviceError> {
|
||||
Ok(Self::new(idevice))
|
||||
}
|
||||
}
|
||||
|
||||
impl NotificationProxyClient {
|
||||
/// Creates a new notification proxy client from an existing device connection
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `idevice` - Pre-established device connection
|
||||
pub fn new(idevice: Idevice) -> Self {
|
||||
Self { idevice }
|
||||
}
|
||||
|
||||
/// Posts a notification to the device
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `notification_name` - Name of the notification to post
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `IdeviceError` if the notification fails to send
|
||||
pub async fn post_notification(
|
||||
&mut self,
|
||||
notification_name: impl Into<String>,
|
||||
) -> Result<(), IdeviceError> {
|
||||
let request = crate::plist!({
|
||||
"Command": "PostNotification",
|
||||
"Name": notification_name.into()
|
||||
});
|
||||
self.idevice.send_plist(request).await
|
||||
}
|
||||
|
||||
/// Registers to observe a specific notification
|
||||
///
|
||||
/// After calling this, use `receive_notification` to wait for events.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `notification_name` - Name of the notification to observe
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `IdeviceError` if the registration fails
|
||||
pub async fn observe_notification(
|
||||
&mut self,
|
||||
notification_name: impl Into<String>,
|
||||
) -> Result<(), IdeviceError> {
|
||||
let request = crate::plist!({
|
||||
"Command": "ObserveNotification",
|
||||
"Name": notification_name.into()
|
||||
});
|
||||
self.idevice.send_plist(request).await
|
||||
}
|
||||
|
||||
/// Registers to observe multiple notifications at once
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `notification_names` - Slice of notification names to observe
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `IdeviceError` if any registration fails
|
||||
pub async fn observe_notifications(
|
||||
&mut self,
|
||||
notification_names: &[&str],
|
||||
) -> Result<(), IdeviceError> {
|
||||
for name in notification_names {
|
||||
self.observe_notification(*name).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Waits for and receives the next notification from the device
|
||||
///
|
||||
/// # Returns
|
||||
/// The name of the received notification
|
||||
///
|
||||
/// # Errors
|
||||
/// - `NotificationProxyDeath` if the proxy connection died
|
||||
/// - `UnexpectedResponse` if the response format is invalid
|
||||
pub async fn receive_notification(&mut self) -> Result<String, IdeviceError> {
|
||||
let response = self.idevice.read_plist().await?;
|
||||
|
||||
match response.get("Command").and_then(|c| c.as_string()) {
|
||||
Some("RelayNotification") => match response.get("Name").and_then(|n| n.as_string()) {
|
||||
Some(name) => Ok(name.to_string()),
|
||||
None => Err(IdeviceError::UnexpectedResponse),
|
||||
},
|
||||
Some("ProxyDeath") => {
|
||||
warn!("NotificationProxy died!");
|
||||
Err(IdeviceError::NotificationProxyDeath)
|
||||
}
|
||||
_ => Err(IdeviceError::UnexpectedResponse),
|
||||
}
|
||||
}
|
||||
|
||||
/// Waits for a notification with a timeout
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `interval` - Timeout in seconds to wait for a notification
|
||||
///
|
||||
/// # Returns
|
||||
/// The name of the received notification
|
||||
///
|
||||
/// # Errors
|
||||
/// - `NotificationProxyDeath` if the proxy connection died
|
||||
/// - `UnexpectedResponse` if the response format is invalid
|
||||
/// - `HeartbeatTimeout` if no notification received before interval
|
||||
pub async fn receive_notification_with_timeout(
|
||||
&mut self,
|
||||
interval: u64,
|
||||
) -> Result<String, IdeviceError> {
|
||||
tokio::select! {
|
||||
result = self.receive_notification() => result,
|
||||
_ = tokio::time::sleep(tokio::time::Duration::from_secs(interval)) => {
|
||||
Err(IdeviceError::HeartbeatTimeout)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Continuous stream of notifications.
|
||||
pub fn into_stream(
|
||||
mut self,
|
||||
) -> Pin<Box<dyn Stream<Item = Result<String, IdeviceError>> + Send>> {
|
||||
Box::pin(async_stream::try_stream! {
|
||||
loop {
|
||||
let response = self.idevice.read_plist().await?;
|
||||
|
||||
match response.get("Command").and_then(|c| c.as_string()) {
|
||||
Some("RelayNotification") => {
|
||||
match response.get("Name").and_then(|n| n.as_string()) {
|
||||
Some(name) => yield name.to_string(),
|
||||
None => Err(IdeviceError::UnexpectedResponse)?,
|
||||
}
|
||||
}
|
||||
Some("ProxyDeath") => {
|
||||
warn!("NotificationProxy died!");
|
||||
Err(IdeviceError::NotificationProxyDeath)?;
|
||||
}
|
||||
_ => Err(IdeviceError::UnexpectedResponse)?,
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Shuts down the notification proxy connection
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `IdeviceError` if the shutdown command fails to send
|
||||
pub async fn shutdown(&mut self) -> Result<(), IdeviceError> {
|
||||
let request = crate::plist!({
|
||||
"Command": "Shutdown"
|
||||
});
|
||||
self.idevice.send_plist(request).await?;
|
||||
// Best-effort: wait for ProxyDeath ack
|
||||
let _ = self.idevice.read_plist().await;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -325,4 +325,31 @@ impl SpringBoardServicesClient {
|
||||
|
||||
Ok(orientation)
|
||||
}
|
||||
|
||||
/// Gets the home screen icon layout metrics
|
||||
///
|
||||
/// Returns icon spacing, size, and positioning information
|
||||
///
|
||||
/// # Returns
|
||||
/// A `plist::Dictionary` containing the icon layout metrics
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `IdeviceError` if:
|
||||
/// - Communication fails
|
||||
/// - The response is malformed
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// let metrics = client.get_homescreen_icon_metrics().await?;
|
||||
/// println!("{:?}", metrics);
|
||||
/// ```
|
||||
pub async fn get_homescreen_icon_metrics(&mut self) -> Result<plist::Dictionary, IdeviceError> {
|
||||
let req = crate::plist!({
|
||||
"command": "getHomeScreenIconMetrics",
|
||||
});
|
||||
self.idevice.send_plist(req).await?;
|
||||
|
||||
let res = self.idevice.read_plist().await?;
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
15
justfile
15
justfile
@@ -40,6 +40,9 @@ xcframework: apple-build
|
||||
lipo -create -output swift/libs/idevice-ios-sim.a \
|
||||
target/aarch64-apple-ios-sim/release/libidevice_ffi.a \
|
||||
target/x86_64-apple-ios/release/libidevice_ffi.a
|
||||
lipo -create -output swift/libs/idevice-maccatalyst.a \
|
||||
target/aarch64-apple-ios-macabi/release/libidevice_ffi.a \
|
||||
target/x86_64-apple-ios-macabi/release/libidevice_ffi.a
|
||||
lipo -create -output swift/libs/idevice-macos.a \
|
||||
target/aarch64-apple-darwin/release/libidevice_ffi.a \
|
||||
target/x86_64-apple-darwin/release/libidevice_ffi.a
|
||||
@@ -48,6 +51,7 @@ xcframework: apple-build
|
||||
-library target/aarch64-apple-ios/release/libidevice_ffi.a -headers swift/include \
|
||||
-library swift/libs/idevice-ios-sim.a -headers swift/include \
|
||||
-library swift/libs/idevice-macos.a -headers swift/include \
|
||||
-library swift/libs/idevice-maccatalyst.a -headers swift/include \
|
||||
-output swift/IDevice.xcframework
|
||||
|
||||
zip -r swift/bundle.zip swift/IDevice.xcframework
|
||||
@@ -68,7 +72,16 @@ apple-build: # requires a Mac
|
||||
BINDGEN_EXTRA_CLANG_ARGS="--sysroot=$(xcrun --sdk iphonesimulator --show-sdk-path)" \
|
||||
cargo build --release --target x86_64-apple-ios
|
||||
|
||||
# Mac Catalyst (arm64)
|
||||
# AWS-LC has an a hard time compiling for an iOS with macabi target, so we switch to ring.
|
||||
BINDGEN_EXTRA_CLANG_ARGS="--sysroot=$(xcrun --sdk macosx --show-sdk-path)" \
|
||||
cargo build --release --target aarch64-apple-ios-macabi --no-default-features --features "ring full"
|
||||
|
||||
# Mac Catalyst (x86_64)
|
||||
# AWS-LC has an a hard time compiling for an iOS with macabi target, so we switch to ring.
|
||||
BINDGEN_EXTRA_CLANG_ARGS="--sysroot=$(xcrun --sdk macosx --show-sdk-path)" \
|
||||
cargo build --release --target x86_64-apple-ios-macabi --no-default-features --features "ring full"
|
||||
|
||||
# macOS (native) – no special env needed
|
||||
cargo build --release --target aarch64-apple-darwin
|
||||
cargo build --release --target x86_64-apple-darwin
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ mod lockdown;
|
||||
mod misagent;
|
||||
mod mobilebackup2;
|
||||
mod mounter;
|
||||
mod notification_proxy_client;
|
||||
mod notifications;
|
||||
mod os_trace_relay;
|
||||
mod pair;
|
||||
@@ -113,6 +114,7 @@ async fn main() {
|
||||
.with_subcommand("mobilebackup2", mobilebackup2::register())
|
||||
.with_subcommand("mounter", mounter::register())
|
||||
.with_subcommand("notifications", notifications::register())
|
||||
.with_subcommand("notification_proxy", notification_proxy_client::register())
|
||||
.with_subcommand("os_trace_relay", os_trace_relay::register())
|
||||
.with_subcommand("pair", pair::register())
|
||||
.with_subcommand("pcapd", pcapd::register())
|
||||
@@ -214,6 +216,9 @@ async fn main() {
|
||||
"notifications" => {
|
||||
notifications::main(sub_args, provider).await;
|
||||
}
|
||||
"notification_proxy" => {
|
||||
notification_proxy_client::main(sub_args, provider).await;
|
||||
}
|
||||
"os_trace_relay" => {
|
||||
os_trace_relay::main(sub_args, provider).await;
|
||||
}
|
||||
|
||||
85
tools/src/notification_proxy_client.rs
Normal file
85
tools/src/notification_proxy_client.rs
Normal file
@@ -0,0 +1,85 @@
|
||||
// Jackson Coxson
|
||||
|
||||
use idevice::{
|
||||
IdeviceService, notification_proxy::NotificationProxyClient, provider::IdeviceProvider,
|
||||
};
|
||||
use jkcli::{CollectedArguments, JkArgument, JkCommand};
|
||||
|
||||
pub fn register() -> JkCommand {
|
||||
JkCommand::new()
|
||||
.help("Notification proxy")
|
||||
.with_subcommand(
|
||||
"observe",
|
||||
JkCommand::new()
|
||||
.help("Observe notifications from the device")
|
||||
.with_argument(
|
||||
JkArgument::new()
|
||||
.with_help("The notification ID to observe")
|
||||
.required(true),
|
||||
),
|
||||
)
|
||||
.with_subcommand(
|
||||
"post",
|
||||
JkCommand::new()
|
||||
.help("Post a notification to the device")
|
||||
.with_argument(
|
||||
JkArgument::new()
|
||||
.with_help("The notification ID to post")
|
||||
.required(true),
|
||||
),
|
||||
)
|
||||
.subcommand_required(true)
|
||||
}
|
||||
|
||||
pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
|
||||
let mut client = NotificationProxyClient::connect(&*provider)
|
||||
.await
|
||||
.expect("Unable to connect to notification proxy");
|
||||
|
||||
let (subcommand, sub_args) = arguments.first_subcommand().unwrap();
|
||||
let mut sub_args = sub_args.clone();
|
||||
|
||||
match subcommand.as_str() {
|
||||
"observe" => {
|
||||
let input: String = sub_args
|
||||
.next_argument::<String>()
|
||||
.expect("No notification ID passed");
|
||||
|
||||
let notifications: Vec<&str> = input.split_whitespace().collect();
|
||||
client
|
||||
.observe_notifications(¬ifications)
|
||||
.await
|
||||
.expect("Failed to observe notifications");
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = tokio::signal::ctrl_c() => {
|
||||
println!("\nShutdown signal received, exiting.");
|
||||
break;
|
||||
}
|
||||
|
||||
result = client.receive_notification() => {
|
||||
match result {
|
||||
Ok(notif) => println!("Received notification: {}", notif),
|
||||
Err(e) => {
|
||||
eprintln!("Failed to receive notification: {}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"post" => {
|
||||
let notification: String = sub_args
|
||||
.next_argument::<String>()
|
||||
.expect("No notification ID passed");
|
||||
|
||||
client
|
||||
.post_notification(¬ification)
|
||||
.await
|
||||
.expect("Failed to post notification");
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
@@ -49,6 +49,25 @@ pub fn register() -> JkCommand {
|
||||
"get_interface_orientation",
|
||||
JkCommand::new().help("Gets the device's current screen orientation"),
|
||||
)
|
||||
.with_subcommand(
|
||||
"get_homescreen_icon_metrics",
|
||||
JkCommand::new().help("Gets home screen icon layout metrics"),
|
||||
)
|
||||
.with_subcommand(
|
||||
"get_icon",
|
||||
JkCommand::new()
|
||||
.help("Gets an app's icon as PNG")
|
||||
.with_argument(
|
||||
JkArgument::new()
|
||||
.with_help("Bundle identifier (e.g. com.apple.Maps)")
|
||||
.required(true),
|
||||
)
|
||||
.with_flag(
|
||||
JkFlag::new("save")
|
||||
.with_help("Path to save the icon PNG file, or icon.png by default")
|
||||
.with_argument(JkArgument::new().required(true)),
|
||||
),
|
||||
)
|
||||
.subcommand_required(true)
|
||||
}
|
||||
|
||||
@@ -113,6 +132,30 @@ pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvi
|
||||
.expect("Failed to get interface orientation");
|
||||
println!("{:?}", orientation);
|
||||
}
|
||||
"get_homescreen_icon_metrics" => {
|
||||
let metrics = sbc
|
||||
.get_homescreen_icon_metrics()
|
||||
.await
|
||||
.expect("Failed to get homescreen icon metrics");
|
||||
let metrics_value = plist::Value::Dictionary(metrics);
|
||||
println!("{}", pretty_print_plist(&metrics_value));
|
||||
}
|
||||
"get_icon" => {
|
||||
let bundle_id = sub_args.next_argument::<String>().unwrap();
|
||||
|
||||
let icon_data = sbc
|
||||
.get_icon_pngdata(bundle_id)
|
||||
.await
|
||||
.expect("Failed to get icon");
|
||||
|
||||
let save_path = sub_args
|
||||
.get_flag::<String>("save")
|
||||
.unwrap_or("icon.png".to_string());
|
||||
|
||||
tokio::fs::write(&save_path, icon_data)
|
||||
.await
|
||||
.expect("Failed to save icon");
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user