/* jfs.c - JFS.  */
/*
 *  GRUB  --  GRand Unified Bootloader
 *  Copyright (C) 2004,2005,2006,2007,2008,2009  Free Software Foundation, Inc.
 *
 *  GRUB is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  GRUB is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with GRUB.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <grub/err.h>
#include <grub/file.h>
#include <grub/mm.h>
#include <grub/misc.h>
#include <grub/disk.h>
#include <grub/types.h>
#include <grub/charset.h>

GRUB_MOD_LICENSE("GPLv3+");

#define GRUB_JFS_MAX_SYMLNK_CNT	8
#define GRUB_JFS_FILETYPE_MASK	0170000
#define GRUB_JFS_FILETYPE_REG	0100000
#define GRUB_JFS_FILETYPE_LNK	0120000
#define GRUB_JFS_FILETYPE_DIR	0040000

#define GRUB_JFS_SBLOCK		64
#define GRUB_JFS_AGGR_INODE	2
#define GRUB_JFS_FS1_INODE_BLK	104

#define GRUB_JFS_TREE_LEAF	2

struct grub_jfs_sblock
{
	/* The magic for JFS.  It should contain the string "JFS1".  */
	grub_uint8_t magic[4];
	grub_uint32_t version;
	grub_uint64_t ag_size;

	/* The size of a filesystem block in bytes.  XXX: currently only
	   4096 was tested.  */
	grub_uint32_t blksz;
	grub_uint16_t log2_blksz;
	grub_uint8_t unused[14];
	grub_uint32_t flags;
	grub_uint8_t unused3[61];
	char volname[11];
	grub_uint8_t unused2[24];
	grub_uint8_t uuid[16];
	char volname2[16];
};

GRUB_PACKED_START
struct grub_jfs_extent
{
	/* The length of the extent in filesystem blocks.  */
	grub_uint16_t length;
	grub_uint8_t length2;

	/* The physical offset of the first block on the disk.  */
	grub_uint8_t blk1;
	grub_uint32_t blk2;
};
GRUB_PACKED_END

#define GRUB_JFS_IAG_INODES_OFFSET 3072
#define GRUB_JFS_IAG_INODES_COUNT 128

GRUB_PACKED_START
struct grub_jfs_iag
{
	grub_uint8_t unused[GRUB_JFS_IAG_INODES_OFFSET];
	struct grub_jfs_extent inodes[GRUB_JFS_IAG_INODES_COUNT];
};
GRUB_PACKED_END

/* The head of the tree used to find extents.  */
GRUB_PACKED_START
struct grub_jfs_treehead
{
	grub_uint64_t next;
	grub_uint64_t prev;

	grub_uint8_t flags;
	grub_uint8_t unused;

	grub_uint16_t count;
	grub_uint16_t max;
	grub_uint8_t unused2[10];
};
GRUB_PACKED_END

/* A node in the extent tree.  */
GRUB_PACKED_START
struct grub_jfs_tree_extent
{
	grub_uint8_t flags;
	grub_uint16_t unused;

	/* The offset is the key used to lookup an extent.  */
	grub_uint8_t offset1;
	grub_uint32_t offset2;

	struct grub_jfs_extent extent;
};
GRUB_PACKED_END

/* The tree of directory entries.  */
GRUB_PACKED_START
struct grub_jfs_tree_dir
{
	/* Pointers to the previous and next tree headers of other nodes on
	   this level.  */
	grub_uint64_t nextb;
	grub_uint64_t prevb;

	grub_uint8_t flags;

	/* The amount of dirents in this node.  */
	grub_uint8_t count;
	grub_uint8_t freecnt;
	grub_uint8_t freelist;
	grub_uint8_t maxslot;

	/* The location of the sorted array of pointers to dirents.  */
	grub_uint8_t sindex;
	grub_uint8_t unused[10];
};
GRUB_PACKED_END

/* An internal node in the dirents tree.  */
GRUB_PACKED_START
struct grub_jfs_internal_dirent
{
	struct grub_jfs_extent ex;
	grub_uint8_t next;
	grub_uint8_t len;
	grub_uint16_t namepart[11];
};
GRUB_PACKED_END

/* A leaf node in the dirents tree.  */
GRUB_PACKED_START
struct grub_jfs_leaf_dirent
{
	/* The inode for this dirent.  */
	grub_uint32_t inode;
	grub_uint8_t next;

