
/*
 * NEWSPRUNE.C		Matthew Dillon,  dillon@backplane.com
 *					http://www.backplane.com/
 *
 *			For the public domain
 *
 * NEWSPRUNE, version 2.03
 *
 * Prune the news hierarchy by scanning the history file for files
 * to keep.  No file < 8 hours old will be deleted.  We then
 * scan the news spool WITHOUT having to read each file.  This
 * goes a whole lot faster then scanning the news spool and
 * opening and reading each file.
 *
 * files to DELETE are sent to the standard output.  newsprune itself
 * does not actually delete the files.  Normally you pipe the output
 * to fastrm, or to a file and then run fastrm on the file (to give you
 * a temporary log of what got removed).
 */

#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <dirent.h>

#define HSIZE	32
#define HMASK	(HSIZE-1)

typedef struct Range {
    struct Range *ra_Next;
    int ra_Beg;
    int ra_End;		/* exclusive	*/
} Range;

typedef struct Group {
    struct Group *gr_Next;
    struct Group *gr_Parent;
    struct Group *gr_Hash[HSIZE];
    Range	*gr_Range;
    char	*gr_Name;
    short	gr_Offset;
    short	gr_Len;
    short	gr_HasChildren;
} Group;

Group *GHash[HSIZE];

#ifndef ACTIVE
#define ACTIVE  "/home/news/active"
#endif
#ifndef HISTORY
#define HISTORY	"/home/news/history"
#endif
#ifndef SPOOL
#define SPOOL	"/home/news/spool/news"
#endif

#ifdef NODEBUG
#define dbprintf(n, args)
#else
#define dbprintf(n, args) if (DebugOpt >= n) zprintf args
#endif

void fatal(const char *ctl, ...);
void zprintf(const char *ctl, ...);
void ScanActive(void);
void ScanHistory(void);
void ScanSpool(Group **);
void ScanSpoolDir(Group *grp);
Group **FindHash(Group **hash, const char *dname);
void AddGroupHash(Group **hash, char *grp, int artNo);
void AddArticle(Group *g, int artNo);
int ArticleInRange(Group *g, int artNo);

int DebugOpt;
int NotRoot = 1;

int
main(int ac, char **av)
{
    int i;

    if (getuid() == 0)
	NotRoot = 0;

    for (i = 1; i < ac; ++i) {
	char *ptr = av[i];
	if (*ptr != '-') {
	    fatal(NULL);
	}
	ptr += 2;
	switch(ptr[-1]) {
	case 'd':
	    if (*ptr)
		DebugOpt = strtol(ptr, NULL, 0);
	    else
		++DebugOpt;
	    break;
	default:
	    fatal("Unknown option: %s\n", ptr - 2);
	}
    }
    dbprintf(1, ("ScanActive:\n"));
    ScanActive();
    dbprintf(1, ("ScanHistory:\n"));
    ScanHistory();
    dbprintf(1, ("ScanSpool:\n"));
    ScanSpool(GHash);
    return(0);
}

void 
fatal(const char *ctl, ...)
{
    if (ctl) {
	va_list va;
	va_start(va, ctl);
	vfprintf(stderr, ctl, va);
	va_end(va);
	exit(1);
    } else {
	puts("newsprune [-d] >expire.rm");
	exit(0);
    }
}

void
zprintf(const char *ctl, ...)
{
    va_list va;

    va_start(va, ctl);
    vfprintf(stderr, ctl, va);
    va_end(va);
}

void
ScanActive(void)
{
    FILE *fi = fopen(ACTIVE, "r");
    char buf[1024];

    if (fi == NULL) {
	perror("fopen(" ACTIVE ")");
	exit(1);
    }
    while (fgets(buf, sizeof(buf), fi) != NULL) {
	char *p1;

	if ((p1 = strchr(buf, ' ')) != NULL) {
	    *p1 = 0;
	    AddGroupHash(GHash, buf, -1);
	}
    }
    fclose(fi);
}

