/*
 * Copyright (C) 2010 Sourcefire, Inc.
 * Authors: aCaB <acab@clamav.net>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License version 2.1 as published by the Free Software Foundation.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
 * USA
 */

#if HAVE_CONFIG_H
#include "clamav-config.h"
#endif

#include "clamav.h"
#include "shared/output.h"
#include "mpool.h"
#include "clscanapi.h"
#include "interface.h"

#define FMT(s) "!"__FUNCTION__": "s"\n"
#define FAIL(errcode, fmt, ...) do { logg(FMT(fmt), __VA_ARGS__); return (errcode); } while(0)
#define WIN() do { logg("~%s completed successfully\n", __FUNCTION__); return CLAMAPI_SUCCESS; } while(0)
#define INFN() do { logg("in %s\n", __FUNCTION__); } while(0)

#define MAX_VIRNAME_LEN 1024

HANDLE reload_event;
volatile LONG reload_waiters = 0;

HANDLE monitor_event;
HANDLE monitor_hdl = NULL;

HANDLE engine_mutex;
/* protects the following items */
struct cl_engine *engine = NULL;
char dbdir[PATH_MAX];
char tmpdir[PATH_MAX];
FILETIME last_chk_time = {0, 0};
/* end of protected items */

typedef struct {
    CLAM_SCAN_CALLBACK scancb;
    void *scancb_ctx;
    unsigned int scanopts;
} instance;

struct {
    instance *inst;
    unsigned int refcnt;
} *instances = NULL;
unsigned int ninsts_total = 0;
unsigned int ninsts_avail = 0;
HANDLE instance_mutex;

BOOL minimal_definitions = FALSE;

#define lock_engine()(WaitForSingleObject(engine_mutex, INFINITE) == WAIT_FAILED)
#define unlock_engine() do {ReleaseMutex(engine_mutex);} while(0)

#define lock_instances()(WaitForSingleObject(instance_mutex, INFINITE) == WAIT_FAILED)
#define unlock_instances() do {ReleaseMutex(instance_mutex);} while(0)

cl_error_t prescan_cb(int fd, void *context);
cl_error_t postscan_cb(int fd, int result, const char *virname, void *context);


DWORD WINAPI monitor_thread(VOID *p) {
    char watchme[PATH_MAX];
    HANDLE harr[2], fff;

    if(lock_engine()) {
	logg("monitor_thread: failed to lock engine\n");
	return 0;
    }

    snprintf(watchme, sizeof(watchme), "%s\\forcerld", dbdir);
    watchme[sizeof(watchme)-1] = '\0';

    harr[0] = monitor_event;
    harr[1] = FindFirstChangeNotification(dbdir, FALSE, FILE_NOTIFY_CHANGE_LAST_WRITE | FILE_NOTIFY_CHANGE_FILE_NAME);

    logg("monitor_thread: watching directory changes on %s\n", dbdir);

    unlock_engine();

    if(harr[1] == INVALID_HANDLE_VALUE) {
	logg("monitor_thread: failed to monitor directory changes on %s\n", dbdir);
	return 0;
    }

    while(1) {
	WIN32_FIND_DATA wfd;
	SYSTEMTIME st;

	switch(WaitForMultipleObjects(2, harr, FALSE, INFINITE)) {
	case WAIT_OBJECT_0:
	    logg("monitor_thread: terminating upon request\n");
	    FindCloseChangeNotification(harr[1]);
	    return 0;
	case WAIT_OBJECT_0 + 1:
	    break;
	default:
	    logg("monitor_thread: unexpected wait failure - %u\n", GetLastError());
	    Sleep(1000);
	    continue;
	}
	FindNextChangeNotification(harr[1]);
	if((fff = FindFirstFile(watchme, &wfd)) == INVALID_HANDLE_VALUE)
	    continue;
	FindClose(fff);

	GetSystemTime(&st);
	SystemTimeToFileTime(&st, &wfd.ftCreationTime);
	if(CompareFileTime(&wfd.ftLastWriteTime, &wfd.ftCreationTime) > 0)
	    wfd.ftLastWriteTime = wfd.ftCreationTime;
	if(CompareFileTime(&wfd.ftLastWriteTime, &last_chk_time) <= 0)
	    continue;

	logg("monitor_thread: reload requested!\n");
	Scan_ReloadDatabase();
	GetSystemTime(&st);
	SystemTimeToFileTime(&st, &last_chk_time); /* FIXME: small race here */
    }
}

static wchar_t *threat_type(const char *virname) {
    if(!virname)
	return NULL;
    if(!strncmp(virname, "Trojan", 6))
	return L"Trojan";
    if(!strncmp(virname, "Worm", 4))
	return L"Worm";
    if(!strncmp(virname, "Exploit", 7))
	return L"Exploit";
    if(!strncmp(virname, "Adware", 6))
	return L"Adware";
    return L"Malware";
}

