added files via upload

This commit is contained in:
Balackburn
2023-06-27 09:54:41 +02:00
commit 2ff6aac218
1420 changed files with 88898 additions and 0 deletions

View File

@@ -0,0 +1,28 @@
//
// FLEXDBQueryRowCell.h
// FLEX
//
// Created by Peng Tao on 15/11/24.
// Copyright © 2015年 f. All rights reserved.
//
#import <UIKit/UIKit.h>
@class FLEXDBQueryRowCell;
extern NSString * const kFLEXDBQueryRowCellReuse;
@protocol FLEXDBQueryRowCellLayoutSource <NSObject>
- (CGFloat)dbQueryRowCell:(FLEXDBQueryRowCell *)dbQueryRowCell minXForColumn:(NSUInteger)column;
- (CGFloat)dbQueryRowCell:(FLEXDBQueryRowCell *)dbQueryRowCell widthForColumn:(NSUInteger)column;
@end
@interface FLEXDBQueryRowCell : UITableViewCell
/// An array of NSString, NSNumber, or NSData objects
@property (nonatomic) NSArray *data;
@property (nonatomic, weak) id<FLEXDBQueryRowCellLayoutSource> layoutSource;
@end

View File

@@ -0,0 +1,75 @@
//
// FLEXDBQueryRowCell.m
// FLEX
//
// Created by Peng Tao on 15/11/24.
// Copyright © 2015 f. All rights reserved.
//
#import "FLEXDBQueryRowCell.h"
#import "FLEXMultiColumnTableView.h"
#import "NSArray+FLEX.h"
#import "UIFont+FLEX.h"
#import "FLEXColor.h"
NSString * const kFLEXDBQueryRowCellReuse = @"kFLEXDBQueryRowCellReuse";
@interface FLEXDBQueryRowCell ()
@property (nonatomic) NSInteger columnCount;
@property (nonatomic) NSArray<UILabel *> *labels;
@end
@implementation FLEXDBQueryRowCell
- (void)setData:(NSArray *)data {
_data = data;
self.columnCount = data.count;
[self.labels flex_forEach:^(UILabel *label, NSUInteger idx) {
id content = self.data[idx];
if ([content isKindOfClass:[NSString class]]) {
label.text = content;
} else if (content == NSNull.null) {
label.text = @"<null>";
label.textColor = FLEXColor.deemphasizedTextColor;
} else {
label.text = [content description];
}
}];
}
- (void)setColumnCount:(NSInteger)columnCount {
if (columnCount != _columnCount) {
_columnCount = columnCount;
// Remove existing labels
for (UILabel *l in self.labels) {
[l removeFromSuperview];
}
// Create new labels
self.labels = [NSArray flex_forEachUpTo:columnCount map:^id(NSUInteger i) {
UILabel *label = [UILabel new];
label.font = UIFont.flex_defaultTableCellFont;
label.textAlignment = NSTextAlignmentLeft;
[self.contentView addSubview:label];
return label;
}];
}
}
- (void)layoutSubviews {
[super layoutSubviews];
CGFloat height = self.contentView.frame.size.height;
[self.labels flex_forEach:^(UILabel *label, NSUInteger i) {
CGFloat width = [self.layoutSource dbQueryRowCell:self widthForColumn:i];
CGFloat minX = [self.layoutSource dbQueryRowCell:self minXForColumn:i];
label.frame = CGRectMake(minX + 5, 0, (width - 10), height);
}];
}
@end

View File

@@ -0,0 +1,35 @@
//
// PTDatabaseManager.h
// Derived from:
//
// FMDatabase.h
// FMDB( https://github.com/ccgus/fmdb )
//
// Created by Peng Tao on 15/11/23.
//
// Licensed to Flying Meat Inc. under one or more contributor license agreements.
// See the LICENSE file distributed with this work for the terms under
// which Flying Meat Inc. licenses this file to you.
#import <Foundation/Foundation.h>
#import "FLEXSQLResult.h"
/// Conformers should automatically open and close the database
@protocol FLEXDatabaseManager <NSObject>
@required
/// @return \c nil if the database couldn't be opened
+ (instancetype)managerForDatabase:(NSString *)path;
/// @return a list of all table names
- (NSArray<NSString *> *)queryAllTables;
- (NSArray<NSString *> *)queryAllColumnsOfTable:(NSString *)tableName;
- (NSArray<NSArray *> *)queryAllDataInTable:(NSString *)tableName;
@optional
- (NSArray<NSString *> *)queryRowIDsInTable:(NSString *)tableName;
- (FLEXSQLResult *)executeStatement:(NSString *)SQLStatement;
@end

View File

@@ -0,0 +1,47 @@
//
// PTMultiColumnTableView.h
// PTMultiColumnTableViewDemo
//
// Created by Peng Tao on 15/11/16.
// Copyright © 2015年 Peng Tao. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "FLEXTableColumnHeader.h"
@class FLEXMultiColumnTableView;
@protocol FLEXMultiColumnTableViewDelegate <NSObject>
@required
- (void)multiColumnTableView:(FLEXMultiColumnTableView *)tableView didSelectRow:(NSInteger)row;
- (void)multiColumnTableView:(FLEXMultiColumnTableView *)tableView didSelectHeaderForColumn:(NSInteger)column sortType:(FLEXTableColumnHeaderSortType)sortType;
@end
@protocol FLEXMultiColumnTableViewDataSource <NSObject>
@required
- (NSInteger)numberOfColumnsInTableView:(FLEXMultiColumnTableView *)tableView;
- (NSInteger)numberOfRowsInTableView:(FLEXMultiColumnTableView *)tableView;
- (NSString *)columnTitle:(NSInteger)column;
- (NSString *)rowTitle:(NSInteger)row;
- (NSArray<NSString *> *)contentForRow:(NSInteger)row;
- (CGFloat)multiColumnTableView:(FLEXMultiColumnTableView *)tableView minWidthForContentCellInColumn:(NSInteger)column;
- (CGFloat)multiColumnTableView:(FLEXMultiColumnTableView *)tableView heightForContentCellInRow:(NSInteger)row;
- (CGFloat)heightForTopHeaderInTableView:(FLEXMultiColumnTableView *)tableView;
- (CGFloat)widthForLeftHeaderInTableView:(FLEXMultiColumnTableView *)tableView;
@end
@interface FLEXMultiColumnTableView : UIView
@property (nonatomic, weak) id<FLEXMultiColumnTableViewDataSource> dataSource;
@property (nonatomic, weak) id<FLEXMultiColumnTableViewDelegate> delegate;
- (void)reloadData;
@end

View File

@@ -0,0 +1,339 @@
//
// PTMultiColumnTableView.m
// PTMultiColumnTableViewDemo
//
// Created by Peng Tao on 15/11/16.
// Copyright © 2015 Peng Tao. All rights reserved.
//
#import "FLEXMultiColumnTableView.h"
#import "FLEXDBQueryRowCell.h"
#import "FLEXTableLeftCell.h"
#import "NSArray+FLEX.h"
#import "FLEXColor.h"
@interface FLEXMultiColumnTableView () <
UITableViewDataSource, UITableViewDelegate,
UIScrollViewDelegate, FLEXDBQueryRowCellLayoutSource
>
@property (nonatomic) UIScrollView *contentScrollView;
@property (nonatomic) UIScrollView *headerScrollView;
@property (nonatomic) UITableView *leftTableView;
@property (nonatomic) UITableView *contentTableView;
@property (nonatomic) UIView *leftHeader;
@property (nonatomic) NSArray<UIView *> *headerViews;
/// \c NSNotFound if no column selected
@property (nonatomic) NSInteger sortColumn;
@property (nonatomic) FLEXTableColumnHeaderSortType sortType;
@property (nonatomic, readonly) NSInteger numberOfColumns;
@property (nonatomic, readonly) NSInteger numberOfRows;
@property (nonatomic, readonly) CGFloat topHeaderHeight;
@property (nonatomic, readonly) CGFloat leftHeaderWidth;
@property (nonatomic, readonly) CGFloat columnMargin;
@end
static const CGFloat kColumnMargin = 1;
@implementation FLEXMultiColumnTableView
#pragma mark - Initialization
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.autoresizingMask |= UIViewAutoresizingFlexibleWidth;
self.autoresizingMask |= UIViewAutoresizingFlexibleHeight;
self.autoresizingMask |= UIViewAutoresizingFlexibleTopMargin;
self.backgroundColor = FLEXColor.groupedBackgroundColor;
[self loadHeaderScrollView];
[self loadContentScrollView];
[self loadLeftView];
}
return self;
}
- (void)layoutSubviews {
[super layoutSubviews];
CGFloat width = self.frame.size.width;
CGFloat height = self.frame.size.height;
CGFloat topheaderHeight = self.topHeaderHeight;
CGFloat leftHeaderWidth = self.leftHeaderWidth;
CGFloat topInsets = 0.f;
if (@available (iOS 11.0, *)) {
topInsets = self.safeAreaInsets.top;
}
CGFloat contentWidth = 0.0;
NSInteger columnsCount = self.numberOfColumns;
for (int i = 0; i < columnsCount; i++) {
contentWidth += CGRectGetWidth(self.headerViews[i].bounds);
}
CGFloat contentHeight = height - topheaderHeight - topInsets;
self.leftHeader.frame = CGRectMake(0, topInsets, self.leftHeaderWidth, self.topHeaderHeight);
self.leftTableView.frame = CGRectMake(
0, topheaderHeight + topInsets, leftHeaderWidth, contentHeight
);
self.headerScrollView.frame = CGRectMake(
leftHeaderWidth, topInsets, width - leftHeaderWidth, topheaderHeight
);
self.headerScrollView.contentSize = CGSizeMake(
self.contentTableView.frame.size.width, self.headerScrollView.frame.size.height
);
self.contentTableView.frame = CGRectMake(
0, 0, contentWidth + self.numberOfColumns * self.columnMargin , contentHeight
);
self.contentScrollView.frame = CGRectMake(
leftHeaderWidth, topheaderHeight + topInsets, width - leftHeaderWidth, contentHeight
);
self.contentScrollView.contentSize = self.contentTableView.frame.size;
}
#pragma mark - UI
- (void)loadHeaderScrollView {
UIScrollView *headerScrollView = [UIScrollView new];
headerScrollView.delegate = self;
headerScrollView.backgroundColor = FLEXColor.secondaryGroupedBackgroundColor;
self.headerScrollView = headerScrollView;
[self addSubview:headerScrollView];
}
- (void)loadContentScrollView {
UIScrollView *scrollView = [UIScrollView new];
scrollView.bounces = NO;
scrollView.delegate = self;
UITableView *tableView = [UITableView new];
tableView.delegate = self;
tableView.dataSource = self;
tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
[tableView registerClass:[FLEXDBQueryRowCell class]
forCellReuseIdentifier:kFLEXDBQueryRowCellReuse
];
[scrollView addSubview:tableView];
[self addSubview:scrollView];
self.contentScrollView = scrollView;
self.contentTableView = tableView;
}
- (void)loadLeftView {
UITableView *leftTableView = [UITableView new];
leftTableView.delegate = self;
leftTableView.dataSource = self;
leftTableView.separatorStyle = UITableViewCellSeparatorStyleNone;
self.leftTableView = leftTableView;
[self addSubview:leftTableView];
UIView *leftHeader = [UIView new];
leftHeader.backgroundColor = FLEXColor.secondaryBackgroundColor;
self.leftHeader = leftHeader;
[self addSubview:leftHeader];
}
#pragma mark - Data
- (void)reloadData {
[self loadHeaderData];
[self loadLeftViewData];
[self loadContentData];
}
- (void)loadHeaderData {
// Remove existing headers, if any
for (UIView *subview in self.headerViews) {
[subview removeFromSuperview];
}
__block CGFloat xOffset = 0;
self.headerViews = [NSArray flex_forEachUpTo:self.numberOfColumns map:^id(NSUInteger column) {
FLEXTableColumnHeader *header = [FLEXTableColumnHeader new];
header.titleLabel.text = [self columnTitle:column];
CGSize fittingSize = CGSizeMake(CGFLOAT_MAX, self.topHeaderHeight - 1);
CGFloat width = self.columnMargin + MAX(
[self minContentWidthForColumn:column],
[header sizeThatFits:fittingSize].width
);
header.frame = CGRectMake(xOffset, 0, width, self.topHeaderHeight - 1);
if (column == self.sortColumn) {
header.sortType = self.sortType;
}
// Header tap gesture
UITapGestureRecognizer *gesture = [[UITapGestureRecognizer alloc]
initWithTarget:self action:@selector(contentHeaderTap:)
];
[header addGestureRecognizer:gesture];
header.userInteractionEnabled = YES;
xOffset += width;
[self.headerScrollView addSubview:header];
return header;
}];
}
- (void)contentHeaderTap:(UIGestureRecognizer *)gesture {
NSInteger newSortColumn = [self.headerViews indexOfObject:gesture.view];
FLEXTableColumnHeaderSortType newType = FLEXNextTableColumnHeaderSortType(self.sortType);
// Reset old header
FLEXTableColumnHeader *oldHeader = (id)self.headerViews[self.sortColumn];
oldHeader.sortType = FLEXTableColumnHeaderSortTypeNone;
// Update new header
FLEXTableColumnHeader *newHeader = (id)self.headerViews[newSortColumn];
newHeader.sortType = newType;
// Update self
self.sortColumn = newSortColumn;
self.sortType = newType;
// Notify delegate
[self.delegate multiColumnTableView:self didSelectHeaderForColumn:newSortColumn sortType:newType];
}
- (void)loadContentData {
[self.contentTableView reloadData];
}
- (void)loadLeftViewData {
[self.leftTableView reloadData];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
// Alternating background color
UIColor *backgroundColor = FLEXColor.primaryBackgroundColor;
if (indexPath.row % 2 != 0) {
backgroundColor = FLEXColor.secondaryBackgroundColor;
}
// Left side table view for row numbers
if (tableView == self.leftTableView) {
FLEXTableLeftCell *cell = [FLEXTableLeftCell cellWithTableView:tableView];
cell.contentView.backgroundColor = backgroundColor;
cell.titlelabel.text = [self rowTitle:indexPath.row];
return cell;
}
// Right side table view for data
else {
FLEXDBQueryRowCell *cell = [tableView
dequeueReusableCellWithIdentifier:kFLEXDBQueryRowCellReuse forIndexPath:indexPath
];
cell.contentView.backgroundColor = backgroundColor;
cell.data = [self.dataSource contentForRow:indexPath.row];
cell.layoutSource = self;
NSAssert(cell.data.count == self.numberOfColumns, @"Count of data provided was incorrect");
return cell;
}
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [self.dataSource numberOfRowsInTableView:self];
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return [self.dataSource multiColumnTableView:self heightForContentCellInRow:indexPath.row];
}
// Scroll all scroll views in sync
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
if (scrollView == self.contentScrollView) {
self.headerScrollView.contentOffset = scrollView.contentOffset;
}
else if (scrollView == self.headerScrollView) {
self.contentScrollView.contentOffset = scrollView.contentOffset;
}
else if (scrollView == self.leftTableView) {
self.contentTableView.contentOffset = scrollView.contentOffset;
}
else if (scrollView == self.contentTableView) {
self.leftTableView.contentOffset = scrollView.contentOffset;
}
}
#pragma mark UITableView Delegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
if (tableView == self.leftTableView) {
[self.contentTableView
selectRowAtIndexPath:indexPath
animated:NO
scrollPosition:UITableViewScrollPositionNone
];
}
else if (tableView == self.contentTableView) {
[self.delegate multiColumnTableView:self didSelectRow:indexPath.row];
}
}
#pragma mark FLEXDBQueryRowCellLayoutSource
- (CGFloat)dbQueryRowCell:(FLEXDBQueryRowCell *)dbQueryRowCell minXForColumn:(NSUInteger)column {
return CGRectGetMinX(self.headerViews[column].frame);
}
- (CGFloat)dbQueryRowCell:(FLEXDBQueryRowCell *)dbQueryRowCell widthForColumn:(NSUInteger)column {
return CGRectGetWidth(self.headerViews[column].bounds);
}
#pragma mark DataSource Accessor
- (NSInteger)numberOfRows {
return [self.dataSource numberOfRowsInTableView:self];
}
- (NSInteger)numberOfColumns {
return [self.dataSource numberOfColumnsInTableView:self];
}
- (NSString *)columnTitle:(NSInteger)column {
return [self.dataSource columnTitle:column];
}
- (NSString *)rowTitle:(NSInteger)row {
return [self.dataSource rowTitle:row];
}
- (CGFloat)minContentWidthForColumn:(NSInteger)column {
return [self.dataSource multiColumnTableView:self minWidthForContentCellInColumn:column];
}
- (CGFloat)contentHeightForRow:(NSInteger)row {
return [self.dataSource multiColumnTableView:self heightForContentCellInRow:row];
}
- (CGFloat)topHeaderHeight {
return [self.dataSource heightForTopHeaderInTableView:self];
}
- (CGFloat)leftHeaderWidth {
return [self.dataSource widthForLeftHeaderInTableView:self];
}
- (CGFloat)columnMargin {
return kColumnMargin;
}
@end

View File

@@ -0,0 +1,14 @@
//
// FLEXRealmDatabaseManager.h
// FLEX
//
// Created by Tim Oliver on 28/01/2016.
// Copyright © 2016 Realm. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "FLEXDatabaseManager.h"
@interface FLEXRealmDatabaseManager : NSObject <FLEXDatabaseManager>
@end

View File

@@ -0,0 +1,102 @@
//
// FLEXRealmDatabaseManager.m
// FLEX
//
// Created by Tim Oliver on 28/01/2016.
// Copyright © 2016 Realm. All rights reserved.
//
#import "FLEXRealmDatabaseManager.h"
#import "NSArray+FLEX.h"
#import "FLEXSQLResult.h"
#if __has_include(<Realm/Realm.h>)
#import <Realm/Realm.h>
#import <Realm/RLMRealm_Dynamic.h>
#else
#import "FLEXRealmDefines.h"
#endif
@interface FLEXRealmDatabaseManager ()
@property (nonatomic, copy) NSString *path;
@property (nonatomic) RLMRealm *realm;
@end
@implementation FLEXRealmDatabaseManager
static Class RLMRealmClass = nil;
+ (void)load {
RLMRealmClass = NSClassFromString(@"RLMRealm");
}
+ (instancetype)managerForDatabase:(NSString *)path {
return [[self alloc] initWithPath:path];
}
- (instancetype)initWithPath:(NSString *)path {
if (!RLMRealmClass) {
return nil;
}
self = [super init];
if (self) {
_path = path;
if (![self open]) {
return nil;
}
}
return self;
}
- (BOOL)open {
Class configurationClass = NSClassFromString(@"RLMRealmConfiguration");
if (!RLMRealmClass || !configurationClass) {
return NO;
}
NSError *error = nil;
id configuration = [configurationClass new];
[(RLMRealmConfiguration *)configuration setFileURL:[NSURL fileURLWithPath:self.path]];
self.realm = [RLMRealmClass realmWithConfiguration:configuration error:&error];
return (error == nil);
}
- (NSArray<NSString *> *)queryAllTables {
// Map each schema to its name
NSArray<NSString *> *tableNames = [self.realm.schema.objectSchema flex_mapped:^id(RLMObjectSchema *schema, NSUInteger idx) {
return schema.className ?: nil;
}];
return [tableNames sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)];
}
- (NSArray<NSString *> *)queryAllColumnsOfTable:(NSString *)tableName {
RLMObjectSchema *objectSchema = [self.realm.schema schemaForClassName:tableName];
// Map each column to its name
return [objectSchema.properties flex_mapped:^id(RLMProperty *property, NSUInteger idx) {
return property.name;
}];
}
- (NSArray<NSArray *> *)queryAllDataInTable:(NSString *)tableName {
RLMObjectSchema *objectSchema = [self.realm.schema schemaForClassName:tableName];
RLMResults *results = [self.realm allObjects:tableName];
if (results.count == 0 || !objectSchema) {
return nil;
}
// Map results to an array of rows
return [NSArray flex_mapped:results block:^id(RLMObject *result, NSUInteger idx) {
// Map each row to an array of the values of its properties
return [objectSchema.properties flex_mapped:^id(RLMProperty *property, NSUInteger idx) {
return [result valueForKey:property.name] ?: NSNull.null;
}];
}];
}
@end

View File

@@ -0,0 +1,46 @@
//
// Realm.h
// FLEX
//
// Created by Tim Oliver on 16/02/2016.
// Copyright © 2016 Realm. All rights reserved.
//
#if __has_include(<Realm/Realm.h>)
#else
@class RLMObject, RLMResults, RLMRealm, RLMRealmConfiguration, RLMSchema, RLMObjectSchema, RLMProperty;
@interface RLMRealmConfiguration : NSObject
@property (nonatomic, copy) NSURL *fileURL;
@end
@interface RLMRealm : NSObject
@property (nonatomic, readonly) RLMSchema *schema;
+ (RLMRealm *)realmWithConfiguration:(RLMRealmConfiguration *)configuration error:(NSError **)error;
- (RLMResults *)allObjects:(NSString *)className;
@end
@interface RLMSchema : NSObject
@property (nonatomic, readonly) NSArray<RLMObjectSchema *> *objectSchema;
- (RLMObjectSchema *)schemaForClassName:(NSString *)className;
@end
@interface RLMObjectSchema : NSObject
@property (nonatomic, readonly) NSString *className;
@property (nonatomic, readonly) NSArray<RLMProperty *> *properties;
@end
@interface RLMProperty : NSString
@property (nonatomic, readonly) NSString *name;
@end
@interface RLMResults : NSObject <NSFastEnumeration>
@property (nonatomic, readonly) NSInteger count;
@end
@interface RLMObject : NSObject
@end
#endif

View File

@@ -0,0 +1,48 @@
//
// FLEXSQLResult.h
// FLEX
//
// Created by Tanner on 3/3/20.
// Copyright © 2020 FLEX Team. All rights reserved.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface FLEXSQLResult : NSObject
/// Describes the result of a non-select query, or an error of any kind of query
+ (instancetype)message:(NSString *)message;
/// Describes the result of a known failed execution
+ (instancetype)error:(NSString *)message;
/// @param rowData A list of rows, where each element in the row
/// corresponds to the column given in /c columnNames
+ (instancetype)columns:(NSArray<NSString *> *)columnNames
rows:(NSArray<NSArray<NSString *> *> *)rowData;
@property (nonatomic, readonly, nullable) NSString *message;
/// A value of YES means this is surely an error,
/// but it still might be an error even with a value of NO
@property (nonatomic, readonly) BOOL isError;
/// A list of column names
@property (nonatomic, readonly, nullable) NSArray<NSString *> *columns;
/// A list of rows, where each element in the row corresponds
/// to the value of the column at the same index in \c columns.
///
/// That is, given a row, looping over the contents of the row and
/// the contents of \c columns will give you key-value pairs of
/// column names to column values for that row.
@property (nonatomic, readonly, nullable) NSArray<NSArray<NSString *> *> *rows;
/// A list of rows where the fields are paired to column names.
///
/// This property is lazily constructed by looping over
/// the rows and columns present in the other two properties.
@property (nonatomic, readonly, nullable) NSArray<NSDictionary<NSString *, id> *> *keyedRows;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,53 @@
//
// FLEXSQLResult.m
// FLEX
//
// Created by Tanner on 3/3/20.
// Copyright © 2020 FLEX Team. All rights reserved.
//
#import "FLEXSQLResult.h"
#import "NSArray+FLEX.h"
@implementation FLEXSQLResult
@synthesize keyedRows = _keyedRows;
+ (instancetype)message:(NSString *)message {
return [[self alloc] initWithMessage:message columns:nil rows:nil];
}
+ (instancetype)error:(NSString *)message {
FLEXSQLResult *result = [self message:message];
result->_isError = YES;
return result;
}
+ (instancetype)columns:(NSArray<NSString *> *)columnNames rows:(NSArray<NSArray<NSString *> *> *)rowData {
return [[self alloc] initWithMessage:nil columns:columnNames rows:rowData];
}
- (instancetype)initWithMessage:(NSString *)message columns:(NSArray<NSString *> *)columns rows:(NSArray<NSArray<NSString *> *> *)rows {
NSParameterAssert(message || (columns && rows));
NSParameterAssert(rows.count == 0 || columns.count == rows.firstObject.count);
self = [super init];
if (self) {
_message = message;
_columns = columns;
_rows = rows;
}
return self;
}
- (NSArray<NSDictionary<NSString *,id> *> *)keyedRows {
if (!_keyedRows) {
_keyedRows = [self.rows flex_mapped:^id(NSArray<NSString *> *row, NSUInteger idx) {
return [NSDictionary dictionaryWithObjects:row forKeys:self.columns];
}];
}
return _keyedRows;
}
@end

View File

@@ -0,0 +1,32 @@
//
// PTDatabaseManager.h
// Derived from:
//
// FMDatabase.h
// FMDB( https://github.com/ccgus/fmdb )
//
// Created by Peng Tao on 15/11/23.
//
// Licensed to Flying Meat Inc. under one or more contributor license agreements.
// See the LICENSE file distributed with this work for the terms under
// which Flying Meat Inc. licenses this file to you.
#import <Foundation/Foundation.h>
#import "FLEXDatabaseManager.h"
#import "FLEXSQLResult.h"
@interface FLEXSQLiteDatabaseManager : NSObject <FLEXDatabaseManager>
/// Contains the result of the last operation, which may be an error
@property (nonatomic, readonly) FLEXSQLResult *lastResult;
/// Calls into \c sqlite3_last_insert_rowid()
@property (nonatomic, readonly) NSInteger lastRowID;
/// Given a statement like 'SELECT * from @table where @col = @val' and arguments
/// like { @"table": @"Album", @"col": @"year", @"val" @1 } this method will
/// invoke the statement and properly bind the given arguments to the statement.
///
/// You may pass NSStrings, NSData, NSNumbers, or NSNulls as values.
- (FLEXSQLResult *)executeStatement:(NSString *)statement arguments:(NSDictionary<NSString *, id> *)args;
@end

View File

