~rcr/rirc

e4febc193b633dd360d3d96ece9f5680c21eba8f — Richard Robbins 10 months ago 8c2f704 + 0a72228
Merge branch 'dev' into static_analysis
65 files changed, 2356 insertions(+), 1219 deletions(-)

M .builds/alpine.yml
M .builds/debian.yml
M .gitignore
M CHANGELOG
M LICENSE
M Makefile
M README.md
M config.def.h
M lib/mbedtls
M rirc.1
M scripts/compile_commands.sh
M scripts/coverage.sh
M scripts/pre-commit.sh
R scripts/{coverity_get.sh => sa_coverity_get.sh}
R scripts/{coverity_run.sh => sa_coverity_run.sh}
A scripts/sa_sonarcloud_get.sh
A scripts/sa_sonarcloud_run.sh
M scripts/sanitizers_build.sh
M scripts/sanitizers_test.sh
M src/components/buffer.c
M src/components/buffer.h
M src/components/channel.c
M src/components/channel.h
M src/components/input.c
M src/components/input.h
M src/components/ircv3.h
M src/components/mode.c
M src/components/mode.h
M src/components/server.c
M src/components/server.h
M src/components/user.c
M src/components/user.h
M src/draw.c
M src/draw.h
M src/handlers/irc_ctcp.c
M src/handlers/irc_ctcp.h
M src/handlers/irc_recv.c
M src/handlers/irc_recv.gperf
M src/handlers/irc_recv.h
M src/handlers/irc_send.c
M src/handlers/irc_send.gperf
M src/handlers/irc_send.h
M src/handlers/ircv3.c
M src/handlers/ircv3.h
M src/io.c
M src/io.h
M src/rirc.c
M src/rirc.h
M src/state.c
M src/state.h
M src/utils/list.h
M src/utils/tree.h
M src/utils/utils.c
M src/utils/utils.h
M test/components/channel.c
M test/components/mode.c
M test/components/user.c
M test/handlers/irc_ctcp.c
M test/handlers/irc_recv.c
M test/handlers/irc_send.c
M test/io.mock.c
M test/state.c
M test/state.mock.c
M test/utils/tree.c
M test/utils/utils.c
M .builds/alpine.yml => .builds/alpine.yml +9 -1
@@ 13,6 13,14 @@ tasks:
      cd rirc
      git submodule init
      git submodule update --recursive
      export MAKEFLAGS='-j $(nproc)'
  - build: |
      cd rirc
      make rirc rirc.debug test
      make clean
      make all
      make check

triggers:
  - action: email
    condition: failure
    to: mail+sourcehut+builds@rcr.io

M .builds/debian.yml => .builds/debian.yml +14 -28
@@ 10,9 10,6 @@ packages:
sources:
  - https://git.sr.ht/~rcr/rirc

environment:
  SONAR_VER: 4.4.0.2170

secrets:
  - 8c2439c9-5f91-4b19-b3c3-33d82f1b861f
  - a58d0951-f57f-44ef-8ef2-25f2f84f0e89


@@ 22,36 19,25 @@ tasks:
      cd rirc
      git submodule init
      git submodule update --recursive
      export MAKEFLAGS='-j $(nproc)'
  - build: |
      cd rirc
      make rirc rirc.debug test
      make clean
      make all
      make check
  - static-analysis: |
      cd rirc
      branch=$(git name-rev --name-only HEAD)
      [ $branch = "remotes/origin/static_analysis" ] || complete-build
      [ $(git name-rev --name-only HEAD) = "remotes/origin/static_analysis" ] || complete-build
      set +x
      source ~/export_coverity
      source ~/export_sonarscan
      set -x
      # Coverity
      ./scripts/coverity_get.sh coverity
      ./scripts/coverity_run.sh coverity
      # Sonarcloud
      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.cache.enabled=false"
      echo >> sonar-project.properties "sonar.cfamily.gcov.reportsPath=."
      echo >> sonar-project.properties "sonar.cfamily.threads=1"
      echo >> sonar-project.properties "sonar.coverage.exclusions=test"
      echo >> sonar-project.properties "sonar.host.url=https://sonarcloud.io"
      echo >> sonar-project.properties "sonar.organization=rirc"
      echo >> sonar-project.properties "sonar.projectKey=rirc"
      echo >> sonar-project.properties "sonar.sources=src"
      echo >> sonar-project.properties "sonar.tests=test"
      ./build-wrapper-linux-x86/build-wrapper-linux-x86-64 --out-dir bw-output make clean rirc.debug test
      ./sonar-scanner-$SONAR_VER-linux/bin/sonar-scanner
      ./scripts/sa_coverity_get.sh coverity
      ./scripts/sa_coverity_run.sh coverity
      ./scripts/sa_sonarcloud_get.sh sonarcloud
      ./scripts/sa_sonarcloud_run.sh sonarcloud

triggers:
  - action: email
    condition: failure
    to: mail+sourcehut+builds@rcr.io

M .gitignore => .gitignore +1 -0
@@ 6,6 6,7 @@
*.t
*.td
.clangd
.cache
bld
compile_commands.json
config.h

M CHANGELOG => CHANGELOG +15 -0
@@ 2,6 2,21 @@
Summary of notable changes and features

## Unreleased (dev)
### Features
 - add IRCv3 CAP account-notify
    - add ACCOUNT_THRESHOLD define to config.def.h
 - add IRCv3 CAP away-notify
    - add AWAY_THRESHOLD define to config.def.h
 - add IRCv3 CAP chghost
    - add CHGHOST_THRESHOLD define to config.def.h
 - add IRCv3 CAP invite-notify
 - add IRCv3 CAP extended-join
 - add action message colouring config
    - add ACTION_FG define to config.def.h
    - add ACTION_BG define to config.def.h
### Fixes
 - fix incorrect INVITE message handling
 - fix message filter thresholds

## [0.1.3]
### Features

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

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

M Makefile => Makefile +31 -20
@@ 28,6 28,8 @@ DIR_B := bld
DIR_S := src
DIR_T := test

PWD  := $(shell pwd)

SRC     := $(shell find $(DIR_S) -name '*.c')
SUBDIRS += $(shell find $(DIR_S) -name '*.c' -exec dirname {} \; | sort -u)



@@ 44,27 46,34 @@ OBJS_T += $(DIR_B)/utils/tree.t # Header only file
OBJS_G := $(patsubst %.gperf, %.gperf.out, $(SRC_G))

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

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

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

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

# Testcases
$(DIR_B)/%.t: $(DIR_T)/%.c $(OBJS_G) | $(DIR_B)
	@$(PP) $(CFLAGS_D) -MM -MP -MT $@ -MF $(@:.t=.t.d) $<
	@$(CC) $(CFLAGS_D) -c -o $(@:.t=.t.o) $<
	@$(CC) $(CFLAGS_D) -o $@ $(@:.t=.t.o)
	@$(TEST_EXT) ./$@

# Default config file
config.h:
	cp config.def.h config.h


@@ 73,24 82,26 @@ config.h:
%.gperf.out: %.gperf
	gperf --output-file=$@ $<

# Testcase files
$(DIR_B)/%.t: $(DIR_T)/%.c
	@$(PP) $(CFLAGS_D) -MM -MP -MT $@ -MF $(@:.t=.d) $<
	@$(CC) $(CFLAGS_D) $(LDFLAGS) -o $@ $<
	-@rm -f $(@:.t=.td) && $(TEST_EXT) ./$@ || mv $@ $(@:.t=.td)
	@[ ! -f $(@:.t=.td) ]

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

# TLS libraries
$(TLS_LIBS): $(TLS_CONF)
	@CFLAGS="$(TLS_INCL)" $(MAKE) -C ./lib/mbedtls clean lib
	@CFLAGS="$(TLS_INCL)" $(MAKE) --silent -C ./lib/mbedtls clean
	@CFLAGS="$(TLS_INCL)" $(MAKE) --silent -C ./lib/mbedtls lib

all:
	@$(MAKE) --silent $(TLS_LIBS)
	@$(MAKE) --silent $(BIN_R)
	@$(MAKE) --silent $(BIN_D)

check:
	@$(MAKE) --silent $(OBJS_T)

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

install: $(BIN_R)
	@echo installing executable to $(BIN_DIR)


@@ 105,10 116,10 @@ uninstall:
	rm -f $(BIN_DIR)/rirc
	rm -f $(MAN_DIR)/rirc.1

test: $(DIR_B) $(OBJS_G) $(OBJS_T)
-include $(OBJS_R:.o=.o.d)
-include $(OBJS_D:.o=.o.d)
-include $(OBJS_T:.t=.t.d)

-include $(OBJS_R:.o=.d)
-include $(OBJS_D:.o=.d)
-include $(OBJS_T:.t=.d)
.PHONY: all check clean install uninstall

.PHONY: clean install uninstall test
.PRECIOUS: $(OBJS_T)

M README.md => README.md +2 -3
@@ 93,7 93,7 @@ Commands:
```
  :clear
  :close
  :connect [host [port] [pass] [user] [real]]
  :connect
  :disconnect
  :quit
```


@@ 105,7 105,6 @@ Keys:
  ^P : go to previous channel
  ^L : clear channel
  ^X : close channel
  ^F : find channel
  ^C : cancel input/action
  ^U : scroll buffer up
  ^D : scroll buffer down


@@ 117,4 116,4 @@ Keys:

### More info:

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

M config.def.h => config.def.h +11 -4
@@ 16,12 16,15 @@
#define DEFAULT_USERNAME ""
#define DEFAULT_REALNAME ""

/* User count in channel before filtering JOIN/PART/QUIT messages
/* User count in channel before filtering message types
 *   Integer
 *   (0: no filtering) */
#define JOIN_THRESHOLD 0
#define PART_THRESHOLD 0
#define QUIT_THRESHOLD 0
#define FILTER_THRESHOLD_JOIN    0
#define FILTER_THRESHOLD_PART    0
#define FILTER_THRESHOLD_QUIT    0
#define FILTER_THRESHOLD_ACCOUNT 0
#define FILTER_THRESHOLD_AWAY    0
#define FILTER_THRESHOLD_CHGHOST 0

/* Message sent for PART and QUIT by default */
#define DEFAULT_QUIT_MESG "rirc v" VERSION


@@ 61,6 64,10 @@
#define INPUT_PREFIX_FG 239
#define INPUT_PREFIX_BG -1

/* Action message */
#define ACTION_FG -1
#define ACTION_BG 239

/* Input line text colours */
#define INPUT_FG 250
#define INPUT_BG -1

M lib/mbedtls => lib/mbedtls +1 -1
@@ 1,1 1,1 @@
Subproject commit 523f0554b6cdc7ace5d360885c3f5bbcc73ec0e8
Subproject commit 1c54b5410fd48d6bcada97e30cac417c5c7eea67

M rirc.1 => rirc.1 +1 -2
@@ 68,7 68,6 @@ rirc is controlled by a combination of key bindings and commands, where:
.tab(;);
lb l .
Keys:
  ^F;find channel
  ^N;go to next channel
  ^P;go to previous channel
  ^C;cancel current input/action


@@ 94,7 93,7 @@ lb l .
Commands:
  :clear;
  :close;
  :connect;[host [port] [pass] [user] [real]]
  :connect;
  :disconnect;
  :quit;
.TE

M scripts/compile_commands.sh => scripts/compile_commands.sh +2 -2
@@ 2,9 2,9 @@

set -e

rm -f compile_commands.json

export CC=clang
export CC_EXT="-Wno-empty-translation-unit"

rm -f compile_commands.json

bear make clean rirc.debug

M scripts/coverage.sh => scripts/coverage.sh +32 -23
@@ 5,34 5,46 @@ set -e
CDIR="coverage"

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

make -e clean test
export MAKEFLAGS="-e -j $(nproc)"

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

find . -name "*.gcno" -print0 | xargs -0 -I % mv % $CDIR
find . -name "*.gcda" -print0 | xargs -0 -I % mv % $CDIR
make clean
make check

