Back (Current repo: lowdown)

fork of lowdown from https://kristaps.bsd.lv
To clone this repository:
git clone https://git.viktor1993.net/lowdown.git
Log | Download | Files | Refs | LICENSE

main.c (17672B)


/*	$Id$ */
/*
 * Copyright (c) 2016, 2017, 2020 Kristaps Dzonsons <kristaps@bsd.lv>
 *
 * 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.
 */
#include "config.h"

#if HAVE_SYS_QUEUE
# include <sys/queue.h>
#endif
#include <sys/param.h>
#if HAVE_CAPSICUM
# include <sys/resource.h>
# include <sys/capsicum.h>
#endif
#include <sys/ioctl.h>
#include <sys/stat.h>

#include <assert.h>
#if HAVE_ERR
# include <err.h>
#endif
#include <errno.h>
#include <fcntl.h>
#include <getopt.h>
#include <limits.h> /* INT_MAX */
#include <locale.h> /* set_locale() */
#if HAVE_SANDBOX_INIT
# include <sandbox.h>
#endif
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <termios.h> /* struct winsize */
#include <unistd.h>

#include "lowdown.h"

/*
 * Start with all of the sandboxes.
 * The sandbox_pre() happens before we open our input file for reading,
 * while the sandbox_post() happens afterward.
 */

#if HAVE_PLEDGE

static void
sandbox_post(int fdin, int fddin, int fdout)
{

	if (pledge("stdio", NULL) == -1)
		err(1, "pledge");
}

static void
sandbox_pre(void)
{

	if (pledge("stdio rpath wpath cpath", NULL) == -1)
		err(1, "pledge");
}

#elif HAVE_SANDBOX_INIT

static void
sandbox_post(int fdin, int fddin, int fdout)
{
	char	*ep;
	int	 rc;

	rc = sandbox_init
		(kSBXProfilePureComputation,
		 SANDBOX_NAMED, &ep);
	if (rc != 0)
		errx(1, "sandbox_init: %s", ep);
}

static void
sandbox_pre(void)
{

	/* Do nothing. */
}

#elif HAVE_CAPSICUM

static void
sandbox_post(int fdin, int fddin, int fdout)
{
	cap_rights_t	 rights;

	cap_rights_init(&rights);

	cap_rights_init(&rights, CAP_EVENT, CAP_READ, CAP_FSTAT);
	if (cap_rights_limit(fdin, &rights) < 0)
 		err(1, "cap_rights_limit");

	if (fddin != -1) {
		cap_rights_init(&rights, 
			CAP_EVENT, CAP_READ, CAP_FSTAT);
		if (cap_rights_limit(fddin, &rights) < 0)
			err(1, "cap_rights_limit");
	}

	cap_rights_init(&rights, CAP_EVENT, CAP_WRITE, CAP_FSTAT);
	if (cap_rights_limit(STDERR_FILENO, &rights) < 0)
 		err(1, "cap_rights_limit");

	cap_rights_init(&rights, CAP_EVENT, CAP_WRITE, CAP_FSTAT);
	if (cap_rights_limit(fdout, &rights) < 0)
 		err(1, "cap_rights_limit");

	if (cap_enter())
		err(1, "cap_enter");
}

static void
sandbox_pre(void)
{

	/* Do nothing. */
}

#else /* No sandbox. */

#warning Compiling without sandbox support.

static void
sandbox_post(int fdin, int fddin, int fdout)
{

	/* Do nothing. */
}

static void
sandbox_pre(void)
{

	/* Do nothing. */
}

#endif

static size_t
get_columns(void)
{
	struct winsize	 size;

	memset(&size, 0, sizeof(struct winsize));
	if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &size) == -1)
		return 72;
	return size.ws_col;
}

/*
 * Recognise the metadata format of "foo = bar" and "foo: bar".
 * Translates from the former into the latter.
 * This way "foo = : bar" -> "foo : : bar", etc.
 * Errors out if the metadata is malformed (no colon or equal sign).
 */
