/*
 *  linux/fs/ifs/namei.c
 *
 *  Written 1992,1993 by Werner Almesberger
 */

#include <asm/segment.h>

#include <linux/ifs_fs.h>
#include <linux/kernel.h>
#include <linux/errno.h>
#include <linux/stat.h>


/*
 * Beware: DIR may be locked by the caller.
 */

int ifs_lookup(struct inode *dir,const char *name,int len,
    struct inode **result)
{
	struct inode *temp[IFS_MAX_LAYERS];
	dev_t dev;
	int error,none,n,is_new,status;

Dprintk("ifs_lookup\n");
	if (len == 1 && name[0] == '.') {
		*result = dir;
		return 0;
	}
	if (len == 2 && name[0] == '.' && name[1] == '.')
	    {
		if (!(*result = IFS_I(dir)->parent)) { /* root */
			*result = dir;
			return 0;
		}
		USE_INODE(*result);
		iput(dir);
		return 0;
	}
	if (IFS_DEL_NAME(name,len) || ((status = ifs_status(IFS_NTH(dir,0),name,
	    len)) & IFS_ST_WHITEOUT)) {
Dprintk("deleted\n");
		iput(dir);
		return -ENOENT;
	}
	error = -ENOENT;
	none = 1;
	for (n = 0; n < (status & IFS_ST_HIDE ? 1 : IFS_LAYERS(dir)); n++)
		if (!IFS_CAN_INODE(dir,n,lookup))
			temp[n] = NULL;
		else {
			dev = IFS_NTH(dir,n)->i_dev;
			USE_INODE(IFS_NTH(dir,n));
			error = IFS_DO_INODE(dir,n,lookup,(IFS_NTH(dir,n),name,
			    len,&temp[n]));
			  /* RC1: files may be added or removed to or from
			     upper layers while we scan lower layers */
			if (error) temp[n] = NULL;
			else if (temp[n]->i_dev == dev)
					none = 0;
				else {
					iput(temp[n]);
					temp[n] = NULL;
					error = -ENOENT;
				}
		}
	while (n < IFS_LAYERS(dir)) temp[n++] = NULL;
	/* RC1 yields bogus inode */
	if (none) {
		iput(dir);
		return error;
	}
	if (!(*result = ifs_iget(dir->i_sb,temp,IFS_LAYERS(dir),&is_new))) {
		iput(dir);
		return -EACCES;
	}
	if (!is_new)
		iput(dir);
	else IFS_I(*result)->parent = dir;
Dprintk("ifs_lookup done\n");
	return 0;
}


static struct inode_operations dummy_ops;


static int ifs_creop(off_t op,struct inode *dir,const char *name,int len,
    struct inode *link,struct inode **result,int p1,int p2)
{
	struct inode *old;
	int retval;

Dprintk("ifs_creop: op %d\n",op);
	ifs_lock(dir);
	if (IFS_IS_RO(dir)) {
		ifs_unlock(dir);
		iput(dir);
		return -EROFS;
	}
	if (IFS_DEL_NAME(name,len)) {
		ifs_unlock(dir);
		iput(dir);
		return -EINVAL;
	}
	USE_INODE(dir);
	if (!dir->i_op->lookup(dir,name,len,&old)) {
		ifs_unlock(dir);
		iput(dir);
		iput(old);
		return -EEXIST;
	}
	retval = ifs_build_path(dir);
	if (retval) {
		ifs_unlock(dir);
		iput(dir);
		return retval;
	}
	if (!IFS_NTH(dir,0)->i_op || !(int) (((char *) dir->i_op)+op) ||
	    !dir->i_op || !dir->i_op->lookup) {
		ifs_unlock(dir);
		iput(dir);
		return -EBADF;
	}
	retval = ifs_unwhiteout(dir,name,len);
	if (retval) {
		ifs_unlock(dir);
		iput(dir);
		return retval;
	}
	USE_INODE(dir);
	if (dir->i_op->lookup(dir,name,len,&old))
		old = NULL;
	else {
		if (IFS_NTH(old,0)) {
			(void) ifs_whiteout(dir,name,len);
			ifs_unlock(dir);
			iput(dir);
			iput(old);
			return -EEXIST;
		}
		ifs_lock(old);
	}
	USE_INODE(IFS_NTH(dir,0));
	if (link) {
		USE_INODE(link);
		retval = IFS_DO_INODE(dir,0,link,(link,IFS_NTH(dir,0),name,
		    len));
	}
	else retval = ((int (*)(struct inode *,const char *,int,int,int))
		    *(int *) ((char *) IFS_NTH(dir,0)->i_op+op))(IFS_NTH(dir,0),
		    name,len,p1,p2);
	if (retval) {
		if (old) {
			(void) ifs_whiteout(dir,name,len);
			ifs_unlock(old);
			iput(old);
		}
		ifs_unlock(dir);
		iput(dir);
		return retval;
	}
	if (!result) {
		if (old) {
			if (old && op == IFS_IOP_OFF(mkdir))
				retval = ifs_hide(dir,name,len);
			ifs_unlock(old);
			iput(old);
		}
		ifs_unlock(dir);
		iput(dir);
		return retval;
	}
	if (old) {
		*result = old;
		IFS_NTH(old,0) = *(struct inode **) p2;
		ifs_adjust_ops(old);
		ifs_unlock(old);
		ifs_unlock(dir);
		iput(dir);
		return 0;
	}
	iput(*(struct inode **) p2);
	ifs_unlock(dir);
	return dir->i_op->lookup(dir,name,len,result);
}


