/*
 * This file is part of rcsparse.
 *
 * rcsparse is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published
 * by the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * rcsparse is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with rcsparse.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <sys/types.h>
#include <sys/mman.h>
#include <sys/stat.h>

#include <ctype.h>
#include <err.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include "queue.h"
#include "tree.h"

#include "rcsparse.h"

struct line {
	char		*str;
	size_t		len;
	size_t		atcount;
};

struct stringinfo {
	size_t		size;
	size_t		pos;
	struct line	lines[0];
};

#define	STRNFO_LINES	8

struct rcsdelta {
	SLIST_ENTRY(rcsdelta) link;
	int		action;
	size_t		pos;
	size_t		len;
	struct line	*lines;
};


static int cmprev(struct rcsrev *, struct rcsrev *);
static int cmptokpair(struct rcstokpair *, struct rcstokpair *);
static struct rcstoken *checktok(struct rcsfile *);
static int skipws(struct rcsfile *);
static struct rcstoken *parsestring(struct rcsfile *, struct stringinfo **);
static struct rcstoken *parsetoken(struct rcsfile *);
static int tokcmp(struct rcstoken *, struct rcstoken *);
static int tokeqtok(struct rcstoken *, struct rcstoken *);
static int tokeqstr(struct rcstoken *, const char *);
static int tokeqstrn(struct rcstoken *, const char *, size_t);
static int expecttok(struct rcsfile *, int);
static int opttok(struct rcsfile *, int);
static int expecttokstr(struct rcsfile *, const char *);
static int opttokstr(struct rcsfile *, const char *);
static char *strnfo2txtbuf(struct stringinfo *, size_t *);
static int applydelta(struct stringinfo **, struct stringinfo *);


RB_GENERATE(rcsrevtree, rcsrev, link, cmprev);
RB_GENERATE(rcstokmap, rcstokpair, link, cmptokpair);

static int
cmprev(struct rcsrev *rev1, struct rcsrev *rev2)
{
	return tokcmp(rev1->rev, rev2->rev);
}

static int
cmptokpair(struct rcstokpair *pair1, struct rcstokpair *pair2)
{
	return tokcmp(pair1->first, pair2->first);
}

static struct rcstoken *
checktok(struct rcsfile *rcs)
{
	if (rcs->tok == NULL)
		rcs->lasttok = rcs->tok = calloc(1, sizeof(struct rcstoken));

	return rcs->tok;
}

static int
resizestrnfo(struct stringinfo **si, size_t len)
{
	size_t newsize;

	newsize = (*si)->size;
	while (len > newsize)
		newsize *= 2;

	if (newsize > (*si)->size) {
		struct stringinfo *nsi;

		nsi = realloc(*si, sizeof(struct stringinfo) +
		    newsize * sizeof(struct line));
		if (nsi == NULL)
			return -1;
		nsi->size = newsize;
		*si = nsi;
	}

	return 0;
}

static struct stringinfo *
copystrnfo(struct stringinfo *si)
{
	struct stringinfo *nsi;
	size_t size;

	size = sizeof(*si) + si->size * sizeof(si->lines[0]);
	nsi = malloc(size);
	if (nsi == NULL)
		return NULL;
	memcpy(nsi, si, size);
	return nsi;
}

static int
skipws(struct rcsfile *rcs)
{
	for (; rcs->pos < rcs->end; rcs->pos++) {
		switch (*rcs->pos) {
		case ' ':
		case '\b':
		case '\t':
		case '\n':
		case '\r':
		case '\v':
		case '\f':
			continue;
		}

		break;
	}

	return rcs->pos == rcs->end ? -1 : 0;
}

static struct rcstoken *
parsestring(struct rcsfile *rcs, struct stringinfo **sip)
{
	int atcount;
	struct stringinfo *si = NULL;
	struct rcstoken *tok;

	if (skipws(rcs) < 0)
		return NULL;

	if (*rcs->pos != '@')
		return NULL;

	tok = checktok(rcs);
	if (tok == NULL)
		return NULL;

	rcs->pos++;

	if (sip != NULL) {
		*sip = NULL;
		si = malloc(sizeof(struct stringinfo) + STRNFO_LINES * sizeof(struct line));
		if (si == NULL)
			return NULL;

		si->size = STRNFO_LINES;
		si->pos = 0;
		si->lines[0].str = rcs->pos;
		si->lines[0].atcount = 0;
	}

	tok->str = rcs->pos;
	atcount = 0;
	for (; rcs->pos < rcs->end; rcs->pos++) {
		switch (*rcs->pos) {
		case '\n':
			if (si != NULL) {
				if (resizestrnfo(&si, si->pos + 2) < 0)
					goto fail;

				si->lines[si->pos].len = rcs->pos - si->lines[si->pos].str + 1;
				si->pos++;
				si->lines[si->pos].str = rcs->pos + 1;
				si->lines[si->pos].atcount = 0;
			}
			continue;

		case '@':
			if (rcs->pos + 1 == rcs->end)
				goto fail;

			rcs->pos++;
			if (*rcs->pos != '@')
				break;

			atcount++;
			if (si != NULL)
				si->lines[si->pos].atcount++;
			continue;

		default:
			continue;
		}

		/* If we reached this point, we need to finish */

		break;
	}

	if (si != NULL) {
		si->lines[si->pos].len = rcs->pos - si->lines[si->pos].str - 1;
		if (si->lines[si->pos].len != 0)	/* last line didn't end with '\n' */
			si->pos++;
		*sip = si;
	}

	tok->len = rcs->pos - tok->str - 1;
	tok->type = atcount > 0 ? TOK_STRINGAT : TOK_STRING;

	return tok;

