[<prev] [next>] [<thread-prev] [thread-next>] [day] [month] [year] [list]
Message-Id: <1239490236.6523.20.camel@localhost.localdomain>
Date: Sat, 11 Apr 2009 23:50:36 +0100
From: Adrian McMenamin <adrian@...golddream.dyndns.info>
To: LKML <linux-kernel@...r.kernel.org>
Cc: linux-fsdevel <linux-fsdevel@...r.kernel.org>,
viro <viro@...iv.linux.org.uk>,
linux-sh <linux-sh@...r.kernel.org>
Subject: Re: [RFC][patch] VMUFAT filesystem - v3
A further quick clean up.
fs/Kconfig | 1 +
fs/Makefile | 1 +
fs/vmufat/Kconfig | 14 +
fs/vmufat/Makefile | 7 +
fs/vmufat/inode.c | 1394 ++++++++++++++++++++++++++++++++++++++++++++++++++++
Signed-off by: Adrian McMenamin
---
diff --git a/fs/Kconfig b/fs/Kconfig
index 9f7270f..b7cb23d 100644
--- a/fs/Kconfig
+++ b/fs/Kconfig
@@ -176,6 +176,7 @@ source "fs/romfs/Kconfig"
source "fs/sysv/Kconfig"
source "fs/ufs/Kconfig"
source "fs/exofs/Kconfig"
+source "fs/vmufat/Kconfig"
config NILFS2_FS
tristate "NILFS2 file system support (EXPERIMENTAL)"
diff --git a/fs/Makefile b/fs/Makefile
index af6d047..9d2d469 100644
--- a/fs/Makefile
+++ b/fs/Makefile
@@ -124,3 +124,4 @@ obj-$(CONFIG_OCFS2_FS) += ocfs2/
obj-$(CONFIG_BTRFS_FS) += btrfs/
obj-$(CONFIG_GFS2_FS) += gfs2/
obj-$(CONFIG_EXOFS_FS) += exofs/
+obj-$(CONFIG_VMUFAT_FS) += vmufat/
diff --git a/fs/vmufat/Kconfig b/fs/vmufat/Kconfig
new file mode 100644
index 0000000..bdad4c6
--- /dev/null
+++ b/fs/vmufat/Kconfig
@@ -0,0 +1,14 @@
+config VMUFAT_FS
+ tristate "Dreamcast VMU FAT filesystem"
+ depends on BLOCK
+ help
+ This implements the simple FAT type filesystem found on SEGA
+ Dreamcast visual memory units.
+
+ Dreamcast users who want to mount their VMUs to view the native
+ filesystem will say 'Y' here. The filesystem is hardware independent
+ but is not recommended for any serious use in other circumstances, so
+ just about everyone else should say 'N'.
+
+ To compile this as a module say 'M' here. The module will be called
+ vmufat
diff --git a/fs/vmufat/Makefile b/fs/vmufat/Makefile
new file mode 100644
index 0000000..c716ead
--- /dev/null
+++ b/fs/vmufat/Makefile
@@ -0,0 +1,7 @@
+#
+# Makefile for VMUFAT filesystem
+#
+
+obj-$(CONFIG_VMUFAT_FS) += vmufat.o
+
+vmufat-objs := inode.o
diff --git a/fs/vmufat/inode.c b/fs/vmufat/inode.c
new file mode 100644
index 0000000..a65ac84
--- /dev/null
+++ b/fs/vmufat/inode.c
@@ -0,0 +1,1394 @@
+/*
+ * inode operations for the VMU file system
+ *
+ * Copyright (C) 2002 - 2009 Adrian McMenamin
+ * Copyright (C) 2002 Paul Mundt
+ *
+ * Released under the terms of the GNU GPL.
+ */
+#include <linux/module.h>
+#include <linux/init.h>
+#include <linux/fs.h>
+#include <linux/slab.h>
+#include <linux/blkdev.h>
+#include <linux/buffer_head.h>
+#include <linux/statfs.h>
+#include <linux/mpage.h>
+#include <linux/bcd.h>
+
+#define VMUFAT_NAMELEN 12
+
+/* GNU utils won't list files with inode num 0 */
+#define VMUFAT_ZEROBLOCK 32768
+#define VMU_BLK_SZ 512
+#define VMUFAT_MAGIC 0x55555555
+
+#define FATFILEEND 0xFFFA
+#define FATEMPTY 0xFFFC
+#define DIRRECLEN 0x20
+#define DIRRECBLK 0x10
+
+static struct kmem_cache *vmufat_inode_cachep;
+static struct kmem_cache *vmufat_blist_cachep;
+static const struct inode_operations vmufat_inode_operations;
+static const struct file_operations vmufat_file_operations;
+static const struct address_space_operations vmufat_address_space_operations;
+static const struct file_operations vmufat_file_dir_operations;
+static const struct super_operations vmufat_super_operations;
+
+static struct inode *vmufat_get_inode(struct super_block *sb, long ino);
+static int vmufat_list_blocks(struct inode *in);
+
+struct memcard {
+ long sb_bnum;
+ long fat_bnum;
+ long fat_len;
+ long dir_bnum;
+ long dir_len;
+ long numblocks;
+};
+
+struct vmufat_block_list {
+ struct list_head b_list;
+ int bno;
+};
+
+struct vmufat_inode {
+ struct vmufat_block_list blocks;
+ int nblcks;
+ struct inode vfs_inode;
+};
+
+static struct vmufat_inode *VMUFAT_I(struct inode *in)
+{
+ return container_of(in, struct vmufat_inode, vfs_inode);
+}
+
+struct vmufat_file_info {
+ u8 ftype;
+ u8 copy_pro;
+ u16 fblk;
+ char fname[VMUFATNAMELEN];
+};
+
+static inline int vmufat_index(int fno)
+{
+ return (fno % DIRRECBLK) * DIRRECLEN;
+}
+
+static inline int vmufat_index_16(int fno)
+{
+ return (fno % DIRRECBLK) * DIRECLEN / 2;
+}
+
+/* VMU hardware is flaky, so let's compensate for that
+ * without losing hardare independence -
+ * as it is likely to be where this filesystem is used
+ */
+static inline struct buffer_head *vmufat_sb_bread(struct super_block *sb,
+ sector_t block)
+{
+ struct buffer_head *bh;
+ bh = sb_bread(sb, block);
+ if (bh)
+ return bh;
+ return sb_bread(sb, block);
+}
+
+/* Linear day numbers of the respective 1sts in non-leap years. */
+static int day_n[] =
+ {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334};
+
+static struct dentry *vmufat_inode_lookup(struct inode *in, struct dentry *dent,
+ struct nameidata *nd)
+{
+ struct super_block *sb;
+ struct memcard *vmudetails;
+ struct buffer_head *bh;
+ struct inode *ino;
+ char name[VMUFAT_NAMELEN];
+ long blck_read;
+ int error = 0, fno = 0;
+
+ if (dent->d_name.len > VMUFAT_NAMELEN) {
+ error = -ENAMETOOLONG;
+ goto out;
+ }
+
+ sb = in->i_sb;
+ vmudetails = sb->s_fs_info;
+ blck_read = vmudetails->dir_bnum;
+
+ bh = vmufat_sb_bread(sb, blck_read);
+ if (!bh) {
+ error = -EIO;
+ goto out;
+ }
+
+ do {
+ /* Have we got a file? */
+ if (bh->b_data[vmufat_index(fno)] == 0)
+ goto next;
+
+ /* get file name */
+ memcpy(name,
+ bh->b_data + 4 + vmufat_index(fno), VMUFAT_NAMELEN);
+ /* do names match ?*/
+ if (memcmp(dent->d_name.name, name, dent->d_name.len) == 0) {
+ /* read the inode number from the directory */
+ ino = vmufat_get_inode(sb,
+ le16_to_cpu(((u16 *) bh->b_data)
+ [1 + vmufat_index_16(fno)]));
+ if (!ino) {
+ error = -EACCES;
+ goto release_bh;
+ }
+ if (IS_ERR(ino)) {
+ error = PTR_ERR(ino);
+ goto release_bh;
+ }
+ /* return the entry */
+ d_add(dent, ino);
+ goto release_bh;
+ }
+next:
+ /* did not match, so try the next file */
+ fno++;
+ /* do we need to move to the next block in the directory? */
+ if (fno >= 0x10) {
+ fno = 0;
+ blck_read--;
+ if (blck_read <=
+ vmudetails->dir_bnum - vmudetails->dir_len) {
+ d_add(dent, NULL);
+ break;
+ }
+ brelse(bh);
+ bh = vmufat_sb_bread(sb, blck_read);
+ if (!bh) {
+ error = -EIO;
+ goto out;
+ }
+ }
+ } while (1);
+
+release_bh:
+ brelse(bh);
+out:
+ return ERR_PTR(error);
+}
+
+/*
+ * Find a free block in the FAT and mark it
+ * as the end of a file
+ */
+static int vmufat_find_free(struct super_block *sb)
+{
+ struct memcard *vmudetails = sb->s_fs_info;
+ int nextblock, x;
+ u16 fatdata;
+ struct buffer_head *bh_fat;
+ int error = 0;
+
+ nextblock = vmudetails->fat_bnum + vmudetails->fat_len - 1;
+ x = VMU_BLK_SZ;
+ bh_fat =
+ vmufat_sb_bread(sb, nextblock);
+ if (!bh_fat) {
+ error = -EIO;
+ goto fail;
+ }
+
+ do {
+ fatdata = ((u16 *) bh_fat->b_data)[x];
+ if (fatdata == FATEMPTY)
+ break; /*empty block */
+ if (--x < 0) {
+ put_bh(bh_fat);
+ if (--nextblock >= vmudetails->fat_bnum) {
+ x = VMU_BLK_SZ;
+ bh_fat = vmufat_sb_bread(sb, nextblock);
+ if (!bh_fat) {
+ error = -EIO;
+ goto fail;
+ }
+ } else
+ break;
+ }
+ } while (1);
+
+ if (nextblock < vmudetails->fat_bnum) {
+ printk(KERN_ERR "VMUFAT: device is full\n");
+ error = -ENOSPC;
+ put_bh(bh_fat);
+ goto fail;
+ }
+ put_bh(bh_fat);
+ return x + (nextblock - vmudetails->fat_bnum) * VMU_BLK_SZ;
+
+fail:
+ return error;
+}
+
+/* read the FAT for a given block */
+static u16 vmufat_get_fat(struct super_block *sb, long block)
+{
+ struct memcard *vmudetails = sb->s_fs_info;
+ struct buffer_head *bh;
+ int offset;
+ u16 block_content;
+
+ offset = block/(VMU_BLK_SZ/2);
+ if (offset >= vmudetails->fat_len)
+ return 0xFFFE;
+
+ bh = vmufat_sb_bread(sb, offset + 1 +
+ vmudetails->fat_bnum - vmudetails->fat_len);
+ if (!bh)
+ return 0xFFFF;
+
+ block_content = ((u16 *)bh->b_data)[block % (VMU_BLK_SZ / 2)];
+ put_bh(bh);
+ return block_content;
+}
+
+/* set the FAT for a given block */
+static int vmufat_set_fat(struct super_block *sb, long block, u16 set)
+{
+ struct memcard *vmudetails = sb->s_fs_info;
+ struct buffer_head *bh;
+ int offset;
+
+ offset = block/(VMU_BLK_SZ/2);
+ if (offset >= vmudetails->fat_len)
+ return -EINVAL;
+
+ bh = vmufat_sb_bread(sb, offset + 1 +
+ vmudetails->fat_bnum - vmudetails->fat_len);
+ if (!bh)
+ return -EIO;
+
+ ((u16 *) bh->b_data)[block % (VMU_BLK_SZ / 2)] = set;
+ mark_buffer_dirty(bh);
+ put_bh(bh);
+ return 0;
+}
+
+static int vmufat_inode_create(struct inode *dir, struct dentry *de,
+ int imode, struct nameidata *nd)
+{
+ /* Create an inode */
+ int x = 0, y, z, error = 0, q;
+ long blck_read;
+ struct inode *inode;
+ struct super_block *sb;
+ struct memcard *vmudetails;
+ struct buffer_head *bh_fat = NULL, *bh;
+ unsigned long unix_date;
+ int year, day, nl_day, month; /*inspired by FAT driver */
+ u8 century, u8year;
+
+ if (de->d_name.len > VMUFAT_NAMELEN)
+ return -ENAMETOOLONG;
+
+ sb = dir->i_sb;
+ vmudetails = sb->s_fs_info;
+
+ inode = new_inode(sb);
+ if (!inode) {
+ error = -ENOSPC;
+ goto out;
+ }
+
+ /* Walk through blocks looking for place to write
+ * Is this an executible file? */
+ if (imode & 73) { /*Octal 111 */
+ inode->i_ino = VMUFAT_ZEROBLOCK;
+ /* But this already allocated? */
+ if (vmufat_get_fat(sb, 0) != FATEMPTY) {
+ printk(KERN_ERR
+ "VMUFAT: cannot write executible file to"
+ " filesystem - block 0 already allocated.\n");
+ error = -ENOSPC;
+ goto clean_inode;
+ }
+ q = 0;
+ } else {
+ q = vmufat_find_free(sb);
+ if (q < 0) {
+ error = q;
+ goto clean_inode;
+ }
+ inode->i_ino = q;
+ }
+
+ error = vmufat_set_fat(sb, q, FATFILEEND);
+ if (error)
+ goto clean_inode;
+
+ inode->i_uid = 0;
+ inode->i_gid = 0;
+ inode->i_mtime = inode->i_atime = inode->i_ctime = CURRENT_TIME;
+ inode->i_mode = imode;
+ inode->i_blocks = 1;
+ inode->i_sb = sb;
+ insert_inode_hash(inode);
+ inode->i_op = &vmufat_inode_operations;
+ inode->i_fop = &vmufat_file_operations;
+ inode->i_mapping->a_ops = &vmufat_address_space_operations;
+
+ /* Write to the directory
+ * Now search for space for the directory entry */
+ blck_read = vmudetails->dir_bnum;
+ bh = vmufat_sb_bread(sb, blck_read);
+ if (!bh) {
+ error = -EIO;
+ goto clean_inode;
+ }
+
+ for (y = 0; y < (vmudetails->dir_len * 0x10); y++) {
+ if ((y / 0x10) > (vmudetails->dir_bnum - blck_read)) {
+ brelse(bh);
+ blck_read--;
+ bh = vmufat_sb_bread(sb, blck_read);
+ if (!bh) {
+ error = -EIO;
+ goto clean_fat;
+ }
+ }
+ if (((bh->b_data)[vmufat_index(y)]) == 0)
+ break;
+ }
+ /* Have the directory entry
+ * so now update it */
+ z = vmufat_index(y);
+ if (inode->i_ino != VMUFAT_ZEROBLOCK)
+ bh->b_data[z] = 0x33; /* data file */
+ else
+ bh->b_data[z] = 0xcc;
+
+ if ((bh->b_data[z + 1] != (char) 0x00) &&
+ (bh->b_data[z + 1] != (char) 0xff))
+ bh->b_data[z + 1] = (char) 0x00;
+
+ if (inode->i_ino != VMUFAT_ZEROBLOCK) {
+ ((u16 *) bh->b_data)[z / 2 + 1] =
+ cpu_to_le16(inode->i_ino);
+ ((u16 *) bh->b_data)[z / 2 + 0x0D] = 0;
+ } else {
+ ((u16 *) bh->b_data)[z / 2 + 1] = 0;
+ ((u16 *) bh->b_data)[z / 2 + 0x0D] = 1;
+ }
+
+ /* Name */
+ memset((char *) (bh->b_data + z + 0x04), '\0', 0x0C);
+ memcpy((char *) (bh->b_data + z + 0x04), ((de->d_name).name),
+ de->d_name.len);
+
+ /* BCD timestamp it */
+ unix_date = CURRENT_TIME.tv_sec;
+ day = unix_date / 86400 - 3652;
+ year = day / 365;
+
+ if ((year + 3) / 4 + 365 * year > day)
+ year--;
+
+ day -= (year + 3) / 4 + 365 * year;
+ if (day == 59 && !(year & 3)) {
+ nl_day = day;
+ month = 2;
+ } else {
+ nl_day = (year & 3) || day <= 59 ? day : day - 1;
+ for (month = 0; month < 12; month++)
+ if (day_n[month] > nl_day)
+ break;
+ }
+
+ century = 19;
+ if (year > 19)
+ century = 20;
+
+ bh->b_data[z + 0x10] = bin2bcd(century);
+ u8year = year + 80;
+ if (u8year > 99)
+ u8year = u8year - 100;
+
+ bh->b_data[z + 0x11] = bin2bcd(u8year);
+ bh->b_data[z + 0x12] = bin2bcd(month);
+ bh->b_data[z + 0x13] =
+ bin2bcd(day - day_n[month - 1] + 1);
+ bh->b_data[z + 0x14] =
+ bin2bcd((unix_date / 3600) % 24);
+ bh->b_data[z + 0x15] = bin2bcd((unix_date / 60) % 60);
+ bh->b_data[z + 0x16] = bin2bcd(unix_date % 60);
+
+ ((u16 *) bh->b_data)[z / 2 + 0x0C] =
+ cpu_to_le16(inode->i_blocks);
+
+ inode->i_mtime.tv_sec = unix_date;
+ mark_buffer_dirty(bh);
+ brelse(bh);
+
+ error = vmufat_list_blocks(inode);
+ if (error)
+ goto clean_fat;
+
+ d_instantiate(de, inode);
+ brelse(bh_fat);
+ return error;
+
+clean_fat:
+ ((u16 *)bh_fat->b_data)[x] = FATEMPTY;
+ mark_buffer_dirty(bh_fat);
+ brelse(bh_fat);
+clean_inode:
+ iput(inode);
+out:
+ return error;
+}
+
+static int vmufat_readdir(struct file *filp, void *dirent, filldir_t filldir)
+{
+ int filenamelen, i, error = 0;
+ struct vmufat_file_info *saved_file = NULL;
+ struct dentry *dentry = filp->f_dentry;
+ struct inode *inode = dentry->d_inode;
+ struct super_block *sb = inode->i_sb;
+ struct memcard *vmudetails = sb->s_fs_info;
+ struct buffer_head *bh;
+
+ int blck_read = vmudetails->dir_bnum;
+ bh = vmufat_sb_bread(sb, blck_read);
+ if (!bh) {
+ error = -EIO;
+ goto out;
+ }
+
+ i = filp->f_pos;
+
+ /* handle . for this directory and .. for parent */
+ switch ((unsigned int) filp->f_pos) {
+ case 0:
+ if (filldir(dirent, ".", 1, i++, inode->i_ino, DT_DIR) < 0)
+ goto finish;
+
+ filp->f_pos++;
+ case 1:
+ if (filldir(dirent, "..", 2, i++,
+ dentry->d_parent->d_inode->i_ino, DT_DIR) < 0)
+ goto finish;
+
+ filp->f_pos++;
+ default:
+ break;
+ }
+
+ /* trap reading beyond the end of the directory */
+ if ((i - 2) > (vmudetails->dir_len * 0x10)) {
+ error = -EINVAL;
+ goto release_bh;
+ }
+
+ saved_file =
+ kmalloc(sizeof(struct vmufat_file_info), GFP_KERNEL);
+ if (!saved_file) {
+ error = -ENOMEM;
+ goto release_bh;
+ }
+
+ do {
+ if ((i - 2) / 0x10 > (vmudetails->dir_bnum - blck_read)) {
+ /* move to next block in directory */
+ blck_read--;
+ if (vmudetails->dir_bnum - vmudetails->dir_len <=
+ blck_read)
+ break;
+ brelse(bh);
+ bh = vmufat_sb_bread(sb, blck_read);
+ if (!bh) {
+ kfree(saved_file);
+ error = -EIO;
+ goto out;
+ }
+ }
+
+ saved_file->ftype = bh->b_data[vmufat_index(i - 2)];
+
+ if (saved_file->ftype == 0)
+ break;
+
+ saved_file->fblk =
+ le16_to_cpu(((u16 *) bh->b_data)[1 +
+ vmufat_index_16(i - 2)]);
+ if (saved_file->fblk == 0)
+ saved_file->fblk = VMUFAT_ZEROBLOCK;
+
+ memcpy(saved_file->fname,
+ bh->b_data + 4 + vmufat_index(i - 2), VMUFAT_NAMELEN);
+ filenamelen = strlen(saved_file->fname);
+ if (filenamelen > VMUFAT_NAMELEN)
+ filenamelen = VMUFAT_NAMELEN;
+ if (filldir
+ (dirent, saved_file->fname, filenamelen, i++,
+ saved_file->fblk, DT_REG) < 0) {
+ goto finish;
+ }
+
+ filp->f_pos++;
+ } while (1);
+
+finish:
+ kfree(saved_file);
+release_bh:
+ brelse(bh);
+out:
+ return error;
+}
+
+
+
+static long vmufat_get_date(struct buffer_head *bh, int offset)
+{
+ int century, year, month, day, hour, minute, second;
+
+ century = bcd2bin(bh->b_data[offset++]);
+ year = bcd2bin(bh->b_data[offset++]);
+ month = bcd2bin(bh->b_data[offset++]);
+ day = bcd2bin(bh->b_data[offset++]);
+ hour = bcd2bin(bh->b_data[offset++]);
+ minute = bcd2bin(bh->b_data[offset++]);
+ second = bcd2bin(bh->b_data[offset]);
+
+ return mktime(century * 100 + year, month, day, hour, minute,
+ second);
+}
+
+static struct inode *vmufat_alloc_inode(struct super_block *sb)
+{
+ struct vmufat_inode *vi = kmem_cache_alloc(vmufat_inode_cachep,
+ GFP_KERNEL);
+
+ if (!vi)
+ return NULL;
+ INIT_LIST_HEAD(&vi->blocks.b_list);
+ return &vi->vfs_inode;
+}
+
+static void vmufat_destroy_inode(struct inode *in)
+{
+ struct vmufat_inode *vi = VMUFAT_I(in);
+ struct vmufat_block_list *vb;
+ struct list_head *iter, *iter2;
+
+ list_for_each_safe(iter, iter2, &vi->blocks.b_list) {
+ vb = list_entry(iter, struct vmufat_block_list, b_list);
+ list_del(iter);
+ kmem_cache_free(vmufat_blist_cachep, vb);
+ }
+ kmem_cache_free(vmufat_inode_cachep, vi);
+}
+
+static int vmufat_list_blocks(struct inode *in)
+{
+ struct vmufat_inode *vi = VMUFAT_I(in);
+ struct super_block *sb = in->i_sb;
+ long nextblock;
+ long ino = in->i_ino;
+ int fat_seg;
+ struct buffer_head *bh;
+ struct memcard *vmudetails;
+ int error;
+ struct list_head *iter, *iter2;
+ struct vmufat_block_list *vbl, *nvbl;
+ u16 fatdata;
+
+ vmudetails = sb->s_fs_info;
+ nextblock = ino;
+ fat_seg = nextblock / (sb->s_blocksize / 2);
+ bh = vmufat_sb_bread(sb, vmudetails->fat_bnum -
+ (vmudetails->fat_len - 1) + fat_seg);
+ if (!bh) {
+ error = -EIO;
+ goto out;
+ }
+
+ /* Delete any previous list of blocks */
+ list_for_each_safe(iter, iter2, &vi->blocks.b_list) {
+ vbl = list_entry(iter, struct vmufat_block_list, b_list);
+ list_del(iter);
+ kmem_cache_free(vmufat_blist_cachep, vbl);
+ }
+ vi->nblcks = 0;
+ do {
+ vbl = kmem_cache_alloc(vmufat_blist_cachep,
+ GFP_KERNEL);
+ if (!vbl) {
+ error = -ENOMEM;
+ goto unwind_out;
+ }
+ INIT_LIST_HEAD(&vbl->b_list);
+ vbl->bno = nextblock;
+ list_add_tail(&vbl->b_list, &vi->blocks.b_list);
+ vi->nblcks++;
+
+ /* Find next block in the FAT - if there is one */
+ fatdata = ((u16 *) bh->b_data)
+ [nextblock % (sb->s_blocksize / 2)];
+ if (fatdata == FATEMPTY) {
+ printk(KERN_WARNING "VMUFAT: FAT table appears to have"
+ " been corrupted.\n");
+ error = -EIO;
+ goto unwind_out;
+ }
+ if (fatdata == FATFILEEND)
+ break; /*end of file */
+ if (fatdata / (sb->s_blocksize / 2) != fat_seg) {
+ brelse(bh);
+ fat_seg = fatdata / (sb->s_blocksize / 2);
+ bh = vmufat_sb_bread(sb, vmudetails->fat_bnum -
+ (vmudetails->fat_len - 1) + fat_seg);
+ if (!bh) {
+ error = -ENOMEM;
+ goto unwind_out;
+ }
+ }
+ nextblock = fatdata;
+ } while (1);
+ brelse(bh);
+ return 0;
+
+unwind_out:
+ list_for_each_entry_safe(vbl, nvbl, &vi->blocks.b_list, b_list) {
+ list_del_init(&vbl->b_list);
+ kmem_cache_free(vmufat_blist_cachep, vbl);
+ }
+out:
+ return error;
+}
+
+static struct inode *vmufat_get_inode(struct super_block *sb, long ino)
+{
+ struct buffer_head *bh;
+ int error, blck_read, y, z;
+ struct inode *inode = iget_locked(sb, ino);
+ struct memcard *vmudetails = sb->s_fs_info;
+ long superblock_bno = vmudetails->sb_bnum;
+
+ if (inode && (inode->i_state & I_NEW)) {
+ inode->i_uid = 0;
+ inode->i_gid = 0;
+ inode->i_mode &= ~S_IFMT;
+ if (inode->i_ino == superblock_bno) {
+ bh = vmufat_sb_bread(sb, inode->i_ino);
+ if (!bh) {
+ error = -EIO;
+ goto failed;
+ }
+ inode->i_ctime.tv_sec = inode->i_mtime.tv_sec =
+ vmufat_get_date(bh, 0x30);
+
+ /* Mark as a directory */
+ inode->i_mode = S_IFDIR | S_IRUGO | S_IXUGO;
+
+ inode->i_op = &vmufat_inode_operations;
+ inode->i_fop = &vmufat_file_dir_operations;
+ } else {
+ blck_read = vmudetails->dir_bnum;
+ bh = vmufat_sb_bread(sb, blck_read);
+ if (!bh) {
+ error = -EIO;
+ goto failed;
+ }
+
+ /* Mark file as regular type */
+ inode->i_mode = S_IFREG;
+
+ /* Scan through the directory to find matching file */
+ for (y = 0; y < vmudetails->numblocks; y++) {
+ if ((y / 0x10) >
+ (vmudetails->dir_bnum - blck_read)) {
+ brelse(bh);
+ blck_read--;
+ bh = vmufat_sb_bread(sb, blck_read);
+ if (!bh) {
+ error = -EIO;
+ goto failed;
+ }
+ }
+ if (le16_to_cpu
+ (((u16 *) bh->b_data)[(y % 0x10) * 0x10 +
+ 0x01]) == ino)
+ break;
+ }
+
+ if (y >= vmudetails->numblocks) {
+ brelse(bh);
+ printk(KERN_INFO
+ "vmufat: could not find this "
+ "file on filesystem\n");
+ error = -ENOENT;
+ goto failed;
+ }
+
+ /* identified the correct directory entry */
+ z = vmufat_index(y);
+ inode->i_ctime.tv_sec = inode->i_mtime.tv_sec =
+ vmufat_get_date(bh, z + 0x10);
+
+ /* Execute if a game, write if not copy protected */
+ inode->i_mode &= ~(S_IWUGO | S_IXUGO);
+ inode->i_mode |= S_IRUGO;
+
+ /* Mode - is it write protected? */
+ if ((((u8 *) bh->b_data)[0x01 + z] ==
+ 0x00) & ~(sb->s_flags & MS_RDONLY))
+ inode->i_mode |= S_IWUGO;
+ /* Is file executible - ie a game */
+ if ((((u8 *) bh->b_data)[z] ==
+ 0xcc) & ~(sb->s_flags & MS_NOEXEC))
+ inode->i_mode |= S_IXUGO;
+
+ inode->i_fop = &vmufat_file_operations;
+
+ inode->i_blocks =
+ le16_to_cpu(((u16 *) bh->b_data)
+ [vmufat_index_16(y) + 0x0C]);
+ inode->i_size = inode->i_blocks * sb->s_blocksize;
+
+ inode->i_mapping->a_ops =
+ &vmufat_address_space_operations;
+ inode->i_op = &vmufat_inode_operations;
+ inode->i_fop = &vmufat_file_operations;
+ error = vmufat_list_blocks(inode);
+ if (error)
+ goto failed;
+ }
+ inode->i_atime = CURRENT_TIME;
+ unlock_new_inode(inode);
+ }
+
+ return inode;
+
+failed:
+ iget_failed(inode);
+ return ERR_PTR(error);
+}
+
+static void vmufat_put_super(struct super_block *sb)
+{
+ sb->s_dev = 0;
+ kfree(sb->s_fs_info);
+}
+
+static int vmufat_scan(struct super_block *sb, struct kstatfs *buf)
+{
+ int error = 0;
+ int free = 0;
+ int x;
+ u16 fatdata;
+ struct buffer_head *bh_fat;
+ struct memcard *vmudetails = sb->s_fs_info;
+ long nextblock;
+
+ /* Look through the FAT */
+ nextblock = vmudetails->fat_bnum + vmudetails->fat_len - 1;
+ x = sb->s_blocksize;
+ bh_fat = vmufat_sb_bread(sb, nextblock);
+ if (!bh_fat) {
+ error = -EIO;
+ goto out;
+ }
+
+ do {
+ fatdata = ((u16 *) bh_fat->b_data)[x];
+ if (fatdata == FATEMPTY)
+ free++;
+ if (--x < 0) {
+ brelse(bh_fat);
+ if (--nextblock >= vmudetails->fat_bnum) {
+ x = sb->s_blocksize;
+ bh_fat = vmufat_sb_bread(sb, nextblock);
+ if (!bh_fat) {
+ error = -EIO;
+ goto out;
+ }
+ } else
+ break;
+ }
+ } while (1);
+
+ buf->f_bfree = free;
+ buf->f_bavail = free;
+ buf->f_blocks = vmudetails->numblocks;
+
+out:
+ return error;
+}
+
+static int vmufat_statfs(struct dentry *dentry, struct kstatfs *buf)
+{
+ struct super_block *sb = dentry->d_sb;
+ int error;
+
+ error = vmufat_scan(sb, buf);
+ if (error)
+ return error;
+ buf->f_type = VMUFAT_MAGIC;
+ buf->f_bsize = sb->s_blocksize;
+ buf->f_namelen = VMUFAT_NAMELEN;
+
+ return 0;
+}
+
+/*
+ * Delete inode by marking space as free in FAT
+ * no need to waste time and effort by actually
+ * wiping underlying data on media
+ */
+static void vmufat_delete_inode(struct inode *in)
+{
+ struct buffer_head *bh, *bh_old;
+ struct super_block *sb;
+ struct memcard *vmudetails;
+ int z, y, x, w, v, blck_read, error;
+ u16 nextblock, fatdata;
+
+ if (in->i_ino == VMUFAT_ZEROBLOCK)
+ in->i_ino = 0;
+ sb = in->i_sb;
+ vmudetails = sb->s_fs_info;
+ if (in->i_ino > vmudetails->fat_len * sb->s_blocksize / 2) {
+ printk(KERN_ERR "vmufat: attempting to delete"
+ "inode beyond device size");
+ return;
+ }
+
+ /* Seek start of file and wander through FAT
+ * Marking the blocks as unallocated */
+ nextblock = in->i_ino;
+ do {
+ fatdata = vmufat_get_fat(sb, nextblock);
+ error = vmufat_set_fat(sb, nextblock, FATEMPTY);
+ if (error)
+ goto failure;
+ if (fatdata == FATFILEEND)
+ break;
+ nextblock = fatdata;
+ } while (1);
+
+ /* Now clean the directory entry
+ * Have to wander through this
+ * to find the appropriate entry */
+ blck_read = vmudetails->dir_bnum;
+ bh = vmufat_sb_bread(sb, blck_read);
+ if (!bh)
+ goto failure;
+
+ for (y = 0; y < (vmudetails->dir_len * 0x10); y++) {
+ if ((y / 0x10) > (vmudetails->dir_bnum - blck_read)) {
+ brelse(bh);
+ blck_read--;
+ bh = vmufat_sb_bread(sb, blck_read);
+ if (!bh)
+ goto failure;
+ }
+ if (le16_to_cpu
+ (((u16 *) bh->b_data)[(y % 0x10) * 0x10 +
+ 0x01]) == in->i_ino)
+ break;
+ }
+
+ /* Found directory entry - so NULL it now */
+ w = vmufat_index_16(y);
+ for (z = 0; z < 0x10; z++)
+ ((u16 *) bh->b_data)[w + z] = 0;
+ mark_buffer_dirty(bh);
+ /* Replace it with another entry - if one exists */
+ x = y;
+ for (y = x + 1; y < (vmudetails->dir_len * 0x10); y++) {
+ if ((y / 0x10) > (vmudetails->dir_bnum - blck_read)) {
+ brelse(bh);
+ blck_read--;
+ bh = vmufat_sb_bread(sb, blck_read);
+ if (!bh)
+ return;
+ }
+ /* look for the end of entries in the directory */
+ if (bh->b_data[vmufat_index(y)] == 0) {
+ y--;
+ if (y == x)
+ break; /* At the end in any case */
+ brelse(bh);
+
+ /* force read of correct block */
+ bh = vmufat_sb_bread(sb, vmudetails->dir_bnum -
+ y / 0x10);
+ if (!bh)
+ goto failure;
+ bh_old =
+ vmufat_sb_bread(sb, vmudetails->dir_bnum -
+ x / 0x10);
+ if (!bh_old) {
+ brelse(bh);
+ goto failure;
+ }
+
+ /*
+ * Copy final directory entry into space created
+ * by the deletion of the inode
+ */
+ w = vmufat_index_16(x);
+ v = vmufat_index_16(y);
+ for (z = 0; z < 0x10; z++) {
+ ((u16 *) bh_old->b_data)[w + z] =
+ ((u16 *) bh->b_data)[v + z];
+ ((u16 *) bh->b_data)[v + z] = 0;
+ }
+ mark_buffer_dirty(bh);
+ if (x/0x10 != y/0x10)
+ mark_buffer_dirty(bh_old);
+ brelse(bh_old);
+ break;
+ }
+ }
+
+ brelse(bh);
+ return;
+
+failure:
+ printk(KERN_ERR "vmufat: Failure to read device,"
+ " could not delete inode - filesystem may be damaged\n");
+ return;
+}
+
+/*
+ * vmufat_unlink - delete a file pointed to
+ * by the dentry (only one directory in a
+ * vmufat fs so safe to ignore the inode
+ * upplied here
+ */
+static int vmufat_unlink(struct inode *dir, struct dentry *dentry)
+{
+ struct inode *in;
+
+ in = dentry->d_inode;
+ if (!in)
+ return -EIO;
+ vmufat_delete_inode(in);
+ return 0;
+}
+
+static int vmufat_get_block(struct inode *inode, sector_t iblock,
+ struct buffer_head *bh_result, int create)
+{
+ struct vmufat_inode *vin = VMUFAT_I(inode);
+ struct vmufat_block_list *vlist = &vin->blocks;
+ struct vmufat_block_list *vblk;
+ struct super_block *sb = inode->i_sb;
+ int cural;
+ int finblk, nxtblk, exeblk;
+ struct list_head *iter;
+ sector_t cntdwn = iblock;
+ sector_t phys;
+ int error = -EINVAL;
+
+ /* quick sanity check */
+ if (vin->nblcks <= 0)
+ goto out;
+ if (iblock < vin->nblcks) {
+ /* block is already here so read it into the buffer head */
+ list_for_each(iter, &vlist->b_list) {
+ if (cntdwn-- == 0)
+ break;
+ }
+ vblk = list_entry(iter, struct vmufat_block_list, b_list);
+ clear_buffer_new(bh_result);
+ error = 0;
+ phys = vblk->bno;
+ goto got_it;
+ }
+ if (!create) {
+ error = -EINVAL;
+ goto out;
+ }
+ /*
+ * check not looking for a block too far
+ * beyond the end of the existing file
+ */
+ if (iblock > vin->nblcks) {
+ error = -EINVAL;
+ goto out;
+ }
+
+ /* if looking for a block that is not current - allocate it*/
+ cural = vin->nblcks;
+ list_for_each(iter, &vlist->b_list) {
+ if (cural-- == 1)
+ break;
+ }
+ vblk = list_entry(iter, struct vmufat_block_list, b_list);
+ finblk = vblk->bno;
+
+ /* Exec files have to be linear */
+ if (inode->i_ino == 0) {
+ exeblk = vmufat_get_fat(sb, finblk + 1);
+ if (exeblk != FATEMPTY) {
+ printk(KERN_WARNING "Cannot allocate linear space "
+ "needed for executible\n");
+ error = -ENOSPC;
+ goto out;
+ }
+ nxtblk = finblk + 1;
+ } else {
+ nxtblk = vmufat_find_free(sb);
+ if (nxtblk < 0) {
+ error = nxtblk;
+ goto out;
+ }
+ }
+ error = vmufat_set_fat(sb, finblk, nxtblk);
+ if (error)
+ goto out;
+ error = vmufat_set_fat(sb, nxtblk, FATFILEEND);
+ if (error)
+ goto out;
+ error = vmufat_list_blocks(inode);
+ mark_inode_dirty(inode);
+ if (error)
+ goto out;
+ set_buffer_new(bh_result);
+ phys = nxtblk;
+ error = 0;
+got_it:
+ map_bh(bh_result, sb, phys);
+out:
+ return error;
+}
+
+static int vmufat_writepage(struct page *page, struct writeback_control *wbc)
+{
+ return block_write_full_page(page, vmufat_get_block, wbc);
+}
+
+static int vmufat_write_begin(struct file *file, struct address_space *mapping,
+ loff_t pos, unsigned len, unsigned flags,
+ struct page **pagep, void **fsdata)
+{
+ *pagep = NULL;
+ return block_write_begin(file, mapping, pos, len, flags, pagep, fsdata,
+ vmufat_get_block);
+}
+
+static int vmufat_readpage(struct file *file, struct page *page)
+{
+ return block_read_full_page(page, vmufat_get_block);
+}
+
+/*
+ * There are no inodes on the medium - vmufat_write_inode
+ * updates the directory entry
+ */
+static int vmufat_write_inode(struct inode *in, int wait)
+{
+ struct buffer_head *bh;
+ __u8 century, u8year;
+ unsigned long inode_num;
+ int y, blck_read, z;
+ unsigned long unix_date;
+ int year, day, nl_day, month;
+ struct super_block *sb = in->i_sb;
+ struct memcard *vmudetails =
+ ((struct memcard *) sb->s_fs_info);
+
+ /* As most real world devices are flash
+ * we won't update the superblock every
+ * time we change something else on the fs
+ * - it is ugly but a sensible compromise
+ */
+ if (in->i_ino == vmudetails->sb_bnum)
+ return 0;
+
+ if (in->i_ino == VMUFAT_ZEROBLOCK)
+ inode_num = 0;
+ else
+ inode_num = in->i_ino;
+
+ /* update the directory and inode details */
+ /* Now search for the directory entry */
+ blck_read = vmudetails->dir_bnum;
+ bh = vmufat_sb_bread(sb, blck_read);
+ if (!bh)
+ return -EIO;
+
+ for (y = 0; y < vmudetails->numblocks; y++) {
+ if ((y / 0x10) > (vmudetails->dir_bnum - blck_read)) {
+ brelse(bh);
+ blck_read--;
+ bh = vmufat_sb_bread(sb, blck_read);
+ if (!bh)
+ return -EIO;
+ }
+ if (le16_to_cpu
+ (((__u16 *) bh->b_data)[vmufat_index_16(y) +
+ 0x01]) == inode_num)
+ break;
+ }
+ /* Have the directory entry
+ * so now update it */
+ z = (y % 0x10) * 0x20;
+ if (inode_num != 0)
+ bh->b_data[z] = 0x33; /* data file */
+ else
+ bh->b_data[z] = 0xcc;
+ if (bh->b_data[z + 1] != 0
+ && bh->b_data[z + 1] != (char) 0xff)
+ bh->b_data[z + 1] = 0;
+ ((__u16 *) bh->b_data)[z / 2 + 1] = cpu_to_le16(inode_num);
+
+ /* BCD timestamp it */
+ unix_date = CURRENT_TIME.tv_sec;
+ day = unix_date / 86400 - 3652;
+ year = day / 365;
+ if ((year + 3) / 4 + 365 * year > day)
+ year--;
+ day -= (year + 3) / 4 + 365 * year;
+ if (day == 59 && !(year & 3)) {
+ nl_day = day;
+ month = 2;
+ } else {
+ nl_day = (year & 3) || day <= 59 ? day : day - 1;
+ for (month = 0; month < 12; month++)
+ if (day_n[month] > nl_day)
+ break;
+ }
+
+ century = 19;
+ if (year > 19)
+ century = 20;
+ bh->b_data[z + 0x10] = bin2bcd(century);
+ u8year = year + 80;
+ if (u8year > 99)
+ u8year = u8year - 100;
+ bh->b_data[z + 0x11] = bin2bcd(u8year);
+ bh->b_data[z + 0x12] = bin2bcd(month);
+ bh->b_data[z + 0x13] = bin2bcd(day - day_n[month - 1] + 1);
+ bh->b_data[z + 0x14] = bin2bcd((unix_date / 3600) % 24);
+ bh->b_data[z + 0x15] = bin2bcd((unix_date / 60) % 60);
+ bh->b_data[z + 0x16] = bin2bcd(unix_date % 60);
+
+ ((__u16 *) bh->b_data)[z / 2 + 0x0C] = cpu_to_le16(in->i_blocks);
+ if (inode_num != 0)
+ ((__u16 *) bh->b_data)[z / 2 + 0x0D] = 0;
+ else /* game */
+ ((__u16 *) bh->b_data)[z / 2 + 0x0D] = cpu_to_le16(1);
+ in->i_mtime.tv_sec = unix_date;
+ mark_buffer_dirty(bh);
+ brelse(bh);
+ return 0;
+}
+
+static int check_sb_format(struct buffer_head *bh)
+{
+ u32 s_magic = VMUFAT_MAGIC;
+
+ if (!(((u32 *) bh->b_data)[0] == s_magic &&
+ ((u32 *) bh->b_data)[1] == s_magic &&
+ ((u32 *) bh->b_data)[2] == s_magic &&
+ ((u32 *) bh->b_data)[3] == s_magic))
+ return 0;
+ else
+ return 1;
+}
+
+static void init_once(void *foo)
+{
+ struct vmufat_inode *vi = foo;
+
+ vi->nblcks = 0;
+ inode_init_once(&vi->vfs_inode);
+}
+
+
+static int init_inodecache(void)
+{
+ vmufat_inode_cachep = kmem_cache_create("vmufat_inode_cache",
+ sizeof(struct vmufat_inode), 0,
+ SLAB_RECLAIM_ACCOUNT|SLAB_MEM_SPREAD, init_once);
+ if (!vmufat_inode_cachep)
+ return -ENOMEM;
+
+ vmufat_blist_cachep = kmem_cache_create("vmufat_blocklist_cache",
+ sizeof(struct vmufat_block_list), 0, SLAB_MEM_SPREAD, NULL);
+ if (!vmufat_blist_cachep) {
+ kmem_cache_destroy(vmufat_inode_cachep);
+ return -ENOMEM;
+ }
+ return 0;
+}
+
+static void destroy_inodecache(void)
+{
+ kmem_cache_destroy(vmufat_blist_cachep);
+ kmem_cache_destroy(vmufat_inode_cachep);
+}
+
+static int vmufat_fill_super(struct super_block *sb,
+ void *data, int silent)
+{
+ /*Search for the superblock - assuming likely sizes are
+ * 128, 256, 512 and 1024 blocks */
+
+ struct buffer_head *bh;
+ struct memcard *vmudata;
+ int z;
+ struct inode *root_i;
+ int ret = -EINVAL;
+
+ sb_set_blocksize(sb, VMU_BLK_SZ);
+
+ /* Have to try lowest number first to avoid oops */
+ for (z = 128; z < 1024; z = z * 2) {
+ bh = vmufat_sb_bread(sb, z - 1);
+ if (!bh) {
+ ret = -EIO;
+ goto out;
+ }
+ if (check_sb_format(bh))
+ break;
+ brelse(bh);
+ if (z == 1024) { /* failed */
+ printk(KERN_ERR
+ "vmufat: attempted to mount non vmufat medium"
+ " as vmufat\n");
+ goto out;
+ }
+ }
+ /* Store this data in the super block */
+
+ vmudata = kmalloc(sizeof(struct memcard), GFP_KERNEL);
+ if (!vmudata) {
+ ret = -ENOMEM;
+ goto freebh_out;
+ }
+
+ /* user blocks */
+ vmudata->sb_bnum = z - 1;
+ vmudata->fat_bnum = le16_to_cpu(((u16 *) bh->b_data)[0x46 / 2]);
+ vmudata->fat_len = le16_to_cpu(((u16 *) bh->b_data)[0x48 / 2]);
+ vmudata->dir_bnum = le16_to_cpu(((u16 *) bh->b_data)[0x4a / 2]);
+ vmudata->dir_len = le16_to_cpu(((u16 *) bh->b_data)[0x4c / 2]);
+ /* return the true number of user available blocks - VMUs
+ * return a neat 200 and ignore 40 blocks of usable space -
+ * we get round that in a hardware neutral way */
+ vmudata->numblocks = vmudata->dir_bnum - vmudata->dir_len + 1;
+ sb->s_fs_info = vmudata;
+
+ sb->s_blocksize_bits = ilog2(VMU_BLK_SZ);
+ sb->s_magic = VMUFAT_MAGIC;
+ sb->s_op = &vmufat_super_operations;
+
+ root_i = vmufat_get_inode(sb, vmudata->sb_bnum);
+ if (!root_i) {
+ printk(KERN_ERR "vmufat: get root inode failed\n");
+ ret = -ENOMEM;
+ goto freevmudata_out;
+ }
+ if (IS_ERR(root_i)) {
+ printk(KERN_ERR "vmufat: get root inode failed - error 0x%lX\n",
+ PTR_ERR(root_i));
+ ret = PTR_ERR(root_i);
+ goto freevmudata_out;
+ }
+
+ sb->s_root = d_alloc_root(root_i);
+
+ if (!sb->s_root) {
+ ret = -EIO;
+ goto freeroot_out;
+ }
+ return 0;
+
+freeroot_out:
+ iput(root_i);
+freevmudata_out:
+ kfree(vmudata);
+freebh_out:
+ brelse(bh);
+out:
+ return ret;
+
+}
+
+static const struct address_space_operations vmufat_address_space_operations = {
+ .readpage = vmufat_readpage,
+ .writepage = vmufat_writepage,
+ .write_begin = vmufat_write_begin,
+ .write_end = generic_write_end,
+};
+
+static const struct super_operations vmufat_super_operations = {
+ .alloc_inode = vmufat_alloc_inode,
+ .destroy_inode = vmufat_destroy_inode,
+ .write_inode = vmufat_write_inode,
+ .delete_inode = vmufat_delete_inode,
+ .put_super = vmufat_put_super,
+ .statfs = vmufat_statfs,
+};
+
+static const struct inode_operations vmufat_inode_operations = {
+ .lookup = vmufat_inode_lookup,
+ .create = vmufat_inode_create,
+ .unlink = vmufat_unlink,
+};
+
+static const struct file_operations vmufat_file_dir_operations = {
+ .owner = THIS_MODULE,
+ .read = generic_read_dir,
+ .readdir = vmufat_readdir,
+ .fsync = file_fsync,
+};
+
+static const struct file_operations vmufat_file_operations = {
+ .llseek = generic_file_llseek,
+ .read = do_sync_read,
+ .write = do_sync_write,
+ .aio_read = generic_file_aio_read,
+ .aio_write = generic_file_aio_write,
+};
+
+static int vmufat_get_sb(struct file_system_type *fs_type,
+ int flags, const char *dev_name, void *data, struct vfsmount *mnt)
+{
+ return get_sb_bdev(fs_type, flags, dev_name, data, vmufat_fill_super,
+ mnt);
+}
+
+static struct file_system_type vmufat_fs_type = {
+ .owner = THIS_MODULE,
+ .name = "vmufat",
+ .get_sb = vmufat_get_sb,
+ .kill_sb = kill_block_super,
+ .fs_flags = FS_REQUIRES_DEV,
+};
+
+static int __init init_vmufat_fs(void)
+{
+ int err;
+ err = init_inodecache();
+ if (err)
+ return err;
+ return register_filesystem(&vmufat_fs_type);
+}
+
+static void __exit exit_vmufat_fs(void)
+{
+ destroy_inodecache();
+ unregister_filesystem(&vmufat_fs_type);
+}
+
+module_init(init_vmufat_fs);
+module_exit(exit_vmufat_fs);
+
+MODULE_DESCRIPTION("Filesystem for Sega Dreamcast VMU");
+MODULE_AUTHOR("Adrian McMenamin <adrian@...en.demon.co.uk>");
+MODULE_LICENSE("GPL");
--
To unsubscribe from this list: send the line "unsubscribe linux-kernel" in
the body of a message to majordomo@...r.kernel.org
More majordomo info at http://vger.kernel.org/majordomo-info.html
Please read the FAQ at http://www.tux.org/lkml/
Powered by blists - more mailing lists