diff --git a/Makefile b/Makefile index 829eb5a..6173fb4 100644 --- a/Makefile +++ b/Makefile @@ -26,6 +26,16 @@ ifneq ($(OS), Linux) LDFLAGS += -L$(ARGP)/lib -largp endif +ifeq ($(OS), Darwin) + LDLIBS_UDEV = + F3FIX_OBJS = $(BUILD_DIR)/libutils.o $(BUILD_DIR)/f3fix_darwin.o + LDLIBS_F3FIX = +else + LDLIBS_UDEV = -ludev + F3FIX_OBJS = $(BUILD_DIR)/libutils.o $(BUILD_DIR)/f3fix.o + LDLIBS_F3FIX = -lparted +endif + all: $(TARGETS) extra: $(EXTRA_TARGETS) @@ -64,13 +74,13 @@ $(BUILD_DIR)/f3read: $(BUILD_DIR)/libutils.o $(BUILD_DIR)/libfile.o $(BUILD_DIR) $(CC) -o $@ $^ $(LDFLAGS) -lm $(BUILD_DIR)/f3probe: $(BUILD_DIR)/libutils.o $(BUILD_DIR)/libflow.o $(BUILD_DIR)/libdevs.o $(BUILD_DIR)/libprobe.o $(BUILD_DIR)/f3probe.o - $(CC) -o $@ $^ $(LDFLAGS) -lm -ludev + $(CC) -o $@ $^ $(LDFLAGS) -lm $(LDLIBS_UDEV) $(BUILD_DIR)/f3brew: $(BUILD_DIR)/libutils.o $(BUILD_DIR)/libflow.o $(BUILD_DIR)/libdevs.o $(BUILD_DIR)/f3brew.o - $(CC) -o $@ $^ $(LDFLAGS) -lm -ludev + $(CC) -o $@ $^ $(LDFLAGS) -lm $(LDLIBS_UDEV) -$(BUILD_DIR)/f3fix: $(BUILD_DIR)/libutils.o $(BUILD_DIR)/f3fix.o - $(CC) -o $@ $^ $(LDFLAGS) -lparted +$(BUILD_DIR)/f3fix: $(F3FIX_OBJS) + $(CC) -o $@ $^ $(LDFLAGS) $(LDLIBS_F3FIX) -include $(BUILD_DIR)/*.d diff --git a/src/f3fix_darwin.c b/src/f3fix_darwin.c new file mode 100644 index 0000000..d426cc5 --- /dev/null +++ b/src/f3fix_darwin.c @@ -0,0 +1,259 @@ +/* + * f3fix for macOS — create a partition table sized to a drive's real capacity. + * + * The Linux f3fix uses libparted, which is not available on macOS. + * Fixing a fake drive only ever needs a single MBR partition whose last + * sector is the last *good* sector reported by f3probe, so this + * implementation writes the MBR directly and needs no library at all. + * + * Usage mirrors the Linux tool: + * sudo f3fix --last-sec=SEC /dev/rdiskN + * + * Copyright (C) 2010 Digirati Internet LTDA. + * This file is part of F3. F3 is free software: GPLv3 or later. + */ + +#define _POSIX_C_SOURCE 200809L +#define _FILE_OFFSET_BITS 64 + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "version.h" +#include "libutils.h" + +/* MBR partition type bytes for the filesystems f3fix supports. */ +struct fs_type_entry { + const char *name; + uint8_t mbr_type; +}; + +static const struct fs_type_entry fs_types[] = { + {"fat32", 0x0C}, /* FAT32 LBA */ + {"fat16", 0x0E}, /* FAT16 LBA */ + {"exfat", 0x07}, + {"ntfs", 0x07}, + {NULL, 0} +}; + +static const char doc[] = "F3 Fix -- edit the partition table of " + "a fake flash drive to have a single partition that fully covers " + "the real capacity of the drive (macOS backend, MBR only)"; + +static struct argp_option options[] = { + {"disk-type", 'd', "TYPE", 0, + "Disk type of the partition table; only `msdos' (MBR) is supported on macOS", 2}, + {"fs-type", 'f', "TYPE", 0, + "Type of the file system of the partition: fat32, fat16, exfat, ntfs", 0}, + {"boot", 'b', NULL, 0, "Mark the partition for boot", 0}, + {"no-boot", 'n', NULL, 0, "Do not mark the partition for boot", 0}, + {"first-sec", 'a', "SEC", 0, "Sector where the partition starts", 0}, + {"last-sec", 'l', "SEC", 0, "Sector where the partition ends", 0}, + { 0 } +}; + +struct args { + const char *dev_filename; + uint8_t fs_mbr_type; + bool boot; + uint64_t first_sec; + uint64_t last_sec; + bool last_sec_set; +}; + +static uint8_t lookup_fs_type(const char *name) +{ + const struct fs_type_entry *e; + for (e = fs_types; e->name; e++) + if (!strcasecmp(e->name, name)) + return e->mbr_type; + return 0; +} + +static error_t parse_opt(int key, char *arg, struct argp_state *state) +{ + struct args *args = state->input; + long long ll; + + switch (key) { + case 'd': + if (strcasecmp(arg, "msdos")) + argp_error(state, + "only `msdos' (MBR) partition tables are supported on macOS; GPT is unnecessary for fixing fake drives"); + break; + case 'f': + args->fs_mbr_type = lookup_fs_type(arg); + if (!args->fs_mbr_type) + argp_error(state, + "file system type `%s' is not supported; use fat32, fat16, exfat, or ntfs", arg); + break; + case 'b': + args->boot = true; + break; + case 'n': + args->boot = false; + break; + case 'a': + ll = arg_to_ll_bytes(state, arg); + if (ll < 0) + argp_error(state, "the first sector must be at least 0"); + args->first_sec = ll; + break; + case 'l': + ll = arg_to_ll_bytes(state, arg); + if (ll < 0) + argp_error(state, "the last sector must be at least 0"); + args->last_sec = ll; + args->last_sec_set = true; + break; + case ARGP_KEY_ARG: + if (args->dev_filename) + argp_error(state, "too many arguments; only one device is expected"); + args->dev_filename = arg; + break; + case ARGP_KEY_END: + if (!args->dev_filename) + argp_error(state, "the disk device was not specified"); + if (!args->last_sec_set) + argp_error(state, "option --last-sec is required"); + if (args->first_sec > args->last_sec) + argp_error(state, "the first sector (%" PRIu64 ") must be less than or equal to the last sector (%" PRIu64 ")", + args->first_sec, args->last_sec); + break; + default: + return ARGP_ERR_UNKNOWN; + } + return 0; +} + +static struct argp argp = {options, parse_opt, "", doc, NULL, NULL, NULL}; + +static void write_le32(uint8_t *p, uint32_t v) +{ + p[0] = v & 0xFF; + p[1] = (v >> 8) & 0xFF; + p[2] = (v >> 16) & 0xFF; + p[3] = (v >> 24) & 0xFF; +} + +static int fix_disk(const char *filename, uint8_t fs_type, bool boot, + uint64_t first_sec, uint64_t last_sec) +{ + uint8_t mbr[512]; + uint8_t zero_lba1[512]; + uint8_t *entry = &mbr[446]; + uint64_t sectors = last_sec - first_sec + 1; + uint64_t dev_block_count = 0; + uint32_t dev_block_size = 0; + int fd; + ssize_t written; + + if (first_sec > UINT32_MAX || sectors > UINT32_MAX) { + warnx("partition does not fit in an MBR (first sector %" PRIu64 ", %" PRIu64 " sectors)", + first_sec, sectors); + return 1; + } + + memset(mbr, 0, sizeof(mbr)); + + entry[0] = boot ? 0x80 : 0x00; + /* CHS fields set to the conventional LBA placeholders. */ + entry[1] = 0xFE; entry[2] = 0xFF; entry[3] = 0xFF; + entry[4] = fs_type; + entry[5] = 0xFE; entry[6] = 0xFF; entry[7] = 0xFF; + write_le32(&entry[8], (uint32_t)first_sec); + write_le32(&entry[12], (uint32_t)sectors); + + mbr[510] = 0x55; + mbr[511] = 0xAA; + + fd = open(filename, O_RDWR); + if (fd < 0) { + if (errno == EBUSY) + warnx("device `%s' is busy; unmount it first:\n" + "diskutil unmountDisk %s", filename, filename); + else if (errno == EACCES && getuid()) + warnx("your user doesn't have access to device `%s'; run with sudo", + filename); + else + warn("can't open device `%s'", filename); + return 1; + } + + /* Refuse to create a partition that runs past the device. + * (The Linux backend gets this check from libparted.) + */ + if (!ioctl(fd, DKIOCGETBLOCKCOUNT, &dev_block_count) && + !ioctl(fd, DKIOCGETBLOCKSIZE, &dev_block_size) && + dev_block_size == 512 && last_sec >= dev_block_count) { + warnx("the last sector (%" PRIu64 ") is beyond the end of device `%s' (%" PRIu64 " sectors)", + last_sec, filename, dev_block_count); + close(fd); + return 1; + } + + written = pwrite(fd, mbr, sizeof(mbr), 0); + if (written != (ssize_t)sizeof(mbr)) { + warn("can't write the partition table to `%s'", filename); + close(fd); + return 1; + } + + /* Zero LBA 1 so a leftover GPT header does not shadow the new MBR; + * tools that support GPT prefer it over the MBR when both look valid. + */ + memset(zero_lba1, 0, sizeof(zero_lba1)); + written = pwrite(fd, zero_lba1, sizeof(zero_lba1), 512); + if (written != (ssize_t)sizeof(zero_lba1)) { + warn("can't clear the old GPT header on `%s'", filename); + close(fd); + return 1; + } + if (fsync(fd)) { + warn("can't flush the partition table to `%s'", filename); + close(fd); + return 1; + } + close(fd); + return 0; +} + +int main(int argc, char **argv) +{ + struct args args = { + .dev_filename = NULL, + .fs_mbr_type = 0x0C, /* fat32 */ + .boot = true, + .first_sec = 2048, /* 1MB-aligned */ + .last_sec = 0, + .last_sec_set = false, + }; + int rc; + + argp_parse(&argp, argc, argv, 0, NULL, &args); + + rc = fix_disk(args.dev_filename, args.fs_mbr_type, args.boot, + args.first_sec, args.last_sec); + + if (!rc) { + printf("Drive `%s' was successfully fixed\n\n" + "Now run on macOS:\n" + "diskutil eraseDisk ExFAT NAME %s\n" + "or remove and reinsert the drive and format it " + "with Disk Utility.\n", + args.dev_filename, args.dev_filename); + } + return rc; +} diff --git a/src/libdevs.c b/src/libdevs.c index 7348d8c..cd20118 100644 --- a/src/libdevs.c +++ b/src/libdevs.c @@ -1,5 +1,8 @@ #define _GNU_SOURCE #define _POSIX_C_SOURCE 200809L +#ifdef __APPLE__ +#define _DARWIN_C_SOURCE +#endif #define _FILE_OFFSET_BITS 64 #include @@ -16,9 +19,13 @@ #include #include #include +#ifdef __APPLE__ +#include +#else #include #include #include +#endif #include "libutils.h" #include "libdevs.h" @@ -466,14 +473,154 @@ static int bdev_write_blocks(struct device *dev, const char *buf, rc = fsync(bdev->fd); if (rc) return rc; +#ifdef __APPLE__ + /* The file descriptor is opened with F_NOCACHE (see bdev_open()), + * so there is no page cache to drop. + */ + return 0; +#else return posix_fadvise(bdev->fd, 0, 0, POSIX_FADV_DONTNEED); +#endif } static inline int bdev_open(const char *filename) { +#ifdef __APPLE__ + /* macOS has no O_DIRECT; F_NOCACHE turns the page cache off for + * this descriptor, which is what the probe algorithm requires. + * Prefer the raw device (/dev/rdiskN) for unbuffered access. + */ + int fd = open(filename, O_RDWR); + if (fd >= 0 && fcntl(fd, F_NOCACHE, 1)) + warn("Can't disable caching for device `%s'", filename); + return fd; +#else return open(filename, O_RDWR | O_DIRECT); +#endif +} + +#ifdef __APPLE__ + +static void msleep_ms(unsigned int ms) +{ + struct timespec ts = { .tv_sec = ms / 1000, + .tv_nsec = (long)(ms % 1000) * 1000000L }; + nanosleep(&ts, NULL); +} + +static int apple_dev_size_byte(int fd, uint64_t *size_byte) +{ + uint64_t block_count; + uint32_t block_size; + if (ioctl(fd, DKIOCGETBLOCKCOUNT, &block_count)) + return - errno; + if (ioctl(fd, DKIOCGETBLOCKSIZE, &block_size)) + return - errno; + *size_byte = block_count * block_size; + return 0; +} + +static int apple_dev_block_size(int fd, int *block_size) +{ + uint32_t bs; + if (ioctl(fd, DKIOCGETBLOCKSIZE, &bs)) + return - errno; + *block_size = bs; + return 0; +} + +/* Return true when @filename names a whole disk (e.g. /dev/rdisk5), + * false when it names a partition slice (e.g. /dev/rdisk5s2) or + * something else entirely. + */ +static bool apple_is_whole_disk(const char *filename, char *disk_name, + size_t disk_name_len) +{ + const char *p = filename; + const char *num_start; + if (strncmp(p, "/dev/", 5)) + return false; + p += 5; + if (*p == 'r') + p++; + if (strncmp(p, "disk", 4)) + return false; + p += 4; + num_start = p; + while (*p >= '0' && *p <= '9') + p++; + if (p == num_start) + return false; + if (*p == '\0') + return true; + if (*p == 's') { + /* Partition slice; report the whole-disk name. */ + snprintf(disk_name, disk_name_len, "/dev/rdisk%.*s", + (int)(p - num_start), num_start); + } + return false; +} + +/* Manual reset without libudev: wait for the device node to disappear, + * then to reappear with the same size, and reopen it. + * + * Note that macOS may renumber the disk if other devices enumerate + * meanwhile; in that case this call times out and the caller should + * run the tool again. + */ +static int bdev_manual_usb_reset(struct device *dev) +{ + struct block_device *bdev = dev_bdev(dev); + uint64_t orig_size = dev_get_size_byte(dev); + int waited_ms; + + if (bdev->fd < 0) + return - EBADF; + assert(!close(bdev->fd)); + bdev->fd = -1; + + printf("Please unplug and plug back the USB drive. Waiting..."); + fflush(stdout); + + /* Wait for the device to go away. */ + while (1) { + struct stat st; + if (stat(bdev->filename, &st)) + break; + msleep_ms(200); + } + + /* Wait up to 60s for it to come back with the same size. */ + for (waited_ms = 0; waited_ms < 60000; waited_ms += 200) { + int fd = bdev_open(bdev->filename); + if (fd >= 0) { + uint64_t size_byte; + if (!apple_dev_size_byte(fd, &size_byte) && + size_byte == orig_size) { + bdev->fd = fd; + printf(" Thanks\n\n"); + return 0; + } + close(fd); + } + msleep_ms(200); + } + warnx("Device `%s' did not come back after the reset; " + "macOS may have renumbered the disk. Check with " + "`diskutil list' and run again.", bdev->filename); + return - ENODEV; } +static int bdev_usb_reset(struct device *dev) +{ + (void)dev; + warnx("Software USB reset is not supported on macOS; " + "use the manual reset (unplug/replug) instead"); + return - EOPNOTSUPP; +} + +#else /* !__APPLE__ */ + static struct udev_device *map_dev_to_usb_dev(struct udev_device *dev) { struct udev_device *usb_dev; @@ -815,6 +962,9 @@ static int bdev_usb_reset(struct device *dev) return 0; } + +#endif /* __APPLE__ */ + static int bdev_none_reset(struct device *dev) { UNUSED(dev); @@ -834,6 +984,7 @@ static const char *bdev_get_filename(struct device *dev) return dev_bdev(dev)->filename; } +#ifndef __APPLE__ static struct udev_device *map_partition_to_disk(struct udev_device *dev) { struct udev_device *disk_dev; @@ -848,6 +999,7 @@ static struct udev_device *map_partition_to_disk(struct udev_device *dev) */ return udev_device_ref(disk_dev); } +#endif /* !__APPLE__ */ /* XXX This is borrowing from glibc. * A better solution would be to return proper errors, @@ -858,9 +1010,11 @@ extern const char *__progname; struct device *create_block_device(const char *filename, enum reset_type rt) { struct block_device *bdev; +#ifndef __APPLE__ struct udev *udev; struct udev_device *fd_dev; const char *s; +#endif int block_size, block_order; bdev = malloc(sizeof(*bdev)); @@ -886,6 +1040,28 @@ struct device *create_block_device(const char *filename, enum reset_type rt) } /* Make sure that @bdev->fd is a disk, not a partition. */ +#ifdef __APPLE__ + { + char disk_name[32] = ""; + if (!apple_is_whole_disk(filename, disk_name, + sizeof(disk_name))) { + if (disk_name[0]) + fprintf(stderr, "Device `%s' is a partition of disk device `%s'.\n" + "You must run %s on the disk device as follows:\n" + "%s %s\n", + filename, disk_name, __progname, + __progname, disk_name); + else + fprintf(stderr, "Device `%s' is not a whole-disk device.\n" + "Expected a name like /dev/rdisk5 (see `diskutil list').\n", + filename); + goto fd; + } + if (strncmp(filename, "/dev/rdisk", 10)) + fprintf(stderr, "NOTE: prefer the raw device (/dev/r%s) for correct operation.\n", + filename + 5); + } +#else udev = udev_new(); if (!udev) { warnx("Can't load library udev"); @@ -931,6 +1107,7 @@ struct device *create_block_device(const char *filename, enum reset_type rt) } udev_device_unref(fd_dev); assert(!udev_unref(udev)); +#endif switch (rt) { case RT_MANUAL_USB: @@ -946,9 +1123,14 @@ struct device *create_block_device(const char *filename, enum reset_type rt) assert(0); } +#ifdef __APPLE__ + assert(!apple_dev_size_byte(bdev->fd, &bdev->dev.size_byte)); + assert(!apple_dev_block_size(bdev->fd, &block_size)); +#else assert(!ioctl(bdev->fd, BLKGETSIZE64, &bdev->dev.size_byte)); assert(!ioctl(bdev->fd, BLKSSZGET, &block_size)); +#endif block_order = ilog2(block_size); assert(block_size == (1 << block_order)); bdev->dev.block_order = block_order; @@ -960,10 +1142,12 @@ struct device *create_block_device(const char *filename, enum reset_type rt) return &bdev->dev; +#ifndef __APPLE__ fd_dev: udev_device_unref(fd_dev); udev: assert(!udev_unref(udev)); +#endif fd: assert(!close(bdev->fd)); filename: