fstyp(8): Show exFAT volume labels with -l flag
exfat is fundamentally the same design as fat32. The superblock differs marginally, and there are some additional optional features irrelevant to fstype(8); the structure of dirents has changed slightly to enable, among other things, larger files; the directory entries are no longer DOS 8.3 ASCII or local 8-bit encoding, but instead explicitly UCS-2-LE. (As a result, this change uses iconv to convert a found exfat volume label to the user's locale.) Locating the volume label is identical to FAT32: locate the root directory and walk through dirents until you find a volume label. Like FAT32, follow the FAT chain between root directory clusters as necessary. PR: 242225 Reported by: Victor Sudakov <vas AT sibptus.ru>
This commit is contained in:
parent
9f0058d673
commit
73773fcda9
@ -27,6 +27,14 @@
|
||||
#include <sys/cdefs.h>
|
||||
__FBSDID("$FreeBSD$");
|
||||
|
||||
#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>
|
||||
@ -34,6 +42,10 @@ __FBSDID("$FreeBSD$");
|
||||
|
||||
#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];
|
||||
@ -55,19 +67,300 @@ struct exfat_vbr {
|
||||
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, __DECONST(char **, &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 / sizeof(cluster)));
|
||||
clsectoff = (cluster % (BPS / 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;
|
||||
|
||||
cksect = NULL;
|
||||
ev = (struct exfat_vbr *)read_buf(fp, 0, 512);
|
||||
if (ev == NULL || strncmp(ev->ev_fsname, "EXFAT ", 8) != 0)
|
||||
goto fail;
|
||||
|
||||
if (ev->ev_log_bytes_per_sect < 9 || ev->ev_log_bytes_per_sect > 12) {
|
||||
warnx("exfat: Invalid BytesPerSectorShift");
|
||||
goto done;
|
||||
}
|
||||
|
||||
bytespersec = (1u << ev->ev_log_bytes_per_sect);
|
||||
|
||||
error = exfat_compute_boot_chksum(fp, MAIN_BOOT_REGION_SECT,
|
||||
bytespersec, &chksum);
|
||||
if (error != 0)
|
||||
goto done;
|
||||
|
||||
cksect = read_sect(fp, MAIN_BOOT_REGION_SECT + SUBREGION_CHKSUM_SECT,
|
||||
bytespersec);
|
||||
|
||||
/*
|
||||
* Reading the volume label requires walking the root directory to look
|
||||
* for a special label file. Left as an exercise for the reader.
|
||||
* 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);
|
||||
goto done;
|
||||
}
|
||||
|
||||
if (show_label)
|
||||
exfat_find_label(fp, ev, bytespersec, label, size);
|
||||
|
||||
done:
|
||||
free(cksect);
|
||||
free(ev);
|
||||
return (0);
|
||||
|
||||
|
@ -38,6 +38,8 @@ __FBSDID("$FreeBSD$");
|
||||
#include <capsicum_helpers.h>
|
||||
#include <err.h>
|
||||
#include <errno.h>
|
||||
#include <iconv.h>
|
||||
#include <locale.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
@ -50,24 +52,27 @@ __FBSDID("$FreeBSD$");
|
||||
|
||||
#define LABEL_LEN 256
|
||||
|
||||
bool show_label = false;
|
||||
|
||||
typedef int (*fstyp_function)(FILE *, char *, size_t);
|
||||
|
||||
static struct {
|
||||
const char *name;
|
||||
fstyp_function function;
|
||||
bool unmountable;
|
||||
char *precache_encoding;
|
||||
} fstypes[] = {
|
||||
{ "cd9660", &fstyp_cd9660, false },
|
||||
{ "exfat", &fstyp_exfat, false },
|
||||
{ "ext2fs", &fstyp_ext2fs, false },
|
||||
{ "geli", &fstyp_geli, true },
|
||||
{ "msdosfs", &fstyp_msdosfs, false },
|
||||
{ "ntfs", &fstyp_ntfs, false },
|
||||
{ "ufs", &fstyp_ufs, false },
|
||||
{ "cd9660", &fstyp_cd9660, false, NULL },
|
||||
{ "exfat", &fstyp_exfat, false, EXFAT_ENC },
|
||||
{ "ext2fs", &fstyp_ext2fs, false, NULL },
|
||||
{ "geli", &fstyp_geli, true, NULL },
|
||||
{ "msdosfs", &fstyp_msdosfs, false, NULL },
|
||||
{ "ntfs", &fstyp_ntfs, false, NULL },
|
||||
{ "ufs", &fstyp_ufs, false, NULL },
|
||||
#ifdef HAVE_ZFS
|
||||
{ "zfs", &fstyp_zfs, true },
|
||||
{ "zfs", &fstyp_zfs, true, NULL },
|
||||
#endif
|
||||
{ NULL, NULL, NULL }
|
||||
{ NULL, NULL, NULL, NULL }
|
||||
};
|
||||
|
||||
void *
|
||||
@ -159,7 +164,7 @@ int
|
||||
main(int argc, char **argv)
|
||||
{
|
||||
int ch, error, i, nbytes;
|
||||
bool ignore_type = false, show_label = false, show_unmountable = false;
|
||||
bool ignore_type = false, show_unmountable = false;
|
||||
char label[LABEL_LEN + 1], strvised[LABEL_LEN * 4 + 1];
|
||||
char *path;
|
||||
FILE *fp;
|
||||
@ -188,6 +193,26 @@ main(int argc, char **argv)
|
||||
|
||||
path = argv[0];
|
||||
|
||||
if (setlocale(LC_CTYPE, "") == NULL)
|
||||
err(1, "setlocale");
|
||||
caph_cache_catpages();
|
||||
|
||||
/* Cache iconv conversion data before entering capability mode. */
|
||||
if (show_label) {
|
||||
for (i = 0; i < nitems(fstypes); i++) {
|
||||
iconv_t cd;
|
||||
|
||||
if (fstypes[i].precache_encoding == NULL)
|
||||
continue;
|
||||
cd = iconv_open("", fstypes[i].precache_encoding);
|
||||
if (cd == (iconv_t)-1)
|
||||
err(1, "%s: iconv_open %s", fstypes[i].name,
|
||||
fstypes[i].precache_encoding);
|
||||
/* Iconv keeps a small cache of unused encodings. */
|
||||
iconv_close(cd);
|
||||
}
|
||||
}
|
||||
|
||||
fp = fopen(path, "r");
|
||||
if (fp == NULL)
|
||||
err(1, "%s", path);
|
||||
|
@ -32,8 +32,15 @@
|
||||
#ifndef FSTYP_H
|
||||
#define FSTYP_H
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
#define MIN(a,b) (((a)<(b))?(a):(b))
|
||||
|
||||
/* The spec doesn't seem to permit UTF-16 surrogates; definitely LE. */
|
||||
#define EXFAT_ENC "UCS-2LE"
|
||||
|
||||
extern bool show_label; /* -l flag */
|
||||
|
||||
void *read_buf(FILE *fp, off_t off, size_t len);
|
||||
char *checked_strdup(const char *s);
|
||||
void rtrim(char *label, size_t size);
|
||||
|
@ -68,6 +68,15 @@ exfat_body() {
|
||||
atf_check -s exit:0 -o inline:"exfat\n" fstyp -u exfat.img
|
||||
}
|
||||
|
||||
atf_test_case exfat_label
|
||||
exfat_label_head() {
|
||||
atf_set "descr" "fstyp(8) can read exFAT labels"
|
||||
}
|
||||
exfat_label_body() {
|
||||
bzcat $(atf_get_srcdir)/dfr-01-xfat.img.bz2 > exfat.img
|
||||
atf_check -s exit:0 -o inline:"exfat exFat\n" fstyp -u -l exfat.img
|
||||
}
|
||||
|
||||
atf_test_case empty
|
||||
empty_head() {
|
||||
atf_set "descr" "fstyp(8) should fail on an empty file"
|
||||
@ -253,6 +262,7 @@ atf_init_test_cases() {
|
||||
atf_add_test_case dir
|
||||
atf_add_test_case empty
|
||||
atf_add_test_case exfat
|
||||
atf_add_test_case exfat_label
|
||||
atf_add_test_case ext2
|
||||
atf_add_test_case ext3
|
||||
atf_add_test_case ext4
|
||||
|
Loading…
Reference in New Issue
Block a user