/*	$NetBSD: exfat.c,v 1.6 2021/09/17 21:06:35 christos Exp $	*/

/*
 * Copyright (c) 2017 Conrad Meyer <cem@FreeBSD.org>
 * All rights reserved.
 *
 * 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 AND CONTRIBUTORS ``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 OR CONTRIBUTORS 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.
 */
#include <sys/cdefs.h>
__RCSID("$NetBSD: exfat.c,v 1.6 2021/09/17 21:06:35 christos Exp $");

#include <sys/param.h>
#include <sys/endian.h>

#include <assert.h>
#include <err.h>
#include <errno.h>
#include <iconv.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "fstyp.h"

/*
 * https://docs.microsoft.com/en-us/windows/win32/fileio/exfat-specification
 */

struct exfat_vbr {
	char		ev_jmp[3];
	char		ev_fsname[8];
	char		ev_zeros[53];
	uint64_t	ev_part_offset;
	uint64_t	ev_vol_length;
	uint32_t	ev_fat_offset;
	uint32_t	ev_fat_length;
	uint32_t	ev_cluster_offset;
	uint32_t	ev_cluster_count;
	uint32_t	ev_rootdir_cluster;
	uint32_t	ev_vol_serial;
	uint16_t	ev_fs_revision;
	uint16_t	ev_vol_flags;
	uint8_t		ev_log_bytes_per_sect;
	uint8_t		ev_log_sect_per_clust;
	uint8_t		ev_num_fats;
	uint8_t		ev_drive_sel;
	uint8_t		ev_percent_used;
} __packed;

struct exfat_dirent {
	uint8_t		xde_type;
#define	XDE_TYPE_INUSE_MASK	0x80	/* 1=in use */
#define	XDE_TYPE_INUSE_SHIFT	7
#define	XDE_TYPE_CATEGORY_MASK	0x40	/* 0=primary */
#define	XDE_TYPE_CATEGORY_SHIFT	6
#define	XDE_TYPE_IMPORTNC_MASK	0x20	/* 0=critical */
#define	XDE_TYPE_IMPORTNC_SHIFT	5
#define	XDE_TYPE_CODE_MASK	0x1f
/* InUse=0, ..., TypeCode=0: EOD. */
#define	XDE_TYPE_EOD		0x00
#define	XDE_TYPE_ALLOC_BITMAP	(XDE_TYPE_INUSE_MASK | 0x01)
#define	XDE_TYPE_UPCASE_TABLE	(XDE_TYPE_INUSE_MASK | 0x02)
#define	XDE_TYPE_VOL_LABEL	(XDE_TYPE_INUSE_MASK | 0x03)
#define	XDE_TYPE_FILE		(XDE_TYPE_INUSE_MASK | 0x05)
#define	XDE_TYPE_VOL_GUID	(XDE_TYPE_INUSE_MASK | XDE_TYPE_IMPORTNC_MASK)
#define	XDE_TYPE_STREAM_EXT	(XDE_TYPE_INUSE_MASK | XDE_TYPE_CATEGORY_MASK)
#define	XDE_TYPE_FILE_NAME	(XDE_TYPE_INUSE_MASK | XDE_TYPE_CATEGORY_MASK | 0x01)
#define	XDE_TYPE_VENDOR		(XDE_TYPE_INUSE_MASK | XDE_TYPE_CATEGORY_MASK | XDE_TYPE_IMPORTNC_MASK)
#define	XDE_TYPE_VENDOR_ALLOC	(XDE_TYPE_INUSE_MASK | XDE_TYPE_CATEGORY_MASK | XDE_TYPE_IMPORTNC_MASK | 0x01)
	union {
		uint8_t	xde_generic_[19];
		struct exde_primary {
			/*
			 * Count of "secondary" dirents following this one.
			 *
			 * A single logical entity may be composed of a
			 * sequence of several dirents, starting with a primary
			 * one; the rest are secondary dirents.
			 */
			uint8_t		xde_secondary_count_;
			uint16_t	xde_set_chksum_;
			uint16_t	xde_prim_flags_;
			uint8_t		xde_prim_generic_[14];
		} __packed xde_primary_;
		struct exde_secondary {
			uint8_t		xde_sec_flags_;
			uint8_t		xde_sec_generic_[18];
		} __packed xde_secondary_;
	} u;
	uint32_t	xde_first_cluster;
	uint64_t	xde_data_len;
} __packed;
#define	xde_generic		u.xde_generic_
#define	xde_secondary_count	u.xde_primary_.xde_secondary_count
#define	xde_set_chksum		u.xde_primary_.xde_set_chksum_
#define	xde_prim_flags		u.xde_primary_.xde_prim_flags_
#define	xde_sec_flags		u.xde_secondary_.xde_sec_flags_
_Static_assert(sizeof(struct exfat_dirent) == 32, "spec");

