// // AppleMobileDeviceBridge.m // World Manager for Minecraft // // Created by OpenAI on 2026-05-26. // #import "AppleMobileDeviceBridge.h" #import #import #import NSErrorDomain const WMMMobileDeviceErrorDomain = @"WMMMobileDeviceErrorDomain"; 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 *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 *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 *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 *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 *keys = @[ @"ConnectionType", @"InterfaceType", @"DeviceName", @"ProductType", @"ProductVersion", @"UniqueDeviceID", @"SerialNumber", @"WiFiAddress", @"EthernetAddress" ]; NSMutableDictionary *values = [NSMutableDictionary dictionary]; for (NSString *key in keys) { NSString *value = WMMDeviceStringValue(functions, device, (__bridge CFStringRef)key); values[key] = value.length > 0 ? value : @""; } NSLog(@"[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; } NSLog(@"[HouseArrest] Trying AMDeviceCreateHouseArrestService for %@", bundleIdentifier); AFCConnectionRef directConnection = NULL; int directStatus = functions->AMDeviceCreateHouseArrestService( device, (__bridge CFStringRef)bundleIdentifier, NULL, &directConnection ); NSLog(@"[HouseArrest] AMDeviceCreateHouseArrestService returned %d connection=%p", directStatus, directConnection); if (directStatus == 0 && directConnection != NULL) { return directConnection; } NSArray *commands = @[ @"VendDocuments", @"VendContainer" ]; NSMutableArray *failures = [NSMutableArray array]; [failures addObject:[NSString stringWithFormat:@"AMDeviceCreateHouseArrestService returned %d", directStatus]]; for (NSString *command in commands) { NSLog(@"[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) { NSLog(@"[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); NSLog(@"[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 ); NSLog(@"[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 ); NSLog(@"[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); NSLog(@"[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; } NSLog(@"[HouseArrest] %@ completed and AFC initialized", command); return afcConnection; } functions->AMDServiceConnectionInvalidate(serviceConnection); NSLog(@"[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) { NSLog(@"[HouseArrest] %@ rejected with error: %@", command, serviceError); [failures addObject:[NSString stringWithFormat:@"%@ was rejected: %@", command, serviceError]]; } else { NSLog(@"[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 **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 *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 * _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 *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 *info) { NSString *candidate = info[@"st_mtime"] ?: info[@"st_birthtime"]; if (candidate.length == 0) { return nil; } return WMMDateFromAFCTimestampString(candidate); } static long long WMMFileSizeFromAFCInfo(NSDictionary *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 * _Nullable WMMCopyAFCTreeMetrics( WMMMobileDeviceFunctions *functions, AFCConnectionRef afcConnection, NSString *remotePath, NSError **error ) { NSDictionary *info = WMMCopyAFCFileInfo(functions, afcConnection, remotePath, error); if (info == nil) { return nil; } NSDate *latestModificationDate = WMMModificationDateFromAFCInfo(info); NSMutableArray *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 *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 *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 *entries, NSString *candidate) { for (NSString *entry in entries) { if ([entry isEqualToString:candidate]) { return YES; } } return NO; } 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 * _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 *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 *WMMBuildPackReferenceSummary( NSString *name, NSString *contentType, NSString * _Nullable uuid, NSString * _Nullable version, NSString *source ) { NSMutableDictionary *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 *> *WMMParsePackReferenceSummariesFromData( NSData *data, NSString *contentType ) { id jsonObject = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; if (![jsonObject isKindOfClass:[NSArray class]]) { return @[]; } NSMutableArray *> *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 *> *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 *> *WMMLoadEmbeddedPackReferenceSummaries( WMMMobileDeviceFunctions *functions, AFCConnectionRef afcConnection, NSString *remoteFolderPath, NSString *contentType ) { NSMutableArray *childFolders = nil; if (WMMReadAFCDirectory(functions, afcConnection, remoteFolderPath, &childFolders) != 0 || childFolders == nil) { return @[]; } NSMutableArray *> *references = [NSMutableArray array]; for (NSString *childFolder in childFolders) { if ([childFolder isEqualToString:@"."] || [childFolder isEqualToString:@".."]) { continue; } NSString *manifestPath = [[remoteFolderPath stringByAppendingPathComponent:childFolder] stringByAppendingPathComponent:@"manifest.json"]; NSDictionary *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 *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 *WMMBuildShallowMinecraftItemSummary( NSString *contentType, NSString *collectionFolderName, NSString *itemRelativePath, NSString *folderName, NSArray *entries ) { NSMutableDictionary *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 *> *results ) { NSString *collectionRemotePath = [rootRemotePath stringByAppendingPathComponent:collectionFolderName]; NSMutableArray *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 *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 *> *embeddedCollections = @[ @{ @"folder": @"behavior_packs", @"type": @"Behavior Pack" }, @{ @"folder": @"resource_packs", @"type": @"Resource Pack" } ]; for (NSDictionary *embeddedCollection in embeddedCollections) { NSString *embeddedFolder = embeddedCollection[@"folder"]; NSString *embeddedType = embeddedCollection[@"type"]; NSString *embeddedCollectionPath = [itemRemotePath stringByAppendingPathComponent:embeddedFolder]; NSMutableArray *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 *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 * _Nullable WMMCopyConnectedDeviceSummaries(NSError **error) { WMMMobileDeviceFunctions functions; if (!WMMLoadFunctions(&functions, error)) { return nil; } NSArray *devices = WMMCopyConnectedDevices(&functions, error); if (devices.count == 0) { return nil; } NSMutableArray *> *summaries = [NSMutableArray array]; NSMutableDictionary *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 *lhs, NSDictionary *rhs) { return [lhs[@"deviceName"] localizedStandardCompare:rhs[@"deviceName"]]; }]; return @{ @"devices": summaries }; } NSDictionary * _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 *entries = nil; const int directoryStatus = WMMReadAFCDirectory(&functions, afcConnection, normalizedPath, &entries); if (directoryStatus != 0) { NSMutableArray *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 * _Nullable WMMCopyConnectedDeviceAppPathProbeResults( NSString *deviceIdentifier, NSString *bundleIdentifier, NSArray *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 *> *results = [NSMutableArray array]; for (NSString *candidate in paths) { if (![candidate isKindOfClass:[NSString class]]) { continue; } NSMutableArray *entries = nil; int status = WMMReadAFCDirectory(&functions, afcConnection, candidate, &entries); NSMutableDictionary *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 * _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 *> *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 *lhs, NSDictionary *rhs) { return [lhs[@"displayName"] localizedStandardCompare:rhs[@"displayName"]]; }]; return @{ @"deviceName": deviceName, @"deviceIdentifier": resolvedDeviceIdentifier, @"productType": productType, @"productVersion": productVersion, @"applications": applications }; } NSDictionary * _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 *> *items = [NSMutableArray array]; NSArray *> *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 *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 * _Nullable WMMCopyConnectedDeviceMinecraftMetadataBatch( NSString *deviceIdentifier, NSString *bundleIdentifier, NSString *relativePath, NSArray *> *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 *> *results = [NSMutableArray array]; for (NSDictionary *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 *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 *> *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 *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 }; } 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 * _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 *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 * _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 *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; }