/* $NetBSD: tic.c,v 1.42 2024/05/20 14:41:37 christos Exp $ */

/*
 * Copyright (c) 2009, 2010, 2020 The NetBSD Foundation, Inc.
 *
 * This code is derived from software contributed to The NetBSD Foundation
 * by Roy Marples.
 *
 * 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 ``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 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.
 */

#if HAVE_NBTOOL_CONFIG_H
#include "nbtool_config.h"
#endif

#include <sys/cdefs.h>
__RCSID("$NetBSD: tic.c,v 1.42 2024/05/20 14:41:37 christos Exp $");

#include <sys/types.h>
#include <sys/queue.h>
#include <sys/stat.h>

#if !HAVE_NBTOOL_CONFIG_H || HAVE_SYS_ENDIAN_H
#include <sys/endian.h>
#endif

#include <cdbw.h>
#include <ctype.h>
#include <err.h>
#include <errno.h>
#include <getopt.h>
#include <limits.h>
#include <fcntl.h>
#include <search.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <term_private.h>
#include <term.h>
#include <unistd.h>
#include <util.h>

#define	HASH_SIZE	16384	/* 2012-06-01: 3600 entries */

typedef struct term {
	STAILQ_ENTRY(term) next;
	char *name;
	TIC *tic;
	uint32_t id;
	struct term *base_term;
} TERM;
static STAILQ_HEAD(, term) terms = STAILQ_HEAD_INITIALIZER(terms);

static int error_exit;
static int Sflag;
static size_t nterm, nalias;

static void __printflike(1, 2)
dowarn(const char *fmt, ...)
{
	va_list va;

	error_exit = 1;
	va_start(va, fmt);
	vwarnx(fmt, va);
	va_end(va);
}

static char *
grow_tbuf(TBUF *tbuf, size_t len)
{
	char *buf;

	buf = _ti_grow_tbuf(tbuf, len);
	if (buf == NULL)
		err(EXIT_FAILURE, "_ti_grow_tbuf");
	return buf;
}

static int
save_term(struct cdbw *db, TERM *term)
{
	uint8_t *buf;
	ssize_t len;
	size_t slen = strlen(term->name) + 1;

	if (term->base_term != NULL) {
		char *cap;
		len = (ssize_t)(1 + sizeof(uint32_t) + sizeof(uint16_t) + slen);
		buf = emalloc(len);
		cap = (char *)buf;
		*cap++ = TERMINFO_ALIAS;
		_ti_encode_32(&cap, term->base_term->id);
		_ti_encode_count_str(&cap, term->name, slen);
		if (cdbw_put(db, term->name, slen, buf, len))
			err(EXIT_FAILURE, "cdbw_put");
		free(buf);
		return 0;
	}

	len = _ti_flatten(&buf, term->tic);
	if (len == -1)
		return -1;

	if (cdbw_put_data(db, buf, len, &term->id))
		err(EXIT_FAILURE, "cdbw_put_data");
	if (cdbw_put_key(db, term->name, slen, term->id))
		err(EXIT_FAILURE, "cdbw_put_key");
	free(buf);
	return 0;
}

static TERM *
find_term(const char *name)
{
	ENTRY elem, *elemp;

	elem.key = __UNCONST(name);
	elem.data = NULL;
	elemp = hsearch(elem, FIND);
	return elemp ? (TERM *)elemp->data : NULL;
}

static TERM *
find_newest_term(const char *name)
{
	char *lname;
	TERM *term;

	lname = _ti_getname(TERMINFO_RTYPE, name);
	if (lname == NULL)
		return NULL;
	term = find_term(lname);
	free(lname);
	if (term == NULL)
		term = find_term(name);
	return term;
}

static TERM *
store_term(const char *name, TERM *base_term)
{
	TERM *term;
	ENTRY elem;

	term = ecalloc(1, sizeof(*term));
	term->name = estrdup(name);
	STAILQ_INSERT_TAIL(&terms, term, next);
	elem.key = estrdup(name);
	elem.data = term;
	hsearch(elem, ENTER);

	term->base_term = base_term;
	if (base_term != NULL)
		nalias++;
	else
		nterm++;

	return term;
}

