/*-
 * Copyright (c) 2003-2006, Maxime Henrion <mux@FreeBSD.org>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 *
 * $FreeBSD: releng/9.2/usr.bin/csup/proto.c 229184 2012-01-01 18:42:00Z dim $
 */

#include <sys/param.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/stat.h>

#include <assert.h>
#include <err.h>
#include <errno.h>
#include <inttypes.h>
#include <netdb.h>
#include <pthread.h>
#include <signal.h>
#include <stdarg.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include "auth.h"
#include "config.h"
#include "detailer.h"
#include "fattr.h"
#include "fixups.h"
#include "globtree.h"
#include "keyword.h"
#include "lister.h"
#include "misc.h"
#include "mux.h"
#include "proto.h"
#include "queue.h"
#include "stream.h"
#include "threads.h"
#include "updater.h"

struct killer {
	pthread_t thread;
	sigset_t sigset;
	struct mux *mux;
	int killedby;
};

static void		 killer_start(struct killer *, struct mux *);
static void		*killer_run(void *);
static void		 killer_stop(struct killer *);

static int		 proto_waitconnect(int);
static int		 proto_greet(struct config *);
static int		 proto_negproto(struct config *);
static int		 proto_fileattr(struct config *);
static int		 proto_xchgcoll(struct config *);
static struct mux	*proto_mux(struct config *);

static int		 proto_escape(struct stream *, const char *);
static void		 proto_unescape(char *);

static int
proto_waitconnect(int s)
{
	fd_set readfd;
	socklen_t len;
	int error, rv, soerror;

	FD_ZERO(&readfd);
	FD_SET(s, &readfd);

	do {
		rv = select(s + 1, &readfd, NULL, NULL, NULL);
	} while (rv == -1 && errno == EINTR);
	if (rv == -1)
		return (-1);
	/* Check that the connection was really successful. */
	len = sizeof(soerror);
	error = getsockopt(s, SOL_SOCKET, SO_ERROR, &soerror, &len);
	if (error) {
		/* We have no choice but faking an error here. */
		errno = ECONNREFUSED;
		return (-1);
	}
	if (soerror) {
		errno = soerror;
		return (-1);
	}
	return (0);
}

/* Connect to the CVSup server. */
int
proto_connect(struct config *config, int family, uint16_t port)
{
	char addrbuf[NI_MAXHOST];
	/* Enough to hold sizeof("cvsup") or any port number. */
	char servname[8];
	struct addrinfo *res, *ai, hints;
	int error, opt, s;

	s = -1;
	if (port != 0)
		snprintf(servname, sizeof(servname), "%d", port);
	else {
		strncpy(servname, "cvsup", sizeof(servname) - 1);
		servname[sizeof(servname) - 1] = '\0';
	}
	memset(&hints, 0, sizeof(hints));
	hints.ai_family = family;
	hints.ai_socktype = SOCK_STREAM;
	error = getaddrinfo(config->host, servname, &hints, &res);
	/*
	 * Try with the hardcoded port number for OSes that don't
	 * have cvsup defined in the /etc/services file.
	 */
	if (error == EAI_SERVICE) {
		strncpy(servname, "5999", sizeof(servname) - 1);
		servname[sizeof(servname) - 1] = '\0';
		error = getaddrinfo(config->host, servname, &hints, &res);
	}
	if (error) {
		lprintf(0, "Name lookup failure for \"%s\": %s\n", config->host,
		    gai_strerror(error));
		return (STATUS_TRANSIENTFAILURE);
	}
	for (ai = res; ai != NULL; ai = ai->ai_next) {
		s = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
		if (s != -1) {
			error = 0;
			if (config->laddr != NULL) {
				opt = 1;
				(void)setsockopt(s, SOL_SOCKET, SO_REUSEADDR,
				    &opt, sizeof(opt));
				error = bind(s, config->laddr,
				    config->laddrlen);
			}
			if (!error) {
				error = connect(s, ai->ai_addr, ai->ai_addrlen);
				if (error && errno == EINTR)
					error = proto_waitconnect(s);
			}
			if (error)
				close(s);
		}
		(void)getnameinfo(ai->ai_addr, ai->ai_addrlen, addrbuf,
		    sizeof(addrbuf), NULL, 0, NI_NUMERICHOST);
		if (s == -1 || error) {
			lprintf(0, "Cannot connect to %s: %s\n", addrbuf,
			    strerror(errno));
			continue;
		}
		lprintf(1, "Connected to %s\n", addrbuf);
		freeaddrinfo(res);
		config->socket = s;
		return (STATUS_SUCCESS);
	}
	freeaddrinfo(res);
	return (STATUS_TRANSIENTFAILURE);
}

