//
// tardy - a tar post-processor
// Copyright (C) 1998, 1999, 2001-2004, 2008, 2009, 2011 Peter Miller
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 3 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
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//

#include <libtardy/ac/errno.h>
#include <libtardy/ac/string.h>
#include <libexplain/output.h>

#include <libtardy/tar/format.h>
#include <libtardy/tar/input/tar/bsd.h>
#include <libtardy/tar/input/tar/posix.h>
#include <libtardy/tar/input/tar/ustar.h>


tar_input_tar::~tar_input_tar()
{
}


tar_input_tar::tar_input_tar(const file_input::pointer &a_fp) :
    fp(a_fp),
    state(0)
{
    assert(fp);
}


tar_input::pointer
tar_input_tar::factory(const file_input::pointer &a_fp)
{
    if (tar_input_tar_ustar::candidate(a_fp))
        return tar_input_tar_ustar::create(a_fp);
    if (tar_input_tar_bsd::candidate(a_fp))
        return tar_input_tar_bsd::create(a_fp);
    return tar_input_tar_posix::create(a_fp);
}


static bool
all_zero(char *buf, long len)
{
    while (len > 0)
    {
        if (*buf++)
            return false;
        --len;
    }
    return true;
}


void
tar_input_tar::check_state(int a, int b)
{
    if (state != a && (b >= 0 && state != b))
        fatal("state %d: should be %d", state, a);
    if (state < 3)
    {
        unsigned alignment = fp->get_position() % TBLOCK;
        if (alignment != 0)
            fatal("state %d: offset is %u", state, alignment);
    }
}


int
tar_input_tar::read_data_strict(void *buffer, int maximum_length)
{
    if (!fp)
        explain_output_error_and_die("file not open (bug)");
    int result = 0;
    while (maximum_length > 0)
    {
        int nbytes = fp->read(buffer, maximum_length);
        if (nbytes == 0)
        {
            if (result == 0)
                break;
            fatal
            (
                "short read (asked %d, got %d)",
                maximum_length + result,
                result
            );
        }
        result += nbytes;
        maximum_length -= nbytes;
        buffer = (char *)buffer + nbytes;
    }
    return result;
}


int
tar_input_tar::read_data(void *buffer, int maximum_length)
{
    check_state(2, 3);
    state = 3;

    return read_data_strict(buffer, maximum_length);
}


void
tar_input_tar::read_data_padding(void)
{
    check_state(2, 3);
    state = 0;

    unsigned n = fp->get_position() % TBLOCK;
    if (n == 0)
        return;
    fp->skip(TBLOCK - n);
}


bool
tar_input_tar::read_header_longname(int size, tar_header &arg)
{
    //
    // Read the long name.
    //
    int size2 = (size + TBLOCK - 1) & -TBLOCK;
    char *buffer = new char[size2];
    int nbytes = read_data_strict(buffer, size2);
    if (nbytes != size2)
        fatal("premature end of file (short name read)");
    while (size > 1 && buffer[size - 1] == 0)
        --size;
    rcstring long_file_name = rcstring(buffer, size);
    delete buffer;

    //
    // Read the next file header
    //
    state = 0;
    if (!read_header(arg))
        return false;

    //
    // Replace the name with the long name we just read.
    //
    arg.name = long_file_name;

    //
    // Watch out for trailing slashes
    //
    while (arg.name[arg.name.size() - 1] == '/')
    {
        arg.name =
            rcstring
            (
                arg.name.c_str(),
                arg.name.size() - 1
            );
        arg.type = tar_header::type_directory;
        arg.size = 0;
        arg.link_count = 2;
    }
    return true;
}


bool
tar_input_tar::read_header_longlink(int size, tar_header &arg)
{
    //
    // Read the long name.
    //
    int size2 = (size + TBLOCK - 1) & -TBLOCK;
    char *buffer = new char[size2];
    int nbytes = read_data_strict(buffer, size2);
    if (nbytes != size2)
        fatal("premature end of file (short name read)");
    while (size > 1 && buffer[size - 1] == 0)
        --size;
    rcstring long_link_name = rcstring(buffer, size);
    delete buffer;

    //
    // Read the next file header
    //
    state = 0;
    if (!read_header(arg))
        return false;

    //
    // Replace the name with the long name we just read.
    //
    arg.linkname = long_link_name;
    return true;
}


bool
tar_input_tar::read_header_gzipped(int size, tar_header &arg)
{
    //
    // Read the name of the uncompressed file.
    //
    int size2 = (size + TBLOCK - 1) & -TBLOCK;
    char *buffer = new char[size2];
    int nbytes = read_data_strict(buffer, size2);
    if (nbytes != size2)
        fatal("premature end of file (short name read)");
    while (size > 1 && buffer[size - 1] == 0)
        --size;
    rcstring long_file_name = rcstring(buffer, size);
    delete buffer;

    //
    // Read the next file header
    //
    state = 0;
    if (!read_header(arg))
        return false;

    //
    // Replace the name with the name of the uncompressed file.
    //
    if (long_file_name.size() != 0)
        arg.name = long_file_name;
    arg.type = tar_header::type_normal_gzipped;
    return true;
}