fail:
	if (si != NULL)
		free(si);

	return NULL;
}

static struct rcstoken *
parsetoken(struct rcsfile *rcs)
{
	int ch;
	int type;
	int finish;
	struct rcstoken *tok;

	if (skipws(rcs) < 0)
		return NULL;

	tok = checktok(rcs);
	if (tok == NULL)
		return NULL;

	ch = *rcs->pos;
	switch (ch) {
	case ';':
	case ':':
	case ',':
	case '$':
		tok->type = ch;
		tok->str = rcs->pos;
		rcs->pos++;
		tok->len = 1;
		return tok;

	case '@':
		return parsestring(rcs, NULL);
	}

	tok->str = rcs->pos;
	type = 0;
	finish = 0;
	while (rcs->pos < rcs->end && !finish) {
		ch = *rcs->pos;
		switch (ch) {
		case ' ':
		case '\b':
		case '\t':
		case '\n':
		case '\r':
		case '\v':
		case '\f':
		case ';':
		case ':':
		case ',':
		case '$':
		case '@':
			finish = 1;
			continue;
		}

		if (isdigit(ch))
			type |= TOK_DIGIT;
		else if (ch == '.')
			type |= TOK_DOT;
		else
			type |= TOK_PRINT;

		rcs->pos++;
	}

	tok->type = type;
	tok->len = rcs->pos - tok->str;

	return tok;
}

static int
tokcmp(struct rcstoken *tok1, struct rcstoken *tok2)
{
	char *pos1, *pos2, *end1, *end2;

	pos1 = tok1->str;
	end1 = pos1 + tok1->len;
	pos2 = tok2->str;
	end2 = pos2 + tok2->len;
	for (; pos1 < end1 && pos2 < end2; pos1++, pos2++)
		if (*pos1 != *pos2)
			return *pos1 - *pos2;

	if (pos1 == end1) {
		if (pos2 == end2)
			return 0;
		else
			return -1;
	} else {
		return 1;
	}
}

static int
tokeqtok(struct rcstoken *tok1, struct rcstoken *tok2)
{
	return tokcmp(tok1, tok2) == 0;
}

static int
tokeqstr(struct rcstoken *tok, const char *str)
{
	char *pos, *endpos;

	pos = tok->str;
	endpos = pos + tok->len;
	for (; pos < endpos && *str; pos++, str++) {
		if (*str != *pos)
			return 0;
	}

	if (pos == endpos && *str == '\0')
		return 1;
	else
		return 0;
}

static int
tokeqstrn(struct rcstoken *tok, const char *str, size_t len)
{
	char *pos, *endpos;

	if (tok->len < len)
		return 0;

	pos = tok->str;
	endpos = pos + tok->len;
	for (; len && *str; pos++, str++, len--) {
		if (*str != *pos)
			return 0;
	}

	return len == 0 ? 1 : 0;
}

static int
expecttok(struct rcsfile *rcs, int type)
{
	if (parsetoken(rcs) == NULL)
		return -2;

	if (rcs->tok->type == type)
		return 0;
	else
		return -1;
}

static int
opttok(struct rcsfile *rcs, int type)
{
	return expecttok(rcs, type) + 1;
}

static int
expecttokstr(struct rcsfile *rcs, const char *str)
{
	if (parsetoken(rcs) == NULL)
		return -2;

	return tokeqstr(rcs->tok, str) ? 0 : -1;
}

static int
opttokstr(struct rcsfile *rcs, const char *str)
{
	int ret;

	ret = expecttokstr(rcs, str);
	if (ret == -1)
		rcs->pos = rcs->tok->str;

	return ret + 1;
}

