#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>
#include <time.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <dirent.h>
#include <sys/stat.h>
#include <unistd.h>

#if defined(HAVE_CLAMAV)

# include <clamav.h>
#else
#warning No antivirus library defined, this module will not perform any virus scanning.
#endif

#include <glib.h>

#include "../include/fio.h"
#include "../include/disk.h"
#include "../include/prochandle.h"

#include "avscanop.h"
#include "config.h"


/* Utilities */
static gchar *FORMATED_NUMBER_STRING(gulong i);
static gchar *TIME_LAPSED_STRING(gulong dt);
static void PRINT_ERR(const gchar *path, const gchar *mesg);
static void PRINT_SCANNING_FILE(
	const gulong i, const gulong n, const gchar *path
);
static void PRINT_SCANNING_DIR(const gchar *path);
static void PRINT_REPORT(
	gulong time_start, gulong time_end,
	gulong infected_files,
	gulong size_scanned_mb,
	gulong files_scanned 
);

static gchar *AVScanCopyShortenString(const gchar *s, const gint m);


#if defined(HAVE_CLAMAV)
typedef struct {
	gchar		*database_path;		/* Database file or directory */
 	gchar		*database_dir;		/* Database directory */
	struct cl_node	*database;		/* Database handle */
	gint		database_entries;	/* Entries in the database */
	struct cl_stat	database_stat;		/* Database stats, for
						 * checking database changes */
	gulong		files_scanned,		/* Files scanned so far */
			size_scanned_mb,
			infected_files,
			total_files;		/* Total files to be scanned */
} ClamAVData;
static ClamAVData *AVScanOPClamAVInit(const gchar *db_path);
static void AVScanOPClamAVShutdown(ClamAVData *d);
static gint AVScanOPClamAVScanFile(
	ClamAVData *d, const gchar *path,
	gint *stop_count
);
static gint AVScanOPClamAVScanDir(
	ClamAVData *d, const gchar *path,
	const gboolean recursive,
	const gboolean executables_only,
	const gboolean ignore_links,
	gint *stop_count
);
#endif	/* HAVE_CLAMAV */


static gulong AVScanOPCalculateTotalFilesInDirIterate(
	const gchar *path,
	const gboolean recursive,
	const gboolean executables_only,
	const gboolean ignore_links,
	gint *stop_count
);
static gulong AVScanOPCalculateTotalFiles(
	GList *paths_list,
	const gboolean recursive,
	const gboolean executables_only,
	const gboolean ignore_links,
	gint *stop_count
);

gchar *AVScanGetAVEngineNameVersionString(void);
gint AVScanOP(
	const gchar *db_path,
	GList *paths_list,
	const gboolean recursive,
	const gboolean executables_only,
	const gboolean ignore_links,
	gint *stop_count
);


#define ATOI(s)		(((s) != NULL) ? atoi(s) : 0)
#define ATOL(s)		(((s) != NULL) ? atol(s) : 0)
#define ATOF(s)		(((s) != NULL) ? atof(s) : 0.0f)
#define STRDUP(s)	(((s) != NULL) ? g_strdup(s) : NULL)

#define MAX(a,b)	(((a) > (b)) ? (a) : (b))
#define MIN(a,b)	(((a) < (b)) ? (a) : (b))
#define CLIP(a,l,h)	(MIN(MAX((a),(l)),(h)))
#define STRLEN(s)	(((s) != NULL) ? (gint)strlen(s) : 0)
#define STRISEMPTY(s)	(((s) != NULL) ? (*(s) == '\0') : TRUE)


/*
 *	Returns a dynamically allocated string describing the
 *	specified number.
 */
static gchar *FORMATED_NUMBER_STRING(gulong i)
{
	gint comma_cnt, slen;
	gchar *ss, *ss_ptr, *ts, *ts_ptr;

	ss = g_strdup_printf("%ld", i);
	slen = STRLEN(ss);
			  
	/* 3 digits or less? (no commas needed) */
	if((slen <= 3) || (ss == NULL))
	    return(ss);

	ts_ptr = ts = (gchar *)g_malloc(80);
	if(ts == NULL)
	    return(ss);

	ss_ptr = ss;

	/* Initialize comma counter */
	comma_cnt = slen % 3;
	if(comma_cnt <= 0)
	    comma_cnt = 3;

	/* Iterate through size string until end is reached */
	while(*ss_ptr != '\0')
	{
	    /* Reset comma counter and put in a comma? */
	    if(comma_cnt <= 0)
	    {
		*ts_ptr = ',';
		ts_ptr++;
		comma_cnt = 3;
	    }

	    *ts_ptr = *ss_ptr;
	    ts_ptr++;
	    ss_ptr++;
	    comma_cnt--;
	}

	/* Null terminate return string */
	*ts_ptr = '\0';

	g_free(ss);

	return(ts);
}