struct exfat_de_label {
	uint8_t		xdel_type;	/* XDE_TYPE_VOL_LABEL */
	uint8_t		xdel_char_cnt;	/* Length of UCS-2 label */
	uint16_t	xdel_vol_lbl[11];
	uint8_t		xdel_reserved[8];
} __packed;
_Static_assert(sizeof(struct exfat_de_label) == 32, "spec");

#define	MAIN_BOOT_REGION_SECT	0
#define	BACKUP_BOOT_REGION_SECT	12

#define	SUBREGION_CHKSUM_SECT	11

#define	FIRST_CLUSTER		2
#define	BAD_BLOCK_SENTINEL	0xfffffff7u
#define	END_CLUSTER_SENTINEL	0xffffffffu

static inline void *
read_sectn(FILE *fp, off_t sect, unsigned count, unsigned bytespersec)
{
	return (read_buf(fp, sect * bytespersec, bytespersec * count));
}

static inline void *
read_sect(FILE *fp, off_t sect, unsigned bytespersec)
{
	return (read_sectn(fp, sect, 1, bytespersec));
}

/*
 * Compute the byte-by-byte multi-sector checksum of the given boot region
 * (MAIN or BACKUP), for a given bytespersec (typically 512 or 4096).
 *
 * Endian-safe; result is host endian.
 */
static int
exfat_compute_boot_chksum(FILE *fp, unsigned region, unsigned bytespersec,
    uint32_t *result)
{
	unsigned char *sector;
	unsigned n, sect;
	uint32_t checksum;

	checksum = 0;
	for (sect = 0; sect < 11; sect++) {
		sector = read_sect(fp, region + sect, bytespersec);
		if (sector == NULL)
			return (ENXIO);
		for (n = 0; n < bytespersec; n++) {
			if (sect == 0) {
				switch (n) {
				case 106:
				case 107:
				case 112:
					continue;
				}
			}
			checksum = ((checksum & 1) ? 0x80000000u : 0u) +
			    (checksum >> 1) + (uint32_t)sector[n];
		}
		free(sector);
	}

	*result = checksum;
	return (0);
}

static void
convert_label(const uint16_t *ucs2label /* LE */, unsigned ucs2len, char
    *label_out, size_t label_sz)
{
	const char *label;
	char *label_out_orig;
	iconv_t cd;
	size_t srcleft, rc;

	/* Currently hardcoded in fstyp.c as 256 or so. */
	assert(label_sz > 1);

	if (ucs2len == 0) {
		/*
		 * Kind of seems bogus, but the spec allows an empty label
		 * entry with the same meaning as no label.
		 */
		return;
	}

	if (ucs2len > 11) {
		warnx("exfat: Bogus volume label length: %u", ucs2len);
		return;
	}

	/* dstname="" means convert to the current locale. */
	cd = iconv_open("", EXFAT_ENC);
	if (cd == (iconv_t)-1) {
		warn("exfat: Could not open iconv");
		return;
	}

	label_out_orig = label_out;

	/* Dummy up the byte pointer and byte length iconv's API wants. */
	label = (const void *)ucs2label;
	srcleft = ucs2len * sizeof(*ucs2label);

	rc = iconv(cd, __UNCONST(&label), &srcleft, &label_out,
	    &label_sz);
	if (rc == (size_t)-1) {
		warn("exfat: iconv()");
		*label_out_orig = '\0';
	} else {
		/* NUL-terminate result (iconv advances label_out). */
		if (label_sz == 0)
			label_out--;
		*label_out = '\0';
	}

	iconv_close(cd);
}

/*
 * Using the FAT table, look up the next cluster in this chain.
 */
