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