/*
libutil -- handling xover records

Written by Arnt Gulbrandsen <agulbra@troll.no> and copyright 1995
Troll Tech AS, Postboks 6133 Etterstad, 0602 Oslo, Norway, fax +47
22646949.
Modified by Cornelius Krasel <krasel@wpxx02.toxi.uni-wuerzburg.de>
and Randolf Skerka <Randolf.Skerka@gmx.de>.
Copyright of the modifications 1997.
Modified by Kent Robotti <robotti@erols.com>. Copyright of the
modifications 1998.
Modified by Markus Enzenberger <enz@cip.physik.uni-muenchen.de>.
Copyright of the modifications 1998.
Modified by Cornelius Krasel <krasel@wpxx02.toxi.uni-wuerzburg.de>.
Copyright of the modifications 1998, 1999.
Modified by Ralf Wildenhues <ralf.wildenhues@gmx.de>
Copyright of the modifications 2002.
Modified by Matthias Andree <matthias.andree@gmx.de>
Copyright of the modifications 2000 - 2005, 2007.

See file COPYING for restrictions on the use of this software.
*/

#include "leafnode.h"
#include <fcntl.h>
#include <sys/param.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <ctype.h>
#include <errno.h>
#include <limits.h>
#include <stdlib.h>
#include <netdb.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <syslog.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include "system.h"
#include "strlcpy.h"
#include "ln_log.h"

static void
tabstospaces(char *t)
{
    while (*t) {
	if (*t == '\t')
	    *t = ' ';
	t++;
    }

}

static /*@null@*/ /*@only@*/
char *getxoverline(const char *filename, const char **e /** error message is stored here */)
{
    char *l;
    const char *em;
    char *result;
    FILE *f;

    result = NULL;
    *e = NULL;
    debug = 0;
    if ((f = fopen(filename, "r"))) {
	char *from, *subject, *date, *msgid, *references, *lines, *xref;
	unsigned long bytes, linecount;
	char **h;
	int body;

	from = subject = date = msgid = references = xref = lines = NULL;
	bytes = linecount = 0;
	h = NULL;
	body = 0;

	while (!feof(f) && ((l = getaline(f)) != NULL)) {
	    tabstospaces(l);
	    linecount++;
	    bytes += strlen(l) + 2; /* normalize CR LF -> add 2 per line */
	    if (body || !l) {
		/* do nothing */
	    } else if (!body && !*l) {
		linecount = 0;
		body = 1;
	    } else if (*l && isspace((unsigned char)*l)) {
		/* cater for folded headers */
		if (h) {
		    (*h) = critrealloc(*h, strlen(*h) + strlen(l) + 1,
				       "extending header");
		    strcat(*h, l);	/* RATS: ignore */
		}
	    } else if (!from && !strncasecmp("From:", l, 5)) {
		l += 5;
		SKIPLWS(l);
		if (*l) {
		    from = critstrdup(l, "getxoverline");
		    h = &from;
		}
	    } else if (!subject && !strncasecmp("Subject:", l, 8)) {
		l += 8;
		SKIPLWS(l);
		if (*l) {
		    subject = critstrdup(l, "getxoverline");
		    h = &subject;
		}
	    } else if (!date && !strncasecmp("Date:", l, 5)) {
		l += 5;
		SKIPLWS(l);
		if (*l) {
		    date = critstrdup(l, "getxoverline");
		    h = &date;
		}
	    } else if (!msgid && !strncasecmp("Message-ID:", l, 11)) {
		l += 11;
		SKIPLWS(l);
		if (*l) {
		    msgid = critstrdup(l, "getxoverline");
		    h = &msgid;
		}
	    } else if (!references && !strncasecmp("References:", l, 11)) {
		l += 11;
		SKIPLWS(l);
		if (*l) {
		    references = critstrdup(l, "getxoverline");
		    h = &references;
		}
	    } else if (!lines && !strncasecmp("Lines:", l, 6)) {
		l += 6;
		SKIPLWS(l);
		if (*l) {
		    lines = critstrdup(l, "getxoverline");
		    h = &lines;
		}
	    } else if (!xref && !strncasecmp("Xref:", l, 5)) {
		l += 5;
		SKIPLWS(l);
		if (*l) {
		    xref = critstrdup(l, "getxoverline");
		    h = &xref;
		}
	    } else {
		h = NULL;
	    }
	}
	if (from != NULL && date != NULL && subject != NULL &&
	    msgid != NULL && bytes) {
	    result = critmalloc(strlen(filename) + strlen(subject) + strlen(from) +
				strlen(date) + strlen(msgid) +
				(references ? strlen(references) : 0) +
				100 + (xref ? strlen(xref) : 0),
				"computing overview line");
	    sprintf(result, "%s\t%s\t%s\t%s\t%s\t%s\t%lu\t%lu",	/* RATS: ignore */
		    filename, subject, from, date, msgid,
		    references ? references : "",
		    bytes, lines ? strtoul(lines, NULL, 10) : linecount);
	    if (xref) {
		strcat(result, "\tXref: ");	/* RATS: ignore */
		strcat(result, xref);	/* RATS: ignore */
	    }
	} else {
	    if (from == NULL)
		*e = "missing From: header";
	    else if (date == NULL)
		*e = "missing Date: header";
	    else if (subject == NULL)
		*e = "missing Subject: header";
	    else if (msgid == NULL)
		*e = "missing Message-ID: header";
	    else if (bytes == 0)
		*e = "article has 0 bytes";
	}
	(void)fclose(f);
	if (from)
	    free(from);
	if (date)
	    free(date);
	if (subject)
	    free(subject);
	if (msgid)
	    free(msgid);
	if (references)
	    free(references);
	if (lines)
	    free(lines);
	if (xref)
	    free(xref);
    } else {
	ln_log(LNLOG_SERR, LNLOG_CARTICLE,
		"error: getxoverline: cannot open %s: %m", filename);
    }
    debug = debugmode;
    if (result && !legalxoverline(result, &em)) {
	*e = em;
	free(result);
	result = NULL;
    }
    return result;
}