static int add_instance(instance *inst) {
    unsigned int i;

    INFN();
    if(lock_instances()) {
	logg("!add_instance: failed to lock instances\n");
	return 1;
    }
    if(!ninsts_avail) {
	void *freeme, *new_instances = calloc(ninsts_total + 256, sizeof(*instances));
	if(!new_instances) {
	    unlock_instances();
	    logg("!add_instance: failed to grow instances\n");
	    return 1;
	}
	freeme = instances;
	if(instances && ninsts_total)
	    memcpy(new_instances, instances, ninsts_total * sizeof(*instances));
	ninsts_total += 256;
	ninsts_avail += 256;
	instances = new_instances;
	if(freeme)
	    free(freeme);
	logg("add_instance: instances grown to %u\n", ninsts_total);
    }
    for(i=0; i<ninsts_total; i++) {
	if(instances[i].inst)
	    continue;
	instances[i].inst = inst;
	instances[i].refcnt = 0;
	ninsts_avail--;
	logg("add_instance: now %u/%u instances available\n", ninsts_avail, ninsts_total);
	unlock_instances();
	return 0;
    }
    logg("!add_instances: you should not be reading this\n");
    unlock_instances();
    return 1;
}

static int del_instance(instance *inst) {
    unsigned int i;

    INFN();
    if(lock_instances()) {
	logg("!del_instance: failed to lock instances\n");
	return CL_ELOCK;
    }
    for(i=0; i<ninsts_total; i++) {
	if(instances[i].inst != inst)
	    continue;
	if(instances[i].refcnt) {
	    logg("!del_instance: attempted to free instance with %d active scanners\n", instances[i].refcnt);
	    unlock_instances();
	    return CL_EBUSY;
	}
	instances[i].inst = NULL;
	instances[i].refcnt = 0;
	ninsts_avail++;
	logg("del_instance: %u / %u instances now available\n", ninsts_avail, ninsts_total);
	unlock_instances();
	return CL_SUCCESS;
    }
    logg("!del_instances: instance not found\n");
    unlock_instances();
    return CL_EARG;
}

/* To be called with the instances locked */
static int is_instance(instance *inst) {
    unsigned int i;
    INFN();
    for(i=0; i<ninsts_total; i++)
	if(instances[i].inst == inst)
	    return 1;
    logg("^is_instance: lookup failed for instance %p\n", inst);
    return 0;
}

BOOL interface_setup(void) {
    if(!(engine_mutex = CreateMutex(NULL, FALSE, NULL)))
	return FALSE;
    if(!(reload_event = CreateEvent(NULL, TRUE, TRUE, NULL))) {
	CloseHandle(engine_mutex);
	return FALSE;
    }
    if(!(monitor_event = CreateEvent(NULL, TRUE, FALSE, NULL))) {
	CloseHandle(reload_event);
	CloseHandle(engine_mutex);
	return FALSE;
    }
    if(!(instance_mutex = CreateMutex(NULL, FALSE, NULL))) {
	CloseHandle(monitor_event);
	CloseHandle(reload_event);
	CloseHandle(engine_mutex);
	return FALSE;
    }
    return TRUE;
}

static int sigload_callback(const char *type, const char *name, void *context) {
    if(minimal_definitions && strcmp(type, "fp"))
	return 1;
    return 0;
}

const char* cli_ctime(const time_t *timep, char *buf, const size_t bufsize);
/* Must be called with engine_mutex locked ! */
static void touch_last_update(unsigned signo) {
    char touchme[PATH_MAX];
    HANDLE h;

    snprintf(touchme, sizeof(touchme), "%s\\lastupd", dbdir);
    touchme[sizeof(touchme)-1] = '\0';
    if((h = CreateFile(touchme, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL)) != INVALID_HANDLE_VALUE) {
	DWORD d;
	int err;
	unsigned ver = (unsigned)cl_engine_get_num(engine, CL_ENGINE_DB_VERSION, &err);
	if (ver) {
	    char timestr[32];
	    const char *tstr;
	    time_t t;
	    t = cl_engine_get_num(engine, CL_ENGINE_DB_TIME, NULL);
	    tstr = cli_ctime(&t, timestr, sizeof(timestr));
	    /* cut trailing \n */
	    timestr[strlen(tstr)-1] = '\0';
	    snprintf(touchme, sizeof(touchme), "daily %u/%u sigs\n"
		     "Database version: %u/%s\n"
		     "Known viruses: %u\n"
		     "Reloaded at: %d\n",
		     ver, signo, ver, tstr, signo, (unsigned)time(NULL));
	} else {
	    snprintf(touchme, sizeof(touchme), "no daily/%u sigs\n"
		     "Known viruses: %u\n"
		     "Reloaded at: %d\n",
		     signo, signo, (unsigned)time(NULL));
	}
	touchme[sizeof(touchme)-1] = '\0';
	if(WriteFile(h, touchme, strlen(touchme), &d, NULL)) {
	    /* SetEndOfFile(h); */
	    GetFileTime(h, NULL, NULL, &last_chk_time);
	}
	CloseHandle(h);
    } else
	logg("touch_lastcheck: failed to touch lastreload\n");
}


