/*
 * popen_noshell: A faster implementation of popen() and system() for Linux.
 * Copyright (c) 2009 Ivan Zahariev (famzah)
 * Copyright (c) 2012 Gunnar Beutner
 * Version: 1.0
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; under version 3 of the License.
 *
 * 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses>.
 */

#include "popen_noshell.h"
#include <errno.h>
#include <unistd.h>
#include <err.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <inttypes.h>

/*
 * Wish-list:
 *	1) Make the "ignore_stderr" parameter a mode - ignore, leave unchanged, redirect to stdout (the last is not implemented yet)
 *	2) Code a faster system(): system_noshell(), system_noshell_compat()
 */

//#define POPEN_NOSHELL_DEBUG

// because of C++, we can't call err() or errx() within the child, because they call exit(), and _exit() is what must be called; so we wrap
#define _ERR(EVAL, FMT, ...) \
	{ \
		warn(FMT, ##__VA_ARGS__); \
		_exit(EVAL); \
	}
#define _ERRX(EVAL, FMT, ...) \
	{ \
		warnx(FMT, ##__VA_ARGS__); \
		_exit(EVAL); \
	}

int popen_noshell_reopen_fd_to_dev_null(int fd) {
	int dev_null_fd;

	dev_null_fd = open("/dev/null", O_RDWR);
	if (dev_null_fd < 0) return -1;

	if (close(fd) != 0) {
		return -1;
	}
	if (dup2(dev_null_fd, fd) == -1) {
		return -1;
	}
	if (close(dev_null_fd) != 0) {
		return -1;
	}

	return 0;
}

static int popen_noshell_close_and_dup(int pipefd[2], int closed_pipefd, int target_fd) {
	int dupped_pipefd;

	dupped_pipefd = (closed_pipefd == 0 ? 1 : 0); // get the FD of the other end of the pipe

	if (close(pipefd[closed_pipefd]) != 0) {
		return -1;
	}

	if (close(target_fd) != 0) {
		return -1;
	}
	if (dup2(pipefd[dupped_pipefd], target_fd) == -1) {
		return -1;
	}
	if (close(pipefd[dupped_pipefd]) != 0) {
		return -1;
	}

	return 0;
}

static void popen_noshell_child_process(
	int pipefd_0, int pipefd_1,
	int read_pipe, int ignore_stderr,
	const char *file, const char * const *argv) {
	int closed_child_fd;
	int closed_pipe_fd;
	int dupped_child_fd;
	int pipefd[2] = {pipefd_0, pipefd_1};

	if (ignore_stderr) { /* ignore STDERR completely? */
		if (popen_noshell_reopen_fd_to_dev_null(STDERR_FILENO) != 0) _ERR(255, "popen_noshell_reopen_fd_to_dev_null(%d)", STDERR_FILENO);
	}

	if (read_pipe) {
		closed_child_fd = STDIN_FILENO;		/* re-open STDIN to /dev/null */
		closed_pipe_fd = 0;			/* close read end of pipe */
		dupped_child_fd = STDOUT_FILENO;	/* dup the other pipe end to STDOUT */
	} else {
		closed_child_fd = STDOUT_FILENO;	/* ignore STDOUT completely */
		closed_pipe_fd = 1;			/* close write end of pipe */
		dupped_child_fd = STDIN_FILENO;		/* dup the other pipe end to STDIN */
	}
	if (popen_noshell_reopen_fd_to_dev_null(closed_child_fd) != 0) {
		_ERR(255, "popen_noshell_reopen_fd_to_dev_null(%d)", closed_child_fd);
	}
	if (popen_noshell_close_and_dup(pipefd, closed_pipe_fd, dupped_child_fd) != 0) {
		_ERR(255, "popen_noshell_close_and_dup(%d ,%d)", closed_pipe_fd, dupped_child_fd);
	}

	execvp(file, (char * const *)argv);

	/* if we are here, exec() failed */

	warn("exec(\"%s\") inside the child", file);

	if (fflush(stdout) != 0) _ERR(255, "fflush(stdout)");
	if (fflush(stderr) != 0) _ERR(255, "fflush(stderr)");
	close(STDIN_FILENO);
	close(STDOUT_FILENO);
	close(STDERR_FILENO);

	_exit(255); // call _exit() and not exit(), or you'll have troubles in C++
}

char ** popen_noshell_copy_argv(const char * const *argv_orig) {
	int size = 1; /* there is at least one NULL element */
	char **argv;
	char **argv_new;
	int n;

	argv = (char **) argv_orig;
	while (*argv) {
		++size;
		++argv;
	}

	argv_new = (char **)malloc(sizeof(char *) * size);
	if (!argv_new) return NULL;

	argv = (char **)argv_orig;
	n = 0;
	while (*argv) {
		argv_new[n] = strdup(*argv);
		if (!argv_new[n]) return NULL;
		++argv;
		++n;
	}
	argv_new[n] = (char *)NULL;

	return argv_new;
}

/*
 * Pipe stream to or from process. Similar to popen(), only much faster.
 *
 * "file" is the command to be executed. It is searched within the PATH environment variable.
 * "argv[]" is an array to "char *" elements which are passed as command-line arguments.
 *	Note: The first element must be the same string as "file".
 *	Note: The last element must be a (char *)NULL terminating element.
 * "type" specifies if we are reading from the STDOUT or writing to the STDIN of the executed command "file". Use "r" for reading, "w" for writing.
 * "pid" is a pointer to an interger. The PID of the child process is stored there.
 * "ignore_stderr" has the following meaning:
 *	0: leave STDERR of the child process attached to the current STDERR of the parent process
 * 	1: ignore the STDERR of the child process
 *
 * This function is not very sustainable on failures. This means that if it fails for some reason (out of memory, no such executable, etc.),
 * you are probably in trouble, because the function allocated some memory or file descriptors and never released them.
 * Normally, this function should never fail.
 *
 * Returns NULL on any error, "errno" is set appropriately.
 * On success, a stream pointer is returned.
 * 	When you are done working with the stream, you have to close it by calling pclose_noshell(), or else you will leave zombie processes.
 */
FILE *popen_noshell(const char *file, const char * const *argv, const char *type, struct popen_noshell_pass_to_pclose *pclose_arg, int ignore_stderr) {
	int read_pipe;
	int pipefd[2]; // 0 -> READ, 1 -> WRITE ends
	pid_t pid;
	FILE *fp;

	memset(pclose_arg, 0, sizeof(struct popen_noshell_pass_to_pclose));

	if (strcmp(type, "r") == 0) {
		read_pipe = 1;
	} else if (strcmp(type, "w") == 0) {
		read_pipe = 0;
	} else {
		errno = EINVAL;
		return NULL;
	}

	if (pipe(pipefd) != 0) return NULL;

	pid = vfork();
	if (pid == -1) return NULL;
	if (pid == 0) {
		popen_noshell_child_process(pipefd[0], pipefd[1], read_pipe, ignore_stderr, file, argv);
		errx(EXIT_FAILURE, "This must never happen");
	} // child life ends here, for sure

	/* parent process */

	if (read_pipe) {
		if (close(pipefd[1/*write*/]) != 0) return NULL;
		fp = fdopen(pipefd[0/*read*/], "r");
	} else { // write_pipe
		if (close(pipefd[0/*read*/]) != 0) return NULL;
		fp = fdopen(pipefd[1/*write*/], "w");
	}
	if (fp == NULL) {
		return NULL; // fdopen() failed
	}

	pclose_arg->fp = fp;
	pclose_arg->pid = pid;
	
	return fp; // we should never end up here
}

int popen_noshell_add_ptr_to_argv(char ***argv, int *count, char *start) {
		*count += 1;
		*argv = (char **) realloc(*argv, *count * sizeof(char **));
		if (*argv == NULL) {
			return -1;
		}
		*(*argv + *count - 1) = start;
		return 0;
}

static int popen_noshell_add_token(char ***argv, int *count, char *start, char *command, int *j) {
	if (start != NULL && command + *j - 1 - start >= 0) {
		command[*j] = '\0'; // terminate the token in memory
		*j += 1;
#ifdef POPEN_NOSHELL_DEBUG
		printf("Token: %s\n", start);
#endif
		if (popen_noshell_add_ptr_to_argv(argv, count, start) != 0) {
			return -1;
		}
	}
	return 0;
}

#define popen_noshell_split_return_NULL { if (argv != NULL) free(argv); if (command != NULL) free(command); return NULL; }
char ** popen_noshell_split_command_to_argv(const char *command_original, char **free_this_buf) {
	char *command;
	size_t i, len;
	char *start = NULL;
	char c;
	char **argv = NULL;
	int count = 0;
	const char popen_bash_meta_characters[] = "!\\$`\n|&;()<>";
	int in_sq = 0;
	int in_dq = 0;
	int j = 0;
#ifdef POPEN_NOSHELL_DEBUG
	char **tmp;
#endif

	command = (char *)calloc(strlen(command_original) + 1, sizeof(char));
	if (!command) popen_noshell_split_return_NULL;

	*free_this_buf = command;

	len = strlen(command_original); // get the original length
	j = 0;
	for (i = 0; i < len; ++i) {
		if (!start) start = command + j;
		c = command_original[i];

		if (index(popen_bash_meta_characters, c) != NULL) {
			errno = EINVAL;
			popen_noshell_split_return_NULL;
		}

		if (c == ' ' || c == '\t') {
			if (in_sq || in_dq) {
				command[j++] = c;
				continue;
			}

			// new token
			if (popen_noshell_add_token(&argv, &count, start, command, &j) != 0) {
				popen_noshell_split_return_NULL;
			}
			start = NULL;
			continue;
		}

		if (c == '\'' && !in_dq) {
			in_sq = !in_sq;
			continue;
		}
		if (c == '"' && !in_sq) {
			in_dq = !in_dq;
			continue;
		}

		command[j++] = c;
	}
	if (in_sq || in_dq) { // unmatched single/double quote
		errno = EINVAL;
		popen_noshell_split_return_NULL;
	}

	if (popen_noshell_add_token(&argv, &count, start, command, &j) != 0) {
		popen_noshell_split_return_NULL;
	}

	if (count == 0) {
		errno = EINVAL;
		popen_noshell_split_return_NULL;
	}

	if (popen_noshell_add_ptr_to_argv(&argv, &count, NULL) != 0) { // NULL-terminate the list
		popen_noshell_split_return_NULL;
	}

#ifdef POPEN_NOSHELL_DEBUG
	tmp = argv;
	while (*tmp) {
		printf("ARGV: |%s|\n", *tmp);
		++tmp;
	}
#endif

	return argv;

	/* Example test strings:
		"a'zz bb edd"
		" abc ff  "
		" abc ff"
		"' abc   ff  ' "
		""
		"     "
		"     '"
		"ab\\c"
		"ls -la /proc/self/fd 'z'  'ab'g'z\" zz'   \" abc'd\" ' ab\"c   def '"
	*/
}

/*
 * Pipe stream to or from process. Similar to popen(), only much faster.
 *
 * This is simpler than popen_noshell() but is more INSECURE.
 * Since shells have very complicated expansion, quoting and word splitting algorithms, we do NOT try to re-implement them here.
 * This function does NOT support any special characters. It will immediately return an error if such symbols are encountered in "command".
 * The "command" is split only by space and tab delimiters. The special symbols are pre-defined in popen_bash_meta_characters[].
 * The only special characters supported are single and double quotes. You can enclose arguments in quotes and they should be splitted correctly.
 *
 * If possible, use popen_noshell() because of its better security.
 *
 * "command" is the command and its arguments to be executed. The command is searched within the PATH environment variable.
 *	The whole "command" string is parsed and splitted, so that it can be directly given to popen_noshell() and resp. to exec().
 *	This parsing is very simple and may contain bugs (see above). If possible, use popen_noshell() directly.
 * "type" specifies if we are reading from the STDOUT or writing to the STDIN of the executed command. Use "r" for reading, "w" for writing.
 * "pid" is a pointer to an interger. The PID of the child process is stored there.
 *
 * Returns NULL on any error, "errno" is set appropriately.
 * On success, a stream pointer is returned.
 * 	When you are done working with the stream, you have to close it by calling pclose_noshell(), or else you will leave zombie processes.
 */
FILE *popen_noshell_compat(const char *command, const char *type, struct popen_noshell_pass_to_pclose *pclose_arg) {
	char **argv;
	FILE *fp;
	char *to_free;

	argv = popen_noshell_split_command_to_argv(command, &to_free);
	if (!argv) {
		if (to_free) free(to_free);
		return NULL;
	}

	fp = popen_noshell(argv[0], (const char * const *)argv, type, pclose_arg, 0);

	free(to_free);
	free(argv);

	return fp;
}

/*
 * You have to call this function after you have done working with the FILE pointer "fp" returned by popen_noshell() or by popen_noshell_compat().
 *
 * Returns -1 on any error, "errno" is set appropriately.
 * Returns the "status" of the child process as returned by waitpid().
 */
int pclose_noshell(struct popen_noshell_pass_to_pclose *arg) {
	int status;

	if (fclose(arg->fp) != 0) {
		return -1;
	}

	if (waitpid(arg->pid, &status, 0) != arg->pid) {
		return -1;
	}

	return status;
}