/*
 * Glue code for libmspack handling.
 * Author: 웃 Sebastian Andrzej Siewior
 * ✉ sebastian @ breakpoint ̣cc
 */

#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <fcntl.h>

#include <mspack.h>

#include "clamav.h"
#include "fmap.h"
#include "scanners.h"
#include "others.h"

enum mspack_type {
	FILETYPE_DUNNO,
	FILETYPE_FMAP,
	FILETYPE_FILENAME,
};

struct mspack_name {
	fmap_t *fmap;
	off_t org;
};

struct mspack_system_ex {
	struct mspack_system ops;
	off_t max_size;
};

struct mspack_handle {
	enum mspack_type type;

	fmap_t *fmap;
	off_t org;
	off_t offset;

	FILE *f;
	off_t max_size;
};

#define container_of(ptr, type, member) ({			\
	const typeof( ((type *)0)->member ) *__mptr = (ptr);	\
	(type *)( (char *)__mptr - offsetof(type,member) );})

#define min_t(type, x, y) ({		\
	type __min1 = (x);		\
	type __min2 = (y);		\
	__min1 < __min2 ? __min1: __min2; })

static struct mspack_file *mspack_fmap_open(struct mspack_system *self,
		const char *filename, int mode)
{
	struct mspack_name *mspack_name;
	struct mspack_handle *mspack_handle;
	struct mspack_system_ex *self_ex;
	const char *fmode;

	if (!filename) {
		cli_dbgmsg("%s() failed at %d\n", __func__, __LINE__);
		return NULL;
	}
	mspack_handle = malloc(sizeof(*mspack_handle));
	if (!mspack_handle) {
		cli_dbgmsg("%s() failed at %d\n", __func__, __LINE__);
		return NULL;
	}
	switch (mode) {
	case MSPACK_SYS_OPEN_READ:
		mspack_handle->type = FILETYPE_FMAP;

		mspack_name = (struct mspack_name *)filename;
		mspack_handle->fmap = mspack_name->fmap;
		mspack_handle->org = mspack_name->org;
		mspack_handle->offset = 0;

		return (struct mspack_file *)mspack_handle;

	case MSPACK_SYS_OPEN_WRITE:
		fmode = "wb";
		break;
	case MSPACK_SYS_OPEN_UPDATE:
		fmode = "r+b";
		break;
	case MSPACK_SYS_OPEN_APPEND:
		fmode = "ab";
		break;
	default:
		cli_dbgmsg("%s() wrong mode\n", __func__);
		goto out_err;
	}

	mspack_handle->type = FILETYPE_FILENAME;

	mspack_handle->f = fopen(filename, fmode);
	if (!mspack_handle->f) {
		cli_dbgmsg("%s() failed %d\n", __func__, __LINE__);
		goto out_err;
	}
	self_ex = container_of(self, struct mspack_system_ex, ops);
	mspack_handle->max_size = self_ex->max_size;
	return (struct mspack_file *)mspack_handle;

out_err:
	free(mspack_handle);
	return NULL;
}

static void mspack_fmap_close(struct mspack_file *file)
{
	struct mspack_handle *mspack_handle = (struct mspack_handle *)file;

	if (!mspack_handle)
		return;

	if (mspack_handle->type == FILETYPE_FILENAME)
		fclose(mspack_handle->f);
	free(mspack_handle);
}

static int mspack_fmap_read(struct mspack_file *file, void *buffer, int bytes)
{
	struct mspack_handle *mspack_handle = (struct mspack_handle *)file;
	off_t offset;
	size_t count;
	int ret;

	if (bytes < 0) {
		cli_dbgmsg("%s() %d\n", __func__, __LINE__);
		return -1;
	}
	if (!mspack_handle) {
		cli_dbgmsg("%s() %d\n", __func__, __LINE__);
		return -1;
	}

	if (mspack_handle->type == FILETYPE_FMAP) {
		offset = mspack_handle->offset + mspack_handle->org;

		ret = fmap_readn(mspack_handle->fmap, buffer, offset, bytes);
		if (ret != bytes) {
			cli_dbgmsg("%s() %d %d, %d\n", __func__, __LINE__, bytes, ret);
			return ret;
		}

		mspack_handle->offset += bytes;
		return bytes;
	}
	count = fread(buffer, bytes, 1, mspack_handle->f);
	if (count < 1) {
		cli_dbgmsg("%s() %d %d, %zd\n", __func__, __LINE__, bytes, count);
		return -1;
	}
	return bytes;
}

static int mspack_fmap_write(struct mspack_file *file, void *buffer, int bytes)
{
	struct mspack_handle *mspack_handle = (struct mspack_handle *)file;
	size_t count;
	off_t max_size;

	if (bytes < 0 || !mspack_handle) {
		cli_dbgmsg("%s() err %d\n", __func__, __LINE__);
		return -1;
	}

	if (mspack_handle->type == FILETYPE_FMAP) {
		cli_dbgmsg("%s() err %d\n", __func__, __LINE__);
		return -1;
	}

	if (!bytes)
		return 0;

	max_size = mspack_handle->max_size;
	if (!max_size)
		return bytes;

	max_size = min_t(off_t, max_size, bytes);
	mspack_handle->max_size -= max_size;

	count = fwrite(buffer, max_size, 1, mspack_handle->f);
	if (count < 1) {
		cli_dbgmsg("%s() err %m <%zd %d>\n", __func__, count, bytes);
		return -1;
	}

	return bytes;
}

