/***************************************************************************

    chd.c

    MAME Compressed Hunks of Data file format

****************************************************************************

    Copyright Aaron Giles
    All rights reserved.

    Redistribution and use in source and binary forms, with or without
    modification, are permitted provided that the following conditions are
    met:

        * Redistributions of source code must retain the above copyright
          notice, this list of conditions and the following disclaimer.
        * Redistributions in binary form must reproduce the above copyright
          notice, this list of conditions and the following disclaimer in
          the documentation and/or other materials provided with the
          distribution.
        * Neither the name 'MAME' nor the names of its contributors may be
          used to endorse or promote products derived from this software
          without specific prior written permission.

    THIS SOFTWARE IS PROVIDED BY AARON GILES ''AS IS'' AND ANY EXPRESS OR
    IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
    WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
    DISCLAIMED. IN NO EVENT SHALL AARON GILES BE LIABLE FOR ANY DIRECT,
    INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
    (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
    SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
    HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
    STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
    IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
    POSSIBILITY OF SUCH DAMAGE.

***************************************************************************/

#include "chd.h"
#include "avcomp.h"
#include "md5.h"
#include "sha1.h"
#include <zlib.h>
#include <time.h>
#include <stddef.h>
#include <stdlib.h>




/***************************************************************************
    DEBUGGING
***************************************************************************/

#define PRINTF_MAX_HUNK				(0)



/***************************************************************************
    CONSTANTS
***************************************************************************/

#define MAP_STACK_ENTRIES			512			/* max number of entries to use on the stack */
#define MAP_ENTRY_SIZE				16			/* V3 and later */
#define OLD_MAP_ENTRY_SIZE			8			/* V1-V2 */
#define METADATA_HEADER_SIZE		16			/* metadata header size */
#define CRCMAP_HASH_SIZE			4095		/* number of CRC hashtable entries */

#define MAP_ENTRY_FLAG_TYPE_MASK	0x0f		/* what type of hunk */
#define MAP_ENTRY_FLAG_NO_CRC		0x10		/* no CRC is present */

#define MAP_ENTRY_TYPE_INVALID		0x00		/* invalid type */
#define MAP_ENTRY_TYPE_COMPRESSED	0x01		/* standard compression */
#define MAP_ENTRY_TYPE_UNCOMPRESSED	0x02		/* uncompressed data */
#define MAP_ENTRY_TYPE_MINI			0x03		/* mini: use offset as raw data */
#define MAP_ENTRY_TYPE_SELF_HUNK	0x04		/* same as another hunk in this file */
#define MAP_ENTRY_TYPE_PARENT_HUNK	0x05		/* same as a hunk in the parent file */

#define CHD_V1_SECTOR_SIZE			512			/* size of a "sector" in the V1 header */

#define COOKIE_VALUE				0xbaadf00d
#define MAX_ZLIB_ALLOCS				64

#define END_OF_LIST_COOKIE			"EndOfListCookie"

#define NO_MATCH					(~0)



/***************************************************************************
    MACROS
***************************************************************************/

#define EARLY_EXIT(x)				do { (void)(x); goto cleanup; } while (0)



/***************************************************************************
    TYPE DEFINITIONS
***************************************************************************/

/* interface to a codec */
typedef struct _codec_interface codec_interface;
struct _codec_interface
{
	UINT32		compression;				/* type of compression */
	const char *compname;					/* name of the algorithm */
	UINT8		lossy;						/* is this a lossy algorithm? */
	chd_error	(*init)(chd_file *chd);		/* codec initialize */
	void		(*free)(chd_file *chd);		/* codec free */
	chd_error	(*compress)(chd_file *chd, const void *src, UINT32 *complen); /* compress data */
	chd_error	(*decompress)(chd_file *chd, UINT32 complen, void *dst); /* decompress data */
	chd_error	(*config)(chd_file *chd, int param, void *config); /* configure */
};


/* a single map entry */
typedef struct _map_entry map_entry;
struct _map_entry
{
	UINT64					offset;			/* offset within the file of the data */
	UINT32					crc;			/* 32-bit CRC of the data */
	UINT32					length;			/* length of the data */
	UINT8					flags;			/* misc flags */
};


/* simple linked-list of hunks used for our CRC map */
typedef struct _crcmap_entry crcmap_entry;
struct _crcmap_entry
{
	UINT32					hunknum;		/* hunk number */
	crcmap_entry *			next;			/* next entry in list */
};


/* a single metadata entry */
typedef struct _metadata_entry metadata_entry;
struct _metadata_entry
{
	UINT64					offset;			/* offset within the file of the header */
	UINT64					next;			/* offset within the file of the next header */
	UINT64					prev;			/* offset within the file of the previous header */
	UINT32					length;			/* length of the metadata */
	UINT32					metatag;		/* metadata tag */
	UINT8					flags;			/* flag bits */
};


/* internal representation of an open CHD file */
struct _chd_file
{
	UINT32					cookie;			/* cookie, should equal COOKIE_VALUE */

	core_file *				file;			/* handle to the open core file */
	UINT8					owns_file;		/* flag indicating if this file should be closed on chd_close() */
	chd_header				header;			/* header, extracted from file */

	chd_file *				parent;			/* pointer to parent file, or NULL */

	map_entry *				map;			/* array of map entries */

	UINT8 *					cache;			/* hunk cache pointer */
	UINT32					cachehunk;		/* index of currently cached hunk */

	UINT8 *					compare;		/* hunk compare pointer */
	UINT32					comparehunk;	/* index of current compare data */

	UINT8 *					compressed;		/* pointer to buffer for compressed data */
	const codec_interface *	codecintf;		/* interface to the codec */
	void *					codecdata;		/* opaque pointer to codec data */

	crcmap_entry *			crcmap;			/* CRC map entries */
	crcmap_entry *			crcfree;		/* free list CRC entries */
	crcmap_entry **			crctable;		/* table of CRC entries */

	UINT32					maxhunk;		/* maximum hunk accessed */

	UINT8					compressing;	/* are we compressing? */
	struct MD5Context		compmd5;		/* running MD5 during compression */
	struct sha1_ctx			compsha1;		/* running SHA1 during compression */
	UINT32					comphunk;		/* next hunk we will compress */

	UINT8					verifying;		/* are we verifying? */
	struct MD5Context		vermd5; 		/* running MD5 during verification */
	struct sha1_ctx			versha1;		/* running SHA1 during verification */
	UINT32					verhunk;		/* next hunk we will verify */

	osd_work_queue *		workqueue;		/* pointer to work queue for async operations */
	osd_work_item *			workitem;		/* active work item, or NULL if none */
	UINT32					async_hunknum;	/* hunk index for asynchronous operations */
	void *					async_buffer;	/* buffer pointer for asynchronous operations */
};


/* codec-private data for the ZLIB codec */
typedef struct _zlib_codec_data zlib_codec_data;
struct _zlib_codec_data
{
	z_stream				inflater;
	z_stream				deflater;
	UINT32 *				allocptr[MAX_ZLIB_ALLOCS];
};


/* codec-private data for the A/V codec */
typedef struct _av_codec_data av_codec_data;
struct _av_codec_data
{
	avcomp_state *			compstate;
	av_codec_compress_config compress;
	av_codec_decompress_config decompress;
};


/* a single metadata hash entry */
typedef struct _metadata_hash metadata_hash;
struct _metadata_hash
{
	UINT8					tag[4];			/* tag of the metadata in big-endian */
	UINT8					sha1[CHD_SHA1_BYTES]; /* hash */
};



/***************************************************************************
    GLOBAL VARIABLES
***************************************************************************/

static const UINT8 nullmd5[CHD_MD5_BYTES] = { 0 };
static const UINT8 nullsha1[CHD_SHA1_BYTES] = { 0 };



/***************************************************************************
    PROTOTYPES
***************************************************************************/

/* internal async operations */
static void *async_read_callback(void *param, int threadid);
static void *async_write_callback(void *param, int threadid);

/* internal header operations */
static chd_error header_validate(const chd_header *header);
static chd_error header_read(core_file *file, chd_header *header);
static chd_error header_write(core_file *file, const chd_header *header);

/* internal hunk read/write */
static chd_error hunk_read_into_cache(chd_file *chd, UINT32 hunknum);
static chd_error hunk_read_into_memory(chd_file *chd, UINT32 hunknum, UINT8 *dest);
static chd_error hunk_write_from_memory(chd_file *chd, UINT32 hunknum, const UINT8 *src);

/* internal map access */
static chd_error map_write_initial(core_file *file, chd_file *parent, const chd_header *header);
static chd_error map_read(chd_file *chd);

/* internal CRC map access */
static void crcmap_init(chd_file *chd, int prepopulate);
static void crcmap_add_entry(chd_file *chd, UINT32 hunknum);
static UINT32 crcmap_find_hunk(chd_file *chd, UINT32 hunknum, UINT32 crc, const UINT8 *rawdata);

/* metadata management */
static chd_error metadata_find_entry(chd_file *chd, UINT32 metatag, UINT32 metaindex, metadata_entry *metaentry);
static chd_error metadata_set_previous_next(chd_file *chd, UINT64 prevoffset, UINT64 nextoffset);
static chd_error metadata_set_length(chd_file *chd, UINT64 offset, UINT32 length);
static chd_error metadata_compute_hash(chd_file *chd, const UINT8 *rawsha1, UINT8 *finalsha1);
static int CLIB_DECL metadata_hash_compare(const void *elem1, const void *elem2);

/* zlib compression codec */
static chd_error zlib_codec_init(chd_file *chd);
static void zlib_codec_free(chd_file *chd);
static chd_error zlib_codec_compress(chd_file *chd, const void *src, UINT32 *length);
static chd_error zlib_codec_decompress(chd_file *chd, UINT32 srclength, void *dest);
static voidpf zlib_fast_alloc(voidpf opaque, uInt items, uInt size);
static void zlib_fast_free(voidpf opaque, voidpf address);

/* A/V compression codec */
static chd_error av_codec_init(chd_file *chd);
static void av_codec_free(chd_file *chd);
static chd_error av_codec_compress(chd_file *chd, const void *src, UINT32 *length);
static chd_error av_codec_decompress(chd_file *chd, UINT32 srclength, void *dest);
static chd_error av_codec_config(chd_file *chd, int param, void *config);
static chd_error av_codec_postinit(chd_file *chd);



/***************************************************************************
    CODEC INTERFACES
***************************************************************************/

static const codec_interface codec_interfaces[] =
{
	/* "none" or no compression */
	{
		CHDCOMPRESSION_NONE,
		"none",
		FALSE,
		NULL,
		NULL,
		NULL,
		NULL,
		NULL
	},

	/* standard zlib compression */
	{
		CHDCOMPRESSION_ZLIB,
		"zlib",
		FALSE,
		zlib_codec_init,
		zlib_codec_free,
		zlib_codec_compress,
		zlib_codec_decompress,
		NULL
	},

	/* zlib+ compression */
	{
		CHDCOMPRESSION_ZLIB_PLUS,
		"zlib+",
		FALSE,
		zlib_codec_init,
		zlib_codec_free,
		zlib_codec_compress,
		zlib_codec_decompress,
		NULL
	},

	/* a/v compression */
	{
		CHDCOMPRESSION_AV,
		"A/V",
		TRUE,
		av_codec_init,
		av_codec_free,
		av_codec_compress,
		av_codec_decompress,
		av_codec_config
	}
};



/***************************************************************************
    INLINE FUNCTIONS
***************************************************************************/

/*-------------------------------------------------
    get_bigendian_uint64 - fetch a UINT64 from
    the data stream in bigendian order
-------------------------------------------------*/

INLINE UINT64 get_bigendian_uint64(const UINT8 *base)
{
	return ((UINT64)base[0] << 56) | ((UINT64)base[1] << 48) | ((UINT64)base[2] << 40) | ((UINT64)base[3] << 32) |
			((UINT64)base[4] << 24) | ((UINT64)base[5] << 16) | ((UINT64)base[6] << 8) | (UINT64)base[7];
}


/*-------------------------------------------------
    put_bigendian_uint64 - write a UINT64 to
    the data stream in bigendian order
-------------------------------------------------*/

INLINE void put_bigendian_uint64(UINT8 *base, UINT64 value)
{
	base[0] = value >> 56;
	base[1] = value >> 48;
	base[2] = value >> 40;
	base[3] = value >> 32;
	base[4] = value >> 24;
	base[5] = value >> 16;
	base[6] = value >> 8;
	base[7] = value;
}


/*-------------------------------------------------
    get_bigendian_uint32 - fetch a UINT32 from
    the data stream in bigendian order
-------------------------------------------------*/

INLINE UINT32 get_bigendian_uint32(const UINT8 *base)
{
	return (base[0] << 24) | (base[1] << 16) | (base[2] << 8) | base[3];
}


/*-------------------------------------------------
    put_bigendian_uint32 - write a UINT32 to
    the data stream in bigendian order
-------------------------------------------------*/

INLINE void put_bigendian_uint32(UINT8 *base, UINT32 value)
{
	base[0] = value >> 24;
	base[1] = value >> 16;
	base[2] = value >> 8;
	base[3] = value;
}


/*-------------------------------------------------
    get_bigendian_uint16 - fetch a UINT16 from
    the data stream in bigendian order
-------------------------------------------------*/

INLINE UINT16 get_bigendian_uint16(const UINT8 *base)
{
	return (base[0] << 8) | base[1];
}


/*-------------------------------------------------
    put_bigendian_uint16 - write a UINT16 to
    the data stream in bigendian order
-------------------------------------------------*/

INLINE void put_bigendian_uint16(UINT8 *base, UINT16 value)
{
	base[0] = value >> 8;
	base[1] = value;
}


/*-------------------------------------------------
    map_extract - extract a single map
    entry from the datastream
-------------------------------------------------*/

INLINE void map_extract(const UINT8 *base, map_entry *entry)
{
	entry->offset = get_bigendian_uint64(&base[0]);
	entry->crc = get_bigendian_uint32(&base[8]);
	entry->length = get_bigendian_uint16(&base[12]) | (base[14] << 16);
	entry->flags = base[15];
}


/*-------------------------------------------------
    map_assemble - write a single map
    entry to the datastream
-------------------------------------------------*/

INLINE void map_assemble(UINT8 *base, map_entry *entry)
{
	put_bigendian_uint64(&base[0], entry->offset);
	put_bigendian_uint32(&base[8], entry->crc);
	put_bigendian_uint16(&base[12], entry->length);
	base[14] = entry->length >> 16;
	base[15] = entry->flags;
}


/*-------------------------------------------------
    map_extract_old - extract a single map
    entry in old format from the datastream
-------------------------------------------------*/

INLINE void map_extract_old(const UINT8 *base, map_entry *entry, UINT32 hunkbytes)
{
	entry->offset = get_bigendian_uint64(&base[0]);
	entry->crc = 0;
	entry->length = entry->offset >> 44;
	entry->flags = MAP_ENTRY_FLAG_NO_CRC | ((entry->length == hunkbytes) ? MAP_ENTRY_TYPE_UNCOMPRESSED : MAP_ENTRY_TYPE_COMPRESSED);
#ifdef __MWERKS__
	entry->offset = entry->offset & 0x00000FFFFFFFFFFFLL;
#else
	entry->offset = (entry->offset << 20) >> 20;
#endif
}


/*-------------------------------------------------
    queue_async_operation - queue a new work
    item
-------------------------------------------------*/

INLINE int queue_async_operation(chd_file *chd, osd_work_callback callback)
{
	/* if no queue yet, create one on the fly */
	if (chd->workqueue == NULL)
	{
		chd->workqueue = osd_work_queue_alloc(WORK_QUEUE_FLAG_IO);
		if (chd->workqueue == NULL)
			return FALSE;
	}

	/* make sure we cleared out the previous item */
	if (chd->workitem != NULL)
		return FALSE;

	/* create a new work item to run the job */
	chd->workitem = osd_work_item_queue(chd->workqueue, callback, chd, 0);
	if (chd->workitem == NULL)
		return FALSE;

	return TRUE;
}


/*-------------------------------------------------
    wait_for_pending_async - wait for any pending
    async
-------------------------------------------------*/

INLINE void wait_for_pending_async(chd_file *chd)
{
	/* if something is pending, wait for it */
	if (chd->workitem != NULL)
	{
		/* 10 seconds should be enough for anything! */
		int wait_successful = osd_work_item_wait(chd->workitem, 10 * osd_ticks_per_second());
		if (!wait_successful)
			osd_break_into_debugger("Pending async operation never completed!");
	}
}



/***************************************************************************
    CHD FILE MANAGEMENT
***************************************************************************/

/*-------------------------------------------------
    chd_create_file - create a new CHD file
-------------------------------------------------*/

chd_error chd_create_file(core_file *file, UINT64 logicalbytes, UINT32 hunkbytes, UINT32 compression, chd_file *parent)
{
	chd_file *newchd = NULL;
	chd_header header;
	chd_error err;
	int intfnum;

	/* verify parameters */
	if (file == NULL)
		EARLY_EXIT(err = CHDERR_INVALID_PARAMETER);
	if (parent == NULL && (logicalbytes == 0 || hunkbytes == 0))
		EARLY_EXIT(err = CHDERR_INVALID_PARAMETER);

	/* verify the compression type */
	for (intfnum = 0; intfnum < ARRAY_LENGTH(codec_interfaces); intfnum++)
		if (codec_interfaces[intfnum].compression == compression)
			break;
	if (intfnum == ARRAY_LENGTH(codec_interfaces))
		EARLY_EXIT(err = CHDERR_INVALID_PARAMETER);

	/* if we have a parent, the sizes come from there */
	if (parent != NULL)
	{
		logicalbytes = parent->header.logicalbytes;
		hunkbytes = parent->header.hunkbytes;
	}

	/* if we have a parent, it must be V3 or later */
	if (parent != NULL && parent->header.version < 3)
		EARLY_EXIT(err = CHDERR_UNSUPPORTED_VERSION);

	/* build the header */
	memset(&header, 0, sizeof(header));
	header.length = CHD_V4_HEADER_SIZE;
	header.version = CHD_HEADER_VERSION;
	header.flags = CHDFLAGS_IS_WRITEABLE;
	header.compression = compression;
	header.hunkbytes = hunkbytes;
	header.totalhunks = (logicalbytes + hunkbytes - 1) / hunkbytes;
	header.logicalbytes = logicalbytes;

	/* tweaks if there is a parent */
	if (parent != NULL)
	{
		header.flags |= CHDFLAGS_HAS_PARENT;
		memcpy(&header.parentmd5[0], &parent->header.md5[0], sizeof(header.parentmd5));
		memcpy(&header.parentsha1[0], &parent->header.sha1[0], sizeof(header.parentsha1));
	}

	/* validate it */
	err = header_validate(&header);
	if (err != CHDERR_NONE)
		EARLY_EXIT(err);

	/* write the resulting header */
	err = header_write(file, &header);
	if (err != CHDERR_NONE)
		EARLY_EXIT(err);

	/* create an initial map */
	err = map_write_initial(file, parent, &header);
	if (err != CHDERR_NONE)
		EARLY_EXIT(err);

	/* if we have a parent, clone the metadata */
	if (parent != NULL)
	{
		/* open the new CHD via the standard mechanism */
		err = chd_open_file(file, CHD_OPEN_READWRITE, parent, &newchd);
		if (err != CHDERR_NONE)
			EARLY_EXIT(err);

		/* close the metadata */
		err = chd_clone_metadata(parent, newchd);
		if (err != CHDERR_NONE)
			EARLY_EXIT(err);

		/* close the CHD */
		chd_close(newchd);
	}

	return CHDERR_NONE;

cleanup:
	if (newchd != NULL)
		chd_close(newchd);
	return err;
}


/*-------------------------------------------------
    chd_open_file - open a CHD file for access
-------------------------------------------------*/

chd_error chd_open_file(core_file *file, int mode, chd_file *parent, chd_file **chd)
{
	chd_file *newchd = NULL;
	chd_error err;
	int intfnum;

	/* verify parameters */
	if (file == NULL)
		EARLY_EXIT(err = CHDERR_INVALID_PARAMETER);

	/* punt if invalid parent */
	if (parent != NULL && parent->cookie != COOKIE_VALUE)
		EARLY_EXIT(err = CHDERR_INVALID_PARAMETER);

	/* allocate memory for the final result */
	newchd = (chd_file *)malloc(sizeof(**chd));
	if (newchd == NULL)
		EARLY_EXIT(err = CHDERR_OUT_OF_MEMORY);
	memset(newchd, 0, sizeof(*newchd));
	newchd->cookie = COOKIE_VALUE;
	newchd->parent = parent;
	newchd->file = file;

	/* now attempt to read the header */
	err = header_read(newchd->file, &newchd->header);
	if (err != CHDERR_NONE)
		EARLY_EXIT(err);

	/* validate the header */
	err = header_validate(&newchd->header);
	if (err != CHDERR_NONE)
		EARLY_EXIT(err);

	/* make sure we don't open a read-only file writeable */
	if (mode == CHD_OPEN_READWRITE && !(newchd->header.flags & CHDFLAGS_IS_WRITEABLE))
		EARLY_EXIT(err = CHDERR_FILE_NOT_WRITEABLE);

	/* also, never open an older version writeable */
	if (mode == CHD_OPEN_READWRITE && newchd->header.version < CHD_HEADER_VERSION)
		EARLY_EXIT(err = CHDERR_UNSUPPORTED_VERSION);

	/* if we need a parent, make sure we have one */
	if (parent == NULL && (newchd->header.flags & CHDFLAGS_HAS_PARENT))
		EARLY_EXIT(err = CHDERR_REQUIRES_PARENT);

	/* make sure we have a valid parent */
	if (parent != NULL)
	{
		/* check MD5 if it isn't empty */
		if (memcmp(nullmd5, newchd->header.parentmd5, sizeof(newchd->header.parentmd5)) != 0 &&
			memcmp(nullmd5, newchd->parent->header.md5, sizeof(newchd->parent->header.md5)) != 0 &&
			memcmp(newchd->parent->header.md5, newchd->header.parentmd5, sizeof(newchd->header.parentmd5)) != 0)
			EARLY_EXIT(err = CHDERR_INVALID_PARENT);

		/* check SHA1 if it isn't empty */
		if (memcmp(nullsha1, newchd->header.parentsha1, sizeof(newchd->header.parentsha1)) != 0 &&
			memcmp(nullsha1, newchd->parent->header.sha1, sizeof(newchd->parent->header.sha1)) != 0 &&
			memcmp(newchd->parent->header.sha1, newchd->header.parentsha1, sizeof(newchd->header.parentsha1)) != 0)
			EARLY_EXIT(err = CHDERR_INVALID_PARENT);
	}

	/* now read the hunk map */
	err = map_read(newchd);
	if (err != CHDERR_NONE)
		EARLY_EXIT(err);

	/* allocate and init the hunk cache */
	newchd->cache = (UINT8 *)malloc(newchd->header.hunkbytes);
	newchd->compare = (UINT8 *)malloc(newchd->header.hunkbytes);
	if (newchd->cache == NULL || newchd->compare == NULL)
		EARLY_EXIT(err = CHDERR_OUT_OF_MEMORY);
	newchd->cachehunk = ~0;
	newchd->comparehunk = ~0;

	/* allocate the temporary compressed buffer */
	newchd->compressed = (UINT8 *)malloc(newchd->header.hunkbytes);
	if (newchd->compressed == NULL)
		EARLY_EXIT(err = CHDERR_OUT_OF_MEMORY);

	/* find the codec interface */
	for (intfnum = 0; intfnum < ARRAY_LENGTH(codec_interfaces); intfnum++)
		if (codec_interfaces[intfnum].compression == newchd->header.compression)
		{
			newchd->codecintf = &codec_interfaces[intfnum];
			break;
		}
	if (intfnum == ARRAY_LENGTH(codec_interfaces))
		EARLY_EXIT(err = CHDERR_UNSUPPORTED_FORMAT);

	/* initialize the codec */
	if (newchd->codecintf->init != NULL)
		err = (*newchd->codecintf->init)(newchd);
	if (err != CHDERR_NONE)
		EARLY_EXIT(err);

	/* all done */
	*chd = newchd;
	return CHDERR_NONE;

cleanup:
	if (newchd != NULL)
		chd_close(newchd);
	return err;
}


/*-------------------------------------------------
    chd_create - create a CHD file by
    filename
-------------------------------------------------*/

chd_error chd_create(const char *filename, UINT64 logicalbytes, UINT32 hunkbytes, UINT32 compression, chd_file *parent)
{
	core_file *file = NULL;
	file_error filerr;
	chd_error chderr;

	filerr = core_fopen(filename, OPEN_FLAG_READ | OPEN_FLAG_WRITE | OPEN_FLAG_CREATE, &file);
	if (filerr != FILERR_NONE)
	{
		chderr = CHDERR_FILE_NOT_FOUND;
		goto cleanup;
	}

	chderr = chd_create_file(file, logicalbytes, hunkbytes, compression, parent);
	if (chderr != CHDERR_NONE)
		goto cleanup;

cleanup:
	if (file != NULL)
		core_fclose(file);
	return chderr;
}


/*-------------------------------------------------
    chd_open - open a CHD file by
    filename
-------------------------------------------------*/

chd_error chd_open(const char *filename, int mode, chd_file *parent, chd_file **chd)
{
	chd_error err;
	file_error filerr;
	core_file *file = NULL;
	UINT32 openflags;

	/* choose the proper mode */
	switch(mode)
	{
		case CHD_OPEN_READ:
			openflags = OPEN_FLAG_READ;
			break;

		case CHD_OPEN_READWRITE:
			openflags = OPEN_FLAG_READ | OPEN_FLAG_WRITE;
			break;

		default:
			err = CHDERR_INVALID_PARAMETER;
			goto cleanup;
	}

	/* open the file */
	filerr = core_fopen(filename, openflags, &file);
	if (filerr != FILERR_NONE)
	{
		err = CHDERR_FILE_NOT_FOUND;
		goto cleanup;
	}

	/* now open the CHD */
	err = chd_open_file(file, mode, parent, chd);
	if (err != CHDERR_NONE)
		goto cleanup;

	/* we now own this file */
	(*chd)->owns_file = TRUE;

cleanup:
	if ((err != CHDERR_NONE) && (file != NULL))
		core_fclose(file);
	return err;
}


/*-------------------------------------------------
    chd_close - close a CHD file for access
-------------------------------------------------*/

void chd_close(chd_file *chd)
{
	/* punt if NULL or invalid */
	if (chd == NULL || chd->cookie != COOKIE_VALUE)
		return;

	/* wait for any pending async operations */
	wait_for_pending_async(chd);

	/* kill the work queue and any work item */
	if (chd->workitem != NULL)
		osd_work_item_release(chd->workitem);
	if (chd->workqueue != NULL)
		osd_work_queue_free(chd->workqueue);

	/* deinit the codec */
	if (chd->codecintf != NULL && chd->codecintf->free != NULL)
		(*chd->codecintf->free)(chd);

	/* free the compressed data buffer */
	if (chd->compressed != NULL)
		free(chd->compressed);

	/* free the hunk cache and compare data */
	if (chd->compare != NULL)
		free(chd->compare);
	if (chd->cache != NULL)
		free(chd->cache);

	/* free the hunk map */
	if (chd->map != NULL)
		free(chd->map);

	/* free the CRC table */
	if (chd->crctable != NULL)
		free(chd->crctable);

	/* free the CRC map */
	if (chd->crcmap != NULL)
		free(chd->crcmap);

	/* close the file */
	if (chd->owns_file && chd->file != NULL)
		core_fclose(chd->file);

	if (PRINTF_MAX_HUNK) printf("Max hunk = %d/%d\n", chd->maxhunk, chd->header.totalhunks);

	/* free our memory */
	free(chd);
}


/*-------------------------------------------------
    chd_core_file - return the associated
    core_file
-------------------------------------------------*/

core_file *chd_core_file(chd_file *chd)
{
	return chd->file;
}


/*-------------------------------------------------
    chd_error_string - return an error string for
    the given CHD error
-------------------------------------------------*/

const char *chd_error_string(chd_error err)
{
	switch (err)
	{
		case CHDERR_NONE:						return "no error";
		case CHDERR_NO_INTERFACE:				return "no drive interface";
		case CHDERR_OUT_OF_MEMORY:				return "out of memory";
		case CHDERR_INVALID_FILE:				return "invalid file";
		case CHDERR_INVALID_PARAMETER:			return "invalid parameter";
		case CHDERR_INVALID_DATA:				return "invalid data";
		case CHDERR_FILE_NOT_FOUND:				return "file not found";
		case CHDERR_REQUIRES_PARENT:			return "requires parent";
		case CHDERR_FILE_NOT_WRITEABLE:			return "file not writeable";
		case CHDERR_READ_ERROR:					return "read error";
		case CHDERR_WRITE_ERROR:				return "write error";
		case CHDERR_CODEC_ERROR:				return "codec error";
		case CHDERR_INVALID_PARENT:				return "invalid parent";
		case CHDERR_HUNK_OUT_OF_RANGE:			return "hunk out of range";
		case CHDERR_DECOMPRESSION_ERROR:		return "decompression error";
		case CHDERR_COMPRESSION_ERROR:			return "compression error";
		case CHDERR_CANT_CREATE_FILE:			return "can't create file";
		case CHDERR_CANT_VERIFY:				return "can't verify file";
		case CHDERR_NOT_SUPPORTED:				return "operation not supported";
		case CHDERR_METADATA_NOT_FOUND:			return "can't find metadata";
		case CHDERR_INVALID_METADATA_SIZE:		return "invalid metadata size";
		case CHDERR_UNSUPPORTED_VERSION:		return "unsupported CHD version";
		case CHDERR_VERIFY_INCOMPLETE:			return "incomplete verify";
		case CHDERR_INVALID_METADATA:			return "invalid metadata";
		case CHDERR_INVALID_STATE:				return "invalid state";
		case CHDERR_OPERATION_PENDING:			return "operation pending";
		case CHDERR_NO_ASYNC_OPERATION:			return "no async operation in progress";
		case CHDERR_UNSUPPORTED_FORMAT:			return "unsupported format";
		default:								return "undocumented error";
	}
}



/***************************************************************************
    CHD HEADER MANAGEMENT
***************************************************************************/

/*-------------------------------------------------
    chd_get_header - return a pointer to the
    extracted header data
-------------------------------------------------*/

const chd_header *chd_get_header(chd_file *chd)
{
	/* punt if NULL or invalid */
	if (chd == NULL || chd->cookie != COOKIE_VALUE)
		return NULL;

	return &chd->header;
}


/*-------------------------------------------------
    chd_set_header_file - write the current header to
    the file
-------------------------------------------------*/

chd_error chd_set_header_file(core_file *file, const chd_header *header)
{
	chd_header oldheader;
	chd_error err;

	/* validate the header */
	err = header_validate(header);
	if (err != CHDERR_NONE)
		EARLY_EXIT(err);

	/* read the old header */
	err = header_read(file, &oldheader);
	if (err != CHDERR_NONE)
		EARLY_EXIT(err);

	/* make sure we're only making valid changes */
	if (header->length != oldheader.length)
		EARLY_EXIT(err = CHDERR_INVALID_PARAMETER);
	if (header->version != oldheader.version)
		EARLY_EXIT(err = CHDERR_INVALID_PARAMETER);
	if (header->compression != oldheader.compression)
		EARLY_EXIT(err = CHDERR_INVALID_PARAMETER);
	if (header->hunkbytes != oldheader.hunkbytes)
		EARLY_EXIT(err = CHDERR_INVALID_PARAMETER);
	if (header->totalhunks != oldheader.totalhunks)
		EARLY_EXIT(err = CHDERR_INVALID_PARAMETER);
	if (header->metaoffset != oldheader.metaoffset)
		EARLY_EXIT(err = CHDERR_INVALID_PARAMETER);
	if (header->obsolete_hunksize != oldheader.obsolete_hunksize)
		EARLY_EXIT(err = CHDERR_INVALID_PARAMETER);

	/* write the new header */
	err = header_write(file, header);
	if (err != CHDERR_NONE)
		EARLY_EXIT(err);

	return CHDERR_NONE;

cleanup:
	return err;
}


/*-------------------------------------------------
    chd_set_header - write the current
    header to the file
-------------------------------------------------*/

chd_error chd_set_header(const char *filename, const chd_header *header)
{
	core_file *file = NULL;
	file_error filerr;
	chd_error err;

	filerr = core_fopen(filename, OPEN_FLAG_READ | OPEN_FLAG_WRITE, &file);
	if (filerr != FILERR_NONE)
	{
		err = CHDERR_FILE_NOT_FOUND;
		goto cleanup;
	}

	err = chd_set_header_file(file, header);
	if (err != CHDERR_NONE)
		goto cleanup;

cleanup:
	if (file != NULL)
		core_fclose(file);
	return err;
}



/***************************************************************************
    CORE DATA READ/WRITE
***************************************************************************/

/*-------------------------------------------------
    chd_read - read a single hunk from the CHD
    file
-------------------------------------------------*/

chd_error chd_read(chd_file *chd, UINT32 hunknum, void *buffer)
{
	/* punt if NULL or invalid */
	if (chd == NULL || chd->cookie != COOKIE_VALUE)
		return CHDERR_INVALID_PARAMETER;

	/* if we're past the end, fail */
	if (hunknum >= chd->header.totalhunks)
		return CHDERR_HUNK_OUT_OF_RANGE;

	/* wait for any pending async operations */
	wait_for_pending_async(chd);

	/* perform the read */
	return hunk_read_into_memory(chd, hunknum, (UINT8 *)buffer);
}


/*-------------------------------------------------
    chd_read_async - read a single hunk from the
    CHD file asynchronously
-------------------------------------------------*/

chd_error chd_read_async(chd_file *chd, UINT32 hunknum, void *buffer)
{
	/* punt if NULL or invalid */
	if (chd == NULL || chd->cookie != COOKIE_VALUE)
		return CHDERR_INVALID_PARAMETER;

	/* if we're past the end, fail */
	if (hunknum >= chd->header.totalhunks)
		return CHDERR_HUNK_OUT_OF_RANGE;

	/* wait for any pending async operations */
	wait_for_pending_async(chd);

	/* set the async parameters */
	chd->async_hunknum = hunknum;
	chd->async_buffer = buffer;

	/* queue the work item */
	if (queue_async_operation(chd, async_read_callback))
		return CHDERR_OPERATION_PENDING;

	/* if we fail, fall back on the sync version */
	return chd_read(chd, hunknum, buffer);
}


/*-------------------------------------------------
    chd_write - write a single hunk to the CHD
    file
-------------------------------------------------*/

chd_error chd_write(chd_file *chd, UINT32 hunknum, const void *buffer)
{
	/* punt if NULL or invalid */
	if (chd == NULL || chd->cookie != COOKIE_VALUE)
		return CHDERR_INVALID_PARAMETER;

	/* if we're past the end, fail */
	if (hunknum >= chd->header.totalhunks)
		return CHDERR_HUNK_OUT_OF_RANGE;

	/* wait for any pending async operations */
	wait_for_pending_async(chd);

	/* then write out the hunk */
	return hunk_write_from_memory(chd, hunknum, (const UINT8 *)buffer);
}


/*-------------------------------------------------
    chd_write_async - write a single hunk to the
    CHD file asynchronously
-------------------------------------------------*/

chd_error chd_write_async(chd_file *chd, UINT32 hunknum, const void *buffer)
{
	/* punt if NULL or invalid */
	if (chd == NULL || chd->cookie != COOKIE_VALUE)
		return CHDERR_INVALID_PARAMETER;

	/* if we're past the end, fail */
	if (hunknum >= chd->header.totalhunks)
		return CHDERR_HUNK_OUT_OF_RANGE;

	/* wait for any pending async operations */
	wait_for_pending_async(chd);

	/* set the async parameters */
	chd->async_hunknum = hunknum;
	chd->async_buffer = (void *)buffer;

	/* queue the work item */
	if (queue_async_operation(chd, async_write_callback))
		return CHDERR_OPERATION_PENDING;

	/* if we fail, fall back on the sync version */
	return chd_write(chd, hunknum, buffer);
}


/*-------------------------------------------------
    chd_async_complete - get the result of a
    completed work item and clear it out of the
    system
-------------------------------------------------*/

chd_error chd_async_complete(chd_file *chd)
{
	void *result;

	/* if nothing present, return an error */
	if (chd->workitem == NULL)
		return CHDERR_NO_ASYNC_OPERATION;

	/* wait for the work to complete */
	wait_for_pending_async(chd);

	/* get the result and free the work item */
	result = osd_work_item_result(chd->workitem);
	osd_work_item_release(chd->workitem);
	chd->workitem = NULL;

	return (chd_error)(ptrdiff_t)result;
}



/***************************************************************************
    METADATA MANAGEMENT
***************************************************************************/

/*-------------------------------------------------
    chd_get_metadata - get the indexed metadata
    of the given type
-------------------------------------------------*/

chd_error chd_get_metadata(chd_file *chd, UINT32 searchtag, UINT32 searchindex, void *output, UINT32 outputlen, UINT32 *resultlen, UINT32 *resulttag, UINT8 *resultflags)
{
	metadata_entry metaentry;
	chd_error err;
	UINT32 count;

	/* wait for any pending async operations */
	wait_for_pending_async(chd);

	/* if we didn't find it, just return */
	err = metadata_find_entry(chd, searchtag, searchindex, &metaentry);
	if (err != CHDERR_NONE)
	{
		/* unless we're an old version and they are requesting hard disk metadata */
		if (chd->header.version < 3 && (searchtag == HARD_DISK_METADATA_TAG || searchtag == CHDMETATAG_WILDCARD) && searchindex == 0)
		{
			char faux_metadata[256];
			UINT32 faux_length;

			/* fill in the faux metadata */
			sprintf(faux_metadata, HARD_DISK_METADATA_FORMAT, chd->header.obsolete_cylinders, chd->header.obsolete_heads, chd->header.obsolete_sectors, chd->header.hunkbytes / chd->header.obsolete_hunksize);
			faux_length = (UINT32)strlen(faux_metadata) + 1;

			/* copy the metadata itself */
			memcpy(output, faux_metadata, MIN(outputlen, faux_length));

			/* return the length of the data and the tag */
			if (resultlen != NULL)
				*resultlen = faux_length;
			if (resulttag != NULL)
				*resulttag = HARD_DISK_METADATA_TAG;
			return CHDERR_NONE;
		}
		return err;
	}

	/* read the metadata */
	outputlen = MIN(outputlen, metaentry.length);
	core_fseek(chd->file, metaentry.offset + METADATA_HEADER_SIZE, SEEK_SET);
	count = core_fread(chd->file, output, outputlen);
	if (count != outputlen)
		return CHDERR_READ_ERROR;

	/* return the length of the data and the tag */
	if (resultlen != NULL)
		*resultlen = metaentry.length;
	if (resulttag != NULL)
		*resulttag = metaentry.metatag;
	if (resultflags != NULL)
		*resultflags = metaentry.flags;
	return CHDERR_NONE;
}


/*-------------------------------------------------
    chd_set_metadata - write the indexed metadata
    of the given type
-------------------------------------------------*/

chd_error chd_set_metadata(chd_file *chd, UINT32 metatag, UINT32 metaindex, const void *inputbuf, UINT32 inputlen, UINT8 flags)
{
	UINT8 raw_meta_header[METADATA_HEADER_SIZE];
	metadata_entry metaentry = { 0 };
	chd_error err;
	UINT64 offset;
	UINT32 count;

	/* if the disk is an old version, punt */
	if (chd->header.version < 3)
		return CHDERR_NOT_SUPPORTED;

	/* if the disk isn't writeable, punt */
	if (!(chd->header.flags & CHDFLAGS_IS_WRITEABLE))
		return CHDERR_FILE_NOT_WRITEABLE;

	/* must write at least 1 byte */
	if (inputlen < 1)
		return CHDERR_INVALID_PARAMETER;

	/* no more than 16MB */
	if (inputlen >= 16 * 1024 * 1024)
		return CHDERR_INVALID_PARAMETER;

	/* wait for any pending async operations */
	wait_for_pending_async(chd);

	/* find the entry if it already exists */
	err = metadata_find_entry(chd, metatag, metaindex, &metaentry);

	/* if it's there and it fits, just overwrite */
	if (err == CHDERR_NONE && inputlen <= metaentry.length)
	{
		/* overwrite the original data with our new input data */
		core_fseek(chd->file, metaentry.offset + METADATA_HEADER_SIZE, SEEK_SET);
		count = core_fwrite(chd->file, inputbuf, inputlen);
		if (count != inputlen)
		{
			err = CHDERR_WRITE_ERROR;
			goto update;
		}

		/* if the lengths don't match, we need to update the length in our header */
		if (inputlen != metaentry.length)
			err = metadata_set_length(chd, metaentry.offset, inputlen);
		goto update;
	}

	/* if we already have an entry, unlink it */
	if (err == CHDERR_NONE)
	{
		err = metadata_set_previous_next(chd, metaentry.prev, metaentry.next);
		if (err != CHDERR_NONE)
			goto update;
	}

	/* now build us a new entry */
	put_bigendian_uint32(&raw_meta_header[0], metatag);
	put_bigendian_uint32(&raw_meta_header[4], (inputlen & 0x00ffffff) | (flags << 24));
	put_bigendian_uint64(&raw_meta_header[8], (err == CHDERR_NONE) ? metaentry.next : 0);

	/* write out the new header */
	offset = core_fsize(chd->file);
	core_fseek(chd->file, offset, SEEK_SET);
	count = core_fwrite(chd->file, raw_meta_header, sizeof(raw_meta_header));
	if (count != sizeof(raw_meta_header))
	{
		err = CHDERR_WRITE_ERROR;
		goto update;
	}

	/* follow that with the data */
	core_fseek(chd->file, offset + METADATA_HEADER_SIZE, SEEK_SET);
	count = core_fwrite(chd->file, inputbuf, inputlen);
	if (count != inputlen)
	{
		err = CHDERR_WRITE_ERROR;
		goto update;
	}

	/* set the previous entry to point to us */
	err = metadata_set_previous_next(chd, metaentry.prev, offset);

update:
	/* update the hash */
	if (metadata_compute_hash(chd, chd->header.rawsha1, chd->header.sha1) == CHDERR_NONE)
		err = header_write(chd->file, &chd->header);
	return err;
}


/*-------------------------------------------------
    chd_clone_metadata - clone the metadata from
    one CHD to a second
-------------------------------------------------*/

chd_error chd_clone_metadata(chd_file *source, chd_file *dest)
{
	UINT32 metatag, metasize, metaindex;
	UINT8 metabuffer[1024];
	UINT8 metaflags;
	chd_error err;

	/* clone the metadata */
	for (metaindex = 0; ; metaindex++)
	{
		/* fetch the next piece of metadata */
		err = chd_get_metadata(source, CHDMETATAG_WILDCARD, metaindex, metabuffer, sizeof(metabuffer), &metasize, &metatag, &metaflags);
		if (err != CHDERR_NONE)
		{
			if (err == CHDERR_METADATA_NOT_FOUND)
				err = CHDERR_NONE;
			break;
		}

		/* if that fit, just write it back from the temporary buffer */
		if (metasize <= sizeof(metabuffer))
		{
			/* write it to the target */
			err = chd_set_metadata(dest, metatag, CHD_METAINDEX_APPEND, metabuffer, metasize, metaflags);
			if (err != CHDERR_NONE)
				break;
		}

		/* otherwise, allocate a bigger temporary buffer */
		else
		{
			UINT8 *allocbuffer = (UINT8 *)malloc(metasize);
			if (allocbuffer == NULL)
			{
				err = CHDERR_OUT_OF_MEMORY;
				break;
			}

			/* re-read the whole thing */
			err = chd_get_metadata(source, CHDMETATAG_WILDCARD, metaindex, allocbuffer, metasize, &metasize, &metatag, &metaflags);
			if (err != CHDERR_NONE)
			{
				free(allocbuffer);
				break;
			}

			/* write it to the target */
			err = chd_set_metadata(dest, metatag, CHD_METAINDEX_APPEND, allocbuffer, metasize, metaflags);
			free(allocbuffer);
			if (err != CHDERR_NONE)
				break;
		}
	}
	return err;
}



/***************************************************************************
    COMPRESSION MANAGEMENT
***************************************************************************/

/*-------------------------------------------------
    chd_compress_begin - begin compressing data
    into a CHD
-------------------------------------------------*/

chd_error chd_compress_begin(chd_file *chd)
{
	chd_error err;

	/* verify parameters */
	if (chd == NULL)
		return CHDERR_INVALID_PARAMETER;

	/* wait for any pending async operations */
	wait_for_pending_async(chd);

	/* mark the CHD writeable and write the updated header */
	chd->header.flags |= CHDFLAGS_IS_WRITEABLE;
	err = header_write(chd->file, &chd->header);
	if (err != CHDERR_NONE)
		return err;

	/* create CRC maps for the new CHD and the parent */
	crcmap_init(chd, FALSE);
	if (chd->parent != NULL)
		crcmap_init(chd->parent, TRUE);

	/* init the MD5/SHA1 computations */
	MD5Init(&chd->compmd5);
	sha1_init(&chd->compsha1);
	chd->compressing = TRUE;
	chd->comphunk = 0;

	return CHDERR_NONE;
}


/*-------------------------------------------------
    chd_compress_hunk - append data to a CHD
    that is being compressed
-------------------------------------------------*/

chd_error chd_compress_hunk(chd_file *chd, const void *data, double *curratio)
{
	UINT32 thishunk = chd->comphunk++;
	UINT64 sourceoffset = (UINT64)thishunk * (UINT64)chd->header.hunkbytes;
	UINT32 bytestochecksum;
	const void *crcdata;
	chd_error err;

	/* error if in the wrong state */
	if (!chd->compressing)
		return CHDERR_INVALID_STATE;

	/* write out the hunk */
	err = hunk_write_from_memory(chd, thishunk, (const UINT8 *)data);
	if (err != CHDERR_NONE)
		return err;

	/* if we are lossy, then we need to use the decompressed version in */
	/* the cache as our MD5/SHA1 source */
	crcdata = (chd->codecintf->lossy || data == NULL) ? chd->cache : data;

	/* update the MD5/SHA1 */
	bytestochecksum = chd->header.hunkbytes;
	if (sourceoffset + chd->header.hunkbytes > chd->header.logicalbytes)
	{
		if (sourceoffset >= chd->header.logicalbytes)
			bytestochecksum = 0;
		else
			bytestochecksum = chd->header.logicalbytes - sourceoffset;
	}
	if (bytestochecksum > 0)
	{
		MD5Update(&chd->compmd5, (const unsigned char *)crcdata, bytestochecksum);
		sha1_update(&chd->compsha1, bytestochecksum, (const UINT8 *)crcdata);
	}

	/* update our CRC map */
	if ((chd->map[thishunk].flags & MAP_ENTRY_FLAG_TYPE_MASK) != MAP_ENTRY_TYPE_SELF_HUNK &&
		(chd->map[thishunk].flags & MAP_ENTRY_FLAG_TYPE_MASK) != MAP_ENTRY_TYPE_PARENT_HUNK)
		crcmap_add_entry(chd, thishunk);

	/* update the ratio */
	if (curratio != NULL)
	{
		UINT64 curlength = core_fsize(chd->file);
		*curratio = 1.0 - (double)curlength / (double)((UINT64)chd->comphunk * (UINT64)chd->header.hunkbytes);
	}

	return CHDERR_NONE;
}


/*-------------------------------------------------
    chd_compress_finish - complete compression of
    a CHD
-------------------------------------------------*/

chd_error chd_compress_finish(chd_file *chd, int write_protect)
{
	/* error if in the wrong state */
	if (!chd->compressing)
		return CHDERR_INVALID_STATE;

	/* compute the final MD5/SHA1 values */
	MD5Final(chd->header.md5, &chd->compmd5);
	sha1_final(&chd->compsha1);
	sha1_digest(&chd->compsha1, SHA1_DIGEST_SIZE, chd->header.rawsha1);
	metadata_compute_hash(chd, chd->header.rawsha1, chd->header.sha1);

	/* turn off the writeable flag and re-write the header */
	if (chd->header.compression != CHDCOMPRESSION_NONE || write_protect)
		chd->header.flags &= ~CHDFLAGS_IS_WRITEABLE;
	chd->compressing = FALSE;
	return header_write(chd->file, &chd->header);
}



/***************************************************************************
    VERIFICATION
***************************************************************************/

/*-------------------------------------------------
    chd_verify_begin - begin compressing data
    into a CHD
-------------------------------------------------*/

chd_error chd_verify_begin(chd_file *chd)
{
	/* verify parameters */
	if (chd == NULL)
		return CHDERR_INVALID_PARAMETER;

	/* if this is a writeable file image, we can't verify */
	if (chd->header.flags & CHDFLAGS_IS_WRITEABLE)
		return CHDERR_CANT_VERIFY;

	/* wait for any pending async operations */
	wait_for_pending_async(chd);

	/* init the MD5/SHA1 computations */
	MD5Init(&chd->vermd5);
	sha1_init(&chd->versha1);
	chd->verifying = TRUE;
	chd->verhunk = 0;

	return CHDERR_NONE;
}


/*-------------------------------------------------
    chd_verify_hunk - verify the next hunk in
    the CHD
-------------------------------------------------*/

chd_error chd_verify_hunk(chd_file *chd)
{
	UINT32 thishunk = chd->verhunk++;
	UINT64 hunkoffset = (UINT64)thishunk * (UINT64)chd->header.hunkbytes;
	map_entry *entry;
	chd_error err;

	/* error if in the wrong state */
	if (!chd->verifying)
		return CHDERR_INVALID_STATE;

	/* read the hunk into the cache */
	err = hunk_read_into_cache(chd, thishunk);
	if (err != CHDERR_NONE)
		return err;

	/* update the MD5/SHA1 */
	if (hunkoffset < chd->header.logicalbytes)
	{
		UINT64 bytestochecksum = MIN(chd->header.hunkbytes, chd->header.logicalbytes - hunkoffset);
		if (bytestochecksum > 0)
		{
			MD5Update(&chd->vermd5, chd->cache, bytestochecksum);
			sha1_update(&chd->versha1, bytestochecksum, chd->cache);
		}
	}

	/* validate the CRC if we have one */
	entry = &chd->map[thishunk];
	if (!(entry->flags & MAP_ENTRY_FLAG_NO_CRC) && entry->crc != crc32(0, chd->cache, chd->header.hunkbytes))
		return CHDERR_DECOMPRESSION_ERROR;

	return CHDERR_NONE;
}


/*-------------------------------------------------
    chd_verify_finish - finish verification of
    the CHD
-------------------------------------------------*/

chd_error chd_verify_finish(chd_file *chd, chd_verify_result *result)
{
	/* error if in the wrong state */
	if (!chd->verifying)
		return CHDERR_INVALID_STATE;

	/* compute the final MD5 */
	MD5Final(result->md5, &chd->vermd5);

	/* compute the final SHA1 */
	sha1_final(&chd->versha1);
	sha1_digest(&chd->versha1, SHA1_DIGEST_SIZE, result->rawsha1);

	/* compute the overall hash including metadata */
	metadata_compute_hash(chd, result->rawsha1, result->sha1);

	/* return an error */
	chd->verifying = FALSE;
	return (chd->verhunk < chd->header.totalhunks) ? CHDERR_VERIFY_INCOMPLETE : CHDERR_NONE;
}



/***************************************************************************
    CODEC INTERFACES
***************************************************************************/

/*-------------------------------------------------
    chd_codec_config - set internal codec
    parameters
-------------------------------------------------*/

chd_error chd_codec_config(chd_file *chd, int param, void *config)
{
	/* wait for any pending async operations */
	wait_for_pending_async(chd);

	/* if the codec has a configuration callback, call through to it */
	if (chd->codecintf->config != NULL)
		return (*chd->codecintf->config)(chd, param, config);

	return CHDERR_INVALID_PARAMETER;
}


/*-------------------------------------------------
    chd_get_codec_name - get the name of a
    particular codec
-------------------------------------------------*/

const char *chd_get_codec_name(UINT32 codec)
{
	int intfnum;

	/* look for a matching codec and return its string */
	for (intfnum = 0; intfnum < ARRAY_LENGTH(codec_interfaces); intfnum++)
		if (codec_interfaces[intfnum].compression == codec)
			return codec_interfaces[intfnum].compname;

	return "Unknown";
}



/***************************************************************************
    INTERNAL ASYNC OPERATIONS
***************************************************************************/

/*-------------------------------------------------
    async_read_callback - asynchronous reading
    callback
-------------------------------------------------*/

static void *async_read_callback(void *param, int threadid)
{
	chd_file *chd = (chd_file *)param;
	chd_error err;

	/* read the hunk into the cache */
	err = hunk_read_into_memory(chd, chd->async_hunknum, (UINT8 *)chd->async_buffer);

	/* return the error */
	return (void *)err;
}


/*-------------------------------------------------
    async_write_callback - asynchronous writing
    callback
-------------------------------------------------*/

static void *async_write_callback(void *param, int threadid)
{
	chd_file *chd = (chd_file *)param;
	chd_error err;

	/* write the hunk from memory */
	err = hunk_write_from_memory(chd, chd->async_hunknum, (const UINT8 *)chd->async_buffer);

	/* return the error */
	return (void *)err;
}



/***************************************************************************
    INTERNAL HEADER OPERATIONS
***************************************************************************/

/*-------------------------------------------------
    header_validate - check the validity of a
    CHD header
-------------------------------------------------*/

static chd_error header_validate(const chd_header *header)
{
	int intfnum;

	/* require a valid version */
	if (header->version == 0 || header->version > CHD_HEADER_VERSION)
		return CHDERR_UNSUPPORTED_VERSION;

	/* require a valid length */
	if ((header->version == 1 && header->length != CHD_V1_HEADER_SIZE) ||
		(header->version == 2 && header->length != CHD_V2_HEADER_SIZE) ||
		(header->version == 3 && header->length != CHD_V3_HEADER_SIZE) ||
		(header->version == 4 && header->length != CHD_V4_HEADER_SIZE))
		return CHDERR_INVALID_PARAMETER;

	/* require valid flags */
	if (header->flags & CHDFLAGS_UNDEFINED)
		return CHDERR_INVALID_PARAMETER;

	/* require a supported compression mechanism */
	for (intfnum = 0; intfnum < ARRAY_LENGTH(codec_interfaces); intfnum++)
		if (codec_interfaces[intfnum].compression == header->compression)
			break;
	if (intfnum == ARRAY_LENGTH(codec_interfaces))
		return CHDERR_INVALID_PARAMETER;

	/* require a valid hunksize */
	if (header->hunkbytes == 0 || header->hunkbytes >= 65536 * 256)
		return CHDERR_INVALID_PARAMETER;

	/* require a valid hunk count */
	if (header->totalhunks == 0)
		return CHDERR_INVALID_PARAMETER;

	/* require a valid MD5 and/or SHA1 if we're using a parent */
	if ((header->flags & CHDFLAGS_HAS_PARENT) && memcmp(header->parentmd5, nullmd5, sizeof(nullmd5)) == 0 && memcmp(header->parentsha1, nullsha1, sizeof(nullsha1)) == 0)
		return CHDERR_INVALID_PARAMETER;

	/* if we're V3 or later, the obsolete fields must be 0 */
	if (header->version >= 3 &&
		(header->obsolete_cylinders != 0 || header->obsolete_sectors != 0 ||
		 header->obsolete_heads != 0 || header->obsolete_hunksize != 0))
		return CHDERR_INVALID_PARAMETER;

	/* if we're pre-V3, the obsolete fields must NOT be 0 */
	if (header->version < 3 &&
		(header->obsolete_cylinders == 0 || header->obsolete_sectors == 0 ||
		 header->obsolete_heads == 0 || header->obsolete_hunksize == 0))
		return CHDERR_INVALID_PARAMETER;

	return CHDERR_NONE;
}


/*-------------------------------------------------
    header_read - read a CHD header into the
    internal data structure
-------------------------------------------------*/

static chd_error header_read(core_file *file, chd_header *header)
{
	UINT8 rawheader[CHD_MAX_HEADER_SIZE];
	UINT32 count;

	/* punt if NULL */
	if (header == NULL)
		return CHDERR_INVALID_PARAMETER;

	/* punt if invalid file */
	if (file == NULL)
		return CHDERR_INVALID_FILE;

	/* seek and read */
	core_fseek(file, 0, SEEK_SET);
	count = core_fread(file, rawheader, sizeof(rawheader));
	if (count != sizeof(rawheader))
		return CHDERR_READ_ERROR;

	/* verify the tag */
	if (strncmp((char *)rawheader, "MComprHD", 8) != 0)
		return CHDERR_INVALID_DATA;

	/* extract the direct data */
	memset(header, 0, sizeof(*header));
	header->length        = get_bigendian_uint32(&rawheader[8]);
	header->version       = get_bigendian_uint32(&rawheader[12]);

	/* make sure it's a version we understand */
	if (header->version == 0 || header->version > CHD_HEADER_VERSION)
		return CHDERR_UNSUPPORTED_VERSION;

	/* make sure the length is expected */
	if ((header->version == 1 && header->length != CHD_V1_HEADER_SIZE) ||
		(header->version == 2 && header->length != CHD_V2_HEADER_SIZE) ||
		(header->version == 3 && header->length != CHD_V3_HEADER_SIZE) ||
		(header->version == 4 && header->length != CHD_V4_HEADER_SIZE))
		return CHDERR_INVALID_DATA;

	/* extract the common data */
	header->flags         = get_bigendian_uint32(&rawheader[16]);
	header->compression   = get_bigendian_uint32(&rawheader[20]);

	/* extract the V1/V2-specific data */
	if (header->version < 3)
	{
		int seclen = (header->version == 1) ? CHD_V1_SECTOR_SIZE : get_bigendian_uint32(&rawheader[76]);
		header->obsolete_hunksize  = get_bigendian_uint32(&rawheader[24]);
		header->totalhunks         = get_bigendian_uint32(&rawheader[28]);
		header->obsolete_cylinders = get_bigendian_uint32(&rawheader[32]);
		header->obsolete_heads     = get_bigendian_uint32(&rawheader[36]);
		header->obsolete_sectors   = get_bigendian_uint32(&rawheader[40]);
		memcpy(header->md5, &rawheader[44], CHD_MD5_BYTES);
		memcpy(header->parentmd5, &rawheader[60], CHD_MD5_BYTES);
		header->logicalbytes = (UINT64)header->obsolete_cylinders * (UINT64)header->obsolete_heads * (UINT64)header->obsolete_sectors * (UINT64)seclen;
		header->hunkbytes = seclen * header->obsolete_hunksize;
		header->metaoffset = 0;
	}

	/* extract the V3-specific data */
	else if (header->version == 3)
	{
		header->totalhunks   = get_bigendian_uint32(&rawheader[24]);
		header->logicalbytes = get_bigendian_uint64(&rawheader[28]);
		header->metaoffset   = get_bigendian_uint64(&rawheader[36]);
		memcpy(header->md5, &rawheader[44], CHD_MD5_BYTES);
		memcpy(header->parentmd5, &rawheader[60], CHD_MD5_BYTES);
		header->hunkbytes    = get_bigendian_uint32(&rawheader[76]);
		memcpy(header->sha1, &rawheader[80], CHD_SHA1_BYTES);
		memcpy(header->parentsha1, &rawheader[100], CHD_SHA1_BYTES);
	}

	/* extract the V4-specific data */
	else
	{
		header->totalhunks   = get_bigendian_uint32(&rawheader[24]);
		header->logicalbytes = get_bigendian_uint64(&rawheader[28]);
		header->metaoffset   = get_bigendian_uint64(&rawheader[36]);
		header->hunkbytes    = get_bigendian_uint32(&rawheader[44]);
		memcpy(header->sha1, &rawheader[48], CHD_SHA1_BYTES);
		memcpy(header->parentsha1, &rawheader[68], CHD_SHA1_BYTES);
		memcpy(header->rawsha1, &rawheader[88], CHD_SHA1_BYTES);
	}

	/* guess it worked */
	return CHDERR_NONE;
}


/*-------------------------------------------------
    header_write - write a CHD header from the
    internal data structure
-------------------------------------------------*/

static chd_error header_write(core_file *file, const chd_header *header)
{
	UINT8 rawheader[CHD_MAX_HEADER_SIZE];
	UINT32 count;

	/* punt if NULL */
	if (header == NULL)
		return CHDERR_INVALID_PARAMETER;

	/* punt if invalid file */
	if (file == NULL)
		return CHDERR_INVALID_FILE;

	/* only support writing modern headers */
	if (header->version != 4)
		return CHDERR_INVALID_PARAMETER;

	/* assemble the data */
	memset(rawheader, 0, sizeof(rawheader));
	memcpy(rawheader, "MComprHD", 8);

	put_bigendian_uint32(&rawheader[8],  CHD_V4_HEADER_SIZE);
	put_bigendian_uint32(&rawheader[12], header->version);
	put_bigendian_uint32(&rawheader[16], header->flags);
	put_bigendian_uint32(&rawheader[20], header->compression);
	put_bigendian_uint32(&rawheader[24], header->totalhunks);
	put_bigendian_uint64(&rawheader[28], header->logicalbytes);
	put_bigendian_uint64(&rawheader[36], header->metaoffset);
	put_bigendian_uint32(&rawheader[44], header->hunkbytes);
	memcpy(&rawheader[48], header->sha1, CHD_SHA1_BYTES);
	memcpy(&rawheader[68], header->parentsha1, CHD_SHA1_BYTES);
	memcpy(&rawheader[88], header->rawsha1, CHD_SHA1_BYTES);

	/* seek and write */
	core_fseek(file, 0, SEEK_SET);
	count = core_fwrite(file, rawheader, CHD_V4_HEADER_SIZE);
	if (count != CHD_V4_HEADER_SIZE)
		return CHDERR_WRITE_ERROR;

	return CHDERR_NONE;
}



/***************************************************************************
    INTERNAL HUNK READ/WRITE
***************************************************************************/

/*-------------------------------------------------
    hunk_read_into_cache - read a hunk into
    the CHD's hunk cache
-------------------------------------------------*/

static chd_error hunk_read_into_cache(chd_file *chd, UINT32 hunknum)
{
	chd_error err;

	/* track the max */
	if (hunknum > chd->maxhunk)
		chd->maxhunk = hunknum;

	/* if we're already in the cache, we're done */
	if (chd->cachehunk == hunknum)
		return CHDERR_NONE;
	chd->cachehunk = ~0;

	/* otherwise, read the data */
	err = hunk_read_into_memory(chd, hunknum, chd->cache);
	if (err != CHDERR_NONE)
		return err;

	/* mark the hunk successfully cached in */
	chd->cachehunk = hunknum;
	return CHDERR_NONE;
}


/*-------------------------------------------------
    hunk_read_into_memory - read a hunk into
    memory at the given location
-------------------------------------------------*/

static chd_error hunk_read_into_memory(chd_file *chd, UINT32 hunknum, UINT8 *dest)
{
	map_entry *entry = &chd->map[hunknum];
	chd_error err;
	UINT32 bytes;

	/* return an error if out of range */
	if (hunknum >= chd->header.totalhunks)
		return CHDERR_HUNK_OUT_OF_RANGE;

	/* switch off the entry type */
	switch (entry->flags & MAP_ENTRY_FLAG_TYPE_MASK)
	{
		/* compressed data */
		case MAP_ENTRY_TYPE_COMPRESSED:

			/* read it into the decompression buffer */
			core_fseek(chd->file, entry->offset, SEEK_SET);
			bytes = core_fread(chd->file, chd->compressed, entry->length);
			if (bytes != entry->length)
				return CHDERR_READ_ERROR;

			/* now decompress using the codec */
			err = CHDERR_NONE;
			if (chd->codecintf->decompress != NULL)
				err = (*chd->codecintf->decompress)(chd, entry->length, dest);
			if (err != CHDERR_NONE)
				return err;
			break;

		/* uncompressed data */
		case MAP_ENTRY_TYPE_UNCOMPRESSED:
			core_fseek(chd->file, entry->offset, SEEK_SET);
			bytes = core_fread(chd->file, dest, chd->header.hunkbytes);
			if (bytes != chd->header.hunkbytes)
				return CHDERR_READ_ERROR;
			break;

		/* mini-compressed data */
		case MAP_ENTRY_TYPE_MINI:
			put_bigendian_uint64(&dest[0], entry->offset);
			for (bytes = 8; bytes < chd->header.hunkbytes; bytes++)
				dest[bytes] = dest[bytes - 8];
			break;

		/* self-referenced data */
		case MAP_ENTRY_TYPE_SELF_HUNK:
			if (chd->cachehunk == entry->offset && dest == chd->cache)
				break;
			return hunk_read_into_memory(chd, entry->offset, dest);

		/* parent-referenced data */
		case MAP_ENTRY_TYPE_PARENT_HUNK:
			err = hunk_read_into_memory(chd->parent, entry->offset, dest);
			if (err != CHDERR_NONE)
				return err;
			break;
	}
	return CHDERR_NONE;
}


/*-------------------------------------------------
    hunk_write_from_memory - write a hunk from
    memory into a CHD
-------------------------------------------------*/

static chd_error hunk_write_from_memory(chd_file *chd, UINT32 hunknum, const UINT8 *src)
{
	map_entry *entry = &chd->map[hunknum];
	map_entry newentry;
	UINT8 fileentry[MAP_ENTRY_SIZE];
	const void *data = src;
	UINT32 bytes = 0, match;
	chd_error err;

	/* track the max */
	if (hunknum > chd->maxhunk)
		chd->maxhunk = hunknum;

	/* first compute the CRC of the original data */
	newentry.crc = 0;
	if (src != NULL)
		newentry.crc = crc32(0, &src[0], chd->header.hunkbytes);

	/* if we're not a lossy codec, compute the CRC and look for matches */
	if (!chd->codecintf->lossy && src != NULL)
	{
		/* some extra stuff for zlib+ compression */
		if (chd->header.compression >= CHDCOMPRESSION_ZLIB_PLUS)
		{
			/* see if we can mini-compress first */
			for (bytes = 8; bytes < chd->header.hunkbytes; bytes++)
				if (src[bytes] != src[bytes - 8])
					break;

			/* if so, we don't need to write any data */
			if (bytes == chd->header.hunkbytes)
			{
				newentry.offset = get_bigendian_uint64(&src[0]);
				newentry.length = 0;
				newentry.flags = MAP_ENTRY_TYPE_MINI;
				goto write_entry;
			}

			/* otherwise, see if we can find a match in the current file */
			match = crcmap_find_hunk(chd, hunknum, newentry.crc, &src[0]);
			if (match != NO_MATCH)
			{
				newentry.offset = match;
				newentry.length = 0;
				newentry.flags = MAP_ENTRY_TYPE_SELF_HUNK;
				goto write_entry;
			}

			/* if we have a parent, see if we can find a match in there */
			if (chd->header.flags & CHDFLAGS_HAS_PARENT)
			{
				match = crcmap_find_hunk(chd->parent, ~0, newentry.crc, &src[0]);
				if (match != NO_MATCH)
				{
					newentry.offset = match;
					newentry.length = 0;
					newentry.flags = MAP_ENTRY_TYPE_PARENT_HUNK;
					goto write_entry;
				}
			}
		}
	}

	/* now try compressing the data */
	err = CHDERR_COMPRESSION_ERROR;
	if (chd->codecintf->compress != NULL)
		err = (*chd->codecintf->compress)(chd, src, &bytes);

	/* if that worked, and we're lossy, decompress and CRC the result */
	if (err == CHDERR_NONE && (chd->codecintf->lossy || src == NULL))
	{
		err = (*chd->codecintf->decompress)(chd, bytes, chd->cache);
		if (err == CHDERR_NONE)
			newentry.crc = crc32(0, chd->cache, chd->header.hunkbytes);
	}

	/* if we succeeded in compressing the data, replace our data pointer and mark it so */
	if (err == CHDERR_NONE)
	{
		data = chd->compressed;
		newentry.length = bytes;
		newentry.flags = MAP_ENTRY_TYPE_COMPRESSED;
	}

	/* otherwise, mark it uncompressed and use the original data */
	else
	{
		newentry.length = chd->header.hunkbytes;
		newentry.flags = MAP_ENTRY_TYPE_UNCOMPRESSED;
	}

	/* if the data doesn't fit into the previous entry, make a new one at the eof */
	newentry.offset = entry->offset;
	if (newentry.offset == 0 || newentry.length > entry->length)
		newentry.offset = core_fsize(chd->file);

	/* write the data */
	core_fseek(chd->file, newentry.offset, SEEK_SET);
	bytes = core_fwrite(chd->file, data, newentry.length);
	if (bytes != newentry.length)
		return CHDERR_WRITE_ERROR;

	/* update the entry in memory */
write_entry:
	*entry = newentry;

	/* update the map on file */
	map_assemble(&fileentry[0], &chd->map[hunknum]);
	core_fseek(chd->file, chd->header.length + hunknum * sizeof(fileentry), SEEK_SET);
	bytes = core_fwrite(chd->file, &fileentry[0], sizeof(fileentry));
	if (bytes != sizeof(fileentry))
		return CHDERR_WRITE_ERROR;

	return CHDERR_NONE;
}



/***************************************************************************
    INTERNAL MAP ACCESS
***************************************************************************/

/*-------------------------------------------------
    map_write_initial - write an initial map to
    a new CHD file
-------------------------------------------------*/

static chd_error map_write_initial(core_file *file, chd_file *parent, const chd_header *header)
{
	UINT8 blank_map_entries[MAP_STACK_ENTRIES * MAP_ENTRY_SIZE];
	int fullchunks, remainder, count, i, j;
	map_entry mapentry;
	UINT64 fileoffset;

	/* create a mini hunk of 0's */
	mapentry.offset = 0;
	mapentry.crc = 0;
	mapentry.length = 0;
	mapentry.flags = MAP_ENTRY_TYPE_MINI | MAP_ENTRY_FLAG_NO_CRC;
	for (i = 0; i < MAP_STACK_ENTRIES; i++)
		map_assemble(&blank_map_entries[i * MAP_ENTRY_SIZE], &mapentry);

	/* prepare to write a blank hunk map immediately following */
	fileoffset = header->length;
	fullchunks = header->totalhunks / MAP_STACK_ENTRIES;
	remainder = header->totalhunks % MAP_STACK_ENTRIES;

	/* first write full chunks of blank entries */
	for (i = 0; i < fullchunks; i++)
	{
		/* parent drives need to be mapped through */
		if (parent != NULL)
			for (j = 0; j < MAP_STACK_ENTRIES; j++)
			{
				mapentry.offset = i * MAP_STACK_ENTRIES + j;
				mapentry.crc = parent->map[i * MAP_STACK_ENTRIES + j].crc;
				mapentry.flags = MAP_ENTRY_TYPE_PARENT_HUNK;
				map_assemble(&blank_map_entries[j * MAP_ENTRY_SIZE], &mapentry);
			}

		/* write the chunks */
		core_fseek(file, fileoffset, SEEK_SET);
		count = core_fwrite(file, blank_map_entries, sizeof(blank_map_entries));
		if (count != sizeof(blank_map_entries))
			return CHDERR_WRITE_ERROR;
		fileoffset += sizeof(blank_map_entries);
	}

	/* then write the remainder */
	if (remainder > 0)
	{
		/* parent drives need to be mapped through */
		if (parent != NULL)
			for (j = 0; j < remainder; j++)
			{
				mapentry.offset = i * MAP_STACK_ENTRIES + j;
				mapentry.crc = parent->map[i * MAP_STACK_ENTRIES + j].crc;
				mapentry.flags = MAP_ENTRY_TYPE_PARENT_HUNK;
				map_assemble(&blank_map_entries[j * MAP_ENTRY_SIZE], &mapentry);
			}

		/* write the chunks */
		core_fseek(file, fileoffset, SEEK_SET);
		count = core_fwrite(file, blank_map_entries, remainder * MAP_ENTRY_SIZE);
		if (count != remainder * MAP_ENTRY_SIZE)
			return CHDERR_WRITE_ERROR;
		fileoffset += remainder * MAP_ENTRY_SIZE;
	}

	/* then write a special end-of-list cookie */
	memcpy(&blank_map_entries[0], END_OF_LIST_COOKIE, MAP_ENTRY_SIZE);
	core_fseek(file, fileoffset, SEEK_SET);
	count = core_fwrite(file, blank_map_entries, MAP_ENTRY_SIZE);
	if (count != MAP_ENTRY_SIZE)
		return CHDERR_WRITE_ERROR;

	return CHDERR_NONE;
}


/*-------------------------------------------------
    map_read - read the initial sector map
-------------------------------------------------*/

static chd_error map_read(chd_file *chd)
{
	UINT32 entrysize = (chd->header.version < 3) ? OLD_MAP_ENTRY_SIZE : MAP_ENTRY_SIZE;
	UINT8 raw_map_entries[MAP_STACK_ENTRIES * MAP_ENTRY_SIZE];
	UINT64 fileoffset, maxoffset = 0;
	UINT8 cookie[MAP_ENTRY_SIZE];
	UINT32 count;
	chd_error err;
	int i;

	/* first allocate memory */
	chd->map = (map_entry *)malloc(sizeof(chd->map[0]) * chd->header.totalhunks);
	if (!chd->map)
		return CHDERR_OUT_OF_MEMORY;

	/* read the map entries in in chunks and extract to the map list */
	fileoffset = chd->header.length;
	for (i = 0; i < chd->header.totalhunks; i += MAP_STACK_ENTRIES)
	{
		/* compute how many entries this time */
		int entries = chd->header.totalhunks - i, j;
		if (entries > MAP_STACK_ENTRIES)
			entries = MAP_STACK_ENTRIES;

		/* read that many */
		core_fseek(chd->file, fileoffset, SEEK_SET);
		count = core_fread(chd->file, raw_map_entries, entries * entrysize);
		if (count != entries * entrysize)
		{
			err = CHDERR_READ_ERROR;
			goto cleanup;
		}
		fileoffset += entries * entrysize;

		/* process that many */
		if (entrysize == MAP_ENTRY_SIZE)
		{
			for (j = 0; j < entries; j++)
				map_extract(&raw_map_entries[j * MAP_ENTRY_SIZE], &chd->map[i + j]);
		}
		else
		{
			for (j = 0; j < entries; j++)
				map_extract_old(&raw_map_entries[j * OLD_MAP_ENTRY_SIZE], &chd->map[i + j], chd->header.hunkbytes);
		}

		/* track the maximum offset */
		for (j = 0; j < entries; j++)
			if ((chd->map[i + j].flags & MAP_ENTRY_FLAG_TYPE_MASK) == MAP_ENTRY_TYPE_COMPRESSED ||
				(chd->map[i + j].flags & MAP_ENTRY_FLAG_TYPE_MASK) == MAP_ENTRY_TYPE_UNCOMPRESSED)
				maxoffset = MAX(maxoffset, chd->map[i + j].offset + chd->map[i + j].length);
	}

	/* verify the cookie */
	core_fseek(chd->file, fileoffset, SEEK_SET);
	count = core_fread(chd->file, &cookie, entrysize);
	if (count != entrysize || memcmp(&cookie, END_OF_LIST_COOKIE, entrysize))
	{
		err = CHDERR_INVALID_FILE;
		goto cleanup;
	}

	/* verify the length */
	if (maxoffset > core_fsize(chd->file))
	{
		err = CHDERR_INVALID_FILE;
		goto cleanup;
	}
	return CHDERR_NONE;

cleanup:
	if (chd->map)
		free(chd->map);
	chd->map = NULL;
	return err;
}



/***************************************************************************
    INTERNAL CRC MAP ACCESS
***************************************************************************/

/*-------------------------------------------------
    crcmap_init - initialize the CRC map
-------------------------------------------------*/

static void crcmap_init(chd_file *chd, int prepopulate)
{
	int i;

	/* if we already have one, bail */
	if (chd->crcmap != NULL)
		return;

	/* reset all pointers */
	chd->crcmap = NULL;
	chd->crcfree = NULL;
	chd->crctable = NULL;

	/* allocate a list; one for each hunk */
	chd->crcmap = (crcmap_entry *)malloc(chd->header.totalhunks * sizeof(chd->crcmap[0]));
	if (chd->crcmap == NULL)
		return;

	/* allocate a CRC map table */
	chd->crctable = (crcmap_entry **)malloc(CRCMAP_HASH_SIZE * sizeof(chd->crctable[0]));
	if (chd->crctable == NULL)
	{
		free(chd->crcmap);
		chd->crcmap = NULL;
		return;
	}

	/* initialize the free list */
	for (i = 0; i < chd->header.totalhunks; i++)
	{
		chd->crcmap[i].next = chd->crcfree;
		chd->crcfree = &chd->crcmap[i];
	}

	/* initialize the table */
	memset(chd->crctable, 0, CRCMAP_HASH_SIZE * sizeof(chd->crctable[0]));

	/* if we're to prepopulate, go for it */
	if (prepopulate)
		for (i = 0; i < chd->header.totalhunks; i++)
			crcmap_add_entry(chd, i);
}


/*-------------------------------------------------
    crcmap_add_entry - log a CRC entry
-------------------------------------------------*/

static void crcmap_add_entry(chd_file *chd, UINT32 hunknum)
{
	UINT32 hash = chd->map[hunknum].crc % CRCMAP_HASH_SIZE;
	crcmap_entry *crcmap;

	/* pull a free entry off the list */
	crcmap = chd->crcfree;
	chd->crcfree = crcmap->next;

	/* set up the entry and link it into the hash table */
	crcmap->hunknum = hunknum;
	crcmap->next = chd->crctable[hash];
	chd->crctable[hash] = crcmap;
}


/*-------------------------------------------------
    crcmap_verify_hunk_match - verify that a
    hunk really matches by doing a byte-for-byte
    compare
-------------------------------------------------*/

static int crcmap_verify_hunk_match(chd_file *chd, UINT32 hunknum, const UINT8 *rawdata)
{
	/* we have a potential match -- better be sure */
	/* read the hunk from disk and compare byte-for-byte */
	if (hunknum != chd->comparehunk)
	{
		chd->comparehunk = ~0;
		if (hunk_read_into_memory(chd, hunknum, chd->compare) == CHDERR_NONE)
			chd->comparehunk = hunknum;
	}
	return (hunknum == chd->comparehunk && memcmp(rawdata, chd->compare, chd->header.hunkbytes) == 0);
}


/*-------------------------------------------------
    crcmap_find_hunk - find a hunk with a matching
    CRC in the map
-------------------------------------------------*/

static UINT32 crcmap_find_hunk(chd_file *chd, UINT32 hunknum, UINT32 crc, const UINT8 *rawdata)
{
	UINT32 lasthunk = (hunknum < chd->header.totalhunks) ? hunknum : chd->header.totalhunks;
	int curhunk;

	/* if we have a CRC map, use that */
	if (chd->crctable)
	{
		crcmap_entry *curentry;
		for (curentry = chd->crctable[crc % CRCMAP_HASH_SIZE]; curentry; curentry = curentry->next)
		{
			curhunk = curentry->hunknum;
			if (chd->map[curhunk].crc == crc && !(chd->map[curhunk].flags & MAP_ENTRY_FLAG_NO_CRC) && crcmap_verify_hunk_match(chd, curhunk, rawdata))
				return curhunk;
		}
		return NO_MATCH;
	}

	/* first see if the last match is a valid one */
	if (chd->comparehunk < chd->header.totalhunks && chd->map[chd->comparehunk].crc == crc && !(chd->map[chd->comparehunk].flags & MAP_ENTRY_FLAG_NO_CRC) &&
		memcmp(rawdata, chd->compare, chd->header.hunkbytes) == 0)
		return chd->comparehunk;

	/* scan through the CHD's hunk map looking for a match */
	for (curhunk = 0; curhunk < lasthunk; curhunk++)
		if (chd->map[curhunk].crc == crc && !(chd->map[curhunk].flags & MAP_ENTRY_FLAG_NO_CRC) && crcmap_verify_hunk_match(chd, curhunk, rawdata))
			return curhunk;

	return NO_MATCH;
}



/***************************************************************************
    INTERNAL METADATA ACCESS
***************************************************************************/

/*-------------------------------------------------
    metadata_find_entry - find a metadata entry
-------------------------------------------------*/

static chd_error metadata_find_entry(chd_file *chd, UINT32 metatag, UINT32 metaindex, metadata_entry *metaentry)
{
	/* start at the beginning */
	metaentry->offset = chd->header.metaoffset;
	metaentry->prev = 0;

	/* loop until we run out of options */
	while (metaentry->offset != 0)
	{
		UINT8	raw_meta_header[METADATA_HEADER_SIZE];
		UINT32	count;

		/* read the raw header */
		core_fseek(chd->file, metaentry->offset, SEEK_SET);
		count = core_fread(chd->file, raw_meta_header, sizeof(raw_meta_header));
		if (count != sizeof(raw_meta_header))
			break;

		/* extract the data */
		metaentry->metatag = get_bigendian_uint32(&raw_meta_header[0]);
		metaentry->length = get_bigendian_uint32(&raw_meta_header[4]);
		metaentry->next = get_bigendian_uint64(&raw_meta_header[8]);

		/* flags are encoded in the high byte of length */
		metaentry->flags = metaentry->length >> 24;
		metaentry->length &= 0x00ffffff;

		/* if we got a match, proceed */
		if (metatag == CHDMETATAG_WILDCARD || metaentry->metatag == metatag)
			if (metaindex-- == 0)
				return CHDERR_NONE;

		/* no match, fetch the next link */
		metaentry->prev = metaentry->offset;
		metaentry->offset = metaentry->next;
	}

	/* if we get here, we didn't find it */
	return CHDERR_METADATA_NOT_FOUND;
}


/*-------------------------------------------------
    metadata_set_previous_next - set the 'next'
    offset of a piece of metadata
-------------------------------------------------*/

static chd_error metadata_set_previous_next(chd_file *chd, UINT64 prevoffset, UINT64 nextoffset)
{
	UINT8 raw_meta_header[METADATA_HEADER_SIZE];
	chd_error err;
	UINT32 count;

	/* if we were the first entry, make the next entry the first */
	if (prevoffset == 0)
	{
		chd->header.metaoffset = nextoffset;
		err = header_write(chd->file, &chd->header);
		if (err != CHDERR_NONE)
			return err;
	}

	/* otherwise, update the link in the previous pointer */
	else
	{
		/* read the previous raw header */
		core_fseek(chd->file, prevoffset, SEEK_SET);
		count = core_fread(chd->file, raw_meta_header, sizeof(raw_meta_header));
		if (count != sizeof(raw_meta_header))
			return CHDERR_READ_ERROR;

		/* copy our next pointer into the previous->next offset */
		put_bigendian_uint64(&raw_meta_header[8], nextoffset);

		/* write the previous raw header */
		core_fseek(chd->file, prevoffset, SEEK_SET);
		count = core_fwrite(chd->file, raw_meta_header, sizeof(raw_meta_header));
		if (count != sizeof(raw_meta_header))
			return CHDERR_WRITE_ERROR;
	}

	return CHDERR_NONE;
}


/*-------------------------------------------------
    metadata_set_length - set the length field of
    a piece of metadata
-------------------------------------------------*/

static chd_error metadata_set_length(chd_file *chd, UINT64 offset, UINT32 length)
{
	UINT8 raw_meta_header[METADATA_HEADER_SIZE];
	UINT32 oldlength;
	UINT32 count;

	/* read the raw header */
	core_fseek(chd->file, offset, SEEK_SET);
	count = core_fread(chd->file, raw_meta_header, sizeof(raw_meta_header));
	if (count != sizeof(raw_meta_header))
		return CHDERR_READ_ERROR;

	/* update the length at offset 4, preserving the flags in the upper byte */
	oldlength = get_bigendian_uint32(&raw_meta_header[4]);
	length = (length & 0x00ffffff) | (oldlength & 0xff000000);
	put_bigendian_uint32(&raw_meta_header[4], length);

	/* write the raw header */
	core_fseek(chd->file, offset, SEEK_SET);
	count = core_fwrite(chd->file, raw_meta_header, sizeof(raw_meta_header));
	if (count != sizeof(raw_meta_header))
		return CHDERR_WRITE_ERROR;

	return CHDERR_NONE;
}


/*-------------------------------------------------
    metadata_compute_hash - compute the SHA1
    hash of all metadata that requests it
-------------------------------------------------*/

static chd_error metadata_compute_hash(chd_file *chd, const UINT8 *rawsha1, UINT8 *finalsha1)
{
	metadata_hash *hasharray = NULL;
	chd_error err = CHDERR_NONE;
	struct sha1_ctx sha1;
	UINT32 hashindex = 0;
	UINT32 hashalloc = 0;
	UINT64 offset, next;

	/* only works for V4 and above */
	if (chd->header.version < 4)
	{
		memcpy(finalsha1, rawsha1, SHA1_DIGEST_SIZE);
		return CHDERR_NONE;
	}

	/* loop until we run out of data */
	for (offset = chd->header.metaoffset; offset != 0; offset = next)
	{
		UINT8 raw_meta_header[METADATA_HEADER_SIZE];
		UINT32 count, metalength, metatag;
		UINT8 *tempbuffer;
		UINT8 metaflags;

		/* read the raw header */
		core_fseek(chd->file, offset, SEEK_SET);
		count = core_fread(chd->file, raw_meta_header, sizeof(raw_meta_header));
		if (count != sizeof(raw_meta_header))
			break;

		/* extract the data */
		metatag = get_bigendian_uint32(&raw_meta_header[0]);
		metalength = get_bigendian_uint32(&raw_meta_header[4]);
		next = get_bigendian_uint64(&raw_meta_header[8]);

		/* flags are encoded in the high byte of length */
		metaflags = metalength >> 24;
		metalength &= 0x00ffffff;

		/* if not checksumming, continue */
		if (!(metaflags & CHD_MDFLAGS_CHECKSUM))
			continue;

		/* allocate memory */
		tempbuffer = (UINT8 *)malloc(metalength);
		if (tempbuffer == NULL)
		{
			err = CHDERR_OUT_OF_MEMORY;
			goto cleanup;
		}

		/* seek and read the metadata */
		core_fseek(chd->file, offset + METADATA_HEADER_SIZE, SEEK_SET);
		count = core_fread(chd->file, tempbuffer, metalength);
		if (count != metalength)
		{
			free(tempbuffer);
			err = CHDERR_READ_ERROR;
			goto cleanup;
		}

		/* compute this entry's hash */
		sha1_init(&sha1);
		sha1_update(&sha1, metalength, tempbuffer);
		sha1_final(&sha1);
		free(tempbuffer);

		/* expand the hasharray if necessary */
		if (hashindex >= hashalloc)
		{
			hashalloc += 256;
			hasharray = (metadata_hash *)realloc(hasharray, hashalloc * sizeof(hasharray[0]));
			if (hasharray == NULL)
			{
				err = CHDERR_OUT_OF_MEMORY;
				goto cleanup;
			}
		}

		/* fill in the entry */
		put_bigendian_uint32(hasharray[hashindex].tag, metatag);
		sha1_digest(&sha1, SHA1_DIGEST_SIZE, hasharray[hashindex].sha1);
		hashindex++;
	}

	/* sort the array */
	qsort(hasharray, hashindex, sizeof(hasharray[0]), metadata_hash_compare);

	/* compute the SHA1 of the raw plus the various metadata */
	sha1_init(&sha1);
	sha1_update(&sha1, CHD_SHA1_BYTES, rawsha1);
	sha1_update(&sha1, hashindex * sizeof(hasharray[0]), (const UINT8 *)hasharray);
	sha1_final(&sha1);
	sha1_digest(&sha1, SHA1_DIGEST_SIZE, finalsha1);

cleanup:
	if (hasharray != NULL)
		free(hasharray);
	return err;
}


/*-------------------------------------------------
    metadata_hash_compare - compare two hash
    entries
-------------------------------------------------*/

static int CLIB_DECL metadata_hash_compare(const void *elem1, const void *elem2)
{
	return memcmp(elem1, elem2, sizeof(metadata_hash));
}



/***************************************************************************
    ZLIB COMPRESSION CODEC
***************************************************************************/

/*-------------------------------------------------
    zlib_codec_init - initialize the ZLIB codec
-------------------------------------------------*/

