/* affs.c - Amiga Fast FileSystem.  */
/*
 *  GRUB  --  GRand Unified Bootloader
 *  Copyright (C) 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/types.h>
#include <grub/err.h>
#include <grub/file.h>
#include <grub/mm.h>
#include <grub/misc.h>
#include <grub/disk.h>
#include <grub/fshelp.h>
#include <grub/charset.h>

GRUB_MOD_LICENSE("GPLv3+");

/* The affs bootblock.  */
GRUB_PACKED_START
struct grub_affs_bblock
{
	grub_uint8_t type[3];
	grub_uint8_t flags;
	grub_uint32_t checksum;
	grub_uint32_t rootblock;
};
GRUB_PACKED_END

/* Set if the filesystem is a AFFS filesystem.  Otherwise this is an
   OFS filesystem.  */
#define GRUB_AFFS_FLAG_FFS	1

/* The affs rootblock.  */
GRUB_PACKED_START
struct grub_affs_rblock
{
	grub_uint32_t type;
	grub_uint8_t unused1[8];
	grub_uint32_t htsize;
	grub_uint32_t unused2;
	grub_uint32_t checksum;
	grub_uint32_t hashtable[1];
};
GRUB_PACKED_END

GRUB_PACKED_START
struct grub_affs_time
{
	grub_int32_t day;
	grub_uint32_t min;
	grub_uint32_t hz;
};
GRUB_PACKED_END

/* The second part of a file header block.  */
GRUB_PACKED_START
struct grub_affs_file
{
	grub_uint8_t unused1[12];
	grub_uint32_t size;
	grub_uint8_t unused2[92];
	struct grub_affs_time mtime;
	grub_uint8_t namelen;
	grub_uint8_t name[30];
	grub_uint8_t unused3[5];
	grub_uint32_t hardlink;
	grub_uint32_t unused4[6];
	grub_uint32_t next;
	grub_uint32_t parent;
	grub_uint32_t extension;
	grub_uint32_t type;
};
GRUB_PACKED_END

/* The location of `struct grub_affs_file' relative to the end of a
   file header block.  */
#define	GRUB_AFFS_FILE_LOCATION		200

   /* The offset in both the rootblock and the file header block for the
	  hashtable, symlink and block pointers (all synonyms).  */
#define GRUB_AFFS_HASHTABLE_OFFSET	24
#define GRUB_AFFS_BLOCKPTR_OFFSET	24
#define GRUB_AFFS_SYMLINK_OFFSET	24

enum
{
	GRUB_AFFS_FILETYPE_DIR = 2,
	GRUB_AFFS_FILETYPE_SYMLINK = 3,
	GRUB_AFFS_FILETYPE_HARDLINK = 0xfffffffc,
	GRUB_AFFS_FILETYPE_REG = 0xfffffffd
};

#define AFFS_MAX_LOG_BLOCK_SIZE 4
#define AFFS_MAX_SUPERBLOCK 1

struct grub_fshelp_node
{
	struct grub_affs_data* data;
	grub_uint32_t block;
	struct grub_fshelp_node* parent;
	struct grub_affs_file di;
	grub_uint32_t* block_cache;
	grub_uint32_t last_block_cache;
};

/* Information about a "mounted" affs filesystem.  */
struct grub_affs_data
{
	struct grub_affs_bblock bblock;
	struct grub_fshelp_node diropen;
	grub_disk_t disk;

	/* Log blocksize in sectors.  */
	int log_blocksize;

	/* The number of entries in the hashtable.  */
	unsigned int htsize;
};