static void
alias_terms(TERM *term)
{
	char *p, *e, *alias;

	/* Create aliased terms */
	if (term->tic->alias == NULL)
		return;

	alias = p = estrdup(term->tic->alias);
	while (p != NULL && *p != '\0') {
		e = strchr(p, '|');
		if (e != NULL)
			*e++ = '\0';
		/* No need to lengthcheck the alias because the main
		 * terminfo description already stores all the aliases
		 * in the same length field as the alias. */
		if (find_term(p) != NULL) {
			dowarn("%s: has alias for already assigned"
			    " term %s", term->tic->name, p);
		} else {
			store_term(p, term);
		}
		p = e;
	}
	free(alias);
}

static int
process_entry(TBUF *buf, int flags)
{
	TERM *term;
	TIC *tic;
	TBUF sbuf = *buf;

	if (buf->bufpos == 0)
		return 0;
	/* Terminate the string */
	buf->buf[buf->bufpos - 1] = '\0';
	/* First rewind the buffer for new entries */
	buf->bufpos = 0;

	if (isspace((unsigned char)*buf->buf))
		return 0;

	tic = _ti_compile(buf->buf, flags);
	if (tic == NULL)
		return 0;

	if (find_term(tic->name) != NULL) {
		dowarn("%s: duplicate entry", tic->name);
		_ti_freetic(tic);
		return 0;
	}
	term = store_term(tic->name, NULL);
	term->tic = tic;
	alias_terms(term);

	if (tic->rtype == TERMINFO_RTYPE)
		return process_entry(&sbuf, flags | TIC_COMPAT_V1);

	return 0;
}

static void
merge(TIC *rtic, TIC *utic, int flags)
{
	char flag, type;
	const char *cap, *code, *str;
	short ind, len;
	int num;
	size_t n;

	if (rtic->rtype < utic->rtype)
		errx(EXIT_FAILURE, "merge rtype diff (%s:%d into %s:%d)",
		    utic->name, utic->rtype, rtic->name, rtic->rtype);

	cap = utic->flags.buf;
	for (n = utic->flags.entries; n > 0; n--) {
		ind = _ti_decode_16(&cap);
		flag = *cap++;
		if (VALID_BOOLEAN(flag) &&
		    _ti_find_cap(rtic, &rtic->flags, 'f', ind) == NULL)
		{
			if (!_ti_encode_buf_id_flags(&rtic->flags, ind, flag))
				err(EXIT_FAILURE, "encode flag");
		}
	}

	cap = utic->nums.buf;
	for (n = utic->nums.entries; n > 0; n--) {
		ind = _ti_decode_16(&cap);
		num = _ti_decode_num(&cap, utic->rtype);
		if (VALID_NUMERIC(num) &&
		    _ti_find_cap(rtic, &rtic->nums, 'n', ind) == NULL)
		{
			if (!_ti_encode_buf_id_num(&rtic->nums, ind, num,
			    _ti_numsize(rtic)))
				err(EXIT_FAILURE, "encode num");
		}
	}

	cap = utic->strs.buf;
	for (n = utic->strs.entries; n > 0; n--) {
		ind = _ti_decode_16(&cap);
		len = _ti_decode_16(&cap);
		if (len > 0 &&
		    _ti_find_cap(rtic, &rtic->strs, 's', ind) == NULL)
		{
			if (!_ti_encode_buf_id_count_str(&rtic->strs, ind, cap,
			    len))
				err(EXIT_FAILURE, "encode str");
		}
		cap += len;
	}

	cap = utic->extras.buf;
	for (n = utic->extras.entries; n > 0; n--) {
		num = _ti_decode_16(&cap);
		code = cap;
		cap += num;
		type = *cap++;
		flag = 0;
		str = NULL;
		switch (type) {
		case 'f':
			flag = *cap++;
			if (!VALID_BOOLEAN(flag))
				continue;
			break;
		case 'n':
			num = _ti_decode_num(&cap, utic->rtype);
			if (!VALID_NUMERIC(num))
				continue;
			break;
		case 's':
			num = _ti_decode_16(&cap);
			str = cap;
			cap += num;
			if (num == 0)
				continue;
			break;
		}
		_ti_store_extra(rtic, 0, code, type, flag, num, str, num,
		    flags);
	}
}