/* Greet the server. */
static int
proto_greet(struct config *config)
{
	char *line, *cmd, *msg, *swver;
	struct stream *s;

	s = config->server;
	line = stream_getln(s, NULL);
	cmd = proto_get_ascii(&line);
	if (cmd == NULL)
		goto bad;
	if (strcmp(cmd, "OK") == 0) {
		(void)proto_get_ascii(&line);	/* major number */
		(void)proto_get_ascii(&line);	/* minor number */
		swver = proto_get_ascii(&line);
	} else if (strcmp(cmd, "!") == 0) {
		msg = proto_get_rest(&line);
		if (msg == NULL)
			goto bad;
		lprintf(-1, "Rejected by server: %s\n", msg);
		return (STATUS_TRANSIENTFAILURE);
	} else
		goto bad;
	lprintf(2, "Server software version: %s\n",
	    swver != NULL ? swver : ".");
	return (STATUS_SUCCESS);
bad:
	lprintf(-1, "Invalid greeting from server\n");
	return (STATUS_FAILURE);
}

/* Negotiate protocol version with the server. */
static int
proto_negproto(struct config *config)
{
	struct stream *s;
	char *cmd, *line, *msg;
	int error, maj, min;

	s = config->server;
	proto_printf(s, "PROTO %d %d %s\n", PROTO_MAJ, PROTO_MIN, PROTO_SWVER);
	stream_flush(s);
	line = stream_getln(s, NULL);
	cmd = proto_get_ascii(&line);
	if (cmd == NULL || line == NULL)
		goto bad;
	if (strcmp(cmd, "!") == 0) {
		msg = proto_get_rest(&line);
		lprintf(-1, "Protocol negotiation failed: %s\n", msg);
		return (1);
	} else if (strcmp(cmd, "PROTO") != 0)
		goto bad;
	error = proto_get_int(&line, &maj, 10);
	if (!error)
		error = proto_get_int(&line, &min, 10);
	if (error)
		goto bad;
	if (maj != PROTO_MAJ || min != PROTO_MIN) {
		lprintf(-1, "Server protocol version %d.%d not supported "
		    "by client\n", maj, min);
		return (STATUS_FAILURE);
	}
	return (STATUS_SUCCESS);
bad:
	lprintf(-1, "Invalid PROTO command from server\n");
	return (STATUS_FAILURE);
}

/*
 * File attribute support negotiation.
 */
static int
proto_fileattr(struct config *config)
{
	fattr_support_t support;
	struct stream *s;
	char *line, *cmd;
	int error, i, n, attr;

	s = config->server;
	lprintf(2, "Negotiating file attribute support\n");
	proto_printf(s, "ATTR %d\n", FT_NUMBER);
	for (i = 0; i < FT_NUMBER; i++)
		proto_printf(s, "%x\n", fattr_supported(i));
	proto_printf(s, ".\n");
	stream_flush(s);
	line = stream_getln(s, NULL);
	if (line == NULL)
		goto bad;
	cmd = proto_get_ascii(&line);
	error = proto_get_int(&line, &n, 10);
	if (error || line != NULL || strcmp(cmd, "ATTR") != 0 || n > FT_NUMBER)
		goto bad;
	for (i = 0; i < n; i++) {
		line = stream_getln(s, NULL);
		if (line == NULL)
			goto bad;
		error = proto_get_int(&line, &attr, 16);
		if (error)
			goto bad;
		support[i] = fattr_supported(i) & attr;
	}
	for (i = n; i < FT_NUMBER; i++)
		support[i] = 0;
	line = stream_getln(s, NULL);
	if (line == NULL || strcmp(line, ".") != 0)
		goto bad;
	memcpy(config->fasupport, support, sizeof(config->fasupport));
	return (STATUS_SUCCESS);
bad:
	lprintf(-1, "Protocol error negotiating attribute support\n");
	return (STATUS_FAILURE);
}

