/*
 * Copyright © 1997 Pluto Technologies International, Inc.  Boulder CO
 * Copyright © 1997 interface business GmbH, Dresden.
 *	All rights reserved.
 *
 * This code was written by Jörg Wunsch, Dresden.
 * Direct comments to <joerg_wunsch@interface-business.de>.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. 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.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``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 THE AUTHOR(S) 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.
 *
 * $FreeBSD: src/sys/i386/boot/cdboot/cdrom.c,v 1.4 1999/08/28 00:43:17 peter Exp $
 */


#include "boot.h"

#include <isofs/cd9660/iso.h>

#define BLKSIZE	2048		/* CD-ROM data block size */
#define BIOSSEC	512		/* BIOS sector size */

#define CD2LBA(rba)	((rba) << 2) /* CD-ROM relative block to BIOS LBA */

u_int32_t sessionstart;

static struct iso_primary_descriptor pdesc;

static char *rootdirbuf;
static size_t rootdirsize;
static char xbuf[BLKSIZE];
static u_int32_t curblk, startblk, filesize, offset;

static int bread(u_int32_t rba, size_t nblks, void *buf);
static void badread(const char *msg, u_int32_t blkno);
static struct iso_directory_record *find(const char *path, int list_only);
static char *get_rr_name(struct iso_directory_record *dirp, size_t *len_ret);
static int iread(u_char *buf, size_t len,
		 void (*copyfun)(const void *src, void *dst, size_t size));

static struct daddrpacket dpkt = { 0x10 };

int
devopen(u_int32_t session)
{
	int rv;
	u_int32_t rootdirblk;
	struct iso_directory_record *rootdirp;

	if ((rv = bread(session + 16, 1, &pdesc)) != 0) {
		printf("Error reading primary ISO descriptor: %d\n", rv);
		return -1;
	}
	rootdirp = (struct iso_directory_record *)pdesc.root_directory_record;
	rootdirblk = isonum_733(rootdirp->extent);
	rootdirsize = isonum_733(rootdirp->size);

	/* just in case, round up */
	rootdirsize = (rootdirsize + BLKSIZE - 1) & ~(BLKSIZE - 1);

	if (rootdirbuf != NULL)
		free(rootdirbuf);
	if ((rootdirbuf = malloc(rootdirsize)) == 0) {
		printf("Cannot allocate memory for the root "
		       "directory buffer.\n");
		return -1;
	}
	if ((rv = bread(rootdirblk, rootdirsize / BLKSIZE, rootdirbuf))
	    != 0) {
		printf("Error reading root directory: %d\n", rv);
		return -1;
	}

	DPRINTF(("Root directory is 0x%x bytes @ %d\n",
		 rootdirsize, rootdirblk));

	return 0;
}

static int
bread(u_int32_t rba, size_t nblks, void *buf)
{
	int i, rv;

	for (i = 0, rv = -1; rv != 0 && i < 3; i++) {
		dpkt.nblocks = nblks * (BLKSIZE / BIOSSEC);
		dpkt.boffs = (u_int16_t)((int)buf & 0xffff);
		dpkt.bseg = BOOTSEG;
		dpkt.lba = CD2LBA(rba);

#ifdef DEBUG_VERBOSE
		DPRINTF(("Calling biosreadlba(%d blocks, lba %d) = ",
			 dpkt.nblocks, dpkt.lba));
#endif

		rv = biosreadlba(&dpkt);

#ifdef DEBUG_VERBOSE
		DPRINTF(("%d\n", rv));
#endif
	}
	return rv;
}


void
seek(u_int32_t offs)
{
	offset = offs;
}

static void
badread(const char *msg, u_int32_t blkno)
{
	printf("Error reading block %d from CD-ROM: %s\n",
	       blkno, msg);
}

static __inline size_t
minlen(size_t a, size_t b)
{
	return a < b? a: b;
}

/*
 * Internal form of read()/xread().
 */
static int
iread(u_char *buf, size_t len,
      void (*copyfun)(const void *src, void *dst, size_t size))
{
	u_int32_t newblk, ptr;
	size_t bsize;

	newblk = offset / BLKSIZE + startblk;

	if (newblk != curblk) {
		if (offset + len >= filesize) {
			badread("access beyond file limit", newblk);
			return -1;
		}
		if (bread(newblk, 1, xbuf)) {
			badread("BIOS read error", newblk);
			return -1;
		}
		curblk = newblk;
	}
	ptr = offset & (BLKSIZE - 1);
	if (ptr > 0) {
		/* initial short transfer */
		bsize = minlen(BLKSIZE - ptr, len);
		copyfun(xbuf + ptr, buf, bsize);
		buf += bsize;
		len -= bsize;
		offset += bsize;
	}
	for (; len > 0; len -= bsize) {
		bsize = minlen(len, BLKSIZE);
		newblk = offset / BLKSIZE + startblk;

		if (newblk != curblk) {
			if (offset + bsize > filesize) {
				badread("access beyond file limit", newblk);
				return -1;
			}
			if (bread(newblk, 1, xbuf)) {
				badread("BIOS read error", newblk);
				return -1;
			}
			curblk = newblk;
		}
		copyfun(xbuf, buf, bsize);
		buf += bsize;
		offset += bsize;
	}
	return 0;
}

int
read(u_char *buf, size_t len)
{
	DPRINTF(("read(0x%x, %d)\n", (int)buf, len));
	return iread(buf, len, bcopy);
}