static grub_disk_addr_t
grub_affs_read_block(grub_fshelp_node_t node, grub_disk_addr_t fileblock)
{
	grub_uint32_t target, curblock;
	grub_uint32_t pos;
	struct grub_affs_file file;
	struct grub_affs_data* data = node->data;
	grub_uint64_t mod;

	if (!node->block_cache)
	{
		node->block_cache = grub_malloc(((grub_be_to_cpu32(node->di.size)
			>> (9 + node->data->log_blocksize))
			/ data->htsize + 2)
			* sizeof(node->block_cache[0]));
		if (!node->block_cache)
			return -1;
		node->last_block_cache = 0;
		node->block_cache[0] = node->block;
	}

	/* Files are at most 2G on AFFS, so no need for 64-bit division.  */
	target = (grub_uint32_t)fileblock / data->htsize;
	mod = (grub_uint32_t)fileblock % data->htsize;
	/* Find the block that points to the fileblock we are looking up by
	   following the chain until the right table is reached.  */
	for (curblock = node->last_block_cache + 1; curblock < target + 1; curblock++)
	{
		grub_disk_read(data->disk,
			(((grub_uint64_t)node->block_cache[curblock - 1] + 1)
				<< data->log_blocksize) - 1,
			GRUB_DISK_SECTOR_SIZE - GRUB_AFFS_FILE_LOCATION,
			sizeof(file), &file);
		if (grub_errno)
			return 0;

		node->block_cache[curblock] = grub_be_to_cpu32(file.extension);
		node->last_block_cache = curblock;
	}

	/* Translate the fileblock to the block within the right table.  */
	grub_disk_read(data->disk, (grub_uint64_t)node->block_cache[target]
		<< data->log_blocksize,
		GRUB_AFFS_BLOCKPTR_OFFSET
		+ (data->htsize - mod - 1) * sizeof(pos),
		sizeof(pos), &pos);
	if (grub_errno)
		return 0;

	return grub_be_to_cpu32(pos);
}

static struct grub_affs_data*
grub_affs_mount(grub_disk_t disk)
{
	struct grub_affs_data* data;
	grub_uint32_t* rootblock = 0;
	struct grub_affs_rblock* rblock = 0;
	int log_blocksize = 0;
	int bsnum = 0;

	data = grub_zalloc(sizeof(struct grub_affs_data));
	if (!data)
		return 0;

	for (bsnum = 0; bsnum < AFFS_MAX_SUPERBLOCK + 1; bsnum++)
	{
		/* Read the bootblock.  */
		grub_disk_read(disk, bsnum, 0, sizeof(struct grub_affs_bblock),
			&data->bblock);
		if (grub_errno)
			goto fail;

		/* Make sure this is an affs filesystem.  */
		if (grub_strncmp((char*)(data->bblock.type), "DOS", 3) != 0
			/* Test if the filesystem is a OFS filesystem.  */
			|| !(data->bblock.flags & GRUB_AFFS_FLAG_FFS))
			continue;

		/* No sane person uses more than 8KB for a block.  At least I hope
	   for that person because in that case this won't work.  */
		if (!rootblock)
			rootblock = grub_malloc(GRUB_DISK_SECTOR_SIZE
				<< AFFS_MAX_LOG_BLOCK_SIZE);
		if (!rootblock)
			goto fail;

		rblock = (struct grub_affs_rblock*)rootblock;

		/* The filesystem blocksize is not stored anywhere in the filesystem
	   itself.  One way to determine it is try reading blocks for the
	   rootblock until the checksum is correct.  */
		for (log_blocksize = 0; log_blocksize <= AFFS_MAX_LOG_BLOCK_SIZE;
			log_blocksize++)
		{
			grub_uint32_t* currblock = rootblock;
			unsigned int i;
			grub_uint32_t checksum = 0;

			/* Read the rootblock.  */
			grub_disk_read(disk,
				(grub_uint64_t)grub_be_to_cpu32(data->bblock.rootblock)
				<< log_blocksize, 0,
				GRUB_DISK_SECTOR_SIZE << log_blocksize, rootblock);
			if (grub_errno == GRUB_ERR_OUT_OF_RANGE)
			{
				grub_errno = 0;
				break;
			}
			if (grub_errno)
				goto fail;

			if (rblock->type != grub_cpu_to_be32_compile_time(2)
				|| rblock->htsize == 0
				|| currblock[(GRUB_DISK_SECTOR_SIZE << log_blocksize)
				/ sizeof(*currblock) - 1]
				!= grub_cpu_to_be32_compile_time(1))
				continue;

			for (i = 0; i < (GRUB_DISK_SECTOR_SIZE << log_blocksize)
				/ sizeof(*currblock);
				i++)
				checksum += grub_be_to_cpu32(currblock[i]);

			if (checksum == 0)
			{
				data->log_blocksize = log_blocksize;
				data->disk = disk;
				data->htsize = grub_be_to_cpu32(rblock->htsize);
				data->diropen.data = data;
				data->diropen.block = grub_be_to_cpu32(data->bblock.rootblock);
				data->diropen.parent = NULL;
				grub_memcpy(&data->diropen.di, rootblock,
					sizeof(data->diropen.di));
				grub_free(rootblock);

				return data;
			}
		}
	}

fail:
	if (grub_errno == GRUB_ERR_NONE || grub_errno == GRUB_ERR_OUT_OF_RANGE)
		grub_error(GRUB_ERR_BAD_FS, "not an AFFS filesystem");

	grub_free(data);
	grub_free(rootblock);
	return 0;
}


