/* Copyright (c) 2000  Kevin Sullivan <nite@gis.net>
 *
 * Please refer to the COPYRIGHT file for more information.
 */

/* ---------------------------------------------------------------------- */
/* functions that deal with reading and writing the library file. */

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#include "library.h"
#include "nap.h"
#include "defines.h"
#include "lists.h"
#include "codes.h"
#include "alias.h"
#include "winio.h"

#ifdef MEMWATCH
  #include "memwatch.h"
#endif

masquerade_t *masqlist = NULL;
int masqcounter = 0;

/* read the library file SD into a library data structure. Return NULL
   on failure. */
library_t *read_library_file(char *sd) {
  library_t *l, *elt;
  char *p0, *p1, *p2, *p3, *p4, *r;
  FILE *f;
  
  if (!sd) {
    return NULL;
  }

  f = fopen(sd, "r");
  if (!f) {
    return NULL;
  }

  r = nap_getline(f);
  if (!r) {
    fclose(f);
    return NULL;
  }

  if (strcmp(r, NAP_LIBRARY_HEADER)!=0) {
    fclose(f);
    free(r);
    return 0;
  }

  free(r);

  l = NULL;
  
  while (1) {
    r = nap_getline(f);
    if (!r) {
      break;
    }
    if (*r != '\"') {
      free(r);
      continue;  /* skip headers and junk */
    }
    p0 = strchr(r+1, '\"');
    if (!p0 || p0[1]!=' ') {
      free(r);
      continue;
    }
    p1 = strchr(p0+2, ' ');
    if (!p1) {
      free(r);
      continue;
    }
    p2 = strchr(p1+1, ' ');
    if (!p2) {
      free(r);
      continue;
    }
    p3 = strchr(p2+1, ' ');
    if (!p3) {
      free(r);
      continue;
    }
    p4 = strchr(p3+1, ' ');
    if (!p4) {
      free(r);
      continue;
    }
    elt = malloc(sizeof(library_t));
    if (!elt) {
      free(r);
      continue;
    }
    *p0 = 0;
    *p1 = 0;
    *p2 = 0;
    *p3 = 0;
    *p4 = 0;

    elt->lfn = strdup(r+1);
    elt->hash = strdup(p0+2);
    elt->sz = atoi(p1+1);
    elt->bitrate = atoi(p2+1);
    elt->freq = atoi(p3+1);
    elt->len = atoi(p4+1);

    free(r);
    list_append(library_t, l, elt);
  }

  fclose(f);

  return l;
}  

/* Note: the format of the library file changed in v1.4.4-ps5: two
   lines were added at the beginning: a title, and the "upload" path
   that was used to create the file. This allows us to check whether
   the library needs re-building, see up_to_date(). -PS */

/* "unshare" the old files with the server, then rebuild library, then
   "share" new files. */

int rebuild(int s, char *sd, char *path)
{
  /* tell server to unshare files */
  if (s!=-1) {
    sendpack(s, NAP_UNSHARE, NULL);
  }

  if (buildflist(sd, path) == -1) {
    return(-1);
  }
  
  /* schedule new file list for sending to server */
  lfiles(s, sd);

  return(1);
}

/* check whether the given filename FN is shared, by looking it up in
   the library file SD. This is used for security purposes before
   allowing an upload. Note: filename must use "/" and not "\". Return
   1 if shared, 0 if not. When in doubt, return 0. Note: we compare
   filenames here in a case sensitive manner. This may or may not be
   correct. Pros: we have exact control over which files are shared.
   Cons: dumb remote clients, or dumb servers, may convert filenames
   to lowercase or to uppercase, thus resulting in a request that we
   cannot fulfill. However, I have not seen this happen in practice. */
int isshared(char *fn, char *sd) {
  FILE *f;
  char *r, *p;

  f = fopen(sd, "r");
  if (f == NULL) {
    return(0);
  }
  
  while (1)
  {
    r = nap_getline(f);
    if (!r)
      break;
    if (*r != '\"') {
      free(r);
      continue; /* skip headers and junk */
    }
    p = strchr(r+1, '\"');
    if (!p) {
      free(r);
      continue;
    }
    *p = 0;
    if (!strcmp(r+1, fn)) {
      free(r);
      fclose(f);
      return 1;
    }
    free(r);
  }
  fclose(f);
  return 0;
}
  
/* build the library file. sd is the name of the library file, and
   path is the ";"-separated sequence of upload directories. New in
   1.5.1: read the old contents first, and do not regenerate file
   stats where they are already known. */
