/* eCAP ClamAV Adapter                                http://www.e-cap.org/
 * Copyright (C) 2011 The Measurement Factory.
 * Distributed under GPL v2 without any warranty.                        */

#include "Adapter.h"
#include "Answer.h"
#include "ClamAv.h"
#include "Debugger.h"
#include <iostream>

#include <libecap/common/name.h>
#include <libecap/common/area.h>
#include <libecap/common/options.h>
#include <cstring>
#include <string>
#include <stdexcept>

static const libecap::Name optDebug("debug");

inline void Throw(const char *message, const char *reason = "")
{
    std::string error(message);
    error += reason;
    error += "\n";
    throw std::runtime_error(error);
}

Adapter::ClamAv::ClamAv():
    engine(0)
{
    static int scannerCount = 0;
    ++scannerCount;
    Debugger(ilNormal|flApplication) << "eClamAV: " <<
        "Initializing engine #" << scannerCount << ".";
}

Adapter::ClamAv::~ClamAv()
{
    close();
}

void Adapter::ClamAv::scan(Answer &answer)
{
    // We assume that cl_*() functions used here are threadsafe.

    const char *virname = 0;
    struct cl_scan_options options;
    memset(&options, 0, sizeof(options));
    options.parse |= ~0; // enable all parsers
    const int eScanResult = cl_scanfile(answer.fileName.c_str(), &virname, 0, engine, &options);

    switch (eScanResult) {
    case CL_CLEAN:
        answer.statusCode = Answer::scClean;
        break;

    case CL_VIRUS:
        answer.statusCode = Answer::scVirus;
        answer.virusName = virname;
        break;

    default:
        answer.statusCode = Answer::scError;
        answer.errorMsg = cl_strerror(eScanResult);
    }
}

void Adapter::ClamAv::configure(const Options &cfg)
{
    setDebugging(cfg.option(optDebug)); // call here to debug cl_init()

    // initialize once, to avoid "bytecode_init: already initialized"
    static bool initialized = false;
    if (!initialized) {
        const int ret = cl_init(CL_INIT_DEFAULT);
        if (ret != CL_SUCCESS)
            Throw("Can't initialize libclamav: ", cl_strerror(ret));
        initialized = true;
    }

    loadDatabase();
}

void Adapter::ClamAv::reconfigure(const Options &cfg)
{
    // TODO: Does ClamAV support reconfiguration?
    setDebugging(cfg.option(optDebug));
}

void Adapter::ClamAv::setDebugging(const libecap::Area &flag)
{
    // TODO: use unstable cl_set_clcb_msg() API instead?

    bool wantDebugging = false; // no debugging by default
    if (flag) {
        if (flag.toString() == "full")
            wantDebugging = true;
        else
        if (flag.toString() == "none")
            wantDebugging = false;
        else
            Throw("invalid debug option value (expected 'none' or 'full'): ",
                flag.toString().c_str());
    }

    // XXX: ClamAV lacks API to query current debugging state
    if (wantDebugging)
        cl_debug();
    // else cli_debug_flag = 0; // XXX: ClamAV lacks cl_nodebug();
}


void Adapter::ClamAv::update()
{
    // XXX: The relationship between dbstat and engine is not clear; do we need
    // multiple dbstats if we have multiple engines? If not, do we update once?
    if (cl_statchkdir(&dbstat) == 1) {

        // reload database
        close();
        loadDatabase();

        // recharge dbstat
        cl_statfree(&dbstat);
        cl_statinidir(cl_retdbdir(), &dbstat);
    }
}

void Adapter::ClamAv::loadDatabase()
{
    if (engine)
        Throw("Internal error: double engine load");

    if (!(engine = cl_engine_new()))
        Throw("Can't create new engine");

    try    {
        unsigned int sigs = 0;
        /* load all available databases from default directory */
        int ret = cl_load(cl_retdbdir(), engine, &sigs, CL_DB_STDOPT);
        if (ret != CL_SUCCESS)
            Throw("cl_load: ", cl_strerror(ret));

        // printf("Loaded %u signatures from %s\n", sigs, cl_retdbdir());

        // build engine
        if ((ret = cl_engine_compile(engine)) != CL_SUCCESS)
            Throw("Database initialization error: ", cl_strerror(ret));;

        memset(&dbstat, 0, sizeof(struct cl_stat));
        cl_statinidir(cl_retdbdir(), &dbstat);
    } catch (...) {
        close();
        throw;
    }
}

void Adapter::ClamAv::close()
{
    if (engine) {
        cl_engine_free(engine);
        engine = 0;
    }
}
