Просмотр исходного кода

Add FLEXRuntimeExporter

Allows you to export the contents of a bundle's objc metadata as an SQLite database

fix

fix
Tanner Bennett лет назад: 6
Родитель
Сommit
1a64da70c9

+ 29 - 0
Classes/GlobalStateExplorers/RuntimeBrowser/DataSources/FLEXRuntimeExporter.h

@@ -0,0 +1,29 @@
+//
+//  FLEXRuntimeExporter.h
+//  FLEX
+//
+//  Created by Tanner Bennett on 3/26/20.
+//  Copyright (c) 2020 Flipboard. 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

+ 868 - 0
Classes/GlobalStateExplorers/RuntimeBrowser/DataSources/FLEXRuntimeExporter.m

@@ -0,0 +1,868 @@
+//
+//  FLEXRuntimeExporter.m
+//  FLEX
+//
+//  Created by Tanner Bennett on 3/26/20.
+//  Copyright (c) 2020 Flipboard. All rights reserved.
+//
+
+#import "FLEXRuntimeExporter.h"
+#import "FLEXSQLiteDatabaseManager.h"
+#import "FLEX-Runtime.h"
+#import "NSObject+Reflection.h"
+#import "FLEXRuntimeController.h"
+#import "FLEXRuntimeClient.h"
+#import "NSArray+Functional.h"
+#import <sqlite3.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

+ 87 - 4
Classes/ObjectExplorers/Sections/Shortcuts/FLEXBundleShortcuts.m

@@ -8,6 +8,9 @@
 
 
 #import "FLEXBundleShortcuts.h"
 #import "FLEXBundleShortcuts.h"
 #import "FLEXShortcut.h"
 #import "FLEXShortcut.h"
+#import "FLEXAlert.h"
+#import "FLEXRuntimeExporter.h"
+#import "FLEXTableListViewController.h"
 #import "FLEXFileBrowserTableViewController.h"
 #import "FLEXFileBrowserTableViewController.h"
 
 
 #pragma mark -
 #pragma mark -
@@ -16,15 +19,95 @@
 
 
 + (instancetype)forObject:(NSBundle *)bundle {
 + (instancetype)forObject:(NSBundle *)bundle {
     return [self forObject:bundle additionalRows:@[
     return [self forObject:bundle additionalRows:@[
-        [FLEXActionShortcut title:@"Browse Bundle Directory" subtitle:nil
-            viewer:^UIViewController *(id view) {
+        [FLEXActionShortcut
+            title:@"Browse Bundle Directory" subtitle:nil
+            viewer:^UIViewController *(NSBundle *bundle) {
                 return [FLEXFileBrowserTableViewController path:bundle.bundlePath];
                 return [FLEXFileBrowserTableViewController path:bundle.bundlePath];
             }
             }
-            accessoryType:^UITableViewCellAccessoryType(id view) {
+            accessoryType:^UITableViewCellAccessoryType(NSBundle *bundle) {
                 return UITableViewCellAccessoryDisclosureIndicator;
                 return UITableViewCellAccessoryDisclosureIndicator;
             }
             }
-        ]
+        ],
+        [FLEXActionShortcut title:@"Browse Bundle as Database…" subtitle:nil
+            selectionHandler:^(UIViewController *host, NSBundle *bundle) {
+                [self promptToExportBundleAsDatabase:bundle host:host];
+            }
+            accessoryType:^UITableViewCellAccessoryType(NSBundle *bundle) {
+                return UITableViewCellAccessoryDisclosureIndicator;
+            }
+        ],
     ]];
     ]];
 }
 }
 
 