@@ -0,0 +1,329 @@
//
// PTDatabaseManager.m
// PTDatabaseReader
//
// Created by Peng Tao on 15/11/23.
// Copyright © 2015 Peng Tao. All rights reserved.
//
#import "FLEXSQLiteDatabaseManager.h"
#import "FLEXManager.h"
#import "NSArray+FLEX.h"
#import "FLEXRuntimeConstants.h"
#import <sqlite3.h>
#define kQuery(name, str) static NSString * const QUERY_##name = str
kQuery(TABLENAMES, @"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name");
kQuery(ROWIDS, @"SELECT rowid FROM \"%@\" ORDER BY rowid ASC");
@interface FLEXSQLiteDatabaseManager ()
@property (nonatomic) sqlite3 *db;
@property (nonatomic, copy) NSString *path;
@end
@implementation FLEXSQLiteDatabaseManager
#pragma mark - FLEXDatabaseManager
+ (instancetype)managerForDatabase:(NSString *)path {
return [[self alloc] initWithPath:path];
}
- (instancetype)initWithPath:(NSString *)path {
self = [super init];
if (self) {
self.path = path;
}
return self;
}
- (void)dealloc {
[self close];
}
- (BOOL)open {
if (self.db) {
return YES;
}
int err = sqlite3_open(self.path.UTF8String, &_db);
#if SQLITE_HAS_CODEC
NSString *defaultSqliteDatabasePassword = FLEXManager.sharedManager.defaultSqliteDatabasePassword;
if (defaultSqliteDatabasePassword) {
const char *key = defaultSqliteDatabasePassword.UTF8String;
sqlite3_key(_db, key, (int)strlen(key));
}
#endif
if (err != SQLITE_OK) {
return [self storeErrorForLastTask:@"Open"];
}
return YES;
}
- (BOOL)close {
if (!self.db) {
return YES;
}
int rc;
BOOL retry, triedFinalizingOpenStatements = NO;
do {
retry = NO;
rc = sqlite3_close(_db);
if (SQLITE_BUSY == rc || SQLITE_LOCKED == rc) {
if (!triedFinalizingOpenStatements) {
triedFinalizingOpenStatements = YES;
sqlite3_stmt *pStmt;
while ((pStmt = sqlite3_next_stmt(_db, nil)) !=0) {
NSLog(@"Closing leaked statement");
sqlite3_finalize(pStmt);
retry = YES;
}
}
} else if (SQLITE_OK != rc) {
[self storeErrorForLastTask:@"Close"];
self.db = nil;
return NO;
}
} while (retry);
self.db = nil;
return YES;
}
- (NSInteger)lastRowID {
return (NSInteger)sqlite3_last_insert_rowid(self.db);
}
- (NSArray<NSString *> *)queryAllTables {
return [[self executeStatement:QUERY_TABLENAMES].rows flex_mapped:^id(NSArray *table, NSUInteger idx) {
return table.firstObject;
}] ?: @[];
}
- (NSArray<NSString *> *)queryAllColumnsOfTable:(NSString *)tableName {
NSString *sql = [NSString stringWithFormat:@"PRAGMA table_info('%@')",tableName];
FLEXSQLResult *results = [self executeStatement:sql];
// https://github.com/FLEXTool/FLEX/issues/554
if (!results.keyedRows.count) {
sql = [NSString stringWithFormat:@"SELECT * FROM pragma_table_info('%@')", tableName];
results = [self executeStatement:sql];
// Fallback to empty query
if (!results.keyedRows.count) {
sql = [NSString stringWithFormat:@"SELECT * FROM \"%@\" where 0=1", tableName];
return [self executeStatement:sql].columns ?: @[];
}
}
return [results.keyedRows flex_mapped:^id(NSDictionary *column, NSUInteger idx) {
return column[@"name"];
}] ?: @[];
}
- (NSArray<NSArray *> *)queryAllDataInTable:(NSString *)tableName {
NSString *command = [NSString stringWithFormat:@"SELECT * FROM \"%@\"", tableName];
return [self executeStatement:command].rows ?: @[];
}
- (NSArray<NSString *> *)queryRowIDsInTable:(NSString *)tableName {
NSString *command = [NSString stringWithFormat:QUERY_ROWIDS, tableName];
NSArray<NSArray<NSString *> *> *data = [self executeStatement:command].rows ?: @[];
return [data flex_mapped:^id(NSArray<NSString *> *obj, NSUInteger idx) {
return obj.firstObject;
}];
}
- (FLEXSQLResult *)executeStatement:(NSString *)sql {
return [self executeStatement:sql arguments:nil];
}
- (FLEXSQLResult *)executeStatement:(NSString *)sql arguments:(NSDictionary *)args {
[self open];
FLEXSQLResult *result = nil;
sqlite3_stmt *pstmt;
int status;
if ((status = sqlite3_prepare_v2(_db, sql.UTF8String, -1, &pstmt, 0)) == SQLITE_OK) {
NSMutableArray<NSArray *> *rows = [NSMutableArray new];
// Bind parameters, if any
if (![self bindParameters:args toStatement:pstmt]) {
return self.lastResult;
}
// Grab columns (columnCount will be 0 for insert/update/delete)
int columnCount = sqlite3_column_count(pstmt);
NSArray<NSString *> *columns = [NSArray flex_forEachUpTo:columnCount map:^id(NSUInteger i) {
return @(sqlite3_column_name(pstmt, (int)i));
}];
// Execute statement
while ((status = sqlite3_step(pstmt)) == SQLITE_ROW) {
// Grab rows if this is a selection query
int dataCount = sqlite3_data_count(pstmt);
if (dataCount > 0) {
[rows addObject:[NSArray flex_forEachUpTo:columnCount map:^id(NSUInteger i) {
return [self objectForColumnIndex:(int)i stmt:pstmt];
}]];
}
}
if (status == SQLITE_DONE) {
// columnCount will be 0 for insert/update/delete
if (rows.count || columnCount > 0) {
// We executed a SELECT query
result = _lastResult = [FLEXSQLResult columns:columns rows:rows];
} else {
// We executed a query like INSERT, UDPATE, or DELETE
int rowsAffected = sqlite3_changes(_db);
NSString *message = [NSString stringWithFormat:@"%d row(s) affected", rowsAffected];
result = _lastResult = [FLEXSQLResult message:message];
}
} else {
// An error occured executing the query
result = _lastResult = [self errorResult:@"Execution"];
}
} else {
// An error occurred creating the prepared statement
result = _lastResult = [self errorResult:@"Prepared statement"];
}
sqlite3_finalize(pstmt);
return result;
}
#pragma mark - Private
/// @return YES on success, NO if an error was encountered and stored in \c lastResult
- (BOOL)bindParameters:(NSDictionary *)args toStatement:(sqlite3_stmt *)pstmt {
for (NSString *param in args.allKeys) {
int status = SQLITE_OK, idx = sqlite3_bind_parameter_index(pstmt, param.UTF8String);
id value = args[param];
if (idx == 0) {
// No parameter matching that arg
@throw NSInternalInconsistencyException;
}
// Null
if ([value isKindOfClass:[NSNull class]]) {
status = sqlite3_bind_null(pstmt, idx);
}
// String params
else if ([value isKindOfClass:[NSString class]]) {
const char *str = [value UTF8String];
status = sqlite3_bind_text(pstmt, idx, str, (int)strlen(str), SQLITE_TRANSIENT);
}
// Data params
else if ([value isKindOfClass:[NSData class]]) {
const void *blob = [value bytes];
status = sqlite3_bind_blob64(pstmt, idx, blob, [value length], SQLITE_TRANSIENT);
}
// Primitive params
else if ([value isKindOfClass:[NSNumber class]]) {
FLEXTypeEncoding type = [value objCType][0];
switch (type) {
case FLEXTypeEncodingCBool:
case FLEXTypeEncodingChar:
case FLEXTypeEncodingUnsignedChar:
case FLEXTypeEncodingShort:
case FLEXTypeEncodingUnsignedShort:
case FLEXTypeEncodingInt:
case FLEXTypeEncodingUnsignedInt:
case FLEXTypeEncodingLong:
case FLEXTypeEncodingUnsignedLong:
case FLEXTypeEncodingLongLong:
case FLEXTypeEncodingUnsignedLongLong:
status = sqlite3_bind_int64(pstmt, idx, (sqlite3_int64)[value longValue]);
break;
case FLEXTypeEncodingFloat:
case FLEXTypeEncodingDouble:
status = sqlite3_bind_double(pstmt, idx, [value doubleValue]);
break;
default:
@throw NSInternalInconsistencyException;
break;
}
}
// Unsupported type
else {
@throw NSInternalInconsistencyException;
}
if (status != SQLITE_OK) {
return [self storeErrorForLastTask:
[NSString stringWithFormat:@"Binding param named '%@'", param]
];
}
}
return YES;
}
- (BOOL)storeErrorForLastTask:(NSString *)action {
_lastResult = [self errorResult:action];
return NO;
}
- (FLEXSQLResult *)errorResult:(NSString *)description {
const char *error = sqlite3_errmsg(_db);
NSString *message = error ? @(error) : [NSString
stringWithFormat:@"(%@: empty error)", description
];
return [FLEXSQLResult error:message];
}
- (id)objectForColumnIndex:(int)columnIdx stmt:(sqlite3_stmt*)stmt {
int columnType = sqlite3_column_type(stmt, columnIdx);
switch (columnType) {
case SQLITE_INTEGER:
return @(sqlite3_column_int64(stmt, columnIdx)).stringValue;
case SQLITE_FLOAT:
return @(sqlite3_column_double(stmt, columnIdx)).stringValue;
case SQLITE_BLOB:
return [NSString stringWithFormat:@"Data (%@ bytes)",
@([self dataForColumnIndex:columnIdx stmt:stmt].length)
];
default:
// Default to a string for everything else
return [self stringForColumnIndex:columnIdx stmt:stmt] ?: NSNull.null;
}
}
- (NSString *)stringForColumnIndex:(int)columnIdx stmt:(sqlite3_stmt *)stmt {
if (sqlite3_column_type(stmt, columnIdx) == SQLITE_NULL || columnIdx < 0) {
return nil;
}
const char *text = (const char *)sqlite3_column_text(stmt, columnIdx);
return text ? @(text) : nil;
}
- (NSData *)dataForColumnIndex:(int)columnIdx stmt:(sqlite3_stmt *)stmt {
if (sqlite3_column_type(stmt, columnIdx) == SQLITE_NULL || (columnIdx < 0)) {
return nil;
}
const void *blob = sqlite3_column_blob(stmt, columnIdx);
NSInteger size = (NSInteger)sqlite3_column_bytes(stmt, columnIdx);
return blob ? [NSData dataWithBytes:blob length:size] : nil;
}
@end

View File

@@ -0,0 +1,38 @@
//
// FLEXTableContentHeaderCell.h
// FLEX
//
// Created by Peng Tao on 15/11/26.
// Copyright © 2015年 f. All rights reserved.
//
#import <UIKit/UIKit.h>
typedef NS_ENUM(NSUInteger, FLEXTableColumnHeaderSortType) {
FLEXTableColumnHeaderSortTypeNone = 0,
FLEXTableColumnHeaderSortTypeAsc,
FLEXTableColumnHeaderSortTypeDesc,
};
NS_INLINE FLEXTableColumnHeaderSortType FLEXNextTableColumnHeaderSortType(
FLEXTableColumnHeaderSortType current) {
switch (current) {
case FLEXTableColumnHeaderSortTypeAsc:
return FLEXTableColumnHeaderSortTypeDesc;
case FLEXTableColumnHeaderSortTypeNone:
case FLEXTableColumnHeaderSortTypeDesc:
return FLEXTableColumnHeaderSortTypeAsc;
}
return FLEXTableColumnHeaderSortTypeNone;
}
@interface FLEXTableColumnHeader : UIView
@property (nonatomic) NSInteger index;
@property (nonatomic, readonly) UILabel *titleLabel;
@property (nonatomic) FLEXTableColumnHeaderSortType sortType;
@end

View File

@@ -0,0 +1,78 @@
//
// FLEXTableContentHeaderCell.m
// FLEX
//
// Created by Peng Tao on 15/11/26.
// Copyright © 2015 f. All rights reserved.
//
#import "FLEXTableColumnHeader.h"
#import "FLEXColor.h"
#import "UIFont+FLEX.h"
#import "FLEXUtility.h"
static const CGFloat kMargin = 5;
static const CGFloat kArrowWidth = 20;
@interface FLEXTableColumnHeader ()
@property (nonatomic, readonly) UILabel *arrowLabel;
@property (nonatomic, readonly) UIView *lineView;
@end
@implementation FLEXTableColumnHeader
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.backgroundColor = FLEXColor.secondaryBackgroundColor;
_titleLabel = [UILabel new];
_titleLabel.font = UIFont.flex_defaultTableCellFont;
[self addSubview:_titleLabel];
_arrowLabel = [UILabel new];
_arrowLabel.font = UIFont.flex_defaultTableCellFont;
[self addSubview:_arrowLabel];
_lineView = [UIView new];
_lineView.backgroundColor = FLEXColor.hairlineColor;
[self addSubview:_lineView];
}
return self;
}
- (void)setSortType:(FLEXTableColumnHeaderSortType)type {
_sortType = type;
switch (type) {
case FLEXTableColumnHeaderSortTypeNone:
_arrowLabel.text = @"";
break;
case FLEXTableColumnHeaderSortTypeAsc:
_arrowLabel.text = @"⬆️";
break;
case FLEXTableColumnHeaderSortTypeDesc:
_arrowLabel.text = @"⬇️";
break;
}
}
- (void)layoutSubviews {
[super layoutSubviews];
CGSize size = self.frame.size;
self.titleLabel.frame = CGRectMake(kMargin, 0, size.width - kArrowWidth - kMargin, size.height);
self.arrowLabel.frame = CGRectMake(size.width - kArrowWidth, 0, kArrowWidth, size.height);
self.lineView.frame = CGRectMake(size.width - 1, 2, FLEXPointsToPixels(1), size.height - 4);
}
- (CGSize)sizeThatFits:(CGSize)size {
CGFloat margins = kArrowWidth - 2 * kMargin;
size = CGSizeMake(size.width - margins, size.height);
CGFloat width = [_titleLabel sizeThatFits:size].width + margins;
return CGSizeMake(width, size.height);
}
@end

View File

@@ -0,0 +1,36 @@
//
// PTTableContentViewController.h
// PTDatabaseReader
//
// Created by Peng Tao on 15/11/23.
// Copyright © 2015年 Peng Tao. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "FLEXDatabaseManager.h"
NS_ASSUME_NONNULL_BEGIN
@interface FLEXTableContentViewController : UIViewController
/// Display a mutable table with the given columns, rows, and name.
///
/// @param columnNames self explanatory.
/// @param rowData an array of rows, where each row is an array of column data.
/// @param rowIDs an array of stringy row IDs. Required for deleting rows.
/// @param tableName an optional name of the table being viewed, if any. Enables adding rows.
/// @param databaseManager an optional manager to allow modifying the table.
/// Required for deleting rows. Required for adding rows if \c tableName is supplied.
+ (instancetype)columns:(NSArray<NSString *> *)columnNames
rows:(NSArray<NSArray<NSString *> *> *)rowData
rowIDs:(NSArray<NSString *> *)rowIDs
tableName:(NSString *)tableName
database:(id<FLEXDatabaseManager>)databaseManager;
/// Display an immutable table with the given columns and rows.
+ (instancetype)columns:(NSArray<NSString *> *)columnNames
rows:(NSArray<NSArray<NSString *> *> *)rowData;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,359 @@
//
// PTTableContentViewController.m
// PTDatabaseReader
//
// Created by Peng Tao on 15/11/23.
// Copyright © 2015 Peng Tao. All rights reserved.
//
#import "FLEXTableContentViewController.h"
#import "FLEXTableRowDataViewController.h"
#import "FLEXMultiColumnTableView.h"
#import "FLEXWebViewController.h"
#import "FLEXUtility.h"
#import "UIBarButtonItem+FLEX.h"
@interface FLEXTableContentViewController () <
FLEXMultiColumnTableViewDataSource, FLEXMultiColumnTableViewDelegate
>
@property (nonatomic, readonly) NSArray<NSString *> *columns;
@property (nonatomic) NSMutableArray<NSArray *> *rows;
@property (nonatomic, readonly) NSString *tableName;
@property (nonatomic, nullable) NSMutableArray<NSString *> *rowIDs;
@property (nonatomic, readonly, nullable) id<FLEXDatabaseManager> databaseManager;
@property (nonatomic, readonly) BOOL canRefresh;
@property (nonatomic) FLEXMultiColumnTableView *multiColumnView;
@end
@implementation FLEXTableContentViewController
+ (instancetype)columns:(NSArray<NSString *> *)columnNames
rows:(NSArray<NSArray<NSString *> *> *)rowData
rowIDs:(NSArray<NSString *> *)rowIDs
tableName:(NSString *)tableName
database:(id<FLEXDatabaseManager>)databaseManager {
return [[self alloc]
initWithColumns:columnNames
rows:rowData
rowIDs:rowIDs
tableName:tableName
database:databaseManager
];
}
+ (instancetype)columns:(NSArray<NSString *> *)cols
rows:(NSArray<NSArray<NSString *> *> *)rowData {
return [[self alloc] initWithColumns:cols rows:rowData rowIDs:nil tableName:nil database:nil];
}
- (instancetype)initWithColumns:(NSArray<NSString *> *)columnNames
rows:(NSArray<NSArray<NSString *> *> *)rowData
rowIDs:(nullable NSArray<NSString *> *)rowIDs
tableName:(nullable NSString *)tableName
database:(nullable id<FLEXDatabaseManager>)databaseManager {
// Must supply all optional parameters as one, or none
BOOL all = rowIDs && tableName && databaseManager;
BOOL none = !rowIDs && !tableName && !databaseManager;
NSParameterAssert(all || none);
self = [super init];
if (self) {
self->_columns = columnNames.copy;
self->_rows = rowData.mutableCopy;
self->_rowIDs = rowIDs.mutableCopy;
self->_tableName = tableName.copy;
self->_databaseManager = databaseManager;
}
return self;
}
- (void)loadView {
[super loadView];
[self.view addSubview:self.multiColumnView];
}
- (void)viewDidLoad {
[super viewDidLoad];
self.title = self.tableName;
[self.multiColumnView reloadData];
[self setupToolbarItems];
}
- (FLEXMultiColumnTableView *)multiColumnView {
if (!_multiColumnView) {
_multiColumnView = [[FLEXMultiColumnTableView alloc]
initWithFrame:FLEXRectSetSize(CGRectZero, self.view.frame.size)
];
_multiColumnView.dataSource = self;
_multiColumnView.delegate = self;
}
return _multiColumnView;
}
- (BOOL)canRefresh {
return self.databaseManager && self.tableName;
}
#pragma mark MultiColumnTableView DataSource
- (NSInteger)numberOfColumnsInTableView:(FLEXMultiColumnTableView *)tableView {
return self.columns.count;
}
- (NSInteger)numberOfRowsInTableView:(FLEXMultiColumnTableView *)tableView {
return self.rows.count;
}
- (NSString *)columnTitle:(NSInteger)column {
return self.columns[column];
}
- (NSString *)rowTitle:(NSInteger)row {
return @(row).stringValue;
}
- (NSArray *)contentForRow:(NSInteger)row {
return self.rows[row];
}
- (CGFloat)multiColumnTableView:(FLEXMultiColumnTableView *)tableView
heightForContentCellInRow:(NSInteger)row {
return 40;
}
- (CGFloat)multiColumnTableView:(FLEXMultiColumnTableView *)tableView
minWidthForContentCellInColumn:(NSInteger)column {
return 100;
}
- (CGFloat)heightForTopHeaderInTableView:(FLEXMultiColumnTableView *)tableView {
return 40;
}
- (CGFloat)widthForLeftHeaderInTableView:(FLEXMultiColumnTableView *)tableView {
NSString *str = [NSString stringWithFormat:@"%lu",(unsigned long)self.rows.count];
NSDictionary *attrs = @{ NSFontAttributeName : [UIFont systemFontOfSize:17.0] };
CGSize size = [str boundingRectWithSize:CGSizeMake(CGFLOAT_MAX, 14)
options:NSStringDrawingUsesLineFragmentOrigin
attributes:attrs context:nil
].size;
return size.width + 20;
}
#pragma mark MultiColumnTableView Delegate
- (void)multiColumnTableView:(FLEXMultiColumnTableView *)tableView didSelectRow:(NSInteger)row {
NSArray<NSString *> *fields = [self.rows[row] flex_mapped:^id(NSString *field, NSUInteger idx) {
return [NSString stringWithFormat:@"%@:\n%@", self.columns[idx], field];
}];
NSArray<NSString *> *values = [self.rows[row] flex_mapped:^id(NSString *value, NSUInteger idx) {
return [NSString stringWithFormat:@"'%@'", value];
}];
[FLEXAlert makeAlert:^(FLEXAlert *make) {
make.title([@"Row " stringByAppendingString:@(row).stringValue]);
NSString *message = [fields componentsJoinedByString:@"\n\n"];
make.message(message);
make.button(@"Copy").handler(^(NSArray<NSString *> *strings) {
UIPasteboard.generalPasteboard.string = message;
});
make.button(@"Copy as CSV").handler(^(NSArray<NSString *> *strings) {
UIPasteboard.generalPasteboard.string = [values componentsJoinedByString:@", "];
});
make.button(@"Focus on Row").handler(^(NSArray<NSString *> *strings) {
UIViewController *focusedRow = [FLEXTableRowDataViewController
rows:[NSDictionary dictionaryWithObjects:self.rows[row] forKeys:self.columns]
];
[self.navigationController pushViewController:focusedRow animated:YES];
});
// Option to delete row
BOOL hasRowID = self.rows.count && row < self.rows.count;
if (hasRowID && self.canRefresh) {
make.button(@"Delete").destructiveStyle().handler(^(NSArray<NSString *> *strings) {
NSString *deleteRow = [NSString stringWithFormat:
@"DELETE FROM %@ WHERE rowid = %@",
self.tableName, self.rowIDs[row]
];
[self executeStatementAndShowResult:deleteRow completion:^(BOOL success) {
// Remove deleted row and reload view
if (success) {
[self reloadTableDataFromDB];
}
}];
});
}
make.button(@"Dismiss").cancelStyle();
} showFrom:self];
}
- (void)multiColumnTableView:(FLEXMultiColumnTableView *)tableView
didSelectHeaderForColumn:(NSInteger)column
sortType:(FLEXTableColumnHeaderSortType)sortType {
NSArray<NSArray *> *sortContentData = [self.rows
sortedArrayWithOptions:NSSortStable
usingComparator:^NSComparisonResult(NSArray *obj1, NSArray *obj2) {
id a = obj1[column], b = obj2[column];
if (a == NSNull.null) {
return NSOrderedAscending;
}
if (b == NSNull.null) {
return NSOrderedDescending;
}
if ([a respondsToSelector:@selector(compare:options:)] &&
[b respondsToSelector:@selector(compare:options:)]) {
return [a compare:b options:NSNumericSearch];
}
if ([a respondsToSelector:@selector(compare:)] && [b respondsToSelector:@selector(compare:)]) {
return [a compare:b];
}
return NSOrderedSame;
}
];
if (sortType == FLEXTableColumnHeaderSortTypeDesc) {
sortContentData = sortContentData.reverseObjectEnumerator.allObjects.copy;
}
self.rows = sortContentData.mutableCopy;
[self.multiColumnView reloadData];
}
#pragma mark - About Transition
- (void)willTransitionToTraitCollection:(UITraitCollection *)newCollection
withTransitionCoordinator:(id <UIViewControllerTransitionCoordinator>)coordinator {
[super willTransitionToTraitCollection:newCollection withTransitionCoordinator:coordinator];
[coordinator animateAlongsideTransition:^(id <UIViewControllerTransitionCoordinatorContext> context) {
if (newCollection.verticalSizeClass == UIUserInterfaceSizeClassCompact) {
self.multiColumnView.frame = CGRectMake(0, 32, self.view.frame.size.width, self.view.frame.size.height - 32);
}
else {
self.multiColumnView.frame = CGRectMake(0, 64, self.view.frame.size.width, self.view.frame.size.height - 64);
}
[self.view setNeedsLayout];
} completion:nil];
}
#pragma mark - Toolbar
- (void)setupToolbarItems {
// We do not support modifying realm databases
if (![self.databaseManager respondsToSelector:@selector(executeStatement:)]) {
return;
}
UIBarButtonItem *trashButton = FLEXBarButtonItemSystem(Trash, self, @selector(trashPressed));
UIBarButtonItem *addButton = FLEXBarButtonItemSystem(Add, self, @selector(addPressed));
// Only allow adding rows or deleting rows if we have a table name
trashButton.enabled = self.canRefresh;
addButton.enabled = self.canRefresh;
self.toolbarItems = @[
UIBarButtonItem.flex_flexibleSpace,
addButton,
UIBarButtonItem.flex_flexibleSpace,
[trashButton flex_withTintColor:UIColor.redColor],
];
}
- (void)trashPressed {
NSParameterAssert(self.tableName);
[FLEXAlert makeAlert:^(FLEXAlert *make) {
make.title(@"Delete All Rows");
make.message(@"All rows in this table will be permanently deleted.\nDo you want to proceed?");
make.button(@"Yes, I'm sure").destructiveStyle().handler(^(NSArray<NSString *> *strings) {
NSString *deleteAll = [NSString stringWithFormat:@"DELETE FROM %@", self.tableName];
[self executeStatementAndShowResult:deleteAll completion:^(BOOL success) {
// Only dismiss on success
if (success) {
[self.navigationController popViewControllerAnimated:YES];
}
}];
});
make.button(@"Cancel").cancelStyle();
} showFrom:self];
}
- (void)addPressed {
NSParameterAssert(self.tableName);
[FLEXAlert makeAlert:^(FLEXAlert *make) {
make.title(@"Add a New Row");
make.message(@"Comma separate values to use in an INSERT statement.\n\n");
make.message(@"INSERT INTO [table] VALUES (your_input)");
make.textField(@"5, 'John Smith', 14,...");
make.button(@"Insert").handler(^(NSArray<NSString *> *strings) {
NSString *statement = [NSString stringWithFormat:
@"INSERT INTO %@ VALUES (%@)", self.tableName, strings[0]
];
[self executeStatementAndShowResult:statement completion:^(BOOL success) {
if (success) {
[self reloadTableDataFromDB];
}
}];
});
make.button(@"Cancel").cancelStyle();
} showFrom:self];
}
#pragma mark - Helpers
- (void)executeStatementAndShowResult:(NSString *)statement
completion:(void (^_Nullable)(BOOL success))completion {
NSParameterAssert(self.databaseManager);
FLEXSQLResult *result = [self.databaseManager executeStatement:statement];
[FLEXAlert makeAlert:^(FLEXAlert *make) {
if (result.isError) {
make.title(@"Error");
}
make.message(result.message ?: @"<no output>");
make.button(@"Dismiss").cancelStyle().handler(^(NSArray<NSString *> *_) {
if (completion) {
completion(!result.isError);
}
});
} showFrom:self];
}
- (void)reloadTableDataFromDB {
if (!self.canRefresh) {
return;
}
NSArray<NSArray *> *rows = [self.databaseManager queryAllDataInTable:self.tableName];
NSArray<NSString *> *rowIDs = nil;
if ([self.databaseManager respondsToSelector:@selector(queryRowIDsInTable:)]) {
rowIDs = [self.databaseManager queryRowIDsInTable:self.tableName];
}
self.rows = rows.mutableCopy;
self.rowIDs = rowIDs.mutableCopy;
[self.multiColumnView reloadData];
}
@end

View File

@@ -0,0 +1,17 @@
//
// FLEXTableLeftCell.h
// FLEX
//
// Created by Peng Tao on 15/11/24.
// Copyright © 2015年 f. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface FLEXTableLeftCell : UITableViewCell
@property (nonatomic) UILabel *titlelabel;
+ (instancetype)cellWithTableView:(UITableView *)tableView;
@end

View File

