mirror of
				https://github.com/SoPat712/YTLitePlus.git
				synced 2025-10-30 20:34:03 -04: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
	 Balackburn
					Balackburn