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

    MAME Compressed Hunks of Data file format

    Copyright (c) 1996-2006, Nicola Salmoria and the MAME Team.
    Visit http://mamedev.org for licensing and usage restrictions.

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

#include "chd.h"
#include "md5.h"
#include "sha1.h"
#include <zlib.h>
#include <time.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	0x000f		/* what type of hunk */
#define MAP_ENTRY_FLAG_NO_CRC		0x0010		/* no CRC is present */

#define MAP_ENTRY_TYPE_INVALID		0x0000		/* invalid type */
#define MAP_ENTRY_TYPE_COMPRESSED	0x0001		/* standard compression */
#define MAP_ENTRY_TYPE_UNCOMPRESSED	0x0002		/* uncompressed data */
#define MAP_ENTRY_TYPE_MINI			0x0003		/* mini: use offset as raw data */
#define MAP_ENTRY_TYPE_SELF_HUNK	0x0004		/* same as another hunk in this file */
#define MAP_ENTRY_TYPE_PARENT_HUNK	0x0005		/* 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 SET_ERROR_AND_CLEANUP(err) do { last_error = (err); goto cleanup; } while (0)



/*************************************
 *
 *  Type definitions
 *
 *************************************/

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


struct _crcmap_entry
{
	UINT32					hunknum;		/* hunk number */
	struct _crcmap_entry *	next;			/* next entry in list */
};
typedef struct _crcmap_entry crcmap_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 */
};
typedef struct _metadata_entry metadata_entry;


struct _chd_file
{
	UINT32					cookie;			/* cookie, should equal COOKIE_VALUE */
	struct _chd_file *		next;			/* pointer to next file in the global list */

	chd_interface_file *	file;		/* handle to the open file */
	chd_header 				header;			/* header, extracted from file */

	struct _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 */
	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 */
};


struct _chd_exfile
{
	chd_file *chd;
	struct MD5Context md5;
	struct sha1_ctx sha;
	int hunknum;
	UINT64 sourceoffset;
};


struct _zlib_codec_data
{
	z_stream				inflater;
	z_stream				deflater;
	UINT32 *				allocptr[MAX_ZLIB_ALLOCS];
};
typedef struct _zlib_codec_data zlib_codec_data;



/*************************************
 *
 *  Local variables
 *
 *************************************/

static chd_interface cur_interface;
static chd_file *first_file;
static int last_error;

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



/*************************************
 *
 *  Prototypes
 *
 *************************************/

static int validate_header(const chd_header *header);
static int read_hunk_into_memory(chd_file *chd, UINT32 hunknum, UINT8 *dest);
static int read_hunk_into_cache(chd_file *chd, UINT32 hunknum);
static int write_hunk_from_memory(chd_file *chd, UINT32 hunknum, const UINT8 *src);
static int read_header(chd_interface_file *file, chd_header *header);
static int write_header(chd_interface_file *file, const chd_header *header);
static int read_hunk_map(chd_file *chd);
static void init_crcmap(chd_file *chd, int prepopulate);
static void add_to_crcmap(chd_file *chd, UINT32 hunknum);
static UINT32 find_matching_hunk(chd_file *chd, UINT32 hunknum, UINT32 crc, const UINT8 *rawdata);
static int find_metadata_entry(chd_file *chd, UINT32 metatag, UINT32 metaindex, metadata_entry *metaentry);

static int init_codec(chd_file *chd);
static void free_codec(chd_file *chd);

static chd_interface_file *multi_open(const char *filename, const char *mode);
static void multi_close(chd_interface_file *file);
static UINT32 multi_read(chd_interface_file *file, UINT64 offset, UINT32 count, void *buffer);
static UINT32 multi_write(chd_interface_file *file, UINT64 offset, UINT32 count, const void *buffer);
static UINT64 multi_length(chd_interface_file *file);



/*************************************
 *
 *  Inline helpers
 *
 *************************************/

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];
}

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;
}

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

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

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

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

INLINE void extract_map_entry(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]);
	entry->flags = get_bigendian_uint16(&base[14]);
}

INLINE void assemble_map_entry(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);
	put_bigendian_uint16(&base[14], entry->flags);
}

INLINE void extract_old_map_entry(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
}

INLINE void assemble_old_map_entry(UINT8 *base, map_entry *entry)
{
	UINT64 data = entry->offset | ((UINT64)entry->length << 44);
	put_bigendian_uint64(&base[0], data);
}



/*************************************
 *
 *  Interface setup
 *
 *************************************/

void chd_set_interface(chd_interface *new_interface)
{
	if (new_interface)
		cur_interface = *new_interface;
	else
		memset(&cur_interface, 0, sizeof(cur_interface));
}



/*************************************
 *
 *  Interface save
 *
 *************************************/

void chd_save_interface(chd_interface *interface_save)
{
	*interface_save = cur_interface;
}



/*************************************
 *
 *  Create a new data file
 *
 *************************************/

int chd_create(const char *filename, UINT64 logicalbytes, UINT32 hunkbytes, UINT32 compression, chd_file *parent)
{
	UINT8 blank_map_entries[MAP_STACK_ENTRIES * MAP_ENTRY_SIZE];
	int fullchunks, remainder, count;
	chd_interface_file *file = NULL;
	map_entry mapentry;
	chd_header header;
	UINT64 fileoffset;
	int i, j, err;

	last_error = CHDERR_NONE;

	/* punt if no interface */
	if (!cur_interface.open)
		SET_ERROR_AND_CLEANUP(CHDERR_NO_INTERFACE);

	/* verify parameters */
	if (!filename)
		SET_ERROR_AND_CLEANUP(CHDERR_FILE_NOT_FOUND);
	if (compression >= CHDCOMPRESSION_MAX)
		SET_ERROR_AND_CLEANUP(CHDERR_INVALID_PARAMETER);
	if (!parent && (logicalbytes == 0 || hunkbytes == 0))
		SET_ERROR_AND_CLEANUP(CHDERR_INVALID_PARAMETER);

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

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

	/* build the header */
	header.length = CHD_V3_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;
	header.metaoffset = 0;
	memset(&header.md5[0], 0, sizeof(header.md5));
	memset(&header.parentmd5[0], 0, sizeof(header.parentmd5));
	memset(&header.sha1[0], 0, sizeof(header.sha1));
	memset(&header.parentsha1[0], 0, sizeof(header.parentsha1));
	header.obsolete_cylinders = 0;
	header.obsolete_sectors = 0;
	header.obsolete_heads = 0;
	header.obsolete_hunksize = 0;

	/* tweaks if there is a parent */
	if (parent)
	{
		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 = validate_header(&header);
	if (err != CHDERR_NONE)
		SET_ERROR_AND_CLEANUP(err);

	/* attempt to create the file */
	file = multi_open(filename, "wb");
	if (!file)
		SET_ERROR_AND_CLEANUP(CHDERR_CANT_CREATE_FILE);

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

	/* 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++)
		assemble_map_entry(&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)
			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;
				assemble_map_entry(&blank_map_entries[j * MAP_ENTRY_SIZE], &mapentry);
			}

		/* write the chunks */
		count = multi_write(file, fileoffset, sizeof(blank_map_entries), blank_map_entries);
		if (count != sizeof(blank_map_entries))
			SET_ERROR_AND_CLEANUP(CHDERR_WRITE_ERROR);
		fileoffset += sizeof(blank_map_entries);
	}

	/* then write the remainder */
	if (remainder)
	{
		/* parent drives need to be mapped through */
		if (parent)
			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;
				assemble_map_entry(&blank_map_entries[j * MAP_ENTRY_SIZE], &mapentry);
			}

		/* write the chunks */
		count = multi_write(file, fileoffset, remainder * MAP_ENTRY_SIZE, blank_map_entries);
		if (count != remainder * MAP_ENTRY_SIZE)
			SET_ERROR_AND_CLEANUP(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);
	count = multi_write(file, fileoffset, MAP_ENTRY_SIZE, blank_map_entries);
	if (count != MAP_ENTRY_SIZE)
		SET_ERROR_AND_CLEANUP(CHDERR_WRITE_ERROR);

	/* all done */
	multi_close(file);

	/* if we have a parent, clone the metadata */
	if (parent)
	{
		UINT8 metadata[CHD_MAX_METADATA_SIZE];
		UINT32 metatag, metasize, metaindex;

		/* open the new CHD */
		chd_file *newchd = chd_open(filename, 1, parent);
		if (newchd == NULL)
			SET_ERROR_AND_CLEANUP(last_error);

		/* clone the metadata */
		for (metaindex = 0; ; metaindex++)
		{
			metatag = CHDMETATAG_WILDCARD;
			metasize = chd_get_metadata(parent, &metatag, metaindex, metadata, sizeof(metadata));
			if (metasize == 0 || chd_get_last_error() == CHDERR_METADATA_NOT_FOUND)
				break;

			err = chd_set_metadata(newchd, metatag, CHD_METAINDEX_APPEND, metadata, metasize);
			if (err != CHDERR_NONE)
				SET_ERROR_AND_CLEANUP(err);
		}

		/* close the file */
		chd_close(newchd);
	}
	return CHDERR_NONE;

cleanup:
	if (file)
		multi_close(file);
	return last_error;
}



/*************************************
 *
 *  Opening a data file
 *
 *************************************/