/*
 *	Returns a dynamically allocated string describing the time
 *	lapsed specified by dt.
 */
static gchar *TIME_LAPSED_STRING(gulong dt)
{
	gchar *s;

	/* Less than one second? */   
	if(dt < 1)
	{
	    s = g_strdup("less than one second");
	}
	/* Less than one minute? */
	else if(dt < (1 * 60))
	{
	    const gulong ct = MAX(dt / 1, 1);
	    s = g_strdup_printf(
		"%ld %s",
		ct, ((ct == 1l) ? "second" : "seconds")
	    );
	}
	/* Less than one hour? */
	else if(dt < (60 * 60))
	{
	    const gulong	ct = MAX(dt / 60, 1),
				ct2 = MAX(dt / 1, 1) % 60;
	    s = g_strdup_printf(
		"%ld %s %ld %s",
		ct, ((ct == 1l) ? "minute" : "minutes"),
		ct2, ((ct2 == 1l) ? "second" : "seconds")
	    );
	}
	/* Less than one day? */
	else if(dt < (24 * 60 * 60))
	{
	    const gulong	ct = MAX(dt / 60 / 60, 1),
				ct2 = MAX(dt / 60, 1) % 60;
	    s = g_strdup_printf(
		"%ld %s %ld %s",
		ct, ((ct == 1l) ? "hour" : "hours"),
		ct2, ((ct2 == 1l) ? "minute" : "minutes")
	    );
	}
	/* Less than one week? */     
	else if(dt < (7 * 24 * 60 * 60))
	{
	    const gulong	ct = MAX(dt / 60 / 60 / 24, 1),
				ct2 = MAX(dt / 60 / 60, 1) % 24;
	    s = g_strdup_printf(
		"%ld %s %ld %s",
		ct, ((ct == 1l) ? "day" : "days"), 
		ct2, ((ct2 == 1l) ? "hour" : "hours")
	    );
	}
	/* Less than one month (30 days)? */
	else if(dt < (30 * 24 * 60 * 60))
	{
	    const gulong	ct = MAX(dt / 60 / 60 / 24 / 7, 1),
				ct2 = MAX(dt / 60 / 60 / 24, 1) % 7;
	    s = g_strdup_printf(
		"%ld %s %ld %s",
		ct, ((ct == 1l) ? "week" : "weeks"),
		ct2, ((ct2 == 1l) ? "day" : "days")
	    );
	}
	/* Less than 6 months ago? */
#if 0
	else if(dt < (6 * 30 * 24 * 60 * 60))
#else
	else
#endif
	{
	    const gulong	ct = MAX(dt / 60 / 60 / 24 / 30, 1),
				ct2 = MAX(dt / 60 / 60 / 24, 1) % 30;
	    s = g_strdup_printf(
		"%ld %s %ld %s",
		ct, ((ct == 1l) ? "month" : "months"),
		ct2, ((ct2 == 1l) ? "day" : "days")
	    );
	}

	return(s);
}


/*
 *	Prints the specified path and message to stderr.
 *
 *	This is intended to indicate and have the monitoring process
 *	record the object specified by path as having a virus or
 *	other problem specified by mesg.
 */
static void PRINT_ERR(const gchar *path, const gchar *mesg)
{
	g_printerr("\"%s\" %s\n", path, mesg);
}

/*
 *	Prints the specified path in a message to stdout to indicate
 *	it is being scanned.
 *
 *	This is intended to indicate and have the monitoring process
 *	display this message on its status.
 */
static void PRINT_SCANNING_FILE(
	const gulong i, const gulong n, const gchar *path
)
{
	g_print("Scanning file: \"%s\" %ld %ld\n", path, i, n);
}
static void PRINT_SCANNING_DIR(const gchar *path)
{
	g_print("Scanning directory: \"%s\"\n", path);
}

/*
 *	Prints the final report in a message to stdout.
 *
 *	This is intended to indicate and have the monitoring process
 *	display this message in a dialog.
 */