static char *
tokstripat(struct rcstoken *tok)
{
	char *ret;

	ret = malloc(tok->len + 1);
	if (ret == NULL)
		return NULL;

	if (tok->type == TOK_STRING) {
		bcopy(tok->str, ret, tok->len);
		ret[tok->len] = '\0';
	} else {
		char *endpos, *at, *ipos, *opos;

		ipos = tok->str;
		endpos = ipos + tok->len;
		opos = ret;
		while ((at = memchr(ipos, '@', endpos - ipos)) != NULL) {
			bcopy(ipos, opos, at - ipos + 1);
			opos += at - ipos + 1;
			ipos = at + 2;
		}
		bcopy(ipos, opos, endpos - ipos);
		opos += endpos - ipos;
		*opos = '\0';
	}

	return ret;
}

static char *
strnfo2txtbuf(struct stringinfo *si, size_t *plen)
{
	struct line *curline;
	size_t lineno, len;
	char *pos, *ret;

	for (len = lineno = 0; lineno < si->pos; lineno++)
		len += si->lines[lineno].len;

	ret = malloc(len + 1);
	if (ret == NULL)
		return NULL;
	ret[len] = '\0';

	pos = ret;
	for (lineno = 0, curline = si->lines; lineno < si->pos; lineno++, curline++) {
		if (curline->atcount == 0) {
			bcopy(curline->str, pos, curline->len);
			pos += curline->len;
		} else {
			char *oldpos, *newpos, *endpos;
			size_t atn;

			oldpos = curline->str;
			endpos = oldpos + curline->len;
			for (atn = 0; atn < curline->atcount; atn++) {
				newpos = memchr(oldpos, '@', endpos - oldpos);
				bcopy(oldpos, pos, newpos - oldpos + 1);
				pos += newpos - oldpos + 1;
				oldpos = newpos + 2;
			}
			bcopy(oldpos, pos, endpos - oldpos);
			pos += endpos - oldpos;
		}
	}

	if (plen != NULL)
		*plen = pos - ret;

	return ret;
}

static int
applydelta(struct stringinfo **text, struct stringinfo *deltatext)
{
	SLIST_HEAD(, rcsdelta) deltas;
	struct rcsdelta *curdelta;
	struct stringinfo *curtext;
	size_t lineno;
	int ret = -1;

	curdelta = NULL;
	SLIST_INIT(&deltas);

	curtext = *text;
	for (lineno = 0; lineno < deltatext->pos; lineno++) {
		char *pos, *endpos;

		pos = deltatext->lines[lineno].str;
		endpos = pos + deltatext->lines[lineno].len;
		if (endpos - pos < 5)	/* check for minimum */
			goto fail;

		curdelta = calloc(1, sizeof(struct rcsdelta));
		if (curdelta == NULL)
			goto fail;

		if (*pos != 'a' && *pos != 'd')
			goto fail;
		curdelta->action = *pos;
		pos++;
		for (curdelta->pos = 0; pos < endpos && isdigit(*pos); pos++)
			curdelta->pos = curdelta->pos * 10 + *pos - '0';

		if (pos == endpos || *pos++ != ' ' || pos == endpos)
			goto fail;

		for (curdelta->len = 0; pos < endpos && isdigit(*pos); pos++)
			curdelta->len = curdelta->len * 10 + *pos - '0';

		if (pos == endpos || *pos != '\n')
			goto fail;

		if (curdelta->len == 0)
			goto fail;

		if (curdelta->action == 'a') {
			curdelta->lines = &deltatext->lines[lineno + 1];
			lineno += curdelta->len;
		}

		SLIST_INSERT_HEAD(&deltas, curdelta, link);
		curdelta = NULL;
	}

	if (lineno != deltatext->pos)
		goto fail;

	while ((curdelta = SLIST_FIRST(&deltas)) != NULL) {
		SLIST_REMOVE_HEAD(&deltas, link);

		switch (curdelta->action) {
		case 'a':
			if (resizestrnfo(&curtext, curtext->pos + curdelta->len) < 0)
				goto fail;

			bcopy(&curtext->lines[curdelta->pos],
			    &curtext->lines[curdelta->pos + curdelta->len],
			    (curtext->pos - curdelta->pos) * sizeof(struct line));
			bcopy(curdelta->lines,
			    &curtext->lines[curdelta->pos],
			    curdelta->len * sizeof(struct line));
			curtext->pos += curdelta->len;
			break;
		case 'd':
			if (curdelta->pos <= 0 ||
			    curdelta->pos > curtext->pos ||
			    curdelta->pos + curdelta->len - 1 > curtext->pos)
				goto fail;
			bcopy(&curtext->lines[curdelta->pos + curdelta->len - 1],
			    &curtext->lines[curdelta->pos - 1],
			    (curtext->pos - curdelta->pos - curdelta->len + 1) *
			    sizeof(struct line));
			curtext->pos -= curdelta->len;
			break;
		}

		free(curdelta);
	}

	ret = 0;

fail:
	if (curdelta != NULL)
		free(curdelta);
	while ((curdelta = SLIST_FIRST(&deltas)) != NULL) {
		SLIST_REMOVE_HEAD(&deltas, link);
		free(curdelta);
	}
	*text = curtext;

	return ret;
}

