/*
 * SPDX-License-Identifier: ISC
 *
 * Copyright (c) 2020 Todd C. Miller <Todd.Miller@sudo.ws>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#include <config.h>

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>
#include <unistd.h>

#define SUDO_ERROR_WRAP 0

#include "sudo_compat.h"
#include "sudo_util.h"
#include "sudo_fatal.h"

#include "iolog_json.h"

sudo_dso_public int main(int argc, char *argv[]);

static bool
json_print_object(struct json_container *jsonc, struct json_object *object)
{
    struct json_item *item;
    struct json_value json_value;
    bool ret = false;

    TAILQ_FOREACH(item, &object->items, entries) {
	switch (item->type) {
	case JSON_STRING:
	    json_value.type = JSON_STRING;
	    json_value.u.string = item->u.string;
	    if (!sudo_json_add_value(jsonc, item->name, &json_value))
		goto oom;
	    break;
	case JSON_NUMBER:
	    json_value.type = JSON_NUMBER;
	    json_value.u.number = item->u.number;
	    if (!sudo_json_add_value(jsonc, item->name, &json_value))
		goto oom;
	    break;
	case JSON_OBJECT:
	    if (!sudo_json_open_object(jsonc, item->name))
		goto oom;
	    if (!json_print_object(jsonc, &item->u.child))
		goto done;
	    if (!sudo_json_close_object(jsonc))
		goto oom;
	    break;
	case JSON_ARRAY:
	    if (!sudo_json_open_array(jsonc, item->name))
		goto oom;
	    if (!json_print_object(jsonc, &item->u.child))
		goto done;
	    if (!sudo_json_close_array(jsonc))
		goto oom;
	    break;
	case JSON_BOOL:
	    json_value.type = JSON_BOOL;
	    json_value.u.boolean = item->u.boolean;
	    if (!sudo_json_add_value(jsonc, item->name, &json_value))
		goto oom;
	    break;
	case JSON_NULL:
	    json_value.type = JSON_NULL;
	    if (!sudo_json_add_value(jsonc, item->name, &json_value))
		goto oom;
	    break;
	default:
	    sudo_warnx("unsupported JSON type %d", item->type);
	    goto done;
	}
    }

    ret = true;
    goto done;

oom:
    sudo_warnx("%s: %s", __func__, "unable to allocate memory");
done:
    return ret;
}

static bool
json_format(struct json_container *jsonc, struct json_object *object)
{
    struct json_item *item;
    bool ret = false;

    /* First object holds all the actual data. */
    item = TAILQ_FIRST(&object->items);
    if (item->type != JSON_OBJECT) {
	sudo_warnx("expected JSON_OBJECT, got %d", item->type);
	goto done;
    }
    object = &item->u.child;

    if (!json_print_object(jsonc, object))
	goto done;

    ret = true;

done:
    return ret;
}

static void
usage(void)
{
    fprintf(stderr, "usage: %s [-c] input_file ...\n",
	getprogname());
    exit(EXIT_FAILURE);
}

static bool
compare(FILE *fp, const char *infile, struct json_container *jsonc)
{
    const char *cp;
    unsigned int lineno = 0;
    size_t linesize = 0;
    char *line = NULL;
    ssize_t len;

    cp = sudo_json_get_buf(jsonc);

    while ((len = getdelim(&line, &linesize, '\n', fp)) != -1) {
	lineno++;

	/* skip open/close brace, not present in formatted output */
	if (lineno == 1 && strcmp(line, "{\n") == 0)
	    continue;
	if (*cp == '\0' && strcmp(line, "}\n") == 0)
	    continue;

	/* Ignore newlines in output to make comparison easier. */
	if (*cp == '\n')
	    cp++;
	if (line[len - 1] == '\n')
	    len--;

	if (strncmp(line, cp, len) != 0) {
	    fprintf(stderr, "%s: mismatch on line %u\n", infile, lineno);
	    fprintf(stderr, "expected: %s", line);
	    fprintf(stderr, "got     : %.*s\n", (int)len, cp);
	    return false;
	}
	cp += len;
    }
    free(line);

    return true;
}

int
main(int argc, char *argv[])
{
    struct json_object root;
    int ch, i, ntests = 0, errors = 0;
    bool cat = false;

    initprogname(argc > 0 ? argv[0] : "check_iolog_json");

    while ((ch = getopt(argc, argv, "c")) != -1) {
	switch (ch) {
	    case 'c':
		cat = true;
		break;
	    default:
		usage();
	}
    }
    argc -= optind;
    argv += optind;

    if (argc < 1)
	usage();

    for (i = 0; i < argc; i++) {
	struct json_container jsonc;
	const char *infile = argv[i];
	const char *outfile = argv[i];
	const char *cp;
	char pathbuf[PATH_MAX];
	FILE *infp = NULL;
	FILE *outfp = NULL;

	ntests++;

	if (!sudo_json_init(&jsonc, 4, false, true, true)) {
	    errors++;
	    continue;
	}

	/* Parse input file. */
	if ((infp = fopen(infile, "r")) == NULL) {
	    sudo_warn("%s", argv[i]);
	    errors++;
	    continue;
	}
	if (!iolog_parse_json(infp, infile, &root)) {
	    errors++;
	    goto next;
	}

	/* Format as pretty-printed JSON */
	if (!json_format(&jsonc, &root)) {
	    errors++;
	    goto next;
	}

	/* Check for a .out.ok file in the same location as the .in file. */
	cp = strrchr(infile, '.');
	if (cp != NULL && strcmp(cp, ".in") == 0) {
	    snprintf(pathbuf, sizeof(pathbuf), "%.*s.out.ok",
		(int)(cp - infile), infile);
	    if ((outfp = fopen(pathbuf, "r")) != NULL)
		outfile = pathbuf;
	}
	if (outfp == NULL)
	    outfp = infp;

	/* Compare output to expected output. */
	rewind(outfp);
	if (!compare(outfp, outfile, &jsonc))
	    errors++;

	/* Write the formatted output to stdout for -c (cat) */
	if (cat) {
	    fprintf(stdout, "{%s\n}\n", sudo_json_get_buf(&jsonc));
	    fflush(stdout);
	}

next:
	free_json_items(&root.items);
	sudo_json_free(&jsonc);
	if (infp != NULL)
	    fclose(infp);
	if (outfp != NULL && outfp != infp)
	    fclose(outfp);
    }

    if (ntests != 0) {
	printf("iolog_json: %d test%s run, %d errors, %d%% success rate\n",
	    ntests, ntests == 1 ? "" : "s", errors,
	    (ntests - errors) * 100 / ntests);
    }

    return errors;
}
