/*
 *  linux/kernel/blk_drv/loop.c
 *
 *  Written by Theodore Ts'o, 3/29/93
 * 
 * Copyright 1993 by Theodore Ts'o.  Redistribution of this file is
 * permitted under the GNU Public License.
 */

/*
 * DES encryption plus some minor changes by Werner Almesberger, 30-MAY-1993
 */

#include <linux/config.h>
#include <linux/sched.h>
#include <linux/fs.h>
#include <linux/stat.h>
#include <linux/kernel.h>
#include <linux/string.h>
#include <linux/errno.h>
#include <linux/loop.h>
#include <linux/des.h>
#include <asm/segment.h>


#define MAJOR_NR 14
#include "blk.h"

#define MAX_LOOP 8

static struct loop_device loop_dev[MAX_LOOP];
static int loop_sizes[MAX_LOOP];

/*
 * Transfer functions
 */
int transfer_none(struct loop_device *lo, int cmd, char *raw_buf,
		  char *loop_buf, int size)
{
	if (cmd == READ)
		memcpy(loop_buf, raw_buf, size);
	else
		memcpy(raw_buf, loop_buf, size);
	return 0;
}

int transfer_xor(struct loop_device *lo, int cmd, char *raw_buf,
		 char *loop_buf, int size)
{
	char	*in, *out, *key;
	int	i, keysize;

	if (cmd == READ) {
		in = raw_buf;
		out = loop_buf;
	} else {
		in = loop_buf;
		out = raw_buf;
	}
	key = lo->lo_encrypt_key;
	keysize = lo->lo_encrypt_key_size;
	for (i=0; i < size; i++)
		*out++ = *in++ ^ key[(i & 511) % keysize];
	return 0;
}

#ifdef DES_AVAILABLE
int transfer_des(struct loop_device *lo, int cmd, char *raw_buf,
		  char *loop_buf, int size)
{
	if (size & 7)
		return -EINVAL;
	if (cmd == READ)
	while (size) {
		if (cmd == READ)
			des_ecb_encrypt((des_cblock *) raw_buf,(des_cblock *)
			    loop_buf,lo->lo_des_key,DES_ENCRYPT);
		else des_ecb_encrypt((des_cblock *) loop_buf,(des_cblock *)
			    raw_buf,lo->lo_des_key,DES_DECRYPT);
		size -= 8;
		raw_buf += 8;
		loop_buf += 8;
	}
	return 0;
}
#endif


static transfer_proc_t xfer_funcs[MAX_LOOP] = {
	transfer_none,		/* LO_CRYPT_NONE */
	transfer_xor,		/* LO_CRYPT_XOR */
#ifdef DES_AVAILABLE
	transfer_des,		/* LO_CRYPT_DES */
#else
	NULL,			/* LO_CRYPT_DES */
#endif
	0			/* LO_CRYPT_IDEA */
};


#define MAX_DISK_SIZE 1024*1024*1024


static void figure_loop_size(struct loop_device *lo)
{
	int	size;

	if (S_ISREG(lo->lo_inode->i_mode))
		size = (lo->lo_inode->i_size - lo->lo_offset) / 1024;
	else {
		if (blk_size[MAJOR(lo->lo_device)])
			size = ((blk_size[MAJOR(lo->lo_device)]
				 [MINOR(lo->lo_device)]) -
				(lo->lo_offset/1024));
		else
			size = MAX_DISK_SIZE;
	}
	loop_sizes[lo->lo_number] = size;
}

