/* bfs.c - The Bee File System.  */
/*
 *  GRUB  --  GRand Unified Bootloader
 *  Copyright (C) 2010,2011  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/>.
 */
/*
  Based on the book "Practical File System Design by Dominic Giampaolo
  with corrections and completitions based on Haiku code.
*/

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

GRUB_MOD_LICENSE ("GPLv3+");

#ifdef MODE_AFS
#define BTREE_ALIGN 4
#define SUPERBLOCK  2
#else
#define BTREE_ALIGN 8
#define SUPERBLOCK  1
#endif

#define grub_bfs_to_cpu16 grub_le_to_cpu16
#define grub_bfs_to_cpu32 grub_le_to_cpu32
#define grub_bfs_to_cpu64 grub_le_to_cpu64

#ifdef MODE_AFS
#define grub_bfs_to_cpu_treehead grub_bfs_to_cpu32
#else
#define grub_bfs_to_cpu_treehead grub_bfs_to_cpu16
#endif

#ifdef MODE_AFS
#define SUPER_BLOCK_MAGIC1 0x41465331
#else
#define SUPER_BLOCK_MAGIC1 0x42465331
#endif
#define SUPER_BLOCK_MAGIC2 0xdd121031
#define SUPER_BLOCK_MAGIC3 0x15b6830e
#define POINTER_INVALID 0xffffffffffffffffULL

#define ATTR_TYPE      0160000
#define ATTR_REG       0100000
#define ATTR_DIR       0040000
#define ATTR_LNK       0120000

#define DOUBLE_INDIRECT_SHIFT 2

#define LOG_EXTENT_SIZE 3
struct grub_bfs_extent
{
  grub_uint32_t ag;
  grub_uint16_t start;
  grub_uint16_t len;
} __attribute__ ((packed));

struct grub_bfs_superblock
{
  char label[32];
  grub_uint32_t magic1;
  grub_uint32_t unused1;
  grub_uint32_t bsize;
  grub_uint32_t log2_bsize;
  grub_uint8_t unused[20];
  grub_uint32_t magic2;
  grub_uint32_t unused2;
  grub_uint32_t log2_ag_size;
  grub_uint8_t unused3[32];
  grub_uint32_t magic3;
  struct grub_bfs_extent root_dir;
} __attribute__ ((packed));

struct grub_bfs_inode
{
  grub_uint8_t unused[20];
  grub_uint32_t mode;
  grub_uint32_t flags;
#ifdef MODE_AFS
  grub_uint8_t unused2[12];
#else
  grub_uint8_t unused2[8];
#endif
  grub_uint64_t mtime;
  grub_uint8_t unused3[8];
  struct grub_bfs_extent attr;
  grub_uint8_t unused4[12];

  union
  {
    struct
    {
      struct grub_bfs_extent direct[12];
      grub_uint64_t max_direct_range;
      struct grub_bfs_extent indirect;
      grub_uint64_t max_indirect_range;
      struct grub_bfs_extent double_indirect;
      grub_uint64_t max_double_indirect_range;
      grub_uint64_t size;
      grub_uint32_t pad[4];
    } __attribute__ ((packed));
    char inplace_link[144];
  } __attribute__ ((packed));
  grub_uint8_t small_data[0];
} __attribute__ ((packed));

enum
{
  LONG_SYMLINK = 0x40
};

struct grub_bfs_small_data_element_header
{
  grub_uint32_t type;
  grub_uint16_t name_len;
  grub_uint16_t value_len;
} __attribute__ ((packed));

struct grub_bfs_btree_header
{
  grub_uint32_t magic;
#ifdef MODE_AFS
  grub_uint64_t root;
  grub_uint32_t level;
  grub_uint32_t node_size;
  grub_uint32_t unused;
#else
  grub_uint32_t node_size;
  grub_uint32_t level;
  grub_uint32_t unused;
  grub_uint64_t root;
#endif
  grub_uint32_t unused2[2];
} __attribute__ ((packed));

struct grub_bfs_btree_node
{
  grub_uint64_t unused;
  grub_uint64_t right;
  grub_uint64_t overflow;
#ifdef MODE_AFS
  grub_uint32_t count_keys;
  grub_uint32_t total_key_len;
#else
  grub_uint16_t count_keys;
  grub_uint16_t total_key_len;
#endif
} __attribute__ ((packed));

struct grub_bfs_data
{
  struct grub_bfs_superblock sb;
  struct grub_bfs_inode ino[0];
};

