/* $FreeBSD$ */
/* $Id: nfs4_idmap.c,v 1.3 2004/06/14 19:38:57 marius Exp $ */

/*
 * copyright (c) 2003
 * the regents of the university of michigan
 * all rights reserved
 * 
 * permission is granted to use, copy, create derivative works and redistribute
 * this software and such derivative works for any purpose, so long as the name
 * of the university of michigan is not used in any advertising or publicity
 * pertaining to the use or distribution of this software without specific,
 * written prior authorization.  if the above copyright notice or any other
 * identification of the university of michigan is included in any copy of any
 * portion of this software, then the disclaimer below must also be included.
 * 
 * this software is provided as is, without representation from the university
 * of michigan as to its fitness for any purpose, and without warranty by the
 * university of michigan of any kind, either express or implied, including
 * without limitation the implied warranties of merchantability and fitness for
 * a particular purpose. the regents of the university of michigan shall not be
 * liable for any damages, including special, indirect, incidental, or
 * consequential damages, with respect to any claim arising out of or in
 * connection with the use of the software, even if it has been or is hereafter
 * advised of the possibility of such damages.
 */

/* TODO: 
 *  o validate ascii 
 *  o pick an appropriate hash value when hash32_strn() is called. 
 * */

#include <sys/param.h>
#include <sys/kernel.h>
#include <sys/systm.h>
#include <sys/malloc.h>
#include <sys/proc.h>
#include <sys/syscall.h>


#ifdef __FreeBSD__ 
#include <sys/lockmgr.h>
#include <sys/fnv_hash.h>
#include <sys/sysent.h>
#include <sys/libkern.h>
#else
#include <sys/lock.h>
#include <libkern/libkern.h>
#endif

/* 
 * Fowler/Noll/Vo hash
 *    http://www.isthe.com/chongo/tech/comp/fnv/
 */

#define FNV_P_32 ((u_int32_t)0x01000193) /* 16777619 */
#define FNV_1_32 ((u_int32_t)0x811c9dc5) /* 2166136261 */

/* u_int32_t fnvhash32(void *, u_int32_t); */

#include <rpcx/rpcclnt.h>

#include <rpcx/rpc_dev.h>
#include <nfs4client/nfs4_idmap.h>
#include <nfs4client/nfs4_glue.h>

/* XXX HACK */
#if defined(__APPLE__)
#include <libkern/libkern.h>
#define strncmp(A,B,C) bcmp(A,B,C)
#endif


#ifdef IDMAPVERBOSE
#define IDMAP_DEBUG(...) printf(__VA_ARGS__);
#else
#define IDMAP_DEBUG(...)
#endif

#define IDMAP_HASH_SIZE 37

#ifdef __FreeBSD__
MALLOC_DEFINE(M_IDMAP, "idmap", "idmap");
#else
#define M_IDMAP M_TEMP
#endif

#define idmap_entry_get(ID) MALLOC((ID), struct idmap_entry, sizeof(struct idmap_entry), M_IDMAP, M_WAITOK | M_ZERO)
#define idmap_entry_put(ID) FREE((ID), M_IDMAP)

#define IDMAP_RLOCK(lock) lockmgr(lock, LK_SHARED, NULL, curcthread)
#define IDMAP_WLOCK(lock) lockmgr(lock, LK_EXCLUSIVE, NULL, curcthread)
#define IDMAP_UNLOCK(lock) lockmgr(lock, LK_RELEASE, NULL, curcthread)


struct idmap_entry {
	struct idmap_msg id_info;

	TAILQ_ENTRY(idmap_entry) id_entry_id;
	TAILQ_ENTRY(idmap_entry) id_entry_name;
};

struct idmap_hash {
	TAILQ_HEAD(, idmap_entry) hash_name[IDMAP_HASH_SIZE];
	TAILQ_HEAD(, idmap_entry) hash_id[IDMAP_HASH_SIZE];

#if __FreeBSD__
	struct lock hash_lock;
#elif __APPLE__
	struct lock__bsd__ hash_lock;
#endif
};



static struct idmap_hash idmap_uid_hash;
static struct idmap_hash idmap_gid_hash;

static struct idmap_entry * idmap_name_lookup(uint32_t, char *);
static struct idmap_entry * idmap_id_lookup(uint32_t, ident_t);
static int idmap_upcall_name(uint32_t, char *, struct idmap_entry **);
static int idmap_upcall_id(uint32_t , ident_t, struct idmap_entry ** );
static int idmap_add(struct idmap_entry *);

