mirror of
https://github.com/xtool-org/xtool.git
synced 2026-02-04 11:53:30 +01:00
Use swift-deps, fix things
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"originHash" : "5b3298fb2e08290a3e47a886cedfefce16ee07f46d1f0ccdc86fcdd488f8c170",
|
||||
"originHash" : "19c490b9f4eaa20eede93f53a3b352116957402a682ed84a47bfaf23c195de03",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "aexml",
|
||||
@@ -28,6 +28,15 @@
|
||||
"version" : "5.5.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "combine-schedulers",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/pointfreeco/combine-schedulers",
|
||||
"state" : {
|
||||
"revision" : "9fa31f4403da54855f1e2aeaeff478f4f0e40b13",
|
||||
"version" : "1.0.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "jsonutilities",
|
||||
"kind" : "remoteSourceControl",
|
||||
@@ -127,6 +136,15 @@
|
||||
"version" : "1.6.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-clocks",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/pointfreeco/swift-clocks",
|
||||
"state" : {
|
||||
"revision" : "b9b24b69e2adda099a1fa381cda1eeec272d5b53",
|
||||
"version" : "1.0.5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-collections",
|
||||
"kind" : "remoteSourceControl",
|
||||
@@ -154,6 +172,15 @@
|
||||
"version" : "3.9.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-dependencies",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/pointfreeco/swift-dependencies",
|
||||
"state" : {
|
||||
"revision" : "5526c8a27675dc7b18d6fa643abfb64bcb200b77",
|
||||
"version" : "1.6.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-http-types",
|
||||
"kind" : "remoteSourceControl",
|
||||
@@ -262,6 +289,15 @@
|
||||
"version" : "1.0.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-syntax",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/swiftlang/swift-syntax",
|
||||
"state" : {
|
||||
"revision" : "0687f71944021d616d34d922343dcef086855920",
|
||||
"version" : "600.0.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-system",
|
||||
"kind" : "remoteSourceControl",
|
||||
@@ -334,6 +370,15 @@
|
||||
"version" : "8.16.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "xctest-dynamic-overlay",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/pointfreeco/xctest-dynamic-overlay",
|
||||
"state" : {
|
||||
"revision" : "a3f634d1a409c7979cabc0a71b3f26ffa9fc8af1",
|
||||
"version" : "1.4.3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "yams",
|
||||
"kind" : "remoteSourceControl",
|
||||
|
||||
@@ -46,6 +46,7 @@ let package = Package(
|
||||
.package(url: "https://github.com/vapor/websocket-kit.git", from: "2.15.0"),
|
||||
|
||||
.package(url: "https://github.com/pointfreeco/swift-concurrency-extras", from: "1.3.0"),
|
||||
.package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.6.2"),
|
||||
|
||||
.package(url: "https://github.com/attaswift/BigInt", from: "5.5.0"),
|
||||
.package(url: "https://github.com/mxcl/Version", from: "2.1.0"),
|
||||
@@ -73,6 +74,7 @@ let package = Package(
|
||||
"CSupersign",
|
||||
.byName(name: "CSupersette", condition: .when(platforms: [.linux])),
|
||||
.product(name: "ConcurrencyExtras", package: "swift-concurrency-extras"),
|
||||
.product(name: "Dependencies", package: "swift-dependencies"),
|
||||
.product(name: "SwiftyMobileDevice", package: "SwiftyMobileDevice"),
|
||||
.product(name: "Zupersign", package: "zsign"),
|
||||
.product(name: "SignerSupport", package: "SuperchargeCore"),
|
||||
|
||||
@@ -221,7 +221,8 @@ public struct DeveloperServicesAddAppOperation: DeveloperServicesOperation {
|
||||
|
||||
let mobileprovision = try await DeveloperServicesFetchProfileOperation(
|
||||
context: self.context,
|
||||
bundleID: newBundleID
|
||||
bundleID: newBundleID,
|
||||
signingInfo: signingInfo
|
||||
).perform()
|
||||
|
||||
return ProvisioningInfo(
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import Foundation
|
||||
import DeveloperAPI
|
||||
import Dependencies
|
||||
|
||||
public typealias DeveloperServicesCertificate = Components.Schemas.Certificate
|
||||
|
||||
@@ -31,6 +32,8 @@ public struct DeveloperServicesFetchCertificateOperation: DeveloperServicesOpera
|
||||
}
|
||||
}
|
||||
|
||||
@Dependency(\.signingInfoManager) var signingInfoManager
|
||||
|
||||
public let context: SigningContext
|
||||
public let confirmRevocation: @Sendable ([DeveloperServicesCertificate]) async -> Bool
|
||||
public init(
|
||||
@@ -84,14 +87,14 @@ public struct DeveloperServicesFetchCertificateOperation: DeveloperServicesOpera
|
||||
try await group.waitForAll()
|
||||
}
|
||||
let signingInfo = try await createCertificate()
|
||||
self.context.signingInfoManager[self.context.auth.identityID] = signingInfo
|
||||
signingInfoManager[self.context.auth.identityID] = signingInfo
|
||||
return signingInfo
|
||||
}
|
||||
|
||||
public func perform() async throws -> SigningInfo {
|
||||
let certificates = try await context.developerAPIClient.certificatesGetCollection().ok.body.json.data
|
||||
|
||||
guard let signingInfo = self.context.signingInfoManager[self.context.auth.identityID] else {
|
||||
guard let signingInfo = signingInfoManager[self.context.auth.identityID] else {
|
||||
return try await self.replaceCertificates(certificates, requireConfirmation: true)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,18 +3,19 @@ import DeveloperAPI
|
||||
import HTTPTypes
|
||||
import OpenAPIRuntime
|
||||
import OpenAPIURLSession
|
||||
import Dependencies
|
||||
|
||||
extension DeveloperAPIClient {
|
||||
public init(
|
||||
auth: DeveloperAPIAuthData,
|
||||
httpFactory: HTTPClientFactory = defaultHTTPClientFactory
|
||||
auth: DeveloperAPIAuthData
|
||||
) {
|
||||
@Dependency(\.httpClient) var httpClient
|
||||
self.init(
|
||||
serverURL: try! Servers.Server1.url(),
|
||||
configuration: .init(
|
||||
dateTranscoder: .iso8601WithFractionalSeconds
|
||||
),
|
||||
transport: httpFactory.makeClient().asOpenAPITransport,
|
||||
transport: httpClient.asOpenAPITransport,
|
||||
middlewares: [
|
||||
auth.middleware
|
||||
]
|
||||
@@ -48,24 +49,21 @@ public enum DeveloperAPIAuthData: Sendable {
|
||||
|
||||
public struct XcodeAuthData: Sendable {
|
||||
public var loginToken: DeveloperServicesLoginToken
|
||||
public var deviceInfo: DeviceInfo
|
||||
public var teamID: DeveloperServicesTeam.ID
|
||||
public var anisetteDataProvider: AnisetteDataProvider
|
||||
|
||||
public init(
|
||||
loginToken: DeveloperServicesLoginToken,
|
||||
deviceInfo: DeviceInfo,
|
||||
teamID: DeveloperServicesTeam.ID,
|
||||
anisetteDataProvider: AnisetteDataProvider
|
||||
teamID: DeveloperServicesTeam.ID
|
||||
) {
|
||||
self.loginToken = loginToken
|
||||
self.deviceInfo = deviceInfo
|
||||
self.teamID = teamID
|
||||
self.anisetteDataProvider = anisetteDataProvider
|
||||
}
|
||||
}
|
||||
|
||||
public struct DeveloperAPIXcodeAuthMiddleware: ClientMiddleware {
|
||||
@Dependency(\.deviceInfoProvider) private var deviceInfoProvider
|
||||
@Dependency(\.anisetteDataProvider) private var anisetteDataProvider
|
||||
|
||||
public var authData: XcodeAuthData
|
||||
|
||||
public init(authData: XcodeAuthData) {
|
||||
@@ -88,6 +86,8 @@ public struct DeveloperAPIXcodeAuthMiddleware: ClientMiddleware {
|
||||
) async throws -> (HTTPTypes.HTTPResponse, OpenAPIRuntime.HTTPBody?) {
|
||||
var request = request
|
||||
|
||||
let deviceInfo = try deviceInfoProvider.fetch()
|
||||
|
||||
// General
|
||||
request.headerFields[.acceptLanguage] = Locale.preferredLanguages.joined(separator: ", ")
|
||||
request.headerFields[.accept] = "application/vnd.api+json"
|
||||
@@ -102,7 +102,7 @@ public struct DeveloperAPIXcodeAuthMiddleware: ClientMiddleware {
|
||||
request.headerFields[.init(DeviceInfo.clientInfoKey)!] = """
|
||||
<VirtualMac2,1> <macOS;15.1.1;24B91> <com.apple.AuthKit/1 (com.apple.dt.Xcode/23505)>
|
||||
""" // deviceInfo.clientInfo.clientString
|
||||
request.headerFields[.init(DeviceInfo.deviceIDKey)!] = authData.deviceInfo.deviceID
|
||||
request.headerFields[.init(DeviceInfo.deviceIDKey)!] = deviceInfo.deviceID
|
||||
|
||||
// GrandSlam authentication
|
||||
request.headerFields[.init("X-Apple-App-Info")!] = AppTokenKey.xcode.rawValue
|
||||
@@ -110,7 +110,7 @@ public struct DeveloperAPIXcodeAuthMiddleware: ClientMiddleware {
|
||||
request.headerFields[.init("X-Apple-GS-Token")!] = authData.loginToken.token
|
||||
|
||||
// Anisette
|
||||
let anisetteData = try await authData.anisetteDataProvider.fetchAnisetteData()
|
||||
let anisetteData = try await anisetteDataProvider.fetchAnisetteData()
|
||||
for (key, value) in anisetteData.dictionary {
|
||||
request.headerFields[.init(key)!] = value
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Dependencies
|
||||
|
||||
public struct DeveloperServicesClient: Sendable {
|
||||
|
||||
@@ -53,28 +54,18 @@ public struct DeveloperServicesClient: Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
public let loginToken: DeveloperServicesLoginToken
|
||||
public let deviceInfo: DeviceInfo
|
||||
public let anisetteDataProvider: AnisetteDataProvider
|
||||
private let httpClient: HTTPClientProtocol
|
||||
@Dependency(\.deviceInfoProvider) var deviceInfoProvider
|
||||
@Dependency(\.anisetteDataProvider) var anisetteDataProvider
|
||||
@Dependency(\.httpClient) var httpClient
|
||||
|
||||
public init(
|
||||
loginToken: DeveloperServicesLoginToken,
|
||||
deviceInfo: DeviceInfo,
|
||||
anisetteProvider: AnisetteDataProvider,
|
||||
httpFactory: HTTPClientFactory = defaultHTTPClientFactory
|
||||
) {
|
||||
public let loginToken: DeveloperServicesLoginToken
|
||||
|
||||
public init(loginToken: DeveloperServicesLoginToken) {
|
||||
self.loginToken = loginToken
|
||||
self.deviceInfo = deviceInfo
|
||||
self.anisetteDataProvider = anisetteProvider
|
||||
self.httpClient = httpFactory.makeClient()
|
||||
}
|
||||
|
||||
public init(authData: XcodeAuthData) {
|
||||
self.loginToken = authData.loginToken
|
||||
self.deviceInfo = authData.deviceInfo
|
||||
self.anisetteDataProvider = authData.anisetteDataProvider
|
||||
self.httpClient = defaultHTTPClientFactory.makeClient()
|
||||
}
|
||||
|
||||
private func send<R: DeveloperServicesRequest>(
|
||||
@@ -85,6 +76,8 @@ public struct DeveloperServicesClient: Sendable {
|
||||
throw Error.malformedRequest
|
||||
}
|
||||
|
||||
let deviceInfo = try deviceInfoProvider.fetch()
|
||||
|
||||
var httpRequest = HTTPRequest(url: url, method: "POST")
|
||||
let acceptedLanguages = Locale.preferredLanguages.joined(separator: ", ")
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Dependencies
|
||||
|
||||
public struct DeveloperServicesLoginToken: Codable, Sendable {
|
||||
public let adsid: String
|
||||
@@ -26,23 +27,9 @@ public struct DeveloperServicesLoginManager: Sendable {
|
||||
case missingLoginToken
|
||||
}
|
||||
|
||||
public let deviceInfo: DeviceInfo
|
||||
public let anisetteProvider: AnisetteDataProvider
|
||||
private let client: GrandSlamClient
|
||||
private let client = GrandSlamClient()
|
||||
|
||||
public init(
|
||||
deviceInfo: DeviceInfo,
|
||||
anisetteProvider: AnisetteDataProvider,
|
||||
httpFactory: HTTPClientFactory = defaultHTTPClientFactory
|
||||
) throws {
|
||||
self.deviceInfo = deviceInfo
|
||||
self.anisetteProvider = anisetteProvider
|
||||
self.client = GrandSlamClient(
|
||||
deviceInfo: deviceInfo,
|
||||
anisetteProvider: anisetteProvider,
|
||||
httpFactory: httpFactory
|
||||
)
|
||||
}
|
||||
public init() {}
|
||||
|
||||
private func logIn(
|
||||
withLoginData loginData: GrandSlamLoginData
|
||||
|
||||
@@ -20,9 +20,11 @@ public struct DeveloperServicesFetchProfileOperation: DeveloperServicesOperation
|
||||
|
||||
public let context: SigningContext
|
||||
public let bundleID: String
|
||||
public init(context: SigningContext, bundleID: String) {
|
||||
public let signingInfo: SigningInfo
|
||||
public init(context: SigningContext, bundleID: String, signingInfo: SigningInfo) {
|
||||
self.context = context
|
||||
self.bundleID = bundleID
|
||||
self.signingInfo = signingInfo
|
||||
}
|
||||
|
||||
public func perform() async throws -> Mobileprovision {
|
||||
@@ -47,18 +49,7 @@ public struct DeveloperServicesFetchProfileOperation: DeveloperServicesOperation
|
||||
throw Errors.tooManyMatchingBundleIDs
|
||||
}
|
||||
|
||||
// note: free developer accounts don't seem to persist profiles at all
|
||||
// so this will often be empty.
|
||||
let profiles = bundleIDs
|
||||
.included?
|
||||
.compactMap { included -> Components.Schemas.Profile? in
|
||||
if case .Profile(let profile) = included, profile.relationships?.bundleId?.data?.id == bundleID.id {
|
||||
profile
|
||||
} else {
|
||||
nil
|
||||
}
|
||||
} ?? []
|
||||
|
||||
let profiles = bundleID.relationships?.profiles?.data ?? []
|
||||
switch profiles.count {
|
||||
case 0:
|
||||
// we're good
|
||||
@@ -70,23 +61,36 @@ public struct DeveloperServicesFetchProfileOperation: DeveloperServicesOperation
|
||||
break
|
||||
}
|
||||
|
||||
let serialNumber = signingInfo.certificate.serialNumber()
|
||||
let certs = try await context.developerAPIClient.certificatesGetCollection(
|
||||
query: .init(
|
||||
filter_lbrack_serialNumber_rbrack_: [serialNumber]
|
||||
)
|
||||
)
|
||||
.ok.body.json.data
|
||||
|
||||
let allDevices = try await context.developerAPIClient.devicesGetCollection()
|
||||
.ok.body.json.data
|
||||
|
||||
let response = try await context.developerAPIClient.profilesCreateInstance(
|
||||
body: .json(
|
||||
.init(
|
||||
data: .init(
|
||||
_type: .profiles,
|
||||
attributes: .init(
|
||||
name: "SC profile \(bundleID)",
|
||||
name: "SC profile \(bundleID.id)",
|
||||
profileType: .iosAppDevelopment
|
||||
),
|
||||
relationships: .init(
|
||||
bundleId: .init(
|
||||
data: .init(
|
||||
_type: .bundleIds,
|
||||
id: bundleID.id
|
||||
)
|
||||
data: .init(_type: .bundleIds, id: bundleID.id)
|
||||
),
|
||||
certificates: .init(data: [])
|
||||
devices: .init(data: allDevices.map {
|
||||
.init(_type: .devices, id: $0.id)
|
||||
}),
|
||||
certificates: .init(data: certs.map {
|
||||
.init(_type: .certificates, id: $0.id)
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Foundation
|
||||
import Crypto
|
||||
import ConcurrencyExtras
|
||||
import Dependencies
|
||||
|
||||
public protocol RawADIProvider: Sendable {
|
||||
func clientInfo() async throws -> String
|
||||
@@ -26,6 +27,43 @@ extension RawADIProvider {
|
||||
}
|
||||
}
|
||||
|
||||
private struct UnimplementedRawADIProvider: RawADIProvider {
|
||||
func startProvisioning(
|
||||
spim: Data,
|
||||
userID: UUID
|
||||
) async throws -> (any RawADIProvisioningSession, Data) {
|
||||
let closure: (Data, UUID) async throws -> (any RawADIProvisioningSession, Data) = unimplemented()
|
||||
return try await closure(spim, userID)
|
||||
}
|
||||
|
||||
func requestOTP(
|
||||
userID: UUID,
|
||||
routingInfo: inout UInt64,
|
||||
provisioningInfo: Data
|
||||
) async throws -> (machineID: Data, otp: Data) {
|
||||
let closure: (UUID, inout UInt64, Data) async throws -> (Data, Data) = unimplemented()
|
||||
return try await closure(userID, &routingInfo, provisioningInfo)
|
||||
}
|
||||
}
|
||||
|
||||
public enum RawADIProviderDependencyKey: DependencyKey {
|
||||
public static let testValue: RawADIProvider = UnimplementedRawADIProvider()
|
||||
public static let liveValue: RawADIProvider = {
|
||||
#if os(Linux)
|
||||
return SupersetteADIProvider()
|
||||
#else
|
||||
return OmnisetteADIProvider()
|
||||
#endif
|
||||
}()
|
||||
}
|
||||
|
||||
extension DependencyValues {
|
||||
public var rawADIProvider: RawADIProvider {
|
||||
get { self[RawADIProviderDependencyKey.self] }
|
||||
set { self[RawADIProviderDependencyKey.self] = newValue }
|
||||
}
|
||||
}
|
||||
|
||||
public protocol RawADIProvisioningSession: Sendable {
|
||||
func endProvisioning(
|
||||
routingInfo: UInt64,
|
||||
@@ -35,7 +73,7 @@ public protocol RawADIProvisioningSession: Sendable {
|
||||
}
|
||||
|
||||
// uses CoreADI APIs
|
||||
public final class ADIDataProvider: AnisetteDataProvider {
|
||||
public struct ADIDataProvider: AnisetteDataProvider {
|
||||
|
||||
public enum ADIError: Error {
|
||||
case hashingFailed
|
||||
@@ -44,41 +82,28 @@ public final class ADIDataProvider: AnisetteDataProvider {
|
||||
case badEndResponse
|
||||
}
|
||||
|
||||
public let rawProvider: RawADIProvider
|
||||
public let deviceInfo: DeviceInfo
|
||||
public let storage: KeyValueStorage
|
||||
@Dependency(\.keyValueStorage) var storage
|
||||
@Dependency(\.rawADIProvider) var rawProvider
|
||||
@Dependency(\.httpClient) var httpClient
|
||||
|
||||
private let httpClient: HTTPClientProtocol
|
||||
private let lookupManager: GrandSlamLookupManager
|
||||
private let lookupManager = GrandSlamLookupManager()
|
||||
private let localUserUID: UUID
|
||||
private let localUserID: String
|
||||
|
||||
private let _clientInfo = LockIsolated<String?>(nil)
|
||||
|
||||
public init(
|
||||
rawProvider: RawADIProvider,
|
||||
deviceInfo: DeviceInfo,
|
||||
storage: KeyValueStorage, // ideally secure, eg keychain
|
||||
provisioningData: ProvisioningData? = nil,
|
||||
httpFactory: HTTPClientFactory = defaultHTTPClientFactory
|
||||
) throws {
|
||||
self.rawProvider = rawProvider
|
||||
self.deviceInfo = deviceInfo
|
||||
self.storage = storage
|
||||
|
||||
self.httpClient = httpFactory.makeClient()
|
||||
self.lookupManager = .init(deviceInfo: deviceInfo, httpFactory: httpFactory)
|
||||
|
||||
public init(provisioningData: ProvisioningData? = nil) {
|
||||
@Dependency(\.keyValueStorage) var storage
|
||||
if let provisioningData {
|
||||
self.localUserUID = provisioningData.localUserUID
|
||||
try? storage.setData(provisioningData.adiPb, forKey: Self.provisioningKey)
|
||||
try? storage.setString("\(provisioningData.routingInfo)", forKey: Self.routingInfoKey)
|
||||
} else if let localUserUIDString = try storage.string(forKey: Self.localUserUIDKey),
|
||||
} else if let localUserUIDString = try? storage.string(forKey: Self.localUserUIDKey),
|
||||
let localUserUID = UUID(uuidString: localUserUIDString) {
|
||||
self.localUserUID = localUserUID
|
||||
} else {
|
||||
let localUserUID = UUID()
|
||||
try storage.setString(localUserUID.uuidString, forKey: Self.localUserUIDKey)
|
||||
try? storage.setString(localUserUID.uuidString, forKey: Self.localUserUIDKey)
|
||||
self.localUserUID = localUserUID
|
||||
}
|
||||
// localUserID = SHA256(local user UID)
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Dependencies
|
||||
|
||||
public protocol AnisetteDataProvider: Sendable {
|
||||
// This is a suggestion and not a requirement.
|
||||
@@ -24,5 +25,24 @@ public struct ProvisioningData: Hashable, Codable, Sendable {
|
||||
|
||||
extension AnisetteDataProvider {
|
||||
public func provisioningData() -> ProvisioningData? { nil }
|
||||
public func setProvisioningData(_ data: ProvisioningData) {}
|
||||
public func resetProvisioning() async {}
|
||||
}
|
||||
|
||||
extension DependencyValues {
|
||||
public var anisetteDataProvider: AnisetteDataProvider {
|
||||
get { self[AnisetteDataProviderDependencyKey.self] }
|
||||
set { self[AnisetteDataProviderDependencyKey.self] = newValue }
|
||||
}
|
||||
}
|
||||
|
||||
public struct AnisetteDataProviderDependencyKey: DependencyKey {
|
||||
public static let testValue: AnisetteDataProvider = UnimplementedAnisetteDataProvider()
|
||||
public static let liveValue: AnisetteDataProvider = ADIDataProvider()
|
||||
}
|
||||
|
||||
private struct UnimplementedAnisetteDataProvider: AnisetteDataProvider {
|
||||
func fetchAnisetteData() async throws -> AnisetteData {
|
||||
let closure: () async throws -> AnisetteData = unimplemented()
|
||||
return try await closure()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Dependencies
|
||||
|
||||
public struct DeviceInfo: Codable, Sendable {
|
||||
|
||||
@@ -90,3 +91,20 @@ public struct DeviceInfo: Codable, Sendable {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public struct DeviceInfoProvider: TestDependencyKey, Sendable {
|
||||
public var fetch: @Sendable () throws -> DeviceInfo
|
||||
|
||||
public init(fetch: @escaping @Sendable () throws -> DeviceInfo) {
|
||||
self.fetch = fetch
|
||||
}
|
||||
|
||||
public static let testValue = DeviceInfoProvider(fetch: unimplemented())
|
||||
}
|
||||
|
||||
extension DependencyValues {
|
||||
public var deviceInfoProvider: DeviceInfoProvider {
|
||||
get { self[DeviceInfoProvider.self] }
|
||||
set { self[DeviceInfoProvider.self] = newValue }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import Foundation
|
||||
import Dependencies
|
||||
|
||||
struct OmnisetteADIProvider: RawADIProvider {
|
||||
@Dependency(\.httpClient) private var client
|
||||
|
||||
// should implement v3 of https://github.com/SideStore/omnisette-server
|
||||
// list: https://servers.sidestore.io/servers.json
|
||||
// e.g. https://ani.sidestore.io
|
||||
private let url: URL
|
||||
private let client: HTTPClientProtocol
|
||||
init(
|
||||
url: URL = URL(string: "https://ani.sidestore.io")!, // URL(string: "http://localhost:6969")!,
|
||||
httpFactory: HTTPClientFactory = defaultHTTPClientFactory
|
||||
url: URL = URL(string: "https://ani.sidestore.io")! // URL(string: "http://localhost:6969")!
|
||||
) {
|
||||
self.url = url
|
||||
self.client = httpFactory.makeClient()
|
||||
}
|
||||
|
||||
static let decoder: JSONDecoder = {
|
||||
@@ -182,27 +182,3 @@ extension UUID {
|
||||
withUnsafeBytes(of: uuid) { Data($0) }
|
||||
}
|
||||
}
|
||||
|
||||
extension ADIDataProvider {
|
||||
public static func adiProvider(
|
||||
deviceInfo: DeviceInfo,
|
||||
storage: KeyValueStorage,
|
||||
provisioningData: ProvisioningData? = nil,
|
||||
httpFactory: HTTPClientFactory = defaultHTTPClientFactory
|
||||
) throws -> ADIDataProvider {
|
||||
#if os(Linux)
|
||||
let provider = SupersetteADIProvider(
|
||||
configDirectory: URL.homeDirectory.appending(path: ".config/Supercharge/Anisette")
|
||||
)
|
||||
#else
|
||||
let provider = OmnisetteADIProvider()
|
||||
#endif
|
||||
return try ADIDataProvider(
|
||||
rawProvider: provider,
|
||||
deviceInfo: deviceInfo,
|
||||
storage: storage,
|
||||
provisioningData: provisioningData,
|
||||
httpFactory: httpFactory
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,19 +2,15 @@
|
||||
|
||||
import Foundation
|
||||
import CSupersette
|
||||
import Dependencies
|
||||
|
||||
public actor SupersetteADIProvider: RawADIProvider {
|
||||
@MainActor private static var loadTask: Task<Void, Error>?
|
||||
@Dependency(\.httpClient) private var httpClient
|
||||
|
||||
public let directory: URL
|
||||
public let httpClient: HTTPClientProtocol
|
||||
|
||||
public init(
|
||||
configDirectory: URL,
|
||||
httpClientFactory: HTTPClientFactory = defaultHTTPClientFactory
|
||||
) {
|
||||
self.directory = configDirectory
|
||||
self.httpClient = httpClientFactory.makeClient()
|
||||
public init() {
|
||||
self.directory = URL.homeDirectory.appending(path: ".config/Supercharge/Anisette")
|
||||
}
|
||||
|
||||
private func _loadADI(id: UUID) async throws {
|
||||
|
||||
@@ -7,28 +7,23 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Dependencies
|
||||
|
||||
final class GrandSlamClient: Sendable {
|
||||
struct GrandSlamClient: Sendable {
|
||||
|
||||
private let encoder = PropertyListEncoder()
|
||||
|
||||
private let lookupManager: GrandSlamLookupManager
|
||||
private let lookupManager = GrandSlamLookupManager()
|
||||
|
||||
let deviceInfo: DeviceInfo
|
||||
let anisetteDataProvider: AnisetteDataProvider
|
||||
private let httpClient: HTTPClientProtocol
|
||||
init(
|
||||
deviceInfo: DeviceInfo,
|
||||
anisetteProvider: AnisetteDataProvider,
|
||||
httpFactory: HTTPClientFactory = defaultHTTPClientFactory
|
||||
) {
|
||||
self.deviceInfo = deviceInfo
|
||||
self.anisetteDataProvider = anisetteProvider
|
||||
self.lookupManager = .init(deviceInfo: deviceInfo, httpFactory: httpFactory)
|
||||
self.httpClient = httpFactory.makeClient()
|
||||
}
|
||||
@Dependency(\.deviceInfoProvider) var deviceInfoProvider
|
||||
@Dependency(\.anisetteDataProvider) var anisetteDataProvider
|
||||
@Dependency(\.httpClient) var httpClient
|
||||
|
||||
init() {}
|
||||
|
||||
func send<R: GrandSlamRequest>(_ request: R) async throws -> R.Decoder.Value {
|
||||
let deviceInfo = try deviceInfoProvider.fetch()
|
||||
|
||||
let anisetteData = try await anisetteDataProvider.fetchAnisetteData()
|
||||
let url = try await lookupManager.fetchURL(forEndpoint: R.endpoint)
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Dependencies
|
||||
|
||||
actor GrandSlamLookupManager {
|
||||
|
||||
@@ -19,14 +20,14 @@ actor GrandSlamLookupManager {
|
||||
private let decoder = PropertyListDecoder()
|
||||
private var endpoints: GrandSlamEndpoints?
|
||||
|
||||
let httpClient: HTTPClientProtocol
|
||||
let deviceInfo: DeviceInfo
|
||||
init(deviceInfo: DeviceInfo, httpFactory: HTTPClientFactory = defaultHTTPClientFactory) {
|
||||
self.deviceInfo = deviceInfo
|
||||
self.httpClient = httpFactory.makeClient()
|
||||
}
|
||||
@Dependency(\.deviceInfoProvider) var deviceInfoProvider
|
||||
@Dependency(\.httpClient) var httpClient
|
||||
|
||||
init() {}
|
||||
|
||||
private func performLookup() async throws -> GrandSlamEndpoints {
|
||||
let deviceInfo = try deviceInfoProvider.fetch()
|
||||
|
||||
/* {
|
||||
"X-Apple-I-Locale" = "en_IN";
|
||||
"X-Apple-I-TimeZone" = "Asia/Kolkata";
|
||||
|
||||
@@ -15,6 +15,22 @@ import NIOFoundationCompat
|
||||
import WebSocketKit
|
||||
import OpenAPIRuntime
|
||||
import OpenAPIAsyncHTTPClient
|
||||
import Dependencies
|
||||
|
||||
extension HTTPClientDependencyKey: DependencyKey {
|
||||
public static let liveValue: HTTPClientProtocol = {
|
||||
// if ssl cert parsing fails we're screwed so we might as well force try
|
||||
// swiftlint:disable:next force_try
|
||||
let appleRootCA = try! NIOSSLCertificate(bytes: Array(appleRootPEM.utf8), format: .pem)
|
||||
var tlsConfiguration: TLSConfiguration = .makeClientConfiguration()
|
||||
tlsConfiguration.additionalTrustRoots = [.certificates([appleRootCA])]
|
||||
let config = HTTPClient.Configuration(
|
||||
tlsConfiguration: tlsConfiguration,
|
||||
decompression: .enabled(limit: .none)
|
||||
)
|
||||
return HTTPClient(configuration: config)
|
||||
}()
|
||||
}
|
||||
|
||||
extension HTTPClient: HTTPClientProtocol {
|
||||
@discardableResult
|
||||
@@ -118,53 +134,34 @@ private final class WebSocketSessionWrapper: WebSocketSession {
|
||||
}
|
||||
}
|
||||
|
||||
final class AsyncHTTPClientFactory: HTTPClientFactory {
|
||||
private static let appleRootPEM = """
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIEuzCCA6OgAwIBAgIBAjANBgkqhkiG9w0BAQUFADBiMQswCQYDVQQGEwJVUzET
|
||||
MBEGA1UEChMKQXBwbGUgSW5jLjEmMCQGA1UECxMdQXBwbGUgQ2VydGlmaWNhdGlv
|
||||
biBBdXRob3JpdHkxFjAUBgNVBAMTDUFwcGxlIFJvb3QgQ0EwHhcNMDYwNDI1MjE0
|
||||
MDM2WhcNMzUwMjA5MjE0MDM2WjBiMQswCQYDVQQGEwJVUzETMBEGA1UEChMKQXBw
|
||||
bGUgSW5jLjEmMCQGA1UECxMdQXBwbGUgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkx
|
||||
FjAUBgNVBAMTDUFwcGxlIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw
|
||||
ggEKAoIBAQDkkakJH5HbHkdQ6wXtXnmELes2oldMVeyLGYne+Uts9QerIjAC6Bg+
|
||||
+FAJ039BqJj50cpmnCRrEdCju+QbKsMflZ56DKRHi1vUFjczy8QPTc4UadHJGXL1
|
||||
XQ7Vf1+b8iUDulWPTV0N8WQ1IxVLFVkds5T39pyez1C6wVhQZ48ItCD3y6wsIG9w
|
||||
tj8BMIy3Q88PnT3zK0koGsj+zrW5DtleHNbLPbU6rfQPDgCSC7EhFi501TwN22IW
|
||||
q6NxkkdTVcGvL0Gz+PvjcM3mo0xFfh9Ma1CWQYnEdGILEINBhzOKgbEwWOxaBDKM
|
||||
aLOPHd5lc/9nXmW8Sdh2nzMUZaF3lMktAgMBAAGjggF6MIIBdjAOBgNVHQ8BAf8E
|
||||
BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUK9BpR5R2Cf70a40uQKb3
|
||||
R01/CF4wHwYDVR0jBBgwFoAUK9BpR5R2Cf70a40uQKb3R01/CF4wggERBgNVHSAE
|
||||
ggEIMIIBBDCCAQAGCSqGSIb3Y2QFATCB8jAqBggrBgEFBQcCARYeaHR0cHM6Ly93
|
||||
d3cuYXBwbGUuY29tL2FwcGxlY2EvMIHDBggrBgEFBQcCAjCBthqBs1JlbGlhbmNl
|
||||
IG9uIHRoaXMgY2VydGlmaWNhdGUgYnkgYW55IHBhcnR5IGFzc3VtZXMgYWNjZXB0
|
||||
YW5jZSBvZiB0aGUgdGhlbiBhcHBsaWNhYmxlIHN0YW5kYXJkIHRlcm1zIGFuZCBj
|
||||
b25kaXRpb25zIG9mIHVzZSwgY2VydGlmaWNhdGUgcG9saWN5IGFuZCBjZXJ0aWZp
|
||||
Y2F0aW9uIHByYWN0aWNlIHN0YXRlbWVudHMuMA0GCSqGSIb3DQEBBQUAA4IBAQBc
|
||||
NplMLXi37Yyb3PN3m/J20ncwT8EfhYOFG5k9RzfyqZtAjizUsZAS2L70c5vu0mQP
|
||||
y3lPNNiiPvl4/2vIB+x9OYOLUyDTOMSxv5pPCmv/K/xZpwUJfBdAVhEedNO3iyM7
|
||||
R6PVbyTi69G3cN8PReEnyvFteO3ntRcXqNx+IjXKJdXZD9Zr1KIkIxH3oayPc4Fg
|
||||
xhtbCS+SsvhESPBgOJ4V9T0mZyCKM2r3DYLP3uujL/lTaltkwGMzd/c6ByxW69oP
|
||||
IQ7aunMZT7XZNn/Bh1XZp5m5MkL72NVxnn6hUrcbvZNCJBIqxw8dtk2cXmPIS4AX
|
||||
UKqK1drk/NAJBzewdXUh
|
||||
-----END CERTIFICATE-----
|
||||
"""
|
||||
|
||||
private let client: HTTPClient
|
||||
private init() {
|
||||
// if ssl cert parsing fails we're screwed so we might as well force try
|
||||
// swiftlint:disable:next force_try
|
||||
let appleRootCA = try! NIOSSLCertificate(bytes: Array(Self.appleRootPEM.utf8), format: .pem)
|
||||
var tlsConfiguration: TLSConfiguration = .makeClientConfiguration()
|
||||
tlsConfiguration.additionalTrustRoots = [.certificates([appleRootCA])]
|
||||
let config = HTTPClient.Configuration(
|
||||
tlsConfiguration: tlsConfiguration,
|
||||
decompression: .enabled(limit: .none)
|
||||
)
|
||||
client = HTTPClient(configuration: config)
|
||||
}
|
||||
static let shared = AsyncHTTPClientFactory()
|
||||
|
||||
func makeClient() -> HTTPClientProtocol { client }
|
||||
}
|
||||
private let appleRootPEM = """
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIEuzCCA6OgAwIBAgIBAjANBgkqhkiG9w0BAQUFADBiMQswCQYDVQQGEwJVUzET
|
||||
MBEGA1UEChMKQXBwbGUgSW5jLjEmMCQGA1UECxMdQXBwbGUgQ2VydGlmaWNhdGlv
|
||||
biBBdXRob3JpdHkxFjAUBgNVBAMTDUFwcGxlIFJvb3QgQ0EwHhcNMDYwNDI1MjE0
|
||||
MDM2WhcNMzUwMjA5MjE0MDM2WjBiMQswCQYDVQQGEwJVUzETMBEGA1UEChMKQXBw
|
||||
bGUgSW5jLjEmMCQGA1UECxMdQXBwbGUgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkx
|
||||
FjAUBgNVBAMTDUFwcGxlIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw
|
||||
ggEKAoIBAQDkkakJH5HbHkdQ6wXtXnmELes2oldMVeyLGYne+Uts9QerIjAC6Bg+
|
||||
+FAJ039BqJj50cpmnCRrEdCju+QbKsMflZ56DKRHi1vUFjczy8QPTc4UadHJGXL1
|
||||
XQ7Vf1+b8iUDulWPTV0N8WQ1IxVLFVkds5T39pyez1C6wVhQZ48ItCD3y6wsIG9w
|
||||
tj8BMIy3Q88PnT3zK0koGsj+zrW5DtleHNbLPbU6rfQPDgCSC7EhFi501TwN22IW
|
||||
q6NxkkdTVcGvL0Gz+PvjcM3mo0xFfh9Ma1CWQYnEdGILEINBhzOKgbEwWOxaBDKM
|
||||
aLOPHd5lc/9nXmW8Sdh2nzMUZaF3lMktAgMBAAGjggF6MIIBdjAOBgNVHQ8BAf8E
|
||||
BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUK9BpR5R2Cf70a40uQKb3
|
||||
R01/CF4wHwYDVR0jBBgwFoAUK9BpR5R2Cf70a40uQKb3R01/CF4wggERBgNVHSAE
|
||||
ggEIMIIBBDCCAQAGCSqGSIb3Y2QFATCB8jAqBggrBgEFBQcCARYeaHR0cHM6Ly93
|
||||
d3cuYXBwbGUuY29tL2FwcGxlY2EvMIHDBggrBgEFBQcCAjCBthqBs1JlbGlhbmNl
|
||||
IG9uIHRoaXMgY2VydGlmaWNhdGUgYnkgYW55IHBhcnR5IGFzc3VtZXMgYWNjZXB0
|
||||
YW5jZSBvZiB0aGUgdGhlbiBhcHBsaWNhYmxlIHN0YW5kYXJkIHRlcm1zIGFuZCBj
|
||||
b25kaXRpb25zIG9mIHVzZSwgY2VydGlmaWNhdGUgcG9saWN5IGFuZCBjZXJ0aWZp
|
||||
Y2F0aW9uIHByYWN0aWNlIHN0YXRlbWVudHMuMA0GCSqGSIb3DQEBBQUAA4IBAQBc
|
||||
NplMLXi37Yyb3PN3m/J20ncwT8EfhYOFG5k9RzfyqZtAjizUsZAS2L70c5vu0mQP
|
||||
y3lPNNiiPvl4/2vIB+x9OYOLUyDTOMSxv5pPCmv/K/xZpwUJfBdAVhEedNO3iyM7
|
||||
R6PVbyTi69G3cN8PReEnyvFteO3ntRcXqNx+IjXKJdXZD9Zr1KIkIxH3oayPc4Fg
|
||||
xhtbCS+SsvhESPBgOJ4V9T0mZyCKM2r3DYLP3uujL/lTaltkwGMzd/c6ByxW69oP
|
||||
IQ7aunMZT7XZNn/Bh1XZp5m5MkL72NVxnn6hUrcbvZNCJBIqxw8dtk2cXmPIS4AX
|
||||
UKqK1drk/NAJBzewdXUh
|
||||
-----END CERTIFICATE-----
|
||||
"""
|
||||
#endif
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
|
||||
import Foundation
|
||||
import OpenAPIRuntime
|
||||
import Dependencies
|
||||
import HTTPTypes
|
||||
|
||||
public struct HTTPRequest: Sendable {
|
||||
public enum Body: Sendable {
|
||||
@@ -57,6 +59,7 @@ public protocol HTTPClientProtocol: Sendable {
|
||||
_ request: HTTPRequest,
|
||||
onProgress: sending @isolated(any) (Double?) -> Void
|
||||
) async throws -> HTTPResponse
|
||||
|
||||
func makeWebSocket(url: URL) async throws -> WebSocketSession
|
||||
}
|
||||
|
||||
@@ -66,6 +69,44 @@ extension HTTPClientProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
private struct UnimplementedHTTPClient: HTTPClientProtocol, ClientTransport {
|
||||
public var asOpenAPITransport: ClientTransport { self }
|
||||
|
||||
func send(
|
||||
_ request: HTTPTypes.HTTPRequest,
|
||||
body: OpenAPIRuntime.HTTPBody?,
|
||||
baseURL: URL,
|
||||
operationID: String
|
||||
) async throws -> (HTTPTypes.HTTPResponse, OpenAPIRuntime.HTTPBody?) {
|
||||
let closure: (HTTPTypes.HTTPRequest, OpenAPIRuntime.HTTPBody?, URL, String) async throws -> (HTTPTypes.HTTPResponse, OpenAPIRuntime.HTTPBody?) = unimplemented()
|
||||
return try await closure(request, body, baseURL, operationID)
|
||||
}
|
||||
|
||||
func makeRequest(
|
||||
_ request: HTTPRequest,
|
||||
onProgress: sending @isolated(any) (Double?) -> Void
|
||||
) async throws -> HTTPResponse {
|
||||
let closure: () throws -> HTTPResponse = unimplemented()
|
||||
return try closure()
|
||||
}
|
||||
|
||||
public func makeWebSocket(url: URL) async throws -> any WebSocketSession {
|
||||
let closure: (URL) async throws -> any WebSocketSession = unimplemented()
|
||||
return try await closure(url)
|
||||
}
|
||||
}
|
||||
|
||||
public enum HTTPClientDependencyKey: TestDependencyKey {
|
||||
public static let testValue: HTTPClientProtocol = UnimplementedHTTPClient()
|
||||
}
|
||||
|
||||
extension DependencyValues {
|
||||
public var httpClient: HTTPClientProtocol {
|
||||
get { self[HTTPClientDependencyKey.self] }
|
||||
set { self[HTTPClientDependencyKey.self] = newValue }
|
||||
}
|
||||
}
|
||||
|
||||
public protocol WebSocketSession: Sendable {
|
||||
func receive() async throws -> WebSocketMessage
|
||||
func send(_ message: WebSocketMessage) async throws
|
||||
@@ -76,15 +117,3 @@ public enum WebSocketMessage: Sendable {
|
||||
case text(String)
|
||||
case data(Data)
|
||||
}
|
||||
|
||||
public protocol HTTPClientFactory: Sendable {
|
||||
func makeClient() -> HTTPClientProtocol
|
||||
}
|
||||
|
||||
public let defaultHTTPClientFactory: HTTPClientFactory = {
|
||||
#if os(Linux)
|
||||
return AsyncHTTPClientFactory.shared
|
||||
#else
|
||||
return URLHTTPClientFactory.shared
|
||||
#endif
|
||||
}()
|
||||
|
||||
@@ -10,15 +10,12 @@ import Foundation
|
||||
import ConcurrencyExtras
|
||||
import OpenAPIRuntime
|
||||
import OpenAPIURLSession
|
||||
import Dependencies
|
||||
|
||||
public struct UnknownHTTPError: Error {}
|
||||
|
||||
final class URLHTTPClientFactory: HTTPClientFactory {
|
||||
static let shared = URLHTTPClientFactory()
|
||||
|
||||
private let client = Client()
|
||||
|
||||
func makeClient() -> HTTPClientProtocol { client }
|
||||
extension HTTPClientDependencyKey: DependencyKey {
|
||||
public static let liveValue: HTTPClientProtocol = Client()
|
||||
}
|
||||
|
||||
private final class Client: HTTPClientProtocol {
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import Foundation
|
||||
import SwiftyMobileDevice
|
||||
import ConcurrencyExtras
|
||||
import Dependencies
|
||||
|
||||
extension LockdownClient {
|
||||
static let installerLabel = "supersign"
|
||||
@@ -17,39 +18,11 @@ extension LockdownClient {
|
||||
#if !os(iOS)
|
||||
|
||||
public protocol IntegratedInstallerDelegate: AnyObject, Sendable {
|
||||
|
||||
func setPresentedMessage(_ message: IntegratedInstaller.Message?)
|
||||
func installerDidUpdate(toStage stage: String, progress: Double?)
|
||||
|
||||
// defaults to always returning true
|
||||
func confirmRevocation(of certificates: [DeveloperServicesCertificate]) async -> Bool
|
||||
|
||||
/// Decompress the zipped ipa file
|
||||
///
|
||||
/// - Parameter ipa: The `ipa` file to decompress.
|
||||
/// - Parameter directory: The directory into which `ipa` should be decompressed.
|
||||
/// - Parameter progress: A closure to which the callee can provide progress updates.
|
||||
/// - Parameter currentProgress: The current progress, or `nil` to indicate it is indeterminate.
|
||||
func decompress(
|
||||
ipa: URL,
|
||||
in directory: URL,
|
||||
progress: @escaping @Sendable (_ currentProgress: Double?) -> Void
|
||||
) async throws
|
||||
|
||||
// `compress` is required because the only way to upload symlinks via afc is by
|
||||
// putting them in a zip archive (afc_make_symlink was disabled due to security or
|
||||
// something)
|
||||
|
||||
/// Compress the app before installation.
|
||||
///
|
||||
/// - Parameter payloadDir: The `Payload` directory which is to be compressed.
|
||||
/// - Parameter progress: A closure to which the callee can provide progress updates.
|
||||
/// - Parameter currentProgress: The current progress, or `nil` to indicate it is indeterminate.
|
||||
func compress(
|
||||
payloadDir: URL,
|
||||
progress: @escaping @Sendable (_ currentProgress: Double?) -> Void
|
||||
) async throws -> URL
|
||||
|
||||
}
|
||||
|
||||
extension IntegratedInstallerDelegate {
|
||||
@@ -83,9 +56,10 @@ public actor IntegratedInstaller {
|
||||
let lookupMode: LookupMode
|
||||
let auth: DeveloperAPIAuthData
|
||||
let configureDevice: Bool
|
||||
let storage: KeyValueStorage
|
||||
public weak var delegate: IntegratedInstallerDelegate?
|
||||
|
||||
@Dependency(\.zipCompressor) private var compressor
|
||||
|
||||
private var appInstaller: AppInstaller?
|
||||
|
||||
private let tempDir = FileManager.default.temporaryDirectoryShim
|
||||
@@ -142,14 +116,12 @@ public actor IntegratedInstaller {
|
||||
lookupMode: LookupMode,
|
||||
auth: DeveloperAPIAuthData,
|
||||
configureDevice: Bool,
|
||||
storage: KeyValueStorage,
|
||||
delegate: IntegratedInstallerDelegate
|
||||
) {
|
||||
self.udid = udid
|
||||
self.lookupMode = lookupMode
|
||||
self.auth = auth
|
||||
self.configureDevice = configureDevice
|
||||
self.storage = storage
|
||||
self.delegate = delegate
|
||||
}
|
||||
|
||||
@@ -251,8 +223,8 @@ public actor IntegratedInstaller {
|
||||
|
||||
switch app.pathExtension {
|
||||
case "ipa":
|
||||
try await delegate?.decompress(
|
||||
ipa: app,
|
||||
try await compressor.decompress(
|
||||
file: app,
|
||||
in: tempDir,
|
||||
progress: { progress in
|
||||
self.queueUpdateTask {
|
||||
@@ -305,16 +277,10 @@ public actor IntegratedInstaller {
|
||||
|
||||
try await updateProgress(to: 1)
|
||||
|
||||
// guard let deviceInfo = DeviceInfo.current() else {
|
||||
// throw Error.deviceInfoFetchFailed
|
||||
// }
|
||||
// let anisetteProvider = try ADIDataProvider.adiProvider(deviceInfo: deviceInfo, storage: storage)
|
||||
|
||||
let context = try SigningContext(
|
||||
udid: udid,
|
||||
deviceName: deviceName,
|
||||
auth: auth,
|
||||
signingInfoManager: KeyValueSigningInfoManager(storage: storage)
|
||||
auth: auth
|
||||
)
|
||||
|
||||
let signer = Signer(context: context) { certs in
|
||||
@@ -335,26 +301,13 @@ public actor IntegratedInstaller {
|
||||
didProvision: { @Sendable [self] in
|
||||
// TODO: reintroduce Superconfig
|
||||
_ = self
|
||||
// if let pairingKeys = pairingKeys {
|
||||
// let info = try context.signingInfoManager.info(forTeamID: context.teamID)
|
||||
// try Superconfig(
|
||||
// udid: udid,
|
||||
// pairingKeys: pairingKeys,
|
||||
// deviceInfo: deviceInfo,
|
||||
// preferredTeamID: teamID.rawValue,
|
||||
// preferredSigningInfo: info,
|
||||
// appleID: appleID,
|
||||
// provisioningData: anisetteProvider.provisioningData(),
|
||||
// token: token
|
||||
// ).save(inAppDir: appDir)
|
||||
// }
|
||||
}
|
||||
)
|
||||
|
||||
try await self.updateStage(to: "Packaging", initialProgress: nil)
|
||||
|
||||
let ipa = try await delegate?.compress(
|
||||
payloadDir: appDir.deletingLastPathComponent(),
|
||||
let ipa = try await compressor.compress(
|
||||
directory: appDir.deletingLastPathComponent(),
|
||||
progress: { progress in
|
||||
self.queueUpdateTask {
|
||||
$0.updateProgressIgnoringCancellation(to: progress)
|
||||
@@ -364,7 +317,7 @@ public actor IntegratedInstaller {
|
||||
|
||||
try await self.updateProgress(to: 1)
|
||||
|
||||
let appInstaller = AppInstaller(ipa: ipa!, udid: udid, connectionPreferences: .init(lookupMode: lookupMode))
|
||||
let appInstaller = AppInstaller(ipa: ipa, udid: udid, connectionPreferences: .init(lookupMode: lookupMode))
|
||||
self.appInstaller = appInstaller
|
||||
try await appInstaller.install(
|
||||
progress: { stage in
|
||||
|
||||
@@ -13,10 +13,7 @@ public struct SigningContext: Sendable {
|
||||
|
||||
public let udid: String
|
||||
public let deviceName: String
|
||||
|
||||
public let auth: DeveloperAPIAuthData
|
||||
|
||||
public let signingInfoManager: SigningInfoManager
|
||||
public let signerImpl: SignerImpl
|
||||
|
||||
public var developerAPIClient: DeveloperAPIClient {
|
||||
@@ -27,13 +24,11 @@ public struct SigningContext: Sendable {
|
||||
udid: String,
|
||||
deviceName: String,
|
||||
auth: DeveloperAPIAuthData,
|
||||
signingInfoManager: SigningInfoManager,
|
||||
signerImpl: SignerImpl? = nil
|
||||
) throws {
|
||||
self.udid = udid
|
||||
self.deviceName = deviceName
|
||||
self.auth = auth
|
||||
self.signingInfoManager = signingInfoManager
|
||||
self.signerImpl = try signerImpl ?? .first()
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import Foundation
|
||||
import ConcurrencyExtras
|
||||
import Dependencies
|
||||
|
||||
public enum KeyValueStorageError: Error {
|
||||
case stringConversionFailure
|
||||
@@ -21,6 +22,17 @@ public protocol KeyValueStorage: Sendable {
|
||||
func setString(_ string: String?, forKey key: String) throws
|
||||
}
|
||||
|
||||
public enum KeyValueStorageDependencyKey: TestDependencyKey {
|
||||
public static let testValue: KeyValueStorage = UnimplementedKeyValueStorage()
|
||||
}
|
||||
|
||||
extension DependencyValues {
|
||||
public var keyValueStorage: KeyValueStorage {
|
||||
get { self[KeyValueStorageDependencyKey.self] }
|
||||
set { self[KeyValueStorageDependencyKey.self] = newValue }
|
||||
}
|
||||
}
|
||||
|
||||
extension KeyValueStorage {
|
||||
public func string(forKey key: String) throws -> String? {
|
||||
try data(forKey: key).map {
|
||||
@@ -44,6 +56,16 @@ extension KeyValueStorage {
|
||||
}
|
||||
}
|
||||
|
||||
private struct UnimplementedKeyValueStorage: KeyValueStorage {
|
||||
func data(forKey key: String) throws -> Data? {
|
||||
unimplemented(placeholder: nil)
|
||||
}
|
||||
|
||||
func setData(_ data: Data?, forKey key: String) throws {
|
||||
unimplemented()
|
||||
}
|
||||
}
|
||||
|
||||
public final class MemoryKeyValueStorage: KeyValueStorage {
|
||||
|
||||
private let dict = LockIsolated<[String: Data]>([:])
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import Foundation
|
||||
import ConcurrencyExtras
|
||||
import Dependencies
|
||||
|
||||
public struct SigningInfo: Codable, Sendable {
|
||||
public let privateKey: PrivateKey
|
||||
@@ -19,6 +20,18 @@ public protocol SigningInfoManager: Sendable {
|
||||
func setInfo(_ info: SigningInfo?, forIdentityID identityID: String) throws
|
||||
}
|
||||
|
||||
public enum SigningInfoManagerDependencyKey: DependencyKey {
|
||||
public static let testValue: SigningInfoManager = UnimplementedSigningInfoManager()
|
||||
public static let liveValue: SigningInfoManager = KeyValueSigningInfoManager()
|
||||
}
|
||||
|
||||
extension DependencyValues {
|
||||
public var signingInfoManager: SigningInfoManager {
|
||||
get { self[SigningInfoManagerDependencyKey.self] }
|
||||
set { self[SigningInfoManagerDependencyKey.self] = newValue }
|
||||
}
|
||||
}
|
||||
|
||||
extension SigningInfoManager {
|
||||
subscript(identityID: String) -> SigningInfo? {
|
||||
get {
|
||||
@@ -30,6 +43,16 @@ extension SigningInfoManager {
|
||||
}
|
||||
}
|
||||
|
||||
private struct UnimplementedSigningInfoManager: SigningInfoManager {
|
||||
func info(forIdentityID identityID: String) throws -> SigningInfo? {
|
||||
unimplemented(placeholder: nil)
|
||||
}
|
||||
|
||||
func setInfo(_ info: SigningInfo?, forIdentityID identityID: String) throws {
|
||||
unimplemented()
|
||||
}
|
||||
}
|
||||
|
||||
public final class MemoryBackedSigningInfoManager: SigningInfoManager {
|
||||
private let infos = LockIsolated<[String: SigningInfo]>([:])
|
||||
|
||||
@@ -50,10 +73,7 @@ public struct KeyValueSigningInfoManager: SigningInfoManager {
|
||||
private let encoder = PropertyListEncoder()
|
||||
private let decoder = PropertyListDecoder()
|
||||
|
||||
public let storage: KeyValueStorage
|
||||
public init(storage: KeyValueStorage) {
|
||||
self.storage = storage
|
||||
}
|
||||
@Dependency(\.keyValueStorage) var storage
|
||||
|
||||
public func info(forIdentityID identityID: String) throws -> SigningInfo? {
|
||||
guard let data = try storage.data(forKey: identityID)
|
||||
|
||||
65
Sources/Supersign/Utilities/ZIPCompressor.swift
Normal file
65
Sources/Supersign/Utilities/ZIPCompressor.swift
Normal file
@@ -0,0 +1,65 @@
|
||||
import Foundation
|
||||
import Dependencies
|
||||
|
||||
public struct ZIPCompressor: TestDependencyKey, Sendable {
|
||||
public var compress: @Sendable (
|
||||
_ directory: URL,
|
||||
_ progress: @escaping @Sendable (_ currentProgress: Double?) -> Void
|
||||
) async throws -> URL
|
||||
|
||||
public var decompress: @Sendable (
|
||||
_ file: URL,
|
||||
_ directory: URL,
|
||||
_ progress: @escaping @Sendable (_ currentProgress: Double?) -> Void
|
||||
) async throws -> Void
|
||||
|
||||
public init(
|
||||
compress: @Sendable @escaping (URL, @Sendable @escaping (Double?) -> Void) async throws -> URL,
|
||||
decompress: @Sendable @escaping (URL, URL, @Sendable @escaping (Double?) -> Void) async throws -> Void
|
||||
) {
|
||||
self.decompress = decompress
|
||||
self.compress = compress
|
||||
}
|
||||
|
||||
public static let testValue = ZIPCompressor(
|
||||
compress: unimplemented(),
|
||||
decompress: unimplemented()
|
||||
)
|
||||
|
||||
/// Decompress the zipped ipa file
|
||||
///
|
||||
/// - Parameter ipa: The `ipa` file to decompress.
|
||||
/// - Parameter directory: The directory into which `ipa` should be decompressed.
|
||||
/// - Parameter progress: A closure to which the callee can provide progress updates.
|
||||
/// - Parameter currentProgress: The current progress, or `nil` to indicate it is indeterminate.
|
||||
public func decompress(
|
||||
file: URL,
|
||||
in directory: URL,
|
||||
progress: @escaping @Sendable (_ currentProgress: Double?) -> Void
|
||||
) async throws {
|
||||
try await decompress(file, directory, progress)
|
||||
}
|
||||
|
||||
// `compress` is required because the only way to upload symlinks via afc is by
|
||||
// putting them in a zip archive (afc_make_symlink was disabled due to security or
|
||||
// something)
|
||||
|
||||
/// Compress the app before installation.
|
||||
///
|
||||
/// - Parameter payloadDir: The `Payload` directory which is to be compressed.
|
||||
/// - Parameter progress: A closure to which the callee can provide progress updates.
|
||||
/// - Parameter currentProgress: The current progress, or `nil` to indicate it is indeterminate.
|
||||
public func compress(
|
||||
directory: URL,
|
||||
progress: @escaping @Sendable (_ currentProgress: Double?) -> Void
|
||||
) async throws -> URL {
|
||||
try await compress(directory, progress)
|
||||
}
|
||||
}
|
||||
|
||||
extension DependencyValues {
|
||||
public var zipCompressor: ZIPCompressor {
|
||||
get { self[ZIPCompressor.self] }
|
||||
set { self[ZIPCompressor.self] = newValue }
|
||||
}
|
||||
}
|
||||
28
Sources/SupersignCLI/SupersignCLI.swift
Normal file
28
Sources/SupersignCLI/SupersignCLI.swift
Normal file
@@ -0,0 +1,28 @@
|
||||
import Foundation
|
||||
import Supersign
|
||||
import SupersignCLISupport
|
||||
import Dependencies
|
||||
|
||||
@main enum SupersignCLIMain {
|
||||
static func main() async throws {
|
||||
try await SupersignCLI.run()
|
||||
}
|
||||
}
|
||||
|
||||
#warning("Improve persistence mechanism")
|
||||
|
||||
// for macOS, we can use KeychainStorage but we need to sign with entitlements.
|
||||
// for Windows, we could use dpapi.h or wincred.h.
|
||||
// for Linux, maybe use libsecret?
|
||||
// see https://github.com/atom/node-keytar
|
||||
extension KeyValueStorageDependencyKey: DependencyKey {
|
||||
public static let liveValue: KeyValueStorage = {
|
||||
// #if os(macOS)
|
||||
// KeychainStorage(service: "com.kabiroberai.Supercharge-Keychain.credentials")
|
||||
// #else
|
||||
DirectoryStorage(
|
||||
base: URL(fileURLWithPath: NSHomeDirectory()).appendingPathComponent(".config/Supercharge/data")
|
||||
)
|
||||
// #endif
|
||||
}()
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import Foundation
|
||||
import Supersign
|
||||
import SupersignCLISupport
|
||||
|
||||
// let app = Bundle.module.url(forResource: "Supercharge", withExtension: "ipa")!
|
||||
|
||||
#warning("Improve persistence mechanism")
|
||||
|
||||
// for macOS, we can use KeychainStorage but we need to sign with entitlements.
|
||||
// for Windows, we could use dpapi.h or wincred.h.
|
||||
// for Linux, maybe use libsecret?
|
||||
// see https://github.com/atom/node-keytar
|
||||
|
||||
let storage: KeyValueStorage
|
||||
// #if os(macOS)
|
||||
// storage = KeychainStorage(service: "com.kabiroberai.Supercharge-Keychain.credentials")
|
||||
// #else
|
||||
let directory = URL(fileURLWithPath: NSHomeDirectory()).appendingPathComponent(".config/Supercharge/data")
|
||||
storage = DirectoryStorage(base: directory)
|
||||
// #endif
|
||||
|
||||
try await SupersignCLI.run(configuration: SupersignCLI.Configuration(
|
||||
superchargeApp: nil,
|
||||
storage: storage
|
||||
))
|
||||
@@ -2,6 +2,7 @@ import Foundation
|
||||
import Supersign
|
||||
import ArgumentParser
|
||||
import Crypto
|
||||
import Dependencies
|
||||
|
||||
enum AuthMode: String, CaseIterable, CustomStringConvertible, ExpressibleByArgument {
|
||||
case key
|
||||
@@ -86,28 +87,15 @@ struct AuthOperation {
|
||||
|
||||
print("Logging in...")
|
||||
|
||||
let deviceInfo = try DeviceInfo.fetch()
|
||||
|
||||
let provider = try ADIDataProvider.adiProvider(
|
||||
deviceInfo: deviceInfo,
|
||||
storage: SupersignCLI.config.storage
|
||||
)
|
||||
let authDelegate = SupersignCLIAuthDelegate()
|
||||
let manager = try DeveloperServicesLoginManager(
|
||||
deviceInfo: deviceInfo,
|
||||
anisetteProvider: provider
|
||||
)
|
||||
let manager = DeveloperServicesLoginManager()
|
||||
let token = try await manager.logIn(
|
||||
withUsername: username,
|
||||
password: password,
|
||||
twoFactorDelegate: authDelegate
|
||||
)
|
||||
|
||||
let client = DeveloperServicesClient(
|
||||
loginToken: token,
|
||||
deviceInfo: deviceInfo,
|
||||
anisetteProvider: provider
|
||||
)
|
||||
let client = DeveloperServicesClient(loginToken: token)
|
||||
let teams = try await client.send(DeveloperServicesListTeamsRequest())
|
||||
let team = try await Console.choose(
|
||||
from: teams,
|
||||
@@ -179,11 +167,8 @@ struct AuthLogoutCommand: AsyncParsableCommand {
|
||||
}
|
||||
|
||||
if reset2FA {
|
||||
try ADIDataProvider.adiProvider(
|
||||
deviceInfo: .fetch(),
|
||||
storage: SupersignCLI.config.storage
|
||||
)
|
||||
.resetProvisioning()
|
||||
@Dependency(\.anisetteDataProvider) var anisetteProvider
|
||||
await anisetteProvider.resetProvisioning()
|
||||
print("Forgot device")
|
||||
}
|
||||
}
|
||||
@@ -225,3 +210,8 @@ extension DeviceInfo {
|
||||
return deviceInfo
|
||||
}
|
||||
}
|
||||
|
||||
extension DeviceInfoProvider: DependencyKey {
|
||||
private static let current = Result { try DeviceInfo.fetch() }
|
||||
public static let liveValue = DeviceInfoProvider { try current.get() }
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Foundation
|
||||
import Supersign
|
||||
import Dependencies
|
||||
|
||||
enum AuthToken: Codable, CustomStringConvertible {
|
||||
struct Xcode: Codable {
|
||||
@@ -38,23 +39,28 @@ enum AuthToken: Codable, CustomStringConvertible {
|
||||
|
||||
extension AuthToken {
|
||||
|
||||
private static var storage: KeyValueStorage {
|
||||
@Dependency(\.keyValueStorage) var storage
|
||||
return storage
|
||||
}
|
||||
|
||||
private static let encoder = JSONEncoder()
|
||||
private static let decoder = JSONDecoder()
|
||||
|
||||
static func saved() throws -> Self {
|
||||
guard let data = try SupersignCLI.config.storage.data(forKey: "SUPAuthToken") else {
|
||||
guard let data = try storage.data(forKey: "SUPAuthToken") else {
|
||||
throw Console.Error("Please log in with `supersign ds login` before running this command.")
|
||||
}
|
||||
return try decoder.decode(AuthToken.self, from: data)
|
||||
}
|
||||
|
||||
static func clear() throws {
|
||||
try SupersignCLI.config.storage.setData(nil, forKey: "SUPAuthToken")
|
||||
try Self.storage.setData(nil, forKey: "SUPAuthToken")
|
||||
}
|
||||
|
||||
func save() throws {
|
||||
let data = try Self.encoder.encode(self)
|
||||
try SupersignCLI.config.storage.setData(data, forKey: "SUPAuthToken")
|
||||
try Self.storage.setData(data, forKey: "SUPAuthToken")
|
||||
}
|
||||
|
||||
func authData() throws -> DeveloperAPIAuthData {
|
||||
@@ -62,19 +68,13 @@ extension AuthToken {
|
||||
case .appStoreConnect(let data):
|
||||
return .appStoreConnect(.init(id: data.id, issuerID: data.issuerID, pem: data.pem))
|
||||
case .xcode(let data):
|
||||
let deviceInfo = try DeviceInfo.fetch()
|
||||
return .xcode(.init(
|
||||
loginToken: DeveloperServicesLoginToken(
|
||||
adsid: data.adsid,
|
||||
token: data.token,
|
||||
expiry: data.expiry
|
||||
),
|
||||
deviceInfo: deviceInfo,
|
||||
teamID: .init(rawValue: data.teamID),
|
||||
anisetteDataProvider: try ADIDataProvider.adiProvider(
|
||||
deviceInfo: deviceInfo,
|
||||
storage: SupersignCLI.config.storage
|
||||
)
|
||||
teamID: .init(rawValue: data.teamID)
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ import Foundation
|
||||
import Supersign
|
||||
import ArgumentParser
|
||||
import DeveloperAPI
|
||||
import OpenAPIRuntime
|
||||
import Dependencies
|
||||
|
||||
struct DSTeamsListCommand: AsyncParsableCommand {
|
||||
static let configuration = CommandConfiguration(
|
||||
@@ -62,13 +64,12 @@ struct DSAnisetteCommand: AsyncParsableCommand {
|
||||
)
|
||||
|
||||
func run() async throws {
|
||||
// swiftlint:disable:next force_try
|
||||
let res = try! await ADIDataProvider(
|
||||
rawProvider: Provider(),
|
||||
deviceInfo: .current()!,
|
||||
storage: SupersignCLI.config.storage
|
||||
).fetchAnisetteData()
|
||||
|
||||
let provider = withDependencies {
|
||||
$0.rawADIProvider = Provider()
|
||||
} operation: {
|
||||
ADIDataProvider()
|
||||
}
|
||||
let res = try await provider.fetchAnisetteData()
|
||||
print(res)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,7 +110,6 @@ struct DevRunCommand: AsyncParsableCommand {
|
||||
lookupMode: .only(client.connectionType),
|
||||
auth: try token.authData(),
|
||||
configureDevice: false,
|
||||
storage: SupersignCLI.config.storage,
|
||||
delegate: installDelegate
|
||||
)
|
||||
|
||||
|
||||
@@ -2,9 +2,7 @@ import Foundation
|
||||
import Supersign
|
||||
import Version
|
||||
import ArgumentParser
|
||||
#if os(Linux)
|
||||
import FoundationNetworking
|
||||
#endif
|
||||
import Dependencies
|
||||
|
||||
struct DevSDKCommand: AsyncParsableCommand {
|
||||
static let configuration = CommandConfiguration(
|
||||
@@ -77,10 +75,9 @@ struct DarwinSDKVersions: Decodable {
|
||||
var current: String
|
||||
var metadata: [String: Metadata]
|
||||
|
||||
static func all(
|
||||
httpFactory: HTTPClientFactory = defaultHTTPClientFactory
|
||||
) async throws -> DarwinSDKVersions {
|
||||
let data = try await httpFactory.makeClient().makeRequest(HTTPRequest(url: url)).body ?? Data()
|
||||
static func all() async throws -> DarwinSDKVersions {
|
||||
@Dependency(\.httpClient) var httpClient
|
||||
let data = try await httpClient.makeRequest(HTTPRequest(url: url)).body ?? Data()
|
||||
return try decoder.decode(self, from: data)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,6 @@ struct InstallCommand: AsyncParsableCommand {
|
||||
lookupMode: .only(client.connectionType),
|
||||
auth: try token.authData(),
|
||||
configureDevice: false,
|
||||
storage: SupersignCLI.config.storage,
|
||||
delegate: installDelegate
|
||||
)
|
||||
|
||||
|
||||
50
Sources/SupersignCLISupport/ProcessZIPCompressor.swift
Normal file
50
Sources/SupersignCLISupport/ProcessZIPCompressor.swift
Normal file
@@ -0,0 +1,50 @@
|
||||
//
|
||||
// File.swift
|
||||
// Supersign
|
||||
//
|
||||
// Created by Kabir Oberai on 23/12/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Supersign
|
||||
import Dependencies
|
||||
|
||||
extension ZIPCompressor: DependencyKey {
|
||||
// TODO: Use `powershell Compress-Archive` and `powershell Expand-Archive` on Windows
|
||||
|
||||
public static let liveValue = ZIPCompressor(
|
||||
compress: { dir, progress in
|
||||
progress(nil)
|
||||
|
||||
let dest = dir.deletingLastPathComponent().appendingPathComponent("app.ipa")
|
||||
|
||||
let zip = Process()
|
||||
zip.executableURL = URL(fileURLWithPath: "/usr/bin/env")
|
||||
zip.currentDirectoryURL = dir.deletingLastPathComponent()
|
||||
zip.arguments = ["zip", "-yqru0", dest.path, "Payload"]
|
||||
try zip.run()
|
||||
await zip.waitForExit()
|
||||
guard zip.terminationStatus == 0 else {
|
||||
throw ZIPCompressorError.compressionFailed
|
||||
}
|
||||
|
||||
return dest
|
||||
},
|
||||
decompress: { ipa, directory, progress in
|
||||
progress(nil)
|
||||
let unzip = Process()
|
||||
unzip.executableURL = URL(fileURLWithPath: "/usr/bin/env")
|
||||
unzip.arguments = ["unzip", "-q", ipa.path, "-d", directory.path]
|
||||
try unzip.run()
|
||||
await unzip.waitForExit()
|
||||
guard unzip.terminationStatus == 0 else {
|
||||
throw ZIPCompressorError.decompressionFailed
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
enum ZIPCompressorError: Error {
|
||||
case compressionFailed
|
||||
case decompressionFailed
|
||||
}
|
||||
@@ -9,11 +9,6 @@ final class SupersignCLIAuthDelegate: TwoFactorAuthDelegate {
|
||||
}
|
||||
|
||||
actor SupersignCLIDelegate: IntegratedInstallerDelegate {
|
||||
public enum Error: Swift.Error {
|
||||
case decompressionFailed
|
||||
case compressionFailed
|
||||
}
|
||||
|
||||
init() {}
|
||||
|
||||
private let updateTask = LockIsolated<Task<Void, Never>?>(nil)
|
||||
@@ -96,57 +91,4 @@ actor SupersignCLIDelegate: IntegratedInstallerDelegate {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Use `powershell Compress-Archive` and `powershell Expand-Archive` on Windows
|
||||
|
||||
func decompress(
|
||||
ipa: URL,
|
||||
in directory: URL,
|
||||
progress: @escaping (Double?) -> Void
|
||||
) async throws {
|
||||
progress(nil)
|
||||
|
||||
let unzip = Process()
|
||||
unzip.executableURL = URL(fileURLWithPath: "/usr/bin/env")
|
||||
unzip.arguments = ["unzip", "-q", ipa.path, "-d", directory.path]
|
||||
try await unzip.launchAndWait()
|
||||
guard unzip.terminationStatus == 0 else {
|
||||
throw Error.decompressionFailed
|
||||
}
|
||||
}
|
||||
|
||||
func compress(
|
||||
payloadDir: URL,
|
||||
progress: @escaping (Double?) -> Void
|
||||
) async throws -> URL {
|
||||
progress(nil)
|
||||
|
||||
let dest = payloadDir.deletingLastPathComponent().appendingPathComponent("app.ipa")
|
||||
|
||||
let zip = Process()
|
||||
zip.executableURL = URL(fileURLWithPath: "/usr/bin/env")
|
||||
zip.currentDirectoryURL = payloadDir.deletingLastPathComponent()
|
||||
zip.arguments = ["zip", "-yqru0", dest.path, "Payload"]
|
||||
try await zip.launchAndWait()
|
||||
guard zip.terminationStatus == 0 else { throw Error.compressionFailed }
|
||||
|
||||
return dest
|
||||
}
|
||||
}
|
||||
|
||||
extension Process {
|
||||
fileprivate func launchAndWait() async throws {
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
terminationHandler = { _ in
|
||||
continuation.resume()
|
||||
}
|
||||
do {
|
||||
try self.run()
|
||||
} catch {
|
||||
continuation.resume(throwing: error)
|
||||
return
|
||||
}
|
||||
Task.detached { self.waitUntilExit() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,24 +3,7 @@ import Supersign
|
||||
import ArgumentParser
|
||||
|
||||
public enum SupersignCLI {
|
||||
public struct Configuration: Sendable {
|
||||
public let superchargeApp: URL?
|
||||
public let storage: KeyValueStorage
|
||||
|
||||
public init(
|
||||
superchargeApp: URL?,
|
||||
storage: KeyValueStorage
|
||||
) {
|
||||
self.superchargeApp = superchargeApp
|
||||
self.storage = storage
|
||||
}
|
||||
}
|
||||
|
||||
private static nonisolated(unsafe) var _config: Configuration!
|
||||
static var config: Configuration { _config }
|
||||
|
||||
public static func run(configuration: Configuration, arguments: [String]? = nil) async throws {
|
||||
_config = configuration
|
||||
public static func run(arguments: [String]? = nil) async throws {
|
||||
await SupersignCommand.cancellableMain(arguments)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user