/*
 * Exchange collection information.
 */
static int
proto_xchgcoll(struct config *config)
{
	struct coll *coll;
	struct stream *s;
	struct globtree *diraccept, *dirrefuse;
	struct globtree *fileaccept, *filerefuse;
	char *line, *cmd, *collname, *pat;
	char *msg, *release, *ident, *rcskey, *prefix;
	size_t i, len;
	int error, flags, options;

	s = config->server;
	lprintf(2, "Exchanging collection information\n");
	STAILQ_FOREACH(coll, &config->colls, co_next) {
		if (coll->co_options & CO_SKIP)
			continue;
		proto_printf(s, "COLL %s %s %o %d\n", coll->co_name,
		    coll->co_release, coll->co_umask, coll->co_options);
		for (i = 0; i < pattlist_size(coll->co_accepts); i++) {
		    proto_printf(s, "ACC %s\n",
			pattlist_get(coll->co_accepts, i));
		}
		for (i = 0; i < pattlist_size(coll->co_refusals); i++) {
		    proto_printf(s, "REF %s\n",
			pattlist_get(coll->co_refusals, i));
		}
		proto_printf(s, ".\n");
	}
	proto_printf(s, ".\n");
	stream_flush(s);

	STAILQ_FOREACH(coll, &config->colls, co_next) {
		if (coll->co_options & CO_SKIP)
			continue;
		coll->co_norsync = globtree_false();
		line = stream_getln(s, NULL);
		if (line == NULL)
			goto bad;
		cmd = proto_get_ascii(&line);
		collname = proto_get_ascii(&line);
		release = proto_get_ascii(&line);
		error = proto_get_int(&line, &options, 10);
		if (error || line != NULL)
			goto bad;
		if (strcmp(cmd, "COLL") != 0 ||
		    strcmp(collname, coll->co_name) != 0 ||
		    strcmp(release, coll->co_release) != 0)
			goto bad;
		coll->co_options =
		    (coll->co_options | (options & CO_SERVMAYSET)) &
		    ~(~options & CO_SERVMAYCLEAR);
		while ((line = stream_getln(s, NULL)) != NULL) {
		 	if (strcmp(line, ".") == 0)
				break;
			cmd = proto_get_ascii(&line);
			if (cmd == NULL)
				goto bad;
			if (strcmp(cmd, "!") == 0) {
				msg = proto_get_rest(&line);
				if (msg == NULL)
					goto bad;
				lprintf(-1, "Server message: %s\n", msg);
			} else if (strcmp(cmd, "PRFX") == 0) {
				prefix = proto_get_ascii(&line);
				if (prefix == NULL || line != NULL)
					goto bad;
				coll->co_cvsroot = xstrdup(prefix);
			} else if (strcmp(cmd, "KEYALIAS") == 0) {
				ident = proto_get_ascii(&line);
				rcskey = proto_get_ascii(&line);
				if (rcskey == NULL || line != NULL)
					goto bad;
				error = keyword_alias(coll->co_keyword, ident,
				    rcskey);
				if (error)
					goto bad;
			} else if (strcmp(cmd, "KEYON") == 0) {
				ident = proto_get_ascii(&line);
				if (ident == NULL || line != NULL)
					goto bad;
				error = keyword_enable(coll->co_keyword, ident);
				if (error)
					goto bad;
			} else if (strcmp(cmd, "KEYOFF") == 0) {
				ident = proto_get_ascii(&line);
				if (ident == NULL || line != NULL)
					goto bad;
				error = keyword_disable(coll->co_keyword,
				    ident);
				if (error)
					goto bad;
			} else if (strcmp(cmd, "NORS") == 0) {
				pat = proto_get_ascii(&line);
				if (pat == NULL || line != NULL)
					goto bad;
				coll->co_norsync = globtree_or(coll->co_norsync,
				    globtree_match(pat, FNM_PATHNAME));
			} else if (strcmp(cmd, "RNORS") == 0) {
				pat = proto_get_ascii(&line);
				if (pat == NULL || line != NULL)
					goto bad;
				coll->co_norsync = globtree_or(coll->co_norsync,
				    globtree_match(pat, FNM_PATHNAME |
				    FNM_LEADING_DIR));
			} else
				goto bad;
		}
		if (line == NULL)
			goto bad;
		keyword_prepare(coll->co_keyword);

		diraccept = globtree_true();
		fileaccept = globtree_true();
		dirrefuse = globtree_false();
		filerefuse = globtree_false();

		if (pattlist_size(coll->co_accepts) > 0) {
			globtree_free(diraccept);
			globtree_free(fileaccept);
			diraccept = globtree_false();
			fileaccept = globtree_false();
			flags = FNM_PATHNAME | FNM_LEADING_DIR |
			    FNM_PREFIX_DIRS;
			for (i = 0; i < pattlist_size(coll->co_accepts); i++) {
				pat = pattlist_get(coll->co_accepts, i);
				diraccept = globtree_or(diraccept,
				    globtree_match(pat, flags));

				len = strlen(pat);
				if (coll->co_options & CO_CHECKOUTMODE &&
				    (len == 0 || pat[len - 1] != '*')) {
					/* We must modify the pattern so that it
					   refers to the RCS file, rather than
					   the checked-out file. */
					xasprintf(&pat, "%s,v", pat);
					fileaccept = globtree_or(fileaccept,
					    globtree_match(pat, flags));
					free(pat);
				} else {
					fileaccept = globtree_or(fileaccept,
					    globtree_match(pat, flags));
				}
			}
		}

		for (i = 0; i < pattlist_size(coll->co_refusals); i++) {
			pat = pattlist_get(coll->co_refusals, i);
			dirrefuse = globtree_or(dirrefuse,
			    globtree_match(pat, 0));
			len = strlen(pat);
			if (coll->co_options & CO_CHECKOUTMODE &&
			    (len == 0 || pat[len - 1] != '*')) {
				/* We must modify the pattern so that it refers
				   to the RCS file, rather than the checked-out
				   file. */
				xasprintf(&pat, "%s,v", pat);
				filerefuse = globtree_or(filerefuse,
				    globtree_match(pat, 0));
				free(pat);
			} else {
				filerefuse = globtree_or(filerefuse,
				    globtree_match(pat, 0));
			}
		}

		coll->co_dirfilter = globtree_and(diraccept,
		    globtree_not(dirrefuse));
		coll->co_filefilter = globtree_and(fileaccept,
		    globtree_not(filerefuse));

		/* Set up a mask of file attributes that we don't want to sync
		   with the server. */
		if (!(coll->co_options & CO_SETOWNER))
			coll->co_attrignore |= FA_OWNER | FA_GROUP;
		if (!(coll->co_options & CO_SETMODE))
			coll->co_attrignore |= FA_MODE;
		if (!(coll->co_options & CO_SETFLAGS))
			coll->co_attrignore |= FA_FLAGS;
	}
	return (STATUS_SUCCESS);
bad:
	lprintf(-1, "Protocol error during collection exchange\n");
	return (STATUS_FAILURE);
}

