/*
 * Read-only filesystem code for SCO's bfs filesystem
 * aeb, 990907
 *
 * Comments are welcome - aeb@cwi.nl
 *
 * Compile with gcc -c -O2 bfsmod.c
 * Usage:
 *   insmod bfsmod.o; mount -t bfs dev dir; ...; umount dev; rmmod bfsmod
 * This was used successfully with Linux 2.2.12.
 */

#define MODULE
#define __KERNEL__

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/locks.h>	/* lock_super */
#include <asm/uaccess.h>	/* copy_to_user */

#define BFS_BLOCKSIZE	512
#define BFS_BLOCKSIZEBITS 9

#define BFS_ROOT_INO	  2

/* Some information about the superblock can be found
   in the Unixware man page fs_bfs(4).
   There, the superblock magic is given as 0xBADFACE.
   (Different versions of the fs? Or a typo?) */

/* on disk superblock */
struct bfssb {
	unsigned int magic;
	unsigned int data_start;
	unsigned int data_end;		/* sizeof(slice)-1 */

	/* for recovery during compaction */
	int fromblock, toblock;		/* src and dest of current transfer */
	int backup_fromblock, backup_toblock;

	/* labels - may well contain garbage */
	char fsname[6];
	char volume[6];
	char stuff[472];		/* unused */
};

#define BFS_SUPER_MAGIC 0x1badface

/* in core data */
struct bfs_sb_info {
        unsigned int s_max_ino;
	unsigned int s_data_end;
};

struct bfs_i_info {
	unsigned int i_first_block;
};

/* Unfortunately, one has to change <linux/fs.h> to put
   bfs_sb_info into the union u. Solve using an ugly define. */
#if 0
#define u_bfs_sb_info_p(s)	(&((s)->u.bfs_sb_info))
#else
#define u_bfs_sb_info_p(s)	((struct bfs_sb_info *)(&((s)->u)))
#endif

#if 0
#define u_bfs_i_info_p(inode)	(&((inode)->u.bfs_inode_info))
#else
#define u_bfs_i_info_p(inode)	((struct bfs_i_info *)(&((inode)->u)))
#endif

/* on disk inode - 64 bytes, 8 per block */
#define BFS_INODE_SIZE	64
#define BFS_INODES_PER_BLOCK	(BFS_BLOCKSIZE / BFS_INODE_SIZE)
struct bfsi {
        unsigned short i_nr;
        unsigned char i_stuff1[2];	/* only nonzero for root */
					/* not mentioned in inode_bfs(4) */
        unsigned long i_first_block;
        unsigned long i_last_block;
        unsigned long i_bytes_to_end;
        unsigned long i_type;		/* 1: file, 2: the unique dir */
        unsigned long i_mode;
	unsigned long i_uid, i_gid;
        unsigned long i_nlinks;
        unsigned long i_atime, i_mtime, i_ctime;
        unsigned char i_stuff3[16]; 	/* only nonzero for root */
};

/* on disk directory entry */
#define BFS_DESIZE	16
#define BFS_NAMELEN	14
struct bfsde {
	unsigned short i_nr;
	char name[BFS_NAMELEN];
};

static int
bfs_bmap(struct inode *inode, int block) {
	return u_bfs_i_info_p(inode)->i_first_block + block;
}

static int
bfs_readdir(struct file *filp, void *dirent, filldir_t filldir) {
	unsigned int offset, block, curblock = 0;
	struct buffer_head *bh = NULL;
	struct bfsde *de;
	struct inode *inode = filp->f_dentry->d_inode;

	if (filp->f_pos & (BFS_DESIZE-1))
		return -EBADF;

	while (filp->f_pos < inode->i_size) {
		offset = filp->f_pos & (BFS_BLOCKSIZE-1);
		block = u_bfs_i_info_p(inode)->i_first_block +
			(filp->f_pos >> BFS_BLOCKSIZEBITS);
		if (!bh || curblock != block) {
			if (bh)
				brelse(bh);
			bh = bread(inode->i_dev, block, BFS_BLOCKSIZE);
			if (!bh)
				return -EIO;
			curblock = block;
		}
		de = (struct bfsde *) (bh->b_data + offset);
		if (de->i_nr) {
			int size = strnlen(de->name, BFS_NAMELEN);
			if (filldir(dirent, de->name, size, filp->f_pos, de->i_nr) < 0)
				break;
		}
		filp->f_pos += BFS_DESIZE;
	}
	if (bh)
		brelse(bh);
	return 0;
}

