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 = <oken;
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