mirror of
https://github.com/SoPat712/YTLitePlus.git
synced 2025-08-25 03:48:52 -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