From b02433f9c6c1610dd0b4260f9438c7ce6fd45b32 Mon Sep 17 00:00:00 2001 From: Michel Machado Date: Wed, 29 Apr 2026 19:41:48 -0400 Subject: [PATCH 1/3] f3brew: add flag --fix-cmd When flag --fix-cmd is passed, f3brew shows how to call f3fix on the largest good region identified. --- .vscode/launch.json | 2 +- changelog | 1 + src/f3brew.c | 166 +++++++++++++++++++++++++++++++------------- src/libutils.h | 1 + 4 files changed, 120 insertions(+), 50 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index ab14d0a..f5c6642 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,7 +10,7 @@ "request": "launch", "cwd": "${workspaceFolder}", "program": "${workspaceFolder}/build/f3brew", - "args": ["--debug", "test"], + "args": ["--debug", "--fix-cmd", "test"], "MIMode": "gdb", }, { diff --git a/changelog b/changelog index 2b87cb3..0ba0b1d 100644 --- a/changelog +++ b/changelog @@ -2,6 +2,7 @@ Version 10.0 - Apr 29, 2026 * f3probe: New probing algorithm and --verbose flag * f3brew: Progress report and rate limiting (i.e., --max-write-rate and --max-read-rate) + * f3brew: Add --fix_cmd flag * f3write/f3read: Report per-file min/max and average speeds * GitHub Actions: FreeBSD and OpenBSD support * Move codebase from C99 to C17 and refactor project diff --git a/src/f3brew.c b/src/f3brew.c index e06a5d3..75bfee1 100644 --- a/src/f3brew.c +++ b/src/f3brew.c @@ -54,6 +54,8 @@ static struct argp_option options[] = { "Where test begins; the default is block zero", 0}, {"end-at", 'e', "BLOCK", 0, "Where test ends; the default is the very last block", 0}, + {"fix-cmd", 'x', NULL, 0, + "Show how to call f3fix to fix the drive", 0}, {"max-read-rate", 'r', "KB/s", 0, "Maximum read rate", 0}, {"max-write-rate", 'w', "KB/s", 0, @@ -78,7 +80,7 @@ struct args { enum reset_type reset_type; bool test_write; bool test_read; - /* 3 free bytes. */ + bool fix_cmd; /* Geometry. */ uint64_t real_size_byte; @@ -217,6 +219,10 @@ static error_t parse_opt(int key, char *arg, struct argp_state *state) args->test_read = false; break; + case 'x': + args->fix_cmd = true; + break; + case ARGP_KEY_INIT: args->filename = NULL; break; @@ -327,30 +333,26 @@ static void test_write_blocks(struct device *dev, struct block_range { enum block_state state; unsigned int block_order; - uint64_t start_sector_offset; - uint64_t end_sector_offset; + uint64_t start_block; + uint64_t end_block; /* Only used by state bs_overwritten. */ - uint64_t found_sector_offset; + uint64_t found_block; }; -static int is_block(uint64_t offset, unsigned int block_order) -{ - return !(((1ULL << block_order) - 1) & offset); -} - -static void print_offset(uint64_t offset, unsigned int block_order) -{ - assert(is_block(offset, block_order)); - printf("block 0x%" PRIx64, offset >> block_order); +#define INIT_UNKNOWN_RANGE(block_order) { \ + .state = bs_unknown, \ + .block_order = (block_order), \ + .start_block = 0, \ + .end_block = UINT64_MAX, \ + .found_block = 0 \ } static void print_block_range(const struct block_range *range) { - printf("[%s] from ", block_state_to_str(range->state)); - print_offset(range->start_sector_offset, range->block_order); - printf(" to "); - print_offset(range->end_sector_offset, range->block_order); + printf("[%s] from block 0x%" PRIx64 " to 0x%" PRIx64, + block_state_to_str(range->state), + range->start_block, range->end_block); switch (range->state) { case bs_good: @@ -359,8 +361,7 @@ static void print_block_range(const struct block_range *range) break; case bs_overwritten: - printf(", found "); - print_offset(range->found_sector_offset, range->block_order); + printf(", found block 0x%" PRIx64, range->found_block); break; default: @@ -370,23 +371,46 @@ static void print_block_range(const struct block_range *range) printf("\n"); } -static void validate_block(struct flow *fw, uint64_t expected_sector_offset, +static inline bool is_block(uint64_t offset, unsigned int block_order) +{ + return !(((1ULL << block_order) - 1) & offset); +} + +static void update_good_range(struct block_range *good_range, + const struct block_range *range) +{ + uint64_t good_range_blocks, range_blocks; + + if (range->state != bs_good) + return; + + good_range_blocks = + good_range->end_block - good_range->start_block + 1; + range_blocks = range->end_block - range->start_block + 1; + if (range_blocks > good_range_blocks) + *good_range = *range; +} + +static void validate_block(struct flow *fw, uint64_t block, const char *probe_blk, unsigned int block_order, - struct block_range *range, struct block_stats *stats) + struct block_range *range, struct block_range *good_range, + struct block_stats *stats) { - uint64_t found_sector_offset; - enum block_state state = validate_block_update_stats(probe_blk, block_order, - expected_sector_offset, &found_sector_offset, 0, stats); + const uint64_t expected_offset = block << block_order; + uint64_t found_offset; + enum block_state state = validate_block_update_stats(probe_blk, + block_order, expected_offset, &found_offset, 0, stats); + const uint64_t found_block = found_offset >> block_order; bool push_range; + assert(is_block(found_offset, block_order)); + push_range = (range->state != state) || ( state == bs_overwritten && ( - (expected_sector_offset - - range->start_sector_offset) + (block - range->start_block) != - (found_sector_offset - - range->found_sector_offset) + (found_block - range->found_block) ) ); @@ -394,30 +418,25 @@ static void validate_block(struct flow *fw, uint64_t expected_sector_offset, if (range->state != bs_unknown) { clear_progress(fw); print_block_range(range); + update_good_range(good_range, range); } range->state = state; - range->start_sector_offset = expected_sector_offset; - range->end_sector_offset = expected_sector_offset; - range->found_sector_offset = found_sector_offset; + range->start_block = block; + range->end_block = block; + range->found_block = found_block; } else { - range->end_sector_offset = expected_sector_offset; + range->end_block = block; } } static void read_blocks(struct device *dev, struct flow *fw, - uint64_t first_block, uint64_t last_block, struct block_stats *stats) + uint64_t first_block, uint64_t last_block, struct block_stats *stats, + struct block_range *good_range) { const unsigned int block_size = dev_get_block_size(dev); const unsigned int block_order = dev_get_block_order(dev); - uint64_t expected_sector_offset = first_block << block_order; uint64_t first_pos = first_block; - struct block_range range = { - .state = bs_unknown, - .block_order = block_order, - .start_sector_offset = 0, - .end_sector_offset = 0, - .found_sector_offset = 0, - }; + struct block_range range = INIT_UNKNOWN_RANGE(block_order); struct dynamic_buffer dbuf; dbuf_init(&dbuf); @@ -444,9 +463,8 @@ static void read_blocks(struct device *dev, struct flow *fw, probe_blk = buffer; for (pos = first_pos; pos < next_pos; pos++) { - validate_block(fw, expected_sector_offset, probe_blk, - block_order, &range, stats); - expected_sector_offset += block_size; + validate_block(fw, pos, probe_blk, block_order, + &range, good_range, stats); probe_blk += block_size; } @@ -456,21 +474,67 @@ static void read_blocks(struct device *dev, struct flow *fw, end_measurement(fw); dbuf_free(&dbuf); - if (range.state != bs_unknown) + if (range.state != bs_unknown) { print_block_range(&range); - else + update_good_range(good_range, &range); + } else { assert(first_block > last_block); + } +} + +static void print_fix_cmd(const char *filename, + const struct block_range *good_range) +{ + const uint64_t first_1MB_block = + MEGABYTE_SIZE >> good_range->block_order; + const unsigned int shift = good_range->block_order - SECTOR_ORDER; + const uint64_t first_good_sector = good_range->start_block << shift; + const uint64_t last_good_sector = + ((good_range->end_block + 1) << shift) - 1; + uint64_t size_byte; + + assert(MEGABYTE_ORDER >= good_range->block_order); + assert(good_range->block_order >= SECTOR_ORDER); + + /* Set size_byte. */ + if (good_range->state != bs_good) { + size_byte = 0; + } else if (good_range->end_block >= first_1MB_block) { + const uint64_t start_block = + good_range->start_block <= first_1MB_block + ? first_1MB_block + : good_range->start_block; + size_byte = (good_range->end_block - start_block + 1) << + good_range->block_order; + } else { + size_byte = 0; + } + + if (size_byte < MEGABYTE_SIZE) { + printf("There is no good region large enough to \"fix\" this device.\n\n"); + return; + } + + printf("You can \"fix\" this device using the following command:\n"); + if (good_range->start_block < first_1MB_block) { + printf("f3fix --last-sec=%" PRIu64 " %s\n\n", + last_good_sector, filename); + return; + } + printf("f3fix --first-sec=%" PRIu64 " --last-sec=%" PRIu64 " %s\n\n", + first_good_sector, last_good_sector, filename); } /* XXX Properly handle return errors. */ static void test_read_blocks(struct device *dev, uint64_t first_block, uint64_t last_block, - long max_read_rate, int show_progress) + long max_read_rate, int show_progress, bool fix_cmd) { const unsigned int block_order = dev_get_block_order(dev); const uint64_t total_blocks = last_block - first_block + 1; struct flow fw; struct block_stats stats = { 0, 0, 0, 0 }; + struct block_range good_range = INIT_UNKNOWN_RANGE(block_order); printf("Reading block%s from 0x%" PRIx64 " to 0x%" PRIx64 ":\n", first_block != last_block ? "s" : "", first_block, last_block); @@ -479,11 +543,14 @@ static void test_read_blocks(struct device *dev, FW_MAX_BLOCKS_PER_DELAY_NONE, show_progress ? printf_flush_cb : dummy_cb, 0); - read_blocks(dev, &fw, first_block, last_block, &stats); + read_blocks(dev, &fw, first_block, last_block, &stats, &good_range); print_stats(&stats, block_order, "block"); print_avg_seq_speed(&fw, "read", false); printf("\n"); + + if (fix_cmd) + print_fix_cmd(dev_get_filename(dev), &good_range); } int main(int argc, char **argv) @@ -495,6 +562,7 @@ int main(int argc, char **argv) .reset_type = RT_MANUAL_USB, .test_write = true, .test_read = true, + .fix_cmd = false, .real_size_byte = 1ULL << 31, .fake_size_byte = 1ULL << 34, .wrap = 31, @@ -552,7 +620,7 @@ int main(int argc, char **argv) if (args.test_read) test_read_blocks(dev, args.first_block, args.last_block, - args.max_read_rate, args.show_progress); + args.max_read_rate, args.show_progress, args.fix_cmd); free_device(dev); return 0; diff --git a/src/libutils.h b/src/libutils.h index 8f7e054..a5cdb4d 100644 --- a/src/libutils.h +++ b/src/libutils.h @@ -12,6 +12,7 @@ #define TERABYTE_ORDER (40) #define SECTOR_SIZE (1ULL << SECTOR_ORDER) +#define MEGABYTE_SIZE (1ULL << MEGABYTE_ORDER) #define GIGABYTE_SIZE (1ULL << GIGABYTE_ORDER) #define UNUSED(x) ((void)x) From 77ae088b3a1549450ce490f1d0ca9b5954c906c9 Mon Sep 17 00:00:00 2001 From: Michel Machado Date: Wed, 29 Apr 2026 19:59:49 -0400 Subject: [PATCH 2/3] Reduce magic numbers with macros *_ORDER and *_SIZE --- src/f3brew.c | 10 +++++----- src/f3fix.c | 11 ++++++----- src/f3probe.c | 32 ++++++++++++++++---------------- src/libdevs.c | 2 +- src/libflow.h | 2 +- src/libprobe.c | 2 +- src/libutils.h | 2 ++ 7 files changed, 32 insertions(+), 29 deletions(-) diff --git a/src/f3brew.c b/src/f3brew.c index 75bfee1..1efdb60 100644 --- a/src/f3brew.c +++ b/src/f3brew.c @@ -139,10 +139,10 @@ static error_t parse_opt(int key, char *arg, struct argp_state *state) case 'b': ll = arg_to_ll_bytes(state, arg); - if (ll != 0 && (ll < SECTOR_ORDER || ll > 20)) + if (ll != 0 && (ll < SECTOR_ORDER || ll > MEGABYTE_ORDER)) argp_error(state, - "Block order must be in the interval [%i, 20] or be zero", - SECTOR_ORDER); + "Block order must be in the interval [%i, %i] or be zero", + SECTOR_ORDER, MEGABYTE_ORDER); args->block_order = ll; args->debug = true; break; @@ -563,8 +563,8 @@ int main(int argc, char **argv) .test_write = true, .test_read = true, .fix_cmd = false, - .real_size_byte = 1ULL << 31, - .fake_size_byte = 1ULL << 34, + .real_size_byte = 2 * GIGABYTE_SIZE, + .fake_size_byte = 16 * GIGABYTE_SIZE, .wrap = 31, .block_order = SECTOR_ORDER, .cache_order = -1, diff --git a/src/f3fix.c b/src/f3fix.c index e77f084..38f3dac 100644 --- a/src/f3fix.c +++ b/src/f3fix.c @@ -183,11 +183,11 @@ static void list_fs_types(void) } static PedSector map_sector_to_logical_sector(PedSector sector, - int logical_sector_size) + unsigned int logical_sector_size) { - assert(logical_sector_size >= 512); - assert(logical_sector_size % 512 == 0); - return sector / (logical_sector_size / 512); + assert(logical_sector_size >= SECTOR_SIZE); + assert((logical_sector_size & (SECTOR_SIZE - 1)) == 0); + return sector / (logical_sector_size >> SECTOR_ORDER); } /* 0 on failure, 1 otherwise. */ @@ -249,7 +249,8 @@ int main (int argc, char *argv[]) .disk_type = ped_disk_type_get("msdos"), .fs_type = ped_file_system_type_get("fat32"), - .first_sec = 2048, /* Skip first 1MB. */ + /* Skip first 1MB to avoid the partition table. */ + .first_sec = MEGABYTE_SIZE / SECTOR_SIZE, }; PedDevice *dev; diff --git a/src/f3probe.c b/src/f3probe.c index ef828aa..e1160b4 100644 --- a/src/f3probe.c +++ b/src/f3probe.c @@ -130,10 +130,10 @@ static error_t parse_opt(int key, char *arg, struct argp_state *state) case 'b': ll = arg_to_ll_bytes(state, arg); - if (ll != 0 && (ll < SECTOR_ORDER || ll > 20)) + if (ll != 0 && (ll < SECTOR_ORDER || ll > MEGABYTE_ORDER)) argp_error(state, - "Block order must be in the interval [%i, 20] or be zero", - SECTOR_ORDER); + "Block order must be in the interval [%i, %i] or be zero", + SECTOR_ORDER, MEGABYTE_ORDER); args->block_order = ll; args->debug = true; break; @@ -239,37 +239,37 @@ struct unit_test_item { static const struct unit_test_item ftype_to_params[] = { /* Smallest good drive. */ - {1ULL << 21, 1ULL << 21, 21, SECTOR_ORDER, -1, false}, + {2 * MEGABYTE_SIZE, 2 * MEGABYTE_SIZE, MEGABYTE_ORDER + 1, SECTOR_ORDER, -1, false}, /* Good, 4KB-block, 1GB drive. */ - {GIGABYTE_SIZE, GIGABYTE_SIZE, GIGABYTE_ORDER, 12, -1, false}, + {GIGABYTE_SIZE, GIGABYTE_SIZE, GIGABYTE_ORDER, KILOBYTE_ORDER + 2, -1, false}, /* Bad drive. */ - {0, GIGABYTE_SIZE, GIGABYTE_ORDER, SECTOR_ORDER, -1, false}, + {0, GIGABYTE_SIZE, GIGABYTE_ORDER, SECTOR_ORDER, -1, false}, /* Geometry of a real limbo drive. */ - {1777645568ULL, 32505331712ULL, 35, SECTOR_ORDER, -1, false}, + {1777645568ULL, 32505331712ULL, GIGABYTE_ORDER + 5, SECTOR_ORDER, -1, false}, /* Wraparound drive. */ - {1ULL << 31, 1ULL << 34, 31, SECTOR_ORDER, -1, false}, + {2 * GIGABYTE_SIZE, 16 * GIGABYTE_SIZE, GIGABYTE_ORDER + 1, SECTOR_ORDER, -1, false}, /* Chain drive. */ - {1ULL << 31, 1ULL << 34, 32, SECTOR_ORDER, -1, false}, + {2 * GIGABYTE_SIZE, 16 * GIGABYTE_SIZE, GIGABYTE_ORDER + 2, SECTOR_ORDER, -1, false}, /* Extreme case for memory usage (limbo drive). */ - {(1ULL<<20)+512,1ULL << 40, 40, SECTOR_ORDER, -1, false}, + {MEGABYTE_SIZE + SECTOR_SIZE, TERABYTE_SIZE, TERABYTE_ORDER, SECTOR_ORDER, -1, false}, /* Geometry of a real limbo drive with 256MB of strict cache. */ - {7600799744ULL, 67108864000ULL, 36, SECTOR_ORDER, 19, true}, + {7600799744ULL, 67108864000ULL, GIGABYTE_ORDER + 6, SECTOR_ORDER, 19, true}, /* The drive before with a non-strict cache. */ - {7600799744ULL, 67108864000ULL, 36, SECTOR_ORDER, 19, false}, + {7600799744ULL, 67108864000ULL, GIGABYTE_ORDER + 6, SECTOR_ORDER, 19, false}, /* The devil drive I. */ - {0, 1ULL << 40, 40, SECTOR_ORDER, 21, true}, + {0, TERABYTE_SIZE, TERABYTE_ORDER, SECTOR_ORDER, 21, true}, /* The devil drive II. */ - {0, 1ULL << 40, 40, SECTOR_ORDER, 21, false}, + {0, TERABYTE_SIZE, TERABYTE_ORDER, SECTOR_ORDER, 21, false}, }; static int unit_test(const char *filename) @@ -571,8 +571,8 @@ int main(int argc, char **argv) .show_progress = isatty(STDOUT_FILENO), .max_read_rate = FW_MAX_PROCESS_RATE_NONE, .max_write_rate = FW_MAX_PROCESS_RATE_NONE, - .real_size_byte = 1ULL << 31, - .fake_size_byte = 1ULL << 34, + .real_size_byte = 2 * GIGABYTE_SIZE, + .fake_size_byte = 16 * GIGABYTE_SIZE, .wrap = 31, .block_order = SECTOR_ORDER, .cache_order = -1, diff --git a/src/libdevs.c b/src/libdevs.c index ba41e90..7348d8c 100644 --- a/src/libdevs.c +++ b/src/libdevs.c @@ -44,7 +44,7 @@ int dev_param_valid(uint64_t real_size_byte, /* Check general ranges. */ if (real_size_byte > announced_size_byte || wrap < 0 || wrap >= 64 || - block_order < SECTOR_ORDER || block_order > 20) + block_order < SECTOR_ORDER || block_order > MEGABYTE_ORDER) return false; /* Check alignment of the sizes. */ diff --git a/src/libflow.h b/src/libflow.h index 2590bcd..ee6b85b 100644 --- a/src/libflow.h +++ b/src/libflow.h @@ -141,7 +141,7 @@ struct dynamic_buffer { /* Ensure that backup_buf has the same memory alignment as * it would have, had it been returned by malloc(). */ - alignas(max_align_t) char backup_buf[1 << 21]; /* 2MB */ + alignas(max_align_t) char backup_buf[2 * MEGABYTE_SIZE]; }; static inline void dbuf_init(struct dynamic_buffer *dbuf) diff --git a/src/libprobe.c b/src/libprobe.c index a8cb174..45156ad 100644 --- a/src/libprobe.c +++ b/src/libprobe.c @@ -833,7 +833,7 @@ int probe_device(struct device *dev, struct probe_results *results, * (@left_pos, @right_pos), we avoid losing the partition table. */ assert(block_order <= MEGABYTE_ORDER); - left_pos = (1ULL << (MEGABYTE_ORDER - block_order)) - 1; + left_pos = (MEGABYTE_SIZE >> block_order) - 1; /* @right_pos must point to a bad block. * We just point to the block after the very last block. diff --git a/src/libutils.h b/src/libutils.h index a5cdb4d..938e241 100644 --- a/src/libutils.h +++ b/src/libutils.h @@ -12,8 +12,10 @@ #define TERABYTE_ORDER (40) #define SECTOR_SIZE (1ULL << SECTOR_ORDER) +#define KILOBYTE_SIZE (1ULL << KILOBYTE_ORDER) #define MEGABYTE_SIZE (1ULL << MEGABYTE_ORDER) #define GIGABYTE_SIZE (1ULL << GIGABYTE_ORDER) +#define TERABYTE_SIZE (1ULL << TERABYTE_ORDER) #define UNUSED(x) ((void)x) #define DIM(x) (sizeof(x) / sizeof((x)[0])) From 886b1993d0ce436a286a94423d425f7eb8c5210c Mon Sep 17 00:00:00 2001 From: Michel Machado Date: Thu, 30 Apr 2026 14:29:49 -0400 Subject: [PATCH 3/3] f3probe: recommend f3brew before f3fix --- src/f3probe.c | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/f3probe.c b/src/f3probe.c index e1160b4..003ef8a 100644 --- a/src/f3probe.c +++ b/src/f3probe.c @@ -502,13 +502,18 @@ static int test_device(struct args *args) case FKTY_LIMBO: case FKTY_WRAPAROUND: case FKTY_CHAIN: { - uint64_t last_good_sector = (results.real_size_byte >> + const uint64_t last_good_block = (results.real_size_byte >> + results.block_order) - 1; + const uint64_t last_good_sector = (results.real_size_byte >> SECTOR_ORDER) - 1; assert(results.block_order >= SECTOR_ORDER); printf("Bad news: The device `%s' is a counterfeit of type %s\n\n" - "You can \"fix\" this device using the following command:\n" + "Use the command below to check all blocks in the usable area:\n" + "f3brew --fix-cmd --end-at=%" PRIu64 " %s\n" + "If you prefer checking with f3write/f3read, use the following command:\n" "f3fix --last-sec=%" PRIu64 " %s\n", args->filename, fake_type_to_name(fake_type), + last_good_block, args->filename, last_good_sector, args->filename); break; }