static struct dentry *
bfs_lookup(struct inode *dir, struct dentry *dentry) {
	unsigned int ino, pos, offset, block, curblock = 0;
	struct buffer_head *bh = NULL;
	struct bfsde *de;
	struct inode *inode;
	const char *name = dentry->d_name.name;
	int len = dentry->d_name.len;

	if (len > BFS_NAMELEN)
		len = BFS_NAMELEN;
	pos = ino = 0;
	while (pos < dir->i_size) {
		offset = pos & (BFS_BLOCKSIZE-1);
		block = u_bfs_i_info_p(dir)->i_first_block +
			(pos >> BFS_BLOCKSIZEBITS);
		if (!bh || curblock != block) {
			if (bh)
				brelse(bh);
			bh = bread(dir->i_dev, block, BFS_BLOCKSIZE);
			if (!bh)
				return ERR_PTR(-EIO);
			curblock = block;
		}
		de = (struct bfsde *) (bh->b_data + offset);
		if (de->i_nr) {
			int size = strnlen(de->name, BFS_NAMELEN);
			if (size == len && strncmp(de->name, name, len) == 0) {
				ino = de->i_nr;
				break;
			}
		}
		pos += BFS_DESIZE;
	}
	if (bh)
		brelse(bh);
	if (!ino)
		return ERR_PTR(-ENOENT);
	inode = iget(dir->i_sb, ino);
	if (!inode)
		return ERR_PTR(-EACCES);
	d_add(dentry, inode);
	return NULL;
}

static struct file_operations bfs_file_operations = {
        NULL,                   /* lseek - default */
        generic_file_read,      /* read */
        NULL,                   /* write - bad */
        NULL,                   /* readdir - bad */
        NULL,                   /* poll - default */
        NULL,                   /* ioctl - default */
        generic_file_mmap,      /* mmap */
        NULL,                   /* open */
        NULL,                   /* flush */
        NULL,                   /* release */
        NULL,                   /* fsync */
        NULL,                   /* fasync */
        NULL,                   /* check_media_change */
        NULL,                   /* revalidate */
};

static struct inode_operations bfs_file_inode_operations = {
        &bfs_file_operations,   /* default file operations */
        NULL,                   /* create */
        NULL,                   /* lookup */
        NULL,                   /* link */
        NULL,                   /* unlink */
        NULL,                   /* symlink */
        NULL,                   /* mkdir */
        NULL,                   /* rmdir */
        NULL,                   /* mknod */
        NULL,                   /* rename */
        NULL,                   /* readlink */
        NULL,                   /* follow_link */
        generic_readpage,       /* readpage */
        NULL,                   /* writepage */
        bfs_bmap,               /* bmap */
        NULL,                   /* truncate */
        NULL,                   /* permission */
        NULL,                   /* smap */
};

static struct file_operations bfs_dir_operations = {
        NULL,                   /* lseek - default */
        NULL,                   /* read */
        NULL,                   /* write - bad */
        bfs_readdir,            /* readdir */
        NULL,                   /* poll - default */
        NULL,                   /* ioctl - default */
        NULL,                   /* mmap */
        NULL,                   /* open */
        NULL,                   /* flush */
        NULL,                   /* release */
        NULL,                   /* fsync */
        NULL,                   /* fasync */
        NULL,                   /* check_media_change */
        NULL,                   /* revalidate */
};

static struct inode_operations bfs_dir_inode_operations = {
        &bfs_dir_operations,
        NULL,                   /* create */
        bfs_lookup,             /* lookup */
        NULL,                   /* link */
        NULL,                   /* unlink */
        NULL,                   /* symlink */
        NULL,                   /* mkdir */
        NULL,                   /* rmdir */
        NULL,                   /* mknod */
        NULL,                   /* rename */
        NULL,                   /* readlink */
        NULL,                   /* follow_link */
        NULL,                   /* readpage */
        NULL,                   /* writepage */
        NULL,                   /* bmap */
        NULL,                   /* truncate */
        NULL,                   /* permission */
        NULL,                   /* smap */
};

