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