static int
dup_tbuf(TBUF *dst, const TBUF *src)
{

	if (src->buflen == 0)
		return 0;
	dst->buf = malloc(src->buflen);
	if (dst->buf == NULL)
		return -1;
	dst->buflen = src->buflen;
	memcpy(dst->buf, src->buf, dst->buflen);
	dst->bufpos = src->bufpos;
	dst->entries = src->entries;
	return 0;
}

static int
promote(TIC *rtic, TIC *utic)
{
	TERM *nrterm = find_newest_term(rtic->name);
	TERM *nuterm = find_newest_term(utic->name);
	TERM *term;
	TIC *tic;

	if (nrterm == NULL || nuterm == NULL)
		return -1;
	if (nrterm->tic->rtype >= nuterm->tic->rtype)
		return 0;

	tic = calloc(1, sizeof(*tic));
	if (tic == NULL)
		return -1;

	tic->name = _ti_getname(TERMINFO_RTYPE, rtic->name);
	if (tic->name == NULL)
		goto err;
	if (rtic->alias != NULL) {
		tic->alias = strdup(rtic->alias);
		if (tic->alias == NULL)
			goto err;
	}
	if (rtic->desc != NULL) {
		tic->desc = strdup(rtic->desc);
		if (tic->desc == NULL)
			goto err;
	}

	tic->rtype = rtic->rtype;
	if (dup_tbuf(&tic->flags, &rtic->flags) == -1)
		goto err;
	if (dup_tbuf(&tic->nums, &rtic->nums) == -1)
		goto err;
	if (dup_tbuf(&tic->strs, &rtic->strs) == -1)
		goto err;
	if (dup_tbuf(&tic->extras, &rtic->extras) == -1)
		goto err;
	if (_ti_promote(tic) == -1)
		goto err;

	term = store_term(tic->name, NULL);
	if (term == NULL)
		goto err;

	term->tic = tic;
	alias_terms(term);
	return 0;

err:
	free(tic->flags.buf);
	free(tic->nums.buf);
	free(tic->strs.buf);
	free(tic->extras.buf);
	free(tic->desc);
	free(tic->alias);
	free(tic->name);
	free(tic);
	return -1;
}

