// SPDX-License-Identifier: LGPL-2.1
/*
 * Copyright (C) 2021 VMware Inc, Steven Rostedt <rostedt@goodmis.org>
 *
 * Updates:
 * Copyright (C) 2021, VMware, Tzvetomir Stoyanov <tz.stoyanov@gmail.com>
 *
 */
#include <trace-seq.h>
#include <stdlib.h>
#include <ctype.h>
#include <errno.h>

#include "tracefs.h"
#include "tracefs-local.h"

enum {
	S_START,
	S_COMPARE,
	S_NOT,
	S_CONJUNCTION,
	S_OPEN_PAREN,
	S_CLOSE_PAREN,
};

static const struct tep_format_field common_timestamp = {
	.type			= "u64",
	.name			= "common_timestamp",
	.size			= 8,
};

static const struct tep_format_field common_timestamp_usecs = {
	.type			= "u64",
	.name			= "common_timestamp.usecs",
	.size			= 8,
};

/*
 * This also must be able to accept fields that are OK via the histograms,
 * such as common_timestamp.
 */
static const struct tep_format_field *get_event_field(struct tep_event *event,
					 const char *field_name)
{
	if (!strcmp(field_name, TRACEFS_TIMESTAMP))
		return &common_timestamp;

	if (!strcmp(field_name, TRACEFS_TIMESTAMP_USECS))
		return &common_timestamp_usecs;

	return tep_find_any_field(event, field_name);
}

__hidden bool
trace_verify_event_field(struct tep_event *event,
			 const char *field_name,
			 const struct tep_format_field **ptr_field)
{
	const struct tep_format_field *field;

	field = get_event_field(event, field_name);
	if (!field) {
		errno = ENODEV;
		return false;
	}

	if (ptr_field)
		*ptr_field = field;

	return true;
}

__hidden int trace_test_state(int state)
{
	switch (state) {
	case S_START:
	case S_CLOSE_PAREN:
	case S_COMPARE:
		return 0;
	}

	errno = EBADE;
	return -1;
}

