//
// tardy - a tar post-processor
// Copyright (C) 2003, 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/string.h>
#include <libtardy/tar/input/filter/gunzip.h>


tar_input_filter_gunzip::~tar_input_filter_gunzip()
{
    delete buffered_data;
    delete buf;
}


tar_input_filter_gunzip::tar_input_filter_gunzip(
    const tar_input::pointer &a_deeper
) :
    tar_input_filter(a_deeper),
    pass_through(true),
    z_eof(true),
    buf(0),
    buffered_data(0),
    buffered_data_pos(0),
    buffered_data_size(0),
    buffered_data_size_max(0),
    inflated_file_pos(0),
    inflated_file_length(0),
    deflated_file_pos(0),
    deflated_file_length(0)
{
}


tar_input::pointer
tar_input_filter_gunzip::create(const tar_input::pointer &a_deeper)
{
    return pointer(new tar_input_filter_gunzip(a_deeper));
}


void
tar_input_filter_gunzip::zlib_fatal_error(int err)
{
    if (err < 0)
        fatal("gunzip: %s (%s)", z_error(err), stream.msg);
}


int
tar_input_filter_gunzip::buffered_getc()
{
    unsigned char c;
    int nbytes = buffered_read(&c, 1);
    if (nbytes < 1)
        return -1;
    return c;
}


int
tar_input_filter_gunzip::buffered_read(void *data, int len)
{
    if (buffered_data_pos < buffered_data_size)
    {
        int nbytes = buffered_data_size - buffered_data_pos;
        if (nbytes > len)
            nbytes = len;
        memcpy(data, buffered_data + buffered_data_pos, nbytes);
        buffered_data_pos += nbytes;
        deflated_file_pos += nbytes;
        return nbytes;
    }
    if (deflated_file_pos >= deflated_file_length)
        return 0;
    int nbytes = deflated_file_length - deflated_file_pos;
    if (nbytes > len)
        nbytes = len;
    int nbytes2 = tar_input_filter::read_data(data, nbytes);
    deflated_file_pos += nbytes2;
    return nbytes2;
}


void
tar_input_filter_gunzip::buffered_unread(const void *data, size_t len)
{
    deflated_file_pos -= len;

    //
    // If we need a bigger buffer, allocate it from the heap
    // and copy the new and old data into it.
    //
    size_t buffered_nbytes = buffered_data_size - buffered_data_pos;
    if (len + buffered_nbytes > buffered_data_size_max)
    {
        int nbytes = len + buffered_nbytes;
        unsigned char *new_buffered_data = new unsigned char[nbytes];
        memcpy(new_buffered_data, data, len);
        memcpy
        (
            new_buffered_data + len,
            buffered_data + buffered_data_pos,
            buffered_nbytes
        );
        delete buffered_data;
        buffered_data = new_buffered_data;
        new_buffered_data = 0;
        buffered_data_pos = 0;
        buffered_data_size = nbytes;
        buffered_data_size_max = nbytes;
        return;
    }

    //
    // See if we need to stuffle the data up to make it fit into the
    // existing buffer.
    //
    if (len > buffered_data_pos)
    {
        memmove
        (
            buffered_data + buffered_data_pos,
            buffered_data + buffered_data_size_max - buffered_nbytes,
            buffered_nbytes
        );
        buffered_data_size = buffered_data_size_max;
        buffered_data_pos = buffered_data_size_max - buffered_nbytes;
    }

    //
    // Add the data onto the *front* of the buffer.
    //
    memcpy(buffered_data + buffered_data_pos - len, data, len);
    buffered_data_pos -= len;
}


unsigned long
tar_input_filter_gunzip::get_long(void)
{
    unsigned char buffer[4];
    unsigned char *bp = buffer;
    size_t len = sizeof(buffer);
    for (;;)
    {
        //
        // weird block boundaries can mess this up.
        //
        size_t nbytes = buffered_read(bp, len);
        if (nbytes == 0)
            fatal("short read");
        len -= nbytes;
        if (!len)
            break;
        bp += nbytes;
    }
    return
        (
            (unsigned long)buffer[0]
        |
            ((unsigned long)buffer[1] << 8)
        |
            ((unsigned long)buffer[2] << 16)
        |
            ((unsigned long)buffer[3] << 24)
        );
}


