/*
 * 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 2008 Sun Microsystems, Inc.  All rights reserved.
 * Use is subject to license terms.
 */

/*	Copyright (c) 1983, 1984, 1985, 1986, 1987, 1988, 1989 AT&T	*/
/*	  All Rights Reserved  	*/

/*
 * Portions of this source code were derived from Berkeley 4.3 BSD
 * under license from the Regents of the University of California.
 */

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

/*
 * libfstyp module for pcfs
 */
#include <sys/param.h>
#include <sys/types.h>
#include <sys/mntent.h>
#include <errno.h>
#include <sys/fs/pc_fs.h>
#include <sys/fs/pc_label.h>
#include <sys/fs/pc_dir.h>
#include <sys/stat.h>
#include <sys/vfs.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <strings.h>
#include <sys/mnttab.h>
#include <locale.h>
#include <libfstyp_module.h>

#define	PC_LABEL_SIZE 11

/* for the <sys/fs/pc_dir.h> PCDL_IS_LFN macro */
int	enable_long_filenames = 1;

struct fstyp_fat16_bs {
	uint8_t		f_drvnum;
	uint8_t		f_reserved1;
	uint8_t		f_bootsig;
	uint8_t		f_volid[4];
	uint8_t		f_label[11];
	uint8_t		f_typestring[8];
};

struct fstyp_fat32_bs {
	uint32_t	f_fatlength;
	uint16_t	f_flags;
	uint8_t		f_major;
	uint8_t		f_minor;
	uint32_t	f_rootcluster;
	uint16_t	f_infosector;
	uint16_t	f_backupboot;
	uint8_t		f_reserved2[12];
	uint8_t		f_drvnum;
	uint8_t		f_reserved1;
	uint8_t		f_bootsig;
	uint8_t		f_volid[4];
	uint8_t		f_label[11];
	uint8_t		f_typestring[8];
};

typedef struct fstyp_pcfs {
	int		fd;
	off_t		offset;
	nvlist_t	*attr;
	struct bootsec	bs;
	struct fstyp_fat16_bs bs16;
	struct fstyp_fat32_bs bs32;
	ushort_t	bps;
	int		fattype;
	char		volume_label[PC_LABEL_SIZE + 1];

	/* parameters derived or calculated per FAT spec */
	ulong_t		FATSz;
	ulong_t		TotSec;
	ulong_t		RootDirSectors;
	ulong_t		FirstDataSector;
	ulong_t		DataSec;
	ulong_t		CountOfClusters;
} fstyp_pcfs_t;


/* We should eventually make the structs "packed" so these won't be needed */
#define	PC_BPSEC(h)	ltohs((h)->bs.bps[0])
#define	PC_RESSEC(h)	ltohs((h)->bs.res_sec[0])
#define	PC_NROOTENT(h)	ltohs((h)->bs.rdirents[0])
#define	PC_NSEC(h)	ltohs((h)->bs.numsect[0])
#define	PC_DRVNUM(h)	(FSTYP_IS_32(h) ? (h)->bs32.f_drvnum : \
			    (h)->bs16.f_drvnum)
#define	PC_VOLID(a)	(FSTYP_IS_32(h) ? ltohi((h)->bs32.f_volid[0]) : \
			    ltohi((h)->bs16.f_volid[0]))
#define	PC_LABEL_ADDR(a) (FSTYP_IS_32(h) ? \
			    &((h)->bs32.f_label[0]) : &((h)->bs16.f_label[0]))

#define	FSTYP_IS_32(h)	((h)->fattype == 32)

#define	FSTYP_MAX_CLUSTER_SIZE	(64 * 1024)	/* though officially 32K */
#define	FSTYP_MAX_DIR_SIZE	(65536 * 32)

static int	read_bootsec(fstyp_pcfs_t *h);
static int	valid_media(fstyp_pcfs_t *h);
static int	well_formed(fstyp_pcfs_t *h);
static void	calculate_parameters(fstyp_pcfs_t *h);
static void	determine_fattype(fstyp_pcfs_t *h);
static void	get_label(fstyp_pcfs_t *h);
static void	get_label_16(fstyp_pcfs_t *h);
static void	get_label_32(fstyp_pcfs_t *h);
static int	next_cluster_32(fstyp_pcfs_t *h, int n);
static boolean_t dir_find_label(fstyp_pcfs_t *h, struct pcdir *d, int nent);
static int	is_pcfs(fstyp_pcfs_t *h);
static int	dumpfs(fstyp_pcfs_t *h, FILE *fout, FILE *ferr);
static int	get_attr(fstyp_pcfs_t *h);

