~rcr/rirc

eb22ed2043153a898292ab5fe64b6fb9a0421480 — Richard Robbins 3 years ago 9f6f565 + e861eaf
Merge branch 'dev' into static_analysis
M CHANGELOG => CHANGELOG +6 -0
@@ 2,10 2,16 @@
Summary of notable changes and features

## Unreleased (dev)
### Refactor
 - input module complete rewrite
   - generic word completion API
   - add full test coverage
   - performance and memory usage improvements
### Fixes
 - fix error in sending PING
 - fix error messages on socket disconnect
 - fix fatal error on /quit
 - close/cleanup servers on :quit

## [0.1.0]
### Initial release

M README.md => README.md +1 -2
@@ 1,9 1,8 @@
[![Coverity Scan Build](https://scan.coverity.com/projects/4940/badge.svg)](https://scan.coverity.com/projects/4940)
![Sonarcloud](https://sonarcloud.io/api/project_badges/measure?project=rcr_rirc&metric=ncloc)
<p align="center"><img src="https://raw.githubusercontent.com/rcr/rirc/master/docs/birb.jpg" alt="birb"/></p>

# rirc
![rirc](docs/birb.jpg?raw=true "rirc")

A minimalistic irc client written in C.

While still under development, it currently supports

M TODO => TODO +32 -16
@@ 1,15 1,39 @@
FIXME:
UNHANDLED ~ 461 PING :Not enough parameters
 - add basic ssl support, with ifdef guards, see lessandro's patch
 - remove server channel from channel list? does this break searching?
   what benefit is there to this? s->channel can then just be s->clist.head
 - are join/part/quit messages working with the threshold?
 - server: channel/ulist/clist
 - posix 2001 with removal of recusive mutex?
 - fatal/error/debug macros with reusage

 - when reconnecting, if cant rejoin channel (e.g. archlinux) and bumped
   to -unregistered... closing it causes 442: not on that channel...
   what happens to messages sent to it?
	If in ##channel that requires authentication (ie bumps you to ##channel-unauthorized
	or similar), /disconnect, /connect, rirc attempts to join ##channel, can't, and is
	bumped to ##channel-unauthorized, but ##channel buffer remains open and not flagged
	as parted, ie could potentially send messages to the channel, but won't receive any
		- or it returns 403, and cant /join the channel because it's not parted

TODO:
 - Make sure all the thread related code is using thread-safe functions,
   and that proper cancellation points are used. Make sure no resources
   can be left hanging (eg: sockets left open when a thread is canceled?)


TODO
---------------------------
next iteration
	- RFC2812 compliance
	- Split and prototype:
		src/handlers
		  \ recv
		  \ send
		  \ command
		  \ numeric
		  \ ctcp
		  \ sasl

Continue refactoring:
	- Rewrite input DLL as circular buffer of gap buffers, with copy on repeat,
		use abstractions in _draw_input
	- Error/warning writing from stateless components, e.g.:
		- mode.c should be able to call err("...") on some passed parameter
		  and have it written to the appropriate buffer with the appropriate


@@ 21,6 45,7 @@ Continue refactoring:
		  be added
	- more debug information, levels, for various events, a debug pannel as a new channel
	  type when debug flag is set
	- split action handling out

action_error
display error to user for confirmation, e.g. :connect <server>


@@ 39,13 64,14 @@ Testing:
	- Fuzzing handlers (afl)
	- Try musl C
	- include-what-you-use for header detangling
	- clang tidy, .clang-tidy config

buffer performance improvements:
	- reduce overall size, pack strings in contiguous realloc'ed array
	- cache line breaks
	- pre-format time

abstract list.h from channel/server/input lists
abstract list.h from channel/server

SASL auth:
	- cli -a/--auth


@@ 452,10 478,6 @@ show disconnected/parted in status bar for channels instead of 0 user count

Keep state of tab complete for successively getting the next nick lexicographically

Make sure all the thread related code is using thread-safe functions,
and that proper cancellation points are used. Make sure no resources
can be left hanging (eg: sockets left open when a thread is canceled?)

Empty trailing should be null, e.g.:
":nick!~user@host PART #channel :" -> "< ~ nick!~user@host has left #channel ()"



@@ 470,12 492,6 @@ parsing whatever it last parsed. Instead, a function like strsep should be imple
which correctly handles NULL input in this case
	-> replace stringtok/stringtok_r with getarg

If in ##channel that requires authentication (ie bumps you to ##channel-unauthorized
or similar), /disconnect, /connect, rirc attempts to join ##channel, can't, and is
bumped to ##channel-unauthorized, but ##channel buffer remains open and not flagged
as parted, ie could potentially send messages to the channel, but won't receive any
	- or it returns 403, and cant /join the channel because it's not parted

use the alternate screen buffer to restore screen when rirc exits

##linux ~ cannot send to channel

M src/components/README.md => src/components/README.md +2 -0
@@ 9,6 9,8 @@ Stateful component hierarchy
       |   |
       |   |__*buffer_line
       |
       |__channel
       |
       |__channel_list
       |   |
       |   |__*channel

M src/components/channel.c => src/components/channel.c +10 -3
@@ 47,7 47,16 @@ channel_free(struct channel *c)
void
channel_list_free(struct channel_list *cl)
{
	/* TODO */;
	struct channel *c1, *c2;

	if ((c1 = cl->head) == NULL)
		return;

	do {
		c2 = c1;
		c1 = c2->next;
		channel_free(c2);
	} while (c1 != cl->head);
}

struct channel*


@@ 72,8 81,6 @@ channel_list_add(struct channel_list *cl, struct channel *c)
	return NULL;
}

// TODO: segault when deleting the tail and try to `prev` the head