FILTER=$(cat <<'EOF'
GCNO=$(find bld -name '*.t.gcno')

FILTER=$(cat << 'EOF'
{
	use Cwd;
	@results;
	@result_ds;
	@result_fs;
	if (eof()) {
		$cov = ($lc / $lt) * 100.0;
		printf("~\n");
		printf("~ total %21d/%d %7.2f%%\n", $lc, $lt, $cov);
		print "- Coverage:";
		print "- ", "=" x 40;
		print "- $_", for sort(@result_fs);
		print "- $_", for sort(@result_ds);
		print "- ", "=" x 40;
		printf("- Total %20d/%d  %6.2f%%\n", $lc, $lt, (($lc / $lt) * 100.0));
	} elsif ($p) {
		chomp $file;
		chomp $_;
		$file =~ s/'//g;
		my @s1 = split / /, $file;
		my @s2 = split /:/, $_;
		my @s3 = split / /, $s2[1];
		chop($s3[0]);
		printf("%-30s%4s %7s%%\n", $s1[1], $s3[2], $s3[0]);
		$lt = $lt + $s3[2];
		$lc = $lc + $s3[2] * ($s3[0] / 100.0);
		$file = substr($file, (length(getcwd()) + 6));
		@s1 = split /:/, $_;
		@s2 = split / /, $s1[1];
		$lt = $lt + $s2[2];
		$lc = $lc + $s2[2] * ($s2[0] / 100.0);
		$result = sprintf("%-25s  %4s  %7s", $file, $s2[2], $s2[0]);
		if ($file =~ m|src/.*/.*|) {
			push @result_fs, $result;
		} else {
			push @result_ds, $result;
		}
		$p = 0;
	}
	$p++ if /^File.*src.*c'/;


@@ 41,13 53,10 @@ FILTER=$(cat <<'EOF'
EOF
)

echo "~ Coverage:"

gcov -pr $CDIR/*.gcno | perl -ne "$FILTER" | sort
gcov --preserve-paths $GCNO | perl -lne "$FILTER"

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

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

M scripts/pre-commit.sh => scripts/pre-commit.sh +1 -1
@@ 5,7 5,7 @@

echo "Running pre-commit hook..."

RESULTS=$(make test)
RESULTS=$(make check)

if [[ "$RESULTS" == *"failure"* ]];
then

R scripts/coverity_get.sh => scripts/sa_coverity_get.sh +2 -4
@@ 17,11 17,9 @@ COVERITY_TGZ="$1/coverity_tool.tgz"

mkdir "$1"

echo "curl https://scan.coverity.com/download/linux64 ..."

curl -fs --show-error https://scan.coverity.com/download/linux64 -o "$COVERITY_TGZ" --data "token=$COVERITY_TOKEN&project=rcr%2Frirc"
curl -fs --show-error https://scan.coverity.com/download/linux64 -o "$COVERITY_MD5" --data "token=$COVERITY_TOKEN&project=rcr%2Frirc&md5=1"
curl -fs --show-error https://scan.coverity.com/download/linux64 -o "$COVERITY_TGZ" --data "token=$COVERITY_TOKEN&project=rcr%2Frirc"

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

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

R scripts/coverity_run.sh => scripts/sa_coverity_run.sh +1 -1
@@ 21,7 21,7 @@ COVERITY_TAR="cov-int.tgz"

VERSION=$(git rev-parse --short HEAD)

PATH=$(pwd)/$1/bin:$PATH cov-build --dir "$COVERITY_OUT" make clean rirc rirc.debug test
PATH=$(pwd)/$1/bin:$PATH cov-build --dir "$COVERITY_OUT" make clean all check

tar czf "$COVERITY_TAR" "$COVERITY_OUT"


A scripts/sa_sonarcloud_get.sh => scripts/sa_sonarcloud_get.sh +30 -0
@@ 0,0 1,30 @@
#!/bin/bash

set -e

fail() { >&2 printf "%s\n" "$*"; exit 1; }

if [[ -z $1 ]]; then
	fail "Usage: '$0 dir'"
fi

SONAR_VER="4.5.0.2216"

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

BUILD_ZIP_URL="https://sonarcloud.io/static/cpp/build-wrapper-linux-x86.zip"
SONAR_ZIP_URL="https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-$SONAR_VER-linux.zip"
SONAR_MD5_URL="https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-$SONAR_VER-linux.zip.md5"

mkdir "$1"

curl -fs --show-error "$BUILD_ZIP_URL" -o "$BUILD_ZIP"
curl -fs --show-error "$SONAR_ZIP_URL" -o "$SONAR_ZIP"
curl -fs --show-error "$SONAR_MD5_URL" -o "$SONAR_MD5"

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

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

A scripts/sa_sonarcloud_run.sh => scripts/sa_sonarcloud_run.sh +66 -0
@@ 0,0 1,66 @@
#!/bin/bash

# FIXME: undo the long live branch pattern in 
# https://sonarcloud.io/project/branches?id=rirc

set -e

fail() { >&2 printf "%s\n" "$*"; exit 1; }

if [[ -z $1 ]]; then
	fail "Usage: '$0 dir'"
fi

SONAR_VER="4.5.0.2216"

BUILD_WRAPPER_BIN="$1/build-wrapper-linux-x86/build-wrapper-linux-x86-64"
SONAR_SCANNER_BIN="$1/sonar-scanner-$SONAR_VER-linux/bin/sonar-scanner"

BUILD_WRAPPER_OUT="$1/bw-out"

SONAR_SCANNER_CONF="sonar-project.properties"

if [[ ! -f "$BUILD_WRAPPER_BIN" ]]; then
	fail "missing build-wrapper binary"
fi

if [[ ! -f "$SONAR_SCANNER_BIN" ]]; then
	fail "missing sonar-scanner binary"
fi

# FIXME:
# "the branch or pull request parameter is missing"

# FIXME: just forget about coverage until it works properly with llvm or something
#        ive wasted enough time on this...
#        add test coverage to README or something? or use something else like coveralls?

cat << EOF >> "$SONAR_SCANNER_CONF"
# Server
sonar.host.url = https://sonarcloud.io

# Project
sonar.organization   = rirc
sonar.projectKey     = rirc
sonar.projectName    = rirc
sonar.projectVersion = $(git rev-parse --short HEAD)
sonar.branch.name    = $(git rev-parse --symbolic-full-name HEAD)
sonar.links.homepage = https://rcr.io/rirc/
sonar.links.scm      = https://git.sr.ht/~rcr/rirc/
sonar.links.ci       = https://builds.sr.ht/~rcr/rirc/

# Source
sonar.sources = src,test

# C
sonar.cfamily.build-wrapper-output = $BUILD_WRAPPER_OUT
sonar.cfamily.cache.enabled        = false
sonar.cfamily.threads              = $(nproc)
EOF

make clean

eval "$BUILD_WRAPPER_BIN --out-dir $BUILD_WRAPPER_OUT make all check"
eval "$SONAR_SCANNER_BIN"

rm -f "$SONAR_SCANNER_CONF"

M scripts/sanitizers_build.sh => scripts/sanitizers_build.sh +1 -1
@@ 11,7 11,7 @@ make -e clean rirc.debug

mv rirc.debug rirc.debug.address

export CC_EXT="-fsanitize=address,undefined -fno-omit-frame-pointer"
export CC_EXT="-fsanitize=thread,undefined -fno-omit-frame-pointer"
export LD_EXT="-fsanitize=thread,undefined"

make -e clean rirc.debug

M scripts/sanitizers_test.sh => scripts/sanitizers_test.sh +2 -3
@@ 3,11 3,10 @@
set -e

export CC=clang

export CC_EXT="-fsanitize=address,undefined -fno-omit-frame-pointer"
export LD_EXT="-fsanitize=address,undefined"
export LD_EXT="-fsanitize=address,undefined -fuse-ld=lld"

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

make -e test
make -e clean check

M src/components/buffer.c => src/components/buffer.c +4 -1
@@ 1,6 1,9 @@
#include "src/components/buffer.h"

#include <string.h>

#include "src/components/buffer.h"
#include "config.h"
#include "src/utils/utils.h"

#define BUFFER_MASK(X) ((X) & (BUFFER_LINES_MAX - 1))


M src/components/buffer.h => src/components/buffer.h +2 -5
@@ 1,11 1,8 @@
#ifndef BUFFER_H
#define BUFFER_H
#ifndef RIRC_COMPONENTS_BUFFER_H
#define RIRC_COMPONENTS_BUFFER_H

#include <time.h>

#include "src/utils/utils.h"
#include "config.h"

#define TEXT_LENGTH_MAX 510 /* FIXME: remove max lengths in favour of growable buffer */
#define FROM_LENGTH_MAX 100


M src/components/channel.c => src/components/channel.c +6 -1
@@ 1,8 1,9 @@
#include "src/components/channel.h"

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

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

struct channel*


@@ 52,6 53,8 @@ channel_list_free(struct channel_list *cl)
void
channel_list_add(struct channel_list *cl, struct channel *c)
{
	cl->count++;

	if (cl->head == NULL) {
		cl->head = c->next = c;
		cl->tail = c->prev = c;


@@ 67,6 70,8 @@ channel_list_add(struct channel_list *cl, struct channel *c)
void
channel_list_del(struct channel_list *cl, struct channel *c)
{
	cl->count--;

	if (cl->head == c && cl->tail == c) {
		cl->head = NULL;
		cl->tail = NULL;

M src/components/channel.h => src/components/channel.h +4 -3
@@ 1,5 1,5 @@
#ifndef CHANNEL_H
#define CHANNEL_H
#ifndef RIRC_COMPONENTS_CHANNEL_H
#define RIRC_COMPONENTS_CHANNEL_H

#include "src/components/buffer.h"
#include "src/components/input.h"


@@ 19,7 19,7 @@ enum activity_t
enum channel_t
{
	CHANNEL_T_INVALID,
	CHANNEL_T_OTHER,   /* Default/all other buffers */
	CHANNEL_T_RIRC,    /* Default buffer */
	CHANNEL_T_CHANNEL, /* Channel message buffer */
	CHANNEL_T_SERVER,  /* Server message buffer */
	CHANNEL_T_PRIVATE, /* Private message buffer */


@@ 49,6 49,7 @@ struct channel_list
{
	struct channel *head;
	struct channel *tail;
	unsigned count;
};

struct channel* channel(const char*, enum channel_t);

M src/components/input.c => src/components/input.c +2 -2
@@ 1,8 1,8 @@
#include "src/components/input.h"

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

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

#define INPUT_MASK(X) ((X) & (INPUT_HIST_MAX - 1))

M src/components/input.h => src/components/input.h +3 -2
@@ 1,5 1,5 @@
#ifndef INPUT_H
#define INPUT_H
#ifndef RIRC_COMPONENTS_INPUT_H
#define RIRC_COMPONENTS_INPUT_H

/* Buffer input
 *


@@ 13,6 13,7 @@
 * copied into the working area when scrolling
 */

#include <stddef.h>
#include <stdint.h>

/* 410 max characters for input should be sufficient given

M src/components/ircv3.h => src/components/ircv3.h +9 -3
@@ 1,13 1,19 @@
#ifndef IRCV3_CAP_H
#define IRCV3_CAP_H
#ifndef RIRC_COMPONENTS_IRCV3_CAP_H
#define RIRC_COMPONENTS_IRCV3_CAP_H

#define IRCV3_CAP_AUTO   (1 << 0)
#define IRCV3_CAP_NO_DEL (1 << 1)
#define IRCV3_CAP_NO_REQ (1 << 2)

#define IRCV3_CAP_VERSION "302"

#define IRCV3_CAPS_DEF \
	X("multi-prefix", multi_prefix, IRCV3_CAP_AUTO)
	X("account-notify", account_notify, IRCV3_CAP_AUTO) \
	X("away-notify",    away_notify,    IRCV3_CAP_AUTO) \
	X("chghost",        chghost,        IRCV3_CAP_AUTO) \
	X("extended-join",  extended_join,  IRCV3_CAP_AUTO) \
	X("invite-notify",  invite_notify,  IRCV3_CAP_AUTO) \
	X("multi-prefix",   multi_prefix,   IRCV3_CAP_AUTO)

/* Extended by testcases */
#ifndef IRCV3_CAPS_TEST

M src/components/mode.c => src/components/mode.c +7 -12
@@ 1,9 1,8 @@
/* TODO: safe channels ('!' prefix) (see RFC2811) */
#include "src/components/mode.h"

#include <ctype.h>
#include <string.h>

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

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


@@ 33,6 32,7 @@ static enum mode_err_t mode_cfg_modes(struct mode_cfg*, const char*);
/* TODO: static inline void mode_bit_set(struct mode*, uint32_t); */
/* TODO: static inline void mode_bit_isset(struct mode*, uint32_t); */
/* TODO: aggregate errors with logging callback */
/* TODO: safe channels ('!' prefix) (see RFC2811) */

static inline int
mode_isset(const struct mode *m, int flag)


@@ 380,15 380,14 @@ mode_prfxmode_prefix(struct mode *m, const struct mode_cfg *cfg, int flag)
	const char *f = cfg->PREFIX.F,
	           *t = cfg->PREFIX.T;

	while (*t != flag) {

		if (*t == 0)
			return MODE_ERR_INVALID_PREFIX;

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

	if (*t == 0)
		return MODE_ERR_INVALID_PREFIX;

	bit = flag_bit(*f);

	if (MODE_ISLOWER(*f))


@@ 399,11 398,7 @@ mode_prfxmode_prefix(struct mode *m, const struct mode_cfg *cfg, int flag)
	f = cfg->PREFIX.F,
	t = cfg->PREFIX.T;

	while (*f) {

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

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

M src/components/mode.h => src/components/mode.h +2 -2
@@ 1,5 1,5 @@
#ifndef MODE_H
#define MODE_H
#ifndef RIRC_COMPONENTS_MODE_H
#define RIRC_COMPONENTS_MODE_H

/* usermodes, chanmodes and prfxmode configuration
 *

M src/components/server.c => src/components/server.c +9 -10
@@ 1,9 1,10 @@
#include "src/components/server.h"

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

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



@@ 75,9 76,7 @@ server_list_get(struct server_list *sl, const char *host, const char *port)
struct server*
server_list_add(struct server_list *sl, struct server *s)
{
	struct server *tmp;

	if ((tmp = server_list_get(sl, s->host, s->port)) != NULL)
	if (server_list_get(sl, s->host, s->port) != NULL)
		return s;

	if (sl->head == NULL) {


@@ 166,15 165,15 @@ server_set_004(struct server *s, char *str)
{
	/* <server_name> <version> <user_modes> <chan_modes> */

	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 */
	const char *user_modes;
	const char *chan_modes;

	if (!(server_name = strsep(&str)))
	/* Not used */
	if (!strsep(&str))
		server_error(s, "invalid numeric 004: server_name is null");

	if (!(version = strsep(&str)))
	/* Not used */
	if (!strsep(&str))
		server_error(s, "invalid numeric 004: version is null");

	if (!(user_modes = strsep(&str)))

M src/components/server.h => src/components/server.h +4 -3
@@ 1,5 1,5 @@
#ifndef SERVER_H
#define SERVER_H
#ifndef RIRC_COMPONENTS_SERVER_H
#define RIRC_COMPONENTS_SERVER_H

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


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

M src/components/user.c => src/components/user.c +13 -13
@@ 1,10 1,9 @@
#include "src/components/user.h"

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

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

static struct user* user(const char*, struct mode);
static inline int user_cmp(struct user*, struct user*, void *arg);
static inline int user_ncmp(struct user*, struct user*, void *arg, size_t);


@@ 39,8 38,8 @@ user(const char *nick, struct mode prfxmodes)
	if ((u = calloc(1, sizeof(*u) + len + 1)) == NULL)
		fatal("calloc: %s", strerror(errno));

	u->nick_len = len;
	u->nick = memcpy(u->_, nick, len + 1);
	u->nick_len = len;
	u->prfxmodes = prfxmodes;

	return u;


@@ 81,20 80,24 @@ user_list_del(struct user_list *ul, enum casemapping_t cm, const char *nick)
enum user_err
user_list_rpl(struct user_list *ul, enum casemapping_t cm, const char *nick_old, const char *nick_new)
{
	/* Replace a user in a list by name, maintaining modes */
	/* Replace a user by name, maintaining modes */

	struct user *old, *new;

	if ((old = user_list_get(ul, cm, nick_old, 0)) == NULL)
	old = user_list_get(ul, cm, nick_old, 0);
	new = user_list_get(ul, cm, nick_new, 0);

	if (old == NULL)
		return USER_ERR_NOT_FOUND;

	if ((new = user_list_get(ul, cm, nick_new, 0)) != NULL)
	/* allow nick to change case  */
	if (new != NULL && irc_strcmp(cm, old->nick, new->nick))
		return USER_ERR_DUPLICATE;

	new = user(nick_new, old->prfxmodes);

	AVL_ADD(user_list, ul, new, &cm);
	AVL_DEL(user_list, ul, old, &cm);
	AVL_ADD(user_list, ul, new, &cm);

	user_free(old);



@@ 104,12 107,9 @@ user_list_rpl(struct user_list *ul, enum casemapping_t cm, const char *nick_old,
struct user*
user_list_get(struct user_list *ul, enum casemapping_t cm, const char *nick, size_t prefix_len)
{
	struct user u2 = { .nick = nick };
	struct user u = { .nick = nick };

	if (prefix_len == 0)
		return AVL_GET(user_list, ul, &u2, &cm);
	else
		return AVL_NGET(user_list, ul, &u2, &cm, prefix_len);
	return AVL_GET(user_list, ul, &u, &cm, prefix_len);
}

void

M src/components/user.h => src/components/user.h +2 -2
@@ 1,5 1,5 @@
#ifndef NICKLIST_H
#define NICKLIST_H
#ifndef RIRC_COMPONENTS_USER_H
#define RIRC_COMPONENTS_USER_H

#include "src/components/mode.h"
#include "src/utils/tree.h"

M src/draw.c => src/draw.c +35 -44
@@ 1,8 1,4 @@
/* draw.c
 *
 * Draw the elements in state.c to the terminal.
 *
 * Assumes vt-100 compatible escape codes, as such YMMV */
#include "src/draw.h"

#include <alloca.h>
#include <stdarg.h>


@@ 13,7 9,6 @@
#include "config.h"
#include "src/components/channel.h"
#include "src/components/input.h"
#include "src/draw.h"
#include "src/io.h"
#include "src/state.h"
#include "src/utils/utils.h"


@@ 127,25 122,15 @@ draw(enum draw_bit bit)
		case DRAW_ALL:
			draw_state.bits.all = -1;
			break;
		case DRAW_CLEAR:
			printf(RESET_ATTRIBUTES);
			printf(CLEAR_FULL);
			break;
		default:
			fatal("unknown draw bit");
	}
}

void
draw_init(void)
{
	draw(DRAW_ALL);
	draw(DRAW_FLUSH);
}

void
draw_term(void)
{
	printf(RESET_ATTRIBUTES);
	printf(CLEAR_FULL);
}

static void
draw_bits(void)
{


@@ 158,7 143,7 @@ draw_bits(void)
	struct coords coords;
	struct channel *c = current_channel();

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


@@ 167,26 152,32 @@ draw_bits(void)
	printf(CURSOR_SAVE);

	if (draw_state.bits.buffer) {
		printf(RESET_ATTRIBUTES);
		coords.c0 = 1;
		coords.cN = io_tty_cols();
		coords.cN = state_cols();
		coords.r0 = 3;
		coords.rN = io_tty_rows() - 2;
		coords.rN = state_rows() - 2;
		draw_buffer(&c->buffer, coords);
	}

	if (draw_state.bits.input) {
		printf(RESET_ATTRIBUTES);
		coords.c0 = 1;
		coords.cN = io_tty_cols();
		coords.r0 = io_tty_rows();
		coords.rN = io_tty_rows();
		coords.cN = state_cols();
		coords.r0 = state_rows();
		coords.rN = state_rows();
		draw_input(&c->input, coords);
	}

	if (draw_state.bits.nav)
	if (draw_state.bits.nav) {
		printf(RESET_ATTRIBUTES);
		draw_nav(c);
	}

	if (draw_state.bits.status)
	if (draw_state.bits.status) {
		printf(RESET_ATTRIBUTES);
		draw_status(c);
	}

	printf(RESET_ATTRIBUTES);
	printf(CURSOR_RESTORE);


@@ 452,7 443,6 @@ draw_input(struct input *inp, struct coords coords)
	unsigned cols_t = coords.cN - coords.c0 + 1,
	         cursor = coords.c0;

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



@@ 479,21 469,24 @@ draw_input(struct input *inp, struct coords coords)
			goto print_input;
	}

	if (!draw_fmt(&input_ptr, &buff_n, &text_n, 0,
			"%s", draw_colour(INPUT_FG, INPUT_BG)))
		goto print_input;
	if (action_message()) {

	if (action_message) {
		if (!draw_fmt(&input_ptr, &buff_n, &text_n, 0,
				"%s", draw_colour(ACTION_FG, ACTION_BG)))
			goto print_input;

		cursor = coords.cN;

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

		cursor = cols_t - text_n + 1;

	} else {
		if (!draw_fmt(&input_ptr, &buff_n, &text_n, 0,
				"%s", draw_colour(INPUT_FG, INPUT_BG)))
			goto print_input;

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



@@ 541,7 534,7 @@ draw_nav(struct channel *c)
	size_t len, total_len = 0;

	/* Bump the channel frames, if applicable */
	if ((total_len = (c->name_len + 2)) >= io_tty_cols())
	if ((total_len = (c->name_len + 2)) >= state_cols())
		return;
	else if (c == frame_prev && frame_prev != c_first)
		frame_prev = channel_get_prev(frame_prev);


@@ 560,7 553,7 @@ draw_nav(struct channel *c)
			tmp = channel_get_next(tmp_next);
			len = tmp->name_len;

			while ((total_len += (len + 2)) < io_tty_cols() && tmp != c_first) {
			while ((total_len += (len + 2)) < state_cols() && tmp != c_first) {

				tmp_next = tmp;



@@ 578,7 571,7 @@ draw_nav(struct channel *c)
			tmp = channel_get_prev(tmp_prev);
			len = tmp->name_len;

			while ((total_len += (len + 2)) < io_tty_cols() && tmp != c_last) {
			while ((total_len += (len + 2)) < state_cols() && tmp != c_last) {

				tmp_prev = tmp;



@@ 593,7 586,7 @@ draw_nav(struct channel *c)
		len = tmp->name_len;

		/* Next channel doesn't fit */
		if ((total_len += (len + 2)) >= io_tty_cols())
		if ((total_len += (len + 2)) >= state_cols())
			break;

		if (nextward)


@@ 638,15 631,13 @@ draw_status(struct channel *c)
	float sb;
	int ret;
	unsigned col = 0;
	unsigned cols = io_tty_cols();
	unsigned rows = io_tty_rows();
	unsigned cols = state_cols();
	unsigned rows = state_rows();

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

	printf(RESET_ATTRIBUTES);

	printf(MOVE(2, 1));
	printf("%.*s", cols, (char *)(memset(alloca(cols), *HORIZONTAL_SEPARATOR, cols)));



@@ 776,7 767,7 @@ draw_colour(int fg, int bg)
	}

	if (bg >= 0 && bg <= 255) {
		if ((ret = snprintf(buf + len, sizeof(buf) - len, ESC"[48;5;%dm", bg)) < 0)
		if ((snprintf(buf + len, sizeof(buf) - len, ESC"[48;5;%dm", bg)) < 0)
			buf[len] = 0;
	}


M src/draw.h => src/draw.h +3 -4
@@ 1,5 1,5 @@
#ifndef DRAW_H
#define DRAW_H
#ifndef RIRC_DRAW_H
#define RIRC_DRAW_H

enum draw_bit
{


@@ 11,10 11,9 @@ enum draw_bit
	DRAW_NAV,    /* set bit to draw nav */
	DRAW_STATUS, /* set bit to draw status */
	DRAW_ALL,    /* set all draw bits aside from bell */
	DRAW_CLEAR,  /* clear the terminal */
};

void draw(enum draw_bit);
void draw_init(void);
void draw_term(void);

#endif

M src/handlers/irc_ctcp.c => src/handlers/irc_ctcp.c +15 -5
@@ 1,12 1,12 @@
#include "src/handlers/irc_ctcp.h"

#include <ctype.h>
#include <errno.h>
#include <string.h>
#include <sys/time.h>

#include "src/components/channel.h"
#include "src/components/server.h"
#include "src/handlers/irc_ctcp.gperf.out"
#include "src/handlers/irc_ctcp.h"
#include "src/io.h"
#include "src/state.h"
#include "src/utils/utils.h"


@@ 22,11 22,21 @@
	         failf((S), "Send fail: %s", io_err(ret)); \
	} while (0)

#define CTCP_CLIENTINFO \
	"ACTION "     \
	"CLIENTINFO " \
	"FINGER "     \
	"PING "       \
	"SOURCE "     \
	"TIME "       \
	"USERINFO "   \
	"VERSION"

static int
parse_ctcp(struct server *s, const char *from, char **args, const char **cmd)
{
	char *message = *args;
	char *command;
	char *message = *args;
	char *p;

	if (!from)


@@ 138,7 148,7 @@ ctcp_request_clientinfo(struct server *s, const char *from, const char *targ, ch
	else
		server_info(s, "CTCP CLIENTINFO from %s", from);

	sendf(s, "NOTICE %s :\001CLIENTINFO ACTION CLIENTINFO PING SOURCE TIME VERSION\001", from);
	sendf(s, "NOTICE %s :\001CLIENTINFO " CTCP_CLIENTINFO "\001", from);

	return 0;
}


@@ 211,7 221,7 @@ ctcp_request_source(struct server *s, const char *from, const char *targ, char *
	else
		server_info(s, "CTCP SOURCE from %s", from);

	sendf(s, "NOTICE %s :\001SOURCE rcr.io/rirc\001", from);
	sendf(s, "NOTICE %s :\001SOURCE https://rcr.io/rirc\001", from);

	return 0;
}

M src/handlers/irc_ctcp.h => src/handlers/irc_ctcp.h +4 -2
@@ 1,5 1,7 @@
#ifndef IRC_CTCP_H
#define IRC_CTCP_H
#ifndef RIRC_HANDLERS_IRC_CTCP_H
#define RIRC_HANDLERS_IRC_CTCP_H

#include "src/components/server.h"

/* Summary of CTCP implementation:
 *

M src/handlers/irc_recv.c => src/handlers/irc_recv.c +214 -113
@@ 1,3 1,5 @@
#include "src/handlers/irc_recv.h"

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


@@ 5,24 7,13 @@
#include "src/components/server.h"
#include "src/handlers/irc_ctcp.h"
#include "src/handlers/irc_recv.gperf.out"
#include "src/handlers/irc_recv.h"
#include "src/handlers/ircv3.h"
#include "src/draw.h"
#include "src/io.h"
#include "src/state.h"
#include "src/utils/utils.h"

#ifndef JOIN_THRESHOLD
#define JOIN_THRESHOLD 0
#endif

#ifndef PART_THRESHOLD
#define PART_THRESHOLD 0
#endif

#ifndef QUIT_THRESHOLD
#define QUIT_THRESHOLD 0
#endif
#include "config.h"

#define failf(S, ...) \
	do { server_error((S), __VA_ARGS__); \


@@ 60,9 51,12 @@ static int irc_recv_numeric(struct server*, struct irc_message*);
static int recv_mode_chanmodes(struct irc_message*, const struct mode_cfg*, struct server*, struct channel*);
static int recv_mode_usermodes(struct irc_message*, const struct mode_cfg*, struct server*);

static const unsigned quit_threshold = QUIT_THRESHOLD;
static const unsigned join_threshold = JOIN_THRESHOLD;
static const unsigned part_threshold = PART_THRESHOLD;
static unsigned quit_threshold    = FILTER_THRESHOLD_QUIT;
static unsigned join_threshold    = FILTER_THRESHOLD_JOIN;
static unsigned part_threshold    = FILTER_THRESHOLD_PART;
static unsigned account_threshold = FILTER_THRESHOLD_ACCOUNT;
static unsigned away_threshold    = FILTER_THRESHOLD_AWAY;
static unsigned chghost_threshold = FILTER_THRESHOLD_CHGHOST;

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


@@ 212,6 206,7 @@ static const irc_recv_f irc_numerics[] = {
	[704] = irc_info,   /* RPL_HELPSTART */
	[705] = irc_info,   /* RPL_HELP */
	[706] = irc_ignore, /* RPL_ENDOFHELP */
	[1000] = NULL       /* Out of range */
};

int


@@ 419,7 414,7 @@ irc_329(struct server *s, struct irc_message *m)
static int
irc_332(struct server *s, struct irc_message *m)
{
	/* 332 <channel> <topic> */
	/* 332 <channel> :<topic> */

	char *chan;
	char *topic;


@@ 489,6 484,7 @@ irc_353(struct server *s, struct irc_message *m)
	char *chan;
	char *nick;
	char *nicks;
	char *prfx;
	char *type;
	struct channel *c;



@@ 507,29 503,18 @@ irc_353(struct server *s, struct irc_message *m)
	if (mode_chanmode_prefix(&(c->chanmodes), &(s->mode_cfg), *type) != MODE_ERR_NONE)
		failf(s, "RPL_NAMEREPLY: invalid channel flag: '%c'", *type);

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

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

				prefix = *nick++;
	while ((prfx = nick = strsep(&nicks))) {

				if (mode_prfxmode_prefix(&m, &(s->mode_cfg), prefix) != MODE_ERR_NONE)
					failf(s, "RPL_NAMEREPLY: invalid user prefix: '%c'", prefix);
		struct mode m = MODE_EMPTY;

			} while (s->ircv3_caps.multi_prefix.set);
		while (mode_prfxmode_prefix(&m, &(s->mode_cfg), *nick) == MODE_ERR_NONE)
			nick++;

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

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

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

	draw(DRAW_STATUS);


@@ 564,55 549,40 @@ irc_recv_numeric(struct server *s, struct irc_message *m)
	/* :server <code> <target> [args] */

	char *targ;
	int code = 0;
	irc_recv_f handler = NULL;

	for (const char *p = m->command; *p; p++) {

		if (!isdigit(*p))
			failf(s, "NUMERIC: invalid");

		code *= 10;
		code += *p - '0';

		if (code > 999)
			failf(s, "NUMERIC: out of range");
	unsigned code = 0;

	if ((m->command[0] && isdigit(m->command[0]))
	 && (m->command[1] && isdigit(m->command[1]))
	 && (m->command[2] && isdigit(m->command[2]))
	 && (m->command[3] == 0))
	{
		code += (m->command[0] - '0') * 100;
		code += (m->command[1] - '0') * 10;
		code += (m->command[2] - '0');
	}

	/* Message target is only used to establish s->nick when registering with a server */
	if (!(irc_message_param(m, &targ))) {
		io_dx(s->connection);
		failf(s, "NUMERIC: target is null");
	}
	if (!code)
		failf(s, "NUMERIC: '%s' invalid", m->command);

	/* Message target should match s->nick or '*' if unregistered, otherwise out of sync */
	if (strcmp(targ, s->nick) && strcmp(targ, "*") && code != 1) {
		io_dx(s->connection);
		failf(s, "NUMERIC: target mismatched, nick is '%s', received '%s'", s->nick, targ);
	}
	if (!(irc_message_param(m, &targ)))
		failf(s, "NUMERIC: target is null");

	if (ARR_ELEM(irc_numerics, code))
		handler = irc_numerics[code];
	if (strcmp(targ, s->nick) && strcmp(targ, "*"))
		failf(s, "NUMERIC: target '%s' is invalid", targ);

	if (handler)
		return (*handler)(s, m);
	if (!irc_numerics[code] && (m->params && *m->params))
		failf(s, "NUMERIC: %u unhandled: [%s]", code, m->params);

	if (m->params)
		failf(s, "Numeric type '%u' unknown: %s", code, m->params);
	else
		failf(s, "Numeric type '%u' unknown", code);
}
	if (!irc_numerics[code])
		failf(s, "NUMERIC: %u unhandled", code);

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

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

	char *message;



@@ 631,20 601,27 @@ recv_invite(struct server *s, struct irc_message *m)

	char *chan;
	char *nick;
	struct channel *c;

	if (!m->from)
		failf(s, "INVITE: sender's nick is null");

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

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

	if (!irc_message_param(m, &nick))
		failf(s, "INVITE: target nick is null");
	if (!strcmp(nick, s->nick)) {
		newlinef(s->channel, 0, FROM_INFO, "%s invited you to %s", m->from, chan);
		return 0;
	}

	if (!strcmp(nick, s->nick))
		newlinef(s->channel, 0, FROM_INFO, "You invited %s to %s", nick, chan);
	else
		newlinef(s->channel, 0, FROM_INFO, "You've been invited to %s by %s", chan, m->from);
	/* IRCv3 CAP invite-notify, sent to all users on the target channel */
	if ((c = channel_list_get(&s->clist, chan, s->casemapping)) == NULL)
		failf(s, "INVITE: channel '%s' not found", chan);

	newlinef(c, 0, FROM_INFO, "%s invited %s to %s", m->from, nick, chan);

	return 0;
}


@@ 652,7 629,8 @@ recv_invite(struct server *s, struct irc_message *m)
static int
recv_join(struct server *s, struct irc_message *m)
{
	/* :nick!user@host JOIN <channel> */
	/* :nick!user@host JOIN <channel>
	 * :nick!user@host JOIN <channel> <account> :<realname> */

	char *chan;
	struct channel *c;


@@ 661,7 639,7 @@ recv_join(struct server *s, struct irc_message *m)
		failf(s, "JOIN: sender's nick is null");

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

	if (!strcmp(m->from, s->nick)) {
		if ((c = channel_list_get(&s->clist, chan, s->casemapping)) == NULL) {


@@ 682,10 660,27 @@ recv_join(struct server *s, struct irc_message *m)
		failf(s, "JOIN: channel '%s' not found", chan);

	if (user_list_add(&(c->users), s->casemapping, m->from, MODE_EMPTY) == USER_ERR_DUPLICATE)
		failf(s, "JOIN: user '%s' alread on channel '%s'", m->from, chan);
		failf(s, "JOIN: user '%s' already on channel '%s'", m->from, chan);

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

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

			char *account;
			char *realname;

	if (!join_threshold || c->users.count <= join_threshold)
		newlinef(c, BUFFER_LINE_JOIN, FROM_JOIN, "%s!%s has joined", m->from, m->host);
			if (!irc_message_param(m, &account))
				failf(s, "JOIN: account is null");

			if (!irc_message_param(m, &realname))
				failf(s, "JOIN: realname is null");

			newlinef(c, BUFFER_LINE_JOIN, FROM_JOIN, "%s!%s has joined [%s - %s]",
				m->from, m->host, account, realname);
		} else {
			newlinef(c, BUFFER_LINE_JOIN, FROM_JOIN, "%s!%s has joined", m->from, m->host);
		}
	}

	draw(DRAW_STATUS);



@@ 695,7 690,7 @@ recv_join(struct server *s, struct irc_message *m)
static int
recv_kick(struct server *s, struct irc_message *m)
{
	/* :nick!user@host KICK <channel> <user> [message] */
	/* :nick!user@host KICK <channel> <user> [:message] */

	char *chan;
	char *message;


@@ 727,7 722,7 @@ recv_kick(struct server *s, struct irc_message *m)

		channel_part(c);

		if (message)
		if (message && *message)
			newlinef(c, 0, FROM_INFO, "Kicked by %s (%s)", m->from, message);
		else
			newlinef(c, 0, FROM_INFO, "Kicked by %s", m->from);


@@ 737,7 732,7 @@ recv_kick(struct server *s, struct irc_message *m)
		if (user_list_del(&(c->users), s->casemapping, user) == USER_ERR_NOT_FOUND)
			failf(s, "KICK: nick '%s' not found in '%s'", user, chan);

		if (message)
		if (message && *message)
			newlinef(c, 0, FROM_INFO, "%s has kicked %s (%s)", m->from, user, message);
		else
			newlinef(c, 0, FROM_INFO, "%s has kicked %s", m->from, user);


@@ 998,7 993,7 @@ recv_nick(struct server *s, struct irc_message *m)
			newlinef(c, BUFFER_LINE_NICK, FROM_NICK, "%s  >>  %s", m->from, nick);

		else if (ret == USER_ERR_DUPLICATE)
			server_error(s, "NICK: user '%s' alread on channel '%s'", m->from, c->name);
			server_error(s, "NICK: user '%s' already on channel '%s'", nick, c->name);

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



@@ 1008,7 1003,7 @@ recv_nick(struct server *s, struct irc_message *m)
static int
recv_notice(struct server *s, struct irc_message *m)
{
	/* :nick!user@host NOTICE <target> <message> */
	/* :nick!user@host NOTICE <target> :<message> */

	char *message;
	char *target;


@@ 1069,7 1064,7 @@ recv_notice(struct server *s, struct irc_message *m)
static int
recv_part(struct server *s, struct irc_message *m)
{
	/* :nick!user@host PART <channel> [message] */
	/* :nick!user@host PART <channel> [:message] */

	char *chan;
	char *message;


@@ 1079,14 1074,16 @@ recv_part(struct server *s, struct irc_message *m)
		failf(s, "PART: sender's nick is null");

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

	irc_message_param(m, &message);

	if (!strcmp(m->from, s->nick)) {

		/* If not found, assume channel was closed */
		if ((c = channel_list_get(&s->clist, chan, s->casemapping)) != NULL) {

			if (irc_message_param(m, &message))
			if (message && *message)
				newlinef(c, BUFFER_LINE_PART, FROM_PART, "you have parted (%s)", message);
			else
				newlinef(c, BUFFER_LINE_PART, FROM_PART, "you have parted");


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

		if (!part_threshold || c->users.count <= part_threshold) {
			if (irc_message_param(m, &message))
		if (!part_threshold || part_threshold > c->users.count) {

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


@@ 1132,7 1130,7 @@ recv_ping(struct server *s, struct irc_message *m)
static int
recv_pong(struct server *s, struct irc_message *m)
{
	/*  PONG <server> [<server2>] */
	/* PONG <server> [<server2>] */

	UNUSED(s);
	UNUSED(m);


@@ 1143,7 1141,7 @@ recv_pong(struct server *s, struct irc_message *m)
static int
recv_privmsg(struct server *s, struct irc_message *m)
{
	/* :nick!user@host PRIVMSG <target> <message> */
	/* :nick!user@host PRIVMSG <target> :<message> */

	char *message;
	char *target;


@@ 1200,9 1198,42 @@ recv_privmsg(struct server *s, struct irc_message *m)
}

static int
recv_quit(struct server *s, struct irc_message *m)
{
	/* :nick!user@host QUIT [:message] */

	char *message;
	struct channel *c = s->channel;

	if (!m->from)
		failf(s, "QUIT: sender's nick is null");

	irc_message_param(m, &message);

	do {
		if (user_list_del(&(c->users), s->casemapping, m->from) == USER_ERR_NONE) {

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

			if (message && *message)
				newlinef(c, BUFFER_LINE_QUIT, FROM_QUIT, "%s!%s has quit (%s)",
					m->from, m->host, message);
			else
				newlinef(c, BUFFER_LINE_QUIT, FROM_QUIT, "%s!%s has quit",
					m->from, m->host);
		}
	} while ((c = c->next) != s->channel);

	draw(DRAW_STATUS);

	return 0;
}

static int
recv_topic(struct server *s, struct irc_message *m)
{
	/* :nick!user@host TOPIC <channel> [topic] */
	/* :nick!user@host TOPIC <channel> [:topic] */

	char *chan;
	char *topic;


@@ 1212,16 1243,16 @@ recv_topic(struct server *s, struct irc_message *m)
		failf(s, "TOPIC: sender's nick is null");

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

	if (!irc_message_param(m, &topic))
		failf(s, "TOPIC: topic is null");

	if ((c = channel_list_get(&s->clist, chan, s->casemapping)) == NULL)
		failf(s, "TOPIC: target channel '%s' not found", chan);
		failf(s, "TOPIC: channel '%s' not found", chan);

	if (*topic) {
		newlinef(c, 0, FROM_INFO, "%s has changed the topic:", m->from);
		newlinef(c, 0, FROM_INFO, "%s has set the topic:", m->from);
		newlinef(c, 0, FROM_INFO, "\"%s\"", topic);
	} else {
		newlinef(c, 0, FROM_INFO, "%s has unset the topic", m->from);


@@ 1231,30 1262,100 @@ recv_topic(struct server *s, struct irc_message *m)
}

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

static int
recv_ircv3_account(struct server *s, struct irc_message *m)
{
	/* :nick!user@host QUIT [message] */
	/* :nick!user@host ACCOUNT <account> */

	char *message = NULL;
	char *account;
	struct channel *c = s->channel;

	if (!m->from)
		failf(s, "QUIT: sender's nick is null");
		failf(s, "ACCOUNT: sender's nick is null");

	if (!irc_message_param(m, &account))
		failf(s, "ACCOUNT: account is null");

	do {
		if (!user_list_get(&(c->users), s->casemapping, m->from, 0))
			continue;

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

		if (!strcmp(account, "*"))
			newlinef(c, 0, FROM_INFO, "%s has logged out", m->from);
		else
			newlinef(c, 0, FROM_INFO, "%s has logged in as %s", m->from, account);

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

	return 0;
}

static int
recv_ircv3_away(struct server *s, struct irc_message *m)
{
	/* :nick!user@host AWAY [:message] */

	char *message;
	struct channel *c = s->channel;

	if (!m->from)
		failf(s, "AWAY: sender's nick is null");

	irc_message_param(m, &message);

	do {
		if (user_list_del(&(c->users), s->casemapping, m->from) == USER_ERR_NONE) {
			if (!quit_threshold || c->users.count <= quit_threshold) {
				if (message)
					newlinef(c, BUFFER_LINE_QUIT, FROM_QUIT, "%s!%s has quit (%s)", m->from, m->host, message);
				else
					newlinef(c, BUFFER_LINE_QUIT, FROM_QUIT, "%s!%s has quit", m->from, m->host);
			}
		}
		if (!user_list_get(&(c->users), s->casemapping, m->from, 0))
			continue;

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

		if (message)
			newlinef(c, 0, FROM_INFO, "%s is now away: %s", m->from, message);
		else
			newlinef(c, 0, FROM_INFO, "%s is no longer away", m->from);

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

	draw(DRAW_STATUS);
	return 0;
}

static int
recv_ircv3_chghost(struct server *s, struct irc_message *m)
{
	/* :nick!user@host CHGHOST new_user new_host */

	char *user;
	char *host;
	struct channel *c = s->channel;

	if (!m->from)
		failf(s, "CHGHOST: sender's nick is null");

	if (!irc_message_param(m, &user))
		failf(s, "CHGHOST: user is null");

	if (!irc_message_param(m, &host))
		failf(s, "CHGHOST: host is null");

	do {
		if (!user_list_get(&(c->users), s->casemapping, m->from, 0))
			continue;

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

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

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

	return 0;
}

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

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


@@ 15,7 14,11 @@
	X(pong) \
	X(privmsg) \
	X(quit) \
	X(topic)
	X(topic) \
	X(ircv3_cap) \
	X(ircv3_account) \
	X(ircv3_away) \
	X(ircv3_chghost)

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


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


@@ 55,4 57,8 @@ PONG,    recv_pong
PRIVMSG, recv_privmsg
QUIT,    recv_quit
TOPIC,   recv_topic
CAP,     recv_ircv3_cap
ACCOUNT, recv_ircv3_account
AWAY,    recv_ircv3_away
CHGHOST, recv_ircv3_chghost
%%

M src/handlers/irc_recv.h => src/handlers/irc_recv.h +2 -2
@@ 1,5 1,5 @@
#ifndef IRC_RECV_H
#define IRC_RECV_H
#ifndef RIRC_HANDLERS_IRC_RECV_H
#define RIRC_HANDLERS_IRC_RECV_H

/* Summary of irc protocol implementation:
 *

M src/handlers/irc_send.c => src/handlers/irc_send.c +69 -41
@@ 1,13 1,12 @@
#include "src/handlers/irc_send.h"

#include <ctype.h>
#include <sys/time.h>

#include "config.h"
#include "src/components/buffer.h"
#include "src/components/channel.h"
#include "src/components/ircv3.h"
#include "src/components/server.h"
#include "src/handlers/irc_send.gperf.out"
#include "src/handlers/irc_send.h"
#include "src/io.h"
#include "src/state.h"
#include "src/utils/utils.h"


@@ 23,12 22,14 @@
	         failf((C), "Send fail: %s", io_err(ret)); \
	} while (0)

static const char* targ_or_type(struct channel*, char*, enum channel_t type);
static const char* nick_or_priv(struct channel*, char*);

int
irc_send_command(struct server *s, struct channel *c, char *m)
{
	char *command, *command_args, *p;
	char *command;
	char *command_args;
	char *p;
	const struct send_handler *send;

	if (!s)


@@ 82,20 83,31 @@ 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)
nick_or_priv(struct channel *c, char *m)
{
	const char *targ;
	const char *nick;

	if ((targ = strsep(&m)))
		return targ;
	if ((nick = strsep(&m)))
		return nick;

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

	return NULL;
}

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

	return 0;
}

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


@@ 169,12 181,28 @@ send_topic(struct server *s, struct channel *c, char *m)
}

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

	sendf(s, c, "PRIVMSG %s :\001ACTION %s\001", c->name, m);
	if (strtrim(&m))
		failf(c, "Usage: /topic-unset");

	sendf(s, c, "TOPIC %s :", c->name);

	return 0;
}

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

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

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

	return 0;
}


@@ 182,12 210,12 @@ send_ctcp_action(struct server *s, struct channel *c, char *m)
static int
send_ctcp_clientinfo(struct server *s, struct channel *c, char *m)
{
	const char *targ;
	const char *nick;

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

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

	return 0;
}


@@ 195,12 223,12 @@ send_ctcp_clientinfo(struct server *s, struct channel *c, char *m)
static int
send_ctcp_finger(struct server *s, struct channel *c, char *m)
{
	const char *targ;
	const char *nick;

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

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

	return 0;
}


@@ 208,15 236,15 @@ send_ctcp_finger(struct server *s, struct channel *c, char *m)
static int
send_ctcp_ping(struct server *s, struct channel *c, char *m)
{
	const char *targ;
	const char *nick;
	struct timeval t;

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

	(void) gettimeofday(&t, NULL);

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



@@ 226,12 254,12 @@ send_ctcp_ping(struct server *s, struct channel *c, char *m)
static int
send_ctcp_source(struct server *s, struct channel *c, char *m)
{
	const char *targ;
	const char *nick;

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

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

	return 0;
}


@@ 239,12 267,12 @@ send_ctcp_source(struct server *s, struct channel *c, char *m)
static int
send_ctcp_time(struct server *s, struct channel *c, char *m)
{
	const char *targ;
	const char *nick;

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

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

	return 0;
}


@@ 252,12 280,12 @@ send_ctcp_time(struct server *s, struct channel *c, char *m)
static int
send_ctcp_userinfo(struct server *s, struct channel *c, char *m)
{
	const char *targ;
	const char *nick;

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

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

	return 0;
}


@@ 265,12 293,12 @@ send_ctcp_userinfo(struct server *s, struct channel *c, char *m)
static int
send_ctcp_version(struct server *s, struct channel *c, char *m)
{
	const char *targ;
	const char *nick;

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

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

	return 0;
}

M src/handlers/irc_send.gperf => src/handlers/irc_send.gperf +5 -1
@@ 2,11 2,13 @@
#include <string.h>

#define SEND_HANDLERS \
	X(away) \
	X(notice) \
	X(part) \
	X(privmsg) \
	X(quit) \
	X(topic)
	X(topic) \
	X(topic_unset)

#define SEND_CTCP_HANDLERS \
	X(action) \


@@ 64,9 66,11 @@ CTCP-SOURCE,     send_ctcp_source
CTCP-TIME,       send_ctcp_time
CTCP-USERINFO,   send_ctcp_userinfo
CTCP-VERSION,    send_ctcp_version
AWAY,            send_away
NOTICE,          send_notice
PART,            send_part
PRIVMSG,         send_privmsg
QUIT,            send_quit
TOPIC,           send_topic
TOPIC-UNSET,     send_topic_unset
%%

M src/handlers/irc_send.h => src/handlers/irc_send.h +2 -2
@@ 1,5 1,5 @@
#ifndef IRC_SEND_H
#define IRC_SEND_H
#ifndef RIRC_HANDLERS_IRC_SEND_H
#define RIRC_HANDLERS_IRC_SEND_H

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

M src/handlers/ircv3.c => src/handlers/ircv3.c +2 -1
@@ 1,6 1,7 @@
#include "src/handlers/ircv3.h"

#include <string.h>

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


M src/handlers/ircv3.h => src/handlers/ircv3.h +2 -2
@@ 1,5 1,5 @@
#ifndef IRCV3_H
#define IRCV3_H
#ifndef RIRC_HANDLERS_IRCV3_H
#define RIRC_HANDLERS_IRCV3_H

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

M src/io.c => src/io.c +132 -159
@@ 3,6 3,7 @@
#include <arpa/inet.h>
#include <errno.h>
#include <netdb.h>
#include <poll.h>
#include <pthread.h>
#include <signal.h>
#include <stdarg.h>


@@ 14,8 15,8 @@
#include <unistd.h>

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

#include "mbedtls/ctr_drbg.h"
#include "mbedtls/entropy.h"


@@ 25,9 26,7 @@
#include "mbedtls/x509_crt.h"

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

#ifndef IO_PING_MIN
#define IO_PING_MIN 150


@@ 72,27 71,23 @@
			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(&io_cb_mutex);   \
		io_cb(__VA_ARGS__); \
		PT_UL(&io_cb_mutex);   \
	} while (0)

/* IO callback */
#define IO_CB(X) \
	do { PT_LK(&io_cb_mutex); (X); PT_UL(&io_cb_mutex); } while (0)

#define io_cxed(C)       IO_CB(io_cb_cxed((C)->obj))
#define io_dxed(C)       IO_CB(io_cb_dxed((C)->obj))
#define io_error(C, ...) IO_CB(io_cb_error((C)->obj,  __VA_ARGS__))
#define io_info(C, ...)  IO_CB(io_cb_info((C)->obj, __VA_ARGS__))
#define io_ping(C, P)    IO_CB(io_cb_ping((C)->obj, P))

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

#define io_cb_cxed(C)        PT_CB(IO_CB_CXED, (C)->obj)
#define io_cb_dxed(C)        PT_CB(IO_CB_DXED, (C)->obj)
#define io_cb_err(C, ...)    PT_CB(IO_CB_ERR, (C)->obj, __VA_ARGS__)
#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,


@@ 119,8 114,7 @@ struct connection
		IO_ST_PING, /* Socket connected, network state in question */
	} st_cur, /* current thread state */
	  st_new; /* new thread state */
	int soc;
	mbedtls_net_context tls_fd;
	mbedtls_net_context net_ctx;
	mbedtls_ssl_config  tls_conf;
	mbedtls_ssl_context tls_ctx;
	pthread_mutex_t mtx;


@@ 134,7 128,7 @@ static enum io_state_t io_state_cxed(struct connection*);
static enum io_state_t io_state_cxng(struct connection*);
static enum io_state_t io_state_ping(struct connection*);
static enum io_state_t io_state_rxng(struct connection*);
static int io_cx_read(struct connection*, unsigned);
static int io_cx_read(struct connection*, uint32_t);
static void io_fatal(const char*, int);
static void io_sig_handle(int);
static void io_sig_init(void);


@@ 149,10 143,7 @@ static mbedtls_entropy_context  tls_entropy;
static mbedtls_x509_crt         tls_x509_crt;
static pthread_mutex_t io_cb_mutex = PTHREAD_MUTEX_INITIALIZER;
static struct termios term;
static 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 const char* io_strerror(char*, size_t);
static int io_net_connect(struct connection*);


@@ 197,13 188,12 @@ int
io_cx(struct connection *cx)
{
	enum io_err_t err = IO_ERR_NONE;
	enum io_state_t st;
	sigset_t sigset;
	sigset_t sigset_old;

	PT_LK(&(cx->mtx));

	switch ((st = cx->st_cur)) {
	switch (cx->st_cur) {
		case IO_ST_DXED:
			PT_CF(sigfillset(&sigset));
			PT_CF(pthread_sigmask(SIG_BLOCK, &sigset, &sigset_old));


@@ 280,34 270,27 @@ io_sendf(struct connection *cx, const char *fmt, ...)
	ret = 0;
	written = 0;

	if (cx->flags & IO_TLS_ENABLED) {
		do {
			if ((ret = mbedtls_ssl_write(&(cx->tls_ctx), sendbuf + ret, len - ret)) < 0) {
				switch (ret) {
					case MBEDTLS_ERR_SSL_WANT_READ:
					case MBEDTLS_ERR_SSL_WANT_WRITE:
						ret = 0;
						continue;
					default:
						io_dx(cx);
						io_cx(cx);
						return IO_ERR_SSL_WRITE;
				}
			}
		} while ((written += ret) < len);
	} else {
		do {
			if ((ret = send(cx->soc, sendbuf + ret, len - ret, 0)) == -1) {
				switch (errno) {
					case EINTR:
						ret = 0;
						continue;
					default:
						return IO_ERR_SSL_WRITE;
				}
			}
		} while ((written += ret) < len);
	}
	do {
		if (cx->flags & IO_TLS_ENABLED) {
			ret = mbedtls_ssl_write(&(cx->tls_ctx), sendbuf + ret, len - ret);
		} else {
			ret = mbedtls_net_send(&(cx->net_ctx), sendbuf + ret, len - ret);
		}

		if (ret >= 0)
			continue;

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


@@ 331,14 314,12 @@ io_start(void)
		ssize_t ret = read(STDIN_FILENO, buf, sizeof(buf));

		if (ret > 0) {
			PT_LK(&io_cb_mutex);
			io_cb_read_inp(buf, ret);
			PT_UL(&io_cb_mutex);
			IO_CB(io_cb_read_inp(buf, ret));
		} else {
			if (errno == EINTR) {
				if (flag_sigwinch_cb) {
					flag_sigwinch_cb = 0;
					io_cb_signal(IO_SIGWINCH);
					io_tty_winsize();
				}
			} else {
				fatal("read: %s", ret ? strerror(errno) : "EOF");


@@ 356,31 337,12 @@ io_stop(void)
static void
io_tty_winsize(void)
{
	static struct winsize tty_ws;

	if (flag_tty_resized == 0) {
		flag_tty_resized = 1;
	struct winsize tty_ws;

		if (ioctl(0, TIOCGWINSZ, &tty_ws) < 0)
			fatal("ioctl: %s", strerror(errno));
	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;
	IO_CB(io_cb_sigwinch(tty_ws.ws_col, tty_ws.ws_row));
}

const char*


@@ 412,7 374,7 @@ io_state_rxng(struct connection *cx)
		);
	}

	io_cb_info(cx, "Attemping reconnect in %02u:%02u",
	io_info(cx, "Attemping reconnect in %02u:%02u",
		(cx->rx_sleep / 60),
		(cx->rx_sleep % 60));



@@ 424,7 386,7 @@ io_state_rxng(struct connection *cx)
static enum io_state_t
io_state_cxng(struct connection *cx)
{
	if ((cx->soc = io_net_connect(cx)) < 0)
	if ((io_net_connect(cx)) < 0)
		return IO_ST_RXNG;

	if ((cx->flags & IO_TLS_ENABLED) && io_tls_establish(cx) < 0)


@@ 438,7 400,7 @@ io_state_cxed(struct connection *cx)
{
	int ret;

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

	if (ret == MBEDTLS_ERR_SSL_TIMEOUT)


@@ 446,23 408,25 @@ io_state_cxed(struct connection *cx)

	switch (ret) {
		case MBEDTLS_ERR_SSL_WANT_READ:
		case MBEDTLS_ERR_SSL_WANT_WRITE:
			break;
		case MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY:
			io_cb_info(cx, "connection closed gracefully");
			io_info(cx, "connection closed gracefully");
			break;
		case MBEDTLS_ERR_NET_CONN_RESET:
		case 0:
			io_cb_err(cx, "connection reset by peer");
			io_error(cx, "connection reset by peer");
			break;
		default:
			io_cb_err(cx, "connection tls error");
			io_error(cx, "connection error");
			break;
	}

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

	if (cx->flags & IO_TLS_ENABLED) {
		mbedtls_ssl_config_free(&(cx->tls_conf));
		mbedtls_ssl_free(&(cx->tls_ctx));
	}

	return IO_ST_CXNG;
}


@@ 475,7 439,7 @@ io_state_ping(struct connection *cx)
	if (cx->ping >= IO_PING_MAX)
		return IO_ST_CXNG;

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

	if (ret == MBEDTLS_ERR_SSL_TIMEOUT)


@@ 483,23 447,25 @@ io_state_ping(struct connection *cx)

	switch (ret) {
		case MBEDTLS_ERR_SSL_WANT_READ:
		case MBEDTLS_ERR_SSL_WANT_WRITE:
			break;
		case MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY:
			io_cb_info(cx, "connection closed gracefully");
			io_info(cx, "connection closed gracefully");
			break;
		case MBEDTLS_ERR_NET_CONN_RESET:
		case 0:
			io_cb_err(cx, "connection reset by peer");
			io_error(cx, "connection reset by peer");
			break;
		default:
			io_cb_err(cx, "connection ssl error");
			io_error(cx, "connection error");
			break;
	}

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

	if (cx->flags & IO_TLS_ENABLED) {
		mbedtls_ssl_config_free(&(cx->tls_conf));
		mbedtls_ssl_free(&(cx->tls_ctx));
	}

	return IO_ST_CXNG;
}


@@ 519,6 485,8 @@ io_thread(void *arg)

	cx->st_cur = IO_ST_CXNG;

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

	do {
		enum io_state_t st_cur;
		enum io_state_t st_new;


@@ 547,43 515,40 @@ io_thread(void *arg)
		switch (ST_X(st_cur, st_new)) {
			case ST_X(IO_ST_DXED, IO_ST_CXNG): /* A1 */
			case ST_X(IO_ST_RXNG, IO_ST_CXNG): /* A2,C */
				io_cb_info(cx, "Connecting to %s:%s", cx->host, cx->port);
				io_info(cx, "Connecting to %s:%s", cx->host, cx->port);
				break;
			case ST_X(IO_ST_CXED, IO_ST_CXNG): /* F1 */
				io_cb_dxed(cx);
				io_dxed(cx);
				break;
			case ST_X(IO_ST_PING, IO_ST_CXNG): /* F2 */
				io_cb_err(cx, "Connection timeout (%u)", cx->ping);
				io_cb_dxed(cx);
				io_error(cx, "Connection timeout (%u)", cx->ping);
				io_dxed(cx);
				break;
			case ST_X(IO_ST_RXNG, IO_ST_DXED): /* B1 */
			case ST_X(IO_ST_CXNG, IO_ST_DXED): /* B2 */
				io_cb_info(cx, "Connection cancelled");
				io_info(cx, "Connection cancelled");
				break;
			case ST_X(IO_ST_CXED, IO_ST_DXED): /* B3 */
			case ST_X(IO_ST_PING, IO_ST_DXED): /* B4 */
				io_cb_info(cx, "Connection closed");
				io_cb_dxed(cx);
				io_info(cx, "Connection closed");
				io_dxed(cx);
				break;
			case ST_X(IO_ST_CXNG, IO_ST_CXED): /* D */
				io_cb_info(cx, " .. Connection successful");
				io_cb_cxed(cx);
				io_info(cx, " .. Connection successful");
				io_cxed(cx);
				cx->rx_sleep = 0;
				break;
			case ST_X(IO_ST_CXNG, IO_ST_RXNG): /* E */
				io_cb_err(cx, " .. Connection failed -- retrying");
				io_error(cx, " .. Connection failed -- retrying");
				break;
			case ST_X(IO_ST_CXED, IO_ST_PING): /* G */
				cx->ping = IO_PING_MIN;
				io_cb_ping_1(cx, cx->ping);
				io_ping(cx, (cx->ping = IO_PING_MIN));
				break;
			case ST_X(IO_ST_PING, IO_ST_PING): /* H */
				cx->ping += IO_PING_REFRESH;
				io_cb_ping_n(cx, cx->ping);
				io_ping(cx, (cx->ping += IO_PING_REFRESH));
				break;
			case ST_X(IO_ST_PING, IO_ST_CXED): /* I */
				cx->ping = 0;
				io_cb_ping_0(cx, cx->ping);
				io_ping(cx, (cx->ping = 0));
				break;
			default:
				fatal("BAD ST_X from: %d to: %d", st_cur, st_new);


@@ 595,24 560,35 @@ io_thread(void *arg)
}

static int
io_cx_read(struct connection *cx, unsigned timeout)
io_cx_read(struct connection *cx, uint32_t timeout)
{
	int ret;
	struct pollfd fd[1];
	unsigned char buf[1024];

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

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

	if (ret == 0)
		return MBEDTLS_ERR_SSL_TIMEOUT;

	if (ret < 0 && errno == EINTR)
		return MBEDTLS_ERR_SSL_WANT_READ;

	if (ret < 0)
		fatal("poll: %s", strerror(errno));

	if (cx->flags & IO_TLS_ENABLED) {
		mbedtls_ssl_conf_read_timeout(&(cx->tls_conf), SEC_IN_MS(timeout));
		if ((ret = mbedtls_ssl_read(&(cx->tls_ctx), buf, sizeof(buf))) > 0) {
			PT_LK(&io_cb_mutex);
			io_cb_read_soc((char *)buf, (size_t)ret,  cx->obj);
			PT_UL(&io_cb_mutex);
		}
		ret = mbedtls_ssl_read(&(cx->tls_ctx), buf, sizeof(buf));
	} else {
		while ((ret = recv(cx->soc, buf, sizeof(buf), 0)) > 0) {
			PT_LK(&io_cb_mutex);
			io_cb_read_soc((char *)buf, (size_t)ret,  cx->obj);
			PT_UL(&io_cb_mutex);
		}
		ret = mbedtls_net_recv(&(cx->net_ctx), buf, sizeof(buf));
	}

	if (ret > 0) {
		IO_CB(io_cb_read_soc((char *)buf, (size_t)ret,  cx->obj));
	}

	return ret;


@@ 633,10 609,8 @@ io_fatal(const char *f, int errnum)
static void
io_sig_handle(int sig)
{
	if (sig == SIGWINCH) {
	if (sig == SIGWINCH)
		flag_sigwinch_cb = 1;
		flag_tty_resized = 0;
	}
}

static void


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

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

	io_tty_winsize();
}

static void


@@ 716,10 692,10 @@ io_net_connect(struct connection *cx)
			return -1;

		if (ret == EAI_SYSTEM) {
			io_cb_err(cx, " .. Failed to resolve host: %s",
			io_error(cx, " .. Failed to resolve host: %s",
				io_strerror(buf, sizeof(buf)));
		} else {
			io_cb_err(cx, " .. Failed to resolve host: %s",
			io_error(cx, " .. Failed to resolve host: %s",
				gai_strerror(ret));
		}



@@ 742,13 718,13 @@ io_net_connect(struct connection *cx)
			goto err;
	}

	if (!p && soc < -1) {
		io_cb_err(cx, " .. Failed to obtain socket: %s", io_strerror(buf, sizeof(buf)));
	if (!p && soc < 0) {
		io_error(cx, " .. Failed to obtain socket: %s", io_strerror(buf, sizeof(buf)));
		goto err;
	}

	if (!p && soc >= 0) {
		io_cb_err(cx, " .. Failed to connect: %s", io_strerror(buf, sizeof(buf)));
		io_error(cx, " .. Failed to connect: %s", io_strerror(buf, sizeof(buf)));
		goto err;
	}



@@ 758,14 734,14 @@ io_net_connect(struct connection *cx)
		addr = &(((struct sockaddr_in6*)p->ai_addr)->sin6_addr);

	if (inet_ntop(p->ai_family, addr, buf, sizeof(buf)))
		io_cb_info(cx, " .. Connected [%s]", buf);
		io_info(cx, " .. Connected [%s]", buf);

	ret = soc;

err:
	freeaddrinfo(res);

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

static void


@@ 793,20 769,17 @@ io_tls_establish(struct connection *cx)
{
	int ret;

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

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

	cx->tls_fd.fd = cx->soc;

	if ((ret = mbedtls_ssl_config_defaults(
			&(cx->tls_conf),
			MBEDTLS_SSL_IS_CLIENT,
			MBEDTLS_SSL_TRANSPORT_STREAM,
			MBEDTLS_SSL_PRESET_DEFAULT))) {
		io_cb_err(cx, " .. %s ", io_tls_err(ret));
		io_error(cx, " .. %s ", io_tls_err(ret));
		goto err;
	}



@@ 834,27 807,27 @@ io_tls_establish(struct connection *cx)
			mbedtls_ssl_conf_authmode(&(cx->tls_conf), MBEDTLS_SSL_VERIFY_REQUIRED);
	}

	if ((ret = mbedtls_net_set_block(&(cx->tls_fd)))) {
		io_cb_err(cx, " .. %s ", io_tls_err(ret));
	if ((ret = mbedtls_net_set_block(&(cx->net_ctx)))) {
		io_error(cx, " .. %s ", io_tls_err(ret));
		goto err;
	}

	if ((ret = mbedtls_ssl_setup(&(cx->tls_ctx), &(cx->tls_conf)))) {
		io_cb_err(cx, " .. %s ", io_tls_err(ret));
		io_error(cx, " .. %s ", io_tls_err(ret));
		goto err;
	}

	if ((ret = mbedtls_ssl_set_hostname(&(cx->tls_ctx), cx->host))) {
		io_cb_err(cx, " .. %s ", io_tls_err(ret));
		io_error(cx, " .. %s ", io_tls_err(ret));
		goto err;
	}

	mbedtls_ssl_set_bio(
		&(cx->tls_ctx),
		&(cx->tls_fd),
		&(cx->net_ctx),
		mbedtls_net_send,
		NULL,
		mbedtls_net_recv_timeout);
		mbedtls_net_recv,
		NULL);

	while ((ret = mbedtls_ssl_handshake(&(cx->tls_ctx)))) {
		if (ret != MBEDTLS_ERR_SSL_WANT_READ


@@ 863,31 836,31 @@ io_tls_establish(struct connection *cx)
	}

	if (ret && cx->flags & IO_TLS_VRFY_DISABLED) {
		io_cb_err(cx, " .. %s ", io_tls_err(ret));
		io_error(cx, " .. %s ", io_tls_err(ret));
		goto err;
	}

	if (io_tls_x509_vrfy(cx) < 0)
		io_cb_err(cx, " .... Unknown x509 error");
		io_error(cx, " .... Unknown x509 error");

	if (ret) {
		io_cb_err(cx, " .. %s ", io_tls_err(ret));
		io_error(cx, " .. %s ", io_tls_err(ret));
		goto err;
	}

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

	return 0;

err:

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

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

	return -1;
}


@@ 914,7 887,7 @@ io_tls_x509_vrfy(struct connection *cx)
		if ((p = strchr(buf, '\n')))
			*p++ = 0;

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

	} while ((s = p));


M src/io.h => src/io.h +23 -57
@@ 1,5 1,5 @@
#ifndef IO_H
#define IO_H
#ifndef RIRC_IO_H
#define RIRC_IO_H

/* Handling off all network io, user input and signals
 *


@@ 49,20 49,17 @@
 *   (B) io_dx: close network connection
 *
 * Network state implicit transitions result in informational callback types:
 *   (C) on connection attempt:  IO_CB_INFO
 *   (E) on connection failure:  IO_CB_ERROR
 *   (D) on connection success:  IO_CB_CXED
 *   (F) on connection loss:     IO_CB_DXED
 *   (G) on ping timeout start:  IO_CB_PING_1
 *   (H) on ping timeout update: IO_CB_PING_N
 *   (I) on ping normal:         IO_CB_PING_0
 *   (D) on connection success:  io_cb_cxed
 *   (F) on connection loss:     io_cb_dxed
 *   (G) on ping timeout start:  io_cb_ping
 *   (H) on ping timeout update: io_cb_ping
 *   (I) on ping normal:         io_cb_ping
 *
 * Successful reads on stdin and connected sockets result in data callbacks:
 *   from stdin:  io_cb_read_inp
 *   from socket: io_cb_read_soc
 *
 * Signals registered to be caught result in non-signal handler context
 * callback with type IO_CB_SIGNAL
 * SIGWINCH results in a non signal-handler context callback io_cb_singwinch
 *
 * Failed connection attempts enter a retry cycle with exponential
 * backoff time given by:


@@ 77,37 74,6 @@
#include <stddef.h>
#include <stdint.h>

struct connection;

enum io_cb_t
{
	IO_CB_INVALID,
	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> */
	IO_CB_PING_1, /* <unsigned ping> */
	IO_CB_PING_N, /* <unsigned ping> */
	IO_CB_SIGNAL, /* <io_sig_t sig> */
	IO_CB_SIZE
};

enum io_log_level
{
	IO_LOG_ERROR,
	IO_LOG_WARN,
	IO_LOG_INFO,
	IO_LOG_DEBUG,
};

enum io_sig_t
{
	IO_SIG_INVALID,
	IO_SIGWINCH,
	IO_SIG_SIZE
};

#define IO_IPV_UNSPEC        (1 << 1)
#define IO_IPV_4             (1 << 2)
#define IO_IPV_6             (1 << 3)


@@ 117,7 83,8 @@ enum io_sig_t
#define IO_TLS_VRFY_OPTIONAL (1 << 7)
#define IO_TLS_VRFY_REQUIRED (1 << 8)

/* Returns a connection, or NULL if limit is reached */
struct connection;

struct connection* connection(
	const void*, /* callback object */
	const char*, /* host */


@@ 133,26 100,25 @@ int io_dx(struct connection*);
/* Formatted write to connection */
int io_sendf(struct connection*, const char*, ...);

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

/* Get tty dimensions */
unsigned io_tty_cols(void);
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*);

/* Log message callback */
void io_cb_log(const void*, enum io_log_level, const char*, ...);
/* IO event callbacks */
void io_cb_cxed(const void*);
void io_cb_dxed(const void*);
void io_cb_ping(const void*, unsigned);
void io_cb_sigwinch(unsigned, unsigned);

/* IO informational callbacks */
void io_cb_error(const void*, const char*, ...);
void io_cb_info(const void*, const char*, ...);

void io_init(void);
void io_start(void);
void io_stop(void);

#endif

M src/rirc.c => src/rirc.c +3 -2
@@ 1,3 1,5 @@
#include "src/rirc.h"

#include <errno.h>
#include <getopt.h>
#include <pwd.h>


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

	state_init();
	draw_init();

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



@@ 344,8 345,8 @@ main(int argc, char **argv)

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

	return ret;

M src/rirc.h => src/rirc.h +2 -2
@@ 1,5 1,5 @@
#ifndef RIRC_H
#define RIRC_H
#ifndef RIRC_RIRC_H
#define RIRC_RIRC_H

/* Default config values obtained at runtime */


M src/state.c => src/state.c +258 -367
@@ 1,8 1,4 @@
/**
 * state.c
 *
 * All manipulation of global program state
 */
#include "src/state.h"

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


@@ 11,22 7,19 @@
#include <stdarg.h>
#include <stdio.h>

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

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

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

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


@@ 36,6 29,15 @@ static uint16_t state_complete(char*, uint16_t, uint16_t, int);
static uint16_t state_complete_list(char*, uint16_t, uint16_t, const char**);
static uint16_t state_complete_user(char*, uint16_t, uint16_t, int);

static void state_channel_clear(int);
static void state_channel_close(int);

static int action_clear(char);
static int action_close(char);
static int action_error(char);
static int (*action_handler)(char);
static char action_buff[256];

static void command(struct channel*, char*);

static struct


@@ 45,6 47,9 @@ static struct
	struct server_list servers;
} state;

static unsigned state_tty_cols;
static unsigned state_tty_rows;

struct server_list*
state_server_list(void)
{


@@ 69,6 74,8 @@ static const char *irc_list[] = {
	"ctcp-time",
	"ctcp-userinfo",
	"ctcp-version",
	"away",
	"topic-unset",
	"admin",   "connect", "info",     "invite", "join",
	"kick",    "kill",    "links",    "list",   "lusers",
	"mode",    "motd",    "names",    "nick",   "notice",


@@ 77,15 84,14 @@ static const char *irc_list[] = {
	"time",    "topic",   "trace",    "user",   "version",
	"who",     "whois",   "whowas",   NULL };

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

void
state_init(void)
{
	state.default_channel = state.current_channel = channel("rirc", CHANNEL_T_OTHER);
	state.default_channel = state.current_channel = channel("rirc", CHANNEL_T_RIRC);

	newline(state.default_channel, 0, "--", "      _");
	newline(state.default_channel, 0, "--", " _ __(_)_ __ ___");


@@ 103,12 109,17 @@ state_init(void)
void
state_term(void)
{
	/* Exit handler; must return normally */

	struct server *s1, *s2;
	struct server *s1;
	struct server *s2;

	channel_free(state.default_channel);

	state.current_channel = NULL;
	state.default_channel = NULL;

	action_handler = NULL;
	action_buff[0] = 0;

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



@@ 118,6 129,21 @@ state_term(void)
		connection_free(s2->connection);
		server_free(s2);
	} while (s1 != state_server_list()->head);

	state.servers.head = NULL;
	state.servers.tail = NULL;
}

unsigned
state_cols(void)
{
	return state_tty_cols;
}

unsigned
state_rows(void)
{
	return state_tty_rows;
}

void


@@ 230,106 256,56 @@ state_server_set_chans(struct server *s, const char *chans)
	return 0;
}

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

/* WIP:
 *
 * removed action subsystem from input.c
 *
 * eventually should go in action.{h,c}
 *
 */
/* Max length of user action message */
#define MAX_ACTION_MESG 256
char *action_message;
static int action_close_server(char);
/* Action handling */
static int (*action_handler)(char);
static char action_buff[MAX_ACTION_MESG];
/* Incremental channel search */
static int action_find_channel(char);
/* TODO: This is a first draft for simple channel searching functionality.
 *
 * It can be cleaned up, and input.c is probably not the most ideal place for this */
#define MAX_SEARCH 128
struct channel *search_cptr; /* Used for iterative searching, before setting the current channel */
static char search_buf[MAX_SEARCH];
static size_t search_i;

static struct channel* search_channels(struct channel*, char*);
static struct channel*
search_channels(struct channel *start, char *search)
{
	if (start == NULL || *search == '\0')
		return NULL;

	/* Start the search one past the input */
	struct channel *c = channel_get_next(start);

	while (c != start) {

		if (strstr(c->name, search))
			return c;

		c = channel_get_next(c);
	}

	return NULL;
}
static int
state_input_action(const char *input, size_t len)
{
	/* Waiting for user confirmation */

	/* ^c canceled the action, or the action was resolved */
	if (len == 1 && (*input == CTRL('c') || action_handler(*input))) {
		/* ^c canceled the action, or the action was resolved */

		action_message = NULL;
		action_handler = NULL;

		return 1;
	}

	return 0;
}

static int
action_close_server(char c)
action_error(char c)
{
	/* Confirm closing a server */

	if (c == 'n' || c == 'N')
		return 1;
	UNUSED(c);

	if (c == 'y' || c == 'Y') {

		int ret;
		struct channel *c = current_channel();
		struct server *s = c->server;
	return 1;
}

		/* If closing the last server */
		if ((state.current_channel = c->server->next->channel) == c->server->channel)
			state.current_channel = state.default_channel;
static int
action_clear(char c)
{
	if (toupper(c) == 'N')
		return 1;

		if ((ret = io_sendf(s->connection, "QUIT :%s", DEFAULT_QUIT_MESG)))
			newlinef(s->channel, 0, "-!!-", "sendf fail: %s", io_err(ret));
	if (toupper(c) == 'Y') {
		state_channel_clear(0);
		return 1;
	}

		io_dx(s->connection);
		connection_free(s->connection);
		server_list_del(state_server_list(), s);
		server_free(s);
	return 0;
}

		draw(DRAW_ALL);
static int
action_close(char c)
{
	if (toupper(c) == 'N')
		return 1;

	if (toupper(c) == 'Y') {
		state_channel_close(0);
		return 1;
	}

	return 0;
}

void
action(int (*a_handler)(char), const char *fmt, ...)
{


@@ 343,130 319,84 @@ action(int (*a_handler)(char), const char *fmt, ...)
	va_list ap;

	va_start(ap, fmt);
	len = vsnprintf(action_buff, MAX_ACTION_MESG, fmt, ap);
	len = vsnprintf(action_buff, sizeof(action_buff), fmt, ap);
	va_end(ap);

	if (len < 0) {
		debug("vsnprintf failed");
	} else {
		action_handler = a_handler;
		action_message = action_buff;
		draw(DRAW_INPUT);
	}
}
/* Action line should be:
 *
 *
 * Find: [current result]/[(server if not current server[socket if not 6697])] : <search input> */
static int
action_find_channel(char c)
{
	/* Incremental channel search */

	/* \n, Esc, ^C cancels a search if no results are found */
	if (c == '\n' || c == 0x1b || c == CTRL('c')) {
const char*
action_message(void)
{
	return (action_handler ? action_buff : NULL);
}

		/* Confirm non-empty match */
		if (c == '\n' && search_cptr)
			channel_set_current(search_cptr);
static void
state_channel_clear(int action_confirm)
{
	struct channel *c = current_channel();

		search_buf[0] = 0;
		search_i = 0;
		search_cptr = NULL;
		return 1;
	if (action_confirm) {
		action(action_clear, "Clear buffer '%s'?   [y/n]", c->name);
	} else {
		memset(&(c->buffer), 0, sizeof(c->buffer));
		draw(DRAW_BUFFER);
	}
}

	/* ^F repeats the search forward from the current result,
	 * or resets search criteria if no match */
	if (c == CTRL('f')) {
		if (search_cptr == NULL) {
			search_buf[0] = 0;
			search_i = 0;
			action(action_find_channel, "Find: ");
			return 0;
		}
static void
state_channel_close(int action_confirm)
{
	/* Close the current channel */

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

	} else if (isprint(c) && search_i < (sizeof(search_buf) - 1)) {
		/* All other input */
		search_buf[search_i++] = c;
		search_buf[search_i] = 0;
		search_cptr = search_channels(current_channel(), search_buf);
	}
	struct channel *c = current_channel();
	struct server *s = c->server;

	/* Reprint the action message */
	if (search_cptr == NULL) {
		if (*search_buf)
			action(action_find_channel, "Find: NO MATCH -- %s", search_buf);
		else
			action(action_find_channel, "Find: ");
	} else {
		/* Found a channel */
		if (search_cptr->server == current_channel()->server) {
			action(action_find_channel, "Find: %s -- %s",
					search_cptr->name, search_buf);
		} else {
			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
				action(action_find_channel, "Find: %s:%s/%s -- %s",
						search_cptr->server->host, search_cptr->server->port,
						search_cptr->name, search_buf);
		}
	if (c->type == CHANNEL_T_RIRC) {
		action(action_error, "Type :quit to exit rirc");
		return;
	}

	return 0;
}
	if (action_confirm) {

void
channel_close(struct channel *c)
{
	/* Close a channel. If the current channel is being
	 * closed, update state appropriately */
		if (c->type == CHANNEL_T_CHANNEL || c->type == CHANNEL_T_PRIVATE)
			action(action_close, "Close '%s'?   [y/n]", c->name);

		if (c->type == CHANNEL_T_SERVER)
			action(action_close, "Close server '%s'? [%d channels]   [y/n])",
				c->name, (s->clist.count - 1));

	if (c == state.default_channel) {
		newline(c, 0, "--", "Type :quit to exit rirc");
		return;
	}

	if (c->type == CHANNEL_T_SERVER) {
		/* Closing a server, confirm the number of channels being closed */
	if (c->type == CHANNEL_T_CHANNEL || c->type == CHANNEL_T_PRIVATE) {

		int num_chans = 0;
		if (s->connected && c->type == CHANNEL_T_CHANNEL && !c->parted)
			io_sendf(s->connection, "PART %s :%s", c->name, DEFAULT_PART_MESG);

		while ((c = c->next)->type != CHANNEL_T_SERVER)
			num_chans++;
		channel_set_current(c->next);
		channel_list_del(&(s->clist), c);
		channel_free(c);
		return;
	}

		if (num_chans)
			action(action_close_server, "Close server '%s'? Channels: %d   [y/n]",
					c->server->host, num_chans);
		else
			action(action_close_server, "Close server '%s'?   [y/n]", c->server->host);
	} else {
		/* Closing a channel */
		if (c->type == CHANNEL_T_CHANNEL && !c->parted) {
			int ret;
			if (0 != (ret = io_sendf(c->server->connection, "PART %s", c->name))) {
				// FIXME: closing a parted channel when server is disconnected isnt an error
				newlinef(c->server->channel, 0, "sendf fail", "%s", io_err(ret));
			}
		}
	if (c->type == CHANNEL_T_SERVER) {

		/* If closing the current channel, update state to a new channel */
		if (c == current_channel()) {
			channel_set_current(c->next);
		} else {
			draw(DRAW_NAV);
		if (s->connected) {
			io_sendf(s->connection, "QUIT :%s", DEFAULT_QUIT_MESG);
			io_dx(s->connection);
		}

		channel_list_del(&c->server->clist, c);
		channel_free(c);
		channel_set_current((s->next != s ? s->next->channel : state.default_channel));
		connection_free(s->connection);
		server_list_del(state_server_list(), s);
		server_free(s);
		return;
	}
}



@@ 482,8 412,8 @@ buffer_scrollback_back(struct channel *c)
	unsigned int buffer_i = b->scrollback,
	             count = 0,
	             text_w = 0,
	             cols = io_tty_cols(),
	             rows = io_tty_rows() - 4;
	             cols = state_tty_cols,
	             rows = state_tty_rows - 4;

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



@@ 524,8 454,8 @@ buffer_scrollback_forw(struct channel *c)

	unsigned int count = 0,
	             text_w = 0,
	             cols = io_tty_cols(),
	             rows = io_tty_rows() - 4;
	             cols = state_tty_cols,
	             rows = state_tty_rows - 4;

	struct buffer *b = &c->buffer;



@@ 691,183 621,80 @@ state_complete(char *str, uint16_t len, uint16_t max, int first)
}

static void
state_io_cxed(struct server *s)
{
	int ret;
	server_reset(s);
	server_nicks_next(s);

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

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

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

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

	draw(DRAW_STATUS);
}

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

	do {
		newline(c, 0, "-!!-", " -- disconnected --");
		channel_reset(c);
		c = c->next;
	} while (c != s->channel);
}

static void
state_io_ping(struct server *s, unsigned int ping)
command(struct channel *c, char *buf)
{
	int ret;
	const char *arg;
	const char *cmd;
	int err;

	s->ping = ping;
	if (!(cmd = strsep(&buf)))
		return;

	if (ping != IO_PING_MIN)
		draw(DRAW_STATUS);
	else if ((ret = io_sendf(s->connection, "PING :%s", s->host)))
		newlinef(s->channel, 0, "-!!-", "sendf fail: %s", io_err(ret));
}
	if (!strcasecmp(cmd, "clear")) {
		if ((arg = strsep(&buf))) {
			action(action_error, "clear: Unknown arg '%s'", arg);
			return;
		}

static void
state_io_signal(enum io_sig_t sig)
{
	switch (sig) {
		case IO_SIGWINCH:
			draw(DRAW_ALL);
			break;
		default:
			newlinef(state.default_channel, 0, "-!!-", "unhandled signal %d", sig);
		state_channel_clear(0);
		return;
	}
}

void
io_cb(enum io_cb_t type, const void *cb_obj, ...)
{
	struct server *s = (struct server *)cb_obj;
	va_list ap;

	va_start(ap, cb_obj);
	if (!strcasecmp(cmd, "close")) {
		if ((arg = strsep(&buf))) {
			action(action_error, "close: Unknown arg '%s'", arg);
			return;
		}

	switch (type) {
		case IO_CB_CXED:
			state_io_cxed(s);
			break;
		case IO_CB_DXED:
			state_io_dxed(s);
			break;
		case IO_CB_PING_0:
		case IO_CB_PING_1:
		case IO_CB_PING_N:
			state_io_ping(s, va_arg(ap, unsigned int));
			break;
		case IO_CB_ERR:
			_newline(s->channel, 0, "-!!-", va_arg(ap, const char *), ap);
			break;
		case IO_CB_INFO:
			_newline(s->channel, 0, "--", va_arg(ap, const char *), ap);
			break;
		case IO_CB_SIGNAL:
			state_io_signal(va_arg(ap, enum io_sig_t));
			break;
		default:
			fatal("unhandled io_cb_t: %d", type);
		state_channel_close(0);
		return;
	}

	va_end(ap);
	if (!strcasecmp(cmd, "connect")) {
		if (!c->server) {
			action(action_error, "connect: This is not a server");
			return;
		}

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

static void
command(struct channel *c, char *buf)
{
	const char *cmnd;
	int err;
		if ((err = io_cx(c->server->connection)))
			action(action_error, "connect: %s", io_err(err));

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

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

	if (!strcasecmp(cmnd, "connect")) {
		// 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;

		if (host == NULL) {
			if (c->server == NULL) {
				newlinef(c, 0, "-!!-", "%s", help);
			} else if ((err = io_cx(c->server->connection))) {
				newlinef(c, 0, "-!!-", "%s", io_err(err));
			}
		} else {
			port = (port ? port : "6697");
			user = (user ? user : default_username);
			real = (real ? real : default_realname);

			if ((s = server_list_get(&state.servers, host, port)) != NULL) {
				channel_set_current(s->channel);
				newlinef(s->channel, 0, "-!!-", "already connected to %s:%s", host, port);
			} else {
				s = server(host, port, pass, user, real);
				s->connection = connection(s, host, port, 0);
				server_list_add(state_server_list(), s);
				channel_set_current(s->channel);
				io_cx(s->connection);
				draw(DRAW_ALL);
			}
		if ((arg = strsep(&buf))) {
			action(action_error, "disconnect: Unknown arg '%s'", arg);
			return;
		}
		return;
	}

	if (!strcasecmp(cmnd, "disconnect")) {
		io_dx(c->server->connection);
		return;
	}
		if ((err = io_dx(c->server->connection)))
			action(action_error, "disconnect: %s", io_err(err));

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

	if (!strcasecmp(cmnd, "close")) {
		channel_close(c);
		return;
	}
	if (!strcasecmp(cmd, "quit")) {
		if ((arg = strsep(&buf))) {
			action(action_error, "quit: Unknown arg '%s'", arg);
			return;
		}

	if (!strcasecmp(cmnd, "set")) {
		/* TODO user, real, nicks, pass, key */
		io_stop();
		return;
	}

	/* TODO:
	 * help
	 * ignore
	 * unignore
	 * version
	 * find
	 * buffers
	 * b#
	 * b<num>
	 */
	action(action_error, "Unknown command '%s'", cmd);
}

static int


@@ 929,16 756,9 @@ state_input_ctrlch(const char *c, size_t len)
			/* Cancel current input */
			return input_reset(&(current_channel()->input));

		case CTRL('f'):
			/* Find channel */
			if (current_channel()->server)
				 action(action_find_channel, "Find: ");
			break;

		case CTRL('l'):
			/* Clear current channel */
			/* TODO: as action with confirmation */
			channel_clear(current_channel());
			state_channel_clear(1);
			break;

		case CTRL('p'):


@@ 952,8 772,7 @@ state_input_ctrlch(const char *c, size_t len)
			break;

		case CTRL('x'):
			/* Close current channel */
			channel_close(current_channel());
			state_channel_close(1);
			break;

		case CTRL('u'):


@@ 981,6 800,8 @@ state_input_linef(struct channel *c)
	if ((len = input_write(&(c->input), buf, sizeof(buf), 0)) == 0)
		return 0;

	input_hist_push(&(c->input));

	switch (buf[0]) {
		case ':':
			if (len > 1 && buf[1] == ':')


@@ 998,8 819,6 @@ state_input_linef(struct channel *c)
			irc_send_privmsg(current_channel()->server, current_channel(), buf);
	}

	input_hist_push(&(c->input));

	return 1;
}



@@ 1010,7 829,7 @@ io_cb_read_inp(char *buf, size_t len)

	if (len == 0)
		fatal("zero length message");
	else if (action_message)
	else if (action_handler)
		redraw_input = state_input_action(buf, len);
	else if (iscntrl(*buf))
		redraw_input = state_input_ctrlch(buf, len);


@@ 1061,25 880,97 @@ io_cb_read_soc(char *buf, size_t len, const void *cb_obj)
}

void
io_cb_log(const void *cb_obj, enum io_log_level lvl, const char *fmt, ...)
io_cb_cxed(const void *cb_obj)
{
	struct server *s = (struct server *)cb_obj;

	int ret;
	server_reset(s);
	server_nicks_next(s);

	s->connected = 1;

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

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

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

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

	draw(DRAW_STATUS);
	draw(DRAW_FLUSH);
}

void
io_cb_dxed(const void *cb_obj)
{
	struct server *s = (struct server *)cb_obj;
	struct channel *c = s->channel;

	s->connected = 0;

	do {
		newline(c, 0, "-!!-", " -- disconnected --");
		channel_reset(c);
		c = c->next;
	} while (c != s->channel);

	draw(DRAW_FLUSH);
}

void
io_cb_ping(const void *cb_obj, unsigned ping)
{
	int ret;
	struct server *s = (struct server *)cb_obj;

	s->ping = ping;

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

	draw(DRAW_FLUSH);
}

void
io_cb_sigwinch(unsigned cols, unsigned rows)
{
	state_tty_cols = cols;
	state_tty_rows = rows;

	draw(DRAW_ALL);
	draw(DRAW_FLUSH);
}

void
io_cb_info(const void *cb_obj, const char *fmt, ...)
{
	va_list ap;
	va_start(ap, fmt);

	switch (lvl) {
		case IO_LOG_ERROR:
			_newline(s->channel, 0, "-!!-", fmt, ap);
			break;
		case IO_LOG_WARN:
		case IO_LOG_INFO:
		case IO_LOG_DEBUG:
			_newline(s->channel, 0, "--", fmt, ap);
			break;
		default:
			fatal("invalid log level");
	}
	_newline(((struct server *)cb_obj)->channel, 0, "--", fmt, ap);

	va_end(ap);

	draw(DRAW_FLUSH);
}

void
io_cb_error(const void *cb_obj, const char *fmt, ...)
{
	va_list ap;
	va_start(ap, fmt);

	_newline(((struct server *)cb_obj)->channel, 0, "-!!-", fmt, ap);

	va_end(ap);

	draw(DRAW_FLUSH);
}

M src/state.h => src/state.h +7 -4
@@ 1,5 1,5 @@
#ifndef STATE_H
#define STATE_H
#ifndef RIRC_STATE_H
#define RIRC_STATE_H

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


@@ 31,6 31,10 @@ struct server_list* state_server_list(void);
void state_init(void);
void state_term(void);

/* Get tty dimensions */
unsigned state_cols(void);
unsigned state_rows(void);

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


@@ 42,7 46,6 @@ struct channel* channel_get_prev(struct channel*);
/* FIXME: */
void buffer_scrollback_back(struct channel*);
void buffer_scrollback_forw(struct channel*);
void channel_clear(struct channel*);

void channel_close(struct channel*);
void channel_move_prev(void);


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

extern char *action_message;
const char *action_message(void);

#endif

M src/utils/list.h => src/utils/list.h +2 -2
@@ 1,5 1,5 @@
#ifndef LIST_H
#define LIST_H
#ifndef RIRC_UTILS_LIST_H
#define RIRC_UTILS_LIST_H

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


M src/utils/tree.h => src/utils/tree.h +9 -21
@@ 1,5 1,5 @@
#ifndef TREE_H
#define TREE_H
#ifndef RIRC_UTILS_TREE_H
#define RIRC_UTILS_TREE_H

#include <stddef.h>



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

#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_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, n) name##_AVL_GET(x, y, z, n)
#define AVL_FOREACH(name, x, y)   name##_AVL_FOREACH(x, y)

#define TREE_HEAD(type) \
    struct type *tree_root


@@ 114,24 113,13 @@ name##_AVL_FOREACH(struct name *head, void (*f)(struct type*))                  
}                                                                                 \
                                                                                  \
static struct type*                                                               \
name##_AVL_GET(struct name *head, struct type *elm, void *arg)                    \
name##_AVL_GET(struct name *head, struct type *elm, void *arg, size_t n)          \
{                                                                                 \
    int comp;                                                                     \
    struct type *tmp = TREE_ROOT(head);                                           \
                                                                                  \
    while (tmp && (comp = cmp(elm, tmp, arg)))                                    \
        tmp = (comp > 0) ? TREE_RIGHT(tmp, field) : TREE_LEFT(tmp, field);        \
                                                                                  \
    return tmp;                                                                   \
}                                                                                 \
                                                                                  \
static struct type*                                                               \
name##_AVL_NGET(struct name *head, struct type *elm, void *arg, size_t n)         \
{                                                                                 \
    int comp;                                                                     \
    struct type *tmp = TREE_ROOT(head);                                           \
                                                                                  \
    while (tmp && (comp = ncmp(elm, tmp, arg, n)))                                \
    /* TODO: this can all be one func, with a1, a2, a3 as arguments   */          \
    while (tmp && (comp = (n ? ncmp(elm, tmp, arg, n) : cmp(elm, tmp, arg))))     \
        tmp = (comp > 0) ? TREE_RIGHT(tmp, field) : TREE_LEFT(tmp, field);        \
                                                                                  \
    return tmp;                                                                   \

M src/utils/utils.c => src/utils/utils.c +49 -50
@@ 1,3 1,5 @@
#include "src/utils/utils.h"

#include <errno.h>
#include <stdarg.h>
#include <stdio.h>


@@ 5,45 7,11 @@
#include <string.h>
#include <strings.h>

#include "src/utils/utils.h"

static inline int irc_ischanchar(char, int);
static inline int irc_isnickchar(char, int);
static inline int irc_toupper(enum casemapping_t, int);

int
irc_isnickchar(char c, int first)
{
	/* RFC 2812, section 2.3.1
	 *
	 * nickname   =  ( letter / special ) *8( letter / digit / special / "-" )
	 * letter     =  %x41-5A / %x61-7A       ; A-Z / a-z
	 * digit      =  %x30-39                 ; 0-9
	 * special    =  %x5B-60 / %x7B-7D       ; "[", "]", "\", "`", "_", "^", "{", "|", "}"
	 */

	return ((c >= 0x41 && c <= 0x7D) || (!first && ((c >= 0x30 && c <= 0x39) || c == '-')));
}

int
irc_ischanchar(char c, int first)
{
	/* RFC 2812, section 2.3.1
	 *
	 * channel    =  ( "#" / "+" / ( "!" channelid ) / "&" ) chanstring
	 *               [ ":" chanstring ]
	 * chanstring =  %x01-07 / %x08-09 / %x0B-0C / %x0E-1F / %x21-2B
	 * chanstring =/ %x2D-39 / %x3B-FF
	 *                 ; any octet except NUL, BELL, CR, LF, " ", "," and ":"
	 * channelid  = 5( %x41-5A / digit )   ; 5( A-Z / 0-9 )
	 */

	/* TODO: CHANTYPES */
	(void)c;
	(void)first;

	return 1;
}

int
irc_isnick(const char *str)
{
	if (!irc_isnickchar(*str++, 1))


@@ 72,21 40,18 @@ irc_ischan(const char *str)
}

int
irc_pinged(enum casemapping_t casemapping, const char *mesg, const char *nick)
irc_pinged(enum casemapping_t cm, const char *mesg, const char *nick)
{
	size_t len = strlen(nick);

	while (*mesg) {

		/* skip any prefixing characters that wouldn't match a valid nick */
		while (!(*mesg >= 0x41 && *mesg <= 0x7D))
		while (*mesg && *mesg != *nick && !irc_isnickchar(*mesg, 1))
			mesg++;

		/* nick prefixes the word, following character is space or symbol */
		if (!irc_strncmp(casemapping, mesg, nick, len) && !irc_isnickchar(*(mesg + len), 0))
		if (!irc_strncmp(cm, mesg, nick, len) && !irc_isnickchar(*(mesg + len), 0))
			return 1;

		/* skip to end of word */
		while (*mesg && *mesg != ' ')
			mesg++;
	}


@@ 95,7 60,7 @@ irc_pinged(enum casemapping_t casemapping, const char *mesg, const char *nick)
}

int
irc_strcmp(enum casemapping_t casemapping, const char *s1, const char *s2)
irc_strcmp(enum casemapping_t cm, const char *s1, const char *s2)
{
	/* Case insensitive comparison of strings s1, s2 in accordance
	 * with RFC 2812, section 2.2 */


@@ 104,8 69,8 @@ irc_strcmp(enum casemapping_t casemapping, const char *s1, const char *s2)

	for (;;) {

		c1 = irc_toupper(casemapping, *s1++);
		c2 = irc_toupper(casemapping, *s2++);
		c1 = irc_toupper(cm, *s1++);
		c2 = irc_toupper(cm, *s2++);

		if ((c1 -= c2))
			return -c1;


@@ 118,7 83,7 @@ irc_strcmp(enum casemapping_t casemapping, const char *s1, const char *s2)
}

int
irc_strncmp(enum casemapping_t casemapping, const char *s1, const char *s2, size_t n)
irc_strncmp(enum casemapping_t cm, const char *s1, const char *s2, size_t n)
{
	/* Case insensitive comparison of strings s1, s2 in accordance
	 * with RFC 2812, section 2.2, up to n characters */


@@ 127,8 92,8 @@ irc_strncmp(enum casemapping_t casemapping, const char *s1, const char *s2, size

	while (n > 0) {

		c1 = irc_toupper(casemapping, *s1++);
		c2 = irc_toupper(casemapping, *s2++);
		c1 = irc_toupper(cm, *s1++);
		c2 = irc_toupper(cm, *s2++);

		if ((c1 -= c2))
			return -c1;


@@ 440,7 405,41 @@ memdup(const void *mem, size_t len)
}

static inline int
irc_toupper(enum casemapping_t casemapping, int c)
irc_ischanchar(char c, int first)
{
	/* RFC 2812, section 2.3.1
	 *
	 * channel    =  ( "#" / "+" / ( "!" channelid ) / "&" ) chanstring
	 *               [ ":" chanstring ]
	 * chanstring =  %x01-07 / %x08-09 / %x0B-0C / %x0E-1F / %x21-2B
	 * chanstring =/ %x2D-39 / %x3B-FF
	 *                 ; any octet except NUL, BELL, CR, LF, " ", "," and ":"
	 * channelid  = 5( %x41-5A / digit )   ; 5( A-Z / 0-9 )
	 */

	/* TODO: CHANTYPES */
	(void)c;
	(void)first;

	return 1;
}

static inline int
irc_isnickchar(char c, int first)
{
	/* RFC 2812, section 2.3.1
	 *
	 * nickname   =  ( letter / special ) *8( letter / digit / special / "-" )
	 * letter     =  %x41-5A / %x61-7A       ; A-Z / a-z
	 * digit      =  %x30-39                 ; 0-9
	 * special    =  %x5B-60 / %x7B-7D       ; "[", "]", "\", "`", "_", "^", "{", "|", "}"
	 */

	return ((c >= 0x41 && c <= 0x7D) || (!first && ((c >= 0x30 && c <= 0x39) || c == '-')));
}

static inline int
irc_toupper(enum casemapping_t cm, int c)
{
	/* RFC 2812, section 2.2
	 *


@@ 449,7 448,7 @@ irc_toupper(enum casemapping_t casemapping, int c)
	 * respectively. This is a critical issue when determining the
	 * equivalence of two nicknames or channel names. */

	switch (casemapping) {
	switch (cm) {
		case CASEMAPPING_RFC1459:
			if (c == '^') return '~';
			/* FALLTHROUGH */

M src/utils/utils.h => src/utils/utils.h +2 -7
@@ 1,5 1,5 @@
#ifndef UTILS_H
#define UTILS_H
#ifndef RIRC_UTILS_UTILS_H
#define RIRC_UTILS_UTILS_H

#include <stdio.h>
#include <stdlib.h>


@@ 7,9 7,6 @@
#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)



@@ 66,9 63,7 @@ struct irc_message
};

int irc_ischan(const char*);
int irc_ischanchar(char, int);
int irc_isnick(const char*);
int irc_isnickchar(char, int);
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);

M test/components/channel.c => test/components/channel.c +3 -3
@@ 19,9 19,9 @@ test_channel_list(void)

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

	c1 = channel("aaa", CHANNEL_T_OTHER);
	c2 = channel("bbb", CHANNEL_T_OTHER);
	c3 = channel("ccc", CHANNEL_T_OTHER);
	c1 = channel("aaa", CHANNEL_T_RIRC);
	c2 = channel("bbb", CHANNEL_T_RIRC);
	c3 = channel("ccc", CHANNEL_T_RIRC);

	channel_list_add(&clist, c1);
	channel_list_add(&clist, c2);

M test/components/mode.c => test/components/mode.c +4 -1
@@ 286,12 286,15 @@ test_prfxmode_prefix(void)
	mode_cfg_chanmodes(&cfg, "abc");

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

	/* Test setting valid prfxmode prefix */
	assert_eq(mode_prfxmode_prefix(&m, &cfg, '2'), MODE_ERR_NONE);
	assert_eq(mode_prfxmode_prefix(&m, &cfg, '3'), MODE_ERR_NONE);

	assert_strcmp(mode_str(&m, &str), "b");
	assert_strcmp(mode_str(&m, &str), "bc");
	assert_eq(m.prefix, '2');
}


M test/components/user.c => test/components/user.c +23 -7
@@ 102,18 102,34 @@ test_user_list_casemapping(void)
	memset(&ulist, 0, sizeof(ulist));

	assert_eq(user_list_add(&ulist, CASEMAPPING_RFC1459, "aaa", MODE_EMPTY), USER_ERR_NONE);
	assert_eq(user_list_add(&ulist, CASEMAPPING_RFC1459, "aAa", MODE_EMPTY), USER_ERR_DUPLICATE);
	assert_eq(user_list_add(&ulist, CASEMAPPING_RFC1459, "AaA", MODE_EMPTY), USER_ERR_DUPLICATE);
	assert_eq(user_list_add(&ulist, CASEMAPPING_RFC1459, "{}^", MODE_EMPTY), USER_ERR_NONE);
	assert_eq(user_list_add(&ulist, CASEMAPPING_RFC1459, "[}~", MODE_EMPTY), USER_ERR_DUPLICATE);
	assert_eq(user_list_add(&ulist, CASEMAPPING_RFC1459, "zzz", MODE_EMPTY), USER_ERR_NONE);

	if ((u = user_list_get(&ulist, CASEMAPPING_RFC1459, "a", 1)) == NULL)
	assert_eq(ulist.count, 3);

	if ((u = user_list_get(&ulist, CASEMAPPING_RFC1459, "AAA", 0)) == NULL)
		test_abort("Failed to retrieve u");

	assert_ptr_eq(user_list_get(&ulist, CASEMAPPING_RFC1459, "Aaa", 0), u);
	assert_ptr_eq(user_list_get(&ulist, CASEMAPPING_RFC1459, "A", 1), u);
	assert_ptr_eq(user_list_get(&ulist, CASEMAPPING_RFC1459, "aAa", 3), u);
	assert_ptr_eq(user_list_get(&ulist, CASEMAPPING_RFC1459, "A",   1), u);

	assert_eq(user_list_rpl(&ulist, CASEMAPPING_RFC1459, "AaA", "bbb"), USER_ERR_NONE);
	assert_eq(user_list_del(&ulist, CASEMAPPING_RFC1459, "aaa"),        USER_ERR_NOT_FOUND);
	assert_eq(user_list_del(&ulist, CASEMAPPING_RFC1459, "BBB"),        USER_ERR_NONE);

	assert_eq(ulist.count, 2);

	assert_eq(user_list_rpl(&ulist, CASEMAPPING_RFC1459, "aaa", "aAa"), USER_ERR_DUPLICATE);
	assert_eq(user_list_rpl(&ulist, CASEMAPPING_RFC1459, "aAa", "zzz"), USER_ERR_NONE);
	assert_eq(user_list_rpl(&ulist, CASEMAPPING_RFC1459, "{}^", "[}~"), USER_ERR_NONE);
	assert_eq(user_list_rpl(&ulist, CASEMAPPING_RFC1459, "zzz", "ZzZ"), USER_ERR_NONE);

	assert_eq(user_list_del(&ulist, CASEMAPPING_RFC1459, "ZZZ"), USER_ERR_NONE);
	assert_eq(ulist.count, 2);

	assert_ptr_not_null(user_list_get(&ulist, CASEMAPPING_RFC1459, "zZz", 0));
	assert_ptr_not_null(user_list_get(&ulist, CASEMAPPING_RFC1459, "{]^", 0));

	user_list_free(&ulist);
}

static void

M test/handlers/irc_ctcp.c => test/handlers/irc_ctcp.c +36 -28
@@ 1,5 1,7 @@
#include "test/test.h"

#include <ctype.h>

#include "src/components/buffer.c"
#include "src/components/channel.c"
#include "src/components/input.c"


@@ 60,26 62,6 @@ static struct irc_message m;
static struct server *s;

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

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

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

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

static void
test_recv_ctcp_request(void)
{
	/* test malformed */


@@ 123,6 105,15 @@ test_recv_ctcp_request(void)

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

	/* test case insensitive */
	CHECK_REQUEST(":nick!user@host PRIVMSG me :\001clientinfo", 0, 1, 1,
		"CTCP CLIENTINFO from nick",
		"NOTICE nick :\001CLIENTINFO " CTCP_CLIENTINFO "\001");

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

static void


@@ 169,6 160,13 @@ test_recv_ctcp_response(void)

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

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

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

static void


@@ 190,7 188,7 @@ test_recv_ctcp_request_action(void)
		assert_eq(ctcp_request(s, (F), (T), (M)), (R)); \
		assert_eq(mock_line_n, 1); \
		assert_eq(mock_send_n, 0); \
		assert_strcmp(mock_chan, (C)); \
		assert_strcmp(mock_chan[0], (C)); \
		assert_strcmp(mock_line[0], (L)); \
	} while (0)



@@ 222,15 220,26 @@ test_recv_ctcp_request_clientinfo(void)
{
	CHECK_REQUEST(":nick!user@host PRIVMSG me :\001CLIENTINFO", 0, 1, 1,
		"CTCP CLIENTINFO from nick",
		"NOTICE nick :\001CLIENTINFO ACTION CLIENTINFO PING SOURCE TIME VERSION\001");
		"NOTICE nick :\001CLIENTINFO " CTCP_CLIENTINFO "\001");

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

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

	char *p, clientinfo[] = CTCP_CLIENTINFO;

	for (p = clientinfo; *p; p++)
		*p = tolower(*p);

	#define X(cmd) assert_ptr_not_null(strstr(clientinfo, #cmd));
		CTCP_EXTENDED_FORMATTING
		CTCP_EXTENDED_QUERY
		CTCP_METADATA_QUERY
	#undef X
}

static void


@@ 270,15 279,15 @@ test_recv_ctcp_request_source(void)
{
	CHECK_REQUEST(":nick!user@host PRIVMSG me :\001SOURCE", 0, 1, 1,
		"CTCP SOURCE from nick",
		"NOTICE nick :\001SOURCE rcr.io/rirc\001");
		"NOTICE nick :\001SOURCE https://rcr.io/rirc\001");

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

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

static void


@@ 488,7 497,6 @@ main(void)
	server_nick_set(s, "me");

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

M test/handlers/irc_recv.c => test/handlers/irc_recv.c +736 -54
@@ 2,6 2,7 @@

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


@@ 20,7 21,7 @@
	char TOKEN(buf, __LINE__)[] = S; \
	assert_eq(irc_message_parse(&m, TOKEN(buf, __LINE__)), 0);

#define CHECK_REQUEST(M, RET, LINE_N, SEND_N, LINE, SEND) \
#define CHECK_RECV(M, RET, LINE_N, SEND_N) \
	do { \
		mock_reset_io(); \
		mock_reset_state(); \


@@ 28,80 29,69 @@
		assert_eq(irc_recv(s, &m), (RET)); \
		assert_eq(mock_line_n, (LINE_N)); \
		assert_eq(mock_send_n, (SEND_N)); \
		assert_strcmp(mock_line[0], (LINE)); \
		assert_strcmp(mock_send[0], (SEND)); \
	} while (0)

static struct irc_message m;

static struct channel *c1;
static struct channel *c2;
static struct channel *c3;
static struct server *s;

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

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

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

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

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

	channel_reset(c);
	CHECK_REQUEST("353 me = #chan", 1, 1, 0,
		"RPL_NAMEREPLY: nicks is null", "");
	CHECK_RECV("353 me", 1, 1, 0);
	assert_strcmp(mock_chan[0], "host");
	assert_strcmp(mock_line[0], "RPL_NAMEREPLY: type is null");
	assert_strcmp(mock_send[0], "");

	channel_reset(c);
	CHECK_REQUEST("353 me = #x :n1", 1, 1, 0,
		"RPL_NAMEREPLY: channel '#x' not found", "");
	CHECK_RECV("353 me =", 1, 1, 0);
	assert_strcmp(mock_line[0], "RPL_NAMEREPLY: channel is null");

	channel_reset(c);
	CHECK_REQUEST("353 me x #chan :n1", 1, 1, 0,
		"RPL_NAMEREPLY: invalid channel flag: 'x'", "");
	CHECK_RECV("353 me = #c1", 1, 1, 0);
	assert_strcmp(mock_line[0], "RPL_NAMEREPLY: nicks is null");

	channel_reset(c);
	CHECK_REQUEST("353 me = #chan :!n1", 1, 1, 0,
		"RPL_NAMEREPLY: invalid user prefix: '!'", "");
	CHECK_RECV("353 me = #x :n1", 1, 1, 0);
	assert_strcmp(mock_line[0], "RPL_NAMEREPLY: channel '#x' not found");

	channel_reset(c);
	CHECK_REQUEST("353 me = #chan :+@n1", 1, 1, 0,
		"RPL_NAMEREPLY: invalid nick: '@n1'", "");
	CHECK_RECV("353 me x #c1 :n1", 1, 1, 0);
	assert_strcmp(mock_line[0], "RPL_NAMEREPLY: invalid channel flag: 'x'");

	channel_reset(c);
	CHECK_REQUEST("353 me = #chan :n1 n2 n1", 1, 1, 0,
		"RPL_NAMEREPLY: duplicate nick: 'n1'", "");
	CHECK_RECV("353 me = #c1 :+@", 1, 1, 0);
	assert_strcmp(mock_line[0], "RPL_NAMEREPLY: invalid nick: '+@'");

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

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

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

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

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

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

	assert_eq(u1->prfxmodes.lower, (flag_bit('o')));


@@ 110,13 100,14 @@ test_353(void)

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

	if (!(u1 = user_list_get(&(c->users), CASEMAPPING_RFC1459, "n1", 0))
	 || !(u2 = user_list_get(&(c->users), CASEMAPPING_RFC1459, "n2", 0))
	 || !(u3 = user_list_get(&(c->users), CASEMAPPING_RFC1459, "n3", 0))
	 || !(u4 = user_list_get(&(c->users), CASEMAPPING_RFC1459, "n4", 0)))
	channel_reset(c1);
	CHECK_RECV("353 me = #c1 :@n1 +n2 @+n3 +@n4", 0, 0, 0);

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

	assert_eq(u1->prfxmodes.prefix, '@');


@@ 127,16 118,707 @@ test_353(void)
	assert_eq(u2->prfxmodes.lower, (flag_bit('v')));
	assert_eq(u3->prfxmodes.lower, (flag_bit('o') | flag_bit('v')));
	assert_eq(u4->prfxmodes.lower, (flag_bit('o') | flag_bit('v')));
}

	server_free(s);
static void
test_recv_error(void)
{
	/* ERROR :<message> */

	server_reset(s);

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

	CHECK_RECV("ERROR error", 0, 1, 0);
	assert_strcmp(mock_line[0], "error");

	s->quitting = 0;
	CHECK_RECV("ERROR :error message", 0, 1, 0);
	assert_strcmp(mock_line[0], "error message");

	s->quitting = 1;
	CHECK_RECV("ERROR :error message", 0, 1, 0);
	assert_strcmp(mock_line[0], "error message");
}

static void
test_recv_invite(void)
{
	/* :nick!user@host INVITE <nick> <channel> */

	channel_reset(c1);
	server_reset(s);

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

	CHECK_RECV(":nick1!user@host INVITE", 1, 1, 0);
	assert_strcmp(mock_line[0], "INVITE: nick is null");

	CHECK_RECV(":nick1!user@host INVITE nick", 1, 1, 0);
	assert_strcmp(mock_line[0], "INVITE: channel is null");

	CHECK_RECV(":nick1!user@host INVITE nick #notfound", 1, 1, 0);
	assert_strcmp(mock_line[0], "INVITE: channel '#notfound' not found");

	CHECK_RECV(":nick1!user@host INVITE me #c1", 0, 1, 0);
	assert_strcmp(mock_chan[0], "host");
	assert_strcmp(mock_line[0], "nick1 invited you to #c1");

	CHECK_RECV(":nick1!user@host INVITE invitee #c1", 0, 1, 0);
	assert_strcmp(mock_chan[0], "#c1");
	assert_strcmp(mock_line[0], "nick1 invited invitee to #c1");
}

static void
test_recv_join(void)
{
	/* :nick!user@host JOIN <channel>
	 * :nick!user@host JOIN <channel> <account> :<realname> */

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

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

	join_threshold = 0;

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

	CHECK_RECV(":nick!user@host JOIN", 1, 1, 0);
	assert_strcmp(mock_line[0], "JOIN: channel is null");

	CHECK_RECV(":nick!user@host JOIN #notfound", 1, 1, 0);
	assert_strcmp(mock_line[0], "JOIN: channel '#notfound' not found");

	CHECK_RECV(":nick1!user@host JOIN #c1", 1, 1, 0);
	assert_strcmp(mock_chan[0], "host");
	assert_strcmp(mock_line[0], "JOIN: user 'nick1' already on channel '#c1'");

	CHECK_RECV(":nick2!user@host JOIN #c1", 0, 1, 0);
	assert_strcmp(mock_chan[0], "#c1");
	assert_strcmp(mock_line[0], "nick2!user@host has joined");

	s->ircv3_caps.extended_join.set = 0;

	CHECK_RECV(":nick3!user@host JOIN #c1 account :real name", 0, 1, 0);
	assert_strcmp(mock_chan[0], "#c1");
	assert_strcmp(mock_line[0], "nick3!user@host has joined");

	s->ircv3_caps.extended_join.set = 1;

	CHECK_RECV(":nick4!user@host JOIN #c1", 1, 1, 0);
	assert_strcmp(mock_chan[0], "host");
	assert_strcmp(mock_line[0], "JOIN: account is null");

	CHECK_RECV(":nick5!user@host JOIN #c1 account", 1, 1, 0);
	assert_strcmp(mock_chan[0], "host");
	assert_strcmp(mock_line[0], "JOIN: realname is null");

	CHECK_RECV(":nick6!user@host JOIN #c1 account :real name", 0, 1, 0);
	assert_strcmp(mock_chan[0], "#c1");
	assert_strcmp(mock_line[0], "nick6!user@host has joined [account - real name]");

	join_threshold = 2;

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

	CHECK_RECV(":me!user@host JOIN #new", 0, 1, 1);
	assert_strcmp(mock_chan[0], "#new");
	assert_strcmp(mock_line[0], "Joined #new");
	assert_strcmp(mock_send[0], "MODE #new");
	assert_ptr_not_null(channel_list_get(&s->clist, "#new", s->casemapping));
}

static void
test_recv_kick(void)
{
	/* :nick!user@host KICK <channel> <user> [:message] */

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

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

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

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

	CHECK_RECV(":nick!user@host KICK", 1, 1, 0);
	assert_strcmp(mock_line[0], "KICK: channel is null");

	CHECK_RECV(":nick!user@host KICK #c1", 1, 1, 0);
	assert_strcmp(mock_line[0], "KICK: user is null");

	CHECK_RECV(":nick!user@host KICK #notfound nick1", 1, 1, 0);
	assert_strcmp(mock_line[0], "KICK: channel '#notfound' not found");

	/* no message */
	CHECK_RECV(":nick!user@host KICK #c1 nick1", 0, 1, 0);
	assert_strcmp(mock_chan[0], "#c1");
	assert_strcmp(mock_line[0], "nick has kicked nick1");
	assert_ptr_null(user_list_get(&(c1->users), s->casemapping, "nick1", 0));

	/* empty message */
	CHECK_RECV(":nick!user@host KICK #c1 nick2 :", 0, 1, 0);
	assert_strcmp(mock_chan[0], "#c1");
	assert_strcmp(mock_line[0], "nick has kicked nick2");
	assert_ptr_null(user_list_get(&(c1->users), s->casemapping, "nick2", 0));

	CHECK_RECV(":nick!user@host KICK #c1 nick3 :kick message", 0, 1, 0);
	assert_strcmp(mock_chan[0], "#c1");
	assert_strcmp(mock_line[0], "nick has kicked nick3 (kick message)");
	assert_ptr_null(user_list_get(&(c1->users), s->casemapping, "nick3", 0));

	/* no message */
	CHECK_RECV(":nick!user@host KICK #c1 me", 0, 1, 0);
	assert_strcmp(mock_chan[0], "#c1");
	assert_strcmp(mock_line[0], "Kicked by nick");
	assert_eq(c1->parted, 1);

	/* empty message */
	CHECK_RECV(":nick!user@host KICK #c2 me :", 0, 1, 0);
	assert_strcmp(mock_chan[0], "#c2");
	assert_strcmp(mock_line[0], "Kicked by nick");
	assert_eq(c2->parted, 1);

	CHECK_RECV(":nick!user@host KICK #c3 me :kick message", 0, 1, 0);
	assert_strcmp(mock_chan[0], "#c3");
	assert_strcmp(mock_line[0], "Kicked by nick (kick message)");
	assert_eq(c3->parted, 1);
}

static void
test_recv_mode(void)
{
	/* TODO */
}

static void
test_recv_mode_chanmodes(void)
{
	/* TODO */
}

static void
test_recv_mode_usermodes(void)
{
	/* TODO */
}

static void
test_recv_nick(void)
{
	/* :nick!user@host NICK <nick> */

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

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

	CHECK_RECV("NICK new_nick", 1, 1, 0);
	assert_strcmp(mock_chan[0], "host");
	assert_strcmp(mock_line[0], "NICK: old nick is null");

	CHECK_RECV(":nick1!user@host NICK", 1, 1, 0);
	assert_strcmp(mock_chan[0], "host");
	assert_strcmp(mock_line[0], "NICK: new nick is null");

	/* user not on channels */
	CHECK_RECV(":nick3!user@host NICK new_nick", 0, 0, 0);

	CHECK_RECV(":nick1!user@host NICK new_nick", 0, 2, 0);
	assert_strcmp(mock_chan[0], "#c1");
	assert_strcmp(mock_line[0], "nick1  >>  new_nick");
	assert_strcmp(mock_chan[1], "#c3");
	assert_strcmp(mock_line[1], "nick1  >>  new_nick");
	assert_ptr_null(user_list_get(&(c1->users), s->casemapping, "nick1", 0));
	assert_ptr_null(user_list_get(&(c3->users), s->casemapping, "nick1", 0));

	CHECK_RECV(":nick2!user@host NICK new_nick", 0, 2, 0);
	assert_strcmp(mock_chan[0], "host");
	assert_strcmp(mock_line[0], "NICK: user 'new_nick' already on channel '#c1'");
	assert_strcmp(mock_chan[1], "host");
	assert_strcmp(mock_line[1], "NICK: user 'new_nick' already on channel '#c3'");

	CHECK_RECV(":me!user@host NICK new_me", 0, 1, 0);
	assert_strcmp(mock_chan[0], "host");
	assert_strcmp(mock_line[0], "Your nick is now 'new_me'");

	/* user can change own nick case */
	assert_eq(user_list_add(&(c1->users), CASEMAPPING_RFC1459, "abc{}|^", MODE_EMPTY), USER_ERR_NONE);

	CHECK_RECV(":abc{}|^!user@host NICK AbC{]|~", 0, 1, 0);
	assert_strcmp(mock_chan[0], "#c1");
	assert_strcmp(mock_line[0], "abc{}|^  >>  AbC{]|~");

	server_nick_set(s, "me");
}

static void
test_recv_notice(void)
{
	/* TODO */
}

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

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

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

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

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

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

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

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

	CHECK_RECV(":hostname 001", 1, 1, 0);
	assert_strcmp(mock_line[0], "NUMERIC: target is null");

	CHECK_RECV(":hostname 001 test", 1, 1, 0);
	assert_strcmp(mock_line[0], "NUMERIC: target 'test' is invalid");

	CHECK_RECV(":hostname 666 me arg1 arg2 :trailing arg", 1, 1, 0);
	assert_strcmp(mock_line[0], "NUMERIC: 666 unhandled: [arg1 arg2 :trailing arg]");

	CHECK_RECV(":hostname 666 me", 1, 1, 0);
	assert_strcmp(mock_line[0], "NUMERIC: 666 unhandled");

	/* Numeric 375 (RPL_MOTDSTART) is ignored */
	CHECK_RECV(":hostname 375 me :trailing arg", 0, 0, 0);
}

static void
test_recv_part(void)
{
	/* :nick!user@host PART <channel> [:message] */

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

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

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

	part_threshold = 0;

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

	CHECK_RECV(":nick1!user@host PART", 1, 1, 0);
	assert_strcmp(mock_line[0], "PART: channel is null");

	CHECK_RECV(":nick1!user@host PART #notfound :quit message", 1, 1, 0);
	assert_strcmp(mock_line[0], "PART: channel '#notfound' not found");

	CHECK_RECV(":nick6!user@host PART #c1 :part message", 1, 1, 0);
	assert_strcmp(mock_line[0], "PART: nick 'nick6' not found in '#c1'");

	CHECK_RECV(":nick1!user@host PART #c1 :part message", 0, 1, 0);
	assert_strcmp(mock_chan[0], "#c1");
	assert_strcmp(mock_line[0], "nick1!user@host has parted (part message)");
	assert_ptr_null(user_list_get(&(c1->users), s->casemapping, "nick1", 0));

	/* no message */
	CHECK_RECV(":nick2!user@host PART #c1", 0, 1, 0);
	assert_strcmp(mock_chan[0], "#c1");
	assert_strcmp(mock_line[0], "nick2!user@host has parted");
	assert_ptr_null(user_list_get(&(c1->users), s->casemapping, "nick2", 0));

	/* empty message */
	CHECK_RECV(":nick3!user@host PART #c1 :", 0, 1, 0);
	assert_strcmp(mock_chan[0], "#c1");
	assert_strcmp(mock_line[0], "nick3!user@host has parted");
	assert_ptr_null(user_list_get(&(c1->users), s->casemapping, "nick3", 0));

	part_threshold = 1;

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

	/* channel not found, assume closed */
	CHECK_RECV(":me!user@host PART #notfound", 0, 0, 0);

	/* no message */
	CHECK_RECV(":me!user@host PART #c1", 0, 1, 0);
	assert_strcmp(mock_chan[0], "#c1");
	assert_strcmp(mock_line[0], "you have parted");
	assert_eq(c1->parted, 1);

	/* empty message */
	CHECK_RECV(":me!user@host PART #c2 :", 0, 1, 0);
	assert_strcmp(mock_chan[0], "#c2");
	assert_strcmp(mock_line[0], "you have parted");
	assert_eq(c2->parted, 1);

	CHECK_RECV(":me!user@host PART #c3 message", 0, 1, 0);
	assert_strcmp(mock_chan[0], "#c3");
	assert_strcmp(mock_line[0], "you have parted (message)");
	assert_eq(c3->parted, 1);
}

static void
test_recv_ping(void)
{
	/* PING <server> */

	server_reset(s);

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

	CHECK_RECV("PING server", 0, 0, 1);
	assert_strcmp(mock_send[0], "PONG server");
}

static void
test_recv_pong(void)
{
	/* PONG <server> [<server2>] */

	server_reset(s);

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

static void
test_recv_privmsg(void)
{
	/* TODO */
}

static void
test_recv_quit(void)
{
	/* :nick!user@host QUIT [:message] */

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

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

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

	quit_threshold = 0;

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

	/* user not on channels */
	CHECK_RECV(":nick6!user@host QUIT :quit message", 0, 0, 0);

	CHECK_RECV(":nick2!user@host QUIT :quit message", 0, 2, 0);
	assert_strcmp(mock_chan[0], "#c1");
	assert_strcmp(mock_line[0], "nick2!user@host has quit (quit message)");
	assert_strcmp(mock_chan[1], "#c3");
	assert_strcmp(mock_line[1], "nick2!user@host has quit (quit message)");
	assert_ptr_null(user_list_get(&(c1->users), s->casemapping, "nick2", 0));
	assert_ptr_null(user_list_get(&(c3->users), s->casemapping, "nick2", 0));

	/* no message */
	CHECK_RECV(":nick3!user@host QUIT", 0, 1, 0);
	assert_strcmp(mock_chan[0], "#c1");
	assert_strcmp(mock_line[0], "nick3!user@host has quit");
	assert_ptr_null(user_list_get(&(c1->users), s->casemapping, "nick3", 0));

	/* empty message */
	CHECK_RECV(":nick4!user@host QUIT :", 0, 1, 0);
	assert_strcmp(mock_chan[0], "#c1");
	assert_strcmp(mock_line[0], "nick4!user@host has quit");
	assert_ptr_null(user_list_get(&(c1->users), s->casemapping, "nick4", 0));

	quit_threshold = 1;

	/* c1 = {nick1, nick5}, c3 = {nick1} */
	CHECK_RECV(":nick1!user@host QUIT", 0, 1, 0);
	assert_strcmp(mock_chan[0], "#c3");
	assert_strcmp(mock_line[0], "nick1!user@host has quit");
	assert_ptr_null(user_list_get(&(c1->users), s->casemapping, "nick1", 0));
	assert_ptr_null(user_list_get(&(c3->users), s->casemapping, "nick1", 0));
}

static void
test_recv_topic(void)
{
	/* :nick!user@host TOPIC <channel> [:topic] */

	channel_reset(c1);
	server_reset(s);

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

	CHECK_RECV(":nick1!user@host TOPIC", 1, 1, 0);
	assert_strcmp(mock_line[0], "TOPIC: channel is null");

	CHECK_RECV(":nick1!user@host TOPIC #c1", 1, 1, 0);
	assert_strcmp(mock_line[0], "TOPIC: topic is null");

	CHECK_RECV(":nick1!user@host TOPIC #notfound message", 1, 1, 0);
	assert_strcmp(mock_line[0], "TOPIC: channel '#notfound' not found");

	CHECK_RECV(":nick1!user@host TOPIC #c1 message", 0, 2, 0);
	assert_strcmp(mock_chan[0], "#c1");
	assert_strcmp(mock_chan[1], "#c1");
	assert_strcmp(mock_line[0], "nick1 has set the topic:");
	assert_strcmp(mock_line[1], "\"message\"");

	CHECK_RECV(":nick1!user@host TOPIC #c1 :topic message", 0, 2, 0);
	assert_strcmp(mock_chan[0], "#c1");
	assert_strcmp(mock_chan[1], "#c1");
	assert_strcmp(mock_line[0], "nick1 has set the topic:");
	assert_strcmp(mock_line[1], "\"topic message\"");

	CHECK_RECV(":nick1!user@host TOPIC #c1 :", 0, 1, 0);
	assert_strcmp(mock_chan[0], "#c1");
	assert_strcmp(mock_line[0], "nick1 has unset the topic");
}

static void
test_recv_ircv3_cap(void)
{
	/* Full IRCv3 CAP coverage in test/handlers/ircv3.c */

	server_reset(s);

	s->registered = 1;

	CHECK_RECV("CAP * LS :cap-1 cap-2 cap-3", 0, 1, 0);
	assert_strcmp(mock_chan[0], "host");
	assert_strcmp(mock_line[0], "CAP LS: cap-1 cap-2 cap-3");
}

static void
test_recv_ircv3_account(void)
{
	/* :nick!user@host ACCOUNT <account> */

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

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

	account_threshold = 0;

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

	CHECK_RECV(":nick1!user@host ACCOUNT", 1, 1, 0);
	assert_strcmp(mock_line[0], "ACCOUNT: account is null");

	/* user not on channels */
	CHECK_RECV(":nick3!user@host ACCOUNT account", 0, 0, 0);

	/* logging in */
	CHECK_RECV(":nick1!user@host ACCOUNT account", 0, 2, 0);
	assert_strcmp(mock_chan[0], "#c1");
	assert_strcmp(mock_line[0], "nick1 has logged in as account");
	assert_strcmp(mock_chan[1], "#c3");
	assert_strcmp(mock_line[1], "nick1 has logged in as account");

	/* logging out */
	CHECK_RECV(":nick1!user@host ACCOUNT *", 0, 2, 0);
	assert_strcmp(mock_chan[0], "#c1");
	assert_strcmp(mock_line[0], "nick1 has logged out");
	assert_strcmp(mock_chan[1], "#c3");
	assert_strcmp(mock_line[1], "nick1 has logged out");

	account_threshold = 2;

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

static void
test_recv_ircv3_away(void)
{
	/* :nick!user@host AWAY [:message] */

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

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

	away_threshold = 0;

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

	/* user not on channels */
	CHECK_RECV(":nick3!user@host AWAY :away message", 0, 0, 0);

	/* away set */
	CHECK_RECV(":nick1!user@host AWAY :away message", 0, 2, 0);
	assert_strcmp(mock_chan[0], "#c1");
	assert_strcmp(mock_line[0], "nick1 is now away: away message");
	assert_strcmp(mock_chan[1], "#c3");
	assert_strcmp(mock_line[1], "nick1 is now away: away message");

	/* away unset */
	CHECK_RECV(":nick1!user@host AWAY", 0, 2, 0);
	assert_strcmp(mock_chan[0], "#c1");
	assert_strcmp(mock_line[0], "nick1 is no longer away");
	assert_strcmp(mock_chan[1], "#c3");
	assert_strcmp(mock_line[1], "nick1 is no longer away");

	away_threshold = 2;

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

static void
test_recv_ircv3_chghost(void)
{
	/* :nick!user@host CHGHOST new_user new_host */

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

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

	chghost_threshold = 0;

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

	CHECK_RECV(":nick1!user@host CHGHOST", 1, 1, 0);
	assert_strcmp(mock_line[0], "CHGHOST: user is null");

	CHECK_RECV(":nick1!user@host CHGHOST new_user", 1, 1, 0);
	assert_strcmp(mock_line[0], "CHGHOST: host is null");

	/* user not on channels */
	CHECK_RECV(":nick3!user@host CHGHOST new_user new_host", 0, 0, 0);

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

	chghost_threshold = 2;

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

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

	if (!s || !c1 || !c2 || !c3)
		test_abort_main("Failed test setup");

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

	server_nick_set(s, "me");

	struct testcase tests[] = {
		TESTCASE(test_353)
		TESTCASE(test_irc_353),
		TESTCASE(test_recv_error),
		TESTCASE(test_recv_invite),
		TESTCASE(test_recv_join),
		TESTCASE(test_recv_kick),
		TESTCASE(test_recv_mode),
		TESTCASE(test_recv_mode_chanmodes),
		TESTCASE(test_recv_mode_usermodes),
		TESTCASE(test_recv_nick),
		TESTCASE(test_recv_notice),
		TESTCASE(test_recv_numeric),
		TESTCASE(test_recv_part),
		TESTCASE(test_recv_ping),
		TESTCASE(test_recv_pong),
		TESTCASE(test_recv_privmsg),
		TESTCASE(test_recv_quit),
		TESTCASE(test_recv_topic),
		TESTCASE(test_recv_ircv3_cap),
		TESTCASE(test_recv_ircv3_account),
		TESTCASE(test_recv_ircv3_away),
		TESTCASE(test_recv_ircv3_chghost)
	};

	return run_tests(tests);
	int ret = run_tests(tests);

	server_free(s);

	return ret;
}

M test/handlers/irc_send.c => test/handlers/irc_send.c +51 -21
@@ 110,6 110,18 @@ test_irc_send_privmsg(void)
}

static void
test_send_away(void)
{
	char m1[] = "away";
	char m2[] = "away ";
	char m3[] = "away testing away message";

	CHECK_SEND_COMMAND(c_chan, m1, 0, 0, 1, "", "AWAY");
	CHECK_SEND_COMMAND(c_chan, m2, 0, 0, 1, "", "AWAY");
	CHECK_SEND_COMMAND(c_chan, m3, 0, 0, 1, "", "AWAY :testing away message");
}

static void
test_send_notice(void)
{
	char m1[] = "notice";


@@ 184,15 196,33 @@ test_send_topic(void)
}

static void
test_send_ctcp_action(void)
test_send_topic_unset(void)
{
	char m1[] = "ctcp-action test action";
	char m2[] = "ctcp-action test action";
	char m3[] = "ctcp-action test action";
	char m1[] = "topic-unset";
	char m2[] = "topic-unset";
	char m3[] = "topic-unset test";
	char m4[] = "topic-unset";
	char m5[] = "topic-unset ";

	CHECK_SEND_COMMAND(c_chan, m1, 0, 0, 1, "", "PRIVMSG chan :\001ACTION test action\001");
	CHECK_SEND_COMMAND(c_priv, m2, 0, 0, 1, "", "PRIVMSG priv :\001ACTION test action\001");
	CHECK_SEND_COMMAND(c_serv, m3, 1, 1, 0, "This is not a channel", "");
	CHECK_SEND_COMMAND(c_serv, m1, 1, 1, 0, "This is not a channel", "");
	CHECK_SEND_COMMAND(c_priv, m2, 1, 1, 0, "This is not a channel", "");
	CHECK_SEND_COMMAND(c_chan, m3, 1, 1, 0, "Usage: /topic-unset", "");
	CHECK_SEND_COMMAND(c_chan, m4, 0, 0, 1, "", "TOPIC chan :");
	CHECK_SEND_COMMAND(c_chan, m5, 0, 0, 1, "", "TOPIC chan :");
}

static void
test_send_ctcp_action(void)
{
	char m1[] = "ctcp-action";
	char m2[] = "ctcp-action target";
	char m3[] = "ctcp-action target ";
	char m4[] = "ctcp-action target action message";

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

static void


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

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


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

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


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

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

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


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

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


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

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


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

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


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

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

M test/io.mock.c => test/io.mock.c +35 -3
@@ 6,6 6,7 @@
static char mock_send[MOCK_SEND_N][MOCK_SEND_LEN];
static unsigned mock_send_i;
static unsigned mock_send_n;
static int cxed;

void
mock_reset_io(void)


@@ 13,6 14,7 @@ mock_reset_io(void)
	mock_send_i = 0;
	mock_send_n = 0;
	memset(mock_send, 0, MOCK_SEND_LEN * MOCK_SEND_N);
	cxed = 0;
}

int


@@ 44,9 46,39 @@ connection(const void *o, const char *h, const char *p, uint32_t f)
	return NULL;
}

const char* io_err(int err) { UNUSED(err); return "err"; }
int io_cx(struct connection *c) { UNUSED(c); return 0; }
int io_dx(struct connection *c) { UNUSED(c); return 0; }
int
io_cx(struct connection *c)
{
	UNUSED(c);

	if (cxed)
		return -1;

	cxed = 1;
	return 0;
}

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

	if (cxed) {
		cxed = 0;
		return 0;
	}

	return -1;
}

const char*
io_err(int err)
{
	UNUSED(err);

	return (cxed ? "cxed" : "dxed");
}

unsigned io_tty_cols(void) { return 0; }
unsigned io_tty_rows(void) { return 0; }
void connection_free(struct connection *c) { UNUSED(c); }

M test/state.c => test/state.c +264 -9
@@ 18,10 18,254 @@

#define INP_S(S) io_cb_read_inp((S), strlen(S))
#define INP_C(C) io_cb_read_inp((char[]){(C)}, 1)
#define CURRENT_LINE (buffer_head(&current_channel()->buffer)->text)

// TODO: tests for
// sending certain /commands to private buffer, server buffer
#define CURRENT_LINE \
	(buffer_head(&current_channel()->buffer) ? \
	 buffer_head(&current_channel()->buffer)->text : NULL)

static void
test_command(void)
{
	state_init();

	INP_S(":unknown command with args");
	INP_C(0x0A);

	assert_strcmp(action_message(), "Unknown command 'unknown'");

	/* clear error */
	INP_C(0x0A);

	state_term();
}

static void
test_command_clear(void)
{
	state_init();

	assert_strcmp(CURRENT_LINE, " - compiled with DEBUG flags");

	INP_S(":clear with args");
	INP_C(0x0A);

	assert_strcmp(action_message(), "clear: Unknown arg 'with'");

	/* clear error */
	INP_C(0x0A);

	assert_strcmp(CURRENT_LINE, " - compiled with DEBUG flags");

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

	assert_ptr_null(CURRENT_LINE);

	state_term();
}

static void
test_command_close(void)
{
	static struct channel *c1;
	static struct channel *c2;
	struct server *s;

	state_init();

	assert_strcmp(CURRENT_LINE, " - compiled with DEBUG flags");

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

	assert_strcmp(action_message(), "Type :quit to exit rirc");

	/* clear error */
	INP_C(0x0A);

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

	if (!s || !c1 || !c2)
		test_abort("Failed to create server and channels");

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

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

	channel_set_cu