/* SPDX-License-Identifier: BSD-3-Clause */

#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "log.h"
#include "tpm2_attr_util.h"

#define dispatch_no_arg_add(x) \
    { .name = str(x), .callback=(action)x, .width = 1 }

#define dispatch_arg_add(x, w) \
    { .name = str(x), .callback=(action)x, .width = w }

#define dispatch_reserved(pos) \
    { .name = "<reserved("xstr(pos)")>", .callback=NULL, .width = 1 }

typedef enum dispatch_error dispatch_error;
enum dispatch_error {
    dispatch_ok = 0, dispatch_err, dispatch_no_match,
};

typedef bool (*action)(void *obj, char *arg);

typedef struct dispatch_table dispatch_table;
struct dispatch_table {
    char *name;
    action callback;
    unsigned width; /* the width of the field, CANNOT be 0 */
};

static bool authread(TPMA_NV *nv, char *arg) {

    UNUSED(arg);
    *nv |= TPMA_NV_AUTHREAD;
    return true;
}

static bool authwrite(TPMA_NV *nv, char *arg) {

    UNUSED(arg);
    *nv |= TPMA_NV_AUTHWRITE;
    return true;
}

static bool clear_stclear(TPMA_NV *nv, char *arg) {

    UNUSED(arg);
    *nv |= TPMA_NV_CLEAR_STCLEAR;
    return true;
}

static bool globallock(TPMA_NV *nv, char *arg) {

    UNUSED(arg);
    *nv |= TPMA_NV_GLOBALLOCK;
    return true;
}

static bool no_da(TPMA_NV *nv, char *arg) {

    UNUSED(arg);
    *nv |= TPMA_NV_NO_DA;
    return true;
}

static bool orderly(TPMA_NV *nv, char *arg) {

    UNUSED(arg);
    *nv |= TPMA_NV_ORDERLY;
    return true;
}

static bool ownerread(TPMA_NV *nv, char *arg) {

    UNUSED(arg);
    *nv |= TPMA_NV_OWNERREAD;
    return true;
}

static bool ownerwrite(TPMA_NV *nv, char *arg) {

    UNUSED(arg);
    *nv |= TPMA_NV_OWNERWRITE;
    return true;
}

static bool platformcreate(TPMA_NV *nv, char *arg) {

    UNUSED(arg);
    *nv |= TPMA_NV_PLATFORMCREATE;
    return true;
}

static bool policyread(TPMA_NV *nv, char *arg) {

    UNUSED(arg);
    *nv |= TPMA_NV_POLICYREAD;
    return true;
}

static bool policywrite(TPMA_NV *nv, char *arg) {

    UNUSED(arg);
    *nv |= TPMA_NV_POLICYWRITE;
    return true;
}

static bool policydelete(TPMA_NV *nv, char *arg) {

    UNUSED(arg);
    *nv |= TPMA_NV_POLICY_DELETE;
    return true;
}

static bool ppread(TPMA_NV *nv, char *arg) {

    UNUSED(arg);
    *nv |= TPMA_NV_PPREAD;
    return true;
}

static bool ppwrite(TPMA_NV *nv, char *arg) {

    UNUSED(arg);
    *nv |= TPMA_NV_PPWRITE;
    return true;
}

static bool readlocked(TPMA_NV *nv, char *arg) {

    UNUSED(arg);
    *nv |= TPMA_NV_READLOCKED;
    return true;
}

static bool read_stclear(TPMA_NV *nv, char *arg) {

    UNUSED(arg);
    *nv |= TPMA_NV_READ_STCLEAR;
    return true;
}

static bool writeall(TPMA_NV *nv, char *arg) {

    UNUSED(arg);
    *nv |= TPMA_NV_WRITEALL;
    return true;
}

static bool writedefine(TPMA_NV *nv, char *arg) {

    UNUSED(arg);
    *nv |= TPMA_NV_WRITEDEFINE;
    return true;
}

static bool writelocked(TPMA_NV *nv, char *arg) {

    UNUSED(arg);
    *nv |= TPMA_NV_WRITELOCKED;
    return true;
}

static bool write_stclear(TPMA_NV *nv, char *arg) {

    UNUSED(arg);
    *nv |= TPMA_NV_WRITE_STCLEAR;
    return true;
}

static bool written(TPMA_NV *nv, char *arg) {

    UNUSED(arg);
    *nv |= TPMA_NV_WRITTEN;
    return true;
}

