/*
 * This file and its contents are supplied under the terms of the
 * Common Development and Distribution License (), version 1.0.
 * You may only use this file in accordance with the terms of version
 * 1.0 of the CDDL.
 *
 * A full copy of the text of the CDDL should have accompanied this
 * source.  A copy of the CDDL is also available via the Internet at
 * http://www.illumos.org/license/CDDL.
 */

/*
 * Copyright 2019 Joyent, Inc.
 * Copyright 2020 Oxide Computer Company
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <errno.h>
#include <err.h>

#include <sys/debug.h>

#include "cryptotest.h"
#include "parser_runner.h"

#define	DATA_PATH	"/opt/crypto-tests/share"

/*
 * Parse NIST test vector data into a format that is simple to run the digest
 * tests against.  The parsing logic is not meant to be especially robust given
 * that we control the data fed into it.
 */

struct parser_ctx {
	FILE	*pc_file;
	size_t	pc_hash_sz;
	char	*pc_line_buf;
	size_t	pc_line_sz;
};

parser_ctx_t *
parser_init(const char *path, size_t hash_len, int *errp)
{
	FILE *fp;
	parser_ctx_t *ctx;

	/* sanity check for SHA1 -> SHA512 */
	ASSERT(hash_len >= 20 && hash_len <= 64);

	fp = fopen(path, "r");
	if (fp == NULL) {
		*errp = errno;
		return (NULL);
	}
	ctx = malloc(sizeof (*ctx));
	if (ctx == NULL) {
		*errp = ENOMEM;
		(void) fclose(fp);
		return (NULL);
	}
	ctx->pc_file = fp;
	ctx->pc_hash_sz = hash_len;
	ctx->pc_line_buf = NULL;
	ctx->pc_line_sz = 0;

	return (ctx);
}

void
parser_fini(parser_ctx_t *ctx)
{
	free(ctx->pc_line_buf);
	(void) fclose(ctx->pc_file);
	free(ctx);
}

static size_t
hex2bytes(const char *hexbuf, size_t hexlen, uchar_t *outbuf, size_t outlen)
{
	size_t count = 0;
	/* naive and lazy conversion */
	errno = 0;
	while (hexlen > 1) {
		long res;
		char buf[3] = {hexbuf[0], hexbuf[1], '\0'};

		res = strtol(buf, NULL, 16);
		if (errno != 0) {
			break;
		}
		*outbuf = res & 0xff;

		hexbuf += 2;
		hexlen -= 2;
		outbuf += 1;
		outlen += 1;
		count++;

		if (outbuf == 0) {
			break;
		}
	}

	return (count);
}

static int
read_len(parser_ctx_t *ctx, size_t *lenp, size_t *szp)
{
	ssize_t sz;
	long parsed;
	const char *search = "Len = ";
	const size_t search_len = strlen(search);

	errno = 0;
	sz = getline(&ctx->pc_line_buf, &ctx->pc_line_sz, ctx->pc_file);
	if (sz < 1) {
		int err = errno;

		if (err == 0 || err == ENOENT) {
			/* EOF reached, bail */
			return (-1);
		} else {
			return (err);
		}
	}
	*szp = sz;
	if (strncmp(ctx->pc_line_buf, search, search_len) != 0) {
		return (-1);
	}

	errno = 0;
	parsed = strtol(ctx->pc_line_buf + search_len, NULL, 10);
	if (parsed == 0 && errno != 0) {
		return (errno);
	}
	if (parsed < 0) {
		return (EINVAL);
	}

	/* length in file is in bits, while we want bytes */
	*lenp = (size_t)parsed / 8;
	return (0);
}

static int
read_msg(parser_ctx_t *ctx, uchar_t *msgbuf, size_t msglen)
{
	ssize_t sz;
	const char *search = "Msg = ";
	const size_t search_len = strlen(search);

	sz = getline(&ctx->pc_line_buf, &ctx->pc_line_sz, ctx->pc_file);
	if (sz < 0) {
		return (errno);
	}
	if (strncmp(ctx->pc_line_buf, search, search_len) != 0) {
		return (-1);
	}

	if (msgbuf == NULL) {
		ASSERT(msglen == 0);
		return (0);
	}

	size_t parsed;
	parsed = hex2bytes(ctx->pc_line_buf + search_len, sz - search_len,
	    msgbuf, msglen);
	if (parsed != msglen) {
		ASSERT3U(parsed, <, msglen);
		return (-1);
	}

	return (0);
}

