/* SPDX-License-Identifier: BSD-2-Clause */
/* Copyright 1996-2020 The NASM Authors - All Rights Reserved */

/*
 * codeview.c Codeview Debug Format support for COFF
 */

#include "version.h"
#include "compiler.h"


#include "nasm.h"
#include "nasmlib.h"
#include "error.h"
#include "preproc.h"
#include "saa.h"
#include "hashtbl.h"
#include "outlib.h"
#include "pecoff.h"
#include "md5.h"

static void cv8_init(void);
static void cv8_linenum(const char *filename, int32_t linenumber,
        int32_t segto);
static void cv8_deflabel(char *name, int32_t segment, int64_t offset,
        int is_global, char *special);
static void cv8_typevalue(int32_t type);
static void cv8_cleanup(void);

const struct dfmt df_cv8 = {
    "Codeview 8+",              /* .fullname */
    "cv8",                      /* .shortname */
    cv8_init,                   /* .init */
    cv8_linenum,                /* .linenum */
    cv8_deflabel,               /* .debug_deflabel */
    NULL,                       /* .debug_smacros */
    NULL,                       /* .debug_include */
    NULL,                       /* .debug_mmacros */
    null_debug_directive,       /* .debug_directive */
    cv8_typevalue,              /* .debug_typevalue */
    NULL,                       /* .debug_output */
    cv8_cleanup,                /* .cleanup */
    NULL                        /* pragma list */
};

/*******************************************************************************
 * dfmt callbacks
 ******************************************************************************/
struct sect_lines
{
    struct SAA *lines;
    uint32_t num_lines;
};

struct source_file;

struct source_file {
    const char *filename;
    char *fullname;
    uint32_t fullnamelen;

    struct source_file *next;

    uint32_t filetbl_off;
    uint32_t sourcetbl_off;

    struct sect_lines *sects;

    unsigned char md5sum[MD5_HASHBYTES];
};

struct linepair {
    uint32_t file_offset;
    uint32_t linenumber;
};

enum symbol_type {
    SYMTYPE_CODE,
    SYMTYPE_PROC,
    SYMTYPE_LDATA,
    SYMTYPE_GDATA,

    SYMTYPE_MAX
};

struct cv8_symbol {
    enum symbol_type type;
    char *name;

    uint32_t secrel;
    uint16_t section;
    uint32_t size;
    uint32_t typeindex;

    enum symtype {
        TYPE_UNREGISTERED = 0x0000, /* T_NOTYPE */
        TYPE_BYTE = 0x0020,
        TYPE_WORD = 0x0021,
        TYPE_DWORD= 0x0022,
        TYPE_QUAD = 0x0023,

        TYPE_REAL32 = 0x0040,
        TYPE_REAL64 = 0x0041,
        TYPE_REAL80 = 0x0042,
        TYPE_REAL128= 0x0043,
        TYPE_REAL256= 0x0044,
        TYPE_REAL512= 0x0045
    } symtype;
};

struct cv8_state {
    int symbol_sect;
    int type_sect;

    struct source_file *source_files, **source_files_tail;
    const char *last_filename;
    struct source_file *last_source_file;
    struct hash_table file_hash;
    unsigned num_files;
    uint32_t total_filename_len;

    struct SAA *symbols;
    struct cv8_symbol *last_sym;
    unsigned num_syms[SYMTYPE_MAX];
    unsigned symbol_lengths;
    unsigned total_syms;

    struct {
        char *name;
        size_t namebytes;
    } outfile;
};
struct cv8_state cv8_state;

static void cv8_init(void)
{
    const uint32_t sect_flags =     IMAGE_SCN_MEM_READ |
                    IMAGE_SCN_MEM_DISCARDABLE |
                    IMAGE_SCN_CNT_INITIALIZED_DATA |
                    IMAGE_SCN_ALIGN_1BYTES;

    cv8_state.symbol_sect = coff_make_section(".debug$S", sect_flags);
    cv8_state.type_sect = coff_make_section(".debug$T", sect_flags);

    cv8_state.source_files = NULL;
    cv8_state.source_files_tail = &cv8_state.source_files;

    cv8_state.num_files = 0;
    cv8_state.total_filename_len = 0;

    cv8_state.symbols = saa_init(sizeof(struct cv8_symbol));
    cv8_state.last_sym = NULL;
}