static u_int32_t
fnvhash32(void *buf, u_int32_t buflen)
{
        u_char *p, *end = (u_char *)buf + buflen;
        u_int32_t hash = FNV_1_32;

        for (p = buf; p < end; p++) {
                hash *= FNV_P_32;
                hash ^= (u_int32_t)*p;
        }

        return (hash);
}

static int
idmap_upcall_name(uint32_t type, char * name, struct idmap_entry ** found)
{
	int error;
	struct idmap_entry * e;
	size_t len, siz;

	if (type > IDMAP_MAX_TYPE || type == 0) {
		IDMAP_DEBUG("bad type %d\n", type);
	 	return EINVAL; /* XXX */ 
	}

	if (name == NULL || (len = strlen(name)) == 0 || len > IDMAP_MAXNAMELEN) {
		IDMAP_DEBUG("idmap_upcall_name: bad name\n");
		return EFAULT;	/* XXX */
	}

	MALLOC(e, struct idmap_entry *, sizeof(struct idmap_entry), M_IDMAP,
	    M_WAITOK | M_ZERO);

	e->id_info.id_type = type;
	bcopy(name, e->id_info.id_name, len); 
	e->id_info.id_namelen = len;


	siz = sizeof(struct idmap_msg);
	error = rpcdev_call(RPCDEV_TYPE_IDMAP, (caddr_t)&e->id_info, siz, 
	    (caddr_t)&e->id_info, &siz, 0);

	if (error) {
		IDMAP_DEBUG("error %d in rpcdev_upcall()\n", error);
		*found = NULL;
		return error;
	}

	if (siz != sizeof(struct idmap_msg)) {
	  	IDMAP_DEBUG("bad size of returned message\n");
		*found = NULL;
		return EFAULT;
	}

		
	*found = e;
	return 0;
}

static int
idmap_upcall_id(uint32_t type, ident_t id, struct idmap_entry ** found)
{
	int error;
	struct idmap_entry * e;
	size_t siz;

	if (type > IDMAP_MAX_TYPE)
	 	panic("bad type"); /* XXX */ 

	MALLOC(e, struct idmap_entry *, sizeof(struct idmap_entry), M_IDMAP,
	    M_WAITOK | M_ZERO);

	e->id_info.id_type = type;
	e->id_info.id_namelen = 0;	/* should already */
	e->id_info.id_id = id;

	siz = sizeof(struct idmap_msg);
	error = rpcdev_call(RPCDEV_TYPE_IDMAP, (caddr_t)&e->id_info, siz, 
	    (caddr_t)&e->id_info, &siz, 0);

	if (error) {
		IDMAP_DEBUG("error %d in rpcdev_upcall()\n", error);
		*found = NULL;
		return error;
	}

	if (siz != sizeof(struct idmap_msg)) {
	  	IDMAP_DEBUG("bad size of returned message\n");
		*found = NULL;
		return EFAULT;
	}

	*found = e;
	return 0;
}

static int 
idmap_hashf(struct idmap_entry *e, uint32_t * hval_id, uint32_t * hval_name) 
{
	switch (e->id_info.id_type) {
                case IDMAP_TYPE_UID:
                        *hval_id = e->id_info.id_id.uid % IDMAP_HASH_SIZE;
                break;
                case IDMAP_TYPE_GID:
                        *hval_id = e->id_info.id_id.gid % IDMAP_HASH_SIZE;
                break;
                default:
			return (EINVAL);
                break;
        }

	if (e->id_info.id_namelen == 0 || strlen(e->id_info.id_name) == 0) 
		return (EINVAL);

	*hval_name = fnvhash32(e->id_info.id_name, e->id_info.id_namelen) % IDMAP_HASH_SIZE;
	return 0;
}

static int
idmap_add(struct idmap_entry * e)
{
	struct idmap_hash * hash;
        uint32_t hval_id, hval_name;
	int error;

	if (e == NULL)
	  panic("idmap_add null");

	if (e->id_info.id_namelen == 0)
	  panic("idmap_add name of len 0");

        switch (e->id_info.id_type) {
                case IDMAP_TYPE_UID:
                        hash = &idmap_uid_hash;
                break;
                case IDMAP_TYPE_GID:
                        hash = &idmap_gid_hash;
                break;
                default:
                        /* XXX yikes */
                        panic("idmap add: bad type!");
                break;
        }

	error = idmap_hashf(e, &hval_id, &hval_name);

	if (error) {
		IDMAP_DEBUG("idmap_hashf fails! %d\n", error);
		return error;
	}

	IDMAP_WLOCK(&hash->hash_lock);

	/* XXX */
	TAILQ_INSERT_TAIL(&hash->hash_id[hval_id], e, id_entry_id);
	TAILQ_INSERT_TAIL(&hash->hash_name[hval_name], e, id_entry_name);

	IDMAP_UNLOCK(&hash->hash_lock);

	return 0;
}

