~rcr/rirc

f2dfce91bff34513bbae70626dd4d1b28c068ff3 — Richard Robbins 2 months ago d9cde15 + 44c19ad master v0.1.5
Merge branch 'static_analysis'
65 files changed, 1732 insertions(+), 1140 deletions(-)

R .builds/{alpine.yml => compat/alpine.yml}
A .builds/compat/fedora.yml
A .builds/compat/freebsd.yml
A .builds/compat/openbsd.yml
R .builds/{debian.yml => dev.yml}
A .gitattributes
M .gitignore
D .gitmodules
M Makefile
M README.md
M config.def.h
R rirc.1 => docs/rirc.1
D lib/mbedtls
A lib/mbedtls.Makefile
M lib/mbedtls.h
A scripts/build.sh
D scripts/compile_commands.sh
M scripts/coverage.sh
M scripts/sa_coverity_get.sh
M scripts/sa_coverity_run.sh
R scripts/{sa_sonarcloud_get.sh => sa_sonarscan_get.sh}
R scripts/{sa_sonarcloud_run.sh => sa_sonarscan_run.sh}
M scripts/sanitizers_build.sh
M scripts/sanitizers_test.sh
M scripts/stack_usage.sh
M src/components/buffer.c
M src/components/buffer.h
M src/components/channel.h
M src/components/input.c
M src/components/mode.c
M src/components/mode.h
M src/components/server.c
M src/components/server.h
M src/components/user.h
M src/draw.c
M src/draw.h
M src/handlers/irc_ctcp.c
M src/handlers/irc_recv.c
M src/handlers/irc_send.c
M src/handlers/irc_send.gperf
M src/handlers/irc_send.h
M src/io.c
M src/rirc.c
M src/state.c
M src/state.h
M src/utils/utils.c
M src/utils/utils.h
M test/components/buffer.c
M test/components/channel.c
M test/components/input.c
M test/components/ircv3.c
M test/components/mode.c
M test/components/server.c
M test/components/user.c
M test/draw.c
M test/handlers/irc_ctcp.c
M test/handlers/irc_recv.c
M test/handlers/irc_send.c
M test/handlers/irc_send.mock.c
M test/handlers/ircv3.c
M test/rirc.c
M test/state.c
M test/test.h
M test/utils/tree.c
M test/utils/utils.c
R .builds/alpine.yml => .builds/compat/alpine.yml +4 -9
@@ 1,23 1,18 @@
image: alpine/edge
image: alpine/latest

packages:
  - cmake
  - curl
  - gperf
  - perl

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

tasks:
  - setup: |
      cd rirc
      git submodule init
      git submodule update --recursive
      export MAKEFLAGS='-j $(nproc)'
  - build: |
      cd rirc
      make clean
      make all
  - check: |
      cd rirc
      make check

triggers:

A .builds/compat/fedora.yml => .builds/compat/fedora.yml +21 -0
@@ 0,0 1,21 @@
image: fedora/latest

packages:
  - curl
  - gperf

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

tasks:
  - build: |
      cd rirc
      make all
  - check: |
      cd rirc
      make check

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

A .builds/compat/freebsd.yml => .builds/compat/freebsd.yml +22 -0
@@ 0,0 1,22 @@
image: freebsd/latest

packages:
  - curl
  - gmake
  - gperf

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

tasks:
  - build: |
      cd rirc
      gmake all
  - check: |
      cd rirc
      gmake check

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

A .builds/compat/openbsd.yml => .builds/compat/openbsd.yml +22 -0
@@ 0,0 1,22 @@
image: openbsd/latest

packages:
  - curl
  - gmake
  - gperf

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

tasks:
  - build: |
      cd rirc
      gmake all
  - check: |
      cd rirc
      gmake check

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

R .builds/debian.yml => .builds/dev.yml +4 -10
@@ 1,10 1,8 @@
image: debian/stable

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

sources:


@@ 15,15 13,11 @@ secrets:
  - a58d0951-f57f-44ef-8ef2-25f2f84f0e89

tasks:
  - setup: |
      cd rirc
      git submodule init
      git submodule update --recursive
      export MAKEFLAGS='-j $(nproc)'
  - build: |
      cd rirc
      make clean
      make all
  - check: |
      cd rirc
      make check
  - static-analysis: |
      cd rirc


@@ 34,8 28,8 @@ tasks:
      set -x
      ./scripts/sa_coverity_get.sh coverity
      ./scripts/sa_coverity_run.sh coverity
      ./scripts/sa_sonarcloud_get.sh sonarcloud
      ./scripts/sa_sonarcloud_run.sh sonarcloud
      ./scripts/sa_sonarscan.sh sonarscan
      ./scripts/sa_sonarscan.sh sonarscan

triggers:
  - action: email

A .gitattributes => .gitattributes +3 -0
@@ 0,0 1,3 @@
.builds        export-ignore
.gitignore     export-ignore
.gitattributes export-ignore

M .gitignore => .gitignore +5 -4
@@ 3,17 3,18 @@
*.gcov
*.gperf.out
*.o
*.out
*.t
*.td
.clangd
.cache
bld
compile_commands.json
build
config.h
coverage
lib/mbedtls-*
lib/mbedtls.sha256
rirc
rirc.1
rirc.debug
rirc.debug.address
rirc.debug.thread
rirc.out
tags

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

M Makefile => Makefile +86 -104
@@ 1,123 1,105 @@
VERSION := 0.1.4

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

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

TLS_CONF := $(PWD)/lib/mbedtls.h
TLS_INCL := -I $(PWD)/lib/mbedtls/include/ -DMBEDTLS_CONFIG_FILE='<$(TLS_CONF)>'
TLS_LIBS := ./lib/mbedtls/library/libmbedtls.a \
            ./lib/mbedtls/library/libmbedx509.a \
            ./lib/mbedtls/library/libmbedcrypto.a

STDS := -std=c11 -D_POSIX_C_SOURCE=200809L

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

# Build, source, test source directories
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)

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

# Release, debug, testcase build objects
OBJS_D := $(patsubst $(DIR_S)/%.c, $(DIR_B)/%.db.o, $(SRC))
OBJS_R := $(patsubst $(DIR_S)/%.c, $(DIR_B)/%.o,    $(SRC))
OBJS_T := $(patsubst $(DIR_S)/%.c, $(DIR_B)/%.t,    $(SRC))
OBJS_T += $(DIR_B)/utils/tree.t # Header only file

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

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

# Debug build executable
$(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)
	@echo " CC  $<"
	@$(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)
	@echo " CC  $<"
	@$(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) ./$@ || mv $@ $(@:.t=.td)
	@[ -f $@ ]

config.h:
VERSION := 0.1.5

PREFIX   ?= /usr/local
PATH_BIN := $(DESTDIR)$(PREFIX)/bin
PATH_MAN := $(DESTDIR)$(PREFIX)/share/man/man1

PATH_BUILD := build
PATH_LIB   := lib
PATH_SRC   := src
PATH_TEST  := test

include lib/mbedtls.Makefile

CONFIG := config.h

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

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

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

LDFLAGS ?= -flto
LDFLAGS += -lpthread

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

# Release objects, debug objects, testcases
OBJS_R := $(patsubst $(PATH_SRC)/%.c, $(PATH_BUILD)/%.o,    $(SRC))
OBJS_D := $(patsubst $(PATH_SRC)/%.c, $(PATH_BUILD)/%.db.o, $(SRC))
OBJS_T := $(patsubst $(PATH_SRC)/%.c, $(PATH_BUILD)/%.t,    $(SRC))
OBJS_T += $(PATH_BUILD)/utils/tree.t # Header only file

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

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

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

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

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

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

$(CONFIG):
	cp config.def.h config.h

%.gperf.out: %.gperf
	gperf --output-file=$@ $<

# 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) --silent -C ./lib/mbedtls clean
	@CFLAGS="$(TLS_INCL)" $(MAKE) --silent -C ./lib/mbedtls lib

all:
	@$(MAKE) --silent $(BIN_R)
	@$(MAKE) --silent $(BIN_D)
	@$(MAKE) --silent rirc
	@$(MAKE) --silent rirc.debug

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

clean:
	@rm -rf $(DIR_B)
	@rm -vf $(BIN_R) $(BIN_D) $(OBJS_G)
	@rm -rfv rirc rirc.debug $(SRC_GPERF) $(PATH_BUILD)

libs: $(TLS_LIBS)
libs:
	@$(MAKE) --silent $(RIRC_LIBS)

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

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

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

.DEFAULT_GOAL := rirc

.PHONY: all check clean libs install uninstall

.SUFFIXES:

M README.md => README.md +13 -23
@@ 30,40 30,30 @@ A minimalistic irc client written in C.

Connections are TLS enabled over port 6697 by default.

### Configuring:
## Building, installing:

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

### Building:

rirc requires the latest version of GNU gperf to compile.
Building rirc from source requires a c11 compiler, GNU gperf and GNU make.

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

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

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

### Installing:

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

```
BIN_DIR = /usr/local/bin
MAN_DIR = /usr/local/share/man/man1
CC, CFLAGS, LDFLAGS, DESTDIR, PREFIX
```

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

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

### Usage:
## Usage:

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


@@ 114,6 104,6 @@ Keys:
   ↓ : input history forward
