/*  $Revision: 1.18 $
**
**  Newsgroups and the active file.
*/
#include <stdio.h>
#include <sys/types.h>
#include "configdata.h"
#include "clibrary.h"
#include "nnrpd.h"
#include "mydir.h"
#include "../actived/protocol.h"
#include <sys/time.h>
#include <unistd.h>


/*
**  Newsgroup hashing stuff.  See comments in innd/ng.c.
*/

#define GRP_HASH(Name, p, j)	\
	for (p = Name, j = 0; *p; ) j = (j << 5) + j + *p++
#define GRP_SIZE	512
#define GRP_BUCKET(j)	&GRPtable[j & (GRP_SIZE - 1)]

typedef struct _GRPHASH {
    int		Size;
    int		Used;
    GROUPENTRY	**Groups;
} GRPHASH;


STATIC GRPHASH		GRPtable[GRP_SIZE];
STATIC GROUPENTRY	*GRPentries;
STATIC int		GRPbuckets;
STATIC int		GRPsize;
STATIC int		GRPactived = -1;
STATIC int		GRPuselocalhash = 0;
STATIC int		NRequestID;


void
NNewRequestID()
{
	static int pid = -1;
	static int count = 123456789;
	struct timeval tv;

	if (pid < 0) {
		pid = getpid();
	}
	gettimeofday(&tv, NULL);
	count += pid;
	NRequestID = tv.tv_sec ^ tv.tv_usec ^ pid ^ count;
}




/*
**  See if a given newsgroup exists.
*/
GROUPENTRY *
XGRPfind(group)
    register char		*group;
{
    register char		*p;
    register unsigned int	j;
    register int		i;
    register GROUPENTRY		**gpp;
    GRPHASH			*htp;
    char			c;

    /* SUPPRESS 6 *//* Over/underflow from plus expression */
    GRP_HASH(group, p, j);
    htp = GRP_BUCKET(j);
    for (c = *group, gpp = htp->Groups, i = htp->Used; --i >= 0; gpp++)
	if (c == gpp[0]->Name[0] && EQ(group, gpp[0]->Name))
	    return gpp[0];
    return NULL;
}



GROUPENTRY *
NGRPfind(group)
    register char		*group;
{
    int now = time(NULL);
    int expireat = now + ACTIVED_TIMEOUT;
    int last = now - 2;
    static struct wireprotocol buffer;
    fd_set fdset;
    struct timeval timeout;
    static GROUPENTRY	data;

    /* Okay.  Let's ask the server for the data. */
    NNewRequestID();
    while (now < expireat) {
	now = time(NULL);
	if (last < now - 1) {
	    last = now;
	    buffer.RequestID = NRequestID;
	    buffer.RequestType = REQ_FIND;
	    strncpy(buffer.Name, group, sizeof(buffer.Name) - 1);
	    buffer.Name[sizeof(buffer.Name) - 1] = '\0';

	    if (write_udp(GRPactived, (char *)&buffer, sizeof(buffer)) != sizeof(buffer)) {
		syslog(L_ERROR, "%s actived socket couldnt be written FIND %m",
		    ClientHost);
		sleep(1);
		continue;
	    }
	}
	FD_ZERO(&fdset);
	FD_SET(GRPactived, &fdset);
	timeout.tv_sec = 1;
	timeout.tv_usec = 0;
	if (select(GRPactived + 1, &fdset, NULL, NULL, &timeout) < 0) {
	    syslog(L_ERROR, "%s actived socket failed select %m",
		ClientHost);
	    sleep(1);
	    continue;
	}
	if (FD_ISSET(GRPactived, &fdset)) {
	    if (read_udp(GRPactived, (char *)&buffer, sizeof(buffer)) != sizeof(buffer)) {
	        syslog(L_ERROR, "%s actived socket couldnt be read FINDRESP %m",
		    ClientHost);
	        sleep(1);
	        continue;
	    }
	    if (buffer.RequestID != NRequestID) {
	        syslog(L_ERROR, "%s actived socket returned a different request-ID %d/%d",
		    ClientHost, buffer.RequestID, NRequestID);
	        sleep(1);
	        continue;
	    }
	    if (buffer.RequestType != REQ_FINDRESP) {
	        syslog(L_ERROR, "%s actived socket returned a non-FINDRESP %d",
		    ClientHost, buffer.RequestType);
	        sleep(1);
	        continue;
	    }

	    /* Was the request successful?  Did we find the group? */
	    if (! buffer.Success) {
		return(NULL);
	    }

	    /* Looks good! Copy all the data to the static "data" struct */
	    if (! buffer.NameNull) {
	        data.Name = buffer.Name;
	    } else {
		data.Name = NULL;
	    }
	    data.High = buffer.High;
	    data.Low = buffer.Low;
	    data.Flag = buffer.Flag;
	    if (! buffer.AliasNull) {
	        data.Alias = buffer.Alias;
	    } else {
		data.Alias = NULL;
	    }
	    return(&data);
        }
    }

    /* Something is very wrong.  Fall back to local access and whine like
       hell.  GRPuselocalhash is explicitly checked by GRPfind, and will
       handle initializing the database for us. */

    GRPuselocalhash++;
    return(NULL);
}


