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

/*
* Copyright (c) 2017 Conrad Meyer <[email protected]>
* 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);
}