TLS: Session resumption, under the EXPERIMENTAL_TLS_RESUME build option.
[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, 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);
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 yield = store_get(EXIM_DATUM_SIZE(result_datum));
320 memcpy(yield, EXIM_DATUM_DATA(result_datum), EXIM_DATUM_SIZE(result_datum));
321 if (length != NULL) *length = EXIM_DATUM_SIZE(result_datum);
322
323 EXIM_DATUM_FREE(result_datum);    /* Some DBM libs require freeing */
324 return yield;
325 }
326
327
328
329 /*************************************************
330 *             Write to database file             *
331 *************************************************/
332
333 /*
334 Arguments:
335   dbblock   a pointer to an open database block
336   key       the key of the record to be written
337   ptr       a pointer to the record to be written
338   length    the length of the record to be written
339
340 Returns:    the yield of the underlying dbm or db "write" function. If this
341             is dbm, the value is zero for OK.
342 */
343
344 int
345 dbfn_write(open_db *dbblock, const uschar *key, void *ptr, int length)
346 {
347 EXIM_DATUM key_datum, value_datum;
348 dbdata_generic *gptr = (dbdata_generic *)ptr;
349 int klen = Ustrlen(key) + 1;
350 uschar * key_copy = store_get(klen);
351
352 memcpy(key_copy, key, klen);
353 gptr->time_stamp = time(NULL);
354
355 DEBUG(D_hints_lookup) debug_printf_indent("dbfn_write: key=%s\n", key);
356
357 EXIM_DATUM_INIT(key_datum);         /* Some DBM libraries require the datum */
358 EXIM_DATUM_INIT(value_datum);       /* to be cleared before use. */
359 EXIM_DATUM_DATA(key_datum) = CS key_copy;
360 EXIM_DATUM_SIZE(key_datum) = klen;
361 EXIM_DATUM_DATA(value_datum) = CS ptr;
362 EXIM_DATUM_SIZE(value_datum) = length;
363 return EXIM_DBPUT(dbblock->dbptr, key_datum, value_datum);
364 }
365
366
367
368 /*************************************************
369 *           Delete record from database file     *
370 *************************************************/
371
372 /*
373 Arguments:
374   dbblock    a pointer to an open database block
375   key        the key of the record to be deleted
376
377 Returns: the yield of the underlying dbm or db "delete" function.
378 */
379
380 int
381 dbfn_delete(open_db *dbblock, const uschar *key)
382 {
383 int klen = Ustrlen(key) + 1;
384 uschar * key_copy = store_get(klen);
385
386 DEBUG(D_hints_lookup) debug_printf_indent("dbfn_delete: key=%s\n", key);
387
388 memcpy(key_copy, key, klen);
389 EXIM_DATUM key_datum;
390 EXIM_DATUM_INIT(key_datum);         /* Some DBM libraries require clearing */
391 EXIM_DATUM_DATA(key_datum) = CS key_copy;
392 EXIM_DATUM_SIZE(key_datum) = klen;
393 return EXIM_DBDEL(dbblock->dbptr, key_datum);
394 }
395
396
397
398 /*************************************************
399 *         Scan the keys of a database file       *
400 *************************************************/
401
402 /*
403 Arguments:
404   dbblock  a pointer to an open database block
405   start    TRUE if starting a new scan
406            FALSE if continuing with the current scan
407   cursor   a pointer to a pointer to a cursor anchor, for those dbm libraries
408            that use the notion of a cursor
409
410 Returns:   the next record from the file, or
411            NULL if there are no more
412 */
413
414 uschar *
415 dbfn_scan(open_db *dbblock, BOOL start, EXIM_CURSOR **cursor)
416 {
417 EXIM_DATUM key_datum, value_datum;
418 uschar *yield;
419 value_datum = value_datum;    /* dummy; not all db libraries use this */
420
421 DEBUG(D_hints_lookup) debug_printf_indent("dbfn_scan\n");
422
423 /* Some dbm require an initialization */
424
425 if (start) EXIM_DBCREATE_CURSOR(dbblock->dbptr, cursor);
426
427 EXIM_DATUM_INIT(key_datum);         /* Some DBM libraries require the datum */
428 EXIM_DATUM_INIT(value_datum);       /* to be cleared before use. */
429
430 yield = (EXIM_DBSCAN(dbblock->dbptr, key_datum, value_datum, start, *cursor))?
431   US EXIM_DATUM_DATA(key_datum) : NULL;
432
433 /* Some dbm require a termination */
434
435 if (!yield) EXIM_DBDELETE_CURSOR(*cursor);
436 return yield;
437 }
438
439
440
441 /*************************************************
442 **************************************************
443 *             Stand-alone test program           *
444 **************************************************
445 *************************************************/
446
447 #ifdef STAND_ALONE
448
449 int
450 main(int argc, char **cargv)
451 {
452 open_db dbblock[8];
453 int max_db = sizeof(dbblock)/sizeof(open_db);
454 int current = -1;
455 int showtime = 0;
456 int i;
457 dbdata_wait *dbwait = NULL;
458 uschar **argv = USS cargv;
459 uschar buffer[256];
460 uschar structbuffer[1024];
461
462 if (argc != 2)
463   {
464   printf("Usage: test_dbfn directory\n");
465   printf("The subdirectory called \"db\" in the given directory is used for\n");
466   printf("the files used in this test program.\n");
467   return 1;
468   }
469
470 /* Initialize */
471
472 spool_directory = argv[1];
473 debug_selector = D_all - D_memory;
474 debug_file = stderr;
475 big_buffer = malloc(big_buffer_size);
476
477 for (i = 0; i < max_db; i++) dbblock[i].dbptr = NULL;
478
479 printf("\nExim's db functions tester: interface type is %s\n", EXIM_DBTYPE);
480 printf("DBM library: ");
481
482 #ifdef DB_VERSION_STRING
483 printf("Berkeley DB: %s\n", DB_VERSION_STRING);
484 #elif defined(BTREEVERSION) && defined(HASHVERSION)
485   #ifdef USE_DB
486   printf("probably Berkeley DB version 1.8x (native mode)\n");
487   #else
488   printf("probably Berkeley DB version 1.8x (compatibility mode)\n");
489   #endif
490 #elif defined(_DBM_RDONLY) || defined(dbm_dirfno)
491 printf("probably ndbm\n");
492 #elif defined(USE_TDB)
493 printf("using tdb\n");
494 #else
495   #ifdef USE_GDBM
496   printf("probably GDBM (native mode)\n");
497   #else
498   printf("probably GDBM (compatibility mode)\n");
499   #endif
500 #endif
501
502 /* Test the functions */
503
504 printf("\nTest the functions\n> ");
505
506 while (Ufgets(buffer, 256, stdin) != NULL)
507   {
508   int len = Ustrlen(buffer);
509   int count = 1;
510   clock_t start = 1;
511   clock_t stop = 0;
512   uschar *cmd = buffer;
513   while (len > 0 && isspace((uschar)buffer[len-1])) len--;
514   buffer[len] = 0;
515
516   if (isdigit((uschar)*cmd))
517     {
518     count = Uatoi(cmd);
519     while (isdigit((uschar)*cmd)) cmd++;
520     while (isspace((uschar)*cmd)) cmd++;
521     }
522
523   if (Ustrncmp(cmd, "open", 4) == 0)
524     {
525     int i;
526     open_db *odb;
527     uschar *s = cmd + 4;
528     while (isspace((uschar)*s)) s++;
529
530     for (i = 0; i < max_db; i++)
531       if (dbblock[i].dbptr == NULL) break;
532
533     if (i >= max_db)
534       {
535       printf("Too many open databases\n> ");
536       continue;
537       }
538
539     start = clock();
540     odb = dbfn_open(s, O_RDWR, dbblock + i, TRUE, TRUE);
541     stop = clock();
542
543     if (odb)
544       {
545       current = i;
546       printf("opened %d\n", current);
547       }
548     /* Other error cases will have written messages */
549     else if (errno == ENOENT)
550       {
551       printf("open failed: %s%s\n", strerror(errno),
552         #ifdef USE_DB
553         " (or other Berkeley DB error)"
554         #else
555         ""
556         #endif
557         );
558       }
559     }
560
561   else if (Ustrncmp(cmd, "write", 5) == 0)
562     {
563     int rc = 0;
564     uschar *key = cmd + 5;
565     uschar *data;
566
567     if (current < 0)
568       {
569       printf("No current database\n");
570       continue;
571       }
572
573     while (isspace((uschar)*key)) key++;
574     data = key;
575     while (*data != 0 && !isspace((uschar)*data)) data++;
576     *data++ = 0;
577     while (isspace((uschar)*data)) data++;
578
579     dbwait = (dbdata_wait *)(&structbuffer);
580     Ustrcpy(dbwait->text, data);
581
582     start = clock();
583     while (count-- > 0)
584       rc = dbfn_write(dbblock + current, key, dbwait,
585         Ustrlen(data) + sizeof(dbdata_wait));
586     stop = clock();
587     if (rc != 0) printf("Failed: %s\n", strerror(errno));
588     }
589
590   else if (Ustrncmp(cmd, "read", 4) == 0)
591     {
592     uschar *key = cmd + 4;
593     if (current < 0)
594       {
595       printf("No current database\n");
596       continue;
597       }
598     while (isspace((uschar)*key)) key++;
599     start = clock();
600     while (count-- > 0)
601       dbwait = (dbdata_wait *)dbfn_read_with_length(dbblock+ current, key, NULL);
602     stop = clock();
603     printf("%s\n", (dbwait == NULL)? "<not found>" : CS dbwait->text);
604     }
605
606   else if (Ustrncmp(cmd, "delete", 6) == 0)
607     {
608     uschar *key = cmd + 6;
609     if (current < 0)
610       {
611       printf("No current database\n");
612       continue;
613       }
614     while (isspace((uschar)*key)) key++;
615     dbfn_delete(dbblock + current, key);
616     }
617
618   else if (Ustrncmp(cmd, "scan", 4) == 0)
619     {
620     EXIM_CURSOR *cursor;
621     BOOL startflag = TRUE;
622     uschar *key;
623     uschar keybuffer[256];
624     if (current < 0)
625       {
626       printf("No current database\n");
627       continue;
628       }
629     start = clock();
630     while ((key = dbfn_scan(dbblock + current, startflag, &cursor)) != NULL)
631       {
632       startflag = FALSE;
633       Ustrcpy(keybuffer, key);
634       dbwait = (dbdata_wait *)dbfn_read_with_length(dbblock + current,
635         keybuffer, NULL);
636       printf("%s: %s\n", keybuffer, dbwait->text);
637       }
638     stop = clock();
639     printf("End of scan\n");
640     }
641
642   else if (Ustrncmp(cmd, "close", 5) == 0)
643     {
644     uschar *s = cmd + 5;
645     while (isspace((uschar)*s)) s++;
646     i = Uatoi(s);
647     if (i >= max_db || dbblock[i].dbptr == NULL) printf("Not open\n"); else
648       {
649       start = clock();
650       dbfn_close(dbblock + i);
651       stop = clock();
652       dbblock[i].dbptr = NULL;
653       if (i == current) current = -1;
654       }
655     }
656
657   else if (Ustrncmp(cmd, "file", 4) == 0)
658     {
659     uschar *s = cmd + 4;
660     while (isspace((uschar)*s)) s++;
661     i = Uatoi(s);
662     if (i >= max_db || dbblock[i].dbptr == NULL) printf("Not open\n");
663       else current = i;
664     }
665
666   else if (Ustrncmp(cmd, "time", 4) == 0)
667     {
668     showtime = ~showtime;
669     printf("Timing %s\n", showtime? "on" : "off");
670     }
671
672   else if (Ustrcmp(cmd, "q") == 0 || Ustrncmp(cmd, "quit", 4) == 0) break;
673
674   else if (Ustrncmp(cmd, "help", 4) == 0)
675     {
676     printf("close  [<number>]              close file [<number>]\n");
677     printf("delete <key>                   remove record from current file\n");
678     printf("file   <number>                make file <number> current\n");
679     printf("open   <name>                  open db file\n");
680     printf("q[uit]                         exit program\n");
681     printf("read   <key>                   read record from current file\n");
682     printf("scan                           scan current file\n");
683     printf("time                           time display on/off\n");
684     printf("write  <key> <rest-of-line>    write record to current file\n");
685     }
686
687   else printf("Eh?\n");
688
689   if (showtime && stop >= start)
690     printf("start=%d stop=%d difference=%d\n", (int)start, (int)stop,
691      (int)(stop - start));
692
693   printf("> ");
694   }
695
696 for (i = 0; i < max_db; i++)
697   {
698   if (dbblock[i].dbptr != NULL)
699     {
700     printf("\nClosing %d", i);
701     dbfn_close(dbblock + i);
702     }
703   }
704
705 printf("\n");
706 return 0;
707 }
708
709 #endif
710
711 /* End of dbfn.c */