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

#include <asm/segment.h>

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


void ifs_lock(struct inode *inode)
{
	while (IFS_I(inode)->trans_lock)
		sleep_on(&IFS_I(inode)->trans_wait);
	IFS_I(inode)->trans_lock = 1;
}


void ifs_unlock(struct inode *inode)
{
	if (!IFS_I(inode)->trans_lock)
		printk("ifs_unlock: not locked\n");
	IFS_I(inode)->trans_lock = 0;
	wake_up(&IFS_I(inode)->trans_wait);
}


static void reduce(struct inode **i1,struct inode **i2,struct inode **i3,
    struct inode **i4)
{
	if (*i1 == *i2) *i2 = NULL;
	if (*i1 == *i3) *i3 = NULL;
	if (*i1 == *i4) *i4 = NULL;
	if (*i2 == *i3) *i3 = NULL;
	if (*i2 == *i4) *i4 = NULL;
	if (*i3 == *i4) *i4 = NULL;
}


#define IFS_SLEEP(i) \
    if (i && IFS_I(i)->trans_lock) sleep_on(&IFS_I(i)->trans_wait);
#define IFS_LOCK(i) if (i) IFS_I(i)->trans_lock = 1;
#define IFS_PROBE(i) (!i || !IFS_I(i)->trans_lock)
#define IFS_UNLOCK(i) \
    if (i) { IFS_I(i)->trans_lock = 0; wake_up(&IFS_I(i)->trans_wait); }


/*
 * This is atomic locking of up to four inodes at the same time. Unused inodes
 * are specified by passing the NULL pointer.
 */

void ifs_lock4(struct inode *i1,struct inode *i2,struct inode *i3,
    struct inode *i4)
{
	reduce(&i1,&i2,&i3,&i4);
	while (1) {
		IFS_SLEEP(i1);
		IFS_SLEEP(i2);
		IFS_SLEEP(i3);
		IFS_SLEEP(i4);
		if (IFS_PROBE(i1) && IFS_PROBE(i2) && IFS_PROBE(i3) &&
		    IFS_PROBE(i4)) {
			IFS_LOCK(i1);
			IFS_LOCK(i2);
			IFS_LOCK(i3);
			IFS_LOCK(i4);
			return;
		}
	}
}


void ifs_unlock4(struct inode *i1,struct inode *i2,struct inode *i3,
    struct inode *i4)
{
	reduce(&i1,&i2,&i3,&i4);
	IFS_UNLOCK(i1);
	IFS_UNLOCK(i2);
	IFS_UNLOCK(i3);
	IFS_UNLOCK(i4);
}


static void cvt_blocks(struct inode *dest,struct inode *src)
{
	dest->i_blksize = 512;
	dest->i_blocks = (src->i_blksize*src->i_blocks+511) >> 9;
}


#define CCI(d,s,e) (d)->i_##e = (s)->i_##e

#define COPY_CORRESPONDING(d,s) \
  (CCI(d,s,mode), CCI(d,s,nlink), CCI(d,s,uid), CCI(d,s,gid), CCI(d,s,rdev), \
  CCI(d,s,size), CCI(d,s,atime), CCI(d,s,mtime), CCI(d,s,ctime), \
  cvt_blocks(d,s))


/*
 * Does not consume ifs_inode. Does not make assumptions about locking of
 * IFS_INODE.
 */

void ifs_adjust_ops(struct inode *ifs_inode)
{
	struct inode *use;
	int n;

	use = NULL;
	for (n = 0; n < IFS_LAYERS(ifs_inode); n++)
		if (IFS_NTH(ifs_inode,n)) {
			use = IFS_NTH(ifs_inode,n);
			break;
		}
	if (!use)
		panic("ifs_adjust_ops: fell through");
	     if (S_ISREG(use->i_mode))
		ifs_inode->i_op = &ifs_file_inode_operations;
	else if (S_ISDIR(use->i_mode))
		ifs_inode->i_op = &ifs_dir_inode_operations;
	else if (S_ISLNK(use->i_mode))
		ifs_inode->i_op = &ifs_symlink_inode_operations;
	else if (S_ISCHR(use->i_mode))
		ifs_inode->i_op = &chrdev_inode_operations;
	else if (S_ISBLK(use->i_mode))
		ifs_inode->i_op = &blkdev_inode_operations;
	else if (S_ISFIFO(use->i_mode))
		init_fifo(ifs_inode);
	else ifs_inode->i_op = NULL; /* sockets or something similar */
}


