/* 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.  */

/* Namespace: init_tarfs */

#include <config.h>
#include <sys/types.h>
#include <errno.h>
#include <ctype.h>

#ifdef hpux
/* major() and minor() macros (among other things) defined here for hpux */
#include <sys/mknod.h>
#endif

#include "../src/global.h"
#include "../src/tty.h"		/* enable/disable interrupt key */
#include "../src/wtools.h"	/* message() */
#include "../src/main.h"	/* print_vfs_message */
#include "utilvfs.h"
#include "gc.h"		/* vfs_rmstamp */
#include "xdirentry.h"

static struct vfs_class vfs_tarfs_ops;

/*
 * Header block on tape.
 *
 * I'm going to use traditional DP naming conventions here.
 * A "block" is a big chunk of stuff that we do I/O on.
 * A "record" is a piece of info that we care about.
 * Typically many "record"s fit into a "block".
 */
#define	RECORDSIZE	512
#define	NAMSIZ		100
#define	TUNMLEN		32
#define	TGNMLEN		32
#define SPARSE_EXT_HDR  21
#define SPARSE_IN_HDR	4

struct sparse {
    char offset[12];
    char numbytes[12];
};

struct sp_array {
    int offset;
    int numbytes;
};

union record {
    char charptr[RECORDSIZE];
    struct header {
	char arch_name[NAMSIZ];
	char mode[8];
	char uid[8];
	char gid[8];
	char size[12];
	char mtime[12];
	char chksum[8];
	char linkflag;
	char arch_linkname[NAMSIZ];
	char magic[8];
	char uname[TUNMLEN];
	char gname[TGNMLEN];
	char devmajor[8];
	char devminor[8];
	/* these following fields were added by JF for gnu */
	/* and are NOT standard */
	char atime[12];
	char ctime[12];
	char offset[12];
	char longnames[4];
	char pad;
	struct sparse sp[SPARSE_IN_HDR];
	char isextended;
	char realsize[12];	/* true size of the sparse file */
	/* char	ending_blanks[12];*//* number of nulls at the
	   end of the file, if any */
    } header;
    struct extended_header {
	struct sparse sp[21];
	char isextended;
    } ext_hdr;
};

/* The checksum field is filled with this while the checksum is computed. */
#define	CHKBLANKS	"        "	/* 8 blanks, no null */

/* The magic field is filled with this if uname and gname are valid. */
#define	TMAGIC		"ustar  "	/* 7 chars and a null */

/* The linkflag defines the type of file */
#define	LF_OLDNORMAL	'\0'	/* Normal disk file, Unix compat */
#define	LF_NORMAL	'0'	/* Normal disk file */
#define	LF_LINK		'1'	/* Link to previously dumped file */
#define	LF_SYMLINK	'2'	/* Symbolic link */
#define	LF_CHR		'3'	/* Character special file */
#define	LF_BLK		'4'	/* Block special file */
#define	LF_DIR		'5'	/* Directory */
#define	LF_FIFO		'6'	/* FIFO special file */
#define	LF_CONTIG	'7'	/* Contiguous file */
/* Further link types may be defined later. */

/* Note that the standards committee allows only capital A through
   capital Z for user-defined expansion.  This means that defining something
   as, say '8' is a *bad* idea. */
#define LF_DUMPDIR	'D'	/* This is a dir entry that contains
					   the names of files that were in
					   the dir at the time the dump
					   was made */
#define LF_LONGLINK	'K'	/* Identifies the NEXT file on the tape
					   as having a long linkname */
#define LF_LONGNAME	'L'	/* Identifies the NEXT file on the tape
					   as having a long name. */
#define LF_MULTIVOL	'M'	/* This is the continuation
					   of a file that began on another
					   volume */
#define LF_NAMES	'N'	/* For storing filenames that didn't
					   fit in 100 characters */
#define LF_SPARSE	'S'	/* This is for sparse files */
#define LF_VOLHDR	'V'	/* This file is a tape/volume header */
/* Ignore it on extraction */

/*
 * Exit codes from the "tar" program
 */
#define	EX_SUCCESS	0	/* success! */
#define	EX_ARGSBAD	1	/* invalid args */
#define	EX_BADFILE	2	/* invalid filename */
#define	EX_BADARCH	3	/* bad archive */
#define	EX_SYSTEM	4	/* system gave unexpected error */
#define EX_BADVOL	5	/* Special error code means
				   Tape volume doesn't match the one
				   specified on the command line */

/*
 * We default to Unix Standard format rather than 4.2BSD tar format.
 * The code can actually produce all three:
 *	f_standard	ANSI standard
 *	f_oldarch	V7
 *	neither		4.2BSD
 * but we don't bother, since 4.2BSD can read ANSI standard format anyway.
 * The only advantage to the "neither" option is that we can cmp our
 * output to the output of 4.2BSD tar, for debugging.
 */
