/** @file
 * @brief Database factories for non-remote databases.
 */
/* Copyright 2002-2024 Olly Betts
 * Copyright 2008 Lemur Consulting Ltd
 *
 * 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
 * <https://www.gnu.org/licenses/>.
 */

#include <config.h>

#include "xapian/dbfactory.h"

#include "xapian/constants.h"
#include "xapian/database.h"
#include "xapian/error.h"
#include "xapian/version.h" // For XAPIAN_HAS_XXX_BACKEND.

#include "backends.h"
#include "databasehelpers.h"
#include "debuglog.h"
#include "filetests.h"
#include "fileutils.h"
#include "posixy_wrapper.h"
#include "str.h"

#include <cerrno>

#ifdef XAPIAN_HAS_GLASS_BACKEND
# include "glass/glass_database.h"
#endif
#include "glass/glass_defs.h"
#ifdef XAPIAN_HAS_HONEY_BACKEND
# include "honey/honey_database.h"
#endif
#include "honey/honey_defs.h"
#ifdef XAPIAN_HAS_INMEMORY_BACKEND
# include "inmemory/inmemory_database.h"
#endif
// Even if none of the above get included, we still need a definition of
// Database::Internal.
#include "backends/databaseinternal.h"

#include <string>

using namespace std;

namespace Xapian {

static void
open_stub(Database& db, string_view file)
{
    read_stub_file(file,
		   [&db](string_view path) {
		       db.add_database(Database(path));
		   },
		   [&db](string_view path) {
#ifdef XAPIAN_HAS_GLASS_BACKEND
		       db.add_database(Database(new GlassDatabase(path)));
#else
		       (void)path;
#endif
		   },
		   [&db](string_view path) {
#ifdef XAPIAN_HAS_HONEY_BACKEND
		       db.add_database(Database(new HoneyDatabase(path)));
#else
		       (void)path;
#endif
		   },
		   [&db](string_view prog, string_view args) {
#ifdef XAPIAN_HAS_REMOTE_BACKEND
		       db.add_database(Remote::open(prog, args));
#else
		       (void)prog;
		       (void)args;
#endif
		   },
		   [&db](string_view host, unsigned port) {
#ifdef XAPIAN_HAS_REMOTE_BACKEND
		       db.add_database(Remote::open(host, port));
#else
		       (void)host;
		       (void)port;
#endif
		   },
		   [&db]() {
#ifdef XAPIAN_HAS_INMEMORY_BACKEND
		       db.add_database(Database(""sv, DB_BACKEND_INMEMORY));
#endif
		   });

    // Allowing a stub database with no databases listed allows things like
    // a "search all databases" feature to be implemented by generating a
    // stub database file without having to special case there not being any
    // databases yet.
    //
    // 1.0.x threw DatabaseOpeningError here, but with a "Bad line" message
    // with the line number just past the end of the file, which was a bit odd.
}

static void
open_stub(WritableDatabase& db, string_view file, int flags)
{
    read_stub_file(file,
		   [&db, flags](string_view path) {
		       db.add_database(WritableDatabase(path, flags));
		   },
		   [&db, &flags](string_view path) {
		       flags |= DB_BACKEND_GLASS;
		       db.add_database(WritableDatabase(path, flags));
		   },
		   [](string_view) {
		       auto msg = "Honey databases don't support writing";
		       throw Xapian::DatabaseOpeningError(msg);
		   },
		   [&db, flags](string_view prog, string_view args) {
#ifdef XAPIAN_HAS_REMOTE_BACKEND
		       db.add_database(Remote::open_writable(prog, args,
							     0, flags));
#else
		       (void)prog;
		       (void)args;
#endif
		   },
		   [&db, flags](string_view host, unsigned port) {
#ifdef XAPIAN_HAS_REMOTE_BACKEND
		       db.add_database(Remote::open_writable(host, port,
							     0, 10000, flags));
#else
		       (void)host;
		       (void)port;
#endif
		   },
		   [&db]() {
		       db.add_database(WritableDatabase(""sv,
							DB_BACKEND_INMEMORY));
		   });

    if (db.internal->size() == 0) {
	throw DatabaseOpeningError(string{file} + ": No databases listed");
    }
}

Database::Database(string_view path, int flags)
    : Database()
{
    LOGCALL_CTOR(API, "Database", path|flags);

    int type = flags & DB_BACKEND_MASK_;
    switch (type) {
	case DB_BACKEND_GLASS:
#ifdef XAPIAN_HAS_GLASS_BACKEND
	    internal = new GlassDatabase(path);
	    return;
#else
	    throw FeatureUnavailableError("Glass backend disabled");
#endif
	case DB_BACKEND_HONEY:
#ifdef XAPIAN_HAS_HONEY_BACKEND
	    internal = new HoneyDatabase(path);
	    return;
#else
	    throw FeatureUnavailableError("Honey backend disabled");
#endif
	case DB_BACKEND_STUB:
	    open_stub(*this, path);
	    return;
	case DB_BACKEND_INMEMORY:
#ifdef XAPIAN_HAS_INMEMORY_BACKEND
	    internal = new InMemoryDatabase();
	    return;
#else
	    throw FeatureUnavailableError("Inmemory backend disabled");
#endif
    }

    string filename{path};
    struct stat statbuf;
    if (stat(filename.c_str(), &statbuf) == -1) {
	if (errno == ENOENT) {
	    throw DatabaseNotFoundError("Couldn't stat '" + filename + "'",
					errno);
	} else {
	    throw DatabaseOpeningError("Couldn't stat '" + filename + "'",
				       errno);
	}
    }

    if (S_ISREG(statbuf.st_mode)) {
	// Could be a stub database file, or a single file glass database.

	// Initialise to avoid bogus warning from GCC 4.9.2 with -Os.
	int fd = -1;
	switch (test_if_single_file_db(statbuf, filename, &fd)) {
	    case BACKEND_GLASS:
#ifdef XAPIAN_HAS_GLASS_BACKEND
		// Single file glass format.
		internal = new GlassDatabase(fd);
		return;
#else
		throw FeatureUnavailableError("Glass backend disabled");
#endif
	    case BACKEND_HONEY:
#ifdef XAPIAN_HAS_HONEY_BACKEND
		// Single file honey format.
		internal = new HoneyDatabase(fd);
		return;
#else
		throw FeatureUnavailableError("Honey backend disabled");
#endif
	}

	open_stub(*this, path);
	return;
    }

    if (rare(!S_ISDIR(statbuf.st_mode))) {
	throw DatabaseOpeningError("Not a regular file or directory: "
				   "'" + filename + "'");
    }

#ifdef XAPIAN_HAS_GLASS_BACKEND
    filename += "/iamglass";
    if (file_exists(filename)) {
	internal = new GlassDatabase(path);
	return;
    }
#endif

#ifdef XAPIAN_HAS_HONEY_BACKEND
    filename.resize(path.size());
    filename += "/iamhoney";
    if (file_exists(filename)) {
	internal = new HoneyDatabase(path);
	return;
    }
#endif

    // Check for "stub directories".
    filename.resize(path.size());
    filename += "/XAPIANDB";
    if (usual(file_exists(filename))) {
	open_stub(*this, filename);
	return;
    }

#ifndef XAPIAN_HAS_GLASS_BACKEND
    filename.resize(path.size());
    filename += "/iamglass";
    if (file_exists(filename)) {
	throw FeatureUnavailableError("Glass backend disabled");
    }
#endif
#ifndef XAPIAN_HAS_HONEY_BACKEND
    filename.resize(path.size());
    filename += "/iamhoney";
    if (file_exists(filename)) {
	throw FeatureUnavailableError("Honey backend disabled");
    }
#endif
    filename.resize(path.size());
    filename += "/iamchert";
    if (file_exists(filename)) {
	throw FeatureUnavailableError("Chert backend no longer supported");
    }
    filename.resize(path.size());
    filename += "/iamflint";
    if (file_exists(filename)) {
	throw FeatureUnavailableError("Flint backend no longer supported");
    }

    throw DatabaseNotFoundError("Couldn't detect type of database");
}

/** Helper factory function.
 *
 *  This allows us to initialise Database::internal via the constructor's
 *  initialiser list, which we want to be able to do as Database::internal
 *  is an intrusive_ptr_nonnull, so we can't set it to NULL in the initialiser
 *  list and then fill it in later in the constructor body.
 */
static Database::Internal*
database_factory(int fd, int flags)
{
    if (rare(fd < 0))
	throw InvalidArgumentError("fd < 0", EBADF);

#if defined XAPIAN_HAS_GLASS_BACKEND || defined XAPIAN_HAS_HONEY_BACKEND
    int type = flags & DB_BACKEND_MASK_;
    if (type == 0) {
	switch (test_if_single_file_db(fd)) {
	  case BACKEND_GLASS:
	    type = DB_BACKEND_GLASS;
	    break;
	  case BACKEND_HONEY:
	    type = DB_BACKEND_HONEY;
	    break;
	}
    }
    switch (type) {
#ifdef XAPIAN_HAS_GLASS_BACKEND
	case DB_BACKEND_GLASS:
	    return new GlassDatabase(fd);
#endif
#ifdef XAPIAN_HAS_HONEY_BACKEND
	case DB_BACKEND_HONEY:
	    return new HoneyDatabase(fd);
#endif
    }
#endif

    (void)::close(fd);
    throw DatabaseOpeningError("Couldn't detect type of database fd");
}

Database::Database(int fd, int flags)
    : internal(database_factory(fd, flags))
{
    LOGCALL_CTOR(API, "Database", fd|flags);
}

#if defined XAPIAN_HAS_GLASS_BACKEND
#define HAVE_DISK_BACKEND
#endif

WritableDatabase::WritableDatabase(std::string_view path,
				   int flags,
				   int block_size)
    : Database()
{
    LOGCALL_CTOR(API, "WritableDatabase", path|flags|block_size);
    // Avoid warning if all disk-based backends are disabled.
    (void)block_size;
    string filename{path};
    int type = flags & DB_BACKEND_MASK_;
    // Clear the backend bits, so we just pass on other flags to open_stub, etc.
    flags &= ~DB_BACKEND_MASK_;
    if (type == 0) {
	struct stat statbuf;
	if (stat(filename.c_str(), &statbuf) == -1) {
	    // ENOENT probably just means that we need to create the directory.
	    if (errno != ENOENT)
		throw DatabaseOpeningError("Couldn't stat '" + filename + "'",
					   errno);
	} else {
	    // File or directory already exists.

	    if (S_ISREG(statbuf.st_mode)) {
		// The path is a file, so assume it is a stub database file.
		open_stub(*this, path, flags);
		return;
	    }

	    if (rare(!S_ISDIR(statbuf.st_mode))) {
		throw DatabaseOpeningError("Not a regular file or directory: "
					   "'" + filename + "'");
	    }

	    filename += "/iamglass";
	    if (file_exists(filename)) {
		// Existing glass DB.
#ifdef XAPIAN_HAS_GLASS_BACKEND
		type = DB_BACKEND_GLASS;
#else
		throw FeatureUnavailableError("Glass backend disabled");
#endif
	    }

	    filename.resize(path.size());
	    filename += "/iamhoney";
	    if (file_exists(filename)) {
		// Existing honey DB.
		throw InvalidOperationError("Honey backend doesn't support "
					    "updating existing databases");
	    }

	    filename.resize(path.size());
	    filename += "/iamchert";
	    if (file_exists(filename)) {
		// Existing chert DB.
		throw FeatureUnavailableError("Chert backend no longer supported");
	    }

	    filename.resize(path.size());
	    filename += "/iamflint";
	    if (file_exists(filename)) {
		// Existing flint DB.
		throw FeatureUnavailableError("Flint backend no longer supported");
	    }

	    // Check for "stub directories".
	    filename.resize(path.size());
	    filename += "/XAPIANDB";
	    if (usual(file_exists(filename))) {
		open_stub(*this, filename, flags);
		return;
	    }
	}
    }

    switch (type) {
	case DB_BACKEND_STUB:
	    open_stub(*this, path, flags);
	    return;
	case 0:
	    // Fall through to first enabled case, so order the remaining cases
	    // by preference.
#ifdef XAPIAN_HAS_GLASS_BACKEND
	case DB_BACKEND_GLASS:
	    internal = new GlassWritableDatabase(path, flags, block_size);
	    return;
#endif
	case DB_BACKEND_HONEY:
	    throw InvalidArgumentError("Honey backend doesn't support "
				       "updating existing databases");
	case DB_BACKEND_INMEMORY:
#ifdef XAPIAN_HAS_INMEMORY_BACKEND
	    internal = new InMemoryDatabase();
	    return;
#else
	    throw FeatureUnavailableError("Inmemory backend disabled");
#endif
    }
#ifndef HAVE_DISK_BACKEND
    throw FeatureUnavailableError("No disk-based writable backend is enabled");
#endif
}

}