GROUPENTRY *
GRPfind(group)
    register char		*group;
{
    GROUPENTRY *rval;

    /*
     * If we are not flagged to use local hash, call NGRPfind.
     * That could potentially fail (server no answer, etc) in which
     * case we fall back to standard INN and scream bloody murder.
     * NGRPfind will toggle GRPuselocalhash to TRUE if it has problems.
     */

    if (! GRPuselocalhash) {
	rval = NGRPfind(group);
	if (! GRPuselocalhash) {
		return(rval);
	}

	/* Ow!  We are falling back to local access since NGRPfind failed! */
	XGetGroupList();
    }

    return(XGRPfind(group));
}


STATIC void
XGRPhash()
{
    register char		*p;
    register int		i;
    register GROUPENTRY		*gp;
    register unsigned int	j;
    register GRPHASH		*htp;

    /* Set up the default hash buckets. */
    GRPbuckets = GRPsize / GRP_SIZE;
    if (GRPbuckets == 0)
	GRPbuckets = 1;
    if (GRPtable[0].Groups)
	for (i = GRP_SIZE, htp = GRPtable; --i >= 0; htp++)
	    htp->Used = 0;
    else
	for (i = GRP_SIZE, htp = GRPtable; --i >= 0; htp++) {
	    htp->Size = GRPbuckets;
	    htp->Groups = NEW(GROUPENTRY*, htp->Size);
	    htp->Used = 0;
	}

    /* Now put all groups into the hash table. */
    for (i = GRPsize, gp = GRPentries; --i >= 0; gp++) {
	/* SUPPRESS 6 *//* Over/underflow from plus expression */
	GRP_HASH(gp->Name, p, j);
	htp = GRP_BUCKET(j);
	if (htp->Used >= htp->Size) {
	    htp->Size += GRPbuckets;
	    RENEW(htp->Groups, GROUPENTRY*, htp->Size);
	}
	htp->Groups[htp->Used++] = gp;
    }

    /* Note that we don't sort the buckets. */
}


