chiark / gitweb /
run debian/rules patch
[inn-innduct.git] / innd / newsfeeds.c
1 /*  $Id: newsfeeds.c 7730 2008-04-06 08:27:16Z iulius $
2 **
3 **  Routines for the in-core data structures for the newsfeeds file.
4 */
5
6 #include "config.h"
7 #include "clibrary.h"
8
9 #include "inn/innconf.h"
10 #include "innd.h"
11
12 /*
13 ** List of variables assigned in the configuration file.
14 */
15 typedef struct _SITEVARIABLES {
16     char      *Name;
17     char      *Value;
18     int               Elements;
19     struct _SITEVARIABLES     *Next;
20 } SITEVARIABLES;
21
22 /* The character which introduces a variable assignment or reference. */
23 #define VARIABLE_CHAR '$'
24
25 static SITE     SITEnull;
26 static char     *SITEfeedspath = NULL;
27 static SITEVARIABLES  *SITEvariables = NULL;
28
29
30 /*
31 **  Return a copy of an array of strings.
32 */
33 static char **
34 SITEcopystrings(char **av)
35 {
36     char        **new;
37     char        **pp;
38     char        **save;
39
40     for (pp = av; *pp; pp++)
41         continue;
42     for (new = save = xmalloc((pp - av + 1) * sizeof(char *)), pp = av; *pp; pp++)
43         *new++ = xstrdup(*pp);
44     *new = NULL;
45     return save;
46 }
47
48 /*
49 ** Adds a variable from a line.
50 */
51 static bool
52 SITEaddvariable(char *line)
53 {
54     char *p, *q;
55     SITEVARIABLES *v, *w;
56     
57     if (*line != VARIABLE_CHAR)
58         return false;
59         
60     for (p = line + 1; *p != '\0' && CTYPE(isalnum, *p); p++)
61         ;
62     if (*p != '=')
63         return false;
64     if (p - line > 32) {
65         syslog(L_FATAL, "%s bad_newsfeed variable name '%s' too long", 
66                LogName, line+1);
67         return false;
68     }
69
70     /* Chop off trailing spaces. */
71     q = p + strlen(p) - 1;
72     while (q > p && (*q == ' ' || *q == '\t'))
73         *q-- = '\0';
74
75     /* Seperate variable name from contents. */
76     *p++ = '\0';        
77     if (*p == '\0')
78         return false;
79
80     /* Is variable already defined?  Free and reassign. */
81     w = NULL;
82     v = SITEvariables;
83     while (v && strcmp(line + 1, v->Name)) {
84         w = v;
85         v = v->Next;
86     }
87     if (v)
88         free(v->Value);
89     else {
90         v = xmalloc(sizeof(SITEVARIABLES));
91         if (!SITEvariables)
92             SITEvariables = v;
93         if (w)
94             w->Next = v;
95         v->Name = xstrdup(line + 1);
96         v->Next = NULL;
97     }
98
99     /* Add variable's contents. */
100     v->Elements = 1;
101     for (q = v->Value = xmalloc(strlen(p) + 1); *p != '\0'; p++) {
102         if (*p != ' ' && *p != '\t')
103             *q++ = *p;
104         if (*p == ',')
105             v->Elements++;
106     }
107     *q = '\0';
108     return true;        
109 }
110
111 static void
112 SITEclearvariables(void)
113 {
114     SITEVARIABLES *v, *w;
115     
116     v = SITEvariables;
117     while (v) {
118         free(v->Name);
119         free(v->Value);
120         w = v;
121         v = v->Next;
122         free(w);
123     }
124     SITEvariables = NULL;
125 }
126
127 static SITEVARIABLES *
128 SITEfindvariable(char *name)
129 {
130     SITEVARIABLES *v;
131
132     v = SITEvariables;
133     while (v && strcmp(v->Name, name) != 0)
134         v = v->Next;
135     return v;
136 }
137
138 static char *
139 SITEexpandvariables(char *site)
140 {
141     char *p, *r, *s;
142     char *q = NULL;
143     int c = 0;
144     char modifier;
145     char varname[64];
146     SITEVARIABLES *v;
147
148     /* Count characters. */
149     *varname = '\0';
150     modifier = '\0';
151     for (p = site; p <= site + strlen(site); p++) {
152         /* In variable name. */
153         if (*varname) {
154             if (CTYPE(isalnum, *p)) {
155                 if (q - varname > 32) {
156                     /* Add ignored modifier. */
157                     if (modifier)
158                         c++;
159                     /* Add ignored $ and characters. */
160                     c += strlen(varname);
161                     /* Add this character. */
162                     c++;
163                     *varname = '\0';
164                     modifier = '\0';
165                     continue;
166                 }
167                 /* Append to variable name. */
168                 *q++ = *p;
169                 continue;
170             } else {
171                 v = SITEfindvariable(varname + 1);
172                 if (v != NULL) {
173                     /* Add length of contents. */
174                     c += strlen(v->Value);
175                     /* If modified add number of mods. */
176                     if (modifier)
177                         c += v->Elements;
178                 } else {
179                     /* Add ignored modifier. */
180                     if (modifier)
181                         c++;
182                     c += strlen(varname); /* add ignored $ and characters */
183                 }
184                 *varname = '\0';
185                 modifier = '\0';
186             }
187         }
188         /* New variable starts */
189         if (*p == VARIABLE_CHAR) {
190             q = varname;
191             memset(varname, 0, sizeof(varname));
192             *q++ = VARIABLE_CHAR;
193             continue;
194         }
195         if (modifier) {
196             /* Add last modifier */
197             c++;
198             modifier = '\0';
199         }
200         if (*p == SUB_NEGATE || *p == SUB_POISON) {
201             modifier = *p;
202         } else {
203             /* Add this character. */
204             c++;
205         }
206     }
207
208     /* Copy contents. */
209     s = r = xmalloc(c + 1);
210     *varname = '\0';
211     modifier = '\0';
212     for (p = site; p <= site + strlen(site); p++) {
213         /* In variable name. */
214         if (*varname) {
215             if (CTYPE(isalnum, *p)) {
216                 if (q - varname > 32) {
217                     if (modifier)
218                         *s++ = modifier;
219                     for (q = varname; *q; q++)
220                         *s++ = *q;
221                     *s++ = *p;
222                     *varname = '\0';
223                     modifier = '\0';
224                     continue;
225                 }
226                 *q++ = *p;
227                 continue;
228             } else {
229                 v = SITEfindvariable(varname + 1);
230                 if (v != NULL) {
231                     if (modifier)
232                         *s++ = modifier;
233                     for (q = v->Value; *q; q++) {
234                         *s++ = *q;
235                         if (*q == ',' && modifier)
236                             *s++ = modifier;
237                     }
238                 } else {
239                     if (modifier)
240                         *s++ = modifier;
241                     for (q = varname; *q; q++)
242                         *s++ = *q;
243                 }
244                 *varname = '\0';
245                 modifier = '\0';
246             }
247         }
248         /* New variable starts. */
249         if (*p == VARIABLE_CHAR) {
250             q = varname;
251             memset(varname, 0, sizeof(varname));
252             *q++ = VARIABLE_CHAR;
253             continue;
254         }
255         if (modifier) {
256             *s++ = modifier;
257             modifier = '\0';
258         }
259         if (*p == SUB_NEGATE || *p == SUB_POISON)
260             modifier = *p;
261         else
262             *s++ = *p;
263     }
264     *s++ = '\0';
265
266     return r;
267 }
268
269 /*
270 **  Read the newsfeeds file, return a string array of entries.
271 */
272 char **
273 SITEreadfile(const bool ReadOnly)
274 {
275     static char         **old_strings;
276     static time_t       old_mtime;
277     static ino_t        old_ino;
278     static off_t        old_size;
279     char                *p;
280     char                *to;
281     char                *site;
282     int                 i;
283     struct stat         Sb;
284     char                *data;
285
286     if (SITEfeedspath == NULL)
287         SITEfeedspath = concatpath(innconf->pathetc, _PATH_NEWSFEEDS);
288     if (old_strings != NULL) {
289         /* If the file hasn't changed, return a copy of the old data. */
290         if (stat(SITEfeedspath, &Sb) >= 0
291          && Sb.st_ino == old_ino
292          && Sb.st_size == old_size
293          && Sb.st_mtime == old_mtime)
294             return ReadOnly ? old_strings : SITEcopystrings(old_strings);
295
296         /* Data's bad, toss it. */
297         for (i = 0; old_strings[i] != NULL; i++)
298             free(old_strings[i]);
299         free(old_strings);
300     }
301
302     /* Read in the file, note its statistics. */
303     if ((data = ReadInFile(SITEfeedspath, &Sb)) == NULL) {
304         syslog(L_FATAL, "%s cant read %s %m", LogName, SITEfeedspath);
305         exit(1);
306     }
307     old_mtime = Sb.st_mtime;
308     old_ino = Sb.st_ino;
309     old_size = Sb.st_size;
310
311     /* Get a gross count of the number of sites. */
312     for (p = data, i = 0; (p = strchr(p, '\n')) != NULL; p++, i++)
313         continue;
314
315     /* Scan the file, parse all multi-line entries. */
316     for (old_strings = xmalloc((i + 1) * sizeof(char *)), i = 0, to = p = data; *p; ) {
317         for (site = to; *p; ) {
318             if (*p == '\n') {
319                 p++;
320                 *to = '\0';
321                 break;
322             }
323             if (*p == '\\' && p[1] == '\n')
324                 while (*++p && CTYPE(isspace, *p))
325                     continue;
326             else
327                 *to++ = *p++;
328         }
329         *to++ = '\0';
330         if (*site == '#' || *site == '\0')
331             continue ;
332         if (*site == VARIABLE_CHAR && SITEaddvariable(site))
333             continue ;        
334         if (strspn(site," \t") == strlen (site))
335             continue;
336         if (SITEvariables)
337             old_strings[i++] = SITEexpandvariables(site);
338         else
339             old_strings[i++] = xstrdup(site);
340     }
341     old_strings[i] = NULL;
342     
343     SITEclearvariables();
344     free(data);
345     return ReadOnly ? old_strings : SITEcopystrings(old_strings);
346 }
347
348
349 /*
350 **  Modify "subbed" according to the patterns in "patlist."
351 */
352 static void
353 SITEsetlist(char **patlist, char *subbed, char *poison, bool *poisonEntry)
354 {
355     char        *pat;
356     char        *p;
357     char        *u;
358     char        subvalue;
359     char        poisonvalue;
360     NEWSGROUP   *ngp;
361     int         i;
362
363     while ((pat = *patlist++) != NULL) {
364         subvalue = *pat != SUB_NEGATE && *pat != SUB_POISON;
365         poisonvalue = *pat == SUB_POISON;
366         if (!subvalue)
367             pat++;
368         if (!*pat)
369             continue;
370         if (!*poisonEntry && poisonvalue)
371             *poisonEntry = true;
372
373         /* See if pattern is a simple newsgroup name.  If so, set the
374          * right subbed element for that one group (if found); if not,
375          * pattern-match against all the groups. */
376         for (p = pat; *p; p++)
377             if (*p == '?' || *p == '*' || *p == '[')
378                 break;
379         if (*p == '\0') {
380             /* Simple string; look it up, set it. */
381             if ((ngp = NGfind(pat)) != NULL) {
382                 subbed[ngp - Groups] = subvalue;
383                 poison[ngp - Groups] = poisonvalue;
384             }
385         }
386         else
387             for (p = subbed, u = poison, ngp = Groups, i = nGroups;
388                         --i >= 0; ngp++, p++, u++)
389                 if (uwildmat(ngp->Name, pat)) {
390                     *p = subvalue;
391                     *u = poisonvalue;
392                 }
393     }
394 }
395
396 /*
397 **  Split text into slash-separated fields.  Return an allocated
398 **  NULL-terminated array of the fields within the modified argument that
399 **  the caller is expected to save or free.  We don't use strchr() since
400 **  the text is expected to be either relatively short or "slash-dense."
401 */
402 static char **
403 SlashSplit(char *text)
404 {
405     int         i;
406     char        *p;
407     char        **av;
408     char        **save;
409
410     /* How much space do we need? */
411     for (i = 2, p = text; *p; p++)
412         if (*p == '/')
413             i++;
414
415     for (av = save = xmalloc(i * sizeof(char *)), *av++ = p = text; *p; )
416         if (*p == '/') {
417             *p++ = '\0';
418             *av++ = p;
419         }
420         else
421             p++;
422     *av = NULL;
423     return save;
424 }
425
426 /*
427 **  Parse an individual site entry.  Subbed is used to build the subscription
428 **  list.  Since this routine is called once for each site, the caller
429 **  allocates subbed once, and frees it after the last site has been parsed.
430 **  If subbed is NULL, we don't update the SITE array, since we're just
431 **  doing syntax checking.
432 */
433 const char *
434 SITEparseone(char *Entry, SITE *sp, char *subbed, char *poison)
435 {
436     int                 i;
437     int                 j;
438     NEWSGROUP           *ngp;
439     char                *p;
440     char                *u;
441     char                *f2;
442     char                *f3;
443     char                *f4;
444     char                **save;
445     char                **argv;
446     bool                JustModerated;
447     bool                JustUnmoderated;
448     int                 isp;
449     SITE                *nsp;
450     struct buffer       b;
451     HASHFEEDLIST        *hf;
452
453     b = sp->Buffer;
454     *sp = SITEnull;
455     sp->Buffer = b;
456     sp->Master = NOSITE;
457     sp->Funnel = NOSITE;
458     sp->PoisonEntry = false;
459     sp->Process = -1;
460     sp->Next = sp->Prev = NOSITE;
461     sp->Entry = Entry;
462     sp->Originator = NULL;
463     sp->FileFlags[0] = FEED_NAME;
464     sp->FileFlags[1] = '\0';
465     sp->Nice = innconf->nicekids;
466     sp->ControlOnly = false;
467     sp->DontWantNonExist = false;
468     sp->NeedOverviewCreation = false;
469     sp->FeedwithoutOriginator = false;
470     sp->DropFiltered = false;
471     sp->HashFeedList = NULL;
472
473     /* Nip off the first field, the site name. */
474     if ((f2 = strchr(Entry, NF_FIELD_SEP)) == NULL)
475         return "missing field 2";
476     *f2++ = '\0';
477     sp->Name = Entry;
478     if ((p = strchr(sp->Name, NF_SUBFIELD_SEP)) != NULL) {
479         /* Exclusions within the site field. */
480         *p++ = '\0';
481         if (*p)
482         sp->Exclusions = CommaSplit(p);
483     }
484     sp->NameLength = strlen(sp->Name);
485
486     /* Parse the second field, the subscriptions. */
487     if ((f3 = strchr(f2, NF_FIELD_SEP)) == NULL)
488         return "missing field 3";
489     *f3++ = '\0';
490     if ((p = strchr(f2, NF_SUBFIELD_SEP)) != NULL) {
491         /* Distributions within the subscription field. */
492         *p++ = '\0';
493         if (*p)
494         sp->Distributions = CommaSplit(p);
495     }
496     if (f2)
497     sp->Patterns = CommaSplit(f2);
498
499     if (subbed) {
500         /* Read the subscription patterns and set the bits. */
501         memset(subbed, SUB_DEFAULT, nGroups);
502         memset(poison, SUB_DEFAULT, nGroups);
503         if (ME.Patterns)
504             SITEsetlist(ME.Patterns, subbed, poison, &ME.PoisonEntry);
505         SITEsetlist(sp->Patterns, subbed, poison, &sp->PoisonEntry);
506     }
507
508     /* Get the third field, the flags. */
509     if ((f4 = strchr(f3, NF_FIELD_SEP)) == NULL)
510         return "missing field 4";
511     *f4++ = '\0';
512     JustModerated = false;
513     JustUnmoderated = false;
514     sp->Type = FTfile;
515     for (save = argv = CommaSplit(f3); (p = *argv++) != NULL; )
516         switch (*p) {
517         default:
518             return "unknown field 3 flag";
519         case '\0':
520             break;
521         case '<':
522             if (*++p && CTYPE(isdigit, *p))
523                 sp->MaxSize = atol(p);
524             break;
525         case '>':
526             if (*++p && CTYPE(isdigit, *p))
527                 sp->MinSize = atol(p);
528             break;
529         case 'A':
530             while (*++p)
531                 switch (*p) {
532                 default:
533                     return "unknown A param in field 3";
534                 case 'c': sp->IgnoreControl = true;
535                           sp->ControlOnly = false;
536                           break;
537                 case 'C': sp->ControlOnly = true;
538                           sp->IgnoreControl = false;
539                           break;
540                 case 'd': sp->DistRequired = true;      break;
541                 case 'e': sp->DontWantNonExist = true;  break;
542                 case 'f': sp->DropFiltered = true;      break;
543                 case 'o': sp->NeedOverviewCreation = true;      break;
544                 case 'O': sp->FeedwithoutOriginator = true;     break;
545                 case 'p': sp->IgnorePath = true;        break;
546                 }
547             break;
548         case 'B':
549             if (*++p && CTYPE(isdigit, *p)) {
550                 sp->StartWriting = atoi(p);
551                 if ((p = strchr(p, NF_SUBFIELD_SEP)) != NULL
552                  && *++p
553                  && CTYPE(isdigit, *p))
554                     sp->StopWriting = atoi(p);
555             }
556             break;
557         case 'C':
558             if (*++p && CTYPE(isdigit, *p))
559                 sp->Crosscount = atoi(p);
560             else
561                 sp->Crosscount = 1;
562             break;
563         case 'F':
564             if (*++p == '\0')
565                 return "missing file name for F param in field 3";
566             else if (*p == '/')
567                 sp->SpoolName = xstrdup(p);
568             else {
569                 sp->SpoolName = xmalloc(strlen(innconf->pathoutgoing) + 1 +
570                                                 strlen(p) + 1);
571                 FileGlue(sp->SpoolName, innconf->pathoutgoing, '/', p);
572             }
573             break;
574         case 'G':
575             if (*++p && CTYPE(isdigit, *p))
576                 sp->Groupcount = atoi(p);
577             else
578                 sp->Groupcount = 1;
579             break;
580         case 'H':
581             if (*++p && CTYPE(isdigit, *p))
582                 sp->Hops = atoi(p);
583             else
584                 sp->Hops = 1;
585             break;
586         case 'I':
587             if (*++p && CTYPE(isdigit, *p))
588                 sp->Flushpoint = atol(p);
589             break;
590         case 'N':
591             while (*++p)
592                 switch (*p) {
593                 default:
594                     return "unknown N param in field 3";
595                 case 'm': JustModerated = true;         break;
596                 case 'u': JustUnmoderated = true;       break;
597                 }
598             break;
599         case 'O':
600             if (*++p == '\0')
601                 return "missing originator name for O param in field 3";
602             sp->Originator = SlashSplit(p);
603             break;
604         case 'P':
605             if (*++p && CTYPE(isdigit, *p))
606                 sp->Nice = atoi(p);
607             break;
608         case 'Q':
609             hf = xmalloc(sizeof(HASHFEEDLIST));
610             p++;
611             /* Check whether it is a quickhash or a MD5 hashfeed. */
612             if (*p == '@') {
613                 p++;
614                 hf->type = HASHFEED_QH;
615             } else
616                 hf->type = HASHFEED_MD5;
617             /* Check the presence of a starting byte-offset for hashfeed. */
618             if ((u = strchr(p, '_')) != NULL) {
619                 if (sscanf(u + 1, "%u", &hf->offset) != 1 || hf->offset > 12) {
620                     free(hf);
621                     return "invalid hash offset for Q param in field 3";
622                 }
623             } else
624                 hf->offset = 0;
625             if (sscanf(p, "%u/%u", &hf->begin, &hf->mod) == 2) {
626                 hf->end = hf->begin;
627             } else if (sscanf(p, "%u-%u/%u", &hf->begin, &hf->end,
628                               &hf->mod) != 3) {
629                 free(hf);
630                 return "hash not in x/z or x-y/z format for Q param in field 3";
631             }
632             if (hf->begin > hf->end || hf->end > hf->mod) {
633                 free(hf);
634                 return "incorrect hash values for Q param in field 3";
635             }
636             hf->next = sp->HashFeedList;
637             sp->HashFeedList = hf;
638             break;
639         case 'S':
640             if (*++p && CTYPE(isdigit, *p))
641                 sp->StartSpooling = atol(p);
642             break;
643         case 'T':
644             switch (*++p) {
645             default:
646                 return "unknown T param in field 3";
647             case 'c': sp->Type = FTchannel;     break;
648             case 'l': sp->Type = FTlogonly;     break;
649             case 'f': sp->Type = FTfile;        break;
650             case 'm': sp->Type = FTfunnel;      break;
651             case 'p': sp->Type = FTprogram;     break;
652             case 'x': sp->Type = FTexploder;    break;
653             }
654             break;
655         case 'U':
656             if (*++p && CTYPE(isdigit, *p))
657                 sp->Followcount = atoi(p);
658             else
659                 sp->Followcount = 1;
660             break;
661         case 'W':
662             for (i = 0; *++p && i < FEED_MAXFLAGS; ) {
663                 switch (*p) {
664                 default:
665                     return "unknown W param in field 3";
666                 case FEED_FNLNAMES:             /* Funnel feed names    */
667                     sp->FNLwantsnames = true;
668                     break;
669                 case FEED_HEADERS:              /* Article headers      */
670                     NeedHeaders = true;
671                     break;
672                 case FEED_OVERVIEW:
673                     NeedOverview = true;        /* Overview data        */
674                     break;
675                 case FEED_PATH:                 /* Path                 */
676                     NeedPath = true;
677                     break;
678                 case FEED_BYTESIZE:             /* Size in bytes        */
679                 case FEED_FULLNAME:             /* Full filename        */
680                 case FEED_HASH:                 /* Hash                 */
681                 case FEED_HDR_DISTRIB:          /* Distribution header  */
682                 case FEED_STOREDGROUP:          /* stored newsgroup     */
683                     NeedStoredGroup = true;
684                     break;
685                 case FEED_HDR_NEWSGROUP:        /* Newsgroup header     */
686                 case FEED_MESSAGEID:            /* Message-ID           */
687                 case FEED_NAME:                 /* Filename             */
688                 case FEED_NEWSGROUP:            /* Newsgroup            */
689                 case FEED_REPLIC:               /* Replication data     */
690                     NeedReplicdata = true;
691                     break;
692                 case FEED_SITE:                 /* Site that gave it    */
693                 case FEED_TIMERECEIVED:         /* When received        */
694                 case FEED_TIMEPOSTED:           /* When posted          */
695                 case FEED_TIMEEXPIRED:          /* When will be expired */
696                     break;
697                 }
698                 sp->FileFlags[i++] = *p;
699             }
700             if (*p)
701                 return "too many W param values";
702             sp->FileFlags[i] = '\0';
703             break;
704         }
705     free(save);
706     if (sp->Flushpoint && sp->Type != FTfile)
707         return "I param with non-file feed";
708     if (sp->Flushpoint == 0 && sp->Type == FTfile)
709         sp->Flushpoint = SITE_BUFFER_SIZE;
710
711     if (subbed) {
712         /* Modify the subscription list based on the flags. */
713         if (JustModerated)
714             for (p = subbed, ngp = Groups, i = nGroups; --i >= 0; ngp++, p++)
715                 if (ngp->Rest[0] != NF_FLAG_MODERATED)
716                     *p = false;
717         if (JustUnmoderated)
718             for (p = subbed, ngp = Groups, i = nGroups; --i >= 0; ngp++, p++)
719                 if (ngp->Rest[0] == NF_FLAG_MODERATED)
720                     *p = false;
721     }
722
723     /* Get the fourth field, the param. */
724     if (*f4 == '\0' && sp != &ME) {
725         if (sp->Type != FTfile && sp->Type != FTlogonly)
726             return "empty field 4";
727         sp->Param = xmalloc(strlen(innconf->pathoutgoing) + 1 +
728                                                 sp->NameLength + 1);
729         FileGlue(sp->Param, innconf->pathoutgoing, '/', sp->Name);
730     }
731     else if (sp->Type == FTfile && *f4 != '/') {
732         sp->Param = xmalloc(strlen(innconf->pathoutgoing) + 1 +
733                                                 strlen(f4) + 1);
734         FileGlue(sp->Param, innconf->pathoutgoing, '/', f4);
735     }
736     else
737         sp->Param = xstrdup(f4);
738
739     if (sp->SpoolName == NULL) {
740         sp->SpoolName = xmalloc(strlen(innconf->pathoutgoing) + 1 +
741                                                 strlen(sp->Name) + 1);
742         FileGlue(sp->SpoolName, innconf->pathoutgoing, '/', sp->Name);
743     }
744
745     /* Make sure there is only one %s, and only one *. */
746     if (sp->Type == FTprogram) {
747         f3 = NULL;
748         for (f2 = sp->Param; *f2; f2 = p + 1) {
749             p = strchr(f2, '%');
750             if (p == NULL)
751                 break;
752             if (p[1] == '%') {
753                 p++;
754                 continue;
755             }
756             if (f3 != NULL)
757                 return "bad (extra) sprintf format for field 4";
758             f3 = p;
759             while (*++p && *p != '*' && !CTYPE(isalpha, *p))
760                 continue;
761             if (*p != 's')
762                 return "bad sprintf format for field 4";
763         }
764         if (sp->FNLwantsnames
765          && ((p = strchr(sp->Param, '*')) == NULL
766           || strchr(++p, '*') != NULL))
767             return "multiple or no *'s in field 4";
768     }
769
770     /* Now tell the groups this site gets that they should feed this site. */
771     if (sp != &ME && subbed) {
772         isp = sp - Sites;
773         for (p = subbed, u = poison, ngp = Groups, i = nGroups;
774                 --i >= 0; ngp++) {
775             if (*p++) {
776                 for (j = 0; j < ngp->nSites; j++)
777                     if (ngp->Sites[j] == NOSITE) {
778                         ngp->Sites[j] = isp;
779                         break;
780                     }
781                 if (j == ngp->nSites)
782                     ngp->Sites[ngp->nSites++] = isp;
783             }
784             if (*u++) {
785                 for (j = 0; j < ngp->nPoison; j++)
786                     if (ngp->Poison[j] == NOSITE) {
787                         ngp->Poison[j] = isp;
788                         break;
789                     }
790                 if (j == ngp->nPoison)
791                     ngp->Poison[ngp->nPoison++] = isp;
792             }
793         }
794     }
795
796     /* If this is a duplicate name, find the master. */
797     nsp = SITEfind(sp->Name);
798     if (nsp == sp)
799         nsp = SITEfindnext(sp->Name, nsp);
800     if (nsp != NULL) {
801         if (nsp->Master != NOSITE)
802             nsp = &Sites[nsp->Master];
803         if (nsp != sp) {
804             sp->Master = nsp - Sites;
805             nsp->IsMaster = true;
806         }
807     }
808
809     return NULL;
810 }
811
812
813 /*
814 **  Patch up the funnel references.
815 */
816 bool
817 SITEfunnelpatch(void)
818 {
819     int         i;
820     int         length;
821     SITE        *sp;
822     SITE        *funnel;
823     bool        result;
824
825     /* Get worst-case length of all sitenames. */
826     for (length = 0, i = nSites, sp = Sites; --i >= 0; sp++)
827         if (sp->Name != NULL)
828             length += 1 + strlen(sp->Name);
829
830     /* Loop over all funnel feeds. */
831     for (result = true, i = nSites, sp = Sites; --i >= 0; sp++) {
832         if (sp->Name == NULL || sp->Type != FTfunnel)
833             continue;
834
835         /* Find the entry they feed in to, give that entry a buffer. */
836         if (sp->Param == NULL) {
837             syslog(L_FATAL, "%s funnel NULL", sp->Name);
838             SITEfree(sp);
839             result = false;
840             continue;
841         }
842         if ((funnel = SITEfind(sp->Param)) == NULL) {
843             syslog(L_FATAL, "%s funnel_bad", sp->Name);
844             SITEfree(sp);
845             result = false;
846             continue;
847         }
848         if (funnel->Type == FTfunnel) {
849             syslog(L_FATAL, "%s funnels to funnel %s", sp->Name, funnel->Name);
850             SITEfree(sp);
851             result = false;
852             continue;
853         }
854         if (funnel->FNLnames.data == NULL) {
855             funnel->FNLnames.size = length;
856             funnel->FNLnames.data = xmalloc(length);
857         }
858         else if (funnel->FNLnames.size != length) {
859             funnel->FNLnames.size = length;
860             funnel->FNLnames.data = xrealloc(funnel->FNLnames.data, length);
861         }
862         sp->Funnel = funnel - Sites;
863     }
864
865     return result;
866 }
867
868
869 /*
870 **  Read the entries in the newsfeeds file, and parse them one at a time.
871 */
872 void
873 SITEparsefile(bool StartSite)
874 {
875   int           i;
876   char *        p;
877   char **       strings;
878   SITE *        sp;
879   char *        subbed;
880   char *        poison;
881   const char *  error;
882   int           errors;
883   int           setuperrors;
884
885   /* Free old sites info. */
886   if (Sites) {
887     for (i = nSites, sp = Sites; --i >= 0; sp++) {
888       SITEflush(sp, false);
889       SITEfree(sp);
890     }
891     free(Sites);
892     SITEfree(&ME);
893   }
894
895   /* Count the number of sites. */
896   for (strings = SITEreadfile(false), nSites = 0; strings[nSites]; nSites++)
897     continue;
898   Sites = xcalloc(nSites, sizeof(SITE));
899
900   /* Set up scratch subscription list. */
901   subbed = xmalloc(nGroups);
902   poison = xmalloc(nGroups);
903   /* reset global variables */
904   NeedHeaders = NeedOverview = NeedPath = NeedStoredGroup = NeedReplicdata
905     = false;
906
907   ME.Prev = 0; /* Used as a flag to ensure exactly one ME entry */
908   for (sp = Sites, errors = 0, setuperrors = 0, i = 0; i < nSites; i++) {
909     p = strings[i];
910     if (p[0] == 'M' && p[1] == 'E' &&
911       ((p[2] == NF_FIELD_SEP) || (p[2] == NF_SUBFIELD_SEP))) {
912       if (ME.Prev == NOSITE) {
913         syslog(L_FATAL, "bad_newsfeeds. Must have exactly one ME entry");
914         errors++;
915       } else if ((error = SITEparseone(p, &ME, subbed, poison)) != NULL) {
916         syslog(L_FATAL, "%s bad_newsfeeds %s", MaxLength(p, p), error);
917         errors++;
918       }
919       continue;
920     }
921     if ((error = SITEparseone(p, sp, subbed, poison)) != NULL) {
922       syslog(L_FATAL, "%s bad_newsfeeds %s", MaxLength(p, p), error);
923       errors++;
924       continue;
925     }
926     if (StartSite && !SITEsetup(sp)) {
927       syslog(L_FATAL, "%s cant setup %m", sp->Name);
928       setuperrors++;
929       continue;
930     }
931     sp->Working = true;
932     sp++;
933   }
934   if (ME.Prev != NOSITE) {
935     syslog(L_FATAL, "bad_newsfeeds. Must have exactly one ME entry");
936     errors++;
937   }
938
939   if (errors || setuperrors) {
940     if (errors)
941       syslog(L_FATAL, "%s syntax_error %s", LogName, SITEfeedspath);
942     if (setuperrors)
943       syslog(L_FATAL, "%s setup_error %s", LogName, SITEfeedspath);
944     JustCleanup();
945     exit(1);
946   }
947
948   /* Free our scratch array, set up the funnel links. */
949   nSites = sp - Sites;
950   free(subbed);
951   free(poison);
952   free(strings);
953   if (!SITEfunnelpatch()) {
954     JustCleanup();
955     exit(1);
956   }
957 }