/* Must be called with engine_mutex locked ! */
static int load_db(void) {
    unsigned int signo = 0;
    size_t used, total;
    int ret;


    INFN();

    cl_engine_set_clcb_sigload(engine, sigload_callback, NULL);
    if((ret = cl_load(dbdir, engine, &signo, CL_DB_STDOPT & ~CL_DB_PHISHING & ~CL_DB_PHISHING_URLS)) != CL_SUCCESS) {
	cl_engine_free(engine);
	engine = NULL;
	FAIL(ret, "Failed to load database: %s", cl_strerror(ret));
    }

    if((ret = cl_engine_compile(engine))) {
	cl_engine_free(engine);
	engine = NULL;
	FAIL(ret, "Failed to compile engine: %s", cl_strerror(ret));
    }

    logg("load_db: loaded %d signatures\n", signo);
    if (!mpool_getstats(engine, &used, &total))
	logg("load_db: memory %.3f MB / %.3f MB\n", used/(1024*1024.0), total/(1024*1024.0));

    touch_last_update(signo);

    WIN();
}

static void free_engine_and_unlock(void) {
    cl_engine_free(engine);
    engine = NULL;
    unlock_engine();
}

int CLAMAPI Scan_Initialize(const wchar_t *pEnginesFolder, const wchar_t *pTempRoot, const wchar_t *pLicenseKey, BOOL bLoadMinDefs) {
    BOOL cant_convert;
    int ret;

    logg("in Scan_Initialize(pEnginesFolder = %S, pTempRoot = %S)\n", pEnginesFolder, pTempRoot);
    if(!pEnginesFolder)
	FAIL(CL_ENULLARG, "pEnginesFolder is NULL");
    if(!pTempRoot)
	FAIL(CL_ENULLARG, "pTempRoot is NULL");
    if(lock_engine())
	FAIL(CL_ELOCK, "failed to lock engine");
    if(engine) {
	unlock_engine();
	FAIL(CL_ESTATE, "Already initialized");
    }

    if(!(engine = cl_engine_new())) {
	unlock_engine();
	FAIL(CL_EMEM, "Not enough memory for a new engine");
    }
    cl_engine_set_clcb_pre_scan(engine, prescan_cb);
    cl_engine_set_clcb_post_scan(engine, postscan_cb);
    
    minimal_definitions = bLoadMinDefs;
    if(bLoadMinDefs)
	logg("!MINIMAL DEFINITIONS MODE ON!\n");

    if(!WideCharToMultiByte(CP_ACP, WC_NO_BEST_FIT_CHARS, pTempRoot, -1, tmpdir, sizeof(tmpdir), NULL, &cant_convert) || cant_convert) {
	free_engine_and_unlock();
	FAIL(CL_EARG, "Can't translate pTempRoot");
    }
    if((ret = cl_engine_set_str(engine, CL_ENGINE_TMPDIR, tmpdir))) {
	free_engine_and_unlock();
	FAIL(ret, "Failed to set engine tempdir: %s", cl_strerror(ret));
    }
    if(!WideCharToMultiByte(CP_ACP, WC_NO_BEST_FIT_CHARS, pEnginesFolder, -1, dbdir, sizeof(dbdir), NULL, &cant_convert) || cant_convert) {
	free_engine_and_unlock();
	FAIL(CL_EARG, "Can't translate pEnginesFolder");
    }
    ret = load_db();
    unlock_engine();

    ResetEvent(monitor_event);
    if(!(monitor_hdl = CreateThread(NULL, 0, monitor_thread, NULL, 0, NULL)))
	logg("!Falied to start db monitoring thread\n");

    logg("Scan_Initialize: returning %d\n", ret);
    return ret;
}

int uninitialize_called = 0;
int CLAMAPI Scan_Uninitialize(void) {
 //   int rett;
 //   __asm {
	//MOV eax, [ebp + 4]
	//mov rett, eax
 //   }
 //   logg("%x", rett);
    uninitialize_called = 1;
    INFN();
    if(lock_engine())
	FAIL(CL_ELOCK, "failed to lock engine");
    if(!engine) {
	unlock_engine();
	FAIL(CL_ESTATE, "attempted to uninit a NULL engine");
    }
   if(lock_instances()) {
	unlock_engine();
	FAIL(CL_ELOCK, "failed to lock instances");
    }
    if(ninsts_avail != ninsts_total) {
	volatile unsigned int refcnt = ninsts_total - ninsts_avail;
	unlock_instances();
	unlock_engine();
	FAIL(CL_EBUSY, "Attempted to uninit the engine with %u active instances", refcnt);
    }
    unlock_instances();
    free_engine_and_unlock();

    if(monitor_hdl) {
	SetEvent(monitor_event);
	if(WaitForSingleObject(monitor_hdl, 5000) != WAIT_OBJECT_0) {
	    logg("Scan_Uninitialize: forcibly terminating monitor thread after 60 seconds\n");
	    TerminateThread(monitor_hdl, 0);
	}
    }
    monitor_hdl = NULL;
    WIN();
}

int CLAMAPI Scan_CreateInstance(CClamAVScanner **ppScanner) {
    instance *inst;

    INFN();
    if(!ppScanner)
	FAIL(CL_ENULLARG, "NULL pScanner");
    inst = calloc(1, sizeof(*inst));
    if(!inst)
	FAIL(CL_EMEM, "CreateInstance: OOM");
    if(lock_engine()) {
	free(inst);
	FAIL(CL_ELOCK, "Failed to lock engine");
    }
    if(!engine) {
	free(inst);
	unlock_engine();
	FAIL(CL_ESTATE, "Create instance called with no engine");
    }
    if(add_instance(inst)) {
	free(inst);
	unlock_engine();
	FAIL(CL_EMEM, "add_instance failed");
    }
    unlock_engine();
    inst->scanopts = CL_SCAN_STDOPT;
    *ppScanner = (CClamAVScanner *)inst;
    logg("Created new instance %p\n", inst);
    WIN();
}