@@ -0,0 +1,33 @@
//
// FLEXTableLeftCell.m
// FLEX
//
// Created by Peng Tao on 15/11/24.
// Copyright © 2015 f. All rights reserved.
//
#import "FLEXTableLeftCell.h"
@implementation FLEXTableLeftCell
+ (instancetype)cellWithTableView:(UITableView *)tableView {
static NSString *identifier = @"FLEXTableLeftCell";
FLEXTableLeftCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
if (!cell) {
cell = [[FLEXTableLeftCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier];
UILabel *textLabel = [UILabel new];
textLabel.textAlignment = NSTextAlignmentCenter;
textLabel.font = [UIFont systemFontOfSize:13.0];
[cell.contentView addSubview:textLabel];
cell.titlelabel = textLabel;
}
return cell;
}
- (void)layoutSubviews {
[super layoutSubviews];
self.titlelabel.frame = self.contentView.frame;
}
@end

View File

@@ -0,0 +1,16 @@
//
// PTTableListViewController.h
// PTDatabaseReader
//
// Created by Peng Tao on 15/11/23.
// Copyright © 2015年 Peng Tao. All rights reserved.
//
#import "FLEXFilteringTableViewController.h"
@interface FLEXTableListViewController : FLEXFilteringTableViewController
+ (BOOL)supportsExtension:(NSString *)extension;
- (instancetype)initWithPath:(NSString *)path;
@end

View File

@@ -0,0 +1,165 @@
//
// PTTableListViewController.m
// PTDatabaseReader
//
// Created by Peng Tao on 15/11/23.
// Copyright © 2015 Peng Tao. All rights reserved.
//
#import "FLEXTableListViewController.h"
#import "FLEXDatabaseManager.h"
#import "FLEXSQLiteDatabaseManager.h"
#import "FLEXRealmDatabaseManager.h"
#import "FLEXTableContentViewController.h"
#import "FLEXMutableListSection.h"
#import "NSArray+FLEX.h"
#import "FLEXAlert.h"
#import "FLEXMacros.h"
@interface FLEXTableListViewController ()
@property (nonatomic, readonly) id<FLEXDatabaseManager> dbm;
@property (nonatomic, readonly) NSString *path;
@property (nonatomic, readonly) FLEXMutableListSection<NSString *> *tables;
+ (NSArray<NSString *> *)supportedSQLiteExtensions;
+ (NSArray<NSString *> *)supportedRealmExtensions;
@end
@implementation FLEXTableListViewController
- (instancetype)initWithPath:(NSString *)path {
self = [super initWithStyle:UITableViewStyleGrouped];
if (self) {
_path = path.copy;
_dbm = [self databaseManagerForFileAtPath:path];
}
return self;
}
- (void)viewDidLoad {
[super viewDidLoad];
self.showsSearchBar = YES;
// Compose query button //
UIBarButtonItem *composeQuery = [[UIBarButtonItem alloc]
initWithBarButtonSystemItem:UIBarButtonSystemItemCompose
target:self
action:@selector(queryButtonPressed)
];
// Cannot run custom queries on realm databases
composeQuery.enabled = [self.dbm
respondsToSelector:@selector(executeStatement:)
];
[self addToolbarItems:@[composeQuery]];
}
- (NSArray<FLEXTableViewSection *> *)makeSections {
_tables = [FLEXMutableListSection list:[self.dbm queryAllTables]
cellConfiguration:^(__kindof UITableViewCell *cell, NSString *tableName, NSInteger row) {
cell.textLabel.text = tableName;
} filterMatcher:^BOOL(NSString *filterText, NSString *tableName) {
return [tableName localizedCaseInsensitiveContainsString:filterText];
}
];
self.tables.selectionHandler = ^(FLEXTableListViewController *host, NSString *tableName) {
NSArray *rows = [host.dbm queryAllDataInTable:tableName];
NSArray *columns = [host.dbm queryAllColumnsOfTable:tableName];
NSArray *rowIDs = nil;
if ([host.dbm respondsToSelector:@selector(queryRowIDsInTable:)]) {
rowIDs = [host.dbm queryRowIDsInTable:tableName];
}
UIViewController *resultsScreen = [FLEXTableContentViewController
columns:columns rows:rows rowIDs:rowIDs tableName:tableName database:host.dbm
];
[host.navigationController pushViewController:resultsScreen animated:YES];
};
return @[self.tables];
}
- (void)reloadData {
self.tables.customTitle = [NSString
stringWithFormat:@"Tables (%@)", @(self.tables.filteredList.count)
];
[super reloadData];
}
- (void)queryButtonPressed {
FLEXSQLiteDatabaseManager *database = self.dbm;
[FLEXAlert makeAlert:^(FLEXAlert *make) {
make.title(@"Execute an SQL query");
make.textField(nil);
make.button(@"Run").handler(^(NSArray<NSString *> *strings) {
FLEXSQLResult *result = [database executeStatement:strings[0]];
if (result.message) {
[FLEXAlert showAlert:@"Message" message:result.message from:self];
} else {
UIViewController *resultsScreen = [FLEXTableContentViewController
columns:result.columns rows:result.rows
];
[self.navigationController pushViewController:resultsScreen animated:YES];
}
});
make.button(@"Cancel").cancelStyle();
} showFrom:self];
}
- (id<FLEXDatabaseManager>)databaseManagerForFileAtPath:(NSString *)path {
NSString *pathExtension = path.pathExtension.lowercaseString;
NSArray<NSString *> *sqliteExtensions = FLEXTableListViewController.supportedSQLiteExtensions;
if ([sqliteExtensions indexOfObject:pathExtension] != NSNotFound) {
return [FLEXSQLiteDatabaseManager managerForDatabase:path];
}
NSArray<NSString *> *realmExtensions = FLEXTableListViewController.supportedRealmExtensions;
if (realmExtensions != nil && [realmExtensions indexOfObject:pathExtension] != NSNotFound) {
return [FLEXRealmDatabaseManager managerForDatabase:path];
}
return nil;
}
#pragma mark - FLEXTableListViewController
+ (BOOL)supportsExtension:(NSString *)extension {
extension = extension.lowercaseString;
NSArray<NSString *> *sqliteExtensions = FLEXTableListViewController.supportedSQLiteExtensions;
if (sqliteExtensions.count > 0 && [sqliteExtensions indexOfObject:extension] != NSNotFound) {
return YES;
}
NSArray<NSString *> *realmExtensions = FLEXTableListViewController.supportedRealmExtensions;
if (realmExtensions.count > 0 && [realmExtensions indexOfObject:extension] != NSNotFound) {
return YES;
}
return NO;
}
+ (NSArray<NSString *> *)supportedSQLiteExtensions {
return @[@"db", @"sqlite", @"sqlite3"];
}
+ (NSArray<NSString *> *)supportedRealmExtensions {
if (NSClassFromString(@"RLMRealm") == nil) {
return nil;
}
return @[@"realm"];
}
@end

View File

@@ -0,0 +1,14 @@
//
// FLEXTableRowDataViewController.h
// FLEX
//
// Created by Chaoshuai Lu on 7/8/20.
//
#import "FLEXFilteringTableViewController.h"
@interface FLEXTableRowDataViewController : FLEXFilteringTableViewController
+ (instancetype)rows:(NSDictionary<NSString *, id> *)rowData;
@end

View File

@@ -0,0 +1,54 @@
//
// FLEXTableRowDataViewController.m
// FLEX
//
// Created by Chaoshuai Lu on 7/8/20.
//
#import "FLEXTableRowDataViewController.h"
#import "FLEXMutableListSection.h"
#import "FLEXAlert.h"
@interface FLEXTableRowDataViewController ()
@property (nonatomic) NSDictionary<NSString *, NSString *> *rowsByColumn;
@end
@implementation FLEXTableRowDataViewController
#pragma mark - Initialization
+ (instancetype)rows:(NSDictionary<NSString *, id> *)rowData {
FLEXTableRowDataViewController *controller = [self new];
controller.rowsByColumn = rowData;
return controller;
}
#pragma mark - Overrides
- (NSArray<FLEXTableViewSection *> *)makeSections {
NSDictionary<NSString *, NSString *> *rowsByColumn = self.rowsByColumn;
FLEXMutableListSection<NSString *> *section = [FLEXMutableListSection list:self.rowsByColumn.allKeys
cellConfiguration:^(UITableViewCell *cell, NSString *column, NSInteger row) {
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
cell.textLabel.text = column;
cell.detailTextLabel.text = rowsByColumn[column].description;
} filterMatcher:^BOOL(NSString *filterText, NSString *column) {
return [column localizedCaseInsensitiveContainsString:filterText] ||
[rowsByColumn[column] localizedCaseInsensitiveContainsString:filterText];
}
];
section.selectionHandler = ^(UIViewController *host, NSString *column) {
UIPasteboard.generalPasteboard.string = rowsByColumn[column].description;
[FLEXAlert makeAlert:^(FLEXAlert *make) {
make.title(@"Column Copied to Clipboard");
make.message(rowsByColumn[column].description);
make.button(@"Dismiss").cancelStyle();
} showFrom:host];
};
return @[section];
}
@end

View File

@@ -0,0 +1,21 @@
FMDB
Copyright (c) 2008-2014 Flying Meat Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,13 @@
//
// FLEXAddressExplorerCoordinator.h
// FLEX
//
// Created by Tanner Bennett on 7/10/19.
// Copyright © 2020 FLEX Team. All rights reserved.
//
#import "FLEXGlobalsEntry.h"
@interface FLEXAddressExplorerCoordinator : NSObject <FLEXGlobalsEntry>
@end

View File

@@ -0,0 +1,95 @@
//
// FLEXAddressExplorerCoordinator.m
// FLEX
//
// Created by Tanner Bennett on 7/10/19.
// Copyright © 2020 FLEX Team. All rights reserved.
//
#import "FLEXAddressExplorerCoordinator.h"
#import "FLEXGlobalsViewController.h"
#import "FLEXObjectExplorerFactory.h"
#import "FLEXObjectExplorerViewController.h"
#import "FLEXRuntimeUtility.h"
#import "FLEXUtility.h"
@interface UITableViewController (FLEXAddressExploration)
- (void)deselectSelectedRow;
- (void)tryExploreAddress:(NSString *)addressString safely:(BOOL)safely;
@end
@implementation FLEXAddressExplorerCoordinator
#pragma mark - FLEXGlobalsEntry
+ (NSString *)globalsEntryTitle:(FLEXGlobalsRow)row {
return @"🔎 Address Explorer";
}
+ (FLEXGlobalsEntryRowAction)globalsEntryRowAction:(FLEXGlobalsRow)row {
return ^(UITableViewController *host) {
NSString *title = @"Explore Object at Address";
NSString *message = @"Paste a hexadecimal address below, starting with '0x'. "
"Use the unsafe option if you need to bypass pointer validation, "
"but know that it may crash the app if the address is invalid.";
[FLEXAlert makeAlert:^(FLEXAlert *make) {
make.title(title).message(message);
make.configuredTextField(^(UITextField *textField) {
NSString *copied = UIPasteboard.generalPasteboard.string;
textField.placeholder = @"0x00000070deadbeef";
// Go ahead and paste our clipboard if we have an address copied
if ([copied hasPrefix:@"0x"]) {
textField.text = copied;
[textField selectAll:nil];
}
});
make.button(@"Explore").handler(^(NSArray<NSString *> *strings) {
[host tryExploreAddress:strings.firstObject safely:YES];
});
make.button(@"Unsafe Explore").destructiveStyle().handler(^(NSArray *strings) {
[host tryExploreAddress:strings.firstObject safely:NO];
});
make.button(@"Cancel").cancelStyle();
} showFrom:host];
};
}
@end
@implementation UITableViewController (FLEXAddressExploration)
- (void)deselectSelectedRow {
NSIndexPath *selected = self.tableView.indexPathForSelectedRow;
[self.tableView deselectRowAtIndexPath:selected animated:YES];
}
- (void)tryExploreAddress:(NSString *)addressString safely:(BOOL)safely {
NSScanner *scanner = [NSScanner scannerWithString:addressString];
unsigned long long hexValue = 0;
BOOL didParseAddress = [scanner scanHexLongLong:&hexValue];
const void *pointerValue = (void *)hexValue;
NSString *error = nil;
if (didParseAddress) {
if (safely && ![FLEXRuntimeUtility pointerIsValidObjcObject:pointerValue]) {
error = @"The given address is unlikely to be a valid object.";
}
} else {
error = @"Malformed address. Make sure it's not too long and starts with '0x'.";
}
if (!error) {
id object = (__bridge id)pointerValue;
FLEXObjectExplorerViewController *explorer = [FLEXObjectExplorerFactory explorerViewControllerForObject:object];
[self.navigationController pushViewController:explorer animated:YES];
} else {
[FLEXAlert showAlert:@"Uh-oh" message:error from:self];
[self deselectSelectedRow];
}
}
@end

View File

@@ -0,0 +1,14 @@
//
// FLEXCookiesViewController.h
// FLEX
//
// Created by Rich Robinson on 19/10/2015.
// Copyright © 2020 FLEX Team. All rights reserved.
//
#import "FLEXGlobalsEntry.h"
#import "FLEXFilteringTableViewController.h"
@interface FLEXCookiesViewController : FLEXFilteringTableViewController <FLEXGlobalsEntry>
@end

View File

@@ -0,0 +1,76 @@
//
// FLEXCookiesViewController.m
// FLEX
//
// Created by Rich Robinson on 19/10/2015.
// Copyright © 2020 FLEX Team. All rights reserved.
//
#import "FLEXCookiesViewController.h"
#import "FLEXObjectExplorerFactory.h"
#import "FLEXMutableListSection.h"
#import "FLEXUtility.h"
@interface FLEXCookiesViewController ()
@property (nonatomic, readonly) FLEXMutableListSection<NSHTTPCookie *> *cookies;
@property (nonatomic) NSString *headerTitle;
@end
@implementation FLEXCookiesViewController
#pragma mark - Overrides
- (void)viewDidLoad {
[super viewDidLoad];
self.title = @"Cookies";
}
- (NSArray<FLEXTableViewSection *> *)makeSections {
NSSortDescriptor *nameSortDescriptor = [[NSSortDescriptor alloc]
initWithKey:@"name" ascending:YES selector:@selector(caseInsensitiveCompare:)
];
NSArray *cookies = [NSHTTPCookieStorage.sharedHTTPCookieStorage.cookies
sortedArrayUsingDescriptors:@[nameSortDescriptor]
];
_cookies = [FLEXMutableListSection list:cookies
cellConfiguration:^(UITableViewCell *cell, NSHTTPCookie *cookie, NSInteger row) {
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
cell.textLabel.text = [cookie.name stringByAppendingFormat:@" (%@)", cookie.value];
cell.detailTextLabel.text = [cookie.domain stringByAppendingFormat:@" — %@", cookie.path];
} filterMatcher:^BOOL(NSString *filterText, NSHTTPCookie *cookie) {
return [cookie.name localizedCaseInsensitiveContainsString:filterText] ||
[cookie.value localizedCaseInsensitiveContainsString:filterText] ||
[cookie.domain localizedCaseInsensitiveContainsString:filterText] ||
[cookie.path localizedCaseInsensitiveContainsString:filterText];
}
];
self.cookies.selectionHandler = ^(UIViewController *host, NSHTTPCookie *cookie) {
[host.navigationController pushViewController:[
FLEXObjectExplorerFactory explorerViewControllerForObject:cookie
] animated:YES];
};
return @[self.cookies];
}
- (void)reloadData {
self.headerTitle = [NSString stringWithFormat:
@"%@ cookies", @(self.cookies.filteredList.count)
];
[super reloadData];
}
#pragma mark - FLEXGlobalsEntry
+ (NSString *)globalsEntryTitle:(FLEXGlobalsRow)row {
return @"🍪 Cookies";
}
+ (UIViewController *)globalsEntryViewController:(FLEXGlobalsRow)row {
return [self new];
}
@end

View File

@@ -0,0 +1,14 @@
//
// FLEXLiveObjectsController.h
// Flipboard
//
// Created by Ryan Olson on 5/28/14.
// Copyright (c) 2020 FLEX Team. All rights reserved.
//
#import "FLEXTableViewController.h"
#import "FLEXGlobalsEntry.h"
@interface FLEXLiveObjectsController : FLEXTableViewController <FLEXGlobalsEntry>
@end

View File

@@ -0,0 +1,236 @@
//
// FLEXLiveObjectsController.m
// Flipboard
//
// Created by Ryan Olson on 5/28/14.
// Copyright (c) 2020 FLEX Team. All rights reserved.
//
#import "FLEXLiveObjectsController.h"
#import "FLEXHeapEnumerator.h"
#import "FLEXObjectListViewController.h"
#import "FLEXUtility.h"
#import "FLEXScopeCarousel.h"
#import "FLEXTableView.h"
#import <objc/runtime.h>
static const NSInteger kFLEXLiveObjectsSortAlphabeticallyIndex = 0;
static const NSInteger kFLEXLiveObjectsSortByCountIndex = 1;
static const NSInteger kFLEXLiveObjectsSortBySizeIndex = 2;
@interface FLEXLiveObjectsController ()
@property (nonatomic) NSDictionary<NSString *, NSNumber *> *instanceCountsForClassNames;
@property (nonatomic) NSDictionary<NSString *, NSNumber *> *instanceSizesForClassNames;
@property (nonatomic, readonly) NSArray<NSString *> *allClassNames;
@property (nonatomic) NSArray<NSString *> *filteredClassNames;
@property (nonatomic) NSString *headerTitle;
@end
@implementation FLEXLiveObjectsController
- (void)viewDidLoad {
[super viewDidLoad];
self.showsSearchBar = YES;
self.showSearchBarInitially = YES;
self.activatesSearchBarAutomatically = YES;
self.searchBarDebounceInterval = kFLEXDebounceInstant;
self.showsCarousel = YES;
self.carousel.items = @[@"A→Z", @"Count", @"Size"];
self.refreshControl = [UIRefreshControl new];
[self.refreshControl addTarget:self action:@selector(refreshControlDidRefresh:) forControlEvents:UIControlEventValueChanged];
[self reloadTableData];
}
- (NSArray<NSString *> *)allClassNames {
return self.instanceCountsForClassNames.allKeys;
}
- (void)reloadTableData {
// Set up a CFMutableDictionary with class pointer keys and NSUInteger values.
// We abuse CFMutableDictionary a little to have primitive keys through judicious casting, but it gets the job done.
// The dictionary is intialized with a 0 count for each class so that it doesn't have to expand during enumeration.
// While it might be a little cleaner to populate an NSMutableDictionary with class name string keys to NSNumber counts,
// we choose the CF/primitives approach because it lets us enumerate the objects in the heap without allocating any memory during enumeration.
// The alternative of creating one NSString/NSNumber per object on the heap ends up polluting the count of live objects quite a bit.
unsigned int classCount = 0;
Class *classes = objc_copyClassList(&classCount);
CFMutableDictionaryRef mutableCountsForClasses = CFDictionaryCreateMutable(NULL, classCount, NULL, NULL);
for (unsigned int i = 0; i < classCount; i++) {
CFDictionarySetValue(mutableCountsForClasses, (__bridge const void *)classes[i], (const void *)0);
}
// Enumerate all objects on the heap to build the counts of instances for each class.
[FLEXHeapEnumerator enumerateLiveObjectsUsingBlock:^(__unsafe_unretained id object, __unsafe_unretained Class actualClass) {
NSUInteger instanceCount = (NSUInteger)CFDictionaryGetValue(mutableCountsForClasses, (__bridge const void *)actualClass);
instanceCount++;
CFDictionarySetValue(mutableCountsForClasses, (__bridge const void *)actualClass, (const void *)instanceCount);
}];
// Convert our CF primitive dictionary into a nicer mapping of class name strings to counts that we will use as the table's model.
NSMutableDictionary<NSString *, NSNumber *> *mutableCountsForClassNames = [NSMutableDictionary new];
NSMutableDictionary<NSString *, NSNumber *> *mutableSizesForClassNames = [NSMutableDictionary new];
for (unsigned int i = 0; i < classCount; i++) {
Class class = classes[i];
NSUInteger instanceCount = (NSUInteger)CFDictionaryGetValue(mutableCountsForClasses, (__bridge const void *)(class));
NSString *className = @(class_getName(class));
if (instanceCount > 0) {
[mutableCountsForClassNames setObject:@(instanceCount) forKey:className];
}
[mutableSizesForClassNames setObject:@(class_getInstanceSize(class)) forKey:className];
}
free(classes);
self.instanceCountsForClassNames = mutableCountsForClassNames;
self.instanceSizesForClassNames = mutableSizesForClassNames;
[self updateSearchResults:nil];
}
- (void)refreshControlDidRefresh:(id)sender {
[self reloadTableData];
[self.refreshControl endRefreshing];
}
- (void)updateHeaderTitle {
NSUInteger totalCount = 0;
NSUInteger totalSize = 0;
for (NSString *className in self.allClassNames) {
NSUInteger count = self.instanceCountsForClassNames[className].unsignedIntegerValue;
totalCount += count;
totalSize += count * self.instanceSizesForClassNames[className].unsignedIntegerValue;
}
NSUInteger filteredCount = 0;
NSUInteger filteredSize = 0;
for (NSString *className in self.filteredClassNames) {
NSUInteger count = self.instanceCountsForClassNames[className].unsignedIntegerValue;
filteredCount += count;
filteredSize += count * self.instanceSizesForClassNames[className].unsignedIntegerValue;
}
if (filteredCount == totalCount) {
// Unfiltered
self.headerTitle = [NSString
stringWithFormat:@"%@ objects, %@",
@(totalCount), [NSByteCountFormatter
stringFromByteCount:totalSize
countStyle:NSByteCountFormatterCountStyleFile
]
];
} else {
self.headerTitle = [NSString
stringWithFormat:@"%@ of %@ objects, %@",
@(filteredCount), @(totalCount), [NSByteCountFormatter
stringFromByteCount:filteredSize
countStyle:NSByteCountFormatterCountStyleFile
]
];
}
}
#pragma mark - FLEXGlobalsEntry
+ (NSString *)globalsEntryTitle:(FLEXGlobalsRow)row {
return @"💩 Heap Objects";
}
+ (UIViewController *)globalsEntryViewController:(FLEXGlobalsRow)row {
FLEXLiveObjectsController *liveObjectsViewController = [self new];
liveObjectsViewController.title = [self globalsEntryTitle:row];
return liveObjectsViewController;
}
#pragma mark - Search bar
- (void)updateSearchResults:(NSString *)filter {
NSInteger selectedScope = self.selectedScope;
if (filter.length) {
NSPredicate *searchPredicate = [NSPredicate predicateWithFormat:@"SELF CONTAINS[cd] %@", filter];
self.filteredClassNames = [self.allClassNames filteredArrayUsingPredicate:searchPredicate];
} else {
self.filteredClassNames = self.allClassNames;
}
if (selectedScope == kFLEXLiveObjectsSortAlphabeticallyIndex) {
self.filteredClassNames = [self.filteredClassNames sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)];
} else if (selectedScope == kFLEXLiveObjectsSortByCountIndex) {
self.filteredClassNames = [self.filteredClassNames sortedArrayUsingComparator:^NSComparisonResult(NSString *className1, NSString *className2) {
NSNumber *count1 = self.instanceCountsForClassNames[className1];
NSNumber *count2 = self.instanceCountsForClassNames[className2];
// Reversed for descending counts.
return [count2 compare:count1];
}];
} else if (selectedScope == kFLEXLiveObjectsSortBySizeIndex) {
self.filteredClassNames = [self.filteredClassNames sortedArrayUsingComparator:^NSComparisonResult(NSString *className1, NSString *className2) {
NSNumber *count1 = self.instanceCountsForClassNames[className1];
NSNumber *count2 = self.instanceCountsForClassNames[className2];
NSNumber *size1 = self.instanceSizesForClassNames[className1];
NSNumber *size2 = self.instanceSizesForClassNames[className2];
// Reversed for descending sizes.
return [@(count2.integerValue * size2.integerValue) compare:@(count1.integerValue * size1.integerValue)];
}];
}
[self updateHeaderTitle];
[self.tableView reloadData];
}
#pragma mark - Table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.filteredClassNames.count;
}
- (UITableViewCell *)tableView:(__kindof UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView
dequeueReusableCellWithIdentifier:kFLEXDefaultCell
forIndexPath:indexPath
];
NSString *className = self.filteredClassNames[indexPath.row];
NSNumber *count = self.instanceCountsForClassNames[className];
NSNumber *size = self.instanceSizesForClassNames[className];
unsigned long totalSize = count.unsignedIntegerValue * size.unsignedIntegerValue;
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
cell.textLabel.text = [NSString stringWithFormat:@"%@ (%ld, %@)",
className, (long)[count integerValue],
[NSByteCountFormatter
stringFromByteCount:totalSize
countStyle:NSByteCountFormatterCountStyleFile
]
];
return cell;
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
return self.headerTitle;
}
#pragma mark - Table view delegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
NSString *className = self.filteredClassNames[indexPath.row];
UIViewController *instances = [FLEXObjectListViewController
instancesOfClassWithName:className
retained:YES
];
[self.navigationController pushViewController:instances animated:YES];
}
@end

View File

@@ -0,0 +1,19 @@
//
// FLEXObjectListViewController.h
// Flipboard
//
// Created by Ryan Olson on 5/28/14.
// Copyright (c) 2020 FLEX Team. All rights reserved.
//
#import "FLEXFilteringTableViewController.h"
@interface FLEXObjectListViewController : FLEXFilteringTableViewController
/// This will either return a list of the instances, or take you straight
/// to the explorer itself if there is only one instance.
+ (UIViewController *)instancesOfClassWithName:(NSString *)className retained:(BOOL)retain;
+ (instancetype)subclassesOfClassWithName:(NSString *)className;
+ (instancetype)objectsWithReferencesToObject:(id)object retained:(BOOL)retain;
@end

View File

