# ESP External Partition Tables
This component provides an API to parse and generate external partition tables.
Currently only [MBR (Master boot record)](https://en.wikipedia.org/wiki/Master_boot_record) is supported.
## Features
- Parse MBR partition tables from raw data (e.g., SD card, USB drive)
- Generate and manipulate partition lists in memory
- Deep copy and de-initialize partition lists
- Access partition information (address, size, type, label)
- Filter partitions with a caller predicate (e.g. only mountable filesystems)
- Example projects included
## Detecting the format of an unknown medium
The first sector of a partitioned medium is always an MBR, so `esp_mbr_bdl_read` (or
`esp_mbr_parse` on a sector you read yourself) is a valid first step even when you do
not know what the device holds. What it returns tells you the format:
| Device | Result |
|---|---|
| MBR | `ESP_OK`, the real partitions |
| GPT | `ESP_OK`, a single partition of type `ESP_EXT_PART_TYPE_GPT_PROTECTIVE_MBR` |
| Neither | `ESP_ERR_NOT_FOUND` (no MBR boot signature) |
| Filesystem without a partition table ("superfloppy") | `ESP_ERR_NOT_FOUND` |
A medium formatted without a partition table has a filesystem boot sector in its
first sector, which also ends in the `0x55AA` signature. It is told apart from an MBR
by the partition entries' status bytes, which must be `0x00` or `0x80` in an MBR.
The block-device helpers read and write the first I/O unit of the device: 512 B, or
the device's read/write/erase granularity if larger. `esp_mbr_bdl_write` reads that
unit, updates only the MBR fields in it (the bootstrap code and any other data in the
unit are kept) and erases it before writing if the device requires it. The MBR sector
size is not taken from the device; pass `sector_size` in the extra arguments for media
whose sectors are not 512 B.
A GPT disk carries a *protective MBR* in the first sector: one entry of type `0xEE`
spanning the whole device, which exists so that MBR-only tools do not treat the disk as
unpartitioned. Parsing it therefore succeeds, but the partitions it describes are not
the real ones - this component cannot read a GPT table. **If you may encounter GPT
media, check for `ESP_EXT_PART_TYPE_GPT_PROTECTIVE_MBR` before using the result.**
To decide up front rather than from the parsed result, probe the device:
```c
esp_ext_part_signature_type_t type;
esp_err_t err = esp_ext_part_probe(handle, &type);
if (err == ESP_OK && type == ESP_EXT_PART_LIST_SIGNATURE_MBR) {
err = esp_mbr_bdl_read(handle, &part_list, NULL);
} // type == ESP_EXT_PART_LIST_SIGNATURE_GPT -> GPT, not readable by this component
```
## Partition selection and filtering (MBR parsing)
`esp_mbr_parse` inserts **every recognized partition** into the list by default,
regardless of whether ESP-IDF can mount it (FAT12/16/32, LittleFS, `0xDA` raw data,
and also recognized-but-undrivable types such as exFAT/NTFS, Linux and
GPT-protective MBR). Only truly unknown/extended type codes are skipped.
To select a subset, iterate with a predicate via `esp_ext_part_list_next_matching`,
or filter at parse time with `esp_mbr_parse_extra_args_t.match`.
The usage classes are a coarse, *type-intrinsic* hint. Whether a filesystem is
actually **mountable in a given firmware** depends on which drivers are linked in
that build (for example LittleFS is an optional external component), which the
library cannot decide on its own. So mountability is expressed as a predicate.
The library ships a ready-made matcher, `esp_ext_part_match_mountable()`, so you
usually do not need to write your own. It treats FAT12/16/32 as always mountable
(FatFs is part of ESP-IDF) and LittleFS as mountable when the LittleFS component is
visible at compile time (detected via `__has_include("esp_littlefs.h")`, or forced by
defining `ESP_EXT_PART_HAS_LITTLEFS`) and the partition has a block size (see below):
```c
esp_ext_part_match_t matcher = esp_ext_part_match_mountable();
for (esp_ext_part_list_item_t *it = esp_ext_part_list_next_matching(NULL, &part_list, &matcher);
it != NULL;
it = esp_ext_part_list_next_matching(it, &part_list, &matcher)) {
// it points to a partition this build can mount
}
```
To decide differently - e.g. gate on a specific Kconfig option, or branch on a
runtime field such as a LittleFS block size stored in `info->extra` - supply your own
predicate. It receives the full partition info plus an opaque context:
```c
static bool i_can_mount(const esp_ext_part_t *info, void *ctx)
{
switch (info->type) {
case ESP_EXT_PART_TYPE_FAT12:
case ESP_EXT_PART_TYPE_FAT16:
case ESP_EXT_PART_TYPE_FAT32:
return true; // FatFs is part of ESP-IDF
#ifdef CONFIG_LITTLEFS_PAGE_SIZE // only if THIS build links the LittleFS component
case ESP_EXT_PART_TYPE_LITTLEFS:
return true;
#endif
default:
return false;
}
}
// esp_ext_part_match_t matcher = { .fn = i_can_mount };
// ... esp_ext_part_list_next_matching(NULL, &part_list, &matcher) ...
```
The same predicate can filter **at parse time** via `esp_mbr_parse_extra_args_t`, so
unwanted partitions are never inserted:
```c
esp_mbr_parse_extra_args_t args = { .match = esp_ext_part_match_mountable() };
esp_mbr_parse((void*) loaded_mbr, &part_list, &args); // only mountable partitions inserted
```
Whenever the parser skips a partition (an unknown/extended type, or one rejected by
`match`), it sets `ESP_EXT_PART_LIST_FLAG_LOSSY` on the list. When that flag is
**unset**, every partition on the source medium was captured, so regenerating an MBR
from the list is functionally equivalent to the source (ignoring cosmetic differences
such as CHS values or the disk signature).
The parsed table fully defines the list, so `esp_mbr_parse` requires an empty list and
returns `ESP_ERR_INVALID_STATE` otherwise. Call `esp_ext_part_list_deinit()` before
parsing into a list you have already used.
## Alignment and layout validation (MBR generation)
When generating an MBR (`esp_mbr_generate` / `esp_mbr_bdl_write`), the
behavior is controlled through `esp_mbr_generate_extra_args_t` (a zero-initialized
struct selects all defaults):
The sector size used for all byte<->sector math comes from the partition list's
`sector_size` field (set by `esp_mbr_parse`, or assigned directly on a freshly
built list, e.g. `part_list.sector_size = ESP_EXT_PART_SECTOR_SIZE_4KiB;`). It can
be overridden per call via `extra_args.sector_size`; if neither is set it defaults
to 512 B.
- `total_size`: when non-zero, partitions that extend past this many bytes are
rejected. The block-device write helper auto-fills this from the device geometry
when left `0`.
- `alignment`: partition start alignment. `ESP_EXT_PART_ALIGN_AUTO` (the value a
zero-initialized struct selects) resolves to a 1 MiB default;
`ESP_EXT_PART_ALIGN_NONE` leaves start LBAs untouched; `ESP_EXT_PART_ALIGN_4KiB`
and `ESP_EXT_PART_ALIGN_1MiB` request a specific alignment.
- `align_policy`: how partitions with an explicit address are treated. Partitions
placed by the library (`ESP_EXT_PART_FLAG_AUTO_ADDRESS`) always start on an
aligned LBA.
- `ESP_EXT_PART_ALIGN_POLICY_KEEP_ADDRESS` (default): written exactly where given,
aligned or not. Regenerating a parsed table therefore never moves existing
partitions (which would orphan their filesystems). This matches `fdisk`/`parted`,
which align only start values they choose themselves.
- `ESP_EXT_PART_ALIGN_POLICY_REJECT`: return an error if an explicit start is not
aligned.
- `ESP_EXT_PART_ALIGN_POLICY_PRESERVE_END`: align the start up and shrink the size
so the end stays at the requested `address + size`.
- `ESP_EXT_PART_ALIGN_POLICY_KEEP_SIZE`: align the start up and keep the requested
size. Only for new layouts; it moves the partition.
Overlapping partitions are always rejected. A list item with type
`ESP_EXT_PART_TYPE_NONE` is rejected with `ESP_ERR_INVALID_ARG`, one whose type has
no MBR type code with `ESP_ERR_NOT_SUPPORTED`, and one with size 0 (other than
AUTO_ADDRESS + FILL) with `ESP_ERR_INVALID_SIZE`.
## Table slots and partition numbering (MBR)
The four MBR slots do not have to be used consecutively: deleting a partition with
`fdisk` or Windows leaves its slot empty, and the remaining partitions keep their
numbers (e.g. Linux `sdX2`, `sdX4`). `esp_mbr_parse` reads all four slots and records
each partition's 1-based slot number in `esp_ext_part_t.slot`.
By default `esp_mbr_generate` writes the list to consecutive slots from the first one
and ignores `slot`, so a table with empty slots is compacted and its partitions are
renumbered. Set `esp_mbr_generate_extra_args_t.preserve_slots = true` to keep the
numbering: items with `slot` 1..4 are written to that slot, and items with `slot == 0`
(e.g. newly added ones) fill the lowest free slots in list order.
The partition list is the single source of truth for the generated table: all four
MBR entries are either built from a list item or zeroed, so nothing from a
previously loaded MBR survives in the buffer. The bootstrap code area is left
untouched, which makes the read-modify-write cycle (parse an existing MBR, edit the
list, regenerate with `keep_signature`) safe.
## Automatic partition placement (MBR generation)
Instead of computing every start address by hand, a partition can be placed
automatically by setting flags on `esp_ext_part_t.flags`:
- `ESP_EXT_PART_FLAG_AUTO_ADDRESS`: the library computes the partition's start,
placing it right after the previous partition and aligning it. `info.address` is
ignored. The first auto-placed partition lands on the first aligned LBA (after
the MBR sector).
- `ESP_EXT_PART_FLAG_FILL` (with `AUTO_ADDRESS` and `info.size == 0`): the
partition is sized to fill from its computed start to the end of the disk. This
needs a known disk size - either `extra_args->total_size`, or (via
`esp_mbr_bdl_write`) the block device geometry.
The caller's partition list is never modified; addresses/sizes are resolved into
internal copies during generation.
```c
// First partition: auto-placed, fixed size. Last partition: auto-placed, fills the rest.
esp_ext_part_list_item_t p0 = {
.info = {
.size = 16 * 1024 * 1024, // 16 MiB
.type = ESP_EXT_PART_TYPE_FAT32,
.flags = ESP_EXT_PART_FLAG_AUTO_ADDRESS,
}
};
esp_ext_part_list_item_t p1 = {
.info = {
.size = 0, // filled to the end of the disk
.type = ESP_EXT_PART_TYPE_LITTLEFS,
.extra = 4096,
.flags = ESP_EXT_PART_FLAG_AUTO_ADDRESS | ESP_EXT_PART_FLAG_FILL | ESP_EXT_PART_FLAG_EXTRA,
}
};
// esp_ext_part_list_insert(&part_list, &p0/&p1); then esp_mbr_bdl_write(...)
```
### LittleFS block size
MBR has no field for a LittleFS block size, so this library stores it in the CHS-start
bytes of the type `0xC3` entry (`esp_ext_part_t.extra`, 24 bits). Only a power of two
from 128 B to 1 MiB is accepted:
- When parsing, a stored value outside that set is ignored: `extra` stays 0 and
`ESP_EXT_PART_FLAG_EXTRA` is not set. Type `0xC3` is also used by other software
(historically "hidden Linux swap"), whose entries contain real CHS bytes there.
- When generating, `extra == 0` writes no block size (zeros, with a warning), so a
parsed table containing such an entry can still be regenerated. Any other value
outside the set is rejected with `ESP_ERR_INVALID_SIZE` instead of being truncated.
- `esp_ext_part_match_mountable()` reports a LittleFS partition as mountable only if
it has a block size.
## Example code
```c
// loaded_mbr -> Pointer to an array of 512 bytes containing MBR loaded from somewhere (SD card, etc.)
#include <stdio.h>
#include <inttypes.h>
#include "esp_err.h"
#include "esp_ext_part_tables.h"
#include "esp_mbr.h"
esp_err_t err = ESP_OK;
esp_ext_part_list_t part_list = {0};
err = esp_mbr_parse((void*) loaded_mbr, &part_list, NULL); // Parse the array containing MBR and fill `esp_ext_part_list_t part_list` structure
if (err != ESP_OK) {
return err;
}
esp_ext_part_list_item_t* item;
item = esp_ext_part_list_item_head(&part_list); // Get the first partition
for (int i = 0; item != NULL; i++) {
printf("Partition %d:\n\taddress: %" PRIu64 "\n\tsize: %" PRIu64 "\n\ttype: %" PRIu32 "\n\tlabel: %s\n",
i, item->info.address, item->info.size, (uint32_t) item->info.type,
item->info.label ? item->info.label : ""); // item->info.type is of `esp_ext_part_type_known_t` enum type
item = esp_ext_part_list_item_next(item); // Get the next partition
}
// ...
// Clean up when done
esp_ext_part_list_deinit(&part_list);
```
## More Examples
Runnable example projects can be found in [`examples/`](/esp_ext_part_tables/examples/) folder.
## API Reference
See [`esp_ext_part_tables.h`](/esp_ext_part_tables/include/esp_ext_part_tables.h) for the full API documentation.
More advanced API documentation can be found here: [`esp_mbr.h`](/esp_ext_part_tables/include/esp_mbr.h), [`esp_mbr_utils.h`](/esp_ext_part_tables/include/esp_mbr_utils.h).
## Tests
The component is pure partition-table logic, so its tests in
[`test_apps/`](/esp_ext_part_tables/test_apps/) need no storage hardware: they parse and
generate MBRs in memory, and the block-device tests run against a RAM-backed
`esp_blockdev` device. They run in two configurations:
- on the **linux host target** (`pytest -m host_test`), which is what most of CI uses;
- on **target under QEMU** (`pytest -m qemu`, esp32s3 and esp32c3), which additionally
covers 32-bit pointer width and real heap accounting for the leak checks.
Neither configuration exercises a physical SD card, eMMC or USB medium, so the
interaction with a real storage driver is intentionally out of scope for these tests.
69e8b21e1a20c8c1c48f5d5cffceeb0bc262eed0
idf.py add-dependency "espressif/esp_ext_part_tables^1.0.0"