Hintsdb sqlite: use transaction rather than file lock
[exim.git] / src / src / hintsdb.h
index b8e6744d688afacc945217be793433dcf98c7a04..c5a856abc97bdb8783fae93e7fa02145a927e139 100644 (file)
@@ -2,9 +2,10 @@
 *     Exim - an Internet mail transport agent    *
 *************************************************/
 
 *     Exim - an Internet mail transport agent    *
 *************************************************/
 
-/* Copyright (c) The Exim Maintainers 2020 - 2022 */
+/* Copyright (c) The Exim Maintainers 2020 - 2024 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* This header file contains macro definitions so that a variety of DBM
 libraries can be used by Exim. Nigel Metheringham provided the original set for
 
 /* This header file contains macro definitions so that a variety of DBM
 libraries can be used by Exim. Nigel Metheringham provided the original set for
@@ -15,13 +16,362 @@ from Pierre A. Humblet, so Exim could be made to work with Cygwin.
 
 For convenience, the definitions of the structures used in the various hints
 databases are also kept in this file, which is used by the maintenance
 
 For convenience, the definitions of the structures used in the various hints
 databases are also kept in this file, which is used by the maintenance
-utilities as well as the main Exim binary. */
+utilities as well as the main Exim binary.
+
+A key/value store is supported (only).  Keys are strings; values arbitrary
+binary blobs.
+
+The API is:
+  Functions:
+    exim_lockfile_needed       API semantics predicate
+    exim_dbopen
+    exim_dbclose
+    exim_dbget
+    exim_dbput
+    exim_dbputb                        non-overwriting put
+    exim_dbdel
+    exim_dbcreate_cursor
+    exim_dbscan                        get, and bump cursor
+    exim_dbdelete_cursor
+    exim_datum_init
+    exim_datum_size_get/set
+    exim_datum_data_get/set
+    exim_datum_free
+  Defines:
+    EXIM_DB            access handle
+    EXIM_CURSOR                datatype for cursor
+    EXIM_DATUM         datatype for "value"
+    EXIM_DBTYPE                text for logging & debuug
+
+Selection of the shim layer implementation, and backend, is by #defines.
+
+The users of this API are:
+  hintsdb interface    dbfn.c
+  hintsdb utilities    exim_dbutil.c and exim_dbmvuild.c
+  dbmdb lookup         lookups/dbmdb,c
+  autoreply transport  transports/autoreply.c
+
+Note that the dbmdb lookup use, bypassing the dbfn.c layer,
+means that no file-locking is done.
+XXX This feels like a layering violation; I don't see it commented on
+anywhere.
+
+Future: consider re-architecting to support caching of the open-handle
+for hintsdb uses (the dbmdb use gets that already).  This would need APIs
+for transaction locks.  Perhaps merge the implementation with the lookups
+layer, in some way, for the open-handle caching (since that manages closes
+required by Exim's process transisitions)?
+*/
 
 #ifndef HINTSDB_H
 #define HINTSDB_H
 
 
 
 #ifndef HINTSDB_H
 #define HINTSDB_H
 
 
-#if defined(USE_TDB)
+#ifdef USE_SQLITE
+# if defined(USE_DB) || defined(USE_GDBM) || defined(USE_TDB)
+#  error USE_SQLITE conflict with alternate definition
+# endif
+
+/* ********************* sqlite3 interface ************************ */
+
+# include <sqlite3.h>
+
+/* Basic DB type */
+# define EXIM_DB sqlite3
+
+# define EXIM_CURSOR int
+
+# /* The datum type used for queries */
+# define EXIM_DATUM blob
+
+/* Some text for messages */
+# define EXIM_DBTYPE "sqlite3"
+
+# /* Access functions */
+
+static inline BOOL
+exim_lockfile_needed(void)
+{
+return FALSE;  /* We do transaction; no extra locking needed */
+}
+
+/* EXIM_DBOPEN - return pointer to an EXIM_DB, NULL if failed */
+static inline EXIM_DB *
+exim_dbopen__(const uschar * name, const uschar * dirname, int flags,
+  unsigned mode)
+{
+EXIM_DB * dbp;
+int ret, sflags = flags & O_RDWR ? SQLITE_OPEN_READWRITE : SQLITE_OPEN_READONLY;
+if (flags & O_CREAT) sflags |= SQLITE_OPEN_CREATE;
+if ((ret = sqlite3_open_v2(CCS name, &dbp, sflags, NULL)) == SQLITE_OK)
+  {
+  sqlite3_busy_timeout(dbp, 5000);
+  ret = sqlite3_exec(dbp, "BEGIN TRANSACTION;", NULL, NULL, NULL);
+  if (ret == SQLITE_OK && flags & O_CREAT)
+    ret = sqlite3_exec(dbp,
+           "CREATE TABLE IF NOT EXISTS tbl (ky TEXT PRIMARY KEY, dat BLOB);",
+           NULL, NULL, NULL);
+  if (ret != SQLITE_OK)
+    sqlite3_close(dbp);
+  }
+//else
+//  fprintf(stderr, "sqlite3_open_v2: %s\n", sqlite3_errmsg(dbp));
+return ret == SQLITE_OK ? dbp : NULL;
+}
+
+/* EXIM_DBGET - returns TRUE if successful, FALSE otherwise */
+/* note we alloc'n'copy - the caller need not do so */
+/* result has a NUL appended, but the length is as per the DB */
+
+static inline BOOL
+exim_dbget__(EXIM_DB * dbp, const uschar * s, EXIM_DATUM * res)
+{
+sqlite3_stmt * statement;
+int ret;
+
+res->len = (size_t) -1;
+/* fprintf(stderr, "exim_dbget__(%s)\n", s); */
+if ((ret = sqlite3_prepare_v2(dbp, CCS s, -1, &statement, NULL)) != SQLITE_OK)
+  {
+/* fprintf(stderr, "prepare fail: %s\n", sqlite3_errmsg(dbp)); */
+  return FALSE;
+  }
+if (sqlite3_step(statement) != SQLITE_ROW)
+  {
+/* fprintf(stderr, "step fail: %s\n", sqlite3_errmsg(dbp)); */
+  sqlite3_finalize(statement);
+  return FALSE;
+  }
+
+res->len = sqlite3_column_bytes(statement, 0);
+# ifdef COMPILE_UTILITY
+res->data = malloc(res->len);
+# else
+res->data = store_get(res->len, GET_TAINTED);
+# endif
+memcpy(res->data, sqlite3_column_blob(statement, 0), res->len);
+res->data[res->len] = '\0';
+/* fprintf(stderr, "res %d bytes: '%.*s'\n", (int)res->len, (int)res->len, res->data); */
+sqlite3_finalize(statement);
+return TRUE;
+}
+
+static inline BOOL
+exim_dbget(EXIM_DB * dbp, EXIM_DATUM * key, EXIM_DATUM * res)
+{
+# define FMT "SELECT dat FROM tbl WHERE ky = '%.*s';"
+uschar * qry;
+int i;
+BOOL ret;
+
+# ifdef COMPILE_UTILITY
+/* fprintf(stderr, "exim_dbget(k len %d '%.*s')\n", (int)key->len, (int)key->len, key->data); */
+qry = malloc(i = snprintf(NULL, 0, FMT, (int) key->len, key->data));
+snprintf(CS qry, i, FMT, (int) key->len, key->data);
+ret = exim_dbget__(dbp, qry, res);
+free(qry);
+# else
+/* fprintf(stderr, "exim_dbget(k len %d '%.*s')\n", (int)key->len, (int)key->len, key->data); */
+qry = string_sprintf(FMT, (int) key->len, key->data);
+ret = exim_dbget__(dbp, qry, res);
+# endif
+
+return ret;
+# undef FMT
+}
+
+/**/
+# define EXIM_DBPUTB_OK  0
+# define EXIM_DBPUTB_DUP (-1)
+
+static inline int
+exim_s_dbp(EXIM_DB * dbp, EXIM_DATUM * key, EXIM_DATUM * data, const uschar * alt)
+{
+int hlen = data->len * 2, off = 0, res;
+# define FMT "INSERT OR %s INTO tbl (ky,dat) VALUES ('%.*s', X'%.*s');"
+# ifdef COMPILE_UTILITY
+uschar * hex = malloc(hlen+1);
+# else
+uschar * hex = store_get(hlen+1, data->data);
+# endif
+uschar * qry;
+
+for (const uschar * s = data->data, * t = s + data->len; s < t; s++, off += 2)
+  sprintf(CS hex + off, "%02X", *s);
+
+# ifdef COMPILE_UTILITY
+res = snprintf(CS hex, 0, FMT, alt, (int) key->len, key->data, hlen, hex);
+qry = malloc(res);
+snprintf(CS qry, res, FMT, alt, (int) key->len, key->data, hlen, hex);
+/* fprintf(stderr, "exim_s_dbp(%s)\n", qry); */
+res = sqlite3_exec(dbp, CS qry, NULL, NULL, NULL);
+free(qry);
+free(hex);
+# else
+qry = string_sprintf(FMT, alt, (int) key->len, key->data, hlen, hex);
+/* fprintf(stderr, "exim_s_dbp(%s)\n", qry); */
+res = sqlite3_exec(dbp, CS qry, NULL, NULL, NULL);
+/* fprintf(stderr, "exim_s_dbp res %d\n", res); */
+# endif
+
+if (res != SQLITE_OK)
+  fprintf(stderr, "sqlite3_exec: %s\n", sqlite3_errmsg(dbp));
+
+return res == SQLITE_OK ? EXIM_DBPUTB_OK : EXIM_DBPUTB_DUP;
+# undef FMT
+}
+
+/* EXIM_DBPUT - returns nothing useful, assumes replace mode */
+
+static inline int
+exim_dbput(EXIM_DB * dbp, EXIM_DATUM * key, EXIM_DATUM * data)
+{
+/* fprintf(stderr, "exim_dbput()\n"); */
+(void) exim_s_dbp(dbp, key, data, US"REPLACE");
+return 0;
+}
+
+/* EXIM_DBPUTB - non-overwriting for use by dbmbuild */
+
+/* Returns from EXIM_DBPUTB */
+
+static inline int
+exim_dbputb(EXIM_DB * dbp, EXIM_DATUM * key, EXIM_DATUM * data)
+{
+return exim_s_dbp(dbp, key, data, US"ABORT");
+}
+
+/* EXIM_DBDEL */
+static inline int
+exim_dbdel(EXIM_DB * dbp, EXIM_DATUM * key)
+{
+# define FMT "DELETE FROM tbl WHERE ky = '%.*s';"
+uschar * qry;
+int res;
+
+# ifdef COMPILE_UTILITY
+res = snprintf(NULL, 0, FMT, (int) key->len, key->data); /* res excludes nul */
+qry = malloc(res);
+snprintf(CS qry, res, FMT, (int) key->len, key->data);
+res = sqlite3_exec(dbp, CS qry, NULL, NULL, NULL);
+free(qry);
+# else
+qry = string_sprintf(FMT, (int) key->len, key->data);
+res = sqlite3_exec(dbp, CS qry, NULL, NULL, NULL);
+# endif
+
+return res;
+# undef FMT
+}
+
+
+/* EXIM_DBCREATE_CURSOR - initialize for scanning operation */
+/* Cursors are inefficiently emulated by repeating searches */
+
+static inline EXIM_CURSOR *
+exim_dbcreate_cursor(EXIM_DB * dbp)
+{
+# ifdef COMPILE_UTILITY
+EXIM_CURSOR * c = malloc(sizeof(int));
+# else
+EXIM_CURSOR * c = store_malloc(sizeof(int));
+# endif
+*c = 0;
+return c;
+}
+
+/* EXIM_DBSCAN */
+/* Note that we return the (next) key, not the record value */
+static inline BOOL
+exim_dbscan(EXIM_DB * dbp, EXIM_DATUM * key, EXIM_DATUM * res, BOOL first,
+  EXIM_CURSOR * cursor)
+{
+# define FMT "SELECT ky FROM tbl ORDER BY ky LIMIT 1 OFFSET %d;"
+uschar * qry;
+int i;
+BOOL ret;
+
+# ifdef COMPILE_UTILITY
+qry = malloc((i = snprintf(NULL, 0, FMT, *cursor)));
+snprintf(CS qry, i-1, FMT, *cursor);
+/* fprintf(stderr, "exim_dbscan(%s)\n", qry); */
+ret = exim_dbget__(dbp, qry, key);
+free(qry);
+/* fprintf(stderr, "exim_dbscan ret %c\n", ret ? 'T':'F'); */
+# else
+qry = string_sprintf(FMT, *cursor);
+/* fprintf(stderr, "exim_dbscan(%s)\n", qry); */
+ret = exim_dbget__(dbp, qry, key);
+/* fprintf(stderr, "exim_dbscan ret %c\n", ret ? 'T':'F'); */
+# endif
+if (ret) *cursor = *cursor + 1;
+return ret;
+# undef FMT
+}
+
+/* EXIM_DBDELETE_CURSOR - terminate scanning operation. */
+static inline void
+exim_dbdelete_cursor(EXIM_CURSOR * cursor)
+{
+# ifdef COMPILE_UTILITY
+free(cursor);
+# else
+store_free(cursor);
+# endif
+}
+
+
+/* EXIM_DBCLOSE */
+static void
+exim_dbclose__(EXIM_DB * dbp)
+{
+(void) sqlite3_exec(dbp, "COMMIT TRANSACTION;", NULL, NULL, NULL);
+sqlite3_close(dbp);
+}
+
+
+/* Datum access */
+
+static uschar *
+exim_datum_data_get(EXIM_DATUM * dp)
+{ return US dp->data; }
+static void
+exim_datum_data_set(EXIM_DATUM * dp, void * s)
+{ dp->data = s; }
+static unsigned
+exim_datum_size_get(EXIM_DATUM * dp)
+{ return dp->len; }
+static void
+exim_datum_size_set(EXIM_DATUM * dp, unsigned n)
+{ dp->len = n; }
+
+
+
+static inline void
+exim_datum_init(EXIM_DATUM * dp)
+{ dp->data = NULL; }                   /* compiler quietening */
+
+/* No free needed for a datum */
+
+static inline void
+exim_datum_free(EXIM_DATUM * dp)
+{ }
+
+/* size limit */
+
+# define EXIM_DB_RLIMIT        150
+
+
+
+
+
+
+#elif defined(USE_TDB)
+
+# if defined(USE_DB) || defined(USE_GDBM) || defined(USE_SQLITE)
+#  error USE_TDB conflict with alternate definition
+# endif
 
 /* ************************* tdb interface ************************ */
 /*XXX https://manpages.org/tdb/3 mentions concurrent writes.
 
 /* ************************* tdb interface ************************ */
 /*XXX https://manpages.org/tdb/3 mentions concurrent writes.
@@ -44,6 +394,12 @@ tdb_traverse to be called) */
 
 /* Access functions */
 
 
 /* Access functions */
 
+static inline BOOL
+exim_lockfile_needed(void)
+{
+return TRUE;
+}
+
 /* EXIM_DBOPEN - return pointer to an EXIM_DB, NULL if failed */
 static inline EXIM_DB *
 exim_dbopen__(const uschar * name, const uschar * dirname, int flags,
 /* EXIM_DBOPEN - return pointer to an EXIM_DB, NULL if failed */
 static inline EXIM_DB *
 exim_dbopen__(const uschar * name, const uschar * dirname, int flags,
@@ -84,7 +440,11 @@ exim_dbdel(EXIM_DB * dbp, EXIM_DATUM * key)
 static inline EXIM_CURSOR *
 exim_dbcreate_cursor(EXIM_DB * dbp)
 {
 static inline EXIM_CURSOR *
 exim_dbcreate_cursor(EXIM_DB * dbp)
 {
+# ifdef COMPILE_UTILITY
+EXIM_CURSOR * c = malloc(sizeof(TDB_DATA));
+# else
 EXIM_CURSOR * c = store_malloc(sizeof(TDB_DATA));
 EXIM_CURSOR * c = store_malloc(sizeof(TDB_DATA));
+# endif
 c->dptr = NULL;
 return c;
 }
 c->dptr = NULL;
 return c;
 }
@@ -156,6 +516,10 @@ d->dptr = NULL;
 
 #elif defined USE_DB
 
 
 #elif defined USE_DB
 
+# if defined(USE_TDB) || defined(USE_GDBM) || defined(USE_SQLITE)
+#  error USE_DB conflict with alternate definition
+# endif
+
 # include <db.h>
 
 /* 1.x did no locking
 # include <db.h>
 
 /* 1.x did no locking
@@ -211,7 +575,13 @@ log_write(0, LOG_MAIN, "Berkeley DB error: %s", msg);
 
 
 
 
 
 
-/* Access functions */
+/* Access functions (BDB 4.1+) */
+
+static inline BOOL
+exim_lockfile_needed(void)
+{
+return TRUE;
+}
 
 /* EXIM_DBOPEN - return pointer to an EXIM_DB, NULL if failed */
 /* The API changed for DB 4.1. - and we also starting using the "env" with a
 
 /* EXIM_DBOPEN - return pointer to an EXIM_DB, NULL if failed */
 /* The API changed for DB 4.1. - and we also starting using the "env" with a
@@ -234,8 +604,9 @@ if (db_create(&b, dbp, 0) == 0)
   {
   dbp->app_private = b;
   if (b->open(b, NULL, CS name, NULL,
   {
   dbp->app_private = b;
   if (b->open(b, NULL, CS name, NULL,
-             flags == O_RDONLY ? DB_UNKNOWN : DB_HASH,
-             flags == O_RDONLY ? DB_RDONLY : DB_CREATE,
+             flags & O_CREAT ? DB_HASH : DB_UNKNOWN,
+             flags & O_CREAT ? DB_CREATE
+             : flags & (O_WRONLY|O_RDWR) ? 0 : DB_RDONLY,
              mode) == 0
          )
     return dbp;
              mode) == 0
          )
     return dbp;
@@ -357,7 +728,13 @@ exim_datum_free(EXIM_DATUM * d)
 /* Some text for messages */
 #   define EXIM_DBTYPE   "db (v3/4)"
 
 /* Some text for messages */
 #   define EXIM_DBTYPE   "db (v3/4)"
 
-/* Access functions */
+/* Access functions (BDB 3/4) */
+
+static inline BOOL
+exim_lockfile_needed(void)
+{
+return TRUE;
+}
 
 /* EXIM_DBOPEN - return pointer to an EXIM_DB, NULL if failed */
 static inline EXIM_DB *
 
 /* EXIM_DBOPEN - return pointer to an EXIM_DB, NULL if failed */
 static inline EXIM_DB *
@@ -368,8 +745,9 @@ EXIM_DB * dbp;
 return db_create(&dbp, NULL, 0) == 0
   && (  dbp->set_errcall(dbp, dbfn_bdb_error_callback),
        dbp->open(dbp, CS name, NULL,
 return db_create(&dbp, NULL, 0) == 0
   && (  dbp->set_errcall(dbp, dbfn_bdb_error_callback),
        dbp->open(dbp, CS name, NULL,
-         flags == O_RDONLY ? DB_UNKNOWN : DB_HASH,
-         flags == O_RDONLY ? DB_RDONLY : DB_CREATE,
+         flags & O_CREAT ? DB_HASH : DB_UNKNOWN,
+         flags & O_CREAT ? DB_CREATE
+         : flags & (O_WRONLY|O_RDWR) ? 0 : DB_RDONLY,
          mode)
      ) == 0
   ? dbp : NULL;
          mode)
      ) == 0
   ? dbp : NULL;
@@ -479,7 +857,11 @@ exim_datum_free(EXIM_DATUM * d)
 /********************* gdbm interface definitions **********************/
 
 #elif defined USE_GDBM
 /********************* gdbm interface definitions **********************/
 
 #elif defined USE_GDBM
-/*XXX TODO: exim's locfile not needed */
+/*XXX TODO: exim's lockfile not needed? */
+
+# if defined(USE_TDB) || defined(USE_DB) || defined(USE_SQLITE)
+#  error USE_GDBM conflict with alternate definition
+# endif
 
 # include <gdbm.h>
 
 
 # include <gdbm.h>
 
@@ -499,7 +881,13 @@ typedef struct {
 
 # define EXIM_DBTYPE "gdbm"
 
 
 # define EXIM_DBTYPE "gdbm"
 
-/* Access functions */
+/* Access functions (gdbm) */
+
+static inline BOOL
+exim_lockfile_needed(void)
+{
+return TRUE;
+}
 
 /* EXIM_DBOPEN - return pointer to an EXIM_DB, NULL if failed */
 static inline EXIM_DB *
 
 /* EXIM_DBOPEN - return pointer to an EXIM_DB, NULL if failed */
 static inline EXIM_DB *
@@ -512,8 +900,7 @@ if (dbp)
   dbp->lkey.dptr = NULL;
   dbp->gdbm = gdbm_open(CS name, 0,
     flags & O_CREAT ? GDBM_WRCREAT
   dbp->lkey.dptr = NULL;
   dbp->gdbm = gdbm_open(CS name, 0,
     flags & O_CREAT ? GDBM_WRCREAT
-    : flags & (O_RDWR|O_WRONLY) ? GDBM_WRITER
-    : GDBM_READER,
+    : flags & (O_RDWR|O_WRONLY) ? GDBM_WRITER : GDBM_READER,
     mode, 0);
   if (dbp->gdbm) return dbp;
   free(dbp);
     mode, 0);
   if (dbp->gdbm) return dbp;
   free(dbp);
@@ -608,7 +995,7 @@ static inline void
 exim_datum_free(EXIM_DATUM * d)
 { free(d->dptr); }
 
 exim_datum_free(EXIM_DATUM * d)
 { free(d->dptr); }
 
-/* size limit */
+/* size limit. GDBM is int-max limited, but we want to be less silly */
 
 # define EXIM_DB_RLIMIT        150
 
 
 # define EXIM_DB_RLIMIT        150
 
@@ -619,8 +1006,8 @@ exim_datum_free(EXIM_DATUM * d)
 
 
 
 
 
 
-/* If none of USE_DB, USG_GDBM, or USE_TDB are set, the default is the NDBM
-interface */
+/* If none of USE_DB, USG_GDBM, USE_SQLITE or USE_TDB are set,
+the default is the NDBM interface (which seems to be a wrapper for GDBM) */
 
 
 /********************* ndbm interface definitions **********************/
 
 
 /********************* ndbm interface definitions **********************/
@@ -640,7 +1027,13 @@ interface */
 
 # define EXIM_DBTYPE "ndbm"
 
 
 # define EXIM_DBTYPE "ndbm"
 
-/* Access functions */
+/* Access functions (ndbm) */
+
+static inline BOOL
+exim_lockfile_needed(void)
+{
+return TRUE;
+}
 
 /* EXIM_DBOPEN - returns a EXIM_DB *, NULL if failed */
 /* Check that the name given is not present. This catches
 
 /* EXIM_DBOPEN - returns a EXIM_DB *, NULL if failed */
 /* Check that the name given is not present. This catches
@@ -744,7 +1137,7 @@ exim_datum_free(EXIM_DATUM * d)
 
 # define EXIM_DB_RLIMIT        150
 
 
 # define EXIM_DB_RLIMIT        150
 
-#endif /* USE_GDBM */
+#endif /* !USE_GDBM */
 
 
 
 
 
 
@@ -808,3 +1201,5 @@ exim_dbclose__(dbp);
 
 #endif /* whole file */
 /* End of hintsdb.h */
 
 #endif /* whole file */
 /* End of hintsdb.h */
+/* vi: aw ai sw=2
+*/