/*
 * return 1 if xover is a legal overview line, 0 else
 */
int
legalxoverline(const char *xover, const char **e)
{
    const char *p;
    const char *q;

    if (!xover)
	return 0;

    /* anything that isn't tab, printable ascii, or latin-* -> kill */

    p = xover;
    while (*p) {
	int c = (unsigned char)*p++;

	if ((c != '\t' && c < ' ') || (c > 126 && c < 160)) {
	    *e = "non-printable characters in headers (relaxed check allows for iso-8859*)";
	    return 0;
	}
    }

    p = xover;
    q = strchr(p, '\t');
    if (!q) {
	*e = "missing Subject: header";
	return 0;
    }

    /* article number */

    while (p != q) {
	if (!isdigit((unsigned char)*p)) {
	    *e = "article number contains non-digit characters";
	    return 0;
	}
	p++;
    }

    p = q + 1;
    q = strchr(p, '\t');
    if (!q) {
	*e = "missing From: header";
	return 0;
    }

    /* subject: no limitations */

    p = q + 1;
    q = strchr(p, '\t');
    if (!q) {
	*e = "missing Date: header";
	return 0;
    }

    /* from: no limitations */

    p = q + 1;
    q = strchr(p, '\t');
    if (!q) {
	*e = "missing Message-ID: header";
	return 0;
    }

    /* date: no limitations */

    p = q + 1;
    q = strchr(p, '\t');
    if (!q) {
	*e = "missing References: or Bytes: header";
	return 0;
    }

    /* message-id: <*@*> */

    if (*p != '<') {
	*e = "Message-ID: does not start with \"<\"";
	return 0;
    }
    while (p != q && *p != '@' && *p != '>' && *p != ' ')
	p++;
    if (*p != '@') {
	*e = "Message-ID: does not contain @";
	return 0;
    }
    while (p != q && *p != '>' && *p != ' ')
	p++;
    if (*p != '>') {
	*e = "Message-ID: does not end with \">\"";
	return 0;
    }
    if (++p != q) {
	*e = "Message-ID: does not end with \">\"";
	return 0;
    }

    p = q + 1;
    q = strchr(p, '\t');
    if (!q) {
	*e = "missing Bytes: header";
	return 0;
    }

    /* references: a series of <*@*> separated by space */

#if 0
    while (p != q) {
	/* reference validation - users don't like it */
	if (*p != '<') {
	    *e = "References: does not start with \"<\"";
	    return 0;
	}
	while (p != q && *p != '@' && *p != '>' && *p != ' ')
	    p++;
	if (*p != '@') {
	    *e = "References: does not contain @";
	    return 0;
	}
	while (p != q && *p != '>' && *p != ' ')
	    p++;
	if (*p++ != '>') {
	    *e = "References: does not end with \">\"";
	    return 0;
	}
	while (p != q && *p == ' ')
	    p++;
    }
#endif

    p = q + 1;
    q = strchr(p, '\t');
    if (!q) {
	*e = "missing Lines: header";
	return 0;
    }

    /* byte count */

    while (p != q) {
	if (!isdigit((unsigned char)*p)) {
	    *e = "non-digit character in Bytes: header";
	    return 0;
	}
	p++;
    }

    p = q + 1;
    q = strchr(p, '\t');

    /* line count */

    while (p && *p && p != q) {
	if (!isdigit((unsigned char)*p)) {
	    *e = "non-digit character in Lines: header";
	    return 0;
	}
	p++;
    }

    if (!q) {
	*e = "missing Xref: entry";
	return 0;
    }

    {
	p = q + 1;

	/* xref */
	if (0 != strncasecmp(p, "Xref:", 5)) {
	    *e = "Xref header is missing or lacks Xref: tag";
	    return 0;
	}
    }

    return 1;
}

