~rcr/rirc

bbc84bcb6b150dc5369f2143c251385b4d93eb9c — Richard Robbins 1 year, 8 months ago 56b22ba + 803f557
Merge branch 'dev' into static_analysis
A .builds/alpine.yml => .builds/alpine.yml +23 -0
@@ 0,0 1,23 @@
image: alpine/edge

packages:
  - cmake
  - gperf
  - perl

sources:
  - https://git.sr.ht/~rcr/rirc

tasks:
  - setup: |
      cd rirc
      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 .
  - build: |
      cd rirc
      make rirc debug test

A .builds/archlinux.yml => .builds/archlinux.yml +54 -0
@@ 0,0 1,54 @@
image: archlinux

packages:
  - cmake
  - curl
  - gperf
  - perl
  - unzip

sources:
  - https://git.sr.ht/~rcr/rirc

environment:
  SONAR_VER: 4.2.0.1873

secrets:
  - 3f03ec26-5538-4843-b43c-aea06c080cc2

tasks:
  - setup: |
      cd rirc
      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 .
  - build: |
      cd rirc
      make rirc
      make debug
      make test
  - static-analysis: |
      cd rirc
      branch=$(git name-rev --name-only HEAD)
      [ $branch = "remotes/origin/static_analysis" ] || complete-build
      curl -o build-wrapper.zip https://sonarcloud.io/static/cpp/build-wrapper-linux-x86.zip
      curl -o sonar-scanner.zip https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-$SONAR_VER-linux.zip
      unzip build-wrapper.zip
      unzip sonar-scanner.zip
      ./scripts/coverage.sh
      echo >> sonar-project.properties "sonar.branch.name=$branch"
      echo >> sonar-project.properties "sonar.cfamily.build-wrapper-output=bw-output"
      echo >> sonar-project.properties "sonar.cfamily.gcov.reportsPath=."
      echo >> sonar-project.properties "sonar.coverage.exclusions=test"
      echo >> sonar-project.properties "sonar.host.url=https://sonarcloud.io"
      echo >> sonar-project.properties "sonar.organization=rcr-github"
      echo >> sonar-project.properties "sonar.projectKey=rcr_rirc"
      echo >> sonar-project.properties "sonar.sources=src"
      echo >> sonar-project.properties "sonar.tests=test"
      cat ~/sonarcloud-login >> sonar-project.properties
      ./build-wrapper-linux-x86/build-wrapper-linux-x86-64 --out-dir bw-output make clean debug test
      ./sonar-scanner-$SONAR_VER-linux/bin/sonar-scanner

M .gitignore => .gitignore +3 -0
@@ 6,7 6,10 @@
*.t
*.td
bld
compile_commands.json
config.h
coverage
mbedtls
rirc
rirc.debug
rirc.out

A .gitmodules => .gitmodules +3 -0
@@ 0,0 1,3 @@
[submodule "mbedtls"]
	path = mbedtls
	url = https://github.com/ARMmbed/mbedtls.git

D .travis.yml => .travis.yml +0 -53
@@ 1,53 0,0 @@
sudo: false
language: c

env:
  global:
    - secure: "NXwB/s0ZpQr0kJTESNxuVCrIVgX2ZsivmiODwK2LDFT1Nchdijd/Ope0cHb92FxURLcDetnl69EWlmLD5ROZQ5Qc+XakAdAEfy2ZrrCq14S9fSZzUtLB0wMW/GZ5hVneSnFmbprR3zQbTbcVvpTyvzDEr26Dd+HQNP0QQsOJ7B5kanLwKeiuNy437pjIOm7x4sjaylnA/b6FlV3a26C7eGN2ZiFCGT++9y3YlJAlxNlySSI+phRe0bihna/RygoYAs5efXefAuibEjtG5kL29cKMKzwEdXEFfDZcl0QYjkrAJqXUUC54YqIWnY56mhDzn0nfWIScQYth0ZS/vhteA8x+MYKFgGnqD9LBw+TsvRO3ZQ1iVrI5NBM6P2Ka6mAVv8OODN2EwWpwoqm2PJ36ZZ+2vJHBwHw10i3pDR48Fzw0x9Av6rkI5lS0hFLMogXtTaM2IlWu4VIevVRBICzrEz47LeOvJs17MZPYeOUhCFrG0ZP/8DR9nqJpmHVFA9FYcaalD/SXfDbBJTI+yV37mgfSM1zJ8tApPy4KOZ1wM2F0+COqEdxHqTJp2u9c1VQo1NegsYxxdB3mOYMLrTnAzQ5WOUdDezcFkAT+QE5WVhGUzpT4lzM1kVirJa2y3beh2i7uHfP2X+EtzJvWRbefJYQJNo8nYXXEwpdJ5v/0LyI="

addons:
  coverity_scan:
    project:
      name: "rcr/rirc"
      description: "Coverity scan of the static_analysis branch"
    notification_email: mail@rcr.io
    build_command_prepend: "make clean"
    build_command: "make test debug rirc -j2"
    branch_pattern: static_analysis

  sonarcloud:
    organization: "rcr-github"

  apt:
    packages: gperf
  homebrew:
    packages: gperf

matrix:
  include:
    - os: linux
      env: CC=gcc

    - os: linux
      env: CC=clang

    - os: osx
      env: CC=clang

before_install:
  - test "${TRAVIS_BRANCH}" != "static_analysis" -o "${TRAVIS_JOB_NUMBER##*.}" = "1" || exit 0

script:
  - set -e
  - make -e clean test rirc debug
  - test "${TRAVIS_JOB_NUMBER##*.}" = "1" || exit 0
  - build-wrapper-linux-x86-64 --out-dir bw-output make clean debug test
  - ./scripts/coverage.sh
  - echo "" > sonar-project.properties
  - echo "sonar.cfamily.build-wrapper-output=bw-output" >> sonar-project.properties
  - echo "sonar.cfamily.gcov.reportsPath=." >> sonar-project.properties
  - echo "sonar.coverage.exclusions=test" >> sonar-project.properties
  - echo "sonar.projectKey=rcr_rirc" >> sonar-project.properties
  - echo "sonar.sources=src" >> sonar-project.properties
  - echo "sonar.tests=test" >> sonar-project.properties
  - sonar-scanner

M CHANGELOG => CHANGELOG +8 -0
@@ 3,6 3,14 @@ Summary of notable changes and features

## Unreleased (dev)
### Features
 - add command :disconnect
 - add SSL support
    - add mbedtls git submodule
    - add CA_CERT_PATH define to config.h
    - changed default port to 6697 for SSL
 - changed standard versions
    - c99 -> c11
    - POSIX.1-2001 -> POSIX.1-2008
### Fixes

## [0.1.2]

M Makefile => Makefile +39 -26
@@ 3,24 3,32 @@
VERSION := 0.1.2

# Release and debug build executable names
EXE_R := rirc
EXE_D := rirc.debug
BIN_R := rirc
BIN_D := rirc.debug

# Install paths
EXE_DIR = /usr/local/bin
BIN_DIR = /usr/local/bin
MAN_DIR = /usr/local/share/man/man1

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

TLS_INCLUDE := \
 -I./mbedtls/include

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

CC := cc
PP := cc -E
CFLAGS   := $(CC_EXT) -I. $(STDS) -DVERSION=\"$(VERSION)\" -Wall -Wextra -pedantic -O2 -flto
CFLAGS_D := $(CC_EXT) -I. $(STDS) -DVERSION=\"$(VERSION)\" -Wall -Wextra -pedantic -O0 -g -DDEBUG
LDFLAGS  := $(LD_EXT) -pthread
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
LDFLAGS  := $(LD_EXT) -lpthread

# Build, source, test source directories
DIR_B := bld


@@ 37,28 45,29 @@ SUBDIRS += $(shell find $(DIR_S) -name '*.gperf' -exec dirname {} \; | sort -u)
OBJS_D := $(patsubst $(DIR_S)/%.c, $(DIR_B)/%.db.o, $(SRC))
OBJS_R := $(patsubst $(DIR_S)/%.c, $(DIR_B)/%.o,    $(SRC))
OBJS_T := $(patsubst $(DIR_S)/%.c, $(DIR_B)/%.t,    $(SRC))
OBJS_T += $(DIR_B)/utils/tree.t # Header only file

# Gperf generated source files
OBJS_G := $(patsubst %.gperf, %.gperf.out, $(SRC_G))

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

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

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

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


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

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

config.h:
	cp config.def.h config.h

clean:
	rm -rf $(DIR_B) $(EXE_R) $(EXE_D)
	rm -rf $(DIR_B) $(BIN_R) $(BIN_D)
	find . -name "*gperf.out" -print0 | xargs -0 -I % rm %

install: $(EXE_R)
	@echo installing executable to $(EXE_DIR)
install: $(BIN_R)
	@echo installing executable to $(BIN_DIR)
	@echo installing manual page to $(MAN_DIR)
	@mkdir -p $(EXE_DIR)
	@mkdir -p $(BIN_DIR)
	@mkdir -p $(MAN_DIR)
	@cp -f rirc $(EXE_DIR)
	@chmod 755 $(EXE_DIR)/rirc
	@cp -f rirc $(BIN_DIR)
	@chmod 755 $(BIN_DIR)/rirc
	@sed "s/VERSION/$(VERSION)/g" < rirc.1 > $(MAN_DIR)/rirc.1

uninstall:
	rm -f $(EXE_DIR)/rirc
	rm -f $(BIN_DIR)/rirc
	rm -f $(MAN_DIR)/rirc.1

all:   $(EXE_R)
debug: $(EXE_D)
all:   $(BIN_R)
debug: $(BIN_D)
test:  $(DIR_B) $(OBJS_G) $(OBJS_T)

-include $(OBJS_R:.o=.d)

M README.md => README.md +21 -8
@@ 27,19 27,31 @@
# rirc
A minimalistic irc client written in C.

While still under development, it currently supports many
features which you would expect from a basic irc client.
rirc supports only TLS connections, the default port is 6697

## Configuring:

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

## Building:
rirc requires the latest version of GNU gperf to compile.

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

    make
Initialize, configure and build mbedtls:

Or
    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 ..

    make debug
Build rirc:

    make

## Installing:
Default install path:


@@ 54,11 66,11 @@ Edit `Makefile` to alter install path if needed, then:
## Usage:

    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
    

    Options:
      -s, --server=SERVER       Connect to SERVER
      -p, --port=PORT           Connect to SERVER using PORT


@@ 70,10 82,11 @@ Edit `Makefile` to alter install path if needed, then:

Commands:

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

Keys:


R config.h => config.def.h +2 -0
@@ 85,6 85,8 @@

/* [NETWORK] */

#define CA_CERT_PATH "/etc/ssl/"

/* Seconds before displaying ping
 *   Integer, [0, 150, 86400]
 *   (0: no ping handling) */

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

M rirc.1 => rirc.1 +5 -5
@@ 23,6 23,8 @@ rirc \- a minimalistic irc client written in C
.P
rirc is a lightweight Internet Relay Chat client.
.P
rirc only supports TLS connections, the default port is 6697
.P
Customization of rirc can be accomplished by editing the
.I config.h
file and (re)compiling the source code.


@@ 112,6 114,7 @@ Commands:
  :clear;
  :close;
  :connect;[host [port] [pass] [user] [real]]
  :disconnect
  :quit;
.TE



@@ 144,10 147,7 @@ See the
file in the source directory for the terms of redistribution.
.SH SEE ALSO
See
.UR http://rcr.io/rirc/
.UR
http://rcr.io/rirc/
.UE
for additional documentation.
.SH BUGS
See the
.I TODO
file in the source directory for current known issues.

M scripts/coverage.sh => scripts/coverage.sh +4 -5
@@ 4,12 4,11 @@ set -e

CDIR="coverage"

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

eval $ENV make -e clean test
make -e clean test

rm -rf $CDIR
mkdir -p $CDIR

A scripts/pre-commit.sh => scripts/pre-commit.sh +14 -0
@@ 0,0 1,14 @@
#!/bin/sh
#
# pre-commit testing hook, setup:
#   ln -s ../../scripts/pre-commit.sh .git/hooks/pre-commit

echo "Running pre-commit hook..."

RESULTS=$(make test)

if [[ "$RESULTS" == *"failure"* ]];
then
	echo -e "$RESULTS"
	exit 1
fi

M scripts/stack_usage.sh => scripts/stack_usage.sh +4 -5
@@ 2,10 2,9 @@

set -e

ENV=""
ENV="$ENV CC=\"gcc\""
ENV="$ENV CC_EXT=\"-fstack-usage\""
export CC=gcc
export CC_EXT="-fstack-usage"

eval $ENV make -e clean debug
make -e clean debug

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

M src/components/server.c => src/components/server.c +52 -48
@@ 19,7 19,7 @@ struct opt
	char *val;
};

static int parse_opt(struct opt*, char**);
static int parse_005(struct opt*, char**);
static int server_cmp(const struct server*, const char*, const char*);

#define X(cmd) static int server_set_##cmd(struct server*, char*);