	/* The size of the name.  */
	grub_uint8_t len;
	grub_uint16_t namepart[11];
	grub_uint32_t index;
};
GRUB_PACKED_END

/* A leaf in the dirents tree.  This one is used if the previously
   dirent was not big enough to store the name.  */
GRUB_PACKED_START
struct grub_jfs_leaf_next_dirent
{
	grub_uint8_t next;
	grub_uint8_t len;
	grub_uint16_t namepart[15];
};
GRUB_PACKED_END

GRUB_PACKED_START
struct grub_jfs_time
{
	grub_int32_t sec;
	grub_int32_t nanosec;
};
GRUB_PACKED_END

GRUB_PACKED_START
struct grub_jfs_inode
{
	grub_uint32_t stamp;
	grub_uint32_t fileset;
	grub_uint32_t inode;
	grub_uint8_t unused[12];
	grub_uint64_t size;
	grub_uint8_t unused2[20];
	grub_uint32_t mode;
	struct grub_jfs_time atime;
	struct grub_jfs_time ctime;
	struct grub_jfs_time mtime;
	grub_uint8_t unused3[48];
	grub_uint8_t unused4[96];

	union
	{
		/* The tree describing the extents of the file.  */
		struct
		{
			struct grub_jfs_treehead tree;
			struct grub_jfs_tree_extent extents[16];
		} file;
		union
		{
			/* The tree describing the dirents.  */
			struct
			{
				grub_uint8_t unused[16];
				grub_uint8_t flags;

				/* Amount of dirents in this node.  */
				grub_uint8_t count;
				grub_uint8_t freecnt;
				grub_uint8_t freelist;
				grub_uint32_t idotdot;
				grub_uint8_t sorted[8];
			} header;
			struct grub_jfs_leaf_dirent dirents[8];
		} dir;
		/* Fast symlink.  */
		struct
		{
			grub_uint8_t unused[32];
			grub_uint8_t path[256];
		} symlink;
	};
};
GRUB_PACKED_END

GRUB_PACKED_START
struct grub_jfs_data
{
	struct grub_jfs_sblock sblock;
	grub_disk_t disk;
	struct grub_jfs_inode fileset;
	struct grub_jfs_inode currinode;
	int caseins;
	int pos;
	int linknest;
	int namecomponentlen;
};
GRUB_PACKED_END

GRUB_PACKED_START
struct grub_jfs_diropen
{
	int index;
	union
	{
		struct grub_jfs_tree_dir header;
		struct grub_jfs_leaf_dirent dirent[0];
		struct grub_jfs_leaf_next_dirent next_dirent[0];
		grub_uint8_t sorted[0];
	}* dirpage;
	struct grub_jfs_data* data;
	struct grub_jfs_inode* inode;
	int count;
	grub_uint8_t* sorted;
	struct grub_jfs_leaf_dirent* leaf;
	struct grub_jfs_leaf_next_dirent* next_leaf;

	/* The filename and inode of the last read dirent.  */
	/* On-disk name is at most 255 UTF-16 codepoints.
	   Every UTF-16 codepoint is at most 4 UTF-8 bytes.
	 */
	char name[256 * GRUB_MAX_UTF8_PER_UTF16 + 1];
	grub_uint32_t ino;
};
GRUB_PACKED_END

static grub_err_t grub_jfs_lookup_symlink(struct grub_jfs_data* data, grub_uint32_t ino);

