/* Copyright (C) 2000-1 drscholl@users.sourceforge.net
   This is free software distributed under the terms of the
   GNU Public License.

   $Id: add_file.c,v 1.93 2001/02/23 23:38:48 drscholl Exp $ */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <limits.h>
#include "opennap.h"
#include "debug.h"

#ifndef ROUTING_ONLY

/* allowed bitrates for MPEG V1/V2 Layer III */
const int BitRate[18] =
    { 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 192, 224,
    256, 320
};

/* allowed sample rates for MPEG V2/3 */
const int SampleRate[6] = { 16000, 24000, 22050, 32000, 44100, 48000 };

#if DEBUG
int
validate_flist (FLIST * p)
{
    ASSERT_RETURN_IF_FAIL (VALID_LEN (p, sizeof (FLIST)), 0);
    ASSERT_RETURN_IF_FAIL (p->magic == MAGIC_FLIST, 0);
    ASSERT_RETURN_IF_FAIL (VALID_LEN (p->key, strlen (p->key) + 1), 0);
    ASSERT_RETURN_IF_FAIL (p->count == list_count (p->list), 0);
    return 1;
}
#endif

static void
fdb_add (HASH * table, char *key, DATUM * d, int track_key)
{
    FLIST  *files;
    LIST   *list;

    ASSERT (table != 0);
    ASSERT (key != 0);
    ASSERT (d != 0);
    files = hash_lookup (table, key);
    /* if there is no entry for this particular word, create one now */
    if (!files)
    {
	files = CALLOC (1, sizeof (FLIST));
	if (!files)
	{
	    OUTOFMEMORY ("fdb_add");
	    return;
	}
#if DEBUG
	files->magic = MAGIC_FLIST;
#endif
	files->key = STRDUP (key);
	if (!files->key)
	{
	    OUTOFMEMORY ("fdb_add");
	    FREE (files);
	    return;
	}
	if (hash_add (table, files->key, files))
	{
	    FREE (files->key);
	    FREE (files);
	    return;
	}
    }
    ASSERT (validate_flist (files));

    list = CALLOC (1, sizeof (LIST));
    if (!list)
    {
	OUTOFMEMORY ("list");
	if (!files->list)
	    hash_remove (table, files->key);
	return;
    }
    list->data = d;
    /* order is not important, push to beginning of list for speed */
    files->list = list_push (files->list, list);
    files->count++;

    /* this is conditional since we don't keep track of the key when
     * inserting the hash value for the RESUME function.
     */
    if (track_key)
    {
	/* keep a list of search tokens that match this file */
	list = CALLOC (1, sizeof (LIST));
	if (!list)
	{
	    /* this is bad.  we've already committed a lot of things, just
	     * bail out as best as possible.
	     */
	    OUTOFMEMORY ("fdb_add");
	    return;
	}
	list->data = files;
	d->tokens = list_push (d->tokens, list);
    }
}

/* common code for inserting a file into the various hash tables */
static void
insert_datum (DATUM * info, char *av)
{
    LIST   *tokens, *ptr;
    unsigned int fsize;

    ASSERT (info != 0);
    ASSERT (av != 0);

    if (!info->user->con->uopt->files)
    {
	/* create the hash table */
	info->user->con->uopt->files =
	    hash_init (257, (hash_destroy) free_datum);
	if (!info->user->con->uopt->files)
	{
	    OUTOFMEMORY ("insert_datum");
	    return;
	}
    }

    hash_add (info->user->con->uopt->files, info->filename, info);

    /* split the filename into words */
    tokens = tokenize (av, NULL);

    /* the filename may not consist of any searchable words, in which
     * case its not entered into the index.  this file will only be seen
     * when browsing the user possessing it
     */
    for (ptr = tokens; ptr; ptr = ptr->next)
	fdb_add (File_Table, ptr->data, info, 1);

    list_free (tokens, 0);

#if RESUME
    /* index by md5 hash */
    fdb_add (MD5, info->hash, info, 0);
#endif

    fsize = info->size / 1024;	/* in kbytes */
    info->user->shared++;
    info->user->libsize += fsize;
    Num_Gigs += fsize;		/* this is actually kB, not gB */
    Num_Files++;
    Local_Files++;
    info->user->sharing = 1;	/* note that we began sharing */
}

