/*
** 1998-08-02 -	This module deals with file types. It provides services to initialize the
**		default types, add/delete/move previously created types, and even using all
**		this type information to analyze files. This is useful stuff.
** 1998-08-15 -	Mild redesign; all functions now take a "generic" GList rather than a CfgInfo.
**		This allows use of these functions on styles _not_ sitting on the global
**		CfgInfo.style list. Very handy when editing.
** 1998-08-26 -	Massive hacking to implement support for 'file' RE matching. Got a lot nicer
**		than I had first expected, actually. If used, 'file' is always envoked
**		exactly once for each dirpane. This cuts down on the overhead of 'file'
**		reading and parsing its rather hefty (~120 KB on my system) config file.
** 1998-09-15 -	Added support for case-insensitive regular expressions.
** 1998-09-16 -	Added priorities to file types, controlling the order in which they are
**		checked (and listed, of course). Priorities are in 0..254, which I really
**		think should be enough. If I'm wrong, I'll just square that number. :) 0 is
**		the highest priority (which should explain why 255 is reserved for "Unknown").
** 1998-09-18 -	Regular expressions are now handled by the POSIX code in the standard C library.
**		No longer any need for Henry Spencer's code. Feels good.
** 1998-12-13 -	Priorities removed. Types now explicitly ordered by user in config.
*/

#include "gentoo.h"

#include <assert.h>
#include <ctype.h>
#include <fcntl.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/wait.h>
#include <unistd.h>

#include <stddef.h>

#include "strutil.h"
#include "styles.h"
#include "fileutil.h"
#include "types.h"

/* ----------------------------------------------------------------------------------------- */

/* These improve readability of file I/O calls when dealing with 'file'. */
#define	FD_INPUT	(0)
#define	FD_OUTPUT	(1)

/* This list is used between typ_identify_begin() and typ_identify_end() to hold directory
** lines that seem to need to be shown to the external 'file' command.
*/
static GList	*file_list = NULL;
/* Another global (shudder), this one signals if there *are* any 'file'-using types around. */
static gint	file_used = 0;

/* ----------------------------------------------------------------------------------------- */

/* 1998-08-02 -	Create a new type, with the given <name> and identification strings. Use
**		NULL for an identifier that should not be used. Returns a pointer to a new
**		FType structure, or NULL on failure.
** 1998-08-11 -	Now takes the name of a <style> to apply to files of this type, too.
** 1998-09-07 -	Added another argument, for the new permissions support. That's eight
**		arguments; pretty close to my personal limit. :)
** 1999-05-29 -	Removed the <style> argument again, since it was too complex. Use typ_type_set_style().
*/
FType * typ_type_new(CfgInfo *cfg, const gchar *name, mode_t mode, int perm, const gchar *suffix, const gchar *name_re, const gchar *file_re)
{
	FType	*type;

	if((type = malloc(sizeof *type)) != NULL)
	{
		str_strncpy(type->name, name, sizeof type->name);
		type->mode  = mode;
		type->perm  = perm;
		type->flags = 0UL;
		type->suffix[0] = '\0';
		type->name_re_src[0] = '\0';
		type->name_re = NULL;
		type->file_re_src[0] = '\0';
		type->file_re = NULL;
		if(perm != 0)
			type->flags |= FTFL_REQPERM;

		if(suffix != NULL)
		{
			str_strncpy(type->suffix, suffix, sizeof type->suffix);
			type->flags |= FTFL_REQSUFFIX;
		}
	
		if(name_re != NULL)
		{
			str_strncpy(type->name_re_src, name_re, sizeof type->name_re_src);
			type->flags |= FTFL_NAMEMATCH;
		}
		if(file_re != NULL)
		{
			str_strncpy(type->file_re_src, file_re, sizeof type->file_re_src);
			type->flags |= FTFL_FILEMATCH;
		}
		type->style = NULL;
	}
	return type;
}

/* 1998-08-14 -	Create a copy of the <old> type. Has the side-effect of clearing all compiled
**		regular expressions in the original (and the copy).
*/
FType * typ_type_copy(FType *old)
{
	FType	*nt;

	if((nt = malloc(sizeof *nt)) != NULL)
	{
		if(old->name_re != NULL)
		{
			regfree(old->name_re);
			g_free(old->name_re);
			old->name_re = NULL;
		}
		if(old->file_re != NULL)
		{
			regfree(old->file_re);
			g_free(old->file_re);
			old->file_re = NULL;
		}
		memcpy(nt, old, sizeof *nt);
	}
	return nt;
}

void typ_type_destroy(FType *type)
{
	if(type->name_re != NULL)
	{
		regfree(type->name_re);		
		g_free(type->name_re);
	}
	if(type->file_re != NULL)
	{
		regfree(type->file_re);
		g_free(type->file_re);
	}
	free(type);
}