static struct source_file *register_file(const char *filename);
static int find_section(int32_t segto);

static void cv8_linenum(const char *filename, int32_t linenumber,
        int32_t segto)
{
    struct linepair *li;
    struct sect_lines *sl;
    int i;

    i = find_section(segto);
    if (i == -1)
        return;

    if ((coff_sects[i]->flags & IMAGE_SCN_MEM_EXECUTE) == 0)
        return;

    sl = &register_file(filename)->sects[i];

    if (!sl->lines)
        sl->lines = saa_init(sizeof(struct linepair));

    li = saa_wstruct(sl->lines);
    li->file_offset = coff_sects[i]->len;
    li->linenumber = linenumber;

    sl->num_lines++;
}

static void cv8_deflabel(char *name, int32_t segment, int64_t offset,
        int is_global, char *special)
{
    struct cv8_symbol *sym;
    int i;

    (void)special;

    /* Skip macro-local labels */
    if (name[0] == '.' && name[1] == '.' && name[2] == '@' && name[3] >= '0' && name[3] <= '9')
        return;

    i = find_section(segment);
    if (i == -1)
        return;

    /* MS linker errors on relocations to .pdata section, so skip such symbols */
    if (!strcmp(coff_sects[i]->name, ".pdata"))
        return;

    sym = saa_wstruct(cv8_state.symbols);

    if (coff_sects[i]->flags & IMAGE_SCN_MEM_EXECUTE)
        sym->type = is_global ? SYMTYPE_PROC : SYMTYPE_CODE;
    else
        sym->type = is_global ?  SYMTYPE_GDATA : SYMTYPE_LDATA;
    cv8_state.num_syms[sym->type]++;
    cv8_state.total_syms++;

    sym->section = segment;
    sym->secrel = offset;
    sym->symtype = TYPE_UNREGISTERED;
    sym->size = 0;
    sym->typeindex = 0;

    sym->name = nasm_strdup(name);
    cv8_state.symbol_lengths += strlen(sym->name) + 1;

    if (cv8_state.last_sym && cv8_state.last_sym->section == segment)
        cv8_state.last_sym->size = offset - cv8_state.last_sym->secrel;
    cv8_state.last_sym = sym;
}

static void cv8_typevalue(int32_t type)
{
    if (!cv8_state.last_sym)
        return;
    if (cv8_state.last_sym->symtype != TYPE_UNREGISTERED)
        return;

    switch (TYM_TYPE(type)) {
    case TY_BYTE:
        cv8_state.last_sym->symtype = TYPE_BYTE;
        break;
    case TY_WORD:
        cv8_state.last_sym->symtype = TYPE_WORD;
        break;
    case TY_DWORD:
        cv8_state.last_sym->symtype = TYPE_DWORD;
        break;
    case TY_QWORD:
        cv8_state.last_sym->symtype = TYPE_QUAD;
        break;
    case TY_FLOAT:
        cv8_state.last_sym->symtype = TYPE_REAL32;
        break;
    case TY_TBYTE:
        cv8_state.last_sym->symtype = TYPE_REAL80;
        break;
    case TY_OWORD:
        cv8_state.last_sym->symtype = TYPE_REAL128;
        break;
    case TY_YWORD:
        cv8_state.last_sym->symtype = TYPE_REAL256;
        break;
    case TY_ZWORD:
        cv8_state.last_sym->symtype = TYPE_REAL512;
        break;
    case TY_UNKNOWN:
        break;
    case TY_LABEL:
        break;
    }
}

static void build_symbol_table(struct coff_Section *const sect);
static void build_type_table(struct coff_Section *const sect);

