// // WQPackages.m // FilzaSlop // // In-process dpkg/apt for the normally signed build. Packages are unpacked // under Documents/.terminal/root; this is a plugin/utility prefix, not the // system root, because the app is sandboxed. // #import "WQPackages.h" #import #import #import #import #import #import typedef struct archive WQArchive; typedef struct archive_entry WQArchiveEntry; #define WQ_ARCHIVE_OK 0 #define ARCHIVE_OK 0 #define WQ_AE_IFMT 0170000 #define WQ_AE_IFREG 0100000 #define WQ_AE_IFDIR 0040000 #define WQ_AE_IFLNK 0120000 extern WQArchive *archive_read_new(void); extern int archive_read_support_filter_all(WQArchive *); extern int archive_read_support_format_all(WQArchive *); extern int archive_read_support_format_raw(WQArchive *); extern int archive_read_open_memory(WQArchive *, const void *, size_t); extern int archive_read_next_header(WQArchive *, WQArchiveEntry **); extern const char *archive_entry_pathname(WQArchiveEntry *); extern const char *archive_entry_symlink(WQArchiveEntry *); extern mode_t archive_entry_perm(WQArchiveEntry *); extern mode_t archive_entry_filetype(WQArchiveEntry *); extern long long archive_entry_size(WQArchiveEntry *); extern ssize_t archive_read_data(WQArchive *, void *, size_t); extern int archive_read_data_skip(WQArchive *); extern int archive_read_free(WQArchive *); extern const char *archive_error_string(WQArchive *); static NSString *WQPackageRoot(void) { NSString *configured = NSProcessInfo.processInfo.environment[@"TERM_ROOT"]; if (configured.length) return configured; NSString *documents = NSSearchPathForDirectoriesInDomains( NSDocumentDirectory, NSUserDomainMask, YES).firstObject; return [documents stringByAppendingPathComponent:@".terminal"]; } static NSString *WQInstallRoot(void) { return [WQPackageRoot() stringByAppendingPathComponent:@"root"]; } static NSString *WQStatusPath(void) { return [WQPackageRoot() stringByAppendingPathComponent:@"var/lib/dpkg/status"]; } static NSString *WQInfoDirectory(void) { return [WQPackageRoot() stringByAppendingPathComponent:@"var/lib/dpkg/info"]; } static void WQEnsurePackageDirectories(void) { NSFileManager *fm = NSFileManager.defaultManager; NSArray *directories = @[ WQPackageRoot(), WQInstallRoot(), WQInfoDirectory(), [WQPackageRoot() stringByAppendingPathComponent:@"var/lib/apt/lists"], [WQPackageRoot() stringByAppendingPathComponent:@"var/cache/apt/archives"], [WQPackageRoot() stringByAppendingPathComponent:@"etc/apt"], [WQPackageRoot() stringByAppendingPathComponent:@"tmp"], ]; for (NSString *directory in directories) [fm createDirectoryAtPath:directory withIntermediateDirectories:YES attributes:nil error:nil]; NSString *status = WQStatusPath(); if (![fm fileExistsAtPath:status]) [@"" writeToFile:status atomically:YES encoding:NSUTF8StringEncoding error:nil]; } #pragma mark - ar / deb static NSData *WQArMember(NSData *archive, NSString *namePrefix) { const uint8_t *bytes = archive.bytes; NSUInteger length = archive.length; if (length < 8 || memcmp(bytes, "!\n", 8) != 0) return nil; NSUInteger offset = 8; while (offset + 60 <= length) { const uint8_t *header = bytes + offset; NSString *name = [[[NSString alloc] initWithBytes:header length:16 encoding:NSASCIIStringEncoding] stringByTrimmingCharactersInSet: NSCharacterSet.whitespaceCharacterSet]; name = [name stringByTrimmingCharactersInSet: [NSCharacterSet characterSetWithCharactersInString:@"/"]]; NSString *sizeText = [[[NSString alloc] initWithBytes:header + 48 length:10 encoding:NSASCIIStringEncoding] stringByTrimmingCharactersInSet: NSCharacterSet.whitespaceCharacterSet]; NSUInteger size = (NSUInteger)sizeText.longLongValue; offset += 60; if (offset + size > length) break; if ([name hasPrefix:namePrefix]) return [archive subdataWithRange:NSMakeRange(offset, size)]; offset += size + (size & 1); } return nil; } static NSString *WQArchivePath(NSString *raw) { NSString *path = raw; while ([path hasPrefix:@"./"]) path = [path substringFromIndex:2]; while ([path hasPrefix:@"/"]) path = [path substringFromIndex:1]; if ([path hasPrefix:@"var/jb/"]) path = [path substringFromIndex:7]; if (path.length == 0 || [path containsString:@".."]) return nil; return path; } static BOOL WQExtractArchiveData(NSData *archiveData, NSString *destination, NSMutableArray *installedFiles) { if (!archiveData.length) return NO; WQArchive *archive = archive_read_new(); if (!archive) return NO; archive_read_support_filter_all(archive); archive_read_support_format_all(archive); if (archive_read_open_memory(archive, archiveData.bytes, archiveData.length) != ARCHIVE_OK) { archive_read_free(archive); return NO; } NSFileManager *fm = NSFileManager.defaultManager; struct archive_entry *entry; BOOL success = YES; while (archive_read_next_header(archive, &entry) == WQ_ARCHIVE_OK) { const char *rawName = archive_entry_pathname(entry); if (!rawName) { archive_read_data_skip(archive); continue; } NSString *relative = WQArchivePath([NSString stringWithUTF8String:rawName]); if (!relative.length) { archive_read_data_skip(archive); continue; } NSString *target = [destination stringByAppendingPathComponent:relative]; mode_t filetype = archive_entry_filetype(entry); mode_t permissions = archive_entry_perm(entry); if (filetype == WQ_AE_IFDIR) { [fm createDirectoryAtPath:target withIntermediateDirectories:YES attributes:@{ NSFilePosixPermissions: @(permissions ?: 0755) } error:nil]; continue; } [fm createDirectoryAtPath:target.stringByDeletingLastPathComponent withIntermediateDirectories:YES attributes:nil error:nil]; if (filetype == WQ_AE_IFLNK) { const char *link = archive_entry_symlink(entry); [fm removeItemAtPath:target error:nil]; if (link && symlink(link, target.fileSystemRepresentation) == 0) [installedFiles addObject:relative]; } else if (filetype == WQ_AE_IFREG) { int descriptor = open(target.fileSystemRepresentation, O_WRONLY | O_CREAT | O_TRUNC, permissions ?: 0644); if (descriptor < 0) { success = NO; archive_read_data_skip(archive); continue; } uint8_t buffer[65536]; ssize_t got; while ((got = archive_read_data(archive, buffer, sizeof(buffer))) > 0) { ssize_t written = 0; while (written < got) { ssize_t step = write(descriptor, buffer + written, (size_t)(got - written)); if (step <= 0) break; written += step; } } close(descriptor); chmod(target.fileSystemRepresentation, permissions ?: 0644); [installedFiles addObject:relative]; } else { archive_read_data_skip(archive); } } archive_read_free(archive); return success; } static NSData *WQArchiveEntryNamed(NSData *archiveData, NSString *wanted) { WQArchive *archive = archive_read_new(); if (!archive) return nil; archive_read_support_filter_all(archive); archive_read_support_format_all(archive); if (archive_read_open_memory(archive, archiveData.bytes, archiveData.length) != ARCHIVE_OK) { archive_read_free(archive); return nil; } struct archive_entry *entry; NSData *result = nil; while (archive_read_next_header(archive, &entry) == WQ_ARCHIVE_OK) { const char *name = archive_entry_pathname(entry); if (name && (strcmp(name, wanted.UTF8String) == 0 || strcmp(name + (strlen(name) > 2 && name[0] == '.' && name[1] == '/' ? 2 : 0), wanted.UTF8String) == 0)) { NSMutableData *data = [NSMutableData data]; uint8_t buffer[65536]; ssize_t got; while ((got = archive_read_data(archive, buffer, sizeof(buffer))) > 0) [data appendBytes:buffer length:(NSUInteger)got]; result = data; break; } archive_read_data_skip(archive); } archive_read_free(archive); return result; } static NSDictionary *WQParseControl(NSString *control) { NSMutableDictionary *fields = [NSMutableDictionary dictionary]; NSString *currentKey = nil; NSMutableString *currentValue = nil; for (NSString *line in [control componentsSeparatedByString:@"\n"]) { if (line.length == 0) continue; if ([line hasPrefix:@" "] || [line hasPrefix:@"\t"]) { if (currentKey && currentValue) [currentValue appendFormat:@"\n%@", [line stringByTrimmingCharactersInSet: NSCharacterSet.whitespaceCharacterSet]]; continue; } NSRange colon = [line rangeOfString:@":"]; if (colon.location == NSNotFound) continue; if (currentKey && currentValue) fields[currentKey] = currentValue.copy; currentKey = [[line substringToIndex:colon.location] stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceCharacterSet]; currentValue = [[[line substringFromIndex:colon.location + 1] stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceCharacterSet] mutableCopy]; } if (currentKey && currentValue) fields[currentKey] = currentValue.copy; return fields; } NSString *WQDebInfoText(NSString *path) { NSData *archive = [NSData dataWithContentsOfFile:path]; if (!archive.length) return nil; NSData *controlArchive = WQArMember(archive, @"control.tar"); if (!controlArchive.length) return nil; NSData *controlData = WQArchiveEntryNamed(controlArchive, @"control"); if (!controlData.length) return nil; NSString *control = [[NSString alloc] initWithData:controlData encoding:NSUTF8StringEncoding]; if (!control.length) return nil; NSMutableString *text = [NSMutableString stringWithFormat: @" new Debian package, version 2.0.\n" @" size %lu bytes: control archive %lu bytes.\n", (unsigned long)archive.length, (unsigned long)controlArchive.length]; [text appendString:control]; if (![text hasSuffix:@"\n"]) [text appendString:@"\n"]; return text; } static NSString *WQStanzaForKey(NSString *text, NSString *key, NSString *value) { NSArray *stanzas = [text componentsSeparatedByString:@"\n\n"]; for (NSString *stanza in stanzas) { BOOL match = NO; for (NSString *line in [stanza componentsSeparatedByString:@"\n"]) { if ([line hasPrefix:[key stringByAppendingString:@": "]]) { match = [[line substringFromIndex:key.length + 2] isEqualToString:value]; break; } } if (match) return stanza; } return nil; } static NSString *WQFieldFromStanza(NSString *stanza, NSString *key) { for (NSString *line in [stanza componentsSeparatedByString:@"\n"]) if ([line hasPrefix:[key stringByAppendingString:@": "]]) return [line substringFromIndex:key.length + 2]; return nil; } static void WQRemoveStatusStanza(NSString *package) { NSString *text = [NSString stringWithContentsOfFile:WQStatusPath() encoding:NSUTF8StringEncoding error:nil] ?: @""; NSMutableArray *keep = [NSMutableArray array]; for (NSString *stanza in [text componentsSeparatedByString:@"\n\n"]) { if (stanza.length == 0) continue; NSString *name = WQFieldFromStanza(stanza, @"Package"); if (![name isEqualToString:package]) [keep addObject:stanza]; } [[keep componentsJoinedByString:@"\n\n"] writeToFile:WQStatusPath() atomically:YES encoding:NSUTF8StringEncoding error:nil]; } static BOOL WQInstallDeb(NSString *path, FILE *out, FILE *err, NSDictionary *options) { WQEnsurePackageDirectories(); NSData *archive = [NSData dataWithContentsOfFile:path]; if (!archive.length) { fprintf(err, "dpkg: cannot open %s\n", path.UTF8String); return NO; } NSData *controlArchive = WQArMember(archive, @"control.tar"); NSData *dataArchive = WQArMember(archive, @"data.tar"); if (!controlArchive || !dataArchive) { fprintf(err, "dpkg: %s is not a Debian archive\n", path.UTF8String); return NO; } NSData *controlData = WQArchiveEntryNamed(controlArchive, @"control"); if (!controlData) { fprintf(err, "dpkg: missing control file in %s\n", path.UTF8String); return NO; } NSString *control = [[NSString alloc] initWithData:controlData encoding:NSUTF8StringEncoding]; NSDictionary *fields = WQParseControl(control ?: @""); NSString *package = fields[@"Package"] ?: @"unknown"; NSString *version = fields[@"Version"] ?: @"0"; NSString *architecture = fields[@"Architecture"] ?: @"iphoneos-arm64"; BOOL force = options[@"force"] != nil; if (!force && ![architecture isEqualToString:@"iphoneos-arm64"] && ![architecture isEqualToString:@"iphoneos-arm"] && ![architecture isEqualToString:@"darwin-arm64"] && ![architecture isEqualToString:@"all"]) { fprintf(err, "dpkg: package architecture %s is not native " "(use --force-architecture)\n", architecture.UTF8String); return NO; } fprintf(out, "Selecting previously unselected package %s.\n", package.UTF8String); NSMutableArray *installedFiles = [NSMutableArray array]; if (!WQExtractArchiveData(dataArchive, WQInstallRoot(), installedFiles)) { fprintf(err, "dpkg: failed to unpack %s\n", path.UTF8String); return NO; } WQRemoveStatusStanza(package); NSMutableString *status = [NSMutableString stringWithContentsOfFile:WQStatusPath() encoding:NSUTF8StringEncoding error:nil] ?: [NSMutableString string]; if (status.length && ![status hasSuffix:@"\n\n"]) [status appendString:@"\n\n"]; [status appendFormat: @"Package: %@\n" @"Status: install ok installed\n" @"Priority: %@\n" @"Section: %@\n" @"Installed-Size: %@\n" @"Maintainer: %@\n" @"Architecture: %@\n" @"Version: %@\n" @"Description: %@\n\n", package, fields[@"Priority"] ?: @"optional", fields[@"Section"] ?: @"utility", fields[@"Installed-Size"] ?: @"0", fields[@"Maintainer"] ?: @"unknown", architecture, version, fields[@"Description"] ?: package]; [status writeToFile:WQStatusPath() atomically:YES encoding:NSUTF8StringEncoding error:nil]; NSString *listPath = [WQInfoDirectory() stringByAppendingPathComponent:[package stringByAppendingString:@".list"]]; [[installedFiles componentsJoinedByString:@"\n"] writeToFile:listPath atomically:YES encoding:NSUTF8StringEncoding error:nil]; fprintf(out, "Unpacking %s (%s) ...\n", package.UTF8String, version.UTF8String); fprintf(out, "Setting up %s (%s) ...\n", package.UTF8String, version.UTF8String); fprintf(out, "Installed to %s\n", WQInstallRoot().UTF8String); return YES; } #pragma mark - dpkg static void WQDPKGUsage(FILE *out) { fprintf(out, "Usage: dpkg \n" " -i, --install Install a package\n" " -r, --remove Remove a package\n" " -P, --purge Remove a package\n" " -l, --list List installed packages\n" " -s, --status Show package status\n" " -L, --listfiles List package files\n" " -x, --extract Extract data archive\n" " --contents List .deb contents\n" " --print-architecture Print iphoneos-arm64\n" " --force-architecture Allow non-native packages\n" " --version Show version\n"); } int WQDPKGMain(int argc, char **argv, FILE *out, FILE *err) { WQEnsurePackageDirectories(); if (argc < 2) { WQDPKGUsage(out); return 1; } NSString *action = [NSString stringWithUTF8String:argv[1]]; BOOL force = NO; int index = 2; for (int i = 2; i < argc; i++) { NSString *arg = [NSString stringWithUTF8String:argv[i]]; if ([arg isEqualToString:@"--force-architecture"]) force = YES; } NSDictionary *options = force ? @{ @"force": @YES } : @{}; if ([action isEqualToString:@"--version"]) { fprintf(out, "dpkg 1.22 (FilzaSlop in-process)\n"); return 0; } if ([action isEqualToString:@"--print-architecture"]) { fprintf(out, "iphoneos-arm64\n"); return 0; } if ([action isEqualToString:@"-l"] || [action isEqualToString:@"--list"]) { NSString *text = [NSString stringWithContentsOfFile:WQStatusPath() encoding:NSUTF8StringEncoding error:nil] ?: @""; fprintf(out, "Desired=Unknown/Install/Remove/Purge/Hold\n" "||/ Name Version Architecture Description\n"); for (NSString *stanza in [text componentsSeparatedByString:@"\n\n"]) { if (stanza.length == 0) continue; NSString *name = WQFieldFromStanza(stanza, @"Package"); NSString *version = WQFieldFromStanza(stanza, @"Version") ?: @""; NSString *arch = WQFieldFromStanza(stanza, @"Architecture") ?: @""; NSString *description = WQFieldFromStanza(stanza, @"Description") ?: @""; fprintf(out, "ii %-20s %-14s %-14s %s\n", (name ?: @"").UTF8String, version.UTF8String, arch.UTF8String, description.UTF8String); } return 0; } if ([action isEqualToString:@"-s"] || [action isEqualToString:@"--status"]) { if (index >= argc) { fprintf(err, "dpkg: missing package\n"); return 1; } NSString *package = [NSString stringWithUTF8String:argv[index]]; NSString *text = [NSString stringWithContentsOfFile:WQStatusPath() encoding:NSUTF8StringEncoding error:nil] ?: @""; NSString *stanza = WQStanzaForKey(text, @"Package", package); if (!stanza) { fprintf(err, "dpkg: package %s is not installed\n", package.UTF8String); return 1; } fprintf(out, "%s\n", stanza.UTF8String); return 0; } if ([action isEqualToString:@"-L"] || [action isEqualToString:@"--listfiles"]) { if (index >= argc) { fprintf(err, "dpkg: missing package\n"); return 1; } NSString *package = [NSString stringWithUTF8String:argv[index]]; NSString *list = [NSString stringWithContentsOfFile: [WQInfoDirectory() stringByAppendingPathComponent: [package stringByAppendingString:@".list"]] encoding:NSUTF8StringEncoding error:nil]; if (!list) { fprintf(err, "dpkg: package %s has no file list\n", package.UTF8String); return 1; } fprintf(out, "%s\n", list.UTF8String); return 0; } if ([action isEqualToString:@"-r"] || [action isEqualToString:@"--remove"] || [action isEqualToString:@"-P"] || [action isEqualToString:@"--purge"]) { if (index >= argc) { fprintf(err, "dpkg: missing package\n"); return 1; } NSString *package = [NSString stringWithUTF8String:argv[index]]; NSString *listPath = [WQInfoDirectory() stringByAppendingPathComponent:[package stringByAppendingString:@".list"]]; NSString *list = [NSString stringWithContentsOfFile:listPath encoding:NSUTF8StringEncoding error:nil]; NSFileManager *fm = NSFileManager.defaultManager; for (NSString *relative in [list componentsSeparatedByString:@"\n"]) { if (!relative.length) continue; NSString *target = [WQInstallRoot() stringByAppendingPathComponent:relative]; [fm removeItemAtPath:target error:nil]; } [fm removeItemAtPath:listPath error:nil]; WQRemoveStatusStanza(package); fprintf(out, "Removing %s ...\n", package.UTF8String); return 0; } if ([action isEqualToString:@"-x"] || [action isEqualToString:@"--extract"] || [action isEqualToString:@"--contents"]) { if (index >= argc) { fprintf(err, "dpkg: missing .deb\n"); return 1; } NSString *path = [NSString stringWithUTF8String:argv[index]]; NSData *archive = [NSData dataWithContentsOfFile:path]; NSData *data = WQArMember(archive, @"data.tar"); if (!data) { fprintf(err, "dpkg: no data archive\n"); return 1; } if ([action isEqualToString:@"--contents"]) { NSMutableArray *files = [NSMutableArray array]; WQExtractArchiveData(data, NSTemporaryDirectory(), files); for (NSString *file in files) fprintf(out, "%s\n", file.UTF8String); return 0; } NSString *destination = index + 1 < argc ? [NSString stringWithUTF8String:argv[index + 1]] : WQInstallRoot(); NSMutableArray *files = [NSMutableArray array]; return WQExtractArchiveData(data, destination, files) ? 0 : 1; } if ([action isEqualToString:@"-i"] || [action isEqualToString:@"--install"]) { if (index >= argc) { fprintf(err, "dpkg: missing .deb\n"); return 1; } BOOL all = YES; for (; index < argc; index++) { NSString *path = [NSString stringWithUTF8String:argv[index]]; if ([path isEqualToString:@"--force-architecture"]) continue; if (!WQInstallDeb(path, out, err, options)) all = NO; } return all ? 0 : 1; } WQDPKGUsage(out); return 1; } #pragma mark - apt static NSString *WQAptIndexPath(void) { return [WQPackageRoot() stringByAppendingPathComponent: @"var/lib/apt/lists/Packages"]; } static NSString *WQAptSourcesPath(void) { return [WQPackageRoot() stringByAppendingPathComponent:@"etc/apt/sources.list"]; } static NSArray *WQAptStanzas(void) { NSString *text = [NSString stringWithContentsOfFile:WQAptIndexPath() encoding:NSUTF8StringEncoding error:nil] ?: @""; NSMutableArray *stanzas = [NSMutableArray array]; for (NSString *stanza in [text componentsSeparatedByString:@"\n\n"]) if (stanza.length) [stanzas addObject:stanza]; return stanzas; } static NSData *WQDownloadURL(NSString *urlString) { NSURL *url = [NSURL URLWithString:urlString]; if (!url) return nil; if ([url isFileURL]) return [NSData dataWithContentsOfURL:url]; dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); __block NSData *result = nil; NSURLSessionDataTask *task = [NSURLSession.sharedSession dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { if (!error && [(NSHTTPURLResponse *)response statusCode] / 100 == 2) result = data; dispatch_semaphore_signal(semaphore); }]; [task resume]; dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 30 * NSEC_PER_SEC)); return result; } static void WQEnsureSources(void) { NSString *path = WQAptSourcesPath(); if ([[NSFileManager defaultManager] fileExistsAtPath:path]) return; NSString *root = WQPackageRoot(); NSString *line = [NSString stringWithFormat:@"deb [trusted=yes] file://%@/repo ./\n", root]; [line writeToFile:path atomically:YES encoding:NSUTF8StringEncoding error:nil]; } static void WQAptUsage(FILE *out) { fprintf(out, "Usage: apt \n" " update Refresh package indexes\n" " list List packages in the index\n" " search Search package names/descriptions\n" " show Show package details\n" " install Install a package\n" " remove|purge Remove a package\n" " clean Clear downloaded archives\n" " --version Show version\n" "Root: %s\n", WQPackageRoot().UTF8String); } int WQAptMain(int argc, char **argv, FILE *out, FILE *err) { WQEnsurePackageDirectories(); WQEnsureSources(); if (argc < 2) { WQAptUsage(out); return 1; } NSString *action = [NSString stringWithUTF8String:argv[1]]; if ([action isEqualToString:@"--version"]) { fprintf(out, "apt 2.7 (FilzaSlop in-process)\n"); return 0; } if ([action isEqualToString:@"update"]) { fprintf(out, "apt: updating package lists...\n"); NSString *sources = [NSString stringWithContentsOfFile:WQAptSourcesPath() encoding:NSUTF8StringEncoding error:nil] ?: @""; NSMutableString *index = [NSMutableString string]; for (NSString *line in [sources componentsSeparatedByString:@"\n"]) { if (![line hasPrefix:@"deb "]) continue; NSArray *parts = [line componentsSeparatedByString:@" "]; NSString *uri = nil; for (NSString *part in parts) { if ([part hasPrefix:@"http://"] || [part hasPrefix:@"https://"] || [part hasPrefix:@"file://"]) { uri = part; break; } } if (!uri.length) continue; NSArray *names = @[@"Packages", @"Packages.gz"]; BOOL loaded = NO; for (NSString *name in names) { NSString *url = [uri stringByAppendingPathComponent:name]; NSData *data = WQDownloadURL(url); if (!data.length) continue; if ([name hasSuffix:@".gz"]) { // Decompress gzip through libarchive's raw reader. WQArchive *archive = archive_read_new(); archive_read_support_filter_all(archive); archive_read_support_format_raw(archive); if (archive_read_open_memory(archive, data.bytes, data.length) != ARCHIVE_OK) { archive_read_free(archive); continue; } struct archive_entry *entry; NSMutableData *plain = [NSMutableData data]; if (archive_read_next_header(archive, &entry) == WQ_ARCHIVE_OK) { uint8_t buffer[65536]; ssize_t got; while ((got = archive_read_data(archive, buffer, sizeof(buffer))) > 0) [plain appendBytes:buffer length:(NSUInteger)got]; } archive_read_free(archive); data = plain; } NSString *text = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; if (text.length) { [index appendString:text]; if (![index hasSuffix:@"\n"]) [index appendString:@"\n"]; loaded = YES; break; } } fprintf(out, "%s: %s\n", loaded ? "Get" : "Err", uri.UTF8String); } [index writeToFile:WQAptIndexPath() atomically:YES encoding:NSUTF8StringEncoding error:nil]; fprintf(out, "Reading package lists... Done\n"); return 0; } if ([action isEqualToString:@"list"]) { for (NSString *stanza in WQAptStanzas()) { NSString *name = WQFieldFromStanza(stanza, @"Package") ?: @""; NSString *version = WQFieldFromStanza(stanza, @"Version") ?: @""; NSString *description = WQFieldFromStanza(stanza, @"Description") ?: @""; fprintf(out, "%s/%s %s %s\n", name.UTF8String, (WQFieldFromStanza(stanza, @"Architecture") ?: @"iphoneos-arm64").UTF8String, version.UTF8String, description.UTF8String); } return 0; } if ([action isEqualToString:@"search"]) { NSString *text = argc > 2 ? [NSString stringWithUTF8String:argv[2]] : @""; for (NSString *stanza in WQAptStanzas()) if (text.length == 0 || [stanza.lowercaseString containsString:text.lowercaseString]) { NSString *name = WQFieldFromStanza(stanza, @"Package") ?: @""; NSString *description = WQFieldFromStanza(stanza, @"Description") ?: @""; fprintf(out, "%s - %s\n", name.UTF8String, description.UTF8String); } return 0; } if ([action isEqualToString:@"show"]) { if (argc < 3) { fprintf(err, "apt: missing package\n"); return 1; } NSString *package = [NSString stringWithUTF8String:argv[2]]; for (NSString *stanza in WQAptStanzas()) if ([WQFieldFromStanza(stanza, @"Package") isEqualToString:package]) { fprintf(out, "%s\n", stanza.UTF8String); return 0; } fprintf(err, "apt: package %s not found\n", package.UTF8String); return 1; } if ([action isEqualToString:@"install"]) { if (argc < 3) { fprintf(err, "apt: missing package\n"); return 1; } NSString *target = [NSString stringWithUTF8String:argv[2]]; if ([[NSFileManager defaultManager] fileExistsAtPath:target] || [target hasSuffix:@".deb"]) return WQDPKGMain(3, (char *[]){(char *)"dpkg", (char *)"-i", (char *)target.UTF8String}, out, err) ? 0 : 1; for (NSString *stanza in WQAptStanzas()) { if (![WQFieldFromStanza(stanza, @"Package") isEqualToString:target]) continue; NSString *filename = WQFieldFromStanza(stanza, @"Filename"); NSString *source = @""; NSString *sources = [NSString stringWithContentsOfFile:WQAptSourcesPath() encoding:NSUTF8StringEncoding error:nil] ?: @""; for (NSString *line in [sources componentsSeparatedByString:@"\n"]) for (NSString *part in [line componentsSeparatedByString:@" "]) if ([part hasPrefix:@"http"] || [part hasPrefix:@"file://"]) { source = part; break; } NSString *url = [source stringByAppendingPathComponent:filename ?: @""]; NSData *deb = WQDownloadURL(url); NSString *cache = [[WQPackageRoot() stringByAppendingPathComponent:@"var/cache/apt/archives"] stringByAppendingPathComponent:filename.lastPathComponent ?: @"package.deb"]; if (!deb.length || ![deb writeToFile:cache atomically:YES]) { fprintf(err, "apt: unable to download %s\n", url.UTF8String); return 1; } return WQDPKGMain(3, (char *[]){(char *)"dpkg", (char *)"-i", (char *)cache.UTF8String}, out, err) ? 0 : 1; } fprintf(err, "apt: package %s not found; run 'apt update'\n", target.UTF8String); return 1; } if ([action isEqualToString:@"remove"] || [action isEqualToString:@"purge"]) { if (argc < 3) { fprintf(err, "apt: missing package\n"); return 1; } return WQDPKGMain(argc, (char *[]){(char *)"dpkg", (char *)"-r", argv[2], NULL}, out, err) ? 0 : 1; } if ([action isEqualToString:@"clean"]) { NSString *archives = [WQPackageRoot() stringByAppendingPathComponent:@"var/cache/apt/archives"]; for (NSString *name in [NSFileManager.defaultManager contentsOfDirectoryAtPath:archives error:nil]) [NSFileManager.defaultManager removeItemAtPath: [archives stringByAppendingPathComponent:name] error:nil]; fprintf(out, "apt: cache cleaned\n"); return 0; } WQAptUsage(out); return 1; } BOOL WQInstallDebPackage(NSString *path, NSString **message) { FILE *out = tmpfile(); FILE *err = tmpfile(); BOOL result = WQInstallDeb(path, out, err, @{}); NSMutableString *text = [NSMutableString string]; if (out) { fflush(out); rewind(out); char buffer[4096]; size_t got; while ((got = fread(buffer, 1, sizeof(buffer), out)) > 0) [text appendString:[[NSString alloc] initWithBytes:buffer length:got encoding:NSUTF8StringEncoding] ?: @""]; fclose(out); } if (err) { fflush(err); rewind(err); char buffer[4096]; size_t got; while ((got = fread(buffer, 1, sizeof(buffer), err)) > 0) [text appendString:[[NSString alloc] initWithBytes:buffer length:got encoding:NSUTF8StringEncoding] ?: @""]; fclose(err); } if (message) *message = text; return result; }