/*
 *  Copyright (C) 2015-2022 Cisco Systems, Inc. and/or its affiliates. All rights reserved.
 *
 *  Authors: Kevin Lin <kevlin2@cisco.com>
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License version 2 as
 *  published by the Free Software Foundation.
 *
 *  This program 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 General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; 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 "others.h"
#include "tiff.h"

#define tiff32_to_host(be, x) (be ? be32_to_host(x) : le32_to_host(x))
#define tiff16_to_host(be, x) (be ? be16_to_host(x) : le16_to_host(x))

struct tiff_ifd {
    uint16_t tag;
    uint16_t type;
    uint32_t numval;
    uint32_t value;
};

cl_error_t cli_parsetiff(cli_ctx *ctx)
{
    cl_error_t status = CL_ERROR;

    fmap_t *map = NULL;
    unsigned char magic[4];
    int big_endian;
    uint32_t offset = 0, ifd_count = 0;
    uint16_t i, num_entries;
    struct tiff_ifd entry;
    size_t value_size;
    uint32_t last_offset = 0;

    cli_dbgmsg("in cli_parsetiff()\n");

    if (NULL == ctx) {
        cli_dbgmsg("TIFF: passed context was NULL\n");
        status = CL_EARG;
        goto done;
    }
    map = ctx->fmap;

    /* check the magic */
    if (fmap_readn(map, magic, offset, 4) != 4) {
        status = CL_CLEAN;
        goto done;
    }
    offset += 4;

    if (!memcmp(magic, "\x4d\x4d\x00\x2a", 4))
        big_endian = 1;
    else if (!memcmp(magic, "\x49\x49\x2a\x00", 4))
        big_endian = 0;
    else {
        status = CL_CLEAN; /* Not a TIFF file */
        goto done;
    }

    cli_dbgmsg("cli_parsetiff: %s-endian tiff file\n", big_endian ? "big" : "little");

    /* acquire offset of first IFD */
    if (fmap_readn(map, &offset, offset, 4) != 4) {
        cli_dbgmsg("cli_parsetiff: Failed to acquire offset of first IFD, file appears to be truncated.\n");
        cli_append_possibly_unwanted(ctx, "Heuristics.Broken.Media.TIFF.EOFReadingFirstIFDOffset");
        status = CL_EPARSE;
        goto done;
    }
    /* offset of the first IFD */
    offset = tiff32_to_host(big_endian, offset);

    cli_dbgmsg("cli_parsetiff: first IFD located @ offset %u\n", offset);

    if (!offset) {
        cli_errmsg("cli_parsetiff: Invalid offset for first IFD\n");
        cli_append_possibly_unwanted(ctx, "Heuristics.Broken.Media.TIFF.InvalidIFDOffset");
        status = CL_EPARSE;
        goto done;
    }

    /* each IFD represents a subfile, though only the first one normally matters */
    do {
        /* acquire number of directory entries in current IFD */
        if (fmap_readn(map, &num_entries, offset, 2) != 2) {
            cli_dbgmsg("cli_parsetiff: Failed to acquire number of directory entries in current IFD, file appears to be truncated.\n");
            cli_append_possibly_unwanted(ctx, "Heuristics.Broken.Media.TIFF.EOFReadingNumIFDDirectoryEntries");
            status = CL_EPARSE;
            goto done;
        }
        offset += 2;
        num_entries = tiff16_to_host(big_endian, num_entries);

        cli_dbgmsg("cli_parsetiff: IFD %u declared %u directory entries\n", ifd_count, num_entries);

        /* transverse IFD entries */
        for (i = 0; i < num_entries; i++) {
            if (fmap_readn(map, &entry, offset, sizeof(entry)) != sizeof(entry)) {
                cli_dbgmsg("cli_parsetiff: Failed to read next IFD entry, file appears to be truncated.\n");
                cli_append_possibly_unwanted(ctx, "Heuristics.Broken.Media.TIFF.EOFReadingIFDEntry");
                status = CL_EPARSE;
                goto done;
            }
            offset += sizeof(entry);

            entry.tag    = tiff16_to_host(big_endian, entry.tag);
            entry.type   = tiff16_to_host(big_endian, entry.type);
            entry.numval = tiff32_to_host(big_endian, entry.numval);
            entry.value  = tiff32_to_host(big_endian, entry.value);

            //cli_dbgmsg("%02u: %u %u %u %u\n", i, entry.tag, entry.type, entry.numval, entry.value);

            value_size = entry.numval;
            switch (entry.type) {
                case 1: /* BYTE */
                    value_size *= 1;
                    break;
                case 2: /* ASCII */
                    value_size *= 1;
                    break;
                case 3: /* SHORT */
                    value_size *= 2;
                    break;
                case 4: /* LONG */
                    value_size *= 4;
                    break;
                case 5: /* RATIONAL (LONG/LONG) */
                    value_size *= 8;
                    break;

                    /* TIFF 6.0 Types */
                case 6: /* SBYTE */
                    value_size *= 1;
                    break;
                case 7: /* UNDEFINED */
                    value_size *= 1;
                    break;
                case 8: /* SSHORT */
                    value_size *= 2;
                    break;
                case 9: /* SLONG */
                    value_size *= 4;
                    break;
                case 10: /* SRATIONAL (SLONG/SLONG) */
                    value_size *= 8;
                    break;
                case 11: /* FLOAT */
                    value_size *= 4;
                    break;
                case 12: /* DOUBLE */
                    value_size *= 8;
                    break;

                default: /* INVALID or NEW Type */
                    value_size *= 0;
                    break;
            }

            if (value_size > sizeof(entry.value)) {
                if (entry.value + value_size > map->len) {
                    cli_warnmsg("cli_parsetiff: TFD entry field %u exceeds bounds of TIFF file [%llu > %llu]\n",
                                i, (long long unsigned)(entry.value + value_size), (long long unsigned)map->len);
                    status = cli_append_virus(ctx, "Heuristics.Broken.Media.TIFF.OutOfBoundsAccess");
                    goto done;
                }
            }
        }

        ifd_count++;

        last_offset = offset;

        /* acquire next IFD location, gets 0 if last IFD */
        if (fmap_readn(map, &offset, offset, sizeof(offset)) != sizeof(offset)) {
            cli_dbgmsg("cli_parsetiff: Failed to aquire next IFD location, file appears to be truncated.\n");
            cli_append_possibly_unwanted(ctx, "Heuristics.Broken.Media.TIFF.EOFReadingChunkCRC");
            status = CL_EPARSE;
            goto done;
        }
        offset = tiff32_to_host(big_endian, offset);

        if (offset) {
            /*If the offsets are not in order, that is suspicious.*/
            if (last_offset >= offset) {
                cli_dbgmsg("cli_parsetiff: Next offset is before current offset, file appears to be malformed.\n");
                cli_append_possibly_unwanted(ctx, "Heuristics.Broken.Media.TIFF.OutOfOrderIFDOffset");
                status = CL_EPARSE;
                goto done;
            }
        }
    } while (offset);

    cli_dbgmsg("cli_parsetiff: examined %u IFD(s)\n", ifd_count);

    status = CL_CLEAN;

done:
    if (status == CL_EPARSE) {
        /* We added with cli_append_possibly_unwanted so it will alert at the end if nothing else matches. */
        status = CL_CLEAN;
    }

    return status;
}