static void cv8_cleanup(void)
{
    struct cv8_symbol *sym;
    struct source_file *file, *ftmp;

    struct coff_Section *symbol_sect = coff_sects[cv8_state.symbol_sect];
    struct coff_Section *type_sect = coff_sects[cv8_state.type_sect];

    cv8_state.outfile.name = nasm_realpath(outname);
    cv8_state.outfile.namebytes = strlen(cv8_state.outfile.name) + 1;

    build_symbol_table(symbol_sect);
    build_type_table(type_sect);

    list_for_each_safe(file, ftmp, cv8_state.source_files) {
        int i;
        nasm_free(file->fullname);
        for (i = 0; i < coff_nsects; i++)
            if (file->sects[i].lines)
                saa_free(file->sects[i].lines);
        nasm_free(file->sects);
        nasm_free(file);
    }
    hash_free(&cv8_state.file_hash);

    saa_rewind(cv8_state.symbols);
    while ((sym = saa_rstruct(cv8_state.symbols)))
        nasm_free(sym->name);
    saa_free(cv8_state.symbols);

    nasm_free(cv8_state.outfile.name);
}

/*******************************************************************************
 * implementation
 ******************************************************************************/
static void calc_md5(const char *const filename,
        unsigned char sum[MD5_HASHBYTES])
{
    int success = 0;
    unsigned char *file_buf;
    FILE *f;
    MD5_CTX ctx;

    f = pp_input_fopen(filename, NF_BINARY);
    if (!f)
        goto done;

    file_buf = nasm_zalloc(BUFSIZ);

    MD5Init(&ctx);
    while (!feof(f)) {
        size_t i = fread(file_buf, 1, BUFSIZ, f);
        if (ferror(f))
            goto done_0;
        else if (i == 0)
            break;
        MD5Update(&ctx, file_buf, i);
    }
    MD5Final(sum, &ctx);

    success = 1;
done_0:
    nasm_free(file_buf);
    fclose(f);
done:
    if (!success) {
        nasm_nonfatal("unable to hash file %s. "
                      "Debug information may be unavailable.",
                      filename);
    }
    return;
}

static struct source_file *register_file(const char *filename)
{
    struct source_file *file;
    void **filep;
    char *fullpath;
    struct hash_insert hi;

    /*
     * The common case is that we are invoked with the same filename
     * as we were last time.  Make this a pointer comparison: this is
     * safe because the NASM core code allocates each filename once
     * and never frees it.
     */
    if (likely(cv8_state.last_filename == filename))
        return cv8_state.last_source_file;

    cv8_state.last_filename = filename;

    filep = hash_find(&cv8_state.file_hash, filename, &hi);
    if (likely(filep)) {
        file = *filep;
    } else {
        /* New filename encounter */

        fullpath = nasm_realpath(filename);

        nasm_new(file);
        file->filename = filename;
        file->fullname = fullpath;
        file->fullnamelen = strlen(fullpath);
        file->sects = nasm_calloc(coff_nsects, sizeof(*file->sects));
        *cv8_state.source_files_tail = file;
        cv8_state.source_files_tail = &file->next;
        calc_md5(fullpath, file->md5sum);

        hash_add(&hi, filename, file);

        cv8_state.num_files++;
        cv8_state.total_filename_len += file->fullnamelen + 1;
    }

    cv8_state.last_source_file = file;
    return file;
}

static int find_section(int32_t segto)
{
    int i;

    for (i = 0; i < coff_nsects; i++)
        if (segto == coff_sects[i]->index)
            return i;

    return -1;
}

static void register_reloc(struct coff_Section *const sect,
        int reloc_sect, char *sym, uint32_t addr, uint16_t type)
{
    struct coff_Reloc *r;
    uint32_t i;

    r = *sect->tail = nasm_malloc(sizeof(struct coff_Reloc));
    sect->tail = &r->next;
    r->next = NULL;
    sect->nrelocs++;

    r->address = addr;
    r->symbase = SECT_SYMBOLS;
    r->type = type;

    r->symbol = reloc_sect * 2;
    if (reloc_sect < coff_nsects)
        return;

    saa_rewind(coff_syms);
    for (i = 0; i < coff_nsyms; i++) {
        struct coff_Symbol *s = saa_rstruct(coff_syms);
        r->symbol++;
        if (s->strpos == -1 && !strcmp(sym, s->name)) {
            return;
        } else if (s->strpos != -1) {
            int res;
            char *symname;

            symname = nasm_malloc(s->namlen + 1);
            saa_fread(coff_strs, s->strpos-4, symname, s->namlen);
            symname[s->namlen] = '\0';
            res = strcmp(sym, symname);
            nasm_free(symname);
            if (!res)
                return;
        }
    }
    nasm_panic("codeview: relocation for unregistered symbol: %s", sym);
}