/* ----------------------------------------------------------------------------------------- */

/* 1998-09-07 -	Check if the file described by <line> matches the permissions requirements
**		in <type>. Returns 1 if so, otherwise 0.
** 1999-03-14 -	Made the access to the DirRow (previously DirLine) a lot more abstract.
*/
static int check_perm(FType *type, DirRow *row)
{
	int	pr = 1, pw = 1, px = 1;
	mode_t	mode = 0;
	uid_t	uid;
	gid_t	gid;

	/* First build mode mask, for SetUID, SetGID and sticky. */
	if(type->perm & FTPM_SETUID)
		mode |= S_ISUID;
	if(type->perm & FTPM_SETGID)
		mode |= S_ISGID;
	if(type->perm & FTPM_STICKY)
		mode |= S_ISVTX;

	/* Now we know the mode requirements - check if fulfilled. */
	if((mode != 0) && ((DP_ROW_STAT(row).st_mode & mode) != mode))
		return 0;

	uid = geteuid();
	gid = getegid();

	/* Still here? Fine, then do (unorthodox, user-centric) read/write/execute permission checks. */
	if(type->perm & FTPM_READ)
		pr = fut_can_read(&DP_ROW_STAT(row), uid, gid);
	if(type->perm & FTPM_WRITE)
		pw = fut_can_write(&DP_ROW_STAT(row), uid, gid);
	if(type->perm & FTPM_EXECUTE)
		px = fut_can_execute(&DP_ROW_STAT(row), uid, gid);

	return pr && pw && px;
}

/* ----------------------------------------------------------------------------------------- */

/* 1998-09-15 -	Check the RE in <re>, which has source <re_src>, against <string>.
**		Returns 1 on match, 0 on failure.
*/
static int check_re(const gchar *re_src, regex_t **re, gboolean glob, gboolean nocase, const gchar *string)
{
	const gchar	*glob_re = NULL;

	if(*re == NULL)					/* RE not compiled? */
	{
		if(glob)
		{
			glob_re = str_glob_to_re(re_src);
			re_src = glob_re;
		}
		*re = g_malloc(sizeof **re);
		regcomp(*re, re_src, REG_EXTENDED | REG_NOSUB | (nocase ? REG_ICASE : 0));
		if(glob_re)
			g_free((void *) glob_re);			/* Free the globbed version. */
	}
	if(*re != NULL)
		return regexec(*re, string, 0, NULL, 0) != REG_NOMATCH;
	return REG_NOMATCH;
}

/* 1998-08-02 -	Attempt to apply the identification rules given in by the type <type> to the
**		dirpane line in <user>. The string <fout> is output from 'file' on this very file,
**		or NULL if 'file' hasn't yet been run. Returns TRUE if there is indeed a match. Note how
**		regular expressions are compiled if they're not already, and that the compiled
**		version is then kept for future use. This places some demands on the memory
**		handling later (config etc) but I'll deal with that then...
** 1998-08-26 -	Extended. Now actually does check against 'file' output.
** 1998-08-30 -	Now supports glob->RE translations for name and file matches. Smooth.
** 1998-09-15 -	Moved the actual RE checking out to a function of its own, since it was getting complex.
** 1999-03-14 -	Replaced DirLine with new, opaque, DirRow.
*/
static int check_type(FType *type, DirRow *row, const gchar *fout)
{
	guint	tries = 0, hits = 0;
	gchar	*name;

	/* Everything matches the unknown type, and that's a fact. */
	if(TYP_IS_UNKNOWN(type))
		return 1;

	name = DP_ROW_LINKNAME(row);
	if(name == NULL)
		name = DP_ROW_NAME(row);

	/* Apply the mode test first, since it's no doubt the fastest. */
	if(((DP_ROW_STAT(row).st_mode) & S_IFMT) != type->mode)
		return 0;
	if(type->flags & FTFL_REQPERM)
	{
		tries++;
		hits += check_perm(type, row);
	}
	if(type->flags & FTFL_REQSUFFIX)
	{
		tries++;
		hits += str_has_suffix(name, type->suffix);
	}
	if(type->flags & FTFL_NAMEMATCH)
	{
		tries++;
		hits += check_re(type->name_re_src, &type->name_re, type->flags & FTFL_NAMEGLOB, type->flags & FTFL_NAMENOCASE, name);
	}

	if(type->flags & FTFL_FILEMATCH)
	{
		tries++;
		if(fout != NULL)
			hits += check_re(type->file_re_src, &type->file_re, type->flags & FTFL_FILEGLOB, type->flags & FTFL_FILENOCASE, fout);
	}

	return tries == hits;
}

