/*
 * Copyright (C) 2017 Oracle.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, see http://www.gnu.org/copyleft/gpl.txt
 */

#include "smatch.h"
#include "smatch_extra.h"

static int my_id;

struct allocator {
	const char *func;
	int param;
	int param2;
};

static struct allocator generic_allocator_table[] = {
	{"malloc", 0},
	{"memdup", 1},
	{"realloc", 1},
};

static struct allocator kernel_allocator_table[] = {
	{"kmalloc", 0},
	{"kzalloc", 0},
	{"vmalloc", 0},
	{"__vmalloc", 0},
	{"vzalloc", 0},
	{"sock_kmalloc", 1},
	{"kmemdup", 1},
	{"memdup_user", 1},
	{"dma_alloc_attrs", 1},
	{"dma_alloc_coherent", 1},
	{"devm_kmalloc", 1},
	{"devm_kzalloc", 1},
	{"krealloc", 1},
};

static struct allocator calloc_table[] = {
	{"calloc", 0, 1},
	{"kcalloc", 0, 1},
	{"kmalloc_array", 0, 1},
	{"devm_kcalloc", 1, 2},
};

static int bytes_per_element(struct expression *expr)
{
	struct symbol *type;

	type = get_type(expr);
	if (!type)
		return 0;

	if (type->type != SYM_PTR && type->type != SYM_ARRAY)
		return 0;

	type = get_base_type(type);
	return type_bytes(type);
}

static void save_constraint_required(struct expression *pointer, int op, struct expression *constraint)
{
	char *data, *limit;

	data = get_constraint_str(pointer);
	if (!data)
		return;

	limit = get_constraint_str(constraint);
	if (!limit) {
		// FIXME deal with <= also
		if (op == '<')
			set_state_expr(my_id, constraint, alloc_state_expr(pointer));
		goto free_data;
	}

	sql_save_constraint_required(data, op, limit);

	free_string(limit);
free_data:
	free_string(data);
}

static int handle_zero_size_arrays(struct expression *pointer, struct expression *size)
{
	struct expression *left, *right;
	struct symbol *type, *array, *array_type;
	sval_t struct_size;
	char *limit;
	char data[128];

	if (size->type != EXPR_BINOP || size->op != '+')
		return 0;

	type = get_type(pointer);
	if (!type || type->type != SYM_PTR)
		return 0;
	type = get_real_base_type(type);
	if (!type || !type->ident || type->type != SYM_STRUCT)
		return 0;
	if (!last_member_is_resizable(type))
		return 0;
	array = last_ptr_list((struct ptr_list *)type->symbol_list);
	if (!array || !array->ident)
		return 0;
	array_type = get_real_base_type(array);
	if (!array_type || array_type->type != SYM_ARRAY)
		return 0;
	array_type = get_real_base_type(array_type);

	left = strip_expr(size->left);
	right = strip_expr(size->right);

	if (!get_implied_value(left, &struct_size))
		return 0;
	if (struct_size.value != type_bytes(type))
		return 0;

	if (right->type == EXPR_BINOP && right->op == '*') {
		struct expression *mult_left, *mult_right;
		sval_t sval;

		mult_left = strip_expr(right->left);
		mult_right = strip_expr(right->right);

		if (get_implied_value(mult_left, &sval) &&
		    sval.value == type_bytes(array_type))
			size = mult_right;
		else if (get_implied_value(mult_right, &sval) &&
		    sval.value == type_bytes(array_type))
			size = mult_left;
		else
			return 0;
	}

	snprintf(data, sizeof(data), "(struct %s)->%s", type->ident->name, array->ident->name);
	limit = get_constraint_str(size);
	if (!limit) {
		set_state_expr(my_id, size, alloc_state_expr(
			       member_expression(deref_expression(pointer), '*', array->ident)));
		return 1;
	}

	sql_save_constraint_required(data, '<', limit);

	free_string(limit);
	return 1;
}

