/* [ʞ] soda - libsodium front end
 *  ~ lexi hale <lexi@hale.su>
 *  🄯 GNU AGPL v3
 *  @ vim: ft=c
 *  ! ascii armor decoding is completely broken;
 *    needs to be fixed before this program can
 *    become usable
 */

#define _POSIX_C_SOURCE 200809L
#include <unistd.h>
#include <stdio.h>
#include <errno.h> /* boo hiss */
#include <stdint.h>
#include <limits.h>
#include <stdbool.h>
#include <string.h>
#include <sodium.h>
#include <fcntl.h>
#define sz(x) (sizeof(x)/sizeof((x)[0]))

#define _IAIA_FN_ITOA str_of_int
#define _IAIA_FN_ATOI int_of_str
#include "clib/iaia.c"

#ifdef _WIN32
#	define open  _open
#	define close _close
#	define read  _read
#	define write _write
#endif

#define _self_name "soda"

typedef unsigned long long word;

typedef enum atom {
	/* commands */
	atom_none,
	atom_key,
	atom_gen,
	atom_pub,
	atom_psk,
	atom_pair,
	atom_cert,
	atom_sign,
	atom_verify,
	atom_enc,
	atom_dec,
	atom_wrap,
	atom_unwrap,
	atom_help,
	_atom_cmd_n,

	/* flags */
	atom_verbose,
	atom_quiet,
	atom_noise,
	atom_ascii_armor,
	atom_color_on,
	atom_color_off,
	atom_out, atom_outfd,
	atom_in, atom_infd,
	atom_pw, atom_pwfd, atom_pwtty,
	atom_sig, atom_sigfd,
	atom_symmetric,
	atom_raw,
	atom_fd,
	atom_fmtv,
	_atom_flag_n,

	/* debug values */
	atom_debug,
	atom_info,
	atom_warn,
	atom_fatal,
	atom_silent,

	_atom_n,
	atom_capture = _atom_n
} atom;

enum noise {
	noise_debug,
	noise_info,
	noise_warn,
	noise_fatal,
	noise_silent,

	_noise_n
};
struct { const char* label; unsigned char color; }
noise_level_text[] = {
	[noise_debug] = {"(debug",5},
	[noise_info ] = {" (info", 4},
	[noise_warn ] = {" (warn", 3},
	[noise_fatal] = {"(fatal",1},
};

struct cmd_spec { const char* desc; const char** params; struct cmd_spec* sub; }
cmd_specs[_atom_cmd_n] = { {NULL,NULL,NULL},
	[atom_key] = { "create and manipulate keypairs for public-key crypto", NULL, (struct cmd_spec[_atom_cmd_n]){
		[atom_gen] = { "generate new keypair", NULL, NULL },
		[atom_pub] = { "extract the public key portion from a keypair", NULL, NULL},
		[atom_wrap] = { "convert a binary key to its ascii-armor representation", NULL, NULL},
	}},
	[atom_psk] = { "create and manipulate keys for secret-key crypto", NULL, (struct cmd_spec[_atom_cmd_n]){
		[atom_gen] = { "generate new PSK", NULL, NULL },
	}},
	[atom_help] = { "display usage listings for commands",
		(const char*[]){"commands…",NULL}, NULL },
	[atom_sign] = { "sign data using public-key crypto",
		(const char*[]){"keyfile",NULL}, NULL },
	[atom_verify] = { "verify signature on data",
		(const char*[]){"pubfile",NULL}, NULL },
	[atom_enc] = { "encrypt data", NULL, (struct cmd_spec[_atom_cmd_n]){
		[atom_pub] = { "encrypt against public key", (const char*[]){
			"pubfile", NULL
		}, NULL},
		[atom_psk] = { "encrypt against symmetric, pre-shared key", (const char*[]){
			"pskfile", NULL
		}, NULL},
	}},
	[atom_wrap] = {"wrap binary input with ascii armor so it can survive transmission mechanisms that are not 8-bit clean", NULL,NULL},
	[atom_unwrap] = {"translate ascii-armored input back to the original binary", NULL,NULL},
};

enum say { say_stop, say_string, say_string_list, say_char, say_uint, say_sint, say_hex };
struct sayp {
	enum say kind; union {
		const char*  str;
		const char** strlist;
		char ch;
		long long unsigned int uint;
		long long signed   int sint;
	};
};

enum feature {
	feature_off = 0,
	feature_on = 1,
	feature_auto = 2,
};

struct ctx {
	size_t argc; const char** argv;
	size_t cmdc; atom* cmdv;
	const char* bin;
	struct {
		enum noise noise;
		enum feature
			ascii_armor,
			color;
		bool fd;
		bool pw_force_tty;
		const char* infile,* outfile,* pwfile,* sigfile;
		word infd, outfd, pwfd, sigfd;
		bool symcrypt;
		word fmtv;
	} flags;
};

bool
isws(const char c) {
	/* TODO: maybe ignore all characters outside the ascii armory? */
	return (c == ' ' || c == '\t' || c == '\n');
}

enum { _buffer_max = 512 };
struct buffer {
	int fd; /* where to flush */
	size_t sz;
	char bytes[_buffer_max];
};
void buffer_flush(struct buffer* b) { write(b->fd, b->bytes, b->sz); b->sz = 0; }
#define _buffer_write(s,b) buffer_write(s, b, sizeof(b))
void buffer_write(struct buffer* b, const char* str, size_t sz) {
	if (sz == 0) {
		size_t i;
		for (i = 0; str[i] != 0; ++i) {
			const size_t ofs = (b->sz + i) % _buffer_max;
			b->bytes[ofs] = str[i];
			if (ofs == 0 && i != 0) buffer_flush(b);
		}
		b -> sz = (b->sz + i) % _buffer_max;
	} else if (sz > _buffer_max) {
		buffer_flush(b);
		write(b->fd, str, sz);
		b->sz = sz;
	} else if (_buffer_max - b->sz < sz) {
		buffer_flush(b);
		memcpy(b->bytes, str, sz);
		b->sz = sz;
	} else {
		memcpy(b->bytes + b->sz, str, sz);
		b->sz += sz;
	}
}
void buffer_push(struct buffer* b, const char c) { buffer_write(b, &c, 1); }