static DATUM *
new_datum (char *filename, char *hash)
{
    DATUM  *info = CALLOC (1, sizeof (DATUM));

    (void) hash;
    if (!info)
    {
	OUTOFMEMORY ("new_datum");
	return 0;
    }
    info->filename = STRDUP (filename);
    if (!info->filename)
    {
	OUTOFMEMORY ("new_datum");
	FREE (info);
	return 0;
    }
#if RESUME
    info->hash = STRDUP (hash);
    if (!info->hash)
    {
	OUTOFMEMORY ("new_datum");
	FREE (info->filename);
	FREE (info);
	return 0;
    }
#endif
    return info;
}

static int
bitrateToMask (int bitrate, USER * user)
{
    unsigned int i;

    for (i = 0; i < sizeof (BitRate) / sizeof (int); i++)

    {
	if (bitrate <= BitRate[i])
	    return i;
    }
    log ("bitrateToMask(): invalid bit rate %d (%s, \"%s\")", bitrate,
	 user->nick, user->clientinfo);
    return 0;			/* invalid bitrate */
}

static int
freqToMask (int freq, USER * user)
{
    unsigned int i;
    for (i = 0; i < sizeof (SampleRate) / sizeof (int); i++)

    {
	if (freq <= SampleRate[i])
	    return i;
    }
    log ("freqToMask(): invalid sample rate %d (%s, \"%s\")", freq,
	 user->nick, user->clientinfo);
    return 0;
}

/* 100 "<filename>" <md5sum> <size> <bitrate> <frequency> <time>
   client adding file to the shared database */
HANDLER (add_file)
{
    char   *av[6];
    DATUM  *info;
    unsigned int fsize;

    (void) tag;
    (void) len;
    ASSERT (validate_connection (con));

    CHECK_USER_CLASS ("add_file");

    ASSERT (validate_user (con->user));

    if (!option(ON_ALLOW_SHARE))
	return;

    if (Max_Shared && con->user->shared > Max_Shared)
    {
	send_cmd (con, MSG_SERVER_NOSUCH,
		  "You may only share %d files", Max_Shared);
	return;
    }

    if (split_line (av, sizeof (av) / sizeof (char *), pkt) != 6)
    {
	unparsable (con);
	return;
    }

    if (is_blocked (av[0]))
	return;

    if (av[1] - av[0] > _POSIX_PATH_MAX + 2)
    {
	send_cmd (con, MSG_SERVER_NOSUCH, "filename too long");
	return;
    }

    /* ensure we have a valid byte count */
    fsize = strtoul (av[2], 0, 10);
    /* check for overflow */
    if (con->user->libsize + fsize < con->user->libsize)
    {
	log ("add_file(): %u byte file would overflow %s's library size",
	     fsize, con->user->nick);
	return;
    }

    /* make sure this isn't a duplicate - only compare the basename, not
     * including the directory component
     */
    if (con->uopt->files && hash_lookup (con->uopt->files, av[0]))
    {
	send_cmd (con, MSG_SERVER_NOSUCH, "duplicate file");
	return;
    }

    /* create the db record for this file */
    if (!(info = new_datum (av[0], av[1])))
	return;
    info->user = con->user;
    info->size = fsize;
    info->bitrate = bitrateToMask (atoi (av[3]), con->user);
    info->frequency = freqToMask (atoi (av[4]), con->user);
    info->duration = atoi (av[5]);
    info->type = CT_MP3;

    insert_datum (info, av[0]);
}

char   *Content_Types[] = {
    "mp3",			/* not a real type, but what we use for audio/mp3 */
    "audio",
    "video",
    "application",
    "image",
    "text"
};