static bool lookup_nt_friendly_name(const char *arg, uint16_t *type) {

    if (!strcmp(arg, "ordinary")) {
        // Nothing to do
    } else if (!strcmp(arg, "counter")) {
        *type = TPM2_NT_COUNTER;
    } else if (!strcmp(arg, "bits")) {
        *type = TPM2_NT_BITS;
    } else if (!strcmp(arg, "extend")) {
        *type = TPM2_NT_EXTEND;
    } else if (!strcmp(arg, "pinfail")) {
        *type = TPM2_NT_PIN_FAIL;
    } else if (!strcmp(arg, "pinpass")) {
        *type = TPM2_NT_PIN_PASS;
    } else {
        LOG_ERR("Unknown NT type specifier, got: \"%s\"", arg);
        return false;
    }

    return true;
}

static bool nt(TPMA_NV *nv, char *arg) {

    uint16_t value;
    bool result = tpm2_util_string_to_uint16(arg, &value);
    if (!result) {
        result = lookup_nt_friendly_name(arg, &value);
        if (!result) {
            LOG_ERR("Could not convert NT field, got \"%s\"."
                    "Expected a number or friendly name", arg);
            return false;
        }
    }

    /* nt space is 4 bits, so max of 15 */
    if (value > 0x0F) {
        LOG_ERR("Field TPM_NT of type TPMA_NV is only 4 bits,"
                "value \"%s\" to big!", arg);
        return false;
    }

    *nv &= ~TPMA_NV_TPM2_NT_MASK;
    *nv |= value << 4;
    return true;
}

/*
 * The order of this table must be in order with the bit defines in table 204:
 * https://trustedcomputinggroup.org/wp-content/uploads/TPM-Rev-2.0-Part-2-Structures-01.38.pdf
 *
 * This table is in bitfield order, thus the index of a bit set in a TPMA_NV
 * can be used to lookup the name.
 *
 * if not the logic in tpm2_nv_util_strtoattr would need to change!
 */
static dispatch_table nv_attr_table[] = { // Bit Index
    dispatch_no_arg_add(ppwrite),         //  0
    dispatch_no_arg_add(ownerwrite),      //  1
    dispatch_no_arg_add(authwrite),       //  2
    dispatch_no_arg_add(policywrite),     //  3
    dispatch_arg_add(nt, 4),              //  4
    dispatch_arg_add(nt, 3),              //  5
    dispatch_arg_add(nt, 2),              //  6
    dispatch_arg_add(nt, 1),              //  7
    dispatch_reserved(8),                 //  8
    dispatch_reserved(9),                 //  9
    dispatch_no_arg_add(policydelete),    // 10
    dispatch_no_arg_add(writelocked),     // 11
    dispatch_no_arg_add(writeall),        // 12
    dispatch_no_arg_add(writedefine),     // 13
    dispatch_no_arg_add(write_stclear),   // 14
    dispatch_no_arg_add(globallock),      // 15
    dispatch_no_arg_add(ppread),          // 16
    dispatch_no_arg_add(ownerread),       // 17
    dispatch_no_arg_add(authread),        // 18
    dispatch_no_arg_add(policyread),      // 19
    dispatch_reserved(20),                // 20
    dispatch_reserved(21),                // 21
    dispatch_reserved(22),                // 22
    dispatch_reserved(23),                // 23
    dispatch_reserved(24),                // 24
    dispatch_no_arg_add(no_da),           // 25
    dispatch_no_arg_add(orderly),         // 26
    dispatch_no_arg_add(clear_stclear),   // 27
    dispatch_no_arg_add(readlocked),      // 28
    dispatch_no_arg_add(written),         // 29
    dispatch_no_arg_add(platformcreate),  // 30
    dispatch_no_arg_add(read_stclear),    // 31
};

static bool fixedtpm(TPMA_OBJECT *obj, char *arg) {

    UNUSED(arg);
    *obj |= TPMA_OBJECT_FIXEDTPM;
    return true;
}

static bool stclear(TPMA_OBJECT *obj, char *arg) {

    UNUSED(arg);
    *obj |= TPMA_OBJECT_STCLEAR;
    return true;
}

static bool fixedparent(TPMA_OBJECT *obj, char *arg) {

    UNUSED(arg);
    *obj |= TPMA_OBJECT_FIXEDPARENT;
    return true;
}

static bool sensitivedataorigin(TPMA_OBJECT *obj, char *arg) {

    UNUSED(arg);
    *obj |= TPMA_OBJECT_SENSITIVEDATAORIGIN;
    return true;
}

static bool userwithauth(TPMA_OBJECT *obj, char *arg) {

    UNUSED(arg);
    *obj |= TPMA_OBJECT_USERWITHAUTH;
    return true;
}

static bool adminwithpolicy(TPMA_OBJECT *obj, char *arg) {

    UNUSED(arg);
    *obj |= TPMA_OBJECT_ADMINWITHPOLICY;
    return true;
}

static bool noda(TPMA_OBJECT *obj, char *arg) {

    UNUSED(arg);
    *obj |= TPMA_OBJECT_NODA;
    return true;
}