void say(struct ctx c, enum noise kind, struct sayp* s) {
	struct buffer o = {2,0};
	if (c.flags.noise > kind) return; 
	if (noise_level_text -> label != NULL)
	if (c.flags.color) {
		buffer_write(&o,"\x1b[1;3",0);
		buffer_push(&o, 0x30 + noise_level_text[kind].color);
		_buffer_write(&o,"m");
		buffer_write(&o, noise_level_text[kind].label, 0);
		_buffer_write(&o,")\x1b[m");
	} else {
		buffer_write(&o, noise_level_text[kind].label, 0);
		buffer_push(&o,')');
	}

	bool bright = false;
	while(s -> kind != say_stop) {
		if (c.flags.color) {
			if (bright) _buffer_write(&o," \x1b[1m");
				   else _buffer_write(&o," \x1b[m");
		} else {
			buffer_push(&o,' ');
		}
		char intbuf[128];
		char* const intbuf_end = intbuf + sizeof intbuf;
		char* start;

		explain: switch(s -> kind) {
			case say_string: buffer_write(&o, s -> str, 0);  break;
			case say_char:   buffer_push(&o, s -> ch);       break;
			case say_uint:
				str_of_int(10, s -> uint, intbuf, intbuf_end - 1, &start, true);
				buffer_write(&o, start, intbuf_end - start);
				break;
			case say_sint:
				/* TODO oops! iaia doesn't have signed int support yet 🙃 */
				/* str_of_int(10, s -> uint, intbuf, intbuf + sizeof intbuf, &end, true) */
				/* buffer_write(&o, intbuf, end - intbuf); */
				break;
			case say_hex:
				str_of_int(16, s -> uint, intbuf, intbuf_end - 1, &start, true);
				_buffer_write(&o, "0x");
				buffer_write(&o, start, intbuf_end - start);
				break;
			case say_string_list:
				buffer_push(&o,'[');
				for(size_t i = 0; s -> strlist[i] != NULL; ++i) {
					if (i != 0) dprintf(2,", ");
					_buffer_write(&o,"“");
					buffer_write(&o,s -> strlist[i],0);
					_buffer_write(&o,"”");
				}
				buffer_push(&o,']');
			break;
		}

		++s; bright = !bright;
	}

	if (c.flags.color) _buffer_write(&o, "\x1b[m\n");
		else buffer_push(&o, '\n');
	
	buffer_flush(&o);
}

enum { _atom_max_str_len = 12 };
struct { atom val; const char* str;}
/* first name listed for an atom is treated as the canonical name
 * by reconstitute() and displayed in usage listings */
atom_strings[] = {
	/* commands */
	{atom_key, "key"}, {atom_key, "privkey"}, {atom_key, "priv"},
	{atom_pub, "pub"}, {atom_pub, "pubkey"},
	{atom_psk, "psk"},
	{atom_cert, "cert"}, {atom_cert, "certificate"},
	{atom_sign, "sign"},
	{atom_verify, "verify"}, {atom_verify, "vfy"}, {atom_verify, "v"},
	{atom_enc, "enc"}, {atom_enc, "encipher"}, {atom_enc, "encrypt"},
	{atom_dec, "dec"}, {atom_dec, "decipher"}, {atom_dec, "decrypt"},
	{atom_wrap, "wrap"}, {atom_wrap, "armor"},
	{atom_unwrap, "unwrap"}, {atom_unwrap, "dearmor"}, {atom_unwrap, "disarmor"},
	{atom_gen, "gen"}, {atom_gen, "generate"}, {atom_gen, "create"}, {atom_gen, "new"},
	{atom_help, "help"},

	/* flags */
	{atom_quiet, "quiet"},
	{atom_ascii_armor, "ascii-armor"},
	{atom_raw, "raw"},
	{atom_sig, "sig"}, {atom_sig, "sigfile"},
	{atom_sigfd, "sig-fd"}, {atom_sig, "sigfd"},
	{atom_verbose, "verbose"},
	{atom_noise, "noise"},
	{atom_color_on, "color"},
	{atom_color_off, "no-color"},
	{atom_out, "out"}, {atom_out, "output"}, {atom_out, "outfile"},
	{atom_outfd, "out-fd"}, {atom_outfd, "outfd"},
	{atom_in, "in"}, {atom_out, "input"}, {atom_out, "infile"},
	{atom_infd, "in-fd"}, {atom_infd, "infd"},
	{atom_pw, "pw"}, {atom_pw, "passphrase"}, {atom_pw, "password"},
	{atom_pwfd, "pw-fd"}, {atom_pwfd, "pwfd"},
	{atom_pwtty, "pw-tty"}, {atom_pwtty, "pwtty"},
	{atom_fd, "fd"},
	{atom_symmetric, "symmetric"}, {atom_symmetric, "sym"}, {atom_symmetric, "symcrypt"},
	{atom_fmtv, "format"}, {atom_fmtv, "fmt"}, {atom_fmtv, "format-version"}, 

	/* debug levels */
	{atom_silent, "silent"},
	{atom_debug, "debug"}, {atom_debug, "dbg"},
	{atom_info, "info"},
	{atom_warn, "warn"}, {atom_warn, "warnings"},
	{atom_fatal, "fatal"}, {atom_fatal, "error"},
};

