/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License (the "License").
 * You may not use this file except in compliance with the License.
 *
 * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
 * or http://www.opensolaris.org/os/licensing.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information: Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 */
/*
 * Copyright 2006 Sun Microsystems, Inc.  All rights reserved.
 * Use is subject to license terms.
 */

#pragma ident	"%Z%%M%	%I%	%E% SMI"

/*
 * Basic file system reading code for standalone I/O system.
 * Simulates a primitive UNIX I/O system (read(), write(), open(), etc).
 * Does not support writes.
 */

#include <sys/param.h>
#include <sys/sysmacros.h>
#include <sys/vnode.h>
#include <sys/fs/ufs_fsdir.h>
#include <sys/fs/ufs_fs.h>
#include <sys/fs/ufs_inode.h>

#include <sys/fs/hsfs_spec.h>
#include <sys/fs/hsfs_isospec.h>
#include <sys/fs/hsfs_node.h>
#include <sys/fs/hsfs_susp.h>
#include <sys/fs/hsfs_rrip.h>
#include <sys/bootvfs.h>
#include <sys/filep.h>

#ifdef	_BOOT
#include "../common/util.h"
#else
#include <sys/sunddi.h>
#endif

#define	hdbtodb(n)	((ISO_SECTOR_SIZE / DEV_BSIZE) * (n))

#define	HSFS_NUM_SIG    14

#define	SUSP_SP_IX	0
#define	SUSP_CE_IX	1
#define	SUSP_PD_IX	2
#define	SUSP_ST_IX	3
#define	SUSP_ER_IX	4
#define	RRIP_PX_IX	5
#define	RRIP_PN_IX	6
#define	RRIP_SL_IX	7
#define	RRIP_CL_IX	8
#define	RRIP_PL_IX	9
#define	RRIP_RE_IX	10
#define	RRIP_RF_IX	11
#define	RRIP_RR_IX	12
#define	RRIP_NM_IX	13

#ifdef	_BOOT
#define	dprintf	if (bootrd_debug) printf
#else
#define	printf	kobj_printf
#define	dprintf	if (bootrd_debug) kobj_printf

/* PRINTFLIKE1 */
extern void kobj_printf(char *, ...);
#endif

extern int bootrd_debug;
extern void *bkmem_alloc(size_t);
extern void bkmem_free(void *, size_t);

struct dirstuff {
	int loc;
	fileid_t *filep;
};

struct hs_direct {
    struct	direct  hs_ufs_dir;
    struct	hs_direntry hs_dir;
};

static uint_t root_ino = 0;
static struct hs_volume *hsfsp;
static fileid_t *head;

static char *hsfs_sig_tab[] = {
	SUSP_SP,
	SUSP_CE,
	SUSP_PD,
	SUSP_ST,
	SUSP_ER,
	RRIP_PX,
	RRIP_PN,
	RRIP_SL,
	RRIP_CL,
	RRIP_PL,
	RRIP_RE,
	RRIP_TF,
	RRIP_RR,
	RRIP_NM
};

static int hsfs_num_sig = sizeof (hsfs_sig_tab) / sizeof (hsfs_sig_tab[0]);

/*
 *  Local prototypes
 */
static struct hs_direct *readdir(struct dirstuff *);
static uint_t parse_dir(fileid_t *, int, struct hs_direct *);
static uint_t parse_susp(char *, uint_t *, struct hs_direct *);
static ino_t dlook(char *, fileid_t *);
static int opendir(ino_t, fileid_t *);
static ino_t find(char *, fileid_t *);

static int bhsfs_mountroot(char *str);
static int bhsfs_unmountroot(void);
static int bhsfs_open(char *str, int flags);
static int bhsfs_close(int fd);
static void bhsfs_closeall(void);
static ssize_t bhsfs_read(int fdesc, char *buf, size_t count);
static off_t bhsfs_lseek(int fdesc, off_t addr, int whence);
static int bhsfs_fstat(int fdesc, struct bootstat *stp);

