/*
 * Copyright (C) 2004-2008 Kay Sievers <kay.sievers@vrfy.org>
 * Copyright (C) 2008 Karel Zak <kzak@redhat.com>
 *
 * This file may be redistributed under the terms of the
 * GNU Lesser General Public License.
 */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <inttypes.h>

#include "superblocks.h"
#include "md5.h"

/* HFS / HFS+ */
struct hfs_finder_info {
        uint32_t        boot_folder;
        uint32_t        start_app;
        uint32_t        open_folder;
        uint32_t        os9_folder;
        uint32_t        reserved;
        uint32_t        osx_folder;
        uint8_t         id[8];
} __attribute__((packed));

#define HFS_SECTOR_SIZE         512

struct hfs_mdb {
        uint8_t         signature[2];
        uint32_t        cr_date;
        uint32_t        ls_Mod;
        uint16_t        atrb;
        uint16_t        nm_fls;
        uint16_t        vbm_st;
        uint16_t        alloc_ptr;
        uint16_t        nm_al_blks;
        uint32_t        al_blk_size;
        uint32_t        clp_size;
        uint16_t        al_bl_st;
        uint32_t        nxt_cnid;
        uint16_t        free_bks;
        uint8_t         label_len;
        uint8_t         label[27];
        uint32_t        vol_bkup;
        uint16_t        vol_seq_num;
        uint32_t        wr_cnt;
        uint32_t        xt_clump_size;
        uint32_t        ct_clump_size;
        uint16_t        num_root_dirs;
        uint32_t        file_count;
        uint32_t        dir_count;
        struct hfs_finder_info finder_info;
        uint8_t         embed_sig[2];
        uint16_t        embed_startblock;
        uint16_t        embed_blockcount;
} __attribute__((packed));


#define HFS_NODE_LEAF			0xff
#define HFSPLUS_POR_CNID		1

struct hfsplus_bnode_descriptor {
	uint32_t		next;
	uint32_t		prev;
	uint8_t		type;
	uint8_t		height;
	uint16_t		num_recs;
	uint16_t		reserved;
} __attribute__((packed));

struct hfsplus_bheader_record {
	uint16_t		depth;
	uint32_t		root;
	uint32_t		leaf_count;
	uint32_t		leaf_head;
	uint32_t		leaf_tail;
	uint16_t		node_size;
} __attribute__((packed));

struct hfsplus_catalog_key {
	uint16_t	key_len;
	uint32_t	parent_id;
	uint16_t	unicode_len;
	uint8_t		unicode[255 * 2];
} __attribute__((packed));

struct hfsplus_extent {
	uint32_t		start_block;
	uint32_t		block_count;
} __attribute__((packed));

#define HFSPLUS_EXTENT_COUNT		8
struct hfsplus_fork {
	uint64_t		total_size;
	uint32_t		clump_size;
	uint32_t		total_blocks;
	struct hfsplus_extent extents[HFSPLUS_EXTENT_COUNT];
} __attribute__((packed));

struct hfsplus_vol_header {
	uint8_t		signature[2];
	uint16_t		version;
	uint32_t		attributes;
	uint32_t		last_mount_vers;
	uint32_t		reserved;
	uint32_t		create_date;
	uint32_t		modify_date;
	uint32_t		backup_date;
	uint32_t		checked_date;
	uint32_t		file_count;
	uint32_t		folder_count;
	uint32_t		blocksize;
	uint32_t		total_blocks;
	uint32_t		free_blocks;
	uint32_t		next_alloc;
	uint32_t		rsrc_clump_sz;
	uint32_t		data_clump_sz;
	uint32_t		next_cnid;
	uint32_t		write_count;
	uint64_t		encodings_bmp;
	struct hfs_finder_info finder_info;
	struct hfsplus_fork alloc_file;
	struct hfsplus_fork ext_file;
	struct hfsplus_fork cat_file;
	struct hfsplus_fork attr_file;
	struct hfsplus_fork start_file;
}  __attribute__((packed));

#define HFSPLUS_SECTOR_SIZE        512