static void do_lo_request(void)
{
	int	real_block, block, offset, len, blksize, size;
	char	*dest_addr;
	struct loop_device *lo;
	struct buffer_head *bh;
	
repeat:
	INIT_REQUEST;
	if (MINOR(CURRENT->dev) >= MAX_LOOP)
		goto error_out;
	lo = &loop_dev[MINOR(CURRENT->dev)];
	if (!lo->lo_inode)
		goto error_out;
	if (!(blksize = lo->lo_inode->i_blksize))
		blksize = 1024;
	dest_addr = CURRENT->buffer;
	
	if (blksize < 512) {
		block = CURRENT->sector * (512/blksize);
		offset = 0;
	} else {
		block = CURRENT->sector / (blksize/512);
		offset = CURRENT->sector % (blksize/512);
	}
	block += lo->lo_offset / blksize;
	offset += lo->lo_offset % blksize;
	if (offset > blksize) {
		block++;
		offset -= blksize;
	}
	len = CURRENT->nr_sectors << 9;
	if ((CURRENT->cmd != WRITE) && (CURRENT->cmd != READ)) {
		printk("unknown loop device command (%d)?!?", CURRENT->cmd);
		goto error_out;
	}
	while (len > 0) {
		real_block = block;
		if (lo->lo_flags & LO_FLAGS_DO_BMAP) {
			real_block = bmap(lo->lo_inode, block);
			if (!real_block) {
				printk("loop: block %d not present\n", block);
				goto error_out;
			}
		}
		bh = getblk(lo->lo_device, real_block, blksize);
		if (!bh) {
			printk("loop: getblk(%d, %d, %d) returned NULL",
			       lo->lo_inode->i_dev, block, blksize);
			goto error_out;
		}
		if (!bh->b_uptodate && ((CURRENT->cmd == READ) ||
					(offset || (len < blksize)))) {
			ll_rw_block(READ, 1, &bh);
			wait_on_buffer(bh);
			if (!bh->b_uptodate) {
				brelse(bh);
				goto error_out;
			}
		}
		size = blksize - offset;
		if (size > len)
			size = len;
		if ((lo->transfer)(lo, CURRENT->cmd, bh->b_data + offset,
				   dest_addr, size)) {
			printk("loop: transfer error block %d\n", block);
			brelse(bh);
			goto error_out;
		}
		if (CURRENT->cmd == WRITE) {
			bh->b_dirt = 1;
			ll_rw_block(WRITE, 1, &bh);
			wait_on_buffer(bh);
			if (bh->b_dirt) {
				brelse(bh);
				goto error_out;
			}
		}
		brelse(bh);
		dest_addr += size;
		len -= size;
		offset = 0;
		block++;
	}
	end_request(1);
	goto repeat;
error_out:
	end_request(0);
	goto repeat;
}

static int loop_set_fd(struct loop_device *lo, unsigned int arg)
{
	struct inode	*inode;
	
	if (arg >= NR_OPEN || !current->filp[arg])
		return -EBADF;
	if (lo->lo_inode)
		return -EBUSY;
	inode = current->filp[arg]->f_inode;
	if (!inode) {
		printk("loop_set_fd: NULL inode?!?\n");
		return -EINVAL;
	}
	if (S_ISREG(inode->i_mode)) {
		lo->lo_device = inode->i_dev;
		lo->lo_flags |= LO_FLAGS_DO_BMAP;
	} else if (S_ISBLK(inode->i_mode))
			lo->lo_device = inode->i_rdev;
		else
			return -EINVAL;
	lo->lo_inode = inode;
	lo->lo_inode->i_count++;
	figure_loop_size(lo);
	return 0;
}

static int loop_clr_fd(struct loop_device *lo, dev_t dev)
{
	if (!lo->lo_inode)
		return -ENXIO;
	if (lo->lo_refcnt > 1)
		return -EBUSY;
	lo->lo_inode->i_count--;
	lo->lo_device = 0;
	lo->lo_inode = 0;
	lo->lo_encrypt_type = 0;
	lo->lo_offset = 0;
	lo->lo_encrypt_key_size = 0;
	memset(lo->lo_encrypt_key, 0, LO_KEY_SIZE);
	memset(lo->lo_name, 0, LO_NAME_SIZE);
	invalidate_buffers(dev);
	return 0;
}

static int loop_set_status(struct loop_device *lo, struct loop_info *arg)
{
	struct loop_info info;

	if (!lo->lo_inode)
		return -ENXIO;
	if (!arg)
		return -EINVAL;
	memcpy_fromfs(&info, arg, sizeof(info));
	if (info.lo_encrypt_key_size > LO_KEY_SIZE)
		return -EINVAL;
	switch (info.lo_encrypt_type) {
	case LO_CRYPT_NONE:
		break;
	case LO_CRYPT_XOR:
		if (info.lo_encrypt_key_size < 0)
			return -EINVAL;
		break;
#ifdef DES_AVAILABLE
	case LO_CRYPT_DES:
		if (!des_available)
			return -EINVAL;
		if (info.lo_encrypt_key_size != 8)
			return -EINVAL;
		des_set_key((des_cblock *) lo->lo_encrypt_key,
		   lo->lo_des_key);
		break;
#endif
	default:
		return -EINVAL;
	}
	lo->lo_offset = info.lo_offset;
	strncpy(lo->lo_name, info.lo_name, LO_NAME_SIZE);
	lo->lo_encrypt_type = info.lo_encrypt_type;
	lo->transfer = xfer_funcs[lo->lo_encrypt_type];
	lo->lo_encrypt_key_size = info.lo_encrypt_key_size;
	if (info.lo_encrypt_key_size)
		memcpy(lo->lo_encrypt_key, info.lo_encrypt_key,
		       info.lo_encrypt_key_size);
	figure_loop_size(lo);
	return 0;
}