static fileid_t *
find_fp(int fd)
{
	fileid_t *filep = head;

	if (fd >= 0) {
		while ((filep = filep->fi_forw) != head)
			if (fd == filep->fi_filedes)
				return (filep->fi_taken ? filep : 0);
	}

	return (0);
}

static int
opendir(ino_t inode, fileid_t *filep)
{
	struct hs_direct hsdep;

	dprintf("opendir: inode = %ld\n", inode);
	/* Set up the IO request */
	filep->fi_offset = 0;
	filep->fi_blocknum = hdbtodb(inode);
	filep->fi_count = ISO_SECTOR_SIZE;
	filep->fi_memp = 0;

	if (diskread(filep))
		return (0);

	filep->fi_offset = 0;
	filep->fi_blocknum = hdbtodb(inode);

	if (inode != root_ino)
	    return (0);

	if (parse_dir(filep, 0, &hsdep) > 0) {
		struct inode *ip;

		ip = filep->fi_inode;
		if (ip == NULL)
			ip = filep->fi_inode = bkmem_alloc(sizeof (*ip));

		ip->i_size = hsdep.hs_dir.ext_size;
		ip->i_smode = hsdep.hs_dir.mode;
		ip->i_number = inode;
		return (0);
	}
	return (1);
}

static ino_t
find(char *path, fileid_t *filep)
{
	char *q;
	char c;
	ino_t n;

	dprintf("find: %s\n", path);
	if (path == NULL || *path == '\0')
		return (0);

	if (opendir(root_ino, filep))
		return (0);

	while (*path) {
		while (*path == '/')
			path++;
		q = path;
		while (*q != '/' && *q != '\0')
			q++;
		c = *q;
		*q = '\0';
		n = dlook(path, filep);
		*q = c;
		path = q;

		if (n != 0) {
			if (c == '\0')
				break;
			if (opendir(n, filep))
				return (0);
			continue;
		} else {
			return (0);
		}
	}
	return ((ino_t)n);
}

static ino_t
dlook(char *s, fileid_t *filep)
{
	struct hs_direct *hsdep;
	struct direct *udp;
	struct inode *ip;
	struct dirstuff dirp;
	int len;

	dprintf("dlook: %s\n", s);
	ip = filep->fi_inode;
	if (s == NULL || *s == '\0')
		return (0);
	if ((ip->i_smode & IFMT) != IFDIR) {
		return (0);
	}
	if (ip->i_size == 0) {
		return (0);
	}
	len = strlen(s);
	dirp.loc = 0;
	dirp.filep = filep;
	for (hsdep = readdir(&dirp); hsdep != NULL; hsdep = readdir(&dirp)) {
		udp = &hsdep->hs_ufs_dir;
		if (udp->d_namlen == 1 &&
		    udp->d_name[0] == '.' &&
		    udp->d_name[1] == '\0')
			continue;
		if (udp->d_namlen == 2 &&
		    udp->d_name[0] == '.' &&
		    udp->d_name[1] == '.' &&
		    udp->d_name[2] == '\0')
			continue;
		if (udp->d_namlen == len && (strcmp(s, udp->d_name)) == 0) {
			struct inode *ip = filep->fi_inode;

			filep->fi_offset = 0;
			filep->fi_blocknum = hdbtodb(udp->d_ino);

			bzero(filep->fi_inode, sizeof (struct inode));
			ip->i_size = hsdep->hs_dir.ext_size;
			ip->i_smode = hsdep->hs_dir.mode;
			ip->i_number = udp->d_ino;
			return (udp->d_ino);
		}
	}
	return (0);
}

/*
 * get next entry in a directory.
 */
