NiFAT32 is a FAT32-like filesystem designed to tolerate single-event and multiple-event upsets. It protects boot sectors, FAT entries, directory entries, journals, and persistent error records with redundant copies, checksums, voting, and Hamming encoding.
P.S.: NiFAT32 is not wire-compatible with a regular FAT32 implementation. Images must be accessed through this library or compatible firmware.
The following measurements were produced on Fedora Linux x86-64 with GCC 15.2.1 and GNU Binutils 2.45.1.
P.S.: Embedded footprint is measured with size: ROM is text + data, static RAM is data + bss. All rows use -ffunction-sections -fdata-sections and -Wl,--gc-sections.
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | ROM | Static RAM |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| + | 45,544 B | 517,356 B | |||||||||||||||
| + | + | 35,104 B | 517,236 B | ||||||||||||||
| + | + | + | 35,104 B | 517,236 B | |||||||||||||
| + | + | + | + | 34,425 B | 5,192 B | ||||||||||||
| + | + | + | + | + | 32,869 B | 5,172 B | |||||||||||
| + | + | + | + | + | + | 32,845 B | 5,172 B | ||||||||||
| + | + | + | + | + | + | + | 32,549 B | 1,908 B | |||||||||
| + | + | + | + | + | + | + | 31,957 B | 1,348 B | |||||||||
| + | + | + | + | + | + | + | + | 25,449 B | 1,296 B | ||||||||
| + | + | + | + | + | + | + | + | 21,801 B | 1,280 B | ||||||||
| + | + | + | + | + | + | + | + | 21,490 B | 1,280 B | ||||||||
| + | + | + | + | + | + | + | + | + | 21,525 B | 1,312 B | |||||||
| + | + | + | + | + | + | + | + | + | + | 21,257 B | 1,312 B | ||||||
| + | + | + | + | + | + | + | + | + | + | + | 17,001 B | 1,312 B | |||||
| + | + | + | + | + | + | + | + | + | + | + | + | 6,782 B | 856 B | ||||
| + | + | + | + | + | + | + | + | + | + | + | + | + | 6,767 B | 856 B |
- 1 -
-O2 - 2 -
NIFAT32_RO=1 - 3 -
NO_HEAP=1 - 4 -
NO_DEFAULT_MM_MANAGER=1 - 5 -
NIFAT32_NO_ERROR=1 - 6 -
NO_ENTRY_VALIDATION=1 - 7 -
CONTENT_TABLE_SIZE=8 IO_THREADS_MAX=4 - 8 -
CONTENT_TABLE_SIZE=1 IO_THREADS_MAX=1 - 9 -
NIFAT32_NO_ECACHE=1 NO_FAT_CACHE=1 NO_FAT_MAP=1 - 10 -
-Os - 11 -
-Oz - 12 -
-flto - 13 -
NO_THREADSAFE=1 - 14 -
-fno-asynchronous-unwind-tables -fno-unwind-tables - 15 -
-Wl,--version-script=nifat32.exports - 16 -
-fno-plt -Wl,--build-id=none -Wl,-z,noseparate-code
The minimum measured embedded footprint is 6,767 bytes of ROM and 856 bytes of static RAM. This keeps the public NIFAT32_* API exported and hides internal symbols from the shared object so that LTO and section garbage collection can discard unused internals.
For comparison, enabling every logging group with debug-friendly optimization produces:
| Profile | Configuration | ROM | Static RAM | ELF file | Stripped ELF |
|---|---|---|---|---|---|
| Maximum feature/debug profile | -O0 -g3, all *_LOGS=1, default features and numeric limits |
63.9 KiB | 505.2 KiB | 164.6 KiB | 71.1 KiB |
Minimum measured build:
make shared \
BUILD_DIR=builds/minimum \
CFLAGS="-Oz -flto -ffunction-sections -fdata-sections -fno-asynchronous-unwind-tables -fno-unwind-tables -fno-plt" \
LDFLAGS="-flto -Wl,--gc-sections -Wl,--version-script=nifat32.exports -Wl,--build-id=none -Wl,-z,noseparate-code" \
NIFAT32_RO=1 \
NO_HEAP=1 \
NO_DEFAULT_MM_MANAGER=1 \
NIFAT32_NO_ECACHE=1 \
NO_FAT_CACHE=1 \
NO_FAT_MAP=1 \
NIFAT32_NO_ERROR=1 \
NO_ENTRY_VALIDATION=1 \
NO_THREADSAFE=1 \
CONTENT_TABLE_SIZE=1 \
IO_THREADS_MAX=1Maximum feature/debug build:
make shared \
BUILD_DIR=builds/maximum \
CFLAGS="-O0 -g3" \
ERROR_LOGS=1 WARN_LOGS=1 INFO_LOGS=1 DEBUG_LOGS=1 \
IO_LOGS=1 MEM_LOGS=1 LOGGING_LOGS=1 SPECIAL_LOGS=1These numbers do not include thread stacks, temporary stack buffers, or memory supplied by a custom allocator. With the built-in allocator, its entire pool is counted in .bss even when only part of it is used at runtime. The default static RAM is therefore dominated by the 500 KiB ALLOC_BUFFER_SIZE. For the current root build, pass NIFAT32_NO_ECACHE=1 NO_FAT_CACHE=1 NO_FAT_MAP=1 explicitly to disable heap-backed caches, and use a smaller ALLOC_BUFFER_SIZE or NO_DEFAULT_MM_MANAGER=1 to remove the large built-in pool.
The root Makefile tracks header dependencies and rebuilds objects when build options change.
make # builds/nifat32.so
make static # builds/nifat32.a
make unix # builds/unix_nifat32
make formatter # formatter/formatter
make tools # formatter and Unix utility
make clean # remove root build artifacts
make distclean # also clean the formatter
make helpThe usual toolchain variables can be overridden:
make CC=clang CFLAGS="-O3" BUILD_DIR=outCPPFLAGS, CFLAGS, LDFLAGS, and LDLIBS may also be supplied or extended by the caller.
Boolean Make variables accept 0 or 1 and default to 0.
| Make variable | C macro | Effect |
|---|---|---|
DEBUG |
- | Adds -O0 -g3 |
NIFAT32_RO |
NIFAT32_RO |
Compiles storage mutations as successful no-ops |
NO_HEAP |
NO_HEAP |
Requests entry-cache, FAT-cache, FAT-map, and default-allocator disabling through header macros; pass the explicit flags for the current multi-file build |
NO_DEFAULT_MM_MANAGER |
NO_DEFAULT_MM_MANAGER |
Removes the built-in static allocator; callbacks must be supplied |
NIFAT32_NO_ERROR |
NIFAT32_NO_ERROR |
Disables persistent error storage |
NIFAT32_NO_ECACHE |
NIFAT32_NO_ECACHE |
Disables directory entry indexing |
NO_FAT_CACHE |
NO_FAT_CACHE |
Disables the in-memory FAT value cache |
NO_FAT_MAP |
NO_FAT_MAP |
Disables the free-cluster bitmap |
NO_ENTRY_VALIDATION |
NO_ENTRY_VALIDATION |
Skips directory-entry checksum validation |
| - | NO_THREADSAFE |
Make the FS not thread safe (exclude related code from the final binary) |
| - | NO_HAMMING |
Exclude all hamming-code related logic |
| - | ENTRY_WITH_TIME |
Adds timestamp fields to directory entries; this changes the create_entry function signature and increases the size of each entry |
ENTRY_WITH_TIME is a C macro-only option. It stores FAT-style timestamp fields in each directory_entry_t, so fewer entries fit in one decoded directory cluster. Callers must also pass timestamp values to create_entry.
For instance, this is the signature without this flag:
create_entry(fullname, is_dir, first_cluster, file_size, entry);And this is with ENTRY_WITH_TIME:
create_entry(
fullname,
is_dir,
first_cluster,
file_size,
second,
centisecond,
hour,
minute,
year,
mounth,
day,
entry
);Numeric configuration variables:
| Make variable | C macro | Default | Effect |
|---|---|---|---|
ALLOC_BUFFER_SIZE |
ALLOC_BUFFER_SIZE |
512000 |
Built-in allocator buffer size in bytes |
CONTENT_TABLE_SIZE |
CONTENT_TABLE_SIZE |
50 |
Maximum number of simultaneously open content entries |
IO_THREADS_MAX |
IO_THREADS_MAX |
25 |
Maximum number of tracked I/O lock areas |
NO_HEAP=1 defines NIFAT32_NO_ECACHE, NO_FAT_CACHE, and NO_FAT_MAP inside nifat32.h. NON_DEFAULT_MM_MANAGER is still accepted by the source as a legacy alias, but new builds should use NO_DEFAULT_MM_MANAGER.
Examples:
make NIFAT32_RO=1 NO_HEAP=1
make ALLOC_BUFFER_SIZE=1048576 CONTENT_TABLE_SIZE=100
make NO_DEFAULT_MM_MANAGER=1All log groups are disabled by default. Each Make variable enables the corresponding compile-time macro.
| Make variable | C macro | Log category |
|---|---|---|
ERROR_LOGS |
ERROR_LOGS |
Errors |
WARN_LOGS |
WARNING_LOGS |
Warnings |
INFO_LOGS |
INFO_LOGS |
General information |
DEBUG_LOGS |
DEBUG_LOGS |
Debug messages |
IO_LOGS |
IO_OPERATION_LOGS |
Disk operations |
MEM_LOGS |
MEM_OPERATION_LOGS |
Allocator operations |
LOGGING_LOGS |
LOGGING_LOGS |
Public API calls |
SPECIAL_LOGS |
SPECIAL_LOGS |
Special-purpose diagnostics |
make ERROR_LOGS=1 WARN_LOGS=1 INFO_LOGS=1
make DEBUG=1 DEBUG_LOGS=1 IO_LOGS=1Enabled logging also requires valid logging callbacks in
nifat32_params_t. Passing NULL callbacks disables output at runtime.
Build and run the formatter:
make formatter
./formatter/formatter -o nifat32.imgCreate a 128 MB image and populate it from data/:
./formatter/formatter \
-o nifat32.img \
-s data \
--volume-size 128 \
--spc 8 \
--fc 4 \
--bsbc 5 \
--b-bsbc 0 \
--jc 2| Option | Description | Default |
|---|---|---|
-o PATH |
Output image; existing files are overwritten | Required |
-s PATH |
Source directory copied recursively into the image | Empty image |
--volume-size N |
Image size in MB | 64 |
--spc N |
Sectors per cluster | 8 |
--fc N |
FAT copy count | 4 |
--bsbc N |
Boot sector copy count | 5 |
--b-bsbc N |
Deliberately damaged boot sector copies for tests | 0 |
--jc N |
Journal sector count | 2 |
The formatter creates a raw filesystem starting at sector zero, without an
MBR or GPT. See formatter/README.md for SD card
instructions and safety notes.
Include the public header:
#include "nifat32.h"NiFAT32 does not perform POSIX I/O itself. The platform supplies sector I/O, logging, and optionally memory callbacks.
static int read_sector(
sector_addr_t sector,
sector_offset_t offset,
unsigned char *buffer,
int size
) {
/* Read size bytes from sector * SECTOR_SIZE + offset. */
return 1;
}
static int write_sector(
sector_addr_t sector,
sector_offset_t offset,
const unsigned char *data,
int size
) {
/* Write size bytes to sector * SECTOR_SIZE + offset. */
return 1;
}Both callbacks return nonzero on success and zero on failure.
#define SECTOR_SIZE 512
#define IMAGE_SIZE_BYTES (64u * 1024u * 1024u)
nifat32_params_t params = {
.fat_cache = CACHE,
.bs_num = 0,
.bs_count = 5,
.ts = IMAGE_SIZE_BYTES / SECTOR_SIZE,
.jc = 2,
.ec = 1,
.disk_io = {
.read_sector = read_sector,
.write_sector = write_sector,
.sector_size = SECTOR_SIZE
},
.logg_io = {
.fd_fprintf = NULL,
.fd_vfprintf = NULL
},
.mm_manager = { DEFAULT_MM_MANAGER }
};
if (!NIFAT32_init(¶ms)) {
/* Mount failed. */
}| Field | Meaning |
|---|---|
fat_cache |
Bit mask made from NO_CACHE, CACHE, HARD_CACHE, and MAP_CACHE |
bs_num |
First boot sector copy to try; normally 0 |
bs_count |
Number of boot sector copies created by --bsbc |
ts |
Total logical sectors in the image |
jc |
Journal copy count created by --jc |
ec |
Persistent error-storage copy count; use 0 to disable at runtime |
ef |
Entries offset which means it will enable presudo-random placement over the image |
disk_io |
Sector read/write callbacks and logical sector size |
logg_io |
Optional fprintf-like and vfprintf-like callbacks |
mm_manager |
Allocator callbacks or DEFAULT_MM_MANAGER |
NIFAT32_init() increments bs_num while trying damaged boot sector copies, so the structure may be modified after a failed or recovered mount.
Cache modes must be combined with CACHE:
| Value | Behavior |
|---|---|
NO_CACHE |
Read FAT values from storage |
CACHE |
Allocate a lazy FAT cache |
CACHE | HARD_CACHE |
Load the complete FAT into the cache during mount |
CACHE | MAP_CACHE |
Allocate a free-cluster bitmap |
CACHE | HARD_CACHE | MAP_CACHE |
Enable both full loading and the bitmap |
The image geometry and mount parameters must agree. In particular,
bs_count, ts, jc, ec, and sector_size must describe the formatted
image. FAT count and sectors per cluster are read from the boot sector.
The callback signatures are:
static int mm_init(void);
static void *mm_alloc(unsigned long size);
static void mm_free(void *ptr);Configure and build:
nifat32_params_t params = {
/* ... */
.mm_manager = {
.init = mm_init,
.malloc = mm_alloc,
.free = mm_free
}
};make NO_DEFAULT_MM_MANAGER=1When using the built-in manager, { DEFAULT_MM_MANAGER } selects its static buffer. Increase ALLOC_BUFFER_SIZE when FAT caching or directory indexes need more memory.
NiFAT32 stores and accepts names in uppercase FAT 8.3 form. Convert regular names and paths before passing them to the public API.
char path[128] = { 0 };
nft32_path_to_fatnames("root/dir/hello.txt", path);
/* path is now ROOT/DIR/HELLO TXT */Available helpers from std/fatname.h:
| Function | Purpose |
|---|---|
nft32_name_to_fatname |
Convert hello.txt to HELLO TXT |
nft32_fatname_to_name |
Convert HELLO TXT to a regular name |
nft32_path_to_fatnames |
Convert a path to the NiFAT32 path form |
nft32_extract_name |
Extract the final path component |
nft32_unpack_83_name |
Split an 11-byte FAT name into name and extension |
Open modes:
| Flag | Meaning |
|---|---|
R_MODE |
Permit reads |
W_MODE |
Permit writes |
CR_MODE |
Create missing path components |
NO_TARGET |
No explicit type for the final component |
FILE_TARGET |
Create the final component as a file |
DIR_TARGET |
Create the final component as a directory |
DF_MODE |
MODE(R_MODE | W_MODE, NO_TARGET) |
Use MODE(access_flags, target) to pack access and target flags:
char fat_path[64] = { 0 };
nft32_path_to_fatnames("root/hello.txt", fat_path);
ci_t file = NIFAT32_open_content(
NO_RCI,
fat_path,
MODE(R_MODE | W_MODE | CR_MODE, FILE_TARGET)
);NO_RCI starts lookup at the filesystem root. Passing an open directory index instead starts lookup relative to that directory. A NULL path opens the root directory.
All public functions are declared in nifat32.h.
NIFAT32_content_exists() searches from the filesystem root and returns 1 when the path exists.
char fat_path[64] = { 0 };
nft32_path_to_fatnames("data/config.bin", fat_path);
if (NIFAT32_content_exists(fat_path)) {
// The entry exists and can be opened.
}Open an entry with the required access mode. A nonnegative value is a valid content index.
char fat_path[64] = { 0 };
nft32_path_to_fatnames("data/config.bin", fat_path);
ci_t file = NIFAT32_open_content(
NO_RCI,
fat_path,
MODE(R_MODE | W_MODE, FILE_TARGET)
);
if (file < 0) {
// -1 means that the content table is full.
// -2 means that the path was not found.
}Pass an open directory as rci to search relative to it:
char directory_name[12] = { 0 };
char file_name[12] = { 0 };
nft32_name_to_fatname("data", directory_name);
nft32_name_to_fatname("config.bin", file_name);
ci_t directory = NIFAT32_open_content(NO_RCI, directory_name, DF_MODE);
if (directory >= 0) {
ci_t file = NIFAT32_open_content(directory, file_name, DF_MODE);
if (file >= 0) NIFAT32_close_content(file);
NIFAT32_close_content(directory);
}Add CR_MODE to create missing path components. Intermediate components are created as directories.
char fat_path[64] = { 0 };
nft32_path_to_fatnames("logs/boot.txt", fat_path);
ci_t file = NIFAT32_open_content(
NO_RCI,
fat_path,
MODE(R_MODE | W_MODE | CR_MODE, FILE_TARGET)
);
if (file >= 0) {
// logs/boot.txt now exists and is open.
}Use NIFAT32_put_content() when cluster reservation is required:
ci_t root = NIFAT32_open_content(NO_RCI, NULL, DF_MODE);
cinfo_t info = {
.full_name = "LOG BIN",
.type = STAT_FILE
};
if (root >= 0) {
NIFAT32_put_content(root, &info, 4); // Preallocate four clusters.
NIFAT32_close_content(root);
}NO_RESERVE is 1. Creation through NIFAT32_open_content() is more convenient for paths, but it does not expose cluster reservation.
The content must be opened with R_MODE. The function returns the number of bytes copied to the buffer.
char buffer[64] = { 0 };
int read_size = NIFAT32_read_content2buffer(
file,
0, // Byte offset in the content.
(buffer_t)buffer,
sizeof(buffer)
);
if (read_size > 0) {
// buffer[0..read_size-1] contains the data.
}Reading a directory returns its raw encoded directory data.
The content must be opened with W_MODE. The function returns the number of bytes written.
const char message[] = "System started\n";
int written = NIFAT32_write_buffer2content(
file,
0, // Byte offset in the content.
(const_buffer_t)message,
sizeof(message) - 1
);
if (written != (int)(sizeof(message) - 1)) {
// The complete message was not written.
}The current write implementation grows cluster chains but does not automatically update file-size metadata. Use NIFAT32_change_meta() or NIFAT32_truncate_content() when the stored size must be updated.
NIFAT32_stat_content() fills a cinfo_t structure with the entry name and type.
cinfo_t stat = { 0 };
if (NIFAT32_stat_content(file, &stat)) {
// stat.full_name contains the FAT 8.3 name.
// stat.type is STAT_FILE or STAT_DIR.
}| Field | Meaning |
|---|---|
full_name[12] |
11-byte FAT 8.3 name plus storage for a terminator |
name[8] |
Base name |
extention[4] |
Extension; the public field retains this spelling |
size |
File size metadata |
type |
STAT_FILE or STAT_DIR |
The current NIFAT32_stat_content() implementation does not populate size for files. Do not read that field after stat until the implementation is corrected.
NIFAT32_change_meta() changes directory entry metadata. It does not move or resize the cluster chain.
cinfo_t info = {
.full_name = "CONFIG BAK",
.size = 128,
.type = STAT_FILE
};
if (!NIFAT32_change_meta(file, &info)) {
// Metadata update failed.
}Truncation changes the occupied cluster chain and stored file size. The content must be open with W_MODE.
if (NIFAT32_truncate_content(file, 0, 128)) {
// Keep 128 bytes starting from offset zero.
}Index frequently searched directories to speed up repeated lookups.
char directory_name[12] = { 0 };
char file_name[12] = { 0 };
nft32_name_to_fatname("data", directory_name);
nft32_name_to_fatname("config.bin", file_name);
ci_t directory = NIFAT32_open_content(NO_RCI, directory_name, DF_MODE);
if (directory >= 0) {
NIFAT32_index_content(directory);
// Relative lookups can now use the directory index.
ci_t file = NIFAT32_open_content(directory, file_name, DF_MODE);
if (file >= 0) NIFAT32_close_content(file);
NIFAT32_close_content(directory);
}Indexing requires allocator support and has no useful effect when compiled with NIFAT32_NO_ECACHE.
The destination must already exist as a placeholder:
| Copy type | Behavior |
|---|---|
DEEP_COPY |
Allocate a new cluster chain and recursively duplicate directory data |
SHALLOW_COPY |
Replace the destination with a link to the source cluster chain |
char source_path[64] = { 0 };
char copy_path[64] = { 0 };
nft32_path_to_fatnames("data/config.bin", source_path);
nft32_path_to_fatnames("backup.bin", copy_path);
ci_t source = NIFAT32_open_content(NO_RCI, source_path, DF_MODE);
ci_t copy = NIFAT32_open_content(
NO_RCI,
copy_path,
MODE(R_MODE | W_MODE | CR_MODE, FILE_TARGET)
);
if (source >= 0 && copy >= 0) {
NIFAT32_copy_content(source, copy, DEEP_COPY);
}
if (source >= 0) NIFAT32_close_content(source);
if (copy >= 0) NIFAT32_close_content(copy);Copying deallocates the destination's previous data. A shallow copy must not be placed in the same directory under a conflicting name.
Deleting a directory also removes its children recursively. The supplied content index is closed by the function.
char fat_path[64] = { 0 };
nft32_path_to_fatnames("backup.bin", fat_path);
ci_t file = NIFAT32_open_content(NO_RCI, fat_path, DF_MODE);
if (file >= 0) {
NIFAT32_delete_content(file);
// Do not close file: NIFAT32_delete_content() already did it.
}NIFAT32_repair_bootsectors() rebuilds every boot sector copy from the currently mounted geometry.
NIFAT32_repair_bootsectors(); // Rewrite all boot sector copies.NIFAT32_repair_content() reads, corrects and rewrites directory entries. Pass a nonzero second argument to visit subdirectories recursively.
ci_t root = NIFAT32_open_content(NO_RCI, NULL, DF_MODE);
if (root >= 0) {
NIFAT32_repair_content(root, 1); // Recursive repair.
NIFAT32_close_content(root);
}NIFAT32_get_fs_data() copies the currently mounted geometry.
fat_data_t fs = NIFAT32_get_fs_data();Persistent errors are consumed from the error ring one at a time.
error_code_t error = NIFAT32_get_last_error();
if (error >= 0) {
// Handle the registered error code.
}The function returns -1 when no error is available. With NIFAT32_NO_ERROR=1, registration is disabled and the function returns NO_ERROR.
Close every successfully opened content index when it is no longer needed.
if (file >= 0) {
NIFAT32_close_content(file);
}Call NIFAT32_unload() after all work is complete. It releases the FAT cache and destroys the content table.
NIFAT32_unload();With NIFAT32_RO=1, reads, lookup, stat, indexing, and lifecycle operations remain available. Storage-mutating paths are compiled as no-ops. In the current API these no-op functions generally return success, so callers must not use the return value to infer that persistent data changed.
Affected operations include write, truncate, metadata changes, creation, copy, deletion, journal writes, FAT writes, and repair writes.
Build:
make unixRun:
./builds/unix_nifat32 nifat32.img 64 512 5 2Arguments are image path, image size in MB, sector size, boot sector copy count, and journal count. Supported commands include cd, ls, mkdir, mkfile, read, write, trunc, cp, mv, rm, frename, rs, ws, and get_le.
The test runner can build a fresh image and execute the C test suite:
python3 test/nifat32_tests.py \
--new-image \
--formatter formatter \
--tests-folder test \
--root-folder . \
--clean \
--test-type defaultFault-injection tests:
python3 test/nifat32_tests.py \
--new-image \
--formatter formatter \
--tests-folder test \
--root-folder . \
--clean \
--test-type bitflip \
--injector-scenario test/injector_scenario.txtSee test/README.md for campaign and collector options.