void ifs_init_inode(struct inode *ifs_inode,struct inode **layers)
{
	int n;

Dprintk("ifs_init_inode\n");
	memcpy((char *) IFS_I(ifs_inode)->layer,layers,IFS_LAYERS(ifs_inode)*
	    sizeof(struct inode *));
	for (n = 0; n < IFS_LAYERS(ifs_inode); n++)
		if (IFS_NTH(ifs_inode,n)) {
			COPY_CORRESPONDING(ifs_inode,
			    IFS_NTH(ifs_inode,n));
			IFS_I(ifs_inode)->val_uid = IFS_NTH(ifs_inode,n)->i_uid;
			IFS_I(ifs_inode)->val_gid = IFS_NTH(ifs_inode,n)->i_gid;
			IFS_I(ifs_inode)->val_mode = IFS_NTH(ifs_inode,n)->
			    i_mode;
			IFS_I(ifs_inode)->val_size = IFS_NTH(ifs_inode,n)->
			    i_size;
			break;
		}
	ifs_adjust_ops(ifs_inode);
}


struct inode *ifs_iget(struct super_block *sb,struct inode **res,int layers,
    int *is_new)
{
	struct inode *scan,*inode;
	ino_t current;
	int size;

	while (IFS_SB(sb)->ino_lock)
		sleep_on(&IFS_SB(sb)->ino_wait);
	IFS_SB(sb)->ino_lock = 1;
	size = sizeof(struct inode *)*(layers-1);
	for (scan = IFS_SB(sb)->ifs_inodes; scan; scan = IFS_I(scan)->next) {
Dprintk("iget: checking inode %d\n",scan->i_ino);
		if (scan->i_dev == sb->s_dev && !memcmp((char *) &IFS_I(scan)->
		    layer[1],&res[1],size) && (IFS_I(scan)->layer[0] ==
		    res[0] || IFS_I(scan)->pop_lock))
			break;
}
	if (scan) {
		IFS_SB(sb)->ino_lock = 0;
		wake_up(&IFS_SB(sb)->ino_wait);
		USE_INODE(scan);
		while (IFS_I(scan)->pop_lock)
			sleep_on(&IFS_I(scan)->pop_wait);
		*is_new = 0;
Dprintk("iget: old ino = %d\n",scan->i_ino);
		return scan;
	}
	do {
		current = IFS_SB(sb)->ifs_ino;
		IFS_SB(sb)->ifs_ino = IFS_SB(sb)->ifs_ino >= IFS_MAX_INO ?
		    IFS_MIN_INO : IFS_SB(sb)->ifs_ino+1;
		for (scan = IFS_SB(sb)->ifs_inodes; scan; scan = IFS_I(scan)->
		    next)
			if (scan->i_dev == sb->s_dev && scan->i_ino == current)
				break;
	}
	while (scan);
	if (!(inode = iget(sb,current))) {
		IFS_SB(sb)->ino_lock = 0;
		wake_up(&IFS_SB(sb)->ino_wait);
		return NULL;
	}
	IFS_I(inode)->next = IFS_SB(sb)->ifs_inodes;
	IFS_I(inode)->prev = NULL;
	if (IFS_SB(sb)->ifs_inodes)
		IFS_I(IFS_SB(sb)->ifs_inodes)->prev = inode;
	IFS_SB(sb)->ifs_inodes = inode;
	*is_new = 1;
	ifs_init_inode(inode,res);
	IFS_SB(sb)->ino_lock = 0;
	wake_up(&IFS_SB(sb)->ino_wait);
Dprintk("iget: new ino = %d\n",inode->i_ino);
	return inode;
}


/*
 * Does not swallow DIR. DIR may or may not be locked by the caller.
 */

