~rcr/rirc

44c19ad7dc58b4574b69e776eb6846929d963662 — Richard Robbins 4 months ago 69431d4 + a5c1a6f
Merge branch 'dev' into static_analysis
M .builds/dev.yml => .builds/dev.yml +2 -2
@@ 28,8 28,8 @@ tasks:
      set -x
      ./scripts/sa_coverity_get.sh coverity
      ./scripts/sa_coverity_run.sh coverity
      ./scripts/sa_sonarcloud_get.sh sonarcloud
      ./scripts/sa_sonarcloud_run.sh sonarcloud
      ./scripts/sa_sonarscan.sh sonarscan
      ./scripts/sa_sonarscan.sh sonarscan

triggers:
  - action: email

M Makefile => Makefile +10 -9
@@ 1,4 1,4 @@
VERSION := 0.1.4
VERSION := 0.1.5

PREFIX   ?= /usr/local
PATH_BIN := $(DESTDIR)$(PREFIX)/bin


@@ 22,9 22,10 @@ RIRC_CFLAGS += -std=c11 -I. -DVERSION=\"$(VERSION)\"
RIRC_CFLAGS += -D_POSIX_C_SOURCE=200809L
RIRC_CFLAGS += -D_DARWIN_C_SOURCE

LDFLAGS ?= -flto
LDFLAGS += -lpthread

SRC       := $(shell find $(PATH_SRC) -name '*.c')
SRC       := $(shell find $(PATH_SRC) -name '*.c' | sort)
SRC_GPERF := $(patsubst %, %.out, $(shell find $(PATH_SRC) -name '*.gperf'))

# Release objects, debug objects, testcases


@@ 34,29 35,29 @@ OBJS_T := $(patsubst $(PATH_SRC)/%.c, $(PATH_BUILD)/%.t,    $(SRC))
OBJS_T += $(PATH_BUILD)/utils/tree.t # Header only file

rirc: $(RIRC_LIBS) $(SRC_GPERF) $(OBJS_R)
	@echo "  CC    $@"
	@echo "$(CC) $(LDFLAGS) $@"
	@$(CC) $(LDFLAGS) -o $@ $(OBJS_R) $(RIRC_LIBS)

rirc.debug: $(RIRC_LIBS) $(SRC_GPERF) $(OBJS_D)
	@echo "  CC    $@"
	@echo "$(CC) $(LDFLAGS) $@"
	@$(CC) $(LDFLAGS) -o $@ $(OBJS_D) $(RIRC_LIBS)

$(PATH_BUILD)/%.o: $(PATH_SRC)/%.c $(CONFIG) | $(PATH_BUILD)
	@echo "  CC    $<"
	@echo "$(CC) $(CFLAGS) $<"
	@$(CPP) $(CFLAGS) $(RIRC_CFLAGS) -MM -MP -MT $@ -MF $(@:.o=.o.d) $<
	@$(CC)  $(CFLAGS) $(RIRC_CFLAGS) -c -o $@ $<

$(PATH_BUILD)/%.db.o: $(PATH_SRC)/%.c $(CONFIG) | $(PATH_BUILD)
	@echo "  CC    $<"
	@echo "$(CC) $(CFLAGS_DEBUG) $<"
	@$(CPP) $(CFLAGS_DEBUG) $(RIRC_CFLAGS) -MM -MP -MT $@ -MF $(@:.o=.o.d) $<
	@$(CC)  $(CFLAGS_DEBUG) $(RIRC_CFLAGS) -c -o $@ $<

$(PATH_BUILD)/%.t: $(PATH_TEST)/%.c $(SRC_GPERF) $(CONFIG) | $(PATH_BUILD)
	@rm -f $(@:.t=.td)
	@$(CPP) $(CFLAGS_DEBUG) $(RIRC_CFLAGS) -MM -MP -MT $@ -MF $(@:.t=.t.d) $<
	@$(CC)  $(CFLAGS_DEBUG) $(RIRC_CFLAGS) -c -o $(@:.t=.t.o) $<
	@$(CC)  $(CFLAGS_DEBUG) $(RIRC_CFLAGS) -o $@ $(@:.t=.t.o)
	@./$@ || mv $@ $(@:.t=.td)
	@[ -f $@ ]

$(PATH_BUILD):
	@mkdir -p $(patsubst $(PATH_SRC)%, $(PATH_BUILD)%, $(shell find $(PATH_SRC) -type d))


@@ 71,8 72,8 @@ all:
	@$(MAKE) --silent rirc
	@$(MAKE) --silent rirc.debug

check:
	@$(MAKE) --silent $(OBJS_T)
check: $(OBJS_T)
	@[ ! "$$(find $(PATH_BUILD) -name '*.td' -print -quit)" ] && echo OK

clean:
	@rm -rfv rirc rirc.debug $(SRC_GPERF) $(PATH_BUILD)

M README.md => README.md +1 -1
@@ 106,4 106,4 @@ Keys:

## More info:

[https://rcr.io/rirc/](http://rcr.io/rirc/)
[https://rcr.io/rirc/](https://rcr.io/rirc/)

M config.def.h => config.def.h +3 -1
@@ 110,7 110,9 @@

/* [NETWORK] */

#define CA_CERT_PATH "/etc/ssl/certs/"
/* Default CA certifate file path
 *   ("": a list of known paths is checked) */
#define CA_CERT_PATH ""

/* Seconds before displaying ping
 *   Integer, [0, 150, 86400]

M docs/rirc.1 => docs/rirc.1 +1 -1
@@ 121,6 121,6 @@ IRC commands:
Richard 'rcr' Robbins
.ME .
.SH SEE ALSO
.UR http://rcr.io/rirc/
.UR https://rcr.io/rirc/
Additional documentation
.UE .

M lib/mbedtls.Makefile => lib/mbedtls.Makefile +2 -2
@@ 1,5 1,5 @@
MBEDTLS_VER     := 2.25.0
MBEDTLS_VER_SHA := f838f670f51070bc6b4ebf0c084affd9574652ded435b064969f36ce4e8b586d
MBEDTLS_VER     := 3.0.0
MBEDTLS_VER_SHA := 525bfde06e024c1218047dee1c8b4c89312df1a4b5658711009086cda5dfaa55

MBEDTLS_CFG := $(abspath $(PATH_LIB)/mbedtls.h)
MBEDTLS_SHA := $(abspath $(PATH_LIB)/mbedtls.sha256)

M lib/mbedtls.h => lib/mbedtls.h +2 -6
@@ 1,5 1,4 @@
#ifndef MBEDTLS_CONFIG_H
#define MBEDTLS_CONFIG_H
#define MBEDTLS_CONFIG_VERSION 0x03000000

/* Enabled ciphersuites, in order of preference.
 *   - Only ECHDE key exchanges, AEAD ciphers


@@ 92,6 91,7 @@
#define MBEDTLS_PK_PARSE_C
#define MBEDTLS_RSA_C
#define MBEDTLS_SHA1_C
#define MBEDTLS_SHA224_C
#define MBEDTLS_SHA256_C
#define MBEDTLS_SHA512_C
#define MBEDTLS_SSL_TLS_C


@@ 110,7 110,3 @@

/* Error strings */
#define MBEDTLS_ERROR_C

#include "mbedtls/check_config.h"

#endif

M scripts/coverage.sh => scripts/coverage.sh +1 -1
@@ 14,7 14,7 @@ rm -rf $CDIR && mkdir -p $CDIR
make clean
make check

GCNO=$(find bld -name '*.t.gcno')
GCNO=$(find build -name '*.t.gcno')

FILTER=$(cat << 'EOF'
{

M scripts/sa_coverity_get.sh => scripts/sa_coverity_get.sh +1 -1
@@ 24,6 24,6 @@ echo "*" > "$DIR/.gitignore"
curl -fsS https://scan.coverity.com/download/linux64 -o "$COVERITY_MD5" --data "token=$COVERITY_TOKEN&project=rcr%2Frirc&md5=1"
curl -fsS https://scan.coverity.com/download/linux64 -o "$COVERITY_TGZ" --data "token=$COVERITY_TOKEN&project=rcr%2Frirc"

printf "%s  %s" "$(cat "$COVERITY_MD5")" "$COVERITY_TGZ" | md5sum -c -
printf "%s  %s" "$(cat "$COVERITY_MD5")" "$COVERITY_TGZ" | md5sum --quiet -c -

tar -xzf "$COVERITY_TGZ" -C "$DIR" --strip-components 1

R scripts/sa_sonarcloud_get.sh => scripts/sa_sonarscan_get.sh +2 -2
@@ 8,7 8,7 @@ if [[ -z $1 ]]; then
	fail "Usage: '$0 dir'"
fi

SONAR_VER="4.6.0.2311"
SONAR_VER="4.6.2.2472"

BUILD_ZIP="$1/build-wrapper.zip"
SONAR_ZIP="$1/sonar-scanner.zip"


@@ 26,7 26,7 @@ curl -fsS "$BUILD_ZIP_URL" -o "$BUILD_ZIP"
curl -fsS "$SONAR_ZIP_URL" -o "$SONAR_ZIP"
curl -fsS "$SONAR_MD5_URL" -o "$SONAR_MD5"

printf "%s  %s" "$(cat "$SONAR_MD5")" "$SONAR_TGZ" | md5sum -c -
printf "%s  %s" "$(cat "$SONAR_MD5")" "$SONAR_ZIP" | md5sum --quiet -c -

unzip -qq "$BUILD_ZIP" -d "$1"
unzip -qq "$SONAR_ZIP" -d "$1"

R scripts/sa_sonarcloud_run.sh => scripts/sa_sonarscan_run.sh +1 -1
@@ 10,7 10,7 @@ fi

DIR="$1"

SONAR_VER="4.6.0.2311"
SONAR_VER="4.6.2.2472"

SONAR_CONFIG="$DIR/sonar-project.properties"


M scripts/sanitizers_build.sh => scripts/sanitizers_build.sh +2 -2
@@ 7,13 7,13 @@ export CC=clang
export CFLAGS_DEBUG="-fsanitize=address,undefined -fno-omit-frame-pointer"
export LDFLAGS="-fsanitize=address,undefined -fuse-ld=lld"

make -e clean rirc.debug
make clean rirc.debug

mv rirc.debug rirc.debug.address

export CFLAGS_DEBUG="-fsanitize=thread,undefined -fno-omit-frame-pointer"
export LDFLAGS="-fsanitize=thread,undefined -fuse-ld=lld"

make -e clean rirc.debug
make clean rirc.debug

mv rirc.debug rirc.debug.thread

M scripts/sanitizers_test.sh => scripts/sanitizers_test.sh +1 -1
@@ 9,4 9,4 @@ export LDFLAGS="-fsanitize=address,undefined -fuse-ld=lld"
# for core dumps:
# export ASAN_OPTIONS="abort_on_error=1:disable_coredump=0"

make -e clean check
make clean check

M scripts/stack_usage.sh => scripts/stack_usage.sh +3 -3
@@ 3,8 3,8 @@
set -e

export CC=gcc
export CFLAGS_DEBUG="-fstack-usage"
export CFLAGS="-fstack-usage"

make -e clean rirc.debug
make clean rirc

find bld -name "*.su" -exec cat "{}" ";" | sort -n -k2 | column -t
find build -name "*.su" -exec cat "{}" ";" | sort -n -k2 | column -t

M src/components/channel.h => src/components/channel.h +2 -2
@@ 20,9 20,9 @@ enum channel_type
{
	CHANNEL_T_INVALID,
	CHANNEL_T_RIRC,    /* Default buffer */
	CHANNEL_T_CHANNEL, /* Channel message buffer */
	CHANNEL_T_CHANNEL, /* Channel buffer */
	CHANNEL_T_PRIVMSG, /* Privmsg buffer */
	CHANNEL_T_SERVER,  /* Server message buffer */
	CHANNEL_T_PRIVATE, /* Private message buffer */
	CHANNEL_T_SIZE
};


M src/components/server.c => src/components/server.c +1 -1
@@ 190,7 190,7 @@ server_set_chans(struct server *s, const char *str)
		if (irc_ischan(chan))
			c = channel(chan, CHANNEL_T_CHANNEL);
		else
			c = channel(chan, CHANNEL_T_PRIVATE);
			c = channel(chan, CHANNEL_T_PRIVMSG);

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

M src/draw.c => src/draw.c +9 -12
@@ 636,13 636,13 @@ static void
draw_status(struct channel *c)
{
	/* server buffer:
	 *  -[usermodes]-(ping)-(scrollback)
	 *  -[+usermodes]-(ping)-(scrollback)
	 *
	 * private buffer:
	 *  -[usermodes]-[priv]-(ping)-(scrollback)
	 * privmsg buffer:
	 *  -[+usermodes]-[privmsg]-(ping)-(scrollback)
	 *
	 * channel buffer:
	 *  -[usermodes]-[chantype chanmodes chancount]-(ping)-(scrollback)
	 *  -[+usermodes]-[+chanmodes chancount]-(ping)-(scrollback)
	 */

	#define STATUS_SEP_HORZ \


@@ 666,21 666,18 @@ draw_status(struct channel *c)
	}

	/* -[priv] */
	if (c->type == CHANNEL_T_PRIVATE) {
	if (c->type == CHANNEL_T_PRIVMSG) {
		if (!drawf(&cols, STATUS_SEP_HORZ))
			return;
		if (!drawf(&cols, "[priv]"))
		if (!drawf(&cols, "[privmsg]"))
			return;
	}

	/* -[chantype chanmodes chancount] */
	if (c->type == CHANNEL_T_CHANNEL) {
	/* -[chanmodes chancount] */
	if (c->type == CHANNEL_T_CHANNEL && c->joined) {
		if (!drawf(&cols, STATUS_SEP_HORZ))
			return;
		if (!drawf(&cols, "[%c %s %u]",
				c->chanmodes.prefix,
				c->chanmodes_str.str,
				c->users.count))
		if (!drawf(&cols, "[+%s %u]", c->chanmodes_str.str, c->users.count))
			return;
	}


M src/handlers/irc_ctcp.c => src/handlers/irc_ctcp.c +1 -1
@@ 113,7 113,7 @@ ctcp_request_action(struct server *s, const char *from, const char *targ, char *

	if (!irc_strcmp(s->casemapping, targ, s->nick)) {
		if ((c = channel_list_get(&s->clist, from, s->casemapping)) == NULL) {
			c = channel(from, CHANNEL_T_PRIVATE);
			c = channel(from, CHANNEL_T_PRIVMSG);
			c->activity = ACTIVITY_PINGED;
			c->server = s;
			channel_list_add(&s->clist, c);

M src/handlers/irc_recv.c => src/handlers/irc_recv.c +67 -15
@@ 46,18 46,20 @@ static int irc_numeric_329(struct server*, struct irc_message*);
static int irc_numeric_332(struct server*, struct irc_message*);
static int irc_numeric_333(struct server*, struct irc_message*);
static int irc_numeric_353(struct server*, struct irc_message*);
static int irc_numeric_401(struct server*, struct irc_message*);
static int irc_numeric_403(struct server*, struct irc_message*);
static int irc_numeric_433(struct server*, struct irc_message*);

static int irc_recv_numeric(struct server*, struct irc_message*);
static int recv_mode_chanmodes(struct irc_message*, const struct mode_cfg*, struct server*, struct channel*);
static int recv_mode_usermodes(struct irc_message*, const struct mode_cfg*, struct server*);

static unsigned quit_threshold    = FILTER_THRESHOLD_QUIT;
static unsigned join_threshold    = FILTER_THRESHOLD_JOIN;
static unsigned part_threshold    = FILTER_THRESHOLD_PART;
static unsigned account_threshold = FILTER_THRESHOLD_ACCOUNT;
static unsigned away_threshold    = FILTER_THRESHOLD_AWAY;
static unsigned chghost_threshold = FILTER_THRESHOLD_CHGHOST;
static unsigned threshold_account = FILTER_THRESHOLD_ACCOUNT;
static unsigned threshold_away    = FILTER_THRESHOLD_AWAY;
static unsigned threshold_chghost = FILTER_THRESHOLD_CHGHOST;
static unsigned threshold_join    = FILTER_THRESHOLD_JOIN;
static unsigned threshold_part    = FILTER_THRESHOLD_PART;
static unsigned threshold_quit    = FILTER_THRESHOLD_QUIT;

static const irc_recv_f irc_numerics[] = {
	  [1] = irc_numeric_001,    /* RPL_WELCOME */


@@ 154,9 156,9 @@ static const irc_recv_f irc_numerics[] = {
	[381] = irc_generic_info,   /* RPL_YOUREOPER */
	[391] = irc_generic_info,   /* RPL_TIME */
	[396] = irc_generic_info,   /* RPL_VISIBLEHOST */
	[401] = irc_generic_error,  /* ERR_NOSUCHNICK */
	[401] = irc_numeric_401,    /* ERR_NOSUCHNICK */
	[402] = irc_generic_error,  /* ERR_NOSUCHSERVER */
	[403] = irc_generic_error,  /* ERR_NOSUCHCHANNEL */
	[403] = irc_numeric_403,    /* ERR_NOSUCHCHANNEL */
	[404] = irc_generic_error,  /* ERR_CANNOTSENDTOCHAN */
	[405] = irc_generic_error,  /* ERR_TOOMANYCHANNELS */
	[406] = irc_generic_error,  /* ERR_WASNOSUCHNICK */


@@ 556,6 558,56 @@ irc_numeric_353(struct server *s, struct irc_message *m)
}

static int
irc_numeric_401(struct server *s, struct irc_message *m)
{
	/* <nick> :No such nick/channel */

	char *message;
	char *nick;
	struct channel *c;

	if (!irc_message_param(m, &nick))
		failf(s, "ERR_NOSUCHNICK: nick is null");

	if (!(c = channel_list_get(&(s->clist), nick, s->casemapping)))
		c = s->channel;

	irc_message_param(m, &message);

	if (message && *message)
		newlinef(c, 0, FROM_ERROR, "[%s] %s", nick, message);
	else
		newlinef(c, 0, FROM_ERROR, "[%s] No such nick/channel", nick);

	return 0;
}

static int
irc_numeric_403(struct server *s, struct irc_message *m)
{
	/* <chan> :No such channel */

	char *message;
	char *chan;
	struct channel *c;

	if (!irc_message_param(m, &chan))
		failf(s, "ERR_NOSUCHCHANNEL: chan is null");

	if (!(c = channel_list_get(&(s->clist), chan, s->casemapping)))
		c = s->channel;

	irc_message_param(m, &message);

	if (message && *message)
		newlinef(c, 0, FROM_ERROR, "[%s] %s", chan, message);
	else
		newlinef(c, 0, FROM_ERROR, "[%s] No such channel", chan);

	return 0;
}

static int
irc_numeric_433(struct server *s, struct irc_message *m)
{
	/* 433 <nick> :Nickname is already in use */


@@ 692,7 744,7 @@ recv_join(struct server *s, struct irc_message *m)
	if (user_list_add(&(c->users), s->casemapping, m->from, MODE_EMPTY) == USER_ERR_DUPLICATE)
		failf(s, "JOIN: user '%s' already on channel '%s'", m->from, chan);

	if (!join_threshold || join_threshold > c->users.count) {
	if (!threshold_join || threshold_join > c->users.count) {

		if (s->ircv3_caps.extended_join.set) {



@@ 1099,7 1151,7 @@ recv_part(struct server *s, struct irc_message *m)
		if (user_list_del(&(c->users), s->casemapping, m->from) == USER_ERR_NOT_FOUND)
			failf(s, "PART: nick '%s' not found in '%s'", m->from, chan);

		if (!part_threshold || part_threshold > c->users.count) {
		if (!threshold_part || threshold_part > c->users.count) {

			if (message && *message)
				newlinef(c, 0, FROM_PART, "%s!%s has parted (%s)", m->from, m->host, message);


@@ 1167,7 1219,7 @@ recv_privmsg(struct server *s, struct irc_message *m)
	if (!strcmp(target, s->nick)) {

		if ((c = channel_list_get(&s->clist, m->from, s->casemapping)) == NULL) {
			c = channel(m->from, CHANNEL_T_PRIVATE);
			c = channel(m->from, CHANNEL_T_PRIVMSG);
			c->server = s;
			channel_list_add(&s->clist, c);
		}


@@ 1214,7 1266,7 @@ recv_quit(struct server *s, struct irc_message *m)
	do {
		if (user_list_del(&(c->users), s->casemapping, m->from) == USER_ERR_NONE) {

			if (quit_threshold && quit_threshold <= c->users.count)
			if (threshold_quit && threshold_quit <= c->users.count)
				continue;

			if (message && *message)


@@ 1286,7 1338,7 @@ recv_ircv3_account(struct server *s, struct irc_message *m)
		if (!user_list_get(&(c->users), s->casemapping, m->from, 0))
			continue;

		if (account_threshold && account_threshold <= c->users.count)
		if (threshold_account && threshold_account <= c->users.count)
			continue;

		if (!strcmp(account, "*"))


@@ 1316,7 1368,7 @@ recv_ircv3_away(struct server *s, struct irc_message *m)
		if (!user_list_get(&(c->users), s->casemapping, m->from, 0))
			continue;

		if (away_threshold && away_threshold <= c->users.count)
		if (threshold_away && threshold_away <= c->users.count)
			continue;

		if (message)


@@ 1351,7 1403,7 @@ recv_ircv3_chghost(struct server *s, struct irc_message *m)
		if (!user_list_get(&(c->users), s->casemapping, m->from, 0))
			continue;

		if (chghost_threshold && chghost_threshold <= c->users.count)
		if (threshold_chghost && threshold_chghost <= c->users.count)
			continue;

		newlinef(c, 0, FROM_INFO, "%s has changed user/host: %s/%s", m->from, user, host);

M src/handlers/irc_send.c => src/handlers/irc_send.c +94 -63
@@ 22,7 22,7 @@
	         failf((C), "Send fail: %s", io_err(ret)); \
	} while (0)

static const char* nick_or_priv(struct channel*, char*);
static const char* irc_send_target(struct channel*, char*);

int
irc_send_command(struct server *s, struct channel *c, char *m)


@@ 58,7 58,7 @@ irc_send_command(struct server *s, struct channel *c, char *m)
}

int
irc_send_privmsg(struct server *s, struct channel *c, char *m)
irc_send_message(struct server *s, struct channel *c, const char *m)
{
	if (!s)
		failf(c, "This is not a server");


@@ 66,7 66,7 @@ irc_send_privmsg(struct server *s, struct channel *c, char *m)
	if (!s->registered)
		failf(c, "Not registered with server");

	if (!(c->type == CHANNEL_T_CHANNEL || c->type == CHANNEL_T_PRIVATE))
	if (!(c->type == CHANNEL_T_CHANNEL || c->type == CHANNEL_T_PRIVMSG))
		failf(c, "This is not a channel");

	if (c->type == CHANNEL_T_CHANNEL && (!c->joined || c->parted))


@@ 83,21 83,21 @@ irc_send_privmsg(struct server *s, struct channel *c, char *m)
}

static const char*
nick_or_priv(struct channel *c, char *m)
irc_send_target(struct channel *c, char *m)
{
	const char *nick;
	const char *target;

	if ((nick = irc_strsep(&m)))
		return nick;
	if ((target = irc_strsep(&m)))
		return target;

	if (c->type == CHANNEL_T_PRIVATE)
	if (c->type == CHANNEL_T_PRIVMSG)
		return c->name;

	return NULL;
}

static int
send_away(struct server *s, struct channel *c, char *m)
irc_send_away(struct server *s, struct channel *c, char *m)
{
	if (irc_strtrim(&m))
		sendf(s, c, "AWAY :%s", m);


@@ 108,23 108,23 @@ send_away(struct server *s, struct channel *c, char *m)
}

static int
send_notice(struct server *s, struct channel *c, char *m)
irc_send_notice(struct server *s, struct channel *c, char *m)
{
	const char *targ;
	const char *target;

	if (!(targ = irc_strsep(&m)))
	if (!(target = irc_strsep(&m)))
		failf(c, "Usage: /notice <target> <message>");

	if (!m || !*m)
		failf(c, "Usage: /notice <target> <message>");

	sendf(s, c, "NOTICE %s :%s", targ, m);
	sendf(s, c, "NOTICE %s :%s", target, m);

	return 0;
}

static int
send_part(struct server *s, struct channel *c, char *m)
irc_send_part(struct server *s, struct channel *c, char *m)
{
	if (c->type != CHANNEL_T_CHANNEL)
		failf(c, "This is not a channel");


@@ 138,23 138,54 @@ send_part(struct server *s, struct channel *c, char *m)
}

static int
send_privmsg(struct server *s, struct channel *c, char *m)
irc_send_privmsg(struct server *s, struct channel *c, char *m)
{
	const char *targ;
	const char *target;
	char *dup;
	char *p1;
	char *p2;

	if (!(targ = irc_strsep(&m)))
	if (!(target = irc_strsep(&m)))
		failf(c, "Usage: /privmsg <target> <message>");

	if (!m || !*m)
		failf(c, "Usage: /privmsg <target> <message>");

	sendf(s, c, "PRIVMSG %s :%s", targ, m);
	p2 = dup = strdup(target);

	do {
		struct channel *c;

		p1 = p2;
		p2 = strchr(p2, ',');

		if (p2)
			*p2++ = 0;

		if (!(c = channel_list_get(&s->clist, p1, s->casemapping))) {

			if (irc_isnick(p1))
				c = channel(p1, CHANNEL_T_PRIVMSG);
			else
				c = channel(p1, CHANNEL_T_CHANNEL);

			c->server = s;
			channel_list_add(&s->clist, c);
		}

		newlinef(c, BUFFER_LINE_CHAT, s->nick, "%s", m);

	} while (p2);

	free(dup);

	sendf(s, c, "PRIVMSG %s :%s", target, m);

	return 0;
}

static int
send_quit(struct server *s, struct channel *c, char *m)
irc_send_quit(struct server *s, struct channel *c, char *m)
{
	s->quitting = 1;



@@ 167,7 198,7 @@ send_quit(struct server *s, struct channel *c, char *m)
}

static int
send_topic(struct server *s, struct channel *c, char *m)
irc_send_topic(struct server *s, struct channel *c, char *m)
{
	if (c->type != CHANNEL_T_CHANNEL)
		failf(c, "This is not a channel");


@@ 181,7 212,7 @@ send_topic(struct server *s, struct channel *c, char *m)
}

static int
send_topic_unset(struct server *s, struct channel *c, char *m)
irc_send_topic_unset(struct server *s, struct channel *c, char *m)
{
	if (c->type != CHANNEL_T_CHANNEL)
		failf(c, "This is not a channel");


@@ 195,56 226,56 @@ send_topic_unset(struct server *s, struct channel *c, char *m)
}

static int
send_ctcp_action(struct server *s, struct channel *c, char *m)
irc_send_ctcp_action(struct server *s, struct channel *c, char *m)
{
	const char *nick;
	const char *target;

	if (!(nick = irc_strsep(&m)) || !irc_strtrim(&m))
		failf(c, "Usage: /ctcp-action <nick> <text>");
	if (!(target = irc_strsep(&m)) || !irc_strtrim(&m))
		failf(c, "Usage: /ctcp-action <target> <text>");

	sendf(s, c, "PRIVMSG %s :\001ACTION %s\001", nick, m);
	sendf(s, c, "PRIVMSG %s :\001ACTION %s\001", target, m);

	return 0;
}

static int
send_ctcp_clientinfo(struct server *s, struct channel *c, char *m)
irc_send_ctcp_clientinfo(struct server *s, struct channel *c, char *m)
{
	const char *nick;
	const char *target;

	if (!(nick = nick_or_priv(c, m)))
		failf(c, "Usage: /ctcp-clientinfo <nick>");
	if (!(target = irc_send_target(c, m)))
		failf(c, "Usage: /ctcp-clientinfo <target>");

	sendf(s, c, "PRIVMSG %s :\001CLIENTINFO\001", nick);
	sendf(s, c, "PRIVMSG %s :\001CLIENTINFO\001", target);

	return 0;
}

static int
send_ctcp_finger(struct server *s, struct channel *c, char *m)
irc_send_ctcp_finger(struct server *s, struct channel *c, char *m)
{
	const char *nick;
	const char *target;

	if (!(nick = nick_or_priv(c, m)))
		failf(c, "Usage: /ctcp-finger <nick>");
	if (!(target = irc_send_target(c, m)))
		failf(c, "Usage: /ctcp-finger <target>");

	sendf(s, c, "PRIVMSG %s :\001FINGER\001", nick);
	sendf(s, c, "PRIVMSG %s :\001FINGER\001", target);

	return 0;
}

static int
send_ctcp_ping(struct server *s, struct channel *c, char *m)
irc_send_ctcp_ping(struct server *s, struct channel *c, char *m)
{
	const char *nick;
	const char *target;
	struct timeval t;

	if (!(nick = nick_or_priv(c, m)))
		failf(c, "Usage: /ctcp-ping <nick>");
	if (!(target = irc_send_target(c, m)))
		failf(c, "Usage: /ctcp-ping <target>");

	(void) gettimeofday(&t, NULL);

	sendf(s, c, "PRIVMSG %s :\001PING %llu %llu\001", nick,
	sendf(s, c, "PRIVMSG %s :\001PING %llu %llu\001", target,
		(unsigned long long)t.tv_sec,
		(unsigned long long)t.tv_usec);



@@ 252,59 283,59 @@ send_ctcp_ping(struct server *s, struct channel *c, char *m)
}

static int
send_ctcp_source(struct server *s, struct channel *c, char *m)
irc_send_ctcp_source(struct server *s, struct channel *c, char *m)
{
	const char *nick;
	const char *target;

	if (!(nick = nick_or_priv(c, m)))
		failf(c, "Usage: /ctcp-source <nick>");
	if (!(target = irc_send_target(c, m)))
		failf(c, "Usage: /ctcp-source <target>");

	sendf(s, c, "PRIVMSG %s :\001SOURCE\001", nick);
	sendf(s, c, "PRIVMSG %s :\001SOURCE\001", target);

	return 0;
}

static int
send_ctcp_time(struct server *s, struct channel *c, char *m)
irc_send_ctcp_time(struct server *s, struct channel *c, char *m)
{
	const char *nick;
	const char *target;

	if (!(nick = nick_or_priv(c, m)))
		failf(c, "Usage: /ctcp-time <nick>");
	if (!(target = irc_send_target(c, m)))
		failf(c, "Usage: /ctcp-time <target>");

	sendf(s, c, "PRIVMSG %s :\001TIME\001", nick);
	sendf(s, c, "PRIVMSG %s :\001TIME\001", target);

	return 0;
}

static int
send_ctcp_userinfo(struct server *s, struct channel *c, char *m)
irc_send_ctcp_userinfo(struct server *s, struct channel *c, char *m)
{
	const char *nick;
	const char *target;

	if (!(nick = nick_or_priv(c, m)))
		failf(c, "Usage: /ctcp-userinfo <nick>");
	if (!(target = irc_send_target(c, m)))
		failf(c, "Usage: /ctcp-userinfo <target>");

	sendf(s, c, "PRIVMSG %s :\001USERINFO\001", nick);
	sendf(s, c, "PRIVMSG %s :\001USERINFO\001", target);

	return 0;
}

static int
send_ctcp_version(struct server *s, struct channel *c, char *m)
irc_send_ctcp_version(struct server *s, struct channel *c, char *m)
{
	const char *nick;
	const char *target;

	if (!(nick = nick_or_priv(c, m)))
		failf(c, "Usage: /ctcp-version <nick>");
	if (!(target = irc_send_target(c, m)))
		failf(c, "Usage: /ctcp-version <target>");

	sendf(s, c, "PRIVMSG %s :\001VERSION\001", nick);
	sendf(s, c, "PRIVMSG %s :\001VERSION\001", target);

	return 0;
}

static int
send_ircv3_cap_ls(struct server *s, struct channel *c, char *m)
irc_send_ircv3_cap_ls(struct server *s, struct channel *c, char *m)
{
	if (irc_strtrim(&m))
		failf(c, "Usage: /cap-ls");


@@ 315,7 346,7 @@ send_ircv3_cap_ls(struct server *s, struct channel *c, char *m)
}

static int
send_ircv3_cap_list(struct server *s, struct channel *c, char *m)
irc_send_ircv3_cap_list(struct server *s, struct channel *c, char *m)
{
	if (irc_strtrim(&m))
		failf(c, "Usage: /cap-list");

M src/handlers/irc_send.gperf => src/handlers/irc_send.gperf +20 -20
@@ 24,15 24,15 @@
	X(ls) \
	X(list)

#define X(cmd) static int send_##cmd(struct server*, struct channel*, char*);
#define X(cmd) static int irc_send_##cmd(struct server*, struct channel*, char*);
SEND_HANDLERS
#undef X

#define X(cmd) static int send_ctcp_##cmd(struct server*, struct channel*, char*);
#define X(cmd) static int irc_send_ctcp_##cmd(struct server*, struct channel*, char*);
SEND_CTCP_HANDLERS
#undef X

#define X(cmd) static int send_ircv3_cap_##cmd(struct server*, struct channel*, char*);
#define X(cmd) static int irc_send_ircv3_cap_##cmd(struct server*, struct channel*, char*);
SEND_IRCV3_CAP_HANDLERS
#undef X



@@ 56,21 56,21 @@ struct send_handler
%define initializer-suffix ,(irc_send_f)0
struct send_handler;
%%
CAP-LS,          send_ircv3_cap_ls
CAP-LIST,        send_ircv3_cap_list
CTCP-ACTION,     send_ctcp_action
CTCP-CLIENTINFO, send_ctcp_clientinfo
CTCP-FINGER,     send_ctcp_finger
CTCP-PING,       send_ctcp_ping
CTCP-SOURCE,     send_ctcp_source
CTCP-TIME,       send_ctcp_time
CTCP-USERINFO,   send_ctcp_userinfo
CTCP-VERSION,    send_ctcp_version
AWAY,            send_away
NOTICE,          send_notice
PART,            send_part
PRIVMSG,         send_privmsg
QUIT,            send_quit
TOPIC,           send_topic
TOPIC-UNSET,     send_topic_unset
CAP-LS,          irc_send_ircv3_cap_ls
CAP-LIST,        irc_send_ircv3_cap_list
CTCP-ACTION,     irc_send_ctcp_action
CTCP-CLIENTINFO, irc_send_ctcp_clientinfo
CTCP-FINGER,     irc_send_ctcp_finger
CTCP-PING,       irc_send_ctcp_ping
CTCP-SOURCE,     irc_send_ctcp_source
CTCP-TIME,       irc_send_ctcp_time
CTCP-USERINFO,   irc_send_ctcp_userinfo
CTCP-VERSION,    irc_send_ctcp_version
AWAY,            irc_send_away
NOTICE,          irc_send_notice
PART,            irc_send_part
PRIVMSG,         irc_send_privmsg
QUIT,            irc_send_quit
TOPIC,           irc_send_topic
TOPIC-UNSET,     irc_send_topic_unset
%%

M src/handlers/irc_send.h => src/handlers/irc_send.h +1 -1
@@ 5,6 5,6 @@
#include "src/components/server.h"

int irc_send_command(struct server*, struct channel*, char*);
int irc_send_privmsg(struct server*, struct channel*, char*);
int irc_send_message(struct server*, struct channel*, const char*);

#endif

M src/io.c => src/io.c +32 -20
@@ 156,6 156,15 @@ static int io_tls_x509_vrfy(struct connection*);
static void io_tls_init(void);
static void io_tls_term(void);

const char *ca_cert_paths[] = {
	"/etc/ssl/ca-bundle.pem",
	"/etc/ssl/cert.pem",
	"/etc/ssl/certs/ca-certificates.crt",
	"/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem",
	"/etc/pki/tls/cacert.pem",
	"/etc/pki/tls/certs/ca-bundle.crt",
};

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


@@ 308,6 317,8 @@ io_start(void)
{
	io_running = 1;

	io_tty_winsize();

	while (io_running) {

		char buf[128];


@@ 566,7 577,7 @@ io_cx_read(struct connection *cx, uint32_t timeout)
	struct pollfd fd[1];
	unsigned char buf[1024];

	fd[0].fd = cx->net_ctx.fd;
	fd[0].fd = cx->net_ctx.MBEDTLS_PRIVATE(fd);
	fd[0].events = POLLIN;

	while ((ret = poll(fd, 1, timeout)) < 0 && errno == EAGAIN)


@@ 650,8 661,6 @@ io_tty_init(void)

	if (atexit(io_tty_term))
		fatal("atexit");

	io_tty_winsize();
}

static void


@@ 741,7 750,7 @@ io_net_connect(struct connection *cx)
err:
	freeaddrinfo(res);

	return (cx->net_ctx.fd = ret);
	return (cx->net_ctx.MBEDTLS_PRIVATE(fd) = ret);
}

static void


@@ 881,15 890,13 @@ io_tls_x509_vrfy(struct connection *cx)
	if (mbedtls_x509_crt_verify_info(buf, sizeof(buf), "", ret) < 0)
		return -1;

	s = buf;
	for (s = buf; s && *s; s = p) {

	do {
		if ((p = strchr(buf, '\n')))
			*p++ = 0;

		io_error(cx, " .... %s", s);

	} while ((s = p));
	}

	return 0;
}


@@ 911,9 918,8 @@ io_tls_err(int err)
static void
io_tls_init(void)
{
	char buf[512];
	const unsigned char pers[] = "rirc-drbg-seed";
	int ret;
	struct timespec ts;

	mbedtls_ctr_drbg_init(&tls_ctr_drbg);
	mbedtls_entropy_init(&tls_entropy);


@@ 922,23 928,29 @@ io_tls_init(void)
	if (atexit(io_tls_term))
		fatal("atexit");

	if (timespec_get(&ts, TIME_UTC) != TIME_UTC)
		fatal("timespec_get");

	if (snprintf(buf, sizeof(buf), "rirc-%lu-%lu", ts.tv_sec, ts.tv_nsec) < 0)
		fatal("snprintf");

	if ((ret = mbedtls_ctr_drbg_seed(
			&tls_ctr_drbg,
			mbedtls_entropy_func,
			&tls_entropy,
			(const unsigned char *)buf,
			strlen(buf)))) {
			pers,
			sizeof(pers)))) {
		fatal("mbedtls_ctr_drbg_seed: %s", io_tls_err(ret));
	}

	if ((ret = mbedtls_x509_crt_parse_path(&tls_x509_crt, ca_cert_path)) < 0)
		fatal("mbedtls_x509_crt_parse_path: %s", io_tls_err(ret));
	if (ca_cert_path && *ca_cert_path) {

		if ((ret = mbedtls_x509_crt_parse_file(&tls_x509_crt, ca_cert_path)) < 0)
			fatal("mbedtls_x509_crt_parse_file: '%s': %s", ca_cert_path, io_tls_err(ret));

	} else {

		for (size_t i = 0; i < ARR_LEN(ca_cert_paths); i++) {
			if ((ret = mbedtls_x509_crt_parse_file(&tls_x509_crt, ca_cert_paths[i])) >= 0)
				return;
		}

		fatal("Failed to load ca cert: %s", io_tls_err(ret));
	}
}

static void

M src/rirc.c => src/rirc.c +1 -1
@@ 28,7 28,7 @@ static int rirc_parse_args(int, char**);
#ifdef CA_CERT_PATH
const char *ca_cert_path = CA_CERT_PATH;
#else
#error "CA_CERT_PATH required"
const char *ca_cert_path;
#endif

#ifdef DEFAULT_NICKS

M src/state.c => src/state.c +10 -8
@@ 326,7 326,7 @@ state_channel_close(int action_confirm)

	if (action_confirm) {

		if (c->type == CHANNEL_T_CHANNEL || c->type == CHANNEL_T_PRIVATE)
		if (c->type == CHANNEL_T_CHANNEL || c->type == CHANNEL_T_PRIVMSG)
			action(action_close, "Close '%s'?   [y/n]", c->name);

		if (c->type == CHANNEL_T_SERVER)


@@ 336,14 336,18 @@ state_channel_close(int action_confirm)
		return;
	}

	if (c->type == CHANNEL_T_CHANNEL || c->type == CHANNEL_T_PRIVATE) {
	if (c->type == CHANNEL_T_CHANNEL || c->type == CHANNEL_T_PRIVMSG) {

		if (s->connected && c->type == CHANNEL_T_CHANNEL && !c->parted) {
			if ((ret = io_sendf(s->connection, "PART %s :%s", c->name, DEFAULT_PART_MESG)))
				server_error(s, "sendf fail: %s", io_err(ret));
		}

		channel_set_current(c->next);
		if (c == channel_get_last())
			channel_set_current(channel_get_prev(c));
		else
			channel_set_current(channel_get_next(c));

		channel_list_del(&(s->clist), c);
		channel_free(c);
		return;


@@ 597,7 601,6 @@ command(struct channel *c, char *buf)
			action(action_error, "clear: Unknown arg '%s'", arg);
			return;
		}

		state_channel_clear(0);
		return;
	}


@@ 607,7 610,6 @@ command(struct channel *c, char *buf)
			action(action_error, "close: Unknown arg '%s'", arg);
			return;
		}

		state_channel_close(0);
		return;
	}


@@ 767,18 769,18 @@ state_input_linef(struct channel *c)
	switch (buf[0]) {
		case ':':
			if (len > 1 && buf[1] == ':')
				irc_send_privmsg(current_channel()->server, current_channel(), buf + 1);
				irc_send_message(current_channel()->server, current_channel(), buf + 1);
			else
				command(current_channel(), buf + 1);
			break;
		case '/':
			if (len > 1 && buf[1] == '/')
				irc_send_privmsg(current_channel()->server, current_channel(), buf + 1);
				irc_send_message(current_channel()->server, current_channel(), buf + 1);
			else
				irc_send_command(current_channel()->server, current_channel(), buf + 1);
			break;
		default:
			irc_send_privmsg(current_channel()->server, current_channel(), buf);
			irc_send_message(current_channel()->server, current_channel(), buf);
	}

	return 1;

M src/utils/utils.h => src/utils/utils.h +2 -0
@@ 4,6 4,8 @@
#include <stdio.h>
#include <stdlib.h>

#define ARR_LEN(A) (sizeof((A)) / sizeof((A)[0]))

#define MAX(A, B) ((A) > (B) ? (A) : (B))
#define MIN(A, B) ((A) > (B) ? (B) : (A))


M test/components/buffer.c => test/components/buffer.c +1 -1
@@ 449,5 449,5 @@ main(void)
		TESTCASE(test_buffer_newline_prefix),
	};

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

M test/components/channel.c => test/components/channel.c +1 -1
@@ 51,5 51,5 @@ main(void)
		TESTCASE(test_channel_list)
	};

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

M test/components/input.c => test/components/input.c +1 -1
@@ 571,5 571,5 @@ main(void)
		TESTCASE(test_input_text_size)
	};

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

M test/components/ircv3.c => test/components/ircv3.c +1 -1
@@ 65,5 65,5 @@ main(void)
		TESTCASE(test_ircv3_caps_reset),
	};

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

M test/components/mode.c => test/components/mode.c +1 -1
@@ 546,5 546,5 @@ main(void)
		TESTCASE(test_chanmode_type)
	};

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

M test/components/server.c => test/components/server.c +3 -3
@@ 146,11 146,11 @@ test_server_set_chans(void)

	if (!(c = channel_list_get(&(s->clist), "b", s->casemapping)))
		test_abort("failed to find channel 'b'");
	assert_eq(c->type, CHANNEL_T_PRIVATE);
	assert_eq(c->type, CHANNEL_T_PRIVMSG);

	if (!(c = channel_list_get(&(s->clist), "c", s->casemapping)))
		test_abort("failed to find channel '#c'");
	assert_eq(c->type, CHANNEL_T_PRIVATE);
	assert_eq(c->type, CHANNEL_T_PRIVMSG);

	if (!(c = channel_list_get(&(s->clist), "#d", s->casemapping)))
		test_abort("failed to find channel 'd'");


@@ 304,5 304,5 @@ main(void)
		TESTCASE(test_parse_005)
	};

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

M test/components/user.c => test/components/user.c +1 -1
@@ 173,5 173,5 @@ main(void)
		TESTCASE(test_user_list_free)
	};

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

M test/draw.c => test/draw.c +1 -1
@@ 30,5 30,5 @@ main(void)
		TESTCASE(test_STUB)
	};

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

M test/handlers/irc_ctcp.c => test/handlers/irc_ctcp.c +21 -11
@@ 480,22 480,36 @@ test_recv_ctcp_response_version(void)
		"CTCP VERSION response from nick: 123 456 789");
}

int
main(void)
static int
test_init(void)
{
	c_chan = channel("chan", CHANNEL_T_CHANNEL);
	c_priv = channel("nick", CHANNEL_T_PRIVATE);

	s = server("h1", "p1", NULL, "u1", "r1");

	c_chan = channel("chan", CHANNEL_T_CHANNEL);
	c_priv = channel("nick", CHANNEL_T_PRIVMSG);

	if (!s || !c_chan || !c_priv)
		test_abort_main("Failed test setup");
		return -1;

	channel_list_add(&s->clist, c_chan);
	channel_list_add(&s->clist, c_priv);

	server_nick_set(s, "me");

	return 0;
}

static int
test_term(void)
{
	server_free(s);

	return 0;
}

int
main(void)
{
	struct testcase tests[] = {
		TESTCASE(test_recv_ctcp_request),
		TESTCASE(test_recv_ctcp_response),


@@ 511,9 525,5 @@ main(void)
#undef X
	};

	int ret = run_tests(tests);

	server_free(s);

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

M test/handlers/irc_recv.c => test/handlers/irc_recv.c +122 -22
@@ 36,6 36,9 @@ static struct irc_message m;
static struct channel *c1;
static struct channel *c2;
static struct channel *c3;
static struct channel *p1;
static struct channel *p2;
static struct channel *p3;
static struct server *s;

static void


@@ 232,6 235,82 @@ test_irc_numeric_353(void)
}

static void
test_irc_numeric_401(void)
{
	/* <nick> :No such nick/channel */

	server_reset(s);

	/* test errors */
	CHECK_RECV("401 me", 1, 1, 0);
	assert_strcmp(mock_chan[0], "host");
	assert_strcmp(mock_line[0], "ERR_NOSUCHNICK: nick is null");

	/* test channel buffer not found */
	CHECK_RECV("401 me #notfound", 0, 1, 0);
	assert_strcmp(mock_chan[0], "host");
	assert_strcmp(mock_line[0], "[#notfound] No such nick/channel");

	/* test privmsg buffer not found */
	CHECK_RECV("401 me notfound", 0, 1, 0);
	assert_strcmp(mock_chan[0], "host");
	assert_strcmp(mock_line[0], "[notfound] No such nick/channel");

	/* test channel buffer found */
	CHECK_RECV("401 me #c1", 0, 1, 0);
	assert_strcmp(mock_chan[0], "#c1");
	assert_strcmp(mock_line[0], "[#c1] No such nick/channel");

	/* test privmsg buffer found */
	CHECK_RECV("401 me p1", 0, 1, 0);
	assert_strcmp(mock_chan[0], "p1");
	assert_strcmp(mock_line[0], "[p1] No such nick/channel");

	/* test with message */
	CHECK_RECV("401 me p1 :401 message", 0, 1, 0);
	assert_strcmp(mock_chan[0], "p1");
	assert_strcmp(mock_line[0], "[p1] 401 message");
}

static void
test_irc_numeric_403(void)
{
	/* <chan> :No such channel */

	server_reset(s);

	/* test errors */
	CHECK_RECV("403 me", 1, 1, 0);
	assert_strcmp(mock_chan[0], "host");
	assert_strcmp(mock_line[0], "ERR_NOSUCHCHANNEL: chan is null");

	/* test channel buffer not found */
	CHECK_RECV("403 me #notfound", 0, 1, 0);
	assert_strcmp(mock_chan[0], "host");
	assert_strcmp(mock_line[0], "[#notfound] No such channel");

	/* test privmsg buffer not found */
	CHECK_RECV("403 me notfound", 0, 1, 0);
	assert_strcmp(mock_chan[0], "host");
	assert_strcmp(mock_line[0], "[notfound] No such channel");

	/* test channel buffer found */
	CHECK_RECV("403 me #c1", 0, 1, 0);
	assert_strcmp(mock_chan[0], "#c1");
	assert_strcmp(mock_line[0], "[#c1] No such channel");

	/* test privmsg buffer found */
	CHECK_RECV("403 me p1", 0, 1, 0);
	assert_strcmp(mock_chan[0], "p1");
	assert_strcmp(mock_line[0], "[p1] No such channel");

	/* test with message */
	CHECK_RECV("403 me p1 :403 message", 0, 1, 0);
	assert_strcmp(mock_chan[0], "p1");
	assert_strcmp(mock_line[0], "[p1] 403 message");
}

static void
test_recv(void)
{
	server_reset(s);


@@ 308,7 387,7 @@ test_recv_join(void)
	assert_eq(user_list_add(&(c1->users), CASEMAPPING_RFC1459, "nick1", MODE_EMPTY), USER_ERR_NONE);
	assert_eq(user_list_add(&(c2->users), CASEMAPPING_RFC1459, "nick1", MODE_EMPTY), USER_ERR_NONE);

	join_threshold = 0;
	threshold_join = 0;

	CHECK_RECV("JOIN #c1", 1, 1, 0);
	assert_strcmp(mock_chan[0], "host");


@@ 348,7 427,7 @@ test_recv_join(void)
	assert_strcmp(mock_chan[0], "#c1");
	assert_strcmp(mock_line[0], "nick6!user@host has joined [account - real name]");

	join_threshold = 2;
	threshold_join = 2;

	CHECK_RECV(":nick2!user@host JOIN #c2", 0, 0, 0);



@@ 561,7 640,7 @@ test_recv_part(void)
	assert_eq(user_list_add(&(c1->users), CASEMAPPING_RFC1459, "nick4", MODE_EMPTY), USER_ERR_NONE);
	assert_eq(user_list_add(&(c1->users), CASEMAPPING_RFC1459, "nick5", MODE_EMPTY), USER_ERR_NONE);

	part_threshold = 0;
	threshold_part = 0;

	CHECK_RECV("PART #c1 :part message", 1, 1, 0);
	assert_strcmp(mock_chan[0], "host");


@@ 593,7 672,7 @@ test_recv_part(void)
	assert_strcmp(mock_line[0], "nick3!user@host has parted");
	assert_ptr_null(user_list_get(&(c1->users), s->casemapping, "nick3", 0));

	part_threshold = 1;
	threshold_part = 1;

	CHECK_RECV(":nick4!user@host PART #c1", 0, 0, 0);
	assert_ptr_null(user_list_get(&(c1->users), s->casemapping, "nick4", 0));


@@ 671,7 750,7 @@ test_recv_quit(void)
	assert_eq(user_list_add(&(c3->users), CASEMAPPING_RFC1459, "nick1", MODE_EMPTY), USER_ERR_NONE);
	assert_eq(user_list_add(&(c3->users), CASEMAPPING_RFC1459, "nick2", MODE_EMPTY), USER_ERR_NONE);

	quit_threshold = 0;
	threshold_quit = 0;

	CHECK_RECV("QUIT message", 1, 1, 0);
	assert_strcmp(mock_chan[0], "host");


@@ 700,7 779,7 @@ test_recv_quit(void)
	assert_strcmp(mock_line[0], "nick4!user@host has quit");
	assert_ptr_null(user_list_get(&(c1->users), s->casemapping, "nick4", 0));

	quit_threshold = 1;
	threshold_quit = 1;

	/* c1 = {nick1, nick5}, c3 = {nick1} */
	CHECK_RECV(":nick1!user@host QUIT", 0, 1, 0);


@@ 776,7 855,7 @@ test_recv_ircv3_account(void)
	assert_eq(user_list_add(&(c1->users), CASEMAPPING_RFC1459, "nick2", MODE_EMPTY), USER_ERR_NONE);
	assert_eq(user_list_add(&(c3->users), CASEMAPPING_RFC1459, "nick1", MODE_EMPTY), USER_ERR_NONE);

	account_threshold = 0;
	threshold_account = 0;

	CHECK_RECV("ACCOUNT *", 1, 1, 0);
	assert_strcmp(mock_chan[0], "host");


@@ 802,7 881,7 @@ test_recv_ircv3_account(void)
	assert_strcmp(mock_chan[1], "#c3");
	assert_strcmp(mock_line[1], "nick1 has logged out");

	account_threshold = 2;
	threshold_account = 2;

	CHECK_RECV(":nick1!user@host ACCOUNT *", 0, 1, 0);
	assert_strcmp(mock_chan[0], "#c3");


@@ 822,7 901,7 @@ test_recv_ircv3_away(void)
	assert_eq(user_list_add(&(c1->users), CASEMAPPING_RFC1459, "nick2", MODE_EMPTY), USER_ERR_NONE);
	assert_eq(user_list_add(&(c3->users), CASEMAPPING_RFC1459, "nick1", MODE_EMPTY), USER_ERR_NONE);

	away_threshold = 0;
	threshold_away = 0;

	CHECK_RECV("AWAY *", 1, 1, 0);
	assert_strcmp(mock_chan[0], "host");


@@ 845,7 924,7 @@ test_recv_ircv3_away(void)
	assert_strcmp(mock_chan[1], "#c3");
	assert_strcmp(mock_line[1], "nick1 is no longer away");

	away_threshold = 2;
	threshold_away = 2;

	CHECK_RECV(":nick1!user@host AWAY", 0, 1, 0);
	assert_strcmp(mock_chan[0], "#c3");


@@ 865,7 944,7 @@ test_recv_ircv3_chghost(void)
	assert_eq(user_list_add(&(c1->users), CASEMAPPING_RFC1459, "nick2", MODE_EMPTY), USER_ERR_NONE);
	assert_eq(user_list_add(&(c3->users), CASEMAPPING_RFC1459, "nick1", MODE_EMPTY), USER_ERR_NONE);

	chghost_threshold = 0;
	threshold_chghost = 0;

	CHECK_RECV("CHGHOST new_user new_host", 1, 1, 0);
	assert_strcmp(mock_chan[0], "host");


@@ 886,30 965,53 @@ test_recv_ircv3_chghost(void)
	assert_strcmp(mock_chan[1], "#c3");
	assert_strcmp(mock_line[1], "nick1 has changed user/host: new_user/new_host");

	chghost_threshold = 2;
	threshold_chghost = 2;

	CHECK_RECV(":nick1!user@host CHGHOST new_user new_host", 0, 1, 0);
	assert_strcmp(mock_chan[0], "#c3");
	assert_strcmp(mock_line[0], "nick1 has changed user/host: new_user/new_host");
}

int
main(void)
static int
test_init(void)
{
	s = server("host", "port", NULL, "user", "real");

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

	if (!s || !c1 || !c2 || !c3)
		test_abort_main("Failed test setup");
	p1 = channel("p1", CHANNEL_T_PRIVMSG);
	p2 = channel("p2", CHANNEL_T_PRIVMSG);
	p3 = channel("p3", CHANNEL_T_PRIVMSG);

	if (!s || !c1 || !c2 || !c3 || !p1 || !p2 || !p3)
		return -1;

	channel_list_add(&s->clist, c1);
	channel_list_add(&s->clist, c2);
	channel_list_add(&s->clist, c3);
	channel_list_add(&s->clist, p1);
	channel_list_add(&s->clist, p2);
	channel_list_add(&s->clist, p3);

	server_nick_set(s, "me");

	return 0;
}


static int
test_term(void)
{
	server_free(s);

	return 0;
}

int
main(void)
{
	struct testcase tests[] = {
		TESTCASE(test_irc_generic),
		TESTCASE(test_irc_generic_error),


@@ 917,6 1019,8 @@ main(void)
		TESTCASE(test_irc_generic_info),
		TESTCASE(test_irc_generic_unknown),
		TESTCASE(test_irc_numeric_353),
		TESTCASE(test_irc_numeric_401),
		TESTCASE(test_irc_numeric_403),
		TESTCASE(test_recv),
		TESTCASE(test_recv_error),
		TESTCASE(test_recv_invite),


@@ 940,9 1044,5 @@ main(void)
		TESTCASE(test_recv_ircv3_chghost)
	};

	int ret = run_tests(tests);

	server_free(s);

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

M test/handlers/irc_send.c => test/handlers/irc_send.c +108 -41
@@ 17,7 17,7 @@
	do { \
		mock_reset_io(); \
		mock_reset_state(); \
		assert_eq(irc_send_privmsg(s, (C), (M)), (RET)); \
		assert_eq(irc_send_message(s, (C), (M)), (RET)); \
		assert_eq(mock_line_n, (LINE_N)); \
		assert_eq(mock_send_n, (SEND_N)); \
		assert_strcmp(mock_line[0], (LINE)); \


@@ 69,7 69,7 @@ test_irc_send_command(void)
	CHECK_SEND_COMMAND(c_chan, m2, 1, 1, 0, "Messages beginning with '/' require a command", "");
	CHECK_SEND_COMMAND(c_chan, m3, 0, 0, 1, "", "TEST");
	CHECK_SEND_COMMAND(c_chan, m4, 0, 0, 1, "", "TEST arg1 arg2 arg3");
	CHECK_SEND_COMMAND(c_chan, m5, 0, 0, 1, "", "PRIVMSG targ :test message");
	CHECK_SEND_COMMAND(c_chan, m5, 0, 1, 1, "test message", "PRIVMSG targ :test message");

	s->registered = 0;



@@ 79,7 79,7 @@ test_irc_send_command(void)
}

static void
test_irc_send_privmsg(void)
test_irc_send_message(void)
{
	char m1[] = "chan test 1";
	char m2[] = "serv test 2";


@@ 98,7 98,7 @@ test_irc_send_privmsg(void)

	mock_reset_io();
	mock_reset_state();
	assert_eq(irc_send_privmsg(NULL, c_chan, "test"), 1);
	assert_eq(irc_send_message(NULL, c_chan, "test"), 1);
	assert_strcmp(mock_line[0], "This is not a server");
	assert_strcmp(mock_send[0], "");



@@ 155,16 155,73 @@ static void
test_send_privmsg(void)
{
	char m1[] = "privmsg";
	char m2[] = "privmsg test1";
	char m3[] = "privmsg test2 ";
	char m4[] = "privmsg test3  ";
	char m5[] = "privmsg test4 test privmsg message";
	char m2[] = "privmsg chan";
	char m3[] = "privmsg chan ";

	struct channel *c1;
	struct channel *c2;
	struct channel *c3;
	struct channel *c4;

	assert_eq(s->clist.count, 3);

	CHECK_SEND_COMMAND(c_chan, m1, 1, 1, 0, "Usage: /privmsg <target> <message>", "");
	CHECK_SEND_COMMAND(c_chan, m2, 1, 1, 0, "Usage: /privmsg <target> <message>", "");
	CHECK_SEND_COMMAND(c_chan, m3, 1, 1, 0, "Usage: /privmsg <target> <message>", "");
	CHECK_SEND_COMMAND(c_chan, m4, 0, 0, 1, "", "PRIVMSG test3 : ");
	CHECK_SEND_COMMAND(c_chan, m5, 0, 0, 1, "", "PRIVMSG test4 :test privmsg message");

	/* test sending to existing channel */
	char m4[] = "privmsg chan  ";
	char m5[] = "privmsg chan test 1";

	CHECK_SEND_COMMAND(c_chan, m4, 0, 1, 1, " ",      "PRIVMSG chan : ");
	CHECK_SEND_COMMAND(c_chan, m5, 0, 1, 1, "test 1", "PRIVMSG chan :test 1");

	assert_eq(s->clist.count, 3);

	/* test sending to single new target */
	char m6[] = "privmsg #new1 test 2";

	CHECK_SEND_COMMAND(c_chan, m6, 0, 1, 1, "test 2", "PRIVMSG #new1 :test 2");

	if (!(c1 = channel_list_get(&(s->clist), "#new1", s->casemapping)))
		test_abort("channel '#new1' not found");

	assert_eq(c1->type, CHANNEL_T_CHANNEL);
	assert_eq(s->clist.count, 4);

	/* test sending to multiple new targets */
	char m7[] = "privmsg #new2,priv1,#new3,priv2 test 3";

	CHECK_SEND_COMMAND(c_chan, m7, 0, 4, 1, "test 3", "PRIVMSG #new2,priv1,#new3,priv2 :test 3");

	if (!(c1 = channel_list_get(&(s->clist), "#new2", s->casemapping)))
		test_abort("channel '#new2' not found");

	if (!(c2 = channel_list_get(&(s->clist), "priv1", s->casemapping)))
		test_abort("channel 'priv1' not found");

	if (!(c3 = channel_list_get(&(s->clist), "#new3", s->casemapping)))
		test_abort("channel '#new3' not found");

	if (!(c4 = channel_list_get(&(s->clist), "priv2", s->casemapping)))
		test_abort("channel 'priv2' not found");

	assert_eq(c1->type, CHANNEL_T_CHANNEL);
	assert_eq(c2->type, CHANNEL_T_PRIVMSG);
	assert_eq(c3->type, CHANNEL_T_CHANNEL);
	assert_eq(c4->type, CHANNEL_T_PRIVMSG);
	assert_eq(s->clist.count, 8);

	/* test with some duplicates channels */
	char m8[] = "privmsg priv3,priv1,priv2 test 4";

	CHECK_SEND_COMMAND(c_chan, m8, 0, 3, 1, "test 4", "PRIVMSG priv3,priv1,priv2 :test 4");

	if (!(c1 = channel_list_get(&(s->clist), "priv3", s->casemapping)))
		test_abort("channel 'priv3' not found");

	assert_eq(c1->type, CHANNEL_T_PRIVMSG);
	assert_eq(s->clist.count, 9);
}

static void


@@ 219,9 276,9 @@ test_send_ctcp_action(void)
	char m3[] = "ctcp-action target ";
	char m4[] = "ctcp-action target action message";

	CHECK_SEND_COMMAND(c_chan, m1, 1, 1, 0, "Usage: /ctcp-action <nick> <text>", "");
	CHECK_SEND_COMMAND(c_chan, m2, 1, 1, 0, "Usage: /ctcp-action <nick> <text>", "");
	CHECK_SEND_COMMAND(c_chan, m3, 1, 1, 0, "Usage: /ctcp-action <nick> <text>", "");
	CHECK_SEND_COMMAND(c_chan, m1, 1, 1, 0, "Usage: /ctcp-action <target> <text>", "");
	CHECK_SEND_COMMAND(c_chan, m2, 1, 1, 0, "Usage: /ctcp-action <target> <text>", "");
	CHECK_SEND_COMMAND(c_chan, m3, 1, 1, 0, "Usage: /ctcp-action <target> <text>", "");
	CHECK_SEND_COMMAND(c_chan, m4, 0, 0, 1, "", "PRIVMSG target :\001ACTION action message\001");
}



@@ 233,8 290,8 @@ test_send_ctcp_clientinfo(void)
	char m3[] = "ctcp-clientinfo";
	char m4[] = "ctcp-clientinfo targ";

	CHECK_SEND_COMMAND(c_chan, m1, 1, 1, 0, "Usage: /ctcp-clientinfo <nick>", "");
	CHECK_SEND_COMMAND(c_serv, m2, 1, 1, 0, "Usage: /ctcp-clientinfo <nick>", "");
	CHECK_SEND_COMMAND(c_chan, m1, 1, 1, 0, "Usage: /ctcp-clientinfo <target>", "");
	CHECK_SEND_COMMAND(c_serv, m2, 1, 1, 0, "Usage: /ctcp-clientinfo <target>", "");
	CHECK_SEND_COMMAND(c_priv, m3, 0, 0, 1, "", "PRIVMSG priv :\001CLIENTINFO\001");
	CHECK_SEND_COMMAND(c_priv, m4, 0, 0, 1, "", "PRIVMSG targ :\001CLIENTINFO\001");
}


@@ 247,8 304,8 @@ test_send_ctcp_finger(void)
	char m3[] = "ctcp-finger";
	char m4[] = "ctcp-finger targ";

	CHECK_SEND_COMMAND(c_chan, m1, 1, 1, 0, "Usage: /ctcp-finger <nick>", "");
	CHECK_SEND_COMMAND(c_serv, m2, 1, 1, 0, "Usage: /ctcp-finger <nick>", "");
	CHECK_SEND_COMMAND(c_chan, m1, 1, 1, 0, "Usage: /ctcp-finger <target>", "");
	CHECK_SEND_COMMAND(c_serv, m2, 1, 1, 0, "Usage: /ctcp-finger <target>", "");
	CHECK_SEND_COMMAND(c_priv, m3, 0, 0, 1, "", "PRIVMSG priv :\001FINGER\001");
	CHECK_SEND_COMMAND(c_priv, m4, 0, 0, 1, "", "PRIVMSG targ :\001FINGER\001");
}


@@ 267,8 324,8 @@ test_send_ctcp_ping(void)
	const char *arg2;
	const char *arg3;

	CHECK_SEND_COMMAND(c_chan, m1, 1, 1, 0, "Usage: /ctcp-ping <nick>", "");
	CHECK_SEND_COMMAND(c_serv, m2, 1, 1, 0, "Usage: /ctcp-ping <nick>", "");
	CHECK_SEND_COMMAND(c_chan, m1, 1, 1, 0, "Usage: /ctcp-ping <target>", "");
	CHECK_SEND_COMMAND(c_serv, m2, 1, 1, 0, "Usage: /ctcp-ping <target>", "");

	/* test send to channel */
	errno = 0;


@@ 337,8 394,8 @@ test_send_ctcp_source(void)
	char m3[] = "ctcp-source";
	char m4[] = "ctcp-source targ";

	CHECK_SEND_COMMAND(c_chan, m1, 1, 1, 0, "Usage: /ctcp-source <nick>", "");
	CHECK_SEND_COMMAND(c_serv, m2, 1, 1, 0, "Usage: /ctcp-source <nick>", "");
	CHECK_SEND_COMMAND(c_chan, m1, 1, 1, 0, "Usage: /ctcp-source <target>", "");
	CHECK_SEND_COMMAND(c_serv, m2, 1, 1, 0, "Usage: /ctcp-source <target>", "");
	CHECK_SEND_COMMAND(c_priv, m3, 0, 0, 1, "", "PRIVMSG priv :\001SOURCE\001");
	CHECK_SEND_COMMAND(c_priv, m4, 0, 0, 1, "", "PRIVMSG targ :\001SOURCE\001");
}


@@ 351,8 408,8 @@ test_send_ctcp_time(void)
	char m3[] = "ctcp-time";
	char m4[] = "ctcp-time targ";

	CHECK_SEND_COMMAND(c_chan, m1, 1, 1, 0, "Usage: /ctcp-time <nick>", "");
	CHECK_SEND_COMMAND(c_serv, m2, 1, 1, 0, "Usage: /ctcp-time <nick>", "");
	CHECK_SEND_COMMAND(c_chan, m1, 1, 1, 0, "Usage: /ctcp-time <target>", "");
	CHECK_SEND_COMMAND(c_serv, m2, 1, 1, 0, "Usage: /ctcp-time <target>", "");
	CHECK_SEND_COMMAND(c_priv, m3, 0, 0, 1, "", "PRIVMSG priv :\001TIME\001");
	CHECK_SEND_COMMAND(c_priv, m4, 0, 0, 1, "", "PRIVMSG targ :\001TIME\001");
}


@@ 365,8 422,8 @@ test_send_ctcp_userinfo(void)
	char m3[] = "ctcp-userinfo";
	char m4[] = "ctcp-userinfo targ";

	CHECK_SEND_COMMAND(c_chan, m1, 1, 1, 0, "Usage: /ctcp-userinfo <nick>", "");
	CHECK_SEND_COMMAND(c_serv, m2, 1, 1, 0, "Usage: /ctcp-userinfo <nick>", "");
	CHECK_SEND_COMMAND(c_chan, m1, 1, 1, 0, "Usage: /ctcp-userinfo <target>", "");
	CHECK_SEND_COMMAND(c_serv, m2, 1, 1, 0, "Usage: /ctcp-userinfo <target>", "");
	CHECK_SEND_COMMAND(c_priv, m3, 0, 0, 1, "", "PRIVMSG priv :\001USERINFO\001");
	CHECK_SEND_COMMAND(c_priv, m4, 0, 0, 1, "", "PRIVMSG targ :\001USERINFO\001");
}