bool
tar_input_tar::read_header(tar_header &arg)
{
    check_state(0);
    state = 1;

    //
    // read a block of input
    //
    char block[TBLOCK];
    int nbytes = read_data_strict(block, TBLOCK);
    if (!nbytes)
        return false;
    if (nbytes != TBLOCK)
        fatal("premature end of file (short header read)");
    if (all_zero(block, TBLOCK))
        return false;

    //
    // Build a pointer to the on-tape header block structure.
    // Use this to asist decoding the header block.
    //
    header_ty *hp = (header_ty *)block;

    //
    // chew over header fields
    //
    int hchksum = hp->chksum_get();
    if (hchksum < 0)
        fp->fatal("corrupted checksum field");
    int cs2 = hp->calculate_checksum();
    if (hchksum != cs2)
    {
        hp->dump();
        fp->fatal
        (
            "checksum does not match (calculated 0%o, file has 0%o)",
            cs2,
            hchksum
        );
    }

    arg = tar_header(); // zero out everything

    //
    // Give all files a unique inode number.
    //
    // Let the writer figure out (from the linkname) if they should
    // be the same.
    //
    // FIXME: use a symtab and get the inode_number and link_count more
    // correct.
    //
    static long ino;
    arg.inode_number = ++ino;

    arg.name = hp->name_get();

    int hsize = hp->size_get();
    if (hsize < 0)
        fp->fatal("%s: corrupted size field", arg.name.c_str());
    arg.size = hsize;

    switch (hp->linkflag_get())
    {
    case LF_OLDNORMAL:
    case LF_NORMAL:
        arg.type = tar_header::type_normal;
        //
        // Working out that it is a directory is
        // interesting: you can't rely on the mode bits,
        // and it shows as a normal file.  The clue is
        // that the name will end with a slash.
        //
        if (arg.size == 0)
        {
            while (arg.name[arg.name.size() - 1] == '/')
            {
                arg.name = rcstring(arg.name.c_str(), arg.name.size() - 1);
                arg.type = tar_header::type_directory;
                arg.size = 0;
                arg.link_count = 2;
            }
        }
        break;

    case LF_CONTIG:
        arg.type = tar_header::type_normal_contiguous;
        break;

    case LF_GZIPPED:
        return read_header_gzipped(hsize, arg);

    case LF_LINK:
        arg.linkname = hp->linkname_get();
        arg.type = tar_header::type_link_hard;
        arg.size = 0;
        arg.link_count = 2;
        break;

    case LF_SYMLINK:
        arg.linkname = hp->linkname_get();
        arg.type = tar_header::type_link_symbolic;
        arg.size = 0;
        break;

    case LF_CHR:
        arg.type = tar_header::type_device_character;
        arg.size = 0;
        break;

    case LF_BLK:
        arg.type = tar_header::type_device_block;
        arg.size = 0;
        break;

    case LF_DIR:
        while (arg.name[arg.name.size() - 1] == '/')
            arg.name = rcstring(arg.name.c_str(), arg.name.size() - 1);
        if (arg.size != 0)
        {
            // Some tarball writing programs are storing files as
            // directories.  No idea which program is doing this.  The
            // broken tarballs were from thr HTC Android Download site.
            arg.type = tar_header::type_normal;
            break;
        }
        arg.type = tar_header::type_directory;
        arg.size = 0;
        arg.link_count = 2;
        break;

    case LF_FIFO:
        arg.type = tar_header::type_fifo;
        arg.size = 0;
        break;

    case LF_LONGNAME:
        return read_header_longname(hsize, arg);

    case LF_LONGLINK:
        return read_header_longlink(hsize, arg);

    default:
        fp->fatal("file type \"%c\" unknown", hp->linkflag_get());
        break;
    }

    int hmode = hp->mode_get();
    if (hmode < 0)
        fatal("\"%s\" corrupted mode field", arg.name.c_str());
    arg.mode = hmode;

    int huid = hp->uid_get();
    if (huid < 0)
        fatal("\"%s\" corrupted uid field", arg.name.c_str());
    arg.user_id = huid;

    int hgid = hp->gid_get();
    if (hgid < 0)
        fatal("\"%s\" corrupted gid field", arg.name.c_str());
    arg.group_id = hgid;

    int hmtime = hp->mtime_get();
    if (hmtime < 0)
    {
        //
        // usually because of a file time
        // before 1970 or after 2038
        //
        if (errno != ERANGE)
            fatal("\"%s\" corrupted mtime field", arg.name.c_str());
        warning("\"%s\" mtime field out of range", arg.name.c_str());

        time_t now;
        time(&now);
        hmtime = now;
    }
    arg.mtime = hmtime;

    if (0 == memcmp(hp->magic, TMAGIC, sizeof(hp->magic)))
    {
        arg.user_name = hp->uname_get();
        arg.group_name = hp->gname_get();
        arg.device_major = hp->devmajor_get();
        arg.device_minor = hp->devminor_get();
    }
    else
    {
        arg.user_name = "nobody";
        arg.group_name = "nogroup";
        arg.device_major = 0;
        arg.device_minor = 0;
    }

    //
    // say that we found something
    //
    return true;
}


void
tar_input_tar::read_header_padding(void)
{
    check_state(1);
    state = 2;
}


rcstring
tar_input_tar::filename(void)
    const
{
    return fp->filename();
}


format_family_t
tar_input_tar::get_format_family(void)
    const
{
    return format_family_tar;
}


#if 0

size_t
tar_input_tar::get_maximum_name_length(void)
    const
{
    //
    // This would be true, if it weren't for the GNU long name
    // extensions.  When they are split out into a separate filter, this
    // method will be necessary.
    //
    return NAMSIZ;
}

#endif