static grub_err_t
read_extent (grub_disk_t disk,
	     const struct grub_bfs_superblock *sb,
	     const struct grub_bfs_extent *in,
	     grub_off_t off, grub_off_t byteoff, void *buf, grub_size_t len)
{
#ifdef MODE_AFS
  return grub_disk_read (disk, ((grub_bfs_to_cpu32 (in->ag)
				 << (grub_bfs_to_cpu32 (sb->log2_ag_size)
				     - GRUB_DISK_SECTOR_BITS))
				+ ((grub_bfs_to_cpu16 (in->start) + off)
				   << (grub_bfs_to_cpu32 (sb->log2_bsize)
				       - GRUB_DISK_SECTOR_BITS))),
			 byteoff, len, buf);
#else
  return grub_disk_read (disk, (((grub_bfs_to_cpu32 (in->ag)
				  << grub_bfs_to_cpu32 (sb->log2_ag_size))
				 + grub_bfs_to_cpu16 (in->start) + off)
				<< (grub_bfs_to_cpu32 (sb->log2_bsize)
				    - GRUB_DISK_SECTOR_BITS)),
			 byteoff, len, buf);
#endif
}

#ifdef MODE_AFS
#define RANGE_SHIFT grub_bfs_to_cpu32 (sb->log2_bsize)
#else
#define RANGE_SHIFT 0
#endif