@@ 379,8 436,8 @@ test_send_ctcp_version(void)
	char m3[] = "ctcp-version";
	char m4[] = "ctcp-version targ";

	CHECK_SEND_COMMAND(c_chan, m1, 1, 1, 0, "Usage: /ctcp-version <nick>", "");
	CHECK_SEND_COMMAND(c_serv, m2, 1, 1, 0, "Usage: /ctcp-version <nick>", "");
	CHECK_SEND_COMMAND(c_chan, m1, 1, 1, 0, "Usage: /ctcp-version <target>", "");
	CHECK_SEND_COMMAND(c_serv, m2, 1, 1, 0, "Usage: /ctcp-version <target>", "");
	CHECK_SEND_COMMAND(c_priv, m3, 0, 0, 1, "", "PRIVMSG priv :\001VERSION\001");
	CHECK_SEND_COMMAND(c_priv, m4, 0, 0, 1, "", "PRIVMSG targ :\001VERSION\001");
}


@@ 405,27 462,41 @@ test_send_ircv3_cap_list(void)
	CHECK_SEND_COMMAND(c_chan, m2, 1, 1, 0, "Usage: /cap-list", "");
}

int
main(void)
static int
test_init(void)
{
	c_chan = channel("chan", CHANNEL_T_CHANNEL);
	c_priv = channel("priv", CHANNEL_T_PRIVATE);

	s = server("h1", "p1", NULL, "u1", "r1");

	c_serv = s->channel;

	c_chan = channel("chan", CHANNEL_T_CHANNEL);
	c_priv = channel("priv", CHANNEL_T_PRIVMSG);

	if (!s || !c_chan || !c_priv)
		test_abort_main("Failed test setup");
		return -1;

	channel_list_add(&s->clist, c_chan);
	channel_list_add(&s->clist, c_priv);

	c_serv = s->channel;

	s->registered = 1;

	return 0;
}

