From 4b6725b51f472f620f6202f8e100999cc2511186 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Mon, 21 Jul 2025 08:18:31 -0600 Subject: [PATCH 001/126] Clarify AdapterStream FFI docs --- ffi/src/adapter.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ffi/src/adapter.rs b/ffi/src/adapter.rs index 8e6edb8..f960cc1 100644 --- a/ffi/src/adapter.rs +++ b/ffi/src/adapter.rs @@ -91,7 +91,7 @@ pub unsafe extern "C" fn adapter_pcap( } } -/// Closes the adapter connection +/// Closes the adapter stream connection /// /// # Arguments /// * [`handle`] - The adapter stream handle @@ -119,10 +119,10 @@ pub unsafe extern "C" fn adapter_close(handle: *mut AdapterStreamHandle) -> *mut } } -/// Sends data through the adapter +/// Sends data through the adapter stream /// /// # Arguments -/// * [`handle`] - The adapter handle +/// * [`handle`] - The adapter stream handle /// * [`data`] - The data to send /// * [`length`] - The length of the data /// @@ -156,10 +156,10 @@ pub unsafe extern "C" fn adapter_send( } } -/// Receives data from the adapter +/// Receives data from the adapter stream /// /// # Arguments -/// * [`handle`] - The adapter handle +/// * [`handle`] - The adapter stream handle /// * [`data`] - Pointer to a buffer where the received data will be stored /// * [`length`] - Pointer to store the actual length of received data /// * [`max_length`] - Maximum number of bytes that can be stored in `data` From 66302173029ee5ec14a6dfb1ce1ea415c0c46b9e Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Mon, 21 Jul 2025 08:49:39 -0600 Subject: [PATCH 002/126] Bump version --- idevice/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/idevice/Cargo.toml b/idevice/Cargo.toml index 4549865..c7e0b14 100644 --- a/idevice/Cargo.toml +++ b/idevice/Cargo.toml @@ -2,7 +2,7 @@ name = "idevice" description = "A Rust library to interact with services on iOS devices." authors = ["Jackson Coxson"] -version = "0.1.35" +version = "0.1.36" edition = "2021" license = "MIT" documentation = "https://docs.rs/idevice" From bc25ceecec27836eae5a28e898016c736b725878 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Mon, 21 Jul 2025 16:26:13 -0600 Subject: [PATCH 003/126] Remove testing pcap from app service tool --- Cargo.lock | 2 +- tools/src/app_service.rs | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 015659c..f7dea1a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1182,7 +1182,7 @@ dependencies = [ [[package]] name = "idevice" -version = "0.1.35" +version = "0.1.36" dependencies = [ "base64", "byteorder", diff --git a/tools/src/app_service.rs b/tools/src/app_service.rs index 34b24f3..ea2d547 100644 --- a/tools/src/app_service.rs +++ b/tools/src/app_service.rs @@ -113,10 +113,6 @@ async fn main() { let rsd_port = proxy.handshake.server_rsd_port; let mut adapter = proxy.create_software_tunnel().expect("no software tunnel"); - adapter - .pcap("/Users/jacksoncoxson/Desktop/rs_xpc.pcap") - .await - .unwrap(); let stream = AdapterStream::connect(&mut adapter, rsd_port) .await From 648a92fa37b2fc8ac4b91c06e334695308fbd3ae Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Tue, 22 Jul 2025 10:48:36 -0600 Subject: [PATCH 004/126] Add missing usbmuxd functions to FFI --- ffi/src/usbmuxd.rs | 313 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 310 insertions(+), 3 deletions(-) diff --git a/ffi/src/usbmuxd.rs b/ffi/src/usbmuxd.rs index f39cf9f..f7e163d 100644 --- a/ffi/src/usbmuxd.rs +++ b/ffi/src/usbmuxd.rs @@ -1,18 +1,22 @@ // Jackson Coxson use std::{ - ffi::{CStr, c_char}, + ffi::{CStr, CString, c_char}, ptr::null_mut, }; -use crate::{IdeviceFfiError, RUNTIME, ffi_err, util::c_socket_to_rust}; +use crate::{ + IdeviceFfiError, IdeviceHandle, IdevicePairingFile, RUNTIME, ffi_err, util::c_socket_to_rust, +}; use idevice::{ IdeviceError, - usbmuxd::{UsbmuxdAddr, UsbmuxdConnection}, + usbmuxd::{UsbmuxdAddr, UsbmuxdConnection, UsbmuxdDevice}, }; +use log::error; pub struct UsbmuxdConnectionHandle(pub UsbmuxdConnection); pub struct UsbmuxdAddrHandle(pub UsbmuxdAddr); +pub struct UsbmuxdDeviceHandle(pub UsbmuxdDevice); /// Connects to a usbmuxd instance over TCP /// @@ -108,6 +112,7 @@ pub unsafe extern "C" fn idevice_usbmuxd_new_unix_socket_connection( /// # Safety /// `addr` must be a valid CStr /// `usbmuxd_connection` must be a valid, non-null pointer to a location where the handle will be stored +#[unsafe(no_mangle)] pub unsafe extern "C" fn idevice_usbmuxd_new_default_connection( tag: u32, usbmuxd_connection: *mut *mut UsbmuxdConnectionHandle, @@ -133,6 +138,203 @@ pub unsafe extern "C" fn idevice_usbmuxd_new_default_connection( } } +/// Gets a list of connected devices from usbmuxd. +/// +/// The returned list must be freed with `idevice_usbmuxd_device_list_free`. +/// +/// # Arguments +/// * `usbmuxd_conn` - A valid connection to usbmuxd. +/// * `devices` - A pointer to a C-style array of `UsbmuxdDeviceHandle` pointers. On success, this will be filled. +/// * `count` - A pointer to an integer. On success, this will be filled with the number of devices found. +/// +/// # Returns +/// An `IdeviceFfiError` on error, `null` on success. +/// +/// # Safety +/// * `usbmuxd_conn` must be a valid pointer. +/// * `devices` and `count` must be valid, non-null pointers. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn idevice_usbmuxd_get_devices( + usbmuxd_conn: *mut UsbmuxdConnectionHandle, + devices: *mut *mut *mut UsbmuxdDeviceHandle, + count: *mut libc::c_int, +) -> *mut IdeviceFfiError { + if usbmuxd_conn.is_null() { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + let conn = unsafe { &mut (*usbmuxd_conn).0 }; + + let res = RUNTIME.block_on(async { conn.get_devices().await }); + + match res { + Ok(device_vec) => { + unsafe { + *count = device_vec.len() as libc::c_int; + } + let mut c_arr = Vec::with_capacity(device_vec.len()); + for device in device_vec { + let handle = Box::new(UsbmuxdDeviceHandle(device)); + c_arr.push(Box::into_raw(handle)); + } + let mut c_arr = c_arr.into_boxed_slice(); + unsafe { + *devices = c_arr.as_mut_ptr(); + } + std::mem::forget(c_arr); // Prevent deallocation of the slice's buffer + null_mut() + } + Err(e) => ffi_err!(e), + } +} + +/// Connects to a service on a given device. +/// +/// This function consumes the `UsbmuxdConnectionHandle`. The handle will be invalid after this call +/// and must not be used again. The caller is NOT responsible for freeing it. +/// A new `IdeviceHandle` is returned on success, which must be freed by the caller. +/// +/// # Arguments +/// * `usbmuxd_connection` - The connection to use. It will be consumed. +/// * `device_id` - The ID of the device to connect to. +/// * `port` - The TCP port on the device to connect to. +/// * `idevice` - On success, points to the new device connection handle. +/// +/// # Returns +/// An `IdeviceFfiError` on error, `null` on success. +/// +/// # Safety +/// * `usbmuxd_connection` must be a valid pointer allocated by this library and never used again. +/// The value is consumed. +/// * `idevice` must be a valid, non-null pointer. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn idevice_usbmuxd_connect_to_device( + usbmuxd_connection: *mut UsbmuxdConnectionHandle, + device_id: u32, + port: u16, + label: *const c_char, + idevice: *mut *mut IdeviceHandle, +) -> *mut IdeviceFfiError { + if usbmuxd_connection.is_null() { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + + // Take ownership of the connection handle + let conn = unsafe { + let conn = std::ptr::read(&(*usbmuxd_connection).0); // move the inner connection + drop(Box::from_raw(usbmuxd_connection)); // free the wrapper + conn + }; + + let label = unsafe { + match CStr::from_ptr(label).to_str() { + Ok(s) => s, + Err(_) => return ffi_err!(IdeviceError::FfiInvalidArg), + } + }; + + let res = RUNTIME.block_on(async move { conn.connect_to_device(device_id, port, label).await }); + + match res { + Ok(device_conn) => { + let boxed = Box::new(IdeviceHandle(device_conn)); + unsafe { + *idevice = Box::into_raw(boxed); + } + null_mut() + } + Err(e) => ffi_err!(e), + } +} + +/// Reads the pairing record for a given device UDID. +/// +/// The returned `PairingFileHandle` must be freed with `idevice_pair_record_free`. +/// +/// # Arguments +/// * `usbmuxd_conn` - A valid connection to usbmuxd. +/// * `udid` - The UDID of the device. +/// * `pair_record` - On success, points to the new pairing file handle. +/// +/// # Returns +/// An `IdeviceFfiError` on error, `null` on success. +/// +/// # Safety +/// * `usbmuxd_conn` must be a valid pointer. +/// * `udid` must be a valid, null-terminated C string. +/// * `pair_record` must be a valid, non-null pointer. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn idevice_usbmuxd_get_pair_record( + usbmuxd_conn: *mut UsbmuxdConnectionHandle, + udid: *const c_char, + pair_record: *mut *mut IdevicePairingFile, +) -> *mut IdeviceFfiError { + if usbmuxd_conn.is_null() { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + let conn = unsafe { &mut (*usbmuxd_conn).0 }; + + let udid_str = unsafe { + match CStr::from_ptr(udid).to_str() { + Ok(s) => s, + Err(_) => return ffi_err!(IdeviceError::FfiInvalidArg), + } + }; + + let res = RUNTIME.block_on(async { conn.get_pair_record(udid_str).await }); + + match res { + Ok(pf) => { + let boxed = Box::new(IdevicePairingFile(pf)); + unsafe { + *pair_record = Box::into_raw(boxed); + } + null_mut() + } + Err(e) => ffi_err!(e), + } +} + +/// Reads the BUID (Boot-Unique ID) from usbmuxd. +/// +/// The returned string must be freed with `idevice_string_free`. +/// +/// # Arguments +/// * `usbmuxd_conn` - A valid connection to usbmuxd. +/// * `buid` - On success, points to a newly allocated, null-terminated C string. +/// +/// # Returns +/// An `IdeviceFfiError` on error, `null` on success. +/// +/// # Safety +/// * `usbmuxd_conn` must be a valid pointer. +/// * `buid` must be a valid, non-null pointer. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn idevice_usbmuxd_get_buid( + usbmuxd_conn: *mut UsbmuxdConnectionHandle, + buid: *mut *mut c_char, +) -> *mut IdeviceFfiError { + if usbmuxd_conn.is_null() { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + let conn = unsafe { &mut (*usbmuxd_conn).0 }; + + let res = RUNTIME.block_on(async { conn.get_buid().await }); + + match res { + Ok(buid_str) => match CString::new(buid_str) { + Ok(c_str) => { + unsafe { *buid = c_str.into_raw() }; + null_mut() + } + Err(e) => { + error!("Unable to convert BUID string to CString: {e:?}. Null interior byte."); + ffi_err!(IdeviceError::UnexpectedResponse) + } + }, + Err(e) => ffi_err!(e), + } +} + /// Frees a UsbmuxdConnection handle /// /// # Arguments @@ -225,3 +427,108 @@ pub unsafe extern "C" fn idevice_usbmuxd_addr_free(usbmuxd_addr: *mut UsbmuxdAdd let _ = unsafe { Box::from_raw(usbmuxd_addr) }; } } + +/// Frees a list of devices returned by `idevice_usbmuxd_get_devices`. +/// +/// # Arguments +/// * `devices` - The array of device handles to free. +/// * `count` - The number of elements in the array. +/// +/// # Safety +/// `devices` must be a valid pointer to an array of `count` device handles +/// allocated by this library, or NULL. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn idevice_usbmuxd_device_list_free( + devices: *mut *mut UsbmuxdDeviceHandle, + count: libc::c_int, +) { + if devices.is_null() { + return; + } + let slice = unsafe { std::slice::from_raw_parts_mut(devices, count as usize) }; + for &mut ptr in slice { + if !ptr.is_null() { + let _ = unsafe { Box::from_raw(ptr) }; + } + } +} + +/// Frees a usbmuxd device +/// +/// # Arguments +/// * `device` - The device handle to free. +/// +/// # Safety +/// `device` must be a valid pointer to the device handle +/// allocated by this library, or NULL. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn idevice_usbmuxd_device_free(device: *mut UsbmuxdDeviceHandle) { + if device.is_null() { + return; + } + let _ = unsafe { Box::from_raw(device) }; +} + +/// Gets the UDID from a device handle. +/// The returned string must be freed by the caller using `idevice_string_free`. +/// +/// # Safety +/// `device` must be a valid pointer to a `UsbmuxdDeviceHandle`. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn idevice_usbmuxd_device_get_udid( + device: *const UsbmuxdDeviceHandle, +) -> *mut c_char { + if device.is_null() { + return null_mut(); + } + let device = unsafe { &(*device).0 }; + match CString::new(device.udid.as_str()) { + Ok(s) => s.into_raw(), + Err(_) => null_mut(), + } +} + +/// Gets the device ID from a device handle. +/// +/// # Safety +/// `device` must be a valid pointer to a `UsbmuxdDeviceHandle`. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn idevice_usbmuxd_device_get_device_id( + device: *const UsbmuxdDeviceHandle, +) -> u32 { + if device.is_null() { + return 0; + } + unsafe { (*device).0.device_id } +} + +#[repr(C)] +enum UsbmuxdConnectionType { + Usb = 1, + Network = 2, + Unknown = 3, +} + +/// Gets the connection type (UsbmuxdConnectionType) from a device handle. +/// +/// # Returns +/// The enum value of the connection type, or 0 for null device handles +/// +/// # Safety +/// `device` must be a valid pointer to a `UsbmuxdDeviceHandle`. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn idevice_usbmuxd_device_get_connection_type( + device: *const UsbmuxdDeviceHandle, +) -> u8 { + if device.is_null() { + return 0; + } + let ct = unsafe { &(*device).0.connection_type }; + + let ct = match ct { + idevice::usbmuxd::Connection::Usb => UsbmuxdConnectionType::Usb, + idevice::usbmuxd::Connection::Network(_) => UsbmuxdConnectionType::Network, + idevice::usbmuxd::Connection::Unknown(_) => UsbmuxdConnectionType::Unknown, + }; + ct as u8 +} From f384df91d876ee2d659b5a19c88e6e959dfe6b2b Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Tue, 22 Jul 2025 10:48:53 -0600 Subject: [PATCH 005/126] Wrap C FFI in cpp extern C --- ffi/idevice.hpp | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 ffi/idevice.hpp diff --git a/ffi/idevice.hpp b/ffi/idevice.hpp new file mode 100644 index 0000000..6aac99c --- /dev/null +++ b/ffi/idevice.hpp @@ -0,0 +1,16 @@ +// Jackson Coxson - Bindings to idevice - https://github.com/jkcoxson/idevice +// This file only wraps the C bindings for C++, the C bindings still must be +// generated. +// ``cargo build --release`` + +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include "idevice.h" // this file is generated by bindgen + +#ifdef __cplusplus +} +#endif From 032a6a675153d87adaea53d1c47bcf80de7b544f Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Tue, 22 Jul 2025 10:49:14 -0600 Subject: [PATCH 006/126] usbmuxd class implementation for usbmuxd --- cpp/.clang-format | 28 +++ cpp/.clangd | 3 + cpp/examples/CMakeLists.txt | 77 ++++++++ cpp/examples/idevice_id.cpp | 38 ++++ cpp/include/ffi.hpp | 29 +++ cpp/include/idevice++.hpp | 80 ++++++++ cpp/include/pairing_file.hpp | 82 ++++++++ cpp/include/usbmuxd.hpp | 352 +++++++++++++++++++++++++++++++++++ 8 files changed, 689 insertions(+) create mode 100644 cpp/.clang-format create mode 100644 cpp/.clangd create mode 100644 cpp/examples/CMakeLists.txt create mode 100644 cpp/examples/idevice_id.cpp create mode 100644 cpp/include/ffi.hpp create mode 100644 cpp/include/idevice++.hpp create mode 100644 cpp/include/pairing_file.hpp create mode 100644 cpp/include/usbmuxd.hpp diff --git a/cpp/.clang-format b/cpp/.clang-format new file mode 100644 index 0000000..b084d5a --- /dev/null +++ b/cpp/.clang-format @@ -0,0 +1,28 @@ +# .clang-format +BasedOnStyle: LLVM +IndentWidth: 4 +TabWidth: 4 +UseTab: Never +ColumnLimit: 100 +BreakBeforeBraces: Attach +AllowShortFunctionsOnASingleLine: InlineOnly +AllowShortIfStatementsOnASingleLine: Never +AllowShortLoopsOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: false +AlwaysBreakAfterReturnType: None +AlignConsecutiveAssignments: AcrossEmptyLinesAndComments +AlignConsecutiveDeclarations: AcrossEmptyLinesAndComments +AlignTrailingComments: true +SortIncludes: CaseInsensitive +IncludeBlocks: Preserve +SpaceBeforeParens: ControlStatements +SpacesInParentheses: false +SpacesInSquareBrackets: false +SpacesInAngles: Never +SpaceAfterCStyleCast: true +Cpp11BracedListStyle: true +BreakBeforeBinaryOperators: NonAssignment +ReflowComments: true +PointerAlignment: Left +BinPackArguments: false +BinPackParameters: false diff --git a/cpp/.clangd b/cpp/.clangd new file mode 100644 index 0000000..aa2b394 --- /dev/null +++ b/cpp/.clangd @@ -0,0 +1,3 @@ +CompileFlags: + Add: ["-I/usr/local/include/"] + CompilationDatabase: "examples/build" diff --git a/cpp/examples/CMakeLists.txt b/cpp/examples/CMakeLists.txt new file mode 100644 index 0000000..6e78a4f --- /dev/null +++ b/cpp/examples/CMakeLists.txt @@ -0,0 +1,77 @@ +# Jackson Coxson + +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +cmake_minimum_required(VERSION 3.10) +project(IdeviceFFI CXX) + +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +# Set the paths +set(HEADER_FILE ${CMAKE_SOURCE_DIR}/../../ffi/idevice.h) +set(STATIC_LIB ${CMAKE_SOURCE_DIR}/../../target/release/libidevice_ffi.a) +set(EXAMPLES_DIR ${CMAKE_SOURCE_DIR}/../examples) + +set(IDEVICE_CPP_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/../include) # cpp/include +set(IDEVICE_FFI_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/../../ffi) # ffi/ + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Wpedantic") + +# Find all C++ example files +file(GLOB EXAMPLE_SOURCES ${EXAMPLES_DIR}/*.cpp) + +# Create an executable for each example file +foreach(EXAMPLE_FILE ${EXAMPLE_SOURCES}) + get_filename_component(EXAMPLE_NAME ${EXAMPLE_FILE} NAME_WE) + add_executable(${EXAMPLE_NAME} ${EXAMPLE_FILE}) + + target_include_directories(${EXAMPLE_NAME} PRIVATE + ${IDEVICE_CPP_INCLUDE_DIR} + ${IDEVICE_FFI_INCLUDE_DIR} + ) + + # Include the generated header + target_include_directories(${EXAMPLE_NAME} PRIVATE ${CMAKE_SOURCE_DIR}/..) + + # Link the static Rust library + target_link_libraries(${EXAMPLE_NAME} PRIVATE ${STATIC_LIB}) + + # libplist + + if( APPLE ) + # use static linking + find_library( LIBPLIST libplist-2.0.a REQUIRED ) + message( STATUS "(Static linking) LIBPLIST " ${LIBPLIST} ) + target_link_libraries ( ${EXAMPLE_NAME} PRIVATE ${LIBPLIST} ) + elseif( WIN32) + pkg_search_module(PLIST REQUIRED libplist-2.0) + find_library( LIBPLIST ${PLIST_LIBRARIES} PATH ${PLIST_LIBDIR} ) + target_link_libraries ( ${EXAMPLE_NAME} PRIVATE ${LIBPLIST} ) + else () + pkg_search_module(PLIST libplist>=2.0) + if(NOT PLIST_FOUND) + pkg_search_module(PLIST REQUIRED libplist-2.0) + endif() + find_library( LIBPLIST ${PLIST_LIBRARIES} PATH ${PLIST_LIBDIR} ) + target_link_libraries ( ${EXAMPLE_NAME} PUBLIC ${LIBPLIST} ) + endif() + if ( PLIST_FOUND ) + message( STATUS "found libplist-${PLIST_VERSION}" ) + endif() + target_include_directories( ${EXAMPLE_NAME} PRIVATE ${PLIST_INCLUDE_DIRS} ) + + # Bulk-link common macOS system frameworks + if(APPLE) + target_link_libraries(${EXAMPLE_NAME} PRIVATE + "-framework CoreFoundation" + "-framework Security" + "-framework SystemConfiguration" + "-framework CoreServices" + "-framework IOKit" + "-framework CFNetwork" + ) + endif() +endforeach() + + diff --git a/cpp/examples/idevice_id.cpp b/cpp/examples/idevice_id.cpp new file mode 100644 index 0000000..e2069d0 --- /dev/null +++ b/cpp/examples/idevice_id.cpp @@ -0,0 +1,38 @@ +// Jackson Coxson + +#include "ffi.hpp" +#include "usbmuxd.hpp" +#include +#include + +int main() { + std::cout << "Getting devices from usbmuxd\n"; + + IdeviceFFI::FfiError e; + std::optional u = + IdeviceFFI::UsbmuxdConnection::default_new(0, e); + if (u == std::nullopt) { + std::cerr << "failed to connect to usbmuxd"; + std::cerr << e.message; + } + + auto devices = u->get_devices(e); + if (u == std::nullopt) { + std::cerr << "failed to get devices from usbmuxd"; + std::cerr << e.message; + } + + for (IdeviceFFI::UsbmuxdDevice& d : *devices) { + auto udid = d.get_udid(); + if (!udid) { + std::cerr << "failed to get udid"; + continue; + } + auto connection_type = d.get_connection_type(); + if (!connection_type) { + std::cerr << "failed to get connection type"; + continue; + } + std::cout << *udid << " (" << connection_type->to_string() << ")" << "\n"; + } +} diff --git a/cpp/include/ffi.hpp b/cpp/include/ffi.hpp new file mode 100644 index 0000000..439b020 --- /dev/null +++ b/cpp/include/ffi.hpp @@ -0,0 +1,29 @@ +// Jackson Coxson + +#ifndef IDEVICE_FFI +#define IDEVICE_FFI + +#include "idevice.hpp" +#include + +namespace IdeviceFFI { +struct FfiError { + int32_t code = 0; + std::string message; + + static FfiError from(const IdeviceFfiError* err) { + FfiError out; + if (err) { + out.code = err->code; + out.message = err->message ? err->message : ""; + idevice_error_free(const_cast(err)); + } + return out; + } + + static FfiError success() { return {0, ""}; } + + explicit operator bool() const { return code != 0; } +}; +} // namespace IdeviceFFI +#endif diff --git a/cpp/include/idevice++.hpp b/cpp/include/idevice++.hpp new file mode 100644 index 0000000..dbd8736 --- /dev/null +++ b/cpp/include/idevice++.hpp @@ -0,0 +1,80 @@ +// Jackson Coxson + +#ifndef IDEVICE_CPP +#define IDEVICE_CPP + +#include "ffi.hpp" +#include "pairing_file.hpp" +#include +#include + +namespace IdeviceFFI { + +class Idevice { + public: + static std::optional + create(IdeviceSocketHandle* socket, const std::string& label, FfiError& err) { + IdeviceHandle* handle = nullptr; + IdeviceFfiError* e = idevice_new(socket, label.c_str(), &handle); + if (e) { + err = FfiError::from(e); + return std::nullopt; + } + return Idevice(handle); + } + + static std::optional + create_tcp(const sockaddr* addr, socklen_t addr_len, const std::string& label, FfiError& err) { + IdeviceHandle* handle = nullptr; + IdeviceFfiError* e = idevice_new_tcp_socket(addr, addr_len, label.c_str(), &handle); + if (e) { + err = FfiError::from(e); + return std::nullopt; + } + return Idevice(handle); + } + + std::optional get_type(FfiError& err) { + char* type_cstr = nullptr; + IdeviceFfiError* e = idevice_get_type(handle_, &type_cstr); + if (e) { + err = FfiError::from(e); + return std::nullopt; + } + + std::string type_str(type_cstr); + idevice_string_free(type_cstr); + return type_str; + } + + bool rsd_checkin(FfiError& err) { + IdeviceFfiError* e = idevice_rsd_checkin(handle_); + if (e) { + err = FfiError::from(e); + return false; + } + return true; + } + + bool start_session(PairingFile& pairing_file, FfiError& err) { + IdeviceFfiError* e = idevice_start_session(handle_, pairing_file.raw()); + if (e) { + err = FfiError::from(e); + return false; + } + return true; + } + + ~Idevice() { + if (handle_) + idevice_free(handle_); + } + + explicit Idevice(IdeviceHandle* h) : handle_(h) {} + + private: + IdeviceHandle* handle_; +}; + +} // namespace IdeviceFFI +#endif diff --git a/cpp/include/pairing_file.hpp b/cpp/include/pairing_file.hpp new file mode 100644 index 0000000..9edfcf8 --- /dev/null +++ b/cpp/include/pairing_file.hpp @@ -0,0 +1,82 @@ +// Jackson Coxson + +#ifndef IDEVICE_PAIRING_FILE +#define IDEVICE_PAIRING_FILE + +#pragma once + +#include "ffi.hpp" +#include +#include +#include + +namespace IdeviceFFI { +class PairingFile { + public: + static std::optional read(const std::string& path, FfiError& err) { + IdevicePairingFile* ptr = nullptr; + IdeviceFfiError* e = idevice_pairing_file_read(path.c_str(), &ptr); + if (e) { + err = FfiError::from(e); + return std::nullopt; + } + return PairingFile(ptr); + } + + static std::optional from_bytes(const uint8_t* data, size_t size, FfiError& err) { + IdevicePairingFile* raw = nullptr; + IdeviceFfiError* e = idevice_pairing_file_from_bytes(data, size, &raw); + if (e) { + err = FfiError::from(e); + return std::nullopt; + } + return PairingFile(raw); + } + + ~PairingFile() { + if (ptr_) { + idevice_pairing_file_free(ptr_); + } + } + + // Deleted copy constructor and assignment — use unique ownersship + PairingFile(const PairingFile&) = delete; + PairingFile& operator=(const PairingFile&) = delete; + + // Move constructor and assignment + PairingFile(PairingFile&& other) noexcept : ptr_(other.ptr_) { other.ptr_ = nullptr; } + + PairingFile& operator=(PairingFile&& other) noexcept { + if (this != &other) { + if (ptr_) { + idevice_pairing_file_free(ptr_); + } + ptr_ = other.ptr_; + other.ptr_ = nullptr; + } + return *this; + } + + std::optional> serialize(FfiError& err) const { + uint8_t* data = nullptr; + size_t size = 0; + IdeviceFfiError* e = idevice_pairing_file_serialize(ptr_, &data, &size); + if (e) { + err = FfiError::from(e); + return std::nullopt; + } + + std::vector result(data, data + size); + delete[] data; // NOTE: adjust this if deallocation uses `free` or a custom function + return result; + } + + explicit PairingFile(IdevicePairingFile* ptr) : ptr_(ptr) {} + IdevicePairingFile* raw() const { return ptr_; } + + private: + IdevicePairingFile* ptr_; +}; + +} // namespace IdeviceFFI +#endif diff --git a/cpp/include/usbmuxd.hpp b/cpp/include/usbmuxd.hpp new file mode 100644 index 0000000..8d9f3d8 --- /dev/null +++ b/cpp/include/usbmuxd.hpp @@ -0,0 +1,352 @@ +// Jackson Coxson + +#ifndef IDEVICE_USBMUXD_HPP +#define IDEVICE_USBMUXD_HPP + +#include "ffi.hpp" +#include "idevice++.hpp" +#include "pairing_file.hpp" +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#include +#else +#include +#endif + +namespace IdeviceFFI { + +/// @brief A C++ wrapper for a UsbmuxdAddrHandle. +/// @details This class manages the memory of a UsbmuxdAddrHandle pointer. +/// It is non-copyable but is movable. +class UsbmuxdAddr { + public: + /// @brief Creates a new TCP usbmuxd address. + /// @param addr The socket address to connect to. + /// @param addr_len The length of the socket address. + /// @param err An error that will be populated on failure. + /// @return A UsbmuxdAddr on success, std::nullopt on failure. + static std::optional + tcp_new(const sockaddr* addr, socklen_t addr_len, FfiError& err) { + UsbmuxdAddrHandle* handle = nullptr; + IdeviceFfiError* e = idevice_usbmuxd_tcp_addr_new(addr, addr_len, &handle); + if (e) { + err = FfiError::from(e); + return std::nullopt; + } + return UsbmuxdAddr(handle); + } + +#if defined(__unix__) || defined(__APPLE__) + /// @brief Creates a new Unix socket usbmuxd address. + /// @param path The path to the unix socket. + /// @param err An error that will be populated on failure. + /// @return A UsbmuxdAddr on success, std::nullopt on failure. + static std::optional unix_new(const std::string& path, FfiError& err) { + UsbmuxdAddrHandle* handle = nullptr; + IdeviceFfiError* e = idevice_usbmuxd_unix_addr_new(path.c_str(), &handle); + if (e) { + err = FfiError::from(e); + return std::nullopt; + } + return UsbmuxdAddr(handle); + } +#endif + + ~UsbmuxdAddr() { + if (handle_) { + idevice_usbmuxd_addr_free(handle_); + } + } + + // Delete copy constructor and assignment operator + UsbmuxdAddr(const UsbmuxdAddr&) = delete; + UsbmuxdAddr& operator=(const UsbmuxdAddr&) = delete; + + // Define move constructor and assignment operator + UsbmuxdAddr(UsbmuxdAddr&& other) noexcept : handle_(other.handle_) { other.handle_ = nullptr; } + UsbmuxdAddr& operator=(UsbmuxdAddr&& other) noexcept { + if (this != &other) { + if (handle_) { + idevice_usbmuxd_addr_free(handle_); + } + handle_ = other.handle_; + other.handle_ = nullptr; + } + return *this; + } + + /// @brief Gets the raw handle. + /// @return The raw UsbmuxdAddrHandle pointer. + UsbmuxdAddrHandle* raw() const { return handle_; } + + private: + explicit UsbmuxdAddr(UsbmuxdAddrHandle* handle) : handle_(handle) {} + UsbmuxdAddrHandle* handle_; +}; + +class UsbmuxdConnectionType { + public: + enum Value : uint8_t { Usb = 1, Network = 2, Unknown = 3 }; + explicit UsbmuxdConnectionType(uint8_t v) : _value(static_cast(v)) {} + + std::string to_string() { + switch (_value) { + case UsbmuxdConnectionType::Usb: + return "USB"; + case UsbmuxdConnectionType::Network: + return "Network"; + case UsbmuxdConnectionType::Unknown: + return "Unknown"; + default: + return "UnknownEnumValue"; + } + } + + Value value() const { return _value; } + + bool operator==(Value other) const { return _value == other; } + + private: + Value _value; +}; + +class UsbmuxdDevice { + public: + ~UsbmuxdDevice() { + if (handle_) { + idevice_usbmuxd_device_free(handle_); + } + } + // Delete copy constructor and assignment operator + UsbmuxdDevice(const UsbmuxdDevice&) = delete; + UsbmuxdDevice& operator=(const UsbmuxdDevice&) = delete; + + // Define move constructor and assignment operator + UsbmuxdDevice(UsbmuxdDevice&& other) noexcept : handle_(other.handle_) { + other.handle_ = nullptr; + } + UsbmuxdDevice& operator=(UsbmuxdDevice&& other) noexcept { + if (this != &other) { + if (handle_) { + idevice_usbmuxd_device_free(handle_); + } + handle_ = other.handle_; + other.handle_ = nullptr; + } + return *this; + } + + /// @brief Gets the raw handle. + /// @return The raw UsbmuxdConnectionHandle pointer. + UsbmuxdDeviceHandle* raw() const { return handle_; } + + std::optional get_udid() { + char* udid = idevice_usbmuxd_device_get_udid(handle_); + if (udid) { + std::string cppUdid(udid); + idevice_string_free(udid); + return cppUdid; + } else { + return std::nullopt; + } + } + + std::optional get_id() { + uint32_t id = idevice_usbmuxd_device_get_device_id(handle_); + if (id == 0) { + return std::nullopt; + } else { + return id; + } + } + + std::optional get_connection_type() { + u_int8_t t = idevice_usbmuxd_device_get_connection_type(handle_); + if (t == 0) { + return std::nullopt; + } else { + } + return static_cast(t); + } + explicit UsbmuxdDevice(UsbmuxdDeviceHandle* handle) : handle_(handle) {} + + private: + UsbmuxdDeviceHandle* handle_; +}; + +/// @brief A C++ wrapper for a UsbmuxdConnectionHandle. +/// @details This class manages the memory of a UsbmuxdConnectionHandle pointer. +/// It is non-copyable but is movable. +class UsbmuxdConnection { + public: + /// @brief Creates a new TCP usbmuxd connection. + /// @param addr The socket address to connect to. + /// @param addr_len The length of the socket address. + /// @param tag A tag that will be returned by usbmuxd responses. + /// @param err An error that will be populated on failure. + /// @return A UsbmuxdConnection on success, std::nullopt on failure. + static std::optional + tcp_new(const sockaddr* addr, socklen_t addr_len, uint32_t tag, FfiError& err) { + UsbmuxdConnectionHandle* handle = nullptr; + IdeviceFfiError* e = idevice_usbmuxd_new_tcp_connection(addr, addr_len, tag, &handle); + if (e) { + err = FfiError::from(e); + return std::nullopt; + } + return UsbmuxdConnection(handle); + } + +#if defined(__unix__) || defined(__APPLE__) + /// @brief Creates a new Unix socket usbmuxd connection. + /// @param path The path to the unix socket. + /// @param tag A tag that will be returned by usbmuxd responses. + /// @param err An error that will be populated on failure. + /// @return A UsbmuxdConnection on success, std::nullopt on failure. + static std::optional + unix_new(const std::string& path, uint32_t tag, FfiError& err) { + UsbmuxdConnectionHandle* handle = nullptr; + IdeviceFfiError* e = idevice_usbmuxd_new_unix_socket_connection(path.c_str(), tag, &handle); + if (e) { + err = FfiError::from(e); + return std::nullopt; + } + return UsbmuxdConnection(handle); + } +#endif + + /// @brief Creates a new usbmuxd connection using the default path. + /// @param tag A tag that will be returned by usbmuxd responses. + /// @param err An error that will be populated on failure. + /// @return A UsbmuxdConnection on success, std::nullopt on failure. + static std::optional default_new(uint32_t tag, FfiError& err) { + UsbmuxdConnectionHandle* handle = nullptr; + IdeviceFfiError* e = idevice_usbmuxd_new_default_connection(tag, &handle); + if (e) { + err = FfiError::from(e); + return std::nullopt; + } + return UsbmuxdConnection(handle); + } + + /// @brief Gets a list of all connected devices. + /// @param err An error that will be populated on failure. + /// @return A vector of UsbmuxdDevice objects on success, std::nullopt on failure. + std::optional> get_devices(FfiError& err) { + UsbmuxdDeviceHandle** devices = nullptr; + int count = 0; + IdeviceFfiError* e = idevice_usbmuxd_get_devices(handle_, &devices, &count); + if (e) { + err = FfiError::from(e); + return std::nullopt; + } + + std::vector result; + result.reserve(count); + for (int i = 0; i < count; ++i) { + result.emplace_back(devices[i]); + } + + return result; + } + + /// @brief Gets the BUID from the daemon. + /// @param err An error that will be populated on failure. + /// @return The BUID string on success, std::nullopt on failure. + std::optional get_buid(FfiError& err) { + char* buid_c = nullptr; + IdeviceFfiError* e = idevice_usbmuxd_get_buid(handle_, &buid_c); + if (e) { + err = FfiError::from(e); + return std::nullopt; + } + std::string buid(buid_c); + idevice_string_free(buid_c); + return buid; + } + + /// @brief Gets the pairing record for a device. + /// @param udid The UDID of the target device. + /// @param err An error that will be populated on failure. + /// @return A PairingFile object on success, std::nullopt on failure. + std::optional get_pair_record(const std::string& udid, FfiError& err) { + IdevicePairingFile* pf_handle = nullptr; + IdeviceFfiError* e = idevice_usbmuxd_get_pair_record(handle_, udid.c_str(), &pf_handle); + if (e) { + err = FfiError::from(e); + return std::nullopt; + } + return PairingFile(pf_handle); + } + + /// @brief Connects to a port on a given device. + /// @details This operation consumes the UsbmuxdConnection object. After a successful call, + /// this object will be invalid and should not be used. + /// @param device_id The ID of the target device. + /// @param port The port number to connect to. + /// @param err An error that will be populated on failure. + /// @return An Idevice connection object on success, std::nullopt on failure. + std::optional + connect_to_device(uint32_t device_id, uint16_t port, const std::string& path, FfiError& err) { + if (!handle_) { + // Can't connect with an invalid handle + return std::nullopt; + } + + IdeviceHandle* idevice_handle = nullptr; + IdeviceFfiError* e = idevice_usbmuxd_connect_to_device( + handle_, device_id, port, path.c_str(), &idevice_handle); + + // The handle is always consumed by the FFI call, so we must invalidate it. + handle_ = nullptr; + + if (e) { + err = FfiError::from(e); + return std::nullopt; + } + + return Idevice(idevice_handle); + } + + ~UsbmuxdConnection() { + if (handle_) { + idevice_usbmuxd_connection_free(handle_); + } + } + + // Delete copy constructor and assignment operator + UsbmuxdConnection(const UsbmuxdConnection&) = delete; + UsbmuxdConnection& operator=(const UsbmuxdConnection&) = delete; + + // Define move constructor and assignment operator + UsbmuxdConnection(UsbmuxdConnection&& other) noexcept : handle_(other.handle_) { + other.handle_ = nullptr; + } + UsbmuxdConnection& operator=(UsbmuxdConnection&& other) noexcept { + if (this != &other) { + if (handle_) { + idevice_usbmuxd_connection_free(handle_); + } + handle_ = other.handle_; + other.handle_ = nullptr; + } + return *this; + } + + /// @brief Gets the raw handle. + /// @return The raw UsbmuxdConnectionHandle pointer. + UsbmuxdConnectionHandle* raw() const { return handle_; } + + private: + explicit UsbmuxdConnection(UsbmuxdConnectionHandle* handle) : handle_(handle) {} + UsbmuxdConnectionHandle* handle_; +}; + +} // namespace IdeviceFFI + +#endif From 7fd374b0ed41ed4d7787333fb7874217cf65d165 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Tue, 22 Jul 2025 11:27:49 -0600 Subject: [PATCH 007/126] Add domain to lockdown get values C FFI example --- ffi/examples/lockdownd.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ffi/examples/lockdownd.c b/ffi/examples/lockdownd.c index 8c2d678..ad92b39 100644 --- a/ffi/examples/lockdownd.c +++ b/ffi/examples/lockdownd.c @@ -122,7 +122,7 @@ int main() { // Get all values plist_t all_values = NULL; - err = lockdownd_get_all_values(client, &all_values); + err = lockdownd_get_all_values(client, NULL, &all_values); if (err != NULL) { fprintf(stderr, "Failed to get all values: [%d] %s", err->code, err->message); From 93296079393ed8d3aaece1ea2e854e86d7670450 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Tue, 22 Jul 2025 11:34:35 -0600 Subject: [PATCH 008/126] Remove unused imports from app_service tool --- tools/src/app_service.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/tools/src/app_service.rs b/tools/src/app_service.rs index ea2d547..a90f72d 100644 --- a/tools/src/app_service.rs +++ b/tools/src/app_service.rs @@ -1,12 +1,9 @@ // Jackson Coxson -use std::io::Write; - use clap::{Arg, Command}; use idevice::{ - core_device::AppServiceClient, core_device_proxy::CoreDeviceProxy, - debug_proxy::DebugProxyClient, rsd::RsdHandshake, tcp::stream::AdapterStream, IdeviceService, - RsdService, + core_device::AppServiceClient, core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake, + tcp::stream::AdapterStream, IdeviceService, RsdService, }; mod common; From 1435421132dd629c4716c61651286dd192f2f7e2 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Tue, 22 Jul 2025 11:34:47 -0600 Subject: [PATCH 009/126] Add more just recipes for checking and building --- justfile | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/justfile b/justfile index c80cac0..701b743 100644 --- a/justfile +++ b/justfile @@ -3,6 +3,30 @@ check-features: cargo hack check --feature-powerset --no-dev-deps cd .. +ci-check: build-ffi-native build-tools-native build-cpp build-c + cargo clippy --all-targets --all-features -- -D warnings +macos-ci-check: ci-check xcframework + +[working-directory: 'ffi'] +build-ffi-native: + cargo build --release + +[working-directory: 'tools'] +build-tools-native: + cargo build --release + +create-example-build-folder: + mkdir -p cpp/examples/build + mkdir -p ffi/examples/build + +[working-directory: 'cpp/examples/build'] +build-cpp: build-ffi-native create-example-build-folder + cmake .. && make + +[working-directory: 'ffi/examples/build'] +build-c: build-ffi-native create-example-build-folder + cmake .. && make + xcframework: apple-build rm -rf swift/IDevice.xcframework rm -rf swift/libs From 07ed0612608d33890cb043ffa561634e3f9ee238 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Tue, 22 Jul 2025 11:34:57 -0600 Subject: [PATCH 010/126] Create ci.yml --- .github/workflows/ci.yml | 164 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..b89469c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,164 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +jobs: + macos: + name: macOS Build + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + + - name: Install just + run: | + curl -sL https://just.systems/install.sh | bash -s -- --to ~/.cargo/bin + echo "$HOME/.cargo/bin" >> $GITHUB_PATH + + - name: Cache Cargo + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: macos-cargo-${{ hashFiles('**/Cargo.lock', 'justfile') }} + restore-keys: macos-cargo- + + - name: Build all Apple targets and examples/tools + run: | + just macos-ci-check + + - name: Upload static libraries + uses: actions/upload-artifact@v4 + with: + name: libidevice-macos-a + path: | + target/*apple*/release/libidevice_ffi.a + + - name: Upload headers + uses: actions/upload-artifact@v4 + with: + name: idevice-headers + path: ffi/idevice.h + + - name: Upload macOS+iOS XCFramework + uses: actions/upload-artifact@v4 + with: + name: idevice-xcframework + path: swift/bundle.zip + + - name: Upload C examples/tools + uses: actions/upload-artifact@v4 + with: + name: idevice-c-examples-macos + path: cpp/examples/build-c/* + + - name: Upload C++ examples/tools + uses: actions/upload-artifact@v4 + with: + name: idevice-cpp-examples-macos + path: cpp/examples/build-cpp/* + + linux: + name: Linux Build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install build dependencies + run: sudo apt-get update && sudo apt-get install -y build-essential cmake + + - name: Install just + run: | + curl -sL https://just.systems/install.sh | bash -s -- --to ~/.cargo/bin + echo "$HOME/.cargo/bin" >> $GITHUB_PATH + + - name: Cache Cargo + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: linux-cargo-${{ hashFiles('**/Cargo.lock', 'justfile') }} + restore-keys: linux-cargo- + + - name: Build Rust and examples/tools + run: | + just ci-check + + - name: Upload static library + uses: actions/upload-artifact@v4 + with: + name: libidevice-linux-a + path: target/release/libidevice_ffi.a + + - name: Upload headers + uses: actions/upload-artifact@v4 + with: + name: idevice-headers + path: ffi/idevice.h + + - name: Upload C examples/tools + uses: actions/upload-artifact@v4 + with: + name: idevice-c-examples-linux + path: cpp/examples/build-c/* + + - name: Upload C++ examples/tools + uses: actions/upload-artifact@v4 + with: + name: idevice-cpp-examples-linux + path: cpp/examples/build-cpp/* + + windows: + name: Windows Build + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + + - name: Install just + run: | + iwr https://just.systems/install.ps1 -UseBasicParsing | iex + echo "$env:USERPROFILE\.cargo\bin" | Out-File -Append -FilePath $env:GITHUB_PATH -Encoding utf8 + + - name: Cache Cargo + uses: actions/cache@v4 + with: + path: | + ~\AppData\Local\cargo\registry + ~\AppData\Local\cargo\git + target + key: windows-cargo-${{ hashFiles('**/Cargo.lock', 'justfile') }} + restore-keys: windows-cargo- + + - name: Build Rust and examples/tools + run: | + just ci-check + + - name: Upload static library + uses: actions/upload-artifact@v4 + with: + name: libidevice-windows-a + path: target\release\idevice_ffi.lib + + - name: Upload headers + uses: actions/upload-artifact@v4 + with: + name: idevice-headers + path: ffi\idevice.h + + - name: Upload C examples/tools + uses: actions/upload-artifact@v4 + with: + name: idevice-c-examples-windows + path: cpp\examples\build-c\* + + - name: Upload C++ examples/tools + uses: actions/upload-artifact@v4 + with: + name: idevice-cpp-examples-windows + path: cpp\examples\build-cpp\* From 507ac3cb20c8b94b94376cfb06351fa5c14bb9e7 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Tue, 22 Jul 2025 11:36:08 -0600 Subject: [PATCH 011/126] Trigger CI From b5f47ff23c016bf498a33f2937052c2a34aa13a8 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Tue, 22 Jul 2025 11:36:38 -0600 Subject: [PATCH 012/126] Use master for CI build --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b89469c..91da672 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,7 @@ name: CI on: push: - branches: [main] + branches: [master] pull_request: jobs: From 8a1fbbf7402516790da3a2db922f6851eb0001cd Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Tue, 22 Jul 2025 14:17:46 -0600 Subject: [PATCH 013/126] Fix building FFI on Linux --- cpp/examples/CMakeLists.txt | 2 ++ cpp/include/usbmuxd.hpp | 1 - ffi/examples/CMakeLists.txt | 7 +++++++ ffi/examples/heartbeat.c | 1 - 4 files changed, 9 insertions(+), 2 deletions(-) diff --git a/cpp/examples/CMakeLists.txt b/cpp/examples/CMakeLists.txt index 6e78a4f..5ed223c 100644 --- a/cpp/examples/CMakeLists.txt +++ b/cpp/examples/CMakeLists.txt @@ -18,6 +18,8 @@ set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Wpedantic") +find_package(PkgConfig REQUIRED) + # Find all C++ example files file(GLOB EXAMPLE_SOURCES ${EXAMPLES_DIR}/*.cpp) diff --git a/cpp/include/usbmuxd.hpp b/cpp/include/usbmuxd.hpp index 8d9f3d8..07b0889 100644 --- a/cpp/include/usbmuxd.hpp +++ b/cpp/include/usbmuxd.hpp @@ -9,7 +9,6 @@ #include #include #include -#include #include #ifdef _WIN32 diff --git a/ffi/examples/CMakeLists.txt b/ffi/examples/CMakeLists.txt index 6dbf217..3849009 100644 --- a/ffi/examples/CMakeLists.txt +++ b/ffi/examples/CMakeLists.txt @@ -12,6 +12,8 @@ set(EXAMPLES_DIR ${CMAKE_SOURCE_DIR}/../examples) # Find all C example files file(GLOB EXAMPLE_SOURCES ${EXAMPLES_DIR}/*.c) +find_package(PkgConfig REQUIRED) + # Create an executable for each example file foreach(EXAMPLE_FILE ${EXAMPLE_SOURCES}) # Extract the filename without the path @@ -26,6 +28,11 @@ foreach(EXAMPLE_FILE ${EXAMPLE_SOURCES}) # Link the static Rust library target_link_libraries(${EXAMPLE_NAME} PRIVATE ${STATIC_LIB}) + if(UNIX AND NOT APPLE) + target_link_libraries(${EXAMPLE_NAME} PRIVATE m) + endif() + + # libplist if( APPLE ) diff --git a/ffi/examples/heartbeat.c b/ffi/examples/heartbeat.c index bda431b..7f71d3a 100644 --- a/ffi/examples/heartbeat.c +++ b/ffi/examples/heartbeat.c @@ -5,7 +5,6 @@ #include #include #include -#include int main() { // Initialize logger From a5d7894543efcf090f1ec49d3f069a915e89115f Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Thu, 24 Jul 2025 10:47:26 -0600 Subject: [PATCH 014/126] Install just with scoop on Windows --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 91da672..4a06b80 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -122,8 +122,8 @@ jobs: - name: Install just run: | - iwr https://just.systems/install.ps1 -UseBasicParsing | iex - echo "$env:USERPROFILE\.cargo\bin" | Out-File -Append -FilePath $env:GITHUB_PATH -Encoding utf8 + iex "& {$(irm get.scoop.sh)} -RunAsAdmin" + scoop install just - name: Cache Cargo uses: actions/cache@v4 From 5531392cf39129e1e0afef79f9ae048a6b0205fb Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Thu, 24 Jul 2025 13:32:09 -0600 Subject: [PATCH 015/126] Clean up tools warnings --- tools/src/mounter.rs | 2 +- tools/src/remotexpc.rs | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/tools/src/mounter.rs b/tools/src/mounter.rs index 8e7eff1..681705f 100644 --- a/tools/src/mounter.rs +++ b/tools/src/mounter.rs @@ -208,7 +208,7 @@ async fn main() { unique_chip_id, async |((n, d), _)| { let percent = (n as f64 / d as f64) * 100.0; - print!("\rProgress: {:.2}%", percent); + print!("\rProgress: {percent:.2}%"); std::io::stdout().flush().unwrap(); // Make sure it prints immediately if n == d { println!(); diff --git a/tools/src/remotexpc.rs b/tools/src/remotexpc.rs index 7f79180..e4bc551 100644 --- a/tools/src/remotexpc.rs +++ b/tools/src/remotexpc.rs @@ -3,7 +3,7 @@ use clap::{Arg, Command}; use idevice::{ - core_device_proxy::CoreDeviceProxy, tcp::stream::AdapterStream, xpc::RemoteXpcClient, + core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake, tcp::stream::AdapterStream, IdeviceService, }; @@ -66,13 +66,12 @@ async fn main() { let rsd_port = proxy.handshake.server_rsd_port; let mut adapter = proxy.create_software_tunnel().expect("no software tunnel"); - adapter.pcap("new_xpc.pcap").await.unwrap(); - let conn = AdapterStream::connect(&mut adapter, rsd_port) + + let stream = AdapterStream::connect(&mut adapter, rsd_port) .await .expect("no RSD connect"); // Make the connection to RemoteXPC - let mut client = RemoteXpcClient::new(Box::new(conn)).await.unwrap(); - - println!("{:#?}", client.do_handshake().await); + let handshake = RsdHandshake::new(stream).await.unwrap(); + println!("{:#?}", handshake.services); } From 10cb67f4378968fdfa83ae1e2b59822beb2fb8bb Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Fri, 25 Jul 2025 18:33:42 -0600 Subject: [PATCH 016/126] Add dir arg to crash_logs tool --- tools/src/crash_logs.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tools/src/crash_logs.rs b/tools/src/crash_logs.rs index cda1535..69e79b2 100644 --- a/tools/src/crash_logs.rs +++ b/tools/src/crash_logs.rs @@ -37,7 +37,11 @@ async fn main() { .help("Show about information") .action(clap::ArgAction::SetTrue), ) - .subcommand(Command::new("list").about("Lists the items in the directory")) + .subcommand( + Command::new("list") + .about("Lists the items in the directory") + .arg(Arg::new("dir").required(false).index(1)), + ) .subcommand(Command::new("flush").about("Flushes reports to the directory")) .subcommand( Command::new("pull") From 4abaca9cb88c4a4048b003c2ea964dce1aafcc93 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Tue, 29 Jul 2025 20:10:37 -0600 Subject: [PATCH 017/126] Install libplist on MacOS CI --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4a06b80..affaf6b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,6 +17,10 @@ jobs: curl -sL https://just.systems/install.sh | bash -s -- --to ~/.cargo/bin echo "$HOME/.cargo/bin" >> $GITHUB_PATH + - name: Install libplist + run: | + brew install libplist + - name: Cache Cargo uses: actions/cache@v4 with: From 6c7906b026c6ddb816ece079a74f821eef5af161 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Wed, 30 Jul 2025 14:39:32 -0600 Subject: [PATCH 018/126] Replace libplist with plist_ffi crate --- Cargo.lock | 638 +++++++++++++------------------- ffi/Cargo.toml | 4 +- ffi/examples/CMakeLists.txt | 25 -- ffi/src/adapter.rs | 10 +- ffi/src/installation_proxy.rs | 80 ++-- ffi/src/lockdown.rs | 11 +- ffi/src/mobile_image_mounter.rs | 17 +- ffi/src/util.rs | 22 -- justfile | 147 -------- 9 files changed, 329 insertions(+), 625 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f7dea1a..5373abc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,9 +13,9 @@ dependencies = [ [[package]] name = "adler2" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aho-corasick" @@ -41,20 +41,11 @@ dependencies = [ "libc", ] -[[package]] -name = "ansi_term" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" -dependencies = [ - "winapi", -] - [[package]] name = "anstream" -version = "0.6.18" +version = "0.6.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" dependencies = [ "anstyle", "anstyle-parse", @@ -67,33 +58,33 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" [[package]] name = "anstyle-parse" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" dependencies = [ "windows-sys 0.59.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.8" +version = "3.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6680de5231bd6ee4c6191b8a1325daa282b415391ec9d3a37bd34f2060dc73fa" +checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" dependencies = [ "anstyle", "once_cell_polyfill", @@ -108,9 +99,9 @@ checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" [[package]] name = "async-channel" -version = "2.3.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" dependencies = [ "concurrent-queue", "event-listener-strategy", @@ -130,37 +121,17 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" -[[package]] -name = "atty" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -dependencies = [ - "hermit-abi", - "libc", - "winapi", -] - [[package]] name = "autocfg" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" - -[[package]] -name = "autotools" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef941527c41b0fc0dd48511a8154cd5fc7e29200a0ff8b7203c5d777dbc795cf" -dependencies = [ - "cc", -] +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-lc-rs" -version = "1.13.1" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fcc8f365936c834db5514fc45aee5b1202d677e6b40e48468aaaa8183ca8c7" +checksum = "5c953fe1ba023e6b7730c0d4b031d06f267f23a46167dcbd40316644b10a17ba" dependencies = [ "aws-lc-sys", "zeroize", @@ -168,9 +139,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.29.0" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61b1d86e7705efe1be1b569bab41d4fa1e14e220b60a160f78de2db687add079" +checksum = "dbfd150b5dbdb988bcc8fb1fe787eb6b7ee6180ca24da683b61ea5405f3d43ff" dependencies = [ "bindgen 0.69.5", "cc", @@ -202,32 +173,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.7.3" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3" - -[[package]] -name = "bindgen" -version = "0.59.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bd2a9a458e8f4304c52c43ebb0cfbd520289f8379a52e329a38afda99bf8eb8" -dependencies = [ - "bitflags 1.3.2", - "cexpr", - "clang-sys", - "clap 2.34.0", - "env_logger 0.9.3", - "lazy_static", - "lazycell", - "log", - "peeking_take_while", - "proc-macro2", - "quote", - "regex", - "rustc-hash 1.1.0", - "shlex", - "which", -] +checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" [[package]] name = "bindgen" @@ -235,7 +183,7 @@ version = "0.69.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" dependencies = [ - "bitflags 2.9.1", + "bitflags", "cexpr", "clang-sys", "itertools 0.12.1", @@ -248,17 +196,17 @@ dependencies = [ "regex", "rustc-hash 1.1.0", "shlex", - "syn 2.0.101", + "syn 2.0.104", "which", ] [[package]] name = "bindgen" -version = "0.71.1" +version = "0.72.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" +checksum = "4f72209734318d0b619a5e0f5129918b848c416e122a3c4ce054e03cb87b726f" dependencies = [ - "bitflags 2.9.1", + "bitflags", "cexpr", "clang-sys", "itertools 0.13.0", @@ -269,15 +217,9 @@ dependencies = [ "regex", "rustc-hash 2.1.1", "shlex", - "syn 2.0.101", + "syn 2.0.104", ] -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - [[package]] name = "bitflags" version = "2.9.1" @@ -295,9 +237,9 @@ dependencies = [ [[package]] name = "blocking" -version = "1.6.1" +version = "1.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" dependencies = [ "async-channel", "async-task", @@ -308,9 +250,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.17.0" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "byteorder" @@ -366,28 +308,28 @@ dependencies = [ [[package]] name = "cbindgen" -version = "0.28.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadd868a2ce9ca38de7eeafdcec9c7065ef89b42b32f0839278d55f35c54d1ff" +checksum = "975982cdb7ad6a142be15bdf84aea7ec6a9e5d4d797c004d43185b24cfe4e684" dependencies = [ - "clap 4.5.39", - "heck 0.4.1", + "clap", + "heck", "indexmap", "log", "proc-macro2", "quote", "serde", "serde_json", - "syn 2.0.101", + "syn 2.0.104", "tempfile", "toml", ] [[package]] name = "cc" -version = "1.2.25" +version = "1.2.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0fc897dc1e865cc67c0e05a836d9d3f1df3cbe442aa4a9473b18e12624a4951" +checksum = "deec109607ca693028562ed836a5f1c4b8bd77755c4e132fc5ce11b0b6211ae7" dependencies = [ "jobserver", "libc", @@ -405,9 +347,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" [[package]] name = "cfg_aliases" @@ -442,24 +384,9 @@ dependencies = [ [[package]] name = "clap" -version = "2.34.0" +version = "4.5.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" -dependencies = [ - "ansi_term", - "atty", - "bitflags 1.3.2", - "strsim 0.8.0", - "textwrap", - "unicode-width", - "vec_map", -] - -[[package]] -name = "clap" -version = "4.5.39" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd60e63e9be68e5fb56422e397cf9baddded06dae1d2e523401542383bc72a9f" +checksum = "ed87a9d530bb41a67537289bafcac159cb3ee28460e0a4571123d2a778a6a882" dependencies = [ "clap_builder", "clap_derive", @@ -467,33 +394,33 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.39" +version = "4.5.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89cc6392a1f72bbeb820d71f32108f61fdaf18bc526e1d23954168a67759ef51" +checksum = "64f4f3f3c77c94aff3c7e9aac9a2ca1974a5adf392a8bb751e827d6d127ab966" dependencies = [ "anstream", "anstyle", "clap_lex", - "strsim 0.11.1", + "strsim", ] [[package]] name = "clap_derive" -version = "4.5.32" +version = "4.5.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" +checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491" dependencies = [ - "heck 0.5.0", + "heck", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] name = "clap_lex" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" [[package]] name = "cmake" @@ -506,9 +433,9 @@ dependencies = [ [[package]] name = "colorchoice" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "concurrent-queue" @@ -542,9 +469,9 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", ] @@ -586,7 +513,7 @@ checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -617,7 +544,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -651,19 +578,6 @@ dependencies = [ "regex", ] -[[package]] -name = "env_logger" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7" -dependencies = [ - "atty", - "humantime", - "log", - "regex", - "termcolor", -] - [[package]] name = "env_logger" version = "0.11.8" @@ -685,12 +599,12 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -728,9 +642,9 @@ checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe" [[package]] name = "flate2" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" +checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" dependencies = [ "crc32fast", "miniz_oxide", @@ -835,7 +749,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -884,7 +798,18 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ba121d81ab5ea05b0cd5858516266800bf965531a794f7ac58e3eeb804f364f" dependencies = [ - "bitflags 2.9.1", + "bitflags", + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "getifaddrs" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c016cebf305060d144de015c98191ede05c210af588857bc2d4f8611c04663" +dependencies = [ + "bitflags", "libc", "windows-sys 0.59.0", ] @@ -898,7 +823,7 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi 0.11.1+wasi-snapshot-preview1", "wasm-bindgen", ] @@ -930,15 +855,9 @@ checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" [[package]] name = "hashbrown" -version = "0.15.3" +version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" - -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" [[package]] name = "heck" @@ -946,15 +865,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "hermit-abi" -version = "0.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" -dependencies = [ - "libc", -] - [[package]] name = "home" version = "0.5.11" @@ -1004,12 +914,6 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" -[[package]] -name = "humantime" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f" - [[package]] name = "hyper" version = "1.6.0" @@ -1031,9 +935,9 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.6" +version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03a01595e11bdcec50946522c32dde3fc6914743000a68b93000965f2f02406d" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ "http", "hyper", @@ -1043,14 +947,14 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots 1.0.0", + "webpki-roots 1.0.2", ] [[package]] name = "hyper-util" -version = "0.1.13" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c293b6b3d21eca78250dc7dbebd6b9210ec5530e038cbfe0661b5c47ab06e8" +checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" dependencies = [ "base64", "bytes", @@ -1064,7 +968,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2", + "socket2 0.6.0", "tokio", "tower-service", "tracing", @@ -1188,7 +1092,7 @@ dependencies = [ "byteorder", "bytes", "chrono", - "env_logger 0.11.8", + "env_logger", "futures", "indexmap", "json", @@ -1196,7 +1100,7 @@ dependencies = [ "ns-keyed-archive", "obfstr", "plist", - "rand 0.9.1", + "rand 0.9.2", "reqwest", "rsa", "rustls", @@ -1206,7 +1110,7 @@ dependencies = [ "thiserror 2.0.12", "tokio", "tokio-rustls", - "tun-rs 2.1.4", + "tun-rs 2.5.3", "uuid", "x509-cert", ] @@ -1221,7 +1125,7 @@ dependencies = [ "log", "once_cell", "plist", - "plist_plus", + "plist_ffi", "simplelog", "tokio", ] @@ -1230,8 +1134,8 @@ dependencies = [ name = "idevice-tools" version = "0.1.0" dependencies = [ - "clap 4.5.39", - "env_logger 0.11.8", + "clap", + "env_logger", "idevice", "log", "ns-keyed-archive", @@ -1266,15 +1170,26 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.9.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" dependencies = [ "equivalent", "hashbrown", "serde", ] +[[package]] +name = "io-uring" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" +dependencies = [ + "bitflags", + "cfg-if", + "libc", +] + [[package]] name = "ipnet" version = "2.11.0" @@ -1323,9 +1238,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jiff" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a194df1107f33c79f4f93d02c80798520551949d59dfad22b6157048a88cca93" +checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" dependencies = [ "jiff-static", "log", @@ -1336,13 +1251,13 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c6e1db7ed32c6c71b759497fae34bf7933636f75a251b9e736555da426f6442" +checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -1388,9 +1303,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.172" +version = "0.2.174" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" [[package]] name = "libloading" @@ -1399,7 +1314,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" dependencies = [ "cfg-if", - "windows-targets 0.53.0", + "windows-targets 0.53.3", ] [[package]] @@ -1460,9 +1375,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.4" +version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" [[package]] name = "memoffset" @@ -1473,12 +1388,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - [[package]] name = "minimal-lexical" version = "0.2.1" @@ -1487,9 +1396,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", ] @@ -1501,7 +1410,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi 0.11.1+wasi-snapshot-preview1", "windows-sys 0.59.0", ] @@ -1527,12 +1436,12 @@ dependencies = [ [[package]] name = "netlink-packet-route" -version = "0.22.0" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0e7987b28514adf555dc1f9a5c30dfc3e50750bbaffb1aec41ca7b23dcd8e4" +checksum = "56d83370a96813d7c977f8b63054f1162df6e5784f1c598d689236564fb5a6f2" dependencies = [ "anyhow", - "bitflags 2.9.1", + "bitflags", "byteorder", "libc", "log", @@ -1569,7 +1478,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.9.1", + "bitflags", "cfg-if", "cfg_aliases", "libc", @@ -1582,7 +1491,7 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags 2.9.1", + "bitflags", "cfg-if", "cfg_aliases", "libc", @@ -1614,7 +1523,7 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f237a10fe003123daa55a74b63a0b0a65de1671b2d128711ffe5886891a8f77f" dependencies = [ - "clap 4.5.39", + "clap", "plist", "thiserror 2.0.12", ] @@ -1743,12 +1652,6 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" -[[package]] -name = "peeking_take_while" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" - [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -1810,9 +1713,9 @@ dependencies = [ [[package]] name = "plist" -version = "1.7.1" +version = "1.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac26e981c03a6e53e0aee43c113e3202f5581d5360dae7bd2c70e800dd0451d" +checksum = "3af6b589e163c5a788fab00ce0c0366f6efbb9959c2f9874b224936af7fce7e1" dependencies = [ "base64", "indexmap", @@ -1822,24 +1725,23 @@ dependencies = [ ] [[package]] -name = "plist_plus" -version = "0.2.6" +name = "plist_ffi" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "167429a361cacecf5cab907c235e620b3faf5f36b97d7f72c32907ccdca700cf" +checksum = "06fba796e1466c87b971298751de238bb441f6a2a5b4b03b874880225550e4b3" dependencies = [ - "autotools", - "bindgen 0.59.2", + "cbindgen", "cc", "libc", - "log", - "rand 0.8.5", + "plist", + "serde_json", ] [[package]] name = "portable-atomic" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" [[package]] name = "portable-atomic-util" @@ -1876,12 +1778,12 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.33" +version = "0.2.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dee91521343f4c5c6a63edd65e54f31f5c92fe8978c40a4282f8372194c6a7d" +checksum = "ff24dfcda44452b9816fff4cd4227e1bb73ff5a2f1bc1105aa92fb8565ce44d2" dependencies = [ "proc-macro2", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -1895,9 +1797,9 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.32.0" +version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d3a6e5838b60e0e8fa7a43f22ade549a37d61f8bdbe636d0d7816191de969c2" +checksum = "8927b0664f5c5a98265138b7e3f90aa19a6b21353182469ace36d4ac527b7b1b" dependencies = [ "memchr", ] @@ -1915,7 +1817,7 @@ dependencies = [ "quinn-udp", "rustc-hash 2.1.1", "rustls", - "socket2", + "socket2 0.5.10", "thiserror 2.0.12", "tokio", "tracing", @@ -1931,7 +1833,7 @@ dependencies = [ "bytes", "getrandom 0.3.3", "lru-slab", - "rand 0.9.1", + "rand 0.9.2", "ring", "rustc-hash 2.1.1", "rustls", @@ -1945,14 +1847,14 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.12" +version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee4e529991f949c5e25755532370b8af5d114acae52326361d68d47af64aa842" +checksum = "fcebb1209ee276352ef14ff8732e24cc2b02bbac986cd74a4c81bcb2f9881970" dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2", + "socket2 0.5.10", "tracing", "windows-sys 0.59.0", ] @@ -1968,9 +1870,9 @@ dependencies = [ [[package]] name = "r-efi" -version = "5.2.0" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "rand" @@ -1978,16 +1880,15 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ - "libc", "rand_chacha 0.3.1", "rand_core 0.6.4", ] [[package]] name = "rand" -version = "0.9.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.3", @@ -2033,11 +1934,11 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.12" +version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" +checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" dependencies = [ - "bitflags 2.9.1", + "bitflags", ] [[package]] @@ -2071,9 +1972,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "reqwest" -version = "0.12.19" +version = "0.12.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2f8e5513d63f2e5b386eb5106dc67eaf3f84e95258e210489136b8b92ad6119" +checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531" dependencies = [ "base64", "bytes", @@ -2084,11 +1985,8 @@ dependencies = [ "hyper", "hyper-rustls", "hyper-util", - "ipnet", "js-sys", "log", - "mime", - "once_cell", "percent-encoding", "pin-project-lite", "quinn", @@ -2107,7 +2005,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots 1.0.0", + "webpki-roots 1.0.2", ] [[package]] @@ -2126,17 +2024,17 @@ dependencies = [ [[package]] name = "route_manager" -version = "0.1.4" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f6f4d3aee0d83fd4da6dc58abc2ae1765504e4b8e70d32f5876376c6c8f3f4c" +checksum = "0f3a4eae9e0cc97729d29d7d0936dd30170c80625cca122a56856fd23d619159" dependencies = [ - "bindgen 0.71.1", + "bindgen 0.72.0", "flume", "libc", "netlink-packet-core", "netlink-packet-route", "netlink-sys", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -2162,9 +2060,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.24" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" [[package]] name = "rustc-hash" @@ -2184,7 +2082,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.9.1", + "bitflags", "errno", "libc", "linux-raw-sys 0.4.15", @@ -2193,22 +2091,22 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" +checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" dependencies = [ - "bitflags 2.9.1", + "bitflags", "errno", "libc", "linux-raw-sys 0.9.4", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "rustls" -version = "0.23.27" +version = "0.23.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321" +checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" dependencies = [ "aws-lc-rs", "log", @@ -2241,9 +2139,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.3" +version = "0.103.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" +checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" dependencies = [ "aws-lc-rs", "ring", @@ -2286,14 +2184,14 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] name = "serde_json" -version = "1.0.140" +version = "1.0.141" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +checksum = "30b9eff21ebe718216c6ec64e1d9ac57087aad11efc64e32002bce4a0d4c03d3" dependencies = [ "itoa", "memchr", @@ -2303,9 +2201,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.8" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" dependencies = [ "serde", ] @@ -2382,18 +2280,15 @@ dependencies = [ [[package]] name = "slab" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] +checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" [[package]] name = "smallvec" -version = "1.15.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" @@ -2405,6 +2300,16 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "socket2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "spin" version = "0.9.8" @@ -2430,12 +2335,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" -[[package]] -name = "strsim" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" - [[package]] name = "strsim" version = "0.11.1" @@ -2461,9 +2360,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.101" +version = "2.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" dependencies = [ "proc-macro2", "quote", @@ -2487,7 +2386,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -2499,7 +2398,7 @@ dependencies = [ "fastrand", "getrandom 0.3.3", "once_cell", - "rustix 1.0.7", + "rustix 1.0.8", "windows-sys 0.59.0", ] @@ -2512,15 +2411,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "textwrap" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" -dependencies = [ - "unicode-width", -] - [[package]] name = "thiserror" version = "1.0.69" @@ -2547,7 +2437,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -2558,7 +2448,7 @@ checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -2637,25 +2527,27 @@ checksum = "2d2e76690929402faae40aebdda620a2c0e25dd6d3b9afe48867dfd95991f4bd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] name = "tokio" -version = "1.45.1" +version = "1.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" +checksum = "43864ed400b6043a4757a25c7a64a8efde741aed79a056a2fb348a406701bb35" dependencies = [ "backtrace", "bytes", + "io-uring", "libc", "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2", + "slab", + "socket2 0.6.0", "tokio-macros", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2666,7 +2558,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -2681,9 +2573,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.22" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05ae329d1f08c4d17a59bed7ff5b5a769d062e64a62d34a3261b219e62cd5aae" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", "serde_spanned", @@ -2693,18 +2585,18 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.9" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.22.26" +version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap", "serde", @@ -2716,9 +2608,9 @@ dependencies = [ [[package]] name = "toml_write" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" [[package]] name = "tower" @@ -2737,11 +2629,11 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc2d9e086a412a451384326f521c8123a99a466b329941a9403696bff9b0da2" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" dependencies = [ - "bitflags 2.9.1", + "bitflags", "bytes", "futures-util", "http", @@ -2777,9 +2669,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.33" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ "once_cell", ] @@ -2796,14 +2688,14 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53141e64197ff7e758b8152615e50bb4a3b18c970738876e7906d31f242c7d6e" dependencies = [ - "bitflags 2.9.1", + "bitflags", "blocking", "byteorder", "bytes", "c2rust-bitfields 0.19.0", "cfg-if", "encoding_rs", - "getifaddrs", + "getifaddrs 0.1.5", "ipnet", "libc", "libloading", @@ -2820,17 +2712,17 @@ dependencies = [ [[package]] name = "tun-rs" -version = "2.1.4" +version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89aa659131eb386e5bfe7443d97e2dfb0e0ccef611f2de14fc73549e6db60a7a" +checksum = "36e061d3f14bea7007179e5912e20a6bf4857ab9f3676b2ed55dafccba38410f" dependencies = [ - "bindgen 0.71.1", + "bindgen 0.72.0", "blocking", "byteorder", "bytes", "c2rust-bitfields 0.20.0", "encoding_rs", - "getifaddrs", + "getifaddrs 0.2.0", "ipnet", "libc", "libloading", @@ -2841,7 +2733,7 @@ dependencies = [ "scopeguard", "tokio", "widestring", - "windows-sys 0.59.0", + "windows-sys 0.60.2", "winreg 0.55.0", ] @@ -2857,12 +2749,6 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" -[[package]] -name = "unicode-width" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" - [[package]] name = "untrusted" version = "0.9.0" @@ -2871,9 +2757,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "ureq" -version = "3.0.11" +version = "3.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7a3e9af6113ecd57b8c63d3cd76a385b2e3881365f1f489e54f49801d0c83ea" +checksum = "9f0fde9bc91026e381155f8c67cb354bcd35260b2f4a29bcc84639f762760c39" dependencies = [ "base64", "flate2", @@ -2889,9 +2775,9 @@ dependencies = [ [[package]] name = "ureq-proto" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fadf18427d33828c311234884b7ba2afb57143e6e7e69fda7ee883b624661e36" +checksum = "59db78ad1923f2b1be62b6da81fe80b173605ca0d57f85da2e005382adf693f7" dependencies = [ "base64", "http", @@ -2940,12 +2826,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "vec_map" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" - [[package]] name = "version_check" version = "0.9.5" @@ -2963,9 +2843,9 @@ dependencies = [ [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" @@ -2998,7 +2878,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", "wasm-bindgen-shared", ] @@ -3033,7 +2913,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3073,14 +2953,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.0", + "webpki-roots 1.0.2", ] [[package]] name = "webpki-roots" -version = "1.0.0" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2853738d1cc4f2da3a225c18ec6c3721abb31961096e9dbf5ab35fa88b19cfdb" +checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" dependencies = [ "rustls-pki-types", ] @@ -3155,7 +3035,7 @@ checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -3166,14 +3046,14 @@ checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] name = "windows-link" -version = "0.1.1" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" [[package]] name = "windows-result" @@ -3220,6 +3100,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.3", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -3253,10 +3142,11 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.0" +version = "0.53.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" +checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" dependencies = [ + "windows-link", "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", "windows_i686_gnu 0.53.0", @@ -3407,9 +3297,9 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "winnow" -version = "0.7.10" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec" +checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" dependencies = [ "memchr", ] @@ -3436,15 +3326,15 @@ dependencies = [ [[package]] name = "wintun-bindings" -version = "0.7.31" +version = "0.7.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "605f50b13e12e1f9f99dc5e93701d779dbe47282fec186cb8a079165368d3124" +checksum = "b88303b411e20a1319b368dcd04db1480003ed46ac35193e139f542720b15fbf" dependencies = [ - "c2rust-bitfields 0.19.0", + "c2rust-bitfields 0.20.0", "libloading", "log", "thiserror 2.0.12", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -3453,7 +3343,7 @@ version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ - "bitflags 2.9.1", + "bitflags", ] [[package]] @@ -3496,28 +3386,28 @@ checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.25" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.25" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -3537,7 +3427,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", "synstructure", ] @@ -3558,7 +3448,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -3591,5 +3481,5 @@ checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] diff --git a/ffi/Cargo.toml b/ffi/Cargo.toml index a17fde6..04878d8 100644 --- a/ffi/Cargo.toml +++ b/ffi/Cargo.toml @@ -12,7 +12,7 @@ once_cell = "1.21.1" tokio = { version = "1.44.1", features = ["full"] } libc = "0.2.171" plist = "1.7.1" -plist_plus = { version = "0.2.6", features = ["dynamic"] } +plist_ffi = "0.1.3" [features] afc = ["idevice/afc"] @@ -67,7 +67,7 @@ full = [ default = ["full"] [build-dependencies] -cbindgen = "0.28.0" +cbindgen = "0.29.0" [lib] crate-type = ["staticlib"] diff --git a/ffi/examples/CMakeLists.txt b/ffi/examples/CMakeLists.txt index 3849009..d68866f 100644 --- a/ffi/examples/CMakeLists.txt +++ b/ffi/examples/CMakeLists.txt @@ -32,31 +32,6 @@ foreach(EXAMPLE_FILE ${EXAMPLE_SOURCES}) target_link_libraries(${EXAMPLE_NAME} PRIVATE m) endif() - - # libplist - - if( APPLE ) - # use static linking - find_library( LIBPLIST libplist-2.0.a REQUIRED ) - message( STATUS "(Static linking) LIBPLIST " ${LIBPLIST} ) - target_link_libraries ( ${EXAMPLE_NAME} PRIVATE ${LIBPLIST} ) - elseif( WIN32) - pkg_search_module(PLIST REQUIRED libplist-2.0) - find_library( LIBPLIST ${PLIST_LIBRARIES} PATH ${PLIST_LIBDIR} ) - target_link_libraries ( ${EXAMPLE_NAME} PRIVATE ${LIBPLIST} ) - else () - pkg_search_module(PLIST libplist>=2.0) - if(NOT PLIST_FOUND) - pkg_search_module(PLIST REQUIRED libplist-2.0) - endif() - find_library( LIBPLIST ${PLIST_LIBRARIES} PATH ${PLIST_LIBDIR} ) - target_link_libraries ( ${EXAMPLE_NAME} PUBLIC ${LIBPLIST} ) - endif() - if ( PLIST_FOUND ) - message( STATUS "found libplist-${PLIST_VERSION}" ) - endif() - target_include_directories( ${EXAMPLE_NAME} PRIVATE ${PLIST_INCLUDE_DIRS} ) - # Bulk-link common macOS system frameworks if(APPLE) target_link_libraries(${EXAMPLE_NAME} PRIVATE diff --git a/ffi/src/adapter.rs b/ffi/src/adapter.rs index f960cc1..553162a 100644 --- a/ffi/src/adapter.rs +++ b/ffi/src/adapter.rs @@ -46,7 +46,7 @@ pub unsafe extern "C" fn adapter_connect( null_mut() } Err(e) => { - log::error!("Adapter connect failed: {}", e); + log::error!("Adapter connect failed: {e}"); ffi_err!(e) } } @@ -85,7 +85,7 @@ pub unsafe extern "C" fn adapter_pcap( match res { Ok(_) => null_mut(), Err(e) => { - log::error!("Adapter pcap failed: {}", e); + log::error!("Adapter pcap failed: {e}"); ffi_err!(e) } } @@ -113,7 +113,7 @@ pub unsafe extern "C" fn adapter_close(handle: *mut AdapterStreamHandle) -> *mut match res { Ok(_) => null_mut(), Err(e) => { - log::error!("Adapter close failed: {}", e); + log::error!("Adapter close failed: {e}"); ffi_err!(e) } } @@ -150,7 +150,7 @@ pub unsafe extern "C" fn adapter_send( match res { Ok(_) => null_mut(), Err(e) => { - log::error!("Adapter send failed: {}", e); + log::error!("Adapter send failed: {e}"); ffi_err!(e) } } @@ -200,7 +200,7 @@ pub unsafe extern "C" fn adapter_recv( null_mut() } Err(e) => { - log::error!("Adapter recv failed: {}", e); + log::error!("Adapter recv failed: {e}"); ffi_err!(e) } } diff --git a/ffi/src/installation_proxy.rs b/ffi/src/installation_proxy.rs index e3c4c65..6bdca82 100644 --- a/ffi/src/installation_proxy.rs +++ b/ffi/src/installation_proxy.rs @@ -6,10 +6,9 @@ use idevice::{ IdeviceError, IdeviceService, installation_proxy::InstallationProxyClient, provider::IdeviceProvider, }; +use plist_ffi::{PlistWrapper, plist_t}; -use crate::{ - IdeviceFfiError, IdeviceHandle, RUNTIME, ffi_err, provider::IdeviceProviderHandle, util, -}; +use crate::{IdeviceFfiError, IdeviceHandle, RUNTIME, ffi_err, provider::IdeviceProviderHandle}; pub struct InstallationProxyClientHandle(pub InstallationProxyClient); @@ -132,10 +131,10 @@ pub unsafe extern "C" fn installation_proxy_get_apps( ) }; - let res: Result, IdeviceError> = RUNTIME.block_on(async { + let res: Result, IdeviceError> = RUNTIME.block_on(async { client.0.get_apps(app_type, bundle_ids).await.map(|apps| { apps.into_values() - .map(|v| util::plist_to_libplist(&v)) + .map(|v| PlistWrapper::new_node(v).into_ptr()) .collect() }) }); @@ -192,7 +191,7 @@ pub unsafe extern "C" fn installation_proxy_client_free( pub unsafe extern "C" fn installation_proxy_install( client: *mut InstallationProxyClientHandle, package_path: *const libc::c_char, - options: *mut c_void, + options: plist_t, ) -> *mut IdeviceFfiError { if client.is_null() || package_path.is_null() { return ffi_err!(IdeviceError::FfiInvalidArg); @@ -204,8 +203,9 @@ pub unsafe extern "C" fn installation_proxy_install( let options = if options.is_null() { None } else { - Some(util::libplist_to_plist(options)) - }; + Some(unsafe { &mut *options }) + } + .map(|x| x.borrow_self().clone()); let res = RUNTIME.block_on(async { unsafe { &mut *client } @@ -240,7 +240,7 @@ pub unsafe extern "C" fn installation_proxy_install( pub unsafe extern "C" fn installation_proxy_install_with_callback( client: *mut InstallationProxyClientHandle, package_path: *const libc::c_char, - options: *mut c_void, + options: plist_t, callback: extern "C" fn(progress: u64, context: *mut c_void), context: *mut c_void, ) -> *mut IdeviceFfiError { @@ -254,8 +254,9 @@ pub unsafe extern "C" fn installation_proxy_install_with_callback( let options = if options.is_null() { None } else { - Some(util::libplist_to_plist(options)) - }; + Some(unsafe { &mut *options }) + } + .map(|x| x.borrow_self().clone()); let res = RUNTIME.block_on(async { let callback_wrapper = |(progress, context)| async move { @@ -292,7 +293,7 @@ pub unsafe extern "C" fn installation_proxy_install_with_callback( pub unsafe extern "C" fn installation_proxy_upgrade( client: *mut InstallationProxyClientHandle, package_path: *const libc::c_char, - options: *mut c_void, + options: plist_t, ) -> *mut IdeviceFfiError { if client.is_null() || package_path.is_null() { return ffi_err!(IdeviceError::FfiInvalidArg); @@ -304,8 +305,9 @@ pub unsafe extern "C" fn installation_proxy_upgrade( let options = if options.is_null() { None } else { - Some(util::libplist_to_plist(options)) - }; + Some(unsafe { &mut *options }) + } + .map(|x| x.borrow_self().clone()); let res = RUNTIME.block_on(async { unsafe { &mut *client } @@ -340,7 +342,7 @@ pub unsafe extern "C" fn installation_proxy_upgrade( pub unsafe extern "C" fn installation_proxy_upgrade_with_callback( client: *mut InstallationProxyClientHandle, package_path: *const libc::c_char, - options: *mut c_void, + options: plist_t, callback: extern "C" fn(progress: u64, context: *mut c_void), context: *mut c_void, ) -> *mut IdeviceFfiError { @@ -354,8 +356,9 @@ pub unsafe extern "C" fn installation_proxy_upgrade_with_callback( let options = if options.is_null() { None } else { - Some(util::libplist_to_plist(options)) - }; + Some(unsafe { &mut *options }) + } + .map(|x| x.borrow_self().clone()); let res = RUNTIME.block_on(async { let callback_wrapper = |(progress, context)| async move { @@ -392,7 +395,7 @@ pub unsafe extern "C" fn installation_proxy_upgrade_with_callback( pub unsafe extern "C" fn installation_proxy_uninstall( client: *mut InstallationProxyClientHandle, bundle_id: *const libc::c_char, - options: *mut c_void, + options: plist_t, ) -> *mut IdeviceFfiError { if client.is_null() || bundle_id.is_null() { return ffi_err!(IdeviceError::FfiInvalidArg); @@ -404,8 +407,9 @@ pub unsafe extern "C" fn installation_proxy_uninstall( let options = if options.is_null() { None } else { - Some(util::libplist_to_plist(options)) - }; + Some(unsafe { &mut *options }) + } + .map(|x| x.borrow_self().clone()); let res = RUNTIME.block_on(async { unsafe { &mut *client } @@ -440,7 +444,7 @@ pub unsafe extern "C" fn installation_proxy_uninstall( pub unsafe extern "C" fn installation_proxy_uninstall_with_callback( client: *mut InstallationProxyClientHandle, bundle_id: *const libc::c_char, - options: *mut c_void, + options: plist_t, callback: extern "C" fn(progress: u64, context: *mut c_void), context: *mut c_void, ) -> *mut IdeviceFfiError { @@ -454,8 +458,9 @@ pub unsafe extern "C" fn installation_proxy_uninstall_with_callback( let options = if options.is_null() { None } else { - Some(util::libplist_to_plist(options)) - }; + Some(unsafe { &mut *options }) + } + .map(|x| x.borrow_self().clone()); let res = RUNTIME.block_on(async { let callback_wrapper = |(progress, context)| async move { @@ -494,9 +499,9 @@ pub unsafe extern "C" fn installation_proxy_uninstall_with_callback( #[unsafe(no_mangle)] pub unsafe extern "C" fn installation_proxy_check_capabilities_match( client: *mut InstallationProxyClientHandle, - capabilities: *const *mut c_void, + capabilities: *const plist_t, capabilities_len: libc::size_t, - options: *mut c_void, + options: plist_t, out_result: *mut bool, ) -> *mut IdeviceFfiError { if client.is_null() || out_result.is_null() { @@ -508,15 +513,16 @@ pub unsafe extern "C" fn installation_proxy_check_capabilities_match( } else { unsafe { std::slice::from_raw_parts(capabilities, capabilities_len) } .iter() - .map(|&ptr| util::libplist_to_plist(ptr)) + .map(|ptr| unsafe { &mut **ptr }.borrow_self().clone()) .collect() }; let options = if options.is_null() { None } else { - Some(util::libplist_to_plist(options)) - }; + Some(unsafe { &mut *options }) + } + .map(|x| x.borrow_self().clone()); let res = RUNTIME.block_on(async { unsafe { &mut *client } @@ -553,8 +559,8 @@ pub unsafe extern "C" fn installation_proxy_check_capabilities_match( #[unsafe(no_mangle)] pub unsafe extern "C" fn installation_proxy_browse( client: *mut InstallationProxyClientHandle, - options: *mut c_void, - out_result: *mut *mut c_void, + options: plist_t, + out_result: *mut *mut plist_t, out_result_len: *mut libc::size_t, ) -> *mut IdeviceFfiError { if client.is_null() || out_result.is_null() || out_result_len.is_null() { @@ -564,25 +570,27 @@ pub unsafe extern "C" fn installation_proxy_browse( let options = if options.is_null() { None } else { - Some(util::libplist_to_plist(options)) - }; + Some(unsafe { &mut *options }) + } + .map(|x| x.borrow_self().clone()); - let res: Result, IdeviceError> = RUNTIME.block_on(async { + let res: Result, IdeviceError> = RUNTIME.block_on(async { unsafe { &mut *client }.0.browse(options).await.map(|apps| { apps.into_iter() - .map(|v| util::plist_to_libplist(&v)) + .map(|v| PlistWrapper::new_node(v).into_ptr()) .collect() }) }); match res { - Ok(mut r) => { + Ok(r) => { + let mut r = r.into_boxed_slice(); let ptr = r.as_mut_ptr(); let len = r.len(); std::mem::forget(r); unsafe { - *out_result = ptr as *mut c_void; + *out_result = ptr; *out_result_len = len; } null_mut() diff --git a/ffi/src/lockdown.rs b/ffi/src/lockdown.rs index 02c2cd1..b6470e6 100644 --- a/ffi/src/lockdown.rs +++ b/ffi/src/lockdown.rs @@ -1,8 +1,9 @@ // Jackson Coxson -use std::{ffi::c_void, ptr::null_mut}; +use std::ptr::null_mut; use idevice::{IdeviceError, IdeviceService, lockdown::LockdownClient, provider::IdeviceProvider}; +use plist_ffi::{PlistWrapper, plist_t}; use crate::{ IdeviceFfiError, IdeviceHandle, IdevicePairingFile, RUNTIME, ffi_err, @@ -176,7 +177,7 @@ pub unsafe extern "C" fn lockdownd_get_value( client: *mut LockdowndClientHandle, key: *const libc::c_char, domain: *const libc::c_char, - out_plist: *mut *mut c_void, + out_plist: *mut plist_t, ) -> *mut IdeviceFfiError { if key.is_null() || out_plist.is_null() { return ffi_err!(IdeviceError::FfiInvalidArg); @@ -204,7 +205,7 @@ pub unsafe extern "C" fn lockdownd_get_value( match res { Ok(value) => { unsafe { - *out_plist = crate::util::plist_to_libplist(&value); + *out_plist = plist_ffi::PlistWrapper::new_node(value).into_ptr(); } null_mut() } @@ -228,7 +229,7 @@ pub unsafe extern "C" fn lockdownd_get_value( pub unsafe extern "C" fn lockdownd_get_all_values( client: *mut LockdowndClientHandle, domain: *const libc::c_char, - out_plist: *mut *mut c_void, + out_plist: *mut plist_t, ) -> *mut IdeviceFfiError { if out_plist.is_null() { return ffi_err!(IdeviceError::FfiInvalidArg); @@ -252,7 +253,7 @@ pub unsafe extern "C" fn lockdownd_get_all_values( match res { Ok(dict) => { unsafe { - *out_plist = crate::util::plist_to_libplist(&plist::Value::Dictionary(dict)); + *out_plist = PlistWrapper::new_node(plist::Value::Dictionary(dict)).into_ptr(); } null_mut() } diff --git a/ffi/src/mobile_image_mounter.rs b/ffi/src/mobile_image_mounter.rs index 9a4c797..31ea76e 100644 --- a/ffi/src/mobile_image_mounter.rs +++ b/ffi/src/mobile_image_mounter.rs @@ -6,10 +6,9 @@ use idevice::{ IdeviceError, IdeviceService, mobile_image_mounter::ImageMounter, provider::IdeviceProvider, }; use plist::Value; +use plist_ffi::{PlistWrapper, plist_t}; -use crate::{ - IdeviceFfiError, IdeviceHandle, RUNTIME, ffi_err, provider::IdeviceProviderHandle, util, -}; +use crate::{IdeviceFfiError, IdeviceHandle, RUNTIME, ffi_err, provider::IdeviceProviderHandle}; pub struct ImageMounterHandle(pub ImageMounter); @@ -112,7 +111,7 @@ pub unsafe extern "C" fn image_mounter_free(handle: *mut ImageMounterHandle) { #[unsafe(no_mangle)] pub unsafe extern "C" fn image_mounter_copy_devices( client: *mut ImageMounterHandle, - devices: *mut *mut c_void, + devices: *mut *mut plist_t, devices_len: *mut libc::size_t, ) -> *mut IdeviceFfiError { let res: Result, IdeviceError> = RUNTIME.block_on(async move { @@ -124,14 +123,14 @@ pub unsafe extern "C" fn image_mounter_copy_devices( Ok(devices_list) => { let devices_list = devices_list .into_iter() - .map(|x| util::plist_to_libplist(&x)) - .collect::>(); + .map(|x| plist_ffi::PlistWrapper::new_node(x).into_ptr()) + .collect::>(); let len = devices_list.len(); let boxed_slice = devices_list.into_boxed_slice(); let ptr = Box::leak(boxed_slice).as_mut_ptr(); unsafe { - *devices = ptr as *mut c_void; + *devices = ptr as *mut plist_t; *devices_len = len; } null_mut() @@ -558,7 +557,7 @@ pub unsafe extern "C" fn image_mounter_query_nonce( pub unsafe extern "C" fn image_mounter_query_personalization_identifiers( client: *mut ImageMounterHandle, image_type: *const libc::c_char, - identifiers: *mut *mut c_void, + identifiers: *mut plist_t, ) -> *mut IdeviceFfiError { if identifiers.is_null() { return ffi_err!(IdeviceError::FfiInvalidArg); @@ -583,7 +582,7 @@ pub unsafe extern "C" fn image_mounter_query_personalization_identifiers( match res { Ok(id) => { - let plist = util::plist_to_libplist(&plist::Value::Dictionary(id)); + let plist = PlistWrapper::new_node(Value::Dictionary(id)).into_ptr(); unsafe { *identifiers = plist }; null_mut() } diff --git a/ffi/src/util.rs b/ffi/src/util.rs index 989cf77..5bada5c 100644 --- a/ffi/src/util.rs +++ b/ffi/src/util.rs @@ -3,12 +3,10 @@ use std::{ ffi::c_int, net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, - os::raw::c_void, }; use idevice::IdeviceError; use libc::{sockaddr_in, sockaddr_in6}; -use plist::Value; pub(crate) fn c_socket_to_rust( addr: *const libc::sockaddr, @@ -76,23 +74,3 @@ pub(crate) fn c_addr_to_rust(addr: *const libc::sockaddr) -> Result *mut libc::c_void { - let buf = Vec::new(); - let mut writer = std::io::BufWriter::new(buf); - plist::to_writer_xml(&mut writer, v).unwrap(); - let buf = String::from_utf8(writer.into_inner().unwrap()).unwrap(); - let p = plist_plus::Plist::from_xml(buf).unwrap(); - let ptr = p.get_pointer(); - p.false_drop(); - ptr -} - -pub(crate) fn libplist_to_plist(v: *mut c_void) -> Value { - let v: plist_plus::Plist = v.into(); - let v_string = v.to_string(); - v.false_drop(); - - let reader = std::io::Cursor::new(v_string.as_bytes()); - plist::from_reader(reader).unwrap() -} diff --git a/justfile b/justfile index 701b743..7502428 100644 --- a/justfile +++ b/justfile @@ -66,150 +66,3 @@ apple-build: # requires a Mac cargo build --release --target aarch64-apple-darwin cargo build --release --target x86_64-apple-darwin -lib_name := "plist" -src_dir := "ffi/libplist" - -ios_out := "build/ios" -sim_out := "build/sim" -x86_64_sim_out := "build/x86_64_sim" -mac_out := "build/mac" -x86_64_mac_out := "build/x86_64_mac" - -plist_xcframework: plist_clean build_plist_ios build_plist_sim build_plist_x86_64_sim build_plist_mac build_plist_x86_64_mac plist_merge_archs - rm -rf {{lib_name}}.xcframework - xcodebuild -create-xcframework \ - -framework {{ios_out}}/plist.framework \ - -framework build/universal-sim/plist.framework \ - -framework build/universal-mac/plist.framework \ - -output swift/{{lib_name}}.xcframework - -plist_clean: - rm -rf build - rm -rf swift/plist.xcframework - -plist_merge_archs: - # Merge simulator dylibs (arm64 + x86_64) - mkdir -p build/universal-sim - lipo -create \ - {{sim_out}}/lib/libplist-2.0.4.dylib \ - {{x86_64_sim_out}}/lib/libplist-2.0.4.dylib \ - -output build/universal-sim/libplist-2.0.4.dylib - - mkdir -p build/universal-sim/plist.framework/Headers - mkdir -p build/universal-sim/plist.framework/Modules - cp build/universal-sim/libplist-2.0.4.dylib build/universal-sim/plist.framework/plist - cp {{sim_out}}/include/plist/*.h build/universal-sim/plist.framework/Headers - cp swift/Info.plist build/universal-sim/plist.framework/Info.plist - cp swift/plistinclude/module.modulemap build/universal-sim/plist.framework/Modules/module.modulemap - - # Merge macOS dylibs (arm64 + x86_64) - mkdir -p build/universal-mac - lipo -create \ - {{mac_out}}/lib/libplist-2.0.4.dylib \ - {{x86_64_mac_out}}/lib/libplist-2.0.4.dylib \ - -output build/universal-mac/libplist-2.0.4.dylib - - mkdir -p build/universal-mac/plist.framework/Headers - mkdir -p build/universal-mac/plist.framework/Modules - cp build/universal-mac/libplist-2.0.4.dylib build/universal-mac/plist.framework/plist - cp {{mac_out}}/include/plist/*.h build/universal-mac/plist.framework/Headers - cp swift/Info.plist build/universal-mac/plist.framework/Info.plist - cp swift/plistinclude/module.modulemap build/universal-mac/plist.framework/Modules/module.modulemap - -build_plist_ios: - rm -rf {{ios_out}} build/build-ios - rm -rf build/ios - mkdir -p {{ios_out}} - mkdir -p build/build-ios && cd build/build-ios && \ - ../../ffi/libplist/autogen.sh \ - --host=arm-apple-darwin \ - --prefix="$(pwd)/../../{{ios_out}}" \ - --without-cython \ - --without-tools \ - CC="$(xcrun --sdk iphoneos --find clang)" \ - CFLAGS="-arch arm64 -isysroot $(xcrun --sdk iphoneos --show-sdk-path) -mios-version-min=12.0" \ - CXX="$(xcrun --sdk iphoneos --find clang++)" \ - CXXFLAGS="-arch arm64 -isysroot $(xcrun --sdk iphoneos --show-sdk-path) -mios-version-min=12.0" \ - LDFLAGS="-arch arm64 -isysroot $(xcrun --sdk iphoneos --show-sdk-path) -mios-version-min=12.0" && \ - make clean && make -j$(sysctl -n hw.ncpu) && make install - - install_name_tool -id @rpath/plist.framework/plist {{ios_out}}/lib/libplist-2.0.4.dylib - - mkdir -p {{ios_out}}/plist.framework/Headers - mkdir -p {{ios_out}}/plist.framework/Modules - cp {{ios_out}}/lib/libplist-2.0.4.dylib {{ios_out}}/plist.framework/plist - cp {{ios_out}}/include/plist/*.h {{ios_out}}/plist.framework/Headers - cp swift/Info.plist {{ios_out}}/plist.framework/Info.plist - cp swift/plistinclude/module.modulemap {{ios_out}}/plist.framework/Modules/module.modulemap - -build_plist_sim: - rm -rf {{sim_out}} build/build-sim - mkdir -p {{sim_out}} - mkdir -p build/build-sim && cd build/build-sim && \ - ../../ffi/libplist/autogen.sh \ - --host=arm-apple-darwin \ - --prefix="$(pwd)/../../{{sim_out}}" \ - --without-cython \ - --without-tools \ - CC="$(xcrun --sdk iphonesimulator --find clang)" \ - CFLAGS="-arch arm64 -isysroot $(xcrun --sdk iphonesimulator --show-sdk-path) -mios-simulator-version-min=12.0" \ - CXX="$(xcrun --sdk iphonesimulator --find clang++)" \ - CXXFLAGS="-arch arm64 -isysroot $(xcrun --sdk iphonesimulator --show-sdk-path) -mios-simulator-version-min=12.0" \ - LDFLAGS="-arch arm64 -isysroot $(xcrun --sdk iphonesimulator --show-sdk-path) -mios-simulator-version-min=12.0" && \ - make clean && make -j$(sysctl -n hw.ncpu) && make install - - install_name_tool -id @rpath/plist.framework/plist {{sim_out}}/lib/libplist-2.0.4.dylib - -build_plist_x86_64_sim: - rm -rf {{x86_64_sim_out}} build/build-sim - mkdir -p {{x86_64_sim_out}} - mkdir -p build/build-sim && cd build/build-sim && \ - ../../ffi/libplist/autogen.sh \ - --host=x86_64-apple-darwin \ - --prefix="$(pwd)/../../{{x86_64_sim_out}}" \ - --without-cython \ - --without-tools \ - CC="$(xcrun --sdk iphonesimulator --find clang)" \ - CFLAGS="-arch x86_64 -isysroot $(xcrun --sdk iphonesimulator --show-sdk-path) -mios-simulator-version-min=12.0" \ - CXX="$(xcrun --sdk iphonesimulator --find clang++)" \ - CXXFLAGS="-arch x86_64 -isysroot $(xcrun --sdk iphonesimulator --show-sdk-path) -mios-simulator-version-min=12.0" \ - LDFLAGS="-arch x86_64 -isysroot $(xcrun --sdk iphonesimulator --show-sdk-path) -mios-simulator-version-min=12.0" && \ - make clean && make -j$(sysctl -n hw.ncpu) && make install - - install_name_tool -id @rpath/plist.framework/plist {{x86_64_sim_out}}/lib/libplist-2.0.4.dylib - -build_plist_mac: - rm -rf {{mac_out}} build/build-mac - mkdir -p {{mac_out}} - mkdir -p build/build-mac && cd build/build-mac && \ - ../../ffi/libplist/autogen.sh \ - --host=aarch64-apple-darwin \ - --prefix="$(pwd)/../../{{mac_out}}" \ - --without-cython \ - --without-tools \ - CC="$(xcrun --sdk macosx --find clang)" \ - CFLAGS="-arch arm64 -isysroot $(xcrun --sdk macosx --show-sdk-path) -mmacosx-version-min=11.0" \ - CXX="$(xcrun --sdk macosx --find clang++)" \ - CXXFLAGS="-arch arm64 -isysroot $(xcrun --sdk macosx --show-sdk-path) -mmacosx-version-min=11.0" \ - LDFLAGS="-arch arm64 -isysroot $(xcrun --sdk macosx --show-sdk-path) -mmacosx-version-min=11.0" && \ - make clean && make -j$(sysctl -n hw.ncpu) && make install - - install_name_tool -id @rpath/plist.framework/plist {{mac_out}}/lib/libplist-2.0.4.dylib - -build_plist_x86_64_mac: - rm -rf {{x86_64_mac_out}} build/build-mac - mkdir -p {{x86_64_mac_out}} - mkdir -p build/build-mac && cd build/build-mac && \ - ../../ffi/libplist/autogen.sh \ - --host=x86_64-apple-darwin \ - --prefix="$(pwd)/../../{{x86_64_mac_out}}" \ - --without-cython \ - --without-tools \ - CC="$(xcrun --sdk macosx --find clang)" \ - CFLAGS="-arch x86_64 -isysroot $(xcrun --sdk macosx --show-sdk-path) -mmacosx-version-min=11.0" \ - CXX="$(xcrun --sdk macosx --find clang++)" \ - CXXFLAGS="-arch x86_64 -isysroot $(xcrun --sdk macosx --show-sdk-path) -mmacosx-version-min=11.0" \ - LDFLAGS="-arch x86_64 -isysroot $(xcrun --sdk macosx --show-sdk-path) -mmacosx-version-min=11.0" && \ - make clean && make -j$(sysctl -n hw.ncpu) && make install - - install_name_tool -id @rpath/plist.framework/plist {{x86_64_mac_out}}/lib/libplist-2.0.4.dylib From cb36f510ecd99b2af63700ba6f042aa1097d8233 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Wed, 30 Jul 2025 14:41:57 -0600 Subject: [PATCH 019/126] Remove libplist from cpp examples --- cpp/examples/CMakeLists.txt | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/cpp/examples/CMakeLists.txt b/cpp/examples/CMakeLists.txt index 5ed223c..007c72d 100644 --- a/cpp/examples/CMakeLists.txt +++ b/cpp/examples/CMakeLists.txt @@ -39,29 +39,6 @@ foreach(EXAMPLE_FILE ${EXAMPLE_SOURCES}) # Link the static Rust library target_link_libraries(${EXAMPLE_NAME} PRIVATE ${STATIC_LIB}) - # libplist - - if( APPLE ) - # use static linking - find_library( LIBPLIST libplist-2.0.a REQUIRED ) - message( STATUS "(Static linking) LIBPLIST " ${LIBPLIST} ) - target_link_libraries ( ${EXAMPLE_NAME} PRIVATE ${LIBPLIST} ) - elseif( WIN32) - pkg_search_module(PLIST REQUIRED libplist-2.0) - find_library( LIBPLIST ${PLIST_LIBRARIES} PATH ${PLIST_LIBDIR} ) - target_link_libraries ( ${EXAMPLE_NAME} PRIVATE ${LIBPLIST} ) - else () - pkg_search_module(PLIST libplist>=2.0) - if(NOT PLIST_FOUND) - pkg_search_module(PLIST REQUIRED libplist-2.0) - endif() - find_library( LIBPLIST ${PLIST_LIBRARIES} PATH ${PLIST_LIBDIR} ) - target_link_libraries ( ${EXAMPLE_NAME} PUBLIC ${LIBPLIST} ) - endif() - if ( PLIST_FOUND ) - message( STATUS "found libplist-${PLIST_VERSION}" ) - endif() - target_include_directories( ${EXAMPLE_NAME} PRIVATE ${PLIST_INCLUDE_DIRS} ) # Bulk-link common macOS system frameworks if(APPLE) From 182ec10dc21a44c624d8bc66653b1d676e4c1d12 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Wed, 30 Jul 2025 15:06:58 -0600 Subject: [PATCH 020/126] Cargo clippy fixes --- .github/workflows/ci.yml | 2 ++ cpp/examples/CMakeLists.txt | 3 --- idevice/src/pairing_file.rs | 4 ++-- idevice/src/services/afc/errors.rs | 2 +- idevice/src/services/debug_proxy.rs | 8 ++++---- idevice/src/services/dvt/message.rs | 6 +++--- idevice/src/tunneld.rs | 4 ++-- idevice/src/util.rs | 14 +++++++------- idevice/src/xpc/format.rs | 2 +- justfile | 2 +- 10 files changed, 23 insertions(+), 24 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index affaf6b..612019b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,6 +33,8 @@ jobs: - name: Build all Apple targets and examples/tools run: | + export HOMEBREW_PREFIX=$(brew --prefix) + export CXXFLAGS="-I$HOMEBREW_PREFIX/include" just macos-ci-check - name: Upload static libraries diff --git a/cpp/examples/CMakeLists.txt b/cpp/examples/CMakeLists.txt index 007c72d..63d607d 100644 --- a/cpp/examples/CMakeLists.txt +++ b/cpp/examples/CMakeLists.txt @@ -18,8 +18,6 @@ set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Wpedantic") -find_package(PkgConfig REQUIRED) - # Find all C++ example files file(GLOB EXAMPLE_SOURCES ${EXAMPLES_DIR}/*.cpp) @@ -39,7 +37,6 @@ foreach(EXAMPLE_FILE ${EXAMPLE_SOURCES}) # Link the static Rust library target_link_libraries(${EXAMPLE_NAME} PRIVATE ${STATIC_LIB}) - # Bulk-link common macOS system frameworks if(APPLE) target_link_libraries(${EXAMPLE_NAME} PRIVATE diff --git a/idevice/src/pairing_file.rs b/idevice/src/pairing_file.rs index b4e8dda..7f2b5b2 100644 --- a/idevice/src/pairing_file.rs +++ b/idevice/src/pairing_file.rs @@ -219,7 +219,7 @@ fn ensure_pem_headers(data: &[u8], pem_type: &str) -> Vec { let mut result = Vec::new(); // Add header - let header = format!("-----BEGIN {}-----\n", pem_type); + let header = format!("-----BEGIN {pem_type}-----\n"); result.extend_from_slice(header.as_bytes()); // Add base64 content with line breaks every 64 characters @@ -244,7 +244,7 @@ fn ensure_pem_headers(data: &[u8], pem_type: &str) -> Vec { result.push(b'\n'); // Add footer - let footer = format!("-----END {}-----", pem_type); + let footer = format!("-----END {pem_type}-----"); result.extend_from_slice(footer.as_bytes()); result diff --git a/idevice/src/services/afc/errors.rs b/idevice/src/services/afc/errors.rs index 7aa9fc3..a6c726e 100644 --- a/idevice/src/services/afc/errors.rs +++ b/idevice/src/services/afc/errors.rs @@ -66,7 +66,7 @@ impl std::fmt::Display for AfcError { AfcError::NotEnoughData => "Not enough data", AfcError::DirNotEmpty => "Directory not empty", }; - write!(f, "{}", description) + write!(f, "{description}") } } diff --git a/idevice/src/services/debug_proxy.rs b/idevice/src/services/debug_proxy.rs index 97f230b..8fa2a28 100644 --- a/idevice/src/services/debug_proxy.rs +++ b/idevice/src/services/debug_proxy.rs @@ -107,10 +107,10 @@ impl DebugProxyClient { let checksum = calculate_checksum(&packet_data); // Construct the full packet - let packet = format!("${}#{}", packet_data, checksum); + let packet = format!("${packet_data}#{checksum}"); // Log the packet for debugging - debug!("Sending packet: {}", packet); + debug!("Sending packet: {packet}"); // Send the packet self.socket.write_all(packet.as_bytes()).await?; @@ -237,7 +237,7 @@ impl DebugProxyClient { // Hex encode the argument for byte in arg.bytes() { - let hex = format!("{:02X}", byte); + let hex = format!("{byte:02X}"); pkt[pktp..pktp + 2].copy_from_slice(hex.as_bytes()); pktp += 2; } @@ -290,7 +290,7 @@ impl DebugProxyClient { /// between '$' and '#', formatted as two lowercase hex digits. fn calculate_checksum(data: &str) -> String { let checksum = data.bytes().fold(0u8, |acc, byte| acc.wrapping_add(byte)); - format!("{:02x}", checksum) + format!("{checksum:02x}") } /// Hex-encodes bytes as uppercase string diff --git a/idevice/src/services/dvt/message.rs b/idevice/src/services/dvt/message.rs index 156d352..006fad7 100644 --- a/idevice/src/services/dvt/message.rs +++ b/idevice/src/services/dvt/message.rs @@ -501,15 +501,15 @@ impl Message { impl std::fmt::Debug for AuxValue { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - AuxValue::String(s) => write!(f, "String({:?})", s), + AuxValue::String(s) => write!(f, "String({s:?})"), AuxValue::Array(arr) => write!( f, "Array(len={}, first_bytes={:?})", arr.len(), &arr[..arr.len().min(10)] ), // Show only first 10 bytes - AuxValue::U32(n) => write!(f, "U32({})", n), - AuxValue::I64(n) => write!(f, "I64({})", n), + AuxValue::U32(n) => write!(f, "U32({n})"), + AuxValue::I64(n) => write!(f, "I64({n})"), } } } diff --git a/idevice/src/tunneld.rs b/idevice/src/tunneld.rs index 2c4acbd..dcbb8de 100644 --- a/idevice/src/tunneld.rs +++ b/idevice/src/tunneld.rs @@ -101,8 +101,8 @@ mod tests { async fn test_get_tunneld_devices() { let host = SocketAddr::new(IpAddr::from_str("127.0.0.1").unwrap(), DEFAULT_PORT); match get_tunneld_devices(host).await { - Ok(devices) => println!("Found tunneld devices: {:#?}", devices), - Err(e) => println!("Error querying tunneld: {}", e), + Ok(devices) => println!("Found tunneld devices: {devices:#?}"), + Err(e) => println!("Error querying tunneld: {e}"), } } } diff --git a/idevice/src/util.rs b/idevice/src/util.rs index 1b7ee06..4e69565 100644 --- a/idevice/src/util.rs +++ b/idevice/src/util.rs @@ -103,25 +103,25 @@ fn print_plist(p: &Value, indentation: usize) -> String { .collect(); format!("{{\n{}\n{}}}", items.join(",\n"), indent) } - Value::Boolean(b) => format!("{}", b), + Value::Boolean(b) => format!("{b}"), Value::Data(vec) => { let len = vec.len(); let preview: String = vec .iter() .take(20) - .map(|b| format!("{:02X}", b)) + .map(|b| format!("{b:02X}")) .collect::>() .join(" "); if len > 20 { - format!("Data({}... Len: {})", preview, len) + format!("Data({preview}... Len: {len})") } else { - format!("Data({} Len: {})", preview, len) + format!("Data({preview} Len: {len})") } } Value::Date(date) => format!("Date({})", date.to_xml_format()), - Value::Real(f) => format!("{}", f), - Value::Integer(i) => format!("{}", i), - Value::String(s) => format!("\"{}\"", s), + Value::Real(f) => format!("{f}"), + Value::Integer(i) => format!("{i}"), + Value::String(s) => format!("\"{s}\""), Value::Uid(_uid) => "Uid(?)".to_string(), _ => "Unknown".to_string(), } diff --git a/idevice/src/xpc/format.rs b/idevice/src/xpc/format.rs index 2c73029..2bfbc6a 100644 --- a/idevice/src/xpc/format.rs +++ b/idevice/src/xpc/format.rs @@ -529,7 +529,7 @@ impl std::fmt::Debug for XPCMessage { let known_mask = 0x00000001 | 0x00000100 | 0x00010000 | 0x00400000; let custom_bits = self.flags & !known_mask; if custom_bits != 0 { - parts.push(format!("Custom(0x{:08X})", custom_bits)); + parts.push(format!("Custom(0x{custom_bits:08X})")); } write!( diff --git a/justfile b/justfile index 7502428..f339e34 100644 --- a/justfile +++ b/justfile @@ -45,7 +45,7 @@ xcframework: apple-build -library swift/libs/idevice-macos.a -headers swift/include \ -output swift/IDevice.xcframework - zip -r bundle.zip IDevice.xcframework + zip -r bundle.zip swift/IDevice.xcframework openssl dgst -sha256 bundle.zip [working-directory: 'ffi'] From 329a3612bceaeff88ccba51b4749b14f62cbbfdd Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Wed, 30 Jul 2025 15:08:54 -0600 Subject: [PATCH 021/126] Use brew to install just --- .github/workflows/ci.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 612019b..4b267f9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,8 +14,7 @@ jobs: - name: Install just run: | - curl -sL https://just.systems/install.sh | bash -s -- --to ~/.cargo/bin - echo "$HOME/.cargo/bin" >> $GITHUB_PATH + brew install just - name: Install libplist run: | From eed0090ec72bc779e6e9eb4f28b08d0c6bf69aca Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Wed, 30 Jul 2025 15:16:11 -0600 Subject: [PATCH 022/126] Apt install libplist for Linux CI --- .github/workflows/ci.yml | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4b267f9..015b0c4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,8 +32,6 @@ jobs: - name: Build all Apple targets and examples/tools run: | - export HOMEBREW_PREFIX=$(brew --prefix) - export CXXFLAGS="-I$HOMEBREW_PREFIX/include" just macos-ci-check - name: Upload static libraries @@ -74,7 +72,7 @@ jobs: - uses: actions/checkout@v4 - name: Install build dependencies - run: sudo apt-get update && sudo apt-get install -y build-essential cmake + run: sudo apt-get update && sudo apt-get install -y build-essential cmake libplist - name: Install just run: | @@ -125,10 +123,10 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Install just - run: | - iex "& {$(irm get.scoop.sh)} -RunAsAdmin" - scoop install just + - uses: MinoruSekine/setup-scoop@v4.0.2 + with: + buckets: extras + apps: doxygen plantuml - name: Cache Cargo uses: actions/cache@v4 From 4a34336208f38acc3f3285812ac74d23a27a7a88 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Wed, 30 Jul 2025 15:21:37 -0600 Subject: [PATCH 023/126] Curl plist.h --- .github/workflows/ci.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 015b0c4..fb66914 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,9 @@ jobs: - name: Install libplist run: | - brew install libplist + brew install libplist && \ + mkdir -p /usr/local/include/plist && \ + wget -O /usr/local/include/plist/plist.h https://raw.githubusercontent.com/libimobiledevice/libplist/refs/heads/master/include/plist/plist.h - name: Cache Cargo uses: actions/cache@v4 @@ -72,7 +74,10 @@ jobs: - uses: actions/checkout@v4 - name: Install build dependencies - run: sudo apt-get update && sudo apt-get install -y build-essential cmake libplist + run: | + sudo apt-get update && sudo apt-get install -y build-essential cmake libplist && \ + mkdir -p /usr/local/include/plist && \ + wget -O /usr/local/include/plist/plist.h https://raw.githubusercontent.com/libimobiledevice/libplist/refs/heads/master/include/plist/plist.h - name: Install just run: | From 0938c143a55fa402147fb7e4922ed3da9e4338c3 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Wed, 30 Jul 2025 15:25:18 -0600 Subject: [PATCH 024/126] Don't curl plist.h --- .github/workflows/ci.yml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fb66914..564a62c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,9 +18,7 @@ jobs: - name: Install libplist run: | - brew install libplist && \ - mkdir -p /usr/local/include/plist && \ - wget -O /usr/local/include/plist/plist.h https://raw.githubusercontent.com/libimobiledevice/libplist/refs/heads/master/include/plist/plist.h + brew install libplist - name: Cache Cargo uses: actions/cache@v4 @@ -75,9 +73,7 @@ jobs: - name: Install build dependencies run: | - sudo apt-get update && sudo apt-get install -y build-essential cmake libplist && \ - mkdir -p /usr/local/include/plist && \ - wget -O /usr/local/include/plist/plist.h https://raw.githubusercontent.com/libimobiledevice/libplist/refs/heads/master/include/plist/plist.h + sudo apt-get update && sudo apt-get install -y build-essential cmake libplist - name: Install just run: | From 9f2de4d34025a9775ba2e1d709e06bcc86996a79 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Wed, 30 Jul 2025 15:47:54 -0600 Subject: [PATCH 025/126] Partial diagnostics relay implementation --- Cargo.lock | 2 +- idevice/Cargo.toml | 4 +- idevice/src/services/diagnostics_relay.rs | 113 ++++++++++++++++++++++ idevice/src/services/mod.rs | 2 + tools/Cargo.toml | 4 + 5 files changed, 123 insertions(+), 2 deletions(-) create mode 100644 idevice/src/services/diagnostics_relay.rs diff --git a/Cargo.lock b/Cargo.lock index 5373abc..48fe7b8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1086,7 +1086,7 @@ dependencies = [ [[package]] name = "idevice" -version = "0.1.36" +version = "0.1.37" dependencies = [ "base64", "byteorder", diff --git a/idevice/Cargo.toml b/idevice/Cargo.toml index c7e0b14..ee7f8db 100644 --- a/idevice/Cargo.toml +++ b/idevice/Cargo.toml @@ -2,7 +2,7 @@ name = "idevice" description = "A Rust library to interact with services on iOS devices." authors = ["Jackson Coxson"] -version = "0.1.36" +version = "0.1.37" edition = "2021" license = "MIT" documentation = "https://docs.rs/idevice" @@ -62,6 +62,7 @@ core_device = ["xpc", "dep:uuid"] core_device_proxy = ["dep:serde_json", "dep:json", "dep:byteorder"] crashreportcopymobile = ["afc"] debug_proxy = [] +diagnostics_relay = [] dvt = ["dep:byteorder", "dep:ns-keyed-archive"] heartbeat = ["tokio/macros", "tokio/time"] house_arrest = ["afc"] @@ -88,6 +89,7 @@ full = [ "core_device_proxy", "crashreportcopymobile", "debug_proxy", + "diagnostics_relay", "dvt", "heartbeat", "house_arrest", diff --git a/idevice/src/services/diagnostics_relay.rs b/idevice/src/services/diagnostics_relay.rs new file mode 100644 index 0000000..bb5a545 --- /dev/null +++ b/idevice/src/services/diagnostics_relay.rs @@ -0,0 +1,113 @@ +//! Diagnostics Relay + +use crate::{lockdown::LockdownClient, obf, Idevice, IdeviceError, IdeviceService}; + +/// Client for interacting with the Diagnostics Relay +pub struct DiagnosticsRelayClient { + /// The underlying device connection with established service + pub idevice: Idevice, +} + +impl IdeviceService for DiagnosticsRelayClient { + /// Returns the service name as registered with lockdownd + fn service_name() -> std::borrow::Cow<'static, str> { + obf!("com.apple.mobile.diagnostics_relay") + } + + /// Establishes a connection to the service + /// + /// # Arguments + /// * `provider` - Device connection provider + /// + /// # Returns + /// A connected `DiagnosticsRelayClient` instance + /// + /// # Errors + /// Returns `IdeviceError` if any step of the connection process fails + /// + /// # Process + /// 1. Connects to lockdownd service + /// 2. Starts a lockdown session + /// 3. Requests the service port + /// 4. Establishes connection to the port + /// 5. Optionally starts TLS if required by service + async fn connect( + provider: &dyn crate::provider::IdeviceProvider, + ) -> Result { + let mut lockdown = LockdownClient::connect(provider).await?; + lockdown + .start_session(&provider.get_pairing_file().await?) + .await?; + + let (port, ssl) = lockdown.start_service(Self::service_name()).await?; + + let mut idevice = provider.connect(port).await?; + if ssl { + idevice + .start_session(&provider.get_pairing_file().await?) + .await?; + } + + Ok(Self { idevice }) + } +} + +impl DiagnosticsRelayClient { + /// Creates a new client from an existing device connection + /// + /// # Arguments + /// * `idevice` - Pre-established device connection + pub fn new(idevice: Idevice) -> Self { + Self { idevice } + } + + /// Requests data from the IO registry + /// + /// # Arguments + /// * `current_plane` - The plane to request the tree as + /// * `entry_name` - The entry to get + /// * `entry_class` - The class to filter by + /// + /// # Returns + /// A plist of the tree on success + pub async fn ioregistry( + &mut self, + current_plane: Option>, + entry_name: Option>, + entry_class: Option>, + ) -> Result, IdeviceError> { + let mut req = plist::Dictionary::new(); + if let Some(plane) = current_plane { + let plane = plane.into(); + req.insert("CurrentPlane".into(), plane.into()); + } + if let Some(name) = entry_name { + let name = name.into(); + req.insert("EntryName".into(), name.into()); + } + if let Some(class) = entry_class { + let class = class.into(); + req.insert("EntryClass".into(), class.into()); + } + req.insert("Request".into(), "IORegistry".into()); + self.idevice + .send_plist(plist::Value::Dictionary(req)) + .await?; + let mut res = self.idevice.read_plist().await?; + + match res.get("Status").and_then(|x| x.as_string()) { + Some("Success") => {} + _ => { + return Err(IdeviceError::UnexpectedResponse); + } + } + + let res = res + .remove("Diagnostics") + .and_then(|x| x.into_dictionary()) + .and_then(|mut x| x.remove("IORegistry")) + .and_then(|x| x.into_dictionary()); + + Ok(res) + } +} diff --git a/idevice/src/services/mod.rs b/idevice/src/services/mod.rs index dea53bf..17bdcb2 100644 --- a/idevice/src/services/mod.rs +++ b/idevice/src/services/mod.rs @@ -10,6 +10,8 @@ pub mod core_device_proxy; pub mod crashreportcopymobile; #[cfg(feature = "debug_proxy")] pub mod debug_proxy; +#[cfg(feature = "diagnostics_relay")] +pub mod diagnostics_relay; #[cfg(feature = "dvt")] pub mod dvt; #[cfg(feature = "heartbeat")] diff --git a/tools/Cargo.toml b/tools/Cargo.toml index 8526f46..85a225f 100644 --- a/tools/Cargo.toml +++ b/tools/Cargo.toml @@ -93,6 +93,10 @@ path = "src/lockdown.rs" name = "restore_service" path = "src/restore_service.rs" +[[bin]] +name = "ioreg" +path = "src/ioreg.rs" + [dependencies] idevice = { path = "../idevice", features = ["full"] } tokio = { version = "1.43", features = ["io-util", "macros", "time", "full"] } From 8ee58c350210fe258489d1622183213e05e80bdb Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Thu, 31 Jul 2025 11:14:42 -0600 Subject: [PATCH 026/126] Add ring as optional crypto provider --- idevice/Cargo.toml | 6 +++++- idevice/src/lib.rs | 13 ++++++++++--- tools/Cargo.toml | 6 +----- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/idevice/Cargo.toml b/idevice/Cargo.toml index ee7f8db..5902779 100644 --- a/idevice/Cargo.toml +++ b/idevice/Cargo.toml @@ -13,7 +13,7 @@ keywords = ["lockdownd", "ios"] [dependencies] tokio = { version = "1.43", features = ["io-util"] } tokio-rustls = "0.26" -rustls = "0.23" +rustls = { version = "0.23", default-features = false } plist = { version = "1.7" } serde = { version = "1", features = ["derive"] } @@ -56,6 +56,10 @@ tun-rs = { version = "2.0.8", features = ["async_tokio"] } bytes = "1.10.1" [features] +default = ["aws-lc"] +aws-lc = ["rustls/aws-lc-rs"] +ring = ["rustls/ring"] + afc = ["dep:chrono"] amfi = [] core_device = ["xpc", "dep:uuid"] diff --git a/idevice/src/lib.rs b/idevice/src/lib.rs index 749a31f..5d02877 100644 --- a/idevice/src/lib.rs +++ b/idevice/src/lib.rs @@ -390,9 +390,16 @@ impl Idevice { pairing_file: &pairing_file::PairingFile, ) -> Result<(), IdeviceError> { if CryptoProvider::get_default().is_none() { - if let Err(e) = - CryptoProvider::install_default(rustls::crypto::aws_lc_rs::default_provider()) - { + let crypto_provider = if cfg!(feature = "ring") { + debug!("Using ring crypto backend"); + rustls::crypto::ring::default_provider() + } else if cfg!(feature = "aws-lc") { + debug!("Using aws-lc crypto backend"); + rustls::crypto::aws_lc_rs::default_provider() + } else { + panic!("No crypto provider compiled in! Use one of the features for idevice to specify a provider"); + }; + if let Err(e) = CryptoProvider::install_default(crypto_provider) { // For whatever reason, getting the default provider will return None on iOS at // random. Installing the default provider a second time will return an error, so // we will log it but not propogate it. An issue should be opened with rustls. diff --git a/tools/Cargo.toml b/tools/Cargo.toml index 85a225f..24f4b20 100644 --- a/tools/Cargo.toml +++ b/tools/Cargo.toml @@ -93,13 +93,9 @@ path = "src/lockdown.rs" name = "restore_service" path = "src/restore_service.rs" -[[bin]] -name = "ioreg" -path = "src/ioreg.rs" - [dependencies] idevice = { path = "../idevice", features = ["full"] } -tokio = { version = "1.43", features = ["io-util", "macros", "time", "full"] } +tokio = { version = "1.43", features = ["full"] } log = { version = "0.4" } env_logger = { version = "0.11" } tun-rs = { version = "1.5", features = ["async"] } From 8549a82b5568231294d733773c5141bfdf57c80b Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Thu, 31 Jul 2025 11:35:06 -0600 Subject: [PATCH 027/126] Remove tokio rustls default features --- Cargo.lock | 126 +-------------------------------------------- idevice/Cargo.toml | 7 ++- 2 files changed, 5 insertions(+), 128 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 48fe7b8..b059b5b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -834,11 +834,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", - "js-sys", "libc", "r-efi", "wasi 0.14.2+wasi-0.2.4", - "wasm-bindgen", ] [[package]] @@ -933,23 +931,6 @@ dependencies = [ "want", ] -[[package]] -name = "hyper-rustls" -version = "0.27.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" -dependencies = [ - "http", - "hyper", - "hyper-util", - "rustls", - "rustls-pki-types", - "tokio", - "tokio-rustls", - "tower-service", - "webpki-roots 1.0.2", -] - [[package]] name = "hyper-util" version = "0.1.16" @@ -968,7 +949,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.0", + "socket2", "tokio", "tower-service", "tracing", @@ -1357,12 +1338,6 @@ version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" -[[package]] -name = "lru-slab" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" - [[package]] name = "mac_address" version = "1.1.8" @@ -1804,61 +1779,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "quinn" -version = "0.11.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8" -dependencies = [ - "bytes", - "cfg_aliases", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash 2.1.1", - "rustls", - "socket2 0.5.10", - "thiserror 2.0.12", - "tokio", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-proto" -version = "0.11.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e" -dependencies = [ - "bytes", - "getrandom 0.3.3", - "lru-slab", - "rand 0.9.2", - "ring", - "rustc-hash 2.1.1", - "rustls", - "rustls-pki-types", - "slab", - "thiserror 2.0.12", - "tinyvec", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-udp" -version = "0.5.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcebb1209ee276352ef14ff8732e24cc2b02bbac986cd74a4c81bcb2f9881970" -dependencies = [ - "cfg_aliases", - "libc", - "once_cell", - "socket2 0.5.10", - "tracing", - "windows-sys 0.59.0", -] - [[package]] name = "quote" version = "1.0.40" @@ -1983,21 +1903,16 @@ dependencies = [ "http-body", "http-body-util", "hyper", - "hyper-rustls", "hyper-util", "js-sys", "log", "percent-encoding", "pin-project-lite", - "quinn", - "rustls", - "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", "tokio", - "tokio-rustls", "tower", "tower-http", "tower-service", @@ -2005,7 +1920,6 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots 1.0.2", ] [[package]] @@ -2133,7 +2047,6 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" dependencies = [ - "web-time", "zeroize", ] @@ -2290,16 +2203,6 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" -[[package]] -name = "socket2" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - [[package]] name = "socket2" version = "0.6.0" @@ -2494,21 +2397,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "tinyvec" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - [[package]] name = "tls_codec" version = "0.4.2" @@ -2545,7 +2433,7 @@ dependencies = [ "pin-project-lite", "signal-hook-registry", "slab", - "socket2 0.6.0", + "socket2", "tokio-macros", "windows-sys 0.59.0", ] @@ -2937,16 +2825,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "web-time" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - [[package]] name = "webpki-roots" version = "0.26.11" diff --git a/idevice/Cargo.toml b/idevice/Cargo.toml index 5902779..e28e7ef 100644 --- a/idevice/Cargo.toml +++ b/idevice/Cargo.toml @@ -12,7 +12,7 @@ keywords = ["lockdownd", "ios"] [dependencies] tokio = { version = "1.43", features = ["io-util"] } -tokio-rustls = "0.26" +tokio-rustls = { version = "0.26", default-features = false } rustls = { version = "0.23", default-features = false } plist = { version = "1.7" } @@ -35,7 +35,6 @@ bytes = { version = "1.10", optional = true } reqwest = { version = "0.12", features = [ "json", - "rustls-tls", ], optional = true, default-features = false } rand = { version = "0.9", optional = true } futures = { version = "0.3", optional = true } @@ -57,8 +56,8 @@ bytes = "1.10.1" [features] default = ["aws-lc"] -aws-lc = ["rustls/aws-lc-rs"] -ring = ["rustls/ring"] +aws-lc = ["rustls/aws-lc-rs", "tokio-rustls/aws-lc-rs"] +ring = ["rustls/ring", "tokio-rustls/ring"] afc = ["dep:chrono"] amfi = [] From 1515b1bab40cbf5e6a408594a51e90ba0e5cb61c Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Thu, 31 Jul 2025 11:52:42 -0600 Subject: [PATCH 028/126] Conditionally compile crypto backend --- ffi/Cargo.toml | 8 ++++++-- idevice/src/lib.rs | 35 +++++++++++++++++++++++++++-------- tools/Cargo.toml | 7 ++++++- 3 files changed, 39 insertions(+), 11 deletions(-) diff --git a/ffi/Cargo.toml b/ffi/Cargo.toml index 04878d8..29756ab 100644 --- a/ffi/Cargo.toml +++ b/ffi/Cargo.toml @@ -5,7 +5,7 @@ edition = "2024" [dependencies] -idevice = { path = "../idevice" } +idevice = { path = "../idevice", default-features = false } log = "0.4.26" simplelog = "0.12.2" once_cell = "1.21.1" @@ -15,6 +15,10 @@ plist = "1.7.1" plist_ffi = "0.1.3" [features] +aws-lc = ["idevice/aws-lc"] +ring = ["idevice/ring"] + + afc = ["idevice/afc"] amfi = ["idevice/amfi"] core_device = ["idevice/core_device"] @@ -64,7 +68,7 @@ full = [ "springboardservices", "syslog_relay", ] -default = ["full"] +default = ["full", "aws-lc"] [build-dependencies] cbindgen = "0.29.0" diff --git a/idevice/src/lib.rs b/idevice/src/lib.rs index 5d02877..162cb63 100644 --- a/idevice/src/lib.rs +++ b/idevice/src/lib.rs @@ -390,15 +390,34 @@ impl Idevice { pairing_file: &pairing_file::PairingFile, ) -> Result<(), IdeviceError> { if CryptoProvider::get_default().is_none() { - let crypto_provider = if cfg!(feature = "ring") { - debug!("Using ring crypto backend"); - rustls::crypto::ring::default_provider() - } else if cfg!(feature = "aws-lc") { - debug!("Using aws-lc crypto backend"); - rustls::crypto::aws_lc_rs::default_provider() - } else { - panic!("No crypto provider compiled in! Use one of the features for idevice to specify a provider"); + let crypto_provider = { + #[cfg(all(feature = "ring", not(feature = "aws-lc")))] + { + debug!("Using ring crypto backend"); + rustls::crypto::ring::default_provider() + } + + #[cfg(all(feature = "aws-lc", not(feature = "ring")))] + { + debug!("Using aws-lc crypto backend"); + rustls::crypto::aws_lc_rs::default_provider() + } + + #[cfg(not(any(feature = "ring", feature = "aws-lc")))] + { + panic!( + "No crypto backend was selected! Specify an idevice feature for a crypto backend" + ); + } + + #[cfg(all(feature = "ring", feature = "aws-lc"))] + { + compile_error!( + "Cannot enable both `ring` and `aws-lc` features at the same time" + ); + } }; + if let Err(e) = CryptoProvider::install_default(crypto_provider) { // For whatever reason, getting the default provider will return None on iOS at // random. Installing the default provider a second time will return an error, so diff --git a/tools/Cargo.toml b/tools/Cargo.toml index 24f4b20..049c48a 100644 --- a/tools/Cargo.toml +++ b/tools/Cargo.toml @@ -94,7 +94,7 @@ name = "restore_service" path = "src/restore_service.rs" [dependencies] -idevice = { path = "../idevice", features = ["full"] } +idevice = { path = "../idevice", features = ["full"], default-features = false } tokio = { version = "1.43", features = ["full"] } log = { version = "0.4" } env_logger = { version = "0.11" } @@ -105,3 +105,8 @@ clap = { version = "4.5" } plist = { version = "1.7" } ns-keyed-archive = "0.1.2" uuid = "1.16" + +[features] +default = ["aws-lc"] +aws-lc = ["idevice/aws-lc"] +ring = ["idevice/ring"] From 5ae51d4fbb70bf3bd69ae59dcb8e6e628e15afc8 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Thu, 31 Jul 2025 12:09:54 -0600 Subject: [PATCH 029/126] Clean up rust analyzer for crypto provider block --- idevice/src/lib.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/idevice/src/lib.rs b/idevice/src/lib.rs index 162cb63..23affd7 100644 --- a/idevice/src/lib.rs +++ b/idevice/src/lib.rs @@ -390,7 +390,8 @@ impl Idevice { pairing_file: &pairing_file::PairingFile, ) -> Result<(), IdeviceError> { if CryptoProvider::get_default().is_none() { - let crypto_provider = { + // rust-analyzer will choke on this block, don't worry about it + let crypto_provider: CryptoProvider = { #[cfg(all(feature = "ring", not(feature = "aws-lc")))] { debug!("Using ring crypto backend"); @@ -405,7 +406,7 @@ impl Idevice { #[cfg(not(any(feature = "ring", feature = "aws-lc")))] { - panic!( + compile_error!( "No crypto backend was selected! Specify an idevice feature for a crypto backend" ); } From 21584f4190c86eb3fb40bb16399aee6bb711fa71 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Wed, 6 Aug 2025 15:40:21 -0600 Subject: [PATCH 030/126] Add a method to extract the socket from the device --- idevice/src/lib.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/idevice/src/lib.rs b/idevice/src/lib.rs index 23affd7..76a3034 100644 --- a/idevice/src/lib.rs +++ b/idevice/src/lib.rs @@ -116,6 +116,10 @@ impl Idevice { } } + pub fn get_socket(self) -> Option> { + self.socket + } + /// Queries the device type /// /// Sends a QueryType request and parses the response From d59f0282514d672322e83d2f3d7e000ddac46b6f Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Fri, 8 Aug 2025 10:18:31 -0600 Subject: [PATCH 031/126] Use option<&str> instead of owned option string --- ffi/src/installation_proxy.rs | 17 +++-- ffi/src/lockdown.rs | 74 +++++--------------- ffi/src/mobile_image_mounter.rs | 4 +- idevice/src/services/installation_proxy.rs | 4 +- idevice/src/services/lockdown.rs | 53 +++----------- idevice/src/services/mobile_image_mounter.rs | 11 ++- tools/src/ideviceinfo.rs | 6 +- tools/src/instproxy.rs | 5 +- tools/src/lockdown.rs | 30 +++----- tools/src/mounter.rs | 36 +++++----- 10 files changed, 76 insertions(+), 164 deletions(-) diff --git a/ffi/src/installation_proxy.rs b/ffi/src/installation_proxy.rs index 6bdca82..a749ea4 100644 --- a/ffi/src/installation_proxy.rs +++ b/ffi/src/installation_proxy.rs @@ -109,11 +109,14 @@ pub unsafe extern "C" fn installation_proxy_get_apps( let app_type = if application_type.is_null() { None } else { - Some(unsafe { - std::ffi::CStr::from_ptr(application_type) - .to_string_lossy() - .into_owned() - }) + Some( + match unsafe { std::ffi::CStr::from_ptr(application_type) }.to_str() { + Ok(a) => a, + Err(_) => { + return ffi_err!(IdeviceError::InvalidCString); + } + }, + ) }; let bundle_ids = if bundle_identifiers.is_null() { @@ -125,9 +128,9 @@ pub unsafe extern "C" fn installation_proxy_get_apps( .map(|&s| { unsafe { std::ffi::CStr::from_ptr(s) } .to_string_lossy() - .into_owned() + .to_string() }) - .collect(), + .collect::>(), ) }; diff --git a/ffi/src/lockdown.rs b/ffi/src/lockdown.rs index b6470e6..81b0580 100644 --- a/ffi/src/lockdown.rs +++ b/ffi/src/lockdown.rs @@ -3,7 +3,7 @@ use std::ptr::null_mut; use idevice::{IdeviceError, IdeviceService, lockdown::LockdownClient, provider::IdeviceProvider}; -use plist_ffi::{PlistWrapper, plist_t}; +use plist_ffi::plist_t; use crate::{ IdeviceFfiError, IdeviceHandle, IdevicePairingFile, RUNTIME, ffi_err, @@ -183,18 +183,26 @@ pub unsafe extern "C" fn lockdownd_get_value( return ffi_err!(IdeviceError::FfiInvalidArg); } - let value = unsafe { std::ffi::CStr::from_ptr(key) } - .to_string_lossy() - .into_owned(); + let value = if key.is_null() { + None + } else { + Some(match unsafe { std::ffi::CStr::from_ptr(key) }.to_str() { + Ok(v) => v, + Err(_) => { + return ffi_err!(IdeviceError::InvalidCString); + } + }) + }; let domain = if domain.is_null() { None } else { - Some( - unsafe { std::ffi::CStr::from_ptr(domain) } - .to_string_lossy() - .into_owned(), - ) + Some(match unsafe { std::ffi::CStr::from_ptr(domain) }.to_str() { + Ok(v) => v, + Err(_) => { + return ffi_err!(IdeviceError::InvalidCString); + } + }) }; let res: Result = RUNTIME.block_on(async move { @@ -213,54 +221,6 @@ pub unsafe extern "C" fn lockdownd_get_value( } } -/// Gets all values from lockdownd -/// -/// # Arguments -/// * `client` - A valid LockdowndClient handle -/// * `out_plist` - Pointer to store the returned plist dictionary -/// -/// # Returns -/// An IdeviceFfiError on error, null on success -/// -/// # Safety -/// `client` must be a valid pointer to a handle allocated by this library -/// `out_plist` must be a valid pointer to store the plist -#[unsafe(no_mangle)] -pub unsafe extern "C" fn lockdownd_get_all_values( - client: *mut LockdowndClientHandle, - domain: *const libc::c_char, - out_plist: *mut plist_t, -) -> *mut IdeviceFfiError { - if out_plist.is_null() { - return ffi_err!(IdeviceError::FfiInvalidArg); - } - - let domain = if domain.is_null() { - None - } else { - Some( - unsafe { std::ffi::CStr::from_ptr(domain) } - .to_string_lossy() - .into_owned(), - ) - }; - - let res: Result = RUNTIME.block_on(async move { - let client_ref = unsafe { &mut (*client).0 }; - client_ref.get_all_values(domain).await - }); - - match res { - Ok(dict) => { - unsafe { - *out_plist = PlistWrapper::new_node(plist::Value::Dictionary(dict)).into_ptr(); - } - null_mut() - } - Err(e) => ffi_err!(e), - } -} - /// Frees a LockdowndClient handle /// /// # Arguments diff --git a/ffi/src/mobile_image_mounter.rs b/ffi/src/mobile_image_mounter.rs index 31ea76e..a539509 100644 --- a/ffi/src/mobile_image_mounter.rs +++ b/ffi/src/mobile_image_mounter.rs @@ -514,7 +514,7 @@ pub unsafe extern "C" fn image_mounter_query_nonce( let image_type = if !personalized_image_type.is_null() { let image_type_cstr = unsafe { std::ffi::CStr::from_ptr(personalized_image_type) }; match image_type_cstr.to_str() { - Ok(s) => Some(s.to_string()), + Ok(s) => Some(s), Err(_) => return ffi_err!(IdeviceError::FfiInvalidArg), } } else { @@ -566,7 +566,7 @@ pub unsafe extern "C" fn image_mounter_query_personalization_identifiers( let image_type = if !image_type.is_null() { let image_type_cstr = unsafe { std::ffi::CStr::from_ptr(image_type) }; match image_type_cstr.to_str() { - Ok(s) => Some(s.to_string()), + Ok(s) => Some(s), Err(_) => return ffi_err!(IdeviceError::FfiInvalidArg), } } else { diff --git a/idevice/src/services/installation_proxy.rs b/idevice/src/services/installation_proxy.rs index f338d5d..3b4014f 100644 --- a/idevice/src/services/installation_proxy.rs +++ b/idevice/src/services/installation_proxy.rs @@ -98,10 +98,10 @@ impl InstallationProxyClient { /// ``` pub async fn get_apps( &mut self, - application_type: Option, + application_type: Option<&str>, bundle_identifiers: Option>, ) -> Result, IdeviceError> { - let application_type = application_type.unwrap_or("Any".to_string()); + let application_type = application_type.unwrap_or("Any"); let mut options = plist::Dictionary::new(); if let Some(ids) = bundle_identifiers { let ids = ids diff --git a/idevice/src/services/lockdown.rs b/idevice/src/services/lockdown.rs index 46a3589..2c1627d 100644 --- a/idevice/src/services/lockdown.rs +++ b/idevice/src/services/lockdown.rs @@ -86,16 +86,16 @@ impl LockdownClient { /// ``` pub async fn get_value( &mut self, - key: impl Into, - domain: Option, + key: Option<&str>, + domain: Option<&str>, ) -> Result { - let key = key.into(); - let mut request = plist::Dictionary::new(); request.insert("Label".into(), self.idevice.label.clone().into()); request.insert("Request".into(), "GetValue".into()); - request.insert("Key".into(), key.into()); + if let Some(key) = key { + request.insert("Key".into(), key.into()); + } if let Some(domain) = domain { request.insert("Domain".into(), domain.into()); } @@ -110,43 +110,6 @@ impl LockdownClient { } } - /// Retrieves all available values from the device - /// - /// # Returns - /// A dictionary containing all device values - /// - /// # Errors - /// Returns `IdeviceError` if: - /// - Communication fails - /// - The response is malformed - /// - /// # Example - /// ```rust - /// let all_values = client.get_all_values().await?; - /// for (key, value) in all_values { - /// println!("{}: {:?}", key, value); - /// } - /// ``` - pub async fn get_all_values( - &mut self, - domain: Option, - ) -> Result { - let mut request = plist::Dictionary::new(); - request.insert("Label".into(), self.idevice.label.clone().into()); - request.insert("Request".into(), "GetValue".into()); - if let Some(domain) = domain { - request.insert("Domain".into(), domain.into()); - } - - let message = plist::to_value(&request)?; - self.idevice.send_plist(message).await?; - let message: plist::Dictionary = self.idevice.read_plist().await?; - match message.get("Value") { - Some(m) => Ok(plist::from_value(m)?), - None => Err(IdeviceError::UnexpectedResponse), - } - } - /// Sets a value on the device /// /// # Arguments @@ -167,7 +130,7 @@ impl LockdownClient { &mut self, key: impl Into, value: Value, - domain: Option, + domain: Option<&str>, ) -> Result<(), IdeviceError> { let key = key.into(); @@ -321,7 +284,7 @@ impl LockdownClient { let host_id = host_id.into(); let system_buid = system_buid.into(); - let pub_key = self.get_value("DevicePublicKey", None).await?; + let pub_key = self.get_value(Some("DevicePublicKey"), None).await?; let pub_key = match pub_key.as_data().map(|x| x.to_vec()) { Some(p) => p, None => { @@ -330,7 +293,7 @@ impl LockdownClient { } }; - let wifi_mac = self.get_value("WiFiAddress", None).await?; + let wifi_mac = self.get_value(Some("WiFiAddress"), None).await?; let wifi_mac = match wifi_mac.as_string() { Some(w) => w, None => { diff --git a/idevice/src/services/mobile_image_mounter.rs b/idevice/src/services/mobile_image_mounter.rs index 78c71d6..2472719 100644 --- a/idevice/src/services/mobile_image_mounter.rs +++ b/idevice/src/services/mobile_image_mounter.rs @@ -113,7 +113,7 @@ impl ImageMounter { /// Returns `IdeviceError::NotFound` if image doesn't exist pub async fn lookup_image( &mut self, - image_type: impl Into, + image_type: impl Into<&str>, ) -> Result, IdeviceError> { let image_type = image_type.into(); let mut req = plist::Dictionary::new(); @@ -370,7 +370,7 @@ impl ImageMounter { /// Returns `IdeviceError` if query fails pub async fn query_nonce( &mut self, - personalized_image_type: Option, + personalized_image_type: Option<&str>, ) -> Result, IdeviceError> { let mut req = plist::Dictionary::new(); req.insert("Command".into(), "QueryNonce".into()); @@ -400,7 +400,7 @@ impl ImageMounter { /// Returns `IdeviceError` if query fails pub async fn query_personalization_identifiers( &mut self, - image_type: Option, + image_type: Option<&str>, ) -> Result { let mut req = plist::Dictionary::new(); req.insert("Command".into(), "QueryPersonalizationIdentifiers".into()); @@ -626,10 +626,7 @@ impl ImageMounter { request.insert("ApECID", unique_chip_id); request.insert( "ApNonce", - plist::Value::Data( - self.query_nonce(Some("DeveloperDiskImage".to_string())) - .await?, - ), + plist::Value::Data(self.query_nonce(Some("DeveloperDiskImage")).await?), ); request.insert("ApProductionMode", true); request.insert("ApSecurityDomain", 1); diff --git a/tools/src/ideviceinfo.rs b/tools/src/ideviceinfo.rs index 948a8c1..1ec95b7 100644 --- a/tools/src/ideviceinfo.rs +++ b/tools/src/ideviceinfo.rs @@ -67,7 +67,9 @@ async fn main() { println!( "{:?}", - lockdown_client.get_value("ProductVersion", None).await + lockdown_client + .get_value(Some("ProductVersion"), None) + .await ); println!( @@ -82,5 +84,5 @@ async fn main() { .await ); println!("{:?}", lockdown_client.idevice.get_type().await.unwrap()); - println!("{:#?}", lockdown_client.get_all_values(None).await); + println!("{:#?}", lockdown_client.get_value(None, None).await); } diff --git a/tools/src/instproxy.rs b/tools/src/instproxy.rs index e5f1980..f22b838 100644 --- a/tools/src/instproxy.rs +++ b/tools/src/instproxy.rs @@ -69,10 +69,7 @@ async fn main() { .await .expect("Unable to connect to instproxy"); if matches.subcommand_matches("lookup").is_some() { - let apps = instproxy_client - .get_apps(Some("User".to_string()), None) - .await - .unwrap(); + let apps = instproxy_client.get_apps(Some("User"), None).await.unwrap(); for app in apps.keys() { println!("{app}"); } diff --git a/tools/src/lockdown.rs b/tools/src/lockdown.rs index 00f3def..842c7b4 100644 --- a/tools/src/lockdown.rs +++ b/tools/src/lockdown.rs @@ -39,12 +39,7 @@ async fn main() { .subcommand( Command::new("get") .about("Gets a value") - .arg(arg!(-v --value "the value to get").required(true)) - .arg(arg!(-d --domain "the domain to get in").required(false)), - ) - .subcommand( - Command::new("get_all") - .about("Gets all") + .arg(arg!(-v --value "the value to get").required(false)) .arg(arg!(-d --domain "the domain to get in").required(false)), ) .subcommand( @@ -86,8 +81,8 @@ async fn main() { match matches.subcommand() { Some(("get", sub_m)) => { - let key = sub_m.get_one::("value").unwrap(); - let domain = sub_m.get_one::("domain").cloned(); + let key = sub_m.get_one::("value").map(|x| x.as_str()); + let domain = sub_m.get_one::("domain").map(|x| x.as_str()); match lockdown_client.get_value(key, domain).await { Ok(value) => { @@ -98,27 +93,18 @@ async fn main() { } } } - Some(("get_all", sub_m)) => { - let domain = sub_m.get_one::("domain").cloned(); - - match lockdown_client.get_all_values(domain).await { - Ok(value) => { - println!("{}", pretty_print_plist(&plist::Value::Dictionary(value))); - } - Err(e) => { - eprintln!("Error getting value: {e}"); - } - } - } Some(("set", sub_m)) => { let key = sub_m.get_one::("key").unwrap(); let value_str = sub_m.get_one::("value").unwrap(); - let domain = sub_m.get_one::("domain").cloned(); + let domain = sub_m.get_one::("domain"); let value = Value::String(value_str.clone()); - match lockdown_client.set_value(key, value, domain).await { + match lockdown_client + .set_value(key, value, domain.map(|x| x.as_str())) + .await + { Ok(()) => println!("Successfully set"), Err(e) => eprintln!("Error setting value: {e}"), } diff --git a/tools/src/mounter.rs b/tools/src/mounter.rs index 681705f..c59a472 100644 --- a/tools/src/mounter.rs +++ b/tools/src/mounter.rs @@ -89,7 +89,10 @@ async fn main() { .await .expect("Unable to connect to lockdown"); - let product_version = match lockdown_client.get_value("ProductVersion", None).await { + let product_version = match lockdown_client + .get_value(Some("ProductVersion"), None) + .await + { Ok(p) => p, Err(_) => { lockdown_client @@ -97,7 +100,7 @@ async fn main() { .await .unwrap(); lockdown_client - .get_value("ProductVersion", None) + .get_value(Some("ProductVersion"), None) .await .unwrap() } @@ -182,21 +185,22 @@ async fn main() { .await .expect("Unable to read signature"); - let unique_chip_id = match lockdown_client.get_value("UniqueChipID", None).await { - Ok(u) => u, - Err(_) => { - lockdown_client - .start_session(&provider.get_pairing_file().await.unwrap()) - .await - .expect("Unable to start session"); - lockdown_client - .get_value("UniqueChipID", None) - .await - .expect("Unable to get UniqueChipID") + let unique_chip_id = + match lockdown_client.get_value(Some("UniqueChipID"), None).await { + Ok(u) => u, + Err(_) => { + lockdown_client + .start_session(&provider.get_pairing_file().await.unwrap()) + .await + .expect("Unable to start session"); + lockdown_client + .get_value(Some("UniqueChipID"), None) + .await + .expect("Unable to get UniqueChipID") + } } - } - .as_unsigned_integer() - .expect("Unexpected value for chip IP"); + .as_unsigned_integer() + .expect("Unexpected value for chip IP"); mounter_client .mount_personalized_with_callback( From 713a2ae0c26e87aaf8bb8e3ad476c872b405199b Mon Sep 17 00:00:00 2001 From: Stossy11 <69031796+stossy11@users.noreply.github.com> Date: Mon, 11 Aug 2025 23:23:56 +1000 Subject: [PATCH 032/126] Fix misagent incorrect status value (#19) --- idevice/src/services/misagent.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/idevice/src/services/misagent.rs b/idevice/src/services/misagent.rs index 0018ba1..ab9b4b1 100644 --- a/idevice/src/services/misagent.rs +++ b/idevice/src/services/misagent.rs @@ -105,7 +105,7 @@ impl MisagentClient { match res.remove("Status") { Some(plist::Value::Integer(status)) => { if let Some(status) = status.as_unsigned() { - if status == 1 { + if status == 0 { Ok(()) } else { Err(IdeviceError::MisagentFailure) @@ -155,7 +155,7 @@ impl MisagentClient { match res.remove("Status") { Some(plist::Value::Integer(status)) => { if let Some(status) = status.as_unsigned() { - if status == 1 { + if status == 0 { Ok(()) } else { Err(IdeviceError::MisagentFailure) From 0a0899cd8a10b98adc4c60c86a0fdf8617895e91 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Mon, 11 Aug 2025 10:41:55 -0600 Subject: [PATCH 033/126] Create concurrent TCP handle and implement RSD for non-lifetime structs --- Cargo.lock | 75 ++++ ffi/libplist | 1 - ffi/src/adapter.rs | 2 +- ffi/src/core_device/app_service.rs | 14 +- ffi/src/core_device_proxy.rs | 5 +- ffi/src/debug_proxy.rs | 14 +- ffi/src/remote_server.rs | 3 +- idevice/Cargo.toml | 9 +- idevice/src/lib.rs | 22 +- idevice/src/provider.rs | 19 +- .../src/services/core_device/app_service.rs | 12 +- idevice/src/services/core_device/mod.rs | 6 - idevice/src/services/debug_proxy.rs | 6 +- idevice/src/services/dvt/mod.rs | 6 +- idevice/src/services/misagent.rs | 14 +- idevice/src/services/restore_service.rs | 20 +- idevice/src/services/rsd.rs | 8 +- idevice/src/tcp/adapter.rs | 22 +- idevice/src/tcp/handle.rs | 339 ++++++++++++++++++ idevice/src/tcp/mod.rs | 16 +- idevice/src/xpc/http2/mod.rs | 7 - idevice/src/xpc/mod.rs | 8 - tools/src/app_service.rs | 9 +- tools/src/common.rs | 4 +- tools/src/debug_proxy.rs | 9 +- tools/src/location_simulation.rs | 12 +- tools/src/process_control.rs | 12 +- tools/src/restore_service.rs | 11 +- 28 files changed, 536 insertions(+), 149 deletions(-) delete mode 160000 ffi/libplist create mode 100644 idevice/src/tcp/handle.rs diff --git a/Cargo.lock b/Cargo.lock index b059b5b..ccb56be 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -476,12 +476,74 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-epoch", + "crossbeam-queue", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crossfire" +version = "2.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3881655583172e9ff313a26f910f262271e331c4af54e22ebd395f8d97da3629" +dependencies = [ + "crossbeam", + "enum_dispatch", + "futures", + "parking_lot", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -568,6 +630,18 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "enum_dispatch" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa18ce2bc66555b3218614519ac839ddb759a7d6720732f979ef8d13be147ecd" +dependencies = [ + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "env_filter" version = "0.1.3" @@ -1073,6 +1147,7 @@ dependencies = [ "byteorder", "bytes", "chrono", + "crossfire", "env_logger", "futures", "indexmap", diff --git a/ffi/libplist b/ffi/libplist deleted file mode 160000 index cf5897a..0000000 --- a/ffi/libplist +++ /dev/null @@ -1 +0,0 @@ -Subproject commit cf5897a71ea412ea2aeb1e2f6b5ea74d4fabfd8c diff --git a/ffi/src/adapter.rs b/ffi/src/adapter.rs index 553162a..79c7a8d 100644 --- a/ffi/src/adapter.rs +++ b/ffi/src/adapter.rs @@ -35,7 +35,7 @@ pub unsafe extern "C" fn adapter_connect( } let adapter = unsafe { &mut (*adapter_handle).0 }; - let res = RUNTIME.block_on(async move { AdapterStream::connect(adapter, port).await }); + let res = RUNTIME.block_on(async move { adapter.connect(port).await }); match res { Ok(r) => { diff --git a/ffi/src/core_device/app_service.rs b/ffi/src/core_device/app_service.rs index a877485..5aac7a4 100644 --- a/ffi/src/core_device/app_service.rs +++ b/ffi/src/core_device/app_service.rs @@ -5,7 +5,6 @@ use std::os::raw::{c_float, c_int}; use std::ptr::{self, null_mut}; use idevice::core_device::AppServiceClient; -use idevice::tcp::stream::AdapterStream; use idevice::{IdeviceError, ReadWrite, RsdService}; use crate::core_device_proxy::AdapterHandle; @@ -91,16 +90,17 @@ pub unsafe extern "C" fn app_service_connect_rsd( return ffi_err!(IdeviceError::FfiInvalidArg); } - let res: Result, IdeviceError> = RUNTIME.block_on(async move { - let provider_ref = unsafe { &mut (*provider).0 }; - let handshake_ref = unsafe { &mut (*handshake).0 }; + let res: Result>, IdeviceError> = + RUNTIME.block_on(async move { + let provider_ref = unsafe { &mut (*provider).0 }; + let handshake_ref = unsafe { &mut (*handshake).0 }; - AppServiceClient::connect_rsd(provider_ref, handshake_ref).await - }); + AppServiceClient::connect_rsd(provider_ref, handshake_ref).await + }); match res { Ok(client) => { - let boxed = Box::new(AppServiceHandle(client.box_inner())); + let boxed = Box::new(AppServiceHandle(client)); unsafe { *handle = Box::into_raw(boxed) }; null_mut() } diff --git a/ffi/src/core_device_proxy.rs b/ffi/src/core_device_proxy.rs index 2d9960e..8282681 100644 --- a/ffi/src/core_device_proxy.rs +++ b/ffi/src/core_device_proxy.rs @@ -7,13 +7,12 @@ use std::{ use idevice::{ IdeviceError, IdeviceService, core_device_proxy::CoreDeviceProxy, provider::IdeviceProvider, - tcp::adapter::Adapter, }; use crate::{IdeviceFfiError, IdeviceHandle, RUNTIME, ffi_err, provider::IdeviceProviderHandle}; pub struct CoreDeviceProxyHandle(pub CoreDeviceProxy); -pub struct AdapterHandle(pub Adapter); +pub struct AdapterHandle(pub idevice::tcp::handle::AdapterHandle); /// Automatically creates and connects to Core Device Proxy, returning a client handle /// @@ -312,7 +311,7 @@ pub unsafe extern "C" fn core_device_proxy_create_tcp_adapter( match result { Ok(adapter_obj) => { - let boxed = Box::new(AdapterHandle(adapter_obj)); + let boxed = Box::new(AdapterHandle(adapter_obj.to_async_handle())); unsafe { *adapter = Box::into_raw(boxed) }; null_mut() } diff --git a/ffi/src/debug_proxy.rs b/ffi/src/debug_proxy.rs index 1ab061e..3bf7eef 100644 --- a/ffi/src/debug_proxy.rs +++ b/ffi/src/debug_proxy.rs @@ -5,7 +5,6 @@ use std::os::raw::c_int; use std::ptr::{self, null_mut}; use idevice::debug_proxy::{DebugProxyClient, DebugserverCommand}; -use idevice::tcp::stream::AdapterStream; use idevice::{IdeviceError, ReadWrite, RsdService}; use crate::core_device_proxy::AdapterHandle; @@ -136,13 +135,14 @@ pub unsafe extern "C" fn debug_proxy_connect_rsd( if provider.is_null() || handshake.is_null() || handshake.is_null() { return ffi_err!(IdeviceError::FfiInvalidArg); } - let res: Result, IdeviceError> = RUNTIME.block_on(async move { - let provider_ref = unsafe { &mut (*provider).0 }; - let handshake_ref = unsafe { &mut (*handshake).0 }; + let res: Result>, IdeviceError> = + RUNTIME.block_on(async move { + let provider_ref = unsafe { &mut (*provider).0 }; + let handshake_ref = unsafe { &mut (*handshake).0 }; - // Connect using the reference - DebugProxyClient::connect_rsd(provider_ref, handshake_ref).await - }); + // Connect using the reference + DebugProxyClient::connect_rsd(provider_ref, handshake_ref).await + }); match res { Ok(d) => { diff --git a/ffi/src/remote_server.rs b/ffi/src/remote_server.rs index 0646d11..a9d3c54 100644 --- a/ffi/src/remote_server.rs +++ b/ffi/src/remote_server.rs @@ -6,7 +6,6 @@ use crate::core_device_proxy::AdapterHandle; use crate::rsd::RsdHandshakeHandle; use crate::{IdeviceFfiError, RUNTIME, ReadWriteOpaque, ffi_err}; use idevice::dvt::remote_server::RemoteServerClient; -use idevice::tcp::stream::AdapterStream; use idevice::{IdeviceError, ReadWrite, RsdService}; /// Opaque handle to a RemoteServerClient @@ -77,7 +76,7 @@ pub unsafe extern "C" fn remote_server_connect_rsd( if provider.is_null() || handshake.is_null() || handshake.is_null() { return ffi_err!(IdeviceError::FfiInvalidArg); } - let res: Result, IdeviceError> = + let res: Result>, IdeviceError> = RUNTIME.block_on(async move { let provider_ref = unsafe { &mut (*provider).0 }; let handshake_ref = unsafe { &mut (*handshake).0 }; diff --git a/idevice/Cargo.toml b/idevice/Cargo.toml index e28e7ef..765a3b5 100644 --- a/idevice/Cargo.toml +++ b/idevice/Cargo.toml @@ -14,6 +14,7 @@ keywords = ["lockdownd", "ios"] tokio = { version = "1.43", features = ["io-util"] } tokio-rustls = { version = "0.26", default-features = false } rustls = { version = "0.23", default-features = false } +crossfire = { version = "2.0", optional = true } # TODO: update to 2.1 when it comes out plist = { version = "1.7" } serde = { version = "1", features = ["derive"] } @@ -80,7 +81,13 @@ restore_service = [] rsd = ["xpc"] syslog_relay = ["dep:bytes"] tcp = ["tokio/net"] -tunnel_tcp_stack = ["dep:rand", "dep:futures", "tokio/fs", "tokio/sync"] +tunnel_tcp_stack = [ + "dep:rand", + "dep:futures", + "tokio/fs", + "tokio/sync", + "dep:crossfire", +] tss = ["dep:uuid", "dep:reqwest"] tunneld = ["dep:serde_json", "dep:json", "dep:reqwest"] usbmuxd = ["tokio/net"] diff --git a/idevice/src/lib.rs b/idevice/src/lib.rs index 76a3034..b7bbe13 100644 --- a/idevice/src/lib.rs +++ b/idevice/src/lib.rs @@ -25,7 +25,7 @@ pub use services::*; #[cfg(feature = "xpc")] pub use xpc::RemoteXpcClient; -use log::{debug, error, trace}; +use log::{debug, error, trace, warn}; use provider::{IdeviceProvider, RsdProvider}; use rustls::{crypto::CryptoProvider, pki_types::ServerName}; use std::{ @@ -70,20 +70,17 @@ pub trait IdeviceService: Sized { pub trait RsdService: Sized { fn rsd_service_name() -> std::borrow::Cow<'static, str>; fn from_stream( - stream: Self::Stream, + stream: Box, ) -> impl std::future::Future> + Send; - fn connect_rsd<'a, S>( - provider: &'a mut impl RsdProvider<'a, Stream = S>, + fn connect_rsd( + provider: &mut impl RsdProvider, handshake: &mut rsd::RsdHandshake, ) -> impl std::future::Future> where - Self: crate::RsdService, - S: ReadWrite, + Self: crate::RsdService, { handshake.connect(provider) } - - type Stream: ReadWrite; } /// Type alias for boxed device connection sockets @@ -417,9 +414,12 @@ impl Idevice { #[cfg(all(feature = "ring", feature = "aws-lc"))] { - compile_error!( - "Cannot enable both `ring` and `aws-lc` features at the same time" - ); + // We can't throw a compile error because it breaks rust-analyzer. + // My sanity while debugging the workspace crates are more important. + + debug!("Using ring crypto backend, because both were passed"); + warn!("Both ring && aws-lc are selected as idevice crypto backends!"); + rustls::crypto::ring::default_provider() } }; diff --git a/idevice/src/provider.rs b/idevice/src/provider.rs index 917fa18..bc0250e 100644 --- a/idevice/src/provider.rs +++ b/idevice/src/provider.rs @@ -42,12 +42,11 @@ pub trait IdeviceProvider: Unpin + Send + Sync + std::fmt::Debug { ) -> Pin> + Send>>; } -pub trait RsdProvider<'a>: Unpin + Send + Sync + std::fmt::Debug { +pub trait RsdProvider: Unpin + Send + Sync + std::fmt::Debug { fn connect_to_service_port( - &'a mut self, + &mut self, port: u16, - ) -> impl std::future::Future> + Send; - type Stream: ReadWrite; + ) -> impl std::future::Future, IdeviceError>> + Send; } /// TCP-based device connection provider @@ -159,13 +158,13 @@ impl IdeviceProvider for UsbmuxdProvider { } #[cfg(feature = "tcp")] -impl<'a> RsdProvider<'a> for std::net::IpAddr { +impl RsdProvider for std::net::IpAddr { async fn connect_to_service_port( - &'a mut self, + &mut self, port: u16, - ) -> Result { - Ok(tokio::net::TcpStream::connect((*self, port)).await?) + ) -> Result, IdeviceError> { + Ok(Box::new( + tokio::net::TcpStream::connect((*self, port)).await?, + )) } - - type Stream = tokio::net::TcpStream; } diff --git a/idevice/src/services/core_device/app_service.rs b/idevice/src/services/core_device/app_service.rs index 5d276ab..fc74960 100644 --- a/idevice/src/services/core_device/app_service.rs +++ b/idevice/src/services/core_device/app_service.rs @@ -7,18 +7,16 @@ use crate::{obf, IdeviceError, ReadWrite, RsdService}; use super::CoreDeviceServiceClient; -impl RsdService for AppServiceClient { +impl RsdService for AppServiceClient> { fn rsd_service_name() -> std::borrow::Cow<'static, str> { obf!("com.apple.coredevice.appservice") } - async fn from_stream(stream: R) -> Result { + async fn from_stream(stream: Box) -> Result { Ok(Self { inner: CoreDeviceServiceClient::new(stream).await?, }) } - - type Stream = R; } pub struct AppServiceClient { @@ -137,12 +135,6 @@ impl<'a, R: ReadWrite + 'a> AppServiceClient { }) } - pub fn box_inner(self) -> AppServiceClient> { - AppServiceClient { - inner: self.inner.box_inner(), - } - } - pub async fn list_apps( &mut self, app_clips: bool, diff --git a/idevice/src/services/core_device/mod.rs b/idevice/src/services/core_device/mod.rs index b5151d3..e11fd76 100644 --- a/idevice/src/services/core_device/mod.rs +++ b/idevice/src/services/core_device/mod.rs @@ -24,12 +24,6 @@ impl<'a, R: ReadWrite + 'a> CoreDeviceServiceClient { Ok(Self { inner: client }) } - pub fn box_inner(self) -> CoreDeviceServiceClient> { - CoreDeviceServiceClient { - inner: self.inner.box_inner(), - } - } - pub async fn invoke( &mut self, feature: impl Into, diff --git a/idevice/src/services/debug_proxy.rs b/idevice/src/services/debug_proxy.rs index 8fa2a28..ab3237f 100644 --- a/idevice/src/services/debug_proxy.rs +++ b/idevice/src/services/debug_proxy.rs @@ -10,19 +10,17 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt}; use crate::{obf, IdeviceError, ReadWrite, RsdService}; -impl RsdService for DebugProxyClient { +impl RsdService for DebugProxyClient> { fn rsd_service_name() -> std::borrow::Cow<'static, str> { obf!("com.apple.internal.dt.remote.debugproxy") } - async fn from_stream(stream: R) -> Result { + async fn from_stream(stream: Box) -> Result { Ok(Self { socket: stream, noack_mode: false, }) } - - type Stream = R; } /// Client for interacting with the iOS debug proxy service diff --git a/idevice/src/services/dvt/mod.rs b/idevice/src/services/dvt/mod.rs index fced62f..904f584 100644 --- a/idevice/src/services/dvt/mod.rs +++ b/idevice/src/services/dvt/mod.rs @@ -8,14 +8,12 @@ pub mod message; pub mod process_control; pub mod remote_server; -impl RsdService for remote_server::RemoteServerClient { +impl RsdService for remote_server::RemoteServerClient> { fn rsd_service_name() -> std::borrow::Cow<'static, str> { obf!("com.apple.instruments.dtservicehub") } - async fn from_stream(stream: R) -> Result { + async fn from_stream(stream: Box) -> Result { Ok(Self::new(stream)) } - - type Stream = R; } diff --git a/idevice/src/services/misagent.rs b/idevice/src/services/misagent.rs index ab9b4b1..4f2f03d 100644 --- a/idevice/src/services/misagent.rs +++ b/idevice/src/services/misagent.rs @@ -6,7 +6,7 @@ use log::warn; use plist::Dictionary; -use crate::{lockdown::LockdownClient, obf, Idevice, IdeviceError, IdeviceService}; +use crate::{lockdown::LockdownClient, obf, Idevice, IdeviceError, IdeviceService, RsdService}; /// Client for interacting with the iOS misagent service /// @@ -19,6 +19,18 @@ pub struct MisagentClient { pub idevice: Idevice, } +impl RsdService for MisagentClient { + fn rsd_service_name() -> std::borrow::Cow<'static, str> { + obf!("com.apple.misagent.shim.remote") + } + + async fn from_stream(stream: Box) -> Result { + let mut stream = Idevice::new(stream, ""); + stream.rsd_checkin().await?; + Ok(Self::new(stream)) + } +} + impl IdeviceService for MisagentClient { /// Returns the misagent service name as registered with lockdownd fn service_name() -> std::borrow::Cow<'static, str> { diff --git a/idevice/src/services/restore_service.rs b/idevice/src/services/restore_service.rs index aa23568..2942e71 100644 --- a/idevice/src/services/restore_service.rs +++ b/idevice/src/services/restore_service.rs @@ -6,41 +6,33 @@ use plist::Dictionary; use crate::{obf, IdeviceError, ReadWrite, RemoteXpcClient, RsdService}; /// Client for interacting with the Restore Service -pub struct RestoreServiceClient { +pub struct RestoreServiceClient { /// The underlying device connection with established Restore Service service - pub stream: RemoteXpcClient, + pub stream: RemoteXpcClient>, } -impl RsdService for RestoreServiceClient { +impl RsdService for RestoreServiceClient { fn rsd_service_name() -> std::borrow::Cow<'static, str> { obf!("com.apple.RestoreRemoteServices.restoreserviced") } - async fn from_stream(stream: R) -> Result { + async fn from_stream(stream: Box) -> Result { Self::new(stream).await } - - type Stream = R; } -impl<'a, R: ReadWrite + 'a> RestoreServiceClient { +impl RestoreServiceClient { /// Creates a new Restore Service client a socket connection, /// and connects to the RemoteXPC service. /// /// # Arguments /// * `idevice` - Pre-established device connection - pub async fn new(stream: R) -> Result { + pub async fn new(stream: Box) -> Result { let mut stream = RemoteXpcClient::new(stream).await?; stream.do_handshake().await?; Ok(Self { stream }) } - pub fn box_inner(self) -> RestoreServiceClient> { - RestoreServiceClient { - stream: self.stream.box_inner(), - } - } - /// Enter recovery pub async fn enter_recovery(&mut self) -> Result<(), IdeviceError> { let mut req = Dictionary::new(); diff --git a/idevice/src/services/rsd.rs b/idevice/src/services/rsd.rs index 1e78196..58566b9 100644 --- a/idevice/src/services/rsd.rs +++ b/idevice/src/services/rsd.rs @@ -156,13 +156,9 @@ impl RsdHandshake { }) } - pub async fn connect<'a, T, S>( - &mut self, - provider: &'a mut impl RsdProvider<'a, Stream = S>, - ) -> Result + pub async fn connect(&mut self, provider: &mut impl RsdProvider) -> Result where - T: crate::RsdService, - S: ReadWrite, + T: crate::RsdService, { let service_name = T::rsd_service_name(); let service = match self.services.get(&service_name.to_string()) { diff --git a/idevice/src/tcp/adapter.rs b/idevice/src/tcp/adapter.rs index 0724fd0..4bcc943 100644 --- a/idevice/src/tcp/adapter.rs +++ b/idevice/src/tcp/adapter.rs @@ -147,6 +147,13 @@ impl Adapter { } } + /// Wraps this handle in a new thread. + /// Streams from this handle will be thread safe, with data sent through channels. + /// The handle supports the trait for RSD provider. + pub fn to_async_handle(self) -> super::handle::AdapterHandle { + super::handle::AdapterHandle::new(self) + } + /// Initiates a TCP connection to the specified port. /// /// # Arguments @@ -435,6 +442,19 @@ impl Adapter { } } + pub(crate) fn uncache_all(&mut self, host_port: u16) -> Result, std::io::Error> { + if let Some(state) = self.states.get_mut(&host_port) { + let res = state.read_buffer[..].to_vec(); + state.read_buffer.clear(); + Ok(res) + } else { + Err(std::io::Error::new( + ErrorKind::NotConnected, + "not connected", + )) + } + } + pub(crate) fn cache_read( &mut self, payload: &[u8], @@ -517,7 +537,7 @@ impl Adapter { }) } - async fn process_tcp_packet(&mut self) -> Result<(), std::io::Error> { + pub(crate) async fn process_tcp_packet(&mut self) -> Result<(), std::io::Error> { loop { let ip_packet = self.read_ip_packet().await?; let res = TcpPacket::parse(&ip_packet)?; diff --git a/idevice/src/tcp/handle.rs b/idevice/src/tcp/handle.rs new file mode 100644 index 0000000..86c08ac --- /dev/null +++ b/idevice/src/tcp/handle.rs @@ -0,0 +1,339 @@ +// So originally, streams wrote to the adapter via a mutable reference. +// This worked fine for most applications, but the lifetime requirement of the stream +// makes things difficult. This was especially apparent when trying to integrate with lockdown +// services that were swapped on the heap. This will also allow for usage across threads, +// especially in FFI. Judging the tradeoffs, we'll go forward with it. + +use std::{collections::HashMap, path::PathBuf, sync::Mutex, task::Poll}; + +use crossfire::{mpsc, spsc, stream::AsyncStream, AsyncRx, MTx, Tx}; +use futures::{stream::FuturesUnordered, StreamExt}; +use log::trace; +use tokio::{ + io::{AsyncRead, AsyncWrite}, + sync::oneshot, +}; + +pub type ConnectToPortRes = + oneshot::Sender, std::io::Error>>), std::io::Error>>; + +enum HandleMessage { + /// Returns the host port + ConnectToPort { + target: u16, + res: ConnectToPortRes, + }, + Close { + host_port: u16, + }, + Send { + host_port: u16, + data: Vec, + res: oneshot::Sender>, + }, + Pcap { + path: PathBuf, + res: oneshot::Sender>, + }, +} + +#[derive(Debug)] +pub struct AdapterHandle { + sender: MTx, +} + +impl AdapterHandle { + pub fn new(mut adapter: super::adapter::Adapter) -> Self { + let (tx, rx) = mpsc::unbounded_async(); + tokio::spawn(async move { + let mut handles: HashMap, std::io::Error>>> = HashMap::new(); + let mut tick = tokio::time::interval(std::time::Duration::from_millis(1)); + loop { + tokio::select! { + // check for messages for us + msg = rx.recv() => { + match msg { + Ok(m) => match m { + HandleMessage::ConnectToPort { target, res } => { + let connect_response = match adapter.connect(target).await { + Ok(c) => { + let (ptx, prx) = spsc::unbounded_async(); + handles.insert(c, ptx); + Ok((c, prx)) + } + Err(e) => Err(e), + }; + res.send(connect_response).ok(); + } + HandleMessage::Close { host_port } => { + handles.remove(&host_port); + adapter.close(host_port).await.ok(); + } + HandleMessage::Send { + host_port, + data, + res, + } => { + if let Err(e) = adapter.queue_send(&data, host_port) { + res.send(Err(e)).ok(); + } else { + let response = adapter.write_buffer_flush().await; + res.send(response).ok(); + } + } + HandleMessage::Pcap { + path, + res + } => { + res.send(adapter.pcap(path).await).ok(); + } + }, + Err(_) => { + break; + }, + } + } + + r = adapter.process_tcp_packet() => { + if let Err(e) = r { + // propagate error to all streams; close them + for (hp, tx) in handles.drain() { + let _ = tx.send(Err(e.kind().into())); // or clone/convert + let _ = adapter.close(hp).await; + } + break; + } + + // Push any newly available bytes to per-conn channels + let mut dead = Vec::new(); + for (&hp, tx) in &handles { + match adapter.uncache_all(hp) { + Ok(buf) if !buf.is_empty() => { + if tx.send(Ok(buf)).is_err() { + dead.push(hp); + } + } + Err(e) => { + let _ = tx.send(Err(e)); + dead.push(hp); + } + _ => {} + } + } + for hp in dead { + handles.remove(&hp); + let _ = adapter.close(hp).await; + } + } + + _ = tick.tick() => { + let _ = adapter.write_buffer_flush().await; + } + } + } + }); + + Self { sender: tx } + } + + pub async fn connect(&mut self, port: u16) -> Result { + let (res_tx, res_rx) = oneshot::channel(); + if self + .sender + .send(HandleMessage::ConnectToPort { + target: port, + res: res_tx, + }) + .is_err() + { + return Err(std::io::Error::new( + std::io::ErrorKind::NetworkUnreachable, + "adapter closed", + )); + } + + match res_rx.await { + Ok(r) => { + let (host_port, recv_channel) = r?; + Ok(StreamHandle { + host_port, + recv_channel: Mutex::new(recv_channel.into_stream()), + send_channel: self.sender.clone(), + read_buffer: Vec::new(), + pending_writes: FuturesUnordered::new(), + }) + } + Err(_) => Err(std::io::Error::new( + std::io::ErrorKind::BrokenPipe, + "adapter closed", + )), + } + } + + pub async fn pcap(&mut self, path: impl Into) -> Result<(), std::io::Error> { + let (res_tx, res_rx) = oneshot::channel(); + let path: PathBuf = path.into(); + + if self + .sender + .send(HandleMessage::Pcap { path, res: res_tx }) + .is_err() + { + return Err(std::io::Error::new( + std::io::ErrorKind::NetworkUnreachable, + "adapter closed", + )); + } + + match res_rx.await { + Ok(r) => r, + Err(_) => Err(std::io::Error::new( + std::io::ErrorKind::BrokenPipe, + "adapter closed", + )), + } + } +} + +#[derive(Debug)] +pub struct StreamHandle { + host_port: u16, + recv_channel: Mutex, std::io::Error>>>, + send_channel: MTx, + + read_buffer: Vec, + pending_writes: FuturesUnordered>>, +} + +impl AsyncRead for StreamHandle { + /// Attempts to read from the connection into the provided buffer. + /// + /// Uses an internal read buffer to cache any extra received data. + /// + /// # Returns + /// * `Poll::Ready(Ok(()))` if data was read successfully + /// * `Poll::Ready(Err(e))` if an error occurred + /// * `Poll::Pending` if operation would block + /// + /// # Errors + /// * Returns `NotConnected` if adapter isn't connected + /// * Propagates any underlying transport errors + fn poll_read( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &mut tokio::io::ReadBuf<'_>, + ) -> std::task::Poll> { + // 1) Serve from cache first. + if !self.read_buffer.is_empty() { + let n = buf.remaining().min(self.read_buffer.len()); + buf.put_slice(&self.read_buffer[..n]); + self.read_buffer.drain(..n); // fewer allocs than to_vec + reassign + return Poll::Ready(Ok(())); + } + + // 2) Poll the channel directly; this registers the waker on Empty. + let mut lock = self + .recv_channel + .lock() + .expect("somehow the mutex was poisoned"); + // this should always return, since we're the only owner of the mutex. The mutex is only + // used to satisfy the `Send` bounds of ReadWrite. + let mut extend_slice = Vec::new(); + let res = match lock.poll_item(cx) { + Poll::Pending => Poll::Pending, + + // Disconnected/ended: map to BrokenPipe + Poll::Ready(None) => Poll::Ready(Err(std::io::Error::new( + std::io::ErrorKind::BrokenPipe, + "channel closed", + ))), + + // Got a chunk: copy what fits; cache the tail. + Poll::Ready(Some(res)) => match res { + Ok(data) => { + let n = buf.remaining().min(data.len()); + buf.put_slice(&data[..n]); + if n < data.len() { + extend_slice = data[n..].to_vec(); + } + Poll::Ready(Ok(())) + } + Err(e) => Poll::Ready(Err(e)), + }, + }; + std::mem::drop(lock); + self.read_buffer.extend(extend_slice); + res + } +} + +impl AsyncWrite for StreamHandle { + /// Attempts to write data to the connection. + /// + /// Data is buffered internally until flushed. + /// + /// # Returns + /// * `Poll::Ready(Ok(n))` with number of bytes written + /// * `Poll::Ready(Err(e))` if an error occurred + /// * `Poll::Pending` if operation would block + /// + /// # Errors + /// * Returns `NotConnected` if adapter isn't connected + fn poll_write( + self: std::pin::Pin<&mut Self>, + _cx: &mut std::task::Context<'_>, + buf: &[u8], + ) -> std::task::Poll> { + trace!("poll psh {}", buf.len()); + let (tx, rx) = oneshot::channel(); + self.send_channel + .send(HandleMessage::Send { + host_port: self.host_port, + data: buf.to_vec(), + res: tx, + }) + .map_err(|_| std::io::Error::new(std::io::ErrorKind::BrokenPipe, "channel closed"))?; + self.pending_writes.push(rx); + Poll::Ready(Ok(buf.len())) + } + + fn poll_flush( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> Poll> { + while let Poll::Ready(maybe) = self.pending_writes.poll_next_unpin(cx) { + match maybe { + Some(Ok(Ok(()))) => {} + Some(Ok(Err(e))) => return Poll::Ready(Err(e)), + Some(Err(_canceled)) => { + return Poll::Ready(Err(std::io::Error::new( + std::io::ErrorKind::BrokenPipe, + "channel closed", + ))) + } + None => break, // nothing pending + } + } + if self.pending_writes.is_empty() { + Poll::Ready(Ok(())) + } else { + Poll::Pending + } + } + + fn poll_shutdown( + self: std::pin::Pin<&mut Self>, + _cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + // Just a drop will close the channel, which will trigger a close + std::task::Poll::Ready(Ok(())) + } +} + +impl Drop for StreamHandle { + fn drop(&mut self) { + let _ = self.send_channel.send(HandleMessage::Close { + host_port: self.host_port, + }); + } +} diff --git a/idevice/src/tcp/mod.rs b/idevice/src/tcp/mod.rs index 0c4424d..64302be 100644 --- a/idevice/src/tcp/mod.rs +++ b/idevice/src/tcp/mod.rs @@ -6,12 +6,12 @@ use std::{ }; use log::debug; -use stream::AdapterStream; use tokio::io::AsyncWriteExt; -use crate::provider::RsdProvider; +use crate::{provider::RsdProvider, ReadWrite}; pub mod adapter; +pub mod handle; pub mod packets; pub mod stream; @@ -39,16 +39,14 @@ pub(crate) fn log_packet(file: &Arc>, packet }); } -impl<'a> RsdProvider<'a> for adapter::Adapter { +impl RsdProvider for handle::AdapterHandle { async fn connect_to_service_port( - &'a mut self, + &mut self, port: u16, - ) -> Result, crate::IdeviceError> { - let s = stream::AdapterStream::connect(self, port).await?; - Ok(s) + ) -> Result, crate::IdeviceError> { + let s = self.connect(port).await?; + Ok(Box::new(s)) } - - type Stream = AdapterStream<'a>; } #[cfg(test)] diff --git a/idevice/src/xpc/http2/mod.rs b/idevice/src/xpc/http2/mod.rs index 7e233e3..5679c66 100644 --- a/idevice/src/xpc/http2/mod.rs +++ b/idevice/src/xpc/http2/mod.rs @@ -28,13 +28,6 @@ impl<'a, R: ReadWrite + 'a> Http2Client { }) } - pub fn box_inner(self) -> Http2Client> { - Http2Client { - inner: Box::new(self.inner), - cache: self.cache, - } - } - pub async fn set_settings( &mut self, settings: Vec, diff --git a/idevice/src/xpc/mod.rs b/idevice/src/xpc/mod.rs index 0931c00..aac5308 100644 --- a/idevice/src/xpc/mod.rs +++ b/idevice/src/xpc/mod.rs @@ -29,14 +29,6 @@ impl<'a, R: ReadWrite + 'a> RemoteXpcClient { }) } - pub fn box_inner(self) -> RemoteXpcClient> { - RemoteXpcClient { - h2_client: self.h2_client.box_inner(), - root_id: self.root_id, - reply_id: self.reply_id, - } - } - pub async fn do_handshake(&mut self) -> Result<(), IdeviceError> { self.h2_client .set_settings( diff --git a/tools/src/app_service.rs b/tools/src/app_service.rs index a90f72d..27f95df 100644 --- a/tools/src/app_service.rs +++ b/tools/src/app_service.rs @@ -3,7 +3,7 @@ use clap::{Arg, Command}; use idevice::{ core_device::AppServiceClient, core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake, - tcp::stream::AdapterStream, IdeviceService, RsdService, + IdeviceService, RsdService, }; mod common; @@ -109,11 +109,10 @@ async fn main() { .expect("no core proxy"); let rsd_port = proxy.handshake.server_rsd_port; - let mut adapter = proxy.create_software_tunnel().expect("no software tunnel"); + let adapter = proxy.create_software_tunnel().expect("no software tunnel"); + let mut adapter = adapter.to_async_handle(); - let stream = AdapterStream::connect(&mut adapter, rsd_port) - .await - .expect("no RSD connect"); + let stream = adapter.connect(rsd_port).await.expect("no RSD connect"); // Make the connection to RemoteXPC let mut handshake = RsdHandshake::new(stream).await.unwrap(); diff --git a/tools/src/common.rs b/tools/src/common.rs index ed9ecfd..8e54744 100644 --- a/tools/src/common.rs +++ b/tools/src/common.rs @@ -18,9 +18,7 @@ pub async fn get_provider( pairing_file: Option<&String>, label: &str, ) -> Result, String> { - let provider: Box = if udid.is_some() { - let udid = udid.unwrap(); - + let provider: Box = if let Some(udid) = udid { let mut usbmuxd = if let Ok(var) = std::env::var("USBMUXD_SOCKET_ADDRESS") { let socket = SocketAddr::from_str(&var).expect("Bad USBMUXD_SOCKET_ADDRESS"); let socket = tokio::net::TcpStream::connect(socket) diff --git a/tools/src/debug_proxy.rs b/tools/src/debug_proxy.rs index ba3be4d..7aa983e 100644 --- a/tools/src/debug_proxy.rs +++ b/tools/src/debug_proxy.rs @@ -5,7 +5,7 @@ use std::io::Write; use clap::{Arg, Command}; use idevice::{ core_device_proxy::CoreDeviceProxy, debug_proxy::DebugProxyClient, rsd::RsdHandshake, - tcp::stream::AdapterStream, IdeviceService, RsdService, + IdeviceService, RsdService, }; mod common; @@ -71,10 +71,9 @@ async fn main() { .expect("no core proxy"); let rsd_port = proxy.handshake.server_rsd_port; - let mut adapter = proxy.create_software_tunnel().expect("no software tunnel"); - let stream = AdapterStream::connect(&mut adapter, rsd_port) - .await - .expect("no RSD connect"); + let adapter = proxy.create_software_tunnel().expect("no software tunnel"); + let mut adapter = adapter.to_async_handle(); + let stream = adapter.connect(rsd_port).await.expect("no RSD connect"); // Make the connection to RemoteXPC let mut handshake = RsdHandshake::new(stream).await.unwrap(); diff --git a/tools/src/location_simulation.rs b/tools/src/location_simulation.rs index 0ee50f5..02c0a6d 100644 --- a/tools/src/location_simulation.rs +++ b/tools/src/location_simulation.rs @@ -2,10 +2,7 @@ // Just lists apps for now use clap::{Arg, Command}; -use idevice::{ - core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake, tcp::stream::AdapterStream, - IdeviceService, RsdService, -}; +use idevice::{core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake, IdeviceService, RsdService}; mod common; @@ -71,10 +68,9 @@ async fn main() { .expect("no core proxy"); let rsd_port = proxy.handshake.server_rsd_port; - let mut adapter = proxy.create_software_tunnel().expect("no software tunnel"); - let stream = AdapterStream::connect(&mut adapter, rsd_port) - .await - .expect("no RSD connect"); + let adapter = proxy.create_software_tunnel().expect("no software tunnel"); + let mut adapter = adapter.to_async_handle(); + let stream = adapter.connect(rsd_port).await.expect("no RSD connect"); // Make the connection to RemoteXPC let mut handshake = RsdHandshake::new(stream).await.unwrap(); diff --git a/tools/src/process_control.rs b/tools/src/process_control.rs index 173633d..511df79 100644 --- a/tools/src/process_control.rs +++ b/tools/src/process_control.rs @@ -1,10 +1,7 @@ // Jackson Coxson use clap::{Arg, Command}; -use idevice::{ - core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake, tcp::stream::AdapterStream, - IdeviceService, RsdService, -}; +use idevice::{core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake, IdeviceService, RsdService}; mod common; @@ -79,10 +76,9 @@ async fn main() { .expect("no core proxy"); let rsd_port = proxy.handshake.server_rsd_port; - let mut adapter = proxy.create_software_tunnel().expect("no software tunnel"); - let stream = AdapterStream::connect(&mut adapter, rsd_port) - .await - .expect("no RSD connect"); + let adapter = proxy.create_software_tunnel().expect("no software tunnel"); + let mut adapter = adapter.to_async_handle(); + let stream = adapter.connect(rsd_port).await.expect("no RSD connect"); // Make the connection to RemoteXPC let mut handshake = RsdHandshake::new(stream).await.unwrap(); diff --git a/tools/src/restore_service.rs b/tools/src/restore_service.rs index b1d1e85..e2dcd8a 100644 --- a/tools/src/restore_service.rs +++ b/tools/src/restore_service.rs @@ -3,8 +3,7 @@ use clap::{Arg, Command}; use idevice::{ core_device_proxy::CoreDeviceProxy, pretty_print_dictionary, - restore_service::RestoreServiceClient, rsd::RsdHandshake, tcp::stream::AdapterStream, - IdeviceService, RsdService, + restore_service::RestoreServiceClient, rsd::RsdHandshake, IdeviceService, RsdService, }; mod common; @@ -75,11 +74,9 @@ async fn main() { .expect("no core proxy"); let rsd_port = proxy.handshake.server_rsd_port; - let mut adapter = proxy.create_software_tunnel().expect("no software tunnel"); - - let stream = AdapterStream::connect(&mut adapter, rsd_port) - .await - .expect("no RSD connect"); + let adapter = proxy.create_software_tunnel().expect("no software tunnel"); + let mut adapter = adapter.to_async_handle(); + let stream = adapter.connect(rsd_port).await.expect("no RSD connect"); // Make the connection to RemoteXPC let mut handshake = RsdHandshake::new(stream).await.unwrap(); From c80512f37fccf11d6a52cc59ae046070c384c88f Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Mon, 11 Aug 2025 13:56:09 -0600 Subject: [PATCH 034/126] Unify IdeviceService creation behavior with trait --- .github/workflows/ci.yml | 6 +-- idevice/src/lib.rs | 24 +++++++++-- idevice/src/services/afc/mod.rs | 27 +------------ idevice/src/services/amfi.rs | 39 ++---------------- idevice/src/services/core_device_proxy.rs | 31 +------------- idevice/src/services/crashreportcopymobile.rs | 39 +----------------- idevice/src/services/diagnostics_relay.rs | 39 ++---------------- idevice/src/services/heartbeat.rs | 40 ++----------------- idevice/src/services/house_arrest.rs | 39 ++---------------- idevice/src/services/installation_proxy.rs | 36 +---------------- idevice/src/services/lockdown.rs | 4 ++ idevice/src/services/misagent.rs | 36 +---------------- idevice/src/services/mobile_image_mounter.rs | 39 ++---------------- idevice/src/services/os_trace_relay.rs | 37 +---------------- idevice/src/services/springboardservices.rs | 39 ++---------------- idevice/src/services/syslog_relay.rs | 39 ++---------------- 16 files changed, 59 insertions(+), 455 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 564a62c..88ecc35 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,10 +16,6 @@ jobs: run: | brew install just - - name: Install libplist - run: | - brew install libplist - - name: Cache Cargo uses: actions/cache@v4 with: @@ -73,7 +69,7 @@ jobs: - name: Install build dependencies run: | - sudo apt-get update && sudo apt-get install -y build-essential cmake libplist + sudo apt-get update && sudo apt-get install -y build-essential cmake - name: Install just run: | diff --git a/idevice/src/lib.rs b/idevice/src/lib.rs index b7bbe13..e1d64f8 100644 --- a/idevice/src/lib.rs +++ b/idevice/src/lib.rs @@ -37,6 +37,8 @@ use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; pub use util::{pretty_print_dictionary, pretty_print_plist}; +use crate::services::lockdown::LockdownClient; + /// A trait combining all required characteristics for a device communication socket /// /// This serves as a convenience trait for any type that can be used as an asynchronous @@ -61,9 +63,25 @@ pub trait IdeviceService: Sized { /// /// # Arguments /// * `provider` - The device provider that can supply connections - fn connect( - provider: &dyn IdeviceProvider, - ) -> impl std::future::Future> + Send; + async fn connect(provider: &dyn IdeviceProvider) -> Result { + let mut lockdown = LockdownClient::connect(provider).await?; + lockdown + .start_session(&provider.get_pairing_file().await?) + .await?; + + let (port, ssl) = lockdown.start_service(Self::service_name()).await?; + + let mut idevice = provider.connect(port).await?; + if ssl { + idevice + .start_session(&provider.get_pairing_file().await?) + .await?; + } + + Self::from_stream(idevice).await + } + + async fn from_stream(idevice: Idevice) -> Result; } #[cfg(feature = "rsd")] diff --git a/idevice/src/services/afc/mod.rs b/idevice/src/services/afc/mod.rs index c84a692..634e379 100644 --- a/idevice/src/services/afc/mod.rs +++ b/idevice/src/services/afc/mod.rs @@ -11,7 +11,7 @@ use log::warn; use opcode::{AfcFopenMode, AfcOpcode}; use packet::{AfcPacket, AfcPacketHeader}; -use crate::{lockdown::LockdownClient, obf, Idevice, IdeviceError, IdeviceService}; +use crate::{obf, Idevice, IdeviceError, IdeviceService}; pub mod errors; pub mod file; @@ -65,30 +65,7 @@ impl IdeviceService for AfcClient { obf!("com.apple.afc") } - /// Connects to the AFC service on the device - /// - /// # Arguments - /// * `provider` - The iDevice provider to use for the connection - /// - /// # Returns - /// A new `AfcClient` instance on success - async fn connect( - provider: &dyn crate::provider::IdeviceProvider, - ) -> Result { - let mut lockdown = LockdownClient::connect(provider).await?; - lockdown - .start_session(&provider.get_pairing_file().await?) - .await?; - - let (port, ssl) = lockdown.start_service(Self::service_name()).await?; - - let mut idevice = provider.connect(port).await?; - if ssl { - idevice - .start_session(&provider.get_pairing_file().await?) - .await?; - } - + async fn from_stream(idevice: Idevice) -> Result { Ok(Self { idevice, package_number: 0, diff --git a/idevice/src/services/amfi.rs b/idevice/src/services/amfi.rs index 41c92bd..c317127 100644 --- a/idevice/src/services/amfi.rs +++ b/idevice/src/services/amfi.rs @@ -2,7 +2,7 @@ use plist::Dictionary; -use crate::{lockdown::LockdownClient, obf, Idevice, IdeviceError, IdeviceService}; +use crate::{obf, Idevice, IdeviceError, IdeviceService}; /// Client for interacting with the AMFI service on the device pub struct AmfiClient { @@ -16,41 +16,8 @@ impl IdeviceService for AmfiClient { obf!("com.apple.amfi.lockdown") } - /// Establishes a connection to the amfi service - /// - /// # Arguments - /// * `provider` - Device connection provider - /// - /// # Returns - /// A connected `AmfiClient` instance - /// - /// # Errors - /// Returns `IdeviceError` if any step of the connection process fails - /// - /// # Process - /// 1. Connects to lockdownd service - /// 2. Starts a lockdown session - /// 3. Requests the amfi service port - /// 4. Establishes connection to the amfi port - /// 5. Optionally starts TLS if required by service - async fn connect( - provider: &dyn crate::provider::IdeviceProvider, - ) -> Result { - let mut lockdown = LockdownClient::connect(provider).await?; - lockdown - .start_session(&provider.get_pairing_file().await?) - .await?; - - let (port, ssl) = lockdown.start_service(Self::service_name()).await?; - - let mut idevice = provider.connect(port).await?; - if ssl { - idevice - .start_session(&provider.get_pairing_file().await?) - .await?; - } - - Ok(Self { idevice }) + async fn from_stream(idevice: Idevice) -> Result { + Ok(Self::new(idevice)) } } diff --git a/idevice/src/services/core_device_proxy.rs b/idevice/src/services/core_device_proxy.rs index 6c714e3..6aafed3 100644 --- a/idevice/src/services/core_device_proxy.rs +++ b/idevice/src/services/core_device_proxy.rs @@ -13,7 +13,7 @@ //! # Features //! - `tunnel_tcp_stack`: Enables software TCP/IP tunnel creation using a virtual adapter. See the tcp moduel. -use crate::{lockdown::LockdownClient, obf, Idevice, IdeviceError, IdeviceService}; +use crate::{obf, Idevice, IdeviceError, IdeviceService}; use byteorder::{BigEndian, WriteBytesExt}; use serde::{Deserialize, Serialize}; @@ -97,34 +97,7 @@ impl IdeviceService for CoreDeviceProxy { obf!("com.apple.internal.devicecompute.CoreDeviceProxy") } - /// Connects to the CoreDeviceProxy service - /// - /// # Arguments - /// - /// * `provider` - An implementation of `IdeviceProvider` that supplies - /// pairing data and socket connections. - /// - /// # Returns - /// - /// * `Ok(CoreDeviceProxy)` if connection and handshake succeed. - /// * `Err(IdeviceError)` if any step fails. - async fn connect( - provider: &dyn crate::provider::IdeviceProvider, - ) -> Result { - let mut lockdown = LockdownClient::connect(provider).await?; - lockdown - .start_session(&provider.get_pairing_file().await?) - .await?; - - let (port, ssl) = lockdown.start_service(Self::service_name()).await?; - - let mut idevice = provider.connect(port).await?; - if ssl { - idevice - .start_session(&provider.get_pairing_file().await?) - .await?; - } - + async fn from_stream(idevice: Idevice) -> Result { Self::new(idevice).await } } diff --git a/idevice/src/services/crashreportcopymobile.rs b/idevice/src/services/crashreportcopymobile.rs index afc2b1f..7b426d4 100644 --- a/idevice/src/services/crashreportcopymobile.rs +++ b/idevice/src/services/crashreportcopymobile.rs @@ -26,43 +26,8 @@ impl IdeviceService for CrashReportCopyMobileClient { obf!("com.apple.crashreportcopymobile") } - /// Connects to the CrashReportCopyMobile service on the device. - /// - /// # Arguments - /// * `provider` - The provider used to access the device and pairing info. - /// - /// # Returns - /// A connected `CrashReportCopyMobileClient`. - /// - /// # Errors - /// Returns `IdeviceError` if the connection fails at any stage. - /// - /// # Process - /// 1. Connects to the lockdownd service. - /// 2. Starts a lockdown session. - /// 3. Requests the CrashReportCopyMobile service. - /// 4. Establishes a connection to the service. - /// 5. Performs SSL handshake if required. - async fn connect( - provider: &dyn crate::provider::IdeviceProvider, - ) -> Result { - let mut lockdown = LockdownClient::connect(provider).await?; - lockdown - .start_session(&provider.get_pairing_file().await?) - .await?; - - let (port, ssl) = lockdown.start_service(Self::service_name()).await?; - - let mut idevice = provider.connect(port).await?; - if ssl { - idevice - .start_session(&provider.get_pairing_file().await?) - .await?; - } - - Ok(Self { - afc_client: AfcClient::new(idevice), - }) + async fn from_stream(idevice: Idevice) -> Result { + Ok(Self::new(idevice)) } } diff --git a/idevice/src/services/diagnostics_relay.rs b/idevice/src/services/diagnostics_relay.rs index bb5a545..7811b85 100644 --- a/idevice/src/services/diagnostics_relay.rs +++ b/idevice/src/services/diagnostics_relay.rs @@ -1,6 +1,6 @@ //! Diagnostics Relay -use crate::{lockdown::LockdownClient, obf, Idevice, IdeviceError, IdeviceService}; +use crate::{obf, Idevice, IdeviceError, IdeviceService}; /// Client for interacting with the Diagnostics Relay pub struct DiagnosticsRelayClient { @@ -14,41 +14,8 @@ impl IdeviceService for DiagnosticsRelayClient { obf!("com.apple.mobile.diagnostics_relay") } - /// Establishes a connection to the service - /// - /// # Arguments - /// * `provider` - Device connection provider - /// - /// # Returns - /// A connected `DiagnosticsRelayClient` instance - /// - /// # Errors - /// Returns `IdeviceError` if any step of the connection process fails - /// - /// # Process - /// 1. Connects to lockdownd service - /// 2. Starts a lockdown session - /// 3. Requests the service port - /// 4. Establishes connection to the port - /// 5. Optionally starts TLS if required by service - async fn connect( - provider: &dyn crate::provider::IdeviceProvider, - ) -> Result { - let mut lockdown = LockdownClient::connect(provider).await?; - lockdown - .start_session(&provider.get_pairing_file().await?) - .await?; - - let (port, ssl) = lockdown.start_service(Self::service_name()).await?; - - let mut idevice = provider.connect(port).await?; - if ssl { - idevice - .start_session(&provider.get_pairing_file().await?) - .await?; - } - - Ok(Self { idevice }) + async fn from_stream(idevice: Idevice) -> Result { + Ok(Self::new(idevice)) } } diff --git a/idevice/src/services/heartbeat.rs b/idevice/src/services/heartbeat.rs index cf8d144..a57690d 100644 --- a/idevice/src/services/heartbeat.rs +++ b/idevice/src/services/heartbeat.rs @@ -3,7 +3,7 @@ //! iOS automatically closes service connections if there is no heartbeat client connected and //! responding. -use crate::{lockdown::LockdownClient, obf, Idevice, IdeviceError, IdeviceService}; +use crate::{obf, Idevice, IdeviceError, IdeviceService}; /// Client for interacting with the iOS device heartbeat service /// @@ -22,42 +22,8 @@ impl IdeviceService for HeartbeatClient { fn service_name() -> std::borrow::Cow<'static, str> { obf!("com.apple.mobile.heartbeat") } - - /// Establishes a connection to the heartbeat service - /// - /// # Arguments - /// * `provider` - Device connection provider - /// - /// # Returns - /// A connected `HeartbeatClient` instance - /// - /// # Errors - /// Returns `IdeviceError` if any step of the connection process fails - /// - /// # Process - /// 1. Connects to lockdownd service - /// 2. Starts a lockdown session - /// 3. Requests the heartbeat service port - /// 4. Establishes connection to the heartbeat port - /// 5. Optionally starts TLS if required by service - async fn connect( - provider: &dyn crate::provider::IdeviceProvider, - ) -> Result { - let mut lockdown = LockdownClient::connect(provider).await?; - lockdown - .start_session(&provider.get_pairing_file().await?) - .await?; - - let (port, ssl) = lockdown.start_service(Self::service_name()).await?; - - let mut idevice = provider.connect(port).await?; - if ssl { - idevice - .start_session(&provider.get_pairing_file().await?) - .await?; - } - - Ok(Self { idevice }) + async fn from_stream(idevice: Idevice) -> Result { + Ok(Self::new(idevice)) } } diff --git a/idevice/src/services/house_arrest.rs b/idevice/src/services/house_arrest.rs index 2a4c619..f4de8ab 100644 --- a/idevice/src/services/house_arrest.rs +++ b/idevice/src/services/house_arrest.rs @@ -6,7 +6,7 @@ use plist::{Dictionary, Value}; -use crate::{lockdown::LockdownClient, obf, Idevice, IdeviceError, IdeviceService}; +use crate::{obf, Idevice, IdeviceError, IdeviceService}; use super::afc::AfcClient; @@ -25,41 +25,8 @@ impl IdeviceService for HouseArrestClient { obf!("com.apple.mobile.house_arrest") } - /// Establishes a connection to the HouseArrest service - /// - /// # Arguments - /// * `provider` - Device connection provider - /// - /// # Returns - /// A connected `HouseArrestClient` instance - /// - /// # Errors - /// Returns `IdeviceError` if any step of the connection process fails - /// - /// # Process - /// 1. Connect to the lockdownd service - /// 2. Start a lockdown session - /// 3. Request the HouseArrest service - /// 4. Connect to the returned service port - /// 5. Start TLS if required by the service - async fn connect( - provider: &dyn crate::provider::IdeviceProvider, - ) -> Result { - let mut lockdown = LockdownClient::connect(provider).await?; - lockdown - .start_session(&provider.get_pairing_file().await?) - .await?; - - let (port, ssl) = lockdown.start_service(Self::service_name()).await?; - - let mut idevice = provider.connect(port).await?; - if ssl { - idevice - .start_session(&provider.get_pairing_file().await?) - .await?; - } - - Ok(Self { idevice }) + async fn from_stream(idevice: Idevice) -> Result { + Ok(Self::new(idevice)) } } diff --git a/idevice/src/services/installation_proxy.rs b/idevice/src/services/installation_proxy.rs index 3b4014f..b7111f2 100644 --- a/idevice/src/services/installation_proxy.rs +++ b/idevice/src/services/installation_proxy.rs @@ -8,7 +8,7 @@ use std::collections::HashMap; use log::warn; use plist::Dictionary; -use crate::{lockdown::LockdownClient, obf, Idevice, IdeviceError, IdeviceService}; +use crate::{obf, Idevice, IdeviceError, IdeviceService}; /// Client for interacting with the iOS installation proxy service /// @@ -25,39 +25,7 @@ impl IdeviceService for InstallationProxyClient { obf!("com.apple.mobile.installation_proxy") } - /// Establishes a connection to the installation proxy service - /// - /// # Arguments - /// * `provider` - Device connection provider - /// - /// # Returns - /// A connected `InstallationProxyClient` instance - /// - /// # Errors - /// Returns `IdeviceError` if any step of the connection process fails - /// - /// # Process - /// 1. Connects to lockdownd service - /// 2. Starts a lockdown session - /// 3. Requests the installation proxy service port - /// 4. Establishes connection to the service port - /// 5. Optionally starts TLS if required by service - async fn connect( - provider: &dyn crate::provider::IdeviceProvider, - ) -> Result { - let mut lockdown = LockdownClient::connect(provider).await?; - lockdown - .start_session(&provider.get_pairing_file().await?) - .await?; - let (port, ssl) = lockdown.start_service(Self::service_name()).await?; - - let mut idevice = provider.connect(port).await?; - if ssl { - idevice - .start_session(&provider.get_pairing_file().await?) - .await?; - } - + async fn from_stream(idevice: Idevice) -> Result { Ok(Self::new(idevice)) } } diff --git a/idevice/src/services/lockdown.rs b/idevice/src/services/lockdown.rs index 2c1627d..cbbd2a9 100644 --- a/idevice/src/services/lockdown.rs +++ b/idevice/src/services/lockdown.rs @@ -42,6 +42,10 @@ impl IdeviceService for LockdownClient { let idevice = provider.connect(Self::LOCKDOWND_PORT).await?; Ok(Self::new(idevice)) } + + async fn from_stream(idevice: Idevice) -> Result { + Ok(Self::new(idevice)) + } } /// Internal structure for lockdown protocol requests diff --git a/idevice/src/services/misagent.rs b/idevice/src/services/misagent.rs index 4f2f03d..a3e3787 100644 --- a/idevice/src/services/misagent.rs +++ b/idevice/src/services/misagent.rs @@ -6,7 +6,7 @@ use log::warn; use plist::Dictionary; -use crate::{lockdown::LockdownClient, obf, Idevice, IdeviceError, IdeviceService, RsdService}; +use crate::{obf, Idevice, IdeviceError, IdeviceService, RsdService}; /// Client for interacting with the iOS misagent service /// @@ -37,39 +37,7 @@ impl IdeviceService for MisagentClient { obf!("com.apple.misagent") } - /// Establishes a connection to the misagent service - /// - /// # Arguments - /// * `provider` - Device connection provider - /// - /// # Returns - /// A connected `MisagentClient` instance - /// - /// # Errors - /// Returns `IdeviceError` if any step of the connection process fails - /// - /// # Process - /// 1. Connects to lockdownd service - /// 2. Starts a lockdown session - /// 3. Requests the misagent service port - /// 4. Establishes connection to the service port - /// 5. Optionally starts TLS if required by service - async fn connect( - provider: &dyn crate::provider::IdeviceProvider, - ) -> Result { - let mut lockdown = LockdownClient::connect(provider).await?; - lockdown - .start_session(&provider.get_pairing_file().await?) - .await?; - let (port, ssl) = lockdown.start_service(Self::service_name()).await?; - - let mut idevice = provider.connect(port).await?; - if ssl { - idevice - .start_session(&provider.get_pairing_file().await?) - .await?; - } - + async fn from_stream(idevice: Idevice) -> Result { Ok(Self::new(idevice)) } } diff --git a/idevice/src/services/mobile_image_mounter.rs b/idevice/src/services/mobile_image_mounter.rs index 2472719..8ec22d3 100644 --- a/idevice/src/services/mobile_image_mounter.rs +++ b/idevice/src/services/mobile_image_mounter.rs @@ -9,7 +9,7 @@ use log::debug; -use crate::{lockdown::LockdownClient, obf, Idevice, IdeviceError, IdeviceService}; +use crate::{obf, Idevice, IdeviceError, IdeviceService}; use sha2::{Digest, Sha384}; #[cfg(feature = "tss")] @@ -33,41 +33,8 @@ impl IdeviceService for ImageMounter { obf!("com.apple.mobile.mobile_image_mounter") } - /// Establishes a connection to the image mounter service - /// - /// # Arguments - /// * `provider` - Device connection provider - /// - /// # Returns - /// A connected `ImageMounter` instance - /// - /// # Errors - /// Returns `IdeviceError` if any step of the connection process fails - /// - /// # Process - /// 1. Connects to lockdownd service - /// 2. Starts a lockdown session - /// 3. Requests the image mounter service port - /// 4. Establishes connection to the service port - /// 5. Optionally starts TLS if required by service - async fn connect( - provider: &dyn crate::provider::IdeviceProvider, - ) -> Result { - let mut lockdown = LockdownClient::connect(provider).await?; - lockdown - .start_session(&provider.get_pairing_file().await?) - .await?; - - let (port, ssl) = lockdown.start_service(Self::service_name()).await?; - - let mut idevice = provider.connect(port).await?; - if ssl { - idevice - .start_session(&provider.get_pairing_file().await?) - .await?; - } - - Ok(Self { idevice }) + async fn from_stream(idevice: Idevice) -> Result { + Ok(Self::new(idevice)) } } diff --git a/idevice/src/services/os_trace_relay.rs b/idevice/src/services/os_trace_relay.rs index 8ff203d..0a6d4eb 100644 --- a/idevice/src/services/os_trace_relay.rs +++ b/idevice/src/services/os_trace_relay.rs @@ -7,7 +7,7 @@ use chrono::{DateTime, NaiveDateTime}; use plist::Dictionary; use tokio::io::AsyncWriteExt; -use crate::{lockdown::LockdownClient, obf, Idevice, IdeviceError, IdeviceService}; +use crate::{obf, Idevice, IdeviceError, IdeviceService}; /// Client for interacting with the iOS device OsTraceRelay service pub struct OsTraceRelayClient { @@ -21,40 +21,7 @@ impl IdeviceService for OsTraceRelayClient { obf!("com.apple.os_trace_relay") } - /// Establishes a connection to the OsTraceRelay service - /// - /// # Arguments - /// * `provider` - Device connection provider - /// - /// # Returns - /// A connected `OsTraceRelayClient` instance - /// - /// # Errors - /// Returns `IdeviceError` if any step of the connection process fails - /// - /// # Process - /// 1. Connects to lockdownd service - /// 2. Starts a lockdown session - /// 3. Requests the OsTraceRelay service port - /// 4. Establishes connection to the OsTraceRelay port - /// 5. Optionally starts TLS if required by service - async fn connect( - provider: &dyn crate::provider::IdeviceProvider, - ) -> Result { - let mut lockdown = LockdownClient::connect(provider).await?; - lockdown - .start_session(&provider.get_pairing_file().await?) - .await?; - - let (port, ssl) = lockdown.start_service(Self::service_name()).await?; - - let mut idevice = provider.connect(port).await?; - if ssl { - idevice - .start_session(&provider.get_pairing_file().await?) - .await?; - } - + async fn from_stream(idevice: Idevice) -> Result { Ok(Self { idevice }) } } diff --git a/idevice/src/services/springboardservices.rs b/idevice/src/services/springboardservices.rs index a2da261..99a8564 100644 --- a/idevice/src/services/springboardservices.rs +++ b/idevice/src/services/springboardservices.rs @@ -3,7 +3,7 @@ //! Provides functionality for interacting with the SpringBoard services on iOS devices, //! which manages home screen and app icon related operations. -use crate::{lockdown::LockdownClient, obf, Idevice, IdeviceError, IdeviceService}; +use crate::{obf, Idevice, IdeviceError, IdeviceService}; /// Client for interacting with the iOS SpringBoard services /// @@ -20,41 +20,8 @@ impl IdeviceService for SpringBoardServicesClient { obf!("com.apple.springboardservices") } - /// Establishes a connection to the SpringBoard services - /// - /// # Arguments - /// * `provider` - Device connection provider - /// - /// # Returns - /// A connected `SpringBoardServicesClient` instance - /// - /// # Errors - /// Returns `IdeviceError` if any step of the connection process fails - /// - /// # Process - /// 1. Connects to lockdownd service - /// 2. Starts a lockdown session - /// 3. Requests the SpringBoard services port - /// 4. Establishes connection to the service port - /// 5. Optionally starts TLS if required by service - async fn connect( - provider: &dyn crate::provider::IdeviceProvider, - ) -> Result { - let mut lockdown = LockdownClient::connect(provider).await?; - lockdown - .start_session(&provider.get_pairing_file().await?) - .await?; - - let (port, ssl) = lockdown.start_service(Self::service_name()).await?; - - let mut idevice = provider.connect(port).await?; - if ssl { - idevice - .start_session(&provider.get_pairing_file().await?) - .await?; - } - - Ok(Self { idevice }) + async fn from_stream(idevice: Idevice) -> Result { + Ok(Self::new(idevice)) } } diff --git a/idevice/src/services/syslog_relay.rs b/idevice/src/services/syslog_relay.rs index 3553757..cf1069a 100644 --- a/idevice/src/services/syslog_relay.rs +++ b/idevice/src/services/syslog_relay.rs @@ -1,6 +1,6 @@ //! iOS Device SyslogRelay Service Abstraction -use crate::{lockdown::LockdownClient, obf, Idevice, IdeviceError, IdeviceService}; +use crate::{obf, Idevice, IdeviceError, IdeviceService}; /// Client for interacting with the iOS device SyslogRelay service pub struct SyslogRelayClient { @@ -14,41 +14,8 @@ impl IdeviceService for SyslogRelayClient { obf!("com.apple.syslog_relay") } - /// Establishes a connection to the SyslogRelay service - /// - /// # Arguments - /// * `provider` - Device connection provider - /// - /// # Returns - /// A connected `SyslogRelayClient` instance - /// - /// # Errors - /// Returns `IdeviceError` if any step of the connection process fails - /// - /// # Process - /// 1. Connects to lockdownd service - /// 2. Starts a lockdown session - /// 3. Requests the SyslogRelay service port - /// 4. Establishes connection to the SyslogRelay port - /// 5. Optionally starts TLS if required by service - async fn connect( - provider: &dyn crate::provider::IdeviceProvider, - ) -> Result { - let mut lockdown = LockdownClient::connect(provider).await?; - lockdown - .start_session(&provider.get_pairing_file().await?) - .await?; - - let (port, ssl) = lockdown.start_service(Self::service_name()).await?; - - let mut idevice = provider.connect(port).await?; - if ssl { - idevice - .start_session(&provider.get_pairing_file().await?) - .await?; - } - - Ok(Self { idevice }) + async fn from_stream(idevice: Idevice) -> Result { + Ok(Self::new(idevice)) } } From d4fa2b603216d449867112570299be49b53517d1 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Mon, 11 Aug 2025 13:59:06 -0600 Subject: [PATCH 035/126] Ignore async fn trait warning --- idevice/src/lib.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/idevice/src/lib.rs b/idevice/src/lib.rs index e1d64f8..9320b91 100644 --- a/idevice/src/lib.rs +++ b/idevice/src/lib.rs @@ -63,6 +63,12 @@ pub trait IdeviceService: Sized { /// /// # Arguments /// * `provider` - The device provider that can supply connections + /// + // From the docs + // │ │ ├╴ use of `async fn` in public traits is discouraged as auto trait bounds cannot be specified + // │ │ │ you can suppress this lint if you plan to use the trait only in your own code, or do not care about auto traits like `Send` on the `Future` + // │ │ │ `#[warn(async_fn_in_trait)]` on by default rustc (async_fn_in_trait) [66, 5] + #[allow(async_fn_in_trait)] async fn connect(provider: &dyn IdeviceProvider) -> Result { let mut lockdown = LockdownClient::connect(provider).await?; lockdown @@ -81,6 +87,7 @@ pub trait IdeviceService: Sized { Self::from_stream(idevice).await } + #[allow(async_fn_in_trait)] async fn from_stream(idevice: Idevice) -> Result; } From f8477ed77cd1d57dc21e9bdb9e7ade5a042337ee Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Mon, 11 Aug 2025 16:39:45 -0600 Subject: [PATCH 036/126] Add more nice features to plist_macro --- idevice/src/plist_macro.rs | 229 ++++++++++++++++++++++++++++++++++++- 1 file changed, 223 insertions(+), 6 deletions(-) diff --git a/idevice/src/plist_macro.rs b/idevice/src/plist_macro.rs index 6624e3b..c117179 100644 --- a/idevice/src/plist_macro.rs +++ b/idevice/src/plist_macro.rs @@ -123,6 +123,21 @@ macro_rules! plist_internal { $crate::plist_unexpected!($unexpected) }; + (@array [$($elems:expr,)*] ? $maybe:expr , $($rest:tt)*) => { + if let Some(__v) = $crate::plist_macro::plist_maybe($maybe) { + $crate::plist_internal!(@array [$($elems,)* __v,] $($rest)*) + } else { + $crate::plist_internal!(@array [$($elems,)*] $($rest)*) + } + }; + (@array [$($elems:expr,)*] ? $maybe:expr) => { + if let Some(__v) = $crate::plist_macro::plist_maybe($maybe) { + $crate::plist_internal!(@array [$($elems,)* __v]) + } else { + $crate::plist_internal!(@array [$($elems,)*]) + } + }; + ////////////////////////////////////////////////////////////////////////// // TT muncher for parsing the inside of an object {...}. Each entry is // inserted into the given map variable. @@ -177,6 +192,62 @@ macro_rules! plist_internal { $crate::plist_internal!(@object $object [$($key)+] ($crate::plist_internal!({$($map)*})) $($rest)*); }; + // Optional insert with trailing comma: key?: expr, + (@object $object:ident ($($key:tt)+) (:? $value:expr , $($rest:tt)*) $copy:tt) => { + if let Some(__v) = $crate::plist_macro::plist_maybe($value) { + let _ = $object.insert(($($key)+).into(), __v); + } + $crate::plist_internal!(@object $object () ($($rest)*) ($($rest)*)); + }; + + // Optional insert, last entry: key?: expr + (@object $object:ident ($($key:tt)+) (:? $value:expr) $copy:tt) => { + if let Some(__v) = $crate::plist_macro::plist_maybe($value) { + let _ = $object.insert(($($key)+).into(), __v); + } + }; + + (@object $object:ident () ( :< $value:expr , $($rest:tt)*) $copy:tt) => { + { + let __v = $crate::plist_internal!($value); + let __dict = $crate::plist_macro::IntoPlistDict::into_plist_dict(__v); + for (__k, __val) in __dict { + let _ = $object.insert(__k, __val); + } + } + $crate::plist_internal!(@object $object () ($($rest)*) ($($rest)*)); + }; + + // Merge: last entry `:< expr` + (@object $object:ident () ( :< $value:expr ) $copy:tt) => { + { + let __v = $crate::plist_internal!($value); + let __dict = $crate::plist_macro::IntoPlistDict::into_plist_dict(__v); + for (__k, __val) in __dict { + let _ = $object.insert(__k, __val); + } + } + }; + + // Optional merge: `:< ? expr,` — only merge if Some(...) + (@object $object:ident () ( :< ? $value:expr , $($rest:tt)*) $copy:tt) => { + if let Some(__dict) = $crate::plist_macro::maybe_into_dict($value) { + for (__k, __val) in __dict { + let _ = $object.insert(__k, __val); + } + } + $crate::plist_internal!(@object $object () ($($rest)*) ($($rest)*)); + }; + + // Optional merge: last entry `:< ? expr` + (@object $object:ident () ( :< ? $value:expr ) $copy:tt) => { + if let Some(__dict) = $crate::plist_macro::maybe_into_dict($value) { + for (__k, __val) in __dict { + let _ = $object.insert(__k, __val); + } + } + }; + // Next value is an expression followed by comma. (@object $object:ident ($($key:tt)+) (: $value:expr , $($rest:tt)*) $copy:tt) => { $crate::plist_internal!(@object $object [$($key)+] ($crate::plist_internal!($value)) , $($rest)*); @@ -315,6 +386,12 @@ impl PlistConvertible for &str { } } +impl PlistConvertible for i16 { + fn to_plist_value(self) -> plist::Value { + plist::Value::Integer(self.into()) + } +} + impl PlistConvertible for i32 { fn to_plist_value(self) -> plist::Value { plist::Value::Integer(self.into()) @@ -327,6 +404,12 @@ impl PlistConvertible for i64 { } } +impl PlistConvertible for u16 { + fn to_plist_value(self) -> plist::Value { + plist::Value::Integer((self as i64).into()) + } +} + impl PlistConvertible for u32 { fn to_plist_value(self) -> plist::Value { plist::Value::Integer((self as i64).into()) @@ -357,6 +440,27 @@ impl PlistConvertible for bool { } } +impl<'a> PlistConvertible for std::borrow::Cow<'a, str> { + fn to_plist_value(self) -> plist::Value { + plist::Value::String(self.into_owned()) + } +} +impl PlistConvertible for Vec { + fn to_plist_value(self) -> plist::Value { + plist::Value::Data(self) + } +} +impl PlistConvertible for &[u8] { + fn to_plist_value(self) -> plist::Value { + plist::Value::Data(self.to_vec()) + } +} +impl PlistConvertible for std::time::SystemTime { + fn to_plist_value(self) -> plist::Value { + plist::Value::Date(self.into()) + } +} + impl PlistConvertible for Vec { fn to_plist_value(self) -> plist::Value { plist::Value::Array(self.into_iter().map(|item| item.to_plist_value()).collect()) @@ -423,15 +527,96 @@ where } } -impl PlistConvertible for Option { - fn to_plist_value(self) -> plist::Value { +// Treat plain T as Some(T) and Option as-is. +pub trait MaybePlist { + fn into_option_value(self) -> Option; +} + +impl MaybePlist for T { + fn into_option_value(self) -> Option { + Some(self.to_plist_value()) + } +} + +impl MaybePlist for Option { + fn into_option_value(self) -> Option { + self.map(|v| v.to_plist_value()) + } +} + +#[doc(hidden)] +pub fn plist_maybe(v: T) -> Option { + v.into_option_value() +} + +// Convert things into a Dictionary we can merge. +pub trait IntoPlistDict { + fn into_plist_dict(self) -> plist::Dictionary; +} + +impl IntoPlistDict for plist::Dictionary { + fn into_plist_dict(self) -> plist::Dictionary { + self + } +} + +impl IntoPlistDict for plist::Value { + fn into_plist_dict(self) -> plist::Dictionary { match self { - Some(value) => value.to_plist_value(), - None => plist::Value::String("".to_string()), // or however you want to handle None + plist::Value::Dictionary(d) => d, + other => panic!("plist :< expects a dictionary, got {other:?}"), } } } +impl IntoPlistDict for std::collections::HashMap +where + K: Into, + V: PlistConvertible, +{ + fn into_plist_dict(self) -> plist::Dictionary { + let mut d = plist::Dictionary::new(); + for (k, v) in self { + d.insert(k.into(), v.to_plist_value()); + } + d + } +} + +impl IntoPlistDict for std::collections::BTreeMap +where + K: Into, + V: PlistConvertible, +{ + fn into_plist_dict(self) -> plist::Dictionary { + let mut d = plist::Dictionary::new(); + for (k, v) in self { + d.insert(k.into(), v.to_plist_value()); + } + d + } +} + +// Optional version: T or Option. +pub trait MaybeIntoPlistDict { + fn into_option_plist_dict(self) -> Option; +} +impl MaybeIntoPlistDict for T { + fn into_option_plist_dict(self) -> Option { + Some(self.into_plist_dict()) + } +} +impl MaybeIntoPlistDict for Option { + fn into_option_plist_dict(self) -> Option { + self.map(|t| t.into_plist_dict()) + } +} + +#[doc(hidden)] +pub fn maybe_into_dict(v: T) -> Option { + v.into_option_plist_dict() +} + #[cfg(test)] mod tests { #[test] @@ -440,7 +625,7 @@ mod tests { "name": "test", "count": 42, "active": true, - "items": ["a", "b", "c"] + "items": ["a", ?"b", "c"] }); if let plist::Value::Dictionary(dict) = value { @@ -460,11 +645,24 @@ mod tests { let name = "dynamic"; let count = 100; let items = vec!["x", "y"]; + let none: Option = None; + let to_merge = plist!({ + "reee": "cool beans" + }); + let maybe_merge = Some(plist!({ + "yeppers": "what did I say about yeppers", + "replace me": 2, + })); let value = plist!({ "name": name, "count": count, - "items": items + "items": items, + "omit me":? none, + "keep me":? Some(123), + "replace me": 1, + :< to_merge, + : Date: Mon, 11 Aug 2025 16:40:04 -0600 Subject: [PATCH 037/126] Add companion proxy support --- Cargo.lock | 159 +----------------------- idevice/Cargo.toml | 4 +- idevice/src/lib.rs | 8 ++ idevice/src/services/companion_proxy.rs | 150 ++++++++++++++++++++++ idevice/src/services/mod.rs | 2 + tools/Cargo.toml | 12 +- tools/src/companion_proxy.rs | 150 ++++++++++++++++++++++ 7 files changed, 326 insertions(+), 159 deletions(-) create mode 100644 idevice/src/services/companion_proxy.rs create mode 100644 tools/src/companion_proxy.rs diff --git a/Cargo.lock b/Cargo.lock index ccb56be..0d67243 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -266,33 +266,13 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" -[[package]] -name = "c2rust-bitfields" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "367e5d1b30f28be590b6b3868da1578361d29d9bfac516d22f497d28ed7c9055" -dependencies = [ - "c2rust-bitfields-derive 0.19.0", -] - [[package]] name = "c2rust-bitfields" version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46dc7d2bffa0d0b3d47eb2dc69973466858281446c2ac9f6d8a10e92ab1017df" dependencies = [ - "c2rust-bitfields-derive 0.20.0", -] - -[[package]] -name = "c2rust-bitfields-derive" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a279db9c50c4024eeca1a763b6e0f033848ce74e83e47454bcf8a8a98f7b0b56" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", + "c2rust-bitfields-derive", ] [[package]] @@ -866,17 +846,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "getifaddrs" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ba121d81ab5ea05b0cd5858516266800bf965531a794f7ac58e3eeb804f364f" -dependencies = [ - "bitflags", - "libc", - "windows-sys 0.59.0", -] - [[package]] name = "getifaddrs" version = "0.2.0" @@ -1166,7 +1135,7 @@ dependencies = [ "thiserror 2.0.12", "tokio", "tokio-rustls", - "tun-rs 2.5.3", + "tun-rs", "uuid", "x509-cert", ] @@ -1198,7 +1167,6 @@ dependencies = [ "plist", "sha2", "tokio", - "tun-rs 1.5.0", "ureq", "uuid", ] @@ -2645,34 +2613,6 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" -[[package]] -name = "tun-rs" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53141e64197ff7e758b8152615e50bb4a3b18c970738876e7906d31f242c7d6e" -dependencies = [ - "bitflags", - "blocking", - "byteorder", - "bytes", - "c2rust-bitfields 0.19.0", - "cfg-if", - "encoding_rs", - "getifaddrs 0.1.5", - "ipnet", - "libc", - "libloading", - "log", - "mac_address", - "nix 0.29.0", - "scopeguard", - "thiserror 2.0.12", - "tokio", - "windows-sys 0.59.0", - "winreg 0.52.0", - "wintun-bindings", -] - [[package]] name = "tun-rs" version = "2.5.3" @@ -2683,9 +2623,9 @@ dependencies = [ "blocking", "byteorder", "bytes", - "c2rust-bitfields 0.20.0", + "c2rust-bitfields", "encoding_rs", - "getifaddrs 0.2.0", + "getifaddrs", "ipnet", "libc", "libloading", @@ -2697,7 +2637,7 @@ dependencies = [ "tokio", "widestring", "windows-sys 0.60.2", - "winreg 0.55.0", + "winreg", ] [[package]] @@ -3026,15 +2966,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] - [[package]] name = "windows-sys" version = "0.52.0" @@ -3062,21 +2993,6 @@ dependencies = [ "windows-targets 0.53.3", ] -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] - [[package]] name = "windows-targets" version = "0.52.6" @@ -3110,12 +3026,6 @@ dependencies = [ "windows_x86_64_msvc 0.53.0", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -3128,12 +3038,6 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -3146,12 +3050,6 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -3176,12 +3074,6 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -3194,12 +3086,6 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -3212,12 +3098,6 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -3230,12 +3110,6 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -3257,16 +3131,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "winreg" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" -dependencies = [ - "cfg-if", - "windows-sys 0.48.0", -] - [[package]] name = "winreg" version = "0.55.0" @@ -3277,19 +3141,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "wintun-bindings" -version = "0.7.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b88303b411e20a1319b368dcd04db1480003ed46ac35193e139f542720b15fbf" -dependencies = [ - "c2rust-bitfields 0.20.0", - "libloading", - "log", - "thiserror 2.0.12", - "windows-sys 0.60.2", -] - [[package]] name = "wit-bindgen-rt" version = "0.39.0" diff --git a/idevice/Cargo.toml b/idevice/Cargo.toml index 765a3b5..113cc75 100644 --- a/idevice/Cargo.toml +++ b/idevice/Cargo.toml @@ -3,7 +3,7 @@ name = "idevice" description = "A Rust library to interact with services on iOS devices." authors = ["Jackson Coxson"] version = "0.1.37" -edition = "2021" +edition = "2024" license = "MIT" documentation = "https://docs.rs/idevice" repository = "https://github.com/jkcoxson/idevice" @@ -62,6 +62,7 @@ ring = ["rustls/ring", "tokio-rustls/ring"] afc = ["dep:chrono"] amfi = [] +companion_proxy = [] core_device = ["xpc", "dep:uuid"] core_device_proxy = ["dep:serde_json", "dep:json", "dep:byteorder"] crashreportcopymobile = ["afc"] @@ -95,6 +96,7 @@ xpc = ["dep:indexmap", "dep:uuid"] full = [ "afc", "amfi", + "companion_proxy", "core_device", "core_device_proxy", "crashreportcopymobile", diff --git a/idevice/src/lib.rs b/idevice/src/lib.rs index 9320b91..b9fca66 100644 --- a/idevice/src/lib.rs +++ b/idevice/src/lib.rs @@ -660,6 +660,10 @@ pub enum IdeviceError { FfiInvalidString = -61, #[error("buffer passed is too small - needs {0}, got {1}")] FfiBufferTooSmall(usize, usize) = -62, + #[error("unsupported watch key")] + UnsupportedWatchKey = -63, + #[error("malformed command")] + MalformedCommand = -64, } impl IdeviceError { @@ -683,6 +687,8 @@ impl IdeviceError { "UserDeniedPairing" => Some(Self::UserDeniedPairing), #[cfg(feature = "pair")] "PasswordProtected" => Some(Self::PasswordProtected), + "UnsupportedWatchKey" => Some(Self::UnsupportedWatchKey), + "MalformedCommand" => Some(Self::MalformedCommand), "InternalError" => { let detailed_error = context .get("DetailedError") @@ -806,6 +812,8 @@ impl IdeviceError { IdeviceError::FfiInvalidArg => -60, IdeviceError::FfiInvalidString => -61, IdeviceError::FfiBufferTooSmall(_, _) => -62, + IdeviceError::UnsupportedWatchKey => -63, + IdeviceError::MalformedCommand => -64, } } } diff --git a/idevice/src/services/companion_proxy.rs b/idevice/src/services/companion_proxy.rs new file mode 100644 index 0000000..462966d --- /dev/null +++ b/idevice/src/services/companion_proxy.rs @@ -0,0 +1,150 @@ +//! Companion Proxy is Apple's bridge to connect to the Apple Watch + +use log::warn; + +use crate::{Idevice, IdeviceError, IdeviceService, RsdService, obf}; + +pub struct CompanionProxy { + idevice: Idevice, +} + +pub struct CompanionProxyStream { + proxy: CompanionProxy, +} + +impl IdeviceService for CompanionProxy { + fn service_name() -> std::borrow::Cow<'static, str> { + obf!("com.apple.companion_proxy") + } + + async fn from_stream(idevice: Idevice) -> Result { + Ok(Self::new(idevice)) + } +} + +impl RsdService for CompanionProxy { + fn rsd_service_name() -> std::borrow::Cow<'static, str> { + obf!("com.apple.companion_proxy.shim.remote") + } + + async fn from_stream(stream: Box) -> Result { + let mut idevice = Idevice::new(stream, ""); + idevice.rsd_checkin().await?; + Ok(Self::new(idevice)) + } +} + +impl CompanionProxy { + pub fn new(idevice: Idevice) -> Self { + Self { idevice } + } + + pub async fn get_device_registry(&mut self) -> Result, IdeviceError> { + let command = crate::plist!({ + "Command": "GetDeviceRegistry" + }); + + self.idevice.send_plist(command).await?; + let res = self.idevice.read_plist().await?; + let list = match res.get("PairedDevicesArray").and_then(|x| x.as_array()) { + Some(l) => l, + None => { + warn!("Didn't get PairedDevicesArray array"); + return Err(IdeviceError::UnexpectedResponse); + } + }; + + let mut res = Vec::new(); + for l in list { + if let plist::Value::String(l) = l { + res.push(l.to_owned()); + } + } + + Ok(res) + } + + pub async fn listen_for_devices(mut self) -> Result { + let command = crate::plist!({ + "Command": "StartListeningForDevices" + }); + self.idevice.send_plist(command).await?; + + Ok(CompanionProxyStream { proxy: self }) + } + + pub async fn get_value( + &mut self, + udid: impl Into, + key: impl Into, + ) -> Result { + let udid = udid.into(); + let key = key.into(); + let command = crate::plist!({ + "Command": "GetValueFromRegistry", + "GetValueGizmoUDIDKey": udid, + "GetValueKeyKey": key.clone() + }); + self.idevice.send_plist(command).await?; + let mut res = self.idevice.read_plist().await?; + if let Some(v) = res + .remove("RetrievedValueDictionary") + .and_then(|x| x.into_dictionary()) + .and_then(|mut x| x.remove(&key)) + { + Ok(v) + } else { + Err(IdeviceError::NotFound) + } + } + + pub async fn start_forwarding_service_port( + &mut self, + port: u16, + service_name: Option<&str>, + options: Option, + ) -> Result { + let command = crate::plist!({ + "Command": "StartForwardingServicePort", + "GizmoRemotePortNumber": port, + "IsServiceLowPriority": false, + "PreferWifi": false, + "ForwardedServiceName":? service_name, + : Result<(), IdeviceError> { + let command = crate::plist!({ + "Command": "StopForwardingServicePort", + "GizmoRemotePortNumber": port + }); + + self.idevice.send_plist(command).await?; + let res = self.idevice.read_plist().await?; + if let Some(c) = res.get("Command").and_then(|x| x.as_string()) + && (c == "ComandSuccess" || c == "CommandSuccess") + // Apple you spelled this wrong, adding the right spelling just in case you fix it smh + { + Ok(()) + } else { + Err(IdeviceError::UnexpectedResponse) + } + } +} + +impl CompanionProxyStream { + pub async fn next(&mut self) -> Result { + self.proxy.idevice.read_plist().await + } +} diff --git a/idevice/src/services/mod.rs b/idevice/src/services/mod.rs index 17bdcb2..f0fb2c6 100644 --- a/idevice/src/services/mod.rs +++ b/idevice/src/services/mod.rs @@ -2,6 +2,8 @@ pub mod afc; #[cfg(feature = "amfi")] pub mod amfi; +#[cfg(feature = "companion_proxy")] +pub mod companion_proxy; #[cfg(feature = "core_device")] pub mod core_device; #[cfg(feature = "core_device_proxy")] diff --git a/tools/Cargo.toml b/tools/Cargo.toml index 049c48a..a2dc6ac 100644 --- a/tools/Cargo.toml +++ b/tools/Cargo.toml @@ -25,9 +25,9 @@ path = "src/instproxy.rs" name = "mounter" path = "src/mounter.rs" -[[bin]] -name = "core_device_proxy_tun" -path = "src/core_device_proxy_tun.rs" +# [[bin]] +# name = "core_device_proxy_tun" +# path = "src/core_device_proxy_tun.rs" [[bin]] name = "idevice_id" @@ -93,12 +93,16 @@ path = "src/lockdown.rs" name = "restore_service" path = "src/restore_service.rs" +[[bin]] +name = "companion_proxy" +path = "src/companion_proxy.rs" + [dependencies] idevice = { path = "../idevice", features = ["full"], default-features = false } tokio = { version = "1.43", features = ["full"] } log = { version = "0.4" } env_logger = { version = "0.11" } -tun-rs = { version = "1.5", features = ["async"] } +# tun-rs = { version = "1.5", features = ["async"] } sha2 = { version = "0.10" } ureq = { version = "3" } clap = { version = "4.5" } diff --git a/tools/src/companion_proxy.rs b/tools/src/companion_proxy.rs new file mode 100644 index 0000000..4c1df7c --- /dev/null +++ b/tools/src/companion_proxy.rs @@ -0,0 +1,150 @@ +// Jackson Coxson + +use clap::{arg, Arg, Command}; +use idevice::{ + companion_proxy::CompanionProxy, core_device_proxy::CoreDeviceProxy, pretty_print_dictionary, + pretty_print_plist, rsd::RsdHandshake, IdeviceService, RsdService, +}; + +mod common; + +#[tokio::main] +async fn main() { + env_logger::init(); + + let matches = Command::new("companion_proxy") + .about("Apple Watch things") + .arg( + Arg::new("host") + .long("host") + .value_name("HOST") + .help("IP address of the device"), + ) + .arg( + Arg::new("pairing_file") + .long("pairing-file") + .value_name("PATH") + .help("Path to the pairing file"), + ) + .arg( + Arg::new("udid") + .value_name("UDID") + .help("UDID of the device (overrides host/pairing file)") + .index(1), + ) + .arg( + Arg::new("about") + .long("about") + .help("Show about information") + .action(clap::ArgAction::SetTrue), + ) + .subcommand(Command::new("list").about("List the companions on the device")) + .subcommand(Command::new("listen").about("Listen for devices")) + .subcommand( + Command::new("get") + .about("Gets a value") + .arg(arg!(-d --device_udid "the device udid to get from").required(true)) + .arg(arg!(-v --value "the value to get").required(true)), + ) + .subcommand( + Command::new("start") + .about("Starts a service") + .arg(arg!(-p --port "the port").required(true)) + .arg(arg!(-n --name "the optional service name").required(false)), + ) + .subcommand( + Command::new("stop") + .about("Starts a service") + .arg(arg!(-p --port "the port").required(true)), + ) + .get_matches(); + + if matches.get_flag("about") { + println!("companion_proxy"); + println!("Copyright (c) 2025 Jackson Coxson"); + return; + } + + let udid = matches.get_one::("udid"); + let host = matches.get_one::("host"); + let pairing_file = matches.get_one::("pairing_file"); + + let provider = match common::get_provider(udid, host, pairing_file, "amfi-jkcoxson").await { + Ok(p) => p, + Err(e) => { + eprintln!("{e}"); + return; + } + }; + + let proxy = CoreDeviceProxy::connect(&*provider) + .await + .expect("no core_device_proxy"); + let rsd_port = proxy.handshake.server_rsd_port; + let mut provider = proxy + .create_software_tunnel() + .expect("no tunnel") + .to_async_handle(); + let mut handshake = RsdHandshake::new(provider.connect(rsd_port).await.unwrap()) + .await + .unwrap(); + let mut proxy = CompanionProxy::connect_rsd(&mut provider, &mut handshake) + .await + .expect("no companion proxy connect"); + + // let mut proxy = CompanionProxy::connect(&*provider) + // .await + // .expect("Failed to connect to companion proxy"); + + if matches.subcommand_matches("list").is_some() { + proxy.get_device_registry().await.expect("Failed to show"); + } else if matches.subcommand_matches("listen").is_some() { + let mut stream = proxy.listen_for_devices().await.expect("Failed to show"); + while let Ok(v) = stream.next().await { + println!("{}", pretty_print_dictionary(&v)); + } + } else if let Some(matches) = matches.subcommand_matches("get") { + let key = matches.get_one::("value").expect("no value passed"); + let udid = matches + .get_one::("device_udid") + .expect("no AW udid passed"); + + match proxy.get_value(udid, key).await { + Ok(value) => { + println!("{}", pretty_print_plist(&value)); + } + Err(e) => { + eprintln!("Error getting value: {e}"); + } + } + } else if let Some(matches) = matches.subcommand_matches("start") { + let port: u16 = matches + .get_one::("port") + .expect("no port passed") + .parse() + .expect("not a number"); + let name = matches.get_one::("name").map(|x| x.as_str()); + + match proxy.start_forwarding_service_port(port, name, None).await { + Ok(value) => { + println!("started on port {value}"); + } + Err(e) => { + eprintln!("Error starting: {e}"); + } + } + } else if let Some(matches) = matches.subcommand_matches("stop") { + let port: u16 = matches + .get_one::("port") + .expect("no port passed") + .parse() + .expect("not a number"); + + if let Err(e) = proxy.stop_forwarding_service_port(port).await { + eprintln!("Error starting: {e}"); + } + } else { + eprintln!("Invalid usage, pass -h for help"); + } + return; +} From 4c9977157b9870d3769119b95c36baad6bca3c78 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Tue, 12 Aug 2025 07:52:44 -0600 Subject: [PATCH 038/126] Use downloaded plist.h --- .gitignore | 1 + Cargo.lock | 1 + ffi/Cargo.toml | 1 + ffi/build.rs | 12 +++++++++++- ffi/examples/lockdownd.c | 4 ++-- 5 files changed, 16 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index f5383ee..9a67810 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ Image.dmg.trustcache .DS_Store *.pcap idevice.h +plist.h /ffi/examples/build /.cache /ffi/examples/.cache diff --git a/Cargo.lock b/Cargo.lock index 0d67243..fa93e66 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1153,6 +1153,7 @@ dependencies = [ "plist_ffi", "simplelog", "tokio", + "ureq", ] [[package]] diff --git a/ffi/Cargo.toml b/ffi/Cargo.toml index 29756ab..f9fc7a9 100644 --- a/ffi/Cargo.toml +++ b/ffi/Cargo.toml @@ -72,6 +72,7 @@ default = ["full", "aws-lc"] [build-dependencies] cbindgen = "0.29.0" +ureq = "3" [lib] crate-type = ["staticlib"] diff --git a/ffi/build.rs b/ffi/build.rs index b777e71..bbf09be 100644 --- a/ffi/build.rs +++ b/ffi/build.rs @@ -12,10 +12,20 @@ fn main() { ) .with_language(cbindgen::Language::C) .with_sys_include("sys/socket.h") - .with_sys_include("plist/plist.h") + .with_include("plist.h") .generate() .expect("Unable to generate bindings") .write_to_file("idevice.h"); + // download plist.h + let h = ureq::get("https://raw.githubusercontent.com/libimobiledevice/libplist/refs/heads/master/include/plist/plist.h") + .call() + .expect("failed to download plist.h"); + let h = h + .into_body() + .read_to_string() + .expect("failed to get string content"); + std::fs::write("plist.h", h).expect("failed to save to string"); + println!("cargo:rustc-link-arg=-lplist-2.0"); } diff --git a/ffi/examples/lockdownd.c b/ffi/examples/lockdownd.c index ad92b39..6658d15 100644 --- a/ffi/examples/lockdownd.c +++ b/ffi/examples/lockdownd.c @@ -1,7 +1,7 @@ // Jackson Coxson #include "idevice.h" -#include "plist/plist.h" +#include "plist.h" #include #include #include @@ -122,7 +122,7 @@ int main() { // Get all values plist_t all_values = NULL; - err = lockdownd_get_all_values(client, NULL, &all_values); + err = lockdownd_get_value(client, NULL, NULL, &all_values); if (err != NULL) { fprintf(stderr, "Failed to get all values: [%d] %s", err->code, err->message); From 0e4f12f0bf766905865e8f420c23376e6e4b21ae Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Tue, 12 Aug 2025 08:05:02 -0600 Subject: [PATCH 039/126] Append plist.h to idevice header --- ffi/build.rs | 10 +++++----- ffi/src/lib.rs | 4 ++++ 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/ffi/build.rs b/ffi/build.rs index bbf09be..e92a349 100644 --- a/ffi/build.rs +++ b/ffi/build.rs @@ -1,6 +1,6 @@ // Jackson Coxson -use std::env; +use std::{env, fs::OpenOptions, io::Write}; fn main() { let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); @@ -12,7 +12,6 @@ fn main() { ) .with_language(cbindgen::Language::C) .with_sys_include("sys/socket.h") - .with_include("plist.h") .generate() .expect("Unable to generate bindings") .write_to_file("idevice.h"); @@ -25,7 +24,8 @@ fn main() { .into_body() .read_to_string() .expect("failed to get string content"); - std::fs::write("plist.h", h).expect("failed to save to string"); - - println!("cargo:rustc-link-arg=-lplist-2.0"); + let mut f = OpenOptions::new().append(true).open("idevice.h").unwrap(); + f.write_all(b"\n\n\n").unwrap(); + f.write_all(&h.into_bytes()) + .expect("failed to append plist.h"); } diff --git a/ffi/src/lib.rs b/ffi/src/lib.rs index 04ed09f..65c32ab 100644 --- a/ffi/src/lib.rs +++ b/ffi/src/lib.rs @@ -73,6 +73,10 @@ pub struct ReadWriteOpaque { pub struct IdeviceHandle(pub Idevice); pub struct IdeviceSocketHandle(IdeviceSocket); +/// Stub to avoid header problems +#[allow(non_camel_case_types)] +pub type plist_t = *mut std::ffi::c_void; + // https://github.com/mozilla/cbindgen/issues/539 #[allow(non_camel_case_types, unused)] struct sockaddr; From 5ee385c95c958175cc2bf4db862cd8ea26f89bf4 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Tue, 12 Aug 2025 08:19:31 -0600 Subject: [PATCH 040/126] Fix cargo clippy warnings --- ffi/src/core_device/app_service.rs | 8 ++++---- ffi/src/process_control.rs | 8 ++++---- idevice/src/lib.rs | 4 ++-- idevice/src/services/afc/mod.rs | 8 ++++---- .../src/services/core_device/app_service.rs | 4 ++-- idevice/src/services/core_device/mod.rs | 4 ++-- idevice/src/services/dvt/remote_server.rs | 12 ++++++------ idevice/src/services/installation_proxy.rs | 18 +++++++++--------- idevice/src/services/mobile_image_mounter.rs | 7 +++---- idevice/src/tss.rs | 18 +++++++++--------- idevice/src/xpc/http2/mod.rs | 2 +- idevice/src/xpc/mod.rs | 15 +++++++-------- tools/Cargo.toml | 2 +- tools/src/common.rs | 8 +++++--- 14 files changed, 59 insertions(+), 59 deletions(-) diff --git a/ffi/src/core_device/app_service.rs b/ffi/src/core_device/app_service.rs index 5aac7a4..908bd26 100644 --- a/ffi/src/core_device/app_service.rs +++ b/ffi/src/core_device/app_service.rs @@ -329,10 +329,10 @@ pub unsafe extern "C" fn app_service_launch_app( if !argv.is_null() && argc > 0 { let argv_slice = unsafe { std::slice::from_raw_parts(argv, argc) }; for &arg in argv_slice { - if !arg.is_null() { - if let Ok(arg_str) = unsafe { CStr::from_ptr(arg) }.to_str() { - args.push(arg_str); - } + if !arg.is_null() + && let Ok(arg_str) = unsafe { CStr::from_ptr(arg) }.to_str() + { + args.push(arg_str); } } } diff --git a/ffi/src/process_control.rs b/ffi/src/process_control.rs index 2ed84b9..b771125 100644 --- a/ffi/src/process_control.rs +++ b/ffi/src/process_control.rs @@ -105,10 +105,10 @@ pub unsafe extern "C" fn process_control_launch_app( for &env_var in env_vars_slice { if !env_var.is_null() { let env_var = unsafe { CStr::from_ptr(env_var) }; - if let Ok(env_var) = env_var.to_str() { - if let Some((key, value)) = env_var.split_once('=') { - env_dict.insert(key.to_string(), Value::String(value.to_string())); - } + if let Ok(env_var) = env_var.to_str() + && let Some((key, value)) = env_var.split_once('=') + { + env_dict.insert(key.to_string(), Value::String(value.to_string())); } } } diff --git a/idevice/src/lib.rs b/idevice/src/lib.rs index b9fca66..3313de8 100644 --- a/idevice/src/lib.rs +++ b/idevice/src/lib.rs @@ -25,7 +25,7 @@ pub use services::*; #[cfg(feature = "xpc")] pub use xpc::RemoteXpcClient; -use log::{debug, error, trace, warn}; +use log::{debug, error, trace}; use provider::{IdeviceProvider, RsdProvider}; use rustls::{crypto::CryptoProvider, pki_types::ServerName}; use std::{ @@ -443,7 +443,7 @@ impl Idevice { // My sanity while debugging the workspace crates are more important. debug!("Using ring crypto backend, because both were passed"); - warn!("Both ring && aws-lc are selected as idevice crypto backends!"); + log::warn!("Both ring && aws-lc are selected as idevice crypto backends!"); rustls::crypto::ring::default_provider() } }; diff --git a/idevice/src/services/afc/mod.rs b/idevice/src/services/afc/mod.rs index 634e379..c6319be 100644 --- a/idevice/src/services/afc/mod.rs +++ b/idevice/src/services/afc/mod.rs @@ -11,7 +11,7 @@ use log::warn; use opcode::{AfcFopenMode, AfcOpcode}; use packet::{AfcPacket, AfcPacketHeader}; -use crate::{obf, Idevice, IdeviceError, IdeviceService}; +use crate::{Idevice, IdeviceError, IdeviceService, obf}; pub mod errors; pub mod file; @@ -376,11 +376,11 @@ impl AfcClient { /// /// # Returns /// A `FileDescriptor` struct for the opened file - pub async fn open( - &mut self, + pub async fn open<'f>( + &'f mut self, path: impl Into, mode: AfcFopenMode, - ) -> Result { + ) -> Result, IdeviceError> { let path = path.into(); let mut header_payload = (mode as u64).to_le_bytes().to_vec(); header_payload.extend(path.as_bytes()); diff --git a/idevice/src/services/core_device/app_service.rs b/idevice/src/services/core_device/app_service.rs index fc74960..d4a2952 100644 --- a/idevice/src/services/core_device/app_service.rs +++ b/idevice/src/services/core_device/app_service.rs @@ -3,7 +3,7 @@ use log::warn; use serde::Deserialize; -use crate::{obf, IdeviceError, ReadWrite, RsdService}; +use crate::{IdeviceError, ReadWrite, RsdService, obf}; use super::CoreDeviceServiceClient; @@ -128,7 +128,7 @@ pub struct IconUuid { pub classes: Vec, } -impl<'a, R: ReadWrite + 'a> AppServiceClient { +impl AppServiceClient { pub async fn new(stream: R) -> Result { Ok(Self { inner: CoreDeviceServiceClient::new(stream).await?, diff --git a/idevice/src/services/core_device/mod.rs b/idevice/src/services/core_device/mod.rs index e11fd76..6114a0e 100644 --- a/idevice/src/services/core_device/mod.rs +++ b/idevice/src/services/core_device/mod.rs @@ -4,8 +4,8 @@ use log::warn; use crate::{ - xpc::{self, XPCObject}, IdeviceError, ReadWrite, RemoteXpcClient, + xpc::{self, XPCObject}, }; mod app_service; @@ -17,7 +17,7 @@ pub struct CoreDeviceServiceClient { inner: RemoteXpcClient, } -impl<'a, R: ReadWrite + 'a> CoreDeviceServiceClient { +impl CoreDeviceServiceClient { pub async fn new(inner: R) -> Result { let mut client = RemoteXpcClient::new(inner).await?; client.do_handshake().await?; diff --git a/idevice/src/services/dvt/remote_server.rs b/idevice/src/services/dvt/remote_server.rs index 91e9684..c4111a8 100644 --- a/idevice/src/services/dvt/remote_server.rs +++ b/idevice/src/services/dvt/remote_server.rs @@ -55,8 +55,8 @@ use log::{debug, warn}; use tokio::io::AsyncWriteExt; use crate::{ - dvt::message::{Aux, AuxValue, Message, MessageHeader, PayloadHeader}, IdeviceError, ReadWrite, + dvt::message::{Aux, AuxValue, Message, MessageHeader, PayloadHeader}, }; /// Message type identifier for instruments protocol @@ -112,7 +112,7 @@ impl RemoteServerClient { } /// Returns a handle to the root channel (channel 0) - pub fn root_channel(&mut self) -> Channel { + pub fn root_channel<'c>(&'c mut self) -> Channel<'c, R> { Channel { client: self, channel: 0, @@ -131,10 +131,10 @@ impl RemoteServerClient { /// # Errors /// * `IdeviceError::UnexpectedResponse` if server responds with unexpected data /// * Other IO or serialization errors - pub async fn make_channel( - &mut self, + pub async fn make_channel<'c>( + &'c mut self, identifier: impl Into, - ) -> Result, IdeviceError> { + ) -> Result, IdeviceError> { let code = self.new_channel; self.new_channel += 1; @@ -164,7 +164,7 @@ impl RemoteServerClient { self.build_channel(code) } - fn build_channel(&mut self, code: u32) -> Result, IdeviceError> { + fn build_channel<'c>(&'c mut self, code: u32) -> Result, IdeviceError> { Ok(Channel { client: self, channel: code, diff --git a/idevice/src/services/installation_proxy.rs b/idevice/src/services/installation_proxy.rs index b7111f2..9cf28fb 100644 --- a/idevice/src/services/installation_proxy.rs +++ b/idevice/src/services/installation_proxy.rs @@ -8,7 +8,7 @@ use std::collections::HashMap; use log::warn; use plist::Dictionary; -use crate::{obf, Idevice, IdeviceError, IdeviceService}; +use crate::{Idevice, IdeviceError, IdeviceService, obf}; /// Client for interacting with the iOS installation proxy service /// @@ -376,10 +376,10 @@ impl InstallationProxyClient { break; } - if let Some(status) = res.get("Status").and_then(|x| x.as_string()) { - if status == "Complete" { - break; - } + if let Some(status) = res.get("Status").and_then(|x| x.as_string()) + && status == "Complete" + { + break; } } Ok(values) @@ -424,10 +424,10 @@ impl InstallationProxyClient { callback((c, state.clone())).await; } - if let Some(c) = res.remove("Status").and_then(|x| x.into_string()) { - if c == "Complete" { - break; - } + if let Some(c) = res.remove("Status").and_then(|x| x.into_string()) + && c == "Complete" + { + break; } } Ok(()) diff --git a/idevice/src/services/mobile_image_mounter.rs b/idevice/src/services/mobile_image_mounter.rs index 8ec22d3..d0735bd 100644 --- a/idevice/src/services/mobile_image_mounter.rs +++ b/idevice/src/services/mobile_image_mounter.rs @@ -9,7 +9,7 @@ use log::debug; -use crate::{obf, Idevice, IdeviceError, IdeviceService}; +use crate::{Idevice, IdeviceError, IdeviceService, obf}; use sha2::{Digest, Sha384}; #[cfg(feature = "tss")] @@ -710,10 +710,9 @@ impl ImageMounter { .and_then(|l| l.as_dictionary()) .and_then(|l| l.get("Info")) .and_then(|i| i.as_dictionary()) + && let Some(plist::Value::Array(rules)) = info.get("RestoreRequestRules") { - if let Some(plist::Value::Array(rules)) = info.get("RestoreRequestRules") { - crate::tss::apply_restore_request_rules(&mut tss_entry, ¶meters, rules); - } + crate::tss::apply_restore_request_rules(&mut tss_entry, ¶meters, rules); } if manifest_item.get("Digest").is_none() { diff --git a/idevice/src/tss.rs b/idevice/src/tss.rs index d240ecc..df7491c 100644 --- a/idevice/src/tss.rs +++ b/idevice/src/tss.rs @@ -8,7 +8,7 @@ use log::{debug, warn}; use plist::Value; -use crate::{util::plist_to_xml_bytes, IdeviceError}; +use crate::{IdeviceError, util::plist_to_xml_bytes}; /// TSS client version string sent in requests const TSS_CLIENT_VERSION_STRING: &str = "libauthinstall-1033.0.2"; @@ -172,15 +172,15 @@ pub fn apply_restore_request_rules( for (key, value) in actions { // Skip special values (255 typically means "ignore") - if let Some(i) = value.as_unsigned_integer() { - if i == 255 { - continue; - } + if let Some(i) = value.as_unsigned_integer() + && i == 255 + { + continue; } - if let Some(i) = value.as_signed_integer() { - if i == 255 { - continue; - } + if let Some(i) = value.as_signed_integer() + && i == 255 + { + continue; } input.remove(key); // Explicitly remove before inserting diff --git a/idevice/src/xpc/http2/mod.rs b/idevice/src/xpc/http2/mod.rs index 5679c66..b6ff0b1 100644 --- a/idevice/src/xpc/http2/mod.rs +++ b/idevice/src/xpc/http2/mod.rs @@ -17,7 +17,7 @@ pub struct Http2Client { cache: HashMap>>, } -impl<'a, R: ReadWrite + 'a> Http2Client { +impl Http2Client { /// Writes the magic and inits the caches pub async fn new(mut inner: R) -> Result { inner.write_all(HTTP2_MAGIC).await?; diff --git a/idevice/src/xpc/mod.rs b/idevice/src/xpc/mod.rs index aac5308..c6fc217 100644 --- a/idevice/src/xpc/mod.rs +++ b/idevice/src/xpc/mod.rs @@ -17,15 +17,14 @@ const REPLY_CHANNEL: u32 = 3; pub struct RemoteXpcClient { h2_client: http2::Http2Client, root_id: u64, - reply_id: u64, + // reply_id: u64 // maybe not used? } -impl<'a, R: ReadWrite + 'a> RemoteXpcClient { +impl RemoteXpcClient { pub async fn new(socket: R) -> Result { Ok(Self { h2_client: http2::Http2Client::new(socket).await?, root_id: 1, - reply_id: 1, }) } @@ -86,11 +85,11 @@ impl<'a, R: ReadWrite + 'a> RemoteXpcClient { match msg.message { Some(msg) => { - if let Some(d) = msg.as_dictionary() { - if d.is_empty() { - msg_buffer.clear(); - continue; - } + if let Some(d) = msg.as_dictionary() + && d.is_empty() + { + msg_buffer.clear(); + continue; } break Ok(msg.to_plist()); } diff --git a/tools/Cargo.toml b/tools/Cargo.toml index a2dc6ac..17d5510 100644 --- a/tools/Cargo.toml +++ b/tools/Cargo.toml @@ -3,7 +3,7 @@ name = "idevice-tools" description = "Rust binary tools to interact with services on iOS devices." authors = ["Jackson Coxson"] version = "0.1.0" -edition = "2021" +edition = "2024" license = "MIT" documentation = "https://docs.rs/idevice" repository = "https://github.com/jkcoxson/idevice" diff --git a/tools/src/common.rs b/tools/src/common.rs index 8e54744..34be9e5 100644 --- a/tools/src/common.rs +++ b/tools/src/common.rs @@ -38,14 +38,16 @@ pub async fn get_provider( } }; Box::new(dev.to_provider(UsbmuxdAddr::from_env_var().unwrap(), label)) - } else if host.is_some() && pairing_file.is_some() { - let host = match IpAddr::from_str(host.unwrap()) { + } else if let Some(host) = host + && let Some(pairing_file) = pairing_file + { + let host = match IpAddr::from_str(host) { Ok(h) => h, Err(e) => { return Err(format!("Invalid host: {e:?}")); } }; - let pairing_file = match PairingFile::read_from_file(pairing_file.unwrap()) { + let pairing_file = match PairingFile::read_from_file(pairing_file) { Ok(p) => p, Err(e) => { return Err(format!("Unable to read pairing file: {e:?}")); From 9f6bde458d5c523199c55060c35ac0be7d562199 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Tue, 12 Aug 2025 08:20:18 -0600 Subject: [PATCH 041/126] Remove plist.h from lockdown example --- ffi/examples/lockdownd.c | 1 - 1 file changed, 1 deletion(-) diff --git a/ffi/examples/lockdownd.c b/ffi/examples/lockdownd.c index 6658d15..7b98775 100644 --- a/ffi/examples/lockdownd.c +++ b/ffi/examples/lockdownd.c @@ -1,7 +1,6 @@ // Jackson Coxson #include "idevice.h" -#include "plist.h" #include #include #include From 70fd0648f5cbfbd9084300b8a3e8629e002b7f1b Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Tue, 12 Aug 2025 08:24:56 -0600 Subject: [PATCH 042/126] remove plist.h from mounter example --- ffi/examples/mounter.c | 1 - 1 file changed, 1 deletion(-) diff --git a/ffi/examples/mounter.c b/ffi/examples/mounter.c index 17870e9..79bc5fd 100644 --- a/ffi/examples/mounter.c +++ b/ffi/examples/mounter.c @@ -1,7 +1,6 @@ // Jackson Coxson #include "idevice.h" -#include "plist/plist.h" #include #include #include From 1f0b3bae00a51ce87f0b856cc35cd978ea28d97d Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Tue, 12 Aug 2025 08:35:45 -0600 Subject: [PATCH 043/126] rustup install CI targets --- .github/workflows/ci.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 88ecc35..d8b600b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,6 +26,12 @@ jobs: key: macos-cargo-${{ hashFiles('**/Cargo.lock', 'justfile') }} restore-keys: macos-cargo- + - name: Install rustup targets + run: | + rustup target add aarch64-apple-ios && rustup target add x86_64-apple-ios && \ + rustup target add aarch64-apple-ios-sim && rustup target add aarch64-apple-darwin && \ + rustup target add x86_64-apple-darwin + - name: Build all Apple targets and examples/tools run: | just macos-ci-check From 59f8058d3daa20940b8dcae221d6f62e43ae3aa2 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Tue, 12 Aug 2025 09:04:54 -0600 Subject: [PATCH 044/126] Upload the correct folders --- .github/workflows/ci.yml | 82 ++++++++++++++++++++++++++++++++++--- cpp/examples/CMakeLists.txt | 1 + ffi/examples/CMakeLists.txt | 1 + justfile | 1 + 4 files changed, 79 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d8b600b..9af92a0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,13 +59,51 @@ jobs: uses: actions/upload-artifact@v4 with: name: idevice-c-examples-macos - path: cpp/examples/build-c/* + path: cpp/examples/build/bin/* - name: Upload C++ examples/tools uses: actions/upload-artifact@v4 with: name: idevice-cpp-examples-macos - path: cpp/examples/build-cpp/* + path: cpp/examples/build/bin/* + + - name: Stage Rust tools (arm64) + shell: bash + run: | + mkdir -p dist/arm64 + find target/release -maxdepth 1 -type f -exec sh -c ' + for f in "$@"; do + if file "$f" | grep -Eq "Mach-O .* executable|ELF .* executable"; then + cp "$f" dist/arm64/ + fi + done + ' sh {} + + + - name: Upload Rust tools (arm64) + uses: actions/upload-artifact@v4 + with: + name: idevice-tools-macos-arm + path: dist/arm64/* + if-no-files-found: error + + - name: Stage Rust tools (x64) + shell: bash + run: | + mkdir -p dist/x64 + find target/x86_64-apple-darwin/release -maxdepth 1 -type f -exec sh -c ' + for f in "$@"; do + if file "$f" | grep -Eq "Mach-O .* executable|ELF .* executable"; then + cp "$f" dist/x64/ + fi + done + ' sh {} + + + - name: Upload Rust tools (x64) + uses: actions/upload-artifact@v4 + with: + name: idevice-tools-macos-intel + path: dist/x64/* + if-no-files-found: error linux: name: Linux Build @@ -112,13 +150,32 @@ jobs: uses: actions/upload-artifact@v4 with: name: idevice-c-examples-linux - path: cpp/examples/build-c/* + path: cpp/examples/build/bin/* - name: Upload C++ examples/tools uses: actions/upload-artifact@v4 with: name: idevice-cpp-examples-linux - path: cpp/examples/build-cpp/* + path: cpp/examples/build/bin/* + + - name: Stage Rust tools (linux) + shell: bash + run: | + mkdir -p dist + find target/release -maxdepth 1 -type f -exec sh -c ' + for f in "$@"; do + if file "$f" | grep -Eq "ELF .* executable"; then + cp "$f" dist/ + fi + done + ' sh {} + + + - name: Upload Rust tools + uses: actions/upload-artifact@v4 + with: + name: idevice-tools-linux + path: dist/* + if-no-files-found: error windows: name: Windows Build @@ -161,10 +218,23 @@ jobs: uses: actions/upload-artifact@v4 with: name: idevice-c-examples-windows - path: cpp\examples\build-c\* + path: cpp\examples\build\bin\* - name: Upload C++ examples/tools uses: actions/upload-artifact@v4 with: name: idevice-cpp-examples-windows - path: cpp\examples\build-cpp\* + path: cpp\examples\build\bin\* + + - name: Stage Rust tools (windows) + shell: pwsh + run: | + New-Item -ItemType Directory -Force -Path dist | Out-Null + Get-ChildItem target\release\*.exe -File | Copy-Item -Destination dist + + - name: Upload Rust tools + uses: actions/upload-artifact@v4 + with: + name: idevice-tools-windows + path: dist\*.exe + if-no-files-found: error diff --git a/cpp/examples/CMakeLists.txt b/cpp/examples/CMakeLists.txt index 63d607d..548a39f 100644 --- a/cpp/examples/CMakeLists.txt +++ b/cpp/examples/CMakeLists.txt @@ -10,6 +10,7 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) set(HEADER_FILE ${CMAKE_SOURCE_DIR}/../../ffi/idevice.h) set(STATIC_LIB ${CMAKE_SOURCE_DIR}/../../target/release/libidevice_ffi.a) set(EXAMPLES_DIR ${CMAKE_SOURCE_DIR}/../examples) +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) set(IDEVICE_CPP_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/../include) # cpp/include set(IDEVICE_FFI_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/../../ffi) # ffi/ diff --git a/ffi/examples/CMakeLists.txt b/ffi/examples/CMakeLists.txt index d68866f..e9d5567 100644 --- a/ffi/examples/CMakeLists.txt +++ b/ffi/examples/CMakeLists.txt @@ -8,6 +8,7 @@ project(IdeviceFFI C) set(HEADER_FILE ${CMAKE_SOURCE_DIR}/../idevice.h) set(STATIC_LIB ${CMAKE_SOURCE_DIR}/../../target/release/libidevice_ffi.a) set(EXAMPLES_DIR ${CMAKE_SOURCE_DIR}/../examples) +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) # Find all C example files file(GLOB EXAMPLE_SOURCES ${EXAMPLES_DIR}/*.c) diff --git a/justfile b/justfile index f339e34..924d8b7 100644 --- a/justfile +++ b/justfile @@ -6,6 +6,7 @@ check-features: ci-check: build-ffi-native build-tools-native build-cpp build-c cargo clippy --all-targets --all-features -- -D warnings macos-ci-check: ci-check xcframework + cd tools && cargo build --release --target x86_64-apple-darwin [working-directory: 'ffi'] build-ffi-native: From 4488ac3e73f819122844ccb9e98c1e12d81c66b5 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Tue, 12 Aug 2025 09:11:54 -0600 Subject: [PATCH 045/126] Install bindgen-cli in CI --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9af92a0..c07e33f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,7 +30,7 @@ jobs: run: | rustup target add aarch64-apple-ios && rustup target add x86_64-apple-ios && \ rustup target add aarch64-apple-ios-sim && rustup target add aarch64-apple-darwin && \ - rustup target add x86_64-apple-darwin + rustup target add x86_64-apple-darwin && cargo install --force --locked bindgen-cli - name: Build all Apple targets and examples/tools run: | From d7055a5c44777366ba941520c7d86be139a1c9d1 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Tue, 12 Aug 2025 09:14:46 -0600 Subject: [PATCH 046/126] Upload FFI for C binaries --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c07e33f..0cc7e47 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,7 +59,7 @@ jobs: uses: actions/upload-artifact@v4 with: name: idevice-c-examples-macos - path: cpp/examples/build/bin/* + path: ffi/examples/build/bin/* - name: Upload C++ examples/tools uses: actions/upload-artifact@v4 @@ -150,7 +150,7 @@ jobs: uses: actions/upload-artifact@v4 with: name: idevice-c-examples-linux - path: cpp/examples/build/bin/* + path: ffi/examples/build/bin/* - name: Upload C++ examples/tools uses: actions/upload-artifact@v4 @@ -218,7 +218,7 @@ jobs: uses: actions/upload-artifact@v4 with: name: idevice-c-examples-windows - path: cpp\examples\build\bin\* + path: ffi\examples\build\bin\* - name: Upload C++ examples/tools uses: actions/upload-artifact@v4 From 91ba38ce732bc2e3af601cf694248a4e4fb5d341 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Tue, 12 Aug 2025 09:33:20 -0600 Subject: [PATCH 047/126] Only Linux uploads the idevice header (CI) --- .github/workflows/ci.yml | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0cc7e47..fd6e7cb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,12 +43,6 @@ jobs: path: | target/*apple*/release/libidevice_ffi.a - - name: Upload headers - uses: actions/upload-artifact@v4 - with: - name: idevice-headers - path: ffi/idevice.h - - name: Upload macOS+iOS XCFramework uses: actions/upload-artifact@v4 with: @@ -208,12 +202,6 @@ jobs: name: libidevice-windows-a path: target\release\idevice_ffi.lib - - name: Upload headers - uses: actions/upload-artifact@v4 - with: - name: idevice-headers - path: ffi\idevice.h - - name: Upload C examples/tools uses: actions/upload-artifact@v4 with: From a19ce5d2904368af4cd9a2ab6f4efc7a99383b92 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Tue, 12 Aug 2025 09:57:22 -0600 Subject: [PATCH 048/126] Move xcframework bundle.zip to swift folder --- justfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/justfile b/justfile index 924d8b7..c834bfa 100644 --- a/justfile +++ b/justfile @@ -46,8 +46,8 @@ xcframework: apple-build -library swift/libs/idevice-macos.a -headers swift/include \ -output swift/IDevice.xcframework - zip -r bundle.zip swift/IDevice.xcframework - openssl dgst -sha256 bundle.zip + zip -r swift/bundle.zip swift/IDevice.xcframework + openssl dgst -sha256 swift/bundle.zip [working-directory: 'ffi'] apple-build: # requires a Mac From 90786e95777aa062c326715b4c5b31becec8ebee Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Tue, 12 Aug 2025 10:06:06 -0600 Subject: [PATCH 049/126] Add just to path on Windows CI --- .github/workflows/ci.yml | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fd6e7cb..1e6a93e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -177,10 +177,24 @@ jobs: steps: - uses: actions/checkout@v4 + # Install Rust (adds cargo/rustc to PATH) + - name: Install Rust (stable) + uses: dtolnay/rust-toolchain@stable + with: + targets: x86_64-pc-windows-msvc + + # Use Scoop to install just (reuse your existing scoop setup) - uses: MinoruSekine/setup-scoop@v4.0.2 with: - buckets: extras - apps: doxygen plantuml + buckets: main extras + apps: just doxygen plantuml + + # (Paranoid) ensure shims and cargo bin are on PATH for subsequent steps + - name: Ensure tools on PATH + shell: pwsh + run: | + echo "$env:USERPROFILE\scoop\shims" >> $env:GITHUB_PATH + echo "$env:USERPROFILE\.cargo\bin" >> $env:GITHUB_PATH - name: Cache Cargo uses: actions/cache@v4 @@ -193,8 +207,7 @@ jobs: restore-keys: windows-cargo- - name: Build Rust and examples/tools - run: | - just ci-check + run: just ci-check - name: Upload static library uses: actions/upload-artifact@v4 From 618500fd0c0a6c2a680121e5188707e9ca8d0b2e Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Tue, 12 Aug 2025 11:35:13 -0600 Subject: [PATCH 050/126] Use platform-independent socket for FFI Windows is truly awful Remove config.toml --- Cargo.lock | 1 + ffi/Cargo.toml | 3 + ffi/build.rs | 27 ++++++- ffi/src/lib.rs | 22 ++--- ffi/src/provider.rs | 32 ++++---- ffi/src/usbmuxd.rs | 50 +++++++----- ffi/src/util.rs | 190 ++++++++++++++++++++++++++++++++++---------- 7 files changed, 235 insertions(+), 90 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fa93e66..4ba49d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1154,6 +1154,7 @@ dependencies = [ "simplelog", "tokio", "ureq", + "windows-sys 0.60.2", ] [[package]] diff --git a/ffi/Cargo.toml b/ffi/Cargo.toml index f9fc7a9..f1ca3f9 100644 --- a/ffi/Cargo.toml +++ b/ffi/Cargo.toml @@ -14,6 +14,9 @@ libc = "0.2.171" plist = "1.7.1" plist_ffi = "0.1.3" +[target.'cfg(windows)'.dependencies] +windows-sys = { version = "0.60", features = ["Win32_Networking_WinSock"] } + [features] aws-lc = ["idevice/aws-lc"] ring = ["idevice/ring"] diff --git a/ffi/build.rs b/ffi/build.rs index e92a349..d26e3b8 100644 --- a/ffi/build.rs +++ b/ffi/build.rs @@ -2,16 +2,35 @@ use std::{env, fs::OpenOptions, io::Write}; +const HEADER: &str = r#"// Jackson Coxson +// Bindings to idevice - https://github.com/jkcoxson/idevice + +#ifdef _WIN32 + #ifndef WIN32_LEAN_AND_MEAN + #define WIN32_LEAN_AND_MEAN + #endif + #include + #include + typedef int idevice_socklen_t; + typedef struct sockaddr idevice_sockaddr; +#else + #include + #include + typedef socklen_t idevice_socklen_t; + typedef struct sockaddr idevice_sockaddr; +#endif +"#; + fn main() { let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); cbindgen::Builder::new() .with_crate(crate_dir) - .with_header( - "// Jackson Coxson\n// Bindings to idevice - https://github.com/jkcoxson/idevice", - ) + .with_header(HEADER) .with_language(cbindgen::Language::C) - .with_sys_include("sys/socket.h") + .with_include_guard("IDEVICE_H") + .exclude_item("idevice_socklen_t") + .exclude_item("idevice_sockaddr") .generate() .expect("Unable to generate bindings") .write_to_file("idevice.h"); diff --git a/ffi/src/lib.rs b/ffi/src/lib.rs index 65c32ab..33f04af 100644 --- a/ffi/src/lib.rs +++ b/ffi/src/lib.rs @@ -136,22 +136,23 @@ pub unsafe extern "C" fn idevice_new( /// `addr` must be a valid sockaddr /// `label` must be a valid null-terminated C string /// `idevice` must be a valid, non-null pointer to a location where the handle will be stored +#[cfg(unix)] #[unsafe(no_mangle)] pub unsafe extern "C" fn idevice_new_tcp_socket( - addr: *const libc::sockaddr, - addr_len: libc::socklen_t, + addr: *const idevice_sockaddr, + addr_len: idevice_socklen_t, label: *const c_char, idevice: *mut *mut IdeviceHandle, ) -> *mut IdeviceFfiError { - if addr.is_null() { - log::error!("socket addr null pointer"); + if addr.is_null() || label.is_null() || idevice.is_null() { + log::error!("null pointer(s) to idevice_new_tcp_socket"); return ffi_err!(IdeviceError::FfiInvalidArg); } + let addr = addr as *const SockAddr; - // Convert C string to Rust string let label = match unsafe { CStr::from_ptr(label).to_str() } { Ok(s) => s, - Err(_) => return ffi_err!(IdeviceError::FfiInvalidArg), + Err(_) => return ffi_err!(IdeviceError::FfiInvalidString), }; let addr = match util::c_socket_to_rust(addr, addr_len) { @@ -159,9 +160,10 @@ pub unsafe extern "C" fn idevice_new_tcp_socket( Err(e) => return ffi_err!(e), }; - let device: Result = RUNTIME.block_on(async move { - Ok(idevice::Idevice::new( - Box::new(tokio::net::TcpStream::connect(addr).await?), + let device = RUNTIME.block_on(async move { + let stream = tokio::net::TcpStream::connect(addr).await?; + Ok::(idevice::Idevice::new( + Box::new(stream), label, )) }); @@ -170,7 +172,7 @@ pub unsafe extern "C" fn idevice_new_tcp_socket( Ok(dev) => { let boxed = Box::new(IdeviceHandle(dev)); unsafe { *idevice = Box::into_raw(boxed) }; - null_mut() + std::ptr::null_mut() } Err(e) => ffi_err!(e), } diff --git a/ffi/src/provider.rs b/ffi/src/provider.rs index f6cb00a..95fd169 100644 --- a/ffi/src/provider.rs +++ b/ffi/src/provider.rs @@ -1,9 +1,11 @@ // Jackson Coxson use idevice::provider::{IdeviceProvider, TcpProvider, UsbmuxdProvider}; +use std::net::IpAddr; use std::os::raw::c_char; use std::{ffi::CStr, ptr::null_mut}; +use crate::util::{SockAddr, idevice_sockaddr}; use crate::{IdeviceFfiError, ffi_err, usbmuxd::UsbmuxdAddrHandle, util}; pub struct IdeviceProviderHandle(pub Box); @@ -24,33 +26,27 @@ pub struct IdeviceProviderHandle(pub Box); /// `pairing_file` is consumed must never be used again /// `label` must be a valid Cstr /// `provider` must be a valid, non-null pointer to a location where the handle will be stored -#[cfg(feature = "tcp")] #[unsafe(no_mangle)] pub unsafe extern "C" fn idevice_tcp_provider_new( - ip: *const libc::sockaddr, + ip: *const idevice_sockaddr, pairing_file: *mut crate::pairing_file::IdevicePairingFile, label: *const c_char, provider: *mut *mut IdeviceProviderHandle, ) -> *mut IdeviceFfiError { - if ip.is_null() || label.is_null() || provider.is_null() { - return ffi_err!(IdeviceError::FfiInvalidArg); - } - - let addr = match util::c_addr_to_rust(ip) { + let ip = ip as *const SockAddr; + let addr: IpAddr = match util::c_addr_to_rust(ip) { Ok(i) => i, - Err(e) => { - return ffi_err!(e); - } - }; - let label = match unsafe { CStr::from_ptr(label) }.to_str() { - Ok(l) => l.to_string(), - Err(e) => { - log::error!("Invalid label string: {e:?}"); - return ffi_err!(IdeviceError::FfiInvalidString); - } + Err(e) => return ffi_err!(e), }; + let label = match unsafe { CStr::from_ptr(label).to_str() } { + Ok(s) => s.to_string(), + Err(_) => return ffi_err!(IdeviceError::FfiInvalidString), + }; + + // consume the pairing file on success let pairing_file = unsafe { Box::from_raw(pairing_file) }; + let t = TcpProvider { addr, pairing_file: pairing_file.0, @@ -59,7 +55,7 @@ pub unsafe extern "C" fn idevice_tcp_provider_new( let boxed = Box::new(IdeviceProviderHandle(Box::new(t))); unsafe { *provider = Box::into_raw(boxed) }; - null_mut() + std::ptr::null_mut() } /// Frees an IdeviceProvider handle diff --git a/ffi/src/usbmuxd.rs b/ffi/src/usbmuxd.rs index f7e163d..56da89e 100644 --- a/ffi/src/usbmuxd.rs +++ b/ffi/src/usbmuxd.rs @@ -6,7 +6,8 @@ use std::{ }; use crate::{ - IdeviceFfiError, IdeviceHandle, IdevicePairingFile, RUNTIME, ffi_err, util::c_socket_to_rust, + IdeviceFfiError, IdeviceHandle, IdevicePairingFile, RUNTIME, ffi_err, + util::{SockAddr, c_socket_to_rust, idevice_sockaddr, idevice_socklen_t}, }; use idevice::{ IdeviceError, @@ -32,28 +33,33 @@ pub struct UsbmuxdDeviceHandle(pub UsbmuxdDevice); /// # Safety /// `addr` must be a valid sockaddr /// `usbmuxd_connection` must be a valid, non-null pointer to a location where the handle will be stored -#[unsafe(no_mangle)] pub unsafe extern "C" fn idevice_usbmuxd_new_tcp_connection( - addr: *const libc::sockaddr, - addr_len: libc::socklen_t, + addr: *const idevice_sockaddr, + addr_len: idevice_socklen_t, tag: u32, - usbmuxd_connection: *mut *mut UsbmuxdConnectionHandle, + out: *mut *mut UsbmuxdConnectionHandle, ) -> *mut IdeviceFfiError { - let addr = match c_socket_to_rust(addr, addr_len) { + if addr.is_null() || out.is_null() { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + + // Reinterpret as the real platform sockaddr for parsing + let addr = addr as *const SockAddr; + + let addr = match c_socket_to_rust(addr, addr_len as _) { Ok(a) => a, Err(e) => return ffi_err!(e), }; - let res: Result = RUNTIME.block_on(async move { + let res = RUNTIME.block_on(async move { let stream = tokio::net::TcpStream::connect(addr).await?; - Ok(UsbmuxdConnection::new(Box::new(stream), tag)) + Ok::<_, IdeviceError>(UsbmuxdConnection::new(Box::new(stream), tag)) }); match res { - Ok(r) => { - let boxed = Box::new(UsbmuxdConnectionHandle(r)); - unsafe { *usbmuxd_connection = Box::into_raw(boxed) }; - null_mut() + Ok(conn) => { + unsafe { *out = Box::into_raw(Box::new(UsbmuxdConnectionHandle(conn))) }; + std::ptr::null_mut() } Err(e) => ffi_err!(e), } @@ -367,20 +373,28 @@ pub unsafe extern "C" fn idevice_usbmuxd_connection_free( /// `usbmuxd_Addr` must be a valid, non-null pointer to a location where the handle will be stored #[unsafe(no_mangle)] pub unsafe extern "C" fn idevice_usbmuxd_tcp_addr_new( - addr: *const libc::sockaddr, - addr_len: libc::socklen_t, + addr: *const idevice_sockaddr, // <- portable + addr_len: idevice_socklen_t, usbmuxd_addr: *mut *mut UsbmuxdAddrHandle, ) -> *mut IdeviceFfiError { - let addr = match c_socket_to_rust(addr, addr_len) { + if addr.is_null() || usbmuxd_addr.is_null() { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + + // Reinterpret as the real platform sockaddr for parsing + let addr = addr as *const SockAddr; + + let addr = match c_socket_to_rust(addr, addr_len as _) { Ok(a) => a, Err(e) => return ffi_err!(e), }; let u = UsbmuxdAddr::TcpSocket(addr); - let boxed = Box::new(UsbmuxdAddrHandle(u)); - unsafe { *usbmuxd_addr = Box::into_raw(boxed) }; - null_mut() + unsafe { + *usbmuxd_addr = Box::into_raw(boxed); + } + std::ptr::null_mut() } /// Creates a new UsbmuxdAddr struct with a unix socket diff --git a/ffi/src/util.rs b/ffi/src/util.rs index 5bada5c..6088109 100644 --- a/ffi/src/util.rs +++ b/ffi/src/util.rs @@ -1,75 +1,185 @@ // Jackson Coxson -use std::{ - ffi::c_int, - net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, -}; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; use idevice::IdeviceError; -use libc::{sockaddr_in, sockaddr_in6}; + +// portable FFI-facing types (only used in signatures) +#[allow(non_camel_case_types)] +#[repr(C)] +pub struct idevice_sockaddr { + _priv: [u8; 0], // opaque; acts as "struct sockaddr" placeholder +} +#[cfg(unix)] +#[allow(non_camel_case_types)] +pub type idevice_socklen_t = libc::socklen_t; +#[cfg(windows)] +#[allow(non_camel_case_types)] +pub type idevice_socklen_t = i32; + +// platform sockaddr aliases for implementation +#[cfg(unix)] +pub(crate) type SockAddr = libc::sockaddr; +#[cfg(windows)] +use windows_sys::Win32::Networking::WinSock as winsock; +#[cfg(windows)] +pub(crate) type SockAddr = winsock::SOCKADDR; + +#[cfg(unix)] +use libc::{self, sockaddr_in, sockaddr_in6}; + +#[cfg(windows)] +use windows_sys::Win32::Networking::WinSock::{ + AF_INET, AF_INET6, SOCKADDR_IN as sockaddr_in, SOCKADDR_IN6 as sockaddr_in6, +}; + +#[cfg(unix)] +type SockLen = libc::socklen_t; +#[cfg(windows)] +type SockLen = i32; // socklen_t is an int on Windows + +#[inline] +fn invalid_arg() -> Result { + Err(IdeviceError::FfiInvalidArg) +} pub(crate) fn c_socket_to_rust( - addr: *const libc::sockaddr, - addr_len: libc::socklen_t, + addr: *const SockAddr, + addr_len: SockLen, ) -> Result { - Ok(unsafe { - match (*addr).sa_family as c_int { + if addr.is_null() { + log::error!("null sockaddr"); + return invalid_arg(); + } + + unsafe { + let family = (*addr).sa_family; + + #[cfg(unix)] + match family as i32 { libc::AF_INET => { if (addr_len as usize) < std::mem::size_of::() { log::error!("Invalid sockaddr_in size"); - return Err(IdeviceError::FfiInvalidArg); + return invalid_arg(); } - let addr_in = *(addr as *const sockaddr_in); - let ip = std::net::Ipv4Addr::from(u32::from_be(addr_in.sin_addr.s_addr)); - let port = u16::from_be(addr_in.sin_port); - std::net::SocketAddr::V4(std::net::SocketAddrV4::new(ip, port)) + let a = &*(addr as *const sockaddr_in); + let ip = Ipv4Addr::from(u32::from_be(a.sin_addr.s_addr)); + let port = u16::from_be(a.sin_port); + Ok(SocketAddr::V4(std::net::SocketAddrV4::new(ip, port))) } libc::AF_INET6 => { - if addr_len as usize >= std::mem::size_of::() { - let addr_in6 = *(addr as *const sockaddr_in6); - let ip = std::net::Ipv6Addr::from(addr_in6.sin6_addr.s6_addr); - let port = u16::from_be(addr_in6.sin6_port); - std::net::SocketAddr::V6(std::net::SocketAddrV6::new( - ip, - port, - addr_in6.sin6_flowinfo, - addr_in6.sin6_scope_id, - )) - } else { + if (addr_len as usize) < std::mem::size_of::() { log::error!("Invalid sockaddr_in6 size"); - return Err(IdeviceError::FfiInvalidArg); + return invalid_arg(); } + let a = &*(addr as *const sockaddr_in6); + let ip = Ipv6Addr::from(a.sin6_addr.s6_addr); + let port = u16::from_be(a.sin6_port); + Ok(SocketAddr::V6(std::net::SocketAddrV6::new( + ip, + port, + a.sin6_flowinfo, + a.sin6_scope_id, + ))) + } + _ => { + log::error!( + "Unsupported socket address family: {}", + (*addr).sa_family as i32 + ); + invalid_arg() + } + } + + #[cfg(windows)] + match family { + AF_INET => { + if (addr_len as usize) < std::mem::size_of::() { + log::error!("Invalid SOCKADDR_IN size"); + return invalid_arg(); + } + let a = &*(addr as *const sockaddr_in); + // IN_ADDR is a union; use S_un.S_addr (network byte order) + let ip_be = a.sin_addr.S_un.S_addr; + let ip = Ipv4Addr::from(u32::from_be(ip_be)); + let port = u16::from_be(a.sin_port); + Ok(SocketAddr::V4(std::net::SocketAddrV4::new(ip, port))) + } + AF_INET6 => { + if (addr_len as usize) < std::mem::size_of::() { + log::error!("Invalid SOCKADDR_IN6 size"); + return invalid_arg(); + } + let a = &*(addr as *const sockaddr_in6); + // IN6_ADDR is a union; read the 16 Byte array + let bytes: [u8; 16] = a.sin6_addr.u.Byte; + let ip = Ipv6Addr::from(bytes); + let port = u16::from_be(a.sin6_port); + let scope_id = a.Anonymous.sin6_scope_id; + Ok(SocketAddr::V6(std::net::SocketAddrV6::new( + ip, + port, + a.sin6_flowinfo, + scope_id, + ))) } _ => { log::error!("Unsupported socket address family: {}", (*addr).sa_family); - return Err(IdeviceError::FfiInvalidArg); + invalid_arg() } } - }) + } } -pub(crate) fn c_addr_to_rust(addr: *const libc::sockaddr) -> Result { +pub(crate) fn c_addr_to_rust(addr: *const SockAddr) -> Result { + if addr.is_null() { + log::error!("null sockaddr"); + return invalid_arg(); + } + unsafe { - // Check the address family - match (*addr).sa_family as c_int { + #[cfg(unix)] + let family = (*addr).sa_family as i32; + #[cfg(windows)] + let family = (*addr).sa_family; + + #[cfg(unix)] + match family { libc::AF_INET => { - // Convert sockaddr_in (IPv4) to IpAddr - let sockaddr_in = addr as *const sockaddr_in; - let ip = (*sockaddr_in).sin_addr.s_addr; - let octets = u32::from_be(ip).to_be_bytes(); + let a = &*(addr as *const sockaddr_in); + let octets = u32::from_be(a.sin_addr.s_addr).to_be_bytes(); Ok(IpAddr::V4(Ipv4Addr::new( octets[0], octets[1], octets[2], octets[3], ))) } libc::AF_INET6 => { - // Convert sockaddr_in6 (IPv6) to IpAddr - let sockaddr_in6 = addr as *const sockaddr_in6; - let ip = (*sockaddr_in6).sin6_addr.s6_addr; - Ok(IpAddr::V6(Ipv6Addr::from(ip))) + let a = &*(addr as *const sockaddr_in6); + Ok(IpAddr::V6(Ipv6Addr::from(a.sin6_addr.s6_addr))) + } + _ => { + log::error!( + "Unsupported socket address family: {}", + (*addr).sa_family as i32 + ); + invalid_arg() + } + } + + #[cfg(windows)] + match family { + AF_INET => { + let a = &*(addr as *const sockaddr_in); + let ip_be = a.sin_addr.S_un.S_addr; + Ok(IpAddr::V4(Ipv4Addr::from(u32::from_be(ip_be)))) + } + AF_INET6 => { + let a = &*(addr as *const sockaddr_in6); + let bytes: [u8; 16] = a.sin6_addr.u.Byte; + Ok(IpAddr::V6(Ipv6Addr::from(bytes))) } _ => { log::error!("Unsupported socket address family: {}", (*addr).sa_family); - Err(IdeviceError::FfiInvalidArg) + invalid_arg() } } } From 873505b9dbb306c7103e3a59a8fad10b8d7e47f0 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Tue, 12 Aug 2025 11:41:12 -0600 Subject: [PATCH 051/126] Use crate util types in FFI lib.rs --- ffi/src/lib.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ffi/src/lib.rs b/ffi/src/lib.rs index 33f04af..a7b64fc 100644 --- a/ffi/src/lib.rs +++ b/ffi/src/lib.rs @@ -54,6 +54,9 @@ use std::{ }; use tokio::runtime::{self, Runtime}; +#[cfg(unix)] +use crate::util::{idevice_sockaddr, idevice_socklen_t}; + static RUNTIME: Lazy = Lazy::new(|| { runtime::Builder::new_multi_thread() .enable_io() @@ -144,6 +147,8 @@ pub unsafe extern "C" fn idevice_new_tcp_socket( label: *const c_char, idevice: *mut *mut IdeviceHandle, ) -> *mut IdeviceFfiError { + use crate::util::SockAddr; + if addr.is_null() || label.is_null() || idevice.is_null() { log::error!("null pointer(s) to idevice_new_tcp_socket"); return ffi_err!(IdeviceError::FfiInvalidArg); From 388b50246aa48a7dbf8142c9a061b05172ed25f0 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Tue, 12 Aug 2025 12:03:55 -0600 Subject: [PATCH 052/126] Re-add no-mangle to usbmuxd ffi function --- cpp/include/usbmuxd.hpp | 2 +- ffi/src/usbmuxd.rs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/cpp/include/usbmuxd.hpp b/cpp/include/usbmuxd.hpp index 07b0889..fe33aad 100644 --- a/cpp/include/usbmuxd.hpp +++ b/cpp/include/usbmuxd.hpp @@ -191,7 +191,7 @@ class UsbmuxdConnection { /// @param err An error that will be populated on failure. /// @return A UsbmuxdConnection on success, std::nullopt on failure. static std::optional - tcp_new(const sockaddr* addr, socklen_t addr_len, uint32_t tag, FfiError& err) { + tcp_new(const idevice_sockaddr* addr, idevice_socklen_t addr_len, uint32_t tag, FfiError& err) { UsbmuxdConnectionHandle* handle = nullptr; IdeviceFfiError* e = idevice_usbmuxd_new_tcp_connection(addr, addr_len, tag, &handle); if (e) { diff --git a/ffi/src/usbmuxd.rs b/ffi/src/usbmuxd.rs index 56da89e..fcccd94 100644 --- a/ffi/src/usbmuxd.rs +++ b/ffi/src/usbmuxd.rs @@ -33,6 +33,7 @@ pub struct UsbmuxdDeviceHandle(pub UsbmuxdDevice); /// # Safety /// `addr` must be a valid sockaddr /// `usbmuxd_connection` must be a valid, non-null pointer to a location where the handle will be stored +#[unsafe(no_mangle)] pub unsafe extern "C" fn idevice_usbmuxd_new_tcp_connection( addr: *const idevice_sockaddr, addr_len: idevice_socklen_t, From bc5b15bd395ef6e6aba8827666e902ff7e09a8d6 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Tue, 12 Aug 2025 12:18:14 -0600 Subject: [PATCH 053/126] Use platform-agnostic make for CI --- justfile | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/justfile b/justfile index c834bfa..faf7f76 100644 --- a/justfile +++ b/justfile @@ -22,11 +22,13 @@ create-example-build-folder: [working-directory: 'cpp/examples/build'] build-cpp: build-ffi-native create-example-build-folder - cmake .. && make + cmake -S .. -B . -DCMAKE_BUILD_TYPE=Release + cmake --build . --config Release --parallel [working-directory: 'ffi/examples/build'] build-c: build-ffi-native create-example-build-folder - cmake .. && make + cmake -S .. -B . -DCMAKE_BUILD_TYPE=Release + cmake --build . --config Release --parallel xcframework: apple-build rm -rf swift/IDevice.xcframework From d59d16696ccb865ada3561496eab3320ff014edd Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Tue, 12 Aug 2025 13:36:33 -0600 Subject: [PATCH 054/126] Define u types for Windows in ++ library --- cpp/examples/CMakeLists.txt | 12 ++++++++++-- cpp/include/idevice++.hpp | 8 ++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/cpp/examples/CMakeLists.txt b/cpp/examples/CMakeLists.txt index 548a39f..cea3aca 100644 --- a/cpp/examples/CMakeLists.txt +++ b/cpp/examples/CMakeLists.txt @@ -8,7 +8,11 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # Set the paths set(HEADER_FILE ${CMAKE_SOURCE_DIR}/../../ffi/idevice.h) -set(STATIC_LIB ${CMAKE_SOURCE_DIR}/../../target/release/libidevice_ffi.a) +if (MSVC) + set(STATIC_LIB ${CMAKE_SOURCE_DIR}/../../target/release/idevice_ffi.lib) +else() + set(STATIC_LIB ${CMAKE_SOURCE_DIR}/../../target/release/libidevice_ffi.a) +endif() set(EXAMPLES_DIR ${CMAKE_SOURCE_DIR}/../examples) set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) @@ -17,7 +21,11 @@ set(IDEVICE_FFI_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/../../ffi) # ffi/ set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) -set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Wpedantic") +if (MSVC) + add_compile_options(/W4 /permissive- /EHsc) +else() + add_compile_options(-Wall -Wextra -Wpedantic) +endif() # Find all C++ example files file(GLOB EXAMPLE_SOURCES ${EXAMPLES_DIR}/*.cpp) diff --git a/cpp/include/idevice++.hpp b/cpp/include/idevice++.hpp index dbd8736..b16217e 100644 --- a/cpp/include/idevice++.hpp +++ b/cpp/include/idevice++.hpp @@ -8,6 +8,14 @@ #include #include +#if defined(_WIN32) && !defined(__MINGW32__) +// MSVC doesn't have BSD u_int* types +using u_int8_t = std::uint8_t; +using u_int16_t = std::uint16_t; +using u_int32_t = std::uint32_t; +using u_int64_t = std::uint64_t; +#endif + namespace IdeviceFFI { class Idevice { From 7fbad8399fcc329156efabb281f20b3c64a01e02 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Tue, 12 Aug 2025 13:38:13 -0600 Subject: [PATCH 055/126] Don't build C examples on Windows --- .github/workflows/ci.yml | 8 +------- justfile | 1 + 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1e6a93e..56aba55 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -207,7 +207,7 @@ jobs: restore-keys: windows-cargo- - name: Build Rust and examples/tools - run: just ci-check + run: just windows-ci-check - name: Upload static library uses: actions/upload-artifact@v4 @@ -215,12 +215,6 @@ jobs: name: libidevice-windows-a path: target\release\idevice_ffi.lib - - name: Upload C examples/tools - uses: actions/upload-artifact@v4 - with: - name: idevice-c-examples-windows - path: ffi\examples\build\bin\* - - name: Upload C++ examples/tools uses: actions/upload-artifact@v4 with: diff --git a/justfile b/justfile index faf7f76..238d4ef 100644 --- a/justfile +++ b/justfile @@ -7,6 +7,7 @@ ci-check: build-ffi-native build-tools-native build-cpp build-c cargo clippy --all-targets --all-features -- -D warnings macos-ci-check: ci-check xcframework cd tools && cargo build --release --target x86_64-apple-darwin +windows-ci-check: build-ffi-native build-tools-native build-cpp [working-directory: 'ffi'] build-ffi-native: From 5477571a80ad531a5a274d81d289b8c7e21cb802 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Tue, 12 Aug 2025 13:39:41 -0600 Subject: [PATCH 056/126] Link to the Windows standard libraries on Windows for ++ --- cpp/examples/CMakeLists.txt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/cpp/examples/CMakeLists.txt b/cpp/examples/CMakeLists.txt index cea3aca..c04a0eb 100644 --- a/cpp/examples/CMakeLists.txt +++ b/cpp/examples/CMakeLists.txt @@ -56,6 +56,16 @@ foreach(EXAMPLE_FILE ${EXAMPLE_SOURCES}) "-framework IOKit" "-framework CFNetwork" ) + elseif (WIN32) + # System libs required by Rust std/tokio/reqwest/ring on Windows + target_link_libraries(${EXAMPLE_NAME} PRIVATE + ws2_32 # WSAStartup/WSACleanup, closesocket, freeaddrinfo + userenv # GetUserProfileDirectoryW + ntdll # NtReadFile, RtlNtStatusToDosError + bcrypt # ring RNG (BCryptGenRandom) + # iphlpapi # (optional) GetAdaptersAddresses, if you ever see it unresolved + # secur32 crypt32 ncrypt # (only if you switch to schannel/native-tls) + ) endif() endforeach() From 0bb5deada84298e50381351fce741d0aba9d72bf Mon Sep 17 00:00:00 2001 From: Ylarod Date: Wed, 13 Aug 2025 21:41:48 +0800 Subject: [PATCH 057/126] feat: impl parts of diagnostics and mobilebackup2 (#20) * feat: add udid cache to idevice * feat: impl diagnostics * feat: impl mobilebackup2 * docs: update README.md * fix: make clippy happy * fix: make linux clippy happy * fix: make linux clippy happy again * fix: make clippy happy again * fix: small updates --- README.md | 6 +- idevice/Cargo.toml | 4 +- idevice/src/lib.rs | 22 + idevice/src/services/diagnostics_relay.rs | 220 +++- idevice/src/services/mobilebackup2.rs | 1115 +++++++++++++++++++++ idevice/src/services/mod.rs | 2 + tools/Cargo.toml | 8 + tools/src/diagnostics.rs | 300 ++++++ tools/src/mobilebackup2.rs | 562 +++++++++++ 9 files changed, 2234 insertions(+), 5 deletions(-) create mode 100644 idevice/src/services/mobilebackup2.rs create mode 100644 tools/src/diagnostics.rs create mode 100644 tools/src/mobilebackup2.rs diff --git a/README.md b/README.md index 2bed136..50a311a 100644 --- a/README.md +++ b/README.md @@ -28,20 +28,22 @@ To keep dependency bloat and compile time down, everything is contained in featu | `core_device_proxy` | Start a secure tunnel to access protected services. | | `crashreportcopymobile`| Copy crash reports.| | `debug_proxy` | Send GDB commands to the device.| +| `diagnostics_relay` | Access device diagnostics information (IORegistry, MobileGestalt, battery, NAND, device control).| | `dvt` | Access Apple developer tools (e.g. Instruments).| | `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.| | `location_simulation` | Simulate GPS locations on the device.| | `pair` | Pair the device.| | `syslog_relay` | Relay system 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. | +| `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. | diff --git a/idevice/Cargo.toml b/idevice/Cargo.toml index 113cc75..6c84b41 100644 --- a/idevice/Cargo.toml +++ b/idevice/Cargo.toml @@ -75,6 +75,7 @@ installation_proxy = [] springboardservices = [] misagent = [] mobile_image_mounter = ["dep:sha2"] +mobilebackup2 = [] location_simulation = [] pair = ["chrono/default", "tokio/time", "dep:sha2", "dep:rsa", "dep:x509-cert"] obfuscate = ["dep:obfstr"] @@ -109,6 +110,7 @@ full = [ "location_simulation", "misagent", "mobile_image_mounter", + "mobilebackup2", "pair", "restore_service", "rsd", @@ -123,4 +125,4 @@ full = [ ] [package.metadata.docs.rs] -all-features = true +all-features = true \ No newline at end of file diff --git a/idevice/src/lib.rs b/idevice/src/lib.rs index 3313de8..48003ee 100644 --- a/idevice/src/lib.rs +++ b/idevice/src/lib.rs @@ -74,6 +74,11 @@ pub trait IdeviceService: Sized { lockdown .start_session(&provider.get_pairing_file().await?) .await?; + // Best-effort fetch UDID for downstream defaults (e.g., MobileBackup2 Target/Source identifiers) + let udid_value = match lockdown.get_value(Some("UniqueDeviceID"), None).await { + Ok(v) => v.as_string().map(|s| s.to_string()), + Err(_) => None, + }; let (port, ssl) = lockdown.start_service(Self::service_name()).await?; @@ -84,6 +89,10 @@ pub trait IdeviceService: Sized { .await?; } + if let Some(udid) = udid_value { + idevice.set_udid(udid); + } + Self::from_stream(idevice).await } @@ -123,6 +132,8 @@ pub struct Idevice { socket: Option>, /// Unique label identifying this connection label: String, + /// Cached device UDID for convenience in higher-level protocols + udid: Option, } impl Idevice { @@ -135,6 +146,7 @@ impl Idevice { Self { socket: Some(socket), label: label.into(), + udid: None, } } @@ -142,6 +154,16 @@ impl Idevice { self.socket } + /// Sets cached UDID + pub fn set_udid(&mut self, udid: impl Into) { + self.udid = Some(udid.into()); + } + + /// Returns cached UDID if available + pub fn udid(&self) -> Option<&str> { + self.udid.as_deref() + } + /// Queries the device type /// /// Sends a QueryType request and parses the response diff --git a/idevice/src/services/diagnostics_relay.rs b/idevice/src/services/diagnostics_relay.rs index 7811b85..7b525c5 100644 --- a/idevice/src/services/diagnostics_relay.rs +++ b/idevice/src/services/diagnostics_relay.rs @@ -20,7 +20,7 @@ impl IdeviceService for DiagnosticsRelayClient { } impl DiagnosticsRelayClient { - /// Creates a new client from an existing device connection + /// Creates a new client from an existing device connection /// /// # Arguments /// * `idevice` - Pre-established device connection @@ -74,7 +74,223 @@ impl DiagnosticsRelayClient { .and_then(|x| x.into_dictionary()) .and_then(|mut x| x.remove("IORegistry")) .and_then(|x| x.into_dictionary()); + Ok(res) } -} + + /// Requests MobileGestalt information from the device + /// + /// # Arguments + /// * `keys` - Optional list of specific keys to request. If None, requests all available keys + /// + /// # Returns + /// A dictionary containing the requested MobileGestalt information + pub async fn mobilegestalt( + &mut self, + keys: Option>, + ) -> Result, IdeviceError> { + let mut req = plist::Dictionary::new(); + req.insert("Request".into(), "MobileGestalt".into()); + + if let Some(keys) = keys { + let keys_array: Vec = keys.into_iter().map(|k| k.into()).collect(); + req.insert("MobileGestaltKeys".into(), plist::Value::Array(keys_array)); + } + + self.idevice + .send_plist(plist::Value::Dictionary(req)) + .await?; + let mut res = self.idevice.read_plist().await?; + + match res.get("Status").and_then(|x| x.as_string()) { + Some("Success") => {} + _ => { + return Err(IdeviceError::UnexpectedResponse); + } + } + + let res = res + .remove("Diagnostics") + .and_then(|x| x.into_dictionary()); + + Ok(res) + } + + /// Requests gas gauge information from the device + /// + /// # Returns + /// A dictionary containing gas gauge (battery) information + pub async fn gasguage(&mut self) -> Result, IdeviceError> { + let mut req = plist::Dictionary::new(); + req.insert("Request".into(), "GasGauge".into()); + + self.idevice + .send_plist(plist::Value::Dictionary(req)) + .await?; + let mut res = self.idevice.read_plist().await?; + + match res.get("Status").and_then(|x| x.as_string()) { + Some("Success") => {} + _ => { + return Err(IdeviceError::UnexpectedResponse); + } + } + + let res = res + .remove("Diagnostics") + .and_then(|x| x.into_dictionary()); + + Ok(res) + } + + /// Requests NAND information from the device + /// + /// # Returns + /// A dictionary containing NAND flash information + pub async fn nand(&mut self) -> Result, IdeviceError> { + let mut req = plist::Dictionary::new(); + req.insert("Request".into(), "NAND".into()); + + self.idevice + .send_plist(plist::Value::Dictionary(req)) + .await?; + let mut res = self.idevice.read_plist().await?; + + match res.get("Status").and_then(|x| x.as_string()) { + Some("Success") => {} + _ => { + return Err(IdeviceError::UnexpectedResponse); + } + } + + let res = res + .remove("Diagnostics") + .and_then(|x| x.into_dictionary()); + + Ok(res) + } + + /// Requests all available diagnostics information + /// + /// # Returns + /// A dictionary containing all diagnostics information + pub async fn all(&mut self) -> Result, IdeviceError> { + let mut req = plist::Dictionary::new(); + req.insert("Request".into(), "All".into()); + + self.idevice + .send_plist(plist::Value::Dictionary(req)) + .await?; + let mut res = self.idevice.read_plist().await?; + + match res.get("Status").and_then(|x| x.as_string()) { + Some("Success") => {} + _ => { + return Err(IdeviceError::UnexpectedResponse); + } + } + + let res = res + .remove("Diagnostics") + .and_then(|x| x.into_dictionary()); + + Ok(res) + } + + /// Restarts the device + /// + /// # Returns + /// Result indicating success or failure + pub async fn restart(&mut self) -> Result<(), IdeviceError> { + let mut req = plist::Dictionary::new(); + req.insert("Request".into(), "Restart".into()); + + self.idevice + .send_plist(plist::Value::Dictionary(req)) + .await?; + let res = self.idevice.read_plist().await?; + + match res.get("Status").and_then(|x| x.as_string()) { + Some("Success") => Ok(()), + _ => Err(IdeviceError::UnexpectedResponse), + } + } + + /// Shuts down the device + /// + /// # Returns + /// Result indicating success or failure + pub async fn shutdown(&mut self) -> Result<(), IdeviceError> { + let mut req = plist::Dictionary::new(); + req.insert("Request".into(), "Shutdown".into()); + + self.idevice + .send_plist(plist::Value::Dictionary(req)) + .await?; + let res = self.idevice.read_plist().await?; + + match res.get("Status").and_then(|x| x.as_string()) { + Some("Success") => Ok(()), + _ => Err(IdeviceError::UnexpectedResponse), + } + } + + /// Puts the device to sleep + /// + /// # Returns + /// Result indicating success or failure + pub async fn sleep(&mut self) -> Result<(), IdeviceError> { + let mut req = plist::Dictionary::new(); + req.insert("Request".into(), "Sleep".into()); + + self.idevice + .send_plist(plist::Value::Dictionary(req)) + .await?; + let res = self.idevice.read_plist().await?; + + match res.get("Status").and_then(|x| x.as_string()) { + Some("Success") => Ok(()), + _ => Err(IdeviceError::UnexpectedResponse), + } + } + + /// Requests WiFi diagnostics from the device + pub async fn wifi(&mut self) -> Result, IdeviceError> { + let mut req = plist::Dictionary::new(); + req.insert("Request".into(), "WiFi".into()); + + self.idevice + .send_plist(plist::Value::Dictionary(req)) + .await?; + let mut res = self.idevice.read_plist().await?; + + match res.get("Status").and_then(|x| x.as_string()) { + Some("Success") => {} + _ => { + return Err(IdeviceError::UnexpectedResponse); + } + } + + let res = res + .remove("Diagnostics") + .and_then(|x| x.into_dictionary()); + + Ok(res) + } + + /// Sends Goodbye request signaling end of communication + pub async fn goodbye(&mut self) -> Result<(), IdeviceError> { + let mut req = plist::Dictionary::new(); + req.insert("Request".into(), "Goodbye".into()); + self.idevice + .send_plist(plist::Value::Dictionary(req)) + .await?; + let res = self.idevice.read_plist().await?; + match res.get("Status").and_then(|x| x.as_string()) { + Some("Success") => Ok(()), + Some("UnknownRequest") => Err(IdeviceError::UnexpectedResponse), + _ => Err(IdeviceError::UnexpectedResponse), + } + } +} \ No newline at end of file diff --git a/idevice/src/services/mobilebackup2.rs b/idevice/src/services/mobilebackup2.rs new file mode 100644 index 0000000..2276e8a --- /dev/null +++ b/idevice/src/services/mobilebackup2.rs @@ -0,0 +1,1115 @@ +//! iOS Mobile Backup 2 Service Client +//! +//! Provides functionality for interacting with the mobilebackup2 service on iOS devices, +//! which allows creating, restoring, and managing device backups. + +use log::{debug, warn}; +use plist::Dictionary; +use tokio::io::AsyncReadExt; +use std::fs; +use std::io::{Read, Write}; +use std::path::Path; + +use crate::{Idevice, IdeviceError, IdeviceService, obf}; + +/// DeviceLink message codes used in MobileBackup2 binary streams +pub const DL_CODE_SUCCESS: u8 = 0x00; +pub const DL_CODE_ERROR_LOCAL: u8 = 0x06; +pub const DL_CODE_ERROR_REMOTE: u8 = 0x0b; +pub const DL_CODE_FILE_DATA: u8 = 0x0c; + +/// Client for interacting with the iOS mobile backup 2 service +/// +/// This service provides access to device backup functionality including +/// creating backups, restoring from backups, and managing backup data. +pub struct MobileBackup2Client { + /// The underlying device connection with established mobilebackup2 service + pub idevice: Idevice, + /// Protocol version negotiated with the device + pub protocol_version: f64, +} + +impl IdeviceService for MobileBackup2Client { + /// Returns the mobile backup 2 service name as registered with lockdownd + fn service_name() -> std::borrow::Cow<'static, str> { + obf!("com.apple.mobilebackup2") + } + + async fn from_stream(idevice: Idevice) -> Result { + let mut client = Self::new(idevice); + // Perform DeviceLink handshake first + client.dl_version_exchange().await?; + // Perform version exchange after connection + client.version_exchange().await?; + Ok(client) + } +} + +/// Backup message types used in the mobilebackup2 protocol +#[derive(Debug, Clone)] +pub enum BackupMessageType { + /// Request to start a backup operation + BackupMessageTypeBackup, + /// Request to restore from a backup + BackupMessageTypeRestore, + /// Information message + BackupMessageTypeInfo, + /// List available backups + BackupMessageTypeList, + /// Upload files to backup + BackupMessageTypeUploadFiles, + /// Download files from backup + BackupMessageTypeDownloadFiles, + /// Clear backup data + BackupMessageTypeClearBackupData, + /// Move files in backup + BackupMessageTypeMoveFiles, + /// Remove files from backup + BackupMessageTypeRemoveFiles, + /// Create directory in backup + BackupMessageTypeCreateDirectory, + /// Acquire lock for backup operation + BackupMessageTypeAcquireLock, + /// Release lock after backup operation + BackupMessageTypeReleaseLock, + /// Copy item in backup + BackupMessageTypeCopyItem, + /// Disconnect from service + BackupMessageTypeDisconnect, + /// Process message + BackupMessageTypeProcessMessage, + /// Get freespace information + BackupMessageTypeGetFreespace, + /// Factory info + BackupMessageTypeFactoryInfo, + /// Check if backup is encrypted + BackupMessageTypeCheckBackupEncryption, +} + +impl BackupMessageType { + /// Convert message type to string representation + pub fn as_str(&self) -> &'static str { + match self { + // These map to MobileBackup2 request names per libimobiledevice + BackupMessageType::BackupMessageTypeBackup => "Backup", + BackupMessageType::BackupMessageTypeRestore => "Restore", + BackupMessageType::BackupMessageTypeInfo => "Info", + BackupMessageType::BackupMessageTypeList => "List", + // The following are DL control messages and not sent via MessageName + BackupMessageType::BackupMessageTypeUploadFiles => "DLMessageUploadFiles", + BackupMessageType::BackupMessageTypeDownloadFiles => "DLMessageDownloadFiles", + BackupMessageType::BackupMessageTypeClearBackupData => "DLMessageClearBackupData", + BackupMessageType::BackupMessageTypeMoveFiles => "DLMessageMoveFiles", + BackupMessageType::BackupMessageTypeRemoveFiles => "DLMessageRemoveFiles", + BackupMessageType::BackupMessageTypeCreateDirectory => "DLMessageCreateDirectory", + BackupMessageType::BackupMessageTypeAcquireLock => "DLMessageAcquireLock", + BackupMessageType::BackupMessageTypeReleaseLock => "DLMessageReleaseLock", + BackupMessageType::BackupMessageTypeCopyItem => "DLMessageCopyItem", + BackupMessageType::BackupMessageTypeDisconnect => "DLMessageDisconnect", + BackupMessageType::BackupMessageTypeProcessMessage => "DLMessageProcessMessage", + BackupMessageType::BackupMessageTypeGetFreespace => "DLMessageGetFreeDiskSpace", + BackupMessageType::BackupMessageTypeFactoryInfo => "FactoryInfo", + BackupMessageType::BackupMessageTypeCheckBackupEncryption => "CheckBackupEncryption", + } + } +} + +/// Backup information structure +#[derive(Debug, Clone)] +pub struct BackupInfo { + /// Backup UUID + pub uuid: String, + /// Device name + pub device_name: String, + /// Display name + pub display_name: String, + /// Last backup date + pub last_backup_date: Option, + /// Backup version + pub version: String, + /// Whether backup is encrypted + pub is_encrypted: bool, +} + +/// High-level builder for restore options so callers don't need to remember raw keys +#[derive(Debug, Clone)] +pub struct RestoreOptions { + pub reboot: bool, + pub copy: bool, + pub preserve_settings: bool, + pub system_files: bool, + pub remove_items_not_restored: bool, + pub password: Option, +} + +impl Default for RestoreOptions { + fn default() -> Self { + Self { + reboot: true, + copy: true, + preserve_settings: true, + system_files: false, + remove_items_not_restored: false, + password: None, + } + } +} + +impl RestoreOptions { + pub fn new() -> Self { Self::default() } + pub fn with_reboot(mut self, reboot: bool) -> Self { self.reboot = reboot; self } + pub fn with_copy(mut self, copy: bool) -> Self { self.copy = copy; self } + pub fn with_preserve_settings(mut self, preserve: bool) -> Self { self.preserve_settings = preserve; self } + pub fn with_system_files(mut self, system: bool) -> Self { self.system_files = system; self } + pub fn with_remove_items_not_restored(mut self, remove: bool) -> Self { self.remove_items_not_restored = remove; self } + pub fn with_password(mut self, password: impl Into) -> Self { self.password = Some(password.into()); self } + + pub fn to_plist(&self) -> Dictionary { + let mut opts = Dictionary::new(); + opts.insert("RestoreShouldReboot".into(), plist::Value::Boolean(self.reboot)); + opts.insert("RestoreDontCopyBackup".into(), plist::Value::Boolean(!self.copy)); + opts.insert("RestorePreserveSettings".into(), plist::Value::Boolean(self.preserve_settings)); + opts.insert("RestoreSystemFiles".into(), plist::Value::Boolean(self.system_files)); + opts.insert("RemoveItemsNotRestored".into(), plist::Value::Boolean(self.remove_items_not_restored)); + if let Some(pw) = &self.password { + opts.insert("Password".into(), plist::Value::String(pw.clone())); + } + opts + } +} + +impl MobileBackup2Client { + /// Creates a new mobile backup 2 client from an existing device connection + /// + /// # Arguments + /// * `idevice` - Pre-established device connection + pub fn new(idevice: Idevice) -> Self { + Self { + idevice, + protocol_version: 0.0, + } + } + + /// Performs DeviceLink version exchange handshake + /// + /// Sequence: + /// 1) Receive ["DLMessageVersionExchange", major, minor] + /// 2) Send ["DLMessageVersionExchange", "DLVersionsOk", 400] + /// 3) Receive ["DLMessageDeviceReady"] + async fn dl_version_exchange(&mut self) -> Result<(), IdeviceError> { + debug!("Starting DeviceLink version exchange"); + // 1) Receive DLMessageVersionExchange + let (msg, _arr) = self.receive_dl_message().await?; + if msg != "DLMessageVersionExchange" { + warn!("Expected DLMessageVersionExchange, got {msg}"); + return Err(IdeviceError::UnexpectedResponse); + } + + // 2) Send DLVersionsOk with version 400 + let out = vec![ + plist::Value::String("DLMessageVersionExchange".into()), + plist::Value::String("DLVersionsOk".into()), + plist::Value::Integer(400u64.into()), + ]; + self.send_dl_array(out).await?; + + // 3) Receive DLMessageDeviceReady + let (msg2, _arr2) = self.receive_dl_message().await?; + if msg2 != "DLMessageDeviceReady" { + warn!("Expected DLMessageDeviceReady, got {msg2}"); + return Err(IdeviceError::UnexpectedResponse); + } + Ok(()) + } + + /// Sends a raw DL array as binary plist + async fn send_dl_array(&mut self, array: Vec) -> Result<(), IdeviceError> { + self.idevice + .send_bplist(plist::Value::Array(array)) + .await + } + + /// Receives any DL* message and returns (message_tag, full_array_value) + pub async fn receive_dl_message(&mut self) -> Result<(String, plist::Value), IdeviceError> { + if let Some(socket) = &mut self.idevice.socket { + let mut buf = [0u8; 4]; + socket.read_exact(&mut buf).await?; + let len = u32::from_be_bytes(buf); + let mut body = vec![0; len as usize]; + socket.read_exact(&mut body).await?; + let value: plist::Value = plist::from_bytes(&body)?; + if let plist::Value::Array(arr) = &value + && let Some(plist::Value::String(tag)) = arr.first() + { + return Ok((tag.clone(), value)); + } + warn!("Invalid DL message format"); + Err(IdeviceError::UnexpectedResponse) + } else { + Err(IdeviceError::NoEstablishedConnection) + } + } + + /// Performs version exchange with the device + /// + /// This is required by the mobilebackup2 protocol and must be called + /// before any other operations. + /// + /// # Returns + /// `Ok(())` on successful version negotiation + /// + /// # Errors + /// Returns `IdeviceError` if version exchange fails + async fn version_exchange(&mut self) -> Result<(), IdeviceError> { + debug!("Starting mobilebackup2 version exchange"); + + // Send supported protocol versions (matching libimobiledevice) + let mut hello_dict = Dictionary::new(); + let versions = vec![plist::Value::Real(2.0), plist::Value::Real(2.1)]; + hello_dict.insert("SupportedProtocolVersions".into(), plist::Value::Array(versions)); + + self.send_device_link_message("Hello", Some(hello_dict)).await?; + + // Receive response + let response = self.receive_device_link_message("Response").await?; + + // Check for error + if let Some(error_code) = response.get("ErrorCode") + && let Some(code) = error_code.as_unsigned_integer() + && code != 0 + { + warn!("Version exchange failed with error code: {code}"); + return Err(IdeviceError::UnexpectedResponse); + } + + // Get negotiated protocol version + if let Some(version) = response.get("ProtocolVersion").and_then(|v| v.as_real()) { + self.protocol_version = version; + debug!("Negotiated protocol version: {version}"); + } else { + warn!("No protocol version in response"); + return Err(IdeviceError::UnexpectedResponse); + } + + Ok(()) + } + + /// Sends a device link message (DLMessageProcessMessage format) + /// + /// This follows the device_link_service protocol used by mobilebackup2 + /// + /// # Arguments + /// * `message_name` - The message name (e.g., "Hello", "kBackupMessageTypeInfo") + /// * `options` - Optional dictionary of options for the message + /// + /// # Returns + /// `Ok(())` on successful message send + /// + /// # Errors + /// Returns `IdeviceError` if communication fails + async fn send_device_link_message( + &mut self, + message_name: &str, + options: Option, + ) -> Result<(), IdeviceError> { + // Create DLMessageProcessMessage array format + let mut message_array = Vec::new(); + message_array.push(plist::Value::String("DLMessageProcessMessage".into())); + + // Create the actual message dictionary + let mut message_dict = Dictionary::new(); + message_dict.insert("MessageName".into(), message_name.into()); + + if let Some(opts) = options { + for (key, value) in opts { + message_dict.insert(key, value); + } + } + + message_array.push(plist::Value::Dictionary(message_dict)); + + debug!("Sending device link message: {message_name}"); + self.idevice + .send_bplist(plist::Value::Array(message_array)) + .await + } + + /// Receives a device link message and validates the message name + /// + /// Arguments + /// * `expected_message` - The expected message name to validate + /// + /// # Returns + /// The message dictionary on success + /// + /// # Errors + /// Returns `IdeviceError` if communication fails or message name doesn't match + async fn receive_device_link_message(&mut self, expected_message: &str) -> Result { + // Read raw bytes and parse as plist::Value to handle array format + if let Some(socket) = &mut self.idevice.socket { + debug!("Reading response size"); + let mut buf = [0u8; 4]; + socket.read_exact(&mut buf).await?; + let len = u32::from_be_bytes(buf); + let mut buf = vec![0; len as usize]; + socket.read_exact(&mut buf).await?; + let response_value: plist::Value = plist::from_bytes(&buf)?; + + // Parse DLMessageProcessMessage format + if let plist::Value::Array(array) = response_value + && array.len() >= 2 + && let Some(plist::Value::String(dl_message)) = array.first() + && let Some(plist::Value::Dictionary(dict)) = array.get(1) + && dl_message == "DLMessageProcessMessage" + { + // Check MessageName if expected + if !expected_message.is_empty() { + if let Some(message_name) = dict.get("MessageName").and_then(|v| v.as_string()) { + if message_name != expected_message { + warn!("Expected message '{expected_message}', got '{message_name}'"); + return Err(IdeviceError::UnexpectedResponse); + } + } else { + warn!("No MessageName in response"); + return Err(IdeviceError::UnexpectedResponse); + } + } + return Ok(dict.clone()); + } + + warn!("Invalid device link message format"); + Err(IdeviceError::UnexpectedResponse) + } else { + Err(IdeviceError::NoEstablishedConnection) + } + } + + /// Sends a backup message to the device + /// + /// # Arguments + /// * `message_type` - The type of backup message to send + /// * `options` - Optional dictionary of options for the message + /// + /// # Returns + /// `Ok(())` on successful message send + /// + /// # Errors + /// Returns `IdeviceError` if communication fails + async fn send_backup_message( + &mut self, + message_type: BackupMessageType, + options: Option, + ) -> Result<(), IdeviceError> { + self.send_device_link_message(message_type.as_str(), options).await + } + + /// Sends a MobileBackup2 request with proper envelope and identifiers + pub async fn send_request( + &mut self, + request: &str, + target_identifier: Option<&str>, + source_identifier: Option<&str>, + options: Option, + ) -> Result<(), IdeviceError> { + let mut dict = Dictionary::new(); + if let Some(t) = target_identifier { + dict.insert("TargetIdentifier".into(), t.into()); + } + if let Some(s) = source_identifier { + dict.insert("SourceIdentifier".into(), s.into()); + } + if let Some(opts) = options { + dict.insert("Options".into(), plist::Value::Dictionary(opts)); + // Special cases like Unback/EnableCloudBackup are handled by caller if needed + } + self.send_device_link_message(request, Some(dict)).await + } + + /// Sends a DLMessageStatusResponse array + pub async fn send_status_response( + &mut self, + status_code: i64, + status1: Option<&str>, + status2: Option, + ) -> Result<(), IdeviceError> { + let arr = vec![ + plist::Value::String("DLMessageStatusResponse".into()), + plist::Value::Integer(status_code.into()), + plist::Value::String(status1.unwrap_or("___EmptyParameterString___").into()), + status2.unwrap_or_else(|| plist::Value::String("___EmptyParameterString___".into())), + ]; + self.send_dl_array(arr).await + } + + /// Receives a response from the backup service + /// + /// # Returns + /// The response as a plist Dictionary + /// + /// # Errors + /// Returns `IdeviceError` if communication fails or response is malformed + async fn receive_backup_response(&mut self) -> Result { + self.receive_device_link_message("").await + } + + /// Requests device information for backup + /// + /// # Returns + /// A dictionary containing device information + /// + /// # Errors + /// Returns `IdeviceError` if the request fails + pub async fn request_backup_info(&mut self) -> Result { + // Per protocol use MessageName "Info" + self.send_backup_message(BackupMessageType::BackupMessageTypeInfo, None) + .await?; + + let response = self.receive_backup_response().await?; + + // Check for error in response + if let Some(error) = response.get("ErrorCode") { + warn!("Backup info request failed with error: {error:?}"); + return Err(IdeviceError::UnexpectedResponse); + } + + Ok(response) + } + + /// Lists available backups on the device + /// + /// # Returns + /// A vector of backup information + /// + /// # Errors + /// Returns `IdeviceError` if the request fails + pub async fn list_backups(&mut self) -> Result, IdeviceError> { + self.send_backup_message(BackupMessageType::BackupMessageTypeList, None) + .await?; + + let response = self.receive_backup_response().await?; + + // Check for error in response + if let Some(error) = response.get("ErrorCode") { + warn!("List backups request failed with error: {error:?}"); + return Err(IdeviceError::UnexpectedResponse); + } + + let mut backups = Vec::new(); + + if let Some(plist::Value::Array(backup_list)) = response.get("BackupList") { + for backup_item in backup_list { + if let plist::Value::Dictionary(backup_dict) = backup_item { + let uuid = backup_dict + .get("BackupUUID") + .and_then(|v| v.as_string()) + .unwrap_or_default() + .to_string(); + + let device_name = backup_dict + .get("DeviceName") + .and_then(|v| v.as_string()) + .unwrap_or_default() + .to_string(); + + let display_name = backup_dict + .get("DisplayName") + .and_then(|v| v.as_string()) + .unwrap_or_default() + .to_string(); + + let last_backup_date = backup_dict + .get("LastBackupDate") + .and_then(|v| v.as_string()) + .map(|s| s.to_string()); + + let version = backup_dict + .get("Version") + .and_then(|v| v.as_string()) + .unwrap_or("Unknown") + .to_string(); + + let is_encrypted = backup_dict + .get("IsEncrypted") + .and_then(|v| v.as_boolean()) + .unwrap_or(false); + + backups.push(BackupInfo { + uuid, + device_name, + display_name, + last_backup_date, + version, + is_encrypted, + }); + } + } + } + + Ok(backups) + } + + /// Starts a backup operation + /// + /// # Arguments + /// * `target_identifier` - Optional target identifier for the backup + /// * `source_identifier` - Optional source identifier for the backup + /// * `options` - Optional backup options + /// + /// # Returns + /// `Ok(())` on successful backup start + /// + /// # Errors + /// Returns `IdeviceError` if the backup fails to start + pub async fn start_backup( + &mut self, + target_identifier: Option<&str>, + source_identifier: Option<&str>, + options: Option, + ) -> Result<(), IdeviceError> { + self.send_request( + BackupMessageType::BackupMessageTypeBackup.as_str(), + target_identifier, + source_identifier, + options, + ) + .await?; + + let response = self.receive_backup_response().await?; + + // Check for error in response + if let Some(error) = response.get("ErrorCode") { + warn!("Backup start failed with error: {error:?}"); + return Err(IdeviceError::UnexpectedResponse); + } + + debug!("Backup started successfully"); + Ok(()) + } + + /// Starts a restore operation + /// + /// # Arguments + /// * `backup_uuid` - UUID of the backup to restore from + /// * `options` - Optional restore options + /// + /// # Returns + /// `Ok(())` on successful restore start + /// + /// # Errors + /// Returns `IdeviceError` if the restore fails to start + #[deprecated(note = "Use restore_from_path; restore via BackupUUID is not supported by device/mobilebackup2")] + pub async fn start_restore( + &mut self, + _backup_uuid: &str, + options: Option, + ) -> Result<(), IdeviceError> { + let mut opts = options.unwrap_or_default(); + // Align default restore options with pymobiledevice semantics + // Caller-specified values (if any) take precedence. + if !opts.contains_key("RestoreShouldReboot") { + opts.insert("RestoreShouldReboot".into(), plist::Value::Boolean(true)); + } + if !opts.contains_key("RestoreDontCopyBackup") { + // pymobiledevice: copy=True -> RestoreDontCopyBackup=False + opts.insert("RestoreDontCopyBackup".into(), plist::Value::Boolean(false)); + } + if !opts.contains_key("RestorePreserveSettings") { + opts.insert("RestorePreserveSettings".into(), plist::Value::Boolean(true)); + } + if !opts.contains_key("RestoreSystemFiles") { + opts.insert("RestoreSystemFiles".into(), plist::Value::Boolean(false)); + } + if !opts.contains_key("RemoveItemsNotRestored") { + opts.insert("RemoveItemsNotRestored".into(), plist::Value::Boolean(false)); + } + // Avoid borrowing self while sending request + let target_udid_owned = self.idevice.udid().map(|s| s.to_string()); + let target_udid = target_udid_owned.as_deref(); + self.send_request( + BackupMessageType::BackupMessageTypeRestore.as_str(), + // default identifiers to current UDID if available + target_udid, + target_udid, + Some(opts), + ) + .await?; + + let response = self.receive_backup_response().await?; + + // Check for error in response + if let Some(error) = response.get("ErrorCode") { + warn!("Restore start failed with error: {error:?}"); + return Err(IdeviceError::UnexpectedResponse); + } + + debug!("Restore started successfully"); + Ok(()) + } + + /// High-level API: Restore from a local backup directory using DeviceLink file exchange + /// + /// - `backup_root` should point to the backup root directory (which contains the `` subdirectory) + /// - If `source_identifier` is None, the current connected device's UDID will be used by default + /// - `options` should be constructed using the `RestoreOptions` builder; if not provided, defaults will be used + pub async fn restore_from_path( + &mut self, + backup_root: &Path, + source_identifier: Option<&str>, + options: Option, + ) -> Result<(), IdeviceError> { + // Take owned UDID to avoid aliasing borrows + let target_udid_owned = self.idevice.udid().map(|s| s.to_string()); + let target_udid = target_udid_owned.as_deref(); + let source: &str = match source_identifier { + Some(s) => s, + None => target_udid.ok_or(IdeviceError::InvalidHostID)?, + }; + + // 简单存在性校验:backup_root/source 必须存在 + let backup_dir = backup_root.join(source); + if !backup_dir.exists() { + return Err(IdeviceError::NotFound); + } + + let opts = options.unwrap_or_default().to_plist(); + self.send_request( + BackupMessageType::BackupMessageTypeRestore.as_str(), + target_udid, + Some(source), + Some(opts), + ).await?; + + // 进入 DeviceLink 文件交换循环,根目录传入 backup_root(协议请求包含 source 前缀) + let _ = self.process_restore_dl_loop(backup_root).await?; + Ok(()) + } + + async fn process_restore_dl_loop(&mut self, host_dir: &Path) -> Result, IdeviceError> { + loop { + let (tag, value) = self.receive_dl_message().await?; + match tag.as_str() { + "DLMessageDownloadFiles" => { + self.handle_download_files(&value, host_dir).await?; + } + "DLMessageUploadFiles" => { + self.handle_upload_files(&value, host_dir).await?; + } + "DLMessageGetFreeDiskSpace" => { + // Minimal implementation: report 0 with success + self.send_status_response(0, None, Some(plist::Value::Integer(0u64.into()))).await?; + } + "DLContentsOfDirectory" => { + let empty = plist::Value::Dictionary(Dictionary::new()); + self.send_status_response(0, None, Some(empty)).await?; + } + "DLMessageCreateDirectory" => { + let status = Self::create_directory_from_message(&value, host_dir); + self.send_status_response(status, None, None).await?; + } + "DLMessageMoveFiles" | "DLMessageMoveItems" => { + let status = Self::move_files_from_message(&value, host_dir); + self.send_status_response(status, None, Some(plist::Value::Dictionary(Dictionary::new()))).await?; + } + "DLMessageRemoveFiles" | "DLMessageRemoveItems" => { + let status = Self::remove_files_from_message(&value, host_dir); + self.send_status_response(status, None, Some(plist::Value::Dictionary(Dictionary::new()))).await?; + } + "DLMessageCopyItem" => { + let status = Self::copy_item_from_message(&value, host_dir); + self.send_status_response(status, None, Some(plist::Value::Dictionary(Dictionary::new()))).await?; + } + "DLMessageProcessMessage" => { + if let plist::Value::Array(arr) = value + && let Some(plist::Value::Dictionary(dict)) = arr.get(1) + { + return Ok(Some(dict.clone())); + } + return Ok(None); + } + "DLMessageDisconnect" => { + return Ok(None); + } + other => { + warn!("Unsupported DL message: {other}"); + self.send_status_response(-1, Some("Operation not supported"), None).await?; + } + } + } + } + + async fn handle_download_files(&mut self, dl_value: &plist::Value, host_dir: &Path) -> Result<(), IdeviceError> { + let mut err_any = false; + if let plist::Value::Array(arr) = dl_value + && arr.len() >= 2 + && let Some(plist::Value::Array(files)) = arr.get(1) + { + for pv in files { + if let Some(path) = pv.as_string() + && let Err(e) = self.send_single_file(host_dir, path).await + { + warn!("Failed to send file {path}: {e}"); + err_any = true; + } + } + } + // terminating zero dword + self.idevice.send_raw(&0u32.to_be_bytes()).await?; + if err_any { + self.send_status_response(-13, Some("Multi status"), Some(plist::Value::Dictionary(Dictionary::new()))).await + } else { + self.send_status_response(0, None, Some(plist::Value::Dictionary(Dictionary::new()))).await + } + } + + async fn send_single_file(&mut self, host_dir: &Path, rel_path: &str) -> Result<(), IdeviceError> { + let full = host_dir.join(rel_path); + let path_bytes = rel_path.as_bytes().to_vec(); + let nlen = (path_bytes.len() as u32).to_be_bytes(); + self.idevice.send_raw(&nlen).await?; + self.idevice.send_raw(&path_bytes).await?; + + let mut f = match std::fs::File::open(&full) { + Ok(f) => f, + Err(e) => { + // send error + let desc = e.to_string(); + let size = (desc.len() as u32 + 1).to_be_bytes(); + let mut hdr = Vec::with_capacity(5); + hdr.extend_from_slice(&size); + hdr.push(DL_CODE_ERROR_LOCAL); + self.idevice.send_raw(&hdr).await?; + self.idevice.send_raw(desc.as_bytes()).await?; + return Ok(()); + } + }; + let mut buf = [0u8; 32768]; + loop { + let read = f.read(&mut buf).unwrap_or(0); + if read == 0 { break; } + let size = ((read as u32) + 1).to_be_bytes(); + let mut hdr = Vec::with_capacity(5); + hdr.extend_from_slice(&size); + hdr.push(DL_CODE_FILE_DATA); + self.idevice.send_raw(&hdr).await?; + self.idevice.send_raw(&buf[..read]).await?; + } + // success trailer + let mut ok = [0u8; 5]; + ok[..4].copy_from_slice(&1u32.to_be_bytes()); + ok[4] = DL_CODE_SUCCESS; + self.idevice.send_raw(&ok).await?; + Ok(()) + } + + async fn handle_upload_files(&mut self, _dl_value: &plist::Value, host_dir: &Path) -> Result<(), IdeviceError> { + loop { + let dlen = self.read_be_u32().await?; + if dlen == 0 { break; } + let dname = self.read_exact_string(dlen as usize).await?; + let flen = self.read_be_u32().await?; + if flen == 0 { break; } + let fname = self.read_exact_string(flen as usize).await?; + let dst = host_dir.join(&fname); + if let Some(parent) = dst.parent() { let _ = fs::create_dir_all(parent); } + let mut file = std::fs::File::create(&dst).map_err(|e| IdeviceError::InternalError(e.to_string()))?; + loop { + let nlen = self.read_be_u32().await?; + if nlen == 0 { break; } + let code = self.read_one().await?; + if code == DL_CODE_FILE_DATA { + let size = (nlen - 1) as usize; + let data = self.read_exact(size).await?; + file.write_all(&data).map_err(|e| IdeviceError::InternalError(e.to_string()))?; + } else { + let _ = self.read_exact((nlen - 1) as usize).await?; + } + } + let _ = dname; // unused + } + self.send_status_response(0, None, Some(plist::Value::Dictionary(Dictionary::new()))).await + } + + async fn read_be_u32(&mut self) -> Result { + let buf = self.idevice.read_raw(4).await?; + Ok(u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]])) + } + + async fn read_one(&mut self) -> Result { + let buf = self.idevice.read_raw(1).await?; + Ok(buf[0]) + } + + async fn read_exact(&mut self, size: usize) -> Result, IdeviceError> { + self.idevice.read_raw(size).await + } + + async fn read_exact_string(&mut self, size: usize) -> Result { + let buf = self.idevice.read_raw(size).await?; + Ok(String::from_utf8_lossy(&buf).to_string()) + } + + fn create_directory_from_message(dl_value: &plist::Value, host_dir: &Path) -> i64 { + if let plist::Value::Array(arr) = dl_value + && arr.len() >= 2 + && let Some(plist::Value::String(dir)) = arr.get(1) + { + let path = host_dir.join(dir); + return match fs::create_dir_all(&path) { Ok(_) => 0, Err(_) => -1 }; + } + -1 + } + + fn move_files_from_message(dl_value: &plist::Value, host_dir: &Path) -> i64 { + if let plist::Value::Array(arr) = dl_value + && arr.len() >= 2 + && let Some(plist::Value::Dictionary(map)) = arr.get(1) + { + for (from, to_v) in map.iter() { + if let Some(to) = to_v.as_string() { + let old = host_dir.join(from); + let newp = host_dir.join(to); + if let Some(parent) = newp.parent() { let _ = fs::create_dir_all(parent); } + if fs::rename(&old, &newp).is_err() { return -1; } + } + } + return 0; + } + -1 + } + + fn remove_files_from_message(dl_value: &plist::Value, host_dir: &Path) -> i64 { + if let plist::Value::Array(arr) = dl_value + && arr.len() >= 2 + && let Some(plist::Value::Array(items)) = arr.get(1) + { + for it in items { + if let Some(p) = it.as_string() { + let path = host_dir.join(p); + if path.is_dir() { + if fs::remove_dir_all(&path).is_err() { return -1; } + } else if path.exists() && fs::remove_file(&path).is_err() { return -1; } + } + } + return 0; + } + -1 + } + + fn copy_item_from_message(dl_value: &plist::Value, host_dir: &Path) -> i64 { + if let plist::Value::Array(arr) = dl_value + && arr.len() >= 3 + && let (Some(plist::Value::String(src)), Some(plist::Value::String(dst))) = (arr.get(1), arr.get(2)) + { + let from = host_dir.join(src); + let to = host_dir.join(dst); + if let Some(parent) = to.parent() { let _ = fs::create_dir_all(parent); } + if from.is_dir() { + return match fs::create_dir_all(&to) { Ok(_) => 0, Err(_) => -1 }; + } else { + return match fs::copy(&from, &to) { Ok(_) => 0, Err(_) => -1 }; + } + } + -1 + } + + /// Starts a restore using the typed RestoreOptions builder + #[deprecated(note = "Use restore_from_path; restore via BackupUUID is not supported by device/mobilebackup2")] + pub async fn start_restore_with( + &mut self, + _backup_uuid: &str, + opts: RestoreOptions, + ) -> Result<(), IdeviceError> { + let dict = opts.to_plist(); + // Avoid borrowing self during request + let target_udid_owned = self.idevice.udid().map(|s| s.to_string()); + let target_udid = target_udid_owned.as_deref(); + self.send_request( + BackupMessageType::BackupMessageTypeRestore.as_str(), + target_udid, + target_udid, + Some(dict), + ) + .await?; + + let response = self.receive_backup_response().await?; + if let Some(error) = response.get("ErrorCode") { + warn!("Restore start failed with error: {error:?}"); + return Err(IdeviceError::UnexpectedResponse); + } + debug!("Restore started successfully"); + Ok(()) + } + + /// Assert backup dir structure exists for a given source identifier (UDID) + fn assert_backup_exists(&self, backup_root: &Path, source: &str) -> Result<(), IdeviceError> { + let device_dir = backup_root.join(source); + if device_dir.join("Info.plist").exists() + && device_dir.join("Manifest.plist").exists() + && device_dir.join("Status.plist").exists() + { + Ok(()) + } else { + Err(IdeviceError::NotFound) + } + } + + /// Get backup information using DeviceLink against a given backup root/source + pub async fn info_from_path( + &mut self, + backup_root: &Path, + source_identifier: Option<&str>, + ) -> Result { + let target_udid = self.idevice.udid(); + let source = source_identifier.or(target_udid).ok_or(IdeviceError::InvalidHostID)?; + self.assert_backup_exists(backup_root, source)?; + + let mut dict = Dictionary::new(); + dict.insert("TargetIdentifier".into(), plist::Value::String(target_udid.unwrap().to_string())); + if let Some(src) = source_identifier { dict.insert("SourceIdentifier".into(), plist::Value::String(src.to_string())); } + self.send_device_link_message("Info", Some(dict)).await?; + + match self.process_restore_dl_loop(backup_root).await? { + Some(res) => Ok(res), + None => Err(IdeviceError::UnexpectedResponse), + } + } + + /// List last backup contents (returns raw response dictionary) + pub async fn list_from_path( + &mut self, + backup_root: &Path, + source_identifier: Option<&str>, + ) -> Result { + let target_udid = self.idevice.udid(); + let source = source_identifier.or(target_udid).ok_or(IdeviceError::InvalidHostID)?; + self.assert_backup_exists(backup_root, source)?; + + let mut dict = Dictionary::new(); + dict.insert("MessageName".into(), plist::Value::String("List".into())); + dict.insert("TargetIdentifier".into(), plist::Value::String(target_udid.unwrap().to_string())); + dict.insert("SourceIdentifier".into(), plist::Value::String(source.to_string())); + self.send_device_link_message("List", Some(dict)).await?; + + match self.process_restore_dl_loop(backup_root).await? { + Some(res) => Ok(res), + None => Err(IdeviceError::UnexpectedResponse), + } + } + + /// Unpack a complete backup to device hierarchy + pub async fn unback_from_path( + &mut self, + backup_root: &Path, + password: Option<&str>, + source_identifier: Option<&str>, + ) -> Result<(), IdeviceError> { + let target_udid = self.idevice.udid(); + let source = source_identifier.or(target_udid).ok_or(IdeviceError::InvalidHostID)?; + self.assert_backup_exists(backup_root, source)?; + + let mut dict = Dictionary::new(); + dict.insert("TargetIdentifier".into(), plist::Value::String(target_udid.unwrap().to_string())); + dict.insert("MessageName".into(), plist::Value::String("Unback".into())); + dict.insert("SourceIdentifier".into(), plist::Value::String(source.to_string())); + if let Some(pw) = password { dict.insert("Password".into(), plist::Value::String(pw.to_string())); } + self.send_device_link_message("Unback", Some(dict)).await?; + let _ = self.process_restore_dl_loop(backup_root).await?; + Ok(()) + } + + /// Extract a single file from a previous backup + pub async fn extract_from_path( + &mut self, + domain_name: &str, + relative_path: &str, + backup_root: &Path, + password: Option<&str>, + source_identifier: Option<&str>, + ) -> Result<(), IdeviceError> { + let target_udid = self.idevice.udid(); + let source = source_identifier.or(target_udid).ok_or(IdeviceError::InvalidHostID)?; + self.assert_backup_exists(backup_root, source)?; + + let mut dict = Dictionary::new(); + dict.insert("MessageName".into(), plist::Value::String("Extract".into())); + dict.insert("TargetIdentifier".into(), plist::Value::String(target_udid.unwrap().to_string())); + dict.insert("DomainName".into(), plist::Value::String(domain_name.to_string())); + dict.insert("RelativePath".into(), plist::Value::String(relative_path.to_string())); + dict.insert("SourceIdentifier".into(), plist::Value::String(source.to_string())); + if let Some(pw) = password { dict.insert("Password".into(), plist::Value::String(pw.to_string())); } + self.send_device_link_message("Extract", Some(dict)).await?; + let _ = self.process_restore_dl_loop(backup_root).await?; + Ok(()) + } + + /// Change backup password (enable/disable if new/old missing) + pub async fn change_password_from_path( + &mut self, + backup_root: &Path, + old: Option<&str>, + new: Option<&str>, + ) -> Result<(), IdeviceError> { + let target_udid = self.idevice.udid(); + let mut dict = Dictionary::new(); + dict.insert("MessageName".into(), plist::Value::String("ChangePassword".into())); + dict.insert("TargetIdentifier".into(), plist::Value::String(target_udid.ok_or(IdeviceError::InvalidHostID)?.to_string())); + if let Some(o) = old { dict.insert("OldPassword".into(), plist::Value::String(o.to_string())); } + if let Some(n) = new { dict.insert("NewPassword".into(), plist::Value::String(n.to_string())); } + self.send_device_link_message("ChangePassword", Some(dict)).await?; + let _ = self.process_restore_dl_loop(backup_root).await?; + Ok(()) + } + + /// Erase device via mobilebackup2 + pub async fn erase_device_from_path(&mut self, backup_root: &Path) -> Result<(), IdeviceError> { + let target_udid = self.idevice.udid(); + let mut dict = Dictionary::new(); + dict.insert("MessageName".into(), plist::Value::String("EraseDevice".into())); + dict.insert("TargetIdentifier".into(), plist::Value::String(target_udid.ok_or(IdeviceError::InvalidHostID)?.to_string())); + self.send_device_link_message("EraseDevice", Some(dict)).await?; + let _ = self.process_restore_dl_loop(backup_root).await?; + Ok(()) + } + + /// Gets free space information from the device + /// + /// # Returns + /// Free space in bytes + /// + /// # Errors + /// Returns `IdeviceError` if the request fails + pub async fn get_freespace(&mut self) -> Result { + // Not a valid host-initiated request in protocol; device asks via DLMessageGetFreeDiskSpace + Err(IdeviceError::UnexpectedResponse) + } + + /// Checks if backup encryption is enabled + /// + /// # Returns + /// `true` if backup encryption is enabled, `false` otherwise + /// + /// # Errors + /// Returns `IdeviceError` if the request fails + pub async fn check_backup_encryption(&mut self) -> Result { + // Not part of host-initiated MB2 protocol; caller should inspect Manifest/lockdown + Err(IdeviceError::UnexpectedResponse) + } + + /// Disconnects from the backup service + /// + /// # Returns + /// `Ok(())` on successful disconnection + /// + /// # Errors + /// Returns `IdeviceError` if disconnection fails + pub async fn disconnect(&mut self) -> Result<(), IdeviceError> { + // Send DLMessageDisconnect array per DeviceLink protocol + let arr = vec![ + plist::Value::String("DLMessageDisconnect".into()), + plist::Value::String("___EmptyParameterString___".into()), + ]; + self.send_dl_array(arr).await?; + debug!("Disconnected from backup service"); + Ok(()) + } +} \ No newline at end of file diff --git a/idevice/src/services/mod.rs b/idevice/src/services/mod.rs index f0fb2c6..fd341ec 100644 --- a/idevice/src/services/mod.rs +++ b/idevice/src/services/mod.rs @@ -27,6 +27,8 @@ pub mod lockdown; pub mod misagent; #[cfg(feature = "mobile_image_mounter")] pub mod mobile_image_mounter; +#[cfg(feature = "mobilebackup2")] +pub mod mobilebackup2; #[cfg(feature = "syslog_relay")] pub mod os_trace_relay; #[cfg(feature = "restore_service")] diff --git a/tools/Cargo.toml b/tools/Cargo.toml index 17d5510..2cc126e 100644 --- a/tools/Cargo.toml +++ b/tools/Cargo.toml @@ -97,6 +97,14 @@ path = "src/restore_service.rs" name = "companion_proxy" path = "src/companion_proxy.rs" +[[bin]] +name = "diagnostics" +path = "src/diagnostics.rs" + +[[bin]] +name = "mobilebackup2" +path = "src/mobilebackup2.rs" + [dependencies] idevice = { path = "../idevice", features = ["full"], default-features = false } tokio = { version = "1.43", features = ["full"] } diff --git a/tools/src/diagnostics.rs b/tools/src/diagnostics.rs new file mode 100644 index 0000000..c6a007d --- /dev/null +++ b/tools/src/diagnostics.rs @@ -0,0 +1,300 @@ +// Jackson Coxson +// idevice Rust implementation of libimobiledevice's idevicediagnostics + +use clap::{Arg, Command, ArgMatches}; +use idevice::{services::diagnostics_relay::DiagnosticsRelayClient, IdeviceService}; + +mod common; + +#[tokio::main] +async fn main() { + env_logger::init(); + + let matches = Command::new("idevicediagnostics") + .about("Interact with the diagnostics interface of a device") + .arg( + Arg::new("host") + .long("host") + .value_name("HOST") + .help("IP address of the device"), + ) + .arg( + Arg::new("pairing_file") + .long("pairing-file") + .value_name("PATH") + .help("Path to the pairing file"), + ) + .arg( + Arg::new("udid") + .value_name("UDID") + .help("UDID of the device (overrides host/pairing file)") + .index(1), + ) + .arg( + Arg::new("about") + .long("about") + .help("Show about information") + .action(clap::ArgAction::SetTrue), + ) + .subcommand( + Command::new("ioregistry") + .about("Print IORegistry information") + .arg( + Arg::new("plane") + .long("plane") + .value_name("PLANE") + .help("IORegistry plane to query (e.g., IODeviceTree, IOService)") + ) + .arg( + Arg::new("name") + .long("name") + .value_name("NAME") + .help("Entry name to filter by") + ) + .arg( + Arg::new("class") + .long("class") + .value_name("CLASS") + .help("Entry class to filter by") + ) + ) + .subcommand( + Command::new("mobilegestalt") + .about("Print MobileGestalt information") + .arg( + Arg::new("keys") + .long("keys") + .value_name("KEYS") + .help("Comma-separated list of keys to query") + .value_delimiter(',') + .num_args(1..) + ) + ) + .subcommand( + Command::new("gasguage") + .about("Print gas gauge (battery) information") + ) + .subcommand( + Command::new("nand") + .about("Print NAND flash information") + ) + .subcommand( + Command::new("all") + .about("Print all available diagnostics information") + ) + .subcommand( + Command::new("wifi") + .about("Print WiFi diagnostics information") + ) + .subcommand( + Command::new("goodbye") + .about("Send Goodbye to diagnostics relay") + ) + .subcommand( + Command::new("restart") + .about("Restart the device") + ) + .subcommand( + Command::new("shutdown") + .about("Shutdown the device") + ) + .subcommand( + Command::new("sleep") + .about("Put the device to sleep") + ) + .get_matches(); + + if matches.get_flag("about") { + println!("idevicediagnostics - interact with the diagnostics interface of a device. Reimplementation of libimobiledevice's binary."); + println!("Copyright (c) 2025 Jackson Coxson"); + return; + } + + let udid = matches.get_one::("udid"); + let host = matches.get_one::("host"); + let pairing_file = matches.get_one::("pairing_file"); + + let provider = + match common::get_provider(udid, host, pairing_file, "idevicediagnostics-jkcoxson").await { + Ok(p) => p, + Err(e) => { + eprintln!("{e}"); + return; + } + }; + + let mut diagnostics_client = match DiagnosticsRelayClient::connect(&*provider).await { + Ok(client) => client, + Err(e) => { + eprintln!("Unable to connect to diagnostics relay: {e:?}"); + return; + } + }; + + match matches.subcommand() { + Some(("ioregistry", sub_matches)) => { + handle_ioregistry(&mut diagnostics_client, sub_matches).await; + } + Some(("mobilegestalt", sub_matches)) => { + handle_mobilegestalt(&mut diagnostics_client, sub_matches).await; + } + Some(("gasguage", _)) => { + handle_gasguage(&mut diagnostics_client).await; + } + Some(("nand", _)) => { + handle_nand(&mut diagnostics_client).await; + } + Some(("all", _)) => { + handle_all(&mut diagnostics_client).await; + } + Some(("wifi", _)) => { + handle_wifi(&mut diagnostics_client).await; + } + Some(("restart", _)) => { + handle_restart(&mut diagnostics_client).await; + } + Some(("shutdown", _)) => { + handle_shutdown(&mut diagnostics_client).await; + } + Some(("sleep", _)) => { + handle_sleep(&mut diagnostics_client).await; + } + Some(("goodbye", _)) => { + handle_goodbye(&mut diagnostics_client).await; + } + _ => { + eprintln!("No subcommand specified. Use --help for usage information."); + } + } +} + +async fn handle_ioregistry(client: &mut DiagnosticsRelayClient, matches: &ArgMatches) { + let plane = matches.get_one::("plane").map(|s| s.as_str()); + let name = matches.get_one::("name").map(|s| s.as_str()); + let class = matches.get_one::("class").map(|s| s.as_str()); + + match client.ioregistry(plane, name, class).await { + Ok(Some(data)) => { + println!("{data:#?}"); + } + Ok(None) => { + println!("No IORegistry data returned"); + } + Err(e) => { + eprintln!("Failed to get IORegistry data: {e:?}"); + } + } +} + +async fn handle_mobilegestalt(client: &mut DiagnosticsRelayClient, matches: &ArgMatches) { + let keys = matches.get_many::("keys") + .map(|values| values.map(|s| s.to_string()).collect::>()); + + match client.mobilegestalt(keys).await { + Ok(Some(data)) => { + println!("{data:#?}"); + } + Ok(None) => { + println!("No MobileGestalt data returned"); + } + Err(e) => { + eprintln!("Failed to get MobileGestalt data: {e:?}"); + } + } +} + +async fn handle_gasguage(client: &mut DiagnosticsRelayClient) { + match client.gasguage().await { + Ok(Some(data)) => { + println!("{data:#?}"); + } + Ok(None) => { + println!("No gas gauge data returned"); + } + Err(e) => { + eprintln!("Failed to get gas gauge data: {e:?}"); + } + } +} + +async fn handle_nand(client: &mut DiagnosticsRelayClient) { + match client.nand().await { + Ok(Some(data)) => { + println!("{data:#?}"); + } + Ok(None) => { + println!("No NAND data returned"); + } + Err(e) => { + eprintln!("Failed to get NAND data: {e:?}"); + } + } +} + +async fn handle_all(client: &mut DiagnosticsRelayClient) { + match client.all().await { + Ok(Some(data)) => { + println!("{data:#?}"); + } + Ok(None) => { + println!("No diagnostics data returned"); + } + Err(e) => { + eprintln!("Failed to get all diagnostics data: {e:?}"); + } + } +} + +async fn handle_wifi(client: &mut DiagnosticsRelayClient) { + match client.wifi().await { + Ok(Some(data)) => { + println!("{data:#?}"); + } + Ok(None) => { + println!("No WiFi diagnostics returned"); + } + Err(e) => { + eprintln!("Failed to get WiFi diagnostics: {e:?}"); + } + } +} + +async fn handle_restart(client: &mut DiagnosticsRelayClient) { + match client.restart().await { + Ok(()) => { + println!("Device restart command sent successfully"); + } + Err(e) => { + eprintln!("Failed to restart device: {e:?}"); + } + } +} + +async fn handle_shutdown(client: &mut DiagnosticsRelayClient) { + match client.shutdown().await { + Ok(()) => { + println!("Device shutdown command sent successfully"); + } + Err(e) => { + eprintln!("Failed to shutdown device: {e:?}"); + } + } +} + +async fn handle_sleep(client: &mut DiagnosticsRelayClient) { + match client.sleep().await { + Ok(()) => { + println!("Device sleep command sent successfully"); + } + Err(e) => { + eprintln!("Failed to put device to sleep: {e:?}"); + } + } +} + +async fn handle_goodbye(client: &mut DiagnosticsRelayClient) { + match client.goodbye().await { + Ok(()) => println!("Goodbye acknowledged by device"), + Err(e) => eprintln!("Goodbye failed: {e:?}"), + } +} \ No newline at end of file diff --git a/tools/src/mobilebackup2.rs b/tools/src/mobilebackup2.rs new file mode 100644 index 0000000..4ec7259 --- /dev/null +++ b/tools/src/mobilebackup2.rs @@ -0,0 +1,562 @@ +// Jackson Coxson +// Mobile Backup 2 tool for iOS devices + +use clap::{Arg, Command}; +use idevice::{mobilebackup2::{MobileBackup2Client, RestoreOptions}, IdeviceService}; +use plist::Dictionary; +use std::fs; +use std::io::{Read, Write}; +use std::path::Path; + +mod common; + +#[tokio::main] +async fn main() { + env_logger::init(); + + let matches = Command::new("mobilebackup2") + .about("Mobile Backup 2 tool for iOS devices") + .arg( + Arg::new("host") + .long("host") + .value_name("HOST") + .help("IP address of the device"), + ) + .arg( + Arg::new("pairing_file") + .long("pairing-file") + .value_name("PATH") + .help("Path to the pairing file"), + ) + .arg( + Arg::new("udid") + .value_name("UDID") + .help("UDID of the device (overrides host/pairing file)") + .index(1), + ) + .arg( + Arg::new("about") + .long("about") + .help("Show about information") + .action(clap::ArgAction::SetTrue), + ) + .subcommand( + Command::new("info") + .about("Get backup information from a local backup directory") + .arg(Arg::new("dir").long("dir").value_name("DIR").required(true)) + .arg(Arg::new("source").long("source").value_name("SOURCE").help("Source identifier (defaults to current UDID)")) + ) + .subcommand( + Command::new("list") + .about("List files of the last backup from a local backup directory") + .arg(Arg::new("dir").long("dir").value_name("DIR").required(true)) + .arg(Arg::new("source").long("source").value_name("SOURCE")) + ) + .subcommand( + Command::new("backup") + .about("Start a backup operation") + .arg( + Arg::new("dir") + .long("dir") + .value_name("DIR") + .help("Backup directory on host") + .required(true), + ) + .arg( + Arg::new("target") + .long("target") + .value_name("TARGET") + .help("Target identifier for the backup"), + ) + .arg( + Arg::new("source") + .long("source") + .value_name("SOURCE") + .help("Source identifier for the backup"), + ), + ) + .subcommand( + Command::new("restore") + .about("Restore from a local backup directory (DeviceLink)") + .arg(Arg::new("dir").long("dir").value_name("DIR").required(true)) + .arg(Arg::new("source").long("source").value_name("SOURCE").help("Source UDID; defaults to current device UDID")) + .arg(Arg::new("password").long("password").value_name("PWD").help("Backup password if encrypted")) + .arg(Arg::new("no-reboot").long("no-reboot").action(clap::ArgAction::SetTrue)) + .arg(Arg::new("no-copy").long("no-copy").action(clap::ArgAction::SetTrue)) + .arg(Arg::new("no-settings").long("no-settings").action(clap::ArgAction::SetTrue)) + .arg(Arg::new("system").long("system").action(clap::ArgAction::SetTrue)) + .arg(Arg::new("remove").long("remove").action(clap::ArgAction::SetTrue)) + ) + .subcommand( + Command::new("unback") + .about("Unpack a complete backup to device hierarchy") + .arg(Arg::new("dir").long("dir").value_name("DIR").required(true)) + .arg(Arg::new("source").long("source").value_name("SOURCE")) + .arg(Arg::new("password").long("password").value_name("PWD")) + ) + .subcommand( + Command::new("extract") + .about("Extract a file from a previous backup") + .arg(Arg::new("dir").long("dir").value_name("DIR").required(true)) + .arg(Arg::new("source").long("source").value_name("SOURCE")) + .arg(Arg::new("domain").long("domain").value_name("DOMAIN").required(true)) + .arg(Arg::new("path").long("path").value_name("REL_PATH").required(true)) + .arg(Arg::new("password").long("password").value_name("PWD")) + ) + .subcommand( + Command::new("change-password") + .about("Change backup password") + .arg(Arg::new("dir").long("dir").value_name("DIR").required(true)) + .arg(Arg::new("old").long("old").value_name("OLD")) + .arg(Arg::new("new").long("new").value_name("NEW")) + ) + .subcommand( + Command::new("erase-device") + .about("Erase the device via mobilebackup2") + .arg(Arg::new("dir").long("dir").value_name("DIR").required(true)) + ) + .subcommand(Command::new("freespace").about("Get free space information")) + .subcommand(Command::new("encryption").about("Check backup encryption status")) + .get_matches(); + + if matches.get_flag("about") { + println!("mobilebackup2 - manage device backups using Mobile Backup 2 service"); + println!("Copyright (c) 2025 Jackson Coxson"); + return; + } + + let udid = matches.get_one::("udid"); + let host = matches.get_one::("host"); + let pairing_file = matches.get_one::("pairing_file"); + + let provider = match common::get_provider(udid, host, pairing_file, "mobilebackup2-jkcoxson").await + { + Ok(p) => p, + Err(e) => { + eprintln!("Error creating provider: {e}"); + return; + } + }; + + let mut backup_client = match MobileBackup2Client::connect(&*provider).await { + Ok(client) => client, + Err(e) => { + eprintln!("Unable to connect to mobilebackup2 service: {e}"); + return; + } + }; + + match matches.subcommand() { + Some(("info", sub)) => { + let dir = sub.get_one::("dir").unwrap(); + let source = sub.get_one::("source").map(|s| s.as_str()); + match backup_client.info_from_path(Path::new(dir), source).await { + Ok(dict) => { + println!("Backup Information:"); + for (k, v) in dict { println!(" {k}: {v:?}"); } + } + Err(e) => eprintln!("Failed to get info: {e}"), + } + } + Some(("list", sub)) => { + let dir = sub.get_one::("dir").unwrap(); + let source = sub.get_one::("source").map(|s| s.as_str()); + match backup_client.list_from_path(Path::new(dir), source).await { + Ok(dict) => { + println!("List Response:"); + for (k, v) in dict { println!(" {k}: {v:?}"); } + } + Err(e) => eprintln!("Failed to list: {e}"), + } + } + Some(("backup", sub_matches)) => { + let target = sub_matches.get_one::("target").map(|s| s.as_str()); + let source = sub_matches.get_one::("source").map(|s| s.as_str()); + let dir = sub_matches.get_one::("dir").expect("dir is required"); + + println!("Starting backup operation..."); + let res = backup_client + .send_request("Backup", target, source, None::) + .await; + if let Err(e) = res { + eprintln!("Failed to send backup request: {e}"); + } else if let Err(e) = process_dl_loop(&mut backup_client, Path::new(dir)).await { + eprintln!("Backup failed during DL loop: {e}"); + } else { + println!("Backup flow finished"); + } + } + Some(("restore", sub)) => { + let dir = sub.get_one::("dir").unwrap(); + let source = sub.get_one::("source").map(|s| s.as_str()); + let mut ropts = RestoreOptions::new(); + if sub.get_flag("no-reboot") { ropts = ropts.with_reboot(false); } + if sub.get_flag("no-copy") { ropts = ropts.with_copy(false); } + if sub.get_flag("no-settings") { ropts = ropts.with_preserve_settings(false); } + if sub.get_flag("system") { ropts = ropts.with_system_files(true); } + if sub.get_flag("remove") { ropts = ropts.with_remove_items_not_restored(true); } + if let Some(pw) = sub.get_one::("password") { ropts = ropts.with_password(pw); } + match backup_client.restore_from_path(Path::new(dir), source, Some(ropts)).await { + Ok(_) => println!("Restore flow finished"), + Err(e) => eprintln!("Restore failed: {e}"), + } + } + Some(("unback", sub)) => { + let dir = sub.get_one::("dir").unwrap(); + let source = sub.get_one::("source").map(|s| s.as_str()); + let password = sub.get_one::("password").map(|s| s.as_str()); + match backup_client.unback_from_path(Path::new(dir), password, source).await { + Ok(_) => println!("Unback finished"), + Err(e) => eprintln!("Unback failed: {e}"), + } + } + Some(("extract", sub)) => { + let dir = sub.get_one::("dir").unwrap(); + let source = sub.get_one::("source").map(|s| s.as_str()); + let domain = sub.get_one::("domain").unwrap(); + let rel = sub.get_one::("path").unwrap(); + let password = sub.get_one::("password").map(|s| s.as_str()); + match backup_client.extract_from_path(domain, rel, Path::new(dir), password, source).await { + Ok(_) => println!("Extract finished"), + Err(e) => eprintln!("Extract failed: {e}"), + } + } + Some(("change-password", sub)) => { + let dir = sub.get_one::("dir").unwrap(); + let old = sub.get_one::("old").map(|s| s.as_str()); + let newv = sub.get_one::("new").map(|s| s.as_str()); + match backup_client.change_password_from_path(Path::new(dir), old, newv).await { + Ok(_) => println!("Change password finished"), + Err(e) => eprintln!("Change password failed: {e}"), + } + } + Some(("erase-device", sub)) => { + let dir = sub.get_one::("dir").unwrap(); + match backup_client.erase_device_from_path(Path::new(dir)).await { + Ok(_) => println!("Erase device command sent"), + Err(e) => eprintln!("Erase device failed: {e}"), + } + } + Some(("freespace", _)) => { + match backup_client.get_freespace().await { + Ok(freespace) => { + let freespace_gb = freespace as f64 / (1024.0 * 1024.0 * 1024.0); + println!("Free space: {freespace} bytes ({freespace_gb:.2} GB)"); + } + Err(e) => eprintln!("Failed to get free space: {e}"), + } + } + Some(("encryption", _)) => { + match backup_client.check_backup_encryption().await { + Ok(is_encrypted) => { + println!("Backup encryption: {}", if is_encrypted { "Enabled" } else { "Disabled" }); + } + Err(e) => eprintln!("Failed to check backup encryption: {e}"), + } + } + _ => { + println!("No subcommand provided. Use --help for available commands."); + } + } + + // Disconnect from the service + if let Err(e) = backup_client.disconnect().await { + eprintln!("Warning: Failed to disconnect cleanly: {e}"); + } +} + +use idevice::services::mobilebackup2::{ + DL_CODE_ERROR_LOCAL as CODE_ERROR_LOCAL, + DL_CODE_FILE_DATA as CODE_FILE_DATA, + DL_CODE_SUCCESS as CODE_SUCCESS, +}; + +async fn process_dl_loop( + client: &mut MobileBackup2Client, + host_dir: &Path, +) -> Result, idevice::IdeviceError> { + loop { + let (tag, value) = client.receive_dl_message().await?; + match tag.as_str() { + "DLMessageDownloadFiles" => { + handle_download_files(client, &value, host_dir).await?; + } + "DLMessageUploadFiles" => { + handle_upload_files(client, &value, host_dir).await?; + } + "DLMessageGetFreeDiskSpace" => { + // Minimal implementation: report unknown/zero with success + client + .send_status_response(0, None, Some(plist::Value::Integer(0u64.into()))) + .await?; + } + "DLContentsOfDirectory" => { + // Minimal: return empty listing + let empty = plist::Value::Dictionary(Dictionary::new()); + client.send_status_response(0, None, Some(empty)).await?; + } + "DLMessageCreateDirectory" => { + let status = create_directory_from_message(&value, host_dir); + client + .send_status_response(status, None, None) + .await?; + } + "DLMessageMoveFiles" | "DLMessageMoveItems" => { + let status = move_files_from_message(&value, host_dir); + client + .send_status_response(status, None, Some(plist::Value::Dictionary(Dictionary::new()))) + .await?; + } + "DLMessageRemoveFiles" | "DLMessageRemoveItems" => { + let status = remove_files_from_message(&value, host_dir); + client + .send_status_response(status, None, Some(plist::Value::Dictionary(Dictionary::new()))) + .await?; + } + "DLMessageCopyItem" => { + let status = copy_item_from_message(&value, host_dir); + client + .send_status_response(status, None, Some(plist::Value::Dictionary(Dictionary::new()))) + .await?; + } + "DLMessageProcessMessage" => { + // Final status/content: return inner dict + if let plist::Value::Array(arr) = value + && let Some(plist::Value::Dictionary(dict)) = arr.get(1) + { + return Ok(Some(dict.clone())); + } + return Ok(None); + } + "DLMessageDisconnect" => { + return Ok(None); + } + other => { + eprintln!("Unsupported DL message: {other}"); + client + .send_status_response(-1, Some("Operation not supported"), None) + .await?; + } + } + } +} + +async fn handle_download_files( + client: &mut MobileBackup2Client, + dl_value: &plist::Value, + host_dir: &Path, +) -> Result<(), idevice::IdeviceError> { + // dl_value is an array: ["DLMessageDownloadFiles", [paths...], progress?] + let mut err_any = false; + if let plist::Value::Array(arr) = dl_value + && arr.len() >= 2 + && let Some(plist::Value::Array(files)) = arr.get(1) + { + for pv in files { + if let Some(path) = pv.as_string() + && let Err(e) = send_single_file(client, host_dir, path).await + { + eprintln!("Failed to send file {path}: {e}"); + err_any = true; + } + } + } + // terminating zero dword + client + .idevice + .send_raw(&0u32.to_be_bytes()) + .await?; + // status response + if err_any { + client + .send_status_response(-13, Some("Multi status"), Some(plist::Value::Dictionary(Dictionary::new()))) + .await + } else { + client + .send_status_response(0, None, Some(plist::Value::Dictionary(Dictionary::new()))) + .await + } +} + +async fn send_single_file( + client: &mut MobileBackup2Client, + host_dir: &Path, + rel_path: &str, +) -> Result<(), idevice::IdeviceError> { + let full = host_dir.join(rel_path); + let path_bytes = rel_path.as_bytes().to_vec(); + let nlen = (path_bytes.len() as u32).to_be_bytes(); + client.idevice.send_raw(&nlen).await?; + client.idevice.send_raw(&path_bytes).await?; + + let mut f = match std::fs::File::open(&full) { + Ok(f) => f, + Err(e) => { + // send error + let desc = e.to_string(); + let size = (desc.len() as u32 + 1).to_be_bytes(); + let mut hdr = Vec::with_capacity(5); + hdr.extend_from_slice(&size); + hdr.push(CODE_ERROR_LOCAL); + client.idevice.send_raw(&hdr).await?; + client.idevice.send_raw(desc.as_bytes()).await?; + return Ok(()); + } + }; + let mut buf = [0u8; 32768]; + loop { + let read = f.read(&mut buf).unwrap_or(0); + if read == 0 { + break; + } + let size = ((read as u32) + 1).to_be_bytes(); + let mut hdr = Vec::with_capacity(5); + hdr.extend_from_slice(&size); + hdr.push(CODE_FILE_DATA); + client.idevice.send_raw(&hdr).await?; + client.idevice.send_raw(&buf[..read]).await?; + } + // success trailer + let mut ok = [0u8; 5]; + ok[..4].copy_from_slice(&1u32.to_be_bytes()); + ok[4] = CODE_SUCCESS; + client.idevice.send_raw(&ok).await?; + Ok(()) +} + +async fn handle_upload_files( + client: &mut MobileBackup2Client, + _dl_value: &plist::Value, + host_dir: &Path, +) -> Result<(), idevice::IdeviceError> { + // Minimal receiver: read pairs of (dir, filename) and block stream + // Receive dir name + loop { + let dlen = read_be_u32(client).await?; + if dlen == 0 { + break; + } + let dname = read_exact_string(client, dlen as usize).await?; + let flen = read_be_u32(client).await?; + if flen == 0 { + break; + } + let fname = read_exact_string(client, flen as usize).await?; + let dst = host_dir.join(&fname); + if let Some(parent) = dst.parent() { + let _ = fs::create_dir_all(parent); + } + let mut file = std::fs::File::create(&dst).map_err(|e| idevice::IdeviceError::InternalError(e.to_string()))?; + loop { + let nlen = read_be_u32(client).await?; + if nlen == 0 { + break; + } + let code = read_one(client).await?; + if code == CODE_FILE_DATA { + let size = (nlen - 1) as usize; + let data = read_exact(client, size).await?; + file.write_all(&data).map_err(|e| idevice::IdeviceError::InternalError(e.to_string()))?; + } else { + let _ = read_exact(client, (nlen - 1) as usize).await?; + } + } + let _ = dname; // not used + } + client + .send_status_response(0, None, Some(plist::Value::Dictionary(Dictionary::new()))) + .await +} + +async fn read_be_u32(client: &mut MobileBackup2Client) -> Result { + let buf = client.idevice.read_raw(4).await?; + Ok(u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]])) +} + +async fn read_one(client: &mut MobileBackup2Client) -> Result { + let buf = client.idevice.read_raw(1).await?; + Ok(buf[0]) +} + +async fn read_exact(client: &mut MobileBackup2Client, size: usize) -> Result, idevice::IdeviceError> { + client.idevice.read_raw(size).await +} + +async fn read_exact_string(client: &mut MobileBackup2Client, size: usize) -> Result { + let buf = client.idevice.read_raw(size).await?; + Ok(String::from_utf8_lossy(&buf).to_string()) +} + +fn create_directory_from_message(dl_value: &plist::Value, host_dir: &Path) -> i64 { + if let plist::Value::Array(arr) = dl_value + && arr.len() >= 2 + && let Some(plist::Value::String(dir)) = arr.get(1) + { + let path = host_dir.join(dir); + return match fs::create_dir_all(&path) { + Ok(_) => 0, + Err(_) => -1, + }; + } + -1 +} + +fn move_files_from_message(dl_value: &plist::Value, host_dir: &Path) -> i64 { + if let plist::Value::Array(arr) = dl_value + && arr.len() >= 2 + && let Some(plist::Value::Dictionary(map)) = arr.get(1) + { + for (from, to_v) in map.iter() { + if let Some(to) = to_v.as_string() { + let old = host_dir.join(from); + let newp = host_dir.join(to); + if let Some(parent) = newp.parent() { + let _ = fs::create_dir_all(parent); + } + if fs::rename(&old, &newp).is_err() { + return -1; + } + } + } + return 0; + } + -1 +} + +fn remove_files_from_message(dl_value: &plist::Value, host_dir: &Path) -> i64 { + if let plist::Value::Array(arr) = dl_value + && arr.len() >= 2 + && let Some(plist::Value::Array(items)) = arr.get(1) + { + for it in items { + if let Some(p) = it.as_string() { + let path = host_dir.join(p); + if path.is_dir() { + if fs::remove_dir_all(&path).is_err() { return -1; } + } else if path.exists() && fs::remove_file(&path).is_err() { + return -1; + } + } + } + return 0; + } + -1 +} + +fn copy_item_from_message(dl_value: &plist::Value, host_dir: &Path) -> i64 { + if let plist::Value::Array(arr) = dl_value + && arr.len() >= 3 + && let (Some(plist::Value::String(src)), Some(plist::Value::String(dst))) = (arr.get(1), arr.get(2)) + { + let from = host_dir.join(src); + let to = host_dir.join(dst); + if let Some(parent) = to.parent() { let _ = fs::create_dir_all(parent); } + if from.is_dir() { + // shallow copy: create dir + return match fs::create_dir_all(&to) { Ok(_) => 0, Err(_) => -1 }; + } else { + return match fs::copy(&from, &to) { Ok(_) => 0, Err(_) => -1 }; + } + } + -1 +} \ No newline at end of file From 114397ee1c929b6c9a33d8dfe51a006d467f547c Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Wed, 13 Aug 2025 08:01:44 -0600 Subject: [PATCH 058/126] cargofmt --- idevice/src/services/mobilebackup2.rs | 411 +++++++++++++++++++------- 1 file changed, 299 insertions(+), 112 deletions(-) diff --git a/idevice/src/services/mobilebackup2.rs b/idevice/src/services/mobilebackup2.rs index 2276e8a..23b5f05 100644 --- a/idevice/src/services/mobilebackup2.rs +++ b/idevice/src/services/mobilebackup2.rs @@ -5,10 +5,10 @@ use log::{debug, warn}; use plist::Dictionary; -use tokio::io::AsyncReadExt; use std::fs; use std::io::{Read, Write}; use std::path::Path; +use tokio::io::AsyncReadExt; use crate::{Idevice, IdeviceError, IdeviceService, obf}; @@ -156,21 +156,56 @@ impl Default for RestoreOptions { } impl RestoreOptions { - pub fn new() -> Self { Self::default() } - pub fn with_reboot(mut self, reboot: bool) -> Self { self.reboot = reboot; self } - pub fn with_copy(mut self, copy: bool) -> Self { self.copy = copy; self } - pub fn with_preserve_settings(mut self, preserve: bool) -> Self { self.preserve_settings = preserve; self } - pub fn with_system_files(mut self, system: bool) -> Self { self.system_files = system; self } - pub fn with_remove_items_not_restored(mut self, remove: bool) -> Self { self.remove_items_not_restored = remove; self } - pub fn with_password(mut self, password: impl Into) -> Self { self.password = Some(password.into()); self } + pub fn new() -> Self { + Self::default() + } + pub fn with_reboot(mut self, reboot: bool) -> Self { + self.reboot = reboot; + self + } + pub fn with_copy(mut self, copy: bool) -> Self { + self.copy = copy; + self + } + pub fn with_preserve_settings(mut self, preserve: bool) -> Self { + self.preserve_settings = preserve; + self + } + pub fn with_system_files(mut self, system: bool) -> Self { + self.system_files = system; + self + } + pub fn with_remove_items_not_restored(mut self, remove: bool) -> Self { + self.remove_items_not_restored = remove; + self + } + pub fn with_password(mut self, password: impl Into) -> Self { + self.password = Some(password.into()); + self + } pub fn to_plist(&self) -> Dictionary { let mut opts = Dictionary::new(); - opts.insert("RestoreShouldReboot".into(), plist::Value::Boolean(self.reboot)); - opts.insert("RestoreDontCopyBackup".into(), plist::Value::Boolean(!self.copy)); - opts.insert("RestorePreserveSettings".into(), plist::Value::Boolean(self.preserve_settings)); - opts.insert("RestoreSystemFiles".into(), plist::Value::Boolean(self.system_files)); - opts.insert("RemoveItemsNotRestored".into(), plist::Value::Boolean(self.remove_items_not_restored)); + opts.insert( + "RestoreShouldReboot".into(), + plist::Value::Boolean(self.reboot), + ); + opts.insert( + "RestoreDontCopyBackup".into(), + plist::Value::Boolean(!self.copy), + ); + opts.insert( + "RestorePreserveSettings".into(), + plist::Value::Boolean(self.preserve_settings), + ); + opts.insert( + "RestoreSystemFiles".into(), + plist::Value::Boolean(self.system_files), + ); + opts.insert( + "RemoveItemsNotRestored".into(), + plist::Value::Boolean(self.remove_items_not_restored), + ); if let Some(pw) = &self.password { opts.insert("Password".into(), plist::Value::String(pw.clone())); } @@ -184,7 +219,7 @@ impl MobileBackup2Client { /// # Arguments /// * `idevice` - Pre-established device connection pub fn new(idevice: Idevice) -> Self { - Self { + Self { idevice, protocol_version: 0.0, } @@ -224,9 +259,7 @@ impl MobileBackup2Client { /// Sends a raw DL array as binary plist async fn send_dl_array(&mut self, array: Vec) -> Result<(), IdeviceError> { - self.idevice - .send_bplist(plist::Value::Array(array)) - .await + self.idevice.send_bplist(plist::Value::Array(array)).await } /// Receives any DL* message and returns (message_tag, full_array_value) @@ -262,17 +295,21 @@ impl MobileBackup2Client { /// Returns `IdeviceError` if version exchange fails async fn version_exchange(&mut self) -> Result<(), IdeviceError> { debug!("Starting mobilebackup2 version exchange"); - + // Send supported protocol versions (matching libimobiledevice) let mut hello_dict = Dictionary::new(); let versions = vec![plist::Value::Real(2.0), plist::Value::Real(2.1)]; - hello_dict.insert("SupportedProtocolVersions".into(), plist::Value::Array(versions)); - - self.send_device_link_message("Hello", Some(hello_dict)).await?; - + hello_dict.insert( + "SupportedProtocolVersions".into(), + plist::Value::Array(versions), + ); + + self.send_device_link_message("Hello", Some(hello_dict)) + .await?; + // Receive response let response = self.receive_device_link_message("Response").await?; - + // Check for error if let Some(error_code) = response.get("ErrorCode") && let Some(code) = error_code.as_unsigned_integer() @@ -281,7 +318,7 @@ impl MobileBackup2Client { warn!("Version exchange failed with error code: {code}"); return Err(IdeviceError::UnexpectedResponse); } - + // Get negotiated protocol version if let Some(version) = response.get("ProtocolVersion").and_then(|v| v.as_real()) { self.protocol_version = version; @@ -290,7 +327,7 @@ impl MobileBackup2Client { warn!("No protocol version in response"); return Err(IdeviceError::UnexpectedResponse); } - + Ok(()) } @@ -315,19 +352,19 @@ impl MobileBackup2Client { // Create DLMessageProcessMessage array format let mut message_array = Vec::new(); message_array.push(plist::Value::String("DLMessageProcessMessage".into())); - + // Create the actual message dictionary let mut message_dict = Dictionary::new(); message_dict.insert("MessageName".into(), message_name.into()); - + if let Some(opts) = options { for (key, value) in opts { message_dict.insert(key, value); } } - + message_array.push(plist::Value::Dictionary(message_dict)); - + debug!("Sending device link message: {message_name}"); self.idevice .send_bplist(plist::Value::Array(message_array)) @@ -344,7 +381,10 @@ impl MobileBackup2Client { /// /// # Errors /// Returns `IdeviceError` if communication fails or message name doesn't match - async fn receive_device_link_message(&mut self, expected_message: &str) -> Result { + async fn receive_device_link_message( + &mut self, + expected_message: &str, + ) -> Result { // Read raw bytes and parse as plist::Value to handle array format if let Some(socket) = &mut self.idevice.socket { debug!("Reading response size"); @@ -354,7 +394,7 @@ impl MobileBackup2Client { let mut buf = vec![0; len as usize]; socket.read_exact(&mut buf).await?; let response_value: plist::Value = plist::from_bytes(&buf)?; - + // Parse DLMessageProcessMessage format if let plist::Value::Array(array) = response_value && array.len() >= 2 @@ -364,7 +404,8 @@ impl MobileBackup2Client { { // Check MessageName if expected if !expected_message.is_empty() { - if let Some(message_name) = dict.get("MessageName").and_then(|v| v.as_string()) { + if let Some(message_name) = dict.get("MessageName").and_then(|v| v.as_string()) + { if message_name != expected_message { warn!("Expected message '{expected_message}', got '{message_name}'"); return Err(IdeviceError::UnexpectedResponse); @@ -376,7 +417,7 @@ impl MobileBackup2Client { } return Ok(dict.clone()); } - + warn!("Invalid device link message format"); Err(IdeviceError::UnexpectedResponse) } else { @@ -400,7 +441,8 @@ impl MobileBackup2Client { message_type: BackupMessageType, options: Option, ) -> Result<(), IdeviceError> { - self.send_device_link_message(message_type.as_str(), options).await + self.send_device_link_message(message_type.as_str(), options) + .await } /// Sends a MobileBackup2 request with proper envelope and identifiers @@ -463,15 +505,15 @@ impl MobileBackup2Client { // Per protocol use MessageName "Info" self.send_backup_message(BackupMessageType::BackupMessageTypeInfo, None) .await?; - + let response = self.receive_backup_response().await?; - + // Check for error in response if let Some(error) = response.get("ErrorCode") { warn!("Backup info request failed with error: {error:?}"); return Err(IdeviceError::UnexpectedResponse); } - + Ok(response) } @@ -485,17 +527,17 @@ impl MobileBackup2Client { pub async fn list_backups(&mut self) -> Result, IdeviceError> { self.send_backup_message(BackupMessageType::BackupMessageTypeList, None) .await?; - + let response = self.receive_backup_response().await?; - + // Check for error in response if let Some(error) = response.get("ErrorCode") { warn!("List backups request failed with error: {error:?}"); return Err(IdeviceError::UnexpectedResponse); } - + let mut backups = Vec::new(); - + if let Some(plist::Value::Array(backup_list)) = response.get("BackupList") { for backup_item in backup_list { if let plist::Value::Dictionary(backup_dict) = backup_item { @@ -504,35 +546,35 @@ impl MobileBackup2Client { .and_then(|v| v.as_string()) .unwrap_or_default() .to_string(); - + let device_name = backup_dict .get("DeviceName") .and_then(|v| v.as_string()) .unwrap_or_default() .to_string(); - + let display_name = backup_dict .get("DisplayName") .and_then(|v| v.as_string()) .unwrap_or_default() .to_string(); - + let last_backup_date = backup_dict .get("LastBackupDate") .and_then(|v| v.as_string()) .map(|s| s.to_string()); - + let version = backup_dict .get("Version") .and_then(|v| v.as_string()) .unwrap_or("Unknown") .to_string(); - + let is_encrypted = backup_dict .get("IsEncrypted") .and_then(|v| v.as_boolean()) .unwrap_or(false); - + backups.push(BackupInfo { uuid, device_name, @@ -544,7 +586,7 @@ impl MobileBackup2Client { } } } - + Ok(backups) } @@ -573,15 +615,15 @@ impl MobileBackup2Client { options, ) .await?; - + let response = self.receive_backup_response().await?; - + // Check for error in response if let Some(error) = response.get("ErrorCode") { warn!("Backup start failed with error: {error:?}"); return Err(IdeviceError::UnexpectedResponse); } - + debug!("Backup started successfully"); Ok(()) } @@ -597,7 +639,9 @@ impl MobileBackup2Client { /// /// # Errors /// Returns `IdeviceError` if the restore fails to start - #[deprecated(note = "Use restore_from_path; restore via BackupUUID is not supported by device/mobilebackup2")] + #[deprecated( + note = "Use restore_from_path; restore via BackupUUID is not supported by device/mobilebackup2" + )] pub async fn start_restore( &mut self, _backup_uuid: &str, @@ -614,13 +658,19 @@ impl MobileBackup2Client { opts.insert("RestoreDontCopyBackup".into(), plist::Value::Boolean(false)); } if !opts.contains_key("RestorePreserveSettings") { - opts.insert("RestorePreserveSettings".into(), plist::Value::Boolean(true)); + opts.insert( + "RestorePreserveSettings".into(), + plist::Value::Boolean(true), + ); } if !opts.contains_key("RestoreSystemFiles") { opts.insert("RestoreSystemFiles".into(), plist::Value::Boolean(false)); } if !opts.contains_key("RemoveItemsNotRestored") { - opts.insert("RemoveItemsNotRestored".into(), plist::Value::Boolean(false)); + opts.insert( + "RemoveItemsNotRestored".into(), + plist::Value::Boolean(false), + ); } // Avoid borrowing self while sending request let target_udid_owned = self.idevice.udid().map(|s| s.to_string()); @@ -633,15 +683,15 @@ impl MobileBackup2Client { Some(opts), ) .await?; - + let response = self.receive_backup_response().await?; - + // Check for error in response if let Some(error) = response.get("ErrorCode") { warn!("Restore start failed with error: {error:?}"); return Err(IdeviceError::UnexpectedResponse); } - + debug!("Restore started successfully"); Ok(()) } @@ -677,14 +727,18 @@ impl MobileBackup2Client { target_udid, Some(source), Some(opts), - ).await?; + ) + .await?; // 进入 DeviceLink 文件交换循环,根目录传入 backup_root(协议请求包含 source 前缀) let _ = self.process_restore_dl_loop(backup_root).await?; Ok(()) } - async fn process_restore_dl_loop(&mut self, host_dir: &Path) -> Result, IdeviceError> { + async fn process_restore_dl_loop( + &mut self, + host_dir: &Path, + ) -> Result, IdeviceError> { loop { let (tag, value) = self.receive_dl_message().await?; match tag.as_str() { @@ -696,7 +750,8 @@ impl MobileBackup2Client { } "DLMessageGetFreeDiskSpace" => { // Minimal implementation: report 0 with success - self.send_status_response(0, None, Some(plist::Value::Integer(0u64.into()))).await?; + self.send_status_response(0, None, Some(plist::Value::Integer(0u64.into()))) + .await?; } "DLContentsOfDirectory" => { let empty = plist::Value::Dictionary(Dictionary::new()); @@ -708,15 +763,30 @@ impl MobileBackup2Client { } "DLMessageMoveFiles" | "DLMessageMoveItems" => { let status = Self::move_files_from_message(&value, host_dir); - self.send_status_response(status, None, Some(plist::Value::Dictionary(Dictionary::new()))).await?; + self.send_status_response( + status, + None, + Some(plist::Value::Dictionary(Dictionary::new())), + ) + .await?; } "DLMessageRemoveFiles" | "DLMessageRemoveItems" => { let status = Self::remove_files_from_message(&value, host_dir); - self.send_status_response(status, None, Some(plist::Value::Dictionary(Dictionary::new()))).await?; + self.send_status_response( + status, + None, + Some(plist::Value::Dictionary(Dictionary::new())), + ) + .await?; } "DLMessageCopyItem" => { let status = Self::copy_item_from_message(&value, host_dir); - self.send_status_response(status, None, Some(plist::Value::Dictionary(Dictionary::new()))).await?; + self.send_status_response( + status, + None, + Some(plist::Value::Dictionary(Dictionary::new())), + ) + .await?; } "DLMessageProcessMessage" => { if let plist::Value::Array(arr) = value @@ -731,13 +801,18 @@ impl MobileBackup2Client { } other => { warn!("Unsupported DL message: {other}"); - self.send_status_response(-1, Some("Operation not supported"), None).await?; + self.send_status_response(-1, Some("Operation not supported"), None) + .await?; } } } } - async fn handle_download_files(&mut self, dl_value: &plist::Value, host_dir: &Path) -> Result<(), IdeviceError> { + async fn handle_download_files( + &mut self, + dl_value: &plist::Value, + host_dir: &Path, + ) -> Result<(), IdeviceError> { let mut err_any = false; if let plist::Value::Array(arr) = dl_value && arr.len() >= 2 @@ -755,13 +830,23 @@ impl MobileBackup2Client { // terminating zero dword self.idevice.send_raw(&0u32.to_be_bytes()).await?; if err_any { - self.send_status_response(-13, Some("Multi status"), Some(plist::Value::Dictionary(Dictionary::new()))).await + self.send_status_response( + -13, + Some("Multi status"), + Some(plist::Value::Dictionary(Dictionary::new())), + ) + .await } else { - self.send_status_response(0, None, Some(plist::Value::Dictionary(Dictionary::new()))).await + self.send_status_response(0, None, Some(plist::Value::Dictionary(Dictionary::new()))) + .await } } - async fn send_single_file(&mut self, host_dir: &Path, rel_path: &str) -> Result<(), IdeviceError> { + async fn send_single_file( + &mut self, + host_dir: &Path, + rel_path: &str, + ) -> Result<(), IdeviceError> { let full = host_dir.join(rel_path); let path_bytes = rel_path.as_bytes().to_vec(); let nlen = (path_bytes.len() as u32).to_be_bytes(); @@ -785,7 +870,9 @@ impl MobileBackup2Client { let mut buf = [0u8; 32768]; loop { let read = f.read(&mut buf).unwrap_or(0); - if read == 0 { break; } + if read == 0 { + break; + } let size = ((read as u32) + 1).to_be_bytes(); let mut hdr = Vec::with_capacity(5); hdr.extend_from_slice(&size); @@ -801,32 +888,47 @@ impl MobileBackup2Client { Ok(()) } - async fn handle_upload_files(&mut self, _dl_value: &plist::Value, host_dir: &Path) -> Result<(), IdeviceError> { + async fn handle_upload_files( + &mut self, + _dl_value: &plist::Value, + host_dir: &Path, + ) -> Result<(), IdeviceError> { loop { let dlen = self.read_be_u32().await?; - if dlen == 0 { break; } + if dlen == 0 { + break; + } let dname = self.read_exact_string(dlen as usize).await?; let flen = self.read_be_u32().await?; - if flen == 0 { break; } + if flen == 0 { + break; + } let fname = self.read_exact_string(flen as usize).await?; let dst = host_dir.join(&fname); - if let Some(parent) = dst.parent() { let _ = fs::create_dir_all(parent); } - let mut file = std::fs::File::create(&dst).map_err(|e| IdeviceError::InternalError(e.to_string()))?; + if let Some(parent) = dst.parent() { + let _ = fs::create_dir_all(parent); + } + let mut file = std::fs::File::create(&dst) + .map_err(|e| IdeviceError::InternalError(e.to_string()))?; loop { let nlen = self.read_be_u32().await?; - if nlen == 0 { break; } + if nlen == 0 { + break; + } let code = self.read_one().await?; if code == DL_CODE_FILE_DATA { let size = (nlen - 1) as usize; let data = self.read_exact(size).await?; - file.write_all(&data).map_err(|e| IdeviceError::InternalError(e.to_string()))?; + file.write_all(&data) + .map_err(|e| IdeviceError::InternalError(e.to_string()))?; } else { let _ = self.read_exact((nlen - 1) as usize).await?; } } let _ = dname; // unused } - self.send_status_response(0, None, Some(plist::Value::Dictionary(Dictionary::new()))).await + self.send_status_response(0, None, Some(plist::Value::Dictionary(Dictionary::new()))) + .await } async fn read_be_u32(&mut self) -> Result { @@ -854,7 +956,10 @@ impl MobileBackup2Client { && let Some(plist::Value::String(dir)) = arr.get(1) { let path = host_dir.join(dir); - return match fs::create_dir_all(&path) { Ok(_) => 0, Err(_) => -1 }; + return match fs::create_dir_all(&path) { + Ok(_) => 0, + Err(_) => -1, + }; } -1 } @@ -868,8 +973,12 @@ impl MobileBackup2Client { if let Some(to) = to_v.as_string() { let old = host_dir.join(from); let newp = host_dir.join(to); - if let Some(parent) = newp.parent() { let _ = fs::create_dir_all(parent); } - if fs::rename(&old, &newp).is_err() { return -1; } + if let Some(parent) = newp.parent() { + let _ = fs::create_dir_all(parent); + } + if fs::rename(&old, &newp).is_err() { + return -1; + } } } return 0; @@ -886,8 +995,12 @@ impl MobileBackup2Client { if let Some(p) = it.as_string() { let path = host_dir.join(p); if path.is_dir() { - if fs::remove_dir_all(&path).is_err() { return -1; } - } else if path.exists() && fs::remove_file(&path).is_err() { return -1; } + if fs::remove_dir_all(&path).is_err() { + return -1; + } + } else if path.exists() && fs::remove_file(&path).is_err() { + return -1; + } } } return 0; @@ -898,22 +1011,33 @@ impl MobileBackup2Client { fn copy_item_from_message(dl_value: &plist::Value, host_dir: &Path) -> i64 { if let plist::Value::Array(arr) = dl_value && arr.len() >= 3 - && let (Some(plist::Value::String(src)), Some(plist::Value::String(dst))) = (arr.get(1), arr.get(2)) + && let (Some(plist::Value::String(src)), Some(plist::Value::String(dst))) = + (arr.get(1), arr.get(2)) { let from = host_dir.join(src); let to = host_dir.join(dst); - if let Some(parent) = to.parent() { let _ = fs::create_dir_all(parent); } + if let Some(parent) = to.parent() { + let _ = fs::create_dir_all(parent); + } if from.is_dir() { - return match fs::create_dir_all(&to) { Ok(_) => 0, Err(_) => -1 }; + return match fs::create_dir_all(&to) { + Ok(_) => 0, + Err(_) => -1, + }; } else { - return match fs::copy(&from, &to) { Ok(_) => 0, Err(_) => -1 }; + return match fs::copy(&from, &to) { + Ok(_) => 0, + Err(_) => -1, + }; } } -1 } /// Starts a restore using the typed RestoreOptions builder - #[deprecated(note = "Use restore_from_path; restore via BackupUUID is not supported by device/mobilebackup2")] + #[deprecated( + note = "Use restore_from_path; restore via BackupUUID is not supported by device/mobilebackup2" + )] pub async fn start_restore_with( &mut self, _backup_uuid: &str, @@ -960,12 +1084,22 @@ impl MobileBackup2Client { source_identifier: Option<&str>, ) -> Result { let target_udid = self.idevice.udid(); - let source = source_identifier.or(target_udid).ok_or(IdeviceError::InvalidHostID)?; + let source = source_identifier + .or(target_udid) + .ok_or(IdeviceError::InvalidHostID)?; self.assert_backup_exists(backup_root, source)?; let mut dict = Dictionary::new(); - dict.insert("TargetIdentifier".into(), plist::Value::String(target_udid.unwrap().to_string())); - if let Some(src) = source_identifier { dict.insert("SourceIdentifier".into(), plist::Value::String(src.to_string())); } + dict.insert( + "TargetIdentifier".into(), + plist::Value::String(target_udid.unwrap().to_string()), + ); + if let Some(src) = source_identifier { + dict.insert( + "SourceIdentifier".into(), + plist::Value::String(src.to_string()), + ); + } self.send_device_link_message("Info", Some(dict)).await?; match self.process_restore_dl_loop(backup_root).await? { @@ -981,13 +1115,21 @@ impl MobileBackup2Client { source_identifier: Option<&str>, ) -> Result { let target_udid = self.idevice.udid(); - let source = source_identifier.or(target_udid).ok_or(IdeviceError::InvalidHostID)?; + let source = source_identifier + .or(target_udid) + .ok_or(IdeviceError::InvalidHostID)?; self.assert_backup_exists(backup_root, source)?; let mut dict = Dictionary::new(); dict.insert("MessageName".into(), plist::Value::String("List".into())); - dict.insert("TargetIdentifier".into(), plist::Value::String(target_udid.unwrap().to_string())); - dict.insert("SourceIdentifier".into(), plist::Value::String(source.to_string())); + dict.insert( + "TargetIdentifier".into(), + plist::Value::String(target_udid.unwrap().to_string()), + ); + dict.insert( + "SourceIdentifier".into(), + plist::Value::String(source.to_string()), + ); self.send_device_link_message("List", Some(dict)).await?; match self.process_restore_dl_loop(backup_root).await? { @@ -1004,14 +1146,24 @@ impl MobileBackup2Client { source_identifier: Option<&str>, ) -> Result<(), IdeviceError> { let target_udid = self.idevice.udid(); - let source = source_identifier.or(target_udid).ok_or(IdeviceError::InvalidHostID)?; + let source = source_identifier + .or(target_udid) + .ok_or(IdeviceError::InvalidHostID)?; self.assert_backup_exists(backup_root, source)?; let mut dict = Dictionary::new(); - dict.insert("TargetIdentifier".into(), plist::Value::String(target_udid.unwrap().to_string())); + dict.insert( + "TargetIdentifier".into(), + plist::Value::String(target_udid.unwrap().to_string()), + ); dict.insert("MessageName".into(), plist::Value::String("Unback".into())); - dict.insert("SourceIdentifier".into(), plist::Value::String(source.to_string())); - if let Some(pw) = password { dict.insert("Password".into(), plist::Value::String(pw.to_string())); } + dict.insert( + "SourceIdentifier".into(), + plist::Value::String(source.to_string()), + ); + if let Some(pw) = password { + dict.insert("Password".into(), plist::Value::String(pw.to_string())); + } self.send_device_link_message("Unback", Some(dict)).await?; let _ = self.process_restore_dl_loop(backup_root).await?; Ok(()) @@ -1027,16 +1179,32 @@ impl MobileBackup2Client { source_identifier: Option<&str>, ) -> Result<(), IdeviceError> { let target_udid = self.idevice.udid(); - let source = source_identifier.or(target_udid).ok_or(IdeviceError::InvalidHostID)?; + let source = source_identifier + .or(target_udid) + .ok_or(IdeviceError::InvalidHostID)?; self.assert_backup_exists(backup_root, source)?; let mut dict = Dictionary::new(); dict.insert("MessageName".into(), plist::Value::String("Extract".into())); - dict.insert("TargetIdentifier".into(), plist::Value::String(target_udid.unwrap().to_string())); - dict.insert("DomainName".into(), plist::Value::String(domain_name.to_string())); - dict.insert("RelativePath".into(), plist::Value::String(relative_path.to_string())); - dict.insert("SourceIdentifier".into(), plist::Value::String(source.to_string())); - if let Some(pw) = password { dict.insert("Password".into(), plist::Value::String(pw.to_string())); } + dict.insert( + "TargetIdentifier".into(), + plist::Value::String(target_udid.unwrap().to_string()), + ); + dict.insert( + "DomainName".into(), + plist::Value::String(domain_name.to_string()), + ); + dict.insert( + "RelativePath".into(), + plist::Value::String(relative_path.to_string()), + ); + dict.insert( + "SourceIdentifier".into(), + plist::Value::String(source.to_string()), + ); + if let Some(pw) = password { + dict.insert("Password".into(), plist::Value::String(pw.to_string())); + } self.send_device_link_message("Extract", Some(dict)).await?; let _ = self.process_restore_dl_loop(backup_root).await?; Ok(()) @@ -1051,11 +1219,22 @@ impl MobileBackup2Client { ) -> Result<(), IdeviceError> { let target_udid = self.idevice.udid(); let mut dict = Dictionary::new(); - dict.insert("MessageName".into(), plist::Value::String("ChangePassword".into())); - dict.insert("TargetIdentifier".into(), plist::Value::String(target_udid.ok_or(IdeviceError::InvalidHostID)?.to_string())); - if let Some(o) = old { dict.insert("OldPassword".into(), plist::Value::String(o.to_string())); } - if let Some(n) = new { dict.insert("NewPassword".into(), plist::Value::String(n.to_string())); } - self.send_device_link_message("ChangePassword", Some(dict)).await?; + dict.insert( + "MessageName".into(), + plist::Value::String("ChangePassword".into()), + ); + dict.insert( + "TargetIdentifier".into(), + plist::Value::String(target_udid.ok_or(IdeviceError::InvalidHostID)?.to_string()), + ); + if let Some(o) = old { + dict.insert("OldPassword".into(), plist::Value::String(o.to_string())); + } + if let Some(n) = new { + dict.insert("NewPassword".into(), plist::Value::String(n.to_string())); + } + self.send_device_link_message("ChangePassword", Some(dict)) + .await?; let _ = self.process_restore_dl_loop(backup_root).await?; Ok(()) } @@ -1064,9 +1243,16 @@ impl MobileBackup2Client { pub async fn erase_device_from_path(&mut self, backup_root: &Path) -> Result<(), IdeviceError> { let target_udid = self.idevice.udid(); let mut dict = Dictionary::new(); - dict.insert("MessageName".into(), plist::Value::String("EraseDevice".into())); - dict.insert("TargetIdentifier".into(), plist::Value::String(target_udid.ok_or(IdeviceError::InvalidHostID)?.to_string())); - self.send_device_link_message("EraseDevice", Some(dict)).await?; + dict.insert( + "MessageName".into(), + plist::Value::String("EraseDevice".into()), + ); + dict.insert( + "TargetIdentifier".into(), + plist::Value::String(target_udid.ok_or(IdeviceError::InvalidHostID)?.to_string()), + ); + self.send_device_link_message("EraseDevice", Some(dict)) + .await?; let _ = self.process_restore_dl_loop(backup_root).await?; Ok(()) } @@ -1112,4 +1298,5 @@ impl MobileBackup2Client { debug!("Disconnected from backup service"); Ok(()) } -} \ No newline at end of file +} + From 3b22bf24c6026a26aa284beaeb8cc824f5acb70c Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Wed, 13 Aug 2025 08:03:03 -0600 Subject: [PATCH 059/126] Translate Chinese comments --- idevice/src/services/mobilebackup2.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/idevice/src/services/mobilebackup2.rs b/idevice/src/services/mobilebackup2.rs index 23b5f05..e8703ba 100644 --- a/idevice/src/services/mobilebackup2.rs +++ b/idevice/src/services/mobilebackup2.rs @@ -715,7 +715,7 @@ impl MobileBackup2Client { None => target_udid.ok_or(IdeviceError::InvalidHostID)?, }; - // 简单存在性校验:backup_root/source 必须存在 + // Simple existence check: backup_root/source must exist let backup_dir = backup_root.join(source); if !backup_dir.exists() { return Err(IdeviceError::NotFound); @@ -730,7 +730,8 @@ impl MobileBackup2Client { ) .await?; - // 进入 DeviceLink 文件交换循环,根目录传入 backup_root(协议请求包含 source 前缀) + // Enter the DeviceLink file exchange loop, and pass the root directory to backup_root + // (the protocol request contains the source prefix) let _ = self.process_restore_dl_loop(backup_root).await?; Ok(()) } From 876e88d6fbb65018c5965478eef34e305b1eaff2 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Wed, 13 Aug 2025 08:03:24 -0600 Subject: [PATCH 060/126] Bump version --- Cargo.lock | 2 +- idevice/Cargo.toml | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4ba49d9..6b143e4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1110,7 +1110,7 @@ dependencies = [ [[package]] name = "idevice" -version = "0.1.37" +version = "0.1.38" dependencies = [ "base64", "byteorder", diff --git a/idevice/Cargo.toml b/idevice/Cargo.toml index 6c84b41..8eafae5 100644 --- a/idevice/Cargo.toml +++ b/idevice/Cargo.toml @@ -2,7 +2,7 @@ name = "idevice" description = "A Rust library to interact with services on iOS devices." authors = ["Jackson Coxson"] -version = "0.1.37" +version = "0.1.38" edition = "2024" license = "MIT" documentation = "https://docs.rs/idevice" @@ -125,4 +125,5 @@ full = [ ] [package.metadata.docs.rs] -all-features = true \ No newline at end of file +all-features = true + From 4fca58a2f3dbfde63cc846c8ce1161f29253c7ff Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Wed, 13 Aug 2025 08:06:58 -0600 Subject: [PATCH 061/126] Cargo fmt check during CI --- idevice/src/ca.rs | 4 +-- idevice/src/pairing_file.rs | 2 +- idevice/src/provider.rs | 2 +- idevice/src/services/amfi.rs | 2 +- idevice/src/services/core_device_proxy.rs | 2 +- idevice/src/services/crashreportcopymobile.rs | 2 +- idevice/src/services/debug_proxy.rs | 2 +- idevice/src/services/diagnostics_relay.rs | 27 ++++++------------- .../src/services/dvt/location_simulation.rs | 3 ++- idevice/src/services/dvt/message.rs | 6 ++--- idevice/src/services/dvt/mod.rs | 2 +- idevice/src/services/dvt/process_control.rs | 2 +- idevice/src/services/heartbeat.rs | 2 +- idevice/src/services/house_arrest.rs | 2 +- idevice/src/services/lockdown.rs | 2 +- idevice/src/services/misagent.rs | 2 +- idevice/src/services/mobilebackup2.rs | 1 - idevice/src/services/os_trace_relay.rs | 2 +- idevice/src/services/restore_service.rs | 2 +- idevice/src/services/rsd.rs | 2 +- idevice/src/services/springboardservices.rs | 2 +- idevice/src/services/syslog_relay.rs | 2 +- idevice/src/sni.rs | 12 ++++----- idevice/src/tcp/adapter.rs | 4 +-- idevice/src/tcp/handle.rs | 6 ++--- idevice/src/tcp/mod.rs | 2 +- idevice/src/tunneld.rs | 1 - idevice/src/usbmuxd/mod.rs | 2 +- justfile | 1 + 29 files changed, 46 insertions(+), 57 deletions(-) diff --git a/idevice/src/ca.rs b/idevice/src/ca.rs index 72aab2e..9429a68 100644 --- a/idevice/src/ca.rs +++ b/idevice/src/ca.rs @@ -4,19 +4,19 @@ use std::str::FromStr; use rsa::{ + RsaPrivateKey, RsaPublicKey, pkcs1::DecodeRsaPublicKey, pkcs1v15::SigningKey, pkcs8::{EncodePrivateKey, LineEnding, SubjectPublicKeyInfo}, - RsaPrivateKey, RsaPublicKey, }; use sha2::Sha256; use x509_cert::{ + Certificate, builder::{Builder, CertificateBuilder, Profile}, der::EncodePem, name::Name, serial_number::SerialNumber, time::Validity, - Certificate, }; #[derive(Clone, Debug)] diff --git a/idevice/src/pairing_file.rs b/idevice/src/pairing_file.rs index 7f2b5b2..9288bae 100644 --- a/idevice/src/pairing_file.rs +++ b/idevice/src/pairing_file.rs @@ -7,7 +7,7 @@ use std::path::Path; use log::warn; use plist::Data; -use rustls::pki_types::{pem::PemObject, CertificateDer}; +use rustls::pki_types::{CertificateDer, pem::PemObject}; use serde::{Deserialize, Serialize}; /// Represents a complete iOS device pairing record diff --git a/idevice/src/provider.rs b/idevice/src/provider.rs index bc0250e..91d96c6 100644 --- a/idevice/src/provider.rs +++ b/idevice/src/provider.rs @@ -8,7 +8,7 @@ use std::{future::Future, pin::Pin}; #[cfg(feature = "tcp")] use tokio::net::TcpStream; -use crate::{pairing_file::PairingFile, Idevice, IdeviceError, ReadWrite}; +use crate::{Idevice, IdeviceError, ReadWrite, pairing_file::PairingFile}; #[cfg(feature = "usbmuxd")] use crate::usbmuxd::UsbmuxdAddr; diff --git a/idevice/src/services/amfi.rs b/idevice/src/services/amfi.rs index c317127..3cf2b91 100644 --- a/idevice/src/services/amfi.rs +++ b/idevice/src/services/amfi.rs @@ -2,7 +2,7 @@ use plist::Dictionary; -use crate::{obf, Idevice, IdeviceError, IdeviceService}; +use crate::{Idevice, IdeviceError, IdeviceService, obf}; /// Client for interacting with the AMFI service on the device pub struct AmfiClient { diff --git a/idevice/src/services/core_device_proxy.rs b/idevice/src/services/core_device_proxy.rs index 6aafed3..36e4f5c 100644 --- a/idevice/src/services/core_device_proxy.rs +++ b/idevice/src/services/core_device_proxy.rs @@ -13,7 +13,7 @@ //! # Features //! - `tunnel_tcp_stack`: Enables software TCP/IP tunnel creation using a virtual adapter. See the tcp moduel. -use crate::{obf, Idevice, IdeviceError, IdeviceService}; +use crate::{Idevice, IdeviceError, IdeviceService, obf}; use byteorder::{BigEndian, WriteBytesExt}; use serde::{Deserialize, Serialize}; diff --git a/idevice/src/services/crashreportcopymobile.rs b/idevice/src/services/crashreportcopymobile.rs index 7b426d4..f98f1a4 100644 --- a/idevice/src/services/crashreportcopymobile.rs +++ b/idevice/src/services/crashreportcopymobile.rs @@ -9,7 +9,7 @@ use log::{debug, warn}; -use crate::{afc::AfcClient, lockdown::LockdownClient, obf, Idevice, IdeviceError, IdeviceService}; +use crate::{Idevice, IdeviceError, IdeviceService, afc::AfcClient, lockdown::LockdownClient, obf}; /// Client for managing crash logs on an iOS device. /// diff --git a/idevice/src/services/debug_proxy.rs b/idevice/src/services/debug_proxy.rs index ab3237f..7e5c28f 100644 --- a/idevice/src/services/debug_proxy.rs +++ b/idevice/src/services/debug_proxy.rs @@ -8,7 +8,7 @@ use log::debug; use std::fmt::Write; use tokio::io::{AsyncReadExt, AsyncWriteExt}; -use crate::{obf, IdeviceError, ReadWrite, RsdService}; +use crate::{IdeviceError, ReadWrite, RsdService, obf}; impl RsdService for DebugProxyClient> { fn rsd_service_name() -> std::borrow::Cow<'static, str> { diff --git a/idevice/src/services/diagnostics_relay.rs b/idevice/src/services/diagnostics_relay.rs index 7b525c5..9b31f78 100644 --- a/idevice/src/services/diagnostics_relay.rs +++ b/idevice/src/services/diagnostics_relay.rs @@ -1,6 +1,6 @@ //! Diagnostics Relay -use crate::{obf, Idevice, IdeviceError, IdeviceService}; +use crate::{Idevice, IdeviceError, IdeviceService, obf}; /// Client for interacting with the Diagnostics Relay pub struct DiagnosticsRelayClient { @@ -74,7 +74,6 @@ impl DiagnosticsRelayClient { .and_then(|x| x.into_dictionary()) .and_then(|mut x| x.remove("IORegistry")) .and_then(|x| x.into_dictionary()); - Ok(res) } @@ -92,7 +91,7 @@ impl DiagnosticsRelayClient { ) -> Result, IdeviceError> { let mut req = plist::Dictionary::new(); req.insert("Request".into(), "MobileGestalt".into()); - + if let Some(keys) = keys { let keys_array: Vec = keys.into_iter().map(|k| k.into()).collect(); req.insert("MobileGestaltKeys".into(), plist::Value::Array(keys_array)); @@ -110,9 +109,7 @@ impl DiagnosticsRelayClient { } } - let res = res - .remove("Diagnostics") - .and_then(|x| x.into_dictionary()); + let res = res.remove("Diagnostics").and_then(|x| x.into_dictionary()); Ok(res) } @@ -137,9 +134,7 @@ impl DiagnosticsRelayClient { } } - let res = res - .remove("Diagnostics") - .and_then(|x| x.into_dictionary()); + let res = res.remove("Diagnostics").and_then(|x| x.into_dictionary()); Ok(res) } @@ -164,9 +159,7 @@ impl DiagnosticsRelayClient { } } - let res = res - .remove("Diagnostics") - .and_then(|x| x.into_dictionary()); + let res = res.remove("Diagnostics").and_then(|x| x.into_dictionary()); Ok(res) } @@ -191,9 +184,7 @@ impl DiagnosticsRelayClient { } } - let res = res - .remove("Diagnostics") - .and_then(|x| x.into_dictionary()); + let res = res.remove("Diagnostics").and_then(|x| x.into_dictionary()); Ok(res) } @@ -272,9 +263,7 @@ impl DiagnosticsRelayClient { } } - let res = res - .remove("Diagnostics") - .and_then(|x| x.into_dictionary()); + let res = res.remove("Diagnostics").and_then(|x| x.into_dictionary()); Ok(res) } @@ -293,4 +282,4 @@ impl DiagnosticsRelayClient { _ => Err(IdeviceError::UnexpectedResponse), } } -} \ No newline at end of file +} diff --git a/idevice/src/services/dvt/location_simulation.rs b/idevice/src/services/dvt/location_simulation.rs index ecea710..ec90f51 100644 --- a/idevice/src/services/dvt/location_simulation.rs +++ b/idevice/src/services/dvt/location_simulation.rs @@ -37,11 +37,12 @@ use plist::Value; use crate::{ + IdeviceError, ReadWrite, dvt::{ message::AuxValue, remote_server::{Channel, RemoteServerClient}, }, - obf, IdeviceError, ReadWrite, + obf, }; /// A client for the location simulation service diff --git a/idevice/src/services/dvt/message.rs b/idevice/src/services/dvt/message.rs index 006fad7..4298e77 100644 --- a/idevice/src/services/dvt/message.rs +++ b/idevice/src/services/dvt/message.rs @@ -278,9 +278,9 @@ impl Aux { let mut res = Vec::new(); let buffer_size = 496_u32; res.extend_from_slice(&buffer_size.to_le_bytes()); // TODO: find what - // this means and how to actually serialize it - // go-ios just uses 496 - // pymobiledevice3 doesn't seem to parse the header at all + // this means and how to actually serialize it + // go-ios just uses 496 + // pymobiledevice3 doesn't seem to parse the header at all res.extend_from_slice(&0_u32.to_le_bytes()); res.extend_from_slice(&(values_payload.len() as u32).to_le_bytes()); res.extend_from_slice(&0_u32.to_le_bytes()); diff --git a/idevice/src/services/dvt/mod.rs b/idevice/src/services/dvt/mod.rs index 904f584..90a3bab 100644 --- a/idevice/src/services/dvt/mod.rs +++ b/idevice/src/services/dvt/mod.rs @@ -1,6 +1,6 @@ // Jackson Coxson -use crate::{obf, IdeviceError, ReadWrite, RsdService}; +use crate::{IdeviceError, ReadWrite, RsdService, obf}; #[cfg(feature = "location_simulation")] pub mod location_simulation; diff --git a/idevice/src/services/dvt/process_control.rs b/idevice/src/services/dvt/process_control.rs index 8ae7e15..c1f6edb 100644 --- a/idevice/src/services/dvt/process_control.rs +++ b/idevice/src/services/dvt/process_control.rs @@ -37,7 +37,7 @@ use log::warn; use plist::{Dictionary, Value}; -use crate::{dvt::message::AuxValue, obf, IdeviceError, ReadWrite}; +use crate::{IdeviceError, ReadWrite, dvt::message::AuxValue, obf}; use super::remote_server::{Channel, RemoteServerClient}; diff --git a/idevice/src/services/heartbeat.rs b/idevice/src/services/heartbeat.rs index a57690d..745abaf 100644 --- a/idevice/src/services/heartbeat.rs +++ b/idevice/src/services/heartbeat.rs @@ -3,7 +3,7 @@ //! iOS automatically closes service connections if there is no heartbeat client connected and //! responding. -use crate::{obf, Idevice, IdeviceError, IdeviceService}; +use crate::{Idevice, IdeviceError, IdeviceService, obf}; /// Client for interacting with the iOS device heartbeat service /// diff --git a/idevice/src/services/house_arrest.rs b/idevice/src/services/house_arrest.rs index f4de8ab..a38a85c 100644 --- a/idevice/src/services/house_arrest.rs +++ b/idevice/src/services/house_arrest.rs @@ -6,7 +6,7 @@ use plist::{Dictionary, Value}; -use crate::{obf, Idevice, IdeviceError, IdeviceService}; +use crate::{Idevice, IdeviceError, IdeviceService, obf}; use super::afc::AfcClient; diff --git a/idevice/src/services/lockdown.rs b/idevice/src/services/lockdown.rs index cbbd2a9..3ef2133 100644 --- a/idevice/src/services/lockdown.rs +++ b/idevice/src/services/lockdown.rs @@ -7,7 +7,7 @@ use log::error; use plist::Value; use serde::{Deserialize, Serialize}; -use crate::{obf, pairing_file, Idevice, IdeviceError, IdeviceService}; +use crate::{Idevice, IdeviceError, IdeviceService, obf, pairing_file}; /// Client for interacting with the iOS lockdown service /// diff --git a/idevice/src/services/misagent.rs b/idevice/src/services/misagent.rs index a3e3787..9f8de5a 100644 --- a/idevice/src/services/misagent.rs +++ b/idevice/src/services/misagent.rs @@ -6,7 +6,7 @@ use log::warn; use plist::Dictionary; -use crate::{obf, Idevice, IdeviceError, IdeviceService, RsdService}; +use crate::{Idevice, IdeviceError, IdeviceService, RsdService, obf}; /// Client for interacting with the iOS misagent service /// diff --git a/idevice/src/services/mobilebackup2.rs b/idevice/src/services/mobilebackup2.rs index e8703ba..6fb47c3 100644 --- a/idevice/src/services/mobilebackup2.rs +++ b/idevice/src/services/mobilebackup2.rs @@ -1300,4 +1300,3 @@ impl MobileBackup2Client { Ok(()) } } - diff --git a/idevice/src/services/os_trace_relay.rs b/idevice/src/services/os_trace_relay.rs index 0a6d4eb..793f80a 100644 --- a/idevice/src/services/os_trace_relay.rs +++ b/idevice/src/services/os_trace_relay.rs @@ -7,7 +7,7 @@ use chrono::{DateTime, NaiveDateTime}; use plist::Dictionary; use tokio::io::AsyncWriteExt; -use crate::{obf, Idevice, IdeviceError, IdeviceService}; +use crate::{Idevice, IdeviceError, IdeviceService, obf}; /// Client for interacting with the iOS device OsTraceRelay service pub struct OsTraceRelayClient { diff --git a/idevice/src/services/restore_service.rs b/idevice/src/services/restore_service.rs index 2942e71..b2f4590 100644 --- a/idevice/src/services/restore_service.rs +++ b/idevice/src/services/restore_service.rs @@ -3,7 +3,7 @@ use log::warn; use plist::Dictionary; -use crate::{obf, IdeviceError, ReadWrite, RemoteXpcClient, RsdService}; +use crate::{IdeviceError, ReadWrite, RemoteXpcClient, RsdService, obf}; /// Client for interacting with the Restore Service pub struct RestoreServiceClient { diff --git a/idevice/src/services/rsd.rs b/idevice/src/services/rsd.rs index 58566b9..9c41982 100644 --- a/idevice/src/services/rsd.rs +++ b/idevice/src/services/rsd.rs @@ -6,7 +6,7 @@ use std::collections::HashMap; use log::warn; use serde::Deserialize; -use crate::{provider::RsdProvider, IdeviceError, ReadWrite, RemoteXpcClient}; +use crate::{IdeviceError, ReadWrite, RemoteXpcClient, provider::RsdProvider}; /// Describes an available XPC service #[derive(Debug, Clone, Deserialize)] diff --git a/idevice/src/services/springboardservices.rs b/idevice/src/services/springboardservices.rs index 99a8564..f115f30 100644 --- a/idevice/src/services/springboardservices.rs +++ b/idevice/src/services/springboardservices.rs @@ -3,7 +3,7 @@ //! Provides functionality for interacting with the SpringBoard services on iOS devices, //! which manages home screen and app icon related operations. -use crate::{obf, Idevice, IdeviceError, IdeviceService}; +use crate::{Idevice, IdeviceError, IdeviceService, obf}; /// Client for interacting with the iOS SpringBoard services /// diff --git a/idevice/src/services/syslog_relay.rs b/idevice/src/services/syslog_relay.rs index cf1069a..0b35380 100644 --- a/idevice/src/services/syslog_relay.rs +++ b/idevice/src/services/syslog_relay.rs @@ -1,6 +1,6 @@ //! iOS Device SyslogRelay Service Abstraction -use crate::{obf, Idevice, IdeviceError, IdeviceService}; +use crate::{Idevice, IdeviceError, IdeviceService, obf}; /// Client for interacting with the iOS device SyslogRelay service pub struct SyslogRelayClient { diff --git a/idevice/src/sni.rs b/idevice/src/sni.rs index 965d12c..1bf9806 100644 --- a/idevice/src/sni.rs +++ b/idevice/src/sni.rs @@ -7,16 +7,16 @@ // Assuming that there's no use for unchecked certs is naive. use rustls::{ - client::{ - danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}, - WebPkiServerVerifier, - }, - pki_types::{pem::PemObject, CertificateDer, PrivateKeyDer, ServerName, UnixTime}, ClientConfig, DigitallySignedStruct, + client::{ + WebPkiServerVerifier, + danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}, + }, + pki_types::{CertificateDer, PrivateKeyDer, ServerName, UnixTime, pem::PemObject}, }; use std::sync::Arc; -use crate::{pairing_file::PairingFile, IdeviceError}; +use crate::{IdeviceError, pairing_file::PairingFile}; #[derive(Debug)] pub struct NoServerNameVerification { diff --git a/idevice/src/tcp/adapter.rs b/idevice/src/tcp/adapter.rs index 4bcc943..714f5f3 100644 --- a/idevice/src/tcp/adapter.rs +++ b/idevice/src/tcp/adapter.rs @@ -206,7 +206,7 @@ impl Adapter { break; } ConnectionStatus::Error(e) => { - return Err(std::io::Error::new(e, "failed to connect")) + return Err(std::io::Error::new(e, "failed to connect")); } ConnectionStatus::WaitingForSyn => { continue; @@ -236,7 +236,7 @@ impl Adapter { file.write_all(&0_i32.to_le_bytes()).await?; // timezone file.write_all(&0_u32.to_le_bytes()).await?; // accuracy file.write_all(&(u16::MAX as u32).to_le_bytes()).await?; // snaplen - // https://www.tcpdump.org/linktypes.html + // https://www.tcpdump.org/linktypes.html file.write_all(&101_u32.to_le_bytes()).await?; // link type self.pcap = Some(Arc::new(Mutex::new(file))); diff --git a/idevice/src/tcp/handle.rs b/idevice/src/tcp/handle.rs index 86c08ac..43e6612 100644 --- a/idevice/src/tcp/handle.rs +++ b/idevice/src/tcp/handle.rs @@ -6,8 +6,8 @@ use std::{collections::HashMap, path::PathBuf, sync::Mutex, task::Poll}; -use crossfire::{mpsc, spsc, stream::AsyncStream, AsyncRx, MTx, Tx}; -use futures::{stream::FuturesUnordered, StreamExt}; +use crossfire::{AsyncRx, MTx, Tx, mpsc, spsc, stream::AsyncStream}; +use futures::{StreamExt, stream::FuturesUnordered}; use log::trace; use tokio::{ io::{AsyncRead, AsyncWrite}, @@ -309,7 +309,7 @@ impl AsyncWrite for StreamHandle { return Poll::Ready(Err(std::io::Error::new( std::io::ErrorKind::BrokenPipe, "channel closed", - ))) + ))); } None => break, // nothing pending } diff --git a/idevice/src/tcp/mod.rs b/idevice/src/tcp/mod.rs index 64302be..df755bd 100644 --- a/idevice/src/tcp/mod.rs +++ b/idevice/src/tcp/mod.rs @@ -8,7 +8,7 @@ use std::{ use log::debug; use tokio::io::AsyncWriteExt; -use crate::{provider::RsdProvider, ReadWrite}; +use crate::{ReadWrite, provider::RsdProvider}; pub mod adapter; pub mod handle; diff --git a/idevice/src/tunneld.rs b/idevice/src/tunneld.rs index dcbb8de..f6f747c 100644 --- a/idevice/src/tunneld.rs +++ b/idevice/src/tunneld.rs @@ -106,4 +106,3 @@ mod tests { } } } - diff --git a/idevice/src/usbmuxd/mod.rs b/idevice/src/usbmuxd/mod.rs index 3d18005..d1eebad 100644 --- a/idevice/src/usbmuxd/mod.rs +++ b/idevice/src/usbmuxd/mod.rs @@ -15,7 +15,7 @@ use log::{debug, warn}; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use crate::{ - pairing_file::PairingFile, provider::UsbmuxdProvider, Idevice, IdeviceError, ReadWrite, + Idevice, IdeviceError, ReadWrite, pairing_file::PairingFile, provider::UsbmuxdProvider, }; mod des; diff --git a/justfile b/justfile index 238d4ef..9b3788c 100644 --- a/justfile +++ b/justfile @@ -5,6 +5,7 @@ check-features: ci-check: build-ffi-native build-tools-native build-cpp build-c cargo clippy --all-targets --all-features -- -D warnings + cargo fmt -- --check macos-ci-check: ci-check xcframework cd tools && cargo build --release --target x86_64-apple-darwin windows-ci-check: build-ffi-native build-tools-native build-cpp From 0c6a214a6685c48e73a99036a9881587b8a7c5ec Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Wed, 13 Aug 2025 08:16:24 -0600 Subject: [PATCH 062/126] Cargo fmt tools --- tools/src/afc.rs | 6 +- tools/src/amfi.rs | 2 +- tools/src/app_service.rs | 4 +- tools/src/companion_proxy.rs | 7 +- tools/src/crash_logs.rs | 2 +- tools/src/debug_proxy.rs | 4 +- tools/src/diagnostics.rs | 65 +++----- tools/src/heartbeat_client.rs | 2 +- tools/src/ideviceinfo.rs | 6 +- tools/src/instproxy.rs | 6 +- tools/src/location_simulation.rs | 2 +- tools/src/lockdown.rs | 8 +- tools/src/misagent.rs | 8 +- tools/src/mobilebackup2.rs | 264 ++++++++++++++++++++++--------- tools/src/mounter.rs | 10 +- tools/src/os_trace_relay.rs | 2 +- tools/src/pair.rs | 2 +- tools/src/process_control.rs | 2 +- tools/src/remotexpc.rs | 4 +- tools/src/restore_service.rs | 8 +- tools/src/syslog_relay.rs | 2 +- 21 files changed, 257 insertions(+), 159 deletions(-) diff --git a/tools/src/afc.rs b/tools/src/afc.rs index d70c926..e2dcefe 100644 --- a/tools/src/afc.rs +++ b/tools/src/afc.rs @@ -2,11 +2,11 @@ use std::path::PathBuf; -use clap::{value_parser, Arg, Command}; +use clap::{Arg, Command, value_parser}; use idevice::{ - afc::{opcode::AfcFopenMode, AfcClient}, - house_arrest::HouseArrestClient, IdeviceService, + afc::{AfcClient, opcode::AfcFopenMode}, + house_arrest::HouseArrestClient, }; mod common; diff --git a/tools/src/amfi.rs b/tools/src/amfi.rs index ff0b4fa..48d1e37 100644 --- a/tools/src/amfi.rs +++ b/tools/src/amfi.rs @@ -1,7 +1,7 @@ // Jackson Coxson use clap::{Arg, Command}; -use idevice::{amfi::AmfiClient, IdeviceService}; +use idevice::{IdeviceService, amfi::AmfiClient}; mod common; diff --git a/tools/src/app_service.rs b/tools/src/app_service.rs index 27f95df..9751e28 100644 --- a/tools/src/app_service.rs +++ b/tools/src/app_service.rs @@ -2,8 +2,8 @@ use clap::{Arg, Command}; use idevice::{ - core_device::AppServiceClient, core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake, - IdeviceService, RsdService, + IdeviceService, RsdService, core_device::AppServiceClient, core_device_proxy::CoreDeviceProxy, + rsd::RsdHandshake, }; mod common; diff --git a/tools/src/companion_proxy.rs b/tools/src/companion_proxy.rs index 4c1df7c..2568df5 100644 --- a/tools/src/companion_proxy.rs +++ b/tools/src/companion_proxy.rs @@ -1,9 +1,10 @@ // Jackson Coxson -use clap::{arg, Arg, Command}; +use clap::{Arg, Command, arg}; use idevice::{ - companion_proxy::CompanionProxy, core_device_proxy::CoreDeviceProxy, pretty_print_dictionary, - pretty_print_plist, rsd::RsdHandshake, IdeviceService, RsdService, + IdeviceService, RsdService, companion_proxy::CompanionProxy, + core_device_proxy::CoreDeviceProxy, pretty_print_dictionary, pretty_print_plist, + rsd::RsdHandshake, }; mod common; diff --git a/tools/src/crash_logs.rs b/tools/src/crash_logs.rs index 69e79b2..9cbfc24 100644 --- a/tools/src/crash_logs.rs +++ b/tools/src/crash_logs.rs @@ -2,8 +2,8 @@ use clap::{Arg, Command}; use idevice::{ - crashreportcopymobile::{flush_reports, CrashReportCopyMobileClient}, IdeviceService, + crashreportcopymobile::{CrashReportCopyMobileClient, flush_reports}, }; mod common; diff --git a/tools/src/debug_proxy.rs b/tools/src/debug_proxy.rs index 7aa983e..b7e5625 100644 --- a/tools/src/debug_proxy.rs +++ b/tools/src/debug_proxy.rs @@ -4,8 +4,8 @@ use std::io::Write; use clap::{Arg, Command}; use idevice::{ - core_device_proxy::CoreDeviceProxy, debug_proxy::DebugProxyClient, rsd::RsdHandshake, - IdeviceService, RsdService, + IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, debug_proxy::DebugProxyClient, + rsd::RsdHandshake, }; mod common; diff --git a/tools/src/diagnostics.rs b/tools/src/diagnostics.rs index c6a007d..3ea5fe1 100644 --- a/tools/src/diagnostics.rs +++ b/tools/src/diagnostics.rs @@ -1,8 +1,8 @@ // Jackson Coxson // idevice Rust implementation of libimobiledevice's idevicediagnostics -use clap::{Arg, Command, ArgMatches}; -use idevice::{services::diagnostics_relay::DiagnosticsRelayClient, IdeviceService}; +use clap::{Arg, ArgMatches, Command}; +use idevice::{IdeviceService, services::diagnostics_relay::DiagnosticsRelayClient}; mod common; @@ -43,20 +43,20 @@ async fn main() { Arg::new("plane") .long("plane") .value_name("PLANE") - .help("IORegistry plane to query (e.g., IODeviceTree, IOService)") + .help("IORegistry plane to query (e.g., IODeviceTree, IOService)"), ) .arg( Arg::new("name") .long("name") .value_name("NAME") - .help("Entry name to filter by") + .help("Entry name to filter by"), ) .arg( Arg::new("class") .long("class") .value_name("CLASS") - .help("Entry class to filter by") - ) + .help("Entry class to filter by"), + ), ) .subcommand( Command::new("mobilegestalt") @@ -67,45 +67,23 @@ async fn main() { .value_name("KEYS") .help("Comma-separated list of keys to query") .value_delimiter(',') - .num_args(1..) - ) - ) - .subcommand( - Command::new("gasguage") - .about("Print gas gauge (battery) information") - ) - .subcommand( - Command::new("nand") - .about("Print NAND flash information") - ) - .subcommand( - Command::new("all") - .about("Print all available diagnostics information") - ) - .subcommand( - Command::new("wifi") - .about("Print WiFi diagnostics information") - ) - .subcommand( - Command::new("goodbye") - .about("Send Goodbye to diagnostics relay") - ) - .subcommand( - Command::new("restart") - .about("Restart the device") - ) - .subcommand( - Command::new("shutdown") - .about("Shutdown the device") - ) - .subcommand( - Command::new("sleep") - .about("Put the device to sleep") + .num_args(1..), + ), ) + .subcommand(Command::new("gasguage").about("Print gas gauge (battery) information")) + .subcommand(Command::new("nand").about("Print NAND flash information")) + .subcommand(Command::new("all").about("Print all available diagnostics information")) + .subcommand(Command::new("wifi").about("Print WiFi diagnostics information")) + .subcommand(Command::new("goodbye").about("Send Goodbye to diagnostics relay")) + .subcommand(Command::new("restart").about("Restart the device")) + .subcommand(Command::new("shutdown").about("Shutdown the device")) + .subcommand(Command::new("sleep").about("Put the device to sleep")) .get_matches(); if matches.get_flag("about") { - println!("idevicediagnostics - interact with the diagnostics interface of a device. Reimplementation of libimobiledevice's binary."); + println!( + "idevicediagnostics - interact with the diagnostics interface of a device. Reimplementation of libimobiledevice's binary." + ); println!("Copyright (c) 2025 Jackson Coxson"); return; } @@ -187,7 +165,8 @@ async fn handle_ioregistry(client: &mut DiagnosticsRelayClient, matches: &ArgMat } async fn handle_mobilegestalt(client: &mut DiagnosticsRelayClient, matches: &ArgMatches) { - let keys = matches.get_many::("keys") + let keys = matches + .get_many::("keys") .map(|values| values.map(|s| s.to_string()).collect::>()); match client.mobilegestalt(keys).await { @@ -297,4 +276,4 @@ async fn handle_goodbye(client: &mut DiagnosticsRelayClient) { Ok(()) => println!("Goodbye acknowledged by device"), Err(e) => eprintln!("Goodbye failed: {e:?}"), } -} \ No newline at end of file +} diff --git a/tools/src/heartbeat_client.rs b/tools/src/heartbeat_client.rs index bfb8d80..2ed1979 100644 --- a/tools/src/heartbeat_client.rs +++ b/tools/src/heartbeat_client.rs @@ -2,7 +2,7 @@ // Heartbeat client use clap::{Arg, Command}; -use idevice::{heartbeat::HeartbeatClient, IdeviceService}; +use idevice::{IdeviceService, heartbeat::HeartbeatClient}; mod common; diff --git a/tools/src/ideviceinfo.rs b/tools/src/ideviceinfo.rs index 1ec95b7..60bdf34 100644 --- a/tools/src/ideviceinfo.rs +++ b/tools/src/ideviceinfo.rs @@ -2,7 +2,7 @@ // idevice Rust implementation of libimobiledevice's ideviceinfo use clap::{Arg, Command}; -use idevice::{lockdown::LockdownClient, IdeviceService}; +use idevice::{IdeviceService, lockdown::LockdownClient}; mod common; @@ -39,7 +39,9 @@ async fn main() { .get_matches(); if matches.get_flag("about") { - println!("ideviceinfo - get information from the idevice. Reimplementation of libimobiledevice's binary."); + println!( + "ideviceinfo - get information from the idevice. Reimplementation of libimobiledevice's binary." + ); println!("Copyright (c) 2025 Jackson Coxson"); return; } diff --git a/tools/src/instproxy.rs b/tools/src/instproxy.rs index f22b838..960a80c 100644 --- a/tools/src/instproxy.rs +++ b/tools/src/instproxy.rs @@ -2,7 +2,7 @@ // Just lists apps for now use clap::{Arg, Command}; -use idevice::{installation_proxy::InstallationProxyClient, IdeviceService}; +use idevice::{IdeviceService, installation_proxy::InstallationProxyClient}; mod common; @@ -47,7 +47,9 @@ async fn main() { .get_matches(); if matches.get_flag("about") { - println!("instproxy - query and manage apps installed on a device. Reimplementation of libimobiledevice's binary."); + println!( + "instproxy - query and manage apps installed on a device. Reimplementation of libimobiledevice's binary." + ); println!("Copyright (c) 2025 Jackson Coxson"); return; } diff --git a/tools/src/location_simulation.rs b/tools/src/location_simulation.rs index 02c0a6d..10f62a0 100644 --- a/tools/src/location_simulation.rs +++ b/tools/src/location_simulation.rs @@ -2,7 +2,7 @@ // Just lists apps for now use clap::{Arg, Command}; -use idevice::{core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake, IdeviceService, RsdService}; +use idevice::{IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake}; mod common; diff --git a/tools/src/lockdown.rs b/tools/src/lockdown.rs index 842c7b4..ec164c5 100644 --- a/tools/src/lockdown.rs +++ b/tools/src/lockdown.rs @@ -1,7 +1,7 @@ // Jackson Coxson -use clap::{arg, Arg, Command}; -use idevice::{lockdown::LockdownClient, pretty_print_plist, IdeviceService}; +use clap::{Arg, Command, arg}; +use idevice::{IdeviceService, lockdown::LockdownClient, pretty_print_plist}; use plist::Value; mod common; @@ -52,7 +52,9 @@ async fn main() { .get_matches(); if matches.get_flag("about") { - println!("lockdown - query and manage values on a device. Reimplementation of libimobiledevice's binary."); + println!( + "lockdown - query and manage values on a device. Reimplementation of libimobiledevice's binary." + ); println!("Copyright (c) 2025 Jackson Coxson"); return; } diff --git a/tools/src/misagent.rs b/tools/src/misagent.rs index 04057ac..966b86f 100644 --- a/tools/src/misagent.rs +++ b/tools/src/misagent.rs @@ -2,8 +2,8 @@ use std::path::PathBuf; -use clap::{arg, value_parser, Arg, Command}; -use idevice::{misagent::MisagentClient, IdeviceService}; +use clap::{Arg, Command, arg, value_parser}; +use idevice::{IdeviceService, misagent::MisagentClient}; mod common; @@ -52,7 +52,9 @@ async fn main() { .get_matches(); if matches.get_flag("about") { - println!("mounter - query and manage images mounted on a device. Reimplementation of libimobiledevice's binary."); + println!( + "mounter - query and manage images mounted on a device. Reimplementation of libimobiledevice's binary." + ); println!("Copyright (c) 2025 Jackson Coxson"); return; } diff --git a/tools/src/mobilebackup2.rs b/tools/src/mobilebackup2.rs index 4ec7259..e852d8f 100644 --- a/tools/src/mobilebackup2.rs +++ b/tools/src/mobilebackup2.rs @@ -2,7 +2,10 @@ // Mobile Backup 2 tool for iOS devices use clap::{Arg, Command}; -use idevice::{mobilebackup2::{MobileBackup2Client, RestoreOptions}, IdeviceService}; +use idevice::{ + IdeviceService, + mobilebackup2::{MobileBackup2Client, RestoreOptions}, +}; use plist::Dictionary; use std::fs; use std::io::{Read, Write}; @@ -44,13 +47,18 @@ async fn main() { Command::new("info") .about("Get backup information from a local backup directory") .arg(Arg::new("dir").long("dir").value_name("DIR").required(true)) - .arg(Arg::new("source").long("source").value_name("SOURCE").help("Source identifier (defaults to current UDID)")) + .arg( + Arg::new("source") + .long("source") + .value_name("SOURCE") + .help("Source identifier (defaults to current UDID)"), + ), ) .subcommand( Command::new("list") .about("List files of the last backup from a local backup directory") .arg(Arg::new("dir").long("dir").value_name("DIR").required(true)) - .arg(Arg::new("source").long("source").value_name("SOURCE")) + .arg(Arg::new("source").long("source").value_name("SOURCE")), ) .subcommand( Command::new("backup") @@ -79,41 +87,81 @@ async fn main() { Command::new("restore") .about("Restore from a local backup directory (DeviceLink)") .arg(Arg::new("dir").long("dir").value_name("DIR").required(true)) - .arg(Arg::new("source").long("source").value_name("SOURCE").help("Source UDID; defaults to current device UDID")) - .arg(Arg::new("password").long("password").value_name("PWD").help("Backup password if encrypted")) - .arg(Arg::new("no-reboot").long("no-reboot").action(clap::ArgAction::SetTrue)) - .arg(Arg::new("no-copy").long("no-copy").action(clap::ArgAction::SetTrue)) - .arg(Arg::new("no-settings").long("no-settings").action(clap::ArgAction::SetTrue)) - .arg(Arg::new("system").long("system").action(clap::ArgAction::SetTrue)) - .arg(Arg::new("remove").long("remove").action(clap::ArgAction::SetTrue)) + .arg( + Arg::new("source") + .long("source") + .value_name("SOURCE") + .help("Source UDID; defaults to current device UDID"), + ) + .arg( + Arg::new("password") + .long("password") + .value_name("PWD") + .help("Backup password if encrypted"), + ) + .arg( + Arg::new("no-reboot") + .long("no-reboot") + .action(clap::ArgAction::SetTrue), + ) + .arg( + Arg::new("no-copy") + .long("no-copy") + .action(clap::ArgAction::SetTrue), + ) + .arg( + Arg::new("no-settings") + .long("no-settings") + .action(clap::ArgAction::SetTrue), + ) + .arg( + Arg::new("system") + .long("system") + .action(clap::ArgAction::SetTrue), + ) + .arg( + Arg::new("remove") + .long("remove") + .action(clap::ArgAction::SetTrue), + ), ) .subcommand( Command::new("unback") .about("Unpack a complete backup to device hierarchy") .arg(Arg::new("dir").long("dir").value_name("DIR").required(true)) .arg(Arg::new("source").long("source").value_name("SOURCE")) - .arg(Arg::new("password").long("password").value_name("PWD")) + .arg(Arg::new("password").long("password").value_name("PWD")), ) .subcommand( Command::new("extract") .about("Extract a file from a previous backup") .arg(Arg::new("dir").long("dir").value_name("DIR").required(true)) .arg(Arg::new("source").long("source").value_name("SOURCE")) - .arg(Arg::new("domain").long("domain").value_name("DOMAIN").required(true)) - .arg(Arg::new("path").long("path").value_name("REL_PATH").required(true)) - .arg(Arg::new("password").long("password").value_name("PWD")) + .arg( + Arg::new("domain") + .long("domain") + .value_name("DOMAIN") + .required(true), + ) + .arg( + Arg::new("path") + .long("path") + .value_name("REL_PATH") + .required(true), + ) + .arg(Arg::new("password").long("password").value_name("PWD")), ) .subcommand( Command::new("change-password") .about("Change backup password") .arg(Arg::new("dir").long("dir").value_name("DIR").required(true)) .arg(Arg::new("old").long("old").value_name("OLD")) - .arg(Arg::new("new").long("new").value_name("NEW")) + .arg(Arg::new("new").long("new").value_name("NEW")), ) .subcommand( Command::new("erase-device") .about("Erase the device via mobilebackup2") - .arg(Arg::new("dir").long("dir").value_name("DIR").required(true)) + .arg(Arg::new("dir").long("dir").value_name("DIR").required(true)), ) .subcommand(Command::new("freespace").about("Get free space information")) .subcommand(Command::new("encryption").about("Check backup encryption status")) @@ -129,14 +177,14 @@ async fn main() { let host = matches.get_one::("host"); let pairing_file = matches.get_one::("pairing_file"); - let provider = match common::get_provider(udid, host, pairing_file, "mobilebackup2-jkcoxson").await - { - Ok(p) => p, - Err(e) => { - eprintln!("Error creating provider: {e}"); - return; - } - }; + let provider = + match common::get_provider(udid, host, pairing_file, "mobilebackup2-jkcoxson").await { + Ok(p) => p, + Err(e) => { + eprintln!("Error creating provider: {e}"); + return; + } + }; let mut backup_client = match MobileBackup2Client::connect(&*provider).await { Ok(client) => client, @@ -153,7 +201,9 @@ async fn main() { match backup_client.info_from_path(Path::new(dir), source).await { Ok(dict) => { println!("Backup Information:"); - for (k, v) in dict { println!(" {k}: {v:?}"); } + for (k, v) in dict { + println!(" {k}: {v:?}"); + } } Err(e) => eprintln!("Failed to get info: {e}"), } @@ -164,7 +214,9 @@ async fn main() { match backup_client.list_from_path(Path::new(dir), source).await { Ok(dict) => { println!("List Response:"); - for (k, v) in dict { println!(" {k}: {v:?}"); } + for (k, v) in dict { + println!(" {k}: {v:?}"); + } } Err(e) => eprintln!("Failed to list: {e}"), } @@ -172,7 +224,9 @@ async fn main() { Some(("backup", sub_matches)) => { let target = sub_matches.get_one::("target").map(|s| s.as_str()); let source = sub_matches.get_one::("source").map(|s| s.as_str()); - let dir = sub_matches.get_one::("dir").expect("dir is required"); + let dir = sub_matches + .get_one::("dir") + .expect("dir is required"); println!("Starting backup operation..."); let res = backup_client @@ -190,13 +244,28 @@ async fn main() { let dir = sub.get_one::("dir").unwrap(); let source = sub.get_one::("source").map(|s| s.as_str()); let mut ropts = RestoreOptions::new(); - if sub.get_flag("no-reboot") { ropts = ropts.with_reboot(false); } - if sub.get_flag("no-copy") { ropts = ropts.with_copy(false); } - if sub.get_flag("no-settings") { ropts = ropts.with_preserve_settings(false); } - if sub.get_flag("system") { ropts = ropts.with_system_files(true); } - if sub.get_flag("remove") { ropts = ropts.with_remove_items_not_restored(true); } - if let Some(pw) = sub.get_one::("password") { ropts = ropts.with_password(pw); } - match backup_client.restore_from_path(Path::new(dir), source, Some(ropts)).await { + if sub.get_flag("no-reboot") { + ropts = ropts.with_reboot(false); + } + if sub.get_flag("no-copy") { + ropts = ropts.with_copy(false); + } + if sub.get_flag("no-settings") { + ropts = ropts.with_preserve_settings(false); + } + if sub.get_flag("system") { + ropts = ropts.with_system_files(true); + } + if sub.get_flag("remove") { + ropts = ropts.with_remove_items_not_restored(true); + } + if let Some(pw) = sub.get_one::("password") { + ropts = ropts.with_password(pw); + } + match backup_client + .restore_from_path(Path::new(dir), source, Some(ropts)) + .await + { Ok(_) => println!("Restore flow finished"), Err(e) => eprintln!("Restore failed: {e}"), } @@ -205,7 +274,10 @@ async fn main() { let dir = sub.get_one::("dir").unwrap(); let source = sub.get_one::("source").map(|s| s.as_str()); let password = sub.get_one::("password").map(|s| s.as_str()); - match backup_client.unback_from_path(Path::new(dir), password, source).await { + match backup_client + .unback_from_path(Path::new(dir), password, source) + .await + { Ok(_) => println!("Unback finished"), Err(e) => eprintln!("Unback failed: {e}"), } @@ -216,7 +288,10 @@ async fn main() { let domain = sub.get_one::("domain").unwrap(); let rel = sub.get_one::("path").unwrap(); let password = sub.get_one::("password").map(|s| s.as_str()); - match backup_client.extract_from_path(domain, rel, Path::new(dir), password, source).await { + match backup_client + .extract_from_path(domain, rel, Path::new(dir), password, source) + .await + { Ok(_) => println!("Extract finished"), Err(e) => eprintln!("Extract failed: {e}"), } @@ -225,7 +300,10 @@ async fn main() { let dir = sub.get_one::("dir").unwrap(); let old = sub.get_one::("old").map(|s| s.as_str()); let newv = sub.get_one::("new").map(|s| s.as_str()); - match backup_client.change_password_from_path(Path::new(dir), old, newv).await { + match backup_client + .change_password_from_path(Path::new(dir), old, newv) + .await + { Ok(_) => println!("Change password finished"), Err(e) => eprintln!("Change password failed: {e}"), } @@ -237,23 +315,22 @@ async fn main() { Err(e) => eprintln!("Erase device failed: {e}"), } } - Some(("freespace", _)) => { - match backup_client.get_freespace().await { - Ok(freespace) => { - let freespace_gb = freespace as f64 / (1024.0 * 1024.0 * 1024.0); - println!("Free space: {freespace} bytes ({freespace_gb:.2} GB)"); - } - Err(e) => eprintln!("Failed to get free space: {e}"), + Some(("freespace", _)) => match backup_client.get_freespace().await { + Ok(freespace) => { + let freespace_gb = freespace as f64 / (1024.0 * 1024.0 * 1024.0); + println!("Free space: {freespace} bytes ({freespace_gb:.2} GB)"); } - } - Some(("encryption", _)) => { - match backup_client.check_backup_encryption().await { - Ok(is_encrypted) => { - println!("Backup encryption: {}", if is_encrypted { "Enabled" } else { "Disabled" }); - } - Err(e) => eprintln!("Failed to check backup encryption: {e}"), + Err(e) => eprintln!("Failed to get free space: {e}"), + }, + Some(("encryption", _)) => match backup_client.check_backup_encryption().await { + Ok(is_encrypted) => { + println!( + "Backup encryption: {}", + if is_encrypted { "Enabled" } else { "Disabled" } + ); } - } + Err(e) => eprintln!("Failed to check backup encryption: {e}"), + }, _ => { println!("No subcommand provided. Use --help for available commands."); } @@ -266,8 +343,7 @@ async fn main() { } use idevice::services::mobilebackup2::{ - DL_CODE_ERROR_LOCAL as CODE_ERROR_LOCAL, - DL_CODE_FILE_DATA as CODE_FILE_DATA, + DL_CODE_ERROR_LOCAL as CODE_ERROR_LOCAL, DL_CODE_FILE_DATA as CODE_FILE_DATA, DL_CODE_SUCCESS as CODE_SUCCESS, }; @@ -297,26 +373,36 @@ async fn process_dl_loop( } "DLMessageCreateDirectory" => { let status = create_directory_from_message(&value, host_dir); - client - .send_status_response(status, None, None) - .await?; + client.send_status_response(status, None, None).await?; } "DLMessageMoveFiles" | "DLMessageMoveItems" => { let status = move_files_from_message(&value, host_dir); client - .send_status_response(status, None, Some(plist::Value::Dictionary(Dictionary::new()))) + .send_status_response( + status, + None, + Some(plist::Value::Dictionary(Dictionary::new())), + ) .await?; } "DLMessageRemoveFiles" | "DLMessageRemoveItems" => { let status = remove_files_from_message(&value, host_dir); client - .send_status_response(status, None, Some(plist::Value::Dictionary(Dictionary::new()))) + .send_status_response( + status, + None, + Some(plist::Value::Dictionary(Dictionary::new())), + ) .await?; } "DLMessageCopyItem" => { let status = copy_item_from_message(&value, host_dir); client - .send_status_response(status, None, Some(plist::Value::Dictionary(Dictionary::new()))) + .send_status_response( + status, + None, + Some(plist::Value::Dictionary(Dictionary::new())), + ) .await?; } "DLMessageProcessMessage" => { @@ -353,23 +439,24 @@ async fn handle_download_files( && let Some(plist::Value::Array(files)) = arr.get(1) { for pv in files { - if let Some(path) = pv.as_string() - && let Err(e) = send_single_file(client, host_dir, path).await - { - eprintln!("Failed to send file {path}: {e}"); + if let Some(path) = pv.as_string() + && let Err(e) = send_single_file(client, host_dir, path).await + { + eprintln!("Failed to send file {path}: {e}"); err_any = true; } } } // terminating zero dword - client - .idevice - .send_raw(&0u32.to_be_bytes()) - .await?; + client.idevice.send_raw(&0u32.to_be_bytes()).await?; // status response if err_any { client - .send_status_response(-13, Some("Multi status"), Some(plist::Value::Dictionary(Dictionary::new()))) + .send_status_response( + -13, + Some("Multi status"), + Some(plist::Value::Dictionary(Dictionary::new())), + ) .await } else { client @@ -446,7 +533,8 @@ async fn handle_upload_files( if let Some(parent) = dst.parent() { let _ = fs::create_dir_all(parent); } - let mut file = std::fs::File::create(&dst).map_err(|e| idevice::IdeviceError::InternalError(e.to_string()))?; + let mut file = std::fs::File::create(&dst) + .map_err(|e| idevice::IdeviceError::InternalError(e.to_string()))?; loop { let nlen = read_be_u32(client).await?; if nlen == 0 { @@ -456,7 +544,8 @@ async fn handle_upload_files( if code == CODE_FILE_DATA { let size = (nlen - 1) as usize; let data = read_exact(client, size).await?; - file.write_all(&data).map_err(|e| idevice::IdeviceError::InternalError(e.to_string()))?; + file.write_all(&data) + .map_err(|e| idevice::IdeviceError::InternalError(e.to_string()))?; } else { let _ = read_exact(client, (nlen - 1) as usize).await?; } @@ -478,11 +567,17 @@ async fn read_one(client: &mut MobileBackup2Client) -> Result Result, idevice::IdeviceError> { +async fn read_exact( + client: &mut MobileBackup2Client, + size: usize, +) -> Result, idevice::IdeviceError> { client.idevice.read_raw(size).await } -async fn read_exact_string(client: &mut MobileBackup2Client, size: usize) -> Result { +async fn read_exact_string( + client: &mut MobileBackup2Client, + size: usize, +) -> Result { let buf = client.idevice.read_raw(size).await?; Ok(String::from_utf8_lossy(&buf).to_string()) } @@ -532,7 +627,9 @@ fn remove_files_from_message(dl_value: &plist::Value, host_dir: &Path) -> i64 { if let Some(p) = it.as_string() { let path = host_dir.join(p); if path.is_dir() { - if fs::remove_dir_all(&path).is_err() { return -1; } + if fs::remove_dir_all(&path).is_err() { + return -1; + } } else if path.exists() && fs::remove_file(&path).is_err() { return -1; } @@ -546,17 +643,26 @@ fn remove_files_from_message(dl_value: &plist::Value, host_dir: &Path) -> i64 { fn copy_item_from_message(dl_value: &plist::Value, host_dir: &Path) -> i64 { if let plist::Value::Array(arr) = dl_value && arr.len() >= 3 - && let (Some(plist::Value::String(src)), Some(plist::Value::String(dst))) = (arr.get(1), arr.get(2)) + && let (Some(plist::Value::String(src)), Some(plist::Value::String(dst))) = + (arr.get(1), arr.get(2)) { let from = host_dir.join(src); let to = host_dir.join(dst); - if let Some(parent) = to.parent() { let _ = fs::create_dir_all(parent); } + if let Some(parent) = to.parent() { + let _ = fs::create_dir_all(parent); + } if from.is_dir() { // shallow copy: create dir - return match fs::create_dir_all(&to) { Ok(_) => 0, Err(_) => -1 }; + return match fs::create_dir_all(&to) { + Ok(_) => 0, + Err(_) => -1, + }; } else { - return match fs::copy(&from, &to) { Ok(_) => 0, Err(_) => -1 }; + return match fs::copy(&from, &to) { + Ok(_) => 0, + Err(_) => -1, + }; } } -1 -} \ No newline at end of file +} diff --git a/tools/src/mounter.rs b/tools/src/mounter.rs index c59a472..1564a9d 100644 --- a/tools/src/mounter.rs +++ b/tools/src/mounter.rs @@ -3,10 +3,10 @@ use std::{io::Write, path::PathBuf}; -use clap::{arg, value_parser, Arg, Command}; +use clap::{Arg, Command, arg, value_parser}; use idevice::{ - lockdown::LockdownClient, mobile_image_mounter::ImageMounter, pretty_print_plist, - IdeviceService, + IdeviceService, lockdown::LockdownClient, mobile_image_mounter::ImageMounter, + pretty_print_plist, }; mod common; @@ -67,7 +67,9 @@ async fn main() { .get_matches(); if matches.get_flag("about") { - println!("mounter - query and manage images mounted on a device. Reimplementation of libimobiledevice's binary."); + println!( + "mounter - query and manage images mounted on a device. Reimplementation of libimobiledevice's binary." + ); println!("Copyright (c) 2025 Jackson Coxson"); return; } diff --git a/tools/src/os_trace_relay.rs b/tools/src/os_trace_relay.rs index 45c1b76..b04d0f7 100644 --- a/tools/src/os_trace_relay.rs +++ b/tools/src/os_trace_relay.rs @@ -1,7 +1,7 @@ // Jackson Coxson use clap::{Arg, Command}; -use idevice::{os_trace_relay::OsTraceRelayClient, IdeviceService}; +use idevice::{IdeviceService, os_trace_relay::OsTraceRelayClient}; mod common; diff --git a/tools/src/pair.rs b/tools/src/pair.rs index 4ca173d..baf3315 100644 --- a/tools/src/pair.rs +++ b/tools/src/pair.rs @@ -2,9 +2,9 @@ use clap::{Arg, Command}; use idevice::{ + IdeviceService, lockdown::LockdownClient, usbmuxd::{Connection, UsbmuxdAddr, UsbmuxdConnection}, - IdeviceService, }; #[tokio::main] diff --git a/tools/src/process_control.rs b/tools/src/process_control.rs index 511df79..e74b4e9 100644 --- a/tools/src/process_control.rs +++ b/tools/src/process_control.rs @@ -1,7 +1,7 @@ // Jackson Coxson use clap::{Arg, Command}; -use idevice::{core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake, IdeviceService, RsdService}; +use idevice::{IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake}; mod common; diff --git a/tools/src/remotexpc.rs b/tools/src/remotexpc.rs index e4bc551..38a1a8a 100644 --- a/tools/src/remotexpc.rs +++ b/tools/src/remotexpc.rs @@ -3,8 +3,8 @@ use clap::{Arg, Command}; use idevice::{ - core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake, tcp::stream::AdapterStream, - IdeviceService, + IdeviceService, core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake, + tcp::stream::AdapterStream, }; mod common; diff --git a/tools/src/restore_service.rs b/tools/src/restore_service.rs index e2dcd8a..448bd8c 100644 --- a/tools/src/restore_service.rs +++ b/tools/src/restore_service.rs @@ -2,8 +2,8 @@ use clap::{Arg, Command}; use idevice::{ - core_device_proxy::CoreDeviceProxy, pretty_print_dictionary, - restore_service::RestoreServiceClient, rsd::RsdHandshake, IdeviceService, RsdService, + IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, pretty_print_dictionary, + restore_service::RestoreServiceClient, rsd::RsdHandshake, }; mod common; @@ -51,7 +51,9 @@ async fn main() { .get_matches(); if matches.get_flag("about") { - println!("mounter - query and manage images mounted on a device. Reimplementation of libimobiledevice's binary."); + println!( + "mounter - query and manage images mounted on a device. Reimplementation of libimobiledevice's binary." + ); println!("Copyright (c) 2025 Jackson Coxson"); return; } diff --git a/tools/src/syslog_relay.rs b/tools/src/syslog_relay.rs index 0ebf483..c556fec 100644 --- a/tools/src/syslog_relay.rs +++ b/tools/src/syslog_relay.rs @@ -1,7 +1,7 @@ // Jackson Coxson use clap::{Arg, Command}; -use idevice::{syslog_relay::SyslogRelayClient, IdeviceService}; +use idevice::{IdeviceService, syslog_relay::SyslogRelayClient}; mod common; From 54caafb4da6fd5bfcbc3400fc683c897ce1f47bb Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Thu, 14 Aug 2025 13:34:43 -0600 Subject: [PATCH 063/126] Include std and tls12 in rustls This fixes FFI somehow. --- idevice/Cargo.toml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/idevice/Cargo.toml b/idevice/Cargo.toml index 8eafae5..1102aca 100644 --- a/idevice/Cargo.toml +++ b/idevice/Cargo.toml @@ -13,8 +13,11 @@ keywords = ["lockdownd", "ios"] [dependencies] tokio = { version = "1.43", features = ["io-util"] } tokio-rustls = { version = "0.26", default-features = false } -rustls = { version = "0.23", default-features = false } -crossfire = { version = "2.0", optional = true } # TODO: update to 2.1 when it comes out +rustls = { version = "0.23", default-features = false, features = [ + "std", + "tls12", +] } +crossfire = { version = "2.0", optional = true } # TODO: update to 2.1 when it comes out plist = { version = "1.7" } serde = { version = "1", features = ["derive"] } @@ -126,4 +129,3 @@ full = [ [package.metadata.docs.rs] all-features = true - From a16405f01190d8135fedbf78001c65244b2b1272 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Thu, 14 Aug 2025 17:02:58 -0600 Subject: [PATCH 064/126] Separate headers into cpp source files --- cpp/examples/CMakeLists.txt | 121 +++++---- cpp/examples/idevice_id.cpp | 5 +- cpp/examples/ideviceinfo.cpp | 64 +++++ cpp/include/ffi.hpp | 29 -- cpp/include/idevice++.hpp | 88 ------- cpp/include/idevice++/bindings.hpp | 8 + cpp/include/idevice++/ffi.hpp | 21 ++ cpp/include/idevice++/idevice.hpp | 63 +++++ cpp/include/idevice++/lockdown.hpp | 45 ++++ cpp/include/idevice++/pairing_file.hpp | 44 ++++ cpp/include/idevice++/provider.hpp | 49 ++++ cpp/include/idevice++/usbmuxd.hpp | 127 +++++++++ cpp/include/pairing_file.hpp | 82 ------ cpp/include/usbmuxd.hpp | 351 ------------------------- cpp/src/ffi.cpp | 17 ++ cpp/src/idevice.cpp | 56 ++++ cpp/src/lockdown.cpp | 67 +++++ cpp/src/pairing_file.cpp | 53 ++++ cpp/src/provider.cpp | 52 ++++ cpp/src/usbmuxd.cpp | 161 ++++++++++++ ffi/idevice.hpp | 16 -- ffi/src/lib.rs | 15 ++ ffi/src/lockdown.rs | 2 +- ffi/src/usbmuxd.rs | 20 ++ 24 files changed, 940 insertions(+), 616 deletions(-) create mode 100644 cpp/examples/ideviceinfo.cpp delete mode 100644 cpp/include/ffi.hpp delete mode 100644 cpp/include/idevice++.hpp create mode 100644 cpp/include/idevice++/bindings.hpp create mode 100644 cpp/include/idevice++/ffi.hpp create mode 100644 cpp/include/idevice++/idevice.hpp create mode 100644 cpp/include/idevice++/lockdown.hpp create mode 100644 cpp/include/idevice++/pairing_file.hpp create mode 100644 cpp/include/idevice++/provider.hpp create mode 100644 cpp/include/idevice++/usbmuxd.hpp delete mode 100644 cpp/include/pairing_file.hpp delete mode 100644 cpp/include/usbmuxd.hpp create mode 100644 cpp/src/ffi.cpp create mode 100644 cpp/src/idevice.cpp create mode 100644 cpp/src/lockdown.cpp create mode 100644 cpp/src/pairing_file.cpp create mode 100644 cpp/src/provider.cpp create mode 100644 cpp/src/usbmuxd.cpp delete mode 100644 ffi/idevice.hpp diff --git a/cpp/examples/CMakeLists.txt b/cpp/examples/CMakeLists.txt index c04a0eb..7754f81 100644 --- a/cpp/examples/CMakeLists.txt +++ b/cpp/examples/CMakeLists.txt @@ -1,13 +1,17 @@ # Jackson Coxson -set(CMAKE_EXPORT_COMPILE_COMMANDS ON) -cmake_minimum_required(VERSION 3.10) +cmake_minimum_required(VERSION 3.15) project(IdeviceFFI CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) -# Set the paths -set(HEADER_FILE ${CMAKE_SOURCE_DIR}/../../ffi/idevice.h) +# Paths +set(IDEVICE_CPP_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/../include) # public C++ headers +set(IDEVICE_CPP_SRC_DIR ${CMAKE_SOURCE_DIR}/../src) # C++ .cpp files +set(IDEVICE_FFI_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/../../ffi) # Rust FFI headers (idevice.h) if (MSVC) set(STATIC_LIB ${CMAKE_SOURCE_DIR}/../../target/release/idevice_ffi.lib) else() @@ -16,57 +20,84 @@ endif() set(EXAMPLES_DIR ${CMAKE_SOURCE_DIR}/../examples) set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) -set(IDEVICE_CPP_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/../include) # cpp/include -set(IDEVICE_FFI_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/../../ffi) # ffi/ - -set(CMAKE_CXX_STANDARD 17) -set(CMAKE_CXX_STANDARD_REQUIRED ON) +# Warnings if (MSVC) add_compile_options(/W4 /permissive- /EHsc) else() add_compile_options(-Wall -Wextra -Wpedantic) endif() -# Find all C++ example files +# ---- Build the C++ wrapper library ----------------------------------------- + +# Collect your .cpps (prefer listing explicitly in real projects) +file(GLOB IDEVICE_CPP_SOURCES + ${IDEVICE_CPP_SRC_DIR}/*.cpp +) + +add_library(idevice_cpp STATIC ${IDEVICE_CPP_SOURCES}) + +# Public headers for consumers; FFI headers are needed by your .cpps only +target_include_directories(idevice_cpp + PUBLIC + ${IDEVICE_CPP_INCLUDE_DIR} + PRIVATE + ${IDEVICE_FFI_INCLUDE_DIR} +) + +# Link to the Rust static lib (+ system libs/frameworks). Mark as PUBLIC so dependents inherit. +target_link_libraries(idevice_cpp + PUBLIC + ${STATIC_LIB} +) + +# Unix-y extras frequently required by Rust static libs +if (UNIX AND NOT APPLE) + find_package(Threads REQUIRED) + target_link_libraries(idevice_cpp PUBLIC Threads::Threads dl m) +endif() + +# Apple frameworks (propagate to dependents) +if (APPLE) + target_link_libraries(idevice_cpp PUBLIC + "-framework CoreFoundation" + "-framework Security" + "-framework SystemConfiguration" + "-framework CoreServices" + "-framework IOKit" + "-framework CFNetwork" + ) +endif() + +# Windows system libs often needed with Rust std/tokio/ring +if (WIN32) + target_link_libraries(idevice_cpp PUBLIC + ws2_32 + userenv + ntdll + bcrypt + # iphlpapi # uncomment if needed + # secur32 crypt32 ncrypt # if you switch TLS stacks + ) +endif() + +# Optional: position independent code if you later turn idevice_cpp into a shared lib +set_target_properties(idevice_cpp PROPERTIES POSITION_INDEPENDENT_CODE ON) + +# ---- Build each example and link against idevice_cpp ------------------------ + file(GLOB EXAMPLE_SOURCES ${EXAMPLES_DIR}/*.cpp) -# Create an executable for each example file foreach(EXAMPLE_FILE ${EXAMPLE_SOURCES}) - get_filename_component(EXAMPLE_NAME ${EXAMPLE_FILE} NAME_WE) - add_executable(${EXAMPLE_NAME} ${EXAMPLE_FILE}) + get_filename_component(EXAMPLE_NAME ${EXAMPLE_FILE} NAME_WE) + add_executable(${EXAMPLE_NAME} ${EXAMPLE_FILE}) - target_include_directories(${EXAMPLE_NAME} PRIVATE - ${IDEVICE_CPP_INCLUDE_DIR} - ${IDEVICE_FFI_INCLUDE_DIR} - ) + # Examples include public headers and (if they directly include FFI headers) the FFI dir. + target_include_directories(${EXAMPLE_NAME} PRIVATE + ${IDEVICE_CPP_INCLUDE_DIR} + ${IDEVICE_FFI_INCLUDE_DIR} + ) - # Include the generated header - target_include_directories(${EXAMPLE_NAME} PRIVATE ${CMAKE_SOURCE_DIR}/..) - - # Link the static Rust library - target_link_libraries(${EXAMPLE_NAME} PRIVATE ${STATIC_LIB}) - - # Bulk-link common macOS system frameworks - if(APPLE) - target_link_libraries(${EXAMPLE_NAME} PRIVATE - "-framework CoreFoundation" - "-framework Security" - "-framework SystemConfiguration" - "-framework CoreServices" - "-framework IOKit" - "-framework CFNetwork" - ) - elseif (WIN32) - # System libs required by Rust std/tokio/reqwest/ring on Windows - target_link_libraries(${EXAMPLE_NAME} PRIVATE - ws2_32 # WSAStartup/WSACleanup, closesocket, freeaddrinfo - userenv # GetUserProfileDirectoryW - ntdll # NtReadFile, RtlNtStatusToDosError - bcrypt # ring RNG (BCryptGenRandom) - # iphlpapi # (optional) GetAdaptersAddresses, if you ever see it unresolved - # secur32 crypt32 ncrypt # (only if you switch to schannel/native-tls) - ) - endif() + # Link to your C++ wrapper (inherits Rust lib + frameworks/system libs) + target_link_libraries(${EXAMPLE_NAME} PRIVATE idevice_cpp) endforeach() - diff --git a/cpp/examples/idevice_id.cpp b/cpp/examples/idevice_id.cpp index e2069d0..7ebabf7 100644 --- a/cpp/examples/idevice_id.cpp +++ b/cpp/examples/idevice_id.cpp @@ -1,13 +1,10 @@ // Jackson Coxson -#include "ffi.hpp" -#include "usbmuxd.hpp" +#include #include #include int main() { - std::cout << "Getting devices from usbmuxd\n"; - IdeviceFFI::FfiError e; std::optional u = IdeviceFFI::UsbmuxdConnection::default_new(0, e); diff --git a/cpp/examples/ideviceinfo.cpp b/cpp/examples/ideviceinfo.cpp new file mode 100644 index 0000000..a45968f --- /dev/null +++ b/cpp/examples/ideviceinfo.cpp @@ -0,0 +1,64 @@ +// Jackson Coxson + +#include +#include +#include +#include +#include + +int main() { + idevice_init_logger(Debug, Disabled, NULL); + + IdeviceFFI::FfiError e; + std::optional u = + IdeviceFFI::UsbmuxdConnection::default_new(0, e); + if (!u) { + std::cerr << "failed to connect to usbmuxd"; + std::cerr << e.message; + } + + auto devices = u->get_devices(e); + if (!devices) { + std::cerr << "failed to get devices from usbmuxd"; + std::cerr << e.message; + } + if (devices->empty()) { + std::cerr << "no devices connected"; + std::cerr << e.message; + } + + auto& dev = (*devices)[0]; + + auto udid = dev.get_udid(); + if (!udid) { + std::cerr << "no udid\n"; + return 1; + } + + auto id = dev.get_id(); + if (!id) { + std::cerr << "no id\n"; + return 1; + } + + IdeviceFFI::UsbmuxdAddr addr = IdeviceFFI::UsbmuxdAddr::default_new(); + auto prov = + IdeviceFFI::Provider::usbmuxd_new(std::move(addr), /*tag*/ 0, *udid, *id, "reeeeeeeee", e); + if (!prov) { + std::cerr << "provider failed: " << e.message << "\n"; + return 1; + } + + auto client = IdeviceFFI::Lockdown::connect(*prov, e); + if (!client) { + std::cerr << "lockdown connect failed: " << e.message << "\n"; + return 1; + } + + auto values = client->get_value("", "", e); + if (!values) { + std::cerr << "get values failed: " << e.message << "\n"; + return 1; + } + plist_free(*values); +} diff --git a/cpp/include/ffi.hpp b/cpp/include/ffi.hpp deleted file mode 100644 index 439b020..0000000 --- a/cpp/include/ffi.hpp +++ /dev/null @@ -1,29 +0,0 @@ -// Jackson Coxson - -#ifndef IDEVICE_FFI -#define IDEVICE_FFI - -#include "idevice.hpp" -#include - -namespace IdeviceFFI { -struct FfiError { - int32_t code = 0; - std::string message; - - static FfiError from(const IdeviceFfiError* err) { - FfiError out; - if (err) { - out.code = err->code; - out.message = err->message ? err->message : ""; - idevice_error_free(const_cast(err)); - } - return out; - } - - static FfiError success() { return {0, ""}; } - - explicit operator bool() const { return code != 0; } -}; -} // namespace IdeviceFFI -#endif diff --git a/cpp/include/idevice++.hpp b/cpp/include/idevice++.hpp deleted file mode 100644 index b16217e..0000000 --- a/cpp/include/idevice++.hpp +++ /dev/null @@ -1,88 +0,0 @@ -// Jackson Coxson - -#ifndef IDEVICE_CPP -#define IDEVICE_CPP - -#include "ffi.hpp" -#include "pairing_file.hpp" -#include -#include - -#if defined(_WIN32) && !defined(__MINGW32__) -// MSVC doesn't have BSD u_int* types -using u_int8_t = std::uint8_t; -using u_int16_t = std::uint16_t; -using u_int32_t = std::uint32_t; -using u_int64_t = std::uint64_t; -#endif - -namespace IdeviceFFI { - -class Idevice { - public: - static std::optional - create(IdeviceSocketHandle* socket, const std::string& label, FfiError& err) { - IdeviceHandle* handle = nullptr; - IdeviceFfiError* e = idevice_new(socket, label.c_str(), &handle); - if (e) { - err = FfiError::from(e); - return std::nullopt; - } - return Idevice(handle); - } - - static std::optional - create_tcp(const sockaddr* addr, socklen_t addr_len, const std::string& label, FfiError& err) { - IdeviceHandle* handle = nullptr; - IdeviceFfiError* e = idevice_new_tcp_socket(addr, addr_len, label.c_str(), &handle); - if (e) { - err = FfiError::from(e); - return std::nullopt; - } - return Idevice(handle); - } - - std::optional get_type(FfiError& err) { - char* type_cstr = nullptr; - IdeviceFfiError* e = idevice_get_type(handle_, &type_cstr); - if (e) { - err = FfiError::from(e); - return std::nullopt; - } - - std::string type_str(type_cstr); - idevice_string_free(type_cstr); - return type_str; - } - - bool rsd_checkin(FfiError& err) { - IdeviceFfiError* e = idevice_rsd_checkin(handle_); - if (e) { - err = FfiError::from(e); - return false; - } - return true; - } - - bool start_session(PairingFile& pairing_file, FfiError& err) { - IdeviceFfiError* e = idevice_start_session(handle_, pairing_file.raw()); - if (e) { - err = FfiError::from(e); - return false; - } - return true; - } - - ~Idevice() { - if (handle_) - idevice_free(handle_); - } - - explicit Idevice(IdeviceHandle* h) : handle_(h) {} - - private: - IdeviceHandle* handle_; -}; - -} // namespace IdeviceFFI -#endif diff --git a/cpp/include/idevice++/bindings.hpp b/cpp/include/idevice++/bindings.hpp new file mode 100644 index 0000000..3646e86 --- /dev/null +++ b/cpp/include/idevice++/bindings.hpp @@ -0,0 +1,8 @@ +// Jackson Coxson + +#ifndef IDEVICE_BINDINGS_H +#define IDEVICE_BINDINGS_H +extern "C" { +#include // this file is generated by bindgen +} +#endif diff --git a/cpp/include/idevice++/ffi.hpp b/cpp/include/idevice++/ffi.hpp new file mode 100644 index 0000000..75cea52 --- /dev/null +++ b/cpp/include/idevice++/ffi.hpp @@ -0,0 +1,21 @@ +// Jackson Coxson + +#ifndef IDEVICE_FFI +#define IDEVICE_FFI + +#include +#include + +namespace IdeviceFFI { +class FfiError { + public: + int32_t code = 0; + std::string message; + + FfiError(const IdeviceFfiError* err); + FfiError(); + + explicit operator bool() const { return code != 0; } +}; +} // namespace IdeviceFFI +#endif diff --git a/cpp/include/idevice++/idevice.hpp b/cpp/include/idevice++/idevice.hpp new file mode 100644 index 0000000..c5ce89f --- /dev/null +++ b/cpp/include/idevice++/idevice.hpp @@ -0,0 +1,63 @@ +// Jackson Coxson + +#ifndef IDEVICE_CPP +#define IDEVICE_CPP + +#include +#include +#include +#include + +#if defined(_WIN32) && !defined(__MINGW32__) +// MSVC doesn't have BSD u_int* types +using u_int8_t = std::uint8_t; +using u_int16_t = std::uint16_t; +using u_int32_t = std::uint32_t; +using u_int64_t = std::uint64_t; +#endif + +namespace IdeviceFFI { + +// Generic “bind a free function” deleter +template struct FnDeleter { + void operator()(T* p) const noexcept { + if (p) + FreeFn(p); + } +}; + +using IdevicePtr = std::unique_ptr>; + +class Idevice { + public: + static std::optional + create(IdeviceSocketHandle* socket, const std::string& label, FfiError& err); + + static std::optional + create_tcp(const sockaddr* addr, socklen_t addr_len, const std::string& label, FfiError& err); + + // Methods + std::optional get_type(FfiError& err) const; + bool rsd_checkin(FfiError& err); + bool start_session(const PairingFile& pairing_file, FfiError& err); + + // Ownership/RAII + ~Idevice() noexcept = default; + Idevice(Idevice&&) noexcept = default; + Idevice& operator=(Idevice&&) noexcept = default; + Idevice(const Idevice&) = delete; + Idevice& operator=(const Idevice&) = delete; + + static Idevice adopt(IdeviceHandle* h) noexcept { return Idevice(h); } + + // Accessor + IdeviceHandle* raw() const noexcept { return handle_.get(); } + IdeviceHandle* release() noexcept { return handle_.release(); } + + private: + explicit Idevice(IdeviceHandle* h) noexcept : handle_(h) {} + IdevicePtr handle_{}; +}; + +} // namespace IdeviceFFI +#endif diff --git a/cpp/include/idevice++/lockdown.hpp b/cpp/include/idevice++/lockdown.hpp new file mode 100644 index 0000000..9c962ae --- /dev/null +++ b/cpp/include/idevice++/lockdown.hpp @@ -0,0 +1,45 @@ + +#pragma once +#include +#include +#include +#include +#include +#include +#include + +namespace IdeviceFFI { + +using LockdownPtr = + std::unique_ptr>; + +class Lockdown { + public: + // Factory: connect via Provider + static std::optional connect(Provider& provider, FfiError& err); + + // Factory: wrap an existing Idevice socket (consumes it on success) + static std::optional from_socket(Idevice&& socket, FfiError& err); + + // Ops + bool start_session(const PairingFile& pf, FfiError& err); + std::optional> start_service(const std::string& identifier, + FfiError& err); + std::optional get_value(const char* key, const char* domain, FfiError& err); + + // RAII / moves + ~Lockdown() noexcept = default; + Lockdown(Lockdown&&) noexcept = default; + Lockdown& operator=(Lockdown&&) noexcept = default; + Lockdown(const Lockdown&) = delete; + Lockdown& operator=(const Lockdown&) = delete; + + LockdowndClientHandle* raw() const noexcept { return handle_.get(); } + static Lockdown adopt(LockdowndClientHandle* h) noexcept { return Lockdown(h); } + + private: + explicit Lockdown(LockdowndClientHandle* h) noexcept : handle_(h) {} + LockdownPtr handle_{}; +}; + +} // namespace IdeviceFFI diff --git a/cpp/include/idevice++/pairing_file.hpp b/cpp/include/idevice++/pairing_file.hpp new file mode 100644 index 0000000..04fffa6 --- /dev/null +++ b/cpp/include/idevice++/pairing_file.hpp @@ -0,0 +1,44 @@ +// Jackson Coxson + +#ifndef IDEVICE_PAIRING_FILE +#define IDEVICE_PAIRING_FILE + +#pragma once + +#include +#include +#include +#include + +namespace IdeviceFFI { +struct PairingFileDeleter { + void operator()(IdevicePairingFile* p) const noexcept; +}; + +using PairingFilePtr = std::unique_ptr; + +class PairingFile { + public: + static std::optional read(const std::string& path, FfiError& err); + static std::optional from_bytes(const uint8_t* data, size_t size, FfiError& err); + + ~PairingFile() noexcept = default; // unique_ptr handles destruction + + PairingFile(const PairingFile&) = delete; + PairingFile& operator=(const PairingFile&) = delete; + + PairingFile(PairingFile&&) noexcept = default; // move is correct by default + PairingFile& operator=(PairingFile&&) noexcept = default; + + std::optional> serialize(FfiError& err) const; + + explicit PairingFile(IdevicePairingFile* ptr) noexcept : ptr_(ptr) {} + IdevicePairingFile* raw() const noexcept { return ptr_.get(); } + IdevicePairingFile* release() noexcept { return ptr_.release(); } + + private: + PairingFilePtr ptr_{}; // owns the handle +}; + +} // namespace IdeviceFFI +#endif diff --git a/cpp/include/idevice++/provider.hpp b/cpp/include/idevice++/provider.hpp new file mode 100644 index 0000000..72a7175 --- /dev/null +++ b/cpp/include/idevice++/provider.hpp @@ -0,0 +1,49 @@ +// Jackson Coxson + +#pragma once +#include +#include +#include +#include +#include +#include + +namespace IdeviceFFI { + +class FfiError; +class PairingFile; // has: IdevicePairingFile* raw() const; void release_on_success(); +class UsbmuxdAddr; // has: UsbmuxdAddrHandle* raw() const; void release_on_success(); + +using ProviderPtr = + std::unique_ptr>; + +class Provider { + public: + static std::optional tcp_new(const idevice_sockaddr* ip, + PairingFile&& pairing, + const std::string& label, + FfiError& err); + + static std::optional usbmuxd_new(UsbmuxdAddr&& addr, + uint32_t tag, + const std::string& udid, + uint32_t device_id, + const std::string& label, + FfiError& err); + + ~Provider() noexcept = default; + Provider(Provider&&) noexcept = default; + Provider& operator=(Provider&&) noexcept = default; + Provider(const Provider&) = delete; + Provider& operator=(const Provider&) = delete; + + IdeviceProviderHandle* raw() const noexcept { return handle_.get(); } + static Provider adopt(IdeviceProviderHandle* h) noexcept { return Provider(h); } + IdeviceProviderHandle* release() noexcept { return handle_.release(); } + + private: + explicit Provider(IdeviceProviderHandle* h) noexcept : handle_(h) {} + ProviderPtr handle_{}; +}; + +} // namespace IdeviceFFI diff --git a/cpp/include/idevice++/usbmuxd.hpp b/cpp/include/idevice++/usbmuxd.hpp new file mode 100644 index 0000000..91fc7b4 --- /dev/null +++ b/cpp/include/idevice++/usbmuxd.hpp @@ -0,0 +1,127 @@ +// Jackson Coxson + +#ifndef IDEVICE_USBMUXD_HPP +#define IDEVICE_USBMUXD_HPP + +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#include +#else +#include +#endif + +namespace IdeviceFFI { + +using AddrPtr = + std::unique_ptr>; +using DevicePtr = std::unique_ptr>; +using ConnectionPtr = + std::unique_ptr>; + +class UsbmuxdAddr { + public: + static std::optional + tcp_new(const sockaddr* addr, socklen_t addr_len, FfiError& err); +#if defined(__unix__) || defined(__APPLE__) + static std::optional unix_new(const std::string& path, FfiError& err); +#endif + static UsbmuxdAddr default_new(); + + ~UsbmuxdAddr() noexcept = default; + UsbmuxdAddr(UsbmuxdAddr&&) noexcept = default; + UsbmuxdAddr& operator=(UsbmuxdAddr&&) noexcept = default; + UsbmuxdAddr(const UsbmuxdAddr&) = delete; + UsbmuxdAddr& operator=(const UsbmuxdAddr&) = delete; + + UsbmuxdAddrHandle* raw() const noexcept { return handle_.get(); } + UsbmuxdAddrHandle* release() noexcept { return handle_.release(); } + static UsbmuxdAddr adopt(UsbmuxdAddrHandle* h) noexcept { return UsbmuxdAddr(h); } + + private: + explicit UsbmuxdAddr(UsbmuxdAddrHandle* h) noexcept : handle_(h) {} + AddrPtr handle_{}; +}; + +class UsbmuxdConnectionType { + public: + enum class Value : uint8_t { Usb = 1, Network = 2, Unknown = 3 }; + explicit UsbmuxdConnectionType(uint8_t v) : _value(static_cast(v)) {} + + std::string to_string() const; // body in .cpp + Value value() const noexcept { return _value; } + bool operator==(Value other) const noexcept { return _value == other; } + + private: + Value _value{Value::Unknown}; +}; + +class UsbmuxdDevice { + public: + ~UsbmuxdDevice() noexcept = default; + UsbmuxdDevice(UsbmuxdDevice&&) noexcept = default; + UsbmuxdDevice& operator=(UsbmuxdDevice&&) noexcept = default; + UsbmuxdDevice(const UsbmuxdDevice&) = delete; + UsbmuxdDevice& operator=(const UsbmuxdDevice&) = delete; + + static UsbmuxdDevice adopt(UsbmuxdDeviceHandle* h) noexcept { return UsbmuxdDevice(h); } + + UsbmuxdDeviceHandle* raw() const noexcept { return handle_.get(); } + + std::optional get_udid() const; + std::optional get_id() const; + std::optional get_connection_type() const; + + private: + explicit UsbmuxdDevice(UsbmuxdDeviceHandle* h) noexcept : handle_(h) {} + DevicePtr handle_{}; + + friend class UsbmuxdConnection; +}; + +class PairingFile; + +class UsbmuxdConnection { + public: + static std::optional + tcp_new(const idevice_sockaddr* addr, idevice_socklen_t addr_len, uint32_t tag, FfiError& err); +#if defined(__unix__) || defined(__APPLE__) + static std::optional + unix_new(const std::string& path, uint32_t tag, FfiError& err); +#endif + static std::optional default_new(uint32_t tag, FfiError& err); + + ~UsbmuxdConnection() noexcept = default; + UsbmuxdConnection(UsbmuxdConnection&&) noexcept = default; + UsbmuxdConnection& operator=(UsbmuxdConnection&&) noexcept = default; + UsbmuxdConnection(const UsbmuxdConnection&) = delete; + UsbmuxdConnection& operator=(const UsbmuxdConnection&) = delete; + + std::optional> get_devices(FfiError& err) const; + std::optional get_buid(FfiError& err) const; + std::optional get_pair_record(const std::string& udid, FfiError& err); + + std::optional + connect_to_device(uint32_t device_id, uint16_t port, const std::string& path, FfiError& err) &&; + std::optional + connect_to_device(uint32_t, uint16_t, const std::string&, FfiError&) & = delete; + std::optional + connect_to_device(uint32_t, uint16_t, const std::string&, FfiError&) const& = delete; + + UsbmuxdConnectionHandle* raw() const noexcept { return handle_.get(); } + + private: + explicit UsbmuxdConnection(UsbmuxdConnectionHandle* h) noexcept : handle_(h) {} + ConnectionPtr handle_{}; +}; + +} // namespace IdeviceFFI +#endif diff --git a/cpp/include/pairing_file.hpp b/cpp/include/pairing_file.hpp deleted file mode 100644 index 9edfcf8..0000000 --- a/cpp/include/pairing_file.hpp +++ /dev/null @@ -1,82 +0,0 @@ -// Jackson Coxson - -#ifndef IDEVICE_PAIRING_FILE -#define IDEVICE_PAIRING_FILE - -#pragma once - -#include "ffi.hpp" -#include -#include -#include - -namespace IdeviceFFI { -class PairingFile { - public: - static std::optional read(const std::string& path, FfiError& err) { - IdevicePairingFile* ptr = nullptr; - IdeviceFfiError* e = idevice_pairing_file_read(path.c_str(), &ptr); - if (e) { - err = FfiError::from(e); - return std::nullopt; - } - return PairingFile(ptr); - } - - static std::optional from_bytes(const uint8_t* data, size_t size, FfiError& err) { - IdevicePairingFile* raw = nullptr; - IdeviceFfiError* e = idevice_pairing_file_from_bytes(data, size, &raw); - if (e) { - err = FfiError::from(e); - return std::nullopt; - } - return PairingFile(raw); - } - - ~PairingFile() { - if (ptr_) { - idevice_pairing_file_free(ptr_); - } - } - - // Deleted copy constructor and assignment — use unique ownersship - PairingFile(const PairingFile&) = delete; - PairingFile& operator=(const PairingFile&) = delete; - - // Move constructor and assignment - PairingFile(PairingFile&& other) noexcept : ptr_(other.ptr_) { other.ptr_ = nullptr; } - - PairingFile& operator=(PairingFile&& other) noexcept { - if (this != &other) { - if (ptr_) { - idevice_pairing_file_free(ptr_); - } - ptr_ = other.ptr_; - other.ptr_ = nullptr; - } - return *this; - } - - std::optional> serialize(FfiError& err) const { - uint8_t* data = nullptr; - size_t size = 0; - IdeviceFfiError* e = idevice_pairing_file_serialize(ptr_, &data, &size); - if (e) { - err = FfiError::from(e); - return std::nullopt; - } - - std::vector result(data, data + size); - delete[] data; // NOTE: adjust this if deallocation uses `free` or a custom function - return result; - } - - explicit PairingFile(IdevicePairingFile* ptr) : ptr_(ptr) {} - IdevicePairingFile* raw() const { return ptr_; } - - private: - IdevicePairingFile* ptr_; -}; - -} // namespace IdeviceFFI -#endif diff --git a/cpp/include/usbmuxd.hpp b/cpp/include/usbmuxd.hpp deleted file mode 100644 index fe33aad..0000000 --- a/cpp/include/usbmuxd.hpp +++ /dev/null @@ -1,351 +0,0 @@ -// Jackson Coxson - -#ifndef IDEVICE_USBMUXD_HPP -#define IDEVICE_USBMUXD_HPP - -#include "ffi.hpp" -#include "idevice++.hpp" -#include "pairing_file.hpp" -#include -#include -#include -#include - -#ifdef _WIN32 -#include -#include -#else -#include -#endif - -namespace IdeviceFFI { - -/// @brief A C++ wrapper for a UsbmuxdAddrHandle. -/// @details This class manages the memory of a UsbmuxdAddrHandle pointer. -/// It is non-copyable but is movable. -class UsbmuxdAddr { - public: - /// @brief Creates a new TCP usbmuxd address. - /// @param addr The socket address to connect to. - /// @param addr_len The length of the socket address. - /// @param err An error that will be populated on failure. - /// @return A UsbmuxdAddr on success, std::nullopt on failure. - static std::optional - tcp_new(const sockaddr* addr, socklen_t addr_len, FfiError& err) { - UsbmuxdAddrHandle* handle = nullptr; - IdeviceFfiError* e = idevice_usbmuxd_tcp_addr_new(addr, addr_len, &handle); - if (e) { - err = FfiError::from(e); - return std::nullopt; - } - return UsbmuxdAddr(handle); - } - -#if defined(__unix__) || defined(__APPLE__) - /// @brief Creates a new Unix socket usbmuxd address. - /// @param path The path to the unix socket. - /// @param err An error that will be populated on failure. - /// @return A UsbmuxdAddr on success, std::nullopt on failure. - static std::optional unix_new(const std::string& path, FfiError& err) { - UsbmuxdAddrHandle* handle = nullptr; - IdeviceFfiError* e = idevice_usbmuxd_unix_addr_new(path.c_str(), &handle); - if (e) { - err = FfiError::from(e); - return std::nullopt; - } - return UsbmuxdAddr(handle); - } -#endif - - ~UsbmuxdAddr() { - if (handle_) { - idevice_usbmuxd_addr_free(handle_); - } - } - - // Delete copy constructor and assignment operator - UsbmuxdAddr(const UsbmuxdAddr&) = delete; - UsbmuxdAddr& operator=(const UsbmuxdAddr&) = delete; - - // Define move constructor and assignment operator - UsbmuxdAddr(UsbmuxdAddr&& other) noexcept : handle_(other.handle_) { other.handle_ = nullptr; } - UsbmuxdAddr& operator=(UsbmuxdAddr&& other) noexcept { - if (this != &other) { - if (handle_) { - idevice_usbmuxd_addr_free(handle_); - } - handle_ = other.handle_; - other.handle_ = nullptr; - } - return *this; - } - - /// @brief Gets the raw handle. - /// @return The raw UsbmuxdAddrHandle pointer. - UsbmuxdAddrHandle* raw() const { return handle_; } - - private: - explicit UsbmuxdAddr(UsbmuxdAddrHandle* handle) : handle_(handle) {} - UsbmuxdAddrHandle* handle_; -}; - -class UsbmuxdConnectionType { - public: - enum Value : uint8_t { Usb = 1, Network = 2, Unknown = 3 }; - explicit UsbmuxdConnectionType(uint8_t v) : _value(static_cast(v)) {} - - std::string to_string() { - switch (_value) { - case UsbmuxdConnectionType::Usb: - return "USB"; - case UsbmuxdConnectionType::Network: - return "Network"; - case UsbmuxdConnectionType::Unknown: - return "Unknown"; - default: - return "UnknownEnumValue"; - } - } - - Value value() const { return _value; } - - bool operator==(Value other) const { return _value == other; } - - private: - Value _value; -}; - -class UsbmuxdDevice { - public: - ~UsbmuxdDevice() { - if (handle_) { - idevice_usbmuxd_device_free(handle_); - } - } - // Delete copy constructor and assignment operator - UsbmuxdDevice(const UsbmuxdDevice&) = delete; - UsbmuxdDevice& operator=(const UsbmuxdDevice&) = delete; - - // Define move constructor and assignment operator - UsbmuxdDevice(UsbmuxdDevice&& other) noexcept : handle_(other.handle_) { - other.handle_ = nullptr; - } - UsbmuxdDevice& operator=(UsbmuxdDevice&& other) noexcept { - if (this != &other) { - if (handle_) { - idevice_usbmuxd_device_free(handle_); - } - handle_ = other.handle_; - other.handle_ = nullptr; - } - return *this; - } - - /// @brief Gets the raw handle. - /// @return The raw UsbmuxdConnectionHandle pointer. - UsbmuxdDeviceHandle* raw() const { return handle_; } - - std::optional get_udid() { - char* udid = idevice_usbmuxd_device_get_udid(handle_); - if (udid) { - std::string cppUdid(udid); - idevice_string_free(udid); - return cppUdid; - } else { - return std::nullopt; - } - } - - std::optional get_id() { - uint32_t id = idevice_usbmuxd_device_get_device_id(handle_); - if (id == 0) { - return std::nullopt; - } else { - return id; - } - } - - std::optional get_connection_type() { - u_int8_t t = idevice_usbmuxd_device_get_connection_type(handle_); - if (t == 0) { - return std::nullopt; - } else { - } - return static_cast(t); - } - explicit UsbmuxdDevice(UsbmuxdDeviceHandle* handle) : handle_(handle) {} - - private: - UsbmuxdDeviceHandle* handle_; -}; - -/// @brief A C++ wrapper for a UsbmuxdConnectionHandle. -/// @details This class manages the memory of a UsbmuxdConnectionHandle pointer. -/// It is non-copyable but is movable. -class UsbmuxdConnection { - public: - /// @brief Creates a new TCP usbmuxd connection. - /// @param addr The socket address to connect to. - /// @param addr_len The length of the socket address. - /// @param tag A tag that will be returned by usbmuxd responses. - /// @param err An error that will be populated on failure. - /// @return A UsbmuxdConnection on success, std::nullopt on failure. - static std::optional - tcp_new(const idevice_sockaddr* addr, idevice_socklen_t addr_len, uint32_t tag, FfiError& err) { - UsbmuxdConnectionHandle* handle = nullptr; - IdeviceFfiError* e = idevice_usbmuxd_new_tcp_connection(addr, addr_len, tag, &handle); - if (e) { - err = FfiError::from(e); - return std::nullopt; - } - return UsbmuxdConnection(handle); - } - -#if defined(__unix__) || defined(__APPLE__) - /// @brief Creates a new Unix socket usbmuxd connection. - /// @param path The path to the unix socket. - /// @param tag A tag that will be returned by usbmuxd responses. - /// @param err An error that will be populated on failure. - /// @return A UsbmuxdConnection on success, std::nullopt on failure. - static std::optional - unix_new(const std::string& path, uint32_t tag, FfiError& err) { - UsbmuxdConnectionHandle* handle = nullptr; - IdeviceFfiError* e = idevice_usbmuxd_new_unix_socket_connection(path.c_str(), tag, &handle); - if (e) { - err = FfiError::from(e); - return std::nullopt; - } - return UsbmuxdConnection(handle); - } -#endif - - /// @brief Creates a new usbmuxd connection using the default path. - /// @param tag A tag that will be returned by usbmuxd responses. - /// @param err An error that will be populated on failure. - /// @return A UsbmuxdConnection on success, std::nullopt on failure. - static std::optional default_new(uint32_t tag, FfiError& err) { - UsbmuxdConnectionHandle* handle = nullptr; - IdeviceFfiError* e = idevice_usbmuxd_new_default_connection(tag, &handle); - if (e) { - err = FfiError::from(e); - return std::nullopt; - } - return UsbmuxdConnection(handle); - } - - /// @brief Gets a list of all connected devices. - /// @param err An error that will be populated on failure. - /// @return A vector of UsbmuxdDevice objects on success, std::nullopt on failure. - std::optional> get_devices(FfiError& err) { - UsbmuxdDeviceHandle** devices = nullptr; - int count = 0; - IdeviceFfiError* e = idevice_usbmuxd_get_devices(handle_, &devices, &count); - if (e) { - err = FfiError::from(e); - return std::nullopt; - } - - std::vector result; - result.reserve(count); - for (int i = 0; i < count; ++i) { - result.emplace_back(devices[i]); - } - - return result; - } - - /// @brief Gets the BUID from the daemon. - /// @param err An error that will be populated on failure. - /// @return The BUID string on success, std::nullopt on failure. - std::optional get_buid(FfiError& err) { - char* buid_c = nullptr; - IdeviceFfiError* e = idevice_usbmuxd_get_buid(handle_, &buid_c); - if (e) { - err = FfiError::from(e); - return std::nullopt; - } - std::string buid(buid_c); - idevice_string_free(buid_c); - return buid; - } - - /// @brief Gets the pairing record for a device. - /// @param udid The UDID of the target device. - /// @param err An error that will be populated on failure. - /// @return A PairingFile object on success, std::nullopt on failure. - std::optional get_pair_record(const std::string& udid, FfiError& err) { - IdevicePairingFile* pf_handle = nullptr; - IdeviceFfiError* e = idevice_usbmuxd_get_pair_record(handle_, udid.c_str(), &pf_handle); - if (e) { - err = FfiError::from(e); - return std::nullopt; - } - return PairingFile(pf_handle); - } - - /// @brief Connects to a port on a given device. - /// @details This operation consumes the UsbmuxdConnection object. After a successful call, - /// this object will be invalid and should not be used. - /// @param device_id The ID of the target device. - /// @param port The port number to connect to. - /// @param err An error that will be populated on failure. - /// @return An Idevice connection object on success, std::nullopt on failure. - std::optional - connect_to_device(uint32_t device_id, uint16_t port, const std::string& path, FfiError& err) { - if (!handle_) { - // Can't connect with an invalid handle - return std::nullopt; - } - - IdeviceHandle* idevice_handle = nullptr; - IdeviceFfiError* e = idevice_usbmuxd_connect_to_device( - handle_, device_id, port, path.c_str(), &idevice_handle); - - // The handle is always consumed by the FFI call, so we must invalidate it. - handle_ = nullptr; - - if (e) { - err = FfiError::from(e); - return std::nullopt; - } - - return Idevice(idevice_handle); - } - - ~UsbmuxdConnection() { - if (handle_) { - idevice_usbmuxd_connection_free(handle_); - } - } - - // Delete copy constructor and assignment operator - UsbmuxdConnection(const UsbmuxdConnection&) = delete; - UsbmuxdConnection& operator=(const UsbmuxdConnection&) = delete; - - // Define move constructor and assignment operator - UsbmuxdConnection(UsbmuxdConnection&& other) noexcept : handle_(other.handle_) { - other.handle_ = nullptr; - } - UsbmuxdConnection& operator=(UsbmuxdConnection&& other) noexcept { - if (this != &other) { - if (handle_) { - idevice_usbmuxd_connection_free(handle_); - } - handle_ = other.handle_; - other.handle_ = nullptr; - } - return *this; - } - - /// @brief Gets the raw handle. - /// @return The raw UsbmuxdConnectionHandle pointer. - UsbmuxdConnectionHandle* raw() const { return handle_; } - - private: - explicit UsbmuxdConnection(UsbmuxdConnectionHandle* handle) : handle_(handle) {} - UsbmuxdConnectionHandle* handle_; -}; - -} // namespace IdeviceFFI - -#endif diff --git a/cpp/src/ffi.cpp b/cpp/src/ffi.cpp new file mode 100644 index 0000000..fc95293 --- /dev/null +++ b/cpp/src/ffi.cpp @@ -0,0 +1,17 @@ +// Jackson Coxson + +#include +#include +#include + +namespace IdeviceFFI { +FfiError::FfiError(const IdeviceFfiError* err) + : code(err ? err->code : 0), message(err && err->message ? err->message : "") { + if (err) { + idevice_error_free(const_cast(err)); + } +} + +FfiError::FfiError() : code(0), message("") { +} +} // namespace IdeviceFFI diff --git a/cpp/src/idevice.cpp b/cpp/src/idevice.cpp new file mode 100644 index 0000000..6899092 --- /dev/null +++ b/cpp/src/idevice.cpp @@ -0,0 +1,56 @@ +// Jackson Coxson + +#include + +namespace IdeviceFFI { + +std::optional +Idevice::create(IdeviceSocketHandle* socket, const std::string& label, FfiError& err) { + IdeviceHandle* h = nullptr; + if (IdeviceFfiError* e = idevice_new(socket, label.c_str(), &h)) { + err = FfiError(e); + return std::nullopt; + } + return Idevice(h); +} + +std::optional Idevice::create_tcp(const sockaddr* addr, + socklen_t addr_len, + const std::string& label, + FfiError& err) { + IdeviceHandle* h = nullptr; + if (IdeviceFfiError* e = idevice_new_tcp_socket(addr, addr_len, label.c_str(), &h)) { + err = FfiError(e); + return std::nullopt; + } + return Idevice(h); +} + +std::optional Idevice::get_type(FfiError& err) const { + char* cstr = nullptr; + if (IdeviceFfiError* e = idevice_get_type(handle_.get(), &cstr)) { + err = FfiError(e); + return std::nullopt; + } + std::string out(cstr); + idevice_string_free(cstr); + return out; +} + +bool Idevice::rsd_checkin(FfiError& err) { + if (IdeviceFfiError* e = idevice_rsd_checkin(handle_.get())) { + err = FfiError(e); + return false; + } + return true; +} + +bool Idevice::start_session(const PairingFile& pairing_file, FfiError& err) { + if (IdeviceFfiError* e = idevice_start_session(handle_.get(), pairing_file.raw())) { + err = FfiError(e); + return false; + } + return true; +} + +} // namespace IdeviceFFI diff --git a/cpp/src/lockdown.cpp b/cpp/src/lockdown.cpp new file mode 100644 index 0000000..e00659a --- /dev/null +++ b/cpp/src/lockdown.cpp @@ -0,0 +1,67 @@ +// Jackson Coxson + +#include +#include +#include +#include + +namespace IdeviceFFI { + +std::optional Lockdown::connect(Provider& provider, FfiError& err) { + LockdowndClientHandle* out = nullptr; + + if (IdeviceFfiError* e = ::lockdownd_connect(provider.raw(), &out)) { + // Rust freed the provider on error -> abandon our ownership to avoid double free. + // Your Provider wrapper should expose release(). + provider.release(); + err = FfiError(e); + return std::nullopt; + } + // Success: provider is NOT consumed; keep ownership. + return Lockdown::adopt(out); +} + +std::optional Lockdown::from_socket(Idevice&& socket, FfiError& err) { + LockdowndClientHandle* out = nullptr; + + if (IdeviceFfiError* e = ::lockdownd_new(socket.raw(), &out)) { + // Error: Rust did NOT consume the socket (it returns early for invalid args), + // so keep ownership; report error. + err = FfiError(e); + return std::nullopt; + } + // Success: Rust consumed the socket -> abandon our ownership. + socket.release(); + return Lockdown::adopt(out); +} + +bool Lockdown::start_session(const PairingFile& pf, FfiError& err) { + if (IdeviceFfiError* e = ::lockdownd_start_session(handle_.get(), pf.raw())) { + err = FfiError(e); + return false; + } + return true; +} + +std::optional> Lockdown::start_service(const std::string& identifier, + FfiError& err) { + uint16_t port = 0; + bool ssl = false; + if (IdeviceFfiError* e = + ::lockdownd_start_service(handle_.get(), identifier.c_str(), &port, &ssl)) { + err = FfiError(e); + return std::nullopt; + } + return std::make_pair(port, ssl); +} + +std::optional Lockdown::get_value(const char* key, const char* domain, FfiError& err) { + plist_t out = nullptr; + if (IdeviceFfiError* e = ::lockdownd_get_value(handle_.get(), key, domain, &out)) { + err = FfiError(e); + return std::nullopt; + } + return out; // caller now owns `out` and must free with the plist API +} + +} // namespace IdeviceFFI diff --git a/cpp/src/pairing_file.cpp b/cpp/src/pairing_file.cpp new file mode 100644 index 0000000..bc2f388 --- /dev/null +++ b/cpp/src/pairing_file.cpp @@ -0,0 +1,53 @@ +// Jackson Coxson + +#include +#include +#include + +namespace IdeviceFFI { + +// Deleter definition (out-of-line) +void PairingFileDeleter::operator()(IdevicePairingFile* p) const noexcept { + if (p) + idevice_pairing_file_free(p); +} + +// Static member definitions +std::optional PairingFile::read(const std::string& path, FfiError& err) { + IdevicePairingFile* ptr = nullptr; + if (IdeviceFfiError* e = idevice_pairing_file_read(path.c_str(), &ptr)) { + err = FfiError(e); + return std::nullopt; + } + return PairingFile(ptr); +} + +std::optional +PairingFile::from_bytes(const uint8_t* data, size_t size, FfiError& err) { + IdevicePairingFile* raw = nullptr; + if (IdeviceFfiError* e = idevice_pairing_file_from_bytes(data, size, &raw)) { + err = FfiError(e); + return std::nullopt; + } + return PairingFile(raw); +} + +std::optional> PairingFile::serialize(FfiError& err) const { + if (!ptr_) { + return std::nullopt; + } + + uint8_t* data = nullptr; + size_t size = 0; + + if (IdeviceFfiError* e = idevice_pairing_file_serialize(ptr_.get(), &data, &size)) { + err = FfiError(e); + return std::nullopt; + } + + std::vector out(data, data + size); + idevice_data_free(data, size); + return out; +} + +} // namespace IdeviceFFI diff --git a/cpp/src/provider.cpp b/cpp/src/provider.cpp new file mode 100644 index 0000000..80c9c3c --- /dev/null +++ b/cpp/src/provider.cpp @@ -0,0 +1,52 @@ +// Jackson Coxson + +#include +#include +#include + +namespace IdeviceFFI { + +std::optional Provider::tcp_new(const idevice_sockaddr* ip, + PairingFile&& pairing, + const std::string& label, + FfiError& err) { + IdeviceProviderHandle* out = nullptr; + + // Call with exact types; do NOT cast to void* + if (IdeviceFfiError* e = idevice_tcp_provider_new( + ip, static_cast(pairing.raw()), label.c_str(), &out)) { + err = FfiError(e); + return std::nullopt; + } + + // Success: Rust consumed the pairing file -> abandon our ownership + pairing.release(); // implement as: ptr_.release() in PairingFile + + return Provider::adopt(out); +} + +std::optional Provider::usbmuxd_new(UsbmuxdAddr&& addr, + uint32_t tag, + const std::string& udid, + uint32_t device_id, + const std::string& label, + FfiError& err) { + IdeviceProviderHandle* out = nullptr; + + if (IdeviceFfiError* e = usbmuxd_provider_new(static_cast(addr.raw()), + tag, + udid.c_str(), + device_id, + label.c_str(), + &out)) { + err = FfiError(e); + return std::nullopt; + } + + // Success: Rust consumed the addr -> abandon our ownership + addr.release(); // implement as: ptr_.release() in UsbmuxdAddr + + return Provider::adopt(out); +} + +} // namespace IdeviceFFI diff --git a/cpp/src/usbmuxd.cpp b/cpp/src/usbmuxd.cpp new file mode 100644 index 0000000..45bdb08 --- /dev/null +++ b/cpp/src/usbmuxd.cpp @@ -0,0 +1,161 @@ +// Jackson Coxson + +#include +#include + +namespace IdeviceFFI { + +// ---------- UsbmuxdAddr ---------- +std::optional +UsbmuxdAddr::tcp_new(const sockaddr* addr, socklen_t addr_len, FfiError& err) { + UsbmuxdAddrHandle* h = nullptr; + if (IdeviceFfiError* e = idevice_usbmuxd_tcp_addr_new(addr, addr_len, &h)) { + err = FfiError(e); + return std::nullopt; + } + return UsbmuxdAddr(h); +} + +#if defined(__unix__) || defined(__APPLE__) +std::optional UsbmuxdAddr::unix_new(const std::string& path, FfiError& err) { + UsbmuxdAddrHandle* h = nullptr; + if (IdeviceFfiError* e = idevice_usbmuxd_unix_addr_new(path.c_str(), &h)) { + err = FfiError(e); + return std::nullopt; + } + return UsbmuxdAddr(h); +} +#endif + +UsbmuxdAddr UsbmuxdAddr::default_new() { + UsbmuxdAddrHandle* h = nullptr; + idevice_usbmuxd_default_addr_new(&h); + return UsbmuxdAddr::adopt(h); +} + +// ---------- UsbmuxdConnectionType ---------- +std::string UsbmuxdConnectionType::to_string() const { + switch (_value) { + case Value::Usb: + return "USB"; + case Value::Network: + return "Network"; + case Value::Unknown: + return "Unknown"; + default: + return "UnknownEnumValue"; + } +} + +// ---------- UsbmuxdDevice ---------- +std::optional UsbmuxdDevice::get_udid() const { + char* c = idevice_usbmuxd_device_get_udid(handle_.get()); + if (!c) + return std::nullopt; + std::string out(c); + idevice_string_free(c); + return out; +} + +std::optional UsbmuxdDevice::get_id() const { + uint32_t id = idevice_usbmuxd_device_get_device_id(handle_.get()); + if (id == 0) + return std::nullopt; // adjust if 0 can be valid + return id; +} + +std::optional UsbmuxdDevice::get_connection_type() const { + uint8_t t = idevice_usbmuxd_device_get_connection_type(handle_.get()); + if (t == 0) + return std::nullopt; // adjust to your API contract + return UsbmuxdConnectionType(t); +} + +// ---------- UsbmuxdConnection ---------- +std::optional UsbmuxdConnection::tcp_new(const idevice_sockaddr* addr, + idevice_socklen_t addr_len, + uint32_t tag, + FfiError& err) { + UsbmuxdConnectionHandle* h = nullptr; + if (IdeviceFfiError* e = idevice_usbmuxd_new_tcp_connection(addr, addr_len, tag, &h)) { + err = FfiError(e); + return std::nullopt; + } + return UsbmuxdConnection(h); +} + +#if defined(__unix__) || defined(__APPLE__) +std::optional +UsbmuxdConnection::unix_new(const std::string& path, uint32_t tag, FfiError& err) { + UsbmuxdConnectionHandle* h = nullptr; + if (IdeviceFfiError* e = idevice_usbmuxd_new_unix_socket_connection(path.c_str(), tag, &h)) { + err = FfiError(e); + return std::nullopt; + } + return UsbmuxdConnection(h); +} +#endif + +std::optional UsbmuxdConnection::default_new(uint32_t tag, FfiError& err) { + UsbmuxdConnectionHandle* h = nullptr; + if (IdeviceFfiError* e = idevice_usbmuxd_new_default_connection(tag, &h)) { + err = FfiError(e); + return std::nullopt; + } + return UsbmuxdConnection(h); +} + +std::optional> UsbmuxdConnection::get_devices(FfiError& err) const { + UsbmuxdDeviceHandle** list = nullptr; + int count = 0; + if (IdeviceFfiError* e = idevice_usbmuxd_get_devices(handle_.get(), &list, &count)) { + err = FfiError(e); + return std::nullopt; + } + std::vector out; + out.reserve(count); + for (int i = 0; i < count; ++i) { + out.emplace_back(UsbmuxdDevice::adopt(list[i])); + } + return out; +} + +std::optional UsbmuxdConnection::get_buid(FfiError& err) const { + char* c = nullptr; + if (IdeviceFfiError* e = idevice_usbmuxd_get_buid(handle_.get(), &c)) { + err = FfiError(e); + return std::nullopt; + } + std::string out(c); + idevice_string_free(c); + return out; +} + +std::optional UsbmuxdConnection::get_pair_record(const std::string& udid, + FfiError& err) { + IdevicePairingFile* pf = nullptr; + if (IdeviceFfiError* e = idevice_usbmuxd_get_pair_record(handle_.get(), udid.c_str(), &pf)) { + err = FfiError(e); + return std::nullopt; + } + return PairingFile(pf); +} + +std::optional UsbmuxdConnection::connect_to_device(uint32_t device_id, + uint16_t port, + const std::string& path, + FfiError& err) && { + UsbmuxdConnectionHandle* raw = handle_.release(); + + IdeviceHandle* out = nullptr; + IdeviceFfiError* e = + idevice_usbmuxd_connect_to_device(raw, device_id, port, path.c_str(), &out); + + if (e) { + err = FfiError(e); + return std::nullopt; + } + return Idevice::adopt(out); +} + +} // namespace IdeviceFFI diff --git a/ffi/idevice.hpp b/ffi/idevice.hpp deleted file mode 100644 index 6aac99c..0000000 --- a/ffi/idevice.hpp +++ /dev/null @@ -1,16 +0,0 @@ -// Jackson Coxson - Bindings to idevice - https://github.com/jkcoxson/idevice -// This file only wraps the C bindings for C++, the C bindings still must be -// generated. -// ``cargo build --release`` - -#pragma once - -#ifdef __cplusplus -extern "C" { -#endif - -#include "idevice.h" // this file is generated by bindgen - -#ifdef __cplusplus -} -#endif diff --git a/ffi/src/lib.rs b/ffi/src/lib.rs index a7b64fc..9bd1833 100644 --- a/ffi/src/lib.rs +++ b/ffi/src/lib.rs @@ -315,3 +315,18 @@ pub unsafe extern "C" fn idevice_string_free(string: *mut c_char) { let _ = unsafe { CString::from_raw(string) }; } } + +/// Frees data allocated by this library +/// +/// # Arguments +/// * [`data`] - The data to free +/// +/// # Safety +/// `data` must be a valid pointer to data that was allocated by this library, +/// or NULL (in which case this function does nothing) +#[unsafe(no_mangle)] +pub unsafe extern "C" fn idevice_data_free(data: *mut u8, len: usize) { + if !data.is_null() { + let _ = unsafe { std::slice::from_raw_parts(data, len) }; + } +} diff --git a/ffi/src/lockdown.rs b/ffi/src/lockdown.rs index 81b0580..63bf36d 100644 --- a/ffi/src/lockdown.rs +++ b/ffi/src/lockdown.rs @@ -12,7 +12,7 @@ use crate::{ pub struct LockdowndClientHandle(pub LockdownClient); -/// Connects to lockdownd service using TCP provider +/// Connects to lockdownd service using provider /// /// # Arguments /// * [`provider`] - An IdeviceProvider diff --git a/ffi/src/usbmuxd.rs b/ffi/src/usbmuxd.rs index fcccd94..3f0b932 100644 --- a/ffi/src/usbmuxd.rs +++ b/ffi/src/usbmuxd.rs @@ -428,6 +428,26 @@ pub unsafe extern "C" fn idevice_usbmuxd_unix_addr_new( null_mut() } +/// Creates a default UsbmuxdAddr struct for the platform +/// +/// # Arguments +/// * [`usbmuxd_addr`] - On success, will be set to point to a newly allocated UsbmuxdAddr handle +/// +/// # Returns +/// An IdeviceFfiError on error, null on success +/// +/// # Safety +/// `usbmuxd_addr` must be a valid, non-null pointer to a location where the handle will be stored +#[unsafe(no_mangle)] +pub unsafe extern "C" fn idevice_usbmuxd_default_addr_new( + usbmuxd_addr: *mut *mut UsbmuxdAddrHandle, +) -> *mut IdeviceFfiError { + let addr = UsbmuxdAddr::default(); + let boxed = Box::new(UsbmuxdAddrHandle(addr)); + unsafe { *usbmuxd_addr = Box::into_raw(boxed) }; + null_mut() +} + /// Frees a UsbmuxdAddr handle /// /// # Arguments From f152f18f8e3ed46cbf73753e74001517e95ea47b Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Thu, 14 Aug 2025 17:41:59 -0600 Subject: [PATCH 065/126] Fix building on Linux --- cpp/include/idevice++/pairing_file.hpp | 1 + 1 file changed, 1 insertion(+) diff --git a/cpp/include/idevice++/pairing_file.hpp b/cpp/include/idevice++/pairing_file.hpp index 04fffa6..46203e9 100644 --- a/cpp/include/idevice++/pairing_file.hpp +++ b/cpp/include/idevice++/pairing_file.hpp @@ -9,6 +9,7 @@ #include #include #include +#include namespace IdeviceFFI { struct PairingFileDeleter { From c607909beb0f765126751a236ef248ec004e48e2 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Thu, 14 Aug 2025 18:07:56 -0600 Subject: [PATCH 066/126] Implement ideviceinfo in cpp --- cpp/examples/ideviceinfo.cpp | 12 +++++++++++- cpp/include/idevice++/provider.hpp | 2 ++ cpp/src/provider.cpp | 11 +++++++++++ ffi/src/lockdown.rs | 2 +- ffi/src/provider.rs | 30 ++++++++++++++++++++++++++++++ 5 files changed, 55 insertions(+), 2 deletions(-) diff --git a/cpp/examples/ideviceinfo.cpp b/cpp/examples/ideviceinfo.cpp index a45968f..8c3246c 100644 --- a/cpp/examples/ideviceinfo.cpp +++ b/cpp/examples/ideviceinfo.cpp @@ -15,16 +15,19 @@ int main() { if (!u) { std::cerr << "failed to connect to usbmuxd"; std::cerr << e.message; + return 1; } auto devices = u->get_devices(e); if (!devices) { std::cerr << "failed to get devices from usbmuxd"; std::cerr << e.message; + return 1; } if (devices->empty()) { std::cerr << "no devices connected"; std::cerr << e.message; + return 1; } auto& dev = (*devices)[0]; @@ -55,7 +58,14 @@ int main() { return 1; } - auto values = client->get_value("", "", e); + auto pf = prov->get_pairing_file(e); + if (!pf) { + std::cerr << "failed to get pairing file: " << e.message << "\n"; + return 1; + } + client->start_session(*pf, e); + + auto values = client->get_value(NULL, NULL, e); if (!values) { std::cerr << "get values failed: " << e.message << "\n"; return 1; diff --git a/cpp/include/idevice++/provider.hpp b/cpp/include/idevice++/provider.hpp index 72a7175..e976058 100644 --- a/cpp/include/idevice++/provider.hpp +++ b/cpp/include/idevice++/provider.hpp @@ -37,6 +37,8 @@ class Provider { Provider(const Provider&) = delete; Provider& operator=(const Provider&) = delete; + std::optional get_pairing_file(FfiError& err); + IdeviceProviderHandle* raw() const noexcept { return handle_.get(); } static Provider adopt(IdeviceProviderHandle* h) noexcept { return Provider(h); } IdeviceProviderHandle* release() noexcept { return handle_.release(); } diff --git a/cpp/src/provider.cpp b/cpp/src/provider.cpp index 80c9c3c..6a5f606 100644 --- a/cpp/src/provider.cpp +++ b/cpp/src/provider.cpp @@ -49,4 +49,15 @@ std::optional Provider::usbmuxd_new(UsbmuxdAddr&& addr, return Provider::adopt(out); } +std::optional Provider::get_pairing_file(FfiError& err) { + + IdevicePairingFile* out = nullptr; + if (IdeviceFfiError* e = idevice_provider_get_pairing_file(handle_.get(), &out)) { + err = FfiError(e); + return std::nullopt; + } + + return PairingFile(out); +} + } // namespace IdeviceFFI diff --git a/ffi/src/lockdown.rs b/ffi/src/lockdown.rs index 63bf36d..a6f8959 100644 --- a/ffi/src/lockdown.rs +++ b/ffi/src/lockdown.rs @@ -179,7 +179,7 @@ pub unsafe extern "C" fn lockdownd_get_value( domain: *const libc::c_char, out_plist: *mut plist_t, ) -> *mut IdeviceFfiError { - if key.is_null() || out_plist.is_null() { + if out_plist.is_null() { return ffi_err!(IdeviceError::FfiInvalidArg); } diff --git a/ffi/src/provider.rs b/ffi/src/provider.rs index 95fd169..4ef967c 100644 --- a/ffi/src/provider.rs +++ b/ffi/src/provider.rs @@ -7,6 +7,7 @@ use std::{ffi::CStr, ptr::null_mut}; use crate::util::{SockAddr, idevice_sockaddr}; use crate::{IdeviceFfiError, ffi_err, usbmuxd::UsbmuxdAddrHandle, util}; +use crate::{IdevicePairingFile, RUNTIME}; pub struct IdeviceProviderHandle(pub Box); @@ -136,3 +137,32 @@ pub unsafe extern "C" fn usbmuxd_provider_new( null_mut() } + +/// Gets the pairing file for the device +/// +/// # Arguments +/// * [`provider`] - A pointer to the provider +/// * [`pairing_file`] - A pointer to the newly allocated pairing file +/// +/// # Returns +/// An IdeviceFfiError on error, null on success +/// +/// # Safety +/// `provider` must be a valid, non-null pointer to the provider +#[unsafe(no_mangle)] +pub unsafe extern "C" fn idevice_provider_get_pairing_file( + provider: *mut IdeviceProviderHandle, + pairing_file: *mut *mut IdevicePairingFile, +) -> *mut IdeviceFfiError { + let provider = unsafe { &mut *provider }; + + let res = RUNTIME.block_on(async move { provider.0.get_pairing_file().await }); + match res { + Ok(pf) => { + let pf = Box::new(IdevicePairingFile(pf)); + unsafe { *pairing_file = Box::into_raw(pf) }; + null_mut() + } + Err(e) => ffi_err!(e), + } +} From 855e8748a62f44b4b028d01dcee610e31cf8d2ab Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Fri, 15 Aug 2025 12:24:07 -0600 Subject: [PATCH 067/126] Add plist_ffi cpp sources --- .gitmodules | 3 +++ cpp/examples/CMakeLists.txt | 16 ++++++++++------ cpp/examples/ideviceinfo.cpp | 5 ++++- cpp/plist_ffi | 1 + 4 files changed, 18 insertions(+), 7 deletions(-) create mode 160000 cpp/plist_ffi diff --git a/.gitmodules b/.gitmodules index 1cffb04..9b7710e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "ffi/libplist"] path = ffi/libplist url = https://github.com/libimobiledevice/libplist.git +[submodule "cpp/plist_ffi"] + path = cpp/plist_ffi + url = https://github.com/jkcoxson/plist_ffi.git diff --git a/cpp/examples/CMakeLists.txt b/cpp/examples/CMakeLists.txt index 7754f81..ecc3e84 100644 --- a/cpp/examples/CMakeLists.txt +++ b/cpp/examples/CMakeLists.txt @@ -12,6 +12,8 @@ set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) set(IDEVICE_CPP_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/../include) # public C++ headers set(IDEVICE_CPP_SRC_DIR ${CMAKE_SOURCE_DIR}/../src) # C++ .cpp files set(IDEVICE_FFI_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/../../ffi) # Rust FFI headers (idevice.h) +set(PLIST_CPP_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/../plist_ffi/cpp/include) +set(PLIST_CPP_SRC_DIR ${CMAKE_SOURCE_DIR}/../plist_ffi/cpp/src) if (MSVC) set(STATIC_LIB ${CMAKE_SOURCE_DIR}/../../target/release/idevice_ffi.lib) else() @@ -29,19 +31,23 @@ endif() # ---- Build the C++ wrapper library ----------------------------------------- -# Collect your .cpps (prefer listing explicitly in real projects) +# Collect your .cpps file(GLOB IDEVICE_CPP_SOURCES ${IDEVICE_CPP_SRC_DIR}/*.cpp ) -add_library(idevice_cpp STATIC ${IDEVICE_CPP_SOURCES}) +file(GLOB PLIST_CPP_SOURCES + ${PLIST_CPP_SRC_DIR}/*.cpp +) + +add_library(idevice_cpp STATIC ${IDEVICE_CPP_SOURCES} ${PLIST_CPP_SOURCES}) -# Public headers for consumers; FFI headers are needed by your .cpps only target_include_directories(idevice_cpp PUBLIC ${IDEVICE_CPP_INCLUDE_DIR} PRIVATE ${IDEVICE_FFI_INCLUDE_DIR} + ${PLIST_CPP_INCLUDE_DIR} ) # Link to the Rust static lib (+ system libs/frameworks). Mark as PUBLIC so dependents inherit. @@ -68,15 +74,12 @@ if (APPLE) ) endif() -# Windows system libs often needed with Rust std/tokio/ring if (WIN32) target_link_libraries(idevice_cpp PUBLIC ws2_32 userenv ntdll bcrypt - # iphlpapi # uncomment if needed - # secur32 crypt32 ncrypt # if you switch TLS stacks ) endif() @@ -95,6 +98,7 @@ foreach(EXAMPLE_FILE ${EXAMPLE_SOURCES}) target_include_directories(${EXAMPLE_NAME} PRIVATE ${IDEVICE_CPP_INCLUDE_DIR} ${IDEVICE_FFI_INCLUDE_DIR} + ${PLIST_CPP_INCLUDE_DIR} ) # Link to your C++ wrapper (inherits Rust lib + frameworks/system libs) diff --git a/cpp/examples/ideviceinfo.cpp b/cpp/examples/ideviceinfo.cpp index 8c3246c..d0078ca 100644 --- a/cpp/examples/ideviceinfo.cpp +++ b/cpp/examples/ideviceinfo.cpp @@ -1,10 +1,12 @@ // Jackson Coxson +#include "plist/Dictionary.h" #include #include #include #include #include +#include int main() { idevice_init_logger(Debug, Disabled, NULL); @@ -70,5 +72,6 @@ int main() { std::cerr << "get values failed: " << e.message << "\n"; return 1; } - plist_free(*values); + PList::Dictionary res = PList::Dictionary(*values); + std::cout << res.ToXml(); } diff --git a/cpp/plist_ffi b/cpp/plist_ffi new file mode 160000 index 0000000..44b0453 --- /dev/null +++ b/cpp/plist_ffi @@ -0,0 +1 @@ +Subproject commit 44b04532705d9919678a02fb9a2f5f20a6713e11 From da8c5ce377587662a38c385d7567e22d3a7c11c2 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Fri, 15 Aug 2025 13:50:18 -0600 Subject: [PATCH 068/126] Bump plist_ffi --- Cargo.lock | 4 ++-- ffi/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6b143e4..3176418 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1746,9 +1746,9 @@ dependencies = [ [[package]] name = "plist_ffi" -version = "0.1.3" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06fba796e1466c87b971298751de238bb441f6a2a5b4b03b874880225550e4b3" +checksum = "22a5ca928241bc2e8c5fd28b81772962389efdbfcb71dfc9ec694369e063cb3a" dependencies = [ "cbindgen", "cc", diff --git a/ffi/Cargo.toml b/ffi/Cargo.toml index f1ca3f9..3c21df6 100644 --- a/ffi/Cargo.toml +++ b/ffi/Cargo.toml @@ -12,7 +12,7 @@ once_cell = "1.21.1" tokio = { version = "1.44.1", features = ["full"] } libc = "0.2.171" plist = "1.7.1" -plist_ffi = "0.1.3" +plist_ffi = { version = "0.1.5" } [target.'cfg(windows)'.dependencies] windows-sys = { version = "0.60", features = ["Win32_Networking_WinSock"] } From e3f7aa8cb0437bb7231d2aeb89197bfe1e0150c1 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Fri, 15 Aug 2025 14:55:08 -0600 Subject: [PATCH 069/126] Create async handle in RUNTIME --- ffi/src/core_device_proxy.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ffi/src/core_device_proxy.rs b/ffi/src/core_device_proxy.rs index 8282681..c3928db 100644 --- a/ffi/src/core_device_proxy.rs +++ b/ffi/src/core_device_proxy.rs @@ -311,7 +311,10 @@ pub unsafe extern "C" fn core_device_proxy_create_tcp_adapter( match result { Ok(adapter_obj) => { - let boxed = Box::new(AdapterHandle(adapter_obj.to_async_handle())); + // We have to run this in the RUNTIME since we're spawning a new thread + let adapter_handle = RUNTIME.block_on(async move { adapter_obj.to_async_handle() }); + + let boxed = Box::new(AdapterHandle(adapter_handle)); unsafe { *adapter = Box::into_raw(boxed) }; null_mut() } From 50896b1dfe608b4ffb4419a9a46c6b913db277a6 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Fri, 15 Aug 2025 14:55:36 -0600 Subject: [PATCH 070/126] Use TCP multithreaded handle instead of lifetime handle for FFI --- ffi/src/adapter.rs | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/ffi/src/adapter.rs b/ffi/src/adapter.rs index 79c7a8d..6c466b4 100644 --- a/ffi/src/adapter.rs +++ b/ffi/src/adapter.rs @@ -3,12 +3,13 @@ use std::ffi::{CStr, c_char}; use std::ptr::null_mut; -use idevice::tcp::stream::AdapterStream; +use idevice::tcp::handle::StreamHandle; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; use crate::core_device_proxy::AdapterHandle; use crate::{IdeviceFfiError, RUNTIME, ReadWriteOpaque, ffi_err}; -pub struct AdapterStreamHandle<'a>(pub AdapterStream<'a>); +pub struct AdapterStreamHandle(pub StreamHandle); /// Connects the adapter to a specific port /// @@ -108,15 +109,9 @@ pub unsafe extern "C" fn adapter_close(handle: *mut AdapterStreamHandle) -> *mut } let adapter = unsafe { &mut (*handle).0 }; - let res = RUNTIME.block_on(async move { adapter.close().await }); + RUNTIME.block_on(async move { adapter.close() }); - match res { - Ok(_) => null_mut(), - Err(e) => { - log::error!("Adapter close failed: {e}"); - ffi_err!(e) - } - } + null_mut() } /// Sends data through the adapter stream @@ -145,7 +140,7 @@ pub unsafe extern "C" fn adapter_send( let adapter = unsafe { &mut (*handle).0 }; let data_slice = unsafe { std::slice::from_raw_parts(data, length) }; - let res = RUNTIME.block_on(async move { adapter.psh(data_slice).await }); + let res = RUNTIME.block_on(async move { adapter.write_all(data_slice).await }); match res { Ok(_) => null_mut(), @@ -183,7 +178,11 @@ pub unsafe extern "C" fn adapter_recv( } let adapter = unsafe { &mut (*handle).0 }; - let res = RUNTIME.block_on(async move { adapter.recv().await }); + let res: Result, std::io::Error> = RUNTIME.block_on(async move { + let mut buf = [0; 2048]; + let res = adapter.read(&mut buf).await?; + Ok(buf[..res].to_vec()) + }); match res { Ok(received_data) => { From b00be3fa2630429892bdc21faa46fca1399fa586 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Fri, 15 Aug 2025 14:55:47 -0600 Subject: [PATCH 071/126] Implement close for StreamHandle --- idevice/src/tcp/handle.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/idevice/src/tcp/handle.rs b/idevice/src/tcp/handle.rs index 43e6612..9ee240a 100644 --- a/idevice/src/tcp/handle.rs +++ b/idevice/src/tcp/handle.rs @@ -205,6 +205,14 @@ pub struct StreamHandle { pending_writes: FuturesUnordered>>, } +impl StreamHandle { + pub fn close(&mut self) { + let _ = self.send_channel.send(HandleMessage::Close { + host_port: self.host_port, + }); + } +} + impl AsyncRead for StreamHandle { /// Attempts to read from the connection into the provided buffer. /// From 36770ffd67fc03e608d57e1f82b547949cb02d09 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Fri, 15 Aug 2025 14:56:02 -0600 Subject: [PATCH 072/126] Create location simulation example in cpp --- cpp/examples/location_simulation.cpp | 135 ++++++++++++++++++ cpp/include/idevice++/adapter_stream.hpp | 49 +++++++ cpp/include/idevice++/core_device_proxy.hpp | 101 +++++++++++++ cpp/include/idevice++/location_simulation.hpp | 38 +++++ cpp/include/idevice++/readwrite.hpp | 45 ++++++ cpp/include/idevice++/remote_server.hpp | 44 ++++++ cpp/include/idevice++/rsd.hpp | 55 +++++++ cpp/src/adapter_stream.cpp | 43 ++++++ cpp/src/core_device.cpp | 119 +++++++++++++++ cpp/src/location_simulation.cpp | 32 +++++ cpp/src/remote_server.cpp | 30 ++++ cpp/src/rsd.cpp | 129 +++++++++++++++++ 12 files changed, 820 insertions(+) create mode 100644 cpp/examples/location_simulation.cpp create mode 100644 cpp/include/idevice++/adapter_stream.hpp create mode 100644 cpp/include/idevice++/core_device_proxy.hpp create mode 100644 cpp/include/idevice++/location_simulation.hpp create mode 100644 cpp/include/idevice++/readwrite.hpp create mode 100644 cpp/include/idevice++/remote_server.hpp create mode 100644 cpp/include/idevice++/rsd.hpp create mode 100644 cpp/src/adapter_stream.cpp create mode 100644 cpp/src/core_device.cpp create mode 100644 cpp/src/location_simulation.cpp create mode 100644 cpp/src/remote_server.cpp create mode 100644 cpp/src/rsd.cpp diff --git a/cpp/examples/location_simulation.cpp b/cpp/examples/location_simulation.cpp new file mode 100644 index 0000000..824eebc --- /dev/null +++ b/cpp/examples/location_simulation.cpp @@ -0,0 +1,135 @@ +// Jackson Coxson + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace IdeviceFFI; + +static void die(const char* msg, const FfiError& e) { + std::cerr << msg << ": " << e.message << "\n"; + std::exit(1); +} + +int main(int argc, char** argv) { + // Usage: + // simulate_location clear + // simulate_location set + bool do_clear = false; + std::optional lat, lon; + + if (argc == 2 && std::string(argv[1]) == "clear") { + do_clear = true; + } else if (argc == 4 && std::string(argv[1]) == "set") { + lat = std::stod(argv[2]); + lon = std::stod(argv[3]); + } else { + std::cerr << "Usage:\n" + << " " << argv[0] << " clear\n" + << " " << argv[0] << " set \n"; + return 2; + } + + FfiError err; + + // 1) Connect to usbmuxd and pick first device + auto mux = UsbmuxdConnection::default_new(/*tag*/ 0, err); + if (!mux) + die("failed to connect to usbmuxd", err); + + auto devices = mux->get_devices(err); + if (!devices) + die("failed to list devices", err); + if (devices->empty()) { + std::cerr << "no devices connected\n"; + return 1; + } + auto& dev = (*devices)[0]; + + auto udidOpt = dev.get_udid(); + if (!udidOpt) { + std::cerr << "device has no UDID\n"; + return 1; + } + auto idOpt = dev.get_id(); + if (!idOpt) { + std::cerr << "device has no mux id\n"; + return 1; + } + + // 2) Make a Provider for this device via default addr + auto addr = UsbmuxdAddr::default_new(); + + const uint32_t tag = 0; + const std::string label = "simulate_location-jkcoxson"; + + auto provider = Provider::usbmuxd_new(std::move(addr), tag, *udidOpt, *idOpt, label, err); + if (!provider) + die("failed to create provider", err); + + // 3) Connect CoreDeviceProxy (borrow provider) + auto cdp = CoreDeviceProxy::connect(*provider, err); + if (!cdp) + die("failed to connect CoreDeviceProxy", err); + + // 4) Read handshake’s server RSD port + auto rsd_port = cdp->get_server_rsd_port(err); + if (!rsd_port) + die("failed to get server RSD port", err); + + // 5) Create software tunnel adapter (consumes proxy) + auto adapter = std::move(*cdp).create_tcp_adapter(err); + if (!adapter) + die("failed to create software tunnel adapter", err); + + // 6) Connect adapter to RSD port → ReadWrite stream + auto stream = adapter->connect(*rsd_port, err); + if (!stream) + die("failed to connect RSD stream", err); + + // 7) RSD handshake (consumes stream) + auto rsd = RsdHandshake::from_socket(std::move(*stream), err); + if (!rsd) + die("failed RSD handshake", err); + + // 8) RemoteServer over RSD (borrows adapter + handshake) + auto rs = RemoteServer::connect_rsd(*adapter, *rsd, err); + if (!rs) + die("failed to connect RemoteServer", err); + + // 9) LocationSimulation client (borrows RemoteServer) + auto sim = LocationSimulation::create(*rs, err); + if (!sim) + die("failed to create LocationSimulation client", err); + + if (do_clear) { + if (!sim->clear(err)) + die("clear failed", err); + std::cout << "Location cleared!\n"; + return 0; + } + + // set path + if (!sim->set(*lat, *lon, err)) + die("set failed", err); + std::cout << "Location set to (" << *lat << ", " << *lon << ")\n"; + std::cout << "Press Ctrl-C to stop\n"; + + // keep process alive like the Rust example + for (;;) { + if (!sim->set(*lat, *lon, err)) + die("set failed", err); + std::this_thread::sleep_for(std::chrono::seconds(3)); + } +} diff --git a/cpp/include/idevice++/adapter_stream.hpp b/cpp/include/idevice++/adapter_stream.hpp new file mode 100644 index 0000000..862e91c --- /dev/null +++ b/cpp/include/idevice++/adapter_stream.hpp @@ -0,0 +1,49 @@ +// Jackson Coxson + +#pragma once +#include +#include +#include +#include +#include + +struct IdeviceFfiError; +struct AdapterStreamHandle; + +namespace IdeviceFFI { + +// Non-owning view over a stream (must call close(); no implicit free provided) +class AdapterStream { + public: + explicit AdapterStream(AdapterStreamHandle* h) noexcept : h_(h) {} + + AdapterStream(const AdapterStream&) = delete; + AdapterStream& operator=(const AdapterStream&) = delete; + + AdapterStream(AdapterStream&& other) noexcept : h_(other.h_) { other.h_ = nullptr; } + AdapterStream& operator=(AdapterStream&& other) noexcept { + if (this != &other) { + h_ = other.h_; + other.h_ = nullptr; + } + return *this; + } + + ~AdapterStream() noexcept = default; // no auto-close; caller controls + + AdapterStreamHandle* raw() const noexcept { return h_; } + + bool close(FfiError& err); + bool send(const uint8_t* data, size_t len, FfiError& err); + bool send(const std::vector& buf, FfiError& err) { + return send(buf.data(), buf.size(), err); + } + + // recv into caller-provided buffer (resizes to actual length) + bool recv(std::vector& out, FfiError& err, size_t max_hint = 2048); + + private: + AdapterStreamHandle* h_{}; +}; + +} // namespace IdeviceFFI diff --git a/cpp/include/idevice++/core_device_proxy.hpp b/cpp/include/idevice++/core_device_proxy.hpp new file mode 100644 index 0000000..2b392d2 --- /dev/null +++ b/cpp/include/idevice++/core_device_proxy.hpp @@ -0,0 +1,101 @@ +// Jackson Coxson + +#ifndef IDEVICE_CORE_DEVICE_PROXY_H +#define IDEVICE_CORE_DEVICE_PROXY_H + +#include +#include +#include +#include + +namespace IdeviceFFI { + +using CoreProxyPtr = std::unique_ptr>; +using AdapterPtr = std::unique_ptr>; + +struct CoreClientParams { + uint16_t mtu{}; + std::string address; // freed from Rust side after copy + std::string netmask; // freed from Rust side after copy +}; + +class Adapter { + public: + ~Adapter() noexcept = default; + Adapter(Adapter&&) noexcept = default; + Adapter& operator=(Adapter&&) noexcept = default; + Adapter(const Adapter&) = delete; + Adapter& operator=(const Adapter&) = delete; + + static Adapter adopt(AdapterHandle* h) noexcept { return Adapter(h); } + AdapterHandle* raw() const noexcept { return handle_.get(); } + + // Enable PCAP + bool pcap(const std::string& path, FfiError& err) { + if (IdeviceFfiError* e = ::adapter_pcap(handle_.get(), path.c_str())) { + err = FfiError(e); + return false; + } + return true; + } + + // Connect to a port, returns a ReadWrite stream (to be consumed by RSD/CoreDeviceProxy) + std::optional connect(uint16_t port, FfiError& err) { + ReadWriteOpaque* s = nullptr; + if (IdeviceFfiError* e = ::adapter_connect(handle_.get(), port, &s)) { + err = FfiError(e); + return std::nullopt; + } + return ReadWrite::adopt(s); + } + + private: + explicit Adapter(AdapterHandle* h) noexcept : handle_(h) {} + AdapterPtr handle_{}; +}; + +class CoreDeviceProxy { + public: + // Factory: connect using a Provider (NOT consumed on success or error) + static std::optional connect(Provider& provider, FfiError& err); + + // Factory: from a socket; Rust consumes the socket regardless of result → we release before + // call + static std::optional from_socket(Idevice&& socket, FfiError& err); + + // Send/recv + bool send(const uint8_t* data, size_t len, FfiError& err); + bool send(const std::vector& buf, FfiError& err) { + return send(buf.data(), buf.size(), err); + } + + // recv into a pre-sized buffer; resizes to actual bytes received + bool recv(std::vector& out, FfiError& err); + + // Handshake info + std::optional get_client_parameters(FfiError& err) const; + std::optional get_server_address(FfiError& err) const; + std::optional get_server_rsd_port(FfiError& err) const; + + // Consuming creation of a TCP adapter: Rust consumes the proxy handle + std::optional create_tcp_adapter(FfiError& err) &&; + + // RAII / moves + ~CoreDeviceProxy() noexcept = default; + CoreDeviceProxy(CoreDeviceProxy&&) noexcept = default; + CoreDeviceProxy& operator=(CoreDeviceProxy&&) noexcept = default; + CoreDeviceProxy(const CoreDeviceProxy&) = delete; + CoreDeviceProxy& operator=(const CoreDeviceProxy&) = delete; + + CoreDeviceProxyHandle* raw() const noexcept { return handle_.get(); } + CoreDeviceProxyHandle* release() noexcept { return handle_.release(); } + static CoreDeviceProxy adopt(CoreDeviceProxyHandle* h) noexcept { return CoreDeviceProxy(h); } + + private: + explicit CoreDeviceProxy(CoreDeviceProxyHandle* h) noexcept : handle_(h) {} + CoreProxyPtr handle_{}; +}; + +} // namespace IdeviceFFI +#endif diff --git a/cpp/include/idevice++/location_simulation.hpp b/cpp/include/idevice++/location_simulation.hpp new file mode 100644 index 0000000..5324292 --- /dev/null +++ b/cpp/include/idevice++/location_simulation.hpp @@ -0,0 +1,38 @@ +// Jackson Coxson + +#pragma once +#include +#include +#include +#include + +namespace IdeviceFFI { + +using LocSimPtr = std::unique_ptr>; + +class LocationSimulation { + public: + // Factory: borrows the RemoteServer; not consumed + static std::optional create(RemoteServer& server, FfiError& err); + + bool clear(FfiError& err); + bool set(double latitude, double longitude, FfiError& err); + + ~LocationSimulation() noexcept = default; + LocationSimulation(LocationSimulation&&) noexcept = default; + LocationSimulation& operator=(LocationSimulation&&) noexcept = default; + LocationSimulation(const LocationSimulation&) = delete; + LocationSimulation& operator=(const LocationSimulation&) = delete; + + LocationSimulationHandle* raw() const noexcept { return handle_.get(); } + static LocationSimulation adopt(LocationSimulationHandle* h) noexcept { + return LocationSimulation(h); + } + + private: + explicit LocationSimulation(LocationSimulationHandle* h) noexcept : handle_(h) {} + LocSimPtr handle_{}; +}; + +} // namespace IdeviceFFI diff --git a/cpp/include/idevice++/readwrite.hpp b/cpp/include/idevice++/readwrite.hpp new file mode 100644 index 0000000..f9a8000 --- /dev/null +++ b/cpp/include/idevice++/readwrite.hpp @@ -0,0 +1,45 @@ +// Jackson Coxson + +#pragma once + +#include + +namespace IdeviceFFI { + +// A move-only holder for a fat-pointer stream. It does NOT free on destruction. +// Always pass ownership to an FFI that consumes it by calling release(). +class ReadWrite { + public: + ReadWrite() noexcept : ptr_(nullptr) {} + explicit ReadWrite(ReadWriteOpaque* p) noexcept : ptr_(p) {} + + ReadWrite(const ReadWrite&) = delete; + ReadWrite& operator=(const ReadWrite&) = delete; + + ReadWrite(ReadWrite&& other) noexcept : ptr_(other.ptr_) { other.ptr_ = nullptr; } + ReadWrite& operator=(ReadWrite&& other) noexcept { + if (this != &other) { + ptr_ = other.ptr_; + other.ptr_ = nullptr; + } + return *this; + } + + ~ReadWrite() noexcept = default; // no dtor – Rust consumers own free/drop + + ReadWriteOpaque* raw() const noexcept { return ptr_; } + ReadWriteOpaque* release() noexcept { + auto* p = ptr_; + ptr_ = nullptr; + return p; + } + + static ReadWrite adopt(ReadWriteOpaque* p) noexcept { return ReadWrite(p); } + + explicit operator bool() const noexcept { return ptr_ != nullptr; } + + private: + ReadWriteOpaque* ptr_; +}; + +} // namespace IdeviceFFI diff --git a/cpp/include/idevice++/remote_server.hpp b/cpp/include/idevice++/remote_server.hpp new file mode 100644 index 0000000..51f8d98 --- /dev/null +++ b/cpp/include/idevice++/remote_server.hpp @@ -0,0 +1,44 @@ +// Jackson Coxson + +#ifndef IDEVICE_REMOTE_SERVER_H +#define IDEVICE_REMOTE_SERVER_H + +#pragma once +#include +#include +#include +#include +#include +#include + +namespace IdeviceFFI { + +using RemoteServerPtr = + std::unique_ptr>; + +class RemoteServer { + public: + // Factory: consumes the ReadWrite stream regardless of result + static std::optional from_socket(ReadWrite&& rw, FfiError& err); + + // Factory: borrows adapter + handshake (neither is consumed) + static std::optional + connect_rsd(Adapter& adapter, RsdHandshake& rsd, FfiError& err); + + // RAII / moves + ~RemoteServer() noexcept = default; + RemoteServer(RemoteServer&&) noexcept = default; + RemoteServer& operator=(RemoteServer&&) noexcept = default; + RemoteServer(const RemoteServer&) = delete; + RemoteServer& operator=(const RemoteServer&) = delete; + + RemoteServerHandle* raw() const noexcept { return handle_.get(); } + static RemoteServer adopt(RemoteServerHandle* h) noexcept { return RemoteServer(h); } + + private: + explicit RemoteServer(RemoteServerHandle* h) noexcept : handle_(h) {} + RemoteServerPtr handle_{}; +}; + +} // namespace IdeviceFFI +#endif diff --git a/cpp/include/idevice++/rsd.hpp b/cpp/include/idevice++/rsd.hpp new file mode 100644 index 0000000..b3898ec --- /dev/null +++ b/cpp/include/idevice++/rsd.hpp @@ -0,0 +1,55 @@ +// Jackson Coxson + +#ifndef IDEVICE_RSD_H +#define IDEVICE_RSD_H + +#include +#include +#include +#include + +namespace IdeviceFFI { + +struct RsdService { + std::string name; + std::string entitlement; + uint16_t port{}; + bool uses_remote_xpc{}; + std::vector features; + int64_t service_version{-1}; +}; + +using RsdPtr = + std::unique_ptr>; + +class RsdHandshake { + public: + // Factory: consumes the ReadWrite socket regardless of result + static std::optional from_socket(ReadWrite&& rw, FfiError& err); + + // Basic info + std::optional protocol_version(FfiError& err) const; + std::optional uuid(FfiError& err) const; + + // Services + std::optional> services(FfiError& err) const; + std::optional service_available(const std::string& name, FfiError& err) const; + std::optional service_info(const std::string& name, FfiError& err) const; + + // RAII / moves + ~RsdHandshake() noexcept = default; + RsdHandshake(RsdHandshake&&) noexcept = default; + RsdHandshake& operator=(RsdHandshake&&) noexcept = default; + RsdHandshake(const RsdHandshake&) = delete; + RsdHandshake& operator=(const RsdHandshake&) = delete; + + RsdHandshakeHandle* raw() const noexcept { return handle_.get(); } + static RsdHandshake adopt(RsdHandshakeHandle* h) noexcept { return RsdHandshake(h); } + + private: + explicit RsdHandshake(RsdHandshakeHandle* h) noexcept : handle_(h) {} + RsdPtr handle_{}; +}; + +} // namespace IdeviceFFI +#endif diff --git a/cpp/src/adapter_stream.cpp b/cpp/src/adapter_stream.cpp new file mode 100644 index 0000000..2d782bf --- /dev/null +++ b/cpp/src/adapter_stream.cpp @@ -0,0 +1,43 @@ +// Jackson Coxson + +#include + +namespace IdeviceFFI { + +bool AdapterStream::close(FfiError& err) { + if (!h_) + return true; + if (IdeviceFfiError* e = ::adapter_close(h_)) { + err = FfiError(e); + return false; + } + h_ = nullptr; + return true; +} + +bool AdapterStream::send(const uint8_t* data, size_t len, FfiError& err) { + if (!h_) + return false; + if (IdeviceFfiError* e = ::adapter_send(h_, data, len)) { + err = FfiError(e); + return false; + } + return true; +} + +bool AdapterStream::recv(std::vector& out, FfiError& err, size_t max_hint) { + if (!h_) + return false; + if (max_hint == 0) + max_hint = 2048; + out.resize(max_hint); + size_t actual = 0; + if (IdeviceFfiError* e = ::adapter_recv(h_, out.data(), &actual, out.size())) { + err = FfiError(e); + return false; + } + out.resize(actual); + return true; +} + +} // namespace IdeviceFFI diff --git a/cpp/src/core_device.cpp b/cpp/src/core_device.cpp new file mode 100644 index 0000000..07e91e3 --- /dev/null +++ b/cpp/src/core_device.cpp @@ -0,0 +1,119 @@ +// Jackson Coxson + +#include + +namespace IdeviceFFI { + +// ---- Factories ---- + +std::optional CoreDeviceProxy::connect(Provider& provider, FfiError& err) { + CoreDeviceProxyHandle* out = nullptr; + if (IdeviceFfiError* e = ::core_device_proxy_connect(provider.raw(), &out)) { + err = FfiError(e); + return std::nullopt; + } + return CoreDeviceProxy::adopt(out); +} + +std::optional CoreDeviceProxy::from_socket(Idevice&& socket, FfiError& err) { + CoreDeviceProxyHandle* out = nullptr; + + // Rust consumes the socket regardless of result → release BEFORE call + IdeviceHandle* raw = socket.release(); + + if (IdeviceFfiError* e = ::core_device_proxy_new(raw, &out)) { + // socket is already consumed on error; do NOT touch it + err = FfiError(e); + return std::nullopt; + } + return CoreDeviceProxy::adopt(out); +} + +// ---- IO ---- + +bool CoreDeviceProxy::send(const uint8_t* data, size_t len, FfiError& err) { + if (IdeviceFfiError* e = ::core_device_proxy_send(handle_.get(), data, len)) { + err = FfiError(e); + return false; + } + return true; +} + +bool CoreDeviceProxy::recv(std::vector& out, FfiError& err) { + if (out.empty()) + out.resize(4096); // a reasonable default; caller can pre-size + size_t actual = 0; + if (IdeviceFfiError* e = + ::core_device_proxy_recv(handle_.get(), out.data(), &actual, out.size())) { + err = FfiError(e); + return false; + } + out.resize(actual); + return true; +} + +// ---- Handshake ---- + +std::optional CoreDeviceProxy::get_client_parameters(FfiError& err) const { + uint16_t mtu = 0; + char* addr_c = nullptr; + char* mask_c = nullptr; + + if (IdeviceFfiError* e = + ::core_device_proxy_get_client_parameters(handle_.get(), &mtu, &addr_c, &mask_c)) { + err = FfiError(e); + return std::nullopt; + } + + CoreClientParams params; + params.mtu = mtu; + if (addr_c) { + params.address = addr_c; + ::idevice_string_free(addr_c); + } + if (mask_c) { + params.netmask = mask_c; + ::idevice_string_free(mask_c); + } + return params; +} + +std::optional CoreDeviceProxy::get_server_address(FfiError& err) const { + char* addr_c = nullptr; + if (IdeviceFfiError* e = ::core_device_proxy_get_server_address(handle_.get(), &addr_c)) { + err = FfiError(e); + return std::nullopt; + } + std::string s; + if (addr_c) { + s = addr_c; + ::idevice_string_free(addr_c); + } + return s; +} + +std::optional CoreDeviceProxy::get_server_rsd_port(FfiError& err) const { + uint16_t port = 0; + if (IdeviceFfiError* e = ::core_device_proxy_get_server_rsd_port(handle_.get(), &port)) { + err = FfiError(e); + return std::nullopt; + } + return port; +} + +// ---- Adapter creation (consumes *this) ---- + +std::optional CoreDeviceProxy::create_tcp_adapter(FfiError& err) && { + AdapterHandle* out = nullptr; + + // Rust consumes the proxy regardless of result → release BEFORE call + CoreDeviceProxyHandle* raw = this->release(); + + if (IdeviceFfiError* e = ::core_device_proxy_create_tcp_adapter(raw, &out)) { + err = FfiError(e); + return std::nullopt; + } + return Adapter::adopt(out); +} + +} // namespace IdeviceFFI diff --git a/cpp/src/location_simulation.cpp b/cpp/src/location_simulation.cpp new file mode 100644 index 0000000..c53eef6 --- /dev/null +++ b/cpp/src/location_simulation.cpp @@ -0,0 +1,32 @@ +// Jackson Coxson + +#include + +namespace IdeviceFFI { + +std::optional LocationSimulation::create(RemoteServer& server, FfiError& err) { + LocationSimulationHandle* out = nullptr; + if (IdeviceFfiError* e = ::location_simulation_new(server.raw(), &out)) { + err = FfiError(e); + return std::nullopt; + } + return LocationSimulation::adopt(out); +} + +bool LocationSimulation::clear(FfiError& err) { + if (IdeviceFfiError* e = ::location_simulation_clear(handle_.get())) { + err = FfiError(e); + return false; + } + return true; +} + +bool LocationSimulation::set(double latitude, double longitude, FfiError& err) { + if (IdeviceFfiError* e = ::location_simulation_set(handle_.get(), latitude, longitude)) { + err = FfiError(e); + return false; + } + return true; +} + +} // namespace IdeviceFFI diff --git a/cpp/src/remote_server.cpp b/cpp/src/remote_server.cpp new file mode 100644 index 0000000..90e1b06 --- /dev/null +++ b/cpp/src/remote_server.cpp @@ -0,0 +1,30 @@ +// Jackson Coxson + +#include + +namespace IdeviceFFI { + +std::optional RemoteServer::from_socket(ReadWrite&& rw, FfiError& err) { + RemoteServerHandle* out = nullptr; + + // Rust consumes the stream regardless of result, release BEFORE the call + ReadWriteOpaque* raw = rw.release(); + + if (IdeviceFfiError* e = ::remote_server_new(raw, &out)) { + err = FfiError(e); + return std::nullopt; + } + return RemoteServer::adopt(out); +} + +std::optional +RemoteServer::connect_rsd(Adapter& adapter, RsdHandshake& rsd, FfiError& err) { + RemoteServerHandle* out = nullptr; + if (IdeviceFfiError* e = ::remote_server_connect_rsd(adapter.raw(), rsd.raw(), &out)) { + err = FfiError(e); + return std::nullopt; + } + return RemoteServer::adopt(out); +} + +} // namespace IdeviceFFI diff --git a/cpp/src/rsd.cpp b/cpp/src/rsd.cpp new file mode 100644 index 0000000..3575c6f --- /dev/null +++ b/cpp/src/rsd.cpp @@ -0,0 +1,129 @@ +// Jackson Coxson + +#include + +namespace IdeviceFFI { + +// ---------- helpers to copy/free CRsdService ---------- +static RsdService to_cpp_and_free(CRsdService* c) { + RsdService s; + if (c->name) + s.name = c->name; + if (c->entitlement) + s.entitlement = c->entitlement; + s.port = c->port; + s.uses_remote_xpc = c->uses_remote_xpc; + s.service_version = c->service_version; + + // features + if (c->features && c->features_count > 0) { + auto** arr = c->features; + s.features.reserve(c->features_count); + for (size_t i = 0; i < c->features_count; ++i) { + if (arr[i]) + s.features.emplace_back(arr[i]); + } + } + + // release the C allocation now that we've copied + rsd_free_service(c); + return s; +} + +static std::vector to_cpp_and_free(CRsdServiceArray* arr) { + std::vector out; + if (!arr || !arr->services || arr->count == 0) { + if (arr) + rsd_free_services(arr); + return out; + } + out.reserve(arr->count); + auto* begin = arr->services; + for (size_t i = 0; i < arr->count; ++i) { + out.emplace_back(RsdService{begin[i].name ? begin[i].name : "", + begin[i].entitlement ? begin[i].entitlement : "", + begin[i].port, + begin[i].uses_remote_xpc, + {}, // features, fill below + begin[i].service_version}); + // features for this service + if (begin[i].features && begin[i].features_count > 0) { + auto** feats = begin[i].features; + out.back().features.reserve(begin[i].features_count); + for (size_t j = 0; j < begin[i].features_count; ++j) { + if (feats[j]) + out.back().features.emplace_back(feats[j]); + } + } + } + // free the array + nested C strings now that we've copied + rsd_free_services(arr); + return out; +} + +// ---------- factory ---------- +std::optional RsdHandshake::from_socket(ReadWrite&& rw, FfiError& err) { + RsdHandshakeHandle* out = nullptr; + + // Rust consumes the socket regardless of result ⇒ release BEFORE call. + ReadWriteOpaque* raw = rw.release(); + + if (IdeviceFfiError* e = rsd_handshake_new(raw, &out)) { + err = FfiError(e); + return std::nullopt; + } + return RsdHandshake::adopt(out); +} + +// ---------- queries ---------- +std::optional RsdHandshake::protocol_version(FfiError& err) const { + size_t v = 0; + if (IdeviceFfiError* e = rsd_get_protocol_version(handle_.get(), &v)) { + err = FfiError(e); + return std::nullopt; + } + return v; +} + +std::optional RsdHandshake::uuid(FfiError& err) const { + char* c = nullptr; + if (IdeviceFfiError* e = rsd_get_uuid(handle_.get(), &c)) { + err = FfiError(e); + return std::nullopt; + } + std::string out; + if (c) { + out = c; + rsd_free_string(c); + } + return out; +} + +std::optional> RsdHandshake::services(FfiError& err) const { + CRsdServiceArray* arr = nullptr; + if (IdeviceFfiError* e = rsd_get_services(handle_.get(), &arr)) { + err = FfiError(e); + return std::nullopt; + } + return to_cpp_and_free(arr); +} + +std::optional RsdHandshake::service_available(const std::string& name, FfiError& err) const { + bool avail = false; + if (IdeviceFfiError* e = rsd_service_available(handle_.get(), name.c_str(), &avail)) { + err = FfiError(e); + return std::nullopt; + } + return avail; +} + +std::optional RsdHandshake::service_info(const std::string& name, FfiError& err) const { + CRsdService* svc = nullptr; + if (IdeviceFfiError* e = rsd_get_service_info(handle_.get(), name.c_str(), &svc)) { + err = FfiError(e); + return std::nullopt; + } + return to_cpp_and_free(svc); +} + +} // namespace IdeviceFFI From 2d0d3920607d383fabb5771ae477e519c634c478 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Fri, 15 Aug 2025 15:10:19 -0600 Subject: [PATCH 073/126] Remove broken include in ideviceinfo example --- cpp/examples/ideviceinfo.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/cpp/examples/ideviceinfo.cpp b/cpp/examples/ideviceinfo.cpp index d0078ca..ae9cfc4 100644 --- a/cpp/examples/ideviceinfo.cpp +++ b/cpp/examples/ideviceinfo.cpp @@ -1,6 +1,5 @@ // Jackson Coxson -#include "plist/Dictionary.h" #include #include #include From c9e81db7a89f9c7d73ecd81d7eb223458f17269d Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Fri, 15 Aug 2025 15:35:40 -0600 Subject: [PATCH 074/126] Include plist CPP dir --- cpp/examples/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/cpp/examples/CMakeLists.txt b/cpp/examples/CMakeLists.txt index ecc3e84..fe65446 100644 --- a/cpp/examples/CMakeLists.txt +++ b/cpp/examples/CMakeLists.txt @@ -45,6 +45,7 @@ add_library(idevice_cpp STATIC ${IDEVICE_CPP_SOURCES} ${PLIST_CPP_SOURCES}) target_include_directories(idevice_cpp PUBLIC ${IDEVICE_CPP_INCLUDE_DIR} + ${PLIST_CPP_INCLUDE_DIR} PRIVATE ${IDEVICE_FFI_INCLUDE_DIR} ${PLIST_CPP_INCLUDE_DIR} From 22e94e724f916a2866c57d4eb8d3ca01b587f551 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Fri, 15 Aug 2025 15:38:36 -0600 Subject: [PATCH 075/126] Bump plist_ffi submodule --- cpp/plist_ffi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/plist_ffi b/cpp/plist_ffi index 44b0453..66e6e63 160000 --- a/cpp/plist_ffi +++ b/cpp/plist_ffi @@ -1 +1 @@ -Subproject commit 44b04532705d9919678a02fb9a2f5f20a6713e11 +Subproject commit 66e6e6362b58bc361725c3e1b0a34af498aa2b3a From 94a361eb4e503accc8915c5270c02c6052c0b33e Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Fri, 15 Aug 2025 16:14:24 -0600 Subject: [PATCH 076/126] App service cpp example --- cpp/examples/CMakeLists.txt | 1 - cpp/examples/app_service.cpp | 221 ++++++++++++++++++++++++++ cpp/include/idevice++/app_service.hpp | 117 ++++++++++++++ cpp/src/app_service.cpp | 196 +++++++++++++++++++++++ ffi/src/core_device/app_service.rs | 6 +- tools/src/app_service.rs | 2 +- 6 files changed, 538 insertions(+), 5 deletions(-) create mode 100644 cpp/examples/app_service.cpp create mode 100644 cpp/include/idevice++/app_service.hpp create mode 100644 cpp/src/app_service.cpp diff --git a/cpp/examples/CMakeLists.txt b/cpp/examples/CMakeLists.txt index fe65446..f832b74 100644 --- a/cpp/examples/CMakeLists.txt +++ b/cpp/examples/CMakeLists.txt @@ -84,7 +84,6 @@ if (WIN32) ) endif() -# Optional: position independent code if you later turn idevice_cpp into a shared lib set_target_properties(idevice_cpp PROPERTIES POSITION_INDEPENDENT_CODE ON) # ---- Build each example and link against idevice_cpp ------------------------ diff --git a/cpp/examples/app_service.cpp b/cpp/examples/app_service.cpp new file mode 100644 index 0000000..6b5c861 --- /dev/null +++ b/cpp/examples/app_service.cpp @@ -0,0 +1,221 @@ +// Jackson Coxson + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +using namespace IdeviceFFI; + +static void die(const char* msg, const FfiError& e) { + std::cerr << msg << ": " << e.message << "\n"; + std::exit(1); +} + +static void usage(const char* argv0) { + std::cerr << "Usage:\n" + << " " << argv0 << " list\n" + << " " << argv0 << " launch \n" + << " " << argv0 << " processes\n" + << " " << argv0 << " uninstall \n" + << " " << argv0 << " signal \n" + << " " << argv0 << " icon [hw=1.0] [scale=1.0]\n"; +} + +int main(int argc, char** argv) { + if (argc < 2) { + usage(argv[0]); + return 2; + } + + std::string cmd = argv[1]; + + FfiError err; + + // 1) Connect to usbmuxd and pick first device + auto mux = UsbmuxdConnection::default_new(/*tag*/ 0, err); + if (!mux) + die("failed to connect to usbmuxd", err); + + auto devices = mux->get_devices(err); + if (!devices) + die("failed to list devices", err); + if (devices->empty()) { + std::cerr << "no devices connected\n"; + return 1; + } + auto& dev = (*devices)[0]; + + auto udid = dev.get_udid(); + if (!udid) { + std::cerr << "device has no UDID\n"; + return 1; + } + auto mux_id = dev.get_id(); + if (!mux_id) { + std::cerr << "device has no mux id\n"; + return 1; + } + + // 2) Provider via default usbmuxd addr + auto addr = UsbmuxdAddr::default_new(); + + const uint32_t tag = 0; + const std::string label = "app_service-jkcoxson"; + + auto provider = Provider::usbmuxd_new(std::move(addr), tag, *udid, *mux_id, label, err); + if (!provider) + die("failed to create provider", err); + + // 3) CoreDeviceProxy + auto cdp = CoreDeviceProxy::connect(*provider, err); + if (!cdp) + die("failed to connect CoreDeviceProxy", err); + + auto rsd_port = cdp->get_server_rsd_port(err); + if (!rsd_port) + die("failed to get server RSD port", err); + + // 4) Create software tunnel adapter (consumes proxy) + auto adapter = std::move(*cdp).create_tcp_adapter(err); + if (!adapter) + die("failed to create software tunnel adapter", err); + + // 5) Connect adapter to RSD → ReadWrite stream + auto stream = adapter->connect(*rsd_port, err); + if (!stream) + die("failed to connect RSD stream", err); + + // 6) RSD handshake (consumes stream) + auto rsd = RsdHandshake::from_socket(std::move(*stream), err); + if (!rsd) + die("failed RSD handshake", err); + + // 7) AppService over RSD (borrows adapter + handshake) + auto app = AppService::connect_rsd(*adapter, *rsd, err); + if (!app) + die("failed to connect AppService", err); + + // 8) Commands + if (cmd == "list") { + auto apps = app->list_apps(/*app_clips*/ true, + /*removable*/ true, + /*hidden*/ true, + /*internal*/ true, + /*default_apps*/ true, + err); + if (!apps) + die("list_apps failed", err); + + for (const auto& a : *apps) { + std::cout << "- " << a.bundle_identifier << " | name=" << a.name + << " | version=" << (a.version ? *a.version : std::string("")) + << " | dev=" << (a.is_developer_app ? "y" : "n") + << " | hidden=" << (a.is_hidden ? "y" : "n") << "\n"; + } + return 0; + + } else if (cmd == "launch") { + if (argc < 3) { + std::cerr << "No bundle ID passed\n"; + return 2; + } + std::string bundle_id = argv[2]; + + std::vector args; // empty in this example + auto resp = app->launch(bundle_id, + args, + /*kill_existing*/ false, + /*start_suspended*/ false, + err); + if (!resp) + die("launch failed", err); + + std::cout << "Launched pid=" << resp->pid << " exe=" << resp->executable_url + << " piv=" << resp->process_identifier_version + << " audit_token_len=" << resp->audit_token.size() << "\n"; + return 0; + + } else if (cmd == "processes") { + auto procs = app->list_processes(err); + if (!procs) + die("list_processes failed", err); + + for (const auto& p : *procs) { + std::cout << p.pid << " : " + << (p.executable_url ? *p.executable_url : std::string("")) << "\n"; + } + return 0; + + } else if (cmd == "uninstall") { + if (argc < 3) { + std::cerr << "No bundle ID passed\n"; + return 2; + } + std::string bundle_id = argv[2]; + + if (!app->uninstall(bundle_id, err)) + die("uninstall failed", err); + std::cout << "Uninstalled " << bundle_id << "\n"; + return 0; + + } else if (cmd == "signal") { + if (argc < 4) { + std::cerr << "Usage: signal \n"; + return 2; + } + uint32_t pid = static_cast(std::stoul(argv[2])); + uint32_t signal = static_cast(std::stoul(argv[3])); + + auto res = app->send_signal(pid, signal, err); + if (!res) + die("send_signal failed", err); + + std::cout << "Signaled pid=" << res->pid << " signal=" << res->signal + << " ts_ms=" << res->device_timestamp_ms + << " exe=" << (res->executable_url ? *res->executable_url : std::string("")) + << "\n"; + return 0; + + } else if (cmd == "icon") { + if (argc < 4) { + std::cerr << "Usage: icon [hw=1.0] [scale=1.0]\n"; + return 2; + } + std::string bundle_id = argv[2]; + std::string save_path = argv[3]; + float hw = (argc >= 5) ? std::stof(argv[4]) : 1.0f; + float scale = (argc >= 6) ? std::stof(argv[5]) : 1.0f; + + auto icon = app->fetch_icon(bundle_id, hw, hw, scale, /*allow_placeholder*/ true, err); + if (!icon) + die("fetch_app_icon failed", err); + + std::ofstream out(save_path, std::ios::binary); + if (!out) { + std::cerr << "Failed to open " << save_path << " for writing\n"; + return 1; + } + out.write(reinterpret_cast(icon->data.data()), + static_cast(icon->data.size())); + out.close(); + + std::cout << "Saved icon to " << save_path << " (" << icon->data.size() << " bytes, " + << icon->icon_width << "x" << icon->icon_height << ", min " << icon->minimum_width + << "x" << icon->minimum_height << ")\n"; + return 0; + + } else { + usage(argv[0]); + return 2; + } +} diff --git a/cpp/include/idevice++/app_service.hpp b/cpp/include/idevice++/app_service.hpp new file mode 100644 index 0000000..12c2de9 --- /dev/null +++ b/cpp/include/idevice++/app_service.hpp @@ -0,0 +1,117 @@ +// Jackson Coxson + +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace IdeviceFFI { + +using AppServicePtr = + std::unique_ptr>; + +struct AppInfo { + bool is_removable{}; + std::string name; + bool is_first_party{}; + std::string path; + std::string bundle_identifier; + bool is_developer_app{}; + std::optional bundle_version; + bool is_internal{}; + bool is_hidden{}; + bool is_app_clip{}; + std::optional version; +}; + +struct LaunchResponse { + uint32_t process_identifier_version{}; + uint32_t pid{}; + std::string executable_url; + std::vector audit_token; // raw words +}; + +struct ProcessToken { + uint32_t pid{}; + std::optional executable_url; +}; + +struct SignalResponse { + uint32_t pid{}; + std::optional executable_url; + uint64_t device_timestamp_ms{}; + uint32_t signal{}; +}; + +struct IconData { + std::vector data; + double icon_width{}; + double icon_height{}; + double minimum_width{}; + double minimum_height{}; +}; + +class AppService { + public: + // Factory: connect via RSD (borrows adapter & handshake) + static std::optional + connect_rsd(Adapter& adapter, RsdHandshake& rsd, FfiError& err); + + // Factory: from socket Box (consumes it). + // Only use if you actually obtain such a pointer from C. + static std::optional from_readwrite_ptr(ReadWriteOpaque* consumed, FfiError& err); + + // nice ergonomic overload: consume a C++ ReadWrite by releasing it + static std::optional from_readwrite(ReadWrite&& rw, FfiError& err); + + // API + std::optional> list_apps(bool app_clips, + bool removable, + bool hidden, + bool internal, + bool default_apps, + FfiError& err) const; + + std::optional launch(const std::string& bundle_id, + const std::vector& argv, + bool kill_existing, + bool start_suspended, + FfiError& err); + + std::optional> list_processes(FfiError& err) const; + + bool uninstall(const std::string& bundle_id, FfiError& err); + + std::optional send_signal(uint32_t pid, uint32_t signal, FfiError& err); + + std::optional fetch_icon(const std::string& bundle_id, + float width, + float height, + float scale, + bool allow_placeholder, + FfiError& err); + + // RAII / moves + ~AppService() noexcept = default; + AppService(AppService&&) noexcept = default; + AppService& operator=(AppService&&) noexcept = default; + AppService(const AppService&) = delete; + AppService& operator=(const AppService&) = delete; + + AppServiceHandle* raw() const noexcept { return handle_.get(); } + static AppService adopt(AppServiceHandle* h) noexcept { return AppService(h); } + + private: + explicit AppService(AppServiceHandle* h) noexcept : handle_(h) {} + AppServicePtr handle_{}; +}; + +} // namespace IdeviceFFI diff --git a/cpp/src/app_service.cpp b/cpp/src/app_service.cpp new file mode 100644 index 0000000..725c1d4 --- /dev/null +++ b/cpp/src/app_service.cpp @@ -0,0 +1,196 @@ +// Jackson Coxson + +#include + +namespace IdeviceFFI { + +// ---- Factories ---- +std::optional +AppService::connect_rsd(Adapter& adapter, RsdHandshake& rsd, FfiError& err) { + AppServiceHandle* out = nullptr; + if (IdeviceFfiError* e = ::app_service_connect_rsd(adapter.raw(), rsd.raw(), &out)) { + err = FfiError(e); + return std::nullopt; + } + return AppService::adopt(out); +} + +std::optional AppService::from_readwrite_ptr(ReadWriteOpaque* consumed, FfiError& err) { + AppServiceHandle* out = nullptr; + if (IdeviceFfiError* e = ::app_service_new(consumed, &out)) { + err = FfiError(e); + return std::nullopt; + } + return AppService::adopt(out); +} + +std::optional AppService::from_readwrite(ReadWrite&& rw, FfiError& err) { + // Rust consumes the stream regardless of result → release BEFORE call + return from_readwrite_ptr(rw.release(), err); +} + +// ---- Helpers to copy/free C arrays ---- +static std::vector copy_and_free_app_list(AppListEntryC* arr, size_t n) { + std::vector out; + out.reserve(n); + for (size_t i = 0; i < n; ++i) { + const auto& c = arr[i]; + AppInfo a; + a.is_removable = c.is_removable != 0; + if (c.name) + a.name = c.name; + a.is_first_party = c.is_first_party != 0; + if (c.path) + a.path = c.path; + if (c.bundle_identifier) + a.bundle_identifier = c.bundle_identifier; + a.is_developer_app = c.is_developer_app != 0; + if (c.bundle_version) + a.bundle_version = std::string(c.bundle_version); + a.is_internal = c.is_internal != 0; + a.is_hidden = c.is_hidden != 0; + a.is_app_clip = c.is_app_clip != 0; + if (c.version) + a.version = std::string(c.version); + out.emplace_back(std::move(a)); + } + ::app_service_free_app_list(arr, n); + return out; +} + +static std::vector copy_and_free_process_list(ProcessTokenC* arr, size_t n) { + std::vector out; + out.reserve(n); + for (size_t i = 0; i < n; ++i) { + ProcessToken p; + p.pid = arr[i].pid; + if (arr[i].executable_url) + p.executable_url = std::string(arr[i].executable_url); + out.emplace_back(std::move(p)); + } + ::app_service_free_process_list(arr, n); + return out; +} + +// ---- API impls ---- +std::optional> AppService::list_apps(bool app_clips, + bool removable, + bool hidden, + bool internal, + bool default_apps, + FfiError& err) const { + AppListEntryC* arr = nullptr; + size_t n = 0; + if (IdeviceFfiError* e = ::app_service_list_apps(handle_.get(), + app_clips ? 1 : 0, + removable ? 1 : 0, + hidden ? 1 : 0, + internal ? 1 : 0, + default_apps ? 1 : 0, + &arr, + &n)) { + err = FfiError(e); + return std::nullopt; + } + return copy_and_free_app_list(arr, n); +} + +std::optional AppService::launch(const std::string& bundle_id, + const std::vector& argv, + bool kill_existing, + bool start_suspended, + FfiError& err) { + std::vector c_argv; + c_argv.reserve(argv.size()); + for (auto& s : argv) + c_argv.push_back(s.c_str()); + + LaunchResponseC* resp = nullptr; + if (IdeviceFfiError* e = ::app_service_launch_app(handle_.get(), + bundle_id.c_str(), + c_argv.empty() ? nullptr : c_argv.data(), + c_argv.size(), + kill_existing ? 1 : 0, + start_suspended ? 1 : 0, + &resp)) { + err = FfiError(e); + return std::nullopt; + } + + LaunchResponse out; + out.process_identifier_version = resp->process_identifier_version; + out.pid = resp->pid; + if (resp->executable_url) + out.executable_url = resp->executable_url; + if (resp->audit_token && resp->audit_token_len > 0) { + out.audit_token.assign(resp->audit_token, resp->audit_token + resp->audit_token_len); + } + ::app_service_free_launch_response(resp); + return out; +} + +std::optional> AppService::list_processes(FfiError& err) const { + ProcessTokenC* arr = nullptr; + size_t n = 0; + if (IdeviceFfiError* e = ::app_service_list_processes(handle_.get(), &arr, &n)) { + err = FfiError(e); + return std::nullopt; + } + return copy_and_free_process_list(arr, n); +} + +bool AppService::uninstall(const std::string& bundle_id, FfiError& err) { + if (IdeviceFfiError* e = ::app_service_uninstall_app(handle_.get(), bundle_id.c_str())) { + err = FfiError(e); + return false; + } + return true; +} + +std::optional +AppService::send_signal(uint32_t pid, uint32_t signal, FfiError& err) { + SignalResponseC* c = nullptr; + if (IdeviceFfiError* e = ::app_service_send_signal(handle_.get(), pid, signal, &c)) { + err = FfiError(e); + return std::nullopt; + } + SignalResponse out; + out.pid = c->pid; + if (c->executable_url) + out.executable_url = std::string(c->executable_url); + out.device_timestamp_ms = c->device_timestamp; + out.signal = c->signal; + ::app_service_free_signal_response(c); + return out; +} + +std::optional AppService::fetch_icon(const std::string& bundle_id, + float width, + float height, + float scale, + bool allow_placeholder, + FfiError& err) { + IconDataC* c = nullptr; + if (IdeviceFfiError* e = ::app_service_fetch_app_icon(handle_.get(), + bundle_id.c_str(), + width, + height, + scale, + allow_placeholder ? 1 : 0, + &c)) { + err = FfiError(e); + return std::nullopt; + } + IconData out; + if (c->data && c->data_len) { + out.data.assign(c->data, c->data + c->data_len); + } + out.icon_width = c->icon_width; + out.icon_height = c->icon_height; + out.minimum_width = c->minimum_width; + out.minimum_height = c->minimum_height; + ::app_service_free_icon_data(c); + return out; +} + +} // namespace IdeviceFFI diff --git a/ffi/src/core_device/app_service.rs b/ffi/src/core_device/app_service.rs index 908bd26..c8f0616 100644 --- a/ffi/src/core_device/app_service.rs +++ b/ffi/src/core_device/app_service.rs @@ -9,7 +9,7 @@ use idevice::{IdeviceError, ReadWrite, RsdService}; use crate::core_device_proxy::AdapterHandle; use crate::rsd::RsdHandshakeHandle; -use crate::{IdeviceFfiError, RUNTIME, ffi_err}; +use crate::{IdeviceFfiError, RUNTIME, ReadWriteOpaque, ffi_err}; /// Opaque handle to an AppServiceClient pub struct AppServiceHandle(pub AppServiceClient>); @@ -122,7 +122,7 @@ pub unsafe extern "C" fn app_service_connect_rsd( /// `handle` must be a valid pointer to a location where the handle will be stored #[unsafe(no_mangle)] pub unsafe extern "C" fn app_service_new( - socket: *mut Box, + socket: *mut ReadWriteOpaque, handle: *mut *mut AppServiceHandle, ) -> *mut IdeviceFfiError { if socket.is_null() || handle.is_null() { @@ -130,7 +130,7 @@ pub unsafe extern "C" fn app_service_new( } let socket = unsafe { Box::from_raw(socket) }; - let res = RUNTIME.block_on(async move { AppServiceClient::new(*socket).await }); + let res = RUNTIME.block_on(async move { AppServiceClient::new(socket.inner.unwrap()).await }); match res { Ok(client) => { diff --git a/tools/src/app_service.rs b/tools/src/app_service.rs index 9751e28..9951666 100644 --- a/tools/src/app_service.rs +++ b/tools/src/app_service.rs @@ -97,7 +97,7 @@ async fn main() { let host = matches.get_one::("host"); let provider = - match common::get_provider(udid, host, pairing_file, "debug-proxy-jkcoxson").await { + match common::get_provider(udid, host, pairing_file, "app_service-jkcoxson").await { Ok(p) => p, Err(e) => { eprintln!("{e}"); From 46635e162a0cb10e3257b4c994c2002eec3e57f8 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Fri, 15 Aug 2025 16:38:56 -0600 Subject: [PATCH 077/126] Debug proxy cpp example --- cpp/examples/debug_proxy.cpp | 133 ++++++++++++++++++++++++ cpp/include/idevice++/app_service.hpp | 1 - cpp/include/idevice++/debug_proxy.hpp | 120 ++++++++++++++++++++++ cpp/src/debug_proxy.cpp | 141 ++++++++++++++++++++++++++ cpp/src/lockdown.cpp | 1 - cpp/src/usbmuxd.cpp | 2 +- ffi/src/debug_proxy.rs | 6 +- 7 files changed, 398 insertions(+), 6 deletions(-) create mode 100644 cpp/examples/debug_proxy.cpp create mode 100644 cpp/include/idevice++/debug_proxy.hpp create mode 100644 cpp/src/debug_proxy.cpp diff --git a/cpp/examples/debug_proxy.cpp b/cpp/examples/debug_proxy.cpp new file mode 100644 index 0000000..55a24c4 --- /dev/null +++ b/cpp/examples/debug_proxy.cpp @@ -0,0 +1,133 @@ +// Jackson Coxson + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +static void die(const char* msg, const IdeviceFFI::FfiError& e) { + std::cerr << msg << ": " << e.message << "\n"; + std::exit(1); +} + +static std::vector split_args(const std::string& line) { + std::istringstream iss(line); + std::vector toks; + std::string tok; + while (iss >> tok) + toks.push_back(tok); + return toks; +} + +int main() { + IdeviceFFI::FfiError err; + + // 1) usbmuxd → pick first device + auto mux = IdeviceFFI::UsbmuxdConnection::default_new(/*tag*/ 0, err); + if (!mux) + die("failed to connect to usbmuxd", err); + + auto devices = mux->get_devices(err); + if (!devices) + die("failed to list devices", err); + if (devices->empty()) { + std::cerr << "no devices connected\n"; + return 1; + } + + auto& dev = (*devices)[0]; + auto udid = dev.get_udid(); + if (!udid) { + std::cerr << "device has no UDID\n"; + return 1; + } + auto mux_id = dev.get_id(); + if (!mux_id) { + std::cerr << "device has no mux id\n"; + return 1; + } + + // 2) Provider via default usbmuxd addr + auto addr = IdeviceFFI::UsbmuxdAddr::default_new(); + + const uint32_t tag = 0; + const std::string label = "debug-proxy-jkcoxson"; + auto provider = + IdeviceFFI::Provider::usbmuxd_new(std::move(addr), tag, *udid, *mux_id, label, err); + if (!provider) + die("failed to create provider", err); + + // 3) CoreDeviceProxy + auto cdp = IdeviceFFI::CoreDeviceProxy::connect(*provider, err); + if (!cdp) + die("failed CoreDeviceProxy connect", err); + + auto rsd_port = cdp->get_server_rsd_port(err); + if (!rsd_port) + die("failed to get RSD port", err); + + // 4) Software tunnel → stream + auto adapter = std::move(*cdp).create_tcp_adapter(err); + if (!adapter) + die("failed to create software tunnel adapter", err); + + auto stream = adapter->connect(*rsd_port, err); + if (!stream) + die("failed to connect RSD stream", err); + + // 5) RSD handshake + auto rsd = IdeviceFFI::RsdHandshake::from_socket(std::move(*stream), err); + if (!rsd) + die("failed RSD handshake", err); + + // 6) DebugProxy over RSD + auto dbg = IdeviceFFI::DebugProxy::connect_rsd(*adapter, *rsd, err); + if (!dbg) + die("failed to connect DebugProxy", err); + + std::cout << "Shell connected! Type 'exit' to quit.\n"; + for (;;) { + std::cout << "> " << std::flush; + + std::string line; + if (!std::getline(std::cin, line)) + break; + // trim + auto first = line.find_first_not_of(" \t\r\n"); + if (first == std::string::npos) + continue; + auto last = line.find_last_not_of(" \t\r\n"); + line = line.substr(first, last - first + 1); + + if (line == "exit") + break; + + // Interpret: first token = command name, rest = argv + auto toks = split_args(line); + if (toks.empty()) + continue; + + std::string name = toks.front(); + std::vector argv(toks.begin() + 1, toks.end()); + + auto res = dbg->send_command(name, argv, err); + if (!res && err) { + std::cerr << "send_command failed: " << err.message << "\n"; + // clear error for next loop + err = IdeviceFFI::FfiError{}; + continue; + } + if (res && !res->empty()) { + std::cout << *res << "\n"; + } + } + + return 0; +} diff --git a/cpp/include/idevice++/app_service.hpp b/cpp/include/idevice++/app_service.hpp index 12c2de9..aada1a6 100644 --- a/cpp/include/idevice++/app_service.hpp +++ b/cpp/include/idevice++/app_service.hpp @@ -66,7 +66,6 @@ class AppService { connect_rsd(Adapter& adapter, RsdHandshake& rsd, FfiError& err); // Factory: from socket Box (consumes it). - // Only use if you actually obtain such a pointer from C. static std::optional from_readwrite_ptr(ReadWriteOpaque* consumed, FfiError& err); // nice ergonomic overload: consume a C++ ReadWrite by releasing it diff --git a/cpp/include/idevice++/debug_proxy.hpp b/cpp/include/idevice++/debug_proxy.hpp new file mode 100644 index 0000000..cafd386 --- /dev/null +++ b/cpp/include/idevice++/debug_proxy.hpp @@ -0,0 +1,120 @@ +// Jackson Coxson + +#pragma once +#include +#include +#include +#include +#include +#include + +// Bring in the global C ABI (all C structs/functions are global) +#include +#include +#include + +namespace IdeviceFFI { + +class DebugProxy { + public: + DebugProxy() = default; + DebugProxy(const DebugProxy&) = delete; + DebugProxy& operator=(const DebugProxy&) = delete; + + DebugProxy(DebugProxy&& other) noexcept : handle_(other.handle_) { other.handle_ = nullptr; } + DebugProxy& operator=(DebugProxy&& other) noexcept { + if (this != &other) { + reset(); + handle_ = other.handle_; + other.handle_ = nullptr; + } + return *this; + } + + ~DebugProxy() { reset(); } + + // Factory: connect over RSD (borrows adapter & handshake; does not consume them) + static std::optional + connect_rsd(Adapter& adapter, RsdHandshake& rsd, FfiError& err); + + // Factory: consume a ReadWrite stream (fat pointer) + static std::optional from_readwrite_ptr(::ReadWriteOpaque* consumed, FfiError& err); + + // Convenience: consume a C++ ReadWrite wrapper by releasing it into the ABI + static std::optional from_readwrite(ReadWrite&& rw, FfiError& err); + + // API + std::optional + send_command(const std::string& name, const std::vector& argv, FfiError& err); + + std::optional read_response(FfiError& err); + + bool send_raw(const std::vector& data, FfiError& err); + + // Reads up to `len` bytes; ABI returns a heap C string (we treat as bytes → string) + std::optional read(std::size_t len, FfiError& err); + + // Sets argv, returns textual reply (OK/echo/etc) + std::optional set_argv(const std::vector& argv, FfiError& err); + + bool send_ack(FfiError& err); + bool send_nack(FfiError& err); + + // No error object in ABI; immediate effect + void set_ack_mode(bool enabled) { ::debug_proxy_set_ack_mode(handle_, enabled ? 1 : 0); } + + ::DebugProxyHandle* raw() const { return handle_; } + + private: + explicit DebugProxy(::DebugProxyHandle* h) : handle_(h) {} + + void reset() { + if (handle_) { + ::debug_proxy_free(handle_); + handle_ = nullptr; + } + } + + ::DebugProxyHandle* handle_ = nullptr; +}; + +// Small helper that owns a DebugserverCommandHandle +class DebugCommand { + public: + DebugCommand() = default; + DebugCommand(const DebugCommand&) = delete; + DebugCommand& operator=(const DebugCommand&) = delete; + + DebugCommand(DebugCommand&& other) noexcept : handle_(other.handle_) { + other.handle_ = nullptr; + } + DebugCommand& operator=(DebugCommand&& other) noexcept { + if (this != &other) { + reset(); + handle_ = other.handle_; + other.handle_ = nullptr; + } + return *this; + } + + ~DebugCommand() { reset(); } + + static std::optional make(const std::string& name, + const std::vector& argv); + + ::DebugserverCommandHandle* raw() const { return handle_; } + + private: + explicit DebugCommand(::DebugserverCommandHandle* h) : handle_(h) {} + + void reset() { + if (handle_) { + ::debugserver_command_free(handle_); + handle_ = nullptr; + } + } + + ::DebugserverCommandHandle* handle_ = nullptr; +}; + +} // namespace IdeviceFFI diff --git a/cpp/src/debug_proxy.cpp b/cpp/src/debug_proxy.cpp new file mode 100644 index 0000000..f3143cb --- /dev/null +++ b/cpp/src/debug_proxy.cpp @@ -0,0 +1,141 @@ +// Jackson Coxson + +#include +#include + +namespace IdeviceFFI { + +// ---- helpers ---- +static std::optional take_cstring(char* p) { + if (!p) + return std::nullopt; + std::string s(p); + ::idevice_string_free(p); + return s; +} + +// ---- DebugCommand ---- +std::optional DebugCommand::make(const std::string& name, + const std::vector& argv) { + std::vector c_argv; + c_argv.reserve(argv.size()); + for (auto& a : argv) + c_argv.push_back(a.c_str()); + + auto* h = ::debugserver_command_new( + name.c_str(), + c_argv.empty() ? nullptr : const_cast(c_argv.data()), + c_argv.size()); + if (!h) + return std::nullopt; + return DebugCommand(h); +} + +// ---- DebugProxy factories ---- +std::optional +DebugProxy::connect_rsd(Adapter& adapter, RsdHandshake& rsd, FfiError& err) { + ::DebugProxyHandle* out = nullptr; + if (IdeviceFfiError* e = ::debug_proxy_connect_rsd(adapter.raw(), rsd.raw(), &out)) { + err = FfiError(e); + return std::nullopt; + } + return DebugProxy(out); +} + +std::optional DebugProxy::from_readwrite_ptr(::ReadWriteOpaque* consumed, + FfiError& err) { + ::DebugProxyHandle* out = nullptr; + if (IdeviceFfiError* e = ::debug_proxy_new(consumed, &out)) { + err = FfiError(e); + return std::nullopt; + } + return DebugProxy(out); +} + +std::optional DebugProxy::from_readwrite(ReadWrite&& rw, FfiError& err) { + // Rust consumes the pointer regardless of outcome; release before calling + return from_readwrite_ptr(rw.release(), err); +} + +// ---- DebugProxy API ---- +std::optional DebugProxy::send_command(const std::string& name, + const std::vector& argv, + FfiError& err) { + auto cmd = DebugCommand::make(name, argv); + if (!cmd) { + // treat as invalid arg + err.code = -1; + err.message = "debugserver_command_new failed"; + return std::nullopt; + } + + char* resp_c = nullptr; + if (IdeviceFfiError* e = ::debug_proxy_send_command(handle_, cmd->raw(), &resp_c)) { + err = FfiError(e); + return std::nullopt; + } + return take_cstring(resp_c); // may be null → std::nullopt +} + +std::optional DebugProxy::read_response(FfiError& err) { + char* resp_c = nullptr; + if (IdeviceFfiError* e = ::debug_proxy_read_response(handle_, &resp_c)) { + err = FfiError(e); + return std::nullopt; + } + return take_cstring(resp_c); +} + +bool DebugProxy::send_raw(const std::vector& data, FfiError& err) { + if (IdeviceFfiError* e = ::debug_proxy_send_raw(handle_, data.data(), data.size())) { + err = FfiError(e); + return false; + } + return true; +} + +std::optional DebugProxy::read(std::size_t len, FfiError& err) { + char* resp_c = nullptr; + if (IdeviceFfiError* e = ::debug_proxy_read(handle_, len, &resp_c)) { + err = FfiError(e); + return std::nullopt; + } + return take_cstring(resp_c); +} + +std::optional DebugProxy::set_argv(const std::vector& argv, + FfiError& err) { + std::vector c_argv; + c_argv.reserve(argv.size()); + for (auto& a : argv) + c_argv.push_back(a.c_str()); + + char* resp_c = nullptr; + if (IdeviceFfiError* e = ::debug_proxy_set_argv( + handle_, + c_argv.empty() ? nullptr : const_cast(c_argv.data()), + c_argv.size(), + &resp_c)) { + err = FfiError(e); + return std::nullopt; + } + return take_cstring(resp_c); +} + +bool DebugProxy::send_ack(FfiError& err) { + if (IdeviceFfiError* e = ::debug_proxy_send_ack(handle_)) { + err = FfiError(e); + return false; + } + return true; +} + +bool DebugProxy::send_nack(FfiError& err) { + if (IdeviceFfiError* e = ::debug_proxy_send_nack(handle_)) { + err = FfiError(e); + return false; + } + return true; +} + +} // namespace IdeviceFFI diff --git a/cpp/src/lockdown.cpp b/cpp/src/lockdown.cpp index e00659a..107439e 100644 --- a/cpp/src/lockdown.cpp +++ b/cpp/src/lockdown.cpp @@ -12,7 +12,6 @@ std::optional Lockdown::connect(Provider& provider, FfiError& err) { if (IdeviceFfiError* e = ::lockdownd_connect(provider.raw(), &out)) { // Rust freed the provider on error -> abandon our ownership to avoid double free. - // Your Provider wrapper should expose release(). provider.release(); err = FfiError(e); return std::nullopt; diff --git a/cpp/src/usbmuxd.cpp b/cpp/src/usbmuxd.cpp index 45bdb08..1ae2bba 100644 --- a/cpp/src/usbmuxd.cpp +++ b/cpp/src/usbmuxd.cpp @@ -67,7 +67,7 @@ std::optional UsbmuxdDevice::get_id() const { std::optional UsbmuxdDevice::get_connection_type() const { uint8_t t = idevice_usbmuxd_device_get_connection_type(handle_.get()); if (t == 0) - return std::nullopt; // adjust to your API contract + return std::nullopt; return UsbmuxdConnectionType(t); } diff --git a/ffi/src/debug_proxy.rs b/ffi/src/debug_proxy.rs index 3bf7eef..8d7152a 100644 --- a/ffi/src/debug_proxy.rs +++ b/ffi/src/debug_proxy.rs @@ -9,7 +9,7 @@ use idevice::{IdeviceError, ReadWrite, RsdService}; use crate::core_device_proxy::AdapterHandle; use crate::rsd::RsdHandshakeHandle; -use crate::{IdeviceFfiError, RUNTIME, ffi_err}; +use crate::{IdeviceFfiError, RUNTIME, ReadWriteOpaque, ffi_err}; /// Opaque handle to a DebugProxyClient pub struct DebugProxyHandle(pub DebugProxyClient>); @@ -170,7 +170,7 @@ pub unsafe extern "C" fn debug_proxy_connect_rsd( /// `handle` must be a valid pointer to a location where the handle will be stored #[unsafe(no_mangle)] pub unsafe extern "C" fn debug_proxy_new( - socket: *mut Box, + socket: *mut ReadWriteOpaque, handle: *mut *mut DebugProxyHandle, ) -> *mut IdeviceFfiError { if socket.is_null() || handle.is_null() { @@ -178,7 +178,7 @@ pub unsafe extern "C" fn debug_proxy_new( } let socket = unsafe { Box::from_raw(socket) }; - let client = DebugProxyClient::new(*socket); + let client = DebugProxyClient::new(socket.inner.unwrap()); let new_handle = DebugProxyHandle(client); unsafe { *handle = Box::into_raw(Box::new(new_handle)) }; From 2d528ae21c1049cd4c34f57c2b0fb7cdfe0645e8 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Fri, 15 Aug 2025 20:12:32 -0600 Subject: [PATCH 078/126] Respond to TCP keep-alive probes --- idevice/src/tcp/adapter.rs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/idevice/src/tcp/adapter.rs b/idevice/src/tcp/adapter.rs index 714f5f3..41d399b 100644 --- a/idevice/src/tcp/adapter.rs +++ b/idevice/src/tcp/adapter.rs @@ -62,7 +62,7 @@ use std::{collections::HashMap, io::ErrorKind, net::IpAddr, path::Path, sync::Arc}; -use log::trace; +use log::{debug, trace, warn}; use tokio::{io::AsyncWriteExt, sync::Mutex}; use crate::ReadWrite; @@ -542,6 +542,22 @@ impl Adapter { let ip_packet = self.read_ip_packet().await?; let res = TcpPacket::parse(&ip_packet)?; let mut ack_me = None; + + if let Some(state) = self.states.get(&res.destination_port) { + // A keep-alive probe: ACK set, no payload, and seq == RCV.NXT - 1 + let is_keepalive = res.flags.ack + && res.payload.is_empty() + && res.sequence_number.wrapping_add(1) == state.ack; + + if is_keepalive { + // Don't update any seq/ack state; just ACK what we already expect. + debug!("responding to keep-alive probe"); + let port = res.destination_port; + self.ack(port).await?; + break; + } + } + if let Some(state) = self.states.get_mut(&res.destination_port) { if state.peer_seq > res.sequence_number { // ignore retransmission @@ -560,6 +576,7 @@ impl Adapter { state.read_buffer.extend(res.payload); } if res.flags.rst { + warn!("stream rst"); state.status = ConnectionStatus::Error(ErrorKind::ConnectionReset); } if res.flags.fin { From e881d6ef073e8ea8b11d7c804b9cc475e8c9170f Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Fri, 15 Aug 2025 20:13:03 -0600 Subject: [PATCH 079/126] Implement file stream XPC messages --- idevice/src/xpc/format.rs | 32 +++++++++++++++ idevice/src/xpc/http2/mod.rs | 8 +++- idevice/src/xpc/mod.rs | 76 ++++++++++++++++++++++++++++++++++++ 3 files changed, 114 insertions(+), 2 deletions(-) diff --git a/idevice/src/xpc/format.rs b/idevice/src/xpc/format.rs index 2bfbc6a..ccfd6cb 100644 --- a/idevice/src/xpc/format.rs +++ b/idevice/src/xpc/format.rs @@ -18,6 +18,9 @@ pub enum XPCFlag { WantingReply, InitHandshake, + FileTxStreamRequest, + FileTxStreamResponse, + Custom(u32), } @@ -28,6 +31,8 @@ impl From for u32 { XPCFlag::DataFlag => 0x00000100, XPCFlag::WantingReply => 0x00010000, XPCFlag::InitHandshake => 0x00400000, + XPCFlag::FileTxStreamRequest => 0x00100000, + XPCFlag::FileTxStreamResponse => 0x00200000, XPCFlag::Custom(inner) => inner, } } @@ -68,6 +73,7 @@ pub enum XPCType { String = 0x00009000, Data = 0x00008000, Uuid = 0x0000a000, + FileTransfer = 0x0001a000, } impl TryFrom for XPCType { @@ -85,6 +91,7 @@ impl TryFrom for XPCType { 0x00009000 => Ok(Self::String), 0x00008000 => Ok(Self::Data), 0x0000a000 => Ok(Self::Uuid), + 0x0001a000 => Ok(Self::FileTransfer), _ => Err(IdeviceError::UnknownXpcType(value))?, } } @@ -107,6 +114,8 @@ pub enum XPCObject { String(String), Data(Vec), Uuid(uuid::Uuid), + + FileTransfer { msg_id: u64, data: Box }, } impl From for XPCObject { @@ -153,6 +162,12 @@ impl XPCObject { } plist::Value::Dictionary(dict) } + Self::FileTransfer { msg_id, data } => { + crate::plist!({ + "msg_id": *msg_id, + "data": data.to_plist(), + }) + } } } @@ -240,6 +255,11 @@ impl XPCObject { buf.extend_from_slice(&16_u32.to_le_bytes()); buf.extend_from_slice(uuid.as_bytes()); } + XPCObject::FileTransfer { msg_id, data } => { + buf.extend_from_slice(&(XPCType::FileTransfer as u32).to_le_bytes()); + buf.extend_from_slice(&msg_id.to_le_bytes()); + data.encode_object(buf)?; + } } Ok(()) } @@ -370,6 +390,18 @@ impl XPCObject { cursor.read_exact(&mut data)?; Ok(XPCObject::Uuid(uuid::Builder::from_bytes(data).into_uuid())) } + XPCType::FileTransfer => { + let mut id_buf = [0u8; 8]; + cursor.read_exact(&mut id_buf)?; + let msg_id = u64::from_le_bytes(id_buf); + + // The next thing in the stream is a full XPC object + let inner = Self::decode_object(cursor)?; + Ok(XPCObject::FileTransfer { + msg_id, + data: Box::new(inner), + }) + } } } diff --git a/idevice/src/xpc/http2/mod.rs b/idevice/src/xpc/http2/mod.rs index b6ff0b1..88ce037 100644 --- a/idevice/src/xpc/http2/mod.rs +++ b/idevice/src/xpc/http2/mod.rs @@ -60,7 +60,8 @@ impl Http2Client { } pub async fn open_stream(&mut self, stream_id: u32) -> Result<(), IdeviceError> { - self.cache.insert(stream_id, VecDeque::new()); + // Sometimes Apple is silly and sends data to a stream that isn't open + self.cache.entry(stream_id).or_default(); let frame = frame::HeadersFrame { stream_id }.serialize(); self.inner.write_all(&frame).await?; self.inner.flush().await?; @@ -124,11 +125,14 @@ impl Http2Client { let c = match self.cache.get_mut(&data_frame.stream_id) { Some(c) => c, None => { + // Sometimes Apple is a little silly and sends data before the + // stream is open. warn!( "Received message for stream ID {} not in cache", data_frame.stream_id ); - continue; + self.cache.insert(data_frame.stream_id, VecDeque::new()); + self.cache.get_mut(&data_frame.stream_id).unwrap() } }; c.push_back(data_frame.payload); diff --git a/idevice/src/xpc/mod.rs b/idevice/src/xpc/mod.rs index c6fc217..3b580dc 100644 --- a/idevice/src/xpc/mod.rs +++ b/idevice/src/xpc/mod.rs @@ -1,5 +1,7 @@ // Jackson Coxson +use async_stream::try_stream; +use futures::Stream; use http2::Setting; use log::debug; @@ -133,4 +135,78 @@ impl RemoteXpcClient { .await?; Ok(()) } + + pub fn iter_file_chunks<'a>( + &'a mut self, + total_size: usize, + file_idx: u32, + ) -> impl Stream, IdeviceError>> + 'a { + let stream_id = (file_idx + 1) * 2; + + try_stream! { + fn strip_xpc_wrapper_prefix(buf: &[u8]) -> (&[u8], bool) { + // Returns (data_after_wrapper, stripped_anything) + const MAGIC: u32 = 0x29b00b92; + + if buf.len() < 24 { + return (buf, false); + } + + let magic = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]); + if magic != MAGIC { + return (buf, false); + } + + // flags at [4..8] – not needed to compute size + let body_len = u64::from_le_bytes([ + buf[8], buf[9], buf[10], buf[11], buf[12], buf[13], buf[14], buf[15], + ]) as usize; + + let wrapper_len = 24 + body_len; + if buf.len() < wrapper_len { + // Incomplete wrapper (shouldn’t happen with your read API), keep as-is. + return (buf, false); + } + + (&buf[wrapper_len..], true) + } + self.open_file_stream_for_response(stream_id).await?; + + let mut got = 0usize; + while got < total_size { + let bytes = self.h2_client.read(stream_id).await?; + let (after, stripped) = strip_xpc_wrapper_prefix(&bytes); + if stripped && after.is_empty() { + continue; // pure control wrapper, don't count + } + + let data = if stripped { after.to_vec() } else { bytes }; + + if data.is_empty() { + continue; + } + + got += data.len(); + yield data; + } + } + } + + pub async fn open_file_stream_for_response( + &mut self, + stream_id: u32, + ) -> Result<(), IdeviceError> { + // 1) Open the HTTP/2 stream + self.h2_client.open_stream(stream_id).await?; + + // 2) Send an empty XPC wrapper on that same stream with FILE_TX_STREAM_RESPONSE + let flags = XPCFlag::AlwaysSet | XPCFlag::FileTxStreamResponse; + + let msg = XPCMessage::new(Some(flags), None, Some(0)); + + // IMPORTANT: send on `stream_id`, not ROOT/REPLY + let bytes = msg.encode(0)?; + self.h2_client.send(bytes, stream_id).await?; + Ok(()) + } } From ef7811b3a69c9f9a3a0b0e17b8cb9cc4a4b73907 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Fri, 15 Aug 2025 20:19:37 -0600 Subject: [PATCH 080/126] Implement diagnosticsservice --- Cargo.lock | 24 ++++ idevice/Cargo.toml | 3 +- .../core_device/diagnosticsservice.rs | 72 ++++++++++++ idevice/src/services/core_device/mod.rs | 2 + tools/Cargo.toml | 5 + tools/src/diagnosticsservice.rs | 106 ++++++++++++++++++ 6 files changed, 211 insertions(+), 1 deletion(-) create mode 100644 idevice/src/services/core_device/diagnosticsservice.rs create mode 100644 tools/src/diagnosticsservice.rs diff --git a/Cargo.lock b/Cargo.lock index 3176418..c82aeb9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -109,6 +109,28 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "async-task" version = "4.7.1" @@ -1112,6 +1134,7 @@ dependencies = [ name = "idevice" version = "0.1.38" dependencies = [ + "async-stream", "base64", "byteorder", "bytes", @@ -1163,6 +1186,7 @@ version = "0.1.0" dependencies = [ "clap", "env_logger", + "futures-util", "idevice", "log", "ns-keyed-archive", diff --git a/idevice/Cargo.toml b/idevice/Cargo.toml index 1102aca..86836b1 100644 --- a/idevice/Cargo.toml +++ b/idevice/Cargo.toml @@ -42,6 +42,7 @@ reqwest = { version = "0.12", features = [ ], optional = true, default-features = false } rand = { version = "0.9", optional = true } futures = { version = "0.3", optional = true } +async-stream = { version = "0.3.6", optional = true } sha2 = { version = "0.10", optional = true, features = ["oid"] } @@ -96,7 +97,7 @@ tunnel_tcp_stack = [ tss = ["dep:uuid", "dep:reqwest"] tunneld = ["dep:serde_json", "dep:json", "dep:reqwest"] usbmuxd = ["tokio/net"] -xpc = ["dep:indexmap", "dep:uuid"] +xpc = ["dep:indexmap", "dep:uuid", "dep:async-stream"] full = [ "afc", "amfi", diff --git a/idevice/src/services/core_device/diagnosticsservice.rs b/idevice/src/services/core_device/diagnosticsservice.rs new file mode 100644 index 0000000..084a390 --- /dev/null +++ b/idevice/src/services/core_device/diagnosticsservice.rs @@ -0,0 +1,72 @@ +// Jackson Coxson + +use std::pin::Pin; + +use futures::Stream; +use log::warn; + +use crate::{IdeviceError, ReadWrite, RsdService, obf}; + +impl RsdService for DiagnostisServiceClient> { + fn rsd_service_name() -> std::borrow::Cow<'static, str> { + obf!("com.apple.coredevice.diagnosticsservice") + } + + async fn from_stream(stream: Box) -> Result { + Ok(Self { + inner: super::CoreDeviceServiceClient::new(stream).await?, + }) + } +} + +pub struct DiagnostisServiceClient { + inner: super::CoreDeviceServiceClient, +} + +pub struct SysdiagnoseResponse<'a> { + pub preferred_filename: String, + pub stream: Pin, IdeviceError>> + 'a>>, + pub expected_length: usize, +} + +impl DiagnostisServiceClient { + pub async fn capture_sysdiagnose<'a>( + &'a mut self, + dry_run: bool, + ) -> Result, IdeviceError> { + let req = crate::plist!({ + "options": { + "collectFullLogs": true + }, + "isDryRun": dry_run + }) + .into_dictionary() + .unwrap(); + + let res = self + .inner + .invoke("com.apple.coredevice.feature.capturesysdiagnose", Some(req)) + .await?; + + if let Some(len) = res + .as_dictionary() + .and_then(|x| x.get("fileTransfer")) + .and_then(|x| x.as_dictionary()) + .and_then(|x| x.get("expectedLength")) + .and_then(|x| x.as_unsigned_integer()) + && let Some(name) = res + .as_dictionary() + .and_then(|x| x.get("preferredFilename")) + .and_then(|x| x.as_string()) + { + Ok(SysdiagnoseResponse { + stream: Box::pin(self.inner.inner.iter_file_chunks(len as usize, 0)), + preferred_filename: name.to_string(), + expected_length: len as usize, + }) + } else { + warn!("Did not get expected responses from RemoteXPC"); + Err(IdeviceError::UnexpectedResponse) + } + } +} diff --git a/idevice/src/services/core_device/mod.rs b/idevice/src/services/core_device/mod.rs index 6114a0e..e6ef7f7 100644 --- a/idevice/src/services/core_device/mod.rs +++ b/idevice/src/services/core_device/mod.rs @@ -9,7 +9,9 @@ use crate::{ }; mod app_service; +mod diagnosticsservice; pub use app_service::*; +pub use diagnosticsservice::*; const CORE_SERVICE_VERSION: &str = "443.18"; diff --git a/tools/Cargo.toml b/tools/Cargo.toml index 2cc126e..54d0d79 100644 --- a/tools/Cargo.toml +++ b/tools/Cargo.toml @@ -105,6 +105,10 @@ path = "src/diagnostics.rs" name = "mobilebackup2" path = "src/mobilebackup2.rs" +[[bin]] +name = "diagnosticsservice" +path = "src/diagnosticsservice.rs" + [dependencies] idevice = { path = "../idevice", features = ["full"], default-features = false } tokio = { version = "1.43", features = ["full"] } @@ -117,6 +121,7 @@ clap = { version = "4.5" } plist = { version = "1.7" } ns-keyed-archive = "0.1.2" uuid = "1.16" +futures-util = { version = "0.3" } [features] default = ["aws-lc"] diff --git a/tools/src/diagnosticsservice.rs b/tools/src/diagnosticsservice.rs new file mode 100644 index 0000000..e043381 --- /dev/null +++ b/tools/src/diagnosticsservice.rs @@ -0,0 +1,106 @@ +// Jackson Coxson + +use clap::{Arg, Command}; +use futures_util::StreamExt; +use idevice::{ + IdeviceService, RsdService, core_device::DiagnostisServiceClient, + core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake, +}; +use tokio::io::AsyncWriteExt; + +mod common; + +#[tokio::main] +async fn main() { + env_logger::init(); + + let matches = Command::new("remotexpc") + .about("Gets a sysdiagnose") + .arg( + Arg::new("host") + .long("host") + .value_name("HOST") + .help("IP address of the device"), + ) + .arg( + Arg::new("pairing_file") + .long("pairing-file") + .value_name("PATH") + .help("Path to the pairing file"), + ) + .arg( + Arg::new("udid") + .value_name("UDID") + .help("UDID of the device (overrides host/pairing file)") + .index(1), + ) + .arg( + Arg::new("tunneld") + .long("tunneld") + .help("Use tunneld") + .action(clap::ArgAction::SetTrue), + ) + .arg( + Arg::new("about") + .long("about") + .help("Show about information") + .action(clap::ArgAction::SetTrue), + ) + .get_matches(); + + if matches.get_flag("about") { + println!("debug_proxy - connect to the debug proxy and run commands"); + println!("Copyright (c) 2025 Jackson Coxson"); + return; + } + + let udid = matches.get_one::("udid"); + let pairing_file = matches.get_one::("pairing_file"); + let host = matches.get_one::("host"); + + let provider = + match common::get_provider(udid, host, pairing_file, "diagnosticsservice-jkcoxson").await { + Ok(p) => p, + Err(e) => { + eprintln!("{e}"); + return; + } + }; + let proxy = CoreDeviceProxy::connect(&*provider) + .await + .expect("no core proxy"); + let rsd_port = proxy.handshake.server_rsd_port; + + let adapter = proxy.create_software_tunnel().expect("no software tunnel"); + let mut adapter = adapter.to_async_handle(); + + let stream = adapter.connect(rsd_port).await.expect("no RSD connect"); + + // Make the connection to RemoteXPC + let mut handshake = RsdHandshake::new(stream).await.unwrap(); + + let mut dsc = DiagnostisServiceClient::connect_rsd(&mut adapter, &mut handshake) + .await + .expect("no connect"); + + println!("Getting sysdiagnose, this takes a while! iOS is slow..."); + let mut res = dsc + .capture_sysdiagnose(false) + .await + .expect("no sysdiagnose"); + println!("Got sysdaignose! Saving to file"); + + let mut written = 0usize; + let mut out = tokio::fs::File::create(&res.preferred_filename) + .await + .expect("no file?"); + while let Some(chunk) = res.stream.next().await { + let buf = chunk.expect("stream stopped?"); + if !buf.is_empty() { + out.write_all(&buf).await.expect("no write all?"); + written += buf.len(); + } + println!("wrote {written}/{} bytes", res.expected_length); + } + println!("Done! Saved to {}", res.preferred_filename); +} From 5cbdb2505a4b9bd74d6b776f39bcda9074f4f94b Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Fri, 15 Aug 2025 20:25:03 -0600 Subject: [PATCH 081/126] Propogate stack errors to handle callers --- idevice/src/tcp/adapter.rs | 2 +- idevice/src/tcp/handle.rs | 19 +++++++++++++++++++ idevice/src/tcp/mod.rs | 4 ++-- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/idevice/src/tcp/adapter.rs b/idevice/src/tcp/adapter.rs index 41d399b..c8d85f1 100644 --- a/idevice/src/tcp/adapter.rs +++ b/idevice/src/tcp/adapter.rs @@ -581,7 +581,7 @@ impl Adapter { } if res.flags.fin { ack_me = Some(res.destination_port); - state.status = ConnectionStatus::Error(ErrorKind::ConnectionReset); + state.status = ConnectionStatus::Error(ErrorKind::UnexpectedEof); } if res.flags.syn && res.flags.ack { ack_me = Some(res.destination_port); diff --git a/idevice/src/tcp/handle.rs b/idevice/src/tcp/handle.rs index 9ee240a..3434f9f 100644 --- a/idevice/src/tcp/handle.rs +++ b/idevice/src/tcp/handle.rs @@ -14,6 +14,8 @@ use tokio::{ sync::oneshot, }; +use crate::tcp::adapter::ConnectionStatus; + pub type ConnectToPortRes = oneshot::Sender, std::io::Error>>), std::io::Error>>; @@ -124,6 +126,23 @@ impl AdapterHandle { handles.remove(&hp); let _ = adapter.close(hp).await; } + + let mut to_close = Vec::new(); + for (&hp, tx) in &handles { + if let Ok(ConnectionStatus::Error(kind)) = adapter.get_status(hp) { + if kind == std::io::ErrorKind::UnexpectedEof { + to_close.push(hp); + } else { + let _ = tx.send(Err(std::io::Error::from(kind))); + to_close.push(hp); + } + } + } + for hp in to_close { + handles.remove(&hp); + // Best-effort close. For RST this just tidies state on our side + let _ = adapter.close(hp).await; + } } _ = tick.tick() => { diff --git a/idevice/src/tcp/mod.rs b/idevice/src/tcp/mod.rs index df755bd..c984672 100644 --- a/idevice/src/tcp/mod.rs +++ b/idevice/src/tcp/mod.rs @@ -5,7 +5,7 @@ use std::{ time::{SystemTime, UNIX_EPOCH}, }; -use log::debug; +use log::trace; use tokio::io::AsyncWriteExt; use crate::{ReadWrite, provider::RsdProvider}; @@ -16,7 +16,7 @@ pub mod packets; pub mod stream; pub(crate) fn log_packet(file: &Arc>, packet: &[u8]) { - debug!("Logging {} byte packet", packet.len()); + trace!("Logging {} byte packet", packet.len()); let packet = packet.to_vec(); let file = file.to_owned(); let now = SystemTime::now(); From 2b75fe1c0574249a48b17d3a77685e7f062d89bd Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Sat, 16 Aug 2025 12:21:47 -0600 Subject: [PATCH 082/126] Diagnosticsservice FFI and CPP bindings --- Cargo.lock | 1 + cpp/examples/diagnosticsservice.cpp | 128 ++++++++++++ cpp/include/idevice++/diagnosticsservice.hpp | 110 ++++++++++ cpp/src/diagnosticsservice.cpp | 88 ++++++++ ffi/Cargo.toml | 3 +- ffi/src/core_device/diagnosticsservice.rs | 209 +++++++++++++++++++ ffi/src/core_device/mod.rs | 1 + 7 files changed, 539 insertions(+), 1 deletion(-) create mode 100644 cpp/examples/diagnosticsservice.cpp create mode 100644 cpp/include/idevice++/diagnosticsservice.hpp create mode 100644 cpp/src/diagnosticsservice.cpp create mode 100644 ffi/src/core_device/diagnosticsservice.rs diff --git a/Cargo.lock b/Cargo.lock index c82aeb9..a33c11e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1168,6 +1168,7 @@ name = "idevice-ffi" version = "0.1.0" dependencies = [ "cbindgen", + "futures", "idevice", "libc", "log", diff --git a/cpp/examples/diagnosticsservice.cpp b/cpp/examples/diagnosticsservice.cpp new file mode 100644 index 0000000..46bfbb1 --- /dev/null +++ b/cpp/examples/diagnosticsservice.cpp @@ -0,0 +1,128 @@ +// Jackson Coxson + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +using namespace IdeviceFFI; + +static void fail(const char* msg, const FfiError& e) { + std::cerr << msg; + if (e) + std::cerr << ": " << e.message; + std::cerr << "\n"; + std::exit(1); +} + +int main() { + idevice_init_logger(Debug, Disabled, NULL); + FfiError err; + + // 1) usbmuxd, pick first device + auto mux = UsbmuxdConnection::default_new(/*tag*/ 0, err); + if (!mux) + fail("failed to connect to usbmuxd", err); + + auto devices = mux->get_devices(err); + if (!devices) + fail("failed to list devices", err); + if (devices->empty()) { + std::cerr << "no devices connected\n"; + return 1; + } + + auto& dev = (*devices)[0]; + auto udid = dev.get_udid(); + auto mux_id = dev.get_id(); + if (!udid || !mux_id) { + std::cerr << "device missing udid or mux id\n"; + return 1; + } + + // 2) Provider via default usbmuxd addr + auto addr = UsbmuxdAddr::default_new(); + + const uint32_t tag = 0; + const std::string label = "diagnosticsservice-jkcoxson"; + auto provider = Provider::usbmuxd_new(std::move(addr), tag, *udid, *mux_id, label, err); + if (!provider) + fail("failed to create provider", err); + + // 3) CoreDeviceProxy + auto cdp = CoreDeviceProxy::connect(*provider, err); + if (!cdp) + fail("failed CoreDeviceProxy connect", err); + + auto rsd_port = cdp->get_server_rsd_port(err); + if (!rsd_port) + fail("failed to get RSD port", err); + + // 4) Software tunnel → connect to RSD + auto adapter = std::move(*cdp).create_tcp_adapter(err); + if (!adapter) + fail("failed to create software tunnel adapter", err); + + auto stream = adapter->connect(*rsd_port, err); + if (!stream) + fail("failed to connect RSD stream", err); + + // 5) RSD handshake + auto rsd = RsdHandshake::from_socket(std::move(*stream), err); + if (!rsd) + fail("failed RSD handshake", err); + // 6) Diagnostics Service over RSD + auto diag = DiagnosticsService::connect_rsd(*adapter, *rsd, err); + if (!diag) + fail("failed to connect DiagnosticsService", err); + + std::cout << "Getting sysdiagnose, this takes a while! iOS is slow...\n"; + + auto cap = diag->capture_sysdiagnose(/*dry_run=*/false, err); + if (!cap) + fail("capture_sysdiagnose failed", err); + + std::cout << "Got sysdiagnose! Saving to file: " << cap->preferred_filename << "\n"; + + // 7) Stream to file with progress + std::ofstream out(cap->preferred_filename, std::ios::binary); + if (!out) { + std::cerr << "failed to open output file\n"; + return 1; + } + + std::size_t written = 0; + const std::size_t total = cap->expected_length; + + for (;;) { + auto chunk = cap->stream.next_chunk(err); + if (!chunk) { + if (err) + fail("stream error", err); // err set only on real error + break; // nullptr means end-of-stream + } + if (!chunk->empty()) { + out.write(reinterpret_cast(chunk->data()), + static_cast(chunk->size())); + if (!out) { + std::cerr << "write failed\n"; + return 1; + } + written += chunk->size(); + } + std::cout << "wrote " << written << "/" << total << " bytes\r" << std::flush; + } + + out.flush(); + std::cout << "\nDone! Saved to " << cap->preferred_filename << "\n"; + return 0; +} diff --git a/cpp/include/idevice++/diagnosticsservice.hpp b/cpp/include/idevice++/diagnosticsservice.hpp new file mode 100644 index 0000000..b70e848 --- /dev/null +++ b/cpp/include/idevice++/diagnosticsservice.hpp @@ -0,0 +1,110 @@ +// Jackson Coxson + +#pragma once +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace IdeviceFFI { + +class SysdiagnoseStream { + public: + SysdiagnoseStream() = default; + SysdiagnoseStream(const SysdiagnoseStream&) = delete; + SysdiagnoseStream& operator=(const SysdiagnoseStream&) = delete; + + SysdiagnoseStream(SysdiagnoseStream&& other) noexcept : h_(other.h_) { other.h_ = nullptr; } + SysdiagnoseStream& operator=(SysdiagnoseStream&& other) noexcept { + if (this != &other) { + reset(); + h_ = other.h_; + other.h_ = nullptr; + } + return *this; + } + + ~SysdiagnoseStream() { reset(); } + + // Pull next chunk. Returns nullopt on end-of-stream. On error, returns nullopt and sets `err`. + std::optional> next_chunk(FfiError& err); + + SysdiagnoseStreamHandle* raw() const { return h_; } + + private: + friend class DiagnosticsService; + explicit SysdiagnoseStream(::SysdiagnoseStreamHandle* h) : h_(h) {} + + void reset() { + if (h_) { + ::sysdiagnose_stream_free(h_); + h_ = nullptr; + } + } + + ::SysdiagnoseStreamHandle* h_ = nullptr; +}; + +// The result of starting a sysdiagnose capture. +struct SysdiagnoseCapture { + std::string preferred_filename; + std::size_t expected_length = 0; + SysdiagnoseStream stream; +}; + +// RAII for Diagnostics service client +class DiagnosticsService { + public: + DiagnosticsService() = default; + DiagnosticsService(const DiagnosticsService&) = delete; + DiagnosticsService& operator=(const DiagnosticsService&) = delete; + + DiagnosticsService(DiagnosticsService&& other) noexcept : h_(other.h_) { other.h_ = nullptr; } + DiagnosticsService& operator=(DiagnosticsService&& other) noexcept { + if (this != &other) { + reset(); + h_ = other.h_; + other.h_ = nullptr; + } + return *this; + } + + ~DiagnosticsService() { reset(); } + + // Connect via RSD (borrows adapter & handshake; does not consume them) + static std::optional + connect_rsd(Adapter& adapter, RsdHandshake& rsd, FfiError& err); + + // Create from a ReadWrite stream (consumes it) + static std::optional from_stream_ptr(::ReadWriteOpaque* consumed, + FfiError& err); + + static std::optional from_stream(ReadWrite&& rw, FfiError& err) { + return from_stream_ptr(rw.release(), err); + } + + // Start sysdiagnose capture; on success returns filename, length and a byte stream + std::optional capture_sysdiagnose(bool dry_run, FfiError& err); + + ::DiagnosticsServiceHandle* raw() const { return h_; } + + private: + explicit DiagnosticsService(::DiagnosticsServiceHandle* h) : h_(h) {} + + void reset() { + if (h_) { + ::diagnostics_service_free(h_); + h_ = nullptr; + } + } + + ::DiagnosticsServiceHandle* h_ = nullptr; +}; + +} // namespace IdeviceFFI diff --git a/cpp/src/diagnosticsservice.cpp b/cpp/src/diagnosticsservice.cpp new file mode 100644 index 0000000..26aa91e --- /dev/null +++ b/cpp/src/diagnosticsservice.cpp @@ -0,0 +1,88 @@ +// Jackson Coxson + +#include + +namespace IdeviceFFI { + +// Local helper: take ownership of a C string and convert to std::string +static std::optional take_cstring(char* p) { + if (!p) + return std::nullopt; + std::string s(p); + ::idevice_string_free(p); + return s; +} + +// -------- SysdiagnoseStream -------- +std::optional> SysdiagnoseStream::next_chunk(FfiError& err) { + if (!h_) + return std::nullopt; + + uint8_t* data = nullptr; + std::size_t len = 0; + + if (IdeviceFfiError* e = ::sysdiagnose_stream_next(h_, &data, &len)) { + err = FfiError(e); + return std::nullopt; + } + + if (!data || len == 0) { + // End of stream + return std::nullopt; + } + + // Copy into a C++ buffer + std::vector out(len); + std::memcpy(out.data(), data, len); + + idevice_data_free(data, len); + + return out; +} + +// -------- DiagnosticsService -------- +std::optional +DiagnosticsService::connect_rsd(Adapter& adapter, RsdHandshake& rsd, FfiError& err) { + ::DiagnosticsServiceHandle* out = nullptr; + if (IdeviceFfiError* e = ::diagnostics_service_connect_rsd(adapter.raw(), rsd.raw(), &out)) { + err = FfiError(e); + return std::nullopt; + } + return DiagnosticsService(out); +} + +std::optional DiagnosticsService::from_stream_ptr(::ReadWriteOpaque* consumed, + FfiError& err) { + ::DiagnosticsServiceHandle* out = nullptr; + if (IdeviceFfiError* e = ::diagnostics_service_new(consumed, &out)) { + err = FfiError(e); + return std::nullopt; + } + return DiagnosticsService(out); +} + +std::optional DiagnosticsService::capture_sysdiagnose(bool dry_run, + FfiError& err) { + if (!h_) + return std::nullopt; + + char* filename_c = nullptr; + std::size_t expected_len = 0; + ::SysdiagnoseStreamHandle* stream_h = nullptr; + + if (IdeviceFfiError* e = ::diagnostics_service_capture_sysdiagnose( + h_, dry_run ? true : false, &filename_c, &expected_len, &stream_h)) { + err = FfiError(e); + return std::nullopt; + } + + auto fname = take_cstring(filename_c).value_or(std::string{}); + SysdiagnoseStream stream(stream_h); + + SysdiagnoseCapture cap{/*preferred_filename*/ std::move(fname), + /*expected_length*/ expected_len, + /*stream*/ std::move(stream)}; + return cap; +} + +} // namespace IdeviceFFI diff --git a/ffi/Cargo.toml b/ffi/Cargo.toml index 3c21df6..33b96fa 100644 --- a/ffi/Cargo.toml +++ b/ffi/Cargo.toml @@ -6,6 +6,7 @@ edition = "2024" [dependencies] idevice = { path = "../idevice", default-features = false } +futures = { version = "0.3", optional = true } log = "0.4.26" simplelog = "0.12.2" once_cell = "1.21.1" @@ -24,7 +25,7 @@ ring = ["idevice/ring"] afc = ["idevice/afc"] amfi = ["idevice/amfi"] -core_device = ["idevice/core_device"] +core_device = ["idevice/core_device", "dep:futures"] core_device_proxy = ["idevice/core_device_proxy"] crashreportcopymobile = ["idevice/crashreportcopymobile"] debug_proxy = ["idevice/debug_proxy"] diff --git a/ffi/src/core_device/diagnosticsservice.rs b/ffi/src/core_device/diagnosticsservice.rs new file mode 100644 index 0000000..24476db --- /dev/null +++ b/ffi/src/core_device/diagnosticsservice.rs @@ -0,0 +1,209 @@ +// Jackson Coxson + +use std::ffi::{CString, c_char}; +use std::pin::Pin; +use std::ptr::null_mut; + +use futures::{Stream, StreamExt}; +use idevice::core_device::DiagnostisServiceClient; +use idevice::{IdeviceError, ReadWrite, RsdService}; + +use crate::core_device_proxy::AdapterHandle; +use crate::rsd::RsdHandshakeHandle; +use crate::{IdeviceFfiError, RUNTIME, ReadWriteOpaque, ffi_err}; + +/// Opaque handle to an AppServiceClient +pub struct DiagnosticsServiceHandle(pub DiagnostisServiceClient>); +pub struct SysdiagnoseStreamHandle<'a>( + pub Pin, IdeviceError>> + 'a>>, +); + +/// Creates a new DiagnosticsServiceClient using RSD connection +/// +/// # Arguments +/// * [`provider`] - An adapter created by this library +/// * [`handshake`] - An RSD handshake from the same provider +/// * [`handle`] - Pointer to store the newly created handle +/// +/// # Returns +/// An IdeviceFfiError on error, null on success +/// +/// # Safety +/// `provider` and `handshake` must be valid pointers to handles allocated by this library +/// `handle` must be a valid pointer to a location where the handle will be stored +#[unsafe(no_mangle)] +pub unsafe extern "C" fn diagnostics_service_connect_rsd( + provider: *mut AdapterHandle, + handshake: *mut RsdHandshakeHandle, + handle: *mut *mut DiagnosticsServiceHandle, +) -> *mut IdeviceFfiError { + if provider.is_null() || handshake.is_null() || handle.is_null() { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + + let res: Result>, IdeviceError> = + RUNTIME.block_on(async move { + let provider_ref = unsafe { &mut (*provider).0 }; + let handshake_ref = unsafe { &mut (*handshake).0 }; + + DiagnostisServiceClient::connect_rsd(provider_ref, handshake_ref).await + }); + + match res { + Ok(client) => { + let boxed = Box::new(DiagnosticsServiceHandle(client)); + unsafe { *handle = Box::into_raw(boxed) }; + null_mut() + } + Err(e) => ffi_err!(e), + } +} + +/// Creates a new DiagnostisServiceClient from a socket +/// +/// # Arguments +/// * [`socket`] - The socket to use for communication +/// * [`handle`] - Pointer to store the newly created handle +/// +/// # Returns +/// An IdeviceFfiError on error, null on success +/// +/// # Safety +/// `socket` must be a valid pointer to a handle allocated by this library +/// `handle` must be a valid pointer to a location where the handle will be stored +#[unsafe(no_mangle)] +pub unsafe extern "C" fn diagnostics_service_new( + socket: *mut ReadWriteOpaque, + handle: *mut *mut DiagnosticsServiceHandle, +) -> *mut IdeviceFfiError { + if socket.is_null() || handle.is_null() { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + + let socket = unsafe { Box::from_raw(socket) }; + let res = RUNTIME + .block_on(async move { DiagnostisServiceClient::from_stream(socket.inner.unwrap()).await }); + + match res { + Ok(client) => { + let new_handle = DiagnosticsServiceHandle(client); + unsafe { *handle = Box::into_raw(Box::new(new_handle)) }; + null_mut() + } + Err(e) => ffi_err!(e), + } +} + +/// Captures a sysdiagnose from the device. +/// Note that this will take a LONG time to return while the device collects enough information to +/// return to the service. This function returns a stream that can be called on to get the next +/// chunk of data. A typical sysdiagnose is roughly 1-2 GB. +/// +/// # Arguments +/// * [`handle`] - The handle to the client +/// * [`dry_run`] - Whether or not to do a dry run with a simple .txt file from the device +/// * [`preferred_filename`] - The name the device wants to save the sysdaignose as +/// * [`expected_length`] - The size in bytes of the sysdiagnose +/// * [`stream_handle`] - The handle that will be set to capture bytes for +/// +/// # Returns +/// An IdeviceFfiError on error, null on success +/// +/// # Safety +/// Pointers must be all valid. Handle must be allocated by this library. Preferred filename must +/// be freed `idevice_string_free`. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn diagnostics_service_capture_sysdiagnose( + handle: *mut DiagnosticsServiceHandle, + dry_run: bool, + preferred_filename: *mut *mut c_char, + expected_length: *mut usize, + stream_handle: *mut *mut SysdiagnoseStreamHandle, +) -> *mut IdeviceFfiError { + if handle.is_null() + || preferred_filename.is_null() + || expected_length.is_null() + || stream_handle.is_null() + { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + let handle = unsafe { &mut *handle }; + let res = RUNTIME.block_on(async move { handle.0.capture_sysdiagnose(dry_run).await }); + match res { + Ok(res) => { + let filename = CString::new(res.preferred_filename).unwrap(); + unsafe { + *preferred_filename = filename.into_raw(); + *expected_length = res.expected_length; + *stream_handle = Box::into_raw(Box::new(SysdiagnoseStreamHandle(res.stream))); + } + null_mut() + } + Err(e) => ffi_err!(e), + } +} + +/// Gets the next packet from the stream. +/// Data will be set to 0 when there is no more data to get from the stream. +/// +/// # Arguments +/// * [`handle`] - The handle to the stream +/// * [`data`] - A pointer to the bytes +/// * [`len`] - The length of the bytes written +/// +/// # Returns +/// An IdeviceFfiError on error, null on success +/// +/// # Safety +/// Pass valid pointers. The handle must be allocated by this library. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn sysdiagnose_stream_next( + handle: *mut SysdiagnoseStreamHandle, + data: *mut *mut u8, + len: *mut usize, +) -> *mut IdeviceFfiError { + if handle.is_null() || data.is_null() || len.is_null() { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + let handle = unsafe { &mut *handle }; + let res = RUNTIME.block_on(async move { handle.0.next().await }); + match res { + Some(Ok(res)) => { + let mut res = res.into_boxed_slice(); + unsafe { + *len = res.len(); + *data = res.as_mut_ptr(); + } + std::mem::forget(res); + null_mut() + } + Some(Err(e)) => ffi_err!(e), + None => { + // we're empty + unsafe { *data = null_mut() }; + null_mut() + } + } +} + +/// Frees a DiagnostisServiceClient handle +/// +/// # Safety +/// `handle` must be a valid pointer to a handle allocated by this library or NULL +#[unsafe(no_mangle)] +pub unsafe extern "C" fn diagnostics_service_free(handle: *mut DiagnosticsServiceHandle) { + if !handle.is_null() { + let _ = unsafe { Box::from_raw(handle) }; + } +} + +/// Frees a SysdiagnoseStreamHandle handle +/// +/// # Safety +/// `handle` must be a valid pointer to a handle allocated by this library or NULL +#[unsafe(no_mangle)] +pub unsafe extern "C" fn sysdiagnose_stream_free(handle: *mut SysdiagnoseStreamHandle) { + if !handle.is_null() { + let _ = unsafe { Box::from_raw(handle) }; + } +} diff --git a/ffi/src/core_device/mod.rs b/ffi/src/core_device/mod.rs index 3eb1865..409a2df 100644 --- a/ffi/src/core_device/mod.rs +++ b/ffi/src/core_device/mod.rs @@ -1,3 +1,4 @@ // Jackson Coxson pub mod app_service; +pub mod diagnosticsservice; From 15180b29688665cd8fa1ce155789bb03104ab7a6 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Sun, 17 Aug 2025 18:03:05 -0600 Subject: [PATCH 083/126] Bump version --- idevice/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/idevice/Cargo.toml b/idevice/Cargo.toml index 86836b1..0471810 100644 --- a/idevice/Cargo.toml +++ b/idevice/Cargo.toml @@ -2,7 +2,7 @@ name = "idevice" description = "A Rust library to interact with services on iOS devices." authors = ["Jackson Coxson"] -version = "0.1.38" +version = "0.1.39" edition = "2024" license = "MIT" documentation = "https://docs.rs/idevice" From 47dbab0155eaa34a3591c8964a2dc1b3ebf43dcb Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Sun, 17 Aug 2025 20:44:53 -0600 Subject: [PATCH 084/126] Implement bt_packet_logger --- Cargo.lock | 2 +- idevice/Cargo.toml | 2 + idevice/src/services/bt_packet_logger.rs | 204 +++++++++++++++++++++++ idevice/src/services/mod.rs | 2 + tools/Cargo.toml | 4 + tools/src/bt_packet_logger.rs | 104 ++++++++++++ tools/src/pcap.rs | 60 +++++++ 7 files changed, 377 insertions(+), 1 deletion(-) create mode 100644 idevice/src/services/bt_packet_logger.rs create mode 100644 tools/src/bt_packet_logger.rs create mode 100644 tools/src/pcap.rs diff --git a/Cargo.lock b/Cargo.lock index a33c11e..e5df891 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1132,7 +1132,7 @@ dependencies = [ [[package]] name = "idevice" -version = "0.1.38" +version = "0.1.39" dependencies = [ "async-stream", "base64", diff --git a/idevice/Cargo.toml b/idevice/Cargo.toml index 0471810..d6ed576 100644 --- a/idevice/Cargo.toml +++ b/idevice/Cargo.toml @@ -66,6 +66,7 @@ ring = ["rustls/ring", "tokio-rustls/ring"] afc = ["dep:chrono"] amfi = [] +bt_packet_logger = [] companion_proxy = [] core_device = ["xpc", "dep:uuid"] core_device_proxy = ["dep:serde_json", "dep:json", "dep:byteorder"] @@ -101,6 +102,7 @@ xpc = ["dep:indexmap", "dep:uuid", "dep:async-stream"] full = [ "afc", "amfi", + "bt_packet_logger", "companion_proxy", "core_device", "core_device_proxy", diff --git a/idevice/src/services/bt_packet_logger.rs b/idevice/src/services/bt_packet_logger.rs new file mode 100644 index 0000000..a4883e5 --- /dev/null +++ b/idevice/src/services/bt_packet_logger.rs @@ -0,0 +1,204 @@ +//! Abstraction for BTPacketLogger +//! You must have the Bluetooth profile installed, or you'll get no data. +//! https://developer.apple.com/bug-reporting/profiles-and-logs/?name=bluetooth + +use std::pin::Pin; + +use futures::Stream; +use log::{debug, warn}; + +use crate::{Idevice, IdeviceError, IdeviceService, obf}; + +/// Client for interacting with the BTPacketLogger service on the device. +/// You must have the Bluetooth profile installed, or you'll get no data. +/// +/// ``https://developer.apple.com/bug-reporting/profiles-and-logs/?name=bluetooth`` +pub struct BtPacketLoggerClient { + /// The underlying device connection with established logger service + pub idevice: Idevice, +} + +#[derive(Debug, Clone)] +pub struct BtFrame { + pub hdr: BtHeader, + pub kind: BtPacketKind, + /// H4-ready payload (first byte is H4 type: 0x01 cmd, 0x02 ACL, 0x03 SCO, 0x04 evt) + pub h4: Vec, +} + +#[derive(Debug, Clone, Copy)] +pub struct BtHeader { + /// Advisory length for [kind + payload]; may not equal actual frame len - 12 + pub length: u32, // BE on the wire + pub ts_secs: u32, // BE + pub ts_usecs: u32, // BE +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BtPacketKind { + HciCmd, // 0x00 + HciEvt, // 0x01 + AclSent, // 0x02 + AclRecv, // 0x03 + ScoSent, // 0x08 + ScoRecv, // 0x09 + Other(u8), +} + +impl BtPacketKind { + fn from_byte(b: u8) -> Self { + match b { + 0x00 => BtPacketKind::HciCmd, + 0x01 => BtPacketKind::HciEvt, + 0x02 => BtPacketKind::AclSent, + 0x03 => BtPacketKind::AclRecv, + 0x08 => BtPacketKind::ScoSent, + 0x09 => BtPacketKind::ScoRecv, + x => BtPacketKind::Other(x), + } + } + fn h4_type(self) -> Option { + match self { + BtPacketKind::HciCmd => Some(0x01), + BtPacketKind::AclSent | BtPacketKind::AclRecv => Some(0x02), + BtPacketKind::ScoSent | BtPacketKind::ScoRecv => Some(0x03), + BtPacketKind::HciEvt => Some(0x04), + BtPacketKind::Other(_) => None, + } + } +} + +impl IdeviceService for BtPacketLoggerClient { + fn service_name() -> std::borrow::Cow<'static, str> { + obf!("com.apple.bluetooth.BTPacketLogger") + } + + async fn from_stream(idevice: Idevice) -> Result { + Ok(Self::new(idevice)) + } +} + +impl BtPacketLoggerClient { + pub fn new(idevice: Idevice) -> Self { + Self { idevice } + } + + /// Read a single *outer* frame and return one parsed record from it. + /// (This service typically delivers one record per frame.) + pub async fn next_packet( + &mut self, + ) -> Result)>, IdeviceError> { + // 2-byte outer length is **little-endian** + let len = self.idevice.read_raw(2).await?; + if len.len() != 2 { + return Ok(None); // EOF + } + let frame_len = u16::from_le_bytes([len[0], len[1]]) as usize; + + if !(13..=64 * 1024).contains(&frame_len) { + return Err(IdeviceError::UnexpectedResponse); + } + + let frame = self.idevice.read_raw(frame_len).await?; + if frame.len() != frame_len { + return Err(IdeviceError::NotEnoughBytes(frame.len(), frame_len)); + } + + // Parse header at fixed offsets (BE u32s) + let (hdr, off) = BtHeader::parse(&frame).ok_or(IdeviceError::UnexpectedResponse)?; + // packet_type at byte 12, payload starts at 13 + let kind = BtPacketKind::from_byte(frame[off]); + let payload = &frame[off + 1..]; // whatever remains + + // Optional soft check of advisory header.length + let advisory = hdr.length as usize; + let actual = 1 + payload.len(); // kind + payload + if advisory != actual { + debug!( + "BTPacketLogger advisory length {} != actual {}, proceeding", + advisory, actual + ); + } + + // Build H4 buffer (prepend type byte) + let mut h4 = Vec::with_capacity(1 + payload.len()); + if let Some(t) = kind.h4_type() { + h4.push(t); + } else { + return Ok(None); + } + h4.extend_from_slice(payload); + + Ok(Some((hdr, kind, h4))) + } + + /// Continuous stream of parsed frames. + pub fn into_stream( + mut self, + ) -> Pin> + Send>> { + Box::pin(async_stream::try_stream! { + loop { + // outer length (LE) + let len = self.idevice.read_raw(2).await?; + if len.len() != 2 { break; } + let frame_len = u16::from_le_bytes([len[0], len[1]]) as usize; + if !(13..=64 * 1024).contains(&frame_len) { + warn!("invalid frame_len {}", frame_len); + continue; + } + + // frame bytes + let frame = self.idevice.read_raw(frame_len).await?; + if frame.len() != frame_len { + Err(IdeviceError::NotEnoughBytes(frame.len(), frame_len))?; + } + + // header + kind + payload + let (hdr, off) = BtHeader::parse(&frame).ok_or(IdeviceError::UnexpectedResponse)?; + let kind = BtPacketKind::from_byte(frame[off]); + let payload = &frame[off + 1..]; + + // soft advisory check + let advisory = hdr.length as usize; + let actual = 1 + payload.len(); + if advisory != actual { + debug!("BTPacketLogger advisory length {} != actual {}", advisory, actual); + } + + // make H4 buffer + let mut h4 = Vec::with_capacity(1 + payload.len()); + if let Some(t) = kind.h4_type() { + h4.push(t); + } else { + // unknown kind + continue; + } + h4.extend_from_slice(payload); + + yield BtFrame { hdr, kind, h4 }; + } + }) + } +} + +impl BtHeader { + /// Parse 12-byte header at the start of a frame. + /// Returns (header, next_offset) where next_offset == 12 (start of packet_type). + fn parse(buf: &[u8]) -> Option<(Self, usize)> { + if buf.len() < 12 { + return None; + } + let length = u32::from_be_bytes(buf[0..4].try_into().ok()?); + let ts_secs = u32::from_be_bytes(buf[4..8].try_into().ok()?); + let ts_usecs = u32::from_be_bytes(buf[8..12].try_into().ok()?); + Some(( + BtHeader { + length, + ts_secs, + ts_usecs, + }, + 12, + )) + } +} + diff --git a/idevice/src/services/mod.rs b/idevice/src/services/mod.rs index fd341ec..6b14605 100644 --- a/idevice/src/services/mod.rs +++ b/idevice/src/services/mod.rs @@ -2,6 +2,8 @@ pub mod afc; #[cfg(feature = "amfi")] pub mod amfi; +#[cfg(feature = "bt_packet_logger")] +pub mod bt_packet_logger; #[cfg(feature = "companion_proxy")] pub mod companion_proxy; #[cfg(feature = "core_device")] diff --git a/tools/Cargo.toml b/tools/Cargo.toml index 54d0d79..717838a 100644 --- a/tools/Cargo.toml +++ b/tools/Cargo.toml @@ -109,6 +109,10 @@ path = "src/mobilebackup2.rs" name = "diagnosticsservice" path = "src/diagnosticsservice.rs" +[[bin]] +name = "bt_packet_logger" +path = "src/bt_packet_logger.rs" + [dependencies] idevice = { path = "../idevice", features = ["full"], default-features = false } tokio = { version = "1.43", features = ["full"] } diff --git a/tools/src/bt_packet_logger.rs b/tools/src/bt_packet_logger.rs new file mode 100644 index 0000000..eef5d2d --- /dev/null +++ b/tools/src/bt_packet_logger.rs @@ -0,0 +1,104 @@ +// Jackson Coxson + +use clap::{Arg, Command}; +use futures_util::StreamExt; +use idevice::{IdeviceService, bt_packet_logger::BtPacketLoggerClient}; +use tokio::io::AsyncWrite; + +use crate::pcap::{write_pcap_header, write_pcap_record}; + +mod common; +mod pcap; + +#[tokio::main] +async fn main() { + env_logger::init(); + + let matches = Command::new("amfi") + .about("Capture Bluetooth packets") + .arg( + Arg::new("host") + .long("host") + .value_name("HOST") + .help("IP address of the device"), + ) + .arg( + Arg::new("pairing_file") + .long("pairing-file") + .value_name("PATH") + .help("Path to the pairing file"), + ) + .arg( + Arg::new("udid") + .value_name("UDID") + .help("UDID of the device (overrides host/pairing file)") + .index(1), + ) + .arg( + Arg::new("about") + .long("about") + .help("Show about information") + .action(clap::ArgAction::SetTrue), + ) + .arg( + Arg::new("out") + .long("out") + .value_name("PCAP") + .help("Write PCAP to this file (use '-' for stdout)"), + ) + .get_matches(); + + if matches.get_flag("about") { + println!("bt_packet_logger - capture bluetooth packets"); + println!("Copyright (c) 2025 Jackson Coxson"); + return; + } + + let udid = matches.get_one::("udid"); + let host = matches.get_one::("host"); + let pairing_file = matches.get_one::("pairing_file"); + let out = matches.get_one::("out").map(String::to_owned); + + let provider = match common::get_provider(udid, host, pairing_file, "amfi-jkcoxson").await { + Ok(p) => p, + Err(e) => { + eprintln!("{e}"); + return; + } + }; + + let logger_client = BtPacketLoggerClient::connect(&*provider) + .await + .expect("Failed to connect to amfi"); + + let mut s = logger_client.into_stream(); + + // Open output (default to stdout if --out omitted) + let mut out_writer: Box = match out.as_deref() { + Some("-") | None => Box::new(tokio::io::stdout()), + Some(path) => Box::new(tokio::fs::File::create(path).await.expect("open pcap")), + }; + + // Write global header + write_pcap_header(&mut out_writer) + .await + .expect("pcap header"); + + // Drain stream to PCAP + while let Some(res) = s.next().await { + match res { + Ok(frame) => { + write_pcap_record( + &mut out_writer, + frame.hdr.ts_secs, + frame.hdr.ts_usecs, + frame.kind, + &frame.h4, + ) + .await + .unwrap_or_else(|e| eprintln!("pcap write error: {e}")); + } + Err(e) => eprintln!("Failed to get next packet: {e:?}"), + } + } +} diff --git a/tools/src/pcap.rs b/tools/src/pcap.rs new file mode 100644 index 0000000..a752ecf --- /dev/null +++ b/tools/src/pcap.rs @@ -0,0 +1,60 @@ +use idevice::bt_packet_logger::BtPacketKind; +use tokio::io::{AsyncWrite, AsyncWriteExt}; + +// Classic PCAP (big-endian) global header for DLT_BLUETOOTH_HCI_H4_WITH_PHDR (201) +const PCAP_GLOBAL_HEADER_BE: [u8; 24] = [ + 0xA1, 0xB2, 0xC3, 0xD4, // magic (big-endian stream) + 0x00, 0x02, // version maj + 0x00, 0x04, // version min + 0x00, 0x00, 0x00, 0x00, // thiszone + 0x00, 0x00, 0x00, 0x00, // sigfigs + 0x00, 0x00, 0x08, 0x00, // snaplen = 2048 + 0x00, 0x00, 0x00, 201, // network = 201 (HCI_H4_WITH_PHDR) +]; + +#[inline] +fn be32(x: u32) -> [u8; 4] { + [(x >> 24) as u8, (x >> 16) as u8, (x >> 8) as u8, x as u8] +} + +#[inline] +fn dir_flag(kind: BtPacketKind) -> Option { + use BtPacketKind::*; + Some(match kind { + HciCmd | AclSent | ScoSent => 0, + HciEvt | AclRecv | ScoRecv => 1, + _ => return None, + }) +} + +pub async fn write_pcap_header(w: &mut W) -> std::io::Result<()> { + w.write_all(&PCAP_GLOBAL_HEADER_BE).await +} + +pub async fn write_pcap_record( + w: &mut W, + ts_sec: u32, + ts_usec: u32, + kind: BtPacketKind, + h4_payload: &[u8], // starts with H4 type followed by HCI bytes +) -> std::io::Result<()> { + // Prepend 4-byte direction flag to the packet body + let Some(dir) = dir_flag(kind) else { + return Ok(()); + }; + let cap_len = 4u32 + h4_payload.len() as u32; + + // PCAP record header (big-endian fields to match magic above) + // ts_sec, ts_usec, incl_len, orig_len + let mut rec = [0u8; 16]; + rec[0..4].copy_from_slice(&be32(ts_sec)); + rec[4..8].copy_from_slice(&be32(ts_usec)); + rec[8..12].copy_from_slice(&be32(cap_len)); + rec[12..16].copy_from_slice(&be32(cap_len)); + + // Write: rec hdr, dir flag (as 4 BE bytes), then H4 bytes + w.write_all(&rec).await?; + w.write_all(&be32(dir)).await?; + w.write_all(h4_payload).await?; + Ok(()) +} From f388aaaf2d2da2bb6ffe731bbc8d4fce87115066 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Sun, 17 Aug 2025 22:31:19 -0600 Subject: [PATCH 085/126] Port some services to plist macro --- idevice/src/plist_macro.rs | 17 ++ .../src/services/core_device/app_service.rs | 13 +- idevice/src/services/dvt/process_control.rs | 7 +- idevice/src/services/heartbeat.rs | 9 +- idevice/src/services/installation_proxy.rs | 86 ++++---- idevice/src/services/lockdown.rs | 128 ++++------- idevice/src/services/mobile_image_mounter.rs | 146 ++++++------- idevice/src/services/mobilebackup2.rs | 201 ++++++------------ idevice/src/services/os_trace_relay.rs | 49 ++--- idevice/src/services/restore_service.rs | 61 +++--- idevice/src/services/springboardservices.rs | 11 +- 11 files changed, 288 insertions(+), 440 deletions(-) diff --git a/idevice/src/plist_macro.rs b/idevice/src/plist_macro.rs index c117179..3453b43 100644 --- a/idevice/src/plist_macro.rs +++ b/idevice/src/plist_macro.rs @@ -52,6 +52,23 @@ /// ``` #[macro_export] macro_rules! plist { + // Force: dictionary out + (dict { $($tt:tt)+ }) => {{ + let mut object = plist::Dictionary::new(); + $crate::plist_internal!(@object object () ($($tt)+) ($($tt)+)); + object + }}; + + // Force: value out (explicit, though default already does this) + (value { $($tt:tt)+ }) => { + $crate::plist_internal!({ $($tt)+ }) + }; + + // Force: raw vec of plist::Value out + (array [ $($tt:tt)+ ]) => { + $crate::plist_internal!(@array [] $($tt)+) + }; + // Hide distracting implementation details from the generated rustdoc. ($($plist:tt)+) => { $crate::plist_internal!($($plist)+) diff --git a/idevice/src/services/core_device/app_service.rs b/idevice/src/services/core_device/app_service.rs index d4a2952..eedd185 100644 --- a/idevice/src/services/core_device/app_service.rs +++ b/idevice/src/services/core_device/app_service.rs @@ -143,12 +143,13 @@ impl AppServiceClient { internal_apps: bool, default_apps: bool, ) -> Result, IdeviceError> { - let mut options = plist::Dictionary::new(); - options.insert("includeAppClips".into(), app_clips.into()); - options.insert("includeRemovableApps".into(), removable_apps.into()); - options.insert("includeHiddenApps".into(), hidden_apps.into()); - options.insert("includeInternalApps".into(), internal_apps.into()); - options.insert("includeDefaultApps".into(), default_apps.into()); + let options = crate::plist!(dict { + "includeAppClips": app_clips, + "includeRemovableApps": removable_apps, + "includeHiddenApps": hidden_apps, + "includeInternalApps": internal_apps, + "includeDefaultApps": default_apps, + }); let res = self .inner .invoke("com.apple.coredevice.feature.listapps", Some(options)) diff --git a/idevice/src/services/dvt/process_control.rs b/idevice/src/services/dvt/process_control.rs index c1f6edb..f31d046 100644 --- a/idevice/src/services/dvt/process_control.rs +++ b/idevice/src/services/dvt/process_control.rs @@ -98,9 +98,10 @@ impl<'a, R: ReadWrite> ProcessControlClient<'a, R> { "launchSuspendedProcessWithDevicePath:bundleIdentifier:environment:arguments:options:" .into(), ); - let mut options = Dictionary::new(); - options.insert("StartSuspendedKey".into(), start_suspended.into()); - options.insert("KillExisting".into(), kill_existing.into()); + let options = crate::plist!(dict { + "StartSuspendedKey": start_suspended, + "KillExisting": kill_existing + }); let env_vars = match env_vars { Some(e) => e, diff --git a/idevice/src/services/heartbeat.rs b/idevice/src/services/heartbeat.rs index 745abaf..f198d48 100644 --- a/idevice/src/services/heartbeat.rs +++ b/idevice/src/services/heartbeat.rs @@ -90,11 +90,10 @@ impl HeartbeatClient { /// # Errors /// Returns `IdeviceError` if the message fails to send pub async fn send_polo(&mut self) -> Result<(), IdeviceError> { - let mut req = plist::Dictionary::new(); - req.insert("Command".into(), "Polo".into()); - self.idevice - .send_plist(plist::Value::Dictionary(req.clone())) - .await?; + let req = crate::plist!({ + "Command": "Polo" + }); + self.idevice.send_plist(req).await?; Ok(()) } } diff --git a/idevice/src/services/installation_proxy.rs b/idevice/src/services/installation_proxy.rs index 9cf28fb..325d210 100644 --- a/idevice/src/services/installation_proxy.rs +++ b/idevice/src/services/installation_proxy.rs @@ -70,22 +70,15 @@ impl InstallationProxyClient { bundle_identifiers: Option>, ) -> Result, IdeviceError> { let application_type = application_type.unwrap_or("Any"); - let mut options = plist::Dictionary::new(); - if let Some(ids) = bundle_identifiers { - let ids = ids - .into_iter() - .map(plist::Value::String) - .collect::>(); - options.insert("BundleIDs".into(), ids.into()); - } - options.insert("ApplicationType".into(), application_type.into()); - let mut req = plist::Dictionary::new(); - req.insert("Command".into(), "Lookup".into()); - req.insert("ClientOptions".into(), plist::Value::Dictionary(options)); - self.idevice - .send_plist(plist::Value::Dictionary(req)) - .await?; + let req = crate::plist!({ + "Command": "Lookup", + "ClientOptions": { + "ApplicationType": application_type, + "BundleIDs":? bundle_identifiers, + } + }); + self.idevice.send_plist(req).await?; let mut res = self.idevice.read_plist().await?; match res.remove("LookupResult") { @@ -155,14 +148,13 @@ impl InstallationProxyClient { let package_path = package_path.into(); let options = options.unwrap_or(plist::Value::Dictionary(Dictionary::new())); - let mut command = Dictionary::new(); - command.insert("Command".into(), "Install".into()); - command.insert("ClientOptions".into(), options); - command.insert("PackagePath".into(), package_path.into()); + let command = crate::plist!({ + "Command": "Install", + "ClientOptions": options, + "PackagePath": package_path, + }); - self.idevice - .send_plist(plist::Value::Dictionary(command)) - .await?; + self.idevice.send_plist(command).await?; self.watch_completion(callback, state).await } @@ -220,14 +212,13 @@ impl InstallationProxyClient { let package_path = package_path.into(); let options = options.unwrap_or(plist::Value::Dictionary(Dictionary::new())); - let mut command = Dictionary::new(); - command.insert("Command".into(), "Upgrade".into()); - command.insert("ClientOptions".into(), options); - command.insert("PackagePath".into(), package_path.into()); + let command = crate::plist!({ + "Command": "Upgrade", + "ClientOptions": options, + "PackagePath": package_path, + }); - self.idevice - .send_plist(plist::Value::Dictionary(command)) - .await?; + self.idevice.send_plist(command).await?; self.watch_completion(callback, state).await } @@ -285,14 +276,13 @@ impl InstallationProxyClient { let bundle_id = bundle_id.into(); let options = options.unwrap_or(plist::Value::Dictionary(Dictionary::new())); - let mut command = Dictionary::new(); - command.insert("Command".into(), "Uninstall".into()); - command.insert("ApplicationIdentifier".into(), bundle_id.into()); - command.insert("ClientOptions".into(), options); + let command = crate::plist!({ + "Command": "Uninstall", + "ApplicationIdentifier": bundle_id, + "ClientOptions": options, + }); - self.idevice - .send_plist(plist::Value::Dictionary(command)) - .await?; + self.idevice.send_plist(command).await?; self.watch_completion(callback, state).await } @@ -317,14 +307,13 @@ impl InstallationProxyClient { ) -> Result { let options = options.unwrap_or(plist::Value::Dictionary(Dictionary::new())); - let mut command = Dictionary::new(); - command.insert("Command".into(), "CheckCapabilitiesMatch".into()); - command.insert("ClientOptions".into(), options); - command.insert("Capabilities".into(), capabilities.into()); + let command = crate::plist!({ + "Command": "CheckCapabilitiesMatch", + "ClientOptions": options, + "Capabilities": capabilities + }); - self.idevice - .send_plist(plist::Value::Dictionary(command)) - .await?; + self.idevice.send_plist(command).await?; let mut res = self.idevice.read_plist().await?; if let Some(caps) = res.remove("LookupResult").and_then(|x| x.as_boolean()) { @@ -355,13 +344,12 @@ impl InstallationProxyClient { ) -> Result, IdeviceError> { let options = options.unwrap_or(plist::Value::Dictionary(Dictionary::new())); - let mut command = Dictionary::new(); - command.insert("Command".into(), "Browse".into()); - command.insert("ClientOptions".into(), options); + let command = crate::plist!({ + "Command": "Browse", + "ClientOptions": options, + }); - self.idevice - .send_plist(plist::Value::Dictionary(command)) - .await?; + self.idevice.send_plist(command).await?; let mut values = Vec::new(); loop { diff --git a/idevice/src/services/lockdown.rs b/idevice/src/services/lockdown.rs index 3ef2133..7c07972 100644 --- a/idevice/src/services/lockdown.rs +++ b/idevice/src/services/lockdown.rs @@ -93,20 +93,13 @@ impl LockdownClient { key: Option<&str>, domain: Option<&str>, ) -> Result { - let mut request = plist::Dictionary::new(); - request.insert("Label".into(), self.idevice.label.clone().into()); - request.insert("Request".into(), "GetValue".into()); - - if let Some(key) = key { - request.insert("Key".into(), key.into()); - } - if let Some(domain) = domain { - request.insert("Domain".into(), domain.into()); - } - - self.idevice - .send_plist(plist::Value::Dictionary(request)) - .await?; + let request = crate::plist!({ + "Label": self.idevice.label.clone(), + "Request": "GetValue", + "Key":? key, + "Domain":? domain + }); + self.idevice.send_plist(request).await?; let message: plist::Dictionary = self.idevice.read_plist().await?; match message.get("Value") { Some(m) => Ok(m.to_owned()), @@ -138,19 +131,15 @@ impl LockdownClient { ) -> Result<(), IdeviceError> { let key = key.into(); - let mut req = plist::Dictionary::new(); - req.insert("Label".into(), self.idevice.label.clone().into()); - req.insert("Request".into(), "SetValue".into()); - req.insert("Key".into(), key.into()); - req.insert("Value".into(), value); + let req = crate::plist!({ + "Label": self.idevice.label.clone(), + "Request": "SetValue", + "Key": key, + "Value": value, + "Domain":? domain + }); - if let Some(domain) = domain { - req.insert("Domain".into(), domain.into()); - } - - self.idevice - .send_plist(plist::Value::Dictionary(req)) - .await?; + self.idevice.send_plist(req).await?; self.idevice.read_plist().await?; Ok(()) @@ -177,28 +166,14 @@ impl LockdownClient { return Err(IdeviceError::NoEstablishedConnection); } - let mut request = plist::Dictionary::new(); - request.insert( - "Label".to_string(), - plist::Value::String(self.idevice.label.clone()), - ); + let request = crate::plist!({ + "Label": self.idevice.label.clone(), + "Request": "StartSession", + "HostID": pairing_file.host_id.clone(), + "SystemBUID": pairing_file.system_buid.clone() - request.insert( - "Request".to_string(), - plist::Value::String("StartSession".to_string()), - ); - request.insert( - "HostID".to_string(), - plist::Value::String(pairing_file.host_id.clone()), - ); - request.insert( - "SystemBUID".to_string(), - plist::Value::String(pairing_file.system_buid.clone()), - ); - - self.idevice - .send_plist(plist::Value::Dictionary(request)) - .await?; + }); + self.idevice.send_plist(request).await?; let response = self.idevice.read_plist().await?; match response.get("EnableSessionSSL") { @@ -236,12 +211,11 @@ impl LockdownClient { identifier: impl Into, ) -> Result<(u16, bool), IdeviceError> { let identifier = identifier.into(); - let mut req = plist::Dictionary::new(); - req.insert("Request".into(), "StartService".into()); - req.insert("Service".into(), identifier.into()); - self.idevice - .send_plist(plist::Value::Dictionary(req)) - .await?; + let req = crate::plist!({ + "Request": "StartService", + "Service": identifier, + }); + self.idevice.send_plist(req).await?; let response = self.idevice.read_plist().await?; let ssl = match response.get("EnableServiceSSL") { @@ -307,37 +281,29 @@ impl LockdownClient { }; let ca = crate::ca::generate_certificates(&pub_key, None).unwrap(); - let mut pair_record = plist::Dictionary::new(); - pair_record.insert("DevicePublicKey".into(), plist::Value::Data(pub_key)); - pair_record.insert("DeviceCertificate".into(), plist::Value::Data(ca.dev_cert)); - pair_record.insert( - "HostCertificate".into(), - plist::Value::Data(ca.host_cert.clone()), - ); - pair_record.insert("HostID".into(), host_id.into()); - pair_record.insert("RootCertificate".into(), plist::Value::Data(ca.host_cert)); - pair_record.insert( - "RootPrivateKey".into(), - plist::Value::Data(ca.private_key.clone()), - ); - pair_record.insert("WiFiMACAddress".into(), wifi_mac.into()); - pair_record.insert("SystemBUID".into(), system_buid.into()); + let mut pair_record = crate::plist!(dict { + "DevicePublicKey": pub_key, + "DeviceCertificate": ca.dev_cert, + "HostCertificate": ca.host_cert.clone(), + "HostID": host_id, + "RootCertificate": ca.host_cert, + "RootPrivateKey": ca.private_key.clone(), + "WiFiMACAddress": wifi_mac, + "SystemBUID": system_buid, + }); - let mut options = plist::Dictionary::new(); - options.insert("ExtendedPairingErrors".into(), true.into()); - - let mut req = plist::Dictionary::new(); - req.insert("Label".into(), self.idevice.label.clone().into()); - req.insert("Request".into(), "Pair".into()); - req.insert( - "PairRecord".into(), - plist::Value::Dictionary(pair_record.clone()), - ); - req.insert("ProtocolVersion".into(), "2".into()); - req.insert("PairingOptions".into(), plist::Value::Dictionary(options)); + let req = crate::plist!({ + "Label": self.idevice.label.clone(), + "Request": "Pair", + "PairRecord": pair_record.clone(), + "ProtocolVersion": "2", + "PairingOptions": { + "ExtendedPairingErrors": true + } + }); loop { - self.idevice.send_plist(req.clone().into()).await?; + self.idevice.send_plist(req.clone()).await?; match self.idevice.read_plist().await { Ok(escrow) => { pair_record.insert("HostPrivateKey".into(), plist::Value::Data(ca.private_key)); diff --git a/idevice/src/services/mobile_image_mounter.rs b/idevice/src/services/mobile_image_mounter.rs index d0735bd..432acef 100644 --- a/idevice/src/services/mobile_image_mounter.rs +++ b/idevice/src/services/mobile_image_mounter.rs @@ -55,11 +55,10 @@ impl ImageMounter { /// # Errors /// Returns `IdeviceError` if communication fails or response is malformed pub async fn copy_devices(&mut self) -> Result, IdeviceError> { - let mut req = plist::Dictionary::new(); - req.insert("Command".into(), "CopyDevices".into()); - self.idevice - .send_plist(plist::Value::Dictionary(req)) - .await?; + let req = crate::plist!({ + "Command": "CopyDevices" + }); + self.idevice.send_plist(req).await?; let mut res = self.idevice.read_plist().await?; match res.remove("EntryList") { @@ -83,12 +82,11 @@ impl ImageMounter { image_type: impl Into<&str>, ) -> Result, IdeviceError> { let image_type = image_type.into(); - let mut req = plist::Dictionary::new(); - req.insert("Command".into(), "LookupImage".into()); - req.insert("ImageType".into(), image_type.into()); - self.idevice - .send_plist(plist::Value::Dictionary(req)) - .await?; + let req = crate::plist!({ + "Command": "LookupImage", + "ImageType": image_type + }); + self.idevice.send_plist(req).await?; let res = self.idevice.read_plist().await?; match res.get("ImageSignature") { @@ -152,14 +150,13 @@ impl ImageMounter { } }; - let mut req = plist::Dictionary::new(); - req.insert("Command".into(), "ReceiveBytes".into()); - req.insert("ImageType".into(), image_type.into()); - req.insert("ImageSize".into(), image_size.into()); - req.insert("ImageSignature".into(), plist::Value::Data(signature)); - self.idevice - .send_plist(plist::Value::Dictionary(req)) - .await?; + let req = crate::plist!({ + "Command": "ReceiveBytes", + "ImageType": image_type, + "ImageSize": image_size, + "ImageSignature": signature, + }); + self.idevice.send_plist(req).await?; let res = self.idevice.read_plist().await?; match res.get("Status") { @@ -210,19 +207,14 @@ impl ImageMounter { ) -> Result<(), IdeviceError> { let image_type = image_type.into(); - let mut req = plist::Dictionary::new(); - req.insert("Command".into(), "MountImage".into()); - req.insert("ImageType".into(), image_type.into()); - req.insert("ImageSignature".into(), plist::Value::Data(signature)); - if let Some(trust_cache) = trust_cache { - req.insert("ImageTrustCache".into(), plist::Value::Data(trust_cache)); - } - if let Some(info_plist) = info_plist { - req.insert("ImageInfoPlist".into(), info_plist); - } - self.idevice - .send_plist(plist::Value::Dictionary(req)) - .await?; + let req = crate::plist!({ + "Command": "MountImage", + "ImageType": image_type, + "ImageSignature": signature, + "ImageTrustCache":? trust_cache, + "ImageInfoPlist":? info_plist, + }); + self.idevice.send_plist(req).await?; let res = self.idevice.read_plist().await?; @@ -253,12 +245,11 @@ impl ImageMounter { mount_path: impl Into, ) -> Result<(), IdeviceError> { let mount_path = mount_path.into(); - let mut req = plist::Dictionary::new(); - req.insert("Command".into(), "UnmountImage".into()); - req.insert("MountPath".into(), mount_path.into()); - self.idevice - .send_plist(plist::Value::Dictionary(req)) - .await?; + let req = crate::plist!({ + "Command": "UnmountImage", + "MountPath": mount_path, + }); + self.idevice.send_plist(req).await?; let res = self.idevice.read_plist().await?; match res.get("Status") { @@ -288,14 +279,13 @@ impl ImageMounter { ) -> Result, IdeviceError> { let image_type = image_type.into(); - let mut req = plist::Dictionary::new(); - req.insert("Command".into(), "QueryPersonalizationManifest".into()); - req.insert("PersonalizedImageType".into(), image_type.clone().into()); - req.insert("ImageType".into(), image_type.into()); - req.insert("ImageSignature".into(), plist::Value::Data(signature)); - self.idevice - .send_plist(plist::Value::Dictionary(req)) - .await?; + let req = crate::plist!({ + "Command": "QueryPersonalizationManifest", + "PersonalizedImageType": image_type.clone(), + "ImageType": image_type, + "ImageSignature": signature + }); + self.idevice.send_plist(req).await?; let mut res = self.idevice.read_plist().await?; match res.remove("ImageSignature") { @@ -312,11 +302,10 @@ impl ImageMounter { /// # Errors /// Returns `IdeviceError` if query fails pub async fn query_developer_mode_status(&mut self) -> Result { - let mut req = plist::Dictionary::new(); - req.insert("Command".into(), "QueryDeveloperModeStatus".into()); - self.idevice - .send_plist(plist::Value::Dictionary(req)) - .await?; + let req = crate::plist!({ + "Command": "QueryDeveloperModeStatus" + }); + self.idevice.send_plist(req).await?; let res = self.idevice.read_plist().await?; match res.get("DeveloperModeStatus") { @@ -339,14 +328,11 @@ impl ImageMounter { &mut self, personalized_image_type: Option<&str>, ) -> Result, IdeviceError> { - let mut req = plist::Dictionary::new(); - req.insert("Command".into(), "QueryNonce".into()); - if let Some(image_type) = personalized_image_type { - req.insert("PersonalizedImageType".into(), image_type.into()); - } - self.idevice - .send_plist(plist::Value::Dictionary(req)) - .await?; + let req = crate::plist!({ + "Command": "QueryNonce", + "PersonalizedImageType":? personalized_image_type, + }); + self.idevice.send_plist(req).await?; let res = self.idevice.read_plist().await?; match res.get("PersonalizationNonce") { @@ -369,14 +355,11 @@ impl ImageMounter { &mut self, image_type: Option<&str>, ) -> Result { - let mut req = plist::Dictionary::new(); - req.insert("Command".into(), "QueryPersonalizationIdentifiers".into()); - if let Some(image_type) = image_type { - req.insert("PersonalizedImageType".into(), image_type.into()); - } - self.idevice - .send_plist(plist::Value::Dictionary(req)) - .await?; + let req = crate::plist!({ + "Command": "QueryPersonalizationIdentifiers", + "PersonalizedImageType":? image_type, + }); + self.idevice.send_plist(req).await?; let res = self.idevice.read_plist().await?; match res.get("PersonalizationIdentifiers") { @@ -390,11 +373,10 @@ impl ImageMounter { /// # Errors /// Returns `IdeviceError` if operation fails pub async fn roll_personalization_nonce(&mut self) -> Result<(), IdeviceError> { - let mut req = plist::Dictionary::new(); - req.insert("Command".into(), "RollPersonalizationNonce".into()); - self.idevice - .send_plist(plist::Value::Dictionary(req)) - .await?; + let req = crate::plist!({ + "Command": "RollPersonalizationNonce" + }); + self.idevice.send_plist(req).await?; Ok(()) } @@ -404,11 +386,10 @@ impl ImageMounter { /// # Errors /// Returns `IdeviceError` if operation fails pub async fn roll_cryptex_nonce(&mut self) -> Result<(), IdeviceError> { - let mut req = plist::Dictionary::new(); - req.insert("Command".into(), "RollCryptexNonce".into()); - self.idevice - .send_plist(plist::Value::Dictionary(req)) - .await?; + let req = crate::plist!({ + "Command": "RollCryptexNonce" + }); + self.idevice.send_plist(req).await?; Ok(()) } @@ -669,11 +650,12 @@ impl ImageMounter { } }; - let mut parameters = plist::Dictionary::new(); - parameters.insert("ApProductionMode".into(), true.into()); - parameters.insert("ApSecurityDomain".into(), 1.into()); - parameters.insert("ApSecurityMode".into(), true.into()); - parameters.insert("ApSupportsImg4".into(), true.into()); + let parameters = crate::plist!(dict { + "ApProductionMode": true, + "ApSecurityMode": 1, + "ApSecurityMode": true, + "ApSupportsImg4": true + }); for (key, manifest_item) in manifest { println!("{key}, {manifest_item:?}"); diff --git a/idevice/src/services/mobilebackup2.rs b/idevice/src/services/mobilebackup2.rs index 6fb47c3..b186710 100644 --- a/idevice/src/services/mobilebackup2.rs +++ b/idevice/src/services/mobilebackup2.rs @@ -185,31 +185,14 @@ impl RestoreOptions { } pub fn to_plist(&self) -> Dictionary { - let mut opts = Dictionary::new(); - opts.insert( - "RestoreShouldReboot".into(), - plist::Value::Boolean(self.reboot), - ); - opts.insert( - "RestoreDontCopyBackup".into(), - plist::Value::Boolean(!self.copy), - ); - opts.insert( - "RestorePreserveSettings".into(), - plist::Value::Boolean(self.preserve_settings), - ); - opts.insert( - "RestoreSystemFiles".into(), - plist::Value::Boolean(self.system_files), - ); - opts.insert( - "RemoveItemsNotRestored".into(), - plist::Value::Boolean(self.remove_items_not_restored), - ); - if let Some(pw) = &self.password { - opts.insert("Password".into(), plist::Value::String(pw.clone())); - } - opts + crate::plist!(dict { + "RestoreShouldReboot": self.reboot, + "RestoreDontCopyBackup": !self.copy, + "RestorePreserveSettings": self.preserve_settings, + "RestoreSystemFiles": self.system_files, + "RemoveItemsNotRestored": self.remove_items_not_restored, + "Password":? self.password.clone() + }) } } @@ -297,12 +280,11 @@ impl MobileBackup2Client { debug!("Starting mobilebackup2 version exchange"); // Send supported protocol versions (matching libimobiledevice) - let mut hello_dict = Dictionary::new(); - let versions = vec![plist::Value::Real(2.0), plist::Value::Real(2.1)]; - hello_dict.insert( - "SupportedProtocolVersions".into(), - plist::Value::Array(versions), - ); + let hello_dict = crate::plist!(dict { + "SupportedProtocolVersions": [ + 2.0, 2.1 + ] + }); self.send_device_link_message("Hello", Some(hello_dict)) .await?; @@ -349,25 +331,15 @@ impl MobileBackup2Client { message_name: &str, options: Option, ) -> Result<(), IdeviceError> { - // Create DLMessageProcessMessage array format - let mut message_array = Vec::new(); - message_array.push(plist::Value::String("DLMessageProcessMessage".into())); - // Create the actual message dictionary - let mut message_dict = Dictionary::new(); - message_dict.insert("MessageName".into(), message_name.into()); - - if let Some(opts) = options { - for (key, value) in opts { - message_dict.insert(key, value); - } - } - - message_array.push(plist::Value::Dictionary(message_dict)); + let message_dict = crate::plist!(dict { + "MessageName": message_name, + :, options: Option, ) -> Result<(), IdeviceError> { - let mut dict = Dictionary::new(); - if let Some(t) = target_identifier { - dict.insert("TargetIdentifier".into(), t.into()); - } - if let Some(s) = source_identifier { - dict.insert("SourceIdentifier".into(), s.into()); - } - if let Some(opts) = options { - dict.insert("Options".into(), plist::Value::Dictionary(opts)); + let dict = crate::plist!(dict { + "TargetIdentifier":? target_identifier, + "SourceIdentifier":? source_identifier, + "Options":? options, // Special cases like Unback/EnableCloudBackup are handled by caller if needed - } + }); self.send_device_link_message(request, Some(dict)).await } @@ -1090,17 +1057,10 @@ impl MobileBackup2Client { .ok_or(IdeviceError::InvalidHostID)?; self.assert_backup_exists(backup_root, source)?; - let mut dict = Dictionary::new(); - dict.insert( - "TargetIdentifier".into(), - plist::Value::String(target_udid.unwrap().to_string()), - ); - if let Some(src) = source_identifier { - dict.insert( - "SourceIdentifier".into(), - plist::Value::String(src.to_string()), - ); - } + let dict = crate::plist!(dict { + "TargetIdentifier": target_udid.unwrap(), + "SourceIdentifier":? source_identifier, + }); self.send_device_link_message("Info", Some(dict)).await?; match self.process_restore_dl_loop(backup_root).await? { @@ -1121,16 +1081,11 @@ impl MobileBackup2Client { .ok_or(IdeviceError::InvalidHostID)?; self.assert_backup_exists(backup_root, source)?; - let mut dict = Dictionary::new(); - dict.insert("MessageName".into(), plist::Value::String("List".into())); - dict.insert( - "TargetIdentifier".into(), - plist::Value::String(target_udid.unwrap().to_string()), - ); - dict.insert( - "SourceIdentifier".into(), - plist::Value::String(source.to_string()), - ); + let dict = crate::plist!(dict { + "MessageName": "List", + "TargetIdentifier": target_udid.unwrap(), + "SourceIdentifier": source, + }); self.send_device_link_message("List", Some(dict)).await?; match self.process_restore_dl_loop(backup_root).await? { @@ -1151,20 +1106,12 @@ impl MobileBackup2Client { .or(target_udid) .ok_or(IdeviceError::InvalidHostID)?; self.assert_backup_exists(backup_root, source)?; - - let mut dict = Dictionary::new(); - dict.insert( - "TargetIdentifier".into(), - plist::Value::String(target_udid.unwrap().to_string()), - ); - dict.insert("MessageName".into(), plist::Value::String("Unback".into())); - dict.insert( - "SourceIdentifier".into(), - plist::Value::String(source.to_string()), - ); - if let Some(pw) = password { - dict.insert("Password".into(), plist::Value::String(pw.to_string())); - } + let dict = crate::plist!(dict { + "TargetIdentifier": target_udid.unwrap(), + "MessageName": "Unback", + "SourceIdentifier": source, + "Password":? password + }); self.send_device_link_message("Unback", Some(dict)).await?; let _ = self.process_restore_dl_loop(backup_root).await?; Ok(()) @@ -1184,28 +1131,14 @@ impl MobileBackup2Client { .or(target_udid) .ok_or(IdeviceError::InvalidHostID)?; self.assert_backup_exists(backup_root, source)?; - - let mut dict = Dictionary::new(); - dict.insert("MessageName".into(), plist::Value::String("Extract".into())); - dict.insert( - "TargetIdentifier".into(), - plist::Value::String(target_udid.unwrap().to_string()), - ); - dict.insert( - "DomainName".into(), - plist::Value::String(domain_name.to_string()), - ); - dict.insert( - "RelativePath".into(), - plist::Value::String(relative_path.to_string()), - ); - dict.insert( - "SourceIdentifier".into(), - plist::Value::String(source.to_string()), - ); - if let Some(pw) = password { - dict.insert("Password".into(), plist::Value::String(pw.to_string())); - } + let dict = crate::plist!(dict { + "MessageName": "Extract", + "TargetIdentifier": target_udid.unwrap(), + "DomainName": domain_name, + "RelativePath": relative_path, + "SourceIdentifier": source, + "Password":? password, + }); self.send_device_link_message("Extract", Some(dict)).await?; let _ = self.process_restore_dl_loop(backup_root).await?; Ok(()) @@ -1219,21 +1152,12 @@ impl MobileBackup2Client { new: Option<&str>, ) -> Result<(), IdeviceError> { let target_udid = self.idevice.udid(); - let mut dict = Dictionary::new(); - dict.insert( - "MessageName".into(), - plist::Value::String("ChangePassword".into()), - ); - dict.insert( - "TargetIdentifier".into(), - plist::Value::String(target_udid.ok_or(IdeviceError::InvalidHostID)?.to_string()), - ); - if let Some(o) = old { - dict.insert("OldPassword".into(), plist::Value::String(o.to_string())); - } - if let Some(n) = new { - dict.insert("NewPassword".into(), plist::Value::String(n.to_string())); - } + let dict = crate::plist!(dict { + "MessageName": "ChangePassword", + "TargetIdentifier": target_udid.ok_or(IdeviceError::InvalidHostID)?, + "OldPassword":? old, + "NewPassword":? new + }); self.send_device_link_message("ChangePassword", Some(dict)) .await?; let _ = self.process_restore_dl_loop(backup_root).await?; @@ -1243,15 +1167,10 @@ impl MobileBackup2Client { /// Erase device via mobilebackup2 pub async fn erase_device_from_path(&mut self, backup_root: &Path) -> Result<(), IdeviceError> { let target_udid = self.idevice.udid(); - let mut dict = Dictionary::new(); - dict.insert( - "MessageName".into(), - plist::Value::String("EraseDevice".into()), - ); - dict.insert( - "TargetIdentifier".into(), - plist::Value::String(target_udid.ok_or(IdeviceError::InvalidHostID)?.to_string()), - ); + let dict = crate::plist!(dict { + "MessageName": "EraseDevice", + "TargetIdentifier": target_udid.ok_or(IdeviceError::InvalidHostID)? + }); self.send_device_link_message("EraseDevice", Some(dict)) .await?; let _ = self.process_restore_dl_loop(backup_root).await?; @@ -1291,10 +1210,10 @@ impl MobileBackup2Client { /// Returns `IdeviceError` if disconnection fails pub async fn disconnect(&mut self) -> Result<(), IdeviceError> { // Send DLMessageDisconnect array per DeviceLink protocol - let arr = vec![ - plist::Value::String("DLMessageDisconnect".into()), - plist::Value::String("___EmptyParameterString___".into()), - ]; + let arr = crate::plist!(array [ + "DLMessageDisconnect", + "___EmptyParameterString___" + ]); self.send_dl_array(arr).await?; debug!("Disconnected from backup service"); Ok(()) diff --git a/idevice/src/services/os_trace_relay.rs b/idevice/src/services/os_trace_relay.rs index 793f80a..9fb07d3 100644 --- a/idevice/src/services/os_trace_relay.rs +++ b/idevice/src/services/os_trace_relay.rs @@ -4,7 +4,6 @@ //! https://github.com/doronz88/pymobiledevice3/blob/master/pymobiledevice3/services/os_trace.py use chrono::{DateTime, NaiveDateTime}; -use plist::Dictionary; use tokio::io::AsyncWriteExt; use crate::{Idevice, IdeviceError, IdeviceService, obf}; @@ -70,15 +69,14 @@ impl OsTraceRelayClient { Some(p) => p as i64, None => -1, }; - let mut req = Dictionary::new(); - req.insert("Request".into(), "StartActivity".into()); - req.insert("Pid".into(), Into::into(pid)); - req.insert("MessageFilter".into(), Into::into(65_535)); - req.insert("StreamFlags".into(), Into::into(60)); + let req = crate::plist!({ + "Request": "StartActivity", + "Pid": pid, + "MessageFilter": 65_535, + "StreamFlags": 60 + }); - self.idevice - .send_bplist(plist::Value::Dictionary(req)) - .await?; + self.idevice.send_bplist(req).await?; // Read a single byte self.idevice.read_raw(1).await?; @@ -100,12 +98,11 @@ impl OsTraceRelayClient { /// Get the list of available PIDs pub async fn get_pid_list(&mut self) -> Result, IdeviceError> { - let mut req = Dictionary::new(); - req.insert("Request".into(), "PidList".into()); + let req = crate::plist!({ + "Request": "PidList" + }); - self.idevice - .send_bplist(plist::Value::Dictionary(req)) - .await?; + self.idevice.send_bplist(req).await?; // Read a single byte self.idevice.read_raw(1).await?; @@ -133,24 +130,14 @@ impl OsTraceRelayClient { age_limit: Option, start_time: Option, ) -> Result<(), IdeviceError> { - let mut req = Dictionary::new(); - req.insert("Request".into(), "CreateArchive".into()); + let req = crate::plist!({ + "Request": "CreateArchive", + "SizeLimit":? size_limit, + "AgeLimit":? age_limit, + "StartTime":? start_time, + }); - if let Some(size) = size_limit { - req.insert("SizeLimit".into(), size.into()); - } - - if let Some(age) = age_limit { - req.insert("AgeLimit".into(), age.into()); - } - - if let Some(time) = start_time { - req.insert("StartTime".into(), time.into()); - } - - self.idevice - .send_bplist(plist::Value::Dictionary(req)) - .await?; + self.idevice.send_bplist(req).await?; // Read a single byte if self.idevice.read_raw(1).await?[0] != 1 { diff --git a/idevice/src/services/restore_service.rs b/idevice/src/services/restore_service.rs index b2f4590..1f3dd73 100644 --- a/idevice/src/services/restore_service.rs +++ b/idevice/src/services/restore_service.rs @@ -35,12 +35,11 @@ impl RestoreServiceClient { /// Enter recovery pub async fn enter_recovery(&mut self) -> Result<(), IdeviceError> { - let mut req = Dictionary::new(); - req.insert("command".into(), "recovery".into()); + let req = crate::plist!({ + "command": "recovery" + }); - self.stream - .send_object(plist::Value::Dictionary(req), true) - .await?; + self.stream.send_object(req, true).await?; let res = self.stream.recv().await?; let mut res = match res { @@ -69,12 +68,10 @@ impl RestoreServiceClient { /// Reboot pub async fn reboot(&mut self) -> Result<(), IdeviceError> { - let mut req = Dictionary::new(); - req.insert("command".into(), "reboot".into()); - - self.stream - .send_object(plist::Value::Dictionary(req), true) - .await?; + let req = crate::plist!({ + "command": "reboot" + }); + self.stream.send_object(req, true).await?; let res = self.stream.recv().await?; let mut res = match res { @@ -103,12 +100,10 @@ impl RestoreServiceClient { /// Get preflightinfo pub async fn get_preflightinfo(&mut self) -> Result { - let mut req = Dictionary::new(); - req.insert("command".into(), "getpreflightinfo".into()); - - self.stream - .send_object(plist::Value::Dictionary(req), true) - .await?; + let req = crate::plist!({ + "command": "getpreflightinfo" + }); + self.stream.send_object(req, true).await?; let res = self.stream.recv().await?; let mut res = match res { @@ -133,12 +128,10 @@ impl RestoreServiceClient { /// Get nonces /// Doesn't seem to work pub async fn get_nonces(&mut self) -> Result { - let mut req = Dictionary::new(); - req.insert("command".into(), "getnonces".into()); - - self.stream - .send_object(plist::Value::Dictionary(req), true) - .await?; + let req = crate::plist!({ + "command": "getnonces" + }); + self.stream.send_object(req, true).await?; let res = self.stream.recv().await?; let mut res = match res { @@ -163,12 +156,10 @@ impl RestoreServiceClient { /// Get app parameters /// Doesn't seem to work pub async fn get_app_parameters(&mut self) -> Result { - let mut req = Dictionary::new(); - req.insert("command".into(), "getappparameters".into()); - - self.stream - .send_object(plist::Value::Dictionary(req), true) - .await?; + let req = crate::plist!({ + "command": "getappparameters" + }); + self.stream.send_object(req, true).await?; let res = self.stream.recv().await?; let mut res = match res { @@ -195,13 +186,11 @@ impl RestoreServiceClient { pub async fn restore_lang(&mut self, language: impl Into) -> Result<(), IdeviceError> { let language = language.into(); - let mut req = Dictionary::new(); - req.insert("command".into(), "restorelang".into()); - req.insert("argument".into(), language.into()); - - self.stream - .send_object(plist::Value::Dictionary(req), true) - .await?; + let req = crate::plist!({ + "command": "restorelang", + "argument": language, + }); + self.stream.send_object(req, true).await?; let res = self.stream.recv().await?; let mut res = match res { diff --git a/idevice/src/services/springboardservices.rs b/idevice/src/services/springboardservices.rs index f115f30..e8edfec 100644 --- a/idevice/src/services/springboardservices.rs +++ b/idevice/src/services/springboardservices.rs @@ -57,12 +57,11 @@ impl SpringBoardServicesClient { &mut self, bundle_identifier: String, ) -> Result, IdeviceError> { - let mut req = plist::Dictionary::new(); - req.insert("command".into(), "getIconPNGData".into()); - req.insert("bundleId".into(), bundle_identifier.into()); - self.idevice - .send_plist(plist::Value::Dictionary(req)) - .await?; + let req = crate::plist!({ + "command": "getIconPNGData", + "bundleId": bundle_identifier, + }); + self.idevice.send_plist(req).await?; let mut res = self.idevice.read_plist().await?; match res.remove("pngData") { From 6ce6777479af3c1b921a25a73ffa23d6f679180f Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Wed, 20 Aug 2025 12:41:09 -0600 Subject: [PATCH 086/126] Read IP packets outside of tokio::select to avoid cancel --- idevice/src/tcp/adapter.rs | 108 ++++++++++++++++--------------- idevice/src/tcp/handle.rs | 126 ++++++++++++++++++++++++------------- idevice/src/tcp/packets.rs | 52 ++++++++++----- 3 files changed, 175 insertions(+), 111 deletions(-) diff --git a/idevice/src/tcp/adapter.rs b/idevice/src/tcp/adapter.rs index c8d85f1..287b59f 100644 --- a/idevice/src/tcp/adapter.rs +++ b/idevice/src/tcp/adapter.rs @@ -112,7 +112,7 @@ impl ConnectionState { #[derive(Debug)] pub struct Adapter { /// The underlying transport connection - peer: Box, + pub(crate) peer: Box, /// The local IP address host_ip: IpAddr, /// The remote peer's IP address @@ -538,63 +538,67 @@ impl Adapter { } pub(crate) async fn process_tcp_packet(&mut self) -> Result<(), std::io::Error> { - loop { - let ip_packet = self.read_ip_packet().await?; - let res = TcpPacket::parse(&ip_packet)?; - let mut ack_me = None; + let ip_packet = self.read_ip_packet().await?; + self.process_tcp_packet_from_payload(&ip_packet).await + } - if let Some(state) = self.states.get(&res.destination_port) { - // A keep-alive probe: ACK set, no payload, and seq == RCV.NXT - 1 - let is_keepalive = res.flags.ack - && res.payload.is_empty() - && res.sequence_number.wrapping_add(1) == state.ack; + pub(crate) async fn process_tcp_packet_from_payload( + &mut self, + payload: &[u8], + ) -> Result<(), std::io::Error> { + let res = TcpPacket::parse(payload)?; + let mut ack_me = None; - if is_keepalive { - // Don't update any seq/ack state; just ACK what we already expect. - debug!("responding to keep-alive probe"); - let port = res.destination_port; - self.ack(port).await?; - break; - } + if let Some(state) = self.states.get(&res.destination_port) { + // A keep-alive probe: ACK set, no payload, and seq == RCV.NXT - 1 + let is_keepalive = res.flags.ack + && res.payload.is_empty() + && res.sequence_number.wrapping_add(1) == state.ack; + + if is_keepalive { + // Don't update any seq/ack state; just ACK what we already expect. + debug!("responding to keep-alive probe"); + let port = res.destination_port; + self.ack(port).await?; + return Ok(()); + } + } + + if let Some(state) = self.states.get_mut(&res.destination_port) { + if state.peer_seq > res.sequence_number { + // ignore retransmission + return Ok(()); } - if let Some(state) = self.states.get_mut(&res.destination_port) { - if state.peer_seq > res.sequence_number { - // ignore retransmission - continue; - } - - state.peer_seq = res.sequence_number + res.payload.len() as u32; - state.ack = res.sequence_number - + if res.payload.is_empty() && state.status != ConnectionStatus::Connected { - 1 - } else { - res.payload.len() as u32 - }; - if res.flags.psh || !res.payload.is_empty() { - ack_me = Some(res.destination_port); - state.read_buffer.extend(res.payload); - } - if res.flags.rst { - warn!("stream rst"); - state.status = ConnectionStatus::Error(ErrorKind::ConnectionReset); - } - if res.flags.fin { - ack_me = Some(res.destination_port); - state.status = ConnectionStatus::Error(ErrorKind::UnexpectedEof); - } - if res.flags.syn && res.flags.ack { - ack_me = Some(res.destination_port); - state.seq = state.seq.wrapping_add(1); - state.status = ConnectionStatus::Connected; - } + state.peer_seq = res.sequence_number + res.payload.len() as u32; + state.ack = res.sequence_number + + if res.payload.is_empty() && state.status != ConnectionStatus::Connected { + 1 + } else { + res.payload.len() as u32 + }; + if res.flags.psh || !res.payload.is_empty() { + ack_me = Some(res.destination_port); + state.read_buffer.extend(res.payload); } - - // we have to ack outside of the mutable state borrow - if let Some(a) = ack_me { - self.ack(a).await?; + if res.flags.rst { + warn!("stream rst"); + state.status = ConnectionStatus::Error(ErrorKind::ConnectionReset); } - break; + if res.flags.fin { + ack_me = Some(res.destination_port); + state.status = ConnectionStatus::Error(ErrorKind::UnexpectedEof); + } + if res.flags.syn && res.flags.ack { + ack_me = Some(res.destination_port); + state.seq = state.seq.wrapping_add(1); + state.status = ConnectionStatus::Connected; + } + } + + // we have to ack outside of the mutable state borrow + if let Some(a) = ack_me { + self.ack(a).await?; } Ok(()) } diff --git a/idevice/src/tcp/handle.rs b/idevice/src/tcp/handle.rs index 3434f9f..b7049a9 100644 --- a/idevice/src/tcp/handle.rs +++ b/idevice/src/tcp/handle.rs @@ -8,13 +8,16 @@ use std::{collections::HashMap, path::PathBuf, sync::Mutex, task::Poll}; use crossfire::{AsyncRx, MTx, Tx, mpsc, spsc, stream::AsyncStream}; use futures::{StreamExt, stream::FuturesUnordered}; -use log::trace; +use log::{debug, trace}; use tokio::{ - io::{AsyncRead, AsyncWrite}, + io::{AsyncRead, AsyncReadExt, AsyncWrite}, sync::oneshot, }; -use crate::tcp::adapter::ConnectionStatus; +use crate::tcp::{ + adapter::ConnectionStatus, + packets::{IpParseError, Ipv6Packet}, +}; pub type ConnectToPortRes = oneshot::Sender, std::io::Error>>), std::io::Error>>; @@ -50,6 +53,9 @@ impl AdapterHandle { tokio::spawn(async move { let mut handles: HashMap, std::io::Error>>> = HashMap::new(); let mut tick = tokio::time::interval(std::time::Duration::from_millis(1)); + + let mut read_buf = [0u8; 4096]; + let mut bytes_in_buf = 0; loop { tokio::select! { // check for messages for us @@ -96,53 +102,85 @@ impl AdapterHandle { } } - r = adapter.process_tcp_packet() => { - if let Err(e) = r { - // propagate error to all streams; close them + result = adapter.peer.read(&mut read_buf[bytes_in_buf..]) => { + match result { + Ok(0) => { + debug!("Underlying stream closed (EOF)"); + break; // Exit the main actor loop + } + Ok(s) => { + bytes_in_buf += s; + loop { + match Ipv6Packet::parse(&read_buf[..bytes_in_buf]) { + IpParseError::Ok { packet, bytes_consumed } => { + // We got a full packet! Process it. + if let Err(e) = adapter.process_tcp_packet_from_payload(&packet.payload).await { + debug!("CRITICAL: Failed to process IP packet: {e:?}"); + } + + // And remove it from the buffer by shifting the remaining bytes + read_buf.copy_within(bytes_consumed..bytes_in_buf, 0); + bytes_in_buf -= bytes_consumed; + // Push any newly available bytes to per-conn channels + let mut dead = Vec::new(); + for (&hp, tx) in &handles { + match adapter.uncache_all(hp) { + Ok(buf) if !buf.is_empty() => { + if tx.send(Ok(buf)).is_err() { + dead.push(hp); + } + } + Err(e) => { + let _ = tx.send(Err(e)); + dead.push(hp); + } + _ => {} + } + } + for hp in dead { + handles.remove(&hp); + let _ = adapter.close(hp).await; + } + + let mut to_close = Vec::new(); + for (&hp, tx) in &handles { + if let Ok(ConnectionStatus::Error(kind)) = adapter.get_status(hp) { + if kind == std::io::ErrorKind::UnexpectedEof { + to_close.push(hp); + } else { + let _ = tx.send(Err(std::io::Error::from(kind))); + to_close.push(hp); + } + } + } + for hp in to_close { + handles.remove(&hp); + // Best-effort close. For RST this just tidies state on our side + let _ = adapter.close(hp).await; + } + } + IpParseError::NotEnough => { + // Buffer doesn't have a full packet, wait for the next read + break; + } + IpParseError::Invalid => { + // Corrupted data, close the connection + // ... (error handling) ... + return; + } + } + } + + } + Err(e) => { + debug!("Failed to read: {e:?}, closing stack"); for (hp, tx) in handles.drain() { let _ = tx.send(Err(e.kind().into())); // or clone/convert let _ = adapter.close(hp).await; } - break; - } - - // Push any newly available bytes to per-conn channels - let mut dead = Vec::new(); - for (&hp, tx) in &handles { - match adapter.uncache_all(hp) { - Ok(buf) if !buf.is_empty() => { - if tx.send(Ok(buf)).is_err() { - dead.push(hp); - } - } - Err(e) => { - let _ = tx.send(Err(e)); - dead.push(hp); - } - _ => {} + break; } } - for hp in dead { - handles.remove(&hp); - let _ = adapter.close(hp).await; - } - - let mut to_close = Vec::new(); - for (&hp, tx) in &handles { - if let Ok(ConnectionStatus::Error(kind)) = adapter.get_status(hp) { - if kind == std::io::ErrorKind::UnexpectedEof { - to_close.push(hp); - } else { - let _ = tx.send(Err(std::io::Error::from(kind))); - to_close.push(hp); - } - } - } - for hp in to_close { - handles.remove(&hp); - // Best-effort close. For RST this just tidies state on our side - let _ = adapter.close(hp).await; - } } _ = tick.tick() => { diff --git a/idevice/src/tcp/packets.rs b/idevice/src/tcp/packets.rs index fa9e94f..0186ebe 100644 --- a/idevice/src/tcp/packets.rs +++ b/idevice/src/tcp/packets.rs @@ -6,6 +6,7 @@ use std::{ sync::Arc, }; +use log::debug; use tokio::{ io::{AsyncRead, AsyncReadExt}, sync::Mutex, @@ -109,6 +110,7 @@ impl Ipv4Packet { let ihl = (version_ihl & 0x0F) * 4; if version != 4 || ihl < 20 { + debug!("Got an invalid IPv4 header"); return Err(std::io::Error::new( std::io::ErrorKind::InvalidData, "Invalid IPv4 header", @@ -220,21 +222,34 @@ pub struct Ipv6Packet { pub payload: Vec, } +#[derive(Debug, Clone)] +pub(crate) enum IpParseError { + Ok { packet: T, bytes_consumed: usize }, + NotEnough, + Invalid, +} + impl Ipv6Packet { - pub fn parse(packet: &[u8]) -> Option { + pub(crate) fn parse(packet: &[u8]) -> IpParseError { if packet.len() < 40 { - return None; + return IpParseError::NotEnough; } let version = packet[0] >> 4; if version != 6 { - return None; + return IpParseError::Invalid; } let traffic_class = ((packet[0] & 0x0F) << 4) | (packet[1] >> 4); let flow_label = ((packet[1] as u32 & 0x0F) << 16) | ((packet[2] as u32) << 8) | packet[3] as u32; let payload_length = u16::from_be_bytes([packet[4], packet[5]]); + let total_packet_len = 40 + payload_length as usize; + + if packet.len() < total_packet_len { + return IpParseError::NotEnough; + } + let next_header = packet[6]; let hop_limit = packet[7]; let source = Ipv6Addr::new( @@ -258,19 +273,22 @@ impl Ipv6Packet { u16::from_be_bytes([packet[36], packet[37]]), u16::from_be_bytes([packet[38], packet[39]]), ); - let payload = packet[40..].to_vec(); + let payload = packet[40..total_packet_len].to_vec(); - Some(Self { - version, - traffic_class, - flow_label, - payload_length, - next_header, - hop_limit, - source, - destination, - payload, - }) + IpParseError::Ok { + packet: Self { + version, + traffic_class, + flow_label, + payload_length, + next_header, + hop_limit, + source, + destination, + payload, + }, + bytes_consumed: total_packet_len, + } } pub async fn from_reader( @@ -278,14 +296,17 @@ impl Ipv6Packet { log: &Option>>, ) -> Result { let mut log_packet = Vec::new(); + let mut header = [0u8; 40]; // IPv6 header size is fixed at 40 bytes reader.read_exact(&mut header).await?; + if log.is_some() { log_packet.extend_from_slice(&header); } let version = header[0] >> 4; if version != 6 { + debug!("Got an invalid IPv6 header"); return Err(std::io::Error::new( std::io::ErrorKind::InvalidData, "Invalid IPv6 header", @@ -457,6 +478,7 @@ pub struct TcpPacket { impl TcpPacket { pub fn parse(packet: &[u8]) -> Result { if packet.len() < 20 { + debug!("Got an invalid TCP header"); return Err(std::io::Error::new( std::io::ErrorKind::InvalidData, "Not enough bytes for TCP header", From 2a90f926ca8b334e9139db7a1fd75782f03c5dbf Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Wed, 20 Aug 2025 12:42:40 -0600 Subject: [PATCH 087/126] Implement FFI object stack --- cpp/include/idevice++/tcp_object_stack.hpp | 175 +++++++++++++++++ cpp/src/tcp_callback_feeder.cpp | 113 +++++++++++ ffi/src/lib.rs | 2 + ffi/src/tcp_object_stack.rs | 217 +++++++++++++++++++++ 4 files changed, 507 insertions(+) create mode 100644 cpp/include/idevice++/tcp_object_stack.hpp create mode 100644 cpp/src/tcp_callback_feeder.cpp create mode 100644 ffi/src/tcp_object_stack.rs diff --git a/cpp/include/idevice++/tcp_object_stack.hpp b/cpp/include/idevice++/tcp_object_stack.hpp new file mode 100644 index 0000000..d4e1363 --- /dev/null +++ b/cpp/include/idevice++/tcp_object_stack.hpp @@ -0,0 +1,175 @@ +// Jackson Coxson + +#pragma once +#include +#include +#include +#include +#include + +#include +#include + +namespace IdeviceFFI { + +// ---------------- OwnedBuffer: RAII for zero-copy read buffers ---------------- +class OwnedBuffer { + public: + OwnedBuffer() noexcept : p_(nullptr), n_(0) {} + OwnedBuffer(const OwnedBuffer&) = delete; + OwnedBuffer& operator=(const OwnedBuffer&) = delete; + + OwnedBuffer(OwnedBuffer&& o) noexcept : p_(o.p_), n_(o.n_) { + o.p_ = nullptr; + o.n_ = 0; + } + OwnedBuffer& operator=(OwnedBuffer&& o) noexcept { + if (this != &o) { + reset(); + p_ = o.p_; + n_ = o.n_; + o.p_ = nullptr; + o.n_ = 0; + } + return *this; + } + + ~OwnedBuffer() { reset(); } + + const uint8_t* data() const noexcept { return p_; } + uint8_t* data() noexcept { return p_; } + std::size_t size() const noexcept { return n_; } + bool empty() const noexcept { return n_ == 0; } + + void reset() noexcept { + if (p_) { + ::idevice_data_free(p_, n_); + p_ = nullptr; + n_ = 0; + } + } + + private: + friend class TcpObjectStackEater; + void adopt(uint8_t* p, std::size_t n) noexcept { + reset(); + p_ = p; + n_ = n; + } + + uint8_t* p_; + std::size_t n_; +}; + +// ---------------- TcpFeeder: push inbound IP packets into the stack ---------- +class TcpObjectStackFeeder { + public: + TcpObjectStackFeeder() = default; + TcpObjectStackFeeder(const TcpObjectStackFeeder&) = delete; + TcpObjectStackFeeder& operator=(const TcpObjectStackFeeder&) = delete; + + TcpObjectStackFeeder(TcpObjectStackFeeder&& o) noexcept : h_(o.h_) { o.h_ = nullptr; } + TcpObjectStackFeeder& operator=(TcpObjectStackFeeder&& o) noexcept { + if (this != &o) { + reset(); + h_ = o.h_; + o.h_ = nullptr; + } + return *this; + } + + ~TcpObjectStackFeeder() { reset(); } + + bool write(const uint8_t* data, std::size_t len, FfiError& err) const; + ::TcpFeedObject* raw() const { return h_; } + + private: + friend class TcpObjectStack; + explicit TcpObjectStackFeeder(::TcpFeedObject* h) : h_(h) {} + + void reset() { + if (h_) { + ::idevice_free_tcp_feed_object(h_); + h_ = nullptr; + } + } + + ::TcpFeedObject* h_ = nullptr; +}; + +// ---------------- TcpEater: blocking read of outbound packets ---------------- +class TcpObjectStackEater { + public: + TcpObjectStackEater() = default; + TcpObjectStackEater(const TcpObjectStackEater&) = delete; + TcpObjectStackEater& operator=(const TcpObjectStackEater&) = delete; + + TcpObjectStackEater(TcpObjectStackEater&& o) noexcept : h_(o.h_) { o.h_ = nullptr; } + TcpObjectStackEater& operator=(TcpObjectStackEater&& o) noexcept { + if (this != &o) { + reset(); + h_ = o.h_; + o.h_ = nullptr; + } + return *this; + } + + ~TcpObjectStackEater() { reset(); } + + // Blocks until a packet is available. On success, 'out' adopts the buffer + // and you must keep 'out' alive until done (RAII frees via idevice_data_free). + bool read(OwnedBuffer& out, FfiError& err) const; + + ::TcpEatObject* raw() const { return h_; } + + private: + friend class TcpObjectStack; + explicit TcpObjectStackEater(::TcpEatObject* h) : h_(h) {} + + void reset() { + if (h_) { + ::idevice_free_tcp_eat_object(h_); + h_ = nullptr; + } + } + + ::TcpEatObject* h_ = nullptr; +}; + +// ---------------- Stack builder: returns feeder + eater + adapter ------------ +class TcpObjectStack { + public: + TcpObjectStack() = default; + TcpObjectStack(const TcpObjectStack&) = delete; // no sharing + TcpObjectStack& operator=(const TcpObjectStack&) = delete; + TcpObjectStack(TcpObjectStack&&) noexcept = default; // movable + TcpObjectStack& operator=(TcpObjectStack&&) noexcept = default; + + // Build the stack (dual-handle). Name kept to minimize churn. + static std::optional + create(const std::string& our_ip, const std::string& their_ip, FfiError& err); + + TcpObjectStackFeeder& feeder(); + const TcpObjectStackFeeder& feeder() const; + + TcpObjectStackEater& eater(); + const TcpObjectStackEater& eater() const; + + Adapter& adapter(); + const Adapter& adapter() const; + + std::optional release_feeder(); // nullptr inside wrapper after call + std::optional release_eater(); // nullptr inside wrapper after call + std::optional release_adapter(); + + private: + struct Impl { + TcpObjectStackFeeder feeder; + TcpObjectStackEater eater; + std::optional adapter; + }; + // Unique ownership so there’s a single point of truth to release from + std::unique_ptr impl_; +}; + +} // namespace IdeviceFFI diff --git a/cpp/src/tcp_callback_feeder.cpp b/cpp/src/tcp_callback_feeder.cpp new file mode 100644 index 0000000..9967554 --- /dev/null +++ b/cpp/src/tcp_callback_feeder.cpp @@ -0,0 +1,113 @@ +// Jackson Coxson + +#include +#include + +namespace IdeviceFFI { + +// ---------- TcpFeeder ---------- +bool TcpObjectStackFeeder::write(const uint8_t* data, std::size_t len, FfiError& err) const { + if (IdeviceFfiError* e = ::idevice_tcp_feed_object_write(h_, data, len)) { + err = FfiError(e); + return false; + } + return true; +} + +// ---------- TcpEater ---------- +bool TcpObjectStackEater::read(OwnedBuffer& out, FfiError& err) const { + uint8_t* ptr = nullptr; + std::size_t len = 0; + if (IdeviceFfiError* e = ::idevice_tcp_eat_object_read(h_, &ptr, &len)) { + err = FfiError(e); + return false; + } + // Success: adopt the buffer (freed via idevice_data_free in OwnedBuffer dtor) + out.adopt(ptr, len); + return true; +} + +// ---------- TcpStackFromCallback ---------- +std::optional +TcpObjectStack::create(const std::string& our_ip, const std::string& their_ip, FfiError& err) { + ::TcpFeedObject* feeder_h = nullptr; + ::TcpEatObject* eater_h = nullptr; + ::AdapterHandle* adapter_h = nullptr; + + if (IdeviceFfiError* e = ::idevice_tcp_stack_into_sync_objects( + our_ip.c_str(), their_ip.c_str(), &feeder_h, &eater_h, &adapter_h)) { + err = FfiError(e); + return std::nullopt; + } + + auto impl = std::make_unique(); + impl->feeder = TcpObjectStackFeeder(feeder_h); + impl->eater = TcpObjectStackEater(eater_h); + impl->adapter = Adapter::adopt(adapter_h); + + TcpObjectStack out; + out.impl_ = std::move(impl); + return out; +} + +TcpObjectStackFeeder& TcpObjectStack::feeder() { + return impl_->feeder; +} +const TcpObjectStackFeeder& TcpObjectStack::feeder() const { + return impl_->feeder; +} + +TcpObjectStackEater& TcpObjectStack::eater() { + return impl_->eater; +} +const TcpObjectStackEater& TcpObjectStack::eater() const { + return impl_->eater; +} + +Adapter& TcpObjectStack::adapter() { + if (!impl_ || !impl_->adapter) { + static Adapter* never = nullptr; + return *never; + } + return *(impl_->adapter); +} +const Adapter& TcpObjectStack::adapter() const { + if (!impl_ || !impl_->adapter) { + static Adapter* never = nullptr; + return *never; + } + return *(impl_->adapter); +} + +// ---------- Release APIs ---------- +std::optional TcpObjectStack::release_feeder() { + if (!impl_) + return std::nullopt; + auto has = impl_->feeder.raw() != nullptr; + if (!has) + return std::nullopt; + TcpObjectStackFeeder out = std::move(impl_->feeder); + // impl_->feeder is now empty (h_ == nullptr) thanks to move + return std::optional(std::move(out)); +} + +std::optional TcpObjectStack::release_eater() { + if (!impl_) + return std::nullopt; + auto has = impl_->eater.raw() != nullptr; + if (!has) + return std::nullopt; + TcpObjectStackEater out = std::move(impl_->eater); + return std::optional(std::move(out)); +} + +std::optional TcpObjectStack::release_adapter() { + if (!impl_ || !impl_->adapter) + return std::nullopt; + // Move out and clear our optional + auto out = std::move(*(impl_->adapter)); + impl_->adapter.reset(); + return std::optional(std::move(out)); +} + +} // namespace IdeviceFFI diff --git a/ffi/src/lib.rs b/ffi/src/lib.rs index 9bd1833..f5738ab 100644 --- a/ffi/src/lib.rs +++ b/ffi/src/lib.rs @@ -39,6 +39,8 @@ pub mod rsd; pub mod springboardservices; #[cfg(feature = "syslog_relay")] pub mod syslog_relay; +#[cfg(feature = "tunnel_tcp_stack")] +pub mod tcp_object_stack; #[cfg(feature = "usbmuxd")] pub mod usbmuxd; pub mod util; diff --git a/ffi/src/tcp_object_stack.rs b/ffi/src/tcp_object_stack.rs new file mode 100644 index 0000000..70e844c --- /dev/null +++ b/ffi/src/tcp_object_stack.rs @@ -0,0 +1,217 @@ +//! Just to make things more complicated, some setups need an IP input from FFI. Or maybe a packet +//! input that is sync only. This is a stupid simple shim between callbacks and an input for the +//! legendary idevice TCP stack. + +use std::{ + ffi::{CStr, c_char, c_void}, + ptr::null_mut, + sync::Arc, +}; + +use log::debug; +use tokio::sync::Mutex; +use tokio::{ + io::AsyncWriteExt, + net::tcp::{OwnedReadHalf, OwnedWriteHalf}, +}; + +use crate::{IdeviceFfiError, RUNTIME, core_device_proxy::AdapterHandle, ffi_err}; + +pub struct TcpFeedObject { + sender: Arc>, +} +pub struct TcpEatObject { + receiver: Arc>, +} + +#[repr(transparent)] +#[derive(Clone)] +pub struct UserContext(*mut c_void); +unsafe impl Send for UserContext {} +unsafe impl Sync for UserContext {} + +/// # Safety +/// Pass valid pointers. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn idevice_tcp_stack_into_sync_objects( + our_ip: *const c_char, + their_ip: *const c_char, + feeder: *mut *mut TcpFeedObject, // feed the TCP stack with IP packets + tcp_receiver: *mut *mut TcpEatObject, + adapter_handle: *mut *mut AdapterHandle, // this object can be used throughout the rest of the + // idevice ecosystem +) -> *mut IdeviceFfiError { + if our_ip.is_null() || their_ip.is_null() || feeder.is_null() || adapter_handle.is_null() { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + + let our_ip = unsafe { CStr::from_ptr(our_ip) } + .to_string_lossy() + .to_string(); + let our_ip = match our_ip.parse::() { + Ok(o) => o, + Err(_) => { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + }; + let their_ip = unsafe { CStr::from_ptr(their_ip) } + .to_string_lossy() + .to_string(); + let their_ip = match their_ip.parse::() { + Ok(o) => o, + Err(_) => { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + }; + + let res = RUNTIME.block_on(async { + let mut port = 4000; + loop { + if port > 4050 { + return None; + } + let listener = match tokio::net::TcpListener::bind(format!("127.0.0.1:{port}")).await { + Ok(l) => l, + Err(_) => { + port += 1; + continue; + } + }; + + let stream = tokio::net::TcpStream::connect(format!("127.0.0.1:{port}")) + .await + .ok()?; + stream.set_nodelay(true).ok()?; + let (stream2, _) = listener.accept().await.ok()?; + stream2.set_nodelay(true).ok()?; + break Some((stream, stream2)); + } + }); + + let (stream, stream2) = match res { + Some(x) => x, + None => { + return ffi_err!(IdeviceError::NoEstablishedConnection); + } + }; + + let (r, w) = stream2.into_split(); + let w = Arc::new(Mutex::new(w)); + let r = Arc::new(Mutex::new(r)); + + // let w = Arc::new(Mutex::new(stream2)); + // let r = w.clone(); + + let feed_object = TcpFeedObject { sender: w }; + let eat_object = TcpEatObject { receiver: r }; + + // we must be inside the runtime for the inner function to spawn threads + let new_adapter = RUNTIME.block_on(async { + idevice::tcp::adapter::Adapter::new(Box::new(stream), our_ip, their_ip).to_async_handle() + }); + // this object can now be used with the rest of the idevice FFI library + + unsafe { + *feeder = Box::into_raw(Box::new(feed_object)); + *tcp_receiver = Box::into_raw(Box::new(eat_object)); + *adapter_handle = Box::into_raw(Box::new(AdapterHandle(new_adapter))); + } + + null_mut() +} + +/// Feed the TCP stack with data +/// # Safety +/// Pass valid pointers. Data is cloned out of slice. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn idevice_tcp_feed_object_write( + object: *mut TcpFeedObject, + data: *const u8, + len: usize, +) -> *mut IdeviceFfiError { + if object.is_null() || data.is_null() { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + let object = unsafe { &mut *object }; + let data = unsafe { std::slice::from_raw_parts(data, len) }; + RUNTIME.block_on(async move { + let mut lock = object.sender.lock().await; + match lock.write_all(data).await { + Ok(_) => { + lock.flush().await.ok(); + null_mut() + } + Err(e) => { + ffi_err!(IdeviceError::Socket(std::io::Error::new( + std::io::ErrorKind::BrokenPipe, + format!("could not send: {e:?}") + ))) + } + } + }) +} + +/// Block on getting a block of data to write to the underlying stream. +/// Write this to the stream as is, and free the data with idevice_data_free +/// +/// # Safety +/// Pass valid pointers +#[unsafe(no_mangle)] +pub unsafe extern "C" fn idevice_tcp_eat_object_read( + object: *mut TcpEatObject, + data: *mut *mut u8, + len: *mut usize, +) -> *mut IdeviceFfiError { + let object = unsafe { &mut *object }; + let mut buf = [0; 2048]; + RUNTIME.block_on(async { + let lock = object.receiver.lock().await; + match lock.try_read(&mut buf) { + Ok(size) => { + debug!("EATING"); + let bytes = buf[..size].to_vec(); + let mut res = bytes.into_boxed_slice(); + unsafe { + *len = res.len(); + *data = res.as_mut_ptr(); + } + std::mem::forget(res); + std::ptr::null_mut() + } + Err(e) => match e.kind() { + std::io::ErrorKind::WouldBlock => { + unsafe { + *len = 0; + } + std::ptr::null_mut() + } + _ => { + ffi_err!(IdeviceError::Socket(std::io::Error::new( + std::io::ErrorKind::BrokenPipe, + "channel closed" + ))) + } + }, + } + }) +} + +/// # Safety +/// Pass a valid pointer allocated by this library +#[unsafe(no_mangle)] +pub unsafe extern "C" fn idevice_free_tcp_feed_object(object: *mut TcpFeedObject) { + if object.is_null() { + return; + } + let _ = unsafe { Box::from_raw(object) }; +} + +/// # Safety +/// Pass a valid pointer allocated by this library +#[unsafe(no_mangle)] +pub unsafe extern "C" fn idevice_free_tcp_eat_object(object: *mut TcpEatObject) { + if object.is_null() { + return; + } + let _ = unsafe { Box::from_raw(object) }; +} From af815c2f362f8025639a0bf79d6f48a6e28a4a4a Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Wed, 20 Aug 2025 12:42:49 -0600 Subject: [PATCH 088/126] cmake for building idevice++ --- cpp/CMakeLists.txt | 117 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 cpp/CMakeLists.txt diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt new file mode 100644 index 0000000..ea895be --- /dev/null +++ b/cpp/CMakeLists.txt @@ -0,0 +1,117 @@ +cmake_minimum_required(VERSION 3.16) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +# Project +project(idevice++ + VERSION 0.1.0 + LANGUAGES CXX +) + +# ---- Options --------------------------------------------------------------- + +# Path to the Rust static library (override on the command line if needed): +# cmake -DIDEVICE_FFI_PATH=/absolute/path/to/libidevice_ffi.a .. +set(IDEVICE_FFI_PATH + "${CMAKE_CURRENT_SOURCE_DIR}/../target/release/libidevice_ffi.a" + CACHE FILEPATH "Path to libidevice_ffi.a produced by Rust build" +) + +# Optional extra libraries/frameworks if your Rust/C bridge needs them: +# e.g. -framework CoreFoundation on macOS, etc. +set(IDEVICEPP_EXTRA_LIBS + "" + CACHE STRING "Extra libraries to link (space-separated)" +) + +# ---- Tooling --------------------------------------------------------------- + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_POSITION_INDEPENDENT_CODE ON) # Helpful for shared libs + +# Threads +find_package(Threads REQUIRED) + +# On some platforms dl is required for Rust staticlibs that use libdl +include(CheckLibraryExists) +set(HAVE_LIBDL FALSE) +if(UNIX AND NOT APPLE) + check_library_exists(dl dlopen "" HAVE_LIBDL) +endif() + +# ---- Imported Rust static library ------------------------------------------ + +if(NOT EXISTS "${IDEVICE_FFI_PATH}") + message(FATAL_ERROR "IDEVICE_FFI_PATH does not exist: ${IDEVICE_FFI_PATH}") +endif() + +add_library(idevice_ffi STATIC IMPORTED GLOBAL) +set_target_properties(idevice_ffi PROPERTIES + IMPORTED_LOCATION "${IDEVICE_FFI_PATH}" +) + +# ---- Our C++ library ------------------------------------------------------- + +# Collect sources (convenience: tracks new files when you re-run CMake) +file(GLOB_RECURSE IDEVICEPP_SOURCES + CONFIGURE_DEPENDS + "${CMAKE_CURRENT_SOURCE_DIR}/src/*.cc" + "${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/*.cxx" +) + +# Respect BUILD_SHARED_LIBS (OFF=static, ON=shared) +add_library(${PROJECT_NAME}) +target_sources(${PROJECT_NAME} PRIVATE ${IDEVICEPP_SOURCES}) + +# Public headers live under include/, e.g. include/idevice++/Foo.hpp +target_include_directories(${PROJECT_NAME} + PUBLIC + "$" + "$" +) + +# Link dependencies +target_link_libraries(${PROJECT_NAME} + PUBLIC + idevice_ffi + Threads::Threads +) + +# Link libdl on Linux if present +if(HAVE_LIBDL) + target_link_libraries(${PROJECT_NAME} PUBLIC dl) +endif() + +# Windows winsock (if you ever build there) +if(WIN32) + target_link_libraries(${PROJECT_NAME} PUBLIC ws2_32) +endif() + +# Extra user-specified libs/frameworks +if(IDEVICEPP_EXTRA_LIBS) + # Split by spaces and link each one + separate_arguments(_extra_libs NATIVE_COMMAND ${IDEVICEPP_EXTRA_LIBS}) + target_link_libraries(${PROJECT_NAME} PUBLIC ${_extra_libs}) +endif() + +# Visibility / warnings (tweak to taste) +if(MSVC) + target_compile_options(${PROJECT_NAME} PRIVATE /permissive- /W4 /WX-) +else() + target_compile_options(${PROJECT_NAME} PRIVATE -Wall -Wextra -Wpedantic) +endif() + +# ---- Examples (optional) --------------------------------------------------- + +option(BUILD_EXAMPLES "Build examples in examples/ directory" OFF) +if(BUILD_EXAMPLES) + file(GLOB EXAMPLE_SOURCES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/examples/*.cpp") + foreach(ex_src IN LISTS EXAMPLE_SOURCES) + get_filename_component(exe_name "${ex_src}" NAME_WE) + add_executable(${exe_name} "${ex_src}") + target_link_libraries(${exe_name} PRIVATE ${PROJECT_NAME}) + target_include_directories(${exe_name} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/include") + endforeach() +endif() + From b772a2eeaec4da43e4ed4369e0d0ff5b06601176 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Wed, 20 Aug 2025 12:45:39 -0600 Subject: [PATCH 089/126] Remove debug logging from tcp object stack s --- ffi/src/tcp_object_stack.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/ffi/src/tcp_object_stack.rs b/ffi/src/tcp_object_stack.rs index 70e844c..8917305 100644 --- a/ffi/src/tcp_object_stack.rs +++ b/ffi/src/tcp_object_stack.rs @@ -8,7 +8,6 @@ use std::{ sync::Arc, }; -use log::debug; use tokio::sync::Mutex; use tokio::{ io::AsyncWriteExt, @@ -168,7 +167,6 @@ pub unsafe extern "C" fn idevice_tcp_eat_object_read( let lock = object.receiver.lock().await; match lock.try_read(&mut buf) { Ok(size) => { - debug!("EATING"); let bytes = buf[..size].to_vec(); let mut res = bytes.into_boxed_slice(); unsafe { From 6d908790964cb7416e7704d34ccbcb1197bcecf5 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Wed, 20 Aug 2025 18:27:38 -0600 Subject: [PATCH 090/126] Implement clone for RsdHandshake --- cpp/include/idevice++/rsd.hpp | 12 +++++++----- cpp/src/rsd.cpp | 18 ++++++++++++++++++ ffi/src/rsd.rs | 17 +++++++++++++++++ idevice/src/services/rsd.rs | 1 + 4 files changed, 43 insertions(+), 5 deletions(-) diff --git a/cpp/include/idevice++/rsd.hpp b/cpp/include/idevice++/rsd.hpp index b3898ec..1231a87 100644 --- a/cpp/include/idevice++/rsd.hpp +++ b/cpp/include/idevice++/rsd.hpp @@ -37,11 +37,13 @@ class RsdHandshake { std::optional service_info(const std::string& name, FfiError& err) const; // RAII / moves - ~RsdHandshake() noexcept = default; - RsdHandshake(RsdHandshake&&) noexcept = default; - RsdHandshake& operator=(RsdHandshake&&) noexcept = default; - RsdHandshake(const RsdHandshake&) = delete; - RsdHandshake& operator=(const RsdHandshake&) = delete; + ~RsdHandshake() noexcept = default; + RsdHandshake(RsdHandshake&&) noexcept = default; + RsdHandshake& operator=(RsdHandshake&&) noexcept = default; + + // Enable Copying + RsdHandshake(const RsdHandshake& other); + RsdHandshake& operator=(const RsdHandshake& other); RsdHandshakeHandle* raw() const noexcept { return handle_.get(); } static RsdHandshake adopt(RsdHandshakeHandle* h) noexcept { return RsdHandshake(h); } diff --git a/cpp/src/rsd.cpp b/cpp/src/rsd.cpp index 3575c6f..0511c54 100644 --- a/cpp/src/rsd.cpp +++ b/cpp/src/rsd.cpp @@ -61,6 +61,24 @@ static std::vector to_cpp_and_free(CRsdServiceArray* arr) { return out; } +RsdHandshake::RsdHandshake(const RsdHandshake& other) { + if (other.handle_) { + // Call the Rust FFI to clone the underlying handle + handle_.reset(rsd_handshake_clone(other.handle_.get())); + } + // If other.handle_ is null, our new handle_ will also be null, which is correct. +} + +RsdHandshake& RsdHandshake::operator=(const RsdHandshake& other) { + // Check for self-assignment + if (this != &other) { + // Create a temporary copy, then swap ownership + RsdHandshake temp(other); + std::swap(handle_, temp.handle_); + } + return *this; +} + // ---------- factory ---------- std::optional RsdHandshake::from_socket(ReadWrite&& rw, FfiError& err) { RsdHandshakeHandle* out = nullptr; diff --git a/ffi/src/rsd.rs b/ffi/src/rsd.rs index 9ab2bcb..0ac757f 100644 --- a/ffi/src/rsd.rs +++ b/ffi/src/rsd.rs @@ -10,6 +10,7 @@ use idevice::rsd::RsdHandshake; use crate::{IdeviceFfiError, RUNTIME, ReadWriteOpaque, ffi_err}; /// Opaque handle to an RsdHandshake +#[derive(Clone)] pub struct RsdHandshakeHandle(pub RsdHandshake); /// C-compatible representation of an RSD service @@ -370,6 +371,22 @@ pub unsafe extern "C" fn rsd_get_service_info( null_mut() } +/// Clones an RSD handshake +/// +/// # Safety +/// Pass a valid pointer allocated by this library +#[unsafe(no_mangle)] +pub unsafe extern "C" fn rsd_handshake_clone( + handshake: *mut RsdHandshakeHandle, +) -> *mut RsdHandshakeHandle { + if handshake.is_null() { + return null_mut(); + } + let handshake = unsafe { &mut *handshake }; + let new_handshake = handshake.clone(); + Box::into_raw(Box::new(new_handshake)) +} + /// Frees a string returned by RSD functions /// /// # Arguments diff --git a/idevice/src/services/rsd.rs b/idevice/src/services/rsd.rs index 9c41982..4013816 100644 --- a/idevice/src/services/rsd.rs +++ b/idevice/src/services/rsd.rs @@ -23,6 +23,7 @@ pub struct RsdService { pub service_version: Option, } +#[derive(Debug, Clone)] pub struct RsdHandshake { pub services: HashMap, pub protocol_version: usize, From 2a37865340f1cc29671ad6b9002da1ffb09c766c Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Thu, 21 Aug 2025 08:48:10 -0600 Subject: [PATCH 091/126] Keep buffered IP packet read internal to struct --- idevice/src/tcp/adapter.rs | 53 +++++++++++----- idevice/src/tcp/handle.rs | 125 +++++++++++++------------------------ idevice/src/tcp/packets.rs | 4 +- 3 files changed, 83 insertions(+), 99 deletions(-) diff --git a/idevice/src/tcp/adapter.rs b/idevice/src/tcp/adapter.rs index 287b59f..c366992 100644 --- a/idevice/src/tcp/adapter.rs +++ b/idevice/src/tcp/adapter.rs @@ -63,9 +63,12 @@ use std::{collections::HashMap, io::ErrorKind, net::IpAddr, path::Path, sync::Arc}; use log::{debug, trace, warn}; -use tokio::{io::AsyncWriteExt, sync::Mutex}; +use tokio::{ + io::{AsyncReadExt, AsyncWriteExt}, + sync::Mutex, +}; -use crate::ReadWrite; +use crate::{ReadWrite, tcp::packets::IpParseError}; use super::packets::{Ipv4Packet, Ipv6Packet, ProtocolNumber, TcpFlags, TcpPacket}; @@ -112,7 +115,7 @@ impl ConnectionState { #[derive(Debug)] pub struct Adapter { /// The underlying transport connection - pub(crate) peer: Box, + peer: Box, /// The local IP address host_ip: IpAddr, /// The remote peer's IP address @@ -121,6 +124,8 @@ pub struct Adapter { /// The states of the connections states: HashMap, // host port by state dropped: Vec, + read_buf: [u8; 4096], + bytes_in_buf: usize, /// Optional PCAP file for packet logging pcap: Option>>, @@ -143,6 +148,8 @@ impl Adapter { peer_ip, states: HashMap::new(), dropped: Vec::new(), + read_buf: [0u8; 4096], + bytes_in_buf: 0, pcap: None, } } @@ -518,22 +525,36 @@ impl Adapter { async fn read_ip_packet(&mut self) -> Result, std::io::Error> { self.write_buffer_flush().await?; Ok(loop { - match self.host_ip { - IpAddr::V4(_) => { - let packet = Ipv4Packet::from_reader(&mut self.peer, &self.pcap).await?; - trace!("IPv4 packet: {packet:#?}"); - if packet.protocol == 6 { - break packet.payload; - } + // try the data we already have + match Ipv6Packet::parse(&self.read_buf[..self.bytes_in_buf]) { + IpParseError::Ok { + packet, + bytes_consumed, + } => { + // And remove it from the buffer by shifting the remaining bytes + self.read_buf + .copy_within(bytes_consumed..self.bytes_in_buf, 0); + self.bytes_in_buf -= bytes_consumed; + break packet.payload; } - IpAddr::V6(_) => { - let packet = Ipv6Packet::from_reader(&mut self.peer, &self.pcap).await?; - trace!("IPv6 packet: {packet:#?}"); - if packet.next_header == 6 { - break packet.payload; - } + IpParseError::NotEnough => { + // Buffer doesn't have a full packet, wait for the next read + } + IpParseError::Invalid => { + // Corrupted data, close the connection + return Err(std::io::Error::new( + ErrorKind::InvalidData, + "invalid IPv6 parse", + )); } } + // go get more + let s = self + .peer + .read(&mut self.read_buf[self.bytes_in_buf..]) + .await?; + + self.bytes_in_buf += s; }) } diff --git a/idevice/src/tcp/handle.rs b/idevice/src/tcp/handle.rs index b7049a9..7da3aff 100644 --- a/idevice/src/tcp/handle.rs +++ b/idevice/src/tcp/handle.rs @@ -8,16 +8,13 @@ use std::{collections::HashMap, path::PathBuf, sync::Mutex, task::Poll}; use crossfire::{AsyncRx, MTx, Tx, mpsc, spsc, stream::AsyncStream}; use futures::{StreamExt, stream::FuturesUnordered}; -use log::{debug, trace}; +use log::trace; use tokio::{ - io::{AsyncRead, AsyncReadExt, AsyncWrite}, + io::{AsyncRead, AsyncWrite}, sync::oneshot, }; -use crate::tcp::{ - adapter::ConnectionStatus, - packets::{IpParseError, Ipv6Packet}, -}; +use crate::tcp::adapter::ConnectionStatus; pub type ConnectToPortRes = oneshot::Sender, std::io::Error>>), std::io::Error>>; @@ -54,8 +51,6 @@ impl AdapterHandle { let mut handles: HashMap, std::io::Error>>> = HashMap::new(); let mut tick = tokio::time::interval(std::time::Duration::from_millis(1)); - let mut read_buf = [0u8; 4096]; - let mut bytes_in_buf = 0; loop { tokio::select! { // check for messages for us @@ -102,85 +97,53 @@ impl AdapterHandle { } } - result = adapter.peer.read(&mut read_buf[bytes_in_buf..]) => { - match result { - Ok(0) => { - debug!("Underlying stream closed (EOF)"); - break; // Exit the main actor loop - } - Ok(s) => { - bytes_in_buf += s; - loop { - match Ipv6Packet::parse(&read_buf[..bytes_in_buf]) { - IpParseError::Ok { packet, bytes_consumed } => { - // We got a full packet! Process it. - if let Err(e) = adapter.process_tcp_packet_from_payload(&packet.payload).await { - debug!("CRITICAL: Failed to process IP packet: {e:?}"); - } - - // And remove it from the buffer by shifting the remaining bytes - read_buf.copy_within(bytes_consumed..bytes_in_buf, 0); - bytes_in_buf -= bytes_consumed; - // Push any newly available bytes to per-conn channels - let mut dead = Vec::new(); - for (&hp, tx) in &handles { - match adapter.uncache_all(hp) { - Ok(buf) if !buf.is_empty() => { - if tx.send(Ok(buf)).is_err() { - dead.push(hp); - } - } - Err(e) => { - let _ = tx.send(Err(e)); - dead.push(hp); - } - _ => {} - } - } - for hp in dead { - handles.remove(&hp); - let _ = adapter.close(hp).await; - } - - let mut to_close = Vec::new(); - for (&hp, tx) in &handles { - if let Ok(ConnectionStatus::Error(kind)) = adapter.get_status(hp) { - if kind == std::io::ErrorKind::UnexpectedEof { - to_close.push(hp); - } else { - let _ = tx.send(Err(std::io::Error::from(kind))); - to_close.push(hp); - } - } - } - for hp in to_close { - handles.remove(&hp); - // Best-effort close. For RST this just tidies state on our side - let _ = adapter.close(hp).await; - } - } - IpParseError::NotEnough => { - // Buffer doesn't have a full packet, wait for the next read - break; - } - IpParseError::Invalid => { - // Corrupted data, close the connection - // ... (error handling) ... - return; - } - } - } - - } - Err(e) => { - debug!("Failed to read: {e:?}, closing stack"); + r = adapter.process_tcp_packet() => { + if let Err(e) = r { + // propagate error to all streams; close them for (hp, tx) in handles.drain() { let _ = tx.send(Err(e.kind().into())); // or clone/convert let _ = adapter.close(hp).await; } - break; + break; + } + + // Push any newly available bytes to per-conn channels + let mut dead = Vec::new(); + for (&hp, tx) in &handles { + match adapter.uncache_all(hp) { + Ok(buf) if !buf.is_empty() => { + if tx.send(Ok(buf)).is_err() { + dead.push(hp); + } + } + Err(e) => { + let _ = tx.send(Err(e)); + dead.push(hp); + } + _ => {} } } + for hp in dead { + handles.remove(&hp); + let _ = adapter.close(hp).await; + } + + let mut to_close = Vec::new(); + for (&hp, tx) in &handles { + if let Ok(ConnectionStatus::Error(kind)) = adapter.get_status(hp) { + if kind == std::io::ErrorKind::UnexpectedEof { + to_close.push(hp); + } else { + let _ = tx.send(Err(std::io::Error::from(kind))); + to_close.push(hp); + } + } + } + for hp in to_close { + handles.remove(&hp); + // Best-effort close. For RST this just tidies state on our side + let _ = adapter.close(hp).await; + } } _ = tick.tick() => { diff --git a/idevice/src/tcp/packets.rs b/idevice/src/tcp/packets.rs index 0186ebe..251dc14 100644 --- a/idevice/src/tcp/packets.rs +++ b/idevice/src/tcp/packets.rs @@ -110,7 +110,7 @@ impl Ipv4Packet { let ihl = (version_ihl & 0x0F) * 4; if version != 4 || ihl < 20 { - debug!("Got an invalid IPv4 header"); + debug!("Got an invalid IPv4 header from reader"); return Err(std::io::Error::new( std::io::ErrorKind::InvalidData, "Invalid IPv4 header", @@ -306,7 +306,7 @@ impl Ipv6Packet { let version = header[0] >> 4; if version != 6 { - debug!("Got an invalid IPv6 header"); + debug!("Got an invalid IPv6 header from reader"); return Err(std::io::Error::new( std::io::ErrorKind::InvalidData, "Invalid IPv6 header", From 7fdaac9327597784370d6bbe34c531b6ba6903d2 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Thu, 21 Aug 2025 09:02:26 -0600 Subject: [PATCH 092/126] Include cstring for Linux cpp build --- cpp/CMakeLists.txt | 2 ++ cpp/src/diagnosticsservice.cpp | 1 + 2 files changed, 3 insertions(+) diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index ea895be..0bbee44 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -50,6 +50,8 @@ set_target_properties(idevice_ffi PROPERTIES IMPORTED_LOCATION "${IDEVICE_FFI_PATH}" ) +configure_file("${CMAKE_CURRENT_SOURCE_DIR}/../ffi/idevice.h" "${CMAKE_CURRENT_SOURCE_DIR}/include" COPYONLY) + # ---- Our C++ library ------------------------------------------------------- # Collect sources (convenience: tracks new files when you re-run CMake) diff --git a/cpp/src/diagnosticsservice.cpp b/cpp/src/diagnosticsservice.cpp index 26aa91e..f67d866 100644 --- a/cpp/src/diagnosticsservice.cpp +++ b/cpp/src/diagnosticsservice.cpp @@ -1,6 +1,7 @@ // Jackson Coxson #include +#include namespace IdeviceFFI { From a7b8c1778ea3207e81ed3cb50a017b09296832bc Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Thu, 21 Aug 2025 09:15:19 -0600 Subject: [PATCH 093/126] Checkout submodules in runner --- .github/workflows/ci.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 56aba55..510daa2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,6 +11,8 @@ jobs: runs-on: macos-latest steps: - uses: actions/checkout@v4 + with: + submodules: "true" - name: Install just run: | @@ -104,6 +106,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + submodules: "true" - name: Install build dependencies run: | @@ -176,6 +180,8 @@ jobs: runs-on: windows-latest steps: - uses: actions/checkout@v4 + with: + submodules: "true" # Install Rust (adds cargo/rustc to PATH) - name: Install Rust (stable) From a606870aa1f41aa6a7eab35ac43b49a7148ccf3b Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Thu, 21 Aug 2025 09:24:16 -0600 Subject: [PATCH 094/126] Cargofmt --- idevice/src/services/bt_packet_logger.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/idevice/src/services/bt_packet_logger.rs b/idevice/src/services/bt_packet_logger.rs index a4883e5..ab17158 100644 --- a/idevice/src/services/bt_packet_logger.rs +++ b/idevice/src/services/bt_packet_logger.rs @@ -201,4 +201,3 @@ impl BtHeader { )) } } - From 11c53dac86e2f121e4bf8846d5426436c485fd2a Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Fri, 22 Aug 2025 18:27:44 -0600 Subject: [PATCH 095/126] Re-set lat/lon repeatedly in location sim tool --- tools/src/location_simulation.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tools/src/location_simulation.rs b/tools/src/location_simulation.rs index 10f62a0..c017fbb 100644 --- a/tools/src/location_simulation.rs +++ b/tools/src/location_simulation.rs @@ -114,7 +114,11 @@ async fn main() { println!("Location set!"); println!("Press ctrl-c to stop"); loop { - tokio::time::sleep(std::time::Duration::from_secs(1)).await; + ls_client + .set(latitude, longitude) + .await + .expect("Failed to set location"); + tokio::time::sleep(std::time::Duration::from_secs(5)).await; } } else { eprintln!("Invalid usage, pass -h for help"); From 104b5c1543dea6a3f3359491856bc6c935132d46 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Sat, 23 Aug 2025 09:01:58 -0600 Subject: [PATCH 096/126] Implement ser/de for OsTraceRelay outputs --- Cargo.lock | 1 + idevice/Cargo.toml | 4 +++- idevice/src/services/os_trace_relay.rs | 7 ++++--- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e5df891..81b11c2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -369,6 +369,7 @@ dependencies = [ "iana-time-zone", "js-sys", "num-traits", + "serde", "wasm-bindgen", "windows-link", ] diff --git a/idevice/Cargo.toml b/idevice/Cargo.toml index d6ed576..e69ac18 100644 --- a/idevice/Cargo.toml +++ b/idevice/Cargo.toml @@ -30,7 +30,9 @@ base64 = { version = "0.22" } indexmap = { version = "2.7", features = ["serde"], optional = true } uuid = { version = "1.12", features = ["serde", "v4"], optional = true } -chrono = { version = "0.4.40", optional = true, default-features = false } +chrono = { version = "0.4.40", optional = true, default-features = false, features = [ + "serde", +] } serde_json = { version = "1", optional = true } json = { version = "0.12", optional = true } diff --git a/idevice/src/services/os_trace_relay.rs b/idevice/src/services/os_trace_relay.rs index 9fb07d3..e675ef6 100644 --- a/idevice/src/services/os_trace_relay.rs +++ b/idevice/src/services/os_trace_relay.rs @@ -4,6 +4,7 @@ //! https://github.com/doronz88/pymobiledevice3/blob/master/pymobiledevice3/services/os_trace.py use chrono::{DateTime, NaiveDateTime}; +use serde::{Deserialize, Serialize}; use tokio::io::AsyncWriteExt; use crate::{Idevice, IdeviceError, IdeviceService, obf}; @@ -30,7 +31,7 @@ pub struct OsTraceRelayReceiver { inner: OsTraceRelayClient, } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct OsTraceLog { pub pid: u32, pub timestamp: NaiveDateTime, @@ -41,13 +42,13 @@ pub struct OsTraceLog { pub label: Option, } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct SyslogLabel { pub subsystem: String, pub category: String, } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum LogLevel { Notice = 0, Info = 1, From 4d5e646a6b174a0cffe0db6a7b180f0b49ae78bf Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Sat, 23 Aug 2025 09:45:41 -0600 Subject: [PATCH 097/126] Replace manual plist creation with macro in diagnostics relay --- idevice/src/services/diagnostics_relay.rs | 120 +++++++++------------- 1 file changed, 48 insertions(+), 72 deletions(-) diff --git a/idevice/src/services/diagnostics_relay.rs b/idevice/src/services/diagnostics_relay.rs index 9b31f78..8f65654 100644 --- a/idevice/src/services/diagnostics_relay.rs +++ b/idevice/src/services/diagnostics_relay.rs @@ -39,27 +39,17 @@ impl DiagnosticsRelayClient { /// A plist of the tree on success pub async fn ioregistry( &mut self, - current_plane: Option>, - entry_name: Option>, - entry_class: Option>, + current_plane: Option<&str>, + entry_name: Option<&str>, + entry_class: Option<&str>, ) -> Result, IdeviceError> { - let mut req = plist::Dictionary::new(); - if let Some(plane) = current_plane { - let plane = plane.into(); - req.insert("CurrentPlane".into(), plane.into()); - } - if let Some(name) = entry_name { - let name = name.into(); - req.insert("EntryName".into(), name.into()); - } - if let Some(class) = entry_class { - let class = class.into(); - req.insert("EntryClass".into(), class.into()); - } - req.insert("Request".into(), "IORegistry".into()); - self.idevice - .send_plist(plist::Value::Dictionary(req)) - .await?; + let req = crate::plist!({ + "Request": "IORegistry", + "CurrentPlane":? current_plane, + "EntryName":? entry_name, + "EntryClass":? entry_class, + }); + self.idevice.send_plist(req).await?; let mut res = self.idevice.read_plist().await?; match res.get("Status").and_then(|x| x.as_string()) { @@ -89,17 +79,11 @@ impl DiagnosticsRelayClient { &mut self, keys: Option>, ) -> Result, IdeviceError> { - let mut req = plist::Dictionary::new(); - req.insert("Request".into(), "MobileGestalt".into()); - - if let Some(keys) = keys { - let keys_array: Vec = keys.into_iter().map(|k| k.into()).collect(); - req.insert("MobileGestaltKeys".into(), plist::Value::Array(keys_array)); - } - - self.idevice - .send_plist(plist::Value::Dictionary(req)) - .await?; + let req = crate::plist!({ + "Request": "MobileGestalt", + "MobileGestaltKeys":? keys, + }); + self.idevice.send_plist(req).await?; let mut res = self.idevice.read_plist().await?; match res.get("Status").and_then(|x| x.as_string()) { @@ -119,12 +103,10 @@ impl DiagnosticsRelayClient { /// # Returns /// A dictionary containing gas gauge (battery) information pub async fn gasguage(&mut self) -> Result, IdeviceError> { - let mut req = plist::Dictionary::new(); - req.insert("Request".into(), "GasGauge".into()); - - self.idevice - .send_plist(plist::Value::Dictionary(req)) - .await?; + let req = crate::plist!({ + "Request": "GasGauge" + }); + self.idevice.send_plist(req).await?; let mut res = self.idevice.read_plist().await?; match res.get("Status").and_then(|x| x.as_string()) { @@ -144,12 +126,11 @@ impl DiagnosticsRelayClient { /// # Returns /// A dictionary containing NAND flash information pub async fn nand(&mut self) -> Result, IdeviceError> { - let mut req = plist::Dictionary::new(); - req.insert("Request".into(), "NAND".into()); + let req = crate::plist!({ + "Request": "NAND" + }); - self.idevice - .send_plist(plist::Value::Dictionary(req)) - .await?; + self.idevice.send_plist(req).await?; let mut res = self.idevice.read_plist().await?; match res.get("Status").and_then(|x| x.as_string()) { @@ -169,12 +150,11 @@ impl DiagnosticsRelayClient { /// # Returns /// A dictionary containing all diagnostics information pub async fn all(&mut self) -> Result, IdeviceError> { - let mut req = plist::Dictionary::new(); - req.insert("Request".into(), "All".into()); + let req = crate::plist!({ + "Request": "All" + }); - self.idevice - .send_plist(plist::Value::Dictionary(req)) - .await?; + self.idevice.send_plist(req).await?; let mut res = self.idevice.read_plist().await?; match res.get("Status").and_then(|x| x.as_string()) { @@ -194,12 +174,11 @@ impl DiagnosticsRelayClient { /// # Returns /// Result indicating success or failure pub async fn restart(&mut self) -> Result<(), IdeviceError> { - let mut req = plist::Dictionary::new(); - req.insert("Request".into(), "Restart".into()); + let req = crate::plist!({ + "Request": "Restart", + }); - self.idevice - .send_plist(plist::Value::Dictionary(req)) - .await?; + self.idevice.send_plist(req).await?; let res = self.idevice.read_plist().await?; match res.get("Status").and_then(|x| x.as_string()) { @@ -213,12 +192,11 @@ impl DiagnosticsRelayClient { /// # Returns /// Result indicating success or failure pub async fn shutdown(&mut self) -> Result<(), IdeviceError> { - let mut req = plist::Dictionary::new(); - req.insert("Request".into(), "Shutdown".into()); + let req = crate::plist!({ + "Request": "Shutdown" + }); - self.idevice - .send_plist(plist::Value::Dictionary(req)) - .await?; + self.idevice.send_plist(req).await?; let res = self.idevice.read_plist().await?; match res.get("Status").and_then(|x| x.as_string()) { @@ -232,12 +210,11 @@ impl DiagnosticsRelayClient { /// # Returns /// Result indicating success or failure pub async fn sleep(&mut self) -> Result<(), IdeviceError> { - let mut req = plist::Dictionary::new(); - req.insert("Request".into(), "Sleep".into()); + let req = crate::plist!({ + "Request": "Sleep" + }); - self.idevice - .send_plist(plist::Value::Dictionary(req)) - .await?; + self.idevice.send_plist(req).await?; let res = self.idevice.read_plist().await?; match res.get("Status").and_then(|x| x.as_string()) { @@ -248,12 +225,11 @@ impl DiagnosticsRelayClient { /// Requests WiFi diagnostics from the device pub async fn wifi(&mut self) -> Result, IdeviceError> { - let mut req = plist::Dictionary::new(); - req.insert("Request".into(), "WiFi".into()); + let req = crate::plist!({ + "Request": "WiFi" + }); - self.idevice - .send_plist(plist::Value::Dictionary(req)) - .await?; + self.idevice.send_plist(req).await?; let mut res = self.idevice.read_plist().await?; match res.get("Status").and_then(|x| x.as_string()) { @@ -270,11 +246,11 @@ impl DiagnosticsRelayClient { /// Sends Goodbye request signaling end of communication pub async fn goodbye(&mut self) -> Result<(), IdeviceError> { - let mut req = plist::Dictionary::new(); - req.insert("Request".into(), "Goodbye".into()); - self.idevice - .send_plist(plist::Value::Dictionary(req)) - .await?; + let req = crate::plist!({ + "Request": "Goodbye" + }); + + self.idevice.send_plist(req).await?; let res = self.idevice.read_plist().await?; match res.get("Status").and_then(|x| x.as_string()) { Some("Success") => Ok(()), From ac551c9bc8bd065d5f1d1053ebece39acab6682f Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Sat, 23 Aug 2025 09:48:17 -0600 Subject: [PATCH 098/126] Macro plist creation in tss --- idevice/src/tss.rs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/idevice/src/tss.rs b/idevice/src/tss.rs index df7491c..46ff2cb 100644 --- a/idevice/src/tss.rs +++ b/idevice/src/tss.rs @@ -30,13 +30,11 @@ impl TSSRequest { /// - Client version string /// - Random UUID for request identification pub fn new() -> Self { - let mut inner = plist::Dictionary::new(); - inner.insert("@HostPlatformInfo".into(), "mac".into()); - inner.insert("@VersionInfo".into(), TSS_CLIENT_VERSION_STRING.into()); - inner.insert( - "@UUID".into(), - uuid::Uuid::new_v4().to_string().to_uppercase().into(), - ); + let inner = crate::plist!(dict { + "@HostPlatformInfo": "mac", + "@VersionInfo": TSS_CLIENT_VERSION_STRING, + "@UUID": uuid::Uuid::new_v4().to_string().to_uppercase() + }); Self { inner } } From 9b6f167356d4492c03788038ccb30c1dd90679b1 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Sat, 23 Aug 2025 09:53:01 -0600 Subject: [PATCH 099/126] Plist macro creation in lib.rs --- idevice/src/lib.rs | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/idevice/src/lib.rs b/idevice/src/lib.rs index 48003ee..7637998 100644 --- a/idevice/src/lib.rs +++ b/idevice/src/lib.rs @@ -174,11 +174,12 @@ impl Idevice { /// # Errors /// Returns `IdeviceError` if communication fails or response is invalid pub async fn get_type(&mut self) -> Result { - let mut req = plist::Dictionary::new(); - req.insert("Label".into(), self.label.clone().into()); - req.insert("Request".into(), "QueryType".into()); - let message = plist::to_value(&req)?; - self.send_plist(message).await?; + let req = crate::plist!({ + "Label": self.label.clone(), + "Request": "QueryType", + }); + self.send_plist(req).await?; + let message: plist::Dictionary = self.read_plist().await?; match message.get("Type") { Some(m) => Ok(plist::from_value(m)?), @@ -193,11 +194,13 @@ impl Idevice { /// # Errors /// Returns `IdeviceError` if the protocol sequence isn't followed correctly pub async fn rsd_checkin(&mut self) -> Result<(), IdeviceError> { - let mut req = plist::Dictionary::new(); - req.insert("Label".into(), self.label.clone().into()); - req.insert("ProtocolVersion".into(), "2".into()); - req.insert("Request".into(), "RSDCheckin".into()); - self.send_plist(plist::to_value(&req).unwrap()).await?; + let req = crate::plist!({ + "Label": self.label.clone(), + "ProtocolVersion": "2", + "Request": "RSDCheckin", + }); + + self.send_plist(req).await?; let res = self.read_plist().await?; match res.get("Request").and_then(|x| x.as_string()) { Some(r) => { From cf4604c2ea6bfc4e5cbbece35ce88a9e08473f68 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Sat, 23 Aug 2025 09:58:25 -0600 Subject: [PATCH 100/126] AMFI macro plist creation --- idevice/src/services/amfi.rs | 53 +++++++++++++++--------------------- 1 file changed, 22 insertions(+), 31 deletions(-) diff --git a/idevice/src/services/amfi.rs b/idevice/src/services/amfi.rs index 3cf2b91..3c468c8 100644 --- a/idevice/src/services/amfi.rs +++ b/idevice/src/services/amfi.rs @@ -1,7 +1,5 @@ //! Abstraction for Apple Mobile File Integrity -use plist::Dictionary; - use crate::{Idevice, IdeviceError, IdeviceService, obf}; /// Client for interacting with the AMFI service on the device @@ -33,11 +31,10 @@ impl AmfiClient { /// Shows the developer mode option in settings in iOS 18+ /// Settings -> Privacy & Security -> Developer Mode pub async fn reveal_developer_mode_option_in_ui(&mut self) -> Result<(), IdeviceError> { - let mut request = Dictionary::new(); - request.insert("action".into(), 0.into()); - self.idevice - .send_plist(plist::Value::Dictionary(request)) - .await?; + let request = crate::plist!({ + "action": 0, + }); + self.idevice.send_plist(request).await?; let res = self.idevice.read_plist().await?; if res.get("success").is_some() { @@ -49,11 +46,10 @@ impl AmfiClient { /// Enables developer mode, triggering a reboot on iOS 18+ pub async fn enable_developer_mode(&mut self) -> Result<(), IdeviceError> { - let mut request = Dictionary::new(); - request.insert("action".into(), 1.into()); - self.idevice - .send_plist(plist::Value::Dictionary(request)) - .await?; + let request = crate::plist!({ + "action": 1, + }); + self.idevice.send_plist(request).await?; let res = self.idevice.read_plist().await?; if res.get("success").is_some() { @@ -65,11 +61,10 @@ impl AmfiClient { /// Shows the accept dialogue for enabling developer mode pub async fn accept_developer_mode(&mut self) -> Result<(), IdeviceError> { - let mut request = Dictionary::new(); - request.insert("action".into(), 2.into()); - self.idevice - .send_plist(plist::Value::Dictionary(request)) - .await?; + let request = crate::plist!({ + "action": 2, + }); + self.idevice.send_plist(request).await?; let res = self.idevice.read_plist().await?; if res.get("success").is_some() { @@ -81,11 +76,10 @@ impl AmfiClient { /// Gets the developer mode status pub async fn get_developer_mode_status(&mut self) -> Result { - let mut request = Dictionary::new(); - request.insert("action".into(), 3.into()); - self.idevice - .send_plist(plist::Value::Dictionary(request)) - .await?; + let request = crate::plist!({ + "action": 3, + }); + self.idevice.send_plist(request).await?; let res = self.idevice.read_plist().await?; match res.get("success").and_then(|x| x.as_boolean()) { @@ -104,15 +98,12 @@ impl AmfiClient { &mut self, uuid: impl Into, ) -> Result { - let mut request = Dictionary::new(); - request.insert("action".into(), 4.into()); - request.insert( - "input_profile_uuid".into(), - plist::Value::String(uuid.into()), - ); - self.idevice - .send_plist(plist::Value::Dictionary(request)) - .await?; + let request = crate::plist!({ + "action": 4, + "input_profile_uuid": uuid.into(), + }); + + self.idevice.send_plist(request).await?; let res = self.idevice.read_plist().await?; match res.get("success").and_then(|x| x.as_boolean()) { From 78ea183f48d49c41419b6227ca2569bccaa74b0b Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Sat, 23 Aug 2025 09:58:39 -0600 Subject: [PATCH 101/126] House arrest macro plist creation --- idevice/src/services/house_arrest.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/idevice/src/services/house_arrest.rs b/idevice/src/services/house_arrest.rs index a38a85c..b7fb73e 100644 --- a/idevice/src/services/house_arrest.rs +++ b/idevice/src/services/house_arrest.rs @@ -4,8 +4,6 @@ //! installed on an iOS device. This is typically used for file transfer and inspection of //! app-specific data during development or diagnostics. -use plist::{Dictionary, Value}; - use crate::{Idevice, IdeviceError, IdeviceService, obf}; use super::afc::AfcClient; @@ -91,10 +89,12 @@ impl HouseArrestClient { /// # Errors /// Returns `IdeviceError` if the request or AFC setup fails async fn vend(mut self, bundle_id: String, cmd: String) -> Result { - let mut req = Dictionary::new(); - req.insert("Command".into(), cmd.into()); - req.insert("Identifier".into(), bundle_id.into()); - self.idevice.send_plist(Value::Dictionary(req)).await?; + let req = crate::plist!({ + "Command": cmd, + "Identifier": bundle_id + }); + + self.idevice.send_plist(req).await?; self.idevice.read_plist().await?; Ok(AfcClient::new(self.idevice)) From 2efc20522145f82d8815117efa92ff64369d0b0a Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Sat, 23 Aug 2025 10:01:27 -0600 Subject: [PATCH 102/126] Misagent plist macro creation --- idevice/src/services/misagent.rs | 38 ++++++++++++++------------------ 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/idevice/src/services/misagent.rs b/idevice/src/services/misagent.rs index 9f8de5a..c033908 100644 --- a/idevice/src/services/misagent.rs +++ b/idevice/src/services/misagent.rs @@ -4,7 +4,6 @@ //! which manages provisioning profiles and certificates. use log::warn; -use plist::Dictionary; use crate::{Idevice, IdeviceError, IdeviceService, RsdService, obf}; @@ -71,14 +70,13 @@ impl MisagentClient { /// client.install(profile_data).await?; /// ``` pub async fn install(&mut self, profile: Vec) -> Result<(), IdeviceError> { - let mut req = Dictionary::new(); - req.insert("MessageType".into(), "Install".into()); - req.insert("Profile".into(), plist::Value::Data(profile)); - req.insert("ProfileType".into(), "Provisioning".into()); + let req = crate::plist!({ + "MessageType": "Install", + "Profile": profile, + "ProfileType": "Provisioning" + }); - self.idevice - .send_plist(plist::Value::Dictionary(req)) - .await?; + self.idevice.send_plist(req).await?; let mut res = self.idevice.read_plist().await?; @@ -121,14 +119,13 @@ impl MisagentClient { /// client.remove("asdf").await?; /// ``` pub async fn remove(&mut self, id: &str) -> Result<(), IdeviceError> { - let mut req = Dictionary::new(); - req.insert("MessageType".into(), "Remove".into()); - req.insert("ProfileID".into(), id.into()); - req.insert("ProfileType".into(), "Provisioning".into()); + let req = crate::plist!({ + "MessageType": "Remove", + "ProfileID": id, + "ProfileType": "Provisioning" + }); - self.idevice - .send_plist(plist::Value::Dictionary(req)) - .await?; + self.idevice.send_plist(req).await?; let mut res = self.idevice.read_plist().await?; @@ -170,13 +167,12 @@ impl MisagentClient { /// } /// ``` pub async fn copy_all(&mut self) -> Result>, IdeviceError> { - let mut req = Dictionary::new(); - req.insert("MessageType".into(), "CopyAll".into()); - req.insert("ProfileType".into(), "Provisioning".into()); + let req = crate::plist!({ + "MessageType": "CopyAll", + "ProfileType": "Provisioning" + }); - self.idevice - .send_plist(plist::Value::Dictionary(req)) - .await?; + self.idevice.send_plist(req).await?; let mut res = self.idevice.read_plist().await?; match res.remove("Payload") { From bb432d8807092f6fa1c42089765cb60d7491aaaf Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Sun, 24 Aug 2025 17:52:21 -0600 Subject: [PATCH 103/126] Properly serialize UUIDs in remotexpc --- idevice/src/xpc/format.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/idevice/src/xpc/format.rs b/idevice/src/xpc/format.rs index ccfd6cb..bc67f63 100644 --- a/idevice/src/xpc/format.rs +++ b/idevice/src/xpc/format.rs @@ -252,7 +252,6 @@ impl XPCObject { } XPCObject::Uuid(uuid) => { buf.extend_from_slice(&(XPCType::Uuid as u32).to_le_bytes()); - buf.extend_from_slice(&16_u32.to_le_bytes()); buf.extend_from_slice(uuid.as_bytes()); } XPCObject::FileTransfer { msg_id, data } => { @@ -412,6 +411,13 @@ impl XPCObject { } } + pub fn to_dictionary(self) -> Option { + match self { + XPCObject::Dictionary(dict) => Some(dict), + _ => None, + } + } + pub fn as_array(&self) -> Option<&Vec> { match self { XPCObject::Array(array) => Some(array), From 3d3a8eb55f90eb908e7e572a1141731b122f3fff Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Sun, 24 Aug 2025 17:52:38 -0600 Subject: [PATCH 104/126] Implement openstdiosocket --- .../src/services/core_device/app_service.rs | 89 +++++++++++-------- .../core_device/diagnosticsservice.rs | 2 +- idevice/src/services/core_device/mod.rs | 24 +++-- .../services/core_device/openstdiosocket.rs | 33 +++++++ 4 files changed, 106 insertions(+), 42 deletions(-) create mode 100644 idevice/src/services/core_device/openstdiosocket.rs diff --git a/idevice/src/services/core_device/app_service.rs b/idevice/src/services/core_device/app_service.rs index eedd185..5b177d9 100644 --- a/idevice/src/services/core_device/app_service.rs +++ b/idevice/src/services/core_device/app_service.rs @@ -3,7 +3,7 @@ use log::warn; use serde::Deserialize; -use crate::{IdeviceError, ReadWrite, RsdService, obf}; +use crate::{IdeviceError, ReadWrite, RsdService, obf, xpc::XPCObject}; use super::CoreDeviceServiceClient; @@ -152,7 +152,7 @@ impl AppServiceClient { }); let res = self .inner - .invoke("com.apple.coredevice.feature.listapps", Some(options)) + .invoke_with_plist("com.apple.coredevice.feature.listapps", options) .await?; let res = match res.as_array() { @@ -178,6 +178,16 @@ impl AppServiceClient { Ok(desd) } + /// Launches an application by a bundle ID. + /// + /// # Notes + /// * `start_suspended` - If set to true, you will need to attach a debugger using + /// `DebugServer` to continue. + /// + /// * `stdio_uuid` - Create a new ``OpenStdioSocketClient``, read the UUID, and pass it to this + /// function. Note that if the process already has another stdio UUID, this parameter is ignored by + /// iOS. Either make sure the proccess isn't running, or pass ``kill_existing: true`` + #[allow(clippy::too_many_arguments)] // still didn't ask pub async fn launch_application( &mut self, bundle_id: impl Into, @@ -186,6 +196,7 @@ impl AppServiceClient { start_suspended: bool, environment: Option, platform_options: Option, + stdio_uuid: Option, ) -> Result { let bundle_id = bundle_id.into(); @@ -196,20 +207,34 @@ impl AppServiceClient { } }, "options": { - "arguments": arguments, // Now this will work directly + "arguments": arguments, "environmentVariables": environment.unwrap_or_default(), "standardIOUsesPseudoterminals": true, "startStopped": start_suspended, "terminateExisting": kill_existing, "user": { - "shortName": "mobile" + "active": true, }, "platformSpecificOptions": plist::Value::Data(crate::util::plist_to_xml_bytes(&platform_options.unwrap_or_default())), }, - "standardIOIdentifiers": {} - }) - .into_dictionary() - .unwrap(); + }); + + let req: XPCObject = req.into(); + let mut req = req.to_dictionary().unwrap(); + req.insert( + "standardIOIdentifiers".into(), + match stdio_uuid { + Some(u) => { + let u = XPCObject::Uuid(u); + let mut d = crate::xpc::Dictionary::new(); + d.insert("standardInput".into(), u.clone()); + d.insert("standardOutput".into(), u.clone()); + d.insert("standardError".into(), u.clone()); + d.into() + } + None => crate::xpc::Dictionary::new().into(), + }, + ); let res = self .inner @@ -259,13 +284,11 @@ impl AppServiceClient { ) -> Result<(), IdeviceError> { let bundle_id = bundle_id.into(); self.inner - .invoke( + .invoke_with_plist( "com.apple.coredevice.feature.uninstallapp", - Some( - crate::plist!({"bundleIdentifier": bundle_id}) - .into_dictionary() - .unwrap(), - ), + crate::plist!({"bundleIdentifier": bundle_id}) + .into_dictionary() + .unwrap(), ) .await?; @@ -279,16 +302,14 @@ impl AppServiceClient { ) -> Result { let res = self .inner - .invoke( + .invoke_with_plist( "com.apple.coredevice.feature.sendsignaltoprocess", - Some( - crate::plist!({ - "process": { "processIdentifier": pid as i64}, - "signal": signal as i64, - }) - .into_dictionary() - .unwrap(), - ), + crate::plist!({ + "process": { "processIdentifier": pid as i64}, + "signal": signal as i64, + }) + .into_dictionary() + .unwrap(), ) .await?; @@ -314,19 +335,17 @@ impl AppServiceClient { let bundle_id = bundle_id.into(); let res = self .inner - .invoke( + .invoke_with_plist( "com.apple.coredevice.feature.fetchappicons", - Some( - crate::plist!({ - "width": width, - "height": height, - "scale": scale, - "allowPlaceholder": allow_placeholder, - "bundleIdentifier": bundle_id - }) - .into_dictionary() - .unwrap(), - ), + crate::plist!({ + "width": width, + "height": height, + "scale": scale, + "allowPlaceholder": allow_placeholder, + "bundleIdentifier": bundle_id + }) + .into_dictionary() + .unwrap(), ) .await?; diff --git a/idevice/src/services/core_device/diagnosticsservice.rs b/idevice/src/services/core_device/diagnosticsservice.rs index 084a390..803f1fb 100644 --- a/idevice/src/services/core_device/diagnosticsservice.rs +++ b/idevice/src/services/core_device/diagnosticsservice.rs @@ -45,7 +45,7 @@ impl DiagnostisServiceClient { let res = self .inner - .invoke("com.apple.coredevice.feature.capturesysdiagnose", Some(req)) + .invoke_with_plist("com.apple.coredevice.feature.capturesysdiagnose", req) .await?; if let Some(len) = res diff --git a/idevice/src/services/core_device/mod.rs b/idevice/src/services/core_device/mod.rs index e6ef7f7..1f405ce 100644 --- a/idevice/src/services/core_device/mod.rs +++ b/idevice/src/services/core_device/mod.rs @@ -10,8 +10,10 @@ use crate::{ mod app_service; mod diagnosticsservice; +mod openstdiosocket; pub use app_service::*; pub use diagnosticsservice::*; +pub use openstdiosocket::*; const CORE_SERVICE_VERSION: &str = "443.18"; @@ -26,13 +28,26 @@ impl CoreDeviceServiceClient { Ok(Self { inner: client }) } + pub async fn invoke_with_plist( + &mut self, + feature: impl Into, + input: plist::Dictionary, + ) -> Result { + let input: XPCObject = plist::Value::Dictionary(input).into(); + let input = input.to_dictionary().unwrap(); + self.invoke(feature, Some(input)).await + } + pub async fn invoke( &mut self, feature: impl Into, - input: Option, + input: Option, ) -> Result { let feature = feature.into(); - let input = input.unwrap_or_default(); + let input: crate::xpc::XPCObject = match input { + Some(i) => i.into(), + None => crate::xpc::Dictionary::new().into(), + }; let mut req = xpc::Dictionary::new(); req.insert( @@ -52,10 +67,7 @@ impl CoreDeviceServiceClient { "CoreDevice.featureIdentifier".into(), XPCObject::String(feature), ); - req.insert( - "CoreDevice.input".into(), - plist::Value::Dictionary(input).into(), - ); + req.insert("CoreDevice.input".into(), input); req.insert( "CoreDevice.invocationIdentifier".into(), XPCObject::String(uuid::Uuid::new_v4().to_string()), diff --git a/idevice/src/services/core_device/openstdiosocket.rs b/idevice/src/services/core_device/openstdiosocket.rs new file mode 100644 index 0000000..e5630ce --- /dev/null +++ b/idevice/src/services/core_device/openstdiosocket.rs @@ -0,0 +1,33 @@ +// Jackson Coxson + +use tokio::io::AsyncReadExt; + +use crate::{IdeviceError, ReadWrite, RsdService, obf}; + +impl RsdService for OpenStdioSocketClient { + fn rsd_service_name() -> std::borrow::Cow<'static, str> { + obf!("com.apple.coredevice.openstdiosocket") + } + + async fn from_stream(stream: Box) -> Result { + Ok(Self { inner: stream }) + } +} + +/// Call ``read_uuid`` to get the UUID. Pass that to app service launch to connect to the stream of +/// the launched app. Inner is exposed to read and write to, using Tokio's AsyncReadExt/AsyncWriteExt +pub struct OpenStdioSocketClient { + pub inner: Box, +} + +impl OpenStdioSocketClient { + /// iOS assigns a UUID to a newly opened stream. That UUID is then passed to the launch + /// parameters of app service to start a stream. + pub async fn read_uuid(&mut self) -> Result { + let mut buf = [0u8; 16]; + self.inner.read_exact(&mut buf).await?; + + let res = uuid::Uuid::from_bytes(buf); + Ok(res) + } +} From b70f531e1e5a1746c7efae9fcaff85104f437a85 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Sun, 24 Aug 2025 17:52:53 -0600 Subject: [PATCH 105/126] Start console for app launch --- tools/src/app_service.rs | 38 +++++++++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/tools/src/app_service.rs b/tools/src/app_service.rs index 9951666..334cb67 100644 --- a/tools/src/app_service.rs +++ b/tools/src/app_service.rs @@ -2,7 +2,9 @@ use clap::{Arg, Command}; use idevice::{ - IdeviceService, RsdService, core_device::AppServiceClient, core_device_proxy::CoreDeviceProxy, + IdeviceService, RsdService, + core_device::{AppServiceClient, OpenStdioSocketClient}, + core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake, }; @@ -136,12 +138,42 @@ async fn main() { } }; + let mut stdio_conn = OpenStdioSocketClient::connect_rsd(&mut adapter, &mut handshake) + .await + .expect("no stdio"); + + let stdio_uuid = stdio_conn.read_uuid().await.expect("no uuid"); + println!("stdio uuid: {stdio_uuid:?}"); + let res = asc - .launch_application(bundle_id, &[], false, false, None, None) + .launch_application(bundle_id, &[], true, false, None, None, Some(stdio_uuid)) .await .expect("no launch"); - println!("{res:#?}"); + println!("Launch response {res:#?}"); + + let (mut remote_reader, mut remote_writer) = tokio::io::split(stdio_conn.inner); + let mut local_stdin = tokio::io::stdin(); + let mut local_stdout = tokio::io::stdout(); + + tokio::select! { + // Task 1: Copy data from the remote process to local stdout + res = tokio::io::copy(&mut remote_reader, &mut local_stdout) => { + if let Err(e) = res { + eprintln!("Error copying from remote to local: {}", e); + } + println!("\nRemote connection closed."); + return; + } + // Task 2: Copy data from local stdin to the remote process + res = tokio::io::copy(&mut local_stdin, &mut remote_writer) => { + if let Err(e) = res { + eprintln!("Error copying from local to remote: {}", e); + } + println!("\nLocal stdin closed."); + return; + } + } } else if matches.subcommand_matches("processes").is_some() { let p = asc.list_processes().await.expect("no processes?"); println!("{p:#?}"); From 82c3328afc3a6c0da66563e2f8498bb0380ae04a Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Sun, 24 Aug 2025 17:53:05 -0600 Subject: [PATCH 106/126] Update FFI for app launch arguments --- Cargo.lock | 1 + cpp/src/app_service.cpp | 1 + ffi/Cargo.toml | 3 ++- ffi/src/core_device/app_service.rs | 10 ++++++++++ 4 files changed, 14 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 81b11c2..1210618 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1179,6 +1179,7 @@ dependencies = [ "simplelog", "tokio", "ureq", + "uuid", "windows-sys 0.60.2", ] diff --git a/cpp/src/app_service.cpp b/cpp/src/app_service.cpp index 725c1d4..c009aaf 100644 --- a/cpp/src/app_service.cpp +++ b/cpp/src/app_service.cpp @@ -112,6 +112,7 @@ std::optional AppService::launch(const std::string& c_argv.size(), kill_existing ? 1 : 0, start_suspended ? 1 : 0, + NULL, // TODO: stdio handling &resp)) { err = FfiError(e); return std::nullopt; diff --git a/ffi/Cargo.toml b/ffi/Cargo.toml index 33b96fa..a1e1339 100644 --- a/ffi/Cargo.toml +++ b/ffi/Cargo.toml @@ -14,6 +14,7 @@ tokio = { version = "1.44.1", features = ["full"] } libc = "0.2.171" plist = "1.7.1" plist_ffi = { version = "0.1.5" } +uuid = { version = "1.12", features = ["v4"], optional = true } [target.'cfg(windows)'.dependencies] windows-sys = { version = "0.60", features = ["Win32_Networking_WinSock"] } @@ -25,7 +26,7 @@ ring = ["idevice/ring"] afc = ["idevice/afc"] amfi = ["idevice/amfi"] -core_device = ["idevice/core_device", "dep:futures"] +core_device = ["idevice/core_device", "dep:futures", "dep:uuid"] core_device_proxy = ["idevice/core_device_proxy"] crashreportcopymobile = ["idevice/crashreportcopymobile"] debug_proxy = ["idevice/debug_proxy"] diff --git a/ffi/src/core_device/app_service.rs b/ffi/src/core_device/app_service.rs index c8f0616..6078635 100644 --- a/ffi/src/core_device/app_service.rs +++ b/ffi/src/core_device/app_service.rs @@ -299,6 +299,7 @@ pub unsafe extern "C" fn app_service_free_app_list(apps: *mut AppListEntryC, cou /// * [`argc`] - Number of arguments /// * [`kill_existing`] - Whether to kill existing instances /// * [`start_suspended`] - Whether to start suspended +/// * [`stdio_uuid`] - The UUID received from openstdiosocket, null for none /// * [`response`] - Pointer to store the launch response (caller must free) /// /// # Returns @@ -314,6 +315,7 @@ pub unsafe extern "C" fn app_service_launch_app( argc: usize, kill_existing: c_int, start_suspended: c_int, + stdio_uuid: *const u8, response: *mut *mut LaunchResponseC, ) -> *mut IdeviceFfiError { if handle.is_null() || bundle_id.is_null() || response.is_null() { @@ -337,6 +339,13 @@ pub unsafe extern "C" fn app_service_launch_app( } } + let stdio_uuid = if stdio_uuid.is_null() { + None + } else { + let stdio_uuid = unsafe { std::slice::from_raw_parts(stdio_uuid, 16) }; + Some(uuid::Uuid::from_bytes(stdio_uuid.try_into().unwrap())) + }; + let client = unsafe { &mut (*handle).0 }; let res = RUNTIME.block_on(async move { client @@ -347,6 +356,7 @@ pub unsafe extern "C" fn app_service_launch_app( start_suspended != 0, None, // environment None, // platform_options + stdio_uuid, ) .await }); From b6f93d7a069b853889a61a440103691976ed161c Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Sun, 24 Aug 2025 17:53:24 -0600 Subject: [PATCH 107/126] Bump version --- Cargo.lock | 2 +- idevice/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1210618..453622f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1133,7 +1133,7 @@ dependencies = [ [[package]] name = "idevice" -version = "0.1.39" +version = "0.1.40" dependencies = [ "async-stream", "base64", diff --git a/idevice/Cargo.toml b/idevice/Cargo.toml index e69ac18..a69ede4 100644 --- a/idevice/Cargo.toml +++ b/idevice/Cargo.toml @@ -2,7 +2,7 @@ name = "idevice" description = "A Rust library to interact with services on iOS devices." authors = ["Jackson Coxson"] -version = "0.1.39" +version = "0.1.40" edition = "2024" license = "MIT" documentation = "https://docs.rs/idevice" From 584adc5014d8eaa200a52c726d8b26c31d8d748f Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Mon, 25 Aug 2025 15:53:01 -0600 Subject: [PATCH 108/126] Create xcproj for idevice++ --- .gitignore | 1 + cpp/idevice++.xcodeproj/project.pbxproj | 497 ++++++++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/xcschemes/idevice++.xcscheme | 67 +++ cpp/xcode_build_rust.sh | 130 +++++ 5 files changed, 702 insertions(+) create mode 100644 cpp/idevice++.xcodeproj/project.pbxproj create mode 100644 cpp/idevice++.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 cpp/idevice++.xcodeproj/xcshareddata/xcschemes/idevice++.xcscheme create mode 100755 cpp/xcode_build_rust.sh diff --git a/.gitignore b/.gitignore index 9a67810..e03e4e3 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ bundle.zip /swift/include/*.h /swift/.build /swift/.swiftpm +xcuserdata diff --git a/cpp/idevice++.xcodeproj/project.pbxproj b/cpp/idevice++.xcodeproj/project.pbxproj new file mode 100644 index 0000000..fda15b7 --- /dev/null +++ b/cpp/idevice++.xcodeproj/project.pbxproj @@ -0,0 +1,497 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + 198077932E5CA6EF00CB501E /* adapter_stream.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1980776C2E5CA69800CB501E /* adapter_stream.cpp */; }; + 198077942E5CA6EF00CB501E /* app_service.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1980776D2E5CA69800CB501E /* app_service.cpp */; }; + 198077952E5CA6EF00CB501E /* core_device.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1980776E2E5CA69800CB501E /* core_device.cpp */; }; + 198077962E5CA6EF00CB501E /* debug_proxy.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1980776F2E5CA69800CB501E /* debug_proxy.cpp */; }; + 198077972E5CA6EF00CB501E /* diagnosticsservice.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 198077702E5CA69800CB501E /* diagnosticsservice.cpp */; }; + 198077982E5CA6EF00CB501E /* ffi.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 198077712E5CA69800CB501E /* ffi.cpp */; }; + 198077992E5CA6EF00CB501E /* idevice.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 198077722E5CA69800CB501E /* idevice.cpp */; }; + 1980779A2E5CA6EF00CB501E /* location_simulation.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 198077732E5CA69800CB501E /* location_simulation.cpp */; }; + 1980779B2E5CA6EF00CB501E /* lockdown.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 198077742E5CA69800CB501E /* lockdown.cpp */; }; + 1980779C2E5CA6EF00CB501E /* pairing_file.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 198077752E5CA69800CB501E /* pairing_file.cpp */; }; + 1980779D2E5CA6EF00CB501E /* provider.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 198077762E5CA69800CB501E /* provider.cpp */; }; + 1980779E2E5CA6EF00CB501E /* remote_server.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 198077772E5CA69800CB501E /* remote_server.cpp */; }; + 1980779F2E5CA6EF00CB501E /* rsd.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 198077782E5CA69800CB501E /* rsd.cpp */; }; + 198077A02E5CA6EF00CB501E /* tcp_callback_feeder.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 198077792E5CA69800CB501E /* tcp_callback_feeder.cpp */; }; + 198077A12E5CA6EF00CB501E /* usbmuxd.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1980777A2E5CA69800CB501E /* usbmuxd.cpp */; }; + 198077B72E5CA6FC00CB501E /* core_device_proxy.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 198077A62E5CA6FC00CB501E /* core_device_proxy.hpp */; }; + 198077B82E5CA6FC00CB501E /* pairing_file.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 198077AD2E5CA6FC00CB501E /* pairing_file.hpp */; }; + 198077B92E5CA6FC00CB501E /* bindings.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 198077A52E5CA6FC00CB501E /* bindings.hpp */; }; + 198077BA2E5CA6FC00CB501E /* diagnosticsservice.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 198077A82E5CA6FC00CB501E /* diagnosticsservice.hpp */; }; + 198077BB2E5CA6FC00CB501E /* idevice.h in Headers */ = {isa = PBXBuildFile; fileRef = 198077B52E5CA6FC00CB501E /* idevice.h */; }; + 198077BC2E5CA6FC00CB501E /* debug_proxy.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 198077A72E5CA6FC00CB501E /* debug_proxy.hpp */; }; + 198077BD2E5CA6FC00CB501E /* ffi.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 198077A92E5CA6FC00CB501E /* ffi.hpp */; }; + 198077BE2E5CA6FC00CB501E /* rsd.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 198077B12E5CA6FC00CB501E /* rsd.hpp */; }; + 198077BF2E5CA6FC00CB501E /* tcp_object_stack.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 198077B22E5CA6FC00CB501E /* tcp_object_stack.hpp */; }; + 198077C02E5CA6FC00CB501E /* remote_server.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 198077B02E5CA6FC00CB501E /* remote_server.hpp */; }; + 198077C12E5CA6FC00CB501E /* readwrite.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 198077AF2E5CA6FC00CB501E /* readwrite.hpp */; }; + 198077C22E5CA6FC00CB501E /* location_simulation.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 198077AB2E5CA6FC00CB501E /* location_simulation.hpp */; }; + 198077C32E5CA6FC00CB501E /* adapter_stream.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 198077A32E5CA6FC00CB501E /* adapter_stream.hpp */; }; + 198077C42E5CA6FC00CB501E /* lockdown.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 198077AC2E5CA6FC00CB501E /* lockdown.hpp */; }; + 198077C52E5CA6FC00CB501E /* usbmuxd.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 198077B32E5CA6FC00CB501E /* usbmuxd.hpp */; }; + 198077C62E5CA6FC00CB501E /* app_service.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 198077A42E5CA6FC00CB501E /* app_service.hpp */; }; + 198077C72E5CA6FC00CB501E /* idevice.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 198077AA2E5CA6FC00CB501E /* idevice.hpp */; }; + 198077C82E5CA6FC00CB501E /* provider.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 198077AE2E5CA6FC00CB501E /* provider.hpp */; }; + 198077DF2E5CCA2900CB501E /* libidevice_ffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 198077DB2E5CC33000CB501E /* libidevice_ffi.a */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 1980776C2E5CA69800CB501E /* adapter_stream.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = adapter_stream.cpp; sourceTree = ""; }; + 1980776D2E5CA69800CB501E /* app_service.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = app_service.cpp; sourceTree = ""; }; + 1980776E2E5CA69800CB501E /* core_device.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = core_device.cpp; sourceTree = ""; }; + 1980776F2E5CA69800CB501E /* debug_proxy.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = debug_proxy.cpp; sourceTree = ""; }; + 198077702E5CA69800CB501E /* diagnosticsservice.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = diagnosticsservice.cpp; sourceTree = ""; }; + 198077712E5CA69800CB501E /* ffi.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = ffi.cpp; sourceTree = ""; }; + 198077722E5CA69800CB501E /* idevice.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = idevice.cpp; sourceTree = ""; }; + 198077732E5CA69800CB501E /* location_simulation.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = location_simulation.cpp; sourceTree = ""; }; + 198077742E5CA69800CB501E /* lockdown.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = lockdown.cpp; sourceTree = ""; }; + 198077752E5CA69800CB501E /* pairing_file.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = pairing_file.cpp; sourceTree = ""; }; + 198077762E5CA69800CB501E /* provider.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = provider.cpp; sourceTree = ""; }; + 198077772E5CA69800CB501E /* remote_server.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = remote_server.cpp; sourceTree = ""; }; + 198077782E5CA69800CB501E /* rsd.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = rsd.cpp; sourceTree = ""; }; + 198077792E5CA69800CB501E /* tcp_callback_feeder.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = tcp_callback_feeder.cpp; sourceTree = ""; }; + 1980777A2E5CA69800CB501E /* usbmuxd.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = usbmuxd.cpp; sourceTree = ""; }; + 1980778F2E5CA6C700CB501E /* libidevice++.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libidevice++.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 198077A32E5CA6FC00CB501E /* adapter_stream.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = adapter_stream.hpp; sourceTree = ""; }; + 198077A42E5CA6FC00CB501E /* app_service.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = app_service.hpp; sourceTree = ""; }; + 198077A52E5CA6FC00CB501E /* bindings.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = bindings.hpp; sourceTree = ""; }; + 198077A62E5CA6FC00CB501E /* core_device_proxy.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = core_device_proxy.hpp; sourceTree = ""; }; + 198077A72E5CA6FC00CB501E /* debug_proxy.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = debug_proxy.hpp; sourceTree = ""; }; + 198077A82E5CA6FC00CB501E /* diagnosticsservice.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = diagnosticsservice.hpp; sourceTree = ""; }; + 198077A92E5CA6FC00CB501E /* ffi.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = ffi.hpp; sourceTree = ""; }; + 198077AA2E5CA6FC00CB501E /* idevice.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = idevice.hpp; sourceTree = ""; }; + 198077AB2E5CA6FC00CB501E /* location_simulation.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = location_simulation.hpp; sourceTree = ""; }; + 198077AC2E5CA6FC00CB501E /* lockdown.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = lockdown.hpp; sourceTree = ""; }; + 198077AD2E5CA6FC00CB501E /* pairing_file.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = pairing_file.hpp; sourceTree = ""; }; + 198077AE2E5CA6FC00CB501E /* provider.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = provider.hpp; sourceTree = ""; }; + 198077AF2E5CA6FC00CB501E /* readwrite.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = readwrite.hpp; sourceTree = ""; }; + 198077B02E5CA6FC00CB501E /* remote_server.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = remote_server.hpp; sourceTree = ""; }; + 198077B12E5CA6FC00CB501E /* rsd.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = rsd.hpp; sourceTree = ""; }; + 198077B22E5CA6FC00CB501E /* tcp_object_stack.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = tcp_object_stack.hpp; sourceTree = ""; }; + 198077B32E5CA6FC00CB501E /* usbmuxd.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = usbmuxd.hpp; sourceTree = ""; }; + 198077B52E5CA6FC00CB501E /* idevice.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = idevice.h; sourceTree = ""; }; + 198077DB2E5CC33000CB501E /* libidevice_ffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libidevice_ffi.a; path = "${DESTINATION_PATH}/libidevice_ffi.a"; sourceTree = ""; }; + 198077E02E5CD49200CB501E /* xcode_build_rust.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = xcode_build_rust.sh; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 1980778D2E5CA6C700CB501E /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 198077DF2E5CCA2900CB501E /* libidevice_ffi.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 198077562E5CA62F00CB501E = { + isa = PBXGroup; + children = ( + 1980777B2E5CA69800CB501E /* src */, + 198077612E5CA62F00CB501E /* Products */, + 198077B62E5CA6FC00CB501E /* include */, + 198077D92E5CC31100CB501E /* Frameworks */, + 198077E02E5CD49200CB501E /* xcode_build_rust.sh */, + ); + sourceTree = ""; + }; + 198077612E5CA62F00CB501E /* Products */ = { + isa = PBXGroup; + children = ( + 1980778F2E5CA6C700CB501E /* libidevice++.a */, + ); + name = Products; + sourceTree = ""; + }; + 1980777B2E5CA69800CB501E /* src */ = { + isa = PBXGroup; + children = ( + 1980776C2E5CA69800CB501E /* adapter_stream.cpp */, + 1980776D2E5CA69800CB501E /* app_service.cpp */, + 1980776E2E5CA69800CB501E /* core_device.cpp */, + 1980776F2E5CA69800CB501E /* debug_proxy.cpp */, + 198077702E5CA69800CB501E /* diagnosticsservice.cpp */, + 198077712E5CA69800CB501E /* ffi.cpp */, + 198077722E5CA69800CB501E /* idevice.cpp */, + 198077732E5CA69800CB501E /* location_simulation.cpp */, + 198077742E5CA69800CB501E /* lockdown.cpp */, + 198077752E5CA69800CB501E /* pairing_file.cpp */, + 198077762E5CA69800CB501E /* provider.cpp */, + 198077772E5CA69800CB501E /* remote_server.cpp */, + 198077782E5CA69800CB501E /* rsd.cpp */, + 198077792E5CA69800CB501E /* tcp_callback_feeder.cpp */, + 1980777A2E5CA69800CB501E /* usbmuxd.cpp */, + ); + path = src; + sourceTree = ""; + }; + 198077B42E5CA6FC00CB501E /* idevice++ */ = { + isa = PBXGroup; + children = ( + 198077A32E5CA6FC00CB501E /* adapter_stream.hpp */, + 198077A42E5CA6FC00CB501E /* app_service.hpp */, + 198077A52E5CA6FC00CB501E /* bindings.hpp */, + 198077A62E5CA6FC00CB501E /* core_device_proxy.hpp */, + 198077A72E5CA6FC00CB501E /* debug_proxy.hpp */, + 198077A82E5CA6FC00CB501E /* diagnosticsservice.hpp */, + 198077A92E5CA6FC00CB501E /* ffi.hpp */, + 198077AA2E5CA6FC00CB501E /* idevice.hpp */, + 198077AB2E5CA6FC00CB501E /* location_simulation.hpp */, + 198077AC2E5CA6FC00CB501E /* lockdown.hpp */, + 198077AD2E5CA6FC00CB501E /* pairing_file.hpp */, + 198077AE2E5CA6FC00CB501E /* provider.hpp */, + 198077AF2E5CA6FC00CB501E /* readwrite.hpp */, + 198077B02E5CA6FC00CB501E /* remote_server.hpp */, + 198077B12E5CA6FC00CB501E /* rsd.hpp */, + 198077B22E5CA6FC00CB501E /* tcp_object_stack.hpp */, + 198077B32E5CA6FC00CB501E /* usbmuxd.hpp */, + ); + path = "idevice++"; + sourceTree = ""; + }; + 198077B62E5CA6FC00CB501E /* include */ = { + isa = PBXGroup; + children = ( + 198077B42E5CA6FC00CB501E /* idevice++ */, + 198077B52E5CA6FC00CB501E /* idevice.h */, + ); + path = include; + sourceTree = ""; + }; + 198077D92E5CC31100CB501E /* Frameworks */ = { + isa = PBXGroup; + children = ( + 198077DB2E5CC33000CB501E /* libidevice_ffi.a */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 1980778B2E5CA6C700CB501E /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 198077B72E5CA6FC00CB501E /* core_device_proxy.hpp in Headers */, + 198077B82E5CA6FC00CB501E /* pairing_file.hpp in Headers */, + 198077B92E5CA6FC00CB501E /* bindings.hpp in Headers */, + 198077BA2E5CA6FC00CB501E /* diagnosticsservice.hpp in Headers */, + 198077BB2E5CA6FC00CB501E /* idevice.h in Headers */, + 198077BC2E5CA6FC00CB501E /* debug_proxy.hpp in Headers */, + 198077BD2E5CA6FC00CB501E /* ffi.hpp in Headers */, + 198077BE2E5CA6FC00CB501E /* rsd.hpp in Headers */, + 198077BF2E5CA6FC00CB501E /* tcp_object_stack.hpp in Headers */, + 198077C02E5CA6FC00CB501E /* remote_server.hpp in Headers */, + 198077C12E5CA6FC00CB501E /* readwrite.hpp in Headers */, + 198077C22E5CA6FC00CB501E /* location_simulation.hpp in Headers */, + 198077C32E5CA6FC00CB501E /* adapter_stream.hpp in Headers */, + 198077C42E5CA6FC00CB501E /* lockdown.hpp in Headers */, + 198077C52E5CA6FC00CB501E /* usbmuxd.hpp in Headers */, + 198077C62E5CA6FC00CB501E /* app_service.hpp in Headers */, + 198077C72E5CA6FC00CB501E /* idevice.hpp in Headers */, + 198077C82E5CA6FC00CB501E /* provider.hpp in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 1980778E2E5CA6C700CB501E /* idevice++ */ = { + isa = PBXNativeTarget; + buildConfigurationList = 198077902E5CA6C700CB501E /* Build configuration list for PBXNativeTarget "idevice++" */; + buildPhases = ( + 198077E22E5CD4C900CB501E /* ShellScript */, + 1980778B2E5CA6C700CB501E /* Headers */, + 1980778C2E5CA6C700CB501E /* Sources */, + 1980778D2E5CA6C700CB501E /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "idevice++"; + packageProductDependencies = ( + ); + productName = "idevice++"; + productReference = 1980778F2E5CA6C700CB501E /* libidevice++.a */; + productType = "com.apple.product-type.library.static"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 198077572E5CA62F00CB501E /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1640; + LastUpgradeCheck = 1640; + TargetAttributes = { + 1980778E2E5CA6C700CB501E = { + CreatedOnToolsVersion = 16.4; + }; + }; + }; + buildConfigurationList = 1980775A2E5CA62F00CB501E /* Build configuration list for PBXProject "idevice++" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 198077562E5CA62F00CB501E; + minimizedProjectReferenceProxies = 1; + preferredProjectObjectVersion = 77; + productRefGroup = 198077612E5CA62F00CB501E /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 1980778E2E5CA6C700CB501E /* idevice++ */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXShellScriptBuildPhase section */ + 198077E22E5CD4C900CB501E /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + "", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "#!/bin/sh\n./xcode_build_rust.sh\n"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 1980778C2E5CA6C700CB501E /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 198077932E5CA6EF00CB501E /* adapter_stream.cpp in Sources */, + 198077942E5CA6EF00CB501E /* app_service.cpp in Sources */, + 198077952E5CA6EF00CB501E /* core_device.cpp in Sources */, + 198077962E5CA6EF00CB501E /* debug_proxy.cpp in Sources */, + 198077972E5CA6EF00CB501E /* diagnosticsservice.cpp in Sources */, + 198077982E5CA6EF00CB501E /* ffi.cpp in Sources */, + 198077992E5CA6EF00CB501E /* idevice.cpp in Sources */, + 1980779A2E5CA6EF00CB501E /* location_simulation.cpp in Sources */, + 1980779B2E5CA6EF00CB501E /* lockdown.cpp in Sources */, + 1980779C2E5CA6EF00CB501E /* pairing_file.cpp in Sources */, + 1980779D2E5CA6EF00CB501E /* provider.cpp in Sources */, + 1980779E2E5CA6EF00CB501E /* remote_server.cpp in Sources */, + 1980779F2E5CA6EF00CB501E /* rsd.cpp in Sources */, + 198077A02E5CA6EF00CB501E /* tcp_callback_feeder.cpp in Sources */, + 198077A12E5CA6EF00CB501E /* usbmuxd.cpp in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 198077672E5CA63000CB501E /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = 4FW3Q8784L; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 198077682E5CA63000CB501E /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = 4FW3Q8784L; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SWIFT_COMPILATION_MODE = wholemodule; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 198077912E5CA6C700CB501E /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 4FW3Q8784L; + EXECUTABLE_PREFIX = lib; + LIBRARY_SEARCH_PATHS = ( + "${TARGET_BUILD_DIR}/**", + "$(PROJECT_DIR)/${DESTINATION_PATH}", + ); + MACOSX_DEPLOYMENT_TARGET = 15.5; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator macosx watchos watchsimulator"; + SUPPORTS_MACCATALYST = NO; + TARGETED_DEVICE_FAMILY = "1,2,3,4"; + }; + name = Debug; + }; + 198077922E5CA6C700CB501E /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 4FW3Q8784L; + EXECUTABLE_PREFIX = lib; + LIBRARY_SEARCH_PATHS = ( + "${TARGET_BUILD_DIR}/**", + "$(PROJECT_DIR)/${DESTINATION_PATH}", + ); + MACOSX_DEPLOYMENT_TARGET = 15.5; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator macosx watchos watchsimulator"; + SUPPORTS_MACCATALYST = NO; + TARGETED_DEVICE_FAMILY = "1,2,3,4"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 1980775A2E5CA62F00CB501E /* Build configuration list for PBXProject "idevice++" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 198077672E5CA63000CB501E /* Debug */, + 198077682E5CA63000CB501E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 198077902E5CA6C700CB501E /* Build configuration list for PBXNativeTarget "idevice++" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 198077912E5CA6C700CB501E /* Debug */, + 198077922E5CA6C700CB501E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 198077572E5CA62F00CB501E /* Project object */; +} diff --git a/cpp/idevice++.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/cpp/idevice++.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/cpp/idevice++.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/cpp/idevice++.xcodeproj/xcshareddata/xcschemes/idevice++.xcscheme b/cpp/idevice++.xcodeproj/xcshareddata/xcschemes/idevice++.xcscheme new file mode 100644 index 0000000..779227d --- /dev/null +++ b/cpp/idevice++.xcodeproj/xcshareddata/xcschemes/idevice++.xcscheme @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/cpp/xcode_build_rust.sh b/cpp/xcode_build_rust.sh new file mode 100755 index 0000000..eab5b62 --- /dev/null +++ b/cpp/xcode_build_rust.sh @@ -0,0 +1,130 @@ +#!/bin/sh + +# This script builds a Rust library for use in an Xcode project. +# It's designed to be used as a "Run Script" build phase on a native Xcode target. +# It handles multiple architectures by building for each and combining them with `lipo`. + +# --- Configuration --- +# The name of your Rust crate (the name in Cargo.toml) +CRATE_NAME="idevice_ffi" +# The path to your Rust project's root directory (containing Cargo.toml) +RUST_PROJECT_PATH="${PROJECT_DIR}/../ffi" + +# --- Environment Setup --- +# Augment the PATH to include common locations for build tools like cmake and go. +export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/local/go/bin:$PATH" + +# --- Locate Cargo --- +# Xcode's build environment often has a minimal PATH, so we need to find cargo explicitly. +if [ -x "${HOME}/.cargo/bin/cargo" ]; then + CARGO="${HOME}/.cargo/bin/cargo" +else + CARGO=$(command -v cargo) + if [ -z "$CARGO" ]; then + echo "Error: cargo executable not found." >&2 + echo "Please ensure Rust is installed and cargo is in your PATH or at ~/.cargo/bin/" >&2 + exit 1 + fi +fi + +# --- Script Logic --- + +# Exit immediately if a command exits with a non-zero status. +set -e + +# --- Platform & SDK Configuration --- +# In a "Run Script" phase on a native target, PLATFORM_NAME is reliable. +# We use it to determine the correct SDK and build parameters. +PLATFORM_SUFFIX="" +SDK_NAME="" + +if [ "$PLATFORM_NAME" = "iphoneos" ]; then + PLATFORM_SUFFIX="-iphoneos" + SDK_NAME="iphoneos" +elif [ "$PLATFORM_NAME" = "iphonesimulator" ]; then + PLATFORM_SUFFIX="-iphonesimulator" + SDK_NAME="iphonesimulator" +elif [ "$PLATFORM_NAME" = "macosx" ]; then + PLATFORM_SUFFIX="" + SDK_NAME="macosx" +else + echo "Error: Unsupported platform '$PLATFORM_NAME'" >&2 + exit 1 +fi + +# Get the SDK path. This is crucial for cross-compilation. +SDK_PATH=$(xcrun --sdk ${SDK_NAME} --show-sdk-path) +echo "Configured for cross-compilation with SDK: ${SDK_PATH}" + +# Export variables needed by crates like `bindgen` to find the correct headers. +export BINDGEN_EXTRA_CLANG_ARGS="--sysroot=${SDK_PATH}" +export SDKROOT="${SDK_PATH}" # Also respected by some build scripts. + +STATIC_LIB_NAME="lib$(echo $CRATE_NAME | sed 's/-/_/g').a" +LIPO_INPUT_FILES="" + +# Determine if this is a release or debug build. +if [ "$CONFIGURATION" = "Release" ]; then + RELEASE_FLAG="--release" + RUST_BUILD_SUBDIR="release" +else + RELEASE_FLAG="" + RUST_BUILD_SUBDIR="debug" +fi + +# Loop through each architecture specified by Xcode. +for ARCH in $ARCHS; do + # Determine the Rust target triple based on the architecture and platform. + if [ "$PLATFORM_NAME" = "macosx" ]; then + if [ "$ARCH" = "arm64" ]; then + RUST_TARGET="aarch64-apple-darwin" + else + RUST_TARGET="x86_64-apple-darwin" + fi + elif [ "$PLATFORM_NAME" = "iphoneos" ]; then + RUST_TARGET="aarch64-apple-ios" + elif [ "$PLATFORM_NAME" = "iphonesimulator" ]; then + if [ "$ARCH" = "arm64" ]; then + RUST_TARGET="aarch64-apple-ios-sim" + else + RUST_TARGET="x86_64-apple-ios" + fi + fi + + echo "Building for arch: ${ARCH}, Rust target: ${RUST_TARGET}" + + # --- Configure Linker for Cargo --- + # Use RUSTFLAGS to pass linker arguments directly to rustc. This is the most + # reliable way to ensure the linker finds system libraries in the correct SDK. + export RUSTFLAGS="-C link-arg=-L${SDK_PATH}/usr/lib" + # export PATH="${SDK_PATH}:$PATH" + + # Run the cargo build command. It will inherit the exported RUSTFLAGS. + (cd "$RUST_PROJECT_PATH" && ${CARGO} build ${RELEASE_FLAG} --target ${RUST_TARGET}) + + BUILT_LIB_PATH="${RUST_PROJECT_PATH}/../target/${RUST_TARGET}/${RUST_BUILD_SUBDIR}/${STATIC_LIB_NAME}" + + # Add the path of the built library to our list for `lipo`. + LIPO_INPUT_FILES="${LIPO_INPUT_FILES} ${BUILT_LIB_PATH}" +done + +# --- Universal Library Creation --- + +# Construct the correct, platform-specific destination directory. +DESTINATION_DIR="${BUILT_PRODUCTS_DIR}" +mkdir -p "${DESTINATION_DIR}" +DESTINATION_PATH="${DESTINATION_DIR}/${STATIC_LIB_NAME}" + +echo "Creating universal library for architectures: $ARCHS" +echo "Input files: ${LIPO_INPUT_FILES}" +echo "Output path: ${DESTINATION_PATH}" + +# Use `lipo` to combine the individual architecture libraries into one universal library. +lipo -create ${LIPO_INPUT_FILES} -output "${DESTINATION_PATH}" + +echo "Universal library created successfully." + +# Verify the architectures in the final library. +lipo -info "${DESTINATION_PATH}" + +echo "Rust build script finished successfully." From 8846f9a429b63c4af43c0af432c9061078710fc4 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Mon, 25 Aug 2025 16:12:34 -0600 Subject: [PATCH 109/126] Copy idevice.h to cpp include folder during xcode build Move to include folder --- cpp/xcode_build_rust.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cpp/xcode_build_rust.sh b/cpp/xcode_build_rust.sh index eab5b62..c67e474 100755 --- a/cpp/xcode_build_rust.sh +++ b/cpp/xcode_build_rust.sh @@ -1,5 +1,7 @@ #!/bin/sh +cp ../ffi/idevice.h include/ + # This script builds a Rust library for use in an Xcode project. # It's designed to be used as a "Run Script" build phase on a native Xcode target. # It handles multiple architectures by building for each and combining them with `lipo`. From dff0c62ec72cfd3138a2f6c92a96c676fcb50b9e Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Mon, 25 Aug 2025 17:06:36 -0600 Subject: [PATCH 110/126] Always compile with release flag a --- Cargo.toml | 8 +++++++- cpp/xcode_build_rust.sh | 13 ++----------- ffi/Cargo.toml | 2 +- 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 55f15b7..89e80ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,4 +1,10 @@ [workspace] resolver = "2" -members = [ "ffi","idevice", "tools"] +members = ["ffi", "idevice", "tools"] + +[profile.release] +opt-level = "z" +codegen-units = 1 +lto = true +panic = "abort" diff --git a/cpp/xcode_build_rust.sh b/cpp/xcode_build_rust.sh index c67e474..00b1b5a 100755 --- a/cpp/xcode_build_rust.sh +++ b/cpp/xcode_build_rust.sh @@ -65,15 +65,6 @@ export SDKROOT="${SDK_PATH}" # Also respected by some build scripts. STATIC_LIB_NAME="lib$(echo $CRATE_NAME | sed 's/-/_/g').a" LIPO_INPUT_FILES="" -# Determine if this is a release or debug build. -if [ "$CONFIGURATION" = "Release" ]; then - RELEASE_FLAG="--release" - RUST_BUILD_SUBDIR="release" -else - RELEASE_FLAG="" - RUST_BUILD_SUBDIR="debug" -fi - # Loop through each architecture specified by Xcode. for ARCH in $ARCHS; do # Determine the Rust target triple based on the architecture and platform. @@ -102,9 +93,9 @@ for ARCH in $ARCHS; do # export PATH="${SDK_PATH}:$PATH" # Run the cargo build command. It will inherit the exported RUSTFLAGS. - (cd "$RUST_PROJECT_PATH" && ${CARGO} build ${RELEASE_FLAG} --target ${RUST_TARGET}) + (cd "$RUST_PROJECT_PATH" && ${CARGO} build --release --target ${RUST_TARGET}) - BUILT_LIB_PATH="${RUST_PROJECT_PATH}/../target/${RUST_TARGET}/${RUST_BUILD_SUBDIR}/${STATIC_LIB_NAME}" + BUILT_LIB_PATH="${RUST_PROJECT_PATH}/../target/${RUST_TARGET}/release/${STATIC_LIB_NAME}" # Add the path of the built library to our list for `lipo`. LIPO_INPUT_FILES="${LIPO_INPUT_FILES} ${BUILT_LIB_PATH}" diff --git a/ffi/Cargo.toml b/ffi/Cargo.toml index a1e1339..833c700 100644 --- a/ffi/Cargo.toml +++ b/ffi/Cargo.toml @@ -80,4 +80,4 @@ cbindgen = "0.29.0" ureq = "3" [lib] -crate-type = ["staticlib"] +crate-type = ["staticlib", "cdylib"] From f18b607bcaea7546155f7ad16c7176ed9bd0495b Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Mon, 25 Aug 2025 21:26:10 -0600 Subject: [PATCH 111/126] Add visual studio solution to idevice++ --- .gitignore | 4 + cpp/sln/idevice++.sln | 37 ++++ cpp/sln/idevice++.vcxproj | 340 ++++++++++++++++++++++++++++++ cpp/sln/idevice++.vcxproj.filters | 120 +++++++++++ cpp/vs_build_rust.bat | 62 ++++++ 5 files changed, 563 insertions(+) create mode 100644 cpp/sln/idevice++.sln create mode 100644 cpp/sln/idevice++.vcxproj create mode 100644 cpp/sln/idevice++.vcxproj.filters create mode 100644 cpp/vs_build_rust.bat diff --git a/.gitignore b/.gitignore index e03e4e3..8874511 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,7 @@ bundle.zip /swift/.build /swift/.swiftpm xcuserdata + +._* +*.vcxproj.user +.vs diff --git a/cpp/sln/idevice++.sln b/cpp/sln/idevice++.sln new file mode 100644 index 0000000..2debadb --- /dev/null +++ b/cpp/sln/idevice++.sln @@ -0,0 +1,37 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36327.8 d17.14 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "idevice++", "idevice++.vcxproj", "{EBC5A8CF-BC80-454B-95B5-F2D14770A41D}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|ARM64 = Debug|ARM64 + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|ARM64 = Release|ARM64 + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {EBC5A8CF-BC80-454B-95B5-F2D14770A41D}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {EBC5A8CF-BC80-454B-95B5-F2D14770A41D}.Debug|ARM64.Build.0 = Debug|ARM64 + {EBC5A8CF-BC80-454B-95B5-F2D14770A41D}.Debug|x64.ActiveCfg = Debug|x64 + {EBC5A8CF-BC80-454B-95B5-F2D14770A41D}.Debug|x64.Build.0 = Debug|x64 + {EBC5A8CF-BC80-454B-95B5-F2D14770A41D}.Debug|x86.ActiveCfg = Debug|Win32 + {EBC5A8CF-BC80-454B-95B5-F2D14770A41D}.Debug|x86.Build.0 = Debug|Win32 + {EBC5A8CF-BC80-454B-95B5-F2D14770A41D}.Release|ARM64.ActiveCfg = Release|ARM64 + {EBC5A8CF-BC80-454B-95B5-F2D14770A41D}.Release|ARM64.Build.0 = Release|ARM64 + {EBC5A8CF-BC80-454B-95B5-F2D14770A41D}.Release|x64.ActiveCfg = Release|x64 + {EBC5A8CF-BC80-454B-95B5-F2D14770A41D}.Release|x64.Build.0 = Release|x64 + {EBC5A8CF-BC80-454B-95B5-F2D14770A41D}.Release|x86.ActiveCfg = Release|Win32 + {EBC5A8CF-BC80-454B-95B5-F2D14770A41D}.Release|x86.Build.0 = Release|Win32 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {9A195DE0-F99B-4101-80B4-C1CFC7BFC06F} + EndGlobalSection +EndGlobal diff --git a/cpp/sln/idevice++.vcxproj b/cpp/sln/idevice++.vcxproj new file mode 100644 index 0000000..9b28b3a --- /dev/null +++ b/cpp/sln/idevice++.vcxproj @@ -0,0 +1,340 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + Debug + ARM64 + + + Release + ARM64 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 17.0 + Win32Proj + {ebc5a8cf-bc80-454b-95b5-f2d14770a41d} + idevice + 10.0 + + + + StaticLibrary + true + v143 + Unicode + + + StaticLibrary + false + v143 + true + Unicode + + + StaticLibrary + true + v143 + Unicode + + + StaticLibrary + false + v143 + true + Unicode + + + StaticLibrary + true + v143 + Unicode + + + StaticLibrary + false + v143 + true + Unicode + + + + + + + + + + + + + + + + + + + + + + + + + + + $(VC_IncludePath);$(WindowsSDK_IncludePath);$(ProjectDir)..\include; + $(VC_IncludePath);$(WindowsSDK_IncludePath);$(ProjectDir)..\include; + $(SolutionDir)Build\64\$(Configuration)\ + $(SolutionDir)Build\64\$(Configuration)\Temp\$(ProjectName)\ + + + $(VC_IncludePath);$(WindowsSDK_IncludePath); + $(VC_IncludePath);$(WindowsSDK_IncludePath);$(ProjectDir)..\ + $(SolutionDir)Build\64\$(Configuration)\ + $(SolutionDir)Build\64\$(Configuration)\Temp\$(ProjectName)\ + + + $(VC_IncludePath);$(WindowsSDK_IncludePath);$(ProjectDir)..\include; + $(VC_IncludePath);$(WindowsSDK_IncludePath);$(ProjectDir)..\include; + $(SolutionDir)Build\aarch64\$(Configuration)\ + $(SolutionDir)Build\aarch64\$(Configuration)\Temp\$(ProjectName)\ + + + $(VC_IncludePath);$(WindowsSDK_IncludePath);$(ProjectDir)..\ + $(SolutionDir)Build\aarch64\$(Configuration)\ + $(SolutionDir)Build\aarch64\$(Configuration)\Temp\$(ProjectName)\ + + + $(VC_IncludePath);$(WindowsSDK_IncludePath);$(ProjectDir)..\include; + $(VC_IncludePath);$(WindowsSDK_IncludePath);$(ProjectDir)..\include; + $(SolutionDir)Build\32\$(Configuration)\ + $(SolutionDir)Build\32\$(Configuration)\Temp\$(ProjectName)\ + + + $(VC_IncludePath);$(WindowsSDK_IncludePath);$(ProjectDir)..\ + $(SolutionDir)Build\32\$(Configuration)\ + $(SolutionDir)Build\32\$(Configuration)\Temp\$(ProjectName)\ + + + + Level3 + true + WIN32;_DEBUG;_LIB;%(PreprocessorDefinitions) + true + $(ProjectDir)..\include;$(ProjectDir)..\..\ffi + + + stdcpp20 + + + + + true + + + call "$(ProjectDir)vs_build_rust.bat" $(Platform) $(OutDir) + + + idevice_ffi.lib + $(OutDir) + + + + + Level3 + true + true + true + WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions) + true + + + + + $(ProjectDir)..\include;$(ProjectDir)..\..\ffi + stdcpp20 + + + + + true + + + call "$(ProjectDir)vs_build_rust.bat" $(Platform) $(OutDir) + + + idevice_ffi.lib + $(OutDir) + + + + + Level3 + true + _DEBUG;_LIB;%(PreprocessorDefinitions) + true + + + + + $(ProjectDir)..\include;$(ProjectDir)..\..\ffi + stdcpp20 + + + + + true + + + call "$(ProjectDir)..\vs_build_rust.bat" "$(Platform)" "$(OutDir)" + + + call "$(ProjectDir)vs_build_rust.bat" $(Platform) $(OutDir) + + + idevice_ffi.lib + $(OutDir) + + + + + Level3 + true + true + true + NDEBUG;_LIB;%(PreprocessorDefinitions) + true + + + + + $(ProjectDir)..\include;$(ProjectDir)..\..\ffi + stdcpp20 + + + + + true + + + call "$(ProjectDir)..\vs_build_rust.bat" "$(Platform)" "$(OutDir)" + + + call "$(ProjectDir)vs_build_rust.bat" $(Platform) $(OutDir) + + + idevice_ffi.lib + $(OutDir) + + + + + Level3 + true + _DEBUG;_LIB;%(PreprocessorDefinitions) + true + + + + + $(ProjectDir)..\include;$(ProjectDir)..\..\ffi + stdcpp20 + + + + + true + + + call "$(ProjectDir)vs_build_rust.bat" $(Platform) $(OutDir) + + + idevice_ffi.lib + $(OutDir) + + + + + Level3 + true + true + true + NDEBUG;_LIB;%(PreprocessorDefinitions) + true + + + + + $(ProjectDir)..\include;$(ProjectDir)..\..\ffi + stdcpp20 + + + + + true + + + call "$(ProjectDir)vs_build_rust.bat" $(Platform) $(OutDir) + + + idevice_ffi.lib + $(OutDir) + + + + + + \ No newline at end of file diff --git a/cpp/sln/idevice++.vcxproj.filters b/cpp/sln/idevice++.vcxproj.filters new file mode 100644 index 0000000..2f6b401 --- /dev/null +++ b/cpp/sln/idevice++.vcxproj.filters @@ -0,0 +1,120 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + {b4e9aee7-7e94-4e5f-b443-09677e8d69c2} + + + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + + + Header Files\idevice++ + + + Header Files\idevice++ + + + Header Files\idevice++ + + + Header Files\idevice++ + + + Header Files\idevice++ + + + Header Files\idevice++ + + + Header Files\idevice++ + + + Header Files\idevice++ + + + Header Files\idevice++ + + + Header Files\idevice++ + + + Header Files\idevice++ + + + Header Files\idevice++ + + + Header Files\idevice++ + + + Header Files\idevice++ + + + Header Files\idevice++ + + + Header Files\idevice++ + + + Header Files\idevice++ + + + \ No newline at end of file diff --git a/cpp/vs_build_rust.bat b/cpp/vs_build_rust.bat new file mode 100644 index 0000000..438fd9e --- /dev/null +++ b/cpp/vs_build_rust.bat @@ -0,0 +1,62 @@ +@echo off +setlocal + +REM --- Configuration --- +SET "CRATE_NAME=idevice_ffi" +SET "RUST_PROJECT_PATH=%~dp0..\ffi" + +echo "--- Rust Build Script Started ---" +echo "Rust Project Path: %RUST_PROJECT_PATH%" +echo "Visual Studio Platform: %1" + +REM --- Header File Copy --- +xcopy /Y "%RUST_PROJECT_PATH%\idevice.h" "%~dp0\include\" + +REM --- Locate Cargo --- +REM Check if cargo is in the PATH. +where cargo >nul 2>nul +if %errorlevel% neq 0 ( + echo Error: cargo.exe not found in PATH. + echo Please ensure the Rust toolchain is installed and configured. + exit /b 1 +) + +REM --- Determine Rust Target --- +SET "RUST_TARGET=" +IF /I "%~1" == "x64" ( + SET "RUST_TARGET=x86_64-pc-windows-msvc" +) +IF /I "%~1" == "ARM64" ( + SET "RUST_TARGET=aarch64-pc-windows-msvc" +) + +IF NOT DEFINED RUST_TARGET ( + echo Error: Unsupported Visual Studio platform '%~1'. + echo This script supports 'x64' and 'ARM64'. + exit /b 1 +) + +echo "Building for Rust target: %RUST_TARGET%" + +REM --- Run Cargo Build --- +SET "STATIC_LIB_NAME=%CRATE_NAME%.lib" +SET "BUILT_LIB_PATH=%RUST_PROJECT_PATH%\..\target\%RUST_TARGET%\release\%STATIC_LIB_NAME%" + +REM Change to the Rust project directory and run the build. +pushd "%RUST_PROJECT_PATH%" +cargo build --release --target %RUST_TARGET% --features ring,full --no-default-features +if %errorlevel% neq 0 ( + echo Error: Cargo build failed. + popd + exit /b 1 +) +popd + +echo "Cargo build successful." + +REM --- Copy Artifacts --- +echo "Copying '%BUILT_LIB_PATH%' to '%2'" +xcopy /Y "%BUILT_LIB_PATH%" "%2" + +echo "--- Rust Build Script Finished ---" +exit /b 0 From 7baa8a73b5a48bd2511c46c1b255e176cfc368c2 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Mon, 25 Aug 2025 21:41:34 -0600 Subject: [PATCH 112/126] Support Win32 cpp build --- cpp/sln/idevice++.vcxproj | 12 ++++++++++++ cpp/vs_build_rust.bat | 3 +++ 2 files changed, 15 insertions(+) diff --git a/cpp/sln/idevice++.vcxproj b/cpp/sln/idevice++.vcxproj index 9b28b3a..a3fe149 100644 --- a/cpp/sln/idevice++.vcxproj +++ b/cpp/sln/idevice++.vcxproj @@ -191,6 +191,9 @@ idevice_ffi.lib $(OutDir) + + call "$(ProjectDir)..\vs_build_rust.bat" "$(Platform)" "$(OutDir)" + @@ -219,6 +222,9 @@ idevice_ffi.lib $(OutDir) + + call "$(ProjectDir)..\vs_build_rust.bat" "$(Platform)" "$(OutDir)" + @@ -305,6 +311,9 @@ idevice_ffi.lib $(OutDir) + + call "$(ProjectDir)..\vs_build_rust.bat" "$(Platform)" "$(OutDir)" + @@ -333,6 +342,9 @@ idevice_ffi.lib $(OutDir) + + call "$(ProjectDir)..\vs_build_rust.bat" "$(Platform)" "$(OutDir)" + diff --git a/cpp/vs_build_rust.bat b/cpp/vs_build_rust.bat index 438fd9e..1c15458 100644 --- a/cpp/vs_build_rust.bat +++ b/cpp/vs_build_rust.bat @@ -29,6 +29,9 @@ IF /I "%~1" == "x64" ( IF /I "%~1" == "ARM64" ( SET "RUST_TARGET=aarch64-pc-windows-msvc" ) +IF /I "%~1" == "Win32" ( + SET "RUST_TARGET=i686-pc-windows-msvc" +) IF NOT DEFINED RUST_TARGET ( echo Error: Unsupported Visual Studio platform '%~1'. From 4fde7cf06beb44cddb01c62d38d73313420ea4e0 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Wed, 27 Aug 2025 11:38:44 -0600 Subject: [PATCH 113/126] Timeout on port connect syn --- ffi/src/core_device/diagnosticsservice.rs | 6 ++++++ idevice/src/services/rsd.rs | 6 +++++- idevice/src/tcp/adapter.rs | 20 +++++++++++++++++--- idevice/src/tcp/handle.rs | 11 ++++++++--- idevice/src/tcp/packets.rs | 14 ++++++++++++-- 5 files changed, 48 insertions(+), 9 deletions(-) diff --git a/ffi/src/core_device/diagnosticsservice.rs b/ffi/src/core_device/diagnosticsservice.rs index 24476db..31ca02a 100644 --- a/ffi/src/core_device/diagnosticsservice.rs +++ b/ffi/src/core_device/diagnosticsservice.rs @@ -7,6 +7,7 @@ use std::ptr::null_mut; use futures::{Stream, StreamExt}; use idevice::core_device::DiagnostisServiceClient; use idevice::{IdeviceError, ReadWrite, RsdService}; +use log::debug; use crate::core_device_proxy::AdapterHandle; use crate::rsd::RsdHandshakeHandle; @@ -45,12 +46,17 @@ pub unsafe extern "C" fn diagnostics_service_connect_rsd( RUNTIME.block_on(async move { let provider_ref = unsafe { &mut (*provider).0 }; let handshake_ref = unsafe { &mut (*handshake).0 }; + debug!( + "Connecting to DiagnosticsService: provider {provider_ref:?}, handshake: {:?}", + handshake_ref.uuid + ); DiagnostisServiceClient::connect_rsd(provider_ref, handshake_ref).await }); match res { Ok(client) => { + debug!("Connected to DiagnosticsService"); let boxed = Box::new(DiagnosticsServiceHandle(client)); unsafe { *handle = Box::into_raw(boxed) }; null_mut() diff --git a/idevice/src/services/rsd.rs b/idevice/src/services/rsd.rs index 4013816..1f2555a 100644 --- a/idevice/src/services/rsd.rs +++ b/idevice/src/services/rsd.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; -use log::warn; +use log::{debug, warn}; use serde::Deserialize; use crate::{IdeviceError, ReadWrite, RemoteXpcClient, provider::RsdProvider}; @@ -169,6 +169,10 @@ impl RsdHandshake { } }; + debug!( + "Connecting to RSD service {service_name} on port {}", + service.port + ); let stream = provider.connect_to_service_port(service.port).await?; T::from_stream(stream).await } diff --git a/idevice/src/tcp/adapter.rs b/idevice/src/tcp/adapter.rs index c366992..9d60868 100644 --- a/idevice/src/tcp/adapter.rs +++ b/idevice/src/tcp/adapter.rs @@ -205,6 +205,7 @@ impl Adapter { // Wait for the syn ack self.states.insert(host_port, state); + let start_time = std::time::Instant::now(); loop { self.process_tcp_packet().await?; if let Some(s) = self.states.get(&host_port) { @@ -216,6 +217,12 @@ impl Adapter { return Err(std::io::Error::new(e, "failed to connect")); } ConnectionStatus::WaitingForSyn => { + if start_time.elapsed() > std::time::Duration::from_secs(5) { + return Err(std::io::Error::new( + std::io::ErrorKind::TimedOut, + "didn't syn in time", + )); + } continue; } } @@ -526,7 +533,7 @@ impl Adapter { self.write_buffer_flush().await?; Ok(loop { // try the data we already have - match Ipv6Packet::parse(&self.read_buf[..self.bytes_in_buf]) { + match Ipv6Packet::parse(&self.read_buf[..self.bytes_in_buf], &self.pcap) { IpParseError::Ok { packet, bytes_consumed, @@ -559,8 +566,15 @@ impl Adapter { } pub(crate) async fn process_tcp_packet(&mut self) -> Result<(), std::io::Error> { - let ip_packet = self.read_ip_packet().await?; - self.process_tcp_packet_from_payload(&ip_packet).await + tokio::select! { + ip_packet = self.read_ip_packet() => { + let ip_packet = ip_packet?; + self.process_tcp_packet_from_payload(&ip_packet).await + } + _ = tokio::time::sleep(std::time::Duration::from_secs(15)) => { + Ok(()) + } + } } pub(crate) async fn process_tcp_packet_from_payload( diff --git a/idevice/src/tcp/handle.rs b/idevice/src/tcp/handle.rs index 7da3aff..183704b 100644 --- a/idevice/src/tcp/handle.rs +++ b/idevice/src/tcp/handle.rs @@ -12,6 +12,7 @@ use log::trace; use tokio::{ io::{AsyncRead, AsyncWrite}, sync::oneshot, + time::timeout, }; use crate::tcp::adapter::ConnectionStatus; @@ -172,8 +173,8 @@ impl AdapterHandle { )); } - match res_rx.await { - Ok(r) => { + match timeout(std::time::Duration::from_secs(8), res_rx).await { + Ok(Ok(r)) => { let (host_port, recv_channel) = r?; Ok(StreamHandle { host_port, @@ -183,10 +184,14 @@ impl AdapterHandle { pending_writes: FuturesUnordered::new(), }) } - Err(_) => Err(std::io::Error::new( + Ok(Err(_)) => Err(std::io::Error::new( std::io::ErrorKind::BrokenPipe, "adapter closed", )), + Err(_) => Err(std::io::Error::new( + std::io::ErrorKind::TimedOut, + "channel recv timeout", + )), } } diff --git a/idevice/src/tcp/packets.rs b/idevice/src/tcp/packets.rs index 251dc14..4ae328f 100644 --- a/idevice/src/tcp/packets.rs +++ b/idevice/src/tcp/packets.rs @@ -230,7 +230,10 @@ pub(crate) enum IpParseError { } impl Ipv6Packet { - pub(crate) fn parse(packet: &[u8]) -> IpParseError { + pub(crate) fn parse( + packet: &[u8], + log: &Option>>, + ) -> IpParseError { if packet.len() < 40 { return IpParseError::NotEnough; } @@ -275,6 +278,13 @@ impl Ipv6Packet { ); let payload = packet[40..total_packet_len].to_vec(); + if let Some(log) = log { + let mut log_packet = Vec::new(); + log_packet.extend_from_slice(&packet[..40]); + log_packet.extend_from_slice(&payload); + super::log_packet(log, &log_packet); + } + IpParseError::Ok { packet: Self { version, @@ -699,7 +709,7 @@ mod tests { ); println!("{b1:02X?}"); - let ip1 = Ipv6Packet::parse(&b1); + let ip1 = Ipv6Packet::parse(&b1, &None); println!("{ip1:#?}"); } From 1169408da1795db2a2f350ca11ed2a58532bd668 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Fri, 29 Aug 2025 14:19:28 -0600 Subject: [PATCH 114/126] Remove cpp 17 features and implement Rust into CPP --- cpp/.clang-format | 4 + cpp/examples/CMakeLists.txt | 2 +- cpp/examples/app_service.cpp | 145 ++++---- cpp/examples/debug_proxy.cpp | 105 +++--- cpp/examples/diagnosticsservice.cpp | 135 ++++---- cpp/examples/idevice_id.cpp | 23 +- cpp/examples/ideviceinfo.cpp | 73 +++-- cpp/examples/location_simulation.cpp | 105 +++--- cpp/idevice++.xcodeproj/project.pbxproj | 24 ++ cpp/include/idevice++/adapter_stream.hpp | 14 +- cpp/include/idevice++/app_service.hpp | 76 ++--- cpp/include/idevice++/core_device_proxy.hpp | 63 ++-- cpp/include/idevice++/debug_proxy.hpp | 36 +- cpp/include/idevice++/diagnosticsservice.hpp | 25 +- cpp/include/idevice++/ffi.hpp | 5 +- cpp/include/idevice++/idevice.hpp | 18 +- cpp/include/idevice++/location_simulation.hpp | 8 +- cpp/include/idevice++/lockdown.hpp | 12 +- cpp/include/idevice++/option.hpp | 184 +++++++++++ cpp/include/idevice++/pairing_file.hpp | 12 +- cpp/include/idevice++/provider.hpp | 42 ++- cpp/include/idevice++/remote_server.hpp | 6 +- cpp/include/idevice++/result.hpp | 231 +++++++++++++ cpp/include/idevice++/rsd.hpp | 12 +- cpp/include/idevice++/tcp_object_stack.hpp | 45 +-- cpp/include/idevice++/usbmuxd.hpp | 63 ++-- cpp/src/adapter_stream.cpp | 67 ++-- cpp/src/app_service.cpp | 310 +++++++++--------- cpp/src/core_device.cpp | 165 +++++----- cpp/src/debug_proxy.cpp | 206 ++++++------ cpp/src/diagnosticsservice.cpp | 74 +++-- cpp/src/ffi.cpp | 15 + cpp/src/idevice.cpp | 57 ++-- cpp/src/location_simulation.cpp | 30 +- cpp/src/lockdown.cpp | 61 ++-- cpp/src/pairing_file.cpp | 36 +- cpp/src/provider.cpp | 63 ++-- cpp/src/remote_server.cpp | 21 +- cpp/src/rsd.cpp | 82 ++--- cpp/src/tcp_callback_feeder.cpp | 61 ++-- cpp/src/usbmuxd.cpp | 134 ++++---- 41 files changed, 1638 insertions(+), 1212 deletions(-) create mode 100644 cpp/include/idevice++/option.hpp create mode 100644 cpp/include/idevice++/result.hpp diff --git a/cpp/.clang-format b/cpp/.clang-format index b084d5a..4491888 100644 --- a/cpp/.clang-format +++ b/cpp/.clang-format @@ -26,3 +26,7 @@ ReflowComments: true PointerAlignment: Left BinPackArguments: false BinPackParameters: false +AllowShortBlocksOnASingleLine: Never +MacroBlockBegin: "^#define" +MacroBlockEnd: "^#undef" +InsertBraces: true diff --git a/cpp/examples/CMakeLists.txt b/cpp/examples/CMakeLists.txt index f832b74..bce540a 100644 --- a/cpp/examples/CMakeLists.txt +++ b/cpp/examples/CMakeLists.txt @@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.15) project(IdeviceFFI CXX) -set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) diff --git a/cpp/examples/app_service.cpp b/cpp/examples/app_service.cpp index 6b5c861..c9cf136 100644 --- a/cpp/examples/app_service.cpp +++ b/cpp/examples/app_service.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -16,8 +17,9 @@ using namespace IdeviceFFI; +[[noreturn]] static void die(const char* msg, const FfiError& e) { - std::cerr << msg << ": " << e.message << "\n"; + std::cerr << msg << ": " << e.message << "(" << e.code << ")\n"; std::exit(1); } @@ -42,26 +44,25 @@ int main(int argc, char** argv) { FfiError err; // 1) Connect to usbmuxd and pick first device - auto mux = UsbmuxdConnection::default_new(/*tag*/ 0, err); - if (!mux) - die("failed to connect to usbmuxd", err); + auto mux = UsbmuxdConnection::default_new(/*tag*/ 0); + if_let_err(mux, err, { die("failed to connect to usbmuxd", err); }); - auto devices = mux->get_devices(err); - if (!devices) - die("failed to list devices", err); - if (devices->empty()) { + auto devices_res = mux.unwrap().get_devices(); + if_let_err(devices_res, err, { die("failed to list devices", err); }); + auto& devices = devices_res.unwrap(); + if (devices.empty()) { std::cerr << "no devices connected\n"; return 1; } - auto& dev = (*devices)[0]; + auto& dev = (devices)[0]; auto udid = dev.get_udid(); - if (!udid) { + if (udid.is_none()) { std::cerr << "device has no UDID\n"; return 1; } auto mux_id = dev.get_id(); - if (!mux_id) { + if (mux_id.is_none()) { std::cerr << "device has no mux id\n"; return 1; } @@ -72,53 +73,49 @@ int main(int argc, char** argv) { const uint32_t tag = 0; const std::string label = "app_service-jkcoxson"; - auto provider = Provider::usbmuxd_new(std::move(addr), tag, *udid, *mux_id, label, err); - if (!provider) - die("failed to create provider", err); + auto provider_res = + Provider::usbmuxd_new(std::move(addr), tag, udid.unwrap(), mux_id.unwrap(), label); + if_let_err(provider_res, err, { die("failed to create provider", err); }); + auto& provider = provider_res.unwrap(); // 3) CoreDeviceProxy - auto cdp = CoreDeviceProxy::connect(*provider, err); - if (!cdp) - die("failed to connect CoreDeviceProxy", err); + auto cdp = CoreDeviceProxy::connect(provider).unwrap_or_else( + [](FfiError e) -> CoreDeviceProxy { die("failed to connect CoreDeviceProxy", e); }); - auto rsd_port = cdp->get_server_rsd_port(err); - if (!rsd_port) - die("failed to get server RSD port", err); + auto rsd_port = cdp.get_server_rsd_port().unwrap_or_else( + [](FfiError err) -> uint16_t { die("failed to get server RSD port", err); }); // 4) Create software tunnel adapter (consumes proxy) - auto adapter = std::move(*cdp).create_tcp_adapter(err); - if (!adapter) - die("failed to create software tunnel adapter", err); + auto adapter = std::move(cdp).create_tcp_adapter(); + if_let_err(adapter, err, { die("failed to create software tunnel adapter", err); }); // 5) Connect adapter to RSD → ReadWrite stream - auto stream = adapter->connect(*rsd_port, err); - if (!stream) - die("failed to connect RSD stream", err); + auto stream = adapter.unwrap().connect(rsd_port); + if_let_err(stream, err, { die("failed to connect RSD stream", err); }); // 6) RSD handshake (consumes stream) - auto rsd = RsdHandshake::from_socket(std::move(*stream), err); - if (!rsd) - die("failed RSD handshake", err); + auto rsd = RsdHandshake::from_socket(std::move(stream.unwrap())); + if_let_err(rsd, err, { die("failed RSD handshake", err); }); // 7) AppService over RSD (borrows adapter + handshake) - auto app = AppService::connect_rsd(*adapter, *rsd, err); - if (!app) - die("failed to connect AppService", err); + auto app = AppService::connect_rsd(adapter.unwrap(), rsd.unwrap()) + .unwrap_or_else([&](FfiError e) -> AppService { + die("failed to connect AppService", e); // never returns + }); // 8) Commands if (cmd == "list") { - auto apps = app->list_apps(/*app_clips*/ true, - /*removable*/ true, - /*hidden*/ true, - /*internal*/ true, - /*default_apps*/ true, - err); - if (!apps) - die("list_apps failed", err); + auto apps = app.list_apps(/*app_clips*/ true, + /*removable*/ true, + /*hidden*/ true, + /*internal*/ true, + /*default_apps*/ true) + .unwrap_or_else( + [](FfiError e) -> std::vector { die("list_apps failed", e); }); - for (const auto& a : *apps) { - std::cout << "- " << a.bundle_identifier << " | name=" << a.name - << " | version=" << (a.version ? *a.version : std::string("")) + for (const auto& a : apps) { + std::cout << "- " << a.bundle_identifier << " | name=" << a.name << " | version=" + << (a.version.is_some() ? a.version.unwrap() : std::string("")) << " | dev=" << (a.is_developer_app ? "y" : "n") << " | hidden=" << (a.is_hidden ? "y" : "n") << "\n"; } @@ -132,27 +129,27 @@ int main(int argc, char** argv) { std::string bundle_id = argv[2]; std::vector args; // empty in this example - auto resp = app->launch(bundle_id, - args, - /*kill_existing*/ false, - /*start_suspended*/ false, - err); - if (!resp) - die("launch failed", err); + auto resp = + app.launch(bundle_id, + args, + /*kill_existing*/ false, + /*start_suspended*/ false) + .unwrap_or_else([](FfiError e) -> LaunchResponse { die("launch failed", e); }); - std::cout << "Launched pid=" << resp->pid << " exe=" << resp->executable_url - << " piv=" << resp->process_identifier_version - << " audit_token_len=" << resp->audit_token.size() << "\n"; + std::cout << "Launched pid=" << resp.pid << " exe=" << resp.executable_url + << " piv=" << resp.process_identifier_version + << " audit_token_len=" << resp.audit_token.size() << "\n"; return 0; } else if (cmd == "processes") { - auto procs = app->list_processes(err); - if (!procs) - die("list_processes failed", err); + auto procs = app.list_processes().unwrap_or_else( + [](FfiError e) -> std::vector { die("list_processes failed", e); }); - for (const auto& p : *procs) { + for (const auto& p : procs) { std::cout << p.pid << " : " - << (p.executable_url ? *p.executable_url : std::string("")) << "\n"; + << (p.executable_url.is_some() ? p.executable_url.unwrap() + : std::string("")) + << "\n"; } return 0; @@ -163,8 +160,7 @@ int main(int argc, char** argv) { } std::string bundle_id = argv[2]; - if (!app->uninstall(bundle_id, err)) - die("uninstall failed", err); + if_let_err(app.uninstall(bundle_id), err, { die("uninstall failed", err); }); std::cout << "Uninstalled " << bundle_id << "\n"; return 0; @@ -176,13 +172,14 @@ int main(int argc, char** argv) { uint32_t pid = static_cast(std::stoul(argv[2])); uint32_t signal = static_cast(std::stoul(argv[3])); - auto res = app->send_signal(pid, signal, err); - if (!res) - die("send_signal failed", err); + auto res = app.send_signal(pid, signal).unwrap_or_else([](FfiError e) -> SignalResponse { + die("send_signal failed", e); + }); - std::cout << "Signaled pid=" << res->pid << " signal=" << res->signal - << " ts_ms=" << res->device_timestamp_ms - << " exe=" << (res->executable_url ? *res->executable_url : std::string("")) + std::cout << "Signaled pid=" << res.pid << " signal=" << res.signal + << " ts_ms=" << res.device_timestamp_ms << " exe=" + << (res.executable_url.is_some() ? res.executable_url.unwrap() + : std::string("")) << "\n"; return 0; @@ -196,22 +193,22 @@ int main(int argc, char** argv) { float hw = (argc >= 5) ? std::stof(argv[4]) : 1.0f; float scale = (argc >= 6) ? std::stof(argv[5]) : 1.0f; - auto icon = app->fetch_icon(bundle_id, hw, hw, scale, /*allow_placeholder*/ true, err); - if (!icon) - die("fetch_app_icon failed", err); + auto icon = + app.fetch_icon(bundle_id, hw, hw, scale, /*allow_placeholder*/ true) + .unwrap_or_else([](FfiError e) -> IconData { die("fetch_app_icon failed", e); }); std::ofstream out(save_path, std::ios::binary); if (!out) { std::cerr << "Failed to open " << save_path << " for writing\n"; return 1; } - out.write(reinterpret_cast(icon->data.data()), - static_cast(icon->data.size())); + out.write(reinterpret_cast(icon.data.data()), + static_cast(icon.data.size())); out.close(); - std::cout << "Saved icon to " << save_path << " (" << icon->data.size() << " bytes, " - << icon->icon_width << "x" << icon->icon_height << ", min " << icon->minimum_width - << "x" << icon->minimum_height << ")\n"; + std::cout << "Saved icon to " << save_path << " (" << icon.data.size() << " bytes, " + << icon.icon_width << "x" << icon.icon_height << ", min " << icon.minimum_width + << "x" << icon.minimum_height << ")\n"; return 0; } else { diff --git a/cpp/examples/debug_proxy.cpp b/cpp/examples/debug_proxy.cpp index 55a24c4..182b85d 100644 --- a/cpp/examples/debug_proxy.cpp +++ b/cpp/examples/debug_proxy.cpp @@ -8,10 +8,14 @@ #include #include #include +#include #include #include #include +using namespace IdeviceFFI; + +[[noreturn]] static void die(const char* msg, const IdeviceFFI::FfiError& e) { std::cerr << msg << ": " << e.message << "\n"; std::exit(1); @@ -21,8 +25,9 @@ static std::vector split_args(const std::string& line) { std::istringstream iss(line); std::vector toks; std::string tok; - while (iss >> tok) + while (iss >> tok) { toks.push_back(tok); + } return toks; } @@ -30,103 +35,99 @@ int main() { IdeviceFFI::FfiError err; // 1) usbmuxd → pick first device - auto mux = IdeviceFFI::UsbmuxdConnection::default_new(/*tag*/ 0, err); - if (!mux) - die("failed to connect to usbmuxd", err); + auto mux = IdeviceFFI::UsbmuxdConnection::default_new(/*tag*/ 0); + if_let_err(mux, err, { die("failed to connect to usbmuxd", err); }); - auto devices = mux->get_devices(err); - if (!devices) - die("failed to list devices", err); - if (devices->empty()) { + auto devices = mux.unwrap().get_devices(); + if_let_err(devices, err, { die("failed to list devices", err); }); + if (devices.unwrap().empty()) { std::cerr << "no devices connected\n"; return 1; } - auto& dev = (*devices)[0]; + auto& dev = (devices.unwrap())[0]; auto udid = dev.get_udid(); - if (!udid) { + if (udid.is_none()) { std::cerr << "device has no UDID\n"; return 1; } auto mux_id = dev.get_id(); - if (!mux_id) { + if (mux_id.is_none()) { std::cerr << "device has no mux id\n"; return 1; } // 2) Provider via default usbmuxd addr - auto addr = IdeviceFFI::UsbmuxdAddr::default_new(); + auto addr = IdeviceFFI::UsbmuxdAddr::default_new(); - const uint32_t tag = 0; - const std::string label = "debug-proxy-jkcoxson"; - auto provider = - IdeviceFFI::Provider::usbmuxd_new(std::move(addr), tag, *udid, *mux_id, label, err); - if (!provider) - die("failed to create provider", err); + const uint32_t tag = 0; + const std::string label = "debug-proxy-jkcoxson"; + auto provider = IdeviceFFI::Provider::usbmuxd_new( + std::move(addr), tag, udid.unwrap(), mux_id.unwrap(), label); + if_let_err(provider, err, { die("failed to create provider", err); }); // 3) CoreDeviceProxy - auto cdp = IdeviceFFI::CoreDeviceProxy::connect(*provider, err); - if (!cdp) - die("failed CoreDeviceProxy connect", err); + auto cdp = CoreDeviceProxy::connect(provider.unwrap()) + .unwrap_or_else([](FfiError e) -> CoreDeviceProxy { + die("failed to connect CoreDeviceProxy", e); + }); - auto rsd_port = cdp->get_server_rsd_port(err); - if (!rsd_port) - die("failed to get RSD port", err); + auto rsd_port = cdp.get_server_rsd_port().unwrap_or_else( + [](FfiError err) -> uint16_t { die("failed to get server RSD port", err); }); - // 4) Software tunnel → stream - auto adapter = std::move(*cdp).create_tcp_adapter(err); - if (!adapter) - die("failed to create software tunnel adapter", err); + // 4) Create software tunnel adapter (consumes proxy) + auto adapter = std::move(cdp).create_tcp_adapter(); + if_let_err(adapter, err, { die("failed to create software tunnel adapter", err); }); - auto stream = adapter->connect(*rsd_port, err); - if (!stream) - die("failed to connect RSD stream", err); + // 5) Connect adapter to RSD → ReadWrite stream + auto stream = adapter.unwrap().connect(rsd_port); + if_let_err(stream, err, { die("failed to connect RSD stream", err); }); - // 5) RSD handshake - auto rsd = IdeviceFFI::RsdHandshake::from_socket(std::move(*stream), err); - if (!rsd) - die("failed RSD handshake", err); + // 6) RSD handshake (consumes stream) + auto rsd = RsdHandshake::from_socket(std::move(stream.unwrap())); + if_let_err(rsd, err, { die("failed RSD handshake", err); }); // 6) DebugProxy over RSD - auto dbg = IdeviceFFI::DebugProxy::connect_rsd(*adapter, *rsd, err); - if (!dbg) - die("failed to connect DebugProxy", err); + auto dbg_res = IdeviceFFI::DebugProxy::connect_rsd(adapter.unwrap(), rsd.unwrap()); + if_let_err(dbg_res, err, { die("failed to connect DebugProxy", err); }); + auto& dbg = dbg_res.unwrap(); std::cout << "Shell connected! Type 'exit' to quit.\n"; for (;;) { std::cout << "> " << std::flush; std::string line; - if (!std::getline(std::cin, line)) + if (!std::getline(std::cin, line)) { break; + } // trim auto first = line.find_first_not_of(" \t\r\n"); - if (first == std::string::npos) + if (first == std::string::npos) { continue; + } auto last = line.find_last_not_of(" \t\r\n"); line = line.substr(first, last - first + 1); - if (line == "exit") + if (line == "exit") { break; + } // Interpret: first token = command name, rest = argv auto toks = split_args(line); - if (toks.empty()) + if (toks.empty()) { continue; + } std::string name = toks.front(); std::vector argv(toks.begin() + 1, toks.end()); - auto res = dbg->send_command(name, argv, err); - if (!res && err) { - std::cerr << "send_command failed: " << err.message << "\n"; - // clear error for next loop - err = IdeviceFFI::FfiError{}; - continue; - } - if (res && !res->empty()) { - std::cout << *res << "\n"; - } + auto res = dbg.send_command(name, argv); + match_result( + res, + ok_value, + { if_let_some(ok_value, some_value, { std::cout << some_value << "\n"; }); }, + err_value, + { std::cerr << "send_command failed: " << err_value.message << "\n"; }); } return 0; diff --git a/cpp/examples/diagnosticsservice.cpp b/cpp/examples/diagnosticsservice.cpp index 46bfbb1..bcf2eb0 100644 --- a/cpp/examples/diagnosticsservice.cpp +++ b/cpp/examples/diagnosticsservice.cpp @@ -3,7 +3,6 @@ #include #include #include -#include #include #include @@ -16,10 +15,12 @@ using namespace IdeviceFFI; -static void fail(const char* msg, const FfiError& e) { +[[noreturn]] +static void die(const char* msg, const FfiError& e) { std::cerr << msg; - if (e) + if (e) { std::cerr << ": " << e.message; + } std::cerr << "\n"; std::exit(1); } @@ -28,101 +29,105 @@ int main() { idevice_init_logger(Debug, Disabled, NULL); FfiError err; - // 1) usbmuxd, pick first device - auto mux = UsbmuxdConnection::default_new(/*tag*/ 0, err); - if (!mux) - fail("failed to connect to usbmuxd", err); + // 1) usbmuxd → pick first device + auto mux = IdeviceFFI::UsbmuxdConnection::default_new(/*tag*/ 0); + if_let_err(mux, err, { die("failed to connect to usbmuxd", err); }); - auto devices = mux->get_devices(err); - if (!devices) - fail("failed to list devices", err); - if (devices->empty()) { + auto devices = mux.unwrap().get_devices(); + if_let_err(devices, err, { die("failed to list devices", err); }); + if (devices.unwrap().empty()) { std::cerr << "no devices connected\n"; return 1; } - auto& dev = (*devices)[0]; - auto udid = dev.get_udid(); - auto mux_id = dev.get_id(); - if (!udid || !mux_id) { - std::cerr << "device missing udid or mux id\n"; + auto& dev = (devices.unwrap())[0]; + auto udid = dev.get_udid(); + if (udid.is_none()) { + std::cerr << "device has no UDID\n"; + return 1; + } + auto mux_id = dev.get_id(); + if (mux_id.is_none()) { + std::cerr << "device has no mux id\n"; return 1; } // 2) Provider via default usbmuxd addr - auto addr = UsbmuxdAddr::default_new(); + auto addr = IdeviceFFI::UsbmuxdAddr::default_new(); - const uint32_t tag = 0; - const std::string label = "diagnosticsservice-jkcoxson"; - auto provider = Provider::usbmuxd_new(std::move(addr), tag, *udid, *mux_id, label, err); - if (!provider) - fail("failed to create provider", err); + const uint32_t tag = 0; + const std::string label = "debug-proxy-jkcoxson"; + auto provider = IdeviceFFI::Provider::usbmuxd_new( + std::move(addr), tag, udid.unwrap(), mux_id.unwrap(), label); + if_let_err(provider, err, { die("failed to create provider", err); }); // 3) CoreDeviceProxy - auto cdp = CoreDeviceProxy::connect(*provider, err); - if (!cdp) - fail("failed CoreDeviceProxy connect", err); + auto cdp = CoreDeviceProxy::connect(provider.unwrap()) + .unwrap_or_else([](FfiError e) -> CoreDeviceProxy { + die("failed to connect CoreDeviceProxy", e); + }); - auto rsd_port = cdp->get_server_rsd_port(err); - if (!rsd_port) - fail("failed to get RSD port", err); + auto rsd_port = cdp.get_server_rsd_port().unwrap_or_else( + [](FfiError err) -> uint16_t { die("failed to get server RSD port", err); }); - // 4) Software tunnel → connect to RSD - auto adapter = std::move(*cdp).create_tcp_adapter(err); - if (!adapter) - fail("failed to create software tunnel adapter", err); + // 4) Create software tunnel adapter (consumes proxy) + auto adapter = std::move(cdp).create_tcp_adapter(); + if_let_err(adapter, err, { die("failed to create software tunnel adapter", err); }); - auto stream = adapter->connect(*rsd_port, err); - if (!stream) - fail("failed to connect RSD stream", err); + // 5) Connect adapter to RSD → ReadWrite stream + auto stream = adapter.unwrap().connect(rsd_port); + if_let_err(stream, err, { die("failed to connect RSD stream", err); }); - // 5) RSD handshake - auto rsd = RsdHandshake::from_socket(std::move(*stream), err); - if (!rsd) - fail("failed RSD handshake", err); - // 6) Diagnostics Service over RSD - auto diag = DiagnosticsService::connect_rsd(*adapter, *rsd, err); - if (!diag) - fail("failed to connect DiagnosticsService", err); + // 6) RSD handshake (consumes stream) + auto rsd = RsdHandshake::from_socket(std::move(stream.unwrap())); + if_let_err(rsd, err, { die("failed RSD handshake", err); }); + + // 6) DebugProxy over RSD + auto diag = DiagnosticsService::connect_rsd(adapter.unwrap(), rsd.unwrap()); + if_let_err(diag, err, { die("failed to connect DebugProxy", err); }); std::cout << "Getting sysdiagnose, this takes a while! iOS is slow...\n"; - auto cap = diag->capture_sysdiagnose(/*dry_run=*/false, err); - if (!cap) - fail("capture_sysdiagnose failed", err); + auto cap = diag.unwrap().capture_sysdiagnose(/*dry_run=*/false); + if_let_err(cap, err, { die("capture_sysdiagnose failed", err); }); - std::cout << "Got sysdiagnose! Saving to file: " << cap->preferred_filename << "\n"; + std::cout << "Got sysdiagnose! Saving to file: " << cap.unwrap().preferred_filename << "\n"; // 7) Stream to file with progress - std::ofstream out(cap->preferred_filename, std::ios::binary); + std::ofstream out(cap.unwrap().preferred_filename, std::ios::binary); if (!out) { std::cerr << "failed to open output file\n"; return 1; } std::size_t written = 0; - const std::size_t total = cap->expected_length; + const std::size_t total = cap.unwrap().expected_length; for (;;) { - auto chunk = cap->stream.next_chunk(err); - if (!chunk) { - if (err) - fail("stream error", err); // err set only on real error - break; // nullptr means end-of-stream - } - if (!chunk->empty()) { - out.write(reinterpret_cast(chunk->data()), - static_cast(chunk->size())); - if (!out) { - std::cerr << "write failed\n"; - return 1; - } - written += chunk->size(); - } + auto chunk = cap.unwrap().stream.next_chunk(); + match_result( + chunk, + res, + { + if_let_some(res, chunk_res, { + out.write(reinterpret_cast(chunk_res.data()), + static_cast(chunk_res.size())); + if (!out) { + std::cerr << "write failed\n"; + return 1; + } + written += chunk_res.size(); + }); + if (res.is_none()) { + break; + } + }, + err, + { die("stream error", err); }); std::cout << "wrote " << written << "/" << total << " bytes\r" << std::flush; } out.flush(); - std::cout << "\nDone! Saved to " << cap->preferred_filename << "\n"; + std::cout << "\nDone! Saved to " << cap.unwrap().preferred_filename << "\n"; return 0; } diff --git a/cpp/examples/idevice_id.cpp b/cpp/examples/idevice_id.cpp index 7ebabf7..c99f254 100644 --- a/cpp/examples/idevice_id.cpp +++ b/cpp/examples/idevice_id.cpp @@ -2,34 +2,31 @@ #include #include -#include int main() { - IdeviceFFI::FfiError e; - std::optional u = - IdeviceFFI::UsbmuxdConnection::default_new(0, e); - if (u == std::nullopt) { + auto u = IdeviceFFI::UsbmuxdConnection::default_new(0); + if_let_err(u, e, { std::cerr << "failed to connect to usbmuxd"; std::cerr << e.message; - } + }); - auto devices = u->get_devices(e); - if (u == std::nullopt) { + auto devices = u.unwrap().get_devices(); + if_let_err(devices, e, { std::cerr << "failed to get devices from usbmuxd"; std::cerr << e.message; - } + }); - for (IdeviceFFI::UsbmuxdDevice& d : *devices) { + for (IdeviceFFI::UsbmuxdDevice& d : devices.unwrap()) { auto udid = d.get_udid(); - if (!udid) { + if (udid.is_none()) { std::cerr << "failed to get udid"; continue; } auto connection_type = d.get_connection_type(); - if (!connection_type) { + if (connection_type.is_none()) { std::cerr << "failed to get connection type"; continue; } - std::cout << *udid << " (" << connection_type->to_string() << ")" << "\n"; + std::cout << udid.unwrap() << " (" << connection_type.unwrap().to_string() << ")" << "\n"; } } diff --git a/cpp/examples/ideviceinfo.cpp b/cpp/examples/ideviceinfo.cpp index ae9cfc4..d93756d 100644 --- a/cpp/examples/ideviceinfo.cpp +++ b/cpp/examples/ideviceinfo.cpp @@ -4,73 +4,80 @@ #include #include #include -#include #include int main() { idevice_init_logger(Debug, Disabled, NULL); - IdeviceFFI::FfiError e; - std::optional u = - IdeviceFFI::UsbmuxdConnection::default_new(0, e); - if (!u) { + auto u_res = IdeviceFFI::UsbmuxdConnection::default_new(0); + if_let_err(u_res, e, { std::cerr << "failed to connect to usbmuxd"; std::cerr << e.message; return 1; - } + }); + auto& u = u_res.unwrap(); - auto devices = u->get_devices(e); - if (!devices) { + auto devices_res = u.get_devices(); + if_let_err(devices_res, e, { std::cerr << "failed to get devices from usbmuxd"; std::cerr << e.message; return 1; - } - if (devices->empty()) { + }); + auto devices = std::move(devices_res).unwrap(); + + if (devices.empty()) { std::cerr << "no devices connected"; - std::cerr << e.message; return 1; } - auto& dev = (*devices)[0]; + auto& dev = (devices)[0]; auto udid = dev.get_udid(); - if (!udid) { + if (udid.is_none()) { std::cerr << "no udid\n"; return 1; } auto id = dev.get_id(); - if (!id) { + if (id.is_none()) { std::cerr << "no id\n"; return 1; } - IdeviceFFI::UsbmuxdAddr addr = IdeviceFFI::UsbmuxdAddr::default_new(); - auto prov = - IdeviceFFI::Provider::usbmuxd_new(std::move(addr), /*tag*/ 0, *udid, *id, "reeeeeeeee", e); - if (!prov) { + IdeviceFFI::UsbmuxdAddr addr = IdeviceFFI::UsbmuxdAddr::default_new(); + auto prov_res = IdeviceFFI::Provider::usbmuxd_new( + std::move(addr), /*tag*/ 0, udid.unwrap(), id.unwrap(), "reeeeeeeee"); + if_let_err(prov_res, e, { std::cerr << "provider failed: " << e.message << "\n"; return 1; - } + }); + auto& prov = prov_res.unwrap(); - auto client = IdeviceFFI::Lockdown::connect(*prov, e); - if (!client) { + auto client_res = IdeviceFFI::Lockdown::connect(prov); + if_let_err(client_res, e, { std::cerr << "lockdown connect failed: " << e.message << "\n"; return 1; - } + }); + auto& client = client_res.unwrap(); - auto pf = prov->get_pairing_file(e); - if (!pf) { + auto pf = prov.get_pairing_file(); + if_let_err(pf, e, { std::cerr << "failed to get pairing file: " << e.message << "\n"; return 1; - } - client->start_session(*pf, e); + }); + client.start_session(pf.unwrap()); - auto values = client->get_value(NULL, NULL, e); - if (!values) { - std::cerr << "get values failed: " << e.message << "\n"; - return 1; - } - PList::Dictionary res = PList::Dictionary(*values); - std::cout << res.ToXml(); + auto values = client.get_value(NULL, NULL); + match_result( + values, + ok_val, + { + PList::Dictionary res = PList::Dictionary(ok_val); + std::cout << res.ToXml(); + }, + e, + { + std::cerr << "get values failed: " << e.message << "\n"; + return 1; + }); } diff --git a/cpp/examples/location_simulation.cpp b/cpp/examples/location_simulation.cpp index 824eebc..9211ae1 100644 --- a/cpp/examples/location_simulation.cpp +++ b/cpp/examples/location_simulation.cpp @@ -2,7 +2,6 @@ #include #include -#include #include #include @@ -17,6 +16,7 @@ using namespace IdeviceFFI; +[[noreturn]] static void die(const char* msg, const FfiError& e) { std::cerr << msg << ": " << e.message << "\n"; std::exit(1); @@ -26,8 +26,8 @@ int main(int argc, char** argv) { // Usage: // simulate_location clear // simulate_location set - bool do_clear = false; - std::optional lat, lon; + bool do_clear = false; + Option lat, lon; if (argc == 2 && std::string(argv[1]) == "clear") { do_clear = true; @@ -41,95 +41,82 @@ int main(int argc, char** argv) { return 2; } - FfiError err; + // 1) usbmuxd → pick first device + auto mux = IdeviceFFI::UsbmuxdConnection::default_new(/*tag*/ 0); + if_let_err(mux, err, { die("failed to connect to usbmuxd", err); }); - // 1) Connect to usbmuxd and pick first device - auto mux = UsbmuxdConnection::default_new(/*tag*/ 0, err); - if (!mux) - die("failed to connect to usbmuxd", err); - - auto devices = mux->get_devices(err); - if (!devices) - die("failed to list devices", err); - if (devices->empty()) { + auto devices = mux.unwrap().get_devices(); + if_let_err(devices, err, { die("failed to list devices", err); }); + if (devices.unwrap().empty()) { std::cerr << "no devices connected\n"; return 1; } - auto& dev = (*devices)[0]; - auto udidOpt = dev.get_udid(); - if (!udidOpt) { + auto& dev = (devices.unwrap())[0]; + auto udid = dev.get_udid(); + if (udid.is_none()) { std::cerr << "device has no UDID\n"; return 1; } - auto idOpt = dev.get_id(); - if (!idOpt) { + auto mux_id = dev.get_id(); + if (mux_id.is_none()) { std::cerr << "device has no mux id\n"; return 1; } - // 2) Make a Provider for this device via default addr - auto addr = UsbmuxdAddr::default_new(); + // 2) Provider via default usbmuxd addr + auto addr = IdeviceFFI::UsbmuxdAddr::default_new(); - const uint32_t tag = 0; - const std::string label = "simulate_location-jkcoxson"; + const uint32_t tag = 0; + const std::string label = "debug-proxy-jkcoxson"; + auto provider = IdeviceFFI::Provider::usbmuxd_new( + std::move(addr), tag, udid.unwrap(), mux_id.unwrap(), label); + if_let_err(provider, err, { die("failed to create provider", err); }); - auto provider = Provider::usbmuxd_new(std::move(addr), tag, *udidOpt, *idOpt, label, err); - if (!provider) - die("failed to create provider", err); + // 3) CoreDeviceProxy + auto cdp = CoreDeviceProxy::connect(provider.unwrap()) + .unwrap_or_else([](FfiError e) -> CoreDeviceProxy { + die("failed to connect CoreDeviceProxy", e); + }); - // 3) Connect CoreDeviceProxy (borrow provider) - auto cdp = CoreDeviceProxy::connect(*provider, err); - if (!cdp) - die("failed to connect CoreDeviceProxy", err); + auto rsd_port = cdp.get_server_rsd_port().unwrap_or_else( + [](FfiError err) -> uint16_t { die("failed to get server RSD port", err); }); - // 4) Read handshake’s server RSD port - auto rsd_port = cdp->get_server_rsd_port(err); - if (!rsd_port) - die("failed to get server RSD port", err); + // 4) Create software tunnel adapter (consumes proxy) + auto adapter = std::move(cdp).create_tcp_adapter(); + if_let_err(adapter, err, { die("failed to create software tunnel adapter", err); }); - // 5) Create software tunnel adapter (consumes proxy) - auto adapter = std::move(*cdp).create_tcp_adapter(err); - if (!adapter) - die("failed to create software tunnel adapter", err); + // 5) Connect adapter to RSD → ReadWrite stream + auto stream = adapter.unwrap().connect(rsd_port); + if_let_err(stream, err, { die("failed to connect RSD stream", err); }); - // 6) Connect adapter to RSD port → ReadWrite stream - auto stream = adapter->connect(*rsd_port, err); - if (!stream) - die("failed to connect RSD stream", err); - - // 7) RSD handshake (consumes stream) - auto rsd = RsdHandshake::from_socket(std::move(*stream), err); - if (!rsd) - die("failed RSD handshake", err); + // 6) RSD handshake (consumes stream) + auto rsd = RsdHandshake::from_socket(std::move(stream.unwrap())); + if_let_err(rsd, err, { die("failed RSD handshake", err); }); // 8) RemoteServer over RSD (borrows adapter + handshake) - auto rs = RemoteServer::connect_rsd(*adapter, *rsd, err); - if (!rs) - die("failed to connect RemoteServer", err); + auto rs = RemoteServer::connect_rsd(adapter.unwrap(), rsd.unwrap()); + if_let_err(rs, err, { die("failed to connect RemoteServer", err); }); // 9) LocationSimulation client (borrows RemoteServer) - auto sim = LocationSimulation::create(*rs, err); - if (!sim) - die("failed to create LocationSimulation client", err); + auto sim_res = LocationSimulation::create(rs.unwrap()); + if_let_err(sim_res, err, { die("failed to create LocationSimulation client", err); }); + auto& sim = sim_res.unwrap(); if (do_clear) { - if (!sim->clear(err)) - die("clear failed", err); + if_let_err(sim.clear(), err, { die("clear failed", err); }); std::cout << "Location cleared!\n"; return 0; } // set path - if (!sim->set(*lat, *lon, err)) - die("set failed", err); - std::cout << "Location set to (" << *lat << ", " << *lon << ")\n"; + if_let_err(sim.set(lat.unwrap(), lon.unwrap()), err, { die("set failed", err); }); + std::cout << "Location set to (" << lat.unwrap() << ", " << lon.unwrap() << ")\n"; std::cout << "Press Ctrl-C to stop\n"; // keep process alive like the Rust example for (;;) { - if (!sim->set(*lat, *lon, err)) - die("set failed", err); + if_let_err(sim.set(lat.unwrap(), lon.unwrap()), err, { die("set failed", err); }); std::this_thread::sleep_for(std::chrono::seconds(3)); } } diff --git a/cpp/idevice++.xcodeproj/project.pbxproj b/cpp/idevice++.xcodeproj/project.pbxproj index fda15b7..13e2dcf 100644 --- a/cpp/idevice++.xcodeproj/project.pbxproj +++ b/cpp/idevice++.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 1914C7972E623CC2002EAB6E /* option.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 1914C7962E623CC2002EAB6E /* option.hpp */; }; + 1914C7992E623CC8002EAB6E /* result.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 1914C7982E623CC8002EAB6E /* result.hpp */; }; 198077932E5CA6EF00CB501E /* adapter_stream.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1980776C2E5CA69800CB501E /* adapter_stream.cpp */; }; 198077942E5CA6EF00CB501E /* app_service.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1980776D2E5CA69800CB501E /* app_service.cpp */; }; 198077952E5CA6EF00CB501E /* core_device.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1980776E2E5CA69800CB501E /* core_device.cpp */; }; @@ -44,6 +46,8 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 1914C7962E623CC2002EAB6E /* option.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = option.hpp; sourceTree = ""; }; + 1914C7982E623CC8002EAB6E /* result.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = result.hpp; sourceTree = ""; }; 1980776C2E5CA69800CB501E /* adapter_stream.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = adapter_stream.cpp; sourceTree = ""; }; 1980776D2E5CA69800CB501E /* app_service.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = app_service.cpp; sourceTree = ""; }; 1980776E2E5CA69800CB501E /* core_device.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = core_device.cpp; sourceTree = ""; }; @@ -155,6 +159,8 @@ 198077B12E5CA6FC00CB501E /* rsd.hpp */, 198077B22E5CA6FC00CB501E /* tcp_object_stack.hpp */, 198077B32E5CA6FC00CB501E /* usbmuxd.hpp */, + 1914C7962E623CC2002EAB6E /* option.hpp */, + 1914C7982E623CC8002EAB6E /* result.hpp */, ); path = "idevice++"; sourceTree = ""; @@ -196,11 +202,13 @@ 198077C12E5CA6FC00CB501E /* readwrite.hpp in Headers */, 198077C22E5CA6FC00CB501E /* location_simulation.hpp in Headers */, 198077C32E5CA6FC00CB501E /* adapter_stream.hpp in Headers */, + 1914C7972E623CC2002EAB6E /* option.hpp in Headers */, 198077C42E5CA6FC00CB501E /* lockdown.hpp in Headers */, 198077C52E5CA6FC00CB501E /* usbmuxd.hpp in Headers */, 198077C62E5CA6FC00CB501E /* app_service.hpp in Headers */, 198077C72E5CA6FC00CB501E /* idevice.hpp in Headers */, 198077C82E5CA6FC00CB501E /* provider.hpp in Headers */, + 1914C7992E623CC8002EAB6E /* result.hpp in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -433,14 +441,22 @@ 198077912E5CA6C700CB501E /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = YES; CODE_SIGN_STYLE = Automatic; + DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 4FW3Q8784L; EXECUTABLE_PREFIX = lib; + GCC_WARN_INHIBIT_ALL_WARNINGS = NO; + GCC_WARN_SIGN_COMPARE = YES; + GCC_WARN_UNKNOWN_PRAGMAS = YES; + GCC_WARN_UNUSED_LABEL = YES; + GCC_WARN_UNUSED_PARAMETER = YES; LIBRARY_SEARCH_PATHS = ( "${TARGET_BUILD_DIR}/**", "$(PROJECT_DIR)/${DESTINATION_PATH}", ); MACOSX_DEPLOYMENT_TARGET = 15.5; + OTHER_LDFLAGS = "-Wall"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; SKIP_INSTALL = YES; @@ -453,14 +469,22 @@ 198077922E5CA6C700CB501E /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = YES; CODE_SIGN_STYLE = Automatic; + DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 4FW3Q8784L; EXECUTABLE_PREFIX = lib; + GCC_WARN_INHIBIT_ALL_WARNINGS = NO; + GCC_WARN_SIGN_COMPARE = YES; + GCC_WARN_UNKNOWN_PRAGMAS = YES; + GCC_WARN_UNUSED_LABEL = YES; + GCC_WARN_UNUSED_PARAMETER = YES; LIBRARY_SEARCH_PATHS = ( "${TARGET_BUILD_DIR}/**", "$(PROJECT_DIR)/${DESTINATION_PATH}", ); MACOSX_DEPLOYMENT_TARGET = 15.5; + OTHER_LDFLAGS = "-Wall"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; SKIP_INSTALL = YES; diff --git a/cpp/include/idevice++/adapter_stream.hpp b/cpp/include/idevice++/adapter_stream.hpp index 862e91c..f92a5b1 100644 --- a/cpp/include/idevice++/adapter_stream.hpp +++ b/cpp/include/idevice++/adapter_stream.hpp @@ -5,6 +5,8 @@ #include #include #include +#include +#include #include struct IdeviceFfiError; @@ -31,16 +33,16 @@ class AdapterStream { ~AdapterStream() noexcept = default; // no auto-close; caller controls - AdapterStreamHandle* raw() const noexcept { return h_; } + AdapterStreamHandle* raw() const noexcept { return h_; } - bool close(FfiError& err); - bool send(const uint8_t* data, size_t len, FfiError& err); - bool send(const std::vector& buf, FfiError& err) { - return send(buf.data(), buf.size(), err); + Result close(); + Result send(const uint8_t* data, size_t len); + Result send(const std::vector& buf) { + return send(buf.data(), buf.size()); } // recv into caller-provided buffer (resizes to actual length) - bool recv(std::vector& out, FfiError& err, size_t max_hint = 2048); + Result, FfiError> recv(size_t max_hint = 2048); private: AdapterStreamHandle* h_{}; diff --git a/cpp/include/idevice++/app_service.hpp b/cpp/include/idevice++/app_service.hpp index aada1a6..0adfb9c 100644 --- a/cpp/include/idevice++/app_service.hpp +++ b/cpp/include/idevice++/app_service.hpp @@ -9,7 +9,6 @@ #include #include #include -#include #include #include @@ -19,17 +18,17 @@ using AppServicePtr = std::unique_ptr>; struct AppInfo { - bool is_removable{}; - std::string name; - bool is_first_party{}; - std::string path; - std::string bundle_identifier; - bool is_developer_app{}; - std::optional bundle_version; - bool is_internal{}; - bool is_hidden{}; - bool is_app_clip{}; - std::optional version; + bool is_removable{}; + std::string name; + bool is_first_party{}; + std::string path; + std::string bundle_identifier; + bool is_developer_app{}; + Option bundle_version; + bool is_internal{}; + bool is_hidden{}; + bool is_app_clip{}; + Option version; }; struct LaunchResponse { @@ -40,15 +39,15 @@ struct LaunchResponse { }; struct ProcessToken { - uint32_t pid{}; - std::optional executable_url; + uint32_t pid{}; + Option executable_url; }; struct SignalResponse { - uint32_t pid{}; - std::optional executable_url; - uint64_t device_timestamp_ms{}; - uint32_t signal{}; + uint32_t pid{}; + Option executable_url; + uint64_t device_timestamp_ms{}; + uint32_t signal{}; }; struct IconData { @@ -62,41 +61,34 @@ struct IconData { class AppService { public: // Factory: connect via RSD (borrows adapter & handshake) - static std::optional - connect_rsd(Adapter& adapter, RsdHandshake& rsd, FfiError& err); + static Result connect_rsd(Adapter& adapter, RsdHandshake& rsd); // Factory: from socket Box (consumes it). - static std::optional from_readwrite_ptr(ReadWriteOpaque* consumed, FfiError& err); + static Result from_readwrite_ptr(ReadWriteOpaque* consumed); // nice ergonomic overload: consume a C++ ReadWrite by releasing it - static std::optional from_readwrite(ReadWrite&& rw, FfiError& err); + static Result from_readwrite(ReadWrite&& rw); // API - std::optional> list_apps(bool app_clips, - bool removable, - bool hidden, - bool internal, - bool default_apps, - FfiError& err) const; + Result, FfiError> + list_apps(bool app_clips, bool removable, bool hidden, bool internal, bool default_apps) const; - std::optional launch(const std::string& bundle_id, - const std::vector& argv, - bool kill_existing, - bool start_suspended, - FfiError& err); + Result launch(const std::string& bundle_id, + const std::vector& argv, + bool kill_existing, + bool start_suspended); - std::optional> list_processes(FfiError& err) const; + Result, FfiError> list_processes() const; - bool uninstall(const std::string& bundle_id, FfiError& err); + Result uninstall(const std::string& bundle_id); - std::optional send_signal(uint32_t pid, uint32_t signal, FfiError& err); + Result send_signal(uint32_t pid, uint32_t signal); - std::optional fetch_icon(const std::string& bundle_id, - float width, - float height, - float scale, - bool allow_placeholder, - FfiError& err); + Result fetch_icon(const std::string& bundle_id, + float width, + float height, + float scale, + bool allow_placeholder); // RAII / moves ~AppService() noexcept = default; diff --git a/cpp/include/idevice++/core_device_proxy.hpp b/cpp/include/idevice++/core_device_proxy.hpp index 2b392d2..730fca4 100644 --- a/cpp/include/idevice++/core_device_proxy.hpp +++ b/cpp/include/idevice++/core_device_proxy.hpp @@ -5,8 +5,10 @@ #include #include +#include #include #include +#include namespace IdeviceFFI { @@ -22,32 +24,33 @@ struct CoreClientParams { class Adapter { public: - ~Adapter() noexcept = default; - Adapter(Adapter&&) noexcept = default; - Adapter& operator=(Adapter&&) noexcept = default; - Adapter(const Adapter&) = delete; - Adapter& operator=(const Adapter&) = delete; + ~Adapter() noexcept = default; + Adapter(Adapter&&) noexcept = default; + Adapter& operator=(Adapter&&) noexcept = default; + Adapter(const Adapter&) = delete; + Adapter& operator=(const Adapter&) = delete; - static Adapter adopt(AdapterHandle* h) noexcept { return Adapter(h); } - AdapterHandle* raw() const noexcept { return handle_.get(); } + static Adapter adopt(AdapterHandle* h) noexcept { return Adapter(h); } + AdapterHandle* raw() const noexcept { return handle_.get(); } // Enable PCAP - bool pcap(const std::string& path, FfiError& err) { - if (IdeviceFfiError* e = ::adapter_pcap(handle_.get(), path.c_str())) { - err = FfiError(e); - return false; + Result pcap(const std::string& path) { + FfiError e(::adapter_pcap(handle_.get(), path.c_str())); + if (e) { + return Err(e); } - return true; + return Ok(); } - // Connect to a port, returns a ReadWrite stream (to be consumed by RSD/CoreDeviceProxy) - std::optional connect(uint16_t port, FfiError& err) { + // Connect to a port, returns a ReadWrite stream (to be consumed by + // RSD/CoreDeviceProxy) + Result connect(uint16_t port) { ReadWriteOpaque* s = nullptr; - if (IdeviceFfiError* e = ::adapter_connect(handle_.get(), port, &s)) { - err = FfiError(e); - return std::nullopt; + FfiError e(::adapter_connect(handle_.get(), port, &s)); + if (e) { + return Err(e); } - return ReadWrite::adopt(s); + return Ok(ReadWrite::adopt(s)); } private: @@ -58,28 +61,28 @@ class Adapter { class CoreDeviceProxy { public: // Factory: connect using a Provider (NOT consumed on success or error) - static std::optional connect(Provider& provider, FfiError& err); + static Result connect(Provider& provider); - // Factory: from a socket; Rust consumes the socket regardless of result → we release before - // call - static std::optional from_socket(Idevice&& socket, FfiError& err); + // Factory: from a socket; Rust consumes the socket regardless of result → we + // release before call + static Result from_socket(Idevice&& socket); // Send/recv - bool send(const uint8_t* data, size_t len, FfiError& err); - bool send(const std::vector& buf, FfiError& err) { - return send(buf.data(), buf.size(), err); + Result send(const uint8_t* data, size_t len); + Result send(const std::vector& buf) { + return send(buf.data(), buf.size()); } // recv into a pre-sized buffer; resizes to actual bytes received - bool recv(std::vector& out, FfiError& err); + Result recv(std::vector& out); // Handshake info - std::optional get_client_parameters(FfiError& err) const; - std::optional get_server_address(FfiError& err) const; - std::optional get_server_rsd_port(FfiError& err) const; + Result get_client_parameters() const; + Result get_server_address() const; + Result get_server_rsd_port() const; // Consuming creation of a TCP adapter: Rust consumes the proxy handle - std::optional create_tcp_adapter(FfiError& err) &&; + Result create_tcp_adapter() &&; // RAII / moves ~CoreDeviceProxy() noexcept = default; diff --git a/cpp/include/idevice++/debug_proxy.hpp b/cpp/include/idevice++/debug_proxy.hpp index cafd386..1e8b832 100644 --- a/cpp/include/idevice++/debug_proxy.hpp +++ b/cpp/include/idevice++/debug_proxy.hpp @@ -4,13 +4,13 @@ #include #include #include -#include #include #include // Bring in the global C ABI (all C structs/functions are global) #include #include +#include #include namespace IdeviceFFI { @@ -33,32 +33,33 @@ class DebugProxy { ~DebugProxy() { reset(); } - // Factory: connect over RSD (borrows adapter & handshake; does not consume them) - static std::optional - connect_rsd(Adapter& adapter, RsdHandshake& rsd, FfiError& err); + // Factory: connect over RSD (borrows adapter & handshake; does not consume + // them) + static Result connect_rsd(Adapter& adapter, RsdHandshake& rsd); // Factory: consume a ReadWrite stream (fat pointer) - static std::optional from_readwrite_ptr(::ReadWriteOpaque* consumed, FfiError& err); + static Result from_readwrite_ptr(::ReadWriteOpaque* consumed); // Convenience: consume a C++ ReadWrite wrapper by releasing it into the ABI - static std::optional from_readwrite(ReadWrite&& rw, FfiError& err); + static Result from_readwrite(ReadWrite&& rw); // API - std::optional - send_command(const std::string& name, const std::vector& argv, FfiError& err); + Result, FfiError> send_command(const std::string& name, + const std::vector& argv); - std::optional read_response(FfiError& err); + Result, FfiError> read_response(); - bool send_raw(const std::vector& data, FfiError& err); + Result send_raw(const std::vector& data); - // Reads up to `len` bytes; ABI returns a heap C string (we treat as bytes → string) - std::optional read(std::size_t len, FfiError& err); + // Reads up to `len` bytes; ABI returns a heap C string (we treat as bytes → + // string) + Result, FfiError> read(std::size_t len); // Sets argv, returns textual reply (OK/echo/etc) - std::optional set_argv(const std::vector& argv, FfiError& err); + Result, FfiError> set_argv(const std::vector& argv); - bool send_ack(FfiError& err); - bool send_nack(FfiError& err); + Result send_ack(); + Result send_nack(); // No error object in ABI; immediate effect void set_ack_mode(bool enabled) { ::debug_proxy_set_ack_mode(handle_, enabled ? 1 : 0); } @@ -99,10 +100,9 @@ class DebugCommand { ~DebugCommand() { reset(); } - static std::optional make(const std::string& name, - const std::vector& argv); + static Option make(const std::string& name, const std::vector& argv); - ::DebugserverCommandHandle* raw() const { return handle_; } + ::DebugserverCommandHandle* raw() const { return handle_; } private: explicit DebugCommand(::DebugserverCommandHandle* h) : handle_(h) {} diff --git a/cpp/include/idevice++/diagnosticsservice.hpp b/cpp/include/idevice++/diagnosticsservice.hpp index b70e848..b0c81a0 100644 --- a/cpp/include/idevice++/diagnosticsservice.hpp +++ b/cpp/include/idevice++/diagnosticsservice.hpp @@ -3,7 +3,6 @@ #pragma once #include #include -#include #include #include @@ -32,10 +31,11 @@ class SysdiagnoseStream { ~SysdiagnoseStream() { reset(); } - // Pull next chunk. Returns nullopt on end-of-stream. On error, returns nullopt and sets `err`. - std::optional> next_chunk(FfiError& err); + // Pull next chunk. Returns nullopt on end-of-stream. On error, returns + // nullopt and sets `err`. + Result>, FfiError> next_chunk(); - SysdiagnoseStreamHandle* raw() const { return h_; } + SysdiagnoseStreamHandle* raw() const { return h_; } private: friend class DiagnosticsService; @@ -78,21 +78,20 @@ class DiagnosticsService { ~DiagnosticsService() { reset(); } // Connect via RSD (borrows adapter & handshake; does not consume them) - static std::optional - connect_rsd(Adapter& adapter, RsdHandshake& rsd, FfiError& err); + static Result connect_rsd(Adapter& adapter, RsdHandshake& rsd); // Create from a ReadWrite stream (consumes it) - static std::optional from_stream_ptr(::ReadWriteOpaque* consumed, - FfiError& err); + static Result from_stream_ptr(::ReadWriteOpaque* consumed); - static std::optional from_stream(ReadWrite&& rw, FfiError& err) { - return from_stream_ptr(rw.release(), err); + static Result from_stream(ReadWrite&& rw) { + return from_stream_ptr(rw.release()); } - // Start sysdiagnose capture; on success returns filename, length and a byte stream - std::optional capture_sysdiagnose(bool dry_run, FfiError& err); + // Start sysdiagnose capture; on success returns filename, length and a byte + // stream + Result capture_sysdiagnose(bool dry_run); - ::DiagnosticsServiceHandle* raw() const { return h_; } + ::DiagnosticsServiceHandle* raw() const { return h_; } private: explicit DiagnosticsService(::DiagnosticsServiceHandle* h) : h_(h) {} diff --git a/cpp/include/idevice++/ffi.hpp b/cpp/include/idevice++/ffi.hpp index 75cea52..73cdf06 100644 --- a/cpp/include/idevice++/ffi.hpp +++ b/cpp/include/idevice++/ffi.hpp @@ -15,7 +15,10 @@ class FfiError { FfiError(const IdeviceFfiError* err); FfiError(); - explicit operator bool() const { return code != 0; } + explicit operator bool() const { return code != 0; } + + static FfiError NotConnected(); + static FfiError InvalidArgument(); }; } // namespace IdeviceFFI #endif diff --git a/cpp/include/idevice++/idevice.hpp b/cpp/include/idevice++/idevice.hpp index c5ce89f..2fe93ce 100644 --- a/cpp/include/idevice++/idevice.hpp +++ b/cpp/include/idevice++/idevice.hpp @@ -5,7 +5,7 @@ #include #include -#include +#include #include #if defined(_WIN32) && !defined(__MINGW32__) @@ -21,8 +21,9 @@ namespace IdeviceFFI { // Generic “bind a free function” deleter template struct FnDeleter { void operator()(T* p) const noexcept { - if (p) + if (p) { FreeFn(p); + } } }; @@ -30,16 +31,15 @@ using IdevicePtr = std::unique_ptr - create(IdeviceSocketHandle* socket, const std::string& label, FfiError& err); + static Result create(IdeviceSocketHandle* socket, const std::string& label); - static std::optional - create_tcp(const sockaddr* addr, socklen_t addr_len, const std::string& label, FfiError& err); + static Result + create_tcp(const sockaddr* addr, socklen_t addr_len, const std::string& label); // Methods - std::optional get_type(FfiError& err) const; - bool rsd_checkin(FfiError& err); - bool start_session(const PairingFile& pairing_file, FfiError& err); + Result get_type() const; + Result rsd_checkin(); + Result start_session(const PairingFile& pairing_file); // Ownership/RAII ~Idevice() noexcept = default; diff --git a/cpp/include/idevice++/location_simulation.hpp b/cpp/include/idevice++/location_simulation.hpp index 5324292..be4dd85 100644 --- a/cpp/include/idevice++/location_simulation.hpp +++ b/cpp/include/idevice++/location_simulation.hpp @@ -3,8 +3,8 @@ #pragma once #include #include +#include #include -#include namespace IdeviceFFI { @@ -14,10 +14,10 @@ using LocSimPtr = std::unique_ptr create(RemoteServer& server, FfiError& err); + static Result create(RemoteServer& server); - bool clear(FfiError& err); - bool set(double latitude, double longitude, FfiError& err); + Result clear(); + Result set(double latitude, double longitude); ~LocationSimulation() noexcept = default; LocationSimulation(LocationSimulation&&) noexcept = default; diff --git a/cpp/include/idevice++/lockdown.hpp b/cpp/include/idevice++/lockdown.hpp index 9c962ae..14dfdff 100644 --- a/cpp/include/idevice++/lockdown.hpp +++ b/cpp/include/idevice++/lockdown.hpp @@ -5,7 +5,6 @@ #include #include #include -#include #include namespace IdeviceFFI { @@ -16,16 +15,15 @@ using LockdownPtr = class Lockdown { public: // Factory: connect via Provider - static std::optional connect(Provider& provider, FfiError& err); + static Result connect(Provider& provider); // Factory: wrap an existing Idevice socket (consumes it on success) - static std::optional from_socket(Idevice&& socket, FfiError& err); + static Result from_socket(Idevice&& socket); // Ops - bool start_session(const PairingFile& pf, FfiError& err); - std::optional> start_service(const std::string& identifier, - FfiError& err); - std::optional get_value(const char* key, const char* domain, FfiError& err); + Result start_session(const PairingFile& pf); + Result, FfiError> start_service(const std::string& identifier); + Result get_value(const char* key, const char* domain); // RAII / moves ~Lockdown() noexcept = default; diff --git a/cpp/include/idevice++/option.hpp b/cpp/include/idevice++/option.hpp new file mode 100644 index 0000000..b522b92 --- /dev/null +++ b/cpp/include/idevice++/option.hpp @@ -0,0 +1,184 @@ +// So here's the thing, std::optional and friends weren't added until C++17. +// Some consumers of this codebase aren't on C++17 yet, so this won't work. +// Plus, as a professional Rust evangelist, it's my duty to place as many Rust +// idioms into other languages as possible to give everyone a taste of greatness. +// Required error handling is correct error handling. And they called me a mad man. + +// Heavily influced from https://github.com/oktal/result, thank you + +#pragma once + +#include +#include +#include +#include + +namespace IdeviceFFI { + +struct none_t {}; +constexpr none_t None{}; + +template class Option { + bool has_; + typename std::aligned_storage::type storage_; + + T* ptr() { return reinterpret_cast(&storage_); } + const T* ptr() const { return reinterpret_cast(&storage_); } + + public: + // None + Option() noexcept : has_(false) {} + Option(none_t) noexcept : has_(false) {} + + // Some + Option(const T& v) : has_(true) { ::new (ptr()) T(v); } + Option(T&& v) : has_(true) { ::new (ptr()) T(std::move(v)); } + + // Copy / move + Option(const Option& o) : has_(o.has_) { + if (has_) { + ::new (ptr()) T(*o.ptr()); + } + } + Option(Option&& o) noexcept(std::is_nothrow_move_constructible::value) : has_(o.has_) { + if (has_) { + ::new (ptr()) T(std::move(*o.ptr())); + o.reset(); + } + } + + Option& operator=(Option o) noexcept(std::is_nothrow_move_constructible::value + && std::is_nothrow_move_assignable::value) { + swap(o); + return *this; + } + + ~Option() { reset(); } + + void reset() noexcept { + if (has_) { + ptr()->~T(); + has_ = false; + } + } + + void swap(Option& other) noexcept(std::is_nothrow_move_constructible::value) { + if (has_ && other.has_) { + using std::swap; + swap(*ptr(), *other.ptr()); + } else if (has_ && !other.has_) { + ::new (other.ptr()) T(std::move(*ptr())); + other.has_ = true; + reset(); + } else if (!has_ && other.has_) { + ::new (ptr()) T(std::move(*other.ptr())); + has_ = true; + other.reset(); + } + } + + // State + bool is_some() const noexcept { return has_; } + bool is_none() const noexcept { return !has_; } + + // Unwraps (ref-qualified) + T& unwrap() & { + if (!has_) { + throw std::runtime_error("unwrap on None"); + } + return *ptr(); + } + const T& unwrap() const& { + if (!has_) { + throw std::runtime_error("unwrap on None"); + } + return *ptr(); + } + T unwrap() && { + if (!has_) { + throw std::runtime_error("unwrap on None"); + } + T tmp = std::move(*ptr()); + reset(); + return tmp; + } + + // unwrap_or / unwrap_or_else + T unwrap_or(T default_value) const& { return has_ ? *ptr() : std::move(default_value); } + T unwrap_or(T default_value) && { return has_ ? std::move(*ptr()) : std::move(default_value); } + + template T unwrap_or_else(F&& f) const& { + return has_ ? *ptr() : static_cast(f()); + } + template T unwrap_or_else(F&& f) && { + return has_ ? std::move(*ptr()) : static_cast(f()); + } + + // map + template + auto map(F&& f) const -> Option::type> { + using U = typename std::decay::type; + if (has_) { + return Option(f(*ptr())); + } + return Option(None); + } +}; + +// Helpers +template inline Option::type> Some(T&& v) { + return Option::type>(std::forward(v)); +} +inline Option Some() = delete; // no Option + +// template inline Option None() { +// return Option(none); +// } // still needs T specified + +// Prefer this at call sites (lets return-type drive the type): +// return none; + +#define match_option(opt, SOME, NONE) \ + if ((opt).is_some()) { \ + auto&& SOME = (opt).unwrap(); +#define or_else \ + } \ + else { \ + NONE; \ + } + +// --- Option helpers: if_let_some / if_let_some_move / if_let_none --- + +#define _opt_concat(a, b) a##b +#define _opt_unique(base) _opt_concat(base, __LINE__) + +/* Bind a reference to the contained value if Some(...) */ +#define if_let_some(expr, name, block) \ + do { \ + auto _opt_unique(_opt_) = (expr); \ + if (_opt_unique(_opt_).is_some()) { \ + auto&& name = _opt_unique(_opt_).unwrap(); \ + block \ + } \ + } while (0) + +/* Move the contained value out (consumes the Option) if Some(...) */ +#define if_let_some_move(expr, name, block) \ + do { \ + auto _opt_unique(_opt_) = (expr); \ + if (_opt_unique(_opt_).is_some()) { \ + auto name = std::move(_opt_unique(_opt_)).unwrap(); \ + block \ + } \ + } while (0) + +/* Run a block if the option is None */ +#define if_let_none(expr, block) \ + do { \ + auto _opt_unique(_opt_) = (expr); \ + if (_opt_unique(_opt_).is_none()) { \ + block \ + } \ + } while (0) + +} // namespace IdeviceFFI diff --git a/cpp/include/idevice++/pairing_file.hpp b/cpp/include/idevice++/pairing_file.hpp index 46203e9..724c836 100644 --- a/cpp/include/idevice++/pairing_file.hpp +++ b/cpp/include/idevice++/pairing_file.hpp @@ -6,10 +6,10 @@ #pragma once #include -#include +#include +#include #include #include -#include namespace IdeviceFFI { struct PairingFileDeleter { @@ -20,8 +20,8 @@ using PairingFilePtr = std::unique_ptr; class PairingFile { public: - static std::optional read(const std::string& path, FfiError& err); - static std::optional from_bytes(const uint8_t* data, size_t size, FfiError& err); + static Result read(const std::string& path); + static Result from_bytes(const uint8_t* data, size_t size); ~PairingFile() noexcept = default; // unique_ptr handles destruction @@ -29,9 +29,9 @@ class PairingFile { PairingFile& operator=(const PairingFile&) = delete; PairingFile(PairingFile&&) noexcept = default; // move is correct by default - PairingFile& operator=(PairingFile&&) noexcept = default; + PairingFile& operator=(PairingFile&&) noexcept = default; - std::optional> serialize(FfiError& err) const; + Result, FfiError> serialize() const; explicit PairingFile(IdevicePairingFile* ptr) noexcept : ptr_(ptr) {} IdevicePairingFile* raw() const noexcept { return ptr_.get(); } diff --git a/cpp/include/idevice++/provider.hpp b/cpp/include/idevice++/provider.hpp index e976058..65c1383 100644 --- a/cpp/include/idevice++/provider.hpp +++ b/cpp/include/idevice++/provider.hpp @@ -5,43 +5,41 @@ #include #include #include -#include #include namespace IdeviceFFI { class FfiError; -class PairingFile; // has: IdevicePairingFile* raw() const; void release_on_success(); -class UsbmuxdAddr; // has: UsbmuxdAddrHandle* raw() const; void release_on_success(); +class PairingFile; // has: IdevicePairingFile* raw() const; void + // release_on_success(); +class UsbmuxdAddr; // has: UsbmuxdAddrHandle* raw() const; void + // release_on_success(); using ProviderPtr = std::unique_ptr>; class Provider { public: - static std::optional tcp_new(const idevice_sockaddr* ip, - PairingFile&& pairing, - const std::string& label, - FfiError& err); + static Result + tcp_new(const idevice_sockaddr* ip, PairingFile&& pairing, const std::string& label); - static std::optional usbmuxd_new(UsbmuxdAddr&& addr, - uint32_t tag, - const std::string& udid, - uint32_t device_id, - const std::string& label, - FfiError& err); + static Result usbmuxd_new(UsbmuxdAddr&& addr, + uint32_t tag, + const std::string& udid, + uint32_t device_id, + const std::string& label); - ~Provider() noexcept = default; - Provider(Provider&&) noexcept = default; - Provider& operator=(Provider&&) noexcept = default; - Provider(const Provider&) = delete; - Provider& operator=(const Provider&) = delete; + ~Provider() noexcept = default; + Provider(Provider&&) noexcept = default; + Provider& operator=(Provider&&) noexcept = default; + Provider(const Provider&) = delete; + Provider& operator=(const Provider&) = delete; - std::optional get_pairing_file(FfiError& err); + Result get_pairing_file(); - IdeviceProviderHandle* raw() const noexcept { return handle_.get(); } - static Provider adopt(IdeviceProviderHandle* h) noexcept { return Provider(h); } - IdeviceProviderHandle* release() noexcept { return handle_.release(); } + IdeviceProviderHandle* raw() const noexcept { return handle_.get(); } + static Provider adopt(IdeviceProviderHandle* h) noexcept { return Provider(h); } + IdeviceProviderHandle* release() noexcept { return handle_.release(); } private: explicit Provider(IdeviceProviderHandle* h) noexcept : handle_(h) {} diff --git a/cpp/include/idevice++/remote_server.hpp b/cpp/include/idevice++/remote_server.hpp index 51f8d98..09b1022 100644 --- a/cpp/include/idevice++/remote_server.hpp +++ b/cpp/include/idevice++/remote_server.hpp @@ -9,7 +9,6 @@ #include #include #include -#include namespace IdeviceFFI { @@ -19,11 +18,10 @@ using RemoteServerPtr = class RemoteServer { public: // Factory: consumes the ReadWrite stream regardless of result - static std::optional from_socket(ReadWrite&& rw, FfiError& err); + static Result from_socket(ReadWrite&& rw); // Factory: borrows adapter + handshake (neither is consumed) - static std::optional - connect_rsd(Adapter& adapter, RsdHandshake& rsd, FfiError& err); + static Result connect_rsd(Adapter& adapter, RsdHandshake& rsd); // RAII / moves ~RemoteServer() noexcept = default; diff --git a/cpp/include/idevice++/result.hpp b/cpp/include/idevice++/result.hpp new file mode 100644 index 0000000..fd4872e --- /dev/null +++ b/cpp/include/idevice++/result.hpp @@ -0,0 +1,231 @@ +// Jackson Coxson + +#pragma once + +#include +#include +#include +#include + +namespace IdeviceFFI { +namespace types { +template struct Ok { + T val; + + Ok(const T& val) : val(val) {} + Ok(T&& val) : val(std::move(val)) {} +}; + +template <> struct Ok {}; + +template struct Err { + E val; + + Err(const E& val) : val(val) {} + Err(E&& val) : val(std::move(val)) {} +}; +} // namespace types + +template inline types::Ok::type> Ok(T&& val) { + return types::Ok::type>(std::forward(val)); +} + +inline types::Ok Ok() { + return types::Ok(); +} + +template inline types::Err::type> Err(E&& val) { + return types::Err::type>(std::forward(val)); +} + +// ======================= +// Result +// ======================= +template class Result { + bool is_ok_; + union { + T ok_value_; + E err_value_; + }; + + public: + Result(types::Ok ok_val) : is_ok_(true), ok_value_(std::move(ok_val.val)) {} + Result(types::Err err_val) : is_ok_(false), err_value_(std::move(err_val.val)) {} + + Result(const Result& other) : is_ok_(other.is_ok_) { + if (is_ok_) { + new (&ok_value_) T(other.ok_value_); + } else { + new (&err_value_) E(other.err_value_); + } + } + + Result(Result&& other) noexcept : is_ok_(other.is_ok_) { + if (is_ok_) { + new (&ok_value_) T(std::move(other.ok_value_)); + } else { + new (&err_value_) E(std::move(other.err_value_)); + } + } + + ~Result() { + if (is_ok_) { + ok_value_.~T(); + } else { + err_value_.~E(); + } + } + + bool is_ok() const { return is_ok_; } + bool is_err() const { return !is_ok_; } + + // lvalue (mutable) + T& unwrap() & { + if (!is_ok_) { + std::fprintf(stderr, "unwrap on Err\n"); + std::terminate(); + } + return ok_value_; + } + + // lvalue (const) + const T& unwrap() const& { + if (!is_ok_) { + std::fprintf(stderr, "unwrap on Err\n"); + std::terminate(); + } + return ok_value_; + } + + // rvalue (consume/move) + T unwrap() && { + if (!is_ok_) { + std::fprintf(stderr, "unwrap on Err\n"); + std::terminate(); + } + return std::move(ok_value_); + } + + E& unwrap_err() & { + if (is_ok_) { + std::fprintf(stderr, "unwrap_err on Ok\n"); + std::terminate(); + } + return err_value_; + } + + const E& unwrap_err() const& { + if (is_ok_) { + std::fprintf(stderr, "unwrap_err on Ok\n"); + std::terminate(); + } + return err_value_; + } + + E unwrap_err() && { + if (is_ok_) { + std::fprintf(stderr, "unwrap_err on Ok\n"); + std::terminate(); + } + return std::move(err_value_); + } + + T unwrap_or(T&& default_value) const { return is_ok_ ? ok_value_ : std::move(default_value); } + + template T unwrap_or_else(F&& f) & { + return is_ok_ ? ok_value_ : static_cast(f(err_value_)); + } + + // const lvalue: returns T by copy + template T unwrap_or_else(F&& f) const& { + return is_ok_ ? ok_value_ : static_cast(f(err_value_)); + } + + // rvalue: moves Ok(T) out; on Err(E), allow the handler to consume/move E + template T unwrap_or_else(F&& f) && { + if (is_ok_) { + return std::move(ok_value_); + } + return static_cast(std::forward(f)(std::move(err_value_))); + } +}; + +// Result specialization + +template class Result { + bool is_ok_; + union { + char dummy_; + E err_value_; + }; + + public: + Result(types::Ok) : is_ok_(true), dummy_() {} + Result(types::Err err_val) : is_ok_(false), err_value_(std::move(err_val.val)) {} + + Result(const Result& other) : is_ok_(other.is_ok_) { + if (!is_ok_) { + new (&err_value_) E(other.err_value_); + } + } + + Result(Result&& other) noexcept : is_ok_(other.is_ok_) { + if (!is_ok_) { + new (&err_value_) E(std::move(other.err_value_)); + } + } + + ~Result() { + if (!is_ok_) { + err_value_.~E(); + } + } + + bool is_ok() const { return is_ok_; } + bool is_err() const { return !is_ok_; } + + void unwrap() const { + if (!is_ok_) { + std::fprintf(stderr, "Attempted to unwrap an error Result\n"); + std::terminate(); + } + } + + const E& unwrap_err() const { + if (is_ok_) { + std::fprintf(stderr, "Attempted to unwrap_err on an ok Result\n"); + std::terminate(); + } + return err_value_; + } + + E& unwrap_err() { + if (is_ok_) { + std::fprintf(stderr, "Attempted to unwrap_err on an ok Result\n"); + std::terminate(); + } + return err_value_; + } +}; + +#define match_result(res, ok_name, ok_block, err_name, err_block) \ + if ((res).is_ok()) { \ + auto&& ok_name = (res).unwrap(); \ + ok_block \ + } else { \ + auto&& err_name = (res).unwrap_err(); \ + err_block \ + } + +#define if_let_err(res, name, block) \ + if ((res).is_err()) { \ + auto&& name = (res).unwrap_err(); \ + block \ + } + +#define if_let_ok(res, name, block) \ + if ((res).is_ok()) { \ + auto&& name = (res).unwrap(); \ + block \ + } +} // namespace IdeviceFFI diff --git a/cpp/include/idevice++/rsd.hpp b/cpp/include/idevice++/rsd.hpp index 1231a87..6532c3e 100644 --- a/cpp/include/idevice++/rsd.hpp +++ b/cpp/include/idevice++/rsd.hpp @@ -25,16 +25,16 @@ using RsdPtr = class RsdHandshake { public: // Factory: consumes the ReadWrite socket regardless of result - static std::optional from_socket(ReadWrite&& rw, FfiError& err); + static Result from_socket(ReadWrite&& rw); // Basic info - std::optional protocol_version(FfiError& err) const; - std::optional uuid(FfiError& err) const; + Result protocol_version() const; + Result uuid() const; // Services - std::optional> services(FfiError& err) const; - std::optional service_available(const std::string& name, FfiError& err) const; - std::optional service_info(const std::string& name, FfiError& err) const; + Result, FfiError> services() const; + Result service_available(const std::string& name) const; + Result service_info(const std::string& name) const; // RAII / moves ~RsdHandshake() noexcept = default; diff --git a/cpp/include/idevice++/tcp_object_stack.hpp b/cpp/include/idevice++/tcp_object_stack.hpp index d4e1363..7670ca0 100644 --- a/cpp/include/idevice++/tcp_object_stack.hpp +++ b/cpp/include/idevice++/tcp_object_stack.hpp @@ -4,7 +4,6 @@ #include #include #include -#include #include #include @@ -12,7 +11,8 @@ namespace IdeviceFFI { -// ---------------- OwnedBuffer: RAII for zero-copy read buffers ---------------- +// ---------------- OwnedBuffer: RAII for zero-copy read buffers +// ---------------- class OwnedBuffer { public: OwnedBuffer() noexcept : p_(nullptr), n_(0) {} @@ -117,7 +117,8 @@ class TcpObjectStackEater { ~TcpObjectStackEater() { reset(); } // Blocks until a packet is available. On success, 'out' adopts the buffer - // and you must keep 'out' alive until done (RAII frees via idevice_data_free). + // and you must keep 'out' alive until done (RAII frees via + // idevice_data_free). bool read(OwnedBuffer& out, FfiError& err) const; ::TcpEatObject* raw() const { return h_; } @@ -139,34 +140,34 @@ class TcpObjectStackEater { // ---------------- Stack builder: returns feeder + eater + adapter ------------ class TcpObjectStack { public: - TcpObjectStack() = default; - TcpObjectStack(const TcpObjectStack&) = delete; // no sharing - TcpObjectStack& operator=(const TcpObjectStack&) = delete; - TcpObjectStack(TcpObjectStack&&) noexcept = default; // movable - TcpObjectStack& operator=(TcpObjectStack&&) noexcept = default; + TcpObjectStack() = default; + TcpObjectStack(const TcpObjectStack&) = delete; // no sharing + TcpObjectStack& operator=(const TcpObjectStack&) = delete; + TcpObjectStack(TcpObjectStack&&) noexcept = default; // movable + TcpObjectStack& operator=(TcpObjectStack&&) noexcept = default; // Build the stack (dual-handle). Name kept to minimize churn. - static std::optional - create(const std::string& our_ip, const std::string& their_ip, FfiError& err); + static Result create(const std::string& our_ip, + const std::string& their_ip); - TcpObjectStackFeeder& feeder(); - const TcpObjectStackFeeder& feeder() const; + TcpObjectStackFeeder& feeder(); + const TcpObjectStackFeeder& feeder() const; - TcpObjectStackEater& eater(); - const TcpObjectStackEater& eater() const; + TcpObjectStackEater& eater(); + const TcpObjectStackEater& eater() const; - Adapter& adapter(); - const Adapter& adapter() const; + Adapter& adapter(); + const Adapter& adapter() const; - std::optional release_feeder(); // nullptr inside wrapper after call - std::optional release_eater(); // nullptr inside wrapper after call - std::optional release_adapter(); + Option release_feeder(); // nullptr inside wrapper after call + Option release_eater(); // nullptr inside wrapper after call + Option release_adapter(); private: struct Impl { - TcpObjectStackFeeder feeder; - TcpObjectStackEater eater; - std::optional adapter; + TcpObjectStackFeeder feeder; + TcpObjectStackEater eater; + Option adapter; }; // Unique ownership so there’s a single point of truth to release from std::unique_ptr impl_; diff --git a/cpp/include/idevice++/usbmuxd.hpp b/cpp/include/idevice++/usbmuxd.hpp index 91fc7b4..018f312 100644 --- a/cpp/include/idevice++/usbmuxd.hpp +++ b/cpp/include/idevice++/usbmuxd.hpp @@ -5,8 +5,8 @@ #include #include +#include #include -#include #include #include @@ -29,10 +29,9 @@ using ConnectionPtr = class UsbmuxdAddr { public: - static std::optional - tcp_new(const sockaddr* addr, socklen_t addr_len, FfiError& err); + static Result tcp_new(const sockaddr* addr, socklen_t addr_len); #if defined(__unix__) || defined(__APPLE__) - static std::optional unix_new(const std::string& path, FfiError& err); + static Result unix_new(const std::string& path); #endif static UsbmuxdAddr default_new(); @@ -66,19 +65,19 @@ class UsbmuxdConnectionType { class UsbmuxdDevice { public: - ~UsbmuxdDevice() noexcept = default; - UsbmuxdDevice(UsbmuxdDevice&&) noexcept = default; - UsbmuxdDevice& operator=(UsbmuxdDevice&&) noexcept = default; - UsbmuxdDevice(const UsbmuxdDevice&) = delete; - UsbmuxdDevice& operator=(const UsbmuxdDevice&) = delete; + ~UsbmuxdDevice() noexcept = default; + UsbmuxdDevice(UsbmuxdDevice&&) noexcept = default; + UsbmuxdDevice& operator=(UsbmuxdDevice&&) noexcept = default; + UsbmuxdDevice(const UsbmuxdDevice&) = delete; + UsbmuxdDevice& operator=(const UsbmuxdDevice&) = delete; - static UsbmuxdDevice adopt(UsbmuxdDeviceHandle* h) noexcept { return UsbmuxdDevice(h); } + static UsbmuxdDevice adopt(UsbmuxdDeviceHandle* h) noexcept { return UsbmuxdDevice(h); } - UsbmuxdDeviceHandle* raw() const noexcept { return handle_.get(); } + UsbmuxdDeviceHandle* raw() const noexcept { return handle_.get(); } - std::optional get_udid() const; - std::optional get_id() const; - std::optional get_connection_type() const; + Option get_udid() const; + Option get_id() const; + Option get_connection_type() const; private: explicit UsbmuxdDevice(UsbmuxdDeviceHandle* h) noexcept : handle_(h) {} @@ -91,30 +90,28 @@ class PairingFile; class UsbmuxdConnection { public: - static std::optional - tcp_new(const idevice_sockaddr* addr, idevice_socklen_t addr_len, uint32_t tag, FfiError& err); + static Result + tcp_new(const idevice_sockaddr* addr, idevice_socklen_t addr_len, uint32_t tag); #if defined(__unix__) || defined(__APPLE__) - static std::optional - unix_new(const std::string& path, uint32_t tag, FfiError& err); + static Result unix_new(const std::string& path, uint32_t tag); #endif - static std::optional default_new(uint32_t tag, FfiError& err); + static Result default_new(uint32_t tag); - ~UsbmuxdConnection() noexcept = default; - UsbmuxdConnection(UsbmuxdConnection&&) noexcept = default; - UsbmuxdConnection& operator=(UsbmuxdConnection&&) noexcept = default; - UsbmuxdConnection(const UsbmuxdConnection&) = delete; - UsbmuxdConnection& operator=(const UsbmuxdConnection&) = delete; + ~UsbmuxdConnection() noexcept = default; + UsbmuxdConnection(UsbmuxdConnection&&) noexcept = default; + UsbmuxdConnection& operator=(UsbmuxdConnection&&) noexcept = default; + UsbmuxdConnection(const UsbmuxdConnection&) = delete; + UsbmuxdConnection& operator=(const UsbmuxdConnection&) = delete; - std::optional> get_devices(FfiError& err) const; - std::optional get_buid(FfiError& err) const; - std::optional get_pair_record(const std::string& udid, FfiError& err); + Result, FfiError> get_devices() const; + Result get_buid() const; + Result get_pair_record(const std::string& udid); - std::optional - connect_to_device(uint32_t device_id, uint16_t port, const std::string& path, FfiError& err) &&; - std::optional - connect_to_device(uint32_t, uint16_t, const std::string&, FfiError&) & = delete; - std::optional - connect_to_device(uint32_t, uint16_t, const std::string&, FfiError&) const& = delete; + Result + connect_to_device(uint32_t device_id, uint16_t port, const std::string& path) &&; + Result connect_to_device(uint32_t, uint16_t, const std::string&) & = delete; + Result + connect_to_device(uint32_t, uint16_t, const std::string&) const& = delete; UsbmuxdConnectionHandle* raw() const noexcept { return handle_.get(); } diff --git a/cpp/src/adapter_stream.cpp b/cpp/src/adapter_stream.cpp index 2d782bf..2c2df76 100644 --- a/cpp/src/adapter_stream.cpp +++ b/cpp/src/adapter_stream.cpp @@ -1,43 +1,50 @@ // Jackson Coxson #include +#include namespace IdeviceFFI { -bool AdapterStream::close(FfiError& err) { - if (!h_) - return true; - if (IdeviceFfiError* e = ::adapter_close(h_)) { - err = FfiError(e); - return false; - } - h_ = nullptr; - return true; +Result AdapterStream::close() { + if (!h_) + return Ok(); + + FfiError e(::adapter_close(h_)); + if (e) { + return Err(e); + } + + h_ = nullptr; + return Ok(); } -bool AdapterStream::send(const uint8_t* data, size_t len, FfiError& err) { - if (!h_) - return false; - if (IdeviceFfiError* e = ::adapter_send(h_, data, len)) { - err = FfiError(e); - return false; - } - return true; +Result AdapterStream::send(const uint8_t *data, size_t len) { + if (!h_) + return Err(FfiError::NotConnected()); + FfiError e(::adapter_send(h_, data, len)); + if (e) { + return Err(e); + } + return Ok(); } -bool AdapterStream::recv(std::vector& out, FfiError& err, size_t max_hint) { - if (!h_) - return false; - if (max_hint == 0) - max_hint = 2048; - out.resize(max_hint); - size_t actual = 0; - if (IdeviceFfiError* e = ::adapter_recv(h_, out.data(), &actual, out.size())) { - err = FfiError(e); - return false; - } - out.resize(actual); - return true; +Result, FfiError> AdapterStream::recv(size_t max_hint) { + if (!h_) + return Err(FfiError::NotConnected()); + + if (max_hint == 0) + max_hint = 2048; + + std::vector out(max_hint); + size_t actual = 0; + + FfiError e(::adapter_recv(h_, out.data(), &actual, out.size())); + if (e) { + return Err(e); + } + + out.resize(actual); + return Ok(std::move(out)); } } // namespace IdeviceFFI diff --git a/cpp/src/app_service.cpp b/cpp/src/app_service.cpp index c009aaf..ae9d9d9 100644 --- a/cpp/src/app_service.cpp +++ b/cpp/src/app_service.cpp @@ -5,193 +5,177 @@ namespace IdeviceFFI { // ---- Factories ---- -std::optional -AppService::connect_rsd(Adapter& adapter, RsdHandshake& rsd, FfiError& err) { - AppServiceHandle* out = nullptr; - if (IdeviceFfiError* e = ::app_service_connect_rsd(adapter.raw(), rsd.raw(), &out)) { - err = FfiError(e); - return std::nullopt; - } - return AppService::adopt(out); +Result AppService::connect_rsd(Adapter &adapter, + RsdHandshake &rsd) { + AppServiceHandle *out = nullptr; + if (IdeviceFfiError *e = + ::app_service_connect_rsd(adapter.raw(), rsd.raw(), &out)) { + return Err(FfiError(e)); + } + return Ok(AppService::adopt(out)); } -std::optional AppService::from_readwrite_ptr(ReadWriteOpaque* consumed, FfiError& err) { - AppServiceHandle* out = nullptr; - if (IdeviceFfiError* e = ::app_service_new(consumed, &out)) { - err = FfiError(e); - return std::nullopt; - } - return AppService::adopt(out); +Result +AppService::from_readwrite_ptr(ReadWriteOpaque *consumed) { + AppServiceHandle *out = nullptr; + if (IdeviceFfiError *e = ::app_service_new(consumed, &out)) { + return Err(FfiError(e)); + } + return Ok(AppService::adopt(out)); } -std::optional AppService::from_readwrite(ReadWrite&& rw, FfiError& err) { - // Rust consumes the stream regardless of result → release BEFORE call - return from_readwrite_ptr(rw.release(), err); +Result AppService::from_readwrite(ReadWrite &&rw) { + // Rust consumes the stream regardless of result → release BEFORE call + return from_readwrite_ptr(rw.release()); } // ---- Helpers to copy/free C arrays ---- -static std::vector copy_and_free_app_list(AppListEntryC* arr, size_t n) { - std::vector out; - out.reserve(n); - for (size_t i = 0; i < n; ++i) { - const auto& c = arr[i]; - AppInfo a; - a.is_removable = c.is_removable != 0; - if (c.name) - a.name = c.name; - a.is_first_party = c.is_first_party != 0; - if (c.path) - a.path = c.path; - if (c.bundle_identifier) - a.bundle_identifier = c.bundle_identifier; - a.is_developer_app = c.is_developer_app != 0; - if (c.bundle_version) - a.bundle_version = std::string(c.bundle_version); - a.is_internal = c.is_internal != 0; - a.is_hidden = c.is_hidden != 0; - a.is_app_clip = c.is_app_clip != 0; - if (c.version) - a.version = std::string(c.version); - out.emplace_back(std::move(a)); - } - ::app_service_free_app_list(arr, n); - return out; +static std::vector copy_and_free_app_list(AppListEntryC *arr, + size_t n) { + std::vector out; + out.reserve(n); + for (size_t i = 0; i < n; ++i) { + const auto &c = arr[i]; + AppInfo a; + a.is_removable = c.is_removable != 0; + if (c.name) + a.name = c.name; + a.is_first_party = c.is_first_party != 0; + if (c.path) + a.path = c.path; + if (c.bundle_identifier) + a.bundle_identifier = c.bundle_identifier; + a.is_developer_app = c.is_developer_app != 0; + if (c.bundle_version) + a.bundle_version = std::string(c.bundle_version); + a.is_internal = c.is_internal != 0; + a.is_hidden = c.is_hidden != 0; + a.is_app_clip = c.is_app_clip != 0; + if (c.version) + a.version = std::string(c.version); + out.emplace_back(std::move(a)); + } + ::app_service_free_app_list(arr, n); + return out; } -static std::vector copy_and_free_process_list(ProcessTokenC* arr, size_t n) { - std::vector out; - out.reserve(n); - for (size_t i = 0; i < n; ++i) { - ProcessToken p; - p.pid = arr[i].pid; - if (arr[i].executable_url) - p.executable_url = std::string(arr[i].executable_url); - out.emplace_back(std::move(p)); - } - ::app_service_free_process_list(arr, n); - return out; +static std::vector copy_and_free_process_list(ProcessTokenC *arr, + size_t n) { + std::vector out; + out.reserve(n); + for (size_t i = 0; i < n; ++i) { + ProcessToken p; + p.pid = arr[i].pid; + if (arr[i].executable_url) + p.executable_url = std::string(arr[i].executable_url); + out.emplace_back(std::move(p)); + } + ::app_service_free_process_list(arr, n); + return out; } // ---- API impls ---- -std::optional> AppService::list_apps(bool app_clips, - bool removable, - bool hidden, - bool internal, - bool default_apps, - FfiError& err) const { - AppListEntryC* arr = nullptr; - size_t n = 0; - if (IdeviceFfiError* e = ::app_service_list_apps(handle_.get(), - app_clips ? 1 : 0, - removable ? 1 : 0, - hidden ? 1 : 0, - internal ? 1 : 0, - default_apps ? 1 : 0, - &arr, - &n)) { - err = FfiError(e); - return std::nullopt; - } - return copy_and_free_app_list(arr, n); +Result, FfiError> +AppService::list_apps(bool app_clips, bool removable, bool hidden, + bool internal, bool default_apps) const { + AppListEntryC *arr = nullptr; + size_t n = 0; + if (IdeviceFfiError *e = ::app_service_list_apps( + handle_.get(), app_clips ? 1 : 0, removable ? 1 : 0, hidden ? 1 : 0, + internal ? 1 : 0, default_apps ? 1 : 0, &arr, &n)) { + + return Err(FfiError(e)); + } + return Ok(copy_and_free_app_list(arr, n)); } -std::optional AppService::launch(const std::string& bundle_id, - const std::vector& argv, - bool kill_existing, - bool start_suspended, - FfiError& err) { - std::vector c_argv; - c_argv.reserve(argv.size()); - for (auto& s : argv) - c_argv.push_back(s.c_str()); +Result +AppService::launch(const std::string &bundle_id, + const std::vector &argv, bool kill_existing, + bool start_suspended) { + std::vector c_argv; + c_argv.reserve(argv.size()); + for (auto &s : argv) + c_argv.push_back(s.c_str()); - LaunchResponseC* resp = nullptr; - if (IdeviceFfiError* e = ::app_service_launch_app(handle_.get(), - bundle_id.c_str(), - c_argv.empty() ? nullptr : c_argv.data(), - c_argv.size(), - kill_existing ? 1 : 0, - start_suspended ? 1 : 0, - NULL, // TODO: stdio handling - &resp)) { - err = FfiError(e); - return std::nullopt; - } + LaunchResponseC *resp = nullptr; + if (IdeviceFfiError *e = ::app_service_launch_app( + handle_.get(), bundle_id.c_str(), + c_argv.empty() ? nullptr : c_argv.data(), c_argv.size(), + kill_existing ? 1 : 0, start_suspended ? 1 : 0, + NULL, // TODO: stdio handling + &resp)) { + return Err(FfiError(e)); + } - LaunchResponse out; - out.process_identifier_version = resp->process_identifier_version; - out.pid = resp->pid; - if (resp->executable_url) - out.executable_url = resp->executable_url; - if (resp->audit_token && resp->audit_token_len > 0) { - out.audit_token.assign(resp->audit_token, resp->audit_token + resp->audit_token_len); - } - ::app_service_free_launch_response(resp); - return out; + LaunchResponse out; + out.process_identifier_version = resp->process_identifier_version; + out.pid = resp->pid; + if (resp->executable_url) + out.executable_url = resp->executable_url; + if (resp->audit_token && resp->audit_token_len > 0) { + out.audit_token.assign(resp->audit_token, + resp->audit_token + resp->audit_token_len); + } + ::app_service_free_launch_response(resp); + return Ok(std::move(out)); } -std::optional> AppService::list_processes(FfiError& err) const { - ProcessTokenC* arr = nullptr; - size_t n = 0; - if (IdeviceFfiError* e = ::app_service_list_processes(handle_.get(), &arr, &n)) { - err = FfiError(e); - return std::nullopt; - } - return copy_and_free_process_list(arr, n); +Result, FfiError> AppService::list_processes() const { + ProcessTokenC *arr = nullptr; + size_t n = 0; + if (IdeviceFfiError *e = + ::app_service_list_processes(handle_.get(), &arr, &n)) { + return Err(FfiError(e)); + } + return Ok(copy_and_free_process_list(arr, n)); } -bool AppService::uninstall(const std::string& bundle_id, FfiError& err) { - if (IdeviceFfiError* e = ::app_service_uninstall_app(handle_.get(), bundle_id.c_str())) { - err = FfiError(e); - return false; - } - return true; +Result AppService::uninstall(const std::string &bundle_id) { + if (IdeviceFfiError *e = + ::app_service_uninstall_app(handle_.get(), bundle_id.c_str())) { + return Err(FfiError(e)); + } + return Ok(); } -std::optional -AppService::send_signal(uint32_t pid, uint32_t signal, FfiError& err) { - SignalResponseC* c = nullptr; - if (IdeviceFfiError* e = ::app_service_send_signal(handle_.get(), pid, signal, &c)) { - err = FfiError(e); - return std::nullopt; - } - SignalResponse out; - out.pid = c->pid; - if (c->executable_url) - out.executable_url = std::string(c->executable_url); - out.device_timestamp_ms = c->device_timestamp; - out.signal = c->signal; - ::app_service_free_signal_response(c); - return out; +Result AppService::send_signal(uint32_t pid, + uint32_t signal) { + SignalResponseC *c = nullptr; + if (IdeviceFfiError *e = + ::app_service_send_signal(handle_.get(), pid, signal, &c)) { + return Err(FfiError(e)); + } + SignalResponse out; + out.pid = c->pid; + if (c->executable_url) + out.executable_url = std::string(c->executable_url); + out.device_timestamp_ms = c->device_timestamp; + out.signal = c->signal; + ::app_service_free_signal_response(c); + return Ok(std::move(out)); } -std::optional AppService::fetch_icon(const std::string& bundle_id, - float width, - float height, - float scale, - bool allow_placeholder, - FfiError& err) { - IconDataC* c = nullptr; - if (IdeviceFfiError* e = ::app_service_fetch_app_icon(handle_.get(), - bundle_id.c_str(), - width, - height, - scale, - allow_placeholder ? 1 : 0, - &c)) { - err = FfiError(e); - return std::nullopt; - } - IconData out; - if (c->data && c->data_len) { - out.data.assign(c->data, c->data + c->data_len); - } - out.icon_width = c->icon_width; - out.icon_height = c->icon_height; - out.minimum_width = c->minimum_width; - out.minimum_height = c->minimum_height; - ::app_service_free_icon_data(c); - return out; +Result AppService::fetch_icon(const std::string &bundle_id, + float width, float height, + float scale, + bool allow_placeholder) { + IconDataC *c = nullptr; + if (IdeviceFfiError *e = ::app_service_fetch_app_icon( + handle_.get(), bundle_id.c_str(), width, height, scale, + allow_placeholder ? 1 : 0, &c)) { + return Err(FfiError(e)); + } + IconData out; + if (c->data && c->data_len) { + out.data.assign(c->data, c->data + c->data_len); + } + out.icon_width = c->icon_width; + out.icon_height = c->icon_height; + out.minimum_width = c->minimum_width; + out.minimum_height = c->minimum_height; + ::app_service_free_icon_data(c); + return Ok(std::move(out)); } } // namespace IdeviceFFI diff --git a/cpp/src/core_device.cpp b/cpp/src/core_device.cpp index 07e91e3..5349211 100644 --- a/cpp/src/core_device.cpp +++ b/cpp/src/core_device.cpp @@ -6,114 +6,115 @@ namespace IdeviceFFI { // ---- Factories ---- -std::optional CoreDeviceProxy::connect(Provider& provider, FfiError& err) { - CoreDeviceProxyHandle* out = nullptr; - if (IdeviceFfiError* e = ::core_device_proxy_connect(provider.raw(), &out)) { - err = FfiError(e); - return std::nullopt; - } - return CoreDeviceProxy::adopt(out); +Result CoreDeviceProxy::connect(Provider &provider) { + CoreDeviceProxyHandle *out = nullptr; + FfiError e(::core_device_proxy_connect(provider.raw(), &out)); + if (e) { + return Err(e); + } + return Ok(CoreDeviceProxy::adopt(out)); } -std::optional CoreDeviceProxy::from_socket(Idevice&& socket, FfiError& err) { - CoreDeviceProxyHandle* out = nullptr; +Result +CoreDeviceProxy::from_socket(Idevice &&socket) { + CoreDeviceProxyHandle *out = nullptr; - // Rust consumes the socket regardless of result → release BEFORE call - IdeviceHandle* raw = socket.release(); + // Rust consumes the socket regardless of result → release BEFORE call + IdeviceHandle *raw = socket.release(); - if (IdeviceFfiError* e = ::core_device_proxy_new(raw, &out)) { - // socket is already consumed on error; do NOT touch it - err = FfiError(e); - return std::nullopt; - } - return CoreDeviceProxy::adopt(out); + FfiError e(::core_device_proxy_new(raw, &out)); + if (e) { + return Err(e); + } + return Ok(CoreDeviceProxy::adopt(out)); } // ---- IO ---- -bool CoreDeviceProxy::send(const uint8_t* data, size_t len, FfiError& err) { - if (IdeviceFfiError* e = ::core_device_proxy_send(handle_.get(), data, len)) { - err = FfiError(e); - return false; - } - return true; +Result CoreDeviceProxy::send(const uint8_t *data, size_t len) { + FfiError e(::core_device_proxy_send(handle_.get(), data, len)); + if (e) { + return Err(e); + } + return Ok(); } -bool CoreDeviceProxy::recv(std::vector& out, FfiError& err) { - if (out.empty()) - out.resize(4096); // a reasonable default; caller can pre-size - size_t actual = 0; - if (IdeviceFfiError* e = - ::core_device_proxy_recv(handle_.get(), out.data(), &actual, out.size())) { - err = FfiError(e); - return false; - } - out.resize(actual); - return true; +Result CoreDeviceProxy::recv(std::vector &out) { + if (out.empty()) + out.resize(4096); // a reasonable default; caller can pre-size + size_t actual = 0; + FfiError e( + ::core_device_proxy_recv(handle_.get(), out.data(), &actual, out.size())); + if (e) { + return Err(e); + } + out.resize(actual); + return Ok(); } // ---- Handshake ---- -std::optional CoreDeviceProxy::get_client_parameters(FfiError& err) const { - uint16_t mtu = 0; - char* addr_c = nullptr; - char* mask_c = nullptr; +Result +CoreDeviceProxy::get_client_parameters() const { + uint16_t mtu = 0; + char *addr_c = nullptr; + char *mask_c = nullptr; - if (IdeviceFfiError* e = - ::core_device_proxy_get_client_parameters(handle_.get(), &mtu, &addr_c, &mask_c)) { - err = FfiError(e); - return std::nullopt; - } + FfiError e(::core_device_proxy_get_client_parameters(handle_.get(), &mtu, + &addr_c, &mask_c)); + if (e) { + return Err(e); + } - CoreClientParams params; - params.mtu = mtu; - if (addr_c) { - params.address = addr_c; - ::idevice_string_free(addr_c); - } - if (mask_c) { - params.netmask = mask_c; - ::idevice_string_free(mask_c); - } - return params; + CoreClientParams params; + params.mtu = mtu; + if (addr_c) { + params.address = addr_c; + ::idevice_string_free(addr_c); + } + if (mask_c) { + params.netmask = mask_c; + ::idevice_string_free(mask_c); + } + return Ok(std::move(params)); } -std::optional CoreDeviceProxy::get_server_address(FfiError& err) const { - char* addr_c = nullptr; - if (IdeviceFfiError* e = ::core_device_proxy_get_server_address(handle_.get(), &addr_c)) { - err = FfiError(e); - return std::nullopt; - } - std::string s; - if (addr_c) { - s = addr_c; - ::idevice_string_free(addr_c); - } - return s; +Result CoreDeviceProxy::get_server_address() const { + char *addr_c = nullptr; + FfiError e(::core_device_proxy_get_server_address(handle_.get(), &addr_c)); + if (e) { + return Err(e); + } + std::string s; + if (addr_c) { + s = addr_c; + ::idevice_string_free(addr_c); + } + return Ok(s); } -std::optional CoreDeviceProxy::get_server_rsd_port(FfiError& err) const { - uint16_t port = 0; - if (IdeviceFfiError* e = ::core_device_proxy_get_server_rsd_port(handle_.get(), &port)) { - err = FfiError(e); - return std::nullopt; - } - return port; +Result CoreDeviceProxy::get_server_rsd_port() const { + uint16_t port = 0; + FfiError e(::core_device_proxy_get_server_rsd_port(handle_.get(), &port)); + if (e) { + return Err(e); + } + return Ok(port); } // ---- Adapter creation (consumes *this) ---- -std::optional CoreDeviceProxy::create_tcp_adapter(FfiError& err) && { - AdapterHandle* out = nullptr; +Result CoreDeviceProxy::create_tcp_adapter() && { + AdapterHandle *out = nullptr; - // Rust consumes the proxy regardless of result → release BEFORE call - CoreDeviceProxyHandle* raw = this->release(); + // Rust consumes the proxy regardless of result → release BEFORE call + CoreDeviceProxyHandle *raw = this->release(); - if (IdeviceFfiError* e = ::core_device_proxy_create_tcp_adapter(raw, &out)) { - err = FfiError(e); - return std::nullopt; - } - return Adapter::adopt(out); + FfiError e(::core_device_proxy_create_tcp_adapter(raw, &out)); + if (e) { + return Err(e); + } + return Ok(Adapter::adopt(out)); } } // namespace IdeviceFFI diff --git a/cpp/src/debug_proxy.cpp b/cpp/src/debug_proxy.cpp index f3143cb..9e8321d 100644 --- a/cpp/src/debug_proxy.cpp +++ b/cpp/src/debug_proxy.cpp @@ -6,136 +6,138 @@ namespace IdeviceFFI { // ---- helpers ---- -static std::optional take_cstring(char* p) { - if (!p) - return std::nullopt; - std::string s(p); - ::idevice_string_free(p); - return s; +static Option take_cstring(char *p) { + if (!p) + return None; + std::string s(p); + ::idevice_string_free(p); + return Some(s); } // ---- DebugCommand ---- -std::optional DebugCommand::make(const std::string& name, - const std::vector& argv) { - std::vector c_argv; - c_argv.reserve(argv.size()); - for (auto& a : argv) - c_argv.push_back(a.c_str()); +Option DebugCommand::make(const std::string &name, + const std::vector &argv) { + std::vector c_argv; + c_argv.reserve(argv.size()); + for (auto &a : argv) + c_argv.push_back(a.c_str()); - auto* h = ::debugserver_command_new( - name.c_str(), - c_argv.empty() ? nullptr : const_cast(c_argv.data()), - c_argv.size()); - if (!h) - return std::nullopt; - return DebugCommand(h); + auto *h = ::debugserver_command_new( + name.c_str(), + c_argv.empty() ? nullptr : const_cast(c_argv.data()), + c_argv.size()); + if (!h) + return None; + return Some(DebugCommand(h)); } // ---- DebugProxy factories ---- -std::optional -DebugProxy::connect_rsd(Adapter& adapter, RsdHandshake& rsd, FfiError& err) { - ::DebugProxyHandle* out = nullptr; - if (IdeviceFfiError* e = ::debug_proxy_connect_rsd(adapter.raw(), rsd.raw(), &out)) { - err = FfiError(e); - return std::nullopt; - } - return DebugProxy(out); +Result DebugProxy::connect_rsd(Adapter &adapter, + RsdHandshake &rsd) { + ::DebugProxyHandle *out = nullptr; + FfiError e(::debug_proxy_connect_rsd(adapter.raw(), rsd.raw(), &out)); + if (e) { + return Err(e); + } + return Ok(DebugProxy(out)); } -std::optional DebugProxy::from_readwrite_ptr(::ReadWriteOpaque* consumed, - FfiError& err) { - ::DebugProxyHandle* out = nullptr; - if (IdeviceFfiError* e = ::debug_proxy_new(consumed, &out)) { - err = FfiError(e); - return std::nullopt; - } - return DebugProxy(out); +Result +DebugProxy::from_readwrite_ptr(::ReadWriteOpaque *consumed) { + ::DebugProxyHandle *out = nullptr; + FfiError e(::debug_proxy_new(consumed, &out)); + if (e) { + return Err(e); + } + return Ok(DebugProxy(out)); } -std::optional DebugProxy::from_readwrite(ReadWrite&& rw, FfiError& err) { - // Rust consumes the pointer regardless of outcome; release before calling - return from_readwrite_ptr(rw.release(), err); +Result DebugProxy::from_readwrite(ReadWrite &&rw) { + // Rust consumes the pointer regardless of outcome; release before calling + return from_readwrite_ptr(rw.release()); } // ---- DebugProxy API ---- -std::optional DebugProxy::send_command(const std::string& name, - const std::vector& argv, - FfiError& err) { - auto cmd = DebugCommand::make(name, argv); - if (!cmd) { - // treat as invalid arg - err.code = -1; - err.message = "debugserver_command_new failed"; - return std::nullopt; - } +Result, FfiError> +DebugProxy::send_command(const std::string &name, + const std::vector &argv) { + auto cmdRes = DebugCommand::make(name, argv); + if (cmdRes.is_none()) { + // treat as invalid arg + FfiError err; + err.code = -1; + err.message = "debugserver_command_new failed"; + return Err(err); + } + auto cmd = std::move(cmdRes).unwrap(); - char* resp_c = nullptr; - if (IdeviceFfiError* e = ::debug_proxy_send_command(handle_, cmd->raw(), &resp_c)) { - err = FfiError(e); - return std::nullopt; - } - return take_cstring(resp_c); // may be null → std::nullopt + char *resp_c = nullptr; + FfiError e(::debug_proxy_send_command(handle_, cmd.raw(), &resp_c)); + if (e) { + return Err(e); + } + + return Ok(take_cstring(resp_c)); } -std::optional DebugProxy::read_response(FfiError& err) { - char* resp_c = nullptr; - if (IdeviceFfiError* e = ::debug_proxy_read_response(handle_, &resp_c)) { - err = FfiError(e); - return std::nullopt; - } - return take_cstring(resp_c); +Result, FfiError> DebugProxy::read_response() { + char *resp_c = nullptr; + FfiError e(::debug_proxy_read_response(handle_, &resp_c)); + if (e) { + return Err(e); + } + return Ok(take_cstring(resp_c)); } -bool DebugProxy::send_raw(const std::vector& data, FfiError& err) { - if (IdeviceFfiError* e = ::debug_proxy_send_raw(handle_, data.data(), data.size())) { - err = FfiError(e); - return false; - } - return true; +Result DebugProxy::send_raw(const std::vector &data) { + FfiError e(::debug_proxy_send_raw(handle_, data.data(), data.size())); + if (e) { + return Err(e); + } + return Ok(); } -std::optional DebugProxy::read(std::size_t len, FfiError& err) { - char* resp_c = nullptr; - if (IdeviceFfiError* e = ::debug_proxy_read(handle_, len, &resp_c)) { - err = FfiError(e); - return std::nullopt; - } - return take_cstring(resp_c); +Result, FfiError> DebugProxy::read(std::size_t len) { + char *resp_c = nullptr; + FfiError e(::debug_proxy_read(handle_, len, &resp_c)); + if (e) { + return Err(e); + } + return Ok(take_cstring(resp_c)); } -std::optional DebugProxy::set_argv(const std::vector& argv, - FfiError& err) { - std::vector c_argv; - c_argv.reserve(argv.size()); - for (auto& a : argv) - c_argv.push_back(a.c_str()); +Result, FfiError> +DebugProxy::set_argv(const std::vector &argv) { + std::vector c_argv; + c_argv.reserve(argv.size()); + for (auto &a : argv) + c_argv.push_back(a.c_str()); - char* resp_c = nullptr; - if (IdeviceFfiError* e = ::debug_proxy_set_argv( - handle_, - c_argv.empty() ? nullptr : const_cast(c_argv.data()), - c_argv.size(), - &resp_c)) { - err = FfiError(e); - return std::nullopt; - } - return take_cstring(resp_c); + char *resp_c = nullptr; + FfiError e(::debug_proxy_set_argv( + handle_, + c_argv.empty() ? nullptr : const_cast(c_argv.data()), + c_argv.size(), &resp_c)); + if (e) { + return Err(e); + } + return Ok(take_cstring(resp_c)); } -bool DebugProxy::send_ack(FfiError& err) { - if (IdeviceFfiError* e = ::debug_proxy_send_ack(handle_)) { - err = FfiError(e); - return false; - } - return true; +Result DebugProxy::send_ack() { + FfiError e(::debug_proxy_send_ack(handle_)); + if (e) { + return Err(e); + } + return Ok(); } -bool DebugProxy::send_nack(FfiError& err) { - if (IdeviceFfiError* e = ::debug_proxy_send_nack(handle_)) { - err = FfiError(e); - return false; - } - return true; +Result DebugProxy::send_nack() { + FfiError e(::debug_proxy_send_nack(handle_)); + if (e) { + return Err(e); + } + return Ok(); } } // namespace IdeviceFFI diff --git a/cpp/src/diagnosticsservice.cpp b/cpp/src/diagnosticsservice.cpp index f67d866..8e95cdb 100644 --- a/cpp/src/diagnosticsservice.cpp +++ b/cpp/src/diagnosticsservice.cpp @@ -1,35 +1,39 @@ // Jackson Coxson -#include #include +#include +#include namespace IdeviceFFI { // Local helper: take ownership of a C string and convert to std::string -static std::optional take_cstring(char* p) { - if (!p) - return std::nullopt; +static Option take_cstring(char* p) { + if (!p) { + return None; + } + std::string s(p); ::idevice_string_free(p); - return s; + return Some(std::move(s)); } // -------- SysdiagnoseStream -------- -std::optional> SysdiagnoseStream::next_chunk(FfiError& err) { - if (!h_) - return std::nullopt; +Result>, FfiError> SysdiagnoseStream::next_chunk() { + if (!h_) { + return Err(FfiError::NotConnected()); + } uint8_t* data = nullptr; std::size_t len = 0; - if (IdeviceFfiError* e = ::sysdiagnose_stream_next(h_, &data, &len)) { - err = FfiError(e); - return std::nullopt; + FfiError e(::sysdiagnose_stream_next(h_, &data, &len)); + if (e) { + return Err(e); } if (!data || len == 0) { // End of stream - return std::nullopt; + return Ok(Option>(None)); } // Copy into a C++ buffer @@ -38,52 +42,52 @@ std::optional> SysdiagnoseStream::next_chunk(FfiError& err) idevice_data_free(data, len); - return out; + return Ok(Some(out)); } // -------- DiagnosticsService -------- -std::optional -DiagnosticsService::connect_rsd(Adapter& adapter, RsdHandshake& rsd, FfiError& err) { +Result DiagnosticsService::connect_rsd(Adapter& adapter, + RsdHandshake& rsd) { ::DiagnosticsServiceHandle* out = nullptr; - if (IdeviceFfiError* e = ::diagnostics_service_connect_rsd(adapter.raw(), rsd.raw(), &out)) { - err = FfiError(e); - return std::nullopt; + FfiError e(::diagnostics_service_connect_rsd(adapter.raw(), rsd.raw(), &out)); + if (e) { + return Err(e); } - return DiagnosticsService(out); + return Ok(DiagnosticsService(out)); } -std::optional DiagnosticsService::from_stream_ptr(::ReadWriteOpaque* consumed, - FfiError& err) { +Result +DiagnosticsService::from_stream_ptr(::ReadWriteOpaque* consumed) { ::DiagnosticsServiceHandle* out = nullptr; - if (IdeviceFfiError* e = ::diagnostics_service_new(consumed, &out)) { - err = FfiError(e); - return std::nullopt; + FfiError e(::diagnostics_service_new(consumed, &out)); + if (e) { + return Err(e); } - return DiagnosticsService(out); + return Ok(DiagnosticsService(out)); } -std::optional DiagnosticsService::capture_sysdiagnose(bool dry_run, - FfiError& err) { - if (!h_) - return std::nullopt; +Result DiagnosticsService::capture_sysdiagnose(bool dry_run) { + if (!h_) { + return Err(FfiError::NotConnected()); + } char* filename_c = nullptr; std::size_t expected_len = 0; ::SysdiagnoseStreamHandle* stream_h = nullptr; - if (IdeviceFfiError* e = ::diagnostics_service_capture_sysdiagnose( - h_, dry_run ? true : false, &filename_c, &expected_len, &stream_h)) { - err = FfiError(e); - return std::nullopt; + FfiError e(::diagnostics_service_capture_sysdiagnose( + h_, dry_run ? true : false, &filename_c, &expected_len, &stream_h)); + if (e) { + return Err(e); } - auto fname = take_cstring(filename_c).value_or(std::string{}); + auto fname = take_cstring(filename_c).unwrap_or(std::string{}); SysdiagnoseStream stream(stream_h); SysdiagnoseCapture cap{/*preferred_filename*/ std::move(fname), /*expected_length*/ expected_len, /*stream*/ std::move(stream)}; - return cap; + return Ok(std::move(cap)); } } // namespace IdeviceFFI diff --git a/cpp/src/ffi.cpp b/cpp/src/ffi.cpp index fc95293..51be26f 100644 --- a/cpp/src/ffi.cpp +++ b/cpp/src/ffi.cpp @@ -2,6 +2,7 @@ #include #include +#include #include namespace IdeviceFFI { @@ -14,4 +15,18 @@ FfiError::FfiError(const IdeviceFfiError* err) FfiError::FfiError() : code(0), message("") { } + +FfiError FfiError::NotConnected() { + FfiError err; + err.code = -11; // from idevice/lib.rs + err.message = "No established socket connection"; + return err; +} +FfiError FfiError::InvalidArgument() { + FfiError err; + err.code = -57; // from idevice/lib.rs + err.message = "No established socket connection"; + return err; +} + } // namespace IdeviceFFI diff --git a/cpp/src/idevice.cpp b/cpp/src/idevice.cpp index 6899092..caa490b 100644 --- a/cpp/src/idevice.cpp +++ b/cpp/src/idevice.cpp @@ -4,53 +4,50 @@ namespace IdeviceFFI { -std::optional -Idevice::create(IdeviceSocketHandle* socket, const std::string& label, FfiError& err) { +Result Idevice::create(IdeviceSocketHandle* socket, const std::string& label) { IdeviceHandle* h = nullptr; - if (IdeviceFfiError* e = idevice_new(socket, label.c_str(), &h)) { - err = FfiError(e); - return std::nullopt; + FfiError e(idevice_new(socket, label.c_str(), &h)); + if (e) { + return Err(e); } - return Idevice(h); + return Ok(Idevice(h)); } -std::optional Idevice::create_tcp(const sockaddr* addr, - socklen_t addr_len, - const std::string& label, - FfiError& err) { +Result +Idevice::create_tcp(const sockaddr* addr, socklen_t addr_len, const std::string& label) { IdeviceHandle* h = nullptr; - if (IdeviceFfiError* e = idevice_new_tcp_socket(addr, addr_len, label.c_str(), &h)) { - err = FfiError(e); - return std::nullopt; + FfiError e(idevice_new_tcp_socket(addr, addr_len, label.c_str(), &h)); + if (e) { + return Err(e); } - return Idevice(h); + return Ok(Idevice(h)); } -std::optional Idevice::get_type(FfiError& err) const { - char* cstr = nullptr; - if (IdeviceFfiError* e = idevice_get_type(handle_.get(), &cstr)) { - err = FfiError(e); - return std::nullopt; +Result Idevice::get_type() const { + char* cstr = nullptr; + FfiError e(idevice_get_type(handle_.get(), &cstr)); + if (e) { + return Err(e); } std::string out(cstr); idevice_string_free(cstr); - return out; + return Ok(out); } -bool Idevice::rsd_checkin(FfiError& err) { - if (IdeviceFfiError* e = idevice_rsd_checkin(handle_.get())) { - err = FfiError(e); - return false; +Result Idevice::rsd_checkin() { + FfiError e(idevice_rsd_checkin(handle_.get())); + if (e) { + return Err(e); } - return true; + return Ok(); } -bool Idevice::start_session(const PairingFile& pairing_file, FfiError& err) { - if (IdeviceFfiError* e = idevice_start_session(handle_.get(), pairing_file.raw())) { - err = FfiError(e); - return false; +Result Idevice::start_session(const PairingFile& pairing_file) { + FfiError e(idevice_start_session(handle_.get(), pairing_file.raw())); + if (e) { + return Err(e); } - return true; + return Ok(); } } // namespace IdeviceFFI diff --git a/cpp/src/location_simulation.cpp b/cpp/src/location_simulation.cpp index c53eef6..99abb85 100644 --- a/cpp/src/location_simulation.cpp +++ b/cpp/src/location_simulation.cpp @@ -4,29 +4,29 @@ namespace IdeviceFFI { -std::optional LocationSimulation::create(RemoteServer& server, FfiError& err) { +Result LocationSimulation::create(RemoteServer& server) { LocationSimulationHandle* out = nullptr; - if (IdeviceFfiError* e = ::location_simulation_new(server.raw(), &out)) { - err = FfiError(e); - return std::nullopt; + FfiError e(::location_simulation_new(server.raw(), &out)); + if (e) { + return Err(e); } - return LocationSimulation::adopt(out); + return Ok(LocationSimulation::adopt(out)); } -bool LocationSimulation::clear(FfiError& err) { - if (IdeviceFfiError* e = ::location_simulation_clear(handle_.get())) { - err = FfiError(e); - return false; +Result LocationSimulation::clear() { + FfiError e(::location_simulation_clear(handle_.get())); + if (e) { + return Err(e); } - return true; + return Ok(); } -bool LocationSimulation::set(double latitude, double longitude, FfiError& err) { - if (IdeviceFfiError* e = ::location_simulation_set(handle_.get(), latitude, longitude)) { - err = FfiError(e); - return false; +Result LocationSimulation::set(double latitude, double longitude) { + FfiError e(::location_simulation_set(handle_.get(), latitude, longitude)); + if (e) { + return Err(e); } - return true; + return Ok(); } } // namespace IdeviceFFI diff --git a/cpp/src/lockdown.cpp b/cpp/src/lockdown.cpp index 107439e..2a798a1 100644 --- a/cpp/src/lockdown.cpp +++ b/cpp/src/lockdown.cpp @@ -7,60 +7,51 @@ namespace IdeviceFFI { -std::optional Lockdown::connect(Provider& provider, FfiError& err) { +Result Lockdown::connect(Provider& provider) { LockdowndClientHandle* out = nullptr; - - if (IdeviceFfiError* e = ::lockdownd_connect(provider.raw(), &out)) { - // Rust freed the provider on error -> abandon our ownership to avoid double free. + FfiError e(::lockdownd_connect(provider.raw(), &out)); + if (e) { provider.release(); - err = FfiError(e); - return std::nullopt; + return Err(e); } - // Success: provider is NOT consumed; keep ownership. - return Lockdown::adopt(out); + return Ok(Lockdown::adopt(out)); } -std::optional Lockdown::from_socket(Idevice&& socket, FfiError& err) { +Result Lockdown::from_socket(Idevice&& socket) { LockdowndClientHandle* out = nullptr; - - if (IdeviceFfiError* e = ::lockdownd_new(socket.raw(), &out)) { - // Error: Rust did NOT consume the socket (it returns early for invalid args), - // so keep ownership; report error. - err = FfiError(e); - return std::nullopt; + FfiError e(::lockdownd_new(socket.raw(), &out)); + if (e) { + return Err(e); } - // Success: Rust consumed the socket -> abandon our ownership. socket.release(); - return Lockdown::adopt(out); + return Ok(Lockdown::adopt(out)); } -bool Lockdown::start_session(const PairingFile& pf, FfiError& err) { - if (IdeviceFfiError* e = ::lockdownd_start_session(handle_.get(), pf.raw())) { - err = FfiError(e); - return false; +Result Lockdown::start_session(const PairingFile& pf) { + FfiError e(::lockdownd_start_session(handle_.get(), pf.raw())); + if (e) { + return Err(e); } - return true; + return Ok(); } -std::optional> Lockdown::start_service(const std::string& identifier, - FfiError& err) { +Result, FfiError> Lockdown::start_service(const std::string& identifier) { uint16_t port = 0; bool ssl = false; - if (IdeviceFfiError* e = - ::lockdownd_start_service(handle_.get(), identifier.c_str(), &port, &ssl)) { - err = FfiError(e); - return std::nullopt; + FfiError e(::lockdownd_start_service(handle_.get(), identifier.c_str(), &port, &ssl)); + if (e) { + return Err(e); } - return std::make_pair(port, ssl); + return Ok(std::make_pair(port, ssl)); } -std::optional Lockdown::get_value(const char* key, const char* domain, FfiError& err) { - plist_t out = nullptr; - if (IdeviceFfiError* e = ::lockdownd_get_value(handle_.get(), key, domain, &out)) { - err = FfiError(e); - return std::nullopt; +Result Lockdown::get_value(const char* key, const char* domain) { + plist_t out = nullptr; + FfiError e(::lockdownd_get_value(handle_.get(), key, domain, &out)); + if (e) { + return Err(e); } - return out; // caller now owns `out` and must free with the plist API + return Ok(out); } } // namespace IdeviceFFI diff --git a/cpp/src/pairing_file.cpp b/cpp/src/pairing_file.cpp index bc2f388..9ba8f40 100644 --- a/cpp/src/pairing_file.cpp +++ b/cpp/src/pairing_file.cpp @@ -8,46 +8,46 @@ namespace IdeviceFFI { // Deleter definition (out-of-line) void PairingFileDeleter::operator()(IdevicePairingFile* p) const noexcept { - if (p) + if (p) { idevice_pairing_file_free(p); + } } // Static member definitions -std::optional PairingFile::read(const std::string& path, FfiError& err) { +Result PairingFile::read(const std::string& path) { IdevicePairingFile* ptr = nullptr; - if (IdeviceFfiError* e = idevice_pairing_file_read(path.c_str(), &ptr)) { - err = FfiError(e); - return std::nullopt; + FfiError e(idevice_pairing_file_read(path.c_str(), &ptr)); + if (e) { + return Err(e); } - return PairingFile(ptr); + return Ok(PairingFile(ptr)); } -std::optional -PairingFile::from_bytes(const uint8_t* data, size_t size, FfiError& err) { +Result PairingFile::from_bytes(const uint8_t* data, size_t size) { IdevicePairingFile* raw = nullptr; - if (IdeviceFfiError* e = idevice_pairing_file_from_bytes(data, size, &raw)) { - err = FfiError(e); - return std::nullopt; + FfiError e(idevice_pairing_file_from_bytes(data, size, &raw)); + if (e) { + return Err(e); } - return PairingFile(raw); + return Ok(PairingFile(raw)); } -std::optional> PairingFile::serialize(FfiError& err) const { +Result, FfiError> PairingFile::serialize() const { if (!ptr_) { - return std::nullopt; + return Err(FfiError::InvalidArgument()); } uint8_t* data = nullptr; size_t size = 0; - if (IdeviceFfiError* e = idevice_pairing_file_serialize(ptr_.get(), &data, &size)) { - err = FfiError(e); - return std::nullopt; + FfiError e(idevice_pairing_file_serialize(ptr_.get(), &data, &size)); + if (e) { + return Err(e); } std::vector out(data, data + size); idevice_data_free(data, size); - return out; + return Ok(out); } } // namespace IdeviceFFI diff --git a/cpp/src/provider.cpp b/cpp/src/provider.cpp index 6a5f606..7c47d56 100644 --- a/cpp/src/provider.cpp +++ b/cpp/src/provider.cpp @@ -6,58 +6,53 @@ namespace IdeviceFFI { -std::optional Provider::tcp_new(const idevice_sockaddr* ip, - PairingFile&& pairing, - const std::string& label, - FfiError& err) { +Result +Provider::tcp_new(const idevice_sockaddr* ip, PairingFile&& pairing, const std::string& label) { IdeviceProviderHandle* out = nullptr; - // Call with exact types; do NOT cast to void* - if (IdeviceFfiError* e = idevice_tcp_provider_new( - ip, static_cast(pairing.raw()), label.c_str(), &out)) { - err = FfiError(e); - return std::nullopt; + FfiError e(idevice_tcp_provider_new( + ip, static_cast(pairing.raw()), label.c_str(), &out)); + if (e) { + return Err(e); } // Success: Rust consumed the pairing file -> abandon our ownership - pairing.release(); // implement as: ptr_.release() in PairingFile + pairing.release(); - return Provider::adopt(out); + return Ok(Provider::adopt(out)); } -std::optional Provider::usbmuxd_new(UsbmuxdAddr&& addr, - uint32_t tag, - const std::string& udid, - uint32_t device_id, - const std::string& label, - FfiError& err) { +Result Provider::usbmuxd_new(UsbmuxdAddr&& addr, + uint32_t tag, + const std::string& udid, + uint32_t device_id, + const std::string& label) { IdeviceProviderHandle* out = nullptr; - if (IdeviceFfiError* e = usbmuxd_provider_new(static_cast(addr.raw()), - tag, - udid.c_str(), - device_id, - label.c_str(), - &out)) { - err = FfiError(e); - return std::nullopt; + FfiError e(usbmuxd_provider_new(static_cast(addr.raw()), + tag, + udid.c_str(), + device_id, + label.c_str(), + &out)); + if (e) { + return Err(e); } // Success: Rust consumed the addr -> abandon our ownership - addr.release(); // implement as: ptr_.release() in UsbmuxdAddr - - return Provider::adopt(out); + addr.release(); + return Ok(Provider::adopt(out)); } -std::optional Provider::get_pairing_file(FfiError& err) { +Result Provider::get_pairing_file() { IdevicePairingFile* out = nullptr; - if (IdeviceFfiError* e = idevice_provider_get_pairing_file(handle_.get(), &out)) { - err = FfiError(e); - return std::nullopt; + FfiError e(idevice_provider_get_pairing_file(handle_.get(), &out)); + if (e) { + return Err(e); } - - return PairingFile(out); + + return Ok(PairingFile(out)); } } // namespace IdeviceFFI diff --git a/cpp/src/remote_server.cpp b/cpp/src/remote_server.cpp index 90e1b06..acfc736 100644 --- a/cpp/src/remote_server.cpp +++ b/cpp/src/remote_server.cpp @@ -4,27 +4,26 @@ namespace IdeviceFFI { -std::optional RemoteServer::from_socket(ReadWrite&& rw, FfiError& err) { +Result RemoteServer::from_socket(ReadWrite&& rw) { RemoteServerHandle* out = nullptr; // Rust consumes the stream regardless of result, release BEFORE the call ReadWriteOpaque* raw = rw.release(); - if (IdeviceFfiError* e = ::remote_server_new(raw, &out)) { - err = FfiError(e); - return std::nullopt; + FfiError e(::remote_server_new(raw, &out)); + if (e) { + return Err(e); } - return RemoteServer::adopt(out); + return Ok(RemoteServer::adopt(out)); } -std::optional -RemoteServer::connect_rsd(Adapter& adapter, RsdHandshake& rsd, FfiError& err) { +Result RemoteServer::connect_rsd(Adapter& adapter, RsdHandshake& rsd) { RemoteServerHandle* out = nullptr; - if (IdeviceFfiError* e = ::remote_server_connect_rsd(adapter.raw(), rsd.raw(), &out)) { - err = FfiError(e); - return std::nullopt; + FfiError e(::remote_server_connect_rsd(adapter.raw(), rsd.raw(), &out)); + if (e) { + return Err(e); } - return RemoteServer::adopt(out); + return Ok(RemoteServer::adopt(out)); } } // namespace IdeviceFFI diff --git a/cpp/src/rsd.cpp b/cpp/src/rsd.cpp index 0511c54..281b09d 100644 --- a/cpp/src/rsd.cpp +++ b/cpp/src/rsd.cpp @@ -7,10 +7,12 @@ namespace IdeviceFFI { // ---------- helpers to copy/free CRsdService ---------- static RsdService to_cpp_and_free(CRsdService* c) { RsdService s; - if (c->name) + if (c->name) { s.name = c->name; - if (c->entitlement) + } + if (c->entitlement) { s.entitlement = c->entitlement; + } s.port = c->port; s.uses_remote_xpc = c->uses_remote_xpc; s.service_version = c->service_version; @@ -20,8 +22,9 @@ static RsdService to_cpp_and_free(CRsdService* c) { auto** arr = c->features; s.features.reserve(c->features_count); for (size_t i = 0; i < c->features_count; ++i) { - if (arr[i]) + if (arr[i]) { s.features.emplace_back(arr[i]); + } } } @@ -33,8 +36,9 @@ static RsdService to_cpp_and_free(CRsdService* c) { static std::vector to_cpp_and_free(CRsdServiceArray* arr) { std::vector out; if (!arr || !arr->services || arr->count == 0) { - if (arr) + if (arr) { rsd_free_services(arr); + } return out; } out.reserve(arr->count); @@ -51,8 +55,9 @@ static std::vector to_cpp_and_free(CRsdServiceArray* arr) { auto** feats = begin[i].features; out.back().features.reserve(begin[i].features_count); for (size_t j = 0; j < begin[i].features_count; ++j) { - if (feats[j]) + if (feats[j]) { out.back().features.emplace_back(feats[j]); + } } } } @@ -80,68 +85,67 @@ RsdHandshake& RsdHandshake::operator=(const RsdHandshake& other) { } // ---------- factory ---------- -std::optional RsdHandshake::from_socket(ReadWrite&& rw, FfiError& err) { +Result RsdHandshake::from_socket(ReadWrite&& rw) { RsdHandshakeHandle* out = nullptr; // Rust consumes the socket regardless of result ⇒ release BEFORE call. ReadWriteOpaque* raw = rw.release(); - - if (IdeviceFfiError* e = rsd_handshake_new(raw, &out)) { - err = FfiError(e); - return std::nullopt; + FfiError e(rsd_handshake_new(raw, &out)); + if (e) { + return Err(e); } - return RsdHandshake::adopt(out); + return Ok(RsdHandshake::adopt(out)); } // ---------- queries ---------- -std::optional RsdHandshake::protocol_version(FfiError& err) const { - size_t v = 0; - if (IdeviceFfiError* e = rsd_get_protocol_version(handle_.get(), &v)) { - err = FfiError(e); - return std::nullopt; +Result RsdHandshake::protocol_version() const { + size_t v = 0; + FfiError e(rsd_get_protocol_version(handle_.get(), &v)); + if (e) { + return Err(e); } - return v; + return Ok(v); } -std::optional RsdHandshake::uuid(FfiError& err) const { - char* c = nullptr; - if (IdeviceFfiError* e = rsd_get_uuid(handle_.get(), &c)) { - err = FfiError(e); - return std::nullopt; +Result RsdHandshake::uuid() const { + char* c = nullptr; + FfiError e(rsd_get_uuid(handle_.get(), &c)); + if (e) { + return Err(e); } std::string out; if (c) { out = c; rsd_free_string(c); } - return out; + return Ok(out); } -std::optional> RsdHandshake::services(FfiError& err) const { +Result, FfiError> RsdHandshake::services() const { CRsdServiceArray* arr = nullptr; - if (IdeviceFfiError* e = rsd_get_services(handle_.get(), &arr)) { - err = FfiError(e); - return std::nullopt; + FfiError e(rsd_get_services(handle_.get(), &arr)); + if (e) { + return Err(e); } - return to_cpp_and_free(arr); + return Ok(to_cpp_and_free(arr)); } -std::optional RsdHandshake::service_available(const std::string& name, FfiError& err) const { - bool avail = false; - if (IdeviceFfiError* e = rsd_service_available(handle_.get(), name.c_str(), &avail)) { - err = FfiError(e); - return std::nullopt; +Result RsdHandshake::service_available(const std::string& name) const { + bool avail = false; + FfiError e(rsd_service_available(handle_.get(), name.c_str(), &avail)); + if (e) { + return Err(e); } - return avail; + return Ok(avail); } -std::optional RsdHandshake::service_info(const std::string& name, FfiError& err) const { +Result RsdHandshake::service_info(const std::string& name) const { CRsdService* svc = nullptr; - if (IdeviceFfiError* e = rsd_get_service_info(handle_.get(), name.c_str(), &svc)) { - err = FfiError(e); - return std::nullopt; + FfiError e(rsd_get_service_info(handle_.get(), name.c_str(), &svc)); + if (e) { + return Err(e); } - return to_cpp_and_free(svc); + return Ok(to_cpp_and_free(svc)); } } // namespace IdeviceFFI diff --git a/cpp/src/tcp_callback_feeder.cpp b/cpp/src/tcp_callback_feeder.cpp index 9967554..07c2e6b 100644 --- a/cpp/src/tcp_callback_feeder.cpp +++ b/cpp/src/tcp_callback_feeder.cpp @@ -28,16 +28,16 @@ bool TcpObjectStackEater::read(OwnedBuffer& out, FfiError& err) const { } // ---------- TcpStackFromCallback ---------- -std::optional -TcpObjectStack::create(const std::string& our_ip, const std::string& their_ip, FfiError& err) { +Result TcpObjectStack::create(const std::string& our_ip, + const std::string& their_ip) { ::TcpFeedObject* feeder_h = nullptr; ::TcpEatObject* eater_h = nullptr; ::AdapterHandle* adapter_h = nullptr; - if (IdeviceFfiError* e = ::idevice_tcp_stack_into_sync_objects( - our_ip.c_str(), their_ip.c_str(), &feeder_h, &eater_h, &adapter_h)) { - err = FfiError(e); - return std::nullopt; + FfiError e(::idevice_tcp_stack_into_sync_objects( + our_ip.c_str(), their_ip.c_str(), &feeder_h, &eater_h, &adapter_h)); + if (e) { + return Err(e); } auto impl = std::make_unique(); @@ -47,7 +47,7 @@ TcpObjectStack::create(const std::string& our_ip, const std::string& their_ip, F TcpObjectStack out; out.impl_ = std::move(impl); - return out; + return Ok(std::move(out)); } TcpObjectStackFeeder& TcpObjectStack::feeder() { @@ -65,49 +65,54 @@ const TcpObjectStackEater& TcpObjectStack::eater() const { } Adapter& TcpObjectStack::adapter() { - if (!impl_ || !impl_->adapter) { + if (!impl_ || impl_->adapter.is_some()) { static Adapter* never = nullptr; return *never; } - return *(impl_->adapter); + return (impl_->adapter.unwrap()); } const Adapter& TcpObjectStack::adapter() const { - if (!impl_ || !impl_->adapter) { + if (!impl_ || impl_->adapter.is_none()) { static Adapter* never = nullptr; return *never; } - return *(impl_->adapter); + return (impl_->adapter.unwrap()); } // ---------- Release APIs ---------- -std::optional TcpObjectStack::release_feeder() { - if (!impl_) - return std::nullopt; +Option TcpObjectStack::release_feeder() { + if (!impl_) { + return None; + } auto has = impl_->feeder.raw() != nullptr; - if (!has) - return std::nullopt; + if (!has) { + return None; + } TcpObjectStackFeeder out = std::move(impl_->feeder); // impl_->feeder is now empty (h_ == nullptr) thanks to move - return std::optional(std::move(out)); + return Some(std::move(out)); } -std::optional TcpObjectStack::release_eater() { - if (!impl_) - return std::nullopt; +Option TcpObjectStack::release_eater() { + if (!impl_) { + return None; + } auto has = impl_->eater.raw() != nullptr; - if (!has) - return std::nullopt; + if (!has) { + return None; + } TcpObjectStackEater out = std::move(impl_->eater); - return std::optional(std::move(out)); + return Some(std::move(out)); } -std::optional TcpObjectStack::release_adapter() { - if (!impl_ || !impl_->adapter) - return std::nullopt; +Option TcpObjectStack::release_adapter() { + if (!impl_ || impl_->adapter.is_none()) { + return None; + } // Move out and clear our optional - auto out = std::move(*(impl_->adapter)); + auto out = std::move((impl_->adapter.unwrap())); impl_->adapter.reset(); - return std::optional(std::move(out)); + return Some(std::move(out)); } } // namespace IdeviceFFI diff --git a/cpp/src/usbmuxd.cpp b/cpp/src/usbmuxd.cpp index 1ae2bba..5c4fb04 100644 --- a/cpp/src/usbmuxd.cpp +++ b/cpp/src/usbmuxd.cpp @@ -6,24 +6,23 @@ namespace IdeviceFFI { // ---------- UsbmuxdAddr ---------- -std::optional -UsbmuxdAddr::tcp_new(const sockaddr* addr, socklen_t addr_len, FfiError& err) { +Result UsbmuxdAddr::tcp_new(const sockaddr* addr, socklen_t addr_len) { UsbmuxdAddrHandle* h = nullptr; - if (IdeviceFfiError* e = idevice_usbmuxd_tcp_addr_new(addr, addr_len, &h)) { - err = FfiError(e); - return std::nullopt; + FfiError e(idevice_usbmuxd_tcp_addr_new(addr, addr_len, &h)); + if (e) { + return Err(e); } - return UsbmuxdAddr(h); + return Ok(UsbmuxdAddr(h)); } #if defined(__unix__) || defined(__APPLE__) -std::optional UsbmuxdAddr::unix_new(const std::string& path, FfiError& err) { +Result UsbmuxdAddr::unix_new(const std::string& path) { UsbmuxdAddrHandle* h = nullptr; - if (IdeviceFfiError* e = idevice_usbmuxd_unix_addr_new(path.c_str(), &h)) { - err = FfiError(e); - return std::nullopt; + FfiError e(idevice_usbmuxd_unix_addr_new(path.c_str(), &h)); + if (e) { + return Err(e); } - return UsbmuxdAddr(h); + return Ok(UsbmuxdAddr(h)); } #endif @@ -48,114 +47,109 @@ std::string UsbmuxdConnectionType::to_string() const { } // ---------- UsbmuxdDevice ---------- -std::optional UsbmuxdDevice::get_udid() const { +Option UsbmuxdDevice::get_udid() const { char* c = idevice_usbmuxd_device_get_udid(handle_.get()); - if (!c) - return std::nullopt; + if (!c) { + return None; + } std::string out(c); idevice_string_free(c); - return out; + return Some(out); } -std::optional UsbmuxdDevice::get_id() const { +Option UsbmuxdDevice::get_id() const { uint32_t id = idevice_usbmuxd_device_get_device_id(handle_.get()); - if (id == 0) - return std::nullopt; // adjust if 0 can be valid - return id; + if (id == 0) { + return None; + } + return Some(id); } -std::optional UsbmuxdDevice::get_connection_type() const { +Option UsbmuxdDevice::get_connection_type() const { uint8_t t = idevice_usbmuxd_device_get_connection_type(handle_.get()); - if (t == 0) - return std::nullopt; - return UsbmuxdConnectionType(t); + if (t == 0) { + return None; + } + return Some(UsbmuxdConnectionType(t)); } // ---------- UsbmuxdConnection ---------- -std::optional UsbmuxdConnection::tcp_new(const idevice_sockaddr* addr, - idevice_socklen_t addr_len, - uint32_t tag, - FfiError& err) { +Result +UsbmuxdConnection::tcp_new(const idevice_sockaddr* addr, idevice_socklen_t addr_len, uint32_t tag) { UsbmuxdConnectionHandle* h = nullptr; - if (IdeviceFfiError* e = idevice_usbmuxd_new_tcp_connection(addr, addr_len, tag, &h)) { - err = FfiError(e); - return std::nullopt; + FfiError e(idevice_usbmuxd_new_tcp_connection(addr, addr_len, tag, &h)); + if (e) { + return Err(e); } - return UsbmuxdConnection(h); + return Ok(UsbmuxdConnection(h)); } #if defined(__unix__) || defined(__APPLE__) -std::optional -UsbmuxdConnection::unix_new(const std::string& path, uint32_t tag, FfiError& err) { +Result UsbmuxdConnection::unix_new(const std::string& path, + uint32_t tag) { UsbmuxdConnectionHandle* h = nullptr; - if (IdeviceFfiError* e = idevice_usbmuxd_new_unix_socket_connection(path.c_str(), tag, &h)) { - err = FfiError(e); - return std::nullopt; + FfiError e(idevice_usbmuxd_new_unix_socket_connection(path.c_str(), tag, &h)); + if (e) { + return Err(e); } - return UsbmuxdConnection(h); + return Ok(UsbmuxdConnection(h)); } #endif -std::optional UsbmuxdConnection::default_new(uint32_t tag, FfiError& err) { +Result UsbmuxdConnection::default_new(uint32_t tag) { UsbmuxdConnectionHandle* h = nullptr; - if (IdeviceFfiError* e = idevice_usbmuxd_new_default_connection(tag, &h)) { - err = FfiError(e); - return std::nullopt; + FfiError e(idevice_usbmuxd_new_default_connection(tag, &h)); + if (e) { + return Err(e); } - return UsbmuxdConnection(h); + return Ok(UsbmuxdConnection(h)); } -std::optional> UsbmuxdConnection::get_devices(FfiError& err) const { +Result, FfiError> UsbmuxdConnection::get_devices() const { UsbmuxdDeviceHandle** list = nullptr; int count = 0; - if (IdeviceFfiError* e = idevice_usbmuxd_get_devices(handle_.get(), &list, &count)) { - err = FfiError(e); - return std::nullopt; + FfiError e(idevice_usbmuxd_get_devices(handle_.get(), &list, &count)); + if (e) { + return Err(e); } std::vector out; out.reserve(count); for (int i = 0; i < count; ++i) { out.emplace_back(UsbmuxdDevice::adopt(list[i])); } - return out; + return Ok(std::move(out)); } -std::optional UsbmuxdConnection::get_buid(FfiError& err) const { - char* c = nullptr; - if (IdeviceFfiError* e = idevice_usbmuxd_get_buid(handle_.get(), &c)) { - err = FfiError(e); - return std::nullopt; +Result UsbmuxdConnection::get_buid() const { + char* c = nullptr; + FfiError e(idevice_usbmuxd_get_buid(handle_.get(), &c)); + if (e) { + return Err(e); } std::string out(c); idevice_string_free(c); - return out; + return Ok(out); } -std::optional UsbmuxdConnection::get_pair_record(const std::string& udid, - FfiError& err) { +Result UsbmuxdConnection::get_pair_record(const std::string& udid) { IdevicePairingFile* pf = nullptr; - if (IdeviceFfiError* e = idevice_usbmuxd_get_pair_record(handle_.get(), udid.c_str(), &pf)) { - err = FfiError(e); - return std::nullopt; + FfiError e(idevice_usbmuxd_get_pair_record(handle_.get(), udid.c_str(), &pf)); + if (e) { + return Err(e); } - return PairingFile(pf); + return Ok(PairingFile(pf)); } -std::optional UsbmuxdConnection::connect_to_device(uint32_t device_id, - uint16_t port, - const std::string& path, - FfiError& err) && { +Result UsbmuxdConnection::connect_to_device(uint32_t device_id, + uint16_t port, + const std::string& path) && { UsbmuxdConnectionHandle* raw = handle_.release(); - IdeviceHandle* out = nullptr; - IdeviceFfiError* e = - idevice_usbmuxd_connect_to_device(raw, device_id, port, path.c_str(), &out); - + FfiError e(idevice_usbmuxd_connect_to_device(raw, device_id, port, path.c_str(), &out)); if (e) { - err = FfiError(e); - return std::nullopt; + return Err(e); } - return Idevice::adopt(out); + return Ok(Idevice::adopt(out)); } } // namespace IdeviceFFI From 44b504c72e0e9b538579374e1cf7f7befd854fe8 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Fri, 29 Aug 2025 14:33:28 -0600 Subject: [PATCH 115/126] Increase cpp standard to 14 --- cpp/examples/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/examples/CMakeLists.txt b/cpp/examples/CMakeLists.txt index bce540a..40e3dfb 100644 --- a/cpp/examples/CMakeLists.txt +++ b/cpp/examples/CMakeLists.txt @@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.15) project(IdeviceFFI CXX) -set(CMAKE_CXX_STANDARD 11) +set(CMAKE_CXX_STANDARD 14) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) From c2ef847c0ae9cb9fd752922ffa1a9c89fbd0cf58 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Fri, 29 Aug 2025 14:39:33 -0600 Subject: [PATCH 116/126] Decrease visual studio to cpp14 --- cpp/sln/idevice++.vcxproj | 14 ++++++++------ cpp/sln/idevice++.vcxproj.filters | 9 +++++++++ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/cpp/sln/idevice++.vcxproj b/cpp/sln/idevice++.vcxproj index a3fe149..93f0f54 100644 --- a/cpp/sln/idevice++.vcxproj +++ b/cpp/sln/idevice++.vcxproj @@ -54,10 +54,12 @@ + + @@ -177,7 +179,7 @@ $(ProjectDir)..\include;$(ProjectDir)..\..\ffi - stdcpp20 + Default @@ -208,7 +210,7 @@ $(ProjectDir)..\include;$(ProjectDir)..\..\ffi - stdcpp20 + Default @@ -237,7 +239,7 @@ $(ProjectDir)..\include;$(ProjectDir)..\..\ffi - stdcpp20 + Default @@ -268,7 +270,7 @@ $(ProjectDir)..\include;$(ProjectDir)..\..\ffi - stdcpp20 + Default @@ -297,7 +299,7 @@ $(ProjectDir)..\include;$(ProjectDir)..\..\ffi - stdcpp20 + Default @@ -328,7 +330,7 @@ $(ProjectDir)..\include;$(ProjectDir)..\..\ffi - stdcpp20 + Default diff --git a/cpp/sln/idevice++.vcxproj.filters b/cpp/sln/idevice++.vcxproj.filters index 2f6b401..e5a301e 100644 --- a/cpp/sln/idevice++.vcxproj.filters +++ b/cpp/sln/idevice++.vcxproj.filters @@ -116,5 +116,14 @@ Header Files\idevice++ + + Header Files + + + Header Files\idevice++ + + + Header Files\idevice++ + \ No newline at end of file From d463a53f0408190ccbfc37f97de582bf777a4991 Mon Sep 17 00:00:00 2001 From: Ylarod Date: Sat, 30 Aug 2025 05:19:25 +0800 Subject: [PATCH 117/126] feat: support launch app at ios17- (#23) * feat: support launch app at ios17- * cargo fmt * clippy --- idevice/src/services/dvt/mod.rs | 66 ++++++++++++++++++++++++++++++- tools/src/process_control.rs | 70 ++++++++++++++++++++++++++------- 2 files changed, 121 insertions(+), 15 deletions(-) diff --git a/idevice/src/services/dvt/mod.rs b/idevice/src/services/dvt/mod.rs index 90a3bab..d7db667 100644 --- a/idevice/src/services/dvt/mod.rs +++ b/idevice/src/services/dvt/mod.rs @@ -1,6 +1,8 @@ // Jackson Coxson -use crate::{IdeviceError, ReadWrite, RsdService, obf}; +use crate::provider::IdeviceProvider; +use crate::services::lockdown::LockdownClient; +use crate::{Idevice, IdeviceError, ReadWrite, RsdService, obf}; #[cfg(feature = "location_simulation")] pub mod location_simulation; @@ -17,3 +19,65 @@ impl RsdService for remote_server::RemoteServerClient> { Ok(Self::new(stream)) } } + +// iOS version support notes: +// - com.apple.instruments.dtservicehub (RSD/XPC over HTTP2) is used on iOS 17+. +// - com.apple.instruments.remoteserver is available on pre-iOS 17 (and many older versions). +// - com.apple.instruments.remoteserver.DVTSecureSocketProxy is used by some iOS 14 builds. +// +// This impl enables Lockdown-based connection to Instruments Remote Server for iOS < 17 +// by reusing the same RemoteServerClient but sourcing the transport from StartService. +impl crate::IdeviceService for remote_server::RemoteServerClient> { + fn service_name() -> std::borrow::Cow<'static, str> { + // Primary name for Instruments Remote Server + obf!("com.apple.instruments.remoteserver") + } + + #[allow(async_fn_in_trait)] + async fn connect(provider: &dyn IdeviceProvider) -> Result { + // Establish Lockdown session + let mut lockdown = LockdownClient::connect(provider).await?; + lockdown + .start_session(&provider.get_pairing_file().await?) + .await?; + + // Try main Instruments service first, then DVTSecureSocketProxy (seen on iOS 14) + let try_names = [ + obf!("com.apple.instruments.remoteserver"), + obf!("com.apple.instruments.remoteserver.DVTSecureSocketProxy"), + ]; + + let mut last_err: Option = None; + for name in try_names { + match lockdown.start_service(name).await { + Ok((port, ssl)) => { + let mut idevice = provider.connect(port).await?; + if ssl { + idevice + .start_session(&provider.get_pairing_file().await?) + .await?; + } + // Convert to transport and build client + let socket = idevice + .get_socket() + .ok_or(IdeviceError::NoEstablishedConnection)?; + return Ok(remote_server::RemoteServerClient::new(socket)); + } + Err(e) => { + last_err = Some(e); + } + } + } + + Err(last_err.unwrap_or(IdeviceError::ServiceNotFound)) + } + + #[allow(async_fn_in_trait)] + async fn from_stream(idevice: Idevice) -> Result { + // Not used in our overridden connect path, but implemented for completeness + let socket = idevice + .get_socket() + .ok_or(IdeviceError::NoEstablishedConnection)?; + Ok(remote_server::RemoteServerClient::new(socket)) + } +} diff --git a/tools/src/process_control.rs b/tools/src/process_control.rs index e74b4e9..eebe59f 100644 --- a/tools/src/process_control.rs +++ b/tools/src/process_control.rs @@ -1,6 +1,7 @@ // Jackson Coxson use clap::{Arg, Command}; +use idevice::services::lockdown::LockdownClient; use idevice::{IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake}; mod common; @@ -71,29 +72,70 @@ async fn main() { } }; - let proxy = CoreDeviceProxy::connect(&*provider) + let mut rs_client_opt: Option< + idevice::dvt::remote_server::RemoteServerClient>, + > = None; + + if let Ok(proxy) = CoreDeviceProxy::connect(&*provider).await { + let rsd_port = proxy.handshake.server_rsd_port; + let adapter = proxy.create_software_tunnel().expect("no software tunnel"); + let mut adapter = adapter.to_async_handle(); + let stream = adapter.connect(rsd_port).await.expect("no RSD connect"); + + // Make the connection to RemoteXPC (iOS 17+) + let mut handshake = RsdHandshake::new(stream).await.unwrap(); + let mut rs_client = idevice::dvt::remote_server::RemoteServerClient::connect_rsd( + &mut adapter, + &mut handshake, + ) .await - .expect("no core proxy"); - let rsd_port = proxy.handshake.server_rsd_port; + .expect("no connect"); + rs_client.read_message(0).await.expect("no read??"); + rs_client_opt = Some(rs_client); + } - let adapter = proxy.create_software_tunnel().expect("no software tunnel"); - let mut adapter = adapter.to_async_handle(); - let stream = adapter.connect(rsd_port).await.expect("no RSD connect"); - - // Make the connection to RemoteXPC - let mut handshake = RsdHandshake::new(stream).await.unwrap(); - - let mut rs_client = - idevice::dvt::remote_server::RemoteServerClient::connect_rsd(&mut adapter, &mut handshake) + let mut rs_client = if let Some(c) = rs_client_opt { + c + } else { + // Read iOS version to decide whether we can fallback to remoteserver + let mut lockdown = LockdownClient::connect(&*provider) .await - .expect("no connect"); + .expect("lockdown connect failed"); + lockdown + .start_session(&provider.get_pairing_file().await.expect("pairing file")) + .await + .expect("lockdown start_session failed"); + let pv = lockdown + .get_value(Some("ProductVersion"), None) + .await + .ok() + .and_then(|v| v.as_string().map(|s| s.to_string())) + .unwrap_or_default(); + let major: u32 = pv + .split('.') + .next() + .and_then(|s| s.parse().ok()) + .unwrap_or(0); + + if major >= 17 { + // iOS 17+ with no CoreDeviceProxy: do not attempt remoteserver (would return InvalidService) + panic!("iOS {pv} detected and CoreDeviceProxy unavailable. RemoteXPC tunnel required."); + } + + // iOS 16 and earlier: fallback to Lockdown remoteserver (or DVTSecureSocketProxy) + idevice::dvt::remote_server::RemoteServerClient::connect(&*provider) + .await + .expect("failed to connect to Instruments Remote Server over Lockdown (iOS16-). Ensure Developer Disk Image is mounted.") + }; + + // Note: On both transports, protocol requires reading the initial message on root channel (0) rs_client.read_message(0).await.expect("no read??"); let mut pc_client = idevice::dvt::process_control::ProcessControlClient::new(&mut rs_client) .await .unwrap(); let pid = pc_client - .launch_app(bundle_id, None, None, true, false) + .launch_app(bundle_id, None, None, false, false) .await .expect("no launch??"); pc_client From ca56575c6ce52a71f738da44538e17ec5d585f10 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Wed, 3 Sep 2025 19:26:11 -0600 Subject: [PATCH 118/126] Implement SavePairRecord --- idevice/src/usbmuxd/mod.rs | 26 ++++++++++++++++++++++++++ tools/src/pair.rs | 13 ++++++++----- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/idevice/src/usbmuxd/mod.rs b/idevice/src/usbmuxd/mod.rs index d1eebad..9953feb 100644 --- a/idevice/src/usbmuxd/mod.rs +++ b/idevice/src/usbmuxd/mod.rs @@ -335,6 +335,32 @@ impl UsbmuxdConnection { } } + /// Tells usbmuxd to save the pairing record in its storage + /// + /// # Arguments + /// * `device_id` - usbmuxd device ID + /// * `udid` - the device UDID/serial + /// * `pair_record` - a serialized plist of the pair record + pub async fn save_pair_record( + &mut self, + device_id: u32, + udid: &str, + pair_record: Vec, + ) -> Result<(), IdeviceError> { + let req = crate::plist!(dict { + "MessageType": "SavePairRecord", + "PairRecordData": pair_record, + "DeviceID": device_id, + "PairRecordID": udid, + }); + self.write_plist(req).await?; + let res = self.read_plist().await?; + match res.get("Number").and_then(|x| x.as_unsigned_integer()) { + Some(0) => Ok(()), + _ => Err(IdeviceError::UnexpectedResponse), + } + } + /// Writes a PLIST message to usbmuxd async fn write_plist(&mut self, req: plist::Dictionary) -> Result<(), IdeviceError> { let raw = raw_packet::RawPacket::new( diff --git a/tools/src/pair.rs b/tools/src/pair.rs index baf3315..3425e1f 100644 --- a/tools/src/pair.rs +++ b/tools/src/pair.rs @@ -74,10 +74,13 @@ async fn main() { .expect("Pairing file test failed"); // Add the UDID (jitterbug spec) - pairing_file.udid = Some(dev.udid); + pairing_file.udid = Some(dev.udid.clone()); + let pairing_file = pairing_file.serialize().expect("failed to serialize"); - println!( - "{}", - String::from_utf8(pairing_file.serialize().unwrap()).unwrap() - ); + println!("{}", String::from_utf8(pairing_file.clone()).unwrap()); + + // Save with usbmuxd + u.save_pair_record(dev.device_id, &dev.udid, pairing_file) + .await + .expect("no save"); } From b0e3c5769a74f2d94fb644605247d4e9d186f486 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Wed, 3 Sep 2025 19:48:43 -0600 Subject: [PATCH 119/126] Remove dangerous result/option macros --- cpp/examples/app_service.cpp | 35 ++++----- cpp/examples/debug_proxy.cpp | 46 +++++------ cpp/examples/diagnosticsservice.cpp | 60 +++++++------- cpp/examples/idevice_id.cpp | 15 +--- cpp/examples/ideviceinfo.cpp | 43 +++------- cpp/examples/location_simulation.cpp | 54 ++++++------- cpp/include/idevice++/option.hpp | 50 ++++++------ cpp/include/idevice++/result.hpp | 113 ++++++++++++++++++++++----- 8 files changed, 215 insertions(+), 201 deletions(-) diff --git a/cpp/examples/app_service.cpp b/cpp/examples/app_service.cpp index c9cf136..730c889 100644 --- a/cpp/examples/app_service.cpp +++ b/cpp/examples/app_service.cpp @@ -44,12 +44,9 @@ int main(int argc, char** argv) { FfiError err; // 1) Connect to usbmuxd and pick first device - auto mux = UsbmuxdConnection::default_new(/*tag*/ 0); - if_let_err(mux, err, { die("failed to connect to usbmuxd", err); }); + auto mux = UsbmuxdConnection::default_new(/*tag*/ 0).expect("failed to connect to usbmuxd"); - auto devices_res = mux.unwrap().get_devices(); - if_let_err(devices_res, err, { die("failed to list devices", err); }); - auto& devices = devices_res.unwrap(); + auto devices = mux.get_devices().expect("failed to list devices"); if (devices.empty()) { std::cerr << "no devices connected\n"; return 1; @@ -73,35 +70,31 @@ int main(int argc, char** argv) { const uint32_t tag = 0; const std::string label = "app_service-jkcoxson"; - auto provider_res = - Provider::usbmuxd_new(std::move(addr), tag, udid.unwrap(), mux_id.unwrap(), label); - if_let_err(provider_res, err, { die("failed to create provider", err); }); - auto& provider = provider_res.unwrap(); + auto provider = + Provider::usbmuxd_new(std::move(addr), tag, udid.unwrap(), mux_id.unwrap(), label) + .expect("failed to create provider"); // 3) CoreDeviceProxy - auto cdp = CoreDeviceProxy::connect(provider).unwrap_or_else( + auto cdp = CoreDeviceProxy::connect(provider).unwrap_or_else( [](FfiError e) -> CoreDeviceProxy { die("failed to connect CoreDeviceProxy", e); }); auto rsd_port = cdp.get_server_rsd_port().unwrap_or_else( [](FfiError err) -> uint16_t { die("failed to get server RSD port", err); }); // 4) Create software tunnel adapter (consumes proxy) - auto adapter = std::move(cdp).create_tcp_adapter(); - if_let_err(adapter, err, { die("failed to create software tunnel adapter", err); }); + auto adapter = + std::move(cdp).create_tcp_adapter().expect("failed to create software tunnel adapter"); // 5) Connect adapter to RSD → ReadWrite stream - auto stream = adapter.unwrap().connect(rsd_port); - if_let_err(stream, err, { die("failed to connect RSD stream", err); }); + auto stream = adapter.connect(rsd_port).expect("failed to connect RSD stream"); // 6) RSD handshake (consumes stream) - auto rsd = RsdHandshake::from_socket(std::move(stream.unwrap())); - if_let_err(rsd, err, { die("failed RSD handshake", err); }); + auto rsd = RsdHandshake::from_socket(std::move(stream)).expect("failed RSD handshake"); // 7) AppService over RSD (borrows adapter + handshake) - auto app = AppService::connect_rsd(adapter.unwrap(), rsd.unwrap()) - .unwrap_or_else([&](FfiError e) -> AppService { - die("failed to connect AppService", e); // never returns - }); + auto app = AppService::connect_rsd(adapter, rsd).unwrap_or_else([&](FfiError e) -> AppService { + die("failed to connect AppService", e); // never returns + }); // 8) Commands if (cmd == "list") { @@ -160,7 +153,7 @@ int main(int argc, char** argv) { } std::string bundle_id = argv[2]; - if_let_err(app.uninstall(bundle_id), err, { die("uninstall failed", err); }); + app.uninstall(bundle_id).expect("Uninstall failed"); std::cout << "Uninstalled " << bundle_id << "\n"; return 0; diff --git a/cpp/examples/debug_proxy.cpp b/cpp/examples/debug_proxy.cpp index 182b85d..a451cf8 100644 --- a/cpp/examples/debug_proxy.cpp +++ b/cpp/examples/debug_proxy.cpp @@ -34,18 +34,16 @@ static std::vector split_args(const std::string& line) { int main() { IdeviceFFI::FfiError err; - // 1) usbmuxd → pick first device - auto mux = IdeviceFFI::UsbmuxdConnection::default_new(/*tag*/ 0); - if_let_err(mux, err, { die("failed to connect to usbmuxd", err); }); + // 1) Connect to usbmuxd and pick first device + auto mux = UsbmuxdConnection::default_new(/*tag*/ 0).expect("failed to connect to usbmuxd"); - auto devices = mux.unwrap().get_devices(); - if_let_err(devices, err, { die("failed to list devices", err); }); - if (devices.unwrap().empty()) { + auto devices = mux.get_devices().expect("failed to list devices"); + if (devices.empty()) { std::cerr << "no devices connected\n"; return 1; } + auto& dev = (devices)[0]; - auto& dev = (devices.unwrap())[0]; auto udid = dev.get_udid(); if (udid.is_none()) { std::cerr << "device has no UDID\n"; @@ -58,39 +56,35 @@ int main() { } // 2) Provider via default usbmuxd addr - auto addr = IdeviceFFI::UsbmuxdAddr::default_new(); + auto addr = UsbmuxdAddr::default_new(); - const uint32_t tag = 0; - const std::string label = "debug-proxy-jkcoxson"; - auto provider = IdeviceFFI::Provider::usbmuxd_new( - std::move(addr), tag, udid.unwrap(), mux_id.unwrap(), label); - if_let_err(provider, err, { die("failed to create provider", err); }); + const uint32_t tag = 0; + const std::string label = "app_service-jkcoxson"; + + auto provider = + Provider::usbmuxd_new(std::move(addr), tag, udid.unwrap(), mux_id.unwrap(), label) + .expect("failed to create provider"); // 3) CoreDeviceProxy - auto cdp = CoreDeviceProxy::connect(provider.unwrap()) - .unwrap_or_else([](FfiError e) -> CoreDeviceProxy { - die("failed to connect CoreDeviceProxy", e); - }); + auto cdp = CoreDeviceProxy::connect(provider).unwrap_or_else( + [](FfiError e) -> CoreDeviceProxy { die("failed to connect CoreDeviceProxy", e); }); auto rsd_port = cdp.get_server_rsd_port().unwrap_or_else( [](FfiError err) -> uint16_t { die("failed to get server RSD port", err); }); // 4) Create software tunnel adapter (consumes proxy) - auto adapter = std::move(cdp).create_tcp_adapter(); - if_let_err(adapter, err, { die("failed to create software tunnel adapter", err); }); + auto adapter = + std::move(cdp).create_tcp_adapter().expect("failed to create software tunnel adapter"); // 5) Connect adapter to RSD → ReadWrite stream - auto stream = adapter.unwrap().connect(rsd_port); - if_let_err(stream, err, { die("failed to connect RSD stream", err); }); + auto stream = adapter.connect(rsd_port).expect("failed to connect RSD stream"); // 6) RSD handshake (consumes stream) - auto rsd = RsdHandshake::from_socket(std::move(stream.unwrap())); - if_let_err(rsd, err, { die("failed RSD handshake", err); }); + auto rsd = RsdHandshake::from_socket(std::move(stream)).expect("failed RSD handshake"); // 6) DebugProxy over RSD - auto dbg_res = IdeviceFFI::DebugProxy::connect_rsd(adapter.unwrap(), rsd.unwrap()); - if_let_err(dbg_res, err, { die("failed to connect DebugProxy", err); }); - auto& dbg = dbg_res.unwrap(); + auto dbg = + IdeviceFFI::DebugProxy::connect_rsd(adapter, rsd).expect("failed to connect DebugProxy"); std::cout << "Shell connected! Type 'exit' to quit.\n"; for (;;) { diff --git a/cpp/examples/diagnosticsservice.cpp b/cpp/examples/diagnosticsservice.cpp index bcf2eb0..ffde043 100644 --- a/cpp/examples/diagnosticsservice.cpp +++ b/cpp/examples/diagnosticsservice.cpp @@ -29,18 +29,16 @@ int main() { idevice_init_logger(Debug, Disabled, NULL); FfiError err; - // 1) usbmuxd → pick first device - auto mux = IdeviceFFI::UsbmuxdConnection::default_new(/*tag*/ 0); - if_let_err(mux, err, { die("failed to connect to usbmuxd", err); }); + // 1) Connect to usbmuxd and pick first device + auto mux = UsbmuxdConnection::default_new(/*tag*/ 0).expect("failed to connect to usbmuxd"); - auto devices = mux.unwrap().get_devices(); - if_let_err(devices, err, { die("failed to list devices", err); }); - if (devices.unwrap().empty()) { + auto devices = mux.get_devices().expect("failed to list devices"); + if (devices.empty()) { std::cerr << "no devices connected\n"; return 1; } + auto& dev = (devices)[0]; - auto& dev = (devices.unwrap())[0]; auto udid = dev.get_udid(); if (udid.is_none()) { std::cerr << "device has no UDID\n"; @@ -53,58 +51,54 @@ int main() { } // 2) Provider via default usbmuxd addr - auto addr = IdeviceFFI::UsbmuxdAddr::default_new(); + auto addr = UsbmuxdAddr::default_new(); - const uint32_t tag = 0; - const std::string label = "debug-proxy-jkcoxson"; - auto provider = IdeviceFFI::Provider::usbmuxd_new( - std::move(addr), tag, udid.unwrap(), mux_id.unwrap(), label); - if_let_err(provider, err, { die("failed to create provider", err); }); + const uint32_t tag = 0; + const std::string label = "app_service-jkcoxson"; + + auto provider = + Provider::usbmuxd_new(std::move(addr), tag, udid.unwrap(), mux_id.unwrap(), label) + .expect("failed to create provider"); // 3) CoreDeviceProxy - auto cdp = CoreDeviceProxy::connect(provider.unwrap()) - .unwrap_or_else([](FfiError e) -> CoreDeviceProxy { - die("failed to connect CoreDeviceProxy", e); - }); + auto cdp = CoreDeviceProxy::connect(provider).unwrap_or_else( + [](FfiError e) -> CoreDeviceProxy { die("failed to connect CoreDeviceProxy", e); }); auto rsd_port = cdp.get_server_rsd_port().unwrap_or_else( [](FfiError err) -> uint16_t { die("failed to get server RSD port", err); }); // 4) Create software tunnel adapter (consumes proxy) - auto adapter = std::move(cdp).create_tcp_adapter(); - if_let_err(adapter, err, { die("failed to create software tunnel adapter", err); }); + auto adapter = + std::move(cdp).create_tcp_adapter().expect("failed to create software tunnel adapter"); // 5) Connect adapter to RSD → ReadWrite stream - auto stream = adapter.unwrap().connect(rsd_port); - if_let_err(stream, err, { die("failed to connect RSD stream", err); }); + auto stream = adapter.connect(rsd_port).expect("failed to connect RSD stream"); // 6) RSD handshake (consumes stream) - auto rsd = RsdHandshake::from_socket(std::move(stream.unwrap())); - if_let_err(rsd, err, { die("failed RSD handshake", err); }); + auto rsd = RsdHandshake::from_socket(std::move(stream)).expect("failed RSD handshake"); - // 6) DebugProxy over RSD - auto diag = DiagnosticsService::connect_rsd(adapter.unwrap(), rsd.unwrap()); - if_let_err(diag, err, { die("failed to connect DebugProxy", err); }); + // 7) DebugProxy over RSD + auto diag = + DiagnosticsService::connect_rsd(adapter, rsd).expect("failed to connect DebugProxy"); std::cout << "Getting sysdiagnose, this takes a while! iOS is slow...\n"; - auto cap = diag.unwrap().capture_sysdiagnose(/*dry_run=*/false); - if_let_err(cap, err, { die("capture_sysdiagnose failed", err); }); + auto cap = diag.capture_sysdiagnose(/*dry_run=*/false).expect("capture_sysdiagnose failed"); - std::cout << "Got sysdiagnose! Saving to file: " << cap.unwrap().preferred_filename << "\n"; + std::cout << "Got sysdiagnose! Saving to file: " << cap.preferred_filename << "\n"; // 7) Stream to file with progress - std::ofstream out(cap.unwrap().preferred_filename, std::ios::binary); + std::ofstream out(cap.preferred_filename, std::ios::binary); if (!out) { std::cerr << "failed to open output file\n"; return 1; } std::size_t written = 0; - const std::size_t total = cap.unwrap().expected_length; + const std::size_t total = cap.expected_length; for (;;) { - auto chunk = cap.unwrap().stream.next_chunk(); + auto chunk = cap.stream.next_chunk(); match_result( chunk, res, @@ -128,6 +122,6 @@ int main() { } out.flush(); - std::cout << "\nDone! Saved to " << cap.unwrap().preferred_filename << "\n"; + std::cout << "\nDone! Saved to " << cap.preferred_filename << "\n"; return 0; } diff --git a/cpp/examples/idevice_id.cpp b/cpp/examples/idevice_id.cpp index c99f254..79bcd5e 100644 --- a/cpp/examples/idevice_id.cpp +++ b/cpp/examples/idevice_id.cpp @@ -4,19 +4,10 @@ #include int main() { - auto u = IdeviceFFI::UsbmuxdConnection::default_new(0); - if_let_err(u, e, { - std::cerr << "failed to connect to usbmuxd"; - std::cerr << e.message; - }); + auto u = IdeviceFFI::UsbmuxdConnection::default_new(0).expect("failed to connect to usbmuxd"); + auto devices = u.get_devices().expect("failed to get devices from usbmuxd"); - auto devices = u.unwrap().get_devices(); - if_let_err(devices, e, { - std::cerr << "failed to get devices from usbmuxd"; - std::cerr << e.message; - }); - - for (IdeviceFFI::UsbmuxdDevice& d : devices.unwrap()) { + for (IdeviceFFI::UsbmuxdDevice& d : devices) { auto udid = d.get_udid(); if (udid.is_none()) { std::cerr << "failed to get udid"; diff --git a/cpp/examples/ideviceinfo.cpp b/cpp/examples/ideviceinfo.cpp index d93756d..9642db0 100644 --- a/cpp/examples/ideviceinfo.cpp +++ b/cpp/examples/ideviceinfo.cpp @@ -9,21 +9,9 @@ int main() { idevice_init_logger(Debug, Disabled, NULL); - auto u_res = IdeviceFFI::UsbmuxdConnection::default_new(0); - if_let_err(u_res, e, { - std::cerr << "failed to connect to usbmuxd"; - std::cerr << e.message; - return 1; - }); - auto& u = u_res.unwrap(); + auto u = IdeviceFFI::UsbmuxdConnection::default_new(0).expect("failed to connect to usbmuxd"); - auto devices_res = u.get_devices(); - if_let_err(devices_res, e, { - std::cerr << "failed to get devices from usbmuxd"; - std::cerr << e.message; - return 1; - }); - auto devices = std::move(devices_res).unwrap(); + auto devices = u.get_devices().expect("failed to get devices from usbmuxd"); if (devices.empty()) { std::cerr << "no devices connected"; @@ -44,28 +32,15 @@ int main() { return 1; } - IdeviceFFI::UsbmuxdAddr addr = IdeviceFFI::UsbmuxdAddr::default_new(); - auto prov_res = IdeviceFFI::Provider::usbmuxd_new( - std::move(addr), /*tag*/ 0, udid.unwrap(), id.unwrap(), "reeeeeeeee"); - if_let_err(prov_res, e, { - std::cerr << "provider failed: " << e.message << "\n"; - return 1; - }); - auto& prov = prov_res.unwrap(); + IdeviceFFI::UsbmuxdAddr addr = IdeviceFFI::UsbmuxdAddr::default_new(); + auto prov = IdeviceFFI::Provider::usbmuxd_new( + std::move(addr), /*tag*/ 0, udid.unwrap(), id.unwrap(), "reeeeeeeee") + .expect("Failed to create usbmuxd provider"); - auto client_res = IdeviceFFI::Lockdown::connect(prov); - if_let_err(client_res, e, { - std::cerr << "lockdown connect failed: " << e.message << "\n"; - return 1; - }); - auto& client = client_res.unwrap(); + auto client = IdeviceFFI::Lockdown::connect(prov).expect("lockdown connect failed"); - auto pf = prov.get_pairing_file(); - if_let_err(pf, e, { - std::cerr << "failed to get pairing file: " << e.message << "\n"; - return 1; - }); - client.start_session(pf.unwrap()); + auto pf = prov.get_pairing_file().expect("failed to get pairing file"); + client.start_session(pf).expect("failed to start session"); auto values = client.get_value(NULL, NULL); match_result( diff --git a/cpp/examples/location_simulation.cpp b/cpp/examples/location_simulation.cpp index 9211ae1..b458faa 100644 --- a/cpp/examples/location_simulation.cpp +++ b/cpp/examples/location_simulation.cpp @@ -41,18 +41,16 @@ int main(int argc, char** argv) { return 2; } - // 1) usbmuxd → pick first device - auto mux = IdeviceFFI::UsbmuxdConnection::default_new(/*tag*/ 0); - if_let_err(mux, err, { die("failed to connect to usbmuxd", err); }); + // 1) Connect to usbmuxd and pick first device + auto mux = UsbmuxdConnection::default_new(/*tag*/ 0).expect("failed to connect to usbmuxd"); - auto devices = mux.unwrap().get_devices(); - if_let_err(devices, err, { die("failed to list devices", err); }); - if (devices.unwrap().empty()) { + auto devices = mux.get_devices().expect("failed to list devices"); + if (devices.empty()) { std::cerr << "no devices connected\n"; return 1; } + auto& dev = (devices)[0]; - auto& dev = (devices.unwrap())[0]; auto udid = dev.get_udid(); if (udid.is_none()) { std::cerr << "device has no UDID\n"; @@ -65,58 +63,52 @@ int main(int argc, char** argv) { } // 2) Provider via default usbmuxd addr - auto addr = IdeviceFFI::UsbmuxdAddr::default_new(); + auto addr = UsbmuxdAddr::default_new(); - const uint32_t tag = 0; - const std::string label = "debug-proxy-jkcoxson"; - auto provider = IdeviceFFI::Provider::usbmuxd_new( - std::move(addr), tag, udid.unwrap(), mux_id.unwrap(), label); - if_let_err(provider, err, { die("failed to create provider", err); }); + const uint32_t tag = 0; + const std::string label = "app_service-jkcoxson"; + + auto provider = + Provider::usbmuxd_new(std::move(addr), tag, udid.unwrap(), mux_id.unwrap(), label) + .expect("failed to create provider"); // 3) CoreDeviceProxy - auto cdp = CoreDeviceProxy::connect(provider.unwrap()) - .unwrap_or_else([](FfiError e) -> CoreDeviceProxy { - die("failed to connect CoreDeviceProxy", e); - }); + auto cdp = CoreDeviceProxy::connect(provider).unwrap_or_else( + [](FfiError e) -> CoreDeviceProxy { die("failed to connect CoreDeviceProxy", e); }); auto rsd_port = cdp.get_server_rsd_port().unwrap_or_else( [](FfiError err) -> uint16_t { die("failed to get server RSD port", err); }); // 4) Create software tunnel adapter (consumes proxy) - auto adapter = std::move(cdp).create_tcp_adapter(); - if_let_err(adapter, err, { die("failed to create software tunnel adapter", err); }); + auto adapter = + std::move(cdp).create_tcp_adapter().expect("failed to create software tunnel adapter"); // 5) Connect adapter to RSD → ReadWrite stream - auto stream = adapter.unwrap().connect(rsd_port); - if_let_err(stream, err, { die("failed to connect RSD stream", err); }); + auto stream = adapter.connect(rsd_port).expect("failed to connect RSD stream"); // 6) RSD handshake (consumes stream) - auto rsd = RsdHandshake::from_socket(std::move(stream.unwrap())); - if_let_err(rsd, err, { die("failed RSD handshake", err); }); + auto rsd = RsdHandshake::from_socket(std::move(stream)).expect("failed RSD handshake"); // 8) RemoteServer over RSD (borrows adapter + handshake) - auto rs = RemoteServer::connect_rsd(adapter.unwrap(), rsd.unwrap()); - if_let_err(rs, err, { die("failed to connect RemoteServer", err); }); + auto rs = RemoteServer::connect_rsd(adapter, rsd).expect("failed to connect to RemoteServer"); // 9) LocationSimulation client (borrows RemoteServer) - auto sim_res = LocationSimulation::create(rs.unwrap()); - if_let_err(sim_res, err, { die("failed to create LocationSimulation client", err); }); - auto& sim = sim_res.unwrap(); + auto sim = LocationSimulation::create(rs).expect("failed to create LocationSimulation client"); if (do_clear) { - if_let_err(sim.clear(), err, { die("clear failed", err); }); + sim.clear().expect("clear failed"); std::cout << "Location cleared!\n"; return 0; } // set path - if_let_err(sim.set(lat.unwrap(), lon.unwrap()), err, { die("set failed", err); }); + sim.set(lat.unwrap(), lon.unwrap()).expect("set failed"); std::cout << "Location set to (" << lat.unwrap() << ", " << lon.unwrap() << ")\n"; std::cout << "Press Ctrl-C to stop\n"; // keep process alive like the Rust example for (;;) { - if_let_err(sim.set(lat.unwrap(), lon.unwrap()), err, { die("set failed", err); }); + sim.set(lat.unwrap(), lon.unwrap()).expect("set failed"); std::this_thread::sleep_for(std::chrono::seconds(3)); } } diff --git a/cpp/include/idevice++/option.hpp b/cpp/include/idevice++/option.hpp index b522b92..be25570 100644 --- a/cpp/include/idevice++/option.hpp +++ b/cpp/include/idevice++/option.hpp @@ -8,7 +8,6 @@ #pragma once -#include #include #include #include @@ -106,6 +105,8 @@ template class Option { // unwrap_or / unwrap_or_else T unwrap_or(T default_value) const& { return has_ ? *ptr() : std::move(default_value); } T unwrap_or(T default_value) && { return has_ ? std::move(*ptr()) : std::move(default_value); } + T unwrap_or(const T& default_value) const& { return has_ ? *ptr() : default_value; } + T unwrap_or(T&& default_value) const& { return has_ ? *ptr() : std::move(default_value); } template T unwrap_or_else(F&& f) const& { return has_ ? *ptr() : static_cast(f()); @@ -123,6 +124,16 @@ template class Option { } return Option(None); } + + template + auto map(F&& f) && -> Option::type> { + using U = typename std::decay::type; + if (has_) { + // Move the value into the function + return Option(f(std::move(*ptr()))); + } + return Option(None); + } }; // Helpers @@ -131,21 +142,17 @@ template inline Option::type> Some(T&& v) { } inline Option Some() = delete; // no Option -// template inline Option None() { -// return Option(none); -// } // still needs T specified - -// Prefer this at call sites (lets return-type drive the type): -// return none; - -#define match_option(opt, SOME, NONE) \ - if ((opt).is_some()) { \ - auto&& SOME = (opt).unwrap(); -#define or_else \ - } \ - else { \ - NONE; \ - } +#define match_option(opt, some_name, some_block, none_block) \ + /* NOTE: you may return in a block, but not break/continue */ \ + do { \ + auto&& _option_val = (opt); \ + if (_option_val.is_some()) { \ + auto&& some_name = _option_val.unwrap(); \ + some_block \ + } else { \ + none_block \ + } \ + } while (0) // --- Option helpers: if_let_some / if_let_some_move / if_let_none --- @@ -154,6 +161,7 @@ inline Option Some() = delete; // no Option /* Bind a reference to the contained value if Some(...) */ #define if_let_some(expr, name, block) \ + /* NOTE: you may return in a block, but not break/continue */ \ do { \ auto _opt_unique(_opt_) = (expr); \ if (_opt_unique(_opt_).is_some()) { \ @@ -164,6 +172,7 @@ inline Option Some() = delete; // no Option /* Move the contained value out (consumes the Option) if Some(...) */ #define if_let_some_move(expr, name, block) \ + /* NOTE: you may return in a block, but not break/continue */ \ do { \ auto _opt_unique(_opt_) = (expr); \ if (_opt_unique(_opt_).is_some()) { \ @@ -172,13 +181,4 @@ inline Option Some() = delete; // no Option } \ } while (0) -/* Run a block if the option is None */ -#define if_let_none(expr, block) \ - do { \ - auto _opt_unique(_opt_) = (expr); \ - if (_opt_unique(_opt_).is_none()) { \ - block \ - } \ - } while (0) - } // namespace IdeviceFFI diff --git a/cpp/include/idevice++/result.hpp b/cpp/include/idevice++/result.hpp index fd4872e..0e545bb 100644 --- a/cpp/include/idevice++/result.hpp +++ b/cpp/include/idevice++/result.hpp @@ -76,6 +76,57 @@ template class Result { } } + // Copy Assignment + Result& operator=(const Result& other) { + // Prevent self-assignment + if (this == &other) { + return *this; + } + + // Destroy the current value + if (is_ok_) { + ok_value_.~T(); + } else { + err_value_.~E(); + } + + is_ok_ = other.is_ok_; + + // Construct the new value + if (is_ok_) { + new (&ok_value_) T(other.ok_value_); + } else { + new (&err_value_) E(other.err_value_); + } + + return *this; + } + + // Move Assignment + Result& operator=(Result&& other) noexcept { + if (this == &other) { + return *this; + } + + // Destroy the current value + if (is_ok_) { + ok_value_.~T(); + } else { + err_value_.~E(); + } + + is_ok_ = other.is_ok_; + + // Construct the new value by moving + if (is_ok_) { + new (&ok_value_) T(std::move(other.ok_value_)); + } else { + new (&err_value_) E(std::move(other.err_value_)); + } + + return *this; + } + bool is_ok() const { return is_ok_; } bool is_err() const { return !is_ok_; } @@ -132,6 +183,32 @@ template class Result { T unwrap_or(T&& default_value) const { return is_ok_ ? ok_value_ : std::move(default_value); } + T expect(const char* message) && { + if (is_err()) { + std::fprintf(stderr, "Fatal (expect) error: %s\n", message); + std::terminate(); + } + return std::move(ok_value_); + } + + // Returns a mutable reference from an lvalue Result + T& expect(const char* message) & { + if (is_err()) { + std::fprintf(stderr, "Fatal (expect) error: %s\n", message); + std::terminate(); + } + return ok_value_; + } + + // Returns a const reference from a const lvalue Result + const T& expect(const char* message) const& { + if (is_err()) { + std::fprintf(stderr, "Fatal (expect) error: %s\n", message); + std::terminate(); + } + return ok_value_; + } + template T unwrap_or_else(F&& f) & { return is_ok_ ? ok_value_ : static_cast(f(err_value_)); } @@ -206,26 +283,24 @@ template class Result { } return err_value_; } + + void expect(const char* message) const { + if (is_err()) { + std::fprintf(stderr, "Fatal (expect) error: %s\n", message); + std::terminate(); + } + } }; #define match_result(res, ok_name, ok_block, err_name, err_block) \ - if ((res).is_ok()) { \ - auto&& ok_name = (res).unwrap(); \ - ok_block \ - } else { \ - auto&& err_name = (res).unwrap_err(); \ - err_block \ - } - -#define if_let_err(res, name, block) \ - if ((res).is_err()) { \ - auto&& name = (res).unwrap_err(); \ - block \ - } - -#define if_let_ok(res, name, block) \ - if ((res).is_ok()) { \ - auto&& name = (res).unwrap(); \ - block \ - } + do { \ + auto&& _result_val = (res); \ + if (_result_val.is_ok()) { \ + auto&& ok_name = _result_val.unwrap(); \ + ok_block \ + } else { \ + auto&& err_name = _result_val.unwrap_err(); \ + err_block \ + } \ + } while (0) } // namespace IdeviceFFI From d9bcecb6341729f939e18756b10e28328bdaedab Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Wed, 3 Sep 2025 20:55:26 -0600 Subject: [PATCH 120/126] Specify temporary lvalue in option match Fixes building on Windows --- cpp/include/idevice++/option.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/include/idevice++/option.hpp b/cpp/include/idevice++/option.hpp index be25570..ccc9f09 100644 --- a/cpp/include/idevice++/option.hpp +++ b/cpp/include/idevice++/option.hpp @@ -117,7 +117,7 @@ template class Option { // map template - auto map(F&& f) const -> Option::type> { + auto map(F&& f) const& -> Option::type> { using U = typename std::decay::type; if (has_) { return Option(f(*ptr())); From ddd2f84dd1beba08c69d4e77fa4908def22bce3d Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Thu, 4 Sep 2025 08:47:06 -0600 Subject: [PATCH 121/126] Implement pcapd --- idevice/Cargo.toml | 2 + idevice/src/lib.rs | 31 ++-- idevice/src/services/mod.rs | 2 + idevice/src/services/pcapd.rs | 260 ++++++++++++++++++++++++++++++++++ tools/Cargo.toml | 4 + tools/src/pcapd.rs | 97 +++++++++++++ 6 files changed, 385 insertions(+), 11 deletions(-) create mode 100644 idevice/src/services/pcapd.rs create mode 100644 tools/src/pcapd.rs diff --git a/idevice/Cargo.toml b/idevice/Cargo.toml index a69ede4..454fac0 100644 --- a/idevice/Cargo.toml +++ b/idevice/Cargo.toml @@ -85,6 +85,7 @@ mobile_image_mounter = ["dep:sha2"] mobilebackup2 = [] location_simulation = [] pair = ["chrono/default", "tokio/time", "dep:sha2", "dep:rsa", "dep:x509-cert"] +pcapd = [] obfuscate = ["dep:obfstr"] restore_service = [] rsd = ["xpc"] @@ -120,6 +121,7 @@ full = [ "mobile_image_mounter", "mobilebackup2", "pair", + "pcapd", "restore_service", "rsd", "springboardservices", diff --git a/idevice/src/lib.rs b/idevice/src/lib.rs index 7637998..b93e396 100644 --- a/idevice/src/lib.rs +++ b/idevice/src/lib.rs @@ -373,6 +373,22 @@ impl Idevice { /// # Errors /// Returns `IdeviceError` if reading, parsing fails, or device reports an error async fn read_plist(&mut self) -> Result { + let res = self.read_plist_value().await?; + let res: plist::Dictionary = plist::from_value(&res)?; + debug!("Received plist: {}", pretty_print_dictionary(&res)); + + if let Some(e) = res.get("Error") { + let e: String = plist::from_value(e)?; + if let Some(e) = IdeviceError::from_device_error_type(e.as_str(), &res) { + return Err(e); + } else { + return Err(IdeviceError::UnknownErrorType(e)); + } + } + Ok(res) + } + + async fn read_plist_value(&mut self) -> Result { if let Some(socket) = &mut self.socket { debug!("Reading response size"); let mut buf = [0u8; 4]; @@ -380,17 +396,7 @@ impl Idevice { let len = u32::from_be_bytes(buf); let mut buf = vec![0; len as usize]; socket.read_exact(&mut buf).await?; - let res: plist::Dictionary = plist::from_bytes(&buf)?; - debug!("Received plist: {}", pretty_print_dictionary(&res)); - - if let Some(e) = res.get("Error") { - let e: String = plist::from_value(e)?; - if let Some(e) = IdeviceError::from_device_error_type(e.as_str(), &res) { - return Err(e); - } else { - return Err(IdeviceError::UnknownErrorType(e)); - } - } + let res: plist::Value = plist::from_bytes(&buf)?; Ok(res) } else { Err(IdeviceError::NoEstablishedConnection) @@ -689,6 +695,8 @@ pub enum IdeviceError { UnsupportedWatchKey = -63, #[error("malformed command")] MalformedCommand = -64, + #[error("integer overflow")] + IntegerOverflow = -65, } impl IdeviceError { @@ -839,6 +847,7 @@ impl IdeviceError { IdeviceError::FfiBufferTooSmall(_, _) => -62, IdeviceError::UnsupportedWatchKey => -63, IdeviceError::MalformedCommand => -64, + IdeviceError::IntegerOverflow => -65, } } } diff --git a/idevice/src/services/mod.rs b/idevice/src/services/mod.rs index 6b14605..852f2ab 100644 --- a/idevice/src/services/mod.rs +++ b/idevice/src/services/mod.rs @@ -33,6 +33,8 @@ pub mod mobile_image_mounter; pub mod mobilebackup2; #[cfg(feature = "syslog_relay")] pub mod os_trace_relay; +#[cfg(feature = "pcapd")] +pub mod pcapd; #[cfg(feature = "restore_service")] pub mod restore_service; #[cfg(feature = "rsd")] diff --git a/idevice/src/services/pcapd.rs b/idevice/src/services/pcapd.rs new file mode 100644 index 0000000..e4a1359 --- /dev/null +++ b/idevice/src/services/pcapd.rs @@ -0,0 +1,260 @@ +//! Abstraction for pcapd + +use plist::Value; +use tokio::io::AsyncWrite; +use tokio::io::AsyncWriteExt; + +use crate::{Idevice, IdeviceError, IdeviceService, RsdService, obf}; + +const ETHERNET_HEADER: &[u8] = &[ + 0xBE, 0xEF, 0xBE, 0xEF, 0xBE, 0xEF, 0xBE, 0xEF, 0xBE, 0xEF, 0xBE, 0xEF, 0x08, 0x00, +]; + +/// Client for interacting with the pcapd service on the device. +pub struct PcapdClient { + /// The underlying device connection with established service + pub idevice: Idevice, +} + +impl IdeviceService for PcapdClient { + fn service_name() -> std::borrow::Cow<'static, str> { + obf!("com.apple.pcapd") + } + + async fn from_stream(idevice: Idevice) -> Result { + Ok(Self::new(idevice)) + } +} + +impl RsdService for PcapdClient { + fn rsd_service_name() -> std::borrow::Cow<'static, str> { + obf!("com.apple.pcapd.shim.remote") + } + + async fn from_stream(stream: Box) -> Result { + Ok(Self::new(Idevice::new(stream, "".to_string()))) + } +} + +/// A Rust representation of the iOS pcapd device packet header and data. +#[derive(Debug, Clone)] +pub struct DevicePacket { + pub header_length: u32, + pub header_version: u8, + pub packet_length: u32, + pub interface_type: u8, + pub unit: u16, + pub io: u8, + pub protocol_family: u32, + pub frame_pre_length: u32, + pub frame_post_length: u32, + pub interface_name: String, + pub pid: u32, + pub comm: String, + pub svc: u32, + pub epid: u32, + pub ecomm: String, + pub seconds: u32, + pub microseconds: u32, + pub data: Vec, +} + +impl PcapdClient { + pub fn new(idevice: Idevice) -> Self { + Self { idevice } + } + + pub async fn next_packet(&mut self) -> Result { + let packet = self.idevice.read_plist_value().await?; + let packet = match packet { + Value::Data(p) => p, + _ => { + return Err(IdeviceError::UnexpectedResponse); + } + }; + let mut packet = DevicePacket::from_vec(&packet)?; + packet.normalize_data(); + Ok(packet) + } +} + +impl DevicePacket { + /// Normalizes the packet data by adding a fake Ethernet header if necessary. + /// This is required for tools like Wireshark to correctly dissect raw IP packets. + pub fn normalize_data(&mut self) { + if self.frame_pre_length == 0 { + // Prepend the fake ethernet header for raw IP packets. + let mut new_data = ETHERNET_HEADER.to_vec(); + new_data.append(&mut self.data); + self.data = new_data; + } else if self.interface_name.starts_with("pdp_ip") { + // For cellular interfaces, skip the first 4 bytes of the original data + // before prepending the header. + if self.data.len() >= 4 { + let mut new_data = ETHERNET_HEADER.to_vec(); + new_data.extend_from_slice(&self.data[4..]); + self.data = new_data; + } + } + } + + /// Parses a byte vector into a DevicePacket. + /// + /// This is the primary method for creating a struct from the raw data + /// received from the device. + /// + /// # Arguments + /// * `bytes` - A `Vec` containing the raw bytes of a single packet frame. + /// + /// # Returns + /// A `Result` containing the parsed `DevicePacket` + pub fn from_vec(bytes: &[u8]) -> Result { + let mut r = ByteReader::new(bytes); + + // --- Parse Header --- + let header_length = r.read_u32_be()?; + let header_version = r.read_u8()?; + let packet_length = r.read_u32_be()?; + let interface_type = r.read_u8()?; + let unit = r.read_u16_be()?; + let io = r.read_u8()?; + let protocol_family = r.read_u32_be()?; + let frame_pre_length = r.read_u32_be()?; + let frame_post_length = r.read_u32_be()?; + let interface_name = r.read_cstr(16)?; + let pid = r.read_u32_le()?; // Little Endian + let comm = r.read_cstr(17)?; + let svc = r.read_u32_be()?; + let epid = r.read_u32_le()?; // Little Endian + let ecomm = r.read_cstr(17)?; + let seconds = r.read_u32_be()?; + let microseconds = r.read_u32_be()?; + + // --- Extract Packet Data --- + // The data starts at an absolute offset defined by `header_length`. + let data_start = header_length as usize; + let data_end = data_start.saturating_add(packet_length as usize); + + if data_end > bytes.len() { + return Err(IdeviceError::NotEnoughBytes(bytes.len(), data_end)); + } + let data = bytes[data_start..data_end].to_vec(); + + Ok(DevicePacket { + header_length, + header_version, + packet_length, + interface_type, + unit, + io, + protocol_family, + frame_pre_length, + frame_post_length, + interface_name, + pid, + comm, + svc, + epid, + ecomm, + seconds, + microseconds, + data, + }) + } +} + +/// A helper struct to safely read from a byte slice. +struct ByteReader<'a> { + slice: &'a [u8], + cursor: usize, +} + +impl<'a> ByteReader<'a> { + fn new(slice: &'a [u8]) -> Self { + Self { slice, cursor: 0 } + } + + /// Reads an exact number of bytes and advances the cursor. + fn read_exact(&mut self, len: usize) -> Result<&'a [u8], IdeviceError> { + let end = self + .cursor + .checked_add(len) + .ok_or(IdeviceError::IntegerOverflow)?; + if end > self.slice.len() { + return Err(IdeviceError::NotEnoughBytes(len, self.slice.len())); + } + let result = &self.slice[self.cursor..end]; + self.cursor = end; + Ok(result) + } + + fn read_u8(&mut self) -> Result { + self.read_exact(1).map(|s| s[0]) + } + + fn read_u16_be(&mut self) -> Result { + self.read_exact(2) + .map(|s| u16::from_be_bytes(s.try_into().unwrap())) + } + + fn read_u32_be(&mut self) -> Result { + self.read_exact(4) + .map(|s| u32::from_be_bytes(s.try_into().unwrap())) + } + + fn read_u32_le(&mut self) -> Result { + self.read_exact(4) + .map(|s| u32::from_le_bytes(s.try_into().unwrap())) + } + + /// Reads a fixed-size, null-padded C-style string. + fn read_cstr(&mut self, len: usize) -> Result { + let buffer = self.read_exact(len)?; + let end = buffer.iter().position(|&b| b == 0).unwrap_or(len); + String::from_utf8(buffer[..end].to_vec()).map_err(IdeviceError::Utf8) + } +} + +/// A writer for creating `.pcap` files from DevicePackets without external dependencies. +pub struct PcapFileWriter { + writer: W, +} + +impl PcapFileWriter { + /// Creates a new writer and asynchronously writes the pcap global header. + pub async fn new(mut writer: W) -> Result { + // Correct pcap global header for LINKTYPE_ETHERNET. + // We use big-endian format, as is traditional. + let header = [ + 0xa1, 0xb2, 0xc3, 0xd4, // magic number (big-endian) + 0x00, 0x02, // version_major + 0x00, 0x04, // version_minor + 0x00, 0x00, 0x00, 0x00, // thiszone (GMT) + 0x00, 0x00, 0x00, 0x00, // sigfigs (accuracy) + 0x00, 0x04, 0x00, 0x00, // snaplen (max packet size, 262144) + 0x00, 0x00, 0x00, 0x01, // network (LINKTYPE_ETHERNET) + ]; + writer.write_all(&header).await?; + Ok(Self { writer }) + } + + /// Asynchronously writes a single DevicePacket to the pcap file. + pub async fn write_packet(&mut self, packet: &DevicePacket) -> Result<(), std::io::Error> { + let mut record_header = [0u8; 16]; + + // Use the packet's own timestamp for accuracy. + record_header[0..4].copy_from_slice(&packet.seconds.to_be_bytes()); + record_header[4..8].copy_from_slice(&packet.microseconds.to_be_bytes()); + + // incl_len and orig_len + let len_bytes = (packet.data.len() as u32).to_be_bytes(); + record_header[8..12].copy_from_slice(&len_bytes); + record_header[12..16].copy_from_slice(&len_bytes); + + // Write the record header and packet data sequentially. + self.writer.write_all(&record_header).await?; + self.writer.write_all(&packet.data).await?; + + Ok(()) + } +} diff --git a/tools/Cargo.toml b/tools/Cargo.toml index 717838a..4943428 100644 --- a/tools/Cargo.toml +++ b/tools/Cargo.toml @@ -113,6 +113,10 @@ path = "src/diagnosticsservice.rs" name = "bt_packet_logger" path = "src/bt_packet_logger.rs" +[[bin]] +name = "pcapd" +path = "src/pcapd.rs" + [dependencies] idevice = { path = "../idevice", features = ["full"], default-features = false } tokio = { version = "1.43", features = ["full"] } diff --git a/tools/src/pcapd.rs b/tools/src/pcapd.rs new file mode 100644 index 0000000..682520a --- /dev/null +++ b/tools/src/pcapd.rs @@ -0,0 +1,97 @@ +// Jackson Coxson + +use clap::{Arg, Command}; +use idevice::{ + IdeviceService, + pcapd::{PcapFileWriter, PcapdClient}, +}; + +mod common; +mod pcap; + +#[tokio::main] +async fn main() { + env_logger::init(); + + let matches = Command::new("pcapd") + .about("Capture IP packets") + .arg( + Arg::new("host") + .long("host") + .value_name("HOST") + .help("IP address of the device"), + ) + .arg( + Arg::new("pairing_file") + .long("pairing-file") + .value_name("PATH") + .help("Path to the pairing file"), + ) + .arg( + Arg::new("udid") + .value_name("UDID") + .help("UDID of the device (overrides host/pairing file)") + .index(1), + ) + .arg( + Arg::new("about") + .long("about") + .help("Show about information") + .action(clap::ArgAction::SetTrue), + ) + .arg( + Arg::new("out") + .long("out") + .value_name("PCAP") + .help("Write PCAP to this file (use '-' for stdout)"), + ) + .get_matches(); + + if matches.get_flag("about") { + println!("bt_packet_logger - capture bluetooth packets"); + println!("Copyright (c) 2025 Jackson Coxson"); + return; + } + + let udid = matches.get_one::("udid"); + let host = matches.get_one::("host"); + let pairing_file = matches.get_one::("pairing_file"); + let out = matches.get_one::("out").map(String::to_owned); + + let provider = match common::get_provider(udid, host, pairing_file, "pcapd-jkcoxson").await { + Ok(p) => p, + Err(e) => { + eprintln!("{e}"); + return; + } + }; + + let mut logger_client = PcapdClient::connect(&*provider) + .await + .expect("Failed to connect to pcapd"); + + logger_client.next_packet().await.unwrap(); + + // Open output (default to stdout if --out omitted) + let mut out_writer = match out.as_deref() { + Some(path) => Some( + PcapFileWriter::new(tokio::fs::File::create(path).await.expect("open pcap")) + .await + .expect("write header"), + ), + _ => None, + }; + + println!("Starting packet stream"); + loop { + let packet = logger_client + .next_packet() + .await + .expect("failed to read next packet"); + if let Some(writer) = &mut out_writer { + writer.write_packet(&packet).await.expect("write packet"); + } else { + println!("{packet:?}"); + } + } +} From 2b678369e79c36e732371e5a75c26349aca160b0 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Thu, 4 Sep 2025 19:37:10 -0600 Subject: [PATCH 122/126] Add docs note that the pcapd service is only available over USB --- idevice/src/services/pcapd.rs | 2 ++ tools/src/pcapd.rs | 18 ++---------------- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/idevice/src/services/pcapd.rs b/idevice/src/services/pcapd.rs index e4a1359..91774a8 100644 --- a/idevice/src/services/pcapd.rs +++ b/idevice/src/services/pcapd.rs @@ -1,4 +1,5 @@ //! Abstraction for pcapd +//! Note that this service only works over USB or through RSD. use plist::Value; use tokio::io::AsyncWrite; @@ -11,6 +12,7 @@ const ETHERNET_HEADER: &[u8] = &[ ]; /// Client for interacting with the pcapd service on the device. +/// Note that this service only works over USB or through RSD. pub struct PcapdClient { /// The underlying device connection with established service pub idevice: Idevice, diff --git a/tools/src/pcapd.rs b/tools/src/pcapd.rs index 682520a..167a4aa 100644 --- a/tools/src/pcapd.rs +++ b/tools/src/pcapd.rs @@ -15,18 +15,6 @@ async fn main() { let matches = Command::new("pcapd") .about("Capture IP packets") - .arg( - Arg::new("host") - .long("host") - .value_name("HOST") - .help("IP address of the device"), - ) - .arg( - Arg::new("pairing_file") - .long("pairing-file") - .value_name("PATH") - .help("Path to the pairing file"), - ) .arg( Arg::new("udid") .value_name("UDID") @@ -54,11 +42,9 @@ async fn main() { } let udid = matches.get_one::("udid"); - let host = matches.get_one::("host"); - let pairing_file = matches.get_one::("pairing_file"); let out = matches.get_one::("out").map(String::to_owned); - let provider = match common::get_provider(udid, host, pairing_file, "pcapd-jkcoxson").await { + let provider = match common::get_provider(udid, None, None, "pcapd-jkcoxson").await { Ok(p) => p, Err(e) => { eprintln!("{e}"); @@ -68,7 +54,7 @@ async fn main() { let mut logger_client = PcapdClient::connect(&*provider) .await - .expect("Failed to connect to pcapd"); + .expect("Failed to connect to pcapd! This service is only available over USB!"); logger_client.next_packet().await.unwrap(); From e604b3ec9e50cc98c98b85d142cbfe2ca9b5127b Mon Sep 17 00:00:00 2001 From: Ylarod Date: Fri, 5 Sep 2025 09:47:07 +0800 Subject: [PATCH 123/126] feat: add utils to install local ipa (#21) * feat: add installation utils to install ipa * cargo fmt * clippy --- idevice/src/lib.rs | 1 + idevice/src/utils/installation.rs | 306 ++++++++++++++++++++++++++++++ idevice/src/utils/mod.rs | 4 + tools/Cargo.toml | 4 + tools/src/ideviceinstaller.rs | 103 ++++++++++ 5 files changed, 418 insertions(+) create mode 100644 idevice/src/utils/installation.rs create mode 100644 idevice/src/utils/mod.rs create mode 100644 tools/src/ideviceinstaller.rs diff --git a/idevice/src/lib.rs b/idevice/src/lib.rs index b93e396..009cf32 100644 --- a/idevice/src/lib.rs +++ b/idevice/src/lib.rs @@ -16,6 +16,7 @@ pub mod tunneld; #[cfg(feature = "usbmuxd")] pub mod usbmuxd; mod util; +pub mod utils; #[cfg(feature = "xpc")] pub mod xpc; diff --git a/idevice/src/utils/installation.rs b/idevice/src/utils/installation.rs new file mode 100644 index 0000000..748363a --- /dev/null +++ b/idevice/src/utils/installation.rs @@ -0,0 +1,306 @@ +//! High-level install/upgrade helpers +//! +//! This module provides convenient wrappers that mirror ideviceinstaller's +//! behavior by uploading a package to `PublicStaging` via AFC and then +//! issuing `Install`/`Upgrade` commands through InstallationProxy. +//! +//! Notes: +//! - The package path used by InstallationProxy must be a path inside the +//! AFC jail (e.g. `PublicStaging/`) +//! - For `.ipa` files, we upload the whole file to `PublicStaging/` +//! - For directories (developer bundles), we recursively mirror the directory +//! into `PublicStaging/` and pass that directory path. + +use std::path::Path; + +use crate::{ + IdeviceError, IdeviceService, + provider::IdeviceProvider, + services::{ + afc::{AfcClient, opcode::AfcFopenMode}, + installation_proxy::InstallationProxyClient, + }, +}; + +const PUBLIC_STAGING: &str = "PublicStaging"; + +/// Result of a prepared upload, containing the remote path to use in Install/Upgrade +struct UploadedPackageInfo { + /// Path inside the AFC jail for InstallationProxy `PackagePath` + remote_package_path: String, +} + +/// Ensure `PublicStaging` exists on device via AFC +async fn ensure_public_staging(afc: &mut AfcClient) -> Result<(), IdeviceError> { + // Try to stat and if it fails, create directory + match afc.get_file_info(PUBLIC_STAGING).await { + Ok(_) => Ok(()), + Err(_) => afc.mk_dir(PUBLIC_STAGING).await, + } +} + +/// Upload a single file to a destination path on device using AFC +async fn afc_upload_file( + afc: &mut AfcClient, + local_path: &Path, + remote_path: &str, +) -> Result<(), IdeviceError> { + let mut fd = afc.open(remote_path, AfcFopenMode::WrOnly).await?; + let bytes = tokio::fs::read(local_path).await?; + fd.write(&bytes).await?; + fd.close().await +} + +/// Recursively upload a directory to device via AFC (mirror contents) +async fn afc_upload_dir( + afc: &mut AfcClient, + local_dir: &Path, + remote_dir: &str, +) -> Result<(), IdeviceError> { + use std::collections::VecDeque; + afc.mk_dir(remote_dir).await.ok(); + + let mut queue: VecDeque<(std::path::PathBuf, String)> = VecDeque::new(); + queue.push_back((local_dir.to_path_buf(), remote_dir.to_string())); + + while let Some((cur_local, cur_remote)) = queue.pop_front() { + let mut rd = tokio::fs::read_dir(&cur_local).await?; + while let Some(entry) = rd.next_entry().await? { + let meta = entry.metadata().await?; + let name = entry.file_name(); + let name = name.to_string_lossy().into_owned(); + if name == "." || name == ".." { + continue; + } + let child_local = entry.path(); + let child_remote = format!("{}/{}", cur_remote, name); + if meta.is_dir() { + afc.mk_dir(&child_remote).await.ok(); + queue.push_back((child_local, child_remote)); + } else if meta.is_file() { + afc_upload_file(afc, &child_local, &child_remote).await?; + } + } + } + Ok(()) +} + +/// Upload a package to `PublicStaging` and return its InstallationProxy path +/// +/// - If `local_path` is a file, it will be uploaded to `PublicStaging/` +/// - If it is a directory, it will be mirrored to `PublicStaging/` +async fn upload_package_to_public_staging>( + provider: &dyn IdeviceProvider, + local_path: P, +) -> Result { + // Connect to AFC via the generic service connector + let mut afc = AfcClient::connect(provider).await?; + + ensure_public_staging(&mut afc).await?; + + let local_path = local_path.as_ref(); + let file_name: String = local_path + .file_name() + .map(|s| s.to_string_lossy().into_owned()) + .ok_or_else(|| IdeviceError::InvalidArgument)?; + let remote_path = format!("{}/{}", PUBLIC_STAGING, file_name); + + let meta = tokio::fs::metadata(local_path).await?; + if meta.is_dir() { + afc_upload_dir(&mut afc, local_path, &remote_path).await?; + } else { + afc_upload_file(&mut afc, local_path, &remote_path).await?; + } + + Ok(UploadedPackageInfo { + remote_package_path: remote_path, + }) +} + +/// Install an application by first uploading the local package and then invoking InstallationProxy. +/// +/// - Accepts a local file path or directory path. +/// - `options` is an InstallationProxy ClientOptions dictionary; pass `None` for defaults. +pub async fn install_package>( + provider: &dyn IdeviceProvider, + local_path: P, + options: Option, +) -> Result<(), IdeviceError> { + let UploadedPackageInfo { + remote_package_path, + } = upload_package_to_public_staging(provider, local_path).await?; + + let mut inst = InstallationProxyClient::connect(provider).await?; + inst.install(remote_package_path, options).await +} + +/// Upgrade an application by first uploading the local package and then invoking InstallationProxy. +/// +/// - Accepts a local file path or directory path. +/// - `options` is an InstallationProxy ClientOptions dictionary; pass `None` for defaults. +pub async fn upgrade_package>( + provider: &dyn IdeviceProvider, + local_path: P, + options: Option, +) -> Result<(), IdeviceError> { + let UploadedPackageInfo { + remote_package_path, + } = upload_package_to_public_staging(provider, local_path).await?; + + let mut inst = InstallationProxyClient::connect(provider).await?; + inst.upgrade(remote_package_path, options).await +} + +/// Same as `install_package` but providing a callback that receives `(percent_complete, state)` +/// updates while InstallationProxy performs the operation. +pub async fn install_package_with_callback, Fut, S>( + provider: &dyn IdeviceProvider, + local_path: P, + options: Option, + callback: impl Fn((u64, S)) -> Fut, + state: S, +) -> Result<(), IdeviceError> +where + Fut: std::future::Future, + S: Clone, +{ + let UploadedPackageInfo { + remote_package_path, + } = upload_package_to_public_staging(provider, local_path).await?; + + let mut inst = InstallationProxyClient::connect(provider).await?; + inst.install_with_callback(remote_package_path, options, callback, state) + .await +} + +/// Same as `upgrade_package` but providing a callback that receives `(percent_complete, state)` +/// updates while InstallationProxy performs the operation. +pub async fn upgrade_package_with_callback, Fut, S>( + provider: &dyn IdeviceProvider, + local_path: P, + options: Option, + callback: impl Fn((u64, S)) -> Fut, + state: S, +) -> Result<(), IdeviceError> +where + Fut: std::future::Future, + S: Clone, +{ + let UploadedPackageInfo { + remote_package_path, + } = upload_package_to_public_staging(provider, local_path).await?; + + let mut inst = InstallationProxyClient::connect(provider).await?; + inst.upgrade_with_callback(remote_package_path, options, callback, state) + .await +} + +/// Upload raw bytes to `PublicStaging/` via AFC and return the remote package path. +/// +/// - This is useful when the package is not present on disk or is generated in-memory. +async fn upload_bytes_to_public_staging( + provider: &dyn IdeviceProvider, + data: impl AsRef<[u8]>, + remote_name: &str, +) -> Result { + // Connect to AFC + let mut afc = AfcClient::connect(provider).await?; + ensure_public_staging(&mut afc).await?; + + let remote_path = format!("{}/{}", PUBLIC_STAGING, remote_name); + let mut fd = afc.open(&remote_path, AfcFopenMode::WrOnly).await?; + fd.write(data.as_ref()).await?; + fd.close().await?; + + Ok(UploadedPackageInfo { + remote_package_path: remote_path, + }) +} + +/// Install an application from raw bytes by first uploading them to `PublicStaging` and then +/// invoking InstallationProxy `Install`. +/// +/// - `remote_name` determines the remote filename under `PublicStaging`. +/// - `options` is an InstallationProxy ClientOptions dictionary; pass `None` for defaults. +pub async fn install_bytes( + provider: &dyn IdeviceProvider, + data: impl AsRef<[u8]>, + remote_name: &str, + options: Option, +) -> Result<(), IdeviceError> { + let UploadedPackageInfo { + remote_package_path, + } = upload_bytes_to_public_staging(provider, data, remote_name).await?; + let mut inst = InstallationProxyClient::connect(provider).await?; + inst.install(remote_package_path, options).await +} + +/// Same as `install_bytes` but providing a callback that receives `(percent_complete, state)` +/// updates while InstallationProxy performs the install operation. +/// +/// Tip: +/// - When embedding assets into the binary, you can pass `include_bytes!("path/to/app.ipa")` +/// as the `data` argument and choose a desired `remote_name` (e.g. `"MyApp.ipa"`). +pub async fn install_bytes_with_callback( + provider: &dyn IdeviceProvider, + data: impl AsRef<[u8]>, + remote_name: &str, + options: Option, + callback: impl Fn((u64, S)) -> Fut, + state: S, +) -> Result<(), IdeviceError> +where + Fut: std::future::Future, + S: Clone, +{ + let UploadedPackageInfo { + remote_package_path, + } = upload_bytes_to_public_staging(provider, data, remote_name).await?; + let mut inst = InstallationProxyClient::connect(provider).await?; + inst.install_with_callback(remote_package_path, options, callback, state) + .await +} + +/// Upgrade an application from raw bytes by first uploading them to `PublicStaging` and then +/// invoking InstallationProxy `Upgrade`. +/// +/// - `remote_name` determines the remote filename under `PublicStaging`. +/// - `options` is an InstallationProxy ClientOptions dictionary; pass `None` for defaults. +pub async fn upgrade_bytes( + provider: &dyn IdeviceProvider, + data: impl AsRef<[u8]>, + remote_name: &str, + options: Option, +) -> Result<(), IdeviceError> { + let UploadedPackageInfo { + remote_package_path, + } = upload_bytes_to_public_staging(provider, data, remote_name).await?; + let mut inst = InstallationProxyClient::connect(provider).await?; + inst.upgrade(remote_package_path, options).await +} + +/// Same as `upgrade_bytes` but providing a callback that receives `(percent_complete, state)` +/// updates while InstallationProxy performs the upgrade operation. +/// +/// Tip: +/// - When embedding assets into the binary, you can pass `include_bytes!("path/to/app.ipa")` +/// as the `data` argument and choose a desired `remote_name` (e.g. `"MyApp.ipa"`). +pub async fn upgrade_bytes_with_callback( + provider: &dyn IdeviceProvider, + data: impl AsRef<[u8]>, + remote_name: &str, + options: Option, + callback: impl Fn((u64, S)) -> Fut, + state: S, +) -> Result<(), IdeviceError> +where + Fut: std::future::Future, + S: Clone, +{ + let UploadedPackageInfo { + remote_package_path, + } = upload_bytes_to_public_staging(provider, data, remote_name).await?; + let mut inst = InstallationProxyClient::connect(provider).await?; + inst.upgrade_with_callback(remote_package_path, options, callback, state) + .await +} diff --git a/idevice/src/utils/mod.rs b/idevice/src/utils/mod.rs new file mode 100644 index 0000000..d30df75 --- /dev/null +++ b/idevice/src/utils/mod.rs @@ -0,0 +1,4 @@ +// Utility modules for higher-level operations built on top of services + +#[cfg(all(feature = "afc", feature = "installation_proxy"))] +pub mod installation; diff --git a/tools/Cargo.toml b/tools/Cargo.toml index 4943428..99d1978 100644 --- a/tools/Cargo.toml +++ b/tools/Cargo.toml @@ -21,6 +21,10 @@ path = "src/heartbeat_client.rs" name = "instproxy" path = "src/instproxy.rs" +[[bin]] +name = "ideviceinstaller" +path = "src/ideviceinstaller.rs" + [[bin]] name = "mounter" path = "src/mounter.rs" diff --git a/tools/src/ideviceinstaller.rs b/tools/src/ideviceinstaller.rs new file mode 100644 index 0000000..346af63 --- /dev/null +++ b/tools/src/ideviceinstaller.rs @@ -0,0 +1,103 @@ +// A minimal ideviceinstaller-like CLI to install/upgrade apps + +use clap::{Arg, ArgAction, Command}; +use idevice::utils::installation; + +mod common; + +#[tokio::main] +async fn main() { + env_logger::init(); + + let matches = Command::new("ideviceinstaller") + .about("Install/upgrade apps on an iOS device (AFC + InstallationProxy)") + .arg( + Arg::new("host") + .long("host") + .value_name("HOST") + .help("IP address of the device"), + ) + .arg( + Arg::new("pairing_file") + .long("pairing-file") + .value_name("PATH") + .help("Path to the pairing file"), + ) + .arg( + Arg::new("udid") + .value_name("UDID") + .help("UDID of the device (overrides host/pairing file)") + .index(1), + ) + .arg( + Arg::new("about") + .long("about") + .help("Show about information") + .action(ArgAction::SetTrue), + ) + .subcommand( + Command::new("install") + .about("Install a local .ipa or directory") + .arg(Arg::new("path").required(true).value_name("PATH")), + ) + .subcommand( + Command::new("upgrade") + .about("Upgrade from a local .ipa or directory") + .arg(Arg::new("path").required(true).value_name("PATH")), + ) + .get_matches(); + + if matches.get_flag("about") { + println!("ideviceinstaller - install/upgrade apps using AFC + InstallationProxy (Rust)"); + println!("Copyright (c) 2025"); + return; + } + + let udid = matches.get_one::("udid"); + let host = matches.get_one::("host"); + let pairing_file = matches.get_one::("pairing_file"); + + let provider = match common::get_provider(udid, host, pairing_file, "ideviceinstaller").await { + Ok(p) => p, + Err(e) => { + eprintln!("{e}"); + return; + } + }; + + if let Some(matches) = matches.subcommand_matches("install") { + let path: &String = matches.get_one("path").expect("required"); + match installation::install_package_with_callback( + &*provider, + path, + None, + |(percentage, _)| async move { + println!("Installing: {percentage}%"); + }, + (), + ) + .await + { + Ok(()) => println!("install success"), + Err(e) => eprintln!("Install failed: {e}"), + } + } else if let Some(matches) = matches.subcommand_matches("upgrade") { + let path: &String = matches.get_one("path").expect("required"); + match installation::upgrade_package_with_callback( + &*provider, + path, + None, + |(percentage, _)| async move { + println!("Upgrading: {percentage}%"); + }, + (), + ) + .await + { + Ok(()) => println!("upgrade success"), + Err(e) => eprintln!("Upgrade failed: {e}"), + } + } else { + eprintln!("Invalid usage, pass -h for help"); + } +} From a59414d99b4e543ee760873bb0b7d8abfc974b71 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Thu, 4 Sep 2025 19:51:08 -0600 Subject: [PATCH 124/126] Bump version --- idevice/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/idevice/Cargo.toml b/idevice/Cargo.toml index 454fac0..c4c0598 100644 --- a/idevice/Cargo.toml +++ b/idevice/Cargo.toml @@ -2,7 +2,7 @@ name = "idevice" description = "A Rust library to interact with services on iOS devices." authors = ["Jackson Coxson"] -version = "0.1.40" +version = "0.1.41" edition = "2024" license = "MIT" documentation = "https://docs.rs/idevice" From 3a9c9f47059d1568a391cd8ee10aac9b0906b052 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Fri, 5 Sep 2025 08:12:02 -0600 Subject: [PATCH 125/126] RSD checkin for pcapd --- Cargo.lock | 2 +- idevice/src/services/pcapd.rs | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 453622f..5c76807 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1133,7 +1133,7 @@ dependencies = [ [[package]] name = "idevice" -version = "0.1.40" +version = "0.1.41" dependencies = [ "async-stream", "base64", diff --git a/idevice/src/services/pcapd.rs b/idevice/src/services/pcapd.rs index 91774a8..15bb06f 100644 --- a/idevice/src/services/pcapd.rs +++ b/idevice/src/services/pcapd.rs @@ -34,7 +34,9 @@ impl RsdService for PcapdClient { } async fn from_stream(stream: Box) -> Result { - Ok(Self::new(Idevice::new(stream, "".to_string()))) + let mut idevice = Idevice::new(stream, ""); + idevice.rsd_checkin().await?; + Ok(Self::new(idevice)) } } From a9739b4ce33193ad13643fa64138b28b02e7a6e1 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Fri, 5 Sep 2025 11:40:13 -0600 Subject: [PATCH 126/126] Partial implementation for preboard sevice --- idevice/Cargo.toml | 2 + idevice/src/lib.rs | 21 ++++++- idevice/src/services/mod.rs | 2 + idevice/src/services/preboard_service.rs | 72 ++++++++++++++++++++++ tools/Cargo.toml | 4 ++ tools/src/preboard.rs | 76 ++++++++++++++++++++++++ 6 files changed, 176 insertions(+), 1 deletion(-) create mode 100644 idevice/src/services/preboard_service.rs create mode 100644 tools/src/preboard.rs diff --git a/idevice/Cargo.toml b/idevice/Cargo.toml index c4c0598..f172e4b 100644 --- a/idevice/Cargo.toml +++ b/idevice/Cargo.toml @@ -86,6 +86,7 @@ mobilebackup2 = [] location_simulation = [] pair = ["chrono/default", "tokio/time", "dep:sha2", "dep:rsa", "dep:x509-cert"] pcapd = [] +preboard_service = [] obfuscate = ["dep:obfstr"] restore_service = [] rsd = ["xpc"] @@ -122,6 +123,7 @@ full = [ "mobilebackup2", "pair", "pcapd", + "preboard_service", "restore_service", "rsd", "springboardservices", diff --git a/idevice/src/lib.rs b/idevice/src/lib.rs index 009cf32..33fb862 100644 --- a/idevice/src/lib.rs +++ b/idevice/src/lib.rs @@ -379,7 +379,20 @@ impl Idevice { debug!("Received plist: {}", pretty_print_dictionary(&res)); if let Some(e) = res.get("Error") { - let e: String = plist::from_value(e)?; + let e = match e { + plist::Value::String(e) => e.to_string(), + plist::Value::Integer(e) => { + if let Some(error_string) = res.get("ErrorString").and_then(|x| x.as_string()) { + error_string.to_string() + } else { + e.to_string() + } + } + _ => { + log::error!("Error is not a string or integer from read_plist: {e:?}"); + return Err(IdeviceError::UnexpectedResponse); + } + }; if let Some(e) = IdeviceError::from_device_error_type(e.as_str(), &res) { return Err(e); } else { @@ -698,6 +711,8 @@ pub enum IdeviceError { MalformedCommand = -64, #[error("integer overflow")] IntegerOverflow = -65, + #[error("canceled by user")] + CanceledByUser = -66, } impl IdeviceError { @@ -710,6 +725,9 @@ impl IdeviceError { /// # Returns /// Some(IdeviceError) if the string maps to a known error type, None otherwise fn from_device_error_type(e: &str, context: &plist::Dictionary) -> Option { + if e.contains("NSDebugDescription=Canceled by user.") { + return Some(Self::CanceledByUser); + } match e { "GetProhibited" => Some(Self::GetProhibited), "InvalidHostID" => Some(Self::InvalidHostID), @@ -849,6 +867,7 @@ impl IdeviceError { IdeviceError::UnsupportedWatchKey => -63, IdeviceError::MalformedCommand => -64, IdeviceError::IntegerOverflow => -65, + IdeviceError::CanceledByUser => -66, } } } diff --git a/idevice/src/services/mod.rs b/idevice/src/services/mod.rs index 852f2ab..24afbd6 100644 --- a/idevice/src/services/mod.rs +++ b/idevice/src/services/mod.rs @@ -35,6 +35,8 @@ pub mod mobilebackup2; pub mod os_trace_relay; #[cfg(feature = "pcapd")] pub mod pcapd; +#[cfg(feature = "preboard_service")] +pub mod preboard_service; #[cfg(feature = "restore_service")] pub mod restore_service; #[cfg(feature = "rsd")] diff --git a/idevice/src/services/preboard_service.rs b/idevice/src/services/preboard_service.rs new file mode 100644 index 0000000..8f5d7f9 --- /dev/null +++ b/idevice/src/services/preboard_service.rs @@ -0,0 +1,72 @@ +//! Abstraction for preboard + +use crate::{Idevice, IdeviceError, IdeviceService, RsdService, obf}; + +/// Client for interacting with the preboard service on the device. +pub struct PreboardServiceClient { + /// The underlying device connection with established service + pub idevice: Idevice, +} + +impl IdeviceService for PreboardServiceClient { + fn service_name() -> std::borrow::Cow<'static, str> { + obf!("com.apple.preboardservice_v2") + } + + async fn from_stream(idevice: Idevice) -> Result { + Ok(Self::new(idevice)) + } +} + +impl RsdService for PreboardServiceClient { + fn rsd_service_name() -> std::borrow::Cow<'static, str> { + obf!("com.apple.preboardservice_v2.shim.remote") + } + + async fn from_stream(stream: Box) -> Result { + let mut idevice = Idevice::new(stream, ""); + idevice.rsd_checkin().await?; + Ok(Self::new(idevice)) + } +} + +impl PreboardServiceClient { + pub fn new(idevice: Idevice) -> Self { + Self { idevice } + } + + pub async fn create_stashbag(&mut self, manifest: &[u8]) -> Result<(), IdeviceError> { + let req = crate::plist!({ + "Command": "CreateStashbag", + "Manifest": manifest + }); + self.idevice.send_plist(req).await?; + let res = self.idevice.read_plist().await?; + if let Some(res) = res.get("ShowDialog").and_then(|x| x.as_boolean()) { + if !res { + log::warn!("ShowDialog is not true"); + return Err(IdeviceError::UnexpectedResponse); + } + } else { + log::warn!("No ShowDialog in response from service"); + return Err(IdeviceError::UnexpectedResponse); + } + + self.idevice.read_plist().await?; + Ok(()) + } + + pub async fn commit_stashbag(&mut self, manifest: &[u8]) -> Result<(), IdeviceError> { + let req = crate::plist!({ + "Command": "CommitStashbag", + "Manifest": manifest + }); + self.idevice.send_plist(req).await?; + self.idevice.read_plist().await?; + Ok(()) + } + + pub async fn clear_system_token(&mut self) -> Result<(), IdeviceError> { + todo!() + } +} diff --git a/tools/Cargo.toml b/tools/Cargo.toml index 99d1978..57b5828 100644 --- a/tools/Cargo.toml +++ b/tools/Cargo.toml @@ -121,6 +121,10 @@ path = "src/bt_packet_logger.rs" name = "pcapd" path = "src/pcapd.rs" +[[bin]] +name = "preboard" +path = "src/preboard.rs" + [dependencies] idevice = { path = "../idevice", features = ["full"], default-features = false } tokio = { version = "1.43", features = ["full"] } diff --git a/tools/src/preboard.rs b/tools/src/preboard.rs new file mode 100644 index 0000000..b245037 --- /dev/null +++ b/tools/src/preboard.rs @@ -0,0 +1,76 @@ +// Jackson Coxson + +use clap::{Arg, Command}; +use idevice::{IdeviceService, preboard_service::PreboardServiceClient}; + +mod common; + +#[tokio::main] +async fn main() { + env_logger::init(); + + let matches = Command::new("preboard") + .about("Mess with developer mode") + .arg( + Arg::new("host") + .long("host") + .value_name("HOST") + .help("IP address of the device"), + ) + .arg( + Arg::new("pairing_file") + .long("pairing-file") + .value_name("PATH") + .help("Path to the pairing file"), + ) + .arg( + Arg::new("udid") + .value_name("UDID") + .help("UDID of the device (overrides host/pairing file)") + .index(1), + ) + .arg( + Arg::new("about") + .long("about") + .help("Show about information") + .action(clap::ArgAction::SetTrue), + ) + .subcommand(Command::new("create").about("Create a stashbag??")) + .subcommand(Command::new("commit").about("Commit a stashbag??")) + .get_matches(); + + if matches.get_flag("about") { + println!("preboard - no idea what this does"); + println!("Copyright (c) 2025 Jackson Coxson"); + return; + } + + let udid = matches.get_one::("udid"); + let host = matches.get_one::("host"); + let pairing_file = matches.get_one::("pairing_file"); + + let provider = match common::get_provider(udid, host, pairing_file, "amfi-jkcoxson").await { + Ok(p) => p, + Err(e) => { + eprintln!("{e}"); + return; + } + }; + + let mut pc = PreboardServiceClient::connect(&*provider) + .await + .expect("Failed to connect to Preboard"); + + if matches.subcommand_matches("create").is_some() { + pc.create_stashbag(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 0]) + .await + .expect("Failed to create"); + } else if matches.subcommand_matches("commit").is_some() { + pc.commit_stashbag(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 0]) + .await + .expect("Failed to create"); + } else { + eprintln!("Invalid usage, pass -h for help"); + } + return; +}