int CLAMAPI Scan_DestroyInstance(CClamAVScanner *pScanner) {
    int rc;
    INFN();
    if(!pScanner)
	FAIL(CL_ENULLARG, "NULL pScanner");
    if((rc = del_instance((instance *)pScanner)))
	FAIL(rc, "del_instance failed for %p", pScanner);
    free(pScanner);
    logg("in Scan_DestroyInstance: Instance %p destroyed\n", pScanner);
    WIN();
}

int CLAMAPI Scan_SetScanCallback(CClamAVScanner *pScanner, CLAM_SCAN_CALLBACK pfnCallback, void *pContext) {
    instance *inst;

    logg("in SetScanCallback(pScanner = %p, pfnCallback = %p, pContext = %p)\n", pScanner, pfnCallback, pContext);
    if(!pScanner)
	FAIL(CL_ENULLARG, "NULL pScanner");
    if(lock_instances())
	FAIL(CL_ELOCK, "failed to lock instances for instance %p", pScanner);

    inst = (instance *)pScanner;
    if(is_instance(inst)) {
	inst->scancb = pfnCallback;
	inst->scancb_ctx = pContext;
	unlock_instances();
	WIN();
    }
    unlock_instances();
    FAIL(CL_EARG, "invalid instance %p", inst);
}

int CLAMAPI Scan_SetOption(CClamAVScanner *pScanner, int option, void *value, unsigned long inputLength) {
    instance *inst;
    unsigned int whichopt, newval;
    
    INFN();
    if(!pScanner)
	FAIL(CL_ENULLARG, "NULL pScanner");
    if(!value)
	FAIL(CL_ENULLARG, "NULL value");
    if(lock_instances())
	FAIL(CL_ELOCK, "failed to lock instances");

    inst = (instance *)pScanner;
    if(!is_instance(inst)) {
	unlock_instances();
	FAIL(CL_EARG, "invalid instance %p", inst);
    }
    newval = *(unsigned int *)value;
    switch(option) {
	case CLAM_OPTION_SCAN_ARCHIVE:
	    logg("CLAM_OPTION_SCAN_ARCHIVE: %s on instance %p\n", newval ? "enabled" : "disabled", inst);
	    whichopt = CL_SCAN_ARCHIVE;
	    break;
	case CLAM_OPTION_SCAN_MAIL:
	    logg("CLAM_OPTION_SCAN_MAIL: %s on instance %p\n", newval ? "enabled" : "disabled", inst);
	    whichopt = CL_SCAN_MAIL;
	    break;
	case CLAM_OPTION_SCAN_OLE2:
	    logg("CLAM_OPTION_SCAN_OLE2: %s on instance %p\n", newval ? "enabled" : "disabled", inst);
	    whichopt = CL_SCAN_OLE2;
	    break;
	case CLAM_OPTION_SCAN_HTML:
	    logg("CLAM_OPTION_SCAN_HTML: %s on instance %p\n", newval ? "enabled" : "disabled", inst);
	    whichopt = CL_SCAN_HTML;
	    break;
	case CLAM_OPTION_SCAN_PE:
	    logg("CLAM_OPTION_SCAN_PE: %s on instance %p\n", newval ? "enabled" : "disabled", inst);
	    whichopt = CL_SCAN_PE;
	    break;
	case CLAM_OPTION_SCAN_PDF:
	    logg("CLAM_OPTION_SCAN_PDF: %s on instance %p\n", newval ? "enabled" : "disabled", inst);
	    whichopt = CL_SCAN_PDF;
	    break;
	case CLAM_OPTION_SCAN_ALGORITHMIC:
	    logg("CLAM_OPTION_SCAN_ALGORITHMIC: %s on instance %p\n", newval ? "enabled" : "disabled", inst);
	    whichopt = CL_SCAN_ALGORITHMIC;
	    break;
	case CLAM_OPTION_SCAN_ELF:
	    logg("CLAM_OPTION_SCAN_ELF: %s on instance %p\n", newval ? "enabled" : "disabled", inst);
	    whichopt = CL_SCAN_ELF;
	    break;
	default:
	    unlock_instances();
	    FAIL(CL_EARG, "Unsupported option: %d", option);
    }

    if(!newval)
	inst->scanopts &= ~whichopt;
    else
	inst->scanopts |= whichopt;
    unlock_instances();
    WIN();
}

