[AltServer] Installs Developer disk image before installing AltStore

Allows AltServer to programmatically initiate a debug session with AltStore, which can be used to start a background refresh or enable JIT on demand.

[AltServer] Renames ALTDevice variable name
This commit is contained in:
Riley Testut
2021-05-20 13:11:54 -07:00
parent 279a290b60
commit 8857ccbf86
5 changed files with 402 additions and 40 deletions

View File

@@ -0,0 +1,286 @@
//
// DeveloperDiskManager.swift
// AltServer
//
// Created by Riley Testut on 2/19/21.
// Copyright © 2021 Riley Testut. All rights reserved.
//
import Foundation
import AltSign
enum DeveloperDiskError: LocalizedError
{
case unknownDownloadURL
case unsupportedOperatingSystem
case downloadedDiskNotFound
var errorDescription: String? {
switch self
{
case .unknownDownloadURL: return NSLocalizedString("The URL to download the Developer disk image could not be determined.", comment: "")
case .unsupportedOperatingSystem: return NSLocalizedString("The device's operating system does not support installing Developer disk images.", comment: "")
case .downloadedDiskNotFound: return NSLocalizedString("DeveloperDiskImage.dmg and its signature could not be found in the downloaded archive.", comment: "")
}
}
}
private extension URL
{
#if STAGING
static let developerDiskDownloadURLs = URL(string: "https://f000.backblazeb2.com/file/altstore-staging/altserver/developerdisks.json")!
#else
static let developerDiskDownloadURLs = URL(string: "https://cdn.altstore.io/file/altstore/altserver/developerdisks.json")!
#endif
}
private extension DeveloperDiskManager
{
struct FetchURLsResponse: Decodable
{
struct Disks: Decodable
{
var iOS: [String: DeveloperDiskURL]?
var tvOS: [String: DeveloperDiskURL]?
}
var version: Int
var disks: Disks
}
enum DeveloperDiskURL: Decodable
{
case archive(URL)
case separate(diskURL: URL, signatureURL: URL)
private enum CodingKeys: CodingKey
{
case archive
case disk
case signature
}
init(from decoder: Decoder) throws
{
let container = try decoder.container(keyedBy: CodingKeys.self)
if container.contains(.archive)
{
let archiveURL = try container.decode(URL.self, forKey: .archive)
self = .archive(archiveURL)
}
else
{
let diskURL = try container.decode(URL.self, forKey: .disk)
let signatureURL = try container.decode(URL.self, forKey: .signature)
self = .separate(diskURL: diskURL, signatureURL: signatureURL)
}
}
}
}
class DeveloperDiskManager
{
func downloadDeveloperDisk(for device: ALTDevice, completionHandler: @escaping (Result<(URL, URL), Error>) -> Void)
{
let osVersion = "\(device.osVersion.majorVersion).\(device.osVersion.minorVersion)"
let osKeyPath: KeyPath<FetchURLsResponse.Disks, [String: DeveloperDiskURL]?>
switch device.type
{
case .iphone, .ipad: osKeyPath = \FetchURLsResponse.Disks.iOS
case .appletv: osKeyPath = \FetchURLsResponse.Disks.tvOS
default: return completionHandler(.failure(DeveloperDiskError.unsupportedOperatingSystem))
}
do
{
let developerDiskDirectoryURL = FileManager.default.developerDisksDirectory.appendingPathComponent(osVersion)
try FileManager.default.createDirectory(at: developerDiskDirectoryURL, withIntermediateDirectories: true, attributes: nil)
let developerDiskURL = developerDiskDirectoryURL.appendingPathComponent("DeveloperDiskImage.dmg")
let developerDiskSignatureURL = developerDiskDirectoryURL.appendingPathComponent("DeveloperDiskImage.dmg.signature")
guard !FileManager.default.fileExists(atPath: developerDiskURL.path) || !FileManager.default.fileExists(atPath: developerDiskSignatureURL.path) else {
return completionHandler(.success((developerDiskURL, developerDiskSignatureURL)))
}
func finish(_ result: Result<(URL, URL), Error>)
{
do
{
let (diskFileURL, signatureFileURL) = try result.get()
let developerDiskDirectoryURL = FileManager.default.developerDisksDirectory.appendingPathComponent(osVersion)
try FileManager.default.createDirectory(at: developerDiskDirectoryURL, withIntermediateDirectories: true, attributes: nil)
try FileManager.default.copyItem(at: diskFileURL, to: developerDiskURL)
try FileManager.default.copyItem(at: signatureFileURL, to: developerDiskSignatureURL)
completionHandler(.success((developerDiskURL, developerDiskSignatureURL)))
}
catch
{
completionHandler(.failure(error))
}
}
self.fetchDeveloperDiskURLs { (result) in
do
{
let developerDiskURLs = try result.get()
guard let diskURL = developerDiskURLs[keyPath: osKeyPath]?[osVersion] else { throw DeveloperDiskError.unknownDownloadURL }
switch diskURL
{
case .archive(let archiveURL): self.downloadDiskArchive(from: archiveURL, completionHandler: finish(_:))
case .separate(let diskURL, let signatureURL): self.downloadDisk(from: diskURL, signatureURL: signatureURL, completionHandler: finish(_:))
}
}
catch
{
finish(.failure(error))
}
}
}
catch
{
completionHandler(.failure(error))
}
}
}
private extension DeveloperDiskManager
{
func fetchDeveloperDiskURLs(completionHandler: @escaping (Result<FetchURLsResponse.Disks, Error>) -> Void)
{
let dataTask = URLSession.shared.dataTask(with: .developerDiskDownloadURLs) { (data, response, error) in
do
{
guard let data = data else { throw error! }
let response = try JSONDecoder().decode(FetchURLsResponse.self, from: data)
completionHandler(.success(response.disks))
}
catch
{
completionHandler(.failure(error))
}
}
dataTask.resume()
}
func downloadDiskArchive(from url: URL, completionHandler: @escaping (Result<(URL, URL), Error>) -> Void)
{
let downloadTask = URLSession.shared.downloadTask(with: url) { (fileURL, response, error) in
do
{
guard let fileURL = fileURL else { throw error! }
defer { try? FileManager.default.removeItem(at: fileURL) }
let temporaryDirectory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
try FileManager.default.createDirectory(at: temporaryDirectory, withIntermediateDirectories: true, attributes: nil)
defer { try? FileManager.default.removeItem(at: temporaryDirectory) }
try FileManager.default.unzipArchive(at: fileURL, toDirectory: temporaryDirectory)
guard let enumerator = FileManager.default.enumerator(at: temporaryDirectory, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles, .skipsPackageDescendants]) else {
throw CocoaError(.fileNoSuchFile, userInfo: [NSURLErrorKey: temporaryDirectory])
}
var tempDiskFileURL: URL?
var tempSignatureFileURL: URL?
for case let fileURL as URL in enumerator
{
switch fileURL.pathExtension.lowercased()
{
case "dmg": tempDiskFileURL = fileURL
case "signature": tempSignatureFileURL = fileURL
default: break
}
}
guard let diskFileURL = tempDiskFileURL, let signatureFileURL = tempSignatureFileURL else { throw DeveloperDiskError.downloadedDiskNotFound }
completionHandler(.success((diskFileURL, signatureFileURL)))
}
catch
{
completionHandler(.failure(error))
}
}
downloadTask.resume()
}
func downloadDisk(from diskURL: URL, signatureURL: URL, completionHandler: @escaping (Result<(URL, URL), Error>) -> Void)
{
let temporaryDirectory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
do { try FileManager.default.createDirectory(at: temporaryDirectory, withIntermediateDirectories: true, attributes: nil) }
catch { return completionHandler(.failure(error)) }
var diskFileURL: URL?
var signatureFileURL: URL?
var downloadError: Error?
let dispatchGroup = DispatchGroup()
dispatchGroup.enter()
dispatchGroup.enter()
let diskDownloadTask = URLSession.shared.downloadTask(with: diskURL) { (fileURL, response, error) in
do
{
guard let fileURL = fileURL else { throw error! }
let destinationURL = temporaryDirectory.appendingPathComponent("DeveloperDiskImage.dmg")
try FileManager.default.copyItem(at: fileURL, to: destinationURL)
diskFileURL = destinationURL
}
catch
{
downloadError = error
}
dispatchGroup.leave()
}
let signatureDownloadTask = URLSession.shared.downloadTask(with: signatureURL) { (fileURL, response, error) in
do
{
guard let fileURL = fileURL else { throw error! }
let destinationURL = temporaryDirectory.appendingPathComponent("DeveloperDiskImage.dmg.signature")
try FileManager.default.copyItem(at: fileURL, to: destinationURL)
signatureFileURL = destinationURL
}
catch
{
downloadError = error
}
dispatchGroup.leave()
}
diskDownloadTask.resume()
signatureDownloadTask.resume()
dispatchGroup.notify(queue: .global(qos: .userInitiated)) {
defer {
try? FileManager.default.removeItem(at: temporaryDirectory)
}
guard let diskFileURL = diskFileURL, let signatureFileURL = signatureFileURL else {
return completionHandler(.failure(downloadError ?? DeveloperDiskError.downloadedDiskNotFound))
}
completionHandler(.success((diskFileURL, signatureFileURL)))
}
}
}