static size_t
merge_use(int flags)
{
	size_t skipped, merged, memn;
	const char *cap;
	char *name, *basename;
	uint16_t num;
	TIC *rtic, *utic;
	TERM *term, *uterm;
	bool promoted;

	skipped = merged = 0;
	STAILQ_FOREACH(term, &terms, next) {
		if (term->base_term != NULL)
			continue;
		rtic = term->tic;
		basename = _ti_getname(TERMINFO_RTYPE_O1, rtic->name);
		promoted = false;
		while ((cap = _ti_find_extra(rtic, &rtic->extras, "use"))
		    != NULL) {
			if (*cap++ != 's') {
				dowarn("%s: use is not string", rtic->name);
				break;
			}
			cap += sizeof(uint16_t);
			if (strcmp(basename, cap) == 0) {
				dowarn("%s: uses itself", rtic->name);
				goto remove;
			}
			name = _ti_getname(rtic->rtype, cap);
			if (name == NULL) {
				dowarn("%s: ???: %s", rtic->name, cap);
				goto remove;
			}
			uterm = find_term(name);
			free(name);
			if (uterm == NULL)
				uterm = find_term(cap);
			if (uterm != NULL && uterm->base_term != NULL)
				uterm = uterm->base_term;
			if (uterm == NULL) {
				dowarn("%s: no use record for %s",
				    rtic->name, cap);
				goto remove;
			}
			utic = uterm->tic;
			if (strcmp(utic->name, rtic->name) == 0) {
				dowarn("%s: uses itself", rtic->name);
				goto remove;
			}
			if (_ti_find_extra(utic, &utic->extras, "use")
			    != NULL) {
				skipped++;
				break;
			}

			/* If we need to merge in a term that requires
			 * this term to be promoted, we need to duplicate
			 * this term, promote it and append it to our list. */
			if (!promoted && rtic->rtype != TERMINFO_RTYPE) {
				if (promote(rtic, utic) == -1)
					err(EXIT_FAILURE, "promote");
				promoted = rtic->rtype == TERMINFO_RTYPE;
			}

			merge(rtic, utic, flags);
	remove:
			/* The pointers may have changed, find the use again */
			cap = _ti_find_extra(rtic, &rtic->extras, "use");
			if (cap == NULL)
				dowarn("%s: use no longer exists - impossible",
					rtic->name);
			else {
				char *scap = __UNCONST(
				    cap - (4 + sizeof(uint16_t)));
				cap++;
				num = _ti_decode_16(&cap);
				cap += num;
				memn = rtic->extras.bufpos -
				    (cap - rtic->extras.buf);
				memmove(scap, cap, memn);
				rtic->extras.bufpos -= cap - scap;
				cap = scap;
				rtic->extras.entries--;
				merged++;
			}
		}
		free(basename);
	}

	if (merged == 0 && skipped != 0)
		dowarn("circular use detected");
	return merged;
}

static int
print_dump(int argc, char **argv)
{
	TERM *term;
	uint8_t *buf;
	int i, n;
	size_t j, col;
	ssize_t len;

	printf("struct compiled_term {\n");
	printf("\tconst char *name;\n");
	printf("\tconst char *cap;\n");
	printf("\tsize_t caplen;\n");
	printf("};\n\n");

	printf("const struct compiled_term compiled_terms[] = {\n");

	n = 0;
	for (i = 0; i < argc; i++) {
		term = find_newest_term(argv[i]);
		if (term == NULL) {
			warnx("%s: no description for terminal", argv[i]);
			continue;
		}
		if (term->base_term != NULL) {
			warnx("%s: cannot dump alias", argv[i]);
			continue;
		}
		/* Don't compile the aliases in, save space */
		free(term->tic->alias);
		term->tic->alias = NULL;
		len = _ti_flatten(&buf, term->tic);
		if (len == 0 || len == -1)
			continue;

		printf("\t{\n");
		printf("\t\t\"%s\",\n", argv[i]);
		n++;
		for (j = 0, col = 0; j < (size_t)len; j++) {
			if (col == 0) {
				printf("\t\t\"");
				col = 16;
			}

			col += printf("\\%03o", (uint8_t)buf[j]);
			if (col > 75) {
				printf("\"%s\n",
				    j + 1 == (size_t)len ? "," : "");
				col = 0;
			}
		}
		if (col != 0)
			printf("\",\n");
		printf("\t\t%zu\n", len);
		printf("\t}");
		if (i + 1 < argc)
			printf(",");
		printf("\n");
		free(buf);
	}
	printf("};\n");

	return n;
}

static void
write_database(const char *dbname)
{
	struct cdbw *db;
	char *tmp_dbname;
	TERM *term;
	int fd;
	mode_t m;

	db = cdbw_open();
	if (db == NULL)
		err(EXIT_FAILURE, "cdbw_open failed");
	/* Save the terms */
	STAILQ_FOREACH(term, &terms, next)
		save_term(db, term);

	easprintf(&tmp_dbname, "%s.XXXXXX", dbname);
	fd = mkstemp(tmp_dbname);
	if (fd == -1)
		err(EXIT_FAILURE,
		    "creating temporary database %s failed", tmp_dbname);
	if (cdbw_output(db, fd, "NetBSD terminfo", cdbw_stable_seeder))
		err(EXIT_FAILURE,
		    "writing temporary database %s failed", tmp_dbname);
	m = umask(0);
	(void)umask(m);
	if (fchmod(fd, DEFFILEMODE & ~m))
		err(EXIT_FAILURE, "fchmod failed");
	if (close(fd))
		err(EXIT_FAILURE,
		    "writing temporary database %s failed", tmp_dbname);
	if (rename(tmp_dbname, dbname))
		err(EXIT_FAILURE, "renaming %s to %s failed", tmp_dbname, dbname);
	free(tmp_dbname);
	cdbw_close(db);
}