/* 1998-08-02 -	Identify the <line>, i.e. attempt to find an FType that matches it, and assign
**		a pointer to that FType to the line's "type" field. If no user-defined type
**		is found, we assign the root type ("Unknown").
** 1998-08-26 -	Now returns TRUE if a "sophisticated" type was indeed found, FALSE if the "Unknown"
**		type had to do. Also has become 'static', since you can't call this outside of
**		the framework established by typ_identify_begin()/typ_idenfify_end(). Note use
**		of NULL as third arg to check_type(), since we haven't started running 'file'
**		yet.
** 1999-03-14 -	Replaced DirLine with DirRow.
*/
static gint identify(MainInfo *min, DirRow *row)
{
	GList	*here;

	for(here = min->cfg.type; here != NULL; here = g_list_next(here))
	{
		if(check_type((FType *) here->data, row, NULL))
			break;
	}
	DP_ROW_TYPE(row) = (FType *) here->data;
	return !TYP_IS_UNKNOWN((FType *) here->data);
}

/* ----------------------------------------------------------------------------------------- */

/* 1998-08-26 -	A rewrite of the much of the (three weeks old) file typing architecture, in
**		order to accomodate use of 'file' in a way even resembling something efficient.
**		Rather than just calling typ_identify() on a dirline, you call first this
**		function, which initializes some state. You then, repeatedly, call typ_identify_line()
**		for all lines you wish to type. Then call typ_identify_end(). Done!
*/
void typ_identify_begin(MainInfo *min)
{
	GList	*here;

	if(file_list != NULL)
		fprintf(stderr, "**TYPES: Attempted to nest calls to typ_identify_begin()!\n");

	for(here = min->cfg.type; here != NULL; here = g_list_next(here))
	{
		if(((FType *) here->data)->flags & FTFL_FILEMATCH)
			break;
	}
	file_used = (here != NULL) ? TRUE : FALSE;
}

/* ----------------------------------------------------------------------------------------- */

/* 1998-08-26 -	Identify the given line. If identification fails, i.e. if the "Unknown" type is
**		assigned to the line, remember the line for later exposure to the external
**		'file' command.
*/
void typ_identify(MainInfo *min, DirRow *row)
{
	if((identify(min, row) == FALSE) && (file_used == TRUE))
		file_list = g_list_prepend(file_list, (gpointer) row);
}

/* ----------------------------------------------------------------------------------------- */

/* 1998-08-26 -	Attempt to match all types in list against <line>, knowing that 'file' said
**		<fout>. Only types that include a 'file'-matching RE are checked, of course.
**		Returns the matching type if any, or NULL if there's no match.
*/
static FType * match_file(GList *list, DirRow *row, char *fout)
{
	FType	*type;

	for(; (list != NULL) && (type = (FType *) list->data); list = g_list_next(list))
	{
		if(check_type(type, row, fout))
			return type;
	}

	return NULL;
}

/* 1998-08-26 -	End of batched identification. If any unknown's were found *and* we know that
**		there are 1 or more types using the 'file' recognition support, we need to
**		call 'file'. The idea here is that we only want to execute the 'file' command
**		once, no matter how many files need identification. Also, we use redirection
**		of stdin for 'file' to supply it with file names to check (via its "-f -" option),
**		since writing out the names to disk seems inefficient.
*/
void typ_identify_end(MainInfo *min, const gchar *path)
{
	GList	*here;
	gchar	buf[MAXNAMLEN + 2], *temp_name, line[MAXNAMLEN + 256], *fout;
	gint	fd[2], len, status;
	pid_t	file_pid;
	FType	*type;
	FILE	*in;

	if(file_list == NULL)
		return;
	if((temp_name = tmpnam(NULL)) == NULL)
		return;

	if(pipe(fd) == 0)
	{
		switch((file_pid = fork()))
		{
			case 0:
				assert(close(FD_INPUT) == 0);			/* Kill stdin. */
				assert(dup(fd[FD_INPUT]) == FD_INPUT);		/* Replace with pipe from main. */
				assert(close(fd[FD_INPUT]) == 0);		/* Close the pipe. */
				assert(close(FD_OUTPUT) == 0);			/* Kill stdout. */
				assert(open(temp_name, O_CREAT|O_RDWR, S_IRWXU) == FD_OUTPUT);
				assert(close(fd[FD_OUTPUT]) == 0);		/* Close unused pipe. */
				assert(chdir(path) == 0);			/* Change to the path in question. */
				execlp("file", "file", "-f", "-", NULL);
				exit(EXIT_FAILURE);
			case -1:
				fprintf(stderr, "fork() failed! Damn!\n");
				exit(EXIT_FAILURE);
		}
		assert(close(fd[FD_INPUT]) == 0);		/* Close input part of pipe (used by child). */
		for(here = file_list; here != NULL; here = g_list_next(here))
		{
			len = g_snprintf(buf, sizeof buf, "%s\n", DP_SEL_NAME(here));
			write(fd[FD_OUTPUT], buf, len);
		}

		close(fd[FD_OUTPUT]);
		waitpid(file_pid, &status, 0);

		if(WIFEXITED(status))
		{
			if((in = fopen(temp_name, "rt")) != NULL)
			{
				for(here = file_list; here != NULL && fgets(line, sizeof line, in); here = g_list_next(here))
				{
					if((fout = strchr(line, ':')) == NULL)
						continue;
					for(fout++; isspace((int) *fout); fout++)
						;
					if((type = match_file(min->cfg.type, DP_SEL_ROW(here), fout)) != NULL)
						DP_SEL_TYPE(here) = type;
				}
				fclose(in);
			}
			remove(temp_name);
		}
		else
			fprintf(stderr, "Failure!!\n");
		g_list_free(file_list);
		file_list = NULL;
	}
	else
		fprintf(stderr, "No pipe!\n");
}