static struct hs_direct *
readdir(struct dirstuff *dirp)
{
	static struct hs_direct hsdep;
	struct direct *udp = &hsdep.hs_ufs_dir;
	struct inode *ip;
	fileid_t *filep;
	daddr_t lbn;
	int off;

	dprintf("readdir: start\n");
	filep = dirp->filep;
	ip = filep->fi_inode;
	for (;;) {
		if (dirp->loc >= ip->i_size) {
			return (NULL);
		}
		off = dirp->loc & ((1 << ISO_SECTOR_SHIFT) - 1);
		if (off == 0) {
			lbn = hdbtodb(dirp->loc >> ISO_SECTOR_SHIFT);
			filep->fi_blocknum = lbn + hdbtodb(ip->i_number);
			filep->fi_count = ISO_SECTOR_SIZE;
			filep->fi_memp = 0;
			if (diskread(filep)) {
				dprintf("readdir: diskread failed\n");
				return (NULL);
			}
		}
		dirp->loc += parse_dir(filep, off, &hsdep);
		if (udp->d_reclen == 0 && dirp->loc <= ip->i_size) {
			dirp->loc = roundup(dirp->loc, ISO_SECTOR_SIZE);
			continue;
		}
		return (&hsdep);
	}
}

static int
getblock(fileid_t *filep)
{
	struct inode *ip = filep->fi_inode;
	int off, size, diff;
	daddr_t lbn;

	dprintf("getblock: start\n");
	diff = ip->i_size - filep->fi_offset;
	if (diff <= 0)
		return (-1);

	/* which block (or frag) in the file do we read? */
	lbn = hdbtodb(filep->fi_offset >> ISO_SECTOR_SHIFT);
	filep->fi_blocknum = lbn + hdbtodb(ip->i_number);

	off = filep->fi_offset & ((1 << ISO_SECTOR_SHIFT) - 1);
	size = filep->fi_count = ISO_SECTOR_SIZE;
	filep->fi_memp = 0;
	if (diskread(filep))	/* Trap errors */
		return (-1);

	if (filep->fi_offset - off + size >= ip->i_size)
		filep->fi_count = diff + off;
	filep->fi_count -= off;
	filep->fi_memp += off;
	dprintf("getblock: end\n");
	return (0);
}

static ssize_t
bhsfs_read(int fd, caddr_t buf, size_t count)
{
	int i, j;
	fileid_t *filep;
	struct inode *ip;
	caddr_t n;

	dprintf("bhsfs_read %d, count 0x%lx\n", fd, count);
	filep = find_fp(fd);
	if (filep == NULL)
		return (-1);

	ip = filep->fi_inode;
	n = buf;
	if (filep->fi_offset + count > ip->i_size)
		count = ip->i_size - filep->fi_offset;

	if ((i = count) <= 0)
		return (0);

	while (i > 0) {
		if (filep->fi_count == 0) {
			if (getblock(filep) == -1)
				return (0);
		}
		j = MIN(i, filep->fi_count);
		bcopy(filep->fi_memp, buf, (uint_t)j);
		buf += j;
		filep->fi_memp += j;
		filep->fi_offset += j;
		filep->fi_count -= j;
		i -= j;
	}

	dprintf("bhsfs_read: read 0x%x\n", (int)(buf - n));
	return (buf - n);
}

/*ARGSUSED*/
static int
bhsfs_mountroot(char *str)
{
	char *bufp;

	if (hsfsp != NULL)
		return (0);	/* already mounted */

	dprintf("mounting ramdisk as hsfs\n");

	hsfsp = bkmem_alloc(sizeof (*hsfsp));
	bzero(hsfsp, sizeof (*hsfsp));
	head = bkmem_alloc(sizeof (*head));
	bzero(head, sizeof (*head));
	head->fi_back = head->fi_forw = head;

	/* now read the superblock. */
	head->fi_blocknum = hdbtodb(ISO_VOLDESC_SEC);
	head->fi_offset = 0;
	head->fi_count = ISO_SECTOR_SIZE;
	head->fi_memp = head->fi_buf;
	if (diskread(head)) {
		printf("failed to read superblock\n");
		bhsfs_closeall();
		return (-1);
	}

	/* Since RRIP is based on ISO9660, that's where we start */
	bufp = head->fi_buf;
	if ((ISO_DESC_TYPE(bufp) != ISO_VD_PVD) ||
	    (strncmp((const char *)ISO_std_id(bufp), ISO_ID_STRING,
	    ISO_ID_STRLEN) != 0) || (ISO_STD_VER(bufp) != ISO_ID_VER)) {
		dprintf("volume type does not match\n");
		bhsfs_closeall();
		return (-1);
	}

	/* Now we fill in the volume descriptor */
	hsfsp->vol_size = ISO_VOL_SIZE(bufp);
	hsfsp->lbn_size = ISO_BLK_SIZE(bufp);
	hsfsp->lbn_shift = ISO_SECTOR_SHIFT;
	hsfsp->lbn_secshift = ISO_SECTOR_SHIFT;
	hsfsp->vol_set_size = (ushort_t)ISO_SET_SIZE(bufp);
	hsfsp->vol_set_seq = (ushort_t)ISO_SET_SEQ(bufp);

	/* Make sure we have a valid logical block size */
	if (hsfsp->lbn_size & ~(1 << hsfsp->lbn_shift)) {
		printf("%d invalid logical block size\n", hsfsp->lbn_size);
		bhsfs_closeall();
		return (-1);
	}

	/* Since an HSFS root could be located anywhere on the media! */
	root_ino = IDE_EXT_LBN(ISO_root_dir(bufp));
	return (0);
}