static void PRINT_REPORT(
	gulong time_start, gulong time_end,
	gulong infected_files,
	gulong size_scanned_mb,
	gulong files_scanned
)
{
	time_t t = (time_t)time_start;
	gchar	*s,
		*time_lapsed_s = TIME_LAPSED_STRING(
			time_end - time_start
		),
		*time_start_s = STRDUP(ctime(&t)),
		*infected_files_s = FORMATED_NUMBER_STRING(
		    infected_files
		),
		*size_scanned_mb_s = FORMATED_NUMBER_STRING(
		    size_scanned_mb
		),
		*files_scanned_s = FORMATED_NUMBER_STRING(
		    files_scanned
		);

	/* Remove tailing newline character from time_start_s */
	s = strchr(time_start_s, '\n');
	if(s != NULL)
	    *s = '\0';

	/* Note that all ';' characters indicate newlines for the
	 * monitoring process that reads this
	 */
	if(infected_files > 0)
	    g_print(
"ReportInfected:\
Started On: %s;\
Duration: %s;\
;\
Infected Files: %s;\
Files Scanned: %s;\
Data Scanned: %s mb;\
;\
DO NOT VIEW OR RUN THE INFECTED FILES;\
\n",
		time_start_s,
		time_lapsed_s,
		infected_files_s,
		files_scanned_s,
		size_scanned_mb_s
	    );
	else
	    g_print(
"Report:\
Started On: %s;\
Duration: %s;\
;\
No infected files found;\
Files Scanned: %s;\
Data Scanned: %s mb;\
\n",
		time_start_s,
		time_lapsed_s,
		files_scanned_s,
		size_scanned_mb_s
	    );

	g_free(time_lapsed_s);
	g_free(time_start_s);
	g_free(infected_files_s);
	g_free(size_scanned_mb_s);
	g_free(files_scanned_s);
}

/*
 *	Returns a copy of the string that is no longer than max
 *	characters with appropriate shortened notation where 
 *	appropriate.
 */
static gchar *AVScanCopyShortenString(const gchar *s, const gint m)
{
	gint len;

	if(s == NULL)
	    return(NULL);

	len = STRLEN(s);
	if((len > m) && (m > 3))
	{
	    /* Need to shorten string */
	    gint i = len - m + 3;

	    return(g_strdup_printf(
		"...%s", (const gchar *)(s + i)
	    ));
	}      
	else
	{   
	    return(STRDUP(s));
	}
}


#if defined(HAVE_CLAMAV)
/*
 *	Initializes the Clam AntiVirus.
 */
static ClamAVData *AVScanOPClamAVInit(const gchar *db_path)
{
	gint status;
	gchar *s;
	struct stat stat_buf;
	ClamAVData *d = (ClamAVData *)g_malloc0(
	    sizeof(ClamAVData)
	);
	if(d == NULL)
	{
	    g_print("Memory allocation error\n");
	    return(NULL);
	}

	d->files_scanned = 0l;
	d->size_scanned_mb = 0l;
	d->infected_files = 0l;
	d->total_files = 0l;

	/* If no virus database was specified then get the default
	 * virus database set by ClamAV
	 */
	if(db_path == NULL)
	    db_path = cl_retdbdir();
	if(db_path == NULL)
	{
	    g_print("Virus database location was not specified\n");
	    g_free(d);
	    return(NULL);
	}
	d->database_path = STRDUP(db_path);
	if(d->database_path == NULL)
	{
	    g_print("Memory allocation error\n");
	    g_free(d);
	    return(NULL);
	}

	/* Stat and load the virus database */ 
	s = AVScanCopyShortenString(d->database_path, 50);
	g_print("Loading virus database \"%s\"...\n", s);
	g_free(s);
#ifdef S_ISDIR
	if((!stat(d->database_path, &stat_buf)) ?
	    S_ISDIR(stat_buf.st_mode) : FALSE
	)
#else
	if(FALSE)
#endif
	{
	    d->database_dir = STRDUP(d->database_path);
	    if(d->database_dir == NULL)
	    {
		g_print("Memory allocation error\n");
		g_free(d->database_path);
		g_free(d);
		return(NULL);
	    }

	    memset(&d->database_stat, 0x00, sizeof(struct cl_stat));
	    cl_statinidir(d->database_dir, &d->database_stat);

	    status = cl_loaddbdir(
		d->database_path, &d->database, &d->database_entries
	    );
	}
	else
	{
	    d->database_dir = g_dirname(d->database_path);
	    if(d->database_dir == NULL)
	    {
		g_print("Memory allocation error\n");
		g_free(d->database_path);
		g_free(d);
		return(NULL);
	    }

	    memset(&d->database_stat, 0x00, sizeof(struct cl_stat));
	    cl_statinidir(d->database_dir, &d->database_stat);

	    status = cl_loaddb(
		d->database_path, &d->database, &d->database_entries
	    );
	}
	if(status || (d->database == NULL) || (d->database_entries <= 0))
	{
	    g_print(
"Failed to load the virus database\n"
	    );
	    cl_statfree(&d->database_stat);
	    g_free(d->database_path);
	    g_free(d->database_dir);
	    g_free(d);
	    return(NULL);
	}

	/* Build the virus database */
	if(cl_build(d->database))
	{
	    g_print(
"Failed to build the virus database\n"
	    );
	    cl_statfree(&d->database_stat);
	    g_free(d->database_path);
	    g_free(d->database_dir);
	    g_free(d);
	    return(NULL);
	}

	return(d);
}

