/*
 *  GRUB  --  GRand Unified Bootloader
 *  Copyright (C) 2012  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/>.
 */

 /* HFS+ is documented at http://developer.apple.com/technotes/tn/tn1150.html */

#include <grub/hfsplus.h>
#include <grub/misc.h>
#include <grub/mm.h>
#include <grub/deflate.h>
#include <grub/file.h>

GRUB_MOD_LICENSE("GPLv3+");

/* big-endian.  */
GRUB_PACKED_START
struct grub_hfsplus_compress_header1
{
	grub_uint32_t header_size;
	grub_uint32_t end_descriptor_offset;
	grub_uint32_t total_compressed_size_including_seek_blocks_and_header2;
	grub_uint32_t value_0x32;
	grub_uint8_t unused[0xf0];
};
GRUB_PACKED_END

/* big-endian.  */
GRUB_PACKED_START
struct grub_hfsplus_compress_header2
{
	grub_uint32_t total_compressed_size_including_seek_blocks;
};
GRUB_PACKED_END

/* little-endian.  */
GRUB_PACKED_START
struct grub_hfsplus_compress_header3
{
	grub_uint32_t num_chunks;
};
GRUB_PACKED_END

/* little-endian.  */
struct grub_hfsplus_compress_block_descriptor
{
	grub_uint32_t offset;
	grub_uint32_t size;
};

GRUB_PACKED_START
struct grub_hfsplus_compress_end_descriptor
{
	grub_uint8_t always_the_same[50];
};
GRUB_PACKED_END

GRUB_PACKED_START
struct grub_hfsplus_attr_header
{
	grub_uint8_t unused[3];
	grub_uint8_t type;
	grub_uint32_t unknown[1];
	grub_uint64_t size;
};
GRUB_PACKED_END

GRUB_PACKED_START
struct grub_hfsplus_compress_attr
{
	grub_uint32_t magic;
	grub_uint32_t type;
	grub_uint32_t uncompressed_inline_size;
	grub_uint32_t always_0;
};
GRUB_PACKED_END

enum
{
	HFSPLUS_COMPRESSION_INLINE = 3,
	HFSPLUS_COMPRESSION_RESOURCE = 4
};

static int
grub_hfsplus_cmp_attrkey(struct grub_hfsplus_key* keya,
	struct grub_hfsplus_key_internal* keyb)
{
	struct grub_hfsplus_attrkey* attrkey_a = &keya->attrkey;
	struct grub_hfsplus_attrkey_internal* attrkey_b = &keyb->attrkey;
	grub_uint32_t aparent = grub_be_to_cpu32(attrkey_a->cnid);
	grub_size_t len;
	int diff;

	if (aparent > attrkey_b->cnid)
		return 1;
	if (aparent < attrkey_b->cnid)
		return -1;

	len = grub_be_to_cpu16(attrkey_a->namelen);
	if (len > attrkey_b->namelen)
		len = attrkey_b->namelen;
	/* Since it's big-endian memcmp gives the same result as manually comparing
	   uint16_t but may be faster.  */
	diff = grub_memcmp(attrkey_a->name, attrkey_b->name,
		len * sizeof(attrkey_a->name[0]));
	if (diff == 0)
		diff = grub_be_to_cpu16(attrkey_a->namelen) - attrkey_b->namelen;
	return diff;
}

#define HFSPLUS_COMPRESS_BLOCK_SIZE 65536

static grub_ssize_t
hfsplus_read_compressed_real(struct grub_hfsplus_file* node,
	grub_off_t pos, grub_size_t len, char* buf)
{
	char* tmp_buf = 0;
	grub_size_t len0 = len;

	if (node->compressed == 1)
	{
		grub_memcpy(buf, node->cbuf + pos, len);
		if (grub_file_progress_hook && node->file)
			grub_file_progress_hook(0, 0, len, NULL, node->file);
		return len;
	}

	while (len)
	{
		grub_uint32_t block = pos / HFSPLUS_COMPRESS_BLOCK_SIZE;
		grub_size_t curlen = HFSPLUS_COMPRESS_BLOCK_SIZE
			- (pos % HFSPLUS_COMPRESS_BLOCK_SIZE);

		if (curlen > len)
			curlen = len;

		if (node->cbuf_block != block)
		{
			grub_uint32_t sz = grub_le_to_cpu32(node->compress_index[block].size);
			grub_size_t ts;
			if (!tmp_buf)
				tmp_buf = grub_malloc(HFSPLUS_COMPRESS_BLOCK_SIZE);
			if (!tmp_buf)
				return -1;
			if (grub_hfsplus_read_file(node, 0, 0,
				grub_le_to_cpu32(node->compress_index[block].start) + 0x104,
				sz, tmp_buf)
				!= (grub_ssize_t)sz)
			{
				grub_free(tmp_buf);
				return -1;
			}
			ts = HFSPLUS_COMPRESS_BLOCK_SIZE;
			if (ts > node->size - (pos & ~(HFSPLUS_COMPRESS_BLOCK_SIZE)))
				ts = node->size - (pos & ~(HFSPLUS_COMPRESS_BLOCK_SIZE));
			if (grub_zlib_decompress(tmp_buf, sz, 0,
				node->cbuf, ts) != (grub_ssize_t)ts)
			{
				if (!grub_errno)
					grub_error(GRUB_ERR_BAD_COMPRESSED_DATA,
						"premature end of compressed");

				grub_free(tmp_buf);
				return -1;
			}
			node->cbuf_block = block;
		}
		grub_memcpy(buf, node->cbuf + (pos % HFSPLUS_COMPRESS_BLOCK_SIZE),
			curlen);
		if (grub_file_progress_hook && node->file)
			grub_file_progress_hook(0, 0, curlen, NULL, node->file);
		buf += curlen;
		pos += curlen;
		len -= curlen;
	}
	grub_free(tmp_buf);
	return len0;
}