static int
bhsfs_unmountroot(void)
{
	if (hsfsp == NULL)
		return (-1);

	bhsfs_closeall();

	return (0);
}

/*
 * Open a file.
 */
/*ARGSUSED*/
int
bhsfs_open(char *str, int flags)
{
	static int filedes = 1;

	fileid_t *filep;
	ino_t ino;

	dprintf("open %s\n", str);
	filep = (fileid_t *)bkmem_alloc(sizeof (fileid_t));
	filep->fi_back = head->fi_back;
	filep->fi_forw = head;
	head->fi_back->fi_forw = filep;
	head->fi_back = filep;
	filep->fi_filedes = filedes++;
	filep->fi_taken = 1;
	filep->fi_path = (char *)bkmem_alloc(strlen(str) + 1);
	(void) strcpy(filep->fi_path, str);
	filep->fi_inode = NULL;
	bzero(filep->fi_buf, MAXBSIZE);

	ino = find(str, filep);
	if (ino == 0) {
		(void) bhsfs_close(filep->fi_filedes);
		return (-1);
	}

	filep->fi_blocknum = hdbtodb(ino);
	filep->fi_offset = 0;
	filep->fi_count = 0;
	filep->fi_memp = 0;

	dprintf("open done\n");
	return (filep->fi_filedes);
}

int
bhsfs_close(int fd)
{
	fileid_t *filep;

	dprintf("close %d\n", fd);
	if (!(filep = find_fp(fd)))
		return (-1);

	if (filep->fi_taken == 0 || filep == head) {
		printf("File descripter %d no allocated!\n", fd);
		return (-1);
	}

	/* unlink and deallocate node */
	filep->fi_forw->fi_back = filep->fi_back;
	filep->fi_back->fi_forw = filep->fi_forw;
	if (filep->fi_inode)
		bkmem_free(filep->fi_inode, sizeof (struct inode));
	bkmem_free(filep->fi_path, strlen(filep->fi_path) + 1);
	bkmem_free((char *)filep, sizeof (fileid_t));
	dprintf("close done\n");
	return (0);
}

static void
bhsfs_closeall(void)
{
	fileid_t *filep;

	while ((filep = head->fi_forw) != head)
		if (filep->fi_taken && bhsfs_close(filep->fi_filedes))
			printf("Filesystem may be inconsistent.\n");

	bkmem_free(hsfsp, sizeof (*hsfsp));
	bkmem_free(head, sizeof (fileid_t));
	hsfsp = NULL;
	head = NULL;
}

/*
 * This version of seek() only performs absolute seeks (whence == 0).
 */
static off_t
bhsfs_lseek(int fd, off_t addr, int whence)
{
	fileid_t *filep;

	dprintf("lseek %d, off = %lx\n", fd, addr);
	if (!(filep = find_fp(fd)))
		return (-1);

	switch (whence) {
	case SEEK_CUR:
		filep->fi_offset += addr;
		break;
	case SEEK_SET:
		filep->fi_offset = addr;
		break;
	default:
	case SEEK_END:
		printf("lseek(): invalid whence value %d\n", whence);
		break;
	}

	filep->fi_blocknum = addr / DEV_BSIZE;
	filep->fi_count = 0;
	return (0);
}