static int append_filter(char **filter, unsigned int *state,
			 unsigned int *open_parens,
			 struct tep_event *event,
			 enum tracefs_filter type,
			 const char *field_name,
			 enum tracefs_compare compare,
			 const char *val)
{
	const struct tep_format_field *field;
	bool is_string;
	char *conj = "||";
	char *tmp;

	switch (type) {
	case TRACEFS_FILTER_COMPARE:
		switch (*state) {
		case S_START:
		case S_OPEN_PAREN:
		case S_CONJUNCTION:
		case S_NOT:
			break;
		default:
			goto inval;
		}
		break;

	case TRACEFS_FILTER_AND:
		conj = "&&";
		/* Fall through */
	case TRACEFS_FILTER_OR:
		switch (*state) {
		case S_COMPARE:
		case S_CLOSE_PAREN:
			break;
		default:
			goto inval;
		}
		/* Don't lose old filter on failure */
		tmp = strdup(*filter);
		if (!tmp)
			return -1;
		tmp = append_string(tmp, NULL, conj);
		if (!tmp)
			return -1;
		free(*filter);
		*filter = tmp;
		*state = S_CONJUNCTION;
		return 0;

	case TRACEFS_FILTER_NOT:
		switch (*state) {
		case S_START:
		case S_OPEN_PAREN:
		case S_CONJUNCTION:
		case S_NOT:
			break;
		default:
			goto inval;
		}
		if (*filter) {
			tmp = strdup(*filter);
			tmp = append_string(tmp, NULL, "!");
		} else {
			tmp = strdup("!");
		}
		if (!tmp)
			return -1;
		free(*filter);
		*filter = tmp;
		*state = S_NOT;
		return 0;

	case TRACEFS_FILTER_OPEN_PAREN:
		switch (*state) {
		case S_START:
		case S_OPEN_PAREN:
		case S_NOT:
		case S_CONJUNCTION:
			break;
		default:
			goto inval;
		}
		if (*filter) {
			tmp = strdup(*filter);
			tmp = append_string(tmp, NULL, "(");
		} else {
			tmp = strdup("(");
		}
		if (!tmp)
			return -1;
		free(*filter);
		*filter = tmp;
		*state = S_OPEN_PAREN;
		(*open_parens)++;
		return 0;

	case TRACEFS_FILTER_CLOSE_PAREN:
		switch (*state) {
		case S_CLOSE_PAREN:
		case S_COMPARE:
			break;
		default:
			goto inval;
		}
		if (!*open_parens)
			goto inval;

		tmp = strdup(*filter);
		if (!tmp)
			return -1;
		tmp = append_string(tmp, NULL, ")");
		if (!tmp)
			return -1;
		free(*filter);
		*filter = tmp;
		*state = S_CLOSE_PAREN;
		(*open_parens)--;
		return 0;
	}

	if (!field_name || !val)
		goto inval;

	if (!trace_verify_event_field(event, field_name, &field))
		return -1;

	is_string = field->flags & TEP_FIELD_IS_STRING;

	if (!is_string && (field->flags & TEP_FIELD_IS_ARRAY))
		goto inval;

	if (*filter) {
		tmp = strdup(*filter);
		if (!tmp)
			return -1;
		tmp = append_string(tmp, NULL, field_name);
	} else {
		tmp = strdup(field_name);
	}

	switch (compare) {
	case TRACEFS_COMPARE_EQ: tmp = append_string(tmp, NULL, " == "); break;
	case TRACEFS_COMPARE_NE: tmp = append_string(tmp, NULL, " != "); break;
	case TRACEFS_COMPARE_RE:
		if (!is_string)
			goto inval;
		tmp = append_string(tmp, NULL, "~");
		break;
	default:
		if (is_string)
			goto inval;
	}

	switch (compare) {
	case TRACEFS_COMPARE_GT: tmp = append_string(tmp, NULL, " > "); break;
	case TRACEFS_COMPARE_GE: tmp = append_string(tmp, NULL, " >= "); break;
	case TRACEFS_COMPARE_LT: tmp = append_string(tmp, NULL, " < "); break;
	case TRACEFS_COMPARE_LE: tmp = append_string(tmp, NULL, " <= "); break;
	case TRACEFS_COMPARE_AND: tmp = append_string(tmp, NULL, " & "); break;
	default: break;
	}

	tmp = append_string(tmp, NULL, val);

	if (!tmp)
		return -1;

	free(*filter);
	*filter = tmp;
	*state = S_COMPARE;

	return 0;
inval:
	errno = EINVAL;
	return -1;
}

static int count_parens(char *filter, unsigned int *state)
{
	bool backslash = false;
	int quote = 0;
	int open = 0;
	int i;

	if (!filter)
		return 0;

	for (i = 0; filter[i]; i++) {
		if (quote) {
			if (backslash)
				backslash = false;
			else if (filter[i] == '\\')
				backslash = true;
			else if (quote == filter[i])
				quote = 0;
			continue;
		}

		switch (filter[i]) {
		case '(':
			*state = S_OPEN_PAREN;
			open++;
			break;
		case ')':
			*state = S_CLOSE_PAREN;
			open--;
			break;
		case '\'':
		case '"':
			*state = S_COMPARE;
			quote = filter[i];
			break;
		case '!':
			switch (filter[i+1]) {
			case '=':
			case '~':
				*state = S_COMPARE;
				i++;
				break;
			default:
				*state = S_NOT;
			}
			break;
		case '&':
		case '|':
			if (filter[i] == filter[i+1]) {
				*state = S_CONJUNCTION;
				i++;
				break;
			}
			/* Fall through */
		case '0' ... '9':
		case 'a' ... 'z':
		case 'A' ... 'Z':
		case '_': case '+': case '-': case '*': case '/':
			*state = S_COMPARE;
			break;
		}
	}
	return open;
}

__hidden int trace_append_filter(char **filter, unsigned int *state,
			 unsigned int *open_parens,
			 struct tep_event *event,
			 enum tracefs_filter type,
			 const char *field_name,
			 enum tracefs_compare compare,
			 const char *val)
{
	return append_filter(filter, state, open_parens, event, type,
			     field_name, compare, val);
}

