Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e5851c521 |
No files matched your search
@@ -16,7 +16,7 @@ FilzaApplySandboxExt_FILES = Tweak.m MCMBridge.m MCMFilzaIntegration.m \
|
|||||||
PosterBoardFeature.m UpdateChecker.m \
|
PosterBoardFeature.m UpdateChecker.m \
|
||||||
worse_query.m pathleak.m fallback_query.m \
|
worse_query.m pathleak.m fallback_query.m \
|
||||||
WQFilzaIntegration.m WQWebDAVServer.m WQStartupUI.m \
|
WQFilzaIntegration.m WQWebDAVServer.m WQStartupUI.m \
|
||||||
WQTerminal.m WQPackages.m WQSSHServer.m WQRespring.m WQArchive.m
|
WQTerminal.m WQPackages.m WQSSHServer.m WQRespring.m WQArchive.m WQLSStore.m
|
||||||
|
|
||||||
# --- Flags ---
|
# --- Flags ---
|
||||||
FilzaApplySandboxExt_CFLAGS = -I$(PWD)/compat -I$(PWD) -I$(PWD)/XPF/src -I$(PWD)/XPF/external/ChOma/include \
|
FilzaApplySandboxExt_CFLAGS = -I$(PWD)/compat -I$(PWD) -I$(PWD)/XPF/src -I$(PWD)/XPF/external/ChOma/include \
|
||||||
|
|||||||
@@ -32,6 +32,13 @@ filza_develop.ipa
|
|||||||
|
|
||||||
- Per-container worse_query grants for app, extension, internal-daemon and
|
- Per-container worse_query grants for app, extension, internal-daemon and
|
||||||
system-group data.
|
system-group data.
|
||||||
|
- App Manager names come from lsd's LaunchServices database (no per-app bundle
|
||||||
|
reads needed) and each row shows the app data container size.
|
||||||
|
- App discovery uses the LaunchServices container list instead of a full
|
||||||
|
inode scan on supported versions, so the first launch and App Manager open
|
||||||
|
much faster.
|
||||||
|
- App icons are used where iOS returns them (`_applicationIconImageForBundle-`
|
||||||
|
`Identifier:format:scale:`); otherwise Filza's placeholder is shown.
|
||||||
- Accessible roots, App Data (by app), App Data (UUID), App Groups and System
|
- Accessible roots, App Data (by app), App Data (UUID), App Groups and System
|
||||||
Data links saved as Filza favorites.
|
Data links saved as Filza favorites.
|
||||||
- Startup progress overlay; first launch indexes installed apps and container
|
- Startup progress overlay; first launch indexes installed apps and container
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ typedef int (*WQAddChdirFn)(posix_spawn_file_actions_t *, const char *);
|
|||||||
#include "WQTerminal.h"
|
#include "WQTerminal.h"
|
||||||
#include "WQSSHServer.h"
|
#include "WQSSHServer.h"
|
||||||
#include "WQPackages.h"
|
#include "WQPackages.h"
|
||||||
|
#import "WQLSStore.h"
|
||||||
|
|
||||||
#pragma mark - Root Helper Hooks
|
#pragma mark - Root Helper Hooks
|
||||||
|
|
||||||
@@ -600,6 +601,82 @@ static NSArray *WQLSApplicationSources(id workspace, NSArray *original)
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// App icons: iOS's private UIKit API returns the SpringBoard icon on iOS 27.
|
||||||
|
// iOS 26 returns a placeholder; the bundle PNG fallback is then used when the
|
||||||
|
// bundle happens to be readable. Icons are written into the app's tmp folder
|
||||||
|
// and exposed through ApplicationItem.iconPath.
|
||||||
|
@interface UIImage (WQPrivateIcon)
|
||||||
|
+ (UIImage *)_applicationIconImageForBundleIdentifier:(NSString *)bundleID
|
||||||
|
format:(int)format
|
||||||
|
scale:(CGFloat)scale;
|
||||||
|
@end
|
||||||
|
|
||||||
|
static NSString *WQIconPathForBundleIdentifier(NSString *bundleID)
|
||||||
|
{
|
||||||
|
if (!bundleID.length) return nil;
|
||||||
|
static NSMutableDictionary<NSString *, NSString *> *paths;
|
||||||
|
static NSData *placeholder;
|
||||||
|
static dispatch_once_t onceToken;
|
||||||
|
dispatch_once(&onceToken, ^{
|
||||||
|
paths = [NSMutableDictionary dictionary];
|
||||||
|
if ([UIImage respondsToSelector:
|
||||||
|
@selector(_applicationIconImageForBundleIdentifier:format:scale:)]) {
|
||||||
|
UIImage *image = [UIImage _applicationIconImageForBundleIdentifier:
|
||||||
|
@"com.jbjf.filza.nonexistent-icon" format:2 scale:2.0];
|
||||||
|
if (image.CGImage) placeholder = UIImagePNGRepresentation(image);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
@synchronized (paths) {
|
||||||
|
NSString *cached = paths[bundleID];
|
||||||
|
if (cached.length) {
|
||||||
|
if ([NSFileManager.defaultManager fileExistsAtPath:cached])
|
||||||
|
return cached;
|
||||||
|
[paths removeObjectForKey:bundleID];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BOOL responds = [UIImage respondsToSelector:
|
||||||
|
@selector(_applicationIconImageForBundleIdentifier:format:scale:)];
|
||||||
|
if (getenv("WQ_APP_NAME_DEBUG")) {
|
||||||
|
static int requestDebug;
|
||||||
|
if (requestDebug++ < 10)
|
||||||
|
NSLog(@"[Apps] icon request %@ responds=%d", bundleID, responds);
|
||||||
|
}
|
||||||
|
if (!responds) return nil;
|
||||||
|
UIImage *image = [UIImage _applicationIconImageForBundleIdentifier:bundleID
|
||||||
|
format:2 scale:2.0];
|
||||||
|
if (!image.CGImage) {
|
||||||
|
if (getenv("WQ_APP_NAME_DEBUG")) {
|
||||||
|
static int nilDebug;
|
||||||
|
if (nilDebug++ < 10)
|
||||||
|
NSLog(@"[Apps] icon %@ returned no image", bundleID);
|
||||||
|
}
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
NSData *png = UIImagePNGRepresentation(image);
|
||||||
|
if (!png.length) return nil;
|
||||||
|
BOOL isPlaceholder = placeholder.length && [png isEqualToData:placeholder];
|
||||||
|
if (getenv("WQ_APP_NAME_DEBUG")) {
|
||||||
|
static int iconDebug;
|
||||||
|
if (iconDebug++ < 10)
|
||||||
|
NSLog(@"[Apps] icon %@ image=%dx%d png=%lu placeholder=%d",
|
||||||
|
bundleID, (int)image.size.width, (int)image.size.height,
|
||||||
|
(unsigned long)png.length, isPlaceholder);
|
||||||
|
}
|
||||||
|
if (isPlaceholder) return nil;
|
||||||
|
|
||||||
|
NSString *directory = [NSTemporaryDirectory()
|
||||||
|
stringByAppendingPathComponent:@"WQAppIcons"];
|
||||||
|
[NSFileManager.defaultManager createDirectoryAtPath:directory
|
||||||
|
withIntermediateDirectories:YES attributes:nil error:nil];
|
||||||
|
NSString *name = [bundleID stringByReplacingOccurrencesOfString:@"/"
|
||||||
|
withString:@"_"];
|
||||||
|
NSString *path = [directory stringByAppendingPathComponent:
|
||||||
|
[name stringByAppendingPathExtension:@"png"]];
|
||||||
|
if (![png writeToFile:path atomically:YES]) return nil;
|
||||||
|
@synchronized (paths) { paths[bundleID] = path; }
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
// --- Hook: allApplications fallback ---
|
// --- Hook: allApplications fallback ---
|
||||||
static IMP orig_allApplications = NULL;
|
static IMP orig_allApplications = NULL;
|
||||||
static id hook_allApplications(id self, SEL _cmd) {
|
static id hook_allApplications(id self, SEL _cmd) {
|
||||||
@@ -631,13 +708,12 @@ static id hook_allApplications(id self, SEL _cmd) {
|
|||||||
for (NSDictionary *info in wqApps) {
|
for (NSDictionary *info in wqApps) {
|
||||||
NSString *bundleID = info[@"bundleID"];
|
NSString *bundleID = info[@"bundleID"];
|
||||||
if (!bundleID.length || [known containsObject:bundleID]) continue;
|
if (!bundleID.length || [known containsObject:bundleID]) continue;
|
||||||
id proxy = nil;
|
id proxy = [[WQApplicationProxy alloc] initWithAppInfo:info];
|
||||||
if ([proxyClass respondsToSelector:
|
if (!proxy && [proxyClass respondsToSelector:
|
||||||
NSSelectorFromString(@"applicationProxyForIdentifier:")]) {
|
NSSelectorFromString(@"applicationProxyForIdentifier:")]) {
|
||||||
proxy = ((id(*)(id,SEL,id))objc_msgSend)(proxyClass,
|
proxy = ((id(*)(id,SEL,id))objc_msgSend)(proxyClass,
|
||||||
NSSelectorFromString(@"applicationProxyForIdentifier:"), bundleID);
|
NSSelectorFromString(@"applicationProxyForIdentifier:"), bundleID);
|
||||||
}
|
}
|
||||||
if (!proxy) proxy = [[WQApplicationProxy alloc] initWithAppInfo:info];
|
|
||||||
if (!proxy) continue;
|
if (!proxy) continue;
|
||||||
[apps addObject:proxy];
|
[apps addObject:proxy];
|
||||||
[known addObject:bundleID];
|
[known addObject:bundleID];
|
||||||
@@ -656,26 +732,18 @@ static id hook_allApplications(id self, SEL _cmd) {
|
|||||||
NSString *bundlePath = WQBundlePathForIdentifier(bundleID);
|
NSString *bundlePath = WQBundlePathForIdentifier(bundleID);
|
||||||
if (bundlePath.length &&
|
if (bundlePath.length &&
|
||||||
[knownBundlePaths containsObject:bundlePath]) continue;
|
[knownBundlePaths containsObject:bundlePath]) continue;
|
||||||
id proxy = nil;
|
|
||||||
if ([proxyClass respondsToSelector:
|
|
||||||
NSSelectorFromString(@"applicationProxyForIdentifier:")]) {
|
|
||||||
proxy = ((id(*)(id,SEL,id))objc_msgSend)(proxyClass,
|
|
||||||
NSSelectorFromString(@"applicationProxyForIdentifier:"),
|
|
||||||
bundleID);
|
|
||||||
}
|
|
||||||
if (!proxy) {
|
|
||||||
NSString *error = nil;
|
NSString *error = nil;
|
||||||
NSString *dataPath = MCMFilzaDataContainerPath(bundleID,
|
NSString *dataPath = MCMFilzaDataContainerPath(bundleID,
|
||||||
&error);
|
&error);
|
||||||
NSMutableDictionary *info = [@{
|
NSMutableDictionary *info = [@{
|
||||||
@"bundleID": bundleID,
|
@"bundleID": bundleID,
|
||||||
@"name": WQFallbackDisplayName(bundleID),
|
@"name": WQLSDisplayName(bundleID) ?:
|
||||||
|
WQFallbackDisplayName(bundleID),
|
||||||
@"version": @"1.0",
|
@"version": @"1.0",
|
||||||
} mutableCopy];
|
} mutableCopy];
|
||||||
if (bundlePath.length) info[@"bundlePath"] = bundlePath;
|
if (bundlePath.length) info[@"bundlePath"] = bundlePath;
|
||||||
if (dataPath.length) info[@"dataPath"] = dataPath;
|
if (dataPath.length) info[@"dataPath"] = dataPath;
|
||||||
proxy = [[WQApplicationProxy alloc] initWithAppInfo:info];
|
id proxy = [[WQApplicationProxy alloc] initWithAppInfo:info];
|
||||||
}
|
|
||||||
if (proxy) {
|
if (proxy) {
|
||||||
[apps addObject:proxy];
|
[apps addObject:proxy];
|
||||||
[known addObject:bundleID];
|
[known addObject:bundleID];
|
||||||
@@ -702,18 +770,16 @@ static id hook_allApplications(id self, SEL _cmd) {
|
|||||||
NSDictionary *info = [NSDictionary dictionaryWithContentsOfFile:plist];
|
NSDictionary *info = [NSDictionary dictionaryWithContentsOfFile:plist];
|
||||||
NSString *bid = info[@"CFBundleIdentifier"];
|
NSString *bid = info[@"CFBundleIdentifier"];
|
||||||
if (bid) {
|
if (bid) {
|
||||||
id proxy = [NSClassFromString(@"LSApplicationProxy") applicationProxyForIdentifier:bid];
|
id proxy = [[WQApplicationProxy alloc] initWithAppInfo:@{
|
||||||
if (!proxy) {
|
|
||||||
proxy = [[WQApplicationProxy alloc] initWithAppInfo:@{
|
|
||||||
@"bundleID": bid,
|
@"bundleID": bid,
|
||||||
@"bundlePath": [uuidPath stringByAppendingPathComponent:item],
|
@"bundlePath": [uuidPath stringByAppendingPathComponent:item],
|
||||||
@"dataPath": WQDataPathForIdentifier(bid) ?: @"",
|
@"dataPath": WQDataPathForIdentifier(bid) ?: @"",
|
||||||
@"name": info[@"CFBundleDisplayName"] ?:
|
@"name": WQLSDisplayName(bid) ?:
|
||||||
|
info[@"CFBundleDisplayName"] ?:
|
||||||
info[@"CFBundleName"] ?: bid,
|
info[@"CFBundleName"] ?: bid,
|
||||||
@"version": info[@"CFBundleShortVersionString"] ?:
|
@"version": info[@"CFBundleShortVersionString"] ?:
|
||||||
info[@"CFBundleVersion"] ?: @"1.0",
|
info[@"CFBundleVersion"] ?: @"1.0",
|
||||||
}];
|
}];
|
||||||
}
|
|
||||||
if (proxy) [apps addObject:proxy];
|
if (proxy) [apps addObject:proxy];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -727,18 +793,16 @@ static id hook_allApplications(id self, SEL _cmd) {
|
|||||||
NSDictionary *info = [NSDictionary dictionaryWithContentsOfFile:plist];
|
NSDictionary *info = [NSDictionary dictionaryWithContentsOfFile:plist];
|
||||||
NSString *bid = info[@"CFBundleIdentifier"];
|
NSString *bid = info[@"CFBundleIdentifier"];
|
||||||
if (bid) {
|
if (bid) {
|
||||||
id proxy = [NSClassFromString(@"LSApplicationProxy") applicationProxyForIdentifier:bid];
|
id proxy = [[WQApplicationProxy alloc] initWithAppInfo:@{
|
||||||
if (!proxy) {
|
|
||||||
proxy = [[WQApplicationProxy alloc] initWithAppInfo:@{
|
|
||||||
@"bundleID": bid,
|
@"bundleID": bid,
|
||||||
@"bundlePath": [@"/Applications" stringByAppendingPathComponent:item],
|
@"bundlePath": [@"/Applications" stringByAppendingPathComponent:item],
|
||||||
@"dataPath": WQDataPathForIdentifier(bid) ?: @"",
|
@"dataPath": WQDataPathForIdentifier(bid) ?: @"",
|
||||||
@"name": info[@"CFBundleDisplayName"] ?:
|
@"name": WQLSDisplayName(bid) ?:
|
||||||
|
info[@"CFBundleDisplayName"] ?:
|
||||||
info[@"CFBundleName"] ?: bid,
|
info[@"CFBundleName"] ?: bid,
|
||||||
@"version": info[@"CFBundleShortVersionString"] ?:
|
@"version": info[@"CFBundleShortVersionString"] ?:
|
||||||
info[@"CFBundleVersion"] ?: @"1.0",
|
info[@"CFBundleVersion"] ?: @"1.0",
|
||||||
}];
|
}];
|
||||||
}
|
|
||||||
if (proxy) [apps addObject:proxy];
|
if (proxy) [apps addObject:proxy];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -781,6 +845,8 @@ static void hook_setAppProxy(id self, SEL _cmd, id proxy) {
|
|||||||
// bundle path.
|
// bundle path.
|
||||||
NSString *displayName = nil;
|
NSString *displayName = nil;
|
||||||
if (bundlePath) {
|
if (bundlePath) {
|
||||||
|
if (!WQGrantExtraPath(bundlePath, NO))
|
||||||
|
WQGrantExtraPath(bundlePath.stringByDeletingLastPathComponent, NO);
|
||||||
NSString *plist = [bundlePath stringByAppendingPathComponent:@"Info.plist"];
|
NSString *plist = [bundlePath stringByAppendingPathComponent:@"Info.plist"];
|
||||||
NSDictionary *info = [NSDictionary dictionaryWithContentsOfFile:plist];
|
NSDictionary *info = [NSDictionary dictionaryWithContentsOfFile:plist];
|
||||||
displayName = info[@"CFBundleDisplayName"];
|
displayName = info[@"CFBundleDisplayName"];
|
||||||
@@ -794,7 +860,15 @@ static void hook_setAppProxy(id self, SEL _cmd, id proxy) {
|
|||||||
|
|
||||||
// Fix icon path
|
// Fix icon path
|
||||||
NSString *iconPath = ((id(*)(id,SEL))objc_msgSend)(self, NSSelectorFromString(@"iconPath"));
|
NSString *iconPath = ((id(*)(id,SEL))objc_msgSend)(self, NSSelectorFromString(@"iconPath"));
|
||||||
if (!iconPath && bundlePath) {
|
if (!iconPath.length) {
|
||||||
|
NSString *systemIcon = WQIconPathForBundleIdentifier(bundleId);
|
||||||
|
if (systemIcon.length) {
|
||||||
|
((void(*)(id,SEL,id))objc_msgSend)(self,
|
||||||
|
NSSelectorFromString(@"setIconPath:"), systemIcon);
|
||||||
|
iconPath = systemIcon;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!iconPath.length && bundlePath) {
|
||||||
NSString *plist = [bundlePath stringByAppendingPathComponent:@"Info.plist"];
|
NSString *plist = [bundlePath stringByAppendingPathComponent:@"Info.plist"];
|
||||||
NSDictionary *info = [NSDictionary dictionaryWithContentsOfFile:plist];
|
NSDictionary *info = [NSDictionary dictionaryWithContentsOfFile:plist];
|
||||||
NSString *found = findIconPath(bundlePath, info);
|
NSString *found = findIconPath(bundlePath, info);
|
||||||
@@ -897,6 +971,77 @@ static void hook_appsViewDidAppear(id self, SEL _cmd, BOOL animated)
|
|||||||
dispatch_get_main_queue(), retry);
|
dispatch_get_main_queue(), retry);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static IMP orig_appCalculateDiskUsage;
|
||||||
|
static NSMutableDictionary<NSString *, NSNumber *> *gAppContainerSizes;
|
||||||
|
static NSMutableDictionary<NSString *, NSDate *> *gAppContainerSizeDates;
|
||||||
|
|
||||||
|
static unsigned long long WQDirectoryAllocatedSize(NSString *path)
|
||||||
|
{
|
||||||
|
NSURL *url = [NSURL fileURLWithPath:path isDirectory:YES];
|
||||||
|
NSArray<NSURLResourceKey> *keys = @[
|
||||||
|
NSURLTotalFileAllocatedSizeKey, NSURLFileSizeKey,
|
||||||
|
];
|
||||||
|
NSDirectoryEnumerator<NSURL *> *enumerator =
|
||||||
|
[NSFileManager.defaultManager enumeratorAtURL:url
|
||||||
|
includingPropertiesForKeys:keys options:0
|
||||||
|
errorHandler:^BOOL(NSURL *url, NSError *error) { return YES; }];
|
||||||
|
unsigned long long total = 0;
|
||||||
|
for (NSURL *file in enumerator) {
|
||||||
|
NSDictionary<NSURLResourceKey, id> *values =
|
||||||
|
[file resourceValuesForKeys:keys error:nil];
|
||||||
|
NSNumber *size = values[NSURLTotalFileAllocatedSizeKey];
|
||||||
|
if (![size isKindOfClass:NSNumber.class])
|
||||||
|
size = values[NSURLFileSizeKey];
|
||||||
|
if ([size isKindOfClass:NSNumber.class])
|
||||||
|
total += size.unsignedLongLongValue;
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
static unsigned long long WQAppContainerSize(NSString *path)
|
||||||
|
{
|
||||||
|
if (!path.length) return 0;
|
||||||
|
static dispatch_once_t onceToken;
|
||||||
|
dispatch_once(&onceToken, ^{
|
||||||
|
gAppContainerSizes = [NSMutableDictionary dictionary];
|
||||||
|
gAppContainerSizeDates = [NSMutableDictionary dictionary];
|
||||||
|
});
|
||||||
|
@synchronized (gAppContainerSizes) {
|
||||||
|
NSNumber *cached = gAppContainerSizes[path];
|
||||||
|
NSDate *date = gAppContainerSizeDates[path];
|
||||||
|
if (cached && date &&
|
||||||
|
-[date timeIntervalSinceNow] < 300)
|
||||||
|
return cached.unsignedLongLongValue;
|
||||||
|
unsigned long long size = WQDirectoryAllocatedSize(path);
|
||||||
|
gAppContainerSizes[path] = @(size);
|
||||||
|
gAppContainerSizeDates[path] = [NSDate date];
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void hook_appCalculateDiskUsage(id self, SEL _cmd)
|
||||||
|
{
|
||||||
|
if (orig_appCalculateDiskUsage)
|
||||||
|
((void(*)(id, SEL))orig_appCalculateDiskUsage)(self, _cmd);
|
||||||
|
|
||||||
|
NSString *container = nil;
|
||||||
|
SEL documentSelector = NSSelectorFromString(@"documentPath");
|
||||||
|
if ([self respondsToSelector:documentSelector])
|
||||||
|
container = ((id(*)(id, SEL))objc_msgSend)(self, documentSelector);
|
||||||
|
if (![container isKindOfClass:NSString.class] || !container.length) return;
|
||||||
|
|
||||||
|
unsigned long long size = WQAppContainerSize(container);
|
||||||
|
static int sizeLogCount;
|
||||||
|
if (getenv("WQ_TEST_APPS") && sizeLogCount++ < 8)
|
||||||
|
NSLog(@"[Apps] size %@ = %llu", container.lastPathComponent, size);
|
||||||
|
((void(*)(id, SEL, unsigned long long))objc_msgSend)(self,
|
||||||
|
NSSelectorFromString(@"setFileSize:"), size);
|
||||||
|
NSString *text = [NSByteCountFormatter stringFromByteCount:(long long)size
|
||||||
|
countStyle:NSByteCountFormatterCountStyleFile];
|
||||||
|
((void(*)(id, SEL, id))objc_msgSend)(self,
|
||||||
|
NSSelectorFromString(@"setAFileSizeString:"), text);
|
||||||
|
}
|
||||||
|
|
||||||
#pragma mark - License / Integrity Bypass
|
#pragma mark - License / Integrity Bypass
|
||||||
|
|
||||||
// Suppress "Main binary was modified" and "Not activated" alerts.
|
// Suppress "Main binary was modified" and "Not activated" alerts.
|
||||||
@@ -2308,6 +2453,11 @@ static void installHooks(void) {
|
|||||||
Method m;
|
Method m;
|
||||||
m = class_getInstanceMethod(appItem, NSSelectorFromString(@"setAppProxy:"));
|
m = class_getInstanceMethod(appItem, NSSelectorFromString(@"setAppProxy:"));
|
||||||
if (m) { orig_setAppProxy = method_getImplementation(m); method_setImplementation(m, (IMP)hook_setAppProxy); }
|
if (m) { orig_setAppProxy = method_getImplementation(m); method_setImplementation(m, (IMP)hook_setAppProxy); }
|
||||||
|
m = class_getInstanceMethod(appItem, NSSelectorFromString(@"calculateDiskUsage"));
|
||||||
|
if (m) {
|
||||||
|
orig_appCalculateDiskUsage = method_getImplementation(m);
|
||||||
|
method_setImplementation(m, (IMP)hook_appCalculateDiskUsage);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Class appsVC = NSClassFromString(@"TGApplicationsViewController");
|
Class appsVC = NSClassFromString(@"TGApplicationsViewController");
|
||||||
if (appsVC) {
|
if (appsVC) {
|
||||||
@@ -2496,6 +2646,24 @@ __attribute__((constructor)) void TweakInit(void) {
|
|||||||
installHooks();
|
installHooks();
|
||||||
// Populate the MCM root before Filza restores its initial browser path.
|
// Populate the MCM root before Filza restores its initial browser path.
|
||||||
runMCMPath();
|
runMCMPath();
|
||||||
|
if (getenv("WQ_TEST_APPS")) {
|
||||||
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 30 * NSEC_PER_SEC),
|
||||||
|
dispatch_get_main_queue(), ^{
|
||||||
|
Class appsClass = NSClassFromString(@"TGApplicationsViewController");
|
||||||
|
if (!appsClass) return;
|
||||||
|
UIViewController *apps = [[appsClass alloc] init];
|
||||||
|
UINavigationController *navigation = [[UINavigationController alloc]
|
||||||
|
initWithRootViewController:apps];
|
||||||
|
UIWindow *window = nil;
|
||||||
|
for (UIWindow *candidate in UIApplication.sharedApplication.windows)
|
||||||
|
if (candidate.isKeyWindow && !candidate.isHidden) { window = candidate; break; }
|
||||||
|
UIViewController *root = window.rootViewController;
|
||||||
|
while (root.presentedViewController)
|
||||||
|
root = root.presentedViewController;
|
||||||
|
if (root) [root presentViewController:navigation
|
||||||
|
animated:YES completion:nil];
|
||||||
|
});
|
||||||
|
}
|
||||||
if (getenv("WQ_TEST_SETTINGS")) {
|
if (getenv("WQ_TEST_SETTINGS")) {
|
||||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 26 * NSEC_PER_SEC),
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 26 * NSEC_PER_SEC),
|
||||||
dispatch_get_main_queue(), ^{
|
dispatch_get_main_queue(), ^{
|
||||||
|
|||||||
+98
-14
@@ -12,6 +12,7 @@
|
|||||||
#import "worse_query.h"
|
#import "worse_query.h"
|
||||||
#import "pathleak.h"
|
#import "pathleak.h"
|
||||||
#import "fallback_query.h"
|
#import "fallback_query.h"
|
||||||
|
#import "WQLSStore.h"
|
||||||
|
|
||||||
#import <Foundation/Foundation.h>
|
#import <Foundation/Foundation.h>
|
||||||
#import <Security/Security.h>
|
#import <Security/Security.h>
|
||||||
@@ -88,9 +89,10 @@ static NSInteger WQOSMajor(void)
|
|||||||
|
|
||||||
static BOOL WQUsesAlternateFlags(NSString *path)
|
static BOOL WQUsesAlternateFlags(NSString *path)
|
||||||
{
|
{
|
||||||
// worse_query needs the alternate flags for App Group containers on 26.x.
|
// iOS 26 needs the alternative query flags for app, extension, daemon
|
||||||
return WQOSMajor() == 26 &&
|
// and app-group containers. iOS 27 normally uses the standard flags.
|
||||||
[path containsString:@"/var/mobile/Containers/Shared/AppGroup"];
|
(void)path;
|
||||||
|
return WQOSMajor() < 27;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Paths documented by forcequitOS/worse_query, ordered from broadest to most
|
/// Paths documented by forcequitOS/worse_query, ordered from broadest to most
|
||||||
@@ -177,22 +179,55 @@ NSString *WQBestAppGroup(void)
|
|||||||
|
|
||||||
#pragma mark - Query engine
|
#pragma mark - Query engine
|
||||||
|
|
||||||
|
static BOOL WQGrantActuallyOpens(NSString *target)
|
||||||
|
{
|
||||||
|
int descriptor = open(target.fileSystemRepresentation,
|
||||||
|
O_RDONLY | O_CLOEXEC);
|
||||||
|
if (descriptor >= 0) {
|
||||||
|
close(descriptor);
|
||||||
|
return YES;
|
||||||
|
}
|
||||||
|
// An ordinary permission error is not a failed grant. Only EPERM means
|
||||||
|
// the sandbox extension did not help.
|
||||||
|
return errno != EPERM;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int64_t WQQueryOnce(NSString *target, BOOL alternate,
|
||||||
|
BOOL createIfMissing)
|
||||||
|
{
|
||||||
|
NSString *group = WQBestAppGroup();
|
||||||
|
if (!group.length) return -1;
|
||||||
|
int64_t handle = worse_query(target, group, alternate, createIfMissing);
|
||||||
|
if (handle < 0)
|
||||||
|
handle = fallback_query(target, alternate);
|
||||||
|
return handle;
|
||||||
|
}
|
||||||
|
|
||||||
static int64_t WQAcquireExtension(NSString *target, BOOL createIfMissing)
|
static int64_t WQAcquireExtension(NSString *target, BOOL createIfMissing)
|
||||||
{
|
{
|
||||||
if (![target hasPrefix:@"/"]) return -255;
|
if (![target hasPrefix:@"/"]) return -255;
|
||||||
NSString *group = WQBestAppGroup();
|
if (!WQBestAppGroup().length) return -1;
|
||||||
if (!group.length) return -1;
|
|
||||||
|
|
||||||
[gQueryLock lock];
|
[gQueryLock lock];
|
||||||
int64_t handle = worse_query(target, group,
|
BOOL alternate = WQUsesAlternateFlags(target);
|
||||||
WQUsesAlternateFlags(target), createIfMissing);
|
int64_t handle = WQQueryOnce(target, alternate, createIfMissing);
|
||||||
if (handle < 0) {
|
|
||||||
int64_t first = handle;
|
int64_t first = handle;
|
||||||
handle = fallback_query(target, WQUsesAlternateFlags(target));
|
if (handle >= 0 && !createIfMissing &&
|
||||||
if (handle < 0 && getenv("WQ_VERBOSE")) {
|
!WQGrantActuallyOpens(target)) {
|
||||||
NSLog(@"[WQ] grant failed path=%@ worse_query=%lld fallback=%lld",
|
worse_query_release(handle);
|
||||||
target, (long long)first, (long long)handle);
|
handle = -8;
|
||||||
}
|
}
|
||||||
|
if (handle < 0 && handle != -1 && handle != -2) {
|
||||||
|
int64_t retry = WQQueryOnce(target, !alternate, createIfMissing);
|
||||||
|
if (retry >= 0 && !createIfMissing && !WQGrantActuallyOpens(target)) {
|
||||||
|
worse_query_release(retry);
|
||||||
|
retry = -8;
|
||||||
|
}
|
||||||
|
if (retry >= 0) handle = retry;
|
||||||
|
}
|
||||||
|
if (handle < 0 && getenv("WQ_VERBOSE")) {
|
||||||
|
NSLog(@"[WQ] grant failed path=%@ first=%lld final=%lld",
|
||||||
|
target, (long long)first, (long long)handle);
|
||||||
}
|
}
|
||||||
[gQueryLock unlock];
|
[gQueryLock unlock];
|
||||||
return handle;
|
return handle;
|
||||||
@@ -616,6 +651,8 @@ static NSString *WQInferIdentifierFromDataDirectory(NSString *containerPath)
|
|||||||
static NSString *WQContainerIdentifier(NSString *containerPath)
|
static NSString *WQContainerIdentifier(NSString *containerPath)
|
||||||
{
|
{
|
||||||
if (!containerPath.length) return nil;
|
if (!containerPath.length) return nil;
|
||||||
|
NSString *lsIdentifier = WQLSBundleIDForContainer(containerPath);
|
||||||
|
if (lsIdentifier.length) return lsIdentifier;
|
||||||
NSString *metadataPath = [containerPath
|
NSString *metadataPath = [containerPath
|
||||||
stringByAppendingPathComponent:@".com.apple.mobile_container_manager.metadata.plist"];
|
stringByAppendingPathComponent:@".com.apple.mobile_container_manager.metadata.plist"];
|
||||||
NSFileManager *fm = NSFileManager.defaultManager;
|
NSFileManager *fm = NSFileManager.defaultManager;
|
||||||
@@ -771,9 +808,12 @@ WQAppInfoFromContainers(NSArray<NSString *> *containers)
|
|||||||
} mutableCopy];
|
} mutableCopy];
|
||||||
if (!dataMap[bundleID]) dataMap[bundleID] = containerPath;
|
if (!dataMap[bundleID]) dataMap[bundleID] = containerPath;
|
||||||
|
|
||||||
NSString *bundlePath = gBundleMap[bundleID];
|
NSString *bundlePath = WQLSBundlePathForIdentifier(bundleID);
|
||||||
|
if (!bundlePath.length) bundlePath = gBundleMap[bundleID];
|
||||||
if (bundlePath.length) {
|
if (bundlePath.length) {
|
||||||
info[@"bundlePath"] = bundlePath;
|
info[@"bundlePath"] = bundlePath;
|
||||||
|
if (!WQGrantExtraPath(bundlePath, NO))
|
||||||
|
WQGrantExtraPath(bundlePath.stringByDeletingLastPathComponent, NO);
|
||||||
NSDictionary *infoPlist = [NSDictionary dictionaryWithContentsOfFile:
|
NSDictionary *infoPlist = [NSDictionary dictionaryWithContentsOfFile:
|
||||||
[bundlePath stringByAppendingPathComponent:@"Info.plist"]];
|
[bundlePath stringByAppendingPathComponent:@"Info.plist"]];
|
||||||
NSString *name = infoPlist[@"CFBundleDisplayName"];
|
NSString *name = infoPlist[@"CFBundleDisplayName"];
|
||||||
@@ -783,6 +823,8 @@ WQAppInfoFromContainers(NSArray<NSString *> *containers)
|
|||||||
if (!version.length) version = infoPlist[@"CFBundleVersion"];
|
if (!version.length) version = infoPlist[@"CFBundleVersion"];
|
||||||
if (version.length) info[@"version"] = version;
|
if (version.length) info[@"version"] = version;
|
||||||
}
|
}
|
||||||
|
NSString *lsName = WQLSDisplayName(bundleID);
|
||||||
|
if (lsName.length) info[@"name"] = lsName;
|
||||||
|
|
||||||
Class proxyClass = NSClassFromString(@"LSApplicationProxy");
|
Class proxyClass = NSClassFromString(@"LSApplicationProxy");
|
||||||
id proxy = [proxyClass respondsToSelector:
|
id proxy = [proxyClass respondsToSelector:
|
||||||
@@ -797,6 +839,24 @@ WQAppInfoFromContainers(NSArray<NSString *> *containers)
|
|||||||
(![info[@"name"] isKindOfClass:NSString.class] ||
|
(![info[@"name"] isKindOfClass:NSString.class] ||
|
||||||
[info[@"name"] isEqualToString:bundleID]))
|
[info[@"name"] isEqualToString:bundleID]))
|
||||||
info[@"name"] = proxyName;
|
info[@"name"] = proxyName;
|
||||||
|
if (getenv("WQ_APP_NAME_DEBUG")) {
|
||||||
|
static int debugNames;
|
||||||
|
if (debugNames++ < 40) {
|
||||||
|
NSString *infoName = nil;
|
||||||
|
NSInteger infoKeys = 0;
|
||||||
|
NSString *debugPath = info[@"bundlePath"];
|
||||||
|
if (debugPath.length) {
|
||||||
|
NSDictionary *plist = [NSDictionary dictionaryWithContentsOfFile:
|
||||||
|
[debugPath stringByAppendingPathComponent:@"Info.plist"]];
|
||||||
|
infoKeys = plist.count;
|
||||||
|
infoName = plist[@"CFBundleDisplayName"] ?:
|
||||||
|
plist[@"CFBundleName"];
|
||||||
|
}
|
||||||
|
NSLog(@"[WQ][name-debug] %@ ls=%@ path=%@ plistKeys=%ld plistName=%@",
|
||||||
|
bundleID, WQLSDisplayName(bundleID) ?: @"(nil)",
|
||||||
|
debugPath ?: @"(nil)", (long)infoKeys, infoName ?: @"(nil)");
|
||||||
|
}
|
||||||
|
}
|
||||||
NSString *proxyVersion = [proxy respondsToSelector:NSSelectorFromString(@"bundleVersion")]
|
NSString *proxyVersion = [proxy respondsToSelector:NSSelectorFromString(@"bundleVersion")]
|
||||||
? ((id(*)(id, SEL))objc_msgSend)(proxy,
|
? ((id(*)(id, SEL))objc_msgSend)(proxy,
|
||||||
NSSelectorFromString(@"bundleVersion")) : nil;
|
NSSelectorFromString(@"bundleVersion")) : nil;
|
||||||
@@ -1070,8 +1130,30 @@ static NSArray<NSDictionary<NSString *, NSString *> *> *WQBuildAppList(void)
|
|||||||
// even though each child container is accepted. Always enumerate children
|
// even though each child container is accepted. Always enumerate children
|
||||||
// and request their extensions individually.
|
// and request their extensions individually.
|
||||||
WQGrantRoot(root, @"App Data");
|
WQGrantRoot(root, @"App Data");
|
||||||
|
WQPostStartupProgress(0.20, @"正在读取 LaunchServices 应用数据库…");
|
||||||
|
WQLoadLaunchServicesDatabase();
|
||||||
|
|
||||||
|
NSMutableArray<NSString *> *lsContainers = [NSMutableArray array];
|
||||||
|
NSString *lsPrefix = [root stringByAppendingString:@"/"];
|
||||||
|
for (NSString *path in WQLSContainerPaths() ?: @[]) {
|
||||||
|
NSString *canonical = WQCanonicalPath(path);
|
||||||
|
if (![canonical hasPrefix:lsPrefix]) continue;
|
||||||
|
if (canonical.length != lsPrefix.length + 36) continue;
|
||||||
|
if (![[NSUUID alloc] initWithUUIDString:
|
||||||
|
[canonical substringFromIndex:lsPrefix.length]]) continue;
|
||||||
|
[lsContainers addObject:canonical];
|
||||||
|
}
|
||||||
|
|
||||||
|
NSArray<NSString *> *containers = nil;
|
||||||
|
if (lsContainers.count >= 2) {
|
||||||
|
containers = lsContainers;
|
||||||
|
WQPostStartupProgress(0.30, [NSString stringWithFormat:
|
||||||
|
@"应用数据库已列出 %lu 个数据容器,正在授权…",
|
||||||
|
(unsigned long)containers.count]);
|
||||||
|
} else {
|
||||||
WQPostStartupProgress(0.26, @"正在扫描应用数据容器…");
|
WQPostStartupProgress(0.26, @"正在扫描应用数据容器…");
|
||||||
NSArray<NSString *> *containers = WQEnumerateContainers(root);
|
containers = WQEnumerateContainers(root);
|
||||||
|
}
|
||||||
WQPostStartupProgress(0.40, [NSString stringWithFormat:
|
WQPostStartupProgress(0.40, [NSString stringWithFormat:
|
||||||
@"发现 %lu 个数据容器,正在逐个授权…", (unsigned long)containers.count]);
|
@"发现 %lu 个数据容器,正在逐个授权…", (unsigned long)containers.count]);
|
||||||
NSUInteger granted = 0;
|
NSUInteger granted = 0;
|
||||||
@@ -1229,6 +1311,8 @@ static void WQBuildBundleMapIfNeeded(void)
|
|||||||
NSString *WQBundlePathForIdentifier(NSString *bundleID)
|
NSString *WQBundlePathForIdentifier(NSString *bundleID)
|
||||||
{
|
{
|
||||||
if (!bundleID.length) return nil;
|
if (!bundleID.length) return nil;
|
||||||
|
NSString *lsPath = WQLSBundlePathForIdentifier(bundleID);
|
||||||
|
if (lsPath.length) return lsPath;
|
||||||
if (getenv("WQ_BUNDLE_SCAN")) WQBuildBundleMapIfNeeded();
|
if (getenv("WQ_BUNDLE_SCAN")) WQBuildBundleMapIfNeeded();
|
||||||
NSString *mapped = gBundleMap[bundleID];
|
NSString *mapped = gBundleMap[bundleID];
|
||||||
if (mapped.length) return mapped;
|
if (mapped.length) return mapped;
|
||||||
|
|||||||
+26
@@ -0,0 +1,26 @@
|
|||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
/// Reads lsd's LaunchServices CSStore database (bundle names, display names,
|
||||||
|
/// data-container mapping and .app bundle paths). The first call is blocking;
|
||||||
|
/// run it on a background queue.
|
||||||
|
BOOL WQLoadLaunchServicesDatabase(void);
|
||||||
|
BOOL WQLSDisplayNamesLoaded(void);
|
||||||
|
void WQLSResetMiss(void);
|
||||||
|
|
||||||
|
/// Real display name for a bundle id (database, built-in Apple table, or
|
||||||
|
/// parent-app fallback).
|
||||||
|
NSString *_Nullable WQLSDisplayName(NSString *bundleID);
|
||||||
|
|
||||||
|
/// Bundle id for an app/extension data container.
|
||||||
|
NSString *_Nullable WQLSBundleIDForContainer(NSString *path);
|
||||||
|
|
||||||
|
/// .app bundle path from the database (used for icons where readable).
|
||||||
|
NSString *_Nullable WQLSBundlePathForIdentifier(NSString *bundleID);
|
||||||
|
|
||||||
|
/// App and extension data-container paths listed by the database. Reading the
|
||||||
|
/// store is much faster than enumerating every inode. Nil when unavailable.
|
||||||
|
NSArray<NSString *> *_Nullable WQLSContainerPaths(void);
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
+930
@@ -0,0 +1,930 @@
|
|||||||
|
//
|
||||||
|
// WQLaunchServices.m
|
||||||
|
// WQ Files
|
||||||
|
// GPL-3.0-or-later.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "WQLSStore.h"
|
||||||
|
#import "WQFilzaIntegration.h"
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
#import <stdarg.h>
|
||||||
|
#import <stdlib.h>
|
||||||
|
#import <string.h>
|
||||||
|
#import <sys/stat.h>
|
||||||
|
|
||||||
|
// Compatibility helpers for this project: read/stat/list through the
|
||||||
|
// existing worse_query grant machinery and log with the Filza prefix.
|
||||||
|
static void WQLSLog(NSString *format, ...)
|
||||||
|
{
|
||||||
|
va_list args;
|
||||||
|
va_start(args, format);
|
||||||
|
NSString *message = [[NSString alloc] initWithFormat:format arguments:args];
|
||||||
|
va_end(args);
|
||||||
|
NSLog(@"[WQLS] %@", message);
|
||||||
|
}
|
||||||
|
|
||||||
|
static BOOL WQLSStat(NSString *path, struct stat *status)
|
||||||
|
{
|
||||||
|
if (!path.length || !status) return NO;
|
||||||
|
if (lstat(path.fileSystemRepresentation, status) == 0) return YES;
|
||||||
|
if (WQGrantExtraPath(path, NO) &&
|
||||||
|
lstat(path.fileSystemRepresentation, status) == 0) return YES;
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
|
||||||
|
static NSData *WQLSRead(NSString *path, NSUInteger maxBytes, NSString **errorText)
|
||||||
|
{
|
||||||
|
if (!path.length) {
|
||||||
|
if (errorText) *errorText = @"empty path";
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
NSFileHandle *handle = [NSFileHandle fileHandleForReadingAtPath:path];
|
||||||
|
if (!handle) {
|
||||||
|
WQGrantExtraPath(path, NO);
|
||||||
|
handle = [NSFileHandle fileHandleForReadingAtPath:path];
|
||||||
|
}
|
||||||
|
if (!handle) {
|
||||||
|
if (errorText) *errorText = @"read failed";
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
NSData *data = [handle readDataOfLength:maxBytes];
|
||||||
|
[handle closeFile];
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
static NSArray<NSString *> *WQLSList(NSString *directory, NSString **errorText)
|
||||||
|
{
|
||||||
|
if (!directory.length) return nil;
|
||||||
|
NSError *error = nil;
|
||||||
|
NSArray *items = [NSFileManager.defaultManager
|
||||||
|
contentsOfDirectoryAtPath:directory error:&error];
|
||||||
|
if (!items) {
|
||||||
|
WQGrantExtraPath(directory, NO);
|
||||||
|
error = nil;
|
||||||
|
items = [NSFileManager.defaultManager
|
||||||
|
contentsOfDirectoryAtPath:directory error:&error];
|
||||||
|
}
|
||||||
|
if (!items && errorText)
|
||||||
|
*errorText = error.localizedDescription ?: @"list failed";
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - App names from the LaunchServices database
|
||||||
|
|
||||||
|
// LaunchServices' own API refuses this app on 27.x (OSStatus -54) and the app
|
||||||
|
// bundles cannot be opened, but lsd keeps its database (CSStore2, magic
|
||||||
|
// "bdsl") in an Internal Daemon container that we can read. In it, Bundle
|
||||||
|
// records are 0x250 bytes long, preceded by [u32][u32 0x250]: +0x0c bundle
|
||||||
|
// id, +0x6c bundle name, +0x74 display name, each a string reference
|
||||||
|
// (string id << 2). Strings are stored as [u32 id][u32 length][UTF-8][NUL].
|
||||||
|
// Names that exist only in localized form (most Apple apps) are not decoded;
|
||||||
|
// the built-in table below covers those.
|
||||||
|
|
||||||
|
static NSString *const kWQLSStoreKey = @"WQLaunchServicesStore";
|
||||||
|
// When the database wasn't found: don't search again for a day (each search
|
||||||
|
// lists every daemon container), unless the user refreshes.
|
||||||
|
static NSString *const kWQLSMissKey = @"WQLaunchServicesStoreMissedAt";
|
||||||
|
static const NSTimeInterval kWQLSMissRetry = 24 * 60 * 60;
|
||||||
|
|
||||||
|
// These limits keep a malformed or unexpectedly large database from turning a
|
||||||
|
// synchronous display-name lookup into an unbounded read/parse operation.
|
||||||
|
static const size_t kWQLSMinStoreBytes = 0x1000;
|
||||||
|
static const size_t kWQLSMaxStoreBytes = 64u * 1024u * 1024u;
|
||||||
|
static const uint32_t kWQLSMaxStringID = 0x01000000u;
|
||||||
|
static const NSUInteger kWQLSMaxBundleRecords = 100000;
|
||||||
|
static const NSUInteger kWQLSMaxDecodedStrings = 200000;
|
||||||
|
static const NSUInteger kWQLSMaxDecodedStringBytes = 8u * 1024u * 1024u;
|
||||||
|
|
||||||
|
static NSObject *wq_ls_lock(void)
|
||||||
|
{
|
||||||
|
static NSObject *lock;
|
||||||
|
static dispatch_once_t once;
|
||||||
|
dispatch_once(&once, ^{ lock = [NSObject new]; });
|
||||||
|
return lock;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The regression tests still exercise discovery through the daemon list.
|
||||||
|
#ifdef WQ_LS_TESTING
|
||||||
|
static BOOL wq_ls_test_daemon_search = YES;
|
||||||
|
#else
|
||||||
|
static const BOOL wq_ls_test_daemon_search = NO;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
static NSString *wq_find_ls_store(BOOL *searched);
|
||||||
|
static BOOL wq_ls_candidate_info(NSString *path, struct stat *st);
|
||||||
|
|
||||||
|
static BOOL wq_ls_stat_equal(const struct stat *a, const struct stat *b)
|
||||||
|
{
|
||||||
|
return a->st_dev == b->st_dev && a->st_ino == b->st_ino &&
|
||||||
|
a->st_size == b->st_size &&
|
||||||
|
a->st_mtimespec.tv_sec == b->st_mtimespec.tv_sec &&
|
||||||
|
a->st_mtimespec.tv_nsec == b->st_mtimespec.tv_nsec;
|
||||||
|
}
|
||||||
|
|
||||||
|
static NSDictionary *wq_ls_signature(const struct stat *st)
|
||||||
|
{
|
||||||
|
return @{
|
||||||
|
@"device": @(st->st_dev), @"inode": @(st->st_ino), @"size": @(st->st_size),
|
||||||
|
@"modifiedSeconds": @(st->st_mtimespec.tv_sec),
|
||||||
|
@"modifiedNanoseconds": @(st->st_mtimespec.tv_nsec)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static BOOL wq_ls_signature_equal(NSDictionary *a, NSDictionary *b)
|
||||||
|
{
|
||||||
|
return a && b && [a isEqualToDictionary:b];
|
||||||
|
}
|
||||||
|
|
||||||
|
static NSString *wq_ls_error(NSString *path, NSString *reason)
|
||||||
|
{
|
||||||
|
return [NSString stringWithFormat:@"%@ (%@)", path ?: @"<none>", reason ?: @"unknown error"];
|
||||||
|
}
|
||||||
|
|
||||||
|
static BOOL wq_ls_candidate_info(NSString *path, struct stat *st)
|
||||||
|
{
|
||||||
|
if (!path.length || !WQLSStat(path, st) || !S_ISREG(st->st_mode) ||
|
||||||
|
st->st_size < (off_t)kWQLSMinStoreBytes || st->st_size > (off_t)kWQLSMaxStoreBytes) {
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
NSString *error = nil;
|
||||||
|
NSData *header = WQLSRead(path, 4, &error);
|
||||||
|
if (header.length != 4 || memcmp(header.bytes, "bdsl", 4) != 0) return NO;
|
||||||
|
return YES;
|
||||||
|
}
|
||||||
|
|
||||||
|
static BOOL wq_ls_is_newer(NSString *path, const struct stat *st,
|
||||||
|
NSString *bestPath, const struct stat *best)
|
||||||
|
{
|
||||||
|
if (!bestPath) return YES;
|
||||||
|
if (st->st_mtimespec.tv_sec != best->st_mtimespec.tv_sec)
|
||||||
|
return st->st_mtimespec.tv_sec > best->st_mtimespec.tv_sec;
|
||||||
|
if (st->st_mtimespec.tv_nsec != best->st_mtimespec.tv_nsec)
|
||||||
|
return st->st_mtimespec.tv_nsec > best->st_mtimespec.tv_nsec;
|
||||||
|
if (st->st_size != best->st_size) return st->st_size > best->st_size;
|
||||||
|
return [path compare:bestPath] == NSOrderedAscending;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void wq_ls_consider_candidate(NSString *path, NSString **bestPath,
|
||||||
|
struct stat *best, NSMutableSet<NSString *> *seen)
|
||||||
|
{
|
||||||
|
if (!path.length || [seen containsObject:path]) return;
|
||||||
|
[seen addObject:path];
|
||||||
|
|
||||||
|
struct stat st;
|
||||||
|
if (!wq_ls_candidate_info(path, &st)) return;
|
||||||
|
if (wq_ls_is_newer(path, &st, *bestPath, best)) {
|
||||||
|
*bestPath = path;
|
||||||
|
*best = st;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// *searched: the daemon containers were actually listed (a failed or cancelled
|
||||||
|
// scan says nothing about whether the database exists).
|
||||||
|
static NSString *wq_find_ls_store(BOOL *searched)
|
||||||
|
{
|
||||||
|
if (searched) *searched = NO;
|
||||||
|
NSString *cached = [NSUserDefaults.standardUserDefaults stringForKey:kWQLSStoreKey];
|
||||||
|
NSString *bestPath = nil;
|
||||||
|
struct stat best = {0};
|
||||||
|
struct stat cachedStat;
|
||||||
|
BOOL cachedValid = cached.length && wq_ls_candidate_info(cached, &cachedStat);
|
||||||
|
NSMutableSet<NSString *> *seen = [NSMutableSet set];
|
||||||
|
if (cachedValid) wq_ls_consider_candidate(cached, &bestPath, &best, seen);
|
||||||
|
|
||||||
|
NSArray *daemons = WQChildrenForRoot(@"/private/var/mobile/Containers/Data/InternalDaemon");
|
||||||
|
if (daemons && searched) *searched = YES;
|
||||||
|
if (daemons) {
|
||||||
|
for (NSString *container in daemons) {
|
||||||
|
NSString *caches = [container stringByAppendingPathComponent:@"Library/Caches"];
|
||||||
|
NSArray *items = WQLSList(caches, NULL);
|
||||||
|
for (NSString *name in [items sortedArrayUsingSelector:@selector(compare:)]) {
|
||||||
|
if (![name hasPrefix:@"com.apple.LaunchServices-"] || ![name hasSuffix:@".csstore"]) continue;
|
||||||
|
wq_ls_consider_candidate([caches stringByAppendingPathComponent:name], &bestPath, &best, seen);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep a valid cached path usable if enumeration is temporarily unavailable.
|
||||||
|
if (!bestPath && cachedValid) {
|
||||||
|
bestPath = cached;
|
||||||
|
}
|
||||||
|
return bestPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record layouts by release. Offsets are from the record body (after the
|
||||||
|
// 8-byte header). iOS 26 records are 0x14 bytes shorter, with the container
|
||||||
|
// path and the fields after it 4 bytes earlier than on 27.0.
|
||||||
|
typedef struct {
|
||||||
|
const char *label;
|
||||||
|
uint32_t bundleSize, bundleID, container, name, displayName, bundlePath;
|
||||||
|
uint32_t pluginSize, pluginBundleID, pluginContainer;
|
||||||
|
} WQLSLayout;
|
||||||
|
static const WQLSLayout kWQLSLayouts[] = {
|
||||||
|
{ "27.0", 0x250, 0x0c, 0x68, 0x6c, 0x74, 0x00, 0x118, 0x00, 0x54 },
|
||||||
|
{ "26", 0x23c, 0x0c, 0x64, 0x68, 0x70, 0x00, 0x104, 0x00, 0x50 },
|
||||||
|
// iOS 27.2 (from a device's layout hint): container fields 4 bytes later
|
||||||
|
// than on 27.0. Whether the bundle id moved too isn't known yet, so both
|
||||||
|
// positions are tried; the one that maps the most containers wins.
|
||||||
|
{ "27.2", 0x25c, 0x0c, 0x6c, 0x70, 0x78, 0x00, 0x120, 0x00, 0x58 },
|
||||||
|
{ "27.2b", 0x25c, 0x10, 0x6c, 0x70, 0x78, 0x00, 0x120, 0x04, 0x58 },
|
||||||
|
{ "27.2c", 0x25c, 0x0c, 0x6c, 0x70, 0x78, 0x00, 0x120, 0x04, 0x58 },
|
||||||
|
{ "27.2d", 0x25c, 0x10, 0x6c, 0x70, 0x78, 0x00, 0x120, 0x00, 0x58 },
|
||||||
|
};
|
||||||
|
static const size_t kWQLSLayoutCount = sizeof(kWQLSLayouts) / sizeof(kWQLSLayouts[0]);
|
||||||
|
static const size_t kWQLSMaxRecordSize = 0x25c;
|
||||||
|
|
||||||
|
static uint32_t wq_rd32(const uint8_t *p) { uint32_t v; memcpy(&v, p, 4); return v; }
|
||||||
|
|
||||||
|
static NSString *wq_cs_string(const uint8_t *b, size_t n, size_t o)
|
||||||
|
{
|
||||||
|
if (o > n || n - o < 8) return nil;
|
||||||
|
uint32_t len = wq_rd32(b + o + 4);
|
||||||
|
// Strings are padded with zero bytes to a multiple of 4. One whose length
|
||||||
|
// is already a multiple of 4 has no terminator (container paths are 84 or
|
||||||
|
// 88 bytes), so only shorter ones must end in a zero byte. A string must
|
||||||
|
// still end before the buffer does.
|
||||||
|
if (len == 0 || len >= 0x800 || len >= n - o - 8 || (len % 4 && b[o + 8 + len] != 0)) return nil;
|
||||||
|
if (memchr(b + o + 8, 0, len)) return nil;
|
||||||
|
NSString *s = [[NSString alloc] initWithBytes:b + o + 8 length:len encoding:NSUTF8StringEncoding];
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
static BOOL wq_looks_like_bundle_id(NSString *s)
|
||||||
|
{
|
||||||
|
static NSCharacterSet *bad;
|
||||||
|
static dispatch_once_t once;
|
||||||
|
dispatch_once(&once, ^{
|
||||||
|
bad = [[NSCharacterSet characterSetWithCharactersInString:
|
||||||
|
@"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.-_"] invertedSet];
|
||||||
|
});
|
||||||
|
return s.length > 2 && [s containsString:@"."] && ![s hasPrefix:@"."]
|
||||||
|
&& [s rangeOfCharacterFromSet:bad].location == NSNotFound;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unicode format characters include bidi overrides/isolates, zero-width
|
||||||
|
// characters, and BOM-like markers. They are replaced visibly while ordinary
|
||||||
|
// international letters, accents, and scripts remain unchanged.
|
||||||
|
static BOOL wq_is_name_format_scalar(uint32_t c)
|
||||||
|
{
|
||||||
|
return c == 0x00ad || (c >= 0x0600 && c <= 0x0605) || c == 0x061c || c == 0x06dd ||
|
||||||
|
c == 0x070f || (c >= 0x0890 && c <= 0x0891) || c == 0x08e2 || c == 0x180e ||
|
||||||
|
(c >= 0x200b && c <= 0x200f) || (c >= 0x202a && c <= 0x202e) ||
|
||||||
|
(c >= 0x2060 && c <= 0x206f) || c == 0xfeff;
|
||||||
|
}
|
||||||
|
|
||||||
|
static NSString *wq_safe_name(NSString *value)
|
||||||
|
{
|
||||||
|
if (!value.length) return nil;
|
||||||
|
NSMutableString *safe = [NSMutableString stringWithCapacity:value.length];
|
||||||
|
for (NSUInteger i = 0; i < value.length;) {
|
||||||
|
uint32_t scalar = [value characterAtIndex:i++];
|
||||||
|
if (scalar >= 0xd800 && scalar <= 0xdbff) {
|
||||||
|
if (i >= value.length) return nil;
|
||||||
|
unichar low = [value characterAtIndex:i++];
|
||||||
|
if (low < 0xdc00 || low > 0xdfff) return nil;
|
||||||
|
scalar = 0x10000u + ((scalar - 0xd800u) << 10) + (low - 0xdc00u);
|
||||||
|
} else if (scalar >= 0xdc00 && scalar <= 0xdfff) {
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wq_is_name_format_scalar(scalar) || scalar == 0x2028 || scalar == 0x2029) {
|
||||||
|
[safe appendString:@"\uFFFD"];
|
||||||
|
} else if (scalar <= 0xffff) {
|
||||||
|
[safe appendFormat:@"%C", (unichar)scalar];
|
||||||
|
} else {
|
||||||
|
scalar -= 0x10000u;
|
||||||
|
[safe appendFormat:@"%C%C", (unichar)(0xd800u + (scalar >> 10)),
|
||||||
|
(unichar)(0xdc00u + (scalar & 0x3ffu))];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NSString *normalized = [safe precomposedStringWithCanonicalMapping];
|
||||||
|
normalized = [normalized stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet];
|
||||||
|
if (!normalized.length || [normalized hasPrefix:@"/"] || [normalized containsString:@".app/"]) return nil;
|
||||||
|
if ([normalized rangeOfCharacterFromSet:NSCharacterSet.controlCharacterSet].location != NSNotFound) return nil;
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Data container path -> bundle id. Apps: the Bundle record's +0x68 field;
|
||||||
|
// extensions: 0x118-byte plugin records with the bundle id at +0x00 and the
|
||||||
|
// path at +0x54. On iOS 27.2 the containers' identifier attribute can't be
|
||||||
|
// read, so this is how their folders get a bundle id (and a name).
|
||||||
|
static BOOL wq_is_data_container_path(NSString *path)
|
||||||
|
{
|
||||||
|
static NSArray<NSString *> *prefixes;
|
||||||
|
static dispatch_once_t once;
|
||||||
|
dispatch_once(&once, ^{
|
||||||
|
prefixes = @[@"/private/var/mobile/Containers/Data/Application/",
|
||||||
|
@"/private/var/mobile/Containers/Data/PluginKitPlugin/"];
|
||||||
|
});
|
||||||
|
for (NSString *prefix in prefixes) {
|
||||||
|
if (![path hasPrefix:prefix] || path.length != prefix.length + 36) continue;
|
||||||
|
return [[NSUUID alloc] initWithUUIDString:[path substringFromIndex:prefix.length]] != nil;
|
||||||
|
}
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Diagnostics: how many container paths the store holds, and which record
|
||||||
|
// sizes and offsets refer to them. Only sizes and offsets are logged (no
|
||||||
|
// names), so a Log from a release with a new layout shows where it moved.
|
||||||
|
static int wq_u32_cmp(const void *a, const void *b)
|
||||||
|
{
|
||||||
|
uint32_t x = *(const uint32_t *)a, y = *(const uint32_t *)b;
|
||||||
|
return x < y ? -1 : x > y;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void wq_ls_log_summary(const uint8_t *b, size_t n, NSString *layout, NSUInteger bundleRecords,
|
||||||
|
NSUInteger pluginRecords, NSUInteger names, NSUInteger mapped)
|
||||||
|
{
|
||||||
|
NSMutableData *refData = [NSMutableData new];
|
||||||
|
static const char needle[] = "/var/mobile/Containers/Data/";
|
||||||
|
const size_t needleLen = sizeof(needle) - 1;
|
||||||
|
const uint8_t *cursor = b, *end = b + n;
|
||||||
|
while (cursor < end) {
|
||||||
|
const uint8_t *hit = memmem(cursor, (size_t)(end - cursor), needle, needleLen);
|
||||||
|
if (!hit) break;
|
||||||
|
cursor = hit + needleLen;
|
||||||
|
const uint8_t *start = hit;
|
||||||
|
if ((size_t)(hit - b) >= 8 && memcmp(hit - 8, "/private", 8) == 0) start = hit - 8;
|
||||||
|
size_t o = (size_t)(start - b);
|
||||||
|
if (o < 8 || o % 4) continue;
|
||||||
|
uint32_t len = wq_rd32(start - 4);
|
||||||
|
if (len < needleLen || len > 0x100 || len > (size_t)(end - start)) continue;
|
||||||
|
uint32_t ref = wq_rd32(start - 8) << 2;
|
||||||
|
if (ref) [refData appendBytes:&ref length:sizeof(ref)];
|
||||||
|
if (refData.length / sizeof(uint32_t) >= 20000) break;
|
||||||
|
}
|
||||||
|
NSUInteger pathCount = refData.length / sizeof(uint32_t);
|
||||||
|
uint32_t *refs = refData.mutableBytes;
|
||||||
|
if (pathCount) qsort(refs, pathCount, sizeof(uint32_t), wq_u32_cmp);
|
||||||
|
WQLSLog(@"names: store %.1f MB, layout %@: %lu bundle and %lu plugin records, %lu names, %lu of %lu container paths mapped",
|
||||||
|
n / 1048576.0, layout, (unsigned long)bundleRecords, (unsigned long)pluginRecords, (unsigned long)names,
|
||||||
|
(unsigned long)mapped, (unsigned long)pathCount);
|
||||||
|
if (!pathCount || mapped * 2 >= pathCount) return;
|
||||||
|
|
||||||
|
// Vote for (record size, offset of the container reference).
|
||||||
|
NSCountedSet<NSNumber *> *votes = [NSCountedSet new];
|
||||||
|
for (size_t q = 0; q + 4 <= n; q += 4) {
|
||||||
|
uint32_t v = wq_rd32(b + q);
|
||||||
|
if (!v || (v & 3) || !bsearch(&v, refs, pathCount, sizeof(uint32_t), wq_u32_cmp)) continue;
|
||||||
|
for (size_t k = 0; k < 0x400 && q >= 8 + k; k += 4) {
|
||||||
|
uint32_t size = wq_rd32(b + q - 8 - k + 4);
|
||||||
|
if (size >= 0x40 && size <= 0x800 && size % 4 == 0 && k < size)
|
||||||
|
[votes addObject:@((uint64_t)size << 16 | k)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NSArray *top = [votes.allObjects sortedArrayUsingComparator:^NSComparisonResult(NSNumber *x, NSNumber *y) {
|
||||||
|
NSUInteger cx = [votes countForObject:x], cy = [votes countForObject:y];
|
||||||
|
return cx > cy ? NSOrderedAscending : cx < cy ? NSOrderedDescending : [x compare:y];
|
||||||
|
}];
|
||||||
|
NSMutableArray *parts = [NSMutableArray new];
|
||||||
|
for (NSNumber *key in [top subarrayWithRange:NSMakeRange(0, MIN(top.count, (NSUInteger)8))]) {
|
||||||
|
uint64_t value = key.unsignedLongLongValue;
|
||||||
|
[parts addObject:[NSString stringWithFormat:@"0x%llx@0x%llx x%lu", value >> 16, value & 0xffff,
|
||||||
|
(unsigned long)[votes countForObject:key]]];
|
||||||
|
}
|
||||||
|
WQLSLog(@"names: layout hint (record size@container offset): %@", [parts componentsJoinedByString:@", "]);
|
||||||
|
}
|
||||||
|
|
||||||
|
static BOOL wq_parse_ls_store(NSData *data, NSDictionary **displayOut,
|
||||||
|
NSDictionary **bundleNameOut, NSDictionary **containersOut,
|
||||||
|
NSDictionary **bundlePathsOut)
|
||||||
|
{
|
||||||
|
if (bundlePathsOut) *bundlePathsOut = nil;
|
||||||
|
if (displayOut) *displayOut = nil;
|
||||||
|
if (bundleNameOut) *bundleNameOut = nil;
|
||||||
|
if (containersOut) *containersOut = nil;
|
||||||
|
|
||||||
|
const uint8_t *b = data.bytes;
|
||||||
|
size_t n = data.length;
|
||||||
|
if (n < kWQLSMinStoreBytes || !b || memcmp(b, "bdsl", 4) != 0) {
|
||||||
|
WQLSLog(@"names: not a LaunchServices store");
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
NSRange heapRange = [data rangeOfData:[NSData dataWithBytes:"<string>" length:8]
|
||||||
|
options:0 range:NSMakeRange(0, n)];
|
||||||
|
size_t heap = heapRange.location == NSNotFound ? 0 : heapRange.location;
|
||||||
|
|
||||||
|
// Pass 1: Bundle and plug-in records of every known layout, and which
|
||||||
|
// string ids they need. The layout that maps the most containers wins.
|
||||||
|
const uint32_t kIDs = kWQLSMaxStringID;
|
||||||
|
size_t bitsetBytes = kIDs / 8;
|
||||||
|
uint8_t *need = calloc(bitsetBytes, 1), *inHeap = calloc(bitsetBytes, 1);
|
||||||
|
NSMutableArray<NSMutableData *> *layoutRecs = [NSMutableArray new], *layoutPlugins = [NSMutableArray new];
|
||||||
|
for (size_t l = 0; l < kWQLSLayoutCount; l++) {
|
||||||
|
[layoutRecs addObject:[NSMutableData new]];
|
||||||
|
[layoutPlugins addObject:[NSMutableData new]]; // pairs: bundle id ref, container path ref
|
||||||
|
}
|
||||||
|
if (!need || !inHeap) {
|
||||||
|
free(need);
|
||||||
|
free(inHeap);
|
||||||
|
WQLSLog(@"names: LaunchServices parser allocation failed");
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOL tooManyRecords = NO;
|
||||||
|
for (size_t o = 0; o + 8 + kWQLSMaxRecordSize <= n && !tooManyRecords; o += 4) {
|
||||||
|
uint32_t size = wq_rd32(b + o + 4);
|
||||||
|
const uint8_t *u = b + o + 8;
|
||||||
|
for (size_t l = 0; l < kWQLSLayoutCount; l++) {
|
||||||
|
const WQLSLayout *L = &kWQLSLayouts[l];
|
||||||
|
if (size == L->pluginSize) {
|
||||||
|
NSMutableData *plugins = layoutPlugins[l];
|
||||||
|
uint32_t p[2] = { wq_rd32(u + L->pluginBundleID), wq_rd32(u + L->pluginContainer) };
|
||||||
|
if (p[0] && !(p[0] & 3) && (p[0] >> 2) < kIDs && p[1] && !(p[1] & 3) && (p[1] >> 2) < kIDs &&
|
||||||
|
plugins.length / sizeof(p) < kWQLSMaxBundleRecords) {
|
||||||
|
[plugins appendBytes:p length:sizeof(p)];
|
||||||
|
for (int i = 0; i < 2; i++) need[(p[i] >> 2) / 8] |= 1 << ((p[i] >> 2) % 8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (size != L->bundleSize) continue;
|
||||||
|
NSMutableData *recs = layoutRecs[l];
|
||||||
|
if (recs.length / (5 * sizeof(uint32_t)) >= kWQLSMaxBundleRecords) {
|
||||||
|
tooManyRecords = YES;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
uint32_t r[5] = { wq_rd32(u + L->bundleID), wq_rd32(u + L->name), wq_rd32(u + L->displayName), wq_rd32(u + L->container),
|
||||||
|
wq_rd32(u + L->bundlePath) };
|
||||||
|
if (!r[0] || (r[0] & 3) || (r[0] >> 2) >= kIDs) continue;
|
||||||
|
[recs appendBytes:r length:sizeof(r)];
|
||||||
|
for (int i = 0; i < 5; i++) {
|
||||||
|
if (r[i] && !(r[i] & 3) && (r[i] >> 2) < kIDs)
|
||||||
|
need[(r[i] >> 2) / 8] |= 1 << ((r[i] >> 2) % 8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (tooManyRecords) {
|
||||||
|
free(need);
|
||||||
|
free(inHeap);
|
||||||
|
WQLSLog(@"names: LaunchServices store exceeds record limit");
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass 2: the strings, preferring copies inside the string table. On
|
||||||
|
// iOS 26 container paths live in a second table whose ids overlap the
|
||||||
|
// string table's, so path-shaped strings are also kept by id on their own
|
||||||
|
// (an id with two different paths is unusable).
|
||||||
|
NSMutableDictionary<NSNumber *, NSString *> *strings = [NSMutableDictionary new];
|
||||||
|
NSMutableDictionary<NSNumber *, id> *pathStrings = [NSMutableDictionary new];
|
||||||
|
NSMutableDictionary<NSNumber *, id> *appPathStrings = [NSMutableDictionary new];
|
||||||
|
NSUInteger decodedStringBytes = 0;
|
||||||
|
BOOL tooManyStrings = NO;
|
||||||
|
for (size_t o = 0; o <= n - 8; o += 4) {
|
||||||
|
uint32_t sid = wq_rd32(b + o);
|
||||||
|
if (!sid || sid >= kIDs || !(need[sid / 8] & (1 << (sid % 8)))) continue;
|
||||||
|
BOOL here = o >= heap, had = strings[@(sid)] != nil;
|
||||||
|
BOOL keepGeneral = !had || (here && !(inHeap[sid / 8] & (1 << (sid % 8))));
|
||||||
|
uint32_t peekLen = o + 8 <= n ? wq_rd32(b + o + 4) : 0;
|
||||||
|
BOOL maybePath = peekLen >= 6 && peekLen < 0x400 && o + 8 < n && b[o + 8] == '/';
|
||||||
|
if (!keepGeneral && !maybePath) continue;
|
||||||
|
NSString *s = wq_cs_string(b, n, o);
|
||||||
|
if (!s) continue;
|
||||||
|
if (maybePath) {
|
||||||
|
NSMutableDictionary *table = [s containsString:@"/mobile/Containers/Data/"] ? pathStrings
|
||||||
|
: ([s hasSuffix:@".app"] || [s hasSuffix:@".app/"]) ? appPathStrings : nil;
|
||||||
|
id previous = table[@(sid)];
|
||||||
|
if (table && !previous) table[@(sid)] = s;
|
||||||
|
else if (table && ![previous isEqual:s]) table[@(sid)] = NSNull.null;
|
||||||
|
}
|
||||||
|
if (!keepGeneral) continue;
|
||||||
|
if (!had && (strings.count >= kWQLSMaxDecodedStrings ||
|
||||||
|
s.length > kWQLSMaxDecodedStringBytes - decodedStringBytes)) {
|
||||||
|
tooManyStrings = YES;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
strings[@(sid)] = s;
|
||||||
|
if (!had) decodedStringBytes += s.length;
|
||||||
|
if (here) inHeap[sid / 8] |= 1 << (sid % 8);
|
||||||
|
}
|
||||||
|
free(need);
|
||||||
|
free(inHeap);
|
||||||
|
if (tooManyStrings) {
|
||||||
|
WQLSLog(@"names: LaunchServices store exceeds string limit");
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
NSUInteger anyRecords = 0, anyPlugins = 0;
|
||||||
|
for (size_t l = 0; l < kWQLSLayoutCount; l++) {
|
||||||
|
anyRecords += layoutRecs[l].length / (5 * sizeof(uint32_t));
|
||||||
|
anyPlugins += layoutPlugins[l].length / (2 * sizeof(uint32_t));
|
||||||
|
}
|
||||||
|
if (!anyRecords) {
|
||||||
|
wq_ls_log_summary(b, n, @"none", 0, anyPlugins, 0, 0);
|
||||||
|
WQLSLog(@"names: LaunchServices store contains no bundle records");
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSString *(^pathFor)(uint32_t) = ^NSString *(uint32_t ref) {
|
||||||
|
id path = pathStrings[@(ref >> 2)];
|
||||||
|
if (path == NSNull.null) return nil;
|
||||||
|
return path ?: strings[@(ref >> 2)];
|
||||||
|
};
|
||||||
|
NSDictionary *display = nil, *bundleName = nil, *containers = nil, *bundlePaths = nil;
|
||||||
|
size_t chosen = 0;
|
||||||
|
for (size_t l = 0; l < kWQLSLayoutCount; l++) {
|
||||||
|
NSMutableDictionary *layoutDisplay = [NSMutableDictionary new], *layoutBundleName = [NSMutableDictionary new];
|
||||||
|
NSMutableDictionary *layoutBundlePaths = [NSMutableDictionary new];
|
||||||
|
NSMutableDictionary<NSString *, id> *layoutContainers = [NSMutableDictionary new];
|
||||||
|
// A path claimed by two different bundle ids is left out rather than guessed.
|
||||||
|
// Plug-in records only claim extension containers and bundle records
|
||||||
|
// only app containers, so a record type that happens to share a size
|
||||||
|
// can't knock out the other table's paths.
|
||||||
|
void (^claim)(NSString *, NSString *, NSString *) = ^(NSString *path, NSString *bid, NSString *kind) {
|
||||||
|
// iOS 26 stores container paths with a trailing slash; lookups
|
||||||
|
// use the /private/var form without one.
|
||||||
|
if ([path hasSuffix:@"/"]) path = [path substringToIndex:path.length - 1];
|
||||||
|
if ([path hasPrefix:@"/var/"]) path = [@"/private" stringByAppendingString:path];
|
||||||
|
if (!wq_is_data_container_path(path) || !wq_looks_like_bundle_id(bid) || ![path containsString:kind]) return;
|
||||||
|
id known = layoutContainers[path];
|
||||||
|
if (!known) layoutContainers[path] = bid;
|
||||||
|
else if (![known isEqual:bid]) layoutContainers[path] = NSNull.null;
|
||||||
|
};
|
||||||
|
NSData *plugins = layoutPlugins[l], *recs = layoutRecs[l];
|
||||||
|
const uint32_t *pr = plugins.bytes;
|
||||||
|
for (NSUInteger i = 0; i < plugins.length / sizeof(uint32_t); i += 2)
|
||||||
|
claim(pathFor(pr[i + 1]), strings[@(pr[i] >> 2)], @"/Data/PluginKitPlugin/");
|
||||||
|
const uint32_t *r = recs.bytes;
|
||||||
|
for (NSUInteger i = 0; i < recs.length / sizeof(uint32_t); i += 5) {
|
||||||
|
NSString *bid = strings[@(r[i] >> 2)];
|
||||||
|
if (!wq_looks_like_bundle_id(bid)) continue;
|
||||||
|
if (r[i + 3] && !(r[i + 3] & 3)) claim(pathFor(r[i + 3]), bid, @"/Data/Application/");
|
||||||
|
NSString *nm = (r[i + 1] & 3) ? nil : strings[@(r[i + 1] >> 2)];
|
||||||
|
NSString *dn = (r[i + 2] & 3) ? nil : strings[@(r[i + 2] >> 2)];
|
||||||
|
NSString *safeDisplay = wq_safe_name(dn);
|
||||||
|
NSString *safeBundleName = wq_safe_name(nm);
|
||||||
|
if (safeDisplay && !layoutDisplay[bid]) layoutDisplay[bid] = safeDisplay;
|
||||||
|
if (safeBundleName && !layoutBundleName[bid]) layoutBundleName[bid] = safeBundleName;
|
||||||
|
id appPath = (r[i + 4] && !(r[i + 4] & 3)) ? appPathStrings[@(r[i + 4] >> 2)] : nil;
|
||||||
|
if ([appPath isKindOfClass:NSString.class] && !layoutBundlePaths[bid]) {
|
||||||
|
NSString *clean = [appPath hasSuffix:@"/"] ? [appPath substringToIndex:[appPath length] - 1] : appPath;
|
||||||
|
if ([clean hasPrefix:@"/var/"]) clean = [@"/private" stringByAppendingString:clean];
|
||||||
|
layoutBundlePaths[bid] = clean;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (NSString *path in layoutContainers.allKeys)
|
||||||
|
if (layoutContainers[path] == NSNull.null) [layoutContainers removeObjectForKey:path];
|
||||||
|
if (!containers || layoutContainers.count > containers.count ||
|
||||||
|
(layoutContainers.count == containers.count && layoutDisplay.count > display.count)) {
|
||||||
|
display = layoutDisplay;
|
||||||
|
bundleName = layoutBundleName;
|
||||||
|
bundlePaths = layoutBundlePaths;
|
||||||
|
containers = layoutContainers;
|
||||||
|
chosen = l;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// A few Apple apps' names, to check the name fields of a new layout.
|
||||||
|
NSMutableArray *samples = [NSMutableArray new];
|
||||||
|
for (NSString *bid in @[@"com.apple.mobilesafari", @"com.apple.Preferences", @"com.apple.camera", @"com.apple.mobilenotes"]) {
|
||||||
|
NSString *name = display[bid] ?: bundleName[bid];
|
||||||
|
if (name) [samples addObject:[NSString stringWithFormat:@"%@ = %@", bid, name]];
|
||||||
|
}
|
||||||
|
// The container scan and the name loader each parse the same store; log
|
||||||
|
// its summary once per distinct result, not once per parse.
|
||||||
|
static NSString *lastSummary;
|
||||||
|
NSString *summary = [NSString stringWithFormat:@"%zu %zu %lu %lu", n, chosen, (unsigned long)display.count,
|
||||||
|
(unsigned long)containers.count];
|
||||||
|
BOOL newSummary;
|
||||||
|
@synchronized (NSObject.class) {
|
||||||
|
newSummary = ![summary isEqualToString:lastSummary];
|
||||||
|
if (newSummary) lastSummary = summary;
|
||||||
|
}
|
||||||
|
if (newSummary && samples.count) WQLSLog(@"names: sample: %@", [samples componentsJoinedByString:@", "]);
|
||||||
|
if (newSummary) wq_ls_log_summary(b, n, @(kWQLSLayouts[chosen].label), layoutRecs[chosen].length / (5 * sizeof(uint32_t)),
|
||||||
|
layoutPlugins[chosen].length / (2 * sizeof(uint32_t)), display.count, containers.count);
|
||||||
|
if (bundlePathsOut) *bundlePathsOut = [bundlePaths copy];
|
||||||
|
if (displayOut) *displayOut = [display copy];
|
||||||
|
if (bundleNameOut) *bundleNameOut = [bundleName copy];
|
||||||
|
if (containersOut) *containersOut = [containers copy];
|
||||||
|
return YES;
|
||||||
|
}
|
||||||
|
|
||||||
|
static NSDictionary *gLSDisplay;
|
||||||
|
static NSDictionary *gLSBundleName;
|
||||||
|
static NSDictionary *gLSContainers;
|
||||||
|
static NSDictionary *gLSBundlePaths;
|
||||||
|
static NSString *gLSStorePath;
|
||||||
|
static NSDictionary *gLSStoreSignature;
|
||||||
|
static BOOL gLSLoaded;
|
||||||
|
static NSUInteger gLSFailureCount;
|
||||||
|
static NSTimeInterval gLSNextRetryAt;
|
||||||
|
|
||||||
|
static void wq_ls_clear_snapshot_if_changed(BOOL changed)
|
||||||
|
{
|
||||||
|
if (!changed) return;
|
||||||
|
gLSDisplay = nil;
|
||||||
|
gLSBundleName = nil;
|
||||||
|
gLSContainers = nil;
|
||||||
|
gLSBundlePaths = nil;
|
||||||
|
gLSStorePath = nil;
|
||||||
|
gLSStoreSignature = nil;
|
||||||
|
gLSLoaded = NO;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void wq_ls_note_failure(NSTimeInterval now, NSString *reason)
|
||||||
|
{
|
||||||
|
gLSFailureCount = MIN(gLSFailureCount + 1, (NSUInteger)6);
|
||||||
|
NSUInteger shift = gLSFailureCount > 1 ? gLSFailureCount - 1 : 0;
|
||||||
|
NSTimeInterval delay = (NSTimeInterval)(1u << MIN(shift, (NSUInteger)5));
|
||||||
|
gLSNextRetryAt = now + delay;
|
||||||
|
if (reason) WQLSLog(@"names: %@; retrying in %.0f s", reason, delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void wq_load_ls_names_locked(void)
|
||||||
|
{
|
||||||
|
NSTimeInterval now = NSDate.date.timeIntervalSinceReferenceDate;
|
||||||
|
BOOL currentChanged = NO;
|
||||||
|
if (gLSLoaded && gLSStorePath) {
|
||||||
|
struct stat current;
|
||||||
|
if (wq_ls_candidate_info(gLSStorePath, ¤t)) {
|
||||||
|
NSDictionary *signature = wq_ls_signature(¤t);
|
||||||
|
currentChanged = !wq_ls_signature_equal(signature, gLSStoreSignature);
|
||||||
|
if (!currentChanged) return;
|
||||||
|
} else {
|
||||||
|
currentChanged = YES;
|
||||||
|
}
|
||||||
|
} else if (!gLSLoaded && now < gLSNextRetryAt) {
|
||||||
|
return;
|
||||||
|
} else if (!gLSLoaded && ![NSUserDefaults.standardUserDefaults stringForKey:kWQLSStoreKey].length) {
|
||||||
|
NSDate *missed = [NSUserDefaults.standardUserDefaults objectForKey:kWQLSMissKey];
|
||||||
|
if ([missed isKindOfClass:NSDate.class] && now - missed.timeIntervalSinceReferenceDate < kWQLSMissRetry &&
|
||||||
|
now >= missed.timeIntervalSinceReferenceDate) {
|
||||||
|
gLSNextRetryAt = missed.timeIntervalSinceReferenceDate + kWQLSMissRetry;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOL hadLocation = [NSUserDefaults.standardUserDefaults stringForKey:kWQLSStoreKey].length > 0;
|
||||||
|
BOOL searched = NO;
|
||||||
|
NSString *path = wq_find_ls_store(&searched);
|
||||||
|
if (!path) {
|
||||||
|
wq_ls_clear_snapshot_if_changed(currentChanged);
|
||||||
|
if (!hadLocation && searched) {
|
||||||
|
// Never found, after a complete search: wait a day (or a refresh).
|
||||||
|
[NSUserDefaults.standardUserDefaults setObject:NSDate.date forKey:kWQLSMissKey];
|
||||||
|
gLSNextRetryAt = now + kWQLSMissRetry;
|
||||||
|
WQLSLog(@"names: LaunchServices database not found; searching again in a day or on refresh");
|
||||||
|
} else {
|
||||||
|
// A known database that's briefly unreadable, or a scan that failed.
|
||||||
|
wq_ls_note_failure(now, @"LaunchServices database not found");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
[NSUserDefaults.standardUserDefaults removeObjectForKey:kWQLSMissKey];
|
||||||
|
|
||||||
|
struct stat before;
|
||||||
|
if (!wq_ls_candidate_info(path, &before)) {
|
||||||
|
wq_ls_clear_snapshot_if_changed(currentChanged);
|
||||||
|
wq_ls_note_failure(now, wq_ls_error(path, @"candidate is no longer valid"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
NSDictionary *beforeSignature = wq_ls_signature(&before);
|
||||||
|
BOOL snapshotChanged = !gLSLoaded || ![path isEqualToString:gLSStorePath] ||
|
||||||
|
!wq_ls_signature_equal(beforeSignature, gLSStoreSignature);
|
||||||
|
if (gLSLoaded && [path isEqualToString:gLSStorePath] &&
|
||||||
|
wq_ls_signature_equal(beforeSignature, gLSStoreSignature)) {
|
||||||
|
gLSFailureCount = 0;
|
||||||
|
gLSNextRetryAt = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSString *error = nil;
|
||||||
|
NSData *data = WQLSRead(path, kWQLSMaxStoreBytes + 1, &error);
|
||||||
|
if (!data || data.length > kWQLSMaxStoreBytes) {
|
||||||
|
wq_ls_clear_snapshot_if_changed(snapshotChanged);
|
||||||
|
wq_ls_note_failure(now, wq_ls_error(path, error ?: @"store exceeds size limit"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSDictionary *display = nil, *bundleName = nil, *containers = nil, *bundlePaths = nil;
|
||||||
|
if (!wq_parse_ls_store(data, &display, &bundleName, &containers, &bundlePaths)) {
|
||||||
|
wq_ls_clear_snapshot_if_changed(snapshotChanged);
|
||||||
|
wq_ls_note_failure(now, wq_ls_error(path, @"store rejected by bounded parser"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do not publish a snapshot that was changed while it was being read or parsed.
|
||||||
|
struct stat after;
|
||||||
|
if (!wq_ls_candidate_info(path, &after) || !wq_ls_stat_equal(&before, &after)) {
|
||||||
|
wq_ls_clear_snapshot_if_changed(snapshotChanged);
|
||||||
|
wq_ls_note_failure(now, wq_ls_error(path, @"store changed during read"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dictionaries are immutable copies and are assigned only after a complete,
|
||||||
|
// validated parse. Readers therefore see either the previous snapshot or this one.
|
||||||
|
gLSDisplay = [display copy] ?: @{};
|
||||||
|
gLSBundleName = [bundleName copy] ?: @{};
|
||||||
|
gLSContainers = [containers copy] ?: @{};
|
||||||
|
gLSBundlePaths = [bundlePaths copy] ?: @{};
|
||||||
|
gLSStorePath = [path copy];
|
||||||
|
gLSStoreSignature = [beforeSignature copy];
|
||||||
|
gLSLoaded = YES;
|
||||||
|
WQLSLog(@"names: store path %@", gLSStorePath);
|
||||||
|
gLSFailureCount = 0;
|
||||||
|
gLSNextRetryAt = 0;
|
||||||
|
[NSUserDefaults.standardUserDefaults setObject:gLSStorePath forKey:kWQLSStoreKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apple's own apps and services: LaunchServices refuses this app on 27.x
|
||||||
|
// (OSStatus -54) and /Applications is not listable, so names come from here.
|
||||||
|
static NSDictionary<NSString *, NSString *> *wq_builtin_names(void)
|
||||||
|
{
|
||||||
|
static NSDictionary *names;
|
||||||
|
static dispatch_once_t once;
|
||||||
|
dispatch_once(&once, ^{
|
||||||
|
names = @{
|
||||||
|
@"com.apple.AppStore": @"App Store", @"com.apple.mobilesafari": @"Safari", @"com.apple.weather": @"Weather",
|
||||||
|
@"com.apple.news": @"News", @"com.apple.stocks": @"Stocks", @"com.apple.mobilemail": @"Mail",
|
||||||
|
@"com.apple.MobileSMS": @"Messages", @"com.apple.mobilephone": @"Phone", @"com.apple.mobileslideshow": @"Photos",
|
||||||
|
@"com.apple.camera": @"Camera", @"com.apple.mobilecal": @"Calendar", @"com.apple.mobilenotes": @"Notes",
|
||||||
|
@"com.apple.reminders": @"Reminders", @"com.apple.Maps": @"Maps", @"com.apple.Music": @"Music",
|
||||||
|
@"com.apple.tv": @"TV", @"com.apple.podcasts": @"Podcasts", @"com.apple.iBooks": @"Books",
|
||||||
|
@"com.apple.Health": @"Health", @"com.apple.Passbook": @"Wallet", @"com.apple.Preferences": @"Settings",
|
||||||
|
@"com.apple.mobiletimer": @"Clock", @"com.apple.calculator": @"Calculator", @"com.apple.compass": @"Compass",
|
||||||
|
@"com.apple.VoiceMemos": @"Voice Memos", @"com.apple.facetime": @"FaceTime", @"com.apple.MobileAddressBook": @"Contacts",
|
||||||
|
@"com.apple.DocumentsApp": @"Files", @"com.apple.findmy": @"Find My", @"com.apple.Home": @"Home",
|
||||||
|
@"com.apple.shortcuts": @"Shortcuts", @"com.apple.Translate": @"Translate", @"com.apple.Fitness": @"Fitness",
|
||||||
|
@"com.apple.Bridge": @"Watch", @"com.apple.freeform": @"Freeform", @"com.apple.journal": @"Journal",
|
||||||
|
@"com.apple.Passwords": @"Passwords", @"com.apple.measure": @"Measure", @"com.apple.Magnifier": @"Magnifier",
|
||||||
|
@"com.apple.tips": @"Tips", @"com.apple.siri": @"Siri", @"com.apple.games": @"Games", @"com.apple.gamecenter": @"Game Center",
|
||||||
|
@"com.apple.Image-Playground": @"Image Playground", @"com.apple.Photos": @"Photos", @"com.apple.Keynote": @"Keynote",
|
||||||
|
@"com.apple.Pages": @"Pages", @"com.apple.Numbers": @"Numbers", @"com.apple.mobileme.fmip1": @"Find iPhone",
|
||||||
|
@"com.apple.store.Jolly": @"Apple Store", @"com.apple.supportapp": @"Apple Support", @"com.apple.Diagnostics": @"Diagnostics",
|
||||||
|
@"com.apple.PosterBoard": @"Wallpaper", @"com.apple.MercuryPoster": @"Wallpaper (Mercury)",
|
||||||
|
@"com.apple.WallpaperKit.CollectionsPoster": @"Wallpaper Collections",
|
||||||
|
@"com.apple.PhotosUIPrivate.PhotosPosterProvider": @"Photo Wallpaper",
|
||||||
|
@"com.apple.chrono.WidgetRenderer-Default": @"Widgets", @"com.apple.ScreenshotServicesService": @"Screenshots",
|
||||||
|
@"com.apple.Sharing.AirDrop": @"AirDrop", @"com.apple.AppleMediaServicesUI": @"Apple Media Services",
|
||||||
|
@"com.apple.AppleMediaServices": @"Apple Media Services", @"com.apple.ClarityCamera": @"Assistive Access Camera",
|
||||||
|
@"com.apple.ClarityPhotos": @"Assistive Access Photos", @"com.apple.SystemVoiceAssistant": @"Voice Assistant",
|
||||||
|
@"com.apple.ShazamEventsApp": @"Shazam", @"com.apple.ScreenTimeWidgetApplication": @"Screen Time",
|
||||||
|
@"com.apple.CBRemoteSetup": @"Remote Setup", @"com.apple.DemoApp": @"Demo", @"com.apple.AAUIViewService": @"Apple Account",
|
||||||
|
@"com.apple.AccountAuthenticationDialog": @"Account Sign-In", @"com.apple.StoreKitUISceneService": @"App Store Sheets",
|
||||||
|
@"com.apple.NewDeviceOutreachApp": @"New Device Setup", @"com.apple.PDUIApp": @"Printing", @"com.apple.EventViewService": @"Events",
|
||||||
|
@"com.apple.MXUIService": @"Media Picker", @"com.apple.ctkui": @"Smart Card", @"com.apple.InCallService": @"Call Screen",
|
||||||
|
@"com.apple.springboard": @"SpringBoard", @"com.apple.SafariViewService": @"Safari View", @"com.apple.mobilesms.compose": @"Message Compose",
|
||||||
|
@"com.apple.CompanionViewService": @"Companion", @"com.apple.musicrecognition": @"Music Recognition",
|
||||||
|
@"com.apple.FontInstallViewService": @"Font Install", @"com.apple.CMViewSrvc": @"Continuity Markup",
|
||||||
|
@"com.apple.AppleCashStickerPackHost": @"Apple Cash Stickers", @"com.apple.TVSetupUIService": @"Apple TV Setup",
|
||||||
|
@"com.apple.HearingApp": @"Hearing", @"com.apple.CompanionSetup": @"Companion Setup",
|
||||||
|
@"com.apple.CarPlayWallpaper": @"CarPlay Wallpaper", @"com.apple.CloudKit.ShareBear": @"iCloud Sharing",
|
||||||
|
@"com.apple.EnhancedLogging": @"Enhanced Logging", @"com.apple.tcc.AuthorizationPromptService": @"Privacy Prompts",
|
||||||
|
@"com.apple.WebContentFilter.remoteUI.WebContentAnalysisUI": @"Content Filter",
|
||||||
|
@"com.apple.SystemPaperViewService": @"Quick Note", @"com.apple.HeadphoneProxService": @"Headphones",
|
||||||
|
@"com.apple.datadetectors.DDActionsService": @"Data Detectors", @"com.apple.ServicesPaymentAngel": @"Apple Pay",
|
||||||
|
@"com.apple.HomeCaptiveViewService": @"Wi-Fi Login", @"com.apple.DiagnosticsService": @"Diagnostics",
|
||||||
|
@"com.apple.CarTirePressure": @"Tire Pressure", @"com.apple.FCAuthenticationUI": @"Authentication",
|
||||||
|
@"com.apple.icloud.FindMyDevice.FindMyExtensionContainer": @"Find My",
|
||||||
|
@"com.apple.messages.RemoteAlerts": @"Messages Alerts",
|
||||||
|
@"com.apple.AccessoryNotifications.SourceSelection": @"Accessory Source",
|
||||||
|
@"com.apple.printcenter": @"Print Center", @"com.apple.BarcodeScanner": @"Code Scanner",
|
||||||
|
@"com.apple.chrono.WidgetRenderer-Snapshots": @"Widget Snapshots",
|
||||||
|
@"com.apple.AMSEngagementViewService": @"Apple Media Services",
|
||||||
|
@"com.apple.appManagedFeatures.Restrictions": @"Restrictions", @"com.apple.CarRadio": @"Car Radio",
|
||||||
|
@"com.apple.PassbookUIService": @"Wallet Sheets", @"com.apple.Spotlight": @"Spotlight", @"com.apple.InputUI": @"Keyboard",
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
|
||||||
|
static NSString *wq_known_name_locked(NSString *bundleID)
|
||||||
|
{
|
||||||
|
wq_load_ls_names_locked();
|
||||||
|
return gLSDisplay[bundleID] ?: wq_builtin_names()[bundleID] ?: gLSBundleName[bundleID];
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOL wq_display_names_loaded(void)
|
||||||
|
{
|
||||||
|
@synchronized (wq_ls_lock()) { return gLSLoaded; }
|
||||||
|
}
|
||||||
|
|
||||||
|
void wq_load_display_names(void)
|
||||||
|
{
|
||||||
|
@synchronized (wq_ls_lock()) { wq_load_ls_names_locked(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
void wq_display_names_reset_miss(void)
|
||||||
|
{
|
||||||
|
@synchronized (wq_ls_lock()) {
|
||||||
|
[NSUserDefaults.standardUserDefaults removeObjectForKey:kWQLSMissKey];
|
||||||
|
gLSNextRetryAt = 0;
|
||||||
|
gLSFailureCount = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NSString *wq_bundle_id_for_container(NSString *path)
|
||||||
|
{
|
||||||
|
if (!path.length) return nil;
|
||||||
|
NSString *key = path.stringByStandardizingPath;
|
||||||
|
if ([key hasPrefix:@"/var/"]) key = [@"/private" stringByAppendingString:key];
|
||||||
|
@synchronized (wq_ls_lock()) {
|
||||||
|
wq_load_ls_names_locked();
|
||||||
|
NSString *bid = gLSContainers[key];
|
||||||
|
return [bid isKindOfClass:NSString.class] ? bid : nil;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NSString *wq_display_name(NSString *bundleID)
|
||||||
|
{
|
||||||
|
if (!bundleID) return nil;
|
||||||
|
@synchronized (wq_ls_lock()) {
|
||||||
|
NSString *name = wq_known_name_locked(bundleID);
|
||||||
|
if (name) return name;
|
||||||
|
// Unknown id: use the closest parent app (com.apple.news.widget -> News).
|
||||||
|
NSArray *parts = [bundleID componentsSeparatedByString:@"."];
|
||||||
|
for (NSInteger n = (NSInteger)parts.count - 1; n >= 2; n--) {
|
||||||
|
NSString *prefix = [[parts subarrayWithRange:NSMakeRange(0, (NSUInteger)n)] componentsJoinedByString:@"."];
|
||||||
|
NSString *parent = wq_known_name_locked(prefix);
|
||||||
|
if (parent) {
|
||||||
|
NSString *suffix = wq_safe_name(parts.lastObject);
|
||||||
|
return suffix.length ? [NSString stringWithFormat:@"%@ \u2013 %@", parent, suffix] : parent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NSArray<NSString *> *wq_ls_container_paths(NSString *storeHint)
|
||||||
|
{
|
||||||
|
// No lock and no scan here: the container scan calls this while a name
|
||||||
|
// load may be waiting for that scan.
|
||||||
|
NSUserDefaults *defaults = NSUserDefaults.standardUserDefaults;
|
||||||
|
NSString *path = storeHint.length ? storeHint : [defaults stringForKey:kWQLSStoreKey];
|
||||||
|
if (!path.length) return nil;
|
||||||
|
NSString *error = nil;
|
||||||
|
NSData *data = WQLSRead(path, kWQLSMaxStoreBytes + 1, &error);
|
||||||
|
if (!data || data.length > kWQLSMaxStoreBytes) {
|
||||||
|
WQLSLog(@"names: cannot read %@ (%@)", path, error ?: @"too large");
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
NSDictionary *containers = nil;
|
||||||
|
if (!wq_parse_ls_store(data, NULL, NULL, &containers, NULL) || containers.count < 2) return nil;
|
||||||
|
if (storeHint.length) {
|
||||||
|
[defaults setObject:storeHint forKey:kWQLSStoreKey];
|
||||||
|
[defaults removeObjectForKey:kWQLSMissKey];
|
||||||
|
}
|
||||||
|
// App group containers aren't mapped to an owner here; the store lists
|
||||||
|
// the group containers of installed apps, which is what the list needs.
|
||||||
|
NSMutableSet<NSString *> *all = [NSMutableSet setWithArray:containers.allKeys];
|
||||||
|
static const char needle[] = "/mobile/Containers/Shared/AppGroup/";
|
||||||
|
const uint8_t *bytes = data.bytes, *cursor = bytes, *end = bytes + data.length;
|
||||||
|
while (cursor < end) {
|
||||||
|
const uint8_t *hit = memmem(cursor, (size_t)(end - cursor), needle, sizeof(needle) - 1);
|
||||||
|
if (!hit) break;
|
||||||
|
cursor = hit + sizeof(needle) - 1;
|
||||||
|
if ((size_t)(end - cursor) < 36) break;
|
||||||
|
NSString *uuid = [[NSString alloc] initWithBytes:cursor length:36 encoding:NSASCIIStringEncoding];
|
||||||
|
if (!uuid || ![[NSUUID alloc] initWithUUIDString:uuid]) continue;
|
||||||
|
[all addObject:[@"/private/var/mobile/Containers/Shared/AppGroup/" stringByAppendingString:uuid.uppercaseString]];
|
||||||
|
}
|
||||||
|
return all.allObjects;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSString *wq_bundle_path_for_id(NSString *bundleID)
|
||||||
|
{
|
||||||
|
if (!bundleID.length) return nil;
|
||||||
|
@synchronized (wq_ls_lock()) {
|
||||||
|
wq_load_ls_names_locked();
|
||||||
|
NSString *path = gLSBundlePaths[bundleID];
|
||||||
|
return [path isKindOfClass:NSString.class] ? path : nil;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#pragma mark - Public API
|
||||||
|
|
||||||
|
BOOL WQLoadLaunchServicesDatabase(void)
|
||||||
|
{
|
||||||
|
wq_load_display_names();
|
||||||
|
return wq_display_names_loaded();
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOL WQLSDisplayNamesLoaded(void)
|
||||||
|
{
|
||||||
|
return wq_display_names_loaded();
|
||||||
|
}
|
||||||
|
|
||||||
|
void WQLSResetMiss(void)
|
||||||
|
{
|
||||||
|
wq_display_names_reset_miss();
|
||||||
|
}
|
||||||
|
|
||||||
|
NSString *WQLSDisplayName(NSString *bundleID)
|
||||||
|
{
|
||||||
|
return wq_display_name(bundleID);
|
||||||
|
}
|
||||||
|
|
||||||
|
NSString *WQLSBundleIDForContainer(NSString *path)
|
||||||
|
{
|
||||||
|
return wq_bundle_id_for_container(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
NSString *WQLSBundlePathForIdentifier(NSString *bundleID)
|
||||||
|
{
|
||||||
|
return wq_bundle_path_for_id(bundleID);
|
||||||
|
}
|
||||||
|
|
||||||
|
NSArray<NSString *> *WQLSContainerPaths(void)
|
||||||
|
{
|
||||||
|
return wq_ls_container_paths(nil);
|
||||||
|
}
|
||||||
Reference in new issue
Block a user