int	fstyp_mod_init(int fd, off_t offset, fstyp_mod_handle_t *handle);
void	fstyp_mod_fini(fstyp_mod_handle_t handle);
int	fstyp_mod_ident(fstyp_mod_handle_t handle);
int	fstyp_mod_get_attr(fstyp_mod_handle_t handle, nvlist_t **attrp);
int	fstyp_mod_dump(fstyp_mod_handle_t handle, FILE *fout, FILE *ferr);

int
fstyp_mod_init(int fd, off_t offset, fstyp_mod_handle_t *handle)
{
	struct fstyp_pcfs *h;

	if ((h = calloc(1, sizeof (struct fstyp_pcfs))) == NULL) {
		return (FSTYP_ERR_NOMEM);
	}
	h->fd = fd;
	h->offset = offset;

	*handle = (fstyp_mod_handle_t)h;
	return (0);
}

void
fstyp_mod_fini(fstyp_mod_handle_t handle)
{
	struct fstyp_pcfs *h = (struct fstyp_pcfs *)handle;

	if (h->attr == NULL) {
		nvlist_free(h->attr);
		h->attr = NULL;
	}
	free(h);
}

int
fstyp_mod_ident(fstyp_mod_handle_t handle)
{
	struct fstyp_pcfs *h = (struct fstyp_pcfs *)handle;

	return (is_pcfs(h));
}

int
fstyp_mod_get_attr(fstyp_mod_handle_t handle, nvlist_t **attrp)
{
	struct fstyp_pcfs *h = (struct fstyp_pcfs *)handle;
	int error;

	if (h->attr == NULL) {
		if (nvlist_alloc(&h->attr, NV_UNIQUE_NAME_TYPE, 0)) {
			return (FSTYP_ERR_NOMEM);
		}
		if ((error = get_attr(h)) != 0) {
			nvlist_free(h->attr);
			h->attr = NULL;
			return (error);
		}
	}

	*attrp = h->attr;
	return (0);
}

int
fstyp_mod_dump(fstyp_mod_handle_t handle, FILE *fout, FILE *ferr)
{
	struct fstyp_pcfs *h = (struct fstyp_pcfs *)handle;

	return (dumpfs(h, fout, ferr));
}


/*
 * Read in boot sector. Convert into host endianness where possible.
 */
static int
read_bootsec(fstyp_pcfs_t *h)
{
	char 	buf[PC_SECSIZE];

	(void) lseek(h->fd, h->offset, SEEK_SET);
	if (read(h->fd, buf, sizeof (buf)) != sizeof (buf)) {
		return (FSTYP_ERR_IO);
	}

	bcopy(buf, &h->bs, sizeof (h->bs));
	bcopy(buf + sizeof (struct bootsec), &h->bs16, sizeof (h->bs16));
	bcopy(buf + sizeof (struct bootsec), &h->bs32, sizeof (h->bs32));

	h->bs.fatsec = ltohs(h->bs.fatsec);
	h->bs.spt = ltohs(h->bs.spt);
	h->bs.nhead = ltohs(h->bs.nhead);
	h->bs.hiddensec = ltohi(h->bs.hiddensec);
	h->bs.totalsec = ltohi(h->bs.totalsec);

	h->bs32.f_fatlength = ltohi(h->bs32.f_fatlength);
	h->bs32.f_flags = ltohs(h->bs32.f_flags);
	h->bs32.f_rootcluster = ltohi(h->bs32.f_rootcluster);
	h->bs32.f_infosector = ltohs(h->bs32.f_infosector);
	h->bs32.f_backupboot = ltohs(h->bs32.f_backupboot);

	h->bps = PC_BPSEC(h);

	return (0);
}

static int
valid_media(fstyp_pcfs_t *h)
{
	switch (h->bs.mediadesriptor) {
	case MD_FIXED:
	case SS8SPT:
	case DS8SPT:
	case SS9SPT:
	case DS9SPT:
	case DS18SPT:
	case DS9_15SPT:
		return (1);
	default:
		return (0);
	}
}

