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

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

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

#define LOGMODULE test
#include "tss2_sys.h"
#include "tss2_tcti.h"
#include "tss2/tss2_tcti_spi_helper.h"
#include "util/log.h"
#include "test.h"

typedef struct fuzz_user_data fuzz_user_data;
struct fuzz_user_data {
    size_t len;
    const uint8_t *fuzz_data;
};

typedef enum {
    TPM_DID_VID_HEAD = 0,
    TPM_DID_VID_BODY,
    TPM_ACCESS_HEAD,
    TPM_ACCESS_BODY,
    TPM_STS_HEAD,
    TPM_STS_BODY,
    TPM_RID_HEAD,
    TPM_RID_BODY
} tpm_state_t;

static const unsigned char TPM_DID_VID_0[] = {0x83, 0xd4, 0x0f, 0x00, 0xd1, 0x15, 0x1b, 0x00};
static const unsigned char TPM_ACCESS_0[] = {0x80, 0xd4, 0x00, 0x00, 0xa1};
static const unsigned char TPM_STS_0[] = {0x83, 0xd4, 0x00, 0x18, 0x40, 0x00, 0x00, 0x00};
static const unsigned char TPM_RID_0[] = {0x80, 0xd4, 0x0f, 0x04, 0x00};

static bool is_init = true;

TSS2_RC platform_sleep_ms (void* user_data, int32_t milliseconds)
{
    UNUSED(user_data);
    UNUSED(milliseconds);
    return TSS2_RC_SUCCESS;
}

TSS2_RC platform_start_timeout (void* user_data, int32_t milliseconds)
{
    UNUSED(user_data);
    UNUSED(milliseconds);
    return TSS2_RC_SUCCESS;
}

TSS2_RC platform_timeout_expired (void* user_data, bool *is_timeout_expired)
{
    UNUSED(user_data);
    *is_timeout_expired = true;
    return TSS2_RC_SUCCESS;
}

TSS2_RC platform_spi_acquire (void* user_data)
{
    UNUSED(user_data);
    return TSS2_RC_SUCCESS;
}

TSS2_RC platform_spi_release (void* user_data)
{
    UNUSED(user_data);
    return TSS2_RC_SUCCESS;
}

TSS2_RC platform_spi_transfer_with_wait_state (void* user_data, const void *data_out, void *data_in, size_t cnt)
{
    UNUSED(user_data);
    UNUSED(data_out);
    UNUSED(cnt);

    static tpm_state_t tpm_state = TPM_DID_VID_HEAD;

    switch (tpm_state++) {
    case TPM_DID_VID_HEAD:
        memcpy (data_in, TPM_DID_VID_0, 4);
        ((unsigned char *)data_in)[3] |= 0x01;
        break;
    case TPM_DID_VID_BODY:
        memcpy (data_in, TPM_DID_VID_0 + 4, sizeof (TPM_DID_VID_0) - 4);
        break;
    case TPM_ACCESS_HEAD:
        memcpy (data_in, TPM_ACCESS_0, 4);
        ((unsigned char *)data_in)[3] |= 0x01;
        break;
    case TPM_ACCESS_BODY:
        memcpy (data_in, TPM_ACCESS_0 + 4, sizeof (TPM_ACCESS_0) - 4);
        break;
    case TPM_STS_HEAD:
        memcpy (data_in, TPM_STS_0, 4);
        ((unsigned char *)data_in)[3] |= 0x01;
        break;
    case TPM_STS_BODY:
        memcpy (data_in, TPM_STS_0 + 4, sizeof (TPM_STS_0) - 4);
        break;
    case TPM_RID_HEAD:
        memcpy (data_in, TPM_RID_0, 4);
        ((unsigned char *)data_in)[3] |= 0x01;
        break;
    case TPM_RID_BODY:
        memcpy (data_in, TPM_RID_0 + 4, 1);
        break;
    default:
        return TSS2_TCTI_RC_IO_ERROR;
    }

    return TSS2_RC_SUCCESS;
}

