Files
filza27.2/WQFilzaIntegration.m
T

1593 lines
66 KiB
Objective-C

//
// WQFilzaIntegration.m
// FilzaSlop
//
// worse_query (GPL-3.0) integration for iOS 26/27, including iOS 27.2.
// The primitive needs only a provisioned App Group; it does not depend on
// the MobileHouseArrest code identity or on a kernel exploit.
//
#import "WQFilzaIntegration.h"
#import "MCMFilzaIntegration.h"
#import "worse_query.h"
#import "pathleak.h"
#import "fallback_query.h"
#import "WQLSStore.h"
#import <Foundation/Foundation.h>
#import <Security/Security.h>
#import <UIKit/UIKit.h>
#import <objc/runtime.h>
#import <objc/message.h>
#import <dlfcn.h>
#import <errno.h>
#import <fcntl.h>
#import <sys/stat.h>
#import <unistd.h>
typedef struct __SecTask *WQSecTaskRef;
extern WQSecTaskRef SecTaskCreateFromSelf(CFAllocatorRef allocator);
extern CFTypeRef SecTaskCopyValueForEntitlement(WQSecTaskRef task,
CFStringRef entitlement,
CFErrorRef *error);
static NSString *const kWQFolderName = @"worse_query";
static NSString *const kWQFavoritesMarker = @"WQv1";
static NSLock *gQueryLock;
static NSLock *gAppsLock;
static NSMutableDictionary<NSString *, NSNumber *> *gGrantHandles;
static NSMutableArray<NSString *> *gGrantedRoots;
static NSMutableDictionary<NSString *, NSString *> *gRootDisplayNames;
static NSString *gAppGroup;
static BOOL gStarted;
static BOOL gFavoritesInstalled;
static NSArray<NSDictionary<NSString *, NSString *> *> *gAppsCache;
static NSDate *gAppsCacheDate;
static NSMutableDictionary<NSString *, NSString *> *gBundleMap;
static NSMutableDictionary<NSString *, NSString *> *gDataMap;
static BOOL gBundleMapAttempted;
static NSObject *gBundleMapLock;
static dispatch_queue_t gAppsQueue;
static dispatch_queue_t gFamiliesQueue;
static NSMutableDictionary<NSString *, NSArray<NSString *> *> *gPathleakCache;
static dispatch_group_t gStartupGroup;
static BOOL gStartupActive;
static NSArray<NSString *> *gVolumePaths;
static dispatch_once_t gVolumeOnce;
static NSMutableDictionary<NSString *, NSDictionary *> *gBundleInfo;
static void WQPostStartupProgress(double progress, NSString *status);
static NSArray<NSString *> *WQVolumeChildren(NSString *root);
static NSString *WQContainerIdentifier(NSString *containerPath);
static void WQDiscoverPerContainerFamilies(void);
static NSArray<NSDictionary<NSString *, NSString *> *> *WQBuildAppList(void);
static void WQInstallFavorites(void);
#pragma mark - Paths
static NSString *WQCanonicalPath(NSString *path)
{
if (![path isKindOfClass:NSString.class] || path.length == 0) return nil;
NSString *value = [path stringByStandardizingPath];
if ([value isEqualToString:@"/var"] || [value hasPrefix:@"/var/"])
value = [@"/private" stringByAppendingString:value];
return value;
}
static BOOL WQPathExists(NSString *path)
{
struct stat st;
return path.length > 0 && lstat(path.fileSystemRepresentation, &st) == 0;
}
static NSInteger WQOSMajor(void)
{
return NSProcessInfo.processInfo.operatingSystemVersion.majorVersion;
}
static BOOL WQUsesAlternateFlags(NSString *path)
{
// iOS 26 needs the alternative query flags for app, extension, daemon
// and app-group containers. iOS 27 normally uses the standard flags.
(void)path;
return WQOSMajor() < 27;
}
/// Paths documented by forcequitOS/worse_query, ordered from broadest to most
/// specific. The iOS 27 system locations are separate from the iOS 26 ones.
static NSArray<NSDictionary<NSString *, NSString *> *> *WQCandidateRoots(void)
{
return @[
@{ @"path": @"/private/var/mobile/Containers/Data/Application",
@"name": @"App Data" },
@{ @"path": @"/private/var/mobile/Containers/Shared/AppGroup",
@"name": @"App Groups" },
@{ @"path": @"/private/var/mobile/Containers/Data/PluginKitPlugin",
@"name": @"Extension Data" },
@{ @"path": @"/private/var/mobile/Containers/Data/InternalDaemon",
@"name": @"Internal Daemon Data" },
@{ @"path": @"/private/var/containers/Data/System",
@"name": @"System Data" },
@{ @"path": @"/private/var/containers/Shared/SystemGroup",
@"name": @"System Groups" },
// iOS 26 and earlier system container locations.
@{ @"path": @"/private/var/mobile/Containers/Data/System",
@"name": @"System Data (legacy)" },
@{ @"path": @"/private/var/mobile/Containers/Shared/SystemGroup",
@"name": @"System Groups (legacy)" },
@{ @"path": @"/private/var/mobile/Containers/Data/VPNPlugin",
@"name": @"VPN Plugin Data" },
@{ @"path": @"/private/var/mobile/Containers/Data/Protected",
@"name": @"Protected Data" },
];
}
#pragma mark - App Group
static NSArray<NSString *> *WQEntitledAppGroups(void)
{
NSMutableOrderedSet<NSString *> *groups = [NSMutableOrderedSet orderedSet];
WQSecTaskRef task = SecTaskCreateFromSelf(kCFAllocatorDefault);
if (task) {
CFTypeRef value = SecTaskCopyValueForEntitlement(task,
CFSTR("com.apple.security.application-groups"), NULL);
if (value) {
if (CFGetTypeID(value) == CFArrayGetTypeID()) {
for (id group in (__bridge NSArray *)value)
if ([group isKindOfClass:NSString.class] &&
[(NSString *)group hasPrefix:@"group."])
[groups addObject:group];
}
CFRelease(value);
}
CFRelease(task);
}
id plistGroups = [NSBundle.mainBundle objectForInfoDictionaryKey:@"WQAppGroups"];
if ([plistGroups isKindOfClass:NSArray.class]) {
for (id group in (NSArray *)plistGroups)
if ([group isKindOfClass:NSString.class] && [group length] > 0)
[groups addObject:group];
}
return groups.array;
}
NSString *WQBestAppGroup(void)
{
static NSString *cached;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSFileManager *fm = NSFileManager.defaultManager;
for (NSString *group in WQEntitledAppGroups()) {
NSURL *container = [fm containerURLForSecurityApplicationGroupIdentifier:group];
if (!container) continue;
NSString *library = [container.path stringByAppendingPathComponent:@"Library"];
[fm createDirectoryAtPath:library withIntermediateDirectories:YES
attributes:nil error:nil];
cached = group;
break;
}
NSLog(@"[WQ] entitled app groups=%@ selected=%@",
WQEntitledAppGroups(), cached ?: @"(none)");
});
return cached;
}
#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)
{
if (![target hasPrefix:@"/"]) return -255;
if (!WQBestAppGroup().length) return -1;
[gQueryLock lock];
BOOL alternate = WQUsesAlternateFlags(target);
int64_t handle = WQQueryOnce(target, alternate, createIfMissing);
int64_t first = handle;
if (handle >= 0 && !createIfMissing &&
!WQGrantActuallyOpens(target)) {
worse_query_release(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];
return handle;
}
static BOOL WQGrantPath(NSString *rawPath, NSString *displayName, BOOL publish)
{
NSString *path = WQCanonicalPath(rawPath);
if (!path || !WQPathExists(path)) return NO;
if (!gGrantHandles) gGrantHandles = [NSMutableDictionary dictionary];
if (!gGrantedRoots) gGrantedRoots = [NSMutableArray array];
if (!gRootDisplayNames) gRootDisplayNames = [NSMutableDictionary dictionary];
NSNumber *existing = gGrantHandles[path];
if (existing && existing.longLongValue >= 0) {
if (publish && ![gGrantedRoots containsObject:path]) {
[gGrantedRoots addObject:path];
gRootDisplayNames[path] = displayName ?: path.lastPathComponent;
}
return YES;
}
int64_t handle = WQAcquireExtension(path, NO);
if (handle < 0) return NO;
gGrantHandles[path] = @(handle);
if (publish) {
if (![gGrantedRoots containsObject:path]) [gGrantedRoots addObject:path];
gRootDisplayNames[path] = displayName ?: path.lastPathComponent;
}
int descriptor = open(path.fileSystemRepresentation,
O_RDONLY | O_DIRECTORY | O_CLOEXEC);
int openErrno = descriptor < 0 ? errno : 0;
if (descriptor >= 0) close(descriptor);
NSError *listError = nil;
NSArray *entries = [NSFileManager.defaultManager
contentsOfDirectoryAtPath:path error:&listError];
if (publish) {
NSLog(@"[WQ] granted %@ handle=%lld open=%d errno=%d entries=%lu listErr=%@",
path, (long long)handle, descriptor >= 0, openErrno,
(unsigned long)entries.count, listError.localizedDescription ?: @"none");
}
return YES;
}
static BOOL WQGrantRoot(NSString *rawPath, NSString *displayName)
{
return WQGrantPath(rawPath, displayName, YES);
}
static BOOL WQGrantContainer(NSString *rawPath)
{
return WQGrantPath(rawPath, rawPath.lastPathComponent, NO);
}
BOOL WQIsActive(void)
{
return gGrantHandles.count > 0;
}
NSArray<NSString *> *WQAccessibleRoots(void)
{
return gGrantedRoots ?: @[];
}
NSString *WQVirtualRootFolder(void)
{
return [MCMFilzaVirtualRoot() stringByAppendingPathComponent:kWQFolderName];
}
BOOL WQGrantExtraPath(NSString *path, BOOL createIfMissing)
{
if (!gQueryLock) gQueryLock = [NSLock new];
NSString *canonical = WQCanonicalPath(path);
if (!canonical.length) return NO;
int64_t handle = WQAcquireExtension(canonical, createIfMissing);
if (handle < 0) return NO;
if (!gGrantHandles) gGrantHandles = [NSMutableDictionary dictionary];
gGrantHandles[canonical] = @(handle);
return YES;
}
void WQReleaseGrantHandle(NSNumber *handle)
{
if (!handle) return;
worse_query_release(handle.longLongValue);
for (NSString *key in gGrantHandles.allKeys)
if ([gGrantHandles[key] isEqualToNumber:handle]) {
[gGrantHandles removeObjectForKey:key];
[gGrantedRoots removeObject:key];
}
}
void WQReleaseAllGrants(void)
{
for (NSNumber *handle in gGrantHandles.allValues)
worse_query_release(handle.longLongValue);
[gGrantHandles removeAllObjects];
[gGrantedRoots removeAllObjects];
}
NSArray<NSNumber *> *WQActiveGrantHandles(void)
{
return gGrantHandles.allValues ?: @[];
}
NSArray<NSString *> *WQChildrenForRoot(NSString *root)
{
NSString *canonical = WQCanonicalPath(root);
if (!canonical.length) return @[];
NSArray *children = WQVolumeChildren(canonical);
if (children.count) return children;
return [NSFileManager.defaultManager contentsOfDirectoryAtPath:canonical
error:nil] ?: @[];
}
NSString *WQIdentifyContainerPath(NSString *path)
{
NSString *canonical = WQCanonicalPath(path);
if (!canonical.length) return nil;
return WQContainerIdentifier(canonical);
}
void WQRefreshDiscovery(void)
{
dispatch_async(gFamiliesQueue ?: dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
WQDiscoverPerContainerFamilies();
if (!gAppsQueue) gAppsQueue = dispatch_queue_create(
"com.filzaslop.worsequery.apps", DISPATCH_QUEUE_SERIAL);
dispatch_sync(gAppsQueue, ^{
gAppsCache = WQBuildAppList();
gAppsCacheDate = [NSDate date];
});
dispatch_async(dispatch_get_main_queue(), ^{ WQInstallFavorites(); });
});
}
#pragma mark - Virtual root / links
static BOOL WQCreateLink(NSString *linkPath, NSString *target)
{
NSFileManager *fm = NSFileManager.defaultManager;
NSString *existing = [fm destinationOfSymbolicLinkAtPath:linkPath error:nil];
if (existing.length) {
if ([existing isEqualToString:target]) return YES;
[fm removeItemAtPath:linkPath error:nil];
} else if ([fm fileExistsAtPath:linkPath]) {
// Do not delete a user-created real directory.
NSLog(@"[WQ] keeping existing non-link at %@", linkPath);
return NO;
}
BOOL ok = [fm createSymbolicLinkAtPath:linkPath
withDestinationPath:target error:nil];
if (!ok) NSLog(@"[WQ] link failed %@ -> %@", linkPath, target);
return ok;
}
static NSString *WQAppDataFolder(void)
{
return [WQVirtualRootFolder() stringByAppendingPathComponent:@"App Data (by app)"];
}
static NSString *WQAppDataUUIDFolder(void)
{
return [WQVirtualRootFolder() stringByAppendingPathComponent:@"App Data (UUID)"];
}
static NSArray<NSDictionary<NSString *, NSString *> *> *WQFavoriteEntries(void)
{
NSMutableArray *entries = [NSMutableArray array];
NSString *folder = WQVirtualRootFolder();
[entries addObject:@{ @"name": @"worse_query · Accessible Roots",
@"path": folder }];
NSString *appFolder = WQAppDataFolder();
if ([NSFileManager.defaultManager fileExistsAtPath:appFolder]) {
[entries addObject:@{ @"name": @"worse_query · App Data (by app)",
@"path": appFolder }];
}
NSString *uuidFolder = WQAppDataUUIDFolder();
if ([NSFileManager.defaultManager fileExistsAtPath:uuidFolder]) {
[entries addObject:@{ @"name": @"worse_query · App Data (UUID)",
@"path": uuidFolder }];
}
for (NSString *path in gGrantedRoots ?: @[]) {
NSString *name = gRootDisplayNames[path] ?: path.lastPathComponent;
[entries addObject:@{
@"name": [NSString stringWithFormat:@"WQ · %@", name],
@"path": path,
}];
}
for (NSString *name in @[@"System Groups (containers)",
@"Extension Data (containers)",
@"Internal Daemon Data (containers)"]) {
NSString *path = [folder stringByAppendingPathComponent:name];
if ([NSFileManager.defaultManager fileExistsAtPath:path]) {
[entries addObject:@{ @"name": [NSString stringWithFormat:@"WQ · %@", name],
@"path": path }];
}
}
return entries;
}
static void WQInstallFavorites(void)
{
Class prefsClass = NSClassFromString(@"TGPreferences");
if (!prefsClass) return;
id prefs = [prefsClass respondsToSelector:NSSelectorFromString(@"sharedInstance")]
? ((id(*)(id, SEL))objc_msgSend)(prefsClass,
NSSelectorFromString(@"sharedInstance")) : nil;
if (!prefs) return;
NSArray *existing = [prefs respondsToSelector:NSSelectorFromString(@"favoritedLinks")]
? ((id(*)(id, SEL))objc_msgSend)(prefs,
NSSelectorFromString(@"favoritedLinks")) : nil;
NSMutableArray *mutableLinks = [existing isKindOfClass:NSMutableArray.class]
? (NSMutableArray *)existing : nil;
NSMutableIndexSet *stale = [NSMutableIndexSet indexSet];
NSFileManager *fm = NSFileManager.defaultManager;
if (mutableLinks) {
[mutableLinks enumerateObjectsUsingBlock:^(id item, NSUInteger idx, BOOL *stop) {
if (![item isKindOfClass:NSDictionary.class]) return;
NSString *name = item[@"name"];
NSString *path = item[@"path"];
BOOL isOurs = [name hasPrefix:@"WQ ·"] || [name hasPrefix:@"worse_query ·"];
if (!isOurs) return;
if (!path.length || ![fm fileExistsAtPath:path] ||
[path containsString:@"App Data (apps)"])
[stale addIndex:idx];
}];
if (stale.count) {
[mutableLinks removeObjectsAtIndexes:stale];
((void(*)(id, SEL))objc_msgSend)(prefs,
NSSelectorFromString(@"saveFavoritedLinks"));
NSLog(@"[WQ] removed %lu stale Favorites entries",
(unsigned long)stale.count);
}
}
NSMutableSet<NSString *> *paths = [NSMutableSet set];
for (id item in [existing isKindOfClass:NSArray.class] ? existing : @[]) {
if (![item isKindOfClass:NSDictionary.class]) continue;
NSString *path = item[@"path"];
if (path.length) [paths addObject:path];
}
NSUInteger added = 0;
for (NSDictionary *entry in WQFavoriteEntries()) {
NSString *path = entry[@"path"];
if (!path.length || [paths containsObject:path]) continue;
((void(*)(id, SEL, id))objc_msgSend)(prefs,
NSSelectorFromString(@"addItemToFavoritedLinks:"), entry);
[paths addObject:path];
added++;
}
if (added > 0) {
((void(*)(id, SEL))objc_msgSend)(prefs,
NSSelectorFromString(@"saveFavoritedLinks"));
NSLog(@"[WQ] added %lu Favorites entries (total requested=%lu)",
(unsigned long)added,
(unsigned long)WQFavoriteEntries().count);
}
gFavoritesInstalled = YES;
}
static void WQScheduleFavorites(void)
{
static int attempts;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1500 * NSEC_PER_MSEC),
dispatch_get_main_queue(), ^{
WQInstallFavorites();
if (++attempts < 12) WQScheduleFavorites();
});
}
static void WQInstallBundledTestFiles(void)
{
NSFileManager *fm = NSFileManager.defaultManager;
NSString *folder = WQVirtualRootFolder();
NSString *source = [NSBundle.mainBundle.bundlePath
stringByAppendingPathComponent:@"WQTestFiles"];
NSArray<NSString *> *names = @[@"test.deb", @"test"];
for (NSString *name in names) {
NSString *from = [source stringByAppendingPathComponent:name];
NSString *to = [folder stringByAppendingPathComponent:name];
if (![fm fileExistsAtPath:from]) continue;
NSDictionary *sourceAttributes = [fm attributesOfItemAtPath:from
error:nil];
unsigned long long sourceSize =
[sourceAttributes[NSFileSize] unsignedLongLongValue];
BOOL needsCopy = ![fm fileExistsAtPath:to];
if (!needsCopy) {
NSDictionary *attributes = [fm attributesOfItemAtPath:to error:nil];
needsCopy = [attributes[NSFileSize] unsignedLongLongValue] != sourceSize;
}
if (!needsCopy) continue;
[fm removeItemAtPath:to error:nil];
NSError *error = nil;
if (![fm copyItemAtPath:from toPath:to error:&error]) {
NSLog(@"[WQ] test file copy failed %@: %@", name,
error.localizedDescription);
continue;
}
if ([name isEqualToString:@"test"])
[fm setAttributes:@{ NSFilePosixPermissions: @0755 }
ofItemAtPath:to error:nil];
NSLog(@"[WQ] installed bundled test file %@", name);
}
}
static void WQInstallVirtualRoot(void)
{
NSFileManager *fm = NSFileManager.defaultManager;
NSString *folder = WQVirtualRootFolder();
[fm createDirectoryAtPath:folder withIntermediateDirectories:YES
attributes:@{ NSFilePosixPermissions: @0700 } error:nil];
// The application container family is enumerated per app so that hidden
// apps are still identified from their data containers. The other
// families get a per-container folder only when the root extension is
// refused by containermanagerd.
[fm createDirectoryAtPath:WQAppDataFolder() withIntermediateDirectories:YES
attributes:@{ NSFilePosixPermissions: @0700 } error:nil];
[fm createDirectoryAtPath:WQAppDataUUIDFolder() withIntermediateDirectories:YES
attributes:@{ NSFilePosixPermissions: @0700 } error:nil];
NSDictionary<NSString *, NSString *> *perContainerFolders = @{
@"/private/var/mobile/Containers/Data/Application": @"App Data (apps)",
@"/private/var/containers/Shared/SystemGroup": @"System Groups (containers)",
@"/private/var/mobile/Containers/Shared/SystemGroup": @"System Groups (containers)",
@"/private/var/mobile/Containers/Data/PluginKitPlugin": @"Extension Data (containers)",
@"/private/var/mobile/Containers/Data/InternalDaemon": @"Internal Daemon Data (containers)",
};
for (NSString *root in perContainerFolders) {
if (!WQPathExists(root)) continue;
if ([gGrantedRoots containsObject:root]) continue;
[fm createDirectoryAtPath:
[folder stringByAppendingPathComponent:perContainerFolders[root]]
withIntermediateDirectories:YES attributes:nil error:nil];
}
NSMutableSet<NSString *> *usedNames = [NSMutableSet set];
for (NSString *path in gGrantedRoots ?: @[]) {
NSString *name = gRootDisplayNames[path] ?: path.lastPathComponent;
NSString *candidate = name;
NSUInteger suffix = 2;
while ([usedNames containsObject:candidate])
candidate = [NSString stringWithFormat:@"%@ %lu", name,
(unsigned long)suffix++];
[usedNames addObject:candidate];
WQCreateLink([folder stringByAppendingPathComponent:candidate], path);
}
NSString *readme =
@"worse_query accessible roots\n"
@"===========================\n\n"
@"This folder is generated by FilzaSlop's worse_query integration.\n"
@"Each symlink points at a path family for which the app holds a\n"
@"live sandbox extension. The extension is scoped to the Filza\n"
@"process and is not a jailbreak.\n\n"
@"Supported: iOS 26.0 - 26.7.2 and iOS 27.0 - 27.2.\n"
@"Signing: a normal Apple signing identity plus one App Group is\n"
@"required. No MobileHouseArrest identity is used.\n";
[readme writeToFile:[folder stringByAppendingPathComponent:@"README.txt"]
atomically:YES encoding:NSUTF8StringEncoding error:nil];
WQInstallBundledTestFiles();
}
#pragma mark - App discovery
static BOOL WQLooksLikeBundleIdentifier(NSString *value)
{
if (value.length < 3 || value.length > 255) return NO;
if ([value hasPrefix:@"."] || [value hasSuffix:@"."]) return NO;
if ([value containsString:@".."] || [value containsString:@"/"]) return NO;
if (![value containsString:@"."]) return NO;
if ([value isEqualToString:@"com.apple"] || [value isEqualToString:@"com.apple."] ||
[value isEqualToString:@"group"] || [value isEqualToString:@"systemgroup"])
return NO;
NSCharacterSet *allowed = [NSCharacterSet characterSetWithCharactersInString:
@"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.-_"];
return [value rangeOfCharacterFromSet:allowed.invertedSet].location == NSNotFound;
}
static NSString *WQInferIdentifierFromDataDirectory(NSString *containerPath)
{
NSFileManager *fm = NSFileManager.defaultManager;
NSArray<NSString *> *subdirectories = @[
@"Library/Preferences",
@"Library/Caches",
@"Library/Saved Application State",
@"Library/Application Support",
];
NSMutableArray<NSString *> *allCandidates = [NSMutableArray array];
for (NSString *subdirectory in subdirectories) {
NSString *directory = [containerPath stringByAppendingPathComponent:subdirectory];
NSArray<NSString *> *names = [fm contentsOfDirectoryAtPath:directory error:nil];
for (NSString *name in names ?: @[]) {
NSString *candidate = name;
if ([candidate hasSuffix:@".savedState"])
candidate = [candidate stringByDeletingPathExtension];
if (candidate.pathExtension.length) candidate = candidate.stringByDeletingPathExtension;
if (![candidate containsString:@"."]) continue;
if (!WQLooksLikeBundleIdentifier(candidate)) continue;
if ([candidate hasPrefix:@"com.apple.NS"] ||
[candidate hasPrefix:@"com.apple.CF"] ||
[candidate hasPrefix:@"com.apple.GS"] ||
[candidate hasPrefix:@"com.apple.DT"] ||
[candidate hasPrefix:@"com.apple.App"]) continue;
[allCandidates addObject:candidate];
}
}
// Prefer a third-party identifier over an Apple one when both are present.
for (NSString *candidate in allCandidates)
if (![candidate hasPrefix:@"com.apple."] && ![candidate hasPrefix:@"group."])
return candidate;
return allCandidates.firstObject;
}
static NSString *WQContainerIdentifier(NSString *containerPath)
{
if (!containerPath.length) return nil;
NSString *lsIdentifier = WQLSBundleIDForContainer(containerPath);
if (lsIdentifier.length) return lsIdentifier;
NSString *metadataPath = [containerPath
stringByAppendingPathComponent:@".com.apple.mobile_container_manager.metadata.plist"];
NSFileManager *fm = NSFileManager.defaultManager;
NSError *readError = nil;
NSData *data = [NSData dataWithContentsOfFile:metadataPath
options:0 error:&readError];
if (!data) {
int64_t handle = WQAcquireExtension(metadataPath, NO);
if (handle >= 0) {
data = [NSData dataWithContentsOfFile:metadataPath];
worse_query_release(handle);
}
}
if (data) {
id plist = [NSPropertyListSerialization propertyListWithData:data
options:NSPropertyListImmutable format:NULL error:nil];
if ([plist isKindOfClass:NSDictionary.class]) {
NSString *identifier = plist[@"MCMMetadataIdentifier"];
if ([identifier isKindOfClass:NSString.class] &&
WQLooksLikeBundleIdentifier(identifier)) {
return identifier;
}
}
}
static int debugCount;
BOOL debug = debugCount < 3;
NSString *inferred = WQInferIdentifierFromDataDirectory(containerPath);
if (debug) {
debugCount++;
NSArray *rootEntries = [fm contentsOfDirectoryAtPath:containerPath error:nil];
NSArray *prefs = [fm contentsOfDirectoryAtPath:
[containerPath stringByAppendingPathComponent:@"Library/Preferences"] error:nil];
NSArray *caches = [fm contentsOfDirectoryAtPath:
[containerPath stringByAppendingPathComponent:@"Library/Caches"] error:nil];
int descriptor = open(metadataPath.fileSystemRepresentation, O_RDONLY);
int openErrno = descriptor < 0 ? errno : 0;
if (descriptor >= 0) close(descriptor);
NSLog(@"[WQ][debug] container=%@ metadataExists=%d readErr=%@ openErrno=%d "
@"root=%@ prefs=%@ caches=%@ inferred=%@", containerPath,
[fm fileExistsAtPath:metadataPath], readError.localizedDescription ?: @"none",
openErrno, rootEntries, prefs, caches, inferred ?: @"(nil)");
}
return inferred;
}
static NSString *WQSafeLinkName(NSString *name)
{
NSString *value = [name stringByReplacingOccurrencesOfString:@"/" withString:@"_"];
value = [value stringByReplacingOccurrencesOfString:@":" withString:@"_"];
if (value.length == 0 || [value hasPrefix:@"."]) value = [@"app-" stringByAppendingString:value];
return value;
}
static void WQInstallAppLinks(NSArray<NSDictionary<NSString *, NSString *> *> *apps)
{
NSString *folder = WQAppDataFolder();
NSString *uuidFolder = WQAppDataUUIDFolder();
[NSFileManager.defaultManager createDirectoryAtPath:folder
withIntermediateDirectories:YES attributes:nil error:nil];
[NSFileManager.defaultManager createDirectoryAtPath:uuidFolder
withIntermediateDirectories:YES attributes:nil error:nil];
NSUInteger linked = 0;
NSUInteger uuidLinked = 0;
for (NSDictionary *info in apps) {
NSString *path = info[@"dataPath"];
NSString *bundleID = info[@"bundleID"];
if (!path.length || !bundleID.length) continue;
NSString *name = info[@"name"];
NSString *linkName = WQSafeLinkName(name.length
? [NSString stringWithFormat:@"%@ [%@]", name, bundleID] : bundleID);
if (WQCreateLink([folder stringByAppendingPathComponent:linkName], path))
linked++;
NSString *uuid = path.lastPathComponent;
if (uuid.length &&
WQCreateLink([uuidFolder stringByAppendingPathComponent:uuid], path))
uuidLinked++;
}
NSLog(@"[WQ] app data links byApp=%lu uuid=%lu total=%lu", (unsigned long)linked,
(unsigned long)uuidLinked, (unsigned long)apps.count);
}
NSString *WQFallbackDisplayName(NSString *bundleID)
{
static NSDictionary<NSString *, NSString *> *names;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
names = @{
@"com.apple.mobilesafari": @"Safari",
@"com.apple.mobilenotes": @"Notes",
@"com.apple.Maps": @"Maps",
@"com.apple.mobilemail": @"Mail",
@"com.apple.MobileSMS": @"Messages",
@"com.apple.Photos": @"Photos",
@"com.apple.camera": @"Camera",
@"com.apple.Preferences": @"Settings",
@"com.apple.AppStore": @"App Store",
@"com.apple.Music": @"Music",
@"com.apple.podcasts": @"Podcasts",
@"com.apple.reminders": @"Reminders",
@"com.apple.weather": @"Weather",
@"com.apple.stocks": @"Stocks",
@"com.apple.news": @"News",
@"com.apple.Health": @"Health",
@"com.apple.Fitness": @"Fitness",
@"com.apple.Home": @"Home",
@"com.apple.tv": @"TV",
@"com.apple.shortcuts": @"Shortcuts",
@"com.apple.freeform": @"Freeform",
@"com.apple.DocumentsApp": @"Files",
@"com.apple.Passbook": @"Wallet",
@"com.apple.MobileAddressBook": @"Contacts",
@"com.apple.mobilecal": @"Calendar",
@"com.apple.VoiceMemos": @"Voice Memos",
@"com.apple.measure": @"Measure",
@"com.apple.compass": @"Compass",
@"com.apple.calculator": @"Calculator",
@"com.apple.tips": @"Tips",
@"com.apple.Translate": @"Translate",
@"com.apple.Bridge": @"Watch",
@"com.apple.PosterBoard": @"Wallpaper",
@"com.apple.springboard": @"SpringBoard",
@"com.apple.iBooks": @"Books",
@"com.apple.facetime": @"FaceTime",
@"com.apple.InCallService": @"Phone",
};
});
NSString *known = names[bundleID];
if (known.length) return known;
NSString *last = [bundleID componentsSeparatedByString:@"."].lastObject;
if (last.length > 1)
return [last stringByReplacingOccurrencesOfString:@"-" withString:@" "].capitalizedString;
return bundleID;
}
static NSArray<NSDictionary<NSString *, NSString *> *> *
WQAppInfoFromContainers(NSArray<NSString *> *containers)
{
NSMutableArray *result = [NSMutableArray arrayWithCapacity:containers.count];
NSMutableDictionary *dataMap = [NSMutableDictionary dictionary];
for (NSString *container in containers) {
NSString *containerPath = WQCanonicalPath(container);
if (!containerPath.length) continue;
NSString *bundleID = WQContainerIdentifier(containerPath);
if (!bundleID.length) continue;
NSMutableDictionary *info = [@{
@"bundleID": bundleID,
@"dataPath": containerPath,
@"name": bundleID,
@"version": @"1.0",
} mutableCopy];
if (!dataMap[bundleID]) dataMap[bundleID] = containerPath;
NSString *bundlePath = WQLSBundlePathForIdentifier(bundleID);
if (!bundlePath.length) bundlePath = gBundleMap[bundleID];
if (bundlePath.length) {
info[@"bundlePath"] = bundlePath;
if (!WQGrantExtraPath(bundlePath, NO))
WQGrantExtraPath(bundlePath.stringByDeletingLastPathComponent, NO);
NSDictionary *infoPlist = [NSDictionary dictionaryWithContentsOfFile:
[bundlePath stringByAppendingPathComponent:@"Info.plist"]];
NSString *name = infoPlist[@"CFBundleDisplayName"];
if (!name.length) name = infoPlist[@"CFBundleName"];
if (name.length) info[@"name"] = name;
NSString *version = infoPlist[@"CFBundleShortVersionString"];
if (!version.length) version = infoPlist[@"CFBundleVersion"];
if (version.length) info[@"version"] = version;
}
NSString *lsName = WQLSDisplayName(bundleID);
if (lsName.length) info[@"name"] = lsName;
Class proxyClass = NSClassFromString(@"LSApplicationProxy");
id proxy = [proxyClass respondsToSelector:
NSSelectorFromString(@"applicationProxyForIdentifier:")]
? ((id(*)(id, SEL, id))objc_msgSend)(proxyClass,
NSSelectorFromString(@"applicationProxyForIdentifier:"), bundleID)
: nil;
NSString *proxyName = [proxy respondsToSelector:NSSelectorFromString(@"localizedName")]
? ((id(*)(id, SEL))objc_msgSend)(proxy,
NSSelectorFromString(@"localizedName")) : nil;
if (proxyName.length &&
(![info[@"name"] isKindOfClass:NSString.class] ||
[info[@"name"] isEqualToString:bundleID]))
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")]
? ((id(*)(id, SEL))objc_msgSend)(proxy,
NSSelectorFromString(@"bundleVersion")) : nil;
if (proxyVersion.length && [info[@"version"] isEqualToString:@"1.0"])
info[@"version"] = proxyVersion;
NSURL *proxyBundle = [proxy respondsToSelector:NSSelectorFromString(@"bundleURL")]
? ((id(*)(id, SEL))objc_msgSend)(proxy,
NSSelectorFromString(@"bundleURL")) : nil;
if ([proxyBundle isKindOfClass:NSURL.class] && proxyBundle.path.length &&
!info[@"bundlePath"]) {
info[@"bundlePath"] = proxyBundle.path;
}
static int proxyDebugCount;
if (proxyDebugCount < 6) {
proxyDebugCount++;
NSLog(@"[WQ][proxy-debug] id=%@ proxy=%@ name=%@ bundle=%@",
bundleID, proxy ? @"yes" : @"nil", proxyName ?: @"nil",
proxyBundle.path ?: @"nil");
}
if ([info[@"name"] isEqualToString:bundleID])
info[@"name"] = WQFallbackDisplayName(bundleID);
[result addObject:info];
}
gDataMap = dataMap;
[result sortUsingComparator:^NSComparisonResult(NSDictionary *a, NSDictionary *b) {
return [a[@"name"] localizedStandardCompare:b[@"name"]];
}];
return result;
}
static NSArray<NSString *> *WQVolumePaths(void)
{
dispatch_once(&gVolumeOnce, ^{
NSDate *start = [NSDate date];
NSArray<NSString *> *roots = @[
@"/private/var/mobile/Containers/Data/Application",
@"/private/var/mobile/Containers/Shared/AppGroup",
@"/private/var/mobile/Containers/Data/PluginKitPlugin",
@"/private/var/mobile/Containers/Data/InternalDaemon",
@"/private/var/mobile/Containers/Data/System",
@"/private/var/mobile/Containers/Shared/SystemGroup",
@"/private/var/containers/Data/System",
@"/private/var/containers/Shared/SystemGroup",
@"/private/var/containers/Bundle/Application",
];
gVolumePaths = pathleak_roots(roots, 1, 5000000);
NSTimeInterval elapsed = -[start timeIntervalSinceNow];
NSLog(@"[WQ] volume inode scan paths=%lu elapsed=%.1fs",
(unsigned long)gVolumePaths.count, elapsed);
});
return gVolumePaths ?: @[];
}
static NSArray<NSString *> *WQVolumeChildren(NSString *root)
{
NSString *prefix = [root stringByAppendingString:@"/"];
NSMutableArray<NSString *> *children = [NSMutableArray array];
for (NSString *path in WQVolumePaths()) {
if (![path hasPrefix:prefix]) continue;
NSString *remainder = [path substringFromIndex:prefix.length];
if ([remainder containsString:@"/"]) continue;
[children addObject:path];
}
return children;
}
static NSArray<NSString *> *WQEnumerateContainers(NSString *root)
{
NSFileManager *fm = NSFileManager.defaultManager;
NSError *error = nil;
NSArray<NSString *> *names = [fm contentsOfDirectoryAtPath:root error:&error];
NSLog(@"[WQ] container root %@ direct entries=%lu error=%@",
root, (unsigned long)names.count, error.localizedDescription ?: @"none");
if (names.count > 0) {
NSMutableArray *paths = [NSMutableArray arrayWithCapacity:names.count];
for (NSString *name in names) {
if ([name hasPrefix:@"."]) continue;
[paths addObject:[root stringByAppendingPathComponent:name]];
}
if (paths.count > 0) return paths;
}
// iOS 26/27 can hide container entries from directory enumeration even
// when the path itself is visible. The shared volume scan below converts
// inode numbers to paths once and feeds every container family.
NSArray<NSString *> *volumeChildren = WQVolumeChildren(root);
if (volumeChildren.count > 0) {
NSLog(@"[WQ] volume children %@ count=%lu", root,
(unsigned long)volumeChildren.count);
return volumeChildren;
}
if (!gPathleakCache) gPathleakCache = [NSMutableDictionary dictionary];
NSArray<NSString *> *cached = gPathleakCache[root];
if (cached) {
NSLog(@"[WQ] pathleak cache %@ children=%lu", root,
(unsigned long)cached.count);
return cached;
}
uint64_t maximumInode = 5000000;
NSString *probePath = [NSTemporaryDirectory() stringByAppendingPathComponent:
[NSString stringWithFormat:@".wq-inode-%@", NSUUID.UUID.UUIDString]];
NSData *probeData = [NSData data];
if ([probeData writeToFile:probePath atomically:NO]) {
struct stat status = {0};
if (stat(probePath.fileSystemRepresentation, &status) == 0 && status.st_ino > 0) {
maximumInode = (uint64_t)status.st_ino + (uint64_t)status.st_ino / 10 + 4096;
}
[[NSFileManager defaultManager] removeItemAtPath:probePath error:nil];
}
struct stat homeStatus = {0};
if (stat(NSHomeDirectory().fileSystemRepresentation, &homeStatus) == 0 &&
homeStatus.st_ino > maximumInode)
maximumInode = (uint64_t)homeStatus.st_ino + (uint64_t)homeStatus.st_ino / 10 + 4096;
if (maximumInode > 5000000) maximumInode = 5000000;
NSLog(@"[WQ] pathleak %@ maxInode=%llu", root, (unsigned long long)maximumInode);
NSArray *leaked = pathleak(root, maximumInode);
NSLog(@"[WQ] pathleak %@ returned=%lu", root, (unsigned long)leaked.count);
NSMutableArray *paths = [NSMutableArray arrayWithCapacity:leaked.count];
for (NSString *path in leaked ?: @[]) {
if (![path isKindOfClass:NSString.class]) continue;
[paths addObject:path];
}
gPathleakCache[root] = paths.copy;
return paths;
}
static void WQDiscoverPerContainerFamilies(void)
{
NSArray<NSDictionary<NSString *, NSString *> *> *families = @[
@{ @"path": @"/private/var/containers/Shared/SystemGroup",
@"folder": @"System Groups (containers)" },
@{ @"path": @"/private/var/mobile/Containers/Shared/SystemGroup",
@"folder": @"System Groups (containers)" },
@{ @"path": @"/private/var/mobile/Containers/Data/PluginKitPlugin",
@"folder": @"Extension Data (containers)" },
@{ @"path": @"/private/var/mobile/Containers/Data/InternalDaemon",
@"folder": @"Internal Daemon Data (containers)" },
];
NSString *hub = WQVirtualRootFolder();
NSUInteger familyIndex = 0;
for (NSDictionary *family in families) {
NSString *root = WQCanonicalPath(family[@"path"]);
NSString *folderName = family[@"folder"];
if (!WQPathExists(root)) continue;
if ([gGrantedRoots containsObject:root]) continue;
familyIndex++;
WQPostStartupProgress(0.58 + 0.07 * familyIndex,
[NSString stringWithFormat:@"正在加载 %@…", folderName]);
NSArray<NSString *> *children = WQEnumerateContainers(root);
if (children.count == 0) continue;
NSString *folder = [hub stringByAppendingPathComponent:folderName];
[NSFileManager.defaultManager createDirectoryAtPath:folder
withIntermediateDirectories:YES attributes:nil error:nil];
NSUInteger granted = 0;
for (NSString *child in children) {
if (!WQGrantContainer(child)) continue;
granted++;
NSString *identifier = WQContainerIdentifier(child);
NSString *linkName = WQSafeLinkName(identifier ?: child.lastPathComponent);
WQCreateLink([folder stringByAppendingPathComponent:linkName], child);
}
NSLog(@"[WQ] per-container family %@ children=%lu granted=%lu folder=%@",
root, (unsigned long)children.count, (unsigned long)granted, folder);
}
}
static NSDictionary<NSString *, NSDictionary *> *WQMobileInstallationApps(void)
{
static NSDictionary *apps;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
void *handle = dlopen(
"/System/Library/PrivateFrameworks/MobileInstallation.framework/MobileInstallation",
RTLD_LAZY);
if (!handle) {
NSLog(@"[WQ] MobileInstallation framework unavailable: %s", dlerror());
return;
}
NSDictionary *(*lookup)(NSDictionary *, NSError **) =
(NSDictionary *(*)(NSDictionary *, NSError **))dlsym(handle,
"MobileInstallationLookup");
if (lookup) {
NSError *error = nil;
id value = lookup(nil, &error);
if ([value isKindOfClass:NSDictionary.class]) apps = [value copy];
NSLog(@"[WQ] MobileInstallationLookup apps=%lu error=%@",
(unsigned long)apps.count,
error.localizedDescription ?: @"none");
} else {
NSLog(@"[WQ] MobileInstallationLookup symbol missing");
}
dlclose(handle);
});
return apps ?: @{};
}
static void WQAppendMobileInstallationApps(NSMutableArray *result,
NSMutableSet<NSString *> *known)
{
NSDictionary<NSString *, NSDictionary *> *apps = WQMobileInstallationApps();
for (NSString *key in apps) {
NSDictionary *entry = apps[key];
if (![entry isKindOfClass:NSDictionary.class]) continue;
NSString *bundleID = entry[@"CFBundleIdentifier"];
if (![bundleID isKindOfClass:NSString.class] || !bundleID.length)
bundleID = key;
if (!bundleID.length || [known containsObject:bundleID]) continue;
id containerValue = entry[@"Container"];
NSString *dataPath = [containerValue isKindOfClass:NSURL.class]
? [(NSURL *)containerValue path] : containerValue;
if (![dataPath isKindOfClass:NSString.class] || !dataPath.length) {
NSString *error = nil;
dataPath = MCMFilzaDataContainerPath(bundleID, &error);
}
id bundleValue = entry[@"Path"];
NSString *bundlePath = [bundleValue isKindOfClass:NSURL.class]
? [(NSURL *)bundleValue path] : bundleValue;
if (![bundlePath isKindOfClass:NSString.class] || !bundlePath.length) {
bundleValue = entry[@"BundlePath"];
bundlePath = [bundleValue isKindOfClass:NSURL.class]
? [(NSURL *)bundleValue path] : bundleValue;
}
if (![bundlePath isKindOfClass:NSString.class] || !bundlePath.length)
bundlePath = WQBundlePathForIdentifier(bundleID);
NSMutableDictionary *info = [@{
@"bundleID": bundleID,
@"name": bundleID,
@"version": @"1.0",
} mutableCopy];
if (dataPath.length) info[@"dataPath"] = dataPath;
if (bundlePath.length) {
info[@"bundlePath"] = bundlePath;
NSDictionary *plist = [NSDictionary dictionaryWithContentsOfFile:
[bundlePath stringByAppendingPathComponent:@"Info.plist"]];
NSString *name = plist[@"CFBundleDisplayName"];
if (!name.length) name = plist[@"CFBundleName"];
if (name.length) info[@"name"] = name;
NSString *version = plist[@"CFBundleShortVersionString"];
if (!version.length) version = plist[@"CFBundleVersion"];
if (version.length) info[@"version"] = version;
}
NSString *name = entry[@"CFBundleDisplayName"];
if (!name.length) name = entry[@"CFBundleName"];
if ([name isKindOfClass:NSString.class] && name.length)
info[@"name"] = name;
NSString *version = entry[@"CFBundleShortVersionString"];
if (!version.length) version = entry[@"CFBundleVersion"];
if ([version isKindOfClass:NSString.class] && version.length)
info[@"version"] = version;
if ([info[@"name"] isEqualToString:bundleID])
info[@"name"] = WQFallbackDisplayName(bundleID);
[result addObject:info];
[known addObject:bundleID];
}
if (apps.count)
NSLog(@"[WQ] MobileInstallation merged=%lu total=%lu",
(unsigned long)apps.count, (unsigned long)result.count);
}
static NSArray<NSDictionary<NSString *, NSString *> *> *WQBuildAppList(void)
{
NSString *root = @"/private/var/mobile/Containers/Data/Application";
// The data-container root itself is often rejected by containermanagerd
// even though each child container is accepted. Always enumerate children
// and request their extensions individually.
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, @"正在扫描应用数据容器…");
containers = WQEnumerateContainers(root);
}
WQPostStartupProgress(0.40, [NSString stringWithFormat:
@"发现 %lu 个数据容器,正在逐个授权…", (unsigned long)containers.count]);
NSUInteger granted = 0;
for (NSString *container in containers) {
if (WQGrantContainer(container)) granted++;
}
WQPostStartupProgress(0.48, @"正在从数据目录识别应用…");
NSArray *containerApps = WQAppInfoFromContainers(containers);
NSMutableArray *apps = [containerApps mutableCopy];
NSMutableSet<NSString *> *known = [NSMutableSet set];
for (NSDictionary *info in containerApps)
if (info[@"bundleID"]) [known addObject:info[@"bundleID"]];
WQAppendMobileInstallationApps(apps, known);
WQInstallAppLinks(apps);
NSLog(@"[WQ] app discovery containers=%lu granted=%lu apps=%lu",
(unsigned long)containers.count, (unsigned long)granted,
(unsigned long)apps.count);
return apps;
}
NSArray<NSDictionary<NSString *, NSString *> *> *WQInstalledApplications(void)
{
if (!gAppsQueue) gAppsQueue = dispatch_queue_create(
"com.filzaslop.worsequery.apps", DISPATCH_QUEUE_SERIAL);
__block NSArray *value = nil;
dispatch_sync(gAppsQueue, ^{
NSTimeInterval age = gAppsCacheDate
? -[gAppsCacheDate timeIntervalSinceNow] : 1e9;
// Never cache an empty result: the first call can happen before the
// worse_query grants and app-container scan have finished.
if (!gAppsCache || gAppsCache.count == 0 || age > 120) {
gAppsCache = WQBuildAppList();
gAppsCacheDate = [NSDate date];
}
value = gAppsCache;
});
return value ?: @[];
}
NSString *WQDataPathForIdentifier(NSString *bundleID)
{
if (!bundleID.length) return nil;
WQInstalledApplications();
return gDataMap[bundleID];
}
#pragma mark - Bundle map
static void WQBuildBundleMapIfNeeded(void)
{
if (!gBundleMapLock) gBundleMapLock = [NSObject new];
@synchronized (gBundleMapLock) {
if (gBundleMapAttempted) return;
gBundleMapAttempted = YES;
gBundleMap = [NSMutableDictionary dictionary];
gBundleInfo = [NSMutableDictionary dictionary];
NSString *root = @"/private/var/containers/Bundle/Application";
NSString *prefix = [root stringByAppendingString:@"/"];
NSArray<NSString *> *volumePaths = WQVolumePaths();
BOOL bundleInVolume = NO;
for (NSString *path in volumePaths) {
if ([path hasPrefix:prefix]) { bundleInVolume = YES; break; }
}
if (!bundleInVolume && WQPathExists(root)) {
// The bundle container root can live on a different APFS volume from
// the data containers, so the shared scan above may not include it.
NSLog(@"[WQ] bundle root not in shared volume scan; scanning separately");
volumePaths = pathleak_roots(@[root], 1, 5000000);
NSLog(@"[WQ] bundle-only scan paths=%lu", (unsigned long)volumePaths.count);
}
NSUInteger appBundles = 0;
NSUInteger candidates = 0;
NSUInteger debugCount = 0;
NSUInteger prefixed = 0;
NSUInteger appSuffix = 0;
NSMutableArray *prefixedSamples = [NSMutableArray array];
for (NSString *path in volumePaths) {
if (![path hasPrefix:prefix]) continue;
prefixed++;
if ([path hasSuffix:@".app"]) appSuffix++;
if (prefixedSamples.count < 5 && [path hasSuffix:@".app"])
[prefixedSamples addObject:path];
NSString *remainder = [path substringFromIndex:prefix.length];
NSArray<NSString *> *components = [remainder componentsSeparatedByString:@"/"];
if (components.count != 2) continue;
if (![path hasSuffix:@".app"]) continue;
candidates++;
// Grant the bundle itself. Bundle containers are not in the published
// worse_query surface, but the same per-container query works on this
// iOS build and lets us read Info.plist for names and icons.
BOOL granted = WQGrantContainer(path);
BOOL parentGranted = NO;
if (!granted) {
NSString *uuidPath = [root stringByAppendingPathComponent:components[0]];
parentGranted = WQGrantContainer(uuidPath);
}
NSError *readError = nil;
NSDictionary *plist = [NSDictionary dictionaryWithContentsOfFile:
[path stringByAppendingPathComponent:@"Info.plist"]];
if (debugCount < 3) {
debugCount++;
NSLog(@"[WQ][bundle-debug] path=%@ grant=%d parentGrant=%d plist=%lu",
path, granted, parentGranted, (unsigned long)plist.count);
}
NSString *bundleID = plist[@"CFBundleIdentifier"];
if (![bundleID isKindOfClass:NSString.class] || !bundleID.length) {
// The directory name is still useful for later LaunchServices
// lookups even when Info.plist is protected.
NSString *appName = [components[1] stringByDeletingPathExtension];
if (appName.length) gBundleMap[appName] = path;
continue;
}
gBundleMap[bundleID] = path;
NSString *name = plist[@"CFBundleDisplayName"];
if (!name.length) name = plist[@"CFBundleName"];
NSString *version = plist[@"CFBundleShortVersionString"];
if (!version.length) version = plist[@"CFBundleVersion"];
gBundleInfo[bundleID] = @{
@"path": path,
@"name": name.length ? name : bundleID,
@"version": version.length ? version : @"1.0",
};
appBundles++;
}
NSLog(@"[WQ] bundle map prefixed=%lu appSuffix=%lu candidates=%lu apps=%lu samples=%@",
(unsigned long)prefixed, (unsigned long)appSuffix,
(unsigned long)candidates, (unsigned long)appBundles, prefixedSamples);
// Expose the bundle family in the hub only if at least one app bundle was
// granted, so the user can open an app's .app directory too.
if (appBundles > 0) {
NSString *folder = [WQVirtualRootFolder()
stringByAppendingPathComponent:@"App Bundles"];
[NSFileManager.defaultManager createDirectoryAtPath:folder
withIntermediateDirectories:YES attributes:nil error:nil];
NSUInteger linked = 0;
for (NSString *bundleID in gBundleMap) {
NSDictionary *info = gBundleInfo[bundleID];
NSString *path = info[@"path"] ?: gBundleMap[bundleID];
if (!path.length) continue;
NSString *linkName = WQSafeLinkName(info[@"name"]
? [NSString stringWithFormat:@"%@ [%@]", info[@"name"], bundleID]
: bundleID);
if (WQCreateLink([folder stringByAppendingPathComponent:linkName], path))
linked++;
}
NSLog(@"[WQ] app bundle links=%lu folder=%@", (unsigned long)linked, folder);
}
}
}
NSString *WQBundlePathForIdentifier(NSString *bundleID)
{
if (!bundleID.length) return nil;
NSString *lsPath = WQLSBundlePathForIdentifier(bundleID);
if (lsPath.length) return lsPath;
if (getenv("WQ_BUNDLE_SCAN")) WQBuildBundleMapIfNeeded();
NSString *mapped = gBundleMap[bundleID];
if (mapped.length) return mapped;
NSDictionary *info = gBundleInfo[bundleID];
if ([info[@"path"] isKindOfClass:NSString.class]) return info[@"path"];
NSDictionary *installation = WQMobileInstallationApps()[bundleID];
if ([installation isKindOfClass:NSDictionary.class]) {
NSString *path = installation[@"Path"];
if (![path isKindOfClass:NSString.class] || !path.length)
path = installation[@"BundlePath"];
if ([path isKindOfClass:NSString.class] && path.length) return path;
}
Class proxyClass = NSClassFromString(@"LSApplicationProxy");
id proxy = [proxyClass respondsToSelector:
NSSelectorFromString(@"applicationProxyForIdentifier:")]
? ((id(*)(id, SEL, id))objc_msgSend)(proxyClass,
NSSelectorFromString(@"applicationProxyForIdentifier:"), bundleID)
: nil;
NSURL *bundleURL = [proxy respondsToSelector:@selector(bundleURL)]
? ((id(*)(id, SEL))objc_msgSend)(proxy, @selector(bundleURL)) : nil;
if ([bundleURL isKindOfClass:NSURL.class] && bundleURL.path.length)
return bundleURL.path;
return nil;
}
#pragma mark - Name diagnostics
static void WQDiagnoseAppNames(void)
{
static BOOL ran;
if (ran) return;
ran = YES;
Class proxyClass = NSClassFromString(@"LSApplicationProxy");
unsigned int count = 0;
Method *methods = class_copyMethodList(proxyClass, &count);
NSMutableArray<NSString *> *interesting = [NSMutableArray array];
NSMutableArray<NSString *> *workspaceInteresting = [NSMutableArray array];
for (unsigned int index = 0; index < count; index++) {
NSString *name = NSStringFromSelector(method_getName(methods[index]));
NSString *lower = name.lowercaseString;
if ([lower containsString:@"name"] || [lower containsString:@"display"] ||
[lower containsString:@"title"] || [lower containsString:@"bundleurl"] ||
[lower containsString:@"container"]) {
if (method_getNumberOfArguments(methods[index]) == 2)
[interesting addObject:name];
}
}
free(methods);
NSLog(@"[WQ][names] LSApplicationProxy zero-arg methods=%@", interesting);
id proxy = [proxyClass respondsToSelector:
NSSelectorFromString(@"applicationProxyForIdentifier:")]
? ((id(*)(id, SEL, id))objc_msgSend)(proxyClass,
NSSelectorFromString(@"applicationProxyForIdentifier:"), @"com.rosiepie.flora")
: nil;
for (NSString *selectorName in interesting) {
SEL selector = NSSelectorFromString(selectorName);
if (![proxy respondsToSelector:selector]) continue;
@try {
id value = ((id(*)(id, SEL))objc_msgSend)(proxy, selector);
NSLog(@"[WQ][names] proxy %@ => %@ (%@)", selectorName,
[value isKindOfClass:NSString.class] ||
[value isKindOfClass:NSURL.class] ||
[value isKindOfClass:NSNumber.class] ? value : NSStringFromClass([value class]),
NSStringFromClass([value class]));
} @catch (NSException *exception) {
NSLog(@"[WQ][names] proxy %@ threw %@", selectorName, exception.reason);
}
}
Class workspaceClass = NSClassFromString(@"LSApplicationWorkspace");
methods = class_copyMethodList(workspaceClass, &count);
for (unsigned int index = 0; index < count; index++) {
NSString *name = NSStringFromSelector(method_getName(methods[index]));
NSString *lower = name.lowercaseString;
if (([lower containsString:@"application"] || [lower containsString:@"installed"]) &&
method_getNumberOfArguments(methods[index]) == 2)
[workspaceInteresting addObject:name];
}
free(methods);
NSLog(@"[WQ][names] LSApplicationWorkspace zero-arg methods=%@", workspaceInteresting);
void *handle = dlopen("/System/Library/PrivateFrameworks/MobileInstallation.framework/MobileInstallation", RTLD_LAZY);
if (handle) {
typedef int (*LookupFn)(CFDictionaryRef, CFDictionaryRef *);
LookupFn lookup = (LookupFn)dlsym(handle, "MobileInstallationLookup");
if (lookup) {
CFDictionaryRef info = NULL;
int result = lookup(NULL, &info);
NSDictionary *dictionary = info ? (__bridge NSDictionary *)info : nil;
NSLog(@"[WQ][names] MobileInstallationLookup result=%d apps=%lu sample=%@",
result, (unsigned long)dictionary.count,
dictionary.count ? [dictionary.allKeys.firstObject description] : @"none");
if (info) CFRelease(info);
} else {
NSLog(@"[WQ][names] MobileInstallationLookup symbol missing");
}
dlclose(handle);
} else {
NSLog(@"[WQ][names] MobileInstallation dlopen failed");
}
NSString *systemRoot = @"/private/var/containers/Data/System";
NSArray *systemEntries = [NSFileManager.defaultManager
contentsOfDirectoryAtPath:systemRoot error:nil];
NSLog(@"[WQ][names] SystemData entries=%@", systemEntries);
NSFileManager *fm = NSFileManager.defaultManager;
for (NSString *uuid in systemEntries ?: @[]) {
NSString *container = [systemRoot stringByAppendingPathComponent:uuid];
NSString *caches = [container stringByAppendingPathComponent:@"Library/Caches"];
NSArray *cacheEntries = [fm contentsOfDirectoryAtPath:caches error:nil];
if (cacheEntries.count == 0) continue;
NSLog(@"[WQ][names] system container=%@ caches=%@", uuid, cacheEntries);
for (NSString *name in cacheEntries) {
if (![name hasSuffix:@".csstore"] && ![name hasSuffix:@".db"] &&
![name hasSuffix:@".plist"] && ![name hasSuffix:@".sqlite"]) continue;
NSString *source = [caches stringByAppendingPathComponent:name];
NSDictionary *attributes = [fm attributesOfItemAtPath:source error:nil];
if ([attributes[NSFileSize] unsignedLongLongValue] > 64 * 1024 * 1024)
continue;
NSString *destination = [MCMFilzaVirtualRoot()
stringByAppendingPathComponent:
[@"store-" stringByAppendingString:name]];
BOOL copied = [fm copyItemAtPath:source toPath:destination error:nil];
NSLog(@"[WQ][names] store file %@ size=%llu copied=%d -> %@", name,
[attributes[NSFileSize] unsignedLongLongValue], copied, destination);
}
}
}
#pragma mark - Startup progress
static void WQPostStartupProgress(double progress, NSString *status)
{
if (!gStartupActive) return;
NSDictionary *info = @{ @"progress": @(progress), @"status": status ?: @"" };
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter]
postNotificationName:@"WQStartupProgress" object:nil userInfo:info];
});
}
static void WQPostStartupFinished(void)
{
gStartupActive = NO;
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter]
postNotificationName:@"WQStartupFinished" object:nil];
});
}
#pragma mark - Public entry
void WQFilzaStart(void)
{
if (gStarted) return;
gStarted = YES;
gStartupActive = YES;
gQueryLock = [NSLock new];
gAppsLock = [NSLock new];
gStartupGroup = dispatch_group_create();
WQPostStartupProgress(0.02, @"正在准备 worse_query…");
NSString *group = WQBestAppGroup();
if (!group.length) {
NSLog(@"[WQ] disabled: no provisioned App Group. Normal signing needs "
@"a profile with com.apple.security.application-groups.");
WQPostStartupProgress(0.05, @"未找到 App Group,请检查签名配置");
WQPostStartupFinished();
return;
}
NSLog(@"[WQ] starting with group=%@ os=%@", group,
NSProcessInfo.processInfo.operatingSystemVersionString);
WQPostStartupProgress(0.06, @"已找到 App Group,正在申请沙盒扩展…");
NSUInteger granted = 0;
for (NSDictionary *root in WQCandidateRoots()) {
if (WQGrantRoot(root[@"path"], root[@"name"])) granted++;
}
NSLog(@"[WQ] roots granted=%lu/%lu active=%d", (unsigned long)granted,
(unsigned long)WQCandidateRoots().count, WQIsActive());
WQPostStartupProgress(0.18, [NSString stringWithFormat:
@"根目录扩展 %lu/%lu,正在加载应用数据…",
(unsigned long)granted, (unsigned long)WQCandidateRoots().count]);
WQInstallVirtualRoot();
WQScheduleFavorites();
// Build the app-container index before the user opens Apps manager.
if (!gAppsQueue) gAppsQueue = dispatch_queue_create(
"com.filzaslop.worsequery.apps", DISPATCH_QUEUE_SERIAL);
if (!gFamiliesQueue) gFamiliesQueue = dispatch_queue_create(
"com.filzaslop.worsequery.families", DISPATCH_QUEUE_SERIAL);
// App containers are the priority: the Apps manager must populate before
// the slower system-group sweep begins.
dispatch_group_enter(gStartupGroup);
dispatch_async(gAppsQueue, ^{
WQPostStartupProgress(0.24, @"正在枚举应用数据容器…");
if (!gAppsCache) gAppsCache = WQBuildAppList();
gAppsCacheDate = [NSDate date];
if (getenv("WQ_BUNDLE_SCAN")) WQBuildBundleMapIfNeeded();
NSLog(@"[WQ] app prewarm complete apps=%lu",
(unsigned long)(gAppsCache ?: @[]).count);
WQDiagnoseAppNames();
WQPostStartupProgress(0.55, [NSString stringWithFormat:
@"已识别 %lu 个应用,正在加载系统目录…",
(unsigned long)(gAppsCache ?: @[]).count]);
dispatch_async(dispatch_get_main_queue(), ^{ WQInstallFavorites(); });
dispatch_group_leave(gStartupGroup);
});
dispatch_group_enter(gStartupGroup);
dispatch_async(gFamiliesQueue, ^{
WQPostStartupProgress(0.58, @"正在枚举系统组与扩展目录…");
WQDiscoverPerContainerFamilies();
NSLog(@"[WQ] family prewarm complete grants=%lu roots=%lu",
(unsigned long)gGrantHandles.count,
(unsigned long)WQAccessibleRoots().count);
WQPostStartupProgress(0.90, @"正在生成收藏夹与访问链接…");
dispatch_async(dispatch_get_main_queue(), ^{ WQInstallFavorites(); });
dispatch_group_leave(gStartupGroup);
});
dispatch_group_notify(gStartupGroup, dispatch_get_main_queue(), ^{
WQInstallFavorites();
WQPostStartupProgress(1.0, @"准备完成");
NSLog(@"[WQ] startup complete apps=%lu grants=%lu roots=%lu",
(unsigned long)WQInstalledApplications().count,
(unsigned long)gGrantHandles.count,
(unsigned long)WQAccessibleRoots().count);
WQPostStartupFinished();
});
if (!WQIsActive()) {
NSLog(@"[WQ] no supported root could be granted; check the App Group "
@"entitlement and the iOS version.");
}
}
#pragma mark - Proxy stand-in
@implementation WQApplicationProxy
- (instancetype)initWithAppInfo:(NSDictionary *)info
{
self = [super init];
if (!self) return nil;
NSString *bundleID = info[@"bundleID"];
_applicationIdentifier = bundleID ?: @"";
_bundleIdentifier = bundleID ?: @"";
_localizedName = info[@"name"] ?: bundleID ?: @"";
_bundleVersion = info[@"version"] ?: @"1.0";
_shortVersionString = _bundleVersion;
_applicationType = @"User";
_vendorName = bundleID ?: @"";
NSString *bundlePath = info[@"bundlePath"];
if (bundlePath.length) _bundleURL = [NSURL fileURLWithPath:bundlePath];
NSString *dataPath = info[@"dataPath"];
if (dataPath.length) _dataContainerURL = [NSURL fileURLWithPath:dataPath];
return self;
}
- (NSDictionary *)iconsDictionary { return @{}; }
- (id)iconDataForVariant:(id)variant { return nil; }
- (NSNumber *)staticDiskUsage { return @0; }
- (NSNumber *)dynamicDiskUsage { return @0; }
- (BOOL)isDeletable { return YES; }
- (BOOL)isPlaceholder { return NO; }
- (BOOL)isSystem { return NO; }
@end