/*
 * daemonlet — long-lived helper runner for vdevd (replaces the dash wrapper).
 *
 * Speaks the same stdin/stdout protocol as the old shell runner:
 *   - stdin: KEY=VALUE lines, then "done"; empty EOF → exit
 *   - stdout: one readiness newline at start; then ASCII exit status + newline
 *             per request
 *
 * Environment values are applied with setenv(3) and never passed through shell
 * eval. The helper script is invoked as:
 *   dash -c '. "$1"; main; exit $?' helper
 * with stdout redirected to /dev/null (status comes from waitpid).
 *
 * Copyright (C) 2026  Aitor C.Z. <aitor_czr@gnuinos.org>
 * SPDX-License-Identifier: GPL-3.0-or-later
 */

#define _GNU_SOURCE

#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

#define MAX_ENV_VARS 1024
#define MAX_LINE (256 * 1024)

struct env_pair {
	char *key;
	char *val;
};

static void free_pairs(struct env_pair *pairs, size_t n)
{
	size_t i;

	for (i = 0; i < n; i++) {
		free(pairs[i].key);
		free(pairs[i].val);
		pairs[i].key = NULL;
		pairs[i].val = NULL;
	}
}

static int valid_key(const char *key)
{
	const char *p;

	if (key == NULL || key[0] == '\0')
		return 0;
	if (!(isalpha((unsigned char)key[0]) || key[0] == '_'))
		return 0;
	for (p = key + 1; *p; p++) {
		if (!(isalnum((unsigned char)*p) || *p == '_'))
			return 0;
	}
	return 1;
}

/* Strip one layer of surrounding double quotes if present (legacy shell runner). */
static void strip_optional_dq(char *val)
{
	size_t n;

	if (val == NULL)
		return;
	n = strlen(val);
	if (n >= 2 && val[0] == '"' && val[n - 1] == '"') {
		memmove(val, val + 1, n - 2);
		val[n - 2] = '\0';
	}
}

/*
 * Read one stanza from stdin into pairs[].
 * Returns:
 *   0  success (pairs filled, *n_out set)
 *  -1  EOF / told to exit (no vars or stdin closed)
 *  -2  malformed input
 */
static int read_stanza(struct env_pair *pairs, size_t *n_out)
{
	char *line = NULL;
	size_t cap = 0;
	size_t n = 0;
	ssize_t len;

	*n_out = 0;

	while ((len = getline(&line, &cap, stdin)) >= 0) {
		char *eq;
		char *key;
		char *val;
		size_t i;

		if (len > 0 && line[len - 1] == '\n')
			line[--len] = '\0';

		if (len == 0) {
			free(line);
			free_pairs(pairs, n);
			return -1; /* closed / empty → exit */
		}

		if (strcmp(line, "done") == 0)
			break;

		if (n >= MAX_ENV_VARS) {
			fprintf(stderr, "daemonlet: too many environment variables\n");
			free(line);
			free_pairs(pairs, n);
			return -2;
		}

		if ((size_t)len > MAX_LINE) {
			fprintf(stderr, "daemonlet: line too long\n");
			free(line);
			free_pairs(pairs, n);
			return -2;
		}

		eq = strchr(line, '=');
		if (eq == NULL) {
			fprintf(stderr, "daemonlet: unparseable line (no '=')\n");
			free(line);
			free_pairs(pairs, n);
			return -2;
		}

		*eq = '\0';
		key = line;
		val = eq + 1;

		if (!valid_key(key)) {
			fprintf(stderr, "daemonlet: invalid environment key\n");
			free(line);
			free_pairs(pairs, n);
			return -2;
		}

		/* Reject embedded NUL (getline won't include one mid-line). */
		pairs[n].key = strdup(key);
		pairs[n].val = strdup(val);
		if (pairs[n].key == NULL || pairs[n].val == NULL) {
			free(pairs[n].key);
			free(pairs[n].val);
			free(line);
			free_pairs(pairs, n);
			return -2;
		}
		strip_optional_dq(pairs[n].val);

		/* Last assignment wins if a key repeats. */
		for (i = 0; i < n; i++) {
			if (strcmp(pairs[i].key, pairs[n].key) == 0) {
				free(pairs[i].key);
				free(pairs[i].val);
				pairs[i] = pairs[n];
				pairs[n].key = NULL;
				pairs[n].val = NULL;
				goto next_line;
			}
		}
		n++;
next_line:
		;
	}

	free(line);

	if (n == 0)
		return -1; /* EOF with no vars */

	*n_out = n;
	return 0;
}

static int run_helper(const char *helper, struct env_pair *pairs, size_t n)
{
	pid_t pid;
	int status;
	size_t i;

	pid = fork();
	if (pid < 0) {
		fprintf(stderr, "daemonlet: fork: %s\n", strerror(errno));
		return 127;
	}

	if (pid == 0) {
		int dn;

		/* Child: install env, then source helper and call main(). */
		clearenv();
		if (setenv("PATH", "/sbin:/bin:/usr/sbin:/usr/bin", 1) != 0)
			_exit(127);

		for (i = 0; i < n; i++) {
			if (setenv(pairs[i].key, pairs[i].val, 1) != 0)
				_exit(127);
		}

		dn = open("/dev/null", O_WRONLY | O_CLOEXEC);
		if (dn >= 0) {
			if (dup2(dn, STDOUT_FILENO) < 0) {
				close(dn);
				_exit(127);
			}
			close(dn);
		}

		/*
		 * Helpers define main() and skip the bottom call when
		 * VDEV_DAEMONLET=1. Invoke main explicitly; keep stanza's
		 * VDEV_DAEMONLET for helper-internal checks.
		 */
		execl("/bin/dash", "dash", "-c",
		      ". \"$1\" || exit 127; main; exit $?",
		      "vdev-daemonlet", helper, (char *)NULL);
		/* Fallback if dash is missing (usrmerge / busybox). */
		execl("/bin/sh", "sh", "-c",
		      ". \"$1\" || exit 127; main; exit $?",
		      "vdev-daemonlet", helper, (char *)NULL);
		_exit(127);
	}

	if (waitpid(pid, &status, 0) < 0) {
		fprintf(stderr, "daemonlet: waitpid: %s\n", strerror(errno));
		return 127;
	}

	if (WIFEXITED(status))
		return WEXITSTATUS(status);
	if (WIFSIGNALED(status))
		return 128 + WTERMSIG(status);
	return 127;
}

int main(int argc, char **argv)
{
	const char *helper;
	struct env_pair pairs[MAX_ENV_VARS];
	size_t n = 0;

	if (argc < 2) {
		fprintf(stderr, "usage: %s <helper-script>\n", argv[0]);
		return 1;
	}
	helper = argv[1];

	memset(pairs, 0, sizeof(pairs));

	/* Readiness ping for vdev_daemonlet_start(). */
	if (write(STDOUT_FILENO, "\n", 1) != 1)
		return 1;

	for (;;) {
		int rc;
		int helper_rc;
		char status_buf[32];
		int status_len;

		rc = read_stanza(pairs, &n);
		if (rc == -1)
			return 0;
		if (rc == -2)
			return 1;

		helper_rc = run_helper(helper, pairs, n);
		free_pairs(pairs, n);
		n = 0;

		status_len = snprintf(status_buf, sizeof(status_buf), "%d\n",
				      helper_rc);
		if (status_len < 0 || (size_t)status_len >= sizeof(status_buf))
			return 1;
		if (write(STDOUT_FILENO, status_buf, (size_t)status_len) !=
		    status_len)
			return 1;
	}
}