struct channel*
channel_list_del(struct channel_list *cl, struct channel *c)
{

M src/components/server.c => src/components/server.c +19 -19
@@ 39,21 39,13 @@ server(const char *host, const char *port, const char *pass, const char *user, c
	s->pass = pass ? strdup(pass) : NULL;
	s->username = strdup(user);
	s->realname = strdup(real);

	s->channel = channel(host, CHANNEL_T_SERVER);
	s->mode_str.type = MODE_STR_USERMODE;

	mode_cfg(&(s->mode_cfg), NULL, MODE_CFG_DEFAULTS);

	// FIXME: channel()
	s->channel = new_channel(host, s, CHANNEL_T_SERVER);

	// move this to the state_new_server
	channel_set_current(s->channel);

	if ((s->connection = connection(s, host, port)) == NULL) {
		server_free(s);
		return NULL;
	}
	/* FIXME: remove server pointer from channel, remove
	 * server's channel from clist */
	s->channel->server = s;
	channel_list_add(&(s->clist), s->channel);

	return s;
}


@@ 146,6 138,14 @@ server_reset(struct server *s)
void
server_free(struct server *s)
{
	// FIXME: add this back when removing it from
	// server's channel_list
	// channel_free(s->channel);

	channel_list_free(&(s->clist));
	channel_list_free(&(s->ulist));
	user_list_free(&(s->ignore));

	free((void *)s->host);
	free((void *)s->port);
	free((void *)s->pass);


@@ 183,7 183,7 @@ server_set_004(struct server *s, char *str)

	if (user_modes) {

		DEBUG_MSG("Setting numeric 004 user_modes: %s", user_modes);
		debug("Setting numeric 004 user_modes: %s", user_modes);

		if (mode_cfg(&(s->mode_cfg), user_modes, MODE_CFG_USERMODES) != MODE_ERR_NONE)
			newlinef(c, 0, "-!!-", "invalid numeric 004 user_modes: %s", user_modes);


@@ 191,7 191,7 @@ server_set_004(struct server *s, char *str)

	if (chan_modes) {

		DEBUG_MSG("Setting numeric 004 chan_modes: %s", chan_modes);
		debug("Setting numeric 004 chan_modes: %s", chan_modes);

		if (mode_cfg(&(s->mode_cfg), chan_modes, MODE_CFG_CHANMODES) != MODE_ERR_NONE)
			newlinef(c, 0, "-!!-", "invalid numeric 004 chan_modes: %s", chan_modes);


@@ 352,7 352,7 @@ server_set_CASEMAPPING(struct server *s, char *val)
static int
server_set_CHANMODES(struct server *s, char *val)
{
	DEBUG_MSG("Setting numeric 005 CHANMODES: %s", val);
	debug("Setting numeric 005 CHANMODES: %s", val);

	return (mode_cfg(&(s->mode_cfg), val, MODE_CFG_SUBTYPES) != MODE_ERR_NONE);
}


@@ 360,7 360,7 @@ server_set_CHANMODES(struct server *s, char *val)
static int
server_set_MODES(struct server *s, char *val)
{
	DEBUG_MSG("Setting numeric 005 MODES: %s", val);
	debug("Setting numeric 005 MODES: %s", val);

	return (mode_cfg(&(s->mode_cfg), val, MODE_CFG_MODES) != MODE_ERR_NONE);
}


@@ 368,7 368,7 @@ server_set_MODES(struct server *s, char *val)
static int
server_set_PREFIX(struct server *s, char *val)
{
	DEBUG_MSG("Setting numeric 005 PREFIX: %s", val);
	debug("Setting numeric 005 PREFIX: %s", val);

	return (mode_cfg(&(s->mode_cfg), val, MODE_CFG_PREFIX) != MODE_ERR_NONE);
}


@@ 376,7 376,7 @@ server_set_PREFIX(struct server *s, char *val)
void
server_nick_set(struct server *s, const char *nick)
{
	DEBUG_MSG("Setting server nick: %s", nick);
	debug("Setting server nick: %s", nick);

	if (s->nick)
		free((void *)s->nick);

M src/components/server.h => src/components/server.h +3 -3
@@ 4,7 4,6 @@
#include "src/components/buffer.h"
#include "src/components/channel.h"
#include "src/components/mode.h"
#include "src/io.h"

struct server
{


@@ 22,7 21,7 @@ struct server
	} nicks;
	struct channel *channel;
	struct channel_list clist;
	struct connection *connection;
	struct channel_list ulist; // TODO: seperate privmsg
	struct mode usermodes;
	struct mode_str mode_str;
	struct mode_cfg mode_cfg;


@@ 31,6 30,7 @@ struct server
	struct user_list ignore;
	unsigned ping;
	unsigned quitting : 1;
	void *connection;
};

struct server_list


@@ 49,7 49,7 @@ struct server* server(

struct server* server_list_add(struct server_list*, struct server*);
struct server* server_list_del(struct server_list*, struct server*);
struct server* server_list_get(struct server_list*, const char *, const char*);
struct server* server_list_get(struct server_list*, const char*, const char*);

void server_set_004(struct server*, char*);
void server_set_005(struct server*, char*);

M src/io.c => src/io.c +78 -133
@@ 62,14 62,19 @@
#error "IO_RECONNECT_BACKOFF_MAX: [0, 86400]"
#endif

#define PT_CF(X) do { int ret; if ((ret = (X)) < 0) fatal("%s: %s", (#X), strerror(ret)); } while (0)
#if EAGAIN == EWOULDBLOCK
#define CHECK_BLOCK(X) ((X) == EAGAIN)
#else
#define CHECK_BLOCK(X) ((X) == EAGAIN || (X) == EWOULDBLOCK)
#endif

#define PT_CF(X) do { io_check_fatal((#X), (X)); } while (0)
#define PT_LK(X) PT_CF(pthread_mutex_lock((X)))
#define PT_UL(X) PT_CF(pthread_mutex_unlock((X)))
#define PT_CB(R, ...) \
	do { \
		PT_LK(&cb_mutex); \
		io_cb((R), __VA_ARGS__); \
		PT_UL(&cb_mutex); \
#define PT_CB(...) \
	do { PT_LK(&cb_mutex); \
	     io_cb(__VA_ARGS__); \
	     PT_UL(&cb_mutex); \
	} while (0)

enum io_err_t


@@ 80,13 85,12 @@ enum io_err_t
	IO_ERR_DXED,
	IO_ERR_FMT,
	IO_ERR_SEND,
	IO_ERR_TERM,
	IO_ERR_TRUNC,
};

struct io_lock
{
	pthread_cond_t  cnd;
	pthread_cond_t cnd;
	pthread_mutex_t mtx;
	volatile int predicate;
};


@@ 103,7 107,6 @@ struct connection
		IO_ST_CXNG, /* Socket connection in progress */
		IO_ST_CXED, /* Socket connected */
		IO_ST_PING, /* Socket connected, network state in question */
		IO_ST_TERM /* Terminal thread state */
	} st_c, /* current thread state */
	  st_f; /* forced thread state */
	char ip[INET6_ADDRSTRLEN];


@@ 116,6 119,7 @@ struct connection
	} read;
	struct io_lock lock;
	unsigned rx_backoff;
	pthread_t pt_tid;
};

static const char* io_strerror(struct connection*, int);


@@ 124,30 128,32 @@ static enum io_state_t io_state_cxng(struct connection*);
static enum io_state_t io_state_dxed(struct connection*);
static enum io_state_t io_state_ping(struct connection*);
static enum io_state_t io_state_rxng(struct connection*);
static void io_init(void);
static void io_init_sig(void);
static void io_init_tty(void);
static void io_lock_init(struct io_lock*);
static void io_lock_term(struct io_lock*);
static void io_check_fatal(const char*, int);
static void io_lock_wait(struct io_lock*, struct timespec*);
static void io_lock_wake(struct io_lock*);
static void io_net_set_timeout(struct connection*, unsigned);
static void io_recv(struct connection*, const char*, size_t);
static void io_sig_init(void);
static void io_soc_close(int*);
static void io_soc_shutdown(int);
static void io_state_force(struct connection*, enum io_state_t);
static void io_term(void);
static void io_term_tty(void);
static void io_tty_winsize(void);
static void io_tty_init(void);
static void io_tty_term(void);
static void io_tty_winsize(unsigned*, unsigned*);
static void* io_thread(void*);

static pthread_mutex_t cb_mutex;
static pthread_once_t init_once = PTHREAD_ONCE_INIT;
static int io_running;
static pthread_mutex_t cb_mutex = PTHREAD_MUTEX_INITIALIZER;
static struct termios term;
static struct winsize tty_ws;
static volatile sig_atomic_t flag_sigwinch_cb; /* sigwinch callback */
static volatile sig_atomic_t flag_tty_resized; /* sigwinch ws resize */

static void
io_check_fatal(const char *f, int ret)
{
	if (ret < 0)
		fatal("%s: %s", f, strerror(ret));
}

static const char*
io_strerror(struct connection *c, int errnum)
{


@@ 167,62 173,12 @@ io_soc_close(int *soc)
static void
io_soc_shutdown(int soc)
{
	if (soc >= 0 && shutdown(soc, SHUT_RDWR) < 0) {
	if (soc >= 0 && shutdown(soc, SHUT_RDWR) < 0 && errno != ENOTCONN) {
		fatal("shutdown: %s", strerror(errno));
	}
}

static void
io_init(void)
{
	pthread_mutexattr_t m_attr;

	PT_CF(pthread_mutexattr_init(&m_attr));
	PT_CF(pthread_mutexattr_settype(&m_attr, PTHREAD_MUTEX_ERRORCHECK));
	PT_CF(pthread_mutex_init(&cb_mutex, &m_attr));
	PT_CF(pthread_mutexattr_destroy(&m_attr));

	/* atexit doesn't set errno */
	if (atexit(io_term) != 0)
		fatal("atexit");
}

static void
io_term(void)
{
	int ret;

	if ((ret = pthread_mutex_trylock(&cb_mutex)) < 0 && ret != EBUSY)
		fatal("pthread_mutex_trylock: %s", strerror(ret));

	PT_UL(&cb_mutex);
	PT_CF(pthread_mutex_destroy(&cb_mutex));
}

static void
io_lock_init(struct io_lock *lock)
{
	pthread_mutexattr_t m_attr;

	PT_CF(pthread_mutexattr_init(&m_attr));
	PT_CF(pthread_mutexattr_settype(&m_attr, PTHREAD_MUTEX_RECURSIVE));

	PT_CF(pthread_cond_init(&(lock->cnd), NULL));
	PT_CF(pthread_mutex_init(&(lock->mtx), &m_attr));

	PT_CF(pthread_mutexattr_destroy(&m_attr));

	lock->predicate = 0;
}

static void
io_lock_term(struct io_lock *lock)
{
	PT_CF(pthread_cond_destroy(&(lock->cnd)));
	PT_CF(pthread_mutex_destroy(&(lock->mtx)));
}

static void
io_lock_wait(struct io_lock *lock, struct timespec *timeout)
{
	PT_LK(&(lock->mtx));


@@ 245,15 201,6 @@ io_lock_wait(struct io_lock *lock, struct timespec *timeout)
	PT_UL(&(lock->mtx));
}

static void
io_lock_wake(struct io_lock *lock)
{
	PT_LK(&(lock->mtx));
	lock->predicate = 1;
	PT_CF(pthread_cond_signal(&(lock->cnd)));
	PT_UL(&(lock->mtx));
}

struct connection*
connection(const void *obj, const char *host, const char *port)
{


@@ 267,17 214,9 @@ connection(const void *obj, const char *host, const char *port)
	c->port = strdup(port);
	c->st_c = IO_ST_DXED;
	c->st_f = IO_ST_INVALID;
	io_lock_init(&(c->lock));

	PT_CF(pthread_once(&init_once, io_init));

	pthread_attr_t pt_attr;
	pthread_t pt_tid;

	PT_CF(pthread_attr_init(&pt_attr));
	PT_CF(pthread_attr_setdetachstate(&pt_attr, PTHREAD_CREATE_DETACHED));
	PT_CF(pthread_create(&pt_tid, &pt_attr, io_thread, c));
	PT_CF(pthread_attr_destroy(&pt_attr));
	PT_CF(pthread_cond_init(&(c->lock.cnd), NULL));
	PT_CF(pthread_mutex_init(&(c->lock.mtx), NULL));
	PT_CF(pthread_create(&c->pt_tid, NULL, io_thread, c));

	return c;
}


@@ 305,7 244,7 @@ io_sendf(struct connection *c, const char *fmt, ...)
	if (len >= sizeof(sendbuf) - 2)
		return IO_ERR_TRUNC;

	DEBUG_MSG("send: (%zu) %s", len, sendbuf);
	debug("send: (%zu) %s", len, sendbuf);

	sendbuf[len++] = '\r';
	sendbuf[len++] = '\n';


@@ 329,7 268,6 @@ io_cx(struct connection *c)
		case IO_ST_CXNG: err = IO_ERR_CXNG; break;
		case IO_ST_CXED: err = IO_ERR_CXED; break;
		case IO_ST_PING: err = IO_ERR_CXED; break;
		case IO_ST_TERM: err = IO_ERR_TERM; break;
		default:
			io_state_force(c, IO_ST_CXNG);
	}


@@ 350,7 288,6 @@ io_dx(struct connection *c)

	switch (c->st_c) {
		case IO_ST_DXED: err = IO_ERR_DXED; break;
		case IO_ST_TERM: err = IO_ERR_TERM; break;
		default:
			io_state_force(c, IO_ST_DXED);
	}


@@ 363,11 300,16 @@ io_dx(struct connection *c)
void
io_free(struct connection *c)
{
	/* Force a socket thread into the IO_ST_TERM state */
	pthread_t pt_tid = c->pt_tid;

	PT_LK(&(c->lock.mtx));
	io_state_force(c, IO_ST_TERM);
	PT_UL(&(c->lock.mtx));
	PT_CF(pthread_cancel(pt_tid));
	PT_CF(pthread_join(pt_tid, NULL));
	PT_CF(pthread_cond_destroy(&(c->lock.cnd)));
	PT_CF(pthread_mutex_destroy(&(c->lock.mtx)));
	io_soc_close(&(c->soc));
	free((void*)c->host);
	free((void*)c->port);
	free(c);
}

static void


@@ 380,15 322,14 @@ io_state_force(struct connection *c, enum io_state_t st_f)
	switch (c->st_c) {
		case IO_ST_DXED: /* io_lock_wait() */
		case IO_ST_RXNG: /* io_lock_wait() */
			io_lock_wake(&(c->lock));
			c->lock.predicate = 1;
			PT_CF(pthread_cond_signal(&(c->lock.cnd)));
			break;
		case IO_ST_CXNG: /* connect() */
		case IO_ST_CXED: /* recv() */
		case IO_ST_PING: /* recv() */
			io_soc_shutdown(c->soc);
			break;
		case IO_ST_TERM:
			break;
		default:
			fatal("Unknown net state: %d", c->st_c);
	}


@@ 437,6 378,7 @@ io_state_cxng(struct connection *c)
	/* TODO: mutex should protect access to c->soc, else race condition
	 *       when the main thread tries to shutdown() for cancel */
	/* TODO: how to cancel getaddrinfo/getnameinfo? */
	/* FIXME: addrinfo leak if canceled during connection */

	int ret, soc = -1;



@@ 501,7 443,7 @@ io_state_cxed(struct connection *c)
	while ((ret = recv(c->soc, c->read.tmp, sizeof(c->read.tmp), 0)) > 0)
		io_recv(c, c->read.tmp, (size_t) ret);

	if (errno == EAGAIN || errno == EWOULDBLOCK) {
	if (CHECK_BLOCK(errno)) {
		return IO_ST_PING;
	}



@@ 534,7 476,7 @@ io_state_ping(struct connection *c)

		if (ret == 0) {
			PT_CB(IO_CB_DXED, c->obj, "connection closed");
		} else if (errno == EAGAIN || errno == EWOULDBLOCK) {
		} else if (CHECK_BLOCK(errno)) {
			if ((ping += IO_PING_REFRESH) < IO_PING_MAX) {
				PT_CB(IO_CB_PING_N, c->obj, ping);
				continue;


@@ 592,9 534,6 @@ io_thread(void *arg)

		PT_UL(&(c->lock.mtx));

		if (c->st_c == IO_ST_TERM)
			break;

		if (st_f == IO_ST_PING && st_t == IO_ST_CXED)
			PT_CB(IO_CB_PING_0, c->obj, 0);



@@ 608,14 547,6 @@ io_thread(void *arg)
			c->rx_backoff = 0;
	}

	io_soc_shutdown(c->soc);
	io_soc_close(&(c->soc));
	io_lock_term(&(c->lock));

	free((void*)c->host);
	free((void*)c->port);
	free(c);

	return NULL;
}



@@ 632,7 563,7 @@ io_recv(struct connection *c, const char *buf, size_t n)

			c->read.buf[ci] = 0;

			DEBUG_MSG("recv: (%zu) %s", ci, c->read.buf);
			debug("recv: (%zu) %s", ci, c->read.buf);

			PT_LK(&cb_mutex);
			io_cb_read_soc(c->read.buf, ci, c->obj);


@@ 669,7 600,7 @@ sigaction_sigwinch(int sig)
}

static void
io_init_sig(void)
io_sig_init(void)
{
	struct sigaction sa;



@@ 682,7 613,7 @@ io_init_sig(void)
}

static void
io_init_tty(void)
io_tty_init(void)
{
	struct termios nterm;



@@ 700,27 631,29 @@ io_init_tty(void)
	if (tcsetattr(STDIN_FILENO, TCSANOW, &nterm) < 0)
		fatal("tcsetattr: %s", strerror(errno));

	/* atexit doesn't set errno */
	if (atexit(io_term_tty) < 0)
	if (atexit(io_tty_term) != 0)
		fatal("atexit");
}

static void
io_term_tty(void)
io_tty_term(void)
{
	/* Exit handler, must return normally */

	if (tcsetattr(STDIN_FILENO, TCSADRAIN, &term) < 0)
		fatal("tcsetattr: %s", strerror(errno));
		fatal_noexit("tcsetattr: %s", strerror(errno));
}

void
io_loop(void)
io_init(void)
{
	PT_CF(pthread_once(&init_once, io_init));
	io_sig_init();
	io_tty_init();

	io_init_sig();
	io_init_tty();
	io_running = 1;

	while (io_running) {

	for (;;) {
		char buf[128];
		ssize_t ret = read(STDIN_FILENO, buf, sizeof(buf));



@@ 743,29 676,42 @@ io_loop(void)
	}
}

void
io_term(void)
{
	io_running = 0;
}

static void
io_tty_winsize(void)
io_tty_winsize(unsigned *rows, unsigned *cols)
{
	static struct winsize tty_ws;

	if (flag_tty_resized == 0) {
		flag_tty_resized = 1;

		if (ioctl(0, TIOCGWINSZ, &tty_ws) < 0)
			fatal("ioctl: %s", strerror(errno));
	}

	*rows = tty_ws.ws_row;
	*cols = tty_ws.ws_col;
}

unsigned
io_tty_cols(void)
{
	io_tty_winsize();
	return tty_ws.ws_col;
	unsigned rows, cols;
	io_tty_winsize(&rows, &cols);
	return cols;
}

unsigned
io_tty_rows(void)
{
	io_tty_winsize();
	return tty_ws.ws_row;
	unsigned rows, cols;
	io_tty_winsize(&rows, &cols);
	return rows;
}

const char*


@@ 778,7 724,6 @@ io_err(int err)
		case IO_ERR_DXED:  return "socket not connected";
		case IO_ERR_FMT:   return "failed to format message";
		case IO_ERR_SEND:  return "failed to send message";
		case IO_ERR_TERM:  return "thread is terminating";
		case IO_ERR_TRUNC: return "data truncated";
		default:
			return "unknown error";

M src/io.h => src/io.h +13 -12
@@ 15,16 15,16 @@
 *                            +--------+
 *                 +----(B)-- |  rxng  |
 *                 |          +--------+
 *  INIT           |           |      ^
 *    v            |         (A,C)    |
 *    |            |           |     (E)
 *                 |           |      ^
 *   INIT          |         (A,C)    |
 *    v            |           |     (E)
 *    |            v           v      |
 *    +--> +--------+ --(A)-> +--------+
 *         |  dxed  |         |  cxng  | <--+
 *    +--< +--------+ <-(B)-- +--------+    |
 *    |     ^      ^           |      ^    (F)
 *    v     |      |          (D)     |     |
 *  TERM    |      |           |     (F)    |
 *    |    +--------+ --(A)-> +--------+
 *    +--> |  dxed  |         |  cxng  | <--+
 *         +--------+ <-(B)-- +--------+    |
 *          ^      ^           |      ^    (F)
 *          |      |          (D)     |     |
 *          |      |           |     (F)    |
 *          |      |           v      |     |
 *          |      |          +--------+    |
 *          |      +----(B)-- |  cxed  |    |


@@ 69,7 69,7 @@
 *   t(n) = t(n - 1) * factor
 *   t(0) = base
 *
 * Calling io_loop starts the io context and never returns
 * Calling io_init starts the io context and doesn't return until io_term
 */

#define IO_MAX_CONNECTIONS 8


@@ 119,8 119,9 @@ void io_cb(enum io_cb_t, const void*, ...);
void io_cb_read_inp(char*, size_t);
void io_cb_read_soc(char*, size_t, const void*);

/* Start non-returning IO context */
void io_loop(void);
/* Start/stop IO context */
void io_init(void);
void io_term(void);

/* Get tty dimensions */
unsigned io_tty_cols(void);

M src/mesg.c => src/mesg.c +6 -2
@@ 277,7 277,7 @@ send_mesg(struct server *s, struct channel *chan, char *mesg)
		const struct send_handler* handler = send_handler_lookup(cmd_str, strlen(cmd_str));

		if (handler) {
			handler->func(mesg, chan->server, chan);
			handler->func(mesg, s, chan);
		} else if ((ret = io_sendf(s->connection, "%s %s", cmd_str, mesg)))
			newlinef(chan, 0, "-!!-", "sendf fail: %s", io_err(ret));



@@ 298,7 298,7 @@ send_mesg(struct server *s, struct channel *chan, char *mesg)
			newlinef(chan, 0, "-!!-", "sendf fail: %s", io_err(ret));

		else
			newline(chan, BUFFER_LINE_CHAT, chan->server->nick, mesg);
			newline(chan, BUFFER_LINE_CHAT, s->nick, mesg);
	}
}



@@ 859,6 859,10 @@ recv_ctcp_rpl(struct parsed_mesg *p, struct server *s)
	if (!(cmd = getarg(&mesg, " ")))
		fail(s->channel, "CTCP: command is null");

	// FIXME: CTCP PING replies should come back with the same
	// <second> <millisecond> value that was sent out, and is
	// used to calculate the ping here

	newlinef(s->channel, 0, p->from, "CTCP %s reply: %s", cmd, mesg);

	return 0;

M src/rirc.c => src/rirc.c +23 -15
@@ 13,14 13,13 @@
#define arg_error(...) \
	do { fprintf(stderr, "%s: ", runtime_name); \
	     fprintf(stderr, __VA_ARGS__); \
	     fprintf(stderr, "\n"); \
	     fprintf(stderr, "%s --help for usage\n", runtime_name); \
	     exit(EXIT_FAILURE); \
	     fprintf(stderr, "\n%s --help for usage\n", runtime_name); \
	     return 1; \
	} while (0)

static const char* opt_arg_str(char);
static const char* getpwuid_pw_name(void);
static void parse_args(int, char**);
static int parse_args(int, char**);

#ifndef DEBUG
const char *runtime_name = "rirc";


@@ 103,7 102,7 @@ getpwuid_pw_name(void)
	return passwd->pw_name;
}

static void
static int
parse_args(int argc, char **argv)
{
	int opt_c = 0,


@@ 126,8 125,8 @@ parse_args(int argc, char **argv)
		{"chans",    required_argument, 0, 'c'},
		{"username", required_argument, 0, 'u'},
		{"realname", required_argument, 0, 'r'},
		{"version",  no_argument,       0, 'v'},
		{"help",     no_argument,       0, 'h'},
		{"version",  no_argument,       0, 'v'},
		{0, 0, 0, 0}
	};



@@ 143,7 142,7 @@ parse_args(int argc, char **argv)
	} cli_servers[IO_MAX_CONNECTIONS];

	/* FIXME: getopt_long is a GNU extension */
	while (0 < (opt_c = getopt_long(argc, argv, ":s:p:w:n:c:r:u:vh", long_opts, &opt_i))) {
	while (0 < (opt_c = getopt_long(argc, argv, ":s:p:w:n:c:r:u:hv", long_opts, &opt_i))) {

		switch (opt_c) {



@@ 202,14 201,14 @@ parse_args(int argc, char **argv)

			#undef CHECK_SERVER_OPTARG

			case 'v':
				puts(rirc_version);
				exit(EXIT_SUCCESS);

			case 'h':
				puts(rirc_usage);
				exit(EXIT_SUCCESS);

			case 'v':
				puts(rirc_version);
				exit(EXIT_SUCCESS);

			case '?':
				arg_error("unknown options '%s'", argv[optind - 1]);



@@ 245,8 244,7 @@ parse_args(int argc, char **argv)
			(cli_servers[i].realname ? cli_servers[i].realname : default_realname)
		);

		if (s == NULL)
			arg_error("failed to create: %s:%s", cli_servers[i].host, cli_servers[i].port);
		s->connection = connection(s, cli_servers[i].host, cli_servers[i].port);

		if (server_list_add(state_server_list(), s))
			arg_error("duplicate server: %s:%s", cli_servers[i].host, cli_servers[i].port);


@@ 258,15 256,25 @@ parse_args(int argc, char **argv)
			arg_error("invalid nicks: '%s'", cli_servers[i].nicks);

		cli_servers[i].s = s;

		channel_set_current(s->channel);
	}

	for (size_t i = 0; i < n_servers; i++)
		io_cx(cli_servers[i].s->connection);

	return 0;
}