@@ -0,0 +1,250 @@
//
// FLEXObjectListViewController.m
// Flipboard
//
// Created by Ryan Olson on 5/28/14.
// Copyright (c) 2020 FLEX Team. All rights reserved.
//
#import "FLEXObjectListViewController.h"
#import "FLEXObjectExplorerFactory.h"
#import "FLEXObjectExplorerViewController.h"
#import "FLEXMutableListSection.h"
#import "FLEXRuntimeUtility.h"
#import "FLEXUtility.h"
#import "FLEXHeapEnumerator.h"
#import "FLEXObjectRef.h"
#import "NSString+FLEX.h"
#import "NSObject+FLEX_Reflection.h"
#import "FLEXTableViewCell.h"
#import <malloc/malloc.h>
typedef NS_ENUM(NSUInteger, FLEXObjectReferenceSection) {
FLEXObjectReferenceSectionMain,
FLEXObjectReferenceSectionAutoLayout,
FLEXObjectReferenceSectionKVO,
FLEXObjectReferenceSectionFLEX,
FLEXObjectReferenceSectionCount
};
@interface FLEXObjectListViewController ()
@property (nonatomic, readonly, class) NSArray<NSPredicate *> *defaultPredicates;
@property (nonatomic, readonly, class) NSArray<NSString *> *defaultSectionTitles;
@property (nonatomic, copy) NSArray<FLEXMutableListSection *> *sections;
@property (nonatomic, copy) NSArray<FLEXMutableListSection *> *allSections;
@property (nonatomic, readonly, nullable) NSArray<FLEXObjectRef *> *references;
@property (nonatomic, readonly) NSArray<NSPredicate *> *predicates;
@property (nonatomic, readonly) NSArray<NSString *> *sectionTitles;
@end
@implementation FLEXObjectListViewController
@dynamic sections, allSections;
#pragma mark - Reference Grouping
+ (NSPredicate *)defaultPredicateForSection:(NSInteger)section {
// These are the types of references that we typically don't care about.
// We want this list of "object-ivar pairs" split into two sections.
BOOL(^isKVORelated)(FLEXObjectRef *, NSDictionary *) = ^BOOL(FLEXObjectRef *ref, NSDictionary *bindings) {
NSString *row = ref.reference;
return [row isEqualToString:@"__NSObserver object"] ||
[row isEqualToString:@"_CFXNotificationObjcObserverRegistration _object"];
};
/// These are common AutoLayout related references we also rarely care about.
BOOL(^isConstraintRelated)(FLEXObjectRef *, NSDictionary *) = ^BOOL(FLEXObjectRef *ref, NSDictionary *bindings) {
static NSSet *ignored = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
ignored = [NSSet setWithArray:@[
@"NSLayoutConstraint _container",
@"NSContentSizeLayoutConstraint _container",
@"NSAutoresizingMaskLayoutConstraint _container",
@"MASViewConstraint _installedView",
@"MASLayoutConstraint _container",
@"MASViewAttribute _view"
]];
});
NSString *row = ref.reference;
return ([row hasPrefix:@"NSLayout"] && [row hasSuffix:@" _referenceItem"]) ||
([row hasPrefix:@"NSIS"] && [row hasSuffix:@" _delegate"]) ||
([row hasPrefix:@"_NSAutoresizingMask"] && [row hasSuffix:@" _referenceItem"]) ||
[ignored containsObject:row];
};
/// These are FLEX classes and usually you aren't looking for FLEX references inside FLEX itself
BOOL(^isFLEXClass)(FLEXObjectRef *, NSDictionary *) = ^BOOL(FLEXObjectRef *ref, NSDictionary *bindings) {
return [ref.reference hasPrefix:@"FLEX"];
};
BOOL(^isEssential)(FLEXObjectRef *, NSDictionary *) = ^BOOL(FLEXObjectRef *ref, NSDictionary *bindings) {
return !(
isKVORelated(ref, bindings) ||
isConstraintRelated(ref, bindings) ||
isFLEXClass(ref, bindings)
);
};
switch (section) {
case FLEXObjectReferenceSectionMain:
return [NSPredicate predicateWithBlock:isEssential];
case FLEXObjectReferenceSectionAutoLayout:
return [NSPredicate predicateWithBlock:isConstraintRelated];
case FLEXObjectReferenceSectionKVO:
return [NSPredicate predicateWithBlock:isKVORelated];
case FLEXObjectReferenceSectionFLEX:
return [NSPredicate predicateWithBlock:isFLEXClass];
default: return nil;
}
}
+ (NSArray<NSPredicate *> *)defaultPredicates {
return [NSArray flex_forEachUpTo:FLEXObjectReferenceSectionCount map:^id(NSUInteger i) {
return [self defaultPredicateForSection:i];
}];
}
+ (NSArray<NSString *> *)defaultSectionTitles {
return @[
@"", @"AutoLayout", @"Key-Value Observing", @"FLEX"
];
}
#pragma mark - Initialization
- (id)initWithReferences:(nullable NSArray<FLEXObjectRef *> *)references {
return [self initWithReferences:references predicates:nil sectionTitles:nil];
}
- (id)initWithReferences:(NSArray<FLEXObjectRef *> *)references
predicates:(NSArray<NSPredicate *> *)predicates
sectionTitles:(NSArray<NSString *> *)sectionTitles {
NSParameterAssert(predicates.count == sectionTitles.count);
self = [super initWithStyle:UITableViewStylePlain];
if (self) {
_references = references;
_predicates = predicates;
_sectionTitles = sectionTitles;
}
return self;
}
+ (UIViewController *)instancesOfClassWithName:(NSString *)className retained:(BOOL)retain {
NSArray<FLEXObjectRef *> *references = [FLEXHeapEnumerator
instancesOfClassWithName:className retained:retain
];
if (references.count == 1) {
return [FLEXObjectExplorerFactory
explorerViewControllerForObject:references.firstObject.object
];
}
FLEXObjectListViewController *controller = [[self alloc] initWithReferences:references];
controller.title = [NSString stringWithFormat:@"%@ (%@)", className, @(references.count)];
return controller;
}
+ (instancetype)subclassesOfClassWithName:(NSString *)className {
NSArray<FLEXObjectRef *> *references = [FLEXHeapEnumerator subclassesOfClassWithName:className];
FLEXObjectListViewController *controller = [[self alloc] initWithReferences:references];
controller.title = [NSString stringWithFormat:@"Subclasses of %@ (%@)",
className, @(references.count)
];
return controller;
}
+ (instancetype)objectsWithReferencesToObject:(id)object retained:(BOOL)retain {
static Class SwiftObjectClass = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
SwiftObjectClass = NSClassFromString(@"SwiftObject");
if (!SwiftObjectClass) {
SwiftObjectClass = NSClassFromString(@"Swift._SwiftObject");
}
});
NSArray<FLEXObjectRef *> *instances = [FLEXHeapEnumerator
objectsWithReferencesToObject:object retained:retain
];
FLEXObjectListViewController *viewController = [[self alloc]
initWithReferences:instances
predicates:self.defaultPredicates
sectionTitles:self.defaultSectionTitles
];
viewController.title = [NSString stringWithFormat:@"Referencing %@ %p",
[FLEXRuntimeUtility safeClassNameForObject:object], object
];
return viewController;
}
#pragma mark - Overrides
- (void)viewDidLoad {
[super viewDidLoad];
self.showsSearchBar = YES;
}
- (NSArray<FLEXMutableListSection *> *)makeSections {
if (self.predicates.count) {
return [self buildSections:self.sectionTitles predicates:self.predicates];
} else {
return @[[self makeSection:self.references title:nil]];
}
}
#pragma mark - Private
- (NSArray *)buildSections:(NSArray<NSString *> *)titles predicates:(NSArray<NSPredicate *> *)predicates {
NSParameterAssert(titles.count == predicates.count);
NSParameterAssert(titles); NSParameterAssert(predicates);
return [NSArray flex_forEachUpTo:titles.count map:^id(NSUInteger i) {
NSArray *rows = [self.references filteredArrayUsingPredicate:predicates[i]];
return [self makeSection:rows title:titles[i]];
}];
}
- (FLEXMutableListSection *)makeSection:(NSArray *)rows title:(NSString *)title {
FLEXMutableListSection *section = [FLEXMutableListSection list:rows
cellConfiguration:^(FLEXTableViewCell *cell, FLEXObjectRef *ref, NSInteger row) {
cell.textLabel.text = ref.reference;
cell.detailTextLabel.text = ref.summary;
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
} filterMatcher:^BOOL(NSString *filterText, FLEXObjectRef *ref) {
if (ref.summary && [ref.summary localizedCaseInsensitiveContainsString:filterText]) {
return YES;
}
return [ref.reference localizedCaseInsensitiveContainsString:filterText];
}
];
section.selectionHandler = ^(UIViewController *host, FLEXObjectRef *ref) {
[host.navigationController pushViewController:[
FLEXObjectExplorerFactory explorerViewControllerForObject:ref.object
] animated:YES];
};
section.customTitle = title;
return section;
}
@end

View File

@@ -0,0 +1,41 @@
//
// FLEXObjectRef.h
// FLEX
//
// Created by Tanner Bennett on 7/24/18.
// Copyright (c) 2020 FLEX Team. All rights reserved.
//
#import <Foundation/Foundation.h>
@interface FLEXObjectRef : NSObject
/// Reference an object without affecting its lifespan or or emitting reference-counting operations.
+ (instancetype)unretained:(__unsafe_unretained id)object;
+ (instancetype)unretained:(__unsafe_unretained id)object ivar:(NSString *)ivarName;
/// Reference an object and control its lifespan.
+ (instancetype)retained:(id)object;
+ (instancetype)retained:(id)object ivar:(NSString *)ivarName;
/// Reference an object and conditionally choose to retain it or not.
+ (instancetype)referencing:(__unsafe_unretained id)object retained:(BOOL)retain;
+ (instancetype)referencing:(__unsafe_unretained id)object ivar:(NSString *)ivarName retained:(BOOL)retain;
+ (NSArray<FLEXObjectRef *> *)referencingAll:(NSArray *)objects retained:(BOOL)retain;
/// Classes do not have a summary, and the reference is just the class name.
+ (NSArray<FLEXObjectRef *> *)referencingClasses:(NSArray<Class> *)classes;
/// For example, "NSString 0x1d4085d0" or "NSLayoutConstraint _object"
@property (nonatomic, readonly) NSString *reference;
/// For instances, this is the result of -[FLEXRuntimeUtility summaryForObject:]
/// For classes, there is no summary.
@property (nonatomic, readonly) NSString *summary;
@property (nonatomic, readonly, unsafe_unretained) id object;
/// Retains the referenced object if it is not already retained
- (void)retainObject;
/// Releases the referenced object if it is already retained
- (void)releaseObject;
@end

View File

@@ -0,0 +1,112 @@
//
// FLEXObjectRef.m
// FLEX
//
// Created by Tanner Bennett on 7/24/18.
// Copyright (c) 2020 FLEX Team. All rights reserved.
//
#import "FLEXObjectRef.h"
#import "FLEXRuntimeUtility.h"
#import "NSArray+FLEX.h"
@interface FLEXObjectRef () {
/// Used to retain the object if desired
id _retainer;
}
@property (nonatomic, readonly) BOOL wantsSummary;
@end
@implementation FLEXObjectRef
@synthesize summary = _summary;
+ (instancetype)unretained:(__unsafe_unretained id)object {
return [self referencing:object showSummary:YES retained:NO];
}
+ (instancetype)unretained:(__unsafe_unretained id)object ivar:(NSString *)ivarName {
return [[self alloc] initWithObject:object ivarName:ivarName showSummary:YES retained:NO];
}
+ (instancetype)retained:(id)object {
return [self referencing:object showSummary:YES retained:YES];
}
+ (instancetype)retained:(id)object ivar:(NSString *)ivarName {
return [[self alloc] initWithObject:object ivarName:ivarName showSummary:YES retained:YES];
}
+ (instancetype)referencing:(__unsafe_unretained id)object retained:(BOOL)retain {
return retain ? [self retained:object] : [self unretained:object];
}
+ (instancetype)referencing:(__unsafe_unretained id)object ivar:(NSString *)ivarName retained:(BOOL)retain {
return retain ? [self retained:object ivar:ivarName] : [self unretained:object ivar:ivarName];
}
+ (instancetype)referencing:(__unsafe_unretained id)object showSummary:(BOOL)showSummary retained:(BOOL)retain {
return [[self alloc] initWithObject:object ivarName:nil showSummary:showSummary retained:retain];
}
+ (NSArray<FLEXObjectRef *> *)referencingAll:(NSArray *)objects retained:(BOOL)retain {
return [objects flex_mapped:^id(id obj, NSUInteger idx) {
return [self referencing:obj showSummary:YES retained:retain];
}];
}
+ (NSArray<FLEXObjectRef *> *)referencingClasses:(NSArray<Class> *)classes {
return [classes flex_mapped:^id(id obj, NSUInteger idx) {
return [self referencing:obj showSummary:NO retained:NO];
}];
}
- (id)initWithObject:(__unsafe_unretained id)object
ivarName:(NSString *)ivar
showSummary:(BOOL)showSummary
retained:(BOOL)retain {
self = [super init];
if (self) {
_object = object;
_wantsSummary = showSummary;
if (retain) {
_retainer = object;
}
NSString *class = [FLEXRuntimeUtility safeClassNameForObject:object];
if (ivar) {
_reference = [NSString stringWithFormat:@"%@ %@", class, ivar];
} else if (showSummary) {
_reference = [NSString stringWithFormat:@"%@ %p", class, object];
} else {
_reference = class;
}
}
return self;
}
- (NSString *)summary {
if (self.wantsSummary) {
if (!_summary) {
_summary = [FLEXRuntimeUtility summaryForObject:self.object];
}
return _summary;
}
else {
return nil;
}
}
- (void)retainObject {
if (!_retainer) {
_retainer = _object;
}
}
- (void)releaseObject {
_retainer = nil;
}
@end

View File

@@ -0,0 +1,18 @@
//
// FLEXWebViewController.m
// Flipboard
//
// Created by Ryan Olson on 6/10/14.
// Copyright (c) 2020 FLEX Team. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface FLEXWebViewController : UIViewController
- (id)initWithURL:(NSURL *)url;
- (id)initWithText:(NSString *)text;
+ (BOOL)supportsPathExtension:(NSString *)extension;
@end

View File

@@ -0,0 +1,143 @@
//
// FLEXWebViewController.m
// Flipboard
//
// Created by Ryan Olson on 6/10/14.
// Copyright (c) 2020 FLEX Team. All rights reserved.
//
#import "FLEXWebViewController.h"
#import "FLEXUtility.h"
#import <WebKit/WebKit.h>
@interface FLEXWebViewController () <WKNavigationDelegate>
@property (nonatomic) WKWebView *webView;
@property (nonatomic) NSString *originalText;
@end
@implementation FLEXWebViewController
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
WKWebViewConfiguration *configuration = [WKWebViewConfiguration new];
if (@available(iOS 10.0, *)) {
configuration.dataDetectorTypes = WKDataDetectorTypeLink;
}
self.webView = [[WKWebView alloc] initWithFrame:CGRectZero configuration:configuration];
self.webView.navigationDelegate = self;
}
return self;
}
- (id)initWithText:(NSString *)text {
self = [self initWithNibName:nil bundle:nil];
if (self) {
self.originalText = text;
NSString *html = @"<head><style>:root{ color-scheme: light dark; }</style>"
"<meta name='viewport' content='initial-scale=1.0'></head><body><pre>%@</pre></body>";
// Loading message for when input text takes a long time to escape
NSString *loadingMessage = [NSString stringWithFormat:html, @"Loading..."];
[self.webView loadHTMLString:loadingMessage baseURL:nil];
// Escape HTML on a background thread
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSString *escapedText = [FLEXUtility stringByEscapingHTMLEntitiesInString:text];
NSString *htmlString = [NSString stringWithFormat:html, escapedText];
// Update webview on the main thread
dispatch_async(dispatch_get_main_queue(), ^{
[self.webView loadHTMLString:htmlString baseURL:nil];
});
});
}
return self;
}
- (id)initWithURL:(NSURL *)url {
self = [self initWithNibName:nil bundle:nil];
if (self) {
NSURLRequest *request = [NSURLRequest requestWithURL:url];
[self.webView loadRequest:request];
}
return self;
}
- (void)viewDidLoad {
[super viewDidLoad];
[self.view addSubview:self.webView];
self.webView.frame = self.view.bounds;
self.webView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
if (self.originalText.length > 0) {
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc]
initWithTitle:@"Copy" style:UIBarButtonItemStylePlain target:self action:@selector(copyButtonTapped:)
];
}
}
- (void)copyButtonTapped:(id)sender {
[UIPasteboard.generalPasteboard setString:self.originalText];
}
#pragma mark - WKWebView Delegate
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction
decisionHandler:(void (^)(WKNavigationActionPolicy))handler {
WKNavigationActionPolicy policy = WKNavigationActionPolicyCancel;
if (navigationAction.navigationType == WKNavigationTypeOther) {
// Allow the initial load
policy = WKNavigationActionPolicyAllow;
} else {
// For clicked links, push another web view controller onto the navigation stack
// so that hitting the back button works as expected.
// Don't allow the current web view to handle the navigation.
NSURLRequest *request = navigationAction.request;
FLEXWebViewController *webVC = [[[self class] alloc] initWithURL:request.URL];
webVC.title = request.URL.absoluteString;
[self.navigationController pushViewController:webVC animated:YES];
}
handler(policy);
}
#pragma mark - Class Helpers
+ (BOOL)supportsPathExtension:(NSString *)extension {
BOOL supported = NO;
NSSet<NSString *> *supportedExtensions = [self webViewSupportedPathExtensions];
if ([supportedExtensions containsObject:extension.lowercaseString]) {
supported = YES;
}
return supported;
}
+ (NSSet<NSString *> *)webViewSupportedPathExtensions {
static NSSet<NSString *> *pathExtensions = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// Note that this is not exhaustive, but all these extensions should work well in the web view.
// See https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariWebContent/CreatingContentforSafarioniPhone/CreatingContentforSafarioniPhone.html#//apple_ref/doc/uid/TP40006482-SW7
pathExtensions = [NSSet<NSString *> setWithArray:@[
@"jpg", @"jpeg", @"png", @"gif", @"pdf", @"svg", @"tiff", @"3gp", @"3gpp", @"3g2",
@"3gp2", @"aiff", @"aif", @"aifc", @"cdda", @"amr", @"mp3", @"swa", @"mp4", @"mpeg",
@"mpg", @"mp3", @"wav", @"bwf", @"m4a", @"m4b", @"m4p", @"mov", @"qt", @"mqv", @"m4v"
]];
});
return pathExtensions;
}
@end

View File

@@ -0,0 +1,18 @@
//
// FLEXFileBrowserController.h
// Flipboard
//
// Created by Ryan Olson on 6/9/14.
// Based on previous work by Evan Doll
//
#import "FLEXTableViewController.h"
#import "FLEXGlobalsEntry.h"
#import "FLEXFileBrowserSearchOperation.h"
@interface FLEXFileBrowserController : FLEXTableViewController <FLEXGlobalsEntry>
+ (instancetype)path:(NSString *)path;
- (id)initWithPath:(NSString *)path;
@end

View File

@@ -0,0 +1,560 @@
//
// FLEXFileBrowserController.m
// Flipboard
//
// Created by Ryan Olson on 6/9/14.
//
//
#import "FLEXFileBrowserController.h"
#import "FLEXUtility.h"
#import "FLEXWebViewController.h"
#import "FLEXImagePreviewViewController.h"
#import "FLEXTableListViewController.h"
#import "FLEXObjectExplorerFactory.h"
#import "FLEXObjectExplorerViewController.h"
#import <mach-o/loader.h>
@interface FLEXFileBrowserTableViewCell : UITableViewCell
@end
typedef NS_ENUM(NSUInteger, FLEXFileBrowserSortAttribute) {
FLEXFileBrowserSortAttributeNone = 0,
FLEXFileBrowserSortAttributeName,
FLEXFileBrowserSortAttributeCreationDate,
};
@interface FLEXFileBrowserController () <FLEXFileBrowserSearchOperationDelegate>
@property (nonatomic, copy) NSString *path;
@property (nonatomic, copy) NSArray<NSString *> *childPaths;
@property (nonatomic) NSArray<NSString *> *searchPaths;
@property (nonatomic) NSNumber *recursiveSize;
@property (nonatomic) NSNumber *searchPathsSize;
@property (nonatomic) NSOperationQueue *operationQueue;
@property (nonatomic) UIDocumentInteractionController *documentController;
@property (nonatomic) FLEXFileBrowserSortAttribute sortAttribute;
@end
@implementation FLEXFileBrowserController
+ (instancetype)path:(NSString *)path {
return [[self alloc] initWithPath:path];
}
- (id)init {
return [self initWithPath:NSHomeDirectory()];
}
- (id)initWithPath:(NSString *)path {
self = [super init];
if (self) {
self.path = path;
self.title = [path lastPathComponent];
self.operationQueue = [NSOperationQueue new];
// Compute path size
weakify(self)
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSFileManager *fileManager = NSFileManager.defaultManager;
NSDictionary<NSString *, id> *attributes = [fileManager attributesOfItemAtPath:path error:NULL];
uint64_t totalSize = [attributes fileSize];
for (NSString *fileName in [fileManager enumeratorAtPath:path]) {
attributes = [fileManager attributesOfItemAtPath:[path stringByAppendingPathComponent:fileName] error:NULL];
totalSize += [attributes fileSize];
// Bail if the interested view controller has gone away
if (!self) {
return;
}
}
dispatch_async(dispatch_get_main_queue(), ^{ strongify(self)
self.recursiveSize = @(totalSize);
[self.tableView reloadData];
});
});
[self reloadCurrentPath];
}
return self;
}
#pragma mark - UIViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.showsSearchBar = YES;
self.searchBarDebounceInterval = kFLEXDebounceForAsyncSearch;
[self addToolbarItems:@[
[[UIBarButtonItem alloc] initWithTitle:@"Sort"
style:UIBarButtonItemStylePlain
target:self
action:@selector(sortDidTouchUpInside:)]
]];
}
- (void)sortDidTouchUpInside:(UIBarButtonItem *)sortButton {
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Sort"
message:nil
preferredStyle:UIAlertControllerStyleActionSheet];
[alertController addAction:[UIAlertAction actionWithTitle:@"None"
style:UIAlertActionStyleCancel
handler:^(UIAlertAction * _Nonnull action) {
[self sortWithAttribute:FLEXFileBrowserSortAttributeNone];
}]];
[alertController addAction:[UIAlertAction actionWithTitle:@"Name"
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * _Nonnull action) {
[self sortWithAttribute:FLEXFileBrowserSortAttributeName];
}]];
[alertController addAction:[UIAlertAction actionWithTitle:@"Creation Date"
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * _Nonnull action) {
[self sortWithAttribute:FLEXFileBrowserSortAttributeCreationDate];
}]];
[self presentViewController:alertController animated:YES completion:nil];
}
- (void)sortWithAttribute:(FLEXFileBrowserSortAttribute)attribute {
self.sortAttribute = attribute;
[self reloadDisplayedPaths];
}
#pragma mark - FLEXGlobalsEntry
+ (NSString *)globalsEntryTitle:(FLEXGlobalsRow)row {
switch (row) {
case FLEXGlobalsRowBrowseBundle: return @"📁 Browse Bundle Directory";
case FLEXGlobalsRowBrowseContainer: return @"📁 Browse Container Directory";
default: return nil;
}
}
+ (UIViewController *)globalsEntryViewController:(FLEXGlobalsRow)row {
switch (row) {
case FLEXGlobalsRowBrowseBundle: return [[self alloc] initWithPath:NSBundle.mainBundle.bundlePath];
case FLEXGlobalsRowBrowseContainer: return [[self alloc] initWithPath:NSHomeDirectory()];
default: return [self new];
}
}
#pragma mark - FLEXFileBrowserSearchOperationDelegate
- (void)fileBrowserSearchOperationResult:(NSArray<NSString *> *)searchResult size:(uint64_t)size {
self.searchPaths = searchResult;
self.searchPathsSize = @(size);
[self.tableView reloadData];
}
#pragma mark - Search bar
- (void)updateSearchResults:(NSString *)newText {
[self reloadDisplayedPaths];
}
#pragma mark UISearchControllerDelegate
- (void)willDismissSearchController:(UISearchController *)searchController {
[self.operationQueue cancelAllOperations];
[self reloadCurrentPath];
[self.tableView reloadData];
}
#pragma mark - Table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.searchController.isActive ? self.searchPaths.count : self.childPaths.count;
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
BOOL isSearchActive = self.searchController.isActive;
NSNumber *currentSize = isSearchActive ? self.searchPathsSize : self.recursiveSize;
NSArray<NSString *> *currentPaths = isSearchActive ? self.searchPaths : self.childPaths;
NSString *sizeString = nil;
if (!currentSize) {
sizeString = @"Computing size…";
} else {
sizeString = [NSByteCountFormatter stringFromByteCount:[currentSize longLongValue] countStyle:NSByteCountFormatterCountStyleFile];
}
return [NSString stringWithFormat:@"%lu files (%@)", (unsigned long)currentPaths.count, sizeString];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
NSString *fullPath = [self filePathAtIndexPath:indexPath];
NSDictionary<NSString *, id> *attributes = [NSFileManager.defaultManager attributesOfItemAtPath:fullPath error:NULL];
BOOL isDirectory = [attributes.fileType isEqual:NSFileTypeDirectory];
NSString *subtitle = nil;
if (isDirectory) {
NSUInteger count = [NSFileManager.defaultManager contentsOfDirectoryAtPath:fullPath error:NULL].count;
subtitle = [NSString stringWithFormat:@"%lu item%@", (unsigned long)count, (count == 1 ? @"" : @"s")];
} else {
NSString *sizeString = [NSByteCountFormatter stringFromByteCount:attributes.fileSize countStyle:NSByteCountFormatterCountStyleFile];
subtitle = [NSString stringWithFormat:@"%@ - %@", sizeString, attributes.fileModificationDate ?: @"Never modified"];
}
static NSString *textCellIdentifier = @"textCell";
static NSString *imageCellIdentifier = @"imageCell";
UITableViewCell *cell = nil;
// Separate image and text only cells because otherwise the separator lines get out-of-whack on image cells reused with text only.
UIImage *image = [UIImage imageWithContentsOfFile:fullPath];
NSString *cellIdentifier = image ? imageCellIdentifier : textCellIdentifier;
if (!cell) {
cell = [[FLEXFileBrowserTableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:cellIdentifier];
cell.textLabel.font = UIFont.flex_defaultTableCellFont;
cell.detailTextLabel.font = UIFont.flex_defaultTableCellFont;
cell.detailTextLabel.textColor = UIColor.grayColor;
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
}
NSString *cellTitle = [fullPath lastPathComponent];
cell.textLabel.text = cellTitle;
cell.detailTextLabel.text = subtitle;
if (image) {
cell.imageView.contentMode = UIViewContentModeScaleAspectFit;
cell.imageView.image = image;
}
return cell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[self.tableView deselectRowAtIndexPath:indexPath animated:YES];
NSString *fullPath = [self filePathAtIndexPath:indexPath];
NSString *subpath = fullPath.lastPathComponent;
NSString *pathExtension = subpath.pathExtension;
BOOL isDirectory = NO;
BOOL stillExists = [NSFileManager.defaultManager fileExistsAtPath:fullPath isDirectory:&isDirectory];
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
UIImage *image = cell.imageView.image;
if (!stillExists) {
[FLEXAlert showAlert:@"File Not Found" message:@"The file at the specified path no longer exists." from:self];
[self reloadDisplayedPaths];
return;
}
UIViewController *drillInViewController = nil;
if (isDirectory) {
drillInViewController = [[[self class] alloc] initWithPath:fullPath];
} else if (image) {
drillInViewController = [FLEXImagePreviewViewController forImage:image];
} else {
NSData *fileData = [NSData dataWithContentsOfFile:fullPath];
if (!fileData.length) {
[FLEXAlert showAlert:@"Empty File" message:@"No data returned from the file." from:self];
return;
}
// Special case keyed archives, json, and plists to get more readable data.
NSString *prettyString = nil;
if ([pathExtension isEqualToString:@"json"]) {
prettyString = [FLEXUtility prettyJSONStringFromData:fileData];
} else {
// Regardless of file extension...
id object = nil;
@try {
// Try to decode an archived object regardless of file extension
object = [NSKeyedUnarchiver unarchiveObjectWithData:fileData];
} @catch (NSException *e) { }
// Try to decode other things instead
object = object ?: [NSPropertyListSerialization
propertyListWithData:fileData
options:0
format:NULL
error:NULL
] ?: [NSDictionary dictionaryWithContentsOfFile:fullPath]
?: [NSArray arrayWithContentsOfFile:fullPath];
if (object) {
drillInViewController = [FLEXObjectExplorerFactory explorerViewControllerForObject:object];
} else {
// Is it possibly a mach-O file?
if (fileData.length > sizeof(struct mach_header_64)) {
struct mach_header_64 header;
[fileData getBytes:&header length:sizeof(struct mach_header_64)];
// Does it have the mach header magic number?
if (header.magic == MH_MAGIC_64) {
// See if we can get some classes out of it...
unsigned int count = 0;
const char **classList = objc_copyClassNamesForImage(
fullPath.UTF8String, &count
);
if (count > 0) {
NSArray<NSString *> *classNames = [NSArray flex_forEachUpTo:count map:^id(NSUInteger i) {
return objc_getClass(classList[i]);
}];
drillInViewController = [FLEXObjectExplorerFactory explorerViewControllerForObject:classNames];
}
}
}
}
}
if (prettyString.length) {
drillInViewController = [[FLEXWebViewController alloc] initWithText:prettyString];
} else if ([FLEXWebViewController supportsPathExtension:pathExtension]) {
drillInViewController = [[FLEXWebViewController alloc] initWithURL:[NSURL fileURLWithPath:fullPath]];
} else if ([FLEXTableListViewController supportsExtension:pathExtension]) {
drillInViewController = [[FLEXTableListViewController alloc] initWithPath:fullPath];
}
else if (!drillInViewController) {
NSString *fileString = [NSString stringWithUTF8String:fileData.bytes];
if (fileString.length) {
drillInViewController = [[FLEXWebViewController alloc] initWithText:fileString];
}
}
}
if (drillInViewController) {
drillInViewController.title = subpath.lastPathComponent;
[self.navigationController pushViewController:drillInViewController animated:YES];
} else {
// Share the file otherwise
[self openFileController:fullPath];
}
}
- (BOOL)tableView:(UITableView *)tableView shouldShowMenuForRowAtIndexPath:(NSIndexPath *)indexPath {
UIMenuItem *rename = [[UIMenuItem alloc] initWithTitle:@"Rename" action:@selector(fileBrowserRename:)];
UIMenuItem *delete = [[UIMenuItem alloc] initWithTitle:@"Delete" action:@selector(fileBrowserDelete:)];
UIMenuItem *copyPath = [[UIMenuItem alloc] initWithTitle:@"Copy Path" action:@selector(fileBrowserCopyPath:)];
UIMenuItem *share = [[UIMenuItem alloc] initWithTitle:@"Share" action:@selector(fileBrowserShare:)];
UIMenuController.sharedMenuController.menuItems = @[rename, delete, copyPath, share];
return YES;
}
- (BOOL)tableView:(UITableView *)tableView canPerformAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender {
return action == @selector(fileBrowserDelete:)
|| action == @selector(fileBrowserRename:)
|| action == @selector(fileBrowserCopyPath:)
|| action == @selector(fileBrowserShare:);
}
- (void)tableView:(UITableView *)tableView performAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender {
// Empty, but has to exist for the menu to show
// The table view only calls this method for actions in the UIResponderStandardEditActions informal protocol.
// Since our actions are outside of that protocol, we need to manually handle the action forwarding from the cells.
}
- (UIContextMenuConfiguration *)tableView:(UITableView *)tableView
contextMenuConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath
point:(CGPoint)point __IOS_AVAILABLE(13.0) {
weakify(self)
return [UIContextMenuConfiguration configurationWithIdentifier:nil previewProvider:nil
actionProvider:^UIMenu *(NSArray<UIMenuElement *> *suggestedActions) {
UITableViewCell * const cell = [tableView cellForRowAtIndexPath:indexPath];
UIAction *rename = [UIAction actionWithTitle:@"Rename" image:nil identifier:@"Rename"
handler:^(UIAction *action) { strongify(self)
[self fileBrowserRename:cell];
}
];
UIAction *delete = [UIAction actionWithTitle:@"Delete" image:nil identifier:@"Delete"
handler:^(UIAction *action) { strongify(self)
[self fileBrowserDelete:cell];
}
];
UIAction *copyPath = [UIAction actionWithTitle:@"Copy Path" image:nil identifier:@"Copy Path"
handler:^(UIAction *action) { strongify(self)
[self fileBrowserCopyPath:cell];
}
];
UIAction *share = [UIAction actionWithTitle:@"Share" image:nil identifier:@"Share"
handler:^(UIAction *action) { strongify(self)
[self fileBrowserShare:cell];
}
];
return [UIMenu menuWithTitle:@"Manage File" image:nil
identifier:@"Manage File"
options:UIMenuOptionsDisplayInline
children:@[rename, delete, copyPath, share]
];
}
];
}
- (void)openFileController:(NSString *)fullPath {
UIDocumentInteractionController *controller = [UIDocumentInteractionController new];
controller.URL = [NSURL fileURLWithPath:fullPath];
[controller presentOptionsMenuFromRect:self.view.bounds inView:self.view animated:YES];
self.documentController = controller;
}
- (void)fileBrowserRename:(UITableViewCell *)sender {
NSIndexPath *indexPath = [self.tableView indexPathForCell:sender];
NSString *fullPath = [self filePathAtIndexPath:indexPath];
BOOL stillExists = [NSFileManager.defaultManager fileExistsAtPath:self.path isDirectory:NULL];
if (stillExists) {
[FLEXAlert makeAlert:^(FLEXAlert *make) {
make.title([NSString stringWithFormat:@"Rename %@?", fullPath.lastPathComponent]);
make.configuredTextField(^(UITextField *textField) {
textField.placeholder = @"New file name";
textField.text = fullPath.lastPathComponent;
});
make.button(@"Rename").handler(^(NSArray<NSString *> *strings) {
NSString *newFileName = strings.firstObject;
NSString *newPath = [fullPath.stringByDeletingLastPathComponent stringByAppendingPathComponent:newFileName];
[NSFileManager.defaultManager moveItemAtPath:fullPath toPath:newPath error:NULL];
[self reloadDisplayedPaths];
});
make.button(@"Cancel").cancelStyle();
} showFrom:self];
} else {
[FLEXAlert showAlert:@"File Removed" message:@"The file at the specified path no longer exists." from:self];
}
}
- (void)fileBrowserDelete:(UITableViewCell *)sender {
NSIndexPath *indexPath = [self.tableView indexPathForCell:sender];
NSString *fullPath = [self filePathAtIndexPath:indexPath];
BOOL isDirectory = NO;
BOOL stillExists = [NSFileManager.defaultManager fileExistsAtPath:fullPath isDirectory:&isDirectory];
if (stillExists) {
[FLEXAlert makeAlert:^(FLEXAlert *make) {
make.title(@"Confirm Deletion");
make.message([NSString stringWithFormat:
@"The %@ '%@' will be deleted. This operation cannot be undone",
(isDirectory ? @"directory" : @"file"), fullPath.lastPathComponent
]);
make.button(@"Delete").destructiveStyle().handler(^(NSArray<NSString *> *strings) {
[NSFileManager.defaultManager removeItemAtPath:fullPath error:NULL];
[self reloadDisplayedPaths];
});
make.button(@"Cancel").cancelStyle();
} showFrom:self];
} else {
[FLEXAlert showAlert:@"File Removed" message:@"The file at the specified path no longer exists." from:self];
}
}
- (void)fileBrowserCopyPath:(UITableViewCell *)sender {
NSIndexPath *indexPath = [self.tableView indexPathForCell:sender];
NSString *fullPath = [self filePathAtIndexPath:indexPath];
UIPasteboard.generalPasteboard.string = fullPath;
}
- (void)fileBrowserShare:(UITableViewCell *)sender {
NSIndexPath *indexPath = [self.tableView indexPathForCell:sender];
NSString *pathString = [self filePathAtIndexPath:indexPath];
NSURL *filePath = [NSURL fileURLWithPath:pathString];
BOOL isDirectory = NO;
[NSFileManager.defaultManager fileExistsAtPath:pathString isDirectory:&isDirectory];
if (isDirectory) {
// UIDocumentInteractionController for folders
[self openFileController:pathString];
} else {
// Share sheet for files
UIActivityViewController *shareSheet = [[UIActivityViewController alloc] initWithActivityItems:@[filePath] applicationActivities:nil];
if (UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPad) {
shareSheet.popoverPresentationController.sourceView = sender;
}
[self presentViewController:shareSheet animated:true completion:nil];
}
}
- (void)reloadDisplayedPaths {
if (self.searchController.isActive) {
[self updateSearchPaths];
} else {
[self reloadCurrentPath];
[self.tableView reloadData];
}
}
- (void)reloadCurrentPath {
NSMutableArray<NSString *> *childPaths = [NSMutableArray new];
NSArray<NSString *> *subpaths = [NSFileManager.defaultManager contentsOfDirectoryAtPath:self.path error:NULL];
for (NSString *subpath in subpaths) {
[childPaths addObject:[self.path stringByAppendingPathComponent:subpath]];
}
if (self.sortAttribute != FLEXFileBrowserSortAttributeNone) {
[childPaths sortUsingComparator:^NSComparisonResult(NSString *path1, NSString *path2) {
switch (self.sortAttribute) {
case FLEXFileBrowserSortAttributeNone:
// invalid state
return NSOrderedSame;
case FLEXFileBrowserSortAttributeName:
return [path1 compare:path2];
case FLEXFileBrowserSortAttributeCreationDate: {
NSDictionary<NSFileAttributeKey, id> *path1Attributes = [NSFileManager.defaultManager attributesOfItemAtPath:path1
error:NULL];
NSDictionary<NSFileAttributeKey, id> *path2Attributes = [NSFileManager.defaultManager attributesOfItemAtPath:path2
error:NULL];
NSDate *path1Date = path1Attributes[NSFileCreationDate];
NSDate *path2Date = path2Attributes[NSFileCreationDate];
return [path1Date compare:path2Date];
}
}
}];
}
self.childPaths = childPaths;
}
- (void)updateSearchPaths {
self.searchPaths = nil;
self.searchPathsSize = nil;
//clear pre search request and start a new one
[self.operationQueue cancelAllOperations];
FLEXFileBrowserSearchOperation *newOperation = [[FLEXFileBrowserSearchOperation alloc] initWithPath:self.path searchString:self.searchText];
newOperation.delegate = self;
[self.operationQueue addOperation:newOperation];
}
- (NSString *)filePathAtIndexPath:(NSIndexPath *)indexPath {
return self.searchController.isActive ? self.searchPaths[indexPath.row] : self.childPaths[indexPath.row];
}
@end
@implementation FLEXFileBrowserTableViewCell
- (void)forwardAction:(SEL)action withSender:(id)sender {
id target = [self.nextResponder targetForAction:action withSender:sender];
[UIApplication.sharedApplication sendAction:action to:target from:self forEvent:nil];
}
- (void)fileBrowserRename:(UIMenuController *)sender {
[self forwardAction:_cmd withSender:sender];
}
- (void)fileBrowserDelete:(UIMenuController *)sender {
[self forwardAction:_cmd withSender:sender];
}
- (void)fileBrowserCopyPath:(UIMenuController *)sender {
[self forwardAction:_cmd withSender:sender];
}
- (void)fileBrowserShare:(UIMenuController *)sender {
[self forwardAction:_cmd withSender:sender];
}
@end