static int
bhsfs_fstat(int fd, struct bootstat *stp)
{
	fileid_t	*filep;
	struct inode	*ip;

	if (!(filep = find_fp(fd)))
		return (-1);

	ip = filep->fi_inode;

	stp->st_mode = 0;
	stp->st_size = 0;

	if (ip == NULL)
		return (0);

	switch (ip->i_smode & IFMT) {
	case IFDIR:
		stp->st_mode = S_IFDIR;
		break;
	case IFREG:
		stp->st_mode = S_IFREG;
		break;
	default:
		break;
	}
	stp->st_size = ip->i_size;

	/* file times */
	stp->st_atim.tv_sec = ip->i_atime.tv_sec;
	stp->st_atim.tv_nsec = ip->i_atime.tv_usec * 1000;
	stp->st_mtim.tv_sec = ip->i_mtime.tv_sec;
	stp->st_mtim.tv_nsec = ip->i_mtime.tv_usec * 1000;
	stp->st_ctim.tv_sec = ip->i_ctime.tv_sec;
	stp->st_ctim.tv_nsec = ip->i_ctime.tv_usec * 1000;

	return (0);

}


/*
 * Parse a directory entry.
 *
 */
static uint_t
parse_dir(fileid_t *filep, int offset, struct hs_direct *hsdep)
{
	char *bufp = (char *)(filep->fi_memp + offset);
	struct direct *udp = &hsdep->hs_ufs_dir;  /* ufs-style dir info */
	struct hs_direntry *hdp = &hsdep->hs_dir; /* hsfs-style dir info */
	uint_t ce_lbn;
	uint_t ce_len;
	uint_t nmlen;
	uint_t i;
	uchar_t c;

	dprintf("parse_dir: offset = %d\n", offset);
	/* a zero length dir entry terminates the dir block */
	udp->d_reclen = IDE_DIR_LEN(bufp);
	if (udp->d_reclen == 0)
		return (0);

	/* fill in some basic hsfs info */
	hdp->ext_lbn  = IDE_EXT_LBN(bufp);
	hdp->ext_size = IDE_EXT_SIZE(bufp);
	hdp->xar_len  = IDE_XAR_LEN(bufp);
	hdp->intlf_sz = IDE_INTRLV_SIZE(bufp);
	hdp->intlf_sk = IDE_INTRLV_SKIP(bufp);
	hdp->sym_link = NULL;

	/* we use lbn of data extent as an inode # equivalent */
	udp->d_ino	= hdp->ext_lbn;

	c = IDE_FLAGS(bufp);
	if (IDE_REGULAR_FILE(c)) {
		hdp->type = VREG;
		hdp->mode = IFREG;
		hdp->nlink = 1;
	} else if (IDE_REGULAR_DIR(c)) {
		hdp->type = VDIR;
		hdp->mode = IFDIR;
		hdp->nlink = 2;
	} else {
		printf("pd(): file type=0x%x unknown.\n", c);
	}

	/*
	 * Massage hsfs name, recognizing special entries for . and ..
	 * else lopping off version junk.
	 */

	/* Some initial conditions */
	nmlen = IDE_NAME_LEN(bufp);
	c = *IDE_NAME(bufp);
	/* Special Case: Current Directory */
	if (nmlen == 1 && c == '\0') {
		udp->d_name[0] = '.';
		udp->d_name[1] = '\0';
		udp->d_namlen = 1;
	/* Special Case: Parent Directory */
	} else if (nmlen == 1 && c == '\001') {
		udp->d_name[0] = '.';
		udp->d_name[1] = '.';
		udp->d_name[2] = '\0';
		udp->d_namlen = 2;
	/* Other file name */
	} else {
		udp->d_namlen = 0;
		for (i = 0; i < nmlen; i++) {
			c = *(IDE_name(bufp)+i);
			if (c == ';')
				break;
			else if (c == ' ')
				continue;
			else
				udp->d_name[udp->d_namlen++] = c;
		}
		udp->d_name[udp->d_namlen] = '\0';
	}

	/* System Use Fields */
	ce_len = IDE_SUA_LEN(bufp);

	if (ce_len == 0)
		return (udp->d_reclen);

	/* there is an SUA for this dir entry; go parse it */
	ce_lbn = parse_susp((char *)IDE_sys_use_area(bufp), &ce_len, hsdep);

	if (ce_lbn) {
		/*
		 * store away current position in dir,
		 * as we will be using the iobuf to reading SUA.
		 */
		daddr_t save_bn = filep->fi_blocknum;
		daddr_t save_offset = filep->fi_offset;
		caddr_t save_ma = filep->fi_memp;
		int save_cc = filep->fi_count;
		do {
			filep->fi_count = ISO_SECTOR_SIZE;
			filep->fi_offset = 0;
			filep->fi_blocknum = hdbtodb(ce_lbn);
			filep->fi_memp = 0;
			if (diskread(filep)) {
				printf("failed to read cont. area\n");
				ce_len = 0;
				ce_lbn = 0;
				break;
			}
			ce_lbn = parse_susp(filep->fi_memp, &ce_len,
			    hsdep);
		} while (ce_lbn);
		filep->fi_count = save_cc;
		filep->fi_offset = save_offset;
		filep->fi_blocknum = save_bn;
		filep->fi_memp = save_ma;
	}
	return (udp->d_reclen);
}

