changeset 0:a7e41e1a79c8

Initial revision
author Guido Berhoerster <guido+pwm@berhoerster.name>
date Thu, 19 Jan 2017 22:39:51 +0100
parents
children 55281f14dc9b
files Makefile cmd.c cmd.h compat.h compat/asprintf.c compat/asprintf.h compat/err.c compat/err.h compat/getentropy.c compat/getentropy.h compat/readpassphrase.c compat/readpassphrase.h compat/setprogname.c compat/setprogname.h compat/tree.h deps.sed pwfile.c pwfile.h pwm.c pwm.h tok.c tok.h util.c util.h
diffstat 24 files changed, 3973 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Makefile	Thu Jan 19 22:39:51 2017 +0100
@@ -0,0 +1,167 @@
+#
+# Copyright (C) 2016 Guido Berhoerster <guido+pwm@berhoerster.name>
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+PACKAGE =	pwm
+VERSION =	1
+DISTNAME :=	$(PACKAGE)-$(VERSION)
+
+# gcc, clang, icc, Sun/Solaris Studio
+CC :=		$(CC) -std=c99
+COMPILE.c =	$(CC) $(CFLAGS) $(XCFLAGS) $(CPPFLAGS) $(XCPPFLAGS) $(TARGET_ARCH) -c
+# gcc, clang, icc
+MAKEDEPEND.c =	$(CC) -MM $(CFLAGS) $(XCFLAGS) $(CPPFLAGS) $(XCPPFLAGS)
+# Sun/Solaris Studio
+#MAKEDEPEND.c =	$(CC) -xM1 $(CFLAGS) $(XCFLAGS) $(CPPFLAGS) $(XCPPFLAGS)
+# X makedepend
+#MAKEDEPEND.c =	makedepend -f- -Y -- $(CFLAGS) $(XCFLAGS) $(CPPFLAGS) $(XCPPFLAGS) --
+LINK.c =	$(CC) $(CFLAGS) $(XCFLAGS) $(CPPFLAGS) $(XCPPFLAGS) $(LDFLAGS) $(XLDFLAGS) $(TARGET_ARCH)
+LINK.o =	$(CC) $(LDFLAGS) $(XLDFLAGS) $(TARGET_ARCH)
+CP :=		cp
+INSTALL :=	install
+INSTALL.exec :=	$(INSTALL) -D -m 0755
+INSTALL.data :=	$(INSTALL) -D -m 0644
+INSTALL.link :=	$(CP) -f -P
+PAX :=		pax
+GZIP :=		gzip
+SED :=		sed
+
+DESTDIR ?=
+prefix ?=	/usr/local
+bindir ?=	$(prefix)/bin
+
+OS_NAME :=	$(shell uname -s)
+OS_RELEASE :=	$(shell uname -r)
+
+ifeq ($(OS_NAME),Linux)
+  HAVE_ASPRINTF ?=	1
+  HAVE_ERR_H ?=		1
+  HAVE_READPASSPHRASE_H ?= 0
+  HAVE_SETPROGNAME ?=	0
+  HAVE_SYS_TREE_H ?=	0
+else ifneq ($(findstring $(OS_NAME),FreeBSD DragonFly),)
+  HAVE_ASPRINTF ?=	1
+  HAVE_ERR_H ?=		1
+  HAVE_READPASSPHRASE_H ?= 1
+  HAVE_SETPROGNAME ?=	1
+  HAVE_SYS_TREE_H ?=	1
+else ifeq ($(OS_NAME),NetBSD)
+  HAVE_ASPRINTF ?=	1
+  HAVE_ERR_H ?=		1
+  HAVE_READPASSPHRASE_H ?= 0
+  HAVE_SYS_TREE_H ?=	1
+  HAVE_SETPROGNAME ?=	1
+else ifeq ($(OS_NAME),OpenBSD)
+  HAVE_ASPRINTF ?=	1
+  HAVE_ERR_H ?=		1
+  HAVE_READPASSPHRASE_H ?= 1
+  HAVE_SYS_TREE_H ?=	1
+  HAVE_SETPROGNAME ?=	1
+else ifeq ($(OS_NAME),SunOS)
+  ifeq ($(OS_RELEASE),5.10)
+    HAVE_ASPRINTF ?=	0
+    HAVE_ERR_H ?=	0
+  else
+    HAVE_ASPRINTF ?=	1
+    HAVE_ERR_H ?=	1
+  endif
+  HAVE_READPASSPHRASE_H ?= 0
+  HAVE_SYS_TREE_H ?=	0
+  HAVE_SETPROGNAME ?=	0
+else
+  HAVE_ASPRINTF ?=	0
+  HAVE_ERR_H ?=		0
+  HAVE_READPASSPHRASE_H ?= 0
+  HAVE_SETPROGNAME ?=	0
+  HAVE_SYS_TREE_H ?=	0
+endif
+
+OBJS =	cmd.o \
+	pwfile.o \
+	pwm.o \
+	tok.o \
+	util.o
+
+.DEFAULT_TARGET = all
+
+.PHONY: all clean clobber dist install
+
+all: $(PACKAGE)
+
+XCPPFLAGS =	-DPACKAGE=\"$(PACKAGE)\" \
+		-DVERSION=\"$(VERSION)\"
+LDLIBS =	-lpws -lnettle
+ifeq ($(HAVE_ERR_H),1)
+  XCPPFLAGS +=	-DHAVE_ERR_H
+else
+  OBJS +=	compat/err.o
+endif
+ifeq ($(HAVE_READPASSPHRASE_H),1)
+  XCPPFLAGS +=	-DHAVE_READPASSPHRASE_H
+else
+  OBJS +=	compat/readpassphrase.o
+endif
+ifeq ($(HAVE_SETPROGNAME),1)
+  XCPPFLAGS +=	-DHAVE_SETPROGNAME
+else
+  OBJS +=	compat/setprogname.o
+endif
+ifeq ($(HAVE_SYS_TREE_H),1)
+  XCPPFLAGS +=	-DHAVE_SYS_TREE_H
+endif
+ifneq ($(findstring $(OS_NAME),FreeBSD DragonFly OpenBSD),)
+  XCPPFLAGS +=	-I/usr/local/include
+  XLDFLAGS +=	-L/usr/local/lib
+else ifeq ($(OS_NAME),NetBSD)
+  XCPPFLAGS +=	-I/usr/pkg/include
+  XLDFLAGS +=	-L/usr/pkg/lib
+endif
+ifeq ($(findstring $(OS_NAME),FreeBSD DragonFly NetBSD OpenBSD),)
+  XCPPFLAGS +=	-D_XOPEN_SOURCE=600
+endif
+ifeq ($(OS_NAME),SunOS)
+  XCPPFLAGS +=	-I/usr/xpg4/include -D__EXTENSIONS__
+  XLDFLAGS +=	-L/usr/xpg4/lib -R/usr/xpg4/lib
+endif
+
+$(PACKAGE): $(OBJS)
+	$(LINK.o) $^ $(LDLIBS) -o $@
+
+%.o: %.c
+	$(MAKEDEPEND.c) $< | $(SED) -f deps.sed >$*.d
+	$(COMPILE.c) -o $@ $<
+
+install:
+	$(INSTALL.exec) $(PACKAGE) "$(DESTDIR)$(bindir)/$(PACKAGE)"
+
+clean:
+	rm -f $(PACKAGE) $(OBJS)
+
+clobber: clean
+	rm -f $(patsubst %.o,%.d,$(OBJS))
+
+dist: clobber
+	$(PAX) -w -x ustar -s ',.*/\..*,,' -s ',./[^/]*\.tar\.gz,,' \
+	    -s ',^\.$$,,' -s ',\./,$(DISTNAME)/,' . | \
+	    $(GZIP) > $(DISTNAME).tar.gz
+
+-include $(patsubst %.o,%.d,$(OBJS))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cmd.c	Thu Jan 19 22:39:51 2017 +0100
@@ -0,0 +1,546 @@
+/*
+ * Copyright (C) 2016 Guido Berhoerster <guido+pwm@berhoerster.name>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#include "compat.h"
+
+#ifdef	HAVE_ERR_H
+#include <err.h>
+#endif /* HAVE_ERR_H */
+#include <errno.h>
+#include <limits.h>
+#ifdef	HAVE_READPASSPHRASE_H
+#include <readpassphrase.h>
+#endif /* READPASSPHRASE_H */
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "cmd.h"
+#include "pwfile.h"
+#include "util.h"
+
+enum field_type {
+	FIELD_UNKNOWN = -1,
+	FIELD_GROUP,
+	FIELD_TITLE,
+	FIELD_USERNAME,
+	FIELD_PASSWORD,
+	FIELD_NOTES,
+	FIELD_URL
+};
+
+static enum cmd_return	cmd_list(struct pwm_ctx *, int, char *[]);
+static enum cmd_return	cmd_create(struct pwm_ctx *, int, char *[]);
+static enum cmd_return	cmd_modify(struct pwm_ctx *, int, char *[]);
+static enum cmd_return	cmd_remove(struct pwm_ctx *, int, char *[]);
+static enum cmd_return	cmd_show(struct pwm_ctx *, int, char *[]);
+static enum cmd_return	cmd_pipe(struct pwm_ctx *, int, char *[]);
+static enum cmd_return	cmd_creategroup(struct pwm_ctx *, int, char *[]);
+static enum cmd_return	cmd_removegroup(struct pwm_ctx *, int, char *[]);
+static enum cmd_return	cmd_changepassword(struct pwm_ctx *, int, char *[]);
+static enum cmd_return	cmd_help(struct pwm_ctx *, int, char *[]);
+static enum cmd_return	cmd_write(struct pwm_ctx *, int, char *[]);
+static enum cmd_return	cmd_quit(struct pwm_ctx *, int, char *[]);
+
+static const char *field_names[] = {
+    "group",
+    "title",
+    "username",
+    "password",
+    "notes",
+    "url"
+};
+
+static const char *field_labels[] = {
+    "Group:    ",
+    "Title:    ",
+    "Username: ",
+    "Password: ",
+    "Notes:    ",
+    "URL:      "
+};
+
+static struct cmd cmds[] = {
+    { "ls", "list", "list", "List entries", cmd_list },
+    { "c", "create", "create field=value ...", "Create entry", cmd_create },
+    { "m", "modify", "modify id field=value ...", "Modify entry", cmd_modify },
+    { "rm", "remove", "remove id", "Delete entry", cmd_remove },
+    { "s", "show", "show id field", "Show entry", cmd_show },
+    { "p", "pipe", "pipe id field command", "Pipe entry to external command",
+    cmd_pipe },
+    { "cg", "creategroup", "creategroup name", "Create empty group",
+    cmd_creategroup },
+    { "rg", "removegroup", "removegroup name", "Delete empty group",
+    cmd_removegroup },
+    { "ch", "changepassword", "changepassword", "Change password",
+    cmd_changepassword },
+    { "h", "help", "help", "Show help text", cmd_help },
+    { "w", "write", "write", "Write the database", cmd_write },
+    { "q", "quit", "quit", "Quit", cmd_quit },
+    { 0 }
+};
+
+static enum field_type
+parse_field_name(const char *name)
+{
+	int	i;
+
+	for (i = 0; i < (int)COUNTOF(field_names); i++) {
+		if (strcmp(field_names[i], name) == 0) {
+			return (i);
+		}
+	}
+
+	return (FIELD_UNKNOWN);
+}
+
+static enum field_type
+parse_field_assignment(char *arg, struct record *record)
+{
+	int	i;
+	size_t	field_name_len;
+	char	*value;
+
+	for (i = 0; i < (int)COUNTOF(field_names); i++) {
+		field_name_len = strlen(field_names[i]);
+		if ((strncmp(field_names[i], arg, field_name_len) == 0) &&
+		    (arg[field_name_len] == '=')){
+			value = arg + field_name_len + 1;
+			if (*value == '\0') {
+				/* skip empty assignments */
+				return (i);
+			}
+			switch (i) {
+			case FIELD_GROUP:
+				record->group = value;
+				break;
+			case FIELD_TITLE:
+				record->title = value;
+				break;
+			case FIELD_USERNAME:
+				record->username = value;
+				break;
+			case FIELD_PASSWORD:
+				record->password = value;
+				break;
+			case FIELD_NOTES:
+				record->notes = value;
+				break;
+			case FIELD_URL:
+				record->url = value;
+				break;
+			default:
+				break;
+			}
+			return (i);
+		}
+	}
+
+	return (FIELD_UNKNOWN);
+}
+
+static int
+parse_id(const char *arg, unsigned int *idp)
+{
+	long	x;
+	char	*p;
+
+	errno = 0;
+	x = strtol(arg, &p, 10);
+	if ((errno != 0) || (*arg == '\0') || (*p != '\0') || (x > UINT_MAX) ||
+	    (x <= 0)) {
+		return (-1);
+	}
+	*idp = (unsigned int)x;
+
+	return (0);
+}
+
+static enum cmd_return
+cmd_list(struct pwm_ctx *ctx, int argc, char *argv[])
+{
+	union list_item	**list;
+	size_t		i;
+
+	if (argc != 1) {
+		return (CMD_USAGE);
+	}
+
+	list = pwfile_create_list(ctx);
+	for (i = 0; list[i] != NULL; i++) {
+		if (list[i]->any.type == ITEM_TYPE_GROUP) {
+			printf("[%s]\n", list[i]->group.group);
+		} else {
+			printf("%4u %s\n", list[i]->record.id,
+			    (list[i]->record.title != NULL) ?
+			    list[i]->record.title : "");
+		}
+	}
+	pwfile_destroy_list(list);
+
+	return (CMD_OK);
+}
+
+static enum cmd_return
+cmd_create(struct pwm_ctx *ctx, int argc, char *argv[])
+{
+	int		i;
+	struct record record = { 0 };
+
+	if (argc < 2) {
+		return (CMD_USAGE);
+	}
+
+	for (i = 1; i < argc; i++) {
+		if (parse_field_assignment(argv[i], &record) == FIELD_UNKNOWN) {
+			fprintf(stderr, "bad field assignment \"%s\"\n",
+			    argv[i]);
+			return (CMD_ERR);
+		}
+	}
+
+	pwfile_create_record(ctx, &record);
+
+	return (CMD_OK);
+}
+
+static enum cmd_return
+cmd_modify(struct pwm_ctx *ctx, int argc, char *argv[])
+{
+	unsigned int	id;
+	int		i;
+	struct record record = { 0 };
+
+	if (argc < 2) {
+		return (CMD_USAGE);
+	}
+
+	if (parse_id(argv[1], &id) != 0) {
+		fprintf(stderr, "invalid id %s\n", argv[1]);
+		return (CMD_ERR);
+	}
+
+	for (i = 2; i < argc; i++) {
+		if (parse_field_assignment(argv[i], &record) == FIELD_UNKNOWN) {
+			fprintf(stderr, "bad field assignment \"%s\"\n",
+			    argv[i]);
+			return (CMD_ERR);
+		}
+	}
+
+	pwfile_modify_record(ctx, id, &record);
+
+	return (CMD_OK);
+}
+
+static enum cmd_return
+cmd_remove(struct pwm_ctx *ctx, int argc, char *argv[])
+{
+	unsigned int	id;
+
+	if (argc != 2) {
+		return (CMD_USAGE);
+	}
+
+	if (parse_id(argv[1], &id) != 0) {
+		fprintf(stderr, "invalid id %s\n", argv[1]);
+		return (CMD_ERR);
+	}
+
+	if (pwfile_remove_record(ctx, id) != 0) {
+		fprintf(stderr, "failed to remove record %u\n", id);
+		return (CMD_ERR);
+	}
+
+	return (CMD_OK);
+}
+
+static int
+print_field(const char *label, const char *value, int show_label, FILE *fp)
+{
+	fprintf(fp, "%s%s\n", show_label ? label : "", (value != NULL) ?
+	    value : "");
+	if (ferror(fp)) {
+		warn("fprintf");
+		return (-1);
+	}
+	return (0);
+}
+
+static void
+print_record(struct record *record, int fields[], int show_labels, FILE *fp)
+{
+	if (fields[FIELD_TITLE]) {
+		if (print_field(field_labels[FIELD_TITLE], record->title,
+		    show_labels, fp) != 0) {
+			return;
+		}
+	}
+	if (fields[FIELD_GROUP]) {
+		if (print_field(field_labels[FIELD_GROUP], record->group,
+		    show_labels, fp)) {
+			return;
+		}
+	}
+	if (fields[FIELD_USERNAME]) {
+		if (print_field(field_labels[FIELD_USERNAME], record->username,
+		    show_labels, fp)) {
+			return;
+		}
+	}
+	if (fields[FIELD_PASSWORD]) {
+		if (print_field(field_labels[FIELD_PASSWORD], record->password,
+		    show_labels, fp)) {
+			return;
+		}
+	}
+	if (fields[FIELD_NOTES]) {
+		if (print_field(field_labels[FIELD_NOTES], record->notes,
+		    show_labels, fp)) {
+			return;
+		}
+	}
+	if (fields[FIELD_URL]) {
+		if (print_field(field_labels[FIELD_URL], record->url,
+		    show_labels, fp)) {
+			return;
+		}
+	}
+}
+
+static enum cmd_return
+cmd_show(struct pwm_ctx *ctx, int argc, char *argv[])
+{
+	unsigned int	id;
+	struct record	*record;
+	int		i;
+	enum field_type	type;
+	int		fields[COUNTOF(field_names)] = { 0 };
+
+	if (argc < 2) {
+		return (CMD_USAGE);
+	}
+
+	if (parse_id(argv[1], &id) != 0) {
+		fprintf(stderr, "invalid id %s\n", argv[1]);
+		return (CMD_ERR);
+	}
+
+	for (i = 2; i < argc; i++) {
+		type = parse_field_name(argv[i]);
+		if (type < 0) {
+			fprintf(stderr, "bad field name \"%s\"\n", argv[i]);
+			return (CMD_ERR);
+		}
+		fields[type] = 1;
+	}
+
+	record = pwfile_get_record(ctx, id);
+	if (record == NULL) {
+		fprintf(stderr, "record %u does not exist\n", id);
+		return (CMD_ERR);
+	}
+	print_record(record, fields, 1, stdout);
+	pwfile_destroy_record(record);
+
+	return (CMD_OK);
+}
+
+static enum cmd_return
+cmd_pipe(struct pwm_ctx *ctx, int argc, char *argv[])
+{
+	enum cmd_return	retval = CMD_ERR;
+	unsigned int	id;
+	struct record	*record = NULL;
+	enum field_type	type;
+	int		fields[COUNTOF(field_names)] = { 0 };
+	FILE		*fp = NULL;
+
+	if (argc != 4) {
+		return (CMD_USAGE);
+	}
+
+	if (parse_id(argv[1], &id) != 0) {
+		fprintf(stderr, "invalid id %s\n", argv[1]);
+		return (CMD_ERR);
+	}
+
+	type = parse_field_name(argv[2]);
+	if (type < 0) {
+		fprintf(stderr, "bad field name \"%s\"\n", argv[2]);
+		return (CMD_ERR);
+	}
+	fields[type] = 1;
+
+	fp = popen(argv[3], "w");
+	if (fp == NULL) {
+		warn("popen");
+		goto out;
+	}
+
+	record = pwfile_get_record(ctx, id);
+	if (record == NULL) {
+		fprintf(stderr, "record %u does not exist\n", id);
+		goto out;
+	}
+
+	print_record(record, fields, 0, fp);
+
+	retval = CMD_OK;
+
+out:
+	pwfile_destroy_record(record);
+	if (fp != NULL) {
+		pclose(fp);
+	}
+
+	return (retval);
+}
+
+static enum cmd_return
+cmd_creategroup(struct pwm_ctx *ctx, int argc, char *argv[])
+{
+	if (argc != 2) {
+		return (CMD_USAGE);
+	}
+
+	if (pwfile_create_group(ctx, argv[1]) != 0) {
+		fprintf(stderr, "group \"%s\" already exists\n", argv[1]);
+		return (CMD_ERR);
+	}
+
+	return (CMD_OK);
+}
+
+static enum cmd_return
+cmd_removegroup(struct pwm_ctx *ctx, int argc, char *argv[])
+{
+	if (argc != 2) {
+		return (CMD_USAGE);
+	}
+
+	if (pwfile_remove_group(ctx, argv[1]) != 0) {
+		fprintf(stderr, "group \"%s\" does not exist\n", argv[1]);
+		return (CMD_ERR);
+	}
+
+	return (CMD_OK);
+}
+
+static enum cmd_return
+cmd_changepassword(struct pwm_ctx *ctx, int argc, char *argv[])
+{
+	size_t	password_len;
+	char	password_buf[PWS3_MAX_PASSWORD_LEN + 1];
+	char	confirm_buf[PWS3_MAX_PASSWORD_LEN + 1];
+
+	if (argc > 2) {
+		return (CMD_USAGE);
+	} else if (argc == 2) {
+		password_len = strlen(argv[1]);
+		if (password_len == 0) {
+			fprintf(stderr, "password must not be empty\n");
+			return (CMD_ERR);
+		} else if (password_len + 1 > sizeof (ctx->password)) {
+			fprintf(stderr, "password too long\n");
+			return (CMD_ERR);
+		}
+		memcpy(ctx->password, argv[1], password_len + 1);
+	} else {
+		if (readpassphrase("Enter password: ", password_buf,
+		    sizeof (password_buf), RPP_ECHO_OFF | RPP_REQUIRE_TTY) ==
+		    NULL) {
+			err(1, "readpassphrase");
+		}
+		password_len = strlen(password_buf);
+		if (password_len == 0) {
+			fprintf(stderr, "password must not be empty\n");
+			return (CMD_ERR);
+		}
+		if (readpassphrase("Confirm password: ", confirm_buf,
+		    sizeof (confirm_buf),
+		    RPP_ECHO_OFF | RPP_REQUIRE_TTY) == NULL) {
+			err(1, "readpassphrase");
+		}
+		if (strcmp(password_buf, confirm_buf) != 0) {
+			fprintf(stderr, "passwords do not match\n");
+			return (CMD_ERR);
+		}
+		memcpy(ctx->password, password_buf, password_len + 1);
+	}
+
+	return (CMD_OK);
+}
+
+static enum cmd_return
+cmd_help(struct pwm_ctx *ctx, int argc, char *argv[])
+{
+	struct cmd	*cmd;
+
+	if (argc != 1) {
+		return (CMD_USAGE);
+	}
+
+	printf("Commands:\n");
+	for (cmd = cmds; cmd->cmd_func != NULL; cmd++) {
+		printf("%-2s %-16s %s\n", cmd->abbrev_cmd, cmd->full_cmd,
+		    cmd->description);
+	}
+
+	return (CMD_OK);
+}
+
+static enum cmd_return
+cmd_write(struct pwm_ctx *ctx, int argc, char *argv[])
+{
+	if (argc != 1) {
+		return (CMD_USAGE);
+	}
+
+	return ((pwfile_write_file(ctx) == 0) ? CMD_OK : CMD_ERR);
+}
+
+static enum cmd_return
+cmd_quit(struct pwm_ctx *ctx, int argc, char *argv[])
+{
+	if (argc != 1) {
+		return (CMD_USAGE);
+	}
+
+	return (CMD_QUIT);
+}
+
+struct cmd *
+cmd_match(const char *cmd_name)
+{
+	size_t	i;
+
+	for (i = 0; cmds[i].cmd_func != NULL; i++) {
+		if ((strcmp(cmds[i].abbrev_cmd, cmd_name) == 0) ||
+		    (strcmp(cmds[i].full_cmd, cmd_name) == 0)) {
+			return (&cmds[i]);
+		}
+	}
+
+	return (NULL);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cmd.h	Thu Jan 19 22:39:51 2017 +0100
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2016 Guido Berhoerster <guido+pwm@berhoerster.name>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef	CMD_H
+#define	CMD_H
+
+#include "pwm.h"
+
+enum cmd_return {
+	CMD_OK,
+	CMD_ERR,
+	CMD_USAGE,
+	CMD_QUIT
+};
+
+struct cmd {
+	const char	*abbrev_cmd;
+	const char	*full_cmd;
+	const char	*usage;
+	const char	*description;
+	enum cmd_return (*cmd_func)(struct pwm_ctx *, int argc, char *argv[]);
+};
+
+struct cmd *	cmd_match(const char *);
+
+#endif /* CMD_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/compat.h	Thu Jan 19 22:39:51 2017 +0100
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2016 Guido Berhoerster <guido+pwm@berhoerster.name>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef	COMPAT_H
+#define	COMPAT_H
+
+/* for glibc asprintf, getline */
+#define	_BSD_SOURCE
+
+#ifndef	HAVE_ASPRINTF
+#include "compat/asprintf.h"
+#endif /* !HAVE_ASPRINTF */
+
+#ifndef	HAVE_ERR_H
+#include "compat/err.h"
+#endif /* !HAVE_ERR_H */
+
+#ifndef	HAVE_GETENTROPY
+#include "compat/getentropy.h"
+#endif /* !HAVE_GETENTROPY */
+
+#ifndef	HAVE_READPASSPHRASE_H
+#include "compat/readpassphrase.h"
+#endif /* !HAVE_READPASSPHRASE_H */
+
+#ifndef	HAVE_SETPROGNAME
+#include "compat/setprogname.h"
+#endif /* !HAVE_SETPROGNAME */
+
+#ifndef	HAVE_SYS_TREE_H
+#include "compat/tree.h"
+#endif /* !HAVE_SYS_TREE_H */
+
+#endif /* COMPAT_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/compat/asprintf.c	Thu Jan 19 22:39:51 2017 +0100
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2015 Guido Berhoerster <guido+pwm@berhoerster.name>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <errno.h>
+
+#include "asprintf.h"
+
+int
+vasprintf(char **strp, const char *fmt, va_list args)
+{
+	int	len;
+	va_list	args_new;
+	char	*str;
+
+	*strp = NULL;
+	va_copy(args_new, args);
+
+	len = vsnprintf(NULL, 0, fmt, args);
+	if (len < 0) {
+		goto out;
+	}
+
+	str = malloc(len + 1);
+	if (str == NULL) {
+		goto out;
+	}
+	len = vsnprintf(str, len + 1, fmt, args_new);
+	if (len < 0) {
+		free(str);
+		goto out;
+	}
+	*strp = str;
+out:
+	va_end(args_new);
+
+	return (len);
+}
+
+int
+asprintf(char **strp, const char *fmt, ...)
+{
+	int	len;
+	va_list	args;
+
+	va_start(args, fmt);
+	len = vasprintf(strp, fmt, args);
+	va_end(args);
+
+	return (len);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/compat/asprintf.h	Thu Jan 19 22:39:51 2017 +0100
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2015 Guido Berhoerster <guido+pwm@berhoerster.name>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef	ASPRINTF_H
+#define	ASPRINTF_H
+
+#include <stdarg.h>
+
+int	asprintf(char **, const char *, ...);
+int	vasprintf(char **, const char *, va_list);
+
+#endif /* !ASPRINTF_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/compat/err.c	Thu Jan 19 22:39:51 2017 +0100
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2011 Guido Berhoerster <guido+pwm@berhoerster.name>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <errno.h>
+
+#include "err.h"
+#include "../compat.h"
+
+void
+err(int eval, const char *fmt, ...)
+{
+	va_list	args;
+
+	va_start(args, fmt);
+	vwarn(fmt, args);
+	va_end(args);
+
+	exit(eval);
+}
+
+void
+errx(int eval, const char *fmt, ...)
+{
+	va_list	args;
+
+	va_start(args, fmt);
+	vwarnx(fmt, args);
+	va_end(args);
+
+	exit(eval);
+}
+
+void
+warn(const char *fmt, ...)
+{
+	va_list	args;
+
+	va_start(args, fmt);
+	vwarn(fmt, args);
+	va_end(args);
+}
+
+void
+warnx(const char *fmt, ...)
+{
+	va_list	args;
+
+	va_start(args, fmt);
+	vwarnx(fmt, args);
+	va_end(args);
+}
+
+void
+verr(int eval, const char *fmt, va_list args)
+{
+	vwarn(fmt, args);
+
+	exit(eval);
+}
+
+void
+verrx(int eval, const char *fmt, va_list args)
+{
+	vwarnx(fmt, args);
+
+	exit(eval);
+}
+
+void
+vwarn(const char *fmt, va_list args)
+{
+	int	old_errno = errno;
+
+	fprintf(stderr, "%s: ", getprogname());
+	if (fmt != NULL) {
+		vfprintf(stderr, fmt, args);
+		fprintf(stderr, ": ");
+	}
+	fprintf(stderr, "%s\n", strerror(old_errno));
+	errno = old_errno;
+}
+
+void
+vwarnx(const char *fmt, va_list args)
+{
+	fprintf(stderr, "%s: ", getprogname());
+	if (fmt != NULL) {
+		vfprintf(stderr, fmt, args);
+	}
+	fputc('\n', stderr);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/compat/err.h	Thu Jan 19 22:39:51 2017 +0100
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2011 Guido Berhoerster <guido+pwm@berhoerster.name>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef ERR_H
+#define ERR_H
+
+#include <stdarg.h>
+
+void	err(int, const char *, ...);
+void	errx(int, const char *, ...);
+void	warn(const char *, ...);
+void	warnx(const char *, ...);
+void	verr(int, const char *, va_list);
+void	verrx(int, const char *, va_list);
+void	vwarn(const char *, va_list);
+void	vwarnx(const char *, va_list);
+
+#endif /* ERR_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/compat/getentropy.c	Thu Jan 19 22:39:51 2017 +0100
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2016 Guido Berhoerster <guido+pwm@berhoerster.name>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+/* needed for syscall(2) on Linux */
+#define	_GNU_SOURCE
+
+/* Linux >= 3.17 has getrandom(2) system call */
+#ifdef	__linux__
+#include <unistd.h>
+#include <sys/syscall.h>
+#include <linux/random.h>
+#ifdef	SYS_getrandom
+#define	HAVE_GETRANDOM
+#endif /* SYS_getrandom */
+#endif /* __linux__ */
+/*
+ * on unknown Unix systems without getentropy(2) or Linux without getrandom(2)
+ * fall back to * reading from /dev/(u)random
+ */
+#ifndef	HAVE_GETRANDOM
+#include <stdio.h>
+#ifndef	RANDOM_DEVICE
+#ifdef	__linux__
+/* on Linux /dev/urandom should be good enough */
+#define	RANDOM_DEVICE	"/dev/urandom"
+#else /* __linux__ */
+/* on unknown Unix systems use the possibly blocking /dev/random */
+#define	RANDOM_DEVICE	"/dev/random"
+#endif /* __linux__ */
+#endif /* !RANDOM_DEVICE */
+#endif /* !HAVE_GETRANDOM */
+#include <errno.h>
+
+#ifdef	HAVE_GETRANDOM
+static int
+getentropy_linux_getrandom(void *buf, size_t buf_len)
+{
+	int retval;
+
+	retval = syscall(SYS_getrandom, buf, buf_len, 0);
+	if (retval < 0) {
+		return (-1);
+	} else if ((size_t)retval != buf_len) {
+		errno = EIO;
+		return (-1);
+	}
+
+	return (0);
+}
+#else
+static int
+getentropy_dev_random(void *buf, size_t buf_len)
+{
+	FILE	*fp;
+	int	saved_errno;
+
+	fp = fopen(RANDOM_DEVICE, "r");
+	if (fp == NULL) {
+		return (-1);
+	}
+	if (fread(buf, 1, buf_len, fp) != buf_len) {
+		saved_errno = errno;
+		fclose(fp);
+		errno = saved_errno;
+		return (-1);
+	}
+	if (fclose(fp) != 0) {
+		return (-1);
+	}
+
+	return (0);
+}
+#endif /* HAVE_GETRANDOM */
+
+int
+getentropy(void *buf, size_t buf_len)
+{
+	if (buf_len > 256) {
+		errno = EIO;
+		return (-1);
+	}
+
+	return (
+#ifdef	HAVE_GETRANDOM
+	    getentropy_linux_getrandom(
+#else /* HAVE_GETRANDOM */
+	    getentropy_dev_random(
+#endif /* HAVE_GETRANDOM */
+	    buf, buf_len));
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/compat/getentropy.h	Thu Jan 19 22:39:51 2017 +0100
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2016 Guido Berhoerster <guido+pwm@berhoerster.name>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef	GETENTROPY_H
+#define	GETENTROPY_H
+
+#include <stddef.h>
+
+int	getentropy(void *, size_t);
+
+#endif /* !GETENTROPY_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/compat/readpassphrase.c	Thu Jan 19 22:39:51 2017 +0100
@@ -0,0 +1,209 @@
+/*	$OpenBSD: readpassphrase.c,v 1.24 2013/11/24 23:51:29 deraadt Exp $	*/
+
+/*
+ * Copyright (c) 2000-2002, 2007, 2010
+ *	Todd C. Miller <Todd.Miller@courtesan.com>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ *
+ * Sponsored in part by the Defense Advanced Research Projects
+ * Agency (DARPA) and Air Force Research Laboratory, Air Force
+ * Materiel Command, USAF, under agreement number F39502-99-1-0512.
+ */
+
+#include <ctype.h>
+#include <errno.h>
+#include <fcntl.h>
+#ifdef	HAVE_PATHS_H
+#include <paths.h>
+#endif /* HAVE_PATHS_H */
+#include <pwd.h>
+#include <signal.h>
+#include <string.h>
+#include <termios.h>
+#include <unistd.h>
+#include "readpassphrase.h"
+
+#ifndef TCSASOFT
+#define TCSASOFT 0
+#endif /* !TCSASOFT */
+
+#ifndef	_NSIG
+#ifdef NSIG
+#define	_NSIG NSIG
+#else /* NSIG */
+#define	_NSIG 128
+#endif /* NSIG */
+#endif /* !_NSIG */
+
+#ifndef	_PATH_TTY
+#define	_PATH_TTY	"/dev/tty"
+#endif
+
+static volatile sig_atomic_t signo[_NSIG];
+
+static void handler(int);
+
+char *
+readpassphrase(const char *prompt, char *buf, size_t bufsiz, int flags)
+{
+	ssize_t nr;
+	int input, output, save_errno, i, need_restart;
+	char ch, *p, *end;
+	struct termios term, oterm;
+	struct sigaction sa, savealrm, saveint, savehup, savequit, saveterm;
+	struct sigaction savetstp, savettin, savettou, savepipe;
+
+	/* I suppose we could alloc on demand in this case (XXX). */
+	if (bufsiz == 0) {
+		errno = EINVAL;
+		return(NULL);
+	}
+
+restart:
+	for (i = 0; i < _NSIG; i++)
+		signo[i] = 0;
+	nr = -1;
+	save_errno = 0;
+	need_restart = 0;
+	/*
+	 * Read and write to /dev/tty if available.  If not, read from
+	 * stdin and write to stderr unless a tty is required.
+	 */
+	if ((flags & RPP_STDIN) ||
+	    (input = output = open(_PATH_TTY, O_RDWR)) == -1) {
+		if (flags & RPP_REQUIRE_TTY) {
+			errno = ENOTTY;
+			return(NULL);
+		}
+		input = STDIN_FILENO;
+		output = STDERR_FILENO;
+	}
+
+	/*
+	 * Turn off echo if possible.
+	 * If we are using a tty but are not the foreground pgrp this will
+	 * generate SIGTTOU, so do it *before* installing the signal handlers.
+	 */
+	if (input != STDIN_FILENO && tcgetattr(input, &oterm) == 0) {
+		memcpy(&term, &oterm, sizeof(term));
+		if (!(flags & RPP_ECHO_ON))
+			term.c_lflag &= ~(ECHO | ECHONL);
+#ifdef VSTATUS
+		if (term.c_cc[VSTATUS] != _POSIX_VDISABLE)
+			term.c_cc[VSTATUS] = _POSIX_VDISABLE;
+#endif /* VSTATUS */
+		(void)tcsetattr(input, TCSAFLUSH|TCSASOFT, &term);
+	} else {
+		memset(&term, 0, sizeof(term));
+		term.c_lflag |= ECHO;
+		memset(&oterm, 0, sizeof(oterm));
+		oterm.c_lflag |= ECHO;
+	}
+
+	/*
+	 * Catch signals that would otherwise cause the user to end
+	 * up with echo turned off in the shell.  Don't worry about
+	 * things like SIGXCPU and SIGVTALRM for now.
+	 */
+	sigemptyset(&sa.sa_mask);
+	sa.sa_flags = 0;		/* don't restart system calls */
+	sa.sa_handler = handler;
+	(void)sigaction(SIGALRM, &sa, &savealrm);
+	(void)sigaction(SIGHUP, &sa, &savehup);
+	(void)sigaction(SIGINT, &sa, &saveint);
+	(void)sigaction(SIGPIPE, &sa, &savepipe);
+	(void)sigaction(SIGQUIT, &sa, &savequit);
+	(void)sigaction(SIGTERM, &sa, &saveterm);
+	(void)sigaction(SIGTSTP, &sa, &savetstp);
+	(void)sigaction(SIGTTIN, &sa, &savettin);
+	(void)sigaction(SIGTTOU, &sa, &savettou);
+
+	if (!(flags & RPP_STDIN))
+		(void)write(output, prompt, strlen(prompt));
+	end = buf + bufsiz - 1;
+	p = buf;
+	while ((nr = read(input, &ch, 1)) == 1 && ch != '\n' && ch != '\r') {
+		if (p < end) {
+			if ((flags & RPP_SEVENBIT))
+				ch &= 0x7f;
+			if (isalpha((unsigned char)ch)) {
+				if ((flags & RPP_FORCELOWER))
+					ch = (char)tolower((unsigned char)ch);
+				if ((flags & RPP_FORCEUPPER))
+					ch = (char)toupper((unsigned char)ch);
+			}
+			*p++ = ch;
+		}
+	}
+	*p = '\0';
+	save_errno = errno;
+	if (!(term.c_lflag & ECHO))
+		(void)write(output, "\n", 1);
+
+	/* Restore old terminal settings and signals. */
+	if (memcmp(&term, &oterm, sizeof(term)) != 0) {
+		while (tcsetattr(input, TCSAFLUSH|TCSASOFT, &oterm) == -1 &&
+		    errno == EINTR && !signo[SIGTTOU])
+			continue;
+	}
+	(void)sigaction(SIGALRM, &savealrm, NULL);
+	(void)sigaction(SIGHUP, &savehup, NULL);
+	(void)sigaction(SIGINT, &saveint, NULL);
+	(void)sigaction(SIGQUIT, &savequit, NULL);
+	(void)sigaction(SIGPIPE, &savepipe, NULL);
+	(void)sigaction(SIGTERM, &saveterm, NULL);
+	(void)sigaction(SIGTSTP, &savetstp, NULL);
+	(void)sigaction(SIGTTIN, &savettin, NULL);
+	(void)sigaction(SIGTTOU, &savettou, NULL);
+	if (input != STDIN_FILENO)
+		(void)close(input);
+
+	/*
+	 * If we were interrupted by a signal, resend it to ourselves
+	 * now that we have restored the signal handlers.
+	 */
+	for (i = 0; i < _NSIG; i++) {
+		if (signo[i]) {
+			kill(getpid(), i);
+			switch (i) {
+			case SIGTSTP:
+			case SIGTTIN:
+			case SIGTTOU:
+				need_restart = 1;
+			}
+		}
+	}
+	if (need_restart)
+		goto restart;
+
+	if (save_errno)
+		errno = save_errno;
+	return(nr == -1 ? NULL : buf);
+}
+
+#if 0
+char *
+getpass(const char *prompt)
+{
+	static char buf[_PASSWORD_LEN + 1];
+
+	return(readpassphrase(prompt, buf, sizeof(buf), RPP_ECHO_OFF));
+}
+#endif
+
+static void handler(int s)
+{
+
+	signo[s] = 1;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/compat/readpassphrase.h	Thu Jan 19 22:39:51 2017 +0100
@@ -0,0 +1,38 @@
+/*	$OpenBSD: readpassphrase.h,v 1.5 2003/06/17 21:56:23 millert Exp $	*/
+
+/*
+ * Copyright (c) 2000, 2002 Todd C. Miller <Todd.Miller@courtesan.com>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ *
+ * Sponsored in part by the Defense Advanced Research Projects
+ * Agency (DARPA) and Air Force Research Laboratory, Air Force
+ * Materiel Command, USAF, under agreement number F39502-99-1-0512.
+ */
+
+#ifndef _READPASSPHRASE_H_
+#define _READPASSPHRASE_H_
+
+#define RPP_ECHO_OFF    0x00		/* Turn off echo (default). */
+#define RPP_ECHO_ON     0x01		/* Leave echo on. */
+#define RPP_REQUIRE_TTY 0x02		/* Fail if there is no tty. */
+#define RPP_FORCELOWER  0x04		/* Force input to lower case. */
+#define RPP_FORCEUPPER  0x08		/* Force input to upper case. */
+#define RPP_SEVENBIT    0x10		/* Strip the high bit from input. */
+#define RPP_STDIN       0x20		/* Read from stdin, not /dev/tty */
+
+#include <sys/types.h>
+
+char * readpassphrase(const char *, char *, size_t, int);
+
+#endif /* !_READPASSPHRASE_H_ */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/compat/setprogname.c	Thu Jan 19 22:39:51 2017 +0100
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2015 Guido Berhoerster <guido+pwm@berhoerster.name>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+
+#include "setprogname.h"
+#include "../compat.h"
+
+static const char *progname = "<unknown_program>";
+
+void
+setprogname(const char *name)
+{
+	const char	*p;
+
+	p = strrchr(name, '/');
+	progname = (p != NULL) ? p + 1 : name;
+}
+
+const char *
+getprogname(void)
+{
+	return (progname);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/compat/setprogname.h	Thu Jan 19 22:39:51 2017 +0100
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2015 Guido Berhoerster <guido+pwm@berhoerster.name>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef	SETPROGNAME_H
+#define	SETPROGNAME_H
+
+void		setprogname(const char *);
+const char *	getprogname(void);
+
+#endif /* !SETPROGNAME_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/compat/tree.h	Thu Jan 19 22:39:51 2017 +0100
@@ -0,0 +1,748 @@
+/*	$OpenBSD: tree.h,v 1.13 2011/07/09 00:19:45 pirofti Exp $	*/
+/*
+ * Copyright 2002 Niels Provos <provos@citi.umich.edu>
+ * 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 ``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 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.
+ */
+
+#ifndef	_SYS_TREE_H_
+#define	_SYS_TREE_H_
+
+/*
+ * This file defines data structures for different types of trees:
+ * splay trees and red-black trees.
+ *
+ * A splay tree is a self-organizing data structure.  Every operation
+ * on the tree causes a splay to happen.  The splay moves the requested
+ * node to the root of the tree and partly rebalances it.
+ *
+ * This has the benefit that request locality causes faster lookups as
+ * the requested nodes move to the top of the tree.  On the other hand,
+ * every lookup causes memory writes.
+ *
+ * The Balance Theorem bounds the total access time for m operations
+ * and n inserts on an initially empty tree as O((m + n)lg n).  The
+ * amortized cost for a sequence of m accesses to a splay tree is O(lg n);
+ *
+ * A red-black tree is a binary search tree with the node color as an
+ * extra attribute.  It fulfills a set of conditions:
+ *	- every search path from the root to a leaf consists of the
+ *	  same number of black nodes,
+ *	- each red node (except for the root) has a black parent,
+ *	- each leaf node is black.
+ *
+ * Every operation on a red-black tree is bounded as O(lg n).
+ * The maximum height of a red-black tree is 2lg (n+1).
+ */
+
+#define SPLAY_HEAD(name, type)						\
+struct name {								\
+	struct type *sph_root; /* root of the tree */			\
+}
+
+#define SPLAY_INITIALIZER(root)						\
+	{ NULL }
+
+#define SPLAY_INIT(root) do {						\
+	(root)->sph_root = NULL;					\
+} while (0)
+
+#define SPLAY_ENTRY(type)						\
+struct {								\
+	struct type *spe_left; /* left element */			\
+	struct type *spe_right; /* right element */			\
+}
+
+#define SPLAY_LEFT(elm, field)		(elm)->field.spe_left
+#define SPLAY_RIGHT(elm, field)		(elm)->field.spe_right
+#define SPLAY_ROOT(head)		(head)->sph_root
+#define SPLAY_EMPTY(head)		(SPLAY_ROOT(head) == NULL)
+
+/* SPLAY_ROTATE_{LEFT,RIGHT} expect that tmp hold SPLAY_{RIGHT,LEFT} */
+#define SPLAY_ROTATE_RIGHT(head, tmp, field) do {			\
+	SPLAY_LEFT((head)->sph_root, field) = SPLAY_RIGHT(tmp, field);	\
+	SPLAY_RIGHT(tmp, field) = (head)->sph_root;			\
+	(head)->sph_root = tmp;						\
+} while (0)
+	
+#define SPLAY_ROTATE_LEFT(head, tmp, field) do {			\
+	SPLAY_RIGHT((head)->sph_root, field) = SPLAY_LEFT(tmp, field);	\
+	SPLAY_LEFT(tmp, field) = (head)->sph_root;			\
+	(head)->sph_root = tmp;						\
+} while (0)
+
+#define SPLAY_LINKLEFT(head, tmp, field) do {				\
+	SPLAY_LEFT(tmp, field) = (head)->sph_root;			\
+	tmp = (head)->sph_root;						\
+	(head)->sph_root = SPLAY_LEFT((head)->sph_root, field);		\
+} while (0)
+
+#define SPLAY_LINKRIGHT(head, tmp, field) do {				\
+	SPLAY_RIGHT(tmp, field) = (head)->sph_root;			\
+	tmp = (head)->sph_root;						\
+	(head)->sph_root = SPLAY_RIGHT((head)->sph_root, field);	\
+} while (0)
+
+#define SPLAY_ASSEMBLE(head, node, left, right, field) do {		\
+	SPLAY_RIGHT(left, field) = SPLAY_LEFT((head)->sph_root, field);	\
+	SPLAY_LEFT(right, field) = SPLAY_RIGHT((head)->sph_root, field);\
+	SPLAY_LEFT((head)->sph_root, field) = SPLAY_RIGHT(node, field);	\
+	SPLAY_RIGHT((head)->sph_root, field) = SPLAY_LEFT(node, field);	\
+} while (0)
+
+/* Generates prototypes and inline functions */
+
+#define SPLAY_PROTOTYPE(name, type, field, cmp)				\
+void name##_SPLAY(struct name *, struct type *);			\
+void name##_SPLAY_MINMAX(struct name *, int);				\
+struct type *name##_SPLAY_INSERT(struct name *, struct type *);		\
+struct type *name##_SPLAY_REMOVE(struct name *, struct type *);		\
+									\
+/* Finds the node with the same key as elm */				\
+static __inline struct type *						\
+name##_SPLAY_FIND(struct name *head, struct type *elm)			\
+{									\
+	if (SPLAY_EMPTY(head))						\
+		return(NULL);						\
+	name##_SPLAY(head, elm);					\
+	if ((cmp)(elm, (head)->sph_root) == 0)				\
+		return (head->sph_root);				\
+	return (NULL);							\
+}									\
+									\
+static __inline struct type *						\
+name##_SPLAY_NEXT(struct name *head, struct type *elm)			\
+{									\
+	name##_SPLAY(head, elm);					\
+	if (SPLAY_RIGHT(elm, field) != NULL) {				\
+		elm = SPLAY_RIGHT(elm, field);				\
+		while (SPLAY_LEFT(elm, field) != NULL) {		\
+			elm = SPLAY_LEFT(elm, field);			\
+		}							\
+	} else								\
+		elm = NULL;						\
+	return (elm);							\
+}									\
+									\
+static __inline struct type *						\
+name##_SPLAY_MIN_MAX(struct name *head, int val)			\
+{									\
+	name##_SPLAY_MINMAX(head, val);					\
+        return (SPLAY_ROOT(head));					\
+}
+
+/* Main splay operation.
+ * Moves node close to the key of elm to top
+ */
+#define SPLAY_GENERATE(name, type, field, cmp)				\
+struct type *								\
+name##_SPLAY_INSERT(struct name *head, struct type *elm)		\
+{									\
+    if (SPLAY_EMPTY(head)) {						\
+	    SPLAY_LEFT(elm, field) = SPLAY_RIGHT(elm, field) = NULL;	\
+    } else {								\
+	    int __comp;							\
+	    name##_SPLAY(head, elm);					\
+	    __comp = (cmp)(elm, (head)->sph_root);			\
+	    if(__comp < 0) {						\
+		    SPLAY_LEFT(elm, field) = SPLAY_LEFT((head)->sph_root, field);\
+		    SPLAY_RIGHT(elm, field) = (head)->sph_root;		\
+		    SPLAY_LEFT((head)->sph_root, field) = NULL;		\
+	    } else if (__comp > 0) {					\
+		    SPLAY_RIGHT(elm, field) = SPLAY_RIGHT((head)->sph_root, field);\
+		    SPLAY_LEFT(elm, field) = (head)->sph_root;		\
+		    SPLAY_RIGHT((head)->sph_root, field) = NULL;	\
+	    } else							\
+		    return ((head)->sph_root);				\
+    }									\
+    (head)->sph_root = (elm);						\
+    return (NULL);							\
+}									\
+									\
+struct type *								\
+name##_SPLAY_REMOVE(struct name *head, struct type *elm)		\
+{									\
+	struct type *__tmp;						\
+	if (SPLAY_EMPTY(head))						\
+		return (NULL);						\
+	name##_SPLAY(head, elm);					\
+	if ((cmp)(elm, (head)->sph_root) == 0) {			\
+		if (SPLAY_LEFT((head)->sph_root, field) == NULL) {	\
+			(head)->sph_root = SPLAY_RIGHT((head)->sph_root, field);\
+		} else {						\
+			__tmp = SPLAY_RIGHT((head)->sph_root, field);	\
+			(head)->sph_root = SPLAY_LEFT((head)->sph_root, field);\
+			name##_SPLAY(head, elm);			\
+			SPLAY_RIGHT((head)->sph_root, field) = __tmp;	\
+		}							\
+		return (elm);						\
+	}								\
+	return (NULL);							\
+}									\
+									\
+void									\
+name##_SPLAY(struct name *head, struct type *elm)			\
+{									\
+	struct type __node, *__left, *__right, *__tmp;			\
+	int __comp;							\
+\
+	SPLAY_LEFT(&__node, field) = SPLAY_RIGHT(&__node, field) = NULL;\
+	__left = __right = &__node;					\
+\
+	while ((__comp = (cmp)(elm, (head)->sph_root))) {		\
+		if (__comp < 0) {					\
+			__tmp = SPLAY_LEFT((head)->sph_root, field);	\
+			if (__tmp == NULL)				\
+				break;					\
+			if ((cmp)(elm, __tmp) < 0){			\
+				SPLAY_ROTATE_RIGHT(head, __tmp, field);	\
+				if (SPLAY_LEFT((head)->sph_root, field) == NULL)\
+					break;				\
+			}						\
+			SPLAY_LINKLEFT(head, __right, field);		\
+		} else if (__comp > 0) {				\
+			__tmp = SPLAY_RIGHT((head)->sph_root, field);	\
+			if (__tmp == NULL)				\
+				break;					\
+			if ((cmp)(elm, __tmp) > 0){			\
+				SPLAY_ROTATE_LEFT(head, __tmp, field);	\
+				if (SPLAY_RIGHT((head)->sph_root, field) == NULL)\
+					break;				\
+			}						\
+			SPLAY_LINKRIGHT(head, __left, field);		\
+		}							\
+	}								\
+	SPLAY_ASSEMBLE(head, &__node, __left, __right, field);		\
+}									\
+									\
+/* Splay with either the minimum or the maximum element			\
+ * Used to find minimum or maximum element in tree.			\
+ */									\
+void name##_SPLAY_MINMAX(struct name *head, int __comp) \
+{									\
+	struct type __node, *__left, *__right, *__tmp;			\
+\
+	SPLAY_LEFT(&__node, field) = SPLAY_RIGHT(&__node, field) = NULL;\
+	__left = __right = &__node;					\
+\
+	while (1) {							\
+		if (__comp < 0) {					\
+			__tmp = SPLAY_LEFT((head)->sph_root, field);	\
+			if (__tmp == NULL)				\
+				break;					\
+			if (__comp < 0){				\
+				SPLAY_ROTATE_RIGHT(head, __tmp, field);	\
+				if (SPLAY_LEFT((head)->sph_root, field) == NULL)\
+					break;				\
+			}						\
+			SPLAY_LINKLEFT(head, __right, field);		\
+		} else if (__comp > 0) {				\
+			__tmp = SPLAY_RIGHT((head)->sph_root, field);	\
+			if (__tmp == NULL)				\
+				break;					\
+			if (__comp > 0) {				\
+				SPLAY_ROTATE_LEFT(head, __tmp, field);	\
+				if (SPLAY_RIGHT((head)->sph_root, field) == NULL)\
+					break;				\
+			}						\
+			SPLAY_LINKRIGHT(head, __left, field);		\
+		}							\
+	}								\
+	SPLAY_ASSEMBLE(head, &__node, __left, __right, field);		\
+}
+
+#define SPLAY_NEGINF	-1
+#define SPLAY_INF	1
+
+#define SPLAY_INSERT(name, x, y)	name##_SPLAY_INSERT(x, y)
+#define SPLAY_REMOVE(name, x, y)	name##_SPLAY_REMOVE(x, y)
+#define SPLAY_FIND(name, x, y)		name##_SPLAY_FIND(x, y)
+#define SPLAY_NEXT(name, x, y)		name##_SPLAY_NEXT(x, y)
+#define SPLAY_MIN(name, x)		(SPLAY_EMPTY(x) ? NULL	\
+					: name##_SPLAY_MIN_MAX(x, SPLAY_NEGINF))
+#define SPLAY_MAX(name, x)		(SPLAY_EMPTY(x) ? NULL	\
+					: name##_SPLAY_MIN_MAX(x, SPLAY_INF))
+
+#define SPLAY_FOREACH(x, name, head)					\
+	for ((x) = SPLAY_MIN(name, head);				\
+	     (x) != NULL;						\
+	     (x) = SPLAY_NEXT(name, head, x))
+
+/* Macros that define a red-black tree */
+#define RB_HEAD(name, type)						\
+struct name {								\
+	struct type *rbh_root; /* root of the tree */			\
+}
+
+#define RB_INITIALIZER(root)						\
+	{ NULL }
+
+#define RB_INIT(root) do {						\
+	(root)->rbh_root = NULL;					\
+} while (0)
+
+#define RB_BLACK	0
+#define RB_RED		1
+#define RB_ENTRY(type)							\
+struct {								\
+	struct type *rbe_left;		/* left element */		\
+	struct type *rbe_right;		/* right element */		\
+	struct type *rbe_parent;	/* parent element */		\
+	int rbe_color;			/* node color */		\
+}
+
+#define RB_LEFT(elm, field)		(elm)->field.rbe_left
+#define RB_RIGHT(elm, field)		(elm)->field.rbe_right
+#define RB_PARENT(elm, field)		(elm)->field.rbe_parent
+#define RB_COLOR(elm, field)		(elm)->field.rbe_color
+#define RB_ROOT(head)			(head)->rbh_root
+#define RB_EMPTY(head)			(RB_ROOT(head) == NULL)
+
+#define RB_SET(elm, parent, field) do {					\
+	RB_PARENT(elm, field) = parent;					\
+	RB_LEFT(elm, field) = RB_RIGHT(elm, field) = NULL;		\
+	RB_COLOR(elm, field) = RB_RED;					\
+} while (0)
+
+#define RB_SET_BLACKRED(black, red, field) do {				\
+	RB_COLOR(black, field) = RB_BLACK;				\
+	RB_COLOR(red, field) = RB_RED;					\
+} while (0)
+
+#ifndef RB_AUGMENT
+#define RB_AUGMENT(x)	do {} while (0)
+#endif
+
+#define RB_ROTATE_LEFT(head, elm, tmp, field) do {			\
+	(tmp) = RB_RIGHT(elm, field);					\
+	if ((RB_RIGHT(elm, field) = RB_LEFT(tmp, field))) {		\
+		RB_PARENT(RB_LEFT(tmp, field), field) = (elm);		\
+	}								\
+	RB_AUGMENT(elm);						\
+	if ((RB_PARENT(tmp, field) = RB_PARENT(elm, field))) {		\
+		if ((elm) == RB_LEFT(RB_PARENT(elm, field), field))	\
+			RB_LEFT(RB_PARENT(elm, field), field) = (tmp);	\
+		else							\
+			RB_RIGHT(RB_PARENT(elm, field), field) = (tmp);	\
+	} else								\
+		(head)->rbh_root = (tmp);				\
+	RB_LEFT(tmp, field) = (elm);					\
+	RB_PARENT(elm, field) = (tmp);					\
+	RB_AUGMENT(tmp);						\
+	if ((RB_PARENT(tmp, field)))					\
+		RB_AUGMENT(RB_PARENT(tmp, field));			\
+} while (0)
+
+#define RB_ROTATE_RIGHT(head, elm, tmp, field) do {			\
+	(tmp) = RB_LEFT(elm, field);					\
+	if ((RB_LEFT(elm, field) = RB_RIGHT(tmp, field))) {		\
+		RB_PARENT(RB_RIGHT(tmp, field), field) = (elm);		\
+	}								\
+	RB_AUGMENT(elm);						\
+	if ((RB_PARENT(tmp, field) = RB_PARENT(elm, field))) {		\
+		if ((elm) == RB_LEFT(RB_PARENT(elm, field), field))	\
+			RB_LEFT(RB_PARENT(elm, field), field) = (tmp);	\
+		else							\
+			RB_RIGHT(RB_PARENT(elm, field), field) = (tmp);	\
+	} else								\
+		(head)->rbh_root = (tmp);				\
+	RB_RIGHT(tmp, field) = (elm);					\
+	RB_PARENT(elm, field) = (tmp);					\
+	RB_AUGMENT(tmp);						\
+	if ((RB_PARENT(tmp, field)))					\
+		RB_AUGMENT(RB_PARENT(tmp, field));			\
+} while (0)
+
+/* Generates prototypes and inline functions */
+#define	RB_PROTOTYPE(name, type, field, cmp)				\
+	RB_PROTOTYPE_INTERNAL(name, type, field, cmp,)
+#define	RB_PROTOTYPE_STATIC(name, type, field, cmp)			\
+	RB_PROTOTYPE_INTERNAL(name, type, field, cmp, __attribute__((__unused__)) static)
+#define RB_PROTOTYPE_INTERNAL(name, type, field, cmp, attr)		\
+attr void name##_RB_INSERT_COLOR(struct name *, struct type *);		\
+attr void name##_RB_REMOVE_COLOR(struct name *, struct type *, struct type *);\
+attr struct type *name##_RB_REMOVE(struct name *, struct type *);	\
+attr struct type *name##_RB_INSERT(struct name *, struct type *);	\
+attr struct type *name##_RB_FIND(struct name *, struct type *);		\
+attr struct type *name##_RB_NFIND(struct name *, struct type *);	\
+attr struct type *name##_RB_NEXT(struct type *);			\
+attr struct type *name##_RB_PREV(struct type *);			\
+attr struct type *name##_RB_MINMAX(struct name *, int);			\
+									\
+
+/* Main rb operation.
+ * Moves node close to the key of elm to top
+ */
+#define	RB_GENERATE(name, type, field, cmp)				\
+	RB_GENERATE_INTERNAL(name, type, field, cmp,)
+#define	RB_GENERATE_STATIC(name, type, field, cmp)			\
+	RB_GENERATE_INTERNAL(name, type, field, cmp, __attribute__((__unused__)) static)
+#define RB_GENERATE_INTERNAL(name, type, field, cmp, attr)		\
+attr void								\
+name##_RB_INSERT_COLOR(struct name *head, struct type *elm)		\
+{									\
+	struct type *parent, *gparent, *tmp;				\
+	while ((parent = RB_PARENT(elm, field)) &&			\
+	    RB_COLOR(parent, field) == RB_RED) {			\
+		gparent = RB_PARENT(parent, field);			\
+		if (parent == RB_LEFT(gparent, field)) {		\
+			tmp = RB_RIGHT(gparent, field);			\
+			if (tmp && RB_COLOR(tmp, field) == RB_RED) {	\
+				RB_COLOR(tmp, field) = RB_BLACK;	\
+				RB_SET_BLACKRED(parent, gparent, field);\
+				elm = gparent;				\
+				continue;				\
+			}						\
+			if (RB_RIGHT(parent, field) == elm) {		\
+				RB_ROTATE_LEFT(head, parent, tmp, field);\
+				tmp = parent;				\
+				parent = elm;				\
+				elm = tmp;				\
+			}						\
+			RB_SET_BLACKRED(parent, gparent, field);	\
+			RB_ROTATE_RIGHT(head, gparent, tmp, field);	\
+		} else {						\
+			tmp = RB_LEFT(gparent, field);			\
+			if (tmp && RB_COLOR(tmp, field) == RB_RED) {	\
+				RB_COLOR(tmp, field) = RB_BLACK;	\
+				RB_SET_BLACKRED(parent, gparent, field);\
+				elm = gparent;				\
+				continue;				\
+			}						\
+			if (RB_LEFT(parent, field) == elm) {		\
+				RB_ROTATE_RIGHT(head, parent, tmp, field);\
+				tmp = parent;				\
+				parent = elm;				\
+				elm = tmp;				\
+			}						\
+			RB_SET_BLACKRED(parent, gparent, field);	\
+			RB_ROTATE_LEFT(head, gparent, tmp, field);	\
+		}							\
+	}								\
+	RB_COLOR(head->rbh_root, field) = RB_BLACK;			\
+}									\
+									\
+attr void								\
+name##_RB_REMOVE_COLOR(struct name *head, struct type *parent, struct type *elm) \
+{									\
+	struct type *tmp;						\
+	while ((elm == NULL || RB_COLOR(elm, field) == RB_BLACK) &&	\
+	    elm != RB_ROOT(head)) {					\
+		if (RB_LEFT(parent, field) == elm) {			\
+			tmp = RB_RIGHT(parent, field);			\
+			if (RB_COLOR(tmp, field) == RB_RED) {		\
+				RB_SET_BLACKRED(tmp, parent, field);	\
+				RB_ROTATE_LEFT(head, parent, tmp, field);\
+				tmp = RB_RIGHT(parent, field);		\
+			}						\
+			if ((RB_LEFT(tmp, field) == NULL ||		\
+			    RB_COLOR(RB_LEFT(tmp, field), field) == RB_BLACK) &&\
+			    (RB_RIGHT(tmp, field) == NULL ||		\
+			    RB_COLOR(RB_RIGHT(tmp, field), field) == RB_BLACK)) {\
+				RB_COLOR(tmp, field) = RB_RED;		\
+				elm = parent;				\
+				parent = RB_PARENT(elm, field);		\
+			} else {					\
+				if (RB_RIGHT(tmp, field) == NULL ||	\
+				    RB_COLOR(RB_RIGHT(tmp, field), field) == RB_BLACK) {\
+					struct type *oleft;		\
+					if ((oleft = RB_LEFT(tmp, field)))\
+						RB_COLOR(oleft, field) = RB_BLACK;\
+					RB_COLOR(tmp, field) = RB_RED;	\
+					RB_ROTATE_RIGHT(head, tmp, oleft, field);\
+					tmp = RB_RIGHT(parent, field);	\
+				}					\
+				RB_COLOR(tmp, field) = RB_COLOR(parent, field);\
+				RB_COLOR(parent, field) = RB_BLACK;	\
+				if (RB_RIGHT(tmp, field))		\
+					RB_COLOR(RB_RIGHT(tmp, field), field) = RB_BLACK;\
+				RB_ROTATE_LEFT(head, parent, tmp, field);\
+				elm = RB_ROOT(head);			\
+				break;					\
+			}						\
+		} else {						\
+			tmp = RB_LEFT(parent, field);			\
+			if (RB_COLOR(tmp, field) == RB_RED) {		\
+				RB_SET_BLACKRED(tmp, parent, field);	\
+				RB_ROTATE_RIGHT(head, parent, tmp, field);\
+				tmp = RB_LEFT(parent, field);		\
+			}						\
+			if ((RB_LEFT(tmp, field) == NULL ||		\
+			    RB_COLOR(RB_LEFT(tmp, field), field) == RB_BLACK) &&\
+			    (RB_RIGHT(tmp, field) == NULL ||		\
+			    RB_COLOR(RB_RIGHT(tmp, field), field) == RB_BLACK)) {\
+				RB_COLOR(tmp, field) = RB_RED;		\
+				elm = parent;				\
+				parent = RB_PARENT(elm, field);		\
+			} else {					\
+				if (RB_LEFT(tmp, field) == NULL ||	\
+				    RB_COLOR(RB_LEFT(tmp, field), field) == RB_BLACK) {\
+					struct type *oright;		\
+					if ((oright = RB_RIGHT(tmp, field)))\
+						RB_COLOR(oright, field) = RB_BLACK;\
+					RB_COLOR(tmp, field) = RB_RED;	\
+					RB_ROTATE_LEFT(head, tmp, oright, field);\
+					tmp = RB_LEFT(parent, field);	\
+				}					\
+				RB_COLOR(tmp, field) = RB_COLOR(parent, field);\
+				RB_COLOR(parent, field) = RB_BLACK;	\
+				if (RB_LEFT(tmp, field))		\
+					RB_COLOR(RB_LEFT(tmp, field), field) = RB_BLACK;\
+				RB_ROTATE_RIGHT(head, parent, tmp, field);\
+				elm = RB_ROOT(head);			\
+				break;					\
+			}						\
+		}							\
+	}								\
+	if (elm)							\
+		RB_COLOR(elm, field) = RB_BLACK;			\
+}									\
+									\
+attr struct type *							\
+name##_RB_REMOVE(struct name *head, struct type *elm)			\
+{									\
+	struct type *child, *parent, *old = elm;			\
+	int color;							\
+	if (RB_LEFT(elm, field) == NULL)				\
+		child = RB_RIGHT(elm, field);				\
+	else if (RB_RIGHT(elm, field) == NULL)				\
+		child = RB_LEFT(elm, field);				\
+	else {								\
+		struct type *left;					\
+		elm = RB_RIGHT(elm, field);				\
+		while ((left = RB_LEFT(elm, field)))			\
+			elm = left;					\
+		child = RB_RIGHT(elm, field);				\
+		parent = RB_PARENT(elm, field);				\
+		color = RB_COLOR(elm, field);				\
+		if (child)						\
+			RB_PARENT(child, field) = parent;		\
+		if (parent) {						\
+			if (RB_LEFT(parent, field) == elm)		\
+				RB_LEFT(parent, field) = child;		\
+			else						\
+				RB_RIGHT(parent, field) = child;	\
+			RB_AUGMENT(parent);				\
+		} else							\
+			RB_ROOT(head) = child;				\
+		if (RB_PARENT(elm, field) == old)			\
+			parent = elm;					\
+		(elm)->field = (old)->field;				\
+		if (RB_PARENT(old, field)) {				\
+			if (RB_LEFT(RB_PARENT(old, field), field) == old)\
+				RB_LEFT(RB_PARENT(old, field), field) = elm;\
+			else						\
+				RB_RIGHT(RB_PARENT(old, field), field) = elm;\
+			RB_AUGMENT(RB_PARENT(old, field));		\
+		} else							\
+			RB_ROOT(head) = elm;				\
+		RB_PARENT(RB_LEFT(old, field), field) = elm;		\
+		if (RB_RIGHT(old, field))				\
+			RB_PARENT(RB_RIGHT(old, field), field) = elm;	\
+		if (parent) {						\
+			left = parent;					\
+			do {						\
+				RB_AUGMENT(left);			\
+			} while ((left = RB_PARENT(left, field)));	\
+		}							\
+		goto color;						\
+	}								\
+	parent = RB_PARENT(elm, field);					\
+	color = RB_COLOR(elm, field);					\
+	if (child)							\
+		RB_PARENT(child, field) = parent;			\
+	if (parent) {							\
+		if (RB_LEFT(parent, field) == elm)			\
+			RB_LEFT(parent, field) = child;			\
+		else							\
+			RB_RIGHT(parent, field) = child;		\
+		RB_AUGMENT(parent);					\
+	} else								\
+		RB_ROOT(head) = child;					\
+color:									\
+	if (color == RB_BLACK)						\
+		name##_RB_REMOVE_COLOR(head, parent, child);		\
+	return (old);							\
+}									\
+									\
+/* Inserts a node into the RB tree */					\
+attr struct type *							\
+name##_RB_INSERT(struct name *head, struct type *elm)			\
+{									\
+	struct type *tmp;						\
+	struct type *parent = NULL;					\
+	int comp = 0;							\
+	tmp = RB_ROOT(head);						\
+	while (tmp) {							\
+		parent = tmp;						\
+		comp = (cmp)(elm, parent);				\
+		if (comp < 0)						\
+			tmp = RB_LEFT(tmp, field);			\
+		else if (comp > 0)					\
+			tmp = RB_RIGHT(tmp, field);			\
+		else							\
+			return (tmp);					\
+	}								\
+	RB_SET(elm, parent, field);					\
+	if (parent != NULL) {						\
+		if (comp < 0)						\
+			RB_LEFT(parent, field) = elm;			\
+		else							\
+			RB_RIGHT(parent, field) = elm;			\
+		RB_AUGMENT(parent);					\
+	} else								\
+		RB_ROOT(head) = elm;					\
+	name##_RB_INSERT_COLOR(head, elm);				\
+	return (NULL);							\
+}									\
+									\
+/* Finds the node with the same key as elm */				\
+attr struct type *							\
+name##_RB_FIND(struct name *head, struct type *elm)			\
+{									\
+	struct type *tmp = RB_ROOT(head);				\
+	int comp;							\
+	while (tmp) {							\
+		comp = cmp(elm, tmp);					\
+		if (comp < 0)						\
+			tmp = RB_LEFT(tmp, field);			\
+		else if (comp > 0)					\
+			tmp = RB_RIGHT(tmp, field);			\
+		else							\
+			return (tmp);					\
+	}								\
+	return (NULL);							\
+}									\
+									\
+/* Finds the first node greater than or equal to the search key */	\
+attr struct type *							\
+name##_RB_NFIND(struct name *head, struct type *elm)			\
+{									\
+	struct type *tmp = RB_ROOT(head);				\
+	struct type *res = NULL;					\
+	int comp;							\
+	while (tmp) {							\
+		comp = cmp(elm, tmp);					\
+		if (comp < 0) {						\
+			res = tmp;					\
+			tmp = RB_LEFT(tmp, field);			\
+		}							\
+		else if (comp > 0)					\
+			tmp = RB_RIGHT(tmp, field);			\
+		else							\
+			return (tmp);					\
+	}								\
+	return (res);							\
+}									\
+									\
+/* ARGSUSED */								\
+attr struct type *							\
+name##_RB_NEXT(struct type *elm)					\
+{									\
+	if (RB_RIGHT(elm, field)) {					\
+		elm = RB_RIGHT(elm, field);				\
+		while (RB_LEFT(elm, field))				\
+			elm = RB_LEFT(elm, field);			\
+	} else {							\
+		if (RB_PARENT(elm, field) &&				\
+		    (elm == RB_LEFT(RB_PARENT(elm, field), field)))	\
+			elm = RB_PARENT(elm, field);			\
+		else {							\
+			while (RB_PARENT(elm, field) &&			\
+			    (elm == RB_RIGHT(RB_PARENT(elm, field), field)))\
+				elm = RB_PARENT(elm, field);		\
+			elm = RB_PARENT(elm, field);			\
+		}							\
+	}								\
+	return (elm);							\
+}									\
+									\
+/* ARGSUSED */								\
+attr struct type *							\
+name##_RB_PREV(struct type *elm)					\
+{									\
+	if (RB_LEFT(elm, field)) {					\
+		elm = RB_LEFT(elm, field);				\
+		while (RB_RIGHT(elm, field))				\
+			elm = RB_RIGHT(elm, field);			\
+	} else {							\
+		if (RB_PARENT(elm, field) &&				\
+		    (elm == RB_RIGHT(RB_PARENT(elm, field), field)))	\
+			elm = RB_PARENT(elm, field);			\
+		else {							\
+			while (RB_PARENT(elm, field) &&			\
+			    (elm == RB_LEFT(RB_PARENT(elm, field), field)))\
+				elm = RB_PARENT(elm, field);		\
+			elm = RB_PARENT(elm, field);			\
+		}							\
+	}								\
+	return (elm);							\
+}									\
+									\
+attr struct type *							\
+name##_RB_MINMAX(struct name *head, int val)				\
+{									\
+	struct type *tmp = RB_ROOT(head);				\
+	struct type *parent = NULL;					\
+	while (tmp) {							\
+		parent = tmp;						\
+		if (val < 0)						\
+			tmp = RB_LEFT(tmp, field);			\
+		else							\
+			tmp = RB_RIGHT(tmp, field);			\
+	}								\
+	return (parent);						\
+}
+
+#define RB_NEGINF	-1
+#define RB_INF	1
+
+#define RB_INSERT(name, x, y)	name##_RB_INSERT(x, y)
+#define RB_REMOVE(name, x, y)	name##_RB_REMOVE(x, y)
+#define RB_FIND(name, x, y)	name##_RB_FIND(x, y)
+#define RB_NFIND(name, x, y)	name##_RB_NFIND(x, y)
+#define RB_NEXT(name, x, y)	name##_RB_NEXT(y)
+#define RB_PREV(name, x, y)	name##_RB_PREV(y)
+#define RB_MIN(name, x)		name##_RB_MINMAX(x, RB_NEGINF)
+#define RB_MAX(name, x)		name##_RB_MINMAX(x, RB_INF)
+
+#define RB_FOREACH(x, name, head)					\
+	for ((x) = RB_MIN(name, head);					\
+	     (x) != NULL;						\
+	     (x) = name##_RB_NEXT(x))
+
+#define RB_FOREACH_SAFE(x, name, head, y)				\
+	for ((x) = RB_MIN(name, head);					\
+	    ((x) != NULL) && ((y) = name##_RB_NEXT(x), 1);		\
+	     (x) = (y))
+
+#define RB_FOREACH_REVERSE(x, name, head)				\
+	for ((x) = RB_MAX(name, head);					\
+	     (x) != NULL;						\
+	     (x) = name##_RB_PREV(x))
+
+#define RB_FOREACH_REVERSE_SAFE(x, name, head, y)			\
+	for ((x) = RB_MAX(name, head);					\
+	    ((x) != NULL) && ((y) = name##_RB_PREV(x), 1);		\
+	     (x) = (y))
+
+#endif	/* _SYS_TREE_H_ */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/deps.sed	Thu Jan 19 22:39:51 2017 +0100
@@ -0,0 +1,26 @@
+/^[^:]\{1,\}:.*\\$/{
+    h
+    s/\([^:]\{1,\}:\).*/\1/
+    x
+    s/[^:]\{1,\}://
+}
+/\\$/,/^$/bgen
+/\\$/,/[^\\]$/{
+:gen
+    s/[[:blank:]]*\\$//
+    s/^[[:blank:]]*//
+    G
+    s/\(.*\)\n\(.*\)/\2 \1/
+}
+/^[^:]\{1,\}:[[:blank:]]*$/d
+/^[^:]\{1,\}\.o:/{
+    s/[[:blank:]]*[^[:blank:]]\{1,\}\.[cC][[:blank:]]*/ /g
+    s/[[:blank:]]*[^[:blank:]]\{1,\}\.[cC]$//g
+    s/[[:blank:]]*[^[:blank:]]\{1,\}\.cc[[:blank:]]*/ /g
+    s/[[:blank:]]*[^[:blank:]]\{1,\}\.cc$//g
+    s/[[:blank:]]*[^[:blank:]]\{1,\}\.cpp[[:blank:]]*/ /g
+    s/[[:blank:]]*[^[:blank:]]\{1,\}\.cpp$//g
+    /^[^:]\{1,\}:[[:blank:]]*$/d
+    s/^\([^:]\{1,\}\)\.o[[:blank:]]*:[[:blank:]]*\(.*\)/\1.d: $(wildcard \2)\
+&/
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pwfile.c	Thu Jan 19 22:39:51 2017 +0100
@@ -0,0 +1,783 @@
+/*
+ * Copyright (C) 2016 Guido Berhoerster <guido+pwm@berhoerster.name>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#include "compat.h"
+
+#ifdef	HAVE_ERR_H
+#include <err.h>
+#endif /* HAVE_ERR_H */
+#include <errno.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/stat.h>
+#ifdef	HAVE_SYS_TREE_H
+#include <sys/tree.h>
+#endif
+#include <unistd.h>
+
+#include "pwfile.h"
+#include "util.h"
+
+#define	MIN_ARRAY_SIZE	1024
+
+struct record_id_entry {
+	RB_ENTRY(record_id_entry) record_id_entry;
+	unsigned int	id;
+	unsigned char	uuid[PWS3_UUID_SIZE];
+};
+
+RB_HEAD(record_id_tree, record_id_entry);
+
+static int	record_id_entry_cmp(struct record_id_entry *,
+    struct record_id_entry *);
+RB_PROTOTYPE_STATIC(record_id_tree, record_id_entry, record_id_entry,
+    record_id_entry_cmp)
+RB_GENERATE_STATIC(record_id_tree, record_id_entry, record_id_entry,
+    record_id_entry_cmp)
+
+static int
+record_id_entry_cmp(struct record_id_entry *entry1,
+    struct record_id_entry *entry2)
+{
+	if (entry1->id > entry2->id) {
+		return (-1);
+	} else if (entry1->id < entry2->id) {
+		return (1);
+	}
+	return (0);
+}
+
+static int
+pws_record_cmp(const void *p1, const void *p2)
+{
+	int		retval;
+	struct pws3_record *record1 = *(struct pws3_record **)p1;
+	struct pws3_record *record2 = *(struct pws3_record **)p2;
+	struct pws3_field *group_field1;
+	const char	*group1;
+	struct pws3_field *group_field2;
+	const char	*group2;
+	struct pws3_field *title_field1;
+	const char	*title1;
+	struct pws3_field *title_field2;
+	const char	*title2;
+
+	group_field1 = pws3_record_get_field(record1, PWS3_RECORD_FIELD_GROUP);
+	group1 = (group_field1 != NULL) ?  pws3_field_get_text(group_field1) :
+	    "";
+	group_field2 = pws3_record_get_field(record2, PWS3_RECORD_FIELD_GROUP);
+	group2 = (group_field1 != NULL) ?  pws3_field_get_text(group_field2) :
+	    "";
+	retval = strcmp(group1, group2);
+	if (retval != 0) {
+		return (retval);
+	}
+
+	title_field1 = pws3_record_get_field(record1, PWS3_RECORD_FIELD_TITLE);
+	title1 = (title_field1 != NULL) ?  pws3_field_get_text(title_field1) :
+	    "";
+	title_field2 = pws3_record_get_field(record2, PWS3_RECORD_FIELD_TITLE);
+	title2 = (title_field2 != NULL) ?  pws3_field_get_text(title_field2) :
+	    "";
+	return (strcmp(title1, title2));
+}
+
+static void
+record_id_tree_clear(struct record_id_tree *tree)
+{
+	struct record_id_entry *entry;
+	struct record_id_entry *entry_tmp;
+
+	RB_FOREACH_SAFE(entry, record_id_tree, tree, entry_tmp) {
+		RB_REMOVE(record_id_tree, tree, entry);
+		free(entry);
+	}
+}
+
+static void
+record_id_tree_destroy(struct record_id_tree *tree)
+{
+	if (tree == NULL) {
+		return;
+	}
+
+	record_id_tree_clear(tree);
+	free(tree);
+}
+
+static const unsigned char *
+record_id_tree_get_uuid(struct record_id_tree *tree, unsigned int id)
+{
+	struct record_id_entry *entry;
+
+	entry = RB_FIND(record_id_tree, tree,
+	    &(struct record_id_entry){ .id = id });
+	if (entry == NULL) {
+		return (NULL);
+	}
+
+	return (entry->uuid);
+}
+
+void
+pwfile_init(struct pwm_ctx *ctx)
+{
+	ctx->file = pws3_file_create();
+	if (ctx->file == NULL) {
+		err(1, "pws3_file_create");
+	}
+
+	ctx->next_id = 1;
+
+	ctx->record_id_tree = xmalloc(sizeof (struct record_id_tree));
+	RB_INIT(ctx->record_id_tree);
+}
+
+void
+pwfile_destroy(struct pwm_ctx *ctx)
+{
+	record_id_tree_destroy(ctx->record_id_tree);
+	ctx->record_id_tree = NULL;
+	pws3_file_destroy(ctx->file);
+	ctx->file = NULL;
+}
+
+int
+pwfile_read_file(struct pwm_ctx *ctx, FILE *fp)
+{
+	struct pws3_record *pws3_record;
+	struct pws3_record **pws3_record_list;
+	size_t		record_list_size = MIN_ARRAY_SIZE;
+	size_t		record_list_len = 0;
+	size_t		i;
+	struct pws3_field *uuid_field;
+	const unsigned char *uuid;
+	struct record_id_entry *entry;
+
+	if (pws3_file_read_stream(ctx->file, ctx->password, fp) != 0) {
+		warnx("failed to read password database: %s",
+		    pws3_file_get_error_message(ctx->file));
+		return (-1);
+	}
+
+	record_id_tree_clear(ctx->record_id_tree);
+
+	/* sort records by group and title */
+	pws3_record_list = xmalloc(sizeof (struct pws3_record *) *
+	    record_list_size);
+	for (pws3_record = pws3_file_first_record(ctx->file);
+	    pws3_record != NULL; pws3_record = pws3_file_next_record(ctx->file,
+	    pws3_record)) {
+		if (record_list_len == record_list_size) {
+			record_list_size *= 2;
+			pws3_record_list = xrealloc(pws3_record_list,
+			    sizeof (struct pws3_record *) * record_list_size);
+		}
+		pws3_record_list[record_list_len++] = pws3_record;
+	}
+	qsort(pws3_record_list, record_list_len, sizeof (struct pws3_record *),
+	    pws_record_cmp);
+
+	/* build the tree of record IDs */
+	for (i = 0; i < record_list_len; i++) {
+		uuid_field = pws3_record_get_field(pws3_record_list[i],
+		    PWS3_RECORD_FIELD_UUID);
+		uuid = pws3_field_get_uuid(uuid_field);
+
+		entry = xmalloc(sizeof (struct record_id_entry));
+		entry->id = ctx->next_id++;
+		memcpy(entry->uuid, uuid, sizeof (entry->uuid));
+
+		RB_INSERT(record_id_tree, ctx->record_id_tree, entry);
+	}
+
+	free(pws3_record_list);
+
+	return (0);
+}
+
+static int
+make_backup_copy(const char *filename)
+{
+	int		retval = -1;
+	FILE		*fp_orig = NULL;
+	char		*backup_filename = NULL;
+	char		*tmpfilename = NULL;
+	mode_t		old_mode;
+	int		fd_backup = -1;
+	unsigned char	buf[BUFSIZ];
+	size_t		read_len;
+	FILE		*fp_backup = NULL;
+
+	fp_orig = fopen(filename, "r");
+	if (fp_orig == NULL) {
+		if (errno != ENOENT) {
+			warn("fopen");
+			return (-1);
+		}
+		return (0);
+	}
+
+	xasprintf(&backup_filename, "%s~", filename);
+	xasprintf(&tmpfilename, "%s.XXXXXX", filename);
+
+	/* create temporary file */
+	old_mode = umask(S_IRWXG | S_IRWXO);
+	fd_backup = mkstemp(tmpfilename);
+	umask(old_mode);
+	if (fd_backup == -1) {
+		warn("mkstemp");
+		goto out;
+	}
+	fp_backup = fdopen(fd_backup, "w");
+	if (fp_backup == NULL) {
+		warn("fdopen");
+		goto out;
+	}
+
+	/* copy file contents */
+	while (!feof(fp_orig)) {
+		read_len = fread(buf, 1, sizeof (buf), fp_orig);
+		if ((read_len < sizeof (buf)) && ferror(fp_orig)) {
+			warn("fread");
+			goto out;
+		}
+		if (fwrite(buf, 1, read_len, fp_backup) != read_len) {
+			warn("fwrite");
+			goto out;
+		}
+	}
+	if (fflush(fp_backup) != 0) {
+		warn("fflush");
+		goto out;
+	}
+	if (fsync(fileno(fp_backup)) != 0) {
+		warn("fsync");
+		goto out;
+	}
+
+	retval = 0;
+
+out:
+	if ((fd_backup != -1) && (fp_backup == NULL)) {
+		close(fd_backup);
+	}
+	if (fp_backup != NULL) {
+		fclose(fp_backup);
+	}
+	if (fp_orig != NULL) {
+		fclose(fp_orig);
+	}
+	if (retval == 0) {
+		/* rename temporary file and overwrite existing file */
+		if (rename(tmpfilename, backup_filename) != 0) {
+			warn("rename");
+			retval = -1;
+		}
+	}
+	if ((retval != 0) && ((fd_backup != -1) || (fp_backup != NULL))) {
+		unlink(tmpfilename);
+	}
+	free(tmpfilename);
+	free(backup_filename);
+
+	return (retval);
+}
+
+int
+pwfile_write_file(struct pwm_ctx *ctx)
+{
+	int	retval = -1;
+	char	*tmpfilename = NULL;
+	mode_t	old_mode;
+	int	fd = -1;
+	FILE	*fp = NULL;
+
+	if (make_backup_copy(ctx->filename) != 0) {
+		goto out;
+	}
+
+	xasprintf(&tmpfilename, "%s.XXXXXX", ctx->filename);
+
+	/* create temporary file */
+	old_mode = umask(S_IRWXG | S_IRWXO);
+	fd = mkstemp(tmpfilename);
+	if (fd == -1) {
+		warn("mkstemp");
+		goto out;
+	}
+	umask(old_mode);
+	fp = fdopen(fd, "w");
+	if (fp == NULL) {
+		warn("fdopen");
+		goto out;
+	}
+
+	/* write contents */
+	if (pws3_file_write_stream(ctx->file, ctx->password, 10000, fp) != 0) {
+		warnx("pws3_file_write_stream: %s",
+		    pws3_file_get_error_message(ctx->file));
+		goto out;
+	}
+	if (fflush(fp) != 0) {
+		warn("fflush");
+		goto out;
+	}
+	if (fsync(fileno(fp)) != 0) {
+		warn("fsync");
+		goto out;
+	}
+
+	retval = 0;
+
+out:
+	if ((fd != -1) && (fp == NULL)) {
+		close(fd);
+	}
+	if (fp != NULL) {
+		fclose(fp);
+	}
+	if (retval == 0) {
+		/* rename temporary file and overwrite existing file */
+		if (rename(tmpfilename, ctx->filename) != 0) {
+			warn("rename");
+			retval = -1;
+		}
+	}
+	if ((retval != 0) && ((fd != -1) || (fp != NULL))) {
+		unlink(tmpfilename);
+	}
+	free(tmpfilename);
+
+	return (retval);
+}
+
+static int
+list_item_cmp(const void *p1, const void *p2)
+{
+	int		retval;
+	const union list_item *item1 = *(const union list_item **)p1;
+	const union list_item *item2 = *(const union list_item **)p2;
+	const char	*group1;
+	const char	*group2;
+	const char	*title1;
+	const char	*title2;
+
+	/* sort both groups and records first by group name */
+	group1 = (item1->any.group != NULL) ? item1->any.group : "";
+	group2 = (item2->any.group != NULL) ? item2->any.group : "";
+	retval = strcmp(group1, group2);
+	if ((retval != 0) || ((item1->any.type == ITEM_TYPE_GROUP) &&
+	    (item2->any.type == ITEM_TYPE_GROUP))) {
+		return (retval);
+	} else if ((item1->any.type == ITEM_TYPE_GROUP) &&
+	    (item2->any.type == ITEM_TYPE_RECORD)) {
+		/* groups come before records belonging to it */
+		return (-1);
+	} else if ((item1->any.type == ITEM_TYPE_RECORD) &&
+	    (item2->any.type == ITEM_TYPE_GROUP)) {
+		return (1);
+	}
+
+	/* sort records also by title */
+	title1 = (item1->record.title != NULL) ?  item1->record.title : "";
+	title2 = (item2->record.title != NULL) ?  item2->record.title : "";
+	return (strcmp(title1, title2));
+}
+
+union list_item **
+pwfile_create_list(struct pwm_ctx *ctx)
+{
+	union list_item	**list;
+	size_t		list_size = MIN_ARRAY_SIZE;
+	size_t		list_len = 0;
+	struct record_id_entry *entry;
+	union list_item	*item;
+	struct pws3_record *pws3_record;
+	struct pws3_field *group_field;
+	const char	*group;
+	struct pws3_field *title_field;
+	const char	*title;
+	size_t		i;
+	size_t		records_len;
+	const char	*prev_group = "";
+	struct pws3_field *empty_group_field;
+
+	list = xmalloc(sizeof (union list_item *) * list_size);
+	list[0] = NULL;
+
+	/* build list of records and sort it by group and title */
+	RB_FOREACH(entry, record_id_tree, ctx->record_id_tree) {
+		if (list_len == list_size - 1) {
+			list_size *= 2;
+			list = xrealloc(list, sizeof (union list_item *) *
+			    list_size);
+		}
+
+		pws3_record = pws3_file_get_record(ctx->file, entry->uuid);
+		group_field = pws3_record_get_field(pws3_record,
+		    PWS3_RECORD_FIELD_GROUP);
+		group = (group_field != NULL) ?
+		    pws3_field_get_text(group_field) : NULL;
+		title_field = pws3_record_get_field(pws3_record,
+		    PWS3_RECORD_FIELD_TITLE);
+		title = (title_field != NULL) ?
+		    pws3_field_get_text(title_field) : NULL;
+
+		item = xmalloc(sizeof (union list_item));
+		item->record.type = ITEM_TYPE_RECORD;
+		item->record.group = (group != NULL) ? xstrdup(group) : NULL;
+		item->record.title = (title != NULL) ? xstrdup(title) : NULL;
+		item->record.id = entry->id;
+		memcpy(item->record.uuid, entry->uuid,
+		    sizeof (item->record.uuid));
+
+		list[list_len++] = item;
+		list[list_len] = NULL;
+	}
+	qsort(list, list_len, sizeof (union list_item *), list_item_cmp);
+
+	/* build list of groups by comparing the groups of the sorted records */
+	for (i = 0, records_len = list_len; i < records_len; i++) {
+		if (list_len == list_size - 1) {
+			list_size *= 1.5;
+			list = xrealloc(list, sizeof (union list_item *) *
+			    list_size);
+		}
+
+		group = (list[i]->record.group != NULL) ?
+		    list[i]->record.group : "";
+		if (strcmp(prev_group, group) != 0) {
+			item = xmalloc(sizeof (union list_item));
+			item->record.type = ITEM_TYPE_GROUP;
+			item->record.group = (group != NULL) ? xstrdup(group) :
+			    NULL;
+
+			list[list_len++] = item;
+			list[list_len] = NULL;
+
+			prev_group = group;
+		}
+	}
+
+	/* add empty groups to the list */
+	for (empty_group_field = pws3_file_first_empty_group(ctx->file);
+	    empty_group_field != NULL;
+	    empty_group_field = pws3_file_next_empty_group(ctx->file,
+	    empty_group_field)) {
+		if (list_len == list_size - 1) {
+			list_size *= 1.5;
+			list = xrealloc(list, sizeof (union list_item *) *
+			    list_size);
+		}
+
+		group = pws3_field_get_text(empty_group_field);
+
+		item = xmalloc(sizeof (union list_item));
+		item->record.type = ITEM_TYPE_GROUP;
+		item->record.group = xstrdup(group);
+
+		list[list_len++] = item;
+		list[list_len] = NULL;
+	}
+
+	list_size = list_len + 2;
+	list = xrealloc(list, sizeof (union list_item *) * list_size);
+	/* sort the final list by group and title */
+	qsort(list, list_len, sizeof (union list_item *), list_item_cmp);
+
+	return (list);
+}
+
+void
+pwfile_destroy_list(union list_item **list)
+{
+	size_t	i;
+
+	if (list == NULL) {
+		return;
+	}
+
+	for (i = 0; list[i] != NULL; i++) {
+		if (list[i]->any.type == ITEM_TYPE_RECORD) {
+			free(list[i]->record.title);
+		}
+		free(list[i]->any.group);
+		free(list[i]);
+	}
+
+	free(list);
+}
+
+static void
+update_record(struct pws3_record *pws3_record, struct record *record)
+{
+	struct pws3_field *title_field;
+	struct pws3_field *group_field;
+	struct pws3_field *username_field;
+	struct pws3_field *password_field;
+	struct pws3_field *notes_field;
+	struct pws3_field *url_field;
+
+	if (record->title != NULL) {
+		title_field = pws3_field_create(0, PWS3_RECORD_FIELD_TITLE);
+		if (title_field == NULL) {
+			err(1, "pws3_record_field_create");
+		}
+		if (pws3_field_set_text(title_field,
+		    record->title) != 0) {
+			err(1, "pws3_field_set_text");
+		}
+		pws3_record_set_field(pws3_record, title_field);
+	}
+	if (record->group != NULL) {
+		group_field = pws3_field_create(0, PWS3_RECORD_FIELD_GROUP);
+		if (group_field == NULL) {
+			err(1, "pws3_record_field_create");
+		}
+		if (pws3_field_set_text(group_field,
+		    record->group) != 0) {
+			err(1, "pws3_field_set_text");
+		}
+		pws3_record_set_field(pws3_record, group_field);
+	}
+	if (record->username != NULL) {
+		username_field = pws3_field_create(0,
+		    PWS3_RECORD_FIELD_USERNAME);
+		if (username_field == NULL) {
+			err(1, "pws3_record_field_create");
+		}
+		if (pws3_field_set_text(username_field,
+		    record->username) != 0) {
+			err(1, "pws3_field_set_text");
+		}
+		pws3_record_set_field(pws3_record, username_field);
+	}
+	if (record->password != NULL) {
+		password_field = pws3_field_create(0,
+		    PWS3_RECORD_FIELD_PASSWORD);
+		if (password_field == NULL) {
+			err(1, "pws3_record_field_create");
+		}
+		if (pws3_field_set_text(password_field,
+		    record->password) != 0) {
+			err(1, "pws3_field_set_text");
+		}
+		pws3_record_set_field(pws3_record, password_field);
+	}
+	if (record->notes != NULL) {
+		notes_field = pws3_field_create(0, PWS3_RECORD_FIELD_NOTES);
+		if (notes_field == NULL) {
+			err(1, "pws3_record_field_create");
+		}
+		if (pws3_field_set_text(notes_field, record->notes) != 0) {
+			err(1, "pws3_field_set_text");
+		}
+		pws3_record_set_field(pws3_record, notes_field);
+	}
+	if (record->url != NULL) {
+		url_field = pws3_field_create(0, PWS3_RECORD_FIELD_URL);
+		if (url_field == NULL) {
+			err(1, "pws3_record_field_create");
+		}
+		if (pws3_field_set_text(url_field, record->url) != 0) {
+			err(1, "pws3_field_set_text");
+		}
+		pws3_record_set_field(pws3_record, url_field);
+	}
+}
+
+int
+pwfile_create_record(struct pwm_ctx *ctx, struct record *record)
+{
+	struct pws3_record *pws3_record;
+	const unsigned char *uuid;
+	struct record_id_entry *entry;
+
+	pws3_record = pws3_record_create();
+	if (pws3_record == NULL) {
+		err(1, "pws3_record_create");
+	}
+	update_record(pws3_record, record);
+	pws3_file_insert_record(ctx->file, pws3_record);
+
+	uuid = pws3_field_get_uuid(pws3_record_get_field(pws3_record,
+	    PWS3_RECORD_FIELD_UUID));
+	entry = xmalloc(sizeof (struct record_id_entry));
+	entry->id = ctx->next_id++;
+	memcpy(entry->uuid, uuid, sizeof (entry->uuid));
+	RB_INSERT(record_id_tree, ctx->record_id_tree, entry);
+
+	return (0);
+}
+
+int
+pwfile_modify_record(struct pwm_ctx *ctx, unsigned int id,
+    struct record *record)
+{
+	const unsigned char *uuid;
+
+	uuid = record_id_tree_get_uuid(ctx->record_id_tree, id);
+	if (uuid == NULL) {
+		return (-1);
+	}
+
+	update_record(pws3_file_get_record(ctx->file, uuid), record);
+
+	return (0);
+}
+
+int
+pwfile_remove_record(struct pwm_ctx *ctx, unsigned int id)
+{
+	const unsigned char *uuid;
+	struct record_id_entry *entry;
+
+	uuid = record_id_tree_get_uuid(ctx->record_id_tree, id);
+	if (uuid == NULL) {
+		return (-1);
+	}
+
+	pws3_record_destroy(pws3_file_remove_record(ctx->file, uuid));
+
+	entry = RB_FIND(record_id_tree, ctx->record_id_tree,
+	    &(struct record_id_entry){ .id = id });
+	free(RB_REMOVE(record_id_tree, ctx->record_id_tree, entry));
+
+	return (0);
+}
+
+int
+pwfile_create_group(struct pwm_ctx *ctx, const char *group)
+{
+	struct pws3_record *pws3_record;
+	struct pws3_field *group_field;
+	struct pws3_field *empty_group_field;
+
+	/* check for a record in the given group */
+	for (pws3_record = pws3_file_first_record(ctx->file);
+	    pws3_record != NULL; pws3_record = pws3_file_next_record(ctx->file,
+	    pws3_record)) {
+		group_field = pws3_record_get_field(pws3_record,
+		    PWS3_RECORD_FIELD_GROUP);
+		if ((group_field != NULL) &&
+		    (strcmp(group, pws3_field_get_text(group_field)) == 0)) {
+			return (-1);
+		}
+	}
+
+	empty_group_field = pws3_field_create(1,
+	    PWS3_HEADER_FIELD_EMPTY_GROUPS);
+	if (empty_group_field == NULL) {
+		err(1, "pws3_field_create");
+	}
+	if (pws3_field_set_text(empty_group_field, group) != 0) {
+		err(1, "pws3_field_set_text");
+	}
+	pws3_file_insert_empty_group(ctx->file, empty_group_field);
+
+	return (0);
+}
+
+int
+pwfile_remove_group(struct pwm_ctx *ctx, const char *group)
+{
+	struct pws3_field *empty_group_field;
+
+	empty_group_field = pws3_file_remove_empty_group(ctx->file, group);
+	if (empty_group_field != NULL) {
+		return (-1);
+	}
+	pws3_field_destroy(empty_group_field);
+
+	return (0);
+}
+
+struct record *
+pwfile_get_record(struct pwm_ctx *ctx, unsigned int id)
+{
+	struct record	*record;
+	const unsigned char *uuid;
+	struct pws3_record *pws3_record;
+	struct pws3_field *title_field;
+	struct pws3_field *group_field;
+	struct pws3_field *username_field;
+	struct pws3_field *password_field;
+	struct pws3_field *notes_field;
+	struct pws3_field *url_field;
+
+	uuid = record_id_tree_get_uuid(ctx->record_id_tree, id);
+	if (uuid == NULL) {
+		return (NULL);
+	}
+	pws3_record = pws3_file_get_record(ctx->file, uuid);
+
+	record = xmalloc(sizeof (struct record));
+
+	title_field = pws3_record_get_field(pws3_record,
+	    PWS3_RECORD_FIELD_TITLE);
+	record->title = (title_field != NULL) ?
+	    xstrdup(pws3_field_get_text(title_field)) : NULL;
+
+	group_field = pws3_record_get_field(pws3_record,
+	    PWS3_RECORD_FIELD_GROUP);
+	record->group = (group_field != NULL) ?
+	    xstrdup(pws3_field_get_text(group_field)) : NULL;
+
+	username_field = pws3_record_get_field(pws3_record,
+	    PWS3_RECORD_FIELD_USERNAME);
+	record->username = (username_field != NULL) ?
+	    xstrdup(pws3_field_get_text(username_field)) : NULL;
+
+	password_field = pws3_record_get_field(pws3_record,
+	    PWS3_RECORD_FIELD_PASSWORD);
+	record->password = (password_field != NULL) ?
+	    xstrdup(pws3_field_get_text(password_field)) : NULL;
+
+	notes_field = pws3_record_get_field(pws3_record,
+	    PWS3_RECORD_FIELD_NOTES);
+	record->notes = (notes_field != NULL) ?
+	    xstrdup(pws3_field_get_text(notes_field)) : NULL;
+
+	url_field = pws3_record_get_field(pws3_record, PWS3_RECORD_FIELD_URL);
+	record->url = (url_field != NULL) ?
+	    xstrdup(pws3_field_get_text(url_field)) : NULL;
+
+	return (record);
+}
+
+void
+pwfile_destroy_record(struct record *record)
+{
+	if (record == NULL) {
+		return;
+	}
+
+	free(record->title);
+	free(record->group);
+	free(record->username);
+	free(record->password);
+	free(record->notes);
+	free(record->url);
+	free(record);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pwfile.h	Thu Jan 19 22:39:51 2017 +0100
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2016 Guido Berhoerster <guido+pwm@berhoerster.name>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef	PWFILE_H
+#define	PWFILE_H
+
+#include "pwm.h"
+
+enum item_type {
+	ITEM_TYPE_RECORD,
+	ITEM_TYPE_GROUP
+};
+
+union list_item {
+	struct any_item {
+		enum item_type	type;
+		char		*group;
+	} any;
+	struct record_item {
+		enum item_type	type;
+		char		*group;
+		char		*title;
+		unsigned int	id;
+		unsigned char	uuid[PWS3_UUID_SIZE];
+	} record;
+	struct group_item {
+		enum item_type	type;
+		char		*group;
+	} group;
+};
+
+struct record_id_tree;
+
+struct record {
+	char	*title;
+	char	*group;
+	char	*username;
+	char	*password;
+	char	*notes;
+	char	*url;
+};
+
+void		pwfile_init(struct pwm_ctx *ctx);
+void		pwfile_destroy(struct pwm_ctx *);
+int		pwfile_read_file(struct pwm_ctx *, FILE *);
+int		pwfile_write_file(struct pwm_ctx *);
+union list_item ** pwfile_create_list(struct pwm_ctx *);
+void		pwfile_destroy_list(union list_item **);
+int		pwfile_create_record(struct pwm_ctx *, struct record *);
+int		pwfile_modify_record(struct pwm_ctx *, unsigned int,
+    struct record *);
+int		pwfile_remove_record(struct pwm_ctx *, unsigned int);
+int		pwfile_create_group(struct pwm_ctx *, const char *);
+int		pwfile_remove_group(struct pwm_ctx *, const char *);
+struct record *	pwfile_get_record(struct pwm_ctx *, unsigned int);
+void		pwfile_destroy_record(struct record *);
+
+#endif /* !PWFILE_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pwm.c	Thu Jan 19 22:39:51 2017 +0100
@@ -0,0 +1,366 @@
+/*
+ * Copyright (C) 2016 Guido Berhoerster <guido+pwm@berhoerster.name>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#include "compat.h"
+
+#ifdef	HAVE_ERR_H
+#include <err.h>
+#endif /* HAVE_ERR_H */
+#include <errno.h>
+#include <langinfo.h>
+#include <locale.h>
+#include <pwd.h>
+#ifdef	HAVE_READPASSPHRASE_H
+#include <readpassphrase.h>
+#endif /* READPASSPHRASE_H */
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <pwd.h>
+#include <sys/stat.h>
+#include <time.h>
+#include <unistd.h>
+
+#include "pwm.h"
+#include "cmd.h"
+#include "pwfile.h"
+#include "tok.h"
+#include "util.h"
+
+#ifndef	PWM_LINE_MAX
+#define	PWM_LINE_MAX	16384
+#endif /* !PWM_LINE_MAX */
+
+static void
+usage(void)
+{
+	fprintf(stderr, "usage: %s [-P file] [filename]\n", getprogname());
+}
+
+static int
+run_input_loop(struct pwm_ctx *ctx, int is_interactive)
+{
+	int		retval = -1;
+	char		buf[PWM_LINE_MAX];
+	int		c;
+	int		argc = 0;
+	char		**argv = NULL;
+	struct cmd	*cmd;
+	int		i;
+
+	for (;;) {
+		if (fgets(buf, (int)sizeof (buf), stdin) == NULL) {
+			if (ferror(stdin)) {
+				/* error */
+				warn("failed to read command");
+				goto out;
+			} else if (feof(stdin)) {
+				/* EOF */
+				break;
+			}
+		}
+		if ((buf[strlen(buf) - 1] != '\n') && !feof(stdin)) {
+			/* line was truncated */
+			fprintf(stderr, "line too long\n");
+			if (is_interactive) {
+				/* skip input to next newline */
+				do {
+					errno = 0;
+					c = fgetc(stdin);
+					if ((c == EOF) && (errno != 0)) {
+						warn("failed to read command");
+						goto out;
+					}
+				} while ((c != '\n') && (c != EOF));
+			} else {
+				/* fatal error in non-interactive mode */
+				goto out;
+			}
+		}
+
+		/* tokenize line */
+		switch(tok_tokenize(buf, &argc, &argv)) {
+		case TOK_ERR_SYSTEM_ERROR:
+			err(1, "tok_tokenize");
+		case TOK_ERR_UNTERMINATED_QUOTE:
+			fprintf(stderr, "unterminated quote\n");
+			if (!is_interactive) {
+				goto out;
+			}
+			goto next;
+		case TOK_ERR_TRAILING_BACKSLASH:
+			fprintf(stderr, "trailing backslash\n");
+			if (!is_interactive) {
+				goto out;
+			}
+			goto next;
+		}
+
+		if (argc == 0) {
+			/* empty line */
+			goto next;
+		}
+
+		/* find and execute the command */
+		cmd = cmd_match(argv[0]);
+		if (cmd == NULL) {
+			fprintf(stderr, "unknown command: %s\n", argv[0]);
+			if (is_interactive) {
+				goto next;
+			} else {
+				goto out;
+			}
+		}
+		switch (cmd->cmd_func(ctx, argc, argv)) {
+		case CMD_OK:
+			break;
+		case CMD_USAGE:
+			fprintf(stderr, "usage: %s\n", cmd->usage);
+		case CMD_ERR:	/* FALLTHROUGH */
+			if (!is_interactive) {
+				goto out;
+			}
+			break;
+		case CMD_QUIT:
+			goto quit;
+		}
+
+next:
+		for (i = 0; i < argc; i++) {
+			free(argv[i]);
+		}
+		free(argv);
+		argc = 0;
+		argv = NULL;
+	}
+
+quit:
+	retval = 0;
+
+out:
+	for (i = 0; i < argc; i++) {
+		free(argv[i]);
+	}
+	free(argv);
+
+	return (retval);
+}
+
+static int
+read_password_from_file(const char *filename, char *password,
+    size_t password_size)
+{
+	int	retval = -1;
+	char	*buf = NULL;
+	FILE	*fp = NULL;
+	size_t	password_len = 0;
+
+	buf = xmalloc(password_size);
+
+	fp = fopen(filename, "r");
+	if (fp == NULL) {
+		warn("failed to open master password file \"%s\"", filename);
+		goto out;
+	}
+	if (fgets(buf, password_size, fp) == NULL) {
+		/* read error or empty file */
+		if (ferror(fp)) {
+			/* read error */
+			warn("failed to read master password from \"%s\"",
+			    filename);
+		}
+		goto out;
+	}
+	password_len = strlen(buf);
+	if (buf[password_len - 1] == '\n') {
+		/* strip trailing newline */
+		password_len--;
+		if (password_len == 0) {
+			/* first line is empty */
+			goto out;
+		}
+	} else if (!feof(fp)) {
+		/* the first line was truncated, password is too long */
+		goto out;
+	}
+	memcpy(password, buf, password_size);
+	retval = 0;
+
+out:
+	password[password_len] = '\0';
+
+	if (fp != NULL) {
+		fclose(fp);
+	}
+	free(buf);
+
+	return (retval);
+}
+
+int
+main(int argc, char *argv[])
+{
+	int		status = EXIT_FAILURE;
+	char		*locale;
+	int		is_interactive;
+	int		errflag = 0;
+	int		c;
+	const char	*master_password_filename = NULL;
+	struct pwm_ctx	ctx = { 0 };
+	struct passwd	*passwd;
+	char		*pwm_dirname = NULL;
+	FILE		*fp = NULL;
+	char		password_buf[PWS3_MAX_PASSWORD_LEN + 1];
+
+	setprogname(argv[0]);
+
+	/* set up locale and check for UTF-8 codeset */
+	locale = setlocale(LC_ALL, "");
+	if (locale == NULL) {
+		errx(1, "invalid locale");
+	}
+	if ((strcasecmp(nl_langinfo(CODESET), "UTF-8") != 0) &&
+	    (strcasecmp(nl_langinfo(CODESET), "UTF8") != 0)) {
+		fprintf(stderr, "pwm requires a locale with UTF-8 character "
+		    "encoding.\n");
+		goto out;
+	}
+
+	/* timestamps are processed as UTC */
+	if (setenv("TZ", "", 1) != 0) {
+		goto out;
+	}
+	tzset();
+
+	/* initialize libpws */
+	if (pws_init() != 0) {
+		goto out;
+	}
+
+	is_interactive = isatty(STDIN_FILENO);
+
+	while (!errflag && (c = getopt(argc, argv, "P:h")) != -1) {
+		switch (c) {
+		case 'P':
+			master_password_filename = optarg;
+			break;
+		case 'h':
+			usage();
+			status = EXIT_SUCCESS;
+			goto out;
+		default:
+			errflag = 1;
+		}
+	}
+	if (errflag || (optind + 1 < argc)) {
+		usage();
+		status = EXIT_USAGE;
+		goto out;
+	}
+
+	if (optind == argc) {
+		passwd = getpwuid(getuid());
+		if (passwd == NULL) {
+			err(1, "getpwuid");
+		}
+		xasprintf(&pwm_dirname, "%s/.pwm", passwd->pw_dir);
+		xasprintf(&ctx.filename, "%s/pwm.psafe3", pwm_dirname);
+
+		/* create ~/.pwm directory if necessary */
+		if ((mkdir(pwm_dirname, S_IRWXU) != 0) && (errno != EEXIST)) {
+			warn("failed to create directory \"%s\"", pwm_dirname);
+			goto out;
+		}
+	} else if (optind + 1 == argc) {
+		ctx.filename = xstrdup(argv[optind++]);
+	} else {
+		usage();
+		status = EXIT_USAGE;
+		goto out;
+	}
+
+	if (is_interactive) {
+		printf("pwm version %s\n", VERSION);
+	} else if (master_password_filename == NULL) {
+		fprintf(stderr, "master password file must be specified when "
+		    "running non-interactively\n");
+		goto out;
+	}
+
+	pwfile_init(&ctx);
+
+	fp = fopen(ctx.filename, "r");
+	if ((fp == NULL) && (errno != ENOENT)) {
+		warn("failed to open password database");
+		goto out;
+	}
+	/* obtain master password */
+	if (master_password_filename != NULL) {
+		if (read_password_from_file(master_password_filename,
+		    ctx.password, sizeof (ctx.password)) != 0) {
+			fprintf(stderr, "malformed password database\n");
+			goto out;
+		}
+	} else {
+		if (readpassphrase("Enter password: ", ctx.password,
+		    sizeof (ctx.password), RPP_ECHO_OFF | RPP_REQUIRE_TTY) ==
+		    NULL) {
+			err(1, "readpassphrase");
+		}
+		if (ctx.password[0] == '\0') {
+			fprintf(stderr, "password must not be empty\n");
+			goto out;
+		}
+		if (fp == NULL) {
+			if (readpassphrase("Confirm password: ", password_buf,
+			    sizeof (password_buf),
+			    RPP_ECHO_OFF | RPP_REQUIRE_TTY) == NULL) {
+				err(1, "readpassphrase");
+			}
+			if (strcmp(ctx.password, password_buf) != 0) {
+				fprintf(stderr, "passwords do not match\n");
+				goto out;
+			}
+		}
+	}
+	if (fp != NULL) {
+		if (pwfile_read_file(&ctx, fp) != 0) {
+			goto out;
+		}
+		fclose(fp);
+		fp = NULL;
+	}
+
+	/* run main input loop */
+	status = (run_input_loop(&ctx, is_interactive) != 0);
+
+out:
+	pwfile_destroy(&ctx);
+	if (fp != NULL) {
+		fclose(fp);
+	}
+	free(ctx.filename);
+	free(pwm_dirname);
+
+	exit(status);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pwm.h	Thu Jan 19 22:39:51 2017 +0100
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2016 Guido Berhoerster <guido+pwm@berhoerster.name>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef	PWM_H
+#define	PWM_H
+
+#include <pws.h>
+
+struct pwm_ctx {
+	char		*filename;
+	struct pws3_file *file;
+	struct record_id_tree *record_id_tree;
+	unsigned int	next_id;
+	char		password[PWS3_MAX_PASSWORD_LEN + 1];
+};
+
+#endif /* PWM_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tok.c	Thu Jan 19 22:39:51 2017 +0100
@@ -0,0 +1,246 @@
+/*
+ * Copyright (C) 2016 Guido Berhoerster <guido+pwm@berhoerster.name>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#include "compat.h"
+
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "tok.h"
+
+enum tok_states {
+	STATE_INITIAL,
+	STATE_IN_WORD,
+	STATE_IN_QUOTE,
+	STATE_IN_WORD_ESCAPE,
+	STATE_IN_QUOTE_ESCAPE
+};
+
+static inline int
+strbuf_appendc(char **bufp, size_t *buf_sizep, int c)
+{
+	char	*buf = *bufp;
+	size_t	buf_size = *buf_sizep;
+	size_t	len;
+
+	len = ((buf != NULL) && (c >= 0)) ? strlen(buf) : 0;
+
+	/* allocate buffer if *bufp is NULL and *buf_sizep is 0 */
+	if (buf_size < len + (c >= 0) + 1) {
+		buf_size = (buf_size * 2 > BUFSIZ) ? buf_size * 2 : BUFSIZ;
+		buf = realloc(buf, buf_size);
+		if (buf == NULL) {
+			return (-1);
+		}
+	}
+
+	/* append character to string buffer or reset buffer if c is -1 */
+	if (c >= 0) {
+		buf[len++] = c;
+	}
+	buf[len] = '\0';
+
+	*bufp = buf;
+	*buf_sizep = buf_size;
+
+	return (0);
+}
+
+enum tok_err
+tok_tokenize(const char *s, int *tokencp, char ***tokenvp)
+{
+	int		retval = TOK_ERR_SYSTEM_ERROR;
+	int		saved_errno = 0;
+	char		**tokenv;
+	size_t		tokenc = 0;
+	const char	*p = s;
+	enum tok_states	state = STATE_INITIAL;
+	char		quote;
+	char		*buf = NULL;
+	size_t		buf_size = 0;
+	char		*token;
+	size_t		i;
+
+	/*
+	 * allocate maximum number of tokens including the terminating NULL
+	 * pointer: ceil(length / 2) + 1
+	 */
+	tokenv = malloc(((strlen(s) + 2 - 1) / 2 + 1) * sizeof (char *));
+	if (tokenv == NULL) {
+		saved_errno = errno;
+		goto out;
+	}
+	tokenv[0] = NULL;
+
+	for (;;) {
+		switch (state) {
+		case STATE_INITIAL:
+			switch (*p) {
+			case ' ':	/* FALLTHROUGH */
+			case '\t':	/* FALLTHROUGH */
+			case '\n':
+				/* skip initial whitespace */
+				break;
+			case '"':	/* FALLTHROUGH */
+			case '\'':
+				/* start quoted part of token */
+				state = STATE_IN_QUOTE;
+				quote = *p;
+				if (strbuf_appendc(&buf, &buf_size, -1) != 0) {
+					saved_errno = errno;
+					goto out;
+				}
+				break;
+			case '\\':
+				/* start token with a backslash escape */
+				state = STATE_IN_WORD_ESCAPE;
+				if (strbuf_appendc(&buf, &buf_size, -1) != 0) {
+					saved_errno = errno;
+					goto out;
+				}
+				break;
+			case '\0':
+				/* end of input */
+				retval = 0;
+				goto out;
+			default:
+				/* start token with a word */
+				state = STATE_IN_WORD;
+				if (strbuf_appendc(&buf, &buf_size, -1) != 0) {
+					saved_errno = errno;
+					goto out;
+				}
+				if (strbuf_appendc(&buf, &buf_size, *p) != 0) {
+					saved_errno = errno;
+					goto out;
+				}
+			}
+			break;
+		case STATE_IN_WORD:
+			switch (*p) {
+			case ' ':	/* FALLTHROUGH */
+			case '\t':	/* FALLTHROUGH */
+			case '\n':	/* FALLTHROUGH */
+			case '\0':
+				/* end of token */
+				token = strdup(buf);
+				if (token == NULL) {
+					saved_errno = errno;
+					goto out;
+				}
+				tokenv[tokenc++] = token;
+				tokenv[tokenc] = NULL;
+				if (*p == '\0') {
+					retval = 0;
+					goto out;
+				}
+				state = STATE_INITIAL;
+				break;
+			case '"':	/* FALLTHROUGH */
+			case '\'':
+				/* start quoted part of token */
+				state = STATE_IN_QUOTE;
+				quote = *p;
+				break;
+			case '\\':
+				/* start backslash escape */
+				state = STATE_IN_WORD_ESCAPE;
+				break;
+			default:
+				/* regular character */
+				if (strbuf_appendc(&buf, &buf_size, *p) != 0) {
+					saved_errno = errno;
+					goto out;
+				}
+			}
+			break;
+		case STATE_IN_QUOTE:
+			switch (*p) {
+			case '"':	/* FALLTHROUGH */
+			case '\'':
+				if (*p == quote) {
+					/* end quoted part of token */
+					state = STATE_IN_WORD;
+				} else {
+					/* quote quote character */
+					if (strbuf_appendc(&buf, &buf_size,
+					    *p) != 0) {
+						saved_errno = errno;
+						goto out;
+					}
+				}
+				break;
+			case '\\':
+				/* start quoted backslash escape */
+				state = STATE_IN_QUOTE_ESCAPE;
+				break;
+			case '\0':
+				/* unclosed quote */
+				retval = TOK_ERR_UNTERMINATED_QUOTE;
+				goto out;
+			default:
+				/* regular character */
+				if (strbuf_appendc(&buf, &buf_size, *p) != 0) {
+					saved_errno = errno;
+					goto out;
+				}
+			}
+			break;
+		case STATE_IN_WORD_ESCAPE:	/* FALLTHROUGH */
+		case STATE_IN_QUOTE_ESCAPE:
+			if (*p == '\0') {
+				/* trailing backslash */
+				retval = TOK_ERR_TRAILING_BACKSLASH;
+				goto out;
+			}
+			/* escaped character */
+			state = (state == STATE_IN_WORD_ESCAPE) ?
+			    STATE_IN_WORD : STATE_IN_QUOTE;
+			if (strbuf_appendc(&buf, &buf_size, *p) != 0) {
+				saved_errno = errno;
+				goto out;
+			}
+			break;
+		}
+		p++;
+	}
+
+out:
+	if (retval < 0) {
+		for (i = 0; i < tokenc; i++) {
+			free(tokenv[i]);
+		}
+		free(tokenv);
+	} else {
+		*tokencp = tokenc;
+		*tokenvp = realloc(tokenv, (tokenc + 1) * sizeof (char *));
+	}
+	free(buf);
+	if (retval < 0) {
+		errno = saved_errno;
+	}
+
+	return (retval);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tok.h	Thu Jan 19 22:39:51 2017 +0100
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2016 Guido Berhoerster <guido+pwm@berhoerster.name>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef	TOK_H
+#define	TOK_H
+
+enum tok_err {
+	TOK_ERR_SYSTEM_ERROR = -1,
+	TOK_ERR_UNTERMINATED_QUOTE = -2,
+	TOK_ERR_TRAILING_BACKSLASH = -3
+};
+
+enum tok_err	tok_tokenize(const char *, int *, char ***);
+
+#endif /* !TOK_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/util.c	Thu Jan 19 22:39:51 2017 +0100
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2016 Guido Berhoerster <guido+pwm@berhoerster.name>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#include "compat.h"
+
+#ifdef	HAVE_ERR_H
+#include <err.h>
+#endif /* HAVE_ERR_H */
+#include <stdio.h>
+#include <string.h>
+
+#include "util.h"
+
+void *
+xmalloc(size_t size)
+{
+	void	*ptr;
+
+	ptr = malloc(size);
+	if (ptr == NULL) {
+		err(1, "malloc");
+	}
+
+	return (ptr);
+}
+
+void *
+xrealloc(void *ptr, size_t size)
+{
+	ptr = realloc(ptr, size);
+	if (ptr == NULL) {
+		err(1, "realloc");
+	}
+
+	return (ptr);
+}
+
+char *
+xstrdup(const char *str)
+{
+	char	*new;
+
+	new = strdup(str);
+	if (new == NULL) {
+		err(1, "strdup");
+	}
+
+	return (new);
+}
+
+char *
+xasprintf(char **strp, const char *fmt, ...)
+{
+	va_list	args;
+
+	va_start(args, fmt);
+	if (vasprintf(strp, fmt, args) < 0) {
+		err(1, "vasprintf");
+	}
+	va_end(args);
+
+	return (*strp);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/util.h	Thu Jan 19 22:39:51 2017 +0100
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2016 Guido Berhoerster <guido+pwm@berhoerster.name>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef	UTIL_H
+#define	UTIL_H
+
+#include <stdlib.h>
+
+#define	EXIT_USAGE	2
+
+#define	COUNTOF(x)	((sizeof (x)/sizeof (0[x])) / ((size_t)(!(sizeof (x) % \
+    sizeof (0[x])))))
+
+void *	xmalloc(size_t);
+void *	xrealloc(void *, size_t);
+char *	xstrdup(const char *);
+char *	xasprintf(char **, const char *, ...);
+
+#endif /* UTIL_H */