static int
test_term(void)
{
	server_free(s);

	return 0;
}

int
main(void)
{
	struct testcase tests[] = {
		TESTCASE(test_irc_send_command),
		TESTCASE(test_irc_send_privmsg),
		TESTCASE(test_irc_send_message),
#define X(cmd) TESTCASE(test_send_##cmd),
		SEND_HANDLERS
#undef X


@@ 437,9 508,5 @@ main(void)
#undef X
	};

	int ret = run_tests(tests);

	server_free(s);

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

M test/handlers/irc_send.mock.c => test/handlers/irc_send.mock.c +1 -1
@@ 8,7 8,7 @@ irc_send_command(struct server *s, struct channel *c, char *m)
}

int
irc_send_privmsg(struct server *s, struct channel *c, char *m)
irc_send_message(struct server *s, struct channel *c, const char *m)
{
	UNUSED(s);
	UNUSED(c);

M test/handlers/ircv3.c => test/handlers/ircv3.c +18 -10
@@ 595,14 595,26 @@ test_ircv3_cap_req_send(void)
	assert_strcmp(mock_send[0], "CAP REQ :cap-1 cap-3 cap-5");
}

int
main(void)
static int
test_init(void)
{
	s = server("host", "post", NULL, "user", "real");
	if (!(s = server("host", "post", NULL, "user", "real")))
		return -1;

	return 0;
}

