394 lines
9.4 KiB
C
394 lines
9.4 KiB
C
/**
|
|
* @brief tar - extract archives
|
|
*
|
|
* This is a very minimal and incomplete implementation of tar.
|
|
* It supports on ustar-formatted archives, and its arguments
|
|
* must be the - forms. As of writing, creating archives is not
|
|
* supported. Decompression is supported by piping through gunzip.
|
|
*
|
|
* @copyright
|
|
* This file is part of ToaruOS and is released under the terms
|
|
* of the NCSA / University of Illinois License - see LICENSE.md
|
|
* Copyright (C) 2018-2020 K. Lange
|
|
*/
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
#include <getopt.h>
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/types.h>
|
|
|
|
#include <toaru/inflate.h>
|
|
|
|
struct ustar {
|
|
char filename[100];
|
|
char mode[8];
|
|
char ownerid[8];
|
|
char groupid[8];
|
|
|
|
char size[12];
|
|
char mtime[12];
|
|
|
|
char checksum[8];
|
|
char type[1];
|
|
char link[100];
|
|
|
|
char ustar[6];
|
|
char version[2];
|
|
|
|
char owner[32];
|
|
char group[32];
|
|
|
|
char dev_major[8];
|
|
char dev_minor[8];
|
|
|
|
char prefix[155];
|
|
char padding[12];
|
|
};
|
|
|
|
static struct ustar * extract_file(FILE * f) {
|
|
static struct ustar _ustar;
|
|
if (fread(&_ustar, 1, sizeof(struct ustar), f) != sizeof(struct ustar)) {
|
|
fprintf(stderr, "failed to read file\n");
|
|
return NULL;
|
|
}
|
|
|
|
if (_ustar.ustar[0] != 'u' ||
|
|
_ustar.ustar[1] != 's' ||
|
|
_ustar.ustar[2] != 't' ||
|
|
_ustar.ustar[3] != 'a' ||
|
|
_ustar.ustar[4] != 'r') {
|
|
return NULL;
|
|
}
|
|
return &_ustar;
|
|
}
|
|
|
|
static unsigned int interpret_mode(struct ustar * file) {
|
|
return
|
|
((file->mode[0] - '0') << 18) |
|
|
((file->mode[1] - '0') << 15) |
|
|
((file->mode[2] - '0') << 12) |
|
|
((file->mode[3] - '0') << 9) |
|
|
((file->mode[4] - '0') << 6) |
|
|
((file->mode[5] - '0') << 3) |
|
|
((file->mode[6] - '0') << 0);
|
|
}
|
|
|
|
static unsigned int interpret_size(struct ustar * file) {
|
|
if (file->size[0] != '0') {
|
|
fprintf(stderr, "\033[3;32mWarning:\033[0;3m File is too big.\033[0m\n");
|
|
}
|
|
return
|
|
((file->size[ 0] - '0') << 30) |
|
|
((file->size[ 1] - '0') << 27) |
|
|
((file->size[ 2] - '0') << 24) |
|
|
((file->size[ 3] - '0') << 21) |
|
|
((file->size[ 4] - '0') << 18) |
|
|
((file->size[ 5] - '0') << 15) |
|
|
((file->size[ 6] - '0') << 12) |
|
|
((file->size[ 7] - '0') << 9) |
|
|
((file->size[ 8] - '0') << 6) |
|
|
((file->size[ 9] - '0') << 3) |
|
|
((file->size[10] - '0') << 0);
|
|
}
|
|
|
|
static const char * type_to_string(char type) {
|
|
static char unknown[100];
|
|
switch (type) {
|
|
case '\0':
|
|
case '0':
|
|
return "Normal file";
|
|
case '1':
|
|
return "Hard link (unsupported)";
|
|
case '2':
|
|
return "Symolic link";
|
|
case '3':
|
|
return "Character special (unsupported)";
|
|
case '4':
|
|
return "Block special (unsupported)";
|
|
case '5':
|
|
return "Directory";
|
|
case '6':
|
|
return "FIFO (unsupported)";
|
|
case 'g':
|
|
return "Extended header";
|
|
case 'x':
|
|
return "Extended preheader";
|
|
default:
|
|
sprintf(unknown, "Uknown: %c", type);
|
|
return unknown;
|
|
}
|
|
}
|
|
|
|
#if 0
|
|
static void dump_file(struct ustar * file) {
|
|
fprintf(stdout, "\033[1m%.155s%.100s\033[0m\n", file->prefix, file->filename);
|
|
fprintf(stdout, "%c - %s\n", file->type[0], type_to_string(file->type[0]));
|
|
fprintf(stdout, "File size: %u\n", interpret_size(file));
|
|
}
|
|
#endif
|
|
|
|
#define CHUNK_SIZE 4096
|
|
static void write_file(struct ustar * file, FILE * f, FILE * mf, char * name) {
|
|
size_t length = interpret_size(file);
|
|
char buf[CHUNK_SIZE];
|
|
while (length > CHUNK_SIZE) {
|
|
fread( buf, 1, CHUNK_SIZE, f);
|
|
fwrite(buf, 1, CHUNK_SIZE, mf);
|
|
length -= CHUNK_SIZE;
|
|
}
|
|
if (length > 0) {
|
|
fread( buf, 1, length, f);
|
|
fwrite(buf, 1, length, mf);
|
|
}
|
|
fflush(mf);
|
|
if (mf != stdout) {
|
|
fclose(mf);
|
|
chmod(name, interpret_mode(file));
|
|
}
|
|
}
|
|
|
|
static void _seek_forward(FILE * f, size_t amount) {
|
|
for (size_t i = 0; i < amount; ++i) {
|
|
fgetc(f);
|
|
}
|
|
}
|
|
|
|
static void usage(char * argv[]) {
|
|
fprintf(stderr,
|
|
"tar - extract ustar archives\n"
|
|
"\n"
|
|
"usage: %s [-ctxvaf] [name]\n"
|
|
"\n"
|
|
" -f \033[3mfile archive to open\033[0m\n"
|
|
" -x \033[3mextract\033[0m\n"
|
|
"\n", argv[0]);
|
|
}
|
|
|
|
static int matches_files(int argc, char * argv[], int optind, char * filename) {
|
|
while (optind < argc) {
|
|
if (!strcmp(argv[optind], filename)) return 1;
|
|
optind++;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int main(int argc, char * argv[]) {
|
|
|
|
int opt;
|
|
char * fname = NULL;
|
|
int verbose = 0;
|
|
int action = 0;
|
|
int compressed = 0;
|
|
int to_stdout = 0;
|
|
int only_matches = 0;
|
|
#define TAR_ACTION_EXTRACT 1
|
|
#define TAR_ACTION_CREATE 2
|
|
#define TAR_ACTION_LIST 3
|
|
|
|
while ((opt = getopt(argc, argv, "?ctxzvaf:O")) != -1) {
|
|
switch (opt) {
|
|
case 'c':
|
|
if (action) {
|
|
fprintf(stderr, "%s: %c: already specified action\n", argv[0], opt);
|
|
return 1;
|
|
}
|
|
action = TAR_ACTION_CREATE;
|
|
break;
|
|
case 'f':
|
|
fname = optarg;
|
|
break;
|
|
case 'x':
|
|
if (action) {
|
|
fprintf(stderr, "%s: %c: already specified action\n", argv[0], opt);
|
|
return 1;
|
|
}
|
|
action = TAR_ACTION_EXTRACT;
|
|
break;
|
|
case 't':
|
|
if (action) {
|
|
fprintf(stderr, "%s: %c: already specified action\n", argv[0], opt);
|
|
return 1;
|
|
}
|
|
action = TAR_ACTION_LIST;
|
|
break;
|
|
case 'v':
|
|
verbose = 1;
|
|
break;
|
|
case 'z':
|
|
compressed = 1;
|
|
break;
|
|
case 'O':
|
|
to_stdout = 1;
|
|
break;
|
|
case '?':
|
|
usage(argv);
|
|
return 1;
|
|
default:
|
|
fprintf(stderr, "%s: unsupported option '%c'\n", argv[0], opt);
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
if (!fname) {
|
|
fname = "-";
|
|
}
|
|
|
|
if (optind < argc) {
|
|
only_matches = 1;
|
|
}
|
|
|
|
if (action == TAR_ACTION_EXTRACT || action == TAR_ACTION_LIST) {
|
|
|
|
FILE * f;
|
|
if (!strcmp(fname,"-")) {
|
|
f = stdin;
|
|
} else {
|
|
f = fopen(fname,"r");
|
|
}
|
|
if (!f) {
|
|
fprintf(stderr, "%s: %s: %s\n", argv[0], fname, strerror(errno));
|
|
return 1;
|
|
}
|
|
|
|
if (compressed) {
|
|
int fds[2];
|
|
pipe(fds);
|
|
|
|
int child = fork();
|
|
if (child == 0) {
|
|
/* Close the read end */
|
|
close(fds[0]);
|
|
/* Put f's fd into stdin */
|
|
dup2(fileno(f), STDIN_FILENO);
|
|
/* Make stdout the pipe */
|
|
dup2(fds[1], STDOUT_FILENO);
|
|
/* Execeute gzunzip */
|
|
char * args[] = {"gunzip","-c",NULL};
|
|
exit(execvp("gunzip",args));
|
|
} else if (child < 0) {
|
|
fprintf(stderr, "%s: failed to fork gunzip for compressed archive\n", argv[0]);
|
|
return 1;
|
|
}
|
|
|
|
/* Reattach f to pipe */
|
|
close(fds[1]);
|
|
f = fdopen(fds[0], "r");
|
|
}
|
|
|
|
char tmpname[1024] = {0};
|
|
int last_was_long = 0;
|
|
|
|
while (!feof(f)) {
|
|
struct ustar * file = extract_file(f);
|
|
|
|
if (!file) {
|
|
break;
|
|
}
|
|
|
|
if (action == TAR_ACTION_LIST) {
|
|
if (verbose) {
|
|
fprintf(stdout, "%10d %c %.155s%.100s\n", interpret_size(file), file->type[0], file->prefix, file->filename);
|
|
} else {
|
|
fprintf(stdout, "%.155s%.100s\n", file->prefix, file->filename);
|
|
}
|
|
_seek_forward(f, interpret_size(file));
|
|
} else if (action == TAR_ACTION_EXTRACT) {
|
|
if (verbose) {
|
|
fprintf(stdout, "%.155s%.100s\n", file->prefix, file->filename);
|
|
}
|
|
char name[1025] = {0};
|
|
if (last_was_long) {
|
|
strncat(name, tmpname, 1024);
|
|
last_was_long = 0;
|
|
} else {
|
|
strncat(name, file->prefix, 167);
|
|
strncat(name, file->filename, 512);
|
|
}
|
|
|
|
if (file->type[0] == '0' || file->type[0] == 0) {
|
|
FILE * mf = to_stdout ? stdout : fopen(name,"w");
|
|
if (!mf) {
|
|
fprintf(stderr, "%s: %s: %s: %s\n", argv[0], fname, name, strerror(errno));
|
|
_seek_forward(f, interpret_size(file));
|
|
} else {
|
|
if (!only_matches || matches_files(argc,argv,optind,name)) {
|
|
write_file(file,f,mf,name);
|
|
}
|
|
}
|
|
} else if (file->type[0] == '5') {
|
|
if (!to_stdout) {
|
|
if (name[strlen(name)-1] == '/') {
|
|
name[strlen(name)-1] = '\0';
|
|
}
|
|
if (strlen(name)) {
|
|
if (!only_matches || matches_files(argc,argv,optind,name)) {
|
|
if (mkdir(name, 0777) < 0) {
|
|
if (errno != EEXIST) {
|
|
fprintf(stderr, "%s: %s: %s: %s\n", argv[0], fname, name, strerror(errno));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else if (file->type[0] == '1') {
|
|
if (!to_stdout && (!only_matches || matches_files(argc,argv,optind,name))) {
|
|
char tmp[356] = {0};
|
|
strncat(tmp, file->link, 355);
|
|
FILE * mf = fopen(name,"w");
|
|
if (!mf) {
|
|
fprintf(stderr, "%s: %s: %s: %s\n", argv[0], fname, name, strerror(errno));
|
|
} else {
|
|
FILE * source = fopen(tmp, "r");
|
|
if (!source) {
|
|
fprintf(stderr, "%s: %s: %s: %s\n", argv[0], fname, tmp, strerror(errno));
|
|
} else {
|
|
while (!feof(source)) {
|
|
char buf[4096];
|
|
ssize_t r = fread(buf, 1, 4096, source);
|
|
fwrite(buf, 1, r, mf);
|
|
}
|
|
fclose(source);
|
|
}
|
|
fclose(mf);
|
|
chmod(name, interpret_mode(file));
|
|
}
|
|
}
|
|
_seek_forward(f, interpret_size(file));
|
|
} else if (file->type[0] == '2') {
|
|
if (!to_stdout && (!only_matches || matches_files(argc,argv,optind,name))) {
|
|
char tmp[356] = {0};
|
|
strncat(tmp, file->link, 355);
|
|
if (symlink(tmp, name) < 0) {
|
|
fprintf(stderr, "%s: %s: %s: %s: %s\n", argv[0], fname, name, tmp, strerror(errno));
|
|
}
|
|
}
|
|
_seek_forward(f, interpret_size(file));
|
|
} else if (file->type[0] == 'L') {
|
|
/* This is a GNU Long Name block; store its contents as a file name */
|
|
size_t s = interpret_size(file);
|
|
fread(tmpname, 1, s, f);
|
|
tmpname[s] = '\0';
|
|
last_was_long = 1;
|
|
} else {
|
|
fprintf(stderr, "%s: %s: %s: %s\n", argv[0], fname, name, type_to_string(file->type[0]));
|
|
_seek_forward(f, interpret_size(file));
|
|
}
|
|
}
|
|
|
|
size_t file_size = interpret_size(file);
|
|
if (file_size % 512) {
|
|
_seek_forward(f, 512 - (file_size % 512));
|
|
}
|
|
}
|
|
} else {
|
|
fprintf(stderr, "%s: unsupported action\n", argv[0]);
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|