/*
**  Read the active file into memory, sort it, and set the number of
**  newsgroups read in.  Return TRUE if okay, FALSE on error.
*/
BOOL
XGetGroupList()
{
    static char			*active;
    register char		*p;
    register char		*q;
    register GROUPENTRY		*gp;
    register int		i;

    syslog(L_ERROR, "%s NOT using actived",
	ClientHost);
    /* If re-scanning, free previous groups. */
    if (active != NULL) {
	DISPOSE(active);
	DISPOSE(GRPentries);
    }

    /* Get the new file. */
    active = ReadInFile(ACTIVE, (struct stat *)NULL);
    if (active == NULL) {
	syslog(L_ERROR, "%s cant read %s %m", ClientHost, ACTIVE);
	return FALSE;
    }

    /* Count lines. */
    for (p = active, i = 0; (p = strchr(p, '\n')) != NULL; p++, i++)
	continue;

    /* Fill in the group array. */
    GRPentries = NEW(GROUPENTRY, i);
    for (i = 0, gp = GRPentries, p = active; *p; i++, gp++, p = q + 1) {
	gp->Name = p;
	if ((p = strchr(p, ' ')) == NULL) {
	    syslog(L_ERROR, "%s internal no_space1 \"%.20s...\"",
		ClientHost, gp->Name);
	    return FALSE;
	}
	*p++ = '\0';

	/* Get the high mark. */
	if ((q = strchr(p, ' ')) == NULL) {
	    syslog(L_ERROR, "%s internal no_space2 \"%.20s...\"",
		ClientHost, gp->Name);
	    return FALSE;
	}
	*q++ = '\0';
	gp->High = atol(p);

	/* Get the low mark. */
	if ((p = strchr(q, ' ')) == NULL) {
	    syslog(L_ERROR, "%s internal no_space3 \"%.20s...\"",
		ClientHost, gp->Name);
	    return FALSE;
	}
	*p++ = '\0';
	gp->Low = atol(q);

	/* Kill the newline. */
	if ((q = strchr(p, '\n')) == NULL) {
	    syslog(L_ERROR, "%s internal newline \"%.20s...\"",
		ClientHost, gp->Name);
	    return FALSE;
	}
	*q = '\0';
	gp->Flag = *p;
	gp->Alias = gp->Flag == NF_FLAG_ALIAS ? p + 1 : NULL;
    }

    GRPsize = i;
    XGRPhash();
    return TRUE;
}

BOOL
NGetGroupList()
{
    int now = time(NULL);
    int expireat = now + ACTIVED_TIMEOUT;
    int last = now - 2;
    int s;
    struct wireprotocol buffer;
    fd_set fdset;
    struct timeval timeout;

    if (GRPactived < 0) {
        if ((s = create_udp_socket(0)) < 0) {
	    syslog(L_ERROR, "%s actived socket couldnt be created %m",
		ClientHost);
            return(FALSE);
        }
        if (connect_udp_socket(s, "localhost", 1119) < 0) {
	    syslog(L_ERROR, "%s actived socket couldnt be connected %m",
		ClientHost);
            return(FALSE);
        }
	if (fcntl(s, F_SETFL, O_NDELAY) < 0) {
	    syslog(L_ERROR, "%s actived socket couldnt be fcntl O_NDELAY %m",
		ClientHost);
            return(FALSE);
	}
	GRPactived = s;
    }

    /* Okay.  Let's ask the server if it's there. */
    NNewRequestID();
    while (now < expireat) {
	now = time(NULL);
	if (last < now - 1) {
	    last = now;
	    buffer.RequestID = NRequestID;
	    buffer.RequestType = REQ_AYT;

	    if (write_udp(GRPactived, (char *)&buffer, sizeof(buffer)) != sizeof(buffer)) {
		syslog(L_ERROR, "%s actived socket couldnt be written AYT %m",
		    ClientHost);
		sleep(1);
		continue;
	    }
	}
	FD_ZERO(&fdset);
	FD_SET(GRPactived, &fdset);
	timeout.tv_sec = 1;
	timeout.tv_usec = 0;
	if (select(GRPactived + 1, &fdset, NULL, NULL, &timeout) < 0) {
	    syslog(L_ERROR, "%s actived socket failed select %m",
		ClientHost);
	    sleep(1);
	    return(FALSE);
	}
	if (FD_ISSET(GRPactived, &fdset)) {
	    if (read_udp(GRPactived, (char *)&buffer, sizeof(buffer)) != sizeof(buffer)) {
	        syslog(L_ERROR, "%s actived socket couldnt be read AYTACK %m",
		    ClientHost);
	        sleep(1);
	        continue;
	    }
	    if (buffer.RequestID != NRequestID) {
	        syslog(L_ERROR, "%s actived socket returned a different request-ID %d/%d",
		    ClientHost, buffer.RequestID, NRequestID);
	        sleep(1);
	        continue;
	    }
	    if (buffer.RequestType != REQ_AYTACK) {
	        syslog(L_ERROR, "%s actived socket returned a non-AYTACK %d",
		    ClientHost, buffer.RequestType);
	        sleep(1);
	        continue;
	    }
	    /* Looks good! */
	    return(TRUE);
        }
    }
    return(FALSE);
}

