~rcr/rirc

c2dc4ea1295d3d78584452cbdf801def230536a4 — Richard Robbins a month ago f2dfce9 + cec95ad master v0.1.6
Merge branch '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_sonarscan.sh sonarscan
      ./scripts/sa_sonarscan.sh sonarscan
      ./scripts/sa_sonarscan_get.sh sonarscan
      ./scripts/sa_sonarscan_run.sh sonarscan

triggers:
  - action: email

M .gitignore => .gitignore +0 -6
@@ 1,11 1,5 @@
*.gcda
*.gcno
*.gcov
*.gperf.out
*.o
*.out
*.t
*.td
.cache
build
config.h

M Makefile => Makefile +20 -19
@@ 1,4 1,4 @@
VERSION := 0.1.5
VERSION := 0.1.6

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


@@ 13,17 13,19 @@ include lib/mbedtls.Makefile

CONFIG := config.h

CFLAGS_RIRC += -std=c11 -I. -DVERSION=\"$(VERSION)\"
CFLAGS_RIRC += -D_POSIX_C_SOURCE=200809L
CFLAGS_RIRC += -D_DARWIN_C_SOURCE

CFLAGS ?= -O2 -flto
CFLAGS += -DNDEBUG

CFLAGS_DEBUG += -O0 -g3 -Wall -Wextra -Werror

RIRC_CFLAGS += -std=c11 -I. -DVERSION=\"$(VERSION)\"
RIRC_CFLAGS += -D_POSIX_C_SOURCE=200809L
RIRC_CFLAGS += -D_DARWIN_C_SOURCE

LDFLAGS ?= -flto
LDFLAGS += -lpthread
LDFLAGS += -pthread

LDFLAGS_DEBUG += -pthread

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


@@ 39,25 41,24 @@ rirc: $(RIRC_LIBS) $(SRC_GPERF) $(OBJS_R)
	@$(CC) $(LDFLAGS) -o $@ $(OBJS_R) $(RIRC_LIBS)

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

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

$(PATH_BUILD)/%.db.o: $(PATH_SRC)/%.c $(CONFIG) | $(PATH_BUILD)
	@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)
	@$(CPP) $(CFLAGS_DEBUG) $(CFLAGS_RIRC) -MM -MP -MT $@ -MF $(@:.o=.o.d) $<
	@$(CC)  $(CFLAGS_DEBUG) $(CFLAGS_RIRC) -c -o $@ $<

$(PATH_BUILD)/%.t: $(PATH_TEST)/%.c $(SRC_GPERF) $(CONFIG) | $(RIRC_LIBS) $(PATH_BUILD)
	@$(CPP) $(CFLAGS_DEBUG) $(CFLAGS_RIRC) -MM -MP -MT $@ -MF $(@:.t=.t.d) $<
	@$(CC)  $(CFLAGS_DEBUG) $(CFLAGS_RIRC) -c -o $(@:.t=.t.o) $<
	@$(CC)  $(LDFLAGS_DEBUG) -o $@ $(@:.t=.t.o) $(RIRC_LIBS)
	@{ rm -f $(@:.t=.td) && ./$@; } || mv $@ $(@:.t=.td)

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

A README => README +48 -0
@@ 0,0 1,48 @@
      _
 _ __(_)_ __ ___