static grub_int64_t
getblk(struct grub_jfs_treehead* treehead,
	struct grub_jfs_tree_extent* extents,
	int max_extents,
	struct grub_jfs_data* data,
	grub_uint64_t blk)
{
	int found = -1;
	int i;

	for (i = 0; i < grub_le_to_cpu16(treehead->count) - 2 &&
		i < max_extents; i++)
	{
		if (treehead->flags & GRUB_JFS_TREE_LEAF)
		{
			/* Read the leafnode.  */
			if (grub_le_to_cpu32(extents[i].offset2) <= blk
				&& ((grub_le_to_cpu16(extents[i].extent.length))
					+ (extents[i].extent.length2 << 16)
					+ grub_le_to_cpu32(extents[i].offset2)) > blk)
				return (blk - grub_le_to_cpu32(extents[i].offset2)
					+ grub_le_to_cpu32(extents[i].extent.blk2));
		}
		else
			if (blk >= grub_le_to_cpu32(extents[i].offset2))
				found = i;
	}

	if (found != -1)
	{
		grub_int64_t ret = -1;
		struct
		{
			struct grub_jfs_treehead treehead;
			struct grub_jfs_tree_extent extents[254];
		} *tree;

		tree = grub_zalloc(sizeof(*tree));
		if (!tree)
			return -1;

		if (!grub_disk_read(data->disk,
			((grub_disk_addr_t)grub_le_to_cpu32(extents[found].extent.blk2))
			<< (grub_le_to_cpu16(data->sblock.log2_blksz)
				- GRUB_DISK_SECTOR_BITS), 0,
			sizeof(*tree), (char*)tree))
		{
			if (grub_memcmp(&tree->treehead, treehead, sizeof(struct grub_jfs_treehead)) ||
				grub_memcmp(&tree->extents, extents, 254 * sizeof(struct grub_jfs_tree_extent)))
				ret = getblk(&tree->treehead, &tree->extents[0], 254, data, blk);
			else
			{
				grub_error(GRUB_ERR_BAD_FS, "jfs: infinite recursion detected");
				ret = -1;
			}
		}
		grub_free(tree);
		return ret;
	}

	return -1;
}

/* Get the block number for the block BLK in the node INODE in the
   mounted filesystem DATA.  */
static grub_int64_t
grub_jfs_blkno(struct grub_jfs_data* data, struct grub_jfs_inode* inode,
	grub_uint64_t blk)
{
	return getblk(&inode->file.tree, &inode->file.extents[0], 16, data, blk);
}

static grub_err_t
grub_jfs_read_inode(struct grub_jfs_data* data, grub_uint32_t ino,
	struct grub_jfs_inode* inode)
{
	struct grub_jfs_extent iag_inodes[GRUB_JFS_IAG_INODES_COUNT];
	grub_uint32_t iagnum = ino / 4096;
	unsigned inoext = (ino % 4096) / 32;
	unsigned inonum = (ino % 4096) % 32;
	grub_uint64_t iagblk;
	grub_uint64_t inoblk;

	iagblk = grub_jfs_blkno(data, &data->fileset, iagnum + 1);
	if (grub_errno)
		return grub_errno;

	/* Read in the IAG.  */
	if (grub_disk_read(data->disk,
		iagblk << (grub_le_to_cpu16(data->sblock.log2_blksz)
			- GRUB_DISK_SECTOR_BITS),
		GRUB_JFS_IAG_INODES_OFFSET,
		sizeof(iag_inodes), &iag_inodes))
		return grub_errno;

	inoblk = grub_le_to_cpu32(iag_inodes[inoext].blk2);
	inoblk <<= (grub_le_to_cpu16(data->sblock.log2_blksz)
		- GRUB_DISK_SECTOR_BITS);
	inoblk += inonum;

	if (grub_disk_read(data->disk, inoblk, 0,
		sizeof(struct grub_jfs_inode), inode))
		return grub_errno;

	return 0;
}


static struct grub_jfs_data*
grub_jfs_mount(grub_disk_t disk)
{
	struct grub_jfs_data* data = 0;

	data = grub_malloc(sizeof(struct grub_jfs_data));
	if (!data)
		return 0;

	/* Read the superblock.  */
	if (grub_disk_read(disk, GRUB_JFS_SBLOCK, 0,
		sizeof(struct grub_jfs_sblock), &data->sblock))
		goto fail;

	if (grub_strncmp((char*)(data->sblock.magic), "JFS1", 4))
	{
		grub_error(GRUB_ERR_BAD_FS, "not a JFS filesystem");
		goto fail;
	}

	if (data->sblock.blksz == 0
		|| grub_le_to_cpu32(data->sblock.blksz)
		!= (1U << grub_le_to_cpu16(data->sblock.log2_blksz))
		|| grub_le_to_cpu16(data->sblock.log2_blksz) < GRUB_DISK_SECTOR_BITS)
	{
		grub_error(GRUB_ERR_BAD_FS, "not a JFS filesystem");
		goto fail;
	}

	data->disk = disk;
	data->pos = 0;
	data->linknest = 0;

	/* Read the inode of the first fileset.  */
	if (grub_disk_read(data->disk, GRUB_JFS_FS1_INODE_BLK, 0,
		sizeof(struct grub_jfs_inode), &data->fileset))
		goto fail;

	if (data->sblock.flags & grub_cpu_to_le32_compile_time(0x00200000))
		data->namecomponentlen = 11;
	else
		data->namecomponentlen = 13;

	if (data->sblock.flags & grub_cpu_to_le32_compile_time(0x40000000))
		data->caseins = 1;
	else
		data->caseins = 0;

	return data;

fail:
	grub_free(data);

	if (grub_errno == GRUB_ERR_OUT_OF_RANGE)
		grub_error(GRUB_ERR_BAD_FS, "not a JFS filesystem");

	return 0;
}