static uint32_t
exfat_fat_next(FILE *fp, const struct exfat_vbr *ev, unsigned BPS,
    uint32_t cluster)
{
	uint32_t fat_offset_sect, clsect, clsectoff;
	uint32_t *fatsect, nextclust;

	fat_offset_sect = le32toh(ev->ev_fat_offset);
	clsect = fat_offset_sect + (cluster / (BPS / (uint32_t)sizeof(cluster)));
	clsectoff = (cluster % (BPS / (uint32_t)sizeof(cluster)));

	/* XXX This is pretty wasteful without a block cache for the FAT. */
	fatsect = read_sect(fp, clsect, BPS);
	nextclust = le32toh(fatsect[clsectoff]);
	free(fatsect);

	return (nextclust);
}

static void
exfat_find_label(FILE *fp, const struct exfat_vbr *ev, unsigned BPS,
    char *label_out, size_t label_sz)
{
	uint32_t rootdir_cluster, sects_per_clust, cluster_offset_sect;
	off_t rootdir_sect;
	struct exfat_dirent *declust, *it;

	cluster_offset_sect = le32toh(ev->ev_cluster_offset);
	rootdir_cluster = le32toh(ev->ev_rootdir_cluster);
	sects_per_clust = (1u << ev->ev_log_sect_per_clust);

	if (rootdir_cluster < FIRST_CLUSTER) {
		warnx("%s: invalid rootdir cluster %u < %d", __func__,
		    rootdir_cluster, FIRST_CLUSTER);
		return;
	}


	for (; rootdir_cluster != END_CLUSTER_SENTINEL;
	    rootdir_cluster = exfat_fat_next(fp, ev, BPS, rootdir_cluster)) {
		if (rootdir_cluster == BAD_BLOCK_SENTINEL) {
			warnx("%s: Bogus bad block in root directory chain",
			    __func__);
			return;
		}

		rootdir_sect = (rootdir_cluster - FIRST_CLUSTER) *
		    sects_per_clust + cluster_offset_sect;
		declust = read_sectn(fp, rootdir_sect, sects_per_clust, BPS);
		for (it = declust;
		    it < declust + (sects_per_clust * BPS / sizeof(*it)); it++) {
			bool eod = false;

			/*
			 * Simplistic directory traversal; doesn't do any
			 * validation of "MUST" requirements in spec.
			 */
			switch (it->xde_type) {
			case XDE_TYPE_EOD:
				eod = true;
				break;
			case XDE_TYPE_VOL_LABEL: {
				struct exfat_de_label *lde = (void*)it;
				convert_label(lde->xdel_vol_lbl,
				    lde->xdel_char_cnt, label_out, label_sz);
				free(declust);
				return;
				}
			}

			if (eod)
				break;
		}
		free(declust);
	}
}

int
fstyp_exfat(FILE *fp, char *label, size_t size)
{
	struct exfat_vbr *ev;
	uint32_t *cksect;
	unsigned bytespersec;
	uint32_t chksum;
	int error;

	error = 1;
	cksect = NULL;
	ev = (struct exfat_vbr *)read_buf(fp, 0, 512);
	if (ev == NULL || strncmp(ev->ev_fsname, "EXFAT   ", 8) != 0)
		goto out;

	if (ev->ev_log_bytes_per_sect < 9 || ev->ev_log_bytes_per_sect > 12) {
		warnx("exfat: Invalid BytesPerSectorShift");
		goto out;
	}

	bytespersec = (1u << ev->ev_log_bytes_per_sect);

	error = exfat_compute_boot_chksum(fp, MAIN_BOOT_REGION_SECT,
	    bytespersec, &chksum);
	if (error != 0)
		goto out;

	cksect = read_sect(fp, MAIN_BOOT_REGION_SECT + SUBREGION_CHKSUM_SECT,
	    bytespersec);

	/*
	 * Technically the entire sector should be full of repeating 4-byte
	 * checksum pattern, but we only verify the first.
	 */
	if (chksum != le32toh(cksect[0])) {
		warnx("exfat: Found checksum 0x%08x != computed 0x%08x",
		    le32toh(cksect[0]), chksum);
		error = 1;
		goto out;
	}

	if (show_label)
		exfat_find_label(fp, ev, bytespersec, label, size);

out:
	free(cksect);
	free(ev);
	return (error != 0);
}