int buildflist(char *sd, char *path)
{
  FILE *f;
  char *t, *n;
  dev_inode_t *di_list=NULL, *e;
  char *sharetypes = getval("sharetypes");
  library_t *l, *le;
  struct stat st;
  time_t reftime;
  int r;

  r = stat(sd, &st);
  if (r==0) {
    reftime = st.st_mtime;
    l = read_library_file(sd);
  } else {
    reftime = 0;
    l = NULL;
  }

  f = fopen(sd, "w");
  if (f == NULL)
  {
    wp(NULL, "Error opening \"%s\": %s\n", sd, strerror(errno));
    list_forall_unlink(le, l) {
      free(le->lfn);
      free(le->hash);
      free(le);
    }
    return(-1);
  }
  
  fprintf(f, NAP_LIBRARY_HEADER "\n");
  fprintf(f, "PATH=%s\n", path ? path : "");
  fprintf(f, "SHARETYPES=%s\n", sharetypes ? sharetypes : "");

  /* note: if path==NULL, we just generate an empty file */

  if (path) {
    n = strdup(path);
    
    t = strtok(n, ";");
    
    while (t)
      {
	t = strip(t);  /* remove whitespace on both ends */
	t = home_file(t);
	if (t[0] && t[strlen(t)-1]=='/') {  /* strip '/' from end */
	  t[strlen(t)-1]=0;
	}
	addfile(t, f, &di_list, l, reftime);
	free(t);
	t = strtok(NULL, ";");
      }
    
    free(n);
  }

  fclose(f);

  list_forall_unlink(e, di_list) {
    free(e);
  }

  list_forall_unlink(le, l) {
    free(le->lfn);
    free(le->hash);
    free(le);
  }
  
  return(1);
}

/* add the file or directory nm to the library file f. Be careful
   to avoid loops due to symlinks: DI_LIST is a linked list of
   device/inode pairs (of directories) that we have already seen. */

void addfile(char *fn, FILE *f, dev_inode_t **di_list_p, library_t *l, time_t reftime)
{
  DIR *dir;
  struct stat st;
  int ret;
  dev_inode_t *e;
  mhdr_t *m;
  struct dirent *dirent;
  library_t *elt;

  ret = stat(fn, &st);
  if (ret) { /* ignore files that don't exist */
    return;
  }

  if (S_ISDIR(st.st_mode)) {  /* is it a directory? */

    /* do not recurse if we've seen this dir before */
    list_find(e, *di_list_p, e->dev == st.st_dev && e->ino == st.st_ino);
    if (e) {
      return;
    }

    /* else, remember dir */
    e = (dev_inode_t *)malloc(sizeof(dev_inode_t));
    e->dev = st.st_dev;
    e->ino = st.st_ino;
    list_prepend(*di_list_p, e);

    /* now go through each file in this directory. */
    dir = opendir(fn); /* open directory */
    if (dir==NULL) {   /* ignore failures */
      return;
    }
    while ((dirent = readdir(dir)) != NULL) { /* go through each entry */
      /* make sure to ignore ".." and "." */
      if (strcmp(dirent->d_name, "..")!=0 && strcmp(dirent->d_name, ".")!=0) {
	int len = strlen(fn)+strlen(dirent->d_name)+2;
	char filename[len]; /* Note: this goes on the stack */
	strcpy(filename, fn);
	strcat(filename, "/");
	strcat(filename, dirent->d_name);
	addfile(filename, f, di_list_p, l, reftime);
      }
    }
    closedir(dir);
    return;

  } else if (S_ISREG(st.st_mode)) { /* is it a regular file? */

    /* see if the stats for this file are already known */
    if (st.st_mtime < reftime) {
      list_find(elt, l, strcmp(elt->lfn, fn)==0);

      /* as a special case, if hashing is requested but the cached
	 value is bogus, we don't use the cached value */

      if (elt && !(nvar_default("hash",0) && strncmp(elt->hash, BOGUS_HASH, 32)==0)) {
	fprintf(f, "\"%s\" %s %i %i %i %i\n", elt->lfn, elt->hash, elt->sz, elt->bitrate, elt->freq, elt->len);
	return;
      }
    }

    /* otherwise, calculate stats now */
    m = filestats(fn);
    if (m) {
      fprintf(f, "\"%s\" %s-%i %i %i %i %i\n", fn, m->check, m->sz1, m->sz, m->bitrate, m->freq, m->len);
      free(m);
    }
    return;

  }
}

/* decide whether a given file is shared, and if yes, get the file
   stats (checksum, size, bitrate etc). A file is shared if: either it
   is a valid mp3 file, or its file extension is in the sharetypes
   list. Thus, the sharetypes variable only needs to list file
   extensions other than mp3 - valid mp3 files are shared irrespective
   of their extension. */

