//
// tardy - a tar post-processor
// Copyright (C) 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/zlib.h>
#include <libtardy/ac/string.h>

#include <libtardy/endian.h>
#include <libtardy/file/input/gunzip.h>


#ifndef Z_BUFSIZE
#  ifdef MAXSEG_64K
#    define Z_BUFSIZE 4096 // minimize memory usage for 16-bit DOS
#  else
#    define Z_BUFSIZE 16384
#  endif
#endif

// gzip flag byte
#define ASCII_FLAG   0x01 // bit 0 set: file probably ascii text
#define HEAD_CRC     0x02 // bit 1 set: header CRC present
#define EXTRA_FIELD  0x04 // bit 2 set: extra field present
#define ORIG_NAME    0x08 // bit 3 set: original file name present
#define COMMENT      0x10 // bit 4 set: file comment present
#define RESERVED     0xE0 // bits 5..7: reserved


file_input_gunzip::~file_input_gunzip()
{
    int err = inflateEnd(&stream);
    if (err < 0)
        zlib_fatal_error(err);
    delete [] buf;
    buf = 0;
}


file_input_gunzip::file_input_gunzip(const file_input::pointer &a_deeper) :
    deeper(a_deeper),
    z_eof(false),
    crc(crc32(0L, Z_NULL, 0)),
    pos(0),
    buf(new Byte [Z_BUFSIZE])
{
    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;

    //
    // 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);

    //
    // Now read the file header.
    //
    read_header();
}


file_input::pointer
file_input_gunzip::create(const file_input::pointer &a_deeper)
{
    return pointer(new file_input_gunzip(a_deeper));
}


file_input::pointer
file_input_gunzip::open(const file_input::pointer &deeper)
{
    if (!file_input_gunzip::candidate(deeper))
    {
        //
        // If it is not actually a compressed file,
        // simply return the deeper file.  This will
        // give much better performance.
        //
        return deeper;
    }
    return file_input_gunzip::create(deeper);
}


void
file_input_gunzip::zlib_fatal_error(int err)
{
    if (err >= 0)
        return;
    if (stream.msg)
    {
        fatal
        (
            "gunzip %s: %s (%s)",
            deeper->filename().quote_c().c_str(),
            z_error(err),
            stream.msg
        );
    }
    else
    {
        fatal
        (
            "gunzip %s: %s",
            deeper->filename().quote_c().c_str(),
            z_error(err)
        );
    }
}


unsigned long
file_input_gunzip::get_long(void)
{
    unsigned char buffer[4];
    size_t nbytes = deeper->read(buffer, sizeof(buffer));
    if (nbytes != sizeof(buffer))
        fatal("gunzip: premature end of file");
    return endian_get4le(buffer);
}


unsigned
file_input_gunzip::get_short(void)
{
    unsigned char buffer[2];
    size_t nbytes = deeper->read(buffer, sizeof(buffer));
    if (nbytes != sizeof(buffer))
        fatal("gunzip: premature end of file");
    return endian_get2le(buffer);
}


int
file_input_gunzip::read_inner(void *data, size_t data_size)
{
    if (z_eof)
        return 0;

    Bytef *start = (Bytef *)data; // starting point for crc computation
    stream.next_out = (Bytef *)data;
    stream.avail_out = data_size;

    while (stream.avail_out > 0)
    {
        if (stream.avail_in == 0)
        {
            stream.next_in = buf;
            stream.avail_in = deeper->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)
            {
                deeper->fatal("gunzip: 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.
            // (The way we wrote it, there shouldn't be much.)
            //
            while (stream.avail_in > 0)
            {
                deeper->unread(stream.next_in, stream.avail_in);
                stream.avail_in = 0;
            }

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

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

    //
    // Update the file position.
    //
    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.
        //
        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() != pos)
            fatal("gunzip: length mismatch");
    }

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


unsigned long
file_input_gunzip::get_position_inner(void)
    const
{
    return pos;
}


rcstring
file_input_gunzip::filename(void)
    const
{
    rcstring fn = deeper->filename();
    rcstring fn_lc = fn.downcase();
    if (fn_lc.ends_with(".z"))
        return fn.substring(0, fn.size() - 2);
    if (fn_lc.ends_with(".gz"))
        return fn.substring(0, fn.size() - 3);
    if (fn_lc.ends_with(".tgz"))
        return fn.substring(0, fn.size() - 4) + ".tar";
    return fn;
}


//
// 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 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;
// stream.avail_in is zero for the first time, but may be non-zero
// for concatenated .gz files.
//
static unsigned char gz_magic[2] = {0x1f, 0x8b}; // gzip magic header

bool
file_input_gunzip::candidate(const file_input::pointer &a_deeper)
{
    //
    // Check for the magic number.
    // If it isn't present, assume transparent mode.
    //
    unsigned char buffer[sizeof(gz_magic)];
    if (a_deeper->peek(buffer, sizeof(buffer)) != sizeof(buffer))
        return false;
    return (0 == memcmp(buffer, gz_magic, sizeof(buffer)));
}


void
file_input_gunzip::read_header(void)
{
    //
    // Check for the magic number.
    // If it isn't present, assume transparent mode.
    //
    unsigned char buffer[10];
    if (deeper->read(buffer, sizeof(buffer)) != sizeof(buffer))
        deeper->fatal("gunzip: short header read");
    if (0 != memcmp(buffer, gz_magic, sizeof(gz_magic)))
        deeper->fatal("gunzip: wrong magic number");

    //
    // Magic number present, now we require the rest of the header
    // to be present and correctly formed.
    //
    int method = buffer[2];
    if (method != Z_DEFLATED)
        deeper->fatal("gunzip: not deflated encoding");
    int flags = buffer[3];
    if ((flags & RESERVED) != 0)
        deeper->fatal("gunzip: unknown flags");

    if (flags & EXTRA_FIELD)
    {
        // skip the extra field
        unsigned elen = get_short();
        deeper->skip(elen);
    }
    if (flags & ORIG_NAME)
    {
        // skip the original file name
        skip_string();
    }
    if (flags & COMMENT)
    {
        // skip the .gz file comment
        skip_string();
    }
    if (flags & HEAD_CRC)
    {
        // skip the header crc
        get_short();
    }
}


void
file_input_gunzip::skip_string(void)
{
    for (;;)
    {
        char buffer[256];
        unsigned nbytes = deeper->read(buffer, sizeof(buffer));
        if (nbytes == 0)
            deeper->fatal("gunzip: premature end of file");
        char *p = (char *)memchr(buffer, 0, nbytes);
        if (p)
        {
            // we have found the end
            ++p;
            deeper->unread(p, buffer + sizeof(buffer) - p);
            return;
        }
    }
}