int
main(int argc, char **argv)
{
	parse_args(argc, argv);
	io_loop();
	int ret;

	if ((ret = parse_args(argc, argv)) == 0)
		io_init();

	state_term();

	return ret;
}

M src/state.c => src/state.c +80 -75
@@ 20,13 20,25 @@
// See: https://vt100.net/docs/vt100-ug/chapter3.html
#define CTRL(k) ((k) & 0x1f)

static void _newline(struct channel*, enum buffer_line_t, const char*, const char*, va_list);
static void state_io_cxed(struct server*);
static void state_io_dxed(struct server*, va_list);
static void state_io_ping(struct server*, unsigned int);
static void state_io_signal(enum io_sig_t);

static int state_input_linef(struct channel*);
static int state_input_ctrlch(const char*, size_t);
static int state_input_action(const char*, size_t);

static uint16_t state_complete(char*, uint16_t, uint16_t, int);
static uint16_t state_complete_list(char*, uint16_t, uint16_t, const char**);
static uint16_t state_complete_user(char*, uint16_t, uint16_t, int);

static struct
{
	struct channel *current_channel; /* the current channel being drawn */
	struct channel *default_channel; /* the default rirc channel at startup */

	struct server_list servers;

	union draw draw;
} state;



@@ 36,25 48,11 @@ state_server_list(void)
	return &state.servers;
}