static int
well_formed(fstyp_pcfs_t *h)
{
	int fatmatch;

	if (h->bs16.f_bootsig == 0x29) {
		fatmatch = ((h->bs16.f_typestring[0] == 'F' &&
		    h->bs16.f_typestring[1] == 'A' &&
		    h->bs16.f_typestring[2] == 'T') &&
		    (h->bs.fatsec > 0) &&
		    ((PC_NSEC(h) == 0 && h->bs.totalsec > 0) ||
		    PC_NSEC(h) > 0));
	} else if (h->bs32.f_bootsig == 0x29) {
		fatmatch = ((h->bs32.f_typestring[0] == 'F' &&
		    h->bs32.f_typestring[1] == 'A' &&
		    h->bs32.f_typestring[2] == 'T') &&
		    (h->bs.fatsec == 0 && h->bs32.f_fatlength > 0) &&
		    ((PC_NSEC(h) == 0 && h->bs.totalsec > 0) ||
		    PC_NSEC(h) > 0));
	} else {
		fatmatch = (PC_NSEC(h) > 0 && h->bs.fatsec > 0);
	}

	return (fatmatch && h->bps > 0 && h->bps % 512 == 0 &&
	    h->bs.spcl > 0 && PC_RESSEC(h) >= 1 && h->bs.nfat > 0);
}

static void
calculate_parameters(fstyp_pcfs_t *h)
{
	if (PC_NSEC(h) != 0) {
		h->TotSec = PC_NSEC(h);
	} else {
		h->TotSec = h->bs.totalsec;
	}
	if (h->bs.fatsec != 0) {
		h->FATSz = h->bs.fatsec;
	} else {
		h->FATSz = h->bs32.f_fatlength;
	}
	if ((h->bps == 0) || (h->bs.spcl == 0)) {
		return;
	}
	h->RootDirSectors =
	    ((PC_NROOTENT(h) * 32) + (h->bps - 1)) / h->bps;
	h->FirstDataSector =
	    PC_RESSEC(h) + h->bs.nfat * h->FATSz + h->RootDirSectors;
	h->DataSec = h->TotSec - h->FirstDataSector;
	h->CountOfClusters = h->DataSec / h->bs.spcl;
}

static void
determine_fattype(fstyp_pcfs_t *h)
{
	if ((h->CountOfClusters >= 4085 && h->CountOfClusters <= 4095) ||
	    (h->CountOfClusters >= 65525 && h->CountOfClusters <= 65535)) {
		h->fattype = 0;
	} else if (h->CountOfClusters < 4085) {
		h->fattype = 12;
	} else if (h->CountOfClusters < 65525) {
		h->fattype = 16;
	} else {
		h->fattype = 32;
	}
}

static void
get_label(fstyp_pcfs_t *h)
{
	/*
	 * Use label from the boot sector by default.
	 * Can overwrite later with the one from root directory.
	 */
	(void) memcpy(h->volume_label, PC_LABEL_ADDR(h), PC_LABEL_SIZE);
	h->volume_label[PC_LABEL_SIZE] = '\0';

	if (h->fattype == 0) {
		return;
	} else if (FSTYP_IS_32(h)) {
		get_label_32(h);
	} else {
		get_label_16(h);
	}
}

/*
 * Get volume label from the root directory entry.
 * In FAT12/16 the root directory is of fixed size.
 * It immediately follows the FATs
 */
static void
get_label_16(fstyp_pcfs_t *h)
{
	ulong_t	FirstRootDirSecNum;
	int	secsize;
	off_t	offset;
	uint8_t	buf[PC_SECSIZE * 4];
	int	i;
	int	nent, resid;

	if ((secsize = h->bps) > sizeof (buf)) {
		return;
	}

	FirstRootDirSecNum = PC_RESSEC(h) + h->bs.nfat * h->bs.fatsec;
	offset = h->offset + FirstRootDirSecNum * secsize;
	resid = PC_NROOTENT(h);

	for (i = 0; i < h->RootDirSectors; i++) {
		(void) lseek(h->fd, offset, SEEK_SET);
		if (read(h->fd, buf, secsize) != secsize) {
			return;
		}

		nent = secsize / sizeof (struct pcdir);
		if (nent > resid) {
			nent = resid;
		}
		if (dir_find_label(h, (struct pcdir *)buf, nent)) {
			return;
		}

		resid -= nent;
		offset += PC_SECSIZE;
	}
}

/*
 * Get volume label from the root directory entry.
 * In FAT32 root is a usual directory, a cluster chain.
 * It starts at BPB_RootClus.
 */
