From 032a6a675153d87adaea53d1c47bcf80de7b544f Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Tue, 22 Jul 2025 10:49:14 -0600 Subject: [PATCH] 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