atom
atomize(const char* str) {
	for(size_t i = 0; i < sz(atom_strings); ++i) {
		if (strncmp(atom_strings[i].str, str, _atom_max_str_len) == 0) {
			return atom_strings[i].val;
		}
	}
	return atom_none;
}

const char*
reconstitute(atom a) {
	for(size_t i = 0; i < sz(atom_strings); ++i) {
		if (atom_strings[i].val == a) return atom_strings[i].str;
	}
	return NULL;
}

struct flag {
	enum flag_kind {
		flag_none, 
		_flag_noparam,
			flag_on, flag_off, flag_switch,
			flag_inc, flag_dec, flag_sdec,
		_flag_param,
			flag_atom, flag_str,
			flag_uint, flag_sint,
			flag_raw_atom,
		flag_value
	} kind;
	char shortname;
	const char* desc;
	struct { atom first; atom last; } opts;
	size_t offset;
}

#define _declflag_raw(a,k,s,o,rs,re,d) [atom_##a] = { \
	.kind = flag_##k, \
	.shortname = s, \
	.desc = d, \
	.opts = { rs,re }, \
	.offset = (size_t)&(((struct ctx*)0) -> flags.o) \
},
#define _declflag(a,k,s,o,d) _declflag_raw(a,k,s,o,0,0,d)
#define _declflaga(a,rs,re,s,o,d) _declflag_raw(a,atom,s,o,rs,re,d)

flags[_atom_n] = {
	{flag_none},
	_declflag(ascii_armor, value + feature_on, 'a', ascii_armor,
			"emit ascii-encoded data instead of raw binary "
			"data (default if stdout is a TTY)")
	_declflag(raw, value + feature_off, 'r', ascii_armor,
			"emit binary data instead of raw ascii-armored "
			"data (default if stdout is not a TTY)")
	_declflag(in, str, 'i', infile,
			"read data from a file instead of stdin")
	_declflag(infd, uint, 'I', infd,
			"read data from a file instead of stdin")
	_declflag(out, str, 'o', outfile,
			"write data to a file instead of stdout")
	_declflag(outfd, uint, 'O', outfd,
			"write data to a file instead of stdout")
	_declflag(pw, str, 'p', pwfile,
			"load passphrase from file instead of stdin")
	_declflag(pwfd, uint, 'P', pwfd,
			"load passphrase from file instead of stdin")
	_declflag(pwtty, switch, 'W', pw_force_tty,
			"always read password from the input TTY unless a file is specified instead")
	_declflag(sig, str, 'g', sigfile,
			"read signature from specified file instead of extracting it from input data")
	_declflag(sigfd, uint, 'G', sigfd,
			"read signature from specified file descriptor instead of extracting it from input data")
	_declflag(symmetric, switch, 's', symcrypt,
			"secure sensitive key material with symmetric cryptography")
	_declflag(fd, switch, 'f', fd,
			"interpret filename arguments to commands as file descriptor numbers")
	_declflag(color_on, value + feature_on, 'c', color,
			"enable colorized output (default if stderr is a TTY)")
	_declflag(color_off, value + feature_off, 'C', color,
			"disable colorized output")
	_declflag(verbose, dec, 'v', noise,
			"increase noise level")
	_declflag(quiet, value + noise_silent, 'q', noise,
			"emit nothing to stderr")
	_declflaga(noise, atom_debug, atom_silent, 'n', noise,
			"set noise level")
	_declflag(fmtv, uint, 'F', fmtv,
			"use a specific data file format version instead of the native version (0)")
};

const char** flag_enum_desc[_atom_n] = {
	[atom_noise] = (const char*[_noise_n]){
		[noise_silent] = "run in complete silence",
		[noise_debug] = "emit all notices, including those useful only for debugging",
		[noise_info] = "report on the progress of the operation",
		[noise_warn] = "emit non-fatal warnings",
		[noise_fatal] = "only emit fatal errors",
	},
};

#undef _declflag
#undef _declflaga
#undef _declflag_raw

/* the casts here are a bit wacky. this is unfortunately necessary as C
 * insists on evaluating each branch of the generic expression regardless
 * of which type is picked. why anyone thought this was a good idea is
 * beyond me. */
#define _t(x) (_Generic((x), \
	      const char**: (struct sayp){say_string_list, .strlist  = (const char**)(intptr_t)(x)}, \
	              char: (struct sayp){say_char,   .ch   = (char)(intptr_t)(x)}, \
	             char*: (struct sayp){say_string, .str  = (const char*)(intptr_t)(x)}, \
	       const char*: (struct sayp){say_string, .str  = (const char*)(intptr_t)(x)}, \
				   int: (struct sayp){say_sint,   .sint = (long long)(x)}, \
	             short: (struct sayp){say_sint,   .sint = (long long)(x)}, \
	              long: (struct sayp){say_sint,   .sint = (long long)(x)}, \
	         long long: (struct sayp){say_sint,   .sint = (long long)(x)}, \
		      unsigned: (struct sayp){say_uint,   .uint = (long long unsigned)(x)}, \
	    unsigned short: (struct sayp){say_uint,   .uint = (long long unsigned)(x)}, \
	     long unsigned: (struct sayp){say_uint,   .uint = (long long unsigned)(x)}, \
	long long unsigned: (struct sayp){say_uint,   .uint = (long long unsigned)(x)}, \
	default: (struct sayp){say_hex, .uint = (intptr_t)(x)})),
#define _say(k,...)  say(c, noise_##k, ((struct sayp[]){__VA_ARGS__ {say_stop}}))
#define _report(k,s) _say(k, _t(s))