static char*
grub_affs_read_symlink(grub_fshelp_node_t node)
{
	struct grub_affs_data* data = node->data;
	grub_uint8_t* latin1, * utf8;
	const grub_size_t symlink_size = ((GRUB_DISK_SECTOR_SIZE
		<< data->log_blocksize) - GRUB_AFFS_SYMLINK_OFFSET);

	latin1 = grub_malloc(symlink_size + 1);
	if (!latin1)
		return 0;

	grub_disk_read(data->disk,
		(grub_uint64_t)node->block << data->log_blocksize,
		GRUB_AFFS_SYMLINK_OFFSET,
		symlink_size, latin1);
	if (grub_errno)
	{
		grub_free(latin1);
		return 0;
	}
	latin1[symlink_size] = 0;
	utf8 = grub_calloc(GRUB_MAX_UTF8_PER_LATIN1 + 1, symlink_size);
	if (!utf8)
	{
		grub_free(latin1);
		return 0;
	}
	*grub_latin1_to_utf8(utf8, latin1, symlink_size) = '\0';
	grub_dprintf("affs", "Symlink: `%s'\n", utf8);
	grub_free(latin1);
	if (utf8[0] == ':')
		utf8[0] = '/';
	return (char*)utf8;
}


/* Helper for grub_affs_iterate_dir.  */
static int
grub_affs_create_node(grub_fshelp_node_t dir,
	grub_fshelp_iterate_dir_hook_t hook, void* hook_data,
	struct grub_fshelp_node** node,
	grub_uint32_t block, const struct grub_affs_file* fil)
{
	struct grub_affs_data* data = dir->data;
	int type = GRUB_FSHELP_REG;
	grub_uint8_t name_u8[sizeof(fil->name) * GRUB_MAX_UTF8_PER_LATIN1 + 1];
	grub_size_t len;
	unsigned int nest;

	*node = grub_zalloc(sizeof(**node));
	if (!*node)
		return 1;

	(*node)->data = data;
	(*node)->block = block;
	(*node)->parent = dir;

	len = fil->namelen;
	if (len > sizeof(fil->name))
		len = sizeof(fil->name);
	*grub_latin1_to_utf8(name_u8, fil->name, len) = '\0';

	(*node)->di = *fil;
	for (nest = 0; nest < 8; nest++)
	{
		switch ((*node)->di.type)
		{
		case grub_cpu_to_be32_compile_time(GRUB_AFFS_FILETYPE_REG):
			type = GRUB_FSHELP_REG;
			break;
		case grub_cpu_to_be32_compile_time(GRUB_AFFS_FILETYPE_DIR):
			type = GRUB_FSHELP_DIR;
			break;
		case grub_cpu_to_be32_compile_time(GRUB_AFFS_FILETYPE_SYMLINK):
			type = GRUB_FSHELP_SYMLINK;
			break;
		case grub_cpu_to_be32_compile_time(GRUB_AFFS_FILETYPE_HARDLINK):
		{
			grub_err_t err;
			(*node)->block = grub_be_to_cpu32((*node)->di.hardlink);
			err = grub_disk_read(data->disk,
				(((grub_uint64_t)(*node)->block + 1) << data->log_blocksize)
				- 1,
				GRUB_DISK_SECTOR_SIZE - GRUB_AFFS_FILE_LOCATION,
				sizeof((*node)->di), (char*)&(*node)->di);
			if (err)
			{
				grub_free(*node);
				return 1;
			}
			continue;
		}
		default:
		{
			grub_free(*node);
			return 0;
		}
		}
		break;
	}

	if (nest == 8)
	{
		grub_free(*node);
		return 0;
	}

	type |= GRUB_FSHELP_CASE_INSENSITIVE;

	if (hook((char*)name_u8, type, *node, hook_data))
	{
		*node = 0;
		return 1;
	}
	*node = 0;
	return 0;
}