static grub_err_t
hfsplus_open_compressed_real(struct grub_hfsplus_file* node)
{
	grub_err_t err;
	struct grub_hfsplus_btnode* attr_node;
	grub_off_t attr_off;
	struct grub_hfsplus_key_internal key;
	struct grub_hfsplus_attr_header* attr_head;
	struct grub_hfsplus_compress_attr* cmp_head;
#define c grub_cpu_to_be16_compile_time
	const grub_uint16_t compress_attr_name[] =
	{
	  c('c'), c('o'), c('m'), c('.'), c('a'), c('p'), c('p'), c('l'), c('e'),
	  c('.'), c('d'), c('e'), c('c'), c('m'), c('p'), c('f'), c('s') };
#undef c
	if (node->size)
		return 0;

	key.attrkey.cnid = node->fileid;
	key.attrkey.namelen = sizeof(compress_attr_name) / sizeof(compress_attr_name[0]);
	key.attrkey.name = compress_attr_name;

	err = grub_hfsplus_btree_search(&node->data->attr_tree, &key,
		grub_hfsplus_cmp_attrkey,
		&attr_node, &attr_off);
	if (err || !attr_node)
	{
		grub_errno = 0;
		return 0;
	}

	attr_head = (struct grub_hfsplus_attr_header*)
		((char*)grub_hfsplus_btree_recptr(&node->data->attr_tree,
			attr_node, attr_off)
			+ sizeof(struct grub_hfsplus_attrkey) + sizeof(compress_attr_name));
	if (attr_head->type != 0x10
		|| !(attr_head->size & grub_cpu_to_be64_compile_time(~0xfULL)))
	{
		grub_free(attr_node);
		return 0;
	}
	cmp_head = (struct grub_hfsplus_compress_attr*)(attr_head + 1);
	if (cmp_head->magic != grub_cpu_to_be32_compile_time(0x66706d63))
	{
		grub_free(attr_node);
		return 0;
	}
	node->size = grub_le_to_cpu32(cmp_head->uncompressed_inline_size);

	if (cmp_head->type == grub_cpu_to_le32_compile_time(HFSPLUS_COMPRESSION_RESOURCE))
	{
		grub_uint32_t index_size;
		node->compressed = 2;

		if (grub_hfsplus_read_file(node, 0, 0,
			0x104, sizeof(index_size),
			(char*)&index_size)
			!= 4)
		{
			node->compressed = 0;
			grub_free(attr_node);
			grub_errno = 0;
			return 0;
		}
		node->compress_index_size = grub_le_to_cpu32(index_size);
		node->compress_index = grub_malloc(node->compress_index_size
			* sizeof(node->compress_index[0]));
		if (!node->compress_index)
		{
			node->compressed = 0;
			grub_free(attr_node);
			return grub_errno;
		}
		if (grub_hfsplus_read_file(node, 0, 0,
			0x104 + sizeof(index_size),
			node->compress_index_size
			* sizeof(node->compress_index[0]),
			(char*)node->compress_index)
			!= (grub_ssize_t)(node->compress_index_size
				* sizeof(node->compress_index[0])))
		{
			node->compressed = 0;
			grub_free(attr_node);
			grub_free(node->compress_index);
			grub_errno = 0;
			return 0;
		}

		node->cbuf_block = -1;

		node->cbuf = grub_malloc(HFSPLUS_COMPRESS_BLOCK_SIZE);
		grub_free(attr_node);
		if (!node->cbuf)
		{
			node->compressed = 0;
			grub_free(node->compress_index);
			return grub_errno;
		}
		return 0;
	}
	if (cmp_head->type != HFSPLUS_COMPRESSION_INLINE)
	{
		grub_free(attr_node);
		return 0;
	}

	node->cbuf = grub_malloc(node->size);
	if (!node->cbuf)
		return grub_errno;

	if (grub_zlib_decompress((char*)(cmp_head + 1),
		grub_cpu_to_be64(attr_head->size)
		- sizeof(*cmp_head), 0,
		node->cbuf, node->size)
		!= (grub_ssize_t)node->size)
	{
		if (!grub_errno)
			grub_error(GRUB_ERR_BAD_COMPRESSED_DATA,
				"premature end of compressed");
		return grub_errno;
	}
	node->compressed = 1;
	return 0;
}

GRUB_MOD_INIT(hfspluscomp)
{
	grub_hfsplus_open_compressed = hfsplus_open_compressed_real;
	grub_hfsplus_read_compressed = hfsplus_read_compressed_real;
}

GRUB_MOD_FINI(hfspluscomp)
{
	grub_hfsplus_open_compressed = 0;
	grub_hfsplus_read_compressed = 0;
}