static void
bfs_read_inode(struct inode *inode) {
	unsigned int ino;
	struct super_block *s;
	struct buffer_head *bh;
	struct bfsi *ri;	/* raw inode */
	int block;
	static int warn = 0, warn2 = 0;

	s = inode->i_sb;
	ino = inode->i_ino;
	inode->i_op = NULL;
	inode->i_mode = 0;

	if (ino < BFS_ROOT_INO || ino > u_bfs_sb_info_p(s)->s_max_ino) {
		printk("Bad inode number %d on dev %s\n",
		       ino, kdevname(inode->i_dev));
		return;
	}

	block = 1 + (ino-BFS_ROOT_INO)/BFS_INODES_PER_BLOCK;
	if (!(bh = bread(inode->i_dev, block, BFS_BLOCKSIZE))) {
		printk("Unable to read inode %d from dev %s\n",
		       ino, kdevname(inode->i_dev));
		return;
	}
	ri = ((struct bfsi *)(bh->b_data)) +
		(ino-BFS_ROOT_INO) % BFS_INODES_PER_BLOCK;
	if (ri->i_nr != ino && warn++ == 0)
		printk("bfs: warning: bad fs: inode %d has nr %d\n",
		       ino, ri->i_nr);
	inode->i_mode = (ri->i_mode & 0777);
	if (ri->i_type == 2) {
		inode->i_mode |= S_IFDIR;
		inode->i_op = &bfs_dir_inode_operations;
	} else if (ri->i_type == 1) {
		inode->i_mode |= S_IFREG;
		inode->i_op = &bfs_file_inode_operations;
	} else if(warn2++ == 0)
		printk("bfs: warning: bad fs: inode of unknown type\n");
	inode->i_uid = ri->i_uid;
	inode->i_gid = ri->i_gid;
	inode->i_nlink = ri->i_nlinks;
	inode->i_size = (ri->i_first_block == 0) ? 0 :
		ri->i_bytes_to_end - ri->i_first_block*BFS_BLOCKSIZE + 1;
	inode->i_atime = ri->i_atime;
	inode->i_mtime = ri->i_mtime;
	inode->i_ctime = ri->i_ctime;
	inode->i_blocks = inode->i_blksize = 0;
	u_bfs_i_info_p(inode)->i_first_block = ri->i_first_block;
	brelse(bh);
}

static void
bfs_put_super(struct super_block *s) {
        MOD_DEC_USE_COUNT;
        return;
}

static int
bfs_statfs(struct super_block *sb, struct statfs *buf, int bufsize) {
        struct statfs tmp;

        memset(&tmp, 0, sizeof(tmp));
        tmp.f_type = BFS_SUPER_MAGIC;
        tmp.f_bsize = BFS_BLOCKSIZE;
        tmp.f_blocks = (u_bfs_sb_info_p(sb)->s_data_end + 1) / BFS_BLOCKSIZE;
        tmp.f_namelen = BFS_NAMELEN;
        return copy_to_user(buf, &tmp, bufsize) ? -EFAULT : 0;
}

static struct super_operations bfs_ops = {
        bfs_read_inode,         /* read inode */
        NULL,                   /* write inode */
        NULL,                   /* put inode */
        NULL,                   /* delete inode */
        NULL,                   /* notify change */
        bfs_put_super,          /* put super */
        NULL,                   /* write super */
        bfs_statfs,             /* statfs */
        NULL                    /* remount */
};

static struct super_block *
bfs_read_super(struct super_block *s, void *data, int silent) {
	kdev_t dev;
	struct buffer_head *bh;
	struct bfssb *sb;
	struct inode *root;

	MOD_INC_USE_COUNT;

	lock_super(s);
	dev = s->s_dev;
	set_blocksize(dev, BFS_BLOCKSIZE);
	s->s_blocksize = BFS_BLOCKSIZE;
	s->s_blocksize_bits = BFS_BLOCKSIZEBITS;

	bh = bread(dev, 0, BFS_BLOCKSIZE);
	if (!bh) {
		printk ("bfs: unable to read superblock\n");
                goto outnobh;
        }
	sb = (struct bfssb *) bh->b_data;
	if (sb->magic != BFS_SUPER_MAGIC) {
		if (!silent)
			printk ("VFS: Can't find a bfs filesystem on dev "
				"%s.\n", kdevname(dev));
		goto out;
	}
	s->s_magic = BFS_SUPER_MAGIC;
	s->s_flags |= MS_RDONLY;
	s->s_op = &bfs_ops;

	u_bfs_sb_info_p(s)->s_max_ino =
		BFS_ROOT_INO + (sb->data_start - 512) / BFS_INODE_SIZE - 1;

	u_bfs_sb_info_p(s)->s_data_end = sb->data_end;

	root = iget(s, BFS_ROOT_INO);
	if (!root)
		goto out;
	s->s_root = d_alloc_root(root, NULL);
	if (!s->s_root)
		goto outiput;
	brelse(bh);
	unlock_super(s);
	return s;

 outiput:
	iput(root);
 out:
	brelse(bh);
 outnobh:
	s->s_dev = 0;
	unlock_super(s);
	MOD_DEC_USE_COUNT;
	return NULL;
}

static struct file_system_type bfs_fs_type = {
        "bfs",
        FS_REQUIRES_DEV,
        bfs_read_super,
        NULL
};

int init_bfs_fs(void) {
        return register_filesystem(&bfs_fs_type);
}

#ifdef MODULE

EXPORT_NO_SYMBOLS;

int init_module(void) {
	return init_bfs_fs();
}

void cleanup_module(void) {
	unregister_filesystem(&bfs_fs_type);
}
#endif /* MODULE */