/* Copyright 2011,2012,2013 Bernhard R. Link <brlink@debian.org>
 *
 * 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
 * SOFTWARE IN THE PUBLIC INTEREST, INC. 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 "config.h"

#include <sys/stat.h>
#include <sys/types.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <stdbool.h>
#include <stdint.h>
#include <string.h>
#include <assert.h>
#include <getopt.h>
#include <string.h>
#include <endian.h>

#include "globals.h"
#define EFF_INTERNAL 1
#include "eff.h"

struct statetable {
	int count;
	struct statematrix {
		unsigned int next[256];
	} transits[];
};

static int variablecount = 0;
uint8_t *variables = NULL;

static int goalcount = 0;
static struct item_to_search {
	char *name;
	unsigned char extractstart, extractstartchar, extractresetchar, extractend, extractendchar;
	bool merge;
	int mergeid1, mergeid2, mergeid3;
	int setswitch, condition;
	uint8_t setswitchvalue, conditionmin, conditionmax;
	struct found {
		struct found *next;
		size_t extralen;
		char *extradata;
		size_t count;
		long lineno;
	} *found;
} *items = NULL;

static struct statetable *table = NULL;

static bool read_item(struct eff *f, struct item_to_search *i) {
	const char *t;

	memset(i, 0, sizeof(*i));
	eff_fixedstart(f, "goal");
	t = eff_start(f);
	if (t != NULL && strcmp(t, "name") == 0) {
		i->name = eff_getstring(f);
		eff_end(f);
		t = eff_start(f);
	} else {
		i->name = strdup("");
		if (i->name == NULL)
			oom();
	}
	if (t != NULL && strcmp(t, "extract:simple") == 0) {
		i->extractstart = eff_getuint8(f);
		i->extractend = eff_getuint8(f);
		i->extractresetchar = eff_getuint8(f);
		i->extractstartchar = eff_getuint8(f);
		i->extractendchar = eff_getuint8(f);
		eff_end(f);
		t = eff_start(f);
	}
	if (t != NULL && strcmp(t, "merge:same") == 0) {
		i->merge = true;
		i->mergeid1 = eff_getuint32(f);
		i->mergeid2 = eff_getuint32(f);
		i->mergeid3 = eff_getuint32(f);
		eff_end(f);
		t = eff_start(f);
	}
	if (t != NULL && strcmp(t, "set") == 0) {
		i->setswitch = eff_getuint32(f);
		i->setswitchvalue = eff_getuint8(f);
		eff_end(f);
		t = eff_start(f);
	}
	if (i->setswitch < 0 && i->setswitch > variablecount)
		fail("Inconsistent data");
	if (t != NULL && strcmp(t, "condition") == 0) {
		i->condition = eff_getuint32(f);
		i->conditionmin = eff_getuint8(f);
		i->conditionmax = eff_getuint8(f);
		eff_end(f);
		t = eff_start(f);
	}
	if (i->condition < 0 && i->condition > variablecount)
		fail("Inconsistent data");
	if (t != NULL)
		fail("Not yet supported attributed in rules file!");
	return true;
}

static struct statetable *read_statetable(struct eff *f, int goals) {
	struct statetable *t;
	uint32_t u;
	size_t runcount;
	unsigned char buffer[6];
	int i, j, lastc, nextstate;

	u = eff_getuint32(f);
	t = zNEWvarstruct(1 + goals + u, struct statetable, transits);
	t->count = u + goals + 1;
	/* First one absorbing state for every goal */
	for (i = 0 ; i < goals ; i++) {
		for (j = 0 ; j <= 255 ; j++)
			t->transits[i].next[j] = i;
	}
	/* Then one state for not having found anything: */
	for (j = 0 ; j <= 255 ; j++)
		t->transits[goals].next[j] = goals;

	/* the rest is from the table: */
	for (i = 1 + goals ; i < t->count ; i++) {
		/* anything not given is a failed state: */
		for (j = 0 ; j <= 255 ; j++)
			t->transits[i].next[j] = goals;
		eff_openarray(f, 6, &runcount);
		lastc = -1;
		while (runcount-- > 0) {
			eff_getelement(f, sizeof(buffer), buffer);
			if (buffer[0] > buffer[1] || (int)buffer[0] <= lastc)
				fail("Malformed state table!");
			memcpy(&u, buffer + 2, 4);
			u = u32trans(u);
			if ((u & 0x80000000ul) != 0 || u >= t->count)
				fail("broken state tabele targets");
			nextstate = u;
			for (j = buffer[0] ; j <= buffer[1] ; j++) {
				t->transits[i].next[j] = nextstate;
			}
			lastc = buffer[1];
		}
		eff_closearray(f);
	}
	return t;
}