static void
rcsfreerev(struct rcsrev *rev)
{
	struct rcstoken *tok;

	free(rev->rev);
	free(rev->date);
	free(rev->author);
	if (rev->state != NULL)
		free(rev->state);
	if (rev->next != NULL)
		free(rev->next);
	while ((tok = SLIST_FIRST(&rev->branches)) != NULL) {
		SLIST_REMOVE_HEAD(&rev->branches, link);
		free(tok);
	}
	if (rev->commitid != NULL)
		free(rev->commitid);
	if (rev->log != NULL)
		free(rev->log);
	if (rev->rawtext != NULL)
		free(rev->rawtext);
	if (rev->text != NULL)
		free(rev->text);

	free(rev);
}

int
rcsparseadmin(struct rcsfile *rcs)
{
	if (rcs->revpos != NULL)
		return 0;

	if (expecttokstr(rcs, "head") < 0)
		return -1;

	if (opttok(rcs, ';') == 0) {
		if ((rcs->tok->type & ~TOK_NUM) != 0)
			return -1;

		rcs->admin.head = rcs->tok;
		rcs->tok = NULL;
		if (expecttok(rcs, ';') < 0)
			return -1;
	}

	if (opttokstr(rcs, "branch") > 0) {
		if (opttok(rcs, ';') == 0) {
			if ((rcs->tok->type & ~TOK_NUM) != 0)
				return -1;

			rcs->admin.branch = rcs->tok;
			rcs->tok = NULL;
			if (expecttok(rcs, ';') < 0)
				return -1;
		}
	}

	if (expecttokstr(rcs, "access") < 0)
		return -1;
	while (opttok(rcs, ';') == 0) {
		SLIST_INSERT_HEAD(&rcs->admin.access, rcs->tok, link);
		rcs->tok = NULL;
	}

	if (expecttokstr(rcs, "symbols") < 0)
		return -1;
	while (opttok(rcs, ';') == 0) {
		struct rcstokpair *pair;

		if ((rcs->tok->type & TOK_DOT) != 0)
			return -1;
		pair = calloc(1, sizeof(struct rcstokpair));
		if (pair == NULL)
			return -1;

		pair->first = rcs->tok;
		rcs->tok = NULL;

		if (expecttok(rcs, ':') < 0 ||
		    parsetoken(rcs) == NULL ||
		    (rcs->tok->type & ~TOK_NUM) != 0) {
			free(pair);
			return -1;
		}

		pair->second = rcs->tok;
		rcs->tok = NULL;

		RB_INSERT(rcstokmap, &rcs->admin.symbols, pair);
	}

	if (expecttokstr(rcs, "locks") < 0)
		return -1;
	while (opttok(rcs, ';') == 0) {
		struct rcstokpair *pair;

		pair = calloc(1, sizeof(struct rcstokpair));
		if (pair == NULL)
			return -1;

		pair->first = rcs->tok;
		rcs->tok = NULL;

		if (expecttok(rcs, ':') < 0 ||
		    parsetoken(rcs) == NULL ||
		    (rcs->tok->type & ~TOK_NUM) != 0) {
			free(pair);
			return -1;
		}

		pair->second = rcs->tok;
		rcs->tok = NULL;

		RB_INSERT(rcstokmap, &rcs->admin.locks, pair);
	}

	if (opttokstr(rcs, "strict") > 0) {
		rcs->admin.strict = 1;
		if (expecttok(rcs, ';') < 0)
			return -1;
	}

	if (opttokstr(rcs, "comment") > 0) {
		if (opttok(rcs, ';') == 0) {
			rcs->admin.comment = rcs->tok;
			rcs->tok = NULL;
			if (expecttok(rcs, ';') < 0)
			    return -1;
		}
	}

	if (opttokstr(rcs, "expand") > 0) {
		if (opttok(rcs, ';') == 0) {
			rcs->admin.expand = rcs->tok;
			rcs->tok = NULL;
			if (expecttok(rcs, ';') < 0)
			    return -1;
		}
	}

	for (;;) {
		rcs->revpos = rcs->pos;

		if (parsetoken(rcs) == NULL)
			return -1;
		if (tokeqstr(rcs->tok, "desc")) {
			rcs->pos = rcs->tok->str;
			break;
		}
		if ((rcs->tok->type & ~TOK_NUM) == 0) {
			rcs->pos = rcs->tok->str;
			break;
		}

		while (opttok(rcs, ';') == 0)
			;
	}

	return 0;
}