static struct grub_jfs_diropen*
grub_jfs_opendir(struct grub_jfs_data* data, struct grub_jfs_inode* inode)
{
	struct grub_jfs_internal_dirent* de;
	struct grub_jfs_diropen* diro;
	grub_disk_addr_t blk;

	de = (struct grub_jfs_internal_dirent*)inode->dir.dirents;

	if (!((grub_le_to_cpu32(inode->mode)
		& GRUB_JFS_FILETYPE_MASK) == GRUB_JFS_FILETYPE_DIR))
	{
		grub_error(GRUB_ERR_BAD_FILE_TYPE, N_("not a directory"));
		return 0;
	}

	diro = grub_zalloc(sizeof(struct grub_jfs_diropen));
	if (!diro)
		return 0;

	diro->data = data;
	diro->inode = inode;

	/* Check if the entire tree is contained within the inode.  */
	if (inode->file.tree.flags & GRUB_JFS_TREE_LEAF)
	{
		diro->leaf = inode->dir.dirents;
		diro->next_leaf = (struct grub_jfs_leaf_next_dirent*)de;
		diro->sorted = inode->dir.header.sorted;
		diro->count = inode->dir.header.count;

		return diro;
	}

	diro->dirpage = grub_malloc(grub_le_to_cpu32(data->sblock.blksz));
	if (!diro->dirpage)
	{
		grub_free(diro);
		return 0;
	}

	blk = grub_le_to_cpu32(de[inode->dir.header.sorted[0]].ex.blk2);
	blk <<= (grub_le_to_cpu16(data->sblock.log2_blksz) - GRUB_DISK_SECTOR_BITS);

	/* Read in the nodes until we are on the leaf node level.  */
	do
	{
		int index;
		if (grub_disk_read(data->disk, blk, 0,
			grub_le_to_cpu32(data->sblock.blksz),
			diro->dirpage->sorted))
		{
			grub_free(diro->dirpage);
			grub_free(diro);
			return 0;
		}

		de = (struct grub_jfs_internal_dirent*)diro->dirpage->dirent;
		index = diro->dirpage->sorted[diro->dirpage->header.sindex * 32];
		blk = (grub_le_to_cpu32(de[index].ex.blk2)
			<< (grub_le_to_cpu16(data->sblock.log2_blksz)
				- GRUB_DISK_SECTOR_BITS));
	} while (!(diro->dirpage->header.flags & GRUB_JFS_TREE_LEAF));

	diro->leaf = diro->dirpage->dirent;
	diro->next_leaf = diro->dirpage->next_dirent;
	diro->sorted = &diro->dirpage->sorted[diro->dirpage->header.sindex * 32];
	diro->count = diro->dirpage->header.count;

	return diro;
}


static void
grub_jfs_closedir(struct grub_jfs_diropen* diro)
{
	if (!diro)
		return;
	grub_free(diro->dirpage);
	grub_free(diro);
}

static void
le_to_cpu16_copy(grub_uint16_t* out, grub_uint16_t* in, grub_size_t len)
{
	while (len--)
		*out++ = grub_le_to_cpu16(*in++);
}