mhdr_t *filestats(char *fn) {
  mhdr_t *m;
  char *p, *q;
  int plen, fnlen;
  int r;
  int f;
  struct stat st;
  char *sharetypes;

  /* first, check if this is a valid mp3 file (irrespective of extension) */
  m = mp3info(fn);
  if (m)
    return m;

  /* next, check if it's a valid ogg vorbis file (irrespective of extension) */
  m = ogginfo(fn);
  if (m)
    return m;

  /* not a valid music file - check to see if it is shared by its file
     extension */
  sharetypes = getval("sharetypes");
  if (!sharetypes) {
    return NULL;
  }

  fnlen = strlen(fn);

  p = sharetypes;
  for (p=sharetypes; p != (void *)NULL+1; p = strchr(p, ';')+1) {
    q = strchr(p, ';');
    plen = (q) ? q-p : strlen(p);
    if (plen == 1 && *p=='*')
      goto matches;
    if (fnlen >= plen+1 && fn[fnlen-plen-1]=='.' && strncasecmp(fn+fnlen-plen, p, plen)==0)
      goto matches;
  }
  return NULL;

 matches:
  /* try to open file */
  f = open(fn, O_RDONLY);
  if (f == -1)
    return(NULL);
  
  /* get file attributes, e.g. file size */
  fstat(f, &st);

  m = (mhdr_t *)malloc(sizeof(mhdr_t));
  /* not a music file: set freq, len, and bitrate to 0 */
  m->freq = 0;
  m->len = 0;
  m->bitrate = 0;
  m->sz = st.st_size;
  m->sz1 = st.st_size;
  r = md5hash(m->check, f);
  if (r) {
    free(m);
    close(f);
    return(NULL);
  }

  close(f);
  return(m);
}

/* schedule the files that we are sharing for sending to the
   server. SD is the library filename, S is the server fd. */
int lfiles(int s, char *sd) {
  sock_t *sv;
  library_t *l, *elt;
  char *vfn;

  sv = findsockfd(s);
  if (!sv) {
    return -1;
  }
  l = read_library_file(sd);
  list_forall_unlink(elt, l) {
    if (nvar_default("nomasq", 0) == 0) {
      /* do filename masquerading */
      vfn = masquerade(elt->lfn);
      free(elt->lfn);
      elt->lfn = vfn;
    }
    if (elt->lfn) {
      list_append(library_t, sv->shared_to_send, elt);
    } else {
      /* this should not normally happen: file was not on shared
	 path, or could not be rewritten somehow */
      free(elt->hash);
      free(elt);      
    }
  }
  return 1;
}

/* decide whether the library file SD is up-to-date relative to PATH.
   Check: nap version number, path, and whether any files in the PATH
   have been modified more recently than library file. Return 1 if
   up-to-date, 0 if not. */
int up_to_date(char *sd, char *path) {
  FILE *f;
  char *r, *n, *t;
  struct stat st;
  time_t mtime;
  dev_inode_t *di_list, *e;
  int ret, res;
  char *sharetypes = getval("sharetypes");

  if (!sd) {
    return 0;
  }

  f = fopen(sd, "r");
  if (!f) {
    return 0;
  }

  r = nap_getline(f);
  if (!r) {
    fclose(f);
    return 0;
  }

  if (strcmp(r, NAP_LIBRARY_HEADER)!=0) { 
    fclose(f);
    free(r);
    return 0;
  }

  free(r);

  r = nap_getline(f);
  if (!r) {
    fclose(f);
    return 0;
  }

  if (strncmp(r, "PATH=", 5) || strcasecmp(r+5, path ? path : "")) { 
    fclose(f);
    free(r);
    return 0;
  }
  
  free(r);

  r = nap_getline(f);
  if (!r) {
    fclose(f);
    return 0;
  }

  if (strncmp(r, "SHARETYPES=", 11) || strcmp(r+11, sharetypes ? sharetypes : "")) { 
    fclose(f);
    free(r);
    return 0;
  }
  
  free(r);

  /* the info in the file seemed okay */
  fclose(f);
  
  /* now check the modification times. */
  ret = stat(sd, &st);
  if (ret)
    return 0;

  mtime = st.st_mtime; /* remember modification time of library file */

  di_list = NULL;
  res = 1;

  /* now check whether each directory on path was modified after mtime */
  if (path) {
    n = strdup(path);
    for (t=strtok(n, ";"); t; t=strtok(NULL, ";")) {
      t = strip(t);
      t = home_file(t);
      if (modified_after(mtime, t, &di_list)) {
	res = 0;
	free(t);
	break;
      }
      free(t);
    }
    free(n);
  }
  list_forall_unlink(e, di_list) {
    free(e);
  }
  
  return res;
}

/* check whether file or directory FN was modified at or after time
   MTIME. If it's a directory, also check files and subdirectories
   recursively. Be careful to avoid loops due to symlinks: DI_LIST is
   a linked list of device/inode pairs that we have already seen. */