int
main(int argc, char **argv)
{
	int ch, cflag, sflag, flags;
	char *source, *dbname, *buf, *ofile;
	FILE *f;
	size_t buflen;
	ssize_t len;
	TBUF tbuf;
	struct term *term;

	cflag = sflag = 0;
	ofile = NULL;
	flags = TIC_ALIAS | TIC_DESCRIPTION | TIC_WARNING;
	while ((ch = getopt(argc, argv, "Saco:sx")) != -1)
	    switch (ch) {
	    case 'S':
		    Sflag = 1;
		    /* We still compile aliases so that use= works.
		     * However, it's removed before we flatten to save space. */
		    flags &= ~TIC_DESCRIPTION;
		    break;
	    case 'a':
		    flags |= TIC_COMMENT;
		    break;
	    case 'c':
		    cflag = 1;
		    break;
	    case 'o':
		    ofile = optarg;
		    break;
	    case 's':
		    sflag = 1;
		    break;
	    case 'x':
		    flags |= TIC_EXTRA;
		    break;
	    case '?': /* FALLTHROUGH */
	    default:
		    fprintf(stderr, "usage: %s [-acSsx] [-o file] source\n",
			getprogname());
		    return EXIT_FAILURE;
	    }

	if (optind == argc)
		errx(1, "No source file given");
	source = argv[optind++];
	f = fopen(source, "r");
	if (f == NULL)
		err(EXIT_FAILURE, "fopen: %s", source);

	hcreate(HASH_SIZE);

	buf = tbuf.buf = NULL;
	buflen = tbuf.buflen = tbuf.bufpos = 0;
	while ((len = getline(&buf, &buflen, f)) != -1) {
		/* Skip comments */
		if (*buf == '#')
			continue;
		if (buf[len - 1] != '\n') {
			process_entry(&tbuf, flags);
			dowarn("last line is not a comment"
			    " and does not end with a newline");
			continue;
		}
		/*
		 * If the first char is space not a space then we have a
		 * new entry, so process it.
		 */
		if (!isspace((unsigned char)*buf) && tbuf.bufpos != 0)
			process_entry(&tbuf, flags);

		/* Grow the buffer if needed */
		grow_tbuf(&tbuf, len);
		/* Append the string */
		memcpy(tbuf.buf + tbuf.bufpos, buf, len);
		tbuf.bufpos += len;
	}
	free(buf);
	/* Process the last entry if not done already */
	process_entry(&tbuf, flags);
	free(tbuf.buf);

	/* Merge use entries until we have merged all we can */
	while (merge_use(flags) != 0)
		;

	if (Sflag) {
		print_dump(argc - optind, argv + optind);
		return error_exit;
	}

	if (cflag)
		return error_exit;

	if (ofile == NULL)
		easprintf(&dbname, "%s.cdb", source);
	else
		dbname = ofile;
	write_database(dbname);

	if (sflag != 0)
		fprintf(stderr, "%zu entries and %zu aliases written to %s\n",
		    nterm, nalias, dbname);

	if (ofile == NULL)
		free(dbname);
	while ((term = STAILQ_FIRST(&terms)) != NULL) {
		STAILQ_REMOVE_HEAD(&terms, next);
		_ti_freetic(term->tic);
		free(term->name);
		free(term);
	}
#ifndef HAVE_NBTOOL_CONFIG_H
	/*
	 * hdestroy1 is not standard but we don't really care if we
	 * leak in the tools version
	 */
	hdestroy1(free, NULL);
#endif

	return EXIT_SUCCESS;
}
