/*
 libutil -- deal with active file

 Written by Cornelius Krasel <krasel@wpxx02.toxi.uni-wuerzburg.de>.
 Copyright 2000.
 Reused some old code written by Arnt Gulbrandsen <agulbra@troll.no>,
 copyright 1995, modified by (amongst others) Cornelius Krasel
 <krasel@wpxx02.toxi.uni-wuerzburg.de>, Randolf Skerka
 <Randolf.Skerka@gmx.de>, Kent Robotti <robotti@erols.com> and
 Markus Enzenberger <enz@cip.physik.uni-muenchen.de>. Copyright
 for the modifications 1997-1999.

 Modified and copyright of the modifications by:
 2001 - 2002 Matthias Andree <matthias.andree@gmx.de>
 2002 Ralf Wildenhues <ralf.wildenhues@gmx.de>

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

#include "leafnode.h"
#include "activutil.h"
#include "ln_log.h"
#include "mastring.h"

#include <ctype.h>
#include "system.h"
#include <limits.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#ifdef CHECKGROUPORDER
#include "ln_log.h"

#endif /* CHECKGROUPORDER */
size_t activesize;
struct newsgroup *active = NULL;
size_t oldactivesize;
struct newsgroup *oldactive = NULL;

struct nglist {
    struct newsgroup *entry;
    struct nglist *next;
};

/* warning: this function does not do a deep copy: it does not copy
 * name or description */
void
newsgroup_copy(struct newsgroup *d, const struct newsgroup *s)
{
    d->first = s->first;
    d->last  = s->last;
    d->age   = s->age;
    d->name  = s->name;
    d->desc  = s->desc;
}

int
compactive(const void *a, const void *b)
{
    const struct newsgroup *la = (const struct newsgroup *)a;
    const struct newsgroup *lb = (const struct newsgroup *)b;

    return strcasecmp(la->name, lb->name);
}

static struct nglist *newgroup = NULL;

/*
 * insert a group into a list of groups
 * if oldactive is set, keep old data of known groups
 */
void
insertgroup(const char *name, long unsigned first,
	    long unsigned last, time_t age)
{
    struct nglist *l;
    static struct nglist *lold;
    struct newsgroup *g, *o;
    char *desc = NULL;

    g = findgroup(name);
    if (g)
	return;

    if (*name == '.' || strstr(name, "..") || name[strlen(name)-1] == '.') {
	ln_log(LNLOG_SWARNING, LNLOG_CTOP, "Warning: skipping group \"%s\", "
		"invalid name (NULL component).", name);
	return;
    }

    if (oldactivesize != 0 && oldactive != NULL) {
	    o = xfindgroup(oldactive, name, oldactivesize);
	    if (o) {
		    last = o->last;
		    first = o->first;
		    if (o->age) age = o->age;
		    if (o->desc) desc = critstrdup(o->desc, "insertgroup");
	    }
    }
    
    g = (struct newsgroup *)critmalloc(sizeof(struct newsgroup),
				       "Allocating space for new group");
    g->name = critstrdup(name, "insertgroup");
    g->first = first;
    g->last = last;
    g->age = age;
    g->desc = desc;
    l = (struct nglist *)critmalloc(sizeof(struct nglist),
				    "Allocating space for newsgroup list");
    l->entry = g;
    l->next = NULL;
    if (newgroup == NULL)
	newgroup = l;
    else
	lold->next = l;
    lold = l;
}

void
newgroupdesc(const char *groupname, const char *description)
{
    struct nglist *l = newgroup;

    while(l) {
	if (0 == strcasecmp(groupname, l->entry->name)) {
	    if (l->entry->desc)
		free(l->entry->desc);
	    l->entry->desc = critstrdup(description, "newgroupdesc");
	    break;
	}
	l = l->next;
    }
}


/*
 * change description of newsgroup
 */
void
changegroupdesc(const char *groupname, const char *description)
{
    struct newsgroup *ng;

    if (groupname && description) {
	ng = findgroup(groupname);
	if (ng) {
	    if (ng->desc)
		free(ng->desc);
	    ng->desc = critstrdup(description, "changegroupdesc");
	}
    }
}

/*
 * merge nglist with active group, then free it
 */