static void
get_label_32(fstyp_pcfs_t *h)
{
	off_t	offset;
	int	clustersize;
	int	n;
	ulong_t	FirstSectorofCluster;
	uint8_t	*buf;
	int	nent;
	int	cnt = 0;

	clustersize = h->bs.spcl * h->bps;
	if ((clustersize == 0) || (clustersize > FSTYP_MAX_CLUSTER_SIZE) ||
	    ((buf = calloc(1, clustersize)) == NULL)) {
		return;
	}

	for (n = h->bs32.f_rootcluster; n != 0; n = next_cluster_32(h, n)) {
		FirstSectorofCluster =
		    (n - 2) * h->bs.spcl + h->FirstDataSector;
		offset = h->offset + FirstSectorofCluster * h->bps;
		(void) lseek(h->fd, offset, SEEK_SET);
		if (read(h->fd, buf, clustersize) != clustersize) {
			break;
		}

		nent = clustersize / sizeof (struct pcdir);
		if (dir_find_label(h, (struct pcdir *)buf, nent)) {
			break;
		}

		if (++cnt > FSTYP_MAX_DIR_SIZE / clustersize) {
			break;
		}
	}

	free(buf);
}

/*
 * Get a FAT entry pointing to the next file cluster
 */
int
next_cluster_32(fstyp_pcfs_t *h, int n)
{
	uint8_t	buf[PC_SECSIZE];
	ulong_t	ThisFATSecNum;
	ulong_t	ThisFATEntOffset;
	off_t	offset;
	uint32_t val;
	int	next = 0;

	ThisFATSecNum = PC_RESSEC(h) + (n * 4) / h->bps;
	ThisFATEntOffset = (n * 4) % h->bps;
	offset = h->offset + ThisFATSecNum * h->bps;

	(void) lseek(h->fd, offset, SEEK_SET);
	if (read(h->fd, buf, sizeof (buf)) == sizeof (buf)) {
		val = buf[ThisFATEntOffset] & 0x0fffffff;
		next = ltohi(val);
	}

	return (next);
}

/*
 * Given an array of pcdir structs, find one containing volume label.
 */
static boolean_t
dir_find_label(fstyp_pcfs_t *h, struct pcdir *d, int nent)
{
	int	i;

	for (i = 0; i < nent; i++, d++) {
		if (PCDL_IS_LFN(d))
			continue;
		if ((d->pcd_filename[0] != PCD_UNUSED) &&
		    (d->pcd_filename[0] != PCD_ERASED) &&
		    ((d->pcd_attr & (PCA_LABEL | PCA_DIR)) == PCA_LABEL) &&
		    (d->un.pcd_scluster_hi == 0) &&
		    (d->pcd_scluster_lo == 0)) {
			(void) memcpy(h->volume_label, d->pcd_filename,
			    PC_LABEL_SIZE);
			h->volume_label[PC_LABEL_SIZE] = '\0';
			return (B_TRUE);
		}
	}
	return (B_FALSE);
}

static int
is_pcfs(fstyp_pcfs_t *h)
{
	int	error;

	if ((error = read_bootsec(h)) != 0) {
		return (error);
	}
	if (!valid_media(h)) {
		return (FSTYP_ERR_NO_MATCH);
	}
	if (!well_formed(h)) {
		return (FSTYP_ERR_NO_MATCH);
	}

	calculate_parameters(h);
	determine_fattype(h);
	get_label(h);

	return (0);
}

