// // FLEXRuntimeClient.m // FLEX // // Created by Tanner on 3/22/17. // Copyright © 2017 Tanner Bennett. All rights reserved. // #import "FLEXRuntimeClient.h" #import "NSObject+FLEX_Reflection.h" #import "FLEXMethod.h" #import "NSArray+FLEX.h" #import "FLEXRuntimeSafety.h" #include #define Equals(a, b) ([a compare:b options:NSCaseInsensitiveSearch] == NSOrderedSame) #define Contains(a, b) ([a rangeOfString:b options:NSCaseInsensitiveSearch].location != NSNotFound) #define HasPrefix(a, b) ([a rangeOfString:b options:NSCaseInsensitiveSearch].location == 0) #define HasSuffix(a, b) ([a rangeOfString:b options:NSCaseInsensitiveSearch].location == (a.length - b.length)) @interface FLEXRuntimeClient () { NSMutableArray *_imageDisplayNames; } @property (nonatomic) NSMutableDictionary *bundles_pathToShort; @property (nonatomic) NSMutableDictionary *bundles_shortToPath; @property (nonatomic) NSCache *bundles_pathToClassNames; @property (nonatomic) NSMutableArray *imagePaths; @end /// @return success if the map passes. static inline NSString * TBWildcardMap_(NSString *token, NSString *candidate, NSString *success, TBWildcardOptions options) { switch (options) { case TBWildcardOptionsNone: // Only "if equals" if (Equals(candidate, token)) { return success; } default: { // Only "if contains" if (options & TBWildcardOptionsPrefix && options & TBWildcardOptionsSuffix) { if (Contains(candidate, token)) { return success; } } // Only "if candidate ends with with token" else if (options & TBWildcardOptionsPrefix) { if (HasSuffix(candidate, token)) { return success; } } // Only "if candidate starts with with token" else if (options & TBWildcardOptionsSuffix) { // Case like "Bundle." where we want "" to match anything if (!token.length) { return success; } if (HasPrefix(candidate, token)) { return success; } } } } return nil; } /// @return candidate if the map passes. static inline NSString * TBWildcardMap(NSString *token, NSString *candidate, TBWildcardOptions options) { return TBWildcardMap_(token, candidate, candidate, options); } @implementation FLEXRuntimeClient #pragma mark - Initialization + (instancetype)runtime { static FLEXRuntimeClient *runtime; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ runtime = [self new]; [runtime reloadLibrariesList]; }); return runtime; } - (id)init { self = [super init]; if (self) { _imagePaths = [NSMutableArray new]; _bundles_pathToShort = [NSMutableDictionary new]; _bundles_shortToPath = [NSMutableDictionary new]; _bundles_pathToClassNames = [NSCache new]; } return self; } #pragma mark - Private - (void)reloadLibrariesList { unsigned int imageCount = 0; const char **imageNames = objc_copyImageNames(&imageCount); if (imageNames) { NSMutableArray *imageNameStrings = [NSMutableArray flex_forEachUpTo:imageCount map:^NSString *(NSUInteger i) { return @(imageNames[i]); }]; self.imagePaths = imageNameStrings; free(imageNames); // Sort alphabetically [imageNameStrings sortUsingComparator:^NSComparisonResult(NSString *name1, NSString *name2) { NSString *shortName1 = [self shortNameForImageName:name1]; NSString *shortName2 = [self shortNameForImageName:name2]; return [shortName1 caseInsensitiveCompare:shortName2]; }]; // Cache image display names _imageDisplayNames = [imageNameStrings flex_mapped:^id(NSString *path, NSUInteger idx) { return [self shortNameForImageName:path]; }]; } } - (NSString *)shortNameForImageName:(NSString *)imageName { // Cache NSString *shortName = _bundles_pathToShort[imageName]; if (shortName) { return shortName; } NSArray *components = [imageName componentsSeparatedByString:@"/"]; if (components.count >= 2) { NSString *parentDir = components[components.count - 2]; if ([parentDir hasSuffix:@".framework"] || [parentDir hasSuffix:@".axbundle"]) { if ([imageName hasSuffix:@".dylib"]) { shortName = imageName.lastPathComponent; } else { shortName = parentDir; } } } if (!shortName) { shortName = imageName.lastPathComponent; } _bundles_pathToShort[imageName] = shortName; _bundles_shortToPath[shortName] = imageName; return shortName; } - (NSString *)imageNameForShortName:(NSString *)imageName { return _bundles_shortToPath[imageName]; } - (NSMutableArray *)classNamesInImageAtPath:(NSString *)path { // Check cache NSMutableArray *classNameStrings = [_bundles_pathToClassNames objectForKey:path]; if (classNameStrings) { return classNameStrings.mutableCopy; } unsigned int classCount = 0; const char **classNames = objc_copyClassNamesForImage(path.UTF8String, &classCount); if (classNames) { classNameStrings = [NSMutableArray flex_forEachUpTo:classCount map:^id(NSUInteger i) { return @(classNames[i]); }]; free(classNames); [classNameStrings sortUsingSelector:@selector(caseInsensitiveCompare:)]; [_bundles_pathToClassNames setObject:classNameStrings forKey:path]; return classNameStrings.mutableCopy; } return [NSMutableArray new]; } #pragma mark - Public + (void)initializeWebKitLegacy { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ void *handle = dlopen( "/System/Library/PrivateFrameworks/WebKitLegacy.framework/WebKitLegacy", RTLD_LAZY ); void (*WebKitInitialize)() = dlsym(handle, "WebKitInitialize"); if (WebKitInitialize) { NSAssert(NSThread.isMainThread, @"WebKitInitialize can only be called on the main thread" ); WebKitInitialize(); } }); } - (NSArray *)copySafeClassList { unsigned int count = 0; Class *classes = objc_copyClassList(&count); return [NSArray flex_forEachUpTo:count map:^id(NSUInteger i) { Class cls = classes[i]; return FLEXClassIsSafe(cls) ? cls : nil; }]; } - (NSArray *)copyProtocolList { unsigned int count = 0; Protocol *__unsafe_unretained *protocols = objc_copyProtocolList(&count); return [NSArray arrayWithObjects:protocols count:count]; } - (NSMutableArray *)bundleNamesForToken:(FLEXSearchToken *)token { if (self.imagePaths.count) { TBWildcardOptions options = token.options; NSString *query = token.string; // Optimization, avoid a loop if (options == TBWildcardOptionsAny) { return _imageDisplayNames; } // No dot syntax because imageDisplayNames is only mutable internally return [_imageDisplayNames flex_mapped:^id(NSString *binary, NSUInteger idx) { // NSString *UIName = [self shortNameForImageName:binary]; return TBWildcardMap(query, binary, options); }]; } return [NSMutableArray new]; } - (NSMutableArray *)bundlePathsForToken:(FLEXSearchToken *)token { if (self.imagePaths.count) { TBWildcardOptions options = token.options; NSString *query = token.string; // Optimization, avoid a loop if (options == TBWildcardOptionsAny) { return self.imagePaths; } return [self.imagePaths flex_mapped:^id(NSString *binary, NSUInteger idx) { NSString *UIName = [self shortNameForImageName:binary]; // If query == UIName, -> binary return TBWildcardMap_(query, UIName, binary, options); }]; } return [NSMutableArray new]; } - (NSMutableArray *)classesForToken:(FLEXSearchToken *)token inBundles:(NSMutableArray *)bundles { // Edge case where token is the class we want already; return superclasses if (token.isAbsolute) { if (FLEXClassIsSafe(NSClassFromString(token.string))) { return [NSMutableArray arrayWithObject:token.string]; } return [NSMutableArray new]; } if (bundles.count) { // Get class names, remove unsafe classes NSMutableArray *names = [self _classesForToken:token inBundles:bundles]; return [names flex_mapped:^NSString *(NSString *name, NSUInteger idx) { Class cls = NSClassFromString(name); BOOL safe = FLEXClassIsSafe(cls); return safe ? name : nil; }]; } return [NSMutableArray new]; } - (NSMutableArray *)_classesForToken:(FLEXSearchToken *)token inBundles:(NSMutableArray *)bundles { TBWildcardOptions options = token.options; NSString *query = token.string; // Optimization, avoid unnecessary sorting if (bundles.count == 1) { // Optimization, avoid a loop if (options == TBWildcardOptionsAny) { return [self classNamesInImageAtPath:bundles.firstObject]; } return [[self classNamesInImageAtPath:bundles.firstObject] flex_mapped:^id(NSString *className, NSUInteger idx) { return TBWildcardMap(query, className, options); }]; } else { // Optimization, avoid a loop if (options == TBWildcardOptionsAny) { return [[bundles flex_flatmapped:^NSArray *(NSString *bundlePath, NSUInteger idx) { return [self classNamesInImageAtPath:bundlePath]; }] flex_sortedUsingSelector:@selector(caseInsensitiveCompare:)]; } return [[bundles flex_flatmapped:^NSArray *(NSString *bundlePath, NSUInteger idx) { return [[self classNamesInImageAtPath:bundlePath] flex_mapped:^id(NSString *className, NSUInteger idx) { return TBWildcardMap(query, className, options); }]; }] flex_sortedUsingSelector:@selector(caseInsensitiveCompare:)]; } } - (NSArray *> *)methodsForToken:(FLEXSearchToken *)token instance:(NSNumber *)checkInstance inClasses:(NSArray *)classes { if (classes.count) { TBWildcardOptions options = token.options; BOOL instance = checkInstance.boolValue; NSString *selector = token.string; switch (options) { // In practice I don't think this case is ever used with methods, // since they will always have a suffix wildcard at the end case TBWildcardOptionsNone: { SEL sel = (SEL)selector.UTF8String; return @[[classes flex_mapped:^id(NSString *name, NSUInteger idx) { Class cls = NSClassFromString(name); // Use metaclass if not instance if (!instance) { cls = object_getClass(cls); } // Method is absolute return [FLEXMethod selector:sel class:cls]; }]]; } case TBWildcardOptionsAny: { return [classes flex_mapped:^NSArray *(NSString *name, NSUInteger idx) { // Any means `instance` was not specified Class cls = NSClassFromString(name); return [cls flex_allMethods]; }]; } default: { // Only "if contains" if (options & TBWildcardOptionsPrefix && options & TBWildcardOptionsSuffix) { return [classes flex_mapped:^NSArray *(NSString *name, NSUInteger idx) { Class cls = NSClassFromString(name); return [[cls flex_allMethods] flex_mapped:^id(FLEXMethod *method, NSUInteger idx) { // Method is a prefix-suffix wildcard if (Contains(method.selectorString, selector)) { return method; } return nil; }]; }]; } // Only "if method ends with with selector" else if (options & TBWildcardOptionsPrefix) { return [classes flex_mapped:^NSArray *(NSString *name, NSUInteger idx) { Class cls = NSClassFromString(name); return [[cls flex_allMethods] flex_mapped:^id(FLEXMethod *method, NSUInteger idx) { // Method is a prefix wildcard if (HasSuffix(method.selectorString, selector)) { return method; } return nil; }]; }]; } // Only "if method starts with with selector" else if (options & TBWildcardOptionsSuffix) { assert(checkInstance); return [classes flex_mapped:^NSArray *(NSString *name, NSUInteger idx) { Class cls = NSClassFromString(name); // Case like "Bundle.class.-" where we want "-" to match anything if (!selector.length) { if (instance) { return [cls flex_allInstanceMethods]; } else { return [cls flex_allClassMethods]; } } id mapping = ^id(FLEXMethod *method) { // Method is a suffix wildcard if (HasPrefix(method.selectorString, selector)) { return method; } return nil; }; if (instance) { return [[cls flex_allInstanceMethods] flex_mapped:mapping]; } else { return [[cls flex_allClassMethods] flex_mapped:mapping]; } }]; } } } } return [NSMutableArray new]; } @end