// SPDX-License-Identifier: BSD-2-Clause-FreeBSD
//
// Copyright (c) 2021 Tobias Kortkamp <tobik@FreeBSD.org>
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
// 1. Redistributions of source code must retain the above copyright
//    notice, this list of conditions and the following disclaimer.
// 2. Redistributions in binary form must reproduce the above copyright
//    notice, this list of conditions and the following disclaimer in the
//    documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
// OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
// OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
// SUCH DAMAGE.

#include "config.h"

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

#include "distinfo.h"
#include "flow.h"
#include "io.h"
#include "map.h"
#include "mem.h"
#include "mempool.h"
#include "peg.h"
#include "peg/distinfo.h"
#include "stack.h"
#include "str.h"

struct Distinfo {
	time_t timestamp;
	struct Mempool *pool;
	struct Map *map;
	struct Stack *filenames;
};

// Prototypes
static int hex2int(char);
static bool parse_hexstr(const char *, size_t, uint8_t *, size_t *);
static enum PEGCaptureFlag capture_machine(struct PEGCapture *, void *);

int
hex2int(char c)
{
	switch (c) {
	case '0': return 0;
	case '1': return 1;
	case '2': return 2;
	case '3': return 3;
	case '4': return 4;
	case '5': return 5;
	case '6': return 6;
	case '7': return 7;
	case '8': return 8;
	case '9': return 9;
	case 'A': case 'a': return 10;
	case 'B': case 'b': return 11;
	case 'C': case 'c': return 12;
	case 'D': case 'd': return 13;
	case 'E': case 'e': return 14;
	case 'F': case 'f': return 15;
	default: return -1;
	}
}

bool
parse_hexstr(const char *buf, size_t buflen, uint8_t *outbuf, size_t *outlen)
{
	size_t i = 0;
	size_t j = 0;
	while (i < buflen && j < DISTINFO_MAX_DIGEST_LEN) {
		int c = hex2int(buf[i++]);
		if (c == -1) {
			return false;
		}
		if (i >= buflen) {
			return false;
		}
		int d = hex2int(buf[i++]);
		if (d == -1) {
			return false;
		}
		outbuf[j++] = (uint8_t)((c << 4) | d);
	}
	if (i == buflen) {
		*outlen = j;
		return true;
	} else {
		return false;
	}
}

enum PEGCaptureFlag
capture_machine(struct PEGCapture *capture, void *userdata)
{
	SCOPE_MEMPOOL(pool);
	struct Distinfo *this = userdata;
	switch ((enum PEGDistinfoCaptureState)capture->state) {
	case PEG_DISTINFO_ACCEPT:
		break;
	case PEG_DISTINFO_DIGEST: {
		struct PEGCapture *c = stack_peek(this->filenames);
		const char *filename = str_ndup(this->pool, c->buf, c->len);
		struct DistinfoEntry *e = map_get(this->map, filename);
		unless (e) {
			e = mempool_alloc(this->pool, sizeof(struct DistinfoEntry));
			e->filename = filename;
			e->size = -1;
			map_add(this->map, filename, e);
		}
		unless (parse_hexstr(capture->buf, capture->len, e->digest, &e->digest_len)) {
			return PEG_CAPTURE_STOP;
		}
		break;
	} case PEG_DISTINFO_FILENAME:
		stack_push(this->filenames, capture);
		break;
	case PEG_DISTINFO_SIZE: {
		struct PEGCapture *c = stack_peek(this->filenames);
		const char *filename = str_ndup(this->pool, c->buf, c->len);
		struct DistinfoEntry *e = map_get(this->map, filename);
		unless (e) {
			e = mempool_alloc(this->pool, sizeof(struct DistinfoEntry));
			e->filename = filename;
			e->size = -1;
			map_add(this->map, filename, e);
		}
		const char *error = NULL;
		const char *size = str_ndup(pool, capture->buf, capture->len);
		e->size = strtonum(size, 0, INT64_MAX, &error);
		if (error) {
			return PEG_CAPTURE_STOP;
		}
		break;
	} case PEG_DISTINFO_TIMESTAMP: {
		const char *error = NULL;
		const char *timestamp = str_ndup(pool, capture->buf, capture->len);
		this->timestamp = strtonum(timestamp, 0, INT64_MAX, &error);
		if (error) {
			return PEG_CAPTURE_STOP;
		}
		break;
	} case PEG_DISTINFO_CHECKSUM_ALGORITHM:
		break;
	}
	return PEG_CAPTURE_CONTINUE;
}

