Compare commits

...

7 Commits

Author SHA1 Message Date
Tim Nordell
e3ace9ce98 fixup for 5c87b6864
Duplicate test_relocation.toml into a variant with removeall.
2021-06-13 21:40:36 -05:00
Tim Nordell
40bebef368 fixup for 5c87b6864
Add in lfs_removeall(...) and lfs_rename_with_removeall(...).
2021-06-13 21:39:56 -05:00
Tim Nordell
2612ef130b fixup for 5c87b6864
Move the logic for determining if we should support split directories
into a helper function.  This permits compiled outputs that do not
utilize this to drop this code easily at the linker level.

Additionally, drop the default behavior.
2021-06-13 21:38:37 -05:00
Tim Nordell
a3ea209503 fixup for 5c87b6864
Add in blockers for the normal POSIX like entry into the removal
and rename code; pending naming of the actual function.
2021-06-13 21:11:10 -05:00
Tim Nordell
45d02e5750 fixup for 5c87b6864
Add in missing global state xor for each sub page
within a folder.
2021-06-13 21:10:25 -05:00
Tim Nordell
5c87b6864f Support removal of entire directories that contain only files
Removing directories containing other directories is problematic from
an orphan perspective, but removing a directory only containing files
is fairly easy to do.  Add logic that checks if a directory only
contains files, and if it does, permit removal of this directory.
2021-06-10 20:12:09 -05:00
Tim Nordell
8c1931ac55 Consolidate some of the logic for removals of folders
This routine checks that the folder is empty before removing and is
found in two places.  We can consolidate this for changes to the logic.
2021-05-12 17:14:15 -05:00
3 changed files with 490 additions and 60 deletions

221
lfs.c
View File