int
rcsparsetree(struct rcsfile *rcs)
{
	struct rcsrev searchrev;
	struct rcsrev *rev = NULL;

	if (rcs->deltapos != NULL)
		return 0;

	if (rcsparseadmin(rcs) < 0)
		return -1;

	rcs->pos = rcs->revpos;

	for (;;) {
		if (parsetoken(rcs) == NULL)
			return -1;

		if (tokeqstr(rcs->tok, "desc")) {
			rcs->pos = rcs->tok->str;
			break;
		}

		rev = calloc(1, sizeof(struct rcsrev));
		if (rev == NULL)
			return -1;

		if ((rcs->tok->type & ~TOK_NUM) != 0)
			goto fail;
		rev->rev = rcs->tok;
		rcs->tok = NULL;

		if (expecttokstr(rcs, "date") < 0)
			goto fail;
		if (expecttok(rcs, TOK_NUM) < 0)
			goto fail;
		if (rcs->tok->len != 17 && rcs->tok->len != 19)
			goto fail;
		rev->date = rcs->tok;
		rcs->tok = NULL;
		if (expecttok(rcs, ';') < 0)
			goto fail;

		if (expecttokstr(rcs, "author") < 0)
			goto fail;
		if (parsetoken(rcs) == NULL)
			goto fail;
		if ((rcs->tok->type & (TOK_STRING | TOK_PRINT)) != TOK_PRINT)
			goto fail;
		rev->author = rcs->tok;
		rcs->tok = NULL;
		if (expecttok(rcs, ';') < 0)
			goto fail;

		if (expecttokstr(rcs, "state") < 0)
			goto fail;
		if (opttok(rcs, ';') == 0) {
			rev->state = rcs->tok;
			rcs->tok = NULL;
			if (expecttok(rcs, ';') < 0)
				goto fail;
		}

		if (expecttokstr(rcs, "branches") < 0)
			goto fail;
		while (opttok(rcs, ';') == 0) {
			SLIST_INSERT_HEAD(&rev->branches, rcs->tok, link);
			rcs->tok = NULL;
		}

		if (expecttokstr(rcs, "next") < 0)
			goto fail;
		if (opttok(rcs, ';') == 0) {
			rev->next = rcs->tok;
			rcs->tok = NULL;
			if (expecttok(rcs, ';') < 0)
				goto fail;
		}
		if (expecttokstr(rcs, "commitid") < 0) {
			/* No hit, rewind */
			rcs->pos = rcs->tok->str;
		} else {
			if (parsetoken(rcs) == NULL)
				goto fail;
			rev->commitid = rcs->tok;
			rcs->tok = NULL;
			if (expecttok(rcs, ';') < 0)
				goto fail;
		}

		for (;;) {
			if (parsetoken(rcs) == NULL)
				return -1;
			if (tokeqstr(rcs->tok, "desc")) {
				rcs->pos = rcs->tok->str;
				break;
			}
			if ((rcs->tok->type & ~TOK_NUM) == 0) {
				rcs->pos = rcs->tok->str;
				break;
			}

			while (opttok(rcs, ';') == 0)
				;
		}

		RB_INSERT(rcsrevtree, &rcs->admin.revs, rev);
		rev = NULL;
	}

	parsetoken(rcs);		/* We *know* it is good */
	if (parsetoken(rcs) == NULL)
		return -1;
	rcs->admin.desc = rcs->tok;
	rcs->tok = NULL;

	rcs->deltapos = rcs->pos;

	/* There are empty RCS files around */
	if (rcs->admin.head == NULL)
		return 0;

	if (parsetoken(rcs) == NULL)
		goto fail;
	if ((rcs->tok->type & ~TOK_NUM) != 0)
		goto fail;
	if (!tokeqtok(rcs->tok, rcs->admin.head))
		goto fail;

	searchrev.rev = rcs->tok;
	rev = RB_FIND(rcsrevtree, &rcs->admin.revs, &searchrev);
	if (rev == NULL)
		goto fail;

	rev->logpos = rcs->pos;

	return 0;

fail:
	if (rev != NULL)
		rcsfreerev(rev);

	return -1;
}

static int
rcsparsetext(struct rcsfile *rcs, struct rcsrev *rrev)
{
	struct rcsrev searchrev;

	if (rrev->log != NULL)
		return 0;

	if (rrev->logpos == NULL)
		return -1;

	rcs->pos = rrev->logpos;

	if (expecttokstr(rcs, "log") < 0)
		return -1;

	if (parsestring(rcs, NULL) == NULL)
		return -1;

	if (rrev->log == NULL) {
		rrev->log = rcs->tok;
		rcs->tok = NULL;
	}

	for (;;) {
		if (parsetoken(rcs) == NULL)
			return -1;
		if (tokeqstr(rcs->tok, "text"))
			break;

		while (opttok(rcs, ';') == 0)
			;
	}

	if (parsestring(rcs, &rrev->rawtext) == NULL)
		return -1;

	if (parsetoken(rcs) == NULL)
		return (0);	/* could be end of file */
	if ((rcs->tok->type & ~TOK_NUM) != 0)
		return -1;

	searchrev.rev = rcs->tok;
	rrev->nextlog = RB_FIND(rcsrevtree, &rcs->admin.revs, &searchrev);
	if (rrev->nextlog == NULL)
		return (-1);

	rrev->nextlog->logpos = rcs->pos;
	return 0;
}

