/*
 * Copyright (c) 2014, Vsevolod Stakhov
 * Copyright (c) 2024, Baptiste Daroussin
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *  * Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *  * Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED ''AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL AUTHOR BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include <sys/param.h>
#include <sys/mount.h>

#include <assert.h>
#include <fcntl.h>
#include <regex.h>
#include <grp.h>
#include <stdlib.h>
#include <stdio.h>
#include <stdbool.h>
#include <string.h>
#include <unistd.h>
#include <libgen.h>

#include <sqlite3.h>

#include <bsd_compat.h>

#include "pkg.h"
#include "private/event.h"
#include "private/pkg.h"
#include "private/pkgdb.h"
#include "private/utils.h"
#include "binary.h"
#include "binary_private.h"

extern struct pkg_ctx ctx;

static int
pkg_repo_binary_get_user_version(sqlite3 *sqlite, int *reposcver)
{
	sqlite3_stmt *stmt;
	int retcode;
	const char *sql = "PRAGMA user_version;";

	if ((stmt = prepare_sql(sqlite, sql)) == NULL)
		return (EPKG_FATAL);

	if (sqlite3_step(stmt) == SQLITE_ROW) {
		*reposcver = sqlite3_column_int64(stmt, 0);
		retcode = EPKG_OK;
	} else {
		*reposcver = -1;
		retcode = EPKG_FATAL;
	}
	sqlite3_finalize(stmt);
	return (retcode);
}

static int
pkg_repo_binary_check_version(struct pkg_repo *repo, sqlite3 *sqlite)
{
	int reposcver;
	int repomajor;
	int ret;

	if ((ret = pkg_repo_binary_get_user_version(sqlite, &reposcver))
	    != EPKG_OK)
		return (ret);	/* sqlite error */

	/*
	 * If the local pkgng uses a repo schema behind that used to
	 * create the repo, we may still be able use it for reading
	 * (ie pkg install), but pkg repo can't do an incremental
	 * update unless the actual schema matches the compiled in
	 * schema version.
	 *
	 * Use a major - minor version schema: as the user_version
	 * PRAGMA takes an integer version, encode this as MAJOR *
	 * 1000 + MINOR.
	 *
	 * So long as the major versions are the same, the local pkgng
	 * should be compatible with any repo created by a more recent
	 * pkgng, although it may need some modification of the repo
	 * schema
	 */

	/* --- Temporary ---- Grandfather in the old repo schema
	   version so this patch doesn't immediately invalidate all
	   the repos out there */

	if (reposcver == 2)
		reposcver = 2000;
	if (reposcver == 3)
		reposcver = 2001;

	repomajor = reposcver / 1000;

	if (repomajor < REPO_SCHEMA_MAJOR) {
		pkg_emit_error("Repo %s (schema version %d) is too old - "
		    "need at least schema %d", repo->name, reposcver,
		    REPO_SCHEMA_MAJOR * 1000);
		return (EPKG_REPOSCHEMA);
	}

	if (repomajor > REPO_SCHEMA_MAJOR) {
		pkg_emit_error("Repo %s (schema version %d) is too new - "
		    "we can accept at most schema %d", repo->name, reposcver,
		    ((REPO_SCHEMA_MAJOR + 1) * 1000) - 1);
		return (EPKG_REPOSCHEMA);
	}

	/* This is a repo schema version we can work with */

	if (reposcver != REPO_SCHEMA_VERSION)
		return (EPKG_REPOSCHEMA);
	return (EPKG_OK);
}