BOOL
GetGroupList()
{
    /*
     * If we are not flagged to use local hash, call NGetGroupList.
     * That could potentially fail (server no answer, etc) in which
     * case we fall back to standard INN and scream bloody murder.
     */
    if (! GRPuselocalhash) {
	if (NGetGroupList() == FALSE) {
		GRPuselocalhash++;
		return(XGetGroupList());
	}
	return(TRUE);
    } else {
	return(XGetGroupList());
    }
}


/*
**  Sorting predicate to put newsgroup names into numeric order.
*/
STATIC int
ARTcompare(p1, p2)
    CPOINTER p1;
    CPOINTER p2;
{
    ARTNUM	*i1;
    ARTNUM	*i2;

    i1 = CAST(ARTNUM*, p1);
    i2 = CAST(ARTNUM*, p2);
    return *i1 - *i2;
}


/*
**  Fill in ARTnumbers with the numbers of the articles in the current
**  group.
*/
STATIC void
GRPscandir(dir)
    char		*dir;
{
    static char		SPOOL[] = _PATH_SPOOL;
    static int		ARTarraysize;
    register DIRENTRY	*ep;
    register DIR	*dp;
    register char	*p;
    register ARTNUM	i;

    /* Go to the directory. */
    if (chdir(SPOOL) < 0) {
	syslog(L_FATAL, "%s cant cd %s %m", ClientHost, SPOOL);
	ExitWithStats(1);
    }

    if (ARTarraysize == 0) {
	ARTarraysize = 1024;
	ARTnumbers = NEW(ARTNUM, ARTarraysize);
    }

    /* The newsgroup directory might not exist; treat it as empty. */
    ARTsize = 0;
    GRPcount++;
    if (chdir(dir) < 0)
	return;
    dp = opendir(".");
    if (dp == NULL) {
	syslog(L_ERROR, "%s cant opendir %s %m", ClientHost, dir);
	return;
    }

    while ((ep = readdir(dp)) != NULL) {
	/* Get the numeric value of the filename, if it's all digits. */
	for (p = ep->d_name, i = 0; *p; p++) {
	    if (!CTYPE(isdigit, *p))
		break;
	    i = i * 10 + *p - '0';
	}
	if (*p || i == 0)
	    continue;

	if (ARTsize + 1 >= ARTarraysize) {
	    ARTarraysize += 1024;
	    RENEW(ARTnumbers, ARTNUM, ARTarraysize);
	}

	ARTnumbers[ARTsize++] = i;
    }
    (void)closedir(dp);

    ARTcache = NULL;
    qsort((POINTER)ARTnumbers, (SIZE_T)ARTsize, sizeof ARTnumbers[0],
	ARTcompare);
}