int modified_after(time_t mtime, char *fn, dev_inode_t **di_list_p) {
  struct stat st;
  int ret;
  dev_inode_t *e;
  DIR *dir;
  struct dirent *dirent;

  ret = stat(fn, &st);
  if (ret) { /* file does not exist? Let's ignore it and hope for the
                best. Note this could be a problem if user deleted an
                entire upload directory. */
    return 0;
  }

  if (st.st_mtime >= mtime) {
    return 1;
  }

  if (!S_ISDIR(st.st_mode)) { /* if it's not a directory, we are done */
    return 0;
  }

  /* do not recurse if we've seen this dir before */
  list_find(e, *di_list_p, e->dev == st.st_dev && e->ino == st.st_ino);
  if (e) {
    return 0;
  }

  /* else remember dir, and recurse */
  e = (dev_inode_t *)malloc(sizeof(dev_inode_t));
  e->dev = st.st_dev;
  e->ino = st.st_ino;
  list_prepend(*di_list_p, e);

  /* now go through each file in this directory. Blah. */

  dir = opendir(fn); /* open directory */
  if (dir==NULL) {   /* if open fails, ignore gracefully */
    return 0;
  }
  while ((dirent = readdir(dir)) != NULL) { /* go through each entry */
    /* make sure to ignore ".." and "." */
    if (strcmp(dirent->d_name, "..")!=0 && strcmp(dirent->d_name, ".")!=0) {
      int len = strlen(fn)+strlen(dirent->d_name)+2;
      char filename[len]; /* Note: don't need alloc. This goes on the stack */
      strcpy(filename, fn);
      strcat(filename, "/");
      strcat(filename, dirent->d_name);
      if (modified_after(mtime, filename, di_list_p)) {
	closedir(dir);
	return 1;
      }
    }
  }

  closedir(dir);
  return 0;
}

/* ---------------------------------------------------------------------- */
/* filename masquerading */

/* Suppose our upload path is PATH1;PATH2. Then we replace absolute
   local filenames such as PATH1/file1 or PATH2/x/file2 by "virtual
   filenames" like /1/file1, /2/x/file2, so that we don't reveal our
   local directory structure to the world. Note: this will not work
   properly if we actually have directories named /1/, /2/ on our
   share path! */

/* convert local pathname to virtual pathname. Returns an allocated
   string, or NULL on failure. In particular, return NULL if the path
   is not shared. (Note however that we do not check whether path is
   in the shared *library* file - this also depends on its filename
   extension, whether the file contents are valid, etc. It needs to be
   checked separately.) */
char *masquerade(char *lfn) {
  char *upload = getval("upload");
  char *vfn=NULL, *t;
  char *match=NULL;
  masquerade_t *elt;
  int len;

  if (!upload) {
    return NULL;
  }

  upload = strdup(upload);
  t = strtok(upload, ";");
  while (t) {
    t = strip(t);
    if (!t[0]) {
      goto next;
    }
    t = home_file(t);
    len = strlen(t);
    if (!t[0] || t[len-1]!='/') {  /* make sure the path ends in '/' */
      t = realloc(t, len+2);
      if (!t) {
	goto next;
      }
      t[len+1] = 0;
      t[len] = '/';
      len++;
    }
    if (strncmp(lfn, t, len)==0) {
      if (!match || len >= strlen(match)) {
	free(match);
	match = strdup(t);
      }
    }

  next:
    free(t);
    t = strtok(NULL, ";");
  }
  free(upload);
  if (!match) {
    return NULL;
  }

  list_find(elt, masqlist, strcmp(elt->path, match)==0);
  if (!elt) {
    elt = malloc(sizeof(masquerade_t));
    if (!elt) {
      free(match);
      return NULL;
    }
    elt->path = strdup(match);
    elt->n = ++masqcounter;
    list_prepend(masqlist, elt);
  }
  /* rewrite */
  msprintf(&vfn, "/%d/%s", elt->n, lfn+strlen(match));
  free(match);
  return vfn;
}

/* convert virtual pathname to local pathname. If not possible, return
   a copy of vfn. In any case, return an allocated string. */
char *unmasquerade(char *vfn) {
  char *lfn=NULL;
  int n;
  char *p;
  masquerade_t *elt;
  
  /* check if vfn begins with /n/, where n is an integer */
  if (vfn[0] != '/') {
    return strdup(vfn);
  }
  n = strtod(vfn+1, &p);
  if (p==vfn+1 || *p != '/') {
    return strdup(vfn);
  }
  /* found an integer; check whether it is known */
  list_find(elt, masqlist, elt->n == n);
  if (!elt) {
    return strdup(vfn);
  }
  /* do the substitution */
  msprintf(&lfn, "%s%s", elt->path, p+1);
  return lfn;
}