static void state_term(void);

static void _newline(struct channel*, enum buffer_line_t, const char*, const char*, va_list);

struct channel* current_channel(void) { return state.current_channel; }
struct channel* default_channel(void) { return state.default_channel; }

static void state_io_cxed(struct server*);
static void state_io_dxed(struct server*, va_list);
static void state_io_ping(struct server*, unsigned int);
static void state_io_signal(enum io_sig_t);

static int state_input_linef(struct channel*);
static int state_input_ctrlch(const char*, size_t);
static int state_input_action(const char*, size_t);

static uint16_t state_complete(char*, uint16_t, uint16_t, int);
static uint16_t state_complete_list(char*, uint16_t, uint16_t, const char**);
static uint16_t state_complete_user(char*, uint16_t, uint16_t, int);
struct channel*
current_channel(void)
{
	return state.current_channel;
}

/* List of IRC commands for tab completion */
static const char *irc_list[] = {


@@ 92,10 90,6 @@ redraw(void)
void
state_init(void)
{
	/* atexit doesn't set errno */
	if (atexit(state_term) != 0)
		fatal("atexit");

	state.default_channel = state.current_channel = new_channel("rirc", NULL, CHANNEL_T_OTHER);

	/* Splashscreen */


@@ 116,25 110,31 @@ state_init(void)
	redraw();
}

static void
void
state_term(void)
{
	/* Exit handler; must return normally */

	struct server *s1, *s2;

	channel_free(state.default_channel);

	/* Reset terminal colours */
	printf("\x1b[38;0;m");
	printf("\x1b[48;0;m");

#ifndef DEBUG
	/* Clear screen */
	if (!fatal_exit) {
		printf("\x1b[H\x1b[J");
		channel_free(state.default_channel);
		/* TODO:
		 * here iterate the server list and quit with some default message,
		 * call server_free */
	}
#endif
	printf("\x1b[H\x1b[J");

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

	do {
		s2 = s1;
		s1 = s2->next;
		io_free(s2->connection);
		server_free(s2);
	} while (s1 != state_server_list()->head);
}

void


@@ 285,8 285,8 @@ static int action_find_channel(char);
 * It can be cleaned up, and input.c is probably not the most ideal place for this */
#define MAX_SEARCH 128
struct channel *search_cptr; /* Used for iterative searching, before setting the current channel */
static char search_buff[MAX_SEARCH];
static char *search_ptr = search_buff;
static char search_buf[MAX_SEARCH + 1];
static size_t search_i;

static struct channel* search_channels(struct channel*, char*);
static struct channel*


@@ 342,9 342,13 @@ action_close_server(char c)
		if ((state.current_channel = c->server->next->channel) == c->server->channel)
			state.current_channel = state.default_channel;

		if ((ret = io_sendf(s->connection, "QUIT %s", DEFAULT_QUIT_MESG)))
		if ((ret = io_sendf(s->connection, "QUIT :%s", DEFAULT_QUIT_MESG)))
			newlinef(s->channel, 0, "-!!-", "sendf fail: %s", io_err(ret));

		// FIXME:
		// - state_server_free shouldn't have to call io/dx/free.
		// - server objects can have void* connections and remove
		//   the dependancy on io.h
		io_dx(s->connection);
		server_list_del(state_server_list(), s);
		io_free(s->connection);


@@ 386,18 390,16 @@ action_find_channel(char c)
{
	/* Incremental channel search */

	/* \n confirms selecting the current match */
	if (c == '\n' && search_cptr) {
		*(search_ptr = search_buff) = '\0';
		channel_set_current(search_cptr);
		search_cptr = NULL;
		draw_all();
		return 1;
	}

	/* \n, Esc, ^C cancels a search if no results are found */
	if (c == '\n' || c == 0x1b || c == CTRL('c')) {
		*(search_ptr = search_buff) = '\0';

		/* Confirm non-empty match */
		if (c == '\n' && search_cptr)
			channel_set_current(search_cptr);

		search_buf[0] = 0;
		search_i = 0;
		search_cptr = NULL;
		return 1;
	}



@@ 405,46 407,44 @@ action_find_channel(char c)
	 * or resets search criteria if no match */
	if (c == CTRL('f')) {
		if (search_cptr == NULL) {
			*(search_ptr = search_buff) = '\0';
			search_buf[0] = 0;
			search_i = 0;
			action(action_find_channel, "Find: ");
			return 0;
		}

		search_cptr = search_channels(search_cptr, search_buff);
	} else if (c == 0x7f && search_ptr > search_buff) {
		search_cptr = search_channels(search_cptr, search_buf);
	} else if (c == 0x7f && search_i) {
		/* Backspace */
		search_buf[--search_i] = 0;
		search_cptr = search_channels(current_channel(), search_buf);

		*(--search_ptr) = '\0';

		search_cptr = search_channels(current_channel(), search_buff);
	} else if (isprint(c) && search_ptr < search_buff + MAX_SEARCH && (search_cptr != NULL || *search_buff == '\0')) {
	} else if (isprint(c) && search_i < MAX_SEARCH) {
		/* All other input */

		*(search_ptr++) = c;
		*search_ptr = '\0';

		search_cptr = search_channels(current_channel(), search_buff);
		search_buf[search_i++] = c;
		search_buf[search_i] = 0;
		search_cptr = search_channels(current_channel(), search_buf);
	}

	/* Reprint the action message */
	if (search_cptr == NULL) {
		if (*search_buff)
			action(action_find_channel, "Find: NO MATCH -- %s", search_buff);
		if (*search_buf)
			action(action_find_channel, "Find: NO MATCH -- %s", search_buf);
		else
			action(action_find_channel, "Find: ");
	} else {
		/* Found a channel */
		if (search_cptr->server == current_channel()->server) {
			action(action_find_channel, "Find: %s -- %s",
					search_cptr->name, search_buff);
					search_cptr->name, search_buf);
		} else {
			if (!strcmp(search_cptr->server->port, "6667"))
				action(action_find_channel, "Find: %s/%s -- %s",
						search_cptr->server->host, search_cptr->name, search_buff);
						search_cptr->server->host, search_cptr->name, search_buf);
			else
				action(action_find_channel, "Find: %s:%s/%s -- %s",
						search_cptr->server->host, search_cptr->server->port,
						search_cptr->name, search_buff);
						search_cptr->name, search_buf);
		}
	}



