676 lines
26 KiB
Objective-C
676 lines
26 KiB
Objective-C
//
|
|
// 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 <Foundation/Foundation.h>
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <sys/stat.h>
|
|
#include <unistd.h>
|
|
|
|
// 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<NSString *> *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<NSString *> *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<NSString *> *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<NSString *> *paths,
|
|
NSString *destination,
|
|
NSString *baseDirectory,
|
|
NSString *prefix,
|
|
NSString **message)
|
|
{
|
|
return WQCreateArchiveWithPrefix(paths, destination, baseDirectory,
|
|
prefix, YES, message);
|
|
}
|
|
|
|
static BOOL WQCreateTarGz(NSArray<NSString *> *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 <filzaslop@localhost>\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<NSString *> *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:"!<arch>\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<NSString *> *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<NSString *> *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];
|
|
}
|