static grub_err_t
read_bfs_file (grub_disk_t disk,
	       const struct grub_bfs_superblock *sb,
	       const struct grub_bfs_inode *ino,
	       grub_off_t off, void *buf, grub_size_t len,
	       void NESTED_FUNC_ATTR (*read_hook) (grub_disk_addr_t sector,
						   unsigned offset,
						   unsigned length))
{
  if (len == 0)
    return GRUB_ERR_NONE;

  if (off + len > grub_bfs_to_cpu64 (ino->size))
    return grub_error (GRUB_ERR_OUT_OF_RANGE,
		       "attempt to read past the end of file");

  if (off < (grub_bfs_to_cpu64 (ino->max_direct_range) << RANGE_SHIFT))
    {
      unsigned i;
      grub_uint64_t pos = 0;
      for (i = 0; i < ARRAY_SIZE (ino->direct); i++)
	{
	  grub_uint64_t newpos;
	  newpos = pos + (grub_bfs_to_cpu16 (ino->direct[i].len)
			  << grub_bfs_to_cpu32 (sb->log2_bsize));
	  if (newpos > off)
	    {
	      grub_size_t read_size;
	      grub_err_t err;
	      read_size = newpos - off;
	      if (read_size > len)
		read_size = len;
	      disk->read_hook = read_hook;
	      err = read_extent (disk, sb, &ino->direct[i], 0, off - pos,
				 buf, read_size);
	      disk->read_hook = 0;
	      if (err)
		return err;
	      off += read_size;
	      len -= read_size;
	      buf = (char *) buf + read_size;
	      if (len == 0)
		return GRUB_ERR_NONE;
	    }
	  pos = newpos;
	}
    }

  if (off < (grub_bfs_to_cpu64 (ino->max_direct_range) << RANGE_SHIFT))
    return grub_error (GRUB_ERR_BAD_FS, "incorrect direct blocks");

  if (off < (grub_bfs_to_cpu64 (ino->max_indirect_range) << RANGE_SHIFT))
    {
      unsigned i;
      struct grub_bfs_extent *entries;
      grub_size_t nentries;
      grub_err_t err;
      grub_uint64_t pos = (grub_bfs_to_cpu64 (ino->max_direct_range)
			   << RANGE_SHIFT);
      nentries = (grub_bfs_to_cpu16 (ino->indirect.len)
		  << (grub_bfs_to_cpu32 (sb->log2_bsize) - LOG_EXTENT_SIZE));
      entries = grub_malloc (nentries << LOG_EXTENT_SIZE);
      if (!entries)
	return grub_errno;
      err = read_extent (disk, sb, &ino->indirect, 0, 0,
			 entries, nentries << LOG_EXTENT_SIZE);
      for (i = 0; i < nentries; i++)
	{
	  grub_uint64_t newpos;
	  newpos = pos + (grub_bfs_to_cpu16 (entries[i].len)
			  << grub_bfs_to_cpu32 (sb->log2_bsize));
	  if (newpos > off)
	    {
	      grub_size_t read_size;
	      read_size = newpos - off;
	      if (read_size > len)
		read_size = len;
	      disk->read_hook = read_hook;
	      err = read_extent (disk, sb, &entries[i], 0, off - pos,
				 buf, read_size);
	      disk->read_hook = 0;
	      if (err)
		{
		  grub_free (entries);
		  return err;
		}
	      off += read_size;
	      len -= read_size;
	      buf = (char *) buf + read_size;
	      if (len == 0)
		{
		  grub_free (entries);
		  return GRUB_ERR_NONE;
		}
	    }
	  pos = newpos;
	}
      grub_free (entries);
    }

  if (off < (grub_bfs_to_cpu64 (ino->max_indirect_range) << RANGE_SHIFT))
    return grub_error (GRUB_ERR_BAD_FS, "incorrect indirect blocks");

  {
    struct grub_bfs_extent *l1_entries, *l2_entries;
    grub_size_t nl1_entries, nl2_entries;
    grub_off_t last_l1n = ~0ULL;
    grub_err_t err;
    nl1_entries = (grub_bfs_to_cpu16 (ino->double_indirect.len)
		   << (grub_bfs_to_cpu32 (sb->log2_bsize) - LOG_EXTENT_SIZE));
    l1_entries = grub_malloc (nl1_entries << LOG_EXTENT_SIZE);
    if (!l1_entries)
      return grub_errno;
    nl2_entries = 0;
    l2_entries = grub_malloc (1 << (DOUBLE_INDIRECT_SHIFT
				    + grub_bfs_to_cpu32 (sb->log2_bsize)));
    if (!l2_entries)
      {
	grub_free (l1_entries);
	return grub_errno;
      }
    err = read_extent (disk, sb, &ino->double_indirect, 0, 0,
		       l1_entries, nl1_entries << LOG_EXTENT_SIZE);
    if (err)
      {
	grub_free (l1_entries);
	grub_free (l2_entries);
	return err;
      }

    while (len > 0)
      {
	grub_off_t boff, l2n, l1n;
	grub_size_t read_size;
	grub_off_t double_indirect_offset;
	double_indirect_offset = off
	  - grub_bfs_to_cpu64 (ino->max_indirect_range);
	boff = (double_indirect_offset
		& ((1 << (grub_bfs_to_cpu32 (sb->log2_bsize)
			  + DOUBLE_INDIRECT_SHIFT)) - 1));
	l2n = ((double_indirect_offset >> (grub_bfs_to_cpu32 (sb->log2_bsize)
					   + DOUBLE_INDIRECT_SHIFT))
	       & ((1 << (grub_bfs_to_cpu32 (sb->log2_bsize) - LOG_EXTENT_SIZE
			 + DOUBLE_INDIRECT_SHIFT)) - 1));
	l1n =
	  (double_indirect_offset >>
	   (2 * grub_bfs_to_cpu32 (sb->log2_bsize) - LOG_EXTENT_SIZE +
	    2 * DOUBLE_INDIRECT_SHIFT));
	if (l1n > nl1_entries)
	  {
	    grub_free (l1_entries);
	    grub_free (l2_entries);
	    return grub_error (GRUB_ERR_BAD_FS,
			       "incorrect double-indirect block");
	  }
	if (l1n != last_l1n)
	  {
	    nl2_entries = (grub_bfs_to_cpu16 (l1_entries[l1n].len)
			   << (grub_bfs_to_cpu32 (sb->log2_bsize)
			       - LOG_EXTENT_SIZE));
	    if (nl2_entries > (1U << (grub_bfs_to_cpu32 (sb->log2_bsize)
				      - LOG_EXTENT_SIZE
				      + DOUBLE_INDIRECT_SHIFT)))
	      nl2_entries = (1 << (grub_bfs_to_cpu32 (sb->log2_bsize)
				   - LOG_EXTENT_SIZE
				   + DOUBLE_INDIRECT_SHIFT));
	    err = read_extent (disk, sb, &l1_entries[l1n], 0, 0,
			       l2_entries, nl2_entries << LOG_EXTENT_SIZE);
	    if (err)
	      {
		grub_free (l1_entries);
		grub_free (l2_entries);
		return err;
	      }
	    last_l1n = l1n;
	  }
	if (l2n > nl2_entries)
	  {
	    grub_free (l1_entries);
	    grub_free (l2_entries);
	    return grub_error (GRUB_ERR_BAD_FS,
			       "incorrect double-indirect block");
	  }

	read_size = (1 << (grub_bfs_to_cpu32 (sb->log2_bsize)
			   + DOUBLE_INDIRECT_SHIFT)) - boff;
	if (read_size > len)
	  read_size = len;
	disk->read_hook = read_hook;
	err = read_extent (disk, sb, &l2_entries[l2n], 0, boff,
			   buf, read_size);
	disk->read_hook = 0;
	if (err)
	  {
	    grub_free (l1_entries);
	    grub_free (l2_entries);
	    return err;
	  }
	off += read_size;
	len -= read_size;
	buf = (char *) buf + read_size;
      }
    return GRUB_ERR_NONE;
  }
}