/* Read in the next dirent from the directory described by DIRO.  */
static grub_err_t
grub_jfs_getent(struct grub_jfs_diropen* diro)
{
	int strpos = 0;
	struct grub_jfs_leaf_dirent* leaf;
	struct grub_jfs_leaf_next_dirent* next_leaf;
	int len;
	int nextent;
	grub_uint16_t filename[256];

	/* The last node, read in more.  */
	if (diro->index == diro->count)
	{
		grub_disk_addr_t next;

		/* If the inode contains the entry tree or if this was the last
	   node, there is nothing to read.  */
		if ((diro->inode->file.tree.flags & GRUB_JFS_TREE_LEAF)
			|| !grub_le_to_cpu64(diro->dirpage->header.nextb))
			return GRUB_ERR_OUT_OF_RANGE;

		next = grub_le_to_cpu64(diro->dirpage->header.nextb);
		next <<= (grub_le_to_cpu16(diro->data->sblock.log2_blksz)
			- GRUB_DISK_SECTOR_BITS);

		if (grub_disk_read(diro->data->disk, next, 0,
			grub_le_to_cpu32(diro->data->sblock.blksz),
			diro->dirpage->sorted))
			return grub_errno;

		diro->leaf = diro->dirpage->dirent;
		diro->next_leaf = diro->dirpage->next_dirent;
		diro->sorted = &diro->dirpage->sorted[diro->dirpage->header.sindex * 32];
		diro->count = diro->dirpage->header.count;
		diro->index = 0;
	}

	leaf = &diro->leaf[diro->sorted[diro->index]];
	next_leaf = &diro->next_leaf[diro->index];

	len = leaf->len;
	if (!len)
	{
		diro->index++;
		return grub_jfs_getent(diro);
	}

	le_to_cpu16_copy(filename + strpos, leaf->namepart, len < diro->data->namecomponentlen ? len
		: diro->data->namecomponentlen);
	strpos += len < diro->data->namecomponentlen ? len
		: diro->data->namecomponentlen;
	diro->ino = grub_le_to_cpu32(leaf->inode);
	len -= diro->data->namecomponentlen;

	/* Move down to the leaf level.  */
	nextent = leaf->next;
	if (leaf->next != 255 && len > 0)
		do
		{
			next_leaf = &diro->next_leaf[nextent];
			le_to_cpu16_copy(filename + strpos, next_leaf->namepart, len < 15 ? len : 15);
			strpos += len < 15 ? len : 15;

			len -= 15;
			nextent = next_leaf->next;
		} while (next_leaf->next != 255 && len > 0);

		diro->index++;

		/* Convert the temporary UTF16 filename to UTF8.  */
		*grub_utf16_to_utf8((grub_uint8_t*)(diro->name), filename, strpos) = '\0';

		return 0;
}

/* Read LEN bytes from the file described by DATA starting with byte
   POS.  Return the amount of read bytes in READ.  */
static grub_ssize_t
grub_jfs_read_file(struct grub_jfs_data* data,
	grub_disk_read_hook_t read_hook, void* read_hook_data,
	grub_off_t pos, grub_size_t len, char* buf)
{
	grub_off_t i;
	grub_off_t blockcnt;

	blockcnt = (len + pos + grub_le_to_cpu32(data->sblock.blksz) - 1)
		>> grub_le_to_cpu16(data->sblock.log2_blksz);

	for (i = pos >> grub_le_to_cpu16(data->sblock.log2_blksz); i < blockcnt; i++)
	{
		grub_disk_addr_t blknr;
		grub_uint32_t blockoff = pos & (grub_le_to_cpu32(data->sblock.blksz) - 1);
		grub_uint32_t blockend = grub_le_to_cpu32(data->sblock.blksz);

		grub_uint64_t skipfirst = 0;

		blknr = grub_jfs_blkno(data, &data->currinode, i);
		if (grub_errno)
			return -1;

		/* Last block.  */
		if (i == blockcnt - 1)
		{
			blockend = (len + pos) & (grub_le_to_cpu32(data->sblock.blksz) - 1);

			if (!blockend)
				blockend = grub_le_to_cpu32(data->sblock.blksz);
		}

		/* First block.  */
		if (i == (pos >> grub_le_to_cpu16(data->sblock.log2_blksz)))
		{
			skipfirst = blockoff;
			blockend -= skipfirst;
		}

		data->disk->read_hook = read_hook;
		data->disk->read_hook_data = read_hook_data;
		grub_disk_read(data->disk,
			blknr << (grub_le_to_cpu16(data->sblock.log2_blksz)
				- GRUB_DISK_SECTOR_BITS),
			skipfirst, blockend, buf);

		data->disk->read_hook = 0;
		if (grub_errno)
			return -1;

		buf += grub_le_to_cpu32(data->sblock.blksz) - skipfirst;
	}

	return len;
}


/* Find the file with the pathname PATH on the filesystem described by
   DATA.  */
