/*
* Copyright (c) 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016
* Jonathan Schleifer <js@heap.zone>
*
* 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"
#import "OFDate.h"
#import "OFSet.h"
#import "OFApplication.h"
#import "OFFileManager.h"
#import "OFStdIOStream.h"
#import "TarArchive.h"
#import "OFZIP.h"
#ifndef S_IRWXG
# define S_IRWXG 0
#endif
#ifndef S_IRWXO
# define S_IRWXO 0
#endif
static OFZIP *app;
static void
setPermissions(OFString *path, OFTarArchiveEntry *entry)
{
#ifdef OF_HAVE_CHMOD
uint32_t mode = [entry mode];
/* Only allow modes that are safe */
mode &= (S_IRWXU | S_IRWXG | S_IRWXO);
[[OFFileManager defaultManager]
changePermissionsOfItemAtPath: path
permissions: mode];
#endif
}
@implementation TarArchive
+ (void)initialize
{
if (self == [TarArchive class])
app = (OFZIP*)[[OFApplication sharedApplication] delegate];
}
+ (instancetype)archiveWithStream: (OF_KINDOF(OFStream*))stream
{
return [[[self alloc] initWithStream: stream] autorelease];
}
- initWithStream: (OF_KINDOF(OFStream*))stream
{
self = [super init];
@try {
_archive = [[OFTarArchive alloc] initWithStream: stream];
} @catch (id e) {
[self release];
@throw e;
}
return self;
}
- (void)dealloc
{
[_archive release];
[super dealloc];
}
- (void)listFiles
{
OFTarArchiveEntry *entry;
while ((entry = [_archive nextEntry]) != nil) {
void *pool = objc_autoreleasePoolPush();
[of_stdout writeLine: [entry fileName]];
if (app->_outputLevel >= 1) {
OFString *date = [[entry modificationDate]
localDateStringWithFormat: @"%Y-%m-%d %H:%M:%S"];
[of_stdout writeFormat:
@"\tSize: %" PRIu64 @" bytes\n"
@"\tMode: %06o\n",
[entry size], [entry mode]];
if ([entry owner] != nil)
[of_stdout writeFormat: @"\tOwner: %@\n",
[entry owner]];
if ([entry group] != nil)
[of_stdout writeFormat: @"\tGroup: %@\n",
[entry group]];
[of_stdout writeFormat: @"\tModification date: %@\n",
date];
}
if (app->_outputLevel >= 2) {
switch ([entry type]) {
case OF_TAR_ARCHIVE_ENTRY_TYPE_FILE:
[of_stdout writeLine: @"\tType: Normal file"];
break;
case OF_TAR_ARCHIVE_ENTRY_TYPE_LINK:
[of_stdout writeLine: @"\tType: Hard link"];
[of_stdout writeFormat:
@"\tTarget file name: %@\n",
[entry targetFileName]];
break;
case OF_TAR_ARCHIVE_ENTRY_TYPE_SYMLINK:
[of_stdout writeLine: @"\tType: Symbolic link"];
[of_stdout writeFormat:
@"\tTarget file name: %@\n",
[entry targetFileName]];
break;
case OF_TAR_ARCHIVE_ENTRY_TYPE_CHARACTER_DEVICE:
[of_stdout writeLine:
@"\tType: Character device"];
[of_stdout writeFormat: @"\tDevice major: %d\n"
@"\tDevice minor: %d\n",
[entry deviceMajor],
[entry deviceMinor]];
break;
case OF_TAR_ARCHIVE_ENTRY_TYPE_BLOCK_DEVICE:
[of_stdout writeLine:
@"\tType: Block device"];
[of_stdout writeFormat: @"\tDevice major: %d\n"
@"\tDevice minor: %d\n",
[entry deviceMajor],
[entry deviceMinor]];
break;
case OF_TAR_ARCHIVE_ENTRY_TYPE_DIRECTORY:
[of_stdout writeLine: @"\tType: Directory"];
break;
case OF_TAR_ARCHIVE_ENTRY_TYPE_FIFO:
[of_stdout writeLine: @"\tType: FIFO"];
break;
case OF_TAR_ARCHIVE_ENTRY_TYPE_CONTIGUOUS_FILE:
[of_stdout writeLine:
@"\tType: Contiguous file"];
break;
default:
[of_stdout writeLine: @"\tType: Unknown"];
break;
}
}
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];
OFTarArchiveEntry *entry;
while ((entry = [_archive nextEntry]) != nil) {
void *pool = objc_autoreleasePoolPush();
OFString *fileName = [entry fileName];
OFString *outFileName = [fileName stringByStandardizingPath];
OFArray OF_GENERIC(OFString*) *pathComponents;
OFString *directory;
OFFile *output;
uint64_t written = 0, size = [entry size];
int8_t percent = -1, newPercent;
if (!all && ![files containsObject: fileName])
continue;
if ([entry type] != OF_TAR_ARCHIVE_ENTRY_TYPE_FILE) {
if (app->_outputLevel >= 0)
[of_stdout writeFormat: @"Skipping %@...\n",
fileName];
continue;
}
[missing removeObject: fileName];
#if !defined(OF_WINDOWS) && !defined(OF_MSDOS)
if ([outFileName hasPrefix: @"/"]) {
#else
if ([outFileName hasPrefix: @"/"] ||
[outFileName containsString: @":"]) {
#endif
[of_stderr writeFormat: @"Refusing to extract %@!\n",
fileName];
app->_exitStatus = 1;
goto outer_loop_end;
}
pathComponents = [outFileName pathComponents];
for (OFString *component in pathComponents) {
if ([component isEqual: OF_PATH_PARENT_DIRECTORY]) {
[of_stderr writeFormat:
@"Refusing to extract %@!\n", fileName];
app->_exitStatus = 1;
goto outer_loop_end;
}
}
outFileName = [OFString pathWithComponents: pathComponents];
if (app->_outputLevel >= 0)
[of_stdout writeFormat: @"Extracting %@...", fileName];
if ([fileName hasSuffix: @"/"]) {
[fileManager createDirectoryAtPath: outFileName
createParents: true];
setPermissions(outFileName, entry);
if (app->_outputLevel >= 0)
[of_stdout writeLine: @" done"];
goto outer_loop_end;
}
directory = [outFileName stringByDeletingLastPathComponent];
if (![fileManager directoryExistsAtPath: directory])
[fileManager createDirectoryAtPath: directory
createParents: true];
if (![app shouldExtractFile: fileName
outFileName: outFileName])
goto outer_loop_end;
output = [OFFile fileWithPath: outFileName
mode: @"wb"];
setPermissions(outFileName, entry);
while (![entry isAtEndOfStream]) {
ssize_t length = [app copyBlockFromStream: entry
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) {
percent = newPercent;
[of_stdout writeFormat:
@"\rExtracting %@... %3u%%",
fileName, percent];
}
}
if (app->_outputLevel >= 0)
[of_stdout writeFormat: @"\rExtracting %@... done\n",
fileName];
outer_loop_end:
objc_autoreleasePoolPop(pool);
}
if ([missing count] > 0) {
for (OFString *file in missing)
[of_stderr writeFormat:
@"File %@ is not in the archive!\n", file];
app->_exitStatus = 1;
}
}
- (void)printFiles: (OFArray OF_GENERIC(OFString*)*)files_
{
OFMutableSet *files;
OFTarArchiveEntry *entry;
if ([files_ count] < 1) {
[of_stderr writeLine: @"Need one or more files to print!"];
app->_exitStatus = 1;
return;
}
files = [OFMutableSet setWithArray: files_];
while ((entry = [_archive nextEntry]) != nil) {
OFString *fileName = [entry fileName];
if (![files containsObject: fileName])
continue;
while (![entry isAtEndOfStream]) {
ssize_t length = [app copyBlockFromStream: entry
toStream: of_stdout
fileName: fileName];
if (length < 0) {
app->_exitStatus = 1;
return;
}
}
[files removeObject: fileName];
[entry close];
}
for (OFString *path in files) {
[of_stderr writeFormat: @"File %@ is not in the archive!\n",
path];
app->_exitStatus = 1;
}
}
@end