mirror of
				https://github.com/SoPat712/YTLitePlus.git
				synced 2025-10-30 20:34:04 -04:00 
			
		
		
		
	added files via upload
This commit is contained in:
		| @@ -0,0 +1,54 @@ | ||||
| // | ||||
| //  FLEXRuntimeClient.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 3/22/17. | ||||
| //  Copyright © 2017 Tanner Bennett. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXSearchToken.h" | ||||
| @class FLEXMethod; | ||||
|  | ||||
| /// Accepts runtime queries given a token. | ||||
| @interface FLEXRuntimeClient : NSObject | ||||
|  | ||||
| @property (nonatomic, readonly, class) FLEXRuntimeClient *runtime; | ||||
|  | ||||
| /// Called automatically when \c FLEXRuntime is first used. | ||||
| /// You may call it again when you think a library has | ||||
| /// been loaded since this method was first called. | ||||
| - (void)reloadLibrariesList; | ||||
|  | ||||
| /// You must call this method on the main thread | ||||
| /// before you attempt to call \c copySafeClassList. | ||||
| + (void)initializeWebKitLegacy; | ||||
|  | ||||
| /// Do not call unless you absolutely need all classes. This will cause | ||||
| /// every class in the runtime to initialize itself, which is not common. | ||||
| /// Before you call this method, call \c initializeWebKitLegacy on the main thread. | ||||
| - (NSArray<Class> *)copySafeClassList; | ||||
|  | ||||
| - (NSArray<Protocol *> *)copyProtocolList; | ||||
|  | ||||
| /// An array of strings representing the currently loaded libraries. | ||||
| @property (nonatomic, readonly) NSArray<NSString *> *imageDisplayNames; | ||||
|  | ||||
| /// "Image name" is the path of the bundle | ||||
| - (NSString *)shortNameForImageName:(NSString *)imageName; | ||||
| /// "Image name" is the path of the bundle | ||||
| - (NSString *)imageNameForShortName:(NSString *)imageName; | ||||
|  | ||||
| /// @return Bundle names for the UI | ||||
| - (NSMutableArray<NSString *> *)bundleNamesForToken:(FLEXSearchToken *)token; | ||||
| /// @return Bundle paths for more queries | ||||
| - (NSMutableArray<NSString *> *)bundlePathsForToken:(FLEXSearchToken *)token; | ||||
| /// @return Class names | ||||
| - (NSMutableArray<NSString *> *)classesForToken:(FLEXSearchToken *)token | ||||
|                                       inBundles:(NSMutableArray<NSString *> *)bundlePaths; | ||||
| /// @return A list of lists of \c FLEXMethods where | ||||
| /// each list corresponds to one of the given classes | ||||
| - (NSArray<NSMutableArray<FLEXMethod *> *> *)methodsForToken:(FLEXSearchToken *)token | ||||
|                                                     instance:(NSNumber *)onlyInstanceMethods | ||||
|                                                    inClasses:(NSArray<NSString *> *)classes; | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,416 @@ | ||||
| // | ||||
| //  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 <dlfcn.h> | ||||
|  | ||||
| #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<NSString *> *_imageDisplayNames; | ||||
| } | ||||
|  | ||||
| @property (nonatomic) NSMutableDictionary *bundles_pathToShort; | ||||
| @property (nonatomic) NSMutableDictionary *bundles_shortToPath; | ||||
| @property (nonatomic) NSCache *bundles_pathToClassNames; | ||||
| @property (nonatomic) NSMutableArray<NSString *> *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<NSString *> *)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)(void) = dlsym(handle, "WebKitInitialize"); | ||||
|         if (WebKitInitialize) { | ||||
|             NSAssert(NSThread.isMainThread, | ||||
|                 @"WebKitInitialize can only be called on the main thread" | ||||
|             ); | ||||
|             WebKitInitialize(); | ||||
|         } | ||||
|     }); | ||||
| } | ||||
|  | ||||
| - (NSArray<Class> *)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<Protocol *> *)copyProtocolList { | ||||
|     unsigned int count = 0; | ||||
|     Protocol *__unsafe_unretained *protocols = objc_copyProtocolList(&count); | ||||
|     return [NSArray arrayWithObjects:protocols count:count]; | ||||
| } | ||||
|  | ||||
| - (NSMutableArray<NSString *> *)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<NSString *> *)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<NSString *> *)classesForToken:(FLEXSearchToken *)token inBundles:(NSMutableArray<NSString *> *)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<NSString *> *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<NSString *> *)_classesForToken:(FLEXSearchToken *)token inBundles:(NSMutableArray<NSString *> *)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<NSMutableArray<FLEXMethod *> *> *)methodsForToken:(FLEXSearchToken *)token | ||||
|                                                     instance:(NSNumber *)checkInstance | ||||
|                                                    inClasses:(NSArray<NSString *> *)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 | ||||
| @@ -0,0 +1,36 @@ | ||||
| // | ||||
| //  FLEXRuntimeController.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 3/23/17. | ||||
| //  Copyright © 2017 Tanner Bennett. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXRuntimeKeyPath.h" | ||||
|  | ||||
| /// Wraps FLEXRuntimeClient and provides extra caching mechanisms | ||||
| @interface FLEXRuntimeController : NSObject | ||||
|  | ||||
| /// @return An array of strings if the key path only evaluates | ||||
| ///         to a class or bundle; otherwise, a list of lists of FLEXMethods. | ||||
| + (NSArray *)dataForKeyPath:(FLEXRuntimeKeyPath *)keyPath; | ||||
|  | ||||
| /// Useful when you need to specify which classes to search in. | ||||
| /// \c dataForKeyPath: will only search classes matching the class key. | ||||
| /// We use this elsewhere when we need to search a class hierarchy. | ||||
| + (NSArray<NSArray<FLEXMethod *> *> *)methodsForToken:(FLEXSearchToken *)token | ||||
|                                              instance:(NSNumber *)onlyInstanceMethods | ||||
|                                             inClasses:(NSArray<NSString*> *)classes; | ||||
|  | ||||
| /// Useful when you need the classes that are associated with the | ||||
| /// double list of methods returned from \c dataForKeyPath | ||||
| + (NSMutableArray<NSString *> *)classesForKeyPath:(FLEXRuntimeKeyPath *)keyPath; | ||||
|  | ||||
| + (NSString *)shortBundleNameForClass:(NSString *)name; | ||||
|  | ||||
| + (NSString *)imagePathWithShortName:(NSString *)suffix; | ||||
|  | ||||
| /// Gives back short names. For example, "Foundation.framework" | ||||
| + (NSArray<NSString*> *)allBundleNames; | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,192 @@ | ||||
| // | ||||
| //  FLEXRuntimeController.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 3/23/17. | ||||
| //  Copyright © 2017 Tanner Bennett. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXRuntimeController.h" | ||||
| #import "FLEXRuntimeClient.h" | ||||
| #import "FLEXMethod.h" | ||||
|  | ||||
| @interface FLEXRuntimeController () | ||||
| @property (nonatomic, readonly) NSCache *bundlePathsCache; | ||||
| @property (nonatomic, readonly) NSCache *bundleNamesCache; | ||||
| @property (nonatomic, readonly) NSCache *classNamesCache; | ||||
| @property (nonatomic, readonly) NSCache *methodsCache; | ||||
| @end | ||||
|  | ||||
| @implementation FLEXRuntimeController | ||||
|  | ||||
| #pragma mark Initialization | ||||
|  | ||||
| static FLEXRuntimeController *controller = nil; | ||||
| + (instancetype)shared { | ||||
|     static dispatch_once_t onceToken; | ||||
|     dispatch_once(&onceToken, ^{ | ||||
|         controller = [self new]; | ||||
|     }); | ||||
|  | ||||
|     return controller; | ||||
| } | ||||
|  | ||||
| - (id)init { | ||||
|     self = [super init]; | ||||
|     if (self) { | ||||
|         _bundlePathsCache = [NSCache new]; | ||||
|         _bundleNamesCache = [NSCache new]; | ||||
|         _classNamesCache  = [NSCache new]; | ||||
|         _methodsCache     = [NSCache new]; | ||||
|     } | ||||
|  | ||||
|     return self; | ||||
| } | ||||
|  | ||||
| #pragma mark Public | ||||
|  | ||||
| + (NSArray *)dataForKeyPath:(FLEXRuntimeKeyPath *)keyPath { | ||||
|     if (keyPath.bundleKey) { | ||||
|         if (keyPath.classKey) { | ||||
|             if (keyPath.methodKey) { | ||||
|                 return [[self shared] methodsForKeyPath:keyPath]; | ||||
|             } else { | ||||
|                 return [[self shared] classesForKeyPath:keyPath]; | ||||
|             } | ||||
|         } else { | ||||
|             return [[self shared] bundleNamesForToken:keyPath.bundleKey]; | ||||
|         } | ||||
|     } else { | ||||
|         return @[]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| + (NSArray<NSArray<FLEXMethod *> *> *)methodsForToken:(FLEXSearchToken *)token | ||||
|                          instance:(NSNumber *)inst | ||||
|                         inClasses:(NSArray<NSString*> *)classes { | ||||
|     return [FLEXRuntimeClient.runtime | ||||
|         methodsForToken:token | ||||
|         instance:inst | ||||
|         inClasses:classes | ||||
|     ]; | ||||
| } | ||||
|  | ||||
| + (NSMutableArray<NSString *> *)classesForKeyPath:(FLEXRuntimeKeyPath *)keyPath { | ||||
|     return [[self shared] classesForKeyPath:keyPath]; | ||||
| } | ||||
|  | ||||
| + (NSString *)shortBundleNameForClass:(NSString *)name { | ||||
|     const char *imageName = class_getImageName(NSClassFromString(name)); | ||||
|     if (!imageName) { | ||||
|         return @"(unspecified)"; | ||||
|     } | ||||
|      | ||||
|     return [FLEXRuntimeClient.runtime shortNameForImageName:@(imageName)]; | ||||
| } | ||||
|  | ||||
| + (NSString *)imagePathWithShortName:(NSString *)suffix { | ||||
|     return [FLEXRuntimeClient.runtime imageNameForShortName:suffix]; | ||||
| } | ||||
|  | ||||
| + (NSArray *)allBundleNames { | ||||
|     return FLEXRuntimeClient.runtime.imageDisplayNames; | ||||
| } | ||||
|  | ||||
| #pragma mark Private | ||||
|  | ||||
| - (NSMutableArray *)bundlePathsForToken:(FLEXSearchToken *)token { | ||||
|     // Only cache if no wildcard | ||||
|     BOOL shouldCache = token == TBWildcardOptionsNone; | ||||
|  | ||||
|     if (shouldCache) { | ||||
|         NSMutableArray<NSString*> *cached = [self.bundlePathsCache objectForKey:token]; | ||||
|         if (cached) { | ||||
|             return cached; | ||||
|         } | ||||
|  | ||||
|         NSMutableArray<NSString*> *bundles = [FLEXRuntimeClient.runtime bundlePathsForToken:token]; | ||||
|         [self.bundlePathsCache setObject:bundles forKey:token]; | ||||
|         return bundles; | ||||
|     } | ||||
|     else { | ||||
|         return [FLEXRuntimeClient.runtime bundlePathsForToken:token]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (NSMutableArray<NSString *> *)bundleNamesForToken:(FLEXSearchToken *)token { | ||||
|     // Only cache if no wildcard | ||||
|     BOOL shouldCache = token == TBWildcardOptionsNone; | ||||
|  | ||||
|     if (shouldCache) { | ||||
|         NSMutableArray<NSString*> *cached = [self.bundleNamesCache objectForKey:token]; | ||||
|         if (cached) { | ||||
|             return cached; | ||||
|         } | ||||
|  | ||||
|         NSMutableArray<NSString*> *bundles = [FLEXRuntimeClient.runtime bundleNamesForToken:token]; | ||||
|         [self.bundleNamesCache setObject:bundles forKey:token]; | ||||
|         return bundles; | ||||
|     } | ||||
|     else { | ||||
|         return [FLEXRuntimeClient.runtime bundleNamesForToken:token]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (NSMutableArray<NSString *> *)classesForKeyPath:(FLEXRuntimeKeyPath *)keyPath { | ||||
|     FLEXSearchToken *classToken = keyPath.classKey; | ||||
|     FLEXSearchToken *bundleToken = keyPath.bundleKey; | ||||
|      | ||||
|     // Only cache if no wildcard | ||||
|     BOOL shouldCache = bundleToken.options == 0 && classToken.options == 0; | ||||
|     NSString *key = nil; | ||||
|  | ||||
|     if (shouldCache) { | ||||
|         key = [@[bundleToken.description, classToken.description] componentsJoinedByString:@"+"]; | ||||
|         NSMutableArray<NSString *> *cached = [self.classNamesCache objectForKey:key]; | ||||
|         if (cached) { | ||||
|             return cached; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     NSMutableArray<NSString *> *bundles = [self bundlePathsForToken:bundleToken]; | ||||
|     NSMutableArray<NSString *> *classes = [FLEXRuntimeClient.runtime | ||||
|         classesForToken:classToken inBundles:bundles | ||||
|     ]; | ||||
|  | ||||
|     if (shouldCache) { | ||||
|         [self.classNamesCache setObject:classes forKey:key]; | ||||
|     } | ||||
|  | ||||
|     return classes; | ||||
| } | ||||
|  | ||||
| - (NSArray<NSMutableArray<FLEXMethod *> *> *)methodsForKeyPath:(FLEXRuntimeKeyPath *)keyPath { | ||||
|     // Only cache if no wildcard, but check cache anyway bc I'm lazy | ||||
|     NSArray<NSMutableArray *> *cached = [self.methodsCache objectForKey:keyPath]; | ||||
|     if (cached) { | ||||
|         return cached; | ||||
|     } | ||||
|  | ||||
|     NSArray<NSString *> *classes = [self classesForKeyPath:keyPath]; | ||||
|     NSArray<NSMutableArray<FLEXMethod *> *> *methodLists = [FLEXRuntimeClient.runtime | ||||
|         methodsForToken:keyPath.methodKey | ||||
|         instance:keyPath.instanceMethods | ||||
|         inClasses:classes | ||||
|     ]; | ||||
|  | ||||
|     for (NSMutableArray<FLEXMethod *> *methods in methodLists) { | ||||
|         [methods sortUsingComparator:^NSComparisonResult(FLEXMethod *m1, FLEXMethod *m2) { | ||||
|             return [m1.description caseInsensitiveCompare:m2.description]; | ||||
|         }]; | ||||
|     } | ||||
|  | ||||
|     // Only cache if no wildcard, otherwise the cache could grow very large | ||||
|     if (keyPath.bundleKey.isAbsolute && | ||||
|         keyPath.classKey.isAbsolute) { | ||||
|         [self.methodsCache setObject:methodLists forKey:keyPath]; | ||||
|     } | ||||
|  | ||||
|     return methodLists; | ||||
| } | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,29 @@ | ||||
| // | ||||
| //  FLEXRuntimeExporter.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner Bennett on 3/26/20. | ||||
| //  Copyright (c) 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import <Foundation/Foundation.h> | ||||
|  | ||||
| NS_ASSUME_NONNULL_BEGIN | ||||
|  | ||||
| /// A class for exporting all runtime metadata to an SQLite database. | ||||
| //API_AVAILABLE(ios(10.0)) | ||||
| @interface FLEXRuntimeExporter : NSObject | ||||
|  | ||||
| + (void)createRuntimeDatabaseAtPath:(NSString *)path | ||||
|                     progressHandler:(void(^)(NSString *status))progress | ||||
|                          completion:(void(^)(NSString *_Nullable error))completion; | ||||
|  | ||||
| + (void)createRuntimeDatabaseAtPath:(NSString *)path | ||||
|                           forImages:(nullable NSArray<NSString *> *)images | ||||
|                     progressHandler:(void(^)(NSString *status))progress | ||||
|                          completion:(void(^)(NSString *_Nullable error))completion; | ||||
|  | ||||
|  | ||||
| @end | ||||
|  | ||||
| NS_ASSUME_NONNULL_END | ||||
| @@ -0,0 +1,875 @@ | ||||
| // | ||||
| //  FLEXRuntimeExporter.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner Bennett on 3/26/20. | ||||
| //  Copyright (c) 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXRuntimeExporter.h" | ||||
| #import "FLEXSQLiteDatabaseManager.h" | ||||
| #import "NSObject+FLEX_Reflection.h" | ||||
| #import "FLEXRuntimeController.h" | ||||
| #import "FLEXRuntimeClient.h" | ||||
| #import "NSArray+FLEX.h" | ||||
| #import "FLEXTypeEncodingParser.h" | ||||
| #import <sqlite3.h> | ||||
|  | ||||
| #import "FLEXProtocol.h" | ||||
| #import "FLEXProperty.h" | ||||
| #import "FLEXIvar.h" | ||||
| #import "FLEXMethodBase.h" | ||||
| #import "FLEXMethod.h" | ||||
| #import "FLEXPropertyAttributes.h" | ||||
|  | ||||
| NSString * const kFREEnableForeignKeys = @"PRAGMA foreign_keys = ON;"; | ||||
|  | ||||
| /// Loaded images | ||||
| NSString * const kFRECreateTableMachOCommand = @"CREATE TABLE MachO( " | ||||
|     "id INTEGER PRIMARY KEY AUTOINCREMENT, " | ||||
|     "shortName TEXT, " | ||||
|     "imagePath TEXT, " | ||||
|     "bundleID TEXT " | ||||
| ");"; | ||||
|  | ||||
| NSString * const kFREInsertImage = @"INSERT INTO MachO ( " | ||||
|     "shortName, imagePath, bundleID " | ||||
| ") VALUES ( " | ||||
|     "$shortName, $imagePath, $bundleID " | ||||
| ");"; | ||||
|  | ||||
| /// Objc classes | ||||
| NSString * const kFRECreateTableClassCommand = @"CREATE TABLE Class( " | ||||
|     "id INTEGER PRIMARY KEY AUTOINCREMENT, " | ||||
|     "className TEXT, " | ||||
|     "superclass INTEGER, " | ||||
|     "instanceSize INTEGER, " | ||||
|     "version INTEGER, " | ||||
|     "image INTEGER, " | ||||
|  | ||||
|     "FOREIGN KEY(superclass) REFERENCES Class(id), " | ||||
|     "FOREIGN KEY(image) REFERENCES MachO(id) " | ||||
| ");"; | ||||
|  | ||||
| NSString * const kFREInsertClass = @"INSERT INTO Class ( " | ||||
|     "className, instanceSize, version, image " | ||||
| ") VALUES ( " | ||||
|     "$className, $instanceSize, $version, $image " | ||||
| ");"; | ||||
|  | ||||
| NSString * const kFREUpdateClassSetSuper = @"UPDATE Class SET superclass = $super WHERE id = $id;"; | ||||
|  | ||||
| /// Unique objc selectors | ||||
| NSString * const kFRECreateTableSelectorCommand = @"CREATE TABLE Selector( " | ||||
|     "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, " | ||||
|     "name text NOT NULL UNIQUE " | ||||
| ");"; | ||||
|  | ||||
| NSString * const kFREInsertSelector = @"INSERT OR IGNORE INTO Selector (name) VALUES ($name);"; | ||||
|  | ||||
| /// Unique objc type encodings | ||||
| NSString * const kFRECreateTableTypeEncodingCommand = @"CREATE TABLE TypeEncoding( " | ||||
|     "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, " | ||||
|     "string text NOT NULL UNIQUE, " | ||||
|     "size integer " | ||||
| ");"; | ||||
|  | ||||
| NSString * const kFREInsertTypeEncoding = @"INSERT OR IGNORE INTO TypeEncoding " | ||||
|     "(string, size) VALUES ($type, $size);"; | ||||
|  | ||||
| /// Unique objc type signatures | ||||
| NSString * const kFRECreateTableTypeSignatureCommand = @"CREATE TABLE TypeSignature( " | ||||
|     "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, " | ||||
|     "string text NOT NULL UNIQUE " | ||||
| ");"; | ||||
|  | ||||
| NSString * const kFREInsertTypeSignature = @"INSERT OR IGNORE INTO TypeSignature " | ||||
|     "(string) VALUES ($type);"; | ||||
|  | ||||
| NSString * const kFRECreateTableMethodSignatureCommand = @"CREATE TABLE MethodSignature( " | ||||
|     "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, " | ||||
|     "typeEncoding TEXT, " | ||||
|     "argc INTEGER, " | ||||
|     "returnType INTEGER, " | ||||
|     "frameLength INTEGER, " | ||||
|  | ||||
|     "FOREIGN KEY(returnType) REFERENCES TypeEncoding(id) " | ||||
| ");"; | ||||
|  | ||||
| NSString * const kFREInsertMethodSignature = @"INSERT INTO MethodSignature ( " | ||||
|     "typeEncoding, argc, returnType, frameLength " | ||||
| ") VALUES ( " | ||||
|     "$typeEncoding, $argc, $returnType, $frameLength " | ||||
| ");"; | ||||
|  | ||||
| NSString * const kFRECreateTableMethodCommand = @"CREATE TABLE Method( " | ||||
|     "id INTEGER PRIMARY KEY AUTOINCREMENT, " | ||||
|     "sel INTEGER, " | ||||
|     "class INTEGER, " | ||||
|     "instance INTEGER, " // 0 if class method, 1 if instance method | ||||
|     "signature INTEGER, " | ||||
|     "image INTEGER, " | ||||
|  | ||||
|     "FOREIGN KEY(sel) REFERENCES Selector(id), " | ||||
|     "FOREIGN KEY(class) REFERENCES Class(id), " | ||||
|     "FOREIGN KEY(signature) REFERENCES MethodSignature(id), " | ||||
|     "FOREIGN KEY(image) REFERENCES MachO(id) " | ||||
| ");"; | ||||
|  | ||||
| NSString * const kFREInsertMethod = @"INSERT INTO Method ( " | ||||
|     "sel, class, instance, signature, image " | ||||
| ") VALUES ( " | ||||
|     "$sel, $class, $instance, $signature, $image " | ||||
| ");"; | ||||
|  | ||||
| NSString * const kFRECreateTablePropertyCommand = @"CREATE TABLE Property( " | ||||
|     "id INTEGER PRIMARY KEY AUTOINCREMENT, " | ||||
|     "name TEXT, " | ||||
|     "class INTEGER, " | ||||
|     "instance INTEGER, " // 0 if class prop, 1 if instance prop | ||||
|     "image INTEGER, " | ||||
|     "attributes TEXT, " | ||||
|  | ||||
|     "customGetter INTEGER, " | ||||
|     "customSetter INTEGER, " | ||||
|  | ||||
|     "type INTEGER, " | ||||
|     "ivar TEXT, " | ||||
|     "readonly INTEGER, " | ||||
|     "copy INTEGER, " | ||||
|     "retained INTEGER, " | ||||
|     "nonatomic INTEGER, " | ||||
|     "dynamic INTEGER, " | ||||
|     "weak INTEGER, " | ||||
|     "canGC INTEGER, " | ||||
|  | ||||
|     "FOREIGN KEY(class) REFERENCES Class(id), " | ||||
|     "FOREIGN KEY(customGetter) REFERENCES Selector(id), " | ||||
|     "FOREIGN KEY(customSetter) REFERENCES Selector(id), " | ||||
|     "FOREIGN KEY(image) REFERENCES MachO(id) " | ||||
| ");"; | ||||
|  | ||||
| NSString * const kFREInsertProperty = @"INSERT INTO Property ( " | ||||
|     "name, class, instance, attributes, image, " | ||||
|     "customGetter, customSetter, type, ivar, readonly, " | ||||
|     "copy, retained, nonatomic, dynamic, weak, canGC " | ||||
| ") VALUES ( " | ||||
|     "$name, $class, $instance, $attributes, $image, " | ||||
|     "$customGetter, $customSetter, $type, $ivar, $readonly, " | ||||
|     "$copy, $retained, $nonatomic, $dynamic, $weak, $canGC " | ||||
| ");"; | ||||
|  | ||||
| NSString * const kFRECreateTableIvarCommand = @"CREATE TABLE Ivar( " | ||||
|     "id INTEGER PRIMARY KEY AUTOINCREMENT, " | ||||
|     "name TEXT, " | ||||
|     "offset INTEGER, " | ||||
|     "type INTEGER, " | ||||
|     "class INTEGER, " | ||||
|     "image INTEGER, " | ||||
|  | ||||
|     "FOREIGN KEY(type) REFERENCES TypeEncoding(id), " | ||||
|     "FOREIGN KEY(class) REFERENCES Class(id), " | ||||
|     "FOREIGN KEY(image) REFERENCES MachO(id) " | ||||
| ");"; | ||||
|  | ||||
| NSString * const kFREInsertIvar = @"INSERT INTO Ivar ( " | ||||
|     "name, offset, type, class, image " | ||||
| ") VALUES ( " | ||||
|     "$name, $offset, $type, $class, $image " | ||||
| ");"; | ||||
|  | ||||
| NSString * const kFRECreateTableProtocolCommand = @"CREATE TABLE Protocol( " | ||||
|     "id INTEGER PRIMARY KEY AUTOINCREMENT, " | ||||
|     "name TEXT, " | ||||
|     "image INTEGER, " | ||||
|  | ||||
|     "FOREIGN KEY(image) REFERENCES MachO(id) " | ||||
| ");"; | ||||
|  | ||||
| NSString * const kFREInsertProtocol = @"INSERT INTO Protocol " | ||||
|     "(name, image) VALUES ($name, $image);"; | ||||
|  | ||||
| NSString * const kFRECreateTableProtocolPropertyCommand = @"CREATE TABLE ProtocolMember( " | ||||
|     "id INTEGER PRIMARY KEY AUTOINCREMENT, " | ||||
|     "protocol INTEGER, " | ||||
|     "required INTEGER, " | ||||
|     "instance INTEGER, " // 0 if class member, 1 if instance member | ||||
|  | ||||
|     // Only of the two below is used | ||||
|     "property TEXT, " | ||||
|     "method TEXT, " | ||||
|  | ||||
|     "image INTEGER, " | ||||
|  | ||||
|     "FOREIGN KEY(protocol) REFERENCES Protocol(id), " | ||||
|     "FOREIGN KEY(image) REFERENCES MachO(id) " | ||||
| ");"; | ||||
|  | ||||
| NSString * const kFREInsertProtocolMember = @"INSERT INTO ProtocolMember ( " | ||||
|     "protocol, required, instance, property, method, image " | ||||
| ") VALUES ( " | ||||
|     "$protocol, $required, $instance, $property, $method, $image " | ||||
| ");"; | ||||
|  | ||||
| /// For protocols conforming to other protocols | ||||
| NSString * const kFRECreateTableProtocolConformanceCommand = @"CREATE TABLE ProtocolConformance( " | ||||
|     "protocol INTEGER, " | ||||
|     "conformance INTEGER, " | ||||
|  | ||||
|     "FOREIGN KEY(protocol) REFERENCES Protocol(id), " | ||||
|     "FOREIGN KEY(conformance) REFERENCES Protocol(id) " | ||||
| ");"; | ||||
|  | ||||
| NSString * const kFREInsertProtocolConformance = @"INSERT INTO ProtocolConformance " | ||||
| "(protocol, conformance) VALUES ($protocol, $conformance);"; | ||||
|  | ||||
| /// For classes conforming to protocols | ||||
| NSString * const kFRECreateTableClassConformanceCommand = @"CREATE TABLE ClassConformance( " | ||||
|     "class INTEGER, " | ||||
|     "conformance INTEGER, " | ||||
|  | ||||
|     "FOREIGN KEY(class) REFERENCES Class(id), " | ||||
|     "FOREIGN KEY(conformance) REFERENCES Protocol(id) " | ||||
| ");"; | ||||
|  | ||||
| NSString * const kFREInsertClassConformance = @"INSERT INTO ClassConformance " | ||||
| "(class, conformance) VALUES ($class, $conformance);"; | ||||
|  | ||||
| @interface FLEXRuntimeExporter () | ||||
| @property (nonatomic, readonly) FLEXSQLiteDatabaseManager *db; | ||||
| @property (nonatomic, copy) NSArray<NSString *> *loadedShortBundleNames; | ||||
| @property (nonatomic, copy) NSArray<NSString *> *loadedBundlePaths; | ||||
| @property (nonatomic, copy) NSArray<FLEXProtocol *> *protocols; | ||||
| @property (nonatomic, copy) NSArray<Class> *classes; | ||||
|  | ||||
| @property (nonatomic) NSMutableDictionary<NSString *, NSNumber *> *bundlePathsToIDs; | ||||
| @property (nonatomic) NSMutableDictionary<NSString *, NSNumber *> *protocolsToIDs; | ||||
| @property (nonatomic) NSMutableDictionary<Class, NSNumber *> *classesToIDs; | ||||
| @property (nonatomic) NSMutableDictionary<NSString *, NSNumber *> *typeEncodingsToIDs; | ||||
| @property (nonatomic) NSMutableDictionary<NSString *, NSNumber *> *methodSignaturesToIDs; | ||||
| @property (nonatomic) NSMutableDictionary<NSString *, NSNumber *> *selectorsToIDs; | ||||
| @end | ||||
|  | ||||
| @implementation FLEXRuntimeExporter | ||||
|  | ||||
| + (NSString *)tempFilename { | ||||
|     NSString *temp = NSTemporaryDirectory(); | ||||
|     NSString *uuid = [NSUUID.UUID.UUIDString substringToIndex:8]; | ||||
|     NSString *filename = [NSString stringWithFormat:@"FLEXRuntimeDatabase-%@.db", uuid]; | ||||
|     return [temp stringByAppendingPathComponent:filename]; | ||||
| } | ||||
|  | ||||
| + (void)createRuntimeDatabaseAtPath:(NSString *)path | ||||
|                     progressHandler:(void(^)(NSString *status))progress | ||||
|                          completion:(void (^)(NSString *))completion { | ||||
|     [self createRuntimeDatabaseAtPath:path forImages:nil progressHandler:progress completion:completion]; | ||||
| } | ||||
|  | ||||
| + (void)createRuntimeDatabaseAtPath:(NSString *)path | ||||
|                           forImages:(NSArray<NSString *> *)images | ||||
|                     progressHandler:(void(^)(NSString *status))progress | ||||
|                          completion:(void(^)(NSString *_Nullable error))completion { | ||||
|     __typeof(completion) callback = ^(NSString *error) { | ||||
|         dispatch_async(dispatch_get_main_queue(), ^{ | ||||
|             completion(error); | ||||
|         }); | ||||
|     }; | ||||
|      | ||||
|     // This must be called on the main thread first | ||||
|     if (NSThread.isMainThread) { | ||||
|         [FLEXRuntimeClient initializeWebKitLegacy]; | ||||
|     } else { | ||||
|         dispatch_sync(dispatch_get_main_queue(), ^{ | ||||
|             [FLEXRuntimeClient initializeWebKitLegacy]; | ||||
|         }); | ||||
|     } | ||||
|      | ||||
|     dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ | ||||
|         NSError *error = nil; | ||||
|         NSString *errorMessage = nil; | ||||
|          | ||||
|         // Get unused temp filename, remove existing database if any | ||||
|         NSString *tempPath = [self tempFilename]; | ||||
|         if ([NSFileManager.defaultManager fileExistsAtPath:tempPath]) { | ||||
|             [NSFileManager.defaultManager removeItemAtPath:tempPath error:&error]; | ||||
|             if (error) { | ||||
|                 callback(error.localizedDescription); | ||||
|                 return; | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         // Attempt to create and populate the database, abort if we fail | ||||
|         FLEXRuntimeExporter *exporter = [self new]; | ||||
|         exporter.loadedBundlePaths = images; | ||||
|         if (![exporter createAndPopulateDatabaseAtPath:tempPath | ||||
|                                        progressHandler:progress | ||||
|                                                  error:&errorMessage]) { | ||||
|             // Remove temp database if it was not moved | ||||
|             if ([NSFileManager.defaultManager fileExistsAtPath:tempPath]) { | ||||
|                 [NSFileManager.defaultManager removeItemAtPath:tempPath error:nil]; | ||||
|             } | ||||
|              | ||||
|             callback(errorMessage); | ||||
|             return; | ||||
|         } | ||||
|          | ||||
|         // Remove old database at given path | ||||
|         if ([NSFileManager.defaultManager fileExistsAtPath:path]) { | ||||
|             [NSFileManager.defaultManager removeItemAtPath:path error:&error]; | ||||
|             if (error) { | ||||
|                 callback(error.localizedDescription); | ||||
|                 return; | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         // Move new database to desired path | ||||
|         [NSFileManager.defaultManager moveItemAtPath:tempPath toPath:path error:&error]; | ||||
|         if (error) { | ||||
|             callback(error.localizedDescription); | ||||
|         } | ||||
|          | ||||
|         // Remove temp database if it was not moved | ||||
|         if ([NSFileManager.defaultManager fileExistsAtPath:tempPath]) { | ||||
|             [NSFileManager.defaultManager removeItemAtPath:tempPath error:nil]; | ||||
|         } | ||||
|          | ||||
|         callback(nil); | ||||
|     }); | ||||
| } | ||||
|  | ||||
| - (id)init { | ||||
|     self = [super init]; | ||||
|     if (self) { | ||||
|         _bundlePathsToIDs = [NSMutableDictionary new]; | ||||
|         _protocolsToIDs = [NSMutableDictionary new]; | ||||
|         _classesToIDs = [NSMutableDictionary new]; | ||||
|         _typeEncodingsToIDs = [NSMutableDictionary new]; | ||||
|         _methodSignaturesToIDs = [NSMutableDictionary new]; | ||||
|         _selectorsToIDs = [NSMutableDictionary new]; | ||||
|          | ||||
|         _bundlePathsToIDs[NSNull.null] = (id)NSNull.null; | ||||
|     } | ||||
|      | ||||
|     return self; | ||||
| } | ||||
|  | ||||
| - (BOOL)createAndPopulateDatabaseAtPath:(NSString *)path | ||||
|                         progressHandler:(void(^)(NSString *status))step | ||||
|                                   error:(NSString **)error { | ||||
|     _db = [FLEXSQLiteDatabaseManager managerForDatabase:path]; | ||||
|      | ||||
|     [self loadMetadata:step]; | ||||
|      | ||||
|     if ([self createTables] && [self addImages:step] && [self addProtocols:step] && | ||||
|         [self addClasses:step] && [self setSuperclasses:step] &&  | ||||
|         [self addProtocolConformances:step] && [self addClassConformances:step] && | ||||
|         [self addIvars:step] && [self addMethods:step] && [self addProperties:step]) { | ||||
|         _db = nil; // Close the database | ||||
|         return YES; | ||||
|     } | ||||
|      | ||||
|     *error = self.db.lastResult.message; | ||||
|     return NO; | ||||
| } | ||||
|  | ||||
| - (void)loadMetadata:(void(^)(NSString *status))progress { | ||||
|     progress(@"Loading metadata…"); | ||||
|      | ||||
|     FLEXRuntimeClient *runtime = FLEXRuntimeClient.runtime; | ||||
|      | ||||
|     // Only load metadata for the existing paths if any | ||||
|     if (self.loadedBundlePaths) { | ||||
|         // Images | ||||
|         self.loadedShortBundleNames = [self.loadedBundlePaths flex_mapped:^id(NSString *path, NSUInteger idx) { | ||||
|             return [runtime shortNameForImageName:path]; | ||||
|         }]; | ||||
|          | ||||
|         // Classes | ||||
|         self.classes = [[runtime classesForToken:FLEXSearchToken.any | ||||
|             inBundles:self.loadedBundlePaths.mutableCopy | ||||
|         ] flex_mapped:^id(NSString *cls, NSUInteger idx) { | ||||
|             return NSClassFromString(cls); | ||||
|         }]; | ||||
|     } else { | ||||
|         // Images | ||||
|         self.loadedShortBundleNames = runtime.imageDisplayNames; | ||||
|         self.loadedBundlePaths = [self.loadedShortBundleNames flex_mapped:^id(NSString *name, NSUInteger idx) { | ||||
|             return [runtime imageNameForShortName:name]; | ||||
|         }]; | ||||
|          | ||||
|         // Classes | ||||
|         self.classes = [runtime copySafeClassList]; | ||||
|     } | ||||
|      | ||||
|     // ...except protocols, because there's not a lot of them | ||||
|     // and there's no way load the protocols for a given image | ||||
|     self.protocols = [[runtime copyProtocolList] flex_mapped:^id(Protocol *proto, NSUInteger idx) { | ||||
|         return [FLEXProtocol protocol:proto]; | ||||
|     }]; | ||||
| } | ||||
|  | ||||
| - (BOOL)createTables { | ||||
|     NSArray<NSString *> *commands = @[ | ||||
|         kFREEnableForeignKeys, | ||||
|         kFRECreateTableMachOCommand, | ||||
|         kFRECreateTableClassCommand, | ||||
|         kFRECreateTableSelectorCommand, | ||||
|         kFRECreateTableTypeEncodingCommand, | ||||
|         kFRECreateTableTypeSignatureCommand, | ||||
|         kFRECreateTableMethodSignatureCommand, | ||||
|         kFRECreateTableMethodCommand, | ||||
|         kFRECreateTablePropertyCommand, | ||||
|         kFRECreateTableIvarCommand, | ||||
|         kFRECreateTableProtocolCommand, | ||||
|         kFRECreateTableProtocolPropertyCommand, | ||||
|         kFRECreateTableProtocolConformanceCommand, | ||||
|         kFRECreateTableClassConformanceCommand | ||||
|     ]; | ||||
|      | ||||
|     for (NSString *command in commands) { | ||||
|         if (![self.db executeStatement:command]) { | ||||
|             return NO; | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     return YES; | ||||
| } | ||||
|  | ||||
| - (BOOL)addImages:(void(^)(NSString *status))progress { | ||||
|     progress(@"Adding loaded images…"); | ||||
|      | ||||
|     FLEXSQLiteDatabaseManager *database = self.db; | ||||
|     NSArray *shortNames = self.loadedShortBundleNames; | ||||
|     NSArray *fullPaths = self.loadedBundlePaths; | ||||
|     NSParameterAssert(shortNames.count == fullPaths.count); | ||||
|      | ||||
|     NSInteger count = shortNames.count; | ||||
|     for (NSInteger i = 0; i < count; i++) { | ||||
|         // Grab bundle ID | ||||
|         NSString *bundleID = [NSBundle | ||||
|             bundleWithPath:fullPaths[i] | ||||
|         ].bundleIdentifier;  | ||||
|          | ||||
|         [database executeStatement:kFREInsertImage arguments:@{ | ||||
|             @"$shortName": shortNames[i], | ||||
|             @"$imagePath": fullPaths[i], | ||||
|             @"$bundleID":  bundleID ?: NSNull.null | ||||
|         }]; | ||||
|          | ||||
|         if (database.lastResult.isError) { | ||||
|             return NO; | ||||
|         } else { | ||||
|             self.bundlePathsToIDs[fullPaths[i]] = @(database.lastRowID); | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     return YES; | ||||
| } | ||||
|  | ||||
| NS_INLINE BOOL FREInsertProtocolMember(FLEXSQLiteDatabaseManager *db, | ||||
|                                        id proto, id required, id instance, | ||||
|                                        id prop, id methSel, id image) { | ||||
|     return ![db executeStatement:kFREInsertProtocolMember arguments:@{ | ||||
|         @"$protocol": proto, | ||||
|         @"$required": required, | ||||
|         @"$instance": instance ?: NSNull.null, | ||||
|         @"$property": prop ?: NSNull.null, | ||||
|         @"$method": methSel ?: NSNull.null, | ||||
|         @"$image": image | ||||
|     }].isError; | ||||
| } | ||||
|  | ||||
| - (BOOL)addProtocols:(void(^)(NSString *status))progress { | ||||
|     progress([NSString stringWithFormat:@"Adding %@ protocols…", @(self.protocols.count)]); | ||||
|      | ||||
|     FLEXSQLiteDatabaseManager *database = self.db; | ||||
|     NSDictionary *imageIDs = self.bundlePathsToIDs; | ||||
|      | ||||
|     for (FLEXProtocol *proto in self.protocols) { | ||||
|         id imagePath = proto.imagePath ?: NSNull.null; | ||||
|         NSNumber *image = imageIDs[imagePath] ?: NSNull.null; | ||||
|         NSNumber *pid = nil; | ||||
|          | ||||
|         // Insert protocol | ||||
|         BOOL failed = [database executeStatement:kFREInsertProtocol arguments:@{ | ||||
|             @"$name": proto.name, @"$image": image | ||||
|         }].isError; | ||||
|          | ||||
|         // Cache rowid | ||||
|         if (failed) { | ||||
|             return NO; | ||||
|         } else { | ||||
|             self.protocolsToIDs[proto.name] = pid = @(database.lastRowID); | ||||
|         } | ||||
|          | ||||
|         // Insert its members // | ||||
|          | ||||
|         // Required methods | ||||
|         for (FLEXMethodDescription *method in proto.requiredMethods) { | ||||
|             NSString *selector = NSStringFromSelector(method.selector); | ||||
|             if (!FREInsertProtocolMember(database, pid, @YES, method.instance, nil, selector, image)) { | ||||
|                 return NO; | ||||
|             } | ||||
|         } | ||||
|         // Optional methods | ||||
|         for (FLEXMethodDescription *method in proto.optionalMethods) { | ||||
|             NSString *selector = NSStringFromSelector(method.selector); | ||||
|             if (!FREInsertProtocolMember(database, pid, @NO, method.instance, nil, selector, image)) { | ||||
|                 return NO; | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         if (@available(iOS 10, *)) { | ||||
|             // Required properties | ||||
|             for (FLEXProperty *property in proto.requiredProperties) { | ||||
|                 BOOL success = FREInsertProtocolMember( | ||||
|                    database, pid, @YES, @(property.isClassProperty), property.name, NSNull.null, image | ||||
|                 ); | ||||
|                  | ||||
|                 if (!success) return NO; | ||||
|             } | ||||
|             // Optional properties | ||||
|             for (FLEXProperty *property in proto.optionalProperties) { | ||||
|                 BOOL success = FREInsertProtocolMember( | ||||
|                     database, pid, @NO, @(property.isClassProperty), property.name, NSNull.null, image | ||||
|                 ); | ||||
|                  | ||||
|                 if (!success) return NO; | ||||
|             } | ||||
|         } else { | ||||
|             // Just... properties. | ||||
|             for (FLEXProperty *property in proto.properties) { | ||||
|                 BOOL success = FREInsertProtocolMember( | ||||
|                     database, pid, nil, @(property.isClassProperty), property.name, NSNull.null, image | ||||
|                 ); | ||||
|                  | ||||
|                 if (!success) return NO; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     return YES; | ||||
| } | ||||
|  | ||||
| - (BOOL)addProtocolConformances:(void(^)(NSString *status))progress { | ||||
|     progress(@"Adding protocol-to-protocol conformances…"); | ||||
|      | ||||
|     FLEXSQLiteDatabaseManager *database = self.db; | ||||
|     NSDictionary *protocolIDs = self.protocolsToIDs; | ||||
|      | ||||
|     for (FLEXProtocol *proto in self.protocols) { | ||||
|         id protoID = protocolIDs[proto.name]; | ||||
|          | ||||
|         for (FLEXProtocol *conform in proto.protocols) { | ||||
|             BOOL failed = [database executeStatement:kFREInsertProtocolConformance arguments:@{ | ||||
|                 @"$protocol": protoID, | ||||
|                 @"$conformance": protocolIDs[conform.name] | ||||
|             }].isError; | ||||
|              | ||||
|             if (failed) { | ||||
|                 return NO; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     return YES; | ||||
| } | ||||
|  | ||||
| - (BOOL)addClasses:(void(^)(NSString *status))progress { | ||||
|     progress([NSString stringWithFormat:@"Adding %@ classes…", @(self.classes.count)]); | ||||
|      | ||||
|     FLEXSQLiteDatabaseManager *database = self.db; | ||||
|     NSDictionary *imageIDs = self.bundlePathsToIDs; | ||||
|      | ||||
|     for (Class cls in self.classes) { | ||||
|         const char *imageName = class_getImageName(cls); | ||||
|         id image = imageName ? imageIDs[@(imageName)] : NSNull.null; | ||||
|         image = image ?: NSNull.null; | ||||
|          | ||||
|         BOOL failed = [database executeStatement:kFREInsertClass arguments:@{ | ||||
|             @"$className":    NSStringFromClass(cls), | ||||
|             @"$instanceSize": @(class_getInstanceSize(cls)), | ||||
|             @"$version":      @(class_getVersion(cls)), | ||||
|             @"$image":        image | ||||
|         }].isError; | ||||
|          | ||||
|         if (failed) { | ||||
|             return NO; | ||||
|         } else { | ||||
|             self.classesToIDs[(id)cls] = @(database.lastRowID); | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     return YES; | ||||
| } | ||||
|  | ||||
| - (BOOL)setSuperclasses:(void(^)(NSString *status))progress { | ||||
|     progress(@"Setting superclasses…"); | ||||
|      | ||||
|     FLEXSQLiteDatabaseManager *database = self.db; | ||||
|      | ||||
|     for (Class cls in self.classes) { | ||||
|         // Grab superclass ID | ||||
|         Class superclass = class_getSuperclass(cls); | ||||
|         NSNumber *superclassID = _classesToIDs[class_getSuperclass(cls)]; | ||||
|          | ||||
|         // ... or add the superclass and cache its ID if the | ||||
|         // superclass does not reside in the target image(s) | ||||
|         if (!superclassID) { | ||||
|             NSDictionary *args = @{ @"$className": NSStringFromClass(superclass) }; | ||||
|             BOOL failed = [database executeStatement:kFREInsertClass arguments:args].isError; | ||||
|             if (failed) { return NO; } | ||||
|              | ||||
|             _classesToIDs[(id)superclass] = superclassID = @(database.lastRowID); | ||||
|         } | ||||
|          | ||||
|         if (superclass) { | ||||
|             BOOL failed = [database executeStatement:kFREUpdateClassSetSuper arguments:@{ | ||||
|                 @"$super": superclassID, @"$id": _classesToIDs[cls] | ||||
|             }].isError; | ||||
|              | ||||
|             if (failed) { | ||||
|                 return NO; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     return YES; | ||||
| } | ||||
|  | ||||
| - (BOOL)addClassConformances:(void(^)(NSString *status))progress { | ||||
|     progress(@"Adding class-to-protocol conformances…"); | ||||
|      | ||||
|     FLEXSQLiteDatabaseManager *database = self.db; | ||||
|     NSDictionary *protocolIDs = self.protocolsToIDs; | ||||
|     NSDictionary *classIDs = self.classesToIDs; | ||||
|      | ||||
|     for (Class cls in self.classes) { | ||||
|         id classID = classIDs[(id)cls]; | ||||
|          | ||||
|         for (FLEXProtocol *conform in FLEXGetConformedProtocols(cls)) { | ||||
|             BOOL failed = [database executeStatement:kFREInsertClassConformance arguments:@{ | ||||
|                 @"$class": classID, | ||||
|                 @"$conformance": protocolIDs[conform.name] | ||||
|             }].isError; | ||||
|              | ||||
|             if (failed) { | ||||
|                 return NO; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     return YES; | ||||
| } | ||||
|  | ||||
| - (BOOL)addIvars:(void(^)(NSString *status))progress { | ||||
|     progress(@"Adding ivars…"); | ||||
|      | ||||
|     FLEXSQLiteDatabaseManager *database = self.db; | ||||
|     NSDictionary *imageIDs = self.bundlePathsToIDs; | ||||
|      | ||||
|     for (Class cls in self.classes) { | ||||
|         for (FLEXIvar *ivar in FLEXGetAllIvars(cls)) { | ||||
|             // Insert type first | ||||
|             if (![self addTypeEncoding:ivar.typeEncoding size:ivar.size]) { | ||||
|                 return NO; | ||||
|             } | ||||
|              | ||||
|             id imagePath = ivar.imagePath ?: NSNull.null; | ||||
|             NSNumber *image = imageIDs[imagePath] ?: NSNull.null; | ||||
|              | ||||
|             BOOL failed = [database executeStatement:kFREInsertIvar arguments:@{ | ||||
|                 @"$name":   ivar.name, | ||||
|                 @"$offset": @(ivar.offset), | ||||
|                 @"$type":   _typeEncodingsToIDs[ivar.typeEncoding], | ||||
|                 @"$class":  _classesToIDs[cls], | ||||
|                 @"$image":  image | ||||
|             }].isError; | ||||
|              | ||||
|             if (failed) { | ||||
|                 return NO; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     return YES; | ||||
| } | ||||
|  | ||||
| - (BOOL)addMethods:(void(^)(NSString *status))progress { | ||||
|     progress(@"Adding methods…"); | ||||
|      | ||||
|     FLEXSQLiteDatabaseManager *database = self.db; | ||||
|     NSDictionary *imageIDs = self.bundlePathsToIDs; | ||||
|      | ||||
|     // Loop over all classes | ||||
|     for (Class cls in self.classes) { | ||||
|         NSNumber *classID = _classesToIDs[(id)cls]; | ||||
|         const char *imageName = class_getImageName(cls); | ||||
|         id image = imageName ? imageIDs[@(imageName)] : NSNull.null; | ||||
|         image = image ?: NSNull.null; | ||||
|          | ||||
|         // Block used to process each message | ||||
|         BOOL (^insert)(FLEXMethod *, NSNumber *) = ^BOOL(FLEXMethod *method, NSNumber *instance) { | ||||
|             // Insert selector and signature first | ||||
|             if (![self addSelector:method.selectorString]) { | ||||
|                 return NO; | ||||
|             } | ||||
|             if (![self addMethodSignature:method]) { | ||||
|                 return NO; | ||||
|             } | ||||
|              | ||||
|             return ![database executeStatement:kFREInsertMethod arguments:@{ | ||||
|                 @"$sel":       self->_selectorsToIDs[method.selectorString], | ||||
|                 @"$class":     classID, | ||||
|                 @"$instance":  instance, | ||||
|                 @"$signature": self->_methodSignaturesToIDs[method.signatureString], | ||||
|                 @"$image":     image | ||||
|             }].isError; | ||||
|         }; | ||||
|          | ||||
|         // Loop over all instance and class methods of that class // | ||||
|          | ||||
|         for (FLEXMethod *method in FLEXGetAllMethods(cls, YES)) { | ||||
|             if (!insert(method, @YES)) { | ||||
|                 return NO; | ||||
|             } | ||||
|         } | ||||
|         for (FLEXMethod *method in FLEXGetAllMethods(object_getClass(cls), NO)) { | ||||
|             if (!insert(method, @NO)) { | ||||
|                 return NO; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     return YES; | ||||
| } | ||||
|  | ||||
| - (BOOL)addProperties:(void(^)(NSString *status))progress { | ||||
|     progress(@"Adding properties…"); | ||||
|      | ||||
|     FLEXSQLiteDatabaseManager *database = self.db; | ||||
|     NSDictionary *imageIDs = self.bundlePathsToIDs; | ||||
|      | ||||
|     // Loop over all classes | ||||
|     for (Class cls in self.classes) { | ||||
|         NSNumber *classID = _classesToIDs[(id)cls]; | ||||
|          | ||||
|         // Block used to process each message | ||||
|         BOOL (^insert)(FLEXProperty *, NSNumber *) = ^BOOL(FLEXProperty *property, NSNumber *instance) { | ||||
|             FLEXPropertyAttributes *attrs = property.attributes; | ||||
|             NSString *customGetter = attrs.customGetterString; | ||||
|             NSString *customSetter = attrs.customSetterString; | ||||
|              | ||||
|             // Insert selectors first | ||||
|             if (customGetter) { | ||||
|                 if (![self addSelector:customGetter]) { | ||||
|                     return NO; | ||||
|                 } | ||||
|             } | ||||
|             if (customSetter) { | ||||
|                 if (![self addSelector:customSetter]) { | ||||
|                     return NO; | ||||
|                 } | ||||
|             } | ||||
|              | ||||
|             // Insert type encoding first | ||||
|             NSInteger size = [FLEXTypeEncodingParser | ||||
|                 sizeForTypeEncoding:attrs.typeEncoding alignment:nil | ||||
|             ]; | ||||
|             if (![self addTypeEncoding:attrs.typeEncoding size:size]) { | ||||
|                 return NO; | ||||
|             } | ||||
|              | ||||
|             id imagePath = property.imagePath ?: NSNull.null; | ||||
|             id image = imageIDs[imagePath] ?: NSNull.null; | ||||
|             return ![database executeStatement:kFREInsertProperty arguments:@{ | ||||
|                 @"$name":       property.name, | ||||
|                 @"$class":      classID, | ||||
|                 @"$instance":   instance, | ||||
|                 @"$image":      image, | ||||
|                 @"$attributes": attrs.string, | ||||
|                  | ||||
|                 @"$customGetter": self->_selectorsToIDs[customGetter] ?: NSNull.null, | ||||
|                 @"$customSetter": self->_selectorsToIDs[customSetter] ?: NSNull.null, | ||||
|                  | ||||
|                 @"$type":      self->_typeEncodingsToIDs[attrs.typeEncoding] ?: NSNull.null, | ||||
|                 @"$ivar":      attrs.backingIvar ?: NSNull.null, | ||||
|                 @"$readonly":  @(attrs.isReadOnly), | ||||
|                 @"$copy":      @(attrs.isCopy), | ||||
|                 @"$retained":  @(attrs.isRetained), | ||||
|                 @"$nonatomic": @(attrs.isNonatomic), | ||||
|                 @"$dynamic":   @(attrs.isDynamic), | ||||
|                 @"$weak":      @(attrs.isWeak), | ||||
|                 @"$canGC":     @(attrs.isGarbageCollectable), | ||||
|             }].isError; | ||||
|         }; | ||||
|          | ||||
|         // Loop over all instance and class methods of that class // | ||||
|          | ||||
|         for (FLEXProperty *property in FLEXGetAllProperties(cls)) { | ||||
|             if (!insert(property, @YES)) { | ||||
|                 return NO; | ||||
|             } | ||||
|         } | ||||
|         for (FLEXProperty *property in FLEXGetAllProperties(object_getClass(cls))) { | ||||
|             if (!insert(property, @NO)) { | ||||
|                 return NO; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     return YES; | ||||
| } | ||||
|  | ||||
| - (BOOL)addSelector:(NSString *)sel { | ||||
|     return [self executeInsert:kFREInsertSelector args:@{ | ||||
|         @"$name": sel | ||||
|     } key:sel cacheResult:_selectorsToIDs]; | ||||
| } | ||||
|  | ||||
| - (BOOL)addTypeEncoding:(NSString *)type size:(NSInteger)size { | ||||
|     return [self executeInsert:kFREInsertTypeEncoding args:@{ | ||||
|         @"$type": type, @"$size": @(size) | ||||
|     } key:type cacheResult:_typeEncodingsToIDs]; | ||||
| } | ||||
|  | ||||
| - (BOOL)addMethodSignature:(FLEXMethod *)method { | ||||
|     NSString *signature = method.signatureString; | ||||
|     NSString *returnType = @((char *)method.returnType); | ||||
|      | ||||
|     // Insert return type first | ||||
|     if (![self addTypeEncoding:returnType size:method.returnSize]) { | ||||
|         return NO; | ||||
|     } | ||||
|      | ||||
|     return [self executeInsert:kFREInsertMethodSignature args:@{ | ||||
|         @"$typeEncoding": signature, | ||||
|         @"$returnType":   _typeEncodingsToIDs[returnType], | ||||
|         @"$argc":         @(method.numberOfArguments), | ||||
|         @"$frameLength":  @(method.signature.frameLength) | ||||
|     } key:signature cacheResult:_methodSignaturesToIDs]; | ||||
| } | ||||
|  | ||||
| - (BOOL)executeInsert:(NSString *)statement | ||||
|                  args:(NSDictionary *)args | ||||
|                   key:(NSString *)cacheKey | ||||
|           cacheResult:(NSMutableDictionary<NSString *, NSNumber *> *)rowids { | ||||
|     // Check if already inserted | ||||
|     if (rowids[cacheKey]) { | ||||
|         return YES; | ||||
|     } | ||||
|      | ||||
|     // Insert | ||||
|     FLEXSQLiteDatabaseManager *database = _db; | ||||
|     [database executeStatement:statement arguments:args]; | ||||
|      | ||||
|     if (database.lastResult.isError) { | ||||
|         return NO; | ||||
|     } | ||||
|      | ||||
|     // Cache rowid | ||||
|     rowids[cacheKey] = @(database.lastRowID); | ||||
|     return YES; | ||||
| } | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,31 @@ | ||||
| // | ||||
| //  FLEXKBToolbarButton.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 6/11/17. | ||||
| //  Copyright © 2017 Tanner Bennett. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import <UIKit/UIKit.h> | ||||
|  | ||||
| typedef void (^FLEXKBToolbarAction)(NSString *buttonTitle, BOOL isSuggestion); | ||||
|  | ||||
|  | ||||
| @interface FLEXKBToolbarButton : UIButton | ||||
|  | ||||
| /// Set to `default` to use the system appearance on iOS 13+ | ||||
| @property (nonatomic) UIKeyboardAppearance appearance; | ||||
|  | ||||
| + (instancetype)buttonWithTitle:(NSString *)title; | ||||
| + (instancetype)buttonWithTitle:(NSString *)title action:(FLEXKBToolbarAction)eventHandler; | ||||
| + (instancetype)buttonWithTitle:(NSString *)title action:(FLEXKBToolbarAction)action forControlEvents:(UIControlEvents)controlEvents; | ||||
|  | ||||
| /// Adds the event handler for the button. | ||||
| /// | ||||
| /// @param eventHandler The event handler block. | ||||
| /// @param controlEvents The type of event. | ||||
| - (void)addEventHandler:(FLEXKBToolbarAction)eventHandler forControlEvents:(UIControlEvents)controlEvents; | ||||
|  | ||||
| @end | ||||
|  | ||||
| @interface FLEXKBToolbarSuggestedButton : FLEXKBToolbarButton @end | ||||
| @@ -0,0 +1,160 @@ | ||||
| // | ||||
| //  FLEXKBToolbarButton.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 6/11/17. | ||||
| //  Copyright © 2017 Tanner Bennett. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXKBToolbarButton.h" | ||||
| #import "UIFont+FLEX.h" | ||||
| #import "FLEXUtility.h" | ||||
| #import "CALayer+FLEX.h" | ||||
|  | ||||
| @interface FLEXKBToolbarButton () | ||||
| @property (nonatomic      ) NSString *title; | ||||
| @property (nonatomic, copy) FLEXKBToolbarAction buttonPressBlock; | ||||
| /// YES if appearance is set to `default` | ||||
| @property (nonatomic, readonly) BOOL useSystemAppearance; | ||||
| /// YES if the current trait collection is set to dark mode and \c useSystemAppearance is YES | ||||
| @property (nonatomic, readonly) BOOL usingDarkMode; | ||||
| @end | ||||
|  | ||||
| @implementation FLEXKBToolbarButton | ||||
|  | ||||
| + (instancetype)buttonWithTitle:(NSString *)title { | ||||
|     return [[self alloc] initWithTitle:title]; | ||||
| } | ||||
|  | ||||
| + (instancetype)buttonWithTitle:(NSString *)title action:(FLEXKBToolbarAction)eventHandler forControlEvents:(UIControlEvents)controlEvent { | ||||
|     FLEXKBToolbarButton *newButton = [self buttonWithTitle:title]; | ||||
|     [newButton addEventHandler:eventHandler forControlEvents:controlEvent]; | ||||
|     return newButton; | ||||
| } | ||||
|  | ||||
| + (instancetype)buttonWithTitle:(NSString *)title action:(FLEXKBToolbarAction)eventHandler { | ||||
|     return [self buttonWithTitle:title action:eventHandler forControlEvents:UIControlEventTouchUpInside]; | ||||
| } | ||||
|  | ||||
| - (id)initWithTitle:(NSString *)title { | ||||
|     self = [super init]; | ||||
|     if (self) { | ||||
|         _title = title; | ||||
|         self.layer.shadowOffset = CGSizeMake(0, 1); | ||||
|         self.layer.shadowOpacity = 0.35; | ||||
|         self.layer.shadowRadius  = 0; | ||||
|         self.layer.cornerRadius  = 5; | ||||
|         self.clipsToBounds       = NO; | ||||
|         self.titleLabel.font     = [UIFont systemFontOfSize:18.0]; | ||||
|         self.layer.flex_continuousCorners = YES; | ||||
|         [self setTitle:self.title forState:UIControlStateNormal]; | ||||
|         [self sizeToFit]; | ||||
|          | ||||
|         if (@available(iOS 13, *)) { | ||||
|             self.appearance = UIKeyboardAppearanceDefault; | ||||
|         } else { | ||||
|             self.appearance = UIKeyboardAppearanceLight; | ||||
|         } | ||||
|          | ||||
|         CGRect frame = self.frame; | ||||
|         frame.size.width  += title.length < 3 ? 30 : 15; | ||||
|         frame.size.height += 10; | ||||
|         self.frame = frame; | ||||
|     } | ||||
|      | ||||
|     return self; | ||||
| } | ||||
|  | ||||
| - (void)addEventHandler:(FLEXKBToolbarAction)eventHandler forControlEvents:(UIControlEvents)controlEvent { | ||||
|     self.buttonPressBlock = eventHandler; | ||||
|     [self addTarget:self action:@selector(buttonPressed) forControlEvents:controlEvent]; | ||||
| } | ||||
|  | ||||
| - (void)buttonPressed { | ||||
|     self.buttonPressBlock(self.title, NO); | ||||
| } | ||||
|  | ||||
| - (void)setAppearance:(UIKeyboardAppearance)appearance { | ||||
|     _appearance = appearance; | ||||
|      | ||||
|     UIColor *titleColor = nil, *backgroundColor = nil; | ||||
|     UIColor *lightColor = [UIColor colorWithRed:253.0/255.0 green:253.0/255.0 blue:254.0/255.0 alpha:1]; | ||||
|     UIColor *darkColor = [UIColor colorWithRed:101.0/255.0 green:102.0/255.0 blue:104.0/255.0 alpha:1]; | ||||
|      | ||||
|     switch (_appearance) { | ||||
|         default: | ||||
|         case UIKeyboardAppearanceDefault: | ||||
|             if (@available(iOS 13, *)) { | ||||
|                 titleColor = UIColor.labelColor; | ||||
|                  | ||||
|                 if (self.usingDarkMode) { | ||||
|                     // style = UIBlurEffectStyleSystemUltraThinMaterialLight; | ||||
|                     backgroundColor = darkColor; | ||||
|                 } else { | ||||
|                     // style = UIBlurEffectStyleSystemMaterialLight; | ||||
|                     backgroundColor = lightColor; | ||||
|                 } | ||||
|                 break; | ||||
|             } | ||||
|         case UIKeyboardAppearanceLight: | ||||
|             titleColor = UIColor.blackColor; | ||||
|             backgroundColor = lightColor; | ||||
|             // style = UIBlurEffectStyleExtraLight; | ||||
|             break; | ||||
|         case UIKeyboardAppearanceDark: | ||||
|             titleColor = UIColor.whiteColor; | ||||
|             backgroundColor = darkColor; | ||||
|             // style = UIBlurEffectStyleDark; | ||||
|             break; | ||||
|     } | ||||
|      | ||||
|     self.backgroundColor = backgroundColor; | ||||
|     [self setTitleColor:titleColor forState:UIControlStateNormal]; | ||||
| } | ||||
|  | ||||
| - (BOOL)isEqual:(id)object { | ||||
|     if ([object isKindOfClass:[FLEXKBToolbarButton class]]) { | ||||
|         return [self.title isEqualToString:[object title]]; | ||||
|     } | ||||
|  | ||||
|     return NO; | ||||
| } | ||||
|  | ||||
| - (NSUInteger)hash { | ||||
|     return self.title.hash; | ||||
| } | ||||
|  | ||||
| - (BOOL)useSystemAppearance { | ||||
|     return self.appearance == UIKeyboardAppearanceDefault; | ||||
| } | ||||
|  | ||||
| - (BOOL)usingDarkMode { | ||||
|     if (@available(iOS 12, *)) { | ||||
|         return self.useSystemAppearance && self.traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark; | ||||
|     } | ||||
|      | ||||
|     return self.appearance == UIKeyboardAppearanceDark; | ||||
| } | ||||
|  | ||||
| - (void)traitCollectionDidChange:(UITraitCollection *)previous { | ||||
|     if (@available(iOS 12, *)) { | ||||
|         // Was darkmode toggled? | ||||
|         if (previous.userInterfaceStyle != self.traitCollection.userInterfaceStyle) { | ||||
|             if (self.useSystemAppearance) { | ||||
|                 // Recreate the background view with the proper colors | ||||
|                 self.appearance = self.appearance; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @end | ||||
|  | ||||
|  | ||||
| @implementation FLEXKBToolbarSuggestedButton | ||||
|  | ||||
| - (void)buttonPressed { | ||||
|     self.buttonPressBlock(self.title, YES); | ||||
| } | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,38 @@ | ||||
| // | ||||
| //  FLEXKeyPathSearchController.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 3/23/17. | ||||
| //  Copyright © 2017 Tanner Bennett. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import <UIKit/UIKit.h> | ||||
| #import "FLEXRuntimeBrowserToolbar.h" | ||||
| #import "FLEXMethod.h" | ||||
|  | ||||
| @protocol FLEXKeyPathSearchControllerDelegate <UITableViewDataSource> | ||||
|  | ||||
| @property (nonatomic, readonly) UITableView *tableView; | ||||
| @property (nonatomic, readonly) UISearchController *searchController; | ||||
|  | ||||
| /// For loaded images which don't have an NSBundle | ||||
| - (void)didSelectImagePath:(NSString *)message shortName:(NSString *)shortName; | ||||
| - (void)didSelectBundle:(NSBundle *)bundle; | ||||
| - (void)didSelectClass:(Class)cls; | ||||
|  | ||||
| @end | ||||
|  | ||||
|  | ||||
| @interface FLEXKeyPathSearchController : NSObject <UISearchBarDelegate, UITableViewDataSource, UITableViewDelegate> | ||||
|  | ||||
| + (instancetype)delegate:(id<FLEXKeyPathSearchControllerDelegate>)delegate; | ||||
|  | ||||
| @property (nonatomic) FLEXRuntimeBrowserToolbar *toolbar; | ||||
|  | ||||
| /// Suggestions for the toolbar | ||||
| @property (nonatomic, readonly) NSArray<NSString *> *suggestions; | ||||
|  | ||||
| - (void)didSelectKeyPathOption:(NSString *)text; | ||||
| - (void)didPressButton:(NSString *)text insertInto:(UISearchBar *)searchBar; | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,417 @@ | ||||
| // | ||||
| //  FLEXKeyPathSearchController.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 3/23/17. | ||||
| //  Copyright © 2017 Tanner Bennett. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXKeyPathSearchController.h" | ||||
| #import "FLEXRuntimeKeyPathTokenizer.h" | ||||
| #import "FLEXRuntimeController.h" | ||||
| #import "NSString+FLEX.h" | ||||
| #import "NSArray+FLEX.h" | ||||
| #import "UITextField+Range.h" | ||||
| #import "NSTimer+FLEX.h" | ||||
| #import "FLEXTableView.h" | ||||
| #import "FLEXUtility.h" | ||||
| #import "FLEXObjectExplorerFactory.h" | ||||
|  | ||||
| @interface FLEXKeyPathSearchController () | ||||
| @property (nonatomic, readonly, weak) id<FLEXKeyPathSearchControllerDelegate> delegate; | ||||
| @property (nonatomic) NSTimer *timer; | ||||
| /// If \c keyPath is \c nil or if it only has a \c bundleKey, this is | ||||
| /// a list of bundle key path components like \c UICatalog or \c UIKit\.framework | ||||
| /// If \c keyPath has more than a \c bundleKey then it is a list of class names. | ||||
| @property (nonatomic) NSArray<NSString *> *bundlesOrClasses; | ||||
| /// nil when search bar is empty | ||||
| @property (nonatomic) FLEXRuntimeKeyPath *keyPath; | ||||
|  | ||||
| @property (nonatomic, readonly) NSString *emptySuggestion; | ||||
|  | ||||
| /// Used to track which methods go with which classes. This is used in | ||||
| /// two scenarios: (1) when the target class is absolute and has classes, | ||||
| /// (this list will include the "leaf" class as well as parent classes in this case) | ||||
| /// or (2) when the class key is a wildcard and we're searching methods in many | ||||
| /// classes at once. Each list in \c classesToMethods correspnds to a class here. | ||||
| @property (nonatomic) NSArray<NSString *> *classes; | ||||
| /// A filtered version of \c classes used when searching for a specific attribute. | ||||
| /// Classes with no matching ivars/properties/methods are not shown. | ||||
| @property (nonatomic) NSArray<NSString *> *filteredClasses; | ||||
| // We use this regardless of whether the target class is absolute, just as above | ||||
| @property (nonatomic) NSArray<NSArray<FLEXMethod *> *> *classesToMethods; | ||||
| @end | ||||
|  | ||||
| @implementation FLEXKeyPathSearchController | ||||
|  | ||||
| + (instancetype)delegate:(id<FLEXKeyPathSearchControllerDelegate>)delegate { | ||||
|     FLEXKeyPathSearchController *controller = [self new]; | ||||
|     controller->_bundlesOrClasses = [FLEXRuntimeController allBundleNames]; | ||||
|     controller->_delegate         = delegate; | ||||
|     controller->_emptySuggestion  = NSBundle.mainBundle.executablePath.lastPathComponent; | ||||
|  | ||||
|     NSParameterAssert(delegate.tableView); | ||||
|     NSParameterAssert(delegate.searchController); | ||||
|  | ||||
|     delegate.tableView.delegate   = controller; | ||||
|     delegate.tableView.dataSource = controller; | ||||
|      | ||||
|     UISearchBar *searchBar = delegate.searchController.searchBar; | ||||
|     searchBar.delegate = controller;    | ||||
|     searchBar.keyboardType = UIKeyboardTypeWebSearch; | ||||
|     searchBar.autocorrectionType = UITextAutocorrectionTypeNo; | ||||
|     if (@available(iOS 11, *)) { | ||||
|         searchBar.smartQuotesType = UITextSmartQuotesTypeNo; | ||||
|         searchBar.smartInsertDeleteType = UITextSmartInsertDeleteTypeNo; | ||||
|     } | ||||
|  | ||||
|     return controller; | ||||
| } | ||||
|  | ||||
| - (void)scrollViewDidScroll:(UIScrollView *)scrollView { | ||||
|     if (scrollView.isTracking || scrollView.isDragging || scrollView.isDecelerating) { | ||||
|         [self.delegate.searchController.searchBar resignFirstResponder]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)setToolbar:(FLEXRuntimeBrowserToolbar *)toolbar { | ||||
|     _toolbar = toolbar; | ||||
|     self.delegate.searchController.searchBar.inputAccessoryView = toolbar; | ||||
| } | ||||
|  | ||||
| - (NSArray<NSString *> *)classesOf:(NSString *)className { | ||||
|     Class baseClass = NSClassFromString(className); | ||||
|     if (!baseClass) { | ||||
|         return @[]; | ||||
|     } | ||||
|  | ||||
|     // Find classes | ||||
|     NSMutableArray<NSString*> *classes = [NSMutableArray arrayWithObject:className]; | ||||
|     while ([baseClass superclass]) { | ||||
|         [classes addObject:NSStringFromClass([baseClass superclass])]; | ||||
|         baseClass = [baseClass superclass]; | ||||
|     } | ||||
|  | ||||
|     return classes; | ||||
| } | ||||
|  | ||||
| #pragma mark Key path stuff | ||||
|  | ||||
| - (void)didSelectKeyPathOption:(NSString *)text { | ||||
|     [_timer invalidate]; // Still might be waiting to refresh when method is selected | ||||
|  | ||||
|     // Change "Bundle.fooba" to "Bundle.foobar." | ||||
|     NSString *orig = self.delegate.searchController.searchBar.text; | ||||
|     NSString *keyPath = [orig flex_stringByReplacingLastKeyPathComponent:text]; | ||||
|     self.delegate.searchController.searchBar.text = keyPath; | ||||
|  | ||||
|     self.keyPath = [FLEXRuntimeKeyPathTokenizer tokenizeString:keyPath]; | ||||
|  | ||||
|     // Get classes if class was selected | ||||
|     if (self.keyPath.classKey.isAbsolute && self.keyPath.methodKey.isAny) { | ||||
|         [self didSelectAbsoluteClass:text]; | ||||
|     } else { | ||||
|         self.classes = nil; | ||||
|         self.filteredClasses = nil; | ||||
|     } | ||||
|  | ||||
|     [self updateTable]; | ||||
| } | ||||
|  | ||||
| - (void)didSelectAbsoluteClass:(NSString *)name { | ||||
|     self.classes          = [self classesOf:name]; | ||||
|     self.filteredClasses  = self.classes; | ||||
|     self.bundlesOrClasses = nil; | ||||
|     self.classesToMethods = nil; | ||||
| } | ||||
|  | ||||
| - (void)didPressButton:(NSString *)text insertInto:(UISearchBar *)searchBar { | ||||
|     [self.toolbar setKeyPath:self.keyPath suggestions:nil]; | ||||
|      | ||||
|     // Available since at least iOS 9, still present in iOS 13 | ||||
|     UITextField *field = [searchBar valueForKey:@"_searchBarTextField"]; | ||||
|  | ||||
|     if ([self searchBar:searchBar shouldChangeTextInRange:field.flex_selectedRange replacementText:text]) { | ||||
|         [field replaceRange:field.selectedTextRange withText:text]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (NSArray<NSString *> *)suggestions { | ||||
|     if (self.bundlesOrClasses) { | ||||
|         if (self.classes) { | ||||
|             if (self.classesToMethods) { | ||||
|                 // We have selected a class and are searching metadata | ||||
|                 return nil; | ||||
|             } | ||||
|              | ||||
|             // We are currently searching classes | ||||
|             return [self.filteredClasses flex_subArrayUpto:10]; | ||||
|         } | ||||
|          | ||||
|         if (!self.keyPath) { | ||||
|             // Search bar is empty | ||||
|             return @[self.emptySuggestion]; | ||||
|         } | ||||
|          | ||||
|         // We are currently searching bundles | ||||
|         return [self.bundlesOrClasses flex_subArrayUpto:10]; | ||||
|     } | ||||
|      | ||||
|     // We have nothing at all to even search | ||||
|     return nil; | ||||
| } | ||||
|  | ||||
| #pragma mark - Filtering + UISearchBarDelegate | ||||
|  | ||||
| - (void)updateTable { | ||||
|     // Compute the method, class, or bundle lists on a background thread | ||||
|     dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ | ||||
|         if (self.classes) { | ||||
|             // Here, our class key is 'absolute'; .classes is a list of superclasses | ||||
|             // and we want to show the methods for those classes specifically | ||||
|             // TODO: add caching to this somehow | ||||
|             NSMutableArray *methods = [FLEXRuntimeController | ||||
|                 methodsForToken:self.keyPath.methodKey | ||||
|                 instance:self.keyPath.instanceMethods | ||||
|                 inClasses:self.classes | ||||
|             ].mutableCopy; | ||||
|              | ||||
|             // Remove classes without results if we're searching for a method | ||||
|             // | ||||
|             // Note: this will remove classes without any methods or overrides | ||||
|             // even if the query doesn't specify a method, like `*.*.` | ||||
|             if (self.keyPath.methodKey) { | ||||
|                 [self setNonEmptyMethodLists:methods withClasses:self.classes.mutableCopy]; | ||||
|             } else { | ||||
|                 self.filteredClasses = self.classes; | ||||
|             } | ||||
|         } | ||||
|         else { | ||||
|             FLEXRuntimeKeyPath *keyPath = self.keyPath; | ||||
|             NSArray *models = [FLEXRuntimeController dataForKeyPath:keyPath]; | ||||
|             if (keyPath.methodKey) { // We're looking at methods | ||||
|                 self.bundlesOrClasses = nil; | ||||
|                  | ||||
|                 NSMutableArray *methods = models.mutableCopy; | ||||
|                 NSMutableArray<NSString *> *classes = [ | ||||
|                     FLEXRuntimeController classesForKeyPath:keyPath | ||||
|                 ]; | ||||
|                 self.classes = classes; | ||||
|                 [self setNonEmptyMethodLists:methods withClasses:classes]; | ||||
|             } else { // We're looking at bundles or classes | ||||
|                 self.bundlesOrClasses = models; | ||||
|                 self.classesToMethods = nil; | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         // Finally, reload the table on the main thread | ||||
|         dispatch_async(dispatch_get_main_queue(), ^{ | ||||
|             [self updateToolbarButtons]; | ||||
|             [self.delegate.tableView reloadData]; | ||||
|         }); | ||||
|     }); | ||||
| } | ||||
|  | ||||
| - (void)updateToolbarButtons { | ||||
|     // Update toolbar buttons | ||||
|     [self.toolbar setKeyPath:self.keyPath suggestions:self.suggestions]; | ||||
| } | ||||
|  | ||||
| /// Assign assign .filteredClasses and .classesToMethods after removing empty sections | ||||
| - (void)setNonEmptyMethodLists:(NSMutableArray<NSArray<FLEXMethod *> *> *)methods | ||||
|                    withClasses:(NSMutableArray<NSString *> *)classes { | ||||
|     // Remove sections with no methods | ||||
|     NSIndexSet *allEmpty = [methods indexesOfObjectsPassingTest:^BOOL(NSArray *list, NSUInteger idx, BOOL *stop) { | ||||
|         return list.count == 0; | ||||
|     }]; | ||||
|     [methods removeObjectsAtIndexes:allEmpty]; | ||||
|     [classes removeObjectsAtIndexes:allEmpty]; | ||||
|      | ||||
|     self.filteredClasses = classes; | ||||
|     self.classesToMethods = methods; | ||||
| } | ||||
|  | ||||
| - (BOOL)searchBar:(UISearchBar *)searchBar shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text { | ||||
|     // Check if character is even legal | ||||
|     if (![FLEXRuntimeKeyPathTokenizer allowedInKeyPath:text]) { | ||||
|         return NO; | ||||
|     } | ||||
|      | ||||
|     BOOL terminatedToken = NO; | ||||
|     BOOL isAppending = range.length == 0 && range.location == searchBar.text.length; | ||||
|     if (isAppending && [text isEqualToString:@"."]) { | ||||
|         terminatedToken = YES; | ||||
|     } | ||||
|  | ||||
|     // Actually parse input | ||||
|     @try { | ||||
|         text = [searchBar.text stringByReplacingCharactersInRange:range withString:text] ?: text; | ||||
|         self.keyPath = [FLEXRuntimeKeyPathTokenizer tokenizeString:text]; | ||||
|         if (self.keyPath.classKey.isAbsolute && terminatedToken) { | ||||
|             [self didSelectAbsoluteClass:self.keyPath.classKey.string]; | ||||
|         } | ||||
|     } @catch (id e) { | ||||
|         return NO; | ||||
|     } | ||||
|  | ||||
|     return YES; | ||||
| } | ||||
|  | ||||
| - (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText { | ||||
|     [_timer invalidate]; | ||||
|  | ||||
|     // Schedule update timer | ||||
|     if (searchText.length) { | ||||
|         if (!self.keyPath.methodKey) { | ||||
|             self.classes = nil; | ||||
|             self.filteredClasses = nil; | ||||
|         } | ||||
|  | ||||
|         self.timer = [NSTimer flex_fireSecondsFromNow:0.15 block:^{ | ||||
|             [self updateTable]; | ||||
|         }]; | ||||
|     } | ||||
|     // ... or remove all rows | ||||
|     else { | ||||
|         _bundlesOrClasses = [FLEXRuntimeController allBundleNames]; | ||||
|         _classesToMethods = nil; | ||||
|         _classes = nil; | ||||
|         _keyPath = nil; | ||||
|         [self updateToolbarButtons]; | ||||
|         [self.delegate.tableView reloadData]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar { | ||||
|     self.keyPath = FLEXRuntimeKeyPath.empty; | ||||
|     [self updateTable]; | ||||
| } | ||||
|  | ||||
| /// Restore key path when going "back" and activating search bar again | ||||
| - (void)searchBarTextDidBeginEditing:(UISearchBar *)searchBar { | ||||
|     searchBar.text = self.keyPath.description; | ||||
| } | ||||
|  | ||||
| - (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar { | ||||
|     [_timer invalidate]; | ||||
|     [searchBar resignFirstResponder]; | ||||
|     [self updateTable]; | ||||
| } | ||||
|  | ||||
| #pragma mark UITableViewDataSource | ||||
|  | ||||
| - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { | ||||
|     return self.filteredClasses.count ?: self.bundlesOrClasses.count; | ||||
| } | ||||
|  | ||||
| - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { | ||||
|     UITableViewCell *cell = [tableView | ||||
|         dequeueReusableCellWithIdentifier:kFLEXMultilineDetailCell | ||||
|         forIndexPath:indexPath | ||||
|     ]; | ||||
|      | ||||
|     if (self.bundlesOrClasses.count) { | ||||
|         cell.accessoryType        = UITableViewCellAccessoryDetailButton; | ||||
|         cell.textLabel.text       = self.bundlesOrClasses[indexPath.row]; | ||||
|         cell.detailTextLabel.text = nil; | ||||
|         if (self.keyPath.classKey) { | ||||
|             cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator; | ||||
|         } | ||||
|     } | ||||
|     // One row per section | ||||
|     else if (self.filteredClasses.count) { | ||||
|         NSArray<FLEXMethod *> *methods = self.classesToMethods[indexPath.row]; | ||||
|         NSMutableString *summary = [NSMutableString new]; | ||||
|         [methods enumerateObjectsUsingBlock:^(FLEXMethod *method, NSUInteger idx, BOOL *stop) { | ||||
|             NSString *format = nil; | ||||
|             if (idx == methods.count-1) { | ||||
|                 format = @"%@%@"; | ||||
|                 *stop = YES; | ||||
|             } else if (idx < 3) { | ||||
|                 format = @"%@%@\n"; | ||||
|             } else { | ||||
|                 format = @"%@%@\n…"; | ||||
|                 *stop = YES; | ||||
|             } | ||||
|  | ||||
|             [summary appendFormat:format, method.isInstanceMethod ? @"-" : @"+", method.selectorString]; | ||||
|         }]; | ||||
|  | ||||
|         cell.accessoryType        = UITableViewCellAccessoryDisclosureIndicator; | ||||
|         cell.textLabel.text       = self.filteredClasses[indexPath.row]; | ||||
|         if (@available(iOS 10, *)) { | ||||
|             cell.detailTextLabel.text = summary.length ? summary : nil; | ||||
|         } | ||||
|  | ||||
|     } | ||||
|     else { | ||||
|         @throw NSInternalInconsistencyException; | ||||
|     } | ||||
|  | ||||
|     return cell; | ||||
| } | ||||
|  | ||||
| - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { | ||||
|     if (self.filteredClasses || self.keyPath.methodKey) { | ||||
|         return @" "; | ||||
|     } else if (self.bundlesOrClasses) { | ||||
|         NSInteger count = self.bundlesOrClasses.count; | ||||
|         if (self.keyPath.classKey) { | ||||
|             return FLEXPluralString(count, @"classes", @"class"); | ||||
|         } else { | ||||
|             return FLEXPluralString(count, @"bundles", @"bundle"); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     return [self.delegate tableView:tableView titleForHeaderInSection:section]; | ||||
| } | ||||
|  | ||||
| - (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section { | ||||
|     if (self.filteredClasses || self.keyPath.methodKey) { | ||||
|         if (section == 0) { | ||||
|             return 55; | ||||
|         } | ||||
|  | ||||
|         return 0; | ||||
|     } | ||||
|  | ||||
|     return 55; | ||||
| } | ||||
|  | ||||
| #pragma mark UITableViewDelegate | ||||
|  | ||||
| - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { | ||||
|     if (self.bundlesOrClasses) { | ||||
|         NSString *bundleSuffixOrClass = self.bundlesOrClasses[indexPath.row]; | ||||
|         if (self.keyPath.classKey) { | ||||
|             NSParameterAssert(NSClassFromString(bundleSuffixOrClass)); | ||||
|             [self.delegate didSelectClass:NSClassFromString(bundleSuffixOrClass)]; | ||||
|         } else { | ||||
|             // Selected a bundle | ||||
|             [self didSelectKeyPathOption:bundleSuffixOrClass]; | ||||
|         } | ||||
|     } else { | ||||
|         if (self.filteredClasses.count) { | ||||
|             Class cls = NSClassFromString(self.filteredClasses[indexPath.row]); | ||||
|             NSParameterAssert(cls); | ||||
|             [self.delegate didSelectClass:cls]; | ||||
|         } else { | ||||
|             @throw NSInternalInconsistencyException; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)tableView:(UITableView *)tableView accessoryButtonTappedForRowWithIndexPath:(NSIndexPath *)indexPath { | ||||
|     NSString *bundleSuffixOrClass = self.bundlesOrClasses[indexPath.row]; | ||||
|     NSString *imagePath = [FLEXRuntimeController imagePathWithShortName:bundleSuffixOrClass]; | ||||
|     NSBundle *bundle = [NSBundle bundleWithPath:imagePath.stringByDeletingLastPathComponent]; | ||||
|  | ||||
|     if (bundle) { | ||||
|         [self.delegate didSelectBundle:bundle]; | ||||
|     } else { | ||||
|         [self.delegate didSelectImagePath:imagePath shortName:bundleSuffixOrClass]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @end | ||||
|  | ||||
| @@ -0,0 +1,18 @@ | ||||
| // | ||||
| //  FLEXKeyboardToolbar.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 6/11/17. | ||||
| //  Copyright © 2017 Tanner Bennett. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXKBToolbarButton.h" | ||||
|  | ||||
| @interface FLEXKeyboardToolbar : UIView | ||||
|  | ||||
| + (instancetype)toolbarWithButtons:(NSArray *)buttons; | ||||
|  | ||||
| @property (nonatomic) NSArray<FLEXKBToolbarButton*> *buttons; | ||||
| @property (nonatomic) UIKeyboardAppearance appearance; | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,225 @@ | ||||
| // | ||||
| //  FLEXKeyboardToolbar.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 6/11/17. | ||||
| //  Copyright © 2017 Tanner Bennett. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXKeyboardToolbar.h" | ||||
| #import "FLEXUtility.h" | ||||
|  | ||||
| #define kToolbarHeight 44 | ||||
| #define kButtonSpacing 6 | ||||
| #define kScrollViewHorizontalMargins 3 | ||||
|  | ||||
| @interface FLEXKeyboardToolbar () | ||||
|  | ||||
| /// The fake top border to replicate the toolbar. | ||||
| @property (nonatomic) CALayer      *topBorder; | ||||
| @property (nonatomic) UIView       *toolbarView; | ||||
| @property (nonatomic) UIScrollView *scrollView; | ||||
| @property (nonatomic) UIVisualEffectView *blurView; | ||||
| /// YES if appearance is set to `default` | ||||
| @property (nonatomic, readonly) BOOL useSystemAppearance; | ||||
| /// YES if the current trait collection is set to dark mode and \c useSystemAppearance is YES | ||||
| @property (nonatomic, readonly) BOOL usingDarkMode; | ||||
| @end | ||||
|  | ||||
| @implementation FLEXKeyboardToolbar | ||||
|  | ||||
| + (instancetype)toolbarWithButtons:(NSArray *)buttons { | ||||
|     return [[self alloc] initWithButtons:buttons]; | ||||
| } | ||||
|  | ||||
| - (id)initWithButtons:(NSArray *)buttons { | ||||
|     self = [super initWithFrame:CGRectMake(0, 0, self.window.rootViewController.view.bounds.size.width, kToolbarHeight)]; | ||||
|     if (self) { | ||||
|         _buttons = [buttons copy]; | ||||
|          | ||||
|         self.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; | ||||
|          | ||||
|         if (@available(iOS 13, *)) { | ||||
|             self.appearance = UIKeyboardAppearanceDefault; | ||||
|         } else { | ||||
|             self.appearance = UIKeyboardAppearanceLight; | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     return self; | ||||
| } | ||||
|  | ||||
| - (void)setAppearance:(UIKeyboardAppearance)appearance { | ||||
|     _appearance = appearance; | ||||
|      | ||||
|     // Remove toolbar if it exits because it will be recreated below | ||||
|     if (self.toolbarView) { | ||||
|         [self.toolbarView removeFromSuperview]; | ||||
|     } | ||||
|      | ||||
|     [self addSubview:self.inputAccessoryView]; | ||||
| } | ||||
|  | ||||
| - (void)layoutSubviews { | ||||
|     [super layoutSubviews]; | ||||
|      | ||||
|     // Layout top border | ||||
|     CGRect frame = _toolbarView.bounds; | ||||
|     frame.size.height = 0.5; | ||||
|     _topBorder.frame = frame; | ||||
|      | ||||
|     // Scroll view // | ||||
|      | ||||
|     frame = CGRectMake(0, 0, self.bounds.size.width, kToolbarHeight); | ||||
|     CGSize contentSize = self.scrollView.contentSize; | ||||
|     CGFloat scrollViewWidth = frame.size.width; | ||||
|      | ||||
|     // If our content size is smaller than the scroll view, | ||||
|     // we want to right-align all the content | ||||
|     if (contentSize.width < scrollViewWidth) { | ||||
|         // Compute the content size to scroll view size difference | ||||
|         UIEdgeInsets insets = self.scrollView.contentInset; | ||||
|         CGFloat margin = insets.left + insets.right; | ||||
|         CGFloat difference = scrollViewWidth - contentSize.width - margin; | ||||
|         // Update the content size to be the full width of the scroll view | ||||
|         contentSize.width += difference; | ||||
|         self.scrollView.contentSize = contentSize; | ||||
|          | ||||
|         // Offset every button by the difference above | ||||
|         // so that every button appears right-aligned | ||||
|         for (UIView *button in self.scrollView.subviews) { | ||||
|             CGRect f = button.frame; | ||||
|             f.origin.x += difference; | ||||
|             button.frame = f; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (UIView *)inputAccessoryView { | ||||
|     _topBorder       = [CALayer new]; | ||||
|     _topBorder.frame = CGRectMake(0.0, 0.0, self.bounds.size.width, 0.5); | ||||
|     [self makeScrollView]; | ||||
|      | ||||
|     UIColor *borderColor = nil, *backgroundColor = nil; | ||||
|     UIColor *lightColor = [UIColor colorWithHue:216.0/360.0 saturation:0.05 brightness:0.85 alpha:1]; | ||||
|     UIColor *darkColor = [UIColor colorWithHue:220.0/360.0 saturation:0.07 brightness:0.16 alpha:1]; | ||||
|      | ||||
|     switch (_appearance) { | ||||
|         case UIKeyboardAppearanceDefault: | ||||
|             if (@available(iOS 13, *)) { | ||||
|                 borderColor = UIColor.systemBackgroundColor; | ||||
|                  | ||||
|                 if (self.usingDarkMode) { | ||||
|                     // style = UIBlurEffectStyleSystemThickMaterial; | ||||
|                     backgroundColor = darkColor; | ||||
|                 } else { | ||||
|                     // style = UIBlurEffectStyleSystemUltraThinMaterialLight; | ||||
|                     backgroundColor = lightColor; | ||||
|                 } | ||||
|                 break; | ||||
|             } | ||||
|         case UIKeyboardAppearanceLight: { | ||||
|             borderColor = UIColor.clearColor; | ||||
|             backgroundColor = lightColor; | ||||
|             break; | ||||
|         } | ||||
|         case UIKeyboardAppearanceDark: { | ||||
|             borderColor = [UIColor colorWithWhite:0.100 alpha:1.000]; | ||||
|             backgroundColor = darkColor; | ||||
|             break; | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     self.toolbarView = [UIView new]; | ||||
|     [self.toolbarView addSubview:self.scrollView]; | ||||
|     [self.toolbarView.layer addSublayer:self.topBorder]; | ||||
|     self.toolbarView.frame = CGRectMake(0, 0, self.bounds.size.width, kToolbarHeight); | ||||
|     self.toolbarView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; | ||||
|      | ||||
|     self.backgroundColor = backgroundColor; | ||||
|     self.topBorder.backgroundColor = borderColor.CGColor; | ||||
|      | ||||
|     return self.toolbarView; | ||||
| } | ||||
|  | ||||
| - (UIScrollView *)makeScrollView { | ||||
|     UIScrollView *scrollView = [UIScrollView new]; | ||||
|     scrollView.backgroundColor  = UIColor.clearColor; | ||||
|     scrollView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; | ||||
|     scrollView.contentInset     = UIEdgeInsetsMake( | ||||
|         8.f, kScrollViewHorizontalMargins, 4.f, kScrollViewHorizontalMargins | ||||
|     ); | ||||
|     scrollView.showsHorizontalScrollIndicator = NO; | ||||
|      | ||||
|     self.scrollView = scrollView; | ||||
|     [self addButtons]; | ||||
|      | ||||
|     return scrollView; | ||||
| } | ||||
|  | ||||
| - (void)addButtons { | ||||
|     NSUInteger originX = 0.f; | ||||
|      | ||||
|     CGRect originFrame; | ||||
|     CGFloat top    = self.scrollView.contentInset.top; | ||||
|     CGFloat bottom = self.scrollView.contentInset.bottom; | ||||
|      | ||||
|     for (FLEXKBToolbarButton *button in self.buttons) { | ||||
|         button.appearance = self.appearance; | ||||
|          | ||||
|         originFrame             = button.frame; | ||||
|         originFrame.origin.x    = originX; | ||||
|         originFrame.origin.y    = 0.f; | ||||
|         originFrame.size.height = kToolbarHeight - (top + bottom); | ||||
|         button.frame            = originFrame; | ||||
|          | ||||
|         [self.scrollView addSubview:button]; | ||||
|          | ||||
|         // originX tracks the origin of the next button to be added, | ||||
|         // so at the end of each iteration of this loop we increment | ||||
|         // it by the size of the last button with some padding | ||||
|         originX += button.bounds.size.width + kButtonSpacing; | ||||
|     } | ||||
|      | ||||
|     // Update contentSize, | ||||
|     // set to the max x value of the last button added | ||||
|     CGSize contentSize = self.scrollView.contentSize; | ||||
|     contentSize.width  = originX - kButtonSpacing; | ||||
|     self.scrollView.contentSize = contentSize; | ||||
|      | ||||
|     // Needed to potentially right-align buttons | ||||
|     [self setNeedsLayout]; | ||||
| } | ||||
|  | ||||
| - (void)setButtons:(NSArray<FLEXKBToolbarButton *> *)buttons { | ||||
|     [_buttons makeObjectsPerformSelector:@selector(removeFromSuperview)]; | ||||
|     _buttons = buttons.copy; | ||||
|      | ||||
|     [self addButtons]; | ||||
| } | ||||
|  | ||||
| - (BOOL)useSystemAppearance { | ||||
|     return self.appearance == UIKeyboardAppearanceDefault; | ||||
| } | ||||
|  | ||||
| - (BOOL)usingDarkMode { | ||||
|     if (@available(iOS 12, *)) { | ||||
|         return self.useSystemAppearance && self.traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark; | ||||
|     } | ||||
|      | ||||
|     return self.appearance == UIKeyboardAppearanceDark; | ||||
| } | ||||
|  | ||||
| - (void)traitCollectionDidChange:(UITraitCollection *)previous { | ||||
|     if (@available(iOS 12, *)) { | ||||
|         // Was darkmode toggled? | ||||
|         if (previous.userInterfaceStyle != self.traitCollection.userInterfaceStyle) { | ||||
|             if (self.useSystemAppearance) { | ||||
|                 // Recreate the background view with the proper colors | ||||
|                 self.appearance = self.appearance; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,14 @@ | ||||
| // | ||||
| //  FLEXObjcRuntimeViewController.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 3/23/17. | ||||
| //  Copyright © 2017 Tanner Bennett. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXTableViewController.h" | ||||
| #import "FLEXGlobalsEntry.h" | ||||
|  | ||||
| @interface FLEXObjcRuntimeViewController : FLEXTableViewController <FLEXGlobalsEntry> | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,178 @@ | ||||
| // | ||||
| //  FLEXObjcRuntimeViewController.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 3/23/17. | ||||
| //  Copyright © 2017 Tanner Bennett. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXObjcRuntimeViewController.h" | ||||
| #import "FLEXKeyPathSearchController.h" | ||||
| #import "FLEXRuntimeBrowserToolbar.h" | ||||
| #import "UIGestureRecognizer+Blocks.h" | ||||
| #import "UIBarButtonItem+FLEX.h" | ||||
| #import "FLEXTableView.h" | ||||
| #import "FLEXObjectExplorerFactory.h" | ||||
| #import "FLEXAlert.h" | ||||
| #import "FLEXRuntimeClient.h" | ||||
| #import <dlfcn.h> | ||||
|  | ||||
| @interface FLEXObjcRuntimeViewController () <FLEXKeyPathSearchControllerDelegate> | ||||
|  | ||||
| @property (nonatomic, readonly ) FLEXKeyPathSearchController *keyPathController; | ||||
| @property (nonatomic, readonly ) UIView *promptView; | ||||
|  | ||||
| @end | ||||
|  | ||||
| @implementation FLEXObjcRuntimeViewController | ||||
|  | ||||
| #pragma mark - Setup, view events | ||||
|  | ||||
| - (void)viewDidLoad { | ||||
|     [super viewDidLoad]; | ||||
|      | ||||
|     // Long press on navigation bar to initialize webkit legacy | ||||
|     // | ||||
|     // We call initializeWebKitLegacy automatically before you search | ||||
|     // all bundles just to be safe (since touching some classes before | ||||
|     // WebKit is initialized will initialize it on a thread other than | ||||
|     // the main thread), but sometimes you can encounter this crash | ||||
|     // without searching through all bundles, of course. | ||||
|     [self.navigationController.navigationBar addGestureRecognizer:[ | ||||
|         [UILongPressGestureRecognizer alloc] | ||||
|             initWithTarget:[FLEXRuntimeClient class] | ||||
|             action:@selector(initializeWebKitLegacy) | ||||
|         ] | ||||
|     ]; | ||||
|      | ||||
|     [self addToolbarItems:@[FLEXBarButtonItem(@"dlopen()", self, @selector(dlopenPressed:))]]; | ||||
|      | ||||
|     // Search bar stuff, must be first because this creates self.searchController | ||||
|     self.showsSearchBar = YES; | ||||
|     self.showSearchBarInitially = YES; | ||||
|     self.activatesSearchBarAutomatically = YES; | ||||
|     // Using pinSearchBar on this screen causes a weird visual | ||||
|     // thing on the next view controller that gets pushed. | ||||
|     // | ||||
|     // self.pinSearchBar = YES; | ||||
|     self.searchController.searchBar.placeholder = @"UIKit*.UIView.-setFrame:"; | ||||
|  | ||||
|     // Search controller stuff | ||||
|     // key path controller automatically assigns itself as the delegate of the search bar | ||||
|     // To avoid a retain cycle below, use local variables | ||||
|     UISearchBar *searchBar = self.searchController.searchBar; | ||||
|     FLEXKeyPathSearchController *keyPathController = [FLEXKeyPathSearchController delegate:self]; | ||||
|     _keyPathController = keyPathController; | ||||
|     _keyPathController.toolbar = [FLEXRuntimeBrowserToolbar toolbarWithHandler:^(NSString *text, BOOL suggestion) { | ||||
|         if (suggestion) { | ||||
|             [keyPathController didSelectKeyPathOption:text]; | ||||
|         } else { | ||||
|             [keyPathController didPressButton:text insertInto:searchBar]; | ||||
|         } | ||||
|     } suggestions:keyPathController.suggestions]; | ||||
| } | ||||
|  | ||||
| - (void)viewWillAppear:(BOOL)animated { | ||||
|     [super viewWillAppear:animated]; | ||||
|     [self.tableView deselectRowAtIndexPath:self.tableView.indexPathForSelectedRow animated:YES]; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark dlopen | ||||
|  | ||||
| /// Prompt user for dlopen shortcuts to choose from | ||||
| - (void)dlopenPressed:(id)sender { | ||||
|     [FLEXAlert makeAlert:^(FLEXAlert *make) { | ||||
|         make.title(@"Dynamically Open Library"); | ||||
|         make.message(@"Invoke dlopen() with the given path. Choose an option below."); | ||||
|          | ||||
|         make.button(@"System Framework").handler(^(NSArray<NSString *> *_) { | ||||
|             [self dlopenWithFormat:@"/System/Library/Frameworks/%@.framework/%@"]; | ||||
|         }); | ||||
|         make.button(@"System Private Framework").handler(^(NSArray<NSString *> *_) { | ||||
|             [self dlopenWithFormat:@"/System/Library/PrivateFrameworks/%@.framework/%@"]; | ||||
|         }); | ||||
|         make.button(@"Arbitrary Binary").handler(^(NSArray<NSString *> *_) { | ||||
|             [self dlopenWithFormat:nil]; | ||||
|         }); | ||||
|          | ||||
|         make.button(@"Cancel").cancelStyle(); | ||||
|     } showFrom:self]; | ||||
| } | ||||
|  | ||||
| /// Prompt user for input and dlopen | ||||
| - (void)dlopenWithFormat:(NSString *)format { | ||||
|     [FLEXAlert makeAlert:^(FLEXAlert *make) { | ||||
|         make.title(@"Dynamically Open Library"); | ||||
|         if (format) { | ||||
|             make.message(@"Pass in a framework name, such as CarKit or FrontBoard."); | ||||
|         } else { | ||||
|             make.message(@"Pass in an absolute path to a binary."); | ||||
|         } | ||||
|          | ||||
|         make.textField(format ? @"ARKit" : @"/System/Library/Frameworks/ARKit.framework/ARKit"); | ||||
|          | ||||
|         make.button(@"Cancel").cancelStyle(); | ||||
|         make.button(@"Open").destructiveStyle().handler(^(NSArray<NSString *> *strings) { | ||||
|             NSString *path = strings[0]; | ||||
|              | ||||
|             if (path.length < 2) { | ||||
|                 [self dlopenInvalidPath]; | ||||
|             } else if (format) { | ||||
|                 path = [NSString stringWithFormat:format, path, path]; | ||||
|             } | ||||
|              | ||||
|             dlopen(path.UTF8String, RTLD_NOW); | ||||
|         }); | ||||
|     } showFrom:self]; | ||||
| } | ||||
|  | ||||
| - (void)dlopenInvalidPath { | ||||
|     [FLEXAlert makeAlert:^(FLEXAlert * _Nonnull make) { | ||||
|         make.title(@"Path or Name Too Short"); | ||||
|         make.button(@"Dismiss").cancelStyle(); | ||||
|     } showFrom:self]; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark Delegate stuff | ||||
|  | ||||
| - (void)didSelectImagePath:(NSString *)path shortName:(NSString *)shortName { | ||||
|     [FLEXAlert makeAlert:^(FLEXAlert *make) { | ||||
|         make.title(shortName); | ||||
|         make.message(@"No NSBundle associated with this path:\n\n"); | ||||
|         make.message(path); | ||||
|  | ||||
|         make.button(@"Copy Path").handler(^(NSArray<NSString *> *strings) { | ||||
|             UIPasteboard.generalPasteboard.string = path; | ||||
|         }); | ||||
|         make.button(@"Dismiss").cancelStyle(); | ||||
|     } showFrom:self]; | ||||
| } | ||||
|  | ||||
| - (void)didSelectBundle:(NSBundle *)bundle { | ||||
|     NSParameterAssert(bundle); | ||||
|     FLEXObjectExplorerViewController *explorer = [FLEXObjectExplorerFactory explorerViewControllerForObject:bundle]; | ||||
|     [self.navigationController pushViewController:explorer animated:YES]; | ||||
| } | ||||
|  | ||||
| - (void)didSelectClass:(Class)cls { | ||||
|     NSParameterAssert(cls); | ||||
|     FLEXObjectExplorerViewController *explorer = [FLEXObjectExplorerFactory explorerViewControllerForObject:cls]; | ||||
|     [self.navigationController pushViewController:explorer animated:YES]; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - FLEXGlobalsEntry | ||||
|  | ||||
| + (NSString *)globalsEntryTitle:(FLEXGlobalsRow)row { | ||||
|     return @"📚  Runtime Browser"; | ||||
| } | ||||
|  | ||||
| + (UIViewController *)globalsEntryViewController:(FLEXGlobalsRow)row { | ||||
|     UIViewController *controller = [self new]; | ||||
|     controller.title = [self globalsEntryTitle:row]; | ||||
|     return controller; | ||||
| } | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,18 @@ | ||||
| // | ||||
| //  FLEXRuntimeBrowserToolbar.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 6/11/17. | ||||
| //  Copyright © 2017 Tanner Bennett. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXKeyboardToolbar.h" | ||||
| #import "FLEXRuntimeKeyPath.h" | ||||
|  | ||||
| @interface FLEXRuntimeBrowserToolbar : FLEXKeyboardToolbar | ||||
|  | ||||
| + (instancetype)toolbarWithHandler:(FLEXKBToolbarAction)tapHandler suggestions:(NSArray<NSString *> *)suggestions; | ||||
|  | ||||
| - (void)setKeyPath:(FLEXRuntimeKeyPath *)keyPath suggestions:(NSArray<NSString *> *)suggestions; | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,92 @@ | ||||
| // | ||||
| //  FLEXRuntimeBrowserToolbar.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 6/11/17. | ||||
| //  Copyright © 2017 Tanner Bennett. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXRuntimeBrowserToolbar.h" | ||||
| #import "FLEXRuntimeKeyPathTokenizer.h" | ||||
|  | ||||
| @interface FLEXRuntimeBrowserToolbar () | ||||
| @property (nonatomic, copy) FLEXKBToolbarAction tapHandler; | ||||
| @end | ||||
|  | ||||
| @implementation FLEXRuntimeBrowserToolbar | ||||
|  | ||||
| + (instancetype)toolbarWithHandler:(FLEXKBToolbarAction)tapHandler suggestions:(NSArray<NSString *> *)suggestions { | ||||
|     NSArray *buttons = [self | ||||
|         buttonsForKeyPath:FLEXRuntimeKeyPath.empty suggestions:suggestions handler:tapHandler | ||||
|     ]; | ||||
|  | ||||
|     FLEXRuntimeBrowserToolbar *me = [self toolbarWithButtons:buttons]; | ||||
|     me.tapHandler = tapHandler; | ||||
|     return me; | ||||
| } | ||||
|  | ||||
| + (NSArray<FLEXKBToolbarButton*> *)buttonsForKeyPath:(FLEXRuntimeKeyPath *)keyPath | ||||
|                                      suggestions:(NSArray<NSString *> *)suggestions | ||||
|                                          handler:(FLEXKBToolbarAction)handler { | ||||
|     NSMutableArray *buttons = [NSMutableArray new]; | ||||
|     FLEXSearchToken *lastKey = nil; | ||||
|     BOOL lastKeyIsMethod = NO; | ||||
|  | ||||
|     if (keyPath.methodKey) { | ||||
|         lastKey = keyPath.methodKey; | ||||
|         lastKeyIsMethod = YES; | ||||
|     } else { | ||||
|         lastKey = keyPath.classKey ?: keyPath.bundleKey; | ||||
|     } | ||||
|  | ||||
|     switch (lastKey.options) { | ||||
|         case TBWildcardOptionsNone: | ||||
|         case TBWildcardOptionsAny: | ||||
|             if (lastKeyIsMethod) { | ||||
|                 if (!keyPath.instanceMethods) { | ||||
|                     [buttons addObject:[FLEXKBToolbarButton buttonWithTitle:@"-" action:handler]]; | ||||
|                     [buttons addObject:[FLEXKBToolbarButton buttonWithTitle:@"+" action:handler]]; | ||||
|                 } | ||||
|                 [buttons addObject:[FLEXKBToolbarButton buttonWithTitle:@"*" action:handler]]; | ||||
|             } else { | ||||
|                 [buttons addObject:[FLEXKBToolbarButton buttonWithTitle:@"*" action:handler]]; | ||||
|                 [buttons addObject:[FLEXKBToolbarButton buttonWithTitle:@"*." action:handler]]; | ||||
|             } | ||||
|             break; | ||||
|  | ||||
|         default: { | ||||
|             if (lastKey.options & TBWildcardOptionsPrefix) { | ||||
|                 if (lastKeyIsMethod) { | ||||
|                     if (lastKey.string.length) { | ||||
|                         [buttons addObject:[FLEXKBToolbarButton buttonWithTitle:@"*" action:handler]]; | ||||
|                     } | ||||
|                 } else { | ||||
|                     if (lastKey.string.length) { | ||||
|                         [buttons addObject:[FLEXKBToolbarButton buttonWithTitle:@"*." action:handler]]; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             else if (lastKey.options & TBWildcardOptionsSuffix) { | ||||
|                 if (!lastKeyIsMethod) { | ||||
|                     [buttons addObject:[FLEXKBToolbarButton buttonWithTitle:@"*" action:handler]]; | ||||
|                     [buttons addObject:[FLEXKBToolbarButton buttonWithTitle:@"*." action:handler]]; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     for (NSString *suggestion in suggestions) { | ||||
|         [buttons addObject:[FLEXKBToolbarSuggestedButton buttonWithTitle:suggestion action:handler]]; | ||||
|     } | ||||
|  | ||||
|     return buttons; | ||||
| } | ||||
|  | ||||
| - (void)setKeyPath:(FLEXRuntimeKeyPath *)keyPath suggestions:(NSArray<NSString *> *)suggestions { | ||||
|     self.buttons = [self.class | ||||
|         buttonsForKeyPath:keyPath suggestions:suggestions handler:self.tapHandler | ||||
|     ]; | ||||
| } | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,43 @@ | ||||
| // | ||||
| //  FLEXRuntimeKeyPath.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 3/22/17. | ||||
| //  Copyright © 2017 Tanner Bennett. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXSearchToken.h" | ||||
| @class FLEXMethod; | ||||
|  | ||||
| NS_ASSUME_NONNULL_BEGIN | ||||
|  | ||||
| /// A key path represents a query into a set of bundles or classes | ||||
| /// for a set of one or more methods. It is composed of three tokens: | ||||
| /// bundle, class, and method. A key path may be incomplete if it | ||||
| /// is missing any of the tokens. A key path is considered "absolute" | ||||
| /// if all tokens have no options and if methodKey.string begins | ||||
| /// with a + or a -. | ||||
| /// | ||||
| /// The @code TBKeyPathTokenizer @endcode class is used to create | ||||
| /// a key path from a string. | ||||
| @interface FLEXRuntimeKeyPath : NSObject | ||||
|  | ||||
| + (instancetype)empty; | ||||
|  | ||||
| /// @param method must start with either a wildcard or a + or -. | ||||
| + (instancetype)bundle:(FLEXSearchToken *)bundle | ||||
|                  class:(FLEXSearchToken *)cls | ||||
|                 method:(FLEXSearchToken *)method | ||||
|             isInstance:(NSNumber *)instance | ||||
|                 string:(NSString *)keyPathString; | ||||
|  | ||||
| @property (nonatomic, nullable, readonly) FLEXSearchToken *bundleKey; | ||||
| @property (nonatomic, nullable, readonly) FLEXSearchToken *classKey; | ||||
| @property (nonatomic, nullable, readonly) FLEXSearchToken *methodKey; | ||||
|  | ||||
| /// Indicates whether the method token specifies instance methods. | ||||
| /// Nil if not specified. | ||||
| @property (nonatomic, nullable, readonly) NSNumber *instanceMethods; | ||||
|  | ||||
| @end | ||||
| NS_ASSUME_NONNULL_END | ||||
| @@ -0,0 +1,75 @@ | ||||
| // | ||||
| //  FLEXRuntimeKeyPath.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 3/22/17. | ||||
| //  Copyright © 2017 Tanner Bennett. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXRuntimeKeyPath.h" | ||||
| #import "FLEXRuntimeClient.h" | ||||
|  | ||||
| @interface FLEXRuntimeKeyPath () { | ||||
|     NSString *flex_description; | ||||
| } | ||||
| @end | ||||
|  | ||||
| @implementation FLEXRuntimeKeyPath | ||||
|  | ||||
| + (instancetype)empty { | ||||
|     static FLEXRuntimeKeyPath *empty = nil; | ||||
|     static dispatch_once_t onceToken; | ||||
|     dispatch_once(&onceToken, ^{ | ||||
|         FLEXSearchToken *any = FLEXSearchToken.any; | ||||
|  | ||||
|         empty = [self new]; | ||||
|         empty->_bundleKey = any; | ||||
|         empty->flex_description = @""; | ||||
|     }); | ||||
|  | ||||
|     return empty; | ||||
| } | ||||
|  | ||||
| + (instancetype)bundle:(FLEXSearchToken *)bundle | ||||
|                  class:(FLEXSearchToken *)cls | ||||
|                 method:(FLEXSearchToken *)method | ||||
|             isInstance:(NSNumber *)instance | ||||
|                 string:(NSString *)keyPathString { | ||||
|     FLEXRuntimeKeyPath *keyPath  = [self new]; | ||||
|     keyPath->_bundleKey = bundle; | ||||
|     keyPath->_classKey  = cls; | ||||
|     keyPath->_methodKey = method; | ||||
|  | ||||
|     keyPath->_instanceMethods = instance; | ||||
|  | ||||
|     // Remove irrelevant trailing '*' for equality purposes | ||||
|     if ([keyPathString hasSuffix:@"*"]) { | ||||
|         keyPathString = [keyPathString substringToIndex:keyPathString.length]; | ||||
|     } | ||||
|     keyPath->flex_description = keyPathString; | ||||
|      | ||||
|     if (bundle.isAny && cls.isAny && method.isAny) { | ||||
|         [FLEXRuntimeClient initializeWebKitLegacy]; | ||||
|     } | ||||
|  | ||||
|     return keyPath; | ||||
| } | ||||
|  | ||||
| - (NSString *)description { | ||||
|     return flex_description; | ||||
| } | ||||
|  | ||||
| - (NSUInteger)hash { | ||||
|     return flex_description.hash; | ||||
| } | ||||
|  | ||||
| - (BOOL)isEqual:(id)object { | ||||
|     if ([object isKindOfClass:[FLEXRuntimeKeyPath class]]) { | ||||
|         FLEXRuntimeKeyPath *kp = object; | ||||
|         return [flex_description isEqualToString:kp->flex_description]; | ||||
|     } | ||||
|  | ||||
|     return NO; | ||||
| } | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,18 @@ | ||||
| // | ||||
| //  FLEXRuntimeKeyPathTokenizer.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 3/22/17. | ||||
| //  Copyright © 2017 Tanner Bennett. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXRuntimeKeyPath.h" | ||||
|  | ||||
| @interface FLEXRuntimeKeyPathTokenizer : NSObject | ||||
|  | ||||
| + (NSUInteger)tokenCountOfString:(NSString *)userInput; | ||||
| + (FLEXRuntimeKeyPath *)tokenizeString:(NSString *)userInput; | ||||
|  | ||||
| + (BOOL)allowedInKeyPath:(NSString *)text; | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,218 @@ | ||||
| // | ||||
| //  FLEXRuntimeKeyPathTokenizer.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 3/22/17. | ||||
| //  Copyright © 2017 Tanner Bennett. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXRuntimeKeyPathTokenizer.h" | ||||
|  | ||||
| #define TBCountOfStringOccurence(target, str) ([target componentsSeparatedByString:str].count - 1) | ||||
|  | ||||
| @implementation FLEXRuntimeKeyPathTokenizer | ||||
|  | ||||
| #pragma mark Initialization | ||||
|  | ||||
| static NSCharacterSet *firstAllowed      = nil; | ||||
| static NSCharacterSet *identifierAllowed = nil; | ||||
| static NSCharacterSet *filenameAllowed   = nil; | ||||
| static NSCharacterSet *keyPathDisallowed = nil; | ||||
| static NSCharacterSet *methodAllowed     = nil; | ||||
| + (void)initialize { | ||||
|     if (self == [self class]) { | ||||
|         NSString *_methodFirstAllowed    = @"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_$"; | ||||
|         NSString *_identifierAllowed     = [_methodFirstAllowed stringByAppendingString:@"1234567890"]; | ||||
|         NSString *_methodAllowedSansType = [_identifierAllowed stringByAppendingString:@":"]; | ||||
|         NSString *_filenameNameAllowed   = [_identifierAllowed stringByAppendingString:@"-+?!"]; | ||||
|         firstAllowed      = [NSCharacterSet characterSetWithCharactersInString:_methodFirstAllowed]; | ||||
|         identifierAllowed = [NSCharacterSet characterSetWithCharactersInString:_identifierAllowed]; | ||||
|         filenameAllowed   = [NSCharacterSet characterSetWithCharactersInString:_filenameNameAllowed]; | ||||
|         methodAllowed     = [NSCharacterSet characterSetWithCharactersInString:_methodAllowedSansType]; | ||||
|  | ||||
|         NSString *_kpDisallowed = [_identifierAllowed stringByAppendingString:@"-+:\\.*"]; | ||||
|         keyPathDisallowed = [NSCharacterSet characterSetWithCharactersInString:_kpDisallowed].invertedSet; | ||||
|     } | ||||
| } | ||||
|  | ||||
| #pragma mark Public | ||||
|  | ||||
| + (FLEXRuntimeKeyPath *)tokenizeString:(NSString *)userInput { | ||||
|     if (!userInput.length) { | ||||
|         return nil; | ||||
|     } | ||||
|  | ||||
|     NSUInteger tokens = [self tokenCountOfString:userInput]; | ||||
|     if (tokens == 0) { | ||||
|         return nil; | ||||
|     } | ||||
|  | ||||
|     if ([userInput containsString:@"**"]) { | ||||
|         @throw NSInternalInconsistencyException; | ||||
|     } | ||||
|  | ||||
|     NSNumber *instance = nil; | ||||
|     NSScanner *scanner = [NSScanner scannerWithString:userInput]; | ||||
|     FLEXSearchToken *bundle    = [self scanToken:scanner allowed:filenameAllowed first:filenameAllowed]; | ||||
|     FLEXSearchToken *cls       = [self scanToken:scanner allowed:identifierAllowed first:firstAllowed]; | ||||
|     FLEXSearchToken *method    = tokens > 2 ? [self scanMethodToken:scanner instance:&instance] : nil; | ||||
|  | ||||
|     return [FLEXRuntimeKeyPath bundle:bundle | ||||
|                        class:cls | ||||
|                       method:method | ||||
|                   isInstance:instance | ||||
|                       string:userInput]; | ||||
| } | ||||
|  | ||||
| + (BOOL)allowedInKeyPath:(NSString *)text { | ||||
|     if (!text.length) { | ||||
|         return YES; | ||||
|     } | ||||
|      | ||||
|     return [text rangeOfCharacterFromSet:keyPathDisallowed].location == NSNotFound; | ||||
| } | ||||
|  | ||||
| #pragma mark Private | ||||
|  | ||||
| + (NSUInteger)tokenCountOfString:(NSString *)userInput { | ||||
|     NSUInteger escapedCount = TBCountOfStringOccurence(userInput, @"\\."); | ||||
|     NSUInteger tokenCount  = TBCountOfStringOccurence(userInput, @".") - escapedCount + 1; | ||||
|  | ||||
|     return tokenCount; | ||||
| } | ||||
|  | ||||
| + (FLEXSearchToken *)scanToken:(NSScanner *)scanner allowed:(NSCharacterSet *)allowedChars first:(NSCharacterSet *)first { | ||||
|     if (scanner.isAtEnd) { | ||||
|         if ([scanner.string hasSuffix:@"."] && ![scanner.string hasSuffix:@"\\."]) { | ||||
|             return [FLEXSearchToken string:nil options:TBWildcardOptionsAny]; | ||||
|         } | ||||
|         return nil; | ||||
|     } | ||||
|  | ||||
|     TBWildcardOptions options = TBWildcardOptionsNone; | ||||
|     NSMutableString *token = [NSMutableString new]; | ||||
|  | ||||
|     // Token cannot start with '.' | ||||
|     if ([scanner scanString:@"." intoString:nil]) { | ||||
|         @throw NSInternalInconsistencyException; | ||||
|     } | ||||
|  | ||||
|     if ([scanner scanString:@"*." intoString:nil]) { | ||||
|         return [FLEXSearchToken string:nil options:TBWildcardOptionsAny]; | ||||
|     } else if ([scanner scanString:@"*" intoString:nil]) { | ||||
|         if (scanner.isAtEnd) { | ||||
|             return FLEXSearchToken.any; | ||||
|         } | ||||
|          | ||||
|         options |= TBWildcardOptionsPrefix; | ||||
|     } | ||||
|  | ||||
|     NSString *tmp = nil; | ||||
|     BOOL stop = NO, didScanDelimiter = NO, didScanFirstAllowed = NO; | ||||
|     NSCharacterSet *disallowed = allowedChars.invertedSet; | ||||
|     while (!stop && ![scanner scanString:@"." intoString:&tmp] && !scanner.isAtEnd) { | ||||
|         // Scan word chars | ||||
|         // In this block, we have not scanned anything yet, except maybe leading '\' or '\.' | ||||
|         if (!didScanFirstAllowed) { | ||||
|             if ([scanner scanCharactersFromSet:first intoString:&tmp]) { | ||||
|                 [token appendString:tmp]; | ||||
|                 didScanFirstAllowed = YES; | ||||
|             } else if ([scanner scanString:@"\\" intoString:nil]) { | ||||
|                 if (options == TBWildcardOptionsPrefix && [scanner scanString:@"." intoString:nil]) { | ||||
|                     [token appendString:@"."]; | ||||
|                 } else if (scanner.isAtEnd && options == TBWildcardOptionsPrefix) { | ||||
|                     // Only allow standalone '\' if prefixed by '*' | ||||
|                     return FLEXSearchToken.any; | ||||
|                 } else { | ||||
|                     // Token starts with a number, period, or something else not allowed, | ||||
|                     // or token is a standalone '\' with no '*' prefix | ||||
|                     @throw NSInternalInconsistencyException; | ||||
|                 } | ||||
|             } else { | ||||
|                 // Token starts with a number, period, or something else not allowed | ||||
|                 @throw NSInternalInconsistencyException; | ||||
|             } | ||||
|         } else if ([scanner scanCharactersFromSet:allowedChars intoString:&tmp]) { | ||||
|             [token appendString:tmp]; | ||||
|         } | ||||
|         // Scan '\.' or trailing '\' | ||||
|         else if ([scanner scanString:@"\\" intoString:nil]) { | ||||
|             if ([scanner scanString:@"." intoString:nil]) { | ||||
|                 [token appendString:@"."]; | ||||
|             } else if (scanner.isAtEnd) { | ||||
|                 // Ignore forward slash not followed by period if at end | ||||
|                 return [FLEXSearchToken string:token options:options | TBWildcardOptionsSuffix]; | ||||
|             } else { | ||||
|                 // Only periods can follow a forward slash | ||||
|                 @throw NSInternalInconsistencyException; | ||||
|             } | ||||
|         } | ||||
|         // Scan '*.' | ||||
|         else if ([scanner scanString:@"*." intoString:nil]) { | ||||
|             options |= TBWildcardOptionsSuffix; | ||||
|             stop = YES; | ||||
|             didScanDelimiter = YES; | ||||
|         } | ||||
|         // Scan '*' not followed by . | ||||
|         else if ([scanner scanString:@"*" intoString:nil]) { | ||||
|             if (!scanner.isAtEnd) { | ||||
|                 // Invalid token, wildcard in middle of token | ||||
|                 @throw NSInternalInconsistencyException; | ||||
|             } | ||||
|         } else if ([scanner scanCharactersFromSet:disallowed intoString:nil]) { | ||||
|             // Invalid token, invalid characters | ||||
|             @throw NSInternalInconsistencyException; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // Did we scan a trailing, un-escsaped '.'? | ||||
|     if ([tmp isEqualToString:@"."]) { | ||||
|         didScanDelimiter = YES; | ||||
|     } | ||||
|  | ||||
|     if (!didScanDelimiter) { | ||||
|         options |= TBWildcardOptionsSuffix; | ||||
|     } | ||||
|  | ||||
|     return [FLEXSearchToken string:token options:options]; | ||||
| } | ||||
|  | ||||
| + (FLEXSearchToken *)scanMethodToken:(NSScanner *)scanner instance:(NSNumber **)instance { | ||||
|     if (scanner.isAtEnd) { | ||||
|         if ([scanner.string hasSuffix:@"."]) { | ||||
|             return [FLEXSearchToken string:nil options:TBWildcardOptionsAny]; | ||||
|         } | ||||
|         return nil; | ||||
|     } | ||||
|  | ||||
|     if ([scanner.string hasSuffix:@"."] && ![scanner.string hasSuffix:@"\\."]) { | ||||
|         // Methods cannot end with '.' except for '\.' | ||||
|         @throw NSInternalInconsistencyException; | ||||
|     } | ||||
|      | ||||
|     if ([scanner scanString:@"-" intoString:nil]) { | ||||
|         *instance = @YES; | ||||
|     } else if ([scanner scanString:@"+" intoString:nil]) { | ||||
|         *instance = @NO; | ||||
|     } else { | ||||
|         if ([scanner scanString:@"*" intoString:nil]) { | ||||
|             // Just checking... It has to start with one of these three! | ||||
|             scanner.scanLocation--; | ||||
|         } else { | ||||
|             @throw NSInternalInconsistencyException; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // -*foo not allowed | ||||
|     if (*instance && [scanner scanString:@"*" intoString:nil]) { | ||||
|         @throw NSInternalInconsistencyException; | ||||
|     } | ||||
|  | ||||
|     if (scanner.isAtEnd) { | ||||
|         return [FLEXSearchToken string:@"" options:TBWildcardOptionsSuffix]; | ||||
|     } | ||||
|  | ||||
|     return [self scanToken:scanner allowed:methodAllowed first:firstAllowed]; | ||||
| } | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,35 @@ | ||||
| // | ||||
| //  FLEXSearchToken.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 3/22/17. | ||||
| //  Copyright © 2017 Tanner Bennett. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import <Foundation/Foundation.h> | ||||
|  | ||||
| typedef NS_OPTIONS(NSUInteger, TBWildcardOptions) { | ||||
|     TBWildcardOptionsNone   = 0, | ||||
|     TBWildcardOptionsAny    = 1, | ||||
|     TBWildcardOptionsPrefix = 1 << 1, | ||||
|     TBWildcardOptionsSuffix = 1 << 2, | ||||
| }; | ||||
|  | ||||
| /// A token may contain wildcards at one or either end, | ||||
| /// but not in the middle of the token (as of now). | ||||
| @interface FLEXSearchToken : NSObject | ||||
|  | ||||
| + (instancetype)any; | ||||
| + (instancetype)string:(NSString *)string options:(TBWildcardOptions)options; | ||||
|  | ||||
| /// Will not contain the wildcard (*) symbol | ||||
| @property (nonatomic, readonly) NSString *string; | ||||
| @property (nonatomic, readonly) TBWildcardOptions options; | ||||
|  | ||||
| /// Opposite of "is ambiguous" | ||||
| @property (nonatomic, readonly) BOOL isAbsolute; | ||||
| @property (nonatomic, readonly) BOOL isAny; | ||||
| /// Still \c isAny, but checks that the string is empty | ||||
| @property (nonatomic, readonly) BOOL isEmpty; | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,88 @@ | ||||
| // | ||||
| //  FLEXSearchToken.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 3/22/17. | ||||
| //  Copyright © 2017 Tanner Bennett. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXSearchToken.h" | ||||
|  | ||||
| @interface FLEXSearchToken () { | ||||
|     NSString *flex_description; | ||||
| } | ||||
| @end | ||||
|  | ||||
| @implementation FLEXSearchToken | ||||
|  | ||||
| + (instancetype)any { | ||||
|     static FLEXSearchToken *any = nil; | ||||
|     static dispatch_once_t onceToken; | ||||
|     dispatch_once(&onceToken, ^{ | ||||
|         any = [self string:nil options:TBWildcardOptionsAny]; | ||||
|     }); | ||||
|  | ||||
|     return any; | ||||
| } | ||||
|  | ||||
| + (instancetype)string:(NSString *)string options:(TBWildcardOptions)options { | ||||
|     FLEXSearchToken *token  = [self new]; | ||||
|     token->_string  = string; | ||||
|     token->_options = options; | ||||
|     return token; | ||||
| } | ||||
|  | ||||
| - (BOOL)isAbsolute { | ||||
|     return _options == TBWildcardOptionsNone; | ||||
| } | ||||
|  | ||||
| - (BOOL)isAny { | ||||
|     return _options == TBWildcardOptionsAny; | ||||
| } | ||||
|  | ||||
| - (BOOL)isEmpty { | ||||
|     return self.isAny && self.string.length == 0; | ||||
| } | ||||
|  | ||||
| - (NSString *)description { | ||||
|     if (flex_description) { | ||||
|         return flex_description; | ||||
|     } | ||||
|  | ||||
|     switch (_options) { | ||||
|         case TBWildcardOptionsNone: | ||||
|             flex_description = _string; | ||||
|             break; | ||||
|         case TBWildcardOptionsAny: | ||||
|             flex_description = @"*"; | ||||
|             break; | ||||
|         default: { | ||||
|             NSMutableString *desc = [NSMutableString new]; | ||||
|             if (_options & TBWildcardOptionsPrefix) { | ||||
|                 [desc appendString:@"*"]; | ||||
|             } | ||||
|             [desc appendString:_string]; | ||||
|             if (_options & TBWildcardOptionsSuffix) { | ||||
|                 [desc appendString:@"*"]; | ||||
|             } | ||||
|             flex_description = desc; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     return flex_description; | ||||
| } | ||||
|  | ||||
| - (NSUInteger)hash { | ||||
|     return self.description.hash; | ||||
| } | ||||
|  | ||||
| - (BOOL)isEqual:(id)object { | ||||
|     if ([object isKindOfClass:[FLEXSearchToken class]]) { | ||||
|         FLEXSearchToken *token = object; | ||||
|         return [_string isEqualToString:token->_string] && _options == token->_options; | ||||
|     } | ||||
|  | ||||
|     return NO; | ||||
| } | ||||
|  | ||||
| @end | ||||
		Reference in New Issue
	
	Block a user
	 Balackburn
					Balackburn