static int
iterate_in_b_tree (grub_disk_t disk,
		   const struct grub_bfs_superblock *sb,
		   const struct grub_bfs_inode *ino,
		   int (*hook) (const char *name, grub_uint64_t value))
{
  struct grub_bfs_btree_header head;
  grub_err_t err;
  int level;
  grub_uint64_t node_off;

  err = read_bfs_file (disk, sb, ino, 0, &head, sizeof (head), 0);
  if (err)
    return 0;
  node_off = grub_bfs_to_cpu64 (head.root);

  level = grub_bfs_to_cpu32 (head.level) - 1;
  while (level--)
    {
      struct grub_bfs_btree_node node;
      grub_uint64_t key_value;
      err = read_bfs_file (disk, sb, ino, node_off, &node, sizeof (node), 0);
      if (err)
	return 0;
      err = read_bfs_file (disk, sb, ino, node_off
			   + ALIGN_UP (sizeof (node) +
				       grub_bfs_to_cpu_treehead (node.
								 total_key_len),
				       BTREE_ALIGN) +
			   grub_bfs_to_cpu_treehead (node.count_keys) *
			   sizeof (grub_uint16_t), &key_value,
			   sizeof (grub_uint64_t), 0);
      if (err)
	return 0;

      node_off = grub_bfs_to_cpu64 (key_value);
    }

  while (1)
    {
      struct grub_bfs_btree_node node;
      err = read_bfs_file (disk, sb, ino, node_off, &node, sizeof (node), 0);
      if (err)
	return 0;
      {
	char key_data[grub_bfs_to_cpu_treehead (node.total_key_len) + 1];
	grub_uint16_t keylen_idx[grub_bfs_to_cpu_treehead (node.count_keys)];
	grub_uint64_t key_values[grub_bfs_to_cpu_treehead (node.count_keys)];
	unsigned i;
	grub_uint16_t start = 0, end = 0;

	err =
	  read_bfs_file (disk, sb, ino, node_off + sizeof (node), key_data,
			 grub_bfs_to_cpu_treehead (node.total_key_len), 0);
	if (err)
	  return 0;
	key_data[grub_bfs_to_cpu_treehead (node.total_key_len)] = 0;
	err = read_bfs_file (disk, sb, ino, node_off
			     + ALIGN_UP (sizeof (node) +
					 grub_bfs_to_cpu_treehead
					 (node.total_key_len), BTREE_ALIGN),
			     keylen_idx,
			     grub_bfs_to_cpu_treehead (node.count_keys) *
			     sizeof (grub_uint16_t), 0);
	if (err)
	  return 0;
	err = read_bfs_file (disk, sb, ino, node_off
			     + ALIGN_UP (sizeof (node) +
					 grub_bfs_to_cpu_treehead
					 (node.total_key_len),
					 BTREE_ALIGN) +
			     grub_bfs_to_cpu_treehead (node.count_keys) *
			     sizeof (grub_uint16_t), key_values,
			     grub_bfs_to_cpu_treehead (node.count_keys) *
			     sizeof (grub_uint64_t), 0);
	if (err)
	  return 0;

	for (i = 0; i < grub_bfs_to_cpu_treehead (node.count_keys); i++)
	  {
	    char c;
	    start = end;
	    end = grub_bfs_to_cpu16 (keylen_idx[i]);
	    if (grub_bfs_to_cpu_treehead (node.total_key_len) <= end)
	      end = grub_bfs_to_cpu_treehead (node.total_key_len);
	    c = key_data[end];
	    key_data[end] = 0;
	    if (hook (key_data + start, grub_bfs_to_cpu64 (key_values[i])))
	      return 1;
	    key_data[end] = c;
	  }
	node_off = grub_bfs_to_cpu64 (node.right);
	if (node_off == POINTER_INVALID)
	  return 0;
      }
    }
}