static int
read_md(parser_ctx_t *ctx, uchar_t *mdbuf, size_t mdlen)
{
	ssize_t sz;
	const char *search = "MD = ";
	const size_t search_len = strlen(search);

	sz = getline(&ctx->pc_line_buf, &ctx->pc_line_sz, ctx->pc_file);
	if (sz < 0) {
		return (errno);
	}
	if (strncmp(ctx->pc_line_buf, search, search_len) != 0) {
		return (-1);
	}

	size_t parsed;
	parsed = hex2bytes(ctx->pc_line_buf + search_len, sz - search_len,
	    mdbuf, mdlen);
	if (parsed != mdlen) {
		ASSERT3U(parsed, <, mdlen);
		return (-1);
	}

	return (0);
}

parser_entry_t *
parser_read(parser_ctx_t *ctx, int *errp)
{
	int err = 0;
	parser_entry_t *res = NULL;
	uchar_t *msgbuf = NULL;
	uchar_t *mdbuf = NULL;

	while (feof(ctx->pc_file) == 0) {
		int ret;
		size_t msglen, sz;

		ret = read_len(ctx, &msglen, &sz);
		if (ret == -1) {
			/*
			 * Did not find a properly formatted "Len = <num>", but
			 * no hard errors were incurred while looking for one,
			 * so continue searching.
			 */
			continue;
		} else if (ret != 0) {
			err = ret;
			break;
		}

		if (msglen != 0) {
			msgbuf = calloc(msglen, 1);
			if (msgbuf == NULL) {
				err = ENOMEM;
				break;
			}
		}

		ret = read_msg(ctx, msgbuf, msglen);
		if (ret == -1) {
			/*
			 * Did not find properly formatted "Msg = <hex data>".
			 * Restart the search for a new record.
			 */
			free(msgbuf);
			msgbuf = NULL;
			continue;
		} else if (ret != 0) {
			err = ret;
			break;
		}

		mdbuf = calloc(1, ctx->pc_hash_sz);
		if (mdbuf == NULL) {
			err = ENOMEM;
			break;
		}
		ret = read_md(ctx, mdbuf, ctx->pc_hash_sz);
		if (ret == -1) {
			/*
			 * Did not find properly formatted "MD = <hash>".
			 * Restart search for new record.
			 */
			free(msgbuf);
			free(mdbuf);
			msgbuf = mdbuf = NULL;
			continue;
		} else if (ret != 0) {
			err = ret;
			break;
		}

		res = malloc(sizeof (*res));
		if (res == NULL) {
			err = ENOMEM;
			break;
		}
		res->pe_msg = msgbuf;
		res->pe_msglen = msglen;
		res->pe_hash = mdbuf;
		break;
	}

	if (err != 0) {
		ASSERT(res == NULL);
		free(msgbuf);
		free(mdbuf);
	}

	/* EOF status indicated by err == 0 and res == NULL */
	*errp = err;
	return (res);
}

void
parser_free(parser_entry_t *ent)
{
	free(ent->pe_msg);
	free(ent->pe_hash);
	free(ent);
}

/*
 * With the above parser, run a the vectors through a given crypto test.
 */
int
digest_runner(char *mech_name, const char *input_file, size_t digest_len)
{
	int fails = 0, error;
	uint8_t N[1024];
	size_t updatelens[] = {
		1, 8, 33, 67, CTEST_UPDATELEN_WHOLE, CTEST_UPDATELEN_END
	};
	cryptotest_t args = {
		.out = N,
		.outlen = sizeof (N),
		.mechname = mech_name,
		.updatelens = updatelens
	};
	parser_ctx_t *ctx;
	parser_entry_t *ent;

	/*
	 * XXX: This could be changed to generate a path relative to that of
	 * the executable to find the data files
	 */
	char *path = NULL;
	if (asprintf(&path, "%s/%s", DATA_PATH, input_file) < 0) {
		err(EXIT_FAILURE, NULL);
	}

	ctx = parser_init(path, digest_len, &error);
	if (ctx == NULL) {
		err(EXIT_FAILURE, "%s", path);
	}
	free(path);

	error = 0;
	while ((ent = parser_read(ctx, &error)) != NULL) {
		args.in = ent->pe_msg;
		args.inlen = ent->pe_msglen;

		fails += run_test(&args, ent->pe_hash, digest_len, DIGEST_FG);
		parser_free(ent);
	}
	if (error != 0) {
		err(EXIT_FAILURE, NULL);
	}
	parser_fini(ctx);

	return (fails);
}