void
mergegroups(void)
{
    struct nglist *l, *la;
    size_t count = 0;

#ifdef CHECKGROUPORDER
    checkgrouporder();
#endif /* CHECKGROUPORDER */
    l = newgroup;
    while (l) {
	count++;
	l = l->next;
    }

    active = (struct newsgroup *)critrealloc((char *)active,
					     (1 + count +
					      activesize) *
					     sizeof(struct newsgroup),
					     "reallocating active");

    l = newgroup;
    count = activesize;
    while (l) {
	la = l;
	newsgroup_copy(active + count, l->entry);
	l = l->next;
	count++;
	free(la->entry);
	free(la);		/* clean up */
    }
    newgroup = NULL;
    active[count].name = NULL;

    activesize = count;
    qsort(active, activesize, sizeof(struct newsgroup), &compactive);
    validateactive();
}

#ifdef CHECKGROUPORDER
void checkgrouporder(void) {
    unsigned long i;
    int s = 1;

    for (i = 1; i < activesize; i++) {
	if (compactive(&active[i-1], &active[i]) >= 0) {
	    ln_log(LNLOG_SERR, LNLOG_CTOP, "in-core active file misorder at pos. %lu: \"%s\" vs. \"%s\"", i-1, active[i-1].name, active[i].name);
	    break;
	    s = 0;
	}
    }
}
#endif /* CHECKGROUPORDER */

/*
 * finds a group by name
 * expects the group list to be sorted in strcasecmp order
 * does a binary search, recursively implemented
 */
static long
helpfindgroup(struct newsgroup *act, const char *name, long low, long high)
{
    int result;
    long newp;

    if (low > high)
	return -1;
    newp = (high - low) / 2 + low;
    result = strcasecmp(name, act[newp].name);
    if (result == 0)
	return newp;
    else if (result < 0)
	return helpfindgroup(act, name, low, newp - 1);
    else
	return helpfindgroup(act, name, newp + 1, high);
}

/*
 * find a newsgroup in the active file
 */
struct newsgroup *
xfindgroup(struct newsgroup *act, const char *name, unsigned long actsize)
{
    long i;

    if (actsize > (unsigned long)LONG_MAX) {
	syslog(LOG_CRIT, "xfindgroup: count %lu too large (max. %ld), aborting",
		actsize, LONG_MAX);
	abort();
    }

    i = helpfindgroup(act, name, 0, actsize - 1);
    if (i < 0)
	return NULL;
    else
	return (&act[i]);
}

struct newsgroup *
findgroup(const char *name) {
#ifdef CHECKGROUPORDER
    checkgrouporder();
#endif /* CHECKGROUPORDER */
    return xfindgroup(active, name, activesize);
}

/* write active file, returns 0 for success, -1 for failure */
int
writeactive(void)
{
    FILE *a;
    struct newsgroup *g;
    mastr *c = mastr_new(PATH_MAX), *d;
    int err;
    size_t count = 0;

    mastr_vcat(c, spooldir, "/leaf.node/groupinfo.new", NULL);
    (void)unlink(mastr_str(c));
    a = fopen(mastr_str(c), "w");
    if (!a) {
	ln_log(LNLOG_SERR, LNLOG_CTOP, "cannot open new groupinfo file \"%s\": %m", mastr_str(c));
	mastr_delete(c);
	return -1;
    }

    /* count members in array and sort it */
    g = active;
    err = 0;
    if (g) {
	while (g->name) {
	    count++;
	    g++;
	}
	qsort(active, count, sizeof(struct newsgroup), &compactive);
	validateactive();

	g = active;
	while ((err != EOF) && g->name) {
	    if (strlen(g->name)) {
		err = fputs(g->name, a);
		if (err == EOF) break;
		if (fprintf(a, " %lu %lu %lu ", g->last, g->first,
			(unsigned long)g->age) < 0) {
		    err = EOF;
		    break;
		}
		if (err == EOF) break;
		err = fputs(g->desc && *(g->desc) ? g->desc : "-x-", a);
		if (err == EOF) break;
		err = fputs("\n", a);
		if (err == EOF) break;
	    }
	    g++;
	}
    }

    if (fflush(a) || fsync(fileno(a)) || fclose(a))
	    err = EOF;

    if (err == EOF) {
	ln_log(LNLOG_SERR, LNLOG_CTOP,
		"failed writing new groupinfo file \"%s\": %m", mastr_str(c));
	unlink(mastr_str(c));
	mastr_delete(c);
	return -1;
    }

    d = mastr_new(PATH_MAX);
    mastr_vcat(d, spooldir, "/leaf.node/groupinfo", NULL);
    if (rename(mastr_str(c), mastr_str(d))) {
	ln_log(LNLOG_SERR, LNLOG_CTOP, "failed to rename new groupinfo \"%s\" file into proper place \"%s\": %m", mastr_str(c), mastr_str(d));
	unlink(mastr_str(c));
	mastr_delete(d);
	mastr_delete(c);
	return -1;
    } else {
	if (verbose) printf("wrote active file with %lu line%s\n",
		(unsigned long)count, PLURAL(count));
	syslog(LOG_INFO, "wrote active file with %lu line%s",
	       (unsigned long)count, PLURAL(count));
    }
    mastr_delete(d);
    mastr_delete(c);
    return 0;
}