static struct idmap_entry * 
idmap_id_lookup(uint32_t type, ident_t id)
{
	struct idmap_hash * hash;
	uint32_t hval;
	struct idmap_entry * e;

	switch (type) {
		case IDMAP_TYPE_UID:
	 		hash = &idmap_uid_hash; 
			hval = id.uid % IDMAP_HASH_SIZE;
		break;
		case IDMAP_TYPE_GID:
	 		hash = &idmap_gid_hash; 
			hval = id.gid % IDMAP_HASH_SIZE;
		break;
		default:
			/* XXX yikes */
			panic("lookup: bad type!");	
		break;
	}
	

	IDMAP_RLOCK(&hash->hash_lock);

	TAILQ_FOREACH(e, &hash->hash_id[hval], id_entry_name) {
	  	if ((type == IDMAP_TYPE_UID && e->id_info.id_id.uid == id.uid)||
		    (type == IDMAP_TYPE_GID  && e->id_info.id_id.gid == id.gid)) {
			IDMAP_UNLOCK(&hash->hash_lock);
			return e;
		}
	}

	IDMAP_UNLOCK(&hash->hash_lock);
	return NULL;
}

static struct idmap_entry * 
idmap_name_lookup(uint32_t type, char * name)
{
	struct idmap_hash * hash;
	uint32_t hval;
	struct idmap_entry * e;
	size_t len;

	switch (type) {
	case IDMAP_TYPE_UID:
	 	hash = &idmap_uid_hash; 
	break;
	case IDMAP_TYPE_GID:
	 	hash = &idmap_gid_hash; 
	break;
	default:
		/* XXX yikes */
		panic("lookup: bad type!");	
	break;
	}
	
	len = strlen(name);

	if (len == 0 || len > IDMAP_MAXNAMELEN) {
		IDMAP_DEBUG("bad name length %d\n", len);
		return NULL;
	}

	hval = fnvhash32(name, len) % IDMAP_HASH_SIZE;

	IDMAP_RLOCK(&hash->hash_lock);

	TAILQ_FOREACH(e, &hash->hash_name[hval], id_entry_name) {
		if ((strlen(e->id_info.id_name) == strlen(name)) && strncmp(e->id_info.id_name, name, strlen(name)) == 0) {
			IDMAP_UNLOCK(&hash->hash_lock);
			return e;
		}
	}

	IDMAP_UNLOCK(&hash->hash_lock);
	return NULL;
}

void 
idmap_init(void)
{
	unsigned int i;

	for (i=0; i<IDMAP_HASH_SIZE; i++) {
		TAILQ_INIT(&idmap_uid_hash.hash_name[i]);
		TAILQ_INIT(&idmap_uid_hash.hash_id[i]);

		TAILQ_INIT(&idmap_gid_hash.hash_name[i]);
		TAILQ_INIT(&idmap_gid_hash.hash_id[i]);
	}

	lockinit(&idmap_uid_hash.hash_lock, PLOCK, "idmap uid hash table", 0,0);
	lockinit(&idmap_gid_hash.hash_lock, PLOCK, "idmap gid hash table", 0,0);

}

void idmap_uninit(void)
{
  	struct idmap_entry * e;
	int i;

#if defined(__FreeBSD__)
	lockdestroy(&idmap_uid_hash.hash_lock);
	lockdestroy(&idmap_gid_hash.hash_lock);
#else
	lockmgr(&idmap_uid_hash.hash_lock, LK_DRAIN, NULL, curcthread);
	lockmgr(&idmap_gid_hash.hash_lock, LK_DRAIN, NULL, curcthread);
#endif

	for (i=0; i<IDMAP_HASH_SIZE; i++) {
		while(!TAILQ_EMPTY(&idmap_uid_hash.hash_name[i])) {
			e = TAILQ_FIRST(&idmap_uid_hash.hash_name[i]);
			TAILQ_REMOVE(&idmap_uid_hash.hash_name[i], e, id_entry_name);
			TAILQ_REMOVE(&idmap_uid_hash.hash_id[i], e, id_entry_id);
			FREE(e, M_IDMAP);
		}

		while(!TAILQ_EMPTY(&idmap_gid_hash.hash_name[i])) {
			e = TAILQ_FIRST(&idmap_gid_hash.hash_name[i]);
			TAILQ_REMOVE(&idmap_gid_hash.hash_name[i], e, id_entry_name);
			TAILQ_REMOVE(&idmap_gid_hash.hash_id[i], e, id_entry_id);
			FREE(e, M_IDMAP);
		}

	}
}