int CLAMAPI Scan_GetOption(CClamAVScanner *pScanner, int option, void *value, unsigned long inputLength, unsigned long *outLength) {
    instance *inst;
    unsigned int whichopt;

    INFN();
    if(!pScanner)
	FAIL(CL_ENULLARG, "NULL pScanner");
    if(!value || !inputLength)
	FAIL(CL_ENULLARG, "NULL value");
    if(lock_instances())
	FAIL(CL_ELOCK, "failed to lock instances");

    inst = (instance *)pScanner;
    if(!is_instance(inst)) {
	unlock_instances();
	FAIL(CL_EARG, "invalid instance %p", inst);
    }
    switch(option) {
	case CLAM_OPTION_SCAN_ARCHIVE:
	    whichopt = CL_SCAN_ARCHIVE;
	    break;
	case CLAM_OPTION_SCAN_MAIL:
	    whichopt = CL_SCAN_MAIL;
	    break;
	case CLAM_OPTION_SCAN_OLE2:
	    whichopt = CL_SCAN_OLE2;
	    break;
	case CLAM_OPTION_SCAN_HTML:
	    whichopt = CL_SCAN_HTML;
	    break;
	case CLAM_OPTION_SCAN_PE:
	    whichopt = CL_SCAN_PE;
	    break;
	case CLAM_OPTION_SCAN_PDF:
	    whichopt = CL_SCAN_PDF;
	    break;
	case CLAM_OPTION_SCAN_ALGORITHMIC:
	    whichopt = CL_SCAN_ALGORITHMIC;
	    break;
	case CLAM_OPTION_SCAN_ELF:
	    whichopt = CL_SCAN_ELF;
	    break;
	default:
	    unlock_instances();
	    FAIL(CL_EARG, "Unsupported option: %d", option);
    }

    *(unsigned int *)value = (inst->scanopts & whichopt) != 0;
    unlock_instances();
    WIN();
}


int CLAMAPI Scan_GetLimit(int option, unsigned int *value) {
    enum cl_engine_field limit;
    long long curlimit;
    int err;

    INFN();
    if(lock_engine())
	FAIL(CL_ELOCK, "Failed to lock engine");
    if(!engine) {
	unlock_engine();
	FAIL(CL_ESTATE, "Engine is NULL");
    }
    switch((enum CLAM_LIMIT_TYPE)option) {
    case CLAM_LIMIT_FILESIZE:
	limit = CL_ENGINE_MAX_FILESIZE;
	break;
    case CLAM_LIMIT_SCANSIZE:
	limit = CL_ENGINE_MAX_SCANSIZE;
	break;
    case CLAM_LIMIT_RECURSION:
	limit = CL_ENGINE_MAX_SCANSIZE;
	break;
    default:
	unlock_engine();
	FAIL(CL_EARG, "Unsupported limit type: %d", option);
    }
    curlimit = cl_engine_get_num(engine, limit, &err);
    if(err) {
	unlock_engine();
	FAIL(err, "Failed to get engine value: %s", cl_strerror(err));
    }
    if(curlimit > 0xffffffff)
	*value = 0xffffffff;
    else
	*value = (unsigned int)curlimit;
    unlock_engine();
    WIN();
}


int CLAMAPI Scan_SetLimit(int option, unsigned int value) {
    enum cl_engine_field limit;
    int err;

    INFN();
    if(lock_engine())
	FAIL(CL_ELOCK, "Failed to lock engine");
    if(!engine) {
	unlock_engine();
	FAIL(CL_ESTATE, "Engine is NULL");
    }
    switch((enum CLAM_LIMIT_TYPE)option) {
    case CLAM_LIMIT_FILESIZE:
	limit = CL_ENGINE_MAX_FILESIZE;
	break;
    case CLAM_LIMIT_SCANSIZE:
	limit = CL_ENGINE_MAX_SCANSIZE;
	break;
    case CLAM_LIMIT_RECURSION:
	limit = CL_ENGINE_MAX_SCANSIZE;
	break;
    default:
	unlock_engine();
	FAIL(CL_EARG, "Unsupported limit type: %d", option);
    }
    err = cl_engine_set_num(engine, limit, (long long)value);
    unlock_engine();
    if(err)
	FAIL(err, "Failed to set engine value: %s", cl_strerror(err));
    WIN();
}


int CLAMAPI Scan_ScanObject(CClamAVScanner *pScanner, const wchar_t *pObjectPath, int *pScanStatus, PCLAM_SCAN_INFO_LIST *pInfoList) {
    HANDLE fhdl;
    int res;
    instance *inst = (instance *)pScanner;

    logg("in Scan_ScanObject(pScanner = %p, pObjectPath = %S)\n", pScanner, pObjectPath);
    if((fhdl = CreateFileW(pObjectPath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_FLAG_RANDOM_ACCESS, NULL)) == INVALID_HANDLE_VALUE)
	FAIL(CL_EOPEN, "open() failed");

    logg("Scan_ScanObject (instance %p) invoking Scan_ScanObjectByHandle for handle %p (%S)\n", pScanner, fhdl, pObjectPath);
    res = Scan_ScanObjectByHandle(pScanner, fhdl, pScanStatus, pInfoList);
    logg("Scan_ScanObject (instance %p) invoking Scan_ScanObjectByHandle returned %d\n", pScanner, res);
    CloseHandle(fhdl);
    return res;
}

struct scan_ctx {
    int entryfd;
    instance *inst;
};