//
// Check the gzip header of a gz_stream opened for reading. Set the
// stream mode to transparent if the gzip magic header is not present;
// set this->err to Z_DATA_ERROR if the magic header is present but the
// rest of the header is incorrect.
//
// IN assertion: the stream this has already been created sucessfully;
// this->stream.avail_in is zero for the first time, but may be non-zero
// for concatenated .gz files.
//

bool
tar_input_filter_gunzip::read_header(tar_header &arg)
{
    if (0 == tar_input_filter::read_header(arg))
        return false;
    pass_through = (arg.type != tar_header::type_normal_gzipped);
    tar_input_filter::read_header_padding();
    if (pass_through)
        return true;
    arg.type = tar_header::type_normal;

    //
    // Check for the magic number.
    // If it isn't present, assume transparent mode.
    //
    deflated_file_pos = 0;
    deflated_file_length = arg.size;
    static unsigned char gz_magic[] = { 0x1F, 0x8B };
    unsigned char magic[sizeof(gz_magic)];
    if
    (
        buffered_read(magic, sizeof(magic)) != sizeof(magic)
    ||
        0 != memcmp(magic, gz_magic, sizeof(magic))
    )
        fatal("gunzip: bad magic number (%02X %02X)", magic[0], magic[1]);

    //
    // Magic number present, now we require the rest of the header
    // to be present and correctly formed.
    //
    int method = buffered_getc();
    if (method != Z_DEFLATED)
        fatal("gunzip: not deflated encoding");
    int flags = buffered_getc();
    if (flags < 0 || (flags & Z_FLAG_RESERVED) != 0)
        fatal("gunzip: unknown flags");

    // Discard time, xflags and OS code:
    for (int oslen = 0; oslen < 6; ++oslen)
        if (buffered_getc() < 0)
            fatal("gunzip: short file");

    if (flags & Z_FLAG_EXTRA_FIELD)
    {
        // low byte of len must be 4
        int len = buffered_getc();
        if (len < 0)
            goto bad_extra;
        if (len != 4)
            goto bad_extra;

        // high byte of len must be zero
        int c = buffered_getc();
        if (c < 0)
            goto bad_extra;
        if (c != 0)
            goto bad_extra;

        inflated_file_length = get_long();
    }
    else
    {
        bad_extra:
        fatal("gunzip: extra field required");
    }

    if (flags & Z_FLAG_ORIG_NAME)
    {
        // skip the original file name
        for (;;)
        {
            int c = buffered_getc();
            if (c < 0)
                fatal("gunzip: short file");
            if (c == 0)
                break;
        }
    }
    if (flags & Z_FLAG_COMMENT)
    {
        // skip the .gz file comment
        for (;;)
        {
            int c = buffered_getc();
            if (c < 0)
                fatal("gunzip: short file");
            if (c == 0)
                break;
        }
    }
    if (flags & Z_FLAG_HEAD_CRC)
    {
        // skip the header crc
        for (int crclen = 0; crclen < 2; ++crclen)
            if (buffered_getc() < 0)
                fatal("gunzip: short file");
    }

    stream.zalloc = (alloc_func)0;
    stream.zfree = (free_func)0;
    stream.opaque = (voidpf)0;
    stream.next_in = Z_NULL;
    stream.avail_in = 0;
    stream.next_out = Z_NULL;
    stream.avail_out = 0;
    if (!buf)
        buf = new Byte[Z_BUFSIZE];
    crc = crc32(0L, Z_NULL, 0);
    inflated_file_pos = 0;
    z_eof = false;
    buffered_data_pos = 0;
    buffered_data_size = 0;

    //
    // windowBits is passed < 0 to tell that there is no zlib header.
    // Note that in this case inflate *requires* an extra "dummy" byte
    // after the compressed stream in order to complete decompression
    // and return Z_STREAM_END. Here the gzip CRC32 ensures that 4
    // bytes are present after the compressed stream.
    //
    int err = inflateInit2(&stream, -MAX_WBITS);
    if (err < 0)
        zlib_fatal_error(err);

    //
    // Re-write the header to reflect the decompressed file data.
    //
    arg.size = inflated_file_length;

    //
    // Report success.
    //
    return true;
}


void
tar_input_filter_gunzip::read_header_padding(void)
{
    // already done.
}