void
usage(struct ctx c, const struct cmd_spec* tree) {
	const char* bin = c.bin;
	const char* fmt_flag,* fmt_usage,* fmt_enum,* head_start,* head_fin;
	if (c.flags.color) {
		fmt_usage = "\x1b[1musage:\x1b[m %s \x1b[32m[flags]\x1b[m%s \x1b[33mcommand\x1b[m\n";
		fmt_flag = "  \x1b[1;36m%c%c \x1b[;95m--%s\x1b[m: %s\n";
		fmt_enum = "      \x1b[94m%s\x1b[m: %s\n";
		head_start = "\x1b[1m"; head_fin = "\x1b[m";
	} else {
		fmt_usage = "usage: %s [flags]%s <command>\n";
		fmt_flag = "  %c%c --%s: %s\n";
		fmt_enum = "      %s: %s\n";
		head_start = head_fin = "";
	}

	char ccmd[32]; char *cptr = ccmd; *cptr = 0;
	if (tree == NULL) tree = cmd_specs;
	else if(c.cmdc > 1) for (size_t i = 1; i<c.cmdc; ++i) {
		*(cptr++)=' ';
		cptr=stpcpy(cptr, reconstitute(c.cmdv[i]));
	}


	dprintf(2,fmt_usage,bin,ccmd);

	dprintf(2,"%scommands:%s\n",head_start,head_fin);
	for (size_t i = 0; i<_atom_cmd_n; ++i) {
		if (tree[i].desc == NULL) continue;
		
		dprintf(2, "  %s", reconstitute(i));
		if (tree[i].params != NULL) {
			for (size_t j = 0; tree[i].params[j] != NULL; ++j) {
				dprintf(2," <%s>", tree[i].params[j]);
			}
		}
		dprintf(2,"%s: %s\n",
				tree[i].sub != NULL ? " …" : "",
				tree[i].desc);
	}

	dprintf(2,"%sflags:%s\n",head_start,head_fin);
	for (size_t i = _atom_cmd_n + 1; i<_atom_flag_n; ++i) {
		/* if (flags[i].kind == flag_none) continue; */
		dprintf(2,fmt_flag,
				flags[i].shortname != 0 ? '-' : ' ',
				flags[i].shortname != 0 ? flags[i].shortname : ' ',
				reconstitute(i), flags[i].desc);
		if (flags[i].kind == flag_atom || flags[i].kind == flag_raw_atom) {
			for (atom a = flags[i].opts.first; a < flags[i].opts.last + 1; ++a) {
				dprintf(2,fmt_enum,reconstitute(a),flag_enum_desc[i][a - flags[i].opts.first]);
			}
		}
	}
}

bool
atom_match(struct ctx c, size_t* match) {
	/* primitive pattern-matching function */
	for (size_t i = 0; i < c.cmdc; ++i) {
		if (match[i] == 0) return false;

		if (match[i] < atom_capture) {
			if (match[i] != c.cmdv[i]) return false;
		} else if (c.cmdv[i] >= _atom_n) {
			const char** dest = (const char**)(match[i] - atom_capture);
			*dest = c.argv[c.cmdv[i] - _atom_n];
		} else {
			return false;
		}
	}
	return match[c.cmdc] == 0;
}

/* the ascii armory is intentionally limited to characters that are
 * not only printable but that are unlikely to have special meaning
 * such that they would require escaping for any form of input.
 * while this is a more limited set than base64, it's significantly
 * stronger armor -- a user can, for instance, simply paste it into
 * most shells without quoting. */
const char ascii_armory[] = "0123456789/abcdefghijklmopqrstuvwxyz."
                            "-ABCDEFGHIJKLMOPQRSTUVWXYZ_";
const char ascii_value[] = 
	"\x25\x24\x0a\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\00\00\00"
	"\00\00\00\00\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30"
	"\x31\x32\00\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e"
	"\00\00\00\00\x3f\00\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13"
	"\x14\x15\x16\x17\00\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20\x21"
	"\x22";
	/* generated by a script. do not touch. */

enum { armor_split_every = 48 };

char* armor
(	char unsigned const* const src,
	char               * const dest,
	size_t               const sz,
	size_t               const brstart,
	bool                 const format
) {
	char* bufptr = dest;
	unsigned char carry = 0;
	for(size_t i = 0; i<sz; ++i) {
		/* insert line breaks every so many chars */
		*(bufptr++)=ascii_armory[src[i] % 64];
		if (format) if ((brstart + (bufptr - dest)) % armor_split_every == 0)
			*(bufptr++)='\n', *(bufptr++)=' ';

		carry<<=2;
		carry |= (src[i] / 64);
		if (i && (i%3 == 0)) { /* :( */
			*(bufptr++)=ascii_armory[carry];
			carry = 0;
		}
		if (format) if ((brstart + (bufptr - dest)) % armor_split_every == 0)
			*(bufptr++)='\n', *(bufptr++)=' ';
	}
	/* if(carry != 0) */ *(bufptr++)=ascii_armory[carry];
	return bufptr;
}

char* disarmor(char unsigned const* const src, char* dest, size_t const sz) {
	/* transforms ascii armor into binary. can transform in place. */
	for(size_t i = 0; i<sz; i += 3) {
		while (isws(src[i])) ++i;
		const char* s = src + i;
		unsigned char carry = ascii_value[s[2] - '-'],
			          b1    = ascii_value[s[0] - '-'] + (64 * ((carry & 12) >> 2)),
		              b2    = ascii_value[s[1] - '-'] + (64 * ( carry &  3));
		*(dest++)=b1; *(dest++)=b2;
	}
	return dest;
}

struct pstr {
	size_t len;
	const char* str;
};
#define _p(x) {sizeof(x)-1,(x)}
struct encoding { struct pstr bin; struct pstr asc; const char* desc;};
enum doc {
	doc_none,
	doc_header,
	doc_footer,