char *
rcscheckout(struct rcsfile *rcs, const char *revstr, size_t *len)
{
	struct rcsrev searchrev;
	struct rcstoken searchtok;
	struct rcsrev *currcsrev, *curtextrev;
	struct stringinfo *curtext;
	struct rcstoken *nextrev;
	char *branchrev, *tmpstr;
	char *retbuf, *rev;

	if (rcsparsetree(rcs) < 0)
		return NULL;

	curtextrev = NULL;
	curtext = NULL;
	nextrev = NULL;
	branchrev = NULL;
	retbuf = NULL;

	rev = rcsrevfromsym(rcs, revstr);
	if (rev == NULL)
		goto fail;

	searchtok.str = rev;
	searchtok.len = strlen(rev);
	searchrev.rev = &searchtok;
	currcsrev = RB_FIND(rcsrevtree, &rcs->admin.revs, &searchrev);
	if (currcsrev == NULL)
		goto fail;

	curtext = currcsrev->text;
	if (curtext != NULL)
		goto done;

	branchrev = strdup(rev);
	if (branchrev == NULL)
		goto fail;
	tmpstr = strchr(branchrev, '.');
	if (tmpstr != NULL)
		tmpstr = strchr(tmpstr + 1, '.');
	if (tmpstr != NULL)
		*tmpstr = '\0';

	searchrev.rev = rcs->admin.head;
	currcsrev = RB_FIND(rcsrevtree, &rcs->admin.revs, &searchrev);
	if (currcsrev == NULL)
		goto fail;

	for (; currcsrev != NULL; currcsrev = currcsrev->nextlog) {
		if (rcsparsetext(rcs, currcsrev) < 0)
			goto fail;

		if (curtext == NULL) {
			curtext = currcsrev->rawtext;
			curtextrev = currcsrev;
		} else {
			if (nextrev == NULL)
				goto fail;

			if (!tokeqtok(currcsrev->rev, nextrev))
				continue;

			if (currcsrev->text) {
				/* Was expanded before */
				if (curtextrev != NULL) {
					free(curtextrev->text);
					curtextrev->text = NULL;
				}
				curtext = currcsrev->text;
				curtextrev = currcsrev;
			} else {
				if (currcsrev->rawtext == NULL)
					goto fail;
				currcsrev->text = copystrnfo(curtext);
				if (currcsrev->text == NULL)
					goto fail;
				if (applydelta(&currcsrev->text, currcsrev->rawtext) < 0)
					goto fail;
				if (curtextrev != NULL) {
					free(curtextrev->text);
					curtextrev->text = NULL;
				}
				curtext = currcsrev->text;
				curtextrev = currcsrev;
			}
		}

		if (tokeqstr(currcsrev->rev, rev))
			break;

		if (tokeqstr(currcsrev->rev, branchrev)) {
			size_t cmplen;

			*tmpstr = '.';
			tmpstr = strchr(tmpstr + 1, '.');
			if (tmpstr != NULL)
				cmplen = tmpstr - branchrev + 1;
			else
				cmplen = strlen(branchrev) + 1;

			SLIST_FOREACH(nextrev, &currcsrev->branches, link)
				if (tokeqstrn(nextrev, branchrev, cmplen))
					break;

			if (tmpstr != NULL) {
				tmpstr = strchr(tmpstr + 1, '.');
				if (tmpstr != NULL)
					*tmpstr = '\0';
			}
		} else {
			nextrev = currcsrev->next;
		}
	}

	if (currcsrev == NULL)
		goto fail;

done:
	if (tokeqstr(currcsrev->state, "dead")) {
		/* TODO: optimize this case */
		retbuf = strdup("");
		if (len != NULL)
			*len = 0;
	} else {
		retbuf = strnfo2txtbuf(curtext, len);
	}

fail:
	if (rev != NULL)
		free(rev);

	free(branchrev);
	return retbuf;
}

