287 lines
10 KiB
Swift
287 lines
10 KiB
Swift
//
|
|
// AppleMobileDeviceAccess.swift
|
|
// World Manager for Minecraft
|
|
//
|
|
// Created by OpenAI on 2026-05-26.
|
|
//
|
|
|
|
import Foundation
|
|
|
|
struct AppleMobileDeviceSummary: Sendable {
|
|
let deviceName: String
|
|
let deviceIdentifier: String
|
|
let productType: String
|
|
let productVersion: String
|
|
let trustState: DeviceTrustState
|
|
}
|
|
|
|
struct AppleMobileDeviceApplicationSummary: Sendable {
|
|
let bundleIdentifier: String
|
|
let displayName: String
|
|
let fileSharingEnabled: Bool
|
|
let supportsOpeningDocumentsInPlace: Bool
|
|
}
|
|
|
|
struct AppleMobileMinecraftLibraryItemSummary: Sendable {
|
|
let contentType: String
|
|
let collectionFolderName: String
|
|
let relativePath: String
|
|
let folderName: String
|
|
let displayName: String
|
|
let packUUID: String?
|
|
let packVersion: String?
|
|
let minimumEngineVersion: String?
|
|
}
|
|
|
|
struct AppleMobileDevicePathMetrics: Sendable {
|
|
let sizeBytes: Int64?
|
|
let modifiedDate: Date?
|
|
}
|
|
|
|
enum AppleMobileDeviceAccess {
|
|
static func firstConnectedDevice() async throws -> AppleMobileDeviceSummary {
|
|
try await Task.detached(priority: .userInitiated) {
|
|
var error: NSError?
|
|
guard let response = WMMCopyFirstConnectedDeviceSummary(&error) else {
|
|
throw error ?? NSError(
|
|
domain: "AppleMobileDeviceAccess",
|
|
code: 1,
|
|
userInfo: [NSLocalizedDescriptionKey: "No connected device could be read from MobileDevice.framework."]
|
|
)
|
|
}
|
|
|
|
guard
|
|
let deviceName = response["deviceName"] as? String,
|
|
let deviceIdentifier = response["deviceIdentifier"] as? String,
|
|
let productType = response["productType"] as? String,
|
|
let productVersion = response["productVersion"] as? String,
|
|
let trustStateRawValue = response["trustState"] as? String,
|
|
let trustState = DeviceTrustState(rawValue: trustStateRawValue)
|
|
else {
|
|
throw NSError(
|
|
domain: "AppleMobileDeviceAccess",
|
|
code: 2,
|
|
userInfo: [NSLocalizedDescriptionKey: "The MobileDevice summary returned an unexpected payload."]
|
|
)
|
|
}
|
|
|
|
return AppleMobileDeviceSummary(
|
|
deviceName: deviceName,
|
|
deviceIdentifier: deviceIdentifier,
|
|
productType: productType,
|
|
productVersion: productVersion,
|
|
trustState: trustState
|
|
)
|
|
}.value
|
|
}
|
|
|
|
static func mirrorSubtree(
|
|
bundleIdentifier: String,
|
|
relativePath: String,
|
|
destinationDirectoryURL: URL
|
|
) async throws {
|
|
try await Task.detached(priority: .userInitiated) {
|
|
var error: NSError?
|
|
let didCopy = WMMCopyFirstConnectedDeviceAppSubtreeToLocalDirectory(
|
|
bundleIdentifier,
|
|
relativePath,
|
|
destinationDirectoryURL,
|
|
&error
|
|
)
|
|
|
|
if !didCopy {
|
|
throw error ?? NSError(
|
|
domain: "AppleMobileDeviceAccess",
|
|
code: 2,
|
|
userInfo: [NSLocalizedDescriptionKey: "The MobileDevice subtree mirror failed."]
|
|
)
|
|
}
|
|
}.value
|
|
}
|
|
|
|
static func listApplications() async throws -> [AppleMobileDeviceApplicationSummary] {
|
|
try await Task.detached(priority: .userInitiated) {
|
|
var error: NSError?
|
|
guard let response = WMMCopyFirstConnectedDeviceApplicationList(&error) else {
|
|
throw error ?? NSError(
|
|
domain: "AppleMobileDeviceAccess",
|
|
code: 3,
|
|
userInfo: [NSLocalizedDescriptionKey: "The MobileDevice application listing failed."]
|
|
)
|
|
}
|
|
|
|
guard let rawApplications = response["applications"] as? [[String: Any]] else {
|
|
throw NSError(
|
|
domain: "AppleMobileDeviceAccess",
|
|
code: 4,
|
|
userInfo: [NSLocalizedDescriptionKey: "The MobileDevice application listing returned an unexpected payload."]
|
|
)
|
|
}
|
|
|
|
return rawApplications.compactMap { application in
|
|
guard
|
|
let bundleIdentifier = application["bundleIdentifier"] as? String,
|
|
let displayName = application["displayName"] as? String
|
|
else {
|
|
return nil
|
|
}
|
|
|
|
return AppleMobileDeviceApplicationSummary(
|
|
bundleIdentifier: bundleIdentifier,
|
|
displayName: displayName,
|
|
fileSharingEnabled: flexibleBool(from: application["uiFileSharingEnabled"]),
|
|
supportsOpeningDocumentsInPlace: flexibleBool(from: application["supportsOpeningDocumentsInPlace"])
|
|
)
|
|
}
|
|
}.value
|
|
}
|
|
|
|
static func listDirectory(
|
|
bundleIdentifier: String,
|
|
relativePath: String
|
|
) async throws -> [String] {
|
|
try await Task.detached(priority: .userInitiated) {
|
|
var error: NSError?
|
|
guard let response = WMMCopyFirstConnectedDeviceAppDirectoryListing(
|
|
bundleIdentifier,
|
|
relativePath,
|
|
&error
|
|
) else {
|
|
throw error ?? NSError(
|
|
domain: "AppleMobileDeviceAccess",
|
|
code: 7,
|
|
userInfo: [NSLocalizedDescriptionKey: "The MobileDevice directory listing failed."]
|
|
)
|
|
}
|
|
|
|
return (response["entries"] as? [String] ?? []).filter { $0 != "." && $0 != ".." }
|
|
}.value
|
|
}
|
|
|
|
static func fileData(
|
|
bundleIdentifier: String,
|
|
relativePath: String
|
|
) async throws -> Data {
|
|
try await Task.detached(priority: .userInitiated) {
|
|
var error: NSError?
|
|
guard let data = WMMCopyFirstConnectedDeviceAppFileData(
|
|
bundleIdentifier,
|
|
relativePath,
|
|
&error
|
|
) else {
|
|
throw error ?? NSError(
|
|
domain: "AppleMobileDeviceAccess",
|
|
code: 8,
|
|
userInfo: [NSLocalizedDescriptionKey: "The MobileDevice file read failed."]
|
|
)
|
|
}
|
|
|
|
return data as Data
|
|
}.value
|
|
}
|
|
|
|
static func minecraftLibrarySnapshot(
|
|
bundleIdentifier: String,
|
|
relativePath: String
|
|
) async throws -> [AppleMobileMinecraftLibraryItemSummary] {
|
|
try await Task.detached(priority: .userInitiated) {
|
|
var error: NSError?
|
|
guard let response = WMMCopyFirstConnectedDeviceMinecraftLibrarySnapshot(
|
|
bundleIdentifier,
|
|
relativePath,
|
|
&error
|
|
) else {
|
|
throw error ?? NSError(
|
|
domain: "AppleMobileDeviceAccess",
|
|
code: 5,
|
|
userInfo: [NSLocalizedDescriptionKey: "The MobileDevice Minecraft library scan failed."]
|
|
)
|
|
}
|
|
|
|
guard let rawItems = response["items"] as? [[String: Any]] else {
|
|
throw NSError(
|
|
domain: "AppleMobileDeviceAccess",
|
|
code: 6,
|
|
userInfo: [NSLocalizedDescriptionKey: "The MobileDevice Minecraft library scan returned an unexpected payload."]
|
|
)
|
|
}
|
|
|
|
return rawItems.compactMap { item in
|
|
guard
|
|
let contentType = item["contentType"] as? String,
|
|
let collectionFolderName = item["collectionFolderName"] as? String,
|
|
let relativePath = item["relativePath"] as? String,
|
|
let folderName = item["folderName"] as? String,
|
|
let displayName = item["displayName"] as? String
|
|
else {
|
|
return nil
|
|
}
|
|
|
|
return AppleMobileMinecraftLibraryItemSummary(
|
|
contentType: contentType,
|
|
collectionFolderName: collectionFolderName,
|
|
relativePath: relativePath,
|
|
folderName: folderName,
|
|
displayName: displayName,
|
|
packUUID: (item["packUUID"] as? String)?.lowercased(),
|
|
packVersion: item["packVersion"] as? String,
|
|
minimumEngineVersion: item["minimumEngineVersion"] as? String
|
|
)
|
|
}
|
|
}.value
|
|
}
|
|
|
|
static func pathMetrics(
|
|
bundleIdentifier: String,
|
|
relativePath: String
|
|
) async throws -> AppleMobileDevicePathMetrics {
|
|
try await Task.detached(priority: .utility) {
|
|
var error: NSError?
|
|
guard let response = WMMCopyFirstConnectedDeviceAppPathMetrics(
|
|
bundleIdentifier,
|
|
relativePath,
|
|
&error
|
|
) else {
|
|
throw error ?? NSError(
|
|
domain: "AppleMobileDeviceAccess",
|
|
code: 9,
|
|
userInfo: [NSLocalizedDescriptionKey: "The MobileDevice path metrics lookup failed."]
|
|
)
|
|
}
|
|
|
|
let rawSize = response["sizeBytes"]
|
|
let sizeBytes: Int64?
|
|
switch rawSize {
|
|
case let number as NSNumber:
|
|
sizeBytes = number.int64Value
|
|
case let value as Int64:
|
|
sizeBytes = value
|
|
case let value as Int:
|
|
sizeBytes = Int64(value)
|
|
default:
|
|
sizeBytes = nil
|
|
}
|
|
|
|
return AppleMobileDevicePathMetrics(
|
|
sizeBytes: sizeBytes,
|
|
modifiedDate: response["modifiedDate"] as? Date
|
|
)
|
|
}.value
|
|
}
|
|
|
|
private static func flexibleBool(from value: Any?) -> Bool {
|
|
switch value {
|
|
case let value as Bool:
|
|
return value
|
|
case let value as NSNumber:
|
|
return value.boolValue
|
|
case let value as NSString:
|
|
return value.boolValue
|
|
case let value as String:
|
|
return NSString(string: value).boolValue
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
}
|