Files
xtool-mirror/Sources/Supersign/DeveloperServices/App IDs/DeveloperServicesAddAppOperation.swift
2024-12-01 22:38:51 -05:00

193 lines
7.9 KiB
Swift

//
// DeveloperServicesAddAppOperation.swift
// Supersign
//
// Created by Kabir Oberai on 14/10/19.
// Copyright © 2019 Kabir Oberai. All rights reserved.
//
import Foundation
public struct DeveloperServicesAddAppOperation: DeveloperServicesOperation {
public enum Error: LocalizedError {
case invalidApp(URL)
case teamNotFound(DeveloperServicesTeam.ID)
public var errorDescription: String? {
switch self {
case .invalidApp(let url):
return url.path.withCString {
String.localizedStringWithFormat(
NSLocalizedString(
"add_app_operation.error.invalid_app", value: "Invalid app: %s", comment: ""
), $0
)
}
case .teamNotFound(let id):
return id.rawValue.withCString {
String.localizedStringWithFormat(
NSLocalizedString(
"add_app_operation.error.team_not_found",
value: "A team with the ID '%s' could not be found. Please select another team.",
comment: ""
), $0
)
}
}
}
}
public let context: SigningContext
public let root: URL
public init(context: SigningContext, root: URL) {
self.context = context
self.root = root
}
/// Registers the app with the given entitlements
private func upsertApp(
bundleID: String,
entitlements: Entitlements,
team: DeveloperServicesTeam,
appIDs: [String: DeveloperServicesAppID]
) async throws -> DeveloperServicesAppID {
if let appID = appIDs[bundleID] {
let request = DeveloperServicesUpdateAppIDRequest(
platform: self.context.platform,
teamID: self.context.teamID,
appIDID: appID.id,
entitlements: entitlements,
additionalFeatures: [],
isFree: team.isFree
)
return try await context.client.send(request)
} else {
let newBundleID = ProvisioningIdentifiers.identifier(fromSanitized: bundleID, context: self.context)
let name = ProvisioningIdentifiers.appName(fromSanitized: bundleID)
let request = DeveloperServicesAddAppIDRequest(
platform: self.context.platform,
teamID: self.context.teamID,
bundleID: newBundleID,
appName: name,
entitlements: entitlements,
additionalFeatures: [],
isFree: team.isFree
)
return try await context.client.send(request)
}
}
/// Registers the app and creates a profile. Returns the resultant entitlements as well as
/// the profile (note that the profile does not necessarily include all of the entitlements
/// that the app has).
private func addApp(
_ app: URL,
team: DeveloperServicesTeam,
appIDs: [String: DeveloperServicesAppID]
) async throws -> ProvisioningInfo {
let infoURL = app.appendingPathComponent("Info.plist")
guard let data = try? Data(contentsOf: infoURL),
let dict = try? PropertyListSerialization.propertyList(from: data, format: nil) as? [String: Any],
let bundleID = dict["CFBundleIdentifier"] as? String,
let executable = dict["CFBundleExecutable"] as? String else {
throw Error.invalidApp(app)
}
let executableURL = app.appendingPathComponent(executable)
var entitlements: Entitlements
// if the executable doesn't already have entitlements, that's
// okay. We don't have to throw an error.
if let entitlementsData = try? await context.signerImpl.analyze(executable: executableURL),
let decodedEntitlements = try? PropertyListDecoder().decode(Entitlements.self, from: entitlementsData) {
entitlements = decodedEntitlements
if team.isFree {
// re-assign rather than using updateEntitlements since the latter will
// retain unrecognized entitlements
let filtered = try entitlements.entitlements().filter { type(of: $0).isFree }
entitlements = try Entitlements(entitlements: filtered)
}
} else {
entitlements = try Entitlements(entitlements: [])
}
let appID = try await upsertApp(bundleID: bundleID, entitlements: entitlements, team: team, appIDs: appIDs)
try entitlements.update(teamID: self.context.teamID, bundleID: appID.bundleID)
// set get-task-allow to YES, required for dev certs
try entitlements.updateEntitlements { ents in
if let getTaskAllow = ents.firstIndex(where: { $0 is GetTaskAllowEntitlement }) {
ents[getTaskAllow] = GetTaskAllowEntitlement(rawValue: true)
} else {
ents.append(GetTaskAllowEntitlement(rawValue: true))
}
}
if var entitlementsArray = try? entitlements.entitlements(),
let groupsIdx = entitlementsArray.firstIndex(where: { $0 is AppGroupEntitlement }),
let groupsEntitlement = entitlementsArray[groupsIdx] as? AppGroupEntitlement {
let groups = groupsEntitlement.rawValue
let newGroups = try await DeveloperServicesAssignAppGroupsOperation(
context: self.context,
groupIDs: groups,
appID: appID
).perform()
entitlementsArray[groupsIdx] = AppGroupEntitlement(rawValue: newGroups)
try entitlements.setEntitlements(entitlementsArray)
}
let profile = try await DeveloperServicesFetchProfileOperation(context: self.context, appID: appID).perform()
guard let mobileprovision = profile.mobileprovision
else { throw Mobileprovision.Error.invalidProfile }
return ProvisioningInfo(
newBundleID: appID.bundleID, entitlements: entitlements, mobileprovision: mobileprovision
)
}
private func getTeam() async throws -> DeveloperServicesTeam {
let request = DeveloperServicesListTeamsRequest()
let teams = try await context.client.send(request)
guard let team = teams.first(where: { $0.id == self.context.teamID })
else { throw Error.teamNotFound(self.context.teamID) }
return team
}
// keyed by sanitized bundle ID
private func getCurrentAppIDs() async throws -> [String: DeveloperServicesAppID] {
let request = DeveloperServicesListAppIDsRequest(platform: context.platform, teamID: context.teamID)
let appIDs = try await context.client.send(request)
let keyedIDs = appIDs.map { (ProvisioningIdentifiers.sanitize(identifier: $0.bundleID), $0) }
return Dictionary(keyedIDs, uniquingKeysWith: { $1 })
}
/// Registers the app + its extensions, returning the profile and entitlements of each
public func perform() async throws -> [URL: ProvisioningInfo] {
var apps: [URL] = [root]
let plugins = root.appendingPathComponent("PlugIns")
if plugins.dirExists {
apps += plugins.implicitContents.filter { $0.pathExtension.lowercased() == "appex" }
}
async let teamTask = getTeam()
async let appIDsTask = getCurrentAppIDs()
let (team, appIDs) = try await (teamTask, appIDsTask)
return try await withThrowingTaskGroup(
of: (URL, ProvisioningInfo).self,
returning: [URL: ProvisioningInfo].self
) { group in
for app in apps {
group.addTask {
let info = try await addApp(app, team: team, appIDs: appIDs)
return (app, info)
}
}
return try await group.reduce(into: [:]) { $0[$1.0] = $1.1 }
}
}
}