/*
** Mfm.c
**	Multimedia File Management
*/
#include <stdio.h>
#include <string.h>
/*#include <unistd.h>*/
#include <stdlib.h>
#include <fcntl.h>
#include <errno.h>

#include <sys/stat.h>
/*#include <dirent.h>*/

#include "Mfm.h"

/*
** When any of the MFM functions is used the first time within
** program, MFM attempts to load the global rules file:
**
** 1) If environment variable defined by MFM_GLOBAL_RULES exists,
**    then the value of that variable is used as the filename of
**    the global rules. Note that existence of this environment
**    variable completely overrides the 'global_files'.
**
** 2) If environment variable is not defined, then the first file
**    in global_files that exists, will be used.
**
** 3) If no global rules is found or some error occurs when reading
**    the selected file, then no global rules are used.
*/
#define MFM_GLOBAL_RULES "MFM_RULES"

static char *global_files[] =
    {
	"/etc/mfm",
	"/usr/local/etc/mfm",
	NULL,
    };

/*
** MFM keeps the .MFM of the last accessed directory in memory.
** This buffer is allocated as large as required, but to prevent
** "startup reallocation burst", a mimimun size is defined.
*/
#define MFM_MIN_RULE_BUFFER_SIZE 2000
/*
** MFM needs to construct filename occasionally. This value gives
** the maximum length of such path.
*/
#define MFM_MAX_FILEPATH_LENGTH 500
/*
** When MFM needs to create the local .MFM files, the following
** is the default file mode that is used
*/
#define MFM_DEFAULT_MODE 0644

/*
**  matches
**	Compare if a given string (name) matches the given
**	mask. The mask can contain wild cards: '*' - match any
**	number of chars, '?' - match any single character. Mask
**	is terminated either with NUL byte or ':' character.
**
**	return	0, if match found, msk points past the terminating
**		   character.
**		1, if not matched, msk points to the next character
**		   *after* the one that caused mismatch.
**
**	*NOTE*	This function is *NOT* fast if the mask has
**		'*'s in it, the more, the slower it will become.
**
**		'nam' must not contain ':'!
*/
static int matches(msk, nam)
char **msk, *nam;
    {
	register char c, m;
	
	for (;;)
	    {
		m = *(*msk)++;
		c = *nam++;
		if (c == '\0')
			break;
		if ((m != c && m != '?') || c == '*')
			break;
	    }
	if (m != '*')
		return c != 0 || (m != ':' && m != 0);
	/*
	** Any *'s following one in mask can be ignored. If mask ends
	** after a '*', there is a match.
	*/
	for (;;)
		if ((m = *(*msk)++) == '\0' || m == ':')
			return 0; /* matched everything, end of mask */
		else if (m != '*')
			break;	  /* more work to do... */
	/*
	** m = next character after '*' in the mask. This must
	** match the character in 'c'.
	*/
	if (m == '?')
	    {
		/*
		** '?' matches with any following single character.
		** Eat nam away one by one and try to match the
		** remainder with current 'msk'. Strings don't match
		** if we run out of nam while doing this.
		*/
		for ( ; *nam; nam++)
		    {
			char *tmp = *msk;
			if (matches(&tmp, nam) == 0)
				return *msk = tmp, 0;
		    }
		return 1;
	    }
	/*
	** m contains a non-wild card character which must
	** exist somewhere in nam for match to be successful.
	** Look forward for this character and when found,
	** eat it away and check if the remainder matches with
	** the mask. If it doesn't, repeat the search and
	** matches for the remaining nam.
	*/
	for ( ;; )
	    {
		char *tmp = *msk;
		while (c != m)
			if ((c = *nam++) == '\0')
				return 1;
		if (matches(&tmp, nam) == 0)
			return *msk = tmp, 0;
		if ((c = *nam++) == '\0')
			return 1;
	    }
    }


/*
** equals
**	Look for exact match from rules without wild card processing.
**	Mask is terminated either with NUL byte or ':' character.
**
**	return	0, if match found, msk points past the terminating
**		   character.
**		1, if not matched, msk points to the next character
**		   *after* the one that caused mismatch.
*/
static int equals(msk, nam)
char **msk, *nam;
    {
	register char c, m;
	
	do
	    {
		m = *(*msk)++;
		c = *nam++;
		if (c == 0)
			return m != ':' && m != 0;
	    }
	while (c == m);
	return 1;
    }