static bool encryptedduplication(TPMA_OBJECT *obj, char *arg) {

    UNUSED(arg);
    *obj |= TPMA_OBJECT_ENCRYPTEDDUPLICATION;
    return true;
}

static bool restricted(TPMA_OBJECT *obj, char *arg) {

    UNUSED(arg);
    *obj |= TPMA_OBJECT_RESTRICTED;
    return true;
}

static bool decrypt(TPMA_OBJECT *obj, char *arg) {

    UNUSED(arg);
    *obj |= TPMA_OBJECT_DECRYPT;
    return true;
}

static bool sign(TPMA_OBJECT *obj, char *arg) {

    UNUSED(arg);
    *obj |= TPMA_OBJECT_SIGN_ENCRYPT;
    return true;
}

static dispatch_table obj_attr_table[] = {         // Bit Index
        dispatch_reserved(0),                      //  0
        dispatch_no_arg_add(fixedtpm),             //  1
        dispatch_no_arg_add(stclear),              //  2
        dispatch_reserved(3),                      //  3
        dispatch_no_arg_add(fixedparent),          //  4
        dispatch_no_arg_add(sensitivedataorigin),  //  5
        dispatch_no_arg_add(userwithauth),         //  6
        dispatch_no_arg_add(adminwithpolicy),      //  7
        dispatch_reserved(8),                      //  8
        dispatch_reserved(9),                      //  9
        dispatch_no_arg_add(noda),                 // 10
        dispatch_no_arg_add(encryptedduplication), // 11
        dispatch_reserved(12),                     // 12
        dispatch_reserved(13),                     // 13
        dispatch_reserved(14),                     // 14
        dispatch_reserved(15),                     // 15
        dispatch_no_arg_add(restricted),           // 16
        dispatch_no_arg_add(decrypt),              // 17
        dispatch_no_arg_add(sign),                 // 18
        dispatch_reserved(19),                     // 19
        dispatch_reserved(20),                     // 20
        dispatch_reserved(21),                     // 21
        dispatch_reserved(22),                     // 22
        dispatch_reserved(23),                     // 23
        dispatch_reserved(24),                     // 24
        dispatch_reserved(25),                     // 25
        dispatch_reserved(26),                     // 26
        dispatch_reserved(27),                     // 27
        dispatch_reserved(28),                     // 28
        dispatch_reserved(29),                     // 29
        dispatch_reserved(30),                     // 30
        dispatch_reserved(31),                     // 31
};

static bool token_match(const char *name, const char *token, bool has_arg,
        char **sep) {

    /* if it has an argument, we expect a separator */
    size_t match_len = strlen(token);
    if (has_arg) {
        *sep = strchr(token, '=');
        if (*sep) {
            match_len = *sep - token;
        }
    }

    return !strncmp(name, token, match_len);
}

static dispatch_error handle_dispatch(dispatch_table *d, char *token,
        TPMA_NV *nvattrs) {

    char *name = d->name;
    action cb = d->callback;
    bool has_arg = d->width > 1;

    /* if no callback, then its a reserved block, just skip it */
    if (!cb) {
        return dispatch_no_match;
    }

    char *sep = NULL;
    bool match = token_match(name, token, has_arg, &sep);
    if (!match) {
        return dispatch_no_match;
    }

    /*
     * If it has an argument, match should have found the separator.
     */
    char *arg = NULL;
    if (has_arg) {
        if (!sep) {
            LOG_ERR("Expected argument for \"%s\", got none.", token);
            return dispatch_err;
        }

        /* split token on = */
        *sep = '\0';
        sep++;
        if (!*sep) {
            LOG_ERR("Expected argument for \"%s\", got none.", token);
            return dispatch_err;
        }

        /* valid argument string, assign */
        arg = sep;
    }

    bool result = cb(nvattrs, arg);
    return result ? dispatch_ok : dispatch_err;
}

static bool common_strtoattr(char *attribute_list, void *attrs,
        dispatch_table *table, size_t size) {

    char *token;
    char *save;

    /*
     * This check is solely to prevent GCC from complaining on:
     * error: ‘attribute_list’ may be used uninitialized in this function [-Werror=maybe-uninitialized]
     * Might as well check nvattrs as well.
     */
    if (!attribute_list || !attrs) {
        LOG_ERR("attribute list or attributes structure is NULL");
        return false;
    }

    while ((token = strtok_r(attribute_list, "|", &save))) {
        attribute_list = NULL;

        bool did_dispatch = false;

        size_t i;
        for (i = 0; i < size; i++) {
            dispatch_table *d = &table[i];

            dispatch_error err = handle_dispatch(d, token, attrs);
            if (err == dispatch_ok) {
                did_dispatch = true;
                break;
            } else if (err == dispatch_err) {
                return false;
            }
            /* dispatch_no_match --> keep looking */
        }

        /* did we dispatch?, If not log error and return */
        if (!did_dispatch) {
            char *tmp = strchr(token, '=');
            if (tmp) {
                *tmp = '\0';
            }
            LOG_ERR("Unknown token: \"%s\" found.", token);
            return false;
        }
    }

    return true;
}

