/*
  NiH: chd.c,v 1.6 2004/06/25 23:31:08 dillo Exp

  chd.c -- accessing chd files
  Copyright (C) 2004 Dieter Baron and Thomas Klausner

  This file is part of ckmame, a program to check rom sets for MAME.
  The authors can be contacted at <nih@giga.or.at>

  This program is free software; you can redistribute it and/or modify
  it under the terms of the GNU General Public License, version 2, as
  published by the Free Software Foundation.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with this program; if not, write to the Free Software
  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/



#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <zlib.h>

#include "chd.h"



#define MAX_HEADERLEN	120		/* maximum header length */
#define TAG		"MComprHD"
#define TAG_LEN		8		/* length of tag */
#define TAG_AND_LEN	12		/* length of tag + header length */

#define MAP_ENTRY_SIZE_V12	8	/* size of map entry, versions 1 & 2 */
#define MAP_ENTRY_SIZE_V3	16	/* size of map entry, version 3 */

#define GET_LONG(b)	(b+=4,((b)[-4]<<24)|((b)[-3]<<16)|((b)[-2]<<8)|(b)[-1])
#define GET_QUAD(b)	(b+=8,((uint64_t)(b)[-8]<<56)|((uint64_t)(b)[-7]<<48) \
			 |((uint64_t)(b)[-6]<<40)|((uint64_t)(b)[-5]<<32)     \
			 |((uint64_t)(b)[-4]<<24)|((uint64_t)(b)[-3]<<16)     \
			 |((uint64_t)(b)[-2]<<8)|((uint64_t)(b)[-1]))
#define GET_SHORT(b)	(b+=2,((b)[-2]<<8)|(b)[-1])

static int read_header(struct chd *);
static int read_map(struct chd *);



void
chd_close(struct chd *chd)
{
    fclose(chd->f);
    free(chd->name);
    free(chd->map);
    free(chd->buf);
    free(chd->hbuf);
    free(chd);
}   



struct chd *
chd_open(const char *name, int *errp)
{
    struct chd *chd;
    FILE *f;

    if ((f=fopen(name, "rb")) == NULL) {
	if (errp)
	    *errp = CHD_ERR_OPEN;
	return NULL;
    }

    if ((chd=malloc(sizeof(*chd))) == NULL) {
	if (errp)
	    *errp = CHD_ERR_NOMEM;
	return NULL;
    }
    chd->f = f;
    if ((chd->name=strdup(name)) == NULL) {
	if (errp)
	    *errp = CHD_ERR_NOMEM;
	chd_close(chd);
	return NULL;
    }
    chd->error = 0;
    chd->map = NULL;
    chd->buf = NULL;
    chd->hno = -1;
    chd->hbuf = NULL;

    if (read_header(chd) < 0) {
	if (errp)
	    *errp = chd->error;
	chd_close(chd);
	return NULL;
    }

    return chd;
}