int ifs_create(struct inode *dir,const char *name,int len,int mode,
    struct inode **result)
{
	struct inode scratch;

	return ifs_creop(IFS_IOP_OFF(create),dir,name,len,NULL,result,
	    mode,(int) &scratch);
}


int ifs_mkdir(struct inode *dir,const char *name,int len,int mode)
{
	return ifs_creop(IFS_IOP_OFF(mkdir),dir,name,len,NULL,NULL,mode,0);
}


int ifs_symlink(struct inode *inode,const char *name,int len,
    const char *symname)
{
	if (len == 3 && get_fs_byte(name) == '.' && get_fs_byte(name+1) == '.'
	    && get_fs_byte(name+2) == '.') {
		iput(inode);
		return -EINVAL;
	}
	return ifs_creop(IFS_IOP_OFF(symlink),inode,name,len,NULL,NULL,
	    (int) symname,0);
}


int ifs_link(struct inode *oldinode,struct inode *dir,const char *name,int len)
{
	printk("IFS: ifs_link not yet implemented\n");
	return -EINVAL;
#if 0
	retval = ifs_creop(IFS_IOP_OFF(link),dir,name,len,oldinode,NULL,0,0);
	iput(link);
	return retval;
#endif
}


int ifs_mknod(struct inode *dir,const char *name,int len,int mode,int rdev)
{
	return ifs_creop(IFS_IOP_OFF(mknod),dir,name,len,NULL,NULL,mode,rdev);
}



static int ifs_remove(int rmdir,struct inode *dir,const char *name,int len)
{
	struct inode *item;
	int retval,empty,n;

	ifs_lock(dir);
	if (IFS_IS_RO(dir)) {
		ifs_unlock(dir);
		iput(dir);
		return -EROFS;
	}
	if (IFS_DEL_NAME(name,len)) {
		ifs_unlock(dir);
		iput(dir);
		return -ENOENT;
	}
	if (!dir->i_op || !dir->i_op->lookup)
		panic("i_op->lookup missing");
	USE_INODE(dir);
	retval = dir->i_op->lookup(dir,name,len,&item);
	if (retval) {
		ifs_unlock(dir);
		iput(dir);
		return retval;
	}
	ifs_lock(item);
	if (rmdir) {
		if (!ifs_empty(item,1))
			retval = -ENOTEMPTY;
		if (!retval && IFS_NTH(item,0) && !ifs_empty(IFS_NTH(item,0),0))
			retval = ifs_purge(IFS_NTH(item,0));
		  /* Bug: whited out entries may become visible for a moment */
		if (!retval && item->i_count > 1)
			retval = -EBUSY;
		if (!retval)
			retval = ifs_unhide(dir,name,len);
		ifs_unlock(item);
		iput(item);
		if (!retval) {
			USE_INODE(dir);
			retval = dir->i_op->lookup(dir,name,len,&item);
		}
		if (retval) {
			ifs_unlock(dir);
			iput(dir);
			return retval;
		}
		ifs_lock(item);
		if (IFS_NTH(item,0))
			iput(IFS_NTH(item,0));
		IFS_NTH(item,0) = NULL;
	}
	if (rmdir ? IFS_CAN_INODE(dir,0,rmdir) : IFS_CAN_INODE(dir,0,unlink)) {
		USE_INODE(IFS_NTH(dir,0));
		retval = rmdir ? IFS_DO_INODE(dir,0,rmdir,(IFS_NTH(dir,0),name,
		    len)) : IFS_DO_INODE(dir,0,unlink,(IFS_NTH(dir,0),name,
		    len));
		if (retval && retval != -ENOENT) { /* detect race condition */
			ifs_unlock(item);
			iput(item);
			ifs_unlock(dir);
			iput(dir);
			return retval;
		}
		if (!retval && IFS_NTH(item,0)) {
			iput(IFS_NTH(item,0));
			IFS_NTH(item,0) = NULL;
		}
	}
	empty = 1;
	for (n = 1; n < IFS_LAYERS(dir); n++)
		if (IFS_NTH(item,n)) {
			empty = 0;
			if (!rmdir && S_ISDIR(IFS_NTH(item,n)->i_mode)) {
				ifs_unlock(item);
				iput(item);
				ifs_unlock(dir);
				iput(dir);
				return -EPERM;
			}
		}
	ifs_unlock(item);
	iput(item);
	if (empty)
		retval = 0;
	else retval = ifs_whiteout(dir,name,len);
	ifs_unlock(dir);
	iput(dir);
	return retval;
}


int ifs_rmdir(struct inode *dir,const char *name,int len)
{
	return ifs_remove(1,dir,name,len);
}