/*
** ******************************
** File Type management utilities
** ******************************
*/

static char *global_rules = NULL;
static int mfm_errno = MfmError_ERROR;

/*
** LoadRulesFile
**	Read the content of the specified rules file into a buffer
**	and zap LF's from it.
*/
static void LoadRulesFile(char *path, char *buf, int len)
    {
	FILE *fp = fopen(path, "rb");

	if (fp != NULL && len > 2)
	    {
		if ((len = fread(buf, 1, len-2, fp)) < 0)
			len = 0;
		fclose(fp);
		buf[len] = 0;
		buf[len+1] = 0;
		while (buf = strchr(buf, '\n'))
			*buf++ = 0;
	    }
	else
	    {
		*buf++ = 0;
		*buf++ = 0;
	    }
    }

/*
** SaveRulesFile
**	Write a new rule (if specified) and append old rules after
**	it.
*/
static int SaveRulesFile(char *path, char *rules, char *name, char *type)
    {
	FILE *fp = fopen(path, "wb");

	if (fp == NULL)
		return MfmError_ERROR;
	if (name)
		fprintf(fp, "%s:%s\n", name, type ? type : "");
	while (*rules)
	    {
		fprintf(fp, "%s\n", rules);
		while (*rules++);
	    }
	fclose(fp);
	return MfmError_SUCCESS;
    }


/*
** ReadConfiguration
**	Read the predefined global configuration file into memory
*/
static void ReadConfiguration()
    {
	char *name;
	struct stat info;
	int i, r;

	if ((name = getenv(MFM_GLOBAL_RULES)) != NULL)
		r = stat(name, &info);
	else for (i = 0, r = -1; name = global_files[i]; ++i)
		if ((r = stat(name, &info)) == 0)
			break;
	if (r == 0 &&
	    (global_rules = (char *)malloc(info.st_size+2)) != NULL)
		LoadRulesFile(name, global_rules, info.st_size+2);
	else
		global_rules = "\0";
    }

/*
** LocalMfmName
**	Build the name of the local MFM which is applicaple to the
**	given name. The returned value is guaranteed to be valid
**	only until next call to this function.
*/
static char *LocalMfmName(char *name)
    {
	static char mfm[] = {'.', 'M', 'F', 'M', 0};
	static char buf[MFM_MAX_FILEPATH_LENGTH+1];

	char *path = strrchr(name, '/');

	if (path == NULL)
		return mfm;
	else if ((path-name)+1+sizeof(mfm) < sizeof(buf))
	    {
		memcpy(buf,name,(path-name)+1);
		memcpy(buf+(path-name)+1, mfm, sizeof(mfm));
		return buf;
	    }
	mfm_errno = MfmError_LONGNAME;
	return NULL; /* Can't handle the file path, too long! */
    }

/*
** ReadLocalRules
**	Read the local rules file matching the specified file path.
**	Return a pointer to the rules if found and NULL otherwise.
**
**	Any previously existing locks on local file will be removed.
**
**	'lock' is non-zero when an update access to the local rules
**	is required. The local rules file (.MFM) will be created if
**	it doesn't exist and locked exclusively.
*/
#define MFM_NO_LOCK 0
#define MFM_LOCK 1

