chiark / gitweb /
Commit 2.4.5-5 as unpacked
[inn-innduct.git] / storage / timecaf / timecaf.c
1 /*  $Id: timecaf.c 7412 2005-10-09 03:44:35Z eagle $
2 **
3 **  Like the timehash storage method (and heavily inspired by it), but uses
4 **  the CAF library to store multiple articles in a single file.
5 */
6
7 #include "config.h"
8 #include "clibrary.h"
9 #include "portable/mmap.h"
10 #include <ctype.h>
11 #include <dirent.h>
12 #include <errno.h>
13 #include <fcntl.h>
14 #include <syslog.h>
15 #include <sys/stat.h>
16 #include <time.h>
17
18 #include "caf.h"
19 #include "inn/innconf.h"
20 #include "inn/wire.h"
21 #include "libinn.h"
22 #include "methods.h"
23 #include "timecaf.h"
24 #include "paths.h"
25
26 /* Needed for htonl() and friends on AIX 4.1. */
27 #include <netinet/in.h>
28
29 typedef struct {
30     char                *artdata; /* start of the article data -- may be mmaped */
31     char                *mmapbase; /* actual start of mmaped region (on pagesize bndry, not necessarily == artdaya */
32     unsigned int        artlen; /* art length. */
33     size_t              mmaplen; /* length of mmap region. */
34     DIR                 *top; /* open handle on top level dir. */
35     DIR                 *sec; /* open handle on the 2nd level directory */
36     DIR                 *ter; /* open handle on 3rd level dir. */
37     struct dirent       *topde; /* last entry we got from top */
38     struct dirent       *secde; /* last entry we got from sec */ 
39     struct dirent       *terde; /* last entry we got from sec */ 
40     CAFTOCENT           *curtoc; 
41     ARTNUM              curartnum;
42     CAFHEADER           curheader;
43 } PRIV_TIMECAF;
44
45 /* current path/fd for an open CAF file */
46 typedef struct {
47     char        *path; /* path to file. */
48     int         fd; /* open fd -- -1 if no file currently open. */
49 } CAFOPENFILE;
50
51 static CAFOPENFILE ReadingFile, WritingFile;
52 static char *DeletePath;
53 static ARTNUM *DeleteArtnums;
54 static unsigned int NumDeleteArtnums, MaxDeleteArtnums;
55
56 typedef enum {FIND_DIR, FIND_CAF, FIND_TOPDIR} FINDTYPE;
57
58 /*
59 ** Structures for the cache for stat information (to make expireover etc. 
60 ** faster. 
61 **
62 ** The first structure contains the TOC info for a single CAF file.  The 2nd
63 ** one has pointers to the info for up to 256 CAF files, indexed
64 ** by the 2nd least significant byte of the arrival time.
65 */
66
67 struct caftoccacheent {
68     CAFTOCENT *toc;
69     CAFHEADER header;
70 };
71 typedef struct caftoccacheent CAFTOCCACHEENT;
72
73 struct caftocl1cache {
74     CAFTOCCACHEENT *entries[256];
75 };
76 typedef struct caftocl1cache CAFTOCL1CACHE;
77
78 /*
79 ** and similar structures indexed by the 3rd and 4th bytes of the arrival time.
80 ** pointing to the lower level structures.  Note that the top level structure
81 ** (the one indexed by the MSByte of the timestamp) is likely to have only
82 ** one active pointer, unless your spool keeps more than 194 days of articles,
83 ** but it doesn't cost much to keep that one structure around and keep the
84 ** code general.
85 */
86
87 struct caftocl2cache {
88     CAFTOCL1CACHE *l1ptr[256];
89 };
90 typedef struct caftocl2cache CAFTOCL2CACHE;
91
92 struct caftocl3cache {
93     CAFTOCL2CACHE *l2ptr[256];
94 };
95 typedef struct caftocl3cache CAFTOCL3CACHE;
96
97 static CAFTOCL3CACHE *TOCCache[256]; /* indexed by storage class! */
98 static int TOCCacheHits, TOCCacheMisses;
99
100     
101 static TOKEN MakeToken(time_t now, int seqnum, STORAGECLASS class, TOKEN *oldtoken) {
102     TOKEN               token;
103     unsigned int        i;
104     unsigned short      s;
105
106     if (oldtoken == (TOKEN *)NULL)
107         memset(&token, '\0', sizeof(token));
108     else 
109         memcpy(&token, oldtoken, sizeof(token));
110     token.type = TOKEN_TIMECAF;
111     token.class = class;
112     i = htonl(now);
113     memcpy(token.token, &i, sizeof(i));
114     if (sizeof(i) > 4)
115         memmove(token.token, &token.token[sizeof(i) - 4], 4);
116     s = htons(seqnum);
117     memcpy(&token.token[4], &s + (sizeof(s) - 2), 2);
118     return token;
119 }
120
121
122 static void BreakToken(TOKEN token, int *now, int *seqnum) {
123     unsigned int        i;
124     unsigned short      s = 0;
125
126     memcpy(&i, token.token, sizeof(i));
127     memcpy(&s, &token.token[4], sizeof(s));
128     *now = ntohl(i);
129     *seqnum = (int)ntohs(s);
130 }
131
132 /* 
133 ** Note: the time here is really "time>>8", i.e. a timestamp that's been
134 ** shifted right by 8 bits.
135 */
136 static char *MakePath(int now, const STORAGECLASS class) {
137     char *path;
138     size_t length;
139     
140     /* innconf->patharticles + '/timecaf-zz/xx/xxxx.CF' */
141     length = strlen(innconf->patharticles) + 32;
142     path = xmalloc(length);
143     snprintf(path, length, "%s/timecaf-%02x/%02x/%02x%02x.CF",
144              innconf->patharticles, class,
145              (now >> 8) & 0xff, (now >> 16) & 0xff, now & 0xff);
146
147     return path;
148 }
149
150 static TOKEN *PathNumToToken(char *path, ARTNUM artnum) {
151     int                 n;
152     unsigned int        t1, t2, class;
153     unsigned int        timestamp;
154     static TOKEN        token;
155
156     n = sscanf(path, "timecaf-%02x/%02x/%04x.CF", &class, &t1, &t2);
157     if (n != 3)
158         return (TOKEN *)NULL;
159     timestamp = ((t1 << 8) & 0xff00) | ((t2 << 8) & 0xff0000) | ((t2 << 0) & 0xff);
160     token = MakeToken(timestamp, artnum, class, (TOKEN *)NULL);
161     return &token;
162 }
163
164
165 bool timecaf_init(SMATTRIBUTE *attr) {
166     if (attr == NULL) {
167         syslog(L_ERROR, "timecaf: attr is NULL");
168         SMseterror(SMERR_INTERNAL, "attr is NULL");
169         return false;
170     }
171     attr->selfexpire = false;
172     attr->expensivestat = false;
173     if (STORAGE_TOKEN_LENGTH < 6) {
174         syslog(L_FATAL, "timecaf: token length is less than 6 bytes");
175         SMseterror(SMERR_TOKENSHORT, NULL);
176         return false;
177     }
178     ReadingFile.fd = WritingFile.fd = -1;
179     ReadingFile.path = WritingFile.path = (char *)NULL;
180     return true;
181 }
182
183 /*
184 ** Routines for managing the 'TOC cache' (cache of TOCs of various CAF files)
185 **
186 ** Attempt to look up a given TOC entry in the cache.  Takes the timestamp
187 ** as arguments. 
188 */
189
190 static CAFTOCCACHEENT *
191 CheckTOCCache(int timestamp, int tokenclass)
192 {
193     CAFTOCL2CACHE *l2;
194     CAFTOCL1CACHE *l1;
195     CAFTOCCACHEENT *cent;
196     unsigned char tmp;
197
198     if (TOCCache[tokenclass] == NULL) return NULL; /* cache is empty */
199
200     tmp = (timestamp>>16) & 0xff;
201     l2 = TOCCache[tokenclass]->l2ptr[tmp];
202     if (l2 == NULL) return NULL;
203
204     tmp = (timestamp>>8) & 0xff;
205     l1 = l2->l1ptr[tmp];
206     if (l1 == NULL) return NULL;
207
208     tmp = (timestamp) & 0xff;
209     cent = l1->entries[tmp];
210
211     ++TOCCacheHits;
212     return cent;
213 }
214
215 /*
216 ** Add given TOC and header to the cache.  Assume entry is not already in
217 ** cache.
218 */
219 static CAFTOCCACHEENT *
220 AddTOCCache(int timestamp, CAFTOCENT *toc, CAFHEADER head, int tokenclass)
221 {
222     CAFTOCL2CACHE *l2;
223     CAFTOCL1CACHE *l1;
224     CAFTOCCACHEENT *cent;
225     unsigned char tmp;
226     int i;
227
228     if (TOCCache[tokenclass] == NULL) {
229         TOCCache[tokenclass] = xmalloc(sizeof(CAFTOCL3CACHE));
230         for (i = 0 ; i < 256 ; ++i) TOCCache[tokenclass]->l2ptr[i] = NULL;
231     }
232
233     tmp = (timestamp>>16) & 0xff;
234     l2 = TOCCache[tokenclass]->l2ptr[tmp];
235     if (l2 == NULL) {
236         TOCCache[tokenclass]->l2ptr[tmp] = l2 = xmalloc(sizeof(CAFTOCL2CACHE));
237         for (i = 0 ; i < 256 ; ++i) l2->l1ptr[i] = NULL;
238     }
239
240     tmp = (timestamp>>8) & 0xff;
241     l1 = l2->l1ptr[tmp];
242     if (l1 == NULL) {
243         l2->l1ptr[tmp] = l1 = xmalloc(sizeof(CAFTOCL1CACHE));
244         for (i = 0 ; i < 256 ; ++i) l1->entries[i] = NULL;
245     }
246
247     tmp = (timestamp) & 0xff;
248     cent = xmalloc(sizeof(CAFTOCCACHEENT));
249     l1->entries[tmp] = cent;
250
251     cent->header = head;
252     cent->toc = toc;
253     ++TOCCacheMisses;
254     return cent;
255 }
256
257 /*
258 ** Do stating of an article, going thru the TOC cache if possible. 
259 */
260
261 static ARTHANDLE *
262 StatArticle(int timestamp, ARTNUM artnum, int tokenclass)
263 {
264     CAFTOCCACHEENT *cent;
265     CAFTOCENT *toc;
266     CAFHEADER head;
267     char *path;
268     CAFTOCENT *tocentry;
269     ARTHANDLE *art;
270
271     cent = CheckTOCCache(timestamp,tokenclass);
272     if (cent == NULL) {
273         path = MakePath(timestamp, tokenclass);
274         toc = CAFReadTOC(path, &head);
275         if (toc == NULL) {
276             if (caf_error == CAF_ERR_ARTNOTHERE) {
277                 SMseterror(SMERR_NOENT, NULL);
278             } else {
279                 SMseterror(SMERR_UNDEFINED, NULL);
280             }
281             free(path);
282             return NULL;
283         }
284         cent = AddTOCCache(timestamp, toc, head, tokenclass);
285         free(path);
286     }
287     
288     /* check current TOC for the given artnum. */
289     if (artnum < cent->header.Low || artnum > cent->header.High) {
290         SMseterror(SMERR_NOENT, NULL);
291         return NULL;
292     }
293     
294     tocentry = &(cent->toc[artnum - cent->header.Low]);
295     if (tocentry->Size == 0) {
296         /* no article with that article number present */
297         SMseterror(SMERR_NOENT, NULL);
298         return NULL;
299     }
300
301     /* stat is a success, so build a null art struct to represent that. */
302     art = xmalloc(sizeof(ARTHANDLE));
303     art->type = TOKEN_TIMECAF;
304     art->data = NULL;
305     art->len = 0;
306     art->private = NULL;
307     return art;
308 }
309         
310
311 static void
312 CloseOpenFile(CAFOPENFILE *foo) {
313     if (foo->fd >= 0) {
314         close(foo->fd);
315         foo->fd = -1;
316         free(foo->path);
317         foo->path = NULL;
318     }
319 }
320
321 TOKEN timecaf_store(const ARTHANDLE article, const STORAGECLASS class) {
322     char                *path;
323     char                *p;
324     time_t              now;
325     int                 timestamp;
326     TOKEN               token;
327     int                 fd;
328     ssize_t             result;
329     ARTNUM              art;
330
331     if (article.arrived == (time_t)0)
332         now = time(NULL);
333     else
334         now = article.arrived;
335
336     timestamp = now>>8;
337     art = 0;  /* magic: 0=="next available article number. */
338
339     path = MakePath(timestamp, class);
340     /* check to see if we have this CAF file already open. */
341     if (WritingFile.fd < 0 || strcmp(WritingFile.path, path) != 0) {
342         /* we're writing to a different file, close old one and start new one. */
343         CloseOpenFile(&WritingFile);
344         fd = CAFOpenArtWrite(path, &art, true, article.len);
345         if (fd < 0) {
346             if (caf_error == CAF_ERR_IO && caf_errno == ENOENT) {
347                 /* directories in the path don't exist, try creating them. */
348                 p = strrchr(path, '/');
349                 *p = '\0';
350                 if (!MakeDirectory(path, true)) {
351                     syslog(L_ERROR, "timecaf: could not make directory %s %m", path);
352                     token.type = TOKEN_EMPTY;
353                     free(path);
354                     SMseterror(SMERR_UNDEFINED, NULL);
355                     return token;
356                 } else {
357                     *p = '/';
358                     fd = CAFOpenArtWrite(path, &art, true, article.len);
359                     if (fd < 0) {
360                         syslog(L_ERROR, "timecaf: could not OpenArtWrite %s/%ld, %s", path, art, CAFErrorStr());
361                         SMseterror(SMERR_UNDEFINED, NULL);
362                         free(path);
363                         token.type = TOKEN_EMPTY;
364                         return token;
365                     }
366                 } 
367             } else {
368                 syslog(L_ERROR, "timecaf: could not OpenArtWrite %s/%ld, %s", path, art, CAFErrorStr());
369                 SMseterror(SMERR_UNDEFINED, NULL);
370                 free(path);
371                 token.type = TOKEN_EMPTY;
372                 return token;
373             }
374         }
375     } else {
376         /* can reuse existing fd, assuming all goes well. */
377         fd = WritingFile.fd;
378
379         /* nuke extraneous copy of path to avoid mem leaks. */
380         free(path);
381         path = WritingFile.path;
382
383         if (CAFStartWriteFd(fd, &art, article.len) < 0) {
384             syslog(L_ERROR, "timecaf: could not OpenArtWrite %s/%ld, %s", path, art, CAFErrorStr());
385             SMseterror(SMERR_UNDEFINED, NULL);
386             free(path);
387             token.type = TOKEN_EMPTY;
388             return token;
389         }
390     }
391     WritingFile.fd = fd;
392     WritingFile.path = path;
393     close_on_exec(fd, true);
394     result = xwritev(fd, article.iov, article.iovcnt);
395     if (result != (ssize_t) article.len) {
396         SMseterror(SMERR_UNDEFINED, NULL);
397         syslog(L_ERROR, "timecaf error writing %s %m", path);
398         token.type = TOKEN_EMPTY;
399         CloseOpenFile(&WritingFile);
400         return token;
401     }
402     if (CAFFinishArtWrite(fd) < 0) { 
403         SMseterror(SMERR_UNDEFINED, NULL);
404         syslog(L_ERROR, "timecaf error writing %s %s", path, CAFErrorStr());
405         token.type = TOKEN_EMPTY;
406         CloseOpenFile(&WritingFile);
407         return token;
408     }
409     
410     return MakeToken(timestamp, art, class, article.token);
411 }
412
413 /* Get a handle to article artnum in CAF-file path. */
414 static ARTHANDLE *OpenArticle(const char *path, ARTNUM artnum, const RETRTYPE amount) {
415     int                 fd;
416     PRIV_TIMECAF        *private;
417     char                *p;
418     size_t              len;
419     ARTHANDLE           *art;
420     static long         pagesize = 0;
421
422     if (pagesize == 0) {
423         pagesize = getpagesize();
424         if (pagesize < 0) {
425             syslog(L_ERROR, "timecaf getpagesize failed: %m");
426             pagesize = 0;
427             return NULL;
428         }
429     }
430
431 /* XXX need to figure some way to cache open fds or something? */
432     if ((fd = CAFOpenArtRead((char *)path, artnum, &len)) < 0) {
433         if (caf_error == CAF_ERR_ARTNOTHERE) {
434             SMseterror(SMERR_NOENT, NULL);
435         } else {
436             SMseterror(SMERR_UNDEFINED, NULL);
437         }
438         return NULL;
439     }
440
441     art = xmalloc(sizeof(ARTHANDLE));
442     art->type = TOKEN_TIMECAF;
443
444     if (amount == RETR_STAT) {
445         art->data = NULL;
446         art->len = 0;
447         art->private = NULL;
448         close(fd);
449         return art;
450     }
451
452     private = xmalloc(sizeof(PRIV_TIMECAF));
453     art->private = (void *)private;
454     private->artlen = len;
455     if (innconf->articlemmap) {
456         off_t curoff, tmpoff;
457         size_t delta;
458
459         curoff = lseek(fd, (off_t) 0, SEEK_CUR);
460         delta = curoff % pagesize;
461         tmpoff = curoff - delta;
462         private->mmaplen = len + delta;
463         if ((private->mmapbase = mmap(NULL, private->mmaplen, PROT_READ, MAP_SHARED, fd, tmpoff)) == MAP_FAILED) {
464             SMseterror(SMERR_UNDEFINED, NULL);
465             syslog(L_ERROR, "timecaf: could not mmap article: %m");
466             free(art->private);
467             free(art);
468             return NULL;
469         }
470         mmap_invalidate(private->mmapbase, private->mmaplen);
471         if (amount == RETR_ALL)
472             madvise(private->mmapbase, private->mmaplen, MADV_WILLNEED);
473         else
474             madvise(private->mmapbase, private->mmaplen, MADV_SEQUENTIAL);
475         private->artdata = private->mmapbase + delta;
476     } else {
477         private->artdata = xmalloc(private->artlen);
478         if (read(fd, private->artdata, private->artlen) < 0) {
479             SMseterror(SMERR_UNDEFINED, NULL);
480             syslog(L_ERROR, "timecaf: could not read article: %m");
481             free(private->artdata);
482             free(art->private);
483             free(art);
484             return NULL;
485         }
486     }
487     close(fd);
488
489     private->top = NULL;
490     private->sec = NULL;
491     private->ter = NULL;
492     private->curtoc = NULL;
493     private->curartnum = 0;
494     private->topde = NULL;
495     private->secde = NULL;
496     private->terde = NULL;
497     
498     if (amount == RETR_ALL) {
499         art->data = private->artdata;
500         art->len = private->artlen;
501         return art;
502     }
503     
504     if ((p = wire_findbody(private->artdata, private->artlen)) == NULL) {
505         SMseterror(SMERR_NOBODY, NULL);
506         if (innconf->articlemmap)
507             munmap(private->mmapbase, private->mmaplen);
508         else
509             free(private->artdata);
510         free(art->private);
511         free(art);
512         return NULL;
513     }
514
515     if (amount == RETR_HEAD) {
516         art->data = private->artdata;
517         art->len = p - private->artdata;
518         return art;
519     }
520
521     if (amount == RETR_BODY) {
522         art->data = p + 4;
523         art->len = art->len - (private->artdata - p - 4);
524         return art;
525     }
526     SMseterror(SMERR_UNDEFINED, "Invalid retrieve request");
527     if (innconf->articlemmap)
528         munmap(private->mmapbase, private->mmaplen);
529     else
530         free(private->artdata);
531     free(art->private);
532     free(art);
533     return NULL;
534 }
535
536 ARTHANDLE *timecaf_retrieve(const TOKEN token, const RETRTYPE amount) {
537     int                 timestamp;
538     int                 artnum;
539     char                *path;
540     ARTHANDLE           *art;
541     static TOKEN        ret_token;
542     time_t              now;
543     
544     if (token.type != TOKEN_TIMECAF) {
545         SMseterror(SMERR_INTERNAL, NULL);
546         return NULL;
547     }
548
549     BreakToken(token, &timestamp, &artnum);
550
551     /*
552     ** Do a possible shortcut on RETR_STAT requests, going thru the "TOC cache"
553     ** we mentioned above.  We only try to go thru the TOC Cache under these
554     ** conditions:
555     **   1) SMpreopen is true (so we're "preopening" the TOCs.)
556     **   2) the timestamp is older than the timestamp corresponding to current
557     ** time. Any timestamp that matches current time (to within 256 secondsf
558     ** would be in a CAF file that innd is actively 
559     ** writing, in which case we would not want to cache the TOC for that
560     ** CAF file. 
561     */
562
563     if (SMpreopen && amount == RETR_STAT) {
564         now = time(NULL);
565         if (timestamp < ((now >> 8) & 0xffffff)) {
566             return StatArticle(timestamp, artnum, token.class);
567         }
568     }
569
570     path = MakePath(timestamp, token.class);
571     if ((art = OpenArticle(path, artnum, amount)) != (ARTHANDLE *)NULL) {
572         art->arrived = timestamp<<8; /* XXX not quite accurate arrival time,
573                                      ** but getting a more accurate one would 
574                                      ** require more fiddling with CAF innards.
575                                      */
576         ret_token = token;
577         art->token = &ret_token;
578     }
579     free(path);
580     return art;
581 }
582
583 void timecaf_freearticle(ARTHANDLE *article) {
584     PRIV_TIMECAF       *private;
585
586     if (!article)
587         return;
588     
589     if (article->private) {
590         private = (PRIV_TIMECAF *)article->private;
591         if (innconf->articlemmap)
592             munmap(private->mmapbase, private->mmaplen);
593         else
594             free(private->artdata);
595         if (private->top)
596             closedir(private->top);
597         if (private->sec)
598             closedir(private->sec);
599         if (private->ter)
600             closedir(private->ter);
601         if (private->curtoc) 
602             free(private->curtoc);
603         free(private);
604     }
605     free(article);
606 }
607
608 /* Do cancels of all the article ids collected for a given pathname. */
609
610 static void
611 DoCancels(void) {
612     if (DeletePath != NULL) {
613         if (NumDeleteArtnums != 0) {
614             /* 
615             ** Murgle. If we are trying to cancel something out of the
616             ** currently open-for-writing file, we need to close it before
617             ** doing CAFRemove...
618             */
619             if (WritingFile.path != NULL && strcmp(WritingFile.path, DeletePath) == 0) {
620                 CloseOpenFile(&WritingFile);
621             }
622             /* XXX should really check err. code here, but not much we can really do. */
623             CAFRemoveMultArts(DeletePath, NumDeleteArtnums, DeleteArtnums);
624             free(DeleteArtnums);
625             DeleteArtnums = NULL;
626             NumDeleteArtnums = MaxDeleteArtnums = 0;
627         }
628         free(DeletePath);
629         DeletePath = NULL;
630     }
631 }
632             
633 bool timecaf_cancel(TOKEN token) {
634     int                 now;
635     int                 seqnum;
636     char                *path;
637
638     BreakToken(token, &now, &seqnum);
639     path = MakePath(now, token.class);
640     if (DeletePath == NULL) {
641         DeletePath = path;
642     } else if (strcmp(DeletePath, path) != 0) {
643         /* different path, so flush all pending cancels. */
644         DoCancels();
645         DeletePath = path;
646     } else {
647         free(path); /* free redundant copy of path */
648     }
649     if (NumDeleteArtnums >= MaxDeleteArtnums) {
650         /* allocate/expand storage for artnums. */
651         if (MaxDeleteArtnums == 0) {
652             MaxDeleteArtnums = 100;
653         } else {
654             MaxDeleteArtnums *= 2;
655         }
656         DeleteArtnums = xrealloc(DeleteArtnums, MaxDeleteArtnums * sizeof(ARTNUM));
657     }
658     DeleteArtnums[NumDeleteArtnums++] = seqnum; 
659
660     return true;
661 }
662
663 static struct dirent *FindDir(DIR *dir, FINDTYPE type) {
664     struct dirent       *de;
665     
666     while ((de = readdir(dir)) != NULL) {
667         if (type == FIND_TOPDIR)
668             if ((strlen(de->d_name) == 10) &&
669                 (strncmp(de->d_name, "timecaf-", 8) == 0) &&
670                 CTYPE(isxdigit, de->d_name[8]) &&
671                 CTYPE(isxdigit, de->d_name[9]))
672                 return de;
673
674         if (type == FIND_DIR)
675             if ((strlen(de->d_name) == 2)
676                 && CTYPE(isxdigit, de->d_name[0])
677                 && CTYPE(isxdigit, de->d_name[1]))
678                 return de;
679
680         if (type == FIND_CAF)
681             if ((strlen(de->d_name) == 7) &&
682                 CTYPE(isxdigit, de->d_name[0]) &&
683                 CTYPE(isxdigit, de->d_name[1]) &&
684                 CTYPE(isxdigit, de->d_name[2]) &&
685                 CTYPE(isxdigit, de->d_name[3]) &&
686                 (de->d_name[4] == '.') &&
687                 (de->d_name[5] == 'C') &&
688                 (de->d_name[6] == 'F'))
689                 return de;
690         }
691
692     return NULL;
693 }
694
695 /* Grovel thru a CAF table-of-contents finding the next still-existing article */
696 static int
697 FindNextArt(const CAFHEADER *head, CAFTOCENT *toc, ARTNUM *artp)
698 {
699     ARTNUM art;
700     CAFTOCENT *tocp;
701     art = *artp;
702     if (art == 0) {
703         art = head->Low - 1; /* we never use art # 0, so 0 is a flag to start
704                                searching at the beginning */
705     }
706     while (true) {
707         art++;
708         if (art > head->High) return false; /* ran off the end of the TOC */
709         tocp = &toc[art - head->Low];
710         if (tocp->Size != 0) {
711             /* got a valid article */
712             *artp = art;
713             return true;
714         }
715     }
716 }
717
718
719
720 ARTHANDLE *timecaf_next(const ARTHANDLE *article, const RETRTYPE amount) {
721     PRIV_TIMECAF        priv, *newpriv;
722     char                *path;
723     ARTHANDLE           *art;
724     size_t              length;
725
726     length = strlen(innconf->patharticles) + 32;
727     path = xmalloc(length);
728     if (article == NULL) {
729         priv.top = NULL;
730         priv.sec = NULL;
731         priv.ter = NULL;
732         priv.curtoc = NULL;
733         priv.topde = NULL;
734         priv.secde = NULL;
735         priv.terde = NULL;
736     } else {
737         priv = *(PRIV_TIMECAF *)article->private;
738         free(article->private);
739         free((void *)article);
740         if (innconf->articlemmap)
741             munmap(priv.mmapbase, priv.mmaplen);
742         else
743             free(priv.artdata);
744     }
745
746     while (priv.curtoc == NULL || !FindNextArt(&priv.curheader, priv.curtoc, &priv.curartnum)) {
747         if (priv.curtoc) {
748             free(priv.curtoc);
749             priv.curtoc = NULL;
750         }
751         while (!priv.ter || ((priv.terde = FindDir(priv.ter, FIND_CAF)) == NULL)) {
752             if (priv.ter) {
753                 closedir(priv.ter);
754                 priv.ter = NULL;
755             }
756             while (!priv.sec || ((priv.secde = FindDir(priv.sec, FIND_DIR)) == NULL)) {
757                 if (priv.sec) {
758                     closedir(priv.sec);
759                     priv.sec = NULL;
760                 }
761                 if (!priv.top || ((priv.topde = FindDir(priv.top, FIND_TOPDIR)) == NULL)) {
762                     if (priv.top) {
763                         /* end of search */
764                         closedir(priv.top);
765                         priv.top = NULL;
766                         free(path);
767                         return NULL;
768                     }
769                     snprintf(path, length, "%s", innconf->patharticles);
770                     if ((priv.top = opendir(path)) == NULL) {
771                         SMseterror(SMERR_UNDEFINED, NULL);
772                         free(path);
773                         return NULL;
774                     }
775                     if ((priv.topde = FindDir(priv.top, FIND_TOPDIR)) == NULL) {
776                         SMseterror(SMERR_UNDEFINED, NULL);
777                         closedir(priv.top);
778                         free(path);
779                         return NULL;
780                     }
781                 }
782                 snprintf(path, length, "%s/%s", innconf->patharticles, priv.topde->d_name);
783                 if ((priv.sec = opendir(path)) == NULL)
784                     continue;
785             }
786             snprintf(path, length, "%s/%s/%s", innconf->patharticles, priv.topde->d_name, priv.secde->d_name);
787             if ((priv.ter = opendir(path)) == NULL)
788                 continue;
789         }
790         snprintf(path, length, "%s/%s/%s/%s", innconf->patharticles, priv.topde->d_name, priv.secde->d_name, priv.terde->d_name);
791         if ((priv.curtoc = CAFReadTOC(path, &priv.curheader)) == NULL)
792             continue;
793         priv.curartnum = 0;
794     }
795     snprintf(path, length, "%s/%s/%s/%s", innconf->patharticles, priv.topde->d_name, priv.secde->d_name, priv.terde->d_name);
796     art = OpenArticle(path, priv.curartnum, amount);
797     if (art == (ARTHANDLE *)NULL) {
798         art = xmalloc(sizeof(ARTHANDLE));
799         art->type = TOKEN_TIMECAF;
800         art->data = NULL;
801         art->len = 0;
802         art->private = xmalloc(sizeof(PRIV_TIMECAF));
803     }
804     newpriv = (PRIV_TIMECAF *)art->private;
805     newpriv->top = priv.top;
806     newpriv->sec = priv.sec;
807     newpriv->ter = priv.ter;
808     newpriv->topde = priv.topde;
809     newpriv->secde = priv.secde;
810     newpriv->terde = priv.terde;
811     newpriv->curheader = priv.curheader;
812     newpriv->curtoc = priv.curtoc;
813     newpriv->curartnum = priv.curartnum;
814     
815     snprintf(path, length, "%s/%s/%s", priv.topde->d_name, priv.secde->d_name, priv.terde->d_name);
816     art->token = PathNumToToken(path, priv.curartnum);
817     art->arrived = priv.curtoc[priv.curartnum - priv.curheader.Low].ModTime;
818     free(path);
819     return art;
820 }
821
822 bool timecaf_ctl(PROBETYPE type, TOKEN *token UNUSED, void *value) {
823     struct artngnum *ann;
824
825     switch (type) {
826     case SMARTNGNUM:
827         if ((ann = (struct artngnum *)value) == NULL)
828             return false;
829         /* make SMprobe() call timecaf_retrieve() */
830         ann->artnum = 0;
831         return true;
832     default:
833         return false;
834     }
835 }
836
837 bool timecaf_flushcacheddata(FLUSHTYPE type) {
838     if (type == SM_ALL || type == SM_CANCELEDART)
839         DoCancels();
840     return true;
841 }
842
843 void
844 timecaf_printfiles(FILE *file, TOKEN token, char **xref UNUSED,
845                    int ngroups UNUSED)
846 {
847     fprintf(file, "%s\n", TokenToText(token));
848 }
849
850 void timecaf_shutdown(void) {
851     CloseOpenFile(&WritingFile);
852     DoCancels();
853 }