View File

@@ -0,0 +1,25 @@
//
// FLEXFileBrowserSearchOperation.h
// FLEX
//
// Created by 啟倫 陳 on 2014/8/4.
// Copyright (c) 2014年 f. All rights reserved.
//
#import <Foundation/Foundation.h>
@protocol FLEXFileBrowserSearchOperationDelegate;
@interface FLEXFileBrowserSearchOperation : NSOperation
@property (nonatomic, weak) id<FLEXFileBrowserSearchOperationDelegate> delegate;
- (id)initWithPath:(NSString *)currentPath searchString:(NSString *)searchString;
@end
@protocol FLEXFileBrowserSearchOperationDelegate <NSObject>
- (void)fileBrowserSearchOperationResult:(NSArray<NSString *> *)searchResult size:(uint64_t)size;
@end

View File

@@ -0,0 +1,118 @@
//
// FLEXFileBrowserSearchOperation.m
// FLEX
//
// Created by on 2014/8/4.
// Copyright (c) 2014 f. All rights reserved.
//
#import "FLEXFileBrowserSearchOperation.h"
@implementation NSMutableArray (FLEXStack)
- (void)flex_push:(id)anObject {
[self addObject:anObject];
}
- (id)flex_pop {
id anObject = self.lastObject;
[self removeLastObject];
return anObject;
}
@end
@interface FLEXFileBrowserSearchOperation ()
@property (nonatomic) NSString *path;
@property (nonatomic) NSString *searchString;
@end
@implementation FLEXFileBrowserSearchOperation
#pragma mark - private
- (uint64_t)totalSizeAtPath:(NSString *)path {
NSFileManager *fileManager = NSFileManager.defaultManager;
NSDictionary<NSString *, id> *attributes = [fileManager attributesOfItemAtPath:path error:NULL];
uint64_t totalSize = [attributes fileSize];
for (NSString *fileName in [fileManager enumeratorAtPath:path]) {
attributes = [fileManager attributesOfItemAtPath:[path stringByAppendingPathComponent:fileName] error:NULL];
totalSize += [attributes fileSize];
}
return totalSize;
}
#pragma mark - instance method
- (id)initWithPath:(NSString *)currentPath searchString:(NSString *)searchString {
self = [super init];
if (self) {
self.path = currentPath;
self.searchString = searchString;
}
return self;
}
#pragma mark - methods to override
- (void)main {
NSFileManager *fileManager = NSFileManager.defaultManager;
NSMutableArray<NSString *> *searchPaths = [NSMutableArray new];
NSMutableDictionary<NSString *, NSNumber *> *sizeMapping = [NSMutableDictionary new];
uint64_t totalSize = 0;
NSMutableArray<NSString *> *stack = [NSMutableArray new];
[stack flex_push:self.path];
//recursive found all match searchString paths, and precomputing there size
while (stack.count) {
NSString *currentPath = [stack flex_pop];
NSArray<NSString *> *directoryPath = [fileManager contentsOfDirectoryAtPath:currentPath error:nil];
for (NSString *subPath in directoryPath) {
NSString *fullPath = [currentPath stringByAppendingPathComponent:subPath];
if ([[subPath lowercaseString] rangeOfString:[self.searchString lowercaseString]].location != NSNotFound) {
[searchPaths addObject:fullPath];
if (!sizeMapping[fullPath]) {
uint64_t fullPathSize = [self totalSizeAtPath:fullPath];
totalSize += fullPathSize;
[sizeMapping setObject:@(fullPathSize) forKey:fullPath];
}
}
BOOL isDirectory;
if ([fileManager fileExistsAtPath:fullPath isDirectory:&isDirectory] && isDirectory) {
[stack flex_push:fullPath];
}
if ([self isCancelled]) {
return;
}
}
}
//sort
NSArray<NSString *> *sortedArray = [searchPaths sortedArrayUsingComparator:^NSComparisonResult(NSString *path1, NSString *path2) {
uint64_t pathSize1 = [sizeMapping[path1] unsignedLongLongValue];
uint64_t pathSize2 = [sizeMapping[path2] unsignedLongLongValue];
if (pathSize1 < pathSize2) {
return NSOrderedAscending;
} else if (pathSize1 > pathSize2) {
return NSOrderedDescending;
} else {
return NSOrderedSame;
}
}];
if ([self isCancelled]) {
return;
}
dispatch_async(dispatch_get_main_queue(), ^{
[self.delegate fileBrowserSearchOperationResult:sortedArray size:totalSize];
});
}
@end

View File

@@ -0,0 +1,105 @@
//
// FLEXGlobalsEntry.h
// FLEX
//
// Created by Javier Soto on 7/26/14.
// Copyright (c) 2020 FLEX Team. All rights reserved.
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
typedef NS_ENUM(NSUInteger, FLEXGlobalsRow) {
FLEXGlobalsRowProcessInfo,
FLEXGlobalsRowNetworkHistory,
FLEXGlobalsRowSystemLog,
FLEXGlobalsRowLiveObjects,
FLEXGlobalsRowAddressInspector,
FLEXGlobalsRowCookies,
FLEXGlobalsRowBrowseRuntime,
FLEXGlobalsRowAppKeychainItems,
FLEXGlobalsRowAppDelegate,
FLEXGlobalsRowRootViewController,
FLEXGlobalsRowUserDefaults,
FLEXGlobalsRowMainBundle,
FLEXGlobalsRowBrowseBundle,
FLEXGlobalsRowBrowseContainer,
FLEXGlobalsRowApplication,
FLEXGlobalsRowKeyWindow,
FLEXGlobalsRowMainScreen,
FLEXGlobalsRowCurrentDevice,
FLEXGlobalsRowPasteboard,
FLEXGlobalsRowURLSession,
FLEXGlobalsRowURLCache,
FLEXGlobalsRowNotificationCenter,
FLEXGlobalsRowMenuController,
FLEXGlobalsRowFileManager,
FLEXGlobalsRowTimeZone,
FLEXGlobalsRowLocale,
FLEXGlobalsRowCalendar,
FLEXGlobalsRowMainRunLoop,
FLEXGlobalsRowMainThread,
FLEXGlobalsRowOperationQueue,
FLEXGlobalsRowCount
};
typedef NSString * _Nonnull (^FLEXGlobalsEntryNameFuture)(void);
/// Simply return a view controller to be pushed on the navigation stack
typedef UIViewController * _Nullable (^FLEXGlobalsEntryViewControllerFuture)(void);
/// Do something like present an alert, then use the host
/// view controller to present or push another view controller.
typedef void (^FLEXGlobalsEntryRowAction)(__kindof UITableViewController * _Nonnull host);
/// For view controllers to conform to to indicate they support being used
/// in the globals table view controller. These methods help create concrete entries.
///
/// Previously, the concrete entries relied on "futures" for the view controller and title.
/// With this protocol, the conforming class itself can act as a future, since the methods
/// will not be invoked until the title and view controller / row action are needed.
///
/// Entries can implement \c globalsEntryViewController: to unconditionally provide a
/// view controller, or \c globalsEntryRowAction: to conditionally provide one and
/// perform some action (such as present an alert) if no view controller is available,
/// or both if there is a mix of rows where some are guaranteed to work and some are not.
/// Where both are implemented, \c globalsEntryRowAction: takes precedence; if it returns
/// an action for the requested row, that will be used instead of \c globalsEntryViewController:
@protocol FLEXGlobalsEntry <NSObject>
+ (NSString *)globalsEntryTitle:(FLEXGlobalsRow)row;
// Must respond to at least one of the below.
// globalsEntryRowAction: takes precedence if both are implemented.
@optional
+ (nullable UIViewController *)globalsEntryViewController:(FLEXGlobalsRow)row;
+ (nullable FLEXGlobalsEntryRowAction)globalsEntryRowAction:(FLEXGlobalsRow)row;
@end
@interface FLEXGlobalsEntry : NSObject
@property (nonatomic, readonly, nonnull) FLEXGlobalsEntryNameFuture entryNameFuture;
@property (nonatomic, readonly, nullable) FLEXGlobalsEntryViewControllerFuture viewControllerFuture;
@property (nonatomic, readonly, nullable) FLEXGlobalsEntryRowAction rowAction;
+ (instancetype)entryWithEntry:(Class<FLEXGlobalsEntry>)entry row:(FLEXGlobalsRow)row;
+ (instancetype)entryWithNameFuture:(FLEXGlobalsEntryNameFuture)nameFuture
viewControllerFuture:(FLEXGlobalsEntryViewControllerFuture)viewControllerFuture;
+ (instancetype)entryWithNameFuture:(FLEXGlobalsEntryNameFuture)nameFuture
action:(FLEXGlobalsEntryRowAction)rowSelectedAction;
@end
@interface NSObject (FLEXGlobalsEntry)
/// @return The result of passing self to +[FLEXGlobalsEntry entryWithEntry:]
/// if the class conforms to FLEXGlobalsEntry, else, nil.
+ (nullable FLEXGlobalsEntry *)flex_concreteGlobalsEntry:(FLEXGlobalsRow)row;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,86 @@
//
// FLEXGlobalsEntry.m
// FLEX
//
// Created by Javier Soto on 7/26/14.
// Copyright (c) 2020 FLEX Team. All rights reserved.
//
#import "FLEXGlobalsEntry.h"
@implementation FLEXGlobalsEntry
+ (instancetype)entryWithEntry:(Class<FLEXGlobalsEntry>)cls row:(FLEXGlobalsRow)row {
BOOL providesVCs = [cls respondsToSelector:@selector(globalsEntryViewController:)];
BOOL providesActions = [cls respondsToSelector:@selector(globalsEntryRowAction:)];
NSParameterAssert(cls);
NSParameterAssert(providesVCs || providesActions);
FLEXGlobalsEntry *entry = [self new];
entry->_entryNameFuture = ^{ return [cls globalsEntryTitle:row]; };
if (providesVCs) {
id action = providesActions ? [cls globalsEntryRowAction:row] : nil;
if (action) {
entry->_rowAction = action;
} else {
entry->_viewControllerFuture = ^{ return [cls globalsEntryViewController:row]; };
}
} else {
entry->_rowAction = [cls globalsEntryRowAction:row];
}
return entry;
}
+ (instancetype)entryWithNameFuture:(FLEXGlobalsEntryNameFuture)nameFuture
viewControllerFuture:(FLEXGlobalsEntryViewControllerFuture)viewControllerFuture {
NSParameterAssert(nameFuture);
NSParameterAssert(viewControllerFuture);
FLEXGlobalsEntry *entry = [self new];
entry->_entryNameFuture = [nameFuture copy];
entry->_viewControllerFuture = [viewControllerFuture copy];
return entry;
}
+ (instancetype)entryWithNameFuture:(FLEXGlobalsEntryNameFuture)nameFuture
action:(FLEXGlobalsEntryRowAction)rowSelectedAction {
NSParameterAssert(nameFuture);
NSParameterAssert(rowSelectedAction);
FLEXGlobalsEntry *entry = [self new];
entry->_entryNameFuture = [nameFuture copy];
entry->_rowAction = [rowSelectedAction copy];
return entry;
}
@end
@interface FLEXGlobalsEntry (Debugging)
@property (nonatomic, readonly) NSString *name;
@end
@implementation FLEXGlobalsEntry (Debugging)
- (NSString *)name {
return self.entryNameFuture();
}
@end
#pragma mark - flex_concreteGlobalsEntry
@implementation NSObject (FLEXGlobalsEntry)
+ (FLEXGlobalsEntry *)flex_concreteGlobalsEntry:(FLEXGlobalsRow)row {
if ([self conformsToProtocol:@protocol(FLEXGlobalsEntry)]) {
return [FLEXGlobalsEntry entryWithEntry:self row:row];
}
return nil;
}
@end

View File

@@ -0,0 +1,20 @@
//
// FLEXGlobalsSection.h
// FLEX
//
// Created by Tanner Bennett on 7/11/19.
// Copyright © 2020 FLEX Team. All rights reserved.
//
#import "FLEXTableViewSection.h"
#import "FLEXGlobalsEntry.h"
NS_ASSUME_NONNULL_BEGIN
@interface FLEXGlobalsSection : FLEXTableViewSection
+ (instancetype)title:(NSString *)title rows:(NSArray<FLEXGlobalsEntry *> *)rows;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,86 @@
//
// FLEXGlobalsSection.m
// FLEX
//
// Created by Tanner Bennett on 7/11/19.
// Copyright © 2020 FLEX Team. All rights reserved.
//
#import "FLEXGlobalsSection.h"
#import "NSArray+FLEX.h"
#import "UIFont+FLEX.h"
@interface FLEXGlobalsSection ()
/// Filtered rows
@property (nonatomic) NSArray<FLEXGlobalsEntry *> *rows;
/// Unfiltered rows
@property (nonatomic) NSArray<FLEXGlobalsEntry *> *allRows;
@end
@implementation FLEXGlobalsSection
#pragma mark - Initialization
+ (instancetype)title:(NSString *)title rows:(NSArray<FLEXGlobalsEntry *> *)rows {
FLEXGlobalsSection *s = [self new];
s->_title = title;
s.allRows = rows;
return s;
}
- (void)setAllRows:(NSArray<FLEXGlobalsEntry *> *)allRows {
_allRows = allRows.copy;
[self reloadData];
}
#pragma mark - Overrides
- (NSInteger)numberOfRows {
return self.rows.count;
}
- (void)setFilterText:(NSString *)filterText {
super.filterText = filterText;
[self reloadData];
}
- (void)reloadData {
NSString *filterText = self.filterText;
if (filterText.length) {
self.rows = [self.allRows flex_filtered:^BOOL(FLEXGlobalsEntry *entry, NSUInteger idx) {
return [entry.entryNameFuture() localizedCaseInsensitiveContainsString:filterText];
}];
} else {
self.rows = self.allRows;
}
}
- (BOOL)canSelectRow:(NSInteger)row {
return YES;
}
- (void (^)(__kindof UIViewController *))didSelectRowAction:(NSInteger)row {
return (id)self.rows[row].rowAction;
}
- (UIViewController *)viewControllerToPushForRow:(NSInteger)row {
return self.rows[row].viewControllerFuture ? self.rows[row].viewControllerFuture() : nil;
}
- (void)configureCell:(__kindof UITableViewCell *)cell forRow:(NSInteger)row {
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
cell.textLabel.font = UIFont.flex_defaultTableCellFont;
cell.textLabel.text = self.rows[row].entryNameFuture();
}
@end
@implementation FLEXGlobalsSection (Subscripting)
- (id)objectAtIndexedSubscript:(NSUInteger)idx {
return self.rows[idx];
}
@end

View File

@@ -0,0 +1,28 @@
//
// FLEXGlobalsViewController.h
// Flipboard
//
// Created by Ryan Olson on 2014-05-03.
// Copyright (c) 2020 FLEX Team. All rights reserved.
//
#import "FLEXFilteringTableViewController.h"
@protocol FLEXGlobalsTableViewControllerDelegate;
typedef NS_ENUM(NSUInteger, FLEXGlobalsSectionKind) {
FLEXGlobalsSectionCustom,
/// NSProcessInfo, Network history, system log,
/// heap, address explorer, libraries, app classes
FLEXGlobalsSectionProcessAndEvents,
/// Browse container, browse bundle, NSBundle.main,
/// NSUserDefaults.standard, UIApplication,
/// app delegate, key window, root VC, cookies
FLEXGlobalsSectionAppShortcuts,
/// UIPasteBoard.general, UIScreen, UIDevice
FLEXGlobalsSectionMisc,
FLEXGlobalsSectionCount
};
@interface FLEXGlobalsViewController : FLEXFilteringTableViewController
@end

View File