bool tpm2_attr_util_nv_strtoattr(char *attribute_list, TPMA_NV *nvattrs) {

    memset(nvattrs, 0, sizeof(*nvattrs));
    return common_strtoattr(attribute_list, nvattrs, nv_attr_table,
            ARRAY_LEN(nv_attr_table));
}

bool tpm2_attr_util_obj_strtoattr(char *attribute_list, TPMA_OBJECT *objattrs) {

    memset(objattrs, 0, sizeof(*objattrs));
    return common_strtoattr(attribute_list, objattrs, obj_attr_table,
            ARRAY_LEN(obj_attr_table));
}

static UINT8 find_first_set(UINT32 bits) {

    UINT8 n = 0;

    if (!bits) {
        return n;
    }

    if (!(bits & 0x0000FFFF)) {
        n += 16;
        bits >>= 16;
    }
    if (!(bits & 0x000000FF)) {
        n += 8;
        bits >>= 8;
    }
    if (!(bits & 0x0000000F)) {
        n += 4;
        bits >>= 4;
    }
    if (!(bits & 0x00000003)) {
        n += 2;
        bits >>= 2;
    }
    if (!(bits & 0x00000001))
        n += 1;

    return n;
}

static char *tpm2_attr_util_common_attrtostr(UINT32 attrs,
        dispatch_table *table, size_t size) {

    if (attrs == 0) {
        return strdup("<none>");
    }

    /*
     * Get how many bits are set in the attributes and then find the longest
     * "name".
     *
     * pop_cnt * max_name_len + pop_cnt - 1 (for the | separators) + 4
     * (for nv field equals in hex) + 1 for null byte.
     *
     * This will provide us an ample buffer size for generating the string
     * in without having to constantly realloc.
     */
    UINT32 pop_cnt = tpm2_util_pop_count(attrs);

    size_t i;
    size_t max_name_len = 0;
    for (i = 0; i < size; i++) {
        dispatch_table *d = &table[i];
        size_t name_len = strlen(d->name);
        max_name_len = name_len > max_name_len ? name_len : max_name_len;
    }

    size_t length = pop_cnt * max_name_len + pop_cnt - 1 + 3;

    char *str = calloc(length, 1);
    if (!str) {
        return NULL;
    }

    size_t string_index = 0;

    /*
     * Start at the lowest, first bit set, index into the array,
     * grab the data needed, and move on.
     */
    while (attrs) {
        UINT8 bit_index = find_first_set(attrs);

        dispatch_table *d = &table[bit_index];

        const char *name = d->name;
        unsigned w = d->width;

        /* current position and size left of the string */
        char *s = &str[string_index];
        size_t left = length - string_index;

        /* this is a mask that is field width wide */
        UINT8 mask = ((UINT32) 1 << w) - 1;

        /* get the value in the field before clearing attrs out */
        UINT8 field_values = (attrs & mask << bit_index) >> bit_index;

        /*
         * turn off the parsed bit(s) index, using width to turn off everything in a
         * field
         */
        attrs &= ~(mask << bit_index);

        int n;
        /*
         * if the callback is NULL, we are either in a field middle or reserved
         * section which is weird, just add the name in. In the case of being
         * in the middle of the field, we will add a bunch of errors to the string,
         * but it would be better to attempt to show all the data in string form,
         * rather than bail.
         *
         * Fields are either 1 or > 1.
         */
        if (w == 1) {
            n = snprintf(s, left, attrs ? "%s|" : "%s", name);
        } else {
            /* deal with the field */
            n = snprintf(s, left, attrs ? "%s=0x%X|" : "%s=0x%X", name,
                    field_values);
        }

        /* check if there was enough space left in s */
        if ((n < 0) || ((unsigned) n >= left)) {
            break;
        }
        string_index += n;
    }

    return str;
}

char *tpm2_attr_util_nv_attrtostr(TPMA_NV nvattrs) {
    return tpm2_attr_util_common_attrtostr(nvattrs, nv_attr_table,
            ARRAY_LEN(nv_attr_table));
}

char *tpm2_attr_util_obj_attrtostr(TPMA_OBJECT objattrs) {
    return tpm2_attr_util_common_attrtostr(objattrs, obj_attr_table,
            ARRAY_LEN(obj_attr_table));
}

bool tpm2_attr_util_obj_from_optarg(char *argvalue, TPMA_OBJECT *objattrs) {

    bool res = tpm2_util_string_to_uint32(argvalue, objattrs);
    if (!res) {
        res = tpm2_attr_util_obj_strtoattr(argvalue, objattrs);
    }

    return res;
}
