/* vi: set sw=4 ts=4: */
/*
 * bare bones chat utility
 * inspired by ppp's chat
 *
 * Copyright (C) 2008 by Vladimir Dronnikov <dronnikov@gmail.com>
 *
 * Licensed under GPLv2, see file LICENSE in this source tree.
 */
//config:config CHAT
//config:	bool "chat (6.3 kb)"
//config:	default y
//config:	help
//config:	Simple chat utility.
//config:
//config:config FEATURE_CHAT_NOFAIL
//config:	bool "Enable NOFAIL expect strings"
//config:	depends on CHAT
//config:	default y
//config:	help
//config:	When enabled expect strings which are started with a dash trigger
//config:	no-fail mode. That is when expectation is not met within timeout
//config:	the script is not terminated but sends next SEND string and waits
//config:	for next EXPECT string. This allows to compose far more flexible
//config:	scripts.
//config:
//config:config FEATURE_CHAT_TTY_HIFI
//config:	bool "Force STDIN to be a TTY"
//config:	depends on CHAT
//config:	default n
//config:	help
//config:	Original chat always treats STDIN as a TTY device and sets for it
//config:	so-called raw mode. This option turns on such behaviour.
//config:
//config:config FEATURE_CHAT_IMPLICIT_CR
//config:	bool "Enable implicit Carriage Return"
//config:	depends on CHAT
//config:	default y
//config:	help
//config:	When enabled make chat to terminate all SEND strings with a "\r"
//config:	unless "\c" is met anywhere in the string.
//config:
//config:config FEATURE_CHAT_SWALLOW_OPTS
//config:	bool "Swallow options"
//config:	depends on CHAT
//config:	default y
//config:	help
//config:	Busybox chat require no options. To make it not fail when used
//config:	in place of original chat (which has a bunch of options) turn
//config:	this on.
//config:
//config:config FEATURE_CHAT_SEND_ESCAPES
//config:	bool "Support weird SEND escapes"
//config:	depends on CHAT
//config:	default y
//config:	help
//config:	Original chat uses some escape sequences in SEND arguments which
//config:	are not sent to device but rather performs special actions.
//config:	E.g. "\K" means to send a break sequence to device.
//config:	"\d" delays execution for a second, "\p" -- for a 1/100 of second.
//config:	Before turning this option on think twice: do you really need them?
//config:
//config:config FEATURE_CHAT_VAR_ABORT_LEN
//config:	bool "Support variable-length ABORT conditions"
//config:	depends on CHAT
//config:	default y
//config:	help
//config:	Original chat uses fixed 50-bytes length ABORT conditions. Say N here.
//config:
//config:config FEATURE_CHAT_CLR_ABORT
//config:	bool "Support revoking of ABORT conditions"
//config:	depends on CHAT
//config:	default y
//config:	help
//config:	Support CLR_ABORT directive.

//applet:IF_CHAT(APPLET(chat, BB_DIR_USR_SBIN, BB_SUID_DROP))

//kbuild:lib-$(CONFIG_CHAT) += chat.o

//usage:#define chat_trivial_usage
//usage:       "EXPECT [SEND [EXPECT [SEND...]]]"
//usage:#define chat_full_usage "\n\n"
//usage:       "Useful for interacting with a modem connected to stdin/stdout.\n"
//usage:       "A script consists of \"expect-send\" argument pairs.\n"
//usage:       "Example:\n"
//usage:       "chat '' ATZ OK ATD123456 CONNECT '' ogin: pppuser word: ppppass '~'"

#include "libbb.h"
#include "common_bufsiz.h"

// default timeout: 45 sec
#define DEFAULT_CHAT_TIMEOUT 45*1000
// max length of "abort string",
// i.e. device reply which causes termination
#define MAX_ABORT_LEN 50

// possible exit codes
enum {
	ERR_OK = 0,     // all's well
	ERR_MEM,        // read too much while expecting
	ERR_IO,         // signalled or I/O error
	ERR_TIMEOUT,    // timed out while expecting
	ERR_ABORT,      // first abort condition was met
//	ERR_ABORT2,     // second abort condition was met
//	...
};

// exit code
#define exitcode bb_got_signal

// trap for critical signals
static void signal_handler(UNUSED_PARAM int signo)
{
	// report I/O error condition
	exitcode = ERR_IO;
}