@@ -0,0 +1,200 @@
//
// FLEXGlobalsViewController.m
// Flipboard
//
// Created by Ryan Olson on 2014-05-03.
// Copyright (c) 2020 FLEX Team. All rights reserved.
//
#import "FLEXGlobalsViewController.h"
#import "FLEXUtility.h"
#import "FLEXRuntimeUtility.h"
#import "FLEXObjcRuntimeViewController.h"
#import "FLEXKeychainViewController.h"
#import "FLEXObjectExplorerViewController.h"
#import "FLEXObjectExplorerFactory.h"
#import "FLEXLiveObjectsController.h"
#import "FLEXFileBrowserController.h"
#import "FLEXCookiesViewController.h"
#import "FLEXGlobalsEntry.h"
#import "FLEXManager+Private.h"
#import "FLEXSystemLogViewController.h"
#import "FLEXNetworkMITMViewController.h"
#import "FLEXAddressExplorerCoordinator.h"
#import "FLEXGlobalsSection.h"
#import "UIBarButtonItem+FLEX.h"
@interface FLEXGlobalsViewController ()
/// Only displayed sections of the table view; empty sections are purged from this array.
@property (nonatomic) NSArray<FLEXGlobalsSection *> *sections;
/// Every section in the table view, regardless of whether or not a section is empty.
@property (nonatomic, readonly) NSArray<FLEXGlobalsSection *> *allSections;
@property (nonatomic, readonly) BOOL manuallyDeselectOnAppear;
@end
@implementation FLEXGlobalsViewController
@dynamic sections, allSections;
#pragma mark - Initialization
+ (NSString *)globalsTitleForSection:(FLEXGlobalsSectionKind)section {
switch (section) {
case FLEXGlobalsSectionCustom:
return @"Custom Additions";
case FLEXGlobalsSectionProcessAndEvents:
return @"Process and Events";
case FLEXGlobalsSectionAppShortcuts:
return @"App Shortcuts";
case FLEXGlobalsSectionMisc:
return @"Miscellaneous";
default:
@throw NSInternalInconsistencyException;
}
}
+ (FLEXGlobalsEntry *)globalsEntryForRow:(FLEXGlobalsRow)row {
switch (row) {
case FLEXGlobalsRowAppKeychainItems:
return [FLEXKeychainViewController flex_concreteGlobalsEntry:row];
case FLEXGlobalsRowAddressInspector:
return [FLEXAddressExplorerCoordinator flex_concreteGlobalsEntry:row];
case FLEXGlobalsRowBrowseRuntime:
return [FLEXObjcRuntimeViewController flex_concreteGlobalsEntry:row];
case FLEXGlobalsRowLiveObjects:
return [FLEXLiveObjectsController flex_concreteGlobalsEntry:row];
case FLEXGlobalsRowCookies:
return [FLEXCookiesViewController flex_concreteGlobalsEntry:row];
case FLEXGlobalsRowBrowseBundle:
case FLEXGlobalsRowBrowseContainer:
return [FLEXFileBrowserController flex_concreteGlobalsEntry:row];
case FLEXGlobalsRowSystemLog:
return [FLEXSystemLogViewController flex_concreteGlobalsEntry:row];
case FLEXGlobalsRowNetworkHistory:
return [FLEXNetworkMITMViewController flex_concreteGlobalsEntry:row];
case FLEXGlobalsRowKeyWindow:
case FLEXGlobalsRowRootViewController:
case FLEXGlobalsRowProcessInfo:
case FLEXGlobalsRowAppDelegate:
case FLEXGlobalsRowUserDefaults:
case FLEXGlobalsRowMainBundle:
case FLEXGlobalsRowApplication:
case FLEXGlobalsRowMainScreen:
case FLEXGlobalsRowCurrentDevice:
case FLEXGlobalsRowPasteboard:
case FLEXGlobalsRowURLSession:
case FLEXGlobalsRowURLCache:
case FLEXGlobalsRowNotificationCenter:
case FLEXGlobalsRowMenuController:
case FLEXGlobalsRowFileManager:
case FLEXGlobalsRowTimeZone:
case FLEXGlobalsRowLocale:
case FLEXGlobalsRowCalendar:
case FLEXGlobalsRowMainRunLoop:
case FLEXGlobalsRowMainThread:
case FLEXGlobalsRowOperationQueue:
return [FLEXObjectExplorerFactory flex_concreteGlobalsEntry:row];
default:
@throw [NSException
exceptionWithName:NSInternalInconsistencyException
reason:@"Missing globals case in switch" userInfo:nil
];
}
}
+ (NSArray<FLEXGlobalsSection *> *)defaultGlobalSections {
static NSMutableArray<FLEXGlobalsSection *> *sections = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSDictionary<NSNumber *, NSArray<FLEXGlobalsEntry *> *> *rowsBySection = @{
@(FLEXGlobalsSectionProcessAndEvents) : @[
[self globalsEntryForRow:FLEXGlobalsRowNetworkHistory],
[self globalsEntryForRow:FLEXGlobalsRowSystemLog],
[self globalsEntryForRow:FLEXGlobalsRowProcessInfo],
[self globalsEntryForRow:FLEXGlobalsRowLiveObjects],
[self globalsEntryForRow:FLEXGlobalsRowAddressInspector],
[self globalsEntryForRow:FLEXGlobalsRowBrowseRuntime],
],
@(FLEXGlobalsSectionAppShortcuts) : @[
[self globalsEntryForRow:FLEXGlobalsRowBrowseBundle],
[self globalsEntryForRow:FLEXGlobalsRowBrowseContainer],
[self globalsEntryForRow:FLEXGlobalsRowMainBundle],
[self globalsEntryForRow:FLEXGlobalsRowUserDefaults],
[self globalsEntryForRow:FLEXGlobalsRowAppKeychainItems],
[self globalsEntryForRow:FLEXGlobalsRowApplication],
[self globalsEntryForRow:FLEXGlobalsRowAppDelegate],
[self globalsEntryForRow:FLEXGlobalsRowKeyWindow],
[self globalsEntryForRow:FLEXGlobalsRowRootViewController],
[self globalsEntryForRow:FLEXGlobalsRowCookies],
],
@(FLEXGlobalsSectionMisc) : @[
[self globalsEntryForRow:FLEXGlobalsRowPasteboard],
[self globalsEntryForRow:FLEXGlobalsRowMainScreen],
[self globalsEntryForRow:FLEXGlobalsRowCurrentDevice],
[self globalsEntryForRow:FLEXGlobalsRowURLSession],
[self globalsEntryForRow:FLEXGlobalsRowURLCache],
[self globalsEntryForRow:FLEXGlobalsRowNotificationCenter],
[self globalsEntryForRow:FLEXGlobalsRowMenuController],
[self globalsEntryForRow:FLEXGlobalsRowFileManager],
[self globalsEntryForRow:FLEXGlobalsRowTimeZone],
[self globalsEntryForRow:FLEXGlobalsRowLocale],
[self globalsEntryForRow:FLEXGlobalsRowCalendar],
[self globalsEntryForRow:FLEXGlobalsRowMainRunLoop],
[self globalsEntryForRow:FLEXGlobalsRowMainThread],
[self globalsEntryForRow:FLEXGlobalsRowOperationQueue],
]
};
sections = [NSMutableArray array];
for (FLEXGlobalsSectionKind i = FLEXGlobalsSectionCustom + 1; i < FLEXGlobalsSectionCount; ++i) {
NSString *title = [self globalsTitleForSection:i];
[sections addObject:[FLEXGlobalsSection title:title rows:rowsBySection[@(i)]]];
}
});
return sections;
}
#pragma mark - Overrides
- (void)viewDidLoad {
[super viewDidLoad];
self.title = @"💪 FLEX";
self.showsSearchBar = YES;
self.searchBarDebounceInterval = kFLEXDebounceInstant;
self.navigationItem.backBarButtonItem = [UIBarButtonItem flex_backItemWithTitle:@"Back"];
_manuallyDeselectOnAppear = NSProcessInfo.processInfo.operatingSystemVersion.majorVersion < 10;
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self disableToolbar];
if (self.manuallyDeselectOnAppear) {
[self.tableView deselectRowAtIndexPath:self.tableView.indexPathForSelectedRow animated:YES];
}
}
- (NSArray<FLEXGlobalsSection *> *)makeSections {
NSMutableArray<FLEXGlobalsSection *> *sections = [NSMutableArray array];
// Do we have custom sections to add?
if (FLEXManager.sharedManager.userGlobalEntries.count) {
NSString *title = [[self class] globalsTitleForSection:FLEXGlobalsSectionCustom];
FLEXGlobalsSection *custom = [FLEXGlobalsSection
title:title
rows:FLEXManager.sharedManager.userGlobalEntries
];
[sections addObject:custom];
}
[sections addObjectsFromArray:[self.class defaultGlobalSections]];
return sections;
}
@end

View File

@@ -0,0 +1,144 @@
//
// FLEXKeychain.h
//
// Derived from:
// SSKeychain.h in SSKeychain
// Created by Sam Soffes on 5/19/10.
// Copyright (c) 2010-2014 Sam Soffes. All rights reserved.
//
#import <Foundation/Foundation.h>
/// Error code specific to FLEXKeychain that can be returned in NSError objects.
/// For codes returned by the operating system, refer to SecBase.h for your
/// platform.
typedef NS_ENUM(OSStatus, FLEXKeychainErrorCode) {
/// Some of the arguments were invalid.
FLEXKeychainErrorBadArguments = -1001,
};
/// FLEXKeychain error domain
extern NSString *const kFLEXKeychainErrorDomain;
/// Account name.
extern NSString *const kFLEXKeychainAccountKey;
/// Time the item was created.
///
/// The value will be a string.
extern NSString *const kFLEXKeychainCreatedAtKey;
/// Item class.
extern NSString *const kFLEXKeychainClassKey;
/// Item description.
extern NSString *const kFLEXKeychainDescriptionKey;
/// Item group.
extern NSString *const kFLEXKeychainGroupKey;
/// Item label.
extern NSString *const kFLEXKeychainLabelKey;
/// Time the item was last modified.
///
/// The value will be a string.
extern NSString *const kFLEXKeychainLastModifiedKey;
/// Where the item was created.
extern NSString *const kFLEXKeychainWhereKey;
/// A simple wrapper for accessing accounts, getting passwords,
/// setting passwords, and deleting passwords using the system Keychain.
@interface FLEXKeychain : NSObject
#pragma mark - Classic methods
/// @param serviceName The service for which to return the corresponding password.
/// @param account The account for which to return the corresponding password.
/// @return Returns a string containing the password for a given account and service,
/// or `nil` if the Keychain doesn't have a password for the given parameters.
+ (NSString *)passwordForService:(NSString *)serviceName account:(NSString *)account;
+ (NSString *)passwordForService:(NSString *)serviceName account:(NSString *)account error:(NSError **)error;
/// Returns a nsdata containing the password for a given account and service,
/// or `nil` if the Keychain doesn't have a password for the given parameters.
///
/// @param serviceName The service for which to return the corresponding password.
/// @param account The account for which to return the corresponding password.
/// @return Returns a nsdata containing the password for a given account and service,
/// or `nil` if the Keychain doesn't have a password for the given parameters.
+ (NSData *)passwordDataForService:(NSString *)serviceName account:(NSString *)account;
+ (NSData *)passwordDataForService:(NSString *)serviceName account:(NSString *)account error:(NSError **)error;
/// Deletes a password from the Keychain.
///
/// @param serviceName The service for which to delete the corresponding password.
/// @param account The account for which to delete the corresponding password.
/// @return Returns `YES` on success, or `NO` on failure.
+ (BOOL)deletePasswordForService:(NSString *)serviceName account:(NSString *)account;
+ (BOOL)deletePasswordForService:(NSString *)serviceName account:(NSString *)account error:(NSError **)error;
/// Sets a password in the Keychain.
///
/// @param password The password to store in the Keychain.
/// @param serviceName The service for which to set the corresponding password.
/// @param account The account for which to set the corresponding password.
/// @return Returns `YES` on success, or `NO` on failure.
+ (BOOL)setPassword:(NSString *)password forService:(NSString *)serviceName account:(NSString *)account;
+ (BOOL)setPassword:(NSString *)password forService:(NSString *)serviceName account:(NSString *)account error:(NSError **)error;
/// Sets a password in the Keychain.
///
/// @param password The password to store in the Keychain.
/// @param serviceName The service for which to set the corresponding password.
/// @param account The account for which to set the corresponding password.
/// @return Returns `YES` on success, or `NO` on failure.
+ (BOOL)setPasswordData:(NSData *)password forService:(NSString *)serviceName account:(NSString *)account;
+ (BOOL)setPasswordData:(NSData *)password forService:(NSString *)serviceName account:(NSString *)account error:(NSError **)error;
/// @return An array of dictionaries containing the Keychain's accounts, or `nil` if
/// the Keychain doesn't have any accounts. The order of the objects in the array isn't defined.
///
/// @note See the `NSString` constants declared in FLEXKeychain.h for a list of keys that
/// can be used when accessing the dictionaries returned by this method.
+ (NSArray<NSDictionary<NSString *, id> *> *)allAccounts;
+ (NSArray<NSDictionary<NSString *, id> *> *)allAccounts:(NSError *__autoreleasing *)error;
/// @param serviceName The service for which to return the corresponding accounts.
/// @return An array of dictionaries containing the Keychain's accounts for a given `serviceName`,
/// or `nil` if the Keychain doesn't have any accounts for the given `serviceName`.
/// The order of the objects in the array isn't defined.
///
/// @note See the `NSString` constants declared in FLEXKeychain.h for a list of keys that
/// can be used when accessing the dictionaries returned by this method.
+ (NSArray<NSDictionary<NSString *, id> *> *)accountsForService:(NSString *)serviceName;
+ (NSArray<NSDictionary<NSString *, id> *> *)accountsForService:(NSString *)serviceName error:(NSError *__autoreleasing *)error;
#pragma mark - Configuration
#if __IPHONE_4_0 && TARGET_OS_IPHONE
/// Returns the accessibility type for all future passwords saved to the Keychain.
///
/// @return `NULL` or one of the "Keychain Item Accessibility
/// Constants" used for determining when a keychain item should be readable.
+ (CFTypeRef)accessibilityType;
/// Sets the accessibility type for all future passwords saved to the Keychain.
///
/// @param accessibilityType One of the "Keychain Item Accessibility Constants"
/// used for determining when a keychain item should be readable.
/// If the value is `NULL` (the default), the Keychain default will be used which
/// is highly insecure. You really should use at least `kSecAttrAccessibleAfterFirstUnlock`
/// for background applications or `kSecAttrAccessibleWhenUnlocked` for all
/// other applications.
///
/// @note See Security/SecItem.h
+ (void)setAccessibilityType:(CFTypeRef)accessibilityType;
#endif
@end

View File

@@ -0,0 +1,121 @@
//
// FLEXKeychain.m
//
// Forked from:
// SSKeychain.m in SSKeychain
// Created by Sam Soffes on 5/19/10.
// Copyright (c) 2010-2014 Sam Soffes. All rights reserved.
//
#import "FLEXKeychain.h"
#import "FLEXKeychainQuery.h"
NSString * const kFLEXKeychainErrorDomain = @"com.flipboard.flex";
NSString * const kFLEXKeychainAccountKey = @"acct";
NSString * const kFLEXKeychainCreatedAtKey = @"cdat";
NSString * const kFLEXKeychainClassKey = @"labl";
NSString * const kFLEXKeychainDescriptionKey = @"desc";
NSString * const kFLEXKeychainGroupKey = @"agrp";
NSString * const kFLEXKeychainLabelKey = @"labl";
NSString * const kFLEXKeychainLastModifiedKey = @"mdat";
NSString * const kFLEXKeychainWhereKey = @"svce";
#if __IPHONE_4_0 && TARGET_OS_IPHONE
static CFTypeRef FLEXKeychainAccessibilityType = NULL;
#endif
@implementation FLEXKeychain
+ (NSString *)passwordForService:(NSString *)serviceName account:(NSString *)account {
return [self passwordForService:serviceName account:account error:nil];
}
+ (NSString *)passwordForService:(NSString *)serviceName account:(NSString *)account error:(NSError *__autoreleasing *)error {
FLEXKeychainQuery *query = [FLEXKeychainQuery new];
query.service = serviceName;
query.account = account;
[query fetch:error];
return query.password;
}
+ (NSData *)passwordDataForService:(NSString *)serviceName account:(NSString *)account {
return [self passwordDataForService:serviceName account:account error:nil];
}
+ (NSData *)passwordDataForService:(NSString *)serviceName account:(NSString *)account error:(NSError **)error {
FLEXKeychainQuery *query = [FLEXKeychainQuery new];
query.service = serviceName;
query.account = account;
[query fetch:error];
return query.passwordData;
}
+ (BOOL)deletePasswordForService:(NSString *)serviceName account:(NSString *)account {
return [self deletePasswordForService:serviceName account:account error:nil];
}
+ (BOOL)deletePasswordForService:(NSString *)serviceName account:(NSString *)account error:(NSError *__autoreleasing *)error {
FLEXKeychainQuery *query = [FLEXKeychainQuery new];
query.service = serviceName;
query.account = account;
return [query deleteItem:error];
}
+ (BOOL)setPassword:(NSString *)password forService:(NSString *)serviceName account:(NSString *)account {
return [self setPassword:password forService:serviceName account:account error:nil];
}
+ (BOOL)setPassword:(NSString *)password forService:(NSString *)serviceName account:(NSString *)account error:(NSError *__autoreleasing *)error {
FLEXKeychainQuery *query = [FLEXKeychainQuery new];
query.service = serviceName;
query.account = account;
query.password = password;
return [query save:error];
}
+ (BOOL)setPasswordData:(NSData *)password forService:(NSString *)serviceName account:(NSString *)account {
return [self setPasswordData:password forService:serviceName account:account error:nil];
}
+ (BOOL)setPasswordData:(NSData *)password forService:(NSString *)serviceName account:(NSString *)account error:(NSError **)error {
FLEXKeychainQuery *query = [FLEXKeychainQuery new];
query.service = serviceName;
query.account = account;
query.passwordData = password;
return [query save:error];
}
+ (NSArray *)allAccounts {
return [self allAccounts:nil] ?: @[];
}
+ (NSArray *)allAccounts:(NSError *__autoreleasing *)error {
return [self accountsForService:nil error:error];
}
+ (NSArray *)accountsForService:(NSString *)serviceName {
return [self accountsForService:serviceName error:nil];
}
+ (NSArray *)accountsForService:(NSString *)serviceName error:(NSError *__autoreleasing *)error {
FLEXKeychainQuery *query = [FLEXKeychainQuery new];
query.service = serviceName;
return [query fetchAll:error];
}
#if __IPHONE_4_0 && TARGET_OS_IPHONE
+ (CFTypeRef)accessibilityType {
return FLEXKeychainAccessibilityType;
}
+ (void)setAccessibilityType:(CFTypeRef)accessibilityType {
CFRetain(accessibilityType);
if (FLEXKeychainAccessibilityType) {
CFRelease(FLEXKeychainAccessibilityType);
}
FLEXKeychainAccessibilityType = accessibilityType;
}
#endif
@end

View File

@@ -0,0 +1,112 @@
//
// FLEXKeychainQuery.h
//
// Derived from:
// SSKeychainQuery.h in SSKeychain
// Created by Caleb Davenport on 3/19/13.
// Copyright (c) 2010-2014 Sam Soffes. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <Security/Security.h>
#if __IPHONE_7_0 || __MAC_10_9
// Keychain synchronization available at compile time
#define FLEXKEYCHAIN_SYNCHRONIZATION_AVAILABLE 1
#endif
#if __IPHONE_3_0 || __MAC_10_9
// Keychain access group available at compile time
#define FLEXKEYCHAIN_ACCESS_GROUP_AVAILABLE 1
#endif
#ifdef FLEXKEYCHAIN_SYNCHRONIZATION_AVAILABLE
typedef NS_ENUM(NSUInteger, FLEXKeychainQuerySynchronizationMode) {
FLEXKeychainQuerySynchronizationModeAny,
FLEXKeychainQuerySynchronizationModeNo,
FLEXKeychainQuerySynchronizationModeYes
};
#endif
/// Simple interface for querying or modifying keychain items.
@interface FLEXKeychainQuery : NSObject
/// kSecAttrAccount
@property (nonatomic, copy) NSString *account;
/// kSecAttrService
@property (nonatomic, copy) NSString *service;
/// kSecAttrLabel
@property (nonatomic, copy) NSString *label;
#ifdef FLEXKEYCHAIN_ACCESS_GROUP_AVAILABLE
/// kSecAttrAccessGroup (only used on iOS)
@property (nonatomic, copy) NSString *accessGroup;
#endif
#ifdef FLEXKEYCHAIN_SYNCHRONIZATION_AVAILABLE
/// kSecAttrSynchronizable
@property (nonatomic) FLEXKeychainQuerySynchronizationMode synchronizationMode;
#endif
/// Root storage for password information
@property (nonatomic, copy) NSData *passwordData;
/// This property automatically transitions between an object and the value of
/// `passwordData` using NSKeyedArchiver and NSKeyedUnarchiver.
@property (nonatomic, copy) id<NSCoding> passwordObject;
/// Convenience accessor for setting and getting a password string. Passes through
/// to `passwordData` using UTF-8 string encoding.
@property (nonatomic, copy) NSString *password;
#pragma mark Saving & Deleting
/// Save the receiver's attributes as a keychain item. Existing items with the
/// given account, service, and access group will first be deleted.
///
/// @param error Populated should an error occur.
/// @return `YES` if saving was successful, `NO` otherwise.
- (BOOL)save:(NSError **)error;
/// Delete keychain items that match the given account, service, and access group.
///
/// @param error Populated should an error occur.
/// @return `YES` if saving was successful, `NO` otherwise.
- (BOOL)deleteItem:(NSError **)error;
#pragma mark Fetching
/// Fetch all keychain items that match the given account, service, and access
/// group. The values of `password` and `passwordData` are ignored when fetching.
///
/// @param error Populated should an error occur.
/// @return An array of dictionaries that represent all matching keychain items,
/// or `nil` should an error occur. The order of the items is not determined.
- (NSArray<NSDictionary<NSString *, id> *> *)fetchAll:(NSError **)error;
/// Fetch the keychain item that matches the given account, service, and access
/// group. The `password` and `passwordData` properties will be populated unless
/// an error occurs. The values of `password` and `passwordData` are ignored when
/// fetching.
///
/// @param error Populated should an error occur.
/// @return `YES` if fetching was successful, `NO` otherwise.
- (BOOL)fetch:(NSError **)error;
#pragma mark Synchronization Status
#ifdef FLEXKEYCHAIN_SYNCHRONIZATION_AVAILABLE
/// Returns a boolean indicating if keychain synchronization is available on the device at runtime.
/// The #define FLEXKEYCHAIN_SYNCHRONIZATION_AVAILABLE is only for compile time.
/// If you are checking for the presence of synchronization, you should use this method.
///
/// @return A value indicating if keychain synchronization is available
+ (BOOL)isSynchronizationAvailable;
#endif
@end

View File

@@ -0,0 +1,304 @@
//
// FLEXKeychainQuery.m
// FLEXKeychain
//
// Created by Caleb Davenport on 3/19/13.
// Copyright (c) 2013-2014 Sam Soffes. All rights reserved.
//
#import "FLEXKeychainQuery.h"
#import "FLEXKeychain.h"
@implementation FLEXKeychainQuery
#pragma mark - Public
- (BOOL)save:(NSError *__autoreleasing *)error {
OSStatus status = FLEXKeychainErrorBadArguments;
if (!self.service || !self.account || !self.passwordData) {
if (error) {
*error = [self errorWithCode:status];
}
return NO;
}
NSMutableDictionary *query = nil;
NSMutableDictionary * searchQuery = [self query];
status = SecItemCopyMatching((__bridge CFDictionaryRef)searchQuery, nil);
if (status == errSecSuccess) {//item already exists, update it!
query = [[NSMutableDictionary alloc]init];
query[(__bridge id)kSecValueData] = self.passwordData;
#if __IPHONE_4_0 && TARGET_OS_IPHONE
CFTypeRef accessibilityType = FLEXKeychain.accessibilityType;
if (accessibilityType) {
query[(__bridge id)kSecAttrAccessible] = (__bridge id)accessibilityType;
}
#endif
status = SecItemUpdate((__bridge CFDictionaryRef)(searchQuery), (__bridge CFDictionaryRef)(query));
}else if (status == errSecItemNotFound){//item not found, create it!
query = [self query];
if (self.label) {
query[(__bridge id)kSecAttrLabel] = self.label;
}
query[(__bridge id)kSecValueData] = self.passwordData;
#if __IPHONE_4_0 && TARGET_OS_IPHONE
CFTypeRef accessibilityType = FLEXKeychain.accessibilityType;
if (accessibilityType) {
query[(__bridge id)kSecAttrAccessible] = (__bridge id)accessibilityType;
}
#endif
status = SecItemAdd((__bridge CFDictionaryRef)query, NULL);
}
if (status != errSecSuccess && error != NULL) {
*error = [self errorWithCode:status];
}
return (status == errSecSuccess);
}
- (BOOL)deleteItem:(NSError *__autoreleasing *)error {
OSStatus status = FLEXKeychainErrorBadArguments;
if (!self.service || !self.account) {
if (error) {
*error = [self errorWithCode:status];
}
return NO;
}
NSMutableDictionary *query = [self query];
#if TARGET_OS_IPHONE
status = SecItemDelete((__bridge CFDictionaryRef)query);
#else
// On Mac OS, SecItemDelete will not delete a key created in a different
// app, nor in a different version of the same app.
//
// To replicate the issue, save a password, change to the code and
// rebuild the app, and then attempt to delete that password.
//
// This was true in OS X 10.6 and probably later versions as well.
//
// Work around it by using SecItemCopyMatching and SecKeychainItemDelete.
CFTypeRef result = NULL;
query[(__bridge id)kSecReturnRef] = @YES;
status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result);
if (status == errSecSuccess) {
status = SecKeychainItemDelete((SecKeychainItemRef)result);
CFRelease(result);
}
#endif
if (status != errSecSuccess && error != NULL) {
*error = [self errorWithCode:status];
}
return (status == errSecSuccess);
}
- (NSArray *)fetchAll:(NSError *__autoreleasing *)error {
NSMutableDictionary *query = [self query];
query[(__bridge id)kSecReturnAttributes] = @YES;
query[(__bridge id)kSecMatchLimit] = (__bridge id)kSecMatchLimitAll;
#if __IPHONE_4_0 && TARGET_OS_IPHONE
CFTypeRef accessibilityType = FLEXKeychain.accessibilityType;
if (accessibilityType) {
query[(__bridge id)kSecAttrAccessible] = (__bridge id)accessibilityType;
}
#endif
CFTypeRef result = NULL;
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result);
if (status != errSecSuccess && error != NULL) {
*error = [self errorWithCode:status];
return nil;
}
return (__bridge_transfer NSArray *)result ?: @[];
}
- (BOOL)fetch:(NSError *__autoreleasing *)error {
OSStatus status = FLEXKeychainErrorBadArguments;
if (!self.service || !self.account) {
if (error) {
*error = [self errorWithCode:status];
}
return NO;
}
CFTypeRef result = NULL;
NSMutableDictionary *query = [self query];
query[(__bridge id)kSecReturnData] = @YES;
query[(__bridge id)kSecMatchLimit] = (__bridge id)kSecMatchLimitOne;
status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result);
if (status != errSecSuccess) {
if (error) {
*error = [self errorWithCode:status];
}
return NO;
}
self.passwordData = (__bridge_transfer NSData *)result;
return YES;
}
#pragma mark - Accessors
- (void)setPasswordObject:(id<NSCoding>)object {
self.passwordData = [NSKeyedArchiver archivedDataWithRootObject:object];
}
- (id<NSCoding>)passwordObject {
if (self.passwordData.length) {
return [NSKeyedUnarchiver unarchiveObjectWithData:self.passwordData];
}
return nil;
}
- (void)setPassword:(NSString *)password {
self.passwordData = [password dataUsingEncoding:NSUTF8StringEncoding];
}
- (NSString *)password {
if (self.passwordData.length) {
return [[NSString alloc] initWithData:self.passwordData encoding:NSUTF8StringEncoding];
}
return nil;
}
#pragma mark - Synchronization Status
#ifdef FLEXKEYCHAIN_SYNCHRONIZATION_AVAILABLE
+ (BOOL)isSynchronizationAvailable {
#if TARGET_OS_IPHONE
return YES;
#else
return floor(NSFoundationVersionNumber) > NSFoundationVersionNumber10_8_4;
#endif
}
#endif
#pragma mark - Private
- (NSMutableDictionary *)query {
NSMutableDictionary *dictionary = [NSMutableDictionary new];
dictionary[(__bridge id)kSecClass] = (__bridge id)kSecClassGenericPassword;
if (self.service) {
dictionary[(__bridge id)kSecAttrService] = self.service;
}
if (self.account) {
dictionary[(__bridge id)kSecAttrAccount] = self.account;
}
#ifdef FLEXKEYCHAIN_ACCESS_GROUP_AVAILABLE
#if !TARGET_IPHONE_SIMULATOR
if (self.accessGroup) {
dictionary[(__bridge id)kSecAttrAccessGroup] = self.accessGroup;
}
#endif
#endif
#ifdef FLEXKEYCHAIN_SYNCHRONIZATION_AVAILABLE
if ([[self class] isSynchronizationAvailable]) {
id value;
switch (self.synchronizationMode) {
case FLEXKeychainQuerySynchronizationModeNo: {
value = @NO;
break;
}
case FLEXKeychainQuerySynchronizationModeYes: {
value = @YES;
break;
}
case FLEXKeychainQuerySynchronizationModeAny: {
value = (__bridge id)(kSecAttrSynchronizableAny);
break;
}
}
dictionary[(__bridge id)(kSecAttrSynchronizable)] = value;
}
#endif
return dictionary;
}
- (NSError *)errorWithCode:(OSStatus)code {
static dispatch_once_t onceToken;
static NSBundle *resourcesBundle = nil;
dispatch_once(&onceToken, ^{
NSURL *url = [[NSBundle bundleForClass:[self class]] URLForResource:@"FLEXKeychain" withExtension:@"bundle"];
resourcesBundle = [NSBundle bundleWithURL:url];
});
NSString *message = nil;
switch (code) {
case errSecSuccess: return nil;
case FLEXKeychainErrorBadArguments: message = NSLocalizedStringFromTableInBundle(@"FLEXKeychainErrorBadArguments", @"FLEXKeychain", resourcesBundle, nil); break;
#if TARGET_OS_IPHONE
case errSecUnimplemented: {
message = NSLocalizedStringFromTableInBundle(@"errSecUnimplemented", @"FLEXKeychain", resourcesBundle, nil);
break;
}
case errSecParam: {
message = NSLocalizedStringFromTableInBundle(@"errSecParam", @"FLEXKeychain", resourcesBundle, nil);
break;
}
case errSecAllocate: {
message = NSLocalizedStringFromTableInBundle(@"errSecAllocate", @"FLEXKeychain", resourcesBundle, nil);
break;
}
case errSecNotAvailable: {
message = NSLocalizedStringFromTableInBundle(@"errSecNotAvailable", @"FLEXKeychain", resourcesBundle, nil);
break;
}
case errSecDuplicateItem: {
message = NSLocalizedStringFromTableInBundle(@"errSecDuplicateItem", @"FLEXKeychain", resourcesBundle, nil);
break;
}
case errSecItemNotFound: {
message = NSLocalizedStringFromTableInBundle(@"errSecItemNotFound", @"FLEXKeychain", resourcesBundle, nil);
break;
}
case errSecInteractionNotAllowed: {
message = NSLocalizedStringFromTableInBundle(@"errSecInteractionNotAllowed", @"FLEXKeychain", resourcesBundle, nil);
break;
}
case errSecDecode: {
message = NSLocalizedStringFromTableInBundle(@"errSecDecode", @"FLEXKeychain", resourcesBundle, nil);
break;
}
case errSecAuthFailed: {
message = NSLocalizedStringFromTableInBundle(@"errSecAuthFailed", @"FLEXKeychain", resourcesBundle, nil);
break;
}
default: {
message = NSLocalizedStringFromTableInBundle(@"errSecDefault", @"FLEXKeychain", resourcesBundle, nil);
}
#else
default:
message = (__bridge_transfer NSString *)SecCopyErrorMessageString(code, NULL);
#endif
}
NSDictionary *userInfo = message ? @{ NSLocalizedDescriptionKey : message } : nil;
return [NSError errorWithDomain:kFLEXKeychainErrorDomain code:code userInfo:userInfo];
}
@end

