1/*	$NetBSD$	*/
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
66#ifdef HAS_CDB
67
68#include <cdb.h>
69#ifndef TINYCDB_VERSION
70#include <cdb_make.h>
71#endif
72#ifndef cdb_fileno
73#define cdb_fileno(c) ((c)->fd)
74#endif
75
76#ifndef CDB_SUFFIX
77#define CDB_SUFFIX ".cdb"
78#endif
79#ifndef CDB_TMP_SUFFIX
80#define CDB_TMP_SUFFIX CDB_SUFFIX ".tmp"
81#endif
82
83/* Application-specific. */
84
85typedef struct {
86    DICT    dict;			/* generic members */
87    struct cdb cdb;			/* cdb structure */
88} DICT_CDBQ;				/* query interface */
89
90typedef struct {
91    DICT    dict;			/* generic members */
92    struct cdb_make cdbm;		/* cdb_make structure */
93    char   *cdb_path;			/* cdb pathname (.cdb) */
94    char   *tmp_path;			/* temporary pathname (.tmp) */
95} DICT_CDBM;				/* rebuild interface */
96
97/* dict_cdbq_lookup - find database entry, query mode */
98
99static const char *dict_cdbq_lookup(DICT *dict, const char *name)
100{
101    DICT_CDBQ *dict_cdbq = (DICT_CDBQ *) dict;
102    unsigned vlen;
103    int     status = 0;
104    static char *buf;
105    static unsigned len;
106    const char *result = 0;
107
108    dict_errno = 0;
109
110    /* CDB is constant, so do not try to acquire a lock. */
111
112    /*
113     * Optionally fold the key.
114     */
115    if (dict->flags & DICT_FLAG_FOLD_FIX) {
116	if (dict->fold_buf == 0)
117	    dict->fold_buf = vstring_alloc(10);
118	vstring_strcpy(dict->fold_buf, name);
119	name = lowercase(vstring_str(dict->fold_buf));
120    }
121
122    /*
123     * See if this CDB file was written with one null byte appended to key
124     * and value.
125     */
126    if (dict->flags & DICT_FLAG_TRY1NULL) {
127	status = cdb_find(&dict_cdbq->cdb, name, strlen(name) + 1);
128	if (status > 0)
129	    dict->flags &= ~DICT_FLAG_TRY0NULL;
130    }
131
132    /*
133     * See if this CDB file was written with no null byte appended to key and
134     * value.
135     */
136    if (status == 0 && (dict->flags & DICT_FLAG_TRY0NULL)) {
137	status = cdb_find(&dict_cdbq->cdb, name, strlen(name));
138	if (status > 0)
139	    dict->flags &= ~DICT_FLAG_TRY1NULL;
140    }
141    if (status < 0)
142	msg_fatal("error reading %s: %m", dict->name);
143
144    if (status) {
145	vlen = cdb_datalen(&dict_cdbq->cdb);
146	if (len < vlen) {
147	    if (buf == 0)
148		buf = mymalloc(vlen + 1);
149	    else
150		buf = myrealloc(buf, vlen + 1);
151	    len = vlen;
152	}
153	if (cdb_read(&dict_cdbq->cdb, buf, vlen,
154		     cdb_datapos(&dict_cdbq->cdb)) < 0)
155	    msg_fatal("error reading %s: %m", dict->name);
156	buf[vlen] = '\0';
157	result = buf;
158    }
159    /* No locking so not release the lock.  */
160
161    return (result);
162}
163
164/* dict_cdbq_close - close data base, query mode */
165
166static void dict_cdbq_close(DICT *dict)
167{
168    DICT_CDBQ *dict_cdbq = (DICT_CDBQ *) dict;
169
170    cdb_free(&dict_cdbq->cdb);
171    close(dict->stat_fd);
172    if (dict->fold_buf)
173	vstring_free(dict->fold_buf);
174    dict_free(dict);
175}
176
177/* dict_cdbq_open - open data base, query mode */
178
179static DICT *dict_cdbq_open(const char *path, int dict_flags)
180{
181    DICT_CDBQ *dict_cdbq;
182    struct stat st;
183    char   *cdb_path;
184    int     fd;
185
186    cdb_path = concatenate(path, CDB_SUFFIX, (char *) 0);
187
188    if ((fd = open(cdb_path, O_RDONLY)) < 0)
189	msg_fatal("open database %s: %m", cdb_path);
190
191    dict_cdbq = (DICT_CDBQ *) dict_alloc(DICT_TYPE_CDB,
192					 cdb_path, sizeof(*dict_cdbq));
193#if defined(TINYCDB_VERSION)
194    if (cdb_init(&(dict_cdbq->cdb), fd) != 0)
195	msg_fatal("dict_cdbq_open: unable to init %s: %m", cdb_path);
196#else
197    cdb_init(&(dict_cdbq->cdb), fd);
198#endif
199    dict_cdbq->dict.lookup = dict_cdbq_lookup;
200    dict_cdbq->dict.close = dict_cdbq_close;
201    dict_cdbq->dict.stat_fd = fd;
202    if (fstat(fd, &st) < 0)
203	msg_fatal("dict_dbq_open: fstat: %m");
204    dict_cdbq->dict.mtime = st.st_mtime;
205    close_on_exec(fd, CLOSE_ON_EXEC);
206
207    /*
208     * Warn if the source file is newer than the indexed file, except when
209     * the source file changed only seconds ago.
210     */
211    if (stat(path, &st) == 0
212	&& st.st_mtime > dict_cdbq->dict.mtime
213	&& st.st_mtime < time((time_t *) 0) - 100)
214	msg_warn("database %s is older than source file %s", cdb_path, path);
215
216    /*
217     * If undecided about appending a null byte to key and value, choose to
218     * try both in query mode.
219     */
220    if ((dict_flags & (DICT_FLAG_TRY1NULL | DICT_FLAG_TRY0NULL)) == 0)
221	dict_flags |= DICT_FLAG_TRY0NULL | DICT_FLAG_TRY1NULL;
222    dict_cdbq->dict.flags = dict_flags | DICT_FLAG_FIXED;
223    if (dict_flags & DICT_FLAG_FOLD_FIX)
224	dict_cdbq->dict.fold_buf = vstring_alloc(10);
225
226    myfree(cdb_path);
227    return (&dict_cdbq->dict);
228}
229
230/* dict_cdbm_update - add database entry, create mode */
231
232static void dict_cdbm_update(DICT *dict, const char *name, const char *value)
233{
234    DICT_CDBM *dict_cdbm = (DICT_CDBM *) dict;
235    unsigned ksize, vsize;
236    int     r;
237
238    /*
239     * Optionally fold the key.
240     */
241    if (dict->flags & DICT_FLAG_FOLD_FIX) {
242	if (dict->fold_buf == 0)
243	    dict->fold_buf = vstring_alloc(10);
244	vstring_strcpy(dict->fold_buf, name);
245	name = lowercase(vstring_str(dict->fold_buf));
246    }
247    ksize = strlen(name);
248    vsize = strlen(value);
249
250    /*
251     * Optionally append a null byte to key and value.
252     */
253    if (dict->flags & DICT_FLAG_TRY1NULL) {
254	ksize++;
255	vsize++;
256    }
257
258    /*
259     * Do the add operation.  No locking is done.
260     */
261#ifdef TINYCDB_VERSION
262#ifndef CDB_PUT_ADD
263#error please upgrate tinycdb to at least 0.5 version
264#endif
265    if (dict->flags & DICT_FLAG_DUP_IGNORE)
266	r = CDB_PUT_ADD;
267    else if (dict->flags & DICT_FLAG_DUP_REPLACE)
268	r = CDB_PUT_REPLACE;
269    else
270	r = CDB_PUT_INSERT;
271    r = cdb_make_put(&dict_cdbm->cdbm, name, ksize, value, vsize, r);
272    if (r < 0)
273	msg_fatal("error writing %s: %m", dict_cdbm->tmp_path);
274    else if (r > 0) {
275	if (dict->flags & (DICT_FLAG_DUP_IGNORE | DICT_FLAG_DUP_REPLACE))
276	     /* void */ ;
277	else if (dict->flags & DICT_FLAG_DUP_WARN)
278	    msg_warn("%s: duplicate entry: \"%s\"",
279		     dict_cdbm->dict.name, name);
280	else
281	    msg_fatal("%s: duplicate entry: \"%s\"",
282		      dict_cdbm->dict.name, name);
283    }
284#else
285    if (cdb_make_add(&dict_cdbm->cdbm, name, ksize, value, vsize) < 0)
286	msg_fatal("error writing %s: %m", dict_cdbm->tmp_path);
287#endif
288}
289
290/* dict_cdbm_close - close data base and rename file.tmp to file.cdb */
291
292static void dict_cdbm_close(DICT *dict)
293{
294    DICT_CDBM *dict_cdbm = (DICT_CDBM *) dict;
295    int     fd = cdb_fileno(&dict_cdbm->cdbm);
296
297    /*
298     * Note: if FCNTL locking is used, closing any file descriptor on a
299     * locked file cancels all locks that the process may have on that file.
300     * CDB is FCNTL locking safe, because it uses the same file descriptor
301     * for database I/O and locking.
302     */
303    if (cdb_make_finish(&dict_cdbm->cdbm) < 0)
304	msg_fatal("finish database %s: %m", dict_cdbm->tmp_path);
305    if (rename(dict_cdbm->tmp_path, dict_cdbm->cdb_path) < 0)
306	msg_fatal("rename database from %s to %s: %m",
307		  dict_cdbm->tmp_path, dict_cdbm->cdb_path);
308    if (close(fd) < 0)				/* releases a lock */
309	msg_fatal("close database %s: %m", dict_cdbm->cdb_path);
310    myfree(dict_cdbm->cdb_path);
311    myfree(dict_cdbm->tmp_path);
312    if (dict->fold_buf)
313	vstring_free(dict->fold_buf);
314    dict_free(dict);
315}
316
317/* dict_cdbm_open - create database as file.tmp */
318
319static DICT *dict_cdbm_open(const char *path, int dict_flags)
320{
321    DICT_CDBM *dict_cdbm;
322    char   *cdb_path;
323    char   *tmp_path;
324    int     fd;
325    struct stat st0, st1;
326
327    cdb_path = concatenate(path, CDB_SUFFIX, (char *) 0);
328    tmp_path = concatenate(path, CDB_TMP_SUFFIX, (char *) 0);
329
330    /*
331     * Repeat until we have opened *and* locked *existing* file. Since the
332     * new (tmp) file will be renamed to be .cdb file, locking here is
333     * somewhat funny to work around possible race conditions.  Note that we
334     * can't open a file with O_TRUNC as we can't know if another process
335     * isn't creating it at the same time.
336     */
337    for (;;) {
338	if ((fd = open(tmp_path, O_RDWR | O_CREAT, 0644)) < 0
339	    || fstat(fd, &st0) < 0)
340	    msg_fatal("open database %s: %m", tmp_path);
341
342	/*
343	 * Get an exclusive lock - we're going to change the database so we
344	 * can't have any spectators.
345	 */
346	if (myflock(fd, INTERNAL_LOCK, MYFLOCK_OP_EXCLUSIVE) < 0)
347	    msg_fatal("lock %s: %m", tmp_path);
348
349	if (stat(tmp_path, &st1) < 0)
350	    msg_fatal("stat(%s): %m", tmp_path);
351
352	/*
353	 * Compare file's state before and after lock: should be the same,
354	 * and nlinks should be >0, or else we opened non-existing file...
355	 */
356	if (st0.st_ino == st1.st_ino && st0.st_dev == st1.st_dev
357	    && st0.st_rdev == st1.st_rdev && st0.st_nlink == st1.st_nlink
358	    && st0.st_nlink > 0)
359	    break;				/* successefully opened */
360
361	close(fd);
362
363    }
364
365#ifndef NO_FTRUNCATE
366    if (st0.st_size)
367	ftruncate(fd, 0);
368#endif
369
370    dict_cdbm = (DICT_CDBM *) dict_alloc(DICT_TYPE_CDB, path,
371					 sizeof(*dict_cdbm));
372    if (cdb_make_start(&dict_cdbm->cdbm, fd) < 0)
373	msg_fatal("initialize database %s: %m", tmp_path);
374    dict_cdbm->dict.close = dict_cdbm_close;
375    dict_cdbm->dict.update = dict_cdbm_update;
376    dict_cdbm->cdb_path = cdb_path;
377    dict_cdbm->tmp_path = tmp_path;
378    close_on_exec(fd, CLOSE_ON_EXEC);
379
380    /*
381     * If undecided about appending a null byte to key and value, choose a
382     * default to not append a null byte when creating a cdb.
383     */
384    if ((dict_flags & (DICT_FLAG_TRY1NULL | DICT_FLAG_TRY0NULL)) == 0)
385	dict_flags |= DICT_FLAG_TRY0NULL;
386    else if ((dict_flags & DICT_FLAG_TRY1NULL)
387	     && (dict_flags & DICT_FLAG_TRY0NULL))
388	dict_flags &= ~DICT_FLAG_TRY0NULL;
389    dict_cdbm->dict.flags = dict_flags | DICT_FLAG_FIXED;
390    if (dict_flags & DICT_FLAG_FOLD_FIX)
391	dict_cdbm->dict.fold_buf = vstring_alloc(10);
392
393    return (&dict_cdbm->dict);
394}
395
396/* dict_cdb_open - open data base for query mode or create mode */
397
398DICT   *dict_cdb_open(const char *path, int open_flags, int dict_flags)
399{
400    switch (open_flags & (O_RDONLY | O_RDWR | O_WRONLY | O_CREAT | O_TRUNC)) {
401	case O_RDONLY:			/* query mode */
402	return dict_cdbq_open(path, dict_flags);
403    case O_WRONLY | O_CREAT | O_TRUNC:		/* create mode */
404    case O_RDWR | O_CREAT | O_TRUNC:		/* sloppiness */
405	return dict_cdbm_open(path, dict_flags);
406    default:
407	msg_fatal("dict_cdb_open: inappropriate open flags for cdb database"
408		  " - specify O_RDONLY or O_WRONLY|O_CREAT|O_TRUNC");
409    }
410}
411
412#endif					/* HAS_CDB */
413