mirror of
https://github.com/StikDebug/StikJIT.git
synced 2026-10-15 20:10:31 +00:00
Add DDI mount support and document usage requirements (#2)
This commit is contained in:
parent
ee3d5836aa
commit
95b8e4d3f7
5 changed files with 313 additions and 0 deletions
173
Sources/DDISession.swift
Normal file
173
Sources/DDISession.swift
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
import Foundation
|
||||
@_implementationOnly import idevice
|
||||
|
||||
private func ddiMountProgressCallback(progress: size_t, total: size_t, context: UnsafeMutableRawPointer?) {
|
||||
guard let context else { return }
|
||||
let progressHandler = Unmanaged<DDIProgressBox>.fromOpaque(context).takeUnretainedValue()
|
||||
let fraction = total > 0 ? Double(progress) / Double(total) : 0
|
||||
progressHandler.handler(fraction)
|
||||
}
|
||||
|
||||
private final class DDIProgressBox {
|
||||
let handler: (Double) -> Void
|
||||
init(_ handler: @escaping (Double) -> Void) { self.handler = handler }
|
||||
}
|
||||
|
||||
final class DDISession {
|
||||
|
||||
private struct Tunnel {
|
||||
var adapter: OpaquePointer?
|
||||
var handshake: OpaquePointer?
|
||||
func free() {
|
||||
if let handshake { rsd_handshake_free(handshake) }
|
||||
if let adapter { adapter_free(adapter) }
|
||||
}
|
||||
}
|
||||
|
||||
private let pairingFilePath: String
|
||||
private let configuration: StikJIT.Configuration
|
||||
|
||||
init(pairingFilePath: String, configuration: StikJIT.Configuration) {
|
||||
self.pairingFilePath = pairingFilePath
|
||||
self.configuration = configuration
|
||||
}
|
||||
|
||||
func isMounted() throws -> Bool {
|
||||
let tunnel = try makeTunnel()
|
||||
defer { tunnel.free() }
|
||||
|
||||
var client: OpaquePointer?
|
||||
try IdeviceFFI.check("failed to connect to image mounter") {
|
||||
image_mounter_connect_rsd(tunnel.adapter, tunnel.handshake, &client)
|
||||
}
|
||||
guard let client else { throw StikJITError.debugProxyUnavailable }
|
||||
defer { image_mounter_free(client) }
|
||||
|
||||
var devices: UnsafeMutablePointer<plist_t?>?
|
||||
var deviceCount = 0
|
||||
try IdeviceFFI.check("failed to fetch mounted devices") {
|
||||
image_mounter_copy_devices(client, &devices, &deviceCount)
|
||||
}
|
||||
if let devices {
|
||||
for index in 0..<deviceCount { plist_free(devices[index]) }
|
||||
idevice_data_free(
|
||||
UnsafeMutableRawPointer(devices).assumingMemoryBound(to: UInt8.self),
|
||||
UInt(deviceCount * MemoryLayout<plist_t?>.stride))
|
||||
}
|
||||
return deviceCount > 0
|
||||
}
|
||||
|
||||
func mountDDI(imagePath: String, trustcachePath: String, manifestPath: String, progress: @escaping (Double) -> Void) throws {
|
||||
let imageData = try mappedFileData(atPath: imagePath, description: "developer disk image")
|
||||
let trustcacheData = try mappedFileData(atPath: trustcachePath, description: "developer disk image trust cache")
|
||||
let manifestData = try mappedFileData(atPath: manifestPath, description: "developer disk image manifest")
|
||||
|
||||
let tunnel = try makeTunnel()
|
||||
defer { tunnel.free() }
|
||||
|
||||
let uniqueChipID = try fetchUniqueChipID(over: tunnel)
|
||||
|
||||
var client: OpaquePointer?
|
||||
try IdeviceFFI.check("failed to connect to image mounter") {
|
||||
image_mounter_connect_rsd(tunnel.adapter, tunnel.handshake, &client)
|
||||
}
|
||||
guard let client else { throw StikJITError.debugProxyUnavailable }
|
||||
defer { image_mounter_free(client) }
|
||||
|
||||
let progressBox = DDIProgressBox(progress)
|
||||
let context = Unmanaged.passUnretained(progressBox).toOpaque()
|
||||
|
||||
try IdeviceFFI.check("failed to mount personalized developer disk image") {
|
||||
imageData.withUnsafeBytes { imageBuffer -> UnsafeMutablePointer<IdeviceFfiError>? in
|
||||
trustcacheData.withUnsafeBytes { trustcacheBuffer -> UnsafeMutablePointer<IdeviceFfiError>? in
|
||||
manifestData.withUnsafeBytes { manifestBuffer -> UnsafeMutablePointer<IdeviceFfiError>? in
|
||||
image_mounter_mount_personalized_with_callback_rsd(
|
||||
client,
|
||||
tunnel.adapter,
|
||||
tunnel.handshake,
|
||||
imageBuffer.bindMemory(to: UInt8.self).baseAddress,
|
||||
imageData.count,
|
||||
trustcacheBuffer.bindMemory(to: UInt8.self).baseAddress,
|
||||
trustcacheData.count,
|
||||
manifestBuffer.bindMemory(to: UInt8.self).baseAddress,
|
||||
manifestData.count,
|
||||
nil,
|
||||
uniqueChipID,
|
||||
ddiMountProgressCallback,
|
||||
context)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func mappedFileData(atPath path: String, description: String) throws -> Data {
|
||||
guard FileManager.default.fileExists(atPath: path) else {
|
||||
throw StikJITError.ddiFilesMissing("\(description) not found at \(path)")
|
||||
}
|
||||
let data = try Data(contentsOf: URL(fileURLWithPath: path), options: .mappedIfSafe)
|
||||
guard !data.isEmpty else {
|
||||
throw StikJITError.ddiFilesMissing("\(description) is empty at \(path)")
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
private func fetchUniqueChipID(over tunnel: Tunnel) throws -> UInt64 {
|
||||
var client: OpaquePointer?
|
||||
try IdeviceFFI.check("failed to connect to lockdownd") {
|
||||
lockdownd_connect_rsd(tunnel.adapter, tunnel.handshake, &client)
|
||||
}
|
||||
guard let client else { throw StikJITError.debugProxyUnavailable }
|
||||
defer { lockdownd_client_free(client) }
|
||||
|
||||
var uniqueChipIDPlist: plist_t?
|
||||
try IdeviceFFI.check("failed to query UniqueChipID") {
|
||||
"UniqueChipID".withCString { lockdownd_get_value(client, $0, nil, &uniqueChipIDPlist) }
|
||||
}
|
||||
guard let uniqueChipIDPlist else {
|
||||
throw StikJITError.device(code: -1, subCode: 0, message: "UniqueChipID was not returned by lockdownd")
|
||||
}
|
||||
defer { plist_free(uniqueChipIDPlist) }
|
||||
|
||||
var value: UInt64 = 0
|
||||
plist_get_uint_val(uniqueChipIDPlist, &value)
|
||||
return value
|
||||
}
|
||||
|
||||
private func openPairingFile() throws -> OpaquePointer {
|
||||
guard !pairingFilePath.isEmpty, FileManager.default.fileExists(atPath: pairingFilePath) else {
|
||||
throw StikJITError.pairingFile("not found at \(pairingFilePath)")
|
||||
}
|
||||
var handle: OpaquePointer?
|
||||
try IdeviceFFI.check("failed to read pairing file") {
|
||||
pairingFilePath.withCString { rp_pairing_file_read($0, &handle) }
|
||||
}
|
||||
guard let handle else { throw StikJITError.pairingFile("unreadable at \(pairingFilePath)") }
|
||||
return handle
|
||||
}
|
||||
|
||||
private func makeTunnel() throws -> Tunnel {
|
||||
let pairing = try openPairingFile()
|
||||
defer { rp_pairing_file_free(pairing) }
|
||||
|
||||
var address = sockaddr_in()
|
||||
address.sin_family = sa_family_t(AF_INET)
|
||||
address.sin_port = configuration.rsdPort.bigEndian
|
||||
_ = configuration.deviceAddress.withCString { inet_pton(AF_INET, $0, &address.sin_addr) }
|
||||
|
||||
var tunnel = Tunnel()
|
||||
try IdeviceFFI.check("failed to create RSD tunnel") {
|
||||
"StikJIT".withCString { hostname in
|
||||
withUnsafePointer(to: &address) { pointer in
|
||||
pointer.withMemoryRebound(to: sockaddr.self, capacity: 1) { sa in
|
||||
tunnel_create_rppairing(
|
||||
sa, socklen_t(MemoryLayout<sockaddr_in>.stride),
|
||||
hostname, pairing, nil, nil,
|
||||
&tunnel.adapter, &tunnel.handshake)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return tunnel
|
||||
}
|
||||
}
|
||||
90
Sources/DeveloperDiskImageService.swift
Normal file
90
Sources/DeveloperDiskImageService.swift
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
import Foundation
|
||||
|
||||
public struct DDIPaths: Sendable {
|
||||
|
||||
public var imagePath: String
|
||||
|
||||
public var trustcachePath: String
|
||||
|
||||
public var manifestPath: String
|
||||
|
||||
public init(imagePath: String, trustcachePath: String, manifestPath: String) {
|
||||
self.imagePath = imagePath
|
||||
self.trustcachePath = trustcachePath
|
||||
self.manifestPath = manifestPath
|
||||
}
|
||||
|
||||
public static func `default`(in directory: URL) -> DDIPaths {
|
||||
DDIPaths(
|
||||
imagePath: directory.appendingPathComponent("DDI/Image.dmg").path,
|
||||
trustcachePath: directory.appendingPathComponent("DDI/Image.dmg.trustcache").path,
|
||||
manifestPath: directory.appendingPathComponent("DDI/BuildManifest.plist").path)
|
||||
}
|
||||
|
||||
var allFilesExist: Bool {
|
||||
let fileManager = FileManager.default
|
||||
return fileManager.fileExists(atPath: imagePath)
|
||||
&& fileManager.fileExists(atPath: trustcachePath)
|
||||
&& fileManager.fileExists(atPath: manifestPath)
|
||||
}
|
||||
}
|
||||
|
||||
public actor DeveloperDiskImageService {
|
||||
|
||||
public static let shared = DeveloperDiskImageService()
|
||||
|
||||
private let session: URLSession
|
||||
|
||||
public init(session: URLSession = .shared) {
|
||||
self.session = session
|
||||
}
|
||||
|
||||
private struct DownloadItem {
|
||||
let name: String
|
||||
let destinationPath: String
|
||||
let urlString: String
|
||||
}
|
||||
|
||||
private static let baseURL = "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Personalized"
|
||||
|
||||
public func downloadIfNeeded(to paths: DDIPaths, progress: @escaping (Double, String) -> Void = { _, _ in }) async throws {
|
||||
guard !paths.allFilesExist else { return }
|
||||
try await download(to: paths, progress: progress)
|
||||
}
|
||||
|
||||
public func download(to paths: DDIPaths, progress: @escaping (Double, String) -> Void = { _, _ in }) async throws {
|
||||
let items = [
|
||||
DownloadItem(name: "BuildManifest.plist", destinationPath: paths.manifestPath, urlString: "\(Self.baseURL)/BuildManifest.plist"),
|
||||
DownloadItem(name: "Image.dmg", destinationPath: paths.imagePath, urlString: "\(Self.baseURL)/Image.dmg"),
|
||||
DownloadItem(name: "Image.dmg.trustcache", destinationPath: paths.trustcachePath, urlString: "\(Self.baseURL)/Image.dmg.trustcache"),
|
||||
]
|
||||
|
||||
let total = Double(items.count)
|
||||
for (index, item) in items.enumerated() {
|
||||
progress(Double(index) / total, "Downloading \(item.name)...")
|
||||
try await downloadFile(from: item.urlString, to: URL(fileURLWithPath: item.destinationPath))
|
||||
progress(Double(index + 1) / total, "\(item.name) ready")
|
||||
}
|
||||
}
|
||||
|
||||
private func downloadFile(from urlString: String, to destinationURL: URL) async throws {
|
||||
guard let url = URL(string: urlString), url.scheme?.lowercased() == "https" else {
|
||||
throw StikJITError.ddiDownload("invalid URL \(urlString)")
|
||||
}
|
||||
|
||||
let (temporaryURL, response) = try await session.download(from: url)
|
||||
guard let httpResponse = response as? HTTPURLResponse else {
|
||||
throw StikJITError.ddiDownload("invalid response for \(urlString)")
|
||||
}
|
||||
guard (200..<300).contains(httpResponse.statusCode) else {
|
||||
throw StikJITError.ddiDownload("HTTP \(httpResponse.statusCode) for \(urlString)")
|
||||
}
|
||||
|
||||
let fileManager = FileManager.default
|
||||
try fileManager.createDirectory(at: destinationURL.deletingLastPathComponent(), withIntermediateDirectories: true)
|
||||
if fileManager.fileExists(atPath: destinationURL.path) {
|
||||
try fileManager.removeItem(at: destinationURL)
|
||||
}
|
||||
try fileManager.moveItem(at: temporaryURL, to: destinationURL)
|
||||
}
|
||||
}
|
||||
|
|
@ -38,4 +38,25 @@ public enum StikJIT {
|
|||
let session = JITSession(pairingFilePath: pairingFile.path, configuration: configuration)
|
||||
try session.enableJIT(targetPID: targetPID, script: script, progress: progress)
|
||||
}
|
||||
|
||||
public static func isDDIMounted(pairingFile: URL,
|
||||
configuration: Configuration = .default) throws -> Bool {
|
||||
try DDISession(pairingFilePath: pairingFile.path, configuration: configuration).isMounted()
|
||||
}
|
||||
|
||||
public static func downloadDDIIfNeeded(to paths: DDIPaths,
|
||||
progress: @escaping (Double, String) -> Void = { _, _ in }) async throws {
|
||||
try await DeveloperDiskImageService.shared.downloadIfNeeded(to: paths, progress: progress)
|
||||
}
|
||||
|
||||
public static func mountDDI(pairingFile: URL,
|
||||
configuration: Configuration = .default,
|
||||
paths: DDIPaths,
|
||||
progress: @escaping (Double) -> Void = { _ in }) throws {
|
||||
let session = DDISession(pairingFilePath: pairingFile.path, configuration: configuration)
|
||||
try session.mountDDI(imagePath: paths.imagePath,
|
||||
trustcachePath: paths.trustcachePath,
|
||||
manifestPath: paths.manifestPath,
|
||||
progress: progress)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,10 @@ public enum StikJITError: Error, LocalizedError {
|
|||
|
||||
case device(code: Int32, subCode: Int32, message: String)
|
||||
|
||||
case ddiDownload(String)
|
||||
|
||||
case ddiFilesMissing(String)
|
||||
|
||||
public var errorDescription: String? {
|
||||
switch self {
|
||||
case .pairingFile(let detail):
|
||||
|
|
@ -20,6 +24,10 @@ public enum StikJITError: Error, LocalizedError {
|
|||
return "Failed to establish a debugserver connection to the target process."
|
||||
case .device(let code, let subCode, let message):
|
||||
return "\(message) (idevice code \(code)/\(subCode))."
|
||||
case .ddiDownload(let detail):
|
||||
return "Failed to download Developer Disk Image: \(detail)."
|
||||
case .ddiFilesMissing(let detail):
|
||||
return "Developer Disk Image files missing: \(detail)."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue