LittleFS Storage Wrappers

CFGPack provides optional LittleFS-based convenience wrappers for embedded systems with flash storage. These functions handle the file open/read/write/close lifecycle while preserving cfgpack’s zero-heap-allocation guarantee.

Unlike the POSIX io_file.h wrappers (which use FILE* operations), the LittleFS wrappers use lfs_file_opencfg() with a caller-provided file cache buffer, making them compatible with LFS_NO_MALLOC builds.

To use these functions, compile with -DCFGPACK_LITTLEFS and link src/io_littlefs.c and the vendored LittleFS sources with your project.

API

#include "cfgpack/io_littlefs.h"

/* Encode context to a LittleFS file using caller scratch buffer (no heap).
 * scratch must be >= cfg->cache_size + encoded size. */
cfgpack_err_t cfgpack_pageout_lfs(const cfgpack_ctx_t *ctx,
                                  lfs_t *lfs,
                                  const char *path,
                                  uint8_t *scratch,
                                  size_t scratch_cap);

/* Decode context from a LittleFS file using caller scratch buffer (no heap).
 * scratch must be >= cfg->cache_size + file size. */
cfgpack_err_t cfgpack_pagein_lfs(cfgpack_ctx_t *ctx,
                                 lfs_t *lfs,
                                 const char *path,
                                 uint8_t *scratch,
                                 size_t scratch_cap);

Parameters:

  • ctx — Initialized cfgpack context.

  • lfs — Mounted LittleFS instance (caller-owned). The caller is responsible for mounting and unmounting.

  • path — File path within the LittleFS filesystem.

  • scratch — Scratch buffer that serves double duty (see layout below).

  • scratch_cap — Total capacity of the scratch buffer in bytes.

Scratch Buffer Layout

The scratch buffer is split internally into two regions:

scratch buffer:
┌─────────────────────────────┬──────────────────────────────────────┐
│  LittleFS file cache        │  serialized config data              │
│  [0 .. cache_size-1]        │  [cache_size .. scratch_cap-1]       │
└─────────────────────────────┴──────────────────────────────────────┘
  • First cache_size bytes: Passed to lfs_file_opencfg() as struct lfs_file_config.buffer. This is the LittleFS file cache, sized to match lfs->cfg->cache_size.

  • Remaining bytes: Used by cfgpack_pageout() / cfgpack_pagein_buf() for the serialized MessagePack data.

Minimum scratch size: cache_size + max(encoded_config_size, stored_file_size).

If scratch_cap <= cache_size, the functions return CFGPACK_ERR_BOUNDS immediately. The cache_size value comes from your LittleFS configuration (lfs->cfg->cache_size), which is typically set to the flash block or page size.

This design avoids calling lfs_malloc(), so the wrappers work in LFS_NO_MALLOC builds without any changes.

Usage Example

#include "cfgpack/cfgpack.h"
#include "cfgpack/io_littlefs.h"

/* Assume lfs_cfg.cache_size == 256 */
#define SCRATCH_SIZE (256 + 1024)  /* cache + room for encoded config */
static uint8_t scratch[SCRATCH_SIZE];

lfs_t lfs;
/* ... mount LittleFS (caller-owned) ... */
lfs_mount(&lfs, &lfs_cfg);

/* Write config to flash */
cfgpack_err_t err = cfgpack_pageout_lfs(&ctx, &lfs, "/config.bin",
                                         scratch, sizeof(scratch));
if (err != CFGPACK_OK) { /* handle error */ }

/* Read config back from flash */
err = cfgpack_pagein_lfs(&ctx, &lfs, "/config.bin",
                          scratch, sizeof(scratch));
if (err != CFGPACK_OK) { /* handle error */ }

lfs_unmount(&lfs);

Composable I/O Pattern

cfgpack_pagein_lfs() wraps cfgpack_pagein_buf() only — it does not wrap cfgpack_pagein_remap(). This means it loads data directly into the current schema without any index remapping or type widening.