/**
 * tracefs_filter_string_append - create or append a filter for an event
 * @event: tep_event to create / append a filter for
 * @filter: Pointer to string to append to (pointer to NULL to create)
 * @type: The type of element to add to the filter
 * @field: For @type == TRACEFS_FILTER_COMPARE, the field to compare
 * @compare: For @type == TRACEFS_FILTER_COMPARE, how to compare @field to @val
 * @val: For @type == TRACEFS_FILTER_COMPARE, what value @field is to be
 *
 * This will put together a filter string for the starting event
 * of @synth. It check to make sure that what is added is correct compared
 * to the filter that is already built.
 *
 * @type can be:
 *     TRACEFS_FILTER_COMPARE:        See below
 *     TRACEFS_FILTER_AND:            Append "&&" to the filter
 *     TRACEFS_FILTER_OR:             Append "||" to the filter
 *     TRACEFS_FILTER_NOT:            Append "!" to the filter
 *     TRACEFS_FILTER_OPEN_PAREN:     Append "(" to the filter
 *     TRACEFS_FILTER_CLOSE_PAREN:    Append ")" to the filter
 *
 * For all types except TRACEFS_FILTER_COMPARE, the @field, @compare,
 * and @val are ignored.
 *
 * For @type == TRACEFS_FILTER_COMPARE.
 *
 *  @field is the name of the field for the start event to compare.
 *         If it is not a field for the start event, this return an
 *         error.
 *
 *  @compare can be one of:
 *     TRACEFS_COMPARE_EQ:       Test @field == @val
 *     TRACEFS_COMPARE_NE:       Test @field != @val
 *     TRACEFS_COMPARE_GT:       Test @field > @val
 *     TRACEFS_COMPARE_GE:       Test @field >= @val
 *     TRACEFS_COMPARE_LT:       Test @field < @val
 *     TRACEFS_COMPARE_LE:       Test @field <= @val
 *     TRACEFS_COMPARE_RE:       Test @field ~ @val
 *     TRACEFS_COMPARE_AND:      Test @field & @val
 *
 * If the @field is of type string, and @compare is not
 *   TRACEFS_COMPARE_EQ, TRACEFS_COMPARE_NE or TRACEFS_COMPARE_RE,
 *   then this will return an error.
 *
 * Various other checks are made, for instance, if more CLOSE_PARENs
 * are added than existing OPEN_PARENs. Or if AND is added after an
 * OPEN_PAREN or another AND or an OR or a NOT.
 *
 * Returns 0 on success and -1 on failure.
 */
int tracefs_filter_string_append(struct tep_event *event, char **filter,
				 enum tracefs_filter type,
				 const char *field, enum tracefs_compare compare,
				 const char *val)
{
	unsigned int open_parens;
	unsigned int state = 0;
	char *str = NULL;
	int open;
	int ret;

	if (!filter) {
		errno = EINVAL;
		return -1;
	}

	open = count_parens(*filter, &state);
	if (open < 0) {
		errno = EINVAL;
		return -1;
	}

	if (*filter) {
		/* append_filter() will free filter on error */
		str = strdup(*filter);
		if (!str)
			return -1;
	}
	open_parens = open;

	ret = append_filter(&str, &state, &open_parens,
			    event, type, field, compare, val);
	if (!ret) {
		free(*filter);
		*filter = str;
	}
	return 0;
}

static int error_msg(char **err, char *str,
		     const char *filter, int i, const char *msg)
{
	char ws[i+2];
	char *errmsg;

	free(str);

	/* msg is NULL for parsing append_filter failing */
	if (!msg) {
		switch(errno) {
		case ENODEV:
			msg = "field not valid";
			break;
		default:
			msg = "Invalid filter";

		}
	} else
		errno = EINVAL;

	if (!err)
		return -1;

	if (!filter) {
		*err = strdup(msg);
		return -1;
	}

	memset(ws, ' ', i);
	ws[i] = '^';
	ws[i+1] = '\0';

	errmsg = strdup(filter);
	errmsg = append_string(errmsg, "\n", ws);
	errmsg = append_string(errmsg, "\n", msg);
	errmsg = append_string(errmsg, NULL, "\n");

	*err = errmsg;
	return -1;
}