static bool read_rules(struct eff *f) {
	uint32_t i;
	const char *type;

	type = eff_start(f);

	variablecount = 0;
	if (strcmp(type, "variable_count") == 0) {
		variablecount = eff_getuint32(f);
		eff_end(f);
		type = eff_start(f);
	}
	variables = zNEW(variablecount+2, uint8_t);

	if (strcmp(type, "goals") != 0) {
		fail("Unsupported rules format");
	}
	goalcount = eff_getuint32(f);
	items = zNEW(goalcount + 1, struct item_to_search);
	for (i = 0 ; i < goalcount ; i++) {
		if (!read_item(f, items + i))
			return false;
	}
	eff_end(f);
	type = eff_start(f);
	if (strcmp(type, "states") != 0) {
		fail("Unsupported rules format (not yet known data: '%s')", type);
	}
	table = read_statetable(f, goalcount);
	eff_end(f);
	type = eff_start(f);
	if (type != NULL) {
		fail("Unsupported rules format (not yet known data: '%s')", type);
	}
	return true;
}

static void read_rules_file(const char *filename) {
	struct eff *f;

	f = eff_open(filename, "builocheck");
	if (!read_rules(f))
		fail("Cannot parse rules file '%s'", filename);
	eff_close(f);
}

static inline int lenstrcmp(size_t alen, const char *a, size_t blen, const char *b) {
	if (alen < blen)
		return -1;
	if (alen > blen)
		return 1;
	if (alen == 0)
		return 0;
	return memcmp(a, b, alen);
}

static void found(int goal, unsigned char *line, size_t linelen, long lineno) {
	char *extractstart = NULL;
	int extractlen = 0;
	struct item_to_search *founditem = &items[goal];
	struct found **f_p, *n;

	line[linelen] = '\0';

	if (founditem->condition != 0) {
		/* ignore if variable condition does not match: */
		uint8_t var = variables[founditem->condition];
		if (var < founditem->conditionmin
				|| var > founditem->conditionmax)
			return;
	}

	if (founditem->extractend > 0) {
		int todo = founditem->extractstart;
		extractstart = (char*)line;
		while (todo > 0) {
			while (*extractstart != '\0' &&
					*extractstart != founditem->extractstartchar)
				extractstart++;
			if (*extractstart != '\0')
				extractstart++;
			todo--;
		}
		todo = founditem->extractend;
		while (todo > 0) {
			while (extractstart[extractlen] != '\0' && extractstart[extractlen] != founditem->extractendchar) {
				if (founditem->extractresetchar != '\0' && extractstart[extractlen] == founditem->extractresetchar) {
					extractstart += extractlen + 1;
					extractlen = 0;
					continue;
				}
				if (strchr("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-.:,;#'+*~?=!^/ ", extractstart[extractlen]) == NULL)
					extractstart[extractlen] = '?';
				extractlen++;
			}
			if (-- todo > 0 && extractstart[extractlen] != '\0')
				extractlen++;
		}
		char *s;
		/* remove "^buildd-[^/]*\/" and ".*\/buildd-[^/]*/
		if (extractlen > 7 && memcmp(extractstart, "buildd-", 7) == 0) {
			s = extractstart;
			while (s - extractstart < extractlen && *s != '/')
				s++;
			if (s - extractstart < extractlen) {
				s++;
				extractlen -= (s - extractstart);
				extractstart = s;
			}
		} else if (extractlen > 9 && (s = strstr(extractstart, "/buildd-")) != NULL && s < extractstart + extractlen) {
			s++;
			while (s - extractstart < extractlen && *s != '/')
				s++;
			if (s - extractstart < extractlen) {
				s++;
				extractlen -= (s - extractstart);
				extractstart = s;
			}
		}
	}

	if (founditem->setswitch != 0)
		variables[founditem->setswitch] = founditem->setswitchvalue;

	for (f_p = &founditem->found ; *f_p != NULL ; f_p = &(*f_p)->next) {
		int i;

		i = lenstrcmp(extractlen, extractstart,
				(*f_p)->extralen, (*f_p)->extradata);
		if (i < 0)
			break;
		if (i > 0)
			continue;
		if (i == 0) {
			(*f_p)->count++;
			return;
		}
	}
	n = zNEW(1, struct found);
	n->lineno = lineno;
	n->extralen = extractlen;
	if (extractlen != 0) {
		n->extradata = NEW(extractlen + 1, char);
		memcpy(n->extradata, extractstart, extractlen);
		n->extradata[extractlen] = '\0';
	}
	n->next = *f_p;
	*f_p = n;
}

#define STARTSTATE goalcount + 1
#define FAILEDSTATE goalcount
#define ISGOALSTATE(state) (state < goalcount)

