/*
 * Remote-control functions.
 *
 * Copyright 2002-2008, 2010, 2012-2015, 2017, 2021, 2023-2025 Andrew Wood
 *
 * License GPLv3+: GNU GPL version 3 or later; see `docs/COPYING'.
 */

#include "config.h"
#include "options.h"
#include "pv.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <signal.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/time.h>
#include <sys/stat.h>

#ifdef PV_REMOTE_CONTROL
void pv_error(pvstate_t, char *, ...);

struct remote_msg {
	bool progress;			 /* progress bar flag */
	bool timer;			 /* timer flag */
	bool eta;			 /* ETA flag */
	bool fineta;			 /* absolute ETA flag */
	bool rate;			 /* rate counter flag */
	bool average_rate;		 /* average rate counter flag */
	bool bytes;			 /* bytes transferred flag */
	bool bufpercent;		 /* transfer buffer percentage flag */
	size_t lastwritten;		 /* last-written bytes count */
	off_t rate_limit;		 /* rate limit, in bytes per second */
	size_t buffer_size;		 /* buffer size, in bytes (0=default) */
	off_t size;			 /* total size of data */
	double interval;		 /* interval between updates */
	unsigned int width;		 /* screen width */
	unsigned int height;		 /* screen height */
	bool width_set_manually;	 /* width was set manually, not detected */
	bool height_set_manually;	 /* height was set manually, not detected */
	char name[256];			 /* flawfinder: ignore */
	char format[256];		 /* flawfinder: ignore */
	char extra_display[256];	 /* flawfinder: ignore */
};

/*
 * flawfinder rationale: name, format, and extra_display are always
 * explicitly zeroed and bounded to one less than their size so they are
 * always \0 terminated.
 */


/*
 * Return a stream pointer, and populate the filename buffer, for a control
 * file associated with a particular process ID; it will be opened for
 * writing if "sender" is true.  Returns NULL on error.
 */
/*@null@ */
/*@dependent@ */
static FILE *pv__control_file(char *filename, size_t bufsize, pid_t control_pid, bool sender)
{
	int open_flags, open_mode, control_fd;
	/*@dependent@ */ FILE *control_fptr = NULL;

	open_flags = O_RDONLY;
#ifdef O_NOFOLLOW
	open_flags += O_NOFOLLOW;
#endif
	if (sender)
		open_flags = O_WRONLY | O_CREAT | O_EXCL;

	open_mode = 0644;

	(void) pv_snprintf(filename, bufsize, "/run/user/%lu/pv.remote.%lu", (unsigned long) geteuid(),
			   (unsigned long) control_pid);
	control_fd = open(filename, open_flags, open_mode);	/* flawfinder: ignore */

	/*
	 * If /run/user/<uid> wasn't usable, try $HOME/.pv instead.
	 */
	if (control_fd < 0) {
		char *home_dir;

		home_dir = getenv("HOME");  /* flawfinder: ignore */
		if (NULL == home_dir)
			return NULL;
		if ('\0' == home_dir[0])
			return NULL;

		/*
		 * flawfinder rationale: null and zero-size values are
		 * rejected, and the destination buffer is bounded.
		 */

		(void) pv_snprintf(filename, bufsize, "%s/.pv/remote.%lu", home_dir, (unsigned long) control_pid);
		control_fd = open(filename, open_flags, open_mode);	/* flawfinder: ignore */

		/*
		 * If the open failed, try creating the $HOME/.pv directory
		 * first.
		 */
		if (control_fd < 0) {
			(void) pv_snprintf(filename, bufsize, "%s/.pv", home_dir);
			(void) mkdir(filename, 0700);
			/* In case of weird umask, explicitly chmod the dir. */
			(void) chmod(filename, 0700);	/* flawfinder: ignore */
			(void) pv_snprintf(filename, bufsize, "%s/.pv/remote.%lu", home_dir,
					   (unsigned long) control_pid);
			control_fd = open(filename, open_flags, open_mode);	/* flawfinder: ignore */
		}
	}

	/*
	 * flawfinder rationale: the files are in a directory whose parents
	 * cannot be manipulated, and we are not allowing the final
	 * component to be a symbolic link.  We are checking that $HOME is
	 * not NULL, and it's bounded by pv_snprintf() so it can't overshoot
	 * the filename buffer.  When we chmod $HOME/.pv, we assume that an
	 * attacker is unlikely to be able to manipulate $HOME's contents to
	 * make use of us setting $HOME/.pv to mode 700.
	 */

	if (control_fd < 0)
		return NULL;

	debug("%s: %s", "control filename", filename);

	control_fptr = fdopen(control_fd, sender ? "wb" : "rb");

	return control_fptr;
}


/*
 * Set the options of a remote process by writing them to a file, sending a
 * signal to the receiving process, and waiting for the message to be
 * consumed by the remote process.
 *
 * Returns nonzero on error.
 */