int CLAMAPI Scan_ScanObjectByHandle(CClamAVScanner *pScanner, HANDLE object, int *pScanStatus, PCLAM_SCAN_INFO_LIST *pInfoList) {
    instance *inst;
    HANDLE duphdl, self;
    char *virname = NULL;
    int fd, res;
    unsigned int i;
    struct scan_ctx sctx;
    DWORD perf;

    logg("in Scan_ScanObjectByHandle(pScanner = %p, HANDLE = %p, pScanStatus = %p, pInfoList = %p)\n", pScanner, object, pScanStatus, pInfoList);

    if(!pScanner)
	FAIL(CL_ENULLARG, "NULL pScanner");
    if(!pScanStatus)
	FAIL(CL_ENULLARG, "NULL pScanStatus on instance %p", pScanner);

    self = GetCurrentProcess();
    if(!DuplicateHandle(self, object, self, &duphdl, GENERIC_READ, FALSE, 0))
	FAIL(CL_EDUP, "Duplicate handle failed for instance %p", pScanner);

    if((fd = _open_osfhandle((intptr_t)duphdl, _O_RDONLY)) == -1) {
	CloseHandle(duphdl);
	FAIL(CL_EOPEN, "Open handle failed for instance %p", pScanner);
    }

    if(lock_instances()) {
	close(fd);
	FAIL(CL_ELOCK, "failed to lock instances for instance %p", pScanner);
    }
    inst = (instance *)pScanner;
    for(i=0; i<ninsts_total; i++) {
	if(instances[i].inst == inst)
	    break;
    }
    if(i == ninsts_total) {
	unlock_instances();
	close(fd);
	FAIL(CL_EARG, "invalid instance %p", inst);
    }
    instances[i].refcnt++;
    ResetEvent(reload_event);
    unlock_instances();

    sctx.entryfd = fd;
    sctx.inst = inst;
    logg("Scan_ScanObjectByHandle (instance %p) invoking cl_scandesc with clamav context %p\n", inst, &sctx);
    perf = GetTickCount();
    res = cl_scandesc_callback(fd, &virname, NULL, engine, inst->scanopts, &sctx);

    do {
	CLAM_SCAN_INFO si;
	CLAM_ACTION act;
	DWORD cbperf;
	wchar_t wvirname[MAX_VIRNAME_LEN];
	LONG lo = 0, hi = 0, hi2 = 0;

	si.cbSize = sizeof(si);
	si.flags = 0;
	si.scanPhase = SCAN_PHASE_FINAL;
	si.errorCode = CLAMAPI_SUCCESS;
	if(res == CL_VIRUS) {
	    if(MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, virname, -1, wvirname, MAX_VIRNAME_LEN))
		si.pThreatName = wvirname;
	    else
		si.pThreatName = L"INFECTED";
	} else
	    si.pThreatName = NULL;
	logg("in final_cb with clamav context %p, instance %p, fd %d, result %d, virusname %S)\n", &sctx, inst, fd, res, si.pThreatName);
	si.pThreatType = threat_type(virname);
	si.object = duphdl;
	si.pInnerObjectPath = NULL;
	lo = SetFilePointer(duphdl, 0, &hi, FILE_CURRENT);
	SetFilePointer(duphdl, 0, &hi2, FILE_BEGIN);
	logg("final_cb (clamav context %p, instance %p) invoking callback %p with context %p\n", &sctx, inst, inst->scancb, inst->scancb_ctx);
	cbperf = GetTickCount();
	inst->scancb(&si, &act, inst->scancb_ctx);
	cbperf = GetTickCount() - cbperf;
	logg("final_cb (clamav context %p, instance %p) callback completed with %u (result ignored) in %u ms\n", &sctx, inst, act, cbperf);
	SetFilePointer(duphdl, lo, &hi, FILE_BEGIN);
    } while(0);

    perf = GetTickCount() - perf;
    close(fd);
    logg("Scan_ScanObjectByHandle (instance %p): cl_scandesc returned %d in %u ms\n", inst, res, perf);

    if(lock_instances())
	FAIL(CL_ELOCK, "failed to lock instances for instance %p", pScanner);
    instances[i].refcnt--;
    if(!instances[i].refcnt)
	SetEvent(reload_event);
    unlock_instances();

    if(res == CL_VIRUS) {
	logg("Scan_ScanObjectByHandle (instance %p): file is INFECTED with %s\n", inst, virname);
	if(pInfoList) {
	    CLAM_SCAN_INFO_LIST *infolist = calloc(1, sizeof(CLAM_SCAN_INFO_LIST) + sizeof(CLAM_SCAN_INFO) + MAX_VIRNAME_LEN * 2);
	    PCLAM_SCAN_INFO scaninfo;
	    wchar_t *wvirname;
	    if(!infolist)
		FAIL(CL_EMEM, "ScanByHandle (instance %p): OOM while allocating result list", inst);
	    scaninfo = (PCLAM_SCAN_INFO)(infolist + 1);
	    infolist->cbCount = 1;
	    scaninfo->cbSize = sizeof(*scaninfo);
	    scaninfo->scanPhase = SCAN_PHASE_FINAL;
	    scaninfo->errorCode = CLAMAPI_SUCCESS;
	    scaninfo->pThreatType = threat_type(virname);
	    wvirname = (wchar_t *)(scaninfo + 1);
	    scaninfo->pThreatName = wvirname;
	    if(!MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, virname, -1, wvirname, MAX_VIRNAME_LEN))
		scaninfo->pThreatName = L"INFECTED";
	    *pInfoList = infolist;
	    logg("Scan_ScanObjectByHandle (instance %p): created result list %p\n", inst, infolist);
	}
	*pScanStatus = CLAM_INFECTED;
    } else if(res == CL_CLEAN) {
	logg("Scan_ScanObjectByHandle (instance %p): file is CLEAN\n", inst);
        if(pInfoList) *pInfoList = NULL;
	*pScanStatus = CLAM_CLEAN;
    } else {
	FAIL(res, "Scan failed for instance %p: %s", inst, cl_strerror(res));
    }
    WIN();
}