@@ 163,40 163,35 @@ server_set_004(struct server *s, char *str)
{
	/* <server_name> <version> <user_modes> <chan_modes> */

	struct channel *c = s->channel;
	const char *server_name; /* Not used */
	const char *version;     /* Not used */
	const char *user_modes;  /* Configure server usermodes */
	const char *chan_modes;  /* Configure server chanmodes */

	char *saveptr;
	char *server_name, /* Not used */
	     *version,     /* Not used */
	     *user_modes,  /* Configure server usermodes */
	     *chan_modes;  /* Configure server chanmodes */
	if (!(server_name = strsep(&str)))
		server_error(s, "invalid numeric 004: server_name is null");

	if (!(server_name = strtok_r(str, " ", &saveptr)))
		newline(c, 0, "-!!-", "invalid numeric 004: server_name is null");
	if (!(version = strsep(&str)))
		server_error(s, "invalid numeric 004: version is null");

	if (!(version = strtok_r(NULL, " ", &saveptr)))
		newline(c, 0, "-!!-", "invalid numeric 004: version is null");
	if (!(user_modes = strsep(&str)))
		server_error(s, "invalid numeric 004: user_modes is null");

	if (!(user_modes = strtok_r(NULL, " ", &saveptr)))
		newline(c, 0, "-!!-", "invalid numeric 004: user_modes is null");

	if (!(chan_modes = strtok_r(NULL, " ", &saveptr)))
		newline(c, 0, "-!!-", "invalid numeric 004: chan_modes is null");
	if (!(chan_modes = strsep(&str)))
		server_error(s, "invalid numeric 004: chan_modes is null");

	if (user_modes) {

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

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

	if (chan_modes) {

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

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



@@ 207,12 202,25 @@ server_set_005(struct server *s, char *str)

	struct opt opt;

	while (parse_opt(&opt, &str)) {
	while (parse_005(&opt, &str)) {

		int (*server_set)(struct server*, char*) = NULL;

		#define X(cmd) \
		if (!strcmp(opt.arg, #cmd) && server_set_##cmd(s, opt.val)) \
			newlinef(s->channel, 0, "-!!-", "invalid %s: %s", #cmd, opt.val);
		if (!strcmp(opt.arg, #cmd)) \
			server_set = server_set_##cmd;
		HANDLED_005
		#undef X

		if (server_set) {
			if (opt.val == NULL) {
				server_error(s, "invalid numeric 005 %s: value is NULL", opt.arg);
			} else if ((*server_set)(s, opt.val)) {
				server_error(s, "invalid numeric 005 %s: %s", opt.arg, opt.val);
			} else {
				debug("Setting numeric 005 %s: %s", opt.arg, opt.val);
			}
		}
	}
}



@@ 272,7 280,7 @@ server_cmp(const struct server *s, const char *host, const char *port)
}

static int
parse_opt(struct opt *opt, char **str)
parse_005(struct opt *opt, char **str)
{
	/* Parse a single argument from numeric 005 (ISUPPORT)
	 *


@@ 316,7 324,7 @@ parse_opt(struct opt *opt, char **str)
	opt->arg = NULL;
	opt->val = NULL;

	if (!str_trim(&p))
	if (!strtrim(&p))
		return 0;

	if (!isalnum(*p))


@@ 344,18 352,20 @@ parse_opt(struct opt *opt, char **str)
static int
server_set_CASEMAPPING(struct server *s, char *val)
{
	if (val == NULL)
		return 0;
	else if (!strcmp(val, "ascii"))
	if (!strcmp(val, "ascii")) {
		s->casemapping = CASEMAPPING_ASCII;
	else if (!strcmp(val, "rfc1459"))
		return 0;
	}

	if (!strcmp(val, "rfc1459")) {
		s->casemapping = CASEMAPPING_RFC1459;
	else if (!strcmp(val, "strict-rfc1459"))
		s->casemapping = CASEMAPPING_STRICT_RFC1459;
	else
		return 0;
	}

	debug("Setting numeric 005 CASEMAPPING: %s", val);
	if (!strcmp(val, "strict-rfc1459")) {
		s->casemapping = CASEMAPPING_STRICT_RFC1459;
		return 0;
	}

	return 1;
}


@@ 363,25 373,19 @@ server_set_CASEMAPPING(struct server *s, char *val)
static int
server_set_CHANMODES(struct server *s, char *val)
{
	debug("Setting numeric 005 CHANMODES: %s", val);

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

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

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

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

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

void

M src/components/server.h => src/components/server.h +9 -0
@@ 6,6 6,9 @@
#include "src/components/mode.h"
#include "src/utils/utils.h"

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

struct server
{
	const char *host;


@@ 33,6 36,12 @@ struct server
	unsigned ping;
	unsigned quitting : 1;
	void *connection;
	// TODO: move this to utils
	struct {
		size_t i;
		char cl;
		char buf[IRC_MESSAGE_LEN + 1]; /* callback message buffer */
	} read;
};

struct server_list

M src/components/user.h => src/components/user.h +2 -2
@@ 14,7 14,7 @@ enum user_err

struct user
{
	AVL_NODE(user) ul;
	TREE_NODE(user) ul;
	const char *nick;
	size_t nick_len;
	struct mode prfxmodes;


@@ 23,7 23,7 @@ struct user

struct user_list
{
	AVL_HEAD(user);
	TREE_HEAD(user);
	unsigned int count;
};


M src/handlers/irc_ctcp.c => src/handlers/irc_ctcp.c +21 -22
@@ 25,7 25,6 @@
static int
parse_ctcp(struct server *s, const char *from, char **args, const char **cmd)
{
	char *saveptr;
	char *message = *args;
	char *command;
	char *p;


@@ 41,14 40,14 @@ parse_ctcp(struct server *s, const char *from, char **args, const char **cmd)

	*message++ = 0;

	if (!(command = strtok_r(message, " ", &saveptr)))
	if (!(command = strsep(&message)))
		failf(s, "Received empty CTCP from %s", from);

	for (p = command; *p; p++)
		*p = toupper(*p);

	*cmd = command;
	*args = saveptr;
	*args = strtrim(&message);

	return 0;
}


@@ 104,7 103,7 @@ ctcp_request_action(struct server *s, const char *from, const char *targ, char *
		failf(s, "CTCP ACTION: target '%s' not found", targ);
	}

	if (str_trim(&m))
	if (strtrim(&m))
		newlinef(c, 0, "*", "%s %s", from, m);
	else
		newlinef(c, 0, "*", "%s", from);


@@ 117,7 116,7 @@ ctcp_request_clientinfo(struct server *s, const char *from, const char *targ, ch
{
	UNUSED(targ);

	if (str_trim(&m))
	if (strtrim(&m))
		server_info(s, "CTCP CLIENTINFO from %s (%s)", from, m);
	else
		server_info(s, "CTCP CLIENTINFO from %s", from);


@@ 132,7 131,7 @@ ctcp_request_finger(struct server *s, const char *from, const char *targ, char *
{
	UNUSED(targ);

	if (str_trim(&m))
	if (strtrim(&m))
		server_info(s, "CTCP FINGER from %s (%s)", from, m);
	else
		server_info(s, "CTCP FINGER from %s", from);


@@ 147,9 146,10 @@ ctcp_request_ping(struct server *s, const char *from, const char *targ, char *m)
{
	UNUSED(targ);

	server_info(s, "CTCP PING from %s", from);

	sendf(s, "NOTICE %s :\001PING %s\001", from, m);
	if (strtrim(&m)) {
		server_info(s, "CTCP PING from %s", from);
		sendf(s, "NOTICE %s :\001PING %s\001", from, m);
	}

	return 0;
}


@@ 159,7 159,7 @@ ctcp_request_source(struct server *s, const char *from, const char *targ, char *
{
	UNUSED(targ);

	if (str_trim(&m))
	if (strtrim(&m))
		server_info(s, "CTCP SOURCE from %s (%s)", from, m);
	else
		server_info(s, "CTCP SOURCE from %s", from);


@@ 179,7 179,7 @@ ctcp_request_time(struct server *s, const char *from, const char *targ, char *m)

	UNUSED(targ);

	if (str_trim(&m))
	if (strtrim(&m))
		server_info(s, "CTCP TIME from %s (%s)", from, m);
	else
		server_info(s, "CTCP TIME from %s", from);


@@ 207,7 207,7 @@ ctcp_request_userinfo(struct server *s, const char *from, const char *targ, char
{
	UNUSED(targ);

	if (str_trim(&m))
	if (strtrim(&m))
		server_info(s, "CTCP USERINFO from %s (%s)", from, m);
	else
		server_info(s, "CTCP USERINFO from %s", from);


@@ 222,7 222,7 @@ ctcp_request_version(struct server *s, const char *from, const char *targ, char 
{
	UNUSED(targ);

	if (str_trim(&m))
	if (strtrim(&m))
		server_info(s, "CTCP VERSION from %s (%s)", from, m);
	else
		server_info(s, "CTCP VERSION from %s", from);


@@ 237,7 237,7 @@ ctcp_response_clientinfo(struct server *s, const char *from, const char *targ, c
{
	UNUSED(targ);

	if (!str_trim(&m))
	if (!strtrim(&m))
		failf(s, "CTCP CLIENTINFO response from %s: empty message", from);

	server_info(s, "CTCP CLIENTINFO response from %s: %s", from, m);


@@ 250,7 250,7 @@ ctcp_response_finger(struct server *s, const char *from, const char *targ, char 
{
	UNUSED(targ);

	if (!str_trim(&m))
	if (!strtrim(&m))
		failf(s, "CTCP FINGER response from %s: empty message", from);

	server_info(s, "CTCP FINGER response from %s: %s", from, m);


@@ 261,7 261,6 @@ 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)
{
	char *saveptr;
	const char *sec;
	const char *usec;
	long long unsigned res;


@@ 274,10 273,10 @@ ctcp_response_ping(struct server *s, const char *from, const char *targ, char *m

	UNUSED(targ);

	if (!(sec = strtok_r(m, " ", &saveptr)))
	if (!(sec = strsep(&m)))
		failf(s, "CTCP PING response from %s: sec is NULL", from);

	if (!(usec = strtok_r(NULL, " ", &saveptr)))
	if (!(usec = strsep(&m)))
		failf(s, "CTCP PING response from %s: usec is NULL", from);

	for (const char *p = sec; *p; p++) {


@@ 327,7 326,7 @@ ctcp_response_source(struct server *s, const char *from, const char *targ, char 
{
	UNUSED(targ);

	if (!str_trim(&m))
	if (!strtrim(&m))
		failf(s, "CTCP SOURCE response from %s: empty message", from);

	server_info(s, "CTCP SOURCE response from %s: %s", from, m);


@@ 340,7 339,7 @@ ctcp_response_time(struct server *s, const char *from, const char *targ, char *m
{
	UNUSED(targ);

	if (!str_trim(&m))
	if (!strtrim(&m))
		failf(s, "CTCP TIME response from %s: empty message", from);

	server_info(s, "CTCP TIME response from %s: %s", from, m);


@@ 353,7 352,7 @@ ctcp_response_userinfo(struct server *s, const char *from, const char *targ, cha
{
	UNUSED(targ);

	if (!str_trim(&m))
	if (!strtrim(&m))
		failf(s, "CTCP USERINFO response from %s: empty message", from);

	server_info(s, "CTCP USERINFO response from %s: %s", from, m);


@@ 366,7 365,7 @@ ctcp_response_version(struct server *s, const char *from, const char *targ, char
{
	UNUSED(targ);

	if (!str_trim(&m))
	if (!strtrim(&m))
		failf(s, "CTCP VERSION response from %s: empty message", from);

	server_info(s, "CTCP VERSION response from %s: %s", from, m);

M src/handlers/irc_recv.c => src/handlers/irc_recv.c +4 -5
@@ 206,7 206,7 @@ static const irc_recv_f irc_numerics[] = {
	[491] = irc_error,  /* ERR_NOOPERHOST */
	[501] = irc_error,  /* ERR_UMODEUNKNOWNFLAG */
	[502] = irc_error,  /* ERR_USERSDONTMATCH */
	[704] = irc_ignore, /* RPL_HELPSTART */
	[704] = irc_info,   /* RPL_HELPSTART */
	[705] = irc_info,   /* RPL_HELP */
	[706] = irc_ignore, /* RPL_ENDOFHELP */
};


@@ 230,7 230,7 @@ irc_message(struct server *s, struct irc_message *m, const char *from)
{
	char *trailing;

	if (!str_trim(&m->params))
	if (!strtrim(&m->params))
		return 0;

	if (irc_message_split(m, &trailing)) {


@@ 481,7 481,6 @@ irc_353(struct server *s, struct irc_message *m)
{
	/* 353 ("="/"*"/"@") <channel> *([ "@" / "+" ]<nick>) */

	char *saveptr;
	char *chan;
	char *nick;
	char *nicks;


@@ 503,7 502,7 @@ irc_353(struct server *s, struct irc_message *m)
	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);

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


@@ 517,7 516,7 @@ irc_353(struct server *s, struct irc_message *m)
			if (user_list_add(&(c->users), s->casemapping, nick, m) == USER_ERR_DUPLICATE)
				newlinef(c, 0, FROM_ERROR, "Duplicate nick: '%s'", nick);

		} while ((nick = strtok_r(NULL, " ", &saveptr)));
		} while ((nick = strsep(&nicks)));
	}

	draw_status();

M src/handlers/irc_send.c => src/handlers/irc_send.c +17 -19
@@ 29,24 29,25 @@ static const char* targ_or_type(struct channel*, char*, enum channel_t type);
int
irc_send_command(struct server *s, struct channel *c, char *m)
{
	char *saveptr;
	char *command, *p;
	char *command, *command_args, *p;
	const struct send_handler *send;

	if (!s)
		failf(c, "This is not a server");

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

	for (p = command; *p; p++)
		*p = toupper(*p);

	command_args = strtrim(&m);

	if ((send = send_handler_lookup(command, strlen(command))))
		return send->f(s, c, saveptr);
		return send->f(s, c, command_args);

	if (str_trim(&saveptr))
		sendf(s, c, "%s %s", command, saveptr);
	if (strtrim(&command_args))
		sendf(s, c, "%s %s", command, command_args);
	else
		sendf(s, c, "%s", command);



@@ 78,10 79,9 @@ irc_send_privmsg(struct server *s, struct channel *c, char *m)
static const char*
targ_or_type(struct channel *c, char *m, enum channel_t type)
{
	char *saveptr;
	const char *targ;

	if ((targ = strtok_r(m, " ", &saveptr)))
	if ((targ = strsep(&m)))
		return targ;

	if (c->type == type)


@@ 198,16 198,15 @@ send_ctcp_version(struct server *s, struct channel *c, char *m)
static int
send_notice(struct server *s, struct channel *c, char *m)
{
	char *saveptr;
	const char *targ;

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

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

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

	return 0;
}


@@ 218,7 217,7 @@ send_part(struct server *s, struct channel *c, char *m)
	if (c->type != CHANNEL_T_CHANNEL)
		failf(c, "This is not a channel");

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


@@ 229,16 228,15 @@ send_part(struct server *s, struct channel *c, char *m)
static int
send_privmsg(struct server *s, struct channel *c, char *m)
{
	char *saveptr;
	const char *targ;

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

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

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

	return 0;
}


@@ 248,7 246,7 @@ send_quit(struct server *s, struct channel *c, char *m)
{
	s->quitting = 1;

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


@@ 262,7 260,7 @@ send_topic(struct server *s, struct channel *c, char *m)
	if (c->type != CHANNEL_T_CHANNEL)
		failf(c, "This is not a channel");

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

M src/io.c => src/io.c +477 -419
@@ 1,6 1,4 @@
#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <netdb.h>
#include <pthread.h>
#include <signal.h>


@@ 9,18 7,21 @@
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/types.h>
#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"

#define IO_RECV_SIZE 4096

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


@@ 62,21 63,31 @@
#error "IO_RECONNECT_BACKOFF_MAX: [0, 86400]"
#endif

#if EAGAIN == EWOULDBLOCK
#define CHECK_BLOCK(X) ((X) == EAGAIN)
#else
#define CHECK_BLOCK(X) ((X) == EAGAIN || (X) == EWOULDBLOCK)
#endif

#define PT_CF(X) do { io_check_fatal((#X), (X)); } while (0)
#define PT_CF(X) \
	do {                           \
		int _ptcf = (X);           \
		if (_ptcf < 0) {           \
			io_fatal((#X), _ptcf); \
		}                          \
	} while (0)
#define PT_LK(X) PT_CF(pthread_mutex_lock((X)))
#define PT_UL(X) PT_CF(pthread_mutex_unlock((X)))
#define PT_CB(...) \
	do { PT_LK(&cb_mutex); \
	     io_cb(__VA_ARGS__); \
	     PT_UL(&cb_mutex); \
	do {                    \
		PT_LK(&cb_mutex);   \
		io_cb(__VA_ARGS__); \
		PT_UL(&cb_mutex);   \
	} while (0)

#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__)
#define io_cb_info(C, ...)   PT_CB(IO_CB_INFO, (C)->obj, __VA_ARGS__)
#define io_cb_ping_0(C, ...) PT_CB(IO_CB_PING_0, (C)->obj, __VA_ARGS__)
#define io_cb_ping_1(C, ...) PT_CB(IO_CB_PING_1, (C)->obj, __VA_ARGS__)
#define io_cb_ping_n(C, ...) PT_CB(IO_CB_PING_N, (C)->obj, __VA_ARGS__)
#define io_cb_signal(S)      PT_CB(IO_CB_SIGNAL, NULL, (S))

enum io_err_t
{
	IO_ERR_NONE,


@@ 84,17 95,11 @@ enum io_err_t
	IO_ERR_CXNG,
	IO_ERR_DXED,
	IO_ERR_FMT,
	IO_ERR_SEND,
	IO_ERR_SSL_WRITE,
	IO_ERR_THREAD,
	IO_ERR_TRUNC,
};

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

struct connection
{
	const void *obj;


@@ 107,135 112,139 @@ struct connection
		IO_ST_CXNG, /* Socket connection in progress */
		IO_ST_CXED, /* Socket connected */
		IO_ST_PING, /* Socket connected, network state in question */
	} st_c, /* current thread state */
	  st_f; /* forced thread state */
	char ip[INET6_ADDRSTRLEN];
	int soc;
	struct {
		size_t i;
		char cl;
		char buf[IO_MESG_LEN + 1]; /* callback message buffer */
		char tmp[IO_RECV_SIZE];    /* socket recv buffer */
	} read;
	struct io_lock lock;
	unsigned rx_backoff;
	pthread_t pt_tid;
	} 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;
	pthread_mutex_t mtx;
	pthread_t tid;
	unsigned rx_sleep;
};

static const char* io_strerror(struct connection*, int);
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_dxed(struct connection*);
static enum io_state_t io_state_ping(struct connection*);
static enum io_state_t io_state_rxng(struct connection*);
static void io_check_fatal(const char*, int);
static void io_lock_wait(struct io_lock*, struct timespec*);
static void io_net_set_timeout(struct connection*, unsigned);
static void io_recv(struct connection*, const char*, size_t);
static int io_cx_read(struct connection*);
static void io_fatal(const char*, int);
static void io_sig_handle(int);
static void io_sig_init(void);
static void io_soc_close(int*);
static void io_soc_shutdown(int);
static void io_state_force(struct connection*, enum io_state_t);
static void io_ssl_init(void);
static void io_ssl_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 unsigned io_cols;
static unsigned io_rows;

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 pthread_mutex_t cb_mutex = PTHREAD_MUTEX_INITIALIZER;
static struct termios term;
static unsigned io_cols;
static unsigned io_rows;
static volatile sig_atomic_t flag_sigwinch_cb; /* sigwinch callback */
static volatile sig_atomic_t flag_tty_resized; /* sigwinch ws resize */

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

static const char*
io_strerror(struct connection *c, int errnum)
{
	PT_CF(strerror_r(errnum, c->read.tmp, sizeof(c->read.tmp)));
	return c->read.tmp;
}
	if ((cx = calloc(1U, sizeof(*cx))) == NULL)
		fatal("malloc: %s", strerror(errno));

static void
io_soc_close(int *soc)
{
	if (*soc >= 0 && close(*soc) < 0) {
		fatal("close: %s", strerror(errno));
	}
	*soc = -1;
	cx->obj = obj;
	cx->host = strdup(host);
	cx->port = strdup(port);
	cx->st_cur = IO_ST_DXED;
	cx->st_new = IO_ST_INVALID;
	PT_CF(pthread_mutex_init(&(cx->mtx), NULL));

	return cx;
}

static void
io_soc_shutdown(int soc)
void
connection_free(struct connection *cx)
{
	if (soc >= 0 && shutdown(soc, SHUT_RDWR) < 0 && errno != ENOTCONN) {
		fatal("shutdown: %s", strerror(errno));
	}
	PT_CF(pthread_mutex_destroy(&(cx->mtx)));
	free((void*)cx->host);
	free((void*)cx->port);
	free(cx);
}

static void
io_lock_wait(struct io_lock *lock, struct timespec *timeout)
int
io_cx(struct connection *cx)
{
	PT_LK(&(lock->mtx));
	enum io_err_t err = IO_ERR_NONE;
	enum io_state_t st;
	sigset_t sigset;
	sigset_t sigset_old;

	int ret = 0;
	PT_LK(&(cx->mtx));

	while (lock->predicate == 0 && ret == 0) {
		if (timeout) {
			ret = pthread_cond_timedwait(&(lock->cnd), &(lock->mtx), timeout);
		} else {
			ret = pthread_cond_wait(&(lock->cnd), &(lock->mtx));
		}
	switch ((st = cx->st_cur)) {
		case IO_ST_DXED:
			PT_CF(sigfillset(&sigset));
			PT_CF(pthread_sigmask(SIG_BLOCK, &sigset, &sigset_old));
			if (pthread_create(&(cx->tid), NULL, io_thread, cx) < 0)
				err = IO_ERR_THREAD;
			PT_CF(pthread_sigmask(SIG_SETMASK, &sigset_old, NULL));
			break;
		case IO_ST_CXNG:
			err = IO_ERR_CXNG;
			break;
		case IO_ST_CXED:
		case IO_ST_PING:
			err = IO_ERR_CXED;
			break;
		case IO_ST_RXNG:
			PT_CF(pthread_kill(cx->tid, SIGUSR1));
			break;
		default:
			fatal("unknown state");
	}

	if (ret && (timeout == NULL || ret != ETIMEDOUT))
		fatal("io_lock_wait: %s", strerror(ret));

	lock->predicate = 0;
	PT_UL(&(cx->mtx));

	PT_UL(&(lock->mtx));
	return err;
}

struct connection*
connection(const void *obj, const char *host, const char *port)
int
io_dx(struct connection *cx)
{
	struct connection *c;
	enum io_err_t err = IO_ERR_NONE;

	if ((c = calloc(1U, sizeof(*c))) == NULL)
		fatal("malloc: %s", strerror(errno));
	if (cx->st_cur == IO_ST_DXED)
		return IO_ERR_DXED;

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

	c->obj = obj;
	c->host = strdup(host);
	c->port = strdup(port);
	c->st_c = IO_ST_DXED;
	c->st_f = IO_ST_INVALID;
	PT_CF(pthread_cond_init(&(c->lock.cnd), NULL));
	PT_CF(pthread_mutex_init(&(c->lock.mtx), NULL));
	PT_CF(pthread_create(&c->pt_tid, NULL, io_thread, c));
	PT_CF(pthread_detach(cx->tid));
	PT_CF(pthread_kill(cx->tid, SIGUSR1));

	return c;
	return err;
}

int
io_sendf(struct connection *c, const char *fmt, ...)
io_sendf(struct connection *cx, const char *fmt, ...)
{
	char sendbuf[IO_MESG_LEN + 2];
	unsigned char sendbuf[IO_MESG_LEN + 2];
	int ret;
	size_t len;
	size_t written;
	va_list ap;

	if (c->st_c != IO_ST_CXED && c->st_c != IO_ST_PING)
	if (cx->st_cur != IO_ST_CXED && cx->st_cur != IO_ST_PING)
		return IO_ERR_DXED;

	va_start(ap, fmt);
	ret = vsnprintf(sendbuf, sizeof(sendbuf) - 2, fmt, ap);
	ret = vsnprintf((char*)sendbuf, sizeof(sendbuf) - 2, fmt, ap);
	va_end(ap);

	if (ret <= 0)


@@ 251,370 260,503 @@ io_sendf(struct connection *c, const char *fmt, ...)
	sendbuf[len++] = '\r';
	sendbuf[len++] = '\n';

	if (send(c->soc, sendbuf, len, 0) < 0)
		return IO_ERR_SEND;
	ret = 0;
	written = 0;

	do {
		if ((ret = mbedtls_ssl_write(&(cx->ssl_ctx), sendbuf + ret, len - ret)) < 0) {
			switch (ret) {
				case MBEDTLS_ERR_SSL_WANT_READ:
				case MBEDTLS_ERR_SSL_WANT_WRITE:
					ret = 0;
					continue;
				default:
					io_dx(cx);
					io_cx(cx);
					return IO_ERR_SSL_WRITE;
			}
		}
	} while ((written += ret) < len);

	return IO_ERR_NONE;
}

int
io_cx(struct connection *c)
void
io_init(void)
{
	/* Force a socket thread into IO_ST_CXNG state */

	enum io_err_t err = IO_ERR_NONE;

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

	switch (c->st_c) {
		case IO_ST_CXNG: err = IO_ERR_CXNG; break;
		case IO_ST_CXED: err = IO_ERR_CXED; break;
		case IO_ST_PING: err = IO_ERR_CXED; break;
		default:
			io_state_force(c, IO_ST_CXNG);
	}

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

	return err;
	io_sig_init();
	io_tty_init();
	io_ssl_init();
}

int
io_dx(struct connection *c)
void
io_start(void)
{
	/* Force a socket thread into IO_ST_DXED state */
	io_running = 1;

	enum io_err_t err = IO_ERR_NONE;
	while (io_running) {

	PT_LK(&(c->lock.mtx));
		char buf[128];
		ssize_t ret = read(STDIN_FILENO, buf, sizeof(buf));

	switch (c->st_c) {
		case IO_ST_DXED: err = IO_ERR_DXED; break;
		default:
			io_state_force(c, IO_ST_DXED);
		if (ret > 0) {
			PT_LK(&cb_mutex);
			io_cb_read_inp(buf, ret);
			PT_UL(&cb_mutex);
		} else {
			if (errno == EINTR) {
				if (flag_sigwinch_cb) {
					flag_sigwinch_cb = 0;
					io_cb_signal(IO_SIGWINCH);
				}
			} else {
				fatal("read: %s", ret ? strerror(errno) : "EOF");
			}
		}
	}

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

	return err;
}

void
io_free(struct connection *c)
io_stop(void)
{
	pthread_t pt_tid = c->pt_tid;

	PT_CF(pthread_cancel(pt_tid));
	PT_CF(pthread_join(pt_tid, NULL));
	PT_CF(pthread_cond_destroy(&(c->lock.cnd)));
	PT_CF(pthread_mutex_destroy(&(c->lock.mtx)));
	io_soc_close(&(c->soc));
	free((void*)c->host);
	free((void*)c->port);
	free(c);
	io_running = 0;
}

static void
io_state_force(struct connection *c, enum io_state_t st_f)
io_tty_winsize(void)
{
	/* Wake and force a connection thread's state */
	static struct winsize tty_ws;

	c->st_f = st_f;
	if (flag_tty_resized == 0) {
		flag_tty_resized = 1;

	switch (c->st_c) {
		case IO_ST_DXED: /* io_lock_wait() */
		case IO_ST_RXNG: /* io_lock_wait() */
			c->lock.predicate = 1;
			PT_CF(pthread_cond_signal(&(c->lock.cnd)));
			break;
		case IO_ST_CXNG: /* connect() */
		case IO_ST_CXED: /* recv() */
		case IO_ST_PING: /* recv() */
			io_soc_shutdown(c->soc);
			break;
		default:
			fatal("Unknown net state: %d", c->st_c);
		if (ioctl(0, TIOCGWINSZ, &tty_ws) < 0)
			fatal("ioctl: %s", strerror(errno));

		io_rows = tty_ws.ws_row;
		io_cols = tty_ws.ws_col;
	}
}

static enum io_state_t
io_state_dxed(struct connection *c)
unsigned
io_tty_cols(void)
{
	io_lock_wait(&c->lock, NULL);
	io_tty_winsize();
	return io_cols;
}

	return IO_ST_CXNG;
unsigned
io_tty_rows(void)
{
	io_tty_winsize();
	return io_rows;
}

static enum io_state_t
io_state_rxng(struct connection *c)
const char*
io_err(int err)
{
	struct timespec ts;
	switch (err) {
		case IO_ERR_NONE:      return "success";
		case IO_ERR_CXED:      return "socket connected";
		case IO_ERR_CXNG:      return "socket connection in progress";
		case IO_ERR_DXED:      return "socket not connected";
		case IO_ERR_FMT:       return "failed to format message";
		case IO_ERR_THREAD:    return "failed to create thread";
		case IO_ERR_SSL_WRITE: return "ssl write failure";
		case IO_ERR_TRUNC:     return "data truncated";
		default:
			return "unknown error";
	}
}

	if (c->rx_backoff == 0) {
		c->rx_backoff = IO_RECONNECT_BACKOFF_BASE;
static enum io_state_t
io_state_rxng(struct connection *cx)
{
	if (cx->rx_sleep == 0) {
		cx->rx_sleep = IO_RECONNECT_BACKOFF_BASE;
	} else {
		c->rx_backoff = MIN(
			IO_RECONNECT_BACKOFF_FACTOR * c->rx_backoff,
		cx->rx_sleep = MIN(
			IO_RECONNECT_BACKOFF_FACTOR * cx->rx_sleep,
			IO_RECONNECT_BACKOFF_MAX
		);
	}

	PT_CB(IO_CB_INFO, c->obj, "Attemping reconnect in %02u:%02u",
		(c->rx_backoff / 60),
		(c->rx_backoff % 60));

	if (clock_gettime(CLOCK_REALTIME, &ts) < 0)
		fatal("clock_gettime: %s", strerror(errno));
	io_cb_info(cx, "Attemping reconnect in %02u:%02u",
		(cx->rx_sleep / 60),
		(cx->rx_sleep % 60));

	ts.tv_sec += c->rx_backoff;

	io_lock_wait(&c->lock, &ts);
	sleep(cx->rx_sleep);

	return IO_ST_CXNG;
}

static enum io_state_t
io_state_cxng(struct connection *c)
io_state_cxng(struct connection *cx)
{
	/* TODO: handle shutdown() on socket at all points, will fatal for now */
	/* TODO: mutex should protect access to c->soc, else race condition
	 *       when the main thread tries to shutdown() for cancel */
	/* TODO: how to cancel getaddrinfo/getnameinfo? */
	/* FIXME: addrinfo leak if canceled during connection */

	int ret, soc = -1;
	char addr_buf[INET6_ADDRSTRLEN];
	char vrfy_buf[512];
	enum io_state_t st = IO_ST_RXNG;
	int ret;
	int soc;
	uint32_t cert_ret;

	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) {
		switch (ret) {
			case IO_NET_ERR_EINTR:
				st = IO_ST_DXED;
				goto error_net;
			case IO_NET_ERR_SOCKET_FAILED:
				io_cb_err(cx, " ... Failed to obtain socket");
				goto error_net;
			case IO_NET_ERR_UNKNOWN_HOST:
				io_cb_err(cx, " ... Failed to resolve host");
				goto error_net;
			case IO_NET_ERR_CONNECT_FAILED:
				io_cb_err(cx, " ... Failed to connect to host");
				goto error_net;
			default:
				fatal("unknown net error");
		}
	}

	struct addrinfo *p, *res, hints = {
		.ai_family   = AF_UNSPEC,
		.ai_flags    = AI_PASSIVE,
		.ai_protocol = IPPROTO_TCP,
		.ai_socktype = SOCK_STREAM
	};
	if ((ret = io_net_ip_str(soc, addr_buf, sizeof(addr_buf))) != IO_NET_ERR_NONE) {
		if (ret == IO_NET_ERR_EINTR) {
			st = IO_ST_DXED;
			goto error_net;
		}
		io_cb_info(cx, " ... Connected (failed to optain IP address)");
	} else {
		io_cb_info(cx, " ... Connected to [%s]", addr_buf);
	}

	PT_CB(IO_CB_INFO, c->obj, "Connecting to %s:%s ...", c->host, c->port);
	io_cb_info(cx, " ... Establishing SSL");

	if ((ret = getaddrinfo(c->host, c->port, &hints, &res))) {
	mbedtls_net_init(&(cx->ssl_fd));
	mbedtls_ssl_init(&(cx->ssl_ctx));
	mbedtls_ssl_config_init(&(cx->ssl_conf));

		if (ret == EAI_SYSTEM)
			PT_CB(IO_CB_ERR, c->obj, "Error resolving host: %s", io_strerror(c, errno));
		else
			PT_CB(IO_CB_ERR, c->obj, "Error resolving host: %s", gai_strerror(ret));
	cx->ssl_conf = ssl_conf;
	cx->ssl_fd.fd = soc;

		return IO_ST_RXNG;
	if ((ret = mbedtls_net_set_block(&(cx->ssl_fd))) != 0) {
		io_cb_err(cx, " ... mbedtls_net_set_block failure");
		goto error_ssl;
	}

	for (p = res; p != NULL; p = p->ai_next) {

		if ((soc = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) < 0)
			continue;
	if ((ret = mbedtls_ssl_setup(&(cx->ssl_ctx), &(cx->ssl_conf))) != 0) {
		io_cb_err(cx, " ... mbedtls_ssl_setup failure");
		goto error_ssl;
	}

		if (connect(soc, p->ai_addr, p->ai_addrlen) == 0)
			break;
	if ((ret = mbedtls_ssl_set_hostname(&(cx->ssl_ctx), cx->host)) != 0) {
		io_cb_err(cx, " ... mbedtls_ssl_set_hostname failure");
		goto error_ssl;
	}

		io_soc_close(&soc);
	mbedtls_ssl_set_bio(
		&(cx->ssl_ctx),
		&(cx->ssl_fd),
		mbedtls_net_send,
		NULL,
		mbedtls_net_recv_timeout);

	while ((ret = mbedtls_ssl_handshake(&(cx->ssl_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;
		}
	}

	if (p == NULL) {
		PT_CB(IO_CB_ERR, c->obj, "Error connecting: %s", io_strerror(c, errno));
		freeaddrinfo(res);
		return IO_ST_RXNG;
	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) {
			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;
		}
	}

	c->soc = soc;
	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)));

	if ((ret = getnameinfo(p->ai_addr, p->ai_addrlen, c->ip, sizeof(c->ip), NULL, 0, NI_NUMERICHOST))) {
	return IO_ST_CXED;

		if (ret == EAI_SYSTEM)
			PT_CB(IO_CB_ERR, c->obj, "Error resolving numeric host: %s", io_strerror(c, errno));
		else
			PT_CB(IO_CB_ERR, c->obj, "Error resolving numeric host: %s", gai_strerror(ret));
error_ssl:

		*c->ip = 0;
	}
	mbedtls_net_free(&(cx->ssl_fd));
	mbedtls_ssl_free(&(cx->ssl_ctx));
	mbedtls_ssl_config_free(&(cx->ssl_conf));

	freeaddrinfo(res);
	return IO_ST_CXED;
error_net:

	return st;
}

static enum io_state_t
io_state_cxed(struct connection *c)
io_state_cxed(struct connection *cx)
{
	io_net_set_timeout(c, IO_PING_MIN);
	ssize_t ret;
	int ret;
	enum io_state_t st = IO_ST_RXNG;

	while ((ret = recv(c->soc, c->read.tmp, sizeof(c->read.tmp), 0)) > 0)
		io_recv(c, c->read.tmp, (size_t) ret);
	mbedtls_ssl_conf_read_timeout(&(cx->ssl_conf), SEC_IN_MS(IO_PING_MIN));

	if (CHECK_BLOCK(errno)) {
		return IO_ST_PING;
	}
	while ((ret = io_cx_read(cx)) > 0)
		continue;

	// FIXME:
	// EINTR when coming back from sleep?

	if (ret == 0) {
		PT_CB(IO_CB_DXED, c->obj, "connection closed");
	} else if (errno == EPIPE || errno == ECONNRESET) {
		PT_CB(IO_CB_DXED, c->obj, "connection closed by peer");
	} else {
		PT_CB(IO_CB_DXED, c->obj, "recv error: %s", io_strerror(c, errno));
	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 */
			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");
			break;
	}

	io_soc_close(&(c->soc));
	mbedtls_net_free(&(cx->ssl_fd));
	mbedtls_ssl_free(&(cx->ssl_ctx));
	mbedtls_ssl_config_free(&(cx->ssl_conf));

	return (ret == 0 ? IO_ST_DXED : IO_ST_CXNG);
	return st;
}

static enum io_state_t
io_state_ping(struct connection *c)
io_state_ping(struct connection *cx)
{
	io_net_set_timeout(c, IO_PING_REFRESH);
	ssize_t ret;
	unsigned ping = IO_PING_MIN;
	int ping = IO_PING_MIN;
	int ret;
	enum io_state_t st = IO_ST_RXNG;

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

		if ((ret = recv(c->soc, c->read.tmp, sizeof(c->read.tmp), 0)) > 0) {
			io_recv(c, c->read.tmp, (size_t) ret);
			return IO_ST_CXED;
	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 (ret == 0) {
			PT_CB(IO_CB_DXED, c->obj, "connection closed");
		} else if (CHECK_BLOCK(errno)) {
			if ((ping += IO_PING_REFRESH) < IO_PING_MAX) {
				PT_CB(IO_CB_PING_N, c->obj, ping);
				continue;
			}
			PT_CB(IO_CB_DXED, c->obj, "connection timeout (%u)", ping);
		} else {
			PT_CB(IO_CB_DXED, c->obj, "recv error: %s", io_strerror(c, errno));
		}
	if (ret > 0)
		return IO_ST_CXED;

		break;
	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;
			break;
		case MBEDTLS_ERR_SSL_TIMEOUT:
			io_cb_err(cx, "connection timeout (%u)", ping);
			break;
		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");
			break;
	}

	io_soc_close(&(c->soc));
	mbedtls_net_free(&(cx->ssl_fd));
	mbedtls_ssl_free(&(cx->ssl_ctx));
	mbedtls_ssl_config_free(&(cx->ssl_conf));

	return (ret == 0 ? IO_ST_DXED : IO_ST_CXNG);
	return st;
}

static void*
io_thread(void *arg)
{
	struct connection *c = arg;
	struct connection *cx = arg;

	/* SIGUSR1 indicates to a thread that it should return
	 * to the state machine and check for a new state */

	sigset_t sigset;
	PT_CF(sigfillset(&sigset));
	PT_CF(pthread_sigmask(SIG_BLOCK, &sigset, NULL));

	for (;;) {
	PT_CF(sigaddset(&sigset, SIGUSR1));
	PT_CF(pthread_sigmask(SIG_UNBLOCK, &sigset, NULL));

		enum io_state_t st_f, /* transition state from */
		                st_t; /* transition state to */
	cx->st_new = IO_ST_CXNG;

		enum io_state_t (*st_fn)(struct connection*);
	for (;;) {

		enum io_state_t st_from;
		enum io_state_t st_to;

		switch (c->st_c) {
			case IO_ST_DXED: st_fn = io_state_dxed; break;
			case IO_ST_CXNG: st_fn = io_state_cxng; break;
			case IO_ST_RXNG: st_fn = io_state_rxng; break;
			case IO_ST_CXED: st_fn = io_state_cxed; break;
			case IO_ST_PING: st_fn = io_state_ping; break;
		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;
			default:
				fatal("invalid state: %d", c->st_c);
				fatal("invalid state: %d", cx->st_cur);
		}

		st_f = c->st_c;
		st_t = st_fn(c);
		st_from = cx->st_cur;

		PT_LK(&(c->lock.mtx));
		PT_LK(&(cx->mtx));

		if (c->st_f != IO_ST_INVALID) {
			c->st_c = c->st_f;
			c->st_f = IO_ST_INVALID;
		/* 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;
		} else {
			c->st_c = st_t;
			cx->st_cur = st_to;
		}

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

		PT_UL(&(c->lock.mtx));
		if ((st_from == IO_ST_RXNG || st_from == IO_ST_CXNG) && st_to == IO_ST_DXED)
			io_cb_info(cx, "Connection cancelled");

		if (st_f == IO_ST_PING && st_t == IO_ST_CXED)
			PT_CB(IO_CB_PING_0, c->obj, 0);
		if ((st_from == IO_ST_CXED || st_from == IO_ST_PING) && st_to == IO_ST_DXED)
			io_cb_info(cx, "Connection closed");

		if (st_f == IO_ST_CXED && st_t == IO_ST_PING)
			PT_CB(IO_CB_PING_1, c->obj, IO_PING_MIN);
		if (st_from == IO_ST_PING && st_to == IO_ST_CXED)
			io_cb_ping_0(cx, 0);

		if (st_f == IO_ST_CXNG && st_t == IO_ST_CXED)
			PT_CB(IO_CB_CXED, c->obj, "Connected to %s [%s]", c->host, c->ip);
		if (st_from == IO_ST_CXED && st_to == IO_ST_PING)
			io_cb_ping_1(cx, IO_PING_MIN);

		if (st_t == IO_ST_DXED || (st_f == IO_ST_CXNG && st_t == IO_ST_CXED))
			c->rx_backoff = 0;
		/* Exit the thread */
		if (cx->st_cur == IO_ST_DXED) {
			io_cb_dxed(cx);
			cx->rx_sleep = 0;
			break;
		}
	}

	return NULL;
}

static void
io_recv(struct connection *c, const char *buf, size_t n)
static int
io_cx_read(struct connection *cx)
{
	size_t ci = c->read.i;

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

		char cc = buf[i];

		if (ci && cc == '\n' && ((i && buf[i - 1] == '\r') || (!i && c->read.cl == '\r'))) {

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

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

			PT_LK(&cb_mutex);
			io_cb_read_soc(c->read.buf, ci, c->obj);
			PT_UL(&cb_mutex);
	int ret;
	unsigned char ssl_readbuf[1024];

			ci = 0;
		} else if (ci < IO_MESG_LEN && (isprint(cc) || cc == 0x01)) {
			c->read.buf[ci++] = cc;
		}
	if ((ret = mbedtls_ssl_read(&(cx->ssl_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);
	}

	c->read.cl = buf[n - 1];
	c->read.i = ci;
	return ret;
}

static void
io_net_set_timeout(struct connection *c, unsigned timeout)
io_fatal(const char *f, int errnum)
{
	struct timeval tv = {
		.tv_sec = timeout
	};
	char errbuf[512];

	if (setsockopt(c->soc, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) < 0)
		fatal("setsockopt: %s", strerror(errno));
	if (strerror_r(errnum, errbuf, sizeof(errbuf)) == 0) {
		fatal("%s: (%d): %s", f, errnum, errbuf);
	} else {
		fatal("%s: (%d): (failed to get error message)", f, errnum);
	}
}

static void
sigaction_sigwinch(int sig)
io_sig_handle(int sig)
{
	UNUSED(sig);

	flag_sigwinch_cb = 1;
	flag_tty_resized = 0;
	if (sig == SIGWINCH) {
		flag_sigwinch_cb = 1;
		flag_tty_resized = 0;
	}
}

static void
io_sig_init(void)
{
	struct sigaction sa;
	struct sigaction sa = {0};

	sa.sa_handler = sigaction_sigwinch;
	sa.sa_handler = io_sig_handle;
	sa.sa_flags = 0;
	sigemptyset(&sa.sa_mask);

	if (sigaction(SIGWINCH, &sa, NULL) < 0)
		fatal("sigaction - SIGWINCH: %s", strerror(errno));

	if (sigaction(SIGUSR1, &sa, NULL) < 0)
		fatal("sigaction - SIGUSR1: %s", strerror(errno));
}

static void
io_ssl_init(void)
{
	const char *tls_pers = "rirc-drbg-ctr-pers";
	int ret;

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

	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 ((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 ((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");
	}

	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 (atexit(io_ssl_term) != 0)
		fatal("atexit");
}

static void
io_ssl_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);
}

static void


@@ 648,87 790,3 @@ io_tty_term(void)
	if (tcsetattr(STDIN_FILENO, TCSADRAIN, &term) < 0)
		fatal_noexit("tcsetattr: %s", strerror(errno));
}

void
io_init(void)
{
	io_sig_init();
	io_tty_init();

	io_running = 1;

	while (io_running) {

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

		if (ret > 0) {
			PT_LK(&cb_mutex);
			io_cb_read_inp(buf, ret);
			PT_UL(&cb_mutex);
		}

		if (ret <= 0) {
			if (errno == EINTR) {
				if (flag_sigwinch_cb) {
					flag_sigwinch_cb = 0;
					PT_CB(IO_CB_SIGNAL, NULL, IO_SIGWINCH);
				}
			} else {
				fatal("read: %s", ret ? strerror(errno) : "EOF");
			}
		}
	}
}

void
io_term(void)
{
	io_running = 0;
}

static void
io_tty_winsize(void)
{
	static struct winsize tty_ws;

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

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

		io_rows = tty_ws.ws_row;
		io_cols = tty_ws.ws_col;
	}
}

unsigned
io_tty_cols(void)
{
	io_tty_winsize();
	return io_cols;
}

unsigned
io_tty_rows(void)
{
	io_tty_winsize();
	return io_rows;
}

const char*
io_err(int err)
{
	switch (err) {
		case IO_ERR_NONE:  return "success";
		case IO_ERR_CXED:  return "socket connected";
		case IO_ERR_CXNG:  return "socket connection in progress";
		case IO_ERR_DXED:  return "socket not connected";
		case IO_ERR_FMT:   return "failed to format message";
		case IO_ERR_SEND:  return "failed to send message";
		case IO_ERR_TRUNC: return "data truncated";
		default:
			return "unknown error";
	}
}

M src/io.h => src/io.h +15 -15
@@ 69,11 69,10 @@
 *   t(n) = t(n - 1) * factor
 *   t(0) = base
 *
 * Calling io_init starts the io context and doesn't return until io_term
 * Calling io_start starts the io context and doesn't return until after
 * a call to io_stop
 */

#define IO_MAX_CONNECTIONS 8

struct connection;

enum io_sig_t


@@ 86,8 85,8 @@ enum io_sig_t
enum io_cb_t
{
	IO_CB_INVALID,
	IO_CB_CXED,   /* <const char *fmt>, [args, ...] */
	IO_CB_DXED,   /* <const char *fmt>, [args, ...] */
	IO_CB_CXED,   /* no args */
	IO_CB_DXED,   /* no args */
	IO_CB_ERR,    /* <const char *fmt>, [args, ...] */
	IO_CB_INFO,   /* <const char *fmt>, [args, ...] */
	IO_CB_PING_0, /* <unsigned ping> */


@@ 103,7 102,7 @@ struct connection* connection(
	const char*,  /* host */
	const char*); /* port */

void io_free(struct connection*);
void connection_free(struct connection*);

/* Explicit direction of net state */
int io_cx(struct connection*);


@@ 112,16 111,10 @@ int io_dx(struct connection*);
/* Formatted write to connection */
int io_sendf(struct connection*, const char*, ...);

/* IO state callback */
void io_cb(enum io_cb_t, const void*, ...);

/* IO data callback */
void io_cb_read_inp(char*, size_t);
void io_cb_read_soc(char*, size_t, const void*);

/* Start/stop IO context */
/* Init/start/stop IO context */
void io_init(void);
void io_term(void);
void io_start(void);
void io_stop(void);

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


@@ 130,4 123,11 @@ unsigned io_tty_rows(void);
/* IO error string */
const char* io_err(int);

/* IO state callback */
void io_cb(enum io_cb_t, const void*, ...);

/* IO data callback */
void io_cb_read_inp(char*, size_t);
void io_cb_read_soc(char*, size_t, const void*);

#endif

A src/io_net.c => src/io_net.c +79 -0
@@ 0,0 1,79 @@
#include <errno.h>
#include <netdb.h>
#include <unistd.h>

#include "io_net.h"
#include "utils/utils.h"

enum io_net_err
io_net_connect(int *soc_set, const char *host, const char *port)
{
	int soc = -1;
	int ret = IO_NET_ERR_UNKNOWN;
	struct addrinfo *p, *results;

	struct addrinfo hints = {
		.ai_family   = AF_UNSPEC,
		.ai_flags    = AI_PASSIVE,
		.ai_protocol = IPPROTO_TCP,
		.ai_socktype = SOCK_STREAM,
	};

	errno = 0;

	if ((ret = getaddrinfo(host, port, &hints, &results)) != 0) {

		if (ret == EAI_SYSTEM && errno == EINTR)
			return IO_NET_ERR_EINTR;

		return IO_NET_ERR_UNKNOWN_HOST;
	}

	for (p = results; p != NULL; p = p->ai_next) {

		if ((soc = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) < 0) {
			ret = IO_NET_ERR_SOCKET_FAILED;
			continue;
		}

		if (connect(soc, p->ai_addr, p->ai_addrlen) == 0) {
			ret = IO_NET_ERR_NONE;
			break;
		}

		if (errno == EINTR) {
			ret = IO_NET_ERR_EINTR;
			break;
		}

		close(soc);
		ret = IO_NET_ERR_CONNECT_FAILED;
	}

	freeaddrinfo(results);

	if (ret == IO_NET_ERR_NONE)
		*soc_set = soc;

	return ret;
}

enum io_net_err
io_net_ip_str(int soc, char *buf, size_t len)
{
	int ret;
	socklen_t addr_len;
	struct sockaddr addr;

	addr_len = sizeof(addr);

	if ((ret = getpeername(soc, &addr, &addr_len)) == -1)
		return IO_NET_ERR_IP;

	if ((ret = getnameinfo(&addr, addr_len, buf, len, NULL, 0, NI_NUMERICHOST)) < 0) {
		if (ret == EAI_SYSTEM && errno == EINTR)
			return IO_NET_ERR_EINTR;
	}

	return (ret ? IO_NET_ERR_IP : IO_NET_ERR_NONE);
}

A src/io_net.h => src/io_net.h +20 -0
@@ 0,0 1,20 @@
#ifndef IO_NET_H
#define IO_NET_H

#include <stddef.h>

enum io_net_err
{
	IO_NET_ERR_NONE,
	IO_NET_ERR_SOCKET_FAILED,
	IO_NET_ERR_UNKNOWN_HOST,
	IO_NET_ERR_CONNECT_FAILED,
	IO_NET_ERR_EINTR,
	IO_NET_ERR_IP,
	IO_NET_ERR_UNKNOWN
};

enum io_net_err io_net_connect(int*, const char*, const char*);
enum io_net_err io_net_ip_str(int, char*, size_t);

#endif

M src/rirc.c => src/rirc.c +16 -6
@@ 10,6 10,8 @@
#include "src/io.h"
#include "src/state.h"

#define MAX_CLI_SERVERS 16

#define arg_error(...) \
	do { fprintf(stderr, "%s: ", runtime_name); \
	     fprintf(stderr, __VA_ARGS__); \


@@ 27,6 29,12 @@ const char *runtime_name = "rirc";
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
const char *default_nick_set = DEFAULT_NICK_SET;
#else


@@ 139,7 147,7 @@ parse_args(int argc, char **argv)
		const char *username;
		const char *realname;
		struct server *s;
	} cli_servers[IO_MAX_CONNECTIONS];
	} cli_servers[MAX_CLI_SERVERS];

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


@@ 151,11 159,11 @@ parse_args(int argc, char **argv)
				if (*optarg == '-')
					arg_error("-s/--server requires an argument");

				if (++n_servers == IO_MAX_CONNECTIONS)
					arg_error("exceeded maximum number of servers (%d)", IO_MAX_CONNECTIONS);
				if (++n_servers == MAX_CLI_SERVERS)
					arg_error("exceeded maximum number of servers (%d)", MAX_CLI_SERVERS);

				cli_servers[n_servers - 1].host = optarg;
				cli_servers[n_servers - 1].port = "6667";
				cli_servers[n_servers - 1].port = "6697";
				cli_servers[n_servers - 1].pass = NULL;
				cli_servers[n_servers - 1].nicks = NULL;
				cli_servers[n_servers - 1].chans = NULL;


@@ 233,6 241,7 @@ parse_args(int argc, char **argv)
		default_realname = getpwuid_pw_name();

	state_init();
	draw_init();

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



@@ 260,6 269,8 @@ parse_args(int argc, char **argv)
		channel_set_current(s->channel);
	}

	io_init();

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



@@ 273,8 284,7 @@ main(int argc, char **argv)
	int ret;

	if ((ret = parse_args(argc, argv)) == 0) {
		draw_init();
		io_init();
		io_start();
		draw_term();
		state_term();
	}

M src/rirc.h => src/rirc.h +1 -1
@@ 3,10 3,10 @@

/* Default config values obtained at runtime */

extern const char *ca_cert_path;
extern const char *default_nick_set;
extern const char *default_username;
extern const char *default_realname;

extern const char *runtime_name;

#endif

M src/state.c => src/state.c +52 -30
@@ 24,7 24,7 @@

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



@@ 79,7 79,7 @@ static const char *irc_list[] = {
// TODO: from command handler list
/* List of rirc commands for tab completeion */
static const char *cmd_list[] = {
	"clear", "close", "connect", "quit", "set", NULL};
	"clear", "close", "connect", "disconnect", "quit", "set", NULL};

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


@@ 133,7 133,7 @@ state_term(void)
	do {
		s2 = s1;
		s1 = s2->next;
		io_free(s2->connection);
		connection_free(s2->connection);
		server_free(s2);
	} while (s1 != state_server_list()->head);
}


@@ 337,7 337,7 @@ action_close_server(char c)
			newlinef(s->channel, 0, "-!!-", "sendf fail: %s", io_err(ret));

		io_dx(s->connection);
		io_free(s->connection);
		connection_free(s->connection);
		server_list_del(state_server_list(), s);
		server_free(s);



@@ 375,7 375,7 @@ action(int (*a_handler)(char), const char *fmt, ...)
/* Action line should be:
 *
 *
 * Find: [current result]/[(server if not current server[socket if not 6667])] : <search input> */
 * Find: [current result]/[(server if not current server[socket if not 6697])] : <search input> */
static int
action_find_channel(char c)
{


@@ 429,7 429,7 @@ action_find_channel(char c)
			action(action_find_channel, "Find: %s -- %s",
					search_cptr->name, search_buf);
		} else {
			if (!strcmp(search_cptr->server->port, "6667"))
			if (!strcmp(search_cptr->server->port, "6697"))
				action(action_find_channel, "Find: %s/%s -- %s",
						search_cptr->server->host, search_cptr->name, search_buf);
			else


@@ 729,15 729,12 @@ state_io_cxed(struct server *s)
}

static void
state_io_dxed(struct server *s, va_list ap)
state_io_dxed(struct server *s)
{
	struct channel *c = s->channel;
	va_list ap_copy;

	do {
		va_copy(ap_copy, ap);
		_newline(c, 0, "-!!-", va_arg(ap_copy, const char *), ap_copy);
		va_end(ap_copy);
		newline(c, 0, "-!!-", " -- disconnected --");
		channel_reset(c);
		c = c->next;
	} while (c != s->channel);


@@ 779,10 776,9 @@ io_cb(enum io_cb_t type, const void *cb_obj, ...)
	switch (type) {
		case IO_CB_CXED:
			state_io_cxed(s);
			_newline(s->channel, 0, "--", va_arg(ap, const char *), ap);
			break;
		case IO_CB_DXED:
			state_io_dxed(s, ap);
			state_io_dxed(s);
			break;
		case IO_CB_PING_0:
		case IO_CB_PING_1:


@@ 811,25 807,24 @@ static void
command(struct channel *c, char *buf)
{
	const char *cmnd;
	char *saveptr;
	int err;

	if (!(cmnd = strtok_r(buf, " ", &saveptr))) {
	if (!(cmnd = strsep(&buf))) {
		newline(c, 0, "-!!-", "Messages beginning with ':' require a command");
		return;
	}

	if (!strcasecmp(cmnd, "quit")) {
		io_term();
		io_stop();
	}

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

		const char *host = strtok_r(NULL, " ", &saveptr);
		const char *port = strtok_r(NULL, " ", &saveptr);
		const char *pass = strtok_r(NULL, " ", &saveptr);
		const char *user = strtok_r(NULL, " ", &saveptr);
		const char *real = strtok_r(NULL, " ", &saveptr);
		// TODO: parse --args
		const char *host = strsep(&buf);
		const char *port = strsep(&buf);
		const char *pass = strsep(&buf);
		const char *user = strsep(&buf);
		const char *real = strsep(&buf);
		const char *help = ":connect [host [port] [pass] [user] [real]]";
		struct server *s;



@@ 840,7 835,7 @@ command(struct channel *c, char *buf)
				newlinef(c, 0, "-!!-", "%s", io_err(err));
			}
		} else {
			port = (port ? port : "6667");
			port = (port ? port : "6697");
			user = (user ? user : default_username);
			real = (real ? real : default_realname);



@@ 859,6 854,11 @@ command(struct channel *c, char *buf)
		return;
	}

	if (!strcasecmp(cmnd, "disconnect")) {
		io_dx(c->server->connection);
		return;
	}

	if (!strcasecmp(cmnd, "clear")) {
		channel_clear(c);
		return;


@@ 1042,14 1042,36 @@ io_cb_read_inp(char *buf, size_t len)
void
io_cb_read_soc(char *buf, size_t len, const void *cb_obj)
{
	struct channel *c = ((struct server *)cb_obj)->channel;
	struct server *s = (struct server *)cb_obj;
	struct channel *c = s->channel;
	size_t ci = s->read.i;
	size_t n = len;

	struct irc_message m;
	for (size_t i = 0; i < n; i++) {

	if (!(irc_message_parse(&m, buf, len)))
		newlinef(c, 0, "-!!-", "failed to parse message");
	else
		irc_recv((struct server *)cb_obj, &m);
		char cc = buf[i];

	redraw();
		if (ci && cc == '\n' && ((i && buf[i - 1] == '\r') || (!i && s->read.cl == '\r'))) {

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

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

			struct irc_message m;

			if (!(irc_message_parse(&m, s->read.buf)))
				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;
		}
	}

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

M src/utils/tree.h => src/utils/tree.h +5 -200
@@ 1,8 1,6 @@
#ifndef TREE_H
#define TREE_H

/* FIXME: undefined behaviour reported by clang in AVL/SPLAY implementations */

#include <stddef.h>

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


@@ 12,29 10,23 @@
#define TREE_RIGHT(elm, field) (elm)->field.tree_right
#define TREE_ROOT(head)        (head)->tree_root

/* AVL Tree */

#define AVL_HEIGHT(elm, field) (elm)->field.height

#define AVL_ADD(name, x, y, z)     name##_AVL_ADD(x, y, z)
#define AVL_DEL(name, x, y, z)     name##_AVL_DEL(x, y, z)
#define AVL_GET(name, x, y, z)     name##_AVL_GET(x, y, z)
#define AVL_NGET(name, x, y, z, n) name##_AVL_NGET(x, y, z, n)
#define AVL_FOREACH(name, x, y)    name##_AVL_FOREACH(x, y)

#define AVL_FOREACH(name, x, y) name##_AVL_FOREACH(x, y)

#define AVL_HEAD(type) \
#define TREE_HEAD(type) \
    struct type *tree_root


#define AVL_NODE(type)           \
#define TREE_NODE(type)          \
    struct {                     \
        int height;              \
        struct type *tree_left;  \
        struct type *tree_right; \
    }

/* FIXME: scan-build showing NULL pointer dereferences on add */
#define AVL_HEIGHT(elm, field) (elm)->field.height

#define AVL_GENERATE(name, type, field, cmp, ncmp) \
    static struct type* name##_AVL_ADD(struct name*, struct type*, void*);        \


@@ 218,7 210,7 @@ name##_AVL_DEL_REC(struct type **p, struct type *elm, void *arg)                
{                                                                                 \
    int comp, balance;                                                            \
    struct type *n = *p;                                                          \
    struct type *ret;                                                             \
    struct type *ret = NULL;                                                      \
                                                                                  \
    if (n == NULL)                                                                \
        return NULL;                                                              \


@@ 298,191 290,4 @@ name##_AVL_DEL_REC(struct type **p, struct type *elm, void *arg)                
    return ret;                                                                   \
}

/* Splay Tree */

#define SPLAY_ADD(name, x, y)  name##_SPLAY_ADD(x, y)
#define SPLAY_DEL(name, x, y)  name##_SPLAY_DEL(x, y)
#define SPLAY_GET(name, x, y)  name##_SPLAY_GET(x, y)
#define SPLAY_MAX(name, x)     (TREE_EMPTY(x) ? NULL : name##_SPLAY_MAX(x))
#define SPLAY_MIN(name, x)     (TREE_EMPTY(x) ? NULL : name##_SPLAY_MIN(x))


#define SPLAY_HEAD(type) \
    struct type *tree_root


#define SPLAY_NODE(type)         \
    struct {                     \
        struct type *tree_left;  \
        struct type *tree_right; \
    }


#define SPLAY_INIT(root) do {   \
        TREE_ROOT(root) = NULL; \
    } while (0)


#define SPLAY_ROTATE_RIGHT(head, tmp, field) do {                   \
        TREE_LEFT(TREE_ROOT(head), field) = TREE_RIGHT(tmp, field); \
        TREE_RIGHT(tmp, field) = TREE_ROOT(head);                   \
        TREE_ROOT(head) = tmp;                                      \
    } while (0)


#define SPLAY_ROTATE_LEFT(head, tmp, field) do {                    \
        TREE_RIGHT(TREE_ROOT(head), field) = TREE_LEFT(tmp, field); \
        TREE_LEFT(tmp, field) = TREE_ROOT(head);                    \
        TREE_ROOT(head) = tmp;                                      \
    } while (0)


#define SPLAY_LINK_RIGHT(head, tmp, field) do {               \
        TREE_RIGHT(tmp, field) = TREE_ROOT(head);             \
        tmp = TREE_ROOT(head);                                \
        TREE_ROOT(head) = TREE_RIGHT(TREE_ROOT(head), field); \
    } while (0)


#define SPLAY_LINK_LEFT(head, tmp, field) do {               \
        TREE_LEFT(tmp, field) = TREE_ROOT(head);             \
        tmp = TREE_ROOT(head);                               \
        TREE_ROOT(head) = TREE_LEFT(TREE_ROOT(head), field); \
    } while (0)


#define SPLAY_ASSEMBLE(head, node, left, right, field) do {           \
        TREE_RIGHT(left, field) = TREE_LEFT(TREE_ROOT(head), field);  \
        TREE_LEFT(right, field) = TREE_RIGHT(TREE_ROOT(head), field); \
        TREE_LEFT(TREE_ROOT(head), field) = TREE_RIGHT(node, field);  \
        TREE_RIGHT(TREE_ROOT(head), field) = TREE_LEFT(node, field);  \
    } while (0)


#define SPLAY_GENERATE(name, type, field, cmp)                           \
    struct type* name##_SPLAY_ADD(struct name*, struct type*);           \
    struct type* name##_SPLAY_DEL(struct name*, struct type*);           \
    void name##_SPLAY(struct name*, struct type*);                       \
                                                                         \
static inline struct type*                                               \
name##_SPLAY_MIN(struct name *head)                                      \
{                                                                        \
    struct type *x = TREE_ROOT(head);                                    \
                                                                         \
    while (TREE_LEFT(x, field) != NULL)                                  \
        x = TREE_LEFT(x, field);                                         \
                                                                         \
    return x;                                                            \
}                                                                        \
                                                                         \
static inline struct type*                                               \
name##_SPLAY_MAX(struct name *head)                                      \
{                                                                        \
    struct type *x = TREE_ROOT(head);                                    \
                                                                         \
    while (TREE_RIGHT(x, field) != NULL)                                 \
        x = TREE_RIGHT(x, field);                                        \
                                                                         \
    return x;                                                            \
}                                                                        \
                                                                         \
static inline struct type*                                               \
name##_SPLAY_GET(struct name *head, struct type *elm)                    \
{                                                                        \
    if (TREE_EMPTY(head))                                                \
        return NULL;                                                     \
                                                                         \
    name##_SPLAY(head, elm);                                             \
                                                                         \
    if (cmp(elm, TREE_ROOT(head)) == 0)                                  \
        return TREE_ROOT(head);                                          \
                                                                         \
    return NULL;                                                         \
}                                                                        \
                                                                         \
struct type*                                                             \
name##_SPLAY_ADD(struct name *head, struct type *elm)                    \
{                                                                        \
    if (TREE_EMPTY(head)) {                                              \
        TREE_LEFT(elm, field) = TREE_RIGHT(elm, field) = NULL;           \
    } else {                                                             \
        int comp;                                                        \
        name##_SPLAY(head, elm);                                         \
        comp = cmp(elm, TREE_ROOT(head));                                \
        if (comp < 0) {                                                  \
            TREE_LEFT(elm, field) = TREE_LEFT(TREE_ROOT(head), field);   \
            TREE_RIGHT(elm, field) = TREE_ROOT(head);                    \
            TREE_LEFT(TREE_ROOT(head), field) = NULL;                    \
        } else if (comp > 0) {                                           \
            TREE_RIGHT(elm, field) = TREE_RIGHT(TREE_ROOT(head), field); \
            TREE_LEFT(elm, field) = TREE_ROOT(head);                     \
            TREE_RIGHT(TREE_ROOT(head), field) = NULL;                   \
        } else                                                           \
            return (TREE_ROOT(head));                                    \
    }                                                                    \
    TREE_ROOT(head) = (elm);                                             \
    return NULL;                                                         \
}                                                                        \
                                                                         \
struct type*                                                             \
name##_SPLAY_DEL(struct name *head, struct type *elm)                    \
{                                                                        \
    struct type *tmp;                                                    \
    if (TREE_EMPTY(head))                                                \
        return NULL;                                                     \
    name##_SPLAY(head, elm);                                             \
    if (cmp(elm, TREE_ROOT(head)) == 0) {                                \
        if (TREE_LEFT(TREE_ROOT(head), field) == NULL) {                 \
            TREE_ROOT(head) = TREE_RIGHT(TREE_ROOT(head), field);        \
        } else {                                                         \
            tmp = TREE_RIGHT(TREE_ROOT(head), field);                    \
            TREE_ROOT(head) = TREE_LEFT(TREE_ROOT(head), field);         \
            name##_SPLAY(head, elm);                                     \
            TREE_RIGHT(TREE_ROOT(head), field) = tmp;                    \
        }                                                                \
        return (elm);                                                    \
    }                                                                    \
    return NULL;                                                         \
}                                                                        \
                                                                         \
void                                                                     \
name##_SPLAY(struct name *head, struct type *elm)                        \
{                                                                        \
    struct type node, *left, *right, *tmp;                               \
    int comp;                                                            \
                                                                         \
    TREE_LEFT(&node, field) = TREE_RIGHT(&node, field) = NULL;           \
    left = right = &node;                                                \
                                                                         \
    while ((comp = cmp(elm, TREE_ROOT(head))) != 0) {                    \
        if (comp < 0) {                                                  \
            tmp = TREE_LEFT(TREE_ROOT(head), field);                     \
            if (tmp == NULL)                                             \
                break;                                                   \
            if (cmp(elm, tmp) < 0){                                      \
                SPLAY_ROTATE_RIGHT(head, tmp, field);                    \
                if (TREE_LEFT(TREE_ROOT(head), field) == NULL)           \
                    break;                                               \
            }                                                            \
            SPLAY_LINK_LEFT(head, right, field);                         \
        } else if (comp > 0) {                                           \
            tmp = TREE_RIGHT(TREE_ROOT(head), field);                    \
            if (tmp == NULL)                                             \
                break;                                                   \
            if (cmp(elm, tmp) > 0){                                      \
                SPLAY_ROTATE_LEFT(head, tmp, field);                     \
                if (TREE_RIGHT(TREE_ROOT(head), field) == NULL)          \
                    break;                                               \
            }                                                            \
            SPLAY_LINK_RIGHT(head, left, field);                         \
        }                                                                \
    }                                                                    \
    SPLAY_ASSEMBLE(head, &node, left, right, field);                     \
}                                                                        \
                                                                         \
/* Suppress unused function warnings */                                  \
void (*name##_SPLAY_DUMMY[])(void) = {                                   \
    (void(*)(void))(name##_SPLAY_MAX),                                   \
    (void(*)(void))(name##_SPLAY_MIN) };

#endif

M src/utils/utils.c => src/utils/utils.c +91 -65
@@ 9,64 9,6 @@

static inline int irc_toupper(enum casemapping_t, int);

char*
strdup(const char *str)
{
	return memdup(str, strlen(str) + 1);
}

void*
memdup(const void *mem, size_t len)
{
	void *ret;

	if ((ret = malloc(len)) == NULL)
		fatal("malloc: %s", strerror(errno));

	memcpy(ret, mem, len);

	return ret;
}

int
str_trim(char **str)
{
	char *p;

	for (p = *str; *p && *p == ' '; p++)
		;

	*str = p;

	return !!*p;
}

static inline int
irc_toupper(enum casemapping_t casemapping, int c)
{
	/* RFC 2812, section 2.2
	 *
	 * Because of IRC's Scandinavian origin, the characters {}|^ are
	 * considered to be the lower case equivalents of the characters []\~,
	 * respectively. This is a critical issue when determining the
	 * equivalence of two nicknames or channel names. */

	switch (casemapping) {
		case CASEMAPPING_RFC1459:
			if (c == '^') return '~';
			/* FALLTHROUGH */
		case CASEMAPPING_STRICT_RFC1459:
			if (c == '{') return '[';
			if (c == '}') return ']';
			if (c == '|') return '\\';
			/* FALLTHROUGH */
		case CASEMAPPING_ASCII:
			return (c >= 'a' && c <= 'z') ? (c + 'A' - 'a') : c;
		default:
			fatal("Unknown CASEMAPPING");
	}
}

int
irc_isnickchar(char c, int first)
{


@@ 208,7 150,7 @@ irc_message_param(struct irc_message *m, char **param)
	if (m->params == NULL)
		return 0;

	if (!str_trim(&m->params))
	if (!strtrim(&m->params))
		return 0;

	if (!m->split && m->n_params >= 14) {


@@ 237,7 179,7 @@ irc_message_param(struct irc_message *m, char **param)
}

int
irc_message_parse(struct irc_message *m, char *buf, size_t len)
irc_message_parse(struct irc_message *m, char *buf)
{
	/* RFC 2812, section 2.3.1
	 *


@@ 256,11 198,9 @@ irc_message_parse(struct irc_message *m, char *buf, size_t len)
	 * crlf       =   %x0D %x0A   ; "carriage return" "linefeed"
	 */

	UNUSED(len);

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

	if (!str_trim(&buf))
	if (!strtrim(&buf))
		return 0;

	if (*buf == ':') {


@@ 297,7 237,7 @@ irc_message_parse(struct irc_message *m, char *buf, size_t len)
			*buf++ = 0;
	}

	if (!str_trim(&buf))
	if (!strtrim(&buf))
		return 0;

	m->command = buf;


@@ 310,7 250,7 @@ irc_message_parse(struct irc_message *m, char *buf, size_t len)
	if (*buf == ' ')
		*buf++ = 0;

	if (str_trim(&buf))
	if (strtrim(&buf))
		m->params = buf;

	return 1;


@@ 378,6 318,53 @@ irc_message_split(struct irc_message *m, char **trailing)
}

char*
strdup(const char *str)
{
	return memdup(str, strlen(str) + 1);
}

char*
strsep(char **str)
{
	char *p;
	char *ret;

	if (str == NULL || (p = *str) == NULL)
		return NULL;

	if ((ret = strtrim(&p)) == NULL)
		return NULL;

	while (*p && *p != ' ')
		p++;

	if (*p) {
		*p = 0;
		*str = (p + 1);
	} else {
		*str = NULL;
	}

	return *ret ? ret : NULL;
}

char*
strtrim(char **str)
{
	char *p;

	if (*str == NULL)
		return NULL;

	for (p = *str; *p && *p == ' '; p++)
		;

	*str = p;

	return *p ? p : NULL;
}

char*
word_wrap(int n, char **str, char *end)
{
	/* Greedy word wrap algorithm.


@@ 436,3 423,42 @@ word_wrap(int n, char **str, char *end)

	return ret;
}

void*
memdup(const void *mem, size_t len)
{
	void *ret;

	if ((ret = malloc(len)) == NULL)
		fatal("malloc: %s", strerror(errno));

	memcpy(ret, mem, len);

	return ret;
}

static inline int
irc_toupper(enum casemapping_t casemapping, int c)
{
	/* RFC 2812, section 2.2
	 *
	 * Because of IRC's Scandinavian origin, the characters {}|^ are
	 * considered to be the lower case equivalents of the characters []\~,
	 * respectively. This is a critical issue when determining the
	 * equivalence of two nicknames or channel names. */

	switch (casemapping) {
		case CASEMAPPING_RFC1459:
			if (c == '^') return '~';
			/* FALLTHROUGH */
		case CASEMAPPING_STRICT_RFC1459:
			if (c == '{') return '[';
			if (c == '}') return ']';
			if (c == '|') return '\\';
			/* FALLTHROUGH */
		case CASEMAPPING_ASCII:
			return (c >= 'a' && c <= 'z') ? (c + 'A' - 'a') : c;
		default:
			fatal("Unknown CASEMAPPING");
	}
}

M src/utils/utils.h => src/utils/utils.h +10 -9
@@ 4,15 4,15 @@
#include <stdio.h>
#include <stdlib.h>

#define TO_STR(X) #X
#define STR(X) TO_STR(X)

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

#define ELEMS(X) (sizeof((X)) / sizeof((X)[0]))
#define ARR_ELEM(A, E) ((E) >= 0 && (size_t)(E) < ELEMS((A)))

#define SEC_IN_MS(X) ((X) * 1000)
#define SEC_IN_US(X) ((X) * 1000 * 1000)

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

#define MESSAGE(TYPE, ...) \


@@ 56,10 56,6 @@ struct irc_message
	unsigned split : 1;
};

int irc_message_param(struct irc_message*, char**);
int irc_message_parse(struct irc_message*, char*, size_t);
int irc_message_split(struct irc_message*, char**);

int irc_ischan(const char*);
int irc_ischanchar(char, int);
int irc_isnick(const char*);


@@ 68,9 64,14 @@ int irc_pinged(enum casemapping_t, const char*, const char*);
int irc_strcmp(enum casemapping_t, const char*, const char*);
int irc_strncmp(enum casemapping_t, const char*, const char*, size_t);

int str_trim(char**);
char* word_wrap(int, char**, char*);
int irc_message_param(struct irc_message*, char**);
int irc_message_parse(struct irc_message*, char*);
int irc_message_split(struct irc_message*, char**);

char* strdup(const char*);
char* strsep(char**);
char* strtrim(char**);
char* word_wrap(int, char**, char*);
void* memdup(const void*, size_t);

#endif

M test/components/server.c => test/components/server.c +8 -8
@@ 164,7 164,7 @@ test_server_set_nicks(void)
}

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



@@ 174,26 174,26 @@ test_parse_opt(void)

	char opts0[] = "";
	ptr = opts0;
	assert_eq(parse_opt(&opt, &ptr), 0);
	assert_eq(parse_005(&opt, &ptr), 0);

	char opts1[] = "=";
	ptr = opts1;
	assert_eq(parse_opt(&opt, &ptr), 0);
	assert_eq(parse_005(&opt, &ptr), 0);

	char opts2[] = " ";
	ptr = opts2;
	assert_eq(parse_opt(&opt, &ptr), 0);
	assert_eq(parse_005(&opt, &ptr), 0);

	char opts3[] = "=TESTING";
	ptr = opts3;
	assert_eq(parse_opt(&opt, &ptr), 0);
	assert_eq(parse_005(&opt, &ptr), 0);

	char opts4[] = ":test,trailing";
	ptr = opts4;
	assert_eq(parse_opt(&opt, &ptr), 0);
	assert_eq(parse_005(&opt, &ptr), 0);

#define CHECK(R, A, V) \
	assert_eq(parse_opt(&opt, &ptr), (R)); \
	assert_eq(parse_005(&opt, &ptr), (R)); \
	assert_strcmp(opt.arg, (A)); \
	assert_strcmp(opt.val, (V));



@@ 251,7 251,7 @@ main(void)
	struct testcase tests[] = {
		TESTCASE(test_server_list),
		TESTCASE(test_server_set_nicks),
		TESTCASE(test_parse_opt)
		TESTCASE(test_parse_005)
	};

	return run_tests(tests);

M test/draw.c.mock => test/draw.c.mock +1 -0
@@ 1,4 1,5 @@
void draw(union draw d) { UNUSED(d); }
void draw_init(void) { ; }
void draw_bell(void) { ; }
void draw_term(void) { ; }
void

M test/handlers/irc_ctcp.c => test/handlers/irc_ctcp.c +2 -3
@@ 241,9 241,8 @@ test_recv_ctcp_request_ping(void)
	char m2[] = "\001PING 0\001";
	char m3[] = "\001PING 1 123 abc 0\001";

	CHECK_REQUEST("nick", "targ", m1, 0,
		"CTCP PING from nick",
		"NOTICE nick :\001PING \001");
	/* empty PING message, do nothing */
	CHECK_REQUEST("nick", "targ", m1, 0, "", "");

	CHECK_REQUEST("nick", "targ", m2, 0,
		"CTCP PING from nick",

M test/handlers/irc_send.c => test/handlers/irc_send.c +7 -5
@@ 184,7 184,6 @@ test_send_ctcp_ping(void)
	char m3[] = "ctcp-ping";
	char m4[] = "ctcp-ping targ";

	char *saveptr;
	char *p1;
	char *p2;



@@ 204,8 203,8 @@ test_send_ctcp_ping(void)
	*p1++ = 0;
	*p2++ = 0;

	assert_true((strtok_r(p1, " ", &saveptr)));
	assert_true((strtok_r(NULL, " ", &saveptr)));
	assert_ptr_not_null(strsep(&p1));
	assert_ptr_not_null(strsep(&p1));

	assert_strcmp(fail_buf, "");
	assert_strcmp(send_buf, "PRIVMSG priv :");


@@ 223,8 222,8 @@ test_send_ctcp_ping(void)
	*p1++ = 0;
	*p2++ = 0;

	assert_true((strtok_r(p1, " ", &saveptr)));
	assert_true((strtok_r(NULL, " ", &saveptr)));
	assert_ptr_not_null(strsep(&p1));
	assert_ptr_not_null(strsep(&p1));

	assert_strcmp(fail_buf, "");
	assert_strcmp(send_buf, "PRIVMSG targ :");


@@ 382,5 381,8 @@ main(void)

	int ret = run_tests(tests);
	server_free(s);
	channel_free(c_chan);
	channel_free(c_priv);
	channel_free(c_serv);
	return ret;
}

M test/io.c => test/io.c +2 -83
@@ 1,88 1,7 @@
#include "test/test.h"

/* Preclude definition for testing */
#define IO_MESG_LEN 10

#include "src/io.c"

/* Stubbed state callbacks */
void io_cb(enum io_cb_t t, const void *obj, ...) { UNUSED(t); UNUSED(obj); }
void io_cb_read_inp(char *buf, size_t n) { UNUSED(buf); UNUSED(n); }

static int cb_count;
static int cb_size;
static char soc_buf[IO_MESG_LEN + 1];

void io_cb_read_soc(char *buf, size_t n, const void *obj)
{
	UNUSED(obj);
	UNUSED(n);
	cb_count++;
	cb_size = (int)n;
	snprintf(soc_buf, sizeof(soc_buf), "%s", buf);
}

static void
test_io_recv(void)
{
	struct connection c;
	memset(&c, 0, sizeof(c));

#define IO_RECV(S) \
	io_recv(&c, (S), sizeof((S)) - 1);

	/* Test complete message received */
	soc_buf[0] = 0;

	IO_RECV("foo\r\nbar\r\n");
	assert_eq((signed) c.read.i, 0);
	assert_eq(cb_count, 2);
	assert_eq(cb_size, 3);
	assert_strcmp(soc_buf, "bar");

	/* Test empty messages */
	IO_RECV("\r\n\r\n");
	IO_RECV("\r");
	IO_RECV("\n");
	assert_eq(cb_count, 2);
	
	/* Test message received in multiple parts */
	IO_RECV("a");
	IO_RECV("b");
	IO_RECV("c\r");
	IO_RECV("\nx");
	assert_eq(cb_count, 3);
	assert_eq(cb_size, 3);
	assert_strcmp(soc_buf, "abc");
	IO_RECV("yz\r\n");
	assert_eq(cb_count, 4);
	assert_eq(cb_size, 3);
	assert_strcmp(soc_buf, "xyz");

	/* Test non-delimiter, non-CTCP control character are skiped */
	const char str1[] = {'a', 0x00, 0x01, 0x02, '\r', 'b', '\n', 'c', 0x01, '\r', '\n', 0};
	const char str2[] = {'a', 0x01, 'b', 'c', 0x01, 0};
	IO_RECV(str1);
	assert_eq(cb_count, 5);
	assert_eq(cb_size, 5);
	assert_strcmp(soc_buf, str2);

	/* Test buffer overrun */
	IO_RECV("abcdefghijklmnopqrstuvwxyz");
	IO_RECV("\r\n");
	assert_eq(cb_count, 6);
	assert_eq(cb_size, 10);
	assert_strcmp(soc_buf, "abcdefghij");

#undef IO_RECV
}
#include <stdlib.h>

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

	return run_tests(tests);
	return EXIT_SUCCESS;
}

M test/io.c.mock => test/io.c.mock +4 -2
@@ 12,5 12,7 @@ int io_dx(struct connection *c) { UNUSED(c); return 0; }
int io_sendf(struct connection *c, const char *f, ...) { UNUSED(c); UNUSED(f); return 0; }
unsigned io_tty_cols(void) { return 0; }
unsigned io_tty_rows(void) { return 0; }
void io_free(struct connection *c) { UNUSED(c); }
void io_term(void) { ; }
void connection_free(struct connection *c) { UNUSED(c); }
void io_stop(void) { ; }
void io_start(void) { ; }
void io_init(void) { ; }

A test/io_net.c => test/io_net.c +7 -0
@@ 0,0 1,7 @@
#include <stdlib.h>

int
main(void)
{
	return EXIT_SUCCESS;
}

M test/state.c => test/state.c +0 -2
@@ 69,8 69,6 @@ test_state(void)
	assert_ptr_eq(server_list_add(state_server_list(), s1), NULL);
	assert_ptr_eq(server_list_add(state_server_list(), s2), NULL);
	assert_ptr_eq(server_list_add(state_server_list(), s3), NULL);

	state_term();
}

int

M test/test.h => test/test.h +9 -1
@@ 20,6 20,7 @@
 *   Pointer comparison:
 *    - assert_ptr_eq(expr, expected)
 *    - assert_ptr_null(expr)
 *    - assert_ptr_not_null(expr)
 *
 *   Assert that the expression exits rirc fatally:
 *    - assert_fatal(expr)


@@ 195,10 196,17 @@ static void _print_testcase_name_(const char*);
#define assert_ptr_null(X) \
	do { \
		uintptr_t __ret_x = (uintptr_t)(X); \
		if (__ret_x ) \
		if (__ret_x) \
			fail_testf(#X " expected NULL, got '%016" PRIxPTR "'", __ret_x); \
	} while (0)

#define assert_ptr_not_null(X) \
	do { \
		uintptr_t __ret_x = (uintptr_t)(X); \
		if (!__ret_x) \
			fail_test(#X " expected not NULL"); \
	} while (0)

#define assert_fatal(X) \
	do { \
		if (setjmp(_tc_fatal_expected_)) { \

M test/utils/tree.c => test/utils/tree.c +101 -97
@@ 1,58 1,62 @@
#include "test/test.h"
#include "src/utils/tree.h"

struct test_avl
struct test_node
{
	AVL_NODE(test_avl) node;
	TREE_NODE(test_node) node;
	int val;
};

struct test_avl_list
struct test_tree
{
	AVL_HEAD(test_avl);
	TREE_HEAD(test_node);
};

static inline int
test_avl_cmp(struct test_avl *t1, struct test_avl *t2)
test_cmp(struct test_node *t1, struct test_node *t2, void *unused)
{
	(void)unused;

	return (t1->val == t2->val) ? 0 : ((t1->val > t2->val) ? 1 : -1);
}

static inline int
test_avl_cmp_n(struct test_avl *t1, struct test_avl *t2, size_t n)
test_ncmp(struct test_node *t1, struct test_node *t2, void *unused, size_t n)
{
	(void)unused;

	int tmp = t1->val * n;

	return (tmp == t2->val) ? 0 : ((tmp > t2->val) ? 1 : -1);
}

static void
foreach_f(struct test_avl *t)
foreach_f(struct test_node *t)
{
	t->val = 0;
	t->node.tree_left = NULL;
	t->node.tree_right = NULL;
}

AVL_GENERATE(test_avl_list, test_avl, node, test_avl_cmp, test_avl_cmp_n)
AVL_GENERATE(test_tree, test_node, node, test_cmp, test_ncmp)

static void
test_avl_get_height(void)
{
	struct test_avl t0 = { .node = { .height = 1 }};
	struct test_node t0 = { .node = { .height = 1 }};

	assert_eq(test_avl_list_AVL_GET_HEIGHT(&t0),  1);
	assert_eq(test_avl_list_AVL_GET_HEIGHT(NULL), 0);
	assert_eq(test_tree_AVL_GET_HEIGHT(&t0),  1);
	assert_eq(test_tree_AVL_GET_HEIGHT(NULL), 0);
}

static void
test_avl_set_height(void)
{
	struct test_avl
	struct test_node
		t0 = { .node = { .height = 1 }},
		t1 = { .node = { .tree_left = &t0 }};

	assert_eq(test_avl_list_AVL_SET_HEIGHT(&t1), 2);
	assert_eq(test_tree_AVL_SET_HEIGHT(&t1), 2);
}

static void


@@ 67,7 71,7 @@ test_avl_balance(void)
	 *         t00     t01  balance : 0, 0
	 */

	struct test_avl
	struct test_node
		t00 = { .node = { .height = 1 }},
		t01 = { .node = { .height = 1 }},
		t10 = { .node = { .height = 1 }},


@@ 79,13 83,13 @@ test_avl_balance(void)
		t30 = { .node = { .tree_left  = &t20,
		                  .tree_right = &t21 }};

	assert_eq(test_avl_list_AVL_BALANCE(&t00), 0);
	assert_eq(test_avl_list_AVL_BALANCE(&t01), 0);
	assert_eq(test_avl_list_AVL_BALANCE(&t10), 0);
	assert_eq(test_avl_list_AVL_BALANCE(&t11), 0);
	assert_eq(test_avl_list_AVL_BALANCE(&t20), 0);
	assert_eq(test_avl_list_AVL_BALANCE(&t21), 1);
	assert_eq(test_avl_list_AVL_BALANCE(&t30), 2);
	assert_eq(test_tree_AVL_BALANCE(&t00), 0);
	assert_eq(test_tree_AVL_BALANCE(&t01), 0);
	assert_eq(test_tree_AVL_BALANCE(&t10), 0);
	assert_eq(test_tree_AVL_BALANCE(&t11), 0);
	assert_eq(test_tree_AVL_BALANCE(&t20), 0);
	assert_eq(test_tree_AVL_BALANCE(&t21), 1);
	assert_eq(test_tree_AVL_BALANCE(&t30), 2);

	/*         t70      balance : -2
	 *        /   \


@@ 96,7 100,7 @@ test_avl_balance(void)
	 *     t40     t41  balance : 0, 0
	 */

	struct test_avl
	struct test_node
		t40 = { .node = { .height = 1 }},
		t41 = { .node = { .height = 1 }},
		t50 = { .node = { .height = 1 }},


@@ 108,13 112,13 @@ test_avl_balance(void)
		t70 = { .node = { .tree_left  = &t60,
		                  .tree_right = &t61 }};

	assert_eq(test_avl_list_AVL_BALANCE(&t40),  0);
	assert_eq(test_avl_list_AVL_BALANCE(&t41),  0);
	assert_eq(test_avl_list_AVL_BALANCE(&t50),  0);
	assert_eq(test_avl_list_AVL_BALANCE(&t51),  0);
	assert_eq(test_avl_list_AVL_BALANCE(&t60),  1);
	assert_eq(test_avl_list_AVL_BALANCE(&t61),  0);
	assert_eq(test_avl_list_AVL_BALANCE(&t70), -2);
	assert_eq(test_tree_AVL_BALANCE(&t40),  0);
	assert_eq(test_tree_AVL_BALANCE(&t41),  0);
	assert_eq(test_tree_AVL_BALANCE(&t50),  0);
	assert_eq(test_tree_AVL_BALANCE(&t51),  0);
	assert_eq(test_tree_AVL_BALANCE(&t60),  1);
	assert_eq(test_tree_AVL_BALANCE(&t61),  0);
	assert_eq(test_tree_AVL_BALANCE(&t70), -2);
}

static void


@@ 131,9 135,9 @@ test_avl_add(void)
	 * 50     150 250     350
	 */

	struct test_avl_list tl = {0};
	struct test_tree tl = {0};

	struct test_avl
	struct test_node
		t0 = { .val = 200 },
		t1 = { .val = 100 },
		t2 = { .val = 300 },


@@ 142,16 146,16 @@ test_avl_add(void)
		t5 = { .val = 250 },
		t6 = { .val = 350 };

	assert_ptr_eq(test_avl_list_AVL_ADD(&tl, &t0), &t0);
	assert_ptr_eq(test_avl_list_AVL_ADD(&tl, &t1), &t1);
	assert_ptr_eq(test_avl_list_AVL_ADD(&tl, &t2), &t2);
	assert_ptr_eq(test_avl_list_AVL_ADD(&tl, &t3), &t3);
	assert_ptr_eq(test_avl_list_AVL_ADD(&tl, &t4), &t4);
	assert_ptr_eq(test_avl_list_AVL_ADD(&tl, &t5), &t5);
	assert_ptr_eq(test_avl_list_AVL_ADD(&tl, &t6), &t6);
	assert_ptr_eq(test_tree_AVL_ADD(&tl, &t0, 0), &t0);
	assert_ptr_eq(test_tree_AVL_ADD(&tl, &t1, 0), &t1);
	assert_ptr_eq(test_tree_AVL_ADD(&tl, &t2, 0), &t2);
	assert_ptr_eq(test_tree_AVL_ADD(&tl, &t3, 0), &t3);
	assert_ptr_eq(test_tree_AVL_ADD(&tl, &t4, 0), &t4);
	assert_ptr_eq(test_tree_AVL_ADD(&tl, &t5, 0), &t5);
	assert_ptr_eq(test_tree_AVL_ADD(&tl, &t6, 0), &t6);

	/* Duplicate */
	assert_ptr_null(test_avl_list_AVL_ADD(&tl, &t6));
	assert_ptr_null(test_tree_AVL_ADD(&tl, &t6, 0));

	/* Check tree structure */
	assert_ptr_eq(TREE_ROOT(&tl), &t0);


@@ 178,17 182,17 @@ test_avl_add(void)
	assert_ptr_null(t6.node.tree_right);

	/* Retrieve the nodes */
	assert_ptr_eq(test_avl_list_AVL_GET(&tl, &t0), &t0);
	assert_ptr_eq(test_avl_list_AVL_GET(&tl, &t1), &t1);
	assert_ptr_eq(test_avl_list_AVL_GET(&tl, &t2), &t2);
	assert_ptr_eq(test_avl_list_AVL_GET(&tl, &t3), &t3);
	assert_ptr_eq(test_avl_list_AVL_GET(&tl, &t4), &t4);
	assert_ptr_eq(test_avl_list_AVL_GET(&tl, &t5), &t5);
	assert_ptr_eq(test_avl_list_AVL_GET(&tl, &t6), &t6);
	assert_ptr_eq(test_tree_AVL_GET(&tl, &t0, 0), &t0);
	assert_ptr_eq(test_tree_AVL_GET(&tl, &t1, 0), &t1);
	assert_ptr_eq(test_tree_AVL_GET(&tl, &t2, 0), &t2);
	assert_ptr_eq(test_tree_AVL_GET(&tl, &t3, 0), &t3);
	assert_ptr_eq(test_tree_AVL_GET(&tl, &t4, 0), &t4);
	assert_ptr_eq(test_tree_AVL_GET(&tl, &t5, 0), &t5);
	assert_ptr_eq(test_tree_AVL_GET(&tl, &t6, 0), &t6);

	struct test_avl t7 = { .val = -1 };
	struct test_node t7 = { .val = -1 };

	assert_ptr_null(test_avl_list_AVL_GET(&tl, &t7));
	assert_ptr_null(test_tree_AVL_GET(&tl, &t7, 0));
}

static void


@@ 205,9 209,9 @@ test_avl_del(void)
	 * 50     150 250     350
	 */

	struct test_avl_list tl = {0};
	struct test_tree tl = {0};

	struct test_avl
	struct test_node
		t200 = { .val = 200 },
		t100 = { .val = 100 },
		t300 = { .val = 300 },


@@ 217,16 221,16 @@ test_avl_del(void)
		t350 = { .val = 350 },
		t0 = { .val = 0 };

	assert_ptr_eq(test_avl_list_AVL_ADD(&tl, &t200), &t200);
	assert_ptr_eq(test_avl_list_AVL_ADD(&tl, &t100), &t100);
	assert_ptr_eq(test_avl_list_AVL_ADD(&tl, &t300), &t300);
	assert_ptr_eq(test_avl_list_AVL_ADD(&tl, &t050), &t050);
	assert_ptr_eq(test_avl_list_AVL_ADD(&tl, &t150), &t150);
	assert_ptr_eq(test_avl_list_AVL_ADD(&tl, &t250), &t250);
	assert_ptr_eq(test_avl_list_AVL_ADD(&tl, &t350), &t350);
	assert_ptr_eq(test_tree_AVL_ADD(&tl, &t200, 0), &t200);
	assert_ptr_eq(test_tree_AVL_ADD(&tl, &t100, 0), &t100);
	assert_ptr_eq(test_tree_AVL_ADD(&tl, &t300, 0), &t300);
	assert_ptr_eq(test_tree_AVL_ADD(&tl, &t050, 0), &t050);
	assert_ptr_eq(test_tree_AVL_ADD(&tl, &t150, 0), &t150);
	assert_ptr_eq(test_tree_AVL_ADD(&tl, &t250, 0), &t250);
	assert_ptr_eq(test_tree_AVL_ADD(&tl, &t350, 0), &t350);

	/* Test deleting node not found in tree */
	assert_ptr_eq(test_avl_list_AVL_DEL(&tl, &t0), NULL);
	assert_ptr_eq(test_tree_AVL_DEL(&tl, &t0, 0), NULL);

	/* Delete 200; In-order successor is substituted from leaf
	 *


@@ 237,7 241,7 @@ test_avl_del(void)
	 * 50     150        350
	 */

	assert_ptr_eq(test_avl_list_AVL_DEL(&tl, &t200), &t200);
	assert_ptr_eq(test_tree_AVL_DEL(&tl, &t200, 0), &t200);

	/* Check tree structure */
	assert_ptr_eq(TREE_ROOT(&tl), &t250);


@@ 270,7 274,7 @@ test_avl_del(void)
	 *
	 */

	assert_ptr_eq(test_avl_list_AVL_DEL(&tl, &t250), &t250);
	assert_ptr_eq(test_tree_AVL_DEL(&tl, &t250, 0), &t250);

	/* Check tree structure */
	assert_ptr_eq(TREE_ROOT(&tl), &t300);


@@ 306,7 310,7 @@ test_avl_del(void)
	 *        150
	 */

	assert_ptr_eq(test_avl_list_AVL_DEL(&tl, &t300), &t300);
	assert_ptr_eq(test_tree_AVL_DEL(&tl, &t300, 0), &t300);

	/* Check tree structure */
	assert_ptr_eq(TREE_ROOT(&tl), &t100);


@@ 338,7 342,7 @@ test_avl_del(void)
	 * 100     350
	 */

	assert_ptr_eq(test_avl_list_AVL_DEL(&tl, &t050), &t050);
	assert_ptr_eq(test_tree_AVL_DEL(&tl, &t050, 0), &t050);

	/* Check tree structure */
	assert_ptr_eq(TREE_ROOT(&tl), &t150);


@@ 354,13 358,13 @@ test_avl_del(void)

	/* Test same-key based delete returns pointer to the deleted object */

	struct test_avl key_test = { .val = t100.val };
	struct test_node key_test = { .val = t100.val };

	assert_ptr_eq(test_avl_list_AVL_DEL(&tl, &key_test), &t100);
	assert_ptr_eq(test_tree_AVL_DEL(&tl, &key_test, 0), &t100);

	/* Delete remaining nodes */
	assert_ptr_eq(test_avl_list_AVL_DEL(&tl, &t150), &t150);
	assert_ptr_eq(test_avl_list_AVL_DEL(&tl, &t350), &t350);
	assert_ptr_eq(test_tree_AVL_DEL(&tl, &t150, 0), &t150);
	assert_ptr_eq(test_tree_AVL_DEL(&tl, &t350, 0), &t350);

	assert_ptr_null(TREE_ROOT(&tl));
}


@@ 370,20 374,20 @@ test_avl_get_n(void)
{
	/* Test parameterized matching */

	struct test_avl_list tl = {0};
	struct test_tree tl = {0};

	struct test_avl
	struct test_node
		t0 = { .val =   0, },
		t1 = { .val =  10, },
		t2 = { .val = -15, },
		t3 = { .val =   5, };

	assert_ptr_eq(test_avl_list_AVL_ADD(&tl, &t0), &t0);
	assert_ptr_eq(test_avl_list_AVL_ADD(&tl, &t1), &t1);
	assert_ptr_eq(test_avl_list_AVL_ADD(&tl, &t2), &t2);
	assert_ptr_eq(test_tree_AVL_ADD(&tl, &t0, 0), &t0);
	assert_ptr_eq(test_tree_AVL_ADD(&tl, &t1, 0), &t1);
	assert_ptr_eq(test_tree_AVL_ADD(&tl, &t2, 0), &t2);

	assert_ptr_eq(test_avl_list_AVL_NGET(&tl, &t3,  2), &t1);
	assert_ptr_eq(test_avl_list_AVL_NGET(&tl, &t3, -3), &t2);
	assert_ptr_eq(test_tree_AVL_NGET(&tl, &t3, 0,  2), &t1);
	assert_ptr_eq(test_tree_AVL_NGET(&tl, &t3, 0, -3), &t2);
}

static void


@@ 391,7 395,7 @@ test_avl_rotations(void)
{
	/* Exercise all 4 rotation types */

	struct test_avl_list tl = {0};
	struct test_tree tl = {0};

	/* Add 100, 200, 300:
	 *


@@ 408,14 412,14 @@ test_avl_rotations(void)
	 *   100     300
	 */

	struct test_avl
	struct test_node
		t0 = { .val = 100 },
		t1 = { .val = 200 },
		t2 = { .val = 300 };

	assert_ptr_eq(test_avl_list_AVL_ADD(&tl, &t0), &t0);
	assert_ptr_eq(test_avl_list_AVL_ADD(&tl, &t1), &t1);
	assert_ptr_eq(test_avl_list_AVL_ADD(&tl, &t2), &t2);
	assert_ptr_eq(test_tree_AVL_ADD(&tl, &t0, 0), &t0);
	assert_ptr_eq(test_tree_AVL_ADD(&tl, &t1, 0), &t1);
	assert_ptr_eq(test_tree_AVL_ADD(&tl, &t2, 0), &t2);

	assert_ptr_eq(TREE_ROOT(&tl), &t1);



@@ 452,12 456,12 @@ test_avl_rotations(void)
	 *   225
	 */

	struct test_avl
	struct test_node
		t3 = { .val = 225 },
		t4 = { .val = 275 };

	assert_ptr_eq(test_avl_list_AVL_ADD(&tl, &t3), &t3);
	assert_ptr_eq(test_avl_list_AVL_ADD(&tl, &t4), &t4);
	assert_ptr_eq(test_tree_AVL_ADD(&tl, &t3, 0), &t3);
	assert_ptr_eq(test_tree_AVL_ADD(&tl, &t4, 0), &t4);

	assert_ptr_eq(TREE_ROOT(&tl), &t1);



@@ 500,12 504,12 @@ test_avl_rotations(void)
	 *   40    100 225     300
	 */

	struct test_avl
	struct test_node
		t5 = { .val = 50 },
		t6 = { .val = 40 };

	assert_ptr_eq(test_avl_list_AVL_ADD(&tl, &t5), &t5);
	assert_ptr_eq(test_avl_list_AVL_ADD(&tl, &t6), &t6);
	assert_ptr_eq(test_tree_AVL_ADD(&tl, &t5, 0), &t5);
	assert_ptr_eq(test_tree_AVL_ADD(&tl, &t6, 0), &t6);

	assert_ptr_eq(TREE_ROOT(&tl), &t1);



@@ 564,12 568,12 @@ test_avl_rotations(void)
	 *
	 */

	struct test_avl
	struct test_node
		t7 = { .val = 45 },
		t8 = { .val = 42 };

	assert_ptr_eq(test_avl_list_AVL_ADD(&tl, &t7), &t7);
	assert_ptr_eq(test_avl_list_AVL_ADD(&tl, &t8), &t8);
	assert_ptr_eq(test_tree_AVL_ADD(&tl, &t7, 0), &t7);
	assert_ptr_eq(test_tree_AVL_ADD(&tl, &t8, 0), &t8);

	assert_ptr_eq(TREE_ROOT(&tl), &t1);



@@ 615,9 619,9 @@ test_avl_foreach(void)
{
	/* Test that each node can be reached and altered safely (e.g. freed) */

	struct test_avl_list tl = {0};
	struct test_tree tl = {0};

	struct test_avl
	struct test_node
		t200 = { .val = 200 },
		t100 = { .val = 100 },
		t300 = { .val = 300 },


@@ 626,15 630,15 @@ test_avl_foreach(void)
		t250 = { .val = 250 },
		t350 = { .val = 350 };

	assert_ptr_eq(test_avl_list_AVL_ADD(&tl, &t200), &t200);
	assert_ptr_eq(test_avl_list_AVL_ADD(&tl, &t100), &t100);
	assert_ptr_eq(test_avl_list_AVL_ADD(&tl, &t300), &t300);
	assert_ptr_eq(test_avl_list_AVL_ADD(&tl, &t050), &t050);
	assert_ptr_eq(test_avl_list_AVL_ADD(&tl, &t150), &t150);
	assert_ptr_eq(test_avl_list_AVL_ADD(&tl, &t250), &t250);
	assert_ptr_eq(test_avl_list_AVL_ADD(&tl, &t350), &t350);
	assert_ptr_eq(test_tree_AVL_ADD(&tl, &t200, 0), &t200);
	assert_ptr_eq(test_tree_AVL_ADD(&tl, &t100, 0), &t100);
	assert_ptr_eq(test_tree_AVL_ADD(&tl, &t300, 0), &t300);
	assert_ptr_eq(test_tree_AVL_ADD(&tl, &t050, 0), &t050);
	assert_ptr_eq(test_tree_AVL_ADD(&tl, &t150, 0), &t150);
	assert_ptr_eq(test_tree_AVL_ADD(&tl, &t250, 0), &t250);
	assert_ptr_eq(test_tree_AVL_ADD(&tl, &t350, 0), &t350);

	test_avl_list_AVL_FOREACH(&tl, foreach_f);
	test_tree_AVL_FOREACH(&tl, foreach_f);

	assert_eq(t200.val, 0);
	assert_ptr_null(t200.node.tree_left);

M test/utils/utils.c => test/utils/utils.c +49 -9
@@ 12,7 12,7 @@ test_irc_message_param(void)
	assert_strcmp(param, (S));

#define CHECK_IRC_MESSAGE_PARSE(M, R) \
	assert_eq(irc_message_parse(&m, (M), sizeof((M)) - 1), (R));
	assert_eq(irc_message_parse(&m, (M)), (R));

	/* Test ordinary args */
	char mesg1[] = ":nick!user@host.domain.tld CMD arg1 arg2 arg3 :trailing arg";


@@ 86,7 86,7 @@ test_irc_message_parse(void)
	struct irc_message m;

#define CHECK_IRC_MESSAGE_PARSE(M, R) \
	assert_eq(irc_message_parse(&m, (M), sizeof((M)) - 1), (R));
	assert_eq(irc_message_parse(&m, (M)), (R));

	/* Test ordinary message */
	char mesg1[] = ":nick!user@host.domain.tld CMD args :trailing";


@@ 187,7 187,7 @@ test_irc_message_split(void)
	assert_strcmp(param, (S));

#define CHECK_IRC_MESSAGE_PARSE(M, R) \
	assert_eq(irc_message_parse(&m, (M), sizeof((M)) - 1), (R));
	assert_eq(irc_message_parse(&m, (M)), (R));

#define CHECK_IRC_MESSAGE_SPLIT(R, P, T) \
	assert_eq(irc_message_split(&m, &trailing), (R)); \


@@ 450,25 450,64 @@ test_irc_toupper(void)
}

static void
test_str_trim(void)
test_strsep(void)
{
	char *p;

	char mesg1[] = "";
	char mesg2[] = " ";
	char mesg3[] = "  ";

	assert_ptr_null(strsep(NULL));

	p = mesg1;
	assert_ptr_null(strsep(&p));

	p = mesg2;
	assert_ptr_null(strsep(&p));

	p = mesg3;
	assert_ptr_null(strsep(&p));

	char mesg4[] = "test1";
	p = mesg4;
	assert_strcmp(strsep(&p), "test1");
	assert_ptr_null(strsep(&p));

	char mesg5[] = "test2 test3";
	p = mesg5;
	assert_strcmp(strsep(&p), "test2");
	assert_strcmp(strsep(&p), "test3");
	assert_ptr_null(strsep(&p));

	char mesg6[] = "  test4   test5  test6   ";
	p = mesg6;
	assert_strcmp(strsep(&p), "test4");
	assert_strcmp(strsep(&p), "test5");
	assert_strcmp(strsep(&p), "test6");
	assert_ptr_null(strsep(&p));
}

static void
test_strtrim(void)
{
	/* Test skipping space at the begging of a string pointer
	 * and returning 0 when no non-space character is found */

	char *mesg1 = "testing";
	assert_eq(str_trim(&mesg1), 1);
	assert_ptr_eq(strtrim(&mesg1), mesg1);
	assert_strcmp(mesg1, "testing");

	char *mesg2 = " testing ";
	assert_eq(str_trim(&mesg2), 1);
	assert_ptr_eq(strtrim(&mesg2), mesg2);
	assert_strcmp(mesg2, "testing ");

	char *mesg3 = "";
	assert_eq(str_trim(&mesg3), 0);
	assert_ptr_null(strtrim(&mesg3));
	assert_strcmp(mesg3, "");

	char *mesg4 = " ";
	assert_eq(str_trim(&mesg4), 0);
	assert_ptr_null(strtrim(&mesg4));
	assert_strcmp(mesg4, "");
}



@@ 596,7 635,8 @@ main(void)
		TESTCASE(test_irc_strcmp),
		TESTCASE(test_irc_strncmp),
		TESTCASE(test_irc_toupper),
		TESTCASE(test_str_trim),
		TESTCASE(test_strsep),
		TESTCASE(test_strtrim),
		TESTCASE(test_word_wrap)
	};