char *
rcsrevfromsym(struct rcsfile *rcs, const char *sym)
{
	struct rcsrev findrev, *rev;
	struct rcstokpair findpair, *pair;
	struct rcstoken findtok, branchtok, *tok;
	char *pos, *endpos;
	char *lastdot, *last2dot;
	char *retrev;
	size_t dotcount;
	int issym, searchbranch;

	/* To check head we only need admin info */
	if (rcsparseadmin(rcs) < 0)
		return NULL;

	/* Handle special symbol "HEAD" */
	if (sym == NULL || strcmp(sym, "HEAD") == 0) {
		if (rcs->admin.branch == NULL) {
			tok = rcs->admin.head;
			goto found;
		} else {
			findtok = *rcs->admin.branch;
		}
	} else {
		findtok.str = (char *)(unsigned long)sym;
		findtok.len = strlen(sym);
	}

	/* We really need to wade in the revs, so parse them as well */
	if (rcsparsetree(rcs) < 0)
		return NULL;

	dotcount = 0;
	issym = 0;
	lastdot = last2dot = NULL;
	for (pos = findtok.str, endpos = pos + findtok.len; pos < endpos; pos++) {
		if (*pos == '.') {
			/* Two adjacent dots are invalid */
			if (pos == lastdot + 1)
				return NULL;
			dotcount++;
			last2dot = lastdot;
			lastdot = pos;
		} else if (!isdigit(*pos)) {
			issym = 1;
		}
	}

	if (issym && dotcount > 0)
		return NULL;
	if (*findtok.str == '.' || findtok.len == 0 || findtok.str[findtok.len - 1] == '.')
		return NULL;

	if (issym) {
		findpair.first = &findtok;
		pair = RB_FIND(rcstokmap, &rcs->admin.symbols, &findpair);
		if (pair == NULL)
			return NULL;

		findtok = *pair->second;
		for (pos = findtok.str, endpos = pos + findtok.len; pos < endpos; pos++) {
			if (*pos == '.') {
				/* Two adjacent dots are invalid */
				if (pos == lastdot + 1)
					return NULL;
				dotcount++;
				last2dot = lastdot;
				lastdot = pos;
			} else if (!isdigit(*pos)) {
				return NULL;
			}
		}
	}

	searchbranch = 0;
	if (dotcount == 0) {
		branchtok = findtok;
		findtok = *rcs->admin.head;
	} else if (dotcount % 2 == 0 ||
	    (last2dot != NULL && lastdot - last2dot == 2 && *(last2dot + 1) == '0')) {
		/*
		 * We are explicitly searching for a branch or
		 * seeking a magic branch.
		 */
		branchtok.str = lastdot + 1;
		branchtok.len = findtok.str + findtok.len - branchtok.str;
		if (dotcount % 2 == 0)
			findtok.len = lastdot - findtok.str;
		else
			findtok.len = last2dot - findtok.str;

		searchbranch = 1;
	}

	findrev.rev = &findtok;
	rev = RB_FIND(rcsrevtree, &rcs->admin.revs, &findrev);
	if (rev == NULL)
		return NULL;

	if (searchbranch) {
		struct rcsrev *nextrev;
		char *branchstr;
		size_t branchlen;

		/* First locate the right branch, then climb up */
		branchlen = rev->rev->len + branchtok.len + 3;
		branchstr = malloc(branchlen);
		if (branchstr == NULL)
			return NULL;

		bcopy(rev->rev->str, branchstr, rev->rev->len);
		branchstr[rev->rev->len] = '.';
		bcopy(branchtok.str, branchstr + rev->rev->len + 1, branchtok.len);
		branchstr[branchlen - 2] = '.';
		branchstr[branchlen - 1] = '\0';

		SLIST_FOREACH(tok, &rev->branches, link)
		    if (tokeqstrn(tok, branchstr, branchlen - 1))
			    break;

		free(branchstr);

		findrev.rev = tok;
		while (findrev.rev != NULL &&
		    (nextrev = RB_FIND(rcsrevtree, &rcs->admin.revs, &findrev)) != NULL) {
			rev = nextrev;
			findrev.rev = rev->next;
		}
	}

	if (dotcount == 0) {
		for (;;) {
			if (rev->rev->len > branchtok.len + 1 &&
			    memcmp(rev->rev->str, branchtok.str, branchtok.len) == 0 &&
			    rev->rev->str[branchtok.len] == '.')
				break;

			if (rev->next == NULL)
				return NULL;

			findrev.rev = rev->next;
			rev = RB_FIND(rcsrevtree, &rcs->admin.revs, &findrev);
			if (rev == NULL)
				return NULL;
		}
	}
	tok = rev->rev;

found:
	retrev = malloc(tok->len + 1);
	if (retrev == NULL)
		return NULL;
	bcopy(tok->str, retrev, tok->len);
	retrev[tok->len] = '\0';

	return retrev;
}

char *
rcsgetlog(struct rcsfile *rcs, const char *logrev)
{
	struct rcstoken findtok;
	struct rcsrev findrev, *rev;

	if (rcsparsetree(rcs) < 0)
		return NULL;

	findtok.str = (char *)(long)logrev;
	findtok.len = strlen(logrev);
	findrev.rev = &findtok;

	rev = RB_FIND(rcsrevtree, &rcs->admin.revs, &findrev);
	if (rev == NULL)
		return NULL;

	if (rev->log != NULL)
		goto done;

	findrev.rev = rcs->admin.head;
	rev = RB_FIND(rcsrevtree, &rcs->admin.revs, &findrev);

	for (; rev != NULL; rev = rev->nextlog) {
		if (rcsparsetext(rcs, rev) < 0)
			return NULL;

		if (tokeqstr(rev->rev, logrev))
			break;
	}

	if (rev == NULL)
		return NULL;

done:
	return tokstripat(rev->log);
}