static int
test_term(void)
{
	server_free(s);

	if (!s)
		test_abort_main("Failed test setup");
	return 0;
}

int
main(void)
{
	struct testcase tests[] = {
		TESTCASE(test_ircv3_CAP),
		TESTCASE(test_ircv3_CAP_LS),


@@ 615,9 627,5 @@ main(void)
		TESTCASE(test_ircv3_cap_req_send),
	};

	int ret = run_tests(tests);

	server_free(s);

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

M test/rirc.c => test/rirc.c +1 -1
@@ 33,5 33,5 @@ main(void)
		TESTCASE(test_rirc_parse_args)
	};

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

M test/state.c => test/state.c +58 -17
@@ 69,7 69,11 @@ test_command_close(void)
{
	static struct channel *c1;
	static struct channel *c2;
	struct server *s;
	static struct channel *c3;
	static struct channel *c4;
	static struct channel *c5;
	struct server *s1;
	struct server *s2;

	state_init();



@@ 83,19 87,34 @@ test_command_close(void)
	/* clear error */
	INP_C(0x0A);

	/* host1 #c1 #c2, host2 #c3 #c4 */
	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);
	c3 = channel("#c3", CHANNEL_T_CHANNEL);
	c4 = channel("#c4", CHANNEL_T_CHANNEL);
	c5 = channel("#c5", CHANNEL_T_CHANNEL);
	s1 = server("host1", "port1", NULL, "user1", "real1");
	s2 = server("host2", "port2", NULL, "user2", "real2");

	if (!s1 || !s2 || !c1 || !c2 || !c3 || !c4 || !c5)
		test_abort("Failed to create servers and channels");

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

	c3->server = s2;
	c4->server = s2;
	c5->server = s2;
	channel_list_add(&(s2->clist), c3);
	channel_list_add(&(s2->clist), c4);
	channel_list_add(&(s2->clist), c5);

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

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

	channel_set_current(c1);