static struct mux *
proto_mux(struct config *config)
{
	struct mux *m;
	struct stream *s, *wr;
	struct chan *chan0, *chan1;
	int id;

	s = config->server;
	lprintf(2, "Establishing multiplexed-mode data connection\n");
	proto_printf(s, "MUX\n");
	stream_flush(s);
	m = mux_open(config->socket, &chan0);
	if (m == NULL) {
		lprintf(-1, "Cannot open the multiplexer\n");
		return (NULL);
	}
	id = chan_listen(m);
	if (id == -1) {
		lprintf(-1, "ChannelMux.Listen failed: %s\n", strerror(errno));
		mux_close(m);
		return (NULL);
	}
	wr = stream_open(chan0, NULL, (stream_writefn_t *)chan_write, NULL);
	proto_printf(wr, "CHAN %d\n", id);
	stream_close(wr);
	chan1 = chan_accept(m, id);
	if (chan1 == NULL) {
		lprintf(-1, "ChannelMux.Accept failed: %s\n", strerror(errno));
		mux_close(m);
		return (NULL);
	}
	config->chan0 = chan0;
	config->chan1 = chan1;
	return (m);
}

/*
 * Initializes the connection to the CVSup server, that is handle
 * the protocol negotiation, logging in, exchanging file attributes
 * support and collections information, and finally run the update
 * session.
 */