/*
 *	Shuts down the Clam AntiVirus.
 */
static void AVScanOPClamAVShutdown(ClamAVData *d)
{
	if(d == NULL)
	    return;

	/* Virus database */
	if(d->database != NULL)
	    cl_free(d->database);

	/* Virus database stat */
	cl_statfree(&d->database_stat);

	g_free(d->database_path);
	g_free(d->database_dir);

	g_free(d);
}

/*
 *	Use the Clam AntiVirus to scan the specified file.
 */
static gint AVScanOPClamAVScanFile(
	ClamAVData *d, const gchar *path,
	gint *stop_count
)
{
	gint status, fd;
	const gchar *virus_name = NULL;
	guint options;
	gulong size_mb = 0l;
	struct cl_limits limits;

	/* Interrupted? */
	if(*stop_count > 0)
	    return(4);

	PRINT_SCANNING_FILE(d->files_scanned, d->total_files, path);

	/* Open the file for reading */
	fd = open(path, O_RDONLY);
	if(fd < 0)
	{
	    const gint error_code = errno;
	    gchar *error_msg = STRDUP(g_strerror(error_code));
	    if(error_msg != NULL)
	    {
		gchar *s;

		*error_msg = (gchar)toupper((int)*error_msg);
		s = g_strdup_printf(
		    "%s",
		    error_msg
		);
		PRINT_ERR(path, s);
		g_free(s);
		g_free(error_msg);
	    }
	    else
	    {
		PRINT_ERR(
		    path,
		    "Unable to open the file for reading"
		);
	    }
	    return(1);
	}

	/* Interrupted? */
	if(*stop_count > 0)
	{
	    close(fd);
	    return(4);
	}


	/* Set up archive limits */
	memset(&limits, 0x00, sizeof(struct cl_limits));
	limits.maxfiles = 50000;	/* Maximum files to scan within an archive */
	limits.maxfilesize = 10 * 1048576;	/* Maximal archived file size == 10 Mb */
	limits.maxreclevel = 8;		/* Maximal recursion level */

	/* Set up scan options */
#if defined(CL_SCAN_STDOPT)
	options = CL_SCAN_STDOPT;
#elif defined(CL_SCAN_RAW) && defined(CL_SCAN_ARCHIVE) && defined(CL_SCAN_MAIL) && defined(CL_SCAN_HTML)
	options = CL_SCAN_RAW | CL_SCAN_ARCHIVE | CL_SCAN_MAIL |
		  CL_SCAN_HTML;
#elif defined(CL_ARCHIVE) && defined(CL_MAIL)
	options = CL_ARCHIVE | CL_MAIL;
#elif defined(CL_SCAN_RAW)
	options = CL_SCAN_RAW;
#else
#warning No supported CL_SCAN_* options are #defined, options will always be 0.
 	options = 0;
#endif

	/* Scan this file */
	status = cl_scandesc(
	    fd,
	    &virus_name, &size_mb,
	    d->database,
	    &limits,
	    options
	);

	/* Close the file */
	close(fd);

	/* Count this file as scanned and how much scanned */
	d->files_scanned++;
	d->size_scanned_mb += size_mb;

	/* Check the scan results */

	/* Got a virus? */
	if(status == CL_VIRUS)
	{
	    /* Detected a virus */
	    gchar *s = g_strdup_printf(
		"Detected virus \"%s\"",
		virus_name
	    );
	    PRINT_ERR(path, s);
	    g_free(s);

	    d->infected_files++;	/* Count this infected file */

	    return(1);
	}
	/* Clean? */
	else if(status == 0)
	{
	    /* Interrupted? */
	    if(*stop_count > 0)
		return(4);
	    else
		return(0);
	}
	/* Other error */
	else
	{
	    gchar *s, *error_msg = STRDUP(cl_strerror((int)status));
	    if(error_msg != NULL)
	    {
		*error_msg = (gchar)toupper((int)*error_msg);
		s = g_strdup_printf(
		    "%s",
		    error_msg
		);
		PRINT_ERR(path, s);
		g_free(s);
		g_free(error_msg);
	    }
	    return(1);
	}
}

