Kaynağa Gözat

Add support for shrinking a filesystem

This PR adds a new `lfs_fs_shrink`, which functions similarly to
`lfs_fs_grow`, but supports reducing the block count.

This functions first checks that none of the removed block are in use.
If it is the case, it will fail.
Sosthène Guédon 8 ay önce
ebeveyn
işleme
2105e502c5
4 değiştirilmiş dosya ile 286 ekleme ve 20 silme
  1. 65 20
      lfs.c
  2. 11 0
      lfs.h
  3. 104 0
      tests/test_shrink.toml
  4. 106 0
      tests/test_superblocks.toml

+ 65 - 20
lfs.c

@@ -5233,38 +5233,67 @@ 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_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;
@@ -6485,6 +6514,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);

+ 11 - 0
lfs.h

@@ -772,6 +772,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

+ 104 - 0
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);
+    }
+'''

+ 106 - 0
tests/test_superblocks.toml

@@ -524,6 +524,112 @@ 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 = '''
+    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;
+'''
+
 # test that metadata_max does not cause problems for superblock compaction
 [cases.test_superblocks_metadata_max]
 defines.METADATA_MAX = [