int 
idmap_uid_to_name(uid_t uid, char ** name, size_t * len)
{
  	struct idmap_entry * e;
	int error = 0;
	ident_t id;

	id.uid = uid;


	if ((e = idmap_id_lookup(IDMAP_TYPE_UID, id)) == NULL) {
	  	if ((error = idmap_upcall_id(IDMAP_TYPE_UID, id, &e)) != 0) {
			IDMAP_DEBUG("error in upcall\n");
			return error;
		}

		if (e == NULL) {
			IDMAP_DEBUG("no error from upcall, but no data returned\n");
			return EFAULT;
		}

		if (idmap_add(e) != 0) {
			IDMAP_DEBUG("idmap_add failed\n");
			FREE(e, M_IDMAP);
		}
	}

	*name = e->id_info.id_name;
	*len = e->id_info.id_namelen;
	return 0;
}

int 
idmap_gid_to_name(gid_t gid, char ** name, size_t * len)
{
  	struct idmap_entry * e;
	int error = 0;
	ident_t id;
	
	id.gid = gid;


	if ((e = idmap_id_lookup(IDMAP_TYPE_GID, id)) == NULL) {
	  	if ((error = idmap_upcall_id(IDMAP_TYPE_GID, id, &e))) {
			IDMAP_DEBUG("error in upcall\n");
			return error;
		}

		if (e == NULL) {
			IDMAP_DEBUG("no error from upcall, but no data returned\n");
			return EFAULT;
		}

		if (idmap_add(e) != 0) {
			IDMAP_DEBUG("idmap_add failed\n");
			FREE(e, M_IDMAP);
		}
	}

	*name = e->id_info.id_name;
	*len  = e->id_info.id_namelen;
	return 0;
}

int 
idmap_name_to_uid(char * name, size_t len, uid_t * id)
{
  	struct idmap_entry * e;
	int error = 0;
	char * namestr;

	if (name == NULL )
		return EFAULT;

	if (len == 0 || len > IDMAP_MAXNAMELEN) {
	  	IDMAP_DEBUG("idmap_name_to_uid: bad len\n");
	  	return EINVAL;
	}

	/* XXX hack */
	MALLOC(namestr, char *, len + 1, M_TEMP, M_WAITOK);
	bcopy(name, namestr, len);
	namestr[len] = '\0';


	if ((e = idmap_name_lookup(IDMAP_TYPE_UID, namestr)) == NULL) {
	  	if ((error = idmap_upcall_name(IDMAP_TYPE_UID, namestr, &e))) {
			FREE(namestr, M_TEMP);
			return error;
		}

		if (e == NULL) {
			IDMAP_DEBUG("no error from upcall, but no data returned\n");
			FREE(namestr, M_TEMP);
			return EFAULT;
		}

		if (idmap_add(e) != 0) {
			IDMAP_DEBUG("idmap_add failed\n");
			FREE(e, M_IDMAP);
		}
	}

	*id = e->id_info.id_id.uid;
	FREE(namestr, M_TEMP);
	return 0;
}

int 
idmap_name_to_gid(char * name, size_t len, gid_t * id)
{
  	struct idmap_entry * e;
	int error = 0;

	char * namestr;

	if (name == NULL )
		return EFAULT;

	if (len == 0 || len > IDMAP_MAXNAMELEN) {
	  	IDMAP_DEBUG("idmap_name_to_uid: bad len\n");
	  	return EINVAL;
	}

	/* XXX hack */
	MALLOC(namestr, char *, len + 1, M_TEMP, M_WAITOK);
	bcopy(name, namestr, len);
	namestr[len] = '\0';


	if ((e = idmap_name_lookup(IDMAP_TYPE_GID, namestr)) == NULL) {
	  	if ((error = idmap_upcall_name(IDMAP_TYPE_GID, namestr, &e)) != 0) {
			IDMAP_DEBUG("error in upcall\n");
			FREE(namestr, M_TEMP);
			return error;
		}

		if (e == NULL) {
			IDMAP_DEBUG("no error from upcall, but no data returned\n");
			FREE(namestr, M_TEMP);
			return EFAULT;
		}

		if (idmap_add(e) != 0) {
			IDMAP_DEBUG("idmap_add failed\n");
			FREE(e, M_IDMAP);
		}
	}

	*id = e->id_info.id_id.gid;
	FREE(namestr, M_TEMP);
	return 0;
}