View File

@@ -0,0 +1,14 @@
//
// FLEXKeychainViewController.h
// FLEX
//
// Created by ray on 2019/8/17.
// Copyright © 2020 FLEX Team. All rights reserved.
//
#import "FLEXGlobalsEntry.h"
#import "FLEXFilteringTableViewController.h"
@interface FLEXKeychainViewController : FLEXFilteringTableViewController <FLEXGlobalsEntry>
@end

View File

@@ -0,0 +1,254 @@
//
// FLEXKeychainViewController.m
// FLEX
//
// Created by ray on 2019/8/17.
// Copyright © 2020 FLEX Team. All rights reserved.
//
#import "FLEXKeychain.h"
#import "FLEXKeychainQuery.h"
#import "FLEXKeychainViewController.h"
#import "FLEXTableViewCell.h"
#import "FLEXMutableListSection.h"
#import "FLEXUtility.h"
#import "UIPasteboard+FLEX.h"
#import "UIBarButtonItem+FLEX.h"
@interface FLEXKeychainViewController ()
@property (nonatomic, readonly) FLEXMutableListSection<NSDictionary *> *section;
@end
@implementation FLEXKeychainViewController
- (id)init {
return [self initWithStyle:UITableViewStyleGrouped];
}
#pragma mark - Overrides
- (void)viewDidLoad {
[super viewDidLoad];
[self addToolbarItems:@[
FLEXBarButtonItemSystem(Add, self, @selector(addPressed)),
[FLEXBarButtonItemSystem(Trash, self, @selector(trashPressed:)) flex_withTintColor:UIColor.redColor],
]];
[self reloadData];
}
- (NSArray<FLEXTableViewSection *> *)makeSections {
_section = [FLEXMutableListSection list:FLEXKeychain.allAccounts.mutableCopy
cellConfiguration:^(__kindof FLEXTableViewCell *cell, NSDictionary *item, NSInteger row) {
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
id service = item[kFLEXKeychainWhereKey];
if ([service isKindOfClass:[NSString class]]) {
cell.textLabel.text = service;
cell.detailTextLabel.text = [item[kFLEXKeychainAccountKey] description];
} else {
cell.textLabel.text = [NSString stringWithFormat:
@"[%@]\n\n%@",
NSStringFromClass([service class]),
[service description]
];
}
} filterMatcher:^BOOL(NSString *filterText, NSDictionary *item) {
// Loop over contents of the keychain item looking for a match
for (NSString *field in item.allValues) {
if ([field isKindOfClass:[NSString class]]) {
if ([field localizedCaseInsensitiveContainsString:filterText]) {
return YES;
}
}
}
return NO;
}
];
return @[self.section];
}
/// We always want to show this section
- (NSArray<FLEXTableViewSection *> *)nonemptySections {
return @[self.section];
}
- (void)reloadSections {
self.section.list = FLEXKeychain.allAccounts.mutableCopy;
}
- (void)refreshSectionTitle {
self.section.customTitle = FLEXPluralString(
self.section.filteredList.count, @"items", @"item"
);
}
- (void)reloadData {
[self reloadSections];
[self refreshSectionTitle];
[super reloadData];
}
#pragma mark - Private
- (FLEXKeychainQuery *)queryForItemAtIndex:(NSInteger)idx {
NSDictionary *item = self.section.filteredList[idx];
FLEXKeychainQuery *query = [FLEXKeychainQuery new];
query.service = [item[kFLEXKeychainWhereKey] description];
query.account = [item[kFLEXKeychainAccountKey] description];
query.accessGroup = [item[kFLEXKeychainGroupKey] description];
[query fetch:nil];
return query;
}
- (void)deleteItem:(NSDictionary *)item {
NSError *error = nil;
BOOL success = [FLEXKeychain
deletePasswordForService:item[kFLEXKeychainWhereKey]
account:item[kFLEXKeychainAccountKey]
error:&error
];
if (!success) {
[FLEXAlert makeAlert:^(FLEXAlert *make) {
make.title(@"Error Deleting Item");
make.message(error.localizedDescription);
} showFrom:self];
}
}
#pragma mark Buttons
- (void)trashPressed:(UIBarButtonItem *)sender {
[FLEXAlert makeSheet:^(FLEXAlert *make) {
make.title(@"Clear Keychain");
make.message(@"This will remove all keychain items for this app.\n");
make.message(@"This action cannot be undone. Are you sure?");
make.button(@"Yes, clear the keychain").destructiveStyle().handler(^(NSArray *strings) {
[self confirmClearKeychain];
});
make.button(@"Cancel").cancelStyle();
} showFrom:self source:sender];
}
- (void)confirmClearKeychain {
[FLEXAlert makeAlert:^(FLEXAlert *make) {
make.title(@"ARE YOU SURE?");
make.message(@"This action CANNOT BE UNDONE.\nAre you sure you want to continue?\n");
make.message(@"If you're sure, scroll to confirm.");
make.button(@"Yes, clear the keychain").destructiveStyle().handler(^(NSArray *strings) {
for (id account in self.section.list) {
[self deleteItem:account];
}
[self reloadData];
});
make.button(@"Cancel"); make.button(@"Cancel"); make.button(@"Cancel"); make.button(@"Cancel");
make.button(@"Cancel"); make.button(@"Cancel"); make.button(@"Cancel"); make.button(@"Cancel");
make.button(@"Cancel"); make.button(@"Cancel"); make.button(@"Cancel"); make.button(@"Cancel");
make.button(@"Cancel"); make.button(@"Cancel"); make.button(@"Cancel"); make.button(@"Cancel");
make.button(@"Cancel").cancelStyle();
} showFrom:self];
}
- (void)addPressed {
[FLEXAlert makeAlert:^(FLEXAlert *make) {
make.title(@"Add Keychain Item");
make.textField(@"Service name, i.e. Instagram");
make.textField(@"Account");
make.textField(@"Password");
make.button(@"Cancel").cancelStyle();
make.button(@"Save").handler(^(NSArray<NSString *> *strings) {
// Display errors
NSError *error = nil;
if (![FLEXKeychain setPassword:strings[2] forService:strings[0] account:strings[1] error:&error]) {
[FLEXAlert showAlert:@"Error" message:error.localizedDescription from:self];
}
[self reloadData];
});
} showFrom:self];
}
#pragma mark - FLEXGlobalsEntry
+ (NSString *)globalsEntryTitle:(FLEXGlobalsRow)row {
return @"🔑 Keychain";
}
+ (UIViewController *)globalsEntryViewController:(FLEXGlobalsRow)row {
FLEXKeychainViewController *viewController = [self new];
viewController.title = [self globalsEntryTitle:row];
return viewController;
}
#pragma mark - Table View Data Source
- (void)tableView:(UITableView *)tv commitEditingStyle:(UITableViewCellEditingStyle)style forRowAtIndexPath:(NSIndexPath *)ip {
if (style == UITableViewCellEditingStyleDelete) {
// Update the model
NSDictionary *toRemove = self.section.filteredList[ip.row];
[self deleteItem:toRemove];
[self.section mutate:^(NSMutableArray *list) {
[list removeObject:toRemove];
}];
// Delete the row
[tv deleteRowsAtIndexPaths:@[ip] withRowAnimation:UITableViewRowAnimationAutomatic];
// Update the title by refreshing the section without disturbing the delete animation
//
// This is an ugly hack, but literally nothing else works, save for manually getting
// the header and setting its title, which I personally think is worse since it
// would need to make assumptions about the default style of the header (CAPS)
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self refreshSectionTitle];
[tv reloadSections:[NSIndexSet indexSetWithIndex:0] withRowAnimation:UITableViewRowAnimationNone];
});
}
}
#pragma mark - Table View Delegate
- (BOOL)tableView:(UITableView *)tableView shouldHighlightRowAtIndexPath:(NSIndexPath *)indexPath {
return YES;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
FLEXKeychainQuery *query = [self queryForItemAtIndex:indexPath.row];
[FLEXAlert makeAlert:^(FLEXAlert *make) {
make.title(query.service);
make.message(@"Service: ").message(query.service);
make.message(@"\nAccount: ").message(query.account);
make.message(@"\nPassword: ").message(query.password);
make.message(@"\nGroup: ").message(query.accessGroup);
make.button(@"Copy Service").handler(^(NSArray<NSString *> *strings) {
[UIPasteboard.generalPasteboard flex_copy:query.service];
});
make.button(@"Copy Account").handler(^(NSArray<NSString *> *strings) {
[UIPasteboard.generalPasteboard flex_copy:query.account];
});
make.button(@"Copy Password").handler(^(NSArray<NSString *> *strings) {
[UIPasteboard.generalPasteboard flex_copy:query.password];
});
make.button(@"Dismiss").cancelStyle();
} showFrom:self];
[tableView deselectRowAtIndexPath:indexPath animated:YES];
}
@end

View File

@@ -0,0 +1,20 @@
Copyright (c) 2010-2012 Sam Soffes.
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,209 @@
//
// Taken from https://github.com/llvm-mirror/lldb/blob/master/tools/debugserver/source/MacOSX/DarwinLog/ActivityStreamSPI.h
// by Tanner Bennett on 03/03/2019 with minimal modifications.
//
//===-- ActivityStreamAPI.h -------------------------------------*- C++ -*-===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
#ifndef ActivityStreamSPI_h
#define ActivityStreamSPI_h
#include <Foundation/Foundation.h>
#include <sys/time.h>
// #include <xpc/xpc.h>
/* By default, XPC objects are declared as Objective-C types when building with
* an Objective-C compiler. This allows them to participate in ARC, in RR
* management by the Blocks runtime and in leaks checking by the static
* analyzer, and enables them to be added to Cocoa collections.
*
* See <os/object.h> for details.
*/
#if !TARGET_OS_MACCATALYST
#if OS_OBJECT_USE_OBJC
OS_OBJECT_DECL(xpc_object);
#else
typedef void * xpc_object_t;
#endif
#endif
#define OS_ACTIVITY_MAX_CALLSTACK 32
// Enums
typedef NS_ENUM(uint32_t, os_activity_stream_flag_t) {
OS_ACTIVITY_STREAM_PROCESS_ONLY = 0x00000001,
OS_ACTIVITY_STREAM_SKIP_DECODE = 0x00000002,
OS_ACTIVITY_STREAM_PAYLOAD = 0x00000004,
OS_ACTIVITY_STREAM_HISTORICAL = 0x00000008,
OS_ACTIVITY_STREAM_CALLSTACK = 0x00000010,
OS_ACTIVITY_STREAM_DEBUG = 0x00000020,
OS_ACTIVITY_STREAM_BUFFERED = 0x00000040,
OS_ACTIVITY_STREAM_NO_SENSITIVE = 0x00000080,
OS_ACTIVITY_STREAM_INFO = 0x00000100,
OS_ACTIVITY_STREAM_PROMISCUOUS = 0x00000200,
OS_ACTIVITY_STREAM_PRECISE_TIMESTAMPS = 0x00000200
};
typedef NS_ENUM(uint32_t, os_activity_stream_type_t) {
OS_ACTIVITY_STREAM_TYPE_ACTIVITY_CREATE = 0x0201,
OS_ACTIVITY_STREAM_TYPE_ACTIVITY_TRANSITION = 0x0202,
OS_ACTIVITY_STREAM_TYPE_ACTIVITY_USERACTION = 0x0203,
OS_ACTIVITY_STREAM_TYPE_TRACE_MESSAGE = 0x0300,
OS_ACTIVITY_STREAM_TYPE_LOG_MESSAGE = 0x0400,
OS_ACTIVITY_STREAM_TYPE_LEGACY_LOG_MESSAGE = 0x0480,
OS_ACTIVITY_STREAM_TYPE_SIGNPOST_BEGIN = 0x0601,
OS_ACTIVITY_STREAM_TYPE_SIGNPOST_END = 0x0602,
OS_ACTIVITY_STREAM_TYPE_SIGNPOST_EVENT = 0x0603,
OS_ACTIVITY_STREAM_TYPE_STATEDUMP_EVENT = 0x0A00,
};
typedef NS_ENUM(uint32_t, os_activity_stream_event_t) {
OS_ACTIVITY_STREAM_EVENT_STARTED = 1,
OS_ACTIVITY_STREAM_EVENT_STOPPED = 2,
OS_ACTIVITY_STREAM_EVENT_FAILED = 3,
OS_ACTIVITY_STREAM_EVENT_CHUNK_STARTED = 4,
OS_ACTIVITY_STREAM_EVENT_CHUNK_FINISHED = 5,
};
// Types
typedef uint64_t os_activity_id_t;
typedef struct os_activity_stream_s *os_activity_stream_t;
typedef struct os_activity_stream_entry_s *os_activity_stream_entry_t;
#define OS_ACTIVITY_STREAM_COMMON() \
uint64_t trace_id; \
uint64_t timestamp; \
uint64_t thread; \
const uint8_t *image_uuid; \
const char *image_path; \
struct timeval tv_gmt; \
struct timezone tz; \
uint32_t offset
typedef struct os_activity_stream_common_s {
OS_ACTIVITY_STREAM_COMMON();
} * os_activity_stream_common_t;
struct os_activity_create_s {
OS_ACTIVITY_STREAM_COMMON();
const char *name;
os_activity_id_t creator_aid;
uint64_t unique_pid;
};
struct os_activity_transition_s {
OS_ACTIVITY_STREAM_COMMON();
os_activity_id_t transition_id;
};
typedef struct os_log_message_s {
OS_ACTIVITY_STREAM_COMMON();
const char *format;
const uint8_t *buffer;
size_t buffer_sz;
const uint8_t *privdata;
size_t privdata_sz;
const char *subsystem;
const char *category;
uint32_t oversize_id;
uint8_t ttl;
bool persisted;
} * os_log_message_t;
typedef struct os_trace_message_v2_s {
OS_ACTIVITY_STREAM_COMMON();
const char *format;
const void *buffer;
size_t bufferLen;
xpc_object_t __unsafe_unretained payload;
} * os_trace_message_v2_t;
typedef struct os_activity_useraction_s {
OS_ACTIVITY_STREAM_COMMON();
const char *action;
bool persisted;
} * os_activity_useraction_t;
typedef struct os_signpost_s {
OS_ACTIVITY_STREAM_COMMON();
const char *format;
const uint8_t *buffer;
size_t buffer_sz;
const uint8_t *privdata;
size_t privdata_sz;
const char *subsystem;
const char *category;
uint64_t duration_nsec;
uint32_t callstack_depth;
uint64_t callstack[OS_ACTIVITY_MAX_CALLSTACK];
} * os_signpost_t;
typedef struct os_activity_statedump_s {
OS_ACTIVITY_STREAM_COMMON();
char *message;
size_t message_size;
char image_path_buffer[PATH_MAX];
} * os_activity_statedump_t;
struct os_activity_stream_entry_s {
os_activity_stream_type_t type;
// information about the process streaming the data
pid_t pid;
uint64_t proc_id;
const uint8_t *proc_imageuuid;
const char *proc_imagepath;
// the activity associated with this streamed event
os_activity_id_t activity_id;
os_activity_id_t parent_id;
union {
struct os_activity_stream_common_s common;
struct os_activity_create_s activity_create;
struct os_activity_transition_s activity_transition;
struct os_log_message_s log_message;
struct os_trace_message_v2_s trace_message;
struct os_activity_useraction_s useraction;
struct os_signpost_s signpost;
struct os_activity_statedump_s statedump;
};
};
// Blocks
typedef bool (^os_activity_stream_block_t)(os_activity_stream_entry_t entry,
int error);
typedef void (^os_activity_stream_event_block_t)(
os_activity_stream_t stream, os_activity_stream_event_t event);
// SPI entry point prototypes
typedef os_activity_stream_t (*os_activity_stream_for_pid_t)(
pid_t pid, os_activity_stream_flag_t flags,
os_activity_stream_block_t stream_block);
typedef void (*os_activity_stream_resume_t)(os_activity_stream_t stream);
typedef void (*os_activity_stream_cancel_t)(os_activity_stream_t stream);
typedef char *(*os_log_copy_formatted_message_t)(os_log_message_t log_message);
typedef void (*os_activity_stream_set_event_handler_t)(
os_activity_stream_t stream, os_activity_stream_event_block_t block);
#endif /* ActivityStreamSPI_h */

View File

@@ -0,0 +1,18 @@
//
// FLEXASLLogController.h
// FLEX
//
// Created by Tanner on 3/14/19.
// Copyright © 2020 FLEX Team. All rights reserved.
//
#import "FLEXLogController.h"
@interface FLEXASLLogController : NSObject <FLEXLogController>
/// Guaranteed to call back on the main thread.
+ (instancetype)withUpdateHandler:(void(^)(NSArray<FLEXSystemLogMessage *> *newMessages))newMessagesHandler;
- (BOOL)startMonitoring;
@end

View File

@@ -0,0 +1,147 @@
//
// FLEXASLLogController.m
// FLEX
//
// Created by Tanner on 3/14/19.
// Copyright © 2020 FLEX Team. All rights reserved.
//
#import "FLEXASLLogController.h"
#import <asl.h>
// Querying the ASL is much slower in the simulator. We need a longer polling interval to keep things responsive.
#if TARGET_IPHONE_SIMULATOR
#define updateInterval 5.0
#else
#define updateInterval 0.5
#endif
@interface FLEXASLLogController ()
@property (nonatomic, readonly) void (^updateHandler)(NSArray<FLEXSystemLogMessage *> *);
@property (nonatomic) NSTimer *logUpdateTimer;
@property (nonatomic, readonly) NSMutableIndexSet *logMessageIdentifiers;
// ASL stuff
@property (nonatomic) NSUInteger heapSize;
@property (nonatomic) dispatch_queue_t logQueue;
@property (nonatomic) dispatch_io_t io;
@property (nonatomic) NSString *remaining;
@property (nonatomic) int stderror;
@property (nonatomic) NSString *lastTimestamp;
@end
@implementation FLEXASLLogController
+ (instancetype)withUpdateHandler:(void(^)(NSArray<FLEXSystemLogMessage *> *newMessages))newMessagesHandler {
return [[self alloc] initWithUpdateHandler:newMessagesHandler];
}
- (id)initWithUpdateHandler:(void(^)(NSArray<FLEXSystemLogMessage *> *newMessages))newMessagesHandler {
NSParameterAssert(newMessagesHandler);
self = [super init];
if (self) {
_updateHandler = newMessagesHandler;
_logMessageIdentifiers = [NSMutableIndexSet new];
self.logUpdateTimer = [NSTimer scheduledTimerWithTimeInterval:updateInterval
target:self
selector:@selector(updateLogMessages)
userInfo:nil
repeats:YES];
}
return self;
}
- (void)dealloc {
[self.logUpdateTimer invalidate];
}
- (BOOL)startMonitoring {
[self.logUpdateTimer fire];
return YES;
}
- (void)updateLogMessages {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSArray<FLEXSystemLogMessage *> *newMessages;
@synchronized (self) {
newMessages = [self newLogMessagesForCurrentProcess];
if (!newMessages.count) {
return;
}
for (FLEXSystemLogMessage *message in newMessages) {
[self.logMessageIdentifiers addIndex:(NSUInteger)message.messageID];
}
self.lastTimestamp = @(asl_get(newMessages.lastObject.aslMessage, ASL_KEY_TIME) ?: "null");
}
dispatch_async(dispatch_get_main_queue(), ^{
self.updateHandler(newMessages);
});
});
}
#pragma mark - Log Message Fetching
- (NSArray<FLEXSystemLogMessage *> *)newLogMessagesForCurrentProcess {
if (!self.logMessageIdentifiers.count) {
return [self allLogMessagesForCurrentProcess];
}
aslresponse response = [self ASLMessageListForCurrentProcess];
aslmsg aslMessage = NULL;
NSMutableArray<FLEXSystemLogMessage *> *newMessages = [NSMutableArray new];
while ((aslMessage = asl_next(response))) {
NSUInteger messageID = (NSUInteger)atoll(asl_get(aslMessage, ASL_KEY_MSG_ID));
if (![self.logMessageIdentifiers containsIndex:messageID]) {
[newMessages addObject:[FLEXSystemLogMessage logMessageFromASLMessage:aslMessage]];
}
}
asl_release(response);
return newMessages;
}
- (aslresponse)ASLMessageListForCurrentProcess {
static NSString *pidString = nil;
if (!pidString) {
pidString = @([NSProcessInfo.processInfo processIdentifier]).stringValue;
}
// Create system log query object.
asl_object_t query = asl_new(ASL_TYPE_QUERY);
// Filter for messages from the current process.
// Note that this appears to happen by default on device, but is required in the simulator.
asl_set_query(query, ASL_KEY_PID, pidString.UTF8String, ASL_QUERY_OP_EQUAL);
// Filter for messages after the last retrieved message.
if (self.lastTimestamp) {
asl_set_query(query, ASL_KEY_TIME, self.lastTimestamp.UTF8String, ASL_QUERY_OP_GREATER);
}
return asl_search(NULL, query);
}
- (NSArray<FLEXSystemLogMessage *> *)allLogMessagesForCurrentProcess {
aslresponse response = [self ASLMessageListForCurrentProcess];
aslmsg aslMessage = NULL;
NSMutableArray<FLEXSystemLogMessage *> *logMessages = [NSMutableArray new];
while ((aslMessage = asl_next(response))) {
[logMessages addObject:[FLEXSystemLogMessage logMessageFromASLMessage:aslMessage]];
}
asl_release(response);
return logMessages;
}
@end

View File

@@ -0,0 +1,19 @@
//
// FLEXLogController.h
// FLEX
//
// Created by Tanner on 3/17/19.
// Copyright © 2020 FLEX Team. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "FLEXSystemLogMessage.h"
@protocol FLEXLogController <NSObject>
/// Guaranteed to call back on the main thread.
+ (instancetype)withUpdateHandler:(void(^)(NSArray<FLEXSystemLogMessage *> *newMessages))newMessagesHandler;
- (BOOL)startMonitoring;
@end

View File

@@ -0,0 +1,27 @@
//
// FLEXOSLogController.h
// FLEX
//
// Created by Tanner on 12/19/18.
// Copyright © 2020 FLEX Team. All rights reserved.
//
#import "FLEXLogController.h"
#define FLEXOSLogAvailable() (NSProcessInfo.processInfo.operatingSystemVersion.majorVersion >= 10)
/// The log controller used for iOS 10 and up.
@interface FLEXOSLogController : NSObject <FLEXLogController>
+ (instancetype)withUpdateHandler:(void(^)(NSArray<FLEXSystemLogMessage *> *newMessages))newMessagesHandler;
- (BOOL)startMonitoring;
/// Whether log messages are to be recorded and kept in-memory in the background.
/// You do not need to initialize this value, only change it.
@property (nonatomic) BOOL persistent;
/// Used mostly internally, but also used by the log VC to persist messages
/// that were created prior to enabling persistence.
@property (nonatomic) NSMutableArray<FLEXSystemLogMessage *> *messages;
@end

View File