int CLAMAPI Scan_DeleteScanInfo(CClamAVScanner *pScanner, PCLAM_SCAN_INFO_LIST pInfoList) {
    logg("in Scan_DeleteScanInfo(pScanner = %p, pInfoList = %p)\n", pScanner, pInfoList);
    if(!pScanner)
	FAIL(CL_ENULLARG, "NULL pScanner");
    if(!pInfoList)
	FAIL(CL_ENULLARG, "NULL pInfoList");
    /* FIXME checking this is pointelss as the infolist is independent from pscanner */
 //   if(lock_instances())
	//FAIL(CL_EMEM, "failed to lock instances");

 //   inst = (instance *)pScanner;
 //   if(!is_instance(inst)) {
	//unlock_instances();
	//FAIL(CL_EARG, "invalid instance");
 //   }
 //   unlock_instances();

    free(pInfoList);
    WIN();
}

cl_error_t prescan_cb(int fd, void *context) {
    struct scan_ctx *sctx = (struct scan_ctx *)context;
    instance *inst;
    CLAM_SCAN_INFO si;
    CLAM_ACTION act;
    HANDLE fdhdl;
    DWORD perf;
    LONG lo = 0, hi = 0, hi2 = 0;

    if(!context) {
	logg("!prescan_cb called with NULL clamav context\n");
	return CL_CLEAN;
    }
    inst = sctx->inst;
    logg("in prescan_cb with clamav context %p, instance %p, fd %d)\n", context, inst, fd);
    si.cbSize = sizeof(si);
    si.flags = 0;
    si.scanPhase = (fd == sctx->entryfd) ? SCAN_PHASE_INITIAL : SCAN_PHASE_PRESCAN;
    si.errorCode = CLAMAPI_SUCCESS;
    si.pThreatType = NULL;
    si.pThreatName = NULL;
    fdhdl = si.object = (HANDLE)_get_osfhandle(fd);
    si.pInnerObjectPath = NULL;

    lo = SetFilePointer(fdhdl, 0, &hi, FILE_CURRENT);
    SetFilePointer(fdhdl, 0, &hi2, FILE_BEGIN);
    logg("prescan_cb (clamav context %p, instance %p) invoking callback %p with context %p\n", context, inst, inst->scancb, inst->scancb_ctx);
    perf = GetTickCount();
    inst->scancb(&si, &act, inst->scancb_ctx);
    perf = GetTickCount() - perf;
    logg("prescan_cb (clamav context %p, instance %p) callback completed with %u in %u ms\n", context, inst, act, perf);
    SetFilePointer(fdhdl, lo, &hi, FILE_BEGIN);
    switch(act) {
	case CLAM_ACTION_SKIP:
	    logg("prescan_cb (clamav context %p, instance %p) cb result: SKIP\n", context, inst);
	    return CL_BREAK;
	case CLAM_ACTION_ABORT:
	    logg("prescan_cb (clamav context %p, instance %p) cb result: ABORT\n", context, inst);
	    return CL_VIRUS;
	case CLAM_ACTION_CONTINUE:
	    logg("prescan_cb (clamav context %p, instance %p) cb result: CONTINUE\n", context, inst);
	    return CL_CLEAN;
	default:
	    logg("^prescan_cb (clamav context %p, instance %p) cb result: INVALID result %d, assuming continue\n", context, inst, act);
	    return CL_CLEAN;
    }
}