	_doc_versions,
		doc_version_1,
		doc_version_2,
		doc_version_3,

	_doc_types,
		doc_type_priv /* private key */,
		doc_type_pub /* public key */,
		doc_type_psk /* shared secret key */,
		doc_type_asc /* arbitrary ascii-armored data */,
		doc_type_sig /* separate signature */,
		doc_type_signed /* document prefixed with signature */,
		doc_type_master /* master key used to derive keys deterministically */,

	_doc_n
};
const struct encoding docbytes[_doc_n] = { {0,NULL},
	[doc_header]      = {_p("\x26\xe9\x11\x1a"), _p("[" _self_name)},
	[doc_footer]      = {_p(""), _p("]")},
	[doc_version_1]   = {_p("\01"), _p("1-")},
	[doc_version_2]   = {_p("\02"), _p("2-")},
	[doc_version_3]   = {_p("\03"), _p("3-")},
	[doc_type_priv]   = {_p("\01"), _p("priv-"),"private key material"},
	[doc_type_pub]    = {_p("\02"), _p("pub-"), "public key material"},
	[doc_type_psk]    = {_p("\03"), _p("psk-"), "shared key material"},
	[doc_type_asc]    = {_p("\04"), _p("asc-"), "ascii-armored binary data"},
	[doc_type_sig]    = {_p("\05"), _p("sig-"), "separate signature"},
	[doc_type_signed] = {_p("\06"), _p("signed-"), "data with signature"},
	[doc_type_master] = {_p("\07"), _p("master-"), "master key"},
};


char*
compare_wsi(const char* s, char* d, size_t sz) {
	for (size_t i=0; i<sz; ++i) {
		while(isws(*s)) ++s;
		while(isws(*d)) ++d;
		if (*s != *d) return NULL;
		++s; ++d;
	}
	return d;
}

bool
file_cmp(enum doc fld, char** s, bool binary) {
	/* compares against a field and increments the callee's 
	 * pointer appropriately if a match is found */
	if (binary) {
		if (memcmp(*s, docbytes[fld].bin.str, docbytes[fld].bin.len) == 0) {
			*s += docbytes[fld].bin.len;
			return true;
		} else return false;
	} else {
		char* p = compare_wsi(docbytes[fld].asc.str, *s, docbytes[fld].asc.len);
		if (p != NULL) {
			*s = p;
			return true;
		} else return false;
	}
}

struct file {
	enum doc version, type;
	union {
		struct file_signed {
			char* sig, *text;
		} signtext;
		struct file_key {
			char* priv, *pub;
		} key;
		char* pub;
	} data;
	char* datastart;
	size_t rawlen, datlen;
	char raw [];
}* load_file(struct ctx c, const int fd) {
	_say(debug, _t("reading in file from fd") _t(fd));
	size_t run = 512; size_t len = 0;
	struct file* buf = malloc(run + sizeof(struct file)); /* :( */
	int e; while(e = read(fd, buf->raw + len, run - len)) {
		if (e == -1) {
			switch(errno) {
				case EBADF: _say(fatal, _t("file descriptor") _t((unsigned)fd) _t("does not reference an open, readable file")); break;
				case EFAULT: _say(fatal, _t("file descriptor") _t((unsigned)fd) _t("references a file outside your accessible address space")); break;
				case EIO: _say(fatal, _t("file descriptor") _t((unsigned)fd) _t("could not be read due to an I/O error")); break;
				case EISDIR: _say(fatal, _t("file descriptor") _t((unsigned)fd) _t("is a directory, not a readable file")); break;
				default: _say(fatal, _t("file descriptor") _t((unsigned)fd) _t("could not be opened due to an unknown error - consult errno table for code") _t((unsigned)errno)); break;
			}
			goto err;
		}

		len += e;
		if (len == run) {
			run *= 2; buf = realloc(buf, run + sizeof(struct file));
		} else if (len < run) continue;
	}

	buf -> rawlen = len;
	buf -> type = doc_none;
	return buf;

	err: free(buf);
		 return NULL;
}

struct file* read_file(struct ctx c, const int fd) {
	struct file* buf = load_file(c, fd);
	bool binary; size_t len = buf -> rawlen;
	char* cur = buf -> raw;

	if (file_cmp(doc_header,&cur,true)) {
		_report(debug, "parsing binary data");
		binary = true;
	} else if (file_cmp(doc_header,&cur,false)) {
		_report(debug, "parsing ascii-armored data");
		binary = false;
	} else {
		_say(fatal, _t("cannot parse data from fd") _t((unsigned)fd));
		goto err;
	}

	/* write(2, cur, run - (cur - buf->raw)); */

	for (enum doc i = _doc_versions + 1; i < _doc_types; ++ i) {
		if (file_cmp(i, &cur, binary)) {
			_say(debug, _t("file is format version") _t((unsigned) i - _doc_versions));
			buf -> version = i; goto found_version;
		}
	} /* else */ {
		_say(fatal, _t("unrecognized file format version; are you using the most recent release of " _self_name "?"));
		goto err;
	}
	found_version:;
	
	for (enum doc i = _doc_types + 1; i < _doc_n; ++ i) {
		if (file_cmp(i, &cur, binary)) {
			_say(debug, _t("file contains") _t(docbytes[i].desc));
			buf -> type = i; goto found_type;
		}
	} /* else */ {
		_say(fatal, _t("unrecognized file type; are you using the most recent release of " _self_name "?"));
		goto err;
	}
	found_type: if (!binary) {
		size_t s = (len - (cur - buf->raw)) - docbytes[doc_footer].asc.len;
		_say(debug, _t("disarmoring") _t(s) _t("bytes of encoded text"));
		char* end = disarmor(cur, buf -> raw, s);
		buf -> datlen = end - cur;
	} else {
		buf -> datlen = (len - (cur - buf->raw)) - docbytes[doc_footer].bin.len;
	}