static int hfs_set_uuid(blkid_probe pr, unsigned char const *hfs_info, size_t len)
{
	static unsigned char const hash_init[UL_MD5LENGTH] = {
		0xb3, 0xe2, 0x0f, 0x39, 0xf2, 0x92, 0x11, 0xd6,
		0x97, 0xa4, 0x00, 0x30, 0x65, 0x43, 0xec, 0xac
	};
	unsigned char uuid[UL_MD5LENGTH];
	struct UL_MD5Context md5c;

	if (memcmp(hfs_info, "\0\0\0\0\0\0\0\0", len) == 0)
		return -1;

	ul_MD5Init(&md5c);
	ul_MD5Update(&md5c, hash_init, UL_MD5LENGTH);
	ul_MD5Update(&md5c, hfs_info, len);
	ul_MD5Final(uuid, &md5c);

	uuid[6] = 0x30 | (uuid[6] & 0x0f);
	uuid[8] = 0x80 | (uuid[8] & 0x3f);
	return blkid_probe_set_uuid(pr, uuid);
}

static int probe_hfs(blkid_probe pr, const struct blkid_idmag *mag)
{
	const struct hfs_mdb	*hfs;
	int size;

	hfs = blkid_probe_get_sb(pr, mag, struct hfs_mdb);
	if (!hfs)
		return errno ? -errno : 1;

	if ((memcmp(hfs->embed_sig, "H+", 2) == 0) ||
	    (memcmp(hfs->embed_sig, "HX", 2) == 0))
		return 1;	/* Not hfs, but an embedded HFS+ */

	size = be32_to_cpu(hfs->al_blk_size);
	if (!size || (size & (HFS_SECTOR_SIZE - 1))) {
		DBG(LOWPROBE, ul_debug("\tbad allocation size - ignore"));
		return 1;
	}

	hfs_set_uuid(pr, hfs->finder_info.id, sizeof(hfs->finder_info.id));

	size = hfs->label_len;
	if ((size_t) size > sizeof(hfs->label))
		size = sizeof(hfs->label);
	blkid_probe_set_label(pr, hfs->label, size);
	return 0;
}