int
proto_run(struct config *config)
{
	struct thread_args lister_args;
	struct thread_args detailer_args;
	struct thread_args updater_args;
	struct thread_args *args;
	struct killer killer;
	struct threads *workers;
	struct mux *m;
	int i, status;

	/*
	 * We pass NULL for the close() function because we'll reuse
	 * the socket after the stream is closed.
	 */
	config->server = stream_open_fd(config->socket, stream_read_fd,
	    stream_write_fd, NULL);
	status = proto_greet(config);
	if (status == STATUS_SUCCESS)
		status = proto_negproto(config);
	if (status == STATUS_SUCCESS)
		status = auth_login(config);
	if (status == STATUS_SUCCESS)
		status = proto_fileattr(config);
	if (status == STATUS_SUCCESS)
		status = proto_xchgcoll(config);
	if (status != STATUS_SUCCESS)
		return (status);

	/* Multi-threaded action starts here. */
	m = proto_mux(config);
	if (m == NULL)
		return (STATUS_FAILURE);

	stream_close(config->server);
	config->server = NULL;
	config->fixups = fixups_new();
	killer_start(&killer, m);

	/* Start the worker threads. */
	workers = threads_new();
	args = &lister_args;
	args->config = config;
	args->status = -1;
	args->errmsg = NULL;
	args->rd = NULL;
	args->wr = stream_open(config->chan0,
	    NULL, (stream_writefn_t *)chan_write, NULL);
	threads_create(workers, lister, args);

	args = &detailer_args;
	args->config = config;
	args->status = -1;
	args->errmsg = NULL;
	args->rd = stream_open(config->chan0,
	    (stream_readfn_t *)chan_read, NULL, NULL);
	args->wr = stream_open(config->chan1,
	    NULL, (stream_writefn_t *)chan_write, NULL);
	threads_create(workers, detailer, args);

	args = &updater_args;
	args->config = config;
	args->status = -1;
	args->errmsg = NULL;
	args->rd = stream_open(config->chan1,
	    (stream_readfn_t *)chan_read, NULL, NULL);
	args->wr = NULL;
	threads_create(workers, updater, args);

	lprintf(2, "Running\n");
	/* Wait for all the worker threads to finish. */
	status = STATUS_SUCCESS;
	for (i = 0; i < 3; i++) {
		args = threads_wait(workers);
		if (args->rd != NULL)
			stream_close(args->rd);
		if (args->wr != NULL)
			stream_close(args->wr);
		if (args->status != STATUS_SUCCESS) {
			assert(args->errmsg != NULL);
			if (status == STATUS_SUCCESS) {
				status = args->status;
				/* Shutdown the multiplexer to wake up all
				   the other threads. */
				mux_shutdown(m, args->errmsg, status);
			}
			free(args->errmsg);
		}
	}
	threads_free(workers);
	if (status == STATUS_SUCCESS) {
		lprintf(2, "Shutting down connection to server\n");
		chan_close(config->chan0);
		chan_close(config->chan1);
		chan_wait(config->chan0);
		chan_wait(config->chan1);
		mux_shutdown(m, NULL, STATUS_SUCCESS);
	}
	killer_stop(&killer);
	fixups_free(config->fixups);
	status = mux_close(m);
	if (status == STATUS_SUCCESS) {
		lprintf(1, "Finished successfully\n");
	} else if (status == STATUS_INTERRUPTED) {
		lprintf(-1, "Interrupted\n");
		if (killer.killedby != -1)
			kill(getpid(), killer.killedby);
	}
	return (status);
}

/*
 * Write a string into the stream, escaping characters as needed.
 * Characters escaped:
 *
 * SPACE	-> "\_"
 * TAB		->  "\t"
 * NEWLINE	-> "\n"
 * CR		-> "\r"
 * \		-> "\\"
 */