int
chd_read_hunk(struct chd *chd, int idx, char *b)
{
    int i, n, err;

    if (idx < 0 || idx > chd->total_hunks) {
	chd->error = CHD_ERR_INVAL;
	return -1;
    }

    if (chd->map == NULL) {
	if (read_map(chd) < 0)
	    return -1;
    }

    if (chd->map[idx].length > chd->hunk_len) {
	chd->error = CHD_ERR_NOTSUP;
	return -1;
    }

    switch (chd->map[idx].flags & CHD_MAP_TYPE_MASK) {
    case CHD_MAP_TYPE_COMPRESSED:
	/* XXX: CHD_COMP_NONE? */
	if (chd->compression != CHD_COMP_ZLIB
	    && chd->compression != CHD_COMP_ZLIB_PLUS) {
	    chd->error = CHD_ERR_NOTSUP;
	    return -1;
	}

	if (chd->buf == NULL) {
	    if ((chd->buf=malloc(chd->hunk_len)) == NULL) {
		chd->error = CHD_ERR_NOMEM;
		return -1;
	    }
	    chd->z.avail_in = 0;
	    chd->z.zalloc = Z_NULL;
	    chd->z.zfree = Z_NULL;
	    chd->z.opaque = NULL;
	    err = inflateInit2(&chd->z, -MAX_WBITS);
	}
	else
	    err = inflateReset(&chd->z);
	if (err != Z_OK) {
	    chd->error = CHD_ERR_ZLIB;
	    return -1;
	}

	if (fseek(chd->f, chd->map[idx].offset, SEEK_SET) == -1) {
	    chd->error = CHD_ERR_SEEK;
	    return -1;
	}
	if ((n=fread(chd->buf, 1, chd->map[idx].length, chd->f)) < 0) {
	    chd->error = CHD_ERR_READ;
	    return -1;
	}

	chd->z.next_in = chd->buf;
	chd->z.avail_in = n;
	chd->z.next_out = b;
	chd->z.avail_out = chd->hunk_len;
	/* XXX: should use Z_FINISH, but that returns Z_BUF_ERROR */
	if ((err=inflate(&chd->z, 0)) != Z_OK && err != Z_STREAM_END) {
	    chd->error = CHD_ERR_ZLIB;
	    return -1;
	}
	/* XXX: chd->z.avail_out should be 0 */
	n = chd->hunk_len - chd->z.avail_out;
	break;

    case CHD_MAP_TYPE_UNCOMPRESSED:
	if (fseek(chd->f, chd->map[idx].offset, SEEK_SET) == -1) {
	    chd->error = CHD_ERR_SEEK;
	    return -1;
	}
	/* XXX: use chd->hunk_len instead? */
	if ((n=fread(b, 1, chd->map[idx].length, chd->f)) < 0) {
	    chd->error = CHD_ERR_READ;
	    return -1;
	}
	break;

    case CHD_MAP_TYPE_MINI:
	b[0] = (chd->map[idx].offset >> 56) & 0xff;
	b[1] = (chd->map[idx].offset >> 48) & 0xff;
	b[2] = (chd->map[idx].offset >> 40) & 0xff;
	b[3] = (chd->map[idx].offset >> 32) & 0xff;
	b[4] = (chd->map[idx].offset >> 24) & 0xff;
	b[5] = (chd->map[idx].offset >> 16) & 0xff;
	b[6] = (chd->map[idx].offset >> 8) & 0xff;
	b[7] = chd->map[idx].offset & 0xff;
	n = chd->hunk_len;
	for (i=8; i<n; i++)
	    b[i] = b[i-8];
	break;

    case CHD_MAP_TYPE_SELF_HUNK:
	/* XXX: check CRC here too? */
	return chd_read_hunk(chd, chd->map[idx].offset, b);

    case CHD_MAP_TYPE_PARENT_HUNK:
	chd->error = CHD_ERR_NOTSUP;
	return -1;

    default:
	chd->error = CHD_ERR_NOTSUP; /* XXX: wrong error */
	return -1;
    }
    
    if ((chd->map[idx].flags & CHD_MAP_FL_NOCRC) == 0) {
	if (crc32(0, b, n) != chd->map[idx].crc) {
	    chd->error = CHD_ERR_CRC;
	    return -1;
	}
    }
    
    return n;
}



int
chd_read_range(struct chd *chd, char *b, int off, int len)
{
    int i, s, n;
    int copied, o2, l2;

    /* XXX: error handling */

    s = off/chd->hunk_len;
    n = (off+len+chd->hunk_len-1)/chd->hunk_len - s;

    copied = 0;
    o2 = off % chd->hunk_len;
    l2 = chd->hunk_len - o2;

    for (i=0; i<n; i++) {
	if (i == 1) {
	    o2 = 0;
	    l2 = chd->hunk_len;
	}
	if (i == n-1) {
	    if (l2 > len-copied)
		l2 = len-copied;
	}
	if (o2 == 0 && l2 == chd->hunk_len && s+i != chd->hno) {
	    if (chd_read_hunk(chd, s+i, b+copied) < 0)
		return -1;
	    copied += chd->hunk_len;
	}
	else {
	    if (chd->hbuf == NULL)
		if ((chd->hbuf=malloc(chd->hunk_len)) == NULL) {
		    chd->error = CHD_ERR_NOMEM;
		    return -1;
		}
	    if (s+i != chd->hno) {
		if (chd_read_hunk(chd, s+i, chd->hbuf) < 0)
		    return  -1;
		chd->hno = s+i;
	    }
	    memcpy(b+copied, chd->hbuf+o2, l2);
	    copied += l2;
	}
    }

    return len;
}



