chat: now with "svn add"

diff --git a/miscutils/chat.c b/miscutils/chat.c
new file mode 100644
index 0000000..4f55738
--- /dev/null
+++ b/miscutils/chat.c
@@ -0,0 +1,443 @@
+/* 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 tarball for details.
+ */
+#include "libbb.h"
+
+/*
+#define ENABLE_FEATURE_CHAT_NOFAIL              1 // +126 bytes
+#define ENABLE_FEATURE_CHAT_TTY_HIFI            0 // + 70 bytes
+#define ENABLE_FEATURE_CHAT_IMPLICIT_CR         1 // + 44 bytes
+#define ENABLE_FEATURE_CHAT_SEND_ESCAPES        0 // +103 bytes
+#define ENABLE_FEATURE_CHAT_VAR_ABORT_LEN       0 // + 70 bytes
+#define ENABLE_FEATURE_CHAT_CLR_ABORT           0 // +113 bytes
+#define ENABLE_FEATURE_CHAT_SWALLOW_OPTS        0 // + 23 bytes
+*/
+
+// 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
+// N.B> 10 bytes for volatile. Why all these signals?!
+static /*volatile*/ smallint exitcode;
+
+// trap for critical signals
+static void signal_handler(ATTRIBUTE_UNUSED 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, char **argv)
+{
+// should we dump device output? to what fd? by default no.
+// this can be controlled later via ECHO {ON|OFF} chat directive
+//	int echo_fd;
+	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,
+	};
+
+	// 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
+	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"
+			, *argv
+		);
+		if (key >= 0) {
+			// cache directive value
+			char *arg = *++argv;
+			// ON -> 1, anything else -> 0
+			bool onoff = !strcmp("ON", 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) {
+				// 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 (llist_t *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)) {
+						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 output
+				// from stdin (device) to stderr
+				echo = onoff;
+//TODO?				echo_fd = onoff * STDERR_FILENO;
+//TODO?				echo_fd = xopen(arg, O_WRONLY|O_CREAT|O_TRUNC);
+			} else if (DIR_SAY == key) {
+				// just print argument verbatim
+				fprintf(stderr, arg);
+			}
+			// next, please!
+			argv++;
+		// ordinary expect-send pair!
+		} else {
+			//-----------------------
+			// do expect
+			//-----------------------
+			size_t expect_len, 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)
+			) {
+#define buf bb_common_bufsiz1
+				llist_t *l;
+				ssize_t delta;
+
+				// read next char from device
+				if (safe_read(STDIN_FILENO, buf+buf_len, 1) > 0) {
+					// dump device output if ECHO ON or RECORD fname
+//TODO?					if (echo_fd > 0) {
+//TODO?						full_write(echo_fd, buf+buf_len, 1);
+//TODO?					}
+					if (echo > 0)
+						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
+			}
+
+			// 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_open_read_close(buf, NULL);
+				}
+
+				// expand escape sequences in command
+				len = unescape(buf, &nocr);
+
+				// send command
+#if ENABLE_FEATURE_CHAT_SEND_ESCAPES
+				pfd.fd = STDOUT_FILENO;
+				pfd.events = POLLOUT;
+				while (len && !exitcode
+				    && poll(&pfd, 1, timeout) > 0
+				    && (pfd.revents & POLLOUT)
+				) {
+					// ugly! ugly! ugly!
+					// gotta send char by char to achieve this!
+					// Brrr...
+					// "\\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;
+						} else if ('p' == c) {
+							usleep(10000);
+							len--;
+							continue;
+						} else if ('K' == c) {
+							tcsendbreak(STDOUT_FILENO, 0);
+							len--;
+							continue;
+						} else {
+							buf--;
+						}
+					}
+					if (safe_write(STDOUT_FILENO, buf, 1) > 0) {
+						len--;
+						buf++;
+					} else
+						break;
+				}
+#else
+//				if (len) {
+					alarm(timeout);
+					len -= full_write(STDOUT_FILENO, buf, len);
+					alarm(0);
+//				}
+#endif
+
+				// 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 ENABLE_FEATURE_CHAT_TTY_HIFI
+	tcsetattr(STDIN_FILENO, TCSAFLUSH, &tio0);
+#endif
+
+	return exitcode;
+}