1 /*        $NetBSD: dict_cdb.c,v 1.4 2025/02/25 19:15:51 christos Exp $          */
2 
3 /*++
4 /* NAME
5 /*        dict_cdb 3
6 /* SUMMARY
7 /*        dictionary manager interface to CDB files
8 /* SYNOPSIS
9 /*        #include <dict_cdb.h>
10 /*
11 /*        DICT      *dict_cdb_open(path, open_flags, dict_flags)
12 /*        const char *path;
13 /*        int       open_flags;
14 /*        int       dict_flags;
15 /*
16 /* DESCRIPTION
17 /*        dict_cdb_open() opens the specified CDB database.  The result is
18 /*        a pointer to a structure that can be used to access the dictionary
19 /*        using the generic methods documented in dict_open(3).
20 /*
21 /*        Arguments:
22 /* .IP path
23 /*        The database pathname, not including the ".cdb" suffix.
24 /* .IP open_flags
25 /*        Flags passed to open(). Specify O_RDONLY or O_WRONLY|O_CREAT|O_TRUNC.
26 /* .IP dict_flags
27 /*        Flags used by the dictionary interface.
28 /* SEE ALSO
29 /*        dict(3) generic dictionary manager
30 /* DIAGNOSTICS
31 /*        Fatal errors: cannot open file, write error, out of memory.
32 /* LICENSE
33 /* .ad
34 /* .fi
35 /*        The Secure Mailer license must be distributed with this software.
36 /* AUTHOR(S)
37 /*        Michael Tokarev <mjt@tls.msk.ru> based on dict_db.c by
38 /*        Wietse Venema
39 /*        IBM T.J. Watson Research
40 /*        P.O. Box 704
41 /*        Yorktown Heights, NY 10598, USA
42 /*--*/
43 
44 #include "sys_defs.h"
45 
46 /* System library. */
47 
48 #include <sys/stat.h>
49 #include <limits.h>
50 #include <string.h>
51 #include <unistd.h>
52 #include <stdio.h>
53 
54 /* Utility library. */
55 
56 #include "msg.h"
57 #include "mymalloc.h"
58 #include "vstring.h"
59 #include "stringops.h"
60 #include "iostuff.h"
61 #include "myflock.h"
62 #include "stringops.h"
63 #include "dict.h"
64 #include "dict_cdb.h"
65 #include "warn_stat.h"
66 
67 #ifdef HAS_CDB
68 
69 #include <cdb.h>
70 #ifndef TINYCDB_VERSION
71 #include <cdb_make.h>
72 #endif
73 #ifndef cdb_fileno
74 #define cdb_fileno(c) ((c)->fd)
75 #endif
76 
77 #ifndef CDB_SUFFIX
78 #define CDB_SUFFIX ".cdb"
79 #endif
80 #ifndef CDB_TMP_SUFFIX
81 #define CDB_TMP_SUFFIX CDB_SUFFIX ".tmp"
82 #endif
83 
84 /* Application-specific. */
85 
86 typedef struct {
87     DICT    dict;                       /* generic members */
88     struct cdb cdb;                     /* cdb structure */
89     VSTRING *val_buf;                             /* value result */
90 #ifdef TINYCDB_VERSION
91     VSTRING *key_buf;                             /* key result */
92     unsigned seq_cptr;                            /* current sequence pointer */
93 #endif
94 } DICT_CDBQ;                                      /* query interface */
95 
96 typedef struct {
97     DICT    dict;                       /* generic members */
98     struct cdb_make cdbm;               /* cdb_make structure */
99     char   *cdb_path;                             /* cdb pathname (.cdb) */
100     char   *tmp_path;                             /* temporary pathname (.tmp) */
101 } DICT_CDBM;                                      /* rebuild interface */
102 
103 /* dict_cdbq_getdata - get data out of the cdb using given buffer */
104 
dict_cdbq_get_data(DICT_CDBQ * dict_cdbq,VSTRING ** bufp,unsigned len,unsigned pos)105 static const char *dict_cdbq_get_data(DICT_CDBQ *dict_cdbq,
106                                        VSTRING **bufp, unsigned len, unsigned pos)
107 {
108     VSTRING *buf = *bufp;
109 
110     if (!buf)
111           buf = *bufp = vstring_alloc(len < 20 ? 20 : len);
112     VSTRING_RESET(buf);
113     VSTRING_SPACE(buf, len);
114 
115     if (cdb_read(&dict_cdbq->cdb, vstring_str(buf), len, pos) < 0)
116           msg_fatal("error reading %s: %m", dict_cdbq->dict.name);
117     vstring_set_payload_size(buf, len);
118     VSTRING_TERMINATE(buf);
119     return vstring_str(buf);
120 }
121 
122 /* dict_cdbq_lookup - find database entry, query mode */
123 
dict_cdbq_lookup(DICT * dict,const char * name)124 static const char *dict_cdbq_lookup(DICT *dict, const char *name)
125 {
126     DICT_CDBQ *dict_cdbq = (DICT_CDBQ *) dict;
127     int     status = 0;
128     const char *result = 0;
129 
130     dict->error = 0;
131 
132     /* CDB is constant, so do not try to acquire a lock. */
133 
134     /*
135      * Optionally fold the key.
136      */
137     if (dict->flags & DICT_FLAG_FOLD_FIX) {
138           if (dict->fold_buf == 0)
139               dict->fold_buf = vstring_alloc(10);
140           vstring_strcpy(dict->fold_buf, name);
141           name = lowercase(vstring_str(dict->fold_buf));
142     }
143 
144     /*
145      * See if this CDB file was written with one null byte appended to key
146      * and value.
147      */
148     if (dict->flags & DICT_FLAG_TRY1NULL) {
149           status = cdb_find(&dict_cdbq->cdb, name, strlen(name) + 1);
150           if (status > 0)
151               dict->flags &= ~DICT_FLAG_TRY0NULL;
152     }
153 
154     /*
155      * See if this CDB file was written with no null byte appended to key and
156      * value.
157      */
158     if (status == 0 && (dict->flags & DICT_FLAG_TRY0NULL)) {
159           status = cdb_find(&dict_cdbq->cdb, name, strlen(name));
160           if (status > 0)
161               dict->flags &= ~DICT_FLAG_TRY1NULL;
162     }
163     if (status < 0)
164           msg_fatal("error reading %s: %m", dict->name);
165 
166     if (status) {
167           result = dict_cdbq_get_data(dict_cdbq, &dict_cdbq->val_buf,
168                     cdb_datalen(&dict_cdbq->cdb), cdb_datapos(&dict_cdbq->cdb));
169     }
170     /* No locking so not release the lock.  */
171 
172     return (result);
173 }
174 
175 #ifdef TINYCDB_VERSION
176 
177 /* dict_cdbq_sequence - traverse the dictionary */
178 
dict_cdbq_sequence(DICT * dict,int function,const char ** key,const char ** value)179 static int dict_cdbq_sequence(DICT *dict, int function,
180                                             const char **key, const char **value)
181 {
182     const char *myname = "dict_cdbq_sequence";
183     DICT_CDBQ *dict_cdbq = (DICT_CDBQ *) dict;
184     int     status;
185 
186     switch (function) {
187     case DICT_SEQ_FUN_FIRST:
188           cdb_seqinit(&dict_cdbq->seq_cptr, &dict_cdbq->cdb);
189           break;
190     case DICT_SEQ_FUN_NEXT:
191           if (!dict_cdbq->seq_cptr)
192               msg_panic("%s: %s: no cursor", myname, dict_cdbq->dict.name);
193           break;
194     default:
195           msg_panic("%s: invalid function %d", myname, function);
196     }
197 
198     status = cdb_seqnext(&dict_cdbq->seq_cptr, &dict_cdbq->cdb);
199 
200     if (status < 0)
201           msg_fatal("error seeking %s: %m", dict_cdbq->dict.name);
202 
203     if (!status) {
204           dict_cdbq->seq_cptr = 0;
205           return -1;                                        /* not found */
206     }
207     *key = dict_cdbq_get_data(dict_cdbq, &dict_cdbq->key_buf,
208                       cdb_keylen(&dict_cdbq->cdb), cdb_keypos(&dict_cdbq->cdb));
209     *value = dict_cdbq_get_data(dict_cdbq, &dict_cdbq->val_buf,
210                     cdb_datalen(&dict_cdbq->cdb), cdb_datapos(&dict_cdbq->cdb));
211 
212     return 0;
213 }
214 
215 #endif                                            /* TINYCDB_VERSION */
216 
217 /* dict_cdbq_close - close data base, query mode */
218 
dict_cdbq_close(DICT * dict)219 static void dict_cdbq_close(DICT *dict)
220 {
221     DICT_CDBQ *dict_cdbq = (DICT_CDBQ *) dict;
222 
223     cdb_free(&dict_cdbq->cdb);
224     close(dict->stat_fd);
225     if (dict->fold_buf)
226           vstring_free(dict->fold_buf);
227     if (dict_cdbq->val_buf)
228           vstring_free(dict_cdbq->val_buf);
229 #ifdef TINYCDB_VERSION
230     if (dict_cdbq->key_buf)
231           vstring_free(dict_cdbq->key_buf);
232 #endif
233     dict_free(dict);
234 }
235 
236 /* dict_cdbq_open - open data base, query mode */
237 
dict_cdbq_open(const char * path,int dict_flags)238 static DICT *dict_cdbq_open(const char *path, int dict_flags)
239 {
240     DICT_CDBQ *dict_cdbq;
241     struct stat st;
242     char   *cdb_path;
243     int     fd;
244 
245     /*
246      * Let the optimizer worry about eliminating redundant code.
247      */
248 #define DICT_CDBQ_OPEN_RETURN(d) do { \
249           DICT *__d = (d); \
250           myfree(cdb_path); \
251           return (__d); \
252     } while (0)
253 
254     cdb_path = concatenate(path, CDB_SUFFIX, (char *) 0);
255 
256     if ((fd = open(cdb_path, O_RDONLY)) < 0)
257           DICT_CDBQ_OPEN_RETURN(dict_surrogate(DICT_TYPE_CDB, path,
258                                                        O_RDONLY, dict_flags,
259                                                    "open database %s: %m", cdb_path));
260 
261     dict_cdbq = (DICT_CDBQ *) dict_alloc(DICT_TYPE_CDB,
262                                                    cdb_path, sizeof(*dict_cdbq));
263     dict_cdbq->val_buf = 0;
264 #if defined(TINYCDB_VERSION)
265     dict_cdbq->key_buf = 0;
266     dict_cdbq->seq_cptr = 0;
267     if (cdb_init(&(dict_cdbq->cdb), fd) != 0)
268           msg_fatal("dict_cdbq_open: unable to init %s: %m", cdb_path);
269     dict_cdbq->dict.sequence = dict_cdbq_sequence;
270 #else
271     cdb_init(&(dict_cdbq->cdb), fd);
272 #endif
273     dict_cdbq->dict.lookup = dict_cdbq_lookup;
274     dict_cdbq->dict.close = dict_cdbq_close;
275     dict_cdbq->dict.stat_fd = fd;
276     if (fstat(fd, &st) < 0)
277           msg_fatal("dict_dbq_open: fstat: %m");
278     dict_cdbq->dict.mtime = st.st_mtime;
279     dict_cdbq->dict.owner.uid = st.st_uid;
280     dict_cdbq->dict.owner.status = (st.st_uid != 0);
281     close_on_exec(fd, CLOSE_ON_EXEC);
282 
283     /*
284      * Warn if the source file is newer than the indexed file, except when
285      * the source file changed only seconds ago.
286      */
287     if (stat(path, &st) == 0
288           && st.st_mtime > dict_cdbq->dict.mtime
289           && st.st_mtime < time((time_t *) 0) - 100)
290           msg_warn("database %s is older than source file %s", cdb_path, path);
291 
292     /*
293      * If undecided about appending a null byte to key and value, choose to
294      * try both in query mode.
295      */
296     if ((dict_flags & (DICT_FLAG_TRY1NULL | DICT_FLAG_TRY0NULL)) == 0)
297           dict_flags |= DICT_FLAG_TRY0NULL | DICT_FLAG_TRY1NULL;
298     dict_cdbq->dict.flags = dict_flags | DICT_FLAG_FIXED;
299     if (dict_flags & DICT_FLAG_FOLD_FIX)
300           dict_cdbq->dict.fold_buf = vstring_alloc(10);
301 
302     DICT_CDBQ_OPEN_RETURN(DICT_DEBUG (&dict_cdbq->dict));
303 }
304 
305 /* dict_cdbm_update - add database entry, create mode */
306 
dict_cdbm_update(DICT * dict,const char * name,const char * value)307 static int dict_cdbm_update(DICT *dict, const char *name, const char *value)
308 {
309     DICT_CDBM *dict_cdbm = (DICT_CDBM *) dict;
310     unsigned ksize, vsize;
311     int     r;
312 
313     dict->error = 0;
314 
315     /*
316      * Optionally fold the key.
317      */
318     if (dict->flags & DICT_FLAG_FOLD_FIX) {
319           if (dict->fold_buf == 0)
320               dict->fold_buf = vstring_alloc(10);
321           vstring_strcpy(dict->fold_buf, name);
322           name = lowercase(vstring_str(dict->fold_buf));
323     }
324     ksize = strlen(name);
325     vsize = strlen(value);
326 
327     /*
328      * Optionally append a null byte to key and value.
329      */
330     if (dict->flags & DICT_FLAG_TRY1NULL) {
331           ksize++;
332           vsize++;
333     }
334 
335     /*
336      * Do the add operation.  No locking is done.
337      */
338 #ifdef TINYCDB_VERSION
339 #ifndef CDB_PUT_ADD
340 #error please upgrate tinycdb to at least 0.5 version
341 #endif
342     if (dict->flags & DICT_FLAG_DUP_IGNORE)
343           r = CDB_PUT_ADD;
344     else if (dict->flags & DICT_FLAG_DUP_REPLACE)
345           r = CDB_PUT_REPLACE;
346     else
347           r = CDB_PUT_INSERT;
348     r = cdb_make_put(&dict_cdbm->cdbm, name, ksize, value, vsize, r);
349     if (r < 0)
350           msg_fatal("error writing %s: %m", dict_cdbm->tmp_path);
351     else if (r > 0) {
352           if (dict->flags & (DICT_FLAG_DUP_IGNORE | DICT_FLAG_DUP_REPLACE))
353                /* void */ ;
354           else if (dict->flags & DICT_FLAG_DUP_WARN)
355               msg_warn("%s: duplicate entry: \"%s\"",
356                          dict_cdbm->dict.name, name);
357           else
358               msg_fatal("%s: duplicate entry: \"%s\"",
359                           dict_cdbm->dict.name, name);
360     }
361     return (r);
362 #else
363     if (cdb_make_add(&dict_cdbm->cdbm, name, ksize, value, vsize) < 0)
364           msg_fatal("error writing %s: %m", dict_cdbm->tmp_path);
365     return (0);
366 #endif
367 }
368 
369 /* dict_cdbm_close - close data base and rename file.tmp to file.cdb */
370 
dict_cdbm_close(DICT * dict)371 static void dict_cdbm_close(DICT *dict)
372 {
373     DICT_CDBM *dict_cdbm = (DICT_CDBM *) dict;
374     int     fd = cdb_fileno(&dict_cdbm->cdbm);
375 
376     /*
377      * Note: if FCNTL locking is used, closing any file descriptor on a
378      * locked file cancels all locks that the process may have on that file.
379      * CDB is FCNTL locking safe, because it uses the same file descriptor
380      * for database I/O and locking.
381      */
382     if (cdb_make_finish(&dict_cdbm->cdbm) < 0)
383           msg_fatal("finish database %s: %m", dict_cdbm->tmp_path);
384     if (rename(dict_cdbm->tmp_path, dict_cdbm->cdb_path) < 0)
385           msg_fatal("rename database from %s to %s: %m",
386                       dict_cdbm->tmp_path, dict_cdbm->cdb_path);
387     if (close(fd) < 0)                                      /* releases a lock */
388           msg_fatal("close database %s: %m", dict_cdbm->cdb_path);
389     myfree(dict_cdbm->cdb_path);
390     myfree(dict_cdbm->tmp_path);
391     if (dict->fold_buf)
392           vstring_free(dict->fold_buf);
393     dict_free(dict);
394 }
395 
396 /* dict_cdbm_open - create database as file.tmp */
397 
dict_cdbm_open(const char * path,int dict_flags)398 static DICT *dict_cdbm_open(const char *path, int dict_flags)
399 {
400     DICT_CDBM *dict_cdbm;
401     char   *cdb_path;
402     char   *tmp_path;
403     int     fd;
404     struct stat st0, st1;
405 
406     /*
407      * Let the optimizer worry about eliminating redundant code.
408      */
409 #define DICT_CDBM_OPEN_RETURN(d) do { \
410           DICT *__d = (d); \
411           if (cdb_path) \
412               myfree(cdb_path); \
413           if (tmp_path) \
414               myfree(tmp_path); \
415           return (__d); \
416     } while (0)
417 
418     cdb_path = concatenate(path, CDB_SUFFIX, (char *) 0);
419     tmp_path = concatenate(path, CDB_TMP_SUFFIX, (char *) 0);
420 
421     /*
422      * Repeat until we have opened *and* locked *existing* file. Since the
423      * new (tmp) file will be renamed to be .cdb file, locking here is
424      * somewhat funny to work around possible race conditions.  Note that we
425      * can't open a file with O_TRUNC as we can't know if another process
426      * isn't creating it at the same time.
427      */
428     for (;;) {
429           if ((fd = open(tmp_path, O_RDWR | O_CREAT, 0644)) < 0)
430               DICT_CDBM_OPEN_RETURN(dict_surrogate(DICT_TYPE_CDB, path,
431                                                              O_RDWR, dict_flags,
432                                                              "open database %s: %m",
433                                                              tmp_path));
434           if (fstat(fd, &st0) < 0)
435               msg_fatal("fstat(%s): %m", tmp_path);
436 
437           /*
438            * Get an exclusive lock - we're going to change the database so we
439            * can't have any spectators.
440            */
441           if (myflock(fd, INTERNAL_LOCK, MYFLOCK_OP_EXCLUSIVE) < 0)
442               msg_fatal("lock %s: %m", tmp_path);
443 
444           if (stat(tmp_path, &st1) < 0)
445               msg_fatal("stat(%s): %m", tmp_path);
446 
447           /*
448            * Compare file's state before and after lock: should be the same,
449            * and nlinks should be >0, or else we opened non-existing file...
450            */
451           if (st0.st_ino == st1.st_ino && st0.st_dev == st1.st_dev
452               && st0.st_rdev == st1.st_rdev && st0.st_nlink == st1.st_nlink
453               && st0.st_nlink > 0)
454               break;                                        /* successfully opened */
455 
456           close(fd);
457 
458     }
459 
460 #ifndef NO_FTRUNCATE
461     if (st0.st_size)
462           ftruncate(fd, 0);
463 #endif
464 
465     dict_cdbm = (DICT_CDBM *) dict_alloc(DICT_TYPE_CDB, path,
466                                                    sizeof(*dict_cdbm));
467     if (cdb_make_start(&dict_cdbm->cdbm, fd) < 0)
468           msg_fatal("initialize database %s: %m", tmp_path);
469     dict_cdbm->dict.close = dict_cdbm_close;
470     dict_cdbm->dict.update = dict_cdbm_update;
471     dict_cdbm->cdb_path = cdb_path;
472     dict_cdbm->tmp_path = tmp_path;
473     cdb_path = tmp_path = 0;                      /* DICT_CDBM_OPEN_RETURN() */
474     dict_cdbm->dict.owner.uid = st1.st_uid;
475     dict_cdbm->dict.owner.status = (st1.st_uid != 0);
476     close_on_exec(fd, CLOSE_ON_EXEC);
477 
478     /*
479      * If undecided about appending a null byte to key and value, choose a
480      * default to not append a null byte when creating a cdb.
481      */
482     if ((dict_flags & (DICT_FLAG_TRY1NULL | DICT_FLAG_TRY0NULL)) == 0)
483           dict_flags |= DICT_FLAG_TRY0NULL;
484     else if ((dict_flags & DICT_FLAG_TRY1NULL)
485                && (dict_flags & DICT_FLAG_TRY0NULL))
486           dict_flags &= ~DICT_FLAG_TRY0NULL;
487     dict_cdbm->dict.flags = dict_flags | DICT_FLAG_FIXED;
488     if (dict_flags & DICT_FLAG_FOLD_FIX)
489           dict_cdbm->dict.fold_buf = vstring_alloc(10);
490 
491     DICT_CDBM_OPEN_RETURN(DICT_DEBUG (&dict_cdbm->dict));
492 }
493 
494 /* dict_cdb_open - open data base for query mode or create mode */
495 
dict_cdb_open(const char * path,int open_flags,int dict_flags)496 DICT   *dict_cdb_open(const char *path, int open_flags, int dict_flags)
497 {
498     switch (open_flags & (O_RDONLY | O_RDWR | O_WRONLY | O_CREAT | O_TRUNC)) {
499           case O_RDONLY:                          /* query mode */
500           return dict_cdbq_open(path, dict_flags);
501     case O_WRONLY | O_CREAT | O_TRUNC:            /* create mode */
502     case O_RDWR | O_CREAT | O_TRUNC:              /* sloppiness */
503           return dict_cdbm_open(path, dict_flags);
504     default:
505           msg_fatal("dict_cdb_open: inappropriate open flags for cdb database"
506                       " - specify O_RDONLY or O_WRONLY|O_CREAT|O_TRUNC");
507     }
508 }
509 
510 #endif                                            /* HAS_CDB */
511