mc/vfs/tar.c

577 lines
14 KiB
C

/* Virtual File System: GNU Tar file system.
Copyright (C) 1995 The Free Software Foundation
Written by: 1995 Jakub Jelinek
Rewritten by: 1998 Pavel Machek
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU Library General Public License
as published by the Free Software Foundation; either version 2 of
the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Library General Public License for more details.
You should have received a copy of the GNU Library General Public
License along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */
/* Namespace: vfs_tarfs_ops */
#include <config.h>
#include <errno.h>
#include "xdirentry.h"
#include "utilvfs.h"
#include "../src/dialog.h" /* For MSG_ERROR */
#include "tar.h"
#include "names.h"
#define isodigit(c) ( ((c) >= '0') && ((c) <= '7') )
/*
* Quick and dirty octal conversion.
*
* Result is -1 if the field is invalid (all blank, or nonoctal).
*/
static long from_oct (int digs, char *where)
{
register long value;
while (isspace (*where)) { /* Skip spaces */
where++;
if (--digs <= 0)
return -1; /* All blank field */
}
value = 0;
while (digs > 0 && isodigit (*where)) { /* Scan till nonoctal */
value = (value << 3) | (*where++ - '0');
--digs;
}
if (digs > 0 && *where && !isspace (*where))
return -1; /* Ended on non-space/nul */
return value;
}
static struct stat hstat; /* Stat struct corresponding */
static void tar_free_archive (vfs *me, vfs_s_super *archive)
{
if (archive->u.tar.fd != -1)
mc_close(archive->u.tar.fd);
}
/* As we open one archive at a time, it is safe to have this static */
static int current_tar_position = 0;
/* Returns fd of the open tar file */
static int tar_open_archive (vfs *me, char *name, vfs_s_super *archive)
{
int result, type;
long size;
mode_t mode;
struct vfs_s_inode *root;
result = mc_open (name, O_RDONLY);
if (result == -1) {
message_2s (1, MSG_ERROR, _("Couldn't open tar archive\n%s"), name);
ERRNOR (ENOENT, -1);
}
archive->name = g_strdup (name);
mc_stat (name, &(archive->u.tar.tarstat));
archive->u.tar.fd = -1;
/* Find out the method to handle this tar file */
size = is_gunzipable (result, &type);
mc_lseek (result, 0, SEEK_SET);
if (size > 0) {
char *s;
mc_close( result );
s = g_strconcat ( archive->name, decompress_extension (type), NULL );
result = mc_open (s, O_RDONLY);
if (result == -1)
message_2s (1, MSG_ERROR, _("Couldn't open tar archive\n%s"), s);
g_free(s);
if (result == -1)
ERRNOR (ENOENT, -1);
}
archive->u.tar.fd = result;
mode = archive->u.tar.tarstat.st_mode & 07777;
if (mode & 0400) mode |= 0100;
if (mode & 0040) mode |= 0010;
if (mode & 0004) mode |= 0001;
mode |= S_IFDIR;
root = vfs_s_new_inode (me, archive, &archive->u.tar.tarstat);
root->st.st_mode = mode;
root->u.tar.data_offset = -1;
root->st.st_nlink++;
root->st.st_dev = MEDATA->rdev++;
vfs_s_add_dots (me, root, NULL);
archive->root = root;
return result;
}
static union record rec_buf;
static union record *
get_next_record (vfs_s_super *archive, int tard)
{
int n;
n = mc_read (tard, rec_buf.charptr, RECORDSIZE);
if (n != RECORDSIZE)
return NULL; /* An error has occurred */
current_tar_position += RECORDSIZE;
return &rec_buf;
}
static void skip_n_records (vfs_s_super *archive, int tard, int n)
{
mc_lseek (tard, n * RECORDSIZE, SEEK_CUR);
current_tar_position += n * RECORDSIZE;
}
static void fill_stat_from_header (vfs *me, struct stat *st, union record *header)
{
st->st_mode = from_oct (8, header->header.mode);
/* Adjust st->st_mode because there are tar-files with
* linkflag==LF_SYMLINK and S_ISLNK(mod)==0. I don't
* know about the other modes but I think I cause no new
* problem when I adjust them, too. -- Norbert.
*/
if (header->header.linkflag == LF_DIR) {
st->st_mode |= S_IFDIR;
} else if (header->header.linkflag == LF_SYMLINK) {
st->st_mode |= S_IFLNK;
} else if (header->header.linkflag == LF_CHR) {
st->st_mode |= S_IFCHR;
} else if (header->header.linkflag == LF_BLK) {
st->st_mode |= S_IFBLK;
} else if (header->header.linkflag == LF_FIFO) {
st->st_mode |= S_IFIFO;
} else
st->st_mode |= S_IFREG;
st->st_rdev = 0;
if (!strcmp (header->header.magic, TMAGIC)) {
st->st_uid = *header->header.uname ? finduid (header->header.uname) :
from_oct (8, header->header.uid);
st->st_gid = *header->header.gname ? findgid (header->header.gname) :
from_oct (8, header->header.gid);
switch (header->header.linkflag) {
case LF_BLK:
case LF_CHR:
st->st_rdev = (from_oct (8, header->header.devmajor) << 8) |
from_oct (8, header->header.devminor);
}
} else { /* Old Unix tar */
st->st_uid = from_oct (8, header->header.uid);
st->st_gid = from_oct (8, header->header.gid);
}
st->st_size = hstat.st_size;
st->st_mtime = from_oct (1 + 12, header->header.mtime);
st->st_atime = from_oct (1 + 12, header->header.atime);
st->st_ctime = from_oct (1 + 12, header->header.ctime);
}
typedef enum {
STATUS_BADCHECKSUM,
STATUS_SUCCESS,
STATUS_EOFMARK,
STATUS_EOF
} ReadStatus;
/*
* Return 1 for success, 0 if the checksum is bad, EOF on eof,
* 2 for a record full of zeros (EOF marker).
*
*/
static ReadStatus
read_header (vfs *me, vfs_s_super *archive, int tard)
{
register int i;
register long sum, signed_sum, recsum;
register char *p;
register union record *header;
static char *next_long_name = NULL, *next_long_link = NULL;
recurse:
header = get_next_record (archive, tard);
if (NULL == header)
return STATUS_EOF;
recsum = from_oct (8, header->header.chksum);
sum = 0; signed_sum = 0;
p = header->charptr;
for (i = sizeof (*header); --i >= 0;) {
/*
* We can't use unsigned char here because of old compilers,
* e.g. V7.
*/
signed_sum += *p;
sum += 0xFF & *p++;
}
/* Adjust checksum to count the "chksum" field as blanks. */
for (i = sizeof (header->header.chksum); --i >= 0;) {
sum -= 0xFF & header->header.chksum[i];
signed_sum -= (char) header->header.chksum[i];
}
sum += ' ' * sizeof header->header.chksum;
signed_sum += ' ' * sizeof header->header.chksum;
/*
* This is a zeroed record...whole record is 0's except
* for the 8 blanks we faked for the checksum field.
*/
if (sum == 8 * ' ')
return STATUS_EOFMARK;
if (sum != recsum && signed_sum != recsum)
return STATUS_BADCHECKSUM;
/*
* linkflag on BSDI tar (pax) always '\000'
*/
if (header->header.linkflag == '\000' &&
(i = strlen(header->header.arch_name)) &&
header->header.arch_name[i - 1] == '/')
header->header.linkflag = LF_DIR;
/*
* Good record. Decode file size and return.
*/
if (header->header.linkflag == LF_LINK || header->header.linkflag == LF_DIR)
hstat.st_size = 0; /* Links 0 size on tape */
else
hstat.st_size = from_oct (1 + 12, header->header.size);
header->header.arch_name[NAMSIZ - 1] = '\0';
if (header->header.linkflag == LF_LONGNAME
|| header->header.linkflag == LF_LONGLINK) {
char **longp;
char *bp, *data;
int size, written;
longp = ((header->header.linkflag == LF_LONGNAME)
? &next_long_name
: &next_long_link);
if (*longp)
g_free (*longp);
bp = *longp = g_malloc (hstat.st_size);
for (size = hstat.st_size;
size > 0;
size -= written) {
data = get_next_record (archive, tard)->charptr;
if (data == NULL) {
message_1s (1, MSG_ERROR, _("Unexpected EOF on archive file"));
return STATUS_BADCHECKSUM;
}
written = RECORDSIZE;
if (written > size)
written = size;
bcopy (data, bp, written);
bp += written;
}
#if 0
if (hstat.st_size > 1)
bp [hstat.st_size - 1] = 0; /* just to make sure */
#endif
goto recurse;
} else {
struct stat st;
struct vfs_s_entry *entry;
struct vfs_s_inode *inode, *parent;
long data_position;
char *q;
int len;
char *current_file_name, *current_link_name;
current_link_name = (next_long_link
? next_long_link
: g_strdup (header->header.arch_linkname));
len = strlen (current_link_name);
if (len > 1 && current_link_name [len - 1] == '/')
current_link_name[len - 1] = 0;
current_file_name = (next_long_name
? next_long_name
: g_strdup (header->header.arch_name));
len = strlen (current_file_name);
if (current_file_name[len - 1] == '/') {
current_file_name[len - 1] = 0;
}
data_position = current_tar_position;
p = strrchr (current_file_name, '/');
if (p == NULL) {
p = current_file_name;
q = current_file_name + len; /* "" */
} else {
*(p++) = 0;
q = current_file_name;
}
parent = vfs_s_find_inode (me, archive->root, q, LINK_NO_FOLLOW, FL_MKDIR);
if (parent == NULL) {
message_1s (1, MSG_ERROR, _("Inconsistent tar archive"));
return STATUS_BADCHECKSUM;
}
if (header->header.linkflag == LF_LINK) {
inode = vfs_s_find_inode (me, archive->root, current_link_name, LINK_NO_FOLLOW, 0);
if (inode == NULL) {
message_1s (1, MSG_ERROR, _("Inconsistent tar archive"));
} else {
entry = vfs_s_new_entry(me, p, inode);
vfs_s_insert_entry(me, parent, entry);
g_free (current_link_name);
goto done;
}
}
fill_stat_from_header (me, &st, header);
inode = vfs_s_new_inode (me, archive, &st);
inode->u.tar.data_offset = data_position;
if (*current_link_name) {
inode->linkname = current_link_name;
} else if (current_link_name != next_long_link) {
g_free (current_link_name);
}
entry = vfs_s_new_entry (me, p, inode);
vfs_s_insert_entry (me, parent, entry);
g_free (current_file_name);
done:
next_long_link = next_long_name = NULL;
if (header->header.isextended) {
while (get_next_record (archive, tard)->ext_hdr.isextended);
inode->u.tar.data_offset = current_tar_position;
}
return STATUS_SUCCESS;
}
}
/*
* Main loop for reading an archive.
* Returns 0 on success, -1 on error.
*/
static int open_archive (vfs *me, vfs_s_super *archive, char *name, char *op)
{
ReadStatus status = STATUS_EOFMARK; /* Initial status at start of archive */
ReadStatus prev_status;
int tard;
current_tar_position = 0;
if ((tard = tar_open_archive (me, name, archive)) == -1) /* Open for reading */
return -1;
for (;;) {
prev_status = status;
status = read_header (me, archive, tard);
switch (status) {
case STATUS_SUCCESS:
skip_n_records (archive, tard, (hstat.st_size + RECORDSIZE - 1) / RECORDSIZE);
continue;
/*
* Invalid header:
*
* If the previous header was good, tell them
* that we are skipping bad ones.
*/
case STATUS_BADCHECKSUM:
switch (prev_status){
/* Error on first record */
case STATUS_EOFMARK:
message_2s (1, MSG_ERROR, _("Hmm,...\n%s\ndoesn't look like a tar archive."), name);
/* FALL THRU */
/* Error after header rec */
case STATUS_SUCCESS:
/* Error after error */
case STATUS_BADCHECKSUM:
return -1;
case STATUS_EOF:
return 0;
}
/* Record of zeroes */
case STATUS_EOFMARK:
status = prev_status; /* If error after 0's */
/* FALL THRU */
case STATUS_EOF: /* End of archive */
break;
}
break;
};
return 0;
}
static void *tar_super_check(vfs *me, char *archive_name, char *op)
{
static struct stat stat_buf;
if (mc_stat (archive_name, &stat_buf))
return NULL;
return &stat_buf;
}
static int tar_super_same(vfs *me, struct vfs_s_super *parc, char *archive_name, char *op, void *cookie)
{
struct stat *archive_stat = cookie; /* stat of main archive */
if (strcmp (parc->name, archive_name)) return 0;
if (vfs_uid && (!(archive_stat->st_mode & 0004)))
if ((archive_stat->st_gid != vfs_gid) || !(archive_stat->st_mode & 0040))
if ((archive_stat->st_uid != vfs_uid) || !(archive_stat->st_mode & 0400))
return 0;
/* Has the cached archive been changed on the disk? */
if (parc->u.tar.tarstat.st_mtime < archive_stat->st_mtime) { /* Yes, reload! */
(*vfs_tarfs_ops.free) ((vfsid) parc);
vfs_rmstamp (&vfs_tarfs_ops, (vfsid) parc, 0);
return 2;
}
/* Hasn't been modified, give it a new timeout */
vfs_stamp (&vfs_tarfs_ops, (vfsid) parc);
return 1;
}
static int tar_read (void *fh, char *buffer, int count)
{
off_t begin = FH->ino->u.tar.data_offset;
int fd = FH_SUPER->u.tar.fd;
vfs *me = FH_SUPER->me;
if (mc_lseek (fd, begin + FH->pos, SEEK_SET) !=
begin + FH->pos) ERRNOR (EIO, -1);
count = MIN(count, FH->ino->st.st_size - FH->pos);
if ((count = mc_read (fd, buffer, count)) == -1) ERRNOR (errno, -1);
FH->pos += count;
return count;
}
static int tar_ungetlocalcopy (vfs *me, char *path, char *local, int has_changed)
{
/* We do just nothing. (We are read only and do not need to free local,
since it will be freed when tar archive will be freed */
/* We have to report error if file has changed */
ERRNOR (EROFS, -has_changed);
}
static int tar_fh_open (vfs *me, vfs_s_fh *fh, int flags, int mode)
{
if ((flags & O_ACCMODE) != O_RDONLY) ERRNOR (EROFS, -1);
return 0;
}
static struct vfs_s_data tarfs_data = {
NULL,
0,
0,
NULL, /* logfile */
NULL, /* init_inode */
NULL, /* free_inode */
NULL, /* init_entry */
tar_super_check,
tar_super_same,
open_archive,
tar_free_archive,
tar_fh_open, /* fh_open */
NULL, /* fh_close */
vfs_s_find_entry_tree,
NULL,
NULL
};
vfs vfs_tarfs_ops =
{
NULL, /* This is place of next pointer */
"tarfs",
0, /* flags */
"utar", /* prefix */
&tarfs_data,
0, /* errno */
NULL,
NULL,
vfs_s_fill_names,
NULL,
vfs_s_open,
vfs_s_close,
tar_read,
NULL,
vfs_s_opendir,
vfs_s_readdir,
vfs_s_closedir,
vfs_s_telldir,
vfs_s_seekdir,
vfs_s_stat,
vfs_s_lstat,
vfs_s_fstat,
NULL,
NULL,
NULL,
vfs_s_readlink,
NULL,
NULL,
NULL,
NULL,
vfs_s_chdir,
vfs_s_ferrno,
vfs_s_lseek,
NULL,
vfs_s_getid,
vfs_s_nothingisopen,
vfs_s_free,
vfs_s_getlocalcopy,
tar_ungetlocalcopy,
NULL, /* mkdir */
NULL,
NULL,
NULL
MMAPNULL
};