static int mspack_fmap_seek(struct mspack_file *file, off_t offset, int mode)
{
	struct mspack_handle *mspack_handle = (struct mspack_handle *)file;

	if (!mspack_handle) {
		cli_dbgmsg("%s() err %d\n", __func__, __LINE__);
		return -1;
	}

	if (mspack_handle->type == FILETYPE_FMAP) {
		off_t new_pos;

		switch (mode) {
		case MSPACK_SYS_SEEK_START:
			new_pos = offset;
			break;
		case MSPACK_SYS_SEEK_CUR:
			new_pos = mspack_handle->offset + offset;
			break;
		case MSPACK_SYS_SEEK_END:
			new_pos = mspack_handle->fmap->len + offset;
			break;
		default:
			cli_dbgmsg("%s() err %d\n", __func__, __LINE__);
			return -1;
		}
		if (new_pos < 0 || new_pos > mspack_handle->fmap->len) {
			cli_dbgmsg("%s() err %d\n", __func__, __LINE__);
			return -1;
		}

		mspack_handle->offset = new_pos;
		return 0;
	}

	switch (mode) {
	case MSPACK_SYS_SEEK_START:
		mode = SEEK_SET;
		break;
	case MSPACK_SYS_SEEK_CUR:
		mode = SEEK_CUR;
		break;
	case MSPACK_SYS_SEEK_END:
		mode = SEEK_END;
		break;
	default:
		cli_dbgmsg("%s() err %d\n", __func__, __LINE__);
		return -1;
	}

	return fseeko(mspack_handle->f, offset, mode);
}

static off_t mspack_fmap_tell(struct mspack_file *file)
{
	struct mspack_handle *mspack_handle = (struct mspack_handle *)file;

	if (!mspack_handle)
		return -1;

	if (mspack_handle->type == FILETYPE_FMAP)
		return mspack_handle->offset;

	return ftello(mspack_handle->f);
}

static void mspack_fmap_message(struct mspack_file *file, const char *fmt, ...)
{
	cli_dbgmsg("%s() %s\n", __func__, fmt);
}
static void *mspack_fmap_alloc(struct mspack_system *self, size_t num)
{
	return malloc(num);
}

static void mspack_fmap_free(void *mem)
{
	free(mem);
}

static void mspack_fmap_copy(void *src, void *dst, size_t num)
{
	memcpy(dst, src, num);
}

static struct mspack_system mspack_sys_fmap_ops = {
	.open = mspack_fmap_open,
	.close = mspack_fmap_close,
	.read = mspack_fmap_read,
	.write = mspack_fmap_write,
	.seek = mspack_fmap_seek,
	.tell = mspack_fmap_tell,
	.message = mspack_fmap_message,
	.alloc = mspack_fmap_alloc,
	.free = mspack_fmap_free,
	.copy = mspack_fmap_copy,
};

static int cli_scanfile(const char *filename, cli_ctx *ctx)
{
	int fd, ret;

	/* internal version of cl_scanfile with arec/mrec preserved */
	fd = safe_open(filename, O_RDONLY|O_BINARY);
	if (fd < 0)
		return CL_EOPEN;

	ret = cli_magic_scandesc(fd, ctx);

	close(fd);
	return ret;
}