static inline void section_write32(struct coff_Section *sect, uint32_t val)
{
    saa_write32(sect->data, val);
    sect->len += 4;
}

static inline void section_write16(struct coff_Section *sect, uint16_t val)
{
    saa_write16(sect->data, val);
    sect->len += 2;
}

static inline void section_write8(struct coff_Section *sect, uint8_t val)
{
    saa_write8(sect->data, val);
    sect->len++;
}

static inline void section_wbytes(struct coff_Section *sect, const void *buf,
        size_t len)
{
    saa_wbytes(sect->data, buf, len);
    sect->len += len;
}

static void write_filename_table(struct coff_Section *const sect)
{
    uint32_t field_length;
    uint32_t tbl_off = 1;    /* offset starts at 1 to skip NULL entry */
    struct source_file *file;

    nasm_assert(cv8_state.source_files != NULL);
    nasm_assert(cv8_state.num_files > 0);
    nasm_assert(cv8_state.total_filename_len > 0);

    field_length = 1 + cv8_state.total_filename_len;

    section_write32(sect, 0x000000F3);
    section_write32(sect, field_length);

    section_write8(sect, 0);

    list_for_each(file, cv8_state.source_files) {
        section_wbytes(sect, file->fullname, file->fullnamelen + 1);
        file->filetbl_off = tbl_off;
        tbl_off += file->fullnamelen + 1;
    }
}

static void write_sourcefile_table(struct coff_Section *const sect)
{
    const uint32_t entry_size = 4 + 2 + MD5_HASHBYTES + 2;

    uint32_t field_length = 0;
    uint32_t tbl_off = 0;
    struct source_file *file;

    field_length = entry_size * cv8_state.num_files;

    section_write32(sect, 0x000000F4);
    section_write32(sect, field_length);

    list_for_each(file, cv8_state.source_files) {
        nasm_assert(file->filetbl_off > 0);
        section_write32(sect, file->filetbl_off);
        section_write16(sect, 0x0110);
        section_wbytes(sect, file->md5sum, MD5_HASHBYTES);
        section_write16(sect, 0);

        file->sourcetbl_off = tbl_off;
        tbl_off += entry_size;
    }
}

static void write_linenumber_table(struct coff_Section *const sect)
{
    const uint32_t file_field_len = 12;
    const uint32_t line_field_len = 8;

    int i;

    for (i = 0; i < coff_nsects; i++) {
        uint32_t field_length;
        size_t field_base;
        struct source_file *file;
        struct coff_Section *s;
        unsigned num_files = 0;
        unsigned total_lines = 0;

        list_for_each(file, cv8_state.source_files)
            if (file->sects[i].num_lines) {
                num_files++;
                total_lines += file->sects[i].num_lines;
            }
        if (!total_lines)
            continue;

        s = coff_sects[i];

        field_length = 12;
        field_length += (num_files * file_field_len);
        field_length += (total_lines * line_field_len);

        section_write32(sect, 0x000000F2);
        section_write32(sect, field_length);

        field_base = sect->len;
        section_write32(sect, 0); /* SECREL, updated by relocation */
        section_write16(sect, 0); /* SECTION, updated by relocation*/
        section_write16(sect, 0); /* pad */
        section_write32(sect, s->len);

        register_reloc(sect, i, NULL, field_base,
            win64 ? IMAGE_REL_AMD64_SECREL : IMAGE_REL_I386_SECREL);

        register_reloc(sect, i, NULL, field_base + 4,
            win64 ? IMAGE_REL_AMD64_SECTION : IMAGE_REL_I386_SECTION);

        list_for_each(file, cv8_state.source_files) {
            struct linepair *li;

            /* source mapping */
            section_write32(sect, file->sourcetbl_off);
            section_write32(sect, file->sects[i].num_lines);
            section_write32(sect, file_field_len + (file->sects[i].num_lines * line_field_len));

            /* the pairs */
            saa_rewind(file->sects[i].lines);
            while ((li = saa_rstruct(file->sects[i].lines))) {
                section_write32(sect, li->file_offset);
                section_write32(sect, li->linenumber |= 0x80000000);
            }
        }
    }
}