/*
 * Parse the System Use Fields in this System Use Area.
 * Return blk number of continuation/SUA, or 0 if no continuation/not a SUA.
 */
static uint_t
parse_susp(char *bufp, uint_t *len, struct hs_direct *hsdep)
{
	struct direct *udp = &hsdep->hs_ufs_dir; /* ufs-style info */
	char *susp;
	uint_t cur_off = 0;
	uint_t blk_len = *len;
	uint_t susp_len = 0;
	uint_t ce_lbn = 0;
	uint_t i;

	dprintf("parse_susp: len = %d\n", *len);
	while (cur_off < blk_len) {
		susp = (char *)(bufp + cur_off);

		/*
		 * A null entry, or an entry with zero length
		 * terminates the SUSP.
		 */
		if (susp[0] == '\0' || susp[1] == '\0' ||
		    (susp_len = SUF_LEN(susp)) == 0)
			break;

		/*
		 * Compare current entry to all known signatures.
		 */
		for (i = 0; i < hsfs_num_sig; i++)
			if (strncmp(hsfs_sig_tab[i], susp, SUF_SIG_LEN) == 0)
				break;
		switch (i) {
		case SUSP_CE_IX:
			/*
			 * CE signature: continuation of SUSP.
			 * will want to return new lbn, len.
			 */
			ce_lbn = CE_BLK_LOC(susp);
			*len = CE_CONT_LEN(susp);
			break;
		case RRIP_NM_IX:
			/* NM signature: POSIX-style file name */
			if (!RRIP_NAME_FLAGS(susp)) {
				udp->d_namlen = RRIP_NAME_LEN(susp);
				bcopy((char *)RRIP_name(susp),
				    udp->d_name, udp->d_namlen);
				udp->d_name[udp->d_namlen] = '\0';
			}
			break;
		case HSFS_NUM_SIG:
			/* couldn't find a legit susp, terminate loop */
		case SUSP_ST_IX:
			/* ST signature: terminates SUSP */
			return (ce_lbn);
		case SUSP_SP_IX:
		case RRIP_RR_IX:
		default:
			break;
		}
		cur_off += susp_len;
	}
	return (ce_lbn);
}

struct boot_fs_ops bhsfs_ops = {
	"boot_hsfs",
	bhsfs_mountroot,
	bhsfs_unmountroot,
	bhsfs_open,
	bhsfs_close,
	bhsfs_read,
	bhsfs_lseek,
	bhsfs_fstat,
	NULL
};