/*
 * Author: 웃 Sebastian Andrzej Siewior
 * Summary: Glue code for libmspack handling.
 *
 * Acknowledgements: ClamAV uses Stuart Caie's libmspack to parse as number of
 *                   Microsoft file formats.
 * ✉ sebastian @ breakpoint ̣cc
 */

#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdarg.h>
#include <stdio.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;
    uint64_t max_size;
};

struct mspack_handle {
    enum mspack_type type;

    fmap_t *fmap;
    off_t org;
    off_t offset;

    FILE *f;
    uint64_t max_size;
};

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;
    const struct mspack_system *mptr = self;

    if (!filename) {
        cli_dbgmsg("%s() failed at %d\n", __func__, __LINE__);
        return NULL;
    }
    mspack_handle = malloc(sizeof(*mspack_handle));
    memset(mspack_handle, 0, (sizeof(*mspack_handle)));
    if (!mspack_handle) {
        cli_dbgmsg("%s() failed at %d\n", __func__, __LINE__);
        return NULL;
    }
    memset(mspack_handle, 0, sizeof(*mspack_handle));

    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                 = (struct mspack_system_ex *)((char *)mptr - offsetof(struct mspack_system_ex, ops));
    mspack_handle->max_size = self_ex->max_size;
    return (struct mspack_file *)mspack_handle;