```

### More info:
## More info:

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

M config.def.h => config.def.h +31 -11
@@ 3,11 3,11 @@
 * Colours can be set [0, 255], Any other value (e.g. -1) will set
 * the default terminal foreground/background */

/* Default comma separated set of Nicks to try on connection
/* Comma separated set of default nicks to try on connection
 *   String
 *   ("": defaults to effective user id name)
 */
#define DEFAULT_NICK_SET ""
#define DEFAULT_NICKS ""

/* Default Username and Realname sent during connection
 *   String


@@ 30,13 30,14 @@
#define DEFAULT_QUIT_MESG "rirc v" VERSION
#define DEFAULT_PART_MESG "rirc v" VERSION

#define BUFFER_LINE_HEADER_FG_NEUTRAL 239

/* Buffer colours */
#define BUFFER_LINE_HEADER_FG         239
#define BUFFER_LINE_HEADER_BG         -1
#define BUFFER_LINE_HEADER_FG_PINGED  250
#define BUFFER_LINE_HEADER_BG_PINGED  1

#define BUFFER_LINE_TEXT_FG_NEUTRAL 250
#define BUFFER_LINE_TEXT_FG_GREEN   113
#define BUFFER_TEXT_FG 250;
#define BUFFER_TEXT_BG -1;

/* Number of buffer lines to keep in history, must be power of 2 */
#define BUFFER_LINES_MAX (1 << 10)


@@ 54,10 55,17 @@

#define NAV_CURRENT_CHAN 255

/* Characters */
#define QUOTE_CHAR '>'
#define HORIZONTAL_SEPARATOR "-"
#define VERTICAL_SEPARATOR "~"
/* Separator characters */
#define SEP_HORZ "─"
#define SEP_VERT "~"

/* Separator colours */
#define SEP_FG 239
#define SEP_BG -1

/* Status bar colours */
#define STATUS_FG -1
#define STATUS_BG -1

/* Prefix string for the input line and colours */
#define INPUT_PREFIX " >>> "


@@ 72,6 80,16 @@
#define INPUT_FG 250
#define INPUT_BG -1

/* Buffer text quoting
 *   ("": no text quoting) */
#define QUOTE_LEADER ">"
#define QUOTE_TEXT_FG 2
#define QUOTE_TEXT_BG -1

/* Control character print colour */
#define CTRL_FG 0
#define CTRL_BG 9

/* BUFFER_PADDING:
 * How the buffer line headers will be padded [0, 1]
 *


@@ 92,7 110,9 @@

/* [NETWORK] */

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

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

R rirc.1 => docs/rirc.1 +2 -2
@@ 112,7 112,7 @@ IRC commands:
  /me;<message>
  /nick;[nick]
  /part;[target [targets...]] [part message]
  /priv;<target> <message>
  /privmsg;<target> <message>
  /quit;[quit message]
  /raw;<message>
.TE


@@ 121,6 121,6 @@ IRC commands:
Richard 'rcr' Robbins
.ME .
.SH SEE ALSO
.UR http://rcr.io/rirc/
.UR https://rcr.io/rirc/
Additional documentation
.UE .

D lib/mbedtls => lib/mbedtls +0 -1
@@ 1,1 0,0 @@
Subproject commit 1c54b5410fd48d6bcada97e30cac417c5c7eea67

A lib/mbedtls.Makefile => lib/mbedtls.Makefile +38 -0
@@ 0,0 1,38 @@
MBEDTLS_VER     := 3.0.0
MBEDTLS_VER_SHA := 525bfde06e024c1218047dee1c8b4c89312df1a4b5658711009086cda5dfaa55

MBEDTLS_CFG := $(abspath $(PATH_LIB)/mbedtls.h)
MBEDTLS_SHA := $(abspath $(PATH_LIB)/mbedtls.sha256)
MBEDTLS_SRC := $(abspath $(PATH_LIB)/mbedtls-$(MBEDTLS_VER))
MBEDTLS_TAR := $(abspath $(PATH_LIB)/mbedtls-$(MBEDTLS_VER).tar.gz)
MBEDTLS_URL := https://github.com/ARMmbed/mbedtls/archive/v$(MBEDTLS_VER).tar.gz

MBEDTLS_LIBS := \
	$(MBEDTLS_SRC)/library/libmbedtls.a \
	$(MBEDTLS_SRC)/library/libmbedx509.a \
	$(MBEDTLS_SRC)/library/libmbedcrypto.a

$(MBEDTLS_LIBS): $(MBEDTLS_CFG) $(MBEDTLS_SRC)
	@$(MAKE) --silent -C $(MBEDTLS_SRC) clean
	@$(MAKE) --silent -C $(MBEDTLS_SRC) CFLAGS="$(CFLAGS) -DMBEDTLS_CONFIG_FILE='<$(MBEDTLS_CFG)>'" lib

$(MBEDTLS_SRC): $(MBEDTLS_TAR)
	@tar -xmf $(MBEDTLS_TAR) --directory $(PATH_LIB)

$(MBEDTLS_TAR):
	@echo "$(MBEDTLS_TAR)..."
	@curl -LfsS $(MBEDTLS_URL) -o $(MBEDTLS_TAR)
	@eval $(MBEDTLS_SHA_FILE)
	@eval $(MBEDTLS_SHA_CHECK)

ifneq ($(shell command -v shasum 2>/dev/null),)
MBEDTLS_SHA_FILE  := 'echo "$(MBEDTLS_VER_SHA)  $(MBEDTLS_TAR)" > $(MBEDTLS_SHA)'
MBEDTLS_SHA_CHECK := 'shasum -c $(MBEDTLS_SHA)'
endif

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

RIRC_LIBS += $(MBEDTLS_LIBS)

.DELETE_ON_ERROR:

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

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


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


@@ 110,7 110,3 @@

/* Error strings */
#define MBEDTLS_ERROR_C

#include "mbedtls/check_config.h"

#endif

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

# Development build script.
#
#  Usage:
#
#   $ ./scripts/build.sh [make targets]

set -e

export CC=clang
export LDFLAGS="-fuse-ld=lld"

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

if [ -x "$(command -v bear)" ]; then
	BEAR="bear --append --output ./build/compile_commands.json --"
fi

make clean
make build

find -name '*.c' \
  -o -name '*.h' \
  -o -name Makefile | grep -v './lib/' | $ENTR $BEAR make -j $(nproc) "$@"

D scripts/compile_commands.sh => scripts/compile_commands.sh +0 -10
@@ 1,10 0,0 @@
#!/bin/bash

set -e

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 +2 -3
@@ 5,8 5,7 @@ set -e
CDIR="coverage"

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

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



@@ 15,7 14,7 @@ rm -rf $CDIR && mkdir -p $CDIR
make clean
make check

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

FILTER=$(cat << 'EOF'
{

M scripts/sa_coverity_get.sh => scripts/sa_coverity_get.sh +4 -4
@@ 21,9 21,9 @@ mkdir -p "$DIR"

echo "*" > "$DIR/.gitignore"

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"
curl -fsS https://scan.coverity.com/download/linux64 -o "$COVERITY_MD5" --data "token=$COVERITY_TOKEN&project=rcr%2Frirc&md5=1"
curl -fsS https://scan.coverity.com/download/linux64 -o "$COVERITY_TGZ" --data "token=$COVERITY_TOKEN&project=rcr%2Frirc"

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

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

M scripts/sa_coverity_run.sh => scripts/sa_coverity_run.sh +1 -1
@@ 30,7 30,7 @@ make libs

cov-build --dir "$COVERITY_OUT" make all check

tar czf "$COVERITY_TAR" "$COVERITY_OUT"
tar -czf "$COVERITY_TAR" "$COVERITY_OUT"

curl https://scan.coverity.com/builds?project=rcr%2Frirc \
	--form description="$VERSION" \

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

SONAR_VER="4.6.0.2311"
SONAR_VER="4.6.2.2472"

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


@@ 22,11 22,11 @@ mkdir -p "$1"

echo "*" > "$1/.gitignore"

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"
curl -fsS "$BUILD_ZIP_URL" -o "$BUILD_ZIP"
curl -fsS "$SONAR_ZIP_URL" -o "$SONAR_ZIP"
curl -fsS "$SONAR_MD5_URL" -o "$SONAR_MD5"

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

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

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

DIR="$1"

SONAR_VER="4.6.0.2311"
SONAR_VER="4.6.2.2472"

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


M scripts/sanitizers_build.sh => scripts/sanitizers_build.sh +6 -6
@@ 4,16 4,16 @@ set -e

export CC=clang

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

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

mv rirc.debug rirc.debug.address

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

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

mv rirc.debug rirc.debug.thread

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

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

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

make -e clean check
make clean check

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

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

make -e clean rirc.debug
make clean rirc

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

M src/components/buffer.c => src/components/buffer.c +11 -11
@@ 12,18 12,18 @@
#error BUFFER_LINES_MAX must be a power of 2
#endif

static inline unsigned int buffer_full(struct buffer*);
static inline unsigned int buffer_size(struct buffer*);
static inline unsigned buffer_full(struct buffer*);
static inline unsigned buffer_size(struct buffer*);

static struct buffer_line* buffer_push(struct buffer*);

static inline unsigned int
static inline unsigned
buffer_full(struct buffer *b)
{
	return buffer_size(b) == BUFFER_LINES_MAX;
}

static inline unsigned int
static inline unsigned
buffer_size(struct buffer *b)
{
	return b->head - b->tail;


@@ 68,7 68,7 @@ buffer_tail(struct buffer *b)
}

struct buffer_line*
buffer_line(struct buffer *b, unsigned int i)
buffer_line(struct buffer *b, unsigned i)
{
	/* Return the buffer line indexed by i */



@@ 99,8 99,8 @@ buffer_line(struct buffer *b, unsigned int i)
	return &b->buffer_lines[BUFFER_MASK(i)];
}

unsigned int
buffer_line_rows(struct buffer_line *line, unsigned int w)
unsigned
buffer_line_rows(struct buffer_line *line, unsigned w)
{
	/* Return the number of times a buffer line will wrap within w columns */



@@ 172,7 172,7 @@ buffer_newline(
	}
}

float
unsigned
buffer_scrollback_status(struct buffer *b)
{
	/* Return the buffer scrollback status as a number between [0, 100] */


@@ 180,7 180,7 @@ buffer_scrollback_status(struct buffer *b)
	if (buffer_line(b, b->scrollback) == buffer_head(b))
		return 0;

	return (float)(b->head - b->scrollback) / (float)(buffer_size(b));
	return (100 * (float)(b->head - b->scrollback) / (float)(buffer_size(b)));
}

void


@@ 199,7 199,7 @@ buffer_line_split(
	unsigned cols,
	unsigned pad)
{
	unsigned _head_w = sizeof(" HH:MM   "VERTICAL_SEPARATOR" ");
	unsigned _head_w = sizeof(" HH:MM  ");

	if (BUFFER_PADDING)
		_head_w += pad;


@@ 216,5 216,5 @@ buffer_line_split(
		*head_w = _head_w;

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

M src/components/buffer.h => src/components/buffer.h +12 -12
@@ 35,34 35,34 @@ struct buffer_line
	size_t text_len;
	time_t time;
	struct {
		unsigned int colour; /* Cached colour of `from` text */
		unsigned int rows;   /* Cached number of rows occupied when wrapping on w columns */
		unsigned int w;      /* Cached width for rows */
		unsigned int initialized : 1;
		unsigned colour; /* Cached colour of `from` text */
		unsigned rows;   /* Cached number of rows occupied when wrapping on w columns */
		unsigned w;      /* Cached width for rows */
		unsigned initialized : 1;
	} cached;
};

struct buffer
{
	unsigned int head;
	unsigned int tail;
	unsigned int scrollback; /* Index of the current line between [tail, head) for scrollback */
	unsigned head;
	unsigned tail;
	unsigned scrollback; /* Index of the current line between [tail, head) for scrollback */
	size_t pad;              /* Pad 'from' when printing to be at least this wide */
	struct buffer_line buffer_lines[BUFFER_LINES_MAX];
};

float buffer_scrollback_status(struct buffer*);
unsigned buffer_scrollback_status(struct buffer*);

int buffer_page_back(struct buffer*, unsigned int, unsigned int);
int buffer_page_forw(struct buffer*, unsigned int, unsigned int);
int buffer_page_back(struct buffer*, unsigned, unsigned);
int buffer_page_forw(struct buffer*, unsigned, unsigned);

unsigned int buffer_line_rows(struct buffer_line*, unsigned int);
unsigned buffer_line_rows(struct buffer_line*, unsigned);

void buffer(struct buffer*);

struct buffer_line* buffer_head(struct buffer*);
struct buffer_line* buffer_tail(struct buffer*);
struct buffer_line* buffer_line(struct buffer*, unsigned int);
struct buffer_line* buffer_line(struct buffer*, unsigned);

void buffer_newline(
	struct buffer*,

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



@@ 40,8 40,8 @@ struct channel
	struct mode_str chanmodes_str;
	struct server *server;
	struct user_list users;
	unsigned int parted : 1;
	unsigned int joined : 1;
	unsigned parted : 1;
	unsigned joined : 1;
	char _[];
};


M src/components/input.c => src/components/input.c +20 -14
@@ 2,6 2,7 @@

#include "src/utils/utils.h"

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



@@ 82,15 83,21 @@ input_delete_forw(struct input *inp)
int
input_insert(struct input *inp, const char *c, size_t count)
{
	/* TODO: may want to discard control characters */
	if (input_text_isfull(inp))
		return 0;

	while (!input_text_isfull(inp) && count--) {

	size_t i = count;
		if (iscntrl(*c))
			inp->text[inp->head++] = ' ';

	while (!input_text_isfull(inp) && i--) {
		inp->text[inp->head++] = *c++;
		if (isprint(*c))
			inp->text[inp->head++] = *c;

		c++;
	}

	return (i != count);
	return 1;
}

int


@@ 279,24 286,23 @@ input_frame(struct input *inp, char *buf, uint16_t max)
uint16_t
input_write(struct input *inp, char *buf, uint16_t max, uint16_t pos)
{
	uint16_t i = pos,
	         j = 0;
	uint16_t buf_len = 0;

	while (max > 1 && i < inp->head) {
		buf[j++] = inp->text[i++];
	while (max > 1 && pos < inp->head) {
		buf[buf_len++] = inp->text[pos++];
		max--;
	}

	i = inp->tail;
	pos = inp->tail;

	while (max > 1 && i < INPUT_LEN_MAX) {
		buf[j++] = inp->text[i++];
	while (max > 1 && pos < INPUT_LEN_MAX) {
		buf[buf_len++] = inp->text[pos++];
		max--;
	}

	buf[j] = 0;
	buf[buf_len] = 0;

	return j;
	return buf_len;
}

static char*

M src/components/mode.c => src/components/mode.c +1 -1
@@ 664,7 664,7 @@ mode_cfg_modes(struct mode_cfg *cfg, const char *str)
{
	/* Parse and configure MODES, valid values are numeric strings [1-99] */

	unsigned int modes = 0;
	unsigned modes = 0;

	for (; modes < 100 && *str; str++) {
		if (isdigit(*str))

M src/components/mode.h => src/components/mode.h +1 -1
@@ 94,7 94,7 @@ struct mode

struct mode_cfg
{
	unsigned int MODES;    /* Numeric 005 MODES */
	unsigned MODES;        /* Numeric 005 MODES */
	struct mode chanmodes; /* Numeric 004 chanmodes string */
	struct mode usermodes; /* Numeric 004 usermodes string */
	struct

M src/components/server.c => src/components/server.c +91 -41
@@ 155,6 155,97 @@ server_free(struct server *s)
	free(s);
}

int
server_set_chans(struct server *s, const char *str)
{
	char *dup;
	char *p1;
	char *p2;
	size_t n_chans = 0;

	p2 = dup = strdup(str);

	do {
		n_chans++;

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

		if (p2)
			*p2++ = 0;

		if (!irc_ischan(p1) && !irc_isnick(p1)) {
			free(dup);
			return -1;
		}
	} while (p2);

	for (const char *chan = dup; n_chans; n_chans--) {

		struct channel *c;

		if (channel_list_get(&s->clist, chan, s->casemapping))
			continue;

		if (irc_ischan(chan))
			c = channel(chan, CHANNEL_T_CHANNEL);
		else
			c = channel(chan, CHANNEL_T_PRIVMSG);

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

		chan = strchr(chan, 0) + 1;
	}

	free(dup);

	return 0;
}

int
server_set_nicks(struct server *s, const char *str)
{
	char *dup;
	char *p1;
	char *p2;
	size_t n_nicks = 0;

	p2 = dup = strdup(str);

	do {
		n_nicks++;

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

		if (p2)
			*p2++ = 0;

		if (!irc_isnick(p1)) {
			free(dup);
			return -1;
		}
	} while (p2);

	free((void *)s->nicks.base);
	free((void *)s->nicks.set);

	s->nicks.next = 0;
	s->nicks.size = n_nicks;
	s->nicks.base = dup;

	if ((s->nicks.set = malloc(sizeof(*s->nicks.set) * n_nicks)) == NULL)
		fatal("malloc: %s", strerror(errno));

	for (const char **set = s->nicks.set; n_nicks; n_nicks--, set++) {
		*set = dup;
		dup = strchr(dup, 0) + 1;
	}

	return 0;
}

void
server_set_004(struct server *s, char *str)
{


@@ 221,47 312,6 @@ server_set_005(struct server *s, char *str)
	}
}

int
server_set_nicks(struct server *s, const char *nicks)
{
	char *p1, *p2, *base;
	size_t n = 0;

	p2 = base = strdup(nicks);

	do {
		n++;

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

		if (p2)
			*p2++ = 0;

		if (!irc_isnick(p1)) {
			free(base);
			return -1;
		}
	} while (p2);

	free((void *)s->nicks.base);
	free((void *)s->nicks.set);

	s->nicks.next = 0;
	s->nicks.size = n;
	s->nicks.base = base;

	if ((s->nicks.set = malloc(sizeof(*s->nicks.set) * n)) == NULL)
		fatal("malloc: %s", strerror(errno));

	for (const char **set = s->nicks.set; n; n--, set++) {
		*set = base;
		base = strchr(base, 0) + 1;
	}

	return 0;
}

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

M src/components/server.h => src/components/server.h +2 -1
@@ 64,9 64,10 @@ struct server* server_list_add(struct server_list*, struct server*);
struct server* server_list_del(struct server_list*, struct server*);
struct server* server_list_get(struct server_list*, const char*, const char*);

int server_set_chans(struct server*, const char*);
int server_set_nicks(struct server*, const char*);
void server_set_004(struct server*, char*);
void server_set_005(struct server*, char*);
int server_set_nicks(struct server*, const char*);

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

M src/components/user.h => src/components/user.h +1 -1
@@ 24,7 24,7 @@ struct user
struct user_list
{
	TREE_HEAD(user);
	unsigned int count;
	unsigned count;
};

enum user_err user_list_add(struct user_list*, enum casemapping, const char*, struct mode);

M src/draw.c => src/draw.c +408 -340
@@ 7,7 7,7 @@
#include "src/state.h"
#include "src/utils/utils.h"

#include <alloca.h>
#include <ctype.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>


@@ 16,35 16,34 @@
/* Control sequence initiator */
#define CSI "\x1b["

#define ATTR_FG(X)   CSI "38;5;"#X"m"
#define ATTR_BG(X)   CSI "48;5;"#X"m"
#define ATTR_RESET   CSI "0m"

#define CLEAR_FULL   CSI "2J"
#define CLEAR_LINE   CSI "2K"

#define C_MOVE(X, Y) CSI ""#X";"#Y"H"
#define C_SAVE       CSI "s"
#define C_RESTORE    CSI "u"
#define ATTR_BG(X)         CSI "48;5;"#X"m"
#define ATTR_FG(X)         CSI "38;5;"#X"m"
#define ATTR_RESET         CSI "0m"
#define ATTR_RESET_BG      CSI "49m"
#define ATTR_RESET_FG      CSI "39m"
#define CLEAR_FULL         CSI "2J"
#define CLEAR_LINE         CSI "2K"
#define CURSOR_POS(X, Y)   CSI #X";"#Y"H"
#define CURSOR_POS_RESTORE CSI "u"
#define CURSOR_POS_SAVE    CSI "s"

/* Minimum rows or columns to safely draw */
#define COLS_MIN 5
#define ROWS_MIN 5

/* Size of a full colour string for purposes of pre-formating text to print */
#define COLOUR_SIZE sizeof(ATTR_RESET ATTR_FG(255) ATTR_BG(255))

#ifndef BUFFER_PADDING
#define BUFFER_PADDING 1
#endif

#define UTF8_CONT(C) (((unsigned char)(C) & 0xC0) == 0x80)

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


@@ 53,41 52,69 @@

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

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

static struct coords coords(unsigned, unsigned, unsigned, unsigned);
static unsigned nick_col(char*);
static unsigned drawf(unsigned*, const char*, ...);

static void draw_bits(void);
static void draw_buffer(struct buffer*, struct coords);
static void draw_buffer_line(struct buffer_line*, struct coords, unsigned, unsigned, unsigned, unsigned);
static void draw_input(struct input*, struct coords);
static void draw_nav(struct channel*);
static void draw_separators(void);
static void draw_status(struct channel*);

static char* draw_colour(int, int);
static int draw_fmt(char**, size_t*, size_t*, int, const char*, ...);
static unsigned nick_col(char*);
static void check_coords(struct coords);
static void draw_attr_bg(int);
static void draw_attr_fg(int);
static void draw_attr_reset(void);
static void draw_char(int);
static void draw_clear_full(void);
static void draw_clear_line(void);
static void draw_cursor_pos(int, int);
static void draw_cursor_pos_restore(void);
static void draw_cursor_pos_save(void);

static int actv_colours[ACTIVITY_T_SIZE] = ACTIVITY_COLOURS
static int bg_last = -1;
static int fg_last = -1;
static int nick_colours[] = NICK_COLOURS
static struct draw_state draw_state;

static int drawing;

void
draw_init(void)
{
	drawing = 1;
}

void
draw_term(void)
{
	drawing = 0;

	draw(DRAW_CLEAR);
}

void
draw(enum draw_bit bit)


@@ 117,8 144,8 @@ draw(enum draw_bit bit)
			draw_state.bits.all = -1;
			break;
		case DRAW_CLEAR:
			printf(ATTR_RESET);
			printf(CLEAR_FULL);
			draw_attr_reset();
			draw_clear_full();
			break;
		default:
			fatal("unknown draw bit");


@@ 128,53 155,57 @@ draw(enum draw_bit bit)
static void
draw_bits(void)
{
	if (!drawing)
		return;

	if (draw_state.bell && BELL_ON_PINGED)
		putchar('\a');

	if (!draw_state.bits.all)
		return;

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

	if (state_cols() < COLS_MIN || state_rows() < ROWS_MIN) {
		printf(CLEAR_FULL C_MOVE(1, 1) "rirc");
		fflush(stdout);
		return;
	unsigned cols = state_cols();
	unsigned rows = state_rows();

	draw_cursor_pos_save();

	if (cols < COLS_MIN || rows < ROWS_MIN) {
		draw_clear_full();
		draw_cursor_pos(1, 1);
		goto flush;
	}

	printf(C_SAVE);
	if (draw_state.bits.separators) {
		draw_attr_reset();
		draw_separators();
	}

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

	if (draw_state.bits.input) {
		printf(ATTR_RESET);
		coords.c0 = 1;
		coords.cN = state_cols();
		coords.r0 = state_rows();
		coords.rN = state_rows();
		draw_input(&c->input, coords);
		draw_attr_reset();
		draw_input(&c->input, coords(1, cols, rows, rows));
	}

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

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

	printf(ATTR_RESET);
	printf(C_RESTORE);
flush:

	draw_attr_reset();
	draw_cursor_pos_restore();

	fflush(stdout);
}


@@ 192,7 223,7 @@ draw_buffer(struct buffer *b, struct coords coords)
	 * Rows are numbered from the top down, 1 to term_rows, so for term_rows = N,
	 * the drawable area for the buffer is bounded [r3, rN-2]:
	 *      __________________________
	 * r0   |         (nav)          |
	 * r1   |         (nav)          |
	 * r2   |------------------------|
	 * r3   |    ::buffer start::    |
	 *      |                        |


@@ 217,21 248,19 @@ draw_buffer(struct buffer *b, struct coords coords)
	 *    is encountered
	 */

	check_coords(coords);

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

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

	unsigned buffer_i = b->scrollback,
	         head_w,
	         text_w;
	unsigned buffer_i = b->scrollback;
	unsigned col_total = coords.cN - coords.c1 + 1;
	unsigned row;
	unsigned row_count = 0;
	unsigned row_total = coords.rN - coords.r1 + 1;
	unsigned head_w;
	unsigned text_w;

	/* Clear the buffer area */
	for (row = coords.r0; row <= coords.rN; row++)
		printf(C_MOVE(%d, 1) CLEAR_LINE, row);
	for (row = coords.r1; row <= coords.rN; row++) {
		draw_cursor_pos(row, 1);
		draw_clear_line();
	}

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



@@ 271,7 300,7 @@ draw_buffer(struct buffer *b, struct coords coords)
			BUFFER_PADDING ? (b->pad - line->from_len) : 0
		);

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

		if (line == head)
			return;


@@ 280,7 309,7 @@ draw_buffer(struct buffer *b, struct coords coords)
	}

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

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



@@ 293,7 322,7 @@ draw_buffer(struct buffer *b, struct coords coords)
			BUFFER_PADDING ? (b->pad - line->from_len) : 0
		);

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

		if (line == head)
			return;


@@ 302,9 331,6 @@ draw_buffer(struct buffer *b, struct coords coords)
	}
}

/* FIXME: works except when it doesn't.
 *
 * Fails when line headers are very long compared to text. tests/draw.c needed */
static void
draw_buffer_line(
		struct buffer_line *line,


@@ 314,12 340,11 @@ draw_buffer_line(
		unsigned skip,
		unsigned pad)
{
	check_coords(coords);
	char *p1 = line->text;
	char *p2 = line->text + line->text_len;

	char *print_p1,
	     *print_p2,
	     *p1 = line->text,
	     *p2 = line->text + line->text_len;
	unsigned head_col = coords.c1;
	unsigned text_col = coords.c1 + head_w;

	if (!line->cached.initialized) {
		/* Initialize static cached properties of drawn lines */


@@ 329,178 354,175 @@ draw_buffer_line(

	if (skip == 0) {

		/* Print the line header
		 *
		 * Since formatting codes don't occupy columns, enough space
		 * should be allocated for all such sequences
		 * */
		char header[head_w + COLOUR_SIZE * 4 + 1];
		char *header_ptr = header;
		/* Print the line header */

		size_t buff_n = sizeof(header) - 1,
		       text_n = head_w - 1;
		char buf_h[3] = {0};
		char buf_m[3] = {0};
		int from_bg;
		int from_fg;
		unsigned head_cols = head_w;

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

		if (!draw_fmt(&header_ptr, &buff_n, &text_n, 0,
				draw_colour(BUFFER_LINE_HEADER_FG_NEUTRAL, -1)))
			goto print_header;
		(void) snprintf(buf_h, sizeof(buf_h), "%02d", tm->tm_hour);
		(void) snprintf(buf_m, sizeof(buf_h), "%02d", tm->tm_min);

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

		if (!draw_fmt(&header_ptr, &buff_n, &text_n, 1,
				"%*s", pad, ""))
			goto print_header;
		if (!drawf(&head_cols, " %b%f%s:%s%a ",
				BUFFER_LINE_HEADER_BG,
				BUFFER_LINE_HEADER_FG,
				buf_h,
				buf_m))
			goto print_text;

		if (!draw_fmt(&header_ptr, &buff_n, &text_n, 0, ATTR_RESET))
			goto print_header;
		while (pad--) {
			if (!drawf(&head_cols, "%s", " "))
				goto print_text;
		}

		switch (line->type) {
			case BUFFER_LINE_OTHER:
			case BUFFER_LINE_SERVER_INFO:
			case BUFFER_LINE_SERVER_ERROR:
			case BUFFER_LINE_JOIN:
			case BUFFER_LINE_NICK:
			case BUFFER_LINE_PART:
			case BUFFER_LINE_QUIT:
				if (!draw_fmt(&header_ptr, &buff_n, &text_n, 0,
						draw_colour(BUFFER_LINE_HEADER_FG_NEUTRAL, -1)))
					goto print_header;
				break;

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

			case BUFFER_LINE_PINGED:
				if (!draw_fmt(&header_ptr, &buff_n, &text_n, 0,
						draw_colour(BUFFER_LINE_HEADER_FG_PINGED, BUFFER_LINE_HEADER_BG_PINGED)))
					goto print_header;
				from_bg = BUFFER_LINE_HEADER_BG_PINGED;
				from_fg = BUFFER_LINE_HEADER_FG_PINGED;
				break;
			default:
				from_bg = BUFFER_LINE_HEADER_BG;
				from_fg = BUFFER_LINE_HEADER_FG;
				break;

			case BUFFER_LINE_T_SIZE:
				fatal("Invalid line type");
		}

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

print_header:
		/* Print the line header */
		printf(C_MOVE(%d, 1) "%s " ATTR_RESET, coords.r0, header);
		if (!drawf(&head_cols, "%b%f%s%a ",
				from_bg,
				from_fg,
				line->from))
			goto print_text;
	}

print_text:

	while (skip--)
		irc_strwrap(text_w, &p1, p2);

	unsigned text_bg = BUFFER_TEXT_BG;
	unsigned text_fg = BUFFER_TEXT_FG;

	if (strlen(QUOTE_LEADER) && line->type == BUFFER_LINE_CHAT) {
		if (!strncmp(line->text, QUOTE_LEADER, strlen(QUOTE_LEADER))) {
			text_bg = QUOTE_TEXT_BG;
			text_fg = QUOTE_TEXT_FG;
		}
	}

	do {
		char *sep = " "VERTICAL_SEPARATOR" ";
		unsigned text_cols = text_w;

		draw_cursor_pos(coords.r1, text_col);

		if ((coords.cN - coords.c0) >= sizeof(*sep) + text_w) {
			printf(C_MOVE(%d, %d), coords.r0, (int)(coords.cN - (sizeof(*sep) + text_w + 1)));
			fputs(draw_colour(BUFFER_LINE_HEADER_FG_NEUTRAL, -1), stdout);
			fputs(sep, stdout);
		if (!drawf(&text_cols, "%b%f%s%a ",
				BUFFER_LINE_HEADER_BG,
				BUFFER_LINE_HEADER_FG,
				SEP_VERT)) {
			coords.r1++;
			continue;
		}

		if (*p1) {
			printf(C_MOVE(%d, %d), coords.r0, head_w);
			const char *text_p1 = p1;
			const char *text_p2 = irc_strwrap(text_cols, &p1, p2);

			print_p1 = p1;
			print_p2 = irc_strwrap(text_w, &p1, p2);
			draw_attr_bg(text_bg);
			draw_attr_fg(text_fg);

			fputs(draw_colour(line->text[0] == QUOTE_CHAR
					? BUFFER_LINE_TEXT_FG_GREEN
					: BUFFER_LINE_TEXT_FG_NEUTRAL,
					-1),
				stdout);
			for (unsigned i = 0; i < (text_p2 - text_p1); i++)
				draw_char(text_p1[i]);

			printf("%.*s", (int)(print_p2 - print_p1), print_p1);
			draw_attr_reset();
		}

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

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

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

	check_coords(coords);
	unsigned cols = state_cols();

	unsigned cols_t = coords.cN - coords.c0 + 1,
	         cursor = coords.c0;
	draw_cursor_pos(2, 1);

	printf(C_MOVE(%d, 1) CLEAR_LINE, coords.rN);
	printf(C_SAVE);
	draw_attr_bg(SEP_BG);
	draw_attr_fg(SEP_FG);

	/* Insufficient columns for meaningful input drawing */
	if (cols_t < 3)
		return;
	while (drawf(&cols, "%s", SEP_HORZ))
		;
}

	char input[cols_t + COLOUR_SIZE * 2 + 1];
	char *input_ptr = input;
static void
draw_input(struct input *inp, struct coords coords)
{
	/* Draw the input line, or the current action message */

	size_t buff_n = sizeof(input) - 1,
	       text_n = cols_t;
	const char *action;
	unsigned cols = coords.cN - coords.c1 + 1;
	unsigned cursor_row = coords.r1;
	unsigned cursor_col = coords.cN;

	if (sizeof(INPUT_PREFIX)) {
	draw_cursor_pos(coords.r1, coords.c1);

		if (!draw_fmt(&input_ptr, &buff_n, &text_n, 0,
				"%s", draw_colour(INPUT_PREFIX_FG, INPUT_PREFIX_BG)))
			goto print_input;
	if ((action = action_message())) {
		if (!drawf(&cols, "%b%f%s%b%f-- %s --",
				INPUT_PREFIX_BG,
				INPUT_PREFIX_FG,
				INPUT_PREFIX,
				ACTION_BG,
				ACTION_FG,
				action))
			goto cursor;

		cursor = coords.c0 + sizeof(INPUT_PREFIX) - 1;
		cursor_col = coords.cN - coords.c1 - cols + 3;
	} else {
		char input[INPUT_LEN_MAX];
		unsigned cursor_pre;
		unsigned cursor_inp;

		if (!draw_fmt(&input_ptr, &buff_n, &text_n, 1,
		if (!drawf(&cols, "%b%f%s",
				INPUT_PREFIX_BG,
				INPUT_PREFIX_FG,
				INPUT_PREFIX))
			goto print_input;
	}

	if (action_message()) {
			goto cursor;

		if (!draw_fmt(&input_ptr, &buff_n, &text_n, 0,
				"%s", draw_colour(ACTION_FG, ACTION_BG)))
			goto print_input;
		cursor_pre = coords.cN - coords.c1 - cols + 1;
		cursor_inp = input_frame(inp, input, cols);

		cursor = coords.cN;
		if (!drawf(&cols, "%b%f%s",
				INPUT_BG,
				INPUT_FG,
				input))
			goto cursor;

		if (!draw_fmt(&input_ptr, &buff_n, &text_n, 1, "-- %s --", action_message()))
			goto print_input;
		cursor_col = cursor_pre + cursor_inp + 1;
	}

		cursor = cols_t - text_n + 1;
	draw_attr_reset();

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

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

print_input:
	cursor_row = MIN(cursor_row, coords.rN);
	cursor_col = MIN(cursor_col, coords.cN);

	fputs(input, stdout);
	printf(C_MOVE(%d, %d), coords.rN, (cursor >= coords.c0 && cursor <= coords.cN) ? cursor : coords.cN);
	printf(C_SAVE);
	draw_cursor_pos(cursor_row, cursor_col);
	draw_cursor_pos_save();
}

/* TODO
 *
 * | [server-name[:port]] *#chan |
 *
 * - Disconnected/parted channels are printed (#chan)
 * - Servers with non-standard ports are printed: server-name:port
 * - Channels that won't fit are printed at a minimum: #...
 *     - eg: | ...chan #chan2 chan3 |   Right printing
 *           | #chan1 #chan2 #ch... |   Left printing
 * */
static void
draw_nav(struct channel *c)
{


@@ 511,7 533,8 @@ draw_nav(struct channel *c)
	 *  - The nav is kept framed between the first and last channels
	 */

	printf(C_MOVE(1, 1) CLEAR_LINE);
	draw_cursor_pos(1, 1);
	draw_clear_line();

	static struct channel *frame_prev,
	                      *frame_next;


@@ 520,10 543,12 @@ draw_nav(struct channel *c)
	               *c_last = channel_get_last(),
	               *tmp;

	unsigned cols = state_cols();

	c->activity = ACTIVITY_DEFAULT;

	/* By default assume drawing starts towards the next channel */
	int colour, nextward = 1;
	int nextward = 1;

	size_t len, total_len = 0;



@@ 597,12 622,9 @@ draw_nav(struct channel *c)
	/* Draw coloured channel names, from frame to frame */
	for (tmp = frame_prev; ; tmp = channel_get_next(tmp)) {

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

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

		if (printf(" %s ", tmp->name) < 0)
		if (!drawf(&cols, "%f %s ", fg, tmp->name))
			break;

		if (tmp == frame_next)


@@ 613,119 635,88 @@ draw_nav(struct channel *c)
static void
draw_status(struct channel *c)
{
	/* TODO: channel modes, channel type_flag, servermodes */

	/* server / private chat:
	 * |-[usermodes]-(ping)---...|
	/* server buffer:
	 *  -[+usermodes]-(ping)-(scrollback)
	 *
	 * privmsg buffer:
	 *  -[+usermodes]-[privmsg]-(ping)-(scrollback)
	 *
	 * channel:
	 * |-[usermodes]-[chancount chantype chanmodes]/[priv]-(ping)---...|
	 * channel buffer:
	 *  -[+usermodes]-[+chanmodes chancount]-(ping)-(scrollback)
	 */

	float sb;
	int ret;
	unsigned col = 0;
	#define STATUS_SEP_HORZ \
		"%b%f" SEP_HORZ "%b%f", SEP_BG, SEP_FG, STATUS_BG, STATUS_FG

	unsigned cols = state_cols();
	unsigned rows = state_rows();
	unsigned scrollback;

	/* Insufficient columns for meaningful status */
	if (cols < 3)
	if (!cols || !(rows > 1))
		return;

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

	printf(C_MOVE(%d, 1) CLEAR_LINE, rows - 1);

	/* Print status to temporary buffer */
	char status_buff[cols + 1];

	memset(status_buff, 0, cols + 1);
	draw_cursor_pos(rows - 1, 1);

	/* -[usermodes] */
	if (c->server && *(c->server->mode_str.str)) {
		ret = snprintf(status_buff + col, cols - col + 1, "%s", HORIZONTAL_SEPARATOR "[+");
		if (ret < 0 || (col += ret) >= cols)
			goto print_status;

		ret = snprintf(status_buff + col, cols - col + 1, "%s", c->server->mode_str.str);
		if (ret < 0 || (col += ret) >= cols)
			goto print_status;

		ret = snprintf(status_buff + col, cols - col + 1, "%s", "]");
		if (ret < 0 || (col += ret) >= cols)
			goto print_status;
		if (!drawf(&cols, STATUS_SEP_HORZ))
			return;
		if (!drawf(&cols, "[+%s]", c->server->mode_str.str))
			return;
	}

	/* If private chat buffer:
	 * -[priv] */
	if (c->type == CHANNEL_T_PRIVATE) {
		ret = snprintf(status_buff + col, cols - col + 1, "%s", HORIZONTAL_SEPARATOR "[priv]");
		if (ret < 0 || (col += ret) >= cols)
			goto print_status;
	/* -[priv] */
	if (c->type == CHANNEL_T_PRIVMSG) {
		if (!drawf(&cols, STATUS_SEP_HORZ))
			return;
		if (!drawf(&cols, "[privmsg]"))
			return;
	}

	/* If IRC channel buffer:
	 * -[chancount chantype chanmodes] */
	if (c->type == CHANNEL_T_CHANNEL) {

		ret = snprintf(status_buff + col, cols - col + 1,
				HORIZONTAL_SEPARATOR "[%d", c->users.count);
		if (ret < 0 || (col += ret) >= cols)
			goto print_status;

		if (c->chanmodes.prefix) {
			ret = snprintf(status_buff + col, cols - col + 1, " %c", c->chanmodes.prefix);
			if (ret < 0 || (col += ret) >= cols)
				goto print_status;
		}

		if (*(c->chanmodes_str.str)) {
			ret = snprintf(status_buff + col, cols - col + 1, " +%s", c->chanmodes_str.str);
			if (ret < 0 || (col += ret) >= cols)
				goto print_status;
		}

		ret = snprintf(status_buff + col, cols - col + 1, "%s", "]");
		if (ret < 0 || (col += ret) >= cols)
			goto print_status;
	/* -[chanmodes chancount] */
	if (c->type == CHANNEL_T_CHANNEL && c->joined) {
		if (!drawf(&cols, STATUS_SEP_HORZ))
			return;
		if (!drawf(&cols, "[+%s %u]", c->chanmodes_str.str, c->users.count))
			return;
	}

	/* -(ping) */
	if (c->server && c->server->ping) {
		ret = snprintf(status_buff + col, cols - col + 1,
				HORIZONTAL_SEPARATOR "(%llds)", (long long) c->server->ping);
		if (ret < 0 || (col += ret) >= cols)
			goto print_status;
		if (!drawf(&cols, STATUS_SEP_HORZ))
			return;
		if (!drawf(&cols, "(%us)", c->server->ping))
			return;
	}

	/* -(scrollback%) */
	if ((sb = buffer_scrollback_status(&c->buffer))) {
		ret = snprintf(status_buff + col, cols - col + 1,
				HORIZONTAL_SEPARATOR "(%02d%%)", (int)(sb * 100));
		if (ret < 0 || (col += ret) >= cols)
			goto print_status;
	/* -(scrollback) */
	if ((scrollback = buffer_scrollback_status(&c->buffer))) {
		if (!drawf(&cols, STATUS_SEP_HORZ))
			return;
		if (!drawf(&cols, "(%u%s)", scrollback, "%"))
			return;
	}

print_status:

	fputs(status_buff, stdout);
	draw_attr_bg(SEP_BG);
	draw_attr_fg(SEP_FG);

	/* Trailing separator */
	while (col++ < cols)
		printf(HORIZONTAL_SEPARATOR);
	while (drawf(&cols, "%s", SEP_HORZ))
		;
}

static void
check_coords(struct coords coords)
static struct coords
coords(unsigned c1, unsigned cN, unsigned r1, unsigned rN)
{
	/* Check coordinate validity before drawing, ensure at least one row, column */
	unsigned cols = state_cols();
	unsigned rows = state_rows();

	if (!c1 || c1 > cN || cN > cols)
		fatal("Invalid coordinates: cols: %u %u %u", cols, c1, cN);

	if (coords.r0 > coords.rN)
		fatal("row coordinates invalid (%u > %u)", coords.r0, coords.rN);
	if (!r1 || r1 > rN || rN > rows)
		fatal("Invalid coordinates: rows: %u %u %u", rows, r1, rN);

	if (coords.c0 > coords.cN)
		fatal("col coordinates invalid (%u > %u)", coords.c0, coords.cN);
	return (struct coords) { .c1 = c1, .cN = cN, .r1 = r1, .rN = rN };
}

static unsigned


@@ 739,74 730,151 @@ nick_col(char *nick)
	return nick_colours[colour % sizeof(nick_colours) / sizeof(nick_colours[0])];
}

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

	static char buf[COLOUR_SIZE + 1] = ATTR_RESET;
	size_t len = sizeof(ATTR_RESET) - 1;
	int ret = 0;
	/* Draw formatted text up to a given number of
	 * columns. Returns number of unused columns.
	 *
	 *  %a -- attribute reset
	 *  %b -- set background colour attribute
	 *  %f -- set foreground colour attribute
	 *  %c -- output char
	 *  %d -- output signed integer
	 *  %u -- output unsigned integer
	 *  %s -- output string
	 */

	if (fg >= 0 && fg <= 255) {
		if ((ret = snprintf(buf + len, sizeof(buf) - len, ATTR_FG(%d), fg)) < 0)
			buf[len] = 0;
		else
			len += ret;
	char buf[64];
	char c;
	va_list arg;
	unsigned cols;

	if (!(cols = *cols_p))
		return 0;

	va_start(arg, fmt);

	while (cols && (c = *fmt++)) {
		if (c == '%') {
			switch ((c = *fmt++)) {
				case 'a':
					draw_attr_reset();
					break;
				case 'b':
					draw_attr_bg(va_arg(arg, int));
					break;
				case 'f':
					draw_attr_fg(va_arg(arg, int));
					break;
				case 'c':
					draw_char(va_arg(arg, int));
					cols--;
					break;
				case 'd':
					(void) snprintf(buf, sizeof(buf), "%d", va_arg(arg, int));
					cols -= (unsigned) printf("%.*s", cols, buf);
					break;
				case 'u':
					(void) snprintf(buf, sizeof(buf), "%u", va_arg(arg, unsigned));
					cols -= (unsigned) printf("%.*s", cols, buf);
					break;
				case 's':
					for (const char *str = va_arg(arg, const char*); *str && cols; cols--) {
						do {
							draw_char(*str++);
						} while (UTF8_CONT(*str));
					}
					break;
				case '%':
				default:
					fatal("unknown drawf format character '%c'", c);
			}
		} else {
			cols--;
			draw_char(c);
			while (UTF8_CONT(*fmt))
				draw_char(*fmt++);
		}
	}

	if (bg >= 0 && bg <= 255) {
		if ((snprintf(buf + len, sizeof(buf) - len, ATTR_BG(%d), bg)) < 0)
			buf[len] = 0;
	}
	va_end(arg);

	return buf;
	return (*cols_p = cols);
}

static int
draw_fmt(char **ptr, size_t *buff_n, size_t *text_n, int txt, const char *fmt, ...)
static void
draw_attr_bg(int bg)
{
	/* Write formatted text to a buffer for purposes of preparing an object to be drawn
	 * to the terminal.
	 *
	 * Calls to this function should distinguish between formatting and printed text
	 * with the txt flag.
	 *
	 *  - ptr    : pointer to location in buffer being printed to
	 *  - buff_n : remaining bytes available in buff
	 *  - text_n : remaining columns available for text
	 *  - txt    : flag set true if bytes being written are printable text
	 *
	 *  returns 0 on error, or if no more prints to this buffer can occur
	 */
	if (bg == -1)
		printf(ATTR_RESET_BG);

	int ret;
	va_list ap;
	if (bg >= 0 && bg <= 255)
		printf(ATTR_BG(%d), bg);

	va_start(ap, fmt);
	ret = vsnprintf(*ptr, *buff_n, fmt, ap);
	va_end(ap);
	bg_last = bg;
}

	if (ret < 0)
		return (**ptr = 0);
static void
draw_attr_fg(int fg)
{
	if (fg == -1)
		printf(ATTR_RESET_FG);

	size_t _ret = (size_t) ret;
	if (fg >= 0 && fg <= 255)
		printf(ATTR_FG(%d), fg);

	if (!txt && _ret >= *buff_n)
		return (**ptr = 0);
	fg_last = fg;
}

	if (txt) {
		if (*text_n > _ret)
			*text_n -= _ret;
		else {
			*ptr += *text_n;
			**ptr = 0;
			return (*text_n = 0);
		}
static void
draw_attr_reset(void)
{
	printf(ATTR_RESET);
}

static void
draw_clear_full(void)
{
	printf(CLEAR_FULL);
}

static void
draw_clear_line(void)
{
	printf(CLEAR_LINE);
}

static void
draw_char(int c)
{
	if (iscntrl(c)) {
		int ctrl_bg_last = bg_last;
		int ctrl_fg_last = fg_last;
		draw_attr_bg(CTRL_BG);
		draw_attr_fg(CTRL_FG);
		putchar((c | 0x40));
		draw_attr_bg(ctrl_bg_last);
		draw_attr_fg(ctrl_fg_last);
	} else {
		putchar(c);
	}
}

	*ptr += _ret;
static void
draw_cursor_pos(int row, int col)
{
	printf(CURSOR_POS(%d, %d), row, col);
}

static void
draw_cursor_pos_save(void)
{
	printf(CURSOR_POS_SAVE);
}

	return 1;
static void
draw_cursor_pos_restore(void)
{
	printf(CURSOR_POS_RESTORE);
}

M src/draw.h => src/draw.h +3 -0
@@ 14,6 14,9 @@ enum draw_bit
	DRAW_CLEAR,  /* clear the terminal */
};

void draw_init(void);
void draw_term(void);

void draw(enum draw_bit);

#endif

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

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

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

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

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

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


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


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

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

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

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

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

	irc_message_param(m, &message);

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

	return 0;
}

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

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

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

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

	irc_message_param(m, &message);

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

	return 0;
}

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


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

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

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



@@ 1037,7 1089,6 @@ recv_notice(struct server *s, struct irc_message *m)

	char *message;
	char *target;
	int urgent = 0;
	struct channel *c;

	if (!m->from)


@@ 1055,38 1106,10 @@ recv_notice(struct server *s, struct irc_message *m)
	if (IS_CTCP(message))
		return ctcp_response(s, m->from, target, message);

	if (!strcmp(target, "*")) {
	if (!(c = channel_list_get(&(s->clist), m->from, s->casemapping)))
		c = s->channel;
	} else if (!strcmp(target, s->nick)) {

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

		if (c != current_channel())
			urgent = 1;

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

	if (irc_pinged(s->casemapping, message, s->nick)) {

		if (c != current_channel())
			urgent = 1;

		newlinef(c, BUFFER_LINE_PINGED, m->from, "%s", message);
	} else {
		newlinef(c, BUFFER_LINE_CHAT, m->from, "%s", message);
	}

	if (urgent) {
		c->activity = ACTIVITY_PINGED;
		draw(DRAW_BELL);
		draw(DRAW_NAV);
	}
	newlinef(c, BUFFER_LINE_CHAT, m->from, "%s", message);

	return 0;
}


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

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

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


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

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


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

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

			if (message && *message)


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

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

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


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

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

		if (message)


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

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

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

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

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

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


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

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


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

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

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


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

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

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

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

	return NULL;
}

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


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

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

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

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

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

	return 0;
}

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


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

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

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

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

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

	do {
		struct channel *c;

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

		if (p2)
			*p2++ = 0;

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

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

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

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

	} while (p2);

	free(dup);

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

	return 0;
}

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



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

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


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

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


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

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

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

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

	return 0;
}

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

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

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

	return 0;
}

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

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

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

	return 0;
}

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

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

	(void) gettimeofday(&t, NULL);

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



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

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

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

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

	return 0;
}

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

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

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

	return 0;
}

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

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

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

	return 0;
}

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

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

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

	return 0;
}

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


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

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

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

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

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

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



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

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

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

#endif

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

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

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


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

	io_tty_winsize();

	while (io_running) {

		char buf[128];


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

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

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


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

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

	io_tty_winsize();
}

static void


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

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

static void


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

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

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

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

	} while ((s = p));
	}

	return 0;
}


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

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


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

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

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

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

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

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

	} else {

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

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

static void

M src/rirc.c => src/rirc.c +113 -86
@@ 13,29 13,28 @@
#include <string.h>
#include <unistd.h>

#define MAX_CLI_SERVERS 16
#define MAX_CLI_SERVERS 64

#define arg_error(...) \
	do { fprintf(stderr, "%s ", runtime_name); \
	     fprintf(stderr, __VA_ARGS__); \
	     fprintf(stderr, "\n%s --help for usage\n", runtime_name); \
	     return -1; \
	} while (0)

static const char* opt_arg_str(char);
static const char* getpwuid_pw_name(void);
static int parse_args(int, char**);
static const char* rirc_opt_str(char);
static const char* rirc_pw_name(void);
static int rirc_parse_args(int, char**);

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

#ifdef DEFAULT_NICK_SET
const char *default_nick_set = DEFAULT_NICK_SET;
#ifdef DEFAULT_NICKS
const char *default_nicks = DEFAULT_NICKS;
#else
const char *default_nick_set;
const char *default_nicks;
#endif

#ifdef DEFAULT_USERNAME


@@ 90,7 89,7 @@ static const char *const rirc_version =
#endif

static const char*
opt_arg_str(char c)
rirc_opt_str(char c)
{
	switch (c) {
		case 's': return "-s/--server";


@@ 111,9 110,9 @@ opt_arg_str(char c)
}

static const char*
getpwuid_pw_name(void)
rirc_pw_name(void)
{
	static struct passwd *passwd;
	static const struct passwd *passwd;

	errno = 0;



@@ 124,23 123,35 @@ getpwuid_pw_name(void)
}

static int
parse_args(int argc, char **argv)
rirc_parse_args(int argc, char **argv)
{
	int opt_c = 0,
	    opt_i = 0;
	int opt_c = 0;
	int opt_i = 0;

	size_t n_servers = 0;

	opterr = 0;
	struct cli_server {
		const char *host;
		const char *port;
		const char *pass;
		const char *username;
		const char *realname;
		const char *nicks;
		const char *chans;
		int ipv;
		int tls;
		int tls_vrfy;
		struct server *s;
	} cli_servers[MAX_CLI_SERVERS];

	struct option long_opts[] = {
		{"server",      required_argument, 0, 's'},
		{"port",        required_argument, 0, 'p'},
		{"pass",        required_argument, 0, 'w'},
		{"nicks",       required_argument, 0, 'n'},
		{"chans",       required_argument, 0, 'c'},
		{"username",    required_argument, 0, 'u'},
		{"realname",    required_argument, 0, 'r'},
		{"nicks",       required_argument, 0, 'n'},
		{"chans",       required_argument, 0, 'c'},
		{"help",        no_argument,       0, 'h'},
		{"version",     no_argument,       0, 'v'},
		{"ipv4",        no_argument,       0, '4'},


@@ 150,49 161,45 @@ parse_args(int argc, char **argv)
		{0, 0, 0, 0}
	};

	struct cli_server {
		const char *host;
		const char *port;
		const char *pass;
		const char *nicks;
		const char *chans;
		const char *username;
		const char *realname;
		int ipv;
		int tls;
		int tls_vrfy;
		struct server *s;
	} cli_servers[MAX_CLI_SERVERS];
	opterr = 0;

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

		switch (opt_c) {

			case 's': /* Connect to server */

				if (*optarg == '-')
				if (*optarg == '-') {
					arg_error("-s/--server requires an argument");
					return -1;
				}

				if (++n_servers == MAX_CLI_SERVERS)
				if (++n_servers == MAX_CLI_SERVERS) {
					arg_error("exceeded maximum number of servers (%d)", MAX_CLI_SERVERS);
					return -1;
				}

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

			#define CHECK_SERVER_OPTARG(OPT_C, REQ) \
				if ((REQ) && *optarg == '-') \
					arg_error("option '%s' requires an argument", opt_arg_str((OPT_C))); \
				if (n_servers == 0) \
					arg_error("option '%s' requires a server argument first", opt_arg_str((OPT_C)));
				if ((REQ) && *optarg == '-') { \
					arg_error("option '%s' requires an argument", rirc_opt_str((OPT_C))); \
					return -1; \
				} \
				if (n_servers == 0) { \
					arg_error("option '%s' requires a server argument first", rirc_opt_str((OPT_C))); \
					return -1; \
				}

			case 'p': /* Connect using port */
				CHECK_SERVER_OPTARG(opt_c, 1);


@@ 204,24 211,24 @@ parse_args(int argc, char **argv)
				cli_servers[n_servers - 1].pass = optarg;
				break;

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

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

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

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

			case '4': /* Connect using ipv4 only */


@@ 253,7 260,8 @@ parse_args(int argc, char **argv)
					cli_servers[n_servers -1].tls_vrfy = IO_TLS_VRFY_REQUIRED;
					break;
				}
				arg_error("option '--tls-verify' mode must be 'disabled', 'optional', or 'required'");
				arg_error("invalid option for '--tls-verify' '%s'", optarg);
				return -1;

			#undef CHECK_SERVER_OPTARG



@@ 266,29 274,23 @@ parse_args(int argc, char **argv)
				exit(EXIT_SUCCESS);

			case '?':
				arg_error("unknown options '%s'", argv[optind - 1]);
				arg_error("unknown option '%s'", argv[optind - 1]);
				return -1;

			case ':':
				arg_error("option '%s' requires an argument", opt_arg_str(optopt));
				arg_error("option '%s' requires an argument", rirc_opt_str(optopt));
				return -1;

			default:
				arg_error("unknown opt error");
				return -1;
		}
	}

	if (optind < argc)
	if (optind < argc) {
		arg_error("unused option '%s'", argv[optind]);

	if (!default_nick_set || !default_nick_set[0])
		default_nick_set = getpwuid_pw_name();

	if (!default_username || !default_username[0])
		default_username = getpwuid_pw_name();

	if (!default_realname || !default_realname[0])
		default_realname = getpwuid_pw_name();

	state_init();
		return -1;
	}

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



@@ 300,34 302,45 @@ parse_args(int argc, char **argv)
		if (cli_servers[i].port == NULL)
			cli_servers[i].port = (cli_servers[i].tls == IO_TLS_ENABLED) ? "6697" : "6667";

		struct server *s = server(
		cli_servers[i].s = server(
			cli_servers[i].host,
			cli_servers[i].port,
			cli_servers[i].pass,
			(cli_servers[i].username ? cli_servers[i].username : default_username),
			(cli_servers[i].realname ? cli_servers[i].realname : default_realname)
			cli_servers[i].username,
			cli_servers[i].realname
		);

		s->connection = connection(s, cli_servers[i].host, cli_servers[i].port, flags);
		cli_servers[i].s->connection = connection(
			cli_servers[i].s,
			cli_servers[i].host,
			cli_servers[i].port,
			flags);

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

		if (cli_servers[i].chans && state_server_set_chans(s, cli_servers[i].chans))
			arg_error("invalid chans: '%s'", cli_servers[i].chans);

		if (server_set_nicks(s, (cli_servers[i].nicks ? cli_servers[i].nicks : default_nick_set)))
			arg_error("invalid nicks: '%s'", cli_servers[i].nicks);
		if (cli_servers[i].nicks && server_set_nicks(cli_servers[i].s, cli_servers[i].nicks)) {
			arg_error("invalid %s: '%s'", rirc_opt_str('n'), cli_servers[i].nicks);
			return -1;
		}

		cli_servers[i].s = s;
		if (cli_servers[i].chans && server_set_chans(cli_servers[i].s, cli_servers[i].chans)) {
			arg_error("invalid %s: '%s'", rirc_opt_str('c'), cli_servers[i].chans);
			return -1;
		}

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

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

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

		if ((ret = io_cx(cli_servers[i].s->connection)))
			server_error(cli_servers[i].s, "failed to connect: %s", io_err(ret));
	}

	return 0;
}


@@ 336,19 349,33 @@ parse_args(int argc, char **argv)
int
main(int argc, char **argv)
{
	int ret;

	if (argc > 0)
	if (argc)
		runtime_name = argv[0];

	if (!default_username || !default_username[0])
		default_username = rirc_pw_name();

	if (!default_realname || !default_realname[0])
		default_realname = rirc_pw_name();

	if (!default_nicks || !default_nicks[0])
		default_nicks = rirc_pw_name();

	srand(time(NULL));

	if ((ret = parse_args(argc, argv)) == 0) {
		io_start();
	state_init();
	io_init();

	if (rirc_parse_args(argc, argv)) {
		state_term();
		draw(DRAW_CLEAR);
		return EXIT_FAILURE;
	}

	return ret;
	draw_init();
	io_start();
	draw_term();
	state_term();

	return EXIT_SUCCESS;
}
#endif

M src/state.c => src/state.c +24 -56
@@ 94,7 94,7 @@ static const char *cmd_list[] = {
void
state_init(void)
{
	state.default_channel = state.current_channel = channel("rirc", CHANNEL_T_RIRC);
	state.default_channel = channel("rirc", CHANNEL_T_RIRC);

	newlinef(state.default_channel, 0, FROM_INFO, "      _");
	newlinef(state.default_channel, 0, FROM_INFO, " _ __(_)_ __ ___");


@@ 107,6 107,8 @@ state_init(void)
#ifndef NDEBUG
	newlinef(state.default_channel, 0, FROM_INFO, " - compiled with DEBUG flags");
#endif

	channel_set_current(state.default_channel);
}

void


@@ 215,42 217,6 @@ _newline(struct channel *c, enum buffer_line_type type, const char *from, const 
	}
}

int
state_server_set_chans(struct server *s, const char *chans)
{
	char *p1, *p2, *base;
	size_t n = 0;

	p2 = base = strdup(chans);

	do {
		n++;

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

		if (p2)
			*p2++ = 0;

		if (!irc_ischan(p1)) {
			free(base);
			return -1;
		}
	} while (p2);

	for (const char *chan = base; n; n--) {
		struct channel *c;
		c = channel(chan, CHANNEL_T_CHANNEL);
		c->server = s;
		channel_list_add(&s->clist, c);
		chan = strchr(chan, 0) + 1;
	}

	free(base);

	return 0;
}

static int
state_input_action(const char *input, size_t len)
{


@@ 360,7 326,7 @@ state_channel_close(int action_confirm)

	if (action_confirm) {

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

		if (c->type == CHANNEL_T_SERVER)


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

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

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

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

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


@@ 387,7 357,7 @@ state_channel_close(int action_confirm)

		if (s->connected) {
			if ((ret = io_sendf(s->connection, "QUIT :%s", DEFAULT_QUIT_MESG)))
				newlinef(s->channel, 0, FROM_ERROR, "sendf fail: %s", io_err(ret));
				server_error(s, "sendf fail: %s", io_err(ret));
			io_dx(s->connection);
		}



@@ 408,11 378,11 @@ buffer_scrollback_back(struct channel *c)

	struct buffer *b = &c->buffer;

	unsigned int buffer_i = b->scrollback,
	             count = 0,
	             text_w = 0,
	             cols = state_tty_cols,
	             rows = state_tty_rows - 4;
	unsigned buffer_i = b->scrollback,
	         count = 0,
	         text_w = 0,
	         cols = state_tty_cols,
	         rows = state_tty_rows - 4;

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



@@ 451,10 421,10 @@ buffer_scrollback_forw(struct channel *c)
{
	/* Scroll a buffer forward one page */

	unsigned int count = 0,
	             text_w = 0,
	             cols = state_tty_cols,
	             rows = state_tty_rows - 4;
	unsigned count = 0,
	         text_w = 0,
	         cols = state_tty_cols,
	         rows = state_tty_rows - 4;

	struct buffer *b = &c->buffer;



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

		state_channel_clear(0);
		return;
	}


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

		state_channel_close(0);
		return;
	}


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

	return 1;

M src/state.h => src/state.h +0 -1
@@ 26,7 26,6 @@ unsigned state_cols(void);
unsigned state_rows(void);

const char *action_message(void);
int state_server_set_chans(struct server*, const char*);
struct channel* channel_get_first(void);
struct channel* channel_get_last(void);
struct channel* channel_get_next(struct channel*);

M src/utils/utils.c => src/utils/utils.c +15 -5
@@ 399,11 399,21 @@ irc_ischanchar(char c, int first)
	 * channelid  = 5( %x41-5A / digit )   ; 5( A-Z / 0-9 )
	 */

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

	return 1;
	if (first)
		return (c == '#' || c == '+' || c == '&');

	switch (c) {
		case 0x00: /* NUL */
		case 0x07: /* BEL */
		case 0x0D: /* CR */
		case 0x0A: /* LF */
		case ' ':
		case ',':
		case ':':
			return 0;
		default:
			return 1;
	}
}

static inline int

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

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

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


M test/components/buffer.c => test/components/buffer.c +11 -11
@@ 238,19 238,19 @@ test_buffer_scrollback_status(void)
	assert_true(buffer_full(&b));

	b.scrollback = b.tail;
	assert_ueq((100 * buffer_scrollback_status(&b)), 100);
	assert_ueq((buffer_scrollback_status(&b)), 100);

	b.scrollback = b.tail + (BUFFER_LINES_MAX / 2);
	assert_ueq((100 * buffer_scrollback_status(&b)), 50);
	assert_ueq((buffer_scrollback_status(&b)), 50);

	b.scrollback = b.head - 1;
	assert_ueq((100 * buffer_scrollback_status(&b)), 0);
	assert_ueq((buffer_scrollback_status(&b)), 0);
}

static void
test_buffer_index_overflow(void)
{
	/* Test masked indexing after unsigned integer overflow */
	/* Test masked indexing after unsigned overflow */

	struct buffer b;



@@ 284,12 284,12 @@ test_buffer_line_overlength(void)
	buffer(&b);

	/* Indices to first and last positions of lines, total length = 2.5 times the maximum */
	unsigned int f1 = 0,
	             l1 = TEXT_LENGTH_MAX - 1,
	             f2 = TEXT_LENGTH_MAX,
	             l2 = TEXT_LENGTH_MAX * 2 - 1,
	             f3 = TEXT_LENGTH_MAX * 2,
	             l3 = TEXT_LENGTH_MAX * 2 + TEXT_LENGTH_MAX / 2 - 1;
	unsigned f1 = 0,
	         l1 = TEXT_LENGTH_MAX - 1,
	         f2 = TEXT_LENGTH_MAX,
	         l2 = TEXT_LENGTH_MAX * 2 - 1,
	         f3 = TEXT_LENGTH_MAX * 2,
	         l3 = TEXT_LENGTH_MAX * 2 + TEXT_LENGTH_MAX / 2 - 1;

	/* Add a line that's 2.5 times the maximum length */
	char text[(l3 + 1) + 1];


@@ 449,5 449,5 @@ main(void)
		TESTCASE(test_buffer_newline_prefix),
	};

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

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

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

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

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

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

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

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

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

M test/components/server.c => test/components/server.c +50 -1
@@ 112,6 112,54 @@ test_server_list(void)
}

static void
test_server_set_chans(void)
{
	struct channel *c;
	struct server *s;

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

	/* empty values, invalid formats */
	assert_eq(server_set_chans(s, ""), -1);
	assert_eq(s->clist.count, 1);
	assert_eq(server_set_chans(s, ","), -1);
	assert_eq(s->clist.count, 1);
	assert_eq(server_set_chans(s, ",,,"), -1);
	assert_eq(s->clist.count, 1);
	assert_eq(server_set_chans(s, ",#a,#b,c"), -1);
	assert_eq(s->clist.count, 1);
	assert_eq(server_set_chans(s, "#a,#b,c,"), -1);
	assert_eq(s->clist.count, 1);
	assert_eq(server_set_chans(s, "#a,#b,c "), -1);
	assert_eq(s->clist.count, 1);
	assert_eq(server_set_chans(s, "#a b #c"), -1);
	assert_eq(s->clist.count, 1);

	/* valid */
	assert_eq(server_set_chans(s, "#a,b,c,#d"), 0);
	assert_eq(s->clist.count, 5);

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

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

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

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

	server_free(s);
}

static void
test_server_set_nicks(void)
{
	struct server *s = server("host", "port", NULL, "", "");


@@ 251,9 299,10 @@ main(void)
{
	struct testcase tests[] = {
		TESTCASE(test_server_list),
		TESTCASE(test_server_set_chans),
		TESTCASE(test_server_set_nicks),
		TESTCASE(test_parse_005)
	};

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

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

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

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

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

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

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

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

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

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

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

	server_nick_set(s, "me");

	return 0;
}

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

	return 0;
}

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


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

	int ret = run_tests(tests);

	server_free(s);

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

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

static void


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

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

	server_reset(s);

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

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

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

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

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

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

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

	server_reset(s);

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

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

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

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

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

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

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


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

	join_threshold = 0;
	threshold_join = 0;

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


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

	join_threshold = 2;
	threshold_join = 2;

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



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

	part_threshold = 0;
	threshold_part = 0;

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


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

	part_threshold = 1;
	threshold_part = 1;

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


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

	quit_threshold = 0;
	threshold_quit = 0;

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


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

	quit_threshold = 1;
	threshold_quit = 1;

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


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

	account_threshold = 0;
	threshold_account = 0;

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


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

	account_threshold = 2;
	threshold_account = 2;

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


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

	away_threshold = 0;
	threshold_away = 0;

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


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

	away_threshold = 2;
	threshold_away = 2;

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


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

	chghost_threshold = 0;
	threshold_chghost = 0;

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


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

	chghost_threshold = 2;
	threshold_chghost = 2;

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

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

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

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

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

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

	server_nick_set(s, "me");

	return 0;
}


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

	return 0;
}

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


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


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

	int ret = run_tests(tests);

	server_free(s);

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

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


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

	s->registered = 0;



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

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


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

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



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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

static void


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

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



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

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


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

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


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

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

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


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

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


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

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


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

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


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

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


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

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

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

	c_serv = s->channel;

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

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

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

	c_serv = s->channel;

	s->registered = 1;

	return 0;
}

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

	return 0;
}

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


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

	int ret = run_tests(tests);

	server_free(s);

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

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

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

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

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

	return 0;
}

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

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

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


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

	int ret = run_tests(tests);

	server_free(s);

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

M test/rirc.c => test/rirc.c +6 -5
@@ 17,20 17,21 @@
#include "test/io.mock.c"

static void
test_STUB(void)
test_rirc_parse_args(void)
{
	; /* TODO */
	/* TODO */
	(void)rirc_parse_args;
}

int
main(void)
{
	/* FIXME: */
	(void)parse_args;
	(void)rirc_pw_name;

	struct testcase tests[] = {
		TESTCASE(test_STUB)
		TESTCASE(test_rirc_parse_args)
	};

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

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

	state_init();



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

	/* host1 #c1 #c2, host2 #c3 #c4 */
	c1 = channel("#c1", CHANNEL_T_CHANNEL);
	c2 = channel("#c2", CHANNEL_T_CHANNEL);
	s = server("host", "port", NULL, "user", "real");

	if (!s || !c1 || !c2)
		test_abort("Failed to create server and channels");

	c1->server = s;
	c2->server = s;
	channel_list_add(&(s->clist), c1);
	channel_list_add(&(s->clist), c2);
	c3 = channel("#c3", CHANNEL_T_CHANNEL);
	c4 = channel("#c4", CHANNEL_T_CHANNEL);
	c5 = channel("#c5", CHANNEL_T_CHANNEL);
	s1 = server("host1", "port1", NULL, "user1", "real1");
	s2 = server("host2", "port2", NULL, "user2", "real2");

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

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

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

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

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

	channel_set_current(c1);


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

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

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

	assert_ptr_null(action_handler);
	assert_ptr_null(action_message());
	assert_ptr_null(CURRENT_LINE);
	assert_strcmp(current_channel()->name, "#c2");
	assert_strcmp(current_channel()->name, "host2");

	channel_set_current(s->channel);
	/* test closing server channel */
	channel_set_current(s1->channel);

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

	assert_ptr_null(action_handler);
	assert_ptr_null(action_message());
	assert_strcmp(current_channel()->name, "rirc");
	assert_strcmp(CURRENT_LINE, " - compiled with DEBUG flags");
	assert_strcmp(current_channel()->name, "host2");

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

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

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

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

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

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

	state_term();
}


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

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

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

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


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

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

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

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



@@ 58,32 59,32 @@

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

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

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

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


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


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


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



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


@@ 164,62 165,61 @@
#define fatal_noexit _fatal
#endif

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

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

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

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

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

#define run_tests(INIT, TERM, TESTS) \
	_run_tests_(__FILE__, INIT, TERM, TESTS, (sizeof(TESTS) / sizeof(TESTS[0])))

struct testcase
{
	void (*tc_ptr)(void);
	const char *tc_str;
};

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

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

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


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

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


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

		if (!_failures_t_)
			puts("");

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

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

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

	struct testcase *tc;
	printf("%s...\n", filename);

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

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

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

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

		(*tc->tc_ptr)();

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

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

		_tc_failures_t_ += _tc_failures_;
	}

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

	return EXIT_SUCCESS;
}

#endif

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

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

M test/utils/utils.c => test/utils/utils.c +15 -1
@@ 2,6 2,18 @@
#include "src/utils/utils.c"

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

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

static void
test_irc_message_param(void)
{
	char *param;


@@ 646,6 658,8 @@ int
main(void)
{
	struct testcase tests[] = {
		TESTCASE(test_irc_ischan),
		TESTCASE(test_irc_isnick),
		TESTCASE(test_irc_message_param),
		TESTCASE(test_irc_message_parse),
		TESTCASE(test_irc_message_split),


@@ 658,5 672,5 @@ main(void)
		TESTCASE(test_irc_toupper)
	};

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