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