mirror of https://github.com/opa334/TrollStore.git
1166 lines
34 KiB
Objective-C
1166 lines
34 KiB
Objective-C
#import "TSAppInfo.h"
|
|
#import "TSCommonTCCServiceNames.h"
|
|
#import <TSUtil.h>
|
|
|
|
extern CGImageRef LICreateIconForImage(CGImageRef image, int variant, int precomposed);
|
|
extern UIImage* imageWithSize(UIImage* image, CGSize size);
|
|
|
|
@implementation TSAppInfo
|
|
|
|
- (instancetype)initWithIPAPath:(NSString*)ipaPath
|
|
{
|
|
self = [super init];
|
|
|
|
if(self)
|
|
{
|
|
_path = ipaPath;
|
|
_isArchive = YES;
|
|
_archive = nil;
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
- (instancetype)initWithAppBundlePath:(NSString*)bundlePath
|
|
{
|
|
self = [super init];
|
|
|
|
if(self)
|
|
{
|
|
_path = bundlePath;
|
|
_isArchive = NO;
|
|
_archive = nil;
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
- (void)dealloc
|
|
{
|
|
[self closeArchive];
|
|
}
|
|
|
|
- (void)enumerateArchive:(void (^)(struct archive_entry* entry, BOOL* stop))enumerateBlock
|
|
{
|
|
[self openArchive];
|
|
|
|
struct archive_entry *entry;
|
|
int r;
|
|
for (;;)
|
|
{
|
|
r = archive_read_next_header(_archive, &entry);
|
|
if (r == ARCHIVE_EOF)
|
|
break;
|
|
if (r < ARCHIVE_OK)
|
|
fprintf(stderr, "%s\n", archive_error_string(_archive));
|
|
if (r < ARCHIVE_WARN)
|
|
return;
|
|
|
|
BOOL stop = NO;
|
|
enumerateBlock(entry, &stop);
|
|
if(stop) break;
|
|
}
|
|
}
|
|
|
|
- (struct archive_entry*)archiveEntryForSubpath:(NSString*)subpath
|
|
{
|
|
__block struct archive_entry* outEntry = nil;
|
|
[self enumerateArchive:^(struct archive_entry *entry, BOOL *stop) {
|
|
NSString* currentSubpath = [NSString stringWithUTF8String:archive_entry_pathname(entry)];
|
|
if([currentSubpath isEqualToString:subpath])
|
|
{
|
|
outEntry = entry;
|
|
*stop = YES;
|
|
}
|
|
}];
|
|
return outEntry;
|
|
}
|
|
|
|
- (NSError*)determineAppBundleName
|
|
{
|
|
NSError* outError;
|
|
|
|
if(!_cachedAppBundleName)
|
|
{
|
|
if(_isArchive)
|
|
{
|
|
[self enumerateArchive:^(struct archive_entry *entry, BOOL *stop)
|
|
{
|
|
NSString* currentSubpath = [NSString stringWithUTF8String:archive_entry_pathname(entry)];
|
|
if(currentSubpath.pathComponents.count == 3)
|
|
{
|
|
if([currentSubpath.pathComponents[0] isEqualToString:@"Payload"] && [currentSubpath.pathComponents[1].pathExtension isEqualToString:@"app"])
|
|
{
|
|
self->_cachedAppBundleName = currentSubpath.pathComponents[1];
|
|
*stop = YES;
|
|
}
|
|
}
|
|
}];
|
|
|
|
if(!_cachedAppBundleName)
|
|
{
|
|
NSString* errorDescription = @"Unable to locate app bundle inside the .IPA archive.";
|
|
outError = [NSError errorWithDomain:TrollStoreErrorDomain code:301 userInfo:@{NSLocalizedDescriptionKey : errorDescription}];
|
|
}
|
|
}
|
|
}
|
|
|
|
return outError;
|
|
}
|
|
|
|
- (NSError*)loadInfoDictionary
|
|
{
|
|
if(_isArchive && _cachedAppBundleName)
|
|
{
|
|
NSString* mainInfoPlistPath = [NSString stringWithFormat:@"Payload/%@/Info.plist", _cachedAppBundleName];
|
|
struct archive_entry* infoDictEntry = [self archiveEntryForSubpath:mainInfoPlistPath];
|
|
if(infoDictEntry)
|
|
{
|
|
size_t size = archive_entry_size(infoDictEntry);
|
|
void* buf = malloc(size);
|
|
size_t read = archive_read_data(_archive, buf, size);
|
|
|
|
if(read == size)
|
|
{
|
|
NSData* infoPlistData = [NSData dataWithBytes:buf length:size];
|
|
_cachedInfoDictionary = [NSPropertyListSerialization propertyListWithData:infoPlistData options:NSPropertyListImmutable format:nil error:nil];
|
|
}
|
|
free(buf);
|
|
}
|
|
|
|
__block NSMutableDictionary* pluginInfoDictionaries = [NSMutableDictionary new];
|
|
|
|
[self enumerateArchive:^(struct archive_entry *entry, BOOL *stop) {
|
|
NSString* currentSubpath = [NSString stringWithUTF8String:archive_entry_pathname(entry)];
|
|
if([currentSubpath isEqualToString:mainInfoPlistPath]) return;
|
|
|
|
if([currentSubpath.lastPathComponent isEqualToString:@"Info.plist"] && currentSubpath.pathComponents.count == 5)
|
|
{
|
|
if([currentSubpath.pathComponents[2] isEqualToString:@"PlugIns"])
|
|
{
|
|
size_t size = archive_entry_size(entry);
|
|
void* buf = malloc(size);
|
|
size_t read = archive_read_data(self->_archive, buf, size);
|
|
|
|
if(read == size)
|
|
{
|
|
NSData* infoPlistData = [NSData dataWithBytes:buf length:size];
|
|
NSDictionary* pluginPlist = [NSPropertyListSerialization propertyListWithData:infoPlistData options:NSPropertyListImmutable format:nil error:nil];
|
|
pluginInfoDictionaries[currentSubpath.stringByDeletingLastPathComponent] = pluginPlist;
|
|
}
|
|
free(buf);
|
|
}
|
|
}
|
|
}];
|
|
|
|
_cachedInfoDictionariesByPluginSubpaths = pluginInfoDictionaries.copy;
|
|
}
|
|
else
|
|
{
|
|
NSString* mainInfoPlistPath = [_path stringByAppendingPathComponent:@"Info.plist"];
|
|
if([[NSFileManager defaultManager] fileExistsAtPath:mainInfoPlistPath])
|
|
{
|
|
_cachedInfoDictionary = [NSDictionary dictionaryWithContentsOfURL:[NSURL fileURLWithPath:mainInfoPlistPath] error:nil];
|
|
}
|
|
|
|
__block NSMutableDictionary* pluginInfoDictionaries = [NSMutableDictionary new];
|
|
NSArray* plugIns = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:[_path stringByAppendingPathComponent:@"PlugIns"] error:nil];
|
|
for(NSString* plugIn in plugIns)
|
|
{
|
|
NSString* pluginSubpath = [NSString stringWithFormat:@"PlugIns/%@", plugIn];
|
|
NSString* pluginInfoDictionaryPath = [[_path stringByAppendingPathComponent:pluginSubpath] stringByAppendingPathComponent:@"Info.plist"];
|
|
NSDictionary* pluginInfoDictionary = [NSDictionary dictionaryWithContentsOfURL:[NSURL fileURLWithPath:pluginInfoDictionaryPath] error:nil];
|
|
if(pluginInfoDictionary)
|
|
{
|
|
pluginInfoDictionaries[pluginSubpath] = pluginInfoDictionary;
|
|
}
|
|
}
|
|
|
|
_cachedInfoDictionariesByPluginSubpaths = pluginInfoDictionaries.copy;
|
|
}
|
|
|
|
if(!_cachedInfoDictionary)
|
|
{
|
|
NSString* errorDescription = @"Unable to locate Info.plist inside app bundle.";
|
|
return [NSError errorWithDomain:TrollStoreErrorDomain code:302 userInfo:@{NSLocalizedDescriptionKey : errorDescription}];
|
|
}
|
|
|
|
return nil;
|
|
}
|
|
|
|
- (NSError*)loadInstalledState
|
|
{
|
|
if(!_isArchive)
|
|
{
|
|
NSURL* bundleURL = [NSURL fileURLWithPath:_path];
|
|
LSApplicationProxy* appProxy = [LSApplicationProxy applicationProxyForBundleURL:bundleURL];
|
|
if(appProxy)
|
|
{
|
|
if(appProxy && appProxy.isInstalled)
|
|
{
|
|
_cachedRegistrationState = appProxy.applicationType;
|
|
}
|
|
}
|
|
}
|
|
return nil;
|
|
}
|
|
|
|
- (NSError*)loadEntitlements
|
|
{
|
|
if(!_cachedEntitlementsByBinarySubpaths)
|
|
{
|
|
NSMutableDictionary* entitlementsByBinarySubpaths = [NSMutableDictionary new];
|
|
|
|
if(_isArchive)
|
|
{
|
|
if(_cachedInfoDictionary)
|
|
{
|
|
NSString* bundleExecutable = _cachedInfoDictionary[@"CFBundleExecutable"];
|
|
NSString* bundleExecutableSubpath = [NSString stringWithFormat:@"Payload/%@/%@", _cachedAppBundleName, bundleExecutable];
|
|
struct archive_entry* mainBinaryEntry = [self archiveEntryForSubpath:bundleExecutableSubpath];
|
|
if(!mainBinaryEntry)
|
|
{
|
|
NSString* errorDescription = @"Unable to locate main binary inside app bundle.";
|
|
return [NSError errorWithDomain:TrollStoreErrorDomain code:303 userInfo:@{NSLocalizedDescriptionKey : errorDescription}];
|
|
}
|
|
|
|
size_t size = archive_entry_size(mainBinaryEntry);
|
|
void* buf = malloc(size);
|
|
size_t read = archive_read_data(_archive, buf, size);
|
|
|
|
if(read == size)
|
|
{
|
|
NSData* binaryData = [NSData dataWithBytes:buf length:size];
|
|
entitlementsByBinarySubpaths[bundleExecutableSubpath] = dumpEntitlementsFromBinaryData(binaryData);
|
|
}
|
|
free(buf);
|
|
}
|
|
|
|
[_cachedInfoDictionariesByPluginSubpaths enumerateKeysAndObjectsUsingBlock:^(NSString* pluginSubpath, NSDictionary* infoDictionary, BOOL * _Nonnull stop) {
|
|
NSString* pluginExecutable = infoDictionary[@"CFBundleExecutable"];
|
|
NSString* pluginExecutableSubpath = [NSString stringWithFormat:@"%@/%@", pluginSubpath, pluginExecutable];
|
|
struct archive_entry* pluginBinaryEntry = [self archiveEntryForSubpath:pluginExecutableSubpath];
|
|
if(!pluginBinaryEntry) return;
|
|
|
|
size_t size = archive_entry_size(pluginBinaryEntry);
|
|
void* buf = malloc(size);
|
|
size_t read = archive_read_data(_archive, buf, size);
|
|
|
|
if(read == size)
|
|
{
|
|
NSData* binaryData = [NSData dataWithBytes:buf length:size];
|
|
entitlementsByBinarySubpaths[pluginExecutableSubpath] = dumpEntitlementsFromBinaryData(binaryData);
|
|
}
|
|
free(buf);
|
|
}];
|
|
}
|
|
else
|
|
{
|
|
if(_cachedInfoDictionary)
|
|
{
|
|
NSString* bundleExecutable = _cachedInfoDictionary[@"CFBundleExecutable"];
|
|
NSString* bundleExecutablePath = [_path stringByAppendingPathComponent:bundleExecutable];
|
|
|
|
if(![[NSFileManager defaultManager] fileExistsAtPath:bundleExecutablePath])
|
|
{
|
|
NSString* errorDescription = @"Unable to locate main binary inside app bundle.";
|
|
return [NSError errorWithDomain:TrollStoreErrorDomain code:303 userInfo:@{NSLocalizedDescriptionKey : errorDescription}];
|
|
}
|
|
|
|
entitlementsByBinarySubpaths[bundleExecutable] = dumpEntitlementsFromBinaryAtPath(bundleExecutablePath);
|
|
}
|
|
|
|
[_cachedInfoDictionariesByPluginSubpaths enumerateKeysAndObjectsUsingBlock:^(NSString* pluginSubpath, NSDictionary* infoDictionary, BOOL * _Nonnull stop) {
|
|
NSString* pluginExecutable = infoDictionary[@"CFBundleExecutable"];
|
|
NSString* pluginExecutableSubpath = [NSString stringWithFormat:@"%@/%@", pluginSubpath, pluginExecutable];
|
|
|
|
NSString* pluginExecutablePath = [_path stringByAppendingPathComponent:pluginExecutableSubpath];
|
|
entitlementsByBinarySubpaths[pluginExecutableSubpath] = dumpEntitlementsFromBinaryAtPath(pluginExecutablePath);
|
|
}];
|
|
}
|
|
|
|
_cachedEntitlementsByBinarySubpaths = entitlementsByBinarySubpaths.copy;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
- (NSError*)loadSize
|
|
{
|
|
if(_isArchive)
|
|
{
|
|
[self enumerateArchive:^(struct archive_entry* entry, BOOL* stop)
|
|
{
|
|
int64_t size = archive_entry_size(entry);
|
|
_cachedSize += size;
|
|
}];
|
|
}
|
|
else
|
|
{
|
|
NSDirectoryEnumerator* enumerator = [[NSFileManager defaultManager] enumeratorAtURL:[NSURL fileURLWithPath:_path]
|
|
includingPropertiesForKeys:@[NSURLIsRegularFileKey,NSURLFileAllocatedSizeKey,NSURLTotalFileAllocatedSizeKey]
|
|
options:0
|
|
errorHandler:nil];
|
|
|
|
for(NSURL* itemURL in enumerator)
|
|
{
|
|
NSNumber* isRegularFile;
|
|
NSError* error;
|
|
[itemURL getResourceValue:&isRegularFile forKey:NSURLIsRegularFileKey error:&error];
|
|
|
|
if(isRegularFile.boolValue)
|
|
{
|
|
NSNumber* totalFileAllocatedSize;
|
|
[itemURL getResourceValue:&totalFileAllocatedSize forKey:NSURLTotalFileAllocatedSizeKey error:nil];
|
|
if(totalFileAllocatedSize)
|
|
{
|
|
_cachedSize += totalFileAllocatedSize.integerValue;
|
|
}
|
|
else
|
|
{
|
|
NSNumber* fileAllocatedSize;
|
|
[itemURL getResourceValue:&fileAllocatedSize forKey:NSURLFileAllocatedSizeKey error:nil];
|
|
if(fileAllocatedSize)
|
|
{
|
|
_cachedSize += fileAllocatedSize.integerValue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil;
|
|
}
|
|
|
|
- (NSError*)loadPreviewIcon
|
|
{
|
|
int imageVariant;
|
|
CGFloat screenScale = UIScreen.mainScreen.scale;
|
|
|
|
if(screenScale >= 3.0)
|
|
{
|
|
imageVariant = 34;
|
|
}
|
|
else if(screenScale >= 2.0)
|
|
{
|
|
imageVariant = 17;
|
|
}
|
|
else
|
|
{
|
|
imageVariant = 4;
|
|
}
|
|
|
|
CGImageRef liIcon = LICreateIconForImage([[self iconForSize:CGSizeMake(29,29)] CGImage], imageVariant, 0);
|
|
_cachedPreviewIcon = [[UIImage alloc] initWithCGImage:liIcon scale:screenScale orientation:0];;
|
|
return nil;
|
|
}
|
|
|
|
- (int)openArchive
|
|
{
|
|
if(_archive)
|
|
{
|
|
[self closeArchive];
|
|
}
|
|
NSLog(@"open");
|
|
_archive = archive_read_new();
|
|
archive_read_support_format_all(_archive);
|
|
archive_read_support_filter_all(_archive);
|
|
int r = archive_read_open_filename(_archive, _path.UTF8String, 10240);
|
|
return r ? r : 0;
|
|
}
|
|
|
|
- (void)closeArchive
|
|
{
|
|
if(_archive)
|
|
{
|
|
archive_read_close(_archive);
|
|
archive_read_free(_archive);
|
|
_archive = nil;
|
|
}
|
|
}
|
|
|
|
- (NSError*)sync_loadBasicInfo
|
|
{
|
|
NSError* e;
|
|
|
|
e = [self determineAppBundleName];
|
|
if(e) return e;
|
|
|
|
e = [self loadInfoDictionary];
|
|
if(e) return e;
|
|
|
|
e = [self loadInstalledState];
|
|
if(e) return e;
|
|
|
|
return nil;
|
|
}
|
|
|
|
- (NSError*)sync_loadInfo
|
|
{
|
|
NSError* e;
|
|
|
|
e = [self sync_loadBasicInfo];
|
|
if(e) return e;
|
|
|
|
e = [self loadEntitlements];
|
|
if(e) return e;
|
|
|
|
e = [self loadSize];
|
|
if(e) return e;
|
|
|
|
e = [self loadPreviewIcon];
|
|
if(e) return e;
|
|
|
|
return nil;
|
|
}
|
|
|
|
- (void)loadBasicInfoWithCompletion:(void (^)(NSError*))completionBlock
|
|
{
|
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
if(completionBlock) completionBlock([self sync_loadBasicInfo]);
|
|
});
|
|
}
|
|
|
|
- (void)loadInfoWithCompletion:(void (^)(NSError*))completionBlock
|
|
{
|
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
if(completionBlock) completionBlock([self sync_loadInfo]);
|
|
});
|
|
}
|
|
|
|
- (void)enumerateAllInfoDictionaries:(void (^)(NSString* key, NSObject* value, BOOL* stop))enumerateBlock
|
|
{
|
|
if(!enumerateBlock) return;
|
|
|
|
__block BOOL b_stop = NO;
|
|
|
|
[_cachedInfoDictionary enumerateKeysAndObjectsUsingBlock:^(NSString* key, NSObject* value, BOOL* stop) {
|
|
enumerateBlock(key, value, &b_stop);
|
|
if(b_stop) *stop = YES;
|
|
}];
|
|
|
|
if(b_stop) return;
|
|
|
|
[_cachedInfoDictionariesByPluginSubpaths enumerateKeysAndObjectsUsingBlock:^(NSString* pluginSubpath, NSDictionary* pluginInfoDictionary, BOOL* stop_1)
|
|
{
|
|
if([pluginInfoDictionary isKindOfClass:NSDictionary.class])
|
|
{
|
|
[pluginInfoDictionary enumerateKeysAndObjectsUsingBlock:^(NSString* key, NSObject* value, BOOL * _Nonnull stop_2) {
|
|
enumerateBlock(key, value, &b_stop);
|
|
if(b_stop)
|
|
{
|
|
*stop_1 = YES;
|
|
*stop_2 = YES;
|
|
}
|
|
}];
|
|
}
|
|
}];
|
|
}
|
|
|
|
- (void)enumerateAllEntitlements:(void (^)(NSString* key, NSObject* value, BOOL* stop))enumerateBlock
|
|
{
|
|
if(!enumerateBlock) return;
|
|
|
|
__block BOOL b_stop = NO;
|
|
|
|
[_cachedEntitlementsByBinarySubpaths enumerateKeysAndObjectsUsingBlock:^(NSString* binarySubpath, NSDictionary* binaryInfoDictionary, BOOL* stop_1)
|
|
{
|
|
if([binaryInfoDictionary isKindOfClass:NSDictionary.class])
|
|
{
|
|
[binaryInfoDictionary enumerateKeysAndObjectsUsingBlock:^(NSString* key, NSObject* value, BOOL * _Nonnull stop_2) {
|
|
enumerateBlock(key, value, &b_stop);
|
|
if(b_stop)
|
|
{
|
|
*stop_1 = YES;
|
|
*stop_2 = YES;
|
|
}
|
|
}];
|
|
}
|
|
}];
|
|
}
|
|
|
|
- (void)enumerateAvailableIcons:(void (^)(CGSize iconSize, NSUInteger iconScale, NSString* iconPath, BOOL* stop))enumerateBlock
|
|
{
|
|
if(!enumerateBlock) return;
|
|
|
|
if(_cachedInfoDictionary)
|
|
{
|
|
NSString* iconName = nil;
|
|
NSDictionary* cfBundleIcons = _cachedInfoDictionary[@"CFBundleIcons"];
|
|
if(!cfBundleIcons)
|
|
{
|
|
cfBundleIcons = _cachedInfoDictionary[@"CFBundleIcons~ipad"];
|
|
}
|
|
if(cfBundleIcons && [cfBundleIcons isKindOfClass:NSDictionary.class])
|
|
{
|
|
NSDictionary* cfBundlePrimaryIcon = cfBundleIcons[@"CFBundlePrimaryIcon"];
|
|
|
|
if(cfBundlePrimaryIcon && [cfBundlePrimaryIcon isKindOfClass:NSDictionary.class])
|
|
{
|
|
NSString* potentialIconName = cfBundlePrimaryIcon[@"CFBundleIconName"];
|
|
if(potentialIconName && [potentialIconName isKindOfClass:NSString.class])
|
|
{
|
|
iconName = potentialIconName;
|
|
}
|
|
else
|
|
{
|
|
NSArray* cfBundleIconFiles = cfBundlePrimaryIcon[@"CFBundleIconFiles"];
|
|
if(cfBundleIconFiles && [cfBundleIconFiles isKindOfClass:NSArray.class])
|
|
{
|
|
NSString* oneIconFile = cfBundleIconFiles.firstObject;
|
|
NSString* otherIconFile = cfBundleIconFiles.lastObject;
|
|
iconName = [oneIconFile commonPrefixWithString:otherIconFile options:NSLiteralSearch];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if(!iconName) return;
|
|
|
|
void (^wrapperBlock)(NSString* iconPath, BOOL* stop) = ^(NSString* iconPath, BOOL* stop)
|
|
{
|
|
NSString* currentIconName = iconPath.lastPathComponent;
|
|
NSString* iconSuffix = [currentIconName substringFromIndex:[iconName length]];
|
|
NSArray* seperatedIconSuffix = [iconSuffix componentsSeparatedByString:@"@"];
|
|
|
|
NSString* currentIconResolution = seperatedIconSuffix.firstObject;
|
|
NSString* currentIconScale;
|
|
if(seperatedIconSuffix.count > 1)
|
|
{
|
|
currentIconScale = seperatedIconSuffix.lastObject;
|
|
}
|
|
|
|
NSNumberFormatter* f = [[NSNumberFormatter alloc] init];
|
|
f.numberStyle = NSNumberFormatterDecimalStyle;
|
|
|
|
NSArray* separatedIconSize = [currentIconResolution componentsSeparatedByString:@"x"];
|
|
NSNumber* widthNum = [f numberFromString:separatedIconSize.firstObject];
|
|
NSNumber* heightNum = [f numberFromString:separatedIconSize.lastObject];
|
|
|
|
CGSize iconSize = CGSizeMake(widthNum.unsignedIntegerValue, heightNum.unsignedIntegerValue);
|
|
|
|
NSUInteger scale = 1;
|
|
if(currentIconScale)
|
|
{
|
|
NSNumber* scaleNum = [f numberFromString:currentIconScale];
|
|
scale = scaleNum.unsignedIntegerValue;
|
|
}
|
|
|
|
enumerateBlock(iconSize, scale, iconPath, stop);
|
|
};
|
|
|
|
if(_isArchive)
|
|
{
|
|
NSString* iconPrefix = [NSString stringWithFormat:@"Payload/%@/%@", _cachedAppBundleName, iconName];
|
|
[self enumerateArchive:^(struct archive_entry* entry, BOOL* stop)
|
|
{
|
|
NSString* currentSubpath = [NSString stringWithUTF8String:archive_entry_pathname(entry)];
|
|
if([currentSubpath hasPrefix:iconPrefix])
|
|
{
|
|
wrapperBlock(currentSubpath, stop);
|
|
}
|
|
}];
|
|
}
|
|
else
|
|
{
|
|
NSArray<NSString*>* files = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:_path error:nil];
|
|
for(NSString* fileName in files)
|
|
{
|
|
if([fileName hasPrefix:iconName])
|
|
{
|
|
NSString* iconPath = [_path stringByAppendingPathComponent:fileName];
|
|
|
|
BOOL stop = NO;
|
|
wrapperBlock(iconPath, &stop);
|
|
if(stop) return;
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
- (UIImage*)iconForSize:(CGSize)size
|
|
{
|
|
if(size.width != size.height)
|
|
{
|
|
//not supported
|
|
return nil;
|
|
}
|
|
|
|
// Flow: Check if icon with the exact size exists
|
|
// If not, take the next best one and scale it down
|
|
|
|
//UIImage* imageToReturn;
|
|
__block NSString* foundIconPath;
|
|
|
|
// Attempt 1: Check for icon with exact size
|
|
[self enumerateAvailableIcons:^(CGSize iconSize, NSUInteger iconScale, NSString* iconPath, BOOL* stop)
|
|
{
|
|
if(CGSizeEqualToSize(iconSize, size) && UIScreen.mainScreen.scale == iconScale)
|
|
{
|
|
foundIconPath = iconPath;
|
|
//imageToReturn = imageWithSize([UIImage imageWithContentsOfFile:iconPath], size);
|
|
*stop = YES;
|
|
}
|
|
}];
|
|
|
|
if(!foundIconPath)
|
|
{
|
|
// Attempt 2: Check for icon with bigger size
|
|
__block CGSize closestIconSize = CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX);
|
|
|
|
[self enumerateAvailableIcons:^(CGSize iconSize, NSUInteger iconScale, NSString* iconPath, BOOL* stop)
|
|
{
|
|
if(iconSize.width > size.width && iconSize.width < closestIconSize.width)
|
|
{
|
|
closestIconSize = iconSize;
|
|
}
|
|
}];
|
|
|
|
if(closestIconSize.width == CGFLOAT_MAX)
|
|
{
|
|
// Attempt 3: Take biggest icon and scale it up
|
|
closestIconSize = CGSizeMake(0,0);
|
|
[self enumerateAvailableIcons:^(CGSize iconSize, NSUInteger iconScale, NSString* iconPath, BOOL* stop)
|
|
{
|
|
if(iconSize.width > closestIconSize.width)
|
|
{
|
|
closestIconSize = iconSize;
|
|
}
|
|
}];
|
|
}
|
|
|
|
if(closestIconSize.width == 0) return nil;
|
|
|
|
[self enumerateAvailableIcons:^(CGSize iconSize, NSUInteger iconScale, NSString* iconPath, BOOL* stop)
|
|
{
|
|
if(CGSizeEqualToSize(iconSize, closestIconSize))
|
|
{
|
|
closestIconSize = iconSize;
|
|
foundIconPath = iconPath;
|
|
*stop = YES;
|
|
}
|
|
}];
|
|
}
|
|
|
|
if(!foundIconPath) return nil;
|
|
|
|
if(_isArchive)
|
|
{
|
|
__block NSData* iconData;
|
|
|
|
struct archive_entry* iconEntry = [self archiveEntryForSubpath:foundIconPath];
|
|
if(iconEntry)
|
|
{
|
|
size_t size = archive_entry_size(iconEntry);
|
|
void* buf = malloc(size);
|
|
size_t read = archive_read_data(_archive, buf, size);
|
|
|
|
if(read == size)
|
|
{
|
|
iconData = [NSData dataWithBytes:buf length:size];
|
|
}
|
|
|
|
free(buf);
|
|
}
|
|
|
|
if(iconData)
|
|
{
|
|
return imageWithSize([UIImage imageWithData:iconData], size);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
return imageWithSize([UIImage imageWithContentsOfFile:foundIconPath], size);
|
|
}
|
|
return nil;
|
|
}
|
|
|
|
- (NSString*)displayName
|
|
{
|
|
NSString* displayName = _cachedInfoDictionary[@"CFBundleDisplayName"];
|
|
if(!displayName || ![displayName isKindOfClass:NSString.class])
|
|
{
|
|
displayName = _cachedInfoDictionary[@"CFBundleName"];
|
|
if(!displayName || ![displayName isKindOfClass:NSString.class])
|
|
{
|
|
displayName = _cachedInfoDictionary[@"CFBundleExecutable"];
|
|
if(!displayName || ![displayName isKindOfClass:NSString.class])
|
|
{
|
|
if(_isArchive)
|
|
{
|
|
displayName = [_cachedAppBundleName stringByDeletingPathExtension];
|
|
}
|
|
else
|
|
{
|
|
displayName = [[_path lastPathComponent] stringByDeletingPathExtension];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return displayName;
|
|
}
|
|
|
|
- (NSString*)bundleIdentifier
|
|
{
|
|
return _cachedInfoDictionary[@"CFBundleIdentifier"];
|
|
}
|
|
|
|
- (NSString*)versionString
|
|
{
|
|
NSString* version = _cachedInfoDictionary[@"CFBundleShortVersionString"];
|
|
if(!version)
|
|
{
|
|
version = _cachedInfoDictionary[@"CFBundleVersion"];
|
|
}
|
|
return version;
|
|
}
|
|
|
|
- (NSString*)sizeString
|
|
{
|
|
return [NSByteCountFormatter stringFromByteCount:_cachedSize countStyle:NSByteCountFormatterCountStyleFile];
|
|
}
|
|
|
|
- (NSString*)bundlePath
|
|
{
|
|
if(!_isArchive)
|
|
{
|
|
return _path;
|
|
}
|
|
return nil;
|
|
}
|
|
|
|
- (NSString*)registrationState
|
|
{
|
|
return _cachedRegistrationState;
|
|
}
|
|
|
|
- (NSAttributedString*)detailedInfoTitle
|
|
{
|
|
NSString* displayName = [self displayName];
|
|
|
|
NSMutableDictionary* titleAttributes = @{
|
|
NSFontAttributeName : [UIFont boldSystemFontOfSize:16]
|
|
}.mutableCopy;
|
|
NSMutableAttributedString* description = [NSMutableAttributedString new];
|
|
|
|
if(_cachedPreviewIcon)
|
|
{
|
|
titleAttributes[NSBaselineOffsetAttributeName] = @9.0;
|
|
|
|
NSTextAttachment* previewAttachment = [[NSTextAttachment alloc] init];
|
|
previewAttachment.image = _cachedPreviewIcon;
|
|
|
|
[description appendAttributedString:[NSAttributedString attributedStringWithAttachment:previewAttachment]];
|
|
[description appendAttributedString:[[NSAttributedString alloc] initWithString:@" " attributes:titleAttributes]];
|
|
}
|
|
|
|
[description appendAttributedString:[[NSAttributedString alloc] initWithString:displayName attributes:titleAttributes]];
|
|
|
|
return description.copy;
|
|
}
|
|
|
|
- (NSAttributedString*)detailedInfoDescription
|
|
{
|
|
NSString* bundleId = [self bundleIdentifier];
|
|
NSString* version = [self versionString];
|
|
NSString* sizeString = [self sizeString];
|
|
|
|
// Check if any bundle contains a root binary
|
|
__block BOOL containsRootBinary = NO;
|
|
[self enumerateAllInfoDictionaries:^(NSString *key, NSObject *value, BOOL *stop) {
|
|
if([key isEqualToString:@"TSRootBinaries"])
|
|
{
|
|
NSArray* valueArr = (NSArray*)value;
|
|
if([valueArr isKindOfClass:NSArray.class])
|
|
{
|
|
containsRootBinary = valueArr.count;
|
|
if(containsRootBinary) *stop = YES;
|
|
}
|
|
}
|
|
}];
|
|
|
|
// Check if any bundles main binary runs unsandboxed
|
|
__block BOOL isUnsandboxed = NO;
|
|
[self enumerateAllEntitlements:^(NSString *key, NSObject *value, BOOL *stop) {
|
|
if([key isEqualToString:@"com.apple.private.security.container-required"])
|
|
{
|
|
NSNumber* valueNum = (NSNumber*)value;
|
|
if(valueNum && [valueNum isKindOfClass:NSNumber.class])
|
|
{
|
|
isUnsandboxed = !valueNum.boolValue;
|
|
if(isUnsandboxed) *stop = YES;
|
|
}
|
|
} else if([key isEqualToString:@"com.apple.private.security.no-container"])
|
|
{
|
|
NSNumber* valueNum = (NSNumber*)value;
|
|
if(valueNum && [valueNum isKindOfClass:NSNumber.class])
|
|
{
|
|
isUnsandboxed = valueNum.boolValue;
|
|
if(isUnsandboxed) *stop = YES;
|
|
}
|
|
} else if([key isEqualToString:@"com.apple.private.security.no-sandbox"])
|
|
{
|
|
NSNumber* valueNum = (NSNumber*)value;
|
|
if(valueNum && [valueNum isKindOfClass:NSNumber.class])
|
|
{
|
|
isUnsandboxed = valueNum.boolValue;
|
|
if(isUnsandboxed) *stop = YES;
|
|
}
|
|
}
|
|
}];
|
|
|
|
// Check if any bundles main binary can spawn an external binary
|
|
__block BOOL canSpawnBinaries = NO;
|
|
[self enumerateAllEntitlements:^(NSString *key, NSObject *value, BOOL *stop)
|
|
{
|
|
if([key isEqualToString:@"platform-application"])
|
|
{
|
|
NSNumber* valueNum = (NSNumber*)value;
|
|
if(valueNum && [valueNum isKindOfClass:NSNumber.class])
|
|
{
|
|
canSpawnBinaries = valueNum.boolValue;
|
|
if(canSpawnBinaries) *stop = YES;
|
|
}
|
|
}
|
|
}];
|
|
|
|
// Check if any bundles main binary can spawn an external binary as root
|
|
__block BOOL hasPersonaMngmt = NO;
|
|
[self enumerateAllEntitlements:^(NSString *key, NSObject *value, BOOL *stop)
|
|
{
|
|
if([key isEqualToString:@"com.apple.private.persona-mgmt"])
|
|
{
|
|
NSNumber* valueNum = (NSNumber*)value;
|
|
if(valueNum && [valueNum isKindOfClass:NSNumber.class])
|
|
{
|
|
hasPersonaMngmt = valueNum.boolValue;
|
|
if(hasPersonaMngmt) *stop = YES;
|
|
}
|
|
}
|
|
}];
|
|
|
|
// Accessible containers
|
|
// com.apple.developer.icloud-container-identifiers
|
|
// com.apple.security.application-groups
|
|
// Unrestricted if special entitlement
|
|
|
|
__block BOOL unrestrictedContainerAccess = NO;
|
|
[self enumerateAllEntitlements:^(NSString *key, NSObject *value, BOOL *stop)
|
|
{
|
|
if([key isEqualToString:@"com.apple.private.security.storage.AppDataContainers"])
|
|
{
|
|
NSNumber* valueNum = (NSNumber*)value;
|
|
if(valueNum && [valueNum isKindOfClass:NSNumber.class])
|
|
{
|
|
unrestrictedContainerAccess = valueNum.boolValue;
|
|
if(hasPersonaMngmt) *stop = YES;
|
|
}
|
|
}
|
|
}];
|
|
|
|
__block NSMutableArray* accessibleContainers = [NSMutableArray new]; //array by design, should be ordered
|
|
if(!unrestrictedContainerAccess)
|
|
{
|
|
[self enumerateAllInfoDictionaries:^(NSString *key, NSObject *value, BOOL *stop) {
|
|
if([key isEqualToString:@"CFBundleIdentifier"])
|
|
{
|
|
NSString* valueStr = (NSString*)value;
|
|
if([valueStr isKindOfClass:NSString.class])
|
|
{
|
|
[accessibleContainers addObject:valueStr];
|
|
}
|
|
}
|
|
}];
|
|
|
|
[self enumerateAllEntitlements:^(NSString *key, NSObject *value, BOOL *stop)
|
|
{
|
|
if([key isEqualToString:@"com.apple.developer.icloud-container-identifiers"] || [key isEqualToString:@"com.apple.security.application-groups"] || [key isEqualToString:@"com.apple.security.system-groups"])
|
|
{
|
|
NSArray* valueArr = (NSArray*)value;
|
|
if([valueArr isKindOfClass:NSArray.class])
|
|
{
|
|
for(NSString* containerID in valueArr)
|
|
{
|
|
if([containerID isKindOfClass:NSString.class])
|
|
{
|
|
if(![accessibleContainers containsObject:containerID])
|
|
{
|
|
[accessibleContainers addObject:containerID];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}];
|
|
}
|
|
|
|
// Accessible Keychain Groups
|
|
// keychain-access-groups
|
|
// Unrestricted if single * (maybe?)
|
|
__block BOOL unrestrictedKeychainAccess = NO;
|
|
__block NSMutableSet* accessibleKeychainGroups = [NSMutableSet new];
|
|
[self enumerateAllEntitlements:^(NSString *key, NSObject *value, BOOL *stop) {
|
|
if([key isEqualToString:@"keychain-access-groups"])
|
|
{
|
|
NSArray* valueArr = (NSArray*)value;
|
|
if([valueArr isKindOfClass:NSArray.class])
|
|
{
|
|
for(NSString* keychainID in valueArr)
|
|
{
|
|
if([keychainID isKindOfClass:NSString.class])
|
|
{
|
|
if([keychainID isEqualToString:@"*"])
|
|
{
|
|
unrestrictedKeychainAccess = YES;
|
|
}
|
|
else
|
|
{
|
|
[accessibleKeychainGroups addObject:keychainID];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}];
|
|
|
|
__block NSMutableSet* URLSchemes = [NSMutableSet new];
|
|
[self enumerateAllInfoDictionaries:^(NSString *key, NSObject *value, BOOL *stop) {
|
|
if([key isEqualToString:@"CFBundleURLTypes"])
|
|
{
|
|
NSArray* valueArr = (NSArray*)value;
|
|
if([valueArr isKindOfClass:NSArray.class])
|
|
{
|
|
for(NSDictionary* URLTypeDict in valueArr)
|
|
{
|
|
if([URLTypeDict isKindOfClass:NSDictionary.class])
|
|
{
|
|
NSArray* cURLSchemes = URLTypeDict[@"CFBundleURLSchemes"];
|
|
if(cURLSchemes && [cURLSchemes isKindOfClass:NSArray.class])
|
|
{
|
|
for(NSString* URLScheme in cURLSchemes)
|
|
{
|
|
[URLSchemes addObject:URLScheme];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}];
|
|
|
|
__block NSMutableSet* allowedTccServices = [NSMutableSet new];
|
|
[self enumerateAllEntitlements:^(NSString *key, NSObject *value, BOOL *stop) {
|
|
if([key isEqualToString:@"com.apple.private.tcc.allow"])
|
|
{
|
|
NSArray* valueArr = (NSArray*)value;
|
|
if([valueArr isKindOfClass:NSArray.class])
|
|
{
|
|
for(NSString* serviceID in valueArr)
|
|
{
|
|
if([serviceID isKindOfClass:NSString.class])
|
|
{
|
|
NSString* displayName = commonTCCServices[serviceID];
|
|
if(displayName == nil)
|
|
{
|
|
[allowedTccServices addObject:[serviceID stringByReplacingOccurrencesOfString:@"kTCCService" withString:@""]];
|
|
}
|
|
else
|
|
{
|
|
[allowedTccServices addObject:displayName];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if ([key isEqualToString:@"com.apple.locationd.preauthorized"])
|
|
{
|
|
NSNumber* valueNum = (NSNumber*)value;
|
|
if([valueNum isKindOfClass:NSNumber.class])
|
|
{
|
|
if([valueNum boolValue])
|
|
{
|
|
[allowedTccServices addObject:@"Location"];
|
|
}
|
|
}
|
|
}
|
|
}];
|
|
|
|
__block NSMutableSet* allowedMGKeys = [NSMutableSet new];
|
|
[self enumerateAllEntitlements:^(NSString *key, NSObject *value, BOOL *stop) {
|
|
if([key isEqualToString:@"com.apple.private.MobileGestalt.AllowedProtectedKeys"])
|
|
{
|
|
NSArray* valueArr = (NSArray*)value;
|
|
if([valueArr isKindOfClass:NSArray.class])
|
|
{
|
|
for(NSString* protectedKey in valueArr)
|
|
{
|
|
if([protectedKey isKindOfClass:NSString.class])
|
|
{
|
|
[allowedMGKeys addObject:protectedKey];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}];
|
|
|
|
NSMutableParagraphStyle* leftAlignment = [[NSMutableParagraphStyle alloc] init];
|
|
leftAlignment.alignment = NSTextAlignmentLeft;
|
|
|
|
UIColor* dangerColor = [UIColor colorWithDynamicProvider:^UIColor*(UITraitCollection *traitCollection)
|
|
{
|
|
if(traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark)
|
|
{
|
|
return [UIColor orangeColor];
|
|
}
|
|
else
|
|
{
|
|
return [UIColor redColor];
|
|
}
|
|
}];
|
|
|
|
UIColor* warningColor = [UIColor colorWithDynamicProvider:^UIColor*(UITraitCollection *traitCollection)
|
|
{
|
|
if(traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark)
|
|
{
|
|
return [UIColor yellowColor];
|
|
}
|
|
else
|
|
{
|
|
return [UIColor orangeColor];
|
|
}
|
|
}];
|
|
|
|
NSMutableAttributedString* description = [NSMutableAttributedString new];
|
|
|
|
NSDictionary* headerAttributes = @{
|
|
NSFontAttributeName : [UIFont boldSystemFontOfSize:14],
|
|
NSParagraphStyleAttributeName : leftAlignment
|
|
};
|
|
|
|
NSDictionary* bodyAttributes = @{
|
|
NSFontAttributeName : [UIFont systemFontOfSize:11],
|
|
NSParagraphStyleAttributeName : leftAlignment
|
|
};
|
|
|
|
NSDictionary* bodyWarningAttributes = @{
|
|
NSFontAttributeName : [UIFont systemFontOfSize:11],
|
|
NSParagraphStyleAttributeName : leftAlignment,
|
|
NSForegroundColorAttributeName : warningColor
|
|
};
|
|
|
|
NSDictionary* bodyDangerAttributes = @{
|
|
NSFontAttributeName : [UIFont systemFontOfSize:11],
|
|
NSParagraphStyleAttributeName : leftAlignment,
|
|
NSForegroundColorAttributeName : dangerColor
|
|
};
|
|
|
|
[description appendAttributedString:[[NSAttributedString alloc] initWithString:@"Metadata" attributes:headerAttributes]];
|
|
|
|
[description appendAttributedString:[[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"\nBundle Identifier: %@", bundleId] attributes:bodyAttributes]];
|
|
[description appendAttributedString:[[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"\nVersion: %@", version] attributes:bodyAttributes]];
|
|
[description appendAttributedString:[[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"\nSize: %@", sizeString] attributes:bodyAttributes]];
|
|
|
|
[description appendAttributedString:[[NSAttributedString alloc] initWithString:@"\n\nSandboxing" attributes:headerAttributes]];
|
|
if(isUnsandboxed)
|
|
{
|
|
[description appendAttributedString:[[NSAttributedString alloc] initWithString:@"\nThe app runs unsandboxed and can access most of the file system." attributes:bodyWarningAttributes]];
|
|
}
|
|
else
|
|
{
|
|
[description appendAttributedString:[[NSAttributedString alloc] initWithString:@"\nThe app runs sandboxed and can only access the containers listed below." attributes:bodyAttributes]];
|
|
}
|
|
|
|
[description appendAttributedString:[[NSAttributedString alloc] initWithString:@"\n\nCapabilities" attributes:headerAttributes]];
|
|
if(containsRootBinary && canSpawnBinaries && hasPersonaMngmt)
|
|
{
|
|
[description appendAttributedString:[[NSAttributedString alloc] initWithString:@"\nThe app can spawn its own embedded binaries with root privileges." attributes:bodyDangerAttributes]];
|
|
}
|
|
else if(canSpawnBinaries && hasPersonaMngmt)
|
|
{
|
|
[description appendAttributedString:[[NSAttributedString alloc] initWithString:@"\nThe app can spawn arbitary binaries as root, but does not contain any such binaries by itself." attributes:bodyWarningAttributes]];
|
|
}
|
|
else if(canSpawnBinaries)
|
|
{
|
|
[description appendAttributedString:[[NSAttributedString alloc] initWithString:@"\nThe app can spawn arbitary binaries as the mobile user." attributes:bodyWarningAttributes]];
|
|
}
|
|
else
|
|
{
|
|
[description appendAttributedString:[[NSAttributedString alloc] initWithString:@"\nThe app can not spawn other binaries." attributes:bodyAttributes]];
|
|
}
|
|
|
|
if(allowedTccServices.count)
|
|
{
|
|
[description appendAttributedString:[[NSAttributedString alloc] initWithString:@"\n\nPrivacy" attributes:headerAttributes]];
|
|
[description appendAttributedString:[[NSAttributedString alloc] initWithString:@"\nThe app can access the following services without asking for permission:\n" attributes:bodyDangerAttributes]];
|
|
[description appendAttributedString:[[NSAttributedString alloc] initWithString:[NSListFormatter localizedStringByJoiningStrings:[allowedTccServices allObjects]] attributes:bodyAttributes]];
|
|
}
|
|
|
|
if (allowedMGKeys.count)
|
|
{
|
|
[description appendAttributedString:[[NSAttributedString alloc] initWithString:@"\n\nDevice Info" attributes:headerAttributes]];
|
|
[description appendAttributedString:[[NSAttributedString alloc] initWithString:@"\nThe app can access protected information about this device.\n" attributes:bodyWarningAttributes]];
|
|
[description appendAttributedString:[[NSAttributedString alloc] initWithString:[NSListFormatter localizedStringByJoiningStrings:[allowedMGKeys allObjects]] attributes:bodyAttributes]];
|
|
}
|
|
|
|
if(unrestrictedContainerAccess || accessibleContainers.count)
|
|
{
|
|
[description appendAttributedString:[[NSAttributedString alloc] initWithString:@"\n\nAccessible Containers" attributes:headerAttributes]];
|
|
if(unrestrictedContainerAccess)
|
|
{
|
|
[description appendAttributedString:[[NSAttributedString alloc] initWithString:@"\nUnrestricted, the app can access all data containers on the system." attributes:bodyDangerAttributes]];
|
|
}
|
|
else
|
|
{
|
|
for(NSString* containerID in accessibleContainers)
|
|
{
|
|
[description appendAttributedString:[[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"\n%@", containerID] attributes:bodyAttributes]];
|
|
}
|
|
}
|
|
}
|
|
|
|
if(unrestrictedKeychainAccess || accessibleKeychainGroups.count)
|
|
{
|
|
[description appendAttributedString:[[NSAttributedString alloc] initWithString:@"\n\nAccessible Keychain Groups" attributes:headerAttributes]];
|
|
if(unrestrictedKeychainAccess)
|
|
{
|
|
[description appendAttributedString:[[NSAttributedString alloc] initWithString:@"\nUnrestricted, the app can access the entire keychain." attributes:bodyDangerAttributes]];
|
|
}
|
|
else
|
|
{
|
|
for(NSString* keychainID in accessibleKeychainGroups)
|
|
{
|
|
[description appendAttributedString:[[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"\n%@", keychainID] attributes:bodyAttributes]];
|
|
}
|
|
}
|
|
}
|
|
|
|
if(URLSchemes.count)
|
|
{
|
|
[description appendAttributedString:[[NSAttributedString alloc] initWithString:@"\n\nURL Schemes" attributes:headerAttributes]];
|
|
|
|
for(NSString* URLScheme in URLSchemes)
|
|
{
|
|
[description appendAttributedString:[[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"\n%@", URLScheme] attributes:bodyAttributes]];
|
|
}
|
|
}
|
|
|
|
return description;
|
|
}
|
|
|
|
- (void)log
|
|
{
|
|
NSLog(@"entitlements:");
|
|
[self enumerateAllEntitlements:^(NSString *key, NSObject *value, BOOL *stop) {
|
|
NSLog(@"%@ -> %@", key, value);
|
|
}];
|
|
|
|
NSLog(@"info dictionaries:");
|
|
[self enumerateAllInfoDictionaries:^(NSString *key, NSObject *value, BOOL *stop) {
|
|
NSLog(@"%@ -> %@", key, value);
|
|
}];
|
|
}
|
|
|
|
|
|
@end
|