chd_file *chd_open(const char *filename, int writeable, chd_file *parent)
{
	chd_file *finalchd;
	chd_file chd = { 0 };
	int err;

	last_error = CHDERR_NONE;

	/* punt if no interface */
	if (!cur_interface.open)
		SET_ERROR_AND_CLEANUP(CHDERR_NO_INTERFACE);

	/* verify parameters */
	if (!filename)
		SET_ERROR_AND_CLEANUP(CHDERR_FILE_NOT_FOUND);

	/* punt if invalid parent */
	chd.parent = parent;
	if (chd.parent && chd.parent->cookie != COOKIE_VALUE)
		SET_ERROR_AND_CLEANUP(CHDERR_INVALID_PARAMETER);

	/* first attempt to open the file */
	chd.file = multi_open(filename, writeable ? "rb+" : "rb");
	if (!chd.file)
		SET_ERROR_AND_CLEANUP(CHDERR_FILE_NOT_FOUND);

	/* now attempt to read the header */
	err = read_header(chd.file, &chd.header);
	if (err != CHDERR_NONE)
		SET_ERROR_AND_CLEANUP(err);

	/* validate the header */
	err = validate_header(&chd.header);
	if (err != CHDERR_NONE)
		SET_ERROR_AND_CLEANUP(err);

	/* make sure we don't open a read-only file writeable */
	if (writeable && !(chd.header.flags & CHDFLAGS_IS_WRITEABLE))
		SET_ERROR_AND_CLEANUP(CHDERR_FILE_NOT_WRITEABLE);

	/* also, never open an older version writeable */
	if (writeable && chd.header.version < CHD_HEADER_VERSION)
		SET_ERROR_AND_CLEANUP(CHDERR_UNSUPPORTED_VERSION);

	/* if we need a parent, make sure we have one */
	if (!parent && (chd.header.flags & CHDFLAGS_HAS_PARENT))
		SET_ERROR_AND_CLEANUP(CHDERR_REQUIRES_PARENT);

	/* make sure we have a valid parent */
	if (parent)
	{
		/* check MD5 if it isn't empty */
		if (memcmp(nullmd5, chd.header.parentmd5, sizeof(chd.header.parentmd5)) &&
			memcmp(nullmd5, chd.parent->header.md5, sizeof(chd.parent->header.md5)) &&
			memcmp(chd.parent->header.md5, chd.header.parentmd5, sizeof(chd.header.parentmd5)))
			SET_ERROR_AND_CLEANUP(CHDERR_INVALID_PARENT);

		/* check SHA1 if it isn't empty */
		if (memcmp(nullsha1, chd.header.parentsha1, sizeof(chd.header.parentsha1)) &&
			memcmp(nullsha1, chd.parent->header.sha1, sizeof(chd.parent->header.sha1)) &&
			memcmp(chd.parent->header.sha1, chd.header.parentsha1, sizeof(chd.header.parentsha1)))
			SET_ERROR_AND_CLEANUP(CHDERR_INVALID_PARENT);
	}

	/* now read the hunk map */
	err = read_hunk_map(&chd);
	if (err != CHDERR_NONE)
		SET_ERROR_AND_CLEANUP(err);

	/* allocate and init the hunk cache */
	chd.cache = malloc(chd.header.hunkbytes);
	chd.compare = malloc(chd.header.hunkbytes);
	if (!chd.cache || !chd.compare)
		SET_ERROR_AND_CLEANUP(CHDERR_OUT_OF_MEMORY);
	chd.cachehunk = ~0;
	chd.comparehunk = ~0;

	/* allocate the temporary compressed buffer */
	chd.compressed = malloc(chd.header.hunkbytes);
	if (!chd.compressed)
		SET_ERROR_AND_CLEANUP(CHDERR_OUT_OF_MEMORY);

	/* now init the codec */
	err = init_codec(&chd);
	if (err != CHDERR_NONE)
		SET_ERROR_AND_CLEANUP(err);

	/* okay, now allocate our entry and copy it */
	finalchd = malloc(sizeof(chd));
	if (!finalchd)
		SET_ERROR_AND_CLEANUP(CHDERR_OUT_OF_MEMORY);
	*finalchd = chd;

	/* hook us into the global list */
	finalchd->cookie = COOKIE_VALUE;
	finalchd->next = first_file;
	first_file = finalchd;

	/* all done */
	return finalchd;

cleanup:
	if (chd.codecdata)
		free_codec(&chd);
	if (chd.compressed)
		free(chd.compressed);
	if (chd.compare)
		free(chd.compare);
	if (chd.cache)
		free(chd.cache);
	if (chd.map)
		free(chd.map);
	if (chd.file)
		multi_close(chd.file);
	return NULL;
}



/*************************************
 *
 *  Closing a data file
 *
 *************************************/

void chd_close(chd_file *chd)
{
	chd_file *curr, *prev;

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

	/* deinit the codec */
	if (chd->codecdata)
		free_codec(chd);

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

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

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

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

	/* close the file */
	if (chd->file)
		multi_close(chd->file);

	/* unlink ourselves */
	for (prev = NULL, curr = first_file; curr; prev = curr, curr = curr->next)
		if (curr == chd)
		{
			if (prev)
				prev->next = curr->next;
			else
				first_file = curr->next;
			break;
		}

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

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



/*************************************
 *
 *  Closing all open data files
 *
 *************************************/

void chd_close_all(void)
{
	while (first_file)
		chd_close(first_file);
}



/*************************************
 *
 *  Read metadata from a data file
 *
 *************************************/

UINT32 chd_get_metadata(chd_file *chd, UINT32 *metatag, UINT32 metaindex, void *outputbuf, UINT32 outputlen)
{
	metadata_entry metaentry;
	UINT32 count;

	/* if we didn't find it, just return */
	last_error = find_metadata_entry(chd, *metatag, metaindex, &metaentry);
	if (last_error != CHDERR_NONE)
	{
		/* unless we're an old version and they are requesting hard disk metadata */
		if (chd->header.version < 3 && (*metatag == HARD_DISK_STANDARD_METADATA || *metatag == CHDMETATAG_WILDCARD) && metaindex == 0)
		{
			/* fill in the faux metadata */
			char		faux_metadata[256];
			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);

			/* fake it */
			metaentry.length = strlen(faux_metadata) + 1;
			if (outputlen > metaentry.length)
				outputlen = metaentry.length;
			memcpy(outputbuf, faux_metadata, outputlen);

			/* return the length of the data and the tag */
			*metatag = HARD_DISK_STANDARD_METADATA;
			last_error = CHDERR_NONE;
			return metaentry.length;
		}
		return 0;
	}

	/* clamp to the maximum requested size */
	if (outputlen > metaentry.length)
		outputlen = metaentry.length;

	/* read the metadata */
	count = multi_read(chd->file, metaentry.offset + METADATA_HEADER_SIZE, outputlen, outputbuf);
	if (count != outputlen)
		return count;

	/* return the length of the data and the tag */
	*metatag = metaentry.metatag;
	return metaentry.length;
}



/*************************************
 *
 *  Write metadata to a data file
 *
 *************************************/