struct rcsfile *
rcsopen(const char *filename)
{
	struct stat st;
	struct rcsfile *rcs;

	rcs = calloc(1, sizeof(struct rcsfile));
	if (rcs == NULL)
		goto fail;

	rcs->file = open(filename, O_RDONLY);
	if (rcs->file < 0)
		goto fail;

	if (fstat(rcs->file, &st) < 0)
		goto fail;

	rcs->size = st.st_size;

	rcs->data = mmap(NULL, rcs->size, PROT_READ, MAP_PRIVATE, rcs->file, 0);
	if (rcs->data == MAP_FAILED)
		goto fail;

	rcs->end = rcs->data + rcs->size;
	rcs->pos = rcs->data;

	SLIST_INIT(&rcs->admin.access);
	RB_INIT(&rcs->admin.symbols);
	RB_INIT(&rcs->admin.locks);
	RB_INIT(&rcs->admin.revs);

	return rcs;

fail:
	if (rcs != NULL && rcs->file >= 0)
		close(rcs->file);
	if (rcs != NULL)
		free(rcs);

	return NULL;
}

void
rcsclose(struct rcsfile *rcs)
{
	struct rcstoken *tok;
	struct rcstokpair *pair;
	struct rcsrev *rev;

	if (rcs->tok != NULL) {
		free(rcs->tok);

		if (rcs->lasttok != NULL && rcs->lasttok != rcs->tok)
			free(rcs->lasttok);
	}

	if (rcs->admin.head != NULL)
		free(rcs->admin.head);
	if (rcs->admin.branch != NULL)
		free(rcs->admin.branch);
	while ((tok = SLIST_FIRST(&rcs->admin.access)) != NULL) {
		SLIST_REMOVE_HEAD(&rcs->admin.access, link);
		free(tok);
	}
	if (rcs->admin.comment != NULL)
		free(rcs->admin.comment);
	if (rcs->admin.expand != NULL)
		free(rcs->admin.expand);
	if (rcs->admin.desc != NULL)
		free(rcs->admin.desc);

	while ((pair = RB_MIN(rcstokmap, &rcs->admin.symbols)) != NULL) {
		RB_REMOVE(rcstokmap, &rcs->admin.symbols, pair);
		free(pair->first);
		free(pair->second);
		free(pair);
	}

	while ((pair = RB_MIN(rcstokmap, &rcs->admin.locks)) != NULL) {
		RB_REMOVE(rcstokmap, &rcs->admin.locks, pair);
		free(pair->first);
		free(pair->second);
		free(pair);
	}

	while ((rev = RB_MIN(rcsrevtree, &rcs->admin.revs)) != NULL) {
		RB_REMOVE(rcsrevtree, &rcs->admin.revs, rev);
		rcsfreerev(rev);
	}

	munmap(rcs->data, rcs->size);
	close(rcs->file);
	free(rcs);
}

#ifdef TESTING
int
main(int argc, char **argv)
{
	struct rcsfile *rcs;
	char *buf, *rev, *log;
	int i;
	size_t len;

	if (argc < 3)
		errx(1, "invalid arguments");

	rcs = rcsopen(argv[1]);
	if (rcs == NULL)
		return 1;

	if (rcsparseadmin(rcs) < 0)
		return 1;
	if (rcsparsetree(rcs) < 0)
		return 2;

	for (i = 2; i < argc; i++) {
		if (strcmp(argv[i], "all") == 0) {
			struct rcsrev *rrev;

			RB_FOREACH(rrev, rcsrevtree, &rcs->admin.revs) {
				rev = malloc(rrev->rev->len + 1);
				memcpy(rev, rrev->rev->str, rrev->rev->len);
				rev[rrev->rev->len] = 0;
				log = rcsgetlog(rcs, rev);
				free(log);
				buf = rcscheckout(rcs, rev, &len);
				free(buf);
				free(rev);
			}
		} else {
			rev = rcsrevfromsym(rcs, argv[i]);
			if (rev == NULL)
				return 3;

			log = rcsgetlog(rcs, rev);
			if (log == NULL)
				return 5;
			printf("%s\n", log);
			free(log);

			buf = rcscheckout(rcs, rev, &len);
			if (buf == NULL)
				return 4;
			fwrite(buf, 1, len, stdout);
			free(buf);
			free(rev);
		}
	}

	rcsclose(rcs);

	return 0;
}
#endif