int ifs_status(struct inode *dir,const char *name,int len)
{
	struct inode *spec_inode,*inode;
	int found,status;

Dprintk("ifs_is_deleted (%x,%d)\n",(int) dir, dir ? dir->i_ino : 0);
	if (!dir)
		return IFS_ST_NORMAL;
	if (!dir->i_op || !dir->i_op->lookup) panic("iid dir...\n");
	USE_INODE(dir);
	if (dir->i_op->lookup(dir,"...",3,&spec_inode))
		return IFS_ST_NORMAL;
Dprintk("[1]\n");
	if (!spec_inode->i_op || !spec_inode->i_op->lookup)
		panic("spec_inode...\n");
	status = IFS_ST_NORMAL;
	found = !spec_inode->i_op->lookup(spec_inode,name,len,&inode);
	if (found) {
		status = S_ISDIR(inode->i_mode) ? IFS_ST_HIDE : IFS_ST_WHITEOUT;
		iput(inode);
	}
Dprintk("[2]\n");
	return status;
}


/*
 * Creates a white out or hide entry for the specified file. Does not swallow
 * DIR. DIR is locked by the caller.
 */

static int ifs_special(struct inode *dir,const char *name,int len,int hide)
{
	struct inode *spec_dir,*dummy;
	int retval;

	retval = ifs_build_path(dir);
	if (retval)
		return retval;
	if (!IFS_CAN_INODE(dir,0,mkdir) || !IFS_CAN_INODE(dir,0,lookup))
		return -EBADF;
	USE_INODE(IFS_NTH(dir,0));
	retval = IFS_DO_INODE(dir,0,mkdir,(IFS_NTH(dir,0),"...",3,dir->i_mode &
	    0666));
	if (retval && retval != -EEXIST)
		return retval;
	USE_INODE(IFS_NTH(dir,0));
	retval = IFS_DO_INODE(dir,0,lookup,(IFS_NTH(dir,0),"...",3,&spec_dir));
	if (retval)
		return retval;
	if (hide ? !spec_dir->i_op->mkdir : !spec_dir->i_op->create) {
		iput(spec_dir);
		return -EBADF;
	}
	if (hide)
		retval = spec_dir->i_op->mkdir(spec_dir,name,len,spec_dir->
		    i_mode & 0777);
	else {
		retval = spec_dir->i_op->create(spec_dir,name,len,(spec_dir->
		    i_mode & 0777) | S_IFREG,&dummy);
		if (!retval)
			iput(dummy);
	}
	return retval;
}


/*
 * Creates a white out entry for the specified file. Does not swallow DIR.
 * DIR is locked by the caller.
 */

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


/*
 * Creates a hode entry for the specified file. Does not swallow DIR.
 * DIR is locked by the caller.
 */

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


/*
 * Removes a white out or hide entry for the specified file if one exists.
 * Does not swallow DIR. DIR is locked by the caller.
 */

int ifs_unspecial(struct inode *dir,const char *name,int len,int hide,int user)
{
	struct inode *del_dir;
	int retval;

Dprintk("ifs_unspecial\n");
	if (!IFS_NTH(dir,0))
		return user ? -ENOENT : 0;
	if (!IFS_CAN_INODE(dir,0,lookup))
		return -EBADF;
	USE_INODE(IFS_NTH(dir,0));
	retval = IFS_DO_INODE(dir,0,lookup,(IFS_NTH(dir,0),"...",3,&del_dir));
	if (retval)
		return retval == -ENOENT && !user ? 0 : retval;
	if (!del_dir->i_op->unlink) {
		iput(del_dir);
		return -EBADF;
	}
	retval = hide ? del_dir->i_op->rmdir(del_dir,name,len) :
	    del_dir->i_op->unlink(del_dir,name,len);
	if (retval && (user || retval != -ENOENT))
		return retval;
	return 0;
}

/*
 * Removes a white out entry for the specified file if one exists. Does not
 * swallow DIR. DIR is locked by the caller.
 */

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


/*
 * Removes a white out entry for the specified file if one exists. Does not
 * swallow DIR. DIR is locked by the caller. Returns more errors that
 * ifs_unwhiteout.
 */

int ifs_user_unwhiteout(struct inode *dir,const char *name,int len)
{
	return ifs_unspecial(dir,name,len,0,1);
}


/*
 * Removes a hide entry for the specified file if one exists. Does not
 * swallow DIR. DIR is locked by the caller.
 */

