/* [ʞ] mkpw.c - make password
 *  ~ lexi hale <lexi@hale.su>
 *  🄯 GNU AGPL v3
 *  $ cc -O4 mkpw.c -omkpw [-D_CLIPBOARD]
 *    - D_CLIPBOARD enables mkpw to automatically
 *      copy new passwords to the clipboard. it
 *      does this by attempting to execute a sequence
 *      of binaries, and then writing the password
 *      to STDIN of the binary that succeeds.
 *  ? generates passwords
 *  → mkpw is unlikely to be portable to non-POSIX
 *    systems, but should run fine on Linux as well
 *    as BSDs with getrandom() support.
 *  ! for getrandom() to work with the version of
 *    libc on my android phone, the getrandom() call
 *    had to be converted to use the syscall()
 *    interface. this is unlikely to cause problems,
 *    but should be kept in mind.
 */

#include <unistd.h>
#include <sys/random.h>
#include <sys/syscall.h>
#include <stddef.h>
#include <stdint.h>
#include <string.h>
#define sz(a) ( sizeof (a) / sizeof (a) [0] )
#define say(x) (write(2, (x), sizeof (x)))

long syscall(long, ...);

#ifdef _CLIPBOARD
#	include <sys/types.h>
#	include <pwd.h>
#	include <stdlib.h>
#	define _cl_opt o('n',nocopy,copy = false)
#else
#	define _cl_opt o('n',nocopy,/* do nothing*/)
#endif
 
#define options \
	o('l',lower,mode = lower) \
	o('m',mix,mode = mix) \
	o('u',upper,mode = upper)\
	_cl_opt

typedef enum bad {
	ok = 0,
	fail = 1,
	bad_user,
	bad_option,
	bad_syntax,
	bad_entropy,
	bad_copy,
	bad_usage = 64, /* fleabsd idiom */
} bad;

typedef unsigned long long iaia_word_type;
typedef bad iaia_error_type;
enum /* iaia errors */ {
	iaia_e_ok = ok,
	iaia_e_base = fail,
	iaia_e_domain = fail,
	iaia_e_overflow = fail,
};
#define _IAIA_FN_ATOI katoi
#define _IAIA_EXTERNAL_TYPES
#include "clib/iaia.c"

typedef enum {
	tbl_ok = ok, tbl_error = fail
} tbl_error_type;
typedef unsigned char tbl_word_type;
#include "clib/tbl.c"

enum args {
#define o(short, long, code) arg_##long,
	options
#undef o
};

struct tblrow argtbl[] = {
#define o(short, long, code) {arg_##long, #long},
	options
#undef o
};

#define str_ucase "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
#define str_lcase "abcdefghijklmnopqrstuvwxyz"
#define str_num "0123456789" 
const char* reftbl = str_num str_ucase str_lcase;
const char* reftbl_lcase = str_num str_lcase;

#ifdef _CLIPBOARD
char* const* cbd_cmds[] = {
	/* NOTE: these commands must be specified in order of
	 * most- to least-specific. more than one utility may
	 * be present on a given system, so we need to make sure
	 * the right one is called. */
	(char* const[]){"termux-clipboard-set", nullptr},
	(char* const[]){"xsel", "-bi", nullptr},
	/* TODO: allow command to be specified by env var */
	nullptr
};
#endif
int main(int argc, const char** argv) {
	if (argc == 0) return -1;
	if (argc == 1) { 
		size_t namelen = strlen(argv[0]);
		say("\x1b[1musage:\x1b[m ");
		write(2, argv[0], namelen);
		say(" [-lmu"
#		ifdef _CLIPBOARD		
			"n"
#		endif
		"] [--lower] [--mix] [--upper]"
#		ifdef _CLIPBOARD		
			" [--nocopy]"
#		endif
		" <length> [<quantity>]");
		return bad_usage;
	}

	enum { upper, mix, lower } mode = lower;
	unsigned long long len, q = 1;
	bool gotlen = false;
	bool gotq = false;

#	ifdef _CLIPBOARD
		bool copy = true;
#	endif
	for (const char** arg = argv; *arg != nullptr; ++arg) {
		if ((*arg)[0] == '-') {
			if ((*arg)[1] == '-') { /* long opt */
				unsigned char a;
				if (tblget(sz(argtbl), argtbl, *arg + 2, &a) == ok) switch (a) {
#					define o(short, long, code) case arg_##long:code;break;
						options
#					undef o
				} else {
					return bad_option;
				}
			} else { /* short opt */
				for(const char* ptr = (*arg) + 1; *ptr != 0; ++ptr) {
					switch(*ptr) {
#						define o(short, long, code) case short:code;break;
 		   					options
#						undef o
						default: return bad_option;
					}
				}
			}
		} else {
			if (gotq) return bad_syntax;
			else if (gotlen) {
				if (katoi(10, *arg, &q) == ok) {
					gotq = true;
				}
			} else if(katoi(10, *arg, &len) == ok) {
				gotlen = true;
			}
		}
	}
	if (!gotlen) return bad_syntax;
#	ifdef _CLIPBOARD
	if (geteuid() == 0 && copy) {
		/* on a sane system, what we'd do is hike up the process
		 * tree til we found a non-root user. alas, this is UNIX. */
		const char* realuser = getenv("SUDO_USER");
		if (realuser == nullptr) realuser = "nobody";

		say("\x1b[1;33mwarning:\x1b[m you are running \x1b[4m");
			size_t namelen = strlen(argv[0]);
			write(2,argv[0],namelen);
		say("\x1b[24m as \x1b[1mroot\x1b[m! dropping to \x1b[1m");
			write(2,realuser,strlen(realuser));
			setenv("USER", realuser, true);
		say("\x1b[m to prevent malicious behavior\n");

		struct passwd* nobody = getpwnam(realuser);
		if (nobody == nullptr) {
			say("\x1b[1;31mfatal:\x1b[m could not get UID to drop privileges; bailing");
			return bad_user;
		} else {
			setenv("HOME", nobody -> pw_dir, true);
			setenv("SHELL", "/dev/nullptr", true);
			setuid(nobody -> pw_uid);
			if (geteuid() == 0)
				say("\x1b[1;31mnice try:\x1b[m i don't fucking think so, you sneaky bastard");
		}

	}
#	endif
	
	const unsigned char chars = (sizeof str_num - 1) +
		((mode == upper) ? (sizeof str_ucase - 1) :
		((mode == lower) ? (sizeof str_lcase - 1) :
			((sizeof str_ucase - 1) + (sizeof str_lcase - 1))));
	const char* tbl = (mode == upper) ? reftbl :
			((mode == lower) ? reftbl_lcase : reftbl);

	char buf[len+1];
	/* *buf = 0; */

	unsigned char noise[len];
	for (size_t i = 0; i<q; ++i) {
		unsigned char* cur = noise;
		if(syscall(SYS_getrandom, noise, len, 0) == -1)
			return bad_entropy;
		/* getrandom(noise, len, 0); // android doesnt like it */

		for(char* ptr = buf; ptr < buf + len; ++ptr, ++cur) {
			*ptr = tbl[*cur % chars]; /* such a waste of entropy… :( */
		}

		buf[len] = '\n';
		write(1, buf, len + 1);
	}

#	ifdef _CLIPBOARD
		if (copy) {
			char* const clipboard_env = getenv("mkpw_clipboard_setter");
			char* const clipboard_env_arg = getenv("mkpw_clipboard_setter_arg");
			// FIXME: allow multiple args
			int fds[2];
			if (pipe(fds) != 0) return 63;
			if (!fork()) {
				close(fds[1]);
				dup2(fds[0], 0);
				if (clipboard_env != nullptr) {
					execvp(clipboard_env, (char* const[]){
							clipboard_env, clipboard_env_arg, nullptr});
					return bad_copy;
				} else for(char* const** cmd = cbd_cmds; *cmd != nullptr; ++cmd) {
					execvp((*cmd)[0], *cmd);
				}
				return bad_copy; /* exec failed */
			} else {
				close(fds[0]);
				write(fds[1], buf, len);
				write(fds[1], "\n", 1);
				close(fds[1]);
			}
		}
#	endif
	
	return ok;
}