++ (void)promptToExportBundleAsDatabase:(NSBundle *)bundle host:(UIViewController *)host {
+    [FLEXAlert makeAlert:^(FLEXAlert *make) {
+        make.title(@"Save As…").message(
+            @"The database be saved in the Library folder. "
+            "Depending on the number of classes, it may take "
+            "10 minutes or more to finish exporting. 20,000 "
+            "classes takes about 7 minutes."
+        );
+        make.configuredTextField(^(UITextField *field) {
+            field.placeholder = @"FLEXRuntimeExport.objc.db";
+            field.text = [NSString stringWithFormat:
+                @"%@.objc.db", bundle.executablePath.lastPathComponent
+            ];
+        });
+        make.button(@"Start").handler(^(NSArray<NSString *> *strings) {
+            [self browseBundleAsDatabase:bundle host:host name:strings[0]];
+        });
+        make.button(@"Cancel").cancelStyle();
+    } showFrom:host];
+}
+
++ (void)browseBundleAsDatabase:(NSBundle *)bundle host:(UIViewController *)host name:(NSString *)name {
+    NSParameterAssert(name.length);
+    
+    UIAlertController *progress = [FLEXAlert makeAlert:^(FLEXAlert *make) {
+        make.title(@"Generating Database");
+        // Some iOS version glitch out of there is
+        // no initial message and you add one later
+        make.message(@"…");
+    }];
+    
+    [host presentViewController:progress animated:YES completion:^{
+        // Generate path to store db
+        NSString *path = [NSSearchPathForDirectoriesInDomains(
+            NSLibraryDirectory, NSUserDomainMask, YES
+        )[0] stringByAppendingPathComponent:name];
+        
+        progress.message = [path stringByAppendingString:@"\n\nCreating database…"];
+        
+        // Generate db and show progress
+        [FLEXRuntimeExporter createRuntimeDatabaseAtPath:path
+            forImages:@[bundle.executablePath]
+            progressHandler:^(NSString *status) {
+                dispatch_async(dispatch_get_main_queue(), ^{
+                    progress.message = [progress.message
+                        stringByAppendingFormat:@"\n%@", status
+                    ];
+                    [progress.view setNeedsLayout];
+                    [progress.view layoutIfNeeded];
+                });
+            } completion:^(NSString *error) {
+                // Display error if any
+                if (error) {
+                    progress.title = @"Error";
+                    progress.message = error;
+                    [progress addAction:[UIAlertAction
+                        actionWithTitle:@"OK" style:UIAlertActionStyleCancel handler:nil]
+                    ];
+                }
+                // Browse database
+                else {
+                    [progress dismissViewControllerAnimated:YES completion:nil];
+                    [host.navigationController pushViewController:[
+                        [FLEXTableListViewController alloc] initWithPath:path
+                    ] animated:YES];
+                }
+            }
+        ];
+    }];
+}
+
 @end
 @end

+ 8 - 0
FLEX.xcodeproj/project.pbxproj

@@ -318,6 +318,8 @@
 		C3E5D9FD2316E83700E655DB /* FLEXRuntime+Compare.h in Headers */ = {isa = PBXBuildFile; fileRef = C3E5D9FB2316E83700E655DB /* FLEXRuntime+Compare.h */; settings = {ATTRIBUTES = (Public, ); }; };
 		C3E5D9FD2316E83700E655DB /* FLEXRuntime+Compare.h in Headers */ = {isa = PBXBuildFile; fileRef = C3E5D9FB2316E83700E655DB /* FLEXRuntime+Compare.h */; settings = {ATTRIBUTES = (Public, ); }; };
 		C3E5D9FE2316E83700E655DB /* FLEXRuntime+Compare.m in Sources */ = {isa = PBXBuildFile; fileRef = C3E5D9FC2316E83700E655DB /* FLEXRuntime+Compare.m */; };
 		C3E5D9FE2316E83700E655DB /* FLEXRuntime+Compare.m in Sources */ = {isa = PBXBuildFile; fileRef = C3E5D9FC2316E83700E655DB /* FLEXRuntime+Compare.m */; };
 		C3E5DA02231700EE00E655DB /* FLEXObjectInfoSection.h in Headers */ = {isa = PBXBuildFile; fileRef = C3E5DA00231700EE00E655DB /* FLEXObjectInfoSection.h */; settings = {ATTRIBUTES = (Public, ); }; };
 		C3E5DA02231700EE00E655DB /* FLEXObjectInfoSection.h in Headers */ = {isa = PBXBuildFile; fileRef = C3E5DA00231700EE00E655DB /* FLEXObjectInfoSection.h */; settings = {ATTRIBUTES = (Public, ); }; };