int ifs_unlink(struct inode *dir,const char *name,int len)
{
	return ifs_remove(0,dir,name,len);
}


static int do_rename(struct inode *old_dir,const char *old_name,int old_len,
    struct inode *new_dir,const char *new_name,int new_len)
{
	struct inode *old,*_new,*ifs_dir;
	int retval,n,copy;

	if (IFS_IS_RO(new_dir))
		return -EROFS;
	if (IFS_DEL_NAME(old_name,old_len) || IFS_DEL_NAME(new_name,new_len))
		retval = -EINVAL;
	if (!old_dir->i_op || !old_dir->i_op->lookup)
		return -EBADF;
	USE_INODE(old_dir);
	if (old_dir->i_op->lookup(old_dir,old_name,old_len,&old))
		return -ENOENT;
	ifs_lock(new_dir);
	retval = ifs_build_path(new_dir);
	if (retval) {
		ifs_unlock(new_dir);
		iput(old);
		return retval;
	}
	if (!new_dir->i_op || !new_dir->i_op->lookup)
		return -EBADF;
	USE_INODE(new_dir);
	if (new_dir->i_op->lookup(new_dir,new_name,new_len,&_new))
		_new = NULL;
	ifs_unlock(new_dir);
	ifs_lock4(old_dir,new_dir,old,_new);
	if (!IFS_NTH(new_dir,0)) {
		ifs_unlock4(old_dir,new_dir,old,_new);
		iput(old);
		return -ENOENT; /* should be -ERACE :-) */
	}
	for (n = 0; n < IFS_LAYERS(old); n++)
		if (IFS_NTH(old_dir,n) && IFS_NTH(old,n)) break;
	if (n == IFS_LAYERS(old)) {
		ifs_unlock4(old_dir,new_dir,old,_new);
		iput(old);
		return -ENOENT;
	}
	copy = n || IFS_NTH(old,0)->i_dev != IFS_NTH(new_dir,0)->i_dev;
	if (copy) {
		/* copy file to target/... and rename it later because rename
		   must be atomic. */
		if (!IFS_CAN_INODE(new_dir,0,mkdir) || !IFS_CAN_INODE(new_dir,
		    0,lookup))
			retval = -EBADF;
		else {
			USE_INODE(IFS_NTH(new_dir,0));
			retval = IFS_VAL_INODE(new_dir,0,mkdir,(IFS_NTH(new_dir,
			    0),"...",3,new_dir->i_mode & 0666));
		}
		if (retval == -EEXIST)
			retval = 0;
		if (!retval) {
			USE_INODE(IFS_NTH(new_dir,0));
			retval = IFS_VAL_INODE(new_dir,0,lookup,
			    (IFS_NTH(new_dir,0),"...",3,&ifs_dir));
		}
		if (!retval) {
			retval = ifs_copy_object(IFS_NTH(old,n),ifs_dir,"...",
			    3);
			if (!retval && (!ifs_dir->i_op || !ifs_dir->i_op->
			    rename)) {
				iput(ifs_dir);
				retval = -EBADF;
			}
			if (!retval) {
				USE_INODE(IFS_NTH(new_dir,0));
				retval = ifs_dir->i_op->rename(ifs_dir,"...",3,
				    IFS_NTH(new_dir,0),new_name,new_len);
			}
		}
	}
	else {
		USE_INODE(IFS_NTH(old_dir,0));
		USE_INODE(IFS_NTH(new_dir,0));
		retval = IFS_VAL_INODE(old_dir,n,rename,(IFS_NTH(old_dir,n),
		    old_name,old_len,IFS_NTH(new_dir,0),new_name,new_len));
	}
	if (retval) {
		ifs_unlock4(old_dir,new_dir,old,_new);
		if (_new)
			iput(_new);
		iput(old);
		return retval;
	}
	if (copy) {
		if (IFS_NTH(old,0)) {
			IFS_OP_INODE(old,0,unlink,(IFS_NTH(old,0),old_name,
			    old_len)); /* get rid of old,0 */
			IFS_NTH(old,0) = NULL;
		}
	}
	else {
		if (!_new)
			iput(IFS_NTH(old,0));
		else {
			if (IFS_NTH(_new,0))
				iput(IFS_NTH(_new,0));
			IFS_NTH(_new,0) = IFS_NTH(old,0);
		}
		IFS_NTH(old,0) = NULL;
	}
	(void) ifs_unwhiteout(new_dir,new_name,new_len);
	for (n = 0; n < IFS_LAYERS(old); n++)
		if (IFS_NTH(old,n)) break;
	if (n != IFS_LAYERS(old))
		(void) ifs_whiteout(old_dir,old_name,old_len);
	ifs_unlock4(old_dir,new_dir,old,_new);
	if (_new)
		iput(_new);
	iput(old);
	return 0;
}


int ifs_rename(struct inode *old_dir,const char *old_name,int old_len,
    struct inode *new_dir,const char *new_name,int new_len)
{
	int retval;

	retval = do_rename(old_dir,old_name,old_len,new_dir,new_name,new_len);
	iput(old_dir);
	iput(new_dir);
	return retval;
}