/*
 *	Use Clam AntiVirus to scan the specified directory.
 */
static gint AVScanOPClamAVScanDir(
	ClamAVData *d, const gchar *path,
	const gboolean recursive,
	const gboolean executables_only,
	const gboolean ignore_links,
	gint *stop_count
)
{
	mode_t m;
	gint status, status2;
	const gchar *s;
	gchar *child;
	struct dirent *dent;
	struct stat stat_buf;
	DIR *dp;

	/* Interrupted? */
	if(*stop_count > 0)
	    return(4);

	/* Open the directory */
	dp = opendir(path);
	if(dp == NULL)
	{
	    const gint error_code = errno;
	    gchar *error_msg = STRDUP(g_strerror(error_code));
	    if(error_msg != NULL)
	    {
		gchar *s;
		*error_msg = (gchar)toupper((int)*error_msg);
		s = g_strdup_printf(
		    "%s",
		    error_msg
		);
		PRINT_ERR(path, s);
		g_free(s);
		g_free(error_msg);
	    }
	    else
	    {
		PRINT_ERR(path, "Unable to open the directory");
	    }
	    return(1);
	}

	/* Interrupted? */
	if(*stop_count > 0)
	{
	    closedir(dp);
	    return(4);
	}

	/* Scan directory */
	PRINT_SCANNING_DIR(path);
	status = 0;
	while(*stop_count == 0)
	{
	    dent = readdir(dp);
	    if(dent == NULL)
		break;

	    s = (const gchar *)dent->d_name;

	    /* Check for virus database change */
	    if(cl_statchkdir(&d->database_stat) == 1)
	    {
		/* Reload the virus database */
		struct stat stat_buf;
		gchar *s = AVScanCopyShortenString(d->database_path, 50);
		g_print("Reloading virus database \"%s\"...\n", s);
		g_free(s);
#ifdef S_ISDIR
		if((!stat(d->database_path, &stat_buf)) ?
		    S_ISDIR(stat_buf.st_mode) : FALSE
		)
#else
		if(FALSE)
#endif
		    status2 = cl_loaddbdir(
			d->database_path, &d->database, &d->database_entries
		    );
		else
		    status2 = cl_loaddb(
			d->database_path, &d->database, &d->database_entries
		    );
		if(status2 || (d->database == NULL) ||
		   (d->database_entries <= 0)
		)
		{
		    g_print("Failed to reload the virus database\n");
		    break;
		}
		if(cl_build(d->database))
		{
		    g_print("Failed to rebuild the virus database\n");
		    break;
		}
		/* Reget virus database stats */
		cl_statfree(&d->database_stat);
		cl_statinidir(d->database_dir, &d->database_stat);
	    }

	    /* Skip the current and parent directory notations */
	    if(!strcmp((const char *)s, ".") ||
	       !strcmp((const char *)s, "..")
	    )
		continue;

	    /* Get the full path to the child object */
	    child = STRDUP(PrefixPaths(
		(const char *)path, (const char *)s
	    ));
	    if(child == NULL)
		continue;

	    /* Get this object's local stats */
	    if(lstat((const char *)child, &stat_buf))
	    {
		const gint error_code = (gint)errno;
		gchar *error_msg = STRDUP(g_strerror(error_code));
		if(error_msg != NULL)
		{
		    *error_msg = (gchar)toupper((int)*error_msg);
		    PRINT_ERR(child, error_msg);
		    g_free(error_msg);
		}
		else
		{
		    PRINT_ERR(child, "Unable to obtain local stats");
		}
		g_free(child);
		continue;
	    }

	    m = stat_buf.st_mode;

	    /* Link? */
#ifdef S_ISLNK
	    if(S_ISLNK(m))
#else
	    if(FALSE)
#endif
	    {
		gchar *dest;

		/* Ignore links? */
		if(ignore_links)
		{
		    g_free(child);
		    continue;
		}

		/* Get the destination value and check if it is a
		 * reference to the current or parent directory
		 */
		dest = (gchar *)GetAllocLinkDest(child);
		if(dest != NULL)
		{
		    if(!strcmp((const char *)dest, ".") ||
		       !strcmp((const char *)dest, "..")
		    )
		    {
			/* Skip this to prevent infinite recursion */
			g_free(child);
			g_free(dest);
			continue;
		    }
		    g_free(dest);
		}
	    }

	    /* Get this object's destination stats */
	    if(stat((const char *)child, &stat_buf))
	    {
		const gint error_code = (gint)errno;
		gchar *error_msg = STRDUP(g_strerror(error_code));
		if(error_msg != NULL)
		{
		    *error_msg = (gchar)toupper((int)*error_msg);
		    PRINT_ERR(child, error_msg);
		    g_free(error_msg);
		}
		else
		{
		    PRINT_ERR(child, "Unable to obtain stats");
		}
		g_free(child);
		continue;
	    }

	    m = stat_buf.st_mode;

	    /* Directory and recursive? */
#ifdef S_ISDIR
	    if(S_ISDIR(m) && recursive)
#else
	    if(FALSE)
#endif
	    {
		status2 = AVScanOPClamAVScanDir(
		    d, child,
		    recursive, executables_only, ignore_links,
		    stop_count
		);
		if((status2 != 0) && (status == 0))
		    status = status2;
	    }
	    /* Executables only or all? */
	    else if(executables_only ?
	((m & S_IXUSR) || (m & S_IXGRP) || (m & S_IXOTH)) : TRUE
	    )
	    {
		status2 = AVScanOPClamAVScanFile(d, child, stop_count);
		if((status2 != 0) && (status == 0))
		    status = status2;
	    }

	    g_free(child);
	}

	/* Close the directory */
	closedir(dp);

	/* Need to set status to interrupted? */
	if((status == 0) && (*stop_count > 0))
	    status = 4;

	return(status);
}
#endif	/* HAVE_CLAMAV */