+		C3EB6F8D242E9C83006EA386 /* FLEXRuntimeExporter.m in Sources */ = {isa = PBXBuildFile; fileRef = C3EB6F8B242E9C83006EA386 /* FLEXRuntimeExporter.m */; };
+		C3EB6F8E242E9C83006EA386 /* FLEXRuntimeExporter.h in Headers */ = {isa = PBXBuildFile; fileRef = C3EB6F8C242E9C83006EA386 /* FLEXRuntimeExporter.h */; };
 		C3EE76BF22DFC63600EC0AA0 /* FLEXScopeCarousel.h in Headers */ = {isa = PBXBuildFile; fileRef = C3EE76BD22DFC63600EC0AA0 /* FLEXScopeCarousel.h */; settings = {ATTRIBUTES = (Public, ); }; };
 		C3EE76BF22DFC63600EC0AA0 /* FLEXScopeCarousel.h in Headers */ = {isa = PBXBuildFile; fileRef = C3EE76BD22DFC63600EC0AA0 /* FLEXScopeCarousel.h */; settings = {ATTRIBUTES = (Public, ); }; };
 		C3EE76C022DFC63600EC0AA0 /* FLEXScopeCarousel.m in Sources */ = {isa = PBXBuildFile; fileRef = C3EE76BE22DFC63600EC0AA0 /* FLEXScopeCarousel.m */; };
 		C3EE76C022DFC63600EC0AA0 /* FLEXScopeCarousel.m in Sources */ = {isa = PBXBuildFile; fileRef = C3EE76BE22DFC63600EC0AA0 /* FLEXScopeCarousel.m */; };
 		C3F31D3D2267D883003C991A /* FLEXSubtitleTableViewCell.h in Headers */ = {isa = PBXBuildFile; fileRef = C3F31D342267D883003C991A /* FLEXSubtitleTableViewCell.h */; settings = {ATTRIBUTES = (Public, ); }; };
 		C3F31D3D2267D883003C991A /* FLEXSubtitleTableViewCell.h in Headers */ = {isa = PBXBuildFile; fileRef = C3F31D342267D883003C991A /* FLEXSubtitleTableViewCell.h */; settings = {ATTRIBUTES = (Public, ); }; };