@@ 108,23 127,45 @@ test_command_close(void)
	/* clear error */
	INP_C(0x0A);

	/* test closing last channel on server */
	channel_set_current(c2);

	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");
	assert_strcmp(current_channel()->name, "host2");

	channel_set_current(s->channel);
	/* test closing server channel */
	channel_set_current(s1->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");
	assert_strcmp(current_channel()->name, "host2");

	/* test closing middle channel*/
	channel_set_current(c3);

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

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

	/* test closing last channel*/
	channel_set_current(c5);

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

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

	state_term();
}


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

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

M test/test.h => test/test.h +79 -84
@@ 1,5 1,5 @@
#ifndef TEST_H
#define TEST_H
#ifndef RIRC_TEST_H
#define RIRC_TEST_H

/* test.h -- unit test framework for rirc
 *


@@ 30,7 30,6 @@
 *    - test_failf(M, ...)
 *    - test_abort(M)
 *    - test_abortf(M, ...)
 *    - test_abort_main(M)
 */

#include <inttypes.h>


@@ 39,9 38,10 @@
#include <stdlib.h>
#include <string.h>

#define TESTCASE(X) { &(X), #X }
#define TESTING

#define TESTCASE(X) { &(X), #X }

#define TOKEN_PASTE(x, y) x##y
#define TOKEN(x, y) TOKEN_PASTE(x, y)



