Build app list from worse_query path traversal with name resolution

This commit is contained in:
admin committed 2026-11-12 13:22:35 +08:00
1 parent 176ff30ee5
commit 9bc7b9bcc6
5 files changed
+204 -25

No files matched your search

+181 -22
View File
@@ -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"]) {