int chd_set_metadata(chd_file *chd, UINT32 metatag, UINT32 metaindex, const void *inputbuf, UINT32 inputlen)
{
	UINT8 raw_meta_header[METADATA_HEADER_SIZE];
	metadata_entry metaentry = { 0 };
	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 be at least 1 byte */
	if (inputlen < 1 || inputlen > CHD_MAX_METADATA_SIZE)
		return CHDERR_INVALID_METADATA_SIZE;

	/* if the entry fits within the previous entry, just overwrite it */
	last_error = (metaindex != CHD_METAINDEX_APPEND) ? find_metadata_entry(chd, metatag, metaindex, &metaentry) : CHDERR_METADATA_NOT_FOUND;
	if (last_error == CHDERR_NONE && inputlen <= metaentry.length)
	{
		count = multi_write(chd->file, metaentry.offset + METADATA_HEADER_SIZE, inputlen, inputbuf);
		if (count != inputlen)
			return last_error = CHDERR_WRITE_ERROR;

		/* if the lengths don't match, we need to update the length in our header */
		if (inputlen != metaentry.length)
		{
			count = multi_read(chd->file, metaentry.offset, sizeof(raw_meta_header), raw_meta_header);
			if (count != sizeof(raw_meta_header))
				return last_error = CHDERR_READ_ERROR;

			put_bigendian_uint32(&raw_meta_header[4], inputlen);
			count = multi_write(chd->file, metaentry.offset, sizeof(raw_meta_header), raw_meta_header);
			if (count != sizeof(raw_meta_header))
				return last_error = CHDERR_WRITE_ERROR;
		}
	}

	/* otherwise, we need to append an entry */
	else
	{
		/* if we already had an entry, unlink it */
		if (last_error == CHDERR_NONE)
		{
			/* if we were the first entry, make the next entry the first */
			if (metaentry.prev == 0)
			{
				chd->header.metaoffset = metaentry.next;
				last_error = write_header(chd->file, &chd->header);
				if (last_error != CHDERR_NONE)
					return last_error;
			}

			/* otherwise, update the link in the previous pointer */
			else
			{
				count = multi_read(chd->file, metaentry.prev, sizeof(raw_meta_header), raw_meta_header);
				if (count != sizeof(raw_meta_header))
					return last_error = CHDERR_READ_ERROR;

				put_bigendian_uint64(&raw_meta_header[8], metaentry.next);
				count = multi_write(chd->file, metaentry.prev, sizeof(raw_meta_header), raw_meta_header);
				if (count != sizeof(raw_meta_header))
					return last_error = CHDERR_WRITE_ERROR;
			}
		}

		/* now build us a new entry */
		put_bigendian_uint32(&raw_meta_header[0], metatag);
		put_bigendian_uint32(&raw_meta_header[4], inputlen);
		put_bigendian_uint64(&raw_meta_header[8], chd->header.metaoffset);

		/* write out the new header */
		metaentry.offset = multi_length(chd->file);
		count = multi_write(chd->file, metaentry.offset, sizeof(raw_meta_header), raw_meta_header);
		if (count != sizeof(raw_meta_header))
			return last_error = CHDERR_WRITE_ERROR;

		/* follow that with the data */
		count = multi_write(chd->file, metaentry.offset + METADATA_HEADER_SIZE, inputlen, inputbuf);
		if (count != inputlen)
			return last_error = CHDERR_WRITE_ERROR;

		/* finally, update the header */
		chd->header.metaoffset = metaentry.offset;
		last_error = write_header(chd->file, &chd->header);
		if (last_error != CHDERR_NONE)
			return last_error;
	}

	return CHDERR_NONE;
}



/*************************************
 *
 *  Reading from a data file
 *
 *************************************/

UINT32 chd_read(chd_file *chd, UINT32 hunknum, UINT32 hunkcount, void *buffer)
{
	int err;

	last_error = CHDERR_NONE;

	/* for now, just break down multihunk reads into single hunks */
	if (hunkcount > 1)
	{
		UINT32 total = 0;
		while (hunkcount-- && last_error == CHDERR_NONE)
			total += chd_read(chd, hunknum++, 1, (UINT8 *)buffer + total * chd->header.hunkbytes);
		return total;
	}

	/* punt if NULL or invalid */
	if (!chd || chd->cookie != COOKIE_VALUE)
		SET_ERROR_AND_CLEANUP(CHDERR_INVALID_PARAMETER);

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

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

	/* if the hunk is not cached, load and decompress it */
	if (chd->cachehunk != hunknum)
	{
		err = read_hunk_into_cache(chd, hunknum);
		if (err != CHDERR_NONE)
			SET_ERROR_AND_CLEANUP(err);
	}

	/* now copy the data from the cache */
	memcpy(buffer, chd->cache, chd->header.hunkbytes);
	return 1;

cleanup:
	return 0;
}



/*************************************
 *
 *  Writing to a data file
 *
 *************************************/

UINT32 chd_write(chd_file *chd, UINT32 hunknum, UINT32 hunkcount, const void *buffer)
{
	int err;

	last_error = CHDERR_NONE;

	/* for now, just break down multihunk writes into single hunks */
	if (hunkcount > 1)
	{
		UINT32 total = 0;
		while (hunkcount-- && last_error == CHDERR_NONE)
			total += chd_write(chd, hunknum++, 1, (const UINT8 *)buffer + total * chd->header.hunkbytes);
		return total;
	}

	/* punt if NULL or invalid */
	if (!chd || chd->cookie != COOKIE_VALUE)
		SET_ERROR_AND_CLEANUP(CHDERR_INVALID_PARAMETER);

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

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

	/* then write out the hunk */
	err = write_hunk_from_memory(chd, hunknum, buffer);
	if (err != CHDERR_NONE)
		SET_ERROR_AND_CLEANUP(err);
	return 1;

cleanup:
	return 0;
}



/*************************************
 *
 *  Return last error
 *
 *************************************/

int chd_get_last_error(void)
{
	return last_error;
}



/*************************************
 *
 *  Return pointer to header
 *
 *************************************/

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

	return &chd->header;

cleanup:
	return NULL;
}



/*************************************
 *
 *  Set the header
 *
 *************************************/