int ifs_unhide(struct inode *dir,const char *name,int len)
{
	return ifs_unspecial(dir,name,len,1,0);
}


void ifs_close_file(struct file *filp)
{
Dprintk("ifs_close\n");
	if (filp->f_op && filp->f_op->release)
		filp->f_op->release(filp->f_inode,filp);
	filp->f_count--;
	iput(filp->f_inode);
}


/*
 * INODE is locked by the caller. *filp is guaranteed to be NULL if the file
 * can't be opened.
 */

int ifs_open_file(struct inode *inode,struct file **filp,int flags)
{
	int retval;

Dprintk("ifs_open_file\n");
	if (!(*filp = get_empty_filp()))
		return -ENFILE;
	USE_INODE((*filp)->f_inode = inode);
	(*filp)->f_flags = flags;
	(*filp)->f_mode = (flags+1) & O_ACCMODE;
	(*filp)->f_pos = (*filp)->f_reada = 0;
	(*filp)->f_op = (*filp)->f_inode->i_op ? (*filp)->f_inode->i_op->
	    default_file_ops : NULL;
	if ((*filp)->f_op && (*filp)->f_op->open) {
		retval = (*filp)->f_op->open((*filp)->f_inode,*filp);
		if (retval) {
			ifs_close_file(*filp);
			*filp = NULL;
			return retval;
		}
	}
	return 0;
}


/*
 * Returns a non-zero integer if the directory is empty or inaccessible, zero
 * otherwise. Does not consume DIR. IFS_DIR should be set if DIR is an IFS
 * directory.
 */

int ifs_empty(struct inode *dir,int ifs_dir)
{
	struct file *filp;
	struct dirent de;
	unsigned short old_fs;
	int retval;

	if (ifs_open_file(dir,&filp,O_RDONLY))
		return 0;
	if (!filp->f_op || !filp->f_op->readdir) {
		ifs_close_file(filp);
		return 0;
	}
	old_fs = get_fs();
	set_fs(get_ds());
	do retval = filp->f_op->readdir(filp->f_inode,filp,&de,0);
	while (retval > 0 && de.d_name[0] == '.' && (de.d_reclen == 1 ||
	    (de.d_name[1] == '.' && (de.d_reclen == 2 || (ifs_dir &&
	    (de.d_name[2] == '.' && de.d_reclen == 3))))));
	set_fs(old_fs);
	ifs_close_file(filp);
	return !retval;
}


/*
 * Removes the IFS subdirectory of a directory. Does not consume DIR. DIR is
 * a regular inode.
 */

int ifs_purge(struct inode *dir)
{
	struct inode *inode;
	struct file *filp;
	struct dirent de;
	unsigned short old_fs;
	int retval,was_empty;

	if (!dir || !dir->i_op || !dir->i_op->lookup || !dir->i_op->rmdir)
		return -EBADF;
	USE_INODE(dir);
	retval = dir->i_op->lookup(dir,"...",3,&inode);
	if (retval) {
		iput(dir);
		return 0;
	}
	if (!inode->i_op || !inode->i_op->unlink) {
		iput(inode);
		iput(dir);
		return -EBADF;
	}
	retval = ifs_open_file(inode,&filp,O_RDONLY);
	if (!retval && (!filp->f_op || !filp->f_op->readdir))
		retval = -EBADF;
	if (retval) {
		iput(inode);
		iput(dir);
		return retval;
	}
	old_fs = get_fs();
	set_fs(get_ds());
	do {
		was_empty = 1;
		while (1) {
			retval = filp->f_op->readdir(filp->f_inode,filp,&de,0);
			if (retval <= 0)
				break;
			if (de.d_name[0] != '.' || (de.d_reclen != 1 &&
			    (de.d_name[1] != '.' || de.d_reclen != 2))) {
				was_empty = 0;
				USE_INODE(inode);
				retval = inode->i_op->unlink(inode,de.d_name,
				    de.d_reclen);
				if (retval)
					break;
			}
		}
	}
	while (!retval && !was_empty);
	set_fs(old_fs);
	ifs_close_file(filp);
	iput(inode);
	if (!retval) {
		USE_INODE(dir);
		retval = dir->i_op->rmdir(dir,"...",3);
	}
	return retval;
}