static grub_err_t
find_in_b_tree (grub_disk_t disk,
		const struct grub_bfs_superblock *sb,
		const struct grub_bfs_inode *ino, const char *name,
		grub_uint64_t * res)
{
  struct grub_bfs_btree_header head;
  grub_err_t err;
  int level;
  grub_uint64_t node_off;

  err = read_bfs_file (disk, sb, ino, 0, &head, sizeof (head), 0);
  if (err)
    return err;
  node_off = grub_bfs_to_cpu64 (head.root);

  level = grub_bfs_to_cpu32 (head.level) - 1;
  while (1)
    {
      struct grub_bfs_btree_node node;
      err = read_bfs_file (disk, sb, ino, node_off, &node, sizeof (node), 0);
      if (err)
	return err;
      if (node.count_keys == 0)
	return grub_error (GRUB_ERR_FILE_NOT_FOUND, "file `%s' not found",
			   name);
      {
	char key_data[grub_bfs_to_cpu_treehead (node.total_key_len) + 1];
	grub_uint16_t keylen_idx[grub_bfs_to_cpu_treehead (node.count_keys)];
	grub_uint64_t key_values[grub_bfs_to_cpu_treehead (node.count_keys)];
	unsigned i;
	grub_uint16_t start = 0, end = 0;
	err =
	  read_bfs_file (disk, sb, ino, node_off + sizeof (node), key_data,
			 grub_bfs_to_cpu_treehead (node.total_key_len), 0);
	if (err)
	  return err;
	key_data[grub_bfs_to_cpu_treehead (node.total_key_len)] = 0;
	err = read_bfs_file (disk, sb, ino, node_off
			     +
			     ALIGN_UP (sizeof (node) +
				       grub_bfs_to_cpu_treehead (node.
								 total_key_len),
				       BTREE_ALIGN), keylen_idx,
			     grub_bfs_to_cpu_treehead (node.count_keys) *
			     sizeof (grub_uint16_t), 0);
	if (err)
	  return err;
	err = read_bfs_file (disk, sb, ino, node_off
			     + ALIGN_UP (sizeof (node) +
					 grub_bfs_to_cpu_treehead
					 (node.total_key_len),
					 BTREE_ALIGN) +
			     grub_bfs_to_cpu_treehead (node.count_keys) *
			     sizeof (grub_uint16_t), key_values,
			     grub_bfs_to_cpu_treehead (node.count_keys) *
			     sizeof (grub_uint64_t), 0);
	if (err)
	  return err;

	for (i = 0; i < grub_bfs_to_cpu_treehead (node.count_keys); i++)
	  {
	    int cmp;
	    char c;
	    start = end;
	    end = grub_bfs_to_cpu16 (keylen_idx[i]);
	    if (grub_bfs_to_cpu_treehead (node.total_key_len) <= end)
	      end = grub_bfs_to_cpu_treehead (node.total_key_len);
	    c = key_data[end];
	    key_data[end] = 0;
	    cmp = grub_strcmp (key_data + start, name);
	    key_data[end] = c;
	    if (cmp == 0 && level == 0)
	      {
		*res = grub_bfs_to_cpu64 (key_values[i]);
		return GRUB_ERR_NONE;
	      }
	    if (cmp >= 0 && level != 0)
	      {
		node_off = grub_bfs_to_cpu64 (key_values[i]);
		break;
	      }
	  }
	if (i < grub_bfs_to_cpu_treehead (node.count_keys))
	  {
	    level--;
	    continue;
	  }
	if (node.overflow != POINTER_INVALID)
	  {
	    node_off = grub_bfs_to_cpu64 (node.overflow);
	    /* This level-- isn't specified but is needed.  */
	    level--;
	    continue;
	  }
	return grub_error (GRUB_ERR_FILE_NOT_FOUND, "file `%s' not found",
			   name);
      }
    }
}

static grub_err_t
hop_level (grub_disk_t disk,
	   const struct grub_bfs_superblock *sb,
	   struct grub_bfs_inode *ino, const char *name)
{
  grub_err_t err;
  grub_uint64_t res;

  if (((grub_bfs_to_cpu32 (ino->mode) & ATTR_TYPE) != ATTR_DIR))
    return grub_error (GRUB_ERR_BAD_FILE_TYPE, "not a directory");

  err = find_in_b_tree (disk, sb, ino, name, &res);
  if (err)
    return err;

  return grub_disk_read (disk, res
			 << (grub_bfs_to_cpu32 (sb->log2_bsize)
			     - GRUB_DISK_SECTOR_BITS), 0,
			 grub_bfs_to_cpu32 (sb->bsize), (char *) ino);
}

