FBDeviceControl/Management/FBAFCConnection.m (331 lines of code) (raw):
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import "FBAFCConnection.h"
#import <FBControlCore/FBControlCore.h>
#include <dlfcn.h>
#import "FBDeviceControlError.h"
#import "FBAMDServiceConnection.h"
static NSString *AFCCodeKey = @"AFCCode";
static NSString *AFCDomainKey = @"AFCDomain";
static AFCCalls defaultCalls;
static void AFCConnectionCallback(void *connectionRefPtr, void *arg1, void *afcOperationPtr)
{
AFCConnectionRef connection = connectionRefPtr;
AFCOperationRef operation = afcOperationPtr;
id<FBControlCoreLogger> logger = FBControlCoreGlobalConfiguration.defaultLogger;
[logger logFormat:@"Connection %@, operation %@", connection, operation];
}
@implementation FBAFCConnection
#pragma mark Initializers
- (instancetype)initWithConnection:(AFCConnectionRef)connection calls:(AFCCalls)calls logger:(id<FBControlCoreLogger>)logger
{
self = [super init];
if (!self) {
return nil;
}
_connection = connection;
_calls = calls;
_logger = logger;
return self;
}
+ (FBFutureContext<FBAFCConnection *> *)afcFromServiceConnection:(FBAMDServiceConnection *)serviceConnection calls:(AFCCalls)calls logger:(id<FBControlCoreLogger>)logger queue:(dispatch_queue_t)queue
{
return [[FBFuture
onQueue:queue resolve:^{
FBAFCConnection *connection = [serviceConnection asAFCConnectionWithCalls:calls callback:AFCConnectionCallback logger:logger];
if (![connection connectionIsValid]) {
return [[FBDeviceControlError
describeFormat:@"Created AFC Connection %@ is not valid", connection]
failFuture];
}
return [FBFuture futureWithResult:connection];
}]
onQueue:queue contextualTeardown:^(FBAFCConnection *connection, FBFutureState __) {
[connection closeWithError:nil];
return FBFuture.empty;
}];
}
#pragma mark Public Methods
- (BOOL)copyFromHost:(NSString *)hostPath toContainerPath:(NSString *)containerPath error:(NSError **)error
{
BOOL isDir;
if (![NSFileManager.defaultManager fileExistsAtPath:hostPath isDirectory:&isDir]) {
return NO;
}
if (isDir) {
containerPath = [containerPath stringByAppendingPathComponent:hostPath.lastPathComponent];
BOOL success = [self createDirectory:containerPath error:error];
if (!success) {
return NO;
}
return [self copyContentsOfHostDirectory:hostPath toContainerPath:containerPath error:error];
} else {
return [self copyFileFromHost:hostPath toContainerPath:[containerPath stringByAppendingPathComponent:hostPath.lastPathComponent] error:error];
}
}
- (BOOL)createDirectory:(NSString *)path error:(NSError **)error
{
[self.logger logFormat:@"Creating Directory %@", path];
mach_error_t result = self.calls.DirectoryCreate(self.connection, [path UTF8String]);
if (result != 0) {
return [[FBDeviceControlError
describeFormat:@"Error when creating directory: %@", [self errorMessageWithCode:result]]
failBool:error];
}
[self.logger logFormat:@"Created Directory %@", path];
return YES;
}
const char *SingleDot = ".";
const char *DoubleDot = "..";
- (NSArray<NSString *> *)contentsOfDirectory:(NSString *)path error:(NSError **)error
{
[self.logger logFormat:@"Listing contents of directory %@", path];
CFTypeRef directory;
mach_error_t result = self.calls.DirectoryOpen(self.connection, path.UTF8String, &directory);
if (result != 0) {
return [[FBDeviceControlError
describeFormat:@"Error when opening directory %@: %@", path, [self errorMessageWithCode:result]]
fail:error];
}
NSMutableArray<NSString *> *dirs = [NSMutableArray array];
while (YES) {
char *listing = nil;
self.calls.DirectoryRead(self.connection, directory, &listing);
if (!listing) {
break;
}
if (strcmp(listing, SingleDot) == 0 || strcmp(listing, DoubleDot) == 0) {
continue;
}
[dirs addObject:[NSString stringWithUTF8String:listing]];
}
self.calls.DirectoryClose(self.connection, directory);
[self.logger logFormat:@"Contents of directory %@ %@", path, [FBCollectionInformation oneLineDescriptionFromArray:dirs]];
return [NSArray arrayWithArray:dirs];
}
- (NSData *)contentsOfPath:(NSString *)path error:(NSError **)error
{
[self.logger logFormat:@"Contents of path %@", path];
CFTypeRef file;
mach_error_t result = self.calls.FileRefOpen(self.connection, path.UTF8String, FBAFCReadOnlyMode, &file);
if (result != 0) {
return [[FBDeviceControlError
describeFormat:@"Error when opening file %@: %@", path, [self errorMessageWithCode:result]]
fail:error];
}
self.calls.FileRefSeek(self.connection, file, 0, 2);
uint64_t offset = 0;
self.calls.FileRefTell(self.connection, file, &offset);
NSMutableData *buffer = [[NSMutableData alloc] initWithLength:offset];
uint64_t len = offset;
uint64_t toRead = len;
self.calls.FileRefSeek(self.connection, file, 0, 0);
while (toRead > 0) {
uint64_t read = toRead;
result = self.calls.FileRefRead(self.connection, file, [buffer mutableBytes] + (len - toRead), &read);
toRead -= read;
if (result != 0) {
self.calls.FileRefClose(self.connection, file);
return [[FBDeviceControlError
describeFormat:@"Error when reading file %@: %@", path, [self errorMessageWithCode:result]]
fail:error];
}
}
self.calls.FileRefClose(self.connection, file);
[self.logger logFormat:@"Read %lu bytes from path %@", buffer.length, path];
return buffer;
}
- (BOOL)removePath:(NSString *)path recursively:(BOOL)recursively error:(NSError **)error
{
if (recursively) {
return [self removePathAndContents:path error:error];
} else {
[self.logger logFormat:@"Removing file path %@", path];
mach_error_t result = self.calls.RemovePath(self.connection, [path UTF8String]);
if (result != 0) {
return [[FBDeviceControlError
describeFormat:@"Error when removing path %@: %@", path, [self errorMessageWithCode:result]]
failBool:error];
}
[self.logger logFormat:@"Removed file path %@", path];
return YES;
}
}
- (BOOL)renamePath:(NSString *)path destination:(NSString *)destination error:(NSError **)error
{
mach_error_t result = self.calls.RenamePath(self.connection, path.UTF8String, destination.UTF8String);
if (result != 0) {
return [[FBDeviceControlError
describeFormat:@"Error when renaming from %@ to %@: %@", path, destination, [self errorMessageWithCode:result]]
failBool:error];
}
return YES;
}
- (BOOL)closeWithError:(NSError **)error
{
if (!_connection) {
return [[FBDeviceControlError
describe:@"Cannot close a non-existant connection"]
failBool:error];
}
NSString *connectionDescription = CFBridgingRelease(CFCopyDescription(self.connection));
[self.logger logFormat:@"Closing %@", connectionDescription];
int status = self.calls.ConnectionClose(self.connection);
if (status != 0) {
return [[FBDeviceControlError
describeFormat:@"Failed to close connection with error %d", status]
failBool:error];
}
[self.logger logFormat:@"Closed AFC Connection %@", connectionDescription];
// AFCConnectionClose does release the connection.
_connection = NULL;
return YES;
}
#pragma mark Private
- (BOOL)copyFileFromHost:(NSString *)hostPath toContainerPath:(NSString *)containerPath error:(NSError **)error
{
[self.logger logFormat:@"Copying %@ to %@", hostPath, containerPath];
NSData *data = [NSData dataWithContentsOfFile:hostPath];
if (!data) {
return [[FBDeviceControlError
describeFormat:@"Could not find file on host: %@", hostPath]
failBool:error];
}
CFTypeRef fileReference;
mach_error_t result = self.calls.FileRefOpen(self.connection, containerPath.UTF8String, FBAFCreateReadAndWrite, &fileReference);
if (result != 0) {
return [[FBDeviceControlError
describeFormat:@"Error when opening file %@: %@", containerPath, [self errorMessageWithCode:result]]
failBool:error];
}
__block mach_error_t writeResult = 0;
[data enumerateByteRangesUsingBlock:^(const void *bytes, NSRange byteRange, BOOL *stop) {
if (byteRange.length == 0) {
return;
}
writeResult = self.calls.FileRefWrite(self.connection, fileReference, bytes, byteRange.length);
if (writeResult != 0) {
*stop = YES;
}
}];
self.calls.FileRefClose(self.connection, fileReference);
if (writeResult != 0) {
return [[FBDeviceControlError
describeFormat:@"Error when writing file %@: %@", containerPath, [self errorMessageWithCode:writeResult]]
failBool:error];
}
[self.logger logFormat:@"Copied from %@ to %@", hostPath, containerPath];
return YES;
}
- (BOOL)copyContentsOfHostDirectory:(NSString *)hostDirectory toContainerPath:(NSString *)containerPath error:(NSError **)error
{
[self.logger logFormat:@"Copying from %@ to %@", hostDirectory, containerPath];
NSFileManager *fileManager = NSFileManager.defaultManager;
NSDirectoryEnumerator<NSURL *> *urls = [fileManager
enumeratorAtURL:[NSURL fileURLWithPath:hostDirectory]
includingPropertiesForKeys:@[NSURLIsDirectoryKey]
options:NSDirectoryEnumerationSkipsSubdirectoryDescendants
errorHandler:NULL];
for (NSURL *url in urls) {
BOOL success = [self copyFromHost:url.path toContainerPath:containerPath error:error];
if (!success) {
[self.logger logFormat:@"Failed to copy %@ to %@ with error %@", url, containerPath, *error];
return NO;
}
}
[self.logger logFormat:@"Copied from %@ to %@", hostDirectory, containerPath];
return YES;
}
- (BOOL)removePathAndContents:(NSString *)path error:(NSError **)error
{
[self.logger logFormat:@"Removing path %@ and contents", path];
AFCOperationRef operation = self.calls.OperationCreateRemovePathAndContents(
CFGetAllocator(self.connection),
(__bridge CFStringRef _Nonnull)(path),
NULL
);
if (operation == nil) {
return [[FBDeviceControlError
describeFormat:@"Operation for path removal %@ couldn't be created", path]
failBool:error];
}
int op_result = self.calls.ConnectionProcessOperation(self.connection, operation);
if (op_result != 0) {
CFRelease(operation);
return [[FBDeviceControlError
describeFormat:@"Operation couldn't be processed (%d)", op_result]
failBool:error];
}
BOOL success = [self afcOperationSucceeded:operation error:error];
CFRelease(operation);
return success;
}
- (BOOL)afcOperationSucceeded:(AFCOperationRef)operation error:(NSError **)error
{
int status = self.calls.OperationGetResultStatus(operation);
if (status == 0) {
return YES;
}
NSDictionary<NSString *, id> *infoDictionary = (__bridge id)(self.calls.OperationGetResultObject(operation));
if (![infoDictionary isKindOfClass:[NSDictionary class]]) {
return [[FBDeviceControlError
describeFormat:@"AFCOperation failed. status: %d, result object: %@", status, infoDictionary]
failBool:error];
}
NSNumber *code = infoDictionary[AFCCodeKey];
NSString *domain = infoDictionary[AFCDomainKey];
if (!code || ![code respondsToSelector:@selector(integerValue)] || !domain || ![domain isKindOfClass:NSString.class]) {
return [[FBDeviceControlError
describeFormat:@"AFCOperation failed. status: %d, result object: %@", status, infoDictionary]
failBool:error];
}
return [[[FBDeviceControlError
describeFormat:@"AFCOperation failed. underlying error: %@", infoDictionary]
code:code.integerValue]
failBool:error];
}
- (NSString *)errorMessageWithCode:(int)code
{
const char *name = self.calls.ErrorString(code);
NSDictionary<NSString *, id> *info = CFBridgingRelease(self.calls.ConnectionCopyLastErrorInfo(self.connection));
return [NSString stringWithFormat:@"%s %@", name, [FBCollectionInformation oneLineDescriptionFromDictionary:info]];
}
#pragma mark AFC Calls
+ (AFCCalls)defaultCalls
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self populateCallsFromMobileDevice:&defaultCalls];
});
return defaultCalls;
}
#pragma mark Private
- (BOOL)connectionIsValid
{
return (BOOL) self.calls.ConnectionIsValid(self.connection);
}
+ (void)populateCallsFromMobileDevice:(AFCCalls *)calls
{
void *handle = [[NSBundle bundleWithIdentifier:@"com.apple.mobiledevice"] dlopenExecutablePath];
calls->ConnectionClose = FBGetSymbolFromHandle(handle, "AFCConnectionClose");
calls->ConnectionCopyLastErrorInfo = FBGetSymbolFromHandle(handle, "AFCConnectionCopyLastErrorInfo");
calls->ConnectionIsValid = FBGetSymbolFromHandle(handle, "AFCConnectionIsValid");
calls->ConnectionOpen = FBGetSymbolFromHandle(handle, "AFCConnectionOpen");
calls->ConnectionProcessOperation = FBGetSymbolFromHandle(handle, "AFCConnectionProcessOperation");
calls->Create = FBGetSymbolFromHandle(handle, "AFCConnectionCreate");
calls->DirectoryClose = FBGetSymbolFromHandle(handle, "AFCDirectoryClose");
calls->DirectoryCreate = FBGetSymbolFromHandle(handle, "AFCDirectoryCreate");
calls->DirectoryOpen = FBGetSymbolFromHandle(handle, "AFCDirectoryOpen");
calls->DirectoryRead = FBGetSymbolFromHandle(handle, "AFCDirectoryRead");
calls->ErrorString = FBGetSymbolFromHandle(handle, "AFCErrorString");
calls->FileRefClose = FBGetSymbolFromHandle(handle, "AFCFileRefClose");
calls->FileRefOpen = FBGetSymbolFromHandle(handle, "AFCFileRefOpen");
calls->FileRefRead = FBGetSymbolFromHandle(handle, "AFCFileRefRead");
calls->FileRefSeek = FBGetSymbolFromHandle(handle, "AFCFileRefSeek");
calls->FileRefTell = FBGetSymbolFromHandle(handle, "AFCFileRefTell");
calls->FileRefWrite = FBGetSymbolFromHandle(handle, "AFCFileRefWrite");
calls->OperationCreateRemovePathAndContents = FBGetSymbolFromHandle(handle, "AFCOperationCreateRemovePathAndContents");
calls->OperationGetResultObject = FBGetSymbolFromHandle(handle, "AFCOperationGetResultObject");
calls->OperationGetResultStatus = FBGetSymbolFromHandle(handle, "AFCOperationGetResultStatus");
calls->RemovePath = FBGetSymbolFromHandle(handle, "AFCRemovePath");
calls->RenamePath = FBGetSymbolFromHandle(handle, "AFCRenamePath");
calls->SetSecureContext = FBGetSymbolFromHandle(handle, "AFCConnectionSetSecureContext");
}
@end