int chd_set_header(const char *filename, const chd_header *header)
{
	chd_interface_file *file = NULL;
	chd_header oldheader;
	int err;

	/* punt if no interface */
	if (!cur_interface.open)
		SET_ERROR_AND_CLEANUP(CHDERR_NO_INTERFACE);

	/* punt if NULL or invalid */
	if (!filename || !header)
		SET_ERROR_AND_CLEANUP(CHDERR_INVALID_PARAMETER);

	/* validate the header */
	err = validate_header(header);
	if (err != CHDERR_NONE)
		SET_ERROR_AND_CLEANUP(err);

	/* attempt to open the file */
	file = multi_open(filename, "rb+");
	if (!file)
		SET_ERROR_AND_CLEANUP(CHDERR_FILE_NOT_FOUND);

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

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

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

	/* close the file and return */
	multi_close(file);
	return CHDERR_NONE;

cleanup:
	if (file)
		multi_close(file);
	return last_error;
}



/*************************************
 *
 *  All-in-one file compressor
 *
 *************************************/

int chd_compress(chd_file *chd, const char *rawfile, UINT32 offset, void (*progress)(const char *, ...))
{
	chd_interface_file *sourcefile = NULL;
	UINT64 sourceoffset = 0;
	struct MD5Context md5;
	struct sha1_ctx sha;
	clock_t lastupdate;
	int err, hunknum;

	/* punt if no interface */
	if (!cur_interface.open)
		SET_ERROR_AND_CLEANUP(CHDERR_NO_INTERFACE);

	/* verify parameters */
	if (!chd || !rawfile)
		SET_ERROR_AND_CLEANUP(CHDERR_INVALID_PARAMETER);

	/* open the raw file */
	sourcefile = multi_open(rawfile, "rb");
	if (!sourcefile)
		SET_ERROR_AND_CLEANUP(CHDERR_FILE_NOT_FOUND);

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

	/* create CRC maps for the new CHD and the parent */
	init_crcmap(chd, 0);
	if (chd->parent)
		init_crcmap(chd->parent, 1);

	/* init the MD5/SHA1 computations */
	MD5Init(&md5);
	sha1_init(&sha);

	/* loop over source hunks until we run out */
	lastupdate = 0;
	for (hunknum = 0; hunknum < chd->header.totalhunks; hunknum++)
	{
		clock_t curtime = clock();
		UINT32 bytestochecksum;
		UINT32 bytesread;

		/* read the data */
		bytesread = multi_read(sourcefile, sourceoffset + offset, chd->header.hunkbytes, chd->cache);
		if (bytesread < chd->header.hunkbytes)
			memset(&chd->cache[bytesread], 0, chd->header.hunkbytes - bytesread);

		/* progress */
		if (curtime - lastupdate > CLOCKS_PER_SEC / 2)
		{
			UINT64 sourcepos = (UINT64)hunknum * chd->header.hunkbytes;
			if (progress && sourcepos)
				(*progress)("Compressing hunk %d/%d... (ratio=%d%%)  \r", hunknum, chd->header.totalhunks, 100 - multi_length(chd->file) * 100 / sourcepos);
			lastupdate = curtime;
		}

		/* 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)
		{
			MD5Update(&md5, chd->cache, bytestochecksum);
			sha1_update(&sha, bytestochecksum, chd->cache);
		}

		/* write out the hunk */
		err = write_hunk_from_memory(chd, hunknum, chd->cache);
		if (err != CHDERR_NONE)
			SET_ERROR_AND_CLEANUP(err);

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

		/* prepare for the next hunk */
		sourceoffset += chd->header.hunkbytes;
	}

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

	/* turn off the writeable flag and re-write the header */
	chd->header.flags &= ~CHDFLAGS_IS_WRITEABLE;
	err = write_header(chd->file, &chd->header);
	if (err != CHDERR_NONE)
		SET_ERROR_AND_CLEANUP(err);

	/* final progress update */
	if (progress)
	{
		UINT64 sourcepos = (UINT64)hunknum * chd->header.hunkbytes;
		if (sourcepos)
			(*progress)("Compression complete ... final ratio = %d%%            \n", 100 - multi_length(chd->file) * 100 / sourcepos);
	}

	/* close the file */
	multi_close(sourcefile);
	return CHDERR_NONE;

cleanup:
	if (sourcefile)
		multi_close(sourcefile);
	return last_error;
}

/*************************************
 *
 *  All-in-one file verifier
 *
 *************************************/

int chd_verify(chd_file *chd, void (*progress)(const char *, ...), UINT8 actualmd5[CHD_MD5_BYTES], UINT8 actualsha1[CHD_SHA1_BYTES])
{
	struct MD5Context md5;
	struct sha1_ctx sha;
	UINT64 sourceoffset = 0;
	int err, prev_err = CHDERR_NONE, hunknum = 0;
	clock_t lastupdate;

	/* punt if no interface */
	if (!cur_interface.open)
		SET_ERROR_AND_CLEANUP(CHDERR_NO_INTERFACE);

	/* verify parameters */
	if (!chd)
		SET_ERROR_AND_CLEANUP(CHDERR_INVALID_PARAMETER);

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

	/* init the MD5/SHA1 computations */
	MD5Init(&md5);
	sha1_init(&sha);

	/* loop over source hunks until we run out */
	lastupdate = 0;
	for (hunknum = 0; hunknum < chd->header.totalhunks; hunknum++)
	{
		clock_t curtime = clock();
		UINT32 bytestochecksum;

		/* progress */
		if (curtime - lastupdate > CLOCKS_PER_SEC / 2)
		{
			if (progress)
				(*progress)("Verifying hunk %d/%d...\r", hunknum, chd->header.totalhunks);
			lastupdate = curtime;
		}

		/* read the hunk into the cache */
		err = read_hunk_into_cache(chd, hunknum);
		if (err == CHDERR_DECOMPRESSION_ERROR)
		{
			prev_err = CHDERR_DECOMPRESSION_ERROR;
			if (progress)
				(*progress)("Bad hunk %d/%d.        \r\n", hunknum, chd->header.totalhunks);
		}
		else if (err != CHDERR_NONE)
			SET_ERROR_AND_CLEANUP(err);

		/* 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)
		{
			MD5Update(&md5, chd->cache, bytestochecksum);
			sha1_update(&sha, bytestochecksum, chd->cache);
		}

		/* prepare for the next hunk */
		sourceoffset += chd->header.hunkbytes;
	}
	if (prev_err == CHDERR_DECOMPRESSION_ERROR)
		SET_ERROR_AND_CLEANUP(prev_err);

	/* compute the final MD5 */
	MD5Final(actualmd5, &md5);
	sha1_final(&sha);
	sha1_digest(&sha, SHA1_DIGEST_SIZE, actualsha1);

	/* final progress update */
	if (progress)
		(*progress)("Verification complete                                  \n");
	return CHDERR_NONE;

cleanup:
	return last_error;
}