/* 10300 "<filename>" <size> <hash> <content-type> */
HANDLER (share_file)
{
    char   *av[4];
    DATUM  *info;
    int     i, type;
    unsigned int fsize;

    (void) len;
    (void) tag;

    ASSERT (validate_connection (con));
    CHECK_USER_CLASS ("share_file");

    if (!option(ON_ALLOW_SHARE))
	return;
    if (Max_Shared && con->user->shared > Max_Shared)
    {
	log ("add_file(): %s is already sharing %d files", con->user->nick,
	     con->user->shared);
	if (ISUSER (con))
	    send_cmd (con, MSG_SERVER_NOSUCH,
		      "You may only share %d files", Max_Shared);
	return;
    }

    if (split_line (av, sizeof (av) / sizeof (char *), pkt) != 4)
    {
	unparsable (con);
	return;
    }

    if (is_blocked (av[0]))
	return;

    /* make sure the content-type looks correct */
    type = -1;
    for (i = CT_AUDIO; i < CT_UNKNOWN; i++)
    {
	if (!strcasecmp (Content_Types[i], av[3]))
	{
	    type = i;
	    break;
	}
    }
    if (type == -1)
    {
	log ("share_file(): not a valid type: %s", av[3]);
	if (ISUSER (con) == CLASS_USER)
	    send_cmd (con, MSG_SERVER_NOSUCH, "%s is not a valid type",
		      av[3]);
	return;
    }

    if (av[1] - av[0] > _POSIX_PATH_MAX + 2)
    {
	send_cmd (con, MSG_SERVER_NOSUCH, "filename too long");
	return;
    }

    if (con->uopt->files && hash_lookup (con->uopt->files, av[0]))
    {
	send_cmd (con, MSG_SERVER_NOSUCH, "duplicate file");
	return;
    }

    fsize = strtoul (av[1], 0, 10);
    if (fsize + con->user->libsize < con->user->libsize)
    {
	log ("share_file(): %u byte file would overflow %s's library size",
	     fsize, con->user->nick);
	return;
    }

    if (!(info = new_datum (av[0], av[2])))
	return;
    info->user = con->user;
    info->size = fsize;
    info->type = type;

    insert_datum (info, av[0]);
}

/* 870 "<directory>" "<basename>" <md5> <size> <bitrate> <freq> <duration> [ ... ]
   client command to add multiple files in the same directory */
HANDLER (add_directory)
{
    char   *dir, *basename, *md5, *size, *bitrate, *freq, *duration;
    char    path[_POSIX_PATH_MAX], dirbuf[_POSIX_PATH_MAX];
    int     pathlen, fsize;
    DATUM  *info;

    (void) tag;
    (void) len;
    ASSERT (validate_connection (con));
    CHECK_USER_CLASS ("add_directory");
    if (!option(ON_ALLOW_SHARE))
	return;
    dir = next_arg (&pkt);	/* directory */
    if (!dir)
    {
	log ("add_directory(): missing directory component");
	return;
    }
    pathlen = strlen (dir);
    if ((size_t) pathlen >= sizeof (dirbuf) - 1)
    {
	log ("add_directory(): directory component is too long, ignoring");
	return;
    }
    ASSERT ((size_t) pathlen < sizeof (dirbuf) - 1);
    dirbuf[sizeof (dirbuf) - 1] = 0;	/* ensure nul termination */
    strncpy (dirbuf, dir, sizeof (dirbuf) - 1);

    if (pathlen > 0 && dirbuf[pathlen - 1] != '\\')
    {
	dirbuf[pathlen++] = '\\';
	dirbuf[pathlen] = 0;
	if ((size_t) pathlen >= sizeof (dirbuf) - 1)
	{
	    ASSERT ((size_t) pathlen < sizeof (dirbuf));
	    log
		("add_directory(): directory component is too long, ignoring");
	    return;
	}
    }

    /* if the client passes a dir + file that is longer than 255 chars,
     * strncpy() won't write a \0 at the end of the string, so ensure that
     * this always happens
     */
    path[sizeof (path) - 1] = 0;

    while (pkt)
    {
	if (Max_Shared && con->user->shared > Max_Shared)
	{
	    send_cmd (con, MSG_SERVER_NOSUCH,
		      "You may only share %d files", Max_Shared);
	    return;
	}
	basename = next_arg (&pkt);
	md5 = next_arg (&pkt);
	size = next_arg (&pkt);
	bitrate = next_arg (&pkt);
	freq = next_arg (&pkt);
	duration = next_arg (&pkt);
	if (!basename || !md5 || !size || !bitrate || !freq || !duration)
	{
	    unparsable (con);
	    return;
	}

	strncpy (path, dirbuf, sizeof (path) - 1);
	strncpy (path + pathlen, basename, sizeof (path) - 1 - pathlen);

	ASSERT (path[sizeof (path) - 1] == 0);

	/* TODO: still seeing crashes here, we must be overwriting the
	 * stack on occasion.  quit now if we detect this condition
	 */
	if (path[sizeof (path) - 1] != 0)
	{
	    log ("add_directory: ERROR! buffer overflow detected");
	    return;
	}

	if (is_blocked (path))
	    continue;

	if (con->uopt->files && hash_lookup (con->uopt->files, path))
	{
	    send_cmd (con, MSG_SERVER_NOSUCH, "Duplicate file");
	    continue;		/* get next file */
	}

	fsize = atoi (size);
	if (fsize < 1)
	{
	    send_cmd (con, MSG_SERVER_NOSUCH, "invalid size");
	    continue;
	}

	/* create the db record for this file */
	if (!(info = new_datum (path, md5)))
	    return;
	info->user = con->user;
	info->size = fsize;
	info->bitrate = bitrateToMask (atoi (bitrate), con->user);
	info->frequency = freqToMask (atoi (freq), con->user);
	info->duration = atoi (duration);
	info->type = CT_MP3;

	insert_datum (info, path);
    }
}

