/*
 * Copyright (c) 2018 SUSE Inc.
 *
 * This program is licensed under the BSD license, read LICENSE.BSD
 * for further information
 */

/*
 * repo_rpmdb_bdb.h
 *
 * Use BerkeleyDB to access the rpm database
 *
 */


#if !defined(DB_CREATE) && !defined(ENABLE_RPMDB_LIBRPM)
# if defined(SUSE) || defined(HAVE_RPM_DB_H)
#  include <rpm/db.h>
# else
#  include <db.h>
# endif
#endif

#ifdef RPM5
# define RPM_INDEX_SIZE 4	/* just the rpmdbid */
#else
# define RPM_INDEX_SIZE 8	/* rpmdbid + array index */
#endif


/******************************************************************/
/*  Rpm Database stuff
 */

struct rpmdbstate {
  Pool *pool;
  char *rootdir;

  RpmHead *rpmhead;	/* header storage space */
  int rpmheadsize;

  int dbenvopened;	/* database environment opened */
  int pkgdbopened;	/* package database openend */
  int is_ostree;	/* read-only db that lives in /usr/share/rpm */

  DB_ENV *dbenv;	/* database environment */
  DB *db;		/* packages database */
  int byteswapped;	/* endianess of packages database */
  DBC *dbc;		/* iterator over packages database */
};


static int
stat_database(struct rpmdbstate *state, char *dbname, struct stat *statbuf, int seterror)
{
  char *dbpath;
  dbpath = solv_dupjoin(state->rootdir, state->is_ostree ? "/usr/share/rpm/" : "/var/lib/rpm/", dbname);
  if (stat(dbpath, statbuf))
    {
      if (seterror)
        pool_error(state->pool, -1, "%s: %s", dbpath, strerror(errno));
      free(dbpath);
      return -1;
    }
  free(dbpath);
  return 0;
}


static inline Id
db2rpmdbid(unsigned char *db, int byteswapped)
{
#ifdef RPM5
  return db[0] << 24 | db[1] << 16 | db[2] << 8 | db[3];
#else
# if defined(WORDS_BIGENDIAN)
  if (!byteswapped)
# else
  if (byteswapped)
# endif
    return db[0] << 24 | db[1] << 16 | db[2] << 8 | db[3];
  else
    return db[3] << 24 | db[2] << 16 | db[1] << 8 | db[0];
#endif
}

static inline void
rpmdbid2db(unsigned char *db, Id id, int byteswapped)
{
#ifdef RPM5
  db[0] = id >> 24, db[1] = id >> 16, db[2] = id >> 8, db[3] = id;
#else
# if defined(WORDS_BIGENDIAN)
  if (!byteswapped)
# else
  if (byteswapped)
# endif
    db[0] = id >> 24, db[1] = id >> 16, db[2] = id >> 8, db[3] = id;
  else
    db[3] = id >> 24, db[2] = id >> 16, db[1] = id >> 8, db[0] = id;
#endif
}

#if defined(FEDORA) || defined(MAGEIA)
static int
serialize_dbenv_ops(struct rpmdbstate *state)
{
  char *lpath;
  mode_t oldmask;
  int fd;
  struct flock fl;

  lpath = solv_dupjoin(state->rootdir, "/var/lib/rpm/.dbenv.lock", 0);
  oldmask = umask(022);
  fd = open(lpath, (O_RDWR|O_CREAT), 0644);
  free(lpath);
  umask(oldmask);
  if (fd < 0)
    return -1;
  memset(&fl, 0, sizeof(fl));
  fl.l_type = F_WRLCK;
  fl.l_whence = SEEK_SET;
  for (;;)
    {
      if (fcntl(fd, F_SETLKW, &fl) != -1)
	return fd;
      if (errno != EINTR)
	break;
    }
  close(fd);
  return -1;
}

#endif