int
xread(u_char *buf, size_t len)
{
	DPRINTF(("xread(0x%x, %d)\n", (int)buf, len));
	return iread(buf, len, pcpy);
}

static char *
get_rr_name(struct iso_directory_record *dirp, size_t *len_ret)
{
	struct rr_header {
		char	type[2];
		u_char	len;
		u_char	version;
	} *rrp;
	struct rr_nm_header {
		struct rr_header rrh;
		u_char	flags;
		char	name[0]; /* XXX -- using gcc extension */
	} *rrnmp;
	char *cp;

	cp = dirp->name + (u_char)dirp->name_len[0];
	/* round up to 16-bit boundary; ugly */
	cp = (char *)(((int)cp + 1) & ~1);
	rrp = (struct rr_header *)cp;

	if (rrp->type[0] != 'R' || rrp->type[1] != 'R') {
		DPRINTF(("no RR, "));
		return 0;
	}

	DPRINTF(("RR attribs: "));
	cp += rrp->len;
	while (cp - (char *)dirp <= (u_char)dirp->length[0]) {
		rrp = (struct rr_header *)cp;
		DPRINTF(("%c%c ", rrp->type[0], rrp->type[1]));
		if (rrp->type[0] == 'N' && rrp->type[1] == 'M') {
			rrnmp = (struct rr_nm_header *)rrp;
			*len_ret = rrp->len - sizeof(struct rr_nm_header);
			return rrnmp->name;
		}
		cp += rrp->len;
	}

	return 0;
}

static struct iso_directory_record *
find(const char *path, int list_only)
{
	struct iso_directory_record *dirp;
	char *ptr, *rrname;
	size_t len, entrylen;
	char namebuf[256];
	int i;
	int (*comp)(const char *, const char *);

	while (*path && *path == '/')
		path++;

	for (ptr = rootdirbuf, i = 1;
	     ptr < rootdirbuf + rootdirsize;
	     ptr += entrylen, i++) {
		dirp = (struct iso_directory_record *)ptr;
		entrylen = (u_char)dirp->length[0];
		len = (u_char)dirp->name_len[0];

		DPRINTF(("# %d: offset 0x%x, length 0x%x = %d, ",
			 i, (int)(ptr - rootdirbuf), entrylen, entrylen));

		if (entrylen == 0) {
			/*
			 * Dir entry of length 0.  That's the last
			 * entry in this block, advance to the next
			 * block (if any).  In case we get beyond the
			 * end of the directory, we'll fall off the
			 * loop due to the rootdirsize condition in
			 * the `for' statement.
			 */
			DPRINTF(("entrylen 0\n"));
			entrylen = (~((ptr - rootdirbuf) + BLKSIZE - 1))
				& (BLKSIZE - 1);
			continue;
		}
		if (len == 0) {
			DPRINTF(("name_len 0\n"));
			continue;
		}
		if (len == 1 &&
		    (dirp->name[0] == '\0' || dirp->name[1] == '\1')) {
			DPRINTF(("dot/dot-dot entry\n"));
			continue;
		}
		/* don't consider directories */
		if (dirp->flags[0] & 2) {
			DPRINTF(("directory\n"));
			continue;
		}
		rrname = get_rr_name(dirp, &len);
		comp = rrname? strcmp: strcasecmp;

		bcopy(rrname? rrname: dirp->name, namebuf, len);
		namebuf[len] = 0;
		DPRINTF(("name `%s'\n", namebuf));

		if (list_only) {
#ifndef DEBUG
			printf("%s ", namebuf);
#endif
		} else if (comp(path, namebuf) == 0)
			return dirp;
	}
#ifndef DEBUG
	if (list_only)
		printf("\n");
#endif
	return 0;
}

int
openrd(char *name)
{
	char *cp;
	const char *fname;
	u_int32_t oldsession;
	int session, list_only;
	struct iso_directory_record *dirp;

	session = 0;
	fname = name;

	/*
	 * We accept the following boot string:
	 *
	 * [@sessionstart] name
	 */
	for (cp = name; *cp; cp++)
		switch (*cp) {
		/* we don't support filenames with spaces */
		case ' ':	case '\t':
			break;

		case '@':
			if (session) {
				printf("Syntax error\n");
				return -1;
			}
			session++;
			oldsession = sessionstart;
			sessionstart = 0;
			break;

		case '0':	case '1':	case '2':
		case '3':	case '4':	case '5':
		case '6':	case '7':	case '8':
		case '9':
			if (session == 1) {
				sessionstart *= 10;
				sessionstart += *cp - '0';
			}
			break;

		default:
			if (session == 1) {
				session++;
				fname = cp;
			}
		}

	if (session && devopen(sessionstart) == -1) {
		(void)devopen(oldsession);
		sessionstart = oldsession;
	}
	if (session == 1)
		/* XXX no filename, only session arg */
		return -1;

	list_only = fname[0] == '?' && fname[1] == 0;

	DPRINTF(("Calling find(%s, %d):\n", fname, list_only));
	dirp = find(fname, list_only);
	DPRINTF(("find() returned 0x%x\n", (int)dirp));

	if (list_only)
		return -1;
	if (dirp == 0)
		return 1;

	startblk = isonum_733(dirp->extent);
	filesize = isonum_733(dirp->size);

	DPRINTF(("startblk = %d, filesize = %d\n", startblk, filesize));

	curblk = 0;	/* force a re-read, 0 is impossible file start */
	seek(0);

	return 0;
}