static int
grub_affs_iterate_dir(grub_fshelp_node_t dir,
	grub_fshelp_iterate_dir_hook_t hook, void* hook_data)
{
	unsigned int i;
	struct grub_affs_file file;
	struct grub_fshelp_node* node, * orig_node;
	struct grub_affs_data* data = dir->data;
	grub_uint32_t* hashtable;

	/* Create the directory entries for `.' and `..'.  */
	node = orig_node = grub_zalloc(sizeof(*node));
	if (!node)
		return 1;

	*node = *dir;
	if (hook(".", GRUB_FSHELP_DIR, node, hook_data))
		return 1;
	if (dir->parent)
	{
		*node = *dir->parent;
		if (hook("..", GRUB_FSHELP_DIR, node, hook_data))
			return 1;
	}

	hashtable = grub_calloc(data->htsize, sizeof(*hashtable));
	if (!hashtable)
		return 1;

	grub_disk_read(data->disk,
		(grub_uint64_t)dir->block << data->log_blocksize,
		GRUB_AFFS_HASHTABLE_OFFSET,
		data->htsize * sizeof(*hashtable), (char*)hashtable);
	if (grub_errno)
		goto fail;

	for (i = 0; i < data->htsize; i++)
	{
		grub_uint32_t next;

		if (!hashtable[i])
			continue;

		/* Every entry in the hashtable can be chained.  Read the entire
	   chain.  */
		next = grub_be_to_cpu32(hashtable[i]);

		while (next)
		{
			grub_disk_read(data->disk,
				(((grub_uint64_t)next + 1) << data->log_blocksize)
				- 1,
				GRUB_DISK_SECTOR_SIZE - GRUB_AFFS_FILE_LOCATION,
				sizeof(file), (char*)&file);
			if (grub_errno)
				goto fail;

			if (grub_affs_create_node(dir, hook, hook_data, &node, next, &file))
			{
				/* Node has been replaced in function. */
				grub_free(orig_node);
				grub_free(hashtable);
				return 1;
			}

			next = grub_be_to_cpu32(file.next);
		}
	}

fail:
	grub_free(orig_node);
	grub_free(hashtable);
	return 0;
}


/* Open a file named NAME and initialize FILE.  */
static grub_err_t
grub_affs_open(struct grub_file* file, const char* name)
{
	struct grub_affs_data* data;
	struct grub_fshelp_node* fdiro = 0;

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

	grub_fshelp_find_file(name, &data->diropen, &fdiro, grub_affs_iterate_dir,
		grub_affs_read_symlink, GRUB_FSHELP_REG);
	if (grub_errno)
		goto fail;

	file->size = grub_be_to_cpu32(fdiro->di.size);
	data->diropen = *fdiro;
	grub_free(fdiro);

	file->data = data;
	file->offset = 0;

	return 0;

fail:
	if (data && fdiro != &data->diropen)
		grub_free(fdiro);
	grub_free(data);

	return grub_errno;
}

static grub_err_t
grub_affs_close(grub_file_t file)
{
	struct grub_affs_data* data =
		(struct grub_affs_data*)file->data;

	grub_free(data->diropen.block_cache);
	grub_free(file->data);

	return GRUB_ERR_NONE;
}

/* Read LEN bytes data from FILE into BUF.  */
static grub_ssize_t
grub_affs_read(grub_file_t file, char* buf, grub_size_t len)
{
	struct grub_affs_data* data =
		(struct grub_affs_data*)file->data;

	return grub_fshelp_read_file(data->diropen.data->disk, &data->diropen,
		file->read_hook, file->read_hook_data,
		file->offset, len, buf, grub_affs_read_block,
		grub_be_to_cpu32(data->diropen.di.size),
		data->log_blocksize, 0);
}

static grub_int32_t
aftime2ctime(const struct grub_affs_time* t)
{
	return grub_be_to_cpu32(t->day) * 86400
		+ grub_be_to_cpu32(t->min) * 60
		+ grub_be_to_cpu32(t->hz) / 50
		+ 8 * 365 * 86400 + 86400 * 2;
}

