/**
 * @file   sql57_probe.c
 * @brief  sql57 probe
 * @author "Daniel Kopecek" <dkopecek@redhat.com>
 *
 */

/*
 * Copyright 2011--2013 Red Hat Inc., Durham, North Carolina.
 * All Rights Reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 *
 * Authors:
 *      "Daniel Kopecek" <dkopecek@redhat.com>
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include "_seap.h"
#include <probe-api.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <common/debug_priv.h>
#include <common/bfind.h>
#include <time.h>
#include <errno.h>
#include <opendbx/api.h>
#include "sql57_probe.h"

#ifndef SQLPROBE_DEFAULT_CONNTIMEOUT
# define SQLPROBE_DEFAULT_CONNTIMEOUT 30
#endif

typedef struct {
	char *o_engine; /* object engine  */
	char *b_engine; /* backend engine */
} dbEngineMap_t;

static dbEngineMap_t engine_map[] = {
	{ "access",    NULL       },
	{ "db2",       NULL       },
	{ "cache",     NULL       },
	{ "firebird",  "firebird" },
	{ "firstsql",  NULL       },
	{ "foxpro",    NULL       },
	{ "informix",  NULL       },
	{ "ingres",    NULL       },
	{ "interbase", NULL       },
	{ "lightbase", NULL       },
	{ "maxdb",     NULL       },
	{ "monetdb",   NULL       },
	{ "mimer",     NULL       },
	{ "mssql",     "mssql"    }, /* non-standard */
	{ "mysql",     "mysql"    }, /* standard since OVAL 5.10 */
	{ "oracle",    "oracle"   },
	{ "paradox",   NULL       },
	{ "pervasive", NULL       },
	{ "postgre",   "pgsql"    },
	{ "sqlbase",   NULL       },
	{ "sqlite",    "sqlite"   },
	{ "sqlite3",   "sqlite3"  }, /* non-standard */
	{ "sqlserver", NULL       },
	{ "sybase",    "sybase"   }
};

#define ENGINE_MAP_COUNT (sizeof engine_map / sizeof (dbEngineMap_t))

static int engine_cmp (const char *a, const dbEngineMap_t *b)
{
	if (a == NULL || b == NULL) {
		return -1;
	}
	return strcmp(a, b->o_engine);
}

typedef struct {
	char    *host;
	char    *port;
	char    *user;
	char    *pass;
	char    *db;
	long     conn_timeout;
} dbURIInfo_t;

static void __clearmem(void *ptr, int len)
{
	if (ptr != NULL) {
		do {
			register int       l = len / sizeof(uint32_t) - 1;
			register uint32_t *p = (uint32_t *)ptr;

			while (l >= 0) {
				p[l] = (uint32_t)random();
				--l;
			}
		} while (0);

		do {
			register int      l = len % sizeof(uint32_t);
			register uint8_t *p = (uint8_t *)ptr;

			for (int i = 0; i < l; i++) {
				p[i] = (uint8_t)(random() % (1 << 8));
			}
		} while (0);
	}
	return;
}

static void dbURIInfo_clear(dbURIInfo_t *ui)
{
	if (ui == NULL)
		return;

	srandom((long)clock()^(long)(ui->pass));

	if (ui->host != NULL) {
		__clearmem(ui->host, strlen(ui->host));
		free(ui->host);
	}

	if (ui->user != NULL) {
		__clearmem(ui->user, strlen(ui->user));
		free(ui->user);
	}

	if (ui->pass != NULL) {
		__clearmem(ui->pass, strlen(ui->pass));
		free(ui->pass);
	}

	if (ui->db != NULL) {
		__clearmem(ui->db, strlen(ui->db));
		free(ui->db);
	}

	ui->host = NULL;
	ui->port = 0;
	ui->user = NULL;
	ui->pass = NULL;
	ui->db   = NULL;

	return;
}