int
pkg_repo_binary_open(struct pkg_repo *repo, unsigned mode)
{
	const char *filepath;
	sqlite3 *sqlite = NULL;
	int flags, dbdirfd, fd, reposfd, thisrepofd;
	int64_t res;
	struct pkg_repo_it *it;
	struct pkg *pkg = NULL;

	sqlite3_initialize();

	pkgdb_syscall_overload();

	dbdirfd = pkg_get_dbdirfd();
	reposfd = pkg_get_reposdirfd();
	thisrepofd = openat(reposfd, repo->name, O_DIRECTORY|O_CLOEXEC);
	if (thisrepofd == -1) {
		if (mkdirat(reposfd, repo->name, 0755) == -1)
			return( EPKG_FATAL);
		thisrepofd = openat(reposfd, repo->name, O_DIRECTORY|O_CLOEXEC);
	}

	/* Open metafile */
	if ((fd = openat(thisrepofd, "meta", O_RDONLY)) != -1) {
		struct pkg_repo_meta *nmeta = NULL;
		if (pkg_repo_meta_load(fd, &nmeta) != EPKG_OK) {
			pkg_emit_error("Repository %s load error: "
			    "meta file cannot be loaded", repo->name);
			close(fd);
			return (EPKG_FATAL);
		}
		close(fd);
		pkg_repo_meta_free(repo->meta);
		repo->meta = nmeta;
	}

	filepath = pkg_repo_binary_get_filename(repo);

	/* Always want read mode here */
	if (faccessat(dbdirfd, filepath, R_OK | mode, 0) != 0) {
		return (EPKG_ENOACCESS);
	}

	flags = (mode & W_OK) != 0 ? SQLITE_OPEN_READWRITE : SQLITE_OPEN_READONLY;
	if (sqlite3_open_v2(filepath, &sqlite, flags, NULL) != SQLITE_OK) {
		pkgdb_nfs_corruption(sqlite);
		pkg_emit_error("Repository %s load error: "
		    "cannot open sqlite3 db: %s",
		    pkg_repo_name(repo), sqlite3_errmsg(sqlite));
		return (EPKG_FATAL);
	}

	/* Sanitise sqlite database */
	if (get_pragma(sqlite, "SELECT count(name) FROM sqlite_master "
		"WHERE type='table' AND name='repodata';", &res, false) != EPKG_OK) {
		pkg_emit_error("Repository %s load error: unable to query db",
		    pkg_repo_name(repo));
		sqlite3_close(sqlite);
		return (EPKG_FATAL);
	}

	if (res != 1) {
		pkg_emit_error("Repository %s contains no repodata table, "
			"need to re-create database", repo->name);
		sqlite3_close(sqlite);
		return (EPKG_FATAL);
	}

	/* Check package site */
	char *req = sqlite3_mprintf("select count(key) from repodata "
		"WHERE key = \"packagesite\" and value = '%q'", pkg_repo_url(repo));

	res = 0;
	get_pragma(sqlite, req, &res, true);
	sqlite3_free(req);
	if (res != 1) {
		pkg_emit_error("Repository %s has a wrong packagesite, need to "
			"re-create database", repo->name);
		sqlite3_close(sqlite);
		return (EPKG_FATAL);
	}

	/* Check version */
	if (pkg_repo_binary_check_version(repo, sqlite) != EPKG_OK) {
		pkg_emit_error("need to re-create repo %s to upgrade schema version",
			repo->name);
		sqlite3_close(sqlite);
		if (mode & W_OK)
			(void)unlinkat(dbdirfd, filepath, 0);
		return (EPKG_REPOSCHEMA);
	}

	repo->priv = sqlite;
	/* Check digests format */
	if ((it = pkg_repo_binary_query(repo, NULL, NULL, MATCH_ALL)) == NULL)
		return (EPKG_OK);

	if (it->ops->next(it, &pkg, PKG_LOAD_BASIC) != EPKG_OK) {
		it->ops->free(it);
		return (EPKG_OK);
	}
	it->ops->free(it);
	if (pkg->digest == NULL || !pkg_checksum_is_valid(pkg->digest, strlen(pkg->digest))) {
		pkg_emit_error("Repository %s has incompatible checksum format, need to "
			"re-create database", repo->name);
		pkg_free(pkg);
		sqlite3_close(sqlite);
		repo->priv = NULL;
		return (EPKG_FATAL);
	}
	pkg_free(pkg);

	return (EPKG_OK);
}

