Build app list from worse_query path traversal with name resolution
This commit is contained in:
1 parent
176ff30ee5
commit
9bc7b9bcc6
5 files changed
+204
-25
No files matched your search
+181
-22
@@ -355,6 +355,14 @@ static NSString *WBLocalized(NSString *key)
|
||||
@property (nonatomic, strong) NSMutableDictionary<NSString *, NSDictionary<NSString *, NSURL *> *> *groupCache;
|
||||
@end
|
||||
|
||||
@protocol WBLProxyInfo <NSObject>
|
||||
- (NSString *)bundleIdentifier;
|
||||
- (NSString *)localizedName;
|
||||
- (NSURL *)bundleURL;
|
||||
- (NSURL *)dataContainerURL;
|
||||
- (NSString *)shortVersionString;
|
||||
@end
|
||||
|
||||
@implementation WBRegistry
|
||||
|
||||
+ (instancetype)shared
|
||||
@@ -610,6 +618,58 @@ static NSString *WBFindStoreWithProgress(void (^progress)(double, NSString *))
|
||||
return nil;
|
||||
}
|
||||
|
||||
static NSArray *(*gOrigAllInstalled)(id, SEL);
|
||||
|
||||
static NSDictionary *WBReadInfoPlist(NSString *bundlePath)
|
||||
{
|
||||
if (!bundlePath.length) return nil;
|
||||
NSString *path = [bundlePath stringByAppendingPathComponent:@"Info.plist"];
|
||||
NSData *data = wq_read(path, 2 * 1024 * 1024, NULL);
|
||||
if (!data) {
|
||||
wq_grant(bundlePath);
|
||||
data = wq_read(path, 2 * 1024 * 1024, NULL);
|
||||
}
|
||||
if (!data) return nil;
|
||||
id plist = [NSPropertyListSerialization propertyListWithData:data options:0 format:NULL error:nil];
|
||||
return [plist isKindOfClass:NSDictionary.class] ? plist : nil;
|
||||
}
|
||||
|
||||
static NSString *WBInfoString(NSDictionary *info, NSArray<NSString *> *keys)
|
||||
{
|
||||
for (NSString *key in keys) {
|
||||
id value = info[key];
|
||||
if ([value isKindOfClass:NSString.class] && [value length]) return value;
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
static NSString *WBBundleIDFromContainerMetadata(NSString *container)
|
||||
{
|
||||
NSString *path = [container stringByAppendingPathComponent:@".com.apple.mobile_container_manager.metadata.plist"];
|
||||
wq_grant(container);
|
||||
wq_grant(path);
|
||||
NSData *data = wq_read(path, 256 * 1024, NULL);
|
||||
if (!data) return nil;
|
||||
id plist = [NSPropertyListSerialization propertyListWithData:data options:0 format:NULL error:nil];
|
||||
id identifier = [plist isKindOfClass:NSDictionary.class] ? plist[@"MCMMetadataIdentifier"] : nil;
|
||||
return [identifier isKindOfClass:NSString.class] ? identifier : nil;
|
||||
}
|
||||
|
||||
static NSArray *WBOriginalInstalledApplications(void)
|
||||
{
|
||||
Class workspaceClass = NSClassFromString(@"LSApplicationWorkspace");
|
||||
if (!workspaceClass || !gOrigAllInstalled) return nil;
|
||||
id workspace = [workspaceClass respondsToSelector:@selector(defaultWorkspace)]
|
||||
? [workspaceClass performSelector:@selector(defaultWorkspace)] : nil;
|
||||
if (!workspace) return nil;
|
||||
@try {
|
||||
return gOrigAllInstalled(workspace, NSSelectorFromString(@"allInstalledApplications"));
|
||||
} @catch (__unused NSException *exception) {
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (void)prepareWithProgress:(void (^)(double, NSString *))progress
|
||||
completion:(void (^)(BOOL, NSString *))completion
|
||||
{
|
||||
@@ -620,19 +680,77 @@ static NSString *WBFindStoreWithProgress(void (^progress)(double, NSString *))
|
||||
if (completion) completion(NO, WBLocalized(@"no_group"));
|
||||
return;
|
||||
}
|
||||
|
||||
// LaunchServices is optional enrichment: names, bundle paths, and a
|
||||
// container->bundle-id map. The list itself comes from path traversal
|
||||
// below, so a missing or unreadable store no longer produces blank UI.
|
||||
if (progress) progress(0.08, WBLocalized(@"locating_ls"));
|
||||
NSString *store = WBFindStoreWithProgress(progress);
|
||||
if (!store.length) {
|
||||
if (completion) completion(NO, WBLocalized(@"no_ls"));
|
||||
return;
|
||||
NSMutableDictionary<NSString *, NSString *> *lsContainerToID = [NSMutableDictionary new];
|
||||
if (store.length) {
|
||||
[NSUserDefaults.standardUserDefaults setObject:store forKey:@"WQLaunchServicesStore"];
|
||||
[NSUserDefaults.standardUserDefaults synchronize];
|
||||
wq_display_names_reset_miss();
|
||||
wq_load_display_names();
|
||||
for (NSString *path in wq_ls_container_paths(store)) {
|
||||
NSString *bundleID = wq_bundle_id_for_container(path);
|
||||
if (bundleID.length) lsContainerToID[path] = bundleID;
|
||||
}
|
||||
}
|
||||
if (progress) progress(0.40, WBLocalized(@"parsing_apps"));
|
||||
[NSUserDefaults.standardUserDefaults setObject:store forKey:@"WQLaunchServicesStore"];
|
||||
[NSUserDefaults.standardUserDefaults synchronize];
|
||||
NSArray<NSString *> *containers = wq_ls_container_paths(store);
|
||||
wq_display_names_reset_miss();
|
||||
wq_load_display_names();
|
||||
if (!containers.count) {
|
||||
|
||||
// The native LaunchServices API is independent of the CSStore parser
|
||||
// and still provides display names and bundle paths when the store is
|
||||
// missing or unreadable.
|
||||
NSMutableDictionary<NSString *, NSString *> *apiName = [NSMutableDictionary new];
|
||||
NSMutableDictionary<NSString *, NSString *> *apiBundle = [NSMutableDictionary new];
|
||||
NSMutableDictionary<NSString *, NSString *> *apiVersion = [NSMutableDictionary new];
|
||||
NSMutableDictionary<NSString *, NSString *> *apiContainerToID = [NSMutableDictionary new];
|
||||
for (id<WBLProxyInfo> proxy in WBOriginalInstalledApplications()) {
|
||||
NSString *bundleID = [proxy bundleIdentifier];
|
||||
if (!bundleID.length) continue;
|
||||
NSString *name = [proxy localizedName];
|
||||
if (name.length) apiName[bundleID] = name;
|
||||
NSURL *bundleURL = [proxy bundleURL];
|
||||
if (bundleURL.path.length) apiBundle[bundleID] = bundleURL.path;
|
||||
NSString *version = [proxy shortVersionString];
|
||||
if (version.length && ![version isEqualToString:@"null"]) apiVersion[bundleID] = version;
|
||||
NSURL *dataURL = [proxy dataContainerURL];
|
||||
if (dataURL.path.length) apiContainerToID[dataURL.path] = bundleID;
|
||||
}
|
||||
|
||||
// Enumerate app data containers directly through worse_query on every
|
||||
// launch. This does not depend on the LaunchServices database being
|
||||
// readable, which is the failure mode on affected devices.
|
||||
if (progress) progress(0.25, WBLocalized(@"scanning_containers"));
|
||||
NSString *root = @"/private/var/mobile/Containers/Data/Application";
|
||||
wq_set_full_scan_ui(WBScanConfirmC, WBScanProgressC);
|
||||
gWBScanProgressBlock = ^(int64_t done, int64_t total, BOOL __unused finished) {
|
||||
if (progress && total > 0) {
|
||||
progress(0.25 + 0.35 * ((double)done / (double)total), WBLocalized(@"scanning_containers_short"));
|
||||
}
|
||||
};
|
||||
NSArray<NSString *> *enumerated = wq_enumerate(root, 0, nil, ^(int64_t done, int64_t total, NSUInteger __unused count) {
|
||||
if (progress && total > 0) {
|
||||
progress(0.25 + 0.35 * ((double)done / (double)total), WBLocalized(@"scanning_containers_short"));
|
||||
}
|
||||
});
|
||||
gWBScanProgressBlock = nil;
|
||||
NSLog(@"[WB] app containers: %lu enumerated, %lu from LaunchServices API",
|
||||
(unsigned long)enumerated.count, (unsigned long)apiContainerToID.count);
|
||||
|
||||
NSMutableOrderedSet<NSString *> *containerSet = [NSMutableOrderedSet orderedSet];
|
||||
for (NSString *path in enumerated) {
|
||||
if ([path.stringByDeletingLastPathComponent isEqualToString:root]) [containerSet addObject:path];
|
||||
}
|
||||
for (NSString *path in apiContainerToID.allKeys) {
|
||||
if ([path.stringByDeletingLastPathComponent isEqualToString:root]) [containerSet addObject:path];
|
||||
}
|
||||
for (NSString *path in lsContainerToID.allKeys) {
|
||||
if ([path.stringByDeletingLastPathComponent isEqualToString:root]) [containerSet addObject:path];
|
||||
}
|
||||
NSLog(@"[WB] containers: %lu enumerated, %lu API, %lu LaunchServices maps",
|
||||
(unsigned long)enumerated.count, (unsigned long)apiContainerToID.count, (unsigned long)lsContainerToID.count);
|
||||
if (!containerSet.count) {
|
||||
if (completion) completion(NO, WBLocalized(@"no_containers"));
|
||||
return;
|
||||
}
|
||||
@@ -641,30 +759,57 @@ static NSString *WBFindStoreWithProgress(void (^progress)(double, NSString *))
|
||||
NSMutableDictionary<NSString *, WBAppInfo *> *byID = [NSMutableDictionary new];
|
||||
NSMutableArray<WBAppInfo *> *apps = [NSMutableArray new];
|
||||
NSMutableArray<WBAppProxy *> *proxies = [NSMutableArray new];
|
||||
for (NSString *container in containers) {
|
||||
if (![container containsString:@"/Data/Application/"]) continue;
|
||||
NSString *bundleID = wq_bundle_id_for_container(container);
|
||||
if (!bundleID.length || byID[bundleID]) continue;
|
||||
NSString *bundlePath = wq_bundle_path_for_id(bundleID);
|
||||
if (!bundlePath.length || ![bundlePath.pathExtension isEqualToString:@"app"]) continue;
|
||||
NSString *parent = bundlePath.stringByDeletingLastPathComponent;
|
||||
if (![parent.stringByDeletingLastPathComponent isEqualToString:@"/private/var/containers/Bundle/Application"]) continue;
|
||||
NSMutableDictionary<NSString *, NSDictionary *> *infoCache = [NSMutableDictionary new];
|
||||
NSUInteger missingLabels = 0;
|
||||
for (NSString *container in containerSet) {
|
||||
NSString *bundleID = apiContainerToID[container];
|
||||
if (!bundleID.length) bundleID = lsContainerToID[container];
|
||||
if (!bundleID.length) bundleID = wq_container_label(container);
|
||||
if (!bundleID.length) bundleID = WBBundleIDFromContainerMetadata(container);
|
||||
if (!bundleID.length) bundleID = wq_bundle_id_for_container(container);
|
||||
if (!bundleID.length || byID[bundleID]) {
|
||||
if (!bundleID.length) missingLabels++;
|
||||
continue;
|
||||
}
|
||||
|
||||
NSString *bundlePath = apiBundle[bundleID];
|
||||
if (!bundlePath.length) bundlePath = wq_bundle_path_for_id(bundleID);
|
||||
NSDictionary *info = nil;
|
||||
if (bundlePath.length) {
|
||||
info = infoCache[bundlePath];
|
||||
if (!info) {
|
||||
info = WBReadInfoPlist(bundlePath);
|
||||
if (info) infoCache[bundlePath] = info;
|
||||
}
|
||||
}
|
||||
NSString *name = apiName[bundleID];
|
||||
if (!name.length) name = WBInfoString(info, @[@"CFBundleDisplayName", @"CFBundleName", @"CFBundleExecutable"]);
|
||||
if (!name.length) name = wq_display_name(bundleID);
|
||||
if (!name.length) name = WBPrettifyBundleID(bundleID);
|
||||
|
||||
WBAppInfo *app = [WBAppInfo new];
|
||||
app.bundleID = bundleID;
|
||||
app.containerPath = container;
|
||||
app.bundlePath = bundlePath;
|
||||
app.version = apiVersion[bundleID] ?: WBInfoString(info, @[@"CFBundleShortVersionString", @"CFBundleVersion"]);
|
||||
app.isSystem = [bundleID hasPrefix:@"com.apple."];
|
||||
NSString *name = wq_display_name(bundleID);
|
||||
app.name = name.length ? name : WBPrettifyBundleID(bundleID);
|
||||
app.name = name;
|
||||
byID[bundleID] = app;
|
||||
[apps addObject:app];
|
||||
[proxies addObject:[[WBAppProxy alloc] initWithInfo:app]];
|
||||
}
|
||||
|
||||
if (!apps.count) {
|
||||
NSLog(@"[WB] built 0 apps from %lu containers; %lu without bundle id",
|
||||
(unsigned long)containerSet.count, (unsigned long)missingLabels);
|
||||
if (completion) completion(NO, WBLocalized(@"no_containers"));
|
||||
return;
|
||||
}
|
||||
NSLog(@"[WB] built %lu apps from %lu containers; %lu without bundle id",
|
||||
(unsigned long)apps.count, (unsigned long)containerSet.count, (unsigned long)missingLabels);
|
||||
[apps sortUsingComparator:^NSComparisonResult(WBAppInfo *a, WBAppInfo *b) {
|
||||
return [a.name localizedStandardCompare:b.name];
|
||||
}];
|
||||
// Rebuild proxies in sorted order.
|
||||
[proxies removeAllObjects];
|
||||
for (WBAppInfo *app in apps) [proxies addObject:[[WBAppProxy alloc] initWithInfo:app]];
|
||||
|
||||
@@ -773,7 +918,6 @@ static NSString *WBFindStoreWithProgress(void (^progress)(double, NSString *))
|
||||
|
||||
#pragma mark - Hooks
|
||||
|
||||
static NSArray *(*gOrigAllInstalled)(id, SEL);
|
||||
static NSArray *(*gOrigAllApplications)(id, SEL);
|
||||
static id (*gOrigProxyForIdentifier)(id, SEL, NSString *);
|
||||
static NSString *(*gOrigDataPath)(id, SEL);
|
||||
@@ -822,6 +966,7 @@ static BOOL WBWaitForRegistry(void)
|
||||
|
||||
static NSArray *WBHookAllInstalled(id self, SEL _cmd)
|
||||
{
|
||||
if (gPreparing) return gOrigAllInstalled ? gOrigAllInstalled(self, _cmd) : @[];
|
||||
if (!WBRegistry.shared.ready && !NSThread.isMainThread) WBWaitForRegistry();
|
||||
NSArray *proxies = WBRegistry.shared.proxies;
|
||||
if (WBRegistry.shared.ready && proxies.count) return proxies;
|
||||
@@ -830,6 +975,7 @@ static NSArray *WBHookAllInstalled(id self, SEL _cmd)
|
||||
|
||||
static NSArray *WBHookAllApplications(id self, SEL _cmd)
|
||||
{
|
||||
if (gPreparing) return gOrigAllApplications ? gOrigAllApplications(self, _cmd) : @[];
|
||||
if (!WBRegistry.shared.ready && !NSThread.isMainThread) WBWaitForRegistry();
|
||||
NSArray *proxies = WBRegistry.shared.proxies;
|
||||
if (WBRegistry.shared.ready && proxies.count) return proxies;
|
||||
@@ -1711,6 +1857,7 @@ static void WBStartPreparation(void)
|
||||
// Last-resort fallback: start even without a presenter.
|
||||
gPreparing = YES;
|
||||
[WBRegistry.shared prepareWithProgress:nil completion:^(BOOL ok, NSString *message) {
|
||||
gPreparing = NO;
|
||||
gPrepared = YES;
|
||||
[gReadyCondition lock];
|
||||
[gReadyCondition broadcast];
|
||||
@@ -1732,6 +1879,7 @@ static void WBStartPreparation(void)
|
||||
[WBRegistry.shared prepareWithProgress:^(double fraction, NSString *status) {
|
||||
[gProgressController updateFraction:fraction status:status];
|
||||
} completion:^(BOOL ok, NSString *message) {
|
||||
gPreparing = NO;
|
||||
gPrepared = YES;
|
||||
[gReadyCondition lock];
|
||||
[gReadyCondition broadcast];
|
||||
@@ -1741,6 +1889,17 @@ static void WBStartPreparation(void)
|
||||
[gProgressController dismissViewControllerAnimated:YES completion:nil];
|
||||
gProgressController = nil;
|
||||
}
|
||||
// The first load may have started before preparation finished and
|
||||
// received an empty native list. Rebuild it now that the registry
|
||||
// is ready so the path-enumerated apps actually appear.
|
||||
id appsVC = gAppsTableViewController;
|
||||
if (appsVC) {
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 200 * NSEC_PER_MSEC), dispatch_get_main_queue(), ^{
|
||||
if ([appsVC respondsToSelector:@selector(reload)]) {
|
||||
[(id<WBAppsTableRefresh>)appsVC reload];
|
||||
}
|
||||
});
|
||||
}
|
||||
if (!ok && message.length) {
|
||||
NSString *detail = message;
|
||||
if ([message containsString:@"App Group"]) {
|
||||
|
||||
Reference in new issue
Block a user