static int get_field_end(const char *filter, int i, int *end)
{
	int start_i = i;

	for (; filter[i]; i++) {
		switch(filter[i]) {
		case '0' ... '9':
			if (i == start_i)
				return 0;
			/* Fall through */
		case 'a' ... 'z':
		case 'A' ... 'Z':
		case '_':
			continue;
		default:
			*end = i;
			return i - start_i;
		}
	}
	*end = i;
	return i - start_i;
}

static int get_compare(const char *filter, int i, enum tracefs_compare *cmp)
{
	int start_i = i;

	for (; filter[i]; i++) {
		if (!isspace(filter[i]))
			break;
	}

	switch(filter[i]) {
	case '=':
		if (filter[i+1] != '=')
			goto err;
		*cmp = TRACEFS_COMPARE_EQ;
		i++;
		break;
	case '!':
		if (filter[i+1] == '=') {
			*cmp = TRACEFS_COMPARE_NE;
			i++;
			break;
		}
		if (filter[i+1] == '~') {
			/* todo! */
		}
		goto err;
	case '>':
		if (filter[i+1] == '=') {
			*cmp = TRACEFS_COMPARE_GE;
			i++;
			break;
		}
		*cmp = TRACEFS_COMPARE_GT;
		break;
	case '<':
		if (filter[i+1] == '=') {
			*cmp = TRACEFS_COMPARE_LE;
			i++;
			break;
		}
		*cmp = TRACEFS_COMPARE_LT;
		break;
	case '~':
		*cmp = TRACEFS_COMPARE_RE;
		break;
	case '&':
		*cmp = TRACEFS_COMPARE_AND;
		break;
	default:
		goto err;
	}
	i++;

	for (; filter[i]; i++) {
		if (!isspace(filter[i]))
			break;
	}
	return i - start_i;
 err:
	return start_i - i; /* negative or zero */
}

static int get_val_end(const char *filter, int i, int *end)
{
	bool backslash = false;
	int start_i = i;
	int quote;

	switch (filter[i]) {
	case '0':
		i++;
		if (tolower(filter[i+1]) != 'x' &&
		    !isdigit(filter[i+1]))
			break;
		/* fall through */
	case '1' ... '9':
		switch (tolower(filter[i])) {
		case 'x':
			for (i++; filter[i]; i++) {
				if (!isxdigit(filter[i]))
					break;
			}
			break;
		case '0':
			for (i++; filter[i]; i++) {
				if (filter[i] < '0' ||
				    filter[i] > '7')
					break;
			}
			break;
		default:
			for (i++; filter[i]; i++) {
				if (!isdigit(filter[i]))
					break;
			}
			break;
		}
		break;
	case '"':
	case '\'':
		quote = filter[i];
		for (i++; filter[i]; i++) {
			if (backslash) {
				backslash = false;
				continue;
			}
			switch (filter[i]) {
			case '\\':
				backslash = true;
				continue;
			case '"':
			case '\'':
				if (filter[i] == quote)
					break;
				continue;
			default:
				continue;
			}
			break;
		}
		if (filter[i])
			i++;
		break;
	default:
		break;
	}

	*end = i;
	return i - start_i;
}

/**
 * tracefs_filter_string_verify - verify a given filter works for an event
 * @event: The event to test the given filter for
 * @filter: The filter to test
 * @err: Error message for syntax errors (NULL to ignore)
 *
 * Parse the @filter to verify that it is valid for the given @event.
 *
 * Returns 0 on succes and -1 on error, and except for memory allocation
 * errors, @err will be allocated with an error message. It must
 * be freed with free().
 */