#define		f_standard		(!f_oldarch)

#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 tar_from_oct (int digs, char *where)
{
    register long value;

    while (isspace ((unsigned char) *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 ((unsigned char) *where))
	return -1;		/* Ended on non-space/nul */

    return value;
}

static void tar_free_archive (struct vfs_class *me, struct vfs_s_super *archive)
{
    (void) me;

    if (archive->u.arch.fd != -1)
	mc_close(archive->u.arch.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_int (struct vfs_class *me, const char *name,
		      struct vfs_s_super *archive)
{
    int result, type;
    mode_t mode;
    struct vfs_s_inode *root;

    result = mc_open (name, O_RDONLY);
    if (result == -1) {
	message (1, MSG_ERROR, _("Cannot open tar archive\n%s"), name);
	ERRNOR (ENOENT, -1);
    }

    archive->name = g_strdup (name);
    mc_stat (name, &(archive->u.arch.st));
    archive->u.arch.fd = -1;

    /* Find out the method to handle this tar file */
    type = get_compression_type (result);
    mc_lseek (result, 0, SEEK_SET);
    if (type != COMPRESSION_NONE) {
	char *s;
	mc_close (result);
	s = g_strconcat (archive->name, decompress_extension (type), (char *) NULL);
	result = mc_open (s, O_RDONLY);
	if (result == -1)
	    message (1, MSG_ERROR, _("Cannot open tar archive\n%s"), s);
	g_free (s);
	if (result == -1)
	    ERRNOR (ENOENT, -1);
    }

    archive->u.arch.fd = result;
    mode = archive->u.arch.st.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.arch.st);
    root->st.st_mode = mode;
    root->data_offset = -1;
    root->st.st_nlink++;
    root->st.st_dev = MEDATA->rdev++;

    archive->root = root;

    return result;
}

static union record rec_buf;

static union record *
tar_get_next_record (struct vfs_s_super *archive, int tard)
{
    int n;

    (void) archive;

    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 tar_skip_n_records (struct vfs_s_super *archive, int tard, int n)
{
    (void) archive;

    mc_lseek (tard, n * RECORDSIZE, SEEK_CUR);
    current_tar_position += n * RECORDSIZE;
}

static void
tar_fill_stat (struct vfs_class *me, struct stat *st, union record *header,
	       size_t h_size)
{
    (void) me;

    st->st_mode = tar_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 ? vfs_finduid (header->header.
						 uname) : tar_from_oct (8,
									header->
									header.
									uid);
	st->st_gid =
	    *header->header.gname ? vfs_findgid (header->header.
						 gname) : tar_from_oct (8,
									header->
									header.
									gid);
	switch (header->header.linkflag) {
	case LF_BLK:
	case LF_CHR:
	    st->st_rdev =
		(tar_from_oct (8, header->header.devmajor) << 8) |
		tar_from_oct (8, header->header.devminor);
	}
    } else {			/* Old Unix tar */
	st->st_uid = tar_from_oct (8, header->header.uid);
	st->st_gid = tar_from_oct (8, header->header.gid);
    }
    st->st_size = h_size;
    st->st_mtime = tar_from_oct (1 + 12, header->header.mtime);
    st->st_atime = tar_from_oct (1 + 12, header->header.atime);
    st->st_ctime = tar_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
tar_read_header (struct vfs_class *me, struct vfs_s_super *archive,
		 int tard, size_t *h_size)
{
    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 = tar_get_next_record (archive, tard);
    if (NULL == header)
	return STATUS_EOF;

    recsum = tar_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') {
	if (header->header.arch_name[NAMSIZ - 1] != '\0')
	    i = NAMSIZ;
	else
	    i = strlen (header->header.arch_name);

	if (i && 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)
	*h_size = 0;		/* Links 0 size on tape */
    else
	*h_size = tar_from_oct (1 + 12, header->header.size);

    /*
     * Skip over directory snapshot info records that
     * are stored in incremental tar archives.
     */
    if (header->header.linkflag == LF_DUMPDIR)
	return STATUS_SUCCESS;

    if (header->header.linkflag == LF_LONGNAME
	|| header->header.linkflag == LF_LONGLINK) {
	char **longp;
	char *bp, *data;
	int size, written;

	if (*h_size > MC_MAXPATHLEN) {
	    message (1, MSG_ERROR, _("Inconsistent tar archive"));
	    return STATUS_BADCHECKSUM;
	}

	longp = ((header->header.linkflag == LF_LONGNAME)
		 ? &next_long_name : &next_long_link);

	g_free (*longp);
	bp = *longp = g_malloc (*h_size + 1);

	for (size = *h_size; size > 0; size -= written) {
	    data = tar_get_next_record (archive, tard)->charptr;
	    if (data == NULL) {
		g_free (*longp);
		*longp = NULL;
		message (1, MSG_ERROR,
			 _("Unexpected EOF on archive file"));
		return STATUS_BADCHECKSUM;
	    }
	    written = RECORDSIZE;
	    if (written > size)
		written = size;

	    memcpy (bp, data, written);
	    bp += written;
	}

	if (bp - *longp == MC_MAXPATHLEN && bp[-1] != '\0') {
	    g_free (*longp);
	    *longp = NULL;
	    message (1, MSG_ERROR, _("Inconsistent tar archive"));
	    return STATUS_BADCHECKSUM;
	}
	*bp = 0;
	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_strndup (header->header.arch_linkname, NAMSIZ));
	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_strndup (header->header.arch_name, NAMSIZ));
	canonicalize_pathname (current_file_name);
	len = strlen (current_file_name);

	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, q, LINK_NO_FOLLOW, FL_MKDIR);
	if (parent == NULL) {
	    message (1, MSG_ERROR, _("Inconsistent tar archive"));
	    return STATUS_BADCHECKSUM;
	}

	if (header->header.linkflag == LF_LINK) {
	    inode =
		vfs_s_find_inode (me, archive, current_link_name,
				  LINK_NO_FOLLOW, 0);
	    if (inode == NULL) {
		message (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;
	    }
	}

	tar_fill_stat (me, &st, header, *h_size);
	inode = vfs_s_new_inode (me, archive, &st);

	inode->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 (tar_get_next_record (archive, tard)->ext_hdr.
		   isextended);
	    inode->data_offset = current_tar_position;
	}
	return STATUS_SUCCESS;
    }
}