/*************************************
 *
 *  Validate header data
 *
 *************************************/

static int validate_header(const chd_header *header)
{
	/* 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))
		return CHDERR_INVALID_PARAMETER;

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

	/* require a valid compression mechanism */
	if (header->compression >= CHDCOMPRESSION_MAX)
		return CHDERR_INVALID_PARAMETER;

	/* require a valid hunksize */
	if (header->hunkbytes == 0 || header->hunkbytes >= 65536)
		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)) && !memcmp(header->parentsha1, nullsha1, sizeof(nullsha1)))
		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;
}




/*************************************
 *
 *  Hunk read/decompress
 *
 *************************************/

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

	/* 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 */
			bytes = multi_read(chd->file, entry->offset, entry->length, chd->compressed);
			if (bytes != entry->length)
				return CHDERR_READ_ERROR;

			/* now decompress based on the compression method */
			switch (chd->header.compression)
			{
				case CHDCOMPRESSION_ZLIB:
				case CHDCOMPRESSION_ZLIB_PLUS:
				{
					zlib_codec_data *codec = chd->codecdata;

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

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

		/* uncompressed data */
		case MAP_ENTRY_TYPE_UNCOMPRESSED:
			bytes = multi_read(chd->file, entry->offset, chd->header.hunkbytes, dest);
			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 read_hunk_into_memory(chd, entry->offset, dest);

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

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


static int read_hunk_into_cache(chd_file *chd, UINT32 hunknum)
{
	int err;

	/* 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 = read_hunk_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 write/compress
 *
 *************************************/

static int write_hunk_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, match;

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

	/* 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 = find_matching_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 = find_matching_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;
			}
		}
	}

	/* if we get here, we need to compress the data */
	/* first, fill in an uncompressed entry */
	newentry.length = chd->header.hunkbytes;
	newentry.flags = MAP_ENTRY_TYPE_UNCOMPRESSED;

	/* now try compressing the data */
	switch (chd->header.compression)
	{
		case CHDCOMPRESSION_ZLIB:
		case CHDCOMPRESSION_ZLIB_PLUS:
		{
			zlib_codec_data *codec = chd->codecdata;
			int err;

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

			/* do it */
			err = deflate(&codec->deflater, Z_FINISH);

			/* if we didn't run out of space, override the raw data with compressed */
			if (err == Z_STREAM_END && codec->deflater.total_out < newentry.length)
			{
				data = chd->compressed;
				newentry.length = codec->deflater.total_out;
				newentry.flags = MAP_ENTRY_TYPE_COMPRESSED;
			}
			break;
		}
	}

	/* 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 = multi_length(chd->file);

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

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

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

	return CHDERR_NONE;
}



/*************************************
 *
 *  Header read
 *
 *************************************/

static int read_header(chd_interface_file *file, chd_header *header)
{
	UINT8 rawheader[CHD_MAX_HEADER_SIZE];
	UINT32 count;

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

	/* punt if invalid file */
	if (!file)
		return CHDERR_INVALID_FILE;

	/* punt if no interface */
	if (!cur_interface.read)
		return CHDERR_NO_INTERFACE;

	/* seek and read */
	count = multi_read(file, 0, sizeof(rawheader), 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))
		return CHDERR_INVALID_DATA;

	/* extract the common data */
	header->flags         = get_bigendian_uint32(&rawheader[16]);
	header->compression   = get_bigendian_uint32(&rawheader[20]);
	memcpy(header->md5, &rawheader[44], CHD_MD5_BYTES);
	memcpy(header->parentmd5, &rawheader[60], CHD_MD5_BYTES);

	/* 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]);
		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
	{
		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[76]);
		memcpy(header->sha1, &rawheader[80], CHD_SHA1_BYTES);
		memcpy(header->parentsha1, &rawheader[100], CHD_SHA1_BYTES);
	}

	/* guess it worked */
	return CHDERR_NONE;
}



/*************************************
 *
 *  Header write
 *
 *************************************/

static int write_header(chd_interface_file *file, const chd_header *header)
{
	UINT8 rawheader[CHD_MAX_HEADER_SIZE];
	UINT32 count;

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

	/* punt if invalid file */
	if (!file)
		return CHDERR_INVALID_FILE;

	/* punt if no interface */
	if (!cur_interface.write)
		return CHDERR_NO_INTERFACE;

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

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

	put_bigendian_uint32(&rawheader[8],  CHD_V3_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);
	memcpy(&rawheader[44], header->md5, CHD_MD5_BYTES);
	memcpy(&rawheader[60], header->parentmd5, CHD_MD5_BYTES);
	put_bigendian_uint32(&rawheader[76], header->hunkbytes);
	memcpy(&rawheader[80], header->sha1, CHD_SHA1_BYTES);
	memcpy(&rawheader[100], header->parentsha1, CHD_SHA1_BYTES);

	/* seek and write */
	count = multi_write(file, 0, CHD_V3_HEADER_SIZE, rawheader);
	if (count != CHD_V3_HEADER_SIZE)
		return CHDERR_WRITE_ERROR;

	return CHDERR_NONE;
}



/*************************************
 *
 *  Read the sector map
 *
 *************************************/

static int read_hunk_map(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];
	UINT8 cookie[MAP_ENTRY_SIZE];
	UINT64 fileoffset;
	UINT32 count;
	int i, err;

	/* first allocate memory */
	chd->map = 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 */
		count = multi_read(chd->file, fileoffset, entries * entrysize, raw_map_entries);
		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++)
				extract_map_entry(&raw_map_entries[j * MAP_ENTRY_SIZE], &chd->map[i + j]);
		}
		else
		{
			for (j = 0; j < entries; j++)
				extract_old_map_entry(&raw_map_entries[j * OLD_MAP_ENTRY_SIZE], &chd->map[i + j], chd->header.hunkbytes);
		}
	}

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

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