static grub_err_t
find_file (const char *path, grub_disk_t disk,
	   const struct grub_bfs_superblock *sb, struct grub_bfs_inode *ino)
{
  char *ptr, *ptr2;
  char *alloc = NULL;
  grub_err_t err;
  union
  {
    struct grub_bfs_inode ino;
    grub_uint8_t raw[grub_bfs_to_cpu32 (sb->bsize)];
  } old_ino;
  unsigned symlinks_max = 32;

  err = read_extent (disk, sb, &sb->root_dir, 0, 0, ino,
		     grub_bfs_to_cpu32 (sb->bsize));
  if (err)
    return err;

  ptr = (char *) path;
  while (1)
    {
      while (*ptr == '/')
	ptr++;
      if (*ptr == 0)
	{
	  grub_free (alloc);
	  return GRUB_ERR_NONE;
	}
      ptr2 = grub_strchr (ptr, '/');
      grub_memcpy (&old_ino, ino, grub_bfs_to_cpu32 (sb->bsize));
      if (ptr2)
	{
	  char component[ptr2 - ptr + 1];
	  grub_memcpy (component, ptr, ptr2 - ptr);
	  component[ptr2 - ptr] = 0;
	  err = hop_level (disk, sb, ino, component);
	}
      else
	err = hop_level (disk, sb, ino, ptr);
      if (err)
	return err;

      if (((grub_bfs_to_cpu32 (ino->mode) & ATTR_TYPE) == ATTR_LNK))
	{
	  char *old_alloc = alloc;
	  if (--symlinks_max == 0)
	    {
	      grub_free (alloc);
	      return grub_error (GRUB_ERR_SYMLINK_LOOP,
				 "too deep nesting of symlinks");
	    }

#ifndef MODE_AFS
	  if (grub_bfs_to_cpu32 (ino->flags) & LONG_SYMLINK)
#endif
	    {
	      grub_size_t symsize = grub_bfs_to_cpu64 (ino->size);
	      alloc = grub_malloc ((ptr2 ? grub_strlen (ptr2) : 0)
				   + symsize + 1);
	      if (!alloc)
		{
		  grub_free (alloc);
		  return grub_errno;
		}
	      grub_free (old_alloc);
	      err = read_bfs_file (disk, sb, ino, 0, alloc, symsize, 0);
	      if (err)
		{
		  grub_free (alloc);
		  return err;
		}
	      alloc[symsize] = 0;
	    }
#ifndef MODE_AFS
	  else
	    {
	      alloc = grub_malloc ((ptr2 ? grub_strlen (ptr2) : 0)
				   + sizeof (ino->inplace_link) + 1);
	      if (!alloc)
		{
		  grub_free (alloc);
		  return grub_errno;
		}
	      grub_free (old_alloc);
	      grub_memcpy (alloc, ino->inplace_link,
			   sizeof (ino->inplace_link));
	      alloc[sizeof (ino->inplace_link)] = 0;
	    }
#endif
	  if (alloc[0] == '/')
	    {
	      err = read_extent (disk, sb, &sb->root_dir, 0, 0, ino,
				 grub_bfs_to_cpu32 (sb->bsize));
	      if (err)
		{
		  grub_free (alloc);
		  return err;
		}
	    }
	  else
	    grub_memcpy (ino, &old_ino, grub_bfs_to_cpu32 (sb->bsize));
	  ptr = alloc + grub_strlen (alloc);
	  if (ptr2)
	    ptr = grub_stpcpy (ptr, ptr2);
	  *ptr = 0;
	  ptr = ptr2 = alloc;
	  continue;
	}

      if (!ptr2)
	{
	  grub_free (alloc);
	  return GRUB_ERR_NONE;
	}
      ptr = ptr2 + 1;
    }
}

static grub_err_t
mount (grub_disk_t disk, struct grub_bfs_superblock *sb)
{
  grub_err_t err;
  err = grub_disk_read (disk, SUPERBLOCK, 0, sizeof (*sb), sb);
  if (err == GRUB_ERR_OUT_OF_RANGE)
    return grub_error (GRUB_ERR_BAD_FS, 
#ifdef MODE_AFS
		       "not an AFS filesystem"
#else
		       "not a BFS filesystem"
#endif
		       );
  if (err)
    return err;
  if (grub_bfs_to_cpu32 (sb->magic1) != SUPER_BLOCK_MAGIC1
      || grub_bfs_to_cpu32 (sb->magic2) != SUPER_BLOCK_MAGIC2
      || grub_bfs_to_cpu32 (sb->magic3) != SUPER_BLOCK_MAGIC3
      || (grub_bfs_to_cpu32 (sb->bsize)
	  != (1U << grub_bfs_to_cpu32 (sb->log2_bsize))))
    return grub_error (GRUB_ERR_BAD_FS, 
#ifdef MODE_AFS
		       "not an AFS filesystem"
#else
		       "not a BFS filesystem"
#endif
		       );
  return GRUB_ERR_NONE;
}