/* ----------------------------------------------------------------------------------------- */

/* 1998-08-02 -	Initialize the file typing subsystem with some simple default types.
** 1998-09-02 -	Painlessly diked out all but "Unknown" and "Directory", since I think these
**		two are the only ones that are going to be built-in.
*/
void typ_init(CfgInfo *cfg)
{
	FType		*type;

	cfg->type = NULL;
	if((type = typ_type_new(cfg, "Unknown", 0, 0, NULL, NULL, NULL)) != NULL)
	{
		cfg->type = typ_type_insert(cfg->type, NULL, type);
		cfg->type = typ_type_set_style(cfg->type, type, cfg->style, NULL);
	}
	if((type = typ_type_new(cfg, "Directory", S_IFDIR, 0, NULL, NULL, NULL)) != NULL)
	{
		cfg->type = typ_type_insert(cfg->type, NULL, type);
		cfg->type = typ_type_set_style(cfg->type, type, cfg->style, "Directory");
	}
}

/* ----------------------------------------------------------------------------------------- */

/* 1998-12-14 -	Rewritten another time. Now never inserts anything after the "Unknown" type.
**		Pretty lazily implemented, but so?
** 1999-01-09 -	Rewritten again. Now takes an additional <after> argument, and inserts <typ>
**		right after it. If <after> is the "Unknown", we insert before it. If it's
**		NULL, the same thing happens.
*/
GList * typ_type_insert(GList *list, FType *after, FType *type)
{
	gint	li;

	if(after == NULL || TYP_IS_UNKNOWN(after))		/* No reference element given, or "Unknown" ref? */
	{
		GList	*last;

		if((last = g_list_last(list)) != NULL)
		{
			if(TYP_IS_UNKNOWN((FType *) last->data))
			{
				li = g_list_index(list, last->data);
				return g_list_insert(list, type, li);
			}
		}
		return g_list_append(list, type);
	}
	li = g_list_index(list, after);
	return g_list_insert(list, type, li + 1);
}

/* 1998-12-14 -	Remove a type from the list, and return the new list. Not strictly
**		necessary (only called at one place), but makes me feel good. :^)
*/
GList * typ_type_remove(GList *list, FType *type)
{
	if(list == NULL || type == NULL)
		return NULL;

	if(TYP_IS_UNKNOWN(type))		/* Can't remove the "Unknown" type. */
		return list;

	list = g_list_remove(list, type);
	typ_type_destroy(type);

	return list;
}

/* ----------------------------------------------------------------------------------------- */

/* 1998-09-17 -	Change the name of given type. Since the name is used as a fall-back
**		when sorting types of equal priority, this calls for a resort.
** 1998-12-13 -	Removing the priorities also removed the sorting dependency on names,
**		so this became a lot simpler. I could remove the entire function,
**		but I'll keep it. You never know...
*/
GList * typ_type_set_name(GList *list, FType *type, const gchar *name)
{
	str_strncpy(type->name, name, sizeof type->name);
	return list;
}

/* 1999-05-29 -	Set the 'style' field of <type> to the style whose name is <name>. */
GList * typ_type_set_style(GList *list, FType *type, StyleInfo *si, const gchar *name)
{
	if((type != NULL) && (si != NULL))
		type->style = stl_styleinfo_style_find(si, name);
	return list;
}

/* 1998-12-13 -	Move given <type> either up (<delta> == -1) or down (1). Returns
**		new version of <list>. Other <delta> values are illegal.
*/
GList * typ_type_move(GList *list, FType *type, gint delta)
{
	gint	pos, np;

	if(delta != -1 && delta != 1)
		return list;

	pos  = g_list_index(list,  type);
	list = g_list_remove(list, type);
	np   = pos + delta;
	if(np < 0)
		np = 0;
	else if(np > (gint) g_list_length(list) - 1)
		np = (gint) g_list_length(list) - 1;

	return g_list_insert(list, type, np);
}