/*************************************
 *
 *  CRC map initialization
 *
 *************************************/

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

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

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

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

	/* allocate a CRC map table */
	chd->crctable = malloc(CRCMAP_HASH_SIZE * sizeof(chd->crctable[0]));
	if (!chd->crctable)
	{
		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++)
			add_to_crcmap(chd, i);
}



/*************************************
 *
 *  CRC map addition
 *
 *************************************/

static void add_to_crcmap(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;
}



/*************************************
 *
 *  Matching hunk verifier
 *
 *************************************/

static int is_really_matching_hunk(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 (read_hunk_into_memory(chd, hunknum, chd->compare) == CHDERR_NONE)
			chd->comparehunk = hunknum;
	}
	return (hunknum == chd->comparehunk && !memcmp(rawdata, chd->compare, chd->header.hunkbytes));
}



/*************************************
 *
 *  Matching hunk locater
 *
 *************************************/

static UINT32 find_matching_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) && is_really_matching_hunk(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))
		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) && is_really_matching_hunk(chd, curhunk, rawdata))
			return curhunk;

	return NO_MATCH;
}



/*************************************
 *
 *  Internal metadata locator
 *
 *************************************/

static int find_metadata_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 */
		count = multi_read(chd->file, metaentry->offset, sizeof(raw_meta_header), 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]);

		/* 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;
}

/*************************************
 *
 *  Extended compressor, works in segments
 *      chd_compress() should collapse to calls
 *      to this code once it's all verified.
 *
 *************************************/

chd_exfile *chd_start_compress_ex(chd_file *chd)
{
	int err;
	chd_exfile *finalchdex;

	/* punt if no interface */
	if (!cur_interface.open)
		SET_ERROR_AND_CLEANUP(CHDERR_NO_INTERFACE);

	/* verify parameters */
	if (!chd)
		SET_ERROR_AND_CLEANUP(CHDERR_INVALID_PARAMETER);

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

	/* create CRC maps for the new CHD and the parent */
	init_crcmap(chd, 0);
	if (chd->parent)
		init_crcmap(chd->parent, 1);

	finalchdex = malloc(sizeof(chd_exfile));
	if (!finalchdex)
		SET_ERROR_AND_CLEANUP(CHDERR_OUT_OF_MEMORY);

	/* init the MD5/SHA1 computations */
	MD5Init(&finalchdex->md5);
	sha1_init(&finalchdex->sha);

	finalchdex->chd = chd;
	finalchdex->sourceoffset = 0;
	finalchdex->hunknum = 0;

	return finalchdex;

cleanup:
	return NULL;
}

int chd_compress_ex(chd_exfile *chdex, const char *rawfile, UINT64 offset,
		UINT32 inpsecsize, UINT32 srcperhunk, UINT32 hunks_to_read,
		UINT32 hunksecsize, void (*progress)(const char *, ...))
{
	chd_interface_file *sourcefile = NULL;
	chd_file *chd;
	clock_t lastupdate;
	int err;
	UINT64 sourcefileoffset = 0;
	int hunk, blksread = 0;

	/* punt if no interface */
	if (!cur_interface.open)
		SET_ERROR_AND_CLEANUP(CHDERR_NO_INTERFACE);

	/* verify parameters */
	if (!chdex || !rawfile)
		SET_ERROR_AND_CLEANUP(CHDERR_INVALID_PARAMETER);

	chd = chdex->chd;

	/* open the raw file */
	sourcefile = multi_open(rawfile, "rb");
	if (!sourcefile)
		SET_ERROR_AND_CLEANUP(CHDERR_FILE_NOT_FOUND);

	/* loop over source hunks until we run out */
	lastupdate = 0;
	for (hunk = 0; hunk < hunks_to_read; hunk++)
	{
		clock_t curtime = clock();
		UINT32 bytestochecksum;
		UINT32 bytesread;
		int i;

		/* read the data.  first, zero the whole hunk */
		memset(chd->cache, 0, chd->header.hunkbytes);

		/* read each frame to a maximum framesize boundry, automatically padding them out */
		for (i = 0; i < srcperhunk; i++)
		{
			bytesread = multi_read(sourcefile, sourcefileoffset + offset, inpsecsize, &chd->cache[i*hunksecsize]);
			/*
               NOTE: because we pad CD tracks to a hunk boundry, there is a possibility
               that we will run off the end of the sourcefile and bytesread will be zero.
               because we already zero out the hunk beforehand above, no special processing
               need take place here.
            */

			blksread++;
			sourcefileoffset += inpsecsize;
		}

		/* progress */
		if (curtime - lastupdate > CLOCKS_PER_SEC / 2)
		{
			UINT64 sourcepos = (UINT64)hunk+chdex->hunknum * chd->header.hunkbytes;
			if (progress && sourcepos)
				(*progress)("Compressing hunk %d/%d... (ratio=%d%%)  \r", hunk+chdex->hunknum, chd->header.totalhunks, 100 - multi_length(chd->file) * 100 / sourcepos);
			lastupdate = curtime;
		}

		/* update the MD5/SHA1 */
		bytestochecksum = chd->header.hunkbytes;
		if (chdex->sourceoffset + chd->header.hunkbytes > chd->header.logicalbytes)
		{
			if (chdex->sourceoffset >= chd->header.logicalbytes)
				bytestochecksum = 0;
			else
				bytestochecksum = chd->header.logicalbytes - chdex->sourceoffset;
		}
		if (bytestochecksum)
		{
			MD5Update(&chdex->md5, chd->cache, bytestochecksum);
			sha1_update(&chdex->sha, bytestochecksum, chd->cache);
		}

		/* write out the hunk */
		err = write_hunk_from_memory(chd, hunk + chdex->hunknum, chd->cache);
		if (err != CHDERR_NONE)
			SET_ERROR_AND_CLEANUP(err);

		/* update our CRC map */
		if ((chd->map[hunk + chdex->hunknum].flags & MAP_ENTRY_FLAG_TYPE_MASK) != MAP_ENTRY_TYPE_SELF_HUNK &&
			(chd->map[hunk + chdex->hunknum].flags & MAP_ENTRY_FLAG_TYPE_MASK) != MAP_ENTRY_TYPE_PARENT_HUNK)
			add_to_crcmap(chd, hunk + chdex->hunknum);

		/* prepare for the next hunk */
		chdex->sourceoffset += chd->header.hunkbytes;
	}

	chdex->hunknum += hunks_to_read;

	return CHDERR_NONE;

cleanup:
	if (sourcefile)
		multi_close(sourcefile);
	return last_error;
}