static chd_error zlib_codec_init(chd_file *chd)
{
	zlib_codec_data *data;
	chd_error err;
	int zerr;

	/* allocate memory for the 2 stream buffers */
	data = (zlib_codec_data *)malloc(sizeof(*data));
	if (data == NULL)
		return CHDERR_OUT_OF_MEMORY;

	/* clear the buffers */
	memset(data, 0, sizeof(*data));

	/* init the inflater first */
	data->inflater.next_in = (Bytef *)data;	/* bogus, but that's ok */
	data->inflater.avail_in = 0;
	data->inflater.zalloc = zlib_fast_alloc;
	data->inflater.zfree = zlib_fast_free;
	data->inflater.opaque = data;
	zerr = inflateInit2(&data->inflater, -MAX_WBITS);

	/* if that worked, initialize the deflater */
	if (zerr == Z_OK)
	{
		data->deflater.next_in = (Bytef *)data;	/* bogus, but that's ok */
		data->deflater.avail_in = 0;
		data->deflater.zalloc = zlib_fast_alloc;
		data->deflater.zfree = zlib_fast_free;
		data->deflater.opaque = data;
		zerr = deflateInit2(&data->deflater, Z_BEST_COMPRESSION, Z_DEFLATED, -MAX_WBITS, 8, Z_DEFAULT_STRATEGY);
	}

	/* convert errors */
	if (zerr == Z_MEM_ERROR)
		err = CHDERR_OUT_OF_MEMORY;
	else if (zerr != Z_OK)
		err = CHDERR_CODEC_ERROR;
	else
		err = CHDERR_NONE;

	/* handle an error */
	if (err == CHDERR_NONE)
		chd->codecdata = data;
	else
		free(data);

	return err;
}


/*-------------------------------------------------
    zlib_codec_free - free data for the ZLIB
    codec
-------------------------------------------------*/

static void zlib_codec_free(chd_file *chd)
{
	zlib_codec_data *data = (zlib_codec_data *)chd->codecdata;

	/* deinit the streams */
	if (data != NULL)
	{
		int i;

		inflateEnd(&data->inflater);
		deflateEnd(&data->deflater);

		/* free our fast memory */
		for (i = 0; i < MAX_ZLIB_ALLOCS; i++)
			if (data->allocptr[i])
				free(data->allocptr[i]);
		free(data);
	}
}


/*-------------------------------------------------
    zlib_codec_compress - compress data using the
    ZLIB codec
-------------------------------------------------*/

static chd_error zlib_codec_compress(chd_file *chd, const void *src, UINT32 *length)
{
	zlib_codec_data *data = (zlib_codec_data *)chd->codecdata;
	int zerr;

	/* reset the decompressor */
	data->deflater.next_in = (Bytef *)src;
	data->deflater.avail_in = chd->header.hunkbytes;
	data->deflater.total_in = 0;
	data->deflater.next_out = chd->compressed;
	data->deflater.avail_out = chd->header.hunkbytes;
	data->deflater.total_out = 0;
	zerr = deflateReset(&data->deflater);
	if (zerr != Z_OK)
		return CHDERR_COMPRESSION_ERROR;

	/* do it */
	zerr = deflate(&data->deflater, Z_FINISH);

	/* if we ended up with more data than we started with, return an error */
	if (zerr != Z_STREAM_END || data->deflater.total_out >= chd->header.hunkbytes)
		return CHDERR_COMPRESSION_ERROR;

	/* otherwise, fill in the length and return success */
	*length = data->deflater.total_out;
	return CHDERR_NONE;
}


/*-------------------------------------------------
    zlib_codec_decompress - decomrpess data using
    the ZLIB codec
-------------------------------------------------*/

static chd_error zlib_codec_decompress(chd_file *chd, UINT32 srclength, void *dest)
{
	zlib_codec_data *data = (zlib_codec_data *)chd->codecdata;
	int zerr;

	/* reset the decompressor */
	data->inflater.next_in = chd->compressed;
	data->inflater.avail_in = srclength;
	data->inflater.total_in = 0;
	data->inflater.next_out = (Bytef *)dest;
	data->inflater.avail_out = chd->header.hunkbytes;
	data->inflater.total_out = 0;
	zerr = inflateReset(&data->inflater);
	if (zerr != Z_OK)
		return CHDERR_DECOMPRESSION_ERROR;

	/* do it */
	zerr = inflate(&data->inflater, Z_FINISH);
	if (data->inflater.total_out != chd->header.hunkbytes)
		return CHDERR_DECOMPRESSION_ERROR;

	return CHDERR_NONE;
}


/*-------------------------------------------------
    zlib_fast_alloc - fast malloc for ZLIB, which
    allocates and frees memory frequently
-------------------------------------------------*/

static voidpf zlib_fast_alloc(voidpf opaque, uInt items, uInt size)
{
	zlib_codec_data *data = (zlib_codec_data *)opaque;
	UINT32 *ptr;
	int i;

	/* compute the size, rounding to the nearest 1k */
	size = (size * items + 0x3ff) & ~0x3ff;

	/* reuse a hunk if we can */
	for (i = 0; i < MAX_ZLIB_ALLOCS; i++)
	{
		ptr = data->allocptr[i];
		if (ptr && size == *ptr)
		{
			/* set the low bit of the size so we don't match next time */
			*ptr |= 1;
			return ptr + 1;
		}
	}

	/* alloc a new one */
	ptr = (UINT32 *)malloc(size + sizeof(UINT32));
	if (!ptr)
		return NULL;

	/* put it into the list */
	for (i = 0; i < MAX_ZLIB_ALLOCS; i++)
		if (!data->allocptr[i])
		{
			data->allocptr[i] = ptr;
			break;
		}

	/* set the low bit of the size so we don't match next time */
	*ptr = size | 1;
	return ptr + 1;
}


/*-------------------------------------------------
    zlib_fast_free - fast free for ZLIB, which
    allocates and frees memory frequently
-------------------------------------------------*/

static void zlib_fast_free(voidpf opaque, voidpf address)
{
	zlib_codec_data *data = (zlib_codec_data *)opaque;
	UINT32 *ptr = (UINT32 *)address - 1;
	int i;

	/* find the hunk */
	for (i = 0; i < MAX_ZLIB_ALLOCS; i++)
		if (ptr == data->allocptr[i])
		{
			/* clear the low bit of the size to allow matches */
			*ptr &= ~1;
			return;
		}
}



/***************************************************************************
    AV COMPRESSION CODEC
***************************************************************************/

/*-------------------------------------------------
    av_raw_data_size - compute the raw data size
-------------------------------------------------*/

INLINE UINT32 av_raw_data_size(const UINT8 *data)
{
	int size = 0;

	/* make sure we have a correct header */
	if (data[0] == 'c' && data[1] == 'h' && data[2] == 'a' && data[3] == 'v')
	{
		/* add in header size plus metadata length */
		size = 12 + data[4];

		/* add in channels * samples */
		size += 2 * data[5] * ((data[6] << 8) + data[7]);

		/* add in 2 * width * height */
		size += 2 * ((data[8] << 8) + data[9]) * (((data[10] << 8) + data[11]) & 0x7fff);
	}
	return size;
}


/*-------------------------------------------------
    av_codec_init - initialize the A/V codec
-------------------------------------------------*/

static chd_error av_codec_init(chd_file *chd)
{
	av_codec_data *data;

	/* allocate memory for the 2 stream buffers */
	data = (av_codec_data *)malloc(sizeof(*data));
	if (data == NULL)
		return CHDERR_OUT_OF_MEMORY;

	/* clear the buffers */
	memset(data, 0, sizeof(*data));
	chd->codecdata = data;

	/* attempt to do a post-init now; if we're creating a new CHD, this won't work */
	/* but that's ok */
	av_codec_postinit(chd);
	return CHDERR_NONE;
}


/*-------------------------------------------------
    av_codec_free - free data for the A/V
    codec
-------------------------------------------------*/

static void av_codec_free(chd_file *chd)
{
	av_codec_data *data = (av_codec_data *)chd->codecdata;

	/* deinit avcomp */
	if (data != NULL)
	{
		if (data->compstate != NULL)
			avcomp_free(data->compstate);
		free(data);
	}
}


/*-------------------------------------------------
    av_codec_compress - compress data using the
    A/V codec
-------------------------------------------------*/

static chd_error av_codec_compress(chd_file *chd, const void *src, UINT32 *length)
{
	av_codec_data *data = (av_codec_data *)chd->codecdata;
	int averr;
	int size;

	/* if we haven't yet set up the avcomp code, do it now */
	if (data->compstate == NULL)
	{
		chd_error chderr = av_codec_postinit(chd);
		if (chderr != CHDERR_NONE)
			return chderr;
	}

	/* make sure short frames are padded with 0 */
	if (src != NULL)
	{
		size = av_raw_data_size((const UINT8 *)src);
		while (size < chd->header.hunkbytes)
			if (((const UINT8 *)src)[size++] != 0)
				return CHDERR_INVALID_DATA;
	}

	/* encode the audio and video */
	averr = avcomp_encode_data(data->compstate, (const UINT8 *)src, chd->compressed, length);
	if (averr != AVCERR_NONE || *length > chd->header.hunkbytes)
		return CHDERR_COMPRESSION_ERROR;

	return CHDERR_NONE;
}


/*-------------------------------------------------
    av_codec_decompress - decomrpess data using
    the A/V codec
-------------------------------------------------*/

static chd_error av_codec_decompress(chd_file *chd, UINT32 srclength, void *dest)
{
	av_codec_data *data = (av_codec_data *)chd->codecdata;
	const UINT8 *source;
	avcomp_error averr;
	int size;

	/* if we haven't yet set up the avcomp code, do it now */
	if (data->compstate == NULL)
	{
		chd_error chderr = av_codec_postinit(chd);
		if (chderr != CHDERR_NONE)
			return chderr;
	}

	/* decode the audio and video */
	source = chd->compressed;
	averr = avcomp_decode_data(data->compstate, source, srclength, (UINT8 *)dest);
	if (averr != AVCERR_NONE)
		return CHDERR_DECOMPRESSION_ERROR;

	/* pad short frames with 0 */
	if (dest != NULL)
	{
		size = av_raw_data_size((const UINT8 *)dest);
		while (size < chd->header.hunkbytes)
			((UINT8 *)dest)[size++] = 0;
	}

	return CHDERR_NONE;
}


/*-------------------------------------------------
    av_codec_config - codec-specific configuration
    for the A/V codec
-------------------------------------------------*/

static chd_error av_codec_config(chd_file *chd, int param, void *config)
{
	av_codec_data *data = (av_codec_data *)chd->codecdata;

	/* if we're getting the compression configuration, apply it now */
	if (param == AV_CODEC_COMPRESS_CONFIG)
	{
		data->compress = *(av_codec_compress_config *)config;
		if (data->compstate != NULL)
			avcomp_config_compress(data->compstate, &data->compress);
		return CHDERR_NONE;
	}

	/* if we're getting the decompression configuration, apply it now */
	else if (param == AV_CODEC_DECOMPRESS_CONFIG)
	{
		data->decompress = *(av_codec_decompress_config *)config;
		if (data->compstate != NULL)
			avcomp_config_decompress(data->compstate, &data->decompress);
		return CHDERR_NONE;
	}

	/* anything else is invalid */
	return CHDERR_INVALID_PARAMETER;
}


/*-------------------------------------------------
    av_codec_postinit - actual initialization of
    avcomp happens here, on the first attempt
    to compress or decompress data
-------------------------------------------------*/

static chd_error av_codec_postinit(chd_file *chd)
{
	int fps, fpsfrac, width, height, interlaced, channels, rate;
	UINT32 fps_times_1million, max_samples_per_frame, bytes_per_frame;
	av_codec_data *data = (av_codec_data *)chd->codecdata;
	char metadata[256];
	chd_error err;

	/* the code below won't work asynchronously */
	if (chd->workitem != NULL)
		return CHDERR_OPERATION_PENDING;

	/* get the metadata */
	err = chd_get_metadata(chd, AV_METADATA_TAG, 0, metadata, sizeof(metadata), NULL, NULL, NULL);
	if (err != CHDERR_NONE)
		return err;

	/* extract the info */
	if (sscanf(metadata, AV_METADATA_FORMAT, &fps, &fpsfrac, &width, &height, &interlaced, &channels, &rate) != 7)
		return CHDERR_INVALID_METADATA;

	/* compute the bytes per frame */
	fps_times_1million = fps * 1000000 + fpsfrac;
	max_samples_per_frame = ((UINT64)rate * 1000000 + fps_times_1million - 1) / fps_times_1million;
	bytes_per_frame = 12 + channels * max_samples_per_frame * 2 + width * height * 2;
	if (bytes_per_frame > chd->header.hunkbytes)
		return CHDERR_INVALID_METADATA;

	/* create the avcomp state */
	data->compstate = avcomp_init(width, height, channels);

	/* configure the codec */
	avcomp_config_compress(data->compstate, &data->compress);
	avcomp_config_decompress(data->compstate, &data->decompress);
	return CHDERR_NONE;
}