static int dbURIInfo_parse(dbURIInfo_t *info, const char *conn)
{
	char *tmp, *tok, *copy;
	char *conn_copy = strdup(conn);
	copy = conn_copy;
	if (copy == NULL)
		return (-1);

#define skipspace(s) while (isspace(*s)) ++s;

#define matchitem1(tok, first, rest, dst)			\
	case first:						\
		if (strncasecmp((rest), ++(tok), strlen(rest)) == 0) {	\
			tok += strlen(rest);			\
			skipspace(tok);				\
			if (*(tok) != '=') goto __fail;		\
			else (dst) = strdup((tok) + 1);		\
		}						\
		else {	\
			dE("Unrecognized token: '%s'", (tok)-1);	\
		}

#define matchitem2(tok, first, rest1, dst1, rest2, dst2)		\
	case first:							\
		if (strncasecmp((rest1), (tok)+1, strlen(rest1)) == 0) {	\
			tok += 1+strlen(rest1);				\
			skipspace(tok);					\
			if (*(tok) != '=') goto __fail;			\
			else (dst1) = strdup((tok) + 1);		\
		}							\
		else if (strncasecmp((rest2), (tok)+1, strlen(rest2)) == 0) {	\
			tok += 1+strlen(rest2);				\
			skipspace(tok);					\
			if (*(tok) != '=') goto __fail;			\
			else (dst2) = strdup((tok) + 1);		\
		}							\
		else {	\
			dE("Unrecognized token: '%s'", (tok));		\
		}

	tmp = NULL;

	while ((tok = strsep (&copy, ";")) != NULL) {
		dD("tok: '%s'.", tok);
		switch (tolower(*tok)) {
			matchitem1(tok, 's',
				  "erver", info->host); break;
			matchitem2(tok, 'p',
				   "ort", info->port,
				   "wd",  info->pass); break;
			matchitem1(tok, 'd',
				   "atabase", info->db); break;
			matchitem1(tok, 'u',
				   "id",info->user); break;
			matchitem1(tok, 'c',
				   "onnecttimeout", tmp);
			if (tmp != NULL) {
				errno = 0;
				info->conn_timeout = strtol(tmp, NULL, 10);

				if (errno == ERANGE || errno == EINVAL)
					info->conn_timeout = SQLPROBE_DEFAULT_CONNTIMEOUT;

				free(tmp);
			}
			break;

		case '\0':
			break;
		default:
			dE("Unrecognized token: '%s'", (tok));
			break;
		}
	}

	free(conn_copy);
	return (0);
__fail:
	dE("Parsing failed.");
	free(conn_copy);
	return (-1);
}

