/* This file is part of Mailfromd.
   Copyright (C) 2005-2022 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"

extern int yy_flex_debug;

static mu_list_t include_path;
static mu_list_t std_include_path;

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

static int
find_include_file(void *item, void *data)
{
	char *dir = item;
	struct file_data *dptr = data;
	size_t size = strlen(dir) + 1 + dptr->namelen + 1;
	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);
	return dptr->found = access(dptr->buf, F_OK) == 0;
}

void
include_path_setup()
{
	mu_list_create(&include_path);
	mu_list_create(&std_include_path);
	mu_list_append(std_include_path, DEFAULT_VERSION_INCLUDE_DIR);
	mu_list_append(std_include_path, DEFAULT_INCLUDE_DIR);
	mu_list_append(std_include_path, "/usr/share/mailfromd/include");
	mu_list_append(std_include_path, "/usr/local/share/mailfromd/include");
}

void
add_include_dir(const char *dir)
{
	int rc;
	
	if (!include_path) {
		rc = mu_list_create(&include_path);
		if (rc) {
			mu_error(_("cannot create include path list: %s"),
				 mu_strerror(rc));
			return;
		}
	}
	rc = mu_list_append(include_path, mu_strdup(dir));
	if (rc) 
		mu_error(_("cannot append to the include path list: %s"),
			 mu_strerror(rc));
}


#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;
}

static int
try_file(const char *name, int allow_cwd, int err_not_found, char **newp)
{
	static char *cwd = ".";
	struct file_data fd;
			
	fd.name = name;
	fd.namelen = strlen(name);
	fd.buf = NULL;
	fd.buflen = 0;
	fd.found = 0;
	
	if (allow_cwd) {
		mu_list_prepend(include_path, cwd);
		mu_list_foreach(include_path, find_include_file, &fd);
		mu_list_remove(include_path, cwd);
	} else
		mu_list_foreach(include_path, find_include_file, &fd);
			
	if (!fd.found) {
		mu_list_foreach(std_include_path, find_include_file, &fd);
					
		if (!fd.found && err_not_found) {
			parse_error(_("%s: no such file or directory"), name);
			*newp = NULL;
		} 
	}
	if (fd.found)
		*newp = fd.buf;
	return fd.found;
}

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, 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;
}

#define DEFAULT_SUFFIX ".mf"

void
require_module(const char *modname, struct import_rule *import_rules)
{
	size_t len;
	char *fname, *pathname;
	
	len = strlen(modname);
	
	fname = mu_alloc(len + sizeof DEFAULT_SUFFIX);
	strcpy(fname, modname);
	strcat(fname, DEFAULT_SUFFIX);
			
	if (try_file(fname, 0, 1, &pathname))
		begin_module(modname, pathname, import_rules);
	free(fname);
}

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;
}

static void
stderr_redirector(char *ppcmd, char **argv)
{
	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(argv[0], argv);
			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(argv[0], argv);
		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;
	struct mu_wordsplit ws;
	FILE *fp = NULL;
	
	size = strlen(ext_pp) + strlen(name) + 2;
	ppcmd = mu_alloc(size);
	strcpy(ppcmd, ext_pp);
	strcat(ppcmd, " ");
	strcat(ppcmd, name);

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

	if (mu_wordsplit(ppcmd, &ws, MU_WRDSF_DEFFLAGS)) {
		parse_error(_("cannot parse line: %s"),
			    mu_wordsplit_strerror (&ws));
		exit(EX_UNAVAILABLE);
	}
	
	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 unneded descripitors */
		close_fds_above(2);
		stderr_redirector(ppcmd, ws.ws_wordv);
		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;
	}
	mu_wordsplit_free(&ws);
	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(void *item, void *data)
{
	size_t *psize = data;
	*psize += 3 + strlen((char*)item);
	return 0;
}

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

static char *setup_file;

void
alloc_ext_pp()
{
	size_t size;
	
	if (ext_pp_options_given && !ext_pp) {
		if (!DEF_EXT_PP) {
			mu_error(_("default preprocessor is not set; "
				   "use --preprocessor option"));
			exit(EX_USAGE);
		}
		ext_pp = DEF_EXT_PP;
	}		

	if (!ext_pp)
		return;
	ext_pp = mu_strdup(ext_pp);
	
	if (ext_pp_options) {
		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;
	}

	size = 0;
	mu_list_foreach(include_path, _count_include_size, &size);
	if (size) {
		ext_pp = mu_realloc(ext_pp, strlen(ext_pp) + size + 1);
		mu_list_foreach(include_path, _append_includes, ext_pp);
	}

	if (try_file("pp-setup", 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);
	}
}

int
preprocess_input()
{
	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;
}

