/* This file is part of Mailfromd.
   Copyright (C) 2005-2025 Sergey Poznyakoff

   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation; either version 3, or (at your option)
   any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program.  If not, see <http://www.gnu.org/licenses/>. */

#ifdef HAVE_CONFIG_H
# include <config.h>
#endif
#include <sys/types.h>	
#include <sys/stat.h>
#include "mailfromd.h"
#include "prog.h"

void
pathbuf_add(struct pathbuf *pb, char const *dir)
{
	size_t dir_len = strlen(dir);
	if (pb->alloc) {
		size_t init_len = strlen(pb->base);
		int delim = pb->base[pb->pos] != 0;
		pb->base = mu_realloc(pb->base, init_len + 1 + dir_len + 1);
		if (delim) {
			memmove(pb->base + pb->pos + dir_len + 1, pb->base + pb->pos,
				init_len - pb->pos + 1);
			memcpy(pb->base + pb->pos, dir, dir_len);
			pb->pos += dir_len;
			pb->base[pb->pos++] = ':';
		} else {
			pb->base[pb->pos++] = ':';
			strcpy(pb->base + pb->pos, dir);
			pb->pos += dir_len;
		}
	} else if (pb->base) {
		size_t init_len = strlen(pb->base);
		char *p = mu_alloc(init_len + 1 + dir_len + 1);
		memcpy(p, dir, dir_len);
		pb->pos += dir_len;
		p[pb->pos++] = ':';
		strcpy(p + pb->pos, pb->base);
		pb->base = p;
		pb->alloc = 1;
	} else {
		pb->base = strdup(dir);
		pb->pos = strlen(pb->base);
		pb->alloc = 1;
	}
}

char const *default_suffixes[] = { ".mfl", NULL };

struct pathbuf include_path = PATHBUF_INITIALIZER(
	DEFAULT_MODDIR "/include:"
	DEFAULT_VERSION_MODDIR "/include"
);

struct pathbuf module_path = PATHBUF_INITIALIZER(
        DEFAULT_MODDIR ":" DEFAULT_VERSION_MODDIR
);

static void
path_enumerate(char const *path, int (*func)(const char *, void *), void *data)
{
	char *dirbuf = NULL;
	size_t dirsize = 0;

	if (!path)
		return;
	for (;;) {
		size_t len = strcspn(path, ":");
		len++;
		if (len > dirsize) {
			dirsize = len;
			dirbuf = mu_realloc(dirbuf, dirsize);
		}
		len--;
		memcpy(dirbuf, path, len);
		dirbuf[len] = 0;

		if (func(dirbuf, data))
			break;

		path += len;
		if (*path)
			path++;
		else
			break;
	}
	free(dirbuf);
}

struct scan_buffer {
	const char *name;
	size_t namelen;
	char const **suffixes;
	char *buf;
	size_t buflen;
	int found;
};

#define SCAN_BUFFER_INITIALIZER(n, s)			\
	{						\
	        .name = n,				\
		.namelen = strlen(n),			\
		.suffixes = s,				\
		.buf = NULL,				\
		.buflen = 0,				\
		.found = 0				\
	}

static int
test_file(char const *dir, struct scan_buffer *dptr, char const *suf)
{
	size_t size = strlen(dir) + dptr->namelen + (suf ? strlen(suf) : 0) + 2;
	if (size > dptr->buflen) {
		dptr->buflen = size;
		dptr->buf = mu_realloc(dptr->buf, dptr->buflen);
	}
	strcpy(dptr->buf, dir);
	strcat(dptr->buf, "/");
	strcat(dptr->buf, dptr->name);
	if (suf)
		strcat(dptr->buf, suf);
	return dptr->found = (access(dptr->buf, F_OK) == 0);
}

static int
scan_dir_fun(char const *dir, void *data)
{
	struct scan_buffer *dptr = data;

	if (dptr->suffixes) {
		int i;

		for (i = 0; dptr->suffixes[i]; i++) {
			if (test_file(dir, dptr, dptr->suffixes[i]))
				break;
		}
	} else
		test_file(dir, dptr, NULL);
	return dptr->found;
}

char *
path_find_file(const char *path, const char *name, char const **suffixes)
{
	struct scan_buffer sbuf = SCAN_BUFFER_INITIALIZER(name, suffixes);
	path_enumerate(path, scan_dir_fun, &sbuf);
	if (sbuf.found)
		return sbuf.buf;
	free(sbuf.buf);
	return NULL;
}