int tracefs_filter_string_verify(struct tep_event *event, const char *filter,
				 char **err)
{
	enum tracefs_filter filter_type;
	enum tracefs_compare compare;
	char *str = NULL;
	char buf[(filter ? strlen(filter) : 0) + 1];
	char *field;
	char *val;
	unsigned int state = 0;
	unsigned int open = 0;
	int len;
	int end;
	int n;
	int i;

	if (!filter)
		return error_msg(err, str, NULL, 0, "No filter given");

	len = strlen(filter);

	for (i = 0; i < len; i++) {
		field = NULL;
		val = NULL;
		compare = 0;

		switch (filter[i]) {
		case '(':
			filter_type = TRACEFS_FILTER_OPEN_PAREN;
			break;
		case ')':
			filter_type = TRACEFS_FILTER_CLOSE_PAREN;
			break;
		case '!':
			filter_type = TRACEFS_FILTER_NOT;
			break;
		case '&':
		case '|':

			if (filter[i] == filter[i+1]) {
				i++;
				if (filter[i] == '&')
					filter_type = TRACEFS_FILTER_AND;
				else
					filter_type = TRACEFS_FILTER_OR;
				break;
			}
			if (filter[i] == '|')
				return error_msg(err, str, filter, i,
						 "Invalid op");

			return error_msg(err, str, filter, i,
					 "Invalid location for '&'");
		default:
			if (isspace(filter[i]))
				continue;

			field = buf;

			n = get_field_end(filter, i, &end);
			if (!n)
				return error_msg(err, str, filter, i,
						 "Invalid field name");

			strncpy(field, filter+i, n);

			i += n;
			field[n++] = '\0';

			val = field + n;

			n = get_compare(filter, i, &compare);
			if (n <= 0)
				return error_msg(err, str, filter, i - n,
						 "Invalid compare");

			i += n;
			get_val_end(filter, i, &end);
			n = end - i;
			if (!n)
				return error_msg(err, str, filter, i,
						 "Invalid value");
			strncpy(val, filter + i, n);
			val[n] = '\0';
			i += n - 1;

			filter_type = TRACEFS_FILTER_COMPARE;
			break;
		}
		n = append_filter(&str, &state, &open,
				    event, filter_type, field, compare, val);

		if (n < 0)
			return error_msg(err, str, filter, i, NULL);
	}

	if (open)
		return error_msg(err, str, filter, i,
				 "Not enough closed parenthesis");
	switch (state) {
	case S_COMPARE:
	case S_CLOSE_PAREN:
		break;
	default:
		return error_msg(err, str, filter, i,
				 "Unfinished filter");
	}

	free(str);
	return 0;
}

/**
 * tracefs_event_filter_apply - apply given filter on event in given instance
 * @instance: The instance in which the filter will be applied (NULL for toplevel).
 * @event: The event to apply the filter on.
 * @filter: The filter to apply.
 *
 * Apply the @filter to given @event in givem @instance. The @filter string
 * should be created with tracefs_filter_string_append().
 *
 * Returns 0 on succes and -1 on error.
 */
int tracefs_event_filter_apply(struct tracefs_instance *instance,
			       struct tep_event *event, const char *filter)
{
	return tracefs_event_file_write(instance, event->system, event->name,
					"filter", filter);
}

/**
 * tracefs_event_filter_clear - clear the filter on event in given instance
 * @instance: The instance in which the filter will be applied (NULL for toplevel).
 * @event: The event to apply the filter on.
 *
 * Returns 0 on succes and -1 on error.
 */
int tracefs_event_filter_clear(struct tracefs_instance *instance,
			       struct tep_event *event)
{
	return tracefs_event_file_write(instance, event->system, event->name,
					"filter", "0");
}

/** Deprecated **/
int tracefs_event_append_filter(struct tep_event *event, char **filter,
				enum tracefs_filter type,
				const char *field, enum tracefs_compare compare,
				const char *val)
{
	return tracefs_filter_string_append(event, filter, type, field,
					    compare, val);
}
int tracefs_event_verify_filter(struct tep_event *event, const char *filter,
				char **err)
{
	return tracefs_filter_string_verify(event, filter, err);
}