void
ScanHistory(void)
{
    FILE *fi = fopen(HISTORY, "r");
    char buf[4096];
    int cnt = 0;

    if (fi == NULL) {
	perror("fopen(" HISTORY ")");
	exit(1);
    }
    while (fgets(buf, sizeof(buf), fi) != NULL) {
	char *p1;
	char *p2;
	if ((p1 = strchr(buf, '\t')) != NULL) {
	    if ((p1 = strchr(p1 + 1, '\t')) != NULL) {
		if ((p2 = strchr(p1 + 1, '\t')) != NULL)
		    *p2 = 0;
		for (p1 = strtok(p1 + 1, " \r\n");
		     p1;
		     p1 = strtok(NULL, " \r\n")
		) {
		    if ((p2 = strchr(p1, '/')) != NULL) {
			*p2 = 0;
			AddGroupHash(GHash, p1, strtol(p2 + 1, NULL, 0));
			if (++cnt % 10000 == 0) {
			    dbprintf(1, ("%7d articles\n", cnt));
			}
		    }
		}
	    }
	}
    }
    fclose(fi);
}

void
ScanSpool(Group **ghash)
{
    int i;

    for (i = 0; i < HSIZE; ++i) {
	Group *grp;
	for (grp = ghash[i]; grp; grp = grp->gr_Next) {
	    ScanSpoolDir(grp);
	    if (grp->gr_HasChildren)
		ScanSpool(grp->gr_Hash);
	}
    }
}

void
ScanSpoolDir(Group *grp)
{
    char path[1024];
    short len = grp->gr_Offset + grp->gr_Len;
    time_t t = time(NULL);
    DIR *dir;
    int cnt = 0;
    int rem = 0;

    {
	Group *g;
	for (g = grp; g; g = g->gr_Parent) {
	    bcopy(g->gr_Name, path + g->gr_Offset + sizeof(SPOOL), g->gr_Len);
	    path[g->gr_Offset + g->gr_Len + sizeof(SPOOL)] = '/';
	}
	path[len+sizeof(SPOOL)] = 0;
	bcopy(SPOOL, path, sizeof(SPOOL)-1);
	path[sizeof(SPOOL)-1] = '/';
    }
    if (chdir(path) == 0) {
	if ((dir = opendir(".")) != NULL) {
	    struct dirent *den;
	    while ((den = readdir(dir)) != NULL) {
		if (den->d_name[0] < '0' || den->d_name[0] > '9')
		    continue;
		++cnt;
		if (*FindHash(grp->gr_Hash, den->d_name) == NULL) {
		    if (ArticleInRange(grp, strtol(den->d_name, NULL, 0)) < 0) {
			struct stat st;
			++rem;

			/*
			 * The article is a candidate for removal.  If it is not
			 * a directory and it is over 8 hours old (to prevent
			 * new news from being misinterpreted as obsolete news),
			 * then the file is output to stdout.
			 */
			if (lstat(den->d_name, &st) == 0 && !S_ISDIR(st.st_mode)) {
			    long dt = t - st.st_mtime;
			    if (dt > 60 * 60 * 8)
				printf("%s/%s\n", path, den->d_name);
			}
		    }
		}
	    }
	    closedir(dir);
	}
    }
    if (DebugOpt == 1 && rem != 0)
	dbprintf(1, (" Scanning %s\t%d/%d\n", path, rem, cnt));
    else
	dbprintf(2, (" Scanning %s\t%d/%d\n", path, rem, cnt));
}

int
hfunc(const char *p)
{
    int hv = 0xAF54B82C;
    while (*p) {
	hv = (hv << 5) ^ *p ^ (hv >> 23);
	++p;
    }
    hv ^= (hv >> 16);
    return(hv & HMASK);
}

Group **
FindHash(Group **hash, const char *dname)
{
    Group **pg;
    Group *g;

    for (pg = &hash[hfunc(dname)]; (g = *pg) != NULL; pg = &g->gr_Next) {
	if (strcmp(g->gr_Name, dname) == 0)
	    break;
    }
    return(pg);
}

