chiark / gitweb /
Commit 2.4.5-5 as unpacked
[inn-innduct.git] / storage / tradspool / tradspool.c
1 /*  $Id: tradspool.c 7412 2005-10-09 03:44:35Z eagle $
2 **
3 **  Storage manager module for traditional spool format.
4 */
5
6 #include "config.h"
7 #include "clibrary.h"
8 #include "portable/mmap.h"
9 #include <ctype.h>
10 #include <dirent.h>
11 #include <errno.h>
12 #include <fcntl.h>
13 #include <syslog.h>
14 #include <sys/stat.h>
15 #include <sys/uio.h>
16 #include <time.h>
17
18 /* Needed for htonl() and friends on AIX 4.1. */
19 #include <netinet/in.h>
20     
21 #include "inn/innconf.h"
22 #include "inn/qio.h"
23 #include "inn/wire.h"
24 #include "libinn.h"
25 #include "paths.h"
26
27 #include "methods.h"
28 #include "tradspool.h"
29
30 typedef struct {
31     char                *artbase; /* start of the article data -- may be mmaped */
32     unsigned int        artlen; /* art length. */
33     int         nextindex;
34     char                *curdirname;
35     DIR                 *curdir;
36     struct _ngtent      *ngtp;
37     bool                mmapped;
38 } PRIV_TRADSPOOL;
39
40 /*
41 ** The 64-bit hashed representation of a ng name that gets stashed in each token. 
42 */
43
44 #define HASHEDNGLEN 8
45 typedef struct {
46     char hash[HASHEDNGLEN];
47 } HASHEDNG;
48
49 /*
50 ** We have two structures here for facilitating newsgroup name->number mapping
51 ** and number->name mapping.  NGTable is a hash table based on hashing the
52 ** newsgroup name, and is used to give the name->number mapping.  NGTree is
53 ** a binary tree, indexed by newsgroup number, used for the number->name
54 ** mapping.
55 */
56
57 #define NGT_SIZE  2048
58
59 typedef struct _ngtent {
60     char *ngname;
61 /*    HASHEDNG hash; XXX */
62     unsigned long ngnumber;
63     struct _ngtent *next;
64     struct _ngtreenode *node;
65 } NGTENT;
66
67 typedef struct _ngtreenode {
68     unsigned long ngnumber;
69     struct _ngtreenode *left, *right;
70     NGTENT *ngtp;
71 } NGTREENODE;
72
73 NGTENT *NGTable[NGT_SIZE];
74 unsigned long MaxNgNumber = 0;
75 NGTREENODE *NGTree;
76
77 bool NGTableUpdated; /* set to true if we've added any entries since reading 
78                         in the database file */
79
80 /* 
81 ** Convert all .s to /s in a newsgroup name.  Modifies the passed string 
82 ** inplace.
83 */
84 static void
85 DeDotify(char *ngname) {
86     char *p = ngname;
87
88     for ( ; *p ; ++p) {
89         if (*p == '.') *p = '/';
90     }
91     return;
92 }
93
94 /*
95 ** Hash a newsgroup name to an 8-byte.  Basically, we convert all .s to 
96 ** /s (so it doesn't matter if we're passed the spooldir name or newsgroup
97 ** name) and then call Hash to MD5 the mess, then take 4 bytes worth of 
98 ** data from the front of the hash.  This should be good enough for our
99 ** purposes. 
100 */
101
102 static HASHEDNG 
103 HashNGName(char *ng) {
104     HASH hash;
105     HASHEDNG return_hash;
106     char *p;
107
108     p = xstrdup(ng);
109     DeDotify(p);
110     hash = Hash(p, strlen(p));
111     free(p);
112
113     memcpy(return_hash.hash, hash.hash, HASHEDNGLEN);
114
115     return return_hash;
116 }
117
118 #if 0 /* XXX */
119 /* compare two hashes */
120 static int
121 CompareHash(HASHEDNG *h1, HASHEDNG *h2) {
122     int i;
123     for (i = 0 ; i < HASHEDNGLEN ; ++i) {
124         if (h1->hash[i] != h2->hash[i]) {
125             return h1->hash[i] - h2->hash[i];
126         }
127     }
128     return 0;
129 }
130 #endif
131
132 /* Add a new newsgroup name to the NG table. */
133 static void
134 AddNG(char *ng, unsigned long number) {
135     char *p;
136     unsigned int h;
137     HASHEDNG hash;
138     NGTENT *ngtp, **ngtpp;
139     NGTREENODE *newnode, *curnode, **nextnode;
140
141     p = xstrdup(ng);
142     DeDotify(p); /* canonicalize p to standard (/) form. */
143     hash = HashNGName(p);
144
145     h = (unsigned char)hash.hash[0];
146     h = h + (((unsigned char)hash.hash[1])<<8);
147
148     h = h % NGT_SIZE;
149
150     ngtp = NGTable[h];
151     ngtpp = &NGTable[h];
152     while (true) {
153         if (ngtp == NULL) {
154             /* ng wasn't in table, add new entry. */
155             NGTableUpdated = true;
156
157             ngtp = xmalloc(sizeof(NGTENT));
158             ngtp->ngname = p; /* note: we store canonicalized name */
159             /* ngtp->hash = hash XXX */
160             ngtp->next = NULL;
161
162             /* assign a new NG number if needed (not given) */
163             if (number == 0) {
164                 number = ++MaxNgNumber;
165             }
166             ngtp->ngnumber = number;
167
168             /* link new table entry into the hash table chain. */
169             *ngtpp = ngtp;
170
171             /* Now insert an appropriate record into the binary tree */
172             newnode = xmalloc(sizeof(NGTREENODE));
173             newnode->left = newnode->right = (NGTREENODE *) NULL;
174             newnode->ngnumber = number;
175             newnode->ngtp = ngtp;
176             ngtp->node = newnode;
177
178             if (NGTree == NULL) {
179                 /* tree was empty, so put our one element in and return */
180                 NGTree = newnode;
181                 return;
182             } else {
183                 nextnode = &NGTree;
184                 while (*nextnode) {
185                     curnode = *nextnode;
186                     if (curnode->ngnumber < number) {
187                         nextnode = &curnode->right;
188                     } else if (curnode->ngnumber > number) {
189                         nextnode = &curnode->left;
190                     } else {
191                         /* Error, same number is already in NGtree (shouldn't happen!) */
192                         syslog(L_ERROR, "tradspool: AddNG: duplicate newsgroup number in NGtree: %ld(%s)", number, p);
193                         return;
194                     }
195                 }
196                 *nextnode = newnode;
197                 return;
198             }
199         } else if (strcmp(ngtp->ngname, p) == 0) {
200             /* entry in table already, so return */
201             free(p);
202             return;
203 #if 0 /* XXX */
204         } else if (CompareHash(&ngtp->hash, &hash) == 0) {
205             /* eep! we hit a hash collision. */
206             syslog(L_ERROR, "tradspool: AddNG: Hash collison %s/%s", ngtp->ngname, p);
207             free(p);
208             return;
209 #endif 
210         } else {
211             /* not found yet, so advance to next entry in chain */
212             ngtpp = &(ngtp->next);
213             ngtp = ngtp->next;
214         }
215     }
216 }
217
218 /* find a newsgroup table entry, given only the name. */
219 static NGTENT *
220 FindNGByName(char *ngname) {
221     NGTENT *ngtp;
222     unsigned int h;
223     HASHEDNG hash;
224     char *p;
225
226     p = xstrdup(ngname);
227     DeDotify(p); /* canonicalize p to standard (/) form. */
228     hash = HashNGName(p);
229
230     h = (unsigned char)hash.hash[0];
231     h = h + (((unsigned char)hash.hash[1])<<8);
232
233     h = h % NGT_SIZE;
234
235     ngtp = NGTable[h];
236
237     while (ngtp) {
238         if (strcmp(p, ngtp->ngname) == 0) {
239             free(p);
240             return ngtp;
241         }
242         ngtp = ngtp->next;
243     }
244     free(p);
245     return NULL; 
246 }
247
248 /* find a newsgroup/spooldir name, given only the newsgroup number */
249 static char *
250 FindNGByNum(unsigned long ngnumber) {
251     NGTENT *ngtp;
252     NGTREENODE *curnode;
253
254     curnode = NGTree;
255
256     while (curnode) {
257         if (curnode->ngnumber == ngnumber) {
258             ngtp = curnode->ngtp;
259             return ngtp->ngname;
260         }
261         if (curnode->ngnumber < ngnumber) {
262             curnode = curnode->right;
263         } else {
264             curnode = curnode->left;
265         }
266     }
267     /* not in tree, return NULL */
268     return NULL; 
269 }
270
271 #define _PATH_TRADSPOOLNGDB "tradspool.map"
272 #define _PATH_NEWTSNGDB "tradspool.map.new"
273
274
275 /* dump DB to file. */
276 static void
277 DumpDB(void)
278 {
279     char *fname, *fnamenew;
280     NGTENT *ngtp;
281     unsigned int i;
282     FILE *out;
283
284     if (!SMopenmode) return; /* don't write if we're not in read/write mode. */
285     if (!NGTableUpdated) return; /* no need to dump new DB */
286
287     fname = concatpath(innconf->pathspool, _PATH_TRADSPOOLNGDB);
288     fnamenew = concatpath(innconf->pathspool, _PATH_NEWTSNGDB);
289
290     if ((out = fopen(fnamenew, "w")) == NULL) {
291         syslog(L_ERROR, "tradspool: DumpDB: can't write %s: %m", fnamenew);
292         free(fname);
293         free(fnamenew);
294         return;
295     }
296     for (i = 0 ; i < NGT_SIZE ; ++i) {
297         ngtp = NGTable[i];
298         for ( ; ngtp ; ngtp = ngtp->next) {
299             fprintf(out, "%s %lu\n", ngtp->ngname, ngtp->ngnumber);
300         }
301     }
302     if (fclose(out) < 0) {
303         syslog(L_ERROR, "tradspool: DumpDB: can't close %s: %m", fnamenew);
304         free(fname);
305         free(fnamenew);
306         return;
307     }
308     if (rename(fnamenew, fname) < 0) {
309         syslog(L_ERROR, "tradspool: can't rename %s", fnamenew);
310         free(fname);
311         free(fnamenew);
312         return;
313     }
314     free(fname);
315     free(fnamenew);
316     NGTableUpdated = false; /* reset modification flag. */
317     return;
318 }
319
320 /* 
321 ** init NGTable from saved database file and from active.  Note that
322 ** entries in the database file get added first,  and get their specifications
323 ** of newsgroup number from there. 
324 */
325
326 static bool
327 ReadDBFile(void)
328 {
329     char *fname;
330     QIOSTATE *qp;
331     char *line;
332     char *p;
333     unsigned long number;
334
335     fname = concatpath(innconf->pathspool, _PATH_TRADSPOOLNGDB);
336     if ((qp = QIOopen(fname)) == NULL) {
337         /* only warn if db not found. */
338         syslog(L_NOTICE, "tradspool: %s not found", fname);
339     } else {
340         while ((line = QIOread(qp)) != NULL) {
341             p = strchr(line, ' ');
342             if (p == NULL) {
343                 syslog(L_FATAL, "tradspool: corrupt line in active %s", line);
344                 QIOclose(qp);
345                 free(fname);
346                 return false;
347             }
348             *p++ = 0;
349             number = atol(p);
350             AddNG(line, number);
351             if (MaxNgNumber < number) MaxNgNumber = number;
352         }
353         QIOclose(qp);
354     }
355     free(fname);
356     return true;
357 }
358
359 static bool
360 ReadActiveFile(void)
361 {
362     char *fname;
363     QIOSTATE *qp;
364     char *line;
365     char *p;
366
367     fname = concatpath(innconf->pathdb, _PATH_ACTIVE);
368     if ((qp = QIOopen(fname)) == NULL) {
369         syslog(L_FATAL, "tradspool: can't open %s", fname);
370         free(fname);
371         return false;
372     }
373
374     while ((line = QIOread(qp)) != NULL) {
375         p = strchr(line, ' ');
376         if (p == NULL) {
377             syslog(L_FATAL, "tradspool: corrupt line in active %s", line);
378             QIOclose(qp);
379             free(fname);
380             return false;
381         }
382         *p = 0;
383         AddNG(line, 0);
384     }
385     QIOclose(qp);
386     free(fname);
387     /* dump any newly added changes to database */
388     DumpDB();
389     return true;
390 }
391
392 static bool
393 InitNGTable(void)
394 {
395     if (!ReadDBFile()) return false;
396
397     /*
398     ** set NGTableUpdated to false; that way we know if the load of active or
399     ** any AddNGs later on did in fact add new entries to the db.
400     */
401     NGTableUpdated = false; 
402     if (!SMopenmode)
403         /* don't read active unless write mode. */
404         return true;
405     return ReadActiveFile(); 
406 }
407
408 /* 
409 ** Routine called to check every so often to see if we need to reload the
410 ** database and add in any new groups that have been added.   This is primarily
411 ** for the benefit of innfeed in funnel mode, which otherwise would never
412 ** get word that any new newsgroups had been added. 
413 */
414
415 #define RELOAD_TIME_CHECK 600
416
417 static void
418 CheckNeedReloadDB(bool force)
419 {
420     static TIMEINFO lastcheck, oldlastcheck, now;
421     struct stat sb;
422     char *fname;
423
424     if (GetTimeInfo(&now) < 0) return; /* anyone ever seen gettimeofday fail? :-) */
425     if (!force && lastcheck.time + RELOAD_TIME_CHECK > now.time) return;
426
427     oldlastcheck = lastcheck;
428     lastcheck = now;
429
430     fname = concatpath(innconf->pathspool, _PATH_TRADSPOOLNGDB);
431     if (stat(fname, &sb) < 0) {
432         free(fname);
433         return;
434     }
435     free(fname);
436     if (sb.st_mtime > oldlastcheck.time) {
437         /* add any newly added ngs to our in-memory copy of the db. */
438         ReadDBFile();
439     }
440 }
441
442 /* Init routine, called by SMinit */
443
444 bool
445 tradspool_init(SMATTRIBUTE *attr) {
446     if (attr == NULL) {
447         syslog(L_ERROR, "tradspool: attr is NULL");
448         SMseterror(SMERR_INTERNAL, "attr is NULL");
449         return false;
450     }
451     if (!innconf->storeonxref) {
452         syslog(L_ERROR, "tradspool: storeonxref needs to be true");
453         SMseterror(SMERR_INTERNAL, "storeonxref needs to be true");
454         return false;
455     }
456     attr->selfexpire = false;
457     attr->expensivestat = true;
458     return InitNGTable();
459 }
460
461 /* Make a token for an article given the primary newsgroup name and article # */
462 static TOKEN
463 MakeToken(char *ng, unsigned long artnum, STORAGECLASS class) {
464     TOKEN token;
465     NGTENT *ngtp;
466     unsigned long num;
467
468     memset(&token, '\0', sizeof(token));
469
470     token.type = TOKEN_TRADSPOOL;
471     token.class = class;
472
473     /* 
474     ** if not already in the NG Table, be sure to add this ng! This way we
475     ** catch things like newsgroups added since startup. 
476     */
477     if ((ngtp = FindNGByName(ng)) == NULL) {
478         AddNG(ng, 0);
479         DumpDB(); /* flush to disk so other programs can see the change */
480         ngtp = FindNGByName(ng);
481     } 
482
483     num = ngtp->ngnumber;
484     num = htonl(num);
485
486     memcpy(token.token, &num, sizeof(num));
487     artnum = htonl(artnum);
488     memcpy(&token.token[sizeof(num)], &artnum, sizeof(artnum));
489     return token;
490 }
491
492 /* 
493 ** Convert a token back to a pathname. 
494 */
495 static char *
496 TokenToPath(TOKEN token) {
497     unsigned long ngnum;
498     unsigned long artnum;
499     char *ng, *path;
500     size_t length;
501
502     CheckNeedReloadDB(false);
503
504     memcpy(&ngnum, &token.token[0], sizeof(ngnum));
505     memcpy(&artnum, &token.token[sizeof(ngnum)], sizeof(artnum));
506     artnum = ntohl(artnum);
507     ngnum = ntohl(ngnum);
508
509     ng = FindNGByNum(ngnum);
510     if (ng == NULL) {
511         CheckNeedReloadDB(true);
512         ng = FindNGByNum(ngnum);
513         if (ng == NULL)
514             return NULL;
515     }
516
517     length = strlen(ng) + 20 + strlen(innconf->patharticles);
518     path = xmalloc(length);
519     snprintf(path, length, "%s/%s/%lu", innconf->patharticles, ng, artnum);
520     return path;
521 }
522
523 /*
524 ** Crack an Xref line apart into separate strings, each of the form "ng:artnum".
525 ** Return in "num" the number of newsgroups found. 
526 */
527 static char **
528 CrackXref(char *xref, unsigned int *lenp) {
529     char *p;
530     char **xrefs;
531     char *q;
532     unsigned int len, xrefsize;
533
534     len = 0;
535     xrefsize = 5;
536     xrefs = xmalloc(xrefsize * sizeof(char *));
537
538     /* no path element should exist, nor heading white spaces exist */
539     p = xref;
540     while (true) {
541         /* check for EOL */
542         /* shouldn't ever hit null w/o hitting a \r\n first, but best to be paranoid */
543         if (*p == '\n' || *p == '\r' || *p == 0) {
544             /* hit EOL, return. */
545             *lenp = len;
546             return xrefs;
547         }
548         /* skip to next space or EOL */
549         for (q=p; *q && *q != ' ' && *q != '\n' && *q != '\r' ; ++q) ;
550
551         xrefs[len] = xstrndup(p, q - p);
552
553         if (++len == xrefsize) {
554             /* grow xrefs if needed. */
555             xrefsize *= 2;
556             xrefs = xrealloc(xrefs, xrefsize * sizeof(char *));
557         }
558
559         p = q;
560         /* skip spaces */
561         for ( ; *p == ' ' ; p++) ;
562     }
563 }
564
565 TOKEN
566 tradspool_store(const ARTHANDLE article, const STORAGECLASS class) {
567     char **xrefs;
568     char *xrefhdr;
569     TOKEN token;
570     unsigned int numxrefs;
571     char *ng, *p, *onebuffer;
572     unsigned long artnum;
573     char *path, *linkpath, *dirname;
574     int fd;
575     size_t used;
576     char *nonwfarticle; /* copy of article converted to non-wire format */
577     unsigned int i;
578     size_t length, nonwflen;
579     
580     xrefhdr = article.groups;
581     if ((xrefs = CrackXref(xrefhdr, &numxrefs)) == NULL || numxrefs == 0) {
582         token.type = TOKEN_EMPTY;
583         SMseterror(SMERR_UNDEFINED, "bogus Xref: header");
584         if (xrefs != NULL)
585             free(xrefs);
586         return token;
587     }
588
589     if ((p = strchr(xrefs[0], ':')) == NULL) {
590         token.type = TOKEN_EMPTY;
591         SMseterror(SMERR_UNDEFINED, "bogus Xref: header");
592         for (i = 0 ; i < numxrefs; ++i) free(xrefs[i]);
593         free(xrefs);
594         return token;
595     }
596     *p++ = '\0';
597     ng = xrefs[0];
598     DeDotify(ng);
599     artnum = atol(p);
600     
601     token = MakeToken(ng, artnum, class);
602
603     length = strlen(innconf->patharticles) + strlen(ng) + 32;
604     path = xmalloc(length);
605     snprintf(path, length, "%s/%s/%lu", innconf->patharticles, ng, artnum);
606
607     /* following chunk of code boldly stolen from timehash.c  :-) */
608     if ((fd = open(path, O_CREAT|O_EXCL|O_WRONLY, ARTFILE_MODE)) < 0) {
609         p = strrchr(path, '/');
610         *p = '\0';
611         if (!MakeDirectory(path, true)) {
612             syslog(L_ERROR, "tradspool: could not make directory %s %m", path);
613             token.type = TOKEN_EMPTY;
614             free(path);
615             SMseterror(SMERR_UNDEFINED, NULL);
616             for (i = 0 ; i < numxrefs; ++i) free(xrefs[i]);
617             free(xrefs);
618             return token;
619         } else {
620             *p = '/';
621             if ((fd = open(path, O_CREAT|O_EXCL|O_WRONLY, ARTFILE_MODE)) < 0) {
622                 SMseterror(SMERR_UNDEFINED, NULL);
623                 syslog(L_ERROR, "tradspool: could not open %s %m", path);
624                 token.type = TOKEN_EMPTY;
625                 free(path);
626                 for (i = 0 ; i < numxrefs; ++i) free(xrefs[i]);
627                 free(xrefs);
628                 return token;
629             }
630         }
631     }
632     if (innconf->wireformat) {
633         if (xwritev(fd, article.iov, article.iovcnt) != (ssize_t) article.len) {
634             SMseterror(SMERR_UNDEFINED, NULL);
635             syslog(L_ERROR, "tradspool error writing %s %m", path);
636             close(fd);
637             token.type = TOKEN_EMPTY;
638             unlink(path);
639             free(path);
640             for (i = 0 ; i < numxrefs; ++i) free(xrefs[i]);
641             free(xrefs);
642             return token;
643         }
644     } else {
645         onebuffer = xmalloc(article.len);
646         for (used = i = 0 ; i < article.iovcnt ; i++) {
647             memcpy(&onebuffer[used], article.iov[i].iov_base, article.iov[i].iov_len);
648             used += article.iov[i].iov_len;
649         }
650         nonwfarticle = FromWireFmt(onebuffer, used, &nonwflen);
651         free(onebuffer);
652         if (write(fd, nonwfarticle, nonwflen) != (ssize_t) nonwflen) {
653             free(nonwfarticle);
654             SMseterror(SMERR_UNDEFINED, NULL);
655             syslog(L_ERROR, "tradspool error writing %s %m", path);
656             close(fd);
657             token.type = TOKEN_EMPTY;
658             unlink(path);
659             free(path);
660             for (i = 0 ; i < numxrefs; ++i) free(xrefs[i]);
661             free(xrefs);
662             return token;
663         }
664         free(nonwfarticle);
665     }
666     close(fd);
667
668     /* 
669     ** blah, this is ugly.  Have to make symlinks under other pathnames for
670     ** backwards compatiblility purposes. 
671     */
672
673     if (numxrefs > 1) {
674         for (i = 1; i < numxrefs ; ++i) {
675             if ((p = strchr(xrefs[i], ':')) == NULL) continue;
676             *p++ = '\0';
677             ng = xrefs[i];
678             DeDotify(ng);
679             artnum = atol(p);
680
681             length = strlen(innconf->patharticles) + strlen(ng) + 32;
682             linkpath = xmalloc(length);
683             snprintf(linkpath, length, "%s/%s/%lu", innconf->patharticles,
684                      ng, artnum);
685             if (link(path, linkpath) < 0) {
686                 p = strrchr(linkpath, '/');
687                 *p = '\0';
688                 dirname = xstrdup(linkpath);
689                 *p = '/';
690                 if (!MakeDirectory(dirname, true) || link(path, linkpath) < 0) {
691 #if !defined(HAVE_SYMLINK)
692                     SMseterror(SMERR_UNDEFINED, NULL);
693                     syslog(L_ERROR, "tradspool: could not link %s to %s %m", path, linkpath);
694                     token.type = TOKEN_EMPTY;
695                     free(dirname);
696                     free(linkpath);
697                     free(path);
698                     for (i = 0 ; i < numxrefs; ++i) free(xrefs[i]);
699                     free(xrefs);
700                     return token;
701 #else
702                     if (symlink(path, linkpath) < 0) {
703                         SMseterror(SMERR_UNDEFINED, NULL);
704                         syslog(L_ERROR, "tradspool: could not symlink %s to %s %m", path, linkpath);
705                         token.type = TOKEN_EMPTY;
706                         free(dirname);
707                         free(linkpath);
708                         free(path);
709                         for (i = 0 ; i < numxrefs; ++i) free(xrefs[i]);
710                         free(xrefs);
711                         return token;
712                     }
713 #endif  /* !defined(HAVE_SYMLINK) */
714                 }
715                 free(dirname);
716             }
717             free(linkpath);
718         }
719     }
720     free(path);
721     for (i = 0 ; i < numxrefs; ++i) free(xrefs[i]);
722     free(xrefs);
723     return token;
724 }
725
726 static ARTHANDLE *
727 OpenArticle(const char *path, RETRTYPE amount) {
728     int fd;
729     PRIV_TRADSPOOL *private;
730     char *p;
731     struct stat sb;
732     ARTHANDLE *art;
733     char *wfarticle;
734     size_t wflen;
735
736     if (amount == RETR_STAT) {
737         if (access(path, R_OK) < 0) {
738             SMseterror(SMERR_UNDEFINED, NULL);
739             return NULL;
740         }
741         art = xmalloc(sizeof(ARTHANDLE));
742         art->type = TOKEN_TRADSPOOL;
743         art->data = NULL;
744         art->len = 0;
745         art->private = NULL;
746         return art;
747     }
748
749     if ((fd = open(path, O_RDONLY)) < 0) {
750         SMseterror(SMERR_UNDEFINED, NULL);
751         return NULL;
752     }
753
754     art = xmalloc(sizeof(ARTHANDLE));
755     art->type = TOKEN_TRADSPOOL;
756
757     if (fstat(fd, &sb) < 0) {
758         SMseterror(SMERR_UNDEFINED, NULL);
759         syslog(L_ERROR, "tradspool: could not fstat article: %m");
760         free(art);
761         close(fd);
762         return NULL;
763     }
764
765     art->arrived = sb.st_mtime;
766
767     private = xmalloc(sizeof(PRIV_TRADSPOOL));
768     art->private = (void *)private;
769     private->artlen = sb.st_size;
770     if (innconf->articlemmap) {
771         if ((private->artbase = mmap(NULL, sb.st_size, PROT_READ, MAP_SHARED, fd, 0)) == MAP_FAILED) {
772             SMseterror(SMERR_UNDEFINED, NULL);
773             syslog(L_ERROR, "tradspool: could not mmap article: %m");
774             free(art->private);
775             free(art);
776             close(fd);
777             return NULL;
778         }
779         if (amount == RETR_ALL)
780             madvise(private->artbase, sb.st_size, MADV_WILLNEED);
781         else
782             madvise(private->artbase, sb.st_size, MADV_SEQUENTIAL);
783
784         /* consider coexisting both wireformatted and nonwireformatted */
785         p = memchr(private->artbase, '\n', private->artlen);
786         if (p == NULL || p == private->artbase) {
787             SMseterror(SMERR_UNDEFINED, NULL);
788             syslog(L_ERROR, "tradspool: could not mmap article: %m");
789             munmap(private->artbase, private->artlen);
790             free(art->private);
791             free(art);
792             close(fd);
793             return NULL;
794         }
795         if (p[-1] == '\r') {
796             private->mmapped = true;
797         } else {
798             wfarticle = ToWireFmt(private->artbase, private->artlen, &wflen);
799             munmap(private->artbase, private->artlen);
800             private->artbase = wfarticle;
801             private->artlen = wflen;
802             private->mmapped = false;
803         }
804     } else {
805         private->mmapped = false;
806         private->artbase = xmalloc(private->artlen);
807         if (read(fd, private->artbase, private->artlen) < 0) {
808             SMseterror(SMERR_UNDEFINED, NULL);
809             syslog(L_ERROR, "tradspool: could not read article: %m");
810             free(private->artbase);
811             free(art->private);
812             free(art);
813             close(fd);
814             return NULL;
815         }
816         p = memchr(private->artbase, '\n', private->artlen);
817         if (p == NULL || p == private->artbase) {
818             SMseterror(SMERR_UNDEFINED, NULL);
819             syslog(L_ERROR, "tradspool: could not mmap article: %m");
820             free(art->private);
821             free(art);
822             close(fd);
823             return NULL;
824         }
825         if (p[-1] != '\r') {
826             /* need to make a wireformat copy of the article */
827             wfarticle = ToWireFmt(private->artbase, private->artlen, &wflen);
828             free(private->artbase);
829             private->artbase = wfarticle;
830             private->artlen = wflen;
831         }
832     }
833     close(fd);
834     
835     private->ngtp = NULL;
836     private->curdir = NULL;
837     private->curdirname = NULL;
838     private->nextindex = -1;
839
840     if (amount == RETR_ALL) {
841         art->data = private->artbase;
842         art->len = private->artlen;
843         return art;
844     }
845     
846     if (((p = wire_findbody(private->artbase, private->artlen)) == NULL)) {
847         if (private->mmapped)
848             munmap(private->artbase, private->artlen);
849         else
850             free(private->artbase);
851         SMseterror(SMERR_NOBODY, NULL);
852         free(art->private);
853         free(art);
854         return NULL;
855     }
856
857     if (amount == RETR_HEAD) {
858         art->data = private->artbase;
859         art->len = p - private->artbase;
860         return art;
861     }
862
863     if (amount == RETR_BODY) {
864         art->data = p;
865         art->len = private->artlen - (p - private->artbase);
866         return art;
867     }
868     SMseterror(SMERR_UNDEFINED, "Invalid retrieve request");
869     if (private->mmapped)
870         munmap(private->artbase, private->artlen);
871     else
872         free(private->artbase);
873     free(art->private);
874     free(art);
875     return NULL;
876 }
877
878     
879 ARTHANDLE *
880 tradspool_retrieve(const TOKEN token, const RETRTYPE amount) {
881     char *path;
882     ARTHANDLE *art;
883     static TOKEN ret_token;
884
885     if (token.type != TOKEN_TRADSPOOL) {
886         SMseterror(SMERR_INTERNAL, NULL);
887         return NULL;
888     }
889
890     if ((path = TokenToPath(token)) == NULL) {
891         SMseterror(SMERR_NOENT, NULL);
892         return NULL;
893     }
894     if ((art = OpenArticle(path, amount)) != (ARTHANDLE *)NULL) {
895         ret_token = token;
896         art->token = &ret_token;
897     }
898     free(path);
899     return art;
900 }
901
902 void
903 tradspool_freearticle(ARTHANDLE *article)
904 {
905     PRIV_TRADSPOOL *private;
906
907     if (article == NULL)
908         return;
909
910     if (article->private) {
911         private = (PRIV_TRADSPOOL *) article->private;
912         if (private->mmapped)
913             munmap(private->artbase, private->artlen);
914         else
915             free(private->artbase);
916         if (private->curdir)
917             closedir(private->curdir);
918         free(private->curdirname);
919         free(private);
920     }
921     free(article);
922 }
923
924 bool 
925 tradspool_cancel(TOKEN token) {
926     char **xrefs;
927     char *xrefhdr;
928     ARTHANDLE *article;
929     unsigned int numxrefs;
930     char *ng, *p;
931     char *path, *linkpath;
932     unsigned int i;
933     bool result = true;
934     unsigned long artnum;
935     size_t length;
936
937     if ((path = TokenToPath(token)) == NULL) {
938         SMseterror(SMERR_UNDEFINED, NULL);
939         free(path);
940         return false;
941     }
942     /*
943     **  Ooooh, this is gross.  To find the symlinks pointing to this article,
944     ** we open the article and grab its Xref line (since the token isn't long
945     ** enough to store this info on its own).   This is *not* going to do 
946     ** good things for performance of fastrm...  -- rmtodd
947     */
948     if ((article = OpenArticle(path, RETR_HEAD)) == NULL) {
949         free(path);
950         SMseterror(SMERR_UNDEFINED, NULL);
951         return false;
952     }
953
954     xrefhdr = wire_findheader(article->data, article->len, "Xref");
955     if (xrefhdr == NULL) {
956         /* for backwards compatibility; there is no Xref unless crossposted
957            for 1.4 and 1.5 */
958         if (unlink(path) < 0) result = false;
959         free(path);
960         tradspool_freearticle(article);
961         return result;
962     }
963
964     if ((xrefs = CrackXref(xrefhdr, &numxrefs)) == NULL || numxrefs == 0) {
965         if (xrefs != NULL)
966             free(xrefs);
967         free(path);
968         tradspool_freearticle(article);
969         SMseterror(SMERR_UNDEFINED, NULL);
970         return false;
971     }
972
973     tradspool_freearticle(article);
974     for (i = 1 ; i < numxrefs ; ++i) {
975         if ((p = strchr(xrefs[i], ':')) == NULL) continue;
976         *p++ = '\0';
977         ng = xrefs[i];
978         DeDotify(ng);
979         artnum = atol(p);
980
981         length = strlen(innconf->patharticles) + strlen(ng) + 32;
982         linkpath = xmalloc(length);
983         snprintf(linkpath, length, "%s/%s/%lu", innconf->patharticles, ng,
984                  artnum);
985         /* repeated unlinkings of a crossposted article may fail on account
986            of the file no longer existing without it truly being an error */
987         if (unlink(linkpath) < 0)
988             if (errno != ENOENT || i == 1)
989                 result = false;
990         free(linkpath);
991     }
992     if (unlink(path) < 0)
993         if (errno != ENOENT || numxrefs == 1)
994             result = false;
995     free(path);
996     for (i = 0 ; i < numxrefs ; ++i) free(xrefs[i]);
997     free(xrefs);
998     return result;
999 }
1000
1001    
1002 /*
1003 ** Find entries for possible articles in dir. "dir" (directory name "dirname").
1004 ** The dirname is needed so we can do stats in the directory to disambiguate
1005 ** files from symlinks and directories.
1006 */
1007
1008 static struct dirent *
1009 FindDir(DIR *dir, char *dirname) {
1010     struct dirent *de;
1011     int i;
1012     bool flag;
1013     char *path;
1014     struct stat sb;
1015     size_t length;
1016     unsigned char namelen;
1017
1018     while ((de = readdir(dir)) != NULL) {
1019         namelen = strlen(de->d_name);
1020         for (i = 0, flag = true ; i < namelen ; ++i) {
1021             if (!CTYPE(isdigit, de->d_name[i])) {
1022                 flag = false;
1023                 break;
1024             }
1025         }
1026         if (!flag) continue; /* if not all digits, skip this entry. */
1027
1028         length = strlen(dirname) + namelen + 2;
1029         path = xmalloc(length);
1030         strlcpy(path, dirname, length);
1031         strlcat(path, "/", length);
1032         strlcat(path, de->d_name, length);
1033
1034         if (lstat(path, &sb) < 0) {
1035             free(path);
1036             continue;
1037         }
1038         free(path);
1039         if (!S_ISREG(sb.st_mode)) continue;
1040         return de;
1041     }
1042     return NULL;
1043 }
1044
1045 ARTHANDLE *tradspool_next(const ARTHANDLE *article, const RETRTYPE amount) {
1046     PRIV_TRADSPOOL priv;
1047     PRIV_TRADSPOOL *newpriv;
1048     char *path, *linkpath;
1049     struct dirent *de;
1050     ARTHANDLE *art;
1051     unsigned long artnum;
1052     unsigned int i;
1053     static TOKEN token;
1054     char **xrefs;
1055     char *xrefhdr, *ng, *p, *expires, *x;
1056     unsigned int numxrefs;
1057     STORAGE_SUB *sub;
1058     size_t length;
1059
1060     if (article == NULL) {
1061         priv.ngtp = NULL;
1062         priv.curdir = NULL;
1063         priv.curdirname = NULL;
1064         priv.nextindex = -1;
1065     } else {
1066         priv = *(PRIV_TRADSPOOL *) article->private;
1067         free(article->private);
1068         free((void*)article);
1069         if (priv.artbase != NULL) {
1070             if (priv.mmapped)
1071                 munmap(priv.artbase, priv.artlen);
1072             else
1073                 free(priv.artbase);
1074         }
1075     }
1076
1077     while (!priv.curdir || ((de = FindDir(priv.curdir, priv.curdirname)) == NULL)) {
1078         if (priv.curdir) {
1079             closedir(priv.curdir);
1080             priv.curdir = NULL;
1081             free(priv.curdirname);
1082             priv.curdirname = NULL;
1083         }
1084
1085         /*
1086         ** advance ngtp to the next entry, if it exists, otherwise start 
1087         ** searching down another ngtable hashchain. 
1088         */
1089         while (priv.ngtp == NULL || (priv.ngtp = priv.ngtp->next) == NULL) {
1090             /*
1091             ** note that at the start of a search nextindex is -1, so the inc.
1092             ** makes nextindex 0, as it should be.
1093             */
1094             priv.nextindex++;
1095             if (priv.nextindex >= NGT_SIZE) {
1096                 /* ran off the end of the table, so return. */
1097                 return NULL;
1098             }
1099             priv.ngtp = NGTable[priv.nextindex];
1100             if (priv.ngtp != NULL)
1101                 break;
1102         }
1103
1104         priv.curdirname = concatpath(innconf->patharticles, priv.ngtp->ngname);
1105         priv.curdir = opendir(priv.curdirname);
1106     }
1107
1108     path = concatpath(priv.curdirname, de->d_name);
1109     i = strlen(priv.curdirname);
1110     /* get the article number while we're here, we'll need it later. */
1111     artnum = atol(&path[i+1]);
1112
1113     art = OpenArticle(path, amount);
1114     if (art == (ARTHANDLE *)NULL) {
1115         art = xmalloc(sizeof(ARTHANDLE));
1116         art->type = TOKEN_TRADSPOOL;
1117         art->data = NULL;
1118         art->len = 0;
1119         art->private = xmalloc(sizeof(PRIV_TRADSPOOL));
1120         art->expires = 0;
1121         art->groups = NULL;
1122         art->groupslen = 0;
1123         newpriv = (PRIV_TRADSPOOL *) art->private;
1124         newpriv->artbase = NULL;
1125     } else {
1126         /* Skip linked (not symlinked) crossposted articles.
1127
1128            This algorithm is rather questionable; it only works if the first
1129            group/number combination listed in the Xref header is the
1130            canonical path.  This will always be true for spools created by
1131            this implementation, but for traditional INN 1.x servers,
1132            articles are expired indepedently from each group and may expire
1133            out of the first listed newsgroup before other groups.  This
1134            algorithm will orphan such articles, not adding them to history.
1135
1136            The bit of skipping articles by setting the length of the article
1137            to zero is also rather suspect, and I'm not sure what
1138            implications that might have for the callers of SMnext.
1139
1140            Basically, this whole area really needs to be rethought. */
1141         xrefhdr = wire_findheader(art->data, art->len, "Xref");
1142         if (xrefhdr != NULL) {
1143             if ((xrefs = CrackXref(xrefhdr, &numxrefs)) == NULL || numxrefs == 0) {
1144                 art->len = 0;
1145             } else {
1146                 /* assumes first one is the original */
1147                 if ((p = strchr(xrefs[1], ':')) != NULL) {
1148                     *p++ = '\0';
1149                     ng = xrefs[1];
1150                     DeDotify(ng);
1151                     artnum = atol(p);
1152
1153                     length = strlen(innconf->patharticles) + strlen(ng) + 32;
1154                     linkpath = xmalloc(length);
1155                     snprintf(linkpath, length, "%s/%s/%lu",
1156                              innconf->patharticles, ng, artnum);
1157                     if (strcmp(path, linkpath) != 0) {
1158                         /* this is linked article, skip it */
1159                         art->len = 0;
1160                     }
1161                     free(linkpath);
1162                 }
1163             }
1164             for (i = 0 ; i < numxrefs ; ++i) free(xrefs[i]);
1165             free(xrefs);
1166             if (innconf->storeonxref) {
1167                 /* skip path element */
1168                 if ((xrefhdr = strchr(xrefhdr, ' ')) == NULL) {
1169                     art->groups = NULL;
1170                     art->groupslen = 0;
1171                 } else {
1172                     for (xrefhdr++; *xrefhdr == ' '; xrefhdr++);
1173                     art->groups = xrefhdr;
1174                     for (p = xrefhdr ; (*p != '\n') && (*p != '\r') ; p++);
1175                     art->groupslen = p - xrefhdr;
1176                 }
1177             }
1178         }
1179         if (xrefhdr == NULL || !innconf->storeonxref) {
1180             ng = wire_findheader(art->data, art->len, "Newsgroups");
1181             if (ng == NULL) {
1182                 art->groups = NULL;
1183                 art->groupslen = 0;
1184             } else {
1185                 art->groups = ng;
1186                 for (p = ng ; (*p != '\n') && (*p != '\r') ; p++);
1187                 art->groupslen = p - ng;
1188             }
1189         }
1190         expires = wire_findheader(art->data, art->len, "Expires");
1191         if (expires == NULL) {
1192             art->expires = 0;
1193         } else {
1194             /* optionally parse expire header */
1195             for (p = expires + 1; (*p != '\n') && (*(p - 1) != '\r'); p++);
1196             x = xmalloc(p - expires);
1197             memcpy(x, expires, p - expires - 1);
1198             x[p - expires - 1] = '\0';
1199
1200             art->expires = parsedate(x, NULL);
1201             if (art->expires == -1)
1202                 art->expires = 0;
1203             else
1204                 art->expires -= time(0);
1205             free(x);
1206         }
1207         /* for backwards compatibility; assumes no Xref unless crossposted
1208            for 1.4 and 1.5: just fall through */
1209     }
1210     newpriv = (PRIV_TRADSPOOL *) art->private;
1211     newpriv->nextindex = priv.nextindex;
1212     newpriv->curdir = priv.curdir;
1213     newpriv->curdirname = priv.curdirname;
1214     newpriv->ngtp = priv.ngtp;
1215     
1216     if ((sub = SMgetsub(*art)) == NULL || sub->type != TOKEN_TRADSPOOL) {
1217         /* maybe storage.conf is modified, after receiving article */
1218         token = MakeToken(priv.ngtp->ngname, artnum, 0);
1219
1220         /* Only log an error if art->len is non-zero, since otherwise we get
1221            all the ones skipped via the hard-link skipping algorithm
1222            commented above. */
1223         if (art->len > 0)
1224             syslog(L_ERROR, "tradspool: can't determine class: %s: %s",
1225                    TokenToText(token), SMerrorstr);
1226     } else {
1227         token = MakeToken(priv.ngtp->ngname, artnum, sub->class);
1228     }
1229     art->token = &token;
1230     free(path);
1231     return art;
1232 }
1233
1234 static void
1235 FreeNGTree(void)
1236 {
1237     unsigned int i;
1238     NGTENT *ngtp, *nextngtp;
1239
1240     for (i = 0 ; i < NGT_SIZE ; i++) {
1241         ngtp = NGTable[i];
1242         for ( ; ngtp != NULL ; ngtp = nextngtp) {
1243             nextngtp = ngtp->next;
1244             free(ngtp->ngname);
1245             free(ngtp->node);
1246             free(ngtp);
1247         }
1248         NGTable[i] = NULL;
1249     }
1250     MaxNgNumber = 0;
1251     NGTree = NULL;
1252 }
1253
1254 bool tradspool_ctl(PROBETYPE type, TOKEN *token, void *value) {
1255     struct artngnum *ann;
1256     unsigned long ngnum;
1257     unsigned long artnum;
1258     char *ng, *p;
1259
1260     switch (type) { 
1261     case SMARTNGNUM:
1262         if ((ann = (struct artngnum *)value) == NULL)
1263             return false;
1264         CheckNeedReloadDB(false);
1265         memcpy(&ngnum, &token->token[0], sizeof(ngnum));
1266         memcpy(&artnum, &token->token[sizeof(ngnum)], sizeof(artnum));
1267         artnum = ntohl(artnum);
1268         ngnum = ntohl(ngnum);
1269         ng = FindNGByNum(ngnum);
1270         if (ng == NULL) {
1271             CheckNeedReloadDB(true);
1272             ng = FindNGByNum(ngnum);
1273             if (ng == NULL)
1274                 return false;
1275         }
1276         ann->groupname = xstrdup(ng);
1277         for (p = ann->groupname; *p != 0; p++)
1278             if (*p == '/')
1279                 *p = '.';
1280         ann->artnum = (ARTNUM)artnum;
1281         return true;
1282     default:
1283         return false;
1284     }       
1285 }
1286
1287 bool
1288 tradspool_flushcacheddata(FLUSHTYPE type UNUSED)
1289 {
1290     return true;
1291 }
1292
1293 void
1294 tradspool_printfiles(FILE *file, TOKEN token UNUSED, char **xref, int ngroups)
1295 {
1296     int i;
1297     char *path, *p;
1298
1299     for (i = 0; i < ngroups; i++) {
1300         path = xstrdup(xref[i]);
1301         for (p = path; *p != '\0'; p++)
1302             if (*p == '.' || *p == ':')
1303                 *p = '/';
1304         fprintf(file, "%s\n", path);
1305         free(path);
1306     }
1307 }
1308
1309 void
1310 tradspool_shutdown(void) {
1311     DumpDB();
1312     FreeNGTree();
1313 }