/* ARGSUSED */
static int
dumpfs(fstyp_pcfs_t *h, FILE *fout, FILE *ferr)
{
	(void) fprintf(fout,
	    "Bytes Per Sector  %d\t\tSectors Per Cluster    %d\n",
	    h->bps, h->bs.spcl);
	(void) fprintf(fout,
	    "Reserved Sectors  %d\t\tNumber of FATs         %d\n",
	    (unsigned short)PC_RESSEC(h), h->bs.nfat);
	(void) fprintf(fout,
	    "Root Dir Entries  %d\t\tNumber of Sectors      %d\n",
	    (unsigned short)PC_NROOTENT(h), (unsigned short)PC_NSEC(h));
	(void) fprintf(fout,
	    "Sectors Per FAT   %d\t\tSectors Per Track      %d\n",
	    h->bs.fatsec, h->bs.spt);
	(void) fprintf(fout,
	    "Number of Heads   %d\t\tNumber Hidden Sectors  %d\n",
	    h->bs.nhead, h->bs.hiddensec);
	(void) fprintf(fout, "Volume ID: 0x%x\n", PC_VOLID(h));
	(void) fprintf(fout, "Volume Label: %s\n", h->volume_label);
	(void) fprintf(fout, "Drive Number: 0x%x\n", PC_DRVNUM(h));
	(void) fprintf(fout, "Media Type: 0x%x   ", h->bs.mediadesriptor);

	switch (h->bs.mediadesriptor) {
	case MD_FIXED:
		(void) fprintf(fout, "\"Fixed\" Disk\n");
		break;
	case SS8SPT:
		(void) fprintf(fout, "Single Sided, 8 Sectors Per Track\n");
		break;
	case DS8SPT:
		(void) fprintf(fout, "Double Sided, 8 Sectors Per Track\n");
		break;
	case SS9SPT:
		(void) fprintf(fout, "Single Sided, 9 Sectors Per Track\n");
		break;
	case DS9SPT:
		(void) fprintf(fout, "Double Sided, 9 Sectors Per Track\n");
		break;
	case DS18SPT:
		(void) fprintf(fout, "Double Sided, 18 Sectors Per Track\n");
		break;
	case DS9_15SPT:
		(void) fprintf(fout, "Double Sided, 9-15 Sectors Per Track\n");
		break;
	default:
		(void) fprintf(fout, "Unknown Media Type\n");
	}

	return (0);
}

#define	ADD_STRING(h, name, value) \
	if (nvlist_add_string(h->attr, name, value) != 0) { \
		return (FSTYP_ERR_NOMEM); \
	}

#define	ADD_UINT32(h, name, value) \
	if (nvlist_add_uint32(h->attr, name, value) != 0) { \
		return (FSTYP_ERR_NOMEM); \
	}

#define	ADD_UINT64(h, name, value) \
	if (nvlist_add_uint64(h->attr, name, value) != 0) { \
		return (FSTYP_ERR_NOMEM); \
	}

#define	ADD_BOOL(h, name, value) \
	if (nvlist_add_boolean_value(h->attr, name, value) != 0) { \
		return (FSTYP_ERR_NOMEM); \
	}

static int
get_attr(fstyp_pcfs_t *h)
{
	char	s[64];

	ADD_UINT32(h, "bytes_per_sector", h->bps);
	ADD_UINT32(h, "sectors_per_cluster", h->bs.spcl);
	ADD_UINT32(h, "reserved_sectors", PC_RESSEC(h));
	ADD_UINT32(h, "fats", h->bs.nfat);
	ADD_UINT32(h, "root_entry_count", PC_NROOTENT(h));
	ADD_UINT32(h, "total_sectors_16", PC_NSEC(h));
	ADD_UINT32(h, "media", h->bs.mediadesriptor);
	ADD_UINT32(h, "fat_size_16", h->bs.fatsec);
	ADD_UINT32(h, "sectors_per_track", h->bs.spt);
	ADD_UINT32(h, "heads", h->bs.nhead);
	ADD_UINT32(h, "hidden_sectors", h->bs.hiddensec);
	ADD_UINT32(h, "total_sectors_32", h->bs.totalsec);
	ADD_UINT32(h, "drive_number", PC_DRVNUM(h));
	ADD_UINT32(h, "volume_id", PC_VOLID(h));
	ADD_STRING(h, "volume_label", h->volume_label);
	if (FSTYP_IS_32(h)) {
		ADD_UINT32(h, "fat_size_32", h->bs32.f_fatlength);
	}
	ADD_UINT32(h, "total_sectors", h->TotSec);
	ADD_UINT32(h, "fat_size", h->FATSz);
	ADD_UINT32(h, "count_of_clusters", h->CountOfClusters);
	ADD_UINT32(h, "fat_entry_size", h->fattype);

	ADD_BOOL(h, "gen_clean", B_TRUE);
	if (PC_VOLID(a) != 0) {
		(void) snprintf(s, sizeof (s), "%08x", PC_VOLID(a));
		ADD_STRING(h, "gen_guid", s);
	}
	(void) snprintf(s, sizeof (s), "%d", h->fattype);
	ADD_STRING(h, "gen_version", s);
	ADD_STRING(h, "gen_volume_label", h->volume_label);

	return (0);
}