/*
** 1998-05-23 -	This little module helps map user and group IDs to ASCII names.
**		To perform this mapping, it reads /etc/passwd.
** 1998-05-29 -	Heavily modified. Now it is more eager to learn, since it will always
**		store *all* new encountered (uid,uname) and (gid,gname) pairs.
**		This maximizes the amount of user/group info available, and allows
**		this info to be used for other funky things (such as a chown command).
** 1998-06-08 -	Redesigned the menu-creation support, and also added some crucial
**		functionality (squeezed it in).
** 1998-12-22 -	Completely rewrote the /etc/passwd parsing. No longer lets the user control
**		the file name, but considers it the system's business. Uses the BSD-ish
**		getpwent()-API to access the file. Much cleaner and more robust. Also added
**		storing of the user's home directories to another hash (for ~-support).
**		  Then did the exact same thing for group file parsing. These changes resulted
**		in a cleaner interface, since this module is now self-contained.
*/

#include "gentoo.h"

#include <grp.h>
#include <pwd.h>
#include <stdio.h>
#include <stdlib.h>

#include "strutil.h"
#include "userinfo.h"

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

#define	PASSWD_LINE_MAX	(128)
#define	USER_NAME_MAX	(32)
#define	GROUP_LINE_MAX	(PASSWD_LINE_MAX)
#define	GROUP_NAME_MAX	(USER_NAME_MAX)

#define	MENU_INITIAL	(16)
#define	MENU_CHUNK	(8)

typedef enum { MNS_ID = 0, MNS_TEXT } SMode;

typedef struct {	/* This holds data used to build gtk menu items. */
	gint	id;		/* Either user or group ID. */
	gchar	*text;		/* The corresponding user or group name. */
} Item;

typedef struct {
	Item	*item;		/* Menu definition data vector. */
	guint	num_alloc;	/* Number of Item-structs allocated. */
	guint	num_items;	/* Logical vector length, e.g. # of items with content. */
	SMode	sort;		/* Mode to sort in. */
} MenuData;

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

typedef struct {			/* Holds information about system users (maps UIDs to names). */
	GHashTable	*dict_uname;		/* Hashes uid -> uname. */
	GHashTable	*dict_uhome;		/* Hashes uname -> uhome. */
	GHashTable	*dict_gname;		/* Hashes gid -> gname. */
} UsrInfo;

static UsrInfo	the_usr = { NULL, NULL, NULL };

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

static void visit(gpointer key, gpointer data, gpointer user)
{
	MenuData	*mnd = (MenuData *) user;
	Item		*base;

	if(mnd->num_items == mnd->num_alloc)	/* Time to grow the vector? */
	{
		mnd->num_alloc += (mnd->num_alloc == 0) ? MENU_INITIAL : MENU_CHUNK;
		base = g_realloc(mnd->item, mnd->num_alloc * sizeof *mnd->item);
		if(base != NULL)
			mnd->item = base;
		else
			fprintf(stderr, "**WARNING: User/group menu construction ran out of storage!\n");
	}
	mnd->item[mnd->num_items].id   = (int) key;
	mnd->item[mnd->num_items].text = (char *) data;
	mnd->num_items++;
}

static int qsort_cmp_id(const void *ap, const void *bp)
{
	Item	*ai = (Item *) ap, *bi = (Item *) bp;

	return ai->id - bi->id;
}

static void sort_items(MenuData *mnd)
{
	qsort(mnd->item, mnd->num_items, sizeof *mnd->item, qsort_cmp_id);
}

static GtkWidget * build_menu(GHashTable *tab, GtkSignalFunc func, gpointer user, gint id, gint *index)
{
	GtkWidget	*menu = NULL, *item;
	MenuData	mnd;
	gchar		label[64];
	guint		i;

	mnd.num_alloc = 0;
	mnd.num_items = 0;
	mnd.sort      = MNS_ID;
	mnd.item      = NULL;
	g_hash_table_foreach(tab, visit, &mnd);
	sort_items(&mnd);
	menu = gtk_menu_new();
	for(i = 0; i < mnd.num_items; i++)
	{
		g_snprintf(label, sizeof label, "%d %s", mnd.item[i].id, mnd.item[i].text);
		if(mnd.item[i].id == id && index != NULL)
			*index = i;
		if((item = gtk_menu_item_new_with_label(label)) != NULL)
		{
			gtk_object_set_user_data(GTK_OBJECT(item), (gpointer) mnd.item[i].id);
			gtk_signal_connect(GTK_OBJECT(item), "activate", func, user);
			gtk_menu_append(GTK_MENU(menu), item);
			gtk_widget_show(item);
		}
	}
	g_free(mnd.item);

	return menu;
}

/* 1998-06-08 -	Build a menu showing either user (type==UIM_USER) or group (type==UIM_GROUP) names,
**		and also fill in (a non-NULL) <index> with the menu row for <id>, if it exists.
*/
GtkWidget * usr_build_menu(UsrMenuType type, GtkSignalFunc func, gpointer user, int id, gint *index)
{
	switch(type)
	{
		case UIM_USER:
			return build_menu(the_usr.dict_uname, func, user, id, index);
		case UIM_GROUP:
			return build_menu(the_usr.dict_gname, func, user, id, index);
		default:
			fprintf(stderr, "**WARNING: usr_build_menu() called with illegal type parameter\n");
	}
	return NULL;
}

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