static grub_err_t
grub_bfs_dir (grub_device_t device, const char *path,
	      int (*hook_in) (const char *filename,
			      const struct grub_dirhook_info * info))
{
  struct grub_bfs_superblock sb;
  grub_err_t err;
  auto int hook (const char *name, grub_uint64_t value);

  int hook (const char *name, grub_uint64_t value)
  {
    grub_err_t err2;
    union
    {
      struct grub_bfs_inode ino;
      grub_uint8_t raw[grub_bfs_to_cpu32 (sb.bsize)];
    } ino;
    struct grub_dirhook_info info;

    err2 = grub_disk_read (device->disk, value
			   << (grub_bfs_to_cpu32 (sb.log2_bsize)
			       - GRUB_DISK_SECTOR_BITS), 0,
			   grub_bfs_to_cpu32 (sb.bsize), (char *) ino.raw);
    if (err2)
      {
	grub_print_error ();
	return 0;
      }

    info.mtimeset = 1;
#ifdef MODE_AFS
    info.mtime =
      grub_divmod64 (grub_bfs_to_cpu64 (ino.ino.mtime), 1000000, 0);
#else
    info.mtime = grub_bfs_to_cpu64 (ino.ino.mtime) >> 16;
#endif
    info.dir = ((grub_bfs_to_cpu32 (ino.ino.mode) & ATTR_TYPE) == ATTR_DIR);
    return hook_in (name, &info);
  }
  err = mount (device->disk, &sb);
  if (err)
    return err;

  {
    union
    {
      struct grub_bfs_inode ino;
      grub_uint8_t raw[grub_bfs_to_cpu32 (sb.bsize)];
    } ino;
    err = find_file (path, device->disk, &sb, &ino.ino);
    if (err)
      return err;
    if (((grub_bfs_to_cpu32 (ino.ino.mode) & ATTR_TYPE) != ATTR_DIR))
      return grub_error (GRUB_ERR_BAD_FILE_TYPE, "not a directory");
    iterate_in_b_tree (device->disk, &sb, &ino.ino, hook);
  }

  return grub_errno;
}

static grub_err_t
grub_bfs_open (struct grub_file *file, const char *name)
{
  struct grub_bfs_superblock sb;
  grub_err_t err;

  err = mount (file->device->disk, &sb);
  if (err)
    return err;

  {
    union
    {
      struct grub_bfs_inode ino;
      grub_uint8_t raw[grub_bfs_to_cpu32 (sb.bsize)];
    } ino;
    struct grub_bfs_data *data;
    err = find_file (name, file->device->disk, &sb, &ino.ino);
    if (err)
      return err;
    if (((grub_bfs_to_cpu32 (ino.ino.mode) & ATTR_TYPE) != ATTR_REG))
      return grub_error (GRUB_ERR_BAD_FILE_TYPE, "not a regular file");

    data = grub_zalloc (sizeof (struct grub_bfs_data)
			+ grub_bfs_to_cpu32 (sb.bsize));
    if (!data)
      return grub_errno;
    data->sb = sb;
    grub_memcpy (&data->ino, &ino, grub_bfs_to_cpu32 (sb.bsize));
    file->data = data;
    file->size = grub_bfs_to_cpu64 (ino.ino.size);
  }

  return GRUB_ERR_NONE;
}

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

  return GRUB_ERR_NONE;
}

static grub_ssize_t
grub_bfs_read (grub_file_t file, char *buf, grub_size_t len)
{
  grub_err_t err;
  struct grub_bfs_data *data = file->data;

  err = read_bfs_file (file->device->disk, &data->sb,
		       data->ino, file->offset, buf, len, file->read_hook);
  if (err)
    return -1;
  return len;
}

static grub_err_t
grub_bfs_label (grub_device_t device, char **label)
{
  struct grub_bfs_superblock sb;
  grub_err_t err;

  *label = 0;

  err = mount (device->disk, &sb);
  if (err)
    return err;

  *label = grub_strndup (sb.label, sizeof (sb.label));
  return GRUB_ERR_NONE;
}