	buf -> datastart = cur;
	return buf;

	err: free(buf);
		 return NULL;
}

void
write_armored_v1(struct ctx c, const unsigned char* data, size_t sz) {
	struct buffer o = {c.flags.outfd,0};
#	define _wra_fld(f) buffer_write(&o, docbytes[doc_##f].asc.str,\
		docbytes[doc_##f].asc.len);
	_wra_fld(header);
	_wra_fld(version_1);
	_wra_fld(type_asc);

	char buf[sz*2];
	char* end = armor(data,buf,sz,13, isatty(c.flags.outfd));
	buffer_write(&o, buf, end - buf);

	_wra_fld(footer);
#	undef _wra_fld

	if (isatty(c.flags.outfd)) buffer_push(&o, '\n');
	buffer_flush(&o);
}

void
write_privkey_v1(struct ctx c, const unsigned char* material) {
	struct buffer o = {c.flags.outfd,0};
#	define _wrp_fld(f) buffer_write(&o, \
		(c.flags.ascii_armor ? docbytes[doc_##f].asc.str   \
		                     : docbytes[doc_##f].bin.str), \
		(c.flags.ascii_armor ? docbytes[doc_##f].asc.len   \
		                     : docbytes[doc_##f].bin.len))
	
	_wrp_fld(header);
	_wrp_fld(version_1);
	_wrp_fld(type_priv);

	if (c.flags.ascii_armor) {
		char buf[crypto_sign_SECRETKEYBYTES * 2];
		char* end = armor(material,buf,crypto_sign_SECRETKEYBYTES,13,
				isatty(c.flags.outfd));
		buffer_write(&o, buf, end - buf);
	} else {
		buffer_write(&o, material, crypto_sign_SECRETKEYBYTES);
	}

	_wrp_fld(footer);
	if (isatty(c.flags.outfd)) buffer_push(&o, '\n');
	buffer_flush(&o);
}

typedef void (*write_privkey_t)(struct ctx, const unsigned char*);
typedef void (*write_armored_t)(struct ctx, const unsigned char*, size_t);

write_privkey_t write_privkey[] = {
	[0] = write_privkey_v1, /* current version */

	[1] = write_privkey_v1,
};
write_armored_t write_armored[] = {
	[0] = write_armored_v1, /* current version */

	[1] = write_armored_v1,
};

int
route(struct ctx c) {
	if (sodium_init() < 0) {
		_report(fatal, "could not initialize libsodium");
		return 2;
	}

	switch (c.cmdv[0]) {
		case atom_key: {
			switch (c.cmdv[1]) {
				case atom_gen: {
					if (c.cmdc != 2) { usage(c,NULL); return 1; }
					_report(info, "generating new private key");
					if (c.flags.symcrypt == false) {
						_say(warn, _t("not using symmetric encryption; key will be emitted in") _t("plain text"));
					} else {
						/* read passphrase from stdin/file */
					}
					
					/* libsodium is a bit weird. it uses different key types for
					 * encrypting and signing. we emit a signing (ed25519) key
					 * because it can be converted to an encryption key, but not
					 * vice-versa. libsodium also does not have any means to
					 * derive a public key from a private key, even though this
					 * is trivial, but instead stores the public key in the second
					 * half of the private key (despite emitting it AGAIN into the
					 * second parameter). so we generate a signing keypair, discard
					 * the returned public key, and save the "private key" (including
					 * its copy of the public key). this is extremely awkward and
					 * hopefully a better solution will be found in the fullness of
					 * time. */
					unsigned char skey[crypto_sign_SECRETKEYBYTES];
					{ unsigned char _[crypto_sign_PUBLICKEYBYTES];
					  crypto_sign_keypair(_,skey); }
					(*write_privkey[c.flags.fmtv])(c, skey);

				} break;
				case atom_pub: {
					if (c.cmdc != 2) { usage(c,NULL); return 1; }
					struct file* skey = read_file(c, c.flags.infd);
					if (skey == NULL) return 6;

					free(skey);
				} break;
				case atom_wrap: {
					if (c.cmdc != 2) { usage(c,NULL); return 1; }
					struct file* key = read_file(c, c.flags.infd);
					c.flags.ascii_armor = feature_on;

					if (key -> type == doc_type_priv) {
						(*write_privkey[c.flags.fmtv])(c, key -> datastart);
					} else if (key -> type == doc_type_pub) {
						/* (*write_pubkey[c.flags.fmtv])(c, key); */
					} else {
						_report(fatal, "file is not a " _self_name " key");
						free(key); return 8;
					}
					free(key); return 0;
				} break;

				default: goto no_cmd;
			}
		} break;
		case atom_wrap: {
			if (c.cmdc != 1) { usage(c,NULL); return 1; }

			struct file* data = load_file(c, c.flags.infd);
			if (data == NULL) return 6;

			(*write_armored[c.flags.fmtv])(c, data->raw, data->rawlen);

			free(data); return 0;
		} break;
		case atom_unwrap: {
			if (c.cmdc != 1) { usage(c,NULL); return 1; }
			struct file* data = read_file(c, c.flags.infd);
			if (data == NULL) return 6;
			if (isatty(c.flags.outfd)) _say(warn, _t("printing binary values to tty"));
			write(c.flags.outfd, data->datastart, data->datlen);
			free(data); return 0;
		} break;
		case atom_help: {
			if (c.cmdc == 1) usage(c,NULL);
			else {
				const struct cmd_spec* tree = cmd_specs;
				for (size_t i = 1; i < c.cmdc; ++i) {
					if (c.cmdv[i] < _atom_n) {
						if (tree[c.cmdv[i]].sub != NULL) {
							tree = tree[c.cmdv[i]].sub;
						} else {
							_say(fatal, _t("no help available for subcommand") _t(reconstitute(c.cmdv[i])));
							return 3;
						}
					} else {
						_say(fatal, _t("no help available;") _t(c.argv[c.cmdv[i] - _atom_n]) _t("is not a recognized command"));
						return 4;
					}
				}
				usage(c,tree);
			}
			return 0;
		} break;
		default: goto no_cmd;
	}
	return 0;

	no_cmd: _say(fatal, _t("no such command"));
			usage(c,NULL);
			return 1;
}


int
open_file(struct ctx c, const char* path, int flags, word* retfd) {
	int fd = open(path,flags,0600);
	if (fd == -1) {
		switch (errno) {
			case EACCES: _say(fatal, _t("file") _t(path) _t("could not be opened because you do not have permission to read it")); break;
			case EFAULT: _say(fatal, _t("file") _t(path) _t("could not be opened because the path is outside your accessible address space")); break;
			case EISDIR: _say(fatal, _t("file") _t(path) _t("could not be opened because it is a directory, not a regular file")); break;
			case ELOOP: _say(fatal, _t("file") _t(path) _t("could not be opened because too many symbolic links were encountered")); break;
			case EMFILE: _say(fatal, _t("file") _t(path) _t("could not be opened because the per-process file limit is too low")); break;
			case ENOENT: _say(fatal, _t("file") _t(path) _t("could not be opened because it does not exist")); break;
			case ENOMEM: _say(fatal, _t("file") _t(path) _t("could not be opened, either because not enough memory remains to make use of a FIFO or because the kernel is running out of memory (!)")); break;
			case ENOTDIR: _say(fatal, _t("file") _t(path) _t("could not be opened because a file was specified in place of a directory in the path")); break;
			case ENXIO: _say(fatal, _t("file") _t(path) _t("could not be opened because it is not a regular file or FIFO")); break;
			case EPERM: _say(fatal, _t("file") _t(path) _t("could not be opened because it is sealed against access")); break;
			case EROFS: _say(fatal, _t("file") _t(path) _t("could not be opened for writing because it is located on a read-only filesystem")); break;
			default: _say(fatal, _t("file") _t(path) _t("could not be opened for unclear reasons - consult errno entry for code") _t((unsigned)errno)); break;
		}
		return errno;
	}

	*retfd = fd; return 0;
}

int
main(unsigned int argc, const char** argv) {
	atom cmdlist[argc];
	const char* paramlist[argc];
	size_t paramcount = 0;

	struct ctx c = {
		.argv = paramlist,
		.cmdc = 0,
		.cmdv = cmdlist,
		.bin = (argc > 0 ? argv[0] : NULL),
		.flags = {
			.ascii_armor = feature_auto,
			.color = feature_auto,
			.noise = noise_fatal,
			.infile = NULL, .infd = 0,
			.outfile = NULL, .outfd = 1,
			.pwfile = NULL, .pwfd = 0,
			.pw_force_tty = false,
			.sigfile = NULL, .sigfd = 0,
				/* if sigfd == infd, extract sig from input stream */
			.fd = false,
			.symcrypt = false,
			.fmtv = 0, /* 0 = native version */
		},
	};

	if (argc == 0) {
		_report(fatal, "invoked incorrectly or your environment is not sane; the 0th value of the argument array should be the name of or path to the program you are attempting to execute");
		return -1;
	}

	struct flag* paramstack[argc];
	size_t stackptr = 0;
	enum {
		reading_params,
		reading_commands,
		reading_flags
	} reading_mode = reading_flags;
	_say(debug, _t("parsing command line") _t(argv));
	for (size_t i = 1; i < argc; ++i) {
		const char* const p = argv[i];
		
		if (reading_mode >= reading_flags && p[0] == '-') { /* flag */
			bool longflag;
			if (p[1] == '-') {
				if (p[2] == 0) {
					/* "--" has been passed; all further arguments must be
					 * interpreted as parameters or commands, not flags */
					reading_mode = reading_commands;
					continue;
				} else if (p[2] == '-' && p[3] == 0) {
					/* "---" has been passed; all further arguments must be
					 * interpreted as parameters, not flags or commands */
					reading_mode = reading_params;
					continue;
				} else longflag = true;
			} else longflag = false;
			_say(debug, _t("parsing flag") _t(p));
			for (size_t o = 1; p[o] != 0; ++o) {
				atom option = atom_none;

				if (longflag) option = atomize(p+2);
				else { /* short flag */
					_say(debug, _t("parsing short flag") _t(p[o]));
					for (size_t j = 0; j < sz(flags); ++ j)
						if (flags[j].kind != flag_none && flags[j].shortname == p[o]) {
							option = (atom)j; break;
						}
				}

				if (option == atom_none) {
					if (!longflag && p[o] == '-') {
						if (p[o+1] == '-' && p[o+2] == 0) {
							/* shorthand for --- */
							reading_mode = reading_params;
							continue;
						} else if (p[o+1] == 0) {
							/* shorthand for -- */
							reading_mode = reading_commands;
							continue;
						}
					}
					char ftext[3] = {'-',p[o],0};
					_say(fatal, _t("flag") _t(longflag ? p+2 : ftext) _t("not meaningful"));
					goto parse_fail;
				}

				handle_flag:;
					struct flag* f = flags + option;
					void* fld = ((unsigned char*)&c) + (unsigned char)(f -> offset);
					if (f -> kind > _flag_param) {
						if (f->kind>=flag_value) {
							*((unsigned*)fld) = f->kind - flag_value;
						} else paramstack[stackptr++] = f;
					} else if (f -> kind > _flag_noparam) {
						switch(f -> kind) {
							case flag_on:  *((bool*)fld) = true; break;
							case flag_off: *((bool*)fld) = false; break;
							case flag_switch: *((bool*)fld) = !*((bool*)fld); break;
							/* in order to avoid creepy behavior in pathological cases,
							 * we enforce saturation arithmetic for the increment/decrement
							 * flag types */
							case flag_inc: {
								unsigned* v = fld;
								if (*v < UINT_MAX) ++*v;
							} break;
							case flag_dec: {
								unsigned* v = fld;
								if (*v > 0) --*v;
							} break;
							case flag_sdec: {
								signed* v = fld;
								if (*v > INT_MIN) --*v;
							} break;
						}
					}

				if (p[1] == '-') break;
			}
			continue;
		}

		if (stackptr > 0) {
			struct flag* f = paramstack[--stackptr];
			_say(debug, _t("handling argument for flag") _t(reconstitute(f - flags)));
			void* fld = ((unsigned char*)&c) + (unsigned char)(f -> offset);
			switch(f -> kind) {
				case flag_str: *((const char**)fld) = p; break;
				case flag_uint: {
					iaia_error_type e = int_of_str(10, p, (unsigned long long*)fld);
					switch (e) {
						case iaia_e_ok: continue;
						case iaia_e_domain: 
							_say(fatal, _t("argument") _t(p) _t("outside domain for base"));
					}
					return 5;
				} break;
				case flag_atom: case flag_raw_atom: {
					atom a = atomize(p);
					if (a == atom_none || a < f -> opts.first || a > f -> opts.last) {
						_say(fatal, _t("invalid argument") _t(p) _t("for flag")
								_t(reconstitute(f - flags)));
						goto parse_fail;
					}
					*((atom*)fld) = f->kind == flag_raw_atom ? a : a - f -> opts.first;
				} break;

			}
			continue;
		}

		if (reading_mode >= reading_commands) {
			if (p[0] == '=') { /* parameter even if atomizable */
				paramlist[paramcount] = p + 1;
				cmdlist[c.cmdc] = _atom_n + paramcount;
				++c.cmdc; ++ paramcount;
				continue;
			} else {
				atom a = atomize(p);
				if (a != atom_none) {
					cmdlist[c.cmdc] = a;
					++ c.cmdc;
					continue;
				}
			}
		}

		/* only parameters remain to be read */
		paramlist[paramcount] = p;
		cmdlist[c.cmdc] = _atom_n + paramcount;
		++ c.cmdc; ++ paramcount;
	}
	
	parse_done: _say(debug, _t("stored") _t(c.cmdc) _t("arguments and") _t(paramcount) _t("parameters"));
	
	if (c.flags.noise <= noise_debug) for (size_t i = 0; i<c.cmdc; ++i) {
		if (cmdlist[i] < _atom_n) {
			dprintf(2, "\t%zu. atom %u (normal form “%s”)\n", i, cmdlist[i], reconstitute(cmdlist[i]));
		} else {
			dprintf(2,"\t%zu. parameter “%s”\n", i, paramlist[cmdlist[i] - _atom_n]);
		}
	}

	if (c.flags.fmtv >= sz(write_privkey)) {
		_say(fatal, _t("data format version") _t(c.flags.fmtv) _t("is not known. the highest available version is") _t(sz(write_privkey) - 1) _t("- are you sure you're using the latest release of " _self_name "?"));
		return -2;
	}
	int err;
	if (c.flags.pwfile != NULL) {
		err = open_file(c,c.flags.pwfile, O_RDONLY, &c.flags.pwfd);
		if(err) return err;
	}
	if (c.flags.infile != NULL) {
		err = open_file(c,c.flags.infile, O_RDONLY, &c.flags.infd);
		if(err) return err;
	}
	if (c.flags.outfile != NULL) {
		err = open_file(c,c.flags.outfile, O_WRONLY | O_TRUNC | O_CREAT, &c.flags.outfd);
		if(err) return err;
	}
	if (c.flags.sigfile != NULL) {
		err = open_file(c,c.flags.sigfile, O_RDONLY, &c.flags.sigfd);
		if(err) return err;
	}


	if (c.flags.color == feature_auto)
		c.flags.color = isatty(2);
	if (c.flags.ascii_armor == feature_auto)
		c.flags.ascii_armor = isatty(c.flags.outfd);

	/* this is hateful but i can't figure out a better way */
	if (c.flags.pw_force_tty && !isatty(c.flags.pwfd)) {
		_say(debug, _t("passphrases cannot be read from fd") _t(c.flags.pwfd) _t("as it is not a tty and -W was passed;") _t("/dev/tty") _t("will be used instead"));
	} else if (c.flags.pwfd == c.flags.infd) {
		_say(debug, _t("passphrases cannot be read from fd") _t(c.flags.pwfd) _t("as input data is already being read from that descriptor;") _t("/dev/tty") _t("will be used instead"));
	} else if (c.flags.pwfd == c.flags.sigfd) {
		_say(warn, _t("passphrases cannot be read from fd") _t(c.flags.pwfd) _t("as the signature is already being read from that descriptor;") _t("/dev/tty") _t("will be used instead"));
	} else goto skip_pwfd_change;

	pwfd_change: {
		err = open_file(c,"/dev/tty", O_WRONLY, &c.flags.pwfd);
		if(err) return err;
	}

	skip_pwfd_change:

	_say(debug, _t("input fd =") _t(c.flags.infd));
	_say(debug, _t("output fd =") _t(c.flags.outfd));
	_say(debug, _t("pw fd =") _t(c.flags.pwfd));

	if (c.cmdc > 0) return route(c);

	parse_fail: if (c.flags.noise < noise_silent) usage(c,NULL);
	            return 1;
}