int chd_end_compress_ex(chd_exfile *chdex, void (*progress)(const char *, ...))
{
	int err = CHDERR_NONE;
	chd_file *chd;

	chd = chdex->chd;

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

	/* turn off the writeable flag and re-write the header */
	chd->header.flags &= ~CHDFLAGS_IS_WRITEABLE;
	err = write_header(chd->file, &chd->header);
	if (err != CHDERR_NONE)
		SET_ERROR_AND_CLEANUP(err);

	/* final progress update */
	if (progress)
	{
		UINT64 sourcepos = (UINT64)chdex->hunknum * chd->header.hunkbytes;
		if (sourcepos)
			(*progress)("Compression complete ... final ratio = %d%%            \n", 100 - multi_length(chd->file) * 100 / sourcepos);
	}

cleanup:
	free(chdex);
	return err;
}

/*************************************
 *
 *  ZLIB memory hooks
 *
 *************************************/

/*
    Because ZLIB allocates and frees memory frequently (once per compression cycle),
    we don't call malloc/free, but instead keep track of our own memory.
*/

static voidpf fast_alloc(voidpf opaque, uInt items, uInt size)
{
	zlib_codec_data *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 = 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;
}


static void fast_free(voidpf opaque, voidpf address)
{
	zlib_codec_data *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;
		}
}



/*************************************
 *
 *  Compression init
 *
 *************************************/

static int init_codec(chd_file *chd)
{
	int err = CHDERR_NONE;

	/* now decompress based on the compression method */
	switch (chd->header.compression)
	{
		case CHDCOMPRESSION_NONE:
			/* nothing to do */
			break;

		case CHDCOMPRESSION_ZLIB:
		case CHDCOMPRESSION_ZLIB_PLUS:
		{
			zlib_codec_data *data;

			/* allocate memory for the 2 stream buffers */
			chd->codecdata = malloc(sizeof(zlib_codec_data));
			if (!chd->codecdata)
				return CHDERR_OUT_OF_MEMORY;

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

			/* init the first for decompression and the second for compression */
			data->inflater.next_in = chd->compressed;
			data->inflater.avail_in = 0;
			data->inflater.zalloc = fast_alloc;
			data->inflater.zfree = fast_free;
			data->inflater.opaque = data;
			err = inflateInit2(&data->inflater, -MAX_WBITS);
			if (err == Z_OK)
			{
				data->deflater.next_in = chd->compressed;
				data->deflater.avail_in = 0;
				data->deflater.zalloc = fast_alloc;
				data->deflater.zfree = fast_free;
				data->deflater.opaque = data;
				err = deflateInit2(&data->deflater, Z_BEST_COMPRESSION, Z_DEFLATED, -MAX_WBITS, 8, Z_DEFAULT_STRATEGY);
			}

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

			/* handle an error */
			if (err != CHDERR_NONE)
				free(chd->codecdata);
			break;
		}
	}

	/* return the error */
	return err;
}



/*************************************
 *
 *  Compression de-init
 *
 *************************************/

static void free_codec(chd_file *chd)
{
	/* now decompress based on the compression method */
	switch (chd->header.compression)
	{
		case CHDCOMPRESSION_NONE:
			/* nothing to do */
			break;

		case CHDCOMPRESSION_ZLIB:
		case CHDCOMPRESSION_ZLIB_PLUS:
		{
			zlib_codec_data *data = chd->codecdata;

			/* deinit the streams */
			if (data)
			{
				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);
			}
			break;
		}
	}
}



/*************************************
 *
 *  Multifile routines
 *
 *************************************/

static chd_interface_file *multi_open(const char *filename, const char *mode)
{
	return (*cur_interface.open)(filename, mode);
}

static void multi_close(chd_interface_file *file)
{
	(*cur_interface.close)(file);
}

static UINT32 multi_read(chd_interface_file *file, UINT64 offset, UINT32 count, void *buffer)
{
	return (*cur_interface.read)(file, offset, count, buffer);
}

static UINT32 multi_write(chd_interface_file *file, UINT64 offset, UINT32 count, const void *buffer)
{
	return (*cur_interface.write)(file, offset, count, buffer);
}

static UINT64 multi_length(chd_interface_file *file)
{
	return (*cur_interface.length)(file);
}
