Archive packaging menu, auto refresh, trash favorite, test.zip/test.ipa

This commit is contained in:
admin committed 2026-11-12 14:07:57 +08:00
1 parent 718bf032c8
commit 71e5d72561
11 files changed
+954 -83

No files matched your search

+323 -80
View File
@@ -28,6 +28,7 @@ typedef int (*WQAddChdirFn)(posix_spawn_file_actions_t *, const char *);
#include "WQSSHServer.h"
#include "WQPackages.h"
#import "WQLSStore.h"
#import "WQArchive.h"
#pragma mark - Root Helper Hooks
@@ -75,12 +76,7 @@ static NSString *hook_connectionUrlWithHostname(id self, SEL _cmd) {
// so moving a file from an accessible container into this directory is a
// rename, not a cross-volume copy.
static id hook_trashDir(id self, SEL _cmd) {
NSString *root = MCMFilzaVirtualRoot();
NSString *trash = [root stringByAppendingPathComponent:@".Trash"];
[NSFileManager.defaultManager createDirectoryAtPath:trash
withIntermediateDirectories:YES
attributes:@{ NSFilePosixPermissions: @0700 } error:nil];
return trash;
return WQTrashDirectory();
}
// A jailed Filza otherwise restores its usual /var/mobile start path, which is
@@ -291,96 +287,276 @@ static void addFileToZip(zipFile64 zf, NSString *basePath, NSString *relativePat
}
}
static NSString *WQZipperPath(id value)
{
if ([value isKindOfClass:NSString.class]) return value;
if ([value isKindOfClass:NSURL.class]) return [(NSURL *)value path];
SEL selector = NSSelectorFromString(@"filePath");
if ([value respondsToSelector:selector])
return ((id(*)(id, SEL))objc_msgSend)(value, selector);
return nil;
}
static id WQArchiveItemAtPath(NSString *path)
{
Class itemClass = NSClassFromString(@"FileItem");
if (!itemClass || !path.length) return nil;
id item = [[itemClass alloc] init];
if ([item respondsToSelector:NSSelectorFromString(@"setFilePath:attribute:")])
((void(*)(id, SEL, id, id))objc_msgSend)(item,
NSSelectorFromString(@"setFilePath:attribute:"), path, nil);
return item;
}
static id WQArchiveResultForDirectory(NSString *directory)
{
NSArray<NSString *> *names = [NSFileManager.defaultManager
contentsOfDirectoryAtPath:directory error:nil];
if (!names.count) {
id item = WQArchiveItemAtPath(directory);
return item ? @[item] : nil;
}
NSMutableArray *items = [NSMutableArray array];
for (NSString *name in names) {
id item = WQArchiveItemAtPath(
[directory stringByAppendingPathComponent:name]);
if (item) [items addObject:item];
}
return items.count ? items : nil;
}
static void WQCollectViewControllers(UIViewController *controller,
NSMutableArray<UIViewController *> *result)
{
if (!controller) return;
[result addObject:controller];
for (UIViewController *child in controller.childViewControllers)
WQCollectViewControllers(child, result);
if ([controller isKindOfClass:UINavigationController.class])
for (UIViewController *child in
((UINavigationController *)controller).viewControllers)
WQCollectViewControllers(child, result);
if ([controller isKindOfClass:UITabBarController.class])
for (UIViewController *child in
((UITabBarController *)controller).viewControllers)
WQCollectViewControllers(child, result);
if (controller.presentedViewController)
WQCollectViewControllers(controller.presentedViewController, result);
}
static void WQRefreshActiveBrowserOnce(void)
{
NSMutableArray<UIViewController *> *controllers = [NSMutableArray array];
for (UIWindow *window in UIApplication.sharedApplication.windows)
if (!window.isHidden) WQCollectViewControllers(window.rootViewController,
controllers);
for (UIViewController *controller in controllers) {
NSString *className = NSStringFromClass(controller.class);
BOOL browser = [className containsString:@"PageViewController"] ||
[className containsString:@"ApplicationsViewController"] ||
[className containsString:@"FileSystemViewController"];
SEL currentSelector = NSSelectorFromString(@"currentPath");
if (browser && [controller respondsToSelector:currentSelector]) {
SEL loadSelector = NSSelectorFromString(@"doLoadingPage");
if ([controller respondsToSelector:loadSelector])
((void(*)(id, SEL))objc_msgSend)(controller, loadSelector);
loadSelector = NSSelectorFromString(@"loadFilesWithSortMode:");
if ([controller respondsToSelector:loadSelector])
((void(*)(id, SEL, int))objc_msgSend)(controller, loadSelector, 0);
}
if ([controller isKindOfClass:UITableViewController.class])
[((UITableViewController *)controller).tableView reloadData];
}
}
static void WQRefreshActiveBrowser(void)
{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 300 * NSEC_PER_MSEC),
dispatch_get_main_queue(), ^{
WQRefreshActiveBrowserOnce();
});
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1200 * NSEC_PER_MSEC),
dispatch_get_main_queue(), ^{
WQRefreshActiveBrowserOnce();
});
}
typedef NS_ENUM(NSInteger, WQPackagingKind) {
WQPackagingZip,
WQPackagingDeb,
WQPackagingIpa,
};
static void WQRunPackaging(id host, NSString *path, WQPackagingKind kind)
{
if (!path.length) return;
NSString *directory = path.stringByDeletingLastPathComponent;
NSString *base = path.lastPathComponent.stringByDeletingPathExtension;
NSString *suffix = kind == WQPackagingZip ? @".zip" :
kind == WQPackagingDeb ? @".deb" : @".ipa";
NSString *destination = [directory stringByAppendingPathComponent:
[base stringByAppendingString:suffix]];
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
NSString *message = nil;
BOOL ok = NO;
if (kind == WQPackagingZip) {
ok = WQCreateZipArchive(@[path], destination, directory, &message);
} else if (kind == WQPackagingDeb) {
ok = WQCreateDebPackage(path, destination, nil, nil, nil, nil,
&message);
} else {
ok = WQCreateIpaPackage(path, destination, &message);
}
dispatch_async(dispatch_get_main_queue(), ^{
WQRefreshActiveBrowser();
UIAlertController *alert = [UIAlertController
alertControllerWithTitle:ok ? @"打包完成" : @"打包失败"
message:message ?: (ok ? destination : @"未知错误")
preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:@"OK"
style:UIAlertActionStyleDefault handler:nil]];
UIViewController *presenter = nil;
if ([host isKindOfClass:UIViewController.class])
presenter = host;
if (!presenter) {
for (UIWindow *window in UIApplication.sharedApplication.windows)
if (window.isKeyWindow && !window.isHidden) {
presenter = window.rootViewController;
break;
}
}
while (presenter.presentedViewController)
presenter = presenter.presentedViewController;
if (presenter)
[presenter presentViewController:alert animated:YES
completion:nil];
});
});
}
static id WQZipperExtract(id self, SEL _cmd, id file, id toPath,
id currentDirectory, id password, id *outMessage)
{
NSString *archive = WQZipperPath(file);
NSString *destination = WQZipperPath(toPath);
if (!destination.length && archive.length)
destination = WQDefaultExtractDestination(archive);
NSString *message = nil;
BOOL isDeb = [archive.pathExtension.lowercaseString isEqualToString:@"deb"];
BOOL ok = isDeb ? WQExtractDebPayload(archive, destination, &message)
: WQExtractArchive(archive, destination, &message);
if (outMessage) *outMessage = message;
NSLog(@"[Zipper] %@ -> %@: %@", archive, destination,
ok ? @"ok" : (message ?: @"failed"));
if (ok) WQRefreshActiveBrowser();
return ok ? WQArchiveResultForDirectory(destination) : nil;
}
// Hook: -[Zipper ZipFiles:toFilePath:currentDirectory:]
static id hook_ZipFiles(id self, SEL _cmd, id files, id toFilePath, id currentDirectory) {
@try {
loadMinizip();
if (!g_minizipLoaded) return orig_ZipFiles ? ((id(*)(id,SEL,id,id,id))orig_ZipFiles)(self, _cmd, files, toFilePath, currentDirectory) : nil;
zipFile64 zf = p_zipOpen64(((NSString *)toFilePath).UTF8String, 0); // APPEND_STATUS_CREATE=0
if (!zf) { NSLog(@"[Tweak] zipOpen64 failed"); return nil; }
for (id fi in files) {
NSString *fn = [fi performSelector:NSSelectorFromString(@"fileName")];
if (fn) addFileToZip(zf, currentDirectory, fn);
}
p_zipClose(zf, NULL);
// Return FileItem if zip was created (matching original behavior)
if ([[NSFileManager defaultManager] fileExistsAtPath:toFilePath]) {
Class FI = NSClassFromString(@"FileItem");
if (FI) {
id item = [[FI alloc] init];
((void(*)(id,SEL,id,id))objc_msgSend)(item, NSSelectorFromString(@"setFilePath:attribute:"), toFilePath, nil);
return item;
NSString *destination = WQZipperPath(toFilePath);
NSString *base = WQZipperPath(currentDirectory);
NSArray *items = [files isKindOfClass:NSArray.class]
? files : (files ? @[files] : @[]);
NSMutableArray<NSString *> *paths = [NSMutableArray array];
for (id item in items) {
NSString *path = WQZipperPath(item);
if (!path.length && [item isKindOfClass:NSDictionary.class])
path = ((NSDictionary *)item)[@"path"];
if (!path.length &&
[item respondsToSelector:NSSelectorFromString(@"fileName")]) {
NSString *name = ((id(*)(id, SEL))objc_msgSend)(item,
NSSelectorFromString(@"fileName"));
if (name.length && base.length)
path = [base stringByAppendingPathComponent:name];
}
if (path.length) [paths addObject:path];
}
if (!destination.length || !paths.count) {
NSLog(@"[Tweak] Zip: missing destination or inputs");
return nil;
}
[NSFileManager.defaultManager removeItemAtPath:destination error:nil];
NSString *message = nil;
BOOL ok = WQCreateZipArchive(paths, destination, base, &message);
if (!ok) {
NSLog(@"[Tweak] libarchive zip failed: %@", message);
return orig_ZipFiles
? ((id(*)(id,SEL,id,id,id))orig_ZipFiles)(
self, _cmd, files, toFilePath, currentDirectory)
: nil;
}
NSLog(@"[Tweak] Zip: %@", message);
WQRefreshActiveBrowser();
return WQArchiveItemAtPath(destination);
} @catch (NSException *e) {
NSLog(@"[Tweak] Zip error: %@", e);
return nil;
} @catch (NSException *e) { NSLog(@"[Tweak] Zip error: %@", e); return nil; }
}
}
// Hook: -[Zipper unZipFile:toPath:currentDirectory:outMessage:]
static id hook_unZipFile(id self, SEL _cmd, id zipPath, id toPath, id currentDir, id *outMsg) {
@try {
loadMinizip();
if (!g_minizipLoaded) return orig_unZipFile ? ((id(*)(id,SEL,id,id,id,id*))orig_unZipFile)(self, _cmd, zipPath, toPath, currentDir, outMsg) : nil;
// zipPath is a FileItem, get the actual path string
NSString *zipPathStr = zipPath;
if ([zipPath respondsToSelector:NSSelectorFromString(@"filePath")])
zipPathStr = [zipPath performSelector:NSSelectorFromString(@"filePath")];
unzFile64 uf = p_unzOpen64(((NSString *)zipPathStr).UTF8String);
if (!uf) { if (outMsg) *outMsg = @"Failed to open zip"; return nil; }
NSFileManager *fm = [NSFileManager defaultManager];
NSString *destPath = toPath;
[fm createDirectoryAtPath:destPath withIntermediateDirectories:YES attributes:nil error:nil];
char filename[512];
uint8_t buf[32768];
int ret = p_unzGoToFirstFile(uf);
while (ret == 0) {
p_unzGetCurrentFileInfo64(uf, NULL, filename, sizeof(filename), NULL, 0, NULL, 0);
NSString *name = [NSString stringWithUTF8String:filename];
NSString *fullPath = [destPath stringByAppendingPathComponent:name];
if ([name hasSuffix:@"/"]) {
[fm createDirectoryAtPath:fullPath withIntermediateDirectories:YES attributes:nil error:nil];
} else {
[fm createDirectoryAtPath:[fullPath stringByDeletingLastPathComponent]
withIntermediateDirectories:YES attributes:nil error:nil];
if (p_unzOpenCurrentFilePassword(uf, NULL) == 0) {
NSMutableData *fileData = [NSMutableData data];
int bytesRead;
while ((bytesRead = p_unzReadCurrentFile(uf, buf, sizeof(buf))) > 0)
[fileData appendBytes:buf length:bytesRead];
p_unzCloseCurrentFile(uf);
[fileData writeToFile:fullPath atomically:YES];
}
}
ret = p_unzGoToNextFile(uf);
}
p_unzClose(uf);
if (outMsg) *outMsg = @"OK";
// Return array of extracted FileItems (matching original behavior)
NSArray *contents = [fm contentsOfDirectoryAtPath:destPath error:nil];
if (contents.count > 0) {
Class FI = NSClassFromString(@"FileItem");
if (FI) {
id item = [[FI alloc] init];
((void(*)(id,SEL,id,id))objc_msgSend)(item, NSSelectorFromString(@"setFilePath:attribute:"), destPath, nil);
return @[item];
}
NSString *archive = WQZipperPath(zipPath);
NSString *destination = WQZipperPath(toPath);
if (!destination.length && archive.length)
destination = WQDefaultExtractDestination(archive);
NSString *message = nil;
BOOL ok = WQExtractArchive(archive, destination, &message);
if (!ok) {
NSLog(@"[Tweak] libarchive unzip failed: %@", message);
return orig_unZipFile
? ((id(*)(id,SEL,id,id,id,id*))orig_unZipFile)(
self, _cmd, zipPath, toPath, currentDir, outMsg)
: nil;
}
if (outMsg) *outMsg = message;
NSLog(@"[Tweak] Unzip: %@", message);
return WQArchiveResultForDirectory(destination);
} @catch (NSException *e) {
NSLog(@"[Tweak] Unzip error: %@", e);
if (outMsg) *outMsg = e.reason;
return nil;
} @catch (NSException *e) { NSLog(@"[Tweak] Unzip error: %@", e); if (outMsg) *outMsg = [e reason]; return nil; }
}
}
// Hook: -[Zipper unZipFile:toPath:currentDirectory:withPassword:outMessage:]
static id hook_unZipFilePassword(id self, SEL _cmd, id zipPath, id toPath, id currentDir, id password, id *outMsg) {
return hook_unZipFile(self, @selector(unZipFile:toPath:currentDirectory:outMessage:), zipPath, toPath, currentDir, outMsg);
static id hook_unZipFilePassword(id self, SEL _cmd, id zipPath, id toPath,
id currentDir, id password, id *outMsg) {
return WQZipperExtract(self, _cmd, zipPath, toPath, currentDir, password,
outMsg);
}
static IMP orig_un7Zip = NULL, orig_unDEB = NULL, orig_unRar = NULL;
static IMP orig_unRarPassword = NULL, orig_unBzip2 = NULL, orig_unXZip = NULL;
static IMP orig_unTar = NULL, orig_unGzip = NULL;
static id hook_un7Zip(id self, SEL _cmd, id file, id toPath, id cwd, id *out) {
return WQZipperExtract(self, _cmd, file, toPath, cwd, nil, out);
}
static id hook_unDEB(id self, SEL _cmd, id file, id toPath, id cwd, id *out) {
return WQZipperExtract(self, _cmd, file, toPath, cwd, nil, out);
}
static id hook_unRar(id self, SEL _cmd, id file, id toPath, id cwd, id *out) {
return WQZipperExtract(self, _cmd, file, toPath, cwd, nil, out);
}
static id hook_unRarPassword(id self, SEL _cmd, id file, id toPath, id cwd,
id password, id *out) {
return WQZipperExtract(self, _cmd, file, toPath, cwd, password, out);
}
static id hook_unBzip2(id self, SEL _cmd, id file, id toPath, id cwd, id *out) {
return WQZipperExtract(self, _cmd, file, toPath, cwd, nil, out);
}
static id hook_unXZip(id self, SEL _cmd, id file, id toPath, id cwd, id *out) {
return WQZipperExtract(self, _cmd, file, toPath, cwd, nil, out);
}
static id hook_unTar(id self, SEL _cmd, id file, id toPath, id cwd, id *out) {
return WQZipperExtract(self, _cmd, file, toPath, cwd, nil, out);
}
static id hook_unGzip(id self, SEL _cmd, id file, id toPath, id cwd, id *out) {
return WQZipperExtract(self, _cmd, file, toPath, cwd, nil, out);
}
#pragma mark - Apps Manager Fix
@@ -2152,6 +2328,47 @@ static void hook_installerInstall(id self, SEL _cmd, id sender)
});
}
static IMP orig_customMenuItems;
static id hook_customMenuItems(id self, SEL _cmd, id item, id sourceView,
CGRect sourceRect)
{
NSArray *original = orig_customMenuItems
? ((id(*)(id, SEL, id, id, CGRect))orig_customMenuItems)(
self, _cmd, item, sourceView, sourceRect) : @[];
NSMutableArray *items = [original isKindOfClass:NSArray.class]
? [original mutableCopy] : [NSMutableArray array];
NSString *path = WQZipperPath(item);
if (!path.length) return items;
__weak id weakSelf = self;
UIAction *zip = [UIAction actionWithTitle:@"压缩为 ZIP"
image:[UIImage systemImageNamed:@"archivebox"] identifier:nil
handler:^(__unused UIAction *action) {
WQRunPackaging(weakSelf, path, WQPackagingZip);
}];
UIAction *deb = [UIAction actionWithTitle:@"打包为 DEB"
image:[UIImage systemImageNamed:@"shippingbox"] identifier:nil
handler:^(__unused UIAction *action) {
WQRunPackaging(weakSelf, path, WQPackagingDeb);
}];
UIAction *ipa = [UIAction actionWithTitle:@"打包为 IPA"
image:[UIImage systemImageNamed:@"app.badge"] identifier:nil
handler:^(__unused UIAction *action) {
WQRunPackaging(weakSelf, path, WQPackagingIpa);
}];
BOOL isDirectory = NO;
[NSFileManager.defaultManager fileExistsAtPath:path isDirectory:&isDirectory];
if (!isDirectory)
ipa.attributes = UIMenuElementAttributesDisabled;
UIMenu *menu = [UIMenu menuWithTitle:@"压缩 / 打包"
image:[UIImage systemImageNamed:@"archivebox"] identifier:nil
options:0 children:@[zip, deb, ipa]];
[items addObject:menu];
return items;
}
#pragma mark - Hook Installation
static void installHooks(void) {
@@ -2435,6 +2652,32 @@ static void installHooks(void) {
if (m) { orig_unZipFile = method_getImplementation(m); method_setImplementation(m, (IMP)hook_unZipFile); }
m = class_getInstanceMethod(zipper, NSSelectorFromString(@"unZipFile:toPath:currentDirectory:withPassword:outMessage:"));
if (m) { orig_unZipFilePassword = method_getImplementation(m); method_setImplementation(m, (IMP)hook_unZipFilePassword); }
m = class_getInstanceMethod(zipper, NSSelectorFromString(@"un7ZipFile:toPath:currentDirectory:outMessage:"));
if (m) { orig_un7Zip = method_getImplementation(m); method_setImplementation(m, (IMP)hook_un7Zip); }
m = class_getInstanceMethod(zipper, NSSelectorFromString(@"unDEBFile:toPath:currentDirectory:outMessage:"));
if (m) { orig_unDEB = method_getImplementation(m); method_setImplementation(m, (IMP)hook_unDEB); }
m = class_getInstanceMethod(zipper, NSSelectorFromString(@"unRarFile:toPath:currentDirectory:outMessage:"));
if (m) { orig_unRar = method_getImplementation(m); method_setImplementation(m, (IMP)hook_unRar); }
m = class_getInstanceMethod(zipper, NSSelectorFromString(@"unRarFile:toPath:currentDirectory:withPassword:outMessage:"));
if (m) { orig_unRarPassword = method_getImplementation(m); method_setImplementation(m, (IMP)hook_unRarPassword); }
m = class_getInstanceMethod(zipper, NSSelectorFromString(@"unBzip2File:toPath:currentDirectory:outMessage:"));
if (m) { orig_unBzip2 = method_getImplementation(m); method_setImplementation(m, (IMP)hook_unBzip2); }
m = class_getInstanceMethod(zipper, NSSelectorFromString(@"unXZipFile:toPath:currentDirectory:outMessage:"));
if (m) { orig_unXZip = method_getImplementation(m); method_setImplementation(m, (IMP)hook_unXZip); }
m = class_getInstanceMethod(zipper, NSSelectorFromString(@"unTarFile:toPath:currentDirectory:outMessage:"));
if (m) { orig_unTar = method_getImplementation(m); method_setImplementation(m, (IMP)hook_unTar); }
m = class_getInstanceMethod(zipper, NSSelectorFromString(@"unGzipFile:toPath:currentDirectory:outMessage:"));
if (m) { orig_unGzip = method_getImplementation(m); method_setImplementation(m, (IMP)hook_unGzip); }
}
Class pageVC = NSClassFromString(@"TGPageViewController");
if (pageVC) {
Method m = class_getInstanceMethod(pageVC,
NSSelectorFromString(@"customMenuElementItemsForItem:sourceView:sourceRect:"));
if (m) {
orig_customMenuItems = method_getImplementation(m);
method_setImplementation(m, (IMP)hook_customMenuItems);
}
}
// License/integrity bypass