/*
 *  linux/fs/ifs/clone.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/stat.h>
#include <linux/fcntl.h>


static int ifs_do_build_path(struct inode *dir);


static int ifs_get_name(struct inode *parent,struct inode *item,
    struct dirent *de)
{
	struct file *filp;
	int retval;

Dprintk("ifs_get_name\n");
	retval = ifs_open_file(parent,&filp,O_RDONLY);
	if (retval)
		return retval;
	do retval = filp->f_op->readdir(filp->f_inode,filp,de,0);
	while (retval > 0 && de->d_ino != item->i_ino);
	ifs_close_file(filp);
Dprintk("retval = %d\n",retval);
	if (retval <= 0)
		return retval ? retval : -ENOENT;
	return 0;
}


/*
 * Does not consume ITEM. ITEM may or may not be locked by the caller.
 */

static int ifs_prepare_cloning(struct inode *item,struct dirent *de,
    mode_t *mode)
{
	struct inode *parent,*use,*orig;
	int follow,retval;

Dprintk("ifs_prepare_cloning\n");
	if (!(parent = IFS_I(item)->parent))
		panic("Climbing beyond root");
	retval = ifs_do_build_path(parent);
	if (retval)
		return retval;
	for (follow = 1; follow < IFS_LAYERS(item); follow++)
		if (IFS_NTH(item,follow)) break;
	if (follow == IFS_LAYERS(item))
		panic("Parent has no lower inode");
	USE_INODE(orig = IFS_NTH(item,follow));
	if (mode)
		*mode = orig->i_mode;
	USE_INODE(use = IFS_NTH(parent,follow));
	retval = ifs_get_name(use,orig,de);
	iput(orig);
	iput(use);
	return retval;
}


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

static int ifs_do_build_path(struct inode *dir)
{
	struct inode *parent,*use,*new_dir;
	struct dirent de;
	mode_t mode;
	int retval;

Dprintk("ifs_do_build_path (%d)\n",dir);
	if (IFS_NTH(dir,0))
		return 0;
	parent = IFS_I(dir)->parent;
	if (!parent)
		panic("Climbing beyond root");
	retval = ifs_prepare_cloning(dir,&de,&mode);
	if (retval)
		return retval;
	use = IFS_NTH(parent,0); /* can't lock parent */
	if (!use || !use->i_op || !use->i_op->mkdir || !use->i_op->lookup)
		return -EBADF;
	USE_INODE(use); /* to keep inode busy */
	USE_INODE(use); /* for mkdir */
de.d_name[de.d_reclen] = 0;
Dprintk("creating dir '%s'\n",de.d_name);
	retval = use->i_op->mkdir(use,de.d_name,de.d_reclen,mode);
	if (retval) {
		iput(use);
		return retval;
	}
	USE_INODE(use);
	retval = use->i_op->lookup(use,de.d_name,de.d_reclen,&new_dir);
	iput(use);
	if (retval)
		return retval;
	if (IFS_NTH(dir,0)) {
		iput(new_dir);
		return -EEXIST;
	}
	IFS_NTH(dir,0) = new_dir;
	return 0;
}


/*
 * Creates a path in the top level file system that corresponds to a path to DIR
 * in a lower file system. Does not swallow DIR. DIR is (always?) locked by the
 * caller.
 */

int ifs_build_path(struct inode *dir)
{
	int old_fs,retval;

Dprintk("ifs_build_path\n");
	old_fs = get_fs(); /* for readdir */
	set_fs(get_ds());
	retval = ifs_do_build_path(dir);
	set_fs(old_fs);
	return retval;
}


/*
 * ORIG is locked by the caller.
 */

static int ifs_do_clone_by_name(struct inode *orig,struct inode *dir,
    const char *name,int len,struct inode **result)
{
	struct inode *new_file;
	struct file *in,*out;
	unsigned long page;
	int retval,size;

	USE_INODE(dir);
	retval = dir->i_op->create(dir,name,len,orig->i_mode & ~0777,&new_file);
	if (retval)
		return retval;
	new_file->i_uid = orig->i_uid;
	new_file->i_gid = orig->i_gid;
	new_file->i_atime = orig->i_atime;
	new_file->i_mtime = orig->i_mtime;
	new_file->i_ctime = orig->i_ctime;
	if (new_file->i_sb->s_op && new_file->i_sb->s_op->notify_change)
		new_file->i_sb->s_op->notify_change(NOTIFY_UIDGID | NOTIFY_TIME,
		    new_file);
	retval = ifs_open_file(orig,&in,O_RDONLY);
	if (retval) {
		USE_INODE(dir);
		(void) dir->i_op->unlink(dir,name,len);
		iput(new_file);
		return retval;
	}
	retval = ifs_open_file(new_file,&out,O_WRONLY);
	if (retval) {
		ifs_close_file(in);
		(void) dir->i_op->unlink(dir,name,len);
		iput(new_file);
		return retval;
	}
	page = get_free_page(GFP_KERNEL);
	while ((size = in->f_op->read(in->f_inode,in,(char *) page,PAGE_SIZE))
	    > 0)
		if ((retval = out->f_op->write(out->f_inode,out,(char *) page,
		    size)) != size)
			break;
	free_page(page);
	ifs_close_file(in);
	ifs_close_file(out);
	if (size) {
		if (dir->i_op && dir->i_op->unlink)
			(void) dir->i_op->unlink(dir,name,len);
		iput(new_file);
		return size < 0 ? size : retval < 0 ? retval : -ENOSPC;
	}
	new_file->i_mode = orig->i_mode;
	if (new_file->i_sb->s_op && new_file->i_sb->s_op->notify_change)
		new_file->i_sb->s_op->notify_change(NOTIFY_MODE,new_file);
	if (result)
		*result = new_file;
	else iput(new_file);
	return 0;
}