void
AddGroupHash(Group **hash, char *grp, int artNo)
{
    char *p1;
    Group *par = NULL;
    long offset = 0;

    while ((p1 = strchr(grp, '.')) != NULL) {
	Group **pg;
	Group *g;

	*p1 = 0;
	pg = FindHash(hash, grp);
	if ((g = *pg) == NULL) {
	    *pg = g = malloc(sizeof(Group) + strlen(grp) + 1);
	    bzero(g, sizeof(*g));
	    g->gr_Parent = par;
	    g->gr_Name = (char *)(g + 1);
	    g->gr_Offset = offset;
	    strcpy(g->gr_Name, grp);
	    g->gr_Len = strlen(g->gr_Name);
	    g->gr_HasChildren = 1;
	    dbprintf(8, ("new group: %s\n", g->gr_Name));
	}
	grp = p1 + 1;
	hash = (*pg)->gr_Hash;
	par = g;
	offset = g->gr_Offset + g->gr_Len + 1;
    }
    {
	Group **pg;
	Group *g;

	pg = FindHash(hash, grp);
	if ((g = *pg) == NULL) {
	    *pg = g = malloc(sizeof(Group) + strlen(grp) + 1);
	    bzero(g, sizeof(*g));
	    g->gr_Parent = par;
	    g->gr_Name = (char *)(g + 1);
	    g->gr_Offset = offset;
	    strcpy(g->gr_Name, grp);
	    g->gr_Len = strlen(g->gr_Name);
	    g->gr_HasChildren = 1;
	    dbprintf(8, ("new group: %s\n", g->gr_Name));
	}

	/*
	 * Add artNo
	 */
	if (artNo >= 0) {
	    AddArticle(g, artNo);
	}
    }
}

void
AddArticle(Group *g, int artNo)
{
    static Range *RFree;
    Range *prev;
    Range *r;

    for (prev = NULL, r = g->gr_Range; r; (prev = r), r = r->ra_Next) {
	if (artNo < r->ra_Beg)
	    break;
	if (artNo < r->ra_End)
	    return;
    }
    /*
     * Insert record between previous and current
     */
    if (r && artNo == r->ra_Beg - 1) {
	/*
	 * extend current record back one
	 */
	--r->ra_Beg;
	if (prev && prev->ra_End == r->ra_Beg) {
	    /*
	     * Combine previous and current record
	     * together
	     */
	    prev->ra_End = r->ra_End;
	    prev->ra_Next = r->ra_Next;
	    r->ra_Next = RFree;
	    RFree = r;
	    dbprintf(8, ("ext/comb record: %s %d-%d\n", g->gr_Name, prev->ra_Beg, prev->ra_End));
	} else {
	    dbprintf(8, ("ext record: %s %d-%d\n", g->gr_Name, r->ra_Beg, r->ra_End));
	}
    } else if (prev && artNo == prev->ra_End) {
	/*
	 * extend previous record forward one
	 */
	++prev->ra_End;
	dbprintf(8, ("ext record: %s %d-%d\n", g->gr_Name, prev->ra_Beg, prev->ra_End));
    } else {
	/*
	 * insert new record between previous and r
	 */
	Range *rnew;

	dbprintf(8, ("new record: %s %d\n", g->gr_Name, artNo));
	if (RFree) {
	    rnew = RFree;
	    RFree = rnew->ra_Next;
	} else {
	    rnew = malloc(sizeof(Range));
	}
	rnew->ra_Next = r;
	rnew->ra_Beg = artNo;
	rnew->ra_End = artNo + 1;
	if (prev)
	    prev->ra_Next = rnew;
	else
	    g->gr_Range = rnew;
    }
}

int
ArticleInRange(Group *g, int artNo)
{
    Range *r;

    for (r = g->gr_Range; r; r = r->ra_Next) {
	if (artNo < r->ra_Beg)
	    return(-1);
	if (artNo < r->ra_End)
	    return(0);
    }
    return(-1);
}