static char *ReadLocalRules(char *name, int lock)
    {
	static struct stat current;
	static char *local_rules = NULL;
	static int local_size = 0;
	static int locked_fd = -1;
	
	struct stat new;
	char *path = LocalMfmName(name);

	if (locked_fd >= 0)
	    {
		close(locked_fd);
		locked_fd = -1;
		current.st_mtime = 0; /* Force reload! */
	    }
	if (path == NULL)
		return NULL;
	if ((lock &&
	     ((locked_fd = open(path, O_RDWR | O_CREAT, MFM_DEFAULT_MODE))<0 ||
	      lockf(locked_fd, F_LOCK, 0) ||
	      fstat(locked_fd, &new))
	     ) || stat(path, &new))
	    {
		mfm_errno = errno;
		return NULL;
	    }
	if (new.st_dev == current.st_dev && new.st_ino == current.st_ino &&
	    new.st_mtime == current.st_mtime)
		return local_rules;
	current = new;
	if (local_size < current.st_size+2)
	    {
		if (local_rules)
			free(local_rules);
		local_size = current.st_size + 2;
		if (local_size < MFM_MIN_RULE_BUFFER_SIZE)
			local_size = MFM_MIN_RULE_BUFFER_SIZE;
		if ((local_rules = (char *)malloc(local_size)) == NULL)
		    {
			local_size = 0;
			mfm_errno = MfmError_MEMORY;
			return NULL;
		    }
	    }
	LoadRulesFile(path, local_rules, local_size);
	return local_rules;
    }
/*
** LookupRules
*/
static char *LookupRules(char *rules, char *name)
    {
	if (rules != NULL)
		while (*rules)
			if (matches(&rules, name) == 0)
				return rules[-1] ? rules : rules-1;
			else while (*rules++);
	return NULL;
    }
/*
** MfmGetContentType
*/
char *MfmGetContentType(char *name)
    {
	char *m = LookupRules(ReadLocalRules(name, MFM_NO_LOCK), name);

	if (m != NULL)
		return m;
	if (global_rules == NULL)
		ReadConfiguration();
	return LookupRules(global_rules, name);
    }

/*
** MfmPutContentType
*/
int MfmPutContentType(char *name, char *type)
    {
	char *rules = ReadLocalRules(name, MFM_LOCK);
	char *m, *s;

	if (rules == NULL)
		return mfm_errno;
	m = rules;
	while (*m)
		if (equals(&m, name) == 0)
		    {
			/*
			** Remove the matched definition from the rules
			*/
			for (s = --m; s > rules && s[-1]; --s);
			while (*m++);
			while (*m)
				while (*s++ = *m++);
			*s++ = 0;
			break;
		    }
		else while (*m++);
	s = LocalMfmName(name);
	SaveRulesFile(s, rules, type ? name : (char *)NULL, type);
	rules = ReadLocalRules(name, MFM_NO_LOCK);
	return MfmError_SUCCESS;
    }

/*
** MfmSetContentType
**
** *NOTE*
**	This function works properly ONLY IF wildcards are not used
**	in local rules!!!
*/
int MfmSetContentType(char *name, char *type)
    {
	char *t;
	if (global_rules == NULL)
		ReadConfiguration();
	t = LookupRules(global_rules, name);
	if (t == type || (t && type && strcmp(t, type) == 0))
	    {
		/*
		** Global rules give out the same type. Now have to
		** check if local rules would override this.
		*/
		if (LookupRules(ReadLocalRules(name,MFM_NO_LOCK),name)==NULL)
			return MfmError_SUCCESS;
		type = NULL;
	    }
	return MfmPutContentType(name, type);
    }
/*
** ****************************
** Directory Browsing utilities
** ****************************
*/
typedef struct MfmDirectoryRec
    {
	DIR *dirp;
	MfmFilter filter;
	void *data;
    } MfmDirectoryRec;

MfmDirectory MfmOpenDirectory(char *name, MfmFilter filter, void *data)
    {
	MfmDirectory dir;
	DIR *dirp;

	dirp = opendir(name);
	if (dirp == NULL)
		return NULL;
	dir = (MfmDirectory)malloc(sizeof(*dir));
	if (dir == NULL)
		closedir(dirp);
	else
	    {
		dir->filter = filter;
		dir->data = data;
		dir->dirp = dirp;
	    }
	return dir;
    }

char *MfmNextFile(MfmDirectory dir)
    {
	struct dirent *dp;

	if (dir)
		while ((dp = readdir(dir->dirp)) != NULL)
			if (dir->filter == NULL ||
			    (*dir->filter)(dp->d_name, dir->data))
				return dp->d_name;
	return NULL;
    }

void MfmCloseDirectory(MfmDirectory dir)
    {
	if (dir)
	    {
		closedir(dir->dirp);
		free(dir);
	    }
    }