int pv_remote_set(opts_t opts, pvstate_t state)
{
	char control_filename[4096];	 /* flawfinder: ignore */
	FILE *control_fptr;
	struct remote_msg msgbuf;
	pid_t signal_sender;
	long timeout;
	bool received;

	/*
	 * flawfinder rationale: buffer is large enough, explicitly zeroed,
	 * and always bounded properly as we are only writing to it with
	 * pv_snprintf().
	 */

	/*
	 * Check that the remote process exists.
	 */
	if (kill((pid_t) (opts->remote), 0) != 0) {
		pv_error(state, "%u: %s", opts->remote, strerror(errno));
		return PV_ERROREXIT_REMOTE_OR_PID;
	}

	/*
	 * Make sure parameters are within sensible bounds.
	 */
	if (opts->width < 1)
		opts->width = 80;
	if (opts->height < 1)
		opts->height = 25;
	if (opts->width > 999999)
		opts->width = 999999;
	if (opts->height > 999999)
		opts->height = 999999;
	if ((opts->interval > 0) && (opts->interval < 0.1))
		opts->interval = 0.1;
	if (opts->interval > 600)
		opts->interval = 600;

	/*
	 * Copy parameters into message buffer.
	 */
	memset(&msgbuf, 0, sizeof(msgbuf));
	msgbuf.progress = opts->progress;
	msgbuf.timer = opts->timer;
	msgbuf.eta = opts->eta;
	msgbuf.fineta = opts->fineta;
	msgbuf.rate = opts->rate;
	msgbuf.average_rate = opts->average_rate;
	msgbuf.bytes = opts->bytes;
	msgbuf.bufpercent = opts->bufpercent;
	msgbuf.lastwritten = opts->lastwritten;
	msgbuf.rate_limit = opts->rate_limit;
	msgbuf.buffer_size = opts->buffer_size;
	msgbuf.size = opts->size;
	msgbuf.interval = opts->interval;
	msgbuf.width = opts->width;
	msgbuf.height = opts->height;
	msgbuf.width_set_manually = opts->width_set_manually;
	msgbuf.height_set_manually = opts->height_set_manually;

	if (opts->name != NULL) {
		strncpy(msgbuf.name, opts->name, sizeof(msgbuf.name) - 1);	/* flawfinder: ignore */
	}
	if (opts->format != NULL) {
		strncpy(msgbuf.format, opts->format, sizeof(msgbuf.format) - 1);	/* flawfinder: ignore */
	}
	if (opts->extra_display != NULL) {
		strncpy(msgbuf.extra_display, opts->extra_display, sizeof(msgbuf.extra_display) - 1);	/* flawfinder: ignore */
	}

	/*
	 * flawfinder rationale: name, format, and extra_display are
	 * explicitly bounded to 1 less than the size of their buffer and
	 * the buffer is \0 terminated by memset() earlier.
	 */

	/*
	 * Get the filename and file stream to use for remote control.
	 */
	memset(control_filename, 0, sizeof(control_filename));
	control_fptr = pv__control_file(control_filename, sizeof(control_filename), (pid_t) getpid(), true);
	if (NULL == control_fptr) {
		pv_error(state, "%s", strerror(errno));
		return PV_ERROREXIT_REMOTE_OR_PID;
	}

	/*
	 * Write the message buffer to the remote control file, and close
	 * it.
	 */
	if (1 != fwrite(&msgbuf, sizeof(msgbuf), 1, control_fptr)) {
		pv_error(state, "%s", strerror(errno));
		(void) fclose(control_fptr);
		(void) remove(control_filename);
		return PV_ERROREXIT_REMOTE_OR_PID;
	}

	if (0 != fclose(control_fptr)) {
		pv_error(state, "%s", strerror(errno));
		(void) remove(control_filename);
		return PV_ERROREXIT_REMOTE_OR_PID;
	}

	/*
	 * Send a SIGUSR2 signal to the remote process, to tell it a message
	 * is ready to read, after clearing our own "SIGUSR2 received" flag.
	 */
	signal_sender = 0;
	(void) pv_sigusr2_received(state, &signal_sender);
	if (kill((pid_t) (opts->remote), SIGUSR2) != 0) {
		pv_error(state, "%u: %s", opts->remote, strerror(errno));
		(void) remove(control_filename);
		return PV_ERROREXIT_REMOTE_OR_PID;
	}

	debug("%s", "message sent");

	/*
	 * Wait for a signal from the remote process to say it has received
	 * the message.
	 */

	timeout = 1100000;
	received = false;

	while (timeout > 10000 && !received) {
		struct timeval tv;

		memset(&tv, 0, sizeof(tv));
		tv.tv_sec = 0;
		tv.tv_usec = 10000;
		/*@-nullpass@ *//* splint: NULL is OK with select() */
		(void) select(0, NULL, NULL, NULL, &tv);
		/*@+nullpass@ */
		timeout -= 10000;

		if (pv_sigusr2_received(state, &signal_sender)) {
			if (signal_sender == opts->remote) {
				debug("%s", "message received");
				received = true;
			}
		}
	}

	/*
	 * Remove the remote control file.
	 */
	if (0 != remove(control_filename)) {
		pv_error(state, "%s", strerror(errno));
	}

	/*
	 * Return 0 if the message was received.
	 */
	if (received)
		return 0;

	/*@-mustfreefresh@ */
	/*
	 * splint note: the gettext calls made by _() cause memory leak
	 * warnings, but in this case it's unavoidable, and mitigated by the
	 * fact we only translate each string once.
	 */
	pv_error(state, "%u: %s", opts->remote, _("message not received"));
	return PV_ERROREXIT_REMOTE_OR_PID;
	/*@+mustfreefresh @ */
}


