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;