#if !ENABLE_FEATURE_CHAT_IMPLICIT_CR
#define unescape(s, nocr) unescape(s)
#endif
static size_t unescape(char *s, int *nocr)
{
	char *start = s;
	char *p = s;

	while (*s) {
		char c = *s;
		// do we need special processing?
		// standard escapes + \s for space and \N for \0
		// \c inhibits terminating \r for commands and is noop for expects
		if ('\\' == c) {
			c = *++s;
			if (c) {
#if ENABLE_FEATURE_CHAT_IMPLICIT_CR
				if ('c' == c) {
					*nocr = 1;
					goto next;
				}
#endif
				if ('N' == c) {
					c = '\0';
				} else if ('s' == c) {
					c = ' ';
#if ENABLE_FEATURE_CHAT_NOFAIL
				// unescape leading dash only
				// TODO: and only for expect, not command string
				} else if ('-' == c && (start + 1 == s)) {
					//c = '-';
#endif
				} else {
					c = bb_process_escape_sequence((const char **)&s);
					s--;
				}
			}
		// ^A becomes \001, ^B -- \002 and so on...
		} else if ('^' == c) {
			c = *++s-'@';
		}
		// put unescaped char
		*p++ = c;
#if ENABLE_FEATURE_CHAT_IMPLICIT_CR
 next:
#endif
		// next char
		s++;
	}
	*p = '\0';

	return p - start;
}