/* should look in /usr/lib/rpm/macros instead, but we want speed... */
static int
opendbenv(struct rpmdbstate *state)
{
  const char *rootdir = state->rootdir;
  char *dbpath;
  DB_ENV *dbenv = 0;
  int r;

  if (db_env_create(&dbenv, 0))
    return pool_error(state->pool, 0, "db_env_create: %s", strerror(errno));
#if (defined(FEDORA) || defined(MAGEIA)) && (DB_VERSION_MAJOR >= 5 || (DB_VERSION_MAJOR == 4 && DB_VERSION_MINOR >= 5))
  dbenv->set_thread_count(dbenv, 8);
#endif
  dbpath = solv_dupjoin(rootdir, "/var/lib/rpm", 0);
  if (access(dbpath, W_OK) == -1)
    {
      free(dbpath);
      dbpath = solv_dupjoin(rootdir, "/usr/share/rpm/Packages", 0);
      if (access(dbpath, R_OK) == 0)
	state->is_ostree = 1;
      free(dbpath);
      dbpath = solv_dupjoin(rootdir, state->is_ostree ? "/usr/share/rpm" : "/var/lib/rpm", 0);
      r = dbenv->open(dbenv, dbpath, DB_CREATE|DB_PRIVATE|DB_INIT_MPOOL, 0);
    }
  else
    {
#if defined(FEDORA) || defined(MAGEIA)
      int serialize_fd = serialize_dbenv_ops(state);
      int eflags = DB_CREATE|DB_INIT_CDB|DB_INIT_MPOOL;
      r = dbenv->open(dbenv, dbpath, eflags, 0644);
      /* see rpm commit 2822ccbcdf3e898b960fafb23c4d571e26cef0a4 */
      if (r == DB_VERSION_MISMATCH)
	{
	  eflags |= DB_PRIVATE;
	  dbenv->errx(dbenv, "warning: DB_VERSION_MISMATCH, retrying with DB_PRIVATE");
	  r = dbenv->open(dbenv, dbpath, eflags, 0644);
	}
      if (serialize_fd >= 0)
	close(serialize_fd);
#else
      r = dbenv->open(dbenv, dbpath, DB_CREATE|DB_PRIVATE|DB_INIT_MPOOL, 0);
#endif
    }
  if (r)
    {
      pool_error(state->pool, 0, "dbenv->open: %s", strerror(errno));
      free(dbpath);
      dbenv->close(dbenv, 0);
      return 0;
    }
  free(dbpath);
  state->dbenv = dbenv;
  state->dbenvopened = 1;
  return 1;
}

static void
closedbenv(struct rpmdbstate *state)
{
#if defined(FEDORA) || defined(MAGEIA)
  uint32_t eflags = 0;
#endif

  if (!state->dbenv)
    return;
#if defined(FEDORA) || defined(MAGEIA)
  (void)state->dbenv->get_open_flags(state->dbenv, &eflags);
  if (!(eflags & DB_PRIVATE))
    {
      int serialize_fd = serialize_dbenv_ops(state);
      state->dbenv->close(state->dbenv, 0);
      if (serialize_fd >= 0)
	close(serialize_fd);
    }
  else
    state->dbenv->close(state->dbenv, 0);
#else
  state->dbenv->close(state->dbenv, 0);
#endif
  state->dbenv = 0;
  state->dbenvopened = 0;
}

static int
openpkgdb(struct rpmdbstate *state)
{
  if (state->pkgdbopened)
    return state->pkgdbopened > 0 ? 1 : 0;
  state->pkgdbopened = -1;
  if (state->dbenvopened != 1 && !opendbenv(state))
    return 0;
  if (db_create(&state->db, state->dbenv, 0))
    {
      pool_error(state->pool, 0, "db_create: %s", strerror(errno));
      state->db = 0;
      closedbenv(state);
      return 0;
    }
  if (state->db->open(state->db, 0, "Packages", 0, DB_UNKNOWN, DB_RDONLY, 0664))
    {
      pool_error(state->pool, 0, "db->open Packages: %s", strerror(errno));
      state->db->close(state->db, 0);
      state->db = 0;
      closedbenv(state);
      return 0;
    }
  if (state->db->get_byteswapped(state->db, &state->byteswapped))
    {
      pool_error(state->pool, 0, "db->get_byteswapped: %s", strerror(errno));
      state->db->close(state->db, 0);
      state->db = 0;
      closedbenv(state);
      return 0;
    }
  state->pkgdbopened = 1;
  return 1;
}