@@ 586,8 586,12 @@ buffer_scrollback_forw(struct channel *c)
	draw_status();
}

/* Usefull server/channel structure abstractions for drawing */

/* FIXME:
 *  - These abstractions should take into account the new component hierarchy
 *    and have the backwards pointer from channel to server removed, in favour
 *    of passing a current_server() to handlers
 *  - The server's channel should not be part of the server's channel_list
 */
struct channel*
channel_get_first(void)
{


@@ 732,16 736,15 @@ state_io_cxed(struct server *s)
static void
state_io_dxed(struct server *s, va_list ap)
{
	struct channel *c = s->channel->next;
	struct channel *c = s->channel;
	va_list ap_copy;

	do {
		va_copy(ap_copy, ap);
		_newline(s->channel, 0, "-!!-", va_arg(ap_copy, const char *), ap_copy);
		_newline(c, 0, "-!!-", va_arg(ap_copy, const char *), ap_copy);
		va_end(ap_copy);

		channel_reset(c);

		c = c->next;
	} while (c != s->channel);
}



@@ 821,8 824,7 @@ send_cmnd(struct channel *c, char *buf)
	}

	if (!strcasecmp(cmnd, "quit")) {
		/* TODO: send optional quit message, close servers, free */
		exit(EXIT_SUCCESS);
		io_term();
	}

	if (!strcasecmp(cmnd, "connect")) {


@@ 851,10 853,13 @@ send_cmnd(struct channel *c, char *buf)
				channel_set_current(s->channel);
				newlinef(s->channel, 0, "-!!-", "already connected to %s:%s", host, port);
			} else {
				if ((s = server(host, port, pass, user, real)) == NULL)
					fatal("failed to create server");

				s = server(host, port, pass, user, real);
				s->connection = connection(s, host, port);

				// TODO: here just get/set the connection object
				server_list_add(state_server_list(), s);

				channel_set_current(s->channel);
				io_cx(s->connection);
				draw_all();

M src/state.h => src/state.h +1 -0
@@ 22,6 22,7 @@ struct channel* current_channel(void);
struct server_list* state_server_list(void);

void state_init(void);
void state_term(void);

// TODO: most of this stuff can be static
//TODO: move to channel.c, function of server's channel list

M src/utils/utils.c => src/utils/utils.c +0 -2
@@ 36,8 36,6 @@

static inline int irc_toupper(int);

int fatal_exit;

char*
getarg(char **str, const char *sep)
{

M src/utils/utils.h => src/utils/utils.h +11 -16
@@ 15,26 15,23 @@

#define UNUSED(X) ((void)(X))

#define message(TYPE, ...) \
	fprintf(stderr, "%s %s:%d:%s ", (TYPE), __FILE__, __LINE__, __func__); \
	fprintf(stderr, __VA_ARGS__); \
	fprintf(stderr, "\n");

#if (defined DEBUG) && !(defined TESTING)
#define DEBUG_MSG(...) \
	do { fprintf(stderr, "%s:%d:%-12s\t", __FILE__, __LINE__, __func__); \
	     fprintf(stderr,  __VA_ARGS__); \
	     fprintf(stderr, "\n"); \
	} while (0)
#define debug(...) \
	do { message("DEBUG", __VA_ARGS__); } while (0)
#else
#define DEBUG_MSG(...)
#define debug(...)
#endif

/* Irrecoverable error
 *   precluded in test.h to aggregate fatal errors in testcases */
#ifndef fatal
#define fatal(...) \
	do { fprintf(stderr, "FATAL ERROR: %s:%d:%s: ", __FILE__, __LINE__, __func__); \
	     fprintf(stderr, __VA_ARGS__); \
	     fprintf(stderr, "\n"); \
	     fatal_exit = 1; \
	     exit(EXIT_FAILURE); \
	} while (0)
	do { message("FATAL", __VA_ARGS__); exit(EXIT_FAILURE); } while (0)
#define fatal_noexit(...) \
	do { message("FATAL", __VA_ARGS__); } while (0)
#endif

/* FIXME: don't seperate trailing from params


@@ 69,6 66,4 @@ int check_pinged(const char*, const char*);
int parse_mesg(struct parsed_mesg*, char*);
int skip_sp(char**);

extern int fatal_exit;

#endif

M test/components/server.c => test/components/server.c +6 -32
@@ 1,41 1,16 @@
#include "test/test.h"
#include "src/components/server.c"
#include "src/components/mode.c"   /* mode_config_defaults */
#include "src/utils/utils.c"       /* skip_sp */

void
channel_set_current(struct channel *c)
{
	UNUSED(c);
}

struct channel*
new_channel(const char *n, struct server *s, enum channel_t t)
{
	/* FIXME: mock new_channel until channel() is implemented */

	UNUSED(n);
	UNUSED(s);
	UNUSED(t);
	return NULL;
}

struct connection*
connection(const void *s, const char *h, const char *p)
{
	/* Mock */

	UNUSED(s);
	UNUSED(h);
	UNUSED(p);
	return (void *)(s);
}
#include "src/components/channel.c"
#include "src/components/user.c"
#include "src/components/mode.c"
#include "src/components/input.c"
#include "src/components/buffer.c"
#include "src/utils/utils.c"

void
newline(struct channel *c, enum buffer_line_t t, const char *f, const char *m)
{
	/* Mock */

	UNUSED(c);
	UNUSED(t);
	UNUSED(f);


@@ 46,7 21,6 @@ void
newlinef(struct channel *c, enum buffer_line_t t, const char *f, const char *m, ...)
{
	/* Mock */

	UNUSED(c);
	UNUSED(t);
	UNUSED(f);

M test/io.c => test/io.c +0 -2
@@ 80,8 80,6 @@ test_io_recv(void)
int
main(void)
{
	PT_CF(pthread_once(&init_once, io_init));

	testcase tests[] = {
		TESTCASE(test_io_recv),
	};

M test/test.h => test/test.h +3 -1
@@ 80,7 80,7 @@ static void _print_testcase_name_(const char*);
#ifdef fatal
#error "test.h" should be the first include within testcase files
#else
#define fatal(...) \
#define _fatal(...) \
	do { \
		if (_assert_fatal_) { \
			longjmp(_tc_fatal_expected_, 1); \


@@ 90,6 90,8 @@ static void _print_testcase_name_(const char*);
			longjmp(_tc_fatal_unexpected_, 1); \
		} \
	} while (0)
#define fatal        _fatal
#define fatal_noexit _fatal
#endif

#define assert_fatal(X) \