struct Distinfo *
distinfo_parse(FILE *f, struct Mempool *errpool, struct Array **errors)
{
	SCOPE_MEMPOOL(pool);

	const char *s = slurp(f, pool);
	unless (s) {
		return NULL;
	}

	struct Distinfo *this = distinfo_new();
	this->filenames = mempool_stack(pool);
	struct PEG *peg = mempool_add(pool, peg_new(s, strlen(s)), peg_free);
	if (peg_match(peg, peg_distinfo_decode, capture_machine, this)) {
		this->filenames = NULL;
		return this;
	} else {
		if (errors) {
			*errors = peg_backtrace(peg, errpool);
		}
		distinfo_free(this);
		return NULL;
	}
}

struct Distinfo *
distinfo_new()
{
	struct Distinfo *this = xmalloc(sizeof(struct Distinfo));
	this->pool = mempool_new();
	this->map = mempool_map(this->pool, str_compare);
	return this;
}

void
distinfo_free(struct Distinfo *this)
{
	if (this) {
		mempool_free(this->pool);
		free(this);
	}
}

struct Array *
distinfo_entries(struct Distinfo *this, struct Mempool *pool)
{
	return map_values(this->map, pool);
}

struct DistinfoEntry *
distinfo_entry(struct Distinfo *this, const char *filename)
{
	return map_get(this->map, filename);
}

void
distinfo_add_entry(struct Distinfo *this, struct DistinfoEntry *e)
{
	panic_if(e->digest_len > DISTINFO_MAX_DIGEST_LEN, "digest_len > DISTINFO_MAX_DIGEST_LEN");
	panic_unless(e->filename, "adding distinfo entry without filename");
	unless (map_contains(this->map, e->filename)) {
		struct DistinfoEntry *entry = mempool_alloc(this->pool, sizeof(struct DistinfoEntry));
		entry->size = e->size;
		entry->filename = str_dup(this->pool, e->filename);
		if (e->digest_len > 0) {
			memcpy(entry->digest, e->digest, e->digest_len);
			entry->digest_len = e->digest_len;
		}
		map_add(this->map, entry->filename, entry);
	}
}

void
distinfo_remove_entry(struct Distinfo *this, const char *filename)
{
	map_remove(this->map, filename);
}

time_t
distinfo_timestamp(struct Distinfo *this)
{
	return this->timestamp;
}

void
distinfo_set_timestamp(struct Distinfo *this, time_t timestamp)
{
	this->timestamp = timestamp;
}

void
distinfo_entry_serialize(struct DistinfoEntry *entry, FILE *f)
{
	panic_if(entry->digest_len > DISTINFO_MAX_DIGEST_LEN, "digest_len > DISTINFO_MAX_DIGEST_LEN");
	fprintf(f, "SHA256 (%s) = ", entry->filename);
	for (size_t i = 0; i < entry->digest_len; i++) {
		fprintf(f, "%02x", entry->digest[i]);
	}
	fputs("\n", f);
	if (entry->size >= 0) {
		fprintf(f, "SIZE (%s) = %lld\n", entry->filename, (long long)entry->size);
	}
}

void
distinfo_serialize(struct Distinfo *this, FILE *f)
{
	if (this->timestamp > 0) {
		fprintf(f, "TIMESTAMP = %ju\n", (uintmax_t)this->timestamp);
	}
	MAP_FOREACH(this->map, const char *, filename, struct DistinfoEntry *, entry) {
		distinfo_entry_serialize(entry, f);
	}
}
