~rcr/rirc

8983c081b43b3c629836d43829a3770cca97983c — Richard Robbins 1 year, 3 months ago 0777248 + 7893d81
Merge branch 'dev' into static_analysis
M .gitignore => .gitignore +1 -1
@@ 5,11 5,11 @@
*.o
*.t
*.td
.clangd
bld
compile_commands.json
config.h
coverage
mbedtls
rirc
rirc.debug
rirc.out

M .gitmodules => .gitmodules +2 -1
@@ 1,3 1,4 @@
[submodule "mbedtls"]
	path = mbedtls
	path = lib/mbedtls
	url = https://github.com/ARMmbed/mbedtls.git
	ignore = dirty

M CHANGELOG => CHANGELOG +3 -0
@@ 8,6 8,9 @@ Summary of notable changes and features
    - add mbedtls git submodule
    - add CA_CERT_PATH define to config.h
    - changed default port to 6697 for SSL
 - add ircv3 CAP support
    - add command /cap-ls
    - add command /cap-list
 - changed standard versions
    - c99 -> c11
    - POSIX.1-2001 -> POSIX.1-2008

M LICENSE => LICENSE +1 -1
@@ 1,4 1,4 @@
Copyright (C) 2014-2019 Richard Robbins <mail@rcr.io>
Copyright (C) 2014-2020 Richard Robbins <mail@rcr.io>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

M Makefile => Makefile +20 -17
@@ 11,23 11,22 @@ BIN_DIR = /usr/local/bin
MAN_DIR = /usr/local/share/man/man1

STDS := \
 -std=c11 \
 -D_BSD_VISIBLE \
 -D_DARWIN_C_SOURCE \
 -D_POSIX_C_SOURCE=200809L

TLS_INCLUDE := \
 -I./mbedtls/include
	-std=c11 \
	-D_BSD_VISIBLE \
	-D_DARWIN_C_SOURCE \
	-D_POSIX_C_SOURCE=200809L

TLS_CONF := ./lib/mbedtls.h
TLS_LIBS := \
 ./mbedtls/library/libmbedtls.a \
 ./mbedtls/library/libmbedx509.a \
 ./mbedtls/library/libmbedcrypto.a
	./lib/mbedtls/library/libmbedtls.a \
	./lib/mbedtls/library/libmbedx509.a \
	./lib/mbedtls/library/libmbedcrypto.a

CC := cc
PP := cc -E
CFLAGS   := $(CC_EXT) -I. $(TLS_INCLUDE) $(STDS) -DVERSION=\"$(VERSION)\" -Wall -Wextra -pedantic -O2 -flto
CFLAGS_D := $(CC_EXT) -I. $(TLS_INCLUDE) $(STDS) -DVERSION=\"$(VERSION)\" -Wall -Wextra -pedantic -O0 -g -DDEBUG
CFLAGS   := $(CC_EXT) -I. $(STDS) -DVERSION=\"$(VERSION)\" -Wall -Wextra -pedantic
CFLAGS_R := $(CFLAGS) -O2 -flto -DNDEBUG
CFLAGS_D := $(CFLAGS) -O0 -g
LDFLAGS  := $(LD_EXT) -lpthread

# Build, source, test source directories


@@ 51,20 50,20 @@ OBJS_T += $(DIR_B)/utils/tree.t # Header only file
OBJS_G := $(patsubst %.gperf, %.gperf.out, $(SRC_G))

# Release build executable
$(BIN_R): $(DIR_B) $(OBJS_G) $(OBJS_R)
$(BIN_R): $(TLS_LIBS) $(DIR_B) $(OBJS_G) $(OBJS_R)
	@echo cc $@
	@$(CC) $(LDFLAGS) -o $@ $(OBJS_R) $(TLS_LIBS)

# Debug build executable
$(BIN_D): $(DIR_B) $(OBJS_G) $(OBJS_D)
$(BIN_D): $(TLS_LIBS) $(DIR_B) $(OBJS_G) $(OBJS_D)
	@echo cc $@
	@$(CC) $(LDFLAGS) -o $@ $(OBJS_D) $(TLS_LIBS)

# Release build objects
$(DIR_B)/%.o: $(DIR_S)/%.c config.h
	@echo "cc $<..."
	@$(PP) $(CFLAGS) -MM -MP -MT $@ -MF $(@:.o=.d) $<
	@$(CC) $(CFLAGS) -c -o $@ $<
	@$(PP) $(CFLAGS_R) -MM -MP -MT $@ -MF $(@:.o=.d) $<
	@$(CC) $(CFLAGS_R) -c -o $@ $<

# Debug build objects
$(DIR_B)/%.db.o: $(DIR_S)/%.c config.h


@@ 80,9 79,13 @@ $(DIR_B)/%.db.o: $(DIR_S)/%.c config.h
$(DIR_B)/%.t: $(DIR_T)/%.c
	@$(PP) $(CFLAGS_D) -MM -MP -MT $@ -MF $(@:.t=.d) $<
	@$(CC) $(CFLAGS_D) $(LDFLAGS) -o $@ $<
	-@rm -f $(@:.t=.td) && ./$@ || mv $@ $(@:.t=.td)
	-@rm -f $(@:.t=.td) && $(TEST_EXT) ./$@ || mv $@ $(@:.t=.td)
	@[ ! -f $(@:.t=.td) ]

# TLS libraries
$(TLS_LIBS): $(TLS_CONF)
	@CFLAGS="-I$(PWD) -DMBEDTLS_CONFIG_FILE='<$(TLS_CONF)>'" $(MAKE) -C ./lib/mbedtls clean lib

# Build directories
$(DIR_B):
	@for dir in $(patsubst $(DIR_S)/%, %, $(SUBDIRS)); do mkdir -p $(DIR_B)/$$dir; done

M README.md => README.md +65 -58
@@ 5,103 5,110 @@
---

<p align="center">
  <a href="https://sonarcloud.io/dashboard?id=rcr_rirc">
    <img alt="sonarcloud" src="https://sonarcloud.io/api/project_badges/measure?project=rcr_rirc&metric=ncloc"/>
  <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=rcr_rirc">
    <img alt="sonarcloud" src="https://sonarcloud.io/api/project_badges/measure?project=rcr_rirc&metric=sqale_rating"/>
  <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=rcr_rirc">
    <img alt="sonarcloud" src="https://sonarcloud.io/api/project_badges/measure?project=rcr_rirc&metric=reliability_rating"/>
  <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=rcr_rirc">
    <img alt="sonarcloud" src="https://sonarcloud.io/api/project_badges/measure?project=rcr_rirc&metric=security_rating"/>
  <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.

rirc supports only TLS connections, the default port is 6697

## Configuring:
### Configuring:

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

## Building:
### Building:

rirc requires the latest version of GNU gperf to compile.

See: https://www.gnu.org/software/gperf/

Initialize, configure and build mbedtls:

    git submodule init
    git submodule update --recursive
    cd mbedtls
    ./scripts/config.pl set MBEDTLS_THREADING_C
    ./scripts/config.pl set MBEDTLS_THREADING_PTHREAD
    cmake .
    cmake --build .
    cd ..

Build rirc:

    make
```
git submodule init
git submodule update --recursive
make
```

### Installing:

## Installing:
Default install path:

    EXE_DIR = /usr/local/bin
    MAN_DIR = /usr/local/share/man/man1
```
BIN_DIR = /usr/local/bin
MAN_DIR = /usr/local/share/man/man1
```

Edit `Makefile` to alter install path if needed, then:

    make install
```
make install
```

## Usage:
### Usage:

    rirc [-hv] [-s server [-p port] [-w pass] [-u user] [-r real] [-n nicks] [-c chans]], ...]
```
rirc [-hv] [-s server [-p port] [-w pass] [-u user] [-r real] [-n nicks] [-c chans]], ...]

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

    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
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
```

Commands:

      :clear
      :close
      :connect [host [port] [pass] [user] [real]]
      :disconnect
      :quit
```
  :clear
  :close
  :connect [host [port] [pass] [user] [real]]
  :disconnect
  :quit
```

Keys:

      ^N : go to next channel
      ^P : go to previous channel
      ^L : clear channel
      ^X : close channel
      ^F : find 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:
```
  ^N : go to next channel
  ^P : go to previous channel
  ^L : clear channel
  ^X : close channel
  ^F : find 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:

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

M config.def.h => config.def.h +1 -1
@@ 85,7 85,7 @@

/* [NETWORK] */

#define CA_CERT_PATH "/etc/ssl/"
#define CA_CERT_PATH "/etc/ssl/certs/"

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

A lib/mbedtls => lib/mbedtls +1 -0
@@ 0,0 1,1 @@
Subproject commit 0fce215851cc069c5b5def12fcc18725055fa6cf

A lib/mbedtls.h => lib/mbedtls.h +116 -0
@@ 0,0 1,116 @@
#ifndef MBEDTLS_CONFIG_H
#define MBEDTLS_CONFIG_H

/* Enabled ciphersuites, in order of preference.
 *   - Only ECHDE key exchanges, AEAD ciphers
 *   - Ordered by cipher:
 *     - ChaCha
 *     - AES-256-GCM
 *     - AES-256-CCM
 *     - AES-256-CCM_8
 *     - AES-128-GCM
 *     - AES-128-CCM
 *     - AES-128-CCM_8
 *   - Sub-ordered by authentication method:
 *     - ECDSA
 *     - RSA
 * Note: Only stream ciphers were chosen here, which may
 *       reveal the length of exchanged messages.
 */
#define MBEDTLS_SSL_CIPHERSUITES                           \
	/* ChaCha */                                           \
	MBEDTLS_TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, \
	MBEDTLS_TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,   \
	/* AES-256 */                                          \
	MBEDTLS_TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,       \
	MBEDTLS_TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,         \
	MBEDTLS_TLS_ECDHE_ECDSA_WITH_AES_256_CCM,              \
	MBEDTLS_TLS_ECDHE_ECDSA_WITH_AES_256_CCM_8,            \
	/* AES-128 */                                          \
	MBEDTLS_TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,       \
	MBEDTLS_TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,         \
	MBEDTLS_TLS_ECDHE_ECDSA_WITH_AES_128_CCM,              \
	MBEDTLS_TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8

/* Supported ECC curves */
#define MBEDTLS_ECP_DP_BP256R1_ENABLED
#define MBEDTLS_ECP_DP_BP384R1_ENABLED
#define MBEDTLS_ECP_DP_BP512R1_ENABLED
#define MBEDTLS_ECP_DP_SECP256K1_ENABLED
#define MBEDTLS_ECP_DP_SECP256R1_ENABLED
#define MBEDTLS_ECP_DP_SECP384R1_ENABLED
#define MBEDTLS_ECP_DP_SECP521R1_ENABLED
#define MBEDTLS_ECP_DP_CURVE25519_ENABLED
#define MBEDTLS_ECP_DP_CURVE448_ENABLED

/* System support */
#define MBEDTLS_DEPRECATED_REMOVED
#define MBEDTLS_HAVE_ASM
#define MBEDTLS_HAVE_TIME
#define MBEDTLS_HAVE_TIME_DATE
#define MBEDTLS_REMOVE_3DES_CIPHERSUITES
#define MBEDTLS_REMOVE_ARC4_CIPHERSUITES
#define MBEDTLS_THREADING_C
#define MBEDTLS_THREADING_PTHREAD

/* Ciphersuite elements */
#define MBEDTLS_CCM_C
#define MBEDTLS_CHACHA20_C
#define MBEDTLS_CHACHAPOLY_C
#define MBEDTLS_GCM_C
#define MBEDTLS_KEY_EXCHANGE_ECDHE_ECDSA_ENABLED
#define MBEDTLS_KEY_EXCHANGE_ECDHE_RSA_ENABLED
#define MBEDTLS_POLY1305_C

/* TLS 1.2 client */
#define MBEDTLS_SSL_CLI_C
#define MBEDTLS_SSL_PROTO_TLS1_2

/* TLS modules */
#define MBEDTLS_AESNI_C
#define MBEDTLS_AES_C
#define MBEDTLS_ASN1_PARSE_C
#define MBEDTLS_ASN1_WRITE_C
#define MBEDTLS_BASE64_C
#define MBEDTLS_BIGNUM_C
#define MBEDTLS_CERTS_C
#define MBEDTLS_CIPHER_C
#define MBEDTLS_CTR_DRBG_C
#define MBEDTLS_ECDH_C
#define MBEDTLS_ECDSA_C
#define MBEDTLS_ECP_C
#define MBEDTLS_ENTROPY_C
#define MBEDTLS_FS_IO
#define MBEDTLS_GENPRIME
#define MBEDTLS_HMAC_DRBG_C
#define MBEDTLS_MD_C
#define MBEDTLS_NET_C
#define MBEDTLS_OID_C
#define MBEDTLS_PEM_PARSE_C
#define MBEDTLS_PKCS1_V15
#define MBEDTLS_PK_C
#define MBEDTLS_PK_PARSE_C
#define MBEDTLS_RSA_C
#define MBEDTLS_SHA1_C
#define MBEDTLS_SHA256_C
#define MBEDTLS_SHA512_C
#define MBEDTLS_SSL_TLS_C
#define MBEDTLS_X509_CRT_PARSE_C
#define MBEDTLS_X509_USE_C

/* TLS extensions */
#define MBEDTLS_SSL_EXTENDED_MASTER_SECRET /* RFC 7627 */
#define MBEDTLS_SSL_SERVER_NAME_INDICATION /* RFC 6066 */

/* Crypto features */
#define MBEDTLS_ECDSA_DETERMINISTIC
#define MBEDTLS_ECP_NIST_OPTIM
#define MBEDTLS_X509_CHECK_EXTENDED_KEY_USAGE
#define MBEDTLS_X509_CHECK_KEY_USAGE

/* Error strings */
#define MBEDTLS_ERROR_C

#include "mbedtls/check_config.h"

#endif

D mbedtls => mbedtls +0 -1
@@ 1,1 0,0 @@
Subproject commit 0fce215851cc069c5b5def12fcc18725055fa6cf

M scripts/coverage.sh => scripts/coverage.sh +20 -6
@@ 5,8 5,8 @@ set -e
CDIR="coverage"

export CC=gcc
export CC_EXT=-"fprofile-arcs -ftest-coverage"
export LD_EXT=-"fprofile-arcs"
export CC_EXT="-fprofile-arcs -ftest-coverage"
export LD_EXT="-fprofile-arcs"

make -e clean test



@@ 18,22 18,36 @@ find . -name "*.gcda" -print0 | xargs -0 -I % mv % $CDIR

FILTER=$(cat <<'EOF'
{
	if ($p) {
	if (eof()) {
		$cov = ($lc / $lt) * 100.0;
		printf("~\n");
		printf("~ total %21d/%d %7.2f%%\n", $lc, $lt, $cov);
	} elsif ($p) {
		chomp $file;
		chomp $_;
		$file =~ s/'//g;
		my @s1 = split / /, $file;
		my @s2 = split /:/, $_;
		my @s3 = split / /, $s2[1];
		printf("%-28s%5s: %7s\n", $s1[1], $s3[2], $s3[0]);
		chop($s3[0]);
		printf("%-30s%4s %7s%%\n", $s1[1], $s3[2], $s3[0]);
		$lt = $lt + $s3[2];
		$lc = $lc + $s3[2] * ($s3[0] / 100.0);
		$p = 0;
	}
	$p++ if /^File.*src/;
	$p++ if /^File.*src.*c'/;
	$file = $_;
}
EOF
)

gcov -pr $CDIR/*.gcno | perl -ne "$FILTER" | grep -v 'gperf' | sort
echo "~ Coverage:"

gcov -pr $CDIR/*.gcno | perl -ne "$FILTER" | sort

find . -name "*gperf*.gcov" -print0 | xargs -0 -I % rm %
find . -name "*test#*.gcov" -print0 | xargs -0 -I % rm %

if [ -x "$(command -v gcovr)" ]; then
	gcovr -r . --html --html-details --filter "src.*c$" -o $CDIR/index.html
fi

M src/components/README.md => src/components/README.md +4 -0
@@ 34,6 34,10 @@ Stateful component hierarchy
       |
       |__connection
       |
       |__ircv3_caps
       |   |
       |   |__*ircv3_cap
       |
       |__mode
       |__mode_str
       |__mode_cfg

M src/components/buffer.c => src/components/buffer.c +28 -0
@@ 187,3 187,31 @@ buffer(struct buffer *b)

	memset(b, 0, sizeof(*b));
}

void
buffer_line_split(
	struct buffer_line *line,
	unsigned *head_w,
	unsigned *text_w,
	unsigned cols,
	unsigned pad)
{
	unsigned _head_w = sizeof(" HH:MM   "VERTICAL_SEPARATOR" ");

	if (BUFFER_PADDING)
		_head_w += pad;
	else
		_head_w += line->from_len;

	/* If header won't fit, split in half */
	if (_head_w >= cols)
		_head_w = cols / 2;

	_head_w -= 1;

	if (head_w)
		*head_w = _head_w;

	if (text_w)
		*text_w = cols - _head_w + 1;
}

M src/components/buffer.h => src/components/buffer.h +8 -0
@@ 76,4 76,12 @@ void buffer_newline(
	size_t,
	char);

void
buffer_line_split(
	struct buffer_line *line,
	unsigned *head_w,
	unsigned *text_w,
	unsigned cols,
	unsigned pad);

#endif

A src/components/ircv3.c => src/components/ircv3.c +40 -0
@@ 0,0 1,40 @@
#include "src/components/ircv3.h"

#include <string.h>

struct ircv3_cap*
ircv3_cap_get(struct ircv3_caps *caps, const char *cap_str)
{
	#define X(CAP, VAR, ATTRS) \
	if (!strcmp(cap_str, CAP)) \
		return &(caps->VAR);
	IRCV3_CAPS
	#undef X

	return NULL;
}

void
ircv3_caps(struct ircv3_caps *caps)
{
	#define X(CAP, VAR, ATTRS) \
	caps->VAR.req = 0;                                    \
	caps->VAR.set = 0;                                    \
	caps->VAR.supported = 0;                              \
	caps->VAR.supports_del = !(ATTRS & IRCV3_CAP_NO_DEL); \
	caps->VAR.supports_req = !(ATTRS & IRCV3_CAP_NO_REQ); \
	caps->VAR.req_auto = (ATTRS & IRCV3_CAP_AUTO);
	IRCV3_CAPS
	#undef X
}

void
ircv3_caps_reset(struct ircv3_caps *caps)
{
	#define X(CAP, VAR, ATTRS) \
	caps->VAR.req = 0; \
	caps->VAR.set = 0; \
	caps->VAR.supported = 0;
	IRCV3_CAPS
	#undef X
}

A src/components/ircv3.h => src/components/ircv3.h +44 -0
@@ 0,0 1,44 @@
#ifndef IRCV3_CAP_H
#define IRCV3_CAP_H

#define IRCV3_CAP_AUTO   (1 << 0)
#define IRCV3_CAP_NO_DEL (1 << 1)
#define IRCV3_CAP_NO_REQ (1 << 2)
#define IRCV3_CAP_VERSION "302"

#define IRCV3_CAPS_DEF \
	X("multi-prefix", multi_prefix, IRCV3_CAP_AUTO)

/* Extended by testcases */
#ifndef IRCV3_CAPS_TEST
#define IRCV3_CAPS_TEST
#endif

#define IRCV3_CAPS \
	IRCV3_CAPS_DEF \
	IRCV3_CAPS_TEST

struct ircv3_cap
{
	unsigned req          : 1; /* cap REQ sent */
	unsigned req_auto     : 1; /* cap REQ sent during registration */
	unsigned set          : 1; /* cap is unset/set */
	unsigned supported    : 1; /* cap is supported by server */
	unsigned supports_del : 1; /* cap supports CAP DEL */
	unsigned supports_req : 1; /* cap supports CAP REQ */
};

struct ircv3_caps
{
	#define X(CAP, VAR, ATTRS) \
	struct ircv3_cap VAR;
	IRCV3_CAPS
	#undef X
};

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

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

#endif

M src/components/mode.c => src/components/mode.c +12 -0
@@ 396,6 396,18 @@ mode_prfxmode_prefix(struct mode *m, const struct mode_cfg *cfg, int flag)
	else
		MODE_SET(m->upper, bit, MODE_SET_ON);

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

	while (*f) {

		if (mode_isset(m, *f))
			break;

		f++;
		t++;
	}

	m->prefix = *t;

	return MODE_ERR_NONE;

M src/components/server.c => src/components/server.c +3 -0
@@ 42,6 42,7 @@ server(const char *host, const char *port, const char *pass, const char *user, c
	s->channel = channel(host, CHANNEL_T_SERVER);
	s->casemapping = CASEMAPPING_RFC1459;
	s->mode_str.type = MODE_STR_USERMODE;
	ircv3_caps(&(s->ircv3_caps));
	mode_cfg(&(s->mode_cfg), NULL, MODE_CFG_DEFAULTS);
	/* FIXME: remove server pointer from channel, remove
	 * server's channel from clist */


@@ 130,9 131,11 @@ server_list_del(struct server_list *sl, struct server *s)
void
server_reset(struct server *s)
{
	ircv3_caps_reset(&(s->ircv3_caps));
	mode_reset(&(s->usermodes), &(s->mode_str));
	s->ping = 0;
	s->quitting = 0;
	s->registered = 0;
	s->nicks.next = 0;
}


M src/components/server.h => src/components/server.h +3 -1
@@ 3,8 3,8 @@

#include "src/components/buffer.h"
#include "src/components/channel.h"
#include "src/components/ircv3.h"
#include "src/components/mode.h"
#include "src/utils/utils.h"

// TODO: move this to utils
#define IRC_MESSAGE_LEN 510


@@ 27,6 27,7 @@ struct server
	struct channel *channel;
	struct channel_list clist;
	struct channel_list ulist; // TODO: seperate privmsg
	struct ircv3_caps ircv3_caps;
	struct mode usermodes;
	struct mode_str mode_str;
	struct mode_cfg mode_cfg;


@@ 35,6 36,7 @@ struct server
	struct user_list ignore;
	unsigned ping;
	unsigned quitting : 1;
	unsigned registered : 1;
	void *connection;
	// TODO: move this to utils
	struct {

M src/draw.c => src/draw.c +302 -284
@@ 11,8 11,9 @@
#include <string.h>

#include "config.h"
#include "src/components/input.h"
#include "src/components/channel.h"
#include "src/components/input.h"
#include "src/draw.h"
#include "src/io.h"
#include "src/state.h"
#include "src/utils/utils.h"


@@ 48,10 49,9 @@
#error "BUFFER_PADDING options are 0 (no pad), 1 (padded)"
#endif

static int actv_colours[ACTIVITY_T_SIZE] = ACTIVITY_COLOURS
static int nick_colours[] = NICK_COLOURS

/* Terminal coordinate row/column boundaries (inclusive) for objects being drawn
/* Terminal coordinate row/column boundaries (inclusive)
 * for objects being drawn. The origin for terminal
 * coordinates is in the top left, indexed from 1
 *
 *   \ c0     cN
 *    +---------+


@@ 60,106 60,274 @@ static int nick_colours[] = NICK_COLOURS
 *    |         |
 *  rN|         |
 *    +---------+
 *
 * The origin for terminal coordinates is in the top left, indexed from 1
 *
 */

struct coords
{
	unsigned int c1;
	unsigned int cN;
	unsigned int r1;
	unsigned int rN;
	unsigned c0;
	unsigned cN;
	unsigned r0;
	unsigned rN;
};

struct draw_state
{
	union {
		struct {
			unsigned buffer : 1;
			unsigned input  : 1;
			unsigned nav    : 1;
			unsigned status : 1;
		};
		unsigned all;
	} bits;
	unsigned bell : 1;
};

static int _draw_fmt(char**, size_t*, size_t*, int, const char*, ...);
static void draw_bits(void);
static void draw_buffer(struct buffer*, struct coords);
static void draw_buffer_line(struct buffer_line*, struct coords, unsigned, unsigned, unsigned, unsigned);
static void draw_input(struct input*, struct coords);
static void draw_nav(struct channel*);
static void draw_status(struct channel*);

static void _draw_buffer_line(struct buffer_line*, struct coords, unsigned int, unsigned int, unsigned int, unsigned int);
static void _draw_buffer(struct buffer*, struct coords);
static void _draw_input(struct input*, struct coords);
static void _draw_nav(struct channel*);
static void _draw_status(struct channel*);
static char* draw_colour(int, int);
static int draw_fmt(char**, size_t*, size_t*, int, const char*, ...);
static unsigned nick_col(char*);
static void check_coords(struct coords);

static inline unsigned int nick_col(char*);
static inline void check_coords(struct coords);
static int actv_colours[ACTIVITY_T_SIZE] = ACTIVITY_COLOURS
static int nick_colours[] = NICK_COLOURS
static struct draw_state draw_state;

void
draw(enum draw_bit bit)
{
	switch (bit) {
		case DRAW_FLUSH:
			draw_bits();
			draw_state.bits.all = 0;
			draw_state.bell = 0;
			break;
		case DRAW_BELL:
			draw_state.bell = 1;
			break;
		case DRAW_BUFFER:
			draw_state.bits.buffer = 1;
			break;
		case DRAW_INPUT:
			draw_state.bits.input = 1;
			break;
		case DRAW_NAV:
			draw_state.bits.nav = 1;
			break;
		case DRAW_STATUS:
			draw_state.bits.status = 1;
			break;
		case DRAW_ALL:
			draw_state.bits.all = -1;
			break;
		default:
			fatal("unknown draw bit");
	}
}

static char* _colour(int, int);
void
draw_init(void)
{
	draw(DRAW_ALL);
	draw(DRAW_FLUSH);
}

void
draw(union draw draw)
draw_term(void)
{
	if (!draw.all_bits)
	printf(RESET_ATTRIBUTES);
	printf(CLEAR_FULL);
}

static void
draw_bits(void)
{
	if (draw_state.bell && BELL_ON_PINGED)
		putchar('\a');

	if (!draw_state.bits.all)
		return;

	struct coords coords;
	struct channel *c = current_channel();

	if (io_tty_cols() < COLS_MIN || io_tty_rows() < ROWS_MIN) {
		printf(CLEAR_FULL MOVE(1, 1) "rirc");
		goto no_draw;
		fflush(stdout);
		return;
	}

	printf(CURSOR_SAVE);

	if (draw.bits.buffer) _draw_buffer(&c->buffer,
		(struct coords) {
			.c1 = 1,
			.cN = io_tty_cols(),
			.r1 = 3,
			.rN = io_tty_rows() - 2
		});
	if (draw_state.bits.buffer) {
		coords.c0 = 1;
		coords.cN = io_tty_cols();
		coords.r0 = 3;
		coords.rN = io_tty_rows() - 2;
		draw_buffer(&c->buffer, coords);
	}

	if (draw.bits.nav)    _draw_nav(c);
	if (draw_state.bits.input) {
		coords.c0 = 1;
		coords.cN = io_tty_cols();
		coords.r0 = io_tty_rows();
		coords.rN = io_tty_rows();
		draw_input(&c->input, coords);
	}

	if (draw.bits.input)  _draw_input(&c->input,
		(struct coords) {
			.c1 = 1,
			.cN = io_tty_cols(),
			.r1 = io_tty_rows(),
			.rN = io_tty_rows()
		});
	if (draw_state.bits.nav)
		draw_nav(c);

	if (draw.bits.status) _draw_status(c);
	if (draw_state.bits.status)
		draw_status(c);

	printf(RESET_ATTRIBUTES);
	printf(CURSOR_RESTORE);

no_draw:

	fflush(stdout);
}

void
draw_bell(void)
static void
draw_buffer(struct buffer *b, struct coords coords)
{
	if (BELL_ON_PINGED)
		putchar('\a');
}
	/* Dynamically draw the current channel's buffer such that:
	 *
	 * - The scrollback line should always be drawn in full when possible
	 * - Lines wrap on whitespace when possible
	 * - The top-most lines draws partially when required
	 * - Buffers requiring fewer rows than available draw from the top down
	 *
	 * Rows are numbered from the top down, 1 to term_rows, so for term_rows = N,
	 * the drawable area for the buffer is bounded [r3, rN-2]:
	 *      __________________________
	 * r0   |         (nav)          |
	 * r2   |------------------------|
	 * r3   |    ::buffer start::    |
	 *      |                        |
	 * ...  |                        |
	 *      |                        |
	 * rN-2 |     ::buffer end::     |
	 * rN-1 |------------------------|
	 * rN   |________(input)_________|
	 *
	 *
	 * So the general steps for drawing are:
	 *
	 * 1. Starting from line L = scrollback, traverse backwards through the
	 *    buffer summing the rows required to draw lines, until the sum
	 *    exceeds the number of rows available
	 *
	 * 2. L now points to the top-most line to be drawn. L might not be able
	 *    to draw in full, so discard the excessive word-wrapped segments and
	 *    draw the remainder
	 *
	 * 3. Traverse forward through the buffer, drawing lines until buffer.head
	 *    is encountered
	 */

void
draw_init(void)
{
	draw_all();
	redraw();
}
	check_coords(coords);

void
draw_term(void)
{
	printf(RESET_ATTRIBUTES);
	printf(CLEAR_FULL);
	unsigned row,
	         row_count = 0,
	         row_total = coords.rN - coords.r0 + 1;

	unsigned col_total = coords.cN - coords.c0 + 1;

	unsigned buffer_i = b->scrollback,
	         head_w,
	         text_w;

	/* Clear the buffer area */
	for (row = coords.r0; row <= coords.rN; row++)
		printf(MOVE(%d, 1) CLEAR_LINE, row);

	struct buffer_line *line = buffer_line(b, buffer_i);

	if (line == NULL)
		return;

	struct buffer_line *tail = buffer_tail(b);
	struct buffer_line *head = buffer_head(b);

	/* Find top line */
	for (;;) {

		buffer_line_split(line, NULL, &text_w, col_total, b->pad);

		row_count += buffer_line_rows(line, text_w);

		if (line == tail)
			break;

		if (row_count >= row_total)
			break;

		line = buffer_line(b, --buffer_i);
	}

	/* Handle impartial top line print */
	if (row_count > row_total) {

		buffer_line_split(line, &head_w, &text_w, col_total, b->pad);

		draw_buffer_line(
			line,
			coords,
			head_w,
			text_w,
			row_count - row_total,
			BUFFER_PADDING ? (b->pad - line->from_len) : 0
		);

		coords.r0 += buffer_line_rows(line, text_w) - (row_count - row_total);

		if (line == head)
			return;

		line = buffer_line(b, ++buffer_i);
	}

	/* Draw all remaining lines */
	while (coords.r0 <= coords.rN) {

		buffer_line_split(line, &head_w, &text_w, col_total, b->pad);

		draw_buffer_line(
			line,
			coords,
			head_w,
			text_w,
			0,
			BUFFER_PADDING ? (b->pad - line->from_len) : 0
		);

		coords.r0 += buffer_line_rows(line, text_w);

		if (line == head)
			return;

		line = buffer_line(b, ++buffer_i);
	}
}

/* FIXME: works except when it doesn't.
 *
 * Fails when line headers are very long compared to text. tests/draw.c needed */
static void
_draw_buffer_line(
draw_buffer_line(
		struct buffer_line *line,
		struct coords coords,
		unsigned int head_w,
		unsigned int text_w,
		unsigned int skip,
		unsigned int pad)
		unsigned head_w,
		unsigned text_w,
		unsigned skip,
		unsigned pad)
{
	check_coords(coords);



@@ 189,19 357,19 @@ _draw_buffer_line(

		struct tm *line_tm = localtime(&line->time);

		if (!_draw_fmt(&header_ptr, &buff_n, &text_n, 0,
				_colour(BUFFER_LINE_HEADER_FG_NEUTRAL, -1)))
		if (!draw_fmt(&header_ptr, &buff_n, &text_n, 0,
				draw_colour(BUFFER_LINE_HEADER_FG_NEUTRAL, -1)))
			goto print_header;

		if (!_draw_fmt(&header_ptr, &buff_n, &text_n, 1,
		if (!draw_fmt(&header_ptr, &buff_n, &text_n, 1,
				" %02d:%02d ", line_tm->tm_hour, line_tm->tm_min))
			goto print_header;

		if (!_draw_fmt(&header_ptr, &buff_n, &text_n, 1,
		if (!draw_fmt(&header_ptr, &buff_n, &text_n, 1,
				"%*s", pad, ""))
			goto print_header;

		if (!_draw_fmt(&header_ptr, &buff_n, &text_n, 0, RESET_ATTRIBUTES))
		if (!draw_fmt(&header_ptr, &buff_n, &text_n, 0, RESET_ATTRIBUTES))
			goto print_header;

		switch (line->type) {


@@ 212,20 380,20 @@ _draw_buffer_line(
			case BUFFER_LINE_NICK:
			case BUFFER_LINE_PART:
			case BUFFER_LINE_QUIT:
				if (!_draw_fmt(&header_ptr, &buff_n, &text_n, 0,
						_colour(BUFFER_LINE_HEADER_FG_NEUTRAL, -1)))
				if (!draw_fmt(&header_ptr, &buff_n, &text_n, 0,
						draw_colour(BUFFER_LINE_HEADER_FG_NEUTRAL, -1)))
					goto print_header;
				break;

			case BUFFER_LINE_CHAT:
				if (!_draw_fmt(&header_ptr, &buff_n, &text_n, 0,
						_colour(line->cached.colour, -1)))
				if (!draw_fmt(&header_ptr, &buff_n, &text_n, 0,
						draw_colour(line->cached.colour, -1)))
					goto print_header;
				break;

			case BUFFER_LINE_PINGED:
				if (!_draw_fmt(&header_ptr, &buff_n, &text_n, 0,
						_colour(BUFFER_LINE_HEADER_FG_PINGED, BUFFER_LINE_HEADER_BG_PINGED)))
				if (!draw_fmt(&header_ptr, &buff_n, &text_n, 0,
						draw_colour(BUFFER_LINE_HEADER_FG_PINGED, BUFFER_LINE_HEADER_BG_PINGED)))
					goto print_header;
				break;



@@ 233,13 401,13 @@ _draw_buffer_line(
				fatal("Invalid line type");
		}

		if (!_draw_fmt(&header_ptr, &buff_n, &text_n, 1,
		if (!draw_fmt(&header_ptr, &buff_n, &text_n, 1,
				"%s", line->from))
			goto print_header;

print_header:
		/* Print the line header */
		printf(MOVE(%d, 1) "%s " RESET_ATTRIBUTES, coords.r1, header);
		printf(MOVE(%d, 1) "%s " RESET_ATTRIBUTES, coords.r0, header);
	}

	while (skip--)


@@ 248,19 416,19 @@ print_header:
	do {
		char *sep = " "VERTICAL_SEPARATOR" ";

		if ((coords.cN - coords.c1) >= sizeof(*sep) + text_w) {
			printf(MOVE(%d, %d), coords.r1, (int)(coords.cN - (sizeof(*sep) + text_w + 1)));
			fputs(_colour(BUFFER_LINE_HEADER_FG_NEUTRAL, -1), stdout);
		if ((coords.cN - coords.c0) >= sizeof(*sep) + text_w) {
			printf(MOVE(%d, %d), coords.r0, (int)(coords.cN - (sizeof(*sep) + text_w + 1)));
			fputs(draw_colour(BUFFER_LINE_HEADER_FG_NEUTRAL, -1), stdout);
			fputs(sep, stdout);
		}

		if (*p1) {
			printf(MOVE(%d, %d), coords.r1, head_w);
			printf(MOVE(%d, %d), coords.r0, head_w);

			print_p1 = p1;
			print_p2 = word_wrap(text_w, &p1, p2);

			fputs(_colour(line->text[0] == QUOTE_CHAR
			fputs(draw_colour(line->text[0] == QUOTE_CHAR
					? BUFFER_LINE_TEXT_FG_GREEN
					: BUFFER_LINE_TEXT_FG_NEUTRAL,
					-1),


@@ 269,132 437,71 @@ print_header:
			printf("%.*s", (int)(print_p2 - print_p1), print_p1);
		}

		coords.r1++;
		coords.r0++;

	} while (*p1 && coords.r1 <= coords.rN);
	} while (*p1 && coords.r0 <= coords.rN);
}

static void
_draw_buffer(struct buffer *b, struct coords coords)
draw_input(struct input *inp, struct coords coords)
{
	/* Dynamically draw the current channel's buffer such that:
	 *
	 * - The scrollback line should always be drawn in full when possible
	 * - Lines wrap on whitespace when possible
	 * - The top-most lines draws partially when required
	 * - Buffers requiring fewer rows than available draw from the top down
	 *
	 * Rows are numbered from the top down, 1 to term_rows, so for term_rows = N,
	 * the drawable area for the buffer is bounded [r3, rN-2]:
	 *      __________________________
	 * r1   |         (nav)          |
	 * r2   |------------------------|
	 * r3   |    ::buffer start::    |
	 *      |                        |
	 * ...  |                        |
	 *      |                        |
	 * rN-2 |     ::buffer end::     |
	 * rN-1 |------------------------|
	 * rN   |________(input)_________|
	 *
	 *
	 * So the general steps for drawing are:
	 *
	 * 1. Starting from line L = scrollback, traverse backwards through the
	 *    buffer summing the rows required to draw lines, until the sum
	 *    exceeds the number of rows available
	 *
	 * 2. L now points to the top-most line to be drawn. L might not be able
	 *    to draw in full, so discard the excessive word-wrapped segments and
	 *    draw the remainder
	 *
	 * 3. Traverse forward through the buffer, drawing lines until buffer.head
	 *    is encountered
	 */
	/* Draw the input line, or the current action message */

	check_coords(coords);

	unsigned int row,
	             row_count = 0,
	             row_total = coords.rN - coords.r1 + 1;

	unsigned int col_total = coords.cN - coords.c1 + 1;
	unsigned cols_t = coords.cN - coords.c0 + 1,
	         cursor = coords.c0;

	unsigned int buffer_i = b->scrollback,
	             head_w,
	             text_w;

	/* Clear the buffer area */
	for (row = coords.r1; row <= coords.rN; row++)
		printf(MOVE(%d, 1) CLEAR_LINE, row);

	struct buffer_line *line = buffer_line(b, buffer_i);
	printf(RESET_ATTRIBUTES);
	printf(MOVE(%d, 1) CLEAR_LINE, coords.rN);
	printf(CURSOR_SAVE);

	if (line == NULL)
	/* Insufficient columns for meaningful input drawing */
	if (cols_t < 3)
		return;

	struct buffer_line *tail = buffer_tail(b);
	struct buffer_line *head = buffer_head(b);

	/* Find top line */
	for (;;) {
	char input[cols_t + COLOUR_SIZE * 2 + 1];
	char *input_ptr = input;

		split_buffer_cols(line, NULL, &text_w, col_total, b->pad);
	size_t buff_n = sizeof(input) - 1,
	       text_n = cols_t;

		row_count += buffer_line_rows(line, text_w);
	if (sizeof(INPUT_PREFIX)) {

		if (line == tail)
			break;
		if (!draw_fmt(&input_ptr, &buff_n, &text_n, 0,
				"%s", draw_colour(INPUT_PREFIX_FG, INPUT_PREFIX_BG)))
			goto print_input;

		if (row_count >= row_total)
			break;
		cursor = coords.c0 + sizeof(INPUT_PREFIX) - 1;

		line = buffer_line(b, --buffer_i);
		if (!draw_fmt(&input_ptr, &buff_n, &text_n, 1,
				INPUT_PREFIX))
			goto print_input;
	}

	/* Handle impartial top line print */
	if (row_count > row_total) {
	if (!draw_fmt(&input_ptr, &buff_n, &text_n, 0,
			"%s", draw_colour(INPUT_FG, INPUT_BG)))
		goto print_input;

		split_buffer_cols(line, &head_w, &text_w, col_total, b->pad);
	if (action_message) {

		_draw_buffer_line(
			line,
			coords,
			head_w,
			text_w,
			row_count - row_total,
			BUFFER_PADDING ? (b->pad - line->from_len) : 0
		);
		cursor = coords.cN;

		coords.r1 += buffer_line_rows(line, text_w) - (row_count - row_total);
		if (!draw_fmt(&input_ptr, &buff_n, &text_n, 1,
				"%s", action_message))
			goto print_input;

		if (line == head)
			return;
		cursor = cols_t - text_n + 1;

		line = buffer_line(b, ++buffer_i);
	} else {
		cursor += input_frame(inp, input_ptr, text_n);
	}

	/* Draw all remaining lines */
	while (coords.r1 <= coords.rN) {

		split_buffer_cols(line, &head_w, &text_w, col_total, b->pad);

		_draw_buffer_line(
			line,
			coords,
			head_w,
			text_w,
			0,
			BUFFER_PADDING ? (b->pad - line->from_len) : 0
		);

		coords.r1 += buffer_line_rows(line, text_w);

		if (line == head)
			return;
print_input:

		line = buffer_line(b, ++buffer_i);
	}
	fputs(input, stdout);
	printf(MOVE(%d, %d), coords.rN, (cursor >= coords.c0 && cursor <= coords.cN) ? cursor : coords.cN);
	printf(CURSOR_SAVE);
}

/* TODO


@@ 408,7 515,7 @@ _draw_buffer(struct buffer *b, struct coords coords)
 *           | #chan1 #chan2 #ch... |   Left printing
 * */
static void
_draw_nav(struct channel *c)
draw_nav(struct channel *c)
{
	/* Dynamically draw the nav such that:
	 *


@@ 505,7 612,7 @@ _draw_nav(struct channel *c)

		colour = (tmp == c) ? NAV_CURRENT_CHAN : actv_colours[tmp->activity];

		if (fputs(_colour(colour, -1), stdout) < 0)
		if (fputs(draw_colour(colour, -1), stdout) < 0)
			break;

		if (printf(" %s ", tmp->name) < 0)


@@ 517,69 624,7 @@ _draw_nav(struct channel *c)
}

static void
_draw_input(struct input *inp, struct coords coords)
{
	/* Draw the input line, or the current action message */

	check_coords(coords);

	unsigned int cols_t = coords.cN - coords.c1 + 1,
	             cursor = coords.c1;

	printf(RESET_ATTRIBUTES);
	printf(MOVE(%d, 1) CLEAR_LINE, coords.rN);
	printf(CURSOR_SAVE);

	/* Insufficient columns for meaningful input drawing */
	if (cols_t < 3)
		return;

	char input[cols_t + COLOUR_SIZE * 2 + 1];
	char *input_ptr = input;

	size_t buff_n = sizeof(input) - 1,
	       text_n = cols_t;

	if (sizeof(INPUT_PREFIX)) {

		if (!_draw_fmt(&input_ptr, &buff_n, &text_n, 0,
				"%s", _colour(INPUT_PREFIX_FG, INPUT_PREFIX_BG)))
			goto print_input;

		cursor = coords.c1 + sizeof(INPUT_PREFIX) - 1;

		if (!_draw_fmt(&input_ptr, &buff_n, &text_n, 1,
				INPUT_PREFIX))
			goto print_input;
	}

	if (!_draw_fmt(&input_ptr, &buff_n, &text_n, 0,
			"%s", _colour(INPUT_FG, INPUT_BG)))
		goto print_input;

	if (action_message) {

		cursor = coords.cN;

		if (!_draw_fmt(&input_ptr, &buff_n, &text_n, 1,
				"%s", action_message))
			goto print_input;

		cursor = cols_t - text_n + 1;

	} else {
		cursor += input_frame(inp, input_ptr, text_n);
	}

print_input:

	fputs(input, stdout);
	printf(MOVE(%d, %d), coords.rN, (cursor >= coords.c1 && cursor <= coords.cN) ? cursor : coords.cN);
	printf(CURSOR_SAVE);
}

static void
_draw_status(struct channel *c)
draw_status(struct channel *c)
{
	/* TODO: channel modes, channel type_flag, servermodes */



@@ 592,9 637,9 @@ _draw_status(struct channel *c)

	float sb;
	int ret;
	unsigned int col = 0;
	unsigned int cols = io_tty_cols();
	unsigned int rows = io_tty_rows();
	unsigned col = 0;
	unsigned cols = io_tty_cols();
	unsigned rows = io_tty_rows();

	/* Insufficient columns for meaningful status */
	if (cols < 3)


@@ 686,22 731,22 @@ print_status:
		printf(HORIZONTAL_SEPARATOR);
}

static inline void
static void
check_coords(struct coords coords)
{
	/* Check coordinate validity before drawing, ensure at least one row, column */

	if (coords.r1 > coords.rN)
		fatal("row coordinates invalid (%u > %u)", coords.r1, coords.rN);
	if (coords.r0 > coords.rN)
		fatal("row coordinates invalid (%u > %u)", coords.r0, coords.rN);

	if (coords.c1 > coords.cN)
		fatal("col coordinates invalid (%u > %u)", coords.c1, coords.cN);
	if (coords.c0 > coords.cN)
		fatal("col coordinates invalid (%u > %u)", coords.c0, coords.cN);
}

static inline unsigned int
static unsigned
nick_col(char *nick)
{
	unsigned int colour = 0;
	unsigned colour = 0;

	while (*nick)
		colour += *nick++;


@@ 710,7 755,7 @@ nick_col(char *nick)
}

static char*
_colour(int fg, int bg)
draw_colour(int fg, int bg)
{
	/* Set terminal foreground and background colours to a value [0, 255],
	 * or reset colour if given anything else


@@ 739,7 784,7 @@ _colour(int fg, int bg)
}

static int
_draw_fmt(char **ptr, size_t *buff_n, size_t *text_n, int txt, const char *fmt, ...)
draw_fmt(char **ptr, size_t *buff_n, size_t *text_n, int txt, const char *fmt, ...)
{
	/* Write formatted text to a buffer for purposes of preparing an object to be drawn
	 * to the terminal.


@@ 784,30 829,3 @@ _draw_fmt(char **ptr, size_t *buff_n, size_t *text_n, int txt, const char *fmt, 

	return 1;
}

void
split_buffer_cols(
	struct buffer_line *line,
	unsigned int *head_w,
	unsigned int *text_w,
	unsigned int cols,
	unsigned int pad)
{
	unsigned int _head_w = sizeof(" HH:MM   "VERTICAL_SEPARATOR" ");

	if (BUFFER_PADDING)
		_head_w += pad;
	else
		_head_w += line->from_len;

	/* If header won't fit, split in half */
	if (_head_w >= cols)
		_head_w = cols / 2;

	_head_w -= 1;

	if (head_w)
		*head_w = _head_w;
	if (text_w)
		*text_w = cols - _head_w + 1;
}

M src/draw.h => src/draw.h +10 -19
@@ 1,29 1,20 @@
#ifndef DRAW_H
#define DRAW_H

#include "src/components/buffer.h"

/* Draw component, e.g. draw_buffer(); */
#define DRAW_BITS \
	X(buffer) \
	X(input)  \
	X(nav)    \
	X(status)

union draw
enum draw_bit
{
	struct {
		#define X(bit) unsigned int bit : 1;
		DRAW_BITS
		#undef X
	} bits;
	unsigned int all_bits;
	DRAW_INVALID,
	DRAW_FLUSH,  /* immediately draw all set bits */
	DRAW_BELL,   /* set bit to print terminal bell */
	DRAW_BUFFER, /* set bit to draw buffer */
	DRAW_INPUT,  /* set bit to draw input */
	DRAW_NAV,    /* set bit to draw nav */
	DRAW_STATUS, /* set bit to draw status */
	DRAW_ALL,    /* set all draw bits aside from bell */
};

void draw(union draw);
void draw_bell(void);
void draw(enum draw_bit);
void draw_init(void);
void draw_term(void);
void split_buffer_cols(struct buffer_line*, unsigned int*, unsigned int*, unsigned int, unsigned int);

#endif

M src/handlers/irc_ctcp.c => src/handlers/irc_ctcp.c +144 -8
@@ 62,10 62,10 @@ ctcp_request(struct server *s, const char *from, const char *targ, char *message
	if ((ret = parse_ctcp(s, from, &message, &command)) != 0)
		return ret;

	if ((ctcp = ctcp_handler_lookup(command, strlen(command))))
		return ctcp->f_request(s, from, targ, message);
	if (!(ctcp = ctcp_handler_lookup(command, strlen(command))))
		failf(s, "Received unsupported CTCP request '%s' from %s", command, from);

	failf(s, "Received unsupported CTCP request '%s' from %s", command, from);
	return ctcp->f_request(s, from, targ, message);
}

int


@@ 78,15 78,24 @@ ctcp_response(struct server *s, const char *from, const char *targ, char *messag
	if ((ret = parse_ctcp(s, from, &message, &command)) != 0)
		return ret;

	if ((ctcp = ctcp_handler_lookup(command, strlen(command))))
		return ctcp->f_response(s, from, targ, message);
	if (!(ctcp = ctcp_handler_lookup(command, strlen(command))) || !ctcp->f_response)
		failf(s, "Received unsupported CTCP response '%s' from %s", command, from);

	failf(s, "Received unsupported CTCP response '%s' from %s", command, from);
	return ctcp->f_response(s, from, targ, message);
}

static int
ctcp_request_action(struct server *s, const char *from, const char *targ, char *m)
{
	/* Type:     Extended Formatting
	 * Request:  ACTION <text>
	 * Response: -- no response --
	 *
	 * This extended formatting message shows that <text> should be displayed as
	 * a third-person action or emote. If <text> is empty, clients SHOULD still
	 * include a single space after
	 */

	struct channel *c;

	if (!targ)


@@ 114,6 123,14 @@ ctcp_request_action(struct server *s, const char *from, const char *targ, char *
static int
ctcp_request_clientinfo(struct server *s, const char *from, const char *targ, char *m)
{
	/* Type:     Extended Query
	 * Request:  CLIENTINFO
	 * Response: CLIENTINFO <args>
	 *
	 * This extended query returns a list of the CTCP messages that this client
	 * supports and implements, delimited by a single ASCII space.
	 */

	UNUSED(targ);

	if (strtrim(&m))


@@ 129,6 146,15 @@ ctcp_request_clientinfo(struct server *s, const char *from, const char *targ, ch
static int
ctcp_request_finger(struct server *s, const char *from, const char *targ, char *m)
{
	/* Type:     Metadata Query
	 * Request:  FINGER
	 * Response: FINGER <info>
	 *
	 * This metadata query returns miscellaneous info about the user, typically
	 * the same information that’s held in their realname field. However, some
	 * implementations return the client name and version instead.
	 */

	UNUSED(targ);

	if (strtrim(&m))


@@ 144,12 170,26 @@ ctcp_request_finger(struct server *s, const char *from, const char *targ, char *
static int
ctcp_request_ping(struct server *s, const char *from, const char *targ, char *m)
{
	/* Type:     Extended Query
	 * Request:  PING <info>
	 * Response: PING <info>
	 *
	 * This extended query confirms reachability and latency to the target
	 * client. When receiving a CTCP PING, the reply MUST contain exactly
	 * the same parameters as the original query.
	 */

	UNUSED(targ);

	if (strtrim(&m)) {
	if (strtrim(&m))
		server_info(s, "CTCP PING from %s (%s)", from, m);
	else
		server_info(s, "CTCP PING from %s", from);

	if (m)
		sendf(s, "NOTICE %s :\001PING %s\001", from, m);
	}
	else
		sendf(s, "NOTICE %s :\001PING\001", from);

	return 0;
}


@@ 157,6 197,13 @@ ctcp_request_ping(struct server *s, const char *from, const char *targ, char *m)
static int
ctcp_request_source(struct server *s, const char *from, const char *targ, char *m)
{
	/* Type:     Metadata Query
	 * Request:  SOURCE
	 * Response: SOURCE <info>
	 *
	 * This metadata query returns the location of the source code for the client.
	 */

	UNUSED(targ);

	if (strtrim(&m))


@@ 172,6 219,17 @@ ctcp_request_source(struct server *s, const char *from, const char *targ, char *
static int
ctcp_request_time(struct server *s, const char *from, const char *targ, char *m)
{
	/* Type:     Extended Query
	 * Request:  TIME
	 * Response: TIME <timestring>
	 *
	 * This extended query returns the client’s local time in an unspecified
	 * human-readable format. In practice, both the format output by ctime()
	 * and the format described in Section 3.3 of RFC5322 are common. Earlier
	 * specifications recommended prefixing the time string with a colon,
	 * but this is no longer recommended.
	 */

	/* ISO 8601 */
	char buf[sizeof("1970-01-01T00:00:00")];
	struct tm tm;


@@ 205,6 263,15 @@ ctcp_request_time(struct server *s, const char *from, const char *targ, char *m)
static int
ctcp_request_userinfo(struct server *s, const char *from, const char *targ, char *m)
{
	/* Type:     Metadata Query
	 * Request:  USERINFO
	 * Response: USERINFO <info>
	 *
	 * This metadata query returns miscellaneous info about the user, typically
	 * the same information that’s held in their realname field. However, some
	 * implementations return <nickname> (<realname>) instead.
	 */

	UNUSED(targ);

	if (strtrim(&m))


@@ 220,6 287,14 @@ ctcp_request_userinfo(struct server *s, const char *from, const char *targ, char
static int
ctcp_request_version(struct server *s, const char *from, const char *targ, char *m)
{
	/* Type:     Metadata Query
	 * Request:  VERSION
	 * Response: VERSION <verstring>
	 *
	 * This metadata query returns the name and version of the client software in
	 * use. There is no specified format for the version string.
	 */

	UNUSED(targ);

	if (strtrim(&m))


@@ 235,6 310,14 @@ ctcp_request_version(struct server *s, const char *from, const char *targ, char 
static int
ctcp_response_clientinfo(struct server *s, const char *from, const char *targ, char *m)
{
	/* Type:     Extended Query
	 * Request:  CLIENTINFO
	 * Response: CLIENTINFO <args>
	 *
	 * This extended query returns a list of the CTCP messages that this client
	 * supports and implements, delimited by a single ASCII space.
	 */

	UNUSED(targ);

	if (!strtrim(&m))


@@ 248,6 331,15 @@ ctcp_response_clientinfo(struct server *s, const char *from, const char *targ, c
static int
ctcp_response_finger(struct server *s, const char *from, const char *targ, char *m)
{
	/* Type:     Metadata Query
	 * Request:  FINGER
	 * Response: FINGER <info>
	 *
	 * This metadata query returns miscellaneous info about the user, typically
	 * the same information that’s held in their realname field. However, some
	 * implementations return the client name and version instead.
	 */

	UNUSED(targ);

	if (!strtrim(&m))


@@ 261,6 353,15 @@ ctcp_response_finger(struct server *s, const char *from, const char *targ, char 
static int
ctcp_response_ping(struct server *s, const char *from, const char *targ, char *m)
{
	/* Type:     Extended Query
	 * Request:  PING <info>
	 * Response: PING <info>
	 *
	 * This extended query confirms reachability and latency to the target
	 * client. When receiving a CTCP PING, the reply MUST contain exactly
	 * the same parameters as the original query.
	 */

	const char *sec;
	const char *usec;
	long long unsigned res;


@@ 324,6 425,13 @@ ctcp_response_ping(struct server *s, const char *from, const char *targ, char *m
static int
ctcp_response_source(struct server *s, const char *from, const char *targ, char *m)
{
	/* Type:     Metadata Query
	 * Request:  SOURCE
	 * Response: SOURCE <info>
	 *
	 * This metadata query returns the location of the source code for the client.
	 */

	UNUSED(targ);

	if (!strtrim(&m))


@@ 337,6 445,17 @@ ctcp_response_source(struct server *s, const char *from, const char *targ, char 
static int
ctcp_response_time(struct server *s, const char *from, const char *targ, char *m)
{
	/* Type:     Extended Query
	 * Request:  TIME
	 * Response: TIME <timestring>
	 *
	 * This extended query returns the client’s local time in an unspecified
	 * human-readable format. In practice, both the format output by ctime()
	 * and the format described in Section 3.3 of RFC5322 are common. Earlier
	 * specifications recommended prefixing the time string with a colon,
	 * but this is no longer recommended.
	 */

	UNUSED(targ);

	if (!strtrim(&m))


@@ 350,6 469,15 @@ ctcp_response_time(struct server *s, const char *from, const char *targ, char *m
static int
ctcp_response_userinfo(struct server *s, const char *from, const char *targ, char *m)
{
	/* Type:     Metadata Query
	 * Request:  USERINFO
	 * Response: USERINFO <info>
	 *
	 * This metadata query returns miscellaneous info about the user, typically
	 * the same information that’s held in their realname field. However, some
	 * implementations return <nickname> (<realname>) instead.
	 */

	UNUSED(targ);

	if (!strtrim(&m))


@@ 363,6 491,14 @@ ctcp_response_userinfo(struct server *s, const char *from, const char *targ, cha
static int
ctcp_response_version(struct server *s, const char *from, const char *targ, char *m)
{
	/* Type:     Metadata Query
	 * Request:  VERSION
	 * Response: VERSION <verstring>
	 *
	 * This metadata query returns the name and version of the client software in
	 * use. There is no specified format for the version string.
	 */

	UNUSED(targ);

	if (!strtrim(&m))

M src/handlers/irc_recv.c => src/handlers/irc_recv.c +46 -25
@@ 6,6 6,8 @@
#include "src/handlers/irc_ctcp.h"
#include "src/handlers/irc_recv.gperf.out"
#include "src/handlers/irc_recv.h"
#include "src/handlers/ircv3.h"
#include "src/draw.h"
#include "src/io.h"
#include "src/state.h"
#include "src/utils/utils.h"


@@ 55,7 57,7 @@ static int irc_353(struct server*, struct irc_message*);
static int irc_433(struct server*, struct irc_message*);

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

static const unsigned quit_threshold = QUIT_THRESHOLD;


@@ 142,7 144,7 @@ static const irc_recv_f irc_numerics[] = {
	[349] = irc_ignore, /* RPL_ENDOFEXCEPTLIST */
	[351] = irc_info,   /* RPL_VERSION */
	[352] = irc_info,   /* RPL_WHOREPLY */
	[353] = irc_353,    /* RPL_NAMREPLY */
	[353] = irc_353,    /* RPL_NAMEREPLY */
	[364] = irc_info,   /* RPL_LINKS */
	[365] = irc_ignore, /* RPL_ENDOFLINKS */
	[366] = irc_ignore, /* RPL_ENDOFNAMES */


@@ 165,6 167,7 @@ static const irc_recv_f irc_numerics[] = {
	[407] = irc_error,  /* ERR_TOOMANYTARGETS */
	[408] = irc_error,  /* ERR_NOSUCHSERVICE */
	[409] = irc_error,  /* ERR_NOORIGIN */
	[410] = irc_error,  /* ERR_INVALIDCAPCMD */
	[411] = irc_error,  /* ERR_NORECIPIENT */
	[412] = irc_error,  /* ERR_NOTEXTTOSEND */
	[413] = irc_error,  /* ERR_NOTOPLEVEL */


@@ 280,6 283,8 @@ irc_001(struct server *s, struct irc_message *m)
	char *trailing;
	struct channel *c = s->channel;

	s->registered = 1;

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


@@ 349,7 354,7 @@ irc_324(struct server *s, struct irc_message *m)
	if ((c = channel_list_get(&s->clist, chan, s->casemapping)) == NULL)
		failf(s, "RPL_CHANNELMODEIS: channel '%s' not found", chan);

	return recv_mode_chanmodes(m, &(s->mode_cfg), c);
	return recv_mode_chanmodes(m, &(s->mode_cfg), s, c);
}

static int


@@ 479,7 484,7 @@ irc_333(struct server *s, struct irc_message *m)
static int
irc_353(struct server *s, struct irc_message *m)
{
	/* 353 ("="/"*"/"@") <channel> *([ "@" / "+" ]<nick>) */
	/* 353 <nick> <type> <channel> 1*(<modes><nick>) */

	char *chan;
	char *nick;


@@ 500,26 505,34 @@ irc_353(struct server *s, struct irc_message *m)
		failf(s, "RPL_NAMEREPLY: channel '%s' not found", chan);

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

	if ((nick = strsep(&nicks))) {
		do {
			char prefix = 0;
			struct mode m = MODE_EMPTY;

			if (!irc_isnickchar(*nick, 1))
			do {
				if (irc_isnickchar(*nick, 1))
					break;

				prefix = *nick++;

			if (prefix && mode_prfxmode_prefix(&m, &(s->mode_cfg), prefix) != MODE_ERR_NONE)
				newlinef(c, 0, FROM_ERROR, "Invalid user prefix: '%c'", prefix);
				if (mode_prfxmode_prefix(&m, &(s->mode_cfg), prefix) != MODE_ERR_NONE)
					failf(s, "RPL_NAMEREPLY: invalid user prefix: '%c'", prefix);

			} while (s->ircv3_caps.multi_prefix.set);

			if (!irc_isnick(nick))
				failf(s, "RPL_NAMEREPLY: invalid nick: '%s'", nick);

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

		} while ((nick = strsep(&nicks)));
	}

	draw_status();
	draw(DRAW_STATUS);

	return 0;
}


@@ 591,6 604,12 @@ irc_recv_numeric(struct server *s, struct irc_message *m)
}

static int
recv_cap(struct server *s, struct irc_message *m)
{
	return ircv3_recv_CAP(s, m);
}

static int
recv_error(struct server *s, struct irc_message *m)
{
	/* ERROR <message> */


@@ 655,7 674,7 @@ recv_join(struct server *s, struct irc_message *m)
		c->parted = 0;
		newlinef(c, BUFFER_LINE_JOIN, FROM_JOIN, "Joined %s", chan);
		sendf(s, "MODE %s", chan);
		draw_all();
		draw(DRAW_ALL);
		return 0;
	}



@@ 668,7 687,7 @@ recv_join(struct server *s, struct irc_message *m)
	if (!join_threshold || c->users.count <= join_threshold)
		newlinef(c, BUFFER_LINE_JOIN, FROM_JOIN, "%s!%s has joined", m->from, m->host);

	draw_status();
	draw(DRAW_STATUS);

	return 0;
}


@@ 724,7 743,7 @@ recv_kick(struct server *s, struct irc_message *m)
			newlinef(c, 0, FROM_INFO, "%s has kicked %s", m->from, user);
	}

	draw_status();
	draw(DRAW_STATUS);

	return 0;
}


@@ 764,13 783,13 @@ recv_mode(struct server *s, struct irc_message *m)
		return recv_mode_usermodes(m, &(s->mode_cfg), s);

	if ((c = channel_list_get(&s->clist, targ, s->casemapping)))
		return recv_mode_chanmodes(m, &(s->mode_cfg), c);
		return recv_mode_chanmodes(m, &(s->mode_cfg), s, c);

	failf(s, "MODE: target '%s' not found", targ);
}

static int
recv_mode_chanmodes(struct irc_message *m, const struct mode_cfg *cfg, struct channel *c)
recv_mode_chanmodes(struct irc_message *m, const struct mode_cfg *cfg, struct server *s, struct channel *c)
{
	char flag;
	char *modestring;


@@ 780,6 799,8 @@ recv_mode_chanmodes(struct irc_message *m, const struct mode_cfg *cfg, struct ch
	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;


@@ 849,7 870,7 @@ recv_mode_chanmodes(struct irc_message *m, const struct mode_cfg *cfg, struct ch
						continue;
					}

					if (!(user = user_list_get(&(c->users), c->server->casemapping, modearg, 0))) {
					if (!(user = user_list_get(&(c->users), s->casemapping, modearg, 0))) {
						newlinef(c, 0, FROM_ERROR, "MODE: flag '%c' user '%s' not found", flag, modearg);
						continue;
					}


@@ 896,7 917,7 @@ recv_mode_chanmodes(struct irc_message *m, const struct mode_cfg *cfg, struct ch
	} while (irc_message_param(m, &modestring));

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

	return 0;
}


@@ 946,7 967,7 @@ recv_mode_usermodes(struct irc_message *m, const struct mode_cfg *cfg, struct se
	} while (irc_message_param(m, &modestring));

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

	return 0;
}


@@ 967,7 988,7 @@ recv_nick(struct server *s, struct irc_message *m)

	if (!strcmp(m->from, s->nick)) {
		server_nick_set(s, nick);
		newlinef(s->channel, BUFFER_LINE_NICK, FROM_NICK, "Youn nick is '%s'", nick);
		newlinef(s->channel, BUFFER_LINE_NICK, FROM_NICK, "Your nick is now '%s'", nick);
	}

	do {


@@ 1038,8 1059,8 @@ recv_notice(struct server *s, struct irc_message *m)

	if (urgent) {
		c->activity = ACTIVITY_PINGED;
		draw_bell();
		draw_nav();
		draw(DRAW_BELL);
		draw(DRAW_NAV);
	}

	return 0;


@@ 1088,7 1109,7 @@ recv_part(struct server *s, struct irc_message *m)
		}
	}

	draw_status();
	draw(DRAW_STATUS);

	return 0;
}


@@ 1171,8 1192,8 @@ recv_privmsg(struct server *s, struct irc_message *m)

	if (urgent) {
		c->activity = ACTIVITY_PINGED;
		draw_bell();
		draw_nav();
		draw(DRAW_BELL);
		draw(DRAW_NAV);
	}

	return 0;


@@ 1233,7 1254,7 @@ recv_quit(struct server *s, struct irc_message *m)
		}
	} while ((c = c->next) != s->channel);

	draw_status();
	draw(DRAW_STATUS);

	return 0;
}

M src/handlers/irc_recv.gperf => src/handlers/irc_recv.gperf +2 -0
@@ 2,6 2,7 @@
#include <string.h>

#define RECV_HANDLERS \
	X(cap) \
	X(error) \
	X(invite) \
	X(join) \


@@ 40,6 41,7 @@ struct recv_handler
%define initializer-suffix ,(irc_recv_f)0
struct recv_handler;
%%
CAP,     recv_cap
ERROR,   recv_error
INVITE,  recv_invite
JOIN,    recv_join

M src/handlers/irc_recv.h => src/handlers/irc_recv.h +3 -0
@@ 361,6 361,9 @@
 *         --- NOT IMPLEMENTED ---
 */

#include "src/components/server.h"
#include "src/utils/utils.h"

int irc_recv(struct server*, struct irc_message*);

#endif

M src/handlers/irc_send.c => src/handlers/irc_send.c +86 -59
@@ 4,6 4,7 @@
#include "config.h"
#include "src/components/buffer.h"
#include "src/components/channel.h"
#include "src/components/ircv3.h"
#include "src/components/server.h"
#include "src/handlers/irc_send.gperf.out"
#include "src/handlers/irc_send.h"


@@ 11,8 12,6 @@
#include "src/state.h"
#include "src/utils/utils.h"

// TODO: should privmsg/notice open a PRIVATE/CHANNEL buffer for the target?

#define failf(C, ...) \
	do { newlinef((C), 0, FROM_ERROR, __VA_ARGS__); \
	     return 1; \


@@ 35,6 34,9 @@ irc_send_command(struct server *s, struct channel *c, char *m)
	if (!s)
		failf(c, "This is not a server");

	if (!s->registered)
		failf(c, "Not registered with server");

	if (*m == ' ' || !(command = strsep(&m)))
		failf(c, "Messages beginning with '/' require a command");



@@ 60,6 62,9 @@ irc_send_privmsg(struct server *s, struct channel *c, char *m)
	if (!s)
		failf(c, "This is not a server");

	if (!s->registered)
		failf(c, "Not registered with server");

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



@@ 91,6 96,79 @@ targ_or_type(struct channel *c, char *m, enum channel_t type)
}

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

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

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

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

	return 0;
}

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

	if (strtrim(&m))
		sendf(s, c, "PART %s :%s", c->name, m);
	else
		sendf(s, c, "PART %s :%s", c->name, DEFAULT_PART_MESG);

	return 0;
}

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

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

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

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

	return 0;
}

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

	if (strtrim(&m))
		sendf(s, c, "QUIT :%s", m);
	else
		sendf(s, c, "QUIT :%s", DEFAULT_PART_MESG);

	return 0;
}

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

	if (strtrim(&m))
		sendf(s, c, "TOPIC %s :%s", c->name, m);
	else
		sendf(s, c, "TOPIC %s", c->name);

	return 0;
}

static int
send_ctcp_action(struct server *s, struct channel *c, char *m)
{
	if (!(c->type == CHANNEL_T_CHANNEL || c->type == CHANNEL_T_PRIVATE))


@@ 196,74 274,23 @@ send_ctcp_version(struct server *s, struct channel *c, char *m)
}

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

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

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

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

	return 0;
}

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

	if (strtrim(&m))
		sendf(s, c, "PART %s :%s", c->name, m);
	else
		sendf(s, c, "PART %s :%s", c->name, DEFAULT_PART_MESG);

	return 0;
}
		failf(c, "Usage: /cap-ls");

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

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

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

	sendf(s, c, "PRIVMSG %s :%s", targ, m);
	sendf(s, c, "CAP LS " IRCV3_CAP_VERSION);

	return 0;
}

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

	if (strtrim(&m))
		sendf(s, c, "QUIT :%s", m);
	else
		sendf(s, c, "QUIT :%s", DEFAULT_PART_MESG);

	return 0;
}

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

	if (strtrim(&m))
		sendf(s, c, "TOPIC %s :%s", c->name, m);
	else
		sendf(s, c, "TOPIC %s", c->name);
	sendf(s, c, "CAP LIST");

	return 0;
}

M src/handlers/irc_send.gperf => src/handlers/irc_send.gperf +10 -0
@@ 18,6 18,10 @@
	X(userinfo) \
	X(version)

#define SEND_IRCV3_CAP_HANDLERS \
	X(ls) \
	X(list)

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


@@ 26,6 30,10 @@ SEND_HANDLERS
SEND_CTCP_HANDLERS
#undef X

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

typedef int (*irc_send_f)(struct server*, struct channel*, char*);

struct send_handler


@@ 46,6 54,8 @@ struct send_handler
%define initializer-suffix ,(irc_send_f)0
struct send_handler;
%%
CAP-LS,          send_ircv3_cap_ls
CAP-LIST,        send_ircv3_cap_list
CTCP-ACTION,     send_ctcp_action
CTCP-CLIENTINFO, send_ctcp_clientinfo
CTCP-FINGER,     send_ctcp_finger

M src/handlers/irc_send.h => src/handlers/irc_send.h +3 -0
@@ 1,6 1,9 @@
#ifndef IRC_SEND_H
#define IRC_SEND_H

#include "src/components/channel.h"
#include "src/components/server.h"

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


A src/handlers/ircv3.c => src/handlers/ircv3.c +356 -0
@@ 0,0 1,356 @@
#include <string.h>

#include "src/handlers/ircv3.h"
#include "src/io.h"
#include "src/state.h"

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

#define sendf(S, ...) \
	do { int ret; \
	     if ((ret = io_sendf((S)->connection, __VA_ARGS__))) \
	         failf((S), "Send fail: %s", io_err(ret)); \
	} while (0)

#define IRCV3_RECV_HANDLERS \
	X(LIST) \
	X(LS) \
	X(ACK) \
	X(NAK) \
	X(DEL) \
	X(NEW)

#define X(CMD) \
static int ircv3_recv_cap_##CMD(struct server*, struct irc_message*);
IRCV3_RECV_HANDLERS
#undef X

static int ircv3_cap_req_count(struct ircv3_caps*);

int
ircv3_recv_CAP(struct server *s, struct irc_message *m)
{
	char *targ;
	char *cmnd;

	if (!irc_message_param(m, &targ))
		failf(s, "CAP: target is null");

	if (!irc_message_param(m, &cmnd))
		failf(s, "CAP: command is null");

	#define X(CMD) \
	if (!strcmp(cmnd, #CMD)) \
		return ircv3_recv_cap_##CMD(s, m);
	IRCV3_RECV_HANDLERS
	#undef X

	failf(s, "CAP: unrecognized subcommand '%s'", cmnd);
}

static int
ircv3_recv_cap_LS(struct server *s, struct irc_message *m)
{
	/* If no capabilities are available, an empty
	 * parameter MUST be sent.
	 *
	 * Servers MAY send multiple lines in response to
	 * CAP LS and CAP LIST. If the reply contains
	 * multiple lines, all but the last reply MUST
	 * have a parameter containing only an asterisk (*)
	 * preceding the capability list
	 *
	 * CAP <targ> LS [*] :[<cap_1> [...]]
	 */

	char *cap;
	char *caps;
	char *multiline;

	irc_message_param(m, &multiline);
	irc_message_param(m, &caps);

	if (!multiline)
		failf(s, "CAP LS: parameter is null");

	if (!strcmp(multiline, "*") && !caps)
		failf(s, "CAP LS: parameter is null");

	if (strcmp(multiline, "*") && caps)
		failf(s, "CAP LS: invalid parameters");

	if (!caps) {
		caps = multiline;
		multiline = NULL;
	}

	if (s->registered) {
		server_info(s, "CAP LS: %s", (*caps ? caps : "(no capabilities)"));
		return 0;
	}

	while ((cap = strsep(&(caps)))) {

		struct ircv3_cap *c;

		if ((c = ircv3_cap_get(&(s->ircv3_caps), cap)))
			c->supported = 1;
	}

	if (multiline)
		return 0;

	#define X(CAP, VAR, ATTRS)     \
	if (s->ircv3_caps.VAR.supported && s->ircv3_caps.VAR.req_auto) { \
		s->ircv3_caps.VAR.req = 1; \
		sendf(s, "CAP REQ :" CAP); \
	}
	IRCV3_CAPS
	#undef X

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

	return 0;
}

static int
ircv3_recv_cap_LIST(struct server *s, struct irc_message *m)
{
	/* If no capabilities are available, an empty
	 * parameter MUST be sent.
	 *
	 * Servers MAY send multiple lines in response to
	 * CAP LS and CAP LIST. If the reply contains
	 * multiple lines, all but the last reply MUST
	 * have a parameter containing only an asterisk (*)
	 * preceding the capability list
	 *
	 * CAP <targ> LIST [*] :[<cap_1> [...]]
	 */

	char *caps;
	char *multiline;

	irc_message_param(m, &multiline);
	irc_message_param(m, &caps);

	if (!multiline)
		failf(s, "CAP LIST: parameter is null");

	if (multiline && caps && strcmp(multiline, "*"))
		failf(s, "CAP LIST: invalid parameters");

	if (!strcmp(multiline, "*") && !caps)
		failf(s, "CAP LIST: parameter is null");

	if (!caps)
		caps = multiline;

	server_info(s, "CAP LIST: %s", (*caps ? caps : "(no capabilities)"));

	return 0;
}

static int
ircv3_recv_cap_ACK(struct server *s, struct irc_message *m)
{
	/* Each capability name may be prefixed with a
	 * dash (-), indicating that this capability has
	 * been disabled as requested.
	 *
	 * If an ACK reply originating from the server is
	 * spread across multiple lines, a client MUST NOT
	 * change capabilities until the last ACK of the
	 * set is received. Equally, a server MUST NOT change
	 * the capabilities of the client until the last ACK
	 * of the set has been sent.
	 *
	 * CAP <targ> ACK :[-]<cap_1> [[-]<cap_2> [...]]
	 */

	char *cap;
	char *caps;
	int errors = 0;

	if (!irc_message_param(m, &caps))
		failf(s, "CAP ACK: parameter is null");

	if (!(cap = strsep(&(caps))))
		failf(s, "CAP ACK: parameter is empty");

	do {
		int unset;
		struct ircv3_cap *c;

		if ((unset = (*cap == '-')))
			cap++;

		if (!(c = ircv3_cap_get(&(s->ircv3_caps), cap))) {
			server_error(s, "CAP ACK: '%s' not supported", cap);
			errors++;
			continue;
		}

		if (!c->req) {
			server_error(s, "CAP ACK: '%s%s' was not requested", (unset ? "-" : ""), cap);
			errors++;
			continue;
		}

		if (!unset && c->set) {
			server_error(s, "CAP ACK: '%s' was set", cap);
			errors++;
			continue;
		}

		if (unset && !c->set) {
			server_error(s, "CAP ACK: '%s' was not set", cap);
			errors++;
			continue;
		}

		c->req = 0;
		c->set = !unset;

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

	} while ((cap = 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;
}

static int
ircv3_recv_cap_NAK(struct server *s, struct irc_message *m)
{
	/* The server MUST NOT make any change to any
	 * capabilities if it replies with a NAK subcommand.
	 *
	 * CAP <targ> NAK :<cap_1> [<cap_2> [...]]
	 */

	char *cap;
	char *caps;

	if (!irc_message_param(m, &caps))
		failf(s, "CAP NAK: parameter is null");

	if (!(cap = strsep(&(caps))))
		failf(s, "CAP NAK: parameter is empty");

	do {
		struct ircv3_cap *c;

		if ((c = ircv3_cap_get(&(s->ircv3_caps), cap)))
			c->req = 0;

		server_info(s, "capability change rejected: %s", cap);

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

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

	return 0;
}

static int
ircv3_recv_cap_DEL(struct server *s, struct irc_message *m)
{
	/* Upon receiving a CAP DEL message, the client MUST
	 * treat the listed capabilities as cancelled and no
	 * longer available. Clients SHOULD NOT send CAP REQ
	 * messages to cancel the capabilities in CAP DEL,
	 * as they have already been cancelled by the server.
	 *
	 * CAP <targ> DEL :<cap_1> [<cap_2> [...]]
	 */

	char *cap;
	char *caps;

	if (!irc_message_param(m, &caps))
		failf(s, "CAP DEL: parameter is null");

	if (!(cap = strsep(&(caps))))
		failf(s, "CAP DEL: parameter is empty");

	do {
		struct ircv3_cap *c;

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

		if (!c->supports_del)
			failf(s, "CAP DEL: '%s' doesn't support DEL", cap);

		c->req = 0;
		c->set = 0;
		c->supported = 0;

		server_info(s, "capability lost: %s", cap);

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

	return 0;
}

static int
ircv3_recv_cap_NEW(struct server *s, struct irc_message *m)
{
	/* Clients that support CAP NEW messages SHOULD respond
	 * with a CAP REQ message if they wish to enable one or
	 * more of the newly-offered capabilities.
	 *
	 * CAP <targ> NEW :<cap_1> [<cap_2> [...]]
	 */

	char *cap;
	char *caps;

	if (!irc_message_param(m, &caps))
		failf(s, "CAP NEW: parameter is null");

	if (!(cap = strsep(&(caps))))
		failf(s, "CAP NEW: parameter is empty");

	do {
		struct ircv3_cap *c;

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

		c->supported = 1;

		if (!c->set && !c->req && c->req_auto) {
			c->req = 1;
			sendf(s, "CAP REQ :%s", cap);
		}

		server_info(s, "new capability: %s", cap);

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

	return 0;
}

static int
ircv3_cap_req_count(struct ircv3_caps *caps)
{
	int ret = 0;
	#define X(CAP, VAR, ATTRS) \
	if (caps->VAR.req) \
		ret++;
	IRCV3_CAPS
	#undef X
	return ret;
}

A src/handlers/ircv3.h => src/handlers/ircv3.h +22 -0
@@ 0,0 1,22 @@
/* TODO
 *  - cap commands (after registration)
 *      - /cap-req [-]cap [...]
 *  - cap setting for auto on connect/NEW
 *      - :set, set/unset auto, and for printing state
 *  - LS caps:
 *      [name] [set/unset] [auto]
 *      [name] [unsupported]
 *  - LIST caps:
 *      [name] [auto]
 *  - CAP args, e.g. cap=foo,bar
 */

#ifndef IRCV3_H
#define IRCV3_H

#include "src/components/server.h"
#include "src/utils/utils.h"

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

#endif

M src/io.c => src/io.c +206 -177
@@ 1,3 1,5 @@
#include "src/io.h"

#include <errno.h>
#include <netdb.h>
#include <pthread.h>


@@ 10,18 12,21 @@
#include <termios.h>
#include <unistd.h>

#include "mbedtls/ctr_drbg.h"
#include "mbedtls/entropy.h"
#include "mbedtls/net_sockets.h"
#include "mbedtls/ssl.h"
#include "mbedtls/x509.h"

#include "config.h"
#include "rirc.h"
#include "src/io.h"
#include "src/io_net.h"
#include "utils/utils.h"

/* lib/mbedtls.h is the compile time config
 * and must precede the other mbedtls headers */
#include "lib/mbedtls.h"
#include "lib/mbedtls/include/mbedtls/ctr_drbg.h"
#include "lib/mbedtls/include/mbedtls/entropy.h"
#include "lib/mbedtls/include/mbedtls/error.h"
#include "lib/mbedtls/include/mbedtls/net_sockets.h"
#include "lib/mbedtls/include/mbedtls/ssl.h"
#include "lib/mbedtls/include/mbedtls/x509_crt.h"

/* RFC 2812, section 2.3 */
#ifndef IO_MESG_LEN
#define IO_MESG_LEN 510


@@ 79,6 84,9 @@
		PT_UL(&cb_mutex);   \
	} while (0)

/* state transition */
#define ST_X(OLD, NEW) (((OLD) << 3) | (NEW))

#define io_cb_cxed(C)        PT_CB(IO_CB_CXED, (C)->obj)
#define io_cb_dxed(C)        PT_CB(IO_CB_DXED, (C)->obj)
#define io_cb_err(C, ...)    PT_CB(IO_CB_ERR, (C)->obj, __VA_ARGS__)


@@ 114,34 122,35 @@ struct connection
		IO_ST_PING, /* Socket connected, network state in question */
	} st_cur, /* current thread state */
	  st_new; /* new thread state */
	mbedtls_net_context ssl_fd;
	mbedtls_ssl_config ssl_conf;
	mbedtls_ssl_context ssl_ctx;
	mbedtls_net_context tls_fd;
	mbedtls_ssl_config  tls_conf;
	mbedtls_ssl_context tls_ctx;
	pthread_mutex_t mtx;
	pthread_t tid;
	unsigned ping;
	unsigned rx_sleep;
};

static const char* io_tls_strerror(int, char*, size_t);
static enum io_state_t io_state_cxed(struct connection*);
static enum io_state_t io_state_cxng(struct connection*);
static enum io_state_t io_state_ping(struct connection*);
static enum io_state_t io_state_rxng(struct connection*);
static int io_cx_read(struct connection*);
static int io_cx_read(struct connection*, unsigned);
static void io_fatal(const char*, int);
static void io_sig_handle(int);
static void io_sig_init(void);
static void io_ssl_init(void);
static void io_ssl_term(void);
static void io_tls_init(void);
static void io_tls_term(void);
static void io_tty_init(void);
static void io_tty_term(void);
static void io_tty_winsize(void);
static void* io_thread(void*);

static int io_running;
static mbedtls_ctr_drbg_context ssl_ctr_drbg;
static mbedtls_entropy_context ssl_entropy;
static mbedtls_ssl_config ssl_conf;
static mbedtls_x509_crt ssl_cacert;
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 cb_mutex = PTHREAD_MUTEX_INITIALIZER;
static struct termios term;
static unsigned io_cols;


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

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

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


@@ 264,7 273,7 @@ io_sendf(struct connection *cx, const char *fmt, ...)
	written = 0;

	do {
		if ((ret = mbedtls_ssl_write(&(cx->ssl_ctx), sendbuf + ret, len - ret)) < 0) {
		if ((ret = mbedtls_ssl_write(&(cx->tls_ctx), sendbuf + ret, len - ret)) < 0) {
			switch (ret) {
				case MBEDTLS_ERR_SSL_WANT_READ:
				case MBEDTLS_ERR_SSL_WANT_WRITE:


@@ 286,7 295,7 @@ io_init(void)
{
	io_sig_init();
	io_tty_init();
	io_ssl_init();
	io_tls_init();
}

void


@@ 369,6 378,13 @@ io_err(int err)
	}
}

const char*
io_tls_strerror(int err, char *buf, size_t len)
{
	mbedtls_strerror(err, buf, len);
	return buf;
}

static enum io_state_t
io_state_rxng(struct connection *cx)
{


@@ 393,105 409,121 @@ io_state_rxng(struct connection *cx)
static enum io_state_t
io_state_cxng(struct connection *cx)
{
	char addr_buf[INET6_ADDRSTRLEN];
	char vrfy_buf[512];
	char buf[MAX(INET6_ADDRSTRLEN, 512)];
	enum io_state_t st = IO_ST_RXNG;
	int ret;
	int soc;
	uint32_t cert_ret;

	mbedtls_net_init(&(cx->tls_fd));
	mbedtls_ssl_init(&(cx->tls_ctx));
	mbedtls_ssl_config_init(&(cx->tls_conf));

	io_cb_info(cx, "Connecting to %s:%s", cx->host, cx->port);

	if ((ret = io_net_connect(&soc, cx->host, cx->port)) != IO_NET_ERR_NONE) {
	if ((ret = io_net_connect(&(cx->tls_fd.fd), cx->host, cx->port)) != 0) {
		switch (ret) {
			case IO_NET_ERR_EINTR:
				st = IO_ST_DXED;
				goto error_net;
				goto err;
			case IO_NET_ERR_SOCKET_FAILED:
				io_cb_err(cx, " ... Failed to obtain socket");
				goto error_net;
				goto err;
			case IO_NET_ERR_UNKNOWN_HOST:
				io_cb_err(cx, " ... Failed to resolve host");
				goto error_net;
				goto err;
			case IO_NET_ERR_CONNECT_FAILED:
				io_cb_err(cx, " ... Failed to connect to host");
				goto error_net;
				goto err;
			default:
				fatal("unknown net error");
		}
	}

	if ((ret = io_net_ip_str(soc, addr_buf, sizeof(addr_buf))) != IO_NET_ERR_NONE) {
	if ((ret = io_net_ip_str(cx->tls_fd.fd, buf, sizeof(buf))) != 0) {
		if (ret == IO_NET_ERR_EINTR) {
			st = IO_ST_DXED;
			goto error_net;
			goto err;
		}
		io_cb_info(cx, " ... Connected (failed to optain IP address)");
	} else {
		io_cb_info(cx, " ... Connected to [%s]", addr_buf);
		io_cb_info(cx, " ... Connected to [%s]", buf);
	}

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

	if (mbedtls_ssl_config_defaults(
			&(cx->tls_conf),
			MBEDTLS_SSL_IS_CLIENT,
			MBEDTLS_SSL_TRANSPORT_STREAM,
			MBEDTLS_SSL_PRESET_DEFAULT) != 0) {
		io_cb_err(cx, " ... mbedtls_ssl_config_defaults: %s", io_tls_strerror(ret, buf, sizeof(buf)));
		goto err;
	}

	io_cb_info(cx, " ... Establishing SSL");
	mbedtls_ssl_conf_max_version(
			&(cx->tls_conf),
			MBEDTLS_SSL_MAJOR_VERSION_3,
			MBEDTLS_SSL_MINOR_VERSION_3);

	mbedtls_net_init(&(cx->ssl_fd));
	mbedtls_ssl_init(&(cx->ssl_ctx));
	mbedtls_ssl_config_init(&(cx->ssl_conf));
	mbedtls_ssl_conf_min_version(
			&(cx->tls_conf),
			MBEDTLS_SSL_MAJOR_VERSION_3,
			MBEDTLS_SSL_MINOR_VERSION_3);

	cx->ssl_conf = ssl_conf;
	cx->ssl_fd.fd = soc;
	mbedtls_ssl_conf_ca_chain(&(cx->tls_conf), &tls_x509_crt, NULL);
	mbedtls_ssl_conf_rng(&(cx->tls_conf), mbedtls_ctr_drbg_random, &tls_ctr_drbg);

	if ((ret = mbedtls_net_set_block(&(cx->ssl_fd))) != 0) {
		io_cb_err(cx, " ... mbedtls_net_set_block failure");
		goto error_ssl;
	if ((ret = mbedtls_net_set_block(&(cx->tls_fd))) != 0) {
		io_cb_err(cx, " ... mbedtls_net_set_block: %s", io_tls_strerror(ret, buf, sizeof(buf)));
		goto err;
	}

	if ((ret = mbedtls_ssl_setup(&(cx->ssl_ctx), &(cx->ssl_conf))) != 0) {
		io_cb_err(cx, " ... mbedtls_ssl_setup failure");
		goto error_ssl;
	if ((ret = mbedtls_ssl_setup(&(cx->tls_ctx), &(cx->tls_conf))) != 0) {
		io_cb_err(cx, " ... mbedtls_ssl_setup: %s", io_tls_strerror(ret, buf, sizeof(buf)));
		goto err;
	}

	if ((ret = mbedtls_ssl_set_hostname(&(cx->ssl_ctx), cx->host)) != 0) {
		io_cb_err(cx, " ... mbedtls_ssl_set_hostname failure");
		goto error_ssl;
	if ((ret = mbedtls_ssl_set_hostname(&(cx->tls_ctx), cx->host)) != 0) {
		io_cb_err(cx, " ... mbedtls_ssl_set_hostname: %s", io_tls_strerror(ret, buf, sizeof(buf)));
		goto err;
	}

	mbedtls_ssl_set_bio(
		&(cx->ssl_ctx),
		&(cx->ssl_fd),
		&(cx->tls_ctx),
		&(cx->tls_fd),
		mbedtls_net_send,
		NULL,
		mbedtls_net_recv_timeout);

	while ((ret = mbedtls_ssl_handshake(&(cx->ssl_ctx))) != 0) {
	while ((ret = mbedtls_ssl_handshake(&(cx->tls_ctx))) != 0) {
		if (ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE) {
			io_cb_err(cx, " ... mbedtls_ssl_handshake failure");
			goto error_ssl;
			io_cb_err(cx, " ... mbedtls_ssl_handshake: %s", io_tls_strerror(ret, buf, sizeof(buf)));
			goto err;
		}
	}

	if ((cert_ret = mbedtls_ssl_get_verify_result(&(cx->ssl_ctx))) != 0) {
		if (mbedtls_x509_crt_verify_info(vrfy_buf, sizeof(vrfy_buf), "", cert_ret) <= 0) {
	if ((cert_ret = mbedtls_ssl_get_verify_result(&(cx->tls_ctx))) != 0) {
		if (mbedtls_x509_crt_verify_info(buf, sizeof(buf), "", cert_ret) <= 0) {
			io_cb_err(cx, " ... failed to verify cert: unknown failure");
			goto error_ssl;
		} else {
			io_cb_err(cx, " ... failed to verify cert: %s", vrfy_buf);
			goto error_ssl;
			io_cb_err(cx, " ... failed to verify cert: %s", buf);
		}
		goto err;
	}

	io_cb_info(cx, " ... SSL connection established");
	io_cb_info(cx, " ...   - version:     %s", mbedtls_ssl_get_version(&(cx->ssl_ctx)));
	io_cb_info(cx, " ...   - ciphersuite: %s", mbedtls_ssl_get_ciphersuite(&(cx->ssl_ctx)));
	io_cb_info(cx, " ... TLS connection established");
	io_cb_info(cx, " ...   - version:     %s", mbedtls_ssl_get_version(&(cx->tls_ctx)));
	io_cb_info(cx, " ...   - ciphersuite: %s", mbedtls_ssl_get_ciphersuite(&(cx->tls_ctx)));

	return IO_ST_CXED;

error_ssl:
err:

	mbedtls_net_free(&(cx->ssl_fd));
	mbedtls_ssl_free(&(cx->ssl_ctx));
	mbedtls_ssl_config_free(&(cx->ssl_conf));
	io_cb_err(cx, " ... TLS connection failure");

error_net:
	mbedtls_ssl_config_free(&(cx->tls_conf));
	mbedtls_ssl_free(&(cx->tls_ctx));
	mbedtls_net_free(&(cx->tls_fd));

	return st;
}


@@ 500,65 532,56 @@ static enum io_state_t
io_state_cxed(struct connection *cx)
{
	int ret;
	enum io_state_t st = IO_ST_RXNG;

	mbedtls_ssl_conf_read_timeout(&(cx->ssl_conf), SEC_IN_MS(IO_PING_MIN));

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

	if (ret == MBEDTLS_ERR_SSL_TIMEOUT)
		return IO_ST_PING;

	switch (ret) {
		case MBEDTLS_ERR_SSL_WANT_READ:
		case MBEDTLS_ERR_SSL_WANT_WRITE:
			/* EINTR */
			break;
		case MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY:
			/* Graceful termination */
			io_cb_info(cx, "connection closed gracefully");
			break;
		case MBEDTLS_ERR_SSL_TIMEOUT:
			return IO_ST_PING;
		case MBEDTLS_ERR_NET_CONN_RESET:
		case 0:
			io_cb_err(cx, "connection reset by peer");
			break;
		default:
			io_cb_err(cx, "connection ssl error");
			io_cb_err(cx, "connection tls error");
			break;
	}

	mbedtls_net_free(&(cx->ssl_fd));
	mbedtls_ssl_free(&(cx->ssl_ctx));
	mbedtls_ssl_config_free(&(cx->ssl_conf));
	mbedtls_net_free(&(cx->tls_fd));
	mbedtls_ssl_config_free(&(cx->tls_conf));
	mbedtls_ssl_free(&(cx->tls_ctx));

	return st;
	return IO_ST_CXNG;
}

static enum io_state_t
io_state_ping(struct connection *cx)
{
	int ping = IO_PING_MIN;
	int ret;
	enum io_state_t st = IO_ST_RXNG;

	mbedtls_ssl_conf_read_timeout(&(cx->ssl_conf), SEC_IN_MS(IO_PING_REFRESH));

	while ((ret = io_cx_read(cx)) <= 0 && ret == MBEDTLS_ERR_SSL_TIMEOUT) {
		if ((ping += IO_PING_REFRESH) < IO_PING_MAX) {
			io_cb_ping_n(cx, ping);
		}
	}
	if (cx->ping >= IO_PING_MAX)
		return IO_ST_CXNG;

	if (ret > 0)
	if ((ret = io_cx_read(cx, IO_PING_REFRESH)) > 0)
		return IO_ST_CXED;

	if (ret == MBEDTLS_ERR_SSL_TIMEOUT)
		return IO_ST_PING;

	switch (ret) {
		case MBEDTLS_ERR_SSL_WANT_READ:         /* io_dx EINTR */
		case MBEDTLS_ERR_SSL_WANT_WRITE:        /* io_dx EINTR */
		case MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY: /* Graceful termination */
			st = IO_ST_DXED;
		case MBEDTLS_ERR_SSL_WANT_READ:
		case MBEDTLS_ERR_SSL_WANT_WRITE:
			break;
		case MBEDTLS_ERR_SSL_TIMEOUT:
			io_cb_err(cx, "connection timeout (%u)", ping);
		case MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY:
			io_cb_info(cx, "connection closed gracefully");
			break;
		case MBEDTLS_ERR_NET_CONN_RESET:
		case 0:


@@ 569,11 592,11 @@ io_state_ping(struct connection *cx)
			break;
	}

	mbedtls_net_free(&(cx->ssl_fd));
	mbedtls_ssl_free(&(cx->ssl_ctx));
	mbedtls_ssl_config_free(&(cx->ssl_conf));
	mbedtls_net_free(&(cx->tls_fd));
	mbedtls_ssl_config_free(&(cx->tls_conf));
	mbedtls_ssl_free(&(cx->tls_ctx));

	return st;
	return IO_ST_CXNG;
}

static void*


@@ 589,76 612,94 @@ io_thread(void *arg)
	PT_CF(sigaddset(&sigset, SIGUSR1));
	PT_CF(pthread_sigmask(SIG_UNBLOCK, &sigset, NULL));

	cx->st_new = IO_ST_CXNG;

	for (;;) {
	cx->st_cur = IO_ST_CXNG;

		enum io_state_t st_from;
		enum io_state_t st_to;
	do {
		enum io_state_t st_new;
		enum io_state_t st_old;

		switch (cx->st_cur) {
			case IO_ST_CXED: st_to = io_state_cxed(cx); break;
			case IO_ST_CXNG: st_to = io_state_cxng(cx); break;
			case IO_ST_PING: st_to = io_state_ping(cx); break;
			case IO_ST_RXNG: st_to = io_state_rxng(cx); break;
			case IO_ST_DXED: st_to = IO_ST_INVALID; break;
			case IO_ST_CXED: st_new = io_state_cxed(cx); break;
			case IO_ST_CXNG: st_new = io_state_cxng(cx); break;
			case IO_ST_PING: st_new = io_state_ping(cx); break;
			case IO_ST_RXNG: st_new = io_state_rxng(cx); break;
			default:
				fatal("invalid state: %d", cx->st_cur);
		}

		st_from = cx->st_cur;
		st_old = cx->st_cur;

		PT_LK(&(cx->mtx));

		/* New state set by io_cx/io_dx */
		if (cx->st_new != IO_ST_INVALID) {
			cx->st_cur = st_to = cx->st_new;
			cx->st_new = IO_ST_INVALID;
			cx->st_cur = st_new = cx->st_new;
		} else {
			cx->st_cur = st_to;
			cx->st_cur = st_new;
		}

		PT_UL(&(cx->mtx));

		if (st_from == IO_ST_CXNG && st_to == IO_ST_RXNG)
			io_cb_err(cx, " ... Connection failed -- retrying");

		if (st_from == IO_ST_CXNG && st_to == IO_ST_CXED) {
			io_cb_info(cx, " ... Connection successful");
			io_cb_cxed(cx);
			cx->rx_sleep = 0;
		/* State transitions */
		switch (ST_X(st_old, st_new)) {
			case ST_X(IO_ST_DXED, IO_ST_CXNG): /* A1 */
			case ST_X(IO_ST_RXNG, IO_ST_CXNG): /* A2,C */
				break;
			case ST_X(IO_ST_CXED, IO_ST_CXNG): /* F1 */
				io_cb_dxed(cx);
				break;
			case ST_X(IO_ST_PING, IO_ST_CXNG): /* F2 */
				io_cb_err(cx, "connection timeout (%u)", cx->ping);
				io_cb_dxed(cx);
				break;
			case ST_X(IO_ST_RXNG, IO_ST_DXED): /* B1 */
			case ST_X(IO_ST_CXNG, IO_ST_DXED): /* B2 */
				io_cb_info(cx, "Connection cancelled");
				break;
			case ST_X(IO_ST_CXED, IO_ST_DXED): /* B3 */
			case ST_X(IO_ST_PING, IO_ST_DXED): /* B4 */
				io_cb_info(cx, "Connection closed");
				io_cb_dxed(cx);
				break;
			case ST_X(IO_ST_CXNG, IO_ST_CXED): /* D */
				io_cb_info(cx, " ... Connection successful");
				io_cb_cxed(cx);
				cx->rx_sleep = 0;
				break;
			case ST_X(IO_ST_CXNG, IO_ST_RXNG): /* E */
				io_cb_err(cx, " ... Connection failed -- retrying");
				break;
			case ST_X(IO_ST_CXED, IO_ST_PING): /* G */
				cx->ping = IO_PING_MIN;
				io_cb_ping_1(cx, cx->ping);
				break;
			case ST_X(IO_ST_PING, IO_ST_PING): /* H */
				cx->ping += IO_PING_REFRESH;
				io_cb_ping_n(cx, cx->ping);
				break;
			case ST_X(IO_ST_PING, IO_ST_CXED): /* I */
				cx->ping = 0;
				io_cb_ping_0(cx, cx->ping);
				break;
			default:
				fatal("BAD ST_X from: %d to: %d", st_old, st_new);
		}

		if ((st_from == IO_ST_RXNG || st_from == IO_ST_CXNG) && st_to == IO_ST_DXED)
			io_cb_info(cx, "Connection cancelled");

		if ((st_from == IO_ST_CXED || st_from == IO_ST_PING) && st_to == IO_ST_DXED)
			io_cb_info(cx, "Connection closed");

		if (st_from == IO_ST_PING && st_to == IO_ST_CXED)
			io_cb_ping_0(cx, 0);

		if (st_from == IO_ST_CXED && st_to == IO_ST_PING)
			io_cb_ping_1(cx, IO_PING_MIN);

		/* Exit the thread */
		if (cx->st_cur == IO_ST_DXED) {
			io_cb_dxed(cx);
			cx->rx_sleep = 0;
			break;
		}
	}
	} while (cx->st_cur != IO_ST_DXED);

	return NULL;
}

static int
io_cx_read(struct connection *cx)
io_cx_read(struct connection *cx, unsigned timeout)
{
	int ret;
	unsigned char ssl_readbuf[1024];

	if ((ret = mbedtls_ssl_read(&(cx->ssl_ctx), ssl_readbuf, sizeof(ssl_readbuf))) > 0) {
	mbedtls_ssl_conf_read_timeout(&(cx->tls_conf), SEC_IN_MS(timeout));

	if ((ret = mbedtls_ssl_read(&(cx->tls_ctx), ssl_readbuf, sizeof(ssl_readbuf))) > 0) {
		PT_LK(&cb_mutex);
		io_cb_read_soc((char *)ssl_readbuf, (size_t)ret,  cx->obj);
		PT_UL(&cb_mutex);


@@ 705,58 746,46 @@ io_sig_init(void)
}

static void
io_ssl_init(void)
io_tls_init(void)
{
	const char *tls_pers = "rirc-drbg-ctr-pers";
	int ret;
	char buf[512];
	int err;
	struct timespec ts;

	mbedtls_ssl_config_init(&ssl_conf);
	mbedtls_ctr_drbg_init(&ssl_ctr_drbg);
	mbedtls_entropy_init(&ssl_entropy);
	mbedtls_x509_crt_init(&ssl_cacert);
	mbedtls_ctr_drbg_init(&tls_ctr_drbg);
	mbedtls_entropy_init(&tls_entropy);
	mbedtls_x509_crt_init(&tls_x509_crt);

	if ((ret = mbedtls_x509_crt_parse_path(&ssl_cacert, ca_cert_path)) <= 0) {
		if (ret == 0) {
			fatal("no certs found");
		} else {
			fatal("ssl init failed: mbedtls_x509_crt_parse_path");
		}
	}
	if (atexit(io_tls_term) != 0)
		fatal("atexit");

	if ((ret = mbedtls_ctr_drbg_seed(
					&ssl_ctr_drbg,
					mbedtls_entropy_func,
					&ssl_entropy,
					(unsigned char *)tls_pers,
					strlen(tls_pers))) != 0) {
		fatal("ssl init failed: mbedtls_ctr_drbg_seed");
	}
	if (timespec_get(&ts, TIME_UTC) != TIME_UTC)
		fatal("timespec_get");

	if ((ret = mbedtls_ssl_config_defaults(
					&ssl_conf,
					MBEDTLS_SSL_IS_CLIENT,
					MBEDTLS_SSL_TRANSPORT_STREAM,
					MBEDTLS_SSL_PRESET_DEFAULT)) != 0) {
		fatal("ssl init failed: mbedtls_ssl_config_defaults");
	}
	if (snprintf(buf, sizeof(buf), "rirc-%lu-%lu", ts.tv_sec, ts.tv_nsec) < 0)
		fatal("snprintf");

	mbedtls_ssl_conf_ca_chain(&ssl_conf, &ssl_cacert, NULL);
	mbedtls_ssl_conf_read_timeout(&ssl_conf, SEC_IN_MS(IO_PING_MIN));
	mbedtls_ssl_conf_rng(&ssl_conf, mbedtls_ctr_drbg_random, &ssl_ctr_drbg);
	if ((err = mbedtls_ctr_drbg_seed(
			&tls_ctr_drbg,
			mbedtls_entropy_func,
			&tls_entropy,
			(const unsigned char *)buf,
			strlen(buf))) != 0) {
		fatal("mbedtls_ctr_drbg_seed: %s", io_tls_strerror(err, buf, sizeof(buf)));
	}

	if (atexit(io_ssl_term) != 0)
		fatal("atexit");
	if ((err = mbedtls_x509_crt_parse_path(&tls_x509_crt, ca_cert_path)) < 0)
		fatal("mbedtls_x509_crt_parse_path: %s", io_tls_strerror(err, buf, sizeof(buf)));
}

static void
io_ssl_term(void)
io_tls_term(void)
{
	/* Exit handler, must return normally */

	mbedtls_ctr_drbg_free(&ssl_ctr_drbg);
	mbedtls_entropy_free(&ssl_entropy);
	mbedtls_ssl_config_free(&ssl_conf);
	mbedtls_x509_crt_free(&ssl_cacert);
	mbedtls_ctr_drbg_free(&tls_ctr_drbg);
	mbedtls_entropy_free(&tls_entropy);
	mbedtls_x509_crt_free(&tls_x509_crt);
}

static void

M src/io.h => src/io.h +35 -27
@@ 12,33 12,39 @@
 *  - cxed: connected    ~ Socket connected
 *  - ping: timing out   ~ Socket connected, network state in question
 *
 *                            +--------+
 *                 +----(B)-- |  rxng  |
 *                 |          +--------+
 *                 |           |      ^
 *   INIT          |         (A,C)    |
 *    v            |           |     (E)
 *    |            v           v      |
 *    |    +--------+ --(A)-> +--------+
 *    +--> |  dxed  |         |  cxng  | <--+
 *         +--------+ <-(B)-- +--------+    |
 *          ^      ^           |      ^    (F)
 *          |      |          (D)     |     |
 *          |      |           |     (F)    |
 *          |      |           v      |     |
 *          |      |          +--------+    |
 *          |      +----(B)-- |  cxed  |    |
 *          |                 +--------+    |
 *          |                  |      ^     |
 *          |                 (G)     |     |
 *          |                  |     (I)    |
 *          |                  v      |     |
 *          |                 +--------+    |
 *          +-----------(B)-- |  ping  | ---+
 *                            +--------+
 *                             v      ^
 *                             |      |
 *                             +--(H)-+
 *
 *    TODO: how to we label the difference between A, and (A,C) ?
 *          just for the sake of consistency should it be A1, (A2, C)
 *
 *          is the H transition necessary?
 *
 *                             +--------+
 *                 +----(B1)-- |  rxng  |
 *                 |           +--------+
 *                 |            |      ^
 *   INIT          |         (A2,C)    |
 *    v            |            |     (E)
 *    |            v            v      |
 *    |    +--------+ --(A1)-> +--------+
 *    +--> |  dxed  |          |  cxng  | <--+
 *         +--------+ <-(B2)-- +--------+    |
 *          ^      ^            |      ^   (F2)
 *          |      |           (D)     |     |
 *          |      |            |    (F1)    |
 *          |      |            v      |     |
 *          |      |           +--------+    |
 *          |      +----(B3)-- |  cxed  |    |
 *          |                  +--------+    |
 *          |                   |      ^     |
 *          |                  (G)     |     |
 *          |                   |     (I)    |
 *          |                   v      |     |
 *          |                  +--------+    |
 *          +-----------(B4)-- |  ping  | ---+
 *                             +--------+
 *                              v      ^
 *                              |      |
 *                              +--(H)-+
 *
 * This module exposes functions for explicitly directing network
 * state as well declaring callback functions for state transitions


@@ 73,6 79,8 @@
 * a call to io_stop
 */

#include <stddef.h>

struct connection;

enum io_sig_t

M src/io_net.h => src/io_net.h +1 -1
@@ 5,7 5,7 @@

enum io_net_err
{
	IO_NET_ERR_NONE,
	IO_NET_ERR_NONE = 0,
	IO_NET_ERR_SOCKET_FAILED,
	IO_NET_ERR_UNKNOWN_HOST,
	IO_NET_ERR_CONNECT_FAILED,

M src/rirc.c => src/rirc.c +11 -10
@@ 7,6 7,7 @@
#include <unistd.h>

#include "config.h"
#include "src/draw.h"
#include "src/io.h"
#include "src/state.h"



@@ 23,36 24,36 @@ static const char* opt_arg_str(char);
static const char* getpwuid_pw_name(void);
static int parse_args(int, char**);

#ifndef DEBUG
const char *runtime_name = "rirc";
#else
const char *runtime_name = "rirc.debug";
#endif

#ifdef CA_CERT_PATH
const char *ca_cert_path = CA_CERT_PATH;
#else
#error "CA_CERT_PATH required"
#endif

#ifndef DEFAULT_NICK_SET
#ifdef DEFAULT_NICK_SET
const char *default_nick_set = DEFAULT_NICK_SET;
#else
const char *default_nick_set;
#endif

#ifndef DEFAULT_USERNAME
#ifdef DEFAULT_USERNAME
const char *default_username = DEFAULT_USERNAME;
#else
const char *default_username;
#endif

#ifndef DEFAULT_REALNAME
#ifdef DEFAULT_REALNAME
const char *default_realname = DEFAULT_REALNAME;
#else
const char *default_realname;
#endif

#ifndef NDEBUG
const char *runtime_name = "rirc.debug";
#else
const char *runtime_name = "rirc";
#endif

static const char *const rirc_usage =
"\nrirc v"VERSION" ~ Richard C. Robbins <mail@rcr.io>"
"\n"


@@ 74,7 75,7 @@ static const char *const rirc_usage =
"\n";

static const char *const rirc_version =
#ifdef DEBUG
#ifndef NDEBUG
"rirc v"VERSION" (debug build)";
#else
"rirc v"VERSION;

M src/state.c => src/state.c +34 -50
@@ 12,12 12,12 @@
#include <stdio.h>

#include "src/draw.h"
#include "src/handlers/irc_recv.h"
#include "src/handlers/irc_send.h"
#include "src/io.h"
#include "src/rirc.h"
#include "src/state.h"
#include "src/utils/utils.h"
#include "src/handlers/irc_recv.h"
#include "src/handlers/irc_send.h"

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


@@ 43,7 43,6 @@ static struct
	struct channel *current_channel; /* the current channel being drawn */
	struct channel *default_channel; /* the default rirc channel at startup */
	struct server_list servers;
	union draw draw;
} state;

struct server_list*


@@ 60,6 59,8 @@ current_channel(void)

/* List of IRC commands for tab completion */
static const char *irc_list[] = {
	"cap-ls",
	"cap-list",
	"ctcp-action",
	"ctcp-clientinfo",
	"ctcp-finger",


@@ 81,25 82,6 @@ static const char *irc_list[] = {
static const char *cmd_list[] = {
	"clear", "close", "connect", "disconnect", "quit", "set", NULL};

/* Set draw bits */
#define X(BIT) void draw_##BIT(void) { state.draw.bits.BIT = 1; }
DRAW_BITS
#undef X

void
draw_all(void)
{
	state.draw.all_bits = -1;
}

void
redraw(void)
{
	draw(state.draw);

	state.draw.all_bits = 0;
}

void
state_init(void)
{


@@ 113,7 95,7 @@ state_init(void)
	newline(state.default_channel, 0, "--", "");
	newline(state.default_channel, 0, "--", " - version " VERSION);
	newline(state.default_channel, 0, "--", " - compiled " __DATE__ ", " __TIME__);
#ifdef DEBUG
#ifndef NDEBUG
	newline(state.default_channel, 0, "--", " - compiled with DEBUG flags");
#endif
}


@@ 205,10 187,10 @@ _newline(struct channel *c, enum buffer_line_t type, const char *from, const cha
		prefix);

	if (c == current_channel()) {
		draw_buffer();
		draw(DRAW_BUFFER);
	} else {
		c->activity = MAX(c->activity, ACTIVITY_ACTIVE);
		draw_nav();
		draw(DRAW_NAV);
	}
}



@@ 252,7 234,7 @@ void
channel_clear(struct channel *c)
{
	memset(&(c->buffer), 0, sizeof(c->buffer));
	draw_buffer();
	draw(DRAW_BUFFER);
}

/* WIP:


@@ 341,7 323,7 @@ action_close_server(char c)
		server_list_del(state_server_list(), s);
		server_free(s);

		draw_all();
		draw(DRAW_ALL);

		return 1;
	}


@@ 369,7 351,7 @@ action(int (*a_handler)(char), const char *fmt, ...)
	} else {
		action_handler = a_handler;
		action_message = action_buff;
		draw_input();
		draw(DRAW_INPUT);
	}
}
/* Action line should be:


@@ 480,7 462,7 @@ channel_close(struct channel *c)
		if (c == current_channel()) {
			channel_set_current(c->next);
		} else {
			draw_nav();
			draw(DRAW_NAV);
		}

		channel_list_del(&c->server->clist, c);


@@ 512,7 494,7 @@ buffer_scrollback_back(struct channel *c)
	/* Find top line */
	for (;;) {

		split_buffer_cols(line, NULL, &text_w, cols, b->pad);
		buffer_line_split(line, NULL, &text_w, cols, b->pad);

		count += buffer_line_rows(line, text_w);



@@ 531,8 513,8 @@ buffer_scrollback_back(struct channel *c)
	if (count == rows && line != buffer_tail(b))
		b->scrollback--;

	draw_buffer();
	draw_status();
	draw(DRAW_BUFFER);
	draw(DRAW_STATUS);
}

void


@@ 556,7 538,7 @@ buffer_scrollback_forw(struct channel *c)
	/* Find top line */
	for (;;) {

		split_buffer_cols(line, NULL, &text_w, cols, b->pad);
		buffer_line_split(line, NULL, &text_w, cols, b->pad);

		count += buffer_line_rows(line, text_w);



@@ 573,8 555,8 @@ buffer_scrollback_forw(struct channel *c)
	if (count == rows && line != buffer_head(b))
		b->scrollback++;

	draw_buffer();
	draw_status();
	draw(DRAW_BUFFER);
	draw(DRAW_STATUS);
}

/* FIXME:


@@ 625,7 607,7 @@ channel_set_current(struct channel *c)
{
	/* Set the state to an arbitrary channel */
	state.current_channel = c;
	draw_all();
	draw(DRAW_ALL);
}

void


@@ 637,7 619,7 @@ channel_move_prev(void)

	if (c != state.current_channel) {
		state.current_channel = c;
		draw_all();
		draw(DRAW_ALL);
	}
}



@@ 650,7 632,7 @@ channel_move_next(void)

	if (c != state.current_channel) {
		state.current_channel = c;
		draw_all();
		draw(DRAW_ALL);
	}
}



@@ 712,10 694,12 @@ static void
state_io_cxed(struct server *s)
{
	int ret;

	server_reset(s);
	server_nicks_next(s);

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

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



@@ 725,7 709,7 @@ state_io_cxed(struct server *s)
	if ((ret = io_sendf(s->connection, "USER %s 8 * :%s", s->username, s->realname)))
		newlinef(s->channel, 0, "-!!-", "sendf fail: %s", io_err(ret));

	draw_status();
	draw(DRAW_STATUS);
}

static void


@@ 748,7 732,7 @@ state_io_ping(struct server *s, unsigned int ping)
	s->ping = ping;

	if (ping != IO_PING_MIN)
		draw_status();
		draw(DRAW_STATUS);
	else if ((ret = io_sendf(s->connection, "PING :%s", s->host)))
		newlinef(s->channel, 0, "-!!-", "sendf fail: %s", io_err(ret));
}


@@ 758,7 742,7 @@ state_io_signal(enum io_sig_t sig)
{
	switch (sig) {
		case IO_SIGWINCH:
			draw_all();
			draw(DRAW_ALL);
			break;
		default:
			newlinef(state.default_channel, 0, "-!!-", "unhandled signal %d", sig);


@@ 800,7 784,7 @@ io_cb(enum io_cb_t type, const void *cb_obj, ...)

	va_end(ap);

	redraw();
	draw(DRAW_FLUSH);
}

static void


@@ 848,7 832,7 @@ command(struct channel *c, char *buf)
				server_list_add(state_server_list(), s);
				channel_set_current(s->channel);
				io_cx(s->connection);
				draw_all();
				draw(DRAW_ALL);
			}
		}
		return;


@@ 1034,9 1018,9 @@ io_cb_read_inp(char *buf, size_t len)
		redraw_input = input_insert(&current_channel()->input, buf, len);

	if (redraw_input)
		draw_input();
		draw(DRAW_INPUT);

	redraw();
	draw(DRAW_FLUSH);
}

void


@@ 1055,17 1039,15 @@ io_cb_read_soc(char *buf, size_t len, const void *cb_obj)

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

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

			struct irc_message m;

			if (!(irc_message_parse(&m, s->read.buf)))
			if (irc_message_parse(&m, s->read.buf) != 0)
				newlinef(c, 0, "-!!-", "failed to parse message");
			else
				irc_recv(s, &m);

			redraw();

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


@@ 1074,4 1056,6 @@ io_cb_read_soc(char *buf, size_t len, const void *cb_obj)

	s->read.cl = buf[n - 1];
	s->read.i = ci;

	draw(DRAW_FLUSH);
}

M src/state.h => src/state.h +0 -11
@@ 3,9 3,7 @@

#include "src/components/buffer.h"
#include "src/components/channel.h"
#include "src/components/input.h"
#include "src/components/server.h"
#include "src/draw.h"

#define FROM_INFO "--"
#define FROM_ERROR "-!!-"


@@ 54,15 52,6 @@ void channel_set_current(struct channel*);
void newlinef(struct channel*, enum buffer_line_t, const char*, const char*, ...);
void newline(struct channel*, enum buffer_line_t, const char*, const char*);

/* TODO: refactor, should be static in state */
/* Function prototypes for setting draw bits */
#define X(bit) void draw_##bit(void);
DRAW_BITS
#undef X
void draw_all(void);

void redraw(void);

extern char *action_message;

#endif

M src/utils/utils.c => src/utils/utils.c +6 -4
@@ 142,6 142,8 @@ irc_strncmp(enum casemapping_t casemapping, const char *s1, const char *s2, size
	return 0;
}

// TODO: reverse return order
// 0 success, -1 error
int
irc_message_param(struct irc_message *m, char **param)
{


@@ 201,7 203,7 @@ irc_message_parse(struct irc_message *m, char *buf)
	memset(m, 0, sizeof(*m));

	if (!strtrim(&buf))
		return 0;
		return -1;

	if (*buf == ':') {



@@ 221,7 223,7 @@ irc_message_parse(struct irc_message *m, char *buf)
		m->len_from = buf - m->from;

		if (m->len_from == 0)
			return 0;
			return -1;

		if (*buf == '!' || *buf == '@') {
			*buf++ = 0;


@@ 238,7 240,7 @@ irc_message_parse(struct irc_message *m, char *buf)
	}

	if (!strtrim(&buf))
		return 0;
		return -1;

	m->command = buf;



@@ 253,7 255,7 @@ irc_message_parse(struct irc_message *m, char *buf)
	if (strtrim(&buf))
		m->params = buf;

	return 1;
	return 0;
}

int

M src/utils/utils.h => src/utils/utils.h +11 -2
@@ 18,14 18,23 @@
#define MESSAGE(TYPE, ...) \
	fprintf(stderr, "%s %s:%d:%s ", (TYPE), __FILE__, __LINE__, __func__); \
	fprintf(stderr, __VA_ARGS__); \
	fprintf(stderr, "\n");
	fprintf(stderr, "\n"); \
	fflush(stderr);

#if (defined DEBUG) && !(defined TESTING)
#if !(defined NDEBUG) && !(defined TESTING)
#define debug(...) \
	do { MESSAGE("DEBUG", __VA_ARGS__); } while (0)
#define debug_send(L, M) \
	do { fprintf(stderr, "DEBUG (--> %3zu) %s\n", (L), (M)); fflush(stderr); } while (0)
#define debug_recv(L, M) \
	do { fprintf(stderr, "DEBUG (<-- %3zu) %s\n", (L), (M)); fflush(stderr); } while (0)
#else
#define debug(...) \
	do { ; } while (0)
#define debug_send(L, M) \
	do { ; } while (0)
#define debug_recv(L, M) \
	do { ; } while (0)
#endif

#ifndef fatal

M test/components/buffer.c => test/components/buffer.c +1 -1
@@ 10,7 10,7 @@ _fmt_int(int i)
	static char buff[1024];

	if ((snprintf(buff, sizeof(buff), "%d", i)) < 0)
		fail_test("snprintf");
		test_fail("snprintf");

	return buff;
}

A test/components/ircv3.c => test/components/ircv3.c +69 -0
@@ 0,0 1,69 @@
#include "test/test.h"

/* Extends the definition in server.h */
#define IRCV3_CAPS_TEST \
	X("cap-1", cap_1, IRCV3_CAP_AUTO) \
	X("cap-2", cap_2, 0) \
	X("cap-3", cap_3, (IRCV3_CAP_NO_DEL | IRCV3_CAP_NO_REQ))

#include "src/components/ircv3.c"

static void
test_ircv3_caps(void)
{
	struct ircv3_caps caps;

	ircv3_caps(&caps);

	assert_eq(caps.cap_1.req, 0);
	assert_eq(caps.cap_2.req, 0);
	assert_eq(caps.cap_3.req, 0);
	assert_eq(caps.cap_1.req_auto, 1);
	assert_eq(caps.cap_2.req_auto, 0);
	assert_eq(caps.cap_3.req_auto, 0);
	assert_eq(caps.cap_1.set, 0);
	assert_eq(caps.cap_2.set, 0);
	assert_eq(caps.cap_3.set, 0);
	assert_eq(caps.cap_1.supported, 0);
	assert_eq(caps.cap_2.supported, 0);
	assert_eq(caps.cap_3.supported, 0);
	assert_eq(caps.cap_1.supports_del, 1);
	assert_eq(caps.cap_2.supports_del, 1);
	assert_eq(caps.cap_3.supports_del, 0);
	assert_eq(caps.cap_1.supports_req, 1);
	assert_eq(caps.cap_2.supports_req, 1);
	assert_eq(caps.cap_3.supports_req, 0);
}

static void
test_ircv3_caps_reset(void)
{
	struct ircv3_caps caps;

	caps.cap_3.req = 1;
	caps.cap_3.req_auto = 1;
	caps.cap_3.set = 1;
	caps.cap_3.supported = 1;
	caps.cap_3.supports_del = 1;
	caps.cap_3.supports_req = 1;

	ircv3_caps_reset(&caps);

	assert_eq(caps.cap_3.req, 0);
	assert_eq(caps.cap_3.req_auto, 1);
	assert_eq(caps.cap_3.set, 0);
	assert_eq(caps.cap_3.supported, 0);
	assert_eq(caps.cap_3.supports_del, 1);
	assert_eq(caps.cap_3.supports_req, 1);
}

int
main(void)
{
	struct testcase tests[] = {
		TESTCASE(test_ircv3_caps),
		TESTCASE(test_ircv3_caps_reset),
	};

	return run_tests(tests);
}

M test/components/server.c => test/components/server.c +5 -4
@@ 1,10 1,11 @@
#include "test/test.h"
#include "src/components/server.c"
#include "src/components/buffer.c"
#include "src/components/channel.c"
#include "src/components/user.c"
#include "src/components/mode.c"
#include "src/components/input.c"
#include "src/components/buffer.c"
#include "src/components/ircv3.c"
#include "src/components/mode.c"
#include "src/components/server.c"
#include "src/components/user.c"
#include "src/utils/utils.c"

void

M test/components/user.c => test/components/user.c +3 -3
@@ 70,7 70,7 @@ test_user_list(void)
	assert_eq(user_list_rpl(&ulist, CASEMAPPING_RFC1459, "ccc", "ddd"), USER_ERR_NONE);

	if ((u4 = user_list_get(&ulist, CASEMAPPING_RFC1459, "ddd", 0)) == NULL)
		test_abort("Failed to retrieve u4 by prefix");
		test_abort("Failed to retrieve u4");

	assert_eq(u4->prfxmodes.lower, 0x123);
	assert_eq(u4->prfxmodes.upper, 0x456);


@@ 135,14 135,14 @@ test_user_list_free(void)

	for (p = users; *p; p++) {
		if (user_list_add(&ulist, CASEMAPPING_RFC1459, *p, MODE_EMPTY) != USER_ERR_NONE)
			fail_testf("Failed to add user to list: %s", *p);
			test_failf("Failed to add user to list: %s", *p);
	}

	user_list_free(&ulist);

	for (p = users; *p; p++) {
		if (user_list_add(&ulist, CASEMAPPING_RFC1459, *p, MODE_EMPTY) != USER_ERR_NONE)
			fail_testf("Failed to remove user from list: %s", *p);
			test_failf("Failed to remove user from list: %s", *p);
	}

	user_list_free(&ulist);

M test/draw.c => test/draw.c +5 -4
@@ 3,6 3,7 @@
#include "src/components/buffer.c"
#include "src/components/channel.c"
#include "src/components/input.c"
#include "src/components/ircv3.c"
#include "src/components/mode.c"
#include "src/components/server.c"
#include "src/components/user.c"


@@ 10,10 11,10 @@
#include "src/state.c"
#include "src/utils/utils.c"

#include "test/io.c.mock"
#include "test/rirc.c.mock"
#include "test/handlers/irc_recv.c.mock"
#include "test/handlers/irc_send.c.mock"
#include "test/handlers/irc_recv.mock.c"
#include "test/handlers/irc_send.mock.c"
#include "test/io.mock.c"
#include "test/rirc.mock.c"

static void
test_STUB(void)

D test/draw.c.mock => test/draw.c.mock +0 -18
@@ 1,18 0,0 @@
void draw(union draw d) { UNUSED(d); }
void draw_init(void) { ; }
void draw_bell(void) { ; }
void draw_term(void) { ; }
void
split_buffer_cols(
	struct buffer_line *l,
	unsigned int *h,
	unsigned int *t,
	unsigned int c,
	unsigned int p)
{
	UNUSED(l);
	UNUSED(h);
	UNUSED(t);
	UNUSED(c);
	UNUSED(p);
}

A test/draw.mock.c => test/draw.mock.c +8 -0
@@ 0,0 1,8 @@
#ifndef DRAW_MOCK_C
#define DRAW_MOCK_C

void draw(enum draw_bit b) { UNUSED(b); }
void draw_init(void) { ; }
void draw_term(void) { ; }

#endif

M test/handlers/irc_ctcp.c => test/handlers/irc_ctcp.c +292 -276
@@ 3,94 3,46 @@
#include "src/components/buffer.c"
#include "src/components/channel.c"
#include "src/components/input.c"
#include "src/components/ircv3.c"
#include "src/components/mode.c"
#include "src/components/server.c"
#include "src/components/user.c"
#include "src/handlers/irc_ctcp.c"
#include "src/handlers/irc_recv.c"
#include "src/handlers/ircv3.c"
#include "src/utils/utils.c"
#include "test/draw.mock.c"

#define CHECK_REQUEST(F, T, M, R, L, S) \
#include "test/io.mock.c"
#include "test/state.mock.c"

#define IRC_MESSAGE_PARSE(S) \
	char TOKEN(buf, __LINE__)[] = S; \
	assert_eq(irc_message_parse(&m, TOKEN(buf, __LINE__)), 0);

#define CHECK_REQUEST(M, RET, LINE_N, SEND_N, LINE, SEND) \
	do { \
	    line_buf[0] = 0; \
	    send_buf[0] = 0; \
	    assert_eq(ctcp_request(s, (F), (T), (M)), (R)); \
	    assert_strcmp(line_buf, (L)); \
	    assert_strcmp(send_buf, (S)); \
		mock_reset_io(); \
		mock_reset_state(); \
		IRC_MESSAGE_PARSE(M); \
		assert_eq(irc_recv(s, &m), (RET)); \
		assert_eq(mock_line_n, (LINE_N)); \
		assert_eq(mock_send_n, (SEND_N)); \
		assert_strcmp(mock_line[0], (LINE)); \
		assert_strcmp(mock_send[0], (SEND)); \
	} while (0)

#define CHECK_RESPONSE(F, T, M, R, L) \
#define CHECK_RESPONSE(M, RET, LINE) \
	do { \
	    line_buf[0] = 0; \
	    assert_eq(ctcp_response(s, (F), (T), (M)), (R)); \
	    assert_strcmp(line_buf, (L)); \
		mock_reset_io(); \
		mock_reset_state(); \
		IRC_MESSAGE_PARSE(M); \
		assert_eq(irc_recv(s, &m), (RET)); \
		assert_eq(mock_line_n, 1); \
		assert_eq(mock_send_n, 0); \
		assert_strcmp(mock_line[0], (LINE)); \
	} while (0)

static char chan_buf[1024];
static char line_buf[1024];
static char send_buf[1024];
static struct channel *c_chan;
static struct channel *c_priv;
static struct server *s;

/* Mock state.c */
void
newlinef(struct channel *c, enum buffer_line_t t, const char *f, const char *fmt, ...)
{
	va_list ap;
	int r1;
	int r2;

	UNUSED(f);
	UNUSED(t);

	va_start(ap, fmt);
	r1 = snprintf(chan_buf, sizeof(chan_buf), "%s", c->name);
	r2 = vsnprintf(line_buf, sizeof(line_buf), fmt, ap);
	va_end(ap);

	assert_gt(r1, 0);
	assert_gt(r2, 0);
}

void
newline(struct channel *c, enum buffer_line_t t, const char *f, const char *fmt)
{
	int r1;
	int r2;

	UNUSED(f);
	UNUSED(t);

	r1 = snprintf(chan_buf, sizeof(chan_buf), "%s", c->name);
	r2 = snprintf(line_buf, sizeof(line_buf), "%s", fmt);

	assert_gt(r1, 0);
	assert_gt(r2, 0);
}

/* Mock io.c */
const char*
io_err(int err)
{
	UNUSED(err);

	return "err";
}

int
io_sendf(struct connection *c, const char *fmt, ...)
{
	va_list ap;

	UNUSED(c);

	va_start(ap, fmt);
	assert_gt(vsnprintf(send_buf, sizeof(send_buf), fmt, ap), 0);
	va_end(ap);

	return 0;
}

#define X(cmd) static void test_recv_ctcp_request_##cmd(void);
CTCP_EXTENDED_FORMATTING
CTCP_EXTENDED_QUERY


@@ 102,52 54,121 @@ CTCP_EXTENDED_QUERY
CTCP_METADATA_QUERY
#undef X

static struct channel *c_chan;
static struct channel *c_priv;
static struct irc_message m;
static struct server *s;

static void
test_case_insensitive(void)
{
	/* Requests */
	CHECK_REQUEST(":nick!user@host PRIVMSG me :\001clientinfo", 0, 1, 1,
		"CTCP CLIENTINFO from nick",
		"NOTICE nick :\001CLIENTINFO ACTION CLIENTINFO PING SOURCE TIME VERSION\001");

	CHECK_REQUEST(":nick!user@host PRIVMSG me :\001clientinfo\001", 0, 1, 1,
		"CTCP CLIENTINFO from nick",
		"NOTICE nick :\001CLIENTINFO ACTION CLIENTINFO PING SOURCE TIME VERSION\001");

	/* Response */
	CHECK_RESPONSE(":nick!user@host NOTICE me :\001clientinfo FOO BAR BAZ", 0,
		"CTCP CLIENTINFO response from nick: FOO BAR BAZ");

	CHECK_RESPONSE(":nick!user@host NOTICE me :\001clientinfo 123 456 789\001", 0,
		"CTCP CLIENTINFO response from nick: 123 456 789");
}

static void
test_recv_ctcp_request(void)
{
	char m1[] = "";
	char m2[] = " ";
	char m3[] = "\001";
	char m4[] = "\001\001";
	char m5[] = "\001 \001";
	char m6[] = "\001TEST1";
	char m7[] = "\001TEST1\001";
	char m8[] = "\001TEST2 arg1 arg2\001";
	char m9[] = "\001TEST1\001";

	CHECK_REQUEST("nick", "targ", m1, 1, "Received malformed CTCP from nick", "");
	CHECK_REQUEST("nick", "targ", m2, 1, "Received malformed CTCP from nick", "");
	CHECK_REQUEST("nick", "targ", m3, 1, "Received empty CTCP from nick", "");
	CHECK_REQUEST("nick", "targ", m4, 1, "Received empty CTCP from nick", "");
	CHECK_REQUEST("nick", "targ", m5, 1, "Received empty CTCP from nick", "");
	CHECK_REQUEST("nick", "targ", m6, 1, "Received unsupported CTCP request 'TEST1' from nick", "");
	CHECK_REQUEST("nick", "targ", m7, 1, "Received unsupported CTCP request 'TEST1' from nick", "");
	CHECK_REQUEST("nick", "targ", m8, 1, "Received unsupported CTCP request 'TEST2' from nick", "");
	CHECK_REQUEST(NULL, "targ", m9, 1, "Received CTCP from unknown sender", "");
	/* test malformed */
	mock_reset_io();
	mock_reset_state();
	assert_eq(ctcp_request(s, NULL, "me", "\001TEST"), 1);
	assert_eq(mock_line_n, 1);
	assert_eq(mock_send_n, 0);
	assert_strcmp(mock_line[0], "Received CTCP from unknown sender");

	mock_reset_io();
	mock_reset_state();
	assert_eq(ctcp_request(s, "nick", "me", ""), 1);
	assert_eq(mock_line_n, 1);
	assert_eq(mock_send_n, 0);
	assert_strcmp(mock_line[0], "Received malformed CTCP from nick");

	mock_reset_io();
	mock_reset_state();
	assert_eq(ctcp_request(s, "nick", "me", " "), 1);
	assert_eq(mock_line_n, 1);
	assert_eq(mock_send_n, 0);
	assert_strcmp(mock_line[0], "Received malformed CTCP from nick");

	/* test empty */
	CHECK_REQUEST(":nick!user@host PRIVMSG me :\001", 1, 1, 0,
		"Received empty CTCP from nick", "");

	CHECK_REQUEST(":nick!user@host PRIVMSG me :\001\001", 1, 1, 0,
		"Received empty CTCP from nick", "");

	CHECK_REQUEST(":nick!user@host PRIVMSG me :\001 \001", 1, 1, 0,
		"Received empty CTCP from nick", "");

	/* test unsupported */
	CHECK_REQUEST(":nick!user@host PRIVMSG me :\001TEST1", 1, 1, 0,
		"Received unsupported CTCP request 'TEST1' from nick", "");

	CHECK_REQUEST(":nick!user@host PRIVMSG me :\001TEST2\001", 1, 1, 0,
		"Received unsupported CTCP request 'TEST2' from nick", "");

	CHECK_REQUEST(":nick!user@host PRIVMSG me :\001TEST3 arg1 arg2\001", 1, 1, 0,
		"Received unsupported CTCP request 'TEST3' from nick", "");
}

static void
test_recv_ctcp_response(void)
{
	char m1[] = "";
	char m2[] = " ";
	char m3[] = "\001";
	char m4[] = "\001\001";
	char m5[] = "\001 \001";
	char m6[] = "\001TEST1";
	char m7[] = "\001TEST1\001";
	char m8[] = "\001TEST2 arg1 arg2\001";
	char m9[] = "\001TEST1\001";

	CHECK_RESPONSE("nick", "targ", m1, 1, "Received malformed CTCP from nick");
	CHECK_RESPONSE("nick", "targ", m2, 1, "Received malformed CTCP from nick");
	CHECK_RESPONSE("nick", "targ", m3, 1, "Received empty CTCP from nick");
	CHECK_RESPONSE("nick", "targ", m4, 1, "Received empty CTCP from nick");
	CHECK_RESPONSE("nick", "targ", m5, 1, "Received empty CTCP from nick");
	CHECK_RESPONSE("nick", "targ", m6, 1, "Received unsupported CTCP response 'TEST1' from nick");
	CHECK_RESPONSE("nick", "targ", m7, 1, "Received unsupported CTCP response 'TEST1' from nick");
	CHECK_RESPONSE("nick", "targ", m8, 1, "Received unsupported CTCP response 'TEST2' from nick");
	CHECK_RESPONSE(NULL, "targ", m9, 1, "Received CTCP from unknown sender");
	/* test malformed */
	mock_reset_io();
	mock_reset_state();
	assert_eq(ctcp_response(s, NULL, "me", "\001TEST"), 1);
	assert_eq(mock_line_n, 1);
	assert_eq(mock_send_n, 0);
	assert_strcmp(mock_line[0], "Received CTCP from unknown sender");

	mock_reset_io();
	mock_reset_state();
	assert_eq(ctcp_response(s, "nick", "me", ""), 1);
	assert_eq(mock_line_n, 1);
	assert_eq(mock_send_n, 0);
	assert_strcmp(mock_line[0], "Received malformed CTCP from nick");

	mock_reset_io();
	mock_reset_state();
	assert_eq(ctcp_response(s, "nick", "me", " "), 1);
	assert_eq(mock_line_n, 1);
	assert_eq(mock_send_n, 0);
	assert_strcmp(mock_line[0], "Received malformed CTCP from nick");

	/* test empty */
	CHECK_RESPONSE(":nick!user@host NOTICE me :\001", 1,
		"Received empty CTCP from nick");

	CHECK_RESPONSE(":nick!user@host NOTICE me :\001\001", 1,
		"Received empty CTCP from nick");

	CHECK_RESPONSE(":nick!user@host NOTICE me :\001 \001", 1,
		"Received empty CTCP from nick");

	/* test unsupported */
	CHECK_RESPONSE(":nick!user@host NOTICE me :\001TEST1", 1,
		"Received unsupported CTCP response 'TEST1' from nick");

	CHECK_RESPONSE(":nick!user@host NOTICE me :\001TEST2\001", 1,
		"Received unsupported CTCP response 'TEST2' from nick");

	CHECK_RESPONSE(":nick!user@host NOTICE me :\001TEST3 arg1 arg2\001", 1,
		"Received unsupported CTCP response 'TEST3' from nick");
}

static void


@@ 164,26 185,28 @@ test_recv_ctcp_request_action(void)

#define CHECK_ACTION_REQUEST(F, T, M, R, C, L) \
	do { \
	    chan_buf[0] = 0; \
	    line_buf[0] = 0; \
	    assert_eq(ctcp_request(s, (F), (T), (M)), (R)); \
	    assert_strcmp(chan_buf, (C)); \
	    assert_strcmp(line_buf, (L)); \
		mock_reset_io(); \
		mock_reset_state(); \
		assert_eq(ctcp_request(s, (F), (T), (M)), (R)); \
		assert_eq(mock_line_n, 1); \
		assert_eq(mock_send_n, 0); \
		assert_strcmp(mock_chan, (C)); \
		assert_strcmp(mock_line[0], (L)); \
	} while (0)

	/* Action message to me as existing private message */
	CHECK_ACTION_REQUEST("nick", "mynick", m1, 0, "nick", "nick test action 1");
	CHECK_ACTION_REQUEST("nick", "me", m1, 0, "nick", "nick test action 1");

	/* Action message to me as new private message */
	CHECK_ACTION_REQUEST("new_priv", "mynick", m2, 0, "new_priv", "new_priv test action 2");
	CHECK_ACTION_REQUEST("new_priv", "me", m2, 0, "new_priv", "new_priv test action 2");

	/* Action message to existing channel */
	CHECK_ACTION_REQUEST("nick", "chan", m3, 0, "chan", "nick test action 3");

	/* Empty action messages */
	CHECK_ACTION_REQUEST("nick", "mynick", m4, 0, "nick", "nick");
	CHECK_ACTION_REQUEST("nick", "mynick", m5, 0, "nick", "nick");
	CHECK_ACTION_REQUEST("nick", "mynick", m6, 0, "nick", "nick");
	CHECK_ACTION_REQUEST("nick", "me", m4, 0, "nick", "nick");
	CHECK_ACTION_REQUEST("nick", "me", m5, 0, "nick", "nick");
	CHECK_ACTION_REQUEST("nick", "me", m6, 0, "nick", "nick");

	/* Action message to nonexistant channel */
	CHECK_ACTION_REQUEST("nick", "not_a_chan", m7, 1, "h1", "CTCP ACTION: target 'not_a_chan' not found");


@@ 197,19 220,15 @@ test_recv_ctcp_request_action(void)
static void
test_recv_ctcp_request_clientinfo(void)
{
	char m1[] = "\001CLIENTINFO";
	char m2[] = "\001CLIENTINFO\001";
	char m3[] = "\001CLIENTINFO unused args\001";

	CHECK_REQUEST("nick", "targ", m1, 0,
	CHECK_REQUEST(":nick!user@host PRIVMSG me :\001CLIENTINFO", 0, 1, 1,
		"CTCP CLIENTINFO from nick",
		"NOTICE nick :\001CLIENTINFO ACTION CLIENTINFO PING SOURCE TIME VERSION\001");

	CHECK_REQUEST("nick", "targ", m2, 0,
	CHECK_REQUEST(":nick!user@host PRIVMSG me :\001CLIENTINFO\001", 0, 1, 1,
		"CTCP CLIENTINFO from nick",
		"NOTICE nick :\001CLIENTINFO ACTION CLIENTINFO PING SOURCE TIME VERSION\001");

	CHECK_REQUEST("nick", "targ", m3, 0,
	CHECK_REQUEST(":nick!user@host PRIVMSG me :\001CLIENTINFO unused args\001", 0, 1, 1,
		"CTCP CLIENTINFO from nick (unused args)",
		"NOTICE nick :\001CLIENTINFO ACTION CLIENTINFO PING SOURCE TIME VERSION\001");
}


@@ 217,19 236,15 @@ test_recv_ctcp_request_clientinfo(void)
static void
test_recv_ctcp_request_finger(void)
{
	char m1[] = "\001FINGER";
	char m2[] = "\001FINGER\001";
	char m3[] = "\001FINGER unused args\001";

	CHECK_REQUEST("nick", "targ", m1, 0,
	CHECK_REQUEST(":nick!user@host PRIVMSG me :\001FINGER", 0, 1, 1,
		"CTCP FINGER from nick",
		"NOTICE nick :\001FINGER rirc v"VERSION" ("__DATE__")\001");

	CHECK_REQUEST("nick", "targ", m2, 0,
	CHECK_REQUEST(":nick!user@host PRIVMSG me :\001FINGER\001", 0, 1, 1,
		"CTCP FINGER from nick",
		"NOTICE nick :\001FINGER rirc v"VERSION" ("__DATE__")\001");

	CHECK_REQUEST("nick", "targ", m3, 0,
	CHECK_REQUEST(":nick!user@host PRIVMSG me :\001FINGER unused args\001", 0, 1, 1,
		"CTCP FINGER from nick (unused args)",
		"NOTICE nick :\001FINGER rirc v"VERSION" ("__DATE__")\001");
}


@@ 237,38 252,31 @@ test_recv_ctcp_request_finger(void)
static void
test_recv_ctcp_request_ping(void)
{
	char m1[] = "\001PING";
	char m2[] = "\001PING 0\001";
	char m3[] = "\001PING 1 123 abc 0\001";

	/* empty PING message, do nothing */
	CHECK_REQUEST("nick", "targ", m1, 0, "", "");

	CHECK_REQUEST("nick", "targ", m2, 0,
	CHECK_REQUEST(":nick!user@host PRIVMSG me :\001PING", 0, 1, 1,
		"CTCP PING from nick",
		"NOTICE nick :\001PING\001");

	CHECK_REQUEST(":nick!user@host PRIVMSG me :\001PING 0\001", 0, 1, 1,
		"CTCP PING from nick (0)",
		"NOTICE nick :\001PING 0\001");

	CHECK_REQUEST("nick", "targ", m3, 0,
		"CTCP PING from nick",
		"NOTICE nick :\001PING 1 123 abc 0\001");
	CHECK_REQUEST(":nick!user@host PRIVMSG me :\001PING 1 123 abc\001", 0, 1, 1,
		"CTCP PING from nick (1 123 abc)",
		"NOTICE nick :\001PING 1 123 abc\001");
}

static void
test_recv_ctcp_request_source(void)
{
	char m1[] = "\001SOURCE";
	char m2[] = "\001SOURCE\001";
	char m3[] = "\001SOURCE unused args\001";

	CHECK_REQUEST("nick", "targ", m1, 0,
	CHECK_REQUEST(":nick!user@host PRIVMSG me :\001SOURCE", 0, 1, 1,
		"CTCP SOURCE from nick",
		"NOTICE nick :\001SOURCE rcr.io/rirc\001");

	CHECK_REQUEST("nick", "targ", m2, 0,
	CHECK_REQUEST(":nick!user@host PRIVMSG me :\001SOURCE\001", 0, 1, 1,
		"CTCP SOURCE from nick",
		"NOTICE nick :\001SOURCE rcr.io/rirc\001");

	CHECK_REQUEST("nick", "targ", m3, 0,
	CHECK_REQUEST(":nick!user@host PRIVMSG me :\001SOURCE unused args\001", 0, 1, 1,
		"CTCP SOURCE from nick (unused args)",
		"NOTICE nick :\001SOURCE rcr.io/rirc\001");
}


@@ 276,19 284,15 @@ test_recv_ctcp_request_source(void)
static void
test_recv_ctcp_request_time(void)
{
	char m1[] = "\001TIME";
	char m2[] = "\001TIME\001";
	char m3[] = "\001TIME unused args\001";

	CHECK_REQUEST("nick", "targ", m1, 0,
	CHECK_REQUEST(":nick!user@host PRIVMSG me :\001TIME", 0, 1, 1,
		"CTCP TIME from nick",
		"NOTICE nick :\001TIME 1970-01-01T00:00:00\001");

	CHECK_REQUEST("nick", "targ", m2, 0,
	CHECK_REQUEST(":nick!user@host PRIVMSG me :\001TIME\001", 0, 1, 1,
		"CTCP TIME from nick",
		"NOTICE nick :\001TIME 1970-01-01T00:00:00\001");

	CHECK_REQUEST("nick", "targ", m3, 0,
	CHECK_REQUEST(":nick!user@host PRIVMSG me :\001TIME unused args\001", 0, 1, 1,
		"CTCP TIME from nick (unused args)",
		"NOTICE nick :\001TIME 1970-01-01T00:00:00\001");
}


@@ 296,185 300,195 @@ test_recv_ctcp_request_time(void)
static void
test_recv_ctcp_request_userinfo(void)
{
	char m1[] = "\001USERINFO";
	char m2[] = "\001USERINFO\001";
	char m3[] = "\001USERINFO unused args\001";

	CHECK_REQUEST("nick", "targ", m1, 0,
	CHECK_REQUEST(":nick!user@host PRIVMSG me :\001USERINFO", 0, 1, 1,
		"CTCP USERINFO from nick",
		"NOTICE nick :\001USERINFO mynick (r1)\001");
		"NOTICE nick :\001USERINFO me (r1)\001");

	CHECK_REQUEST("nick", "targ", m2, 0,
	CHECK_REQUEST(":nick!user@host PRIVMSG me :\001USERINFO\001", 0, 1, 1,
		"CTCP USERINFO from nick",
		"NOTICE nick :\001USERINFO mynick (r1)\001");
		"NOTICE nick :\001USERINFO me (r1)\001");

	CHECK_REQUEST("nick", "targ", m3, 0,
	CHECK_REQUEST(":nick!user@host PRIVMSG me :\001USERINFO unused args\001", 0, 1, 1,
		"CTCP USERINFO from nick (unused args)",
		"NOTICE nick :\001USERINFO mynick (r1)\001");
		"NOTICE nick :\001USERINFO me (r1)\001");
}

static void
test_recv_ctcp_request_version(void)
{
	char m1[] = "\001VERSION";
	char m2[] = "\001VERSION\001";
	char m3[] = "\001VERSION unused args\001";

	CHECK_REQUEST("nick", "targ", m1, 0,
	CHECK_REQUEST(":nick!user@host PRIVMSG me :\001VERSION", 0, 1, 1,
		"CTCP VERSION from nick",
		"NOTICE nick :\001VERSION rirc v"VERSION" ("__DATE__")\001");

	CHECK_REQUEST("nick", "targ", m2, 0,
	CHECK_REQUEST(":nick!user@host PRIVMSG me :\001VERSION\001", 0, 1, 1,
		"CTCP VERSION from nick",
		"NOTICE nick :\001VERSION rirc v"VERSION" ("__DATE__")\001");

	CHECK_REQUEST("nick", "targ", m3, 0,
	CHECK_REQUEST(":nick!user@host PRIVMSG me :\001VERSION unused args\001", 0, 1, 1,
		"CTCP VERSION from nick (unused args)",
		"NOTICE nick :\001VERSION rirc v"VERSION" ("__DATE__")\001");
}

static void
test_recv_ctcp_response_action(void)
{
	/* CTCP `extended formatting` messages generate no response */

	CHECK_RESPONSE(":nick!user@host NOTICE me :\001ACTION", 1,
		"Received unsupported CTCP response 'ACTION' from nick");

	CHECK_RESPONSE(":nick!user@host NOTICE me :\001ACTION\001", 1,
		"Received unsupported CTCP response 'ACTION' from nick");

	CHECK_RESPONSE(":nick!user@host NOTICE me :\001ACTION foo bar baz\001", 1,
		"Received unsupported CTCP response 'ACTION' from nick");
}

static void
test_recv_ctcp_response_clientinfo(void)
{
	char m1[] = "\001CLIENTINFO";
	char m2[] = "\001CLIENTINFO\001";
	char m3[] = "\001CLIENTINFO FOO BAR BAZ";
	char m4[] = "\001CLIENTINFO 123 456 789\001";

	CHECK_RESPONSE("nick", "targ", m1, 1, "CTCP CLIENTINFO response from nick: empty message");
	CHECK_RESPONSE("nick", "targ", m2, 1, "CTCP CLIENTINFO response from nick: empty message");
	CHECK_RESPONSE("nick", "targ", m3, 0, "CTCP CLIENTINFO response from nick: FOO BAR BAZ");
	CHECK_RESPONSE("nick", "targ", m4, 0, "CTCP CLIENTINFO response from nick: 123 456 789");
	CHECK_RESPONSE(":nick!user@host NOTICE me :\001CLIENTINFO", 1,
		"CTCP CLIENTINFO response from nick: empty message");

	CHECK_RESPONSE(":nick!user@host NOTICE me :\001CLIENTINFO\001", 1,
		"CTCP CLIENTINFO response from nick: empty message");

	CHECK_RESPONSE(":nick!user@host NOTICE me :\001CLIENTINFO FOO BAR BAZ", 0,
		"CTCP CLIENTINFO response from nick: FOO BAR BAZ");

	CHECK_RESPONSE(":nick!user@host NOTICE me :\001CLIENTINFO 123 456 789\001", 0,
		"CTCP CLIENTINFO response from nick: 123 456 789");
}

static void
test_recv_ctcp_response_finger(void)
{
	char m1[] = "\001FINGER";
	char m2[] = "\001FINGER\001";
	char m3[] = "\001FINGER FOO BAR BAZ";
	char m4[] = "\001FINGER 123 456 789\001";

	CHECK_RESPONSE("nick", "targ", m1, 1, "CTCP FINGER response from nick: empty message");
	CHECK_RESPONSE("nick", "targ", m2, 1, "CTCP FINGER response from nick: empty message");
	CHECK_RESPONSE("nick", "targ", m3, 0, "CTCP FINGER response from nick: FOO BAR BAZ");
	CHECK_RESPONSE("nick", "targ", m4, 0, "CTCP FINGER response from nick: 123 456 789");
	CHECK_RESPONSE(":nick!user@host NOTICE me :\001FINGER", 1,
		"CTCP FINGER response from nick: empty message");

	CHECK_RESPONSE(":nick!user@host NOTICE me :\001FINGER\001", 1,
		"CTCP FINGER response from nick: empty message");

	CHECK_RESPONSE(":nick!user@host NOTICE me :\001FINGER FOO BAR BAZ", 0,
		"CTCP FINGER response from nick: FOO BAR BAZ");

	CHECK_RESPONSE(":nick!user@host NOTICE me :\001FINGER 123 456 789\001", 0,
		"CTCP FINGER response from nick: 123 456 789");
}

static void
test_recv_ctcp_response_ping(void)
{
	char m1[] = "\001PING";
	char m2[] = "\001PING 123";
	char m3[] = "\001PING 1a3 345\001";
	char m4[] = "\001PING 123 345a\001";
	char m5[] = "\001PING 125 456789\001";
	char m6[] = "\001PING 123 567890\001";
	char m7[] = "\001PING 120 456789\001";
	char m8[] = "\001PING 120 111111\001";
	char m9[] = "\001PING 120 000000";

	CHECK_RESPONSE("nick", "targ", m1, 1, "CTCP PING response from nick: sec is NULL");
	CHECK_RESPONSE("nick", "targ", m2, 1, "CTCP PING response from nick: usec is NULL");
	CHECK_RESPONSE("nick", "targ", m3, 1, "CTCP PING response from nick: sec is invalid");
	CHECK_RESPONSE("nick", "targ", m4, 1, "CTCP PING response from nick: usec is invalid");
	CHECK_RESPONSE("nick", "targ", m5, 1, "CTCP PING response from nick: invalid timestamp");
	CHECK_RESPONSE("nick", "targ", m6, 1, "CTCP PING response from nick: invalid timestamp");
	CHECK_RESPONSE("nick", "targ", m7, 0, "CTCP PING response from nick: 3.0s");
	CHECK_RESPONSE("nick", "targ", m8, 0, "CTCP PING response from nick: 3.345678s");
	CHECK_RESPONSE("nick", "targ", m9, 0, "CTCP PING response from nick: 3.456789s");
	CHECK_RESPONSE(":nick!user@host NOTICE me :\001PING", 1,
		"CTCP PING response from nick: sec is NULL");

	CHECK_RESPONSE(":nick!user@host NOTICE me :\001PING 123", 1,
		"CTCP PING response from nick: usec is NULL");

	CHECK_RESPONSE(":nick!user@host NOTICE me :\001PING 1a3 345\001", 1,
		"CTCP PING response from nick: sec is invalid");

	CHECK_RESPONSE(":nick!user@host NOTICE me :\001PING 123 345a\001", 1,
		"CTCP PING response from nick: usec is invalid");

	CHECK_RESPONSE(":nick!user@host NOTICE me :\001PING 125 456789\001", 1,
		"CTCP PING response from nick: invalid timestamp");

	CHECK_RESPONSE(":nick!user@host NOTICE me :\001PING 123 567890\001", 1,
		"CTCP PING response from nick: invalid timestamp");

	CHECK_RESPONSE(":nick!user@host NOTICE me :\001PING 120 456789\001", 0,
		"CTCP PING response from nick: 3.0s");

	CHECK_RESPONSE(":nick!user@host NOTICE me :\001PING 120 111111\001", 0,
		"CTCP PING response from nick: 3.345678s");

	CHECK_RESPONSE(":nick!user@host NOTICE me :\001PING 120 000000", 0,
		"CTCP PING response from nick: 3.456789s");
}

static void
test_recv_ctcp_response_source(void)
{
	char m1[] = "\001SOURCE";
	char m2[] = "\001SOURCE\001";
	char m3[] = "\001SOURCE FOO BAR BAZ";
	char m4[] = "\001SOURCE 123 456 789\001";

	CHECK_RESPONSE("nick", "targ", m1, 1, "CTCP SOURCE response from nick: empty message");
	CHECK_RESPONSE("nick", "targ", m2, 1, "CTCP SOURCE response from nick: empty message");
	CHECK_RESPONSE("nick", "targ", m3, 0, "CTCP SOURCE response from nick: FOO BAR BAZ");
	CHECK_RESPONSE("nick", "targ", m4, 0, "CTCP SOURCE response from nick: 123 456 789");
	CHECK_RESPONSE(":nick!user@host NOTICE me :\001SOURCE", 1,
		"CTCP SOURCE response from nick: empty message");

	CHECK_RESPONSE(":nick!user@host NOTICE me :\001SOURCE\001", 1,
		"CTCP SOURCE response from nick: empty message");

	CHECK_RESPONSE(":nick!user@host NOTICE me :\001SOURCE FOO BAR BAZ", 0,
		"CTCP SOURCE response from nick: FOO BAR BAZ");

	CHECK_RESPONSE(":nick!user@host NOTICE me :\001SOURCE 123 456 789\001", 0,
		"CTCP SOURCE response from nick: 123 456 789");
}

static void
test_recv_ctcp_response_time(void)
{
	char m1[] = "\001TIME";
	char m2[] = "\001TIME\001";
	char m3[] = "\001TIME FOO BAR BAZ";
	char m4[] = "\001TIME 123 456 789\001";

	CHECK_RESPONSE("nick", "targ", m1, 1, "CTCP TIME response from nick: empty message");
	CHECK_RESPONSE("nick", "targ", m2, 1, "CTCP TIME response from nick: empty message");
	CHECK_RESPONSE("nick", "targ", m3, 0, "CTCP TIME response from nick: FOO BAR BAZ");
	CHECK_RESPONSE("nick", "targ", m4, 0, "CTCP TIME response from nick: 123 456 789");
	CHECK_RESPONSE(":nick!user@host NOTICE me :\001TIME", 1,
		"CTCP TIME response from nick: empty message");

	CHECK_RESPONSE(":nick!user@host NOTICE me :\001TIME\001", 1,
		"CTCP TIME response from nick: empty message");

	CHECK_RESPONSE(":nick!user@host NOTICE me :\001TIME FOO BAR BAZ", 0,
		"CTCP TIME response from nick: FOO BAR BAZ");

	CHECK_RESPONSE(":nick!user@host NOTICE me :\001TIME 123 456 789\001", 0,
		"CTCP TIME response from nick: 123 456 789");
}

static void
test_recv_ctcp_response_userinfo(void)
{
	char m1[] = "\001USERINFO";
	char m2[] = "\001USERINFO\001";
	char m3[] = "\001USERINFO FOO BAR BAZ";
	char m4[] = "\001USERINFO 123 456 789\001";

	CHECK_RESPONSE("nick", "targs", m1, 1, "CTCP USERINFO response from nick: empty message");
	CHECK_RESPONSE("nick", "targs", m2, 1, "CTCP USERINFO response from nick: empty message");
	CHECK_RESPONSE("nick", "targs", m3, 0, "CTCP USERINFO response from nick: FOO BAR BAZ");
	CHECK_RESPONSE("nick", "targs", m4, 0, "CTCP USERINFO response from nick: 123 456 789");
	CHECK_RESPONSE(":nick!user@host NOTICE me :\001USERINFO", 1,
		"CTCP USERINFO response from nick: empty message");

	CHECK_RESPONSE(":nick!user@host NOTICE me :\001USERINFO\001", 1,
		"CTCP USERINFO response from nick: empty message");

	CHECK_RESPONSE(":nick!user@host NOTICE me :\001USERINFO FOO BAR BAZ", 0,
		"CTCP USERINFO response from nick: FOO BAR BAZ");

	CHECK_RESPONSE(":nick!user@host NOTICE me :\001USERINFO 123 456 789\001", 0,
		"CTCP USERINFO response from nick: 123 456 789");
}

static void
test_recv_ctcp_response_version(void)
{
	char m1[] = "\001VERSION";
	char m2[] = "\001VERSION\001";
	char m3[] = "\001VERSION FOO BAR BAZ";
	char m4[] = "\001VERSION 123 456 789\001";

	CHECK_RESPONSE("nick", "targs", m1, 1, "CTCP VERSION response from nick: empty message");
	CHECK_RESPONSE("nick", "targs", m2, 1, "CTCP VERSION response from nick: empty message");
	CHECK_RESPONSE("nick", "targs", m3, 0, "CTCP VERSION response from nick: FOO BAR BAZ");
	CHECK_RESPONSE("nick", "targs", m4, 0, "CTCP VERSION response from nick: 123 456 789");
}
	CHECK_RESPONSE(":nick!user@host NOTICE me :\001VERSION", 1,
		"CTCP VERSION response from nick: empty message");

static void
test_case_insensitive(void)
{
	/* Requests */
	char m1[] = "\001clientinfo";
	char m2[] = "\001clientinfo\001";
	CHECK_RESPONSE(":nick!user@host NOTICE me :\001VERSION\001", 1,
		"CTCP VERSION response from nick: empty message");

	/* Response */
	char m3[] = "\001clientinfo FOO BAR BAZ";
	char m4[] = "\001clientinfo 123 456 789\001";
	CHECK_RESPONSE(":nick!user@host NOTICE me :\001VERSION FOO BAR BAZ", 0,
		"CTCP VERSION response from nick: FOO BAR BAZ");

	CHECK_REQUEST("nick", "targ", m1, 0,
		"CTCP CLIENTINFO from nick",
		"NOTICE nick :\001CLIENTINFO ACTION CLIENTINFO PING SOURCE TIME VERSION\001");

	CHECK_REQUEST("nick", "targ", m2, 0,
		"CTCP CLIENTINFO from nick",
		"NOTICE nick :\001CLIENTINFO ACTION CLIENTINFO PING SOURCE TIME VERSION\001");

	CHECK_RESPONSE("nick", "targ", m3, 0, "CTCP CLIENTINFO response from nick: FOO BAR BAZ");
	CHECK_RESPONSE("nick", "targ", m4, 0, "CTCP CLIENTINFO response from nick: 123 456 789");
	CHECK_RESPONSE(":nick!user@host NOTICE me :\001VERSION 123 456 789\001", 0,
		"CTCP VERSION response from nick: 123 456 789");
}

int
main(void)
{
	s = server("h1", "p1", NULL, "u1", "r1");
	c_chan = channel("chan", CHANNEL_T_CHANNEL);
	c_priv = channel("nick", CHANNEL_T_PRIVATE);

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

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

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

	server_nick_set(s, "me");

	struct testcase tests[] = {
		TESTCASE(test_case_insensitive),
		TESTCASE(test_recv_ctcp_request),
		TESTCASE(test_recv_ctcp_response),
#define X(cmd) TESTCASE(test_recv_ctcp_request_##cmd),


@@ 483,13 497,15 @@ main(void)
		CTCP_METADATA_QUERY
#undef X
#define X(cmd) TESTCASE(test_recv_ctcp_response_##cmd),
		CTCP_EXTENDED_FORMATTING
		CTCP_EXTENDED_QUERY
		CTCP_METADATA_QUERY
#undef X
		TESTCASE(test_case_insensitive)
	};

	int ret = run_tests(tests);

	server_free(s);

	return ret;
}

M test/handlers/irc_recv.c => test/handlers/irc_recv.c +115 -101
@@ 3,125 3,139 @@
#include "src/components/buffer.c"
#include "src/components/channel.c"
#include "src/components/input.c"
#include "src/components/ircv3.c"
#include "src/components/mode.c"
#include "src/components/server.c"
#include "src/components/user.c"
#include "src/handlers/irc_ctcp.c"
#include "src/handlers/irc_recv.c"
#include "src/handlers/ircv3.c"
#include "src/utils/utils.c"

static char chan_buf[1024];
static char line_buf[1024];
static char send_buf[1024];
#include "test/draw.mock.c"
#include "test/io.mock.c"
#include "test/state.mock.c"

/* Mock state.c */
void
newlinef(struct channel *c, enum buffer_line_t t, const char *f, const char *fmt, ...)
{
	va_list ap;
	int r1;
	int r2;

	UNUSED(f);
	UNUSED(t);

	va_start(ap, fmt);
	r1 = snprintf(chan_buf, sizeof(chan_buf), "%s", c->name);
	r2 = vsnprintf(line_buf, sizeof(line_buf), fmt, ap);
	va_end(ap);

	assert_gt(r1, 0);
	assert_gt(r2, 0);
}

void
newline(struct channel *c, enum buffer_line_t t, const char *f, const char *fmt)
{
	int r1;
	int r2;

	UNUSED(f);
	UNUSED(t);

	r1 = snprintf(chan_buf, sizeof(chan_buf), "%s", c->name);
	r2 = snprintf(line_buf, sizeof(line_buf), "%s", fmt);

	assert_gt(r1, 0);
	assert_gt(r2, 0);
}

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

/* Mock io.c */
const char*
io_err(int err)
{
	UNUSED(err);

	return "err";
}

int
io_sendf(struct connection *c, const char *fmt, ...)
{
	va_list ap;

	UNUSED(c);

	va_start(ap, fmt);
	assert_gt(vsnprintf(send_buf, sizeof(send_buf), fmt, ap), 0);
	va_end(ap);

	return 0;
}

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

	return 0;
}
#define IRC_MESSAGE_PARSE(S) \
	char TOKEN(buf, __LINE__)[] = S; \
	assert_eq(irc_message_parse(&m, TOKEN(buf, __LINE__)), 0);

/* Mock draw.c */
void draw_all(void) { ; }
void draw_bell(void) { ; }
void draw_nav(void) { ; }
void draw_status(void) { ; }
#define CHECK_REQUEST(M, RET, LINE_N, SEND_N, LINE, SEND) \
	do { \
		mock_reset_io(); \
		mock_reset_state(); \
		IRC_MESSAGE_PARSE(M); \
		assert_eq(irc_recv(s, &m), (RET)); \
		assert_eq(mock_line_n, (LINE_N)); \
		assert_eq(mock_send_n, (SEND_N)); \
		assert_strcmp(mock_line[0], (LINE)); \
		assert_strcmp(mock_send[0], (SEND)); \
	} while (0)

/* Mock irc_ctcp.c */
int
ctcp_request(struct server *s, const char *f, const char *t, char *m)
{
	UNUSED(s);
	UNUSED(f);
	UNUSED(t);
	UNUSED(m);
	return 0;
}

int
ctcp_response(struct server *s, const char *f, const char *t, char *m)
{
	UNUSED(s);
	UNUSED(f);
	UNUSED(t);
	UNUSED(m);
	return 0;
}
static struct irc_message m;

static void
test_STUB(void)
test_353(void)
{
	; /* TODO */
	/* 353 <nick> <type> <channel> 1*(<modes><nick>) */

	struct channel *c = channel("#chan", CHANNEL_T_CHANNEL);
	struct server *s = server("host", "post", NULL, "user", "real");
	struct user *u1;
	struct user *u2;
	struct user *u3;
	struct user *u4;

	channel_list_add(&s->clist, c);
	server_nick_set(s, "me");

	/* test errors */
	channel_reset(c);
	CHECK_REQUEST("353 me", 1, 1, 0,
		"RPL_NAMEREPLY: type is null", "");

	channel_reset(c);
	CHECK_REQUEST("353 me =", 1, 1, 0,
		"RPL_NAMEREPLY: channel is null", "");

	channel_reset(c);
	CHECK_REQUEST("353 me = #chan", 1, 1, 0,
		"RPL_NAMEREPLY: nicks is null", "");

	channel_reset(c);
	CHECK_REQUEST("353 me = #x :n1", 1, 1, 0,
		"RPL_NAMEREPLY: channel '#x' not found", "");

	channel_reset(c);
	CHECK_REQUEST("353 me x #chan :n1", 1, 1, 0,
		"RPL_NAMEREPLY: invalid channel flag: 'x'", "");

	channel_reset(c);
	CHECK_REQUEST("353 me = #chan :!n1", 1, 1, 0,
		"RPL_NAMEREPLY: invalid user prefix: '!'", "");

	channel_reset(c);
	CHECK_REQUEST("353 me = #chan :+@n1", 1, 1, 0,
		"RPL_NAMEREPLY: invalid nick: '@n1'", "");

	channel_reset(c);
	CHECK_REQUEST("353 me = #chan :n1 n2 n1", 1, 1, 0,
		"RPL_NAMEREPLY: duplicate nick: 'n1'", "");

	/* test single nick */
	channel_reset(c);
	CHECK_REQUEST("353 me = #chan n1", 0, 0, 0, "", "");

	if (user_list_get(&(c->users), s->casemapping, "n1", 0) == NULL)
		test_fail("Failed to retrieve user n1");

	channel_reset(c);
	CHECK_REQUEST("353 me = #chan :@n1", 0, 0, 0, "", "");

	if (user_list_get(&(c->users), s->casemapping, "n1", 0) == NULL)
		test_fail("Failed to retrieve user n1");

	/* test multiple nicks */
	channel_reset(c);
	CHECK_REQUEST("353 me = #chan :@n1 +n2 n3", 0, 0, 0, "", "");

	if (!(u1 = user_list_get(&(c->users), CASEMAPPING_RFC1459, "n1", 0))
	 || !(u2 = user_list_get(&(c->users), CASEMAPPING_RFC1459, "n2", 0))
	 || !(u3 = user_list_get(&(c->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(u3->prfxmodes.lower, 0);

	/* test multiple nicks, multiprefix enabled */
	s->ircv3_caps.multi_prefix.set = 1;
	channel_reset(c);
	CHECK_REQUEST("353 me = #chan :@n1 +n2 @+n3 +@n4", 0, 0, 0, "", "");

	if (!(u1 = user_list_get(&(c->users), CASEMAPPING_RFC1459, "n1", 0))
	 || !(u2 = user_list_get(&(c->users), CASEMAPPING_RFC1459, "n2", 0))
	 || !(u3 = user_list_get(&(c->users), CASEMAPPING_RFC1459, "n3", 0))
	 || !(u4 = user_list_get(&(c->users), CASEMAPPING_RFC1459, "n4", 0)))
		test_abort("Failed to retrieve users");

	assert_eq(u1->prfxmodes.prefix, '@');
	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')));

	server_free(s);
}


int
main(void)
{
	struct testcase tests[] = {
		TESTCASE(test_STUB)
		TESTCASE(test_353)
	};

	return run_tests(tests);

R test/handlers/irc_recv.c.mock => test/handlers/irc_recv.mock.c +0 -0
M test/handlers/irc_send.c => test/handlers/irc_send.c +218 -191
@@ 3,28 3,36 @@
#include "src/components/buffer.c"
#include "src/components/channel.c"
#include "src/components/input.c"
#include "src/components/ircv3.c"
#include "src/components/mode.c"
#include "src/components/server.c"
#include "src/components/user.c"
#include "src/handlers/irc_send.c"
#include "src/utils/utils.c"

#define CHECK_SEND_PRIVMSG(C, M, R, F, S) \
#include "test/io.mock.c"
#include "test/state.mock.c"

#define CHECK_SEND_PRIVMSG(C, M, RET, LINE_N, SEND_N, LINE, SEND) \
	do { \
	    send_buf[0] = 0; \
	    fail_buf[0] = 0; \
	    assert_eq(irc_send_privmsg(s, (C), (M)), (R)); \
	    assert_strcmp(fail_buf, (F)); \
	    assert_strcmp(send_buf, (S)); \
		mock_reset_io(); \
		mock_reset_state(); \
		assert_eq(irc_send_privmsg(s, (C), (M)), (RET)); \
		assert_eq(mock_line_n, (LINE_N)); \
		assert_eq(mock_send_n, (SEND_N)); \
		assert_strcmp(mock_line[0], (LINE)); \
		assert_strcmp(mock_send[0], (SEND)); \
	} while (0)

#define CHECK_SEND_COMMAND(C, M, R, F, S) \
#define CHECK_SEND_COMMAND(C, M, RET, LINE_N, SEND_N, LINE, SEND) \
	do { \
	    send_buf[0] = 0; \
	    fail_buf[0] = 0; \
	    assert_eq(irc_send_command(s, (C), (M)), (R)); \
	    assert_strcmp(fail_buf, (F)); \
	    assert_strcmp(send_buf, (S)); \
		mock_reset_io(); \
		mock_reset_state(); \
		assert_eq(irc_send_command(s, (C), (M)), (RET)); \
		assert_eq(mock_line_n, (LINE_N)); \
		assert_eq(mock_send_n, (SEND_N)); \
		assert_strcmp(mock_line[0], (LINE)); \
		assert_strcmp(mock_send[0], (SEND)); \
	} while (0)

#define X(cmd) static void test_send_##cmd(void);


@@ 35,105 43,144 @@ SEND_HANDLERS
SEND_CTCP_HANDLERS
#undef X

static char send_buf[1024];
static char fail_buf[1024];
static struct channel *c_chan;
static struct channel *c_priv;
static struct channel *c_serv;
static struct server *s;

/* Mock state.c */
void
newlinef(struct channel *c, enum buffer_line_t t, const char *f, const char *fmt, ...)
static void
test_irc_send_command(void)
{
	va_list ap;
	char m1[] = "";
	char m2[] = " test";
	char m3[] = "test";
	char m4[] = "test arg1 arg2 arg3";
	char m5[] = "privmsg targ test message";
	char m6[] = "privmsg not registered";

	UNUSED(c);
	UNUSED(f);
	UNUSED(t);
	mock_reset_io();
	mock_reset_state();
	assert_eq(irc_send_command(NULL, c_chan, ""), 1);
	assert_eq(mock_line_n, 1);
	assert_eq(mock_send_n, 0);
	assert_strcmp(mock_line[0], "This is not a server");

	va_start(ap, fmt);
	assert_gt(vsnprintf(fail_buf, sizeof(fail_buf), fmt, ap), 0);
	va_end(ap);
}
	CHECK_SEND_COMMAND(c_chan, m1, 1, 1, 0, "Messages beginning with '/' require a command", "");
	CHECK_SEND_COMMAND(c_chan, m2, 1, 1, 0, "Messages beginning with '/' require a command", "");
	CHECK_SEND_COMMAND(c_chan, m3, 0, 0, 1, "", "TEST");
	CHECK_SEND_COMMAND(c_chan, m4, 0, 0, 1, "", "TEST arg1 arg2 arg3");
	CHECK_SEND_COMMAND(c_chan, m5, 0, 0, 1, "", "PRIVMSG targ :test message");

void
newline(struct channel *c, enum buffer_line_t t, const char *f, const char *fmt)
{
	UNUSED(c);
	UNUSED(f);
	UNUSED(t);
	s->registered = 0;

	CHECK_SEND_COMMAND(c_chan, m6, 1, 1, 0, "Not registered with server", "");

	assert_gt(snprintf(fail_buf, sizeof(fail_buf), fmt, sizeof(fail_buf)), 0);
	s->registered = 1;
}

/* Mock io.c */
const char*
io_err(int err)
static void
test_irc_send_privmsg(void)
{
	UNUSED(err);
	char m1[] = "chan test 1";
	char m2[] = "serv test 2";
	char m3[] = "priv test 3";
	char m4[] = "chan test 4";
	char m5[] = "chan not registered";

	CHECK_SEND_PRIVMSG(c_chan, m1, 1, 1, 0, "Not on channel", "");
	CHECK_SEND_PRIVMSG(c_serv, m2, 1, 1, 0, "This is not a channel", "");

	c_chan->joined = 1;

	CHECK_SEND_PRIVMSG(c_priv, m3, 0, 1, 1, "priv test 3", "PRIVMSG priv :priv test 3");
	CHECK_SEND_PRIVMSG(c_chan, m4, 0, 1, 1, "chan test 4", "PRIVMSG chan :chan test 4");
	CHECK_SEND_PRIVMSG(c_chan, "", 1, 1, 0, "Message is empty", "");

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

	return "err";
	s->registered = 0;

	CHECK_SEND_PRIVMSG(c_chan, m5, 1, 1, 0, "Not registered with server", "");

	s->registered = 1;
}

int
io_sendf(struct connection *c, const char *fmt, ...)
static void
test_send_notice(void)
{
	va_list ap;
	char m1[] = "notice";
	char m2[] = "notice test1";
	char m3[] = "notice test2 ";
	char m4[] = "notice test3  ";
	char m5[] = "notice test4 test notice message";

	UNUSED(c);
	CHECK_SEND_COMMAND(c_chan, m1, 1, 1, 0, "Usage: /notice <target> <message>", "");
	CHECK_SEND_COMMAND(c_chan, m2, 1, 1, 0, "Usage: /notice <target> <message>", "");
	CHECK_SEND_COMMAND(c_chan, m3, 1, 1, 0, "Usage: /notice <target> <message>", "");
	CHECK_SEND_COMMAND(c_chan, m4, 0, 0, 1, "", "NOTICE test3 : ");
	CHECK_SEND_COMMAND(c_chan, m5, 0, 0, 1, "", "NOTICE test4 :test notice message");
}

	va_start(ap, fmt);
	assert_gt(vsnprintf(send_buf, sizeof(send_buf), fmt, ap), 0);
	va_end(ap);
static void
test_send_part(void)
{
	char m1[] = "part";
	char m2[] = "part";
	char m3[] = "part";
	char m4[] = "part test part message";

	return 0;
	CHECK_SEND_COMMAND(c_serv, m1, 1, 1, 0, "This is not a channel", "");
	CHECK_SEND_COMMAND(c_priv, m2, 1, 1, 0, "This is not a channel", "");
	CHECK_SEND_COMMAND(c_chan, m3, 0, 0, 1, "", "PART chan :" DEFAULT_PART_MESG);
	CHECK_SEND_COMMAND(c_chan, m4, 0, 0, 1, "", "PART chan :test part message");
}

static void
test_irc_send_command(void)
test_send_privmsg(void)
{
	char m1[] = "";
	char m2[] = " test";
	char m3[] = "test";
	char m4[] = "test arg1 arg2 arg3";
	char m5[] = "privmsg targ test message";
	char m1[] = "privmsg";
	char m2[] = "privmsg test1";
	char m3[] = "privmsg test2 ";
	char m4[] = "privmsg test3  ";
	char m5[] = "privmsg test4 test privmsg message";

	send_buf[0] = 0;
	fail_buf[0] = 0;
	assert_eq(irc_send_command(NULL, c_chan, ""), 1);
	assert_strcmp(fail_buf, "This is not a server");
	assert_strcmp(send_buf, "");

	CHECK_SEND_COMMAND(c_chan, m1, 1, "Messages beginning with '/' require a command", "");
	CHECK_SEND_COMMAND(c_chan, m2, 1, "Messages beginning with '/' require a command", "");
	CHECK_SEND_COMMAND(c_chan, m3, 0, "", "TEST");
	CHECK_SEND_COMMAND(c_chan, m4, 0, "", "TEST arg1 arg2 arg3");
	CHECK_SEND_COMMAND(c_chan, m5, 0, "", "PRIVMSG targ :test message");
	CHECK_SEND_COMMAND(c_chan, m1, 1, 1, 0, "Usage: /privmsg <target> <message>", "");
	CHECK_SEND_COMMAND(c_chan, m2, 1, 1, 0, "Usage: /privmsg <target> <message>", "");
	CHECK_SEND_COMMAND(c_chan, m3, 1, 1, 0, "Usage: /privmsg <target> <message>", "");
	CHECK_SEND_COMMAND(c_chan, m4, 0, 0, 1, "", "PRIVMSG test3 : ");
	CHECK_SEND_COMMAND(c_chan, m5, 0, 0, 1, "", "PRIVMSG test4 :test privmsg message");
}

static void
test_irc_send_privmsg(void)
test_send_quit(void)
{
	char m1[] = "chan test 1";
	char m2[] = "serv test 2";
	char m3[] = "priv test 3";
	char m4[] = "chan test 4";

	CHECK_SEND_PRIVMSG(c_chan, m1, 1, "Not on channel", "");
	CHECK_SEND_PRIVMSG(c_serv, m2, 1, "This is not a channel", "");
	char m1[] = "quit";
	char m2[] = "quit";
	char m3[] = "quit";
	char m4[] = "quit test quit message";

	c_chan->joined = 1;
	CHECK_SEND_COMMAND(c_serv, m1, 0, 0, 1, "", "QUIT :" DEFAULT_QUIT_MESG);
	CHECK_SEND_COMMAND(c_priv, m2, 0, 0, 1, "", "QUIT :" DEFAULT_QUIT_MESG);
	CHECK_SEND_COMMAND(c_chan, m3, 0, 0, 1, "", "QUIT :" DEFAULT_QUIT_MESG);
	CHECK_SEND_COMMAND(c_chan, m4, 0, 0, 1, "", "QUIT :test quit message");
}

	CHECK_SEND_PRIVMSG(c_priv, m3, 0, "priv test 3", "PRIVMSG priv :priv test 3");
	CHECK_SEND_PRIVMSG(c_chan, m4, 0, "chan test 4", "PRIVMSG chan :chan test 4");
	CHECK_SEND_PRIVMSG(c_chan, "", 1, "Message is empty", "");
static void
test_send_topic(void)
{
	char m1[] = "topic";
	char m2[] = "topic";
	char m3[] = "topic";
	char m4[] = "topic test new topic";

	send_buf[0] = 0;
	fail_buf[0] = 0;
	assert_eq(irc_send_privmsg(NULL, c_chan, "test"), 1);
	assert_strcmp(fail_buf, "This is not a server");
	assert_strcmp(send_buf, "");
	CHECK_SEND_COMMAND(c_serv, m1, 1, 1, 0, "This is not a channel", "");
	CHECK_SEND_COMMAND(c_priv, m2, 1, 1, 0, "This is not a channel", "");
	CHECK_SEND_COMMAND(c_chan, m3, 0, 0, 1, "", "TOPIC chan");
	CHECK_SEND_COMMAND(c_chan, m4, 0, 0, 1, "", "TOPIC chan :test new topic");
}

static void


@@ 143,9 190,9 @@ test_send_ctcp_action(void)
	char m2[] = "ctcp-action test action";
	char m3[] = "ctcp-action test action";

	CHECK_SEND_COMMAND(c_chan, m1, 0, "", "PRIVMSG chan :\001ACTION test action\001");
	CHECK_SEND_COMMAND(c_priv, m2, 0, "", "PRIVMSG priv :\001ACTION test action\001");
	CHECK_SEND_COMMAND(c_serv, m3, 1, "This is not a channel", "");
	CHECK_SEND_COMMAND(c_chan, m1, 0, 0, 1, "", "PRIVMSG chan :\001ACTION test action\001");
	CHECK_SEND_COMMAND(c_priv, m2, 0, 0, 1, "", "PRIVMSG priv :\001ACTION test action\001");
	CHECK_SEND_COMMAND(c_serv, m3, 1, 1, 0, "This is not a channel", "");
}

static void


@@ 156,10 203,10 @@ test_send_ctcp_clientinfo(void)
	char m3[] = "ctcp-clientinfo";
	char m4[] = "ctcp-clientinfo targ";

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

static void


@@ 170,10 217,10 @@ test_send_ctcp_finger(void)
	char m3[] = "ctcp-finger";
	char m4[] = "ctcp-finger targ";

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

static void


@@ 186,16 233,21 @@ test_send_ctcp_ping(void)

	char *p1;
	char *p2;
	const char *arg1;
	const char *arg2;
	const char *arg3;

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

	send_buf[0] = 0;
	fail_buf[0] = 0;
	/* test send to channel */
	errno = 0;
	mock_reset_io();
	mock_reset_state();

	assert_eq(irc_send_command(s, c_priv, m3), 0);

	p1 = strchr(send_buf, '\001');
	p1 = strchr(mock_send[0], '\001');
	p2 = strchr(p1 + 1, '\001');
	assert_true(p1 != NULL);
	assert_true(p2 != NULL);


@@ 203,18 255,28 @@ test_send_ctcp_ping(void)
	*p1++ = 0;
	*p2++ = 0;

	assert_ptr_not_null(strsep(&p1));
	assert_ptr_not_null(strsep(&p1));
	assert_eq(mock_line_n, 0);
	assert_eq(mock_send_n, 1);
	/* truncated by ctcp delimeter */
	assert_strcmp(mock_send[0], "PRIVMSG priv :");

	assert_strcmp(fail_buf, "");
	assert_strcmp(send_buf, "PRIVMSG priv :");
	assert_ptr_not_null((arg1 = strsep(&p1)));
	assert_ptr_not_null((arg2 = strsep(&p1)));
	assert_ptr_not_null((arg3 = strsep(&p1)));

	send_buf[0] = 0;
	fail_buf[0] = 0;
	assert_strcmp(arg1, "PING");
	assert_gt(strtoul(arg2, NULL, 10), 0); /* sec */
	assert_gt(strtoul(arg3, NULL, 10), 0); /* usec */
	assert_eq(errno, 0);

	/* test send to target */
	errno = 0;
	mock_reset_io();
	mock_reset_state();

	assert_eq(irc_send_command(s, c_priv, m4), 0);

	p1 = strchr(send_buf, '\001');
	p1 = strchr(mock_send[0], '\001');
	p2 = strchr(p1 + 1, '\001');
	assert_true(p1 != NULL);
	assert_true(p2 != NULL);


@@ 222,11 284,19 @@ test_send_ctcp_ping(void)
	*p1++ = 0;
	*p2++ = 0;

	assert_ptr_not_null(strsep(&p1));
	assert_ptr_not_null(strsep(&p1));
	assert_eq(mock_line_n, 0);
	assert_eq(mock_send_n, 1);
	/* truncated by ctcp delimeter */
	assert_strcmp(mock_send[0], "PRIVMSG targ :");

	assert_ptr_not_null((arg1 = strsep(&p1)));
	assert_ptr_not_null((arg2 = strsep(&p1)));
	assert_ptr_not_null((arg3 = strsep(&p1)));

	assert_strcmp(fail_buf, "");
	assert_strcmp(send_buf, "PRIVMSG targ :");
	assert_strcmp(arg1, "PING");
	assert_gt(strtoul(arg2, NULL, 10), 0); /* sec */
	assert_gt(strtoul(arg3, NULL, 10), 0); /* usec */
	assert_eq(errno, 0);
}

static void


@@ 237,10 307,10 @@ test_send_ctcp_source(void)
	char m3[] = "ctcp-source";
	char m4[] = "ctcp-source targ";

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