static void
closepkgdb(struct rpmdbstate *state)
{
  if (!state->db)
    return;
  state->db->close(state->db, 0);
  state->db = 0;
  state->pkgdbopened = 0;
}

/* get the rpmdbids of all installed packages from the Name index database.
 * This is much faster then querying the big Packages database */
static struct rpmdbentry *
getinstalledrpmdbids(struct rpmdbstate *state, const char *index, const char *match, int *nentriesp, char **namedatap, int keep_gpg_pubkey)
{
  DB_ENV *dbenv = 0;
  DB *db = 0;
  DBC *dbc = 0;
  int byteswapped;
  DBT dbkey;
  DBT dbdata;
  unsigned char *dp;
  int dl;
  Id nameoff;

  char *namedata = 0;
  int namedatal = 0;
  struct rpmdbentry *entries = 0;
  int nentries = 0;

  *nentriesp = 0;
  if (namedatap)
    *namedatap = 0;

  if (state->dbenvopened != 1 && !opendbenv(state))
    return 0;
  dbenv = state->dbenv;
  if (db_create(&db, dbenv, 0))
    {
      pool_error(state->pool, 0, "db_create: %s", strerror(errno));
      return 0;
    }
  if (db->open(db, 0, index, 0, DB_UNKNOWN, DB_RDONLY, 0664))
    {
      pool_error(state->pool, 0, "db->open %s: %s", index, strerror(errno));
      db->close(db, 0);
      return 0;
    }
  if (db->get_byteswapped(db, &byteswapped))
    {
      pool_error(state->pool, 0, "db->get_byteswapped: %s", strerror(errno));
      db->close(db, 0);
      return 0;
    }
  if (db->cursor(db, NULL, &dbc, 0))
    {
      pool_error(state->pool, 0, "db->cursor: %s", strerror(errno));
      db->close(db, 0);
      return 0;
    }
  memset(&dbkey, 0, sizeof(dbkey));
  memset(&dbdata, 0, sizeof(dbdata));
  if (match)
    {
      dbkey.data = (void *)match;
      dbkey.size = strlen(match);
    }
  while (dbc->c_get(dbc, &dbkey, &dbdata, match ? DB_SET : DB_NEXT) == 0)
    {
      if (!match && !keep_gpg_pubkey && dbkey.size == 10 && !memcmp(dbkey.data, "gpg-pubkey", 10))
	continue;
      dl = dbdata.size;
      dp = dbdata.data;
      nameoff = namedatal;
      if (namedatap)
	{
	  namedata = solv_extend(namedata, namedatal, dbkey.size + 1, 1, NAMEDATA_BLOCK);
	  memcpy(namedata + namedatal, dbkey.data, dbkey.size);
	  namedata[namedatal + dbkey.size] = 0;
	  namedatal += dbkey.size + 1;
	}
      while(dl >= RPM_INDEX_SIZE)
	{
	  entries = solv_extend(entries, nentries, 1, sizeof(*entries), ENTRIES_BLOCK);
	  entries[nentries].rpmdbid = db2rpmdbid(dp, byteswapped);
	  entries[nentries].nameoff = nameoff;
	  nentries++;
	  dp += RPM_INDEX_SIZE;
	  dl -= RPM_INDEX_SIZE;
	}
      if (match)
	break;
    }
  dbc->c_close(dbc);
  db->close(db, 0);
  /* make sure that enteries is != 0 if there was no error */
  if (!entries)
    entries = solv_extend(entries, 1, 1, sizeof(*entries), ENTRIES_BLOCK);
  *nentriesp = nentries;
  if (namedatap)
    *namedatap = namedata;
  return entries;
}