static int
try_file(const char *name, const char **suffixes,
	 struct pathbuf *pb,
	 int allow_cwd, int err_not_found, char **newp)
{
	struct scan_buffer sbuf = SCAN_BUFFER_INITIALIZER(name, suffixes);

	if (allow_cwd) {
		if ((*newp = path_find_file(".", name, suffixes)) != NULL)
			return 1;
	}

	path_enumerate(pb->base, scan_dir_fun, &sbuf);

	if (!sbuf.found && err_not_found) {
		parse_error(_("%s: no such file or directory"), name);
		*newp = NULL;
	}

	if (sbuf.found)
		*newp = sbuf.buf;
	else
		free(sbuf.buf);
	return sbuf.found;
}

#define ID_EQ(a,b)						\
	((a).i_node == (b).i_node && (a).device == (a).device)

static int
input_file_ident_cmp(const void *item, const void *data)
{
	const struct input_file_ident *id1 = item;
	const struct input_file_ident *id2 = data;
	return !ID_EQ(*id1, *id2);
}

static void
input_file_ident_free(void *item)
{
	free(item);
}

int
source_lookup(struct input_file_ident *idptr)
{
	int rc;
	if (!top_module->incl_sources) {
		mu_list_create(&top_module->incl_sources);
		mu_list_set_comparator(top_module->incl_sources,
				       input_file_ident_cmp);
		mu_list_set_destroy_item(top_module->incl_sources,
					 input_file_ident_free);
	}
	rc = mu_list_locate (top_module->incl_sources, idptr, NULL);
	if (rc == MU_ERR_NOENT) {
		struct input_file_ident *new_id = mu_alloc(sizeof *new_id);
		*new_id = *idptr;
		mu_list_append(top_module->incl_sources, new_id);
	}
	return rc == 0;
}

int
begin_module(const char *modname, const char *filename,
	     struct import_rule *import_rules)
{
	int rc;
	
	if (access(filename, R_OK)) {
		parse_error(_("input file %s is not readable: %s"),
			    filename,
			    mu_strerror(errno));
		return 1;
	}
	if (set_top_module(modname, filename, import_rules, get_locus()) == 0)
		rc = lex_new_source(filename, LEX_MODULE);
	else
		rc = 0;
	return rc;
}

int
parse_include(const char *text, int once)
{
	struct mu_wordsplit ws;
	char *tmp = NULL;
	char *p = NULL;
	int rc = 1;

	if (mu_wordsplit(text, &ws, MU_WRDSF_DEFFLAGS)) {
		parse_error(_("cannot parse include line: %s"),
			    mu_wordsplit_strerror(&ws));
		return errno;
	} else if (ws.ws_wordc != 2)
		mu_wordsplit_free(&ws);
	else {
		size_t len;
		int allow_cwd;

		p = ws.ws_wordv[1];
		len = strlen(p);
		
		if (p[0] == '<' && p[len - 1] == '>') {
			allow_cwd = 0;
			p[len - 1] = 0;
			p++;
		} else 
			allow_cwd = 1;

		if (p[0] != '/' && try_file(p, NULL, &include_path, allow_cwd, 1, &tmp))
			p = tmp;
	}

	if (p)
		rc = lex_new_source(p, once ? LEX_ONCE : LEX_NONE);
	free(tmp);
	mu_wordsplit_free(&ws);
	return rc;
}

void
require_module(const char *modname, struct import_rule *import_rules)
{
	char *pathname;
	
	if (try_file(modname, default_suffixes, &module_path, 0, 1, &pathname) == 0)
		return;
	begin_module(modname, pathname, import_rules);
}

static int
assign_locus(struct mu_locus_point *ploc, const char *name,
	     const char *linestr)
{
	char *p;
	unsigned long linenum;
	errno = 0;
	linenum = strtoul(linestr, &p, 10);
	if (errno)
		return errno;
	if (*p)
		return MU_ERR_PARSE;
	if (linenum > UINT_MAX)
		return ERANGE;
	mu_locus_point_init(ploc);
	mu_locus_point_set_file(ploc, name);
	ploc->mu_line = (unsigned) linenum;
	return 0;
}