static void scanfile(int fd, const char *filename) {
	int state = STARTSTATE;
	static unsigned char buffer[102400];
	ssize_t got, i;
	size_t lastlinestart, filleduntil;
	long lineno = 0;

	lastlinestart = 0;
	filleduntil = 0;
	do {
		if (filleduntil > lastlinestart) {
			if (filleduntil - lastlinestart >= sizeof(buffer) / 2) {
				/* ridicilous long line, ignoring */
				state = FAILEDSTATE;
				filleduntil = 0;
				lastlinestart = 0;
			} else {
				filleduntil = filleduntil - lastlinestart;
				memmove(buffer, buffer + lastlinestart, filleduntil);
				lastlinestart = 0;
			}
		} else {
			filleduntil = 0;
			lastlinestart = 0;
		}

		got = read(fd, buffer + filleduntil,
				((size_t)(sizeof(buffer) - filleduntil))&~(size_t)1023);
		if (got < 0 && errno == EINTR) {
			errno = 0;
			continue;
		}
		if (got <= 0)
			break;
		i = filleduntil;
		filleduntil += got;
		for ( ; i < filleduntil ; i++) {
			state = table->transits[state].next[buffer[i]];
			if (__builtin_expect(buffer[i] == '\n', 0)) {
				lineno++;
				if (__builtin_expect(ISGOALSTATE(state), 0)) {
					assert (state >= 0);

					found(state, buffer + lastlinestart,
							i - lastlinestart,
							lineno);
					state = 0;
				}
				state = STARTSTATE;
				lastlinestart = i+1;
			}
		}
	} while (got > 0);
}

static void process_results(void) {
	struct item_to_search *i;

	/* merge tags with same data where requested */
	for (i = items ; i->name != NULL ; i++) {
		struct found *a, *b, *c, **a_p, **b_p, **c_p, **r_p;
		if (!i->merge)
			continue;
		if (i->mergeid1 < 0 || i->mergeid1 >= goalcount)
			continue;
		if (i->mergeid2 < 0 || i->mergeid2 >= goalcount)
			continue;
		if (i->mergeid3 >= goalcount)
			continue;
		r_p = &i->found;
		while (*r_p != NULL)
			r_p = &(*r_p)->next;
		a_p = &items[i->mergeid1].found;
		b_p = &items[i->mergeid2].found;
		if (i->mergeid3 < 0)
			c_p = NULL;
		else
			c_p = &items[i->mergeid3].found;
		while ((a = *a_p) != NULL && ((*b_p) != NULL ||
				(c_p == NULL || (*c_p) != NULL))) {
			int i, j;

			b = *b_p;
			if (b != NULL)
				i = lenstrcmp(a->extralen, a->extradata,
					b->extralen, b->extradata);
			else
				i = -1;
			if (c_p != NULL) {
				c = *c_p;
				if (c != NULL)
					j = lenstrcmp(a->extralen, a->extradata,
							c->extralen, c->extradata);
				else
					j = -1;
			} else {
				j = -1;
				c = NULL; /* only against compiler warnings */
			}
			if (i < 0 && j < 0) {
				a_p = &a->next;
			} else if (i > 0) {
				b_p = &b->next;
				if (j > 0)
					c_p = &c->next;
			} else if (j > 0) {
				c_p = &c->next;
			} else {
				*r_p = a;
				*a_p = a->next;
				if (i == 0) {
					*b_p = b->next;
					free(b->extradata);
					free(b);
				}
				if (j == 0) {
					*c_p = c->next;
					free(c->extradata);
					free(c);
				}
				a->next = NULL;
				r_p = &a->next;
			}
		}
	}
	for (i = items ; i->name != NULL ; i++) {
		struct found *f;

		if (i->name[0] == '\0')
			continue;

		for (f = i->found ; f != NULL; f = f->next) {
			printf("%s|%s|%ld\n", i->name, f->extralen?f->extradata:"",
					f->lineno);
		}
	}
}

int main(int argc, char * const argv[]) {
	const char *rulesfile = "builddlogcheck.rules";
	const char *filename;
	int fd;
	int opt;
	static const struct option long_options[] = {
		{"help", no_argument, NULL, 'h'},
		{"rules", required_argument, NULL, 'r'},
		{0, 0, 0, 0}
	};

	while ((opt = getopt_long(argc, argv, "+hr:", long_options, NULL)) != -1) {
		switch (opt) {
			case 'h':
				printf(
"builocheck-scan: scan a file for patterns using a precompiled state machine\n"
"Syntax: %s [<options>] <file to scan>\n"
"Options:\n"
"	--help: show this help\n"
"	--rules <rulefile>: file to contain the precompiled state machine\n",
					argv[0]);
				exit(EXIT_SUCCESS);
			case 'r':
				rulesfile = optarg;
				break;
			case '?':
				fail("Unknown command line options");
		}
	}

	read_rules_file(rulesfile);

	if (optind >= argc) {
		fprintf(stderr, "No file to scan given! (missing command line arguments)\n");
		return EXIT_FAILURE;
	}
	if (optind + 1 < argc) {
		fprintf(stderr, "Too many arguments!\n");
		return EXIT_FAILURE;
	}
	filename = argv[optind];
	if (strcmp(filename, "-") != 0)
		fd = open(argv[optind], O_RDONLY);
	else
		fd = 1;
	if (fd < 0) {
		perror("Error opening file to scan: ");
		return 1;
	}
	scanfile(fd, filename);
	process_results();
	return 0;
}
