1 /* $Id: newsfeeds.c 7730 2008-04-06 08:27:16Z iulius $
3 ** Routines for the in-core data structures for the newsfeeds file.
9 #include "inn/innconf.h"
13 ** List of variables assigned in the configuration file.
15 typedef struct _SITEVARIABLES {
19 struct _SITEVARIABLES *Next;
22 /* The character which introduces a variable assignment or reference. */
23 #define VARIABLE_CHAR '$'
26 static char *SITEfeedspath = NULL;
27 static SITEVARIABLES *SITEvariables = NULL;
31 ** Return a copy of an array of strings.
34 SITEcopystrings(char **av)
40 for (pp = av; *pp; pp++)
42 for (new = save = xmalloc((pp - av + 1) * sizeof(char *)), pp = av; *pp; pp++)
43 *new++ = xstrdup(*pp);
49 ** Adds a variable from a line.
52 SITEaddvariable(char *line)
57 if (*line != VARIABLE_CHAR)
60 for (p = line + 1; *p != '\0' && CTYPE(isalnum, *p); p++)
65 syslog(L_FATAL, "%s bad_newsfeed variable name '%s' too long",
70 /* Chop off trailing spaces. */
71 q = p + strlen(p) - 1;
72 while (q > p && (*q == ' ' || *q == '\t'))
75 /* Seperate variable name from contents. */
80 /* Is variable already defined? Free and reassign. */
83 while (v && strcmp(line + 1, v->Name)) {
90 v = xmalloc(sizeof(SITEVARIABLES));
95 v->Name = xstrdup(line + 1);
99 /* Add variable's contents. */
101 for (q = v->Value = xmalloc(strlen(p) + 1); *p != '\0'; p++) {
102 if (*p != ' ' && *p != '\t')
112 SITEclearvariables(void)
114 SITEVARIABLES *v, *w;
124 SITEvariables = NULL;
127 static SITEVARIABLES *
128 SITEfindvariable(char *name)
133 while (v && strcmp(v->Name, name) != 0)
139 SITEexpandvariables(char *site)
148 /* Count characters. */
151 for (p = site; p <= site + strlen(site); p++) {
152 /* In variable name. */
154 if (CTYPE(isalnum, *p)) {
155 if (q - varname > 32) {
156 /* Add ignored modifier. */
159 /* Add ignored $ and characters. */
160 c += strlen(varname);
161 /* Add this character. */
167 /* Append to variable name. */
171 v = SITEfindvariable(varname + 1);
173 /* Add length of contents. */
174 c += strlen(v->Value);
175 /* If modified add number of mods. */
179 /* Add ignored modifier. */
182 c += strlen(varname); /* add ignored $ and characters */
188 /* New variable starts */
189 if (*p == VARIABLE_CHAR) {
191 memset(varname, 0, sizeof(varname));
192 *q++ = VARIABLE_CHAR;
196 /* Add last modifier */
200 if (*p == SUB_NEGATE || *p == SUB_POISON) {
203 /* Add this character. */
209 s = r = xmalloc(c + 1);
212 for (p = site; p <= site + strlen(site); p++) {
213 /* In variable name. */
215 if (CTYPE(isalnum, *p)) {
216 if (q - varname > 32) {
219 for (q = varname; *q; q++)
229 v = SITEfindvariable(varname + 1);
233 for (q = v->Value; *q; q++) {
235 if (*q == ',' && modifier)
241 for (q = varname; *q; q++)
248 /* New variable starts. */
249 if (*p == VARIABLE_CHAR) {
251 memset(varname, 0, sizeof(varname));
252 *q++ = VARIABLE_CHAR;
259 if (*p == SUB_NEGATE || *p == SUB_POISON)
270 ** Read the newsfeeds file, return a string array of entries.
273 SITEreadfile(const bool ReadOnly)
275 static char **old_strings;
276 static time_t old_mtime;
277 static ino_t old_ino;
278 static off_t old_size;
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);
296 /* Data's bad, toss it. */
297 for (i = 0; old_strings[i] != NULL; i++)
298 free(old_strings[i]);
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);
307 old_mtime = Sb.st_mtime;
309 old_size = Sb.st_size;
311 /* Get a gross count of the number of sites. */
312 for (p = data, i = 0; (p = strchr(p, '\n')) != NULL; p++, i++)
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; ) {
323 if (*p == '\\' && p[1] == '\n')
324 while (*++p && CTYPE(isspace, *p))
330 if (*site == '#' || *site == '\0')
332 if (*site == VARIABLE_CHAR && SITEaddvariable(site))
334 if (strspn(site," \t") == strlen (site))
337 old_strings[i++] = SITEexpandvariables(site);
339 old_strings[i++] = xstrdup(site);
341 old_strings[i] = NULL;
343 SITEclearvariables();
345 return ReadOnly ? old_strings : SITEcopystrings(old_strings);
350 ** Modify "subbed" according to the patterns in "patlist."
353 SITEsetlist(char **patlist, char *subbed, char *poison, bool *poisonEntry)
363 while ((pat = *patlist++) != NULL) {
364 subvalue = *pat != SUB_NEGATE && *pat != SUB_POISON;
365 poisonvalue = *pat == SUB_POISON;
370 if (!*poisonEntry && poisonvalue)
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 == '[')
380 /* Simple string; look it up, set it. */
381 if ((ngp = NGfind(pat)) != NULL) {
382 subbed[ngp - Groups] = subvalue;
383 poison[ngp - Groups] = poisonvalue;
387 for (p = subbed, u = poison, ngp = Groups, i = nGroups;
388 --i >= 0; ngp++, p++, u++)
389 if (uwildmat(ngp->Name, pat)) {
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."
403 SlashSplit(char *text)
410 /* How much space do we need? */
411 for (i = 2, p = text; *p; p++)
415 for (av = save = xmalloc(i * sizeof(char *)), *av++ = p = text; *p; )
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.
434 SITEparseone(char *Entry, SITE *sp, char *subbed, char *poison)
447 bool JustUnmoderated;
458 sp->PoisonEntry = false;
460 sp->Next = sp->Prev = NOSITE;
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;
473 /* Nip off the first field, the site name. */
474 if ((f2 = strchr(Entry, NF_FIELD_SEP)) == NULL)
475 return "missing field 2";
478 if ((p = strchr(sp->Name, NF_SUBFIELD_SEP)) != NULL) {
479 /* Exclusions within the site field. */
482 sp->Exclusions = CommaSplit(p);
484 sp->NameLength = strlen(sp->Name);
486 /* Parse the second field, the subscriptions. */
487 if ((f3 = strchr(f2, NF_FIELD_SEP)) == NULL)
488 return "missing field 3";
490 if ((p = strchr(f2, NF_SUBFIELD_SEP)) != NULL) {
491 /* Distributions within the subscription field. */
494 sp->Distributions = CommaSplit(p);
497 sp->Patterns = CommaSplit(f2);
500 /* Read the subscription patterns and set the bits. */
501 memset(subbed, SUB_DEFAULT, nGroups);
502 memset(poison, SUB_DEFAULT, nGroups);
504 SITEsetlist(ME.Patterns, subbed, poison, &ME.PoisonEntry);
505 SITEsetlist(sp->Patterns, subbed, poison, &sp->PoisonEntry);
508 /* Get the third field, the flags. */
509 if ((f4 = strchr(f3, NF_FIELD_SEP)) == NULL)
510 return "missing field 4";
512 JustModerated = false;
513 JustUnmoderated = false;
515 for (save = argv = CommaSplit(f3); (p = *argv++) != NULL; )
518 return "unknown field 3 flag";
522 if (*++p && CTYPE(isdigit, *p))
523 sp->MaxSize = atol(p);
526 if (*++p && CTYPE(isdigit, *p))
527 sp->MinSize = atol(p);
533 return "unknown A param in field 3";
534 case 'c': sp->IgnoreControl = true;
535 sp->ControlOnly = false;
537 case 'C': sp->ControlOnly = true;
538 sp->IgnoreControl = false;
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;
549 if (*++p && CTYPE(isdigit, *p)) {
550 sp->StartWriting = atoi(p);
551 if ((p = strchr(p, NF_SUBFIELD_SEP)) != NULL
553 && CTYPE(isdigit, *p))
554 sp->StopWriting = atoi(p);
558 if (*++p && CTYPE(isdigit, *p))
559 sp->Crosscount = atoi(p);
565 return "missing file name for F param in field 3";
567 sp->SpoolName = xstrdup(p);
569 sp->SpoolName = xmalloc(strlen(innconf->pathoutgoing) + 1 +
571 FileGlue(sp->SpoolName, innconf->pathoutgoing, '/', p);
575 if (*++p && CTYPE(isdigit, *p))
576 sp->Groupcount = atoi(p);
581 if (*++p && CTYPE(isdigit, *p))
587 if (*++p && CTYPE(isdigit, *p))
588 sp->Flushpoint = atol(p);
594 return "unknown N param in field 3";
595 case 'm': JustModerated = true; break;
596 case 'u': JustUnmoderated = true; break;
601 return "missing originator name for O param in field 3";
602 sp->Originator = SlashSplit(p);
605 if (*++p && CTYPE(isdigit, *p))
609 hf = xmalloc(sizeof(HASHFEEDLIST));
611 /* Check whether it is a quickhash or a MD5 hashfeed. */
614 hf->type = HASHFEED_QH;
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) {
621 return "invalid hash offset for Q param in field 3";
625 if (sscanf(p, "%u/%u", &hf->begin, &hf->mod) == 2) {
627 } else if (sscanf(p, "%u-%u/%u", &hf->begin, &hf->end,
630 return "hash not in x/z or x-y/z format for Q param in field 3";
632 if (hf->begin > hf->end || hf->end > hf->mod) {
634 return "incorrect hash values for Q param in field 3";
636 hf->next = sp->HashFeedList;
637 sp->HashFeedList = hf;
640 if (*++p && CTYPE(isdigit, *p))
641 sp->StartSpooling = atol(p);
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;
656 if (*++p && CTYPE(isdigit, *p))
657 sp->Followcount = atoi(p);
662 for (i = 0; *++p && i < FEED_MAXFLAGS; ) {
665 return "unknown W param in field 3";
666 case FEED_FNLNAMES: /* Funnel feed names */
667 sp->FNLwantsnames = true;
669 case FEED_HEADERS: /* Article headers */
673 NeedOverview = true; /* Overview data */
675 case FEED_PATH: /* Path */
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;
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;
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 */
698 sp->FileFlags[i++] = *p;
701 return "too many W param values";
702 sp->FileFlags[i] = '\0';
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;
712 /* Modify the subscription list based on the flags. */
714 for (p = subbed, ngp = Groups, i = nGroups; --i >= 0; ngp++, p++)
715 if (ngp->Rest[0] != NF_FLAG_MODERATED)
718 for (p = subbed, ngp = Groups, i = nGroups; --i >= 0; ngp++, p++)
719 if (ngp->Rest[0] == NF_FLAG_MODERATED)
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 +
729 FileGlue(sp->Param, innconf->pathoutgoing, '/', sp->Name);
731 else if (sp->Type == FTfile && *f4 != '/') {
732 sp->Param = xmalloc(strlen(innconf->pathoutgoing) + 1 +
734 FileGlue(sp->Param, innconf->pathoutgoing, '/', f4);
737 sp->Param = xstrdup(f4);
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);
745 /* Make sure there is only one %s, and only one *. */
746 if (sp->Type == FTprogram) {
748 for (f2 = sp->Param; *f2; f2 = p + 1) {
757 return "bad (extra) sprintf format for field 4";
759 while (*++p && *p != '*' && !CTYPE(isalpha, *p))
762 return "bad sprintf format for field 4";
764 if (sp->FNLwantsnames
765 && ((p = strchr(sp->Param, '*')) == NULL
766 || strchr(++p, '*') != NULL))
767 return "multiple or no *'s in field 4";
770 /* Now tell the groups this site gets that they should feed this site. */
771 if (sp != &ME && subbed) {
773 for (p = subbed, u = poison, ngp = Groups, i = nGroups;
776 for (j = 0; j < ngp->nSites; j++)
777 if (ngp->Sites[j] == NOSITE) {
781 if (j == ngp->nSites)
782 ngp->Sites[ngp->nSites++] = isp;
785 for (j = 0; j < ngp->nPoison; j++)
786 if (ngp->Poison[j] == NOSITE) {
787 ngp->Poison[j] = isp;
790 if (j == ngp->nPoison)
791 ngp->Poison[ngp->nPoison++] = isp;
796 /* If this is a duplicate name, find the master. */
797 nsp = SITEfind(sp->Name);
799 nsp = SITEfindnext(sp->Name, nsp);
801 if (nsp->Master != NOSITE)
802 nsp = &Sites[nsp->Master];
804 sp->Master = nsp - Sites;
805 nsp->IsMaster = true;
814 ** Patch up the funnel references.
817 SITEfunnelpatch(void)
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);
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)
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);
842 if ((funnel = SITEfind(sp->Param)) == NULL) {
843 syslog(L_FATAL, "%s funnel_bad", sp->Name);
848 if (funnel->Type == FTfunnel) {
849 syslog(L_FATAL, "%s funnels to funnel %s", sp->Name, funnel->Name);
854 if (funnel->FNLnames.data == NULL) {
855 funnel->FNLnames.size = length;
856 funnel->FNLnames.data = xmalloc(length);
858 else if (funnel->FNLnames.size != length) {
859 funnel->FNLnames.size = length;
860 funnel->FNLnames.data = xrealloc(funnel->FNLnames.data, length);
862 sp->Funnel = funnel - Sites;
870 ** Read the entries in the newsfeeds file, and parse them one at a time.
873 SITEparsefile(bool StartSite)
885 /* Free old sites info. */
887 for (i = nSites, sp = Sites; --i >= 0; sp++) {
888 SITEflush(sp, false);
895 /* Count the number of sites. */
896 for (strings = SITEreadfile(false), nSites = 0; strings[nSites]; nSites++)
898 Sites = xcalloc(nSites, sizeof(SITE));
900 /* Set up scratch subscription list. */
901 subbed = xmalloc(nGroups);
902 poison = xmalloc(nGroups);
903 /* reset global variables */
904 NeedHeaders = NeedOverview = NeedPath = NeedStoredGroup = NeedReplicdata
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++) {
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");
915 } else if ((error = SITEparseone(p, &ME, subbed, poison)) != NULL) {
916 syslog(L_FATAL, "%s bad_newsfeeds %s", MaxLength(p, p), error);
921 if ((error = SITEparseone(p, sp, subbed, poison)) != NULL) {
922 syslog(L_FATAL, "%s bad_newsfeeds %s", MaxLength(p, p), error);
926 if (StartSite && !SITEsetup(sp)) {
927 syslog(L_FATAL, "%s cant setup %m", sp->Name);
934 if (ME.Prev != NOSITE) {
935 syslog(L_FATAL, "bad_newsfeeds. Must have exactly one ME entry");
939 if (errors || setuperrors) {
941 syslog(L_FATAL, "%s syntax_error %s", LogName, SITEfeedspath);
943 syslog(L_FATAL, "%s setup_error %s", LogName, SITEfeedspath);
948 /* Free our scratch array, set up the funnel links. */
953 if (!SITEfunnelpatch()) {