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

Binary file not shown.
+8 -2
View File
@@ -15,7 +15,7 @@
- 源码 / Source:[source/](source/)(iOS 27 Hook、内置文件浏览器集成与构建脚本 / iOS 27 hook, file-browser integration and build scripts)
```text
98ff0ad0128b107d1f1a21583d9d6e28617f3491ba4ab923a15312fc8e0c14d4 AppsManager_iOS27.ipa
f33f847a203fde8cb30e659406e2d72e65eb12aaab0de2fb6665350d68e60275 AppsManager_iOS27.ipa
```
## 功能 / Features
@@ -67,6 +67,12 @@
## 更新说明 / Updates
- 应用列表改为每次启动通过 `worse_query` 遍历 `Data/Application` 容器生成,不再依赖 LaunchServices CSStore 解析成功才能显示列表。
The app list is now built by traversing `Data/Application` containers through `worse_query` on each launch; a readable LaunchServices CSStore is no longer required.
- App 名称依次从原生 LaunchServices API、App bundle 的 `Info.plist`、LaunchServices 显示名获取,最后回退到 bundle ID。
App names come from the native LaunchServices API, the app bundle's `Info.plist`, then LaunchServices display names, with the bundle ID as the final fallback.
- 首次全量路径扫描约 30 秒,会显示进度条;后续启动使用增量扫描缓存。
The first full path scan takes about 30 seconds and shows progress; later launches use the incremental scan cache.
- 修复首次启动空白、一次性「开发者提示」遮挡列表的问题。
Fixed the blank first launch and the one-time developer alert covering the list.
- iOS 27 Hook 已移入 `Frameworks/AppsManagerHook.framework`,标准签名工具重签时会把它作为嵌套代码一并签名。
@@ -94,5 +100,5 @@
## 校验 / Checksum
```text
98ff0ad0128b107d1f1a21583d9d6e28617f3491ba4ab923a15312fc8e0c14d4 AppsManager_iOS27.ipa
f33f847a203fde8cb30e659406e2d72e65eb12aaab0de2fb6665350d68e60275 AppsManager_iOS27.ipa
```
+1 -1
View File
@@ -1 +1 @@
98ff0ad0128b107d1f1a21583d9d6e28617f3491ba4ab923a15312fc8e0c14d4 AppsManager_iOS27.ipa
f33f847a203fde8cb30e659406e2d72e65eb12aaab0de2fb6665350d68e60275 AppsManager_iOS27.ipa
+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"]) {