/* common code, return dbid on success, -1 on error */
static int
getrpm_dbdata(struct rpmdbstate *state, DBT *dbdata, int dbid)
{
  unsigned int dsize, cnt, l;
  RpmHead *rpmhead;

  if (dbdata->size < 8)
    return pool_error(state->pool, -1, "corrupt rpm database (size)");
  cnt = getu32((const unsigned char *)dbdata->data);
  dsize = getu32((const unsigned char *)dbdata->data + 4);
  if (cnt >= MAX_HDR_CNT || dsize >= MAX_HDR_DSIZE)
    return pool_error(state->pool, -1, "corrupt rpm database (cnt/dcnt)");
  l = cnt * 16 + dsize;
  if (8 + l > dbdata->size)
    return pool_error(state->pool, -1, "corrupt rpm database (data size)");
  if (l + 1 > state->rpmheadsize)
    {
      state->rpmheadsize = l + 128;
      state->rpmhead = solv_realloc(state->rpmhead, sizeof(*rpmhead) + state->rpmheadsize);
    }
  rpmhead = state->rpmhead;
  rpmhead->cnt = cnt;
  rpmhead->dcnt = dsize;
  memcpy(rpmhead->data, (unsigned char *)dbdata->data + 8, l);
  rpmhead->data[l] = 0;
  rpmhead->dp = rpmhead->data + cnt * 16;
  return dbid;
}

/* retrive header by rpmdbid, returns 0 if not found, -1 on error */
static int
getrpm_dbid(struct rpmdbstate *state, Id dbid)
{
  unsigned char buf[4];
  DBT dbkey;
  DBT dbdata;

  if (dbid <= 0)
    return pool_error(state->pool, -1, "illegal rpmdbid %d", dbid);
  if (state->pkgdbopened != 1 && !openpkgdb(state))
    return -1;
  rpmdbid2db(buf, dbid, state->byteswapped);
  memset(&dbkey, 0, sizeof(dbkey));
  memset(&dbdata, 0, sizeof(dbdata));
  dbkey.data = buf;
  dbkey.size = 4;
  dbdata.data = 0;
  dbdata.size = 0;
  if (state->db->get(state->db, NULL, &dbkey, &dbdata, 0))
    return 0;
  return getrpm_dbdata(state, &dbdata, dbid);
}

static int
count_headers(struct rpmdbstate *state)
{
  Pool *pool = state->pool;
  struct stat statbuf;
  DB *db = 0;
  DBC *dbc = 0;
  int count = 0;
  DBT dbkey;
  DBT dbdata;

  if (stat_database(state, "Name", &statbuf, 0))
    return 0;
  memset(&dbkey, 0, sizeof(dbkey));
  memset(&dbdata, 0, sizeof(dbdata));
  if (db_create(&db, state->dbenv, 0))
    {
      pool_error(pool, 0, "db_create: %s", strerror(errno));
      return 0;
    }
  if (db->open(db, 0, "Name", 0, DB_UNKNOWN, DB_RDONLY, 0664))
    {
      pool_error(pool, 0, "db->open Name: %s", strerror(errno));
      db->close(db, 0);
      return 0;
    }
  if (db->cursor(db, NULL, &dbc, 0))
    {
      db->close(db, 0);
      pool_error(pool, 0, "db->cursor: %s", strerror(errno));
      return 0;
    }
  while (dbc->c_get(dbc, &dbkey, &dbdata, DB_NEXT) == 0)
    count += dbdata.size / RPM_INDEX_SIZE;
  dbc->c_close(dbc);
  db->close(db, 0);
  return count;
}

static int
pkgdb_cursor_open(struct rpmdbstate *state)
{
  if (state->db->cursor(state->db, NULL, &state->dbc, 0))
    return pool_error(state->pool, -1, "db->cursor failed");
  return 0;
}

static void
pkgdb_cursor_close(struct rpmdbstate *state)
{
  state->dbc->c_close(state->dbc);
  state->dbc = 0;
}

/* retrive header by berkeleydb cursor, returns 0 on EOF, -1 on error */
static Id
pkgdb_cursor_getrpm(struct rpmdbstate *state)
{
  DBT dbkey;
  DBT dbdata;
  Id dbid;

  memset(&dbkey, 0, sizeof(dbkey));
  memset(&dbdata, 0, sizeof(dbdata));
  while (state->dbc->c_get(state->dbc, &dbkey, &dbdata, DB_NEXT) == 0)
    {
      if (dbkey.size != 4)
	return pool_error(state->pool, -1, "corrupt Packages database (key size)");
      dbid = db2rpmdbid(dbkey.data, state->byteswapped);
      if (dbid)		/* ignore join key */
        return getrpm_dbdata(state, &dbdata, dbid);
    }
  return 0;	/* no more entries */
}