/*
 * Check for a remote control message and, if there is one, replace the
 * current process's options with those being passed in.
 *
 * NB relies on pv_state_set_format() causing the output format to be
 * reparsed.
 */
void pv_remote_check(pvstate_t state)
{
	pid_t signal_sender;
	char control_filename[4096];	 /* flawfinder: ignore */
	FILE *control_fptr;
	struct remote_msg msgbuf;

	/* flawfinder rationale: as above. */

	/*
	 * Return early if a SIGUSR2 signal has not been received.
	 */
	signal_sender = 0;
	if (!pv_sigusr2_received(state, &signal_sender))
		return;

	memset(control_filename, 0, sizeof(control_filename));
	control_fptr = pv__control_file(control_filename, sizeof(control_filename), signal_sender, false);
	if (NULL == control_fptr) {
		pv_error(state, "%s: %s", control_filename, strerror(errno));
		return;
	}

	/*
	 * Read the message buffer from the remote control file, and close
	 * it.
	 */
	if (1 != fread(&msgbuf, sizeof(msgbuf), 1, control_fptr)) {
		pv_error(state, "%s", strerror(errno));
		(void) fclose(control_fptr);
		return;
	}

	if (0 != fclose(control_fptr)) {
		pv_error(state, "%s", strerror(errno));
		return;
	}

	/*
	 * Send a SIGUSR2 signal to the sending process, to tell it the
	 * message has been received.
	 */
	if (kill(signal_sender, SIGUSR2) != 0) {
		debug("%u: %s", signal_sender, strerror(errno));
	}

	debug("%s", "received remote message");

	pv_state_format_string_set(state, NULL);
	pv_state_name_set(state, NULL);
	pv_state_extra_display_set(state, NULL);

	msgbuf.name[sizeof(msgbuf.name) - 1] = '\0';
	msgbuf.format[sizeof(msgbuf.format) - 1] = '\0';
	msgbuf.extra_display[sizeof(msgbuf.extra_display) - 1] = '\0';

	pv_state_set_format(state, msgbuf.progress, msgbuf.timer,
			    msgbuf.eta, msgbuf.fineta, msgbuf.rate,
			    msgbuf.average_rate,
			    msgbuf.bytes, msgbuf.bufpercent,
			    msgbuf.lastwritten, '\0' == msgbuf.name[0] ? NULL : msgbuf.name);

	if (msgbuf.rate_limit > 0)
		pv_state_rate_limit_set(state, msgbuf.rate_limit);
	if (msgbuf.buffer_size > 0) {
		pv_state_target_buffer_size_set(state, msgbuf.buffer_size);
	}
	if (msgbuf.size > 0)
		pv_state_size_set(state, msgbuf.size);
	if (msgbuf.interval > 0)
		pv_state_interval_set(state, msgbuf.interval);
	if (msgbuf.width > 0 && msgbuf.width_set_manually)
		pv_state_width_set(state, msgbuf.width, msgbuf.width_set_manually);
	if (msgbuf.height > 0 && msgbuf.height_set_manually)
		pv_state_height_set(state, msgbuf.height, msgbuf.height_set_manually);
	if (msgbuf.format[0] != '\0')
		pv_state_format_string_set(state, msgbuf.format);
	if (msgbuf.extra_display[0] != '\0')
		pv_state_extra_display_set(state, msgbuf.extra_display);
}


/*
 * Initialise remote message reception handling.
 */
void pv_remote_init(void)
{
}


/*
 * Clean up after remote message reception handling.
 */
void pv_remote_fini(void)
{
}

#else				/* !PV_REMOTE_CONTROL */

/*
 * Dummy stubs for remote control when we don't have PV_REMOTE_CONTROL.
 */
void pv_remote_init(void)
{
}

void pv_remote_check( /*@unused@ */  __attribute__((unused)) pvstate_t state)
{
}

void pv_remote_fini(void)
{
}

int pv_remote_set(			 /*@unused@ */
			 __attribute__((unused)) opts_t opts, /*@unused@ */  __attribute__((unused)) pvstate_t state)
{
	/*@-mustfreefresh@ *//* splint - see above */
	fprintf(stderr, "%s\n", _("SA_SIGINFO not supported on this system"));
	/*@+mustfreefresh@ */
	return PV_ERROREXIT_REMOTE_OR_PID;
}

#endif				/* SA_SIGINFO */