/*
**  Change to or list the specified newsgroup.  If invalid, stay in the old
**  group.
*/
FUNCTYPE
CMDgroup(ac, av)
    int			ac;
    char		*av[];
{
    static time_t	last_time;
    static char		NOSUCHGROUP[] = NNTP_NOSUCHGROUP;
    register char	*p;
    register int	i;
    time_t		now;
    char		*grplist[2];
    char		*group;
    char		buff[SPOOLNAMEBUFF];

    if (!PERMcanread) {
	Reply("%s\r\n", NOACCESS);
	return;
    }

    /* Parse arguments. */
    if (ac == 1) {
	if (GRPcount == 0) {
	    Printf("%d No group specified\r\n", NNTP_XGTITLE_BAD);
	    return;
	}
	(void)strcpy(buff, GRPlast);
	for (p = buff; *p; p++)
	    if (*p == '/')
		*p = '.';
	group = buff;
    }
    else
	group = av[1];
    if (GRPfind(group) == NULL) {
	Reply("%s %s\r\n", NOSUCHGROUP,group);
	return;
    }

    /* If permission is denied, pretend group doesn't exist. */
    if (PERMspecified) {
	grplist[0] = group;
	grplist[1] = NULL;
	if (!PERMmatch(PERMdefault, PERMlist, grplist)) {
	    Reply("%s\r\n", NOSUCHGROUP);
	    return;
	}
    }
    else if (!PERMdefault) {
	Reply("%s\r\n", NOSUCHGROUP);
	return;
    }

    /* Close out any existing article, report group stats. */
    ARTclose();
    ARTindex = 0;
    GRPreport();

    /* Make the group name a directory name. */
    (void)strcpy(buff, group);
    for (p = buff; *p; p++)
	if (*p == '.')
	    *p = '/';

    /* If we haven't been in the group recently, rescan. */
    (void)time(&now);
    if (!EQ(buff, GRPlast) || now > last_time + NNRP_RESCAN_DELAY) {
	GRPscandir(buff);
	(void)strcpy(GRPlast, buff);
	last_time = now;
    }

    /* Close down any overview file. */
    OVERclose();

    /* Doing a "group" command? */
    if (caseEQ(av[0], "group")) {
	if (ARTsize == 0)
	    Reply("%d 0 0 0 %s\r\n", NNTP_GROUPOK_VAL, group);
	else
	    Reply("%d %d %ld %ld %s\r\n",
		NNTP_GROUPOK_VAL,
		ARTsize, ARTnumbers[0], ARTnumbers[ARTsize - 1], group);
    }
    else {
	/* Must be doing a "listgroup" command. */
	Reply("%d Article list follows\r\n", NNTP_GROUPOK_VAL);
	for (i = 0; i < ARTsize; i++)
	    Printf("%ld\r\n", ARTnumbers[i]);
	Printf(".\r\n");
    }
}


/*
**  Report on the number of articles read in the group, and clear the count.
*/
void
GRPreport()
{
    register char	*p;
    char		buff[SPOOLNAMEBUFF];

    if (GRPlast[0] && GRParticles != 0) {
	(void)strcpy(buff, GRPlast);
	for (p = buff; *p; p++)
	    if (*p == '/')
		*p = '.';
	syslog(L_NOTICE, "%s group %s %ld", ClientHost, buff, GRParticles);
	GRParticles = 0;
    }
}


/*
**  Used by ANU-News clients.
*/
FUNCTYPE
CMDxgtitle(ac, av)
    int			ac;
    char		*av[];
{
    register QIOSTATE	*qp;
    register char	*line;
    register char	*p;
    register char	*q;
    char		save;

    /* Parse the arguments. */
    if (ac == 1) {
	if (GRPcount == 0) {
	    Printf("%d No group specified\r\n", NNTP_XGTITLE_BAD);
	    return;
	}
	p = GRPlast;
    }
    else
	p = av[1];

    /* Open the file, get ready to scan. */
    if ((qp = QIOopen(NEWSGROUPS, QIO_BUFFER)) == NULL) {
	syslog(L_ERROR, "%s cant open %s %m", ClientHost, NEWSGROUPS);
	Printf("%d Can't open %s\r\n", NNTP_XGTITLE_BAD, NEWSGROUPS);
	return;
    }
    Printf("%d list follows\r\n", NNTP_XGTITLE_OK);

    /* Print all lines with matching newsgroup name. */
    while ((line = QIOread(qp)) != NULL) {
	for (q = line; *q && !ISWHITE(*q); q++)
	    continue;
	save = *q;
	*q = '\0';
	if (wildmat(line, p)) {
	    *q = save;
	    Printf("%s\r\n", line);
	}
    }

    /* Done. */
    QIOclose(qp);
    Printf(".\r\n");
}