| '__| | '__/ __|
| |  | | | | (__
|_|  |_|_|  \___|
=================

A minimalistic IRC client written in C.


Configure
---------

Configure compile time defaults by editing `config.h`.
To generate a default config file:

    $ make config.h


Install
-------

    $ make
    $ make install

Building rirc requires a c11 compiler, GNU gperf and GNU make.

    - https://www.gnu.org/software/gperf/
    - https://www.gnu.org/software/make/

The build toolchain and install paths can be configured via
standard environment variables, i.e.:

    $CC, $CFLAGS, $LDFLAGS, $DESTDIR, $PREFIX

The default install paths are:

    /usr/local/bin/rirc
    /usr/local/share/man/man1/rirc.1


Documentation
-------------

See `rirc --help` or `man rirc` for a summary of usage. For
complete documentation, refer to:

    https://rcr.io/rirc/

D README.md => README.md +0 -109
@@ 1,109 0,0 @@
<p align="center">
  <img src="https://raw.githubusercontent.com/rcr/rirc/master/docs/birb.jpg" alt="birb"/>
</p>

---

<p align="center">
  <a href="https://sonarcloud.io/dashboard?id=rirc">
    <img alt="sonarcloud" src="https://sonarcloud.io/api/project_badges/measure?project=rirc&metric=ncloc"/>
  </a>
  <a href="https://scan.coverity.com/projects/4940">
    <img alt="coverity" src="https://scan.coverity.com/projects/4940/badge.svg"/>
  </a>
  <a href="https://sonarcloud.io/dashboard?id=rirc">
    <img alt="sonarcloud" src="https://sonarcloud.io/api/project_badges/measure?project=rirc&metric=sqale_rating"/>
  </a>
  <a href="https://sonarcloud.io/dashboard?id=rirc">
    <img alt="sonarcloud" src="https://sonarcloud.io/api/project_badges/measure?project=rirc&metric=reliability_rating"/>
  </a>
  <a href="https://sonarcloud.io/dashboard?id=rirc">
    <img alt="sonarcloud" src="https://sonarcloud.io/api/project_badges/measure?project=rirc&metric=security_rating"/>
  </a>
</p>

---

# rirc

A minimalistic irc client written in C.

Connections are TLS enabled over port 6697 by default.

## Building, installing:

Building rirc from source requires a c11 compiler, GNU gperf and GNU make.

 - https://www.gnu.org/software/gperf/
 - https://www.gnu.org/software/make/

```
make
make install
```

The build toolchain and install paths can be configured via standard
environment variables, i.e.:

```
CC, CFLAGS, LDFLAGS, DESTDIR, PREFIX
```

## Configuring:

Configure rirc by editing `config.h`. Defaults are in `config.def.h`

## Usage:

```
rirc [-hv] [-s server [...]]

Info:
  -h, --help      Print help message and exit
  -v, --version   Print rirc version and exit

Server options:
  -s, --server=SERVER       Connect to SERVER
  -p, --port=PORT           Connect to SERVER using PORT
  -w, --pass=PASS           Connect to SERVER using PASS
  -u, --username=USERNAME   Connect to SERVER using USERNAME
  -r, --realname=REALNAME   Connect to SERVER using REALNAME
  -n, --nicks=NICKS         Comma separated list of nicks to use for SERVER
  -c, --chans=CHANNELS      Comma separated list of channels to join for SERVER

Server connection options:
   --ipv4                   Connect to server using only ipv4 addresses
   --ipv6                   Connect to server using only ipv6 addresses
   --tls-disable            Set server TLS disabled
   --tls-verify=<mode>      Set server TLS peer certificate verification mode
```

Commands:

```
  :clear
  :close
  :connect
  :disconnect
  :quit
```

Keys:

```
  ^N : go to next channel
  ^P : go to previous channel
  ^L : clear channel
  ^X : close channel
  ^C : cancel input/action
  ^U : scroll buffer up
  ^D : scroll buffer down
   ← : input cursor back
   → : input cursor forward
   ↑ : input history back
   ↓ : input history forward
```

## More info:

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

M config.def.h => config.def.h +5 -1
@@ 110,7 110,11 @@

/* [NETWORK] */

/* Default CA certifate file path
/* Default CA certificate file path
 *   ("": a list of known paths is checked) */
#define CA_CERT_FILE ""

/* Default CA certificate directory path
 *   ("": a list of known paths is checked) */
#define CA_CERT_PATH ""


D docs/birb.jpg => docs/birb.jpg +0 -0
M docs/rirc.1 => docs/rirc.1 +94 -84
@@ 1,126 1,136 @@
'\" t
.TH RIRC 1 rirc\-VERSION
.SH NAME
rirc \- a minimalistic irc client written in C
rirc \- a minimalistic internet relay chat client
.SH SYNOPSIS
\fBrirc\fR [ \fB-hv\fR ] [ \fB-s\fR \fIserver\fR [ \fB...\fR ]]
.SH DESCRIPTION
.PP
rirc is a lightweight Internet Relay Chat client.
.PP
Connections are TLS enabled over port 6697 by default.
.SH OPTIONS
.TP 5
.TP 4
.B "-h, --help"
Print help message and exit
.TP
.B "-v, --version"
Print rirc version and exit
.SS Server options
.TP 5
.TP
.BI "-s, --server=" server
Connect to \fIserver\fP
Set connection \fIhostname\fP
.TP
.BI "-p, --port=" port
Connect to \fIserver\fP using \fIport\fP
Set connection \fIport\fP
.TP
.BI "-w, --pass=" pass
Connect to \fIserver\fP using \fIpass\fP
Set IRC \fIpassword\fP
.TP
.BI "-u, --username=" username
Connect to \fIserver\fP using \fIusername\fP
Set IRC \fIusername\fP
.TP
.BI "-r, --realname=" realname
Connect to \fIserver\fP using \fIrealname\fP
Set IRC \fIrealname\fP
.TP
.BI "-m, --mode=" modes
Set IRC user \fImodes\fP
.TP
.BI "-n, --nicks=" nicks
Comma separated list of \fInicks\fP to use for \fIserver\fP
Set comma separated list of \fInicks\fP to use
.TP
.BI "-c, --chans=" chans
Comma separated list of \fIchannels\fP to join for \fIserver\fP
.SS Server connection options
.TP 5
.B --ipv4
Connect to \fIserver\fP using only ipv4 addresses
Set comma separated list of \fIchannels\fP to join
.TP
.B --ipv6
Connect to \fIserver\fP using only ipv6 addresses
.BI --tls-cert= path
Set TLS client certificate file \fIpath\fP
.TP
.B --tls-disable
Set \fIserver\fP TLS disabled, default port to 6667
.BI --tls-ca-file= path
Set TLS peer certificate file \fIpath\fP
.TP
.BI --tls-ca-path= path
Set TLS peer certificate directory \fIpath\fP
.TP
.BI --tls-verify= mode
Set \fIserver\fP TLS peer certificate verification \fImode\fP:
Set TLS peer certificate verification \fImode\fP:
.EX
\(bu \fIdisabled\fP - cert is not verified
\(bu \fIoptional\fP - cert is verified, handshake continues on error
\(bu \fIrequired\fP - cert is verified, handshake is aborted on error (default)
.EE
.TP
.B --tls-disable
Set TLS disabled
.TP
.BI --sasl= mechanism
Authenticate with SASL \fImechanism\fP:
.EX
\(bu \fIexternal\fP - requires \fI--tls-cert\fP
\(bu \fIplain\fP    - requires \fI--sasl-user\fP and \fI--sasl-pass\fP
.EE
.TP
.BI --sasl-user= user
Authenticate with SASL \fIusername\fP
.TP
.BI --sasl-pass= pass
Authenticate with SASL \fIpassword\fP
.TP
.B --ipv4
Use IPv4 addresses only
.TP
.B --ipv6
Use IPv6 addresses only
.SH USAGE
.TS
l .
rirc is controlled by a combination of key bindings and commands, where:
rirc is controlled by a combination of keys and commands, where:
  <arg> denotes required arguments
  [arg] denotes optional arguments
.TE

.TS
.tab(;);
lb l .
Keys:
  ^N;go to next channel
  ^P;go to previous channel
  ^C;cancel current input/action
  ^L;clear current channel
  ^X;close current channel
  ^U;scroll current buffer up
  ^D;scroll current buffer down
  <left>;input cursor back
  <right>;input cursor forward
  <up>;input history back
  <down>;input history forward
.TE

.TS
l .
Commands beginning with ':' are interpreted as rirc commands
and control the local client, e.g.:
.TE

.TS
.tab(;);
lb l .
.TP
Commands:
  :clear;
  :close;
  :connect;
  :disconnect;
  :quit;
.TE

.TS
l .
Commands beginning with '/' are interpreted as IRC commands
and will be sent to the current server, e.g.:
.TE

.TS
.tab(;);
lb l .
 \fB:clear\fP
 \fB:close\fP
 \fB:connect\fP [hostname] [options]
 \fB:disconnect\fP
 \fB:quit\fP
.TP
Keys:
 \fB^N\fP   Go to next buffer
 \fB^P\fP   Go to previous buffer
 \fB^L\fP   Clear current buffer
 \fB^X\fP   Close current buffer
 \fB^U\fP   Scroll current buffer back
 \fB^D\fP   Scroll current buffer forward
 \fB^C\fP   Cancel current input/action
 \fB ←\fP   Input cursor back
 \fB →\fP   Input cursor forward
 \fB ↑\fP   Input history back
 \fB ↓\fP   Input history forward
.TP
IRC commands:
  /join;[target, [targets...]]
  /me;<message>
  /nick;[nick]
  /part;[target [targets...]] [part message]
  /privmsg;<target> <message>
  /quit;[quit message]
  /raw;<message>
.TE
.SH AUTHORS
.MT mail@rcr.io
Richard 'rcr' Robbins
.ME .
 \fB/away\fP [away message]
 \fB/join\fP [target, [targets...]]
 \fB/kick\fP <channel> <nick>
 \fB/mode\fP <target> <modes>
 \fB/nick\fP <nick>
 \fB/notice\fP <target> <message>
 \fB/part\fP [target] [message]
 \fB/privmsg\fP <target> <message>
 \fB/quit\fP [message]
 \fB/topic\fP [topic]
 \fB/topic-unset\fP
 \fB/whois\fP <target>
.TP
CTCP commands
 \fB/ctcp-action\fP <target> <message>
 \fB/ctcp-clientinfo\fP [target]
 \fB/ctcp-finger\fP [target]
 \fB/ctcp-ping\fP [target]
 \fB/ctcp-source\fP [target]
 \fB/ctcp-time\fP [target]
 \fB/ctcp-userinfo\fP [target]
 \fB/ctcp-version\fP [target]
.TP
IRCv3 commands
 \fB/cap-ls\fP
 \fB/cap-list\fP
.SH SEE ALSO
.UR https://rcr.io/rirc/
Additional documentation
.UE .
.SH AUTHORS
.MT mail@rcr.io
Richard 'rcr' Robbins
.ME .

M lib/mbedtls.Makefile => lib/mbedtls.Makefile +2 -2
@@ 30,8 30,8 @@ MBEDTLS_SHA_FILE  := 'echo "$(MBEDTLS_VER_SHA)  $(MBEDTLS_TAR)" > $(MBEDTLS_SHA)
MBEDTLS_SHA_CHECK := 'shasum -c $(MBEDTLS_SHA)'
endif

RIRC_CFLAGS += -I$(MBEDTLS_SRC)/include/
RIRC_CFLAGS += -DMBEDTLS_CONFIG_FILE='<$(MBEDTLS_CFG)>'
CFLAGS_RIRC += -I$(MBEDTLS_SRC)/include/
CFLAGS_RIRC += -DMBEDTLS_CONFIG_FILE='<$(MBEDTLS_CFG)>'

RIRC_LIBS += $(MBEDTLS_LIBS)


M scripts/build.sh => scripts/build.sh +3 -2
@@ 8,8 8,9 @@

set -e

export CC=clang
export LDFLAGS="-fuse-ld=lld"
# export CC=clang
# export LDFLAGS="-flto -fuse-ld=lld"
# export LDFLAGS_DEBUG="-fuse-ld=lld"

if [ -x "$(command -v entr)" ]; then
	ENTR="entr -c"

M scripts/coverage.sh => scripts/coverage.sh +3 -2
@@ 6,14 6,15 @@ CDIR="coverage"

export CC=gcc
export CFLAGS_DEBUG="-fprofile-arcs -ftest-coverage -fprofile-abs-path"
export LDFLAGS_DEBUG="-fprofile-arcs"

export MAKEFLAGS="-e -j $(nproc)"

rm -rf $CDIR && mkdir -p $CDIR

make clean
make check

rm -rf $CDIR && mkdir -p $CDIR

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

FILTER=$(cat << 'EOF'

M scripts/sanitizers_build.sh => scripts/sanitizers_build.sh +2 -2
@@ 5,14 5,14 @@ set -e
export CC=clang

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

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"
export LDFLAGS_DEBUG="-fsanitize=thread,undefined -fuse-ld=lld"

make clean rirc.debug


M scripts/sanitizers_test.sh => scripts/sanitizers_test.sh +1 -1
@@ 4,7 4,7 @@ set -e

export CC=clang
export CFLAGS_DEBUG="-fsanitize=address,undefined -fno-omit-frame-pointer"
export LDFLAGS="-fsanitize=address,undefined -fuse-ld=lld"
export LDFLAGS_DEBUG="-fsanitize=address,undefined -fuse-ld=lld"

# for core dumps:
# export ASAN_OPTIONS="abort_on_error=1:disable_coredump=0"

M src/components/channel.c => src/components/channel.c +3 -3
@@ 16,7 16,6 @@ channel(const char *name, enum channel_type type)
	if ((c = calloc(1, sizeof(*c) + len + 1)) == NULL)
		fatal("calloc: %s", strerror(errno));

	c->chanmodes_str.type = MODE_STR_CHANMODE;
	c->name_len = len;
	c->name = memcpy(c->_, name, len + 1);
	c->type = type;


@@ 113,13 112,14 @@ void
channel_part(struct channel *c)
{
	channel_reset(c);
	c->joined = 0;
	c->parted = 1;
}

void
channel_reset(struct channel *c)
{
	mode_reset(&(c->chanmodes), &(c->chanmodes_str));
	memset(&(c->chanmodes), 0, sizeof(c->chanmodes));
	memset(&(c->chanmodes_str), 0, sizeof(c->chanmodes_str));
	user_list_free(&(c->users));
	c->joined = 0;
}

M src/components/ircv3.c => src/components/ircv3.c +19 -0
@@ 1,5 1,6 @@
#include "src/components/ircv3.h"

#include <stdlib.h>
#include <string.h>

struct ircv3_cap*


@@ 18,6 19,7 @@ void
ircv3_caps(struct ircv3_caps *caps)
{
	#define X(CAP, VAR, ATTRS) \
	caps->VAR.val = NULL; \
	caps->VAR.req = 0; \
	caps->VAR.set = 0; \
	caps->VAR.supported = 0; \


@@ 32,9 34,26 @@ void
ircv3_caps_reset(struct ircv3_caps *caps)
{
	#define X(CAP, VAR, ATTRS) \
	free((void *)caps->VAR.val); \
	caps->VAR.val = NULL; \
	caps->VAR.req = 0; \
	caps->VAR.set = 0; \
	caps->VAR.supported = 0;
	IRCV3_CAPS
	#undef X
}

void
ircv3_sasl(struct ircv3_sasl *sasl)
{
	memset(sasl, 0, sizeof(*sasl));

	sasl->mech  = IRCV3_SASL_MECH_NONE;
	sasl->state = IRCV3_SASL_STATE_NONE;
}

void
ircv3_sasl_reset(struct ircv3_sasl *sasl)
{
	sasl->state  = IRCV3_SASL_STATE_NONE;
}

M src/components/ircv3.h => src/components/ircv3.h +22 -1
@@ 13,7 13,8 @@
	X("chghost",        chghost,        IRCV3_CAP_AUTO) \
	X("extended-join",  extended_join,  IRCV3_CAP_AUTO) \
	X("invite-notify",  invite_notify,  IRCV3_CAP_AUTO) \
	X("multi-prefix",   multi_prefix,   IRCV3_CAP_AUTO)
	X("multi-prefix",   multi_prefix,   IRCV3_CAP_AUTO) \
	X("sasl",           sasl,           IRCV3_CAP_AUTO)

/* Extended by testcases */
#ifndef IRCV3_CAPS_TEST


@@ 26,6 27,7 @@

struct ircv3_cap
{
	const char *val;           /* cap key=val */
	unsigned req          : 1; /* cap REQ sent */
	unsigned req_auto     : 1; /* cap REQ sent automatically */
	unsigned set          : 1; /* cap is unset/set */


@@ 42,9 44,28 @@ struct ircv3_caps
	#undef X
};

struct ircv3_sasl
{
	enum {
		IRCV3_SASL_MECH_NONE,
		IRCV3_SASL_MECH_EXTERNAL,
		IRCV3_SASL_MECH_PLAIN,
	} mech;
	enum {
		IRCV3_SASL_STATE_NONE,
		IRCV3_SASL_STATE_REQ_MECH,
		IRCV3_SASL_STATE_AUTHENTICATED,
	} state;
	const char *user;
	const char *pass;
};

struct ircv3_cap* ircv3_cap_get(struct ircv3_caps*, const char*);

void ircv3_caps(struct ircv3_caps*);
void ircv3_caps_reset(struct ircv3_caps*);

void ircv3_sasl(struct ircv3_sasl*);
void ircv3_sasl_reset(struct ircv3_sasl*);

#endif

M src/components/mode.c => src/components/mode.c +124 -413
@@ 5,70 5,54 @@
#include <ctype.h>
#include <string.h>

#define MODE_ISLOWER(X) ((X) >= 'a' && (X) <= 'z')
#define MODE_ISUPPER(X) ((X) >= 'A' && (X) <= 'Z')

/* Set bit Y of X to the value of Z: [0, 1] */
#define MODE_SET(X, Y, Z) ((X) ^= (-(Z) ^ (X)) & (Y))

enum mode_chanmode_prefix
{
	MODE_CHANMODE_PREFIX_SECRET  = '@', /* chanmode 's' */
	MODE_CHANMODE_PREFIX_PRIVATE = '*', /* chanmode 'p' */
	MODE_CHANMODE_PREFIX_OTHER   = '=',
};
#define MODE_SET_BIT(X, Y, Z) ((X) ^= (-(Z) ^ (X)) & (Y))

static inline int mode_isset(const struct mode*, int);
static inline uint32_t flag_bit(int);
static int mode_isset(const struct mode*, int);
static void mode_set(struct mode*, int, int);
static uint32_t mode_bit(uint8_t);

static enum mode_err mode_cfg_chanmodes(struct mode_cfg*, const char*);
static enum mode_err mode_cfg_usermodes(struct mode_cfg*, const char*);
static enum mode_err mode_cfg_subtypes(struct mode_cfg*, const char*);
static enum mode_err mode_cfg_prefix(struct mode_cfg*, const char*);
static enum mode_err mode_cfg_modes(struct mode_cfg*, const char*);

/* TODO: check validity of set_t on all mode settings */
/* TODO: static inline void mode_bit_set(struct mode*, uint32_t); */
/* TODO: static inline void mode_bit_isset(struct mode*, uint32_t); */
/* TODO: aggregate errors with logging callback */
/* TODO: safe channels ('!' prefix) (see RFC2811) */

static inline int
mode_isset(const struct mode *m, int flag)
static uint32_t
mode_bit(uint8_t c)
{
	/* Test if mode flag is set, assumes valid flag */
	static const uint32_t mode_bits[] = {
		['a'] = (1U << 0), ['j'] = (1U << 9),  ['s'] = (1U << 18),
		['b'] = (1U << 1), ['k'] = (1U << 10), ['t'] = (1U << 19),
		['c'] = (1U << 2), ['l'] = (1U << 11), ['u'] = (1U << 20),
		['d'] = (1U << 3), ['m'] = (1U << 12), ['v'] = (1U << 21),
		['e'] = (1U << 4), ['n'] = (1U << 13), ['w'] = (1U << 22),
		['f'] = (1U << 5), ['o'] = (1U << 14), ['x'] = (1U << 23),
		['g'] = (1U << 6), ['p'] = (1U << 15), ['y'] = (1U << 24),
		['h'] = (1U << 7), ['q'] = (1U << 16), ['z'] = (1U << 25),
		['i'] = (1U << 8), ['r'] = (1U << 17), [UINT8_MAX] = 0
	};

	if (MODE_ISLOWER(flag) && (m->lower & flag_bit(flag)))
		return 1;
	return mode_bits[tolower(c)];
}

	if (MODE_ISUPPER(flag) && (m->upper & flag_bit(flag)))
		return 1;
static void
mode_set(struct mode *m, int flag, int set)
{
	if (islower(flag))
		MODE_SET_BIT(m->lower, mode_bit(flag), !!set);

	return 0;
	if (isupper(flag))
		MODE_SET_BIT(m->upper, mode_bit(flag), !!set);
}

static inline uint32_t
flag_bit(int c)
static int
mode_isset(const struct mode *m, int flag)
{
	/* Map input character to [az-AZ] bit flag */

	static const uint32_t flag_bits[] = {
		1U << 0,  /* a */ 1U << 1,  /* b */ 1U << 2,  /* c */
		1U << 3,  /* d */ 1U << 4,  /* e */ 1U << 5,  /* f */
		1U << 6,  /* g */ 1U << 7,  /* h */ 1U << 8,  /* i */
		1U << 9,  /* j */ 1U << 10, /* k */ 1U << 11, /* l */
		1U << 12, /* m */ 1U << 13, /* n */ 1U << 14, /* o */
		1U << 15, /* p */ 1U << 16, /* q */ 1U << 17, /* r */
		1U << 18, /* s */ 1U << 19, /* t */ 1U << 20, /* u */
		1U << 21, /* v */ 1U << 22, /* w */ 1U << 23, /* x */
		1U << 24, /* y */ 1U << 25, /* z */
	};

	if (MODE_ISLOWER(c))
		return flag_bits[c - 'a'];
	if (islower(flag) && (m->lower & mode_bit(flag)))
		return 1;

	if (MODE_ISUPPER(c))
		return flag_bits[c - 'A'];
	if (isupper(flag) && (m->upper & mode_bit(flag)))
		return 1;

	return 0;
}


@@ 99,10 83,9 @@ mode_cfg(struct mode_cfg *cfg, const char *cfg_str, enum mode_cfg_type cfg_type)
	 *
	 *   b - set/remove ban mask to keep users out;
	 *   e - set/remove an exception mask to override a ban mask;
	 *   I - set/remove an invitation mask to automatically override the
	 *       invite-only flag;
	 *   I - set/remove an invitation mask to automatically override the invite-only flag;
	 *
	 * Usermodes (RFC2811, section 3.1.5)
	 * Usermodes (RFC2812, section 3.1.5)
	 *
	 *   a - user is flagged as away;
	 *   i - marks a users as invisible;


@@ 111,33 94,17 @@ mode_cfg(struct mode_cfg *cfg, const char *cfg_str, enum mode_cfg_type cfg_type)
	 *   o - operator flag;
	 *   O - local operator flag;
	 *   s - marks a user for receipt of server notices.
	 *
	 * MODES (RFC2811, section 3.2.3)
	 *
	 *   "Note that there is a maximum limit of three (3) changes per command
	 *    for modes that take a parameter."
	 *
	 * Note: PREFIX, MODES and CHANMODES are ubiquitous additions to the IRC
	 *       protocol given by numeric 005 (RPL_ISUPPORT). As such,
	 *       they've been interpreted here in terms of A,B,C,D subcategories
	 *       for the sake of default settings. Numeric 319 (RPL_WHOISCHANNELS)
	 *       states chanmode user prefixes map o,v to @,+ respectively.
	 */

	switch (cfg_type) {

		case MODE_CFG_DEFAULTS:
			*cfg = (struct mode_cfg)
			{
				.PREFIX = {
					.F = "ov",
					.T = "@+"
				},
				.MODES = 3
			};
			memset(cfg, 0, sizeof(*cfg));
			(void) snprintf(cfg->PREFIX.F, sizeof(cfg->PREFIX.F), "ov");
			(void) snprintf(cfg->PREFIX.T, sizeof(cfg->PREFIX.T), "@+");
			mode_cfg_chanmodes(cfg, "OovaimnqpsrtklbeI");
			mode_cfg_usermodes(cfg, "aiwroOs");
			mode_cfg_subtypes(cfg, "beI,k,l,aimnqpsrtO");
			mode_cfg_subtypes(cfg, "IObe,k,l,aimnqpsrt");
			break;

		case MODE_CFG_CHANMODES:


@@ 152,9 119,6 @@ mode_cfg(struct mode_cfg *cfg, const char *cfg_str, enum mode_cfg_type cfg_type)
		case MODE_CFG_PREFIX:
			return mode_cfg_prefix(cfg, cfg_str);

		case MODE_CFG_MODES:
			return mode_cfg_modes(cfg, cfg_str);

		default:
			fatal("mode configuration type unknown: %d", cfg_type);
	}


@@ 163,127 127,41 @@ mode_cfg(struct mode_cfg *cfg, const char *cfg_str, enum mode_cfg_type cfg_type)
}

enum mode_err
mode_chanmode_set(struct mode *m, const struct mode_cfg *cfg, int flag, enum mode_set set)
mode_chanmode_set(struct mode *m, const struct mode_cfg *cfg, int flag, int set)
{
	/* Set/unset chanmode flags
	 *
	 * Only CHANMODE subtypes B,C,D set/unset flags for the channel
	 *
	 * ---
	 *
	 * RFC2812, section 5.1, numeric reply 353 (RPL_NAMREPLY)
	 *
	 * "@" is used for secret channels,   ('s' flag)
	 * "*" for private channels, and      ('p' flag)
	 * "=" for others (public channels).
	 *
	 * RFC2811, section 4.2.6 Private and Secret Channels
	 *
	 * The channel flag 'p' is used to mark a channel "private" and the
	 * channel flag 's' to mark a channel "secret".  Both properties are
	 * similar and conceal the existence of the channel from other users.
	 *
	 * This means that there is no way of getting this channel's name from
	 * the server without being a member.  In other words, these channels
	 * MUST be omitted from replies to queries like the WHOIS command.
	 *
	 * When a channel is "secret", in addition to the restriction above, the
	 * server will act as if the channel does not exist for queries like the
	 * TOPIC, LIST, NAMES commands.  Note that there is one exception to
	 * this rule: servers will correctly reply to the MODE command.
	 * Finally, secret channels are not accounted for in the reply to the
	 * LUSERS command (See "Internet Relay Chat: Client Protocol" [IRC-
	 * CLIENT]) when the <mask> parameter is specified.
	 *
	 * The channel flags 'p' and 's' MUST NOT both be set at the same time.
	 * If a MODE message originating from a server sets the flag 'p' and the
	 * flag 's' is already set for the channel, the change is silently
	 * ignored.  This should only happen during a split healing phase
	 * (mentioned in the "IRC Server Protocol" document [IRC-SERVER]).
	 */

	if (!(set == MODE_SET_ON || set == MODE_SET_OFF))
		return MODE_ERR_INVALID_SET;
	/* Set/unset chanmode flags */

	if (!mode_isset(&(cfg->chanmodes), flag))
		return MODE_ERR_INVALID_FLAG;

	if (set == MODE_SET_ON && mode_isset(m, flag))
		return MODE_ERR_DUPLICATE;

	/* Mode subtypes A don't set a flag */
	if (mode_isset(&(cfg->CHANMODES.A), flag))
		return MODE_ERR_NONE;

	if (flag != 's' && flag != 'p') {

		uint32_t bit = flag_bit(flag);

		if (MODE_ISLOWER(flag))
			MODE_SET(m->lower, bit, set);
		else
			MODE_SET(m->upper, bit, set);

	} else if (flag == 'p') {

		/* Silently ignore */
		if (mode_isset(m, 's'))
			return MODE_ERR_NONE;

		if (set == MODE_SET_OFF) {
			MODE_SET(m->lower, flag_bit('p'), MODE_SET_OFF);

			m->prefix = MODE_CHANMODE_PREFIX_OTHER;
		}

		if (set == MODE_SET_ON) {
			MODE_SET(m->lower, flag_bit('p'), MODE_SET_ON);

			m->prefix = MODE_CHANMODE_PREFIX_PRIVATE;
		}

	} else if (flag == 's') {

		if (set == MODE_SET_OFF) {
			MODE_SET(m->lower, flag_bit('s'), MODE_SET_OFF);
			MODE_SET(m->lower, flag_bit('p'), MODE_SET_OFF);

			m->prefix = MODE_CHANMODE_PREFIX_OTHER;
		}

		if (set == MODE_SET_ON) {
			MODE_SET(m->lower, flag_bit('s'), MODE_SET_ON);
			MODE_SET(m->lower, flag_bit('p'), MODE_SET_OFF);

			m->prefix = MODE_CHANMODE_PREFIX_SECRET;
		}
	}
	mode_set(m, flag, set);

	return MODE_ERR_NONE;
}

enum mode_err
mode_prfxmode_set(struct mode *m, const struct mode_cfg *cfg, int flag, enum mode_set set)
mode_prfxmode_set(struct mode *m, const struct mode_cfg *cfg, int flag, int set)
{
	/* Set/unset prfxmode flags and mode prefix */
	/* Set/unset prfxmode flag or prefix */

	uint32_t bit;
	const char *f = cfg->PREFIX.F;
	const char *t = cfg->PREFIX.T;

	if (!(set == MODE_SET_ON || set == MODE_SET_OFF))
		return MODE_ERR_INVALID_SET;
	while (*f && *t && *f != flag && *t != flag) {
		f++;
		t++;
	}

	if (!strchr(cfg->PREFIX.F, flag))
	if (!*f || !*t)
		return MODE_ERR_INVALID_FLAG;

	bit = flag_bit(flag);
	mode_set(m, *f, set);

	if (MODE_ISLOWER(flag))
		MODE_SET(m->lower, bit, set);
	else
		MODE_SET(m->upper, bit, set);

	const char *f = cfg->PREFIX.F,
	           *t = cfg->PREFIX.T;
	f = cfg->PREFIX.F;
	t = cfg->PREFIX.T;

	while (*f) {



@@ 300,140 178,42 @@ mode_prfxmode_set(struct mode *m, const struct mode_cfg *cfg, int flag, enum mod
}

enum mode_err
mode_usermode_set(struct mode *m, const struct mode_cfg *cfg, int flag, enum mode_set set)
mode_usermode_set(struct mode *m, const struct mode_cfg *cfg, int flag, int set)
{
	/* Set/unset usermode flags */

	uint32_t bit;

	if (!(set == MODE_SET_ON || set == MODE_SET_OFF))
		return MODE_ERR_INVALID_SET;

	if (!mode_isset(&(cfg->usermodes), flag))
		return MODE_ERR_INVALID_FLAG;

	bit = flag_bit(flag);

	if (MODE_ISLOWER(flag))
		MODE_SET(m->lower, bit, set);
	else
		MODE_SET(m->upper, bit, set);

	return MODE_ERR_NONE;
}

enum mode_err
mode_chanmode_prefix(struct mode *m, const struct mode_cfg *cfg, int flag)
{
	/* Set chanmode flag and prefix give the prefix character, e.g.:
	 *
	 * - '@' sets 's', unsets 'p'
	 * - '*' sets 'p'
	 * - '=' sets neither
	 *
	 * All other prefixes are invalid.
	 * Prefixes may override by precendece, but are silentyly ignored otherwise */

	(void)(cfg);

	/* If 's' is set, all other settings are silently ignored */
	if (m->prefix == MODE_CHANMODE_PREFIX_SECRET)
		return MODE_ERR_NONE;

	/* If 'p' is set, only SECRET prefix is accepted */
	if (m->prefix == MODE_CHANMODE_PREFIX_PRIVATE && flag != MODE_CHANMODE_PREFIX_SECRET)
		return MODE_ERR_NONE;

	/* Otherwise, all valid prefixes can be set */
	switch (flag) {
		case MODE_CHANMODE_PREFIX_SECRET:
			MODE_SET(m->lower, flag_bit('p'), MODE_SET_OFF);
			MODE_SET(m->lower, flag_bit('s'), MODE_SET_ON);
			break;
		case MODE_CHANMODE_PREFIX_PRIVATE:
			MODE_SET(m->lower, flag_bit('p'), MODE_SET_ON);
			break;
		case MODE_CHANMODE_PREFIX_OTHER:
			break;
		default:
			return MODE_ERR_INVALID_PREFIX;
	}

	m->prefix = flag;

	return MODE_ERR_NONE;
}

enum mode_err
mode_prfxmode_prefix(struct mode *m, const struct mode_cfg *cfg, int flag)
{
	/* Set prfxmode flag and prefix given the prefix character, e.g.: 
	 *
	 * - if "ov" maps to "@+", then:
	 *   - prfxmode_prefix(cfg, mode, '@')   sets mode flag 'o'
	 *   - prfxmode_prefix(cfg, mode, '+')   sets mode flag 'v'
	 */

	uint32_t bit;

	const char *f = cfg->PREFIX.F,
	           *t = cfg->PREFIX.T;

	while (*t && *t != flag) {
		f++;
		t++;
	}

	if (*t == 0)
		return MODE_ERR_INVALID_PREFIX;

	bit = flag_bit(*f);

	if (MODE_ISLOWER(*f))
		MODE_SET(m->lower, bit, MODE_SET_ON);
	else
		MODE_SET(m->upper, bit, MODE_SET_ON);

	f = cfg->PREFIX.F,
	t = cfg->PREFIX.T;

	while (!mode_isset(m, *f)) {
		f++;
		t++;
	}

	m->prefix = *t;
	mode_set(m, flag, set);

	return MODE_ERR_NONE;
}

const char*
mode_str(const struct mode *m, struct mode_str *m_str)
mode_str(const struct mode *m, struct mode_str *m_str, enum mode_str_type type)
{
	/* Write the mode bits to a mode string */

	char c;
	char *str = m_str->str;

	uint32_t lower = m->lower,
	         upper = m->upper;
	uint32_t lower = m->lower;
	uint32_t upper = m->upper;

	switch (m_str->type) {
	switch (type) {
		case MODE_STR_CHANMODE:
		case MODE_STR_USERMODE:
		case MODE_STR_PRFXMODE:
			break;
		case MODE_STR_UNSET:
			fatal("mode_str type not set");
		default:
			fatal("mode_str type unknown");
	}

	for (c = 'a'; c <= 'z' && lower; c++, lower >>= 1)
	for (char c = 'a'; c <= 'z' && lower; c++, lower >>= 1)
		if (lower & 1)
			*str++ = c;

	for (c = 'A'; c <= 'Z' && upper; c++, upper >>= 1)
	for (char c = 'A'; c <= 'Z' && upper; c++, upper >>= 1)
		if (upper & 1)
			*str++ = c;



@@ 442,22 222,6 @@ mode_str(const struct mode *m, struct mode_str *m_str)
	return m_str->str;
}

void
mode_reset(struct mode *m, struct mode_str *s)
{
	/* Set mode and mode_str to initial state */

	if (!m || !s)
		fatal("mode or mode_str is null");

	enum mode_str_type type = s->type;

	memset(m, 0, sizeof(*m));
	memset(s, 0, sizeof(*s));

	s->type = type;
}

static enum mode_err
mode_cfg_chanmodes(struct mode_cfg *cfg, const char *str)
{


@@ 471,18 235,13 @@ mode_cfg_chanmodes(struct mode_cfg *cfg, const char *str)

	while ((c = *str++)) {

		uint32_t bit;

		if ((bit = flag_bit(c)) == 0)
			continue; /* TODO: aggregate warnings, invalid flag */
		if (!mode_bit(c))
			continue;

		if (mode_isset(chanmodes, c))
			continue; /* TODO: aggregate warnings, duplicate flag */
			continue;

		if (MODE_ISLOWER(c))
			MODE_SET(chanmodes->lower, bit, MODE_SET_ON);
		else
			MODE_SET(chanmodes->upper, bit, MODE_SET_ON);
		mode_set(chanmodes, c, 1);
	}

	return MODE_ERR_NONE;


@@ 501,18 260,13 @@ mode_cfg_usermodes(struct mode_cfg *cfg, const char *str)

	while ((c = *str++)) {

		uint32_t bit;

		if ((bit = flag_bit(c)) == 0)
			continue; /* TODO: aggregate warnings, invalid flag */
		if (!mode_bit(c))
			continue;

		if (mode_isset(usermodes, c))
			continue; /* TODO: aggregate warnings, duplicate flag */
			continue;

		if (MODE_ISLOWER(c))
			MODE_SET(usermodes->lower, bit, MODE_SET_ON);
		else
			MODE_SET(usermodes->upper, bit, MODE_SET_ON);
		mode_set(usermodes, c, 1);
	}

	return MODE_ERR_NONE;


@@ 524,10 278,10 @@ mode_cfg_subtypes(struct mode_cfg *cfg, const char *str)
	/* Parse and configure CHANMODE subtypes, e.g.:
	 *
	 * "abc,d,ef,xyz" sets mode bits:
	 *  - A = a | b | c
	 *  - A = abc
	 *  - B = d
	 *  - C = e | f
	 *  - D = x | y | z
	 *  - C = ef
	 *  - D = xyz
	 */

	char c;


@@ 539,20 293,17 @@ mode_cfg_subtypes(struct mode_cfg *cfg, const char *str)
		&(cfg->CHANMODES.D)
	};

	struct mode duplicates, *setting = subtypes[0];
	struct mode *setting = subtypes[0];

	memset(&(cfg->CHANMODES.A), 0, sizeof (struct mode));
	memset(&(cfg->CHANMODES.B), 0, sizeof (struct mode));
	memset(&(cfg->CHANMODES.C), 0, sizeof (struct mode));
	memset(&(cfg->CHANMODES.D), 0, sizeof (struct mode));
	memset(&duplicates, 0, sizeof (struct mode));
	memset(&(cfg->CHANMODES.A), 0, sizeof(struct mode));
	memset(&(cfg->CHANMODES.B), 0, sizeof(struct mode));
	memset(&(cfg->CHANMODES.C), 0, sizeof(struct mode));
	memset(&(cfg->CHANMODES.D), 0, sizeof(struct mode));

	unsigned commas = 0;

	while ((c = *str++)) {

		uint32_t bit;

		if (c == ',') {
			switch (commas) {
				case 0:


@@ 561,26 312,26 @@ mode_cfg_subtypes(struct mode_cfg *cfg, const char *str)
					setting = subtypes[++commas];
					continue;
				default:
					return MODE_ERR_INVALID_CONFIG;
					goto error;
			}
		}

		if ((bit = flag_bit(c)) == 0)
			continue; /* TODO: aggregate warnings, invalid flag */

		if (mode_isset(&duplicates, c))
			continue; /* TODO: aggregate warnings, duplicate flag */
		if (!mode_bit(c))
			goto error;

		if (MODE_ISLOWER(c)) {
			MODE_SET(duplicates.lower, bit, MODE_SET_ON);
			MODE_SET(setting->lower, bit, MODE_SET_ON);
		} else {
			MODE_SET(duplicates.upper, bit, MODE_SET_ON);
			MODE_SET(setting->upper, bit, MODE_SET_ON);
		}
		mode_set(setting, c, 1);
	}

	return MODE_ERR_NONE;

error:

	memset(&(cfg->CHANMODES.A), 0, sizeof(struct mode));
	memset(&(cfg->CHANMODES.B), 0, sizeof(struct mode));
	memset(&(cfg->CHANMODES.C), 0, sizeof(struct mode));
	memset(&(cfg->CHANMODES.D), 0, sizeof(struct mode));

	return MODE_ERR_INVALID_CONFIG;
}

static enum mode_err


@@ 594,17 345,16 @@ mode_cfg_prefix(struct mode_cfg *cfg, const char *str)
	 *  - c -> #
	 */

	char *str_f, cf,
	     *str_t, ct,
	     *cfg_f = cfg->PREFIX.F,
	     *cfg_t = cfg->PREFIX.T,
	     _str[strlen(str) + 1];
	char *cfg_f = cfg->PREFIX.F;
	char *cfg_t = cfg->PREFIX.T;
	char *dup = strdup(str);
	char *str_f;
	char *str_t;

	struct mode duplicates;
	memset(cfg->PREFIX.F, 0, sizeof(cfg->PREFIX.F));
	memset(cfg->PREFIX.T, 0, sizeof(cfg->PREFIX.T));

	memcpy(_str, str, sizeof(_str));

	if (*(str_f = _str) != '(')
	if (*(str_f = dup) != '(')
		goto error;

	if (!(str_t = strchr(str_f, ')')))


@@ 616,32 366,20 @@ mode_cfg_prefix(struct mode_cfg *cfg, const char *str)
	if (strlen(str_f) != strlen(str_t))
		goto error;

	memset(&duplicates, 0, sizeof duplicates);

	while (*str_f) {

		uint32_t bit;

		cf = *str_f++;
		ct = *str_t++;
		char cf = *str_f++;
		char ct = *str_t++;

		/* Check printable prefix */
		if (!(isgraph(ct)))
		if (!mode_bit(cf))
			goto error;

		/* Check valid flag */
		if ((bit = flag_bit(cf)) == 0)
		if (!(isgraph(ct)))
			goto error;

		/* Check duplicates */
		if (mode_isset(&duplicates, cf))
		if (strchr(cfg->PREFIX.F, cf))
			goto error;

		if (MODE_ISLOWER(cf))
			MODE_SET(duplicates.lower, bit, MODE_SET_ON);
		else
			MODE_SET(duplicates.upper, bit, MODE_SET_ON);

		*cfg_f++ = cf;
		*cfg_t++ = ct;
	}


@@ 649,6 387,8 @@ mode_cfg_prefix(struct mode_cfg *cfg, const char *str)
	*cfg_f = 0;
	*cfg_t = 0;

	free(dup);

	return MODE_ERR_NONE;

error:


@@ 656,62 396,33 @@ error:
	*(cfg->PREFIX.F) = 0;
	*(cfg->PREFIX.T) = 0;

	return MODE_ERR_INVALID_CONFIG;
}
	free(dup);

static enum mode_err
mode_cfg_modes(struct mode_cfg *cfg, const char *str)
{
	/* Parse and configure MODES, valid values are numeric strings [1-99] */

	unsigned modes = 0;

	for (; modes < 100 && *str; str++) {
		if (isdigit(*str))
			modes = modes * 10 + (*str - '0');
		else
			return MODE_ERR_INVALID_CONFIG;
	}

	if (!(modes > 0 && modes < 100))
		return MODE_ERR_INVALID_CONFIG;

	cfg->MODES = modes;

	return MODE_ERR_NONE;
	return MODE_ERR_INVALID_CONFIG;
}

enum chanmode_flag_type
chanmode_type(const struct mode_cfg *cfg, enum mode_set set, int flag)
enum mode_type
mode_type(const struct mode_cfg *cfg, int flag, int set)
{
	/* Return the chanmode flag type specified by config */

	if (!(set == MODE_SET_ON || set == MODE_SET_OFF))
		return MODE_FLAG_INVALID_SET;

	if (mode_isset(&(cfg->chanmodes), flag)) {

		if (strchr(cfg->PREFIX.F, flag))
			return MODE_FLAG_PREFIX;

		if (mode_isset(&(cfg->CHANMODES.A), flag))
			return MODE_FLAG_CHANMODE_PARAM;
	/* Chanmode PREFIX */
	if (strchr(cfg->PREFIX.F, flag))
		return MODE_FLAG_PREFIX;

		if (mode_isset(&(cfg->CHANMODES.B), flag))
			return MODE_FLAG_CHANMODE_PARAM;

		if (mode_isset(&(cfg->CHANMODES.C), flag)) {
	/* Chanmode subtype A, Always has a parameter. */
	if (mode_isset(&(cfg->CHANMODES.A), flag))
		return MODE_FLAG_CHANMODE_PARAM;

			if (set == MODE_SET_ON)
				return MODE_FLAG_CHANMODE_PARAM;
	/* Chanmode subtype B, Always has a parameter. */
	if (mode_isset(&(cfg->CHANMODES.B), flag))
		return MODE_FLAG_CHANMODE_PARAM;

			if (set == MODE_SET_OFF)
				return MODE_FLAG_CHANMODE;
		}
	/* Chanmode subtype C, Only has a parameter when set. */
	if (mode_isset(&(cfg->CHANMODES.C), flag))
		return (set ? MODE_FLAG_CHANMODE_PARAM : MODE_FLAG_CHANMODE);

		if (mode_isset(&(cfg->CHANMODES.D), flag))
			return MODE_FLAG_CHANMODE;
	}
	/* Chanmode subtype D, Never has a parameter. */
	if (mode_isset(&(cfg->CHANMODES.D), flag))
		return MODE_FLAG_CHANMODE;

	return MODE_FLAG_INVALID_FLAG;
}

M src/components/mode.h => src/components/mode.h +24 -48
@@ 30,9 30,6 @@
 *
 * PREFIX modes are not included in CHANMODES
 *
 * MODES specifies the maximum number of channel modes with parameter
 * allowed per MODE command
 *
 * Numeric 353 (RPL_NAMREPLY) sets chanmode and prfxmode for users on a channel
 * by providing the prefix character rather than the flag
 */


@@ 42,47 39,38 @@
/* [azAZ] */
#define MODE_STR_LEN 26 * 2

#define MODE_EMPTY (struct mode) \
{                \
    .prefix = 0, \
    .lower  = 0, \
    .upper  = 0, \
}
#define MODE_EMPTY (struct mode) { 0 }

enum mode_err
{
	MODE_ERR_DUPLICATE      = -5,
	MODE_ERR_INVALID_CONFIG = -4,
	MODE_ERR_INVALID_PREFIX = -3,
	MODE_ERR_INVALID_FLAG   = -2,
	MODE_ERR_INVALID_SET    = -1,
	MODE_ERR_INVALID_CONFIG = -3,
	MODE_ERR_INVALID_PREFIX = -2,
	MODE_ERR_INVALID_FLAG   = -1,
	MODE_ERR_NONE
};

enum mode_type
{
	MODE_FLAG_INVALID_FLAG,
	MODE_FLAG_CHANMODE,       /* Chanmode flag without parameter */
	MODE_FLAG_CHANMODE_PARAM, /* Chanmode flag with parameter */
	MODE_FLAG_PREFIX,         /* Chanmode flag that sets prfxmode */
};

enum mode_cfg_type
{
	MODE_CFG_DEFAULTS,  /* Set RFC2811 mode defaults */
	MODE_CFG_CHANMODES, /* Set numeric 004 chanmdoes string */
	MODE_CFG_USERMODES, /* Set numeric 004 usermodes string */
	MODE_CFG_PREFIX,    /* Set numeric 005 PREFIX */
	MODE_CFG_SUBTYPES,  /* Set numeric 005 CHANMODES subtypes */
	MODE_CFG_MODES,     /* Set numeric 005 MODES */
};

enum mode_set
{
	MODE_SET_OFF = 0,
	MODE_SET_ON = 1,
	MODE_SET_INVALID,
	MODE_CFG_PREFIX,    /* Set numeric 005 PREFIX */
};

enum chanmode_flag_type
enum mode_str_type
{
	MODE_FLAG_INVALID_FLAG,
	MODE_FLAG_INVALID_SET,
	MODE_FLAG_CHANMODE,       /* Chanmode flag without parameter */
	MODE_FLAG_CHANMODE_PARAM, /* Chanmode flag with parameter */
	MODE_FLAG_PREFIX,         /* Chanmode flag that sets prfxmode */
	MODE_STR_CHANMODE,
	MODE_STR_USERMODE,
	MODE_STR_PRFXMODE,
};

struct mode


@@ 94,12 82,11 @@ struct mode

struct mode_cfg
{
	unsigned MODES;        /* Numeric 005 MODES */
	struct mode chanmodes; /* Numeric 004 chanmodes string */
	struct mode usermodes; /* Numeric 004 usermodes string */
	struct
	struct                 /* Numeric 005 CHANMODES substrings */
	{
		struct mode A; /* Numeric 005 CHANMODES substrings */
		struct mode A;
		struct mode B;
		struct mode C;
		struct mode D;


@@ 114,24 101,13 @@ struct mode_cfg
struct mode_str
{
	char str[MODE_STR_LEN + 1];
	enum mode_str_type
	{
		MODE_STR_UNSET = 0,
		MODE_STR_CHANMODE,
		MODE_STR_USERMODE,
		MODE_STR_PRFXMODE,
		MODE_STR_T_SIZE
	} type;
};

const char* mode_str(const struct mode*, struct mode_str*);
enum chanmode_flag_type chanmode_type(const struct mode_cfg*, enum mode_set, int);
const char* mode_str(const struct mode*, struct mode_str*, enum mode_str_type);
enum mode_err mode_cfg(struct mode_cfg*, const char*, enum mode_cfg_type);
enum mode_err mode_chanmode_prefix(struct mode*, const struct mode_cfg*, int);
enum mode_err mode_chanmode_set(struct mode*, const struct mode_cfg*, int, enum mode_set);
enum mode_err mode_prfxmode_prefix(struct mode*, const struct mode_cfg*, int);
enum mode_err mode_prfxmode_set(struct mode*, const struct mode_cfg*, int, enum mode_set);
enum mode_err mode_usermode_set(struct mode*, const struct mode_cfg*, int, enum mode_set);
void mode_reset(struct mode*, struct mode_str*);
enum mode_err mode_chanmode_set(struct mode*, const struct mode_cfg*, int, int);
enum mode_err mode_prfxmode_set(struct mode*, const struct mode_cfg*, int, int);
enum mode_err mode_usermode_set(struct mode*, const struct mode_cfg*, int, int);
enum mode_type mode_type(const struct mode_cfg*, int, int);

#endif

M src/components/server.c => src/components/server.c +42 -17
@@ 7,11 7,11 @@
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>

#define HANDLED_005 \
	X(CASEMAPPING)  \
	X(CHANMODES)    \
	X(MODES)        \
	X(PREFIX)

struct opt


@@ 28,21 28,30 @@ HANDLED_005
#undef X

struct server*
server(const char *host, const char *port, const char *pass, const char *user, const char *real)
server(
	const char *host,
	const char *port,
	const char *pass,
	const char *username,
	const char *realname,
	const char *mode)
{
	struct server *s;

	if ((s = calloc(1, sizeof(*s))) == NULL)
		fatal("calloc: %s", strerror(errno));

	s->host = strdup(host);
	s->port = strdup(port);
	s->pass = pass ? strdup(pass) : NULL;
	s->username = strdup(user);
	s->realname = strdup(real);
	s->host     = strdup(host);
	s->port     = strdup(port);
	s->username = strdup(username);
	s->realname = strdup(realname);

	s->pass = (pass ? strdup(pass) : NULL);
	s->mode = (mode ? strdup(mode) : NULL);

	s->casemapping = CASEMAPPING_RFC1459;
	s->mode_str.type = MODE_STR_USERMODE;
	ircv3_caps(&(s->ircv3_caps));
	ircv3_sasl(&(s->ircv3_sasl));
	mode_cfg(&(s->mode_cfg), NULL, MODE_CFG_DEFAULTS);

	s->channel = channel(host, CHANNEL_T_SERVER);


@@ 130,7 139,9 @@ void
server_reset(struct server *s)
{
	ircv3_caps_reset(&(s->ircv3_caps));
	mode_reset(&(s->usermodes), &(s->mode_str));
	ircv3_sasl_reset(&(s->ircv3_sasl));
	memset(&(s->usermodes), 0, sizeof(s->usermodes));
	memset(&(s->mode_str), 0, sizeof(s->mode_str));
	s->ping = 0;
	s->quitting = 0;
	s->registered = 0;


@@ 142,16 153,17 @@ server_free(struct server *s)
{
	channel_list_free(&(s->clist));

	user_list_free(&(s->ignore));

	free((void *)s->host);
	free((void *)s->port);
	free((void *)s->pass);
	free((void *)s->username);
	free((void *)s->realname);
	free((void *)s->mode);
	free((void *)s->nick);
	free((void *)s->nicks.base);
	free((void *)s->nicks.set);
	free((void *)s->ircv3_sasl.user);
	free((void *)s->ircv3_sasl.pass);
	free(s);
}



@@ 312,6 324,25 @@ server_set_005(struct server *s, char *str)
	}
}

void
server_set_sasl(struct server *s, const char *mech, const char *user, const char *pass)
{
	free((void *)s->ircv3_sasl.user);
	free((void *)s->ircv3_sasl.pass);

	if (!strcasecmp(mech, "EXTERNAL")) {
		s->ircv3_sasl.mech = IRCV3_SASL_MECH_EXTERNAL;
		s->ircv3_sasl.user = NULL;
		s->ircv3_sasl.pass = NULL;
	}

	if (!strcasecmp(mech, "PLAIN")) {
		s->ircv3_sasl.mech = IRCV3_SASL_MECH_PLAIN;
		s->ircv3_sasl.user = (user ? strdup(user) : NULL);
		s->ircv3_sasl.pass = (pass ? strdup(pass) : NULL);
	}
}

static int
server_cmp(const struct server *s, const char *host, const char *port)
{


@@ 424,12 455,6 @@ server_set_CHANMODES(struct server *s, char *val)
}

static int
server_set_MODES(struct server *s, char *val)
{
	return mode_cfg(&(s->mode_cfg), val, MODE_CFG_MODES) != MODE_ERR_NONE;
}

static int
server_set_PREFIX(struct server *s, char *val)
{
	return mode_cfg(&(s->mode_cfg), val, MODE_CFG_PREFIX) != MODE_ERR_NONE;

M src/components/server.h => src/components/server.h +5 -2
@@ 17,6 17,7 @@ struct server
	const char *username;
	const char *realname;
	const char *nick;
	const char *mode;
	enum casemapping casemapping;
	struct {
		size_t next;


@@ 27,12 28,12 @@ struct server
	struct channel *channel;
	struct channel_list clist;
	struct ircv3_caps ircv3_caps;
	struct ircv3_sasl ircv3_sasl;
	struct mode usermodes;
	struct mode_str mode_str;
	struct mode_cfg mode_cfg;
	struct server *next;
	struct server *prev;
	struct user_list ignore;
	unsigned ping;
	unsigned connected  : 1;
	unsigned quitting   : 1;


@@ 58,7 59,8 @@ struct server* server(
	const char*,  /* port */
	const char*,  /* pass */
	const char*,  /* username */
	const char*); /* realname */
	const char*,  /* realname */
	const char*); /* mode */

struct server* server_list_add(struct server_list*, struct server*);
struct server* server_list_del(struct server_list*, struct server*);


@@ 68,6 70,7 @@ int server_set_chans(struct server*, const char*);
int server_set_nicks(struct server*, const char*);
void server_set_004(struct server*, char*);
void server_set_005(struct server*, char*);
void server_set_sasl(struct server*, const char*, const char*, const char*);

void server_nick_set(struct server*, const char*);
void server_nicks_next(struct server*);

M src/draw.c => src/draw.c +18 -9
@@ 438,8 438,19 @@ print_text:
			draw_attr_bg(text_bg);
			draw_attr_fg(text_fg);

			for (unsigned i = 0; i < (text_p2 - text_p1); i++)
				draw_char(text_p1[i]);
			for (unsigned i = 0; i < (text_p2 - text_p1); i++) {
				switch (text_p1[i]) {
					case 0x02:
					case 0x03:
					case 0x0F:
					case 0x16:
					case 0x1D:
					case 0x1F:
						break;
					default:
						draw_char(text_p1[i]);
				}
			}

			draw_attr_reset();
		}


@@ 677,7 688,11 @@ draw_status(struct channel *c)
	if (c->type == CHANNEL_T_CHANNEL && c->joined) {
		if (!drawf(&cols, STATUS_SEP_HORZ))
			return;
		if (!drawf(&cols, "[+%s %u]", c->chanmodes_str.str, c->users.count))
		if (!drawf(&cols, "[%s%s%s%u]",
				(*(c->chanmodes_str.str) ? "+" : ""),
				(*(c->chanmodes_str.str) ? c->chanmodes_str.str : ""),
				(*(c->chanmodes_str.str) ? " " : ""),
				 c->users.count))
			return;
	}



@@ 739,7 754,6 @@ drawf(unsigned *cols_p, const char *fmt, ...)
	 *  %a -- attribute reset
	 *  %b -- set background colour attribute
	 *  %f -- set foreground colour attribute
	 *  %c -- output char
	 *  %d -- output signed integer
	 *  %u -- output unsigned integer
	 *  %s -- output string


@@ 767,10 781,6 @@ drawf(unsigned *cols_p, const char *fmt, ...)
				case 'f':
					draw_attr_fg(va_arg(arg, int));
					break;
				case 'c':
					draw_char(va_arg(arg, int));
					cols--;
					break;
				case 'd':
					(void) snprintf(buf, sizeof(buf), "%d", va_arg(arg, int));
					cols -= (unsigned) printf("%.*s", cols, buf);


@@ 786,7 796,6 @@ drawf(unsigned *cols_p, const char *fmt, ...)
						} while (UTF8_CONT(*str));
					}
					break;
				case '%':
				default:
					fatal("unknown drawf format character '%c'", c);
			}

M src/handlers/irc_recv.c => src/handlers/irc_recv.c +92 -79
@@ 1,16 1,15 @@
#include "src/handlers/irc_recv.h"

#include "config.h"
#include "src/components/server.h"
#include "src/draw.h"
#include "src/handlers/irc_ctcp.h"
#include "src/handlers/irc_recv.gperf.out"
#include "src/handlers/ircv3.h"
#include "src/draw.h"
#include "src/io.h"
#include "src/state.h"
#include "src/utils/utils.h"

#include "config.h"

#include <ctype.h>
#include <errno.h>
#include <stdlib.h>


@@ 210,6 209,15 @@ static const irc_recv_f irc_numerics[] = {
	[704] = irc_generic_info,   /* RPL_HELPSTART */
	[705] = irc_generic_info,   /* RPL_HELP */
	[706] = irc_generic_ignore, /* RPL_ENDOFHELP */
	[900] = ircv3_numeric_900,  /* IRCv3 RPL_LOGGEDIN */
	[901] = ircv3_numeric_901,  /* IRCv3 RPL_LOGGEDOUT */
	[902] = ircv3_numeric_902,  /* IRCv3 ERR_NICKLOCKED */
	[903] = ircv3_numeric_903,  /* IRCv3 RPL_SASLSUCCESS */
	[904] = ircv3_numeric_904,  /* IRCv3 ERR_SASLFAIL */
	[905] = ircv3_numeric_905,  /* IRCv3 ERR_SASLTOOLONG */
	[906] = ircv3_numeric_906,  /* IRCv3 ERR_SASLABORTED */
	[907] = ircv3_numeric_907,  /* IRCv3 ERR_SASLALREADY */
	[908] = ircv3_numeric_908,  /* IRCv3 RPL_SASLMECHS */
	[1000] = NULL               /* Out of range */
};



@@ 313,16 321,19 @@ irc_numeric_001(struct server *s, struct irc_message *m)

	s->registered = 1;

	if (irc_message_split(m, &params, &trailing))
		server_info(s, "%s", trailing);

	server_info(s, "You are known as %s", s->nick);

	if (s->mode)
		sendf(s, "MODE %s +%s", s->nick, s->mode);

	do {
		if (c->type == CHANNEL_T_CHANNEL && !c->parted)
			sendf(s, "JOIN %s", c->name);
	} while ((c = c->next) != s->channel);

	if (irc_message_split(m, &params, &trailing))
		newlinef(s->channel, 0, FROM_INFO, "%s", trailing);

	server_info(s, "You are known as %s", s->nick);

	return 0;
}



@@ 335,9 346,9 @@ irc_numeric_004(struct server *s, struct irc_message *m)
	const char *trailing;

	if (irc_message_split(m, &params, &trailing))
		newlinef(s->channel, 0, FROM_INFO, "%s ~ %s", params, trailing);
		server_info(s, "%s ~ %s", params, trailing);
	else
		newlinef(s->channel, 0, FROM_INFO, "%s", params);
		server_info(s, "%s", params);

	server_set_004(s, m->params);



@@ 353,9 364,9 @@ irc_numeric_005(struct server *s, struct irc_message *m)
	const char *trailing;

	if (irc_message_split(m, &params, &trailing))
		newlinef(s->channel, 0, FROM_INFO, "%s ~ %s", params, trailing);
		server_info(s, "%s ~ %s", params, trailing);
	else
		newlinef(s->channel, 0, FROM_INFO, "%s ~ are supported by this server", params);
		server_info(s, "%s ~ are supported by this server", params);

	server_set_005(s, m->params);



@@ 514,16 525,15 @@ irc_numeric_333(struct server *s, struct irc_message *m)
static int
irc_numeric_353(struct server *s, struct irc_message *m)
{
	/* 353 <nick> <type> <channel> 1*(<modes><nick>) */
	/* <type> <channel> 1*(<modes><nick>) */

	char *chan;
	char *nick;
	char *nicks;
	char *prfx;
	char *type;
	char *prefix;
	struct channel *c;

	if (!irc_message_param(m, &type))
	if (!irc_message_param(m, &prefix))
		failf(s, "RPL_NAMEREPLY: type is null");

	if (!irc_message_param(m, &chan))


@@ 535,18 545,26 @@ irc_numeric_353(struct server *s, struct irc_message *m)
	if ((c = channel_list_get(&s->clist, chan, s->casemapping)) == NULL)
		failf(s, "RPL_NAMEREPLY: channel '%s' not found", chan);

	if (mode_chanmode_prefix(&(c->chanmodes), &(s->mode_cfg), *type) != MODE_ERR_NONE)
		failf(s, "RPL_NAMEREPLY: invalid channel flag: '%c'", *type);
	if (*prefix != '@' && *prefix != '*' && *prefix != '=')
		failf(s, "RPL_NAMEREPLY: invalid channel type: '%c'", *prefix);

	while ((prfx = nick = irc_strsep(&nicks))) {
	if (*prefix == '@')
		(void) mode_chanmode_set(&(c->chanmodes), &(s->mode_cfg), 's', 1);

	if (*prefix == '*')
		(void) mode_chanmode_set(&(c->chanmodes), &(s->mode_cfg), 'p', 1);

	c->chanmodes.prefix = *prefix;

	while ((prefix = nick = irc_strsep(&nicks))) {

		struct mode m = MODE_EMPTY;

		while (mode_prfxmode_prefix(&m, &(s->mode_cfg), *nick) == MODE_ERR_NONE)
		while (mode_prfxmode_set(&m, &(s->mode_cfg), *nick, 1) == MODE_ERR_NONE)
			nick++;

		if (*nick == 0)
			failf(s, "RPL_NAMEREPLY: invalid nick: '%s'", prfx);
			failf(s, "RPL_NAMEREPLY: invalid nick: '%s'", prefix);

		if (user_list_add(&(c->users), s->casemapping, nick, m) == USER_ERR_DUPLICATE)
			failf(s, "RPL_NAMEREPLY: duplicate nick: '%s'", nick);


@@ 617,11 635,11 @@ irc_numeric_433(struct server *s, struct irc_message *m)
	if (!irc_message_param(m, &nick))
		failf(s, "ERR_NICKNAMEINUSE: nick is null");

	newlinef(s->channel, 0, FROM_ERROR, "Nick '%s' in use", nick);
	server_error(s, "Nick '%s' in use", nick);

	if (!strcmp(nick, s->nick)) {
		server_nicks_next(s);
		newlinef(s->channel, 0, FROM_ERROR, "Trying again with '%s'", s->nick);
		server_error(s, "Trying again with '%s'", s->nick);
		sendf(s, "NICK %s", s->nick);
	}



@@ 695,7 713,7 @@ recv_invite(struct server *s, struct irc_message *m)
		failf(s, "INVITE: channel is null");

	if (!strcmp(nick, s->nick)) {
		newlinef(s->channel, 0, FROM_INFO, "%s invited you to %s", m->from, chan);
		server_info(s, "%s invited you to %s", m->from, chan);
		return 0;
	}



@@ 854,7 872,7 @@ recv_mode(struct server *s, struct irc_message *m)
	struct channel *c;

	if (!irc_message_param(m, &targ))
		failf(s, "NICK: new nick is null");
		failf(s, "MODE: target nick is null");

	if (!strcmp(targ, s->nick))
		return recv_mode_usermodes(m, &(s->mode_cfg), s);


@@ 872,48 890,50 @@ recv_mode_chanmodes(struct irc_message *m, const struct mode_cfg *cfg, struct se
	char *modestring;
	char *modearg;
	enum mode_err mode_err;
	enum mode_set mode_set;
	struct mode *chanmodes = &(c->chanmodes);
	struct user *user;

	// TODO: mode string segfaults if args out of order

	if (!irc_message_param(m, &modestring)) {
		newlinef(c, 0, FROM_ERROR, "MODE: modestring is null");
		return 1;
	}

	do {
		mode_set = MODE_SET_INVALID;
		int set = -1;
		mode_err = MODE_ERR_NONE;

		while ((flag = *modestring++)) {

			if (flag == '-') {
				set = 0;
				continue;
			}

			if (flag == '+') {
				mode_set = MODE_SET_ON;
				set = 1;
				continue;
			}

			if (flag == '-') {
				mode_set = MODE_SET_OFF;
			if (set == -1) {
				newlinef(c, 0, FROM_ERROR, "MODE: missing '+'/'-'");
				continue;
			}

			modearg = NULL;

			switch (chanmode_type(cfg, mode_set, flag)) {
			switch (mode_type(cfg, flag, set)) {

				/* Doesn't consume an argument */
				case MODE_FLAG_CHANMODE:

					mode_err = mode_chanmode_set(chanmodes, cfg, flag, mode_set);
					mode_err = mode_chanmode_set(chanmodes, cfg, flag, set);

					if (mode_err == MODE_ERR_NONE) {
						newlinef(c, 0, FROM_INFO, "%s%s%s mode: %c%c",
								(m->from ? m->from : ""),
								(m->from ? " set " : ""),
								c->name,
								(mode_set == MODE_SET_ON ? '+' : '-'),
								(set ? '+' : '-'),
								flag);
					}
					break;


@@ 926,14 946,14 @@ recv_mode_chanmodes(struct irc_message *m, const struct mode_cfg *cfg, struct se
						continue;
					}

					mode_err = mode_chanmode_set(chanmodes, cfg, flag, mode_set);
					mode_err = mode_chanmode_set(chanmodes, cfg, flag, set);

					if (mode_err == MODE_ERR_NONE) {
						newlinef(c, 0, FROM_INFO, "%s%s%s mode: %c%c %s",
								(m->from ? m->from : ""),
								(m->from ? " set " : ""),
								c->name,
								(mode_set == MODE_SET_ON ? '+' : '-'),
								(set ? '+' : '-'),
								flag,
								modearg);
					}


@@ 952,48 972,30 @@ recv_mode_chanmodes(struct irc_message *m, const struct mode_cfg *cfg, struct se
						continue;
					}

					mode_prfxmode_set(&(user->prfxmodes), cfg, flag, mode_set);
					mode_prfxmode_set(&(user->prfxmodes), cfg, flag, set);

					if (mode_err == MODE_ERR_NONE) {
						newlinef(c, 0, FROM_INFO, "%s%suser %s mode: %c%c",
								(m->from ? m->from : ""),
								(m->from ? " set " : ""),
								modearg,
								(mode_set == MODE_SET_ON ? '+' : '-'),
								(set ? '+' : '-'),
								flag);
					}
					break;

				case MODE_FLAG_INVALID_SET:
					mode_err = MODE_ERR_INVALID_SET;
					break;

				case MODE_FLAG_INVALID_FLAG:
					mode_err = MODE_ERR_INVALID_FLAG;
					newlinef(c, 0, FROM_ERROR, "MODE: invalid flag '%c'", flag);
					break;

				default:
					newlinef(c, 0, FROM_ERROR, "MODE: unhandled error, flag '%c'", flag);
					continue;
			}

			switch (mode_err) {

				case MODE_ERR_INVALID_FLAG:
					newlinef(c, 0, FROM_ERROR, "MODE: invalid flag '%c'", flag);
					break;

				case MODE_ERR_INVALID_SET:
					newlinef(c, 0, FROM_ERROR, "MODE: missing '+'/'-'");
					break;

				default:
					break;
			}
		}
	} while (irc_message_param(m, &modestring));

	mode_str(&(c->chanmodes), &(c->chanmodes_str));
	mode_str(&(c->chanmodes), &(c->chanmodes_str), MODE_STR_CHANMODE);
	draw(DRAW_STATUS);

	return 0;


@@ 1005,45 1007,46 @@ recv_mode_usermodes(struct irc_message *m, const struct mode_cfg *cfg, struct se
	char flag;
	char *modestring;
	enum mode_err mode_err;
	enum mode_set mode_set;
	struct mode *usermodes = &(s->usermodes);

	if (!irc_message_param(m, &modestring))
		failf(s, "MODE: modestring is null");

	do {
		mode_set = MODE_SET_INVALID;
		int set = -1;

		while ((flag = *modestring++)) {

			if (flag == '-') {
				set = 0;
				continue;
			}

			if (flag == '+') {
				mode_set = MODE_SET_ON;
				set = 1;
				continue;
			}

			if (flag == '-') {
				mode_set = MODE_SET_OFF;
			if (set == -1) {
				server_error(s, "MODE: missing '+'/'-'");
				continue;
			}

			mode_err = mode_usermode_set(usermodes, cfg, flag, mode_set);
			mode_err = mode_usermode_set(usermodes, cfg, flag, set);

			if (mode_err == MODE_ERR_NONE)
				newlinef(s->channel, 0, FROM_INFO, "%s%smode: %c%c",
				server_info(s, "%s%smode: %c%c",
						(m->from ? m->from : ""),
						(m->from ? " set " : ""),
						(mode_set == MODE_SET_ON ? '+' : '-'),
						(set ? '+' : '-'),
						flag);

			else if (mode_err == MODE_ERR_INVALID_SET)
				newlinef(s->channel, 0, FROM_ERROR, "MODE: missing '+'/'-'");

			else if (mode_err == MODE_ERR_INVALID_FLAG)
				newlinef(s->channel, 0, FROM_ERROR, "MODE: invalid flag '%c'", flag);
				server_error(s, "MODE: invalid flag '%c'", flag);
		}
	} while (irc_message_param(m, &modestring));

	mode_str(usermodes, &(s->mode_str));
	mode_str(usermodes, &(s->mode_str), MODE_STR_USERMODE);
	draw(DRAW_STATUS);

	return 0;


@@ 1100,9 1103,6 @@ recv_notice(struct server *s, struct irc_message *m)
	if (!irc_message_param(m, &message))
		failf(s, "NOTICE: message is null");

	if (user_list_get(&(s->ignore), s->casemapping, m->from, 0))
		return 0;

	if (IS_CTCP(message))
		return ctcp_response(s, m->from, target, message);



@@ 1210,9 1210,6 @@ recv_privmsg(struct server *s, struct irc_message *m)
	if (!irc_message_param(m, &message))
		failf(s, "PRIVMSG: message is null");

	if (user_list_get(&(s->ignore), s->casemapping, m->from, 0))
		return 0;

	if (IS_CTCP(message))
		return ctcp_request(s, m->from, target, message);



@@ 1317,7 1314,23 @@ recv_topic(struct server *s, struct irc_message *m)
static int
recv_ircv3_cap(struct server *s, struct irc_message *m)
{
	return ircv3_recv_CAP(s, m);
	int ret;

	if ((ret = ircv3_recv_CAP(s, m)) && !s->registered)
		io_dx(s->connection);

	return ret;
}

static int
recv_ircv3_authenticate(struct server *s, struct irc_message *m)
{
	int ret;

	if ((ret = ircv3_recv_AUTHENTICATE(s, m)) && !s->registered)
		io_dx(s->connection);

	return ret;
}

static int

M src/handlers/irc_recv.gperf => src/handlers/irc_recv.gperf +20 -18
@@ 15,9 15,10 @@
	X(privmsg) \
	X(quit) \
	X(topic) \
	X(ircv3_cap) \
	X(ircv3_account) \
	X(ircv3_authenticate) \
	X(ircv3_away) \
	X(ircv3_cap) \
	X(ircv3_chghost)

#define X(cmd) static int recv_##cmd(struct server*, struct irc_message*);


@@ 44,21 45,22 @@ struct recv_handler
%define initializer-suffix ,(irc_recv_f)0
struct recv_handler;
%%
ERROR,   recv_error
INVITE,  recv_invite
JOIN,    recv_join
KICK,    recv_kick
MODE,    recv_mode
NICK,    recv_nick
NOTICE,  recv_notice
PART,    recv_part
PING,    recv_ping
PONG,    recv_pong
PRIVMSG, recv_privmsg
QUIT,    recv_quit
TOPIC,   recv_topic
CAP,     recv_ircv3_cap
ACCOUNT, recv_ircv3_account
AWAY,    recv_ircv3_away
CHGHOST, recv_ircv3_chghost
ERROR,        recv_error
INVITE,       recv_invite
JOIN,         recv_join
KICK,         recv_kick
MODE,         recv_mode
NICK,         recv_nick
NOTICE,       recv_notice
PART,         recv_part
PING,         recv_ping
PONG,         recv_pong
PRIVMSG,      recv_privmsg
QUIT,         recv_quit
TOPIC,        recv_topic
ACCOUNT,      recv_ircv3_account
AUTHENTICATE, recv_ircv3_authenticate
AWAY,         recv_ircv3_away
CAP,          recv_ircv3_cap
CHGHOST,      recv_ircv3_chghost
%%

M src/handlers/ircv3.c => src/handlers/ircv3.c +366 -13
@@ 3,11 3,13 @@
#include "src/io.h"
#include "src/state.h"

#include "mbedtls/base64.h"

#include <string.h>

#define failf(S, ...) \
	do { server_error((S), __VA_ARGS__); \
	     return 1; \
	     return -1; \
	} while (0)

#define sendf(S, ...) \


@@ 31,6 33,28 @@ IRCV3_RECV_HANDLERS

static int ircv3_cap_req_count(struct ircv3_caps*);
static int ircv3_cap_req_send(struct ircv3_caps*, struct server*);
static int ircv3_cap_end(struct server*);
static int ircv3_sasl_init(struct server*);

static int ircv3_recv_AUTHENTICATE_EXTERNAL(struct server*, struct irc_message*);
static int ircv3_recv_AUTHENTICATE_PLAIN(struct server*, struct irc_message*);

int
ircv3_recv_AUTHENTICATE(struct server *s, struct irc_message *m)
{
	if (s->ircv3_sasl.mech == IRCV3_SASL_MECH_NONE)
		failf(s, "AUTHENTICATE: no SASL mechanism");

	if (s->ircv3_sasl.mech == IRCV3_SASL_MECH_EXTERNAL)
		return (ircv3_recv_AUTHENTICATE_EXTERNAL(s, m));

	if (s->ircv3_sasl.mech == IRCV3_SASL_MECH_PLAIN)
		return (ircv3_recv_AUTHENTICATE_PLAIN(s, m));

	fatal("unknown SASL authentication mechanism");

	return 0;
}

int
ircv3_recv_CAP(struct server *s, struct irc_message *m)


@@ 53,6 77,202 @@ ircv3_recv_CAP(struct server *s, struct irc_message *m)
	failf(s, "CAP: unrecognized subcommand '%s'", cmnd);
}

int
ircv3_numeric_900(struct server *s, struct irc_message *m)
{
	/* <nick>!<ident>@<host> <account> :You are now logged in as <user> */

	char *account;
	char *message;
	char *nick;

	if (!irc_message_param(m, &nick))
		failf(s, "RPL_LOGGEDIN: missing nick");

	if (!irc_message_param(m, &account))
		failf(s, "RPL_LOGGEDIN: missing account");

	irc_message_param(m, &message);

	if (message && *message)
		server_info(s, "SASL success: %s", message);
	else
		server_info(s, "SASL success: you are logged in as %s", account);

	return 0;
}

int
ircv3_numeric_901(struct server *s, struct irc_message *m)
{
	/* <nick>!<ident>@<host> :You are now logged out */

	char *message;
	char *nick;

	if (!irc_message_param(m, &nick))
		failf(s, "RPL_LOGGEDOUT: missing nick");

	irc_message_param(m, &message);

	if (message && *message)
		server_info(s, "%s", message);
	else
		server_info(s, "You are now logged out");

	return 0;
}

int
ircv3_numeric_902(struct server *s, struct irc_message *m)
{
	/* :You must use a nick assigned to you */

	char *message;

	irc_message_param(m, &message);

	if (message && *message)
		server_error(s, "%s", message);
	else
		server_error(s, "You must use a nick assigned to you");

	s->ircv3_sasl.state = IRCV3_SASL_STATE_NONE;

	if (!s->registered)
		io_dx(s->connection);

	return 0;
}

int
ircv3_numeric_903(struct server *s, struct irc_message *m)
{
	/* :SASL authentication successful */

	char *message;

	irc_message_param(m, &message);

	if (message && *message)
		server_info(s, "%s", message);
	else
		server_info(s, "SASL authentication successful");

	s->ircv3_sasl.state = IRCV3_SASL_STATE_AUTHENTICATED;

	return ircv3_cap_end(s);
}

int
ircv3_numeric_904(struct server *s, struct irc_message *m)
{
	/* :SASL authentication failed */

	char *message;

	irc_message_param(m, &message);

	if (message && *message)
		server_error(s, "%s", message);
	else
		server_error(s, "SASL authentication failed");

	s->ircv3_sasl.state = IRCV3_SASL_STATE_NONE;

	if (!s->registered)
		io_dx(s->connection);

	return 0;
}

int
ircv3_numeric_905(struct server *s, struct irc_message *m)
{
	/* :SASL message too long */

	char *message;

	irc_message_param(m, &message);

	if (message && *message)
		server_error(s, "%s", message);
	else
		server_error(s, "SASL message too long");

	s->ircv3_sasl.state = IRCV3_SASL_STATE_NONE;

	if (!s->registered)
		io_dx(s->connection);

	return 0;
}

int
ircv3_numeric_906(struct server *s, struct irc_message *m)
{
	/* :SASL authentication aborted */

	char *message;

	irc_message_param(m, &message);

	if (message && *message)
		server_error(s, "%s", message);
	else
		server_error(s, "SASL authentication aborted");

	s->ircv3_sasl.state = IRCV3_SASL_STATE_NONE;

	if (!s->registered)
		io_dx(s->connection);

	return 0;
}

int
ircv3_numeric_907(struct server *s, struct irc_message *m)
{
	/* :You have already authenticated using SASL */

	char *message;

	irc_message_param(m, &message);

	if (message && *message)
		server_error(s, "%s", message);
	else
		server_error(s, "You have already authenticated using SASL");

	s->ircv3_sasl.state = IRCV3_SASL_STATE_NONE;

	if (!s->registered)
		io_dx(s->connection);

	return 0;
}

int
ircv3_numeric_908(struct server *s, struct irc_message *m)
{
	/* <mechanisms> :are available SASL mechanisms */

	char *mechanisms;
	char *message;

	if (!irc_message_param(m, &mechanisms))
		failf(s, "RPL_SASLMECHS: missing mechanisms");

	irc_message_param(m, &message);

	if (message && *message)
		server_info(s, "%s %s", mechanisms, message);
	else
		server_info(s, "%s are available SASL mechanisms", mechanisms);

	return 0;
}

static int
ircv3_recv_cap_LS(struct server *s, struct irc_message *m)
{


@@ 68,7 288,8 @@ ircv3_recv_cap_LS(struct server *s, struct irc_message *m)
	 * CAP <targ> LS [*] :[<cap_1> [...]]
	 */

	char *cap;
	char *cap_key;
	char *cap_val;
	char *caps;
	char *multiline;



@@ 94,14 315,18 @@ ircv3_recv_cap_LS(struct server *s, struct irc_message *m)
		return 0;
	}

	while ((cap = irc_strsep(&(caps)))) {
	while ((cap_key = irc_strsep(&(caps)))) {

		struct ircv3_cap *c;

		if (!(c = ircv3_cap_get(&(s->ircv3_caps), cap)))
		if ((cap_val = strchr(cap_key, '=')))
			*cap_val++ = 0;

		if (!(c = ircv3_cap_get(&(s->ircv3_caps), cap_key)))
			continue;

		c->supported = 1;
		c->val = (cap_val ? strdup(cap_val) : NULL);

		if (c->req_auto)
			c->req = 1;


@@ 217,17 442,18 @@ ircv3_recv_cap_ACK(struct server *s, struct irc_message *m)
		c->req = 0;
		c->set = !unset;

		server_info(s, "capability change accepted: %s%s", (unset ? "-" : ""), cap);
		server_info(s, "capability change accepted: %s%s%s%s",
			(unset ? "-" : ""), cap, (c->val ? "=" : ""), (c->val ? c->val : ""));

		if (!strcmp(cap, "sasl"))
			ircv3_sasl_init(s);

	} while ((cap = irc_strsep(&(caps))));

	if (errors)
		failf(s, "CAP ACK: parameter errors");

	if (!s->registered && !ircv3_cap_req_count(&(s->ircv3_caps)))
		sendf(s, "CAP END");

	return 0;
	return ircv3_cap_end(s);
}

static int


@@ 258,10 484,7 @@ ircv3_recv_cap_NAK(struct server *s, struct irc_message *m)

	} while ((cap = irc_strsep(&(caps))));

	if (!s->registered && !ircv3_cap_req_count(&(s->ircv3_caps)))
		sendf(s, "CAP END");

	return 0;
	return ircv3_cap_end(s);
}

static int


@@ 350,6 573,75 @@ ircv3_recv_cap_NEW(struct server *s, struct irc_message *m)
}

static int
ircv3_recv_AUTHENTICATE_EXTERNAL(struct server *s, struct irc_message *m)
{
	/* C: AUTHENTICATE EXTERNAL
	 * S: AUTHENTICATE +
	 * C: AUTHENTICATE +
	 */

	char *resp;

	if (s->ircv3_sasl.state != IRCV3_SASL_STATE_REQ_MECH)
		failf(s, "Invalid SASL state for mechanism EXTERNAL: %d", s->ircv3_sasl.state);

	if (!irc_message_param(m, &resp))
		failf(s, "Invalid SASL response for mechanism EXTERNAL: response is null");

	if (strcmp(resp, "+"))
		failf(s, "Invalid SASL response for mechanism EXTERNAL: '%s'", resp);

	sendf(s, "AUTHENTICATE +");

	return 0;
}

static int
ircv3_recv_AUTHENTICATE_PLAIN(struct server *s, struct irc_message *m)
{
	/* C: AUTHENTICATE PLAIN
	 * S: AUTHENTICATE +
	 * C: AUTHENTICATE base64(<authzid><null><authcid><null><passwd>)
	 */

	/* (((4 * 300 / 3) + 3) & ~3) */
	char *resp;
	unsigned char resp_dec[300];
	unsigned char resp_enc[400];
	size_t len;

	if (!s->ircv3_sasl.user || !*s->ircv3_sasl.user)
		failf(s, "SASL mechanism PLAIN requires a username");

	if (!s->ircv3_sasl.pass || !*s->ircv3_sasl.pass)
		failf(s, "SASL mechanism PLAIN requires a password");

	if (s->ircv3_sasl.state != IRCV3_SASL_STATE_REQ_MECH)
		failf(s, "Invalid SASL state for mechanism PLAIN: %d", s->ircv3_sasl.state);

	if (!irc_message_param(m, &resp))
		failf(s, "Invalid SASL response for mechanism PLAIN: response is null");

	if (strcmp(resp, "+"))
		failf(s, "Invalid SASL response for mechanism PLAIN: '%s'", resp);

	len = snprintf((char *)resp_dec, sizeof(resp_dec), "%s%c%s%c%s",
		s->ircv3_sasl.user, 0,
		s->ircv3_sasl.user, 0,
		s->ircv3_sasl.pass);

	if (len >= sizeof(resp_dec))
		failf(s, "SASL decoded auth message too long");

	if (mbedtls_base64_encode(resp_enc, sizeof(resp_enc), &len, resp_dec, len))
		failf(s, "SASL encoded auth message too long");

	sendf(s, "AUTHENTICATE %s", resp_enc);

	return 0;
}

static int
ircv3_cap_req_count(struct ircv3_caps *caps)
{
	#define X(CAP, VAR, ATTRS) + !!caps->VAR.req


@@ 382,3 674,64 @@ ircv3_cap_req_send(struct ircv3_caps *caps, struct server *s)

	return 0;
}

static int
ircv3_cap_end(struct server *s)
{
	/* Previously registered or doesn't support IRCv3 */
	if (s->registered)
		return 0;

	/* IRCv3 CAP negotiation in progress */
	if (ircv3_cap_req_count(&(s->ircv3_caps)))
		return 0;

	/* IRCv3 SASL authentication in progress */
	if (s->ircv3_sasl.state != IRCV3_SASL_STATE_NONE
	 && s->ircv3_sasl.state != IRCV3_SASL_STATE_AUTHENTICATED)
		return 0;

	sendf(s, "CAP END");
	
	return 0;
}

static int
ircv3_sasl_init(struct server *s)
{
	if (s->ircv3_sasl.mech == IRCV3_SASL_MECH_NONE)
		return 0;

	switch (s->ircv3_sasl.state) {

		/* Start authentication process */
		case IRCV3_SASL_STATE_NONE:
			break;

		/* Authentication in progress */
		case IRCV3_SASL_STATE_REQ_MECH:
			return 0;

		/* Previously authenticated */
		case IRCV3_SASL_STATE_AUTHENTICATED:
			return 0;

		default:
			fatal("unknown SASL state");
	}

	switch (s->ircv3_sasl.mech) {
		case IRCV3_SASL_MECH_EXTERNAL:
			sendf(s, "AUTHENTICATE EXTERNAL");
			break;
		case IRCV3_SASL_MECH_PLAIN:
			sendf(s, "AUTHENTICATE PLAIN");
			break;
		default:
			fatal("unknown SASL authentication mechanism");
	}

	s->ircv3_sasl.state = IRCV3_SASL_STATE_REQ_MECH;

	return 0;
}

M src/handlers/ircv3.h => src/handlers/ircv3.h +11 -0
@@ 4,6 4,17 @@
#include "src/components/server.h"
#include "src/utils/utils.h"

int ircv3_recv_AUTHENTICATE(struct server*, struct irc_message*);
int ircv3_recv_CAP(struct server*, struct irc_message*);

int ircv3_numeric_900(struct server*, struct irc_message*);
int ircv3_numeric_901(struct server*, struct irc_message*);
int ircv3_numeric_902(struct server*, struct irc_message*);
int ircv3_numeric_903(struct server*, struct irc_message*);
int ircv3_numeric_904(struct server*, struct irc_message*);
int ircv3_numeric_905(struct server*, struct irc_message*);
int ircv3_numeric_906(struct server*, struct irc_message*);
int ircv3_numeric_907(struct server*, struct irc_message*);
int ircv3_numeric_908(struct server*, struct irc_message*);

#endif

M src/io.c => src/io.c +147 -69
@@ 105,6 105,9 @@ struct connection
	const void *obj;
	const char *host;
	const char *port;
	const char *tls_ca_file;
	const char *tls_ca_path;
	const char *tls_cert;
	enum io_state {
		IO_ST_INVALID,
		IO_ST_DXED, /* Socket disconnected, passive */


@@ 114,9 117,14 @@ struct connection
		IO_ST_PING, /* Socket connected, network state in question */
	} st_cur, /* current thread state */
	  st_new; /* new thread state */
	mbedtls_ctr_drbg_context tls_ctr_drbg;
	mbedtls_entropy_context tls_entropy;
	mbedtls_net_context net_ctx;
	mbedtls_ssl_config  tls_conf;
	mbedtls_pk_context tls_pk_ctx;
	mbedtls_ssl_config tls_conf;
	mbedtls_ssl_context tls_ctx;
	mbedtls_x509_crt tls_x509_crt_ca;
	mbedtls_x509_crt tls_x509_crt_client;
	pthread_mutex_t mtx;
	pthread_t tid;
	uint32_t flags;


@@ 138,9 146,6 @@ static void io_tty_winsize(void);
static void* io_thread(void*);

static int io_running;
static mbedtls_ctr_drbg_context tls_ctr_drbg;
static mbedtls_entropy_context  tls_entropy;
static mbedtls_x509_crt         tls_x509_crt;
static pthread_mutex_t io_cb_mutex = PTHREAD_MUTEX_INITIALIZER;
static struct termios term;
static volatile sig_atomic_t flag_sigwinch_cb; /* sigwinch callback */


@@ 153,10 158,8 @@ static void io_net_close(int);
static const char* io_tls_err(int);
static int io_tls_establish(struct connection*);
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[] = {
const char *default_ca_certs[] = {
	"/etc/ssl/ca-bundle.pem",
	"/etc/ssl/cert.pem",
	"/etc/ssl/certs/ca-certificates.crt",


@@ 166,7 169,14 @@ const char *ca_cert_paths[] = {
};

struct connection*
connection(const void *obj, const char *host, const char *port, uint32_t flags)
connection(
	const void *obj,
	const char *host,
	const char *port,
	const char *tls_ca_file,
	const char *tls_ca_path,
	const char *tls_cert,
	uint32_t flags)
{
	struct connection *cx;



@@ 177,6 187,9 @@ connection(const void *obj, const char *host, const char *port, uint32_t flags)
	cx->flags = flags;
	cx->host = strdup(host);
	cx->port = strdup(port);
	cx->tls_ca_file = (tls_ca_file ? strdup(tls_ca_file) : NULL);
	cx->tls_ca_path = (tls_ca_path ? strdup(tls_ca_path) : NULL);
	cx->tls_cert = (tls_cert ? strdup(tls_cert) : NULL);
	cx->st_cur = IO_ST_DXED;
	cx->st_new = IO_ST_INVALID;
	PT_CF(pthread_mutex_init(&(cx->mtx), NULL));


@@ 190,6 203,9 @@ connection_free(struct connection *cx)
	PT_CF(pthread_mutex_destroy(&(cx->mtx)));
	free((void*)cx->host);
	free((void*)cx->port);
	free((void*)cx->tls_ca_file);
	free((void*)cx->tls_ca_path);
	free((void*)cx->tls_cert);
	free(cx);
}



@@ 309,7 325,6 @@ io_init(void)
{
	io_sig_init();
	io_tty_init();
	io_tls_init();
}

void


@@ 411,8 426,17 @@ io_state_cxed(struct connection *cx)
{
	int ret;

	while ((ret = io_cx_read(cx, SEC_IN_MS(IO_PING_MIN))) > 0)
		continue;
	do {
		enum io_state st;

		PT_LK(&(cx->mtx));
		st = cx->st_new;
		PT_UL(&(cx->mtx));

		if (st != IO_ST_INVALID)
			return st;

	} while ((ret = io_cx_read(cx, SEC_IN_MS(IO_PING_MIN))) > 0);

	if (ret == MBEDTLS_ERR_SSL_TIMEOUT)
		return IO_ST_PING;


@@ 421,22 445,28 @@ io_state_cxed(struct connection *cx)
		case MBEDTLS_ERR_SSL_WANT_READ:
			break;
		case MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY:
			io_info(cx, "connection closed gracefully");
			io_info(cx, "Connection closed gracefully");
			break;
		case MBEDTLS_ERR_NET_CONN_RESET:
		case 0:
			io_error(cx, "connection reset by peer");
			io_error(cx, "Connection reset by peer");
			break;
		default:
			io_error(cx, "connection error");
			io_error(cx, "Connection error");
			break;
	}

	mbedtls_net_free(&(cx->net_ctx));

	if (cx->flags & IO_TLS_ENABLED) {
		mbedtls_ctr_drbg_free(&(cx->tls_ctr_drbg));
		mbedtls_entropy_free(&(cx->tls_entropy));
		mbedtls_pk_free(&(cx->tls_pk_ctx));
		mbedtls_ssl_config_free(&(cx->tls_conf));
		mbedtls_ssl_free(&(cx->tls_ctx));
		mbedtls_x509_crt_free(&(cx->tls_x509_crt_ca));
		mbedtls_x509_crt_free(&(cx->tls_x509_crt_client));

	}

	return IO_ST_CXNG;


@@ 460,22 490,27 @@ io_state_ping(struct connection *cx)
		case MBEDTLS_ERR_SSL_WANT_READ:
			break;
		case MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY:
			io_info(cx, "connection closed gracefully");
			io_info(cx, "Connection closed gracefully");
			break;
		case MBEDTLS_ERR_NET_CONN_RESET:
		case 0:
			io_error(cx, "connection reset by peer");
			io_error(cx, "Connection reset by peer");
			break;
		default:
			io_error(cx, "connection error");
			io_error(cx, "Connection error");
			break;
	}

	mbedtls_net_free(&(cx->net_ctx));

	if (cx->flags & IO_TLS_ENABLED) {
		mbedtls_ctr_drbg_free(&(cx->tls_ctr_drbg));
		mbedtls_entropy_free(&(cx->tls_entropy));
		mbedtls_pk_free(&(cx->tls_pk_ctx));
		mbedtls_ssl_config_free(&(cx->tls_conf));
		mbedtls_ssl_free(&(cx->tls_ctx));
		mbedtls_x509_crt_free(&(cx->tls_x509_crt_ca));
		mbedtls_x509_crt_free(&(cx->tls_x509_crt_client));
	}

	return IO_ST_CXNG;


@@ 776,12 811,19 @@ io_strerror(char *buf, size_t buflen)
static int
io_tls_establish(struct connection *cx)
{
	const unsigned char pers[] = "rirc-drbg-seed";

	int ret;

	io_info(cx, " .. Establishing TLS connection");

	mbedtls_ctr_drbg_init(&(cx->tls_ctr_drbg));
	mbedtls_entropy_init(&(cx->tls_entropy));
	mbedtls_pk_init(&(cx->tls_pk_ctx));
	mbedtls_ssl_init(&(cx->tls_ctx));
	mbedtls_ssl_config_init(&(cx->tls_conf));
	mbedtls_x509_crt_init(&(cx->tls_x509_crt_ca));
	mbedtls_x509_crt_init(&(cx->tls_x509_crt_client));

	if ((ret = mbedtls_ssl_config_defaults(
			&(cx->tls_conf),


@@ 802,12 844,91 @@ io_tls_establish(struct connection *cx)
			MBEDTLS_SSL_MAJOR_VERSION_3,
			MBEDTLS_SSL_MINOR_VERSION_3);

	mbedtls_ssl_conf_rng(&(cx->tls_conf), mbedtls_ctr_drbg_random, &tls_ctr_drbg);
	if ((ret = mbedtls_ctr_drbg_seed(
			&(cx->tls_ctr_drbg),
			mbedtls_entropy_func,
			&(cx->tls_entropy),
			pers,
			sizeof(pers)))) {
		io_error(cx, " .. %s ", io_tls_err(ret));
		goto err;
	}

	ret = -1;

	if (ret < 0 && cx->tls_ca_file) {
		if ((ret = mbedtls_x509_crt_parse_file(&(cx->tls_x509_crt_ca), cx->tls_ca_file)) < 0) {
			io_error(cx, " .. Failed to load CA cert file: '%s': %s", cx->tls_ca_file, io_tls_err(ret));
			goto err;
		}
	}

	if (ret < 0 && cx->tls_ca_path) {
		if ((ret = mbedtls_x509_crt_parse_path(&(cx->tls_x509_crt_ca), cx->tls_ca_path)) < 0) {
			io_error(cx, " .. Failed to load CA cert path: '%s': %s", cx->tls_ca_path, io_tls_err(ret));
			goto err;
		}
	}

	if (ret < 0 && default_ca_file && *default_ca_file) {
		if ((ret = mbedtls_x509_crt_parse_file(&(cx->tls_x509_crt_ca), default_ca_file)) < 0) {
			io_error(cx, " .. Failed to load CA cert file: '%s': %s", default_ca_file, io_tls_err(ret));
			goto err;
		}
	}

	if (ret < 0 && default_ca_path && *default_ca_path) {
		if ((ret = mbedtls_x509_crt_parse_path(&(cx->tls_x509_crt_ca), default_ca_path)) < 0) {
			io_error(cx, " .. Failed to load CA cert path: '%s': %s", default_ca_path, io_tls_err(ret));
			goto err;
		}
	}

	if (ret < 0) {

		size_t i;

		for (i = 0; i < ARR_LEN(default_ca_certs); i++) {
			if ((ret = mbedtls_x509_crt_parse_file(&(cx->tls_x509_crt_ca), default_ca_certs[i])) >= 0)
				break;
		}

		if (i == ARR_LEN(default_ca_certs)) {
			io_error(cx, " .. Failed to load default CA certs: %s", io_tls_err(ret));
			goto err;
		}
	}

	if (cx->tls_cert) {

		if ((ret = mbedtls_x509_crt_parse_file(&(cx->tls_x509_crt_client), cx->tls_cert)) < 0) {
			io_error(cx, " .. Failed to load client cert: '%s': %s", cx->tls_cert, io_tls_err(ret));
			goto err;
		}

		if ((ret = mbedtls_pk_parse_keyfile(
				&(cx->tls_pk_ctx),
				cx->tls_cert,
				NULL,
				mbedtls_ctr_drbg_random,
				&(cx->tls_ctr_drbg))))
		{
			io_error(cx, " .. Failed to load client cert key: '%s': %s", cx->tls_cert, io_tls_err(ret));
			goto err;
		}

		if ((ret = mbedtls_ssl_conf_own_cert(&(cx->tls_conf), &(cx->tls_x509_crt_client), &(cx->tls_pk_ctx)))) {
			io_error(cx, " .. Failed to configure client cert: '%s': %s", cx->tls_cert, io_tls_err(ret));
			goto err;
		}
	}

	mbedtls_ssl_conf_rng(&(cx->tls_conf), mbedtls_ctr_drbg_random, &(cx->tls_ctr_drbg));

	if (cx->flags & IO_TLS_VRFY_DISABLED) {
		mbedtls_ssl_conf_authmode(&(cx->tls_conf), MBEDTLS_SSL_VERIFY_NONE);
	} else {
		mbedtls_ssl_conf_ca_chain(&(cx->tls_conf), &tls_x509_crt, NULL);
		mbedtls_ssl_conf_ca_chain(&(cx->tls_conf), &(cx->tls_x509_crt_ca), NULL);

		if (cx->flags & IO_TLS_VRFY_OPTIONAL)
			mbedtls_ssl_conf_authmode(&(cx->tls_conf), MBEDTLS_SSL_VERIFY_OPTIONAL);


@@ 858,8 979,8 @@ io_tls_establish(struct connection *cx)
	}

	io_info(cx, " .. TLS connection established");
	io_info(cx, " ..   - version:     %s", mbedtls_ssl_get_version(&(cx->tls_ctx)));
	io_info(cx, " ..   - ciphersuite: %s", mbedtls_ssl_get_ciphersuite(&(cx->tls_ctx)));
	io_info(cx, " .... Version:     %s", mbedtls_ssl_get_version(&(cx->tls_ctx)));
	io_info(cx, " .... Ciphersuite: %s", mbedtls_ssl_get_ciphersuite(&(cx->tls_ctx)));

	return 0;



@@ 867,8 988,13 @@ err:

	io_error(cx, " .. TLS connection failure");

	mbedtls_ctr_drbg_free(&(cx->tls_ctr_drbg));
	mbedtls_entropy_free(&(cx->tls_entropy));
	mbedtls_pk_free(&(cx->tls_pk_ctx));
	mbedtls_ssl_config_free(&(cx->tls_conf));
	mbedtls_ssl_free(&(cx->tls_ctx));
	mbedtls_x509_crt_free(&(cx->tls_x509_crt_ca));
	mbedtls_x509_crt_free(&(cx->tls_x509_crt_client));
	mbedtls_net_free(&(cx->net_ctx));

	return -1;


@@ 914,51 1040,3 @@ io_tls_err(int err)

	return "Unknown error";
}

static void
io_tls_init(void)
{
	const unsigned char pers[] = "rirc-drbg-seed";
	int ret;

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

	if (atexit(io_tls_term))
		fatal("atexit");

	if ((ret = mbedtls_ctr_drbg_seed(
			&tls_ctr_drbg,
			mbedtls_entropy_func,
			&tls_entropy,
			pers,
			sizeof(pers)))) {
		fatal("mbedtls_ctr_drbg_seed: %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
io_tls_term(void)
{
	/* Exit handler, must return normally */

	mbedtls_ctr_drbg_free(&tls_ctr_drbg);
	mbedtls_entropy_free(&tls_entropy);
	mbedtls_x509_crt_free(&tls_x509_crt);
}

M src/io.h => src/io.h +3 -0
@@ 89,6 89,9 @@ struct connection* connection(
	const void*, /* callback object */
	const char*, /* host */
	const char*, /* port */
	const char*, /* tls_ca_file */
	const char*, /* tls_ca_path */
	const char*, /* tls_cert */
	uint32_t);   /* flags */

void connection_free(struct connection*);

M src/rirc.c => src/rirc.c +156 -64
@@ 11,6 11,7 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <unistd.h>

#define MAX_CLI_SERVERS 64


@@ 25,10 26,16 @@ static const char* rirc_opt_str(char);
static const char* rirc_pw_name(void);
static int rirc_parse_args(int, char**);

#ifdef CA_CERT_FILE
const char *default_ca_file = CA_CERT_FILE;
#else
const char *default_ca_file;
#endif

#ifdef CA_CERT_PATH
const char *ca_cert_path = CA_CERT_PATH;
const char *default_ca_path = CA_CERT_PATH;
#else
const char *ca_cert_path;
const char *default_ca_path;
#endif

#ifdef DEFAULT_NICKS


@@ 59,26 66,31 @@ static const char *const rirc_help =
"\nrirc v"VERSION" ~ Richard C. Robbins <mail@rcr.io>"
"\n"
"\nUsage:"
"\n  rirc [-hv] [-s server [...]]"
"\n  rirc [-hv] [-s host [options] ...]"
"\n"
"\nInfo:"
"\n  -h, --help      Print help message and exit"
"\n  -v, --version   Print rirc version and exit"
"\n"
"\nServer options:"
"\n  -s, --server=SERVER       Connect to SERVER"
"\n  -p, --port=PORT           Connect to SERVER using PORT"
"\n  -w, --pass=PASS           Connect to SERVER using PASS"
"\n  -u, --username=USERNAME   Connect to SERVER using USERNAME"
"\n  -r, --realname=REALNAME   Connect to SERVER using REALNAME"
"\n  -n, --nicks=NICKS         Comma separated list of nicks to use for SERVER"
"\n  -c, --chans=CHANNELS      Comma separated list of channels to join for SERVER"
"\n"
"\nServer connection options:"
"\n   --ipv4                   Connect to server using only ipv4 addresses"
"\n   --ipv6                   Connect to server using only ipv6 addresses"
"\n   --tls-disable            Set server TLS disabled"
"\n   --tls-verify=<mode>      Set server TLS peer certificate verification mode"
"\nOptions:"
"\n  -s, --server=HOST         Set connection hostname"
"\n  -p, --port=PORT           Set connection port"
"\n  -w, --pass=PASS           Set IRC password"
"\n  -u, --username=USERNAME   Set IRC username"
"\n  -r, --realname=REALNAME   Set IRC realname"
"\n  -m, --mode=MODE           Set IRC user modes"
"\n  -n, --nicks=NICKS         Set comma separated list of nicks to use"
"\n  -c, --chans=CHANNELS      Set comma separated list of channels to join"
"\n      --tls-cert=PATH       Set TLS client certificate file path"
"\n      --tls-ca-file=PATH    Set TLS peer certificate file path"
"\n      --tls-ca-path=PATH    Set TLS peer certificate directory path"
"\n      --tls-verify=MODE     Set TLS peer certificate verification mode"
"\n      --tls-disable         Set TLS disabled"
"\n      --sasl=MECHANISM      Authenticate with SASL mechanism"
"\n      --sasl-user=USER      Authenticate with SASL username"
"\n      --sasl-pass=PASS      Authenticate with SASL password"
"\n      --ipv4                Use IPv4 addresses only"
"\n      --ipv6                Use IPv6 addresses only"
"\n";

static const char *const rirc_version =


@@ 95,14 107,21 @@ rirc_opt_str(char c)
		case 's': return "-s/--server";
		case 'p': return "-p/--port";
		case 'w': return "-w/--pass";
		case 'n': return "-n/--nicks";
		case 'c': return "-c/--chans";
		case 'u': return "-u/--username";
		case 'r': return "-r/--realname";
		case '4': return "--ipv4";
		case '6': return "--ipv6";
		case 'x': return "--tls-disable";
		case 'y': return "--tls-verify";
		case 'm': return "-m/--mode";
		case 'n': return "-n/--nicks";
		case 'c': return "-c/--chans";
		case '0': return "--tls-cert";
		case '1': return "--tls-ca-file";
		case '2': return "--tls-ca-path";
		case '3': return "--tls-verify";
		case '4': return "--tls-disable";
		case '5': return "--sasl";
		case '6': return "--sasl-user";
		case '7': return "--sasl-pass";
		case '8': return "--ipv4";
		case '9': return "--ipv6";
		default:
			fatal("unknown option flag '%c'", c);
	}


@@ 136,8 155,15 @@ rirc_parse_args(int argc, char **argv)
		const char *pass;
		const char *username;
		const char *realname;
		const char *mode;
		const char *nicks;
		const char *chans;
		const char *tls_ca_file;
		const char *tls_ca_path;
		const char *tls_cert;
		const char *sasl;
		const char *sasl_user;
		const char *sasl_pass;
		int ipv;
		int tls;
		int tls_vrfy;


@@ 150,20 176,27 @@ rirc_parse_args(int argc, char **argv)
		{"pass",        required_argument, 0, 'w'},
		{"username",    required_argument, 0, 'u'},
		{"realname",    required_argument, 0, 'r'},
		{"mode",        required_argument, 0, 'm'},
		{"nicks",       required_argument, 0, 'n'},
		{"chans",       required_argument, 0, 'c'},
		{"help",        no_argument,       0, 'h'},
		{"version",     no_argument,       0, 'v'},
		{"ipv4",        no_argument,       0, '4'},
		{"ipv6",        no_argument,       0, '6'},
		{"tls-disable", no_argument,       0, 'x'},
		{"tls-verify",  required_argument, 0, 'y'},
		{"tls-cert",    required_argument, 0, '0'},
		{"tls-ca-file", required_argument, 0, '1'},
		{"tls-ca-path", required_argument, 0, '2'},
		{"tls-verify",  required_argument, 0, '3'},
		{"tls-disable", no_argument,       0, '4'},
		{"sasl",        required_argument, 0, '5'},
		{"sasl-user",   required_argument, 0, '6'},
		{"sasl-pass",   required_argument, 0, '7'},
		{"ipv4",        no_argument,       0, '8'},
		{"ipv6",        no_argument,       0, '9'},
		{0, 0, 0, 0}
	};

	opterr = 0;

	while (0 < (opt_c = getopt_long(argc, argv, ":s:p:w:r:u:n:c:hv", long_opts, &opt_i))) {
	while (0 < (opt_c = getopt_long(argc, argv, ":s:p:w:u:r:m:n:c:hv", long_opts, &opt_i))) {

		switch (opt_c) {



@@ 179,16 212,23 @@ rirc_parse_args(int argc, char **argv)
					return -1;
				}

				cli_servers[n_servers - 1].host     = optarg;
				cli_servers[n_servers - 1].port     = NULL;
				cli_servers[n_servers - 1].pass     = NULL;
				cli_servers[n_servers - 1].username = default_username;
				cli_servers[n_servers - 1].realname = default_realname;
				cli_servers[n_servers - 1].nicks    = default_nicks;
				cli_servers[n_servers - 1].chans    = NULL;
				cli_servers[n_servers - 1].ipv      = IO_IPV_UNSPEC;
				cli_servers[n_servers - 1].tls      = IO_TLS_ENABLED;
				cli_servers[n_servers - 1].tls_vrfy = IO_TLS_VRFY_REQUIRED;
				cli_servers[n_servers - 1].host        = optarg;
				cli_servers[n_servers - 1].port        = NULL;
				cli_servers[n_servers - 1].pass        = NULL;
				cli_servers[n_servers - 1].username    = default_username;
				cli_servers[n_servers - 1].realname    = default_realname;
				cli_servers[n_servers - 1].mode        = NULL;
				cli_servers[n_servers - 1].nicks       = default_nicks;
				cli_servers[n_servers - 1].chans       = NULL;
				cli_servers[n_servers - 1].tls_ca_file = NULL;
				cli_servers[n_servers - 1].tls_ca_path = NULL;
				cli_servers[n_servers - 1].tls_cert    = NULL;
				cli_servers[n_servers - 1].sasl        = NULL;
				cli_servers[n_servers - 1].sasl_user   = NULL;
				cli_servers[n_servers - 1].sasl_pass   = NULL;
				cli_servers[n_servers - 1].ipv         = IO_IPV_UNSPEC;
				cli_servers[n_servers - 1].tls         = IO_TLS_ENABLED;
				cli_servers[n_servers - 1].tls_vrfy    = IO_TLS_VRFY_REQUIRED;
				break;

			#define CHECK_SERVER_OPTARG(OPT_C, REQ) \


@@ 201,68 241,111 @@ rirc_parse_args(int argc, char **argv)
					return -1; \
				}

			case 'p': /* Connect using port */
			case 'p': /* Set connection port */
				CHECK_SERVER_OPTARG(opt_c, 1);
				cli_servers[n_servers - 1].port = optarg;
				break;

			case 'w': /* Connect using port */
			case 'w': /* Set IRC password */
				CHECK_SERVER_OPTARG(opt_c, 1);
				cli_servers[n_servers - 1].pass = optarg;
				break;

			case 'u': /* Connect using username */
			case 'u': /* Set IRC username */
				CHECK_SERVER_OPTARG(opt_c, 1);
				cli_servers[n_servers - 1].username = optarg;
				break;

			case 'r': /* Connect using realname */
			case 'r': /* Set IRC realname */
				CHECK_SERVER_OPTARG(opt_c, 1);
				cli_servers[n_servers - 1].realname = optarg;
				break;

			case 'n': /* Comma separated list of nicks to use */
			case 'm': /* Set IRC user modes */
				CHECK_SERVER_OPTARG(opt_c, 1);
				cli_servers[n_servers - 1].mode = optarg;
				break;

			case 'n': /* Set comma separated list of nicks to use */
				CHECK_SERVER_OPTARG(opt_c, 1);
				cli_servers[n_servers - 1].nicks = optarg;
				break;

			case 'c': /* Comma separated list of channels to join */
			case 'c': /* Set comma separated list of channels to join */
				CHECK_SERVER_OPTARG(opt_c, 1);
				cli_servers[n_servers - 1].chans = optarg;
				break;

			case '4': /* Connect using ipv4 only */
				CHECK_SERVER_OPTARG(opt_c, 0);
				cli_servers[n_servers -1].ipv = IO_IPV_4;
			case '0': /* Set TLS client certificate file path */
				CHECK_SERVER_OPTARG(opt_c, 1);
				cli_servers[n_servers - 1].tls_cert = optarg;
				break;

			case '6': /* Connect using ipv6 only */
				CHECK_SERVER_OPTARG(opt_c, 0);
				cli_servers[n_servers -1].ipv = IO_IPV_6;
			case '1': /* Set TLS peer certificate file path */
				CHECK_SERVER_OPTARG(opt_c, 1);
				cli_servers[n_servers - 1].tls_ca_file = optarg;
				break;

			case 'x': /* Set server TLS disabled */
				CHECK_SERVER_OPTARG(opt_c, 0);
				cli_servers[n_servers -1].tls = IO_TLS_DISABLED;
			case '2': /* Set TLS peer certificate directory path */
				CHECK_SERVER_OPTARG(opt_c, 1);
				cli_servers[n_servers - 1].tls_ca_path = optarg;
				break;

			case 'y': /* Set server TLS peer certificate verification mode */
			case '3': /* Set TLS peer certificate verification mode */
				CHECK_SERVER_OPTARG(opt_c, 1);
				if (!strcmp(optarg, "0") || !strcmp(optarg, "disabled")) {
				if (!strcmp(optarg, "0") || !strcasecmp(optarg, "DISABLED")) {
					cli_servers[n_servers -1].tls_vrfy = IO_TLS_VRFY_DISABLED;
					break;
				}
				if (!strcmp(optarg, "1") || !strcmp(optarg, "optional")) {
				if (!strcmp(optarg, "1") || !strcasecmp(optarg, "OPTIONAL")) {
					cli_servers[n_servers -1].tls_vrfy = IO_TLS_VRFY_OPTIONAL;
					break;
				}
				if (!strcmp(optarg, "2") || !strcmp(optarg, "required")) {
				if (!strcmp(optarg, "2") || !strcasecmp(optarg, "REQUIRED")) {
					cli_servers[n_servers -1].tls_vrfy = IO_TLS_VRFY_REQUIRED;
					break;
				}
				arg_error("invalid option for '--tls-verify' '%s'", optarg);
				return -1;

			case '4': /* Set TLS disabled */
				CHECK_SERVER_OPTARG(opt_c, 0);
				cli_servers[n_servers -1].tls = IO_TLS_DISABLED;
				break;

			case '5': /* Authenticate with SASL mechanism */
				CHECK_SERVER_OPTARG(opt_c, 1);
				if (!strcasecmp(optarg, "EXTERNAL")) {
					cli_servers[n_servers - 1].sasl = optarg;
					break;
				}
				if (!strcasecmp(optarg, "PLAIN")) {
					cli_servers[n_servers - 1].sasl = optarg;
					break;
				}
				arg_error("invalid option for '--sasl' '%s'", optarg);
				return -1;

			case '6': /* Authenticate with SASL user */
				CHECK_SERVER_OPTARG(opt_c, 1);
				cli_servers[n_servers - 1].sasl_user = optarg;
				break;

			case '7': /* Authenticate with SASL pass */
				CHECK_SERVER_OPTARG(opt_c, 1);
				cli_servers[n_servers - 1].sasl_pass = optarg;
				break;

			case '8': /* Use IPv4 only */
				CHECK_SERVER_OPTARG(opt_c, 0);
				cli_servers[n_servers -1].ipv = IO_IPV_4;
				break;

			case '9': /* Use IPv6 only */
				CHECK_SERVER_OPTARG(opt_c, 0);
				cli_servers[n_servers -1].ipv = IO_IPV_6;
				break;

			#undef CHECK_SERVER_OPTARG

			case 'h':


@@ 294,11 377,6 @@ rirc_parse_args(int argc, char **argv)

	for (size_t i = 0; i < n_servers; i++) {

		uint32_t flags =
			cli_servers[i].ipv |
			cli_servers[i].tls |
			cli_servers[i].tls_vrfy;

		if (cli_servers[i].port == NULL)
			cli_servers[i].port = (cli_servers[i].tls == IO_TLS_ENABLED) ? "6697" : "6667";



@@ 307,14 385,20 @@ rirc_parse_args(int argc, char **argv)
			cli_servers[i].port,
			cli_servers[i].pass,
			cli_servers[i].username,
			cli_servers[i].realname
			cli_servers[i].realname,
			cli_servers[i].mode
		);

		cli_servers[i].s->connection = connection(
			cli_servers[i].s,
			cli_servers[i].host,
			cli_servers[i].port,
			flags);
			cli_servers[i].tls_ca_file,
			cli_servers[i].tls_ca_path,
			cli_servers[i].tls_cert,
			(cli_servers[i].ipv |
			 cli_servers[i].tls |
			 cli_servers[i].tls_vrfy));

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


@@ 331,6 415,14 @@ rirc_parse_args(int argc, char **argv)
			return -1;
		}

		if (cli_servers[i].sasl) {
			server_set_sasl(
				cli_servers[i].s,
				cli_servers[i].sasl,
				cli_servers[i].sasl_user,
				cli_servers[i].sasl_pass);
		}

		channel_set_current(cli_servers[i].s->channel);
	}


M src/rirc.h => src/rirc.h +3 -2
@@ 3,8 3,9 @@

/* Default config values obtained at runtime */

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

M src/state.c => src/state.c +274 -51
@@ 10,15 10,27 @@
#include "src/utils/utils.h"

#include <ctype.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <stdarg.h>
#include <stdio.h>

/* See: https://vt100.net/docs/vt100-ug/chapter3.html */
#define CTRL(k) ((k) & 0x1f)

#define COMMAND_HANDLERS \
	X(clear) \
	X(close) \
	X(connect) \
	X(disconnect) \
	X(quit)

#define X(CMD) \
static void command_##CMD(struct channel*, char*);
COMMAND_HANDLERS
#undef X

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

static int state_input_linef(struct channel*);


@@ 198,8 210,6 @@ _newline(struct channel *c, enum buffer_line_type type, const char *from, const 
		}
	}

	// TODO: preformat the time string here

	buffer_newline(
		&(c->buffer),
		type,


@@ 587,78 597,289 @@ state_complete(char *str, uint16_t len, uint16_t max, int first)
}

static void
command(struct channel *c, char *buf)
command(struct channel *c, char *args)
{
	const char *arg;
	const char *cmd;
	int err;

	if (!(cmd = irc_strsep(&buf)))
	if (!(arg = irc_strsep(&args)))
		return;

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

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

	if (!strcasecmp(arg, "connect")) {
		command_connect(c, args);
		return;
	}

	if (!strcasecmp(arg, "disconnect")) {
		command_disconnect(c, args);
		return;
	}

	if (!strcasecmp(arg, "quit")) {
		command_quit(c, args);
		return;
	}

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

static void
command_clear(struct channel *c, char *args)
{
	UNUSED(c);

	char *arg;

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

	state_channel_clear(0);
}

static void
command_close(struct channel *c, char *args)
{
	UNUSED(c);

	char *arg;

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

	if (!strcasecmp(cmd, "connect")) {
	state_channel_close(0);
}

static void
command_connect(struct channel *c, char *args)
{
	/* :connect [hostname [options]] */

	char *arg;

	if (!(arg = irc_strsep(&args))) {

		int err;

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

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

		int ret;
		struct server *s;

		const char *host        = arg;
		const char *port        = NULL;
		const char *pass        = NULL;
		const char *username    = default_username;
		const char *realname    = default_realname;
		const char *mode        = NULL;
		const char *nicks       = default_nicks;
		const char *chans       = NULL;
		const char *tls_ca_file = NULL;
		const char *tls_ca_path = NULL;
		const char *tls_cert    = NULL;
		const char *sasl        = NULL;
		const char *sasl_user   = NULL;
		const char *sasl_pass   = NULL;
		int ipv                 = IO_IPV_UNSPEC;
		int tls                 = IO_TLS_ENABLED;
		int tls_vrfy            = IO_TLS_VRFY_REQUIRED;

		while ((arg = irc_strsep(&args))) {

			if (*arg != '-') {
				action(action_error, ":connect [hostname [options]]");
				return;
			} else if (!strcmp(arg, "-p") || !strcmp(arg, "--port")) {
				if (!(port = irc_strsep(&args))) {
					action(action_error, "connect: '-p/--port' requires an argument");
					return;
				}
			} else if (!strcmp(arg, "-w") || !strcmp(arg, "--pass")) {
				if (!(pass = irc_strsep(&args))) {
					action(action_error, "connect: '-w/--pass' requires an argument");
					return;
				}
			} else if (!strcmp(arg, "-u") || !strcmp(arg, "--username")) {
				if (!(username = irc_strsep(&args))) {
					action(action_error, "connect: '-u/--username' requires an argument");
					return;
				}
			} else if (!strcmp(arg, "-r") || !strcmp(arg, "--realname")) {
				if (!(realname = irc_strsep(&args))) {
					action(action_error, "connect: '-r/--realname' requires an argument");
					return;
				}
			} else if (!strcmp(arg, "-m") || !strcmp(arg, "--mode")) {
				if (!(mode = irc_strsep(&args))) {
					action(action_error, "connect: '-m/--mode' requires an argument");
					return;
				}
			} else if (!strcmp(arg, "-n") || !strcmp(arg, "--nicks")) {
				if (!(nicks = irc_strsep(&args))) {
					action(action_error, "connect: '-n/--nicks' requires an argument");
					return;
				}
			} else if (!strcmp(arg, "-c") || !strcmp(arg, "--chans")) {
				if (!(chans = irc_strsep(&args))) {
					action(action_error, "connect: '-c/--chans' requires an argument");
					return;
				}
			} else if (!strcmp(arg, "--tls-cert")) {
				if (!(tls_cert = irc_strsep(&args))) {
					action(action_error, "connect: '--tls-cert' requires an argument");
					return;
				}
			} else if (!strcmp(arg, "--tls-ca-file")) {
				if (!(tls_ca_file = irc_strsep(&args))) {
					action(action_error, "connect: '--tls-ca-file' requires an argument");
					return;
				}
			} else if (!strcmp(arg, "--tls-ca-path")) {
				if (!(tls_ca_path = irc_strsep(&args))) {
					action(action_error, "connect: '--tls-ca-path' requires an argument");
					return;
				}
			} else if (!strcmp(arg, "--tls-verify")) {
				if (!(arg = irc_strsep(&args))) {
					action(action_error, "connect: '--tls-verify' requires an argument");
					return;
				} else if (!strcmp(arg, "0") || !strcasecmp(arg, "disabled")) {
					tls_vrfy = IO_TLS_VRFY_DISABLED;
				} else if (!strcmp(arg, "1") || !strcasecmp(arg, "optional")) {
					tls_vrfy = IO_TLS_VRFY_OPTIONAL;
				} else if (!strcmp(arg, "2") || !strcasecmp(arg, "required")) {
					tls_vrfy = IO_TLS_VRFY_REQUIRED;
				} else {
					action(action_error, "connect: invalid option for '--tls-verify' '%s'", arg);
					return;
				}
			} else if (!strcmp(arg, "--tls-disable")) {
				tls = IO_TLS_DISABLED;
			} else if (!strcmp(arg, "--sasl")) {
				if (!(sasl = irc_strsep(&args))) {
					action(action_error, "connect: '--sasl' requires an argument");
					return;
				} else if (!strcasecmp(sasl, "EXTERNAL")) {
					;
				} else if (!strcasecmp(sasl, "PLAIN")) {
					;
				} else {
					action(action_error, "connect: invalid option for '--sasl' '%s'", sasl);
					return;
				}
			} else if (!strcmp(arg, "--sasl-user")) {
				if (!(sasl_user = irc_strsep(&args))) {
					action(action_error, "connect: '--sasl-user' requires an argument");
					return;
				}
			} else if (!strcmp(arg, "--sasl-pass")) {
				if (!(sasl_pass = irc_strsep(&args))) {
					action(action_error, "connect: '--sasl-pass' requires an argument");
					return;
				}
			} else if (!strcmp(arg, "--ipv4")) {
				ipv = IO_IPV_4;
			} else if (!strcmp(arg, "--ipv6")) {
				ipv = IO_IPV_6;
			} else {
				action(action_error, "connect: unknown option '%s'", arg);
				return;
			}
		}

		if ((err = io_cx(c->server->connection)))
			action(action_error, "connect: %s", io_err(err));
		if (port == NULL)
			port = (tls == IO_TLS_ENABLED) ? "6697" : "6667";

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

	if (!strcasecmp(cmd, "disconnect")) {
		if (!c->server) {
			action(action_error, "disconnect: This is not a server");
		if (nicks && server_set_nicks(s, nicks)) {
			action(action_error, "connect: invalid -n/--nicks: '%s'", nicks);
			server_free(s);
			return;
		}

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

		if ((err = io_dx(c->server->connection)))
			action(action_error, "disconnect: %s", io_err(err));
		if (server_list_add(state_server_list(), s)) {
			action(action_error, "connect: duplicate server: %s:%s", host, port);
			server_free(s);
			return;
		}

		if (sasl)
			server_set_sasl(s, sasl, sasl_user, sasl_pass);

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

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

		channel_set_current(s->channel);
	}
}

static void
command_disconnect(struct channel *c, char *args)
{
	char *arg;
	int err;

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

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

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

static void
command_quit(struct channel *c, char *args)
{
	UNUSED(c);

	char *arg;

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

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

static int


@@ 832,7 1053,7 @@ io_cb_read_soc(char *buf, size_t len, const void *cb_obj)
				irc_recv(s, &m);

			ci = 0;
		} else if (ci < IRC_MESSAGE_LEN && (isprint(cc) || cc == 0x01)) {
		} else if (ci < IRC_MESSAGE_LEN && cc && cc != '\n' && cc != '\r') {
			s->read.buf[ci++] = cc;
		}
	}


@@ 849,22 1070,23 @@ io_cb_cxed(const void *cb_obj)
	struct server *s = (struct server *)cb_obj;

	int ret;

	server_reset(s);
	server_nicks_next(s);

	s->connected = 1;

	if ((ret = io_sendf(s->connection, "CAP LS " IRCV3_CAP_VERSION)))
		newlinef(s->channel, 0, FROM_ERROR, "sendf fail: %s", io_err(ret));
		server_error(s, "sendf fail: %s", io_err(ret));

	if (s->pass && (ret = io_sendf(s->connection, "PASS %s", s->pass)))
		newlinef(s->channel, 0, FROM_ERROR, "sendf fail: %s", io_err(ret));
		server_error(s, "sendf fail: %s", io_err(ret));

	if ((ret = io_sendf(s->connection, "NICK %s", s->nick)))
		newlinef(s->channel, 0, FROM_ERROR, "sendf fail: %s", io_err(ret));
		server_error(s, "sendf fail: %s", io_err(ret));

	if ((ret = io_sendf(s->connection, "USER %s 8 * :%s", s->username, s->realname)))
		newlinef(s->channel, 0, FROM_ERROR, "sendf fail: %s", io_err(ret));
	if ((ret = io_sendf(s->connection, "USER %s 0 * :%s", s->username, s->realname)))
		server_error(s, "sendf fail: %s", io_err(ret));

	draw(DRAW_STATUS);
	draw(DRAW_FLUSH);


@@ 876,7 1098,7 @@ io_cb_dxed(const void *cb_obj)
	struct server *s = (struct server *)cb_obj;
	struct channel *c = s->channel;

	s->connected = 0;
	server_reset(s);

	do {
		newlinef(c, 0, FROM_ERROR, " -- disconnected --");


@@ 884,6 1106,7 @@ io_cb_dxed(const void *cb_obj)
		c = c->next;
	} while (c != s->channel);

	draw(DRAW_STATUS);
	draw(DRAW_FLUSH);
}



@@ 898,7 1121,7 @@ io_cb_ping(const void *cb_obj, unsigned ping)
	if (ping != IO_PING_MIN)
		draw(DRAW_STATUS);
	else if ((ret = io_sendf(s->connection, "PING :%s", s->host)))
		newlinef(s->channel, 0, FROM_ERROR, "sendf fail: %s", io_err(ret));
		server_error(s, "sendf fail: %s", io_err(ret));

	draw(DRAW_FLUSH);
}

D src/utils/list.h => src/utils/list.h +0 -6
@@ 1,6 0,0 @@
#ifndef RIRC_UTILS_LIST_H
#define RIRC_UTILS_LIST_H

/* TODO: abstract the list implementations in server/channel */

#endif

M test/components/ircv3.c => test/components/ircv3.c +2 -0
@@ 40,6 40,8 @@ test_ircv3_caps_reset(void)
{
	struct ircv3_caps caps;

	ircv3_caps(&caps);

	caps.cap_3.req = 1;
	caps.cap_3.req_auto = 1;
	caps.cap_3.set = 1;

M test/components/mode.c => test/components/mode.c +118 -274
@@ 5,19 5,19 @@
#define ALL_UPPERS "ABCDEFGHIJKLMNOPQRSTUVWXYZ"

static void
test_flag_bit(void)
test_mode_bit(void)
{
	/* Test mode flag -> mode bit */

	assert_eq(flag_bit('a' - 1), 0);
	assert_eq(flag_bit('a'),     1 << 0);
	assert_eq(flag_bit('z'),     1 << 25);
	assert_eq(flag_bit('z' + 1), 0);
	assert_eq(mode_bit('a' - 1), 0);
	assert_eq(mode_bit('a'),     1 << 0);
	assert_eq(mode_bit('z'),     1 << 25);
	assert_eq(mode_bit('z' + 1), 0);

	assert_eq(flag_bit('A' - 1), 0);
	assert_eq(flag_bit('A'),     1 << 0);
	assert_eq(flag_bit('Z'),     1 << 25);
	assert_eq(flag_bit('Z' + 1), 0);
	assert_eq(mode_bit('A' - 1), 0);
	assert_eq(mode_bit('A'),     1 << 0);
	assert_eq(mode_bit('Z'),     1 << 25);
	assert_eq(mode_bit('Z' + 1), 0);
}

static void


@@ 28,274 28,156 @@ test_mode_str(void)
	struct mode m = MODE_EMPTY;
	struct mode_str str;

	memset(&str, 0, sizeof(struct mode_str));

	/* mode_str type not set */
	assert_fatal(mode_str(&m, &str));

	str.type = MODE_STR_T_SIZE;

	/* mode_str type unknown */
	assert_fatal(mode_str(&m, &str));

	str.type = MODE_STR_USERMODE;
	assert_fatal(mode_str(&m, &str, -1));

	/* Test no mode */
	assert_strcmp(mode_str(&m, &str), "");
	assert_strcmp(mode_str(&m, &str, MODE_STR_USERMODE), "");

	m.lower = UINT32_MAX;
	m.upper = 0;

	assert_strcmp(mode_str(&m, &str), ALL_LOWERS);
	assert_strcmp(mode_str(&m, &str, MODE_STR_USERMODE), ALL_LOWERS);

	m.lower = 0;
	m.upper = UINT32_MAX;

	assert_strcmp(mode_str(&m, &str), ALL_UPPERS);
	assert_strcmp(mode_str(&m, &str, MODE_STR_USERMODE), ALL_UPPERS);

	m.lower = UINT32_MAX;
	m.upper = UINT32_MAX;

	assert_strcmp(mode_str(&m, &str), ALL_LOWERS ALL_UPPERS);
	assert_strcmp(mode_str(&m, &str, MODE_STR_USERMODE), ALL_LOWERS ALL_UPPERS);
}

static void
test_chanmode_set(void)
test_mode_chanmode_set(void)
{
	/* Test setting/unsetting chanmode flag, prefix and mode string */
	/* Test setting/unsetting chanmode flags */

	struct mode m = MODE_EMPTY;
	struct mode_cfg cfg;
	struct mode_str str = { .type = MODE_STR_CHANMODE };
	struct mode_str str;

	mode_cfg_chanmodes(&cfg, "abcdefghijsp");
	mode_cfg_subtypes(&cfg, "abc,def,ghi,jsp");

#define CHECK(STR, PRFX) \
	assert_strcmp(mode_str(&m, &str), (STR)); assert_eq(m.prefix, (PRFX));

	/* Test setting/unsetting invalid chanmode flag */
	assert_eq(mode_chanmode_set(&m, &cfg, 'z', MODE_SET_ON), MODE_ERR_INVALID_FLAG);
	CHECK("", 0);
	assert_eq(mode_chanmode_set(&m, &cfg, 'z', MODE_SET_OFF), MODE_ERR_INVALID_FLAG);
	CHECK("", 0);
	assert_eq(mode_chanmode_set(&m, &cfg, 'z', 1), MODE_ERR_INVALID_FLAG);
	assert_strcmp(mode_str(&m, &str, MODE_STR_CHANMODE), "");

	assert_eq(mode_chanmode_set(&m, &cfg, 'z', 0), MODE_ERR_INVALID_FLAG);
	assert_strcmp(mode_str(&m, &str, MODE_STR_CHANMODE), "");

	/* Test valid CHANMODE subtype A doesn't set flag */
	assert_eq(mode_chanmode_set(&m, &cfg, 'a', MODE_SET_ON), MODE_ERR_NONE);
	CHECK("", 0);
	assert_eq(mode_chanmode_set(&m, &cfg, 'a', 1), MODE_ERR_NONE);
	assert_strcmp(mode_str(&m, &str, MODE_STR_CHANMODE), "");

	/* Test valid CHANMODE subtypes B,C,D set flags */
	assert_eq(mode_chanmode_set(&m, &cfg, 'd', MODE_SET_ON), MODE_ERR_NONE);
	CHECK("d", 0);

	assert_eq(mode_chanmode_set(&m, &cfg, 'e', MODE_SET_ON), MODE_ERR_NONE);
	CHECK("de", 0);

	assert_eq(mode_chanmode_set(&m, &cfg, 'j', MODE_SET_ON), MODE_ERR_NONE);
	CHECK("dej", 0);

	/* Test 's'/'p' chanmodes set prefix but do not appear in mode string
	 *
	 * Ensure 's' always supercedes and unsets 'p',
	 * Ensure 'p' is silently ignored when 's' is set */

	assert_eq(mode_chanmode_set(&m, &cfg, 'p', MODE_SET_ON), MODE_ERR_NONE);
	CHECK("dejp", MODE_CHANMODE_PREFIX_PRIVATE);
	assert_true(mode_isset(&m, 'p'));
	assert_false(mode_isset(&m, 's'));

	/* Unsetting 'p' sets default */
	assert_eq(mode_chanmode_set(&m, &cfg, 'p', MODE_SET_OFF), MODE_ERR_NONE);
	CHECK("dej", MODE_CHANMODE_PREFIX_OTHER);
	assert_false(mode_isset(&m, 'p'));
	assert_false(mode_isset(&m, 's'));

	/* 's' supercedes 'p' */
	assert_eq(mode_chanmode_set(&m, &cfg, 'p', MODE_SET_ON), MODE_ERR_NONE);
	assert_eq(mode_chanmode_set(&m, &cfg, 's', MODE_SET_ON), MODE_ERR_NONE);
	CHECK("dejs", MODE_CHANMODE_PREFIX_SECRET);
	assert_true(mode_isset(&m, 's'));
	assert_false(mode_isset(&m, 'p'));

	/* 'p' is silently ignored when 's' is set */
	assert_eq(mode_chanmode_set(&m, &cfg, 'p', MODE_SET_ON), MODE_ERR_NONE);
	CHECK("dejs", MODE_CHANMODE_PREFIX_SECRET);
	assert_true(mode_isset(&m, 's'));
	assert_false(mode_isset(&m, 'p'));

	/* Unsetting 's' sets default */
	assert_eq(mode_chanmode_set(&m, &cfg, 's', MODE_SET_OFF), MODE_ERR_NONE);
	CHECK("dej", MODE_CHANMODE_PREFIX_OTHER);
	assert_false(mode_isset(&m, 'p'));
	assert_false(mode_isset(&m, 's'));

	/* Test unsetting previous flags */
	assert_eq(mode_chanmode_set(&m, &cfg, 'j', MODE_SET_OFF), MODE_ERR_NONE);
	CHECK("de", MODE_CHANMODE_PREFIX_OTHER);

	assert_eq(mode_chanmode_set(&m, &cfg, 'e', MODE_SET_OFF), MODE_ERR_NONE);
	CHECK("d", MODE_CHANMODE_PREFIX_OTHER);

	assert_eq(mode_chanmode_set(&m, &cfg, 'd', MODE_SET_OFF), MODE_ERR_NONE);
	CHECK("", MODE_CHANMODE_PREFIX_OTHER);
	assert_eq(mode_chanmode_set(&m, &cfg, 'd', 1), MODE_ERR_NONE);
	assert_strcmp(mode_str(&m, &str, MODE_STR_CHANMODE), "d");

#undef CHECK
	assert_eq(mode_chanmode_set(&m, &cfg, 'e', 1), MODE_ERR_NONE);
	assert_strcmp(mode_str(&m, &str, MODE_STR_CHANMODE), "de");

	assert_eq(mode_chanmode_set(&m, &cfg, 'j', 1), MODE_ERR_NONE);
	assert_strcmp(mode_str(&m, &str, MODE_STR_CHANMODE), "dej");

	/* test unsetting flags */
	assert_eq(mode_chanmode_set(&m, &cfg, 's', 0), MODE_ERR_NONE);
	assert_eq(mode_chanmode_set(&m, &cfg, 'j', 0), MODE_ERR_NONE);
	assert_eq(mode_chanmode_set(&m, &cfg, 'e', 0), MODE_ERR_NONE);
	assert_eq(mode_chanmode_set(&m, &cfg, 'd', 0), MODE_ERR_NONE);
	assert_strcmp(mode_str(&m, &str, MODE_STR_CHANMODE), "");
}

static void
test_prfxmode_set(void)
test_mode_prfxmode_set(void)
{
	/* Test setting/unsetting prfxmode flag and prefix */

	struct mode m = MODE_EMPTY;

	struct mode_cfg cfg = {
		.PREFIX = {
			.F = "abc",
			.T = "123"
		}
	};
	struct mode_str str;

	mode_cfg_chanmodes(&cfg, "abc");

	/* Test setting/unsetting invalid prfxmode flag */
	assert_eq(mode_prfxmode_set(&m, &cfg, 'd', MODE_SET_ON), MODE_ERR_INVALID_FLAG);
	assert_eq(mode_prfxmode_set(&m, &cfg, 'd', 1), MODE_ERR_INVALID_FLAG);
	assert_eq(m.prefix, 0);
	assert_eq(mode_prfxmode_set(&m, &cfg, 'd', 0), MODE_ERR_INVALID_FLAG);
	assert_eq(m.prefix, 0);

	/* Test setting/unsetting invalid prfxmode prefix */
	assert_eq(mode_prfxmode_set(&m, &cfg, '4', 1), MODE_ERR_INVALID_FLAG);
	assert_eq(m.prefix, 0);
	assert_eq(mode_prfxmode_set(&m, &cfg, 'd', MODE_SET_OFF), MODE_ERR_INVALID_FLAG);
	assert_eq(mode_prfxmode_set(&m, &cfg, '4', 0), MODE_ERR_INVALID_FLAG);
	assert_eq(m.prefix, 0);

	/* Test setting valid flags respects PREFIX precedence */
	assert_eq(mode_prfxmode_set(&m, &cfg, 'b', MODE_SET_ON), MODE_ERR_NONE);
	assert_eq(mode_prfxmode_set(&m, &cfg, 'b', 1), MODE_ERR_NONE);
	assert_eq(m.prefix, '2');
	assert_eq(mode_prfxmode_set(&m, &cfg, 'c', MODE_SET_ON), MODE_ERR_NONE);
	assert_eq(mode_prfxmode_set(&m, &cfg, 'c', 1), MODE_ERR_NONE);
	assert_eq(m.prefix, '2');
	assert_eq(mode_prfxmode_set(&m, &cfg, 'a', MODE_SET_ON), MODE_ERR_NONE);
	assert_eq(mode_prfxmode_set(&m, &cfg, 'a', 1), MODE_ERR_NONE);
	assert_eq(m.prefix, '1');

	/* Test unsetting valid flags respects PREFIX precedence */
	assert_eq(mode_prfxmode_set(&m, &cfg, 'b', MODE_SET_OFF), MODE_ERR_NONE);
	assert_eq(mode_prfxmode_set(&m, &cfg, 'b', 0), MODE_ERR_NONE);
	assert_eq(m.prefix, '1');
	assert_eq(mode_prfxmode_set(&m, &cfg, 'a', MODE_SET_OFF), MODE_ERR_NONE);
	assert_eq(mode_prfxmode_set(&m, &cfg, 'a', 0), MODE_ERR_NONE);
	assert_eq(m.prefix, '3');
	assert_eq(mode_prfxmode_set(&m, &cfg, 'c', MODE_SET_OFF), MODE_ERR_NONE);
	assert_eq(mode_prfxmode_set(&m, &cfg, 'c', 0), MODE_ERR_NONE);
	assert_eq(m.prefix, 0);

	/* Test setting valid prefixes */
	assert_eq(mode_prfxmode_set(&m, &cfg, '2', 1), MODE_ERR_NONE);
	assert_eq(m.prefix, '2');
	assert_eq(mode_prfxmode_set(&m, &cfg, '3', 1), MODE_ERR_NONE);
	assert_eq(m.prefix, '2');

	assert_strcmp(mode_str(&m, &str, MODE_STR_PRFXMODE), "bc");
}

static void
test_usermode_set(void)
test_mode_usermode_set(void)
{
	/* Test setting/unsetting usermode flag and mode string */

	struct mode m = MODE_EMPTY;
	struct mode_str str = { .type = MODE_STR_USERMODE };
	struct mode_cfg cfg;
	struct mode_str str;

	mode_cfg_usermodes(&cfg, "azAZ");

	/* Test setting invalid usermode flag */
	assert_eq(mode_usermode_set(&m, &cfg, 'b', MODE_SET_ON), MODE_ERR_INVALID_FLAG);
	assert_eq(mode_usermode_set(&m, &cfg, 'b', 1), MODE_ERR_INVALID_FLAG);

	/* Test setting valid flags */
	assert_eq(mode_usermode_set(&m, &cfg, 'a', MODE_SET_ON), MODE_ERR_NONE);
	assert_eq(mode_usermode_set(&m, &cfg, 'Z', MODE_SET_ON), MODE_ERR_NONE);
	assert_strcmp(mode_str(&m, &str), "aZ");
	assert_eq(mode_usermode_set(&m, &cfg, 'a', 1), MODE_ERR_NONE);
	assert_eq(mode_usermode_set(&m, &cfg, 'Z', 1), MODE_ERR_NONE);
	assert_strcmp(mode_str(&m, &str, MODE_STR_USERMODE), "aZ");

	assert_eq(mode_usermode_set(&m, &cfg, 'z', MODE_SET_ON), MODE_ERR_NONE);
	assert_eq(mode_usermode_set(&m, &cfg, 'A', MODE_SET_ON), MODE_ERR_NONE);
	assert_strcmp(mode_str(&m, &str), "azAZ");
	assert_eq(mode_usermode_set(&m, &cfg, 'z', 1), MODE_ERR_NONE);
	assert_eq(mode_usermode_set(&m, &cfg, 'A', 1), MODE_ERR_NONE);
	assert_strcmp(mode_str(&m, &str, MODE_STR_USERMODE), "azAZ");

	/* Test unsetting invalid usermode flag */
	assert_eq(mode_usermode_set(&m, &cfg, 'c', MODE_SET_OFF), MODE_ERR_INVALID_FLAG);
	assert_eq(mode_usermode_set(&m, &cfg, 'c', 0), MODE_ERR_INVALID_FLAG);

	/* Test unsetting valid flags */
	assert_eq(mode_usermode_set(&m, &cfg, 'z', MODE_SET_OFF), MODE_ERR_NONE);
	assert_eq(mode_usermode_set(&m, &cfg, 'Z', MODE_SET_OFF), MODE_ERR_NONE);
	assert_strcmp(mode_str(&m, &str), "aA");

	assert_eq(mode_usermode_set(&m, &cfg, 'a', MODE_SET_OFF), MODE_ERR_NONE);
	assert_eq(mode_usermode_set(&m, &cfg, 'A', MODE_SET_OFF), MODE_ERR_NONE);
	assert_strcmp(mode_str(&m, &str), "");
}

static void
test_chanmode_prefix(void)
{
	/* Test setting chanmode by prefix */

	struct mode m = MODE_EMPTY;
	struct mode_cfg cfg;
	struct mode_str str = { .type = MODE_STR_CHANMODE };

	mode_cfg_chanmodes(&cfg, "sp");

#define CHECK(M, PRFX, P, S, STR) \
	assert_eq((M).prefix, (PRFX));         \
	assert_eq(mode_isset(&(M), 'p'), (P)); \
	assert_eq(mode_isset(&(M), 's'), (S)); \
	assert_strcmp(mode_str(&(M), &str), (STR));

	/* Test setting invalid chanmode prefix */
	assert_eq(mode_chanmode_prefix(&m, &cfg, '$'), MODE_ERR_INVALID_PREFIX);
	CHECK(m, 0, 0, 0, "");

	/* Test setting valid chanmode prefixes by precedence*/
	assert_eq(mode_chanmode_prefix(&m, &cfg, MODE_CHANMODE_PREFIX_OTHER), MODE_ERR_NONE);
	CHECK(m, MODE_CHANMODE_PREFIX_OTHER, 0, 0, "");

	assert_eq(mode_chanmode_prefix(&m, &cfg, MODE_CHANMODE_PREFIX_PRIVATE), MODE_ERR_NONE);
	CHECK(m, MODE_CHANMODE_PREFIX_PRIVATE, 1, 0, "p");

	assert_eq(mode_chanmode_prefix(&m, &cfg, MODE_CHANMODE_PREFIX_SECRET), MODE_ERR_NONE);
	CHECK(m, MODE_CHANMODE_PREFIX_SECRET, 0, 1, "s");

	/* Test silently ignored setting by precedence */

	/* PRIVATE > OTHER */
	struct mode m2 = MODE_EMPTY;

	assert_eq(mode_chanmode_prefix(&m2, &cfg, MODE_CHANMODE_PREFIX_PRIVATE), MODE_ERR_NONE);
	assert_eq(mode_chanmode_prefix(&m2, &cfg, MODE_CHANMODE_PREFIX_OTHER), MODE_ERR_NONE);
	CHECK(m2, MODE_CHANMODE_PREFIX_PRIVATE, 1, 0, "p");

	/* SECRET > PRIVATE, OTHER */
	struct mode m3 = MODE_EMPTY;

	assert_eq(mode_chanmode_prefix(&m3, &cfg, MODE_CHANMODE_PREFIX_SECRET), MODE_ERR_NONE);
	assert_eq(mode_chanmode_prefix(&m3, &cfg, MODE_CHANMODE_PREFIX_PRIVATE), MODE_ERR_NONE);
	assert_eq(mode_chanmode_prefix(&m3, &cfg, MODE_CHANMODE_PREFIX_OTHER), MODE_ERR_NONE);
	CHECK(m3, MODE_CHANMODE_PREFIX_SECRET, 0, 1, "s");

#undef CHECK
}

static void
test_prfxmode_prefix(void)
{
	/* Test setting prfxmode by prefix */

	struct mode m = MODE_EMPTY;
	struct mode_str str = { .type = MODE_STR_PRFXMODE };

	struct mode_cfg cfg = {
		.PREFIX = {
			.F = "abc",
			.T = "123"
		}
	};

	mode_cfg_chanmodes(&cfg, "abc");

	/* Test setting invalid prfxmode prefix */
	assert_eq(mode_prfxmode_prefix(&m, &cfg, 0),   MODE_ERR_INVALID_PREFIX);
	assert_eq(mode_prfxmode_prefix(&m, &cfg, '0'), MODE_ERR_INVALID_PREFIX);
	assert_eq(mode_prfxmode_prefix(&m, &cfg, '4'), MODE_ERR_INVALID_PREFIX);

	/* Test setting valid prfxmode prefix */
	assert_eq(mode_prfxmode_prefix(&m, &cfg, '2'), MODE_ERR_NONE);
	assert_eq(mode_prfxmode_prefix(&m, &cfg, '3'), MODE_ERR_NONE);
	assert_eq(mode_usermode_set(&m, &cfg, 'z', 0), MODE_ERR_NONE);
	assert_eq(mode_usermode_set(&m, &cfg, 'Z', 0), MODE_ERR_NONE);
	assert_strcmp(mode_str(&m, &str, MODE_STR_USERMODE), "aA");

	assert_strcmp(mode_str(&m, &str), "bc");
	assert_eq(m.prefix, '2');
	assert_eq(mode_usermode_set(&m, &cfg, 'a', 0), MODE_ERR_NONE);
	assert_eq(mode_usermode_set(&m, &cfg, 'A', 0), MODE_ERR_NONE);
	assert_strcmp(mode_str(&m, &str, MODE_STR_USERMODE), "");
}

static void


@@ 304,23 186,23 @@ test_mode_cfg_usermodes(void)
	/* Test configuring server usermodes */

	struct mode_cfg cfg;
	struct mode_str str = { .type = MODE_STR_USERMODE };
	struct mode_str str;

	/* Test empty string */
	assert_eq(mode_cfg_usermodes(&cfg, ""), MODE_ERR_NONE);
	assert_strcmp(mode_str(&(cfg.usermodes), &str), "");
	assert_strcmp(mode_str(&(cfg.usermodes), &str, MODE_STR_USERMODE), "");

	/* Test invalid flags */
	assert_eq(mode_cfg_usermodes(&cfg, "$abc1!xyz."), MODE_ERR_NONE);
	assert_strcmp(mode_str(&(cfg.usermodes), &str), "abcxyz");
	assert_strcmp(mode_str(&(cfg.usermodes), &str, MODE_STR_USERMODE), "abcxyz");

	/* Test duplicate flags */
	assert_eq(mode_cfg_usermodes(&cfg, "aaabbc"), MODE_ERR_NONE);
	assert_strcmp(mode_str(&(cfg.usermodes), &str), "abc");
	assert_strcmp(mode_str(&(cfg.usermodes), &str, MODE_STR_USERMODE), "abc");

	/* Test valid string */
	assert_eq(mode_cfg_usermodes(&cfg, ALL_LOWERS ALL_UPPERS), MODE_ERR_NONE);
	assert_strcmp(mode_str(&(cfg.usermodes), &str), ALL_LOWERS ALL_UPPERS);
	assert_strcmp(mode_str(&(cfg.usermodes), &str, MODE_STR_USERMODE), ALL_LOWERS ALL_UPPERS);
}

static void


@@ 329,23 211,23 @@ test_mode_cfg_chanmodes(void)
	/* Test configuring server chanmodes */

	struct mode_cfg cfg;
	struct mode_str str = { .type = MODE_STR_USERMODE };
	struct mode_str str;

	/* Test empty string */
	assert_eq(mode_cfg_chanmodes(&cfg, ""), MODE_ERR_NONE);
	assert_strcmp(mode_str(&(cfg.chanmodes), &str), "");
	assert_strcmp(mode_str(&(cfg.chanmodes), &str, MODE_STR_USERMODE), "");

	/* Test invalid flags */
	assert_eq(mode_cfg_chanmodes(&cfg, "$abc1!xyz."), MODE_ERR_NONE);
	assert_strcmp(mode_str(&(cfg.chanmodes), &str), "abcxyz");
	assert_strcmp(mode_str(&(cfg.chanmodes), &str, MODE_STR_USERMODE), "abcxyz");

	/* Test duplicate flags */
	assert_eq(mode_cfg_chanmodes(&cfg, "aaabbc"), MODE_ERR_NONE);
	assert_strcmp(mode_str(&(cfg.chanmodes), &str), "abc");
	assert_strcmp(mode_str(&(cfg.chanmodes), &str, MODE_STR_USERMODE), "abc");

	/* Test valid string */
	assert_eq(mode_cfg_chanmodes(&cfg, ALL_LOWERS ALL_UPPERS), MODE_ERR_NONE);
	assert_strcmp(mode_str(&(cfg.chanmodes), &str), ALL_LOWERS ALL_UPPERS);
	assert_strcmp(mode_str(&(cfg.chanmodes), &str, MODE_STR_USERMODE), ALL_LOWERS ALL_UPPERS);
}

static void


@@ 354,13 236,13 @@ test_mode_cfg_subtypes(void)
	/* Test configuring CHANMODE subtypes */

	struct mode_cfg cfg;
	struct mode_str str = { .type = MODE_STR_USERMODE };
	struct mode_str str;

#define CHECK(_A, _B, _C, _D) \
	assert_strcmp(mode_str(&(cfg.CHANMODES.A), &str), (_A)); \
	assert_strcmp(mode_str(&(cfg.CHANMODES.B), &str), (_B)); \
	assert_strcmp(mode_str(&(cfg.CHANMODES.C), &str), (_C)); \
	assert_strcmp(mode_str(&(cfg.CHANMODES.D), &str), (_D));
	assert_strcmp(mode_str(&(cfg.CHANMODES.A), &str, MODE_STR_USERMODE), (_A)); \
	assert_strcmp(mode_str(&(cfg.CHANMODES.B), &str, MODE_STR_USERMODE), (_B)); \
	assert_strcmp(mode_str(&(cfg.CHANMODES.C), &str, MODE_STR_USERMODE), (_C)); \
	assert_strcmp(mode_str(&(cfg.CHANMODES.D), &str, MODE_STR_USERMODE), (_D));

	/* Test empty string */
	assert_eq(mode_cfg_subtypes(&cfg, ""), MODE_ERR_NONE);


@@ 372,15 254,11 @@ test_mode_cfg_subtypes(void)

	/* Test extra commas */
	assert_eq(mode_cfg_subtypes(&cfg, "abc,def,,xyz,,,abc"), MODE_ERR_INVALID_CONFIG);
	CHECK("abc", "def", "", "xyz");
	CHECK("", "", "", "");

	/* Test invalid flags */
	assert_eq(mode_cfg_subtypes(&cfg, "!!abc,d123e,fg!-@,^&"), MODE_ERR_NONE);
	CHECK("abc", "de", "fg", "");

	/* Test duplicate flags */
	assert_eq(mode_cfg_subtypes(&cfg, "zaabc,deefz,zghh,zzz"), MODE_ERR_NONE);
	CHECK("abcz", "def", "gh", "");
	assert_eq(mode_cfg_subtypes(&cfg, "!!abc,d123e,fg!-@,^&"), MODE_ERR_INVALID_CONFIG);
	CHECK("", "", "", "");

	const char *all_flags =
		ALL_LOWERS ALL_UPPERS ","


@@ 390,7 268,10 @@ test_mode_cfg_subtypes(void)

	/* Test valid string */
	assert_eq(mode_cfg_subtypes(&cfg, all_flags), MODE_ERR_NONE);
	CHECK(ALL_LOWERS ALL_UPPERS, "", "", "");
	CHECK(ALL_LOWERS ALL_UPPERS,
	      ALL_LOWERS ALL_UPPERS,
	      ALL_LOWERS ALL_UPPERS,
	      ALL_LOWERS ALL_UPPERS);

#undef CHECK
}


@@ 450,38 331,7 @@ test_mode_cfg_prefix(void)
}

static void
test_mode_cfg_modes(void)
{
	/* Test configuring MODES */

	struct mode_cfg cfg = {
		.MODES = 3
	};

	/* Test empty string */
	assert_eq(mode_cfg_modes(&cfg, ""), MODE_ERR_INVALID_CONFIG);
	assert_eq(cfg.MODES, 3);

	/* Test not a number */
	assert_eq(mode_cfg_modes(&cfg, "1abc"), MODE_ERR_INVALID_CONFIG);
	assert_eq(mode_cfg_modes(&cfg, "wxyz"), MODE_ERR_INVALID_CONFIG);
	assert_eq(cfg.MODES, 3);

	/* Test invalid number (i.e.: not [1-99]) */
	assert_eq(mode_cfg_modes(&cfg, "0"),   MODE_ERR_INVALID_CONFIG);
	assert_eq(mode_cfg_modes(&cfg, "100"), MODE_ERR_INVALID_CONFIG);
	assert_eq(cfg.MODES, 3);

	/* Teset valid numbers */
	assert_eq(mode_cfg_modes(&cfg, "1"),  MODE_ERR_NONE);
	assert_eq(cfg.MODES, 1);

	assert_eq(mode_cfg_modes(&cfg, "99"), MODE_ERR_NONE);
	assert_eq(cfg.MODES, 99);
}

static void
test_chanmode_type(void)
test_mode_type(void)
{
	/* Test retrieving a mode flag type */



@@ 497,53 347,47 @@ test_chanmode_type(void)
	if (config_errs != MODE_ERR_NONE)
		test_abort("Configuration error");

	/* Test invalid '+'/'-' */
	assert_eq(chanmode_type(&cfg, MODE_SET_INVALID, 'a'), MODE_FLAG_INVALID_SET);

	/* Test invalid flag */
	assert_eq(chanmode_type(&cfg, MODE_SET_ON, '!'), MODE_FLAG_INVALID_FLAG);
	assert_eq(mode_type(&cfg, '!', 1), MODE_FLAG_INVALID_FLAG);

	/* Test flag not in usermodes, chanmodes */
	assert_eq(chanmode_type(&cfg, MODE_SET_ON, 'z'), MODE_FLAG_INVALID_FLAG);
	assert_eq(mode_type(&cfg, 'z', 1), MODE_FLAG_INVALID_FLAG);

	/* Test chanmode A (always has a parameter) */
	assert_eq(chanmode_type(&cfg, MODE_SET_ON,  'b'), MODE_FLAG_CHANMODE_PARAM);
	assert_eq(chanmode_type(&cfg, MODE_SET_OFF, 'b'), MODE_FLAG_CHANMODE_PARAM);
	assert_eq(mode_type(&cfg, 'b', 1), MODE_FLAG_CHANMODE_PARAM);
	assert_eq(mode_type(&cfg, 'b', 0), MODE_FLAG_CHANMODE_PARAM);

	/* Test chanmode B (always has a parameter) */
	assert_eq(chanmode_type(&cfg, MODE_SET_ON,  'c'), MODE_FLAG_CHANMODE_PARAM);
	assert_eq(chanmode_type(&cfg, MODE_SET_OFF, 'c'), MODE_FLAG_CHANMODE_PARAM);
	assert_eq(mode_type(&cfg, 'c', 1), MODE_FLAG_CHANMODE_PARAM);
	assert_eq(mode_type(&cfg, 'c', 0), MODE_FLAG_CHANMODE_PARAM);

	/* Test chanmode C (only has a parameter when set) */
	assert_eq(chanmode_type(&cfg, MODE_SET_ON,  'd'), MODE_FLAG_CHANMODE_PARAM);
	assert_eq(chanmode_type(&cfg, MODE_SET_OFF, 'd'), MODE_FLAG_CHANMODE);
	assert_eq(mode_type(&cfg, 'd', 1), MODE_FLAG_CHANMODE_PARAM);
	assert_eq(mode_type(&cfg, 'd', 0), MODE_FLAG_CHANMODE);

	/* Test chanmode D (never has a parameter) */
	assert_eq(chanmode_type(&cfg, MODE_SET_ON,  'e'), MODE_FLAG_CHANMODE);
	assert_eq(chanmode_type(&cfg, MODE_SET_OFF, 'e'), MODE_FLAG_CHANMODE);
	assert_eq(mode_type(&cfg, 'e', 1), MODE_FLAG_CHANMODE);
	assert_eq(mode_type(&cfg, 'e', 0), MODE_FLAG_CHANMODE);

	/* Test prefix flag */
	assert_eq(chanmode_type(&cfg, MODE_SET_ON,  'f'), MODE_FLAG_PREFIX);
	assert_eq(chanmode_type(&cfg, MODE_SET_OFF, 'f'), MODE_FLAG_PREFIX);
	assert_eq(mode_type(&cfg, 'f', 1), MODE_FLAG_PREFIX);
	assert_eq(mode_type(&cfg, 'f', 0), MODE_FLAG_PREFIX);
}

int
main(void)
{
	struct testcase tests[] = {
		TESTCASE(test_flag_bit),
		TESTCASE(test_mode_bit),
		TESTCASE(test_mode_str),
		TESTCASE(test_chanmode_set),
		TESTCASE(test_prfxmode_set),
		TESTCASE(test_usermode_set),
		TESTCASE(test_chanmode_prefix),
		TESTCASE(test_prfxmode_prefix),
		TESTCASE(test_mode_chanmode_set),
		TESTCASE(test_mode_prfxmode_set),
		TESTCASE(test_mode_usermode_set),
		TESTCASE(test_mode_cfg_usermodes),
		TESTCASE(test_mode_cfg_chanmodes),
		TESTCASE(test_mode_cfg_subtypes),
		TESTCASE(test_mode_cfg_prefix),
		TESTCASE(test_mode_cfg_modes),
		TESTCASE(test_chanmode_type)
		TESTCASE(test_mode_type)
	};

	return run_tests(NULL, NULL, tests);

M test/components/server.c => test/components/server.c +16 -9
@@ 44,13 44,13 @@ test_server_list(void)

	memset(&servers, 0, sizeof(servers));

	s1 = server("host1", "port1", NULL, "", "");
	s2 = server("host1", "port1", "foo1", "", ""); /* duplicate host, port (s1) */
	s3 = server("host1", "port2", "foo2", "", ""); /* duplicate host (s1), different port */
	s4 = server("host2", "port1", "foo3", "", ""); /* duplicate port (s1), different host */
	s5 = server("host2", "port2", NULL, "", "");   /* duplicate host (s4), duplicate port (s3) */
	s6 = server("host1", "port2", NULL, "", "");   /* duplicate host, port (s4) */
	s7 = server("host2", "port1", NULL, "", "");   /* duplicate host, port (s5) */
	s1 = server("host1", "port1",   NULL, "real", "user", "abc");
	s2 = server("host1", "port1", "foo1", "real", "user", "abc"); /* duplicate host, port (s1) */
	s3 = server("host1", "port2", "foo2", "real", "user", "abc"); /* duplicate host (s1), different port */
	s4 = server("host2", "port1", "foo3", "real", "user", NULL);  /* duplicate port (s1), different host */
	s5 = server("host2", "port2",   NULL, "real", "user", NULL);  /* duplicate host (s4), duplicate port (s3) */
	s6 = server("host1", "port2",   NULL, "real", "user", NULL);  /* duplicate host, port (s4) */
	s7 = server("host2", "port1",   NULL, "real", "user", NULL);  /* duplicate host, port (s5) */

	/* Test add */
	assert_ptr_eq(server_list_add(&servers, s1), NULL);


@@ 117,7 117,7 @@ test_server_set_chans(void)
	struct channel *c;
	struct server *s;

	s = server("host1", "port", NULL, "", "");
	s = server("host1", "port", NULL, "real", "user", NULL);
	assert_eq(s->clist.count, 1);

	/* empty values, invalid formats */


@@ 162,7 162,7 @@ test_server_set_chans(void)
static void
test_server_set_nicks(void)
{
	struct server *s = server("host", "port", NULL, "", "");
	struct server *s = server("host", "port", NULL, "real", "user", NULL);

	server_nicks_next(s);
	assert_strncmp(s->nick, "rirc", 4);


@@ 213,6 213,12 @@ test_server_set_nicks(void)
}

static void
test_server_set_sasl(void)
{
	// TODO
}

static void
test_parse_005(void)
{
	/* Test numeric 005 parsing  */


@@ 301,6 307,7 @@ main(void)
		TESTCASE(test_server_list),
		TESTCASE(test_server_set_chans),
		TESTCASE(test_server_set_nicks),
		TESTCASE(test_server_set_sasl),
		TESTCASE(test_parse_005)
	};


M test/handlers/irc_ctcp.c => test/handlers/irc_ctcp.c +1 -1
@@ 483,7 483,7 @@ test_recv_ctcp_response_version(void)
static int
test_init(void)
{
	s = server("h1", "p1", NULL, "u1", "r1");
	s = server("h1", "p1", NULL, "u1", "r1", NULL);

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

M test/handlers/irc_recv.c => test/handlers/irc_recv.c +69 -90
@@ 44,8 44,6 @@ static struct server *s;
static void
test_irc_generic(void)
{
	server_reset(s);

	#define CHECK_IRC_GENERIC(M, C, F, RET, LINE_N) \
	do { \
		mock_reset_io(); \


@@ 92,8 90,6 @@ test_irc_generic(void)
static void
test_irc_generic_error(void)
{
	server_reset(s);

	char m1[] = "COMMAND arg1 arg2 :trailing arg";

	mock_reset_io();


@@ 108,8 104,6 @@ test_irc_generic_error(void)
static void
test_irc_generic_ignore(void)
{
	server_reset(s);

	char m1[] = "COMMAND arg1 arg2 :trailing arg";

	mock_reset_io();


@@ 123,8 117,6 @@ test_irc_generic_ignore(void)
static void
test_irc_generic_info(void)
{
	server_reset(s);

	char m1[] = "COMMAND arg1 arg2 :trailing arg";

	mock_reset_io();


@@ 139,8 131,6 @@ test_irc_generic_info(void)
static void
test_irc_generic_unknown(void)
{
	server_reset(s);

	char m1[] = "COMMAND arg1 arg2 :trailing arg";

	mock_reset_io();


@@ 153,6 143,66 @@ test_irc_generic_unknown(void)
}

static void
test_irc_numeric_001(void)
{
	/* 001 :<Welcome message> */

	/* test registered */
	mock_reset_io();
	mock_reset_state();

	assert_eq(s->registered, 0);

	CHECK_RECV("001 me", 0, 1, 3);
	assert_strcmp(mock_chan[0], "host");
	assert_strcmp(mock_line[0], "You are known as me");
	assert_strcmp(mock_send[0], "JOIN #c1");
	assert_strcmp(mock_send[1], "JOIN #c2");
	assert_strcmp(mock_send[2], "JOIN #c3");

	assert_eq(s->registered, 1);

	/* test welcome message */
	mock_reset_io();
	mock_reset_state();

	CHECK_RECV("001 me :welcome message", 0, 2, 3);
	assert_strcmp(mock_chan[0], "host");
	assert_strcmp(mock_line[0], "welcome message");
	assert_strcmp(mock_line[1], "You are known as me");
	assert_strcmp(mock_send[0], "JOIN #c1");
	assert_strcmp(mock_send[1], "JOIN #c2");
	assert_strcmp(mock_send[2], "JOIN #c3");

	/* test user modes */
	mock_reset_io();
	mock_reset_state();

	s->mode = strdup("abc");

	CHECK_RECV("001 me", 0, 1, 4);
	assert_strcmp(mock_chan[0], "host");
	assert_strcmp(mock_line[0], "You are known as me");
	assert_strcmp(mock_send[0], "MODE me +abc");
	assert_strcmp(mock_send[1], "JOIN #c1");
	assert_strcmp(mock_send[2], "JOIN #c2");
	assert_strcmp(mock_send[3], "JOIN #c3");

	/* test parted channels aren't auto joined */
	mock_reset_io();
	mock_reset_state();

	c2->parted = 1;

	CHECK_RECV("001 me", 0, 1, 3);
	assert_strcmp(mock_chan[0], "host");
	assert_strcmp(mock_line[0], "You are known as me");
	assert_strcmp(mock_send[0], "MODE me +abc");
	assert_strcmp(mock_send[1], "JOIN #c1");
	assert_strcmp(mock_send[2], "JOIN #c3");
}

static void
test_irc_numeric_353(void)
{
	/* 353 <nick> <type> <channel> 1*(<modes><nick>) */


@@ 162,9 212,6 @@ test_irc_numeric_353(void)
	struct user *u3;
	struct user *u4;

	channel_reset(c1);
	server_reset(s);

	/* test errors */
	CHECK_RECV("353 me", 1, 1, 0);
	assert_strcmp(mock_chan[0], "host");


@@ 181,7 228,7 @@ test_irc_numeric_353(void)
	assert_strcmp(mock_line[0], "RPL_NAMEREPLY: channel '#x' not found");

	CHECK_RECV("353 me x #c1 :n1", 1, 1, 0);
	assert_strcmp(mock_line[0], "RPL_NAMEREPLY: invalid channel flag: 'x'");
	assert_strcmp(mock_line[0], "RPL_NAMEREPLY: invalid channel type: 'x'");

	CHECK_RECV("353 me = #c1 :+@", 1, 1, 0);
	assert_strcmp(mock_line[0], "RPL_NAMEREPLY: invalid nick: '+@'");


@@ 208,8 255,8 @@ test_irc_numeric_353(void)
	 || !(u3 = user_list_get(&(c1->users), CASEMAPPING_RFC1459, "n3", 0)))
		test_abort("Failed to retrieve users");

	assert_eq(u1->prfxmodes.lower, (flag_bit('o')));
	assert_eq(u2->prfxmodes.lower, (flag_bit('v')));
	assert_eq(u1->prfxmodes.lower, (mode_bit('o')));
	assert_eq(u2->prfxmodes.lower, (mode_bit('v')));
	assert_eq(u3->prfxmodes.lower, 0);

	/* test multiple nicks, multiprefix enabled */


@@ 228,10 275,10 @@ test_irc_numeric_353(void)
	assert_eq(u2->prfxmodes.prefix, '+');
	assert_eq(u3->prfxmodes.prefix, '@');
	assert_eq(u4->prfxmodes.prefix, '@');
	assert_eq(u1->prfxmodes.lower, (flag_bit('o')));
	assert_eq(u2->prfxmodes.lower, (flag_bit('v')));
	assert_eq(u3->prfxmodes.lower, (flag_bit('o') | flag_bit('v')));
	assert_eq(u4->prfxmodes.lower, (flag_bit('o') | flag_bit('v')));
	assert_eq(u1->prfxmodes.lower, (mode_bit('o')));
	assert_eq(u2->prfxmodes.lower, (mode_bit('v')));
	assert_eq(u3->prfxmodes.lower, (mode_bit('o') | mode_bit('v')));
	assert_eq(u4->prfxmodes.lower, (mode_bit('o') | mode_bit('v')));
}

static void


@@ 239,8 286,6 @@ 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");


@@ 277,8 322,6 @@ 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");


@@ 313,8 356,6 @@ test_irc_numeric_403(void)
static void
test_recv(void)
{
	server_reset(s);

	/* test unhandled command type */
	CHECK_RECV("UNHANDLED", 0, 1, 0);
	assert_strcmp(mock_chan[0], "host");


@@ 326,8 367,6 @@ test_recv_error(void)
{
	/* ERROR :<message> */

	server_reset(s);

	CHECK_RECV("ERROR", 1, 1, 0);
	assert_strcmp(mock_chan[0], "host");
	assert_strcmp(mock_line[0], "ERROR: message is null");


@@ 349,9 388,6 @@ test_recv_invite(void)
{
	/* :nick!user@host INVITE <nick> <channel> */

	channel_reset(c1);
	server_reset(s);

	CHECK_RECV("INVITE nick channel", 1, 1, 0);
	assert_strcmp(mock_chan[0], "host");
	assert_strcmp(mock_line[0], "INVITE: sender's nick is null");


@@ 380,10 416,6 @@ test_recv_join(void)
	/* :nick!user@host JOIN <channel>
	 * :nick!user@host JOIN <channel> <account> :<realname> */

	channel_reset(c1);
	channel_reset(c2);
	server_reset(s);

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



@@ 443,15 475,6 @@ test_recv_kick(void)
{
	/* :nick!user@host KICK <channel> <user> [:message] */

	channel_reset(c1);
	channel_reset(c2);
	channel_reset(c3);
	server_reset(s);

	c1->parted = 0;
	c2->parted = 0;
	c3->parted = 0;

	assert_eq(user_list_add(&(c1->users), CASEMAPPING_RFC1459, "nick1", MODE_EMPTY), USER_ERR_NONE);
	assert_eq(user_list_add(&(c1->users), CASEMAPPING_RFC1459, "nick2", MODE_EMPTY), USER_ERR_NONE);
	assert_eq(user_list_add(&(c1->users), CASEMAPPING_RFC1459, "nick3", MODE_EMPTY), USER_ERR_NONE);


@@ 527,11 550,6 @@ test_recv_nick(void)
{
	/* :nick!user@host NICK <nick> */

	channel_reset(c1);
	channel_reset(c2);
	channel_reset(c3);
	server_reset(s);

	assert_eq(user_list_add(&(c1->users), CASEMAPPING_RFC1459, "nick1", MODE_EMPTY), USER_ERR_NONE);
	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);


@@ 585,8 603,6 @@ test_recv_notice(void)
static void
test_recv_numeric(void)
{
	server_reset(s);

	CHECK_RECV(":hostname 0 * arg :trailing arg", 1, 1, 0);
	assert_strcmp(mock_line[0], "NUMERIC: '0' invalid");



@@ 625,15 641,6 @@ test_recv_part(void)
{
	/* :nick!user@host PART <channel> [:message] */

	channel_reset(c1);
	channel_reset(c2);
	channel_reset(c3);
	server_reset(s);

	c1->parted = 0;
	c2->parted = 0;
	c3->parted = 0;

	assert_eq(user_list_add(&(c1->users), CASEMAPPING_RFC1459, "nick1", MODE_EMPTY), USER_ERR_NONE);
	assert_eq(user_list_add(&(c1->users), CASEMAPPING_RFC1459, "nick2", MODE_EMPTY), USER_ERR_NONE);
	assert_eq(user_list_add(&(c1->users), CASEMAPPING_RFC1459, "nick3", MODE_EMPTY), USER_ERR_NONE);


@@ 703,8 710,6 @@ test_recv_ping(void)
{
	/* PING <server> */

	server_reset(s);

	CHECK_RECV("PING", 1, 1, 0);
	assert_strcmp(mock_chan[0], "host");
	assert_strcmp(mock_line[0], "PING: server is null");


@@ 718,8 723,6 @@ test_recv_pong(void)
{
	/* PONG <server> [<server2>] */

	server_reset(s);

	CHECK_RECV("PONG", 0, 0, 0);
	CHECK_RECV("PONG s1", 0, 0, 0);
	CHECK_RECV("PONG s1 s2", 0, 0, 0);


@@ 736,11 739,6 @@ test_recv_quit(void)
{
	/* :nick!user@host QUIT [:message] */

	channel_reset(c1);
	channel_reset(c2);
	channel_reset(c3);
	server_reset(s);

	assert_eq(user_list_add(&(c1->users), CASEMAPPING_RFC1459, "nick1", MODE_EMPTY), USER_ERR_NONE);
	assert_eq(user_list_add(&(c1->users), CASEMAPPING_RFC1459, "nick2", MODE_EMPTY), USER_ERR_NONE);
	assert_eq(user_list_add(&(c1->users), CASEMAPPING_RFC1459, "nick3", MODE_EMPTY), USER_ERR_NONE);


@@ 794,9 792,6 @@ test_recv_topic(void)
{
	/* :nick!user@host TOPIC <channel> [:topic] */

	channel_reset(c1);
	server_reset(s);

	CHECK_RECV("TOPIC #c1 message", 1, 1, 0);
	assert_strcmp(mock_chan[0], "host");
	assert_strcmp(mock_line[0], "TOPIC: sender's nick is null");


@@ 832,8 827,6 @@ test_recv_ircv3_cap(void)
{
	/* Full IRCv3 CAP coverage in test/handlers/ircv3.c */

	server_reset(s);

	s->registered = 1;

	CHECK_RECV("CAP * LS :cap-1 cap-2 cap-3", 0, 1, 0);


@@ 846,11 839,6 @@ test_recv_ircv3_account(void)
{
	/* :nick!user@host ACCOUNT <account> */

	channel_reset(c1);
	channel_reset(c2);
	channel_reset(c3);
	server_reset(s);

	assert_eq(user_list_add(&(c1->users), CASEMAPPING_RFC1459, "nick1", MODE_EMPTY), USER_ERR_NONE);
	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);


@@ 892,11 880,6 @@ test_recv_ircv3_away(void)
{
	/* :nick!user@host AWAY [:message] */

	channel_reset(c1);
	channel_reset(c2);
	channel_reset(c3);
	server_reset(s);

	assert_eq(user_list_add(&(c1->users), CASEMAPPING_RFC1459, "nick1", MODE_EMPTY), USER_ERR_NONE);
	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);


@@ 935,11 918,6 @@ test_recv_ircv3_chghost(void)
{
	/* :nick!user@host CHGHOST new_user new_host */

	channel_reset(c1);
	channel_reset(c2);
	channel_reset(c3);
	server_reset(s);

	assert_eq(user_list_add(&(c1->users), CASEMAPPING_RFC1459, "nick1", MODE_EMPTY), USER_ERR_NONE);
	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);


@@ 975,7 953,7 @@ test_recv_ircv3_chghost(void)
static int
test_init(void)
{
	s = server("host", "port", NULL, "user", "real");
	s = server("host", "port", NULL, "user", "real", NULL);

	c1 = channel("#c1", CHANNEL_T_CHANNEL);
	c2 = channel("#c2", CHANNEL_T_CHANNEL);


@@ 1018,6 996,7 @@ main(void)
		TESTCASE(test_irc_generic_ignore),
		TESTCASE(test_irc_generic_info),
		TESTCASE(test_irc_generic_unknown),
		TESTCASE(test_irc_numeric_001),
		TESTCASE(test_irc_numeric_353),
		TESTCASE(test_irc_numeric_401),
		TESTCASE(test_irc_numeric_403),

M test/handlers/irc_send.c => test/handlers/irc_send.c +1 -1
@@ 465,7 465,7 @@ test_send_ircv3_cap_list(void)
static int
test_init(void)
{
	s = server("h1", "p1", NULL, "u1", "r1");
	s = server("h1", "p1", NULL, "u1", "r1", NULL);

	c_serv = s->channel;


M test/handlers/ircv3.c => test/handlers/ircv3.c +485 -40
@@ 42,50 42,50 @@ mock_reset(void)
}

static void
test_ircv3_CAP(void)
test_ircv3_recv_CAP(void)
{
	mock_reset();
	IRC_MESSAGE_PARSE("CAP");
	assert_eq(irc_recv(s, &m), 1);
	assert_eq(irc_recv(s, &m), -1);
	assert_strcmp(mock_line[0], "CAP: target is null");

	mock_reset();
	IRC_MESSAGE_PARSE("CAP *");
	assert_eq(irc_recv(s, &m), 1);
	assert_eq(irc_recv(s, &m), -1);
	assert_strcmp(mock_line[0], "CAP: command is null");

	mock_reset();
	IRC_MESSAGE_PARSE("CAP * ack");
	assert_eq(irc_recv(s, &m), 1);
	assert_eq(irc_recv(s, &m), -1);
	assert_strcmp(mock_line[0], "CAP: unrecognized subcommand 'ack'");

	mock_reset();
	IRC_MESSAGE_PARSE("CAP * xxx");
	assert_eq(irc_recv(s, &m), 1);
	assert_eq(irc_recv(s, &m), -1);
	assert_strcmp(mock_line[0], "CAP: unrecognized subcommand 'xxx'");
}

static void
test_ircv3_CAP_LS(void)
test_ircv3_recv_CAP_LS(void)
{
	/* test empty LS, no parameter */
	mock_reset();
	IRC_MESSAGE_PARSE("CAP * LS");
	assert_eq(irc_recv(s, &m), 1);
	assert_eq(irc_recv(s, &m), -1);
	assert_eq(mock_send_n, 0);
	assert_strcmp(mock_line[0], "CAP LS: parameter is null");

	/* test empty LS, no parameter, multiline */
	mock_reset();
	IRC_MESSAGE_PARSE("CAP * LS *");
	assert_eq(irc_recv(s, &m), 1);
	assert_eq(irc_recv(s, &m), -1);
	assert_eq(mock_send_n, 0);
	assert_strcmp(mock_line[0], "CAP LS: parameter is null");

	/* test multiple parameters, no '*' */
	mock_reset();
	IRC_MESSAGE_PARSE("CAP * LS cap-1 cap-2");
	assert_eq(irc_recv(s, &m), 1);
	assert_eq(irc_recv(s, &m), -1);
	assert_eq(mock_send_n, 0);
	assert_strcmp(mock_line[0], "CAP LS: invalid parameters");



@@ 135,6 135,20 @@ test_ircv3_CAP_LS(void)
	assert_eq(s->ircv3_caps.cap_2.supported, 1);
	assert_eq(s->ircv3_caps.cap_3.supported, 1);

	/* test cap key=val */
	mock_reset();
	ircv3_caps_reset(&(s->ircv3_caps));
	IRC_MESSAGE_PARSE("CAP * LS :cap-1=foo cap-3 cap-4=bar");
	assert_eq(irc_recv(s, &m), 0);
	assert_eq(mock_send_n, 1);
	assert_strcmp(mock_send[0], "CAP REQ :cap-1 cap-3 cap-4");
	assert_eq(s->ircv3_caps.cap_1.supported, 1);
	assert_eq(s->ircv3_caps.cap_3.supported, 1);
	assert_eq(s->ircv3_caps.cap_4.supported, 1);
	assert_strcmp(s->ircv3_caps.cap_1.val, "foo");
	assert_strcmp(s->ircv3_caps.cap_3.val, NULL);
	assert_strcmp(s->ircv3_caps.cap_4.val, "bar");

	/* test multiline */
	mock_reset();
	ircv3_caps_reset(&(s->ircv3_caps));


@@ 164,12 178,12 @@ test_ircv3_CAP_LS(void)
}

static void
test_ircv3_CAP_LIST(void)
test_ircv3_recv_CAP_LIST(void)
{
	/* test empty LIST, no parameter */
	mock_reset();
	IRC_MESSAGE_PARSE("CAP * LIST");
	assert_eq(irc_recv(s, &m), 1);
	assert_eq(irc_recv(s, &m), -1);
	assert_eq(mock_send_n, 0);
	assert_eq(mock_line_n, 1);
	assert_strcmp(mock_line[0], "CAP LIST: parameter is null");


@@ 177,7 191,7 @@ test_ircv3_CAP_LIST(void)
	/* test empty LIST, no parameter, multiline */
	mock_reset();
	IRC_MESSAGE_PARSE("CAP * LIST *");
	assert_eq(irc_recv(s, &m), 1);
	assert_eq(irc_recv(s, &m), -1);
	assert_eq(mock_send_n, 0);
	assert_eq(mock_line_n, 1);
	assert_strcmp(mock_line[0], "CAP LIST: parameter is null");


@@ 185,7 199,7 @@ test_ircv3_CAP_LIST(void)
	/* test multiple parameters, no '*' */
	mock_reset();
	IRC_MESSAGE_PARSE("CAP * LIST cap-1 cap-2");
	assert_eq(irc_recv(s, &m), 1);
	assert_eq(irc_recv(s, &m), -1);
	assert_eq(mock_send_n, 0);
	assert_eq(mock_line_n, 1);
	assert_strcmp(mock_line[0], "CAP LIST: invalid parameters");


@@ 236,12 250,12 @@ test_ircv3_CAP_LIST(void)
}

static void
test_ircv3_CAP_ACK(void)
test_ircv3_recv_CAP_ACK(void)
{
	/* test empty ACK */
	mock_reset();
	IRC_MESSAGE_PARSE("CAP * ACK");
	assert_eq(irc_recv(s, &m), 1);
	assert_eq(irc_recv(s, &m), -1);
	assert_eq(mock_send_n, 0);
	assert_eq(mock_line_n, 1);
	assert_strcmp(mock_line[0], "CAP ACK: parameter is null");


@@ 249,19 263,35 @@ test_ircv3_CAP_ACK(void)
	/* test empty ACK, with leading ':' */
	mock_reset();
	IRC_MESSAGE_PARSE("CAP * ACK :");
	assert_eq(irc_recv(s, &m), 1);
	assert_eq(irc_recv(s, &m), -1);
	assert_eq(mock_send_n, 0);
	assert_eq(mock_line_n, 1);
	assert_strcmp(mock_line[0], "CAP ACK: parameter is empty");

	/* unregisterd, error */
	/* test ack key=val */
	mock_reset();
	s->ircv3_caps.cap_1.req = 1;
	s->ircv3_caps.cap_2.req = 1;
	s->ircv3_caps.cap_3.req = 1;
	s->ircv3_caps.cap_1.val = strdup("foo");
	s->ircv3_caps.cap_3.val = strdup("bar");
	IRC_MESSAGE_PARSE("CAP * ACK :cap-1 cap-2 cap-3");
	assert_eq(irc_recv(s, &m), 0);
	assert_eq(mock_send_n, 1);
	assert_eq(mock_line_n, 3);
	assert_strcmp(mock_line[0], "capability change accepted: cap-1=foo");
	assert_strcmp(mock_line[1], "capability change accepted: cap-2");
	assert_strcmp(mock_line[2], "capability change accepted: cap-3=bar");
	assert_strcmp(mock_send[0], "CAP END");

	/* test unregisterd, error */
	mock_reset();
	s->ircv3_caps.cap_1.set = 0;
	s->ircv3_caps.cap_2.set = 0;
	s->ircv3_caps.cap_1.req = 1;
	s->ircv3_caps.cap_2.req = 1;
	IRC_MESSAGE_PARSE("CAP * ACK :cap-1 cap-aaa cap-2 cap-bbb");
	assert_eq(irc_recv(s, &m), 1);
	assert_eq(irc_recv(s, &m), -1);
	assert_eq(mock_send_n, 0);
	assert_eq(mock_line_n, 5);
	assert_strcmp(mock_line[0], "capability change accepted: cap-1");


@@ 270,7 300,7 @@ test_ircv3_CAP_ACK(void)
	assert_strcmp(mock_line[3], "CAP ACK: 'cap-bbb' not supported");
	assert_strcmp(mock_line[4], "CAP ACK: parameter errors");

	/* unregistered, no error */
	/* test unregistered, no error */
	mock_reset();
	s->ircv3_caps.cap_1.set = 0;
	s->ircv3_caps.cap_2.set = 1;


@@ 293,7 323,7 @@ test_ircv3_CAP_ACK(void)
	assert_strcmp(mock_line[2], "capability change accepted: -cap-3");
	assert_strcmp(mock_send[0], "CAP END");

	/* registered, error */
	/* test registered, error */
	mock_reset();
	s->registered = 1;
	s->ircv3_caps.cap_1.set = 0;


@@ 305,7 335,7 @@ test_ircv3_CAP_ACK(void)
	s->ircv3_caps.cap_3.req = 1;
	s->ircv3_caps.cap_4.req = 0;
	IRC_MESSAGE_PARSE("CAP * ACK :cap-1 cap-2 -cap-3 -cap-4");
	assert_eq(irc_recv(s, &m), 1);
	assert_eq(irc_recv(s, &m), -1);
	assert_eq(mock_send_n, 0);
	assert_eq(mock_line_n, 5);
	assert_strcmp(mock_line[0], "capability change accepted: cap-1");


@@ 314,7 344,7 @@ test_ircv3_CAP_ACK(void)
	assert_strcmp(mock_line[3], "CAP ACK: '-cap-4' was not requested");
	assert_strcmp(mock_line[4], "CAP ACK: parameter errors");

	/* registered, no error */
	/* test registered, no error */
	mock_reset();
	s->registered = 1;
	s->ircv3_caps.cap_1.set = 0;


@@ 332,12 362,12 @@ test_ircv3_CAP_ACK(void)
}

static void
test_ircv3_CAP_NAK(void)
test_ircv3_recv_CAP_NAK(void)
{
	/* test empty NAK */
	mock_reset();
	IRC_MESSAGE_PARSE("CAP * NAK");
	assert_eq(irc_recv(s, &m), 1);
	assert_eq(irc_recv(s, &m), -1);
	assert_eq(mock_send_n, 0);
	assert_eq(mock_line_n, 1);
	assert_strcmp(mock_line[0], "CAP NAK: parameter is null");


@@ 345,7 375,7 @@ test_ircv3_CAP_NAK(void)
	/* test empty NAK, with leading ':' */
	mock_reset();
	IRC_MESSAGE_PARSE("CAP * NAK :");
	assert_eq(irc_recv(s, &m), 1);
	assert_eq(irc_recv(s, &m), -1);
	assert_eq(mock_send_n, 0);
	assert_eq(mock_line_n, 1);
	assert_strcmp(mock_line[0], "CAP NAK: parameter is empty");


@@ 406,12 436,12 @@ test_ircv3_CAP_NAK(void)
}

static void
test_ircv3_CAP_DEL(void)
test_ircv3_recv_CAP_DEL(void)
{
	/* test empty DEL */
	mock_reset();
	IRC_MESSAGE_PARSE("CAP * DEL");
	assert_eq(irc_recv(s, &m), 1);
	assert_eq(irc_recv(s, &m), -1);
	assert_eq(mock_send_n, 0);
	assert_eq(mock_line_n, 1);
	assert_strcmp(mock_line[0], "CAP DEL: parameter is null");


@@ 419,7 449,7 @@ test_ircv3_CAP_DEL(void)
	/* test empty DEL, with leading ':' */
	mock_reset();
	IRC_MESSAGE_PARSE("CAP * DEL :");
	assert_eq(irc_recv(s, &m), 1);
	assert_eq(irc_recv(s, &m), -1);
	assert_eq(mock_send_n, 0);
	assert_eq(mock_line_n, 1);
	assert_strcmp(mock_line[0], "CAP DEL: parameter is empty");


@@ 427,7 457,7 @@ test_ircv3_CAP_DEL(void)
	/* test cap-7 doesn't support DEL */
	mock_reset();
	IRC_MESSAGE_PARSE("CAP * DEL :cap-7");
	assert_eq(irc_recv(s, &m), 1);
	assert_eq(irc_recv(s, &m), -1);
	assert_eq(mock_send_n, 0);
	assert_eq(mock_line_n, 1);
	assert_strcmp(mock_line[0], "CAP DEL: 'cap-7' doesn't support DEL");


@@ 462,12 492,12 @@ test_ircv3_CAP_DEL(void)
}

static void
test_ircv3_CAP_NEW(void)
test_ircv3_recv_CAP_NEW(void)
{
	/* test empty NEW */
	mock_reset();
	IRC_MESSAGE_PARSE("CAP * NEW");
	assert_eq(irc_recv(s, &m), 1);
	assert_eq(irc_recv(s, &m), -1);
	assert_eq(mock_send_n, 0);
	assert_eq(mock_line_n, 1);
	assert_strcmp(mock_line[0], "CAP NEW: parameter is null");


@@ 475,7 505,7 @@ test_ircv3_CAP_NEW(void)
	/* test empty DEL, with leading ':' */
	mock_reset();
	IRC_MESSAGE_PARSE("CAP * NEW :");
	assert_eq(irc_recv(s, &m), 1);
	assert_eq(irc_recv(s, &m), -1);
	assert_eq(mock_send_n, 0);
	assert_eq(mock_line_n, 1);
	assert_strcmp(mock_line[0], "CAP NEW: parameter is empty");


@@ 550,6 580,312 @@ test_ircv3_CAP_NEW(void)
	assert_eq(s->ircv3_caps.cap_7.supported, 0);
}


static void
test_ircv3_recv_AUTHENTICATE(void)
{
	IRC_MESSAGE_PARSE("AUTHENTICATE *");
	assert_eq(irc_recv(s, &m), -1);
	assert_eq(mock_send_n, 0);
	assert_eq(mock_line_n, 1);
	assert_strcmp(mock_line[0], "AUTHENTICATE: no SASL mechanism");

	s->ircv3_sasl.mech = -1;

	IRC_MESSAGE_PARSE("AUTHENTICATE *");
	assert_fatal(irc_recv(s, &m));
}

static void
test_ircv3_recv_AUTHENTICATE_EXTERNAL(void)
{
	server_set_sasl(s, "external", NULL, NULL);

	IRC_MESSAGE_PARSE("AUTHENTICATE *");
	assert_eq(irc_recv(s, &m), -1);
	assert_eq(mock_send_n, 0);
	assert_eq(mock_line_n, 1);
	assert_strcmp(mock_line[0], "Invalid SASL state for mechanism EXTERNAL: 0");

	assert_eq(ircv3_sasl_init(s), 0);
	assert_eq(mock_send_n, 1);
	assert_eq(mock_line_n, 1);
	assert_strcmp(mock_send[0], "AUTHENTICATE EXTERNAL");

	IRC_MESSAGE_PARSE("AUTHENTICATE");
	assert_eq(irc_recv(s, &m), -1);
	assert_eq(mock_send_n, 1);
	assert_eq(mock_line_n, 2);
	assert_strcmp(mock_line[1], "Invalid SASL response for mechanism EXTERNAL: response is null");

	IRC_MESSAGE_PARSE("AUTHENTICATE *");
	assert_eq(irc_recv(s, &m), -1);
	assert_eq(mock_send_n, 1);
	assert_eq(mock_line_n, 3);
	assert_strcmp(mock_line[2], "Invalid SASL response for mechanism EXTERNAL: '*'");

	IRC_MESSAGE_PARSE("AUTHENTICATE +");
	assert_eq(irc_recv(s, &m), 0);
	assert_eq(mock_send_n, 2);
	assert_eq(mock_line_n, 3);
	assert_strcmp(mock_send[1], "AUTHENTICATE +");
}

static void
test_ircv3_recv_AUTHENTICATE_PLAIN(void)
{
	server_set_sasl(s, "plain", NULL, "pass");

	IRC_MESSAGE_PARSE("AUTHENTICATE *");
	assert_eq(irc_recv(s, &m), -1);
	assert_eq(mock_send_n, 0);
	assert_eq(mock_line_n, 1);
	assert_strcmp(mock_line[0], "SASL mechanism PLAIN requires a username");

	server_set_sasl(s, "plain", "user", NULL);

	IRC_MESSAGE_PARSE("AUTHENTICATE *");
	assert_eq(irc_recv(s, &m), -1);
	assert_eq(mock_send_n, 0);
	assert_eq(mock_line_n, 2);
	assert_strcmp(mock_line[1], "SASL mechanism PLAIN requires a password");

	server_set_sasl(s, "plain", "user", "pass");

	IRC_MESSAGE_PARSE("AUTHENTICATE *");
	assert_eq(irc_recv(s, &m), -1);
	assert_eq(mock_send_n, 0);
	assert_eq(mock_line_n, 3);
	assert_strcmp(mock_line[2], "Invalid SASL state for mechanism PLAIN: 0");

	assert_eq(ircv3_sasl_init(s), 0);
	assert_eq(mock_send_n, 1);
	assert_eq(mock_line_n, 3);
	assert_strcmp(mock_send[0], "AUTHENTICATE PLAIN");

	IRC_MESSAGE_PARSE("AUTHENTICATE");
	assert_eq(irc_recv(s, &m), -1);
	assert_eq(mock_send_n, 1);
	assert_eq(mock_line_n, 4);
	assert_strcmp(mock_line[3], "Invalid SASL response for mechanism PLAIN: response is null");

	IRC_MESSAGE_PARSE("AUTHENTICATE *");
	assert_eq(irc_recv(s, &m), -1);
	assert_eq(mock_send_n, 1);
	assert_eq(mock_line_n, 5);
	assert_strcmp(mock_line[4], "Invalid SASL response for mechanism PLAIN: '*'");

	IRC_MESSAGE_PARSE("AUTHENTICATE +");
	assert_eq(irc_recv(s, &m), 0);
	assert_eq(mock_send_n, 2);
	assert_eq(mock_line_n, 5);
	assert_strcmp(mock_send[1], "AUTHENTICATE dXNlcgB1c2VyAHBhc3M=");

	char user1[150] = {0};
	char user2[147] = {0};

	memset(user1, 'x', sizeof(user1) - 1);
	memset(user2, 'x', sizeof(user2) - 1);

	server_set_sasl(s, "plain", user1, "pass");

	IRC_MESSAGE_PARSE("AUTHENTICATE +");
	assert_eq(irc_recv(s, &m), -1);
	assert_eq(mock_send_n, 2);
	assert_eq(mock_line_n, 6);
	assert_strcmp(mock_line[5], "SASL decoded auth message too long");

	server_set_sasl(s, "plain", user2, "pass");

	IRC_MESSAGE_PARSE("AUTHENTICATE +");
	assert_eq(irc_recv(s, &m), -1);
	assert_eq(mock_send_n, 2);
	assert_eq(mock_line_n, 7);
	assert_strcmp(mock_line[6], "SASL encoded auth message too long");
}

static void
test_ircv3_nume