diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f18c23d4..69f2817f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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: diff --git a/lfs.c b/lfs.c index 05b1ca10..c0b0ba38 100644 --- a/lfs.c +++ b/lfs.c @@ -5237,40 +5237,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) { + int err; + + if (block_count == lfs->block_count) { + return 0; + } + + +#ifndef LFS_SHRINKNONRELOCATING // shrinking is not supported LFS_ASSERT(block_count >= lfs->block_count); - - if (block_count > lfs->block_count) { - lfs->block_count = block_count; - - // fetch the root - lfs_mdir_t root; - int err = lfs_dir_fetch(lfs, &root, lfs->root); - 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})); +#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 + lfs->block_count = block_count; + + // fetch the root + lfs_mdir_t root; + err = lfs_dir_fetch(lfs, &root, lfs->root); + 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 diff --git a/lfs.h b/lfs.h index 45315603..3968b5d7 100644 --- a/lfs.h +++ b/lfs.h @@ -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); diff --git a/tests/test_shrink.toml b/tests/test_shrink.toml new file mode 100644 index 00000000..6efa012c --- /dev/null +++ b/tests/test_shrink.toml @@ -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 +''' diff --git a/tests/test_superblocks.toml b/tests/test_superblocks.toml index 5911c9b0..78050f13 100644 --- a/tests/test_superblocks.toml +++ b/tests/test_superblocks.toml @@ -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 = [