static void match_alloc_helper(struct expression *pointer, struct expression *size, int recurse)
{
	struct expression *size_orig, *tmp;
	sval_t sval;
	int cnt = 0;

	pointer = strip_expr(pointer);
	size = strip_expr(size);
	if (!size || !pointer)
		return;

	size_orig = size;
	if (recurse) {
		while ((tmp = get_assigned_expr(size))) {
			size = strip_expr(tmp);
			if (cnt++ > 5)
				break;
		}
		if (size != size_orig) {
			match_alloc_helper(pointer, size, 0);
			size = size_orig;
		}
	}

	if (handle_zero_size_arrays(pointer, size))
		return;

	if (size->type == EXPR_BINOP && size->op == '*') {
		struct expression *mult_left, *mult_right;

		mult_left = strip_expr(size->left);
		mult_right = strip_expr(size->right);

		if (get_implied_value(mult_left, &sval) &&
		    sval.value == bytes_per_element(pointer))
			size = mult_right;
		else if (get_implied_value(mult_right, &sval) &&
		    sval.value == bytes_per_element(pointer))
			size = mult_left;
		else
			return;
	}

	if (size->type == EXPR_BINOP && size->op == '+' &&
	    get_implied_value(size->right, &sval) &&
	    sval.value == 1)
		save_constraint_required(pointer, SPECIAL_LTE, size->left);
	else
		save_constraint_required(pointer, '<', size);
}

static void match_alloc(const char *fn, struct expression *expr, void *_size_arg)
{
	int size_arg = PTR_INT(_size_arg);
	struct expression *call, *arg;

	call = strip_expr(expr->right);
	arg = get_argument_from_call_expr(call->args, size_arg);

	match_alloc_helper(expr->left, arg, 1);
}

static void match_calloc(const char *fn, struct expression *expr, void *_start_arg)
{
	struct expression *pointer, *call, *size;
	struct expression *count = NULL;
	int start_arg = PTR_INT(_start_arg);
	sval_t sval;

	pointer = strip_expr(expr->left);
	call = strip_expr(expr->right);

	size = get_argument_from_call_expr(call->args, start_arg);
	if (get_implied_value(size, &sval) &&
	    sval.value == bytes_per_element(pointer))
		count = get_argument_from_call_expr(call->args, start_arg + 1);
	else {
		size = get_argument_from_call_expr(call->args, start_arg + 1);
		if (get_implied_value(size, &sval) &&
		    sval.value == bytes_per_element(pointer))
			count = get_argument_from_call_expr(call->args, start_arg);
	}

	if (!count)
		return;

	save_constraint_required(pointer, '<', count);
}

static void add_allocation_function(const char *func, void *call_back, int param)
{
	add_function_assign_hook(func, call_back, INT_PTR(param));
}

static void match_assign_size(struct expression *expr)
{
	struct smatch_state *state;
	char *data, *limit;

	state = get_state_expr(my_id, expr->right);
	if (!state || !state->data)
		return;

	data = get_constraint_str(state->data);
	if (!data)
		return;

	limit = get_constraint_str(expr->left);
	if (!limit)
		goto free_data;

	sql_save_constraint_required(data, '<', limit);

	free_string(limit);
free_data:
	free_string(data);
}

static void match_assign_has_buf_comparison(struct expression *expr)
{
	struct expression *size;
	int limit_type;

	if (expr->op != '=')
		return;
	if (expr->right->type == EXPR_CALL)
		return;
	size = get_size_variable(expr->right, &limit_type);
	if (!size)
		return;
	if (limit_type != ELEM_COUNT)
		return;
	match_alloc_helper(expr->left, size, 1);
}

static void match_assign_data(struct expression *expr)
{
	struct expression *right, *arg, *tmp;
	int i;
	int size_arg;
	int size_arg2 = -1;

	if (expr->op != '=')
		return;

	/* Direct calls are handled else where (for now at least) */
	tmp = get_assigned_expr(expr->right);
	if (!tmp)
		return;

	right = strip_expr(tmp);
	if (right->type != EXPR_CALL)
		return;

	if (right->fn->type != EXPR_SYMBOL ||
	    !right->fn->symbol ||
	    !right->fn->symbol->ident)
		return;

	for (i = 0; i < ARRAY_SIZE(generic_allocator_table); i++) {
		if (strcmp(right->fn->symbol->ident->name,
			   generic_allocator_table[i].func) == 0) {
			size_arg = generic_allocator_table[i].param;
			goto found;
		}
	}

	if (option_project != PROJ_KERNEL)
		return;

	for (i = 0; i < ARRAY_SIZE(kernel_allocator_table); i++) {
		if (strcmp(right->fn->symbol->ident->name,
			   kernel_allocator_table[i].func) == 0) {
			size_arg = kernel_allocator_table[i].param;
			goto found;
		}
	}

	for (i = 0; i < ARRAY_SIZE(calloc_table); i++) {
		if (strcmp(right->fn->symbol->ident->name,
			   calloc_table[i].func) == 0) {
			size_arg = calloc_table[i].param;
			size_arg2 = calloc_table[i].param2;
			goto found;
		}
	}

	return;

found:
	arg = get_argument_from_call_expr(right->args, size_arg);
	match_alloc_helper(expr->left, arg, 1);
	if (size_arg2 == -1)
		return;
	arg = get_argument_from_call_expr(right->args, size_arg2);
	match_alloc_helper(expr->left, arg, 1);
}

