Build app list from worse_query path traversal with name resolution
This commit is contained in:
1 parent
176ff30ee5
commit
9bc7b9bcc6
5 files changed
+201
-22
No files matched your search
Binary file not shown.
@@ -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
@@ -1 +1 @@
|
||||
98ff0ad0128b107d1f1a21583d9d6e28617f3491ba4ab923a15312fc8e0c14d4 AppsManager_iOS27.ipa
|
||||
f33f847a203fde8cb30e659406e2d72e65eb12aaab0de2fb6665350d68e60275 AppsManager_iOS27.ipa
|
||||
@@ -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
|
||||
|
||||
|
||||
+178
-19
@@ -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;
|
||||
}
|
||||
if (progress) progress(0.40, WBLocalized(@"parsing_apps"));
|
||||
NSMutableDictionary<NSString *, NSString *> *lsContainerToID = [NSMutableDictionary new];
|
||||
if (store.length) {
|
||||
[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) {
|
||||
for (NSString *path in wq_ls_container_paths(store)) {
|
||||
NSString *bundleID = wq_bundle_id_for_container(path);
|
||||
if (bundleID.length) lsContainerToID[path] = bundleID;
|
||||
}
|
||||
}
|
||||
|
||||
// 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