Fix taint-checking on FreeBSD
[exim.git] / src / src / dbfn.c
1 /*************************************************
2 * Exim - an Internet mail transport agent *
3 *************************************************/
4
5 /* Copyright (c) University of Cambridge 1995 - 2018 */
6 /* See the file NOTICE for conditions of use and distribution. */
7
8
9 #include "exim.h"
10
11
12 /* Functions for accessing Exim's hints database, which consists of a number of
13 different DBM files. This module does not contain code for reading DBM files
14 for (e.g.) alias expansion. That is all contained within the general search
15 functions. As Exim now has support for several DBM interfaces, all the relevant
16 functions are called as macros.
17
18 All the data in Exim's database is in the nature of *hints*. Therefore it
19 doesn't matter if it gets destroyed by accident. These functions are not
20 supposed to implement a "safe" database.
21
22 Keys are passed in as C strings, and the terminating zero *is* used when
23 building the dbm files. This just makes life easier when scanning the files
24 sequentially.
25
26 Synchronization is required on the database files, and this is achieved by
27 means of locking on independent lock files. (Earlier attempts to lock on the
28 DBM files themselves were never completely successful.) Since callers may in
29 general want to do more than one read or write while holding the lock, there
30 are separate open and close functions. However, the calling modules should
31 arrange to hold the locks for the bare minimum of time. */
32
33
34
35 /*************************************************
36 * Berkeley DB error callback *
37 *************************************************/
38
39 /* For Berkeley DB >= 2, we can define a function to be called in case of DB
40 errors. This should help with debugging strange DB problems, e.g. getting "File
41 exists" when you try to open a db file. The API for this function was changed
42 at DB release 4.3. */
43
44 #if defined(USE_DB) && defined(DB_VERSION_STRING)
45 void
46 #if DB_VERSION_MAJOR > 4 || (DB_VERSION_MAJOR == 4 && DB_VERSION_MINOR >= 3)
47 dbfn_bdb_error_callback(const DB_ENV *dbenv, const char *pfx, const char *msg)
48 {
49 dbenv = dbenv;
50 #else
51 dbfn_bdb_error_callback(const char *pfx, char *msg)
52 {
53 #endif
54 pfx = pfx;
55 log_write(0, LOG_MAIN, "Berkeley DB error: %s", msg);
56 }
57 #endif
58
59
60
61
62 /*************************************************
63 * Open and lock a database file *
64 *************************************************/
65
66 /* Used for accessing Exim's hints databases.
67
68 Arguments:
69 name The single-component name of one of Exim's database files.
70 flags Either O_RDONLY or O_RDWR, indicating the type of open required;
71 O_RDWR implies "create if necessary"
72 dbblock Points to an open_db block to be filled in.
73 lof If TRUE, write to the log for actual open failures (locking failures
74 are always logged).
75 panic If TRUE, panic on failure to create the db directory
76
77 Returns: NULL if the open failed, or the locking failed. After locking
78 failures, errno is zero.
79
80 On success, dbblock is returned. This contains the dbm pointer and
81 the fd of the locked lock file.
82
83 There are some calls that use O_RDWR|O_CREAT for the flags. Having discovered
84 this in December 2005, I'm not sure if this is correct or not, but for the
85 moment I haven't changed them.
86 */
87
88 open_db *
89 dbfn_open(uschar *name, int flags, open_db *dbblock, BOOL lof, BOOL panic)
90 {
91 int rc, save_errno;
92 BOOL read_only = flags == O_RDONLY;
93 BOOL created = FALSE;
94 flock_t lock_data;
95 uschar dirname[256], filename[256];
96
97 DEBUG(D_hints_lookup) acl_level++;
98
99 /* The first thing to do is to open a separate file on which to lock. This
100 ensures that Exim has exclusive use of the database before it even tries to
101 open it. Early versions tried to lock on the open database itself, but that
102 gave rise to mysterious problems from time to time - it was suspected that some
103 DB libraries "do things" on their open() calls which break the interlocking.
104 The lock file is never written to, but we open it for writing so we can get a
105 write lock if required. If it does not exist, we create it. This is done
106 separately so we know when we have done it, because when running as root we
107 need to change the ownership - see the bottom of this function. We also try to
108 make the directory as well, just in case. We won't be doing this many times
109 unnecessarily, because usually the lock file will be there. If the directory
110 exists, there is no error. */
111
112 snprintf(CS dirname, sizeof(dirname), "%s/db", spool_directory);
113 snprintf(CS filename, sizeof(filename), "%s/%s.lockfile", dirname, name);
114
115 if ((dbblock->lockfd = Uopen(filename, O_RDWR, EXIMDB_LOCKFILE_MODE)) < 0)
116 {
117 created = TRUE;
118 (void)directory_make(spool_directory, US"db", EXIMDB_DIRECTORY_MODE, panic);
119 dbblock->lockfd = Uopen(filename, O_RDWR|O_CREAT, EXIMDB_LOCKFILE_MODE);
120 }
121
122 if (dbblock->lockfd < 0)
123 {
124 log_write(0, LOG_MAIN, "%s",
125 string_open_failed(errno, "database lock file %s", filename));
126 errno = 0; /* Indicates locking failure */
127 DEBUG(D_hints_lookup) acl_level--;
128 return NULL;
129 }
130
131 /* Now we must get a lock on the opened lock file; do this with a blocking
132 lock that times out. */
133
134 lock_data.l_type = read_only? F_RDLCK : F_WRLCK;
135 lock_data.l_whence = lock_data.l_start = lock_data.l_len = 0;
136
137 DEBUG(D_hints_lookup|D_retry|D_route|D_deliver)
138 debug_printf_indent("locking %s\n", filename);
139
140 sigalrm_seen = FALSE;
141 ALARM(EXIMDB_LOCK_TIMEOUT);
142 rc = fcntl(dbblock->lockfd, F_SETLKW, &lock_data);
143 ALARM_CLR(0);
144
145 if (sigalrm_seen) errno = ETIMEDOUT;
146 if (rc < 0)
147 {
148 log_write(0, LOG_MAIN|LOG_PANIC, "Failed to get %s lock for %s: %s",
149 read_only ? "read" : "write", filename,
150 errno == ETIMEDOUT ? "timed out" : strerror(errno));
151 (void)close(dbblock->lockfd);
152 errno = 0; /* Indicates locking failure */
153 DEBUG(D_hints_lookup) acl_level--;
154 return NULL;
155 }
156
157 DEBUG(D_hints_lookup) debug_printf_indent("locked %s\n", filename);
158
159 /* At this point we have an opened and locked separate lock file, that is,
160 exclusive access to the database, so we can go ahead and open it. If we are
161 expected to create it, don't do so at first, again so that we can detect
162 whether we need to change its ownership (see comments about the lock file
163 above.) There have been regular reports of crashes while opening hints
164 databases - often this is caused by non-matching db.h and the library. To make
165 it easy to pin this down, there are now debug statements on either side of the
166 open call. */
167
168 snprintf(CS filename, sizeof(filename), "%s/%s", dirname, name);
169 EXIM_DBOPEN(filename, dirname, flags, EXIMDB_MODE, &(dbblock->dbptr));
170
171 if (!dbblock->dbptr && errno == ENOENT && flags == O_RDWR)
172 {
173 DEBUG(D_hints_lookup)
174 debug_printf_indent("%s appears not to exist: trying to create\n", filename);
175 created = TRUE;
176 EXIM_DBOPEN(filename, dirname, flags|O_CREAT, EXIMDB_MODE, &(dbblock->dbptr));
177 }
178
179 save_errno = errno;
180
181 /* If we are running as root and this is the first access to the database, its
182 files will be owned by root. We want them to be owned by exim. We detect this
183 situation by noting above when we had to create the lock file or the database
184 itself. Because the different dbm libraries use different extensions for their
185 files, I don't know of any easier way of arranging this than scanning the
186 directory for files with the appropriate base name. At least this deals with
187 the lock file at the same time. Also, the directory will typically have only
188 half a dozen files, so the scan will be quick.
189
190 This code is placed here, before the test for successful opening, because there
191 was a case when a file was created, but the DBM library still returned NULL
192 because of some problem. It also sorts out the lock file if that was created
193 but creation of the database file failed. */
194
195 if (created && geteuid() == root_uid)
196 {
197 DIR *dd;
198 struct dirent *ent;
199 uschar *lastname = Ustrrchr(filename, '/') + 1;
200 int namelen = Ustrlen(name);
201
202 *lastname = 0;
203 dd = opendir(CS filename);
204
205 while ((ent = readdir(dd)))
206 if (Ustrncmp(ent->d_name, name, namelen) == 0)
207 {
208 struct stat statbuf;
209 Ustrcpy(lastname, US ent->d_name);
210 if (Ustat(filename, &statbuf) >= 0 && statbuf.st_uid != exim_uid)
211 {
212 DEBUG(D_hints_lookup) debug_printf_indent("ensuring %s is owned by exim\n", filename);
213 if (exim_chown(filename, exim_uid, exim_gid))
214 DEBUG(D_hints_lookup) debug_printf_indent("failed setting %s to owned by exim\n", filename);
215 }
216 }
217
218 closedir(dd);
219 }
220
221 /* If the open has failed, return NULL, leaving errno set. If lof is TRUE,
222 log the event - also for debugging - but debug only if the file just doesn't
223 exist. */
224
225 if (!dbblock->dbptr)
226 {
227 if (lof && save_errno != ENOENT)
228 log_write(0, LOG_MAIN, "%s", string_open_failed(save_errno, "DB file %s",
229 filename));
230 else
231 DEBUG(D_hints_lookup)
232 debug_printf_indent("%s\n", CS string_open_failed(save_errno, "DB file %s",
233 filename));
234 (void)close(dbblock->lockfd);
235 errno = save_errno;
236 DEBUG(D_hints_lookup) acl_level--;
237 return NULL;
238 }
239
240 DEBUG(D_hints_lookup)
241 debug_printf_indent("opened hints database %s: flags=%s\n", filename,
242 flags == O_RDONLY ? "O_RDONLY"
243 : flags == O_RDWR ? "O_RDWR"
244 : flags == (O_RDWR|O_CREAT) ? "O_RDWR|O_CREAT"
245 : "??");
246
247 /* Pass back the block containing the opened database handle and the open fd
248 for the lock. */
249
250 return dbblock;
251 }
252
253
254
255
256 /*************************************************
257 * Unlock and close a database file *
258 *************************************************/
259
260 /* Closing a file automatically unlocks it, so after closing the database, just
261 close the lock file.
262
263 Argument: a pointer to an open database block
264 Returns: nothing
265 */
266
267 void
268 dbfn_close(open_db *dbblock)
269 {
270 EXIM_DBCLOSE(dbblock->dbptr);
271 (void)close(dbblock->lockfd);
272 DEBUG(D_hints_lookup)
273 { debug_printf_indent("closed hints database and lockfile\n"); acl_level--; }
274 }
275
276
277
278
279 /*************************************************
280 * Read from database file *
281 *************************************************/
282
283 /* Passing back the pointer unchanged is useless, because there is
284 no guarantee of alignment. Since all the records used by Exim need
285 to be properly aligned to pick out the timestamps, etc., we might as
286 well do the copying centrally here.
287
288 Most calls don't need the length, so there is a macro called dbfn_read which
289 has two arguments; it calls this function adding NULL as the third.
290
291 Arguments:
292 dbblock a pointer to an open database block
293 key the key of the record to be read
294 length a pointer to an int into which to return the length, if not NULL
295
296 Returns: a pointer to the retrieved record, or
297 NULL if the record is not found
298 */
299
300 void *
301 dbfn_read_with_length(open_db *dbblock, const uschar *key, int *length)
302 {
303 void *yield;
304 EXIM_DATUM key_datum, result_datum;
305 int klen = Ustrlen(key) + 1;
306 uschar * key_copy = store_get(klen, is_tainted(key));
307
308 memcpy(key_copy, key, klen);
309
310 DEBUG(D_hints_lookup) debug_printf_indent("dbfn_read: key=%s\n", key);
311
312 EXIM_DATUM_INIT(key_datum); /* Some DBM libraries require the datum */
313 EXIM_DATUM_INIT(result_datum); /* to be cleared before use. */
314 EXIM_DATUM_DATA(key_datum) = CS key_copy;
315 EXIM_DATUM_SIZE(key_datum) = klen;
316
317 if (!EXIM_DBGET(dbblock->dbptr, key_datum, result_datum)) return NULL;
318
319 /* Assume the data store could have been tainted. Properly, we should
320 store the taint status with the data. */
321
322 yield = store_get(EXIM_DATUM_SIZE(result_datum), TRUE);
323 memcpy(yield, EXIM_DATUM_DATA(result_datum), EXIM_DATUM_SIZE(result_datum));
324 if (length != NULL) *length = EXIM_DATUM_SIZE(result_datum);
325
326 EXIM_DATUM_FREE(result_datum); /* Some DBM libs require freeing */
327 return yield;
328 }
329
330
331
332 /*************************************************
333 * Write to database file *
334 *************************************************/
335
336 /*
337 Arguments:
338 dbblock a pointer to an open database block
339 key the key of the record to be written
340 ptr a pointer to the record to be written
341 length the length of the record to be written
342
343 Returns: the yield of the underlying dbm or db "write" function. If this
344 is dbm, the value is zero for OK.
345 */
346
347 int
348 dbfn_write(open_db *dbblock, const uschar *key, void *ptr, int length)
349 {
350 EXIM_DATUM key_datum, value_datum;
351 dbdata_generic *gptr = (dbdata_generic *)ptr;
352 int klen = Ustrlen(key) + 1;
353 uschar * key_copy = store_get(klen, is_tainted(key));
354
355 memcpy(key_copy, key, klen);
356 gptr->time_stamp = time(NULL);
357
358 DEBUG(D_hints_lookup) debug_printf_indent("dbfn_write: key=%s\n", key);
359
360 EXIM_DATUM_INIT(key_datum); /* Some DBM libraries require the datum */
361 EXIM_DATUM_INIT(value_datum); /* to be cleared before use. */
362 EXIM_DATUM_DATA(key_datum) = CS key_copy;
363 EXIM_DATUM_SIZE(key_datum) = klen;
364 EXIM_DATUM_DATA(value_datum) = CS ptr;
365 EXIM_DATUM_SIZE(value_datum) = length;
366 return EXIM_DBPUT(dbblock->dbptr, key_datum, value_datum);
367 }
368
369
370
371 /*************************************************
372 * Delete record from database file *
373 *************************************************/
374
375 /*
376 Arguments:
377 dbblock a pointer to an open database block
378 key the key of the record to be deleted
379
380 Returns: the yield of the underlying dbm or db "delete" function.
381 */
382
383 int
384 dbfn_delete(open_db *dbblock, const uschar *key)
385 {
386 int klen = Ustrlen(key) + 1;
387 uschar * key_copy = store_get(klen, is_tainted(key));
388
389 DEBUG(D_hints_lookup) debug_printf_indent("dbfn_delete: key=%s\n", key);
390
391 memcpy(key_copy, key, klen);
392 EXIM_DATUM key_datum;
393 EXIM_DATUM_INIT(key_datum); /* Some DBM libraries require clearing */
394 EXIM_DATUM_DATA(key_datum) = CS key_copy;
395 EXIM_DATUM_SIZE(key_datum) = klen;
396 return EXIM_DBDEL(dbblock->dbptr, key_datum);
397 }
398
399
400
401 /*************************************************
402 * Scan the keys of a database file *
403 *************************************************/
404
405 /*
406 Arguments:
407 dbblock a pointer to an open database block
408 start TRUE if starting a new scan
409 FALSE if continuing with the current scan
410 cursor a pointer to a pointer to a cursor anchor, for those dbm libraries
411 that use the notion of a cursor
412
413 Returns: the next record from the file, or
414 NULL if there are no more
415 */
416
417 uschar *
418 dbfn_scan(open_db *dbblock, BOOL start, EXIM_CURSOR **cursor)
419 {
420 EXIM_DATUM key_datum, value_datum;
421 uschar *yield;
422 value_datum = value_datum; /* dummy; not all db libraries use this */
423
424 DEBUG(D_hints_lookup) debug_printf_indent("dbfn_scan\n");
425
426 /* Some dbm require an initialization */
427
428 if (start) EXIM_DBCREATE_CURSOR(dbblock->dbptr, cursor);
429
430 EXIM_DATUM_INIT(key_datum); /* Some DBM libraries require the datum */
431 EXIM_DATUM_INIT(value_datum); /* to be cleared before use. */
432
433 yield = (EXIM_DBSCAN(dbblock->dbptr, key_datum, value_datum, start, *cursor))?
434 US EXIM_DATUM_DATA(key_datum) : NULL;
435
436 /* Some dbm require a termination */
437
438 if (!yield) EXIM_DBDELETE_CURSOR(*cursor);
439 return yield;
440 }
441
442
443
444 /*************************************************
445 **************************************************
446 * Stand-alone test program *
447 **************************************************
448 *************************************************/
449
450 #ifdef STAND_ALONE
451
452 int
453 main(int argc, char **cargv)
454 {
455 open_db dbblock[8];
456 int max_db = sizeof(dbblock)/sizeof(open_db);
457 int current = -1;
458 int showtime = 0;
459 int i;
460 dbdata_wait *dbwait = NULL;
461 uschar **argv = USS cargv;
462 uschar buffer[256];
463 uschar structbuffer[1024];
464
465 if (argc != 2)
466 {
467 printf("Usage: test_dbfn directory\n");
468 printf("The subdirectory called \"db\" in the given directory is used for\n");
469 printf("the files used in this test program.\n");
470 return 1;
471 }
472
473 /* Initialize */
474
475 spool_directory = argv[1];
476 debug_selector = D_all - D_memory;
477 debug_file = stderr;
478 big_buffer = malloc(big_buffer_size);
479
480 for (i = 0; i < max_db; i++) dbblock[i].dbptr = NULL;
481
482 printf("\nExim's db functions tester: interface type is %s\n", EXIM_DBTYPE);
483 printf("DBM library: ");
484
485 #ifdef DB_VERSION_STRING
486 printf("Berkeley DB: %s\n", DB_VERSION_STRING);
487 #elif defined(BTREEVERSION) && defined(HASHVERSION)
488 #ifdef USE_DB
489 printf("probably Berkeley DB version 1.8x (native mode)\n");
490 #else
491 printf("probably Berkeley DB version 1.8x (compatibility mode)\n");
492 #endif
493 #elif defined(_DBM_RDONLY) || defined(dbm_dirfno)
494 printf("probably ndbm\n");
495 #elif defined(USE_TDB)
496 printf("using tdb\n");
497 #else
498 #ifdef USE_GDBM
499 printf("probably GDBM (native mode)\n");
500 #else
501 printf("probably GDBM (compatibility mode)\n");
502 #endif
503 #endif
504
505 /* Test the functions */
506
507 printf("\nTest the functions\n> ");
508
509 while (Ufgets(buffer, 256, stdin) != NULL)
510 {
511 int len = Ustrlen(buffer);
512 int count = 1;
513 clock_t start = 1;
514 clock_t stop = 0;
515 uschar *cmd = buffer;
516 while (len > 0 && isspace((uschar)buffer[len-1])) len--;
517 buffer[len] = 0;
518
519 if (isdigit((uschar)*cmd))
520 {
521 count = Uatoi(cmd);
522 while (isdigit((uschar)*cmd)) cmd++;
523 while (isspace((uschar)*cmd)) cmd++;
524 }
525
526 if (Ustrncmp(cmd, "open", 4) == 0)
527 {
528 int i;
529 open_db *odb;
530 uschar *s = cmd + 4;
531 while (isspace((uschar)*s)) s++;
532
533 for (i = 0; i < max_db; i++)
534 if (dbblock[i].dbptr == NULL) break;
535
536 if (i >= max_db)
537 {
538 printf("Too many open databases\n> ");
539 continue;
540 }
541
542 start = clock();
543 odb = dbfn_open(s, O_RDWR, dbblock + i, TRUE, TRUE);
544 stop = clock();
545
546 if (odb)
547 {
548 current = i;
549 printf("opened %d\n", current);
550 }
551 /* Other error cases will have written messages */
552 else if (errno == ENOENT)
553 {
554 printf("open failed: %s%s\n", strerror(errno),
555 #ifdef USE_DB
556 " (or other Berkeley DB error)"
557 #else
558 ""
559 #endif
560 );
561 }
562 }
563
564 else if (Ustrncmp(cmd, "write", 5) == 0)
565 {
566 int rc = 0;
567 uschar *key = cmd + 5;
568 uschar *data;
569
570 if (current < 0)
571 {
572 printf("No current database\n");
573 continue;
574 }
575
576 while (isspace((uschar)*key)) key++;
577 data = key;
578 while (*data != 0 && !isspace((uschar)*data)) data++;
579 *data++ = 0;
580 while (isspace((uschar)*data)) data++;
581
582 dbwait = (dbdata_wait *)(&structbuffer);
583 Ustrcpy(dbwait->text, data);
584
585 start = clock();
586 while (count-- > 0)
587 rc = dbfn_write(dbblock + current, key, dbwait,
588 Ustrlen(data) + sizeof(dbdata_wait));
589 stop = clock();
590 if (rc != 0) printf("Failed: %s\n", strerror(errno));
591 }
592
593 else if (Ustrncmp(cmd, "read", 4) == 0)
594 {
595 uschar *key = cmd + 4;
596 if (current < 0)
597 {
598 printf("No current database\n");
599 continue;
600 }
601 while (isspace((uschar)*key)) key++;
602 start = clock();
603 while (count-- > 0)
604 dbwait = (dbdata_wait *)dbfn_read_with_length(dbblock+ current, key, NULL);
605 stop = clock();
606 printf("%s\n", (dbwait == NULL)? "<not found>" : CS dbwait->text);
607 }
608
609 else if (Ustrncmp(cmd, "delete", 6) == 0)
610 {
611 uschar *key = cmd + 6;
612 if (current < 0)
613 {
614 printf("No current database\n");
615 continue;
616 }
617 while (isspace((uschar)*key)) key++;
618 dbfn_delete(dbblock + current, key);
619 }
620
621 else if (Ustrncmp(cmd, "scan", 4) == 0)
622 {
623 EXIM_CURSOR *cursor;
624 BOOL startflag = TRUE;
625 uschar *key;
626 uschar keybuffer[256];
627 if (current < 0)
628 {
629 printf("No current database\n");
630 continue;
631 }
632 start = clock();
633 while ((key = dbfn_scan(dbblock + current, startflag, &cursor)) != NULL)
634 {
635 startflag = FALSE;
636 Ustrcpy(keybuffer, key);
637 dbwait = (dbdata_wait *)dbfn_read_with_length(dbblock + current,
638 keybuffer, NULL);
639 printf("%s: %s\n", keybuffer, dbwait->text);
640 }
641 stop = clock();
642 printf("End of scan\n");
643 }
644
645 else if (Ustrncmp(cmd, "close", 5) == 0)
646 {
647 uschar *s = cmd + 5;
648 while (isspace((uschar)*s)) s++;
649 i = Uatoi(s);
650 if (i >= max_db || dbblock[i].dbptr == NULL) printf("Not open\n"); else
651 {
652 start = clock();
653 dbfn_close(dbblock + i);
654 stop = clock();
655 dbblock[i].dbptr = NULL;
656 if (i == current) current = -1;
657 }
658 }
659
660 else if (Ustrncmp(cmd, "file", 4) == 0)
661 {
662 uschar *s = cmd + 4;
663 while (isspace((uschar)*s)) s++;
664 i = Uatoi(s);
665 if (i >= max_db || dbblock[i].dbptr == NULL) printf("Not open\n");
666 else current = i;
667 }
668
669 else if (Ustrncmp(cmd, "time", 4) == 0)
670 {
671 showtime = ~showtime;
672 printf("Timing %s\n", showtime? "on" : "off");
673 }
674
675 else if (Ustrcmp(cmd, "q") == 0 || Ustrncmp(cmd, "quit", 4) == 0) break;
676
677 else if (Ustrncmp(cmd, "help", 4) == 0)
678 {
679 printf("close [<number>] close file [<number>]\n");
680 printf("delete <key> remove record from current file\n");
681 printf("file <number> make file <number> current\n");
682 printf("open <name> open db file\n");
683 printf("q[uit] exit program\n");
684 printf("read <key> read record from current file\n");
685 printf("scan scan current file\n");
686 printf("time time display on/off\n");
687 printf("write <key> <rest-of-line> write record to current file\n");
688 }
689
690 else printf("Eh?\n");
691
692 if (showtime && stop >= start)
693 printf("start=%d stop=%d difference=%d\n", (int)start, (int)stop,
694 (int)(stop - start));
695
696 printf("> ");
697 }
698
699 for (i = 0; i < max_db; i++)
700 {
701 if (dbblock[i].dbptr != NULL)
702 {
703 printf("\nClosing %d", i);
704 dbfn_close(dbblock + i);
705 }
706 }
707
708 printf("\n");
709 return 0;
710 }
711
712 #endif
713
714 /* End of dbfn.c */