diff --git a/lfs.c b/lfs.c index d35d5d6d..96282175 100644 --- a/lfs.c +++ b/lfs.c @@ -4521,7 +4521,9 @@ static int lfs_mount_(lfs_t *lfs, const struct lfs_config *cfg) { } // this is where we get the block_count from disk if block_count=0 - if (lfs->cfg->block_count + + if ((lfs->cfg->flags & LFS_CFG_DISABLE_BLOCK_COUNT_CHECK) == 0 + && lfs->cfg->block_count && superblock.block_count != lfs->cfg->block_count) { LFS_ERROR("Invalid block count (%"PRIu32" != %"PRIu32")", superblock.block_count, lfs->cfg->block_count); @@ -4529,7 +4531,11 @@ static int lfs_mount_(lfs_t *lfs, const struct lfs_config *cfg) { goto cleanup; } - lfs->block_count = superblock.block_count; + if (lfs->cfg->block_count) { + lfs->block_count = lfs->cfg->block_count; + } else { + lfs->block_count = superblock.block_count; + } if (superblock.block_size != lfs->cfg->block_size) { LFS_ERROR("Invalid block size (%"PRIu32" != %"PRIu32")", @@ -5177,38 +5183,70 @@ static int lfs_fs_gc_(lfs_t *lfs) { #endif #ifndef LFS_READONLY +static int lfs_fs_rewrite_block_count(lfs_t *lfs, lfs_size_t 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})); + if (err) { + return err; + } + return 0; +} + static int lfs_fs_grow_(lfs_t *lfs, lfs_size_t block_count) { // shrinking is not supported LFS_ASSERT(block_count >= lfs->block_count); if (block_count > lfs->block_count) { - lfs->block_count = block_count; + return lfs_fs_rewrite_block_count(lfs, block_count); + } - // fetch the root - lfs_mdir_t root; - int err = lfs_dir_fetch(lfs, &root, lfs->root); - if (err) { - return err; - } + return 0; +} - // 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); +static int lfs_shrink_check_block(void * data, lfs_block_t block) { + lfs_size_t threshold = *((lfs_size_t *) data); + if (block >= threshold) { + return LFS_ERR_NOTEMPTY; + } + return 0; +} - superblock.block_count = lfs->block_count; +static int lfs_fs_shrink_(lfs_t *lfs, lfs_size_t block_count) { + if ( + block_count != lfs->block_count || + (lfs->cfg->flags & LFS_CFG_DISABLE_BLOCK_COUNT_CHECK) == LFS_CFG_DISABLE_BLOCK_COUNT_CHECK + ) { - lfs_superblock_tole32(&superblock); - err = lfs_dir_commit(lfs, &root, LFS_MKATTRS( - {tag, &superblock})); + lfs_block_t threshold = block_count; + + int err = lfs_fs_traverse_(lfs, lfs_shrink_check_block, &threshold, true); if (err) { return err; } + + return lfs_fs_rewrite_block_count(lfs, block_count); } return 0; @@ -6429,6 +6467,22 @@ int lfs_fs_grow(lfs_t *lfs, lfs_size_t block_count) { } #endif +#ifndef LFS_READONLY +int lfs_fs_shrink(lfs_t *lfs, lfs_size_t block_count) { + int err = LFS_LOCK(lfs->cfg); + if (err) { + return err; + } + LFS_TRACE("lfs_fs_shrink(%p, %"PRIu32")", (void*)lfs, block_count); + + err = lfs_fs_shrink_(lfs, block_count); + + LFS_TRACE("lfs_fs_shrink -> %d", err); + LFS_UNLOCK(lfs->cfg); + return err; +} +#endif + #ifdef LFS_MIGRATE int lfs_migrate(lfs_t *lfs, const struct lfs_config *cfg) { int err = LFS_LOCK(cfg); diff --git a/lfs.h b/lfs.h index 84738973..34e4ff31 100644 --- a/lfs.h +++ b/lfs.h @@ -283,6 +283,11 @@ struct lfs_config { // Set to -1 to disable inlined files. lfs_size_t inline_max; + // Configuration flags for the filesystem + // + // See variants of lfs_fs_flags + uint32_t flags; + #ifdef LFS_MULTIVERSION // On-disk version to use when writing in the form of 16-bit major version // + 16-bit minor version. This limiting metadata to what is supported by @@ -292,6 +297,10 @@ struct lfs_config { #endif }; +enum lfs_fs_flags { + LFS_CFG_DISABLE_BLOCK_COUNT_CHECK = 1, // Allow mounting a filesystem with a different block count in the config and the superblock +}; + // File info structure struct lfs_info { // Type of the file, either LFS_TYPE_REG or LFS_TYPE_DIR @@ -772,6 +781,17 @@ int lfs_fs_gc(lfs_t *lfs); int lfs_fs_grow(lfs_t *lfs, lfs_size_t block_count); #endif +#ifndef LFS_READONLY +// Shrinks the filesystem to a new size, updating the superblock with the new +// block count. +// +// Note: This first checks that none of the blocks that are being removed are in use +// and will fail if it is the case +// +// Returns a negative error code on failure. +int lfs_fs_shrink(lfs_t *lfs, lfs_size_t block_count); +#endif + #ifndef LFS_READONLY #ifdef LFS_MIGRATE // Attempts to migrate a previous version of littlefs diff --git a/tests/test_shrink.toml b/tests/test_shrink.toml new file mode 100644 index 00000000..0c317d66 --- /dev/null +++ b/tests/test_shrink.toml @@ -0,0 +1,104 @@ +# 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 = ''' + lfs_t lfs; + lfs_format(&lfs, cfg) => 0; + lfs_mount(&lfs, cfg) => 0; + lfs_fs_shrink(&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; +''' + +# 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 = ''' + 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_shrink(&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); + } +''' diff --git a/tests/test_superblocks.toml b/tests/test_superblocks.toml index e93f02eb..704bd597 100644 --- a/tests/test_superblocks.toml +++ b/tests/test_superblocks.toml @@ -465,7 +465,13 @@ code = ''' lfs_unmount(&lfs) => 0; if (KNOWN_BLOCK_COUNT) { + cfg->block_count = BLOCK_COUNT_2 + 1; + lfs_mount(&lfs, cfg) => LFS_ERR_INVAL; + cfg->flags |= LFS_CFG_DISABLE_BLOCK_COUNT_CHECK; + lfs_mount(&lfs, cfg) => 0; + lfs_unmount(&lfs) => 0; cfg->block_count = BLOCK_COUNT_2; + cfg->flags &= ~LFS_CFG_DISABLE_BLOCK_COUNT_CHECK; } else { cfg->block_count = 0; } @@ -523,3 +529,108 @@ code = ''' assert(memcmp(buffer, "hello!", 6) == 0); 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 = ''' + 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_shrink(&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_shrink(&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_shrink(&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; +'''