Skip to content

Add support for shrinking a filesystem #1094

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
May 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,22 @@ jobs:
run: |
CFLAGS="$CFLAGS -DLFS_NO_INTRINSICS" make test

test-shrink:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: install
run: |
# need a few things
sudo apt-get update -qq
sudo apt-get install -qq gcc python3 python3-pip
pip3 install toml
gcc --version
python3 --version
- name: test-no-intrinsics
run: |
CFLAGS="$CFLAGS -DLFS_SHRINKNONRELOCATING" make test

# run with all trace options enabled to at least make sure these
# all compile
test-yes-trace:
Expand Down
70 changes: 47 additions & 23 deletions lfs.c
Original file line number Diff line number Diff line change
Expand Up @@ -5233,40 +5233,64 @@ static int lfs_fs_gc_(lfs_t *lfs) {
#endif

#ifndef LFS_READONLY
#ifdef LFS_SHRINKNONRELOCATING
static int lfs_shrink_checkblock(void *data, lfs_block_t block) {
lfs_size_t threshold = *((lfs_size_t*)data);
if (block >= threshold) {
return LFS_ERR_NOTEMPTY;
}
return 0;
}
#endif

static int lfs_fs_grow_(lfs_t *lfs, lfs_size_t block_count) {
// shrinking is not supported
LFS_ASSERT(block_count >= lfs->block_count);
int err;

if (block_count > lfs->block_count) {
lfs->block_count = block_count;
if (block_count == lfs->block_count) {
return 0;
}

// fetch the root
lfs_mdir_t root;
int err = lfs_dir_fetch(lfs, &root, lfs->root);

#ifndef LFS_SHRINKNONRELOCATING
// shrinking is not supported
LFS_ASSERT(block_count >= lfs->block_count);
#endif
#ifdef LFS_SHRINKNONRELOCATING
if (block_count < lfs->block_count) {
err = lfs_fs_traverse_(lfs, lfs_shrink_checkblock, &block_count, true);
if (err) {
return err;
}
}
#endif

// update the superblock
lfs_superblock_t superblock;
lfs_stag_t tag = lfs_dir_get(lfs, &root, LFS_MKTAG(0x7ff, 0x3ff, 0),
LFS_MKTAG(LFS_TYPE_INLINESTRUCT, 0, sizeof(superblock)),
&superblock);
if (tag < 0) {
return tag;
}
lfs_superblock_fromle32(&superblock);
lfs->block_count = block_count;

superblock.block_count = lfs->block_count;
// fetch the root
lfs_mdir_t root;
err = lfs_dir_fetch(lfs, &root, lfs->root);
if (err) {
return err;
}

lfs_superblock_tole32(&superblock);
err = lfs_dir_commit(lfs, &root, LFS_MKATTRS(
{tag, &superblock}));
if (err) {
return err;
}
// update the superblock
lfs_superblock_t superblock;
lfs_stag_t tag = lfs_dir_get(lfs, &root, LFS_MKTAG(0x7ff, 0x3ff, 0),
LFS_MKTAG(LFS_TYPE_INLINESTRUCT, 0, sizeof(superblock)),
&superblock);
if (tag < 0) {
return tag;
}
lfs_superblock_fromle32(&superblock);

superblock.block_count = lfs->block_count;

lfs_superblock_tole32(&superblock);
err = lfs_dir_commit(lfs, &root, LFS_MKATTRS(
{tag, &superblock}));
if (err) {
return err;
}
return 0;
}
#endif
Expand Down
6 changes: 5 additions & 1 deletion lfs.h
Original file line number Diff line number Diff line change
Expand Up @@ -766,7 +766,11 @@ int lfs_fs_gc(lfs_t *lfs);
// Grows the filesystem to a new size, updating the superblock with the new
// block count.
//
// Note: This is irreversible.
// If LFS_SHRINKNONRELOCATING is defined, this function will also accept
// block_counts smaller than the current configuration, after checking
// that none of the blocks that are being removed are in use.
// Note that littlefs's pseudorandom block allocation means that
// this is very unlikely to work in the general case.
//
// Returns a negative error code on failure.
int lfs_fs_grow(lfs_t *lfs, lfs_size_t block_count);
Expand Down
109 changes: 109 additions & 0 deletions tests/test_shrink.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# simple shrink
[cases.test_shrink_simple]
defines.BLOCK_COUNT = [10, 15, 20]
defines.AFTER_BLOCK_COUNT = [5, 10, 15, 19]

if = "AFTER_BLOCK_COUNT <= BLOCK_COUNT"
code = '''
#ifdef LFS_SHRINKNONRELOCATING
lfs_t lfs;
lfs_format(&lfs, cfg) => 0;
lfs_mount(&lfs, cfg) => 0;
lfs_fs_grow(&lfs, AFTER_BLOCK_COUNT) => 0;
lfs_unmount(&lfs);
if (BLOCK_COUNT != AFTER_BLOCK_COUNT) {
lfs_mount(&lfs, cfg) => LFS_ERR_INVAL;
}
lfs_t lfs2 = lfs;
struct lfs_config cfg2 = *cfg;
cfg2.block_count = AFTER_BLOCK_COUNT;
lfs2.cfg = &cfg2;
lfs_mount(&lfs2, &cfg2) => 0;
lfs_unmount(&lfs2) => 0;
#endif
'''

# shrinking full
[cases.test_shrink_full]
defines.BLOCK_COUNT = [10, 15, 20]
defines.AFTER_BLOCK_COUNT = [5, 7, 10, 12, 15, 17, 20]
defines.FILES_COUNT = [7, 8, 9, 10]
if = "AFTER_BLOCK_COUNT <= BLOCK_COUNT && FILES_COUNT + 2 < BLOCK_COUNT"
code = '''
#ifdef LFS_SHRINKNONRELOCATING
lfs_t lfs;
lfs_format(&lfs, cfg) => 0;
// create FILES_COUNT files of BLOCK_SIZE - 50 bytes (to avoid inlining)
lfs_mount(&lfs, cfg) => 0;
for (int i = 0; i < FILES_COUNT + 1; i++) {
lfs_file_t file;
char path[1024];
sprintf(path, "file_%03d", i);
lfs_file_open(&lfs, &file, path,
LFS_O_WRONLY | LFS_O_CREAT | LFS_O_EXCL) => 0;
char wbuffer[BLOCK_SIZE];
memset(wbuffer, 'b', BLOCK_SIZE);
// Ensure one block is taken per file, but that files are not inlined.
lfs_size_t size = BLOCK_SIZE - 0x40;
sprintf(wbuffer, "Hi %03d", i);
lfs_file_write(&lfs, &file, wbuffer, size) => size;
lfs_file_close(&lfs, &file) => 0;
}

int err = lfs_fs_grow(&lfs, AFTER_BLOCK_COUNT);
if (err == 0) {
for (int i = 0; i < FILES_COUNT + 1; i++) {
lfs_file_t file;
char path[1024];
sprintf(path, "file_%03d", i);
lfs_file_open(&lfs, &file, path,
LFS_O_RDONLY ) => 0;
lfs_size_t size = BLOCK_SIZE - 0x40;
char wbuffer[size];
char wbuffer_ref[size];
// Ensure one block is taken per file, but that files are not inlined.
memset(wbuffer_ref, 'b', size);
sprintf(wbuffer_ref, "Hi %03d", i);
lfs_file_read(&lfs, &file, wbuffer, BLOCK_SIZE) => size;
lfs_file_close(&lfs, &file) => 0;
for (lfs_size_t j = 0; j < size; j++) {
wbuffer[j] => wbuffer_ref[j];
}
}
} else {
assert(err == LFS_ERR_NOTEMPTY);
}

lfs_unmount(&lfs) => 0;
if (err == 0 ) {
if ( AFTER_BLOCK_COUNT != BLOCK_COUNT ) {
lfs_mount(&lfs, cfg) => LFS_ERR_INVAL;
}

lfs_t lfs2 = lfs;
struct lfs_config cfg2 = *cfg;
cfg2.block_count = AFTER_BLOCK_COUNT;
lfs2.cfg = &cfg2;
lfs_mount(&lfs2, &cfg2) => 0;
for (int i = 0; i < FILES_COUNT + 1; i++) {
lfs_file_t file;
char path[1024];
sprintf(path, "file_%03d", i);
lfs_file_open(&lfs2, &file, path,
LFS_O_RDONLY ) => 0;
lfs_size_t size = BLOCK_SIZE - 0x40;
char wbuffer[size];
char wbuffer_ref[size];
// Ensure one block is taken per file, but that files are not inlined.
memset(wbuffer_ref, 'b', size);
sprintf(wbuffer_ref, "Hi %03d", i);
lfs_file_read(&lfs2, &file, wbuffer, BLOCK_SIZE) => size;
lfs_file_close(&lfs2, &file) => 0;
for (lfs_size_t j = 0; j < size; j++) {
wbuffer[j] => wbuffer_ref[j];
}
}
lfs_unmount(&lfs2);
}
#endif
'''
108 changes: 108 additions & 0 deletions tests/test_superblocks.toml
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,114 @@ code = '''
lfs_unmount(&lfs) => 0;
'''


# mount and grow the filesystem
[cases.test_superblocks_shrink]
defines.BLOCK_COUNT = 'ERASE_COUNT'
defines.BLOCK_COUNT_2 = ['ERASE_COUNT/2', 'ERASE_COUNT/4', '2']
defines.KNOWN_BLOCK_COUNT = [true, false]
code = '''
#ifdef LFS_SHRINKNONRELOCATING
lfs_t lfs;
lfs_format(&lfs, cfg) => 0;

if (KNOWN_BLOCK_COUNT) {
cfg->block_count = BLOCK_COUNT;
} else {
cfg->block_count = 0;
}

// mount with block_size < erase_size
lfs_mount(&lfs, cfg) => 0;
struct lfs_fsinfo fsinfo;
lfs_fs_stat(&lfs, &fsinfo) => 0;
assert(fsinfo.block_size == BLOCK_SIZE);
assert(fsinfo.block_count == BLOCK_COUNT);
lfs_unmount(&lfs) => 0;

// same size is a noop
lfs_mount(&lfs, cfg) => 0;
lfs_fs_grow(&lfs, BLOCK_COUNT) => 0;
lfs_fs_stat(&lfs, &fsinfo) => 0;
assert(fsinfo.block_size == BLOCK_SIZE);
assert(fsinfo.block_count == BLOCK_COUNT);
lfs_unmount(&lfs) => 0;

lfs_mount(&lfs, cfg) => 0;
lfs_fs_stat(&lfs, &fsinfo) => 0;
assert(fsinfo.block_size == BLOCK_SIZE);
assert(fsinfo.block_count == BLOCK_COUNT);
lfs_unmount(&lfs) => 0;

// grow to new size
lfs_mount(&lfs, cfg) => 0;
lfs_fs_grow(&lfs, BLOCK_COUNT_2) => 0;
lfs_fs_stat(&lfs, &fsinfo) => 0;
assert(fsinfo.block_size == BLOCK_SIZE);
assert(fsinfo.block_count == BLOCK_COUNT_2);
lfs_unmount(&lfs) => 0;

if (KNOWN_BLOCK_COUNT) {
cfg->block_count = BLOCK_COUNT_2;
} else {
cfg->block_count = 0;
}

lfs_mount(&lfs, cfg) => 0;
lfs_fs_stat(&lfs, &fsinfo) => 0;
assert(fsinfo.block_size == BLOCK_SIZE);
assert(fsinfo.block_count == BLOCK_COUNT_2);
lfs_unmount(&lfs) => 0;

// mounting with the previous size should fail
cfg->block_count = BLOCK_COUNT;
lfs_mount(&lfs, cfg) => LFS_ERR_INVAL;

if (KNOWN_BLOCK_COUNT) {
cfg->block_count = BLOCK_COUNT_2;
} else {
cfg->block_count = 0;
}

// same size is a noop
lfs_mount(&lfs, cfg) => 0;
lfs_fs_grow(&lfs, BLOCK_COUNT_2) => 0;
lfs_fs_stat(&lfs, &fsinfo) => 0;
assert(fsinfo.block_size == BLOCK_SIZE);
assert(fsinfo.block_count == BLOCK_COUNT_2);
lfs_unmount(&lfs) => 0;

lfs_mount(&lfs, cfg) => 0;
lfs_fs_stat(&lfs, &fsinfo) => 0;
assert(fsinfo.block_size == BLOCK_SIZE);
assert(fsinfo.block_count == BLOCK_COUNT_2);
lfs_unmount(&lfs) => 0;

// do some work
lfs_mount(&lfs, cfg) => 0;
lfs_fs_stat(&lfs, &fsinfo) => 0;
assert(fsinfo.block_size == BLOCK_SIZE);
assert(fsinfo.block_count == BLOCK_COUNT_2);
lfs_file_t file;
lfs_file_open(&lfs, &file, "test",
LFS_O_CREAT | LFS_O_EXCL | LFS_O_WRONLY) => 0;
lfs_file_write(&lfs, &file, "hello!", 6) => 6;
lfs_file_close(&lfs, &file) => 0;
lfs_unmount(&lfs) => 0;

lfs_mount(&lfs, cfg) => 0;
lfs_fs_stat(&lfs, &fsinfo) => 0;
assert(fsinfo.block_size == BLOCK_SIZE);
assert(fsinfo.block_count == BLOCK_COUNT_2);
lfs_file_open(&lfs, &file, "test", LFS_O_RDONLY) => 0;
uint8_t buffer[256];
lfs_file_read(&lfs, &file, buffer, sizeof(buffer)) => 6;
lfs_file_close(&lfs, &file) => 0;
assert(memcmp(buffer, "hello!", 6) == 0);
lfs_unmount(&lfs) => 0;
#endif
'''

# test that metadata_max does not cause problems for superblock compaction
[cases.test_superblocks_metadata_max]
defines.METADATA_MAX = [
Expand Down