/*
 * free active list. Written by Lloyd Zusman
 */
void
freeactive(struct newsgroup *act)
{
    struct newsgroup *g;

    if (act == NULL)
	return;

    g = act;
    while (g->name) {
	free(g->name);
	if (g->desc)
	    free(g->desc);
	g++;
    }

    free(act);
}

/*
 * read active file into memory
 */
void
readactive(void)
{
    char *buf;
    /*@dependent@*/ char *p, *q, *r;
    unsigned long lu;
    off_t n;
    struct stat st;
    FILE *f;
    /*@dependent@*/ struct newsgroup *g;
    char s[SIZE_s+1];

    if (active) {
	freeactive(active);
	active = NULL;
    }

    xsnprintf(s, SIZE_s, "%s/leaf.node/groupinfo", spooldir);
    if ((f = fopen(s, "r")) != NULL) {
	    if (fstat(fileno(f), &st)) {
		    syslog(LOG_ERR, "can't stat %s: %m", s);
		    (void)fclose(f);
		    return;
	    }
	    if (!S_ISREG(st.st_mode)) {
		    syslog(LOG_ERR, "%s not a regular file", s);
		    (void)fclose(f);
		    return;
	    }
	    buf = critmalloc(st.st_size + 2, "Reading group info");
	    n = fread(buf, 1, st.st_size, f);
	    if ((off_t) n < st.st_size) {
		    syslog(LOG_ERR,
				    "Groupinfo file truncated while reading: %ld < %ld.",
				    (long)n, (long)st.st_size);
	    }
	    fclose(f);
    } else {
	    int e = errno;
	    syslog(LOG_ERR, "unable to open %s: %m", s);
	    if (e == ENOENT)
		return;
	    fprintf(stderr, "unable to open %s: %s, aborting", s, strerror(e));
	    exit(1);
    }

    n = ((off_t) n > st.st_size) ? st.st_size : (off_t) n;
    /* to read truncated groupinfo files correctly */
    buf[n] = '\n';
    buf[n + 1] = '\0';		/* 0-terminate string */

    /* delete spurious 0-bytes */
    while ((p = (char *)memchr(buf, '\0', st.st_size)) != NULL)
	*p = ' ';		/* \n might be better, but produces more errors */

    /* count lines = newsgroups */
    activesize = 0;
    p = buf;
    while (p && *p && ((q = (char *)memchr(p, '\n', st.st_size)) != NULL)) {
	activesize++;
	p = q + 1;
    }

    active = (struct newsgroup *)critmalloc((1 + activesize) *
					    sizeof(struct newsgroup),
					    "allocating active");
    g = active;

    p = buf;
    while (p && *p) {
	q = strchr(p, '\n');
	if (q) {
	    *q = '\0';
	    if (strlen(p) == 0) {
		p = q + 1;
		continue;	/* skip blank lines */
	    }
	}
	r = strchr(p, ' ');
	if (!q || !r) {
	    if (!q && r)
		*r = '\0';
	    else if (q && !r)
		*q = '\0';
	    else if (strlen(p) > 30) {
		q = p + 30;
		*q = '\0';
	    }
	    syslog(LOG_ERR,
		   "Groupinfo file possibly truncated or damaged: %s", p);
	    break;
	}
	*r++ = '\0';
	*q++ = '\0';

	g->name = critstrdup(p, "readactive");
	if (sscanf(r, "%lu %lu %lu", &g->last, &g->first, &lu) != 3) {
	    syslog(LOG_ERR,
		   "Groupinfo file possibly truncated or damaged: %s", p);
	    break;
	}
	g->age = lu;
	if (g->first == 0)
	    g->first = 1;	/* pseudoarticle */
	if (g->last == 0)
	    g->last = 1;
	p = r;
	for (n = 0; n < 3; n++) {	/* Skip the numbers */
	    p = strchr(r, ' ');
	    r = p + 1;
	}
	if (strcmp(r, "-x-") == 0)
	    g->desc = NULL;
	else
	    g->desc = critstrdup(r, "readactive");

	p = q;			/* next record */
	g++;
    }
    free(buf);
    /* last record, to mark end of array */
    g->name = NULL;
    g->first = 0;
    g->last = 0;
    g->age = 0;
    g->desc = NULL;

    /* count member in the array - maybe there were some empty lines */
    g = active;
    activesize = 0;
    while (g->name) {
	g++;
	activesize++;
    }

    /* sort the thing, just to be sure */
    qsort(active, activesize, sizeof(struct newsgroup), &compactive);
    validateactive();
}