static void match_assign_ARRAY_SIZE(struct expression *expr)
{
	struct expression *array;
	char *data, *limit;
	const char *macro;

	macro = get_macro_name(expr->right->pos);
	if (!macro || strcmp(macro, "ARRAY_SIZE") != 0)
		return;
	array = strip_expr(expr->right);
	if (array->type != EXPR_BINOP || array->op != '+')
		return;
	array = strip_expr(array->left);
	if (array->type != EXPR_BINOP || array->op != '/')
		return;
	array = strip_expr(array->left);
	if (array->type != EXPR_SIZEOF)
		return;
	array = strip_expr(array->cast_expression);
	if (array->type != EXPR_PREOP || array->op != '*')
		return;
	array = strip_expr(array->unop);

	data = get_constraint_str(array);
	limit = get_constraint_str(expr->left);
	if (!data || !limit)
		goto free;

	sql_save_constraint_required(data, '<', limit);

free:
	free_string(data);
	free_string(limit);
}

static void match_assign_buf_comparison(struct expression *expr)
{
	struct expression *pointer;

	if (expr->op != '=')
		return;
	pointer = get_array_variable(expr->right);
	if (!pointer)
		return;

	match_alloc_helper(pointer, expr->right, 1);
}

static int constraint_found(void *_found, int argc, char **argv, char **azColName)
{
	int *found = _found;

	*found = 1;
	return 0;
}

static int has_constraint(struct expression *expr, const char *constraint)
{
	int found = 0;

	if (get_state_expr(my_id, expr))
		return 1;

	run_sql(constraint_found, &found,
		"select data from constraints_required where bound = '%q' limit 1",
		escape_newlines(constraint));

	return found;
}

static void match_assign_constraint(struct expression *expr)
{
	struct symbol *type;
	char *left, *right;

	if (__in_fake_struct_assign)
		return;
	if (expr->op != '=')
		return;

	type = get_type(expr->left);
	if (!type || type->type != SYM_BASETYPE)
		return;

	left = get_constraint_str(expr->left);
	if (!left)
		return;
	right = get_constraint_str(expr->right);
	if (!right)
		goto free;
	if (!has_constraint(expr->right, right))
		return;
	sql_copy_constraint_required(left, right);
free:
	free_string(right);
	free_string(left);
}

void register_constraints_required(int id)
{
	my_id = id;

	set_dynamic_states(my_id);
	add_hook(&match_assign_size, ASSIGNMENT_HOOK);
	add_hook(&match_assign_data, ASSIGNMENT_HOOK);
	add_hook(&match_assign_has_buf_comparison, ASSIGNMENT_HOOK);

	add_hook(&match_assign_ARRAY_SIZE, ASSIGNMENT_HOOK);
	add_hook(&match_assign_ARRAY_SIZE, GLOBAL_ASSIGNMENT_HOOK);
	add_hook(&match_assign_buf_comparison, ASSIGNMENT_HOOK);
	add_hook(&match_assign_constraint, ASSIGNMENT_HOOK);

	add_allocation_function("malloc", &match_alloc, 0);
	add_allocation_function("memdup", &match_alloc, 1);
	add_allocation_function("realloc", &match_alloc, 1);
	add_allocation_function("realloc", &match_calloc, 0);
	if (option_project == PROJ_KERNEL) {
		add_allocation_function("kmalloc", &match_alloc, 0);
		add_allocation_function("kzalloc", &match_alloc, 0);
		add_allocation_function("vmalloc", &match_alloc, 0);
		add_allocation_function("__vmalloc", &match_alloc, 0);
		add_allocation_function("vzalloc", &match_alloc, 0);
		add_allocation_function("sock_kmalloc", &match_alloc, 1);
		add_allocation_function("kmemdup", &match_alloc, 1);
		add_allocation_function("memdup_user", &match_alloc, 1);
		add_allocation_function("dma_alloc_attrs", &match_alloc, 1);
		add_allocation_function("dma_alloc_coherent", &match_alloc, 1);
		add_allocation_function("devm_kmalloc", &match_alloc, 1);
		add_allocation_function("devm_kzalloc", &match_alloc, 1);
		add_allocation_function("kcalloc", &match_calloc, 0);
		add_allocation_function("kmalloc_array", &match_calloc, 0);
		add_allocation_function("devm_kcalloc", &match_calloc, 1);
		add_allocation_function("krealloc", &match_alloc, 1);
	}
}