int
tar_input_filter_gunzip::read_data(void *data, int len)
{
    if (pass_through)
        return tar_input_filter::read_data(data, len);

    if (z_eof)
        return 0;

    stream.next_out = (Bytef *)data;
    stream.avail_out = len;

    while (stream.avail_out > 0)
    {
        if (stream.avail_in == 0)
        {
            // assert(buf);
            stream.next_in = buf;
            stream.avail_in = buffered_read(buf, Z_BUFSIZE);

            //
            // There should always be something left on the
            // input, because we have the CRC and Length
            // to follow.  Fatal error if not.
            //
            if (stream.avail_in <= 0)
                fatal("premature end of file");
        }
        int err = inflate(&stream, Z_PARTIAL_FLUSH);
        if (err < 0)
            zlib_fatal_error(err);
        if (err == Z_STREAM_END)
        {
            z_eof = true;

            //
            // Push back the unused portion of the input stream.
            // (There shouldn't be much.)
            //
            if (stream.avail_in > 0)
                buffered_unread(stream.next_in, stream.avail_in);

            //
            // Fall out of the loop.
            //
            break;
        }
    }

    //
    // Calculate the running CRC
    //
    long result = stream.next_out - (Bytef *)data;
    crc = crc32(crc, (Bytef *)data, (uInt)result);

    //
    // Update the file position.
    //
    inflated_file_pos += result;

    //
    // At end-of-file we need to do some checking.
    //
    if (z_eof)
    {
        //
        // Check CRC
        //
        // Watch out for 64-bit machines.  This is what
        // those aparrently redundant 0xFFFFFFFF are for.
        //
        unsigned long icrc = get_long();
        if ((icrc & 0xFFFFFFFF) != (crc & 0xFFFFFFFF))
            fatal("gunzip: checksum mismatch (%lX != %lX)", icrc, crc);

        //
        // The uncompressed length here may be different
        // from pos in case of concatenated .gz
        // files.  But we don't write them that way,
        // so give an error if it happens.
        //
        // We shouldn't have 64-bit problems in this case.
        //
        unsigned long ilen = get_long();
        if (ilen != inflated_file_pos)
        {
            fatal
            (
                "gunzip: length mismatch (%ld != %ld)",
                ilen,
                (long)inflated_file_pos
            );
        }
        if (inflated_file_pos != inflated_file_length)
            fatal("inflated length mismatch");
        if (deflated_file_pos != deflated_file_length)
            fatal("deflated length mismatch");

        //
        // Finished with this stream.
        //
        int err = inflateEnd(&stream);
        if (err < 0)
            zlib_fatal_error(err);
    }

    //
    // Return success (failure always goes via input_format_error,
    // or zlib_fatal_error).
    //
    return result;
}


void
tar_input_filter_gunzip::read_data_padding(void)
{
    if (pass_through || z_eof)
    {
        tar_input_filter::read_data_padding();
        return;
    }

    //
    // We should be exactly at the end of input.
    // So we need to read the length and CRC fields to check.
    // To do this, we need a dummy buffer for decompressed data.
    //
    char data[4];
    stream.next_out = (Bytef *)data;
    stream.avail_out = sizeof(data);

    if (stream.avail_in == 0)
    {
        // assert(buf);
        stream.next_in = buf;
        stream.avail_in = buffered_read(buf, Z_BUFSIZE);

        //
        // There should always be something left on the
        // input, because we have the CRC and Length
        // to follow.  Fatal error if not.
        //
        if (stream.avail_in <= 0)
            fatal("premature end of file");
    }
    int err = inflate(&stream, Z_PARTIAL_FLUSH);
    if (err < 0)
        zlib_fatal_error(err);
    if (err != Z_STREAM_END || stream.next_out != (Bytef *)data)
        fatal("gunzip: too much deflated file");

    //
    // Check CRC
    //
    // Watch out for 64-bit machines.  This is what
    // those aparrently redundant 0xFFFFFFFF are for.
    //
    if ((get_long() & 0xFFFFFFFF) != (crc & 0xFFFFFFFF))
        fatal("gunzip: checksum mismatch");

    //
    // The uncompressed length here may be different
    // from pos in case of concatenated .gz
    // files.  But we don't write them that way,
    // so give an error if it happens.
    //
    // We shouldn't have 64-bit problems in this case.
    //
    if (get_long() != inflated_file_pos)
        fatal("gunzip: length mismatch");
    if (inflated_file_pos != inflated_file_length)
        fatal("inflated length mismatch");
    if (deflated_file_pos != deflated_file_length)
        fatal("deflated length mismatch");

    //
    // Finished with this stream.
    //
    err = inflateEnd(&stream);
    if (err < 0)
        zlib_fatal_error(err);

    //
    // Now read the deeper padding
    //
    tar_input_filter::read_data_padding();
}