static uint32_t write_symbolinfo_obj(struct coff_Section *sect)
{
    uint32_t obj_len;

    obj_len = 2 + 4 + cv8_state.outfile.namebytes;

    section_write16(sect, obj_len);
    section_write16(sect, 0x1101);
    section_write32(sect, 0); /* ASM language */
    section_wbytes(sect, cv8_state.outfile.name, cv8_state.outfile.namebytes);

    return obj_len;
}

static uint32_t write_symbolinfo_properties(struct coff_Section *sect,
        const char *const creator_str)
{
    /* https://github.com/Microsoft/microsoft-pdb/blob/1d60e041/include/cvinfo.h#L3313 */
    uint32_t creator_len;

    creator_len = 2 + 4 + 2 + 3*2 + 3*2 + strlen(creator_str)+1 + 2;

    /*
     * We used to use a language ID of 3 for "MASM", since it's closest of the
     * options available; however, BinScope from WACK (the Windows Application
     * Certification Kit) tests for specific minimum MASM versions and trying to
     * match an increasing sequence of random MASM version/build numbers seems
     * like a fool's errand.
     *
     * Instead, use a different language ID (NASM is, after all, not MASM
     * syntax) and just write the actual NASM version number. BinScope appears
     * to be happy with that.
     */

    section_write16(sect, creator_len);
    section_write16(sect, 0x1116);
    section_write32(sect, 'N'); /* language: 'N' (0x4e) for "NASM"; flags are 0 */
    if (win64)
        section_write16(sect, 0x00D0); /* machine */
    else if (win32)
        section_write16(sect, 0x0006); /* machine */
    else
        nasm_panic("neither win32 nor win64 are set!");
    section_write16(sect, 0); /* verFEMajor */
    section_write16(sect, 0); /* verFEMinor */
    section_write16(sect, 0); /* verFEBuild */

    /* BinScope/WACK insist on version >= 8.0.50727 */
    section_write16(sect, NASM_MAJOR_VER); /* verMajor */
    section_write16(sect, NASM_MINOR_VER); /* verMinor */
    section_write16(sect, NASM_SUBMINOR_VER*100 + NASM_PATCHLEVEL_VER); /* verBuild */

    section_wbytes(sect, creator_str, strlen(creator_str)+1); /* verSt */
    /*
     * normally there would be key/value pairs here, but they aren't
     * necessary. They are terminated by 2B
     */
    section_write16(sect, 0);

    return creator_len;
}

static uint32_t write_symbolinfo_symbols(struct coff_Section *sect)
{
    uint32_t len = 0, field_len;
    uint32_t field_base;
    struct cv8_symbol *sym;

    saa_rewind(cv8_state.symbols);
    while ((sym = saa_rstruct(cv8_state.symbols))) {
        switch (sym->type) {
        case SYMTYPE_LDATA:
        case SYMTYPE_GDATA:
            field_len = 12 + strlen(sym->name) + 1;
            len += field_len - 2;
            section_write16(sect, field_len);
            if (sym->type == SYMTYPE_LDATA)
                section_write16(sect, 0x110C);
            else
                section_write16(sect, 0x110D);
            section_write32(sect, sym->symtype);

            field_base = sect->len;
            section_write32(sect, 0); /* SECREL */
            section_write16(sect, 0); /* SECTION */
            break;
        case SYMTYPE_PROC:
        case SYMTYPE_CODE:
            field_len = 9 + strlen(sym->name) + 1;
            len += field_len - 2;
            section_write16(sect, field_len);
            section_write16(sect, 0x1105);

            field_base = sect->len;
            section_write32(sect, 0); /* SECREL */
            section_write16(sect, 0); /* SECTION */
            section_write8(sect, 0); /* FLAG */
            break;
        default:
            nasm_panic("unknown symbol type");
        }

        section_wbytes(sect, sym->name, strlen(sym->name) + 1);

        register_reloc(sect, coff_nsects, sym->name, field_base,
            win64 ? IMAGE_REL_AMD64_SECREL :
                IMAGE_REL_I386_SECREL);
        register_reloc(sect, coff_nsects, sym->name, field_base + 4,
            win64 ? IMAGE_REL_AMD64_SECTION :
                IMAGE_REL_I386_SECTION);
    }

    return len;
}