int cli_scanmscab(cli_ctx *ctx, off_t sfx_offset)
{
	struct mscab_decompressor *cab_d;
	struct mscabd_cabinet *cab_h;
	struct mscabd_file *cab_f;
	int ret;
	int files;
	int virus_num = 0;
	struct mspack_name mspack_fmap = {
		.fmap	= *ctx->fmap,
		.org	= sfx_offset,
	};
	struct mspack_system_ex ops_ex = {
		.ops	= mspack_sys_fmap_ops,
	};

	MSPACK_SYS_SELFTEST(ret);
	if (ret) {
		cli_dbgmsg("%s() failed at %d\n", __func__, __LINE__);
		return CL_EUNPACK;
	}

	cab_d = mspack_create_cab_decompressor(&ops_ex.ops);
	if (!cab_d) {
		cli_dbgmsg("%s() failed at %d\n", __func__, __LINE__);
		return CL_EUNPACK;
	}

	cab_h = cab_d->open(cab_d, (char *)&mspack_fmap);
	if (!cab_h) {
		ret = CL_EFORMAT;
		cli_dbgmsg("%s() failed at %d\n", __func__, __LINE__);
		goto out_dest;
	}
	files = 0;
	for (cab_f = cab_h->files; cab_f; cab_f = cab_f->next) {
		off_t max_size;
		char *tmp_fname;

		ret = cli_matchmeta(ctx, cab_f->filename, 0, cab_f->length, 0,
				files, 0, NULL);
		if (ret) {
			if (ret == CL_VIRUS) {
				virus_num++;
				if (!SCAN_ALL)
					break;
			}
			goto out_close;
		}

		if (ctx->engine->maxscansize) {
			if (ctx->scansize >= ctx->engine->maxscansize) {
				ret = CL_CLEAN;
				break;
			}
		}

		if (ctx->engine->maxscansize &&
				ctx->scansize + ctx->engine->maxfilesize >=
				ctx->engine->maxscansize)
			max_size = ctx->engine->maxscansize -
				ctx->scansize;
		else
			max_size = ctx->engine->maxfilesize ?
				ctx->engine->maxfilesize :
				0xffffffff;

		tmp_fname = cli_gentemp(ctx->engine->tmpdir);
		if (!tmp_fname) {
			ret = CL_EMEM;
			break;
		}

		ops_ex.max_size = max_size;
		/* scan */
		ret = cab_d->extract(cab_d, cab_f, tmp_fname);
		if (ret)
			/* Failed to extract. Try to scan what is there */
			cli_dbgmsg("%s() failed to extract %d\n", __func__, ret);

		ret = cli_scanfile(tmp_fname, ctx);
		if (ret == CL_VIRUS)
			virus_num++;

		if (!ctx->engine->keeptmp) {
			if (!access(tmp_fname, R_OK) && cli_unlink(tmp_fname)) {
				free(tmp_fname);
				ret = CL_EUNLINK;
				break;
			}
		}
		free(tmp_fname);
		files++;
		if (ret == CL_VIRUS && SCAN_ALL)
			continue;
		if (ret)
			break;
	}

out_close:
	cab_d->close(cab_d, cab_h);
out_dest:
	mspack_destroy_cab_decompressor(cab_d);
	if (virus_num)
		return CL_VIRUS;
	return ret;
}

int cli_scanmschm(cli_ctx *ctx)
{
	struct mschm_decompressor *mschm_d;
	struct mschmd_header *mschm_h;
	struct mschmd_file *mschm_f;
	int ret;
	int files;
	int virus_num = 0;
	struct mspack_name mspack_fmap = {
		.fmap = *ctx->fmap,
	};
	struct mspack_system_ex ops_ex = {
		.ops	= mspack_sys_fmap_ops,
	};

	MSPACK_SYS_SELFTEST(ret);
	if (ret) {
		cli_dbgmsg("%s() failed at %d\n", __func__, __LINE__);
		return CL_EUNPACK;
	}

	mschm_d = mspack_create_chm_decompressor(&ops_ex.ops);
	if (!mschm_d) {
		cli_dbgmsg("%s() failed at %d\n", __func__, __LINE__);
		return CL_EUNPACK;
	}

	mschm_h = mschm_d->open(mschm_d, (char *)&mspack_fmap);
	if (!mschm_h) {
		ret = CL_EFORMAT;
		cli_dbgmsg("%s() failed at %d\n", __func__, __LINE__);
		goto out_dest;
	}
	files = 0;
	for (mschm_f = mschm_h->files; mschm_f;	mschm_f = mschm_f->next) {
		off_t max_size;
		char *tmp_fname;

		ret = cli_matchmeta(ctx, mschm_f->filename, 0, mschm_f->length,
				0, files, 0, NULL);
		if (ret) {
			if (ret == CL_VIRUS) {
				virus_num++;
				if (!SCAN_ALL)
					break;
			}
			goto out_close;
		}

		if (ctx->engine->maxscansize) {
			if (ctx->scansize >= ctx->engine->maxscansize) {
				ret = CL_CLEAN;
				break;
			}
		}

		if (ctx->engine->maxscansize &&
				ctx->scansize + ctx->engine->maxfilesize >=
				ctx->engine->maxscansize)
			max_size = ctx->engine->maxscansize -
				ctx->scansize;
		else
			max_size = ctx->engine->maxfilesize ?
				ctx->engine->maxfilesize :
				0xffffffff;

		ops_ex.max_size = max_size;

		tmp_fname = cli_gentemp(ctx->engine->tmpdir);
		if (!tmp_fname) {
			ret = CL_EMEM;
			break;
		}

		/* scan */
		ret = mschm_d->extract(mschm_d, mschm_f, tmp_fname);
		if (ret)
			/* Failed to extract. Try to scan what is there */
			cli_dbgmsg("%s() failed to extract %d\n", __func__, ret);

		ret = cli_scanfile(tmp_fname, ctx);
		if (ret == CL_VIRUS)
			virus_num++;

		if (!ctx->engine->keeptmp) {
			if (!access(tmp_fname, R_OK) && cli_unlink(tmp_fname)) {
				free(tmp_fname);
				ret = CL_EUNLINK;
				break;
			}
		}
		free(tmp_fname);
		files++;
		if (ret == CL_VIRUS && SCAN_ALL)
			continue;
		if (ret)
			break;
	}

out_close:
	mschm_d->close(mschm_d, mschm_h);
out_dest:
	mspack_destroy_chm_decompressor(mschm_d);
	if (virus_num)
		return CL_VIRUS;
	return ret;

	return 0;
}