/*
 * Does not consume INODE. INODE is locked by the caller.
 */

static int ifs_do_clone_by_inode(struct inode *inode)
{
	struct inode *parent,*new_file;
	struct dirent de;
	int retval,n;

Dprintk("ifs_do_clone_by_inode\n");
	if (IFS_NTH(inode,0))
		return -EEXIST;
	if (!(parent = IFS_I(inode)->parent))
		panic("Climbing beyond root");
	retval = ifs_prepare_cloning(inode,&de,NULL);
	if (retval)
		return retval;
	if (!IFS_CAN_INODE(parent,0,create) || !IFS_CAN_INODE(parent,0,unlink))
		return -EBADF;
	while (IFS_I(inode)->pop_lock)
		sleep_on(&IFS_I(inode)->pop_wait);
	IFS_I(inode)->pop_lock = 1;
	for (n = 0; n < IFS_LAYERS(inode); n++)
		if (IFS_NTH(inode,n)) break;
de.d_name[de.d_reclen] = 0;
	if (n == IFS_LAYERS(inode))
		retval = -EBADF;
	else retval = ifs_do_clone_by_name(IFS_NTH(inode,n),IFS_NTH(parent,0),
		    de.d_name,de.d_reclen,&new_file);
	if (!retval)
		IFS_NTH(inode,0) = new_file;
	IFS_I(inode)->pop_lock = 0;
	wake_up(&IFS_I(inode)->pop_wait);
	return retval;
}


/*
 * Does not consume INODE. INODE is not locked by the caller.
 */

int ifs_clone_file(struct inode *inode)
{
	int old_fs,retval;

Dprintk("ifs_clone_file\n");
	old_fs = get_fs(); /* for read/write */
	set_fs(get_ds());
	ifs_lock(inode);
	retval = ifs_do_clone_by_inode(inode);
	ifs_unlock(inode);
	set_fs(old_fs);
	return retval;
}


/*
 * Copies a regular file. OBJECT is locked by the caller.
 */

int ifs_copy_file(struct inode *object,struct inode *dir,const char *name,
    int len)
{
	return ifs_do_clone_by_name(object,dir,name,len,NULL);
}


/*
 * Copies a symbolic link. OBJECT is locked by the caller.
 */

int ifs_copy_link(struct inode *object,struct inode *dir,const char *name,
    int len)
{
	printk("IFS: can't move symbolic links across devices\n");
	return -EINVAL;
}


/*
 * Copies a directory file. OBJECT is locked by the caller.
 */

int ifs_copy_dir(struct inode *object,struct inode *dir,const char *name,
    int len)
{
	printk("IFS: can't move directories across devices\n");
	return -EINVAL;
}


/*
 * Copies a device file, a FIFO or something similar. OBJECT is locked by the
 * caller.
 */

int ifs_copy_node(struct inode *object,struct inode *dir,const char *name,
    int len)
{
	printk("IFS: can't move non-files and non-directories across devices"
	    "\n");
	return -EINVAL;
}


/*
 * Copies the file system object OBJECT to directory PARENT with name NAME.
 * OBJECT must not be located inside PARENT. Does not consume OBJECT or PARENT.
 * OBJECT is locked by the caller.
 */

int ifs_copy_object(struct inode *object,struct inode *parent,const char *name,
    int len)
{
	int retval;

	if (!parent)
		panic("ifs_copy_object: parent == NULL");
             if (S_ISREG(object->i_mode))
		retval = ifs_copy_file(object,parent,name,len);
	else if (S_ISDIR(object->i_mode))
		retval = ifs_copy_dir(object,parent,name,len);
	else if (S_ISLNK(object->i_mode))
		retval = ifs_copy_link(object,parent,name,len);
	else if (S_ISCHR(object->i_mode) || S_ISBLK(object->i_mode) ||
	    S_ISFIFO(object->i_mode))
		retval = ifs_copy_node(object,parent,name,len);
	else {
		printk("IFS: bad inode type 0%o\n",object->i_mode);
		return -EBADF;
	}
	if (retval)
		return retval;
	return 0;
}