/*
 *	Calculates the total files to be scanned in the directory
 *	specified by path.
 *
 *	If recursive is TRUE then files to be scanned in subdirectories
 *	of the specified directory will be counted.
 *
 *	If executables_only is TRUE then only files that are set
 *	executable will be counted.
 */
static gulong AVScanOPCalculateTotalFilesInDirIterate(
	const gchar *path,
	const gboolean recursive,
	const gboolean executables_only,
	const gboolean ignore_links,
	gint *stop_count
)
{
	mode_t m;
	gulong subtotal = 0l;
	gchar *child;
	const gchar *s;
	struct dirent *dent;
	struct stat stat_buf;
	DIR *dp = opendir(path);
	if(dp == NULL)
	    return(subtotal);

	while(*stop_count == 0)
	{
	    dent = readdir(dp);
	    if(dent == NULL)
		break;

	    s = (const gchar *)dent->d_name;

	    /* Skip the current and parent directory notations */
	    if(!strcmp((const char *)s, ".") ||
	       !strcmp((const char *)s, "..")
	    )
		continue;

	    /* Get the full path to the child object */
	    child = STRDUP(PrefixPaths(
		(const char *)path, (const char *)s   
	    ));
	    if(child == NULL)
		continue;

	    /* Get the object's local stats */
	    if(lstat((const char *)child, &stat_buf))
	    {
		/* Unable to get the local stats, do not count this */
		g_free(child);
		continue;
	    }

	    m = stat_buf.st_mode;

#ifdef S_ISLNK
	    /* Link? */
	    if(S_ISLNK(m))
#else
	    if(FALSE)
#endif
	    {
		gchar *dest;

		/* Ignore links? */
		if(ignore_links)
		{
		    /* Do not count this */
		    g_free(child);
		    continue;
		}

		/* Get the destination value and check if it is a
		 * reference to the current or parent directory
		 */
		dest = (gchar *)GetAllocLinkDest((const char *)child);
		if(dest != NULL)
		{
		    if(!strcmp((const char *)dest, ".") ||
		       !strcmp((const char *)dest, "..")
		    )
		    {
			/* Skip this to prevent infinite recursion */
			g_free(child);
			g_free(dest);
			continue;
		    }
		    g_free(dest);
		}
	    }

	    /* Get this object's destination stats */
	    if(stat((const char *)child, &stat_buf))
	    {
		/* Unable to get the destination stats, do not count this */
		g_free(child);
		continue;
	    }

	    m = stat_buf.st_mode;

	    /* Is the destination a directory and should we recurse
	     * into it?
	     */
#ifdef S_ISDIR
	    if(S_ISDIR(m) && recursive)
#else
	    if(FALSE)
#endif
	    {
		subtotal += AVScanOPCalculateTotalFilesInDirIterate(
		    child,
		    recursive, executables_only, ignore_links,
		    stop_count
		);
	    }
	    /* Executable only or all? */
	    else if(executables_only ?
	((m & S_IXUSR) || (m & S_IXGRP) || (m & S_IXOTH)) : TRUE
	    )
	    {
		subtotal++;
	    }

	    g_free(child);
	} 

	closedir(dp);

	return(subtotal);
}