static int
read_header(struct chd *chd)
{
    uint32_t len;

    unsigned char b[MAX_HEADERLEN], *p;

    if (fread(b, TAG_AND_LEN, 1, chd->f) != 1) {
	chd->error = CHD_ERR_READ;
	return -1;
    }

    if (memcmp(b, TAG, TAG_LEN) != 0) {
	chd->error = CHD_ERR_NO_CHD;
	return -1;
    }

    p = b+TAG_LEN;
    len = GET_LONG(p);
    if (len > MAX_HEADERLEN) {
	chd->error = CHD_ERR_NO_CHD;
	return -1;
    }
    if (fread(p, len-TAG_AND_LEN, 1, chd->f) != 1) {
	chd->error = CHD_ERR_READ;
	return -1;
    }
    
    chd->hdr_length = len;
    chd->version = GET_LONG(p);
    chd->flags = GET_LONG(p);
    chd->compression = GET_LONG(p);

    if (chd->version > 3) {
	chd->error = CHD_ERR_VERSION;
	return -1;
    }
    /* XXX: check chd->hdr_length against expected value for version */

    if (chd->version < 3) {
	chd->hunk_len = GET_LONG(p);
	chd->total_hunks = GET_LONG(p);
	p += 12; /* skip c/h/s */
	memcpy(chd->md5, p, sizeof(chd->md5));
	p += sizeof(chd->md5);
	memcpy(chd->parent_md5, p, sizeof(chd->parent_md5));
	p += sizeof(chd->parent_md5);

	if (chd->version == 1)
	    chd->hunk_len *= 512;
	else
	    chd->hunk_len *= GET_LONG(p);
	chd->total_len = chd->hunk_len * chd->total_hunks;
	chd->meta_offset = 0;
	memset(chd->sha1, 0, sizeof(chd->sha1));
	memset(chd->parent_sha1, 0, sizeof(chd->parent_sha1));
    }
    else {
	chd->total_hunks = GET_LONG(p);
	chd->total_len = GET_QUAD(p);
	chd->meta_offset = GET_QUAD(p);
	memcpy(chd->md5, p, sizeof(chd->md5));
	p += sizeof(chd->md5);
	memcpy(chd->parent_md5, p, sizeof(chd->parent_md5));
	p += sizeof(chd->parent_md5);
	chd->hunk_len = GET_LONG(p);
	memcpy(chd->sha1, p, sizeof(chd->sha1));
	p += sizeof(chd->sha1);
    }

    return 0;
}



static int
read_map(struct chd *chd)
{
    unsigned char b[MAP_ENTRY_SIZE_V3], *p;
    int i, len;
    uint64_t v;

    if ((chd->map=malloc(sizeof(*chd->map)*chd->total_hunks)) == NULL) {
	chd->error = CHD_ERR_NOMEM;
	return -1;
    }

    if (chd->version < 3)
	len = MAP_ENTRY_SIZE_V12;
    else
	len = MAP_ENTRY_SIZE_V3;

    for (i=0; i<chd->total_hunks; i++) {
	if (fread(b, len, 1, chd->f) != 1) {
	    chd->error = CHD_ERR_READ;
	    return -1;
	}
	p = b;

	if (i == 1832)
	    chd->version = 3;

	if (chd->version < 3) {
	    v = GET_QUAD(p);
	    chd->map[i].offset = v & 0xFFFFFFFFFFFLL;
	    chd->map[i].crc = 0;
	    chd->map[i].length = v >> 44;
	    chd->map[i].flags = CHD_MAP_FL_NOCRC
		| (chd->map[i].length == chd->hunk_len
		   ? CHD_MAP_TYPE_UNCOMPRESSED : CHD_MAP_TYPE_COMPRESSED);
	}
	else {
	    chd->map[i].offset = GET_QUAD(p);
	    chd->map[i].crc = GET_LONG(p);
	    chd->map[i].length = GET_SHORT(p);
	    chd->map[i].flags = GET_SHORT(p);
	}
    }

    return 0;
}