static void write_symbolinfo_table(struct coff_Section *const sect)
{
    static const char creator_str[] = "The Netwide Assembler " NASM_VER;
    uint32_t obj_length, creator_length, sym_length;
    uint32_t field_length = 0, out_len;

    nasm_assert(cv8_state.outfile.namebytes);

    /* signature, language, outfile NULL */
    obj_length = 2 + 4 + cv8_state.outfile.namebytes;
    creator_length = 2 + 4 + 2 + 3*2 + 3*2 + strlen(creator_str)+1 + 2;

    sym_length =    ( cv8_state.num_syms[SYMTYPE_CODE] *  7) +
            ( cv8_state.num_syms[SYMTYPE_PROC] *  7) +
            ( cv8_state.num_syms[SYMTYPE_LDATA] * 10) +
            ( cv8_state.num_syms[SYMTYPE_GDATA] * 10) +
            cv8_state.symbol_lengths;

    field_length = 2 + obj_length +
               2 + creator_length +
               (4 * cv8_state.total_syms) + sym_length;

    section_write32(sect, 0x000000F1);
    section_write32(sect, field_length);

    /* for sub fields, length proceeds type */

    out_len = write_symbolinfo_obj(sect);
    nasm_assert(out_len == obj_length);

    out_len = write_symbolinfo_properties(sect, creator_str);
    nasm_assert(out_len == creator_length);

    out_len = write_symbolinfo_symbols(sect);
    nasm_assert(out_len == sym_length);
}

static inline void align4_table(struct coff_Section *const sect)
{
    unsigned diff;
    uint32_t zero = 0;
    struct SAA *data = sect->data;

    if (data->wptr % 4 == 0)
        return;

    diff = 4 - (data->wptr % 4);
    if (diff)
        section_wbytes(sect, &zero, diff);
}

static void build_symbol_table(struct coff_Section *const sect)
{
    section_write32(sect, 0x00000004);

    if (cv8_state.source_files) {
        write_filename_table(sect);
        align4_table(sect);
        write_sourcefile_table(sect);
        align4_table(sect);
        write_linenumber_table(sect);
        align4_table(sect);
    }
    write_symbolinfo_table(sect);
    align4_table(sect);
}

static void build_type_table(struct coff_Section *const sect)
{
    uint32_t field_len;
    uint32_t typeindex = 0x1000;
    uint32_t idx_arglist;

    section_write32(sect, 0x00000004);

    /* empty argument list type */
    field_len = 2 + 4;
    section_write16(sect, field_len);
    section_write16(sect, 0x1201); /* ARGLIST */
    section_write32(sect, 0); /* num params */
    idx_arglist = typeindex++;

    /* procedure type: void proc(void) */
    field_len = 2 + 4 + 1 + 1 + 2 + 4;
    section_write16(sect, field_len);
    section_write16(sect, 0x1008); /* PROC type */

    section_write32(sect, 0x00000003); /* return type VOID */
    section_write8(sect, 0);  /* calling convention (default) */
    section_write8(sect, 0);  /* function attributes */
    section_write16(sect, 0); /* # params */
    section_write32(sect, idx_arglist); /* argument list type */
    /* idx_voidfunc = typeindex++; */
}