int chat_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE;
int chat_main(int argc UNUSED_PARAM, char **argv)
{
	int record_fd = -1;
	bool echo = 0;
	// collection of device replies which cause unconditional termination
	llist_t *aborts = NULL;
	// inactivity period
	int timeout = DEFAULT_CHAT_TIMEOUT;
	// maximum length of abort string
#if ENABLE_FEATURE_CHAT_VAR_ABORT_LEN
	size_t max_abort_len = 0;
#else
#define max_abort_len MAX_ABORT_LEN
#endif
#if ENABLE_FEATURE_CHAT_TTY_HIFI
	struct termios tio0, tio;
#endif
	// directive names
	enum {
		DIR_HANGUP = 0,
		DIR_ABORT,
#if ENABLE_FEATURE_CHAT_CLR_ABORT
		DIR_CLR_ABORT,
#endif
		DIR_TIMEOUT,
		DIR_ECHO,
		DIR_SAY,
		DIR_RECORD,
	};

	// make x* functions fail with correct exitcode
	xfunc_error_retval = ERR_IO;

	// trap vanilla signals to prevent process from being killed suddenly
	bb_signals(0
		+ (1 << SIGHUP)
		+ (1 << SIGINT)
		+ (1 << SIGTERM)
		+ (1 << SIGPIPE)
		, signal_handler);

#if ENABLE_FEATURE_CHAT_TTY_HIFI
//TODO: use set_termios_to_raw()
	tcgetattr(STDIN_FILENO, &tio);
	tio0 = tio;
	cfmakeraw(&tio);
	tcsetattr(STDIN_FILENO, TCSAFLUSH, &tio);
#endif

#if ENABLE_FEATURE_CHAT_SWALLOW_OPTS
	getopt32(argv, "vVsSE");
	argv += optind;
#else
	argv++; // goto first arg
#endif
	// handle chat expect-send pairs
	while (*argv) {
		// directive given? process it
		int key = index_in_strings(
			"HANGUP\0" "ABORT\0"
#if ENABLE_FEATURE_CHAT_CLR_ABORT
			"CLR_ABORT\0"
#endif
			"TIMEOUT\0" "ECHO\0" "SAY\0" "RECORD\0"
			, *argv
		);
		if (key >= 0) {
			bool onoff;
			// cache directive value
			char *arg = *++argv;

			if (!arg) {
#if ENABLE_FEATURE_CHAT_TTY_HIFI
				tcsetattr(STDIN_FILENO, TCSAFLUSH, &tio0);
#endif
				bb_show_usage();
			}
			// OFF -> 0, anything else -> 1
			onoff = (0 != strcmp("OFF", arg));
			// process directive
			if (DIR_HANGUP == key) {
				// turn SIGHUP on/off
				signal(SIGHUP, onoff ? signal_handler : SIG_IGN);
			} else if (DIR_ABORT == key) {
				// append the string to abort conditions
#if ENABLE_FEATURE_CHAT_VAR_ABORT_LEN
				size_t len = strlen(arg);
				if (len > max_abort_len)
					max_abort_len = len;
#endif
				llist_add_to_end(&aborts, arg);
#if ENABLE_FEATURE_CHAT_CLR_ABORT
			} else if (DIR_CLR_ABORT == key) {
				llist_t *l;
				// remove the string from abort conditions
				// N.B. gotta refresh maximum length too...
# if ENABLE_FEATURE_CHAT_VAR_ABORT_LEN
				max_abort_len = 0;
# endif
				for (l = aborts; l; l = l->link) {
# if ENABLE_FEATURE_CHAT_VAR_ABORT_LEN
					size_t len = strlen(l->data);
# endif
					if (strcmp(arg, l->data) == 0) {
						llist_unlink(&aborts, l);
						continue;
					}
# if ENABLE_FEATURE_CHAT_VAR_ABORT_LEN
					if (len > max_abort_len)
						max_abort_len = len;
# endif
				}
#endif
			} else if (DIR_TIMEOUT == key) {
				// set new timeout
				// -1 means OFF
				timeout = atoi(arg) * 1000;
				// 0 means default
				// >0 means value in msecs
				if (!timeout)
					timeout = DEFAULT_CHAT_TIMEOUT;
			} else if (DIR_ECHO == key) {
				// turn echo on/off
				// N.B. echo means dumping device input/output to stderr
				echo = onoff;
			} else if (DIR_RECORD == key) {
				// turn record on/off
				// N.B. record means dumping device input to a file
					// close previous record_fd
				if (record_fd > 0)
					close(record_fd);
				// N.B. do we have to die here on open error?
				record_fd = (onoff) ? xopen(arg, O_WRONLY|O_CREAT|O_TRUNC) : -1;
			} else if (DIR_SAY == key) {
				// just print argument verbatim
				// TODO: should we use full_write() to avoid unistd/stdio conflict?
				bb_error_msg("%s", arg);
			}
			// next, please!
			argv++;
		// ordinary expect-send pair!
		} else {
			//-----------------------
			// do expect
			//-----------------------
			int expect_len;
			size_t buf_len = 0;
			size_t max_len = max_abort_len;

			struct pollfd pfd;
#if ENABLE_FEATURE_CHAT_NOFAIL
			int nofail = 0;
#endif
			char *expect = *argv++;

			// sanity check: shall we really expect something?
			if (!expect)
				goto expect_done;

#if ENABLE_FEATURE_CHAT_NOFAIL
			// if expect starts with -
			if ('-' == *expect) {
				// swallow -
				expect++;
				// and enter nofail mode
				nofail++;
			}
#endif

#ifdef ___TEST___BUF___ // test behaviour with a small buffer
#	undef COMMON_BUFSIZE
#	define COMMON_BUFSIZE 6
#endif
			// expand escape sequences in expect
			expect_len = unescape(expect, &expect_len /*dummy*/);
			if (expect_len > max_len)
				max_len = expect_len;
			// sanity check:
			// we should expect more than nothing but not more than input buffer
			// TODO: later we'll get rid of fixed-size buffer
			if (!expect_len)
				goto expect_done;
			if (max_len >= COMMON_BUFSIZE) {
				exitcode = ERR_MEM;
				goto expect_done;
			}

			// get reply
			pfd.fd = STDIN_FILENO;
			pfd.events = POLLIN;
			while (!exitcode
			    && poll(&pfd, 1, timeout) > 0
			    && (pfd.revents & POLLIN)
			) {
				llist_t *l;
				ssize_t delta;
#define buf bb_common_bufsiz1
				setup_common_bufsiz();

				// read next char from device
				if (safe_read(STDIN_FILENO, buf+buf_len, 1) > 0) {
					// dump device input if RECORD fname
					if (record_fd > 0) {
						full_write(record_fd, buf+buf_len, 1);
					}
					// dump device input if ECHO ON
					if (echo) {
//						if (buf[buf_len] < ' ') {
//							full_write(STDERR_FILENO, "^", 1);
//							buf[buf_len] += '@';
//						}
						full_write(STDERR_FILENO, buf+buf_len, 1);
					}
					buf_len++;
					// move input frame if we've reached higher bound
					if (buf_len > COMMON_BUFSIZE) {
						memmove(buf, buf+buf_len-max_len, max_len);
						buf_len = max_len;
					}
				}
				// N.B. rule of thumb: values being looked for can
				// be found only at the end of input buffer
				// this allows to get rid of strstr() and memmem()

				// TODO: make expect and abort strings processed uniformly
				// abort condition is met? -> bail out
				for (l = aborts, exitcode = ERR_ABORT; l; l = l->link, ++exitcode) {
					size_t len = strlen(l->data);
					delta = buf_len-len;
					if (delta >= 0 && !memcmp(buf+delta, l->data, len))
						goto expect_done;
				}
				exitcode = ERR_OK;

				// expected reply received? -> goto next command
				delta = buf_len - expect_len;
				if (delta >= 0 && !memcmp(buf+delta, expect, expect_len))
					goto expect_done;
#undef buf
			} /* while (have data) */

			// device timed out or unexpected reply received
			exitcode = ERR_TIMEOUT;
 expect_done:
#if ENABLE_FEATURE_CHAT_NOFAIL
			// on success and when in nofail mode
			// we should skip following subsend-subexpect pairs
			if (nofail) {
				if (!exitcode) {
					// find last send before non-dashed expect
					while (*argv && argv[1] && '-' == argv[1][0])
						argv += 2;
					// skip the pair
					// N.B. do we really need this?!
					if (!*argv++ || !*argv++)
						break;
				}
				// nofail mode also clears all but IO errors (or signals)
				if (ERR_IO != exitcode)
					exitcode = ERR_OK;
			}
#endif
			// bail out unless we expected successfully
			if (exitcode)
				break;

			//-----------------------
			// do send
			//-----------------------
			if (*argv) {
#if ENABLE_FEATURE_CHAT_IMPLICIT_CR
				int nocr = 0; // inhibit terminating command with \r
#endif
				char *loaded = NULL; // loaded command
				size_t len;
				char *buf = *argv++;

				// if command starts with @
				// load "real" command from file named after @
				if ('@' == *buf) {
					// skip the @ and any following white-space
					trim(++buf);
					buf = loaded = xmalloc_xopen_read_close(buf, NULL);
				}
				// expand escape sequences in command
				len = unescape(buf, &nocr);

				// send command
				alarm(timeout);
				pfd.fd = STDOUT_FILENO;
				pfd.events = POLLOUT;
				while (len && !exitcode
				    && poll(&pfd, 1, -1) > 0
				    && (pfd.revents & POLLOUT)
				) {
#if ENABLE_FEATURE_CHAT_SEND_ESCAPES
					// "\\d" means 1 sec delay, "\\p" means 0.01 sec delay
					// "\\K" means send BREAK
					char c = *buf;
					if ('\\' == c) {
						c = *++buf;
						if ('d' == c) {
							sleep(1);
							len--;
							continue;
						}
						if ('p' == c) {
							usleep(10000);
							len--;
							continue;
						}
						if ('K' == c) {
							tcsendbreak(STDOUT_FILENO, 0);
							len--;
							continue;
						}
						buf--;
					}
					if (safe_write(STDOUT_FILENO, buf, 1) != 1)
						break;
					len--;
					buf++;
#else
					len -= full_write(STDOUT_FILENO, buf, len);
#endif
				} /* while (can write) */
				alarm(0);

				// report I/O error if there still exists at least one non-sent char
				if (len)
					exitcode = ERR_IO;

				// free loaded command (if any)
				if (loaded)
					free(loaded);
#if ENABLE_FEATURE_CHAT_IMPLICIT_CR
				// or terminate command with \r (if not inhibited)
				else if (!nocr)
					xwrite(STDOUT_FILENO, "\r", 1);
#endif
				// bail out unless we sent command successfully
				if (exitcode)
					break;
			} /* if (*argv) */
		}
	} /* while (*argv) */

#if ENABLE_FEATURE_CHAT_TTY_HIFI
	tcsetattr(STDIN_FILENO, TCSAFLUSH, &tio0);
#endif

	return exitcode;
}
