~rcr/rirc

36afe3c5a351ad9f7faec53571ba44a56dc61b31 — Richard Robbins 20 days ago 8bf28d3
add :connect command args
4 files changed, 311 insertions(+), 117 deletions(-)

M src/rirc.h
M src/state.c
M test/rirc.mock.c
M test/state.c
M src/rirc.h => src/rirc.h +1 -1
@@ 4,7 4,7 @@
/* Default config values obtained at runtime */

extern const char *ca_cert_path;
extern const char *default_nick_set;
extern const char *default_nicks;
extern const char *default_username;
extern const char *default_realname;
extern const char *runtime_name;

M src/state.c => src/state.c +136 -28
@@ 589,56 589,164 @@ state_complete(char *str, uint16_t len, uint16_t max, int first)
static void
command(struct channel *c, char *buf)
{
	const char *arg;
	const char *cmd;
	int err;
	const char *str;

	if (!(cmd = irc_strsep(&buf)))
	if (!(str = irc_strsep(&buf)))
		return;

	if (!strcasecmp(cmd, "clear")) {
		if ((arg = irc_strsep(&buf))) {
			action(action_error, "clear: Unknown arg '%s'", arg);
	if (!strcasecmp(str, "clear")) {
		if ((str = irc_strsep(&buf))) {
			action(action_error, "clear: Unknown arg '%s'", str);
			return;
		}
		state_channel_clear(0);
		return;
	}

	if (!strcasecmp(cmd, "close")) {
		if ((arg = irc_strsep(&buf))) {
			action(action_error, "close: Unknown arg '%s'", arg);
	if (!strcasecmp(str, "close")) {
		if ((str = irc_strsep(&buf))) {
			action(action_error, "close: Unknown arg '%s'", str);
			return;
		}
		state_channel_close(0);
		return;
	}

	if (!strcasecmp(cmd, "connect")) {
		if (!c->server) {
			action(action_error, "connect: This is not a server");
			return;
		}
	/* :connect [hostname [options]] */
	if (!strcasecmp(str, "connect")) {

		if ((arg = irc_strsep(&buf))) {
			action(action_error, "connect: Unknown arg '%s'", arg);
			return;
		}
		if (!(str = irc_strsep(&buf))) {

			int err;

		if ((err = io_cx(c->server->connection)))
			action(action_error, "connect: %s", io_err(err));
			if (!c->server) {
				action(action_error, "connect: This is not a server");
			} else if ((err = io_cx(c->server->connection))) {
				action(action_error, "connect: %s", io_err(err));
			}

		} else {

			int ret;
			struct server *s;

			const char *host     = str;
			const char *port     = NULL;
			const char *pass     = NULL;
			const char *username = default_username;
			const char *realname = default_realname;
			const char *nicks    = default_nicks;
			const char *chans    = NULL;
			int ipv      = IO_IPV_UNSPEC;
			int tls      = IO_TLS_ENABLED;
			int tls_vrfy = IO_TLS_VRFY_REQUIRED;

			while ((str = irc_strsep(&buf))) {

				if (*str != '-') {
					action(action_error, ":connect [hostname [options]]");
					return;
				} else if (!strcmp(str, "-p") || !strcmp(str, "--port")) {
					if (!(port = irc_strsep(&buf))) {
						action(action_error, "connect: '-p/--port' requires an argument");
						return;
					}
				} else if (!strcmp(str, "-w") || !strcmp(str, "--pass")) {
					if (!(pass = irc_strsep(&buf))) {
						action(action_error, "connect: '-w/--pass' requires an argument");
						return;
					}
				} else if (!strcmp(str, "-u") || !strcmp(str, "--username")) {
					if (!(username = irc_strsep(&buf))) {
						action(action_error, "connect: '-u/--username' requires an argument");
						return;
					}
				} else if (!strcmp(str, "-r") || !strcmp(str, "--realname")) {
					if (!(realname = irc_strsep(&buf))) {
						action(action_error, "connect: '-r/--realname' requires an argument");
						return;
					}
				} else if (!strcmp(str, "-n") || !strcmp(str, "--nicks")) {
					if (!(nicks = irc_strsep(&buf))) {
						action(action_error, "connect: '-n/--nicks' requires an argument");
						return;
					}
				} else if (!strcmp(str, "-c") || !strcmp(str, "--chans")) {
					if (!(chans = irc_strsep(&buf))) {
						action(action_error, "connect: '-c/--chans' requires an argument");
						return;
					}
				} else if (!strcmp(str, "--ipv4")) {
					ipv = IO_IPV_4;
				} else if (!strcmp(str, "--ipv6")) {
					ipv = IO_IPV_6;
				} else if (!strcmp(str, "--tls-disable")) {
					tls = IO_TLS_DISABLED;
				} else if (!strcmp(str, "--tls-verify")) {
					if (!(str = irc_strsep(&buf))) {
						action(action_error, "connect: '--tls-verify' requires an argument");
						return;
					} else if (!strcmp(str, "0") || !strcmp(str, "disabled")) {
						tls_vrfy = IO_TLS_VRFY_DISABLED;
					} else if (!strcmp(str, "1") || !strcmp(str, "optional")) {
						tls_vrfy = IO_TLS_VRFY_OPTIONAL;
					} else if (!strcmp(str, "2") || !strcmp(str, "required")) {
						tls_vrfy = IO_TLS_VRFY_REQUIRED;
					} else {
						action(action_error, "connect: invalid option for '--tls-verify' '%s'", str);
						return;
					}
				} else {
					action(action_error, "connect: unknown option '%s'", str);
					return;
				}
			}

			if (port == NULL)
				port = (tls == IO_TLS_ENABLED) ? "6697" : "6667";

			s = server(host, port, pass, username, realname);

			if (nicks && server_set_nicks(s, nicks)) {
				action(action_error, "connect: invalid -n/--nicks: '%s'", nicks);
				server_free(s);
				return;
			}

			if (chans && server_set_chans(s, chans)) {
				action(action_error, "connect: invalid -c/--chans: '%s'", chans);
				server_free(s);
				return;
			}

			if (server_list_add(state_server_list(), s)) {
				action(action_error, "connect: duplicate server: %s:%s", host, port);
				server_free(s);
				return;
			}

			s->connection = connection(s, host, port, (ipv | tls | tls_vrfy));

			if ((ret = io_cx(s->connection)))
				server_error(s, "failed to connect: %s", io_err(ret));

			channel_set_current(s->channel);
		}

		return;
	}

	if (!strcasecmp(cmd, "disconnect")) {
	if (!strcasecmp(str, "disconnect")) {

		int err;

		if (!c->server) {
			action(action_error, "disconnect: This is not a server");
			return;
		}

		if ((arg = irc_strsep(&buf))) {
			action(action_error, "disconnect: Unknown arg '%s'", arg);
		if ((str = irc_strsep(&buf))) {
			action(action_error, "disconnect: Unknown arg '%s'", str);
			return;
		}



@@ 648,9 756,9 @@ command(struct channel *c, char *buf)
		return;
	}

	if (!strcasecmp(cmd, "quit")) {
		if ((arg = irc_strsep(&buf))) {
			action(action_error, "quit: Unknown arg '%s'", arg);
	if (!strcasecmp(str, "quit")) {
		if ((str = irc_strsep(&buf))) {
			action(action_error, "quit: Unknown arg '%s'", str);
			return;
		}



@@ 658,7 766,7 @@ command(struct channel *c, char *buf)
		return;
	}

	action(action_error, "Unknown command '%s'", cmd);
	action(action_error, "Unknown command '%s'", str);
}

static int

M test/rirc.mock.c => test/rirc.mock.c +1 -0
@@ 1,2 1,3 @@
const char *default_username = "username";
const char *default_realname = "realname";
const char *default_nicks = "n1,n2,n3";

M test/state.c => test/state.c +173 -88
@@ 19,6 19,12 @@
#define INP_S(S) io_cb_read_inp((S), strlen(S))
#define INP_C(C) io_cb_read_inp((char[]){(C)}, 1)

#define INP_COMMAND(C) \
	do { \
		io_cb_read_inp((C), strlen(C)); \
		io_cb_read_inp("\012", 1); \
	} while (0)

#define CURRENT_LINE \
	(buffer_head(&current_channel()->buffer) ? \
	 buffer_head(&current_channel()->buffer)->text : NULL)


@@ 26,42 32,23 @@
static void
test_command(void)
{
	state_init();

	INP_S(":unknown command with args");
	INP_C(0x0A);
	INP_COMMAND(":unknown command with args");

	assert_strcmp(action_message(), "Unknown command 'unknown'");

	/* clear error */
	INP_C(0x0A);

	state_term();
}

static void
test_command_clear(void)
{
	state_init();

	assert_strcmp(CURRENT_LINE, " - compiled with DEBUG flags");

	INP_S(":clear with args");
	INP_C(0x0A);

	assert_strcmp(action_message(), "clear: Unknown arg 'with'");

	/* clear error */
	INP_C(0x0A);

	assert_strcmp(CURRENT_LINE, " - compiled with DEBUG flags");

	INP_S(":clear");
	INP_C(0x0A);
	INP_COMMAND(":clear");

	assert_ptr_null(CURRENT_LINE);

	state_term();
	INP_COMMAND(":clear with args");

	assert_strcmp(action_message(), "clear: Unknown arg 'with'");
}

static void


@@ 75,12 62,9 @@ test_command_close(void)
	struct server *s1;
	struct server *s2;

	state_init();

	assert_strcmp(CURRENT_LINE, " - compiled with DEBUG flags");

	INP_S(":close");
	INP_C(0x0A);
	INP_COMMAND(":close");

	assert_strcmp(action_message(), "Type :quit to exit rirc");



@@ 119,8 103,7 @@ test_command_close(void)

	channel_set_current(c1);

	INP_S(":close with args");
	INP_C(0x0A);
	INP_COMMAND(":close with args");

	assert_strcmp(action_message(), "close: Unknown arg 'with'");



@@ 130,8 113,7 @@ test_command_close(void)
	/* test closing last channel on server */
	channel_set_current(c2);

	INP_S(":close");
	INP_C(0x0A);
	INP_COMMAND(":close");

	assert_ptr_null(action_handler);
	assert_ptr_null(action_message());


@@ 140,8 122,7 @@ test_command_close(void)
	/* test closing server channel */
	channel_set_current(s1->channel);

	INP_S(":close");
	INP_C(0x0A);
	INP_COMMAND(":close");

	assert_ptr_null(action_handler);
	assert_ptr_null(action_message());


@@ 150,8 131,7 @@ test_command_close(void)
	/* test closing middle channel*/
	channel_set_current(c3);

	INP_S(":close");
	INP_C(0x0A);
	INP_COMMAND(":close");

	assert_ptr_null(action_handler);
	assert_ptr_null(action_message());


@@ 160,14 140,11 @@ test_command_close(void)
	/* test closing last channel*/
	channel_set_current(c5);

	INP_S(":close");
	INP_C(0x0A);
	INP_COMMAND(":close");

	assert_ptr_null(action_handler);
	assert_ptr_null(action_message());
	assert_strcmp(current_channel()->name, "#c4");

	state_term();
}

static void


@@ 175,56 152,165 @@ test_command_connect(void)
{
	struct server *s;

	mock_reset_io();
	INP_COMMAND(":connect");

	state_init();
	assert_strcmp(CURRENT_LINE, " - compiled with DEBUG flags");
	assert_strcmp(action_message(), "connect: This is not a server");
	INP_C(0x0A);

	INP_S(":connect");
	INP_COMMAND(":connect host-1");

	assert_ptr_null(action_handler);
	assert_ptr_null(action_message());
	assert_strcmp(current_channel()->name, "host-1");

	current_channel()->server->connected = 1;

	INP_COMMAND(":connect");

	assert_strcmp(action_message(), "connect: cxed");
	INP_C(0x0A);

	assert_strcmp(CURRENT_LINE, " - compiled with DEBUG flags");
	assert_strcmp(action_message(), "connect: This is not a server");
	/* Test empty arguments */
	INP_COMMAND(":connect host -p");
	assert_strcmp(action_message(), "connect: '-p/--port' requires an argument");
	assert_strcmp(current_channel()->name, "host-1");
	INP_C(0x0A);

	/* clear error */
	INP_COMMAND(":connect host --port");
	assert_strcmp(action_message(), "connect: '-p/--port' requires an argument");
	assert_strcmp(current_channel()->name, "host-1");
	INP_C(0x0A);

	if (!(s = server("host", "port", NULL, "user", "real")))
		test_abort("Failed test setup");
	INP_COMMAND(":connect host -w");
	assert_strcmp(action_message(), "connect: '-w/--pass' requires an argument");
	assert_strcmp(current_channel()->name, "host-1");
	INP_C(0x0A);

	if (server_list_add(state_server_list(), s))
		test_abort("Failed to add server");
	INP_COMMAND(":connect host --pass");
	assert_strcmp(action_message(), "connect: '-w/--pass' requires an argument");
	assert_strcmp(current_channel()->name, "host-1");
	INP_C(0x0A);

	channel_set_current(s->channel);
	INP_COMMAND(":connect host -u");
	assert_strcmp(action_message(), "connect: '-u/--username' requires an argument");
	assert_strcmp(current_channel()->name, "host-1");
	INP_C(0x0A);

	INP_S(":connect");
	INP_COMMAND(":connect host --username");
	assert_strcmp(action_message(), "connect: '-u/--username' requires an argument");
	assert_strcmp(current_channel()->name, "host-1");
	INP_C(0x0A);

	assert_ptr_null(action_handler);
	assert_ptr_null(action_message());
	assert_ptr_null(CURRENT_LINE);
	INP_COMMAND(":connect host -r");
	assert_strcmp(action_message(), "connect: '-r/--realname' requires an argument");
	assert_strcmp(current_channel()->name, "host-1");
	INP_C(0x0A);

	INP_S(":connect with args");
	INP_COMMAND(":connect host --realname");
	assert_strcmp(action_message(), "connect: '-r/--realname' requires an argument");
	assert_strcmp(current_channel()->name, "host-1");
	INP_C(0x0A);

	assert_strcmp(action_message(), "connect: Unknown arg 'with'");
	INP_COMMAND(":connect host -n");
	assert_strcmp(action_message(), "connect: '-n/--nicks' requires an argument");
	assert_strcmp(current_channel()->name, "host-1");
	INP_C(0x0A);

	/* clear error */
	INP_COMMAND(":connect host --nicks");
	assert_strcmp(action_message(), "connect: '-n/--nicks' requires an argument");
	assert_strcmp(current_channel()->name, "host-1");
	INP_C(0x0A);

	INP_S(":connect");
	INP_COMMAND(":connect host -c");
	assert_strcmp(action_message(), "connect: '-c/--chans' requires an argument");
	assert_strcmp(current_channel()->name, "host-1");
	INP_C(0x0A);

	assert_strcmp(action_message(), "connect: cxed");
	INP_COMMAND(":connect host --chans");
	assert_strcmp(action_message(), "connect: '-c/--chans' requires an argument");
	assert_strcmp(current_channel()->name, "host-1");
	INP_C(0x0A);

	/* clear error */
	INP_COMMAND(":connect host --tls-verify");
	assert_strcmp(action_message(), "connect: '--tls-verify' requires an argument");
	assert_strcmp(current_channel()->name, "host-1");
	INP_C(0x0A);

	assert_ptr_null(action_handler);
	assert_ptr_null(action_message());
	assert_ptr_null(CURRENT_LINE);
	/* Test invalid arguments */
	INP_COMMAND(":connect host xyz");
	assert_strcmp(action_message(), ":connect [hostname [options]]");
	assert_strcmp(current_channel()->name, "host-1");
	INP_C(0x0A);

	state_term();
	INP_COMMAND(":connect host --xyz");
	assert_strcmp(action_message(), "connect: unknown option '--xyz'");
	assert_strcmp(current_channel()->name, "host-1");
	INP_C(0x0A);

	INP_COMMAND(":connect host --tls-verify xyz");
	assert_strcmp(action_message(), "connect: invalid option for '--tls-verify' 'xyz'");
	assert_strcmp(current_channel()->name, "host-1");
	INP_C(0x0A);

	INP_COMMAND(":connect host-1");
	assert_strcmp(action_message(), "connect: duplicate server: host-1:6697");
	assert_strcmp(current_channel()->name, "host-1");
	INP_C(0x0A);

	INP_COMMAND(":connect host --nicks 0,1,2");
	assert_strcmp(action_message(), "connect: invalid -n/--nicks: '0,1,2'");
	assert_strcmp(current_channel()->name, "host-1");
	INP_C(0x0A);

	INP_COMMAND(":connect host --chans 0,1,2");
	assert_strcmp(action_message(), "connect: invalid -c/--chans: '0,1,2'");
	assert_strcmp(current_channel()->name, "host-1");
	INP_C(0x0A);

	/* Test default values */
	INP_COMMAND(":connect host-2");

	s = current_channel()->server;

	assert_strcmp(s->host, "host-2");
	assert_strcmp(s->port, "6697");
	assert_strcmp(s->username, "username");
	assert_strcmp(s->realname, "realname");
	assert_strcmp(s->nicks.set[0], "n1");
	assert_strcmp(s->nicks.set[1], "n2");
	assert_strcmp(s->nicks.set[2], "n3");

	/* Test all values */
	INP_COMMAND(":connect host-3"
		" --port 1234"
		" --pass abcdef"
		" --username xxx"
		" --realname yyy"
		" --nicks x0,y1,z2"
		" --chans #a1,b2,#c3"
		" --ipv4"
		" --ipv6"
		" --tls-disable"
		" --tls-verify 0"
		" --tls-verify 1"
		" --tls-verify 2"
		" --tls-verify disabled"
		" --tls-verify optional"
		" --tls-verify required"
	);

	s = current_channel()->server;

	assert_strcmp(s->host, "host-3");
	assert_strcmp(s->port, "1234");
	assert_strcmp(s->username, "xxx");
	assert_strcmp(s->realname, "yyy");
	assert_strcmp(s->nicks.set[0], "x0");
	assert_strcmp(s->nicks.set[1], "y1");
	assert_strcmp(s->nicks.set[2], "z2");
	assert_ptr_not_null(channel_list_get(&(s->clist), "#a1", s->casemapping));
	assert_ptr_not_null(channel_list_get(&(s->clist), "b2", s->casemapping));
	assert_ptr_not_null(channel_list_get(&(s->clist), "#c3", s->casemapping));
}

static void


@@ 234,11 320,9 @@ test_command_disconnect(void)

	mock_reset_io();

	state_init();
	assert_strcmp(CURRENT_LINE, " - compiled with DEBUG flags");

	INP_S(":disconnect");
	INP_C(0x0A);
	INP_COMMAND(":disconnect");

	assert_strcmp(CURRENT_LINE, " - compiled with DEBUG flags");
	assert_strcmp(action_message(), "disconnect: This is not a server");


@@ 256,23 340,20 @@ test_command_disconnect(void)

	io_cx(NULL);

	INP_S(":disconnect");
	INP_C(0x0A);
	INP_COMMAND(":disconnect");

	assert_ptr_null(action_handler);
	assert_ptr_null(action_message());
	assert_ptr_null(CURRENT_LINE);

	INP_S(":disconnect with args");
	INP_C(0x0A);
	INP_COMMAND(":disconnect with args");

	assert_strcmp(action_message(), "disconnect: Unknown arg 'with'");

	/* clear error */
	INP_C(0x0A);

	INP_S(":disconnect");
	INP_C(0x0A);
	INP_COMMAND(":disconnect");

	assert_strcmp(action_message(), "disconnect: dxed");



@@ 282,37 363,27 @@ test_command_disconnect(void)
	assert_ptr_null(action_handler);
	assert_ptr_null(action_message());
	assert_ptr_null(CURRENT_LINE);

	state_term();
}

static void
test_command_quit(void)
{
	state_init();

	INP_S(":quit with args");
	INP_C(0x0A);
	INP_COMMAND(":quit with args");

	assert_strcmp(action_message(), "quit: Unknown arg 'with'");

	/* clear error */
	INP_C(0x0A);

	INP_S(":quit");
	INP_C(0x0A);
	INP_COMMAND(":quit");

	assert_ptr_null(action_handler);
	assert_ptr_null(action_message());

	state_term();
}

static void
test_state(void)
{
	state_init();

	/* Test splash message */
	assert_strcmp(CURRENT_LINE, " - compiled with DEBUG flags");



@@ 358,8 429,22 @@ test_state(void)
	assert_ptr_eq(server_list_add(state_server_list(), s1), NULL);
	assert_ptr_eq(server_list_add(state_server_list(), s2), NULL);
	assert_ptr_eq(server_list_add(state_server_list(), s3), NULL);
}

static int
test_init(void)
{
	state_init();

	return 0;
}

static int
test_term(void)
{
	state_term();

	return 0;
}

int


@@ 375,5 460,5 @@ main(void)
		TESTCASE(test_state),
	};

	return run_tests(NULL, NULL, tests);
	return run_tests(test_init, test_term, tests);
}