static int
proto_escape(struct stream *wr, const char *s)
{
	size_t len;
	ssize_t n;
	char c;

	/* Handle characters that need escaping. */
	do {
		len = strcspn(s, " \t\r\n\\");
		n = stream_write(wr, s, len);
		if (n == -1)
			return (-1);
		c = s[len];
		switch (c) {
		case ' ':
			n = stream_write(wr, "\\_", 2);
			break;
		case '\t':
			n = stream_write(wr, "\\t", 2);
			break;
		case '\r':
			n = stream_write(wr, "\\r", 2);
			break;
		case '\n':
			n = stream_write(wr, "\\n", 2);
			break;
		case '\\':
			n = stream_write(wr, "\\\\", 2);
			break;
		}
		if (n == -1)
			return (-1);
		s += len + 1;
	} while (c != '\0');
	return (0);
}

/*
 * A simple printf() implementation specifically tailored for csup.
 * List of the supported formats:
 *
 * %c		Print a char.
 * %d or %i	Print an int as decimal.
 * %x		Print an int as hexadecimal.
 * %o		Print an int as octal.
 * %t		Print a time_t as decimal.
 * %s		Print a char * escaping some characters as needed.
 * %S		Print a char * without escaping.
 * %f		Print an encoded struct fattr *.
 * %F		Print an encoded struct fattr *, specifying the supported
 * 		attributes.
 */
int
proto_printf(struct stream *wr, const char *format, ...)
{
	fattr_support_t *support;
	long long longval;
	struct fattr *fa;
	const char *fmt;
	va_list ap;
	char *cp, *s, *attr;
	ssize_t n;
	size_t size;
	off_t off;
	int rv, val, ignore;
	char c;

	n = 0;
	rv = 0;
	fmt = format;
	va_start(ap, format);
	while ((cp = strchr(fmt, '%')) != NULL) {
		if (cp > fmt) {
			n = stream_write(wr, fmt, cp - fmt);
			if (n == -1)
				return (-1);
		}
		if (*++cp == '\0')
			goto done;
		switch (*cp) {
		case 'c':
			c = va_arg(ap, int);
			rv = stream_printf(wr, "%c", c);
			break;
		case 'd':
		case 'i':
			val = va_arg(ap, int);
			rv = stream_printf(wr, "%d", val);
			break;
		case 'x':
			val = va_arg(ap, int);
			rv = stream_printf(wr, "%x", val);
			break;
		case 'o':
			val = va_arg(ap, int);
			rv = stream_printf(wr, "%o", val);
			break;
		case 'O':
			off = va_arg(ap, off_t);
			rv = stream_printf(wr, "%" PRId64, off);
			break;
		case 'S':
			s = va_arg(ap, char *);
			assert(s != NULL);
			rv = stream_printf(wr, "%s", s);
			break;
		case 's':
			s = va_arg(ap, char *);
			assert(s != NULL);
			rv = proto_escape(wr, s);
			break;
		case 't':
			longval = (long long)va_arg(ap, time_t);
			rv = stream_printf(wr, "%lld", longval);
			break;
		case 'f':
			fa = va_arg(ap, struct fattr *);
			attr = fattr_encode(fa, NULL, 0);
			rv = proto_escape(wr, attr);
			free(attr);
			break;
		case 'F':
			fa = va_arg(ap, struct fattr *);
			support = va_arg(ap, fattr_support_t *);
			ignore = va_arg(ap, int);
			attr = fattr_encode(fa, *support, ignore);
			rv = proto_escape(wr, attr);
			free(attr);
			break;
		case 'z':
			size = va_arg(ap, size_t);
			rv = stream_printf(wr, "%zu", size);
			break;

		case '%':
			n = stream_write(wr, "%", 1);
			if (n == -1)
				return (-1);
			break;
		}
		if (rv == -1)
			return (-1);
		fmt = cp + 1;
	}
	if (*fmt != '\0') {
		rv = stream_printf(wr, "%s", fmt);
		if (rv == -1)
			return (-1);
	}
done:
	va_end(ap);
	return (0);
}

/*
 * Unescape the string, see proto_escape().
 */
