FBControlCore/Commands/FBFileContainer.m (465 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 "FBFileContainer.h"
#import "FBCollectionInformation.h"
#import "FBControlCoreError.h"
#import "FBProcessBuilder.h"
#import "FBProvisioningProfileCommands.h"
FBFileContainerKind const FBFileContainerKindApplication = @"application";
FBFileContainerKind const FBFileContainerKindAuxillary = @"auxillary";
FBFileContainerKind const FBFileContainerKindCrashes = @"crashes";
FBFileContainerKind const FBFileContainerKindDiskImages = @"disk_images";
FBFileContainerKind const FBFileContainerKindGroup = @"group";
FBFileContainerKind const FBFileContainerKindMDMProfiles = @"mdm_profiles";
FBFileContainerKind const FBFileContainerKindMedia = @"media";
FBFileContainerKind const FBFileContainerKindProvisioningProfiles = @"provisioning_profiles";
FBFileContainerKind const FBFileContainerKindRoot = @"root";
FBFileContainerKind const FBFileContainerKindSpringboardIcons = @"springboard_icons";
FBFileContainerKind const FBFileContainerKindSymbols = @"symbols";
FBFileContainerKind const FBFileContainerKindWallpaper = @"wallpaper";
@interface FBContainedFile_Host : NSObject <FBContainedFile>
@property (nonatomic, strong, readonly) NSFileManager *fileManager;
@property (nonatomic, copy, readonly) NSString *path;
@end
@implementation FBContainedFile_Host
#pragma mark Initializers
- (instancetype)initWithFileManager:(NSFileManager *)fileManager path:(NSString *)path
{
self = [super init];
if (!self) {
return nil;
}
_fileManager = fileManager;
_path = path;
return self;
}
#pragma mark FBContainedFile
- (BOOL)removeItemWithError:(NSError **)error
{
return [self.fileManager removeItemAtPath:self.path error:error];
}
- (NSArray<NSString *> *)contentsOfDirectoryWithError:(NSError **)error
{
return [self.fileManager contentsOfDirectoryAtPath:self.path error:error];
}
- (NSData *)contentsOfFileWithError:(NSError **)error
{
return [NSData dataWithContentsOfFile:self.path options:0 error:error];
}
- (BOOL)moveTo:(id<FBContainedFile>)destination error:(NSError **)error
{
if (![destination isKindOfClass:FBContainedFile_Host.class]) {
return [[FBControlCoreError
describeFormat:@"Cannot move to %@, it is not on the host filesystem", destination]
failBool:error];
}
FBContainedFile_Host *hostDestination = (FBContainedFile_Host *) destination;
return [self.fileManager moveItemAtPath:self.path toPath:hostDestination.path error:error];
}
- (BOOL)createDirectoryWithError:(NSError **)error
{
return [self.fileManager createDirectoryAtPath:self.path withIntermediateDirectories:YES attributes:nil error:error];
}
- (BOOL)fileExistsIsDirectory:(BOOL *)isDirectoryOut
{
return [self.fileManager fileExistsAtPath:self.path isDirectory:isDirectoryOut];
}
- (BOOL)populateWithContentsOfHostPath:(NSString *)path error:(NSError **)error
{
return [self.fileManager copyItemAtPath:path toPath:self.path error:error];
}
- (BOOL)populateHostPathWithContents:(NSString *)path error:(NSError **)error
{
return [self.fileManager copyItemAtPath:self.path toPath:path error:error];
}
- (id<FBContainedFile>)fileByAppendingPathComponent:(NSString *)component error:(NSError **)error
{
return [[FBContainedFile_Host alloc] initWithFileManager:self.fileManager path:[self.path stringByAppendingPathComponent:component]];
}
- (NSString *)pathOnHostFileSystem
{
return self.path;
}
#pragma mark NSObject
- (NSString *)description
{
return [NSString stringWithFormat:@"Host File %@", self.path];
}
@end
@interface FBContainedFile_Mapped_Host : NSObject <FBContainedFile>
@property (nonatomic, copy, readonly) NSDictionary<NSString *, NSString *> *mappingPaths;
@property (nonatomic, strong, readonly) NSFileManager *fileManager;
@end
@implementation FBContainedFile_Mapped_Host
- (instancetype)initWithMappingPaths:(NSDictionary<NSString *, NSString *> *)mappingPaths fileManager:(NSFileManager *)fileManager;
{
self = [super init];
if (!self) {
return nil;
}
_mappingPaths = mappingPaths;
_fileManager = fileManager;
return self;
}
#pragma mark FBContainedFile
- (BOOL)removeItemWithError:(NSError **)error
{
return [[FBControlCoreError
describeFormat:@"%@ does not operate on root virtual containers", NSStringFromSelector(_cmd)]
failBool:error];
}
- (NSArray<NSString *> *)contentsOfDirectoryWithError:(NSError **)error
{
return self.mappingPaths.allKeys;
}
- (BOOL)createDirectoryWithError:(NSError **)error
{
return [[FBControlCoreError
describeFormat:@"%@ does not operate on root virtual containers", NSStringFromSelector(_cmd)]
failBool:error];
}
- (NSData *)contentsOfFileWithError:(NSError **)error
{
return [[FBControlCoreError
describeFormat:@"%@ does not operate on root virtual containers", NSStringFromSelector(_cmd)]
fail:error];
}
- (BOOL)fileExistsIsDirectory:(BOOL *)isDirectoryOut
{
return NO;
}
- (BOOL)moveTo:(id<FBContainedFile>)destination error:(NSError **)error
{
return [[FBControlCoreError
describe:@"Moving files does not work on root virtual containers"]
failBool:error];
}
- (BOOL)populateWithContentsOfHostPath:(NSString *)path error:(NSError **)error
{
return [[FBControlCoreError
describeFormat:@"%@ does not operate on root virtual containers", NSStringFromSelector(_cmd)]
failBool:error];
}
- (BOOL)populateHostPathWithContents:(NSString *)path error:(NSError **)error
{
return [[FBControlCoreError
describeFormat:@"%@ does not operate on root virtual containers", NSStringFromSelector(_cmd)]
failBool:error];
}
- (id<FBContainedFile>)fileByAppendingPathComponent:(NSString *)component error:(NSError **)error
{
// If the provided path represents the root (the mapping itself), then there's nothing to map to.
NSArray<NSString *> *pathComponents = component.pathComponents;
if ([FBContainedFile_Mapped_Host isRootPathOfContainer:pathComponents]) {
return self;
}
NSString *firstComponent = pathComponents.firstObject;
NSString *nextPath = [FBContainedFile_Mapped_Host popFirstPathComponent:pathComponents];
NSString *mappedPath = self.mappingPaths[firstComponent];
if (!mappedPath) {
return [[FBControlCoreError
describeFormat:@"'%@' is not a valid root path out of %@", firstComponent, [FBCollectionInformation oneLineDescriptionFromArray:self.mappingPaths.allKeys]]
fail:error];
}
id<FBContainedFile> mapped = [[FBContainedFile_Host alloc] initWithFileManager:self.fileManager path:mappedPath];
return [mapped fileByAppendingPathComponent:nextPath error:error];
}
- (NSString *)pathOnHostFileSystem
{
return nil;
}
#pragma mark NSObject
- (NSString *)description
{
return [NSString stringWithFormat:@"Root mapping: %@", [FBCollectionInformation oneLineDescriptionFromArray:self.mappingPaths.allKeys]];
}
#pragma mark Private
+ (BOOL)isRootPathOfContainer:(NSArray<NSString *> *)pathComponents
{
// If no path components this must be the root
if (pathComponents.count == 0) {
return YES;
}
// The root is also signified by a query for the root of the container.
NSString *firstPath = pathComponents.firstObject;
if (pathComponents.count == 1 && ([firstPath isEqualToString:@"."] || [firstPath isEqualToString:@"/"])) {
return YES;
}
// Otherwise we can't be the root path
return NO;
}
+ (NSString *)popFirstPathComponent:(NSArray<NSString *> *)pathComponents
{
// Re-assemble the mapped path, discarding the re-mapped first path component.
BOOL isFirstPathComponent = YES;
NSString *next = @"";
for (NSString *pathComponent in pathComponents) {
if (isFirstPathComponent) {
isFirstPathComponent = NO;
continue;
}
next = [next stringByAppendingPathComponent:pathComponent];
}
return next;
}
@end
@interface FBContainedFile_ContainedRoot : NSObject <FBFileContainer>
@property (nonatomic, strong, readonly) dispatch_queue_t queue;
@property (nonatomic, strong, readonly) id<FBContainedFile> rootFile;
@end
@implementation FBContainedFile_ContainedRoot
- (instancetype)initWithRootFile:(id<FBContainedFile>)rootFile queue:(dispatch_queue_t)queue
{
self = [super init];
if (!self) {
return nil;
}
_rootFile = rootFile;
_queue = queue;
return self;
}
#pragma mark FBFileCommands
- (FBFuture<NSNull *> *)copyFromHost:(NSString *)sourcePath toContainer:(NSString *)destinationPath
{
return [[self
mapToContainedFile:destinationPath]
onQueue:self.queue fmap:^ FBFuture<NSNull *> * (id<FBContainedFile> destination) {
// Attempt to delete first to overwrite
NSError *error;
destination = [destination fileByAppendingPathComponent:sourcePath.lastPathComponent error:&error];
if (!destination) {
return [FBFuture futureWithError:error];
}
[destination removeItemWithError:nil];
if (![destination populateWithContentsOfHostPath:sourcePath error:&error]) {
return [[[FBControlCoreError
describeFormat:@"Could not copy from %@ to %@: %@", sourcePath, destinationPath, error]
causedBy:error]
failFuture];
}
return FBFuture.empty;
}];
}
- (FBFuture<NSString *> *)copyFromContainer:(NSString *)sourcePath toHost:(NSString *)destinationPath
{
return [[self
mapToContainedFile:sourcePath]
onQueue:self.queue fmap:^ FBFuture<NSString *> * (id<FBContainedFile> source) {
BOOL sourceIsDirectory = NO;
if (![source fileExistsIsDirectory:&sourceIsDirectory]) {
return [[FBControlCoreError
describeFormat:@"Source path does not exist: %@", source]
failFuture];
}
NSString *dstPath = destinationPath;
if (!sourceIsDirectory) {
NSError *createDirectoryError;
if (![NSFileManager.defaultManager createDirectoryAtPath:dstPath withIntermediateDirectories:YES attributes:@{} error:&createDirectoryError]) {
return [[[FBControlCoreError
describeFormat:@"Could not create temporary directory: %@", createDirectoryError]
causedBy:createDirectoryError]
failFuture];
}
dstPath = [dstPath stringByAppendingPathComponent:[sourcePath lastPathComponent]];
}
// if it already exists at the destination path we should remove it before copying again
BOOL destinationIsDirectory = NO;
if ([NSFileManager.defaultManager fileExistsAtPath:dstPath isDirectory:&destinationIsDirectory]) {
NSError *removeError;
if (![NSFileManager.defaultManager removeItemAtPath:dstPath error:&removeError]) {
return [[[FBControlCoreError
describeFormat:@"Could not remove %@", dstPath]
causedBy:removeError]
failFuture];
}
}
NSError *copyError;
if (![source populateHostPathWithContents:dstPath error:©Error]) {
return [[[FBControlCoreError
describeFormat:@"Could not copy from %@ to %@: %@", source, dstPath, copyError]
causedBy:copyError]
failFuture];
}
return [FBFuture futureWithResult:destinationPath];
}];
}
- (FBFuture<FBFuture<NSNull *> *> *)tail:(NSString *)path toConsumer:(id<FBDataConsumer>)consumer
{
return [[[self
mapToContainedFile:path]
onQueue:self.queue fmap:^ FBFuture<FBProcess<NSNull *, id<FBDataConsumer>, NSData *> *> * (id<FBContainedFile> fileToTail) {
NSString *pathOnHostFileSystem = fileToTail.pathOnHostFileSystem;
if (!pathOnHostFileSystem) {
return [[FBControlCoreError
describeFormat:@"Cannot tail %@, it is not on the local filesystem", fileToTail]
failFuture];
}
return [[[[FBProcessBuilder
withLaunchPath:@"/usr/bin/tail"]
withArguments:@[@"-c+1", @"-f", pathOnHostFileSystem]]
withStdOutConsumer:consumer]
start];
}]
onQueue:self.queue map:^(FBProcess *process) {
return [process.statLoc
onQueue:self.queue respondToCancellation:^{
return [process sendSignal:SIGTERM backingOffToKillWithTimeout:1 logger:nil];
}];
}];
}
- (FBFuture<NSNull *> *)createDirectory:(NSString *)directoryPath
{
return [[self
mapToContainedFile:directoryPath]
onQueue:self.queue fmap:^ FBFuture<NSNull *> * (id<FBContainedFile> directory) {
NSError *error;
if (![directory createDirectoryWithError:&error]) {
return [[[FBControlCoreError
describeFormat:@"Could not create directory %@: %@", directory, error]
causedBy:error]
failFuture];
}
return FBFuture.empty;
}];
}
- (FBFuture<NSNull *> *)moveFrom:(NSString *)sourcePath to:(NSString *)destinationPath
{
return [[FBFuture
futureWithFutures:@[
[self mapToContainedFile:sourcePath],
[self mapToContainedFile:destinationPath],
]]
onQueue:self.queue fmap:^ FBFuture<NSNull *> * (NSArray<id<FBContainedFile>> *providedFiles) {
// If the source and destination are on the same filesystem, they can be moved directly.
id<FBContainedFile> source = providedFiles[0];
id<FBContainedFile> destination = providedFiles[1];
NSError *error = nil;
if (![source moveTo:destination error:&error]) {
return [[[FBControlCoreError
describeFormat:@"Could not move item at %@ to %@: %@", source, destination, error]
causedBy:error]
failFuture];
}
return FBFuture.empty;
}];
}
- (FBFuture<NSNull *> *)remove:(NSString *)path
{
return [[self
mapToContainedFile:path]
onQueue:self.queue fmap:^ FBFuture<NSNull *> * (id<FBContainedFile> file) {
NSError *error;
if (![file removeItemWithError:&error]) {
return [[[FBControlCoreError
describeFormat:@"Could not remove item at path %@: %@", file, error]
causedBy:error]
failFuture];
}
return FBFuture.empty;
}];
}
- (FBFuture<NSArray<NSString *> *> *)contentsOfDirectory:(NSString *)path
{
return [[self
mapToContainedFile:path]
onQueue:self.queue fmap:^(id<FBContainedFile> directory) {
NSError *error;
NSArray<NSString *> *contents = [directory contentsOfDirectoryWithError:&error];
if (!contents) {
return [FBFuture futureWithError:error];
}
return [FBFuture futureWithResult:contents];
}];
}
#pragma mark Private
- (FBFuture<id<FBContainedFile>> *)mapToContainedFile:(NSString *)path
{
NSError *error = nil;
id<FBContainedFile> file = [self.rootFile fileByAppendingPathComponent:path error:&error];
if (!file) {
return [FBFuture futureWithError:error];
}
return [FBFuture futureWithResult:file];
}
@end
@interface FBFileContainer_ProvisioningProfile : NSObject <FBFileContainer>
@property (nonatomic, strong, readonly) id<FBProvisioningProfileCommands> commands;
@property (nonatomic, strong, readonly) dispatch_queue_t queue;
@end
@implementation FBFileContainer_ProvisioningProfile
- (instancetype)initWithCommands:(id<FBProvisioningProfileCommands>)commands queue:(dispatch_queue_t)queue
{
self = [super init];
if (!self) {
return nil;
}
_commands = commands;
_queue = queue;
return self;
}
#pragma mark FBFileContainer Implementation
- (FBFuture<NSNull *> *)copyFromHost:(NSString *)path toContainer:(NSString *)destinationPath
{
return [FBFuture
onQueue:self.queue resolve:^ FBFuture<NSNull *> * {
NSError *error = nil;
NSData *data = [NSData dataWithContentsOfFile:path options:0 error:&error];
if (!data) {
return [FBFuture futureWithError:error];
}
return [[self.commands installProvisioningProfile:data] mapReplace:NSNull.null];
}];
}
- (FBFuture<NSString *> *)copyFromContainer:(NSString *)containerPath toHost:(NSString *)destinationPath
{
return [[FBControlCoreError
describeFormat:@"-[%@ %@] is not implemented", NSStringFromClass(self.class), NSStringFromSelector(_cmd)]
failFuture];
}
- (FBFuture<FBFuture<NSNull *> *> *)tail:(NSString *)containerPath toConsumer:(id<FBDataConsumer>)consumer
{
return [[FBControlCoreError
describeFormat:@"-[%@ %@] is not implemented", NSStringFromClass(self.class), NSStringFromSelector(_cmd)]
failFuture];
}
- (FBFuture<NSNull *> *)createDirectory:(NSString *)directoryPath
{
return [[FBControlCoreError
describeFormat:@"-[%@ %@] is not implemented", NSStringFromClass(self.class), NSStringFromSelector(_cmd)]
failFuture];
}
- (FBFuture<NSNull *> *)moveFrom:(NSString *)originPath to:(NSString *)destinationPath
{
return [[FBControlCoreError
describeFormat:@"-[%@ %@] is not implemented", NSStringFromClass(self.class), NSStringFromSelector(_cmd)]
failFuture];
}
- (FBFuture<NSNull *> *)remove:(NSString *)path
{
return [[self.commands removeProvisioningProfile:path] mapReplace:NSNull.null];
}
- (FBFuture<NSArray<NSString *> *> *)contentsOfDirectory:(NSString *)path
{
return [[self.commands
allProvisioningProfiles]
onQueue:self.queue map:^(NSArray<NSDictionary<NSString *,id> *> *profiles) {
NSMutableArray<NSString *> *files = NSMutableArray.array;
for (NSDictionary<NSString *,id> *profile in profiles) {
[files addObject:profile[@"UUID"]];
}
return files;
}];
}
@end
@implementation FBFileContainer
+ (id<FBFileContainer>)fileContainerForProvisioningProfileCommands:(id<FBProvisioningProfileCommands>)commands queue:(dispatch_queue_t)queue
{
return [[FBFileContainer_ProvisioningProfile alloc] initWithCommands:commands queue:queue];
}
+ (id<FBFileContainer>)fileContainerForBasePath:(NSString *)basePath
{
id<FBContainedFile> rootFile = [[FBContainedFile_Host alloc] initWithFileManager:NSFileManager.defaultManager path:basePath];
return [self fileContainerForRootFile:rootFile];
}
+ (id<FBFileContainer>)fileContainerForPathMapping:(NSDictionary<NSString *, NSString *> *)pathMapping
{
id<FBContainedFile> rootFile = [[FBContainedFile_Mapped_Host alloc] initWithMappingPaths:pathMapping fileManager:NSFileManager.defaultManager];
return [self fileContainerForRootFile:rootFile];
}
#pragma mark Private
+ (id<FBFileContainer>)fileContainerForRootFile:(id<FBContainedFile>)root
{
dispatch_queue_t queue = dispatch_queue_create("com.facebook.fbcontrolcore.file_container", DISPATCH_QUEUE_SERIAL);
return [[FBContainedFile_ContainedRoot alloc] initWithRootFile:root queue:queue];
}
@end