/* Context for grub_affs_dir.  */
struct grub_affs_dir_ctx
{
	grub_fs_dir_hook_t hook;
	void* hook_data;
};

/* Helper for grub_affs_dir.  */
static int
grub_affs_dir_iter(const char* filename, enum grub_fshelp_filetype filetype,
	grub_fshelp_node_t node, void* data)
{
	struct grub_affs_dir_ctx* ctx = data;
	struct grub_dirhook_info info;

	grub_memset(&info, 0, sizeof(info));
	info.dir = ((filetype & GRUB_FSHELP_TYPE_MASK) == GRUB_FSHELP_DIR);
	info.symlink = ((filetype & GRUB_FSHELP_TYPE_MASK) == GRUB_FSHELP_SYMLINK);
	info.mtimeset = 1;
	info.mtime = aftime2ctime(&node->di.mtime);
	grub_free(node);
	return ctx->hook(filename, &info, ctx->hook_data);
}

static grub_err_t
grub_affs_dir(grub_disk_t disk, const char* path,
	grub_fs_dir_hook_t hook, void* hook_data)
{
	struct grub_affs_dir_ctx ctx = { hook, hook_data };
	struct grub_affs_data* data = 0;
	struct grub_fshelp_node* fdiro = 0;

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

	grub_fshelp_find_file(path, &data->diropen, &fdiro, grub_affs_iterate_dir,
		grub_affs_read_symlink, GRUB_FSHELP_DIR);
	if (grub_errno)
		goto fail;

	grub_affs_iterate_dir(fdiro, grub_affs_dir_iter, &ctx);

fail:
	if (data && fdiro != &data->diropen)
		grub_free(fdiro);
	grub_free(data);

	return grub_errno;
}


static grub_err_t
grub_affs_label(grub_disk_t disk, char** label)
{
	struct grub_affs_data* data;
	struct grub_affs_file file;

	data = grub_affs_mount(disk);
	if (data)
	{
		grub_size_t len;
		/* The rootblock maps quite well on a file header block, it's
	   something we can use here.  */
		grub_disk_read(data->disk,
			(((grub_uint64_t)
				grub_be_to_cpu32(data->bblock.rootblock) + 1)
				<< data->log_blocksize) - 1,
			GRUB_DISK_SECTOR_SIZE - GRUB_AFFS_FILE_LOCATION,
			sizeof(file), &file);
		if (grub_errno)
			return grub_errno;

		len = file.namelen;
		if (len > sizeof(file.name))
			len = sizeof(file.name);
		*label = grub_calloc(GRUB_MAX_UTF8_PER_LATIN1 + 1, len);
		if (*label)
			*grub_latin1_to_utf8((grub_uint8_t*)*label, file.name, len) = '\0';
	}
	else
		*label = 0;

	grub_free(data);

	return grub_errno;
}

static grub_err_t
grub_affs_mtime(grub_disk_t disk, grub_int64_t* t)
{
	struct grub_affs_data* data;
	struct grub_affs_time af_time;

	*t = 0;

	data = grub_affs_mount(disk);
	if (!data)
	{
		return grub_errno;
	}

	grub_disk_read(data->disk,
		(((grub_uint64_t)
			grub_be_to_cpu32(data->bblock.rootblock) + 1)
			<< data->log_blocksize) - 1,
		GRUB_DISK_SECTOR_SIZE - 40,
		sizeof(af_time), &af_time);
	if (grub_errno)
	{
		grub_free(data);
		return grub_errno;
	}

	*t = aftime2ctime(&af_time);

	grub_free(data);

	return GRUB_ERR_NONE;
}


static struct grub_fs grub_affs_fs =
{
	.name = "affs",
	.fs_dir = grub_affs_dir,
	.fs_open = grub_affs_open,
	.fs_read = grub_affs_read,
	.fs_close = grub_affs_close,
	.fs_label = grub_affs_label,
	.fs_mtime = grub_affs_mtime,
	.next = 0
};

GRUB_MOD_INIT(affs)
{
	grub_fs_register(&grub_affs_fs);
}

GRUB_MOD_FINI(affs)
{
	grub_fs_unregister(&grub_affs_fs);
}