@@ -672,6 +674,8 @@
 		C3E5D9FB2316E83700E655DB /* FLEXRuntime+Compare.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "FLEXRuntime+Compare.h"; sourceTree = "<group>"; };
 		C3E5D9FB2316E83700E655DB /* FLEXRuntime+Compare.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "FLEXRuntime+Compare.h"; sourceTree = "<group>"; };
 		C3E5D9FC2316E83700E655DB /* FLEXRuntime+Compare.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "FLEXRuntime+Compare.m"; sourceTree = "<group>"; };
 		C3E5D9FC2316E83700E655DB /* FLEXRuntime+Compare.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "FLEXRuntime+Compare.m"; sourceTree = "<group>"; };
 		C3E5DA00231700EE00E655DB /* FLEXObjectInfoSection.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FLEXObjectInfoSection.h; sourceTree = "<group>"; };
 		C3E5DA00231700EE00E655DB /* FLEXObjectInfoSection.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FLEXObjectInfoSection.h; sourceTree = "<group>"; };
+		C3EB6F8B242E9C83006EA386 /* FLEXRuntimeExporter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLEXRuntimeExporter.m; sourceTree = "<group>"; };
+		C3EB6F8C242E9C83006EA386 /* FLEXRuntimeExporter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLEXRuntimeExporter.h; sourceTree = "<group>"; };
 		C3EE76BD22DFC63600EC0AA0 /* FLEXScopeCarousel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FLEXScopeCarousel.h; sourceTree = "<group>"; };
 		C3EE76BD22DFC63600EC0AA0 /* FLEXScopeCarousel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FLEXScopeCarousel.h; sourceTree = "<group>"; };
 		C3EE76BE22DFC63600EC0AA0 /* FLEXScopeCarousel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FLEXScopeCarousel.m; sourceTree = "<group>"; };
 		C3EE76BE22DFC63600EC0AA0 /* FLEXScopeCarousel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FLEXScopeCarousel.m; sourceTree = "<group>"; };
 		C3F31D342267D883003C991A /* FLEXSubtitleTableViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLEXSubtitleTableViewCell.h; sourceTree = "<group>"; };
 		C3F31D342267D883003C991A /* FLEXSubtitleTableViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLEXSubtitleTableViewCell.h; sourceTree = "<group>"; };
@@ -1288,6 +1292,8 @@
 		C398626F23AD71C6007E6793 /* DataSources */ = {
 		C398626F23AD71C6007E6793 /* DataSources */ = {
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
+				C3EB6F8C242E9C83006EA386 /* FLEXRuntimeExporter.h */,
+				C3EB6F8B242E9C83006EA386 /* FLEXRuntimeExporter.m */,
 				C398626723AD71C1007E6793 /* FLEXRuntimeClient.h */,
 				C398626723AD71C1007E6793 /* FLEXRuntimeClient.h */,
 				C398626A23AD71C1007E6793 /* FLEXRuntimeClient.m */,
 				C398626A23AD71C1007E6793 /* FLEXRuntimeClient.m */,
 				C398626823AD71C1007E6793 /* FLEXRuntimeController.h */,
 				C398626823AD71C1007E6793 /* FLEXRuntimeController.h */,
@@ -1577,6 +1583,7 @@
 				C3A9424023C5443A006871A3 /* FHSSnapshotNodes.h in Headers */,
 				C3A9424023C5443A006871A3 /* FHSSnapshotNodes.h in Headers */,
 				C32A195E231732E800EB02AC /* FLEXCollectionContentSection.h in Headers */,
 				C32A195E231732E800EB02AC /* FLEXCollectionContentSection.h in Headers */,
 				3A4C94E11B5B21410088C3F2 /* FLEXResources.h in Headers */,
 				3A4C94E11B5B21410088C3F2 /* FLEXResources.h in Headers */,
+				C3EB6F8E242E9C83006EA386 /* FLEXRuntimeExporter.h in Headers */,
 				779B1ED81C0C4D7C001F5E49 /* FLEXTableLeftCell.h in Headers */,
 				779B1ED81C0C4D7C001F5E49 /* FLEXTableLeftCell.h in Headers */,
 			);
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 			runOnlyForDeploymentPostprocessing = 0;
@@ -1713,6 +1720,7 @@
 				3A4C95101B5B21410088C3F2 /* FLEXMethodCallingViewController.m in Sources */,
 				3A4C95101B5B21410088C3F2 /* FLEXMethodCallingViewController.m in Sources */,
 				C362AE8223C7E9D1005A86AE /* NSMapTable+FLEX_Subscripting.m in Sources */,
 				C362AE8223C7E9D1005A86AE /* NSMapTable+FLEX_Subscripting.m in Sources */,
 				C3F646C2239EAA8F00D4A011 /* UIPasteboard+FLEX.m in Sources */,
 				C3F646C2239EAA8F00D4A011 /* UIPasteboard+FLEX.m in Sources */,
+				C3EB6F8D242E9C83006EA386 /* FLEXRuntimeExporter.m in Sources */,
 				3A4C94F61B5B21410088C3F2 /* FLEXArgumentInputObjectView.m in Sources */,
 				3A4C94F61B5B21410088C3F2 /* FLEXArgumentInputObjectView.m in Sources */,
 				3A4C94EC1B5B21410088C3F2 /* FLEXImagePreviewViewController.m in Sources */,
 				3A4C94EC1B5B21410088C3F2 /* FLEXImagePreviewViewController.m in Sources */,
 				C383C3BA23B6A62A007A321B /* FLEXRuntimeSafety.m in Sources */,
 				C383C3BA23B6A62A007A321B /* FLEXRuntimeSafety.m in Sources */,