static int dbSQL_eval(const char *engine, const char *version,
                      const char *conn, const char *sql, probe_ctx *ctx)
{
	int err = -1;
	dbURIInfo_t uriInfo = { .host = NULL,
				.port = 0,
				.user = NULL,
				.pass = NULL,
				.db   = NULL};

	/*
	 * parse the connection string
	 */
	if (dbURIInfo_parse(&uriInfo, conn) != 0) {
		dE("Malformed connection string: %s", conn);
		goto __exit;
	} else {
		int            sql_err = 0;
		odbx_t        *sql_dbh = NULL; /* handle */
		dbEngineMap_t *sql_dbe; /* engine */
		odbx_result_t *sql_dbr; /* result */
		SEXP_t        *item;

		sql_dbe = oscap_bfind (engine_map, ENGINE_MAP_COUNT, sizeof(dbEngineMap_t), (char *)engine,
				       (int(*)(void *, void *))&engine_cmp);

		if (sql_dbe == NULL) {
			SEXP_t *msg = probe_msg_creatf(OVAL_MESSAGE_LEVEL_ERROR,
				"DB engine not found: %s", engine);
			probe_cobj_add_msg(probe_ctx_getresult(ctx), msg);
			SEXP_free(msg);
			probe_cobj_set_flag(probe_ctx_getresult(ctx), SYSCHAR_FLAG_ERROR);
			err = 0;
			dE("DB engine not found: %s", engine);
			goto __exit;
		}

		if (sql_dbe->b_engine == NULL) {
			SEXP_t *msg = probe_msg_creatf(OVAL_MESSAGE_LEVEL_ERROR,
				"DB engine not supported: %s", engine);
			probe_cobj_add_msg(probe_ctx_getresult(ctx), msg);
			SEXP_free(msg);
			probe_cobj_set_flag(probe_ctx_getresult(ctx), SYSCHAR_FLAG_ERROR);
			err = 0;
			dE("DB engine not supported: %s", engine);
			goto __exit;
		}

		int odbx_res = odbx_init (&sql_dbh, sql_dbe->b_engine,
				uriInfo.host, uriInfo.port);
		if (odbx_res != ODBX_ERR_SUCCESS) {
			const char *error_msg = odbx_error(NULL, odbx_res);
			dE("odbx_init failed: e=%s, h=%s:%s msg=%s",
				sql_dbe->b_engine, uriInfo.host, uriInfo.port,
				error_msg != NULL ? error_msg : "(none)");
			if (odbx_res == -ODBX_ERR_NOTEXIST) {
				SEXP_t *msg = probe_msg_creatf(OVAL_MESSAGE_LEVEL_ERROR,
					"odbx_init failed. Please install the opendbx %s backend", engine);
				probe_cobj_add_msg(probe_ctx_getresult(ctx), msg);
				SEXP_free(msg);
				probe_cobj_set_flag(probe_ctx_getresult(ctx), SYSCHAR_FLAG_ERROR);
				err = 0;
				fprintf(stderr, "Could not connect to the database. "
					"Please install the opendbx %s backend.\n",
					sql_dbe->b_engine);
			}
			goto __exit;
		}

		/* set options */
		odbx_res = odbx_bind(sql_dbh, uriInfo.db, uriInfo.user, uriInfo.pass, ODBX_BIND_SIMPLE);
		if (odbx_res != ODBX_ERR_SUCCESS)
		{
			const char *error_msg = odbx_error(sql_dbh, odbx_res);
			SEXP_t *msg = probe_msg_creatf(OVAL_MESSAGE_LEVEL_ERROR,
				"odbx_bind failed. Could not connect to the database '%s': %s",
				uriInfo.db, error_msg != NULL ? error_msg : "(none)");
			probe_cobj_add_msg(probe_ctx_getresult(ctx), msg);
			SEXP_free(msg);
			probe_cobj_set_flag(probe_ctx_getresult(ctx), SYSCHAR_FLAG_ERROR);
			err = 0;
			dE("odbx_bind failed: db=%s, u=%s, p=%s",
			   uriInfo.db, uriInfo.user, uriInfo.pass);
			odbx_finish(sql_dbh);
			goto __exit;
		}

		if (odbx_query(sql_dbh, sql, strlen (sql)) != ODBX_ERR_SUCCESS) {
			dE("odbx_query failed: q=%s", sql);
			odbx_finish(sql_dbh);

			goto __exit;
		} else {
			sql_dbr = NULL;
                        item    = probe_item_create(OVAL_INDEPENDENT_SQL57, NULL,
                                                    "engine",            OVAL_DATATYPE_STRING, engine,
                                                    "version",           OVAL_DATATYPE_STRING, version,
                                                    "connection_string", OVAL_DATATYPE_STRING, conn,
                                                    "sql",               OVAL_DATATYPE_STRING, sql,
                                                    NULL);

			while ((sql_err = odbx_result (sql_dbh, &sql_dbr, NULL, 0)) == ODBX_RES_ROWS) {
				while (odbx_row_fetch(sql_dbr) == ODBX_ROW_NEXT) {
                                        SEXP_t se_tmp_mem, *field, *result;
                                        const char *col_val, *col_name;
                                        oval_datatype_t col_type;
                                        unsigned int ci;

                                        result = probe_ent_creat1("result", NULL, NULL);
                                        probe_ent_setdatatype(result, OVAL_DATATYPE_RECORD);

                                        for (ci = 0; ci < odbx_column_count(sql_dbr); ++ci) {
                                                col_val  = odbx_field_value (sql_dbr, ci);
                                                col_name = odbx_column_name (sql_dbr, ci);
                                                col_type = OVAL_DATATYPE_UNKNOWN;
                                                field    = NULL;

						dD("Column type: %d.", odbx_column_type(sql_dbr, ci));
                                                switch(odbx_column_type(sql_dbr, ci)) {
                                                case ODBX_TYPE_BOOLEAN:
                                                        break;
                                                case ODBX_TYPE_INTEGER:
                                                case ODBX_TYPE_SMALLINT: {
                                                        char *end = NULL;
                                                        int   val = strtol(col_val, &end, 10);

                                                        if (val == 0 && (end == col_val))
                                                                dE("strtol(%s) failed", col_val);

							field    = probe_ent_creat1(col_name, NULL, SEXP_number_newi_r(&se_tmp_mem, val));
                                                        col_type = OVAL_DATATYPE_INTEGER;
                                                        SEXP_free_r(&se_tmp_mem);

                                                }       break;
                                                case ODBX_TYPE_REAL:
                                                case ODBX_TYPE_DOUBLE:
                                                case ODBX_TYPE_FLOAT: {
                                                        char  *end = NULL;
                                                        double val = strtod(col_val, &end);

                                                        if (val == 0 && (end == col_val))
                                                                dE("strtod(%s) failed", col_val);

							field    = probe_ent_creat1(col_name, NULL, SEXP_number_newf_r(&se_tmp_mem, val));
                                                        col_type = OVAL_DATATYPE_FLOAT;
                                                        SEXP_free_r(&se_tmp_mem);

                                                }       break;
                                                case ODBX_TYPE_CHAR:
                                                case ODBX_TYPE_NCHAR:
                                                case ODBX_TYPE_VARCHAR:
						case ODBX_TYPE_CLOB:
                                                        field    = probe_ent_creat1(col_name, NULL, SEXP_string_new_r(&se_tmp_mem, col_val, strlen(col_val)));
                                                        col_type = OVAL_DATATYPE_STRING;
                                                        SEXP_free_r(&se_tmp_mem);
                                                        break;
                                                case ODBX_TYPE_TIMESTAMP:
                                                        break;
						default:
							dW("Unsupported ODBX type: %d.", odbx_column_type(sql_dbr, ci));
							break;
                                                }

                                                if (field != NULL) {
                                                        probe_ent_setdatatype(field, col_type);

                                                        SEXP_list_add(result, field);
                                                        SEXP_free(field);
                                                }
                                        }

                                        SEXP_list_add(item, result);
                                        SEXP_free(result);
				}

				odbx_result_finish(sql_dbr);
			}

                        probe_item_collect(ctx, item);
		}

		if (odbx_finish(sql_dbh) != ODBX_ERR_SUCCESS) {
			dE("odbx_finish failed");
			goto __exit;
		}
	}

	err = 0;
__exit:
	dbURIInfo_clear(&uriInfo);
	return (err);
}