TSS2_RC platform_spi_transfer_fuzz (void* user_data, const void *data_out, void *data_in, size_t cnt)
{
    UNUSED(data_out);

    fuzz_user_data *udata = (fuzz_user_data *)user_data;
    /* libfuzzer input might only be 2 bytes, but cnt might be 32 */
    size_t min = cnt > udata->len ? cnt : udata->len;
    memcpy(data_in, udata->fuzz_data, min);

    return TSS2_RC_SUCCESS;
}

TSS2_RC platform_spi_transfer (void* user_data, const void *data_out, void *data_in, size_t cnt)
{

    /*
     * Use if we're past init to start fuzzing
     */
    return is_init ?
            platform_spi_transfer_with_wait_state(user_data, data_out, data_in, cnt) :
            platform_spi_transfer_fuzz(user_data, data_out, data_in, cnt);
}


void platform_finalize(void* user_data)
{
    UNUSED(user_data);
}

int
LLVMFuzzerTestOneInput (
        const uint8_t *fuzz_data,
        size_t size)
{
    if (!fuzz_data) {
        return -1;
    }

    TSS2_TCTI_CONTEXT *tcti_ctx = NULL;
    TSS2_SYS_CONTEXT *sapi_ctx = NULL;

    fuzz_user_data udata = {
        .len = size,
        .fuzz_data = fuzz_data
    };

    TSS2_TCTI_SPI_HELPER_PLATFORM conf = {
        .user_data = &udata,
        .sleep_ms = platform_sleep_ms,
        .start_timeout = platform_start_timeout,
        .timeout_expired = platform_timeout_expired,
        .spi_acquire = platform_spi_acquire,
        .spi_release = platform_spi_release,
        .spi_transfer = platform_spi_transfer,
        .finalize = platform_finalize
    };

    size_t tcti_size = 0;
    TSS2_RC rc = Tss2_Tcti_Spi_Helper_Init (
            NULL, &tcti_size, &conf);
    if (rc != TSS2_RC_SUCCESS) {
        LOG_ERROR("Tss2_Tcti_Spi_Helper_Init failed 0x%x", rc);
        return EXIT_FAILURE;
    }

    tcti_ctx = (TSS2_TCTI_CONTEXT *)calloc(1, tcti_size);
    if (!tcti_ctx) {
        LOG_ERROR("OOM allocating TCTI");
        goto error;
    }

    rc = Tss2_Tcti_Spi_Helper_Init (
            tcti_ctx, &tcti_size, &conf);
    if (rc != TSS2_RC_SUCCESS) {
        LOG_ERROR("Tss2_Tcti_Spi_Helper_Init failed 0x%x", rc);
        goto error;
    }

    is_init = false;

    size_t sapi_size = Tss2_Sys_GetContextSize(0);

    sapi_ctx = (TSS2_SYS_CONTEXT *)calloc(1, sapi_size);
    if (!sapi_ctx) {
        LOG_ERROR("OOM allocating SAPI");
        goto error;
    }

    rc = Tss2_Sys_Initialize(sapi_ctx, sapi_size, tcti_ctx, NULL);
    if (rc != TSS2_RC_SUCCESS) {
        LOG_ERROR("Tss2_Sys_Initialize failed 0x%x", rc);
        goto error;
    }

    /*
     * ignore the RC (we expect jibberish from the TCTI)
     * but this should get the TCTI firing through transfer.
     */
    TPM2B_DIGEST buf = { 0 };
    UNUSED(Tss2_Sys_GetRandom(sapi_ctx, NULL, 4, &buf, NULL));

error:
    Tss2_Sys_Finalize(sapi_ctx);
    free(sapi_ctx);

    Tss2_Tcti_Finalize(tcti_ctx);
    free(tcti_ctx);

    return 0;
}