int
pkg_repo_binary_create(struct pkg_repo *repo)
{
	const char *filepath;
	sqlite3 *sqlite = NULL;
	int retcode, dbdirfd;

	sqlite3_initialize();

	dbdirfd = pkg_get_dbdirfd();
	filepath = pkg_repo_binary_get_filename(repo);
	/* Should never ever happen */
	if (faccessat(dbdirfd, filepath, R_OK, 0) == 0)
		return (EPKG_CONFLICT);

	pkgdb_syscall_overload();

	/* Open for read/write/create */
	if (sqlite3_open(filepath, &sqlite) != SQLITE_OK) {
		pkgdb_nfs_corruption(sqlite);
		return (EPKG_FATAL);
	}

	retcode = sql_exec(sqlite, binary_repo_initsql, REPO_SCHEMA_VERSION);

	if (retcode == EPKG_OK) {
		sqlite3_stmt *stmt;
		const char sql[] = ""
			"INSERT OR REPLACE INTO repodata (key, value) "
			"VALUES (\"packagesite\", ?1);";

		/* register the packagesite */
		if (sql_exec(sqlite, "CREATE TABLE IF NOT EXISTS repodata ("
			"   key TEXT UNIQUE NOT NULL,"
			"   value TEXT NOT NULL"
			");") != EPKG_OK) {
			pkg_emit_error("Unable to register the packagesite in the "
				"database");
			retcode = EPKG_FATAL;
			goto cleanup;
		}

		if (sqlite3_prepare_v2(sqlite, sql, -1, &stmt, NULL) != SQLITE_OK) {
			ERROR_SQLITE(sqlite, sql);
			retcode = EPKG_FATAL;
			goto cleanup;
		}

		sqlite3_bind_text(stmt, 1, pkg_repo_url(repo), -1, SQLITE_STATIC);

		if (sqlite3_step(stmt) != SQLITE_DONE) {
			ERROR_STMT_SQLITE(sqlite, stmt);
			sqlite3_finalize(stmt);
			retcode = EPKG_FATAL;
			goto cleanup;
		}

		sqlite3_finalize(stmt);
	}

cleanup:
	sqlite3_close(sqlite);

	return (retcode);
}

int
pkg_repo_binary_init(struct pkg_repo *repo)
{
	int retcode = EPKG_OK;
	sqlite3 *sqlite = PRIV_GET(repo);

	retcode = sql_exec(sqlite, "PRAGMA journal_mode=TRUNCATE;");
	if (retcode != EPKG_OK)
		return (retcode);

	retcode = sql_exec(sqlite, "PRAGMA synchronous=FULL");
	if (retcode != EPKG_OK)
		return (retcode);

	retcode = sql_exec(sqlite, "PRAGMA foreign_keys=on");
	if (retcode != EPKG_OK)
		return (retcode);
	sql_exec(sqlite, "PRAGMA mmap_size=268435456;");


	pkgdb_sqlcmd_init(sqlite, NULL, NULL);

	retcode = pkg_repo_binary_init_prstatements(sqlite);
	if (retcode != EPKG_OK)
		return (retcode);

	repo->priv = sqlite;

	return (EPKG_OK);
}

int
pkg_repo_binary_close(struct pkg_repo *repo, bool commit)
{
	int retcode = EPKG_OK;
	sqlite3 *sqlite = PRIV_GET(repo);

	if (sqlite == NULL)
		return (retcode);

	if (commit) {
		if (pkgdb_transaction_commit_sqlite(sqlite, NULL) != SQLITE_OK)
			retcode = EPKG_FATAL;
	}

	pkg_repo_binary_finalize_prstatements();
	sqlite3_close(sqlite);

	repo->priv = NULL;

	return (retcode);
}

int
pkg_repo_binary_access(struct pkg_repo *repo, unsigned mode)
{
	int			 ret = EPKG_OK;

	ret = pkgdb_check_access(mode,
		pkg_repo_binary_get_filename(repo));

	return (ret);
}