int sql57_probe_main(probe_ctx *ctx, void *arg)
{
	char *engine, *version, *conn, *sqlexp;
	int err;
        SEXP_t *probe_in;

#define get_string(dst, obj, ent_name)					\
	do {								\
		SEXP_t *__sval;						\
									\
		__sval = probe_obj_getentval (obj, ent_name, 1);	\
									\
		if (__sval == NULL) {					\
			dE("Missing entity or value: obj=%p, ent=%s", obj, #ent_name); \
			err = PROBE_ENOENT;				\
			goto __exit;					\
		}							\
									\
		(dst) = SEXP_string_cstr (__sval);			\
									\
		if ((dst) == NULL) {					\
			SEXP_free(__sval);				\
			err = PROBE_EINVAL;				\
			goto __exit;					\
		}							\
									\
		SEXP_free(__sval);					\
	} while (0)

	engine  = NULL;
	version = NULL;
	conn    = NULL;
	sqlexp  = NULL;

        probe_in = probe_ctx_getobject(ctx);

	get_string(engine,  probe_in, "engine");
	get_string(version, probe_in, "version");
	get_string(conn,    probe_in, "connection_string");
	get_string(sqlexp,  probe_in, "sql");

	/*
	 * evaluate the SQL statement
	 */
	err = dbSQL_eval(engine, version, conn, sqlexp, ctx);
__exit:
	if (engine != NULL) {
		__clearmem(engine, strlen(engine));
		free(engine);
	}

	if (sqlexp != NULL) {
		__clearmem(sqlexp, strlen(sqlexp));
		free(sqlexp);
	}

	if (conn != NULL) {
		__clearmem(conn, strlen(conn));
		free(conn);
	}

	if (version != NULL) {
		__clearmem(version, strlen(version));
		free(version);
	}

	return (err);
}