static grub_err_t
grub_jfs_find_file(struct grub_jfs_data* data, const char* path,
	grub_uint32_t start_ino)
{
	const char* name;
	const char* next = path;
	struct grub_jfs_diropen* diro = NULL;

	if (grub_jfs_read_inode(data, start_ino, &data->currinode))
		return grub_errno;

	while (1)
	{
		name = next;
		while (*name == '/')
			name++;
		if (name[0] == 0)
			return GRUB_ERR_NONE;
		for (next = name; *next && *next != '/'; next++);

		if (name[0] == '.' && name + 1 == next)
			continue;

		if (name[0] == '.' && name[1] == '.' && name + 2 == next)
		{
			grub_uint32_t ino = grub_le_to_cpu32(data->currinode.dir.header.idotdot);

			if (grub_jfs_read_inode(data, ino, &data->currinode))
				return grub_errno;

			continue;
		}

		diro = grub_jfs_opendir(data, &data->currinode);
		if (!diro)
			return grub_errno;

		for (;;)
		{
			if (grub_jfs_getent(diro) == GRUB_ERR_OUT_OF_RANGE)
			{
				grub_jfs_closedir(diro);
				return grub_error(GRUB_ERR_FILE_NOT_FOUND, N_("file `%s' not found"), path);
			}

			/* Check if the current direntry matches the current part of the
			   pathname.  */
			if ((data->caseins ? grub_strncasecmp(name, diro->name, next - name) == 0
				: grub_strncmp(name, diro->name, next - name) == 0) && !diro->name[next - name])
			{
				grub_uint32_t ino = diro->ino;
				grub_uint32_t dirino = grub_le_to_cpu32(data->currinode.inode);

				grub_jfs_closedir(diro);

				if (grub_jfs_read_inode(data, ino, &data->currinode))
					break;

				/* Check if this is a symlink.  */
				if ((grub_le_to_cpu32(data->currinode.mode)
					& GRUB_JFS_FILETYPE_MASK) == GRUB_JFS_FILETYPE_LNK)
				{
					grub_jfs_lookup_symlink(data, dirino);
					if (grub_errno)
						return grub_errno;
				}

				break;
			}
		}
	}
}

static grub_err_t
grub_jfs_lookup_symlink(struct grub_jfs_data* data, grub_uint32_t ino)
{
	grub_size_t size = grub_le_to_cpu64(data->currinode.size);
	char* symlink;

	if (++data->linknest > GRUB_JFS_MAX_SYMLNK_CNT)
		return grub_error(GRUB_ERR_SYMLINK_LOOP, N_("too deep nesting of symlinks"));

	symlink = grub_malloc(size + 1);
	if (!symlink)
		return grub_errno;
	if (size <= sizeof(data->currinode.symlink.path))
		grub_memcpy(symlink, (char*)(data->currinode.symlink.path), size);
	else if (grub_jfs_read_file(data, 0, 0, 0, size, symlink) < 0)
	{
		grub_free(symlink);
		return grub_errno;
	}

	symlink[size] = '\0';

	/* The symlink is an absolute path, go back to the root inode.  */
	if (symlink[0] == '/')
		ino = 2;

	grub_jfs_find_file(data, symlink, ino);

	grub_free(symlink);

	return grub_errno;
}


static grub_err_t
grub_jfs_dir(grub_disk_t disk, const char* path,
	grub_fs_dir_hook_t hook, void* hook_data)
{
	struct grub_jfs_data* data = 0;
	struct grub_jfs_diropen* diro = 0;

	data = grub_jfs_mount(disk);
	if (!data)
		goto fail;

	if (grub_jfs_find_file(data, path, GRUB_JFS_AGGR_INODE))
		goto fail;

	diro = grub_jfs_opendir(data, &data->currinode);
	if (!diro)
		goto fail;

	/* Iterate over the dirents in the directory that was found.  */
	while (grub_jfs_getent(diro) != GRUB_ERR_OUT_OF_RANGE)
	{
		struct grub_jfs_inode inode;
		struct grub_dirhook_info info;
		grub_memset(&info, 0, sizeof(info));

		if (grub_jfs_read_inode(data, diro->ino, &inode))
			goto fail;

		info.dir = (grub_le_to_cpu32(inode.mode)
			& GRUB_JFS_FILETYPE_MASK) == GRUB_JFS_FILETYPE_DIR;
		info.symlink = (grub_le_to_cpu32(inode.mode)
			& GRUB_JFS_FILETYPE_MASK) == GRUB_JFS_FILETYPE_LNK;
		info.mtimeset = 1;
		info.mtime = grub_le_to_cpu32(inode.mtime.sec);
		if (hook(diro->name, &info, hook_data))
			goto fail;
	}

	/* XXX: GRUB_ERR_OUT_OF_RANGE is used for the last dirent.  */
	if (grub_errno == GRUB_ERR_OUT_OF_RANGE)
		grub_errno = 0;

fail:
	grub_jfs_closedir(diro);
	grub_free(data);

	return grub_errno;
}