@@ 59,32 59,32 @@

#define assert_eq(X, Y) \
	do { \
		int __ret_x = (int)(X); \
		int __ret_y = (int)(Y); \
		int __ret_x = (X); \
		int __ret_y = (Y); \
		if (__ret_x != __ret_y) \
			test_failf(#X " expected '%d', got '%d'", __ret_y, __ret_x); \
	} while (0)

#define assert_gt(X, Y) \
	do { \
		int __ret_x = (int)(X); \
		int __ret_y = (int)(Y); \
		int __ret_x = (X); \
		int __ret_y = (Y); \
		if (!(__ret_x > __ret_y)) \
			test_failf(#X " expected '%d' to be greater than '%d'", __ret_y, __ret_x); \
	} while (0)

#define assert_lt(X, Y) \
	do { \
		int __ret_x = (int)(X); \
		int __ret_y = (int)(Y); \
		int __ret_x = (X); \
		int __ret_y = (Y); \
		if (!(__ret_x < __ret_y)) \
			test_failf(#X " expected '%d' to be less than '%d'", __ret_y, __ret_x); \
	} while (0)

#define assert_ueq(X, Y) \
	do { \
		unsigned __ret_x = (unsigned)(X); \
		unsigned __ret_y = (unsigned)(Y); \
		unsigned __ret_x = (X); \
		unsigned __ret_y = (Y); \
		if (__ret_x != __ret_y) \
			test_failf(#X " expected '%u', got '%u'", __ret_y, __ret_x); \
	} while (0)


@@ 93,7 93,7 @@
	do { \
		const char *__ret_x = (X); \
		const char *__ret_y = (Y); \
		if (_assert_strcmp(__ret_x, __ret_y)) \
		if (_assert_strcmp_(__ret_x, __ret_y)) \
			test_failf(#X " expected '%s', got '%s'", \
				__ret_y == NULL ? "NULL" : __ret_y, \
				__ret_x == NULL ? "NULL" : __ret_x); \


@@ 103,7 103,7 @@
	do { \
		const char *__ret_x = (X); \
		const char *__ret_y = (Y); \
		if (_assert_strncmp(__ret_x, __ret_y, (N))) \
		if (_assert_strncmp_(__ret_x, __ret_y, (N))) \
			test_failf(#X " expected '%.*s', got '%.*s'", \
				(int)(N), __ret_y == NULL ? "NULL" : __ret_y, \
				(int)(N), __ret_x == NULL ? "NULL" : __ret_x); \


@@ 134,11 134,11 @@
#define assert_fatal(X) \
	do { \
		if (setjmp(_tc_fatal_expected_)) { \
			_assert_fatal_ = 1; \
			_tc_assert_fatal_ = 1; \
			(X); \
			if (_assert_fatal_) \
			if (_tc_assert_fatal_) \
				test_fail("'"#X "' should have exited fatally"); \
			_assert_fatal_ = 0; \
			_tc_assert_fatal_ = 0; \
		} \
	} while (0)



@@ 153,11 153,11 @@
#else
#define _fatal(...) \
	do { \
		if (_assert_fatal_) { \
		if (_tc_assert_fatal_) { \
			longjmp(_tc_fatal_expected_, 1); \
		} else { \
			snprintf(_tc_errbuf_1, sizeof(_tc_errbuf_1), "%s:%d:%s:", __FILE__, __LINE__, __func__); \
			snprintf(_tc_errbuf_2, sizeof(_tc_errbuf_2), __VA_ARGS__); \
			snprintf(_tc_errbuf_1_, sizeof(_tc_errbuf_1_), "%s:%u:%s:", __FILE__, __LINE__, __func__); \
			snprintf(_tc_errbuf_2_, sizeof(_tc_errbuf_2_), __VA_ARGS__); \
			longjmp(_tc_fatal_unexpected_, 1); \
		} \
	} while (0)


@@ 165,51 165,44 @@
#define fatal_noexit _fatal
#endif

#define run_tests(X) \
	_run_tests_(__FILE__, X, sizeof(X) / sizeof(X[0]))

#define test_fail(M) \
	do { \
		_print_testcase_name_(__func__); \
		printf("    %d: " M "\n", __LINE__); \
		_failures_++; \
		printf("    %u: " M "\n", __LINE__); \
		_tc_failures_++; \
	} while (0)

#define test_failf(M, ...) \
	do { \
		_print_testcase_name_(__func__); \
		printf("    %d: ", __LINE__); \
		printf("    %u: ", __LINE__); \
		printf((M), __VA_ARGS__); \
		printf("\n"); \
		_failures_++; \
		_tc_failures_++; \
	} while (0)

#define test_abort(M) \
	do { \
		_print_testcase_name_(__func__); \
		printf("    %d: " M "\n", __LINE__); \
		printf("    ---Testcase aborted---\n"); \
		_failures_++; \
		printf("    %u: " M "\n", __LINE__); \
		printf("    -- Testcase aborted --\n"); \
		_tc_failures_++; \
		return; \
	} while (0)

#define test_abortf(M, ...) \
	do { \
		_print_testcase_name_(__func__); \
		printf("    %d: ", __LINE__); \
		printf("    %u: ", __LINE__); \
		printf((M), __VA_ARGS__); \
		printf("\n"); \
		printf("    ---Testcase aborted---\n"); \
		_failures_++; \
		printf("    -- Testcase aborted --\n"); \
		_tc_failures_++; \
		return; \
	} while (0)

#define test_abort_main(M) \
	do { \
		printf("    %d: " M "\n", __LINE__); \
		printf("    ---Testcase aborted---\n"); \
		return EXIT_FAILURE; \
	} while (0)
#define run_tests(INIT, TERM, TESTS) \
	_run_tests_(__FILE__, INIT, TERM, TESTS, (sizeof(TESTS) / sizeof(TESTS[0])))

struct testcase
{


@@ 217,21 210,16 @@ struct testcase
	const char *tc_str;
};

static int _assert_strcmp(const char*, const char*);
static int _assert_strncmp(const char*, const char*, size_t);
static void _print_testcase_name_(const char*);

static char _tc_errbuf_1[512];
static char _tc_errbuf_2[512];
static char _tc_errbuf_1_[512];
static char _tc_errbuf_2_[512];
static jmp_buf _tc_fatal_expected_;
static jmp_buf _tc_fatal_unexpected_;
static unsigned _assert_fatal_;
static unsigned _failure_printed_;
static unsigned _failures_;
static unsigned _failures_t_;
static unsigned _tc_assert_fatal_;
static unsigned _tc_failures_;
static unsigned _tc_failures_t_;

static int
_assert_strcmp(const char *p1, const char *p2)
_assert_strcmp_(const char *p1, const char *p2)
{
	if (!p1 || !p2)
		return p1 != p2;


@@ 240,7 228,7 @@ _assert_strcmp(const char *p1, const char *p2)
}

static int
_assert_strncmp(const char *p1, const char *p2, size_t n)
_assert_strncmp_(const char *p1, const char *p2, size_t n)
{
	if (!p1 || !p2)
		return p1 != p2;


@@ 251,61 239,68 @@ _assert_strncmp(const char *p1, const char *p2, size_t n)
static void
_print_testcase_name_(const char *name)
{
	/* Print the testcase name once */
	if (!_failure_printed_) {
		_failure_printed_ = 1;

		if (!_failures_t_)
			puts("");

	if (!_tc_failures_)
		printf("  %s:\n", name);
	}
}

static int
_run_tests_(const char *filename, struct testcase testcases[], size_t len)
_run_tests_(
	const char *filename,
	int (*tc_init)(void),
	int (*tc_term)(void),
	const struct testcase testcases[],
	size_t len)
{
	/* Silence compiler warnings for unused test functions/vars */
	((void)(_assert_strcmp));
	((void)(_assert_strncmp));
	((void)(_assert_fatal_));
	((void)(_failure_printed_));
	((void)(_assert_strcmp_));
	((void)(_assert_strncmp_));
	((void)(_tc_assert_fatal_));
	((void)(_tc_fatal_expected_));
	((void)(_tc_fatal_unexpected_));

	printf("%s... ", filename);
	fflush(stdout);
	printf("%s...\n", filename);

	struct testcase *tc;
	for (const struct testcase *tc = testcases; len--; tc++) {

	for (tc = testcases; len--; tc++) {

		_failures_ = 0;
		_failure_printed_ = 0;
		_tc_failures_ = 0;

		if (setjmp(_tc_fatal_unexpected_)) {
			_print_testcase_name_(tc->tc_str);
			printf("    Unexpected fatal error:\n");
			printf("    %s %s\n", _tc_errbuf_1, _tc_errbuf_2);
			printf("    -- aborting testcase --\n");
			_failures_++;
		} else {
			(*tc->tc_ptr)();
			printf("    %s %s\n", _tc_errbuf_1_, _tc_errbuf_2_);
			printf("    -- Testcase aborted --\n"); \
			_tc_failures_++;
			continue;
		}

		if (_failures_) {
			printf("      %d failure%s\n", _failures_, (_failures_ > 1) ? "s" : "");
			_failures_t_ += _failures_;
		if (tc_init && (*tc_init)()) {
			_print_testcase_name_(tc->tc_str);
			printf("    Testcase setup failed\n");
			printf("    -- Tests aborted --\n");
			return EXIT_FAILURE;
		}

		(*tc->tc_ptr)();

		if (tc_term && (*tc_term)()) {
			_print_testcase_name_(tc->tc_str);
			printf("    Testcase cleanup failed\n");
			printf("    -- Tests aborted --\n");
			return EXIT_FAILURE;
		}

		if (_tc_failures_)
			printf("      %u failure%s\n", _tc_failures_, (_tc_failures_ ? "s" : ""));

		_tc_failures_t_ += _tc_failures_;
	}

	if (_failures_t_) {
		printf("  %d failure%s total\n", _failures_t_, (_failures_t_ > 1) ? "s" : "");
	if (_tc_failures_t_) {
		printf("  %u failure%s total\n", _tc_failures_t_, (_tc_failures_t_ ? "s" : ""));
		return EXIT_FAILURE;
	} else {
		printf(" OK\n");
		return EXIT_SUCCESS;
	}

	return EXIT_SUCCESS;
}

#endif

M test/utils/tree.c => test/utils/tree.c +1 -1
@@ 683,5 683,5 @@ main(void)
		TESTCASE(test_avl_foreach)
	};

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

M test/utils/utils.c => test/utils/utils.c +1 -1
@@ 672,5 672,5 @@ main(void)
		TESTCASE(test_irc_toupper)
	};

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