static int loop_get_status(struct loop_device *lo, struct loop_info *arg)
{
	struct loop_info	info;
	
	if (!lo->lo_inode)
		return -ENXIO;
	if (!arg)
		return -EINVAL;
	memset(&info, 0, sizeof(info));
	info.lo_number = lo->lo_number;
	info.lo_device = lo->lo_inode->i_dev;
	info.lo_inode = lo->lo_inode->i_ino;
	info.lo_rdevice = lo->lo_device;
	info.lo_offset = lo->lo_offset;
	info.lo_flags = lo->lo_flags;
	strncpy(info.lo_name, lo->lo_name, LO_NAME_SIZE);
	info.lo_encrypt_type = lo->lo_encrypt_type;
	if (lo->lo_encrypt_key_size && suser()) {
		info.lo_encrypt_key_size = lo->lo_encrypt_key_size;
		memcpy(info.lo_encrypt_key, lo->lo_encrypt_key,
		       lo->lo_encrypt_key_size);
	}
	memcpy_tofs(arg, &info, sizeof(info));
	return 0;
}

static int lo_ioctl(struct inode * inode, struct file * file,
	unsigned int cmd, unsigned long arg)
{
	struct loop_device *lo;
	int dev, err;

	if (!inode)
		return -EINVAL;
	if (MAJOR(inode->i_rdev) != MAJOR_NR) {
		printk("lo_ioctl: pseudo-major != %d\n", MAJOR_NR);
		return -ENODEV;
	}
	dev = MINOR(inode->i_rdev);
	if (dev >= MAX_LOOP)
		return -ENODEV;
	lo = &loop_dev[dev];
	switch (cmd) {
	case LOOP_SET_FD:
		return loop_set_fd(lo, arg);
	case LOOP_CLR_FD:
		return loop_clr_fd(lo, inode->i_rdev);
	case LOOP_SET_STATUS:
		return loop_set_status(lo, (struct loop_info *) arg);
	case LOOP_GET_STATUS:
		return loop_get_status(lo, (struct loop_info *) arg);
	case BLKGETSIZE:   /* Return device size */
		if (!lo->lo_inode)
			return -ENXIO;
		if (!arg)  return -EINVAL;
		err = verify_area(VERIFY_WRITE, (long *) arg, sizeof(long));
		if (err)
			return err;
		put_fs_long(loop_sizes[lo->lo_device] << 1, (long *) arg);
		return 0;
		default:
			return -EINVAL;
	}
	return 0;
}

static int lo_open(struct inode *inode, struct file *file)
{
	struct loop_device *lo;
	int	dev;

	if (!inode)
		return -EINVAL;
	if (MAJOR(inode->i_rdev) != MAJOR_NR) {
		printk("lo_open: pseudo-major != %d\n", MAJOR_NR);
		return -ENODEV;
	}
	dev = MINOR(inode->i_rdev);
	if (dev >= MAX_LOOP)
		return -ENODEV;
	lo = &loop_dev[dev];
	lo->lo_refcnt++;
	return 0;
}

static void lo_release(struct inode *inode, struct file *file)
{
	struct loop_device *lo;
	int	dev;

	if (!inode)
		return;
	if (MAJOR(inode->i_rdev) != MAJOR_NR) {
		printk("lo_release: pseudo-major != %d\n", MAJOR_NR);
		return;
	}
	dev = MINOR(inode->i_rdev);
	if (dev >= MAX_LOOP)
		return;
	lo = &loop_dev[dev];
	if (lo->lo_refcnt <= 0)
		printk("lo_release: refcount(%d) <= 0\n", lo->lo_refcnt);
	else
		lo->lo_refcnt--;
	return;
}

static struct file_operations lo_fops = {
	NULL,			/* lseek - default */
	block_read,		/* read - general block-dev read */
	block_write,		/* write - general block-dev write */
	NULL,			/* readdir - bad */
	NULL,			/* select */
	lo_ioctl,		/* ioctl */
	NULL,			/* mmap */
	lo_open,		/* open */
	lo_release		/* release */
};

/*
 * Returns mem_start increased by the amount of memory which needs to be
 *  reserved.
 */
extern "C" unsigned long loop_init(unsigned long mem_start, unsigned long mem_end)
{
	int	i;

	if (register_blkdev(MAJOR_NR, "lo", &lo_fops)) {
		printk("Unable to get major %d for loop device\n",MAJOR_NR);
		return mem_start;
	}
	blk_dev[MAJOR_NR].request_fn = DEVICE_REQUEST;
	for (i=0; i < MAX_LOOP; i++) {
		memset(&loop_dev[i], 0, sizeof(struct loop_device));
		loop_dev[i].lo_number = i;
	}
	memset(&loop_sizes, 0, sizeof(loop_sizes));
	blk_size[MAJOR_NR] = loop_sizes;
	return mem_start;
}