#ifndef MODE_AFS
static grub_ssize_t
read_bfs_attr (grub_disk_t disk,
	       const struct grub_bfs_superblock *sb,
	       const struct grub_bfs_inode *ino,
	       const char *name, void *buf, grub_size_t len)
{
  grub_uint8_t *ptr = (grub_uint8_t *) ino->small_data;
  grub_uint8_t *end = ((grub_uint8_t *) ino + grub_bfs_to_cpu32 (sb->bsize));

  while (ptr + sizeof (struct grub_bfs_small_data_element_header) < end)
    {
      struct grub_bfs_small_data_element_header *el;
      char *el_name;
      grub_uint8_t *data;
      el = (struct grub_bfs_small_data_element_header *) ptr;
      if (el->name_len == 0)
	break;
      el_name = (char *) (el + 1);
      data = (grub_uint8_t *) el_name + grub_bfs_to_cpu16 (el->name_len) + 3;
      ptr = data + grub_bfs_to_cpu16 (el->value_len) + 1;
      if (grub_memcmp (name, el_name, grub_bfs_to_cpu16 (el->name_len)) == 0
	  && name[el->name_len] == 0)
	{
	  grub_size_t copy;
	  copy = len;
	  if (grub_bfs_to_cpu16 (el->value_len) > copy)
	    copy = grub_bfs_to_cpu16 (el->value_len);
	  grub_memcpy (buf, data, copy);
	  return copy;
	}
    }

  if (ino->attr.len != 0)
    {
      union
      {
	struct grub_bfs_inode ino;
	grub_uint8_t raw[grub_bfs_to_cpu32 (sb->bsize)];
      } ino2;
      grub_size_t read;
      grub_err_t err;
      grub_uint64_t res;

      err = read_extent (disk, sb, &ino->attr, 0, 0, ino2.raw,
			 grub_bfs_to_cpu32 (sb->bsize));
      if (err)
	return -1;

      err = find_in_b_tree (disk, sb, &ino2.ino, name, &res);
      if (err)
	return -1;
      grub_disk_read (disk, res
		      << (grub_bfs_to_cpu32 (sb->log2_bsize)
			  - GRUB_DISK_SECTOR_BITS), 0,
		      grub_bfs_to_cpu32 (sb->bsize), (char *) &ino2);
      read = grub_bfs_to_cpu64 (ino2.ino.size);
      if (read > len)
	read = len;

      err = read_bfs_file (disk, sb, &ino2.ino, 0, buf, read, 0);
      if (err)
	return -1;
      return read;
    }
  return -1;
}

static grub_err_t
grub_bfs_uuid (grub_device_t device, char **uuid)
{
  struct grub_bfs_superblock sb;
  grub_err_t err;

  *uuid = 0;

  err = mount (device->disk, &sb);
  if (err)
    return err;

  {
    union
    {
      struct grub_bfs_inode ino;
      grub_uint8_t raw[grub_bfs_to_cpu32 (sb.bsize)];
    } ino;
    grub_uint64_t vid;

    err = read_extent (device->disk, &sb, &sb.root_dir, 0, 0,
		       &ino, grub_bfs_to_cpu32 (sb.bsize));
    if (err)
      return err;
    if (read_bfs_attr (device->disk, &sb, &ino.ino, "be:volume_id",
		       &vid, sizeof (vid)) == sizeof (vid))
      *uuid =
	grub_xasprintf ("%016" PRIxGRUB_UINT64_T, grub_bfs_to_cpu64 (vid));
  }
  return GRUB_ERR_NONE;
}
#endif

static struct grub_fs grub_bfs_fs = {
#ifdef MODE_AFS
  .name = "afs",
#else
  .name = "bfs",
#endif
  .dir = grub_bfs_dir,
  .open = grub_bfs_open,
  .read = grub_bfs_read,
  .close = grub_bfs_close,
  .label = grub_bfs_label,
#ifndef MODE_AFS
  .uuid = grub_bfs_uuid,
#endif
#ifdef GRUB_UTIL
  .reserved_first_sector = 1,
#endif
};

#ifdef MODE_AFS
GRUB_MOD_INIT (afs)
#else
GRUB_MOD_INIT (bfs)
#endif
{
  COMPILE_TIME_ASSERT (1 << LOG_EXTENT_SIZE ==
		       sizeof (struct grub_bfs_extent));
  grub_fs_register (&grub_bfs_fs);
}

#ifdef MODE_AFS
GRUB_MOD_FINI (afs)
#else
GRUB_MOD_FINI (bfs)
#endif
{
  grub_fs_unregister (&grub_bfs_fs);
}