out_err:
    memset(mspack_handle, 0, (sizeof(*mspack_handle)));
    free(mspack_handle);
    mspack_handle = NULL;
    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)
        if (mspack_handle->f)
            fclose(mspack_handle->f);

    memset(mspack_handle, 0, (sizeof(*mspack_handle)));
    free(mspack_handle);
    mspack_handle = NULL;
    return;
}

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) {
        /* Use fmap */
        offset = mspack_handle->offset + mspack_handle->org;

        count = fmap_readn(mspack_handle->fmap, buffer, (size_t)offset, (size_t)bytes);
        if (count == (size_t)-1) {
            cli_dbgmsg("%s() %d requested %d bytes, read failed (-1)\n", __func__, __LINE__, bytes);
            return -1;
        } else if ((int)count < bytes) {
            cli_dbgmsg("%s() %d requested %d bytes, read %zu bytes\n", __func__, __LINE__, bytes, count);
        }

        mspack_handle->offset += (off_t)count;

        return (int)count;
    } else {
        /* Use file descriptor */
        count = fread(buffer, bytes, 1, mspack_handle->f);
        if (count < 1) {
            cli_dbgmsg("%s() %d requested %d bytes, read failed (%zu)\n", __func__, __LINE__, bytes, count);
            return -1;
        }

        ret = (int)count;

        return ret;
    }
}

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;
    uint64_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 = max_size < (uint64_t)bytes ? max_size : (uint64_t)bytes;

    mspack_handle->max_size -= max_size;

    count = fwrite(buffer, max_size, 1, mspack_handle->f);
    if (count < 1) {
        cli_dbgmsg("%s() err %d <%zu %d>\n", __func__, __LINE__, 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 > (off_t)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 fseek(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 (off_t)ftell(mspack_handle->f);
}

static void mspack_fmap_message(struct mspack_file *file, const char *fmt, ...)
{
    UNUSEDPARAM(file);

    if (UNLIKELY(cli_debug_flag)) {
        va_list args;
        char buff[BUFSIZ];
        size_t len = sizeof("LibClamAV debug: ") - 1;

        memset(buff, 0, BUFSIZ);

        /* Add the prefix */
        strncpy(buff, "LibClamAV debug: ", len);

        va_start(args, fmt);
        vsnprintf(buff + len, sizeof(buff) - len - 2, fmt, args);
        va_end(args);

        /* Add a newline and a null terminator */
        buff[strlen(buff)]     = '\n';
        buff[strlen(buff) + 1] = '\0';

        fputs(buff, stderr);
    }
}

static void *mspack_fmap_alloc(struct mspack_system *self, size_t num)
{
    UNUSEDPARAM(self);
    void *addr = malloc(num);
    if (addr) {
        memset(addr, 0, num);
    }
    return addr;
}

static void mspack_fmap_free(void *mem)
{
    if (mem) {
        free(mem);
        mem = NULL;
    }
    return;
}

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,
};

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 = 0;
    int files;
    int virus_num                  = 0;
    struct mspack_name mspack_fmap = {
        .fmap = ctx->fmap,
        .org  = sfx_offset,
    };
    struct mspack_system_ex ops_ex;
    memset(&ops_ex, 0, sizeof(struct mspack_system_ex));
    ops_ex.ops = mspack_sys_fmap_ops;

    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_d->set_param(cab_d, MSCABD_PARAM_FIXMSZIP, 1);
#if MSCABD_PARAM_SALVAGE
    cab_d->set_param(cab_d, MSCABD_PARAM_SALVAGE, 1);
#endif

    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) {
        uint64_t max_size;
        char *tmp_fname = NULL;

        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_ALLMATCHES)
                    break;
            }
            goto out_close;
        }

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

        if (ctx->engine->maxfilesize > 0) {
            // max filesize has been set
            if ((ctx->engine->maxscansize > 0) &&
                (ctx->scansize + ctx->engine->maxfilesize >= ctx->engine->maxscansize)) {
                // ... but would exceed max scansize, shrink it.
                max_size = ctx->engine->maxscansize - ctx->scansize;
            } else {
                // ... and will work
                max_size = ctx->engine->maxfilesize;
            }
        } else {
            // max filesize not specified
            if ((ctx->engine->maxscansize > 0) &&
                (ctx->scansize + UINT32_MAX >= ctx->engine->maxscansize)) {
                // ... but UINT32_MAX would exceed max scansize, shrink it.
                max_size = ctx->engine->maxscansize - ctx->scansize;
            } else {
                // ... use UINT32_MAX
                max_size = UINT32_MAX;
            }
        }

        tmp_fname = cli_gentemp(ctx->sub_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_magic_scan_file(tmp_fname, ctx, cab_f->filename);
        if (CL_EOPEN == ret) {
            ret = CL_CLEAN;
        } else if (CL_VIRUS == ret) {
            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_ALLMATCHES)
            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 = CL_CLEAN; // Default CLEAN in case CHM contains no files.
    int files;
    int virus_num                  = 0;
    struct mspack_name mspack_fmap = {
        .fmap = ctx->fmap,
    };
    struct mspack_system_ex ops_ex;
    memset(&ops_ex, 0, sizeof(struct mspack_system_ex));
    ops_ex.ops = mspack_sys_fmap_ops;

    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) {
        uint64_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_ALLMATCHES)
                    break;
            }
            goto out_close;
        }

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

        if (ctx->engine->maxfilesize > 0) {
            // max filesize has been set
            if ((ctx->engine->maxscansize > 0) &&
                (ctx->scansize + ctx->engine->maxfilesize >= ctx->engine->maxscansize)) {
                // ... but would exceed max scansize, shrink it.
                max_size = ctx->engine->maxscansize - ctx->scansize;
            } else {
                // ... and will work
                max_size = ctx->engine->maxfilesize;
            }
        } else {
            // max filesize not specified
            if ((ctx->engine->maxscansize > 0) &&
                (ctx->scansize + UINT32_MAX >= ctx->engine->maxscansize)) {
                // ... but UINT32_MAX would exceed max scansize, shrink it.
                max_size = ctx->engine->maxscansize - ctx->scansize;
            } else {
                // ... use UINT32_MAX
                max_size = UINT32_MAX;
            }
        }

        ops_ex.max_size = max_size;

        tmp_fname = cli_gentemp(ctx->sub_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_magic_scan_file(tmp_fname, ctx, mschm_f->filename);
        if (CL_EOPEN == ret) {
            ret = CL_CLEAN;
        } else if (CL_VIRUS == ret) {
            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_ALLMATCHES)
            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;
}