#endif /* ! ROUTING_ONLY */

/* 10012 <nick> <shared> <size>
   remote server is notifying us that one of its users is sharing files */
HANDLER (user_sharing)
{
    char   *av[3];
    USER   *user;
    int     n;
    double  libsize;
    int     err = 0;

    (void) len;
    ASSERT (validate_connection (con));
    CHECK_SERVER_CLASS ("user_sharing");
    if (split_line (av, sizeof (av) / sizeof (char *), pkt) != 3)
    {
	log ("user_sharing: wrong number of arguments");
	return;
    }
    user = hash_lookup (Users, av[0]);
    if (!user)
    {
	log ("user_sharing: no such user %s (from %s)", av[0], con->host);
	return;
    }

    do
    {
	n = atoi (av[1]);
	if (n < 0)
	{
	    log ("user_sharing: negative file count for user %s", user->nick);
	    err = 1;
	    break;
	}
	ASSERT (Num_Files >= user->shared);
	Num_Files -= user->shared;
	Num_Files += n;
	user->shared = n;

	libsize = strtoul (av[2], 0, 10);
	if (libsize < 0)
	{
	    log ("user_sharing: negative library size for user %s",
		 user->nick);
	    err = 1;
	    break;
	}

	if (Num_Gigs < user->libsize)
	{
	    log
		("user_sharing: error: Num_Gigs=%f, user->libsize=%u user->nick=%s",
		 Num_Gigs, user->libsize, user->nick);
	    Num_Gigs = user->libsize;	/* prevent negative count */
	    err = 1;
	    break;
	}
	Num_Gigs -= user->libsize;
	Num_Gigs += libsize;
	user->libsize = libsize;
    }
    while (0);

    if (err)
    {
	/* reset counts */
	Num_Files -= user->shared;
	Num_Gigs -= user->libsize;
	user->shared = 0;
	user->libsize = 0;
    }

    pass_message_args (con, tag, "%s %hu %u", user->nick, user->shared,
		       user->libsize);
}

/* 110 [:sender]
 * unshare all files
 */
HANDLER (unshare_all)
{
    USER   *sender;
    char   *sender_name;

    (void) len;
    if (pop_user_server (con, tag, &pkt, &sender_name, &sender))
	return;
    ASSERT (sender != 0);
#ifndef ROUTING_ONLY
    if (ISUSER (con))
    {
	if (!con->uopt->files)
	{
	    ASSERT (sender->shared == 0);
	    return;	/* nothing shared */
	}
	send_cmd (con, tag, "%d", sender->shared);
	free_hash (con->uopt->files);
	con->uopt->files = 0;
	ASSERT (Local_Files >= sender->shared);
	Local_Files -= sender->shared;
    }
#endif /* !ROUTING_ONLY */
    if (sender->libsize > Num_Gigs)
    {
	log ("unshare_all: sender->libsize=%u Num_Gigs=%f",
		sender->libsize, Num_Gigs);
	Num_Gigs = sender->libsize;
    }
    ASSERT (Num_Gigs >= sender->libsize);
    Num_Gigs -= sender->libsize;
    ASSERT (Num_Files >= sender->shared);
    Num_Files -= sender->shared;
    sender->libsize = 0;
    sender->shared = 0;
    pass_message_args (con, tag, ":%s", sender->nick);
}
