world-manager/World Manager for Minecraft/SourceAccess/ConnectedDevice/AppleMobileDevice/AppleMobileDeviceBridge.m

2388 lines
87 KiB
Objective-C

//
// AppleMobileDeviceBridge.m
// World Manager for Minecraft
//
// Created by OpenAI on 2026-05-26.
//
#import "AppleMobileDeviceBridge.h"
#import <CoreFoundation/CoreFoundation.h>
#import <dlfcn.h>
#import <limits.h>
NSErrorDomain const WMMMobileDeviceErrorDomain = @"WMMMobileDeviceErrorDomain";
static BOOL WMMMobileDeviceVerboseLoggingEnabled(void) {
static BOOL initialized = NO;
static BOOL enabled = NO;
if (!initialized) {
NSString *value = [[[NSProcessInfo processInfo] environment][@"WMM_MOBILEDEVICE_VERBOSE_LOGGING"] lowercaseString];
enabled = [value isEqualToString:@"1"] || [value isEqualToString:@"true"] || [value isEqualToString:@"yes"];
initialized = YES;
}
return enabled;
}
#define WMMBridgeLog(...) do { \
if (WMMMobileDeviceVerboseLoggingEnabled()) { \
NSLog(__VA_ARGS__); \
} \
} while (0)
typedef struct am_device *AMDeviceRef;
typedef struct am_device_notification *AMDeviceNotificationRef;
typedef struct amd_service_connection *AMDServiceConnectionRef;
typedef struct afc_connection *AFCConnectionRef;
typedef struct afc_directory *AFCDirectoryRef;
typedef CFTypeRef AFCFileDescriptorRef;
typedef CFTypeRef AFCIteratorRef;
typedef uint32_t service_conn_t;
struct am_device_notification_callback_info {
AMDeviceRef dev;
unsigned int msg;
};
typedef void (*AMDeviceNotificationCallback)(
struct am_device_notification_callback_info *info,
void *context
);
enum {
WMMAMDConnectedMessage = 1
};
typedef int (*AMDeviceNotificationSubscribeFn)(
AMDeviceNotificationCallback callback,
int unused1,
int unused2,
void *context,
AMDeviceNotificationRef *subscription
);
typedef int (*AMDeviceNotificationUnsubscribeFn)(AMDeviceNotificationRef subscription);
typedef AMDeviceRef (*AMDeviceRetainFn)(AMDeviceRef device);
typedef int (*AMDeviceReleaseFn)(AMDeviceRef device);
typedef int (*AMDeviceConnectFn)(AMDeviceRef device);
typedef int (*AMDeviceDisconnectFn)(AMDeviceRef device);
typedef int (*AMDeviceIsPairedFn)(AMDeviceRef device);
typedef int (*AMDeviceValidatePairingFn)(AMDeviceRef device);
typedef int (*AMDeviceStartSessionFn)(AMDeviceRef device);
typedef int (*AMDeviceStopSessionFn)(AMDeviceRef device);
typedef CFStringRef _Nullable (*AMDeviceCopyDeviceIdentifierFn)(AMDeviceRef device);
typedef CFTypeRef _Nullable (*AMDeviceCopyValueFn)(AMDeviceRef device, CFStringRef _Nullable domain, CFStringRef name);
typedef int (*AMDeviceLookupApplicationsFn)(
AMDeviceRef device,
CFDictionaryRef _Nullable options,
CFDictionaryRef _Nullable *result
);
typedef int (*AMDeviceCreateHouseArrestServiceFn)(
AMDeviceRef device,
CFStringRef identifier,
CFDictionaryRef _Nullable options,
AFCConnectionRef _Nullable *connection
);
typedef int (*AMDeviceSecureStartServiceFn)(
AMDeviceRef device,
CFStringRef serviceName,
CFDictionaryRef _Nullable options,
AMDServiceConnectionRef _Nullable *serviceConnection
);
typedef int (*AMDeviceStartHouseArrestServiceFn)(
AMDeviceRef device,
CFStringRef identifier,
CFDictionaryRef _Nullable options,
service_conn_t *handle,
void * _Nullable *secureContext
);
typedef int (*AMDServiceConnectionSendMessageFn)(
AMDServiceConnectionRef serviceConnection,
CFPropertyListRef message,
int timeout
);
typedef int (*AMDServiceConnectionReceiveMessageFn)(
AMDServiceConnectionRef serviceConnection,
CFPropertyListRef _Nullable *message,
int timeout
);
typedef void (*AMDServiceConnectionInvalidateFn)(AMDServiceConnectionRef serviceConnection);
typedef int (*AMDServiceConnectionGetSocketFn)(AMDServiceConnectionRef serviceConnection);
typedef void * _Nullable (*AMDServiceConnectionGetSecureIOContextFn)(AMDServiceConnectionRef serviceConnection);
typedef AFCConnectionRef _Nullable (*AFCConnectionCreateFn)(
CFAllocatorRef allocator,
int socket,
uint32_t unused1,
void * _Nullable unused2,
void * _Nullable unused3
);
typedef void (*AFCConnectionSetSecureContextFn)(AFCConnectionRef connection, void * _Nullable secureContext);
typedef int (*AFCConnectionOpenFn)(service_conn_t handle, unsigned int ioTimeout, AFCConnectionRef *connection);
typedef int (*AFCConnectionCloseFn)(AFCConnectionRef connection);
typedef int (*AFCDirectoryOpenFn)(AFCConnectionRef connection, const char *path, AFCDirectoryRef *directory);
typedef int (*AFCDirectoryReadFn)(AFCConnectionRef connection, AFCDirectoryRef directory, char **directoryEntry);
typedef int (*AFCDirectoryCloseFn)(AFCConnectionRef connection, AFCDirectoryRef directory);
typedef int (*AFCFileInfoOpenFn)(AFCConnectionRef connection, const char *path, AFCIteratorRef *iterator);
typedef int (*AFCKeyValueReadFn)(AFCIteratorRef iterator, char **key, char **value);
typedef int (*AFCKeyValueCloseFn)(AFCIteratorRef iterator);
typedef int (*AFCFileRefOpenFn)(AFCConnectionRef connection, const char *path, uint64_t mode, AFCFileDescriptorRef *fileDescriptor);
typedef int (*AFCFileRefReadFn)(AFCConnectionRef connection, AFCFileDescriptorRef fileDescriptor, void *buffer, size_t *length);
typedef int (*AFCFileRefCloseFn)(AFCConnectionRef connection, AFCFileDescriptorRef fileDescriptor);
typedef struct {
void *handle;
AMDeviceNotificationSubscribeFn AMDeviceNotificationSubscribe;
AMDeviceNotificationUnsubscribeFn AMDeviceNotificationUnsubscribe;
AMDeviceRetainFn AMDeviceRetain;
AMDeviceReleaseFn AMDeviceRelease;
AMDeviceConnectFn AMDeviceConnect;
AMDeviceDisconnectFn AMDeviceDisconnect;
AMDeviceIsPairedFn AMDeviceIsPaired;
AMDeviceValidatePairingFn AMDeviceValidatePairing;
AMDeviceStartSessionFn AMDeviceStartSession;
AMDeviceStopSessionFn AMDeviceStopSession;
AMDeviceCopyDeviceIdentifierFn AMDeviceCopyDeviceIdentifier;
AMDeviceCopyValueFn AMDeviceCopyValue;
AMDeviceLookupApplicationsFn AMDeviceLookupApplications;
AMDeviceCreateHouseArrestServiceFn AMDeviceCreateHouseArrestService;
AMDeviceSecureStartServiceFn AMDeviceSecureStartService;
AMDeviceStartHouseArrestServiceFn AMDeviceStartHouseArrestService;
AMDServiceConnectionSendMessageFn AMDServiceConnectionSendMessage;
AMDServiceConnectionReceiveMessageFn AMDServiceConnectionReceiveMessage;
AMDServiceConnectionInvalidateFn AMDServiceConnectionInvalidate;
AMDServiceConnectionGetSocketFn AMDServiceConnectionGetSocket;
AMDServiceConnectionGetSecureIOContextFn AMDServiceConnectionGetSecureIOContext;
AFCConnectionCreateFn AFCConnectionCreate;
AFCConnectionSetSecureContextFn AFCConnectionSetSecureContext;
AFCConnectionOpenFn AFCConnectionOpen;
AFCConnectionCloseFn AFCConnectionClose;
AFCDirectoryOpenFn AFCDirectoryOpen;
AFCDirectoryReadFn AFCDirectoryRead;
AFCDirectoryCloseFn AFCDirectoryClose;
AFCFileInfoOpenFn AFCFileInfoOpen;
AFCKeyValueReadFn AFCKeyValueRead;
AFCKeyValueCloseFn AFCKeyValueClose;
AFCFileRefOpenFn AFCFileRefOpen;
AFCFileRefReadFn AFCFileRefRead;
AFCFileRefCloseFn AFCFileRefClose;
} WMMMobileDeviceFunctions;
typedef struct {
WMMMobileDeviceFunctions *functions;
CFRunLoopRef runLoop;
NSMutableArray<NSValue *> *devices;
} WMMDeviceCollectionContext;
static NSString *WMMDeviceStringValue(WMMMobileDeviceFunctions *functions, AMDeviceRef device, CFStringRef key);
static NSInteger WMMConnectionPreferenceRank(AMDeviceRef device);
static NSError *WMMMakeError(NSInteger code, NSString *description) {
return [NSError errorWithDomain:WMMMobileDeviceErrorDomain code:code userInfo:@{
NSLocalizedDescriptionKey: description
}];
}
static void *WMMLoadSymbol(void *handle, const char *name) {
return dlsym(handle, name);
}
static BOOL WMMLoadFunctions(WMMMobileDeviceFunctions *functions, NSError **error) {
static const char *paths[] = {
"/Library/Apple/System/Library/PrivateFrameworks/MobileDevice.framework/MobileDevice",
"/System/Library/PrivateFrameworks/MobileDevice.framework/MobileDevice"
};
void *frameworkHandle = NULL;
for (size_t index = 0; index < sizeof(paths) / sizeof(paths[0]); index += 1) {
frameworkHandle = dlopen(paths[index], RTLD_NOW | RTLD_LOCAL);
if (frameworkHandle != NULL) {
break;
}
}
if (frameworkHandle == NULL) {
if (error != NULL) {
*error = WMMMakeError(1, @"MobileDevice.framework is not available.");
}
return NO;
}
memset(functions, 0, sizeof(*functions));
functions->handle = frameworkHandle;
functions->AMDeviceNotificationSubscribe = (AMDeviceNotificationSubscribeFn)WMMLoadSymbol(frameworkHandle, "AMDeviceNotificationSubscribe");
functions->AMDeviceNotificationUnsubscribe = (AMDeviceNotificationUnsubscribeFn)WMMLoadSymbol(frameworkHandle, "AMDeviceNotificationUnsubscribe");
functions->AMDeviceRetain = (AMDeviceRetainFn)WMMLoadSymbol(frameworkHandle, "AMDeviceRetain");
functions->AMDeviceRelease = (AMDeviceReleaseFn)WMMLoadSymbol(frameworkHandle, "AMDeviceRelease");
functions->AMDeviceConnect = (AMDeviceConnectFn)WMMLoadSymbol(frameworkHandle, "AMDeviceConnect");
functions->AMDeviceDisconnect = (AMDeviceDisconnectFn)WMMLoadSymbol(frameworkHandle, "AMDeviceDisconnect");
functions->AMDeviceIsPaired = (AMDeviceIsPairedFn)WMMLoadSymbol(frameworkHandle, "AMDeviceIsPaired");
functions->AMDeviceValidatePairing = (AMDeviceValidatePairingFn)WMMLoadSymbol(frameworkHandle, "AMDeviceValidatePairing");
functions->AMDeviceStartSession = (AMDeviceStartSessionFn)WMMLoadSymbol(frameworkHandle, "AMDeviceStartSession");
functions->AMDeviceStopSession = (AMDeviceStopSessionFn)WMMLoadSymbol(frameworkHandle, "AMDeviceStopSession");
functions->AMDeviceCopyDeviceIdentifier = (AMDeviceCopyDeviceIdentifierFn)WMMLoadSymbol(frameworkHandle, "AMDeviceCopyDeviceIdentifier");
functions->AMDeviceCopyValue = (AMDeviceCopyValueFn)WMMLoadSymbol(frameworkHandle, "AMDeviceCopyValue");
functions->AMDeviceLookupApplications = (AMDeviceLookupApplicationsFn)WMMLoadSymbol(frameworkHandle, "AMDeviceLookupApplications");
functions->AMDeviceCreateHouseArrestService = (AMDeviceCreateHouseArrestServiceFn)WMMLoadSymbol(frameworkHandle, "AMDeviceCreateHouseArrestService");
functions->AMDeviceSecureStartService = (AMDeviceSecureStartServiceFn)WMMLoadSymbol(frameworkHandle, "AMDeviceSecureStartService");
functions->AMDeviceStartHouseArrestService = (AMDeviceStartHouseArrestServiceFn)WMMLoadSymbol(frameworkHandle, "AMDeviceStartHouseArrestService");
functions->AMDServiceConnectionSendMessage = (AMDServiceConnectionSendMessageFn)WMMLoadSymbol(frameworkHandle, "AMDServiceConnectionSendMessage");
functions->AMDServiceConnectionReceiveMessage = (AMDServiceConnectionReceiveMessageFn)WMMLoadSymbol(frameworkHandle, "AMDServiceConnectionReceiveMessage");
functions->AMDServiceConnectionInvalidate = (AMDServiceConnectionInvalidateFn)WMMLoadSymbol(frameworkHandle, "AMDServiceConnectionInvalidate");
functions->AMDServiceConnectionGetSocket = (AMDServiceConnectionGetSocketFn)WMMLoadSymbol(frameworkHandle, "AMDServiceConnectionGetSocket");
functions->AMDServiceConnectionGetSecureIOContext = (AMDServiceConnectionGetSecureIOContextFn)WMMLoadSymbol(frameworkHandle, "AMDServiceConnectionGetSecureIOContext");
functions->AFCConnectionCreate = (AFCConnectionCreateFn)WMMLoadSymbol(frameworkHandle, "AFCConnectionCreate");
functions->AFCConnectionSetSecureContext = (AFCConnectionSetSecureContextFn)WMMLoadSymbol(frameworkHandle, "AFCConnectionSetSecureContext");
functions->AFCConnectionOpen = (AFCConnectionOpenFn)WMMLoadSymbol(frameworkHandle, "AFCConnectionOpen");
functions->AFCConnectionClose = (AFCConnectionCloseFn)WMMLoadSymbol(frameworkHandle, "AFCConnectionClose");
functions->AFCDirectoryOpen = (AFCDirectoryOpenFn)WMMLoadSymbol(frameworkHandle, "AFCDirectoryOpen");
functions->AFCDirectoryRead = (AFCDirectoryReadFn)WMMLoadSymbol(frameworkHandle, "AFCDirectoryRead");
functions->AFCDirectoryClose = (AFCDirectoryCloseFn)WMMLoadSymbol(frameworkHandle, "AFCDirectoryClose");
functions->AFCFileInfoOpen = (AFCFileInfoOpenFn)WMMLoadSymbol(frameworkHandle, "AFCFileInfoOpen");
functions->AFCKeyValueRead = (AFCKeyValueReadFn)WMMLoadSymbol(frameworkHandle, "AFCKeyValueRead");
functions->AFCKeyValueClose = (AFCKeyValueCloseFn)WMMLoadSymbol(frameworkHandle, "AFCKeyValueClose");
functions->AFCFileRefOpen = (AFCFileRefOpenFn)WMMLoadSymbol(frameworkHandle, "AFCFileRefOpen");
functions->AFCFileRefRead = (AFCFileRefReadFn)WMMLoadSymbol(frameworkHandle, "AFCFileRefRead");
functions->AFCFileRefClose = (AFCFileRefCloseFn)WMMLoadSymbol(frameworkHandle, "AFCFileRefClose");
if (functions->AMDeviceNotificationSubscribe == NULL ||
functions->AMDeviceNotificationUnsubscribe == NULL ||
functions->AMDeviceRetain == NULL ||
functions->AMDeviceRelease == NULL ||
functions->AMDeviceConnect == NULL ||
functions->AMDeviceDisconnect == NULL ||
functions->AMDeviceIsPaired == NULL ||
functions->AMDeviceValidatePairing == NULL ||
functions->AMDeviceStartSession == NULL ||
functions->AMDeviceStopSession == NULL ||
functions->AMDeviceCopyDeviceIdentifier == NULL ||
functions->AMDeviceCopyValue == NULL ||
functions->AMDeviceLookupApplications == NULL ||
functions->AMDeviceCreateHouseArrestService == NULL ||
functions->AMDeviceSecureStartService == NULL ||
functions->AMDeviceStartHouseArrestService == NULL ||
functions->AMDServiceConnectionSendMessage == NULL ||
functions->AMDServiceConnectionReceiveMessage == NULL ||
functions->AMDServiceConnectionInvalidate == NULL ||
functions->AMDServiceConnectionGetSocket == NULL ||
functions->AMDServiceConnectionGetSecureIOContext == NULL ||
functions->AFCConnectionCreate == NULL ||
functions->AFCConnectionSetSecureContext == NULL ||
functions->AFCConnectionOpen == NULL ||
functions->AFCConnectionClose == NULL ||
functions->AFCDirectoryOpen == NULL ||
functions->AFCDirectoryRead == NULL ||
functions->AFCDirectoryClose == NULL ||
functions->AFCFileInfoOpen == NULL ||
functions->AFCKeyValueRead == NULL ||
functions->AFCKeyValueClose == NULL ||
functions->AFCFileRefOpen == NULL ||
functions->AFCFileRefRead == NULL ||
functions->AFCFileRefClose == NULL) {
if (error != NULL) {
*error = WMMMakeError(2, @"MobileDevice.framework symbols could not be loaded.");
}
dlclose(frameworkHandle);
memset(functions, 0, sizeof(*functions));
return NO;
}
return YES;
}
static void WMMDeviceNotificationCallback(struct am_device_notification_callback_info *info, void *contextPointer) {
if (info == NULL || contextPointer == NULL || info->msg != WMMAMDConnectedMessage) {
return;
}
WMMDeviceCollectionContext *context = contextPointer;
AMDeviceRef retainedDevice = context->functions->AMDeviceRetain(info->dev);
[context->devices addObject:[NSValue valueWithPointer:retainedDevice]];
}
static NSArray<NSValue *> *WMMCopyConnectedDevices(WMMMobileDeviceFunctions *functions, NSError **error) {
WMMDeviceCollectionContext context = {
.functions = functions,
.runLoop = CFRunLoopGetCurrent(),
.devices = [NSMutableArray array]
};
AMDeviceNotificationRef subscription = NULL;
const int subscribeStatus = functions->AMDeviceNotificationSubscribe(
WMMDeviceNotificationCallback,
0,
0,
&context,
&subscription
);
if (subscribeStatus != 0) {
if (error != NULL) {
*error = WMMMakeError(3, @"No connected iPhone or iPad was detected through MobileDevice.framework.");
}
return NULL;
}
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.2, false);
functions->AMDeviceNotificationUnsubscribe(subscription);
if (context.devices.count == 0 && error != NULL) {
*error = WMMMakeError(3, @"No connected iPhone or iPad was detected through MobileDevice.framework.");
}
return [context.devices copy];
}
static NSString *WMMResolvedDeviceIdentifier(WMMMobileDeviceFunctions *functions, AMDeviceRef device) {
if (functions->AMDeviceCopyDeviceIdentifier != NULL) {
CFStringRef copiedIdentifier = functions->AMDeviceCopyDeviceIdentifier(device);
if (copiedIdentifier != NULL) {
return CFBridgingRelease(copiedIdentifier);
}
}
return WMMDeviceStringValue(functions, device, CFSTR("UniqueDeviceID")) ?:
WMMDeviceStringValue(functions, device, CFSTR("SerialNumber")) ?:
@"";
}
static void WMMReleaseDeviceValues(WMMMobileDeviceFunctions *functions, NSArray<NSValue *> *devices) {
for (NSValue *value in devices) {
AMDeviceRef device = (AMDeviceRef)value.pointerValue;
if (device != NULL) {
functions->AMDeviceRelease(device);
}
}
}
static AMDeviceRef WMMCopyConnectedDevice(
WMMMobileDeviceFunctions *functions,
NSString *targetDeviceIdentifier,
NSError **error
) {
NSArray<NSValue *> *devices = WMMCopyConnectedDevices(functions, error);
if (devices.count == 0) {
return NULL;
}
AMDeviceRef matchedDevice = NULL;
for (NSValue *value in devices) {
AMDeviceRef device = (AMDeviceRef)value.pointerValue;
NSString *deviceIdentifier = WMMResolvedDeviceIdentifier(functions, device);
BOOL matchesTarget = targetDeviceIdentifier.length == 0 || [deviceIdentifier isEqualToString:targetDeviceIdentifier];
if (!matchesTarget) {
if (device != NULL) {
functions->AMDeviceRelease(device);
}
continue;
}
if (matchedDevice == NULL) {
matchedDevice = device;
continue;
}
if (WMMConnectionPreferenceRank(device) > WMMConnectionPreferenceRank(matchedDevice)) {
functions->AMDeviceRelease(matchedDevice);
matchedDevice = device;
continue;
}
if (device != NULL) {
functions->AMDeviceRelease(device);
}
}
if (matchedDevice == NULL && error != NULL) {
*error = WMMMakeError(3, [NSString stringWithFormat:@"The connected device %@ is no longer available.", targetDeviceIdentifier]);
}
return matchedDevice;
}
static NSString *WMMDeviceStringValue(WMMMobileDeviceFunctions *functions, AMDeviceRef device, CFStringRef key) {
if (functions->AMDeviceCopyValue == NULL) {
return nil;
}
CFTypeRef value = functions->AMDeviceCopyValue(device, NULL, key);
if (value == NULL) {
return nil;
}
if (CFGetTypeID(value) != CFStringGetTypeID()) {
CFRelease(value);
return nil;
}
return CFBridgingRelease(value);
}
static NSString *WMMInferredConnectionType(AMDeviceRef device) {
if (device == NULL) {
return @"USB";
}
NSString *deviceDescription = [(__bridge id)device description];
if (deviceDescription.length == 0) {
return @"USB";
}
if ([deviceDescription containsString:@"FullServiceName = "]) {
return @"Network";
}
if ([deviceDescription containsString:@"location ID = "]) {
return @"USB";
}
return @"USB";
}
static NSInteger WMMConnectionPreferenceRank(AMDeviceRef device) {
NSString *connectionType = WMMInferredConnectionType(device);
if ([connectionType caseInsensitiveCompare:@"USB"] == NSOrderedSame) {
return 2;
}
if ([connectionType caseInsensitiveCompare:@"Network"] == NSOrderedSame) {
return 1;
}
return 0;
}
static void WMMLogDeviceTransportDiagnostics(
WMMMobileDeviceFunctions *functions,
AMDeviceRef device,
NSString *resolvedIdentifier
) {
NSArray<NSString *> *keys = @[
@"ConnectionType",
@"InterfaceType",
@"DeviceName",
@"ProductType",
@"ProductVersion",
@"UniqueDeviceID",
@"SerialNumber",
@"WiFiAddress",
@"EthernetAddress"
];
NSMutableDictionary<NSString *, NSString *> *values = [NSMutableDictionary dictionary];
for (NSString *key in keys) {
NSString *value = WMMDeviceStringValue(functions, device, (__bridge CFStringRef)key);
values[key] = value.length > 0 ? value : @"<nil>";
}
WMMBridgeLog(@"[DeviceSummary] udid=%@ diagnostics=%@", resolvedIdentifier, values);
}
static BOOL WMMConnectAndValidateDevice(
WMMMobileDeviceFunctions *functions,
AMDeviceRef device,
BOOL startSession,
NSError **error
) {
const int connectStatus = functions->AMDeviceConnect(device);
if (connectStatus != 0) {
if (error != NULL) {
*error = WMMMakeError(connectStatus, [NSString stringWithFormat:@"AMDeviceConnect failed (%d).", connectStatus]);
}
return NO;
}
if (!functions->AMDeviceIsPaired(device)) {
functions->AMDeviceDisconnect(device);
if (error != NULL) {
*error = WMMMakeError(5, @"The connected device is not paired with this Mac.");
}
return NO;
}
const int validateStatus = functions->AMDeviceValidatePairing(device);
if (validateStatus != 0) {
functions->AMDeviceDisconnect(device);
if (error != NULL) {
*error = WMMMakeError(validateStatus, [NSString stringWithFormat:@"AMDeviceValidatePairing failed (%d).", validateStatus]);
}
return NO;
}
if (!startSession) {
return YES;
}
const int sessionStatus = functions->AMDeviceStartSession(device);
if (sessionStatus != 0) {
functions->AMDeviceDisconnect(device);
if (error != NULL) {
*error = WMMMakeError(sessionStatus, [NSString stringWithFormat:@"AMDeviceStartSession failed (%d).", sessionStatus]);
}
return NO;
}
return YES;
}
static void WMMDisconnectDevice(
WMMMobileDeviceFunctions *functions,
AMDeviceRef device,
BOOL hadSession
) {
if (hadSession) {
functions->AMDeviceStopSession(device);
}
functions->AMDeviceDisconnect(device);
}
static void WMMCloseVendSession(
WMMMobileDeviceFunctions *functions,
AMDeviceRef _Nullable device,
BOOL hadSession,
AFCConnectionRef _Nullable afcConnection,
AMDServiceConnectionRef _Nullable backingServiceConnection
) {
if (afcConnection != NULL) {
functions->AFCConnectionClose(afcConnection);
}
if (backingServiceConnection != NULL) {
functions->AMDServiceConnectionInvalidate(backingServiceConnection);
}
if (device != NULL) {
WMMDisconnectDevice(functions, device, hadSession);
}
}
static AFCConnectionRef _Nullable WMMCreateAFCConnectionFromServiceConnection(
WMMMobileDeviceFunctions *functions,
AMDServiceConnectionRef serviceConnection
) {
int socket = functions->AMDServiceConnectionGetSocket(serviceConnection);
if (socket < 0) {
return NULL;
}
AFCConnectionRef connection = functions->AFCConnectionCreate(
kCFAllocatorDefault,
socket,
1,
NULL,
NULL
);
if (connection == NULL) {
return NULL;
}
void *secureContext = functions->AMDServiceConnectionGetSecureIOContext(serviceConnection);
if (secureContext != NULL) {
functions->AFCConnectionSetSecureContext(connection, secureContext);
}
return connection;
}
static AFCConnectionRef _Nullable WMMCreateVendAFCConnection(
WMMMobileDeviceFunctions *functions,
AMDeviceRef device,
NSString *bundleIdentifier,
AMDServiceConnectionRef _Nullable * _Nullable backingServiceConnection,
NSError **error
) {
if (backingServiceConnection != NULL) {
*backingServiceConnection = NULL;
}
WMMBridgeLog(@"[HouseArrest] Trying AMDeviceCreateHouseArrestService for %@", bundleIdentifier);
AFCConnectionRef directConnection = NULL;
int directStatus = functions->AMDeviceCreateHouseArrestService(
device,
(__bridge CFStringRef)bundleIdentifier,
NULL,
&directConnection
);
WMMBridgeLog(@"[HouseArrest] AMDeviceCreateHouseArrestService returned %d connection=%p", directStatus, directConnection);
if (directStatus == 0 && directConnection != NULL) {
return directConnection;
}
NSArray<NSString *> *commands = @[ @"VendDocuments", @"VendContainer" ];
NSMutableArray<NSString *> *failures = [NSMutableArray array];
[failures addObject:[NSString stringWithFormat:@"AMDeviceCreateHouseArrestService returned %d", directStatus]];
for (NSString *command in commands) {
WMMBridgeLog(@"[HouseArrest] Starting %@ for %@", command, bundleIdentifier);
AMDServiceConnectionRef serviceConnection = NULL;
int startStatus = functions->AMDeviceSecureStartService(
device,
CFSTR("com.apple.mobile.house_arrest"),
NULL,
&serviceConnection
);
if (startStatus != 0 || serviceConnection == NULL) {
WMMBridgeLog(@"[HouseArrest] %@ service start failed: %d", command, startStatus);
[failures addObject:[NSString stringWithFormat:@"%@ service start failed (%d)", command, startStatus]];
continue;
}
int socket = functions->AMDServiceConnectionGetSocket(serviceConnection);
void *secureContext = functions->AMDServiceConnectionGetSecureIOContext(serviceConnection);
WMMBridgeLog(@"[HouseArrest] %@ service connection socket=%d secureContext=%p", command, socket, secureContext);
NSDictionary *request = @{
@"Command": command,
@"Identifier": bundleIdentifier
};
int sent = functions->AMDServiceConnectionSendMessage(
serviceConnection,
(__bridge CFPropertyListRef)request,
100
);
WMMBridgeLog(@"[HouseArrest] %@ send returned %d", command, sent);
if (sent != 0) {
[failures addObject:[NSString stringWithFormat:@"%@ request failed to send (%d)", command, sent]];
functions->AMDServiceConnectionInvalidate(serviceConnection);
continue;
}
CFPropertyListRef response = NULL;
int received = functions->AMDServiceConnectionReceiveMessage(
serviceConnection,
&response,
0
);
WMMBridgeLog(@"[HouseArrest] %@ receive returned %d", command, received);
if (received != 0 || response == NULL) {
[failures addObject:[NSString stringWithFormat:@"%@ response could not be read (%d)", command, received]];
functions->AMDServiceConnectionInvalidate(serviceConnection);
continue;
}
NSDictionary *responseDictionary = CFBridgingRelease(response);
WMMBridgeLog(@"[HouseArrest] %@ response: %@", command, responseDictionary);
NSString *status = [responseDictionary isKindOfClass:[NSDictionary class]] ? responseDictionary[@"Status"] : nil;
if ([status isKindOfClass:[NSString class]] && [status isEqualToString:@"Complete"]) {
AFCConnectionRef afcConnection = WMMCreateAFCConnectionFromServiceConnection(functions, serviceConnection);
if (afcConnection != NULL) {
if (backingServiceConnection != NULL) {
*backingServiceConnection = serviceConnection;
}
WMMBridgeLog(@"[HouseArrest] %@ completed and AFC initialized", command);
return afcConnection;
}
functions->AMDServiceConnectionInvalidate(serviceConnection);
WMMBridgeLog(@"[HouseArrest] %@ completed but AFC initialization failed", command);
[failures addObject:[NSString stringWithFormat:@"%@ succeeded but AFC initialization failed", command]];
break;
}
NSString *serviceError = [responseDictionary isKindOfClass:[NSDictionary class]] ? responseDictionary[@"Error"] : nil;
if ([serviceError isKindOfClass:[NSString class]] && serviceError.length > 0) {
WMMBridgeLog(@"[HouseArrest] %@ rejected with error: %@", command, serviceError);
[failures addObject:[NSString stringWithFormat:@"%@ was rejected: %@", command, serviceError]];
} else {
WMMBridgeLog(@"[HouseArrest] %@ did not complete", command);
[failures addObject:[NSString stringWithFormat:@"%@ did not complete", command]];
}
functions->AMDServiceConnectionInvalidate(serviceConnection);
}
if (error != NULL) {
NSString *failureSummary = failures.count > 0
? [failures componentsJoinedByString:@"; "]
: @"House Arrest vend request failed.";
*error = WMMMakeError(11, failureSummary);
}
return NULL;
}
static int WMMReadAFCDirectory(
WMMMobileDeviceFunctions *functions,
AFCConnectionRef afcConnection,
NSString *path,
NSMutableArray<NSString *> **entriesOut
) {
NSString *effectivePath = path;
if (effectivePath.length == 0) {
effectivePath = @".";
}
AFCDirectoryRef directory = NULL;
const int openDirectoryStatus = functions->AFCDirectoryOpen(
afcConnection,
effectivePath.fileSystemRepresentation,
&directory
);
if (openDirectoryStatus != 0 || directory == NULL) {
return openDirectoryStatus != 0 ? openDirectoryStatus : -1;
}
NSMutableArray<NSString *> *entries = [NSMutableArray array];
int result = 0;
while (true) {
char *entry = NULL;
const int readStatus = functions->AFCDirectoryRead(afcConnection, directory, &entry);
if (readStatus != 0) {
result = readStatus;
break;
}
if (entry == NULL) {
break;
}
NSString *entryName = [NSString stringWithUTF8String:entry];
if (entryName.length > 0) {
[entries addObject:entryName];
}
}
functions->AFCDirectoryClose(afcConnection, directory);
if (result == 0 && entriesOut != NULL) {
*entriesOut = entries;
}
return result;
}
static NSDictionary<NSString *, NSString *> * _Nullable WMMCopyAFCFileInfo(
WMMMobileDeviceFunctions *functions,
AFCConnectionRef afcConnection,
NSString *path,
NSError **error
) {
AFCIteratorRef iterator = NULL;
const int openStatus = functions->AFCFileInfoOpen(
afcConnection,
path.fileSystemRepresentation,
&iterator
);
if (openStatus != 0 || iterator == NULL) {
if (error != NULL) {
*error = WMMMakeError(openStatus, [NSString stringWithFormat:@"AFCFileInfoOpen failed for %@ (%d).", path, openStatus]);
}
return nil;
}
NSMutableDictionary<NSString *, NSString *> *info = [NSMutableDictionary dictionary];
while (true) {
char *key = NULL;
char *value = NULL;
const int readStatus = functions->AFCKeyValueRead(iterator, &key, &value);
if (readStatus != 0) {
functions->AFCKeyValueClose(iterator);
if (error != NULL) {
*error = WMMMakeError(readStatus, [NSString stringWithFormat:@"AFCKeyValueRead failed for %@ (%d).", path, readStatus]);
}
return nil;
}
if (key == NULL || value == NULL) {
break;
}
NSString *keyString = [NSString stringWithUTF8String:key];
NSString *valueString = [NSString stringWithUTF8String:value];
if (keyString.length > 0 && valueString.length > 0) {
info[keyString] = valueString;
}
}
functions->AFCKeyValueClose(iterator);
return info;
}
static unsigned long long WMMParseUnsignedLongLong(NSString *value) {
if (value.length == 0) {
return 0;
}
NSScanner *hexScanner = [NSScanner scannerWithString:value];
unsigned long long hexValue = 0;
if (([value hasPrefix:@"0x"] || [value hasPrefix:@"0X"])
&& [hexScanner scanString:@"0x" intoString:nil]
&& [hexScanner scanHexLongLong:&hexValue]) {
return hexValue;
}
return strtoull(value.UTF8String, NULL, 10);
}
static NSDate * _Nullable WMMDateFromAFCTimestampString(NSString *value) {
unsigned long long rawValue = WMMParseUnsignedLongLong(value);
if (rawValue == 0) {
return nil;
}
NSTimeInterval seconds;
if (rawValue > 10000000000000000ULL) {
seconds = (NSTimeInterval)rawValue / 1000000000.0;
} else if (rawValue > 10000000000000ULL) {
seconds = (NSTimeInterval)rawValue / 1000000.0;
} else if (rawValue > 10000000000ULL) {
seconds = (NSTimeInterval)rawValue / 1000.0;
} else {
seconds = (NSTimeInterval)rawValue;
}
return [NSDate dateWithTimeIntervalSince1970:seconds];
}
static NSDate * _Nullable WMMModificationDateFromAFCInfo(NSDictionary<NSString *, NSString *> *info) {
NSString *candidate = info[@"st_mtime"] ?: info[@"st_birthtime"];
if (candidate.length == 0) {
return nil;
}
return WMMDateFromAFCTimestampString(candidate);
}
static long long WMMFileSizeFromAFCInfo(NSDictionary<NSString *, NSString *> *info) {
NSString *candidate = info[@"st_size"];
if (candidate.length == 0) {
return 0;
}
unsigned long long parsed = WMMParseUnsignedLongLong(candidate);
if (parsed > LLONG_MAX) {
return LLONG_MAX;
}
return (long long)parsed;
}
static NSDictionary<NSString *, id> * _Nullable WMMCopyAFCTreeMetrics(
WMMMobileDeviceFunctions *functions,
AFCConnectionRef afcConnection,
NSString *remotePath,
NSError **error
) {
NSDictionary<NSString *, NSString *> *info = WMMCopyAFCFileInfo(functions, afcConnection, remotePath, error);
if (info == nil) {
return nil;
}
NSDate *latestModificationDate = WMMModificationDateFromAFCInfo(info);
NSMutableArray<NSString *> *entries = nil;
const int directoryStatus = WMMReadAFCDirectory(functions, afcConnection, remotePath, &entries);
if (directoryStatus != 0) {
return @{
@"sizeBytes": @(WMMFileSizeFromAFCInfo(info)),
@"modifiedDate": latestModificationDate ?: [NSNull null]
};
}
long long totalSize = 0;
for (NSString *entry in entries) {
if ([entry isEqualToString:@"."] || [entry isEqualToString:@".."]) {
continue;
}
NSString *childRemotePath = [remotePath hasSuffix:@"/"]
? [remotePath stringByAppendingString:entry]
: [remotePath stringByAppendingPathComponent:entry];
NSDictionary<NSString *, id> *childMetrics = WMMCopyAFCTreeMetrics(
functions,
afcConnection,
childRemotePath,
error
);
if (childMetrics == nil) {
return nil;
}
totalSize += [childMetrics[@"sizeBytes"] longLongValue];
NSDate *childModifiedDate = childMetrics[@"modifiedDate"];
if ([childModifiedDate isKindOfClass:[NSDate class]]
&& (latestModificationDate == nil || [childModifiedDate compare:latestModificationDate] == NSOrderedDescending)) {
latestModificationDate = childModifiedDate;
}
}
return @{
@"sizeBytes": @(totalSize),
@"modifiedDate": latestModificationDate ?: [NSNull null]
};
}
static BOOL WMMCopyAFCFileToLocalURL(
WMMMobileDeviceFunctions *functions,
AFCConnectionRef afcConnection,
NSString *remotePath,
NSURL *localFileURL,
NSError **error
) {
AFCFileDescriptorRef fileDescriptor = NULL;
const int openStatus = functions->AFCFileRefOpen(
afcConnection,
remotePath.fileSystemRepresentation,
1,
&fileDescriptor
);
if (openStatus != 0 || fileDescriptor == NULL) {
if (error != NULL) {
*error = WMMMakeError(openStatus, [NSString stringWithFormat:@"AFCFileRefOpen failed for %@ (%d).", remotePath, openStatus]);
}
return NO;
}
NSFileManager *fileManager = [NSFileManager defaultManager];
[fileManager createFileAtPath:localFileURL.path contents:nil attributes:nil];
NSFileHandle *handle = [NSFileHandle fileHandleForWritingToURL:localFileURL error:error];
if (handle == nil) {
if (error != NULL && *error != nil) {
*error = [NSError errorWithDomain:(*error).domain code:(*error).code userInfo:@{
NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to open local file %@ for remote AFC path %@: %@",
localFileURL.path,
remotePath,
(*error).localizedDescription]
}];
}
functions->AFCFileRefClose(afcConnection, fileDescriptor);
return NO;
}
BOOL success = YES;
NSMutableData *buffer = [NSMutableData dataWithLength:64 * 1024];
while (true) {
size_t bytesToRead = buffer.length;
const int readStatus = functions->AFCFileRefRead(
afcConnection,
fileDescriptor,
buffer.mutableBytes,
&bytesToRead
);
if (readStatus != 0) {
if (error != NULL) {
*error = WMMMakeError(readStatus, [NSString stringWithFormat:@"AFCFileRefRead failed for %@ (%d).", remotePath, readStatus]);
}
success = NO;
break;
}
if (bytesToRead == 0) {
break;
}
NSData *chunk = [NSData dataWithBytes:buffer.bytes length:bytesToRead];
if (![handle writeData:chunk error:error]) {
if (error != NULL && *error != nil) {
*error = [NSError errorWithDomain:(*error).domain code:(*error).code userInfo:@{
NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed writing local file %@ for remote AFC path %@: %@",
localFileURL.path,
remotePath,
(*error).localizedDescription]
}];
}
success = NO;
break;
}
}
[handle closeFile];
functions->AFCFileRefClose(afcConnection, fileDescriptor);
return success;
}
static BOOL WMMCopyAFCTreeToLocalURL(
WMMMobileDeviceFunctions *functions,
AFCConnectionRef afcConnection,
NSString *remotePath,
NSURL *localURL,
NSError **error
) {
NSMutableArray<NSString *> *entries = nil;
const int directoryStatus = WMMReadAFCDirectory(functions, afcConnection, remotePath, &entries);
if (directoryStatus == 0) {
NSFileManager *fileManager = [NSFileManager defaultManager];
if (![fileManager createDirectoryAtURL:localURL withIntermediateDirectories:YES attributes:nil error:error]) {
if (error != NULL && *error != nil) {
*error = [NSError errorWithDomain:(*error).domain code:(*error).code userInfo:@{
NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to create local directory %@ for remote AFC path %@: %@",
localURL.path,
remotePath,
(*error).localizedDescription]
}];
}
return NO;
}
for (NSString *entry in entries) {
if ([entry isEqualToString:@"."] || [entry isEqualToString:@".."]) {
continue;
}
NSString *childRemotePath = remotePath;
if ([childRemotePath hasSuffix:@"/"]) {
childRemotePath = [childRemotePath stringByAppendingString:entry];
} else {
childRemotePath = [childRemotePath stringByAppendingPathComponent:entry];
}
NSURL *childLocalURL = [localURL URLByAppendingPathComponent:entry];
if (!WMMCopyAFCTreeToLocalURL(functions, afcConnection, childRemotePath, childLocalURL, error)) {
if (error != NULL && *error != nil) {
*error = [NSError errorWithDomain:(*error).domain code:(*error).code userInfo:@{
NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed copying remote AFC path %@ into %@: %@",
childRemotePath,
childLocalURL.path,
(*error).localizedDescription]
}];
}
return NO;
}
}
return YES;
}
return WMMCopyAFCFileToLocalURL(functions, afcConnection, remotePath, localURL, error);
}
static NSString *WMMNormalizedAFCPath(NSString *path) {
NSString *normalizedPath = path.length == 0 ? @"/" : path;
if (![normalizedPath hasPrefix:@"/"]) {
normalizedPath = [@"/" stringByAppendingString:normalizedPath];
}
return normalizedPath;
}
static NSData * _Nullable WMMCopyAFCFileData(
WMMMobileDeviceFunctions *functions,
AFCConnectionRef afcConnection,
NSString *remotePath,
NSError **error
) {
AFCFileDescriptorRef fileDescriptor = NULL;
const int openStatus = functions->AFCFileRefOpen(
afcConnection,
remotePath.fileSystemRepresentation,
1,
&fileDescriptor
);
if (openStatus != 0 || fileDescriptor == NULL) {
if (error != NULL) {
*error = WMMMakeError(openStatus, [NSString stringWithFormat:@"AFCFileRefOpen failed for %@ (%d).", remotePath, openStatus]);
}
return nil;
}
NSMutableData *data = [NSMutableData data];
NSMutableData *buffer = [NSMutableData dataWithLength:64 * 1024];
while (true) {
size_t bytesToRead = buffer.length;
const int readStatus = functions->AFCFileRefRead(
afcConnection,
fileDescriptor,
buffer.mutableBytes,
&bytesToRead
);
if (readStatus != 0) {
if (error != NULL) {
*error = WMMMakeError(readStatus, [NSString stringWithFormat:@"AFCFileRefRead failed for %@ (%d).", remotePath, readStatus]);
}
functions->AFCFileRefClose(afcConnection, fileDescriptor);
return nil;
}
if (bytesToRead == 0) {
break;
}
[data appendBytes:buffer.bytes length:bytesToRead];
}
functions->AFCFileRefClose(afcConnection, fileDescriptor);
return data;
}
static BOOL WMMEntryArrayContainsName(NSArray<NSString *> *entries, NSString *candidate) {
for (NSString *entry in entries) {
if ([entry isEqualToString:candidate]) {
return YES;
}
}
return NO;
}
static NSArray<NSString *> *WMMIconCandidateFileNamesForContentType(NSString *contentType) {
if ([contentType isEqualToString:@"World"]) {
return @[ @"world_icon.jpeg", @"world_icon.jpg", @"world_icon.png" ];
}
return @[ @"pack_icon.png", @"pack_icon.jpeg", @"pack_icon.jpg" ];
}
static NSString * _Nullable WMMReadUTF8TextFile(
WMMMobileDeviceFunctions *functions,
AFCConnectionRef afcConnection,
NSString *remotePath
) {
NSData *data = WMMCopyAFCFileData(functions, afcConnection, remotePath, NULL);
if (data == nil) {
return nil;
}
NSString *string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
return [string stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
}
static NSDictionary<NSString *, id> * _Nullable WMMReadManifestHeader(
WMMMobileDeviceFunctions *functions,
AFCConnectionRef afcConnection,
NSString *remotePath
) {
NSData *data = WMMCopyAFCFileData(functions, afcConnection, remotePath, NULL);
if (data == nil) {
return nil;
}
NSDictionary *jsonObject = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
if (![jsonObject isKindOfClass:[NSDictionary class]]) {
return nil;
}
NSDictionary *header = jsonObject[@"header"];
if (![header isKindOfClass:[NSDictionary class]]) {
return nil;
}
return header;
}
static NSString * _Nullable WMMVersionStringFromValue(id value) {
if ([value isKindOfClass:[NSString class]]) {
NSString *stringValue = [(NSString *)value stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
return stringValue.length > 0 ? stringValue : nil;
}
if ([value isKindOfClass:[NSArray class]]) {
NSMutableArray<NSString *> *components = [NSMutableArray array];
for (id component in (NSArray *)value) {
if ([component isKindOfClass:[NSNumber class]]) {
[components addObject:[(NSNumber *)component stringValue]];
} else if ([component isKindOfClass:[NSString class]]) {
[components addObject:(NSString *)component];
}
}
return components.count > 0 ? [components componentsJoinedByString:@"."] : nil;
}
return nil;
}
static NSDictionary<NSString *, id> *WMMBuildPackReferenceSummary(
NSString *name,
NSString *contentType,
NSString * _Nullable uuid,
NSString * _Nullable version,
NSString *source
) {
NSMutableDictionary<NSString *, id> *summary = [@{
@"name": name,
@"contentType": contentType,
@"source": source
} mutableCopy];
if (uuid.length > 0) {
summary[@"uuid"] = [uuid lowercaseString];
}
if (version.length > 0) {
summary[@"version"] = version;
}
return summary;
}
static NSArray<NSDictionary<NSString *, id> *> *WMMParsePackReferenceSummariesFromData(
NSData *data,
NSString *contentType
) {
id jsonObject = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
if (![jsonObject isKindOfClass:[NSArray class]]) {
return @[];
}
NSMutableArray<NSDictionary<NSString *, id> *> *references = [NSMutableArray array];
for (id entry in (NSArray *)jsonObject) {
if (![entry isKindOfClass:[NSDictionary class]]) {
continue;
}
NSDictionary *entryDictionary = (NSDictionary *)entry;
NSString *uuid = [entryDictionary[@"pack_id"] isKindOfClass:[NSString class]] ? entryDictionary[@"pack_id"] : nil;
NSString *version = WMMVersionStringFromValue(entryDictionary[@"version"]);
NSString *name = uuid.length > 0 ? uuid : @"Referenced Pack";
[references addObject:WMMBuildPackReferenceSummary(
name,
contentType,
uuid,
version,
@"referencedByWorld"
)];
}
return references;
}
static NSArray<NSDictionary<NSString *, id> *> *WMMReadPackReferenceSummariesFile(
WMMMobileDeviceFunctions *functions,
AFCConnectionRef afcConnection,
NSString *remotePath,
NSString *contentType
) {
NSData *data = WMMCopyAFCFileData(functions, afcConnection, remotePath, NULL);
if (data == nil) {
return @[];
}
return WMMParsePackReferenceSummariesFromData(data, contentType);
}
static NSArray<NSDictionary<NSString *, id> *> *WMMLoadEmbeddedPackReferenceSummaries(
WMMMobileDeviceFunctions *functions,
AFCConnectionRef afcConnection,
NSString *remoteFolderPath,
NSString *contentType
) {
NSMutableArray<NSString *> *childFolders = nil;
if (WMMReadAFCDirectory(functions, afcConnection, remoteFolderPath, &childFolders) != 0 || childFolders == nil) {
return @[];
}
NSMutableArray<NSDictionary<NSString *, id> *> *references = [NSMutableArray array];
for (NSString *childFolder in childFolders) {
if ([childFolder isEqualToString:@"."] || [childFolder isEqualToString:@".."]) {
continue;
}
NSString *manifestPath = [[remoteFolderPath stringByAppendingPathComponent:childFolder] stringByAppendingPathComponent:@"manifest.json"];
NSDictionary<NSString *, id> *header = WMMReadManifestHeader(functions, afcConnection, manifestPath);
if (header == nil) {
continue;
}
NSString *name = [header[@"name"] isKindOfClass:[NSString class]] && [header[@"name"] length] > 0
? header[@"name"]
: childFolder;
NSString *uuid = [header[@"uuid"] isKindOfClass:[NSString class]] ? header[@"uuid"] : nil;
NSString *version = WMMVersionStringFromValue(header[@"version"]);
[references addObject:WMMBuildPackReferenceSummary(
name,
contentType,
uuid,
version,
@"embeddedInWorld"
)];
}
return references;
}
static BOOL WMMIsCandidateItem(NSString *contentType, NSArray<NSString *> *entries) {
if ([contentType isEqualToString:@"World"]) {
return WMMEntryArrayContainsName(entries, @"level.dat")
|| WMMEntryArrayContainsName(entries, @"db")
|| WMMEntryArrayContainsName(entries, @"levelname.txt");
}
return WMMEntryArrayContainsName(entries, @"manifest.json")
|| WMMEntryArrayContainsName(entries, @"pack_icon.png")
|| WMMEntryArrayContainsName(entries, @"pack_icon.jpeg")
|| WMMEntryArrayContainsName(entries, @"pack_icon.jpg");
}
static NSDictionary<NSString *, id> *WMMBuildShallowMinecraftItemSummary(
NSString *contentType,
NSString *collectionFolderName,
NSString *itemRelativePath,
NSString *folderName,
NSArray<NSString *> *entries
) {
NSMutableDictionary<NSString *, id> *summary = [@{
@"contentType": contentType,
@"collectionFolderName": collectionFolderName,
@"relativePath": itemRelativePath,
@"folderName": folderName,
@"displayName": folderName
} mutableCopy];
summary[@"hasIcon"] = @(
WMMEntryArrayContainsName(entries, @"world_icon.png")
|| WMMEntryArrayContainsName(entries, @"world_icon.jpeg")
|| WMMEntryArrayContainsName(entries, @"world_icon.jpg")
|| WMMEntryArrayContainsName(entries, @"pack_icon.png")
|| WMMEntryArrayContainsName(entries, @"pack_icon.jpeg")
|| WMMEntryArrayContainsName(entries, @"pack_icon.jpg")
);
return summary;
}
static void WMMAppendCollectionSummaries(
WMMMobileDeviceFunctions *functions,
AFCConnectionRef afcConnection,
NSString *rootRemotePath,
NSString *collectionFolderName,
NSString *contentType,
NSMutableArray<NSDictionary<NSString *, id> *> *results
) {
NSString *collectionRemotePath = [rootRemotePath stringByAppendingPathComponent:collectionFolderName];
NSMutableArray<NSString *> *itemFolderNames = nil;
if (WMMReadAFCDirectory(functions, afcConnection, collectionRemotePath, &itemFolderNames) != 0 || itemFolderNames == nil) {
return;
}
for (NSString *itemFolderName in itemFolderNames) {
if ([itemFolderName isEqualToString:@"."] || [itemFolderName isEqualToString:@".."]) {
continue;
}
NSString *itemRemotePath = [collectionRemotePath stringByAppendingPathComponent:itemFolderName];
NSMutableArray<NSString *> *itemEntries = nil;
if (WMMReadAFCDirectory(functions, afcConnection, itemRemotePath, &itemEntries) != 0 || itemEntries == nil) {
continue;
}
if (!WMMIsCandidateItem(contentType, itemEntries)) {
continue;
}
NSString *itemRelativePath = [collectionFolderName stringByAppendingPathComponent:itemFolderName];
[results addObject:WMMBuildShallowMinecraftItemSummary(
contentType,
collectionFolderName,
itemRelativePath,
itemFolderName,
itemEntries
)];
if (![contentType isEqualToString:@"World"]) {
continue;
}
NSArray<NSDictionary<NSString *, NSString *> *> *embeddedCollections = @[
@{ @"folder": @"behavior_packs", @"type": @"Behavior Pack" },
@{ @"folder": @"resource_packs", @"type": @"Resource Pack" }
];
for (NSDictionary<NSString *, NSString *> *embeddedCollection in embeddedCollections) {
NSString *embeddedFolder = embeddedCollection[@"folder"];
NSString *embeddedType = embeddedCollection[@"type"];
NSString *embeddedCollectionPath = [itemRemotePath stringByAppendingPathComponent:embeddedFolder];
NSMutableArray<NSString *> *embeddedFolderNames = nil;
if (WMMReadAFCDirectory(functions, afcConnection, embeddedCollectionPath, &embeddedFolderNames) != 0 || embeddedFolderNames == nil) {
continue;
}
for (NSString *embeddedFolderName in embeddedFolderNames) {
if ([embeddedFolderName isEqualToString:@"."] || [embeddedFolderName isEqualToString:@".."]) {
continue;
}
NSString *embeddedItemPath = [embeddedCollectionPath stringByAppendingPathComponent:embeddedFolderName];
NSMutableArray<NSString *> *embeddedEntries = nil;
if (WMMReadAFCDirectory(functions, afcConnection, embeddedItemPath, &embeddedEntries) != 0 || embeddedEntries == nil) {
continue;
}
if (!WMMIsCandidateItem(embeddedType, embeddedEntries)) {
continue;
}
NSString *embeddedRelativePath = [itemRelativePath stringByAppendingPathComponent:[embeddedFolder stringByAppendingPathComponent:embeddedFolderName]];
[results addObject:WMMBuildShallowMinecraftItemSummary(
embeddedType,
embeddedFolder,
embeddedRelativePath,
embeddedFolderName,
embeddedEntries
)];
}
}
}
}
NSDictionary<NSString *, id> * _Nullable
WMMCopyConnectedDeviceSummaries(NSError **error) {
WMMMobileDeviceFunctions functions;
if (!WMMLoadFunctions(&functions, error)) {
return nil;
}
NSArray<NSValue *> *devices = WMMCopyConnectedDevices(&functions, error);
if (devices.count == 0) {
return nil;
}
NSMutableArray<NSDictionary<NSString *, id> *> *summaries = [NSMutableArray array];
NSMutableDictionary<NSString *, NSValue *> *preferredDevicesByIdentifier = [NSMutableDictionary dictionary];
for (NSValue *value in devices) {
AMDeviceRef device = (AMDeviceRef)value.pointerValue;
if (device == NULL) {
continue;
}
NSString *deviceIdentifier = WMMResolvedDeviceIdentifier(&functions, device);
if (deviceIdentifier.length == 0) {
continue;
}
NSValue *existingValue = preferredDevicesByIdentifier[deviceIdentifier];
AMDeviceRef existingDevice = existingValue != nil ? (AMDeviceRef)existingValue.pointerValue : NULL;
if (existingDevice != NULL && WMMConnectionPreferenceRank(device) <= WMMConnectionPreferenceRank(existingDevice)) {
continue;
}
preferredDevicesByIdentifier[deviceIdentifier] = value;
}
for (NSString *deviceIdentifier in preferredDevicesByIdentifier) {
AMDeviceRef device = (AMDeviceRef)preferredDevicesByIdentifier[deviceIdentifier].pointerValue;
if (device == NULL) {
continue;
}
NSString *deviceName = @"Unknown Device";
NSString *productType = @"";
NSString *productVersion = @"";
NSString *connectionType = WMMDeviceStringValue(&functions, device, CFSTR("ConnectionType")) ?: WMMInferredConnectionType(device);
if (WMMConnectAndValidateDevice(&functions, device, NO, NULL)) {
deviceName = WMMDeviceStringValue(&functions, device, CFSTR("DeviceName")) ?: @"Unknown Device";
productType = WMMDeviceStringValue(&functions, device, CFSTR("ProductType")) ?: @"";
productVersion = WMMDeviceStringValue(&functions, device, CFSTR("ProductVersion")) ?: @"";
connectionType = WMMDeviceStringValue(&functions, device, CFSTR("ConnectionType")) ?: WMMInferredConnectionType(device);
WMMLogDeviceTransportDiagnostics(&functions, device, deviceIdentifier);
WMMDisconnectDevice(&functions, device, NO);
}
[summaries addObject:@{
@"deviceName": deviceName,
@"deviceIdentifier": deviceIdentifier,
@"productType": productType,
@"productVersion": productVersion,
@"connectionType": connectionType,
@"trustState": @"trusted"
}];
}
WMMReleaseDeviceValues(&functions, devices);
[summaries sortUsingComparator:^NSComparisonResult(NSDictionary<NSString *, id> *lhs, NSDictionary<NSString *, id> *rhs) {
return [lhs[@"deviceName"] localizedStandardCompare:rhs[@"deviceName"]];
}];
return @{ @"devices": summaries };
}
NSDictionary<NSString *, id> * _Nullable
WMMCopyConnectedDeviceAppDirectoryListing(
NSString *deviceIdentifier,
NSString *bundleIdentifier,
NSString *relativePath,
NSError **error
) {
if (bundleIdentifier.length == 0) {
if (error != NULL) {
*error = WMMMakeError(4, @"A bundle identifier is required.");
}
return nil;
}
WMMMobileDeviceFunctions functions;
if (!WMMLoadFunctions(&functions, error)) {
return nil;
}
AMDeviceRef device = WMMCopyConnectedDevice(&functions, deviceIdentifier, error);
if (device == NULL) {
return nil;
}
if (!WMMConnectAndValidateDevice(&functions, device, YES, error)) {
functions.AMDeviceRelease(device);
return nil;
}
NSString *deviceName = WMMDeviceStringValue(&functions, device, CFSTR("DeviceName")) ?: @"Unknown Device";
NSString *productType = WMMDeviceStringValue(&functions, device, CFSTR("ProductType")) ?: @"";
NSString *productVersion = WMMDeviceStringValue(&functions, device, CFSTR("ProductVersion")) ?: @"";
NSString *resolvedDeviceIdentifier = WMMResolvedDeviceIdentifier(&functions, device);
AMDServiceConnectionRef backingServiceConnection = NULL;
AFCConnectionRef afcConnection = WMMCreateVendAFCConnection(
&functions,
device,
bundleIdentifier,
&backingServiceConnection,
error
);
if (afcConnection == NULL) {
WMMCloseVendSession(&functions, device, YES, NULL, backingServiceConnection);
functions.AMDeviceRelease(device);
return nil;
}
NSString *normalizedPath = relativePath.length == 0 ? @"/" : relativePath;
if (![normalizedPath hasPrefix:@"/"]) {
normalizedPath = [@"/" stringByAppendingString:normalizedPath];
}
NSMutableArray<NSString *> *entries = nil;
const int directoryStatus = WMMReadAFCDirectory(&functions, afcConnection, normalizedPath, &entries);
if (directoryStatus != 0) {
NSMutableArray<NSString *> *rootEntries = nil;
const int rootStatus = WMMReadAFCDirectory(&functions, afcConnection, @"/", &rootEntries);
WMMCloseVendSession(&functions, device, YES, afcConnection, backingServiceConnection);
functions.AMDeviceRelease(device);
if (error != NULL) {
NSString *message = [NSString stringWithFormat:@"AFC directory read failed for %@ (%d).", normalizedPath, directoryStatus];
if (rootStatus == 0 && rootEntries.count > 0) {
NSString *rootSummary = [rootEntries componentsJoinedByString:@", "];
message = [message stringByAppendingFormat:@" Vend root contains: %@.", rootSummary];
} else if (rootStatus != 0) {
message = [message stringByAppendingFormat:@" Vend root listing also failed (%d).", rootStatus];
}
*error = WMMMakeError(directoryStatus, message);
}
return nil;
}
WMMCloseVendSession(&functions, device, YES, afcConnection, backingServiceConnection);
functions.AMDeviceRelease(device);
return @{
@"bundleIdentifier": bundleIdentifier,
@"path": normalizedPath,
@"deviceName": deviceName,
@"deviceIdentifier": resolvedDeviceIdentifier,
@"productType": productType,
@"productVersion": productVersion,
@"entries": entries
};
}
NSDictionary<NSString *, id> * _Nullable
WMMCopyConnectedDeviceAppPathProbeResults(
NSString *deviceIdentifier,
NSString *bundleIdentifier,
NSArray<NSString *> *paths,
NSError **error
) {
if (bundleIdentifier.length == 0) {
if (error != NULL) {
*error = WMMMakeError(4, @"A bundle identifier is required.");
}
return nil;
}
WMMMobileDeviceFunctions functions;
if (!WMMLoadFunctions(&functions, error)) {
return nil;
}
AMDeviceRef device = WMMCopyConnectedDevice(&functions, deviceIdentifier, error);
if (device == NULL) {
return nil;
}
if (!WMMConnectAndValidateDevice(&functions, device, YES, error)) {
functions.AMDeviceRelease(device);
return nil;
}
NSString *deviceName = WMMDeviceStringValue(&functions, device, CFSTR("DeviceName")) ?: @"Unknown Device";
NSString *productType = WMMDeviceStringValue(&functions, device, CFSTR("ProductType")) ?: @"";
NSString *productVersion = WMMDeviceStringValue(&functions, device, CFSTR("ProductVersion")) ?: @"";
NSString *resolvedDeviceIdentifier = WMMResolvedDeviceIdentifier(&functions, device);
AMDServiceConnectionRef backingServiceConnection = NULL;
AFCConnectionRef afcConnection = WMMCreateVendAFCConnection(
&functions,
device,
bundleIdentifier,
&backingServiceConnection,
error
);
if (afcConnection == NULL) {
WMMCloseVendSession(&functions, device, YES, NULL, backingServiceConnection);
functions.AMDeviceRelease(device);
return nil;
}
NSMutableArray<NSDictionary<NSString *, id> *> *results = [NSMutableArray array];
for (NSString *candidate in paths) {
if (![candidate isKindOfClass:[NSString class]]) {
continue;
}
NSMutableArray<NSString *> *entries = nil;
int status = WMMReadAFCDirectory(&functions, afcConnection, candidate, &entries);
NSMutableDictionary<NSString *, id> *result = [@{
@"path": candidate,
@"status": @(status),
@"success": @(status == 0)
} mutableCopy];
if (status == 0 && entries != nil) {
result[@"entries"] = entries;
}
[results addObject:result];
}
WMMCloseVendSession(&functions, device, YES, afcConnection, backingServiceConnection);
functions.AMDeviceRelease(device);
return @{
@"bundleIdentifier": bundleIdentifier,
@"deviceName": deviceName,
@"deviceIdentifier": resolvedDeviceIdentifier,
@"productType": productType,
@"productVersion": productVersion,
@"results": results
};
}
NSDictionary<NSString *, id> * _Nullable
WMMCopyConnectedDeviceApplicationList(
NSString *deviceIdentifier,
NSError **error
) {
WMMMobileDeviceFunctions functions;
if (!WMMLoadFunctions(&functions, error)) {
return nil;
}
AMDeviceRef device = WMMCopyConnectedDevice(&functions, deviceIdentifier, error);
if (device == NULL) {
return nil;
}
if (!WMMConnectAndValidateDevice(&functions, device, YES, error)) {
functions.AMDeviceRelease(device);
return nil;
}
NSString *deviceName = WMMDeviceStringValue(&functions, device, CFSTR("DeviceName")) ?: @"Unknown Device";
NSString *productType = WMMDeviceStringValue(&functions, device, CFSTR("ProductType")) ?: @"";
NSString *productVersion = WMMDeviceStringValue(&functions, device, CFSTR("ProductVersion")) ?: @"";
NSString *resolvedDeviceIdentifier = WMMResolvedDeviceIdentifier(&functions, device);
CFDictionaryRef appDictionary = NULL;
const int lookupStatus = functions.AMDeviceLookupApplications(device, NULL, &appDictionary);
WMMDisconnectDevice(&functions, device, YES);
functions.AMDeviceRelease(device);
if (lookupStatus != 0 || appDictionary == NULL) {
if (error != NULL) {
*error = WMMMakeError(lookupStatus, [NSString stringWithFormat:@"AMDeviceLookupApplications failed (%d).", lookupStatus]);
}
return nil;
}
NSMutableArray<NSDictionary<NSString *, id> *> *applications = [NSMutableArray array];
NSDictionary *bridgedApps = CFBridgingRelease(appDictionary);
[bridgedApps enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
(void)stop;
if (![key isKindOfClass:[NSString class]]) {
return;
}
NSString *bundleIdentifier = (NSString *)key;
NSString *displayName = bundleIdentifier;
if ([obj isKindOfClass:[NSDictionary class]]) {
NSDictionary *appInfo = (NSDictionary *)obj;
NSString *candidateName = appInfo[@"CFBundleDisplayName"];
if (![candidateName isKindOfClass:[NSString class]] || candidateName.length == 0) {
candidateName = appInfo[@"CFBundleName"];
}
if (![candidateName isKindOfClass:[NSString class]] || candidateName.length == 0) {
candidateName = appInfo[@"Path"];
}
if ([candidateName isKindOfClass:[NSString class]] && candidateName.length > 0) {
displayName = candidateName;
}
}
NSNumber *uiFileSharingEnabled = @NO;
NSNumber *supportsOpeningDocumentsInPlace = @NO;
if ([obj isKindOfClass:[NSDictionary class]]) {
NSDictionary *appInfo = (NSDictionary *)obj;
id fileSharingValue = appInfo[@"UIFileSharingEnabled"];
if ([fileSharingValue isKindOfClass:[NSNumber class]]) {
uiFileSharingEnabled = fileSharingValue;
}
id openingInPlaceValue = appInfo[@"LSSupportsOpeningDocumentsInPlace"];
if ([openingInPlaceValue isKindOfClass:[NSNumber class]]) {
supportsOpeningDocumentsInPlace = openingInPlaceValue;
}
}
[applications addObject:@{
@"bundleIdentifier": bundleIdentifier,
@"displayName": displayName,
@"uiFileSharingEnabled": uiFileSharingEnabled,
@"supportsOpeningDocumentsInPlace": supportsOpeningDocumentsInPlace
}];
}];
[applications sortUsingComparator:^NSComparisonResult(NSDictionary<NSString *, id> *lhs, NSDictionary<NSString *, id> *rhs) {
return [lhs[@"displayName"] localizedStandardCompare:rhs[@"displayName"]];
}];
return @{
@"deviceName": deviceName,
@"deviceIdentifier": resolvedDeviceIdentifier,
@"productType": productType,
@"productVersion": productVersion,
@"applications": applications
};
}
NSDictionary<NSString *, id> * _Nullable
WMMCopyConnectedDeviceMinecraftLibrarySnapshot(
NSString *deviceIdentifier,
NSString *bundleIdentifier,
NSString *relativePath,
NSError **error
) {
if (bundleIdentifier.length == 0) {
if (error != NULL) {
*error = WMMMakeError(16, @"A bundle identifier is required.");
}
return nil;
}
WMMMobileDeviceFunctions functions;
if (!WMMLoadFunctions(&functions, error)) {
return nil;
}
AMDeviceRef device = WMMCopyConnectedDevice(&functions, deviceIdentifier, error);
if (device == NULL) {
return nil;
}
if (!WMMConnectAndValidateDevice(&functions, device, YES, error)) {
functions.AMDeviceRelease(device);
return nil;
}
AMDServiceConnectionRef backingServiceConnection = NULL;
AFCConnectionRef afcConnection = WMMCreateVendAFCConnection(
&functions,
device,
bundleIdentifier,
&backingServiceConnection,
error
);
if (afcConnection == NULL) {
WMMCloseVendSession(&functions, device, YES, NULL, backingServiceConnection);
functions.AMDeviceRelease(device);
return nil;
}
NSString *normalizedRootPath = WMMNormalizedAFCPath(relativePath);
NSMutableArray<NSDictionary<NSString *, id> *> *items = [NSMutableArray array];
NSArray<NSDictionary<NSString *, NSString *> *> *collections = @[
@{ @"folder": @"minecraftWorlds", @"type": @"World" },
@{ @"folder": @"behavior_packs", @"type": @"Behavior Pack" },
@{ @"folder": @"resource_packs", @"type": @"Resource Pack" },
@{ @"folder": @"skin_packs", @"type": @"Skin Pack" },
@{ @"folder": @"world_templates", @"type": @"World Template" }
];
for (NSDictionary<NSString *, NSString *> *collection in collections) {
WMMAppendCollectionSummaries(
&functions,
afcConnection,
normalizedRootPath,
collection[@"folder"],
collection[@"type"],
items
);
}
WMMCloseVendSession(&functions, device, YES, afcConnection, backingServiceConnection);
functions.AMDeviceRelease(device);
return @{
@"bundleIdentifier": bundleIdentifier,
@"path": normalizedRootPath,
@"items": items
};
}
NSDictionary<NSString *, id> * _Nullable
WMMCopyConnectedDeviceMinecraftMetadataBatch(
NSString *deviceIdentifier,
NSString *bundleIdentifier,
NSString *relativePath,
NSArray<NSDictionary<NSString *, id> *> *items,
NSError **error
) {
if (bundleIdentifier.length == 0) {
if (error != NULL) {
*error = WMMMakeError(19, @"A bundle identifier is required.");
}
return nil;
}
WMMMobileDeviceFunctions functions;
if (!WMMLoadFunctions(&functions, error)) {
return nil;
}
AMDeviceRef device = WMMCopyConnectedDevice(&functions, deviceIdentifier, error);
if (device == NULL) {
return nil;
}
if (!WMMConnectAndValidateDevice(&functions, device, YES, error)) {
functions.AMDeviceRelease(device);
return nil;
}
AMDServiceConnectionRef backingServiceConnection = NULL;
AFCConnectionRef afcConnection = WMMCreateVendAFCConnection(
&functions,
device,
bundleIdentifier,
&backingServiceConnection,
error
);
if (afcConnection == NULL) {
WMMCloseVendSession(&functions, device, YES, NULL, backingServiceConnection);
functions.AMDeviceRelease(device);
return nil;
}
NSString *normalizedRootPath = WMMNormalizedAFCPath(relativePath);
NSMutableArray<NSDictionary<NSString *, id> *> *results = [NSMutableArray array];
for (NSDictionary<NSString *, id> *item in items) {
NSString *contentType = [item[@"contentType"] isKindOfClass:[NSString class]] ? item[@"contentType"] : nil;
NSString *relativeItemPath = [item[@"relativePath"] isKindOfClass:[NSString class]] ? item[@"relativePath"] : nil;
NSString *folderName = [item[@"folderName"] isKindOfClass:[NSString class]] ? item[@"folderName"] : nil;
if (contentType.length == 0 || relativeItemPath.length == 0 || folderName.length == 0) {
continue;
}
NSString *itemRemotePath = [normalizedRootPath stringByAppendingPathComponent:relativeItemPath];
NSMutableDictionary<NSString *, id> *metadata = [@{
@"relativePath": relativeItemPath
} mutableCopy];
if ([contentType isEqualToString:@"World"]) {
NSString *levelName = WMMReadUTF8TextFile(
&functions,
afcConnection,
[itemRemotePath stringByAppendingPathComponent:@"levelname.txt"]
);
metadata[@"displayName"] = levelName.length > 0 ? levelName : folderName;
NSMutableArray<NSDictionary<NSString *, id> *> *packReferences = [NSMutableArray array];
[packReferences addObjectsFromArray:WMMReadPackReferenceSummariesFile(
&functions,
afcConnection,
[itemRemotePath stringByAppendingPathComponent:@"world_behavior_packs.json"],
@"Behavior Pack"
)];
[packReferences addObjectsFromArray:WMMReadPackReferenceSummariesFile(
&functions,
afcConnection,
[itemRemotePath stringByAppendingPathComponent:@"world_resource_packs.json"],
@"Resource Pack"
)];
[packReferences addObjectsFromArray:WMMLoadEmbeddedPackReferenceSummaries(
&functions,
afcConnection,
[itemRemotePath stringByAppendingPathComponent:@"behavior_packs"],
@"Behavior Pack"
)];
[packReferences addObjectsFromArray:WMMLoadEmbeddedPackReferenceSummaries(
&functions,
afcConnection,
[itemRemotePath stringByAppendingPathComponent:@"resource_packs"],
@"Resource Pack"
)];
if (packReferences.count > 0) {
metadata[@"packReferences"] = packReferences;
}
} else {
NSDictionary<NSString *, id> *header = WMMReadManifestHeader(
&functions,
afcConnection,
[itemRemotePath stringByAppendingPathComponent:@"manifest.json"]
);
NSString *manifestName = [header[@"name"] isKindOfClass:[NSString class]] ? header[@"name"] : nil;
metadata[@"displayName"] = manifestName.length > 0 ? manifestName : folderName;
if ([header[@"uuid"] isKindOfClass:[NSString class]]) {
metadata[@"packUUID"] = [header[@"uuid"] lowercaseString];
}
NSString *version = WMMVersionStringFromValue(header[@"version"]);
if (version.length > 0) {
metadata[@"packVersion"] = version;
}
NSString *minimumEngineVersion = WMMVersionStringFromValue(header[@"min_engine_version"]);
if (minimumEngineVersion.length > 0) {
metadata[@"minimumEngineVersion"] = minimumEngineVersion;
}
}
[results addObject:metadata];
}
WMMCloseVendSession(&functions, device, YES, afcConnection, backingServiceConnection);
functions.AMDeviceRelease(device);
return @{
@"bundleIdentifier": bundleIdentifier,
@"path": normalizedRootPath,
@"items": results
};
}
NSDictionary<NSString *, id> * _Nullable
WMMCopyConnectedDeviceMinecraftIconBatch(
NSString *deviceIdentifier,
NSString *bundleIdentifier,
NSString *relativePath,
NSArray<NSDictionary<NSString *, id> *> *items,
NSError **error
) {
if (bundleIdentifier.length == 0) {
if (error != NULL) {
*error = WMMMakeError(20, @"A bundle identifier is required.");
}
return nil;
}
WMMMobileDeviceFunctions functions;
if (!WMMLoadFunctions(&functions, error)) {
return nil;
}
AMDeviceRef device = WMMCopyConnectedDevice(&functions, deviceIdentifier, error);
if (device == NULL) {
return nil;
}
if (!WMMConnectAndValidateDevice(&functions, device, YES, error)) {
functions.AMDeviceRelease(device);
return nil;
}
AMDServiceConnectionRef backingServiceConnection = NULL;
AFCConnectionRef afcConnection = WMMCreateVendAFCConnection(
&functions,
device,
bundleIdentifier,
&backingServiceConnection,
error
);
if (afcConnection == NULL) {
WMMCloseVendSession(&functions, device, YES, NULL, backingServiceConnection);
functions.AMDeviceRelease(device);
return nil;
}
NSString *normalizedRootPath = WMMNormalizedAFCPath(relativePath);
NSMutableArray<NSDictionary<NSString *, id> *> *results = [NSMutableArray array];
for (NSDictionary<NSString *, id> *item in items) {
NSString *contentType = [item[@"contentType"] isKindOfClass:[NSString class]] ? item[@"contentType"] : nil;
NSString *relativeItemPath = [item[@"relativePath"] isKindOfClass:[NSString class]] ? item[@"relativePath"] : nil;
if (contentType.length == 0 || relativeItemPath.length == 0) {
continue;
}
NSString *itemRemotePath = [normalizedRootPath stringByAppendingPathComponent:relativeItemPath];
for (NSString *candidateFileName in WMMIconCandidateFileNamesForContentType(contentType)) {
NSString *candidateRemotePath = [itemRemotePath stringByAppendingPathComponent:candidateFileName];
NSData *data = WMMCopyAFCFileData(&functions, afcConnection, candidateRemotePath, NULL);
if (data == nil) {
continue;
}
[results addObject:@{
@"relativePath": relativeItemPath,
@"iconFileName": candidateFileName,
@"data": data
}];
break;
}
}
WMMCloseVendSession(&functions, device, YES, afcConnection, backingServiceConnection);
functions.AMDeviceRelease(device);
return @{
@"bundleIdentifier": bundleIdentifier,
@"path": normalizedRootPath,
@"items": results
};
}
NSData * _Nullable
WMMCopyConnectedDeviceAppFileData(
NSString *deviceIdentifier,
NSString *bundleIdentifier,
NSString *relativePath,
NSError **error
) {
if (bundleIdentifier.length == 0) {
if (error != NULL) {
*error = WMMMakeError(17, @"A bundle identifier is required.");
}
return nil;
}
WMMMobileDeviceFunctions functions;
if (!WMMLoadFunctions(&functions, error)) {
return nil;
}
AMDeviceRef device = WMMCopyConnectedDevice(&functions, deviceIdentifier, error);
if (device == NULL) {
return nil;
}
if (!WMMConnectAndValidateDevice(&functions, device, YES, error)) {
functions.AMDeviceRelease(device);
return nil;
}
AMDServiceConnectionRef backingServiceConnection = NULL;
AFCConnectionRef afcConnection = WMMCreateVendAFCConnection(
&functions,
device,
bundleIdentifier,
&backingServiceConnection,
error
);
if (afcConnection == NULL) {
WMMCloseVendSession(&functions, device, YES, NULL, backingServiceConnection);
functions.AMDeviceRelease(device);
return nil;
}
NSString *normalizedPath = WMMNormalizedAFCPath(relativePath);
NSData *data = WMMCopyAFCFileData(&functions, afcConnection, normalizedPath, error);
WMMCloseVendSession(&functions, device, YES, afcConnection, backingServiceConnection);
functions.AMDeviceRelease(device);
return data;
}
NSDictionary<NSString *, id> * _Nullable
WMMCopyConnectedDeviceAppPathMetrics(
NSString *deviceIdentifier,
NSString *bundleIdentifier,
NSString *relativePath,
NSError **error
) {
if (bundleIdentifier.length == 0) {
if (error != NULL) {
*error = WMMMakeError(18, @"A bundle identifier is required.");
}
return nil;
}
WMMMobileDeviceFunctions functions;
if (!WMMLoadFunctions(&functions, error)) {
return nil;
}
AMDeviceRef device = WMMCopyConnectedDevice(&functions, deviceIdentifier, error);
if (device == NULL) {
return nil;
}
if (!WMMConnectAndValidateDevice(&functions, device, YES, error)) {
functions.AMDeviceRelease(device);
return nil;
}
AMDServiceConnectionRef backingServiceConnection = NULL;
AFCConnectionRef afcConnection = WMMCreateVendAFCConnection(
&functions,
device,
bundleIdentifier,
&backingServiceConnection,
error
);
if (afcConnection == NULL) {
WMMCloseVendSession(&functions, device, YES, NULL, backingServiceConnection);
functions.AMDeviceRelease(device);
return nil;
}
NSString *normalizedPath = WMMNormalizedAFCPath(relativePath);
NSDictionary<NSString *, id> *metrics = WMMCopyAFCTreeMetrics(
&functions,
afcConnection,
normalizedPath,
error
);
WMMCloseVendSession(&functions, device, YES, afcConnection, backingServiceConnection);
functions.AMDeviceRelease(device);
if (metrics == nil) {
return nil;
}
return @{
@"bundleIdentifier": bundleIdentifier,
@"path": normalizedPath,
@"sizeBytes": metrics[@"sizeBytes"] ?: @0,
@"modifiedDate": metrics[@"modifiedDate"] ?: [NSNull null]
};
}
NSDictionary<NSString *, id> * _Nullable
WMMCopyConnectedDeviceAppPathMetricsBatch(
NSString *deviceIdentifier,
NSString *bundleIdentifier,
NSArray<NSString *> *relativePaths,
NSError **error
) {
if (bundleIdentifier.length == 0) {
if (error != NULL) {
*error = WMMMakeError(21, @"A bundle identifier is required.");
}
return nil;
}
WMMMobileDeviceFunctions functions;
if (!WMMLoadFunctions(&functions, error)) {
return nil;
}
AMDeviceRef device = WMMCopyConnectedDevice(&functions, deviceIdentifier, error);
if (device == NULL) {
return nil;
}
if (!WMMConnectAndValidateDevice(&functions, device, YES, error)) {
functions.AMDeviceRelease(device);
return nil;
}
AMDServiceConnectionRef backingServiceConnection = NULL;
AFCConnectionRef afcConnection = WMMCreateVendAFCConnection(
&functions,
device,
bundleIdentifier,
&backingServiceConnection,
error
);
if (afcConnection == NULL) {
WMMCloseVendSession(&functions, device, YES, NULL, backingServiceConnection);
functions.AMDeviceRelease(device);
return nil;
}
NSMutableArray<NSDictionary<NSString *, id> *> *results = [NSMutableArray array];
for (NSString *relativePath in relativePaths) {
if (![relativePath isKindOfClass:[NSString class]] || relativePath.length == 0) {
continue;
}
NSString *normalizedPath = WMMNormalizedAFCPath(relativePath);
NSDictionary<NSString *, id> *metrics = WMMCopyAFCTreeMetrics(
&functions,
afcConnection,
normalizedPath,
NULL
);
NSMutableDictionary<NSString *, id> *result = [@{
@"relativePath": relativePath
} mutableCopy];
if (metrics != nil) {
result[@"sizeBytes"] = metrics[@"sizeBytes"] ?: @0;
result[@"modifiedDate"] = metrics[@"modifiedDate"] ?: [NSNull null];
}
[results addObject:result];
}
WMMCloseVendSession(&functions, device, YES, afcConnection, backingServiceConnection);
functions.AMDeviceRelease(device);
return @{
@"bundleIdentifier": bundleIdentifier,
@"items": results
};
}
NSDictionary<NSString *, id> * _Nullable
WMMCopyConnectedDeviceApplicationDetails(
NSString *deviceIdentifier,
NSString *bundleIdentifier,
NSError **error
) {
if (bundleIdentifier.length == 0) {
if (error != NULL) {
*error = WMMMakeError(12, @"A bundle identifier is required.");
}
return nil;
}
WMMMobileDeviceFunctions functions;
if (!WMMLoadFunctions(&functions, error)) {
return nil;
}
AMDeviceRef device = WMMCopyConnectedDevice(&functions, deviceIdentifier, error);
if (device == NULL) {
return nil;
}
if (!WMMConnectAndValidateDevice(&functions, device, YES, error)) {
functions.AMDeviceRelease(device);
return nil;
}
NSString *deviceName = WMMDeviceStringValue(&functions, device, CFSTR("DeviceName")) ?: @"Unknown Device";
NSString *productType = WMMDeviceStringValue(&functions, device, CFSTR("ProductType")) ?: @"";
NSString *productVersion = WMMDeviceStringValue(&functions, device, CFSTR("ProductVersion")) ?: @"";
NSString *resolvedDeviceIdentifier = WMMResolvedDeviceIdentifier(&functions, device);
CFDictionaryRef appDictionary = NULL;
const int lookupStatus = functions.AMDeviceLookupApplications(device, NULL, &appDictionary);
WMMDisconnectDevice(&functions, device, YES);
functions.AMDeviceRelease(device);
if (lookupStatus != 0 || appDictionary == NULL) {
if (error != NULL) {
*error = WMMMakeError(lookupStatus, [NSString stringWithFormat:@"AMDeviceLookupApplications failed (%d).", lookupStatus]);
}
return nil;
}
NSDictionary *bridgedApps = CFBridgingRelease(appDictionary);
NSDictionary *appInfo = [bridgedApps objectForKey:bundleIdentifier];
if (![appInfo isKindOfClass:[NSDictionary class]]) {
if (error != NULL) {
*error = WMMMakeError(13, [NSString stringWithFormat:@"No app record was found for %@.", bundleIdentifier]);
}
return nil;
}
NSMutableDictionary<NSString *, NSString *> *details = [NSMutableDictionary dictionary];
[appInfo enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
(void)stop;
if (![key isKindOfClass:[NSString class]]) {
return;
}
NSString *detailKey = (NSString *)key;
if ([obj isKindOfClass:[NSString class]]) {
details[detailKey] = (NSString *)obj;
return;
}
if ([obj isKindOfClass:[NSNumber class]]) {
details[detailKey] = [(NSNumber *)obj stringValue];
return;
}
if ([obj isKindOfClass:[NSArray class]] || [obj isKindOfClass:[NSDictionary class]]) {
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:obj options:0 error:nil];
if (jsonData != nil) {
details[detailKey] = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding] ?: @"";
}
}
}];
return @{
@"deviceName": deviceName,
@"deviceIdentifier": resolvedDeviceIdentifier,
@"productType": productType,
@"productVersion": productVersion,
@"bundleIdentifier": bundleIdentifier,
@"details": details
};
}
BOOL
WMMCopyConnectedDeviceAppSubtreeToLocalDirectory(
NSString *deviceIdentifier,
NSString *bundleIdentifier,
NSString *relativePath,
NSURL *destinationDirectoryURL,
NSError **error
) {
if (bundleIdentifier.length == 0) {
if (error != NULL) {
*error = WMMMakeError(14, @"A bundle identifier is required.");
}
return NO;
}
if (destinationDirectoryURL == nil || !destinationDirectoryURL.isFileURL) {
if (error != NULL) {
*error = WMMMakeError(15, @"A local destination directory is required.");
}
return NO;
}
WMMMobileDeviceFunctions functions;
if (!WMMLoadFunctions(&functions, error)) {
return NO;
}
AMDeviceRef device = WMMCopyConnectedDevice(&functions, deviceIdentifier, error);
if (device == NULL) {
return NO;
}
if (!WMMConnectAndValidateDevice(&functions, device, YES, error)) {
functions.AMDeviceRelease(device);
return NO;
}
AMDServiceConnectionRef backingServiceConnection = NULL;
AFCConnectionRef afcConnection = WMMCreateVendAFCConnection(
&functions,
device,
bundleIdentifier,
&backingServiceConnection,
error
);
if (afcConnection == NULL) {
WMMCloseVendSession(&functions, device, YES, NULL, backingServiceConnection);
functions.AMDeviceRelease(device);
return NO;
}
NSString *normalizedPath = relativePath.length == 0 ? @"/" : relativePath;
if (![normalizedPath hasPrefix:@"/"]) {
normalizedPath = [@"/" stringByAppendingString:normalizedPath];
}
NSFileManager *fileManager = [NSFileManager defaultManager];
[fileManager removeItemAtURL:destinationDirectoryURL error:nil];
BOOL success = WMMCopyAFCTreeToLocalURL(
&functions,
afcConnection,
normalizedPath,
destinationDirectoryURL,
error
);
WMMCloseVendSession(&functions, device, YES, afcConnection, backingServiceConnection);
functions.AMDeviceRelease(device);
if (!success) {
[fileManager removeItemAtURL:destinationDirectoryURL error:nil];
}
return success;
}