static int probe_hfsplus(blkid_probe pr, const struct blkid_idmag *mag)
{
	struct hfsplus_extent extents[HFSPLUS_EXTENT_COUNT];
	const struct hfsplus_bnode_descriptor *descr;
	const struct hfsplus_bheader_record *bnode;
	const struct hfsplus_catalog_key *key;
	const struct hfsplus_vol_header *hfsplus;
	const struct hfs_mdb *sbd;
	unsigned int alloc_block_size;
	unsigned int alloc_first_block;
	unsigned int embed_first_block;
	uint64_t off = 0;
	unsigned int blocksize;
	unsigned int cat_block;
	unsigned int ext_block_start = 0;
	unsigned int ext_block_count;
	unsigned int record_count;
	unsigned int leaf_node_head;
	unsigned int leaf_node_count;
	unsigned int leaf_node_size;
	uint64_t leaf_block;
	int ext;
	uint64_t leaf_off;
	const unsigned char *buf;

	sbd = blkid_probe_get_sb(pr, mag, struct hfs_mdb);
	if (!sbd)
		return errno ? -errno : 1;

	/* Check for a HFS+ volume embedded in a HFS volume */
	if (memcmp(sbd->signature, "BD", 2) == 0) {
		if ((memcmp(sbd->embed_sig, "H+", 2) != 0) &&
		    (memcmp(sbd->embed_sig, "HX", 2) != 0))
			/* This must be an HFS volume, so fail */
			return 1;

		alloc_block_size = be32_to_cpu(sbd->al_blk_size);
		if (alloc_block_size < HFSPLUS_SECTOR_SIZE ||
		    alloc_block_size % HFSPLUS_SECTOR_SIZE)
		    return 1;

		alloc_first_block = be16_to_cpu(sbd->al_bl_st);
		embed_first_block = be16_to_cpu(sbd->embed_startblock);
		off = ((uint64_t) alloc_first_block * 512) +
			((uint64_t) embed_first_block * alloc_block_size);

		buf = blkid_probe_get_buffer(pr,
				off + (mag->kboff * 1024),
				sizeof(struct hfsplus_vol_header));
		hfsplus = (const struct hfsplus_vol_header *) buf;

	} else
		hfsplus = blkid_probe_get_sb(pr, mag,
				struct hfsplus_vol_header);

	if (!hfsplus)
		return errno ? -errno : 1;

	if ((memcmp(hfsplus->signature, "H+", 2) != 0) &&
	    (memcmp(hfsplus->signature, "HX", 2) != 0))
		return 1;

	/* Verify blocksize is initialized */
	blocksize = be32_to_cpu(hfsplus->blocksize);
	if (blocksize < HFSPLUS_SECTOR_SIZE || !is_power_of_2(blocksize))
		return 1;

	/* Save extends (hfsplus buffer may be later overwritten) */
	memcpy(extents, hfsplus->cat_file.extents, sizeof(extents));

	/* Make sure start_block is properly initialized */
	cat_block = be32_to_cpu(extents[0].start_block);
	if (off + ((uint64_t) cat_block * blocksize) > pr->size)
		return 1;

	hfs_set_uuid(pr, hfsplus->finder_info.id, sizeof(hfsplus->finder_info.id));

	blkid_probe_set_fsblocksize(pr, blocksize);
	blkid_probe_set_block_size(pr, blocksize);

	buf = blkid_probe_get_buffer(pr,
			off + ((uint64_t) cat_block * blocksize), 0x2000);
	if (!buf)
		return errno ? -errno : 0;

	bnode = (struct hfsplus_bheader_record *)
		&buf[sizeof(struct hfsplus_bnode_descriptor)];

	leaf_node_head = be32_to_cpu(bnode->leaf_head);
	leaf_node_size = be16_to_cpu(bnode->node_size);
	leaf_node_count = be32_to_cpu(bnode->leaf_count);

	if (leaf_node_size < sizeof(struct hfsplus_bnode_descriptor) +
	    sizeof(struct hfsplus_catalog_key) || leaf_node_count == 0)
		return 0;

	leaf_block = ((uint64_t) leaf_node_head * leaf_node_size) / blocksize;

	/* get physical location */
	for (ext = 0; ext < HFSPLUS_EXTENT_COUNT; ext++) {
		ext_block_start = be32_to_cpu(extents[ext].start_block);
		ext_block_count = be32_to_cpu(extents[ext].block_count);
		if (ext_block_count == 0)
			return 0;

		/* this is our extent */
		if (leaf_block < ext_block_count)
			break;

		leaf_block -= ext_block_count;
	}
	if (ext == HFSPLUS_EXTENT_COUNT)
		return 0;

	leaf_off = ((uint64_t) ext_block_start + leaf_block) * blocksize;

	buf = blkid_probe_get_buffer(pr,
				(uint64_t) off + leaf_off,
				leaf_node_size);
	if (!buf)
		return errno ? -errno : 0;

	descr = (struct hfsplus_bnode_descriptor *) buf;
	record_count = be16_to_cpu(descr->num_recs);
	if (record_count == 0)
		return 0;

	if (descr->type != HFS_NODE_LEAF)
		return 0;

	key = (struct hfsplus_catalog_key *)
		&buf[sizeof(struct hfsplus_bnode_descriptor)];

	if (be32_to_cpu(key->parent_id) != HFSPLUS_POR_CNID ||
	    be16_to_cpu(key->unicode_len) > 255)
		return 0;

	blkid_probe_set_utf8label(pr, key->unicode,
			be16_to_cpu(key->unicode_len) * 2,
			UL_ENCODE_UTF16BE);
	return 0;
}

const struct blkid_idinfo hfs_idinfo =
{
	.name		= "hfs",
	.usage		= BLKID_USAGE_FILESYSTEM,
	.probefunc	= probe_hfs,
	.flags		= BLKID_IDINFO_TOLERANT,
	.magics		=
	{
		{ .magic = "BD", .len = 2, .kboff = 1 },
		{ NULL }
	}
};

const struct blkid_idinfo hfsplus_idinfo =
{
	.name		= "hfsplus",
	.usage		= BLKID_USAGE_FILESYSTEM,
	.probefunc	= probe_hfsplus,
	.magics		=
	{
		{ .magic = "BD", .len = 2, .kboff = 1 },
		{ .magic = "H+", .len = 2, .kboff = 1 },
		{ .magic = "HX", .len = 2, .kboff = 1 },
		{ NULL }
	}
};