View File

@@ -12,6 +12,8 @@ import ObjectiveC
private let appGroupsLock = NSLock()
private let developerDiskManager = DeveloperDiskManager()
enum InstallError: LocalizedError
{
case cancelled
@@ -32,7 +34,7 @@ enum InstallError: LocalizedError
extension ALTDeviceManager
{
func installApplication(at url: URL, to device: ALTDevice, appleID: String, password: String, completion: @escaping (Result<ALTApplication, Error>) -> Void)
func installApplication(at url: URL, to altDevice: ALTDevice, appleID: String, password: String, completion: @escaping (Result<ALTApplication, Error>) -> Void)
{
let destinationDirectoryURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
@@ -60,10 +62,11 @@ extension ALTDeviceManager
{
let team = try result.get()
self.register(device, team: team, session: session) { (result) in
self.register(altDevice, team: team, session: session) { (result) in
do
{
let device = try result.get()
device.osVersion = altDevice.osVersion
self.fetchCertificate(for: team, session: session) { (result) in
do
@@ -75,55 +78,67 @@ extension ALTDeviceManager
// Show alert before downloading remote .ipa.
self.showInstallationAlert(appName: NSLocalizedString("AltStore", comment: ""), deviceName: device.name)
}
self.downloadApp(from: url) { (result) in
do
self.prepare(device) { (result) in
switch result
{
let fileURL = try result.get()
try FileManager.default.createDirectory(at: destinationDirectoryURL, withIntermediateDirectories: true, attributes: nil)
let appBundleURL = try FileManager.default.unzipAppBundle(at: fileURL, toDirectory: destinationDirectoryURL)
guard let application = ALTApplication(fileURL: appBundleURL) else { throw ALTError(.invalidApp) }
if url.isFileURL
{
// Show alert after "downloading" local .ipa.
self.showInstallationAlert(appName: application.name, deviceName: device.name)
}
// Refresh anisette data to prevent session timeouts.
AnisetteDataManager.shared.requestAnisetteData { (result) in
case .failure(let error):
print("Failed to install DeveloperDiskImage.dmg to \(device).", error)
fallthrough // Continue installing app even if we couldn't install Developer disk image.
case .success:
self.downloadApp(from: url) { (result) in
do
{
let anisetteData = try result.get()
session.anisetteData = anisetteData
let fileURL = try result.get()
self.prepareAllProvisioningProfiles(for: application, device: device, team: team, session: session) { (result) in
try FileManager.default.createDirectory(at: destinationDirectoryURL, withIntermediateDirectories: true, attributes: nil)
let appBundleURL = try FileManager.default.unzipAppBundle(at: fileURL, toDirectory: destinationDirectoryURL)
guard let application = ALTApplication(fileURL: appBundleURL) else { throw ALTError(.invalidApp) }
if url.isFileURL
{
// Show alert after "downloading" local .ipa.
self.showInstallationAlert(appName: application.name, deviceName: device.name)
}
appName = application.name
// Refresh anisette data to prevent session timeouts.
AnisetteDataManager.shared.requestAnisetteData { (result) in
do
{
let profiles = try result.get()
let anisetteData = try result.get()
session.anisetteData = anisetteData
self.install(application, to: device, team: team, certificate: certificate, profiles: profiles) { (result) in
finish(result.map { application }, title: "Failed to Install AltStore")
self.prepareAllProvisioningProfiles(for: application, device: device, team: team, session: session) { (result) in
do
{
let profiles = try result.get()
self.install(application, to: device, team: team, certificate: certificate, profiles: profiles) { (result) in
finish(result.map { application }, title: "Failed to Install AltStore")
}
}
catch
{
finish(.failure(error), title: "Failed to Fetch Provisioning Profiles")
}
}
}
catch
{
finish(.failure(error), title: "Failed to Fetch Provisioning Profiles")
finish(.failure(error), title: "Failed to Refresh Anisette Data")
}
}
}
catch
{
finish(.failure(error), title: "Failed to Refresh Anisette Data")
finish(.failure(error), title: "Failed to Download AltStore")
}
}
}
catch
{
finish(.failure(error), title: "Failed to Download AltStore")
}
}
}
catch
@@ -158,6 +173,35 @@ extension ALTDeviceManager
}
}
extension ALTDeviceManager
{
func prepare(_ device: ALTDevice, completionHandler: @escaping (Result<Void, Error>) -> Void)
{
ALTDeviceManager.shared.isDeveloperDiskImageMounted(for: device) { (isMounted, error) in
switch (isMounted, error)
{
case (_, let error?): return completionHandler(.failure(error))
case (true, _): return completionHandler(.success(()))
case (false, _):
developerDiskManager.downloadDeveloperDisk(for: device) { (result) in
switch result
{
case .failure(let error): completionHandler(.failure(error))
case .success((let diskFileURL, let signatureFileURL)):
ALTDeviceManager.shared.installDeveloperDiskImage(at: diskFileURL, signatureURL: signatureFileURL, to: device) { (success, error) in
switch Result(success, error)
{
case .failure(let error): completionHandler(.failure(error))
case .success: completionHandler(.success(()))
}
}
}
}
}
}
}
}
private extension ALTDeviceManager
{
func downloadApp(from url: URL, completionHandler: @escaping (Result<URL, Error>) -> Void)
@@ -270,13 +314,8 @@ private extension ALTDeviceManager
{
let certificates = try Result(certificates, error).get()
let applicationSupportDirectoryURL = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
let altserverDirectoryURL = applicationSupportDirectoryURL.appendingPathComponent("com.rileytestut.AltServer")
let certificatesDirectoryURL = altserverDirectoryURL.appendingPathComponent("Certificates")
try FileManager.default.createDirectory(at: certificatesDirectoryURL, withIntermediateDirectories: true, attributes: nil)
let certificateFileURL = certificatesDirectoryURL.appendingPathComponent(team.identifier + ".p12")
let certificateFileURL = FileManager.default.certificatesDirectory.appendingPathComponent(team.identifier + ".p12")
try FileManager.default.createDirectory(at: FileManager.default.certificatesDirectory, withIntermediateDirectories: true, attributes: nil)
var isCancelled = false

View File

@@ -0,0 +1,29 @@
//
// FileManager+URLs.swift
// AltServer
//
// Created by Riley Testut on 2/23/21.
// Copyright © 2021 Riley Testut. All rights reserved.
//
import Foundation
extension FileManager
{
var altserverDirectory: URL {
let applicationSupportDirectoryURL = self.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
let altserverDirectoryURL = applicationSupportDirectoryURL.appendingPathComponent("com.rileytestut.AltServer")
return altserverDirectoryURL
}
var certificatesDirectory: URL {
let certificatesDirectoryURL = self.altserverDirectory.appendingPathComponent("Certificates")
return certificatesDirectoryURL
}
var developerDisksDirectory: URL {
let developerDisksDirectoryURL = self.altserverDirectory.appendingPathComponent("DeveloperDiskImages")
return developerDisksDirectoryURL
}
}

View File

@@ -119,6 +119,7 @@
BF45884B2298D55000BD7491 /* thread.h in Headers */ = {isa = PBXBuildFile; fileRef = BF4588492298D55000BD7491 /* thread.h */; };
BF4588882298DD3F00BD7491 /* libxml2.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = BF4588872298DD3F00BD7491 /* libxml2.tbd */; };
BF4B78FE24B3D1DB008AB4AC /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF4B78FD24B3D1DB008AB4AC /* SceneDelegate.swift */; };
BF541C0B25E5A5FA00CD46B2 /* FileManager+URLs.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF541C0A25E5A5FA00CD46B2 /* FileManager+URLs.swift */; };
BF56D2AC23DF8E170006506D /* FetchAppIDsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF56D2AB23DF8E170006506D /* FetchAppIDsOperation.swift */; };
BF56D2AF23DF9E310006506D /* AppIDsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF56D2AE23DF9E310006506D /* AppIDsViewController.swift */; };
BF58047E246A28F7008AE704 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF58047D246A28F7008AE704 /* AppDelegate.swift */; };
@@ -211,6 +212,7 @@
BF9ABA4D22DD16DE008935CF /* PillButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9ABA4C22DD16DE008935CF /* PillButton.swift */; };
BFA8172923C56042001B5953 /* ServerConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFA8172823C56042001B5953 /* ServerConnection.swift */; };
BFA8172B23C5633D001B5953 /* FetchAnisetteDataOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFA8172A23C5633D001B5953 /* FetchAnisetteDataOperation.swift */; };
BFAD67A325E0854500D4C4D1 /* DeveloperDiskManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFAD67A225E0854500D4C4D1 /* DeveloperDiskManager.swift */; };
BFAECC522501B0A400528F27 /* CodableServerError.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD44605241188C300EAB90A /* CodableServerError.swift */; };
BFAECC532501B0A400528F27 /* ServerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1E3128229F474900370A3C /* ServerProtocol.swift */; };
BFAECC542501B0A400528F27 /* NSError+ALTServerError.m in Sources */ = {isa = PBXBuildFile; fileRef = BF1E314922A060F400370A3C /* NSError+ALTServerError.m */; };
@@ -562,6 +564,7 @@
BF4588492298D55000BD7491 /* thread.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = thread.h; path = Dependencies/libusbmuxd/common/thread.h; sourceTree = SOURCE_ROOT; };
BF4588872298DD3F00BD7491 /* libxml2.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libxml2.tbd; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk/usr/lib/libxml2.tbd; sourceTree = DEVELOPER_DIR; };
BF4B78FD24B3D1DB008AB4AC /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
BF541C0A25E5A5FA00CD46B2 /* FileManager+URLs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+URLs.swift"; sourceTree = "<group>"; };
BF56D2AB23DF8E170006506D /* FetchAppIDsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchAppIDsOperation.swift; sourceTree = "<group>"; };
BF56D2AE23DF9E310006506D /* AppIDsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIDsViewController.swift; sourceTree = "<group>"; };
BF58047B246A28F7008AE704 /* AltBackup.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AltBackup.app; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -676,6 +679,7 @@
BF9ABA4C22DD16DE008935CF /* PillButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillButton.swift; sourceTree = "<group>"; };
BFA8172823C56042001B5953 /* ServerConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerConnection.swift; sourceTree = "<group>"; };
BFA8172A23C5633D001B5953 /* FetchAnisetteDataOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchAnisetteDataOperation.swift; sourceTree = "<group>"; };
BFAD67A225E0854500D4C4D1 /* DeveloperDiskManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperDiskManager.swift; sourceTree = "<group>"; };
BFB1169C22932DB100BB457C /* apps.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = apps.json; sourceTree = "<group>"; };
BFB364592325985F00CD0EB1 /* FindServerOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FindServerOperation.swift; sourceTree = "<group>"; };
BFB39B5B252BC10E00D1BE50 /* Managed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Managed.swift; sourceTree = "<group>"; };
@@ -889,6 +893,7 @@
isa = PBXGroup;
children = (
BF0241A922F29CCD00129732 /* UserDefaults+AltServer.swift */,
BF541C0A25E5A5FA00CD46B2 /* FileManager+URLs.swift */,
);
path = Extensions;
sourceTree = "<group>";
@@ -965,6 +970,7 @@
BF458695229872EA00BD7491 /* Main.storyboard */,
BFE48974238007CE003239E0 /* AnisetteDataManager.swift */,
BFC712BA2512B9CF00AB5EBE /* PluginManager.swift */,
BFAD67A225E0854500D4C4D1 /* DeveloperDiskManager.swift */,
BF703195229F36FF006E110F /* Devices */,
BFD52BDC22A0A659000B7ED1 /* Connections */,
BF055B4A233B528B0086DEA9 /* Extensions */,
@@ -2269,6 +2275,7 @@
BFF767C82489A74E0097E58C /* WirelessConnectionHandler.swift in Sources */,
BFECAC8024FD950B0077C41F /* ConnectionManager.swift in Sources */,
BFECAC8324FD950B0077C41F /* NetworkConnection.swift in Sources */,
BF541C0B25E5A5FA00CD46B2 /* FileManager+URLs.swift in Sources */,
BFECAC8724FD950B0077C41F /* Bundle+AltStore.swift in Sources */,
BF3F786422CAA41E008FBD20 /* ALTDeviceManager+Installation.swift in Sources */,
BF18BFFD2485A1E400DD5981 /* WiredConnectionHandler.swift in Sources */,
@@ -2287,6 +2294,7 @@
BF4586C52298CDB800BD7491 /* ALTDeviceManager.mm in Sources */,
BF0241AA22F29CCD00129732 /* UserDefaults+AltServer.swift in Sources */,
BFECAC9424FD98BA0077C41F /* NSError+ALTServerError.m in Sources */,
BFAD67A325E0854500D4C4D1 /* DeveloperDiskManager.swift in Sources */,
BFECAC9324FD98BA0077C41F /* CFNotificationName+AltStore.m in Sources */,
BFE48975238007CE003239E0 /* AnisetteDataManager.swift in Sources */,
BFE972E3260A8B2700D0BDAC /* NSError+libimobiledevice.mm in Sources */,