/* 1998-12-22 -	Build a hash table with system's password info in it. This might be very
**		stupid on large systems, in terms of memory usage...
*/
static void scan_uid(void)
{
	struct passwd	*pw;
	gchar		*old, *home, *name;

	setpwent();
	while((pw = getpwent()) != NULL)
	{
		if((old = g_hash_table_lookup(the_usr.dict_uname, GINT_TO_POINTER((int) pw->pw_uid))) != NULL)
		{
			if((home = g_hash_table_lookup(the_usr.dict_uhome, (gpointer) old)) != NULL)
			{
				g_hash_table_remove(the_usr.dict_uname, GINT_TO_POINTER((int) pw->pw_uid));
				g_hash_table_remove(the_usr.dict_uhome, old);
				free(old);
				free(home);
			}
			else
				fprintf(stderr, "USERINFO: passwd data for '%s' (%d) has no home dir!\n", old, (int) pw->pw_uid);
		}
		if((name = g_strdup(pw->pw_name)) != NULL)
		{
			if((home = g_strdup(pw->pw_dir)) != NULL)
			{
				g_hash_table_insert(the_usr.dict_uname, GINT_TO_POINTER((int) pw->pw_uid), (gpointer) name);
				g_hash_table_insert(the_usr.dict_uhome, (gpointer) name, (gpointer) home);
			}
			else
			{
				g_free(name);
				fprintf(stderr, "USERINFO: Couldn't duplicate home dir\n");
			}
		}
		else
			fprintf(stderr, "USERINFO: Couldn't duplicate user name string\n");
	}
	endpwent();
}

/* 1998-12-22 -	Parse some system-specific file containing group information (typically "/etc/group").
**		Store group names in a hash table indexed on group ids, for easy look-ups later.
*/
static void scan_gid(void)
{
	struct group	*gr;
	char		*old, *name;

	setgrent();
	while((gr = getgrent()) != NULL)
	{
		if((old = g_hash_table_lookup(the_usr.dict_gname, GINT_TO_POINTER((int) gr->gr_gid))) != NULL)
		{
			g_hash_table_remove(the_usr.dict_gname, GINT_TO_POINTER((int) gr->gr_gid));
			g_free(old);
		}
		if((name = g_strdup(gr->gr_name)) != NULL)
			g_hash_table_insert(the_usr.dict_gname, GINT_TO_POINTER((int) gr->gr_gid), name);
	}
	endgrent();
}

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

/* 1998-05-29 -	Rewritten & renamed. This routine looks up the name of the user with the given
**		<uid>. Returns pointer to string, or NULL if the user is unknown.
*/
const char * usr_lookup_uname(uid_t uid)
{
	return (const char *) g_hash_table_lookup(the_usr.dict_uname, GINT_TO_POINTER((int) uid));
}

/* 1998-12-22 -	Look up the home directory of a user called <uname>. Returns pointer to string,
**		or NULL if there is no such user. If <uname> is "" or NULL, returns the home
**		directory of the current effective user.
*/
const char * usr_lookup_uhome(const char *uname)
{
	if(uname == NULL || *uname == '\0')
	{
		if((uname = usr_lookup_uname(geteuid())) != NULL)
			return g_hash_table_lookup(the_usr.dict_uhome, (gpointer) uname);
	}
	return g_hash_table_lookup(the_usr.dict_uhome, (gpointer) uname);
}

/* 1998-05-29 -	Rewritten & renamed. This routine looks up the name of the group with the given
**		<gid>. Returns pointer to string, or NULL if the group is unknown.
*/
const char * usr_lookup_gname(uid_t gid)
{
	return g_hash_table_lookup(the_usr.dict_gname, GINT_TO_POINTER((int) gid));
}

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

static gint id_equal(gpointer a, gpointer b)
{
	return GPOINTER_TO_UINT(a) == GPOINTER_TO_UINT(b);
}

/* 1998-12-22 -	Initialize the userinfo module. Calling any of the functions exported from this
**		module without a prior call to usr_init() is illegal, and will likely crash.
**		Returns 1 on success, 0 on failure.
*/
int usr_init(void)
{
	if((the_usr.dict_uname = g_hash_table_new(g_direct_hash, (GCompareFunc) id_equal)) != NULL)
	{
		if((the_usr.dict_uhome = g_hash_table_new(g_str_hash, g_str_equal)) != NULL)
		{
			if((the_usr.dict_gname = g_hash_table_new(g_direct_hash, (GCompareFunc) id_equal)) != NULL)
			{
				scan_uid();
				scan_gid();
				return 1;
			}
			g_hash_table_destroy(the_usr.dict_uhome);
		}
		g_hash_table_destroy(the_usr.dict_uname);
	}
	return 0;
}
