Index: utils/ofarc/Makefile ================================================================== --- utils/ofarc/Makefile +++ utils/ofarc/Makefile @@ -3,11 +3,12 @@ PROG = ofarc${PROG_SUFFIX} SRCS = GZIPArchive.m \ LHAArchive.m \ OFArc.m \ TarArchive.m \ - ZIPArchive.m + ZIPArchive.m \ + ZooArchive.m DATA = localization/de.json \ localization/localizations.json include ../../buildsys.mk Index: utils/ofarc/OFArc.m ================================================================== --- utils/ofarc/OFArc.m +++ utils/ofarc/OFArc.m @@ -30,10 +30,11 @@ #import "OFArc.h" #import "GZIPArchive.h" #import "LHAArchive.h" #import "TarArchive.h" #import "ZIPArchive.h" +#import "ZooArchive.h" #import "OFCreateDirectoryFailedException.h" #import "OFInvalidArgumentException.h" #import "OFInvalidFormatException.h" #import "OFNotImplementedException.h" @@ -59,22 +60,22 @@ @"Options:\n" @" -a --append Append to archive\n" @" -c --create Create archive\n" @" -C --directory= Extract into the specified " @"directory\n" - @" -E --encoding= The encoding used by the archive " - "(only tar and lha files)\n" + @" -E --encoding= The encoding used by the archive\n" + @" (only tar, lha and zoo files)\n" @" -f --force Force / overwrite files\n" @" -h --help Show this help\n" @" -l --list List all files in the archive\n" @" -n --no-clobber Never overwrite files\n" @" -p --print Print one or more files from the " @"archive\n" @" -q --quiet Quiet mode (no output, except " @"errors)\n" @" -t --type= Archive type (gz, lha, tar, tgz, " - @"zip)\n" + @"zip, zoo)\n" @" -v --verbose Verbose output for file list\n" @" -x --extract Extract files")]; } [OFApplication terminateWithStatus: status]; @@ -554,22 +555,27 @@ [OFApplication terminateWithStatus: 1]; } } if (type == nil || [type isEqual: @"auto"]) { + OFString *lowercasePath = path.lowercaseString; + /* This one has to be first for obvious reasons */ - if ([path hasSuffix: @".tar.gz"] || [path hasSuffix: @".tgz"] || - [path hasSuffix: @".TAR.GZ"] || [path hasSuffix: @".TGZ"]) + if ([lowercasePath hasSuffix: @".tar.gz"] || + [lowercasePath hasSuffix: @".tgz"]) type = @"tgz"; - else if ([path hasSuffix: @".gz"] || [path hasSuffix: @".GZ"]) + else if ([lowercasePath hasSuffix: @".gz"]) type = @"gz"; - else if ([path hasSuffix: @".lha"] || - [path hasSuffix: @".lzh"] || [path hasSuffix: @".lzs"] || - [path hasSuffix: @".pma"]) + else if ([lowercasePath hasSuffix: @".lha"] || + [lowercasePath hasSuffix: @".lzh"] || + [lowercasePath hasSuffix: @".lzs"] || + [lowercasePath hasSuffix: @".pma"]) type = @"lha"; - else if ([path hasSuffix: @".tar"] || [path hasSuffix: @".TAR"]) + else if ([lowercasePath hasSuffix: @".tar"]) type = @"tar"; + else if ([lowercasePath hasSuffix: @".zoo"]) + type = @"zoo"; else type = @"zip"; } @try { @@ -597,10 +603,15 @@ mode: modeString encoding: encoding]; } else if ([type isEqual: @"zip"]) archive = [ZIPArchive archiveWithPath: path stream: file + mode: modeString + encoding: encoding]; + else if ([type isEqual: @"zoo"]) + archive = [ZooArchive archiveWithPath: path + stream: file mode: modeString encoding: encoding]; else { [OFStdErr writeLine: OF_LOCALIZED( @"unknown_archive_type", ADDED utils/ofarc/ZooArchive.h Index: utils/ofarc/ZooArchive.h ================================================================== --- utils/ofarc/ZooArchive.h +++ utils/ofarc/ZooArchive.h @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2008-2024 Jonathan Schleifer + * + * All rights reserved. + * + * This file is part of ObjFW. It may be distributed under the terms of the + * Q Public License 1.0, which can be found in the file LICENSE.QPL included in + * the packaging of this file. + * + * Alternatively, it may be distributed under the terms of the GNU General + * Public License, either version 2 or 3, which can be found in the file + * LICENSE.GPLv2 or LICENSE.GPLv3 respectively included in the packaging of this + * file. + */ + +#import "OFZooArchive.h" + +#import "Archive.h" + +@interface ZooArchive: OFObject +{ + OFZooArchive *_archive; +} +@end ADDED utils/ofarc/ZooArchive.m Index: utils/ofarc/ZooArchive.m ================================================================== --- utils/ofarc/ZooArchive.m +++ utils/ofarc/ZooArchive.m @@ -0,0 +1,334 @@ +/* + * Copyright (c) 2008-2024 Jonathan Schleifer + * + * All rights reserved. + * + * This file is part of ObjFW. It may be distributed under the terms of the + * Q Public License 1.0, which can be found in the file LICENSE.QPL included in + * the packaging of this file. + * + * Alternatively, it may be distributed under the terms of the GNU General + * Public License, either version 2 or 3, which can be found in the file + * LICENSE.GPLv2 or LICENSE.GPLv3 respectively included in the packaging of this + * file. + */ + +#include "config.h" + +#include + +#import "OFApplication.h" +#import "OFDate.h" +#import "OFFileManager.h" +#import "OFLocale.h" +#import "OFNumber.h" +#import "OFSet.h" +#import "OFStdIOStream.h" +#import "OFString.h" + +#import "ZooArchive.h" +#import "OFArc.h" + +#import "OFSetItemAttributesFailedException.h" + +static OFArc *app; + +static void +setModificationDate(OFString *path, OFZooArchiveEntry *entry) +{ + OFFileAttributes attributes = [OFDictionary + dictionaryWithObject: entry.modificationDate + forKey: OFFileModificationDate]; + @try { + [[OFFileManager defaultManager] setAttributes: attributes + ofItemAtPath: path]; + } @catch (OFSetItemAttributesFailedException *e) { + if (e.errNo != EISDIR) + @throw e; + } +} + +@implementation ZooArchive ++ (void)initialize +{ + if (self == [ZooArchive class]) + app = (OFArc *)[OFApplication sharedApplication].delegate; +} + ++ (instancetype)archiveWithPath: (OFString *)path + stream: (OF_KINDOF(OFStream *))stream + mode: (OFString *)mode + encoding: (OFStringEncoding)encoding +{ + return [[[self alloc] initWithPath: path + stream: stream + mode: mode + encoding: encoding] autorelease]; +} + +- (instancetype)initWithPath: (OFString *)path + stream: (OF_KINDOF(OFStream *))stream + mode: (OFString *)mode + encoding: (OFStringEncoding)encoding +{ + self = [super init]; + + @try { + _archive = [[OFZooArchive alloc] initWithStream: stream + mode: mode]; + + if (encoding != OFStringEncodingAutodetect) + _archive.encoding = encoding; + } @catch (id e) { + [self release]; + @throw e; + } + + return self; +} + +- (void)dealloc +{ + [_archive release]; + + [super dealloc]; +} + +- (void)listFiles +{ + OFZooArchiveEntry *entry; + + while ((entry = [_archive nextEntry]) != nil) { + void *pool = objc_autoreleasePoolPush(); + + [OFStdOut writeLine: entry.fileName]; + + if (app->_outputLevel >= 1) { + OFString *modificationDate = [entry.modificationDate + localDateStringWithFormat: @"%Y-%m-%d %H:%M:%S"]; + OFString *compressedSize = [OFString stringWithFormat: + @"%llu", entry.compressedSize]; + OFString *uncompressedSize = [OFString stringWithFormat: + @"%llu", entry.uncompressedSize]; + OFString *compressionMethod = [OFString + stringWithFormat: @"%u", entry.compressionMethod]; + OFString *CRC16 = [OFString stringWithFormat: + @"%04" PRIX16, entry.CRC16]; + OFString *deleted = [OFString stringWithFormat: + @"%u", entry.deleted]; + + [OFStdOut writeString: @"\t"]; + [OFStdOut writeLine: OF_LOCALIZED( + @"list_compressed_size", + @"[" + @" 'Compressed: '," + @" [" + @" {'size == 1': '1 byte'}," + @" {'': '%[size] bytes'}" + @" ]" + @"]".objectByParsingJSON, + @"size", compressedSize)]; + [OFStdOut writeString: @"\t"]; + [OFStdOut writeLine: OF_LOCALIZED( + @"list_uncompressed_size", + @"[" + @" 'Uncompressed: '," + @" [" + @" {'size == 1': '1 byte'}," + @" {'': '%[size] bytes'}" + @" ]" + @"]".objectByParsingJSON, + @"size", uncompressedSize)]; + [OFStdOut writeString: @"\t"]; + [OFStdOut writeLine: OF_LOCALIZED( + @"list_compression_method", + @"Compression method: %[method]", + @"method", compressionMethod)]; + [OFStdOut writeString: @"\t"]; + [OFStdOut writeLine: OF_LOCALIZED(@"list_crc16", + @"CRC16: %[crc16]", + @"crc16", CRC16)]; + [OFStdOut writeString: @"\t"]; + [OFStdOut writeLine: OF_LOCALIZED( + @"list_modification_date", + @"Modification date: %[date]", + @"date", modificationDate)]; + [OFStdOut writeString: @"\t"]; + [OFStdOut writeLine: OF_LOCALIZED( + @"list_deleted", + @"[" + @" 'Deleted: '," + @" [" + @" {'deleted == 0': 'No'}," + @" {'': 'Yes'}" + @" ]" + @"]".objectByParsingJSON, + @"deleted", deleted)]; + + if (entry.fileComment.length > 0) { + [OFStdOut writeString: @"\t"]; + [OFStdOut writeLine: OF_LOCALIZED( + @"list_comment", + @"Comment: %[comment]", + @"comment", entry.fileComment)]; + } + } + + objc_autoreleasePoolPop(pool); + } +} + +- (void)extractFiles: (OFArray OF_GENERIC(OFString *) *)files +{ + OFFileManager *fileManager = [OFFileManager defaultManager]; + bool all = (files.count == 0); + OFMutableSet OF_GENERIC(OFString *) *missing = + [OFMutableSet setWithArray: files]; + OFZooArchiveEntry *entry; + + while ((entry = [_archive nextEntry]) != nil) { + void *pool = objc_autoreleasePoolPush(); + OFString *fileName = entry.fileName; + OFString *outFileName, *directory; + OFFile *output; + OFStream *stream; + unsigned long long written = 0, size = entry.uncompressedSize; + int8_t percent = -1, newPercent; + + if (!all && ![files containsObject: fileName]) + continue; + + [missing removeObject: fileName]; + + outFileName = [app safeLocalPathForPath: fileName]; + if (outFileName == nil) { + [OFStdErr writeLine: OF_LOCALIZED( + @"refusing_to_extract_file", + @"Refusing to extract %[file]!", + @"file", fileName)]; + + app->_exitStatus = 1; + goto outer_loop_end; + } + + if (app->_outputLevel >= 0) + [OFStdOut writeString: OF_LOCALIZED(@"extracting_file", + @"Extracting %[file]...", + @"file", fileName)]; + + directory = outFileName.stringByDeletingLastPathComponent; + if (![fileManager directoryExistsAtPath: directory]) + [fileManager createDirectoryAtPath: directory + createParents: true]; + + if (![app shouldExtractFile: fileName outFileName: outFileName]) + goto outer_loop_end; + + stream = [_archive streamForReadingCurrentEntry]; + output = [OFFile fileWithPath: outFileName mode: @"w"]; + + while (!stream.atEndOfStream) { + ssize_t length = [app copyBlockFromStream: stream + toStream: output + fileName: fileName]; + + if (length < 0) { + app->_exitStatus = 1; + goto outer_loop_end; + } + + written += length; + newPercent = (written == size + ? 100 : (int8_t)(written * 100 / size)); + + if (app->_outputLevel >= 0 && percent != newPercent) { + OFString *percentString; + + percent = newPercent; + percentString = [OFString stringWithFormat: + @"%3u", percent]; + + [OFStdOut writeString: @"\r"]; + [OFStdOut writeString: OF_LOCALIZED( + @"extracting_file_percent", + @"Extracting %[file]... %[percent]%", + @"file", fileName, + @"percent", percentString)]; + } + } + + [output close]; + setModificationDate(outFileName, entry); + + if (app->_outputLevel >= 0) { + [OFStdOut writeString: @"\r"]; + [OFStdOut writeLine: OF_LOCALIZED( + @"extracting_file_done", + @"Extracting %[file]... done", + @"file", fileName)]; + } + +outer_loop_end: + objc_autoreleasePoolPop(pool); + } + + if (missing.count > 0) { + for (OFString *file in missing) + [OFStdErr writeLine: OF_LOCALIZED( + @"file_not_in_archive", + @"File %[file] is not in the archive!", + @"file", file)]; + + app->_exitStatus = 1; + } +} + +- (void)printFiles: (OFArray OF_GENERIC(OFString *) *)files_ +{ + OFMutableSet *files; + OFZooArchiveEntry *entry; + + if (files_.count < 1) { + [OFStdErr writeLine: OF_LOCALIZED(@"print_no_file_specified", + @"Need one or more files to print!")]; + app->_exitStatus = 1; + return; + } + + files = [OFMutableSet setWithArray: files_]; + + while ((entry = [_archive nextEntry]) != nil) { + OFString *fileName = entry.fileName; + OFStream *stream; + + if (![files containsObject: fileName]) + continue; + + stream = [_archive streamForReadingCurrentEntry]; + + while (!stream.atEndOfStream) { + ssize_t length = [app copyBlockFromStream: stream + toStream: OFStdOut + fileName: fileName]; + + if (length < 0) { + app->_exitStatus = 1; + return; + } + } + + [files removeObject: fileName]; + [stream close]; + + if (files.count == 0) + break; + } + + for (OFString *file in files) { + [OFStdErr writeLine: OF_LOCALIZED(@"file_not_in_archive", + @"File %[file] is not in the archive!", + @"file", file)]; + app->_exitStatus = 1; + } +} +@end Index: utils/ofarc/localization/de.json ================================================================== --- utils/ofarc/localization/de.json +++ utils/ofarc/localization/de.json @@ -16,20 +16,20 @@ full_usage: [ "Optionen:\n", " -a --append Zu Archiv hinzufügen\n", " -c --create Archiv erstellen\n", " -C --directory= In angegebenes Verzeichnis entpacken\n", - " -E --encoding= Das Encoding des Archivs (nur tar- und ", - "lha-Dateien)\n", + " -E --encoding= Das Encoding des Archivs (nur tar-, ", + "lha- und zoo-Dateien)\n", " -f --force Existierende Dateien überschreiben\n", " -h --help Diese Hilfe anzeigen\n", " -l --list Alle Dateien im Archiv auflisten\n", " -n --no-clobber Dateien niemals überschreiben\n", " -p --print Eine oder mehr Dateien aus dem Archiv ausgeben", "\n", " -q --quiet Ruhiger Modus (keine Ausgabe außer Fehler)\n", - " -t --type= Archiv-Typ (gz, lha, tar, tgz, zip)\n", + " -t --type= Archiv-Typ (gz, lha, tar, tgz, zip, zoo)\n", " -v --verbose Ausführlicher Modus für Datei-Liste\n", " -x --extract Dateien entpacken" ], "2_options_mutually_exclusive": [ "Fehler: -%[shortopt1] / --%[longopt1] und ", @@ -126,10 +126,17 @@ list_version_made_by: "Erstellt mit Version: %[version]", list_min_version_needed: "Mindestens benötigte Version: %[version]", list_general_purpose_bit_flag: "General Purpose Bit Flag: %[gpbf]", list_extra_field: "Extra-Feld: %[extra]", list_comment: "Kommentar: %[comment]", + list_deleted: [ + "Gelöscht: ", + [ + {"deleted == 0": "Nein"}, + {"": "Ja"} + ] + ], refusing_to_extract_file: "Verweigere Entpacken von %[file]!", file_not_in_archive: "Datei %[file] ist nicht im Archiv!", print_no_file_specified: [ "Benötige eine oder mehrere Dateien zum Ausgeben!" ],