From 71e5d7256123c89e4cb505de5722a0f028a497f7 Mon Sep 17 00:00:00 2001 From: admin Date: Thu, 12 Nov 2026 14:07:57 +0800 Subject: [PATCH] Archive packaging menu, auto refresh, trash favorite, test.zip/test.ipa --- .gitignore | 2 + README.md | 6 +- Tweak.m | 403 ++++++++++++++++++++++++++------- WQArchive.h | 35 +++ WQArchive.m | 480 ++++++++++++++++++++++++++++++++++++++++ WQFilzaIntegration.h | 2 + WQFilzaIntegration.m | 50 ++++- WQTerminal.m | 53 ++++- scripts/build_wq_ipa.sh | 6 + tests/test.ipa | Bin 0 -> 1034 bytes tests/test.zip | Bin 0 -> 495 bytes 11 files changed, 954 insertions(+), 83 deletions(-) create mode 100644 tests/test.ipa create mode 100644 tests/test.zip diff --git a/.gitignore b/.gitignore index 5ec7ee2..ec52be3 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,5 @@ MCMIdentifiers.plist # release staging build/ +!tests/test.ipa +!tests/test.zip diff --git a/README.md b/README.md index 5a0c7c5..15865b7 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,10 @@ filza_develop.ipa appears when the LaunchServices database cannot be read. - App icons are used where iOS returns them (`_applicationIconImageForBundle-` `Identifier:format:scale:`); otherwise Filza's placeholder is shown. +- Long-press a file or folder for a **压缩 / 打包** menu: create ZIP, DEB or + IPA. Extraction/creation refreshes the browser automatically. +- `test.zip` and `test.ipa` are copied into the worse_query folder together + with `test.deb` and `test`. - Accessible roots, App Data (by app), App Data (UUID), App Groups and System Data links saved as Filza favorites. - Startup progress overlay; first launch indexes installed apps and container @@ -55,7 +59,7 @@ filza_develop.ipa - `mount` lists mounted filesystems. - `extract`, `unzip`, `untar`, `un7z`, `unrar` extract zip, tar, tar.gz, gz, bz2, xz, zst, 7z and rar through libarchive. -- `test.deb` (official Debian `apt 2.6.1`, arm64) and an executable `test` script +- `test.deb` (official Debian `apt 2.6.1`, arm64), `test.zip`, `test.ipa`, and an executable `test` script are copied into `worse_query` on first launch. - `respring` uses Erosion's WebKit pressure method. diff --git a/Tweak.m b/Tweak.m index a41e9cb..a45267b 100644 --- a/Tweak.m +++ b/Tweak.m @@ -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 *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 *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 *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 *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 diff --git a/WQArchive.h b/WQArchive.h index f996563..498f863 100644 --- a/WQArchive.h +++ b/WQArchive.h @@ -12,4 +12,39 @@ FOUNDATION_EXPORT BOOL WQExtractArchive(NSString *archivePath, /// (archive name with the archive suffix removed). FOUNDATION_EXPORT NSString *WQDefaultExtractDestination(NSString *archivePath); +/// Creates a ZIP archive from the given file/directory paths. Paths are +/// stored relative to `baseDirectory` when possible. +FOUNDATION_EXPORT BOOL WQCreateZipArchive(NSArray *paths, + NSString *destination, + NSString *_Nullable baseDirectory, + NSString *_Nullable *_Nullable message); + +/// Creates a ZIP with every stored path prefixed (for .ipa Payload/...). +FOUNDATION_EXPORT BOOL WQCreateZipArchiveWithPrefix(NSArray *paths, + NSString *destination, + NSString *_Nullable baseDirectory, + NSString *_Nullable prefix, + NSString *_Nullable *_Nullable message); + +/// Creates a .deb from a folder (or .app). A DEBIAN/control file inside the +/// source is reused; otherwise one is generated. +FOUNDATION_EXPORT BOOL WQCreateDebPackage(NSString *source, + NSString *destination, + NSString *_Nullable packageID, + NSString *_Nullable displayName, + NSString *_Nullable version, + NSString *_Nullable description, + NSString *_Nullable *_Nullable message); + +/// Creates an .ipa from a .app folder or a folder containing Payload/. +FOUNDATION_EXPORT BOOL WQCreateIpaPackage(NSString *source, + NSString *destination, + NSString *_Nullable *_Nullable message); + +/// Extracts the data.tar payload of a .deb into destination (not the ar +/// container members). +FOUNDATION_EXPORT BOOL WQExtractDebPayload(NSString *debPath, + NSString *destination, + NSString *_Nullable *_Nullable message); + NS_ASSUME_NONNULL_END diff --git a/WQArchive.m b/WQArchive.m index b45ac89..faec63c 100644 --- a/WQArchive.m +++ b/WQArchive.m @@ -43,6 +43,24 @@ extern const char *archive_error_string(WQArchiveReader *); extern int archive_read_close(WQArchiveReader *); extern int archive_read_free(WQArchiveReader *); +typedef struct archive WQArchiveWriter; +extern WQArchiveWriter *archive_write_new(void); +extern int archive_write_set_format_zip(WQArchiveWriter *); +extern int archive_write_set_format_pax_restricted(WQArchiveWriter *); +extern int archive_write_add_filter_gzip(WQArchiveWriter *); +extern int archive_write_open_filename(WQArchiveWriter *, const char *); +extern int archive_write_header(WQArchiveWriter *, WQArchiveEntry *); +extern ssize_t archive_write_data(WQArchiveWriter *, const void *, size_t); +extern int archive_write_close(WQArchiveWriter *); +extern int archive_write_free(WQArchiveWriter *); +extern WQArchiveEntry *archive_entry_new(void); +extern void archive_entry_free(WQArchiveEntry *); +extern void archive_entry_set_pathname(WQArchiveEntry *, const char *); +extern void archive_entry_set_filetype(WQArchiveEntry *, unsigned int); +extern void archive_entry_set_perm(WQArchiveEntry *, unsigned int); +extern void archive_entry_set_size(WQArchiveEntry *, long long); +extern void archive_entry_set_symlink(WQArchiveEntry *, const char *); + static NSString *WQArchiveSafeRelativePath(const char *name) { if (!name) return nil; @@ -173,6 +191,468 @@ BOOL WQExtractArchive(NSString *archivePath, NSString *destination, return YES; } +static NSString *WQZipRelativePath(NSString *path, NSString *base) +{ + if (!path.length) return nil; + if (base.length) { + NSString *canonicalPath = path.stringByStandardizingPath; + NSString *canonicalBase = base.stringByStandardizingPath; + NSString *prefix = [canonicalBase hasSuffix:@"/"] ? canonicalBase : + [canonicalBase stringByAppendingString:@"/"]; + if ([canonicalPath hasPrefix:prefix]) + return [canonicalPath substringFromIndex:prefix.length]; + if ([canonicalPath isEqualToString:canonicalBase]) + return canonicalPath.lastPathComponent; + } + if (![path isAbsolutePath]) { + NSString *relative = path.stringByStandardizingPath; + while ([relative hasPrefix:@"./"]) + relative = [relative substringFromIndex:2]; + return relative; + } + return path.lastPathComponent; +} + +static BOOL WQZipAddPath(WQArchiveWriter *writer, NSString *path, + NSString *relative, NSString *prefix, + NSString *destination, NSString **message) +{ + if ([path isEqualToString:destination]) return YES; + struct stat st = {0}; + if (lstat(path.fileSystemRepresentation, &st) != 0) { + if (message) *message = [NSString stringWithFormat:@"zip: %@: %s", + path, strerror(errno)]; + return NO; + } + if (!relative.length || [relative hasPrefix:@"/"] || + [relative containsString:@".."]) { + if (message) *message = @"zip: unsafe archive path"; + return NO; + } + WQArchiveEntry *entry = archive_entry_new(); + if (!entry) { + if (message) *message = @"zip: cannot allocate entry"; + return NO; + } + NSString *entryPath = prefix.length + ? [prefix stringByAppendingPathComponent:relative] : relative; + if (!entryPath.length) entryPath = relative; + if (S_ISDIR(st.st_mode)) { + NSString *name = [entryPath hasSuffix:@"/"] ? entryPath : + [entryPath stringByAppendingString:@"/"]; + archive_entry_set_pathname(entry, name.UTF8String); + archive_entry_set_filetype(entry, WQ_AE_IFDIR); + archive_entry_set_perm(entry, st.st_mode & 0777); + archive_entry_set_size(entry, 0); + int result = archive_write_header(writer, entry); + archive_entry_free(entry); + if (result != WQ_ARCHIVE_OK) { + if (message) *message = @"zip: unable to write directory"; + return NO; + } + NSArray *children = [NSFileManager.defaultManager + contentsOfDirectoryAtPath:path error:nil]; + for (NSString *child in [children sortedArrayUsingSelector: + @selector(localizedStandardCompare:)]) { + NSString *childPath = [path stringByAppendingPathComponent:child]; + NSString *childRelative = [relative stringByAppendingPathComponent:child]; + if (!WQZipAddPath(writer, childPath, childRelative, prefix, + destination, message)) + return NO; + } + return YES; + } + if (S_ISLNK(st.st_mode)) { + char target[PATH_MAX]; + ssize_t length = readlink(path.fileSystemRepresentation, target, + sizeof(target) - 1); + if (length <= 0) { + archive_entry_free(entry); + if (message) *message = [NSString stringWithFormat:@"zip: %@: %s", + path, strerror(errno)]; + return NO; + } + target[length] = 0; + NSString *symlinkPath = prefix.length + ? [prefix stringByAppendingPathComponent:relative] : relative; + archive_entry_set_pathname(entry, symlinkPath.UTF8String); + archive_entry_set_filetype(entry, WQ_AE_IFLNK); + archive_entry_set_perm(entry, 0755); + archive_entry_set_symlink(entry, target); + int result = archive_write_header(writer, entry); + archive_entry_free(entry); + if (result != WQ_ARCHIVE_OK) { + if (message) *message = @"zip: unable to write symlink"; + return NO; + } + return YES; + } + if (!S_ISREG(st.st_mode)) { + archive_entry_free(entry); + return YES; + } + archive_entry_set_pathname(entry, entryPath.UTF8String); + archive_entry_set_filetype(entry, WQ_AE_IFREG); + archive_entry_set_perm(entry, st.st_mode & 0777); + archive_entry_set_size(entry, st.st_size); + int result = archive_write_header(writer, entry); + if (result != WQ_ARCHIVE_OK) { + archive_entry_free(entry); + if (message) *message = @"zip: unable to write header"; + return NO; + } + int descriptor = open(path.fileSystemRepresentation, + O_RDONLY | O_CLOEXEC); + if (descriptor < 0) { + archive_entry_free(entry); + if (message) *message = [NSString stringWithFormat:@"zip: %@: %s", + path, strerror(errno)]; + return NO; + } + char buffer[65536]; + BOOL ok = YES; + ssize_t got; + while ((got = read(descriptor, buffer, sizeof(buffer))) > 0) { + ssize_t written = 0; + while (written < got) { + ssize_t step = archive_write_data(writer, buffer + written, + (size_t)(got - written)); + if (step <= 0) { + ok = NO; + break; + } + written += step; + } + if (!ok) break; + } + if (got < 0) ok = NO; + close(descriptor); + archive_entry_free(entry); + if (!ok && message) *message = [NSString stringWithFormat:@"zip: %@: %s", + path, strerror(errno)]; + return ok; +} + +BOOL WQCreateZipArchive(NSArray *paths, NSString *destination, + NSString *baseDirectory, NSString **message) +{ + if (!paths.count || !destination.length) { + if (message) *message = @"zip: missing input or destination"; + return NO; + } + WQArchiveWriter *writer = archive_write_new(); + if (!writer) { + if (message) *message = @"zip: cannot allocate archive"; + return NO; + } + if (archive_write_set_format_zip(writer) != WQ_ARCHIVE_OK || + archive_write_open_filename(writer, + destination.fileSystemRepresentation) != WQ_ARCHIVE_OK) { + if (message) *message = @"zip: cannot open destination"; + archive_write_free(writer); + return NO; + } + BOOL ok = YES; + for (NSString *path in paths) { + NSString *relative = WQZipRelativePath(path, baseDirectory); + if (!WQZipAddPath(writer, path, relative, nil, destination, message)) { + ok = NO; + break; + } + } + archive_write_close(writer); + archive_write_free(writer); + if (ok && message) + *message = [NSString stringWithFormat:@"zip: %lu item(s) -> %@", + (unsigned long)paths.count, destination]; + return ok; +} + +static BOOL WQCreateArchiveWithPrefix(NSArray *paths, + NSString *destination, + NSString *baseDirectory, + NSString *prefix, + BOOL zip, + NSString **message) +{ + if (!paths.count || !destination.length) { + if (message) *message = @"archive: missing input or destination"; + return NO; + } + [NSFileManager.defaultManager removeItemAtPath:destination error:nil]; + WQArchiveWriter *writer = archive_write_new(); + if (!writer) { + if (message) *message = @"archive: cannot allocate writer"; + return NO; + } + int result = zip ? archive_write_set_format_zip(writer) + : archive_write_set_format_pax_restricted(writer); + if (result != WQ_ARCHIVE_OK || + (!zip && archive_write_add_filter_gzip(writer) != WQ_ARCHIVE_OK) || + archive_write_open_filename(writer, + destination.fileSystemRepresentation) != WQ_ARCHIVE_OK) { + if (message) *message = @"archive: cannot open destination"; + archive_write_free(writer); + return NO; + } + BOOL ok = YES; + for (NSString *path in paths) { + NSString *relative = WQZipRelativePath(path, baseDirectory); + if (!WQZipAddPath(writer, path, relative, prefix, destination, + message)) { + ok = NO; + break; + } + } + archive_write_close(writer); + archive_write_free(writer); + if (ok && message) + *message = [NSString stringWithFormat:@"archive: %lu item(s) -> %@", + (unsigned long)paths.count, destination]; + return ok; +} + +BOOL WQCreateZipArchiveWithPrefix(NSArray *paths, + NSString *destination, + NSString *baseDirectory, + NSString *prefix, + NSString **message) +{ + return WQCreateArchiveWithPrefix(paths, destination, baseDirectory, + prefix, YES, message); +} + +static BOOL WQCreateTarGz(NSArray *paths, NSString *destination, + NSString *baseDirectory, NSString *prefix, + NSString **message) +{ + return WQCreateArchiveWithPrefix(paths, destination, baseDirectory, + prefix, NO, message); +} + +static void WQArAppendMember(NSMutableData *archive, NSString *name, + NSData *data) +{ + char header[61] = {0}; + snprintf(header, sizeof(header), + "%-16s%-12s%-6s%-6s%-8s%-10s`\n", + name.UTF8String, "0", "0", "0", "100644", + [[NSString stringWithFormat:@"%lu", (unsigned long)data.length] + UTF8String]); + [archive appendBytes:header length:60]; + [archive appendData:data]; + if (data.length & 1) { + char padding = '\n'; + [archive appendBytes:&padding length:1]; + } +} + +static NSString *WQSafePackageIdentifier(NSString *value) +{ + NSMutableString *result = [NSMutableString string]; + for (NSUInteger i = 0; i < value.length; i++) { + unichar c = [value characterAtIndex:i]; + if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9') || c == '.' || c == '-' || c == '_') + [result appendFormat:@"%C", c]; + else + [result appendString:@"-"]; + } + NSString *trimmed = [result stringByTrimmingCharactersInSet: + [NSCharacterSet characterSetWithCharactersInString:@"-._"]]; + return trimmed.length ? trimmed.lowercaseString : @"com.filzaslop.package"; +} + +BOOL WQCreateDebPackage(NSString *source, NSString *destination, + NSString *packageID, NSString *displayName, + NSString *version, NSString *description, + NSString **message) +{ + NSFileManager *fm = NSFileManager.defaultManager; + BOOL isDirectory = NO; + if (![fm fileExistsAtPath:source isDirectory:&isDirectory]) { + if (message) *message = @"deb: source not found"; + return NO; + } + NSString *name = displayName.length ? displayName : + source.lastPathComponent.stringByDeletingPathExtension; + NSString *package = packageID.length ? packageID : + [@"com.filzaslop." stringByAppendingString: + WQSafePackageIdentifier(name)]; + NSString *controlText = nil; + NSString *existingControl = [[source stringByAppendingPathComponent:@"DEBIAN"] + stringByAppendingPathComponent:@"control"]; + if (isDirectory) + controlText = [NSString stringWithContentsOfFile:existingControl + encoding:NSUTF8StringEncoding error:nil]; + if (!controlText.length) { + controlText = [NSString stringWithFormat: + @"Package: %@\n" + @"Name: %@\n" + @"Version: %@\n" + @"Architecture: iphoneos-arm64\n" + @"Maintainer: FilzaSlop \n" + @"Section: Tweaks\n" + @"Priority: optional\n" + @"Description: %@\n", + package, name, version.length ? version : @"1.0", + description.length ? description : + [NSString stringWithFormat:@"Packaged from %@ by Filza 27.2.", + name]]; + } + + NSString *temp = [NSTemporaryDirectory() stringByAppendingPathComponent: + [NSString stringWithFormat:@"WQDeb-%@", NSUUID.UUID.UUIDString]]; + NSString *controlDir = [temp stringByAppendingPathComponent:@"control"]; + NSString *controlPath = [controlDir stringByAppendingPathComponent:@"control"]; + NSString *controlTar = [temp stringByAppendingPathComponent:@"control.tar.gz"]; + NSString *dataTar = [temp stringByAppendingPathComponent:@"data.tar.gz"]; + NSString *debianBinary = [temp stringByAppendingPathComponent:@"debian-binary"]; + [fm createDirectoryAtPath:controlDir withIntermediateDirectories:YES + attributes:nil error:nil]; + [controlText writeToFile:controlPath atomically:YES + encoding:NSUTF8StringEncoding error:nil]; + [@"2.0\n" writeToFile:debianBinary atomically:YES + encoding:NSUTF8StringEncoding error:nil]; + + NSString *error = nil; + if (!WQCreateTarGz(@[controlPath], controlTar, controlDir, nil, &error)) { + if (message) *message = [@"deb control: " stringByAppendingString: + (error ?: @"failed")]; + [fm removeItemAtPath:temp error:nil]; + return NO; + } + + NSArray *dataPaths = nil; + NSString *dataBase = nil; + NSString *dataPrefix = nil; + if (isDirectory && [source.pathExtension.lowercaseString isEqualToString:@"app"]) { + dataPaths = @[source]; + dataBase = source.stringByDeletingLastPathComponent; + dataPrefix = @"Applications"; + } else if (isDirectory) { + NSArray *children = [fm contentsOfDirectoryAtPath:source error:nil]; + NSMutableArray *paths = [NSMutableArray array]; + for (NSString *child in children ?: @[]) { + if ([child isEqualToString:@"DEBIAN"]) continue; + [paths addObject:[source stringByAppendingPathComponent:child]]; + } + dataPaths = paths; + dataBase = source; + } else { + dataPaths = @[source]; + dataBase = source.stringByDeletingLastPathComponent; + } + if (!WQCreateTarGz(dataPaths ?: @[], dataTar, dataBase, dataPrefix, + &error)) { + if (message) *message = [@"deb data: " stringByAppendingString: + (error ?: @"failed")]; + [fm removeItemAtPath:temp error:nil]; + return NO; + } + + NSData *controlData = [NSData dataWithContentsOfFile:controlTar]; + NSData *dataData = [NSData dataWithContentsOfFile:dataTar]; + NSData *binaryData = [NSData dataWithContentsOfFile:debianBinary]; + if (!controlData.length || !dataData.length || !binaryData.length) { + if (message) *message = @"deb: temporary archive read failed"; + [fm removeItemAtPath:temp error:nil]; + return NO; + } + NSMutableData *ar = [NSMutableData dataWithBytes:"!\n" length:8]; + WQArAppendMember(ar, @"debian-binary", binaryData); + WQArAppendMember(ar, @"control.tar.gz", controlData); + WQArAppendMember(ar, @"data.tar.gz", dataData); + BOOL ok = [ar writeToFile:destination atomically:YES]; + [fm removeItemAtPath:temp error:nil]; + if (ok && message) + *message = [NSString stringWithFormat:@"deb: %@ -> %@", package, + destination]; + else if (!ok && message) + *message = @"deb: cannot write destination"; + return ok; +} + +BOOL WQCreateIpaPackage(NSString *source, NSString *destination, + NSString **message) +{ + NSFileManager *fm = NSFileManager.defaultManager; + BOOL isDirectory = NO; + if (![fm fileExistsAtPath:source isDirectory:&isDirectory] || !isDirectory) { + if (message) *message = @"ipa: source must be a folder"; + return NO; + } + NSArray *paths = nil; + NSString *base = nil; + NSString *prefix = nil; + NSString *payload = [source stringByAppendingPathComponent:@"Payload"]; + BOOL payloadIsDirectory = NO; + if ([fm fileExistsAtPath:payload isDirectory:&payloadIsDirectory] && + payloadIsDirectory) { + paths = @[payload]; + base = source; + } else if ([source.pathExtension.lowercaseString isEqualToString:@"app"]) { + paths = @[source]; + base = source.stringByDeletingLastPathComponent; + prefix = @"Payload"; + } else { + NSString *appPath = nil; + for (NSString *child in [fm contentsOfDirectoryAtPath:source error:nil]) { + if ([child.pathExtension.lowercaseString isEqualToString:@"app"]) { + appPath = [source stringByAppendingPathComponent:child]; + break; + } + } + if (appPath) { + paths = @[appPath]; + base = source; + prefix = @"Payload"; + } + } + if (!paths.count) { + if (message) *message = @"ipa: no Payload/ or .app found"; + return NO; + } + BOOL ok = WQCreateZipArchiveWithPrefix(paths, destination, base, prefix, + message); + if (ok && message) + *message = [NSString stringWithFormat:@"ipa: -> %@", destination]; + return ok; +} + +BOOL WQExtractDebPayload(NSString *debPath, NSString *destination, + NSString **message) +{ + NSFileManager *fm = NSFileManager.defaultManager; + NSString *temp = [NSTemporaryDirectory() stringByAppendingPathComponent: + [NSString stringWithFormat:@"WQDebX-%@", NSUUID.UUID.UUIDString]]; + [fm createDirectoryAtPath:temp withIntermediateDirectories:YES + attributes:nil error:nil]; + NSString *error = nil; + if (!WQExtractArchive(debPath, temp, &error)) { + [fm removeItemAtPath:temp error:nil]; + if (message) *message = error ?: @"deb: cannot unpack container"; + return NO; + } + NSString *dataArchive = nil; + for (NSString *name in [fm contentsOfDirectoryAtPath:temp error:nil]) { + if ([name hasPrefix:@"data.tar"]) { + dataArchive = [temp stringByAppendingPathComponent:name]; + break; + } + } + if (!dataArchive.length) { + [fm removeItemAtPath:temp error:nil]; + if (message) *message = @"deb: data.tar not found"; + return NO; + } + BOOL ok = WQExtractArchive(dataArchive, destination, &error); + [fm removeItemAtPath:temp error:nil]; + if (message) *message = ok ? [NSString stringWithFormat: + @"deb: payload -> %@", destination] : (error ?: @"deb: payload failed"); + return ok; +} + NSString *WQDefaultExtractDestination(NSString *archivePath) { NSString *name = archivePath.lastPathComponent; diff --git a/WQFilzaIntegration.h b/WQFilzaIntegration.h index cb78c48..e25e24d 100644 --- a/WQFilzaIntegration.h +++ b/WQFilzaIntegration.h @@ -56,3 +56,5 @@ FOUNDATION_EXPORT NSString * _Nonnull WQFallbackDisplayName(NSString * _Nonnull /// Clears the cached app list and requests a fresh per-container traversal the /// next time the App Manager asks for applications. FOUNDATION_EXPORT void WQRequestAppManagerRescan(void); +/// The writable trash directory used by Filza under normal signing. +FOUNDATION_EXPORT NSString * _Nonnull WQTrashDirectory(void); diff --git a/WQFilzaIntegration.m b/WQFilzaIntegration.m index 920aebe..b26b232 100644 --- a/WQFilzaIntegration.m +++ b/WQFilzaIntegration.m @@ -455,6 +455,16 @@ static NSArray *> *WQFavoriteEntries(void) return entries; } +NSString *WQTrashDirectory(void) +{ + NSString *trash = [MCMFilzaVirtualRoot() + stringByAppendingPathComponent:@".Trash"]; + [NSFileManager.defaultManager createDirectoryAtPath:trash + withIntermediateDirectories:YES + attributes:@{ NSFilePosixPermissions: @0700 } error:nil]; + return trash; +} + static void WQInstallFavorites(void) { Class prefsClass = NSClassFromString(@"TGPreferences"); @@ -471,6 +481,43 @@ static void WQInstallFavorites(void) ? (NSMutableArray *)existing : nil; NSMutableIndexSet *stale = [NSMutableIndexSet indexSet]; NSFileManager *fm = NSFileManager.defaultManager; + + // Filza's built-in Trash favorite is /var/mobile/Library/Filza/.Trash, + // which does not exist in a normally signed app. Repoint it at the + // writable trash directory inside the app container. + NSString *trash = WQTrashDirectory(); + NSMutableArray *links = mutableLinks; + if (!links && [existing isKindOfClass:NSArray.class]) { + SEL setter = NSSelectorFromString(@"setFavoritedLinks:"); + if ([prefs respondsToSelector:setter]) { + links = [existing mutableCopy]; + ((void(*)(id, SEL, id))objc_msgSend)(prefs, setter, links); + } + } + BOOL trashChanged = NO; + if (links) { + for (NSUInteger index = 0; index < links.count; index++) { + if (![links[index] isKindOfClass:NSDictionary.class]) continue; + NSDictionary *item = links[index]; + NSString *name = item[@"name"]; + NSString *path = item[@"path"]; + BOOL isTrash = [name isEqualToString:@"Trash"] || + [path isEqualToString:@"/var/mobile/Library/Filza/.Trash"]; + if (!isTrash) continue; + if ([path isEqualToString:trash] && [fm fileExistsAtPath:path]) + continue; + NSMutableDictionary *updated = [item mutableCopy]; + updated[@"path"] = trash; + links[index] = updated; + trashChanged = YES; + } + if (trashChanged) { + ((void(*)(id, SEL))objc_msgSend)(prefs, + NSSelectorFromString(@"saveFavoritedLinks")); + NSLog(@"[WQ] Trash favorite -> %@", trash); + } + } + if (mutableLinks) { [mutableLinks enumerateObjectsUsingBlock:^(id item, NSUInteger idx, BOOL *stop) { if (![item isKindOfClass:NSDictionary.class]) return; @@ -533,7 +580,8 @@ static void WQInstallBundledTestFiles(void) NSString *folder = WQVirtualRootFolder(); NSString *source = [NSBundle.mainBundle.bundlePath stringByAppendingPathComponent:@"WQTestFiles"]; - NSArray *names = @[@"test.deb", @"test"]; + NSArray *names = @[@"test.deb", @"test", @"test.zip", + @"test.ipa"]; for (NSString *name in names) { NSString *from = [source stringByAppendingPathComponent:name]; NSString *to = [folder stringByAppendingPathComponent:name]; diff --git a/WQTerminal.m b/WQTerminal.m index cccdf1b..faae774 100644 --- a/WQTerminal.m +++ b/WQTerminal.m @@ -593,7 +593,9 @@ static BOOL WQRunArchiveCommand(NSArray *words, destination = WQDefaultExtractDestination(archive); NSString *message = nil; - BOOL ok = WQExtractArchive(archive, destination, &message); + BOOL isDeb = [archive.pathExtension.lowercaseString isEqualToString:@"deb"]; + BOOL ok = isDeb ? WQExtractDebPayload(archive, destination, &message) + : WQExtractArchive(archive, destination, &message); WQShellPrintf(context, @"%@\r\n", message ?: (ok ? @"extract: ok" : @"extract: failed")); return YES; @@ -775,6 +777,9 @@ static void WQPrintSBXHelp(WQShellContext *context) " sbx_id identify a container\r\n" " mount list mounted filesystems\r\n" " extract [-C dir] unzip/tar/gz/7z/rar/xz/bz2/zst\r\n" + " zip create a ZIP archive\r\n" + " mkdeb [out.deb] build a Debian package\r\n" + " mkipa [out.ipa] build an IPA from .app/Payload\r\n" " neofetch system information\r\n" " respring restart SpringBoard\r\n" " apt / apt-get update|list|search|show|install|remove\r\n" @@ -850,6 +855,52 @@ static void WQShellLoop(WQShellContext *context) [first isEqualToString:@"dpkg-deb"]) { WQRunPackages(words, context.output, context.error); fflush(context.output); + } else if ([first isEqualToString:@"mkdeb"] || + [first isEqualToString:@"mkipa"]) { + if (words.count < 2) { + WQShellPrintf(context, + @"usage: %@ [output]\r\n", + first); + } else { + NSString *source = WQExpandPath(words[1]); + NSString *suffix = [first isEqualToString:@"mkdeb"] + ? @".deb" : @".ipa"; + NSString *destination = words.count > 2 + ? WQExpandPath(words[2]) + : [[source stringByDeletingLastPathComponent] + stringByAppendingPathComponent: + [source.lastPathComponent + stringByAppendingString:suffix]]; + NSString *message = nil; + BOOL ok = [first isEqualToString:@"mkdeb"] + ? WQCreateDebPackage(source, destination, + nil, nil, nil, nil, &message) + : WQCreateIpaPackage(source, destination, + &message); + WQShellPrintf(context, @"%@\r\n", message ?: + (ok ? @"ok" : @"failed")); + } + } else if ([first isEqualToString:@"zip"] || + [first isEqualToString:@"compress"]) { + if (words.count < 3) { + WQShellWrite(context, + "usage: zip ...\r\n"); + } else { + NSString *destination = WQExpandPath(words[1]); + NSMutableArray *paths = [NSMutableArray array]; + for (NSUInteger index = 2; index < words.count; index++) + [paths addObject:WQExpandPath(words[index])]; + char cwdBuffer[PATH_MAX]; + NSString *base = getcwd(cwdBuffer, sizeof(cwdBuffer)) + ? @(cwdBuffer) : nil; + [NSFileManager.defaultManager removeItemAtPath: + destination error:nil]; + NSString *message = nil; + BOOL ok = WQCreateZipArchive(paths, destination, + base, &message); + WQShellPrintf(context, @"%@\r\n", message ?: + (ok ? @"zip: ok" : @"zip: failed")); + } } else if (WQRunArchiveCommand(words, context)) { // extracted with libarchive } else if (WQRunExternalExecutable(words, context)) { diff --git a/scripts/build_wq_ipa.sh b/scripts/build_wq_ipa.sh index a717cda..2ac0751 100755 --- a/scripts/build_wq_ipa.sh +++ b/scripts/build_wq_ipa.sh @@ -106,6 +106,12 @@ if [[ -f "$REPO_ROOT/tests/test" ]]; then cp "$REPO_ROOT/tests/test" "$APP/WQTestFiles/test" chmod 0755 "$APP/WQTestFiles/test" fi +if [[ -f "$REPO_ROOT/tests/test.zip" ]]; then + cp "$REPO_ROOT/tests/test.zip" "$APP/WQTestFiles/test.zip" +fi +if [[ -f "$REPO_ROOT/tests/test.ipa" ]]; then + cp "$REPO_ROOT/tests/test.ipa" "$APP/WQTestFiles/test.ipa" +fi echo "==> Injecting dylib" mkdir -p "$APP/Frameworks" diff --git a/tests/test.ipa b/tests/test.ipa new file mode 100644 index 0000000000000000000000000000000000000000..f30a6874ec75cac14fc4e3c750946198b90956ed GIT binary patch literal 1034 zcmWIWW@h1H00I5{oLDdeN^mg9Fa#u4=Hw@)=!b@IGBA6Fe$4{m(h6<{MwYLP3=AwF zr2#li5JWN|B(=CiFR`G2piv?~BRRm1*}rtFYY32+2*je;i~^a7&t<`3SNu<%x$N($ zbMAqc@3|A<0b1wvwfuVZb$oSKg=l!{>IHhN_3+ix_4GOA8T#RSSko#k4gZZn0T&s9 zW*IEJ;GPoHxP+>@%rxb z&wC&Do%5UOWWUIC)5_&PdiNx$yxOCgvUl#I(rsVveLOS&!t@WuC-z0QY?*XSXY84F=qT-j7V2^_Uf+5?jizz2a|J_nN~#SS5dzaYn<|Ua_@rg~gv)Hi~RJ zkaxzkc0=E4F{ADOSQM)S_R8ysAKw+Ar00^Gs_E+eFlPC&ZF!mZ&no;ryH90l$o@Mk z+=R7zmKMD3FAm!ybhzb>pq$rY(f9MWESh|xe_H+(W1+`C8}nVSy`E^AuCp_H>F#A) z2naB|bp+9fB+3d& zq8P~&w`qb5$fh|VnTC{H@fnLq38;w`Vk|IWFuZN-#bzud=@Mr&Y9d1zy`<3qXamgu da2F#dIb0U7vH=|h%)TrPi9jPJ15+;p0{~IZGphgq literal 0 HcmV?d00001 diff --git a/tests/test.zip b/tests/test.zip new file mode 100644 index 0000000000000000000000000000000000000000..b838d054e2fe34de1c034ba1076213b435bc2afe GIT binary patch literal 495 zcmWIWW@h1H00I5{oY0#J+vse%C(ps~fJN&5IqMAf~J@9kxNpmq@E zgX+!$T9leX$Yl^!3d#9-C8>EOKu0k$$uZ;dtpwPGKu<9&X#_Em{LTvTJDR`IjRWh4 x5-bc5;}{qO7~VRD14SWzCe9>OA0s<#D%2!o|Kl=~l?~(=W*~eEq~Cxz3;-^iV&?z= literal 0 HcmV?d00001