@@ -3190,7 +3190,106 @@ static int lfs_rawstat(lfs_t *lfs, const char *path, struct lfs_info *info) {
}
#ifndef LFS_READONLY
static int lfs_rawremove(lfs_t *lfs, const char *path) {
typedef int (*lfs_dir_prep_helper_t)(lfs_t *, struct lfs_mlist *, lfs_gstate_t *);
static int lfs_dir_prep_remove_nonempty_folders(lfs_t *lfs, struct lfs_mlist *dir,
lfs_gstate_t *tmp_gstate)
{
lfs_gstate_t split_gstate;
uint16_t id = 0;
// Walk tags stored in this directory and check for any directory
// tags. Removal of directories with a directory in them can lead
// to additional orphans in the filesystem, so we return
// LFS_ERR_NOTEMPTY in this case. Otherwise, leave the loaded
// directory for the tail end of the directory split to leave a proper
// view of the filesystem after removal.
while (true) {
if (dir->m.count == id) {
if (!dir->m.split) {
// We have iterated through the folder to the last
// tag.
break;
}
// Before we fetch the next block, update our fetched gstate xor
lfs_dir_getgstate(lfs, &dir->m, &split_gstate);
lfs_gstate_xor(tmp_gstate, &split_gstate);
int err = lfs_dir_fetch(lfs, &dir->m, dir->m.tail);
if (err) {
return err;
}
id = 0;
}
lfs_stag_t tag = lfs_dir_get(lfs, &dir->m, LFS_MKTAG(0x780, 0x3ff, 0),
LFS_MKTAG(LFS_TYPE_NAME, id, 0), NULL);
if (tag < 0) {
return tag;
}
if (lfs_tag_type3(tag) == LFS_TYPE_DIR) {
return LFS_ERR_NOTEMPTY;
}
id += 1;
}
return 0;
}
static int lfs_dir_prep_removal(lfs_t *lfs, struct lfs_mlist *dir,
lfs_mdir_t *newcwd, uint16_t newid, lfs_block_t *pair,
lfs_gstate_t *tmp_gstate, lfs_dir_prep_helper_t helper)
{
lfs_stag_t res = lfs_dir_get(lfs, newcwd, LFS_MKTAG(0x700, 0x3ff, 0),
LFS_MKTAG(LFS_TYPE_STRUCT, newid, 8), pair);
if (res < 0) {
return (int)res;
}
lfs_pair_fromle32(pair);
memset(tmp_gstate, 0, sizeof(*tmp_gstate));
int err = lfs_dir_fetch(lfs, &dir->m, pair);
if (err) {
return err;
}
if (dir->m.count > 0 || dir->m.split) {
// Normal POSIX behavior wouldn't allow a non-empty
// folder to be removed/renamed into in this manner
if (NULL == helper) {
return LFS_ERR_NOTEMPTY;
}
err = helper(lfs, dir, tmp_gstate);
if (err) {
return err;
}
}
// mark fs as orphaned
err = lfs_fs_preporphans(lfs, +1);
if (err) {
return err;
}
// I know it's crazy but yes, dir can be changed by our parent's
// commit (if predecessor is child)
dir->type = 0;
dir->id = 0;
lfs->mlist = dir;
return 0;
}
#endif
#ifndef LFS_READONLY
static int lfs_rawremove(lfs_t *lfs, const char *path, lfs_dir_prep_helper_t helper) {
// deorphan if we haven't yet, needed at most once after poweron
int err = lfs_fs_forceconsistency(lfs);
if (err) {
@@ -3204,37 +3303,16 @@ static int lfs_rawremove(lfs_t *lfs, const char *path) {
}
struct lfs_mlist dir;
lfs_block_t pair[2];
lfs_gstate_t tmp_gstate;
dir.next = lfs->mlist;
if (lfs_tag_type3(tag) == LFS_TYPE_DIR) {
// must be empty before removal
lfs_block_t pair[2];
lfs_stag_t res = lfs_dir_get(lfs, &cwd, LFS_MKTAG(0x700, 0x3ff, 0),
LFS_MKTAG(LFS_TYPE_STRUCT, lfs_tag_id(tag), 8), pair);
if (res < 0) {
return (int)res;
}
lfs_pair_fromle32(pair);
err = lfs_dir_fetch(lfs, &dir.m, pair);
if (err) {
// must be empty before removal to prevent orphans
err = lfs_dir_prep_removal(lfs, &dir, &cwd, lfs_tag_id(tag),
pair, &tmp_gstate, helper);
if (err < 0) {
return err;
}
if (dir.m.count > 0 || dir.m.split) {
return LFS_ERR_NOTEMPTY;
}
// mark fs as orphaned
err = lfs_fs_preporphans(lfs, +1);
if (err) {
return err;
}
// I know it's crazy but yes, dir can be changed by our parent's
// commit (if predecessor is child)
dir.type = 0;
dir.id = 0;
lfs->mlist = &dir;
}
// delete the entry
@@ -3253,11 +3331,15 @@ static int lfs_rawremove(lfs_t *lfs, const char *path) {
return err;
}
err = lfs_fs_pred(lfs, dir.m.pair, &cwd);
err = lfs_fs_pred(lfs, pair, &cwd);
if (err) {
return err;
}
// Merge in gstate from first block splits within the directory;
// lfs_dir_drop will pick up the last gstate entry.
lfs_gstate_xor(&lfs->gdelta, &tmp_gstate);
err = lfs_dir_drop(lfs, &cwd, &dir.m);
if (err) {
return err;
@@ -3269,7 +3351,8 @@ static int lfs_rawremove(lfs_t *lfs, const char *path) {
#endif
#ifndef LFS_READONLY
static int lfs_rawrename(lfs_t *lfs, const char *oldpath, const char *newpath) {
static int lfs_rawrename(lfs_t *lfs, const char *oldpath,
const char *newpath, lfs_dir_prep_helper_t helper) {
// deorphan if we haven't yet, needed at most once after poweron
int err = lfs_fs_forceconsistency(lfs);
if (err) {
@@ -3297,6 +3380,8 @@ static int lfs_rawrename(lfs_t *lfs, const char *oldpath, const char *newpath) {
uint16_t newoldid = lfs_tag_id(oldtag);
struct lfs_mlist prevdir;
lfs_block_t dir_pair[2];
lfs_gstate_t tmp_gstate;
prevdir.next = lfs->mlist;
if (prevtag == LFS_ERR_NOENT) {
// check that name fits
@@ -3317,36 +3402,12 @@ static int lfs_rawrename(lfs_t *lfs, const char *oldpath, const char *newpath) {
// we're renaming to ourselves??
return 0;
} else if (lfs_tag_type3(prevtag) == LFS_TYPE_DIR) {
// must be empty before removal
lfs_block_t prevpair[2];
lfs_stag_t res = lfs_dir_get(lfs, &newcwd, LFS_MKTAG(0x700, 0x3ff, 0),
LFS_MKTAG(LFS_TYPE_STRUCT, newid, 8), prevpair);
if (res < 0) {
return (int)res;
}
lfs_pair_fromle32(prevpair);
// must be empty before removal
err = lfs_dir_fetch(lfs, &prevdir.m, prevpair);
// must be empty before removal to prevent orphans
err = lfs_dir_prep_removal(lfs, &prevdir, &newcwd, newid,
dir_pair, &tmp_gstate, helper);
if (err) {
return err;
}
if (prevdir.m.count > 0 || prevdir.m.split) {
return LFS_ERR_NOTEMPTY;
}
// mark fs as orphaned
err = lfs_fs_preporphans(lfs, +1);
if (err) {
return err;
}
// I know it's crazy but yes, dir can be changed by our parent's
// commit (if predecessor is child)
prevdir.type = 0;
prevdir.id = 0;
lfs->mlist = &prevdir;
}
if (!samepair) {
@@ -3388,11 +3449,15 @@ static int lfs_rawrename(lfs_t *lfs, const char *oldpath, const char *newpath) {
return err;
}
err = lfs_fs_pred(lfs, prevdir.m.pair, &newcwd);
err = lfs_fs_pred(lfs, dir_pair, &newcwd);
if (err) {
return err;
}
// Merge in gstate from first split blocks in the directory;
// lfs_dir_drop will pick up the other gstate entries.
lfs_gstate_xor(&lfs->gdelta, &tmp_gstate);
err = lfs_dir_drop(lfs, &newcwd, &prevdir.m);
if (err) {
return err;
@@ -4994,7 +5059,7 @@ int lfs_remove(lfs_t *lfs, const char *path) {
}
LFS_TRACE("lfs_remove(%p, \"%s\")", (void*)lfs, path);
err = lfs_rawremove(lfs, path);
err = lfs_rawremove(lfs, path, NULL);
LFS_TRACE("lfs_remove -> %d", err);
LFS_UNLOCK(lfs->cfg);
@@ -5002,6 +5067,24 @@ int lfs_remove(lfs_t *lfs, const char *path) {
}
#endif
#ifndef LFS_READONLY
int lfs_removeall(lfs_t *lfs, const char *path) {
int err = LFS_LOCK(lfs->cfg);
if (err) {
return err;
}
LFS_TRACE("lfs_removeall(%p, \"%s\")", (void*)lfs, path);
// Note: We pass in a helper pointer here so that this extra
// logic can be dropped if it is never referenced
err = lfs_rawremove(lfs, path, lfs_dir_prep_remove_nonempty_folders);
LFS_TRACE("lfs_removeall -> %d", err);
LFS_UNLOCK(lfs->cfg);
return err;
}
#endif
#ifndef LFS_READONLY
int lfs_rename(lfs_t *lfs, const char *oldpath, const char *newpath) {
int err = LFS_LOCK(lfs->cfg);
@@ -5010,7 +5093,25 @@ int lfs_rename(lfs_t *lfs, const char *oldpath, const char *newpath) {
}
LFS_TRACE("lfs_rename(%p, \"%s\", \"%s\")", (void*)lfs, oldpath, newpath);
err = lfs_rawrename(lfs, oldpath, newpath);
err = lfs_rawrename(lfs, oldpath, newpath, NULL);
LFS_TRACE("lfs_rename -> %d", err);
LFS_UNLOCK(lfs->cfg);
return err;
}
#endif
#ifndef LFS_READONLY
int lfs_rename_with_removeall(lfs_t *lfs, const char *oldpath, const char *newpath) {
int err = LFS_LOCK(lfs->cfg);
if (err) {
return err;
}
LFS_TRACE("lfs_rename(%p, \"%s\", \"%s\")", (void*)lfs, oldpath, newpath);
// Note: We pass in a helper pointer here so that this extra
// logic can be dropped if it is never referenced
err = lfs_rawrename(lfs, oldpath, newpath, lfs_dir_prep_remove_nonempty_folders);
LFS_TRACE("lfs_rename -> %d", err);
LFS_UNLOCK(lfs->cfg);

24
lfs.h
View File

@@ -458,6 +458,17 @@ int lfs_unmount(lfs_t *lfs);
int lfs_remove(lfs_t *lfs, const char *path);
#endif
#ifndef LFS_READONLY
// Removes a file or directory
//
// If removing a directory, the directory must not have
// any directories but it may contain files. This is
// non-POSIX behavior, and thus is a different call
// than lfs_remove(...)
// Returns a negative error code on failure.
int lfs_removeall(lfs_t *lfs, const char *path);
#endif
#ifndef LFS_READONLY
// Rename or move a file or directory
//
@@ -468,6 +479,19 @@ int lfs_remove(lfs_t *lfs, const char *path);
int lfs_rename(lfs_t *lfs, const char *oldpath, const char *newpath);
#endif
#ifndef LFS_READONLY
// Rename or move a file or directory
//
// If the destination exists, it must match the source in type.
// If the destination is a directory, it may not contain
// any directories but it may contain files. This is
// non-POSIX behavior, and thus is a different call
// than lfs_rename(...)
//
// Returns a negative error code on failure.
int lfs_rename_with_removeall(lfs_t *lfs, const char *oldpath, const char *newpath);
#endif
// Find info about a file or directory
//
// Fills out the info structure, based on the specified file or directory.

View File

@@ -0,0 +1,305 @@
# specific corner cases worth explicitly testing for
[[case]] # dangling split dir test
define.ITERATIONS = 20
define.COUNT = 10
define.LFS_BLOCK_CYCLES = [8, 1]
code = '''
lfs_format(&lfs, &cfg) => 0;
// fill up filesystem so only ~16 blocks are left
lfs_mount(&lfs, &cfg) => 0;
lfs_file_open(&lfs, &file, "padding", LFS_O_CREAT | LFS_O_WRONLY) => 0;
memset(buffer, 0, 512);
while (LFS_BLOCK_COUNT - lfs_fs_size(&lfs) > 16) {
lfs_file_write(&lfs, &file, buffer, 512) => 512;
}
lfs_file_close(&lfs, &file) => 0;
// make a child dir to use in bounded space
lfs_mkdir(&lfs, "child") => 0;
lfs_unmount(&lfs) => 0;
lfs_mount(&lfs, &cfg) => 0;
for (int j = 0; j < ITERATIONS; j++) {
for (int i = 0; i < COUNT; i++) {
sprintf(path, "child/test%03d_loooooooooooooooooong_name", i);
lfs_file_open(&lfs, &file, path, LFS_O_CREAT | LFS_O_WRONLY) => 0;
lfs_file_close(&lfs, &file) => 0;
}
lfs_dir_open(&lfs, &dir, "child") => 0;
lfs_dir_read(&lfs, &dir, &info) => 1;
lfs_dir_read(&lfs, &dir, &info) => 1;
for (int i = 0; i < COUNT; i++) {
sprintf(path, "test%03d_loooooooooooooooooong_name", i);
lfs_dir_read(&lfs, &dir, &info) => 1;
strcmp(info.name, path) => 0;
}
lfs_dir_read(&lfs, &dir, &info) => 0;
lfs_dir_close(&lfs, &dir) => 0;
if (j == ITERATIONS-1) {
break;
}
for (int i = 0; i < COUNT; i++) {
sprintf(path, "child/test%03d_loooooooooooooooooong_name", i);
lfs_removeall(&lfs, path) => 0;
}
}
lfs_unmount(&lfs) => 0;
lfs_mount(&lfs, &cfg) => 0;
lfs_dir_open(&lfs, &dir, "child") => 0;
lfs_dir_read(&lfs, &dir, &info) => 1;
lfs_dir_read(&lfs, &dir, &info) => 1;
for (int i = 0; i < COUNT; i++) {
sprintf(path, "test%03d_loooooooooooooooooong_name", i);
lfs_dir_read(&lfs, &dir, &info) => 1;
strcmp(info.name, path) => 0;
}
lfs_dir_read(&lfs, &dir, &info) => 0;
lfs_dir_close(&lfs, &dir) => 0;
for (int i = 0; i < COUNT; i++) {
sprintf(path, "child/test%03d_loooooooooooooooooong_name", i);
lfs_removeall(&lfs, path) => 0;
}
lfs_unmount(&lfs) => 0;
'''
[[case]] # outdated head test
define.ITERATIONS = 20
define.COUNT = 10
define.LFS_BLOCK_CYCLES = [8, 1]
code = '''
lfs_format(&lfs, &cfg) => 0;
// fill up filesystem so only ~16 blocks are left
lfs_mount(&lfs, &cfg) => 0;
lfs_file_open(&lfs, &file, "padding", LFS_O_CREAT | LFS_O_WRONLY) => 0;
memset(buffer, 0, 512);
while (LFS_BLOCK_COUNT - lfs_fs_size(&lfs) > 16) {
lfs_file_write(&lfs, &file, buffer, 512) => 512;
}
lfs_file_close(&lfs, &file) => 0;
// make a child dir to use in bounded space
lfs_mkdir(&lfs, "child") => 0;
lfs_unmount(&lfs) => 0;
lfs_mount(&lfs, &cfg) => 0;
for (int j = 0; j < ITERATIONS; j++) {
for (int i = 0; i < COUNT; i++) {
sprintf(path, "child/test%03d_loooooooooooooooooong_name", i);
lfs_file_open(&lfs, &file, path, LFS_O_CREAT | LFS_O_WRONLY) => 0;
lfs_file_close(&lfs, &file) => 0;
}
lfs_dir_open(&lfs, &dir, "child") => 0;
lfs_dir_read(&lfs, &dir, &info) => 1;
lfs_dir_read(&lfs, &dir, &info) => 1;
for (int i = 0; i < COUNT; i++) {
sprintf(path, "test%03d_loooooooooooooooooong_name", i);
lfs_dir_read(&lfs, &dir, &info) => 1;
strcmp(info.name, path) => 0;
info.size => 0;
sprintf(path, "child/test%03d_loooooooooooooooooong_name", i);
lfs_file_open(&lfs, &file, path, LFS_O_WRONLY) => 0;
lfs_file_write(&lfs, &file, "hi", 2) => 2;
lfs_file_close(&lfs, &file) => 0;
}
lfs_dir_read(&lfs, &dir, &info) => 0;
lfs_dir_rewind(&lfs, &dir) => 0;
lfs_dir_read(&lfs, &dir, &info) => 1;
lfs_dir_read(&lfs, &dir, &info) => 1;
for (int i = 0; i < COUNT; i++) {
sprintf(path, "test%03d_loooooooooooooooooong_name", i);
lfs_dir_read(&lfs, &dir, &info) => 1;
strcmp(info.name, path) => 0;
info.size => 2;
sprintf(path, "child/test%03d_loooooooooooooooooong_name", i);
lfs_file_open(&lfs, &file, path, LFS_O_WRONLY) => 0;
lfs_file_write(&lfs, &file, "hi", 2) => 2;
lfs_file_close(&lfs, &file) => 0;
}
lfs_dir_read(&lfs, &dir, &info) => 0;
lfs_dir_rewind(&lfs, &dir) => 0;
lfs_dir_read(&lfs, &dir, &info) => 1;
lfs_dir_read(&lfs, &dir, &info) => 1;
for (int i = 0; i < COUNT; i++) {
sprintf(path, "test%03d_loooooooooooooooooong_name", i);
lfs_dir_read(&lfs, &dir, &info) => 1;
strcmp(info.name, path) => 0;
info.size => 2;
}
lfs_dir_read(&lfs, &dir, &info) => 0;
lfs_dir_close(&lfs, &dir) => 0;
for (int i = 0; i < COUNT; i++) {
sprintf(path, "child/test%03d_loooooooooooooooooong_name", i);
lfs_removeall(&lfs, path) => 0;
}
}
lfs_unmount(&lfs) => 0;
'''
[[case]] # reentrant testing for relocations, this is the same as the
# orphan testing, except here we also set block_cycles so that
# almost every tree operation needs a relocation
reentrant = true
# TODO fix this case, caused by non-DAG trees
if = '!(DEPTH == 3 && LFS_CACHE_SIZE != 64)'
define = [
{FILES=6, DEPTH=1, CYCLES=20, LFS_BLOCK_CYCLES=1},
{FILES=26, DEPTH=1, CYCLES=20, LFS_BLOCK_CYCLES=1},
{FILES=3, DEPTH=3, CYCLES=20, LFS_BLOCK_CYCLES=1},
]
code = '''
err = lfs_mount(&lfs, &cfg);
if (err) {
lfs_format(&lfs, &cfg) => 0;
lfs_mount(&lfs, &cfg) => 0;
}
srand(1);
const char alpha[] = "abcdefghijklmnopqrstuvwxyz";
for (int i = 0; i < CYCLES; i++) {
// create random path
char full_path[256];
for (int d = 0; d < DEPTH; d++) {
sprintf(&full_path[2*d], "/%c", alpha[rand() % FILES]);
}
// if it does not exist, we create it, else we destroy
int res = lfs_stat(&lfs, full_path, &info);
if (res == LFS_ERR_NOENT) {
// create each directory in turn, ignore if dir already exists
for (int d = 0; d < DEPTH; d++) {
strcpy(path, full_path);
path[2*d+2] = '\0';
err = lfs_mkdir(&lfs, path);
assert(!err || err == LFS_ERR_EXIST);
}
for (int d = 0; d < DEPTH; d++) {
strcpy(path, full_path);
path[2*d+2] = '\0';
lfs_stat(&lfs, path, &info) => 0;
assert(strcmp(info.name, &path[2*d+1]) == 0);
assert(info.type == LFS_TYPE_DIR);
}
} else {
// is valid dir?
assert(strcmp(info.name, &full_path[2*(DEPTH-1)+1]) == 0);
assert(info.type == LFS_TYPE_DIR);
// try to delete path in reverse order, ignore if dir is not empty
for (int d = DEPTH-1; d >= 0; d--) {
strcpy(path, full_path);
path[2*d+2] = '\0';
err = lfs_removeall(&lfs, path);
assert(!err || err == LFS_ERR_NOTEMPTY);
}
lfs_stat(&lfs, full_path, &info) => LFS_ERR_NOENT;
}
}
lfs_unmount(&lfs) => 0;
'''
[[case]] # reentrant testing for relocations, but now with random renames!
reentrant = true
# TODO fix this case, caused by non-DAG trees
if = '!(DEPTH == 3 && LFS_CACHE_SIZE != 64)'
define = [
{FILES=6, DEPTH=1, CYCLES=20, LFS_BLOCK_CYCLES=1},
{FILES=26, DEPTH=1, CYCLES=20, LFS_BLOCK_CYCLES=1},
{FILES=3, DEPTH=3, CYCLES=20, LFS_BLOCK_CYCLES=1},
]
code = '''
err = lfs_mount(&lfs, &cfg);
if (err) {
lfs_format(&lfs, &cfg) => 0;
lfs_mount(&lfs, &cfg) => 0;
}
srand(1);
const char alpha[] = "abcdefghijklmnopqrstuvwxyz";
for (int i = 0; i < CYCLES; i++) {
// create random path
char full_path[256];
for (int d = 0; d < DEPTH; d++) {
sprintf(&full_path[2*d], "/%c", alpha[rand() % FILES]);
}
// if it does not exist, we create it, else we destroy
int res = lfs_stat(&lfs, full_path, &info);
assert(!res || res == LFS_ERR_NOENT);
if (res == LFS_ERR_NOENT) {
// create each directory in turn, ignore if dir already exists
for (int d = 0; d < DEPTH; d++) {
strcpy(path, full_path);
path[2*d+2] = '\0';
err = lfs_mkdir(&lfs, path);
assert(!err || err == LFS_ERR_EXIST);
}
for (int d = 0; d < DEPTH; d++) {
strcpy(path, full_path);
path[2*d+2] = '\0';
lfs_stat(&lfs, path, &info) => 0;
assert(strcmp(info.name, &path[2*d+1]) == 0);
assert(info.type == LFS_TYPE_DIR);
}
} else {
assert(strcmp(info.name, &full_path[2*(DEPTH-1)+1]) == 0);
assert(info.type == LFS_TYPE_DIR);
// create new random path
char new_path[256];
for (int d = 0; d < DEPTH; d++) {
sprintf(&new_path[2*d], "/%c", alpha[rand() % FILES]);
}
// if new path does not exist, rename, otherwise destroy
res = lfs_stat(&lfs, new_path, &info);
assert(!res || res == LFS_ERR_NOENT);
if (res == LFS_ERR_NOENT) {
// stop once some dir is renamed
for (int d = 0; d < DEPTH; d++) {
strcpy(&path[2*d], &full_path[2*d]);
path[2*d+2] = '\0';
strcpy(&path[128+2*d], &new_path[2*d]);
path[128+2*d+2] = '\0';
err = lfs_rename(&lfs, path, path+128);
assert(!err || err == LFS_ERR_NOTEMPTY);
if (!err) {
strcpy(path, path+128);
}
}
for (int d = 0; d < DEPTH; d++) {
strcpy(path, new_path);
path[2*d+2] = '\0';
lfs_stat(&lfs, path, &info) => 0;
assert(strcmp(info.name, &path[2*d+1]) == 0);
assert(info.type == LFS_TYPE_DIR);
}
lfs_stat(&lfs, full_path, &info) => LFS_ERR_NOENT;
} else {
// try to delete path in reverse order,
// ignore if dir is not empty
for (int d = DEPTH-1; d >= 0; d--) {
strcpy(path, full_path);
path[2*d+2] = '\0';
err = lfs_removeall(&lfs, path);
assert(!err || err == LFS_ERR_NOTEMPTY);
}
lfs_stat(&lfs, full_path, &info) => LFS_ERR_NOENT;
}
}
}
lfs_unmount(&lfs) => 0;
'''