/*
 * Main loop for reading an archive.
 * Returns 0 on success, -1 on error.
 */
static int
tar_open_archive (struct vfs_class *me, struct vfs_s_super *archive,
		  const char *name, char *op)
{
    /* Initial status at start of archive */
    ReadStatus status = STATUS_EOFMARK;
    ReadStatus prev_status;
    int tard;

    (void) op;

    current_tar_position = 0;
    /* Open for reading */
    if ((tard = tar_open_archive_int (me, name, archive)) == -1)
	return -1;

    for (;;) {
	size_t h_size;

	prev_status = status;
	status = tar_read_header (me, archive, tard, &h_size);

	switch (status) {

	case STATUS_SUCCESS:
	    tar_skip_n_records (archive, tard,
				(h_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 (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 (struct vfs_class *me, const char *archive_name, char *op)
{
    static struct stat stat_buf;

    (void) me;
    (void) op;

    if (mc_stat (archive_name, &stat_buf))
	return NULL;
    return &stat_buf;
}

static int
tar_super_same (struct vfs_class *me, struct vfs_s_super *parc,
		const char *archive_name, char *op, void *cookie)
{
    struct stat *archive_stat = cookie;	/* stat of main archive */

    (void) me;
    (void) op;

    if (strcmp (parc->name, archive_name))
	return 0;

    /* Has the cached archive been changed on the disk? */
    if (parc->u.arch.st.st_mtime < archive_stat->st_mtime) {
	/* Yes, reload! */
	(*vfs_tarfs_ops.free) ((vfsid) parc);
	vfs_rmstamp (&vfs_tarfs_ops, (vfsid) parc);
	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->data_offset;
    int fd = FH_SUPER->u.arch.fd;
    struct vfs_class *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_fh_open (struct vfs_class *me, struct vfs_s_fh *fh, int flags, int mode)
{
    (void) fh;
    (void) mode;

    if ((flags & O_ACCMODE) != O_RDONLY) ERRNOR (EROFS, -1);
    return 0;
}

void
init_tarfs (void)
{
    static struct vfs_s_subclass tarfs_subclass;

    tarfs_subclass.flags = VFS_S_READONLY;
    tarfs_subclass.archive_check = tar_super_check;
    tarfs_subclass.archive_same = tar_super_same;
    tarfs_subclass.open_archive = tar_open_archive;
    tarfs_subclass.free_archive = tar_free_archive;
    tarfs_subclass.fh_open = tar_fh_open;

    vfs_s_init_class (&vfs_tarfs_ops, &tarfs_subclass);
    vfs_tarfs_ops.name = "tarfs";
    vfs_tarfs_ops.prefix = "utar";
    vfs_tarfs_ops.read = tar_read;
    vfs_tarfs_ops.setctl = NULL;
    vfs_register_class (&vfs_tarfs_ops);
}