/*
 *	Calculates the total number of files to be scanned from the
 *	specified list of paths.
 *
 *	If a file is not accessable to be scanned in the specified
 *	path then it will be excluded from the returned total.
 */
static gulong AVScanOPCalculateTotalFiles(    
	GList *paths_list,
	const gboolean recursive,
	const gboolean executables_only,
	const gboolean ignore_links,
	gint *stop_count
)
{
	gulong total = 0l;
	mode_t m;
	struct stat stat_buf;
	const gchar *path;
	GList *glist;

	if(paths_list == NULL)
	    return(total);

	/* Iterate through each object in the paths list */
	for(glist = paths_list; glist != NULL; glist = g_list_next(glist))
	{
	    /* Interrupted */
	    if(*stop_count > 0)
		break;

	    path = (const gchar *)glist->data;
	    if(STRISEMPTY(path))
		continue;

	    /* Get this object's local stats */
	    if(lstat((const char *)path, &stat_buf))
	    {
		/* Unable to get local stats, do not count this */
		continue;
	    }

	    m = stat_buf.st_mode;

	    /* Link? */
#ifdef S_ISLNK
	    if(S_ISLNK(m))
#else
	    if(FALSE)
#endif
	    {
		/* Links ignored? */
		if(ignore_links)
		{
		    /* Do not count this */
		    continue;
		}

		/* Get this link's destination stats */
		if(stat((const char *)path, &stat_buf))
		{
		    /* Unable to get destination stats, do not count this */
		    continue;
		}

		m = stat_buf.st_mode;

		/* Is this link's destination a directory? */
#ifdef S_ISDIR
		if(S_ISDIR(m))
#else
		if(FALSE)
#endif
		{
		    total += AVScanOPCalculateTotalFilesInDirIterate(
			path,
			recursive, executables_only, ignore_links,
			stop_count
		    );
		}
		else
		{
		    total++;
		}
	    }
	    /* Directory? */
#ifdef S_ISDIR
	    else if(S_ISDIR(m))
#else
	    else if(FALSE)
#endif
	    {
		total += AVScanOPCalculateTotalFilesInDirIterate(
		    path,
		    recursive, executables_only, ignore_links,
		    stop_count
		);
	    }
	    /* File? */
#ifdef S_ISREG
	    else if(S_ISREG(m))
#else
	    else if(TRUE)
#endif
	    {
		total++;
	    }
	}

	return(total);
}


/*
 *	Returns a dynamically allocated string describing the name and
 *	the version of the Antivirus Engine.
 */
gchar *AVScanGetAVEngineNameVersionString(void)
{
#if defined(HAVE_CLAMAV)
	return(g_strdup_printf(
	    "%s Version %s",
	    "Clam AntiVirus",
	    cl_retver()
	));
#else
	return(NULL);
#endif
}


/*
 *	AntiVirus Scan Operation
 *
 *	This is the front end to performing scans.
 *
 *	The stop_count is a pointer to a gint which should be
 *	initialized to 0 prior to this call. This value will be checked
 *	throughout this module's functions and if (*stop_count > 0) then
 *	it will skip all scanning related calls.
 */