static void
proto_unescape(char *s)
{
	char *cp, *cp2;

	cp = s;
	while ((cp = strchr(cp, '\\')) != NULL) {
		switch (cp[1]) {
		case '_':
			*cp = ' ';
			break;
		case 't':
			*cp = '\t';
			break;
		case 'r':
			*cp = '\r';
			break;
		case 'n':
			*cp = '\n';
			break;
		case '\\':
			*cp = '\\';
			break;
		default:
			*cp = *(cp + 1);
		}
		cp2 = ++cp;
		while (*cp2 != '\0') {
			*cp2 = *(cp2 + 1);
			cp2++;
		}
	}
}

/*
 * Get an ascii token in the string.
 */
char *
proto_get_ascii(char **s)
{
	char *ret;

	ret = strsep(s, " ");
	if (ret == NULL)
		return (NULL);
	/* Make sure we disallow 0-length fields. */
	if (*ret == '\0') {
		*s = NULL;
		return (NULL);
	}
	proto_unescape(ret);
	return (ret);
}

/*
 * Get the rest of the string.
 */
char *
proto_get_rest(char **s)
{
	char *ret;

	if (s == NULL)
		return (NULL);
	ret = *s;
	proto_unescape(ret);
	*s = NULL;
	return (ret);
}

/*
 * Get an int token.
 */
int
proto_get_int(char **s, int *val, int base)
{
	char *cp;
	int error;

	cp = proto_get_ascii(s);
	if (cp == NULL)
		return (-1);
	error = asciitoint(cp, val, base);
	return (error);
}

/*
 * Get a size_t token.
 */
int
proto_get_sizet(char **s, size_t *val, int base)
{
	unsigned long long tmp;
	char *cp, *end;

	cp = proto_get_ascii(s);
	if (cp == NULL)
		return (-1);
	errno = 0;
	tmp = strtoll(cp, &end, base);
	if (errno || *end != '\0')
		return (-1);
	*val = (size_t)tmp;
	return (0);
}

/*
 * Get a time_t token.
 *
 * Ideally, we would use an intmax_t and strtoimax() here, but strtoll()
 * is more portable and 64bits should be enough for a timestamp.
 */
int
proto_get_time(char **s, time_t *val)
{
	long long tmp;
	char *cp, *end;

	cp = proto_get_ascii(s);
	if (cp == NULL)
		return (-1);
	errno = 0;
	tmp = strtoll(cp, &end, 10);
	if (errno || *end != '\0')
		return (-1);
	*val = (time_t)tmp;
	return (0);
}

/* Start the killer thread.  It is used to protect against some signals
   during the multi-threaded run so that we can gracefully fail.  */
static void
killer_start(struct killer *k, struct mux *m)
{
	int error;

	k->mux = m;
	k->killedby = -1;
	sigemptyset(&k->sigset);
	sigaddset(&k->sigset, SIGINT);
	sigaddset(&k->sigset, SIGHUP);
	sigaddset(&k->sigset, SIGTERM);
	sigaddset(&k->sigset, SIGPIPE);
	pthread_sigmask(SIG_BLOCK, &k->sigset, NULL);
	error = pthread_create(&k->thread, NULL, killer_run, k);
	if (error)
		err(1, "pthread_create");
}

/* The main loop of the killer thread. */
static void *
killer_run(void *arg)
{
	struct killer *k;
	int error, sig, old;

	k = arg;
again:
	error = sigwait(&k->sigset, &sig);
	assert(!error);
	if (sig == SIGINT || sig == SIGHUP || sig == SIGTERM) {
		if (k->killedby == -1) {
			k->killedby = sig;
			/* Ensure we don't get canceled during the shutdown. */
			pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &old);
			mux_shutdown(k->mux, "Cleaning up ...",
			    STATUS_INTERRUPTED);
			pthread_setcancelstate(old, NULL);
		}
	}
	goto again;
}

/* Stop the killer thread. */
static void
killer_stop(struct killer *k)
{
	void *val;
	int error;

	error = pthread_cancel(k->thread);
	assert(!error);
	pthread_join(k->thread, &val);
	assert(val == PTHREAD_CANCELED);
	pthread_sigmask(SIG_UNBLOCK, &k->sigset, NULL);
}