For cross-version migration, you need to manually read the raw blob from LittleFS, detect the schema version, and call cfgpack_pagein_remap() yourself:

/* 1. Split scratch the same way the wrappers do internally */
lfs_size_t cache_size = lfs.cfg->cache_size;
uint8_t *file_cache = scratch;
uint8_t *data_buf   = scratch + cache_size;
size_t   data_cap   = sizeof(scratch) - cache_size;

/* 2. Read raw blob from LittleFS */
struct lfs_file_config fcfg = {.buffer = file_cache};
lfs_file_t file;
lfs_file_opencfg(&lfs, &file, "/config.bin", LFS_O_RDONLY, &fcfg);
lfs_ssize_t size = lfs_file_size(&lfs, &file);
lfs_file_read(&lfs, &file, data_buf, size);
lfs_file_close(&lfs, &file);

/* 3. Detect schema version */
char name[64];
cfgpack_peek_name(data_buf, (size_t)size, name, sizeof(name));

/* 4. Apply remap if needed */
if (strcmp(name, "sensor_v1") == 0) {
    cfgpack_pagein_remap(&ctx_v2, data_buf, (size_t)size,
                         v1_to_v2_remap, remap_count);
} else {
    cfgpack_pagein_buf(&ctx_v2, data_buf, (size_t)size);
}

See examples/flash_config/ for a complete working example of this pattern.

Building

src/io_littlefs.c and the vendored LittleFS sources are compiled into libcfgpack.a (the default CFLAGS set -DCFGPACK_LITTLEFS). LittleFS is built with LFS_NO_DEBUG, LFS_NO_WARN and LFS_NO_ERROR so its logging does not pull <stdio.h> into the archive. LFS_NO_MALLOC is deliberately left to the integrator: cfgpack’s wrappers supply every buffer LittleFS needs through the scratch argument, so the choice only affects the application’s own LittleFS calls. If you build the sources yourself rather than linking the archive:

$(CC) -DCFGPACK_LITTLEFS -DCFGPACK_HOSTED \
    -Iinclude -Ithird_party/littlefs \
    myapp.c src/io_littlefs.c \
    third_party/littlefs/lfs.c third_party/littlefs/lfs_util.c \
    -Lbuild/out -lcfgpack -o myapp

For embedded builds, omit -DCFGPACK_HOSTED and adjust the toolchain as needed.

Error Codes

Return

Condition

CFGPACK_OK

Success

CFGPACK_ERR_BOUNDS

scratch_cap <= cache_size (no room for data)

CFGPACK_ERR_ENCODE

Data portion too small for cfgpack_pageout() (pageout only)

CFGPACK_ERR_IO

LittleFS file open, read, write, or size failure

CFGPACK_ERR_CRC

CRC-32C integrity check failed (pagein only)

CFGPACK_ERR_DECODE

Invalid MessagePack payload (pagein only)

Working Example

The examples/flash_config/ example demonstrates a complete LittleFS + LZ4 + schema migration workflow:

  • RAM-backed LittleFS block device — runs on any desktop without real flash hardware

  • LZ4-compressed msgpack binary schemas — built by a pipeline: .map → cfgpack-schema-pack → .msgpack → cfgpack-compress lz4 → .msgpack.lz4

  • Schema migration v1 → v2 using cfgpack_pagein_remap() with all five migration scenarios: keep, widen, move, remove, and add

  • Composable I/O pattern — manual LFS read followed by cfgpack_pagein_remap() for cross-version migration

cd examples/flash_config && make run

Third-Party Library

LittleFS is vendored in third_party/littlefs/ for self-contained builds:

third_party/littlefs/
    lfs.h, lfs.c            # Core filesystem implementation
    lfs_util.h, lfs_util.c  # Utility functions
    LICENSE.md               # BSD-3-Clause license

LittleFS is a little fail-safe filesystem designed for microcontrollers. It provides power-loss resilience, wear leveling, and bounded RAM/ROM usage — making it a natural companion for cfgpack’s zero-allocation architecture.