// // WQArchive.m // FilzaSlop // // In-process archive extraction for the terminal and Filza's file browser. // libarchive is already linked by the package backend; this keeps archive // support working without /usr/bin/unzip, 7z, unrar or a root helper. // #import "WQArchive.h" #import #include #include #include #include // The iOS SDK does not ship libarchive headers. Declare the small ABI surface // used here explicitly (the same approach as WQPackages.m). typedef struct archive WQArchiveReader; typedef struct archive_entry WQArchiveEntry; #define WQ_ARCHIVE_OK 0 #define WQ_ARCHIVE_EOF 1 #define WQ_AE_IFMT 0170000 #define WQ_AE_IFREG 0100000 #define WQ_AE_IFDIR 0040000 #define WQ_AE_IFLNK 0120000 extern WQArchiveReader *archive_read_new(void); extern int archive_read_support_filter_all(WQArchiveReader *); extern int archive_read_support_format_all(WQArchiveReader *); extern int archive_read_support_format_raw(WQArchiveReader *); extern int archive_read_open_filename(WQArchiveReader *, const char *, size_t); extern int archive_read_next_header(WQArchiveReader *, WQArchiveEntry **); extern const char *archive_entry_pathname(WQArchiveEntry *); extern unsigned int archive_entry_filetype(WQArchiveEntry *); extern const char *archive_entry_symlink(WQArchiveEntry *); extern unsigned int archive_entry_perm(WQArchiveEntry *); extern ssize_t archive_read_data(WQArchiveReader *, void *, size_t); extern int archive_read_data_skip(WQArchiveReader *); 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; NSString *path = [NSString stringWithUTF8String:name]; if (!path.length) return nil; while ([path hasPrefix:@"./"]) path = [path substringFromIndex:2]; if ([path isEqualToString:@"."] || [path hasPrefix:@"/"]) return nil; for (NSString *component in [path componentsSeparatedByString:@"/"]) if ([component isEqualToString:@".."]) return nil; return path; } static BOOL WQArchiveWriteEntry(WQArchiveReader *reader, WQArchiveEntry *entry, NSString *target) { NSFileManager *fm = NSFileManager.defaultManager; NSString *parent = target.stringByDeletingLastPathComponent; if (parent.length && ![fm createDirectoryAtPath:parent withIntermediateDirectories:YES attributes:nil error:nil]) return NO; mode_t type = archive_entry_filetype(entry); if (type == WQ_AE_IFDIR) { return [fm createDirectoryAtPath:target withIntermediateDirectories:YES attributes:nil error:nil]; } if (type == WQ_AE_IFLNK) { const char *link = archive_entry_symlink(entry); if (!link || link[0] == '/') return NO; unlink(target.fileSystemRepresentation); return symlink(link, target.fileSystemRepresentation) == 0; } if (type != WQ_AE_IFREG && type != 0) return NO; int descriptor = open(target.fileSystemRepresentation, O_WRONLY | O_CREAT | O_TRUNC, 0644); if (descriptor < 0) return NO; char buffer[65536]; ssize_t got; BOOL ok = YES; while ((got = archive_read_data(reader, 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) { ok = NO; break; } written += step; } if (!ok) break; } if (got < 0) ok = NO; if (close(descriptor) != 0) ok = NO; mode_t permissions = archive_entry_perm(entry); if (permissions) chmod(target.fileSystemRepresentation, permissions & 0777); return ok; } BOOL WQExtractArchive(NSString *archivePath, NSString *destination, NSString **message) { NSFileManager *fm = NSFileManager.defaultManager; if (!archivePath.length || !destination.length) { if (message) *message = @"extract: missing archive or destination"; return NO; } [fm createDirectoryAtPath:destination withIntermediateDirectories:YES attributes:nil error:nil]; WQArchiveReader *reader = archive_read_new(); if (!reader) { if (message) *message = @"extract: cannot allocate libarchive"; return NO; } archive_read_support_filter_all(reader); archive_read_support_format_all(reader); archive_read_support_format_raw(reader); int result = archive_read_open_filename(reader, archivePath.fileSystemRepresentation, 64 * 1024); if (result != WQ_ARCHIVE_OK) { if (message) *message = [NSString stringWithFormat: @"extract: %@: %s", archivePath, archive_error_string(reader) ?: "unreadable"]; archive_read_free(reader); return NO; } NSUInteger extracted = 0; NSUInteger skipped = 0; WQArchiveEntry *entry = NULL; while ((result = archive_read_next_header(reader, &entry)) == WQ_ARCHIVE_OK) { const char *name = archive_entry_pathname(entry); NSString *relative = WQArchiveSafeRelativePath(name); if (!relative.length) { skipped++; archive_read_data_skip(reader); continue; } NSString *target = [destination stringByAppendingPathComponent:relative]; if (WQArchiveWriteEntry(reader, entry, target)) extracted++; else skipped++; } BOOL failed = result != WQ_ARCHIVE_EOF; NSString *error = failed ? [NSString stringWithUTF8String: archive_error_string(reader) ?: "corrupt archive"] : nil; archive_read_close(reader); archive_read_free(reader); if (failed) { if (message) *message = [NSString stringWithFormat: @"extract: %@: %@", archivePath, error ?: @"corrupt archive"]; return NO; } if (message) *message = [NSString stringWithFormat: @"extract: %lu entries -> %@%@", (unsigned long)extracted, destination, skipped ? [NSString stringWithFormat:@" (%lu skipped)", (unsigned long)skipped] : @""]; 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; NSArray *suffixes = @[ @".tar.gz", @".tar.bz2", @".tar.xz", @".tar.zst", @".tgz", @".tbz", @".tbz2", @".txz", @".tzst", @".zip", @".7z", @".rar", @".tar", @".gz", @".bz2", @".xz", @".zst", @".deb", @".iso", ]; NSString *lower = name.lowercaseString; for (NSString *suffix in suffixes) { if ([lower hasSuffix:suffix] && name.length > suffix.length) { name = [name substringToIndex:name.length - suffix.length]; break; } } if (!name.length) name = @"extracted"; return [archivePath.stringByDeletingLastPathComponent stringByAppendingPathComponent:name]; }