gint AVScanOP(
	const gchar *db_path,
	GList *paths_list,
	const gboolean recursive,
	const gboolean executables_only,
	const gboolean ignore_links,
	gint *stop_count		/* Must be preinitialized and not NULL */
)
{
	gint status = 0;
	const gulong time_start = (gulong)time(NULL);
#if defined(HAVE_CLAMAV)
	ClamAVData *d;
#endif

	if(paths_list == NULL)
	{
	    status = 2;
	    return(status);
	}

	/* Interrupted? */
	if(*stop_count > 0)
	{
	    status = 4;
	    return(status);
	}

#if defined(HAVE_CLAMAV)
	/* Initialize ClamAV */
	d = AVScanOPClamAVInit(db_path);
	if(d != NULL)
	{
	    gint status2;
	    const gchar *path;
	    mode_t m;
	    GList *glist;
	    struct stat stat_buf;

	    /* Calculate the total files to be scanned */
	    d->total_files = AVScanOPCalculateTotalFiles(
		paths_list,
		recursive, executables_only, ignore_links,
		stop_count
	    );

	    /* Iterate through each object in the paths list */
	    for(glist = paths_list; glist != NULL; glist = g_list_next(glist))
	    {
		/* Interrupted */
		if(*stop_count > 0)
		    break;

		path = (const gchar *)glist->data;
		if(STRISEMPTY(path))
		    continue;

		/* Get this object's local stats */
		if(lstat(path, &stat_buf))
		{
		    /* Unable to get this object's local stats */
		    const gint error_code = (gint)errno;
		    gchar *error_msg = STRDUP(g_strerror(error_code));
		    if(error_msg != NULL)
		    {
			*error_msg = (gchar)toupper((int)*error_msg);
			PRINT_ERR(path, error_msg);
			g_free(error_msg);
		    }
		    continue;
		}

		m = stat_buf.st_mode;

		/* Link? */
#ifdef S_ISLNK
		if(S_ISLNK(m))
#else
		if(FALSE)
#endif
		{
		    if(ignore_links)
		    {
			/* Print a warning about this because a link
			 * was explicitly specified and ignore_links
			 * was also specified
			 */
			PRINT_ERR(path, "Link ignored");
			continue;
		    }

		    /* Get this link's destination stats */
		    if(stat(path, &stat_buf))
		    {
			/* Unable to get this link's destination stats */
			const gint error_code = (gint)errno;
			gchar *error_msg = STRDUP(g_strerror(error_code));
			if(error_msg != NULL)
			{
			    *error_msg = (gchar)toupper((int)*error_msg);
			    PRINT_ERR(path, error_msg);
			    g_free(error_msg);
			}
			continue;
		    }

		    m = stat_buf.st_mode;

		    /* Is this link's destination a directory? */
#ifdef S_ISDIR
		    if(S_ISDIR(m))
#else
		    if(FALSE)
#endif
		    {
			status2 = AVScanOPClamAVScanDir(
			    d, path,
			    recursive, executables_only, ignore_links,
			    stop_count
			);
		    }
		    else
		    {
			status2 = AVScanOPClamAVScanFile(
			    d, path,
			    stop_count
			);
		    }
		}
		/* Directory? */
#ifdef S_ISDIR
		else if(S_ISDIR(m))
#else
		else if(FALSE)
#endif
		    status2 = AVScanOPClamAVScanDir(
			d, path,
			recursive, executables_only, ignore_links,
			stop_count
		    );
		/* File? */
#ifdef S_ISREG
		else if(S_ISREG(m))
#else
		else if(TRUE)
#endif
		{
		    status2 = AVScanOPClamAVScanFile(
			d, path,
			stop_count
		    );
		}
		else
		{
		    PRINT_ERR(path, "Unsupported object type");
		    status2 = 1;	/* Unsupported object type or nothing to scan */
		}

		if((status2 != 0) && (status == 0))
		    status = status2;
	    }

	    /* Not interrupted? */
	    if(*stop_count == 0)
	    {
		/* Print the final report */
		PRINT_REPORT(
		    time_start,
		    (gulong)time(NULL),
		    d->infected_files,
		    d->size_scanned_mb,
		    d->files_scanned
		);
	    }

	    AVScanOPClamAVShutdown(d);
	}
	else
	{
	    status = 3;
	}
#else
	PRINT_ERR(
	    NULL,
	    "No AntiVirus support enabled at compile time"
	);
	status = 1;
#endif

	/* Need to set status to interrupted? */
	if((status == 0) && (*stop_count > 0))
	    status = 4;

	return(status);
}
