From eb6c361dfa6ee2179679c8cf9098fb6dc74c930d Mon Sep 17 00:00:00 2001 From: Christopher Haster Date: Mon, 27 Nov 2023 22:25:53 -0600 Subject: [PATCH] Adopted lazy orphaned mdir drops This ended up being much less of a simplification than I hoped it would. It's still easier/more efficient to revert to a relocation in most cases when dropping in an mdir split, and the small gain from simplifying how drops/commits interact is overshadowed by the code duplication necessary to separate lfsr_mdir_drop out from lfsr_mdir_commit: code stack before: 30952 2528 after: 31280 (+1.1%) 2648 (+4.7%) Still, this does at least simplify the logical corner cases (we don't need to abort commits when droppable anymore), and lfsr_mdir_drop is ultimately necessary for supporting lazy file creation. Also having a fix-orphans step during mount allows other littlefs implementations the option to create orphanned mdirs without compat issues. So this ends up the more flexible approach. It _might_ be worth having both eager mdir drops and an explicit lfsr_mdir_drop for lazy file creation in the future, but I doubt this will end up worth the code duplication... --- Oh right, I forgot to actually describe this change. This trades eager mdir drops: 1. Drop mdirs from the mtree immediately as soon as their weight goes to zero. For lazy mdir drops: 1. Drop mdirs from the mtree in a second commit. 2. Scan and drop orphaned mdirs on the first write after mount. This sounds very similar to the previous "deorphan" scan, which risked an extreme performance cost during mount, but it should be noted this orphan scan only needs to touch every mdir once. This makes it no worse than the overhead of actually mounting the filesystem. We can also keep an eye out for orphaned mdirs when we mount, so no extra scan is needed unless there was an unlucky powerloss. Eager mdir dropping sounds simpler, but thanks to deferred commits introduces some subtle complexity around aborting commits that would drop an mdir to zero. Remember commits are viewable on-disk as soon as a commit completes. In _theory_, lazy mdir drops simplify the logic around committing to mdirs. Though the real kicker is that lazy mdir drops are required for lazy file creation. The current idea for lazy file creation involves tracking mid-less opened-but-not-yet-created files. These files can have bshrubs, so they need space on an mdir somewhere. But they aren't actually created yet, so they don't have an mid. This is fine (though it's probably going to be tricky) as long as we allocate an mid on file sync, but there is always a risk of losing power with mdirs that contain only RAM-backed files. Fortunately, no-mids means no orphaned files, but it does mean orphaned mdirs with no synced contents. Long story short, lazy mdir drops are currently a necessary evil, and logical simplification, that unfortunately comes with some cost. --- lfs.c | 1163 ++++++++++++++++++++++------------------- lfs.h | 4 + tests/test_mtree.toml | 102 ++++ 3 files changed, 729 insertions(+), 540 deletions(-) diff --git a/lfs.c b/lfs.c index 68dd0db9..0d886abf 100644 --- a/lfs.c +++ b/lfs.c @@ -4619,7 +4619,6 @@ static int lfsr_btree_traverse(lfs_t *lfs, const lfsr_btree_t *btree, // restart from the root if (btraversal->rid >= btraversal->branch.weight) { - btraversal->bid += btraversal->branch.weight; btraversal->rid = btraversal->bid; btraversal->branch = *btree; @@ -4675,7 +4674,7 @@ static int lfsr_btree_traverse(lfs_t *lfs, const lfsr_btree_t *btree, // return inner btree nodes if this is the first time we've // seen them if (btraversal->rid == 0) { - binfo->bid = btraversal->bid + (rid__ - btraversal->rid);; + binfo->bid = btraversal->bid + (rid__ - btraversal->rid); binfo->tag = LFSR_TAG_BRANCH; binfo->weight = btraversal->branch.weight; binfo->u.rbyd = btraversal->branch; @@ -4689,6 +4688,7 @@ static int lfsr_btree_traverse(lfs_t *lfs, const lfsr_btree_t *btree, // note the effectively traverses a full leaf without redoing // the btree walk lfsr_bid_t bid__ = btraversal->bid + (rid__ - btraversal->rid); + btraversal->bid = bid__ + 1; btraversal->rid = rid__ + 1; binfo->bid = bid__; @@ -5682,20 +5682,6 @@ static int lfsr_mdir_commit__(lfs_t *lfs, lfsr_mdir_t *mdir, } } - // don't finish the commit if our weight dropped to zero! - // - // If we finish the commit it becomes immediately visibile, but we really - // need to remove this mdir from the mtree. Leave the actual remove up to - // upper layers. - if (rbyd_.weight == 0 - // unless we are an mroot - && !(mdir->mid == -1 || lfsr_mdir_cmp(mdir, &lfs->mroot) == 0)) { - // mark weight as zero, but note! we can not longer read from this mdir - // as our pcache may get clobbered - mdir->rbyd.weight = 0; - return LFS_ERR_NOENT; - } - // append any gstate? if (start_rid == -1) { int err = lfsr_rbyd_appendgdelta(lfs, &rbyd_); @@ -6086,7 +6072,6 @@ static int lfsr_mroot_commit(lfs_t *lfs, FROMMPTR(lfsr_mdir_mptr(&mrootchild_), mrootchild_buf)))); if (err) { LFS_ASSERT(err != LFS_ERR_RANGE); - LFS_ASSERT(err != LFS_ERR_NOENT); return err; } @@ -6154,7 +6139,6 @@ static int lfsr_mroot_commit(lfs_t *lfs, FROMMPTR(lfsr_mdir_mptr(&mrootchild_), mrootchild_buf)))); if (err) { LFS_ASSERT(err != LFS_ERR_RANGE); - LFS_ASSERT(err != LFS_ERR_NOENT); return err; } } @@ -6242,20 +6226,129 @@ static int lfsr_mtree_commit(lfs_t *lfs, return 0; } +// unlike merging btree nodes, mdirs must be explicitly dropped +// +// this is atomic updates any opened mdirs, lfs_t, gstate, etc +// +static int lfsr_mdir_drop(lfs_t *lfs, const lfsr_mdir_t *mdir) { + // mdir should be empty at this point + LFS_ASSERT(mdir->rbyd.weight == 0); + // yeah, you really shouldn't try to drop the mroot + LFS_ASSERT(mdir->mid != -1 && lfsr_mdir_cmp(mdir, &lfs->mroot) != 0); + + LFS_DEBUG("Dropping mdir %"PRId32" " + "0x{%"PRIx32",%"PRIx32"}", + mdir->mid >> lfs->mbits, + mdir->rbyd.blocks[0], mdir->rbyd.blocks[1]); + + // reset gdelta for new commit + lfsr_fs_flushgdelta(lfs); + + // TODO can we avoid this stack allocation somehow? + // a bit hacky, but we need to update any pending grms here + lfsr_grm_t grm_ = lfs->grm; + if (lfsr_grm_hasrm(&grm_)) { + // fix any pending grms + for (int j = 0; j < lfsr_grm_count(&grm_); j++) { + LFS_ASSERT(lfsr_mid_bid(lfs, grm_.rms[j]) + != lfsr_mid_bid(lfs, lfs_smax32(mdir->mid, 0))); + if (grm_.rms[j] > mdir->mid) { + grm_.rms[j] -= lfsr_mweight(lfs); + } + } + + // xor our fix into our gdelta + uint8_t grm_buf[LFSR_GRM_DSIZE]; + int err = lfsr_grm_xor(lfs, lfs->grm_d, + lfsr_data_fromgrm(&grm_, grm_buf)); + if (err) { + return err; + } + + err = lfsr_grm_xor(lfs, lfs->grm_d, + LFSR_DATA_BUF(lfs->grm_g, LFSR_GRM_DSIZE)); + if (err) { + return err; + } + } + + // consume mdir's gstate so we don't lose any info + int err = lfsr_fs_consumegdelta(lfs, mdir); + if (err) { + return err; + } + +// TODO apply this +// // we should never drop a direct mdir, because we always have our +// // root bookmark +// LFS_ASSERT(!lfsr_mtree_ismptr(lfs)); + + // direct mdir? + if (lfsr_mtree_ismptr(lfs)) { + err = lfsr_mroot_commit(lfs, -1, 0, NULL, LFSR_ATTRS( + LFSR_ATTR(-1, + RM(WIDE(STRUCT)), 0, NULL()))); + if (err) { + return err; + } + + lfs->mtree = LFSR_MTREE_NULL(); + + // update our mtree + } else { + err = lfsr_mtree_commit(lfs, LFSR_ATTRS( + LFSR_ATTR(lfsr_mdir_bid(lfs, mdir), + RM, -lfsr_mweight(lfs), NULL()))); + if (err) { + return err; + } + } + + // success? update in-device state, we must not error at this point + + // gstate must have been committed by a lower-level function at this point + LFS_ASSERT(lfsr_grm_iszero(lfs->grm_d)); + + // update gstate + lfs->grm = grm_; + // keep track of the exact encoding on-disk + lfsr_data_fromgrm(&lfs->grm, lfs->grm_g); + + for (int type = LFS_TYPE_REG; type < LFS_TYPE_REG+3; type++) { + for (lfsr_openedmdir_t *opened = lfs->opened[type-LFS_TYPE_REG]; + opened; + opened = opened->next) { + // update mids + if (opened->mdir.mid > mdir->mid) { + opened->mdir.mid -= lfsr_mweight(lfs); + } + + // update directory bookmarks + if (type == LFS_TYPE_DIR) { + lfsr_dir_t *dir = (lfsr_dir_t*)opened; + if (dir->bookmark > mdir->mid) { + dir->bookmark -= lfsr_mweight(lfs); + } + } + } + } + + return 0; +} + // high-level mdir commit // // this is atomic and updates any opened mdirs, lfs_t, gstate, etc // static int lfsr_mdir_commit(lfs_t *lfs, lfsr_mdir_t *mdir, const lfsr_attr_t *attrs, lfs_size_t attr_count) { - LFS_ASSERT(mdir->mid == -1 - || lfsr_mtree_isnull(lfs) - || mdir->rbyd.weight > 0); LFS_ASSERT(lfsr_mdir_rid(lfs, mdir) <= mdir->rbyd.weight); + // reset gdelta for new commit + lfsr_fs_flushgdelta(lfs); + // parse out any pending gstate, these will get automatically xored // with on-disk gdeltas in lower-level functions - lfsr_fs_flushgdelta(lfs); for (lfs_size_t i = 0; i < attr_count; i++) { if (attrs[i].tag == LFSR_TAG_GRM) { // encode to disk @@ -6308,8 +6401,7 @@ static int lfsr_mdir_commit(lfs_t *lfs, lfsr_mdir_t *mdir, if (lfsr_mdir_cmp(&mdir_, &lfs->mroot) == 0) { err = lfsr_mroot_commit(lfs, -1, -1, &split_rid, attrs, attr_count); - if (err && err != LFS_ERR_RANGE - && err != LFS_ERR_NOENT) { + if (err && err != LFS_ERR_RANGE) { return err; } @@ -6323,8 +6415,7 @@ static int lfsr_mdir_commit(lfs_t *lfs, lfsr_mdir_t *mdir, err = lfsr_mdir_commit_(lfs, &mdir_, -1, -1, &split_rid, attrs, attr_count); - if (err && err != LFS_ERR_RANGE - && err != LFS_ERR_NOENT) { + if (err && err != LFS_ERR_RANGE) { return err; } } @@ -6363,7 +6454,7 @@ static int lfsr_mdir_commit(lfs_t *lfs, lfsr_mdir_t *mdir, err = lfsr_mdir_commit__(lfs, &mdir_, 0, split_rid, attrs, attr_count); - if (err && err != LFS_ERR_NOENT) { + if (err) { LFS_ASSERT(err != LFS_ERR_RANGE); return err; } @@ -6382,7 +6473,7 @@ static int lfsr_mdir_commit(lfs_t *lfs, lfsr_mdir_t *mdir, err = lfsr_mdir_commit__(lfs, &msibling_, split_rid, -1, attrs, attr_count); - if (err && err != LFS_ERR_NOENT) { + if (err) { LFS_ASSERT(err != LFS_ERR_RANGE); return err; } @@ -6399,23 +6490,11 @@ static int lfsr_mdir_commit(lfs_t *lfs, lfsr_mdir_t *mdir, mdir_.rbyd.blocks[0], mdir_.rbyd.blocks[1], msibling_.rbyd.blocks[0], msibling_.rbyd.blocks[1]); - // because of defered commits, both children can still be reduced - // to zero, need to catch this here - - // both siblings reduced to zero - if (mdir_.rbyd.weight == 0 && msibling_.rbyd.weight == 0) { - LFS_DEBUG("Dropping mdir %"PRId32" " - "0x{%"PRIx32",%"PRIx32"}", - mdir_.mid >> lfs->mbits, - mdir_.rbyd.blocks[0], mdir_.rbyd.blocks[1]); - LFS_DEBUG("Dropping mdir %"PRId32" " - "0x{%"PRIx32",%"PRIx32"}", - msibling_.mid >> lfs->mbits, - msibling_.rbyd.blocks[0], msibling_.rbyd.blocks[1]); - goto drop; + // because of defered commits, children can be reduced to zero + // when splitting // one sibling reduced to zero - } else if (msibling_.rbyd.weight == 0) { + if (msibling_.rbyd.weight == 0) { LFS_DEBUG("Dropping mdir %"PRId32" " "0x{%"PRIx32",%"PRIx32"}", msibling_.mid >> lfs->mbits, @@ -6522,77 +6601,6 @@ static int lfsr_mdir_commit(lfs_t *lfs, lfsr_mdir_t *mdir, } } - // mdir reduced to zero? need to drop? - } else if (err == LFS_ERR_NOENT) { - LFS_DEBUG("Dropping mdir %"PRId32" " - "0x{%"PRIx32",%"PRIx32"}", - mdir->mid >> lfs->mbits, - mdir->rbyd.blocks[0], mdir->rbyd.blocks[1]); - - // consume gstate so we don't lose any info - err = lfsr_fs_consumegdelta(lfs, mdir); - if (err) { - return err; - } - - drop:; - mdelta = -lfsr_mweight(lfs); - - // fix any pending grms - for (lfs_size_t i = 0; i < attr_count; i++) { - if (attrs[i].tag == LFSR_TAG_GRM) { - // Assuming we already xored our gdelta with the grm, we first - // need to xor the grm out of the gdelta. We can't just zero - // the gdelta because we may have picked up extra gdelta from - // split/dropped mdirs - // - // gd' = gd xor (grm' xor grm) - // - lfsr_grm_t *grm = (lfsr_grm_t*)attrs[i].data.u.buf.buffer; - uint8_t grm_buf[LFSR_GRM_DSIZE]; - err = lfsr_grm_xor(lfs, lfs->grm_d, - lfsr_data_fromgrm(grm, grm_buf)); - if (err) { - return err; - } - - // fix our grm - for (int j = 0; j < 2; j++) { - if (grm->rms[j] > mdir->mid) { - grm->rms[j] += mdelta; - } - } - - // xor our fix into our gdelta - err = lfsr_grm_xor(lfs, lfs->grm_d, - lfsr_data_fromgrm(grm, grm_buf)); - if (err) { - return err; - } - } - } - - // direct mdir? - if (lfsr_mtree_ismptr(lfs)) { - err = lfsr_mroot_commit(lfs, -1, 0, NULL, LFSR_ATTRS( - LFSR_ATTR(-1, - RM(WIDE(STRUCT)), 0, NULL()))); - if (err) { - return err; - } - - lfs->mtree = LFSR_MTREE_NULL(); - - // update our mtree - } else { - err = lfsr_mtree_commit(lfs, LFSR_ATTRS( - LFSR_ATTR(lfsr_mdir_bid(lfs, &mdir_), - RM, -lfsr_mweight(lfs), NULL()))); - if (err) { - return err; - } - } - // need to relocate? } else if (lfsr_mdir_cmp(mdir, &mdir_) != 0 && lfsr_mdir_cmp(&mdir_, &lfs->mroot) != 0) { @@ -6765,6 +6773,26 @@ static int lfsr_mdir_commit(lfs_t *lfs, lfsr_mdir_t *mdir, mdir->rbyd = mdir_.rbyd; } + // we're not quite done, we want to clean up any mdirs that have been + // reduced to zero + // + // This can error, which probably sounds like it invalidates the previous + // "don't error" comment, but this is technically a second commit. If we + // error at this point, it should be modeled as though we lost power, + // mainly, hasorphans should be set. + // + // We handle drops differently than splits/relocates, since these updates + // become visible as soon as the commit completes. + // + if (lfsr_mdir_cmp(&mdir_, &lfs->mroot) != 0 + && mdir_.rbyd.weight == 0) { + err = lfsr_mdir_drop(lfs, &mdir_); + if (err) { + lfs->hasorphans = true; + return err; + } + } + return 0; } @@ -7025,7 +7053,7 @@ typedef struct lfsr_traversal { uint8_t flags; uint8_t state; union { - // cycle detection state, only valid when traversing mroot anchors + // cycle detection state, only valid when traversing the mroot chain struct { lfsr_mptr_t mptr; lfs_block_t step; @@ -7570,6 +7598,397 @@ static int lfsr_traversal_read(lfs_t *lfs, lfsr_traversal_t *traversal, static int lfs_init(lfs_t *lfs, const struct lfs_config *cfg); static int lfs_deinit(lfs_t *lfs); +static int lfsr_mountmroot(lfs_t *lfs, const lfsr_mdir_t *mroot) { + // has magic string? + lfsr_data_t data; + int err = lfsr_mdir_lookup(lfs, mroot, -1, LFSR_TAG_MAGIC, + NULL, &data); + if (err) { + if (err == LFS_ERR_NOENT) { + LFS_ERROR("No littlefs magic found"); + return LFS_ERR_INVAL; + } + return err; + } + + lfs_scmp_t cmp = lfsr_data_cmp(lfs, &data, "littlefs", 8); + if (cmp < 0) { + return cmp; + } + + // treat corrupted magic as no magic + if (lfs_cmp(cmp) != 0) { + LFS_ERROR("No littlefs magic found"); + return LFS_ERR_INVAL; + } + + // check the disk version + err = lfsr_mdir_lookup(lfs, mroot, -1, LFSR_TAG_VERSION, + NULL, &data); + if (err) { + if (err == LFS_ERR_NOENT) { + LFS_ERROR("No littlefs version found"); + return LFS_ERR_INVAL; + } + return err; + } + + uint32_t major_version; + err = lfsr_data_readleb128(lfs, &data, (int32_t*)&major_version); + if (err && err != LFS_ERR_CORRUPT) { + return err; + } + if (err == LFS_ERR_CORRUPT) { + major_version = -1; + } + + uint32_t minor_version; + err = lfsr_data_readleb128(lfs, &data, (int32_t*)&minor_version); + if (err && err != LFS_ERR_CORRUPT) { + return err; + } + if (err == LFS_ERR_CORRUPT) { + minor_version = -1; + } + + if (major_version != LFS_DISK_VERSION_MAJOR + || minor_version > LFS_DISK_VERSION_MINOR) { + LFS_ERROR("Incompatible version v%"PRId32".%"PRId32 + " (!= v%"PRId32".%"PRId32")", + major_version, + minor_version, + LFS_DISK_VERSION_MAJOR, + LFS_DISK_VERSION_MINOR); + return LFS_ERR_INVAL; + } + + // check for any rflags, we must understand these to read + // the filesystem + err = lfsr_mdir_lookup(lfs, mroot, -1, LFSR_TAG_RFLAGS, + NULL, &data); + if (err && err != LFS_ERR_NOENT) { + return err; + } + + if (err != LFS_ERR_NOENT && lfsr_data_size(&data) > 0) { + LFS_ERROR("Incompatible rflag 0x%s%"PRIx32, + (lfsr_data_size(&data) > 0) ? "?" : "", + 0); + return LFS_ERR_INVAL; + } + + // check for any wflags, we must understand these to write + // the filesystem + err = lfsr_mdir_lookup(lfs, mroot, -1, LFSR_TAG_WFLAGS, + NULL, &data); + if (err && err != LFS_ERR_NOENT) { + return err; + } + + if (err != LFS_ERR_NOENT && lfsr_data_size(&data) > 0) { + LFS_ERROR("Incompatible wflag 0x%s%"PRIx32, + (lfsr_data_size(&data) > 0) ? "?" : "", + 0); + // TODO switch to read-only? + return LFS_ERR_INVAL; + } + + // check for any oflags, these are optional, if we don't + // understand an oflag we can simply clear it + err = lfsr_mdir_lookup(lfs, mroot, -1, LFSR_TAG_OFLAGS, + NULL, &data); + if (err && err != LFS_ERR_NOENT) { + return err; + } + + if (err != LFS_ERR_NOENT && lfsr_data_size(&data) > 0) { + LFS_DEBUG("Found unknown oflag 0x%s%"PRIx32, + (lfsr_data_size(&data) > 0) ? "?" : "", + 0); + // TODO track and clear oflags in mkconsistent? + LFS_ASSERT(false); + } + + // check block size + err = lfsr_mdir_lookup(lfs, mroot, -1, LFSR_TAG_BLOCKSIZE, + NULL, &data); + if (err && err != LFS_ERR_NOENT) { + return err; + } + + uint32_t block_size = 0; + if (err != LFS_ERR_NOENT) { + err = lfsr_data_readleb128(lfs, &data, (int32_t*)&block_size); + if (err && err != LFS_ERR_CORRUPT) { + return err; + } + if (err == LFS_ERR_CORRUPT) { + block_size = -1; + } + } + + if (block_size != lfs->cfg->block_size-1) { + LFS_ERROR("Incompatible block size %"PRId32" (!= %"PRId32")", + block_size+1, + lfs->cfg->block_size); + return LFS_ERR_INVAL; + } + + // check block count + err = lfsr_mdir_lookup(lfs, mroot, -1, LFSR_TAG_BLOCKCOUNT, + NULL, &data); + if (err && err != LFS_ERR_NOENT) { + return err; + } + + uint32_t block_count = 0; + if (err != LFS_ERR_NOENT) { + err = lfsr_data_readleb128(lfs, &data, + (int32_t*)&block_count); + if (err && err != LFS_ERR_CORRUPT) { + return err; + } + if (err == LFS_ERR_CORRUPT) { + block_count = -1; + } + } + + if (block_count != lfs->cfg->block_count-1) { + LFS_ERROR("Incompatible block count %"PRId32" (!= %"PRId32")", + block_count+1, + lfs->cfg->block_count); + return LFS_ERR_INVAL; + } + + // read the name limit + err = lfsr_mdir_lookup(lfs, mroot, -1, LFSR_TAG_NAMELIMIT, + NULL, &data); + if (err) { + if (err == LFS_ERR_NOENT) { + LFS_ERROR("No name limit found"); + return LFS_ERR_INVAL; + } + return err; + } + + uint32_t name_limit; + err = lfsr_data_readleb128(lfs, &data, (int32_t*)&name_limit); + if (err && err != LFS_ERR_CORRUPT) { + return err; + } + if (err == LFS_ERR_CORRUPT) { + name_limit = -1; + } + + if (name_limit > lfs->name_limit) { + LFS_ERROR("Incompatible name limit (%"PRId32" > %"PRId32")", + name_limit, + lfs->name_limit); + return LFS_ERR_INVAL; + } + + lfs->name_limit = name_limit; + + // read the size limit + err = lfsr_mdir_lookup(lfs, mroot, -1, LFSR_TAG_SIZELIMIT, + NULL, &data); + if (err) { + if (err == LFS_ERR_NOENT) { + LFS_ERROR("No size limit found"); + return LFS_ERR_INVAL; + } + return err; + } + + uint32_t size_limit; + err = lfsr_data_readleb128(lfs, &data, (int32_t*)&size_limit); + if (err && err != LFS_ERR_CORRUPT) { + return err; + } + if (err == LFS_ERR_CORRUPT) { + size_limit = -1; + } + + if (size_limit > lfs->size_limit) { + LFS_ERROR("Incompatible size limit (%"PRId32" > %"PRId32")", + size_limit, + lfs->size_limit); + return LFS_ERR_INVAL; + } + + lfs->size_limit = size_limit; + + // check the utag limit + err = lfsr_mdir_lookup(lfs, mroot, -1, LFSR_TAG_UTAGLIMIT, + NULL, &data); + if (err && err != LFS_ERR_NOENT) { + return err; + } + + if (err != LFS_ERR_NOENT) { + uint32_t utag_limit; + err = lfsr_data_readleb128(lfs, &data, (int32_t*)&utag_limit); + if (err && err != LFS_ERR_CORRUPT) { + return err; + } + if (err == LFS_ERR_CORRUPT) { + utag_limit = -1; + } + + // only 7-bit utags are supported + if (utag_limit != 0x7f) { + LFS_ERROR("Incompatible utag limit (%"PRId32" != %"PRId32")", + utag_limit, + 0x7f); + return LFS_ERR_INVAL; + } + } + + // read the uattr limit + err = lfsr_mdir_lookup(lfs, mroot, -1, LFSR_TAG_UATTRLIMIT, + NULL, &data); + if (err) { + if (err == LFS_ERR_NOENT) { + LFS_ERROR("No uattr limit found"); + return LFS_ERR_INVAL; + } + return err; + } + + uint32_t uattr_limit; + err = lfsr_data_readleb128(lfs, &data, (int32_t*)&uattr_limit); + if (err && err != LFS_ERR_CORRUPT) { + return err; + } + if (err == LFS_ERR_CORRUPT) { + uattr_limit = -1; + } + + if (uattr_limit > lfs->uattr_limit) { + LFS_ERROR("Incompatible uattr limit (%"PRId32" > %"PRId32")", + uattr_limit, + lfs->uattr_limit); + return LFS_ERR_INVAL; + } + + lfs->uattr_limit = uattr_limit; + + // check the stag limit + err = lfsr_mdir_lookup(lfs, mroot, -1, LFSR_TAG_STAGLIMIT, + NULL, &data); + if (err && err != LFS_ERR_NOENT) { + return err; + } + + if (err != LFS_ERR_NOENT) { + uint32_t stag_limit; + err = lfsr_data_readleb128(lfs, &data, (int32_t*)&stag_limit); + if (err && err != LFS_ERR_CORRUPT) { + return err; + } + if (err == LFS_ERR_CORRUPT) { + stag_limit = -1; + } + + // only 7-bit stags are supported + if (stag_limit != 0x7f) { + LFS_ERROR("Incompatible stag limit (%"PRId32" != %"PRId32")", + stag_limit, + 0x7f); + return LFS_ERR_INVAL; + } + } + + // read the sattr limit + err = lfsr_mdir_lookup(lfs, mroot, -1, LFSR_TAG_SATTRLIMIT, + NULL, &data); + if (err) { + if (err == LFS_ERR_NOENT) { + LFS_ERROR("No sattr limit found"); + return LFS_ERR_INVAL; + } + return err; + } + + uint32_t sattr_limit; + err = lfsr_data_readleb128(lfs, &data, (int32_t*)&sattr_limit); + if (err && err != LFS_ERR_CORRUPT) { + return err; + } + if (err == LFS_ERR_CORRUPT) { + sattr_limit = -1; + } + + if (sattr_limit > lfs->sattr_limit) { + LFS_ERROR("Incompatible sattr limit (%"PRId32" > %"PRId32")", + sattr_limit, + lfs->sattr_limit); + return LFS_ERR_INVAL; + } + + lfs->sattr_limit = sattr_limit; + + // read the mdir limit + err = lfsr_mdir_lookup(lfs, mroot, -1, LFSR_TAG_MDIRLIMIT, + NULL, &data); + if (err) { + if (err == LFS_ERR_NOENT) { + LFS_ERROR("No mdir limit found"); + return LFS_ERR_INVAL; + } + return err; + } + + uint32_t mdir_limit; + err = lfsr_data_readleb128(lfs, &data, (int32_t*)&mdir_limit); + if (err && err != LFS_ERR_CORRUPT) { + return err; + } + if (err == LFS_ERR_CORRUPT) { + mdir_limit = -1; + } + + // we only support power-of-two-minus-one mdir limits, this is + // unlikely to ever to change since mdir limits are arbitrary + if (lfs_popc(mdir_limit+1) != 1) { + LFS_ERROR("Incompatible mdir limit %"PRId32, + mdir_limit); + return LFS_ERR_INVAL; + } + + lfs->mbits = lfs_nlog2(mdir_limit+1); + + // read the mtree limit + err = lfsr_mdir_lookup(lfs, mroot, -1, LFSR_TAG_MTREELIMIT, + NULL, &data); + if (err) { + if (err == LFS_ERR_NOENT) { + LFS_ERROR("No mtree limit found"); + return LFS_ERR_INVAL; + } + return err; + } + + uint32_t mtree_limit; + err = lfsr_data_readleb128(lfs, &data, (int32_t*)&mtree_limit); + if (err && err != LFS_ERR_CORRUPT) { + return err; + } + if (err == LFS_ERR_CORRUPT) { + mtree_limit = -1; + } + + // TODO should we actually be doing something with mtree_limit? + if (mtree_limit != lfs->size_limit) { + LFS_ERROR("Incompatible mtree limit (%"PRId32" != %"PRId32")", + mtree_limit, + LFS_FILE_MAX); + return LFS_ERR_INVAL; + } + + return 0; +} + static int lfsr_mountinited(lfs_t *lfs) { // zero gdeltas, we'll read these from our mdirs lfsr_fs_flushgdelta(lfs); @@ -7601,428 +8020,33 @@ static int lfsr_mountinited(lfs_t *lfs) { if (tinfo.tag == LFSR_TAG_MDIR) { // found an mroot? if (tinfo.u.mdir.mid == -1) { - // has magic string? - lfsr_data_t data; - err = lfsr_mdir_lookup(lfs, &tinfo.u.mdir, -1, LFSR_TAG_MAGIC, - NULL, &data); + err = lfsr_mountmroot(lfs, &tinfo.u.mdir); if (err) { - if (err == LFS_ERR_NOENT) { - LFS_ERROR("No littlefs magic found"); - return LFS_ERR_INVAL; - } return err; } - lfs_scmp_t cmp = lfsr_data_cmp(lfs, &data, "littlefs", 8); - if (cmp < 0) { - return cmp; - } - - // treat corrupted magic as no magic - if (lfs_cmp(cmp) != 0) { - LFS_ERROR("No littlefs magic found"); - return LFS_ERR_INVAL; - } - - // check the disk version - err = lfsr_mdir_lookup(lfs, &tinfo.u.mdir, - -1, LFSR_TAG_VERSION, - NULL, &data); - if (err) { - if (err == LFS_ERR_NOENT) { - LFS_ERROR("No littlefs version found"); - return LFS_ERR_INVAL; - } - return err; - } - - uint32_t major_version; - err = lfsr_data_readleb128(lfs, &data, - (int32_t*)&major_version); - if (err && err != LFS_ERR_CORRUPT) { - return err; - } - if (err == LFS_ERR_CORRUPT) { - major_version = -1; - } - - uint32_t minor_version; - err = lfsr_data_readleb128(lfs, &data, - (int32_t*)&minor_version); - if (err && err != LFS_ERR_CORRUPT) { - return err; - } - if (err == LFS_ERR_CORRUPT) { - minor_version = -1; - } - - if (major_version != LFS_DISK_VERSION_MAJOR - || minor_version > LFS_DISK_VERSION_MINOR) { - LFS_ERROR("Incompatible version v%"PRId32".%"PRId32 - " (!= v%"PRId32".%"PRId32")", - major_version, - minor_version, - LFS_DISK_VERSION_MAJOR, - LFS_DISK_VERSION_MINOR); - return LFS_ERR_INVAL; - } - - // check for any rflags, we must understand these to read - // the filesystem - err = lfsr_mdir_lookup(lfs, &tinfo.u.mdir, -1, LFSR_TAG_RFLAGS, - NULL, &data); - if (err && err != LFS_ERR_NOENT) { - return err; - } - - if (err != LFS_ERR_NOENT && lfsr_data_size(&data) > 0) { - LFS_ERROR("Incompatible rflag 0x%s%"PRIx32, - (lfsr_data_size(&data) > 0) ? "?" : "", - 0); - return LFS_ERR_INVAL; - } - - // check for any wflags, we must understand these to write - // the filesystem - err = lfsr_mdir_lookup(lfs, &tinfo.u.mdir, -1, LFSR_TAG_WFLAGS, - NULL, &data); - if (err && err != LFS_ERR_NOENT) { - return err; - } - - if (err != LFS_ERR_NOENT && lfsr_data_size(&data) > 0) { - LFS_ERROR("Incompatible wflag 0x%s%"PRIx32, - (lfsr_data_size(&data) > 0) ? "?" : "", - 0); - // TODO switch to read-only? - return LFS_ERR_INVAL; - } - - // check for any oflags, these are optional, if we don't - // understand an oflag we can simply clear it - err = lfsr_mdir_lookup(lfs, &tinfo.u.mdir, -1, LFSR_TAG_OFLAGS, - NULL, &data); - if (err && err != LFS_ERR_NOENT) { - return err; - } - - if (err != LFS_ERR_NOENT && lfsr_data_size(&data) > 0) { - LFS_DEBUG("Found unknown oflag 0x%s%"PRIx32, - (lfsr_data_size(&data) > 0) ? "?" : "", - 0); - // TODO track and clear oflags in mkconsistent? - LFS_ASSERT(false); - } - - // check block size - err = lfsr_mdir_lookup(lfs, &tinfo.u.mdir, - -1, LFSR_TAG_BLOCKSIZE, - NULL, &data); - if (err && err != LFS_ERR_NOENT) { - return err; - } - - uint32_t block_size = 0; - if (err != LFS_ERR_NOENT) { - err = lfsr_data_readleb128(lfs, &data, - (int32_t*)&block_size); - if (err && err != LFS_ERR_CORRUPT) { - return err; - } - if (err == LFS_ERR_CORRUPT) { - block_size = -1; - } - } - - if (block_size != lfs->cfg->block_size-1) { - LFS_ERROR("Incompatible block size %"PRId32" " - "(!= %"PRId32")", - block_size+1, - lfs->cfg->block_size); - return LFS_ERR_INVAL; - } - - // check block count - err = lfsr_mdir_lookup(lfs, &tinfo.u.mdir, - -1, LFSR_TAG_BLOCKCOUNT, - NULL, &data); - if (err && err != LFS_ERR_NOENT) { - return err; - } - - uint32_t block_count = 0; - if (err != LFS_ERR_NOENT) { - err = lfsr_data_readleb128(lfs, &data, - (int32_t*)&block_count); - if (err && err != LFS_ERR_CORRUPT) { - return err; - } - if (err == LFS_ERR_CORRUPT) { - block_count = -1; - } - } - - if (block_count != lfs->cfg->block_count-1) { - LFS_ERROR("Incompatible block count %"PRId32" " - "(!= %"PRId32")", - block_count+1, - lfs->cfg->block_count); - return LFS_ERR_INVAL; - } - - // read the name limit - err = lfsr_mdir_lookup(lfs, &tinfo.u.mdir, - -1, LFSR_TAG_NAMELIMIT, - NULL, &data); - if (err) { - if (err == LFS_ERR_NOENT) { - LFS_ERROR("No name limit found"); - return LFS_ERR_INVAL; - } - return err; - } - - uint32_t name_limit; - err = lfsr_data_readleb128(lfs, &data, (int32_t*)&name_limit); - if (err && err != LFS_ERR_CORRUPT) { - return err; - } - if (err == LFS_ERR_CORRUPT) { - name_limit = -1; - } - - if (name_limit > lfs->name_limit) { - LFS_ERROR("Incompatible name limit " - "(%"PRId32" > %"PRId32")", - name_limit, - lfs->name_limit); - return LFS_ERR_INVAL; - } - - lfs->name_limit = name_limit; - - // read the size limit - err = lfsr_mdir_lookup(lfs, &tinfo.u.mdir, - -1, LFSR_TAG_SIZELIMIT, - NULL, &data); - if (err) { - if (err == LFS_ERR_NOENT) { - LFS_ERROR("No size limit found"); - return LFS_ERR_INVAL; - } - return err; - } - - uint32_t size_limit; - err = lfsr_data_readleb128(lfs, &data, (int32_t*)&size_limit); - if (err && err != LFS_ERR_CORRUPT) { - return err; - } - if (err == LFS_ERR_CORRUPT) { - size_limit = -1; - } - - if (size_limit > lfs->size_limit) { - LFS_ERROR("Incompatible size limit " - "(%"PRId32" > %"PRId32")", - size_limit, - lfs->size_limit); - return LFS_ERR_INVAL; - } - - lfs->size_limit = size_limit; - - // check the utag limit - err = lfsr_mdir_lookup(lfs, &tinfo.u.mdir, - -1, LFSR_TAG_UTAGLIMIT, - NULL, &data); - if (err && err != LFS_ERR_NOENT) { - return err; - } - - if (err != LFS_ERR_NOENT) { - uint32_t utag_limit; - err = lfsr_data_readleb128(lfs, &data, - (int32_t*)&utag_limit); - if (err && err != LFS_ERR_CORRUPT) { - return err; - } - if (err == LFS_ERR_CORRUPT) { - utag_limit = -1; - } - - // only 7-bit utags are supported - if (utag_limit != 0x7f) { - LFS_ERROR("Incompatible utag limit " - "(%"PRId32" != %"PRId32")", - utag_limit, - 0x7f); - return LFS_ERR_INVAL; - } - } - - // read the uattr limit - err = lfsr_mdir_lookup(lfs, &tinfo.u.mdir, - -1, LFSR_TAG_UATTRLIMIT, - NULL, &data); - if (err) { - if (err == LFS_ERR_NOENT) { - LFS_ERROR("No uattr limit found"); - return LFS_ERR_INVAL; - } - return err; - } - - uint32_t uattr_limit; - err = lfsr_data_readleb128(lfs, &data, (int32_t*)&uattr_limit); - if (err && err != LFS_ERR_CORRUPT) { - return err; - } - if (err == LFS_ERR_CORRUPT) { - uattr_limit = -1; - } - - if (uattr_limit > lfs->uattr_limit) { - LFS_ERROR("Incompatible uattr limit " - "(%"PRId32" > %"PRId32")", - uattr_limit, - lfs->uattr_limit); - return LFS_ERR_INVAL; - } - - lfs->uattr_limit = uattr_limit; - - // check the stag limit - err = lfsr_mdir_lookup(lfs, &tinfo.u.mdir, - -1, LFSR_TAG_STAGLIMIT, - NULL, &data); - if (err && err != LFS_ERR_NOENT) { - return err; - } - - if (err != LFS_ERR_NOENT) { - uint32_t stag_limit; - err = lfsr_data_readleb128(lfs, &data, - (int32_t*)&stag_limit); - if (err && err != LFS_ERR_CORRUPT) { - return err; - } - if (err == LFS_ERR_CORRUPT) { - stag_limit = -1; - } - - // only 7-bit stags are supported - if (stag_limit != 0x7f) { - LFS_ERROR("Incompatible stag limit " - "(%"PRId32" != %"PRId32")", - stag_limit, - 0x7f); - return LFS_ERR_INVAL; - } - } - - // read the sattr limit - err = lfsr_mdir_lookup(lfs, &tinfo.u.mdir, - -1, LFSR_TAG_SATTRLIMIT, - NULL, &data); - if (err) { - if (err == LFS_ERR_NOENT) { - LFS_ERROR("No sattr limit found"); - return LFS_ERR_INVAL; - } - return err; - } - - uint32_t sattr_limit; - err = lfsr_data_readleb128(lfs, &data, (int32_t*)&sattr_limit); - if (err && err != LFS_ERR_CORRUPT) { - return err; - } - if (err == LFS_ERR_CORRUPT) { - sattr_limit = -1; - } - - if (sattr_limit > lfs->sattr_limit) { - LFS_ERROR("Incompatible sattr limit " - "(%"PRId32" > %"PRId32")", - sattr_limit, - lfs->sattr_limit); - return LFS_ERR_INVAL; - } - - lfs->sattr_limit = sattr_limit; - - // read the mdir limit - err = lfsr_mdir_lookup(lfs, &tinfo.u.mdir, - -1, LFSR_TAG_MDIRLIMIT, - NULL, &data); - if (err) { - if (err == LFS_ERR_NOENT) { - LFS_ERROR("No mdir limit found"); - return LFS_ERR_INVAL; - } - return err; - } - - uint32_t mdir_limit; - err = lfsr_data_readleb128(lfs, &data, (int32_t*)&mdir_limit); - if (err && err != LFS_ERR_CORRUPT) { - return err; - } - if (err == LFS_ERR_CORRUPT) { - mdir_limit = -1; - } - - // we only support power-of-two-minus-one mdir limits, this is - // unlikely to ever to change since mdir limits are arbitrary - if (lfs_popc(mdir_limit+1) != 1) { - LFS_ERROR("Incompatible mdir limit %"PRId32, - mdir_limit); - return LFS_ERR_INVAL; - } - - lfs->mbits = lfs_nlog2(mdir_limit+1); - - // read the mtree limit - err = lfsr_mdir_lookup(lfs, &tinfo.u.mdir, - -1, LFSR_TAG_MTREELIMIT, - NULL, &data); - if (err) { - if (err == LFS_ERR_NOENT) { - LFS_ERROR("No mtree limit found"); - return LFS_ERR_INVAL; - } - return err; - } - - uint32_t mtree_limit; - err = lfsr_data_readleb128(lfs, &data, (int32_t*)&mtree_limit); - if (err && err != LFS_ERR_CORRUPT) { - return err; - } - if (err == LFS_ERR_CORRUPT) { - mtree_limit = -1; - } - - // TODO should we actually be doing something with mtree_limit? - if (mtree_limit != lfs->size_limit) { - LFS_ERROR("Incompatible mtree limit " - "(%"PRId32" != %"PRId32")", - mtree_limit, - LFS_FILE_MAX); - return LFS_ERR_INVAL; - } - - // keep track of the last mroot we see, this is the "real" mroot + // keep track of the last mroot we see, this is the + // active mroot lfs->mroot = tinfo.u.mdir; } else { - // found a direct mdir? keep track of this as our "mtree" + // found a direct mdir? keep track of this if (lfsr_mtree_isnull(lfs)) { lfs->mtree = LFSR_MTREE_MPTR( *lfsr_mdir_mptr(&tinfo.u.mdir), lfsr_mweight(lfs)); } + + // found an empty non-mroot mdir? this should only happen + // if we lost power + if (tinfo.u.mdir.rbyd.weight == 0) { + LFS_DEBUG("Found orphaned mdir %"PRId32" " + "0x{%"PRIx32",%"PRIx32"}", + tinfo.u.mdir.mid >> lfs->mbits, + tinfo.u.mdir.rbyd.blocks[0], + tinfo.u.mdir.rbyd.blocks[1]); + lfs->hasorphans = true; + } } // collect any gdeltas from this mdir @@ -8033,7 +8057,7 @@ static int lfsr_mountinited(lfs_t *lfs) { // found an mtree inner-node? } else if (tinfo.tag == LFSR_TAG_BRANCH) { - // found the root of the mtree? + // found the root of the mtree? keep track of this if (lfsr_mtree_isnull(lfs)) { lfs->mtree.u.btree = tinfo.u.rbyd; } @@ -8064,6 +8088,7 @@ static int lfsr_mountinited(lfs_t *lfs) { } if (lfsr_grm_hasrm(&lfs->grm)) { + // found pending grms? this should only happen if we lost power if (lfsr_grm_count(&lfs->grm) == 2) { LFS_DEBUG("Found pending grm " "%"PRId32".%"PRId32" %"PRId32".%"PRId32, @@ -8375,35 +8400,91 @@ static int lfsr_fs_fixgrm(lfs_t *lfs) { return 0; } +static int lfsr_fs_fixorphans(lfs_t *lfs) { + // traverse the filesystem and drop any orphaned mdirs + // + // note this never takes longer than lfsr_mount + // + lfsr_traversal_t traversal = LFSR_TRAVERSAL(0); + while (true) { + lfsr_tinfo_t tinfo; + int err = lfsr_traversal_read(lfs, &traversal, &tinfo); + if (err) { + if (err == LFS_ERR_NOENT) { + break; + } + return err; + } + + // found an orphaned mdir? drop + if (tinfo.tag == LFSR_TAG_MDIR + && tinfo.u.mdir.mid != -1 + && tinfo.u.mdir.rbyd.weight == 0) { + err = lfsr_mdir_drop(lfs, &tinfo.u.mdir); + if (err) { + return err; + } + + // TODO should we have a function for this? + // TODO should traversals be "opened" and updated by + // lfsr_mdir_commit/drop? + // + // dropping an orphan changes our mtree, we need to partially + // invalidate out traversal + LFS_ASSERT(traversal.state == LFSR_TRAVERSAL_MDIRBLOCK); + traversal.state = LFSR_TRAVERSAL_MDIRBLOCK; + traversal.u.mtraversal.bid -= lfsr_mweight(lfs); + traversal.u.mtraversal.rid = traversal.u.mtraversal.bid; + traversal.u.mtraversal.branch = lfs->mtree.u.btree; + } + } + + return 0; +} + static int lfsr_fs_preparemutation(lfs_t *lfs) { // checkpoint the allocator lfs_alloc_ack(lfs); // fix pending grms + bool pl = false; if (lfsr_grm_hasrm(&lfs->grm)) { - if (lfsr_grm_count(&lfs->grm) == 2) { - LFS_DEBUG("Fixing pending grm " - "%"PRId32".%"PRId32" %"PRId32".%"PRId32, - lfsr_mid_bid(lfs, lfs->grm.rms[0]) >> lfs->mbits, - lfsr_mid_rid(lfs, lfs->grm.rms[0]), - lfsr_mid_bid(lfs, lfs->grm.rms[1]) >> lfs->mbits, - lfsr_mid_rid(lfs, lfs->grm.rms[1])); - } else if (lfsr_grm_count(&lfs->grm) == 1) { - LFS_DEBUG("Fixing pending grm %"PRId32".%"PRId32, - lfsr_mid_bid(lfs, lfs->grm.rms[0]) >> lfs->mbits, - lfsr_mid_rid(lfs, lfs->grm.rms[0])); - } + LFS_DEBUG("Fixing pending grms..."); + pl = true; int err = lfsr_fs_fixgrm(lfs); if (err) { return err; } - // checkpoint the allocator again since our fixgrm completed some - // work + // checkpoint the allocator again since fixgrm completed + // some work lfs_alloc_ack(lfs); } + // fix orphaned mdirs + // + // this must happen after fixgrm, since dropping mdirs risks outdating + // the grm, fixgrm can also create temporary orphans, but it should + // immediately clean them up + // + if (lfs->hasorphans) { + LFS_DEBUG("Fixing orphaned mdirs..."); + pl = true; + + int err = lfsr_fs_fixorphans(lfs); + if (err) { + return err; + } + + // checkpoint the allocator again since fixorphans completed + // some work + lfs_alloc_ack(lfs); + } + + if (pl) { + LFS_DEBUG("littlefs is now consistent"); + } return 0; } @@ -14049,6 +14130,8 @@ static int lfs_init(lfs_t *lfs, const struct lfs_config *cfg) { // TODO maybe reorganize this function? + lfs->hasorphans = false; + // compute the number of bits we need to reserve for metadata rids // // This is equivalent to the nlog2 of the maximum number of rids we can diff --git a/lfs.h b/lfs.h index 2a052e3b..4ad89b87 100644 --- a/lfs.h +++ b/lfs.h @@ -610,6 +610,10 @@ typedef struct lfs { uint8_t grm_g[LFSR_GRM_DSIZE]; uint8_t grm_d[LFSR_GRM_DSIZE]; + // TODO we should put this flag somewhere, should lfs_t have a general + // purpose flags field? this has been useful for lfsr_file_t + bool hasorphans; + uint8_t mbits; lfsr_mdir_t mroot; lfsr_mtree_t mtree; diff --git a/tests/test_mtree.toml b/tests/test_mtree.toml index b5356437..7ffcb20e 100644 --- a/tests/test_mtree.toml +++ b/tests/test_mtree.toml @@ -4411,3 +4411,105 @@ code = ''' assert(memcmp(&magic[8], "littlefs", 8) == 0); ''' + +## Orphaned mdirs ## + +# orphaned mdirs can happen if we lose power, test we can clean them up +[cases.test_mtree_orphans] +defines.N = 320 +defines.ORPHANS = [1, 2, 3, 4] +defines.SEED = 42 +in = 'lfs.c' +code = ''' + const char *alphas = "abcdefghijklmnopqrstuvwxyz"; + lfs_t lfs; + lfsr_format(&lfs, CFG) => 0; + lfsr_mount(&lfs, CFG) => 0; + lfs_alloc_ack(&lfs); + // remove root bookmark for now + lfsr_mdir_commit(&lfs, &lfs.mroot, LFSR_ATTRS( + LFSR_ATTR(0, RM, -1, NULL()))) => 0; + + // create entries + lfsr_mdir_t mdir; + lfsr_mtree_lookup(&lfs, + lfs_smax32( + lfsr_mtree_weight(&lfs) - lfsr_mweight(&lfs), + 0), + &mdir) => 0; + for (lfs_size_t i = 0; i < N; i++) { + lfsr_mdir_commit(&lfs, &mdir, LFSR_ATTRS( + LFSR_ATTR(mdir.mid, REG, +1, + BUF(&alphas[i % 26], 1)))) => 0; + + uint8_t buffer[4]; + lfsr_mdir_get(&lfs, &mdir, mdir.mid, LFSR_TAG_REG, + buffer, 4) => 1; + assert(memcmp(buffer, &alphas[i % 26], 1) == 0); + + mdir.mid += 1; + } + lfsr_mid_t old_weight = lfsr_mtree_weight(&lfs); + + // this test only works with a full mtree + LFS_ASSERT(lfsr_mtree_isbtree(&lfs)); + + // bypass the mdir logic and create some orphans + uint32_t prng = SEED; + for (lfs_size_t i = 0; i < ORPHANS; i++) { + // note we should never have orphan.mid=0 + lfsr_bid_t bid_ = ((TEST_PRNG(&prng) + % (lfsr_mtree_weight(&lfs)/lfsr_mweight(&lfs))) + 1) + * lfsr_mweight(&lfs); + + // manually allocate/commit an empty mdir, otherwise + // lfsr_mdir_commit automatically cleans up empty mdirs + lfsr_mptr_t mptr; + for (lfs_size_t j = 0; j < 2; j++) { + lfsr_rbyd_t rbyd; + lfsr_rbyd_alloc(&lfs, &rbyd) => 0; + + lfsr_rbyd_commit(&lfs, &rbyd, LFSR_ATTRS( + LFSR_ATTR(0, REG, +1, BUF("a", 1)), + LFSR_ATTR(0, RM, -1, NULL()))) => 0; + mptr.blocks[j] = rbyd.blocks[0]; + } + + // commit orphan to tree + uint8_t mptr_buf[LFSR_MPTR_DSIZE]; + lfsr_mtree_commit(&lfs, LFSR_ATTRS( + LFSR_ATTR(bid_, + MDIR, +lfsr_mweight(&lfs), + FROMMPTR(&mptr, mptr_buf)))) => 0; + } + LFS_ASSERT(lfsr_mtree_weight(&lfs) > old_weight); + + // trigger lfsr_fs_fixorphans + lfs.hasorphans = true; + lfsr_fs_preparemutation(&lfs) => 0; + + // this should have removed all of our orphans + LFS_ASSERT(lfsr_mtree_weight(&lfs) == old_weight); + + // try looking up each entry + lfs_size_t i = 0; + for (lfs_ssize_t mid = 0; + mid < lfs_smax32( + lfsr_mtree_weight(&lfs), + lfsr_mweight(&lfs)); + mid += lfsr_mweight(&lfs)) { + lfsr_mdir_t mdir; + lfsr_mtree_lookup(&lfs, mid, &mdir) => 0; + for (; lfsr_mdir_rid(&lfs, &mdir) < mdir.rbyd.weight; + mdir.mid += 1) { + uint8_t buffer[4]; + lfsr_mdir_get(&lfs, &mdir, mdir.mid, LFSR_TAG_REG, + buffer, 4) => 1; + assert(memcmp(buffer, &alphas[i % 26], 1) == 0); + i += 1; + } + } + assert(i == N); + + lfsr_unmount(&lfs) => 0; +'''