static void killcwd(void) {
    char *t = NULL;
    size_t s_t;

    if (agetcwd(&t, &s_t)) {
	if (chdir(spooldir)) {
	    ln_log(LNLOG_SERR, LNLOG_CTOP, "error: cannot chdir(%s): %m", spooldir);
	}
	if (rmdir(t) && errno != ENOTEMPTY) {
	    ln_log(LNLOG_SERR, LNLOG_CTOP, "error: cannot rmdir(%s): %m", t);
	}
	free(t);
    }
}

void freexover(void) {
    unsigned long art;

    if (xoverinfo) {
	for (art = xfirst; art <= xlast; art++) {
	    if (xoverinfo[art - xfirst].text) {
		free(xoverinfo[art - xfirst].text);
		xoverinfo[art - xfirst].text = NULL;
	    }
	}
	free(xoverinfo);
	xoverinfo = NULL;
    }

}

/* utility routine to pull the xover info into memory
   returns 0 if there's some error, non-zero else */
int
getxover(void)
{
    DIR *d;
    struct dirent *de;
    int fd;
    struct stat st;
    unsigned long art;
    char *overview = NULL;
    int error;
    char *p, *q;
    char *tt = NULL; size_t s_tt;

    error = 0;

    /* free any memory left over from last time */
    freexover();

    /* find article range */
    d = opendir(".");
    if (!d) {
	ln_log(LNLOG_SERR, LNLOG_CTOP, "error: opendir: %m");
	return 0;
    }

    xfirst = ULONG_MAX;
    xlast = 0;
    while ((de = readdir(d))) {
	/* weed out temporary .overview files from aborted earlier run */
	if (0 == strncmp(".overview.", de->d_name, 10))
	    log_unlink(de->d_name, 0);
	if (!isdigit((unsigned char)de->d_name[0]))
	    continue; /* skip files that don't start with a digit */
	/* WARNING: strtoul will happily return the negated value when
	 * fed a string that starts with a minus character! */
	art = strtoul(de->d_name, &p, 10);
	if (art && p && !*p) {
	    if (art < xfirst)
		xfirst = art;
	    if (art > xlast)
		xlast = art;
	}
    }

    if (xlast < xfirst) {
	/* we did not find any article files (1, 17, 815 or the like) */
	closedir(d);
	(void)unlink(".overview");
	if (debugmode) {
	    char *t = NULL; size_t s_t;
	    if (!agetcwd(&t, &s_t)) {
		ln_log(LNLOG_SERR, LNLOG_CGROUP, "error: getcwd: %m");
	    } else {
		syslog(LOG_DEBUG, "removed .overview file for %s", t);
		free(t);
	    }
	}
	killcwd();
	return 0;
    }

    /* next, read .overview, correct it if it seems too different from
       what the directory implies, and write the result back */
    rewinddir(d);

    xoverinfo = (struct xoverinfo *)
	critmalloc(sizeof(struct xoverinfo) * (xlast + 1 - xfirst),
		    "allocating overview array");
    memset(xoverinfo, 0, sizeof(struct xoverinfo) * (xlast + 1 - xfirst));

    if ((fd = open(".overview", O_RDONLY)) >= 0 &&
	    fstat(fd, &st) == 0) {
	overview = (char *)critmalloc(st.st_size + 1, "getxover");
	if ((off_t) read(fd, overview, st.st_size) != st.st_size) {
	    int e = errno;
	    char *t = NULL; size_t s_t;
	    /* short read */
	    close(fd);

	    if (!agetcwd(&t, &s_t)) {
		ln_log(LNLOG_SERR, LNLOG_CGROUP, "error: getcwd: %m");
	    } else {
		ln_log(LNLOG_SWARNING, LNLOG_CGROUP,
			"warning: short read on %s/.overview: %s",
			t, strerror(e));
		free(t);
	    }
	} else {
	    close(fd);
	    overview[st.st_size] = '\0';

	    /* okay, we have the content, so let's parse it roughly */
	    /* iterate line-wise */
	    p = overview;
	    while (p && *p) {
		const char *t;

		while (p && isspace((unsigned char)*p))
		    p++;
		q = strchr(p, '\n');
		if (q)
		    *q++ = '\0';

		
		art = strtoul(p, NULL, 10);
		if (legalxoverline(p, &t)) {
		    if (art > xlast || art < xfirst) {
			error++;
		    } else if (xoverinfo[art - xfirst].text) {
			char *tt = NULL; size_t s_tt;
			error++;
			if (!agetcwd(&tt, &s_tt)) {
			    ln_log(LNLOG_SERR, LNLOG_CARTICLE, "error: getcwd: %m");
			} else {
			    ln_log(LNLOG_SERR, LNLOG_CARTICLE, "error: multiple lines for article %lu "
				    "in .overview for %s", art, tt);
			    free(tt);
			}
			free (xoverinfo[art - xfirst].text);
			xoverinfo[art - xfirst].text = NULL;
			xoverinfo[art - xfirst].exists = -1;
		    } else if (xoverinfo[art - xfirst].exists == 0) {
			xoverinfo[art - xfirst].text = critstrdup(p, "getxover");
		    }
		} else {
		    char *tt = NULL; size_t s_tt;
		    if (!agetcwd(&tt, &s_tt)) {
			ln_log(LNLOG_SERR, LNLOG_CTOP, "error: getcwd: %m");
		    } else {
			ln_log(LNLOG_SNOTICE, LNLOG_CARTICLE, "illegal line for article %lu in .overview for %s: %s", art, tt, t);
			free(tt);
		    }
		}

		p = q;
	    } /* while p && *p */
	} /* if read went fine */
    } /* if open && fstat */

    if (!agetcwd(&tt, &s_tt)) {
	ln_log(LNLOG_SERR, LNLOG_CTOP, "error: getcwd: %m");
	closedir(d);
	return 0;
    }

    /* so, what was missing? */
    while ((de = readdir(d))) {
	if (de->d_name[0] == '.')
	    continue;
	art = strtoul(de->d_name, &p, 10);
	if (p && !*p && art >= xfirst && art <= xlast) {
	    if (!xoverinfo[art - xfirst].text) {
		const char *e;

		xoverinfo[art - xfirst].exists = 0;
		if (debugmode) {
		    syslog(LOG_DEBUG, "reading XOVER info from %s/%s",
			    tt, de->d_name);
		}
		error++;
		if ((xoverinfo[art - xfirst].text =
		     getxoverline(de->d_name, &e)) == NULL) {
		    ln_log(LNLOG_SINFO, LNLOG_CARTICLE,
			   "article %s/%s contained illegal headers: %s",
			   tt, de->d_name, e);
		    if (truncate(de->d_name, (off_t)0))
			ln_log(LNLOG_SWARNING, LNLOG_CARTICLE,
				"warning: failed to truncate broken %s/%s to 0 size: %m",
				tt,de->d_name);
		    if ((lstat(de->d_name, &st) == 0) && S_ISREG(st.st_mode)) {
			if (unlink(de->d_name))
			    ln_log(LNLOG_SWARNING, LNLOG_CARTICLE,
				    "warning: failed to remove broken %s/%s: %m", tt, de->d_name);
		    } else {
			ln_log(LNLOG_SWARNING, LNLOG_CARTICLE, 
				"warning: %s/%s is not a regular file", tt, de->d_name);
		    }
		}
	    }
	}

	if (art >= xfirst && art <= xlast && xoverinfo[art - xfirst].text) {
	    xoverinfo[art - xfirst].exists = 1;
	} else {
	    /* kill non-article files, like "core" */
	    if (art == 0)
	    {
		if (unlink(de->d_name)
		    && errno != EISDIR
		    && errno != EPERM
		    && verbose) {
		    ln_log(LNLOG_SWARNING, LNLOG_CGROUP,
			    "warning: deleting junk %s/%s failed: %s",
			   tt, de->d_name, strerror(errno));
		}
	    }
	}
    } /* while (de = readdir(d)) */

    /* count removed articles */
    for (art = xfirst; art <= xlast; art++) {
	if (xoverinfo[art - xfirst].text
	    && !xoverinfo[art - xfirst].exists) {
	    ++error;
	    free(xoverinfo[art - xfirst].text);
	    xoverinfo[art - xfirst].text = NULL;
	}
    }

    /* if something had to be fixed, write a better file to disk for
       next time - race conditions here, but none dangerous */
    if (error) {
	int wfd;
	char newfile[20]; /* RATS: ignore */

	if (debugmode)
	    syslog(LOG_DEBUG, "updated %d line%s in %s/.overview",
		   error, PLURAL(error), tt);

	strcpy(newfile, ".overview.XXXXXX");
	if ((wfd = mkstemp(newfile)) != -1) {
	    int va;

	    va = 1;
	    for (art = xfirst; art <= xlast; art++) {
		if (xoverinfo[art - xfirst].exists
			&& xoverinfo[art - xfirst].text) {
		    if (writes(wfd, xoverinfo[art - xfirst].text) == - 1
			|| writes(wfd, "\n") == -1) 
		    {
			ln_log(LNLOG_SERR, LNLOG_CGROUP,
			       "error: write() for .overview failed: %m");
			va = 0;
			break;
		    }
		}
	    }
	    if (fchmod(wfd, 0664)) va = 0;
	    if (fsync(wfd)) va = 0;
	    if (close(wfd)) va = 0;
	    if (va) {
		if (rename(newfile, ".overview")) {
		    if (unlink(newfile))
			ln_log(LNLOG_SERR, LNLOG_CGROUP,
			       "error: unlink(%s) failed: %m", newfile);
		    else
			ln_log(LNLOG_SERR, LNLOG_CGROUP,
			       "error: rename(%s/%s, .overview) failed: %m",
			       tt, newfile);
		} else {
		    if (debugmode)
			syslog(LOG_DEBUG, "wrote %s/.overview", tt);
		}
	    } else {
		unlink(newfile);
		/* the group must be newly empty: I want to keep the old
		   .overview file I think */
	    }
	} else {
	    ln_log(LNLOG_SERR, LNLOG_CGROUP,
		    "error: mkstemp of new .overview failed: %m");
	}
    }

    closedir(d);
    free(tt);
    if (overview) {
	free(overview);
    }
    return 1;
}

void
fixxover(void)
{
    DIR *d;
    struct dirent *de;
    char s[SIZE_s + 1];

    xsnprintf(s, SIZE_s, "%s/interesting.groups", spooldir);
    d = opendir(s);
    if (!d) {
	ln_log(LNLOG_SERR, LNLOG_CGROUP, "error: opendir %s: %m", s);
	return;
    }

    while ((de = readdir(d))) {
	if (isalnum((unsigned char)*(de->d_name)) && findgroup(de->d_name)) {
	    if (chdirgroup(de->d_name, FALSE))
		getxover();
	    freexover();
	}
    }
    closedir(d);
}


syntax highlighted by Code2HTML, v. 0.9.1