cl_error_t postscan_cb(int fd, int result, const char *virname, void *context) {
    struct scan_ctx *sctx = (struct scan_ctx *)context;
    instance *inst;
    CLAM_SCAN_INFO si;
    CLAM_ACTION act;
    HANDLE fdhdl;
    DWORD perf;
    wchar_t wvirname[MAX_VIRNAME_LEN];
    LONG lo = 0, hi = 0, hi2 = 0;

    if(!context) {
	logg("!postscan_cb called with NULL clamav context\n");
	return CL_CLEAN;
    }
    if(fd == sctx->entryfd)
	return CL_CLEAN; /* Moved to after cl_scandesc returns due to heuristic results not being yet set in magicscan */

    inst = sctx->inst;
    si.cbSize = sizeof(si);
    si.flags = 0;
    si.scanPhase = SCAN_PHASE_POSTSCAN;
    si.errorCode = CLAMAPI_SUCCESS;
    if(result == CL_VIRUS) {
	if(MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, virname, -1, wvirname, MAX_VIRNAME_LEN))
	    si.pThreatName = wvirname;
	else
	    si.pThreatName = L"INFECTED";
    } else
	    si.pThreatName = NULL;
    logg("in postscan_cb with clamav context %p, instance %p, fd %d, result %d, virusname %S)\n", context, inst, fd, result, si.pThreatName);
    si.pThreatType = threat_type(virname);
    fdhdl = si.object = (HANDLE)_get_osfhandle(fd);
    si.pInnerObjectPath = NULL;
    lo = SetFilePointer(fdhdl, 0, &hi, FILE_CURRENT);
    SetFilePointer(fdhdl, 0, &hi2, FILE_BEGIN);
    logg("postscan_cb (clamav context %p, instance %p) invoking callback %p with context %p\n", context, inst, inst->scancb, inst->scancb_ctx);
    perf = GetTickCount();
    inst->scancb(&si, &act, inst->scancb_ctx);
    perf = GetTickCount() - perf;
    logg("postscan_cb (clamav context %p, instance %p) callback completed with %u in %u ms\n", context, inst, act, perf);
    SetFilePointer(fdhdl, lo, &hi, FILE_BEGIN);
    switch(act) {
	case CLAM_ACTION_SKIP:
	    logg("postscan_cb (clamav context %p, instance %p) cb result: SKIP\n", context, inst);
	    return CL_BREAK;
	case CLAM_ACTION_ABORT:
	    logg("postscan_cb (clamav context %p, instance %p) cb result: ABORT\n", context, inst);
	    return CL_VIRUS;
	case CLAM_ACTION_CONTINUE:
	    logg("postscan_cb (clamav context %p, instance %p) cb result: CONTINUE\n", context, inst);
	    return CL_CLEAN;
	default:
	    logg("^postscan_cb (clamav context %p, instance %p) cb result: INVALID result %d, assuming continue\n", context, inst, act);
	    return CL_CLEAN;
    }
}

CLAMAPI void Scan_ReloadDatabase(void) {
    if(InterlockedIncrement(&reload_waiters)==1) {
	int reload_ok = 0;
	logg("Scan_ReloadDatabase: Database reload requested received, waiting for idle state\n");
	while(1) {
	    unsigned int i;
	    int ret;
	    if(WaitForSingleObject(reload_event, INFINITE) == WAIT_FAILED) {
		logg("!Scan_ReloadDatabase: failed to wait on reload event\n");
		continue;
	    }
	    logg("Scan_ReloadDatabase: Now idle, acquiring engine lock\n");
	    if(lock_engine()) {
		logg("!Scan_ReloadDatabase: failed to lock engine\n");
		break;
	    }
	    if(!engine) {
		logg("!Scan_ReloadDatabase: engine is NULL\n");
		unlock_engine();
		break;
	    }
	    logg("Scan_ReloadDatabase: Engine locked, acquiring instance lock\n");
	    if(lock_instances()) {
		logg("!Scan_ReloadDatabase: failed to lock instances\n");
		unlock_engine();
		break;
	    }
            for(i=0; i<ninsts_total; i++) {
		if(instances[i].inst && instances[i].refcnt)
		    break;
	    }
	    if(i!=ninsts_total) {
		logg("Scan_ScanObjectByHandle: some instances are still in use\n");
		ResetEvent(reload_event);
		unlock_instances();
		unlock_engine();
		continue;
	    }
	    logg("Scan_ReloadDatabase: Destroying old engine\n");
	    cl_engine_free(engine);
	    logg("Scan_ReloadDatabase: Loading new engine\n");

	    // NEW STUFF //
	    if(!(engine = cl_engine_new())) {
		logg("!Scan_ReloadDatabase: Not enough memory for a new engine\n");
		unlock_instances();
		unlock_engine();
		break;
	    }
	    cl_engine_set_clcb_pre_scan(engine, prescan_cb);
	    cl_engine_set_clcb_post_scan(engine, postscan_cb);
    
	    if((ret = cl_engine_set_str(engine, CL_ENGINE_TMPDIR, tmpdir))) {
		unlock_instances();
		free_engine_and_unlock();
		logg("!Scan_ReloadDatabase: Failed to set engine tempdir: %s\n", cl_strerror(ret));
		break;
	    }

	    load_db(); /* FIXME: FIAL? */
	    unlock_instances();
	    unlock_engine();
	    reload_ok = 1;
	    break;
	}
	if(reload_ok)
	    logg("Scan_ReloadDatabase: Database successfully reloaded\n");
	else
	    logg("!Scan_ReloadDatabase: Database reload failed\n");
    } else
	logg("^Database reload requested received while reload is pending\n");
    InterlockedDecrement(&reload_waiters);
}

void msg_callback(enum cl_msg severity, const char *fullmsg, const char *msg, void *ctx)
{
    struct scan_ctx *sctx = (struct scan_ctx*)ctx;
    const void *instance = sctx ? sctx->inst : NULL;
    int fd = sctx ? sctx->entryfd : -1;
    char sv;
    switch (severity) {
	case CL_MSG_ERROR:
	    sv = '!';
	    break;
	case CL_MSG_WARN:
	    sv = '^';
	    break;
	default:
	    sv = '*';
	    break;
    }

    logg("%c[LibClamAV] (instance %p, clamav context %p, fd %d): %s",
	 sv, instance, sctx, fd, msg);
}