/*
 * fake active file if it cannot be retrieved from the server
 */
void
fakeactive(void)
{
    DIR *d;
    struct dirent *de;
    DIR *ng;
    struct dirent *nga;
    long unsigned int i;
    long unsigned first, last;
    char *p;
    char s[SIZE_s+1];
    struct stat st;
    time_t age;

    killactiveread(); /* force reading active file regardless */
    xsnprintf(s, SIZE_s, "%s/interesting.groups", spooldir);
    d = opendir(s);
    if (!d) {
	syslog(LOG_ERR, "cannot open directory %s: %m", s);
	return;
    }

    while ((de = readdir(d))) {
	if (isalnum((unsigned char)*(de->d_name)) &&
	    chdirgroup(de->d_name, FALSE)) {
	    /* get first and last article from the directory. This is
	     * the most secure way to get to that information since the
	     * .overview files may be corrupt as well
	     * If the group doesn't exist, just ignore the active entry.
	     */

	    first = ULONG_MAX;
	    last = 0;

	    ng = opendir(".");
	    while ((nga = readdir(ng)) != NULL) {
		if (isdigit((unsigned char)*(nga->d_name))) {
		    p = NULL;
		    i = strtoul(nga->d_name, &p, 10);
		    if (*p == '\0') {
			if (i < first)
			    first = i;
			if (i > last)
			    last = i;
		    }
		}
	    }
	    if (first > last) {
		first = 1;
		last = 1;
	    }
	    closedir(ng);
	    if (debugmode)
		syslog(LOG_DEBUG, "parsed directory %s: first %lu, last %lu",
		       de->d_name, first, last);
	    if (0 == stat(".", &st))
		age = st.st_ctime;
	    else
		age = 0;
	    insertgroup(de->d_name, first, last, age);
	}
    }
    mergegroups();

    closedir(d);
}

char *
activeread(void)
{
    const char *a = "/active.read";
    size_t l;
    char *t = critmalloc((l = strlen(spooldir)) + strlen(a) + 1, "activeread");
    strcpy(t, spooldir); /* RATS: ignore */
    strcpy(t + l, a); /* RATS: ignore */
    return t;
}

/* Set a mark that the active file needs to be refetched (as though
 * fetchnews -f had been used) next time, by removing active.read file */
int
killactiveread(void)
{
    int rc = 0;
    char *t = activeread();
    if (unlink(t) && errno != ENOENT) {
	ln_log(LNLOG_SERR, LNLOG_CTOP,
		"cannot delete %s: %m", t);
	rc = -1;
    }
    free(t);
    return rc;
}


syntax highlighted by Code2HTML, v. 0.9.1