diff --git a/.github/scripts/update_source.sh b/.github/scripts/update_source.sh
new file mode 100755
index 0000000..5406385
--- /dev/null
+++ b/.github/scripts/update_source.sh
@@ -0,0 +1,22 @@
+#!/bin/bash
+
+set -euo pipefail
+
+website_directory=${1:?Usage: update_source.sh WEBSITE_DIRECTORY RELEASE_VERSION}
+release_version=${2:?Usage: update_source.sh WEBSITE_DIRECTORY RELEASE_VERSION}
+
+cd "$website_directory"
+
+git config user.name 'GitHub Action'
+git config user.email 'action@github.com'
+
+python3 updatesource.py index.json
+git add index.json
+
+if git diff --staged --quiet; then
+ echo 'Website/index.json is already current.'
+ exit 0
+fi
+
+git commit -m "chore: update StikDebug source for $release_version"
+git push origin main
diff --git a/.github/workflows/build_ipa.yml b/.github/workflows/build_ipa.yml
index 806dbce..e0c08bb 100644
--- a/.github/workflows/build_ipa.yml
+++ b/.github/workflows/build_ipa.yml
@@ -3,17 +3,20 @@ name: Build Debug IPA
on:
push:
branches: [ "main" ]
+ tags: [ "[0-9]*" ]
pull_request:
branches: [ "main" ]
workflow_dispatch:
permissions:
- contents: read
+ contents: write
jobs:
build:
name: Build IPA
runs-on: macos-latest
+ outputs:
+ version: ${{ steps.version.outputs.version }}
env:
UPLOAD_IPA: ${{ vars.UPLOAD_IPA || 'true' }}
@@ -50,10 +53,70 @@ jobs:
zip -r StikDebug.ipa Payload
rm -rf Payload StikDebug.app
- - name: 5. Store IPA as Workflow Artifact
+ - name: 5. Name workflow artifact
+ id: artifact
+ if: env.UPLOAD_IPA == 'true'
+ run: |
+ file_name="StikDebug-${GITHUB_SHA::7}.ipa"
+ cp StikDebug.ipa "$file_name"
+ echo "file_name=$file_name" >> "$GITHUB_OUTPUT"
+
+ - name: 6. Store IPA as Workflow Artifact
if: env.UPLOAD_IPA == 'true'
uses: actions/upload-artifact@v7
with:
- name: StikDebug-Debug.ipa
- path: StikDebug.ipa
+ path: ${{ steps.artifact.outputs.file_name }}
+ archive: false
retention-days: 90
+
+ - name: Read version
+ id: version
+ if: startsWith(github.ref, 'refs/tags/')
+ run: |
+ version=$(xcodebuild -project StikDebug.xcodeproj -scheme StikDebug -showBuildSettings | awk -F ' = ' '/ MARKETING_VERSION = / { print $2; exit }')
+ test -n "$version"
+ if [ "$GITHUB_REF_TYPE" = "tag" ]; then
+ test "${GITHUB_REF_NAME#v}" = "$version"
+ fi
+ echo "version=$version" >> "$GITHUB_OUTPUT"
+
+ - name: Name release asset
+ if: startsWith(github.ref, 'refs/tags/')
+ run: mv StikDebug.ipa StikDebug-${{ steps.version.outputs.version }}.ipa
+
+ - name: Publish release
+ if: startsWith(github.ref, 'refs/tags/')
+ uses: softprops/action-gh-release@v3
+ with:
+ name: ${{ steps.version.outputs.version }}
+ tag_name: ${{ github.ref_name }}
+ target_commitish: ${{ github.sha }}
+ files: StikDebug-${{ steps.version.outputs.version }}.ipa
+ generate_release_notes: true
+ fail_on_unmatched_files: true
+
+ update-source:
+ name: Update website source
+ if: startsWith(github.ref, 'refs/tags/')
+ needs: build
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout source
+ uses: actions/checkout@v6
+
+ - name: Checkout website
+ uses: actions/checkout@v6
+ with:
+ repository: StikDebug/Website
+ ref: main
+ path: Website
+ token: ${{ secrets.WEBSITE_TOKEN }}
+
+ - name: Set up Python
+ uses: actions/setup-python@v7
+ with:
+ python-version: '3.x'
+
+ - name: Update StikDebug source
+ run: .github/scripts/update_source.sh Website '${{ needs.build.outputs.version }}'
diff --git a/.github/workflows/updatesource.yml b/.github/workflows/updatesource.yml
deleted file mode 100644
index 79dd171..0000000
--- a/.github/workflows/updatesource.yml
+++ /dev/null
@@ -1,52 +0,0 @@
-name: Update StikDebug Source
-
-on:
- release:
- types: [published]
- workflow_dispatch:
-
-jobs:
- update-source:
- runs-on: ubuntu-latest
- steps:
- - name: Checkout Website-Stuff
- uses: actions/checkout@v6
- with:
- repository: StephenDev0/Website-Stuff
- ref: main
- path: Website-Stuff
- # A repository-scoped GITHUB_TOKEN cannot push to Website-Stuff.
- token: ${{ secrets.WEBSITE_STUFF_TOKEN }}
-
- - name: Set up Python
- uses: actions/setup-python@v4
- with:
- python-version: '3.x'
-
- - name: Update StikDebug source
- id: update_source
- working-directory: Website-Stuff
- run: |
- git config --global user.name 'GitHub Action'
- git config --global user.email 'action@github.com'
-
- python updatesource.py index.json
- git add index.json
- if git diff --staged --quiet; then
- echo "changes=false" >> $GITHUB_OUTPUT
- echo "No changes detected in index.json"
- else
- git commit -m "Update StikDebug source for latest release"
- git push origin main
- echo "changes=true" >> $GITHUB_OUTPUT
- fi
-
- - name: Create job summary
- run: |
- if [[ "${{ steps.update_source.outputs.changes }}" == "true" ]]; then
- echo "## StikDebug source updated" >> $GITHUB_STEP_SUMMARY
- echo "Website-Stuff/index.json was updated and pushed." >> $GITHUB_STEP_SUMMARY
- else
- echo "## StikDebug source already current" >> $GITHUB_STEP_SUMMARY
- echo "No change to Website-Stuff/index.json was needed." >> $GITHUB_STEP_SUMMARY
- fi
diff --git a/README.md b/README.md
index 4f27be8..9e06a52 100644
--- a/README.md
+++ b/README.md
@@ -7,24 +7,16 @@
An on-device debugger/JIT enabler for iOS versions 17.4+ powered by idevice.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
## Features
-- **JIT:** Enable Just In Time compilation for sideloaded apps that have the `get-task-allow` entitlement.
+- **JIT:** Enable Just-In-Time compilation for sideloaded apps that have the `get-task-allow` entitlement.
- **App Launching:** Launch every app installed on your device.
- **Console:** Live app and system logs.
- **Scripts:** Manage automation scripts (mainly used for iOS 26 JIT).
@@ -37,14 +29,11 @@
> [!NOTE]
> **Notice:** StikDebug is no longer available on the App Store. Please use the official download methods below.
-
+
+
+
+
+
## Compatibility
@@ -66,18 +55,18 @@ StikDebug enables **JIT** for sideloaded apps on iOS 17.4+ without needing a com
### Steps
1. **Obtain a pairing file**
- - Detailed guide: [Pairing File Instructions](https://github.com/StephenDev0/StikDebug-Guide/blob/main/pairing_file.md) (or ask in Discord).
+ - Detailed guide: [Pairing File Instructions](https://github.com/StikDebug/StikDebug-Guide/blob/main/pairing_file.md) (or ask in Discord).
2. **Set up VPN**
- Launch LocalDevVPN and enable the VPN.
4. **Enable JIT for an app**
- - Launch StikDebug and tapp the `Enable JIT` button.
+ - Launch StikDebug and tap the `Enable JIT` button.
- Select your sideloaded app from the list in StikDebug.
**Troubleshooting**
- "Connection dropped" or loopback errors → Check iOS version compatibility / beta warnings.
-- Heartbeat errors → Ensure that the VPN is on and that you are connecected to Wi-Fi. It may be a pairing file issue.
+- Heartbeat errors → Ensure that the VPN is on and that you are connected to Wi-Fi. It may be a pairing file issue.
- Pairing file issues → Replace file with device unlocked & trusted.
- Still stuck? Join the [Discord](https://discord.gg/ZnNcrRT3M8) with logs/screenshots.
@@ -96,6 +85,10 @@ StikDebug enables **JIT** for sideloaded apps on iOS 17.4+ without needing a com
## Building from Source
+> [!IMPORTANT]
+> **Integrating StikDebug or StikJIT into your own project?**
+> See the [StikJIT Integration Guide](https://github.com/StikDebug/StikJIT/blob/main/INTEGRATION.md) for integration instructions.
+
### Requirements
- macOS (latest recommended)
- Xcode 16+ (Xcode 26+ preferred for iOS 26+ support)
@@ -106,7 +99,7 @@ StikDebug enables **JIT** for sideloaded apps on iOS 17.4+ without needing a com
### Steps
1. **Clone the repo**
```bash
- git clone https://github.com/StephenDev0/StikDebug.git
+ git clone https://github.com/StikDebug/StikDebug.git
cd StikDebug
```
@@ -146,13 +139,15 @@ To propose a new feature, open a feature request issue and provide:
### Code Contributions (Best Practices)
- Follow normal Swift and SwiftUI style.
-- Write clear and easy to understand code.
+- Write clear and easy-to-understand code.
- Keep your changes consistent with how the project is already set up.
- Make sure everything builds and works without errors.
We appreciate your time and effort in helping improve this project.
## Code Help
-[](https://deepwiki.com/stephendev0/stikdebug)
+
+[](https://deepwiki.com/StikDebug/StikDebug)
+
## License
StikDebug is licensed under **AGPL-3.0**. See [`LICENSE`](LICENSE) for details.
diff --git a/StikDebug.xcodeproj/project.pbxproj b/StikDebug.xcodeproj/project.pbxproj
index 7a3924b..8fef17f 100644
--- a/StikDebug.xcodeproj/project.pbxproj
+++ b/StikDebug.xcodeproj/project.pbxproj
@@ -482,7 +482,7 @@
"$(PROJECT_DIR)/StikDebug/idevice",
);
MACOSX_DEPLOYMENT_TARGET = 15.1;
- MARKETING_VERSION = 3.1.8;
+ MARKETING_VERSION = 3.1.14;
PRODUCT_BUNDLE_IDENTIFIER = com.stik.stikdebug;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -544,7 +544,7 @@
"$(PROJECT_DIR)/StikDebug/idevice",
);
MACOSX_DEPLOYMENT_TARGET = 15.1;
- MARKETING_VERSION = 3.1.8;
+ MARKETING_VERSION = 3.1.14;
PRODUCT_BUNDLE_IDENTIFIER = com.stik.stikdebug;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
diff --git a/StikDebug/App/AppBootstrapper.swift b/StikDebug/App/AppBootstrapper.swift
index ffb94ac..bf7985a 100644
--- a/StikDebug/App/AppBootstrapper.swift
+++ b/StikDebug/App/AppBootstrapper.swift
@@ -9,8 +9,9 @@ import UIKit
enum AppBootstrapper {
static func configure() {
+ IdeviceLogFileManager.shared.prepareForLogging()
registerDefaultSettings()
- startConfiguredKeepAliveServices()
+ ensureKeepAliveSelection()
applyDocumentPickerCopyWorkaround()
}
@@ -27,11 +28,11 @@ enum AppBootstrapper {
])
}
- private static func startConfiguredKeepAliveServices() {
- guard UserDefaults.standard.bool(forKey: "keepAliveAudio") else {
- return
+ private static func ensureKeepAliveSelection() {
+ let defaults = UserDefaults.standard
+ if !defaults.bool(forKey: "keepAliveAudio"), !defaults.bool(forKey: "keepAliveLocation") {
+ defaults.set(true, forKey: "keepAliveAudio")
}
- BackgroundAudioManager.shared.start()
}
private static func applyDocumentPickerCopyWorkaround() {
diff --git a/StikDebug/Device/IdeviceFFIBridge.swift b/StikDebug/Device/IdeviceFFIBridge.swift
index 6871c79..2c2c805 100644
--- a/StikDebug/Device/IdeviceFFIBridge.swift
+++ b/StikDebug/Device/IdeviceFFIBridge.swift
@@ -39,7 +39,8 @@ private enum IdeviceBridge {
}
let code = Int(ffiError.pointee.code)
- let message = string(from: ffiError.pointee.message) ?? fallback
+ let detail = string(from: ffiError.pointee.message) ?? fallback
+ let message = detail == fallback ? fallback : "\(fallback): \(detail)"
idevice_error_free(ffiError)
return makeError(domain: domain, code: code, message: message)
}
@@ -262,31 +263,69 @@ private enum IdeviceBridge {
}
extension JITEnableContext {
- func getMountedDeviceCount() throws -> Int {
+ func isDeveloperDiskImageMounted() throws -> Bool {
try IdeviceBridge.withTunnelHandles(for: self) { adapter, handshake in
- try IdeviceBridge.withConnectedClient(
- fallback: "Failed to connect to image mounter",
- missingClientMessage: "Image mounter client was not created",
- connect: { image_mounter_connect_rsd(adapter, handshake, $0) },
- cleanup: { image_mounter_free($0) }
- ) { client in
- var devices: UnsafeMutablePointer?
- var deviceCount = 0
- if let ffiError = image_mounter_copy_devices(client, &devices, &deviceCount) {
- throw IdeviceBridge.consumeFFIError(ffiError, fallback: "Failed to fetch mounted devices")
- }
-
- if let devices {
- for index in 0..?
+ defer { cryptexd_free_installed_cryptex(installed) }
+ if let ffiError = cryptexd_installed_ddi(adapter, handshake, &installed) {
+ cryptexQueryError = IdeviceBridge.consumeFFIError(ffiError, fallback: "Failed to query installed DDI cryptex")
+ } else {
+ cryptexQuerySucceeded = true
+ if installed != nil {
+ return true
}
- idevice_data_free(
- UnsafeMutableRawPointer(devices).assumingMemoryBound(to: UInt8.self),
- UInt(deviceCount * MemoryLayout.stride)
- )
}
+ }
- return deviceCount
+ do {
+ return try IdeviceBridge.withConnectedClient(
+ fallback: "Failed to connect to image mounter",
+ missingClientMessage: "Image mounter client was not created",
+ connect: { image_mounter_connect_rsd(adapter, handshake, $0) },
+ cleanup: { image_mounter_free($0) }
+ ) { client in
+ var devices: UnsafeMutablePointer?
+ var deviceCount = 0
+ if let error = image_mounter_copy_devices(client, &devices, &deviceCount) {
+ throw IdeviceBridge.consumeFFIError(error, fallback: "Failed to fetch mounted devices")
+ }
+ if let devices {
+ for index in 0...stride)
+ )
+ }
+ return deviceCount > 0
+ }
+ } catch {
+ if cryptexQuerySucceeded {
+ return false
+ }
+ throw cryptexQueryError ?? (error as NSError)
+ }
+ }
+ }
+
+ func installCryptexDDI(from directoryPath: String) throws {
+ var assets: OpaquePointer?
+ if let ffiError = cryptex1_assets_load(directoryPath, &assets) {
+ throw IdeviceBridge.consumeFFIError(ffiError, fallback: "Failed to load DDI cryptex assets")
+ }
+
+ guard let assets else {
+ throw IdeviceBridge.makeError(message: "DDI cryptex assets were not loaded")
+ }
+ defer { cryptex1_assets_free(assets) }
+
+ try IdeviceBridge.withTunnelHandles(for: self) { adapter, handshake in
+ if let ffiError = cryptexd_install_ddi(adapter, handshake, assets, nil) {
+ throw IdeviceBridge.consumeFFIError(ffiError, fallback: "Failed to install DDI cryptex")
}
}
}
@@ -307,13 +346,7 @@ extension JITEnableContext {
if let ffiError = lockdownd_get_value(lockdownClient, "UniqueChipID", nil, &uniqueChipIDPlist) {
throw IdeviceBridge.consumeFFIError(ffiError, fallback: "Failed to query UniqueChipID")
}
-
- defer {
- if let uniqueChipIDPlist {
- plist_free(uniqueChipIDPlist)
- }
- }
-
+ defer { plist_free(uniqueChipIDPlist) }
return try IdeviceBridge.uint64Value(from: uniqueChipIDPlist, fieldName: "UniqueChipID")
}
@@ -323,10 +356,10 @@ extension JITEnableContext {
connect: { image_mounter_connect_rsd(adapter, handshake, $0) },
cleanup: { image_mounter_free($0) }
) { imageMounterClient in
- let ffiError = imageData.withUnsafeBytes { imageBuffer -> UnsafeMutablePointer? in
- trustcacheData.withUnsafeBytes { trustcacheBuffer -> UnsafeMutablePointer? in
- manifestData.withUnsafeBytes { manifestBuffer -> UnsafeMutablePointer? in
- image_mounter_mount_personalized_with_callback_rsd(
+ let ffiError = imageData.withUnsafeBytes { imageBuffer in
+ trustcacheData.withUnsafeBytes { trustcacheBuffer in
+ manifestData.withUnsafeBytes { manifestBuffer in
+ image_mounter_mount_personalized_rsd(
imageMounterClient,
adapter,
handshake,
@@ -337,9 +370,7 @@ extension JITEnableContext {
manifestBuffer.bindMemory(to: UInt8.self).baseAddress,
manifestData.count,
nil,
- uniqueChipID,
- progressCallback,
- nil
+ uniqueChipID
)
}
}
diff --git a/StikDebug/Device/JITEnableContext.swift b/StikDebug/Device/JITEnableContext.swift
index ff6e7cc..d2c65f0 100644
--- a/StikDebug/Device/JITEnableContext.swift
+++ b/StikDebug/Device/JITEnableContext.swift
@@ -17,6 +17,37 @@ typealias SyslogErrorHandler = (NSError?) -> Void
final class JITEnableContext {
static let shared = JITEnableContext()
+ private static func withCStringArray(
+ _ strings: [String], _ body: (UnsafePointer?>?, UInt) -> R
+ ) -> R {
+ if strings.isEmpty {
+ return body(nil, 0)
+ }
+ var cStrings: [UnsafeMutablePointer?] = strings.map { strdup($0) }
+ defer { cStrings.forEach { free($0) } }
+ return cStrings.withUnsafeBufferPointer { buffer in
+ buffer.baseAddress!.withMemoryRebound(to: UnsafePointer?.self, capacity: buffer.count) { rebound in
+ body(rebound, UInt(buffer.count))
+ }
+ }
+ }
+
+ private static func mallocDebugEnvVars() -> [String] {
+ let defaults = UserDefaults.standard
+ let enableAll = defaults.bool(forKey: UserDefaults.Keys.mallocDebug)
+ let guardEdges = enableAll || defaults.bool(forKey: UserDefaults.Keys.mallocGuardEdges)
+ let scribble = enableAll || defaults.bool(forKey: UserDefaults.Keys.mallocScribble)
+
+ var envVars: [String] = []
+ if guardEdges {
+ envVars.append("MallocGuardEdges=1")
+ }
+ if scribble {
+ envVars.append("MallocScribble=1")
+ }
+ return envVars
+ }
+
private struct TunnelHandles {
var adapter: OpaquePointer?
var handshake: OpaquePointer?
@@ -51,11 +82,7 @@ final class JITEnableContext {
var handshakeHandle: OpaquePointer? { handshake }
private init() {
- let logURL = FileManager.default
- .urls(for: .documentDirectory, in: .userDomainMask)[0]
- .appendingPathComponent("idevice_log.txt")
-
- var path = Array(logURL.path.utf8CString)
+ var path = Array(IdeviceLogFileManager.logURL.path.utf8CString)
path.withUnsafeMutableBufferPointer { buffer in
_ = idevice_init_logger(Info, Debug, buffer.baseAddress)
}
@@ -90,7 +117,8 @@ final class JITEnableContext {
guard let ffiError else {
return makeError(fallback)
}
- let message = nsString(from: ffiError.pointee.message, fallback: fallback)
+ let detail = nsString(from: ffiError.pointee.message, fallback: fallback)
+ let message = detail == fallback ? fallback : "\(fallback): \(detail)"
let error = makeError(message, code: Int(ffiError.pointee.code))
idevice_error_free(ffiError)
return error
@@ -188,7 +216,9 @@ final class JITEnableContext {
tunnelLock.unlock()
if let waitSemaphore {
- waitSemaphore.wait()
+ guard waitSemaphore.wait(timeout: .now() + .seconds(15)) == .success else {
+ throw makeError("Timed out waiting for the tunnel connection", code: -19)
+ }
waitSemaphore.signal()
}
@@ -428,22 +458,19 @@ final class JITEnableContext {
private func withConnectedRemoteServer(
_ body: (OpaquePointer) throws -> T
) throws -> T {
- try ensureTunnel()
- guard let adapter, let handshake else {
- throw makeError("Tunnel is not connected")
- }
+ try withFreshDebugTunnel(hostname: "StikDebugRemoteServer") { adapter, handshake in
+ var remoteServer: OpaquePointer?
+ if let ffiError = remote_server_connect_rsd(adapter, handshake, &remoteServer) {
+ throw error(from: ffiError, fallback: "Failed to connect remote server")
+ }
- var remoteServer: OpaquePointer?
- if let ffiError = remote_server_connect_rsd(adapter, handshake, &remoteServer) {
- throw error(from: ffiError, fallback: "Failed to connect remote server")
- }
+ guard let remoteServer else {
+ throw makeError("Remote server handle was not created")
+ }
- guard let remoteServer else {
- throw makeError("Remote server handle was not created")
+ defer { remote_server_free(remoteServer) }
+ return try body(remoteServer)
}
-
- defer { remote_server_free(remoteServer) }
- return try body(remoteServer)
}
private func withProcessControl(
@@ -601,7 +628,9 @@ final class JITEnableContext {
try withProcessControl(remoteServer: remoteServer) { processControl in
var pid: UInt64 = 0
let ffiError = bundleID.withCString { bundleID in
- process_control_launch_app(processControl, bundleID, nil, 0, nil, 0, true, false, &pid)
+ Self.withCStringArray(Self.mallocDebugEnvVars()) { envPtr, envCount in
+ process_control_launch_app(processControl, bundleID, envPtr, envCount, nil, 0, true, false, &pid)
+ }
}
if let ffiError {
diff --git a/StikDebug/Device/MountingProgress.swift b/StikDebug/Device/MountingProgress.swift
index c95fbd4..80d31e1 100644
--- a/StikDebug/Device/MountingProgress.swift
+++ b/StikDebug/Device/MountingProgress.swift
@@ -9,74 +9,108 @@ import idevice
final class MountingProgress: ObservableObject {
static let shared = MountingProgress()
- @Published private(set) var mountProgress: Double = 0.0
@Published private(set) var mountingThread: Thread?
@Published private(set) var coolisMounted: Bool = false
+ private let mountCheckLock = NSLock()
+ private var mountCheckInProgress = false
+ private let mountLock = NSLock()
+ private var mountInProgress = false
+
private init() {}
func checkforMounted() {
- DispatchQueue.global(qos: .utility).async {
- let mounted = isMounted()
- DispatchQueue.main.async {
- self.coolisMounted = mounted
- }
- }
- }
+ guard TunnelManager.shared.isConnected else { return }
- func progressCallback(progress: size_t, total: size_t, context: UnsafeMutableRawPointer?) {
- let percentage = Double(progress) / Double(total) * 100.0
- DispatchQueue.main.async {
- self.mountProgress = percentage
+ mountCheckLock.lock()
+ guard !mountCheckInProgress else {
+ mountCheckLock.unlock()
+ return
+ }
+ mountCheckInProgress = true
+ mountCheckLock.unlock()
+
+ DispatchQueue.global(qos: .utility).async {
+ let status = checkMountStatus()
+
+ self.mountCheckLock.lock()
+ self.mountCheckInProgress = false
+ self.mountCheckLock.unlock()
+
+ DispatchQueue.main.async {
+ if status != .unreachable {
+ self.coolisMounted = status == .mounted
+ }
+ }
}
}
func pubMount() {
- mount()
+ guard TunnelManager.shared.isConnected, DeveloperDiskImageService.filesAreReady else { return }
+
+ mountLock.lock()
+ guard !mountInProgress else {
+ mountLock.unlock()
+ return
+ }
+ mountInProgress = true
+ mountLock.unlock()
+
+ let thread = Thread { [weak self] in
+ self?.mount()
+ }
+ thread.qualityOfService = .background
+ thread.name = "mounting"
+ DispatchQueue.main.async {
+ self.mountingThread = thread
+ }
+ thread.start()
}
private func mount() {
- let currentlyMounted = isMounted()
- DispatchQueue.main.async {
- self.coolisMounted = currentlyMounted
+ switch checkMountStatus() {
+ case .mounted:
+ finishMount {
+ self.coolisMounted = true
+ }
+ return
+ case .unreachable:
+ finishMount()
+ return
+ case .notMounted:
+ break
}
- guard isPairing(), !currentlyMounted else {
+ guard isPairing() else {
+ finishMount()
return
}
- if let mountingThread {
- mountingThread.cancel()
- self.mountingThread = nil
- }
-
- let thread = Thread { [weak self] in
- guard let self else { return }
- let mountError = mountPersonalDDI(
- imagePath: URL.documentsDirectory.appendingPathComponent("DDI/Image.dmg").path,
- trustcachePath: URL.documentsDirectory.appendingPathComponent("DDI/Image.dmg.trustcache").path,
- manifestPath: URL.documentsDirectory.appendingPathComponent("DDI/BuildManifest.plist").path
- )
-
- DispatchQueue.main.async {
- if let mountError {
- showAlert(title: "DDI Mount Failed", message: mountError, showOk: true, showTryAgain: true) { shouldTryAgain in
- if shouldTryAgain {
- self.mount()
- }
+ let mountError = mountDeveloperDiskImage(from: DeveloperDiskImageService.directoryURL.path)
+ let mounted = mountError == nil || checkMountStatus() == .mounted
+ finishMount {
+ if mounted {
+ self.coolisMounted = true
+ self.checkforMounted()
+ } else if let mountError {
+ LogManager.shared.addErrorLog("Failed to mount DDI: \(mountError)")
+ showAlert(title: "DDI Mount Failed", message: mountError, showOk: true, showTryAgain: true) { shouldTryAgain in
+ if shouldTryAgain {
+ self.pubMount()
}
- } else {
- self.coolisMounted = true
- self.checkforMounted()
}
- self.mountingThread = nil
}
}
+ }
- thread.qualityOfService = .background
- thread.name = "mounting"
- thread.start()
- mountingThread = thread
+ private func finishMount(_ completion: @escaping () -> Void = {}) {
+ DispatchQueue.main.async {
+ self.mountingThread = nil
+ self.mountLock.lock()
+ self.mountInProgress = false
+ self.mountLock.unlock()
+ completion()
+ }
}
}
diff --git a/StikDebug/Device/mountDDI.swift b/StikDebug/Device/mountDDI.swift
index 98121c8..0716ebb 100644
--- a/StikDebug/Device/mountDDI.swift
+++ b/StikDebug/Device/mountDDI.swift
@@ -13,10 +13,6 @@ typealias RsdHandshakeHandle = OpaquePointer
typealias ImageMounterHandle = OpaquePointer
typealias LockdowndClientHandle = OpaquePointer
-func progressCallback(progress: size_t, total: size_t, context: UnsafeMutableRawPointer?) {
- MountingProgress.shared.progressCallback(progress: progress, total: total, context: context)
-}
-
enum MountCheckResult {
case mounted
case notMounted
@@ -29,18 +25,25 @@ func isMounted() -> Bool {
func checkMountStatus() -> MountCheckResult {
do {
- let result = try JITEnableContext.shared.getMountedDeviceCount()
- return result > 0 ? .mounted : .notMounted
+ return try JITEnableContext.shared.isDeveloperDiskImageMounted() ? .mounted : .notMounted
} catch {
+ LogManager.shared.addErrorLog("Failed to check DDI mount status: \(error.localizedDescription)")
return .unreachable
}
}
-func mountPersonalDDI(imagePath: String, trustcachePath: String, manifestPath: String) -> String? {
+func mountDeveloperDiskImage(from directoryPath: String) -> String? {
do {
- try JITEnableContext.shared.mountPersonalDDI(withImagePath: imagePath, trustcachePath: trustcachePath, manifestPath: manifestPath)
+ if DeveloperDiskImageService.usesCryptexDDI {
+ try JITEnableContext.shared.installCryptexDDI(from: directoryPath)
+ } else {
+ try JITEnableContext.shared.mountPersonalDDI(
+ withImagePath: URL(fileURLWithPath: directoryPath).appendingPathComponent("Image.dmg").path,
+ trustcachePath: URL(fileURLWithPath: directoryPath).appendingPathComponent("Image.dmg.trustcache").path,
+ manifestPath: URL(fileURLWithPath: directoryPath).appendingPathComponent("BuildManifest.plist").path
+ )
+ }
} catch {
- LogManager.shared.addErrorLog("Failed to mount DDI: \(error.localizedDescription)")
return error.localizedDescription
}
return nil
diff --git a/StikDebug/Features/InstalledApps/InstalledAppsViewModel.swift b/StikDebug/Features/InstalledApps/InstalledAppsViewModel.swift
index 5a93389..7a1ecb1 100644
--- a/StikDebug/Features/InstalledApps/InstalledAppsViewModel.swift
+++ b/StikDebug/Features/InstalledApps/InstalledAppsViewModel.swift
@@ -20,13 +20,26 @@ final class InstalledAppsViewModel: ObservableObject {
private let cacheKeyDebuggable = "cachedDebuggableApps"
private let cacheKeyNonDebuggable = "cachedNonDebuggableApps"
private let cacheKeySystem = "cachedSystemApps"
+ private var cancellables: Set = []
+ private var refreshInProgress = false
+ private var refreshPending = false
init() {
loadCachedApps()
- refreshAppLists()
+ TunnelManager.shared.$isConnected
+ .removeDuplicates()
+ .filter { $0 }
+ .sink { [weak self] _ in self?.refreshAppLists() }
+ .store(in: &cancellables)
}
func refreshAppLists() {
+ guard !refreshInProgress else {
+ refreshPending = true
+ return
+ }
+
+ refreshInProgress = true
isLoading = true
lastError = nil
@@ -42,6 +55,11 @@ final class InstalledAppsViewModel: ObservableObject {
debuggable: debuggable,
hiddenSystem: hiddenSystem
)
+ self.cacheApps(
+ debuggable: debuggable,
+ nonDebuggable: classifiedApps.nonDebuggable,
+ system: classifiedApps.system
+ )
DispatchQueue.main.async {
self.apply(
@@ -49,22 +67,26 @@ final class InstalledAppsViewModel: ObservableObject {
nonDebuggable: classifiedApps.nonDebuggable,
system: classifiedApps.system
)
- self.isLoading = false
- self.cacheApps(
- debuggable: debuggable,
- nonDebuggable: classifiedApps.nonDebuggable,
- system: classifiedApps.system
- )
+ self.finishRefresh()
}
} catch {
DispatchQueue.main.async {
- self.isLoading = false
self.lastError = error.localizedDescription
+ self.finishRefresh()
}
}
}
}
+ private func finishRefresh() {
+ refreshInProgress = false
+ isLoading = false
+
+ guard refreshPending else { return }
+ refreshPending = false
+ refreshAppLists()
+ }
+
func displayName(for bundleID: String) -> String? {
debuggableApps[bundleID] ?? systemApps[bundleID] ?? nonDebuggableApps[bundleID]
}
diff --git a/StikDebug/Services/BackgroundAudioManager.swift b/StikDebug/Services/BackgroundAudioManager.swift
index c19f22b..2cbbdfe 100644
--- a/StikDebug/Services/BackgroundAudioManager.swift
+++ b/StikDebug/Services/BackgroundAudioManager.swift
@@ -11,7 +11,6 @@ final class BackgroundAudioManager {
private var engine = AVAudioEngine()
private var player = AVAudioPlayerNode()
private var isRunning = false
- private var persistentEnabled = false
private var activityCount = 0
private var healthCheckTimer: Timer?
@@ -30,13 +29,7 @@ final class BackgroundAudioManager {
)
}
- func start() {
- persistentEnabled = true
- refreshRunningState()
- }
-
- func stop() {
- persistentEnabled = false
+ func configurationDidChange() {
refreshRunningState()
}
@@ -51,7 +44,7 @@ final class BackgroundAudioManager {
}
private func refreshRunningState() {
- let shouldRun = persistentEnabled || (activityCount > 0 && UserDefaults.standard.bool(forKey: "keepAliveAudio"))
+ let shouldRun = activityCount > 0 && UserDefaults.standard.bool(forKey: "keepAliveAudio")
guard shouldRun != isRunning else {
if shouldRun {
recoverIfNeeded()
diff --git a/StikDebug/Services/BackgroundLocationManager.swift b/StikDebug/Services/BackgroundLocationManager.swift
index 993da4c..a078f74 100644
--- a/StikDebug/Services/BackgroundLocationManager.swift
+++ b/StikDebug/Services/BackgroundLocationManager.swift
@@ -4,6 +4,7 @@
//
import CoreLocation
+import UIKit
final class BackgroundLocationManager: NSObject, CLLocationManagerDelegate {
static let shared = BackgroundLocationManager()
@@ -11,6 +12,7 @@ final class BackgroundLocationManager: NSObject, CLLocationManagerDelegate {
private let locationManager = CLLocationManager()
private var isRunning = false
private var activityCount = 0
+ private var isRequestingAuthorization = false
private override init() {
super.init()
@@ -21,8 +23,44 @@ final class BackgroundLocationManager: NSObject, CLLocationManagerDelegate {
locationManager.pausesLocationUpdatesAutomatically = false
}
- func start() {
- isRunning = true
+ func configurationDidChange() {
+ refreshRunningState()
+ }
+
+ func requestStart() {
+ activityCount += 1
+ refreshRunningState()
+ }
+
+ func requestStop() {
+ activityCount = max(activityCount - 1, 0)
+ refreshRunningState()
+ }
+
+ func requestAuthorizationIfNeeded() {
+ switch locationManager.authorizationStatus {
+ case .authorizedAlways, .authorizedWhenInUse:
+ return
+ case .notDetermined:
+ isRequestingAuthorization = true
+ locationManager.requestAlwaysAuthorization()
+ case .denied, .restricted:
+ restoreAudioFallback()
+ @unknown default:
+ restoreAudioFallback()
+ }
+ }
+
+ private func refreshRunningState() {
+ let shouldRun = activityCount > 0 && UserDefaults.standard.bool(forKey: "keepAliveLocation")
+ guard shouldRun != isRunning else { return }
+
+ isRunning = shouldRun
+ guard shouldRun else {
+ locationManager.stopUpdatingLocation()
+ return
+ }
+
switch locationManager.authorizationStatus {
case .authorizedAlways:
locationManager.startUpdatingLocation()
@@ -30,37 +68,54 @@ final class BackgroundLocationManager: NSObject, CLLocationManagerDelegate {
locationManager.requestAlwaysAuthorization()
case .notDetermined:
locationManager.requestAlwaysAuthorization()
- default:
- break
- }
- }
-
- func stop() {
- isRunning = false
- locationManager.stopUpdatingLocation()
- }
-
- func requestStart() {
- activityCount += 1
- if activityCount == 1, UserDefaults.standard.bool(forKey: "keepAliveLocation") {
- start()
- }
- }
-
- func requestStop() {
- activityCount = max(activityCount - 1, 0)
- if activityCount == 0 {
- stop()
+ case .denied, .restricted:
+ restoreAudioFallback()
+ @unknown default:
+ restoreAudioFallback()
}
}
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
- guard isRunning else { return }
switch manager.authorizationStatus {
case .authorizedAlways, .authorizedWhenInUse:
- manager.startUpdatingLocation()
- default:
- break
+ isRequestingAuthorization = false
+ if isRunning {
+ manager.startUpdatingLocation()
+ }
+ case .denied, .restricted:
+ if isRunning || isRequestingAuthorization {
+ isRequestingAuthorization = false
+ restoreAudioFallback()
+ }
+ case .notDetermined:
+ return
+ @unknown default:
+ if isRunning || isRequestingAuthorization {
+ isRequestingAuthorization = false
+ restoreAudioFallback()
+ }
+ }
+ }
+
+ private func restoreAudioFallback() {
+ let defaults = UserDefaults.standard
+ defaults.set(false, forKey: "keepAliveLocation")
+ defaults.set(true, forKey: "keepAliveAudio")
+ refreshRunningState()
+ BackgroundAudioManager.shared.configurationDidChange()
+
+ showAlert(
+ title: "Location Access Refused",
+ message: "Location access was refused. Open Settings to enable it.",
+ showOk: false,
+ showTryAgain: true,
+ primaryButtonText: "Settings"
+ ) { openSettings in
+ guard openSettings,
+ let settingsURL = URL(string: UIApplication.openSettingsURLString) else {
+ return
+ }
+ UIApplication.shared.open(settingsURL)
}
}
diff --git a/StikDebug/Services/DeveloperDiskImageService.swift b/StikDebug/Services/DeveloperDiskImageService.swift
index 3d09332..d52ed8d 100644
--- a/StikDebug/Services/DeveloperDiskImageService.swift
+++ b/StikDebug/Services/DeveloperDiskImageService.swift
@@ -7,6 +7,17 @@ import Foundation
final class DeveloperDiskImageService {
static let shared = DeveloperDiskImageService()
+ static let directoryURL = URL.documentsDirectory.appendingPathComponent("DDI", isDirectory: true)
+
+ static var filesAreReady: Bool {
+ downloadItems.allSatisfy {
+ FileManager.default.fileExists(atPath: directoryURL.appendingPathComponent($0.fileName).path)
+ } && (try? String(contentsOf: mountMethodURL, encoding: .utf8)) == mountMethod.rawValue
+ }
+
+ static var usesCryptexDDI: Bool {
+ mountMethod == .cryptex
+ }
private let fileManager: FileManager
private let session: URLSession
@@ -17,13 +28,21 @@ final class DeveloperDiskImageService {
}
func downloadMissingFiles() async throws {
+ let installedMethod = try? String(contentsOf: Self.mountMethodURL, encoding: .utf8)
+ let replaceFiles = installedMethod != Self.mountMethod.rawValue
+
+ if !Self.usesCryptexDDI {
+ try removeCryptexOnlyFiles()
+ }
+
for item in Self.downloadItems {
- let destinationURL = URL.documentsDirectory.appendingPathComponent(item.relativePath)
- guard !fileManager.fileExists(atPath: destinationURL.path) else {
+ let destinationURL = Self.directoryURL.appendingPathComponent(item.fileName)
+ guard replaceFiles || !fileManager.fileExists(atPath: destinationURL.path) else {
continue
}
try await downloadFile(from: item.urlString, to: destinationURL)
}
+ try Self.mountMethod.rawValue.write(to: Self.mountMethodURL, atomically: true, encoding: .utf8)
}
func downloadFile(from urlString: String, to destinationURL: URL) async throws {
@@ -56,8 +75,18 @@ final class DeveloperDiskImageService {
var completedStages = 0.0
progressHandler?(0.0, "Removing existing DDI files...")
+ if fileManager.fileExists(atPath: Self.mountMethodURL.path) {
+ try fileManager.removeItem(at: Self.mountMethodURL)
+ }
+ if !Self.usesCryptexDDI {
+ try removeCryptexOnlyFiles()
+ }
+ let completionMarker = Self.directoryURL.appendingPathComponent("Image.dmg.root_hash")
+ if fileManager.fileExists(atPath: completionMarker.path) {
+ try fileManager.removeItem(at: completionMarker)
+ }
for item in Self.downloadItems {
- let fileURL = URL.documentsDirectory.appendingPathComponent(item.relativePath)
+ let fileURL = Self.directoryURL.appendingPathComponent(item.fileName)
if fileManager.fileExists(atPath: fileURL.path) {
try fileManager.removeItem(at: fileURL)
}
@@ -68,37 +97,100 @@ final class DeveloperDiskImageService {
for item in Self.downloadItems {
progressHandler?(completedStages / totalStages, "Downloading \(item.name)...")
- let destinationURL = URL.documentsDirectory.appendingPathComponent(item.relativePath)
+ let destinationURL = Self.directoryURL.appendingPathComponent(item.fileName)
try await downloadFile(from: item.urlString, to: destinationURL)
completedStages += 1.0
progressHandler?(completedStages / totalStages, "\(item.name) ready")
}
+ try Self.mountMethod.rawValue.write(to: Self.mountMethodURL, atomically: true, encoding: .utf8)
+
progressHandler?(1.0, "DDI download complete.")
}
- private static let downloadItems: [DDIDownloadItem] = [
+ private func removeCryptexOnlyFiles() throws {
+ for fileName in Self.cryptexOnlyFileNames {
+ let fileURL = Self.directoryURL.appendingPathComponent(fileName)
+ if fileManager.fileExists(atPath: fileURL.path) {
+ try fileManager.removeItem(at: fileURL)
+ }
+ }
+ }
+
+ private static var mountMethod: DDIMountMethod {
+ let version = ProcessInfo.processInfo.operatingSystemVersion
+ return version.majorVersion > 26 || (version.majorVersion == 26 && version.minorVersion >= 4)
+ ? .cryptex
+ : .personalized
+ }
+
+ private static let mountMethodURL = directoryURL.appendingPathComponent("MountMethod")
+ private static let cryptexOnlyFileNames = ["Image.dmg.cryptex_info", "Image.dmg.root_hash"]
+
+ private static var downloadItems: [DDIDownloadItem] {
+ switch mountMethod {
+ case .cryptex:
+ return cryptexDownloadItems
+ case .personalized:
+ return personalizedDownloadItems
+ }
+ }
+
+ private static let cryptexDownloadItems: [DDIDownloadItem] = [
.init(
name: "Build Manifest",
- relativePath: "DDI/BuildManifest.plist",
+ fileName: "BuildManifest.plist",
+ urlString: "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Cryptex/BuildManifest.plist"
+ ),
+ .init(
+ name: "Image",
+ fileName: "Image.dmg",
+ urlString: "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Cryptex/Image.dmg"
+ ),
+ .init(
+ name: "TrustCache",
+ fileName: "Image.dmg.trustcache",
+ urlString: "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Cryptex/Image.dmg.trustcache"
+ ),
+ .init(
+ name: "Cryptex Info",
+ fileName: "Image.dmg.cryptex_info",
+ urlString: "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Cryptex/Image.dmg.cryptex_info"
+ ),
+ .init(
+ name: "Root Hash",
+ fileName: "Image.dmg.root_hash",
+ urlString: "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Cryptex/Image.dmg.root_hash"
+ )
+ ]
+
+ private static let personalizedDownloadItems: [DDIDownloadItem] = [
+ .init(
+ name: "Build Manifest",
+ fileName: "BuildManifest.plist",
urlString: "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Personalized/BuildManifest.plist"
),
.init(
name: "Image",
- relativePath: "DDI/Image.dmg",
+ fileName: "Image.dmg",
urlString: "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Personalized/Image.dmg"
),
.init(
name: "TrustCache",
- relativePath: "DDI/Image.dmg.trustcache",
+ fileName: "Image.dmg.trustcache",
urlString: "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Personalized/Image.dmg.trustcache"
)
]
}
+private enum DDIMountMethod: String {
+ case cryptex
+ case personalized
+}
+
private struct DDIDownloadItem {
let name: String
- let relativePath: String
+ let fileName: String
let urlString: String
}
diff --git a/StikDebug/Services/IdeviceLogFileManager.swift b/StikDebug/Services/IdeviceLogFileManager.swift
new file mode 100644
index 0000000..30bd068
--- /dev/null
+++ b/StikDebug/Services/IdeviceLogFileManager.swift
@@ -0,0 +1,66 @@
+//
+// IdeviceLogFileManager.swift
+// StikDebug
+//
+
+import Foundation
+
+final class IdeviceLogFileManager {
+ static let shared = IdeviceLogFileManager()
+ static let logURL = URL.documentsDirectory.appendingPathComponent("idevice_log.txt")
+
+ private static let maximumSize: UInt64 = 20 * 1024 * 1024
+ private static let retainedSize: UInt64 = 10 * 1024 * 1024
+ private static let checkInterval: TimeInterval = 5
+
+ private let queue = DispatchQueue(label: "com.stik.ideviceLogFile", qos: .utility)
+ private var timer: DispatchSourceTimer?
+
+ private init() {}
+
+ func prepareForLogging() {
+ compactIfNeeded()
+
+ queue.async {
+ guard self.timer == nil else { return }
+
+ let timer = DispatchSource.makeTimerSource(queue: self.queue)
+ timer.schedule(deadline: .now() + Self.checkInterval, repeating: Self.checkInterval)
+ timer.setEventHandler { [weak self] in
+ self?.compactIfNeeded()
+ }
+ self.timer = timer
+ timer.resume()
+ }
+ }
+
+ private func compactIfNeeded() {
+ let fileManager = FileManager.default
+ guard let attributes = try? fileManager.attributesOfItem(atPath: Self.logURL.path),
+ let fileSize = attributes[.size] as? NSNumber,
+ fileSize.uint64Value > Self.maximumSize,
+ let handle = try? FileHandle(forUpdating: Self.logURL) else {
+ return
+ }
+
+ defer { try? handle.close() }
+
+ do {
+ let endOffset = try handle.seekToEnd()
+ let startOffset = endOffset > Self.retainedSize ? endOffset - Self.retainedSize : 0
+ try handle.seek(toOffset: startOffset)
+ var recentData = try handle.readToEnd() ?? Data()
+
+ if startOffset > 0, let newlineIndex = recentData.firstIndex(of: 0x0A) {
+ recentData.removeSubrange(recentData.startIndex...newlineIndex)
+ }
+
+ try handle.truncate(atOffset: 0)
+ try handle.seek(toOffset: 0)
+ try handle.write(contentsOf: recentData)
+ try handle.synchronize()
+ } catch {
+ return
+ }
+ }
+}
diff --git a/StikDebug/Services/LocationSimulationSession.swift b/StikDebug/Services/LocationSimulationSession.swift
new file mode 100644
index 0000000..8fef9ae
--- /dev/null
+++ b/StikDebug/Services/LocationSimulationSession.swift
@@ -0,0 +1,68 @@
+import Foundation
+import CoreLocation
+import Combine
+
+final class LocationSimulationSession: ObservableObject {
+ static let shared = LocationSimulationSession()
+
+ @Published private(set) var isActive = false
+ @Published private(set) var coordinate: CLLocationCoordinate2D?
+ @Published private(set) var routeCoordinates: [CLLocationCoordinate2D]?
+ private var resendTimer: Timer?
+ private var routeID: UUID?
+
+ private init() {}
+
+ func start(at coordinate: CLLocationCoordinate2D) {
+ self.coordinate = coordinate
+ if !isActive {
+ isActive = true
+ BackgroundAudioManager.shared.requestStart()
+ BackgroundLocationManager.shared.requestStart()
+ }
+ }
+
+ func startResending(at coordinate: CLLocationCoordinate2D, _ operation: @escaping () -> Void) {
+ start(at: coordinate)
+ resendTimer?.invalidate()
+ let timer = Timer(timeInterval: 4, repeats: true) { _ in operation() }
+ RunLoop.main.add(timer, forMode: .common)
+ resendTimer = timer
+ }
+
+ func startRoute(at coordinate: CLLocationCoordinate2D, coordinates: [CLLocationCoordinate2D]) -> UUID {
+ let routeID = UUID()
+ self.routeID = routeID
+ routeCoordinates = coordinates
+ start(at: coordinate)
+ return routeID
+ }
+
+ func isCurrentRoute(_ routeID: UUID) -> Bool {
+ self.routeID == routeID
+ }
+
+ func clearRoute() {
+ routeID = nil
+ routeCoordinates = nil
+ }
+
+ func pauseResending() {
+ resendTimer?.invalidate()
+ resendTimer = nil
+ }
+
+ func updateCoordinate(_ coordinate: CLLocationCoordinate2D) {
+ self.coordinate = coordinate
+ }
+
+ func stop() {
+ pauseResending()
+ clearRoute()
+ guard isActive else { return }
+ isActive = false
+ coordinate = nil
+ BackgroundAudioManager.shared.requestStop()
+ BackgroundLocationManager.shared.requestStop()
+ }
+}
diff --git a/StikDebug/Services/TunnelManager.swift b/StikDebug/Services/TunnelManager.swift
index 2d0c83a..444b88f 100644
--- a/StikDebug/Services/TunnelManager.swift
+++ b/StikDebug/Services/TunnelManager.swift
@@ -70,12 +70,7 @@ final class TunnelManager: ObservableObject {
}
private func mountDeveloperDiskImageIfNeeded() {
- let trustcachePath = URL.documentsDirectory.appendingPathComponent("DDI/Image.dmg.trustcache").path
- guard FileManager.default.fileExists(atPath: trustcachePath),
- !MountingProgress.shared.coolisMounted,
- MountingProgress.shared.mountingThread == nil else {
- return
- }
+ guard DeveloperDiskImageService.filesAreReady else { return }
MountingProgress.shared.pubMount()
}
diff --git a/StikDebug/Support/AutoScriptAssignments.swift b/StikDebug/Support/AutoScriptAssignments.swift
index 44f740c..57eabd9 100644
--- a/StikDebug/Support/AutoScriptAssignments.swift
+++ b/StikDebug/Support/AutoScriptAssignments.swift
@@ -14,13 +14,17 @@ extension ScriptStore {
appNames: [
"Amethyst",
"MeloNX",
- "Melo",
"XeniOS",
- "MeloCafe",
+ "MeloCafé",
"Manic EMU",
- "Manic",
"DukeX",
"TachyonU",
+ "touchHLE",
+ "HyperHLE",
+ "Applesauce",
+ "RPCS3",
+ "AetherPS4",
+ "Husk"
],
resource: ScriptResource(resourceName: "universal", fileName: "universal.js")
),
diff --git a/StikDebug/Support/PairingFileStore.swift b/StikDebug/Support/PairingFileStore.swift
index 20beccb..283e13e 100644
--- a/StikDebug/Support/PairingFileStore.swift
+++ b/StikDebug/Support/PairingFileStore.swift
@@ -7,14 +7,14 @@ import Foundation
import UniformTypeIdentifiers
enum PairingFileStore {
- static let fileName = "rp_pairing_file.plist"
+ static let fileName = "pairingFile.plist"
static let supportedContentTypes: [UTType] = [
UTType(filenameExtension: "mobiledevicepairing", conformingTo: .data)!,
UTType(filenameExtension: "mobiledevicepair", conformingTo: .data)!,
.propertyList
]
- private static let legacyFileName = "pairingFile.plist"
+ private static let legacyFileName = "rp_pairing_file.plist"
static var url: URL {
directoryURL.appendingPathComponent(fileName)
@@ -25,31 +25,31 @@ enum PairingFileStore {
let destination = url
try? fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true)
- guard !fileManager.fileExists(atPath: destination.path) else {
- removeLegacyCopies(fileManager: fileManager)
+ if let documentURL = documentSourceURL(fileManager: fileManager) {
+ if !fileManager.fileExists(atPath: destination.path) ||
+ !fileManager.contentsEqual(atPath: documentURL.path, andPath: destination.path) {
+ try? replaceItem(at: destination, with: documentURL, fileManager: fileManager)
+ protectPairingFile(at: destination, fileManager: fileManager)
+ }
return destination
}
- migrateLegacyCopy(to: destination, fileManager: fileManager)
+ if !fileManager.fileExists(atPath: destination.path) {
+ migrateLegacyCopy(to: destination, fileManager: fileManager)
+ }
return destination
}
static func replace(with sourceURL: URL, fileManager: FileManager = .default) throws {
- let destination = prepareURL(fileManager: fileManager)
+ try fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true)
- let tempURL = directoryURL.appendingPathComponent(UUID().uuidString + ".tmp")
- try? fileManager.removeItem(at: tempURL)
- try fileManager.copyItem(at: sourceURL, to: tempURL)
- defer { try? fileManager.removeItem(at: tempURL) }
-
- if fileManager.fileExists(atPath: destination.path) {
- _ = try fileManager.replaceItemAt(destination, withItemAt: tempURL)
- } else {
- try fileManager.moveItem(at: tempURL, to: destination)
+ let documentsDestination = documentsURL
+ if sourceURL.standardizedFileURL != documentsDestination.standardizedFileURL {
+ try replaceItem(at: documentsDestination, with: sourceURL, fileManager: fileManager)
}
- removeLegacyCopies(fileManager: fileManager)
- protectPairingFile(at: destination, fileManager: fileManager)
+ try replaceItem(at: url, with: documentsDestination, fileManager: fileManager)
+ protectPairingFile(at: url, fileManager: fileManager)
}
static func importFromPicker(_ sourceURL: URL, fileManager: FileManager = .default) throws {
@@ -83,19 +83,33 @@ enum PairingFileStore {
private static var legacyURLs: [URL] {
[
- URL.documentsDirectory.appendingPathComponent(fileName),
+ directoryURL.appendingPathComponent(legacyFileName),
+ documentsURL,
URL.documentsDirectory.appendingPathComponent(legacyFileName)
]
}
+ private static var documentsURL: URL {
+ URL.documentsDirectory.appendingPathComponent(fileName)
+ }
+
+ private static func documentSourceURL(fileManager: FileManager) -> URL? {
+ [documentsURL, URL.documentsDirectory.appendingPathComponent(legacyFileName)]
+ .filter { fileManager.fileExists(atPath: $0.path) }
+ .max {
+ let firstDate = (try? $0.resourceValues(forKeys: [.contentModificationDateKey]))?.contentModificationDate ?? .distantPast
+ let secondDate = (try? $1.resourceValues(forKeys: [.contentModificationDateKey]))?.contentModificationDate ?? .distantPast
+ return firstDate < secondDate
+ }
+ }
+
private static func migrateLegacyCopy(to destination: URL, fileManager: FileManager) {
for legacyURL in legacyURLs where fileManager.fileExists(atPath: legacyURL.path) {
do {
- try fileManager.moveItem(at: legacyURL, to: destination)
+ try replaceItem(at: destination, with: legacyURL, fileManager: fileManager)
} catch {
if let data = try? Data(contentsOf: legacyURL) {
try? data.write(to: destination, options: .atomic)
- try? fileManager.removeItem(at: legacyURL)
}
}
@@ -104,6 +118,21 @@ enum PairingFileStore {
}
}
+ private static func replaceItem(at destination: URL, with source: URL, fileManager: FileManager) throws {
+ let temporary = destination
+ .deletingLastPathComponent()
+ .appendingPathComponent(UUID().uuidString + ".tmp")
+ try? fileManager.removeItem(at: temporary)
+ try fileManager.copyItem(at: source, to: temporary)
+ defer { try? fileManager.removeItem(at: temporary) }
+
+ if fileManager.fileExists(atPath: destination.path) {
+ _ = try fileManager.replaceItemAt(destination, withItemAt: temporary)
+ } else {
+ try fileManager.moveItem(at: temporary, to: destination)
+ }
+ }
+
private static func removeLegacyCopies(fileManager: FileManager) {
for legacyURL in legacyURLs where fileManager.fileExists(atPath: legacyURL.path) {
try? fileManager.removeItem(at: legacyURL)
diff --git a/StikDebug/Support/UserDefaults+Keys.swift b/StikDebug/Support/UserDefaults+Keys.swift
index 85487e0..6f95a66 100644
--- a/StikDebug/Support/UserDefaults+Keys.swift
+++ b/StikDebug/Support/UserDefaults+Keys.swift
@@ -15,5 +15,9 @@ extension UserDefaults {
static let defaultScriptName = "DefaultScriptName"
static let defaultScriptNameValue = ""
static let targetDeviceIP = "TunnelDeviceIP"
+ /// Attaches processes with MallocGuardEdges and MallocScribble.
+ static let mallocDebug = "enableMallocDebug"
+ static let mallocGuardEdges = "enableMallocGuardEdges"
+ static let mallocScribble = "enableMallocScribble"
}
}
diff --git a/StikDebug/Views/ConsoleLogsView.swift b/StikDebug/Views/ConsoleLogsView.swift
index 10db4c4..81510a6 100644
--- a/StikDebug/Views/ConsoleLogsView.swift
+++ b/StikDebug/Views/ConsoleLogsView.swift
@@ -266,7 +266,7 @@ struct ConsoleLogsView: View {
@ViewBuilder
private var exportMenuOption: some View {
- let logURL: URL = URL.documentsDirectory.appendingPathComponent("idevice_log.txt")
+ let logURL = IdeviceLogFileManager.logURL
if FileManager.default.fileExists(atPath: logURL.path) {
ShareLink(
item: logURL,
@@ -309,7 +309,7 @@ struct ConsoleLogsView: View {
guard !isLoadingLogs else { return }
isLoadingLogs = true
- let logPath = URL.documentsDirectory.appendingPathComponent("idevice_log.txt").path
+ let logPath = IdeviceLogFileManager.logURL.path
guard FileManager.default.fileExists(atPath: logPath) else {
await MainActor.run {
@@ -368,7 +368,7 @@ struct ConsoleLogsView: View {
guard !isLoadingLogs else { return }
isLoadingLogs = true
- let logPath = URL.documentsDirectory.appendingPathComponent("idevice_log.txt").path
+ let logPath = IdeviceLogFileManager.logURL.path
let previousCount = lastProcessedLineCount
guard FileManager.default.fileExists(atPath: logPath) else {
diff --git a/StikDebug/Views/HomeView.swift b/StikDebug/Views/HomeView.swift
index 363ba80..48ec835 100644
--- a/StikDebug/Views/HomeView.swift
+++ b/StikDebug/Views/HomeView.swift
@@ -12,17 +12,14 @@ struct HomeView: View {
@AppStorage("bundleID") private var bundleID: String = ""
@AppStorage(UserDefaults.Keys.confirmExternalJITRequests) private var confirmExternalJITRequests = true
- @ObservedObject private var mounting = MountingProgress.shared
-
@State private var hasAppeared = false
@State private var pendingJITEnableConfiguration: JITEnableConfiguration?
@State private var isShowingPairingFilePicker = false
@State private var debugFeedback: DebugFeedback?
@State private var pendingExternalURLAction: HomeExternalAction?
+ @State private var pendingLiveContainerApp: LiveContainerApp?
@State private var scriptRunModel: RunJSViewModel?
- private let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
-
private struct DebugFeedback: Identifiable {
let id = UUID()
let message: String
@@ -30,11 +27,18 @@ struct HomeView: View {
let isWorking: Bool
}
+ private struct LiveContainerApp {
+ let bundleID: String
+ let name: String
+ }
+
var body: some View {
InstalledAppsListView(onSelectApp: { selectedBundle, selectedName in
- bundleID = selectedBundle
- Haptics.medium()
- startJITInBackground(bundleID: selectedBundle, displayName: selectedName)
+ if selectedName.localizedCaseInsensitiveContains("LiveContainer") {
+ pendingLiveContainerApp = LiveContainerApp(bundleID: selectedBundle, name: selectedName)
+ } else {
+ enableJITForSelectedApp(bundleID: selectedBundle, name: selectedName)
+ }
}, showDoneButton: false, onImportPairingFile: { isShowingPairingFilePicker = true })
.overlay(alignment: .bottom) {
if let debugFeedback {
@@ -45,9 +49,6 @@ struct HomeView: View {
}
.onAppear(perform: handleAppear)
.onReceive(NotificationCenter.default.publisher(for: .intentJSScriptReady), perform: handleScriptReadyNotification)
- .onReceive(timer) { _ in
- refreshMountStatusIfNeeded()
- }
.onOpenURL { url in
handleExternalURL(url)
}
@@ -74,6 +75,28 @@ struct HomeView: View {
} message: { action in
Text(action.message)
}
+ .alert(
+ "Enable JIT for LiveContainer?",
+ isPresented: Binding(
+ get: { pendingLiveContainerApp != nil },
+ set: { isPresented in
+ if !isPresented {
+ pendingLiveContainerApp = nil
+ }
+ }
+ ),
+ presenting: pendingLiveContainerApp
+ ) { app in
+ Button("Cancel", role: .cancel) {
+ pendingLiveContainerApp = nil
+ }
+ Button("Proceed") {
+ pendingLiveContainerApp = nil
+ enableJITForSelectedApp(bundleID: app.bundleID, name: app.name)
+ }
+ } message: { _ in
+ Text("StikDebug isn't meant to enable JIT for LiveContainer manually. Select StikDebug in LiveContainer settings, enable \"Run with JIT\" for your app, then open your app normally. It will automatically jump back here.")
+ }
.fileImporter(
isPresented: $isShowingPairingFilePicker,
allowedContentTypes: PairingFileStore.supportedContentTypes,
@@ -92,9 +115,14 @@ struct HomeView: View {
}
}
+ private func enableJITForSelectedApp(bundleID selectedBundle: String, name: String) {
+ bundleID = selectedBundle
+ Haptics.medium()
+ startJITInBackground(bundleID: selectedBundle, displayName: name)
+ }
+
private func handleAppear() {
startTunnelInBackground()
- MountingProgress.shared.checkforMounted()
hasAppeared = true
if let config = pendingJITEnableConfiguration {
@@ -117,13 +145,6 @@ struct HomeView: View {
scriptRunModel = model
}
- private func refreshMountStatusIfNeeded() {
- guard mounting.mountingThread == nil, !mounting.coolisMounted else {
- return
- }
- MountingProgress.shared.checkforMounted()
- }
-
private func importPairingFile(_ result: Result) {
switch result {
case .success(let url):
diff --git a/StikDebug/Views/InstalledAppsListView.swift b/StikDebug/Views/InstalledAppsListView.swift
index 69437a6..3f22192 100644
--- a/StikDebug/Views/InstalledAppsListView.swift
+++ b/StikDebug/Views/InstalledAppsListView.swift
@@ -83,7 +83,9 @@ struct InstalledAppsListView: View {
handleLoadingChange(isLoading)
}
.onReceive(NotificationCenter.default.publisher(for: .pairingFileImported)) { _ in
- viewModel.refreshAppLists()
+ if TunnelManager.shared.isConnected {
+ viewModel.refreshAppLists()
+ }
}
}
diff --git a/StikDebug/Views/MainTabView.swift b/StikDebug/Views/MainTabView.swift
index 17fbb16..f175b75 100644
--- a/StikDebug/Views/MainTabView.swift
+++ b/StikDebug/Views/MainTabView.swift
@@ -7,6 +7,7 @@
import SwiftUI
import Foundation
+import CoreLocation
private enum ExternalLocationAction: Identifiable {
case simulate(URL, Double, Double)
@@ -223,7 +224,22 @@ struct MainTabView: View {
DispatchQueue.main.async {
if code == 0 {
- BackgroundLocationManager.shared.requestStart()
+ LocationSimulationSession.shared.clearRoute()
+ LocationSimulationSession.shared.startResending(
+ at: CLLocationCoordinate2D(
+ latitude: coordinate.latitude,
+ longitude: coordinate.longitude
+ )
+ ) {
+ LocationSimulationCommandQueue.shared.async {
+ _ = simulate_location(
+ DeviceConnectionContext.targetIPAddress,
+ coordinate.latitude,
+ coordinate.longitude,
+ pairingFile.path
+ )
+ }
+ }
LogManager.shared.addInfoLog(
String(format: "Simulated location from URL: %.6f, %.6f", coordinate.latitude, coordinate.longitude)
)
@@ -243,7 +259,7 @@ struct MainTabView: View {
let code = clear_simulated_location()
DispatchQueue.main.async {
if code == 0 {
- BackgroundLocationManager.shared.requestStop()
+ LocationSimulationSession.shared.stop()
LogManager.shared.addInfoLog("Cleared simulated location from URL")
} else {
showAlert(
diff --git a/StikDebug/Views/MapSelectionView.swift b/StikDebug/Views/MapSelectionView.swift
index 037f9f4..95f46ce 100644
--- a/StikDebug/Views/MapSelectionView.swift
+++ b/StikDebug/Views/MapSelectionView.swift
@@ -724,11 +724,11 @@ final class LocationSearchCompleter: NSObject, ObservableObject, MKLocalSearchCo
}
struct LocationSimulationView: View {
+ @ObservedObject private var simulationSession = LocationSimulationSession.shared
@State private var coordinate: CLLocationCoordinate2D?
@State private var position: MapCameraPosition = .userLocation(fallback: .automatic)
@State private var backgroundTaskID: UIBackgroundTaskIdentifier = .invalid
- @State private var resendTimer: Timer?
@State private var routeLoadTask: Task?
@State private var routeSpeedPrefetchTask: Task?
@State private var routePlaybackTask: Task?
@@ -788,11 +788,11 @@ struct LocationSimulationView: View {
}
private var hasActiveSimulation: Bool {
- simulatedCoordinate != nil || routePlaybackTask != nil
+ simulationSession.isActive || simulatedCoordinate != nil || routePlaybackTask != nil
}
private var isRouteRunning: Bool {
- routePlaybackTask != nil
+ routePlaybackTask != nil || simulationSession.routeCoordinates != nil
}
private var hasRouteContext: Bool {
@@ -801,7 +801,12 @@ struct LocationSimulationView: View {
routePlan != nil ||
isLoadingRoute ||
isPrefetchingRouteSpeeds ||
- routePlaybackCoordinate != nil
+ routePlaybackCoordinate != nil ||
+ simulationSession.routeCoordinates != nil
+ }
+
+ private var displayedRouteCoordinate: CLLocationCoordinate2D? {
+ simulationSession.routeCoordinates == nil ? routePlaybackCoordinate : simulationSession.coordinate
}
private var routeSummaryText: String? {
@@ -818,6 +823,9 @@ struct LocationSimulationView: View {
}
private var routeStatusText: String {
+ if simulationSession.routeCoordinates != nil {
+ return "Route simulation active."
+ }
if isLoadingRoute {
return "Calculating route…"
}
@@ -892,8 +900,8 @@ struct LocationSimulationView: View {
Marker("End", coordinate: routeEndCoordinate)
.tint(.red)
}
- if let routePlaybackCoordinate {
- Marker("Current", coordinate: routePlaybackCoordinate)
+ if let displayedRouteCoordinate {
+ Marker("Current", coordinate: displayedRouteCoordinate)
.tint(.blue)
}
} else if let coordinate {
@@ -1025,17 +1033,28 @@ struct LocationSimulationView: View {
}
.onAppear {
loadBookmarks()
+ if let routeCoordinates = simulationSession.routeCoordinates {
+ coordinate = nil
+ routePolyline = makeRoutePolyline(for: routeCoordinates)
+ routeStartSelection = routeCoordinates.first.map {
+ RouteSearchSelection(title: "Route Start", coordinate: $0)
+ }
+ routeEndSelection = routeCoordinates.last.map {
+ RouteSearchSelection(title: "Route End", coordinate: $0)
+ }
+ if let routePolyline {
+ position = .rect(routePolyline.boundingMapRect)
+ }
+ } else if let activeCoordinate = simulationSession.coordinate {
+ coordinate = activeCoordinate
+ simulatedCoordinate = activeCoordinate
+ }
}
.onDisappear {
routeLoadTask?.cancel()
routeLoadTask = nil
routeSpeedPrefetchTask?.cancel()
routeSpeedPrefetchTask = nil
- cancelRoutePlayback(resetMarker: true)
- stopResendLoop()
- if backgroundTaskID != .invalid {
- BackgroundLocationManager.shared.requestStop()
- }
endBackgroundTask()
}
}
@@ -1297,14 +1316,14 @@ struct LocationSimulationView: View {
) {
routePlaybackCoordinate = nil
beginBackgroundTask()
+ LocationSimulationSession.shared.clearRoute()
startResendLoop(with: coord)
- BackgroundLocationManager.shared.requestStart()
}
}
private func simulateRoute() {
guard pairingExists,
- routePlan != nil,
+ let routePlan,
let firstCoordinate = routePlaybackSamples.first?.coordinate,
!isBusy else {
return
@@ -1319,10 +1338,13 @@ struct LocationSimulationView: View {
operation: { locationUpdateCode(for: firstCoordinate) }
) {
beginBackgroundTask()
- BackgroundLocationManager.shared.requestStart()
+ let routeID = LocationSimulationSession.shared.startRoute(
+ at: firstCoordinate,
+ coordinates: routePlan.displayCoordinates
+ )
simulatedCoordinate = nil
routePlaybackCoordinate = firstCoordinate
- startRoutePlayback()
+ startRoutePlayback(routeID: routeID)
}
}
@@ -1356,13 +1378,14 @@ struct LocationSimulationView: View {
routeSpeedPrefetchTask = nil
cancelRoutePlayback(resetMarker: true)
stopResendLoop()
+ LocationSimulationSession.shared.clearRoute()
runLocationCommand(
errorTitle: "Clear Failed",
errorMessage: { code in "Could not clear simulated location (error \(code))." },
operation: clear_simulated_location
) {
endBackgroundTask()
- BackgroundLocationManager.shared.requestStop()
+ LocationSimulationSession.shared.stop()
}
}
@@ -1379,18 +1402,15 @@ struct LocationSimulationView: View {
private func startResendLoop(with coordinate: CLLocationCoordinate2D) {
simulatedCoordinate = coordinate
- resendTimer?.invalidate()
- resendTimer = Timer.scheduledTimer(withTimeInterval: 4, repeats: true) { _ in
- guard let simulatedCoordinate else { return }
+ LocationSimulationSession.shared.startResending(at: coordinate) {
LocationSimulationCommandQueue.shared.async {
- _ = locationUpdateCode(for: simulatedCoordinate)
+ _ = locationUpdateCode(for: coordinate)
}
}
}
private func stopResendLoop() {
- resendTimer?.invalidate()
- resendTimer = nil
+ LocationSimulationSession.shared.pauseResending()
simulatedCoordinate = nil
}
@@ -1520,15 +1540,19 @@ struct LocationSimulationView: View {
}
}
- private func startRoutePlayback() {
+ private func startRoutePlayback(routeID: UUID) {
routePlaybackTask = Task {
var lastSuccessfulCoordinate = routePlaybackSamples.first?.coordinate
for sample in routePlaybackSamples.dropFirst() {
try? await Task.sleep(for: .seconds(sample.delayFromPrevious))
- guard !Task.isCancelled else { return }
+ guard !Task.isCancelled,
+ LocationSimulationSession.shared.isCurrentRoute(routeID) else {
+ return
+ }
let code = await sendLocationUpdate(for: sample.coordinate)
+ guard LocationSimulationSession.shared.isCurrentRoute(routeID) else { return }
guard code == 0 else {
await MainActor.run {
routePlaybackTask = nil
@@ -1546,10 +1570,12 @@ struct LocationSimulationView: View {
lastSuccessfulCoordinate = sample.coordinate
await MainActor.run {
routePlaybackCoordinate = sample.coordinate
+ LocationSimulationSession.shared.updateCoordinate(sample.coordinate)
}
}
await MainActor.run {
+ guard LocationSimulationSession.shared.isCurrentRoute(routeID) else { return }
routePlaybackTask = nil
if let lastSuccessfulCoordinate {
routePlaybackCoordinate = lastSuccessfulCoordinate
diff --git a/StikDebug/Views/SettingsView.swift b/StikDebug/Views/SettingsView.swift
index b2ef6a0..fc4bc3e 100644
--- a/StikDebug/Views/SettingsView.swift
+++ b/StikDebug/Views/SettingsView.swift
@@ -7,8 +7,8 @@ import SwiftUI
import UIKit
private enum SettingsLinks {
- static let githubStars = URL(string: "https://github.com/StephenDev0/StikDebug/stargazers")!
- static let pairingFileGuide = URL(string: "https://github.com/StephenDev0/StikDebug-Guide/blob/main/pairing_file.md")!
+ static let githubStars = URL(string: "https://github.com/StikDebug/StikDebug")!
+ static let pairingFileGuide = URL(string: "https://github.com/StikDebug/StikDebug-Guide/blob/main/pairing_file.md")!
static let localDevVPN = URL(string: "https://apps.apple.com/us/app/localdevvpn/id6755608044")!
static let discord = URL(string: "https://discord.gg/qahjXNTDwS")!
}
@@ -19,6 +19,7 @@ struct SettingsView: View {
@AppStorage("keepAliveAudio") private var keepAliveAudio = true
@AppStorage("keepAliveLocation") private var keepAliveLocation = true
@AppStorage(UserDefaults.Keys.targetDeviceIP) private var targetDeviceIP = DeviceConnectionContext.defaultTargetIPAddress
+ @AppStorage(UserDefaults.Keys.mallocDebug) private var mallocDebug = false
@State private var isShowingPairingFilePicker = false
@State private var isImportingFile = false
@@ -94,8 +95,7 @@ struct SettingsView: View {
}
}
.onChange(of: keepAliveAudio) { _, enabled in
- if enabled { BackgroundAudioManager.shared.start() }
- else { BackgroundAudioManager.shared.stop() }
+ handleAudioKeepAliveChange(enabled)
}
Toggle(isOn: $keepAliveLocation) {
@@ -106,7 +106,7 @@ struct SettingsView: View {
}
}
.onChange(of: keepAliveLocation) { _, enabled in
- if !enabled { BackgroundLocationManager.shared.stop() }
+ handleLocationKeepAliveChange(enabled)
}
} header: {
@@ -131,6 +131,16 @@ struct SettingsView: View {
}
}
+ Section("Debugging") {
+ Toggle(isOn: $mallocDebug) {
+ VStack(alignment: .leading, spacing: 2) {
+ Text("Malloc Debugging")
+ Text("Attaches processes with MallocGuardEdges and MallocScribble to detect memory corruption.")
+ .font(.caption).foregroundStyle(.secondary)
+ }
+ }
+ }
+
Section("Advanced") {
HStack {
Text("Target Device IP")
@@ -231,6 +241,25 @@ struct SettingsView: View {
return "Version \(appVersion) • iOS \(UIDevice.current.systemVersion) • \(txmLabel)"
}
+ private func handleAudioKeepAliveChange(_ enabled: Bool) {
+ if !enabled, !keepAliveLocation {
+ keepAliveLocation = true
+ BackgroundLocationManager.shared.requestAuthorizationIfNeeded()
+ }
+ BackgroundAudioManager.shared.configurationDidChange()
+ BackgroundLocationManager.shared.configurationDidChange()
+ }
+
+ private func handleLocationKeepAliveChange(_ enabled: Bool) {
+ if enabled {
+ BackgroundLocationManager.shared.requestAuthorizationIfNeeded()
+ } else if !keepAliveAudio {
+ keepAliveAudio = true
+ }
+ BackgroundAudioManager.shared.configurationDidChange()
+ BackgroundLocationManager.shared.configurationDidChange()
+ }
+
// MARK: - Business Logic
private func openAppFolder() {
diff --git a/StikDebug/idevice/idevice.h b/StikDebug/idevice/idevice.h
index b429ce2..2aef885 100644
--- a/StikDebug/idevice/idevice.h
+++ b/StikDebug/idevice/idevice.h
@@ -27,6 +27,26 @@
#define LOCKDOWN_PORT 62078
+/**
+ * The nonce domain index cryptexes are personalized against
+ */
+#define IDEVICE_CRYPTEXD_NONCE_DOMAIN_CRYPTEX 2
+
+/**
+ * The `image-type-index` a DeveloperDiskImage install uses
+ */
+#define IDEVICE_CRYPTEXD_DDI_IMAGE_TYPE_INDEX 10
+
+/**
+ * The `persistence` a DeveloperDiskImage install uses
+ */
+#define IDEVICE_CRYPTEXD_DDI_PERSISTENCE 2
+
+/**
+ * The `nonce-persistence` a DeveloperDiskImage install uses
+ */
+#define IDEVICE_CRYPTEXD_DDI_NONCE_PERSISTENCE 1
+
typedef enum AfcFopenMode {
AfcRdOnly = 1,
AfcRw = 2,
@@ -44,6 +64,46 @@ typedef enum AfcLinkType {
Symbolic = 2,
} AfcLinkType;
+/**
+ * The system's light/dark appearance
+ */
+typedef enum IdeviceUserInterfaceStyle {
+ IdeviceUserInterfaceStyleLight = 0,
+ IdeviceUserInterfaceStyleDark = 1,
+} IdeviceUserInterfaceStyle;
+
+/**
+ * Which of the device's filesystem domains a session is scoped to
+ */
+typedef enum IdeviceFileServiceDomain {
+ /**
+ * An app's own data container. The identifier is the bundle ID.
+ */
+ IdeviceFileServiceDomainAppDataContainer = 1,
+ /**
+ * A shared app-group container. The identifier is the group ID.
+ */
+ IdeviceFileServiceDomainAppGroupDataContainer = 2,
+ /**
+ * The temporary directory.
+ */
+ IdeviceFileServiceDomainTemporary = 3,
+ /**
+ * The system crash-log store.
+ */
+ IdeviceFileServiceDomainSystemCrashLogs = 5,
+} IdeviceFileServiceDomain;
+
+/**
+ * Network event type discriminant
+ */
+typedef enum IdeviceNetworkEventType {
+ InterfaceDetection = 0,
+ ConnectionDetection = 1,
+ ConnectionUpdate = 2,
+ Unknown = 255,
+} IdeviceNetworkEventType;
+
typedef enum IdeviceLoggerError {
Success = 0,
FileError = -1,
@@ -60,6 +120,20 @@ typedef enum IdeviceLogLevel {
Trace = 5,
} IdeviceLogLevel;
+/**
+ * The outcome of a `CreateStashbag` request.
+ */
+typedef enum IdeviceStashbagOutcome {
+ /**
+ * The device does not need a stashbag; nothing further to do.
+ */
+ NotRequired = 0,
+ /**
+ * A stashbag was created and must be committed with the AP ticket.
+ */
+ CommitRequired = 1,
+} IdeviceStashbagOutcome;
+
typedef struct AdapterHandle AdapterHandle;
typedef struct AdapterStreamHandle AdapterStreamHandle;
@@ -78,19 +152,53 @@ typedef struct AmfiClientHandle AmfiClientHandle;
*/
typedef struct AppServiceHandle AppServiceHandle;
+/**
+ * Opaque handle to an ApplicationListingClient
+ */
+typedef struct ApplicationListingHandle ApplicationListingHandle;
+
typedef struct BtPacketLoggerClientHandle BtPacketLoggerClientHandle;
typedef struct CompanionProxyClientHandle CompanionProxyClientHandle;
+/**
+ * Opaque handle to a ConditionInducerClient
+ */
+typedef struct ConditionInducerHandle ConditionInducerHandle;
+
+/**
+ * Opaque handle to a ConfigurationServiceClient
+ */
+typedef struct ConfigurationServiceHandle ConfigurationServiceHandle;
+
typedef struct CoreDeviceProxyHandle CoreDeviceProxyHandle;
typedef struct CrashReportCopyMobileHandle CrashReportCopyMobileHandle;
+/**
+ * Opaque handle to the payloads a Cryptex1 DeveloperDiskImage install needs
+ */
+typedef struct Cryptex1AssetsHandle Cryptex1AssetsHandle;
+
+/**
+ * Opaque handle to a CryptexdClient
+ *
+ * The daemon serves one routine per connection, so every call below consumes
+ * the handle: it is freed by the call and must not be used again, even when
+ * the call fails.
+ */
+typedef struct CryptexdHandle CryptexdHandle;
+
/**
* Opaque handle to a DebugProxyClient
*/
typedef struct DebugProxyHandle DebugProxyHandle;
+/**
+ * Opaque handle to a DeviceInfoClient
+ */
+typedef struct DeviceInfoHandle DeviceInfoHandle;
+
typedef struct DiagnosticsRelayClientHandle DiagnosticsRelayClientHandle;
/**
@@ -98,10 +206,24 @@ typedef struct DiagnosticsRelayClientHandle DiagnosticsRelayClientHandle;
*/
typedef struct DiagnosticsServiceHandle DiagnosticsServiceHandle;
+typedef struct EnergyMonitorHandle EnergyMonitorHandle;
+
+/**
+ * Opaque handle to a FileServiceClient
+ */
+typedef struct FileServiceHandle FileServiceHandle;
+
+typedef struct GraphicsHandle GraphicsHandle;
+
typedef struct HeartbeatClientHandle HeartbeatClientHandle;
typedef struct HouseArrestClientHandle HouseArrestClientHandle;
+/**
+ * Opaque handle to an IconServiceClient
+ */
+typedef struct IconServiceHandle IconServiceHandle;
+
/**
* Opaque C-compatible handle to an Idevice connection
*/
@@ -114,6 +236,16 @@ typedef struct IdevicePairingFile IdevicePairingFile;
typedef struct IdeviceProviderHandle IdeviceProviderHandle;
+/**
+ * An opaque, shareable cancellation flag for an in-flight restore.
+ *
+ * Create one with `idevice_restore_cancel_handle_new`, pass it to
+ * `idevice_restore_run`, and call `idevice_restore_cancel` from another thread to
+ * request a graceful cancel (the device is rebooted toward recovery). Free it with
+ * `idevice_restore_cancel_handle_free` once the restore has returned.
+ */
+typedef struct IdeviceRestoreCancelHandle IdeviceRestoreCancelHandle;
+
typedef struct IdeviceSocketHandle IdeviceSocketHandle;
typedef struct ImageMounterHandle ImageMounterHandle;
@@ -122,6 +254,11 @@ typedef struct InstallationProxyClientHandle InstallationProxyClientHandle;
typedef struct InstallcoordinationProxyHandle InstallcoordinationProxyHandle;
+/**
+ * Opaque handle to an opened IPSW archive.
+ */
+typedef struct IpswHandle IpswHandle;
+
/**
* Opaque handle to a ProcessControlClient
*/
@@ -141,12 +278,34 @@ typedef struct MobileActivationdClientHandle MobileActivationdClientHandle;
typedef struct MobileBackup2ClientHandle MobileBackup2ClientHandle;
+/**
+ * Opaque handle to a NetworkMonitorClient
+ */
+typedef struct NetworkMonitorHandle NetworkMonitorHandle;
+
typedef struct NotificationProxyClientHandle NotificationProxyClientHandle;
+typedef struct NotificationsHandle NotificationsHandle;
+
typedef struct OsTraceRelayClientHandle OsTraceRelayClientHandle;
typedef struct OsTraceRelayReceiverHandle OsTraceRelayReceiverHandle;
+/**
+ * Opaque cancellation token for [`pairable_host_accept`].
+ *
+ * Create one with `pairable_host_cancel_new`, hand it to `pairable_host_accept`,
+ * and call `pairable_host_cancel_signal` from any other thread to abort the wait.
+ * Free it with `pairable_host_cancel_free` once the accept has returned.
+ */
+typedef struct PairableHostCancel PairableHostCancel;
+
+/**
+ * Opaque handle holding a generated host identity between
+ * `pairable_host_prepare` and `pairable_host_accept_fd`.
+ */
+typedef struct PairableHostHandle PairableHostHandle;
+
typedef struct PcapdClientHandle PcapdClientHandle;
typedef struct PreboardServiceClientHandle PreboardServiceClientHandle;
@@ -158,6 +317,21 @@ typedef struct ProcessControlHandle ProcessControlHandle;
typedef struct ReadWriteOpaque ReadWriteOpaque;
+/**
+ * Opaque handle to a device in recovery/DFU mode.
+ */
+typedef struct RecoveryDeviceHandle RecoveryDeviceHandle;
+
+/**
+ * Opaque handle to the RemoteXPC-native notification proxy (iOS 17+)
+ */
+typedef struct RemoteNotificationProxyClientHandle RemoteNotificationProxyClientHandle;
+
+/**
+ * Opaque handle to a remote pairing client speaking `RPPairing` over lockdown
+ */
+typedef struct RemotePairingLockdownHandle RemotePairingLockdownHandle;
+
/**
* Opaque handle to a RemoteServerClient
*/
@@ -165,6 +339,11 @@ typedef struct RemoteServerHandle RemoteServerHandle;
typedef struct RestoreServiceClientHandle RestoreServiceClientHandle;
+/**
+ * Opaque handle to a restore-mode `com.apple.mobile.restored` client.
+ */
+typedef struct RestoredClientHandle RestoredClientHandle;
+
/**
* Opaque handle to an RPPairing file
*/
@@ -191,6 +370,11 @@ typedef struct SysdiagnoseStreamHandle SysdiagnoseStreamHandle;
typedef struct SyslogRelayClientHandle SyslogRelayClientHandle;
+/**
+ * Opaque handle to a SysmontapClient
+ */
+typedef struct SysmontapHandle SysmontapHandle;
+
typedef struct TcpEatObject TcpEatObject;
typedef struct TcpFeedObject TcpFeedObject;
@@ -205,6 +389,20 @@ typedef struct UsbmuxdListenerHandle UsbmuxdListenerHandle;
typedef struct Vec_u64 Vec_u64;
+/**
+ * Opaque handle wrapping a [`WdaBridge`].
+ */
+typedef struct WdaBridgeHandle WdaBridgeHandle;
+
+/**
+ * Opaque handle wrapping the WDA client state.
+ *
+ * The handle owns the provider so that subsequent calls can open fresh
+ * per-request connections without the caller juggling a separate
+ * `IdeviceProviderHandle`.
+ */
+typedef struct WdaClientHandle WdaClientHandle;
+
typedef struct IdeviceFfiError {
int32_t code;
int32_t sub_code;
@@ -316,16 +514,117 @@ typedef struct SignalResponseC {
} SignalResponseC;
/**
- * C-compatible icon data
+ * The accessibility color filter's state
*/
-typedef struct IconDataC {
- uint8_t *data;
- uintptr_t data_len;
- double icon_width;
- double icon_height;
- double minimum_width;
- double minimum_height;
-} IconDataC;
+typedef struct ColorFilterC {
+ int enabled;
+ /**
+ * The filter preset's name, or NULL if the device didn't report one.
+ * Free with `idevice_string_free`.
+ */
+ char *filter_type;
+ /**
+ * Filter strength, 0.0 to 1.0. Only meaningful when `has_intensity` is 1.
+ */
+ double intensity;
+ int has_intensity;
+} ColorFilterC;
+
+/**
+ * A rendered app icon
+ */
+typedef struct AppIconC {
+ /**
+ * PNG-encoded image data
+ */
+ uint8_t *png_data;
+ uintptr_t png_data_len;
+ /**
+ * Icon dimensions in pixels, i.e. the points multiplied by the scale
+ */
+ double pixel_width;
+ double pixel_height;
+ /**
+ * Icon dimensions in points, as actually rendered. May be smaller than
+ * what was requested.
+ */
+ double width;
+ double height;
+ double scale;
+ /**
+ * 1 when the device had no real icon for the app and rendered a generic
+ * placeholder instead
+ */
+ int is_placeholder;
+} AppIconC;
+
+/**
+ * A cryptex installed on the device
+ */
+typedef struct InstalledCryptexC {
+ /**
+ * Free with `idevice_string_free`
+ */
+ char *identifier;
+ /**
+ * Free with `idevice_string_free`
+ */
+ char *version;
+} InstalledCryptexC;
+
+/**
+ * Which nonce domain a get-nonce or roll-nonce request refers to
+ */
+typedef struct CryptexNonceDomain {
+ /**
+ * When 1, `value` is a nonce domain handle, e.g. a build identity's
+ * `Cryptex1,NonceDomain`. When 0, it is a domain index, e.g.
+ * `IDEVICE_CRYPTEXD_NONCE_DOMAIN_CRYPTEX`.
+ */
+ int is_handle;
+ uint64_t value;
+} CryptexNonceDomain;
+
+/**
+ * The payloads and parameters one install needs
+ */
+typedef struct CryptexInstallRequestC {
+ /**
+ * The cryptex disk image, i.e. the manifest's `Cryptex1,GenericDmg`
+ */
+ const uint8_t *image;
+ uintptr_t image_len;
+ /**
+ * `Cryptex1,GenericTrustCache`
+ */
+ const uint8_t *trustcache;
+ uintptr_t trustcache_len;
+ /**
+ * The Cryptex1 personalization ticket
+ */
+ const uint8_t *im4m;
+ uintptr_t im4m_len;
+ /**
+ * `Cryptex1,CryptexInfoPlist`, which names and versions the cryptex
+ */
+ const uint8_t *info;
+ uintptr_t info_len;
+ /**
+ * `Cryptex1,GenericVolume` root hash
+ */
+ const uint8_t *volumehash;
+ uintptr_t volumehash_len;
+ /**
+ * The `Cryptex1,*` parameters from the build identity, as a plist
+ * dictionary. Non-negative integers are sent as uint64, which the daemon
+ * requires.
+ */
+ plist_t cryptex1_properties;
+ int64_t image_type_index;
+ uint64_t persistence;
+ uint64_t nonce_persistence;
+ uint64_t auth;
+} CryptexInstallRequestC;
/**
* Represents a debugserver command
@@ -336,6 +635,178 @@ typedef struct DebugserverCommandHandle {
uintptr_t argv_count;
} DebugserverCommandHandle;
+/**
+ * A notification from the mobile notifications instruments channel
+ */
+typedef struct IdeviceNotificationInfo {
+ char *notification_type;
+ int64_t mach_absolute_time;
+ char *exec_name;
+ char *app_name;
+ uint32_t pid;
+ char *state_description;
+} IdeviceNotificationInfo;
+
+/**
+ * A single condition profile
+ */
+typedef struct IdeviceConditionProfile {
+ char *identifier;
+ char *description;
+} IdeviceConditionProfile;
+
+/**
+ * A condition inducer group containing profiles
+ */
+typedef struct IdeviceConditionGroup {
+ char *identifier;
+ struct IdeviceConditionProfile *profiles;
+ uintptr_t profiles_count;
+} IdeviceConditionGroup;
+
+/**
+ * A running process on the device
+ */
+typedef struct IdeviceRunningProcess {
+ uint32_t pid;
+ char *name;
+ char *real_app_name;
+ bool is_application;
+ uint64_t start_page_count;
+} IdeviceRunningProcess;
+
+/**
+ * A parsed per-PID energy sample
+ */
+typedef struct IdeviceEnergySample {
+ uint32_t pid;
+ int64_t timestamp;
+ double total_energy;
+ double cpu_energy;
+ double gpu_energy;
+ double networking_energy;
+ double display_energy;
+ double location_energy;
+ double appstate_energy;
+} IdeviceEnergySample;
+
+/**
+ * A graphics sample from tddhe GPU instruments channel
+ */
+typedef struct IdeviceGraphicsSample {
+ uint64_t timestamp;
+ double fps;
+ uint64_t alloc_system_memory;
+ uint64_t in_use_system_memory;
+ uint64_t in_use_system_memory_driver;
+ char *gpu_bundle_name;
+ uint64_t recovery_count;
+} IdeviceGraphicsSample;
+
+/**
+ * A socket address (IPv4 or IPv6), represented as a null-terminated string + port
+ */
+typedef struct IdeviceSocketAddress {
+ /**
+ * Address family (e.g. 2 = AF_INET, 30 = AF_INET6)
+ */
+ uint8_t family;
+ uint16_t port;
+ /**
+ * Null-terminated address string. Must be freed with `idevice_string_free`.
+ */
+ char *addr;
+} IdeviceSocketAddress;
+
+/**
+ * A network event emitted by the device
+ */
+typedef struct IdeviceNetworkEvent {
+ enum IdeviceNetworkEventType event_type;
+ uint32_t interface_index;
+ /**
+ * Null-terminated interface name. Must be freed with `idevice_string_free`.
+ * Only valid when event_type == InterfaceDetection.
+ */
+ char *interface_name;
+ struct IdeviceSocketAddress local_addr;
+ struct IdeviceSocketAddress remote_addr;
+ /**
+ * PID of the process owning the connection. Valid for ConnectionDetection.
+ */
+ uint32_t pid;
+ uint64_t recv_buffer_size;
+ uint64_t recv_buffer_used;
+ uint64_t serial_number;
+ uint32_t kind;
+ uint64_t rx_packets;
+ uint64_t rx_bytes;
+ uint64_t tx_packets;
+ uint64_t tx_bytes;
+ uint64_t rx_dups;
+ uint64_t rx_ooo;
+ uint64_t tx_retx;
+ uint64_t min_rtt;
+ uint64_t avg_rtt;
+ uint64_t connection_serial;
+ uint64_t time;
+ uint64_t unknown_type;
+} IdeviceNetworkEvent;
+
+/**
+ * Configuration for sysmontap sampling passed over FFI
+ */
+typedef struct IdeviceSysmontapConfig {
+ /**
+ * Sampling interval in milliseconds
+ */
+ uint32_t interval_ms;
+ /**
+ * Array of process attribute name strings (null-terminated C strings)
+ */
+ const char *const *process_attributes;
+ uintptr_t process_attributes_count;
+ /**
+ * Array of system attribute name strings (null-terminated C strings)
+ */
+ const char *const *system_attributes;
+ uintptr_t system_attributes_count;
+} IdeviceSysmontapConfig;
+
+/**
+ * Progress snapshot passed to `on_progress`.
+ *
+ * A session is split into batches of files. `batch_*` describes the batch
+ * currently streaming; `session_*` accumulates across the whole session.
+ * Fields are only ever appended to, so a callback compiled against an older
+ * header stays ABI-compatible.
+ */
+typedef struct Mobilebackup2BackupProgress {
+ /**
+ * Bytes transferred so far in the current batch.
+ */
+ uint64_t batch_bytes_done;
+ /**
+ * Bytes the device said this batch contains, or 0 if unknown. Approximate.
+ */
+ uint64_t batch_bytes_total;
+ /**
+ * Bytes transferred so far across every batch in this session. Monotonic.
+ */
+ uint64_t session_bytes_done;
+ /**
+ * Estimated total bytes for the session, or 0 while not estimable.
+ * Derived from the device's percentage, so it drifts. Never exact.
+ */
+ uint64_t session_bytes_total;
+ /**
+ * Overall progress percentage (0.0-100.0), or negative if not yet known.
+ * Interpolated within a batch and clamped to be monotonic. Not equal to
+ * session_bytes_done / session_bytes_total.
+ */
+ double overall_progress;
+} Mobilebackup2BackupProgress;
+
/**
* C-compatible delegate for mobilebackup2 operations.
*
@@ -365,12 +836,16 @@ typedef struct Mobilebackup2BackupDelegateFFI {
bool (*exists)(const char *path, void *context);
bool (*is_dir)(const char *path, void *context);
/**
- * Optional progress callback. May be NULL.
+ * Optional cancellation callback. May be NULL.
*/
- void (*on_progress)(uint64_t bytes_done,
- uint64_t bytes_total,
- double overall_progress,
- void *context);
+ bool (*is_cancelled)(void *context);
+ /**
+ * Optional progress callback. May be NULL.
+ *
+ * `progress` is owned by the caller and only valid for the duration of the
+ * call; copy out any fields you need to keep.
+ */
+ void (*on_progress)(const struct Mobilebackup2BackupProgress *progress, void *context);
} Mobilebackup2BackupDelegateFFI;
typedef struct SyslogLabel {
@@ -386,8 +861,68 @@ typedef struct OsTraceLog {
const char *filename;
const char *message;
const struct SyslogLabel *label;
+ /**
+ * Unique process ID (the activity stream's `procid` field). Equals `pid`
+ * in practice on iOS.
+ */
+ uint64_t procid;
+ /**
+ * ID of the thread that emitted the entry
+ */
+ uint64_t thread_id;
+ /**
+ * Load address offset of the log call site within the sender image. Pair
+ * with `image_uuid` to symbolicate.
+ */
+ uint32_t image_offset;
+ /**
+ * UUID of the sender image, i.e. the one named by `image_name`
+ */
+ uint8_t image_uuid[16];
+ /**
+ * UUID of the process' main executable, i.e. the one named by `filename`
+ */
+ uint8_t process_image_uuid[16];
+ /**
+ * Raw monotonic device timestamp in mach ticks
+ */
+ uint64_t mach_timestamp;
} OsTraceLog;
+/**
+ * The peer device identity learned during a successful pair-setup.
+ *
+ * Free with `rppairing_peer_device_free`.
+ */
+typedef struct RpPairingPeerDeviceC {
+ /**
+ * Peer identifier, the same identifier a later `verifyManualPairing` returns.
+ */
+ char *account_id;
+ /**
+ * The device's 16-byte `altIRK`, used to match its mDNS `authTag` records.
+ */
+ uint8_t alt_irk[16];
+ /**
+ * Hardware model identifier, e.g. "AppleTV14,1".
+ */
+ char *model;
+ /**
+ * User-visible device name, e.g. "Living Room".
+ */
+ char *name;
+ /**
+ * The device's UDID.
+ */
+ char *udid;
+} RpPairingPeerDeviceC;
+
+/**
+ * Called when the device issues a setup PIN, so the caller can surface it to the
+ * user. May be NULL.
+ */
+typedef void (*PairableHostPinCb)(const char *pin, void *context);
+
/**
* Represents a captured device packet from pcapd
*/
@@ -413,6 +948,164 @@ typedef struct DevicePacketHandle {
uintptr_t data_len;
} DevicePacketHandle;
+/**
+ * C delegate supplying firmware component bytes by archive path.
+ *
+ * `read_component` (required) reads a whole component into a system-allocated
+ * buffer (ownership transfers to the library, which frees it). The optional
+ * streaming trio (`open_component`/`read_chunk`/`close_component`) lets large
+ * source boot objects stream without buffering; when `open_component` is NULL the
+ * library falls back to buffering via `read_component`.
+ */
+typedef struct IdeviceRestoreComponentSourceFFI {
+ void *context;
+ struct IdeviceFfiError *(*read_component)(const char *path,
+ uint8_t **out_data,
+ uintptr_t *out_len,
+ void *context);
+ struct IdeviceFfiError *(*open_component)(const char *path, void **out_reader, void *context);
+ struct IdeviceFfiError *(*read_chunk)(void *reader,
+ uint8_t *buf,
+ uintptr_t buf_len,
+ uintptr_t *out_read,
+ void *context);
+ void (*close_component)(void *reader, void *context);
+} IdeviceRestoreComponentSourceFFI;
+
+/**
+ * C delegate exposing a seekable, sized filesystem (DMG) image for ASR.
+ */
+typedef struct IdeviceRestoreFilesystemImageFFI {
+ void *context;
+ /**
+ * Returns the total image size in bytes.
+ */
+ struct IdeviceFfiError *(*size)(uint64_t *out_size, void *context);
+ /**
+ * Reads up to `len` bytes at `offset` into a system-allocated buffer whose
+ * ownership transfers to the library.
+ */
+ struct IdeviceFfiError *(*read_at)(uint64_t offset,
+ uintptr_t len,
+ uint8_t **out_data,
+ uintptr_t *out_len,
+ void *context);
+} IdeviceRestoreFilesystemImageFFI;
+
+/**
+ * C delegate opening fresh connections to restore-mode data ports.
+ */
+typedef struct IdeviceRestoreDataPortConnectorFFI {
+ void *context;
+ /**
+ * Connects to `port`, yielding a new [`IdeviceHandle`] (ownership transfers
+ * to the library).
+ */
+ struct IdeviceFfiError *(*connect)(uint16_t port,
+ struct IdeviceHandle **out_idevice,
+ void *context);
+} IdeviceRestoreDataPortConnectorFFI;
+
+/**
+ * C delegate receiving restore progress callbacks. Any field may be NULL.
+ */
+typedef struct IdeviceRestoreProgressFFI {
+ void *context;
+ /**
+ * The device's operation code and completion percentage (0–100).
+ */
+ void (*operation)(uint64_t operation, uint64_t progress, void *context);
+ /**
+ * A named host step (the `DataType` being serviced).
+ */
+ void (*step)(const char *name, void *context);
+ void (*transfer)(const char *component,
+ uint64_t sent,
+ uint64_t total,
+ bool has_total,
+ void *context);
+} IdeviceRestoreProgressFFI;
+
+/**
+ * C delegate implementing the raw USB surface of a recovery/DFU device.
+ *
+ * The library implements the iBoot/DFU protocol on top of these calls, so the
+ * caller only supplies USB I/O (via nusb, libusb, etc) against the Apple device
+ * already opened in a recovery/DFU mode.
+ */
+typedef struct IdeviceRestoreRecoveryTransportFFI {
+ void *context;
+ /**
+ * Host to device control transfer; writes the byte count to `out_transferred`.
+ */
+ struct IdeviceFfiError *(*control_out)(uint8_t request_type,
+ uint8_t request,
+ uint16_t value,
+ uint16_t index,
+ const uint8_t *data,
+ uintptr_t data_len,
+ uint32_t timeout_ms,
+ uintptr_t *out_transferred,
+ void *context);
+ /**
+ * Device to host control transfer into a system-allocated buffer (ownership
+ * transfers to the library).
+ */
+ struct IdeviceFfiError *(*control_in)(uint8_t request_type,
+ uint8_t request,
+ uint16_t value,
+ uint16_t index,
+ uint16_t length,
+ uint32_t timeout_ms,
+ uint8_t **out_data,
+ uintptr_t *out_len,
+ void *context);
+ /**
+ * Bulk OUT transfer; writes the byte count to `out_transferred`.
+ */
+ struct IdeviceFfiError *(*bulk_out)(uint8_t endpoint,
+ const uint8_t *data,
+ uintptr_t data_len,
+ uint32_t timeout_ms,
+ uintptr_t *out_transferred,
+ void *context);
+ /**
+ * Writes the NUL-terminated USB serial-number string into `buf`
+ * (capacity `buf_len`).
+ */
+ struct IdeviceFfiError *(*serial_number)(char *buf, uintptr_t buf_len, void *context);
+ /**
+ * Returns the device descriptor's `idProduct`.
+ */
+ uint16_t (*product_id)(void *context);
+ /**
+ * Selects a configuration.
+ */
+ struct IdeviceFfiError *(*set_configuration)(uint8_t configuration, void *context);
+ /**
+ * Claims an interface / alternate setting.
+ */
+ struct IdeviceFfiError *(*claim_interface)(uint8_t iface, uint8_t alt_setting, void *context);
+ /**
+ * Resets the device (it re-enumerates afterwards).
+ */
+ struct IdeviceFfiError *(*reset)(void *context);
+} IdeviceRestoreRecoveryTransportFFI;
+
+/**
+ * C delegate opening FDR trust-channel connections to device ports.
+ */
+typedef struct IdeviceRestoreFdrConnectorFFI {
+ void *context;
+ /**
+ * Connects to `port`, yielding a new [`IdeviceHandle`] (ownership
+ * transfers to the library).
+ */
+ struct IdeviceFfiError *(*connect_device_port)(uint16_t port,
+ struct IdeviceHandle **out_idevice,
+ void *context);
+} IdeviceRestoreFdrConnectorFFI;
+
/**
* C-compatible representation of an RSD service
*/
@@ -469,6 +1162,22 @@ typedef struct ScreenshotData {
uintptr_t length;
} ScreenshotData;
+/**
+ * Localhost endpoints exposed by a running WDA bridge.
+ *
+ * Pointers in this struct are heap-allocated and must be released with
+ * `wda_bridge_endpoints_free`.
+ */
+typedef struct WdaBridgeEndpointsC {
+ char *udid;
+ char *wda_url;
+ char *mjpeg_url;
+ uint16_t local_http;
+ uint16_t local_mjpeg;
+ uint16_t device_http;
+ uint16_t device_mjpeg;
+} WdaBridgeEndpointsC;
+
/**
* Creates a new Idevice connection
*
@@ -570,6 +1279,14 @@ struct IdeviceFfiError *idevice_start_session(struct IdeviceHandle *idevice,
const struct IdevicePairingFile *pairing_file,
bool legacy);
+/**
+ * Sets the timeout on async calls such as TCP connections
+ *
+ * # Safety
+ * This function is safe to call from any thread at any time
+ */
+void idevice_set_global_timeout(uint64_t secs);
+
/**
* Frees an Idevice handle
*
@@ -1658,16 +2375,47 @@ struct IdeviceFfiError *app_service_send_signal(struct AppServiceHandle *handle,
void app_service_free_signal_response(struct SignalResponseC *response);
/**
- * Fetches an app icon
+ * Creates a new ConfigurationServiceClient using RSD connection
*
* # Arguments
- * * [`handle`] - The AppServiceClient handle
- * * [`bundle_id`] - Bundle identifier of the app
- * * [`width`] - Icon width
- * * [`height`] - Icon height
- * * [`scale`] - Icon scale
- * * [`allow_placeholder`] - Whether to allow placeholder icons
- * * [`icon_data`] - Pointer to store the icon data (caller must free)
+ * * [`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
+ */
+struct IdeviceFfiError *configuration_service_connect_rsd(struct AdapterHandle *provider,
+ struct RsdHandshakeHandle *handshake,
+ struct ConfigurationServiceHandle **handle);
+
+/**
+ * Creates a new ConfigurationServiceClient from a socket
+ *
+ * # Arguments
+ * * [`socket`] - The socket to use for communication. Consumed regardless of the result.
+ * * [`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
+ */
+struct IdeviceFfiError *configuration_service_new(struct ReadWriteOpaque *socket,
+ struct ConfigurationServiceHandle **handle);
+
+/**
+ * Reads the device's light/dark appearance
+ *
+ * # Arguments
+ * * [`handle`] - The ConfigurationServiceClient handle
+ * * [`style`] - Pointer to store the appearance
*
* # Returns
* An IdeviceFfiError on error, null on success
@@ -1675,21 +2423,234 @@ void app_service_free_signal_response(struct SignalResponseC *response);
* # Safety
* All pointer parameters must be valid
*/
-struct IdeviceFfiError *app_service_fetch_app_icon(struct AppServiceHandle *handle,
- const char *bundle_id,
- float width,
- float height,
- float scale,
- int allow_placeholder,
- struct IconDataC **icon_data);
+struct IdeviceFfiError *configuration_service_get_user_interface_style(struct ConfigurationServiceHandle *handle,
+ enum IdeviceUserInterfaceStyle *style);
/**
- * Frees an IconDataC structure
+ * Switches the device between light and dark appearance
+ *
+ * # Arguments
+ * * [`handle`] - The ConfigurationServiceClient handle
+ * * [`style`] - The appearance to set
+ *
+ * # Returns
+ * An IdeviceFfiError on error, null on success
*
* # Safety
- * `icon_data` must be a valid pointer allocated by app_service_fetch_app_icon
+ * `handle` must be a valid pointer to a handle allocated by this library
*/
-void app_service_free_icon_data(struct IconDataC *icon_data);
+struct IdeviceFfiError *configuration_service_set_user_interface_style(struct ConfigurationServiceHandle *handle,
+ enum IdeviceUserInterfaceStyle style);
+
+/**
+ * Sets the system liquid-glass opacity
+ *
+ * # Arguments
+ * * [`handle`] - The ConfigurationServiceClient handle
+ * * [`opacity`] - The opacity, 0.0 to 1.0
+ *
+ * # Returns
+ * An IdeviceFfiError on error, null on success
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library
+ */
+struct IdeviceFfiError *configuration_service_set_liquid_glass_opacity(struct ConfigurationServiceHandle *handle,
+ float opacity);
+
+/**
+ * Reads the accessibility color filter's state
+ *
+ * # Arguments
+ * * [`handle`] - The ConfigurationServiceClient handle
+ * * [`filter`] - Pointer to store the state. Free its `filter_type` with
+ * `idevice_string_free`.
+ *
+ * # Returns
+ * An IdeviceFfiError on error, null on success
+ *
+ * # Safety
+ * All pointer parameters must be valid
+ */
+struct IdeviceFfiError *configuration_service_get_color_filter(struct ConfigurationServiceHandle *handle,
+ struct ColorFilterC *filter);
+
+/**
+ * Enables or disables the accessibility color filter
+ *
+ * # Arguments
+ * * [`handle`] - The ConfigurationServiceClient handle
+ * * [`enabled`] - Whether the filter is on
+ * * [`filter_type`] - The preset to use, e.g. `Protanopia`. Required when enabling,
+ * ignored otherwise, and may be NULL when disabling.
+ * * [`intensity`] - Filter strength, 0.0 to 1.0. Ignored unless `has_intensity` is set.
+ * * [`has_intensity`] - Whether to send `intensity`
+ *
+ * # Returns
+ * An IdeviceFfiError on error, null on success
+ *
+ * # Safety
+ * All non-NULL pointer parameters must be valid
+ */
+struct IdeviceFfiError *configuration_service_set_color_filter(struct ConfigurationServiceHandle *handle,
+ int enabled,
+ const char *filter_type,
+ float intensity,
+ int has_intensity);
+
+/**
+ * Reads the dynamic-type size's name, e.g. `medium` or `large`
+ *
+ * # Arguments
+ * * [`handle`] - The ConfigurationServiceClient handle
+ * * [`size`] - Pointer to store the name. Free with `idevice_string_free`.
+ *
+ * # Returns
+ * An IdeviceFfiError on error, null on success
+ *
+ * # Safety
+ * All pointer parameters must be valid
+ */
+struct IdeviceFfiError *configuration_service_get_device_text_size(struct ConfigurationServiceHandle *handle,
+ char **size);
+
+/**
+ * Sets the dynamic-type size by name, e.g. `medium` or `large`
+ *
+ * # Arguments
+ * * [`handle`] - The ConfigurationServiceClient handle
+ * * [`size`] - The size's name
+ *
+ * # Returns
+ * An IdeviceFfiError on error, null on success
+ *
+ * # Safety
+ * All pointer parameters must be valid
+ */
+struct IdeviceFfiError *configuration_service_set_device_text_size(struct ConfigurationServiceHandle *handle,
+ const char *size);
+
+/**
+ * Reads whether Reduce Motion is on
+ *
+ * # Arguments
+ * * [`handle`] - The ConfigurationServiceClient handle
+ * * [`enabled`] - Pointer to store the state
+ *
+ * # Returns
+ * An IdeviceFfiError on error, null on success
+ *
+ * # Safety
+ * All pointer parameters must be valid
+ */
+struct IdeviceFfiError *configuration_service_get_reduce_motion(struct ConfigurationServiceHandle *handle,
+ int *enabled);
+
+/**
+ * Toggles Reduce Motion
+ *
+ * # Arguments
+ * * [`handle`] - The ConfigurationServiceClient handle
+ * * [`enabled`] - Whether to turn it on
+ *
+ * # Returns
+ * An IdeviceFfiError on error, null on success
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library
+ */
+struct IdeviceFfiError *configuration_service_set_reduce_motion(struct ConfigurationServiceHandle *handle,
+ int enabled);
+
+/**
+ * Reads whether Reduce Transparency is on
+ *
+ * # Arguments
+ * * [`handle`] - The ConfigurationServiceClient handle
+ * * [`enabled`] - Pointer to store the state
+ *
+ * # Returns
+ * An IdeviceFfiError on error, null on success
+ *
+ * # Safety
+ * All pointer parameters must be valid
+ */
+struct IdeviceFfiError *configuration_service_get_reduce_transparency(struct ConfigurationServiceHandle *handle,
+ int *enabled);
+
+/**
+ * Toggles Reduce Transparency
+ *
+ * # Arguments
+ * * [`handle`] - The ConfigurationServiceClient handle
+ * * [`enabled`] - Whether to turn it on
+ *
+ * # Returns
+ * An IdeviceFfiError on error, null on success
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library
+ */
+struct IdeviceFfiError *configuration_service_set_reduce_transparency(struct ConfigurationServiceHandle *handle,
+ int enabled);
+
+/**
+ * Reads whether the layout-debug borders overlay is on
+ *
+ * # Arguments
+ * * [`handle`] - The ConfigurationServiceClient handle
+ * * [`enabled`] - Pointer to store the state
+ *
+ * # Returns
+ * An IdeviceFfiError on error, null on success
+ *
+ * # Safety
+ * All pointer parameters must be valid
+ */
+struct IdeviceFfiError *configuration_service_get_show_borders(struct ConfigurationServiceHandle *handle,
+ int *enabled);
+
+/**
+ * Toggles the layout-debug borders overlay
+ *
+ * # Arguments
+ * * [`handle`] - The ConfigurationServiceClient handle
+ * * [`enabled`] - Whether to turn it on
+ *
+ * # Returns
+ * An IdeviceFfiError on error, null on success
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library
+ */
+struct IdeviceFfiError *configuration_service_set_show_borders(struct ConfigurationServiceHandle *handle,
+ int enabled);
+
+/**
+ * Toggles Increase Contrast
+ *
+ * The device offers no getter for this one.
+ *
+ * # Arguments
+ * * [`handle`] - The ConfigurationServiceClient handle
+ * * [`enabled`] - Whether to turn it on
+ *
+ * # Returns
+ * An IdeviceFfiError on error, null on success
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library
+ */
+struct IdeviceFfiError *configuration_service_set_increase_contrast(struct ConfigurationServiceHandle *handle,
+ int enabled);
+
+/**
+ * Frees a ConfigurationServiceClient handle
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library or NULL
+ */
+void configuration_service_free(struct ConfigurationServiceHandle *handle);
/**
* Creates a new DiagnosticsServiceClient using RSD connection
@@ -1788,6 +2749,301 @@ void diagnostics_service_free(struct DiagnosticsServiceHandle *handle);
*/
void sysdiagnose_stream_free(struct SysdiagnoseStreamHandle *handle);
+/**
+ * Creates a new FileServiceClient using RSD connection
+ *
+ * This connects the service's control channel, i.e.
+ * `com.apple.coredevice.fileservice.control`. Downloads additionally need the
+ * data channel, `com.apple.coredevice.fileservice.data`.
+ *
+ * # 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
+ */
+struct IdeviceFfiError *file_service_connect_rsd(struct AdapterHandle *provider,
+ struct RsdHandshakeHandle *handshake,
+ struct FileServiceHandle **handle);
+
+/**
+ * Creates a new FileServiceClient from a socket
+ *
+ * # Arguments
+ * * [`socket`] - The socket to use for communication. Consumed regardless of the result.
+ * * [`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
+ */
+struct IdeviceFfiError *file_service_new(struct ReadWriteOpaque *socket,
+ struct FileServiceHandle **handle);
+
+/**
+ * Opens a session on a domain, which every later command is scoped to
+ *
+ * # Arguments
+ * * [`handle`] - The FileServiceClient handle
+ * * [`domain`] - The domain to scope the session to
+ * * [`identifier`] - The container's identifier, i.e. a bundle ID or an app-group ID.
+ * The domains that don't take one ignore it, and it may be NULL for them.
+ * * [`session_id`] - Pointer to store the new session's ID, or NULL to ignore it.
+ * Free with `idevice_string_free`.
+ *
+ * # Returns
+ * An IdeviceFfiError on error, null on success
+ *
+ * # Safety
+ * All non-NULL pointer parameters must be valid
+ */
+struct IdeviceFfiError *file_service_create_session(struct FileServiceHandle *handle,
+ enum IdeviceFileServiceDomain domain,
+ const char *identifier,
+ char **session_id);
+
+/**
+ * The session ID from the last `file_service_create_session`
+ *
+ * # Arguments
+ * * [`handle`] - The FileServiceClient handle
+ * * [`session_id`] - Pointer to store the ID, set to NULL when there is no
+ * session. Free with `idevice_string_free`.
+ *
+ * # Returns
+ * An IdeviceFfiError on error, null on success
+ *
+ * # Safety
+ * All pointer parameters must be valid
+ */
+struct IdeviceFfiError *file_service_session_id(struct FileServiceHandle *handle,
+ char **session_id);
+
+/**
+ * Lists a directory, relative to the session's domain root
+ *
+ * # Arguments
+ * * [`handle`] - The FileServiceClient handle
+ * * [`path`] - The directory to list
+ * * [`entries`] - Pointer to store the entry names, freed with
+ * `file_service_free_directory_list`
+ * * [`len`] - Pointer to store the number of entries
+ *
+ * # Returns
+ * An IdeviceFfiError on error, null on success
+ *
+ * # Safety
+ * All pointer parameters must be valid
+ */
+struct IdeviceFfiError *file_service_retrieve_directory_list(struct FileServiceHandle *handle,
+ const char *path,
+ char ***entries,
+ uintptr_t *len);
+
+/**
+ * Frees the list from `file_service_retrieve_directory_list`
+ *
+ * # Safety
+ * `entries` must be a pointer returned by `file_service_retrieve_directory_list`
+ * with its reported length, or NULL
+ */
+void file_service_free_directory_list(char **entries, uintptr_t len);
+
+/**
+ * Downloads a file, relative to the session's domain root
+ *
+ * The transfer itself runs on the service's data channel, which the caller
+ * opens by connecting the adapter to the port the RSD handshake reports for
+ * `com.apple.coredevice.fileservice.data`.
+ *
+ * # Arguments
+ * * [`handle`] - The FileServiceClient handle
+ * * [`path`] - The file to download
+ * * [`adapter`] - The adapter the control channel was connected over
+ * * [`data_port`] - The port of `com.apple.coredevice.fileservice.data`
+ * * [`data`] - Pointer to store the contents, freed with `idevice_data_free`
+ * * [`len`] - Pointer to store the number of bytes
+ *
+ * # Returns
+ * An IdeviceFfiError on error, null on success
+ *
+ * # Safety
+ * All pointer parameters must be valid
+ */
+struct IdeviceFfiError *file_service_retrieve_file(struct FileServiceHandle *handle,
+ const char *path,
+ struct AdapterHandle *adapter,
+ uint16_t data_port,
+ uint8_t **data,
+ uintptr_t *len);
+
+/**
+ * Downloads a file over a data channel the caller already opened
+ *
+ * Like `file_service_retrieve_file`, but takes the data channel itself instead
+ * of opening one. Note that the device only accepts the connection once the
+ * control channel has announced the transfer, so a stream opened well in
+ * advance may have been dropped.
+ *
+ * # Arguments
+ * * [`handle`] - The FileServiceClient handle
+ * * [`path`] - The file to download
+ * * [`data_stream`] - The data channel. Consumed regardless of the result.
+ * * [`data`] - Pointer to store the contents, freed with `idevice_data_free`
+ * * [`len`] - Pointer to store the number of bytes
+ *
+ * # Returns
+ * An IdeviceFfiError on error, null on success
+ *
+ * # Safety
+ * All pointer parameters must be valid
+ */
+struct IdeviceFfiError *file_service_retrieve_file_with_stream(struct FileServiceHandle *handle,
+ const char *path,
+ struct ReadWriteOpaque *data_stream,
+ uint8_t **data,
+ uintptr_t *len);
+
+/**
+ * Creates an empty file, relative to the session's domain root
+ *
+ * # Arguments
+ * * [`handle`] - The FileServiceClient handle
+ * * [`path`] - The file to create
+ * * [`file_permissions`] - The file's mode, e.g. 0644
+ * * [`uid`] - The owning user's ID, e.g. 501
+ * * [`gid`] - The owning group's ID, e.g. 501
+ * * [`creation_time`] - The creation time to set
+ * * [`last_modification_time`] - The modification time to set
+ *
+ * # Returns
+ * An IdeviceFfiError on error, null on success
+ *
+ * # Safety
+ * All pointer parameters must be valid
+ */
+struct IdeviceFfiError *file_service_propose_empty_file(struct FileServiceHandle *handle,
+ const char *path,
+ uint32_t file_permissions,
+ uint32_t uid,
+ uint32_t gid,
+ int64_t creation_time,
+ int64_t last_modification_time);
+
+/**
+ * Looks a domain up by the name the device uses, e.g. `appDataContainer`
+ *
+ * # Arguments
+ * * [`name`] - The domain's name
+ * * [`domain`] - Pointer to store the domain
+ *
+ * # Returns
+ * 1 when the name is known, 0 otherwise
+ *
+ * # Safety
+ * All pointer parameters must be valid
+ */
+int file_service_domain_from_name(const char *name, enum IdeviceFileServiceDomain *domain);
+
+/**
+ * Frees a FileServiceClient handle
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library or NULL
+ */
+void file_service_free(struct FileServiceHandle *handle);
+
+/**
+ * Creates a new IconServiceClient 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
+ */
+struct IdeviceFfiError *icon_service_connect_rsd(struct AdapterHandle *provider,
+ struct RsdHandshakeHandle *handshake,
+ struct IconServiceHandle **handle);
+
+/**
+ * Creates a new IconServiceClient from a socket
+ *
+ * # Arguments
+ * * [`socket`] - The socket to use for communication. Consumed regardless of the result.
+ * * [`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
+ */
+struct IdeviceFfiError *icon_service_new(struct ReadWriteOpaque *socket,
+ struct IconServiceHandle **handle);
+
+/**
+ * Fetches an app's icon, rendered as a PNG
+ *
+ * # Arguments
+ * * [`handle`] - The IconServiceClient handle
+ * * [`bundle_identifier`] - Bundle identifier of the app, or NULL to use `app_path`
+ * * [`app_path`] - Path of the app on the device, or NULL to use `bundle_identifier`
+ * * [`width`] - Requested icon width in points
+ * * [`height`] - Requested icon height in points
+ * * [`scale`] - Requested icon scale
+ * * [`allow_placeholder`] - Whether the device may render a generic placeholder
+ * * [`icon`] - Pointer to store the icon, freed with `icon_service_free_icon`
+ *
+ * Exactly one of `bundle_identifier` and `app_path` must be passed.
+ *
+ * # Returns
+ * An IdeviceFfiError on error, null on success
+ *
+ * # Safety
+ * All non-NULL pointer parameters must be valid
+ */
+struct IdeviceFfiError *icon_service_fetch_icon(struct IconServiceHandle *handle,
+ const char *bundle_identifier,
+ const char *app_path,
+ float width,
+ float height,
+ float scale,
+ int allow_placeholder,
+ struct AppIconC **icon);
+
+/**
+ * Frees an AppIconC
+ *
+ * # Safety
+ * `icon` must be a pointer returned by `icon_service_fetch_icon`, or NULL
+ */
+void icon_service_free_icon(struct AppIconC *icon);
+
+/**
+ * Frees an IconServiceClient handle
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library or NULL
+ */
+void icon_service_free(struct IconServiceHandle *handle);
+
/**
* Automatically creates and connects to Core Device Proxy, returning a client handle
*
@@ -2123,6 +3379,338 @@ struct IdeviceFfiError *crash_report_flush(struct IdeviceProviderHandle *provide
*/
void crash_report_client_free(struct CrashReportCopyMobileHandle *handle);
+/**
+ * Creates a new CryptexdClient 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
+ */
+struct IdeviceFfiError *cryptexd_connect_rsd(struct AdapterHandle *provider,
+ struct RsdHandshakeHandle *handshake,
+ struct CryptexdHandle **handle);
+
+/**
+ * Creates a new CryptexdClient from a socket
+ *
+ * # Arguments
+ * * [`socket`] - The socket to use for communication. Consumed regardless of the result.
+ * * [`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
+ */
+struct IdeviceFfiError *cryptexd_new(struct ReadWriteOpaque *socket,
+ struct CryptexdHandle **handle);
+
+/**
+ * Reads the device's AppleImage4 chip instance, which identifies it in a
+ * Cryptex1 personalization request
+ *
+ * The keys are the daemon's `img4_chip_*` names, e.g. `img4_chip_chip`
+ * (ChipID), `img4_chip_bord` (BoardID) and `img4_chip_ecid` (ECID).
+ *
+ * # Arguments
+ * * [`handle`] - The CryptexdClient handle. Consumed by this call.
+ * * [`identifiers`] - Pointer to store the identifiers
+ *
+ * # Returns
+ * An IdeviceFfiError on error, null on success
+ *
+ * # Safety
+ * All pointer parameters must be valid
+ */
+struct IdeviceFfiError *cryptexd_read_personalization_identifiers(struct CryptexdHandle *handle,
+ plist_t *identifiers);
+
+/**
+ * Lists the cryptexes installed on the device
+ *
+ * # Arguments
+ * * [`handle`] - The CryptexdClient handle. Consumed by this call.
+ * * [`cryptexes`] - Pointer to store the list, freed with `cryptexd_free_installed`
+ * * [`len`] - Pointer to store the number of entries
+ *
+ * # Returns
+ * An IdeviceFfiError on error, null on success
+ *
+ * # Safety
+ * All pointer parameters must be valid
+ */
+struct IdeviceFfiError *cryptexd_copy_installed(struct CryptexdHandle *handle,
+ struct InstalledCryptexC **cryptexes,
+ uintptr_t *len);
+
+/**
+ * Frees the list from `cryptexd_copy_installed`
+ *
+ * # Safety
+ * `cryptexes` must be a pointer returned by `cryptexd_copy_installed` with its
+ * reported length, or NULL
+ */
+void cryptexd_free_installed(struct InstalledCryptexC *cryptexes, uintptr_t len);
+
+/**
+ * Frees an InstalledCryptexC allocated by this library
+ *
+ * # Safety
+ * `cryptex` must be a pointer allocated by this library, or NULL
+ */
+void cryptexd_free_installed_cryptex(struct InstalledCryptexC *cryptex);
+
+/**
+ * Reads a nonce domain's nonce structure
+ *
+ * Use `cryptexd_cryptex_nonce` for the nonce a TSS request wants.
+ *
+ * # Arguments
+ * * [`handle`] - The CryptexdClient handle. Consumed by this call.
+ * * [`domain`] - The nonce domain to read
+ * * [`nonce`] - Pointer to store the nonce, freed with `idevice_data_free`
+ * * [`nonce_len`] - Pointer to store the number of bytes
+ *
+ * # Returns
+ * An IdeviceFfiError on error, null on success
+ *
+ * # Safety
+ * All pointer parameters must be valid
+ */
+struct IdeviceFfiError *cryptexd_get_nonce(struct CryptexdHandle *handle,
+ struct CryptexNonceDomain domain,
+ uint8_t **nonce,
+ uintptr_t *nonce_len);
+
+/**
+ * Reads the nonce a Cryptex1 TSS request is personalized against
+ *
+ * # Arguments
+ * * [`handle`] - The CryptexdClient handle. Consumed by this call.
+ * * [`nonce_domain_handle`] - The build identity's `Cryptex1,NonceDomain`
+ * * [`nonce`] - Pointer to store the nonce, freed with `idevice_data_free`
+ * * [`nonce_len`] - Pointer to store the number of bytes
+ *
+ * # Returns
+ * An IdeviceFfiError on error, null on success
+ *
+ * # Safety
+ * All pointer parameters must be valid
+ */
+struct IdeviceFfiError *cryptexd_cryptex_nonce(struct CryptexdHandle *handle,
+ uint64_t nonce_domain_handle,
+ uint8_t **nonce,
+ uintptr_t *nonce_len);
+
+/**
+ * Rolls (regenerates) a nonce domain's nonce, invalidating anything
+ * personalized against the previous one
+ *
+ * # Arguments
+ * * [`handle`] - The CryptexdClient handle. Consumed by this call.
+ * * [`domain`] - The nonce domain to roll
+ *
+ * # Returns
+ * An IdeviceFfiError on error, null on success
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library
+ */
+struct IdeviceFfiError *cryptexd_roll_nonce(struct CryptexdHandle *handle,
+ struct CryptexNonceDomain domain);
+
+/**
+ * Uninstalls a cryptex by the identifier `cryptexd_copy_installed` reports
+ *
+ * # Arguments
+ * * [`handle`] - The CryptexdClient handle. Consumed by this call.
+ * * [`identifier`] - The cryptex's identifier
+ * * [`version`] - The version to scope the uninstall to, or NULL for all of them
+ *
+ * # Returns
+ * An IdeviceFfiError on error, null on success
+ *
+ * # Safety
+ * All non-NULL pointer parameters must be valid
+ */
+struct IdeviceFfiError *cryptexd_uninstall(struct CryptexdHandle *handle,
+ const char *identifier,
+ const char *version);
+
+/**
+ * Installs a cryptex
+ *
+ * # Arguments
+ * * [`handle`] - The CryptexdClient handle. Consumed by this call.
+ * * [`request`] - The payloads and parameters to install
+ *
+ * # Returns
+ * An IdeviceFfiError on error, null on success
+ *
+ * # Safety
+ * All pointer parameters must be valid, and the request's buffers must be
+ * readable for their stated lengths
+ */
+struct IdeviceFfiError *cryptexd_install(struct CryptexdHandle *handle,
+ const struct CryptexInstallRequestC *request);
+
+/**
+ * Extracts the nonce from cryptexd's nonce structure
+ *
+ * # Arguments
+ * * [`blob`] - The structure `cryptexd_get_nonce` returned
+ * * [`blob_len`] - Its length
+ * * [`nonce`] - Pointer to store the nonce, freed with `idevice_data_free`
+ * * [`nonce_len`] - Pointer to store the number of bytes
+ *
+ * # Returns
+ * An IdeviceFfiError on error, null on success
+ *
+ * # Safety
+ * All pointer parameters must be valid, and `blob` must be readable for
+ * `blob_len` bytes
+ */
+struct IdeviceFfiError *cryptexd_unwrap_nonce(const uint8_t *blob,
+ uintptr_t blob_len,
+ uint8_t **nonce,
+ uintptr_t *nonce_len);
+
+/**
+ * Loads the DeveloperDiskImage payloads from an unpacked DDI `Restore` directory
+ *
+ * # Arguments
+ * * [`restore_dir`] - The directory to read
+ * * [`handle`] - Pointer to store the newly created handle
+ *
+ * # Returns
+ * An IdeviceFfiError on error, null on success
+ *
+ * # Safety
+ * All pointer parameters must be valid
+ */
+struct IdeviceFfiError *cryptex1_assets_load(const char *restore_dir,
+ struct Cryptex1AssetsHandle **handle);
+
+/**
+ * Builds the DeveloperDiskImage payloads from buffers the caller already has
+ *
+ * # Arguments
+ * * [`image`] / [`image_len`] - `Cryptex1,GenericDmg`
+ * * [`trustcache`] / [`trustcache_len`] - `Cryptex1,GenericTrustCache`
+ * * [`info`] / [`info_len`] - `Cryptex1,CryptexInfoPlist`
+ * * [`volumehash`] / [`volumehash_len`] - `Cryptex1,GenericVolume`
+ * * [`build_identity`] - The build identity the payloads came from
+ * * [`handle`] - Pointer to store the newly created handle
+ *
+ * # Returns
+ * An IdeviceFfiError on error, null on success
+ *
+ * # Safety
+ * All pointer parameters must be valid, and each buffer must be readable for
+ * its stated length
+ */
+struct IdeviceFfiError *cryptex1_assets_from_parts(const uint8_t *image,
+ uintptr_t image_len,
+ const uint8_t *trustcache,
+ uintptr_t trustcache_len,
+ const uint8_t *info,
+ uintptr_t info_len,
+ const uint8_t *volumehash,
+ uintptr_t volumehash_len,
+ plist_t build_identity,
+ struct Cryptex1AssetsHandle **handle);
+
+/**
+ * The handle of the nonce domain the assets are personalized against, i.e. the
+ * build identity's `Cryptex1,NonceDomain`
+ *
+ * # Arguments
+ * * [`handle`] - The assets handle
+ * * [`nonce_domain`] - Pointer to store the handle
+ *
+ * # Returns
+ * An IdeviceFfiError on error, null on success
+ *
+ * # Safety
+ * All pointer parameters must be valid
+ */
+struct IdeviceFfiError *cryptex1_assets_nonce_domain(struct Cryptex1AssetsHandle *handle,
+ uint64_t *nonce_domain);
+
+/**
+ * Frees a Cryptex1Assets handle
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library or NULL
+ */
+void cryptex1_assets_free(struct Cryptex1AssetsHandle *handle);
+
+/**
+ * Personalizes and installs the DeveloperDiskImage cryptex end to end
+ *
+ * The cryptex equivalent of the image mounter's auto-mount: reads the device's
+ * personalization identifiers and cryptex nonce, has Apple sign a Cryptex1
+ * ticket for them, and installs the assets. Each step opens its own connection
+ * off the adapter, since the daemon serves one routine per connection.
+ *
+ * # Arguments
+ * * [`provider`] - An adapter created by this library
+ * * [`handshake`] - An RSD handshake from the same provider
+ * * [`assets`] - The payloads to install
+ * * [`installed`] - Pointer to store the installed cryptex, freed with
+ * `cryptexd_free_installed_cryptex`. May be NULL to ignore it.
+ *
+ * # Returns
+ * An IdeviceFfiError on error, null on success
+ *
+ * # Safety
+ * All non-NULL pointer parameters must be valid
+ */
+struct IdeviceFfiError *cryptexd_install_ddi(struct AdapterHandle *provider,
+ struct RsdHandshakeHandle *handshake,
+ struct Cryptex1AssetsHandle *assets,
+ struct InstalledCryptexC **installed);
+
+/**
+ * The installed DeveloperDiskImage cryptex, if there is one
+ *
+ * # Arguments
+ * * [`provider`] - An adapter created by this library
+ * * [`handshake`] - An RSD handshake from the same provider
+ * * [`installed`] - Pointer to store the cryptex, set to NULL when no DDI is
+ * installed. Freed with `cryptexd_free_installed_cryptex`.
+ *
+ * # Returns
+ * An IdeviceFfiError on error, null on success
+ *
+ * # Safety
+ * All pointer parameters must be valid
+ */
+struct IdeviceFfiError *cryptexd_installed_ddi(struct AdapterHandle *provider,
+ struct RsdHandshakeHandle *handshake,
+ struct InstalledCryptexC **installed);
+
+/**
+ * Frees a CryptexdClient handle
+ *
+ * Only needed for a handle no routine was invoked on: every routine consumes
+ * the handle it is passed.
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library or NULL
+ */
+void cryptexd_free(struct CryptexdHandle *handle);
+
/**
* Creates a new DebugserverCommand
*
@@ -2616,6 +4204,64 @@ struct IdeviceFfiError *location_simulation_set(struct LocationSimulationHandle
double latitude,
double longitude);
+/**
+ * Frees an IdeviceNotificationInfo and its heap-allocated string fields
+ *
+ * # Safety
+ * `info` must be a valid pointer allocated by this library or NULL
+ */
+void notifications_info_free(struct IdeviceNotificationInfo *info);
+
+/**
+ * Creates a new NotificationsClient from a RemoteServerClient
+ *
+ * # Safety
+ * `server` 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
+ */
+struct IdeviceFfiError *notifications_new(struct RemoteServerHandle *server,
+ struct NotificationsHandle **handle);
+
+/**
+ * Frees a NotificationsClient handle
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library or NULL
+ */
+void notifications_free(struct NotificationsHandle *handle);
+
+/**
+ * Enables application state and memory notifications on the device.
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library
+ */
+struct IdeviceFfiError *notifications_start(struct NotificationsHandle *handle);
+
+/**
+ * Disables application state and memory notifications on the device.
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library
+ */
+struct IdeviceFfiError *notifications_stop(struct NotificationsHandle *handle);
+
+/**
+ * Reads the next notification pushed by the device. Blocks until a notification arrives.
+ *
+ * # Arguments
+ * * [`handle`] - The NotificationsClient handle
+ * * [`info_out`] - On success, set to a heap-allocated IdeviceNotificationInfo
+ *
+ * # Returns
+ * An IdeviceFfiError on error, null on success
+ *
+ * # Safety
+ * All pointers must be valid and non-null. Free the info with `notifications_info_free`.
+ */
+struct IdeviceFfiError *notifications_get_next(struct NotificationsHandle *handle,
+ struct IdeviceNotificationInfo **info_out);
+
/**
* Creates a new ProcessControlClient from a RemoteServerClient
*
@@ -2809,6 +4455,517 @@ struct IdeviceFfiError *screenshot_client_take_screenshot(struct ScreenshotClien
uint8_t **data,
uintptr_t *len);
+/**
+ * Creates a new ApplicationListingClient from a RemoteServerClient
+ *
+ * # Safety
+ * `server` 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
+ */
+struct IdeviceFfiError *application_listing_new(struct RemoteServerHandle *server,
+ struct ApplicationListingHandle **handle);
+
+/**
+ * Frees an ApplicationListingClient handle
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library or NULL
+ */
+void application_listing_free(struct ApplicationListingHandle *handle);
+
+/**
+ * Returns the list of installed applications as an array of plist dictionaries
+ *
+ * # Arguments
+ * * [`handle`] - The ApplicationListingClient handle
+ * * [`apps_out`] - On success, set to a heap-allocated array of plist_t values (each is a dict)
+ * * [`count_out`] - On success, set to the number of apps returned
+ *
+ * # Returns
+ * An IdeviceFfiError on error, null on success
+ *
+ * # Safety
+ * All pointers must be valid and non-null.
+ * Free the returned array with `idevice_plist_array_free`.
+ */
+struct IdeviceFfiError *application_listing_get_apps(struct ApplicationListingHandle *handle,
+ plist_t **apps_out,
+ uintptr_t *count_out);
+
+/**
+ * Creates a new ConditionInducerClient from a RemoteServerClient
+ *
+ * # Safety
+ * `server` 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
+ */
+struct IdeviceFfiError *condition_inducer_new(struct RemoteServerHandle *server,
+ struct ConditionInducerHandle **handle);
+
+/**
+ * Frees a ConditionInducerClient handle
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library or NULL
+ */
+void condition_inducer_free(struct ConditionInducerHandle *handle);
+
+/**
+ * Frees a single IdeviceConditionGroup and all its heap-allocated fields
+ *
+ * # Safety
+ * `group` must be a valid pointer allocated by this library or NULL
+ */
+void condition_inducer_group_free(struct IdeviceConditionGroup *group);
+
+/**
+ * Frees an array of IdeviceConditionGroup pointers
+ *
+ * # Safety
+ * `groups` must be a valid pointer to an array of length `count` allocated by this library,
+ * or NULL
+ */
+void condition_inducer_groups_free(struct IdeviceConditionGroup **groups, uintptr_t count);
+
+/**
+ * Returns the available condition inducer groups
+ *
+ * # Arguments
+ * * [`handle`] - The ConditionInducerClient handle
+ * * [`groups_out`] - On success, set to a heap-allocated array of group pointers
+ * * [`count_out`] - On success, set to the number of groups returned
+ *
+ * # Returns
+ * An IdeviceFfiError on error, null on success
+ *
+ * # Safety
+ * All pointers must be valid and non-null. Free with `condition_inducer_groups_free`.
+ */
+struct IdeviceFfiError *condition_inducer_available_conditions(struct ConditionInducerHandle *handle,
+ struct IdeviceConditionGroup ***groups_out,
+ uintptr_t *count_out);
+
+/**
+ * Enables a specific condition profile
+ *
+ * # Arguments
+ * * [`handle`] - The ConditionInducerClient handle
+ * * [`condition_identifier`] - The condition group identifier (null-terminated C string)
+ * * [`profile_identifier`] - The profile identifier within the group (null-terminated C string)
+ *
+ * # Returns
+ * An IdeviceFfiError on error, null on success
+ *
+ * # Safety
+ * All pointers must be valid and non-null
+ */
+struct IdeviceFfiError *condition_inducer_enable(struct ConditionInducerHandle *handle,
+ const char *condition_identifier,
+ const char *profile_identifier);
+
+/**
+ * Disables the currently active condition
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library
+ */
+struct IdeviceFfiError *condition_inducer_disable(struct ConditionInducerHandle *handle);
+
+/**
+ * Creates a new DeviceInfoClient from a RemoteServerClient
+ *
+ * # Safety
+ * `server` 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
+ */
+struct IdeviceFfiError *device_info_new(struct RemoteServerHandle *server,
+ struct DeviceInfoHandle **handle);
+
+/**
+ * Frees a DeviceInfoClient handle
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library or NULL
+ */
+void device_info_free(struct DeviceInfoHandle *handle);
+
+/**
+ * Frees a single IdeviceRunningProcess struct and its heap-allocated strings
+ *
+ * # Safety
+ * `process` must be a valid pointer allocated by this library or NULL
+ */
+void device_info_running_process_free(struct IdeviceRunningProcess *process);
+
+/**
+ * Frees an array of IdeviceRunningProcess pointers
+ *
+ * # Safety
+ * `processes` must be a valid pointer to an array of length `count` allocated by this library,
+ * or NULL
+ */
+void device_info_running_processes_free(struct IdeviceRunningProcess **processes, uintptr_t count);
+
+/**
+ * Returns the list of running processes on the device
+ *
+ * # Arguments
+ * * [`handle`] - The DeviceInfoClient handle
+ * * [`processes`] - On success, set to a heap-allocated array of process pointers
+ * * [`count`] - On success, set to the number of processes returned
+ *
+ * # Returns
+ * An IdeviceFfiError on error, null on success
+ *
+ * # Safety
+ * All pointers must be valid and non-null
+ */
+struct IdeviceFfiError *device_info_running_processes(struct DeviceInfoHandle *handle,
+ struct IdeviceRunningProcess ***processes,
+ uintptr_t *count);
+
+/**
+ * Returns the executable name for the given PID
+ *
+ * # Safety
+ * All pointers must be valid and non-null. Free the returned string with `idevice_string_free`.
+ */
+struct IdeviceFfiError *device_info_execname_for_pid(struct DeviceInfoHandle *handle,
+ uint32_t pid,
+ char **name_out);
+
+/**
+ * Returns whether the given PID is currently running
+ *
+ * # Safety
+ * All pointers must be valid and non-null
+ */
+struct IdeviceFfiError *device_info_is_running_pid(struct DeviceInfoHandle *handle,
+ uint32_t pid,
+ bool *result);
+
+/**
+ * Returns hardware information as a plist dictionary
+ *
+ * # Safety
+ * All pointers must be valid and non-null. Free the returned plist with `plist_free`.
+ */
+struct IdeviceFfiError *device_info_hardware_information(struct DeviceInfoHandle *handle,
+ plist_t *plist_out);
+
+/**
+ * Returns network information as a plist dictionary
+ *
+ * # Safety
+ * All pointers must be valid and non-null. Free the returned plist with `plist_free`.
+ */
+struct IdeviceFfiError *device_info_network_information(struct DeviceInfoHandle *handle,
+ plist_t *plist_out);
+
+/**
+ * Returns the mach kernel name
+ *
+ * # Safety
+ * All pointers must be valid and non-null. Free the returned string with `idevice_string_free`.
+ */
+struct IdeviceFfiError *device_info_mach_kernel_name(struct DeviceInfoHandle *handle,
+ char **name_out);
+
+/**
+ * Frees a null-terminated string array allocated by this library
+ *
+ * # Safety
+ * `strings` must be a valid pointer to an array of `count` C strings allocated by this library,
+ * or NULL
+ */
+void device_info_string_array_free(char **strings, uintptr_t count);
+
+/**
+ * Returns the list of sysmon process attribute names
+ *
+ * # Safety
+ * All pointers must be valid and non-null. Free with `device_info_string_array_free`.
+ */
+struct IdeviceFfiError *device_info_sysmon_process_attributes(struct DeviceInfoHandle *handle,
+ char ***attrs_out,
+ uintptr_t *count_out);
+
+/**
+ * Returns the list of sysmon system attribute names
+ *
+ * # Safety
+ * All pointers must be valid and non-null. Free with `device_info_string_array_free`.
+ */
+struct IdeviceFfiError *device_info_sysmon_system_attributes(struct DeviceInfoHandle *handle,
+ char ***attrs_out,
+ uintptr_t *count_out);
+
+/**
+ * Returns directory listing for the given path
+ *
+ * # Safety
+ * All pointers must be valid and non-null. Free with `device_info_string_array_free`.
+ */
+struct IdeviceFfiError *device_info_directory_listing(struct DeviceInfoHandle *handle,
+ const char *path,
+ char ***entries_out,
+ uintptr_t *count_out);
+
+/**
+ * Creates a new EnergyMonitorClient from a RemoteServerClient
+ *
+ * # Safety
+ * `server` 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
+ */
+struct IdeviceFfiError *energy_monitor_new(struct RemoteServerHandle *server,
+ struct EnergyMonitorHandle **handle);
+
+/**
+ * Frees an EnergyMonitorClient handle
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library or NULL
+ */
+void energy_monitor_free(struct EnergyMonitorHandle *handle);
+
+/**
+ * Starts energy sampling for the given PIDs.
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library.
+ * If `pids` is non-null it must point to at least `pids_count` readable `u32` values.
+ */
+struct IdeviceFfiError *energy_monitor_start_sampling(struct EnergyMonitorHandle *handle,
+ const uint32_t *pids,
+ uintptr_t pids_count);
+
+/**
+ * Stops energy sampling for the given PIDs.
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library.
+ * If `pids` is non-null it must point to at least `pids_count` readable `u32` values.
+ */
+struct IdeviceFfiError *energy_monitor_stop_sampling(struct EnergyMonitorHandle *handle,
+ const uint32_t *pids,
+ uintptr_t pids_count);
+
+/**
+ * Requests a one-shot energy sample and parses the response.
+ *
+ * # Arguments
+ * * [`handle`] - The EnergyMonitorClient handle
+ * * [`pids`] - Pointer to an array of u32 PIDs to sample
+ * * [`pids_count`] - Number of elements in `pids`
+ * * [`samples_out`] - On success, set to a heap-allocated array of IdeviceEnergySample
+ * * [`samples_count_out`] - On success, set to the number of samples
+ *
+ * # Returns
+ * An IdeviceFfiError on error, null on success
+ *
+ * # Safety
+ * All output pointers must be valid and non-null. Free the array with
+ * `energy_monitor_samples_free`.
+ */
+struct IdeviceFfiError *energy_monitor_sample_attributes(struct EnergyMonitorHandle *handle,
+ const uint32_t *pids,
+ uintptr_t pids_count,
+ struct IdeviceEnergySample **samples_out,
+ uintptr_t *samples_count_out);
+
+/**
+ * Frees an array of IdeviceEnergySample allocated by `energy_monitor_sample_attributes`.
+ *
+ * # Safety
+ * `samples` must be a pointer returned by this library with the matching `count`, or NULL
+ */
+void energy_monitor_samples_free(struct IdeviceEnergySample *samples, uintptr_t count);
+
+/**
+ * Frees an IdeviceGraphicsSample and its heap-allocated string field
+ *
+ * # Safety
+ * `sample` must be a valid pointer allocated by this library or NULL
+ */
+void graphics_sample_free(struct IdeviceGraphicsSample *sample);
+
+/**
+ * Creates a new GraphicsClient from a RemoteServerClient
+ *
+ * # Safety
+ * `server` 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
+ */
+struct IdeviceFfiError *graphics_new(struct RemoteServerHandle *server,
+ struct GraphicsHandle **handle);
+
+/**
+ * Frees a GraphicsClient handle
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library or NULL
+ */
+void graphics_free(struct GraphicsHandle *handle);
+
+/**
+ * Starts graphics sampling at the given interval. Consumes the device's initial reply internally.
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library
+ */
+struct IdeviceFfiError *graphics_start_sampling(struct GraphicsHandle *handle, double interval);
+
+/**
+ * Stops graphics sampling.
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library
+ */
+struct IdeviceFfiError *graphics_stop_sampling(struct GraphicsHandle *handle);
+
+/**
+ * Reads the next graphics data frame pushed by the device. Blocks until a frame arrives.
+ *
+ * # Arguments
+ * * [`handle`] - The GraphicsClient handle
+ * * [`sample_out`] - On success, set to a heap-allocated IdeviceGraphicsSample
+ *
+ * # Returns
+ * An IdeviceFfiError on error, null on success
+ *
+ * # Safety
+ * All pointers must be valid and non-null. Free the sample with `graphics_sample_free`.
+ */
+struct IdeviceFfiError *graphics_next_sample(struct GraphicsHandle *handle,
+ struct IdeviceGraphicsSample **sample_out);
+
+/**
+ * Frees an IdeviceNetworkEvent and its heap-allocated string fields
+ *
+ * # Safety
+ * `event` must be a valid pointer allocated by this library or NULL
+ */
+void network_monitor_event_free(struct IdeviceNetworkEvent *event);
+
+/**
+ * Creates a new NetworkMonitorClient from a RemoteServerClient
+ *
+ * # Safety
+ * `server` 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
+ */
+struct IdeviceFfiError *network_monitor_new(struct RemoteServerHandle *server,
+ struct NetworkMonitorHandle **handle);
+
+/**
+ * Frees a NetworkMonitorClient handle
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library or NULL
+ */
+void network_monitor_free(struct NetworkMonitorHandle *handle);
+
+/**
+ * Starts network monitoring. No reply is expected.
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library
+ */
+struct IdeviceFfiError *network_monitor_start(struct NetworkMonitorHandle *handle);
+
+/**
+ * Stops network monitoring.
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library
+ */
+struct IdeviceFfiError *network_monitor_stop(struct NetworkMonitorHandle *handle);
+
+/**
+ * Reads the next network event pushed by the device. Blocks until an event arrives.
+ *
+ * # Arguments
+ * * [`handle`] - The NetworkMonitorClient handle
+ * * [`event_out`] - On success, set to a heap-allocated IdeviceNetworkEvent
+ *
+ * # Returns
+ * An IdeviceFfiError on error, null on success
+ *
+ * # Safety
+ * All pointers must be valid and non-null. Free the event with `network_monitor_event_free`.
+ */
+struct IdeviceFfiError *network_monitor_next_event(struct NetworkMonitorHandle *handle,
+ struct IdeviceNetworkEvent **event_out);
+
+/**
+ * Creates a new SysmontapClient from a RemoteServerClient
+ *
+ * # Safety
+ * `server` 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
+ */
+struct IdeviceFfiError *sysmontap_new(struct RemoteServerHandle *server,
+ struct SysmontapHandle **handle);
+
+/**
+ * Frees a SysmontapClient handle
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library or NULL
+ */
+void sysmontap_free(struct SysmontapHandle *handle);
+
+/**
+ * Sends configuration to the device
+ *
+ * # Arguments
+ * * [`handle`] - The SysmontapClient handle
+ * * [`config`] - Pointer to an IdeviceSysmontapConfig struct
+ *
+ * # Returns
+ * An IdeviceFfiError on error, null on success
+ *
+ * # Safety
+ * All pointers must be valid and non-null. String arrays must contain valid C strings.
+ */
+struct IdeviceFfiError *sysmontap_set_config(struct SysmontapHandle *handle,
+ const struct IdeviceSysmontapConfig *config);
+
+/**
+ * Starts sampling. Consumes the device's initial ack message internally.
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library
+ */
+struct IdeviceFfiError *sysmontap_start(struct SysmontapHandle *handle);
+
+/**
+ * Stops sampling.
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library
+ */
+struct IdeviceFfiError *sysmontap_stop(struct SysmontapHandle *handle);
+
+/**
+ * Reads the next sysmontap sample. Blocks until data arrives.
+ *
+ * Each output plist is a dictionary (or NULL if that field was not present in the sample):
+ * - `processes_out`: dict of PID → per-process attribute array
+ * - `system_out`: plist array of system attribute values
+ * - `cpu_usage_out`: dict of CPU usage keys
+ *
+ * The caller is responsible for freeing non-NULL plists with `plist_free`.
+ *
+ * # Safety
+ * `handle` must be valid and non-null. Output pointers may be null to ignore that field.
+ */
+struct IdeviceFfiError *sysmontap_next_sample(struct SysmontapHandle *handle,
+ plist_t *processes_out,
+ plist_t *system_out,
+ plist_t *cpu_usage_out);
+
/**
* Frees the IdeviceFfiError
*
@@ -4113,6 +6270,7 @@ struct IdeviceFfiError *image_mounter_mount_personalized(struct ImageMounterHand
* # Returns
* An IdeviceFfiError on error, null on success
*
+ * # Safety
* All pointers must be valid (except optional ones which can be null)
*/
struct IdeviceFfiError *image_mounter_mount_personalized_rsd(struct ImageMounterHandle *client,
@@ -4544,6 +6702,118 @@ void notification_proxy_free_string(char *s);
*/
void notification_proxy_client_free(struct NotificationProxyClientHandle *handle);
+/**
+ * Connects to the remote notification proxy over RSD
+ *
+ * # Arguments
+ * * [`provider`] - An adapter created by this library
+ * * [`handshake`] - An RSD handshake from the same provider
+ * * [`client`] - On success, will be set to point to a newly allocated handle
+ *
+ * # Returns
+ * An IdeviceFfiError on error, null on success
+ *
+ * # Safety
+ * `provider` and `handshake` must be valid pointers to handles allocated by this library
+ * `client` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+struct IdeviceFfiError *remote_notification_proxy_connect_rsd(struct AdapterHandle *provider,
+ struct RsdHandshakeHandle *handshake,
+ struct RemoteNotificationProxyClientHandle **client);
+
+/**
+ * Creates a remote notification proxy client from a socket
+ *
+ * # Arguments
+ * * [`socket`] - The socket to use for communication. Consumed regardless of the result.
+ * * [`client`] - On success, will be set to point to a newly allocated handle
+ *
+ * # Returns
+ * An IdeviceFfiError on error, null on success
+ *
+ * # Safety
+ * `socket` must be a valid pointer to a handle allocated by this library
+ * `client` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+struct IdeviceFfiError *remote_notification_proxy_new(struct ReadWriteOpaque *socket,
+ struct RemoteNotificationProxyClientHandle **client);
+
+/**
+ * Posts a notification on the device
+ *
+ * # Arguments
+ * * [`client`] - A valid handle
+ * * [`name`] - The notification to post
+ *
+ * # Returns
+ * An IdeviceFfiError on error, null on success
+ *
+ * # Safety
+ * All pointer parameters must be valid
+ */
+struct IdeviceFfiError *remote_notification_proxy_post(struct RemoteNotificationProxyClientHandle *client,
+ const char *name);
+
+/**
+ * Registers interest in a notification, after which the device relays it back
+ * whenever it fires
+ *
+ * # Arguments
+ * * [`client`] - A valid handle
+ * * [`name`] - The notification to observe
+ *
+ * # Returns
+ * An IdeviceFfiError on error, null on success
+ *
+ * # Safety
+ * All pointer parameters must be valid
+ */
+struct IdeviceFfiError *remote_notification_proxy_observe(struct RemoteNotificationProxyClientHandle *client,
+ const char *name);
+
+/**
+ * Registers interest in several notifications at once
+ *
+ * # Arguments
+ * * [`client`] - A valid handle
+ * * [`names`] - The notifications to observe
+ * * [`len`] - How many notifications were passed
+ *
+ * # Returns
+ * An IdeviceFfiError on error, null on success
+ *
+ * # Safety
+ * All pointer parameters must be valid, and `names` must hold `len` strings
+ */
+struct IdeviceFfiError *remote_notification_proxy_observe_multiple(struct RemoteNotificationProxyClientHandle *client,
+ const char *const *names,
+ uintptr_t len);
+
+/**
+ * Waits for the next relayed notification and returns its name
+ *
+ * # Arguments
+ * * [`client`] - A valid handle
+ * * [`name_out`] - On success, set to the notification's name. Free with
+ * `notification_proxy_free_string`.
+ *
+ * # Returns
+ * An IdeviceFfiError on error, null on success
+ *
+ * # Safety
+ * All pointer parameters must be valid
+ */
+struct IdeviceFfiError *remote_notification_proxy_receive(struct RemoteNotificationProxyClientHandle *client,
+ char **name_out);
+
+/**
+ * Frees a remote notification proxy handle
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library, or NULL
+ */
+void remote_notification_proxy_free(struct RemoteNotificationProxyClientHandle *handle);
+
/**
* Connects to the relay with the given provider
*
@@ -4666,6 +6936,109 @@ struct IdeviceFfiError *os_trace_relay_next(struct OsTraceRelayReceiverHandle *c
*/
void os_trace_relay_free_log(struct OsTraceLog *log);
+/**
+ * Creates a cancellation token for `pairable_host_accept`.
+ *
+ * Returns NULL only if allocation fails. Free with `pairable_host_cancel_free`.
+ */
+struct PairableHostCancel *pairable_host_cancel_new(void);
+
+/**
+ * Signals a cancellation token, unblocking the `pairable_host_accept` it was passed
+ * to. That call returns the `CanceledByUser` error.
+ *
+ * Safe to call from any thread, before or during the accept, and safe to call more
+ * than once. Cancelling a token that was never passed to an accept, or one whose
+ * accept already returned, does nothing.
+ *
+ * # Safety
+ * `cancel` must be a pointer returned by `pairable_host_cancel_new` that has not yet
+ * been freed.
+ */
+void pairable_host_cancel_signal(const struct PairableHostCancel *cancel);
+
+/**
+ * Frees a cancellation token.
+ *
+ * The in-flight accept holds its own reference to the shared state, so freeing the
+ * token while an accept is still running is safe — it just means nothing can cancel
+ * that accept any more.
+ *
+ * # Safety
+ * `cancel` must be a pointer returned by `pairable_host_cancel_new` or NULL, and must
+ * not be used afterwards.
+ */
+void pairable_host_cancel_free(struct PairableHostCancel *cancel);
+
+/**
+ * Advertises this computer as a pairable host and accepts a single device-initiated
+ * pairing.
+ *
+ * This blocks the calling thread until a device discovers the advertised
+ * `_remotepairing-pairable-host._tcp` service, connects, and the pairing either
+ * completes or fails — or until `cancel` is signalled from another thread. While the
+ * pairing is in progress `pin_callback` is invoked once with the 6-digit setup code
+ * that the user must type into the device.
+ *
+ * On success a freshly generated [`RpPairingFileHandle`] is written to
+ * `out_pairing_file`; it carries this host's long-term keys plus the paired
+ * device's `altIRK`. Persist it (and `out_host_alt_irk`, see below) so the device
+ * keeps recognizing this host on future connections.
+ *
+ * # Arguments
+ * * `name` - human-readable name shown on the device (e.g. "Jackson's MacBook Pro").
+ * * `model` - hardware model identifier shown on the device. `NULL` defaults to
+ * `"Mac17,7"`. iOS treats the host as a computer, so keep this a Mac identifier.
+ * * `port` - TCP port to listen on. `0` picks a free port.
+ * * `pin_callback` - invoked with the setup PIN to display. May be `NULL`.
+ * * `pin_context` - opaque pointer passed back to `pin_callback`.
+ * * `cancel` - optional cancellation token from `pairable_host_cancel_new`. Signal it
+ * from another thread to abort the wait (e.g. the user dismissed the pairing UI).
+ * `NULL` means the call can only be ended by a device connecting. Without one there
+ * is no way to stop advertising short of exiting the process.
+ * * `out_host_alt_irk` - optional. If non-NULL, must point to a 16-byte buffer that
+ * receives the host's generated `altIRK` (needed to re-advertise this host so an
+ * already-paired device recognizes it). May be `NULL`.
+ * * `out_peer_device` - optional. If non-NULL, receives the paired device's identity
+ * (name, model, UDID, `altIRK`), which the caller must free with
+ * `rppairing_peer_device_free`. May be `NULL`.
+ * * `out_pairing_file` - receives the resulting pairing file on success.
+ *
+ * # Safety
+ * `name` must be a valid null-terminated C string. `model` must be NULL or a valid
+ * null-terminated C string. `cancel` must be NULL or a live token from
+ * `pairable_host_cancel_new`. `out_host_alt_irk` must be NULL or point to at least 16
+ * writable bytes. `out_peer_device` must be NULL or a valid writable pointer.
+ * `out_pairing_file` must be valid and non-null.
+ */
+struct IdeviceFfiError *pairable_host_accept(const char *name,
+ const char *model,
+ uint16_t port,
+ void (*pin_callback)(const char *pin, void *context),
+ void *pin_context,
+ const struct PairableHostCancel *cancel,
+ uint8_t *out_host_alt_irk,
+ struct RpPairingPeerDeviceC **out_peer_device,
+ struct RpPairingFileHandle **out_pairing_file);
+
+/**
+ * Same as `pairable_host_accept`, with explicit pairable-host policy options.
+ *
+ * # Safety
+ * Same pointer validity requirements as `pairable_host_accept`.
+ */
+struct IdeviceFfiError *pairable_host_accept_with_options(const char *name,
+ const char *model,
+ uint16_t port,
+ bool allows_pinless_pairing,
+ void (*pin_callback)(const char *pin,
+ void *context),
+ void *pin_context,
+ const struct PairableHostCancel *cancel,
+ uint8_t *out_host_alt_irk,
+ struct RpPairingPeerDeviceC **out_peer_device,
+ struct RpPairingFileHandle **out_pairing_file);
+
/**
* Reads a pairing file from the specified path
*
@@ -4734,6 +7107,95 @@ struct IdeviceFfiError *idevice_pairing_file_serialize(const struct IdevicePairi
*/
void idevice_pairing_file_free(struct IdevicePairingFile *pairing_file);
+/**
+ * Generates a fresh host identity and returns the data a caller needs to publish
+ * its own `_remotepairing-pairable-host._tcp` Bonjour service.
+ *
+ * # Arguments
+ * * `name` - human-readable name shown on the device.
+ * * `model` - hardware model shown on the device. `NULL` defaults to `"Mac17,7"`.
+ * * `allows_pinless_pairing` - if true, advertise pinless pairing and use the
+ * all-zero setup code expected by that flow; if false, generate a random PIN.
+ * * `out_handle` - receives the host handle; pass it to `pairable_host_accept_fd`
+ * and free it with `pairable_host_free`.
+ * * `out_service_id` - receives the Bonjour service instance name. Free with
+ * `idevice_string_free`.
+ * * `out_txt_data`/`out_txt_len` - receive an XML plist dictionary of the TXT
+ * records to publish. Free with `idevice_data_free`.
+ * * `out_host_alt_irk` - optional. If non-NULL, must point to a 16-byte buffer
+ * that receives the generated host `altIRK`; persist it with the pairing file.
+ *
+ * A fresh identity is generated on every call.
+ *
+ * # Safety
+ * `name` must be a valid null-terminated C string. `model` must be NULL or a
+ * valid null-terminated C string. All required out-pointers must be valid and
+ * non-null. `out_host_alt_irk` must be NULL or point to at least 16 writable bytes.
+ */
+struct IdeviceFfiError *pairable_host_prepare(const char *name,
+ const char *model,
+ bool allows_pinless_pairing,
+ struct PairableHostHandle **out_handle,
+ char **out_service_id,
+ uint8_t **out_txt_data,
+ uintptr_t *out_txt_len,
+ uint8_t *out_host_alt_irk);
+
+/**
+ * Backwards-compatible alias for AltStore's original function name.
+ * Prefer `pairable_host_prepare` for new callers.
+ *
+ * # Safety
+ * Same requirements as `pairable_host_prepare`, except `model` is required and
+ * pinless pairing is disabled.
+ */
+struct IdeviceFfiError *pairable_host_new(const char *name,
+ const char *model,
+ struct PairableHostHandle **out_handle,
+ char **out_service_id,
+ uint8_t **out_txt_data,
+ uintptr_t *out_txt_len);
+
+/**
+ * Runs pair-setup against a device that has already connected to `fd`.
+ *
+ * Blocks the calling thread until pairing succeeds or fails. The fd is duplicated
+ * before use, so the caller keeps ownership of the original socket.
+ *
+ * # Safety
+ * `handle` must be a valid handle from `pairable_host_prepare` or
+ * `pairable_host_new`. `fd` must be a valid connected TCP socket.
+ * `out_pairing_file` must be valid and non-null. `out_peer_device` must be NULL
+ * or a valid writable pointer. `pin_cb`/`ctx` must stay valid until this call returns.
+ */
+struct IdeviceFfiError *pairable_host_accept_fd(struct PairableHostHandle *handle,
+ int32_t fd,
+ PairableHostPinCb pin_cb,
+ void *ctx,
+ struct RpPairingPeerDeviceC **out_peer_device,
+ struct RpPairingFileHandle **out_pairing_file);
+
+/**
+ * Backwards-compatible alias for AltStore's original function name.
+ * Prefer `pairable_host_accept_fd` for new callers.
+ *
+ * # Safety
+ * Same requirements as `pairable_host_accept_fd`.
+ */
+struct IdeviceFfiError *pairable_host_handshake(struct PairableHostHandle *handle,
+ int32_t fd,
+ PairableHostPinCb pin_cb,
+ void *ctx,
+ struct RpPairingFileHandle **out_pairing_file);
+
+/**
+ * Frees a `PairableHostHandle`.
+ *
+ * # Safety
+ * `handle` must be a handle from `pairable_host_prepare`/`pairable_host_new`, or NULL.
+ */
+void pairable_host_free(struct PairableHostHandle *handle);
+
/**
* Automatically creates and connects to pcapd, returning a client handle.
* Note that this service only works over USB or through RSD.
@@ -4886,12 +7348,14 @@ struct IdeviceFfiError *preboard_service_new(struct IdeviceHandle *socket,
struct PreboardServiceClientHandle **client);
/**
- * Creates a stashbag on the device
+ * Creates a stashbag on the device from a local preboard manifest (will prompt
+ * for the passcode on the device), writing the outcome to `out_outcome`
*
* # Arguments
* * `client` - A valid PreboardServiceClient handle
* * `manifest` - Pointer to the manifest data
* * `manifest_len` - Length of the manifest data
+ * * `out_outcome` - On success, set to whether a commit is required
*
* # Returns
* An IdeviceFfiError on error, null on success
@@ -4899,10 +7363,12 @@ struct IdeviceFfiError *preboard_service_new(struct IdeviceHandle *socket,
* # Safety
* `client` must be a valid pointer to a handle allocated by this library
* `manifest` must be a valid pointer to `manifest_len` bytes of data
+ * `out_outcome` must be a valid, non-null pointer
*/
struct IdeviceFfiError *preboard_service_create_stashbag(struct PreboardServiceClientHandle *client,
const uint8_t *manifest,
- uintptr_t manifest_len);
+ uintptr_t manifest_len,
+ enum IdeviceStashbagOutcome *out_outcome);
/**
* Commits a stashbag on the device
@@ -5013,6 +7479,619 @@ struct IdeviceFfiError *usbmuxd_provider_new(struct UsbmuxdAddrHandle *addr,
struct IdeviceFfiError *idevice_provider_get_pairing_file(struct IdeviceProviderHandle *provider,
struct IdevicePairingFile **pairing_file);
+/**
+ * Connects to `remotepairingdeviced` over lockdown
+ *
+ * # Arguments
+ * * [`provider`] - An IdeviceProvider
+ * * [`sending_host`] - The name this computer identifies itself by, the same
+ * value the wireless flow uses
+ * * [`handle`] - Pointer to store the newly created handle
+ *
+ * # Returns
+ * An IdeviceFfiError on error, null on success
+ *
+ * # Safety
+ * All pointer parameters must be valid
+ */
+struct IdeviceFfiError *remote_pairing_lockdown_connect(struct IdeviceProviderHandle *provider,
+ const char *sending_host,
+ struct RemotePairingLockdownHandle **handle);
+
+/**
+ * Wraps an existing lockdown connection to `remotepairingdeviced`
+ *
+ * # Arguments
+ * * [`socket`] - A connection to `com.apple.dt.remotepairingdeviced.lockdown`.
+ * Consumed regardless of the result.
+ * * [`sending_host`] - The name this computer identifies itself by
+ * * [`handle`] - Pointer to store the newly created handle
+ *
+ * # Returns
+ * An IdeviceFfiError on error, null on success
+ *
+ * # Safety
+ * All pointer parameters must be valid
+ */
+struct IdeviceFfiError *remote_pairing_lockdown_new(struct IdeviceHandle *socket,
+ const char *sending_host,
+ struct RemotePairingLockdownHandle **handle);
+
+/**
+ * Runs the control channel's handshake and returns what the device reports
+ * about itself
+ *
+ * # Arguments
+ * * [`handle`] - The client handle
+ * * [`handshake`] - Pointer to store the device's response
+ *
+ * # Returns
+ * An IdeviceFfiError on error, null on success
+ *
+ * # Safety
+ * All pointer parameters must be valid
+ */
+struct IdeviceFfiError *remote_pairing_lockdown_attempt_pair_verify(struct RemotePairingLockdownHandle *handle,
+ plist_t *handshake);
+
+/**
+ * Checks whether the device still recognizes a pairing record
+ *
+ * The handshake must have run first, i.e.
+ * `remote_pairing_lockdown_attempt_pair_verify`.
+ *
+ * # Arguments
+ * * [`handle`] - The client handle
+ * * [`pairing_file`] - The RPPairing file to validate
+ *
+ * # Returns
+ * An IdeviceFfiError on error, null on success
+ *
+ * # Safety
+ * All pointer parameters must be valid
+ */
+struct IdeviceFfiError *remote_pairing_lockdown_validate_pairing(struct RemotePairingLockdownHandle *handle,
+ struct RpPairingFileHandle *pairing_file);
+
+/**
+ * Pairs with the device, saving the record into `pairing_file`
+ *
+ * # Arguments
+ * * [`handle`] - The client handle
+ * * [`pairing_file`] - The RPPairing file to pair with, e.g. a fresh one from
+ * `rp_pairing_file_generate`. Updated in place on success, so write it out
+ * afterwards to keep the pairing.
+ * * [`pin`] - The PIN to answer a Trust prompt with, or NULL for `000000`.
+ * Pairing over USB is promptless, so the device should never ask.
+ *
+ * # Returns
+ * An IdeviceFfiError on error, null on success
+ *
+ * # Safety
+ * All non-NULL pointer parameters must be valid
+ */
+struct IdeviceFfiError *remote_pairing_lockdown_pair(struct RemotePairingLockdownHandle *handle,
+ struct RpPairingFileHandle *pairing_file,
+ const char *pin);
+
+/**
+ * Pairs only if the device doesn't already recognize the pairing record
+ *
+ * Runs the handshake, validates `pairing_file`, and pairs when that fails.
+ *
+ * # Arguments
+ * * [`handle`] - The client handle
+ * * [`pairing_file`] - The RPPairing file to validate or pair with. Updated in
+ * place when pairing happens, so write it out afterwards.
+ * * [`pin`] - The PIN to answer a Trust prompt with, or NULL for `000000`
+ *
+ * # Returns
+ * An IdeviceFfiError on error, null on success
+ *
+ * # Safety
+ * All non-NULL pointer parameters must be valid
+ */
+struct IdeviceFfiError *remote_pairing_lockdown_connect_pairing(struct RemotePairingLockdownHandle *handle,
+ struct RpPairingFileHandle *pairing_file,
+ const char *pin);
+
+/**
+ * The encryption key established during pairing, used as the TLS-PSK for
+ * tunnel connections
+ *
+ * # Arguments
+ * * [`handle`] - The client handle
+ * * [`key`] - Pointer to store the key, freed with `idevice_data_free`
+ * * [`key_len`] - Pointer to store the number of bytes
+ *
+ * # Returns
+ * An IdeviceFfiError on error, null on success
+ *
+ * # Safety
+ * All pointer parameters must be valid
+ */
+struct IdeviceFfiError *remote_pairing_lockdown_encryption_key(struct RemotePairingLockdownHandle *handle,
+ uint8_t **key,
+ uintptr_t *key_len);
+
+/**
+ * Frees a remote pairing lockdown handle
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library or NULL
+ */
+void remote_pairing_lockdown_free(struct RemotePairingLockdownHandle *handle);
+
+/**
+ * Connects to `restored` over an existing [`IdeviceHandle`] (consumes it).
+ *
+ * # Safety
+ * `idevice` is consumed and must not be used afterwards. `out_client` must be a
+ * valid, non-null location for the resulting handle.
+ */
+struct IdeviceFfiError *idevice_restored_connect(struct IdeviceHandle *idevice,
+ struct RestoredClientHandle **out_client);
+
+/**
+ * Finds a restore-mode device by ECID over usbmux and connects to `restored`.
+ *
+ * After a normal to restore transition the device re-enumerates with a new usbmux
+ * id, so this polls the device list and matches on `HardwareInfo.UniqueChipID`,
+ * retrying until `timeout_ms` elapses.
+ *
+ * # Safety
+ * `addr` must be a valid `UsbmuxdAddrHandle` (it is borrowed, not consumed);
+ * `out_client` must be valid; `label` a valid C string or NULL.
+ */
+struct IdeviceFfiError *idevice_restored_connect_by_ecid(struct UsbmuxdAddrHandle *addr,
+ uint64_t ecid,
+ const char *label,
+ uint64_t timeout_ms,
+ struct RestoredClientHandle **out_client);
+
+/**
+ * Reads the device's ECID (from `HardwareInfo`).
+ *
+ * # Safety
+ * `client` and `out_ecid` must be valid, non-null pointers.
+ */
+struct IdeviceFfiError *idevice_restored_get_ecid(struct RestoredClientHandle *client,
+ uint64_t *out_ecid);
+
+/**
+ * Reads the usbmux `device_id` the client was found on.
+ *
+ * Only meaningful when the client was created with
+ * `idevice_restored_connect_by_ecid`; writes `true` to `out_has_device_id` in
+ * that case (and the id to `out_device_id`), or `false` otherwise (e.g. clients
+ * built from an existing `Idevice`). Pass the id to
+ * `idevice_restore_connect_usb_port` so data-port / FDR connections target this
+ * same device.
+ *
+ * # Safety
+ * `client`, `out_device_id`, `out_has_device_id` must be valid, non-null pointers.
+ */
+struct IdeviceFfiError *idevice_restored_get_device_id(struct RestoredClientHandle *client,
+ uint32_t *out_device_id,
+ bool *out_has_device_id);
+
+/**
+ * Frees a [`RestoredClientHandle`].
+ *
+ * # Safety
+ * `client` must be a handle allocated by this library, or NULL.
+ */
+void idevice_restored_free(struct RestoredClientHandle *client);
+
+/**
+ * Connects to `port` on the usbmux device identified by `device_id`, returning a
+ * new [`IdeviceHandle`]. A convenience for restore data-port and FDR connectors.
+ *
+ * `device_id` must be the id `idevice_restored_get_device_id` reported for the
+ * restore-mode client, so the connection targets the device being restored.
+ *
+ * The entire find-device-and-connect sequence runs in one async task; splitting
+ * it across separate blocking calls corrupts the shared tokio I/O state, so this
+ * is the supported way to build those connectors from C.
+ *
+ * This is a single attempt (the restore state machine retries data-port
+ * connects itself); it errors rather than blocking when the device or port is
+ * not yet available.
+ *
+ * # Safety
+ * `addr` must be a valid `UsbmuxdAddrHandle` (it is borrowed, not consumed);
+ * `out_idevice` must be valid; `label` a valid C string or NULL.
+ */
+struct IdeviceFfiError *idevice_restore_connect_usb_port(struct UsbmuxdAddrHandle *addr,
+ uint32_t device_id,
+ uint16_t port,
+ const char *label,
+ struct IdeviceHandle **out_idevice);
+
+/**
+ * Allocates a cancellation handle.
+ *
+ * # Safety
+ * `out_handle` must be a valid, non-null location for the handle pointer.
+ */
+struct IdeviceFfiError *idevice_restore_cancel_handle_new(struct IdeviceRestoreCancelHandle **out_handle);
+
+/**
+ * Requests cancellation of the restore this handle was passed to.
+ *
+ * Safe to call from any thread while the restore runs; it is a no-op if `handle`
+ * is NULL.
+ *
+ * # Safety
+ * `handle` must be a valid handle from `idevice_restore_cancel_handle_new` (or NULL).
+ */
+void idevice_restore_cancel(struct IdeviceRestoreCancelHandle *handle);
+
+/**
+ * Frees a cancellation handle.
+ *
+ * # Safety
+ * `handle` must be a valid handle from `idevice_restore_cancel_handle_new` (or NULL)
+ * and must not be used after this call.
+ */
+void idevice_restore_cancel_handle_free(struct IdeviceRestoreCancelHandle *handle);
+
+/**
+ * Builds the default iOS `RestoreOptions` dictionary sent with `StartRestore`.
+ *
+ * The caller may tweak the returned plist before passing it to
+ * `idevice_restore_run`, and must free it with `plist_free`.
+ *
+ * # Safety
+ * `out_options` must be a valid, non-null location for the plist.
+ */
+struct IdeviceFfiError *idevice_restore_options_new(plist_t *out_options);
+
+/**
+ * Drives the restore-mode state machine to completion.
+ *
+ * Sends `StartRestore` with `options`, then services the device's data requests
+ * (personalizing components with `tss_ticket`, streaming the filesystem over
+ * ASR, proxying its key requests) until the device reports success.
+ *
+ * # Arguments
+ * * `client` - connected [`RestoredClientHandle`].
+ * * `build_identity` - the selected build-identity dictionary (plist).
+ * * `board_id`, `chip_id`, `ecid` - device identifiers.
+ * * `tss_ticket`/`tss_ticket_len` - the `ApImg4Ticket` (IM4M) from TSS.
+ * * `components` - component-source delegate (required).
+ * * `filesystem` - filesystem-image delegate, or NULL for a restore that sends
+ * no filesystem.
+ * * `data_ports` - data-port connector delegate (required).
+ * * `progress` - progress delegate, or NULL.
+ * * `cancel` - cancellation handle from `idevice_restore_cancel_handle_new`, or
+ * NULL. When another thread calls `idevice_restore_cancel` on it, the restore
+ * stops and the device is rebooted toward recovery (returning a `Cancelled`
+ * error).
+ * * `options` - the `RestoreOptions` plist (see `idevice_restore_options_new`).
+ *
+ * # Safety
+ * All non-NULL pointers must be valid for the duration of the call, and each
+ * delegate struct must remain valid until this returns.
+ */
+struct IdeviceFfiError *idevice_restore_run(struct RestoredClientHandle *client,
+ plist_t build_identity,
+ uint64_t board_id,
+ uint64_t chip_id,
+ uint64_t ecid,
+ const uint8_t *tss_ticket,
+ uintptr_t tss_ticket_len,
+ struct IdeviceRestoreComponentSourceFFI *components,
+ struct IdeviceRestoreFilesystemImageFFI *filesystem,
+ struct IdeviceRestoreDataPortConnectorFFI *data_ports,
+ struct IdeviceRestoreProgressFFI *progress,
+ struct IdeviceRestoreCancelHandle *cancel,
+ plist_t options);
+
+/**
+ * Opens an IPSW archive from a filesystem path.
+ *
+ * # Safety
+ * `path` must be a valid C string; `out_ipsw` a valid, non-null location.
+ */
+struct IdeviceFfiError *idevice_ipsw_open(const char *path, struct IpswHandle **out_ipsw);
+
+/**
+ * Reads and parses the archive's `BuildManifest.plist`.
+ *
+ * # Safety
+ * `ipsw` must be a valid handle; `out_manifest` a valid, non-null location. The
+ * returned plist must be freed with `plist_free`.
+ */
+struct IdeviceFfiError *idevice_ipsw_build_manifest(struct IpswHandle *ipsw, plist_t *out_manifest);
+
+/**
+ * Reads a component named in `build_identity` into a caller-freed buffer.
+ *
+ * # Safety
+ * `ipsw`, `name`, `out_data`, `out_len` must be valid. Free the buffer with
+ * `idevice_data_free`.
+ */
+struct IdeviceFfiError *idevice_ipsw_read_component(struct IpswHandle *ipsw,
+ plist_t build_identity,
+ const char *name,
+ uint8_t **out_data,
+ uintptr_t *out_len);
+
+/**
+ * Reads an arbitrary archive entry by exact path into a caller-freed buffer.
+ *
+ * # Safety
+ * `ipsw`, `path`, `out_data`, `out_len` must be valid. Free with
+ * `idevice_data_free`.
+ */
+struct IdeviceFfiError *idevice_ipsw_read_file(struct IpswHandle *ipsw,
+ const char *path,
+ uint8_t **out_data,
+ uintptr_t *out_len);
+
+/**
+ * Extracts an archive entry to a file on disk (streamed, for large images).
+ *
+ * # Safety
+ * `ipsw`, `entry_path`, `dest_path` must be valid C strings.
+ */
+struct IdeviceFfiError *idevice_ipsw_extract_to_file(struct IpswHandle *ipsw,
+ const char *entry_path,
+ const char *dest_path);
+
+/**
+ * Frees an [`IpswHandle`].
+ *
+ * # Safety
+ * `ipsw` must be a handle allocated by this library, or NULL.
+ */
+void idevice_ipsw_free(struct IpswHandle *ipsw);
+
+/**
+ * Selects the `BuildIdentity` matching `board_id`/`chip_id` (and, when non-NULL,
+ * `restore_behavior`, e.g. "Erase"/"Update") from a `BuildManifest` plist.
+ *
+ * # Safety
+ * `build_manifest`, `out_identity` must be valid. The result plist must be freed
+ * with `plist_free`.
+ */
+struct IdeviceFfiError *idevice_restore_select_build_identity(plist_t build_manifest,
+ uint64_t board_id,
+ uint64_t chip_id,
+ const char *restore_behavior,
+ plist_t *out_identity);
+
+/**
+ * Resolves the archive path of a component from a build identity's `Manifest`.
+ *
+ * # Safety
+ * `build_identity`, `name`, `out_path` must be valid. Free the string with
+ * `idevice_string_free`.
+ */
+struct IdeviceFfiError *idevice_restore_component_path(plist_t build_identity,
+ const char *name,
+ char **out_path);
+
+/**
+ * Fetches the AP `ApImg4Ticket` (IM4M) from Apple's TSS server for a build
+ * identity and device, returning the ticket bytes.
+ *
+ * `ap_nonce`/`sep_nonce` may be NULL (length 0); a NULL `sep_nonce` is signed
+ * with a zeroed nonce.
+ *
+ * # Safety
+ * `build_identity`, `out_ticket`, `out_ticket_len` must be valid. Free the ticket
+ * with `idevice_data_free`.
+ */
+struct IdeviceFfiError *idevice_restore_fetch_ap_ticket(plist_t build_identity,
+ uint64_t board_id,
+ uint64_t chip_id,
+ uint64_t ecid,
+ const uint8_t *ap_nonce,
+ uintptr_t ap_nonce_len,
+ const uint8_t *sep_nonce,
+ uintptr_t sep_nonce_len,
+ uint8_t **out_ticket,
+ uintptr_t *out_ticket_len);
+
+/**
+ * Stitches an `IM4P` component with the `ApImg4Ticket` into a personalized
+ * `IMG4` the device will accept.
+ *
+ * `fourcc` is either NULL (keep the payload's own type) or a pointer to exactly
+ * four bytes to re-tag the payload with (required for some `Restore*` components;
+ * see the library's `restore_fourcc_override`).
+ *
+ * # Safety
+ * `im4p`, `ticket`, `out_data`, `out_len` must be valid. If non-NULL, `fourcc`
+ * must point to 4 readable bytes. Free the buffer with `idevice_data_free`.
+ */
+struct IdeviceFfiError *idevice_img4_stitch_component(const uint8_t *im4p,
+ uintptr_t im4p_len,
+ const uint8_t *ticket,
+ uintptr_t ticket_len,
+ const uint8_t *fourcc,
+ uint8_t **out_data,
+ uintptr_t *out_len);
+
+/**
+ * Returns the four-character code a `Restore*` component must be re-tagged with,
+ * if any, writing four bytes to `out_fourcc`.
+ *
+ * Returns `true` and fills `out_fourcc` when the component needs re-tagging;
+ * returns `false` and leaves `out_fourcc` untouched otherwise.
+ *
+ * # Safety
+ * `component_name` must be a valid C string; `out_fourcc` must point to 4
+ * writable bytes.
+ */
+bool idevice_img4_restore_fourcc_override(const char *component_name, uint8_t *out_fourcc);
+
+/**
+ * Returns the components iBoot loads during the restore boot, in manifest order,
+ * as a newline-separated, NUL-terminated string (empty when none).
+ *
+ * # Safety
+ * `build_identity`, `out_names` must be valid. Free the string with
+ * `idevice_string_free`.
+ */
+struct IdeviceFfiError *idevice_restore_boot_component_names(plist_t build_identity,
+ char **out_names);
+
+/**
+ * Builds the local (unsigned) `IM4M` preboard manifest for a stashbag request
+ * from a build identity, into a caller-freed buffer.
+ *
+ * # Safety
+ * `build_identity`, `out_data`, `out_len` must be valid. Free the buffer with
+ * `idevice_data_free`.
+ */
+struct IdeviceFfiError *idevice_restore_build_preboard_manifest(plist_t build_identity,
+ uint64_t board_id,
+ uint64_t chip_id,
+ uint8_t **out_data,
+ uintptr_t *out_len);
+
+/**
+ * Opens a recovery/DFU device over a caller-supplied transport delegate.
+ *
+ * The `transport` struct is copied by value; the caller may free its own
+ * storage after this returns (the `context` pointer must stay valid).
+ *
+ * # Safety
+ * `transport` and `out_device` must be valid, non-null pointers.
+ */
+struct IdeviceFfiError *idevice_recovery_device_new(const struct IdeviceRestoreRecoveryTransportFFI *transport,
+ struct RecoveryDeviceHandle **out_device);
+
+/**
+ * Sends an iBoot command (NUL-terminated), with an explicit `bRequest`.
+ *
+ * # Safety
+ * `device`, `command` must be valid.
+ */
+struct IdeviceFfiError *idevice_recovery_send_command(struct RecoveryDeviceHandle *device,
+ const char *command,
+ uint8_t b_request);
+
+/**
+ * Uploads a firmware image (bulk in recovery mode, chunked control transfers
+ * in DFU mode).
+ *
+ * # Safety
+ * `device`, `data` must be valid.
+ */
+struct IdeviceFfiError *idevice_recovery_send_buffer(struct RecoveryDeviceHandle *device,
+ const uint8_t *data,
+ uintptr_t len);
+
+/**
+ * Reads an environment variable via `getenv` into a caller-freed buffer.
+ *
+ * # Safety
+ * `device`, `name`, `out_data`, `out_len` must be valid. Free with
+ * `idevice_data_free`.
+ */
+struct IdeviceFfiError *idevice_recovery_getenv(struct RecoveryDeviceHandle *device,
+ const char *name,
+ uint8_t **out_data,
+ uintptr_t *out_len);
+
+/**
+ * Sets an environment variable via `setenv`.
+ *
+ * # Safety
+ * `device`, `name`, `value` must be valid.
+ */
+struct IdeviceFfiError *idevice_recovery_setenv(struct RecoveryDeviceHandle *device,
+ const char *name,
+ const char *value);
+
+/**
+ * Enables or disables auto-boot and persists it (`saveenv`).
+ *
+ * # Safety
+ * `device` must be valid.
+ */
+struct IdeviceFfiError *idevice_recovery_set_autoboot(struct RecoveryDeviceHandle *device,
+ bool enable);
+
+/**
+ * Issues the zero-length `finish_transfer` control request.
+ *
+ * # Safety
+ * `device` must be valid.
+ */
+struct IdeviceFfiError *idevice_recovery_finish_transfer(struct RecoveryDeviceHandle *device);
+
+/**
+ * Reboots the device.
+ *
+ * # Safety
+ * `device` must be valid.
+ */
+struct IdeviceFfiError *idevice_recovery_reboot(struct RecoveryDeviceHandle *device);
+
+/**
+ * Returns the device's USB `idProduct` (identifying its mode), and whether it
+ * is a recovery (iBoot) mode as opposed to DFU/WTF.
+ *
+ * # Safety
+ * `device`, `out_product_id`, `out_is_recovery` must be valid.
+ */
+struct IdeviceFfiError *idevice_recovery_get_mode(struct RecoveryDeviceHandle *device,
+ uint16_t *out_product_id,
+ bool *out_is_recovery);
+
+/**
+ * Fills device identifiers parsed from the recovery serial string.
+ *
+ * Each `has_*` output is set to whether the corresponding value was present;
+ * missing values leave their `out_*` untouched.
+ *
+ * # Safety
+ * All non-null out-pointers must be valid.
+ */
+struct IdeviceFfiError *idevice_recovery_get_info(struct RecoveryDeviceHandle *device,
+ uint64_t *out_cpid,
+ bool *out_has_cpid,
+ uint64_t *out_bdid,
+ bool *out_has_bdid,
+ uint64_t *out_ecid,
+ bool *out_has_ecid);
+
+/**
+ * Returns the AP nonce (`NONC`) from the recovery serial, if present, into a
+ * caller-freed buffer. Returns `true` when a nonce was present.
+ *
+ * # Safety
+ * `device`, `out_data`, `out_len` must be valid. Free with `idevice_data_free`.
+ */
+bool idevice_recovery_get_ap_nonce(struct RecoveryDeviceHandle *device,
+ uint8_t **out_data,
+ uintptr_t *out_len);
+
+/**
+ * Frees a [`RecoveryDeviceHandle`].
+ *
+ * # Safety
+ * `device` must be a handle allocated by this library, or NULL.
+ */
+void idevice_recovery_device_free(struct RecoveryDeviceHandle *device);
+
+/**
+ * Starts the FDR trust channel: control handshake, then a background listener
+ * running for the rest of the restore.
+ *
+ * The `connector` struct is copied by value (its `context` must stay valid for
+ * the duration of the restore).
+ *
+ * # Safety
+ * `connector` must be a valid, non-null pointer.
+ */
+struct IdeviceFfiError *idevice_restore_fdr_start(const struct IdeviceRestoreFdrConnectorFFI *connector);
+
/**
* Creates a new RestoreServiceClient from a ReadWrite stream
*
@@ -5214,6 +8293,15 @@ struct IdeviceFfiError *rp_pairing_file_write(struct RpPairingFileHandle *handle
*/
void rp_pairing_file_free(struct RpPairingFileHandle *handle);
+/**
+ * Frees a peer device struct and its heap-allocated string fields.
+ *
+ * # Safety
+ * `peer_device` must be a pointer returned by `rppairing_pair_network` or
+ * `pairable_host_accept`, or NULL.
+ */
+void rppairing_peer_device_free(struct RpPairingPeerDeviceC *peer_device);
+
/**
* Creates a new RSD handshake from a ReadWrite connection
*
@@ -5768,8 +8856,12 @@ struct IdeviceFfiError *tunnel_create_remotexpc(const idevice_sockaddr *addr,
* Use this when connecting to a device discovered via `_remotepairing._tcp`.
* The connection goes: direct TCP → RPPairing (JSON) → tunnel.
*
- * This path only supports pair-verify (existing pairing file required).
- * For initial pairing, use `tunnel_pair_usb`.
+ * `pairing_file` is used for pair-verify. If verification fails (typically
+ * because the device has never been paired with this host) a full pair-setup
+ * runs on the same connection and `pairing_file` is updated in place, so the
+ * caller should persist it afterwards regardless of whether it was freshly
+ * generated.
+ *
*
* # Safety
* All pointer arguments must be valid and non-null (except `pin_callback`/`pin_context`).
@@ -5784,6 +8876,38 @@ struct IdeviceFfiError *tunnel_create_rppairing(const idevice_sockaddr *addr,
struct AdapterHandle **out_adapter,
struct RsdHandshakeHandle **out_handshake);
+/**
+ * Pairs with a device over the network via raw RPPairing, without creating a tunnel.
+ *
+ * This is for tvOS.
+ *
+ * On iOS `tunnel_create_rppairing` handles both halves on its own; this function
+ * is only needed there if you want to pair and connect as separate steps.
+ *
+ * # Arguments
+ * * `addr` / `addr_len` - address of the pairing service to connect to.
+ * * `hostname` - name this host presents to the device.
+ * * `pairing_file` - borrowed, not consumed. Updated in place on success. Pass a
+ * freshly generated file (`rp_pairing_file_generate`) for a first-time pairing.
+ * * `pin_callback` / `pin_context` - invoked to obtain the PIN shown on the
+ * device. May be `NULL`.
+ * * `out_peer_device` - optional. If non-NULL, receives the paired device's
+ * identity, which the caller must free with `rppairing_peer_device_free`. Only
+ * written when a pair-setup actually ran; a successful pair-verify leaves it
+ * NULL.
+ *
+ * # Safety
+ * All pointer arguments must be valid and non-null except `pin_callback`,
+ * `pin_context`, and `out_peer_device`.
+ */
+struct IdeviceFfiError *rppairing_pair_network(const idevice_sockaddr *addr,
+ idevice_socklen_t addr_len,
+ const char *hostname,
+ struct RpPairingFileHandle *pairing_file,
+ const char *(*pin_callback)(void *context),
+ void *pin_context,
+ struct RpPairingPeerDeviceC **out_peer_device);
+
/**
* Connects to a usbmuxd instance over TCP
*
@@ -6120,6 +9244,543 @@ uint32_t idevice_usbmuxd_device_get_device_id(const struct UsbmuxdDeviceHandle *
*/
uint8_t idevice_usbmuxd_device_get_connection_type(const struct UsbmuxdDeviceHandle *device);
+/**
+ * Creates a new WDA client bound to the given provider.
+ *
+ * # Arguments
+ * * [`provider`] - An IdeviceProvider. The provider is consumed and may not
+ * be used again, regardless of whether this call succeeds or fails.
+ * * [`handle`] - On success, set to a newly allocated WdaClientHandle.
+ *
+ * # Returns
+ * An IdeviceFfiError on error, null on success.
+ *
+ * # Safety
+ * `provider` must be a valid pointer to a handle allocated by this library.
+ * The provider is consumed, and may not be used again.
+ * `handle` must be a valid, non-null pointer to a location where the handle will be stored.
+ */
+struct IdeviceFfiError *wda_client_new(struct IdeviceProviderHandle *provider,
+ struct WdaClientHandle **handle);
+
+/**
+ * Frees a WDA client handle.
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library or NULL.
+ */
+void wda_client_free(struct WdaClientHandle *handle);
+
+/**
+ * Sets the device-side WDA HTTP and MJPEG ports.
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library.
+ */
+struct IdeviceFfiError *wda_client_set_ports(struct WdaClientHandle *handle,
+ uint16_t http,
+ uint16_t mjpeg);
+
+/**
+ * Sets the per-request timeout in milliseconds.
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library.
+ */
+struct IdeviceFfiError *wda_client_set_timeout_ms(struct WdaClientHandle *handle, uint64_t ms);
+
+/**
+ * Reads the configured device-side WDA ports.
+ *
+ * # Safety
+ * All pointers must be valid and non-null.
+ */
+struct IdeviceFfiError *wda_client_get_ports(struct WdaClientHandle *handle,
+ uint16_t *out_http,
+ uint16_t *out_mjpeg);
+
+/**
+ * Returns the currently tracked session id, or NULL if none.
+ *
+ * # Arguments
+ * * [`handle`] - The WDA client handle.
+ * * [`out_str`] - On success, set to a heap-allocated UTF-8 string, or NULL
+ * if no session is tracked. Free with `idevice_string_free` if non-null.
+ *
+ * # Safety
+ * All pointers must be valid; `out_str` must be non-null.
+ */
+struct IdeviceFfiError *wda_client_session_id(struct WdaClientHandle *handle, char **out_str);
+
+/**
+ * Fetches `/status` from the WDA HTTP endpoint and returns the JSON response.
+ *
+ * # Arguments
+ * * [`handle`] - The WDA client handle.
+ * * [`out_json`] - On success, set to a heap-allocated JSON string. Free with
+ * `idevice_string_free`.
+ *
+ * # Safety
+ * All pointers must be valid and non-null.
+ */
+struct IdeviceFfiError *wda_client_status(struct WdaClientHandle *handle, char **out_json);
+
+/**
+ * Waits until WDA begins responding on its HTTP endpoint.
+ *
+ * # Safety
+ * All pointers must be valid and non-null.
+ */
+struct IdeviceFfiError *wda_client_wait_until_ready(struct WdaClientHandle *handle,
+ uint64_t timeout_ms,
+ char **out_json);
+
+/**
+ * Starts a WDA session and stores the resulting session id on the handle.
+ *
+ * # Arguments
+ * * [`handle`] - The WDA client handle.
+ * * [`bundle_id`] - Optional bundle identifier; pass NULL for an anonymous session.
+ * * [`out_session_id`] - On success, set to a heap-allocated UTF-8 string.
+ * Free with `idevice_string_free`.
+ *
+ * # Safety
+ * `handle` and `out_session_id` must be valid and non-null. `bundle_id` may be NULL.
+ */
+struct IdeviceFfiError *wda_client_start_session(struct WdaClientHandle *handle,
+ const char *bundle_id,
+ char **out_session_id);
+
+/**
+ * Deletes a WDA session.
+ *
+ * # Safety
+ * `handle` and `session_id` must be valid and non-null.
+ */
+struct IdeviceFfiError *wda_client_delete_session(struct WdaClientHandle *handle,
+ const char *session_id);
+
+/**
+ * Finds a single element and returns its WDA element id.
+ *
+ * # Safety
+ * `handle`, `using`, `value`, and `out_element_id` must be valid and non-null.
+ * `session_id` may be NULL to use the handle's tracked session.
+ */
+struct IdeviceFfiError *wda_client_find_element(struct WdaClientHandle *handle,
+ const char *using_,
+ const char *value,
+ const char *session_id,
+ char **out_element_id);
+
+/**
+ * Finds multiple elements and returns their WDA element ids.
+ *
+ * # Arguments
+ * * [`out_array`] - On success, set to a heap-allocated array of NUL-terminated strings.
+ * * [`out_count`] - On success, set to the number of strings.
+ *
+ * Free the array with `wda_client_string_array_free`.
+ *
+ * # Safety
+ * All non-optional pointers must be valid and non-null.
+ */
+struct IdeviceFfiError *wda_client_find_elements(struct WdaClientHandle *handle,
+ const char *using_,
+ const char *value,
+ const char *session_id,
+ char ***out_array,
+ uintptr_t *out_count);
+
+/**
+ * Frees an array of strings allocated by `wda_client_find_elements`.
+ *
+ * # Safety
+ * `arr` must be a pointer returned by `wda_client_find_elements` with the
+ * matching `count`, or NULL.
+ */
+void wda_client_string_array_free(char **arr, uintptr_t count);
+
+/**
+ * Returns a raw attribute value as a JSON-encoded string.
+ *
+ * # Safety
+ * All non-optional pointers must be valid and non-null.
+ */
+struct IdeviceFfiError *wda_client_element_attribute(struct WdaClientHandle *handle,
+ const char *element_id,
+ const char *name,
+ const char *session_id,
+ char **out_json);
+
+/**
+ * Returns the element text-like value as a string.
+ *
+ * # Safety
+ * All non-optional pointers must be valid and non-null.
+ */
+struct IdeviceFfiError *wda_client_element_text(struct WdaClientHandle *handle,
+ const char *element_id,
+ const char *session_id,
+ char **out_str);
+
+/**
+ * Returns the element bounds rectangle as a JSON-encoded string.
+ *
+ * # Safety
+ * All non-optional pointers must be valid and non-null.
+ */
+struct IdeviceFfiError *wda_client_element_rect(struct WdaClientHandle *handle,
+ const char *element_id,
+ const char *session_id,
+ char **out_json);
+
+/**
+ * Returns whether an element is displayed.
+ *
+ * # Safety
+ * All non-optional pointers must be valid and non-null.
+ */
+struct IdeviceFfiError *wda_client_element_displayed(struct WdaClientHandle *handle,
+ const char *element_id,
+ const char *session_id,
+ bool *out_bool);
+
+/**
+ * Returns whether an element is enabled.
+ *
+ * # Safety
+ * All non-optional pointers must be valid and non-null.
+ */
+struct IdeviceFfiError *wda_client_element_enabled(struct WdaClientHandle *handle,
+ const char *element_id,
+ const char *session_id,
+ bool *out_bool);
+
+/**
+ * Returns whether an element is selected.
+ *
+ * # Safety
+ * All non-optional pointers must be valid and non-null.
+ */
+struct IdeviceFfiError *wda_client_element_selected(struct WdaClientHandle *handle,
+ const char *element_id,
+ const char *session_id,
+ bool *out_bool);
+
+/**
+ * Clicks an element by its WDA element id.
+ *
+ * # Safety
+ * `handle` and `element_id` must be valid and non-null.
+ */
+struct IdeviceFfiError *wda_client_click(struct WdaClientHandle *handle,
+ const char *element_id,
+ const char *session_id);
+
+/**
+ * Sends text input to the currently focused element.
+ *
+ * # Safety
+ * `handle` and `text` must be valid and non-null.
+ */
+struct IdeviceFfiError *wda_client_send_keys(struct WdaClientHandle *handle,
+ const char *text,
+ const char *session_id);
+
+/**
+ * Presses a hardware button through WDA.
+ *
+ * # Safety
+ * `handle` and `name` must be valid and non-null.
+ */
+struct IdeviceFfiError *wda_client_press_button(struct WdaClientHandle *handle,
+ const char *name,
+ const char *session_id);
+
+/**
+ * Unlocks the device via WDA.
+ *
+ * # Safety
+ * `handle` must be valid and non-null.
+ */
+struct IdeviceFfiError *wda_client_unlock(struct WdaClientHandle *handle, const char *session_id);
+
+/**
+ * Swipes from one coordinate to another.
+ *
+ * # Safety
+ * `handle` must be valid and non-null.
+ */
+struct IdeviceFfiError *wda_client_swipe(struct WdaClientHandle *handle,
+ int64_t start_x,
+ int64_t start_y,
+ int64_t end_x,
+ int64_t end_y,
+ double duration,
+ const char *session_id);
+
+/**
+ * Performs a tap gesture.
+ *
+ * `Option` arguments are encoded as `(has, value)` pairs. When `has_*`
+ * is false the underlying value is ignored.
+ *
+ * # Safety
+ * `handle` must be valid and non-null. Optional pointers may be NULL.
+ */
+struct IdeviceFfiError *wda_client_tap(struct WdaClientHandle *handle,
+ bool has_x,
+ double x,
+ bool has_y,
+ double y,
+ const char *element_id,
+ const char *session_id);
+
+/**
+ * Performs a double-tap gesture.
+ *
+ * # Safety
+ * `handle` must be valid and non-null. Optional pointers may be NULL.
+ */
+struct IdeviceFfiError *wda_client_double_tap(struct WdaClientHandle *handle,
+ bool has_x,
+ double x,
+ bool has_y,
+ double y,
+ const char *element_id,
+ const char *session_id);
+
+/**
+ * Performs a long-press gesture.
+ *
+ * # Safety
+ * `handle` must be valid and non-null. Optional pointers may be NULL.
+ */
+struct IdeviceFfiError *wda_client_touch_and_hold(struct WdaClientHandle *handle,
+ double duration,
+ bool has_x,
+ double x,
+ bool has_y,
+ double y,
+ const char *element_id,
+ const char *session_id);
+
+/**
+ * Scrolls the current view or an element using a WDA mobile command.
+ *
+ * `Option` arguments are encoded as `(has, value)`.
+ *
+ * # Safety
+ * `handle` must be valid and non-null. Optional string arguments may be NULL.
+ */
+struct IdeviceFfiError *wda_client_scroll(struct WdaClientHandle *handle,
+ const char *direction,
+ const char *name,
+ const char *predicate_string,
+ bool has_to_visible,
+ bool to_visible,
+ const char *element_id,
+ const char *session_id);
+
+/**
+ * Returns the current UI source tree as XML.
+ *
+ * # Safety
+ * `handle` and `out_str` must be valid and non-null.
+ */
+struct IdeviceFfiError *wda_client_source(struct WdaClientHandle *handle,
+ const char *session_id,
+ char **out_str);
+
+/**
+ * Returns a PNG screenshot as raw bytes.
+ *
+ * # Arguments
+ * * [`out_bytes`] - On success, set to a heap-allocated PNG buffer.
+ * * [`out_len`] - On success, set to the buffer length in bytes.
+ *
+ * Free the buffer with `idevice_data_free`.
+ *
+ * # Safety
+ * All non-optional pointers must be valid and non-null.
+ */
+struct IdeviceFfiError *wda_client_screenshot(struct WdaClientHandle *handle,
+ const char *session_id,
+ uint8_t **out_bytes,
+ uintptr_t *out_len);
+
+/**
+ * Returns the current window size payload from WDA.
+ *
+ * # Safety
+ * All non-optional pointers must be valid and non-null.
+ */
+struct IdeviceFfiError *wda_client_window_size(struct WdaClientHandle *handle,
+ const char *session_id,
+ char **out_json);
+
+/**
+ * Returns the current viewport rectangle.
+ *
+ * # Safety
+ * All non-optional pointers must be valid and non-null.
+ */
+struct IdeviceFfiError *wda_client_viewport_rect(struct WdaClientHandle *handle,
+ const char *session_id,
+ char **out_json);
+
+/**
+ * Returns the current orientation as a string.
+ *
+ * # Safety
+ * All non-optional pointers must be valid and non-null.
+ */
+struct IdeviceFfiError *wda_client_orientation(struct WdaClientHandle *handle,
+ const char *session_id,
+ char **out_str);
+
+/**
+ * Launches or activates an application via WDA.
+ *
+ * # Arguments
+ * * [`bundle_id`] - The bundle identifier of the app to launch.
+ * * [`arguments`] - Optional array of argument strings; pass NULL for none.
+ * * [`arguments_count`] - Number of strings in `arguments`; ignored if NULL.
+ * * [`environment_json`] - Optional JSON object string of environment variables; pass NULL for none.
+ *
+ * # Safety
+ * `handle`, `bundle_id`, and `out_json` must be valid and non-null. Optional
+ * pointers may be NULL.
+ */
+struct IdeviceFfiError *wda_client_launch_app(struct WdaClientHandle *handle,
+ const char *bundle_id,
+ const char *const *arguments,
+ uintptr_t arguments_count,
+ const char *environment_json,
+ const char *session_id,
+ char **out_json);
+
+/**
+ * Activates an already running application.
+ *
+ * # Safety
+ * `handle`, `bundle_id`, and `out_json` must be valid and non-null.
+ */
+struct IdeviceFfiError *wda_client_activate_app(struct WdaClientHandle *handle,
+ const char *bundle_id,
+ const char *session_id,
+ char **out_json);
+
+/**
+ * Terminates an application and returns whether termination succeeded.
+ *
+ * # Safety
+ * `handle`, `bundle_id`, and `out_bool` must be valid and non-null.
+ */
+struct IdeviceFfiError *wda_client_terminate_app(struct WdaClientHandle *handle,
+ const char *bundle_id,
+ const char *session_id,
+ bool *out_bool);
+
+/**
+ * Queries the XCTest application state for the given bundle id.
+ *
+ * # Safety
+ * `handle`, `bundle_id`, and `out_state` must be valid and non-null.
+ */
+struct IdeviceFfiError *wda_client_query_app_state(struct WdaClientHandle *handle,
+ const char *bundle_id,
+ const char *session_id,
+ int64_t *out_state);
+
+/**
+ * Backgrounds the current app for the given number of seconds.
+ *
+ * `Option` is encoded as `(has_seconds, seconds)`.
+ *
+ * # Safety
+ * `handle` and `out_json` must be valid and non-null.
+ */
+struct IdeviceFfiError *wda_client_background_app(struct WdaClientHandle *handle,
+ bool has_seconds,
+ double seconds,
+ const char *session_id,
+ char **out_json);
+
+/**
+ * Returns whether the device is currently locked.
+ *
+ * # Safety
+ * `handle` and `out_bool` must be valid and non-null.
+ */
+struct IdeviceFfiError *wda_client_is_locked(struct WdaClientHandle *handle,
+ const char *session_id,
+ bool *out_bool);
+
+/**
+ * Starts a localhost bridge to the device's default WDA ports.
+ *
+ * # Arguments
+ * * [`provider`] - An IdeviceProvider. Provider ownership is transferred —
+ * the caller must not free or reuse the IdeviceProviderHandle on success or failure.
+ * * [`handle`] - On success, set to a newly allocated WdaBridgeHandle.
+ *
+ * # Returns
+ * An IdeviceFfiError on error, null on success.
+ *
+ * # Safety
+ * `provider` must be a valid pointer to a handle allocated by this library.
+ * The provider is consumed, and may not be used again.
+ * `handle` must be a valid, non-null pointer to a location where the handle will be stored.
+ */
+struct IdeviceFfiError *wda_bridge_start(struct IdeviceProviderHandle *provider,
+ struct WdaBridgeHandle **handle);
+
+/**
+ * Starts a localhost bridge to custom device-side WDA ports.
+ *
+ * # Safety
+ * Same requirements as [`wda_bridge_start`].
+ */
+struct IdeviceFfiError *wda_bridge_start_with_ports(struct IdeviceProviderHandle *provider,
+ uint16_t device_http,
+ uint16_t device_mjpeg,
+ struct WdaBridgeHandle **handle);
+
+/**
+ * Reads the endpoints assigned to the running bridge.
+ *
+ * # Arguments
+ * * [`handle`] - The bridge handle.
+ * * [`out_endpoints`] - On success, set to a heap-allocated WdaBridgeEndpointsC.
+ * Free with `wda_bridge_endpoints_free`.
+ *
+ * # Returns
+ * An IdeviceFfiError on error, null on success.
+ *
+ * # Safety
+ * All pointers must be valid and non-null.
+ */
+struct IdeviceFfiError *wda_bridge_endpoints(struct WdaBridgeHandle *handle,
+ struct WdaBridgeEndpointsC **out_endpoints);
+
+/**
+ * Frees a WdaBridgeEndpointsC struct and its heap-allocated string fields.
+ *
+ * # Safety
+ * `endpoints` must be a pointer returned by `wda_bridge_endpoints` or NULL.
+ */
+void wda_bridge_endpoints_free(struct WdaBridgeEndpointsC *endpoints);
+
+/**
+ * Frees a WDA bridge handle. Dropping aborts the underlying forwarder tasks.
+ *
+ * # Safety
+ * `handle` must be a pointer returned by this library or NULL.
+ */
+void wda_bridge_free(struct WdaBridgeHandle *handle);
+
#endif /* IDEVICE_H */
diff --git a/StikDebug/idevice/libidevice_ffi.a b/StikDebug/idevice/libidevice_ffi.a
index 58f0b92..3fb7e2d 100644
Binary files a/StikDebug/idevice/libidevice_ffi.a and b/StikDebug/idevice/libidevice_ffi.a differ