@@ -0,0 +1,214 @@
//
// FLEXOSLogController.m
// FLEX
//
// Created by Tanner on 12/19/18.
// Copyright © 2020 FLEX Team. All rights reserved.
//
#import "FLEXOSLogController.h"
#import "NSUserDefaults+FLEX.h"
#include <dlfcn.h>
#include "ActivityStreamAPI.h"
static os_activity_stream_for_pid_t OSActivityStreamForPID;
static os_activity_stream_resume_t OSActivityStreamResume;
static os_activity_stream_cancel_t OSActivityStreamCancel;
static os_log_copy_formatted_message_t OSLogCopyFormattedMessage;
static os_activity_stream_set_event_handler_t OSActivityStreamSetEventHandler;
static int (*proc_name)(int, char *, unsigned int);
static int (*proc_listpids)(uint32_t, uint32_t, void*, int);
static uint8_t (*OSLogGetType)(void *);
@interface FLEXOSLogController ()
+ (FLEXOSLogController *)sharedLogController;
@property (nonatomic) void (^updateHandler)(NSArray<FLEXSystemLogMessage *> *);
@property (nonatomic) BOOL canPrint;
@property (nonatomic) int filterPid;
@property (nonatomic) BOOL levelInfo;
@property (nonatomic) BOOL subsystemInfo;
@property (nonatomic) os_activity_stream_t stream;
@end
@implementation FLEXOSLogController
+ (void)load {
// Persist logs when the app launches on iOS 10 if we have persistent logs turned on
if (FLEXOSLogAvailable()) {
if (NSUserDefaults.standardUserDefaults.flex_cacheOSLogMessages) {
[self sharedLogController].persistent = YES;
[[self sharedLogController] startMonitoring];
}
}
}
+ (instancetype)sharedLogController {
static FLEXOSLogController *shared = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
shared = [self new];
});
return shared;
}
+ (instancetype)withUpdateHandler:(void(^)(NSArray<FLEXSystemLogMessage *> *newMessages))newMessagesHandler {
FLEXOSLogController *shared = [self sharedLogController];
shared.updateHandler = newMessagesHandler;
return shared;
}
- (id)init {
NSAssert(FLEXOSLogAvailable(), @"os_log is only available on iOS 10 and up");
self = [super init];
if (self) {
_filterPid = NSProcessInfo.processInfo.processIdentifier;
_levelInfo = NO;
_subsystemInfo = NO;
}
return self;
}
- (void)dealloc {
OSActivityStreamCancel(self.stream);
_stream = nil;
}
- (void)setPersistent:(BOOL)persistent {
if (_persistent == persistent) return;
_persistent = persistent;
self.messages = persistent ? [NSMutableArray new] : nil;
}
- (BOOL)startMonitoring {
if (![self lookupSPICalls]) {
// >= iOS 10 is required
return NO;
}
// Are we already monitoring?
if (self.stream) {
// Should we send out the "persisted" messages?
if (self.updateHandler && self.messages.count) {
dispatch_async(dispatch_get_main_queue(), ^{
self.updateHandler(self.messages);
});
}
return YES;
}
// Stream entry handler
os_activity_stream_block_t block = ^bool(os_activity_stream_entry_t entry, int error) {
return [self handleStreamEntry:entry error:error];
};
// Controls which types of messages we see
// 'Historical' appears to just show NSLog stuff
uint32_t activity_stream_flags = OS_ACTIVITY_STREAM_HISTORICAL;
activity_stream_flags |= OS_ACTIVITY_STREAM_PROCESS_ONLY;
// activity_stream_flags |= OS_ACTIVITY_STREAM_PROCESS_ONLY;
self.stream = OSActivityStreamForPID(self.filterPid, activity_stream_flags, block);
// Specify the stream-related event handler
OSActivityStreamSetEventHandler(self.stream, [self streamEventHandlerBlock]);
// Start the stream
OSActivityStreamResume(self.stream);
return YES;
}
- (BOOL)lookupSPICalls {
static BOOL hasSPI = NO;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
void *handle = dlopen("/System/Library/PrivateFrameworks/LoggingSupport.framework/LoggingSupport", RTLD_NOW);
OSActivityStreamForPID = (os_activity_stream_for_pid_t)dlsym(handle, "os_activity_stream_for_pid");
OSActivityStreamResume = (os_activity_stream_resume_t)dlsym(handle, "os_activity_stream_resume");
OSActivityStreamCancel = (os_activity_stream_cancel_t)dlsym(handle, "os_activity_stream_cancel");
OSLogCopyFormattedMessage = (os_log_copy_formatted_message_t)dlsym(handle, "os_log_copy_formatted_message");
OSActivityStreamSetEventHandler = (os_activity_stream_set_event_handler_t)dlsym(handle, "os_activity_stream_set_event_handler");
proc_name = (int(*)(int, char *, unsigned int))dlsym(handle, "proc_name");
proc_listpids = (int(*)(uint32_t, uint32_t, void*, int))dlsym(handle, "proc_listpids");
OSLogGetType = (uint8_t(*)(void *))dlsym(handle, "os_log_get_type");
hasSPI = (OSActivityStreamForPID != NULL) &&
(OSActivityStreamResume != NULL) &&
(OSActivityStreamCancel != NULL) &&
(OSLogCopyFormattedMessage != NULL) &&
(OSActivityStreamSetEventHandler != NULL) &&
(OSLogGetType != NULL) &&
(proc_name != NULL);
});
return hasSPI;
}
- (BOOL)handleStreamEntry:(os_activity_stream_entry_t)entry error:(int)error {
if (!self.canPrint || (self.filterPid != -1 && entry->pid != self.filterPid)) {
return YES;
}
if (!error && entry) {
if (entry->type == OS_ACTIVITY_STREAM_TYPE_LOG_MESSAGE ||
entry->type == OS_ACTIVITY_STREAM_TYPE_LEGACY_LOG_MESSAGE) {
os_log_message_t log_message = &entry->log_message;
// Get date
NSDate *date = [NSDate dateWithTimeIntervalSince1970:log_message->tv_gmt.tv_sec];
// Get log message text
// https://github.com/limneos/oslog/issues/1
// https://github.com/FLEXTool/FLEX/issues/564
const char *messageText = OSLogCopyFormattedMessage(log_message) ?: "";
// move messageText from stack to heap
NSString *msg = [NSString stringWithUTF8String:messageText];
dispatch_async(dispatch_get_main_queue(), ^{
FLEXSystemLogMessage *message = [FLEXSystemLogMessage logMessageFromDate:date text:msg];
if (self.persistent) {
[self.messages addObject:message];
}
if (self.updateHandler) {
self.updateHandler(@[message]);
}
});
}
}
return YES;
}
- (os_activity_stream_event_block_t)streamEventHandlerBlock {
return [^void(os_activity_stream_t stream, os_activity_stream_event_t event) {
switch (event) {
case OS_ACTIVITY_STREAM_EVENT_STARTED:
self.canPrint = YES;
break;
case OS_ACTIVITY_STREAM_EVENT_STOPPED:
break;
case OS_ACTIVITY_STREAM_EVENT_FAILED:
break;
case OS_ACTIVITY_STREAM_EVENT_CHUNK_STARTED:
break;
case OS_ACTIVITY_STREAM_EVENT_CHUNK_FINISHED:
break;
default:
printf("=== Unhandled case ===\n");
break;
}
} copy];
}
@end

View File

@@ -0,0 +1,23 @@
//
// FLEXSystemLogCell.h
// FLEX
//
// Created by Ryan Olson on 1/25/15.
// Copyright (c) 2020 FLEX Team. All rights reserved.
//
#import "FLEXTableViewCell.h"
@class FLEXSystemLogMessage;
extern NSString *const kFLEXSystemLogCellIdentifier;
@interface FLEXSystemLogCell : FLEXTableViewCell
@property (nonatomic) FLEXSystemLogMessage *logMessage;
@property (nonatomic, copy) NSString *highlightedText;
+ (NSString *)displayedTextForLogMessage:(FLEXSystemLogMessage *)logMessage;
+ (CGFloat)preferredHeightForLogMessage:(FLEXSystemLogMessage *)logMessage inWidth:(CGFloat)width;
@end

View File

@@ -0,0 +1,119 @@
//
// FLEXSystemLogCell.m
// FLEX
//
// Created by Ryan Olson on 1/25/15.
// Copyright (c) 2020 FLEX Team. All rights reserved.
//
#import "FLEXSystemLogCell.h"
#import "FLEXSystemLogMessage.h"
#import "UIFont+FLEX.h"
NSString *const kFLEXSystemLogCellIdentifier = @"FLEXSystemLogCellIdentifier";
@interface FLEXSystemLogCell ()
@property (nonatomic) UILabel *logMessageLabel;
@property (nonatomic) NSAttributedString *logMessageAttributedText;
@end
@implementation FLEXSystemLogCell
- (void)postInit {
[super postInit];
self.logMessageLabel = [UILabel new];
self.logMessageLabel.numberOfLines = 0;
self.separatorInset = UIEdgeInsetsZero;
self.selectionStyle = UITableViewCellSelectionStyleNone;
[self.contentView addSubview:self.logMessageLabel];
}
- (void)setLogMessage:(FLEXSystemLogMessage *)logMessage {
if (![_logMessage isEqual:logMessage]) {
_logMessage = logMessage;
self.logMessageAttributedText = nil;
[self setNeedsLayout];
}
}
- (void)setHighlightedText:(NSString *)highlightedText {
if (![_highlightedText isEqual:highlightedText]) {
_highlightedText = highlightedText;
self.logMessageAttributedText = nil;
[self setNeedsLayout];
}
}
- (NSAttributedString *)logMessageAttributedText {
if (!_logMessageAttributedText) {
_logMessageAttributedText = [[self class] attributedTextForLogMessage:self.logMessage highlightedText:self.highlightedText];
}
return _logMessageAttributedText;
}
static const UIEdgeInsets kFLEXLogMessageCellInsets = {10.0, 10.0, 10.0, 10.0};
- (void)layoutSubviews {
[super layoutSubviews];
self.logMessageLabel.attributedText = self.logMessageAttributedText;
self.logMessageLabel.frame = UIEdgeInsetsInsetRect(self.contentView.bounds, kFLEXLogMessageCellInsets);
}
#pragma mark - Stateless helpers
+ (NSAttributedString *)attributedTextForLogMessage:(FLEXSystemLogMessage *)logMessage highlightedText:(NSString *)highlightedText {
NSString *text = [self displayedTextForLogMessage:logMessage];
NSDictionary<NSString *, id> *attributes = @{ NSFontAttributeName : UIFont.flex_codeFont };
NSAttributedString *attributedText = [[NSAttributedString alloc] initWithString:text attributes:attributes];
if (highlightedText.length > 0) {
NSMutableAttributedString *mutableAttributedText = attributedText.mutableCopy;
NSMutableDictionary<NSString *, id> *highlightAttributes = attributes.mutableCopy;
highlightAttributes[NSBackgroundColorAttributeName] = UIColor.yellowColor;
NSRange remainingSearchRange = NSMakeRange(0, text.length);
while (remainingSearchRange.location < text.length) {
remainingSearchRange.length = text.length - remainingSearchRange.location;
NSRange foundRange = [text rangeOfString:highlightedText options:NSCaseInsensitiveSearch range:remainingSearchRange];
if (foundRange.location != NSNotFound) {
remainingSearchRange.location = foundRange.location + foundRange.length;
[mutableAttributedText setAttributes:highlightAttributes range:foundRange];
} else {
break;
}
}
attributedText = mutableAttributedText;
}
return attributedText;
}
+ (NSString *)displayedTextForLogMessage:(FLEXSystemLogMessage *)logMessage {
return [NSString stringWithFormat:@"%@: %@", [self logTimeStringFromDate:logMessage.date], logMessage.messageText];
}
+ (CGFloat)preferredHeightForLogMessage:(FLEXSystemLogMessage *)logMessage inWidth:(CGFloat)width {
UIEdgeInsets insets = kFLEXLogMessageCellInsets;
CGFloat availableWidth = width - insets.left - insets.right;
NSAttributedString *attributedLogText = [self attributedTextForLogMessage:logMessage highlightedText:nil];
CGSize labelSize = [attributedLogText boundingRectWithSize:CGSizeMake(availableWidth, CGFLOAT_MAX) options:NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading context:nil].size;
return labelSize.height + insets.top + insets.bottom;
}
+ (NSString *)logTimeStringFromDate:(NSDate *)date {
static NSDateFormatter *formatter = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
formatter = [NSDateFormatter new];
formatter.dateFormat = @"yyyy-MM-dd HH:mm:ss.SSS";
});
return [formatter stringFromDate:date];
}
@end

View File

@@ -0,0 +1,30 @@
//
// FLEXSystemLogMessage.h
// FLEX
//
// Created by Ryan Olson on 1/25/15.
// Copyright (c) 2020 FLEX Team. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <asl.h>
#import "ActivityStreamAPI.h"
NS_ASSUME_NONNULL_BEGIN
@interface FLEXSystemLogMessage : NSObject
+ (instancetype)logMessageFromASLMessage:(aslmsg)aslMessage;
+ (instancetype)logMessageFromDate:(NSDate *)date text:(NSString *)text;
// ASL specific properties
@property (nonatomic, readonly, nullable) NSString *sender;
@property (nonatomic, readonly, nullable) aslmsg aslMessage;
@property (nonatomic, readonly) NSDate *date;
@property (nonatomic, readonly) NSString *messageText;
@property (nonatomic, readonly) long long messageID;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,88 @@
//
// FLEXSystemLogMessage.m
// FLEX
//
// Created by Ryan Olson on 1/25/15.
// Copyright (c) 2020 FLEX Team. All rights reserved.
//
#import "FLEXSystemLogMessage.h"
@implementation FLEXSystemLogMessage
+ (instancetype)logMessageFromASLMessage:(aslmsg)aslMessage {
NSDate *date = nil;
NSString *sender = nil, *text = nil;
long long identifier = 0;
const char *timestamp = asl_get(aslMessage, ASL_KEY_TIME);
if (timestamp) {
NSTimeInterval timeInterval = [@(timestamp) integerValue];
const char *nanoseconds = asl_get(aslMessage, ASL_KEY_TIME_NSEC);
if (nanoseconds) {
timeInterval += [@(nanoseconds) doubleValue] / NSEC_PER_SEC;
}
date = [NSDate dateWithTimeIntervalSince1970:timeInterval];
}
const char *s = asl_get(aslMessage, ASL_KEY_SENDER);
if (s) {
sender = @(s);
}
const char *messageText = asl_get(aslMessage, ASL_KEY_MSG);
if (messageText) {
text = @(messageText);
}
const char *messageID = asl_get(aslMessage, ASL_KEY_MSG_ID);
if (messageID) {
identifier = [@(messageID) longLongValue];
}
FLEXSystemLogMessage *message = [[self alloc] initWithDate:date sender:sender text:text messageID:identifier];
message->_aslMessage = aslMessage;
return message;
}
+ (instancetype)logMessageFromDate:(NSDate *)date text:(NSString *)text {
return [[self alloc] initWithDate:date sender:nil text:text messageID:0];
}
- (id)initWithDate:(NSDate *)date sender:(NSString *)sender text:(NSString *)text messageID:(long long)identifier {
self = [super init];
if (self) {
_date = date;
_sender = sender;
_messageText = text;
_messageID = identifier;
}
return self;
}
- (BOOL)isEqual:(id)object {
if ([object isKindOfClass:[self class]]) {
if (self.messageID) {
// Only ASL uses messageID, otherwise it is 0
return self.messageID == [object messageID];
} else {
// Test message texts and dates for OS Log
return [self.messageText isEqual:[object messageText]] &&
[self.date isEqualToDate:[object date]];
}
}
return NO;
}
- (NSUInteger)hash {
return (NSUInteger)self.messageID;
}
- (NSString *)description {
NSString *escaped = [self.messageText stringByReplacingOccurrencesOfString:@"\n" withString:@"\\n"];
return [NSString stringWithFormat:@"(%@) %@", @(self.messageText.length), escaped];
}
@end

View File

@@ -0,0 +1,14 @@
//
// FLEXSystemLogViewController.h
// FLEX
//
// Created by Ryan Olson on 1/19/15.
// Copyright (c) 2020 FLEX Team. All rights reserved.
//
#import "FLEXFilteringTableViewController.h"
#import "FLEXGlobalsEntry.h"
@interface FLEXSystemLogViewController : FLEXFilteringTableViewController <FLEXGlobalsEntry>
@end

View File

@@ -0,0 +1,293 @@
//
// FLEXSystemLogViewController.m
// FLEX
//
// Created by Ryan Olson on 1/19/15.
// Copyright (c) 2020 FLEX Team. All rights reserved.
//
#import "FLEXSystemLogViewController.h"
#import "FLEXASLLogController.h"
#import "FLEXOSLogController.h"
#import "FLEXSystemLogCell.h"
#import "FLEXMutableListSection.h"
#import "FLEXUtility.h"
#import "FLEXColor.h"
#import "FLEXResources.h"
#import "UIBarButtonItem+FLEX.h"
#import "NSUserDefaults+FLEX.h"
#import "flex_fishhook.h"
#import <dlfcn.h>
@interface FLEXSystemLogViewController ()
@property (nonatomic, readonly) FLEXMutableListSection<FLEXSystemLogMessage *> *logMessages;
@property (nonatomic, readonly) id<FLEXLogController> logController;
@end
static void (*MSHookFunction)(void *symbol, void *replace, void **result);
static BOOL FLEXDidHookNSLog = NO;
static BOOL FLEXNSLogHookWorks = NO;
BOOL (*os_log_shim_enabled)(void *addr) = nil;
BOOL (*orig_os_log_shim_enabled)(void *addr) = nil;
static BOOL my_os_log_shim_enabled(void *addr) {
return NO;
}
@implementation FLEXSystemLogViewController
#pragma mark - Initialization
+ (void)load {
// User must opt-into disabling os_log
if (!NSUserDefaults.standardUserDefaults.flex_disableOSLog) {
return;
}
// Thanks to @Ram4096 on GitHub for telling me that
// os_log is conditionally enabled by the SDK version
void *addr = __builtin_return_address(0);
void *libsystem_trace = dlopen("/usr/lib/system/libsystem_trace.dylib", RTLD_LAZY);
os_log_shim_enabled = dlsym(libsystem_trace, "os_log_shim_enabled");
if (!os_log_shim_enabled) {
return;
}
FLEXDidHookNSLog = flex_rebind_symbols((struct rebinding[1]) {{
"os_log_shim_enabled",
(void *)my_os_log_shim_enabled,
(void **)&orig_os_log_shim_enabled
}}, 1) == 0;
if (FLEXDidHookNSLog && orig_os_log_shim_enabled != nil) {
// Check if our rebinding worked
FLEXNSLogHookWorks = my_os_log_shim_enabled(addr) == NO;
}
// So, just because we rebind the lazily loaded symbol for
// this function doesn't mean it's even going to be used.
// While it seems to be sufficient for the simulator, for
// whatever reason it is not sufficient on-device. We need
// to actually hook the function with something like Substrate.
// Check if we have substrate, and if so use that instead
void *handle = dlopen("/usr/lib/libsubstrate.dylib", RTLD_LAZY);
if (handle) {
MSHookFunction = dlsym(handle, "MSHookFunction");
if (MSHookFunction) {
// Set the hook and check if it worked
void *unused;
MSHookFunction(os_log_shim_enabled, my_os_log_shim_enabled, &unused);
FLEXNSLogHookWorks = os_log_shim_enabled(addr) == NO;
}
}
}
- (id)init {
return [super initWithStyle:UITableViewStylePlain];
}
#pragma mark - Overrides
- (void)viewDidLoad {
[super viewDidLoad];
self.showsSearchBar = YES;
self.pinSearchBar = YES;
weakify(self)
id logHandler = ^(NSArray<FLEXSystemLogMessage *> *newMessages) { strongify(self)
[self handleUpdateWithNewMessages:newMessages];
};
if (FLEXOSLogAvailable() && !FLEXNSLogHookWorks) {
_logController = [FLEXOSLogController withUpdateHandler:logHandler];
} else {
_logController = [FLEXASLLogController withUpdateHandler:logHandler];
}
self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
self.title = @"Waiting for Logs...";
// Toolbar buttons //
UIBarButtonItem *scrollDown = [UIBarButtonItem
flex_itemWithImage:FLEXResources.scrollToBottomIcon
target:self
action:@selector(scrollToLastRow)
];
UIBarButtonItem *settings = [UIBarButtonItem
flex_itemWithImage:FLEXResources.gearIcon
target:self
action:@selector(showLogSettings)
];
[self addToolbarItems:@[scrollDown, settings]];
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self.logController startMonitoring];
}
- (NSArray<FLEXTableViewSection *> *)makeSections { weakify(self)
_logMessages = [FLEXMutableListSection list:@[]
cellConfiguration:^(FLEXSystemLogCell *cell, FLEXSystemLogMessage *message, NSInteger row) {
strongify(self)
cell.logMessage = message;
cell.highlightedText = self.filterText;
if (row % 2 == 0) {
cell.backgroundColor = FLEXColor.primaryBackgroundColor;
} else {
cell.backgroundColor = FLEXColor.secondaryBackgroundColor;
}
} filterMatcher:^BOOL(NSString *filterText, FLEXSystemLogMessage *message) {
NSString *displayedText = [FLEXSystemLogCell displayedTextForLogMessage:message];
return [displayedText localizedCaseInsensitiveContainsString:filterText];
}
];
self.logMessages.cellRegistrationMapping = @{
kFLEXSystemLogCellIdentifier : [FLEXSystemLogCell class]
};
return @[self.logMessages];
}
- (NSArray<FLEXTableViewSection *> *)nonemptySections {
return @[self.logMessages];
}
#pragma mark - Private
- (void)handleUpdateWithNewMessages:(NSArray<FLEXSystemLogMessage *> *)newMessages {
self.title = [self.class globalsEntryTitle:FLEXGlobalsRowSystemLog];
[self.logMessages mutate:^(NSMutableArray *list) {
[list addObjectsFromArray:newMessages];
}];
// Re-filter messages to filter against new messages
if (self.filterText.length) {
[self updateSearchResults:self.filterText];
}
// "Follow" the log as new messages stream in if we were previously near the bottom.
UITableView *tv = self.tableView;
BOOL wasNearBottom = tv.contentOffset.y >= tv.contentSize.height - tv.frame.size.height - 100.0;
[self reloadData];
if (wasNearBottom) {
[self scrollToLastRow];
}
}
- (void)scrollToLastRow {
NSInteger numberOfRows = [self.tableView numberOfRowsInSection:0];
if (numberOfRows > 0) {
NSIndexPath *last = [NSIndexPath indexPathForRow:numberOfRows - 1 inSection:0];
[self.tableView scrollToRowAtIndexPath:last atScrollPosition:UITableViewScrollPositionBottom animated:YES];
}
}
- (void)showLogSettings {
NSUserDefaults *defaults = NSUserDefaults.standardUserDefaults;
BOOL disableOSLog = defaults.flex_disableOSLog;
BOOL persistent = defaults.flex_cacheOSLogMessages;
NSString *aslToggle = disableOSLog ? @"Enable os_log (default)" : @"Disable os_log";
NSString *persistence = persistent ? @"Disable persistent logging" : @"Enable persistent logging";
NSString *title = @"System Log Settings";
NSString *body = @"In iOS 10 and up, ASL has been replaced by os_log. "
"The os_log API is much more limited. Below, you can opt-into the old behavior "
"if you want cleaner, more reliable logs within FLEX, but this will break "
"anything that expects os_log to be working, such as Console.app. "
"This setting requires the app to restart to take effect. \n\n"
"To get as close to the old behavior as possible with os_log enabled, logs must "
"be collected manually at launch and stored. This setting has no effect "
"on iOS 9 and below, or if os_log is disabled. "
"You should only enable persistent logging when you need it.";
FLEXOSLogController *logController = (FLEXOSLogController *)self.logController;
[FLEXAlert makeAlert:^(FLEXAlert *make) {
make.title(title).message(body);
make.button(aslToggle).destructiveStyle().handler(^(NSArray<NSString *> *strings) {
[defaults flex_toggleBoolForKey:kFLEXDefaultsDisableOSLogForceASLKey];
});
make.button(persistence).handler(^(NSArray<NSString *> *strings) {
[defaults flex_toggleBoolForKey:kFLEXDefaultsiOSPersistentOSLogKey];
logController.persistent = !persistent;
[logController.messages addObjectsFromArray:self.logMessages.list];
});
make.button(@"Dismiss").cancelStyle();
} showFrom:self];
}
#pragma mark - FLEXGlobalsEntry
+ (NSString *)globalsEntryTitle:(FLEXGlobalsRow)row {
return @"⚠️ System Log";
}
+ (UIViewController *)globalsEntryViewController:(FLEXGlobalsRow)row {
return [self new];
}
#pragma mark - Table view data source
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
FLEXSystemLogMessage *logMessage = self.logMessages.filteredList[indexPath.row];
return [FLEXSystemLogCell preferredHeightForLogMessage:logMessage inWidth:self.tableView.bounds.size.width];
}
#pragma mark - Copy on long press
- (BOOL)tableView:(UITableView *)tableView shouldShowMenuForRowAtIndexPath:(NSIndexPath *)indexPath {
return YES;
}
- (BOOL)tableView:(UITableView *)tableView canPerformAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender {
return action == @selector(copy:);
}
- (void)tableView:(UITableView *)tableView performAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender {
if (action == @selector(copy:)) {
// We usually only want to copy the log message itself, not any metadata associated with it.
UIPasteboard.generalPasteboard.string = self.logMessages.filteredList[indexPath.row].messageText ?: @"";
}
}
- (UIContextMenuConfiguration *)tableView:(UITableView *)tableView
contextMenuConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath
point:(CGPoint)point __IOS_AVAILABLE(13.0) {
weakify(self)
return [UIContextMenuConfiguration configurationWithIdentifier:nil previewProvider:nil
actionProvider:^UIMenu *(NSArray<UIMenuElement *> *suggestedActions) {
UIAction *copy = [UIAction actionWithTitle:@"Copy"
image:nil
identifier:@"Copy"
handler:^(UIAction *action) { strongify(self)
// We usually only want to copy the log message itself, not any metadata associated with it.
UIPasteboard.generalPasteboard.string = self.logMessages.filteredList[indexPath.row].messageText ?: @"";
}];
return [UIMenu menuWithTitle:@"" image:nil identifier:nil options:UIMenuOptionsDisplayInline children:@[copy]];
}
];
}
@end

View File

@@ -0,0 +1,276 @@
==============================================================================
The LLVM Project is under the Apache License v2.0 with LLVM Exceptions:
==============================================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
---- LLVM Exceptions to the Apache 2.0 License ----
As an exception, if, as a result of your compiling your source code, portions
of this Software are embedded into an Object form of such source code, you
may redistribute such embedded portions in such Object form without complying
with the conditions of Sections 4(a), 4(b) and 4(d) of the License.
In addition, if you combine or link compiled forms of this Software with
software that is licensed under the GPLv2 ("Combined Software") and if a
court of competent jurisdiction determines that the patent provision (Section
3), the indemnity provision (Section 9) or other Section of the License
conflicts with the conditions of the GPLv2, you may retroactively and
prospectively choose to deem waived or otherwise exclude such Section(s) of
the License, but only in their entirety and only with respect to the Combined
Software.
==============================================================================
Software from third parties included in the LLVM Project:
==============================================================================
The LLVM Project contains third party software which is under different license
terms. All such code will be identified clearly using at least one of two
mechanisms:
1) It will be in a separate directory tree with its own `LICENSE.txt` or
`LICENSE` file at the top containing the specific license and restrictions
which apply to that software, or
2) It will contain specific license and restriction terms at the top of every
file.
==============================================================================
Legacy LLVM License (https://llvm.org/docs/DeveloperPolicy.html#legacy):
==============================================================================
University of Illinois/NCSA
Open Source License
Copyright (c) 2010 Apple Inc.
All rights reserved.
Developed by:
LLDB Team
http://lldb.llvm.org/
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal with
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimers.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimers in the
documentation and/or other materials provided with the distribution.
* Neither the names of the LLDB Team, copyright holders, nor the names of
its contributors may be used to endorse or promote products derived from
this Software without specific prior written permission.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE
SOFTWARE.