/* Open a file named NAME and initialize FILE.  */
static grub_err_t
grub_jfs_open(struct grub_file* file, const char* name)
{
	struct grub_jfs_data* data;

	data = grub_jfs_mount(file->disk);
	if (!data)
		goto fail;

	grub_jfs_find_file(data, name, GRUB_JFS_AGGR_INODE);
	if (grub_errno)
		goto fail;

	/* It is only possible for open regular files.  */
	if (!((grub_le_to_cpu32(data->currinode.mode)
		& GRUB_JFS_FILETYPE_MASK) == GRUB_JFS_FILETYPE_REG))
	{
		grub_error(GRUB_ERR_BAD_FILE_TYPE, N_("not a regular file"));
		goto fail;
	}

	file->data = data;
	file->size = grub_le_to_cpu64(data->currinode.size);

	return 0;

fail:
	grub_free(data);

	return grub_errno;
}


static grub_ssize_t
grub_jfs_read(grub_file_t file, char* buf, grub_size_t len)
{
	struct grub_jfs_data* data =
		(struct grub_jfs_data*)file->data;

	return grub_jfs_read_file(data, file->read_hook, file->read_hook_data,
		file->offset, len, buf);
}


static grub_err_t
grub_jfs_close(grub_file_t file)
{
	grub_free(file->data);

	return GRUB_ERR_NONE;
}

static grub_err_t
grub_jfs_uuid(grub_disk_t disk, char** uuid)
{
	struct grub_jfs_data* data;

	data = grub_jfs_mount(disk);
	if (data)
	{
		*uuid = grub_xasprintf("%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-"
			"%02x%02x%02x%02x%02x%02x",
			data->sblock.uuid[0], data->sblock.uuid[1],
			data->sblock.uuid[2], data->sblock.uuid[3],
			data->sblock.uuid[4], data->sblock.uuid[5],
			data->sblock.uuid[6], data->sblock.uuid[7],
			data->sblock.uuid[8], data->sblock.uuid[9],
			data->sblock.uuid[10], data->sblock.uuid[11],
			data->sblock.uuid[12], data->sblock.uuid[13],
			data->sblock.uuid[14], data->sblock.uuid[15]);
	}
	else
		*uuid = NULL;

	grub_free(data);

	return grub_errno;
}

static grub_err_t
grub_jfs_label(grub_disk_t disk, char** label)
{
	struct grub_jfs_data* data;
	data = grub_jfs_mount(disk);

	if (data)
	{
		if (data->sblock.volname2[0] < ' ')
		{
			char* ptr;
			ptr = data->sblock.volname + sizeof(data->sblock.volname) - 1;
			while (ptr >= data->sblock.volname && *ptr == ' ')
				ptr--;
			*label = grub_strndup(data->sblock.volname,
				ptr - data->sblock.volname + 1);
		}
		else
			*label = grub_strndup(data->sblock.volname2,
				sizeof(data->sblock.volname2));
	}
	else
		*label = 0;

	grub_free(data);

	return grub_errno;
}

static struct grub_fs grub_jfs_fs =
{
	.name = "jfs",
	.fs_dir = grub_jfs_dir,
	.fs_open = grub_jfs_open,
	.fs_read = grub_jfs_read,
	.fs_close = grub_jfs_close,
	.fs_label = grub_jfs_label,
	.fs_uuid = grub_jfs_uuid,
	.next = 0
};

GRUB_MOD_INIT(jfs)
{
	grub_fs_register(&grub_jfs_fs);
}

GRUB_MOD_FINI(jfs)
{
	grub_fs_unregister(&grub_jfs_fs);
}
