~rcr/rirc

7124ca72249bde0b10e229e325e30e005cc4be9e — Richard Robbins 10 months ago b01b186
Fix :connect/:disconnect segfault on non-server buffer
4 files changed, 350 insertions(+), 19 deletions(-)

M .gitignore
M src/state.c
M test/io.mock.c
M test/state.c
M .gitignore => .gitignore +1 -0
@@ 6,6 6,7 @@
*.t
*.td
.clangd
.cache
bld
compile_commands.json
config.h

M src/state.c => src/state.c +59 -13
@@ 109,12 109,17 @@ state_init(void)
void
state_term(void)
{
	/* Exit handler; must return normally */

	struct server *s1, *s2;
	struct server *s1;
	struct server *s2;

	channel_free(state.default_channel);

	state.current_channel = NULL;
	state.default_channel = NULL;

	action_handler = NULL;
	action_buff[0] = 0;

	if ((s1 = state_server_list()->head) == NULL)
		return;



@@ 124,6 129,9 @@ state_term(void)
		connection_free(s2->connection);
		server_free(s2);
	} while (s1 != state_server_list()->head);

	state.servers.head = NULL;
	state.servers.tail = NULL;
}

unsigned


@@ 615,40 623,78 @@ state_complete(char *str, uint16_t len, uint16_t max, int first)
static void
command(struct channel *c, char *buf)
{
	const char *cmnd;
	const char *arg;
	const char *cmd;
	int err;

	if (!(cmnd = strsep(&buf)))
	if (!(cmd = strsep(&buf)))
		return;

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

		state_channel_clear(0);
		return;
	}

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

		state_channel_close(0);
		return;
	}

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

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

		if ((err = io_cx(c->server->connection)))
			action(action_error, "Error: %s", io_err(err));
			action(action_error, "connect: %s", io_err(err));

		return;
	}

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

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

		if ((err = io_dx(c->server->connection)))
			action(action_error, "Error: %s", io_err(err));
			action(action_error, "disconnect: %s", io_err(err));

		return;
	}

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

		io_stop();
		return;
	}

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

static int

M test/io.mock.c => test/io.mock.c +35 -3
@@ 6,6 6,7 @@
static char mock_send[MOCK_SEND_N][MOCK_SEND_LEN];
static unsigned mock_send_i;
static unsigned mock_send_n;
static int cxed;

void
mock_reset_io(void)


@@ 13,6 14,7 @@ mock_reset_io(void)
	mock_send_i = 0;
	mock_send_n = 0;
	memset(mock_send, 0, MOCK_SEND_LEN * MOCK_SEND_N);
	cxed = 0;
}

int


@@ 44,9 46,39 @@ connection(const void *o, const char *h, const char *p, uint32_t f)
	return NULL;
}

const char* io_err(int err) { UNUSED(err); return "err"; }
int io_cx(struct connection *c) { UNUSED(c); return 0; }
int io_dx(struct connection *c) { UNUSED(c); return 0; }
int
io_cx(struct connection *c)
{
	UNUSED(c);

	if (cxed)
		return -1;

	cxed = 1;
	return 0;
}

int
io_dx(struct connection *c)
{
	UNUSED(c);

	if (cxed) {
		cxed = 0;
		return 0;
	}

	return -1;
}

const char*
io_err(int err)
{
	UNUSED(err);

	return (cxed ? "cxed" : "dxed");
}

unsigned io_tty_cols(void) { return 0; }
unsigned io_tty_rows(void) { return 0; }
void connection_free(struct connection *c) { UNUSED(c); }

M test/state.c => test/state.c +255 -3
@@ 18,10 18,254 @@

#define INP_S(S) io_cb_read_inp((S), strlen(S))
#define INP_C(C) io_cb_read_inp((char[]){(C)}, 1)
#define CURRENT_LINE (buffer_head(&current_channel()->buffer)->text)

// TODO: tests for
// sending certain /commands to private buffer, server buffer
#define CURRENT_LINE \
	(buffer_head(&current_channel()->buffer) ? \
	 buffer_head(&current_channel()->buffer)->text : NULL)

static void
test_command(void)
{
	state_init();

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

	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);

	assert_ptr_null(CURRENT_LINE);

	state_term();
}

static void
test_command_close(void)
{
	static struct channel *c1;
	static struct channel *c2;
	struct server *s;

	state_init();

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

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

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

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

	c1 = channel("#c1", CHANNEL_T_CHANNEL);
	c2 = channel("#c2", CHANNEL_T_CHANNEL);
	s = server("host", "port", NULL, "user", "real");

	if (!s || !c1 || !c2)
		test_abort("Failed to create server and channels");

	c1->server = s;
	c2->server = s;
	channel_list_add(&s->clist, c1);
	channel_list_add(&s->clist, c2);

	if (server_list_add(state_server_list(), s))
		test_abort("Failed to add server");

	channel_set_current(c1);

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

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

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

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

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

	channel_set_current(s->channel);

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

	assert_ptr_null(action_handler);
	assert_ptr_null(action_message());
	assert_strcmp(current_channel()->name, "rirc");
	assert_strcmp(CURRENT_LINE, " - compiled with DEBUG flags");

	state_term();
}

static void
test_command_connect(void)
{
	struct server *s;

	mock_reset_io();

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

	INP_S(":connect");
	INP_C(0x0A);

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

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

	if (!(s = server("host", "port", NULL, "user", "real")))
		test_abort("Failed test setup");

	if (server_list_add(state_server_list(), s))
		test_abort("Failed to add server");

	channel_set_current(s->channel);

	INP_S(":connect");
	INP_C(0x0A);

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

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

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

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

	INP_S(":connect");
	INP_C(0x0A);

	assert_strcmp(action_message(), "connect: cxed");

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

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

	state_term();
}

static void
test_command_disconnect(void)
{
	struct server *s;

	mock_reset_io();

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

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

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

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

	if (!(s = server("host", "port", NULL, "user", "real")))
		test_abort("Failed test setup");

	if (server_list_add(state_server_list(), s))
		test_abort("Failed to add server");

	channel_set_current(s->channel);

	io_cx(NULL);

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

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

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

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

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

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

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

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

	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);

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

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

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

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

	state_term();
}

static void
test_state(void)


@@ 73,12 317,20 @@ 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);

	state_term();
}

int
main(void)
{
	struct testcase tests[] = {
		TESTCASE(test_command),
		TESTCASE(test_command_clear),
		TESTCASE(test_command_close),
		TESTCASE(test_command_connect),
		TESTCASE(test_command_disconnect),
		TESTCASE(test_command_quit),
		TESTCASE(test_state),
	};