chiark / gitweb /
Commit 2.4.5-5 as unpacked
[innduct.git] / history / hisv6 / hisv6.c
1 /*  $Id: hisv6.c 7339 2005-06-20 03:16:41Z eagle $
2 **
3 **  History v6 implementation against the history API.
4 **
5 **  Copyright (c) 2001, Thus plc 
6 **  
7 **  Redistribution and use of the source code in source and binary 
8 **  forms, with or without modification, are permitted provided that
9 **  the following 3 conditions are met:
10 **  
11 **  1. Redistributions of the source code must retain the above 
12 **  copyright notice, this list of conditions and the disclaimer 
13 **  set out below. 
14 **  
15 **  2. Redistributions of the source code in binary form must 
16 **  reproduce the above copyright notice, this list of conditions 
17 **  and the disclaimer set out below in the documentation and/or 
18 **  other materials provided with the distribution. 
19 **  
20 **  3. Neither the name of the Thus plc nor the names of its 
21 **  contributors may be used to endorse or promote products 
22 **  derived from this software without specific prior written 
23 **  permission from Thus plc. 
24 **  
25 **  Disclaimer:
26 **  
27 **  "THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
28 **  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
29 **  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
30 **  A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE DIRECTORS
31 **  OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
32 **  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
33 **  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
34 **  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
35 **  THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
36 **  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
37 **  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
38 */
39
40 #include "config.h"
41 #include "clibrary.h"
42 #include <fcntl.h>
43 #include <limits.h>
44 #include <errno.h>
45 #include "hisinterface.h"
46 #include "hisv6.h"
47 #include "hisv6-private.h"
48 #include "dbz.h"
49 #include "inn/innconf.h"
50 #include "inn/timer.h"
51 #include "inn/qio.h"
52 #include "inn/sequence.h"
53 #include "inndcomm.h"
54
55 /*
56 **  because we can only have one open dbz per process, we keep a
57 **  pointer to which of the current history structures owns it
58 */
59 static struct hisv6 *hisv6_dbzowner;
60
61
62 /*
63 **  set error status to that indicated by s; doesn't copy the string,
64 **  assumes the caller did that for us
65 */
66 static void
67 hisv6_seterror(struct hisv6 *h, const char *s)
68 {
69     his_seterror(h->history, s);
70 }
71
72
73 /*
74 **  format line or offset into a string for error reporting
75 */
76 static void
77 hisv6_errloc(char *s, size_t line, off_t offset)
78 {
79     if (offset != -1) {
80         /* really we want an autoconf test for %ll/%L/%I64, sigh */
81         sprintf(s, "@%.0f", (double)offset);
82     } else {
83         sprintf(s, ":%lu", (unsigned long)line);
84     }
85 }
86
87
88 /*
89 **  split a history line into its constituent components; return a
90 **  bitmap indicating which components we're returning are valid (or
91 **  would be valid if a NULL pointer is passed for that component) or
92 **  -1 for error.  *error is set to a string which describes the
93 **  failure.
94 */
95 static int
96 hisv6_splitline(const char *line, const char **error, HASH *hash,
97                  time_t *arrived, time_t *posted, time_t *expires,
98                  TOKEN *token)
99 {
100     char *p = (char *)line;
101     unsigned long l;
102     int r = 0;
103
104     /* parse the [...] hash field */
105     if (*p != '[') {
106         *error = "`[' missing from history line";
107         return -1;
108     }
109     ++p;
110     if (hash)
111         *hash = TextToHash(p);
112     p += 32;
113     if (*p != ']') {
114         *error = "`]' missing from history line";
115         return -1;
116     }
117     ++p;
118     r |= HISV6_HAVE_HASH;
119     if (*p != HISV6_FIELDSEP) {
120         *error = "field separator missing from history line";
121         return -1;
122     }
123
124     /* parse the arrived field */
125     l = strtoul(p + 1, &p, 10);
126     if (l == ULONG_MAX) {
127         *error = "arrived timestamp out of range";
128         return -1;
129     }
130     r |= HISV6_HAVE_ARRIVED;
131     if (arrived)
132         *arrived = (time_t)l;
133     if (*p != HISV6_SUBFIELDSEP) {
134         /* no expires or posted time */
135         if (posted)
136             *posted = 0;
137         if (expires)
138             *expires = 0;
139     } else {
140         /* parse out the expires field */
141         ++p;
142         if (*p == HISV6_NOEXP) {
143             ++p;
144             if (expires)
145                 *expires = 0;
146         } else {
147             l = strtoul(p, &p, 10);
148             if (l == ULONG_MAX) {
149                 *error = "expires timestamp out of range";
150                 return -1;
151             }
152             r |= HISV6_HAVE_EXPIRES;
153             if (expires)
154                 *expires = (time_t)l;
155         }
156         /* parse out the posted field */
157         if (*p != HISV6_SUBFIELDSEP) {
158             /* no posted time */
159             if (posted)
160                 *posted = 0;
161         } else {
162             ++p;
163             l = strtoul(p, &p, 10);
164             if (l == ULONG_MAX) {
165                 *error = "posted timestamp out of range";
166                 return -1;
167             }
168             r |= HISV6_HAVE_POSTED;
169             if (posted)
170                 *posted = (time_t)l;
171         }
172     }
173
174     /* parse the token */
175     if (*p == HISV6_FIELDSEP)
176         ++p;
177     else if (*p != '\0') {
178         *error = "field separator missing from history line";
179         return -1;
180     }
181     /* IsToken false would imply a remembered line, or where someone's
182      * used prunehistory */
183     if (IsToken(p)) {
184         r |= HISV6_HAVE_TOKEN;
185         if (token)
186             *token = TextToToken(p);
187     }
188     return r;
189 }
190
191
192 /*
193 **  Given the time, now, return the time at which we should next check
194 **  the history file
195 */
196 static unsigned long
197 hisv6_nextcheck(struct hisv6 *h, unsigned long now)
198 {
199     return now + h->statinterval;
200 }
201
202
203 /*
204 **  close any dbz structures associated with h; we also manage the
205 **  single dbz instance voodoo
206 */
207 static bool
208 hisv6_dbzclose(struct hisv6 *h)
209 {
210     bool r = true;
211
212     if (h == hisv6_dbzowner) {
213         if (!hisv6_sync(h))
214             r = false;
215         if (!dbzclose()) {
216             hisv6_seterror(h, concat("can't dbzclose ",
217                                       h->histpath, " ",
218                                       strerror(errno), NULL));
219             r = false;
220         }
221         hisv6_dbzowner = NULL;
222     }
223     return r;
224 }
225
226
227 /*
228 **  close an existing history structure, cleaning it to the point
229 **  where we can reopon without leaking resources
230 */
231 static bool
232 hisv6_closefiles(struct hisv6 *h)
233 {
234     bool r = true;
235
236     if (!hisv6_dbzclose(h))
237         r = false;
238
239     if (h->readfd != -1) {
240         if (close(h->readfd) != 0 && errno != EINTR) {
241             hisv6_seterror(h, concat("can't close history ",
242                                       h->histpath, " ",
243                                       strerror(errno),NULL));
244             r = false;
245         }
246         h->readfd = -1;
247     }
248
249     if (h->writefp != NULL) {
250         if (ferror(h->writefp) || fflush(h->writefp) == EOF) {
251             hisv6_seterror(h, concat("error on history ",
252                                       h->histpath, " ",
253                                       strerror(errno), NULL));
254             r = false;
255         }
256         if (Fclose(h->writefp) == EOF) {
257             hisv6_seterror(h, concat("can't fclose history ",
258                                       h->histpath, " ",
259                                       strerror(errno), NULL));
260             r = false;
261         }
262         h->writefp = NULL;
263         h->offset = 0;
264     }
265
266     h->nextcheck = 0;
267     h->st.st_ino = (ino_t)-1;
268     h->st.st_dev = (dev_t)-1;
269     return r;
270 }
271
272
273 /*
274 **  Reopen (or open from fresh) a history structure; assumes the flags
275 **  & path are all set up, ready to roll. If we don't own the dbz, we
276 **  suppress the dbz code; this is needed during expiry (since the dbz
277 **  code doesn't yet understand multiple open contexts... yes its a
278 **  hack)
279 */
280 static bool
281 hisv6_reopen(struct hisv6 *h)
282 {
283     bool r = false;
284
285     if (h->flags & HIS_RDWR) {
286         const char *mode;
287
288         if (h->flags & HIS_CREAT)
289             mode = "w";
290         else
291             mode = "r+";
292         if ((h->writefp = Fopen(h->histpath, mode, INND_HISTORY)) == NULL) {
293             hisv6_seterror(h, concat("can't fopen history ",
294                                       h->histpath, " ",
295                                       strerror(errno), NULL));
296             hisv6_closefiles(h);
297             goto fail;
298         }
299         if (fseeko(h->writefp, 0, SEEK_END) == -1) {
300             hisv6_seterror(h, concat("can't fseek to end of ",
301                                       h->histpath, " ",
302                                       strerror(errno), NULL));
303             hisv6_closefiles(h);
304             goto fail;
305         }
306         h->offset = ftello(h->writefp);
307         if (h->offset == -1) {
308             hisv6_seterror(h, concat("can't ftello ", h->histpath, " ",
309                                       strerror(errno), NULL));
310             hisv6_closefiles(h);
311             goto fail;
312         }
313         close_on_exec(fileno(h->writefp), true);
314     }
315
316     /* Open the history file for reading. */
317     if ((h->readfd = open(h->histpath, O_RDONLY)) < 0) {
318         hisv6_seterror(h, concat("can't open ", h->histpath, " ",
319                                   strerror(errno), NULL));
320         hisv6_closefiles(h);
321         goto fail;
322     }
323     close_on_exec(h->readfd, true);
324     
325     /* if there's no current dbz owner, claim it here */
326     if (hisv6_dbzowner == NULL) {
327         hisv6_dbzowner = h;
328     }
329
330     /* During expiry we need two history structures in place, so we
331        have to select which one gets the dbz file */
332     if (h == hisv6_dbzowner) {
333         dbzoptions opt;
334
335         /* Open the DBZ file. */
336         dbzgetoptions(&opt);
337
338         /* HIS_INCORE usually means we're rebuilding from scratch, so
339            keep the whole lot in core until we flush */
340         if (h->flags & HIS_INCORE) {
341             opt.writethrough = false;
342             opt.pag_incore = INCORE_MEM;
343 #ifndef DO_TAGGED_HASH
344             opt.exists_incore = INCORE_MEM;
345 #endif
346         } else {
347             opt.writethrough = true;
348 #ifdef  DO_TAGGED_HASH
349             opt.pag_incore = INCORE_MMAP;
350 #else
351             /*opt.pag_incore = INCORE_NO;*/
352             opt.pag_incore = (h->flags & HIS_MMAP) ? INCORE_MMAP : INCORE_NO;
353             opt.exists_incore = (h->flags & HIS_MMAP) ? INCORE_MMAP : INCORE_NO;
354
355 # if defined(MMAP_NEEDS_MSYNC) && INND_DBZINCORE == 1
356             /* Systems that have MMAP_NEEDS_MSYNC defined will have their
357                on-disk copies out of sync with the mmap'ed copies most of
358                the time.  So if innd is using INCORE_MMAP, then we force
359                everything else to use it, too (unless we're on NFS) */
360             if(!innconf->nfsreader) {
361                 opt.pag_incore = INCORE_MMAP;
362                 opt.exists_incore = INCORE_MMAP;
363             }
364 # endif
365 #endif
366         }
367         dbzsetoptions(opt);
368         if (h->flags & HIS_CREAT) {
369             size_t npairs;
370                 
371             /* must only do this once! */
372             h->flags &= ~HIS_CREAT;
373             npairs = (h->npairs == -1) ? 0 : h->npairs;
374             if (!dbzfresh(h->histpath, dbzsize(npairs))) {
375                 hisv6_seterror(h, concat("can't dbzfresh ", h->histpath, " ",
376                                           strerror(errno), NULL));
377                 hisv6_closefiles(h);
378                 goto fail;
379             }
380         } else if (!dbzinit(h->histpath)) {
381             hisv6_seterror(h, concat("can't dbzinit ", h->histpath, " ",
382                                       strerror(errno), NULL));
383             hisv6_closefiles(h);
384             goto fail;
385         }
386     }
387     h->nextcheck = hisv6_nextcheck(h, TMRnow());
388     r = true;
389  fail:
390     return r;
391 }
392
393
394 /*
395 ** check if the history file has changed, if so rotate to the new
396 ** history file. Returns false on failure (which is probably fatal as
397 ** we'll have closed the files)
398 */
399 static bool
400 hisv6_checkfiles(struct hisv6 *h)
401 {
402     unsigned long t = TMRnow();
403
404     if (h->statinterval == 0)
405         return true;
406
407     if (h->readfd == -1) {
408         /* this can happen if a previous checkfiles() has failed to
409          * reopen the handles, but our caller hasn't realised... */
410         hisv6_closefiles(h);
411         if (!hisv6_reopen(h)) {
412             hisv6_closefiles(h);
413             return false;
414         }
415     }
416     if (seq_lcompare(t, h->nextcheck) == 1) {
417         struct stat st;
418
419         if (stat(h->histpath, &st) == 0 &&
420             (st.st_ino != h->st.st_ino ||
421              st.st_dev != h->st.st_dev)) {
422             /* there's a possible race on the history file here... */
423             hisv6_closefiles(h);
424             if (!hisv6_reopen(h)) {
425                 hisv6_closefiles(h);
426                 return false;
427             }
428             h->st = st;
429         }
430         h->nextcheck = hisv6_nextcheck(h, t);
431     }
432     return true;
433 }
434
435
436 /*
437 **  dispose (and clean up) an existing history structure
438 */
439 static bool
440 hisv6_dispose(struct hisv6 *h)
441 {
442     bool r;
443
444     r = hisv6_closefiles(h);
445     if (h->histpath) {
446         free(h->histpath);
447         h->histpath = NULL;
448     }
449
450     free(h);
451     return r;
452 }
453
454
455 /*
456 **  return a newly constructed, but empty, history structure
457 */
458 static struct hisv6 *
459 hisv6_new(const char *path, int flags, struct history *history)
460 {
461     struct hisv6 *h;
462
463     h = xmalloc(sizeof *h);
464     h->histpath = path ? xstrdup(path) : NULL;
465     h->flags = flags;
466     h->writefp = NULL;
467     h->offset = 0;
468     h->history = history;
469     h->readfd = -1;
470     h->nextcheck = 0;
471     h->statinterval = 0;
472     h->npairs = 0;
473     h->dirty = 0;
474     h->synccount = 0;
475     h->st.st_ino = (ino_t)-1;
476     h->st.st_dev = (dev_t)-1;
477     return h;
478 }
479
480
481 /*
482 **  open the history database identified by path in mode flags
483 */
484 void *
485 hisv6_open(const char *path, int flags, struct history *history)
486 {
487     struct hisv6 *h;
488
489     his_logger("HISsetup begin", S_HISsetup);
490
491     h = hisv6_new(path, flags, history);
492     if (path) {
493         if (!hisv6_reopen(h)) {
494             hisv6_dispose(h);
495             h = NULL;
496         }
497     }
498     his_logger("HISsetup end", S_HISsetup);
499     return h;
500 }
501
502
503 /*
504 **  close and free a history handle
505 */
506 bool
507 hisv6_close(void *history)
508 {
509     struct hisv6 *h = history;
510     bool r;
511
512     his_logger("HISclose begin", S_HISclose);
513     r = hisv6_dispose(h);
514     his_logger("HISclose end", S_HISclose);
515     return r;
516 }
517
518
519 /*
520 **  synchronise any outstanding history changes to disk
521 */
522 bool
523 hisv6_sync(void *history)
524 {
525     struct hisv6 *h = history;
526     bool r = true;
527
528     if (h->writefp != NULL) {
529         his_logger("HISsync begin", S_HISsync);
530         if (fflush(h->writefp) == EOF) {
531             hisv6_seterror(h, concat("error on history ",
532                                       h->histpath, " ",
533                                       strerror(errno), NULL));
534             r = false;
535         }
536         if (h->dirty && h == hisv6_dbzowner) {
537             if (!dbzsync()) {
538                 hisv6_seterror(h, concat("can't dbzsync ", h->histpath,
539                                           " ", strerror(errno), NULL));
540                 r = false;
541             } else {
542                 h->dirty = 0;
543             }
544         }
545         his_logger("HISsync end", S_HISsync);
546     }
547     return r;
548 }
549
550
551 /*
552 **  fetch the line associated with `hash' in the history database into
553 **  buf; buf must be at least HISV6_MAXLINE+1 bytes. `poff' is filled
554 **  with the offset of the line in the history file.
555 */
556 static bool
557 hisv6_fetchline(struct hisv6 *h, const HASH *hash, char *buf, off_t *poff)
558 {
559     off_t offset;
560     bool r;
561
562     if (h != hisv6_dbzowner) {
563         hisv6_seterror(h, concat("dbz not open for this history file ",
564                                   h->histpath, NULL));
565         return false;
566     }
567     if ((h->flags & (HIS_RDWR | HIS_INCORE)) == (HIS_RDWR | HIS_INCORE)) {
568         /* need to fflush as we may be reading uncommitted data
569            written via writefp */
570         if (fflush(h->writefp) == EOF) {
571             hisv6_seterror(h, concat("error on history ",
572                                       h->histpath, " ",
573                                       strerror(errno), NULL));
574             r = false;
575             goto fail;
576         }
577     }
578
579     /* Get the seek value into the history file. */
580     errno = 0;
581     r = dbzfetch(*hash, &offset);
582 #ifdef ESTALE
583     /* If your history is on NFS need to deal with stale NFS
584      * handles */
585     if (!r && errno == ESTALE) {
586         hisv6_closefiles(h);
587         if (!hisv6_reopen(h)) {
588             hisv6_closefiles(h);
589             r = false;
590             goto fail;
591         }
592     }
593 #endif
594     if (r) {
595         ssize_t n;
596
597         do {
598             n = pread(h->readfd, buf, HISV6_MAXLINE, offset);
599 #ifdef ESTALE
600             if (n == -1 && errno == ESTALE) {
601                 hisv6_closefiles(h);
602                 if (!hisv6_reopen(h)) {
603                     hisv6_closefiles(h);
604                     r = false;
605                     goto fail;
606                 }
607             }
608 #endif
609         } while (n == -1 && errno == EINTR);
610         if (n >= HISV6_MINLINE) {
611             char *p;
612
613             buf[n] = '\0';
614             p = strchr(buf, '\n');
615             if (!p) {
616                 char location[HISV6_MAX_LOCATION];
617
618                 hisv6_errloc(location, (size_t)-1, offset);
619                 hisv6_seterror(h,
620                                 concat("can't locate end of line in history ",
621                                        h->histpath, location,
622                                        NULL));
623                 r = false;
624             } else {
625                 *p = '\0';
626                 *poff = offset;
627                 r = true;
628             }
629         } else {
630             char location[HISV6_MAX_LOCATION];
631
632             hisv6_errloc(location, (size_t)-1, offset);
633             hisv6_seterror(h, concat("line too short in history ",
634                                       h->histpath, location,
635                                       NULL));
636             r = false;
637
638         }
639     } else {
640         /* not found */
641         r = false;
642     }
643  fail:
644     return r;
645 }
646
647
648 /*
649 **  lookup up the entry `key' in the history database, returning
650 **  arrived, posted and expires (for those which aren't NULL
651 **  pointers), and any storage token associated with the entry.
652 **
653 **  If any of arrived, posted or expires aren't available, return zero
654 **  for that component.
655 */
656 bool
657 hisv6_lookup(void *history, const char *key, time_t *arrived,
658              time_t *posted, time_t *expires, TOKEN *token)
659 {
660     struct hisv6 *h = history;
661     HASH messageid;
662     bool r;
663     off_t offset;
664     char buf[HISV6_MAXLINE + 1];
665
666     his_logger("HISfilesfor begin", S_HISfilesfor);
667     hisv6_checkfiles(h);
668
669     messageid = HashMessageID(key);
670     r = hisv6_fetchline(h, &messageid, buf, &offset);
671     if (r == true) {
672         int status;
673         const char *error;
674
675         status = hisv6_splitline(buf, &error, NULL,
676                                   arrived, posted, expires, token);
677         if (status < 0) {
678             char location[HISV6_MAX_LOCATION];
679
680             hisv6_errloc(location, (size_t)-1, offset);
681             hisv6_seterror(h, concat(error, " ",
682                                       h->histpath, location,
683                                       NULL));
684             r = false;
685         } else {
686             /* if we have a token then we have the article */
687             r = !!(status & HISV6_HAVE_TOKEN);
688         }
689     }
690     his_logger("HISfilesfor end", S_HISfilesfor);
691     return r;
692 }
693
694
695 /*
696 **  check `key' has been seen in this history database
697 */
698 bool
699 hisv6_check(void *history, const char *key)
700 {
701     struct hisv6 *h = history;
702     bool r;    
703     HASH hash;
704     
705     if (h != hisv6_dbzowner) {
706         hisv6_seterror(h, concat("dbz not open for this history file ",
707                                   h->histpath, NULL));
708         return false;
709     }
710
711     his_logger("HIShavearticle begin", S_HIShavearticle);
712     hisv6_checkfiles(h);
713     hash = HashMessageID(key);
714     r = dbzexists(hash);
715     his_logger("HIShavearticle end", S_HIShavearticle);
716     return r;
717 }
718
719
720 /*
721 **  Format a history line.  s should hold at least HISV6_MAXLINE + 1
722 **  characters (to allow for the nul).  Returns the length of the data
723 **  written, 0 if there was some error or if the data was too long to write.
724 */
725 static int
726 hisv6_formatline(char *s, const HASH *hash, time_t arrived,
727                   time_t posted, time_t expires, const TOKEN *token)
728 {
729     int i;
730     const char *hashtext = HashToText(*hash);
731
732     if (token == NULL) {
733         i = snprintf(s, HISV6_MAXLINE, "[%s]%c%lu%c%c\n",
734                      hashtext, HISV6_FIELDSEP,
735                      (unsigned long)arrived, HISV6_SUBFIELDSEP, HISV6_NOEXP);
736     } else {
737         const char *texttok;
738
739         texttok = TokenToText(*token);
740         if (expires <= 0) {
741             i = snprintf(s, HISV6_MAXLINE, "[%s]%c%lu%c%c%c%lu%c%s\n",
742                          hashtext, HISV6_FIELDSEP,
743                          (unsigned long)arrived, HISV6_SUBFIELDSEP,
744                          HISV6_NOEXP, HISV6_SUBFIELDSEP,
745                          (unsigned long)posted, HISV6_FIELDSEP,
746                          texttok);
747         } else {
748             i = snprintf(s, HISV6_MAXLINE, "[%s]%c%lu%c%lu%c%lu%c%s\n",
749                          hashtext, HISV6_FIELDSEP,
750                          (unsigned long)arrived, HISV6_SUBFIELDSEP,
751                          (unsigned long)expires, HISV6_SUBFIELDSEP,
752                          (unsigned long)posted, HISV6_FIELDSEP,
753                          texttok);
754         }
755     }
756     if (i < 0 || i >= HISV6_MAXLINE)
757         return 0;
758     return i;
759 }
760
761
762 /*
763 **  write the hash and offset to the dbz
764 */
765 static bool
766 hisv6_writedbz(struct hisv6 *h, const HASH *hash, off_t offset)
767 {
768     bool r;
769     char location[HISV6_MAX_LOCATION];
770     const char *error;
771
772     /* store the offset in the database */
773     switch (dbzstore(*hash, offset)) {
774     case DBZSTORE_EXISTS:
775         error = "dbzstore duplicate message-id ";
776         /* not `false' so that we duplicate the pre-existing
777            behaviour */
778         r = true;
779         break;
780
781     case DBZSTORE_ERROR:
782         error = "dbzstore error ";
783         r = false;
784         break;
785
786     default:
787         error = NULL;
788         r = true;
789         break;
790     }
791     if (error) {
792         hisv6_errloc(location, (size_t)-1, offset);
793         hisv6_seterror(h, concat(error, h->histpath,
794                                   ":[", HashToText(*hash), "]",
795                                   location, " ", strerror(errno), NULL));
796     }
797     if (r && h->synccount != 0 && ++h->dirty >= h->synccount)
798         r = hisv6_sync(h);
799
800     return r;
801 }
802
803
804 /*
805 **  write a history entry, hash, with times arrived, posted and
806 **  expires, and storage token.
807 */
808 static bool
809 hisv6_writeline(struct hisv6 *h, const HASH *hash, time_t arrived,
810                 time_t posted, time_t expires, const TOKEN *token)
811 {
812     bool r;
813     size_t i, length;
814     char hisline[HISV6_MAXLINE + 1];
815     char location[HISV6_MAX_LOCATION];
816
817     if (h != hisv6_dbzowner) {
818         hisv6_seterror(h, concat("dbz not open for this history file ",
819                                   h->histpath, NULL));
820         return false;
821     }
822
823     if (!(h->flags & HIS_RDWR)) {
824         hisv6_seterror(h, concat("history not open for writing ",
825                                   h->histpath, NULL));
826         return false;
827     }
828
829     length = hisv6_formatline(hisline, hash, arrived, posted, expires, token);
830     if (length == 0) {
831         hisv6_seterror(h, concat("error formatting history line ",
832                                   h->histpath, NULL));
833         return false;
834     }   
835
836     i = fwrite(hisline, 1, length, h->writefp);
837
838     /* If the write failed, the history line is now an orphan.  Attempt to
839        rewind the write pointer to our offset to avoid leaving behind a
840        partial write and desyncing the offset from our file position. */
841     if (i < length ||
842         (!(h->flags & HIS_INCORE) && fflush(h->writefp) == EOF)) {
843         hisv6_errloc(location, (size_t)-1, h->offset);
844         hisv6_seterror(h, concat("can't write history ", h->histpath,
845                                   location, " ", strerror(errno), NULL));
846         if (fseeko(h->writefp, h->offset, SEEK_SET) == -1)
847             h->offset += i;
848         r = false;
849         goto fail;
850     }
851
852     r = hisv6_writedbz(h, hash, h->offset);
853     h->offset += length;     /* increment regardless of error from writedbz */
854  fail:
855     return r;
856 }
857
858
859 /*
860 **  write a history entry, key, with times arrived, posted and
861 **  expires, and storage token.
862 */
863 bool
864 hisv6_write(void *history, const char *key, time_t arrived,
865             time_t posted, time_t expires, const TOKEN *token)
866 {
867     struct hisv6 *h = history;
868     HASH hash;
869     bool r;
870
871     his_logger("HISwrite begin", S_HISwrite);
872     hash = HashMessageID(key);
873     r = hisv6_writeline(h, &hash, arrived, posted, expires, token);
874     his_logger("HISwrite end", S_HISwrite);
875     return r;
876 }
877
878
879 /*
880 **  remember a history entry, key, with arrival time arrived.
881 */
882 bool
883 hisv6_remember(void *history, const char *key, time_t arrived)
884 {
885     struct hisv6 *h = history;
886     HASH hash;
887     bool r;
888
889     his_logger("HISwrite begin", S_HISwrite);
890     hash = HashMessageID(key);
891     r = hisv6_writeline(h, &hash, arrived, 0, 0, NULL);
892     his_logger("HISwrite end", S_HISwrite);
893     return r;
894 }
895
896
897 /*
898 **  replace an existing history entry, `key', with times arrived,
899 **  posted and expires, and (optionally) storage token `token'. The
900 **  new history line must fit in the space allocated for the old one -
901 **  if it had previously just been HISremember()ed you'll almost
902 **  certainly lose.
903 */
904 bool
905 hisv6_replace(void *history, const char *key, time_t arrived,
906               time_t posted, time_t expires, const TOKEN *token)
907 {
908     struct hisv6 *h = history;
909     HASH hash;
910     bool r;
911     off_t offset;
912     char old[HISV6_MAXLINE + 1];
913
914     if (!(h->flags & HIS_RDWR)) {
915         hisv6_seterror(h, concat("history not open for writing ",
916                                   h->histpath, NULL));
917         return false;
918     }
919
920     hash = HashMessageID(key);
921     r = hisv6_fetchline(h, &hash, old, &offset);
922     if (r == true) {
923         char new[HISV6_MAXLINE + 1];
924
925         if (hisv6_formatline(new, &hash, arrived, posted, expires,
926                              token) == 0) {
927             hisv6_seterror(h, concat("error formatting history line ",
928                                       h->histpath, NULL));
929             r = false;
930         } else {
931             size_t oldlen, newlen;
932
933             oldlen = strlen(old);
934             newlen = strlen(new);
935             if (new[newlen - 1] == '\n')
936                 newlen--;
937             if (newlen > oldlen) {
938                 hisv6_seterror(h, concat("new history line too long ",
939                                           h->histpath, NULL));
940                 r = false;
941             } else {
942                 ssize_t n;
943
944                 /* space fill any excess in the tail of new */
945                 memset(new + newlen, ' ', oldlen - newlen);
946
947                 do {
948                     n = pwrite(fileno(h->writefp), new, oldlen, offset);
949                 } while (n == -1 && errno == EINTR);
950                 if (n != oldlen) {
951                     char location[HISV6_MAX_LOCATION];
952
953                     hisv6_errloc(location, (size_t)-1, offset);
954                     hisv6_seterror(h, concat("can't write history ",
955                                               h->histpath, location, " ",
956                                               strerror(errno), NULL));
957                     r = false;
958                 }
959             }
960         }
961     }
962     return r;
963 }
964
965
966 /*
967 **  traverse a history database, passing the pieces through a
968 **  callback; note that we have more parameters in the callback than
969 **  the public interface, we add the internal history struct and the
970 **  message hash so we can use those if we need them. If the callback
971 **  returns false we abort the traversal.
972 **/
973 static bool
974 hisv6_traverse(struct hisv6 *h, struct hisv6_walkstate *cookie,
975                const char *reason,
976                bool (*callback)(struct hisv6 *, void *, const HASH *hash,
977                                 time_t, time_t, time_t,
978                                 const TOKEN *))
979 {
980     bool r = false;
981     QIOSTATE *qp;
982     void *p;
983     size_t line;
984     char location[HISV6_MAX_LOCATION];
985
986     if ((qp = QIOopen(h->histpath)) == NULL) {
987         hisv6_seterror(h, concat("can't QIOopen history file ",
988                                   h->histpath, strerror(errno), NULL));
989         return false;
990     }
991
992     line = 1;
993     /* we come back to again after we hit EOF for the first time, when
994        we pause the server & clean up any lines which sneak through in
995        the interim */
996  again:
997     while ((p = QIOread(qp)) != NULL) {
998         time_t arrived, posted, expires;
999         int status;
1000         TOKEN token;
1001         HASH hash;
1002         const char *error;
1003
1004         status = hisv6_splitline(p, &error, &hash,
1005                                   &arrived, &posted, &expires, &token);
1006         if (status > 0) {
1007             r = (*callback)(h, cookie, &hash, arrived, posted, expires,
1008                             (status & HISV6_HAVE_TOKEN) ? &token : NULL);
1009             if (r == false)
1010                 hisv6_seterror(h, concat("callback failed ",
1011                                           h->histpath, NULL));
1012         } else {
1013             hisv6_errloc(location, line, (off_t)-1);
1014             hisv6_seterror(h, concat(error, " ", h->histpath, location,
1015                                       NULL));
1016             /* if we're not ignoring errors set the status */
1017             if (!cookie->ignore)
1018                 r = false;
1019         }
1020         if (r == false)
1021             goto fail;
1022         ++line;
1023     }
1024
1025     if (p == NULL) {
1026         /* read or line-format error? */
1027         if (QIOerror(qp) || QIOtoolong(qp)) {
1028             hisv6_errloc(location, line, (off_t)-1);
1029             if (QIOtoolong(qp)) {
1030                 hisv6_seterror(h, concat("line too long ",
1031                                          h->histpath, location, NULL));
1032                 /* if we're not ignoring errors set the status */
1033                 if (!cookie->ignore)
1034                     r = false;
1035             } else {
1036                 hisv6_seterror(h, concat("can't read line ",
1037                                          h->histpath, location, " ",
1038                                          strerror(errno), NULL));
1039                 r = false;
1040             }
1041             if (r == false)
1042                 goto fail;
1043         }
1044
1045         /* must have been EOF, pause the server & clean up any
1046          * stragglers */
1047         if (reason && !cookie->paused) {
1048             if (ICCpause(reason) != 0) {
1049                 hisv6_seterror(h, concat("can't pause server ",
1050                                           h->histpath, strerror(errno), NULL));
1051                 r = false;
1052                 goto fail;
1053             }
1054             cookie->paused = true;
1055             goto again;
1056         }
1057     }
1058  fail:
1059     QIOclose(qp);
1060     return r;
1061 }
1062
1063
1064 /*
1065 **  internal callback used during hisv6_traverse; we just pass on the
1066 **  parameters the user callback expects
1067 **/
1068 static bool
1069 hisv6_traversecb(struct hisv6 *h UNUSED, void *cookie, const HASH *hash UNUSED,
1070                  time_t arrived, time_t posted, time_t expires,
1071                  const TOKEN *token)
1072 {
1073     struct hisv6_walkstate *hiscookie = cookie;
1074
1075     return (*hiscookie->cb.walk)(hiscookie->cookie,
1076                                  arrived, posted, expires,
1077                                  token);
1078 }
1079
1080
1081 /*
1082 **  history API interface to the database traversal routine
1083 */
1084 bool
1085 hisv6_walk(void *history, const char *reason, void *cookie,
1086            bool (*callback)(void *, time_t, time_t, time_t,
1087                             const TOKEN *))
1088 {
1089     struct hisv6 *h = history;
1090     struct hisv6_walkstate hiscookie;
1091     bool r;
1092
1093     /* our internal walk routine passes too many parameters, so add a
1094        wrapper */
1095     hiscookie.cb.walk = callback;
1096     hiscookie.cookie = cookie;
1097     hiscookie.new = NULL;
1098     hiscookie.paused = false;
1099     hiscookie.ignore = false;
1100
1101     r = hisv6_traverse(h, &hiscookie, reason, hisv6_traversecb);
1102
1103     return r;
1104 }
1105
1106
1107 /*
1108 **  internal callback used during expire
1109 **/
1110 static bool
1111 hisv6_expirecb(struct hisv6 *h, void *cookie, const HASH *hash,
1112                 time_t arrived, time_t posted, time_t expires,
1113                 const TOKEN *token)
1114 {
1115     struct hisv6_walkstate *hiscookie = cookie;
1116     bool r = true;
1117
1118     /* check if we've seen this message id already */
1119     if (hiscookie->new && dbzexists(*hash)) {
1120         /* continue after duplicates, it serious, but not fatal */
1121         hisv6_seterror(h, concat("duplicate message-id [",
1122                                  HashToText(*hash), "] in history ",
1123                                  hiscookie->new->histpath, NULL));
1124     } else {
1125         struct hisv6_walkstate *hiscookie = cookie;
1126         TOKEN ltoken, *t;
1127
1128         /* if we have a token pass it to the discrimination function */
1129         if (token) {
1130             bool keep;
1131
1132             /* make a local copy of the token so the callback can
1133              * modify it */
1134             ltoken = *token;
1135             t = &ltoken;
1136             keep = (*hiscookie->cb.expire)(hiscookie->cookie,
1137                                            arrived, posted, expires,
1138                                            t);
1139             /* if the callback returns true, we should keep the
1140                token for the time being, else we just remember
1141                it */
1142             if (keep == false) {
1143                 t = NULL;
1144                 posted = expires = 0;
1145             }
1146         } else {
1147             t = NULL;
1148         }
1149         if (hiscookie->new &&
1150             (t != NULL || arrived >= hiscookie->threshold)) {
1151             r = hisv6_writeline(hiscookie->new, hash,
1152                                  arrived, posted, expires, t);
1153         }
1154     }
1155     return r;
1156 }
1157
1158
1159 /*
1160 **  unlink files associated with the history structure h
1161 */
1162 static bool
1163 hisv6_unlink(struct hisv6 *h)
1164 {
1165     bool r = true;
1166     char *p;
1167
1168 #ifdef DO_TAGGED_HASH
1169     p = concat(h->histpath, ".pag", NULL);
1170     r = (unlink(p) == 0) && r;
1171     free(p);
1172 #else
1173     p = concat(h->histpath, ".index", NULL);
1174     r = (unlink(p) == 0) && r;
1175     free(p);
1176
1177     p = concat(h->histpath, ".hash", NULL);
1178     r = (unlink(p) == 0) && r;
1179     free(p);
1180 #endif
1181     
1182     p = concat(h->histpath, ".dir", NULL);
1183     r = (unlink(p) == 0) && r;
1184     free(p);
1185
1186     r = (unlink(h->histpath) == 0) && r;
1187     return r;
1188 }
1189
1190
1191 /*
1192 **  rename files associated with hold to hnew
1193 */
1194 static bool
1195 hisv6_rename(struct hisv6 *hold, struct hisv6 *hnew)
1196 {
1197     bool r = true;
1198     char *old, *new;
1199
1200 #ifdef DO_TAGGED_HASH
1201     old = concat(hold->histpath, ".pag", NULL);
1202     new = concat(hnew->histpath, ".pag", NULL);
1203     r = (rename(old, new) == 0) && r;
1204     free(old);
1205     free(new);
1206 #else
1207     old = concat(hold->histpath, ".index", NULL);
1208     new = concat(hnew->histpath, ".index", NULL);
1209     r = (rename(old, new) == 0) && r;
1210     free(old);
1211     free(new);
1212
1213     old = concat(hold->histpath, ".hash", NULL);
1214     new = concat(hnew->histpath, ".hash", NULL);
1215     r = (rename(old, new) == 0) && r;
1216     free(old);
1217     free(new);
1218 #endif
1219     
1220     old = concat(hold->histpath, ".dir", NULL);
1221     new = concat(hnew->histpath, ".dir", NULL);
1222     r = (rename(old, new) == 0) && r;
1223     free(old);
1224     free(new);
1225
1226     r = (rename(hold->histpath, hnew->histpath) == 0) && r;
1227     return r;
1228 }
1229
1230
1231 /*
1232 **  expire the history database, history.
1233 */
1234 bool
1235 hisv6_expire(void *history, const char *path, const char *reason,
1236              bool writing, void *cookie, time_t threshold,
1237              bool (*exists)(void *, time_t, time_t, time_t, TOKEN *))
1238 {
1239     struct hisv6 *h = history, *hnew = NULL;
1240     const char *nhistory = NULL;
1241     dbzoptions opt;
1242     bool r;
1243     struct hisv6_walkstate hiscookie;
1244
1245     /* this flag is always tested in the fail clause, so initialise it
1246        now */
1247     hiscookie.paused = false;
1248
1249     /* during expire we ignore errors whilst reading the history file
1250      * so any errors in it get fixed automagically */
1251     hiscookie.ignore = true;
1252
1253     if (writing && (h->flags & HIS_RDWR)) {
1254         hisv6_seterror(h, concat("can't expire from read/write history ",
1255                                  h->histpath, NULL));
1256         r = false;
1257         goto fail;
1258     }
1259
1260     if (writing) {
1261         /* form base name for new history file */
1262         if (path != NULL) {
1263             nhistory = concat(path, ".n", NULL);
1264         } else {
1265             nhistory = concat(h->histpath, ".n", NULL);
1266         }
1267
1268         hnew = hisv6_new(nhistory, HIS_CREAT | HIS_RDWR | HIS_INCORE,
1269                          h->history);
1270         if (!hisv6_reopen(hnew)) {
1271             hisv6_dispose(hnew);
1272             hnew = NULL;
1273             r = false;
1274             goto fail;
1275         }
1276
1277         /* this is icky... we can only have one dbz open at a time; we
1278            really want to make dbz take a state structure. For now we'll
1279            just close the existing one and create our new one they way we
1280            need it */
1281         if (!hisv6_dbzclose(h)) {
1282             r = false;
1283             goto fail;
1284         }
1285
1286         dbzgetoptions(&opt);
1287         opt.writethrough = false;
1288         opt.pag_incore = INCORE_MEM;
1289 #ifndef DO_TAGGED_HASH
1290         opt.exists_incore = INCORE_MEM;
1291 #endif
1292         dbzsetoptions(opt);
1293
1294         if (h->npairs == 0) {
1295             if (!dbzagain(hnew->histpath, h->histpath)) {
1296                 hisv6_seterror(h, concat("can't dbzagain ",
1297                                          hnew->histpath, ":", h->histpath, 
1298                                          strerror(errno), NULL));
1299                 r = false;
1300                 goto fail;
1301             }
1302         } else {
1303             size_t npairs;
1304
1305             npairs = (h->npairs == -1) ? 0 : h->npairs;
1306             if (!dbzfresh(hnew->histpath, dbzsize(npairs))) {
1307                 hisv6_seterror(h, concat("can't dbzfresh ",
1308                                          hnew->histpath, ":", h->histpath, 
1309                                          strerror(errno), NULL));
1310                 r = false;
1311                 goto fail;
1312             }
1313         }
1314         hisv6_dbzowner = hnew;
1315     }
1316
1317     /* set up the callback handler */
1318     hiscookie.cb.expire = exists;
1319     hiscookie.cookie = cookie;
1320     hiscookie.new = hnew;
1321     hiscookie.threshold = threshold;
1322     r = hisv6_traverse(h, &hiscookie, reason, hisv6_expirecb);
1323
1324  fail:
1325     if (writing) {
1326         if (hnew && !hisv6_closefiles(hnew)) {
1327             /* error will already have been set */
1328             r = false;
1329         }
1330
1331         /* reopen will synchronise the dbz stuff for us */
1332         if (!hisv6_closefiles(h)) {
1333             /* error will already have been set */
1334             r = false;
1335         }
1336
1337         if (r) {
1338             /* if the new path was explicitly specified don't move the
1339                files around, our caller is planning to do it out of
1340                band */
1341             if (path == NULL) {
1342                 /* unlink the old files */
1343                 r = hisv6_unlink(h);
1344             
1345                 if (r) {
1346                     r = hisv6_rename(hnew, h);
1347                 }
1348             }
1349         } else if (hnew) {
1350             /* something went pear shaped, unlink the new files */
1351             hisv6_unlink(hnew);
1352         }
1353
1354         /* re-enable dbz on the old history file */
1355         if (!hisv6_reopen(h)) {
1356             hisv6_closefiles(h);
1357         }
1358     }
1359
1360     if (hnew && !hisv6_dispose(hnew))
1361         r = false;
1362     if (nhistory && nhistory != path)
1363         free((char *)nhistory);
1364     if (r == false && hiscookie.paused)
1365         ICCgo(reason);
1366     return r;
1367 }
1368
1369
1370 /*
1371 **  control interface
1372 */
1373 bool
1374 hisv6_ctl(void *history, int selector, void *val)
1375 {
1376     struct hisv6 *h = history;
1377     bool r = true;
1378
1379     switch (selector) {
1380     case HISCTLG_PATH:
1381         *(char **)val = h->histpath;
1382         break;
1383
1384     case HISCTLS_PATH:
1385         if (h->histpath) {
1386             hisv6_seterror(h, concat("path already set in handle", NULL));
1387             r = false;
1388         } else {
1389             h->histpath = xstrdup((char *)val);
1390             if (!hisv6_reopen(h)) {
1391                 free(h->histpath);
1392                 h->histpath = NULL;
1393                 r = false;
1394             }
1395         }
1396         break;
1397
1398     case HISCTLS_STATINTERVAL:
1399         h->statinterval = *(time_t *)val * 1000;
1400         break;
1401
1402     case HISCTLS_SYNCCOUNT:
1403         h->synccount = *(size_t *)val;
1404         break;
1405
1406     case HISCTLS_NPAIRS:
1407         h->npairs = (ssize_t)*(size_t *)val;
1408         break;
1409
1410     case HISCTLS_IGNOREOLD:
1411         if (h->npairs == 0 && *(bool *)val) {
1412             h->npairs = -1;
1413         } else if (h->npairs == -1 && !*(bool *)val) {
1414             h->npairs = 0;
1415         }
1416         break;
1417
1418     default:
1419         /* deliberately doesn't call hisv6_seterror as we don't want
1420          * to spam the error log if someone's passing in stuff which
1421          * would be relevant to a different history manager */
1422         r = false;
1423         break;
1424     }
1425     return r;
1426 }