int
parse_line(char *text, struct mu_locus_point *ploc)
{
	int rc;
	struct mu_wordsplit ws;

	while (*text && mu_isspace (*text))
		text++;
	text++;
	
	if (mu_wordsplit(text, &ws, MU_WRDSF_DEFFLAGS)) {
		parse_error(_("cannot parse #line line: %s"),
			    mu_wordsplit_strerror (&ws));
		return 1;
	} else {
		if (ws.ws_wordc == 2)
			rc = assign_locus(ploc, NULL, ws.ws_wordv[1]);
		else if (ws.ws_wordc == 3)
			rc = assign_locus(ploc, ws.ws_wordv[2],
					  ws.ws_wordv[1]);
		else 
			rc = 1;

		if (rc) 
			parse_error(_("malformed #line statement"));
	}
	mu_wordsplit_free(&ws);
	return rc;
}

int
parse_line_cpp(char *text, struct mu_locus_point *ploc)
{
	struct mu_wordsplit ws;
	int rc;
	
	if (mu_wordsplit(text, &ws, MU_WRDSF_DEFFLAGS)) {
		parse_error(_("cannot parse #line line: %s"),
			    mu_wordsplit_strerror (&ws));
		rc = 1;
	} else if (ws.ws_wordc < 3) {
		parse_error(_("invalid #line statement"));
		rc = 1;
	} else if (assign_locus(ploc, ws.ws_wordv[2], ws.ws_wordv[1])) {
		parse_error(_("malformed #line statement"));
		rc = 1;
	} else
		rc = 0;
	mu_wordsplit_free(&ws);
	return rc;
}

struct preprocessor preprocessor = {
	.enabled = 1,
	.command = DEF_EXT_PP,
	.pass_includes = 0,
	.pass_defines = 1,
	.setup_file = "pp-setup"
};

static struct mu_cfg_param preprocessor_section_param[] = {
	{ "enable", mu_c_bool, &preprocessor.enabled, 0, NULL,
	  N_("Enable preprocessor (default).") },
	{ "command", mu_c_string, &preprocessor.command, 0, NULL,
	  N_("Preprocessor command line stub.") },
	{ "pass-includes", mu_c_bool, &preprocessor.pass_includes, 0, NULL,
	  N_("Pass include search path to the preprocessor via -I options (default: false)") },
	{ "pass-defines", mu_c_bool, &preprocessor.pass_defines, 0, NULL,
	  N_("Pass feature definition and defines from the command line to the preprocessor via -D options (default: true)") },
	{ "setup-file", mu_c_string, &preprocessor.setup_file, 0, NULL,
	  N_("Name of the preprocessor setup file.  Looked up in include search path, unless absolute.") },
	{ NULL }
};

void
preprocessor_cfg_init(void)
{
	struct mu_cfg_section *section;
	if (mu_create_canned_section("preprocessor", &section) == 0) {
		section->label = NULL;
		section->parser = NULL;
		section->docstring = N_("MFL preprocessor configuration.");
		mu_cfg_section_add_params(section, preprocessor_section_param);
	}
	if (preprocessor.command == NULL)
		preprocessor.enabled = 0;
}

static void
stderr_redirector(char *ppcmd)
{
	char *xargv[] = { getenv("SHELL"), "-c", ppcmd, NULL };

	if (!xargv[0])
		xargv[0] = "/bin/sh";

	if (!logger_flags(LOGF_STDERR)) {
		int p[2];
		char buf[1024];
		FILE *fp;
		pid_t pid;
		
		signal(SIGCHLD, SIG_DFL);
		if (pipe(p)) {
			mu_diag_funcall(MU_DIAG_ERROR, "pipe", "p", errno);
			exit(127);
		}
		
		switch (pid = fork()) {
			/* Grandchild */
		case 0:
			if (p[1] != 2) {
				close(2);
				dup2(p[1], 2);
			}
			close(p[0]);
			
			execvp(xargv[0], xargv);
			exit(127);
			
		case -1:
			/*  Fork failed */
			mf_server_log_setup();
			mu_error("Cannot run `%s': %s",
				 ppcmd, mu_strerror(errno));
			exit(127);
			
		default:
			/* Sub-master */
			close(p[1]);
			fp = fdopen(p[0], "r");
			mf_server_log_setup();
			while (fgets(buf, sizeof(buf), fp))
				mu_error("%s", buf);
			exit(0);
		}
	} else {
		execvp(xargv[0], xargv);
		mf_server_log_setup();
		mu_error("Cannot run `%s': %s",
			 ppcmd, mu_strerror(errno));
		exit(127);
	}
}

FILE *
pp_extrn_start(const char *name, pid_t *ppid)
{
	int pout[2];
	pid_t pid;
	size_t size;
	char *ppcmd;
	FILE *fp = NULL;
	
	size = strlen(preprocessor.command) + strlen(name) + 2;
	ppcmd = mu_alloc(size);
	strcpy(ppcmd, preprocessor.command);
	strcat(ppcmd, " ");
	strcat(ppcmd, name);

	mu_debug(MF_SOURCE_PP, MU_DEBUG_TRACE1,
		 ("Running preprocessor: `%s'", ppcmd));

	if (pipe(pout)) {
		mu_diag_funcall(MU_DIAG_ERROR, "pipe", "pout", errno);
		exit(EX_UNAVAILABLE);
	}
	switch (pid = fork()) {
		/* The child branch.  */
	case 0:
		if (pout[1] != 1) {
			close(1);
			dup2(pout[1], 1);
		}

                /* Close unneeded descriptors */
		close_fds_above(2);
		stderr_redirector(ppcmd);
		break;

	case -1:
		/*  Fork failed */
		mu_error("Cannot run `%s': %s",
			 ppcmd, mu_strerror(errno));
		break;
		
	default:
		close(pout[1]);
		fp = fdopen(pout[0], "r");
		break;
	}
	free(ppcmd);
	*ppid = pid;
	return fp;
}

void
pp_extrn_shutdown(FILE *file, pid_t pid)
{
	int status;
	fclose(file);
	waitpid(pid, &status, 0);
	if (WIFEXITED(status)) {
		status = WEXITSTATUS(status);
		if (status) {
			mu_error(_("preprocessor exited with status %d"),
				 status);
		}
	} else if (WIFSIGNALED(status)) {
		mu_error(_("preprocessor terminated on signal %d"),
			 WTERMSIG(status));
	} else if (WIFSTOPPED(status)) {
		mu_error("%s", _("preprocessor stopped"));
	} else {
		mu_error("%s", _("preprocessor terminated abnormally"));
	}
}

static int
_count_include_size(const char *dir, void *data)
{
	size_t *psize = data;
	*psize += 3 + strlen(dir);
	return 0;
}

static int
_append_includes(const char *dir, void *data)
{
	char *str = data;
	strcat(str, " -I");
	strcat(str, dir);
	return 0;
}

void
preprocessor_finalize(void)
{
	size_t size;
	char *setup_file;
	char *ext_pp;
	
	if (!preprocessor.enabled || !preprocessor.command) {
		if (ext_pp_options_given) {
			mu_error(_("preprocessor options given, "
				   "but the use of preprocessor is disabled; "
				   "use the --preprocessor option"));
			exit(EX_USAGE);
		}
		return;
	}
	
	ext_pp = mu_strdup(preprocessor.command);
	
	if (ext_pp_options && preprocessor.pass_defines) {
		size_t len;
		char *p;
	
		len = strlen(ext_pp) + strlen(ext_pp_options);
		p = mu_alloc(len + 1);
		strcpy(p, ext_pp);
		strcat(p, ext_pp_options);
		ext_pp = p;
	}

	if (preprocessor.pass_includes) {
		size = 0;
		path_enumerate(include_path.base, _count_include_size, &size);
		if (size) {
			ext_pp = mu_realloc(ext_pp, strlen(ext_pp) + size + 1);
			path_enumerate(include_path.base, _append_includes, ext_pp);
		}
	}
	
	setup_file = preprocessor.setup_file;

	if (setup_file[0] == '/' ||
	    try_file(setup_file, NULL, &include_path, 1, 0, &setup_file)) {
		ext_pp = mu_realloc(ext_pp, strlen(ext_pp) +
				    strlen(setup_file) + 2);
		strcat(ext_pp, " ");
		strcat(ext_pp, setup_file);
	}

	preprocessor.command = ext_pp;
}

int
preprocess_input(void)
{
	char buffer[512];
	FILE *file;
	pid_t pid;
	ssize_t n;
	
	file = pp_extrn_start(script_file, &pid);
	if (!file)
		return EX_NOINPUT;
	while ((n = fread(buffer, 1, sizeof buffer, file)) > 0)
		fwrite(buffer, 1, n, stdout);
	pp_extrn_shutdown(file, pid);
	return 0;
}
