├── .gitignore ├── LICENSE.md ├── Package.resolved ├── Package.swift ├── README.md └── Sources └── SwiftyMobileDevice ├── Common └── ConnectionType.swift ├── Device ├── Device+Connection.swift ├── Device+Lookup.swift ├── Device.swift └── Lockdown │ ├── Clients │ ├── AFCClient.swift │ ├── DebugserverClient.swift │ ├── HeartbeatClient.swift │ ├── HouseArrestClient.swift │ ├── InstallationProxyClient.swift │ ├── MISAgentClient.swift │ ├── MobileImageMounterClient.swift │ └── SBServicesClient.swift │ ├── LockdownClient.swift │ └── LockdownService.swift ├── USBMux ├── USBMux+Connection.swift ├── USBMux+Lookup.swift └── USBMux.swift └── Utilities ├── CAPI.swift ├── PlistNodeCoders.swift └── StreamingConnection.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Kabir Oberai 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "d8b9604489beb120e6346a74b1471cc43bf0ac8eb95b9888ab249ffb2b54674f", 3 | "pins" : [ 4 | { 5 | "identity" : "swift-concurrency-extras", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/pointfreeco/swift-concurrency-extras.git", 8 | "state" : { 9 | "revision" : "82a4ae7170d98d8538ec77238b7eb8e7199ef2e8", 10 | "version" : "1.3.1" 11 | } 12 | }, 13 | { 14 | "identity" : "xtool-core", 15 | "kind" : "remoteSourceControl", 16 | "location" : "https://github.com/xtool-org/xtool-core", 17 | "state" : { 18 | "revision" : "63c644eef014f7faff53daaebe8bad37c31fbbe5", 19 | "version" : "1.3.0" 20 | } 21 | } 22 | ], 23 | "version" : 3 24 | } 25 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:6.0 2 | 3 | import PackageDescription 4 | 5 | extension Product.Library.LibraryType { 6 | static var smart: Self { 7 | #if os(Linux) 8 | return .static 9 | #else 10 | return .dynamic 11 | #endif 12 | } 13 | } 14 | 15 | let package = Package( 16 | name: "SwiftyMobileDevice", 17 | platforms: [ 18 | .iOS("14.0"), 19 | .macOS("11.0"), 20 | ], 21 | products: [ 22 | .library( 23 | name: "SwiftyMobileDevice", 24 | type: .smart, 25 | targets: ["SwiftyMobileDevice"] 26 | ), 27 | ], 28 | dependencies: [ 29 | .package(url: "https://github.com/xtool-org/xtool-core", .upToNextMinor(from: "1.3.0")), 30 | ], 31 | targets: [ 32 | .target( 33 | name: "SwiftyMobileDevice", 34 | dependencies: [ 35 | .product(name: "plist", package: "xtool-core"), 36 | .product(name: "libimobiledeviceGlue", package: "xtool-core"), 37 | .product(name: "usbmuxd", package: "xtool-core"), 38 | .product(name: "libimobiledevice", package: "xtool-core"), 39 | .product(name: "Superutils", package: "xtool-core") 40 | ] 41 | ), 42 | ] 43 | ) 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SwiftyMobileDevice 2 | 3 | Swifty wrapper for libimobiledevice. 4 | -------------------------------------------------------------------------------- /Sources/SwiftyMobileDevice/Common/ConnectionType.swift: -------------------------------------------------------------------------------- 1 | public enum ConnectionType: Sendable { 2 | case usb 3 | case network 4 | } 5 | 6 | public enum LookupMode: Hashable, Sendable { 7 | case only(ConnectionType) 8 | case both(preferring: ConnectionType) 9 | } 10 | -------------------------------------------------------------------------------- /Sources/SwiftyMobileDevice/Device/Device+Connection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Device+Connection.swift 3 | // SwiftyMobileDevice 4 | // 5 | // Created by Kabir Oberai on 28/04/20. 6 | // Copyright © 2020 Kabir Oberai. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import libimobiledevice 11 | 12 | extension Device { 13 | 14 | public final class Connection: StreamingConnection { 15 | 16 | public typealias Error = Device.Error 17 | public typealias Raw = idevice_connection_t 18 | 19 | public nonisolated(unsafe) let raw: idevice_connection_t 20 | public init(raw: idevice_connection_t) { self.raw = raw } 21 | public init(device: Device, port: UInt16) throws { 22 | var connection: idevice_connection_t? 23 | try CAPI.check(idevice_connect(device.raw, port, &connection)) 24 | guard let raw = connection else { throw Error.internal } 25 | self.raw = raw 26 | } 27 | deinit { idevice_disconnect(raw) } 28 | 29 | /// - Warning: The file handle returned is only valid while the `Connection` instance 30 | /// exists 31 | public func fileHandle() throws -> FileHandle { 32 | var handle: Int32 = 0 33 | try CAPI.check(idevice_connection_get_fd(raw, &handle)) 34 | return FileHandle(fileDescriptor: handle) 35 | } 36 | 37 | public nonisolated(unsafe) let sendFunc: SendFunc = idevice_connection_send 38 | public nonisolated(unsafe) let receiveFunc: ReceiveFunc = idevice_connection_receive 39 | public nonisolated(unsafe) let receiveTimeoutFunc: ReceiveTimeoutFunc = idevice_connection_receive_timeout 40 | 41 | public func setSSLEnabled(_ enabled: Bool) throws { 42 | try CAPI.check( 43 | (enabled ? idevice_connection_enable_ssl: idevice_connection_disable_ssl)(raw) 44 | ) 45 | } 46 | 47 | } 48 | 49 | public func connect(to port: UInt16) throws -> Connection { 50 | try Connection(device: self, port: port) 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /Sources/SwiftyMobileDevice/Device/Device+Lookup.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Device+Lookup.swift 3 | // SwiftyMobileDevice 4 | // 5 | // Created by Kabir Oberai on 28/04/20. 6 | // Copyright © 2020 Kabir Oberai. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import libimobiledevice 11 | 12 | extension Device { 13 | 14 | public struct Info { 15 | public let udid: String 16 | public let connectionType: ConnectionType 17 | 18 | // copies from raw 19 | init?(raw: idevice_info) { 20 | guard let connectionType = ConnectionType(ideviceRaw: raw.conn_type) 21 | else { return nil } 22 | self.udid = String(cString: raw.udid) 23 | self.connectionType = connectionType 24 | } 25 | } 26 | 27 | public struct Event: Sendable { 28 | public enum EventType: Sendable { 29 | case add 30 | case remove 31 | case paired 32 | 33 | public var raw: idevice_event_type { 34 | switch self { 35 | case .add: return IDEVICE_DEVICE_ADD 36 | case .remove: return IDEVICE_DEVICE_REMOVE 37 | case .paired: return IDEVICE_DEVICE_PAIRED 38 | } 39 | } 40 | 41 | public init?(_ raw: idevice_event_type) { 42 | switch raw { 43 | case IDEVICE_DEVICE_ADD: self = .add 44 | case IDEVICE_DEVICE_REMOVE: self = .remove 45 | case IDEVICE_DEVICE_PAIRED: self = .paired 46 | default: return nil 47 | } 48 | } 49 | } 50 | 51 | public let eventType: EventType 52 | public let udid: String 53 | public let connectionType: ConnectionType 54 | 55 | public init?(raw: idevice_event_t) { 56 | guard let eventType = EventType(raw.event), 57 | let connectionType = ConnectionType(ideviceRaw: raw.conn_type) 58 | else { return nil } 59 | self.eventType = eventType 60 | // we don't own `raw` so we need to copy the udid string 61 | self.udid = String(cString: raw.udid) 62 | self.connectionType = connectionType 63 | } 64 | } 65 | 66 | @available(*, deprecated, renamed: "devices") 67 | public static func udids() throws -> [String] { 68 | try CAPI.getArrayWithCount( 69 | parseFn: { idevice_get_device_list(&$0, &$1) }, 70 | freeFn: { idevice_device_list_free($0) } 71 | ) ?? [] 72 | } 73 | 74 | public static func devices() throws -> [Info] { 75 | var deviceList: UnsafeMutablePointer? 76 | var count: Int32 = 0 77 | try CAPI.check(idevice_get_device_list_extended(&deviceList, &count)) 78 | 79 | guard let devices = deviceList else { throw CAPIGenericError.unexpectedNil } 80 | defer { idevice_device_list_extended_free(deviceList) } 81 | 82 | return UnsafeBufferPointer(start: devices, count: Int(count)) 83 | .compactMap { $0?.pointee } 84 | .compactMap(Info.init) 85 | } 86 | 87 | public static func subscribe() async -> AsyncStream { 88 | await SubscriptionManager.shared.subscribe() 89 | } 90 | 91 | } 92 | 93 | private actor SubscriptionManager { 94 | static let shared = SubscriptionManager() 95 | 96 | private var subscriptionContext: idevice_subscription_context_t? 97 | private var subscribers: [ObjectIdentifier: @Sendable (Device.Event) -> Void] = [:] 98 | 99 | private func yield(event: Device.Event) { 100 | for (_, subscriber) in subscribers { 101 | subscriber(event) 102 | } 103 | } 104 | 105 | private final class SubscriptionToken: Sendable { 106 | fileprivate init() {} 107 | } 108 | 109 | private func actuallySubscribe() { 110 | var context: idevice_subscription_context_t? 111 | idevice_events_subscribe(&context, { @Sendable eventPointer, _ in 112 | guard let rawEvent = eventPointer?.pointee, 113 | let event = Device.Event(raw: rawEvent) 114 | else { return } 115 | Task { 116 | await SubscriptionManager.shared.yield(event: event) 117 | } 118 | }, nil) 119 | self.subscriptionContext = context 120 | } 121 | 122 | public func subscribe() -> AsyncStream { 123 | if subscriptionContext == nil { 124 | actuallySubscribe() 125 | } 126 | let token = SubscriptionToken() 127 | let (stream, continuation) = AsyncStream.makeStream() 128 | subscribers[ObjectIdentifier(token)] = { continuation.yield($0) } 129 | continuation.onTermination = { _ in 130 | Task { await self.unsubscribe(token: token) } 131 | } 132 | return stream 133 | } 134 | 135 | private func unsubscribe(token: SubscriptionToken) { 136 | subscribers.removeValue(forKey: ObjectIdentifier(token)) 137 | if subscribers.isEmpty, let subscriptionContext { 138 | idevice_events_unsubscribe(subscriptionContext) 139 | self.subscriptionContext = nil 140 | } 141 | } 142 | 143 | } 144 | -------------------------------------------------------------------------------- /Sources/SwiftyMobileDevice/Device/Device.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Device.swift 3 | // SwiftyMobileDevice 4 | // 5 | // Created by Kabir Oberai on 13/11/19. 6 | // Copyright © 2019 Kabir Oberai. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import libimobiledevice 11 | 12 | extension ConnectionType { 13 | public var ideviceRaw: idevice_connection_type { 14 | switch self { 15 | case .usb: return CONNECTION_USBMUXD 16 | case .network: return CONNECTION_NETWORK 17 | } 18 | } 19 | 20 | public init?(ideviceRaw: idevice_connection_type) { 21 | switch ideviceRaw { 22 | case CONNECTION_USBMUXD: self = .usb 23 | case CONNECTION_NETWORK: self = .network 24 | default: return nil 25 | } 26 | } 27 | } 28 | 29 | extension LookupMode { 30 | public var ideviceRaw: idevice_options { 31 | switch self { 32 | case .only(.usb): return IDEVICE_LOOKUP_USBMUX 33 | case .only(.network): return IDEVICE_LOOKUP_NETWORK 34 | case .both(preferring: .usb): 35 | return .init( 36 | IDEVICE_LOOKUP_USBMUX.rawValue | 37 | IDEVICE_LOOKUP_NETWORK.rawValue 38 | ) 39 | case .both(preferring: .network): 40 | return .init( 41 | IDEVICE_LOOKUP_USBMUX.rawValue | 42 | IDEVICE_LOOKUP_NETWORK.rawValue | 43 | IDEVICE_LOOKUP_PREFER_NETWORK.rawValue 44 | ) 45 | } 46 | } 47 | } 48 | 49 | public final class Device: Sendable { 50 | 51 | public enum Error: CAPIError { 52 | case unknown 53 | case `internal` 54 | case invalidArg 55 | case noDevice 56 | case notEnoughData 57 | case sslError 58 | case timeout 59 | 60 | public init?(_ raw: idevice_error_t) { 61 | switch raw { 62 | case IDEVICE_E_SUCCESS: return nil 63 | case IDEVICE_E_INVALID_ARG: self = .invalidArg 64 | case IDEVICE_E_NO_DEVICE: self = .noDevice 65 | case IDEVICE_E_NOT_ENOUGH_DATA: self = .notEnoughData 66 | case IDEVICE_E_SSL_ERROR: self = .sslError 67 | case IDEVICE_E_TIMEOUT: self = .timeout 68 | case IDEVICE_E_UNKNOWN_ERROR: self = .unknown 69 | default: self = .unknown 70 | } 71 | } 72 | } 73 | 74 | public enum DebugLevel: Int { 75 | case off = 0 76 | case on = 1 77 | } 78 | 79 | public static func setDebugLevel(_ level: DebugLevel) { 80 | idevice_set_debug_level(.init(level.rawValue)) 81 | } 82 | 83 | public nonisolated(unsafe) let raw: idevice_t 84 | public init(udid: String, lookupMode: LookupMode = .only(.usb)) throws { 85 | var device: idevice_t? 86 | try CAPI.check(idevice_new_with_options(&device, udid, lookupMode.ideviceRaw)) 87 | guard let raw = device else { throw Error.internal } 88 | self.raw = raw 89 | } 90 | deinit { idevice_free(raw) } 91 | 92 | public func udid() throws -> String { 93 | try CAPI.getString { idevice_get_udid(raw, &$0) } 94 | } 95 | 96 | public func handle() throws -> UInt32 { 97 | var handle: UInt32 = 0 98 | try CAPI.check(idevice_get_handle(raw, &handle)) 99 | return handle 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /Sources/SwiftyMobileDevice/Device/Lockdown/Clients/AFCClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AFCClient.swift 3 | // SwiftyMobileDevice 4 | // 5 | // Created by Kabir Oberai on 13/11/19. 6 | // Copyright © 2019 Kabir Oberai. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import libimobiledevice 11 | 12 | public final class AFCClient: LockdownService { 13 | 14 | public enum Error: CAPIError, LocalizedError { 15 | case unknown 16 | case `internal` 17 | case opHeaderInvalid 18 | case noResources 19 | case readError 20 | case writeError 21 | case unknownPacketType 22 | case invalidArg 23 | case objectNotFound 24 | case objectIsDir 25 | case permDenied 26 | case serviceNotConnected 27 | case opTimeout 28 | case tooMuchData 29 | case endOfData 30 | case opNotSupported 31 | case objectExists 32 | case objectBusy 33 | case noSpaceLeft 34 | case opWouldBlock 35 | case ioError 36 | case opInterrupted 37 | case opInProgress 38 | case internalError 39 | case muxError 40 | case noMem 41 | case notEnoughData 42 | case dirNotEmpty 43 | case forceSignedType 44 | 45 | // swiftlint:disable:next cyclomatic_complexity 46 | public init?(_ raw: afc_error_t) { 47 | switch raw { 48 | case AFC_E_SUCCESS: 49 | return nil 50 | case AFC_E_OP_HEADER_INVALID: 51 | self = .opHeaderInvalid 52 | case AFC_E_NO_RESOURCES: 53 | self = .noResources 54 | case AFC_E_READ_ERROR: 55 | self = .readError 56 | case AFC_E_WRITE_ERROR: 57 | self = .writeError 58 | case AFC_E_UNKNOWN_PACKET_TYPE: 59 | self = .unknownPacketType 60 | case AFC_E_INVALID_ARG: 61 | self = .invalidArg 62 | case AFC_E_OBJECT_NOT_FOUND: 63 | self = .objectNotFound 64 | case AFC_E_OBJECT_IS_DIR: 65 | self = .objectIsDir 66 | case AFC_E_PERM_DENIED: 67 | self = .permDenied 68 | case AFC_E_SERVICE_NOT_CONNECTED: 69 | self = .serviceNotConnected 70 | case AFC_E_OP_TIMEOUT: 71 | self = .opTimeout 72 | case AFC_E_TOO_MUCH_DATA: 73 | self = .tooMuchData 74 | case AFC_E_END_OF_DATA: 75 | self = .endOfData 76 | case AFC_E_OP_NOT_SUPPORTED: 77 | self = .opNotSupported 78 | case AFC_E_OBJECT_EXISTS: 79 | self = .objectExists 80 | case AFC_E_OBJECT_BUSY: 81 | self = .objectBusy 82 | case AFC_E_NO_SPACE_LEFT: 83 | self = .noSpaceLeft 84 | case AFC_E_OP_WOULD_BLOCK: 85 | self = .opWouldBlock 86 | case AFC_E_IO_ERROR: 87 | self = .ioError 88 | case AFC_E_OP_INTERRUPTED: 89 | self = .opInterrupted 90 | case AFC_E_OP_IN_PROGRESS: 91 | self = .opInProgress 92 | case AFC_E_INTERNAL_ERROR: 93 | self = .internalError 94 | case AFC_E_MUX_ERROR: 95 | self = .muxError 96 | case AFC_E_NO_MEM: 97 | self = .noMem 98 | case AFC_E_NOT_ENOUGH_DATA: 99 | self = .notEnoughData 100 | case AFC_E_DIR_NOT_EMPTY: 101 | self = .dirNotEmpty 102 | case AFC_E_FORCE_SIGNED_TYPE: 103 | self = .forceSignedType 104 | default: self = .unknown 105 | } 106 | } 107 | 108 | public var errorDescription: String? { 109 | "AFCClient.Error.\(self)" 110 | } 111 | } 112 | 113 | public enum LinkType { 114 | case hardlink 115 | case symlink 116 | 117 | var raw: afc_link_type_t { 118 | switch self { 119 | case .hardlink: return AFC_HARDLINK 120 | case .symlink: return AFC_SYMLINK 121 | } 122 | } 123 | } 124 | 125 | public final class File: Sendable { 126 | public enum Mode { 127 | /// `r` (`O_RDONLY`) 128 | case readOnly 129 | /// `r+` (`O_RDWR | O_CREAT`) 130 | case readWrite 131 | /// `w` (`O_WRONLY | O_CREAT | O_TRUNC`) 132 | case writeOnly 133 | /// `w+` (`O_RDWR | O_CREAT | O_TRUNC`) 134 | case readWriteTruncate 135 | /// `a` (`O_WRONLY | O_APPEND | O_CREAT`) 136 | case append 137 | /// `a+` (`O_RDWR | O_APPEND | O_CREAT`) 138 | case readAppend 139 | 140 | var raw: afc_file_mode_t { 141 | switch self { 142 | case .readOnly: return AFC_FOPEN_RDONLY 143 | case .readWrite: return AFC_FOPEN_RW 144 | case .writeOnly: return AFC_FOPEN_WRONLY 145 | case .readWriteTruncate: return AFC_FOPEN_WR 146 | case .append: return AFC_FOPEN_APPEND 147 | case .readAppend: return AFC_FOPEN_RDAPPEND 148 | } 149 | } 150 | } 151 | 152 | public enum LockOperation { 153 | case shared 154 | case exclusive 155 | case unlock 156 | 157 | var raw: afc_lock_op_t { 158 | switch self { 159 | case .shared: return AFC_LOCK_SH 160 | case .exclusive: return AFC_LOCK_EX 161 | case .unlock: return AFC_LOCK_UN 162 | } 163 | } 164 | } 165 | 166 | public enum Whence { 167 | case start 168 | case current 169 | case end 170 | 171 | var raw: Int32 { 172 | switch self { 173 | case .start: return SEEK_SET 174 | case .current: return SEEK_CUR 175 | case .end: return SEEK_END 176 | } 177 | } 178 | } 179 | 180 | public let client: AFCClient 181 | let handle: UInt64 182 | init?(client: AFCClient, handle: UInt64) { 183 | guard handle != 0 else { return nil } 184 | self.client = client 185 | self.handle = handle 186 | } 187 | deinit { afc_file_close(client.raw, handle) } 188 | 189 | public func lock(operation: LockOperation) throws { 190 | try CAPI.check(afc_file_lock(client.raw, handle, operation.raw)) 191 | } 192 | 193 | public func read(maxLength: Int) throws -> Data { 194 | try CAPI.getData(maxLength: maxLength) { data, received in 195 | afc_file_read(client.raw, handle, data, .init(maxLength), &received) 196 | } 197 | } 198 | 199 | public func write(_ data: Data) throws -> Int { 200 | var written: UInt32 = 0 201 | try data.withUnsafeBytes { bytes in 202 | let bound = bytes.bindMemory(to: Int8.self) 203 | try CAPI.check( 204 | afc_file_write(client.raw, handle, bound.baseAddress, .init(bound.count), &written) 205 | ) 206 | } 207 | return .init(written) 208 | } 209 | 210 | public func seek(to offset: UInt64, from whence: Whence) throws { 211 | try CAPI.check(afc_file_seek(client.raw, handle, .init(offset), whence.raw)) 212 | } 213 | 214 | public func tell() throws -> UInt64 { 215 | var position: UInt64 = 0 216 | try CAPI.check(afc_file_tell(client.raw, handle, &position)) 217 | return position 218 | } 219 | 220 | public func truncate(to newSize: UInt64) throws { 221 | try CAPI.check(afc_file_truncate(client.raw, handle, newSize)) 222 | } 223 | } 224 | 225 | public typealias Raw = afc_client_t 226 | public static let serviceIdentifier = AFC_SERVICE_NAME 227 | public static nonisolated(unsafe) let newFunc: NewFunc = afc_client_new 228 | public static nonisolated(unsafe) let startFunc: StartFunc = afc_client_start_service 229 | public nonisolated(unsafe) let raw: afc_client_t 230 | public required init(raw: afc_client_t) { 231 | self.raw = raw 232 | self.associatedValue = nil 233 | } 234 | deinit { afc_client_free(raw) } 235 | 236 | private let associatedValue: Sendable? 237 | init(raw: afc_client_t, associatedValue: Sendable?) { 238 | self.raw = raw 239 | self.associatedValue = associatedValue 240 | } 241 | 242 | public func deviceInfo() throws -> [String: String] { 243 | try CAPI.getDictionary( 244 | parseFn: { afc_get_device_info(raw, &$0) }, 245 | freeFn: { afc_dictionary_free($0) } 246 | ) 247 | } 248 | 249 | public func contentsOfDirectory(at url: URL) throws -> [String] { 250 | try url.withUnsafeFileSystemRepresentation { path in 251 | try CAPI.getNullTerminatedArray( 252 | parseFn: { afc_read_directory(raw, path, &$0) }, 253 | freeFn: { afc_dictionary_free($0) } 254 | ) 255 | } 256 | } 257 | 258 | public func fileInfo(for url: URL) throws -> [String: String] { 259 | try url.withUnsafeFileSystemRepresentation { path in 260 | try CAPI.getDictionary( 261 | parseFn: { afc_get_file_info(raw, path, &$0) }, 262 | freeFn: { afc_dictionary_free($0) } 263 | ) 264 | } 265 | } 266 | 267 | public func fileExists(at url: URL) throws -> Bool { 268 | do { 269 | _ = try fileInfo(for: url) 270 | } catch let error as Error where error == .objectNotFound { 271 | return false 272 | } catch { 273 | throw error 274 | } 275 | return true 276 | } 277 | 278 | public func open(_ url: URL, mode: File.Mode) throws -> File { 279 | var handle: UInt64 = 0 280 | try url.withUnsafeFileSystemRepresentation { path in 281 | try CAPI.check(afc_file_open(raw, path, mode.raw, &handle)) 282 | } 283 | guard let file = File(client: self, handle: handle) 284 | else { throw Error.internal } 285 | return file 286 | } 287 | 288 | public func removeItem(at url: URL) throws { 289 | try url.withUnsafeFileSystemRepresentation { 290 | try CAPI.check(afc_remove_path(raw, $0)) 291 | } 292 | } 293 | 294 | public func moveItem(at url: URL, to newURL: URL) throws { 295 | try url.withUnsafeFileSystemRepresentation { path in 296 | try newURL.withUnsafeFileSystemRepresentation { newPath in 297 | try CAPI.check(afc_rename_path(raw, path, newPath)) 298 | } 299 | } 300 | } 301 | 302 | public func createDirectory(at url: URL) throws { 303 | try url.withUnsafeFileSystemRepresentation { path in 304 | try CAPI.check(afc_make_directory(raw, path)) 305 | } 306 | } 307 | 308 | public func truncateFile(at url: URL, to newSize: Int) throws { 309 | try url.withUnsafeFileSystemRepresentation { path in 310 | try CAPI.check(afc_truncate(raw, path, .init(newSize))) 311 | } 312 | } 313 | 314 | public func linkItem(at destination: String, to newURL: URL, type: LinkType) throws { 315 | try newURL.withUnsafeFileSystemRepresentation { newPath in 316 | try CAPI.check(afc_make_link(raw, type.raw, destination, newPath)) 317 | } 318 | } 319 | 320 | public func setTime(at url: URL, to date: Date) throws { 321 | let ns = UInt64(date.timeIntervalSince1970 * 1_000_000_000) 322 | try url.withUnsafeFileSystemRepresentation { path in 323 | try CAPI.check(afc_set_file_time(raw, path, ns)) 324 | } 325 | } 326 | 327 | public func removeItemAndContents(at url: URL) throws { 328 | try url.withUnsafeFileSystemRepresentation { path in 329 | try CAPI.check(afc_remove_path_and_contents(raw, path)) 330 | } 331 | } 332 | 333 | } 334 | -------------------------------------------------------------------------------- /Sources/SwiftyMobileDevice/Device/Lockdown/Clients/DebugserverClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DebugserverClient.swift 3 | // SwiftyMobileDevice 4 | // 5 | // Created by Kabir Oberai on 21/03/21. 6 | // Copyright © 2021 Kabir Oberai. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import libimobiledevice 11 | 12 | public final class DebugserverClient: LockdownService { 13 | 14 | public enum Error: CAPIError, LocalizedError { 15 | case unknown 16 | case `internal` 17 | case invalidArg 18 | case muxError 19 | case sslError 20 | case responseError 21 | case timeout 22 | 23 | public init?(_ raw: debugserver_error_t) { 24 | switch raw { 25 | case DEBUGSERVER_E_SUCCESS: 26 | return nil 27 | case DEBUGSERVER_E_INVALID_ARG: 28 | self = .invalidArg 29 | case DEBUGSERVER_E_MUX_ERROR: 30 | self = .muxError 31 | case DEBUGSERVER_E_SSL_ERROR: 32 | self = .sslError 33 | case DEBUGSERVER_E_RESPONSE_ERROR: 34 | self = .responseError 35 | case DEBUGSERVER_E_TIMEOUT: 36 | self = .timeout 37 | default: 38 | self = .unknown 39 | } 40 | } 41 | 42 | public var errorDescription: String? { 43 | "DebugserverClient.Error.\(self)" 44 | } 45 | } 46 | 47 | public typealias Raw = debugserver_client_t 48 | public static let serviceIdentifier = DEBUGSERVER_SERVICE_NAME // not used 49 | public static nonisolated(unsafe) let newFunc: NewFunc = debugserver_client_new 50 | public static nonisolated(unsafe) let startFunc: StartFunc = debugserver_client_start_service 51 | public nonisolated(unsafe) let raw: debugserver_client_t 52 | public required init(raw: debugserver_client_t) { self.raw = raw } 53 | public static func startService( 54 | withFunc fn: (String) throws -> lockdownd_service_descriptor_t 55 | ) throws -> lockdownd_service_descriptor_t { 56 | // try secure version first 57 | try (try? fn("com.apple.debugserver.DVTSecureSocketProxy")) 58 | ?? fn(DEBUGSERVER_SERVICE_NAME) 59 | } 60 | deinit { debugserver_client_free(raw) } 61 | 62 | @discardableResult 63 | public func send(command commandName: String, arguments: [String]) throws -> Data { 64 | var cArgs: [UnsafeMutablePointer?] = arguments.map { strdup($0) } 65 | defer { cArgs.forEach { $0.map { free($0) } } } 66 | var rawCommand: debugserver_command_t? 67 | try CAPI.check(debugserver_command_new(commandName, Int32(cArgs.count), &cArgs, &rawCommand)) 68 | guard let command = rawCommand else { throw Error.internal } 69 | defer { debugserver_command_free(command) } 70 | return try CAPI.getData { resp, rawSize in 71 | var size: Int { 72 | get { Int(rawSize) } 73 | set { rawSize = .init(newValue) } 74 | } 75 | return debugserver_client_send_command(raw, command, &resp, &size) 76 | } 77 | } 78 | 79 | public func setACKEnabled(_ enabled: Bool) throws { 80 | try CAPI.check(debugserver_client_set_ack_mode(raw, enabled ? 1 : 0)) 81 | } 82 | 83 | public func setEnvironment(key: String, value: String) throws -> String { 84 | try CAPI.getString { 85 | debugserver_client_set_environment_hex_encoded(raw, "\(key)=\(value)", &$0) 86 | } 87 | } 88 | 89 | public func launch(executable: URL, arguments: [String]) throws -> String { 90 | var rawArgs = [executable.withUnsafeFileSystemRepresentation { strdup($0!) }] 91 | + arguments.map { strdup($0) } 92 | defer { rawArgs.forEach { $0.map { free($0) } } } 93 | return try CAPI.getString { 94 | debugserver_client_set_argv(raw, Int32(rawArgs.count), &rawArgs, &$0) 95 | } 96 | } 97 | 98 | public static func hexEncode(_ string: String) throws -> Data { 99 | try CAPI.getData { debugserver_encode_string(string, &$0, &$1) } 100 | } 101 | 102 | public static func hexDecode(_ data: Data) throws -> String { 103 | try data.withUnsafeBytes { buf in 104 | let bound = buf.bindMemory(to: Int8.self) 105 | return try CAPI.getString { 106 | debugserver_decode_string(bound.baseAddress!, bound.count, &$0) 107 | } 108 | } 109 | } 110 | 111 | } 112 | -------------------------------------------------------------------------------- /Sources/SwiftyMobileDevice/Device/Lockdown/Clients/HeartbeatClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HeartbeatClient.swift 3 | // SwiftyMobileDevice 4 | // 5 | // Created by Kabir Oberai on 13/11/19. 6 | // Copyright © 2019 Kabir Oberai. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import libimobiledevice 11 | 12 | public final class HeartbeatClient: LockdownService { 13 | 14 | public enum Error: CAPIError, LocalizedError { 15 | case unknown 16 | case `internal` 17 | case invalidArg 18 | case plistError 19 | case muxError 20 | case sslError 21 | case notEnoughData 22 | case timeout 23 | 24 | public init?(_ raw: heartbeat_error_t) { 25 | switch raw { 26 | case HEARTBEAT_E_SUCCESS: return nil 27 | case HEARTBEAT_E_INVALID_ARG: self = .invalidArg 28 | case HEARTBEAT_E_PLIST_ERROR: self = .plistError 29 | case HEARTBEAT_E_MUX_ERROR: self = .muxError 30 | case HEARTBEAT_E_SSL_ERROR: self = .sslError 31 | case HEARTBEAT_E_NOT_ENOUGH_DATA: self = .notEnoughData 32 | case HEARTBEAT_E_TIMEOUT: self = .timeout 33 | default: self = .unknown 34 | } 35 | } 36 | 37 | public var errorDescription: String? { 38 | "HeartbeatClient.Error.\(self)" 39 | } 40 | } 41 | 42 | public typealias Raw = heartbeat_client_t 43 | public static let serviceIdentifier = HEARTBEAT_SERVICE_NAME 44 | public static nonisolated(unsafe) let newFunc: NewFunc = heartbeat_client_new 45 | public static nonisolated(unsafe) let startFunc: StartFunc = heartbeat_client_start_service 46 | public nonisolated(unsafe) let raw: heartbeat_client_t 47 | public required init(raw: heartbeat_client_t) { self.raw = raw } 48 | deinit { heartbeat_client_free(raw) } 49 | 50 | private let encoder = PlistNodeEncoder() 51 | private let decoder = PlistNodeDecoder() 52 | 53 | public func send(_ value: T) throws { 54 | try CAPI.check(encoder.withEncoded(value) { heartbeat_send(raw, $0) }) 55 | } 56 | 57 | public func receive(_ type: T.Type, timeout: TimeInterval? = nil) throws -> T { 58 | try decoder.decode(type) { buf in 59 | try CAPI.check( 60 | timeout.map { 61 | heartbeat_receive_with_timeout(raw, &buf, .init($0 * 1000)) 62 | } ?? heartbeat_receive(raw, &buf) 63 | ) 64 | } 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /Sources/SwiftyMobileDevice/Device/Lockdown/Clients/HouseArrestClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HouseArrestClient.swift 3 | // 4 | // 5 | // Created by Kabir Oberai on 10/05/21. 6 | // 7 | 8 | import Foundation 9 | import libimobiledevice 10 | 11 | public final class HouseArrestClient: LockdownService { 12 | 13 | public enum Error: CAPIError, LocalizedError { 14 | case unknown 15 | case invalidArg 16 | case plistError 17 | case connFailed 18 | case invalidMode 19 | 20 | public init?(_ raw: house_arrest_error_t) { 21 | switch raw { 22 | case HOUSE_ARREST_E_SUCCESS: 23 | return nil 24 | case HOUSE_ARREST_E_INVALID_ARG: 25 | self = .invalidArg 26 | case HOUSE_ARREST_E_PLIST_ERROR: 27 | self = .plistError 28 | case HOUSE_ARREST_E_CONN_FAILED: 29 | self = .connFailed 30 | case HOUSE_ARREST_E_INVALID_MODE: 31 | self = .invalidMode 32 | default: 33 | self = .unknown 34 | } 35 | } 36 | 37 | public var errorDescription: String? { 38 | "HouseArrestClient.Error.\(self)" 39 | } 40 | } 41 | 42 | public struct RequestFailure: LocalizedError { 43 | public let reason: String? 44 | public var errorDescription: String? { reason } 45 | } 46 | 47 | public enum Vendable { 48 | case container 49 | case documents 50 | 51 | var command: String { 52 | switch self { 53 | case .container: 54 | return "VendContainer" 55 | case .documents: 56 | return "VendDocuments" 57 | } 58 | } 59 | } 60 | 61 | public typealias Raw = house_arrest_client_t 62 | public static let serviceIdentifier = HOUSE_ARREST_SERVICE_NAME 63 | public static nonisolated(unsafe) let newFunc: NewFunc = house_arrest_client_new 64 | public static nonisolated(unsafe) let startFunc: StartFunc = house_arrest_client_start_service 65 | public nonisolated(unsafe) let raw: house_arrest_client_t 66 | public required init(raw: house_arrest_client_t) { self.raw = raw } 67 | deinit { house_arrest_client_free(raw) } 68 | 69 | private static let decoder = PlistNodeDecoder() 70 | 71 | private struct Result: Decodable { 72 | enum Status: String, Decodable { 73 | case complete = "Complete" 74 | } 75 | let status: Status? 76 | let error: String? 77 | 78 | private enum CodingKeys: String, CodingKey { 79 | case status = "Status" 80 | case error = "Error" 81 | } 82 | } 83 | 84 | public func vend(_ vendable: Vendable, forApp appID: String) throws -> AFCClient { 85 | try CAPI.check(house_arrest_send_command(raw, vendable.command, appID)) 86 | let result = try Self.decoder.decode(Result.self) { 87 | try CAPI.check(house_arrest_get_result(raw, &$0)) 88 | } 89 | guard result.status == .complete else { 90 | throw RequestFailure(reason: result.error) 91 | } 92 | 93 | var afcRaw: afc_client_t? 94 | try CAPI.check(afc_client_new_from_house_arrest_client(raw, &afcRaw)) 95 | guard let afcRaw else { throw CAPIGenericError.unexpectedNil } 96 | 97 | return AFCClient(raw: afcRaw, associatedValue: self) 98 | } 99 | 100 | } 101 | -------------------------------------------------------------------------------- /Sources/SwiftyMobileDevice/Device/Lockdown/Clients/InstallationProxyClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InstallationProxyClient.swift 3 | // SwiftyMobileDevice 4 | // 5 | // Created by Kabir Oberai on 14/11/19. 6 | // Copyright © 2019 Kabir Oberai. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Superutils 11 | import libimobiledevice 12 | import plist 13 | 14 | private func requestCallbackC(command: plist_t?, status: plist_t?, userData: UnsafeMutableRawPointer?) { 15 | InstallationProxyClient.requestCallback(rawCommand: command, rawStatus: status, rawUserData: userData) 16 | } 17 | 18 | public final class InstallationProxyClient: LockdownService { 19 | 20 | public enum Error: CAPIError, LocalizedError { 21 | case unknown 22 | case `internal` 23 | case invalidArg 24 | case plistError 25 | case connFailed 26 | case opInProgress 27 | case opFailed 28 | case receiveTimeout 29 | case alreadyArchived 30 | case apiInternalError 31 | case applicationAlreadyInstalled 32 | case applicationMoveFailed 33 | case applicationSinfCaptureFailed 34 | case applicationSandboxFailed 35 | case applicationVerificationFailed 36 | case archiveDestructionFailed 37 | case bundleVerificationFailed 38 | case carrierBundleCopyFailed 39 | case carrierBundleDirectoryCreationFailed 40 | case carrierBundleMissingSupportedSims 41 | case commCenterNotificationFailed 42 | case containerCreationFailed 43 | case containerP0wnFailed 44 | case containerRemovalFailed 45 | case embeddedProfileInstallFailed 46 | case executableTwiddleFailed 47 | case existenceCheckFailed 48 | case installMapUpdateFailed 49 | case manifestCaptureFailed 50 | case mapGenerationFailed 51 | case missingBundleExecutable 52 | case missingBundleIdentifier 53 | case missingBundlePath 54 | case missingContainer 55 | case notificationFailed 56 | case packageExtractionFailed 57 | case packageInspectionFailed 58 | case packageMoveFailed 59 | case pathConversionFailed 60 | case restoreContainerFailed 61 | case seatbeltProfileRemovalFailed 62 | case stageCreationFailed 63 | case symlinkFailed 64 | case unknownCommand 65 | case itunesArtworkCaptureFailed 66 | case itunesMetadataCaptureFailed 67 | case deviceOsVersionTooLow 68 | case deviceFamilyNotSupported 69 | case packagePatchFailed 70 | case incorrectArchitecture 71 | case pluginCopyFailed 72 | case breadcrumbFailed 73 | case breadcrumbUnlockFailed 74 | case geojsonCaptureFailed 75 | case newsstandArtworkCaptureFailed 76 | case missingCommand 77 | case notEntitled 78 | case missingPackagePath 79 | case missingContainerPath 80 | case missingApplicationIdentifier 81 | case missingAttributeValue 82 | case lookupFailed 83 | case dictCreationFailed 84 | case installProhibited 85 | case uninstallProhibited 86 | case missingBundleVersion 87 | 88 | // swiftlint:disable:next cyclomatic_complexity function_body_length 89 | public init?(_ raw: instproxy_error_t) { 90 | switch raw { 91 | case INSTPROXY_E_SUCCESS: 92 | return nil 93 | case INSTPROXY_E_INVALID_ARG: 94 | self = .invalidArg 95 | case INSTPROXY_E_PLIST_ERROR: 96 | self = .plistError 97 | case INSTPROXY_E_CONN_FAILED: 98 | self = .connFailed 99 | case INSTPROXY_E_OP_IN_PROGRESS: 100 | self = .opInProgress 101 | case INSTPROXY_E_OP_FAILED: 102 | self = .opFailed 103 | case INSTPROXY_E_RECEIVE_TIMEOUT: 104 | self = .receiveTimeout 105 | case INSTPROXY_E_ALREADY_ARCHIVED: 106 | self = .alreadyArchived 107 | case INSTPROXY_E_API_INTERNAL_ERROR: 108 | self = .apiInternalError 109 | case INSTPROXY_E_APPLICATION_ALREADY_INSTALLED: 110 | self = .applicationAlreadyInstalled 111 | case INSTPROXY_E_APPLICATION_MOVE_FAILED: 112 | self = .applicationMoveFailed 113 | case INSTPROXY_E_APPLICATION_SINF_CAPTURE_FAILED: 114 | self = .applicationSinfCaptureFailed 115 | case INSTPROXY_E_APPLICATION_SANDBOX_FAILED: 116 | self = .applicationSandboxFailed 117 | case INSTPROXY_E_APPLICATION_VERIFICATION_FAILED: 118 | self = .applicationVerificationFailed 119 | case INSTPROXY_E_ARCHIVE_DESTRUCTION_FAILED: 120 | self = .archiveDestructionFailed 121 | case INSTPROXY_E_BUNDLE_VERIFICATION_FAILED: 122 | self = .bundleVerificationFailed 123 | case INSTPROXY_E_CARRIER_BUNDLE_COPY_FAILED: 124 | self = .carrierBundleCopyFailed 125 | case INSTPROXY_E_CARRIER_BUNDLE_DIRECTORY_CREATION_FAILED: 126 | self = .carrierBundleDirectoryCreationFailed 127 | case INSTPROXY_E_CARRIER_BUNDLE_MISSING_SUPPORTED_SIMS: 128 | self = .carrierBundleMissingSupportedSims 129 | case INSTPROXY_E_COMM_CENTER_NOTIFICATION_FAILED: 130 | self = .commCenterNotificationFailed 131 | case INSTPROXY_E_CONTAINER_CREATION_FAILED: 132 | self = .containerCreationFailed 133 | case INSTPROXY_E_CONTAINER_P0WN_FAILED: 134 | self = .containerP0wnFailed 135 | case INSTPROXY_E_CONTAINER_REMOVAL_FAILED: 136 | self = .containerRemovalFailed 137 | case INSTPROXY_E_EMBEDDED_PROFILE_INSTALL_FAILED: 138 | self = .embeddedProfileInstallFailed 139 | case INSTPROXY_E_EXECUTABLE_TWIDDLE_FAILED: 140 | self = .executableTwiddleFailed 141 | case INSTPROXY_E_EXISTENCE_CHECK_FAILED: 142 | self = .existenceCheckFailed 143 | case INSTPROXY_E_INSTALL_MAP_UPDATE_FAILED: 144 | self = .installMapUpdateFailed 145 | case INSTPROXY_E_MANIFEST_CAPTURE_FAILED: 146 | self = .manifestCaptureFailed 147 | case INSTPROXY_E_MAP_GENERATION_FAILED: 148 | self = .mapGenerationFailed 149 | case INSTPROXY_E_MISSING_BUNDLE_EXECUTABLE: 150 | self = .missingBundleExecutable 151 | case INSTPROXY_E_MISSING_BUNDLE_IDENTIFIER: 152 | self = .missingBundleIdentifier 153 | case INSTPROXY_E_MISSING_BUNDLE_PATH: 154 | self = .missingBundlePath 155 | case INSTPROXY_E_MISSING_CONTAINER: 156 | self = .missingContainer 157 | case INSTPROXY_E_NOTIFICATION_FAILED: 158 | self = .notificationFailed 159 | case INSTPROXY_E_PACKAGE_EXTRACTION_FAILED: 160 | self = .packageExtractionFailed 161 | case INSTPROXY_E_PACKAGE_INSPECTION_FAILED: 162 | self = .packageInspectionFailed 163 | case INSTPROXY_E_PACKAGE_MOVE_FAILED: 164 | self = .packageMoveFailed 165 | case INSTPROXY_E_PATH_CONVERSION_FAILED: 166 | self = .pathConversionFailed 167 | case INSTPROXY_E_RESTORE_CONTAINER_FAILED: 168 | self = .restoreContainerFailed 169 | case INSTPROXY_E_SEATBELT_PROFILE_REMOVAL_FAILED: 170 | self = .seatbeltProfileRemovalFailed 171 | case INSTPROXY_E_STAGE_CREATION_FAILED: 172 | self = .stageCreationFailed 173 | case INSTPROXY_E_SYMLINK_FAILED: 174 | self = .symlinkFailed 175 | case INSTPROXY_E_UNKNOWN_COMMAND: 176 | self = .unknownCommand 177 | case INSTPROXY_E_ITUNES_ARTWORK_CAPTURE_FAILED: 178 | self = .itunesArtworkCaptureFailed 179 | case INSTPROXY_E_ITUNES_METADATA_CAPTURE_FAILED: 180 | self = .itunesMetadataCaptureFailed 181 | case INSTPROXY_E_DEVICE_OS_VERSION_TOO_LOW: 182 | self = .deviceOsVersionTooLow 183 | case INSTPROXY_E_DEVICE_FAMILY_NOT_SUPPORTED: 184 | self = .deviceFamilyNotSupported 185 | case INSTPROXY_E_PACKAGE_PATCH_FAILED: 186 | self = .packagePatchFailed 187 | case INSTPROXY_E_INCORRECT_ARCHITECTURE: 188 | self = .incorrectArchitecture 189 | case INSTPROXY_E_PLUGIN_COPY_FAILED: 190 | self = .pluginCopyFailed 191 | case INSTPROXY_E_BREADCRUMB_FAILED: 192 | self = .breadcrumbFailed 193 | case INSTPROXY_E_BREADCRUMB_UNLOCK_FAILED: 194 | self = .breadcrumbUnlockFailed 195 | case INSTPROXY_E_GEOJSON_CAPTURE_FAILED: 196 | self = .geojsonCaptureFailed 197 | case INSTPROXY_E_NEWSSTAND_ARTWORK_CAPTURE_FAILED: 198 | self = .newsstandArtworkCaptureFailed 199 | case INSTPROXY_E_MISSING_COMMAND: 200 | self = .missingCommand 201 | case INSTPROXY_E_NOT_ENTITLED: 202 | self = .notEntitled 203 | case INSTPROXY_E_MISSING_PACKAGE_PATH: 204 | self = .missingPackagePath 205 | case INSTPROXY_E_MISSING_CONTAINER_PATH: 206 | self = .missingContainerPath 207 | case INSTPROXY_E_MISSING_APPLICATION_IDENTIFIER: 208 | self = .missingApplicationIdentifier 209 | case INSTPROXY_E_MISSING_ATTRIBUTE_VALUE: 210 | self = .missingAttributeValue 211 | case INSTPROXY_E_LOOKUP_FAILED: 212 | self = .lookupFailed 213 | case INSTPROXY_E_DICT_CREATION_FAILED: 214 | self = .dictCreationFailed 215 | case INSTPROXY_E_INSTALL_PROHIBITED: 216 | self = .installProhibited 217 | case INSTPROXY_E_UNINSTALL_PROHIBITED: 218 | self = .uninstallProhibited 219 | case INSTPROXY_E_MISSING_BUNDLE_VERSION: 220 | self = .missingBundleVersion 221 | default: 222 | self = .unknown 223 | } 224 | } 225 | 226 | public var errorDescription: String? { 227 | "InstallationProxyClient.Error.\(self)" 228 | } 229 | } 230 | 231 | public struct StatusError: LocalizedError { 232 | public let type: Error 233 | public let name: String 234 | public let details: String? 235 | public let code: Int 236 | 237 | init?(raw: plist_t) { 238 | var rawName: UnsafeMutablePointer? 239 | var rawDetails: UnsafeMutablePointer? 240 | var rawCode: UInt64 = 0 241 | guard let type = Error(instproxy_status_get_error(raw, &rawName, &rawDetails, &rawCode)), 242 | let name = rawName.flatMap({ 243 | String(bytesNoCopy: $0, length: strlen($0), encoding: .utf8, freeWhenDone: true) 244 | }) 245 | else { return nil } 246 | 247 | self.type = type 248 | self.name = name 249 | self.details = rawDetails.flatMap { 250 | String(bytesNoCopy: $0, length: strlen($0), encoding: .utf8, freeWhenDone: true) 251 | } 252 | self.code = .init(rawCode) 253 | } 254 | 255 | public var errorDescription: String? { 256 | "\(name) (0x\(String(code, radix: 16)))\(details.map { ": \($0)" } ?? "")" 257 | } 258 | } 259 | 260 | public struct RequestProgress { 261 | public let details: String 262 | public let progress: Double? 263 | } 264 | 265 | // open so that extra options may be added 266 | open class Options: Encodable { 267 | public var skipUninstall: Bool? 268 | public var applicationSINF: Data? 269 | public var itunesMetadata: Data? 270 | public var applicationType: String? 271 | public var returnAttributes: [String]? 272 | public var additionalOptions: [String: String] = [:] 273 | 274 | private enum CodingKeys: String, CodingKey { 275 | case skipUninstall = "SkipUninstall" 276 | case applicationSINF = "ApplicationSINF" 277 | case itunesMetadata = "iTunesMetadata" 278 | case applicationType = "ApplicationType" 279 | case returnAttributes = "ReturnAttributes" 280 | } 281 | 282 | public func encode(to encoder: Encoder) throws { 283 | var keyedContainer = encoder.container(keyedBy: CodingKeys.self) 284 | try skipUninstall.map { try keyedContainer.encode($0, forKey: .skipUninstall) } 285 | try applicationSINF.map { try keyedContainer.encode($0, forKey: .applicationSINF) } 286 | try itunesMetadata.map { try keyedContainer.encode($0, forKey: .itunesMetadata) } 287 | try applicationType.map { try keyedContainer.encode($0, forKey: .applicationType) } 288 | try returnAttributes.map { try keyedContainer.encode($0, forKey: .returnAttributes) } 289 | try additionalOptions.encode(to: encoder) 290 | } 291 | 292 | public init( 293 | skipUninstall: Bool? = nil, 294 | applicationSINF: Data? = nil, 295 | itunesMetadata: Data? = nil, 296 | applicationType: String? = nil, 297 | returnAttributes: [String]? = nil, 298 | additionalOptions: [String: String] = [:] 299 | ) { 300 | self.skipUninstall = skipUninstall 301 | self.applicationSINF = applicationSINF 302 | self.itunesMetadata = itunesMetadata 303 | self.applicationType = applicationType 304 | self.returnAttributes = returnAttributes 305 | self.additionalOptions = additionalOptions 306 | } 307 | } 308 | 309 | private final class RequestUserData: Sendable { 310 | enum Updater: Sendable { 311 | // list is moved 312 | case browse(@Sendable (_ currIndex: Int, _ total: Int, _ list: plist_t?) -> Void) 313 | case progress(@Sendable (RequestProgress) -> Void) 314 | } 315 | 316 | let updater: Updater 317 | let completion: @Sendable (Result<(), Swift.Error>) -> Void 318 | private let stream: AsyncThrowingStream 319 | 320 | init(updater: Updater) { 321 | let (stream, continuation) = AsyncThrowingStream.makeStream() 322 | self.updater = updater 323 | self.completion = { result in 324 | switch result { 325 | case .success: 326 | continuation.finish() 327 | case .failure(let error): 328 | continuation.finish(throwing: error) 329 | } 330 | } 331 | self.stream = stream 332 | } 333 | 334 | func opaque() -> UnsafeMutableRawPointer { 335 | Unmanaged.passUnretained(self).toOpaque() 336 | } 337 | 338 | func waitForCompletion() async throws { 339 | for try await _ in stream {} 340 | } 341 | } 342 | 343 | public typealias Raw = instproxy_client_t 344 | public static let serviceIdentifier = INSTPROXY_SERVICE_NAME 345 | public static nonisolated(unsafe) let newFunc: NewFunc = instproxy_client_new 346 | public static nonisolated(unsafe) let startFunc: StartFunc = instproxy_client_start_service 347 | public nonisolated(unsafe) let raw: instproxy_client_t 348 | public required init(raw: instproxy_client_t) { self.raw = raw } 349 | deinit { instproxy_client_free(raw) } 350 | 351 | private let encoder = PlistNodeEncoder() 352 | private let decoder = PlistNodeDecoder() 353 | 354 | fileprivate static func requestCallback(rawCommand: plist_t?, rawStatus: plist_t?, rawUserData: UnsafeMutableRawPointer?) { 355 | let userData = Unmanaged.fromOpaque(rawUserData!).takeUnretainedValue() 356 | 357 | func complete(_ result: Result<(), Swift.Error>) { 358 | userData.completion(result) 359 | } 360 | 361 | if let error = StatusError(raw: rawStatus!) { 362 | return complete(.failure(error)) 363 | } 364 | 365 | let statusName = Result { 366 | try CAPI.getString { 367 | instproxy_status_get_name(rawStatus, &$0) 368 | } 369 | } 370 | let isComplete = (try? statusName.get()) == "Complete" 371 | 372 | switch userData.updater { 373 | case .browse(let callback): 374 | var total: UInt64 = 0 375 | var currIndex: UInt64 = 0 376 | var currAmount: UInt64 = 0 377 | var list: plist_t? 378 | instproxy_status_get_current_list(rawStatus, &total, &currIndex, &currAmount, &list) 379 | callback(Int(currIndex), Int(total), list) 380 | if isComplete { complete(.success(())) } 381 | case .progress(let callback): 382 | let progress: Double? 383 | if isComplete { 384 | progress = 1 385 | } else { 386 | var rawPercent: Int32 = -1 387 | instproxy_status_get_percent_complete(rawStatus, &rawPercent) 388 | progress = rawPercent >= 0 ? (Double(rawPercent) / 100) : nil 389 | } 390 | statusName.get(withErrorHandler: complete).map { 391 | callback(.init(details: $0, progress: progress)) 392 | } 393 | if isComplete { return complete(.success(())) } 394 | } 395 | } 396 | 397 | public func install( 398 | package: URL, 399 | upgrade: Bool = false, 400 | options: Options = .init(), 401 | progress: @escaping @Sendable (RequestProgress) -> Void 402 | ) async throws { 403 | let userData = RequestUserData(updater: .progress(progress)) 404 | 405 | let fn = upgrade ? instproxy_upgrade : instproxy_install 406 | 407 | // Note: build performance 408 | let err = try encoder.withEncoded(options) { (rawOptions: plist_t) -> instproxy_error_t in 409 | package.withUnsafeFileSystemRepresentation { (path: UnsafePointer?) -> instproxy_error_t in 410 | fn(raw, path, rawOptions, requestCallbackC, userData.opaque()) 411 | } 412 | } 413 | try CAPI.check(err) 414 | 415 | try await userData.waitForCompletion() 416 | } 417 | 418 | public func uninstall( 419 | bundleID: String, 420 | options: Options = .init(), 421 | progress: @escaping @Sendable (RequestProgress) -> Void 422 | ) async throws { 423 | let userData = RequestUserData(updater: .progress(progress)) 424 | 425 | let err = try encoder.withEncoded(options) { (rawOptions: plist_t) -> instproxy_error_t in 426 | instproxy_uninstall(raw, bundleID, rawOptions, requestCallbackC, userData.opaque()) 427 | } 428 | try CAPI.check(err) 429 | 430 | try await userData.waitForCompletion() 431 | } 432 | 433 | public func archive( 434 | app: String, 435 | options: Options = .init(), 436 | progress: @escaping @Sendable (RequestProgress) -> Void 437 | ) async throws { 438 | let userData = RequestUserData(updater: .progress(progress)) 439 | 440 | try encoder.withEncoded(options) { 441 | try CAPI.check(instproxy_archive(raw, app, $0, requestCallbackC, userData.opaque())) 442 | } 443 | 444 | try await userData.waitForCompletion() 445 | } 446 | 447 | public func restore( 448 | app: String, 449 | options: Options = .init(), 450 | progress: @escaping @Sendable (RequestProgress) -> Void 451 | ) async throws { 452 | let userData = RequestUserData(updater: .progress(progress)) 453 | 454 | try encoder.withEncoded(options) { 455 | try CAPI.check(instproxy_restore(raw, app, $0, requestCallbackC, userData.opaque())) 456 | } 457 | 458 | try await userData.waitForCompletion() 459 | } 460 | 461 | public func lookupArchives(resultType: T.Type) throws -> [String: T] { 462 | return try decoder.decode([String: T].self) { (result: inout plist_t?) throws -> Void in 463 | try CAPI.check(instproxy_lookup_archives(raw, nil, &result)) 464 | } 465 | } 466 | 467 | public func removeArchive( 468 | app: String, 469 | progress: @escaping @Sendable (RequestProgress) -> Void 470 | ) async throws { 471 | let userData = RequestUserData(updater: .progress(progress)) 472 | try CAPI.check(instproxy_remove_archive(raw, app, nil, requestCallbackC, userData.opaque())) 473 | try await userData.waitForCompletion() 474 | } 475 | 476 | public func lookup( 477 | resultType: T.Type, 478 | apps: [String], 479 | options: Options = .init() 480 | ) throws -> [String: T] { 481 | let rawIDs = apps.map { strdup($0) } 482 | defer { rawIDs.forEach { $0.map { free($0) } } } 483 | 484 | var ids = rawIDs.map { UnsafePointer($0) } 485 | 486 | return try decoder.decode([String: T].self) { (result: inout plist_t?) throws -> Void in 487 | try encoder.withEncoded(options) { (rawOpts: plist_t) throws -> Void in 488 | try CAPI.check(instproxy_lookup(raw, &ids, rawOpts, &result)) 489 | } 490 | } 491 | } 492 | 493 | public func browse( 494 | resultType: T.Type, 495 | options: Options = .init(), 496 | progress: @escaping @Sendable (_ currIndex: Int, _ total: Int, _ apps: Result<[T], Swift.Error>) -> Void 497 | ) async throws { 498 | let updater = RequestUserData.Updater.browse { currIdx, total, rawApps in 499 | let apps = rawApps.map { unwrapped in 500 | Result { try self.decoder.decode([T].self, moving: unwrapped) } 501 | } ?? .success([]) 502 | progress(currIdx, total, apps) 503 | } 504 | let userData = RequestUserData(updater: updater) 505 | try encoder.withEncoded(options) { rawOpts in 506 | instproxy_browse_with_callback(raw, rawOpts, requestCallbackC, userData.opaque()) 507 | } 508 | try await userData.waitForCompletion() 509 | } 510 | 511 | public func executable(forBundleID bundleID: String) throws -> URL { 512 | var rawPath: UnsafeMutablePointer? 513 | try CAPI.check(instproxy_client_get_path_for_bundle_identifier(raw, bundleID, &rawPath)) 514 | guard let path = rawPath else { throw CAPIGenericError.unexpectedNil } 515 | return URL(fileURLWithFileSystemRepresentation: path, isDirectory: false, relativeTo: nil) 516 | } 517 | 518 | } 519 | -------------------------------------------------------------------------------- /Sources/SwiftyMobileDevice/Device/Lockdown/Clients/MISAgentClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MISAgentClient.swift 3 | // SwiftyMobileDevice 4 | // 5 | // Created by Kabir Oberai on 21/03/21. 6 | // Copyright © 2021 Kabir Oberai. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import libimobiledevice 11 | import plist 12 | 13 | public final class MISAgentClient: LockdownService { 14 | 15 | public enum Error: CAPIError, LocalizedError { 16 | case unknown 17 | case invalidArg 18 | case plistError 19 | case connFailed 20 | case requestFailed 21 | 22 | public init?(_ raw: misagent_error_t) { 23 | switch raw { 24 | case MISAGENT_E_SUCCESS: 25 | return nil 26 | case MISAGENT_E_INVALID_ARG: 27 | self = .invalidArg 28 | case MISAGENT_E_PLIST_ERROR: 29 | self = .plistError 30 | case MISAGENT_E_CONN_FAILED: 31 | self = .connFailed 32 | case MISAGENT_E_REQUEST_FAILED: 33 | self = .requestFailed 34 | default: 35 | self = .unknown 36 | } 37 | } 38 | 39 | public var errorDescription: String? { 40 | "MISAgentClient.Error.\(self)" 41 | } 42 | } 43 | 44 | public typealias Raw = misagent_client_t 45 | public static let serviceIdentifier = MISAGENT_SERVICE_NAME 46 | public static nonisolated(unsafe) let newFunc: NewFunc = misagent_client_new 47 | public static nonisolated(unsafe) let startFunc: StartFunc = misagent_client_start_service 48 | public nonisolated(unsafe) let raw: misagent_client_t 49 | public required init(raw: misagent_client_t) { self.raw = raw } 50 | deinit { misagent_client_free(raw) } 51 | 52 | private let encoder = PlistNodeEncoder() 53 | private let decoder = PlistNodeDecoder() 54 | 55 | public var statusCode: Int32 { misagent_get_status_code(raw) } 56 | 57 | public func install(profile: Data) throws { 58 | try encoder.withEncoded(profile) { 59 | try CAPI.check(misagent_install(raw, $0)) 60 | } 61 | } 62 | 63 | // iOS < 9.3 64 | public func profilesLegacy() throws -> [Data] { 65 | try decoder.decode([Data].self) { try CAPI.check(misagent_copy(raw, &$0)) } 66 | } 67 | 68 | // iOS >= 9.3 69 | public func profiles() throws -> [Data] { 70 | try decoder.decode([Data].self) { try CAPI.check(misagent_copy_all(raw, &$0)) } 71 | } 72 | 73 | public func removeProfile(withUUID uuid: String) throws { 74 | try CAPI.check(misagent_remove(raw, uuid)) 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /Sources/SwiftyMobileDevice/Device/Lockdown/Clients/MobileImageMounterClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MobileImageMounterClient.swift 3 | // SwiftyMobileDevice 4 | // 5 | // Created by Kabir Oberai on 21/03/21. 6 | // Copyright © 2021 Kabir Oberai. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import libimobiledevice 11 | import plist 12 | 13 | public final class MobileImageMounterClient: LockdownService { 14 | 15 | public enum Error: CAPIError, LocalizedError { 16 | case unknown 17 | case invalidArg 18 | case plistError 19 | case connFailed 20 | case commandFailed 21 | case deviceLocked 22 | 23 | public init?(_ raw: mobile_image_mounter_error_t) { 24 | switch raw { 25 | case MOBILE_IMAGE_MOUNTER_E_SUCCESS: 26 | return nil 27 | case MOBILE_IMAGE_MOUNTER_E_INVALID_ARG: 28 | self = .invalidArg 29 | case MOBILE_IMAGE_MOUNTER_E_PLIST_ERROR: 30 | self = .plistError 31 | case MOBILE_IMAGE_MOUNTER_E_CONN_FAILED: 32 | self = .connFailed 33 | case MOBILE_IMAGE_MOUNTER_E_COMMAND_FAILED: 34 | self = .commandFailed 35 | case MOBILE_IMAGE_MOUNTER_E_DEVICE_LOCKED: 36 | self = .deviceLocked 37 | default: 38 | self = .unknown 39 | } 40 | } 41 | 42 | public var errorDescription: String? { 43 | "MobileImageMounterClient.Error.\(self)" 44 | } 45 | } 46 | 47 | public typealias Raw = mobile_image_mounter_client_t 48 | public static let serviceIdentifier = MOBILE_IMAGE_MOUNTER_SERVICE_NAME 49 | public static nonisolated(unsafe) let newFunc: NewFunc = mobile_image_mounter_new 50 | public static nonisolated(unsafe) let startFunc: StartFunc = mobile_image_mounter_start_service 51 | public nonisolated(unsafe) let raw: mobile_image_mounter_client_t 52 | public required init(raw: mobile_image_mounter_client_t) { self.raw = raw } 53 | deinit { 54 | mobile_image_mounter_hangup(raw) 55 | mobile_image_mounter_free(raw) 56 | } 57 | 58 | private let decoder = PlistNodeDecoder() 59 | 60 | public func lookup(imageType: String, resultType: T.Type) throws -> T { 61 | try decoder.decode(resultType) { 62 | try CAPI.check(mobile_image_mounter_lookup_image(raw, imageType, &$0)) 63 | } 64 | } 65 | 66 | // the caller must open/close the stream themselves. 67 | public func upload(imageType: String, file: InputStream, size: Int, signature: Data?) throws { 68 | let userData = Unmanaged.passRetained(file) 69 | let finalSig = signature ?? Data() 70 | try finalSig.withUnsafeBytes { (buf: UnsafeRawBufferPointer) in 71 | let bound = buf.bindMemory(to: Int8.self) 72 | try CAPI.check( 73 | mobile_image_mounter_upload_image( 74 | raw, imageType, size, bound.baseAddress!, .init(bound.count), 75 | { chunk, size, rawUserData in 76 | // swiftlint:disable:previous opening_brace 77 | let file = Unmanaged.fromOpaque(rawUserData!).takeUnretainedValue() 78 | let bytesRead = file.read(chunk!.assumingMemoryBound(to: UInt8.self), maxLength: size) 79 | return bytesRead == 0 ? -1 : bytesRead 80 | }, userData.toOpaque() 81 | ) 82 | ) 83 | } 84 | userData.release() 85 | } 86 | 87 | public func mount( 88 | imageType: String, imageURL: URL? = nil, signature: Data, resultType: T.Type 89 | ) throws -> T { 90 | // the url is ignored on iOS >= 7 91 | let url = imageURL ?? URL(fileURLWithPath: "/private/var/mobile/Media/PublicStaging/staging.dimage") 92 | return try url.withUnsafeFileSystemRepresentation { destRaw in 93 | try signature.withUnsafeBytes { sigBuf in 94 | let sigBound = sigBuf.bindMemory(to: Int8.self) 95 | return try decoder.decode(resultType) { 96 | try CAPI.check( 97 | mobile_image_mounter_mount_image( 98 | raw, destRaw, sigBound.baseAddress, .init(sigBound.count), imageType, &$0 99 | ) 100 | ) 101 | } 102 | } 103 | } 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /Sources/SwiftyMobileDevice/Device/Lockdown/Clients/SBServicesClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SBServicesClient.swift 3 | // SwiftyMobileDevice 4 | // 5 | // Created by Kabir Oberai on 29/03/21. 6 | // Copyright © 2021 Kabir Oberai. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import libimobiledevice 11 | import plist 12 | 13 | public final class SBServicesClient: LockdownService { 14 | 15 | public enum Error: CAPIError, LocalizedError { 16 | case unknown 17 | case invalidArg 18 | case plistError 19 | case connFailed 20 | 21 | public init?(_ raw: sbservices_error_t) { 22 | switch raw { 23 | case SBSERVICES_E_SUCCESS: 24 | return nil 25 | case SBSERVICES_E_INVALID_ARG: 26 | self = .invalidArg 27 | case SBSERVICES_E_PLIST_ERROR: 28 | self = .plistError 29 | case SBSERVICES_E_CONN_FAILED: 30 | self = .connFailed 31 | default: 32 | self = .unknown 33 | } 34 | } 35 | 36 | public var errorDescription: String? { 37 | "SBServicesClient.Error.\(self)" 38 | } 39 | } 40 | 41 | public enum InterfaceOrientation { 42 | case unknown 43 | case portrait 44 | case portraitUpsideDown 45 | case landscapeLeft 46 | case landscapeRight 47 | 48 | init(raw: sbservices_interface_orientation_t) { 49 | switch raw { 50 | case SBSERVICES_INTERFACE_ORIENTATION_PORTRAIT: 51 | self = .portrait 52 | case SBSERVICES_INTERFACE_ORIENTATION_PORTRAIT_UPSIDE_DOWN: 53 | self = .portraitUpsideDown 54 | case SBSERVICES_INTERFACE_ORIENTATION_LANDSCAPE_LEFT: 55 | self = .landscapeLeft 56 | case SBSERVICES_INTERFACE_ORIENTATION_LANDSCAPE_RIGHT: 57 | self = .landscapeRight 58 | default: 59 | self = .unknown 60 | } 61 | } 62 | } 63 | 64 | public typealias Raw = sbservices_client_t 65 | public static let serviceIdentifier = SBSERVICES_SERVICE_NAME 66 | public static nonisolated(unsafe) let newFunc: NewFunc = sbservices_client_new 67 | public static nonisolated(unsafe) let startFunc: StartFunc = sbservices_client_start_service 68 | public nonisolated(unsafe) let raw: sbservices_client_t 69 | public required init(raw: sbservices_client_t) { self.raw = raw } 70 | deinit { sbservices_client_free(raw) } 71 | 72 | private let encoder = PlistNodeEncoder() 73 | private let decoder = PlistNodeDecoder() 74 | 75 | // png 76 | public func icon(forApp bundleID: String) throws -> Data { 77 | try CAPI.getData { buf, rawSize in 78 | var size: UInt64 { 79 | get { .init(rawSize) } 80 | set { rawSize = .init(newValue) } 81 | } 82 | return sbservices_get_icon_pngdata(raw, bundleID, &buf, &size) 83 | } 84 | } 85 | 86 | // png 87 | public func wallpaper() throws -> Data { 88 | try CAPI.getData { buf, rawSize in 89 | var size: UInt64 { 90 | get { .init(rawSize) } 91 | set { rawSize = .init(newValue) } 92 | } 93 | return sbservices_get_home_screen_wallpaper_pngdata(raw, &buf, &size) 94 | } 95 | } 96 | 97 | public func interfaceOrientation() throws -> InterfaceOrientation { 98 | var orientation = SBSERVICES_INTERFACE_ORIENTATION_UNKNOWN 99 | try CAPI.check(sbservices_get_interface_orientation(raw, &orientation)) 100 | return .init(raw: orientation) 101 | } 102 | 103 | public func iconState(ofType type: T.Type, format: String?) throws -> T { 104 | try decoder.decode(type) { 105 | try CAPI.check(sbservices_get_icon_state(raw, &$0, format)) 106 | } 107 | } 108 | 109 | public func setIconState(_ state: T) throws { 110 | try encoder.withEncoded(state) { 111 | try CAPI.check(sbservices_set_icon_state(raw, $0)) 112 | } 113 | } 114 | 115 | } 116 | -------------------------------------------------------------------------------- /Sources/SwiftyMobileDevice/Device/Lockdown/LockdownClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LockdownClient.swift 3 | // SwiftyMobileDevice 4 | // 5 | // Created by Kabir Oberai on 13/11/19. 6 | // Copyright © 2019 Kabir Oberai. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Superutils 11 | import libimobiledevice 12 | 13 | public final class LockdownClient: Sendable { 14 | 15 | public enum Error: CAPIError, LocalizedError { 16 | case unknown 17 | case `internal` 18 | case invalidArg 19 | case invalidConf 20 | case plistError 21 | case pairingFailed 22 | case sslError 23 | case dictError 24 | case receiveTimeout 25 | case muxError 26 | case noRunningSession 27 | case invalidResponse 28 | case missingKey 29 | case missingValue 30 | case getProhibited 31 | case setProhibited 32 | case removeProhibited 33 | case immutableValue 34 | case passwordProtected 35 | case userDeniedPairing 36 | case pairingDialogResponsePending 37 | case missingHostID 38 | case invalidHostID 39 | case sessionActive 40 | case sessionInactive 41 | case missingSessionID 42 | case invalidSessionID 43 | case missingService 44 | case invalidService 45 | case serviceLimit 46 | case missingPairRecord 47 | case savePairRecordFailed 48 | case invalidPairRecord 49 | case invalidActivationRecord 50 | case missingActivationRecord 51 | case serviceProhibited 52 | case escrowLocked 53 | case pairingProhibitedOverThisConnection 54 | case fmipProtected 55 | case mcProtected 56 | case mcChallengeRequired 57 | 58 | // swiftlint:disable:next cyclomatic_complexity 59 | public init?(_ raw: lockdownd_error_t) { 60 | switch raw { 61 | case LOCKDOWN_E_SUCCESS: 62 | return nil 63 | case LOCKDOWN_E_INVALID_ARG: 64 | self = .invalidArg 65 | case LOCKDOWN_E_INVALID_CONF: 66 | self = .invalidConf 67 | case LOCKDOWN_E_PLIST_ERROR: 68 | self = .plistError 69 | case LOCKDOWN_E_PAIRING_FAILED: 70 | self = .pairingFailed 71 | case LOCKDOWN_E_SSL_ERROR: 72 | self = .sslError 73 | case LOCKDOWN_E_DICT_ERROR: 74 | self = .dictError 75 | case LOCKDOWN_E_RECEIVE_TIMEOUT: 76 | self = .receiveTimeout 77 | case LOCKDOWN_E_MUX_ERROR: 78 | self = .muxError 79 | case LOCKDOWN_E_NO_RUNNING_SESSION: 80 | self = .noRunningSession 81 | case LOCKDOWN_E_INVALID_RESPONSE: 82 | self = .invalidResponse 83 | case LOCKDOWN_E_MISSING_KEY: 84 | self = .missingKey 85 | case LOCKDOWN_E_MISSING_VALUE: 86 | self = .missingValue 87 | case LOCKDOWN_E_GET_PROHIBITED: 88 | self = .getProhibited 89 | case LOCKDOWN_E_SET_PROHIBITED: 90 | self = .setProhibited 91 | case LOCKDOWN_E_REMOVE_PROHIBITED: 92 | self = .removeProhibited 93 | case LOCKDOWN_E_IMMUTABLE_VALUE: 94 | self = .immutableValue 95 | case LOCKDOWN_E_PASSWORD_PROTECTED: 96 | self = .passwordProtected 97 | case LOCKDOWN_E_USER_DENIED_PAIRING: 98 | self = .userDeniedPairing 99 | case LOCKDOWN_E_PAIRING_DIALOG_RESPONSE_PENDING: 100 | self = .pairingDialogResponsePending 101 | case LOCKDOWN_E_MISSING_HOST_ID: 102 | self = .missingHostID 103 | case LOCKDOWN_E_INVALID_HOST_ID: 104 | self = .invalidHostID 105 | case LOCKDOWN_E_SESSION_ACTIVE: 106 | self = .sessionActive 107 | case LOCKDOWN_E_SESSION_INACTIVE: 108 | self = .sessionInactive 109 | case LOCKDOWN_E_MISSING_SESSION_ID: 110 | self = .missingSessionID 111 | case LOCKDOWN_E_INVALID_SESSION_ID: 112 | self = .invalidSessionID 113 | case LOCKDOWN_E_MISSING_SERVICE: 114 | self = .missingService 115 | case LOCKDOWN_E_INVALID_SERVICE: 116 | self = .invalidService 117 | case LOCKDOWN_E_SERVICE_LIMIT: 118 | self = .serviceLimit 119 | case LOCKDOWN_E_MISSING_PAIR_RECORD: 120 | self = .missingPairRecord 121 | case LOCKDOWN_E_SAVE_PAIR_RECORD_FAILED: 122 | self = .savePairRecordFailed 123 | case LOCKDOWN_E_INVALID_PAIR_RECORD: 124 | self = .invalidPairRecord 125 | case LOCKDOWN_E_INVALID_ACTIVATION_RECORD: 126 | self = .invalidActivationRecord 127 | case LOCKDOWN_E_MISSING_ACTIVATION_RECORD: 128 | self = .missingActivationRecord 129 | case LOCKDOWN_E_SERVICE_PROHIBITED: 130 | self = .serviceProhibited 131 | case LOCKDOWN_E_ESCROW_LOCKED: 132 | self = .escrowLocked 133 | case LOCKDOWN_E_PAIRING_PROHIBITED_OVER_THIS_CONNECTION: 134 | self = .pairingProhibitedOverThisConnection 135 | case LOCKDOWN_E_FMIP_PROTECTED: 136 | self = .fmipProtected 137 | case LOCKDOWN_E_MC_PROTECTED: 138 | self = .mcProtected 139 | case LOCKDOWN_E_MC_CHALLENGE_REQUIRED: 140 | self = .mcChallengeRequired 141 | default: 142 | self = .unknown 143 | } 144 | } 145 | 146 | public var errorDescription: String? { 147 | "LockdownClient.Error.\(self)" 148 | } 149 | } 150 | 151 | public final class ServiceDescriptor { 152 | public let port: UInt16 153 | public let isSSLEnabled: Bool 154 | public let identifier: String 155 | 156 | public let raw: lockdownd_service_descriptor_t 157 | public init(raw: lockdownd_service_descriptor_t) { 158 | self.raw = raw 159 | self.port = raw.pointee.port 160 | self.isSSLEnabled = raw.pointee.ssl_enabled != 0 161 | self.identifier = String(cString: raw.pointee.identifier) 162 | } 163 | 164 | public convenience init( 165 | client: LockdownClient, type: T.Type = T.self, sendEscrowBag: Bool = false 166 | ) throws { 167 | let raw = try T.startService { id in 168 | var descriptor: lockdownd_service_descriptor_t? 169 | try CAPI.check( 170 | (sendEscrowBag ? lockdownd_start_service_with_escrow_bag : lockdownd_start_service)( 171 | client.raw, id, &descriptor 172 | ) 173 | ) 174 | return try descriptor.orThrow(Error.internal) 175 | } 176 | self.init(raw: raw) 177 | } 178 | 179 | deinit { lockdownd_service_descriptor_free(raw) } 180 | } 181 | 182 | public struct SessionID: RawRepresentable { 183 | public let rawValue: String 184 | public init(rawValue: String) { self.rawValue = rawValue } 185 | } 186 | 187 | public struct PairRecord { 188 | // these certificates must be PEM-encoded 189 | public let deviceCertificate: Data 190 | public let hostCertificate: Data 191 | public let rootCertificate: Data 192 | 193 | public let hostID: String 194 | public let systemBUID: String 195 | 196 | public init?( 197 | deviceCertificate: Data, 198 | hostCertificate: Data, 199 | rootCertificate: Data, 200 | hostID: String, 201 | systemBUID: String 202 | ) { 203 | func nullTerminating(_ data: Data) -> Data { 204 | if data.last == 0 { return data } 205 | var copy = data 206 | copy.append(0) 207 | return copy 208 | } 209 | 210 | self.deviceCertificate = nullTerminating(deviceCertificate) 211 | self.hostCertificate = nullTerminating(hostCertificate) 212 | self.rootCertificate = nullTerminating(rootCertificate) 213 | self.hostID = hostID 214 | self.systemBUID = systemBUID 215 | } 216 | 217 | // the members of `raw` are copied 218 | public init(raw: lockdownd_pair_record) { 219 | // data from null terminated bytes 220 | func data(from bytes: UnsafePointer) -> Data { 221 | Data(bytes: bytes, count: strlen(bytes) + 1) // include NUL byte 222 | } 223 | deviceCertificate = data(from: raw.device_certificate) 224 | hostCertificate = data(from: raw.host_certificate) 225 | rootCertificate = data(from: raw.root_certificate) 226 | hostID = String(cString: raw.host_id) 227 | systemBUID = String(cString: raw.system_buid) 228 | } 229 | 230 | public init(raw: lockdownd_pair_record_t) { 231 | self.init(raw: raw.pointee) 232 | } 233 | 234 | public func withRaw(_ block: (lockdownd_pair_record_t) throws -> Result) rethrows -> Result { 235 | try deviceCertificate.withUnsafeBytes { buf in 236 | let boundDeviceCertificate = UnsafeMutableBufferPointer(mutating: buf.bindMemory(to: Int8.self)) 237 | return try hostCertificate.withUnsafeBytes { buf in 238 | let boundHostCertificate = UnsafeMutableBufferPointer(mutating: buf.bindMemory(to: Int8.self)) 239 | return try rootCertificate.withUnsafeBytes { buf in 240 | let boundRootCertificate = UnsafeMutableBufferPointer(mutating: buf.bindMemory(to: Int8.self)) 241 | return try hostID.withCString { cHostID in 242 | let mutableHostID = UnsafeMutablePointer(mutating: cHostID) 243 | return try systemBUID.withCString { cSystemBUID in 244 | let mutableSystemBUID = UnsafeMutablePointer(mutating: cSystemBUID) 245 | // the base addresses are known to be non-nil because the data values are 246 | // null terminated so they have to have at least one byte (the NUL character) 247 | var record = lockdownd_pair_record( 248 | device_certificate: boundDeviceCertificate.baseAddress!, 249 | host_certificate: boundHostCertificate.baseAddress!, 250 | root_certificate: boundRootCertificate.baseAddress!, 251 | host_id: mutableHostID, 252 | system_buid: mutableSystemBUID 253 | ) 254 | return try withUnsafeMutablePointer(to: &record) { ptr in 255 | try block(ptr) 256 | } 257 | } 258 | } 259 | } 260 | } 261 | } 262 | } 263 | } 264 | 265 | private let encoder = PlistNodeEncoder() 266 | private let decoder = PlistNodeDecoder() 267 | 268 | public nonisolated(unsafe) let raw: lockdownd_client_t 269 | public init(raw: lockdownd_client_t) { self.raw = raw } 270 | public init(device: Device, label: String?, performHandshake: Bool) throws { 271 | var client: lockdownd_client_t? 272 | try CAPI.check( 273 | (performHandshake ? lockdownd_client_new_with_handshake : lockdownd_client_new)( 274 | device.raw, &client, label 275 | ) 276 | ) 277 | guard let raw = client else { throw Error.internal } 278 | self.raw = raw 279 | } 280 | deinit { lockdownd_client_free(raw) } 281 | 282 | public func setLabel(_ label: String?) { 283 | lockdownd_client_set_label(raw, label) 284 | } 285 | 286 | public func deviceUDID() throws -> String { 287 | try CAPI.getString { lockdownd_get_device_udid(raw, &$0) } 288 | } 289 | 290 | public func deviceName() throws -> String { 291 | try CAPI.getString { lockdownd_get_device_name(raw, &$0) } 292 | } 293 | 294 | public func queryType() throws -> String { 295 | try CAPI.getString { lockdownd_query_type(raw, &$0) } 296 | } 297 | 298 | public func syncDataClasses() throws -> [String] { 299 | try CAPI.getArrayWithCount( 300 | parseFn: { lockdownd_get_sync_data_classes(raw, &$0, &$1) }, 301 | freeFn: { lockdownd_data_classes_free($0) } 302 | ) ?? [] 303 | } 304 | 305 | public func value(ofType type: T.Type, forDomain domain: String?, key: String?) throws -> T { 306 | try decoder.decode(type) { 307 | try CAPI.check(lockdownd_get_value(raw, domain, key, &$0)) 308 | } 309 | } 310 | 311 | public func setValue(_ value: T?, forDomain domain: String, key: String) throws { 312 | if let value = value { 313 | // this follows move semantics, so we aren't responsible for freeing the created plist_t 314 | try CAPI.check(lockdownd_set_value(raw, domain, key, encoder.encode(value))) 315 | } else { 316 | try CAPI.check(lockdownd_remove_value(raw, domain, key)) 317 | } 318 | } 319 | 320 | public func startService( 321 | ofType type: T.Type = T.self, 322 | sendEscrowBag: Bool = false 323 | ) throws -> ServiceDescriptor { 324 | try ServiceDescriptor(client: self, type: T.self, sendEscrowBag: sendEscrowBag) 325 | } 326 | 327 | public func startSession( 328 | withHostID hostID: String 329 | ) throws -> (sessionID: SessionID, isSSLEnabled: Bool) { 330 | var isSSLEnabled: Int32 = 0 331 | let sessionID = try CAPI.getString { 332 | lockdownd_start_session(raw, hostID, &$0, &isSSLEnabled) 333 | } 334 | return (.init(rawValue: sessionID), isSSLEnabled != 0) 335 | } 336 | 337 | public func stopSession(_ sessionID: SessionID) throws { 338 | try CAPI.check(lockdownd_stop_session(raw, sessionID.rawValue)) 339 | } 340 | 341 | public func send(_ value: T) throws { 342 | try CAPI.check(encoder.withEncoded(value) { 343 | lockdownd_send(raw, $0) 344 | }) 345 | } 346 | 347 | public func receive(_ type: T.Type) throws -> T { 348 | try decoder.decode(type) { 349 | try CAPI.check(lockdownd_receive(raw, &$0)) 350 | } 351 | } 352 | 353 | private func withRawRecord( 354 | _ record: PairRecord?, 355 | _ block: (lockdownd_pair_record_t?) throws -> Result 356 | ) rethrows -> Result { 357 | if let record = record { 358 | return try record.withRaw { try block($0) } 359 | } else { 360 | return try block(nil) 361 | } 362 | } 363 | 364 | public func pair( 365 | withRecord record: PairRecord? = nil, 366 | options: [String: Encodable] = ["ExtendedPairingErrors": true] 367 | ) throws { 368 | try CAPI.check(encoder.withEncoded(options.mapValues(AnyEncodable.init)) { encodedOptions in 369 | withRawRecord(record) { lockdownd_pair_with_options(raw, $0, encodedOptions, nil) } 370 | }) 371 | } 372 | 373 | public func pair( 374 | returnType: D.Type, 375 | record: PairRecord? = nil, 376 | options: [String: Encodable] = ["ExtendedPairingErrors": true] 377 | ) throws -> D { 378 | try decoder.decode(returnType) { buf in 379 | try CAPI.check(encoder.withEncoded(options.mapValues(AnyEncodable.init)) { encodedOptions in 380 | withRawRecord(record) { lockdownd_pair_with_options(raw, $0, encodedOptions, &buf) } 381 | }) 382 | } 383 | } 384 | 385 | private func validateRecord(_ record: PairRecord?) throws { 386 | try CAPI.check(withRawRecord(record) { lockdownd_validate_pair(raw, $0) }) 387 | } 388 | 389 | public func validate(record: PairRecord) throws { 390 | try validateRecord(record) 391 | } 392 | 393 | public func validateInternalRecord() throws { 394 | try validateRecord(nil) 395 | } 396 | 397 | public func unpair(withRecord record: PairRecord? = nil) throws { 398 | try CAPI.check(withRawRecord(record) { lockdownd_unpair(raw, $0) }) 399 | } 400 | 401 | public func activate(withActivationRecord record: [String: Encodable]) throws { 402 | try CAPI.check(encoder.withEncoded(record.mapValues(AnyEncodable.init)) { 403 | lockdownd_activate(raw, $0) 404 | }) 405 | } 406 | 407 | public func deactivate() throws { 408 | try CAPI.check(lockdownd_deactivate(raw)) 409 | } 410 | 411 | public func enterRecovery() throws { 412 | try CAPI.check(lockdownd_enter_recovery(raw)) 413 | } 414 | 415 | public func sendGoodbye() throws { 416 | try CAPI.check(lockdownd_goodbye(raw)) 417 | } 418 | 419 | } 420 | -------------------------------------------------------------------------------- /Sources/SwiftyMobileDevice/Device/Lockdown/LockdownService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LockdownService.swift 3 | // SwiftyMobileDevice 4 | // 5 | // Created by Kabir Oberai on 13/11/19. 6 | // Copyright © 2019 Kabir Oberai. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import libimobiledevice 11 | 12 | public protocol LockdownService: Sendable { 13 | 14 | associatedtype Error: CAPIError 15 | 16 | associatedtype Raw 17 | var raw: Raw { get } 18 | 19 | static var serviceIdentifier: String { get } 20 | 21 | static var newFunc: NewFunc { get } 22 | static var startFunc: StartFunc { get } 23 | 24 | // adds a customization point for service descriptors 25 | // (used by DebugserverClient) 26 | static func startService( 27 | withFunc fn: (String) throws -> lockdownd_service_descriptor_t 28 | ) throws -> lockdownd_service_descriptor_t 29 | 30 | init(raw: Raw) 31 | 32 | } 33 | 34 | public extension LockdownService { 35 | 36 | typealias NewFunc = ( 37 | idevice_t, lockdownd_service_descriptor_t, UnsafeMutablePointer? 38 | ) -> Error.Raw 39 | 40 | typealias StartFunc = ( 41 | idevice_t, UnsafeMutablePointer?, UnsafePointer? 42 | ) -> Error.Raw 43 | 44 | init(device: Device, service: LockdownClient.ServiceDescriptor) throws { 45 | var client: Raw? 46 | try CAPI.check(Self.newFunc(device.raw, service.raw, &client)) 47 | guard let raw = client else { throw CAPIGenericError.unexpectedNil } 48 | self.init(raw: raw) 49 | } 50 | 51 | init(device: Device, label: String?) throws { 52 | var client: Raw? 53 | try CAPI.check(Self.startFunc(device.raw, &client, label)) 54 | guard let raw = client else { throw CAPIGenericError.unexpectedNil } 55 | self.init(raw: raw) 56 | } 57 | 58 | static func startService( 59 | withFunc fn: (String) throws -> lockdownd_service_descriptor_t 60 | ) throws -> lockdownd_service_descriptor_t { 61 | try fn(serviceIdentifier) 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /Sources/SwiftyMobileDevice/USBMux/USBMux+Connection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // USBMux+Connection.swift 3 | // SwiftyMobileDevice 4 | // 5 | // Created by Kabir Oberai on 11/04/20. 6 | // Copyright © 2020 Kabir Oberai. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import usbmuxd 11 | 12 | extension USBMux { 13 | 14 | public final class Connection: StreamingConnection { 15 | 16 | public typealias Error = USBMux.Error 17 | public typealias Raw = Int32 18 | 19 | public let raw: Int32 20 | public init(raw: Int32) { 21 | self.raw = raw 22 | } 23 | public init(handle: Device.Handle, port: UInt16) throws { 24 | let ret = usbmuxd_connect(handle.raw, port) 25 | try CAPI.check(ret) 26 | self.raw = ret 27 | } 28 | deinit { usbmuxd_disconnect(raw) } 29 | 30 | public nonisolated(unsafe) let sendFunc: SendFunc = usbmuxd_send 31 | public nonisolated(unsafe) let receiveFunc: ReceiveFunc = usbmuxd_recv 32 | public nonisolated(unsafe) let receiveTimeoutFunc: ReceiveTimeoutFunc = usbmuxd_recv_timeout 33 | 34 | } 35 | 36 | public static func connect(withHandle handle: Device.Handle, port: UInt16) throws -> Connection { 37 | try Connection(handle: handle, port: port) 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /Sources/SwiftyMobileDevice/USBMux/USBMux+Lookup.swift: -------------------------------------------------------------------------------- 1 | // 2 | // USBMux+Lookup.swift 3 | // SwiftyMobileDevice 4 | // 5 | // Created by Kabir Oberai on 11/04/20. 6 | // Copyright © 2020 Kabir Oberai. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import usbmuxd 11 | 12 | extension USBMux { 13 | 14 | public enum LookupMode: Sendable { 15 | case only(ConnectionType) 16 | case both(preferring: ConnectionType) 17 | 18 | public var raw: usbmux_lookup_options { 19 | switch self { 20 | case .only(.usb): return DEVICE_LOOKUP_USBMUX 21 | case .only(.network): return DEVICE_LOOKUP_NETWORK 22 | case .both(preferring: .usb): 23 | return .init( 24 | DEVICE_LOOKUP_USBMUX.rawValue | 25 | DEVICE_LOOKUP_NETWORK.rawValue 26 | ) 27 | case .both(preferring: .network): 28 | return .init( 29 | DEVICE_LOOKUP_USBMUX.rawValue | 30 | DEVICE_LOOKUP_NETWORK.rawValue | 31 | DEVICE_LOOKUP_PREFER_NETWORK.rawValue 32 | ) 33 | } 34 | } 35 | } 36 | 37 | public struct Event: Sendable { 38 | public enum Kind: Sendable { 39 | case added 40 | case removed 41 | case paired 42 | 43 | var raw: usbmuxd_event_type { 44 | switch self { 45 | case .added: return UE_DEVICE_ADD 46 | case .removed: return UE_DEVICE_REMOVE 47 | case .paired: return UE_DEVICE_PAIRED 48 | } 49 | } 50 | 51 | init?(raw: usbmuxd_event_type) { 52 | switch raw { 53 | case UE_DEVICE_ADD: self = .added 54 | case UE_DEVICE_REMOVE: self = .removed 55 | case UE_DEVICE_PAIRED: self = .paired 56 | default: return nil 57 | } 58 | } 59 | } 60 | 61 | public let kind: Kind 62 | public let device: Device 63 | 64 | init?(raw: usbmuxd_event_t) { 65 | guard let kind = Kind(raw: .init(.init(raw.event))), 66 | let device = Device(raw: raw.device) 67 | else { return nil } 68 | self.kind = kind 69 | self.device = device 70 | } 71 | } 72 | 73 | public static func subscribe() throws -> AsyncStream { 74 | final class SubscriptionUserData: Sendable { 75 | let callback: @Sendable (Event) -> Void 76 | init(callback: @escaping @Sendable (Event) -> Void) { 77 | self.callback = callback 78 | } 79 | } 80 | 81 | let (stream, continuation) = AsyncStream.makeStream() 82 | 83 | let userData = SubscriptionUserData { continuation.yield($0) } 84 | 85 | nonisolated(unsafe) var context: usbmuxd_subscription_context_t? 86 | try CAPI.check(usbmuxd_events_subscribe( 87 | &context, 88 | { rawEvent, opaqueUserData in 89 | let userData = Unmanaged.fromOpaque(opaqueUserData!).takeUnretainedValue() 90 | guard let event = Event(raw: rawEvent!.pointee) else { return } 91 | userData.callback(event) 92 | }, 93 | Unmanaged.passUnretained(userData).toOpaque() 94 | )) 95 | 96 | continuation.onTermination = { _ in 97 | try? CAPI.check(usbmuxd_events_unsubscribe(context!)) 98 | _ = userData // retain until unsubscribe 99 | } 100 | 101 | return stream 102 | } 103 | 104 | public static func allDevices() throws -> [Device] { 105 | var devices: UnsafeMutablePointer? 106 | defer { devices.map { free($0) } } 107 | 108 | let count = usbmuxd_get_device_list(&devices) 109 | switch count { 110 | case 0: 111 | return [] 112 | case -ECONNREFUSED, -ENOENT: 113 | // these error codes may crop up if no devices are connected 114 | return [] 115 | case ..<0: 116 | throw Error.errno(-.init(count)) 117 | default: 118 | return UnsafeBufferPointer(start: devices!, count: .init(count)).compactMap(Device.init) 119 | } 120 | } 121 | 122 | public static func device(withUDID udid: String, mode: LookupMode? = nil) throws -> Device? { 123 | var device = usbmuxd_device_info_t() 124 | let result: Int32 125 | if let mode = mode { 126 | result = usbmuxd_get_device(udid, &device, mode.raw) 127 | } else { 128 | result = usbmuxd_get_device_by_udid(udid, &device) 129 | } 130 | switch result { 131 | case 1: // found 132 | return Device(raw: device) 133 | case 0: // not found 134 | return nil 135 | case let error: // error (-ve) 136 | throw Error.errno(-.init(error)) 137 | } 138 | } 139 | 140 | } 141 | -------------------------------------------------------------------------------- /Sources/SwiftyMobileDevice/USBMux/USBMux.swift: -------------------------------------------------------------------------------- 1 | // 2 | // USBMux.swift 3 | // SwiftyMobileDevice 4 | // 5 | // Created by Kabir Oberai on 11/04/20. 6 | // Copyright © 2020 Kabir Oberai. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import usbmuxd 11 | 12 | extension ConnectionType { 13 | public var usbmuxRaw: usbmux_connection_type { 14 | switch self { 15 | case .usb: return CONNECTION_TYPE_USB 16 | case .network: return CONNECTION_TYPE_NETWORK 17 | } 18 | } 19 | 20 | public init?(usbmuxRaw: usbmux_connection_type) { 21 | switch usbmuxRaw { 22 | case CONNECTION_TYPE_USB: self = .usb 23 | case CONNECTION_TYPE_NETWORK: self = .network 24 | default: return nil 25 | } 26 | } 27 | } 28 | 29 | public enum USBMux { 30 | 31 | public enum Error: LocalizedError, CAPIError { 32 | case errno(Int32) 33 | 34 | public var errorDescription: String? { 35 | switch self { 36 | case .errno(let raw): 37 | // We need to make a copy here. strerror uses a static buffer for 38 | // unknown errors, which may be overwritten by future calls. 39 | return String(cString: strerror(raw)!) 40 | } 41 | } 42 | 43 | public init?(_ raw: Int32) { 44 | guard raw != 0 else { return nil } 45 | self = .errno(raw) 46 | } 47 | } 48 | 49 | public struct Device: Sendable { 50 | public struct Handle: Sendable { 51 | public let raw: UInt32 52 | public init(raw: UInt32) { 53 | self.raw = raw 54 | } 55 | } 56 | 57 | public let handle: Handle 58 | public let productID: UInt32 59 | public let udid: String 60 | public let connectionType: ConnectionType 61 | public let connectionData: Data 62 | 63 | init?(raw: usbmuxd_device_info_t) { 64 | self.handle = Handle(raw: raw.handle) 65 | 66 | self.productID = raw.product_id 67 | 68 | var udidRaw = raw.udid 69 | let udidSize = MemoryLayout.size(ofValue: udidRaw) 70 | self.udid = withUnsafePointer(to: &udidRaw) { 71 | UnsafeRawPointer($0) 72 | // Tuple is also bound to type of elements (if homogeneous) so this is legal 73 | .assumingMemoryBound(to: Int8.self) 74 | // UInt8 has same size and stride as Int8 so this is okay too 75 | .withMemoryRebound(to: UInt8.self, capacity: udidSize, String.init(cString:)) 76 | } 77 | 78 | guard let connectionType = ConnectionType(usbmuxRaw: raw.conn_type) else { return nil } 79 | self.connectionType = connectionType 80 | 81 | var dataRaw = raw.conn_data 82 | self.connectionData = Data(bytes: &dataRaw.0, count: MemoryLayout.size(ofValue: dataRaw)) 83 | } 84 | } 85 | 86 | public static func buid() throws -> String { 87 | try CAPI.getString { usbmuxd_read_buid(&$0) } 88 | } 89 | 90 | public static func pairRecord(forUDID udid: String) throws -> Data { 91 | try CAPI.getData { usbmuxd_read_pair_record(udid, &$0, &$1) } 92 | } 93 | 94 | public static func savePairRecord(_ record: Data, forUDID udid: String, handle: Device.Handle? = nil) throws { 95 | try record.withUnsafeBytes { buf in 96 | let bound = buf.bindMemory(to: Int8.self) 97 | try CAPI.check( 98 | usbmuxd_save_pair_record_with_device_id(udid, handle?.raw ?? 0, bound.baseAddress, .init(bound.count)) 99 | ) 100 | } 101 | } 102 | 103 | public static func deletePairRecord(forUDID udid: String) throws { 104 | try CAPI.check(usbmuxd_delete_pair_record(udid)) 105 | } 106 | 107 | public static func setUseInotify(_ useInotify: Bool) { 108 | libusbmuxd_set_use_inotify(useInotify ? 1 : 0) 109 | } 110 | 111 | public static func setDebugLevel(_ debugLevel: Int) { 112 | libusbmuxd_set_debug_level(.init(debugLevel)) 113 | } 114 | 115 | } 116 | -------------------------------------------------------------------------------- /Sources/SwiftyMobileDevice/Utilities/CAPI.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CAPI.swift 3 | // SwiftyMobileDevice 4 | // 5 | // Created by Kabir Oberai on 13/11/19. 6 | // Copyright © 2019 Kabir Oberai. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Superutils 11 | 12 | public enum CAPIGenericError: Error { 13 | case unexpectedNil 14 | } 15 | 16 | public protocol CAPIError: Swift.Error { 17 | associatedtype Raw 18 | init?(_ raw: Raw) 19 | } 20 | 21 | public enum CAPINoError: CAPIError { 22 | public init?(_ raw: Void) { nil } 23 | } 24 | 25 | public enum CAPI {} 26 | 27 | extension CAPI { 28 | 29 | static func check(_ error: Error.Raw) throws { 30 | try Error(error).map { throw $0 } 31 | } 32 | 33 | static func getArrayWithCount( 34 | parseFn: (inout UnsafeMutablePointer?>?, inout Int32) -> Error.Raw, 35 | freeFn: (UnsafeMutablePointer?>) -> Void 36 | ) throws -> [String]? { 37 | var rawValues: UnsafeMutablePointer?>? 38 | var count: Int32 = 0 39 | try check(parseFn(&rawValues, &count)) 40 | guard let values = rawValues else { return nil } 41 | 42 | defer { freeFn(values) } 43 | 44 | return UnsafeBufferPointer(start: values, count: Int(count)) 45 | .compactMap { $0 } 46 | .map { String(cString: $0) } 47 | } 48 | 49 | static func getNullTerminatedArray( 50 | parseFn: (inout UnsafeMutablePointer?>?) -> Error.Raw, 51 | freeFn: (UnsafeMutablePointer?>) -> Void 52 | ) throws -> [String] { 53 | var rawValues: UnsafeMutablePointer?>? 54 | try Self.check(parseFn(&rawValues)) 55 | guard let values = rawValues else { throw CAPIGenericError.unexpectedNil } 56 | 57 | defer { freeFn(values) } 58 | 59 | return sequence(state: values) { ( 60 | // swiftlint:disable:next closure_parameter_position 61 | currValue: inout UnsafeMutablePointer?> 62 | ) -> UnsafeMutablePointer? in 63 | defer { currValue += 1 } 64 | return currValue.pointee 65 | }.map { String(cString: $0) } 66 | } 67 | 68 | static func getDictionary( 69 | parseFn: (inout UnsafeMutablePointer?>?) -> Error.Raw, 70 | freeFn: (UnsafeMutablePointer?>) -> Void 71 | ) throws -> [String: String] { 72 | let strings = try getNullTerminatedArray(parseFn: parseFn, freeFn: freeFn) 73 | // Note: build performance 74 | let pairs = stride(from: 0, to: strings.count, by: 2).map { (idx: Int) -> (String, String) in 75 | (strings[idx], strings[idx + 1]) 76 | } 77 | return Dictionary(uniqueKeysWithValues: pairs) 78 | } 79 | 80 | static func getData( 81 | maxLength: Int, 82 | parseFn: (UnsafeMutablePointer, inout UInt32) -> Error.Raw 83 | ) throws -> Data { 84 | let bytes = UnsafeMutablePointer.allocate(capacity: maxLength) 85 | var received: UInt32 = 0 86 | try check(parseFn(bytes, &received)) 87 | return Data(bytesNoCopy: bytes, count: .init(received), deallocator: .deallocate) 88 | } 89 | 90 | // if `isOwner`, we're responsible for freeing the data 91 | static func getData( 92 | isOwner: Bool = true, 93 | parseFn: (inout UnsafeMutablePointer?, inout UInt32) -> Error.Raw 94 | ) throws -> Data { 95 | var optionalBuf: UnsafeMutablePointer? 96 | var length: UInt32 = 0 97 | try check(parseFn(&optionalBuf, &length)) 98 | guard length != 0 else { return Data() } 99 | let buf = try optionalBuf.orThrow(CAPIGenericError.unexpectedNil) 100 | let count = Int(length) 101 | if isOwner { 102 | return Data(bytesNoCopy: buf, count: count, deallocator: .free) 103 | } else { 104 | return Data(bytes: buf, count: count) 105 | } 106 | } 107 | 108 | static func getString( 109 | isOwner: Bool = true, 110 | parseFn: (inout UnsafeMutablePointer?) -> Error.Raw 111 | ) throws -> String { 112 | var bytes: UnsafeMutablePointer? 113 | try check(parseFn(&bytes)) 114 | return try bytes.flatMap { 115 | if isOwner { 116 | return String(bytesNoCopy: $0, length: strlen($0), encoding: .utf8, freeWhenDone: true) 117 | } else { 118 | return String(cString: $0) 119 | } 120 | }.orThrow(CAPIGenericError.unexpectedNil) 121 | } 122 | 123 | } 124 | -------------------------------------------------------------------------------- /Sources/SwiftyMobileDevice/Utilities/PlistNodeCoders.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlistNodeCoders.swift 3 | // SwiftyMobileDevice 4 | // 5 | // Created by Kabir Oberai on 13/11/19. 6 | // Copyright © 2019 Kabir Oberai. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Superutils 11 | import plist 12 | 13 | final class PlistNodeEncoder: Sendable { 14 | enum Error: Swift.Error { 15 | case failedToEncode 16 | } 17 | 18 | private let encoder: PropertyListEncoder 19 | var userInfo: [CodingUserInfoKey: Any] { 20 | get { encoder.userInfo } 21 | set { encoder.userInfo = newValue } 22 | } 23 | 24 | init() { 25 | encoder = PropertyListEncoder() 26 | // binary is more efficient than xml 27 | encoder.outputFormat = .binary 28 | } 29 | 30 | // the lifetime of the plist is scoped to `block`. The callee manages this. 31 | @discardableResult func withEncoded(_ value: V, block: (plist_t) throws -> T) throws -> T { 32 | // we need to embed value in an array/dict, because those are the only permitted objects 33 | // at the top level 34 | let data = try encoder.encode([value]) 35 | 36 | var optionalArray: plist_t? 37 | data.withUnsafeBytes { (buf: UnsafeRawBufferPointer) in 38 | let bound = buf.bindMemory(to: Int8.self) 39 | plist_from_bin(bound.baseAddress, UInt32(bound.count), &optionalArray) 40 | } 41 | 42 | guard let array = optionalArray, 43 | plist_array_get_size(array) == 1, 44 | let item = plist_array_get_item(array, 0) 45 | else { throw Error.failedToEncode } 46 | defer { plist_free(array) } 47 | 48 | return try block(item) 49 | } 50 | 51 | // the caller is responsible for freeing the returned plist with `plist_free` 52 | func encode(_ value: V) throws -> plist_t { 53 | try withEncoded(value) { plist_copy($0) } 54 | } 55 | 56 | } 57 | 58 | final class PlistNodeDecoder: Sendable { 59 | enum Error: Swift.Error { 60 | case failedToDecode 61 | case acceptorFailed 62 | } 63 | 64 | private let decoder: PropertyListDecoder 65 | var userInfo: [CodingUserInfoKey: Any] { 66 | get { decoder.userInfo } 67 | set { decoder.userInfo = newValue } 68 | } 69 | 70 | init() { 71 | decoder = PropertyListDecoder() 72 | } 73 | 74 | // takes ownership 75 | func decode(_ type: T.Type, moving node: plist_t) throws -> T { 76 | let array = plist_new_array() 77 | defer { plist_free(array) } 78 | // move ownership of `node` to `array` 79 | plist_array_append_item(array, node) 80 | 81 | let data = try CAPI.getData { plist_to_bin(array, &$0, &$1) } 82 | 83 | let decoded = try decoder.decode([T].self, from: data) 84 | guard decoded.count == 1 else { throw Error.failedToDecode } 85 | return decoded[0] 86 | } 87 | 88 | // takes ownership 89 | func decode(_ type: T.Type, acceptor: (inout plist_t?) throws -> Void) throws -> T { 90 | var raw: plist_t? 91 | try acceptor(&raw) 92 | let node = try raw.orThrow(Error.acceptorFailed) 93 | return try decode(type, moving: node) 94 | } 95 | 96 | // doesn't take ownership 97 | func decode(_ type: T.Type, from node: plist_t) throws -> T { 98 | try decode(type, moving: plist_copy(node)) 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /Sources/SwiftyMobileDevice/Utilities/StreamingConnection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StreamingConnection.swift 3 | // SwiftyMobileDevice 4 | // 5 | // Created by Kabir Oberai on 28/04/20. 6 | // Copyright © 2020 Kabir Oberai. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Represents a connection using a streaming protocol such as TCP 12 | public protocol StreamingConnection: Sendable { 13 | associatedtype Error: CAPIError 14 | 15 | associatedtype Raw 16 | var raw: Raw { get } 17 | 18 | var sendFunc: SendFunc { get } 19 | var receiveFunc: ReceiveFunc { get } 20 | var receiveTimeoutFunc: ReceiveTimeoutFunc { get } 21 | } 22 | 23 | extension StreamingConnection { 24 | 25 | public typealias SendFunc = ( 26 | Raw, UnsafePointer?, UInt32, UnsafeMutablePointer 27 | ) -> Error.Raw 28 | 29 | public typealias ReceiveFunc = ( 30 | Raw, UnsafeMutablePointer, UInt32, UnsafeMutablePointer 31 | ) -> Error.Raw 32 | 33 | public typealias ReceiveTimeoutFunc = ( 34 | Raw, UnsafeMutablePointer, UInt32, UnsafeMutablePointer, UInt32 35 | ) -> Error.Raw 36 | 37 | /// - Returns: the number of bytes sent 38 | public func send(_ data: Data) throws -> Int { 39 | var sent: UInt32 = 0 40 | try data.withUnsafeBytes { (bytes: UnsafeRawBufferPointer) in 41 | let bound = bytes.bindMemory(to: Int8.self) 42 | try CAPI.check( 43 | sendFunc(raw, bound.baseAddress, .init(bound.count), &sent) 44 | ) 45 | } 46 | return .init(sent) 47 | } 48 | 49 | private func receiveRaw( 50 | data: UnsafeMutablePointer, received: inout UInt32, 51 | maxLength: Int, timeout: TimeInterval? 52 | ) -> Error.Raw { 53 | timeout.map { 54 | receiveTimeoutFunc( 55 | raw, data, .init(maxLength), &received, .init($0 * 1000) 56 | ) 57 | } ?? receiveFunc(raw, data, .init(maxLength), &received) 58 | } 59 | 60 | public func receive(maxLength: Int, timeout: TimeInterval? = nil) throws -> Data { 61 | try CAPI.getData(maxLength: maxLength) { 62 | receiveRaw(data: $0, received: &$1, maxLength: maxLength, timeout: timeout) 63 | } 64 | } 65 | 66 | /// receive all data until the end of the stream 67 | public func receiveAll(bufferSize: Int = 64 << 10, timeout: TimeInterval? = nil) throws -> Data { 68 | let buf = UnsafeMutablePointer.allocate(capacity: bufferSize) 69 | defer { buf.deallocate() } 70 | var received: UInt32 = 0 71 | var data = Data() 72 | 73 | repeat { 74 | try CAPI.check( 75 | receiveRaw(data: buf, received: &received, maxLength: bufferSize, timeout: timeout) 76 | ) 77 | buf.withMemoryRebound(to: UInt8.self, capacity: bufferSize) { ptr in 78 | data.append(ptr, count: .init(received)) 79 | } 80 | } while received == bufferSize 81 | 82 | return data 83 | } 84 | 85 | } 86 | --------------------------------------------------------------------------------