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

+14
View File
@@ -33,6 +33,20 @@ source/
- Detail page version/path/date/app-group support.
- Replace the `filza://` prompt with the built-in WQ Files browser.
- Present media through the native `AVPlayerViewController`.
- Build the app list by traversing `/private/var/mobile/Containers/Data/Application` through `worse_query` on every launch. LaunchServices is optional and only used for names and bundle paths.
- Resolve app names from the native LaunchServices API, the bundle `Info.plist`, LaunchServices display names, and finally a prettified bundle ID.
- Rebuild the list after preparation if the first load ran before the container registry was ready.
## 应用列表 / App list
The list no longer requires a readable LaunchServices CSStore:
1. Enumerate app data containers through `wq_enumerate` (MCM / LaunchServices API / full inode scan fallback).
2. Resolve each container's bundle ID from the native API, LaunchServices map, container xattr, metadata plist, or bundle ID resolver.
3. Resolve the display name from the native API, bundle `Info.plist`, LaunchServices display names, or the bundle ID.
4. If the screen loaded before preparation finished, reload it once the registry is ready.
The first full scan on a device where the LaunchServices API and container manager return nothing can take around 30 seconds; the launch progress sheet is shown. Later launches reuse the incremental scan cache.
## 构建要求 / Build requirements
+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"]) {