static void
metadata_parse(char opt, char ***vals, size_t *valsz, const char *arg)
{
	const char	*loceq, *loccol;
	char		*cp;

	loceq = strchr(arg, '=');
	loccol = strchr(arg, ':');

	if ((loceq != NULL && loccol == NULL) ||
	    (loceq != NULL && loccol != NULL && loceq < loccol)) {
		if (asprintf(&cp, "%.*s: %s\n",
		    (int)(loceq - arg), arg, loceq + 1) == -1)
			err(1, NULL);
		*vals = reallocarray(*vals, *valsz + 1, sizeof(char *));
		if (*vals == NULL)
			err(1, NULL);
		(*vals)[*valsz] = cp;
		(*valsz)++;
		return;
	}
	if ((loccol != NULL && loceq == NULL) ||
	    (loccol != NULL && loceq != NULL && loccol < loceq)) {
		if (asprintf(&cp, "%s\n", arg) == -1)
			err(1, NULL);
		*vals = reallocarray(*vals, *valsz + 1, sizeof(char *));
		if (*vals == NULL)
			err(1, NULL);
		(*vals)[*valsz] = cp;
		(*valsz)++;
		return;
	}
	errx(1, "-%c: malformed metadata", opt);
}

int
main(int argc, char *argv[])
{
	FILE			*fin = stdin, *fout = stdout, 
				*din = NULL;
	const char		*fnin = "<stdin>", *fnout = NULL,
	      	 		*fndin = NULL, *extract = NULL, *er,
				*mainopts = "M:m:sT:t:o:X:",
	      			*diffopts = "M:m:sT:t:o:", *odtstyfn = NULL;
	struct lowdown_opts 	 opts;
	struct stat		 st;
	int			 c, diff = 0, fd,
				 status = 0, aoflag = 0, roflag = 0,
				 aiflag = 0, riflag = 0, centre = 0;
	char			*ret = NULL, *cp, *odtsty = NULL;
	size_t		 	 i, retsz = 0, rcols, sz;
	ssize_t			 ssz;
	struct lowdown_meta 	*m;
	struct lowdown_metaq	 mq;
	struct option 		 lo[] = {
		{ "html-skiphtml",	no_argument,	&aoflag, LOWDOWN_HTML_SKIP_HTML },
		{ "html-no-skiphtml",	no_argument,	&roflag, LOWDOWN_HTML_SKIP_HTML },
		{ "html-escapehtml",	no_argument,	&aoflag, LOWDOWN_HTML_ESCAPE },
		{ "html-no-escapehtml",	no_argument,	&roflag, LOWDOWN_HTML_ESCAPE },
		{ "html-hardwrap",	no_argument,	&aoflag, LOWDOWN_HTML_HARD_WRAP },
		{ "html-no-hardwrap",	no_argument,	&roflag, LOWDOWN_HTML_HARD_WRAP },
		{ "html-head-ids",	no_argument,	&aoflag, LOWDOWN_HTML_HEAD_IDS },
		{ "html-no-head-ids",	no_argument,	&roflag, LOWDOWN_HTML_HEAD_IDS },
		{ "html-owasp",		no_argument,	&aoflag, LOWDOWN_HTML_OWASP },
		{ "html-no-owasp",	no_argument,	&roflag, LOWDOWN_HTML_OWASP },
		{ "html-num-ent",	no_argument,	&aoflag, LOWDOWN_HTML_NUM_ENT },
		{ "html-no-num-ent",	no_argument,	&roflag, LOWDOWN_HTML_NUM_ENT },
		{ "latex-numbered",	no_argument,	&aoflag, LOWDOWN_LATEX_NUMBERED },
		{ "latex-no-numbered",	no_argument,	&roflag, LOWDOWN_LATEX_NUMBERED },
		{ "latex-skiphtml",	no_argument,	&aoflag, LOWDOWN_LATEX_SKIP_HTML },
		{ "latex-no-skiphtml",	no_argument,	&roflag, LOWDOWN_LATEX_SKIP_HTML },
		{ "nroff-skiphtml",	no_argument,	&aoflag, LOWDOWN_NROFF_SKIP_HTML },
		{ "nroff-no-skiphtml",	no_argument,	&roflag, LOWDOWN_NROFF_SKIP_HTML },
		{ "nroff-groff",	no_argument,	&aoflag, LOWDOWN_NROFF_GROFF },
		{ "nroff-no-groff",	no_argument,	&roflag, LOWDOWN_NROFF_GROFF },
		{ "nroff-numbered",	no_argument,	&aoflag, LOWDOWN_NROFF_NUMBERED },
		{ "nroff-no-numbered",	no_argument,	&roflag, LOWDOWN_NROFF_NUMBERED },
		{ "nroff-shortlinks",	no_argument, 	&aoflag, LOWDOWN_NROFF_SHORTLINK },
		{ "nroff-no-shortlinks",no_argument, 	&roflag, LOWDOWN_NROFF_SHORTLINK },
		{ "nroff-nolinks",	no_argument, 	&aoflag, LOWDOWN_NROFF_NOLINK },
		{ "nroff-no-nolinks",	no_argument, 	&roflag, LOWDOWN_NROFF_NOLINK },
		{ "odt-skiphtml",	no_argument,	&aoflag, LOWDOWN_ODT_SKIP_HTML },
		{ "odt-no-skiphtml",	no_argument,	&roflag, LOWDOWN_ODT_SKIP_HTML },
		{ "odt-style",		required_argument, NULL, 6 },
		{ "term-width",		required_argument, NULL, 1 },
		{ "term-hmargin",	required_argument, NULL, 2 },
		{ "term-vmargin",	required_argument, NULL, 3 },
		{ "term-columns",	required_argument, NULL, 4 },
		{ "gemini-link-end",	no_argument, 	&aoflag, LOWDOWN_GEMINI_LINK_END },
		{ "gemini-no-link-end",	no_argument, 	&roflag, LOWDOWN_GEMINI_LINK_END },
		{ "gemini-link-roman",	no_argument, 	&aoflag, LOWDOWN_GEMINI_LINK_ROMAN },
		{ "gemini-no-link-roman", no_argument, 	&roflag, LOWDOWN_GEMINI_LINK_ROMAN },
		{ "gemini-link-noref",	no_argument, 	&aoflag, LOWDOWN_GEMINI_LINK_NOREF },
		{ "gemini-no-link-noref", no_argument, 	&roflag, LOWDOWN_GEMINI_LINK_NOREF },
		{ "gemini-link-inline",	no_argument, 	&aoflag, LOWDOWN_GEMINI_LINK_IN },
		{ "gemini-no-link-inline",no_argument, 	&roflag, LOWDOWN_GEMINI_LINK_IN },
		{ "gemini-metadata",	no_argument, 	&aoflag, LOWDOWN_GEMINI_METADATA },
		{ "gemini-no-metadata",	no_argument, 	&roflag, LOWDOWN_GEMINI_METADATA },
		{ "term-shortlinks",	no_argument, 	&aoflag, LOWDOWN_TERM_SHORTLINK },
		{ "term-no-shortlinks",	no_argument, 	&roflag, LOWDOWN_TERM_SHORTLINK },
		{ "term-nolinks",	no_argument, 	&aoflag, LOWDOWN_TERM_NOLINK },
		{ "term-no-nolinks",	no_argument, 	&roflag, LOWDOWN_TERM_NOLINK },
		{ "term-no-colour",	no_argument, 	&aoflag, LOWDOWN_TERM_NOCOLOUR },
		{ "term-colour",	no_argument, 	&roflag, LOWDOWN_TERM_NOCOLOUR },
		{ "term-no-ansi",	no_argument, 	&aoflag, LOWDOWN_TERM_NOANSI },
		{ "term-ansi",		no_argument, 	&roflag, LOWDOWN_TERM_NOANSI },
		{ "out-smarty",		no_argument,	&aoflag, LOWDOWN_SMARTY },
		{ "out-no-smarty",	no_argument,	&roflag, LOWDOWN_SMARTY },
		{ "out-standalone",	no_argument,	&aoflag, LOWDOWN_STANDALONE },
		{ "out-no-standalone",	no_argument,	&roflag, LOWDOWN_STANDALONE },
		{ "parse-hilite",	no_argument,	&aiflag, LOWDOWN_HILITE },
		{ "parse-no-hilite",	no_argument,	&riflag, LOWDOWN_HILITE },
		{ "parse-tables",	no_argument,	&aiflag, LOWDOWN_TABLES },
		{ "parse-no-tables",	no_argument,	&riflag, LOWDOWN_TABLES },
		{ "parse-fenced",	no_argument,	&aiflag, LOWDOWN_FENCED },
		{ "parse-no-fenced",	no_argument,	&riflag, LOWDOWN_FENCED },
		{ "parse-footnotes",	no_argument,	&aiflag, LOWDOWN_FOOTNOTES },
		{ "parse-no-footnotes",	no_argument,	&riflag, LOWDOWN_FOOTNOTES },
		{ "parse-autolink",	no_argument,	&aiflag, LOWDOWN_AUTOLINK },
		{ "parse-no-autolink",	no_argument,	&riflag, LOWDOWN_AUTOLINK },
		{ "parse-strike",	no_argument,	&aiflag, LOWDOWN_STRIKE },
		{ "parse-no-strike",	no_argument,	&riflag, LOWDOWN_STRIKE },
		{ "parse-super",	no_argument,	&aiflag, LOWDOWN_SUPER },
		{ "parse-no-super",	no_argument,	&riflag, LOWDOWN_SUPER },
		{ "parse-math",		no_argument,	&aiflag, LOWDOWN_MATH },
		{ "parse-no-math",	no_argument,	&riflag, LOWDOWN_MATH },
		{ "parse-codeindent",	no_argument,	&riflag, LOWDOWN_NOCODEIND },
		{ "parse-no-codeindent",no_argument,	&aiflag, LOWDOWN_NOCODEIND },
		{ "parse-intraemph",	no_argument,	&riflag, LOWDOWN_NOINTEM },
		{ "parse-no-intraemph",	no_argument,	&aiflag, LOWDOWN_NOINTEM },
		{ "parse-metadata",	no_argument,	&aiflag, LOWDOWN_METADATA },
		{ "parse-no-metadata",	no_argument,	&riflag, LOWDOWN_METADATA },
		{ "parse-cmark",	no_argument,	&aiflag, LOWDOWN_COMMONMARK },
		{ "parse-no-cmark",	no_argument,	&riflag, LOWDOWN_COMMONMARK },
		{ "parse-deflists",	no_argument,	&aiflag, LOWDOWN_DEFLIST },
		{ "parse-no-deflists",	no_argument,	&riflag, LOWDOWN_DEFLIST },
		{ "parse-img-ext",	no_argument,	&aiflag, LOWDOWN_IMG_EXT }, /* TODO: remove */
		{ "parse-no-img-ext",	no_argument,	&riflag, LOWDOWN_IMG_EXT }, /* TODO: remove */
		{ "parse-ext-attrs",	no_argument,	&aiflag, LOWDOWN_ATTRS },
		{ "parse-no-ext-attrs",	no_argument,	&riflag, LOWDOWN_ATTRS },
		{ "parse-tasklists",	no_argument,	&aiflag, LOWDOWN_TASKLIST },
		{ "parse-no-tasklists",	no_argument,	&riflag, LOWDOWN_TASKLIST },
		{ "parse-maxdepth",	required_argument, NULL, 5 },
		{ NULL,			0,	NULL,	0 }
	};

	/* Get the real number of columns or 72. */

	rcols = get_columns();

	sandbox_pre();

	TAILQ_INIT(&mq);
	memset(&opts, 0, sizeof(struct lowdown_opts));

	opts.maxdepth = 128;
	opts.type = LOWDOWN_HTML;
	opts.feat =
		LOWDOWN_ATTRS |
		LOWDOWN_AUTOLINK |
		LOWDOWN_COMMONMARK |
		LOWDOWN_DEFLIST |
		LOWDOWN_FENCED |
		LOWDOWN_FOOTNOTES |
		LOWDOWN_METADATA |
		LOWDOWN_STRIKE |
		LOWDOWN_SUPER |
		LOWDOWN_TABLES |
		LOWDOWN_TASKLIST;
	opts.oflags = 
		LOWDOWN_HTML_ESCAPE |
		LOWDOWN_HTML_HEAD_IDS |
		LOWDOWN_HTML_NUM_ENT |
		LOWDOWN_HTML_OWASP |
		LOWDOWN_HTML_SKIP_HTML |
		LOWDOWN_NROFF_GROFF |
		LOWDOWN_NROFF_NUMBERED |
		LOWDOWN_NROFF_SKIP_HTML |
		LOWDOWN_ODT_SKIP_HTML |
		LOWDOWN_LATEX_SKIP_HTML |
		LOWDOWN_LATEX_NUMBERED |
		LOWDOWN_SMARTY;

	if (strcasecmp(getprogname(), "lowdown-diff") == 0) 
		diff = 1;

	while ((c = getopt_long(argc, argv, 
	       diff ? diffopts : mainopts, lo, NULL)) != -1)
		switch (c) {
		case 'M':
			metadata_parse(c, &opts.metaovr, 
				&opts.metaovrsz, optarg);
			break;
		case 'm':
			metadata_parse(c, &opts.meta, 
				&opts.metasz, optarg);
			break;
		case 'o':
			fnout = optarg;
			break;
		case 's':
			opts.oflags |= LOWDOWN_STANDALONE;
			break;
		case 't':
		case 'T':
			if (strcasecmp(optarg, "ms") == 0)
				opts.type = LOWDOWN_NROFF;
			else if (strcasecmp(optarg, "gemini") == 0)
				opts.type = LOWDOWN_GEMINI;
			else if (strcasecmp(optarg, "html") == 0)
				opts.type = LOWDOWN_HTML;
			else if (strcasecmp(optarg, "latex") == 0)
				opts.type = LOWDOWN_LATEX;
			else if (strcasecmp(optarg, "man") == 0)
				opts.type = LOWDOWN_MAN;
			else if (strcasecmp(optarg, "fodt") == 0)
				opts.type = LOWDOWN_FODT;
			else if (strcasecmp(optarg, "term") == 0)
				opts.type = LOWDOWN_TERM;
			else if (strcasecmp(optarg, "tree") == 0)
				opts.type = LOWDOWN_TREE;
			else if (strcasecmp(optarg, "null") == 0)
				opts.type = LOWDOWN_NULL;
			else
				goto usage;
			break;
		case 'X':
			extract = optarg;
			break;
		case 0:
			if (roflag)
				opts.oflags &= ~roflag;
			if (aoflag)
				opts.oflags |= aoflag;
			if (riflag)
				opts.feat &= ~riflag;
			if (aiflag)
				opts.feat |= aiflag;
			break;
		case 1:
			opts.cols = strtonum(optarg, 0, INT_MAX, &er);
			if (er == NULL)
				break;
			errx(1, "--term-width: %s", er);
		case 2:
			if (strcmp(optarg, "centre") == 0 ||
			    strcmp(optarg, "centre") == 0) {
				centre = 1;
				break;
			}
			opts.hmargin = strtonum
				(optarg, 0, INT_MAX, &er);
			if (er == NULL)
				break;
			errx(1, "--term-hmargin: %s", er);
		case 3:
			opts.vmargin = strtonum(optarg, 0, INT_MAX, &er);
			if (er == NULL)
				break;
			errx(1, "--term-vmargin: %s", er);
		case 4:
			rcols = strtonum(optarg, 1, INT_MAX, &er);
			if (er == NULL)
				break;
			errx(1, "--term-columns: %s", er);
		case 5:
			opts.maxdepth = strtonum(optarg, 0, INT_MAX, &er);
			if (er == NULL)
				break;
			errx(1, "--parse-maxdepth: %s", er);
		case 6:
			odtstyfn = optarg;
			break;
		default:
			goto usage;
		}

	argc -= optind;
	argv += optind;

	if (opts.type == LOWDOWN_TERM ||
 	    opts.type == LOWDOWN_GEMINI)
		setlocale(LC_CTYPE, "");

	/* 
	 * By default, try to show 80 columns.
	 * Don't show more than the number of available columns.
	 */

	if (opts.cols == 0) {
		if ((opts.cols = rcols) > 80)
			opts.cols = 80;
	} else if (opts.cols > rcols)
		opts.cols = rcols;

	/* If we're centred, set our margins. */

	if (centre && opts.cols < rcols)
		opts.hmargin = (rcols - opts.cols) / 2;

	/* 
	 * Diff mode takes two arguments: the first is mandatory (the
	 * old file) and the second (the new one) is optional.
	 * Non-diff mode takes an optional single argument.
	 */

	if ((diff && (argc == 0 || argc > 2)) || (!diff && argc > 1))
		goto usage;

	if (diff) {
		if (argc > 1 && strcmp(argv[1], "-")) {
			fnin = argv[1];
			if ((fin = fopen(fnin, "r")) == NULL)
				err(1, "%s", fnin);
		}
		fndin = argv[0];
		if ((din = fopen(fndin, "r")) == NULL)
			err(1, "%s", fndin);
	} else {
		if (argc && strcmp(argv[0], "-")) {
			fnin = argv[0];
			if ((fin = fopen(fnin, "r")) == NULL)
				err(1, "%s", fnin);
		}
	}

	/*
	 * If we have a style sheet specified for -Tfodt, load it now
	 * before we drop privileges.
	 */

	if (opts.type == LOWDOWN_FODT && odtstyfn != NULL) {
		if ((fd = open(odtstyfn, O_RDONLY)) == -1)
			err(1, "%s", odtstyfn);
		if (fstat(fd, &st) == -1)
			err(1, "%s", odtstyfn);
		if ((uint64_t)st.st_size > SIZE_MAX - 1)
			errx(1, "%s: file too long", odtstyfn);
		sz = (size_t)st.st_size;
		if ((odtsty = cp = malloc(sz + 1)) == NULL)
			err(1, NULL);
		while (sz > 0) {
			if ((ssz = read(fd, cp, sz)) == -1)
				err(1, "%s", odtstyfn);
			if (ssz == 0)
				errx(1, "%s: short file", odtstyfn);
			sz -= (size_t)ssz;
			cp += ssz;
		}
		*cp = '\0';
		close(fd);
		opts.odt.sty = odtsty;
	}

	/* Configure the output file. */

	if (fnout != NULL && strcmp(fnout, "-") &&
	    (fout = fopen(fnout, "w")) == NULL)
		err(1, "%s", fnout);

	sandbox_post(fileno(fin), din == NULL ? 
		-1 : fileno(din), fileno(fout));

	/* We're now completely sandboxed. */

	/* Require metadata when extracting. */

	if (extract)
		opts.feat |= LOWDOWN_METADATA;

	/* 
	 * Allow NO_COLOUR to dictate colours.
	 * This only works for -Tterm output when not in diff mode.
	 */

	if (getenv("NO_COLOR") != NULL ||
	    getenv("NO_COLOUR") != NULL)
		opts.oflags |= LOWDOWN_TERM_NOCOLOUR;

	if (diff) {
		opts.oflags &= ~LOWDOWN_TERM_NOCOLOUR;
		if (!lowdown_file_diff
		    (&opts, fin, din, &ret, &retsz))
			errx(1, "%s: failed parse", fnin);
	} else {
		if (!lowdown_file(&opts, fin, &ret, &retsz, &mq))
			errx(1, "%s: failed parse", fnin);
	}

	if (extract != NULL) {
		assert(!diff);
		TAILQ_FOREACH(m, &mq, entries) 
			if (strcasecmp(m->key, extract) == 0)
				break;
		if (m != NULL) {
			fprintf(fout, "%s\n", m->value);
		} else {
			status = 1;
			warnx("%s: unknown keyword", extract);
		}
	} else
		fwrite(ret, 1, retsz, fout);

	free(ret);
	free(odtsty);

	if (fout != stdout)
		fclose(fout);
	if (din != NULL)
		fclose(din);
	if (fin != stdin)
		fclose(fin);

	for (i = 0; i < opts.metasz; i++)
		free(opts.meta[i]);
	for (i = 0; i < opts.metaovrsz; i++)
		free(opts.metaovr[i]);

	free(opts.meta);
	free(opts.metaovr);

	lowdown_metaq_free(&mq);
	return status;
usage:
	if (!diff) {
		fprintf(stderr, 
			"usage: lowdown [-s] [input_options] [output_options] [-M metadata]\n"
			"               [-m metadata] [-o output] [-t mode] [-X keyword] [file]\n");
	} else
		fprintf(stderr, 
			"usage: lowdown-diff [-s] [input_options] [output_options] [-M metadata